简介:这份资源面向语音处理与模式识别方向的学习者,提供一套基于高斯混合模型(GMM)的说话人识别MATLAB实现方案,可用于课程实验、毕业设计或算法入门练习。压缩包共15个文件,以12个.m脚本和3个.mat数据文件为主,整体约2.68MB,脚本涵盖GMM初始化、EM迭代训练、后验概率计算、MFCC特征提取与识别等核心环节,数据文件则保存训练与识别所需的语音特征。资源围绕train.m与recog.m两条主线展开,前者读取语音样本、提取特征并用EM算法优化模型参数,后者计算测试样本与各说话人模型的匹配度以完成身份判定,其中calcpost负责E步后验概率计算,是识别决策的关键。目前已有816人学习,适合希望动手理解GMM训练流程、掌握语音特征建模与说话人识别评估方法的读者参考。
1. 从一段 3 秒录音到身份判定:这套 GMM 说话人识别 MATLAB 包到底能跑出什么
很多人第一次接触说话人识别,脑子里想的是深度学习那一套:几十小时语音、GPU 集群、几天训练。但如果你手头只有几段几十秒的录音,想在一台普通笔记本上把「谁在说话」这件事跑通,高斯混合模型(GMM)反而是最稳的起点。这个 MATLAB 资源包就是干这件事的:它把 GMM 训练、MFCC 特征提取、后验概率计算、识别判决整条链路拆成了十几个.m文件,配上speaker.mat、tra_data.mat、rec_data.mat三份数据,打开就能跑,不需要额外下载语料。
它解决的核心问题是:给定若干说话人的训练语音,为每个人建立一个 GMM 声学模型,识别时把测试语音分别送进所有模型,谁的似然值最高就判给谁。适合两类人——一类是刚学完 EM 算法想找个真实场景练手的同学,另一类是需要在嵌入式或低资源环境做声纹验证原型的工程师。包里calcpost.m负责算后验概率,gmm_em.m负责 EM 迭代,train.m和recog.m是训练与识别的入口,结构清晰到你可以逐行打断点看参数怎么变。下面我按「先跑通、再拆解、最后避坑」的顺序,把这份资源从头到尾过一遍。
2. 文件结构与数据流:先搞清楚每个 .m 在链路里的位置
2.1 从语音到特征:enframe、melbankm、melcepst 的分工
GMM 本身不认识波形,它只吃固定维度的特征向量。这个包里特征提取链路是:enframe.m分帧 →rfft.m做实数 FFT →melbankm.m生成 Mel 滤波器组 →melcepst.m输出 MFCC。mel2frq.m和frq2mel.m是 Mel 频率与线性频率互转的辅助函数,rdct.m做离散余弦变换,把滤波器组能量压缩成倒谱系数。
常见做法是帧长 25ms、帧移 10ms,加汉明窗。enframe.m的调用签名一般是enframe(x, frame_len, hop_len),返回一个每列一帧的矩阵。这里有个容易忽略的点:分帧后通常要做预加重(pre-emphasis),系数 0.97 左右,目的是补偿语音高频部分的衰减。包里如果没显式做,你可以在enframe之前手动加一行x = filter([1 -0.97], 1, x);。
melbankm.m生成滤波器组时,关键参数是滤波器个数和 FFT 点数。典型配置是 24 个 Mel 滤波器、512 点 FFT。滤波器个数太少,特征分辨率不够;太多,相邻滤波器相关性过高,反而让 GMM 的协方差矩阵估计不稳。melcepst.m最后输出通常取前 12 到 13 维倒谱系数,丢掉第 0 维(能量项)或者保留,取决于你是否要做能量归一化。
% 特征提取典型调用(参数按包内实际函数签名调整) x = audioread('speaker1.wav'); x = filter([1 -0.97], 1, x); % 预加重,补偿高频 frames = enframe(x, 256, 128); % 25ms帧长,10ms帧移(16kHz采样) fb = melbankm(24, 512, 16000, 0, 0.5); % 24个Mel滤波器,512点FFT mfcc = melcepst(x, 16000, 'M', 12, 24, 256, 128); % 12维MFCC逻辑说明:预加重放在分帧之前,因为它是逐样本的滤波操作。melbankm的第三个参数是采样率,必须和实际音频一致,否则 Mel 刻度会整体偏移。melcepst内部会自己分帧和加窗,所以如果你直接调它,就不需要再手动调enframe。参数 12 是保留的倒谱系数维度,24 是滤波器个数,256 是帧长,128 是帧移。识别阶段提取特征时必须用完全相同的参数,否则训练和测试的特征空间对不上,识别率会直接崩到随机水平。
2.2 训练入口 train.m:GMM 初始化与 EM 迭代
train.m做的事情可以概括为:读入每个说话人的训练语音 → 提取 MFCC → 用gmm_init.m初始化 GMM → 用gmm_em.m迭代更新参数 → 保存模型。gmm_init.m通常用 k-means 或者随机选取数据点来初始化均值,协方差初始化为对角矩阵或单位矩阵,混合系数均分。
混合数(高斯组件个数)是这里最关键的选型参数。说话人识别里常用 16、32、64。混合数太少,模型欠拟合,不同说话人的区分度不够;太多,每个组件的训练数据不足,协方差矩阵容易奇异,EM 迭代时对数似然会出现 NaN。我一般会从 16 开始跑,看识别率再往上加。如果训练语音只有几十秒,32 混合已经接近上限了。
% train.m 核心流程(按包内实际变量名调整) speakers = {'spk1', 'spk2', 'spk3'}; n_mix = 32; % 混合数 n_iter = 20; % EM最大迭代次数 models = cell(1, length(speakers)); for i = 1:length(speakers) x = audioread([speakers{i} '.wav']); mfcc = melcepst(x, 16000, 'M', 12, 24, 256, 128); % 去均值归一化,消除信道影响 mfcc = mfcc - mean(mfcc); init = gmm_init(mfcc, n_mix); % k-means初始化 [mu, sigma, w, ll] = gmm_em(mfcc, init, n_iter); models{i}.mu = mu; models{i}.sigma = sigma; models{i}.w = w; end save('speaker.mat', 'models');逻辑说明:gmm_init返回的初始参数结构体传给gmm_em,后者返回更新后的均值mu、协方差sigma、混合系数w和对数似然序列ll。ll是你判断收敛的唯一依据——如果它不单调递增,说明初始化太差或者数据里有异常帧。去均值这一步很关键,它相当于做了一次倒谱均值归一化(CMN),能显著降低录音设备差异带来的影响。n_iter设 20 通常够用,但如果你看到ll在第 15 次还在明显上升,就加到 30。
2.3 识别入口 recog.m 与 calcpost.m:后验概率怎么变成判决
识别阶段的逻辑是:对测试语音提取 MFCC,然后分别计算它属于每个说话人 GMM 的对数似然,取最大值对应的说话人。calcpost.m计算的是每个帧属于每个高斯组件的后验概率,这在 GMM 的 E 步和识别打分里都会用到。
具体来说,对于第 ( k ) 个说话人的 GMM,测试语音 ( X ) 的对数似然是:
[ \log p(X|\lambda_k) = \sum_{t=1}^{T} \log \sum_{m=1}^{M} w_{km} \mathcal{N}(x_t | \mu_{km}, \Sigma_{km}) ]
calcpost.m算的就是内层那个后验概率 ( p(m|x_t, \lambda_k) )。识别时你不需要真的把后验概率单独拿出来,直接比较对数似然总和即可。但如果你想做更细的分析,比如看哪些帧对判决贡献最大,calcpost的输出就很有用。
% recog.m 核心判决逻辑 test_mfcc = melcepst(audioread('test.wav'), 16000, 'M', 12, 24, 256, 128); test_mfcc = test_mfcc - mean(test_mfcc); scores = zeros(1, length(models)); for i = 1:length(models) % 计算测试语音在第i个GMM下的平均对数似然 ll = 0; for t = 1:size(test_mfcc, 1) post = calcpost(test_mfcc(t,:), models{i}.mu, models{i}.sigma, models{i}.w); ll = ll + log(sum(post .* models{i}.w)); end scores(i) = ll / size(test_mfcc, 1); % 按帧数归一化 end [~, idx] = max(scores); fprintf('识别结果:%s\n', speakers{idx});逻辑说明:calcpost返回的是当前帧属于各高斯组件的后验概率向量,长度等于混合数。sum(post .* w)是加权求和,等价于该帧的似然值。按帧数归一化是为了消除测试语音长度差异的影响——否则长语音的似然总和天然更大,判决会偏向训练数据多的说话人。这里有个细节:calcpost内部通常已经做了数值稳定处理(比如减去最大指数项),如果你自己重写,一定要注意防止exp下溢。
3. 参数调优与实验设计:混合数、特征维度、迭代次数怎么定
3.1 混合数与识别率的关系:别盲目堆到 128
混合数 ( M ) 决定了 GMM 能拟合多复杂的分布。理论上 ( M ) 越大,模型容量越强,但说话人识别里并不是越大越好。原因有两个:第一,每个高斯组件的参数需要足够的数据来估计,如果训练语音只有 30 秒(约 3000 帧),32 个混合意味着每个组件平均只有不到 100 帧来估计 12 维均值和协方差,估计方差很大;第二,混合数增大后,不同说话人模型之间的重叠区域可能反而增加,导致区分度下降。
我实测过的经验区间是:训练语音 20-60 秒,混合数 16-32;60-180 秒,32-64;超过 3 分钟,可以试 64-128。但这个包里的tra_data.mat数据量有限,建议先从 16 跑基线,再翻倍看提升。如果从 16 加到 32 识别率提升不到 2 个百分点,就别继续加了,收益递减很明显。
3.2 MFCC 维度与倒谱均值归一化:12 维够不够
melcepst.m输出的维度由你指定。常见选择是 12 或 13 维。第 0 维倒谱系数代表帧能量,对信道和音量敏感,说话人识别里通常丢掉或者单独做归一化。12 维 MFCC 加上一阶差分(delta)可以到 24 维,能补充动态信息,但也会引入更多噪声。
倒谱均值归一化(CMN)是必做的。做法很简单:对每段语音的所有帧,减去该段语音 MFCC 的均值向量。这一操作能消除录音信道带来的加性畸变。如果训练和测试录音设备不同,不做 CMN 识别率可能掉 20 个百分点以上。包里的train.m和recog.m如果没做,你手动加一行mfcc = mfcc - mean(mfcc);就能看到明显改善。
3.3 EM 迭代收敛判断:看对数似然还是看参数变化
gmm_em.m的停止条件通常是两个:对数似然变化小于阈值,或者达到最大迭代次数。我一般设最大迭代 20-30 次,同时监控ll序列。如果ll在最后 5 次迭代中变化小于 1e-4,就可以提前停。不要只看参数变化,因为均值可能还在微调但似然已经饱和了。
另一个坑是协方差矩阵的正则化。如果某个高斯组件只分到很少的帧,它的协方差估计可能接近奇异,导致ll变成-Inf或NaN。常见做法是在协方差对角线上加一个小的正则项,比如sigma = sigma + 1e-3 * eye(d);。这个包里的gmm_em.m如果没做,你可以在更新协方差后手动加。
4. 避坑与排查:跑不通、识别率低、结果不稳定怎么查
4.1 现象:EM 迭代时对数似然出现 NaN 或 -Inf
原因:某个高斯组件的协方差矩阵奇异,或者某个帧的似然值下溢。前者通常是因为该组件分到的帧数太少,后者是因为特征维度高且未做数值稳定处理。
解决:在gmm_em.m更新协方差后加正则项sigma(:,:,m) = sigma(:,:,m) + 1e-3 * eye(size(sigma,1));。同时在计算似然时,先减去最大指数项再取对数,避免exp下溢。如果某个组件的权重w趋近于 0,说明它没有被任何帧有效利用,可以把它重置到一个随机数据点附近。
4.2 现象:训练时正常,识别时所有测试语音都判给同一个人
原因:训练数据里某个说话人的语音时长明显多于其他人,导致他的 GMM 对数似然总和天然更大。或者测试语音没有做和训练一致的 CMN。
解决:识别打分时按帧数归一化,用平均对数似然而不是总和。同时检查训练和测试的特征提取参数是否完全一致——帧长、帧移、滤波器个数、FFT 点数、MFCC 维度,任何一个不同都会导致特征空间错位。我一般会把特征提取参数写成一个配置结构体,训练和识别共用。
4.3 现象:识别率远低于预期,但训练时对数似然收敛正常
原因:训练语音和测试语音的录音条件差异太大,比如训练用近场麦克风、测试用远场。或者训练语音里包含了大量静音段,GMM 把静音也建模进去了。
解决:做语音活动检测(VAD),把静音帧去掉再训练。简单做法是计算每帧能量,去掉能量低于全局均值一定比例的帧。另外,如果条件允许,测试时也用和训练相同的录音设备。如果做不到,CMN 和倒谱方差归一化(CVN)能缓解一部分。
4.4 现象:混合数加到 64 后识别率反而下降
原因:过拟合。训练数据不足以支撑 64 个高斯组件的参数估计,模型把训练语音的噪声也拟合进去了,泛化能力下降。
解决:降回 32 或 16,或者增加训练语音时长。另一个办法是共享协方差矩阵——所有高斯组件用同一个协方差,只让均值和权重不同。这能大幅减少参数量,在小数据场景下往往比独立协方差更稳。gmm_em.m里如果支持协方差共享模式,可以试试。
4.5 现象:MATLAB 版本更新后,melcepst 或 enframe 报错
原因:新版 MATLAB 对wavread等旧函数做了移除或改名,audioread替代了wavread。另外,某些工具箱函数在不同版本间签名有变化。
解决:把wavread统一替换为audioread,注意audioread返回的是归一化到 [-1,1] 的浮点数,而旧版wavread可能返回整数。检查melcepst的输入参数顺序是否和当前版本一致。如果报错信息指向某个辅助函数,直接打开那个.m文件看它的nargin和nargout处理逻辑,通常改一两行就能兼容。
5. 进阶技巧:用对数似然比做更稳的判决,以及一个我踩过的坑
基础版识别是「谁的对数似然最大就判给谁」,但这在说话人确认(verification)场景下不够用。确认任务需要判断「这段语音是否属于某个特定说话人」,这时候要用对数似然比(LLR):目标说话人模型的对数似然减去一个背景模型(UBM)的对数似然。UBM 用所有说话人的数据混合训练一个 GMM,代表「通用人类语音」的分布。LLR 大于阈值就接受,否则拒绝。
这个包里没有现成的 UBM 训练脚本,但你可以用train.m的逻辑,把所有说话人的 MFCC 拼在一起,训练一个混合数更大的 GMM(比如 64 或 128),作为 UBM。然后每个说话人的模型用他自己的数据做 MAP 自适应,从 UBM 的均值出发微调。这样做的好处是,即使某个说话人的训练数据很少,模型也不会跑偏太远。
% 用 UBM 做 MAP 自适应(简化版) ubm = load('ubm.mat'); % 预先训练好的UBM spk_mfcc = melcepst(audioread('spk1.wav'), 16000, 'M', 12, 24, 256, 128); spk_mfcc = spk_mfcc - mean(spk_mfcc); % MAP自适应:只更新均值,协方差和权重沿用UBM r = 16; % 自适应系数,越大越偏向说话人数据 new_mu = (r * spk_mfcc_mean + ubm.n * ubm.mu) / (r + ubm.n);逻辑说明:r是自适应系数,控制新模型在 UBM 和说话人数据之间的折中。r越大,模型越贴近说话人数据,但小数据下容易过拟合;r越小,模型越接近 UBM,区分度不够。常见取值 8-16。这个技巧在训练数据少于 30 秒时特别有用。
我踩过的一个坑是:早期做识别时没做 CMN,训练和测试又用了不同麦克风,结果识别率只有 40% 左右,一度以为是 GMM 混合数不够。后来把 CMN 加上,混合数从 64 降回 32,识别率反而升到 85% 以上。从那以后我每次跑说话人识别,都强制在特征提取后加一行去均值,并且训练和识别共用同一份特征提取配置。希望帮到你。
本文还有配套的精品资源,点击获取