
简介这套基于高斯混合模型GMM的说话人身份识别仿真资源面向语音处理方向的初学者和毕业设计开发者完整覆盖了从语音特征提取、GMM初始化与EM迭代训练到话者分类识别的全过程。资源包共十六个文件包含十二个M脚本用于倒谱特征计算、模型参数估计与识别主程序、三个MAT数据文件存放训练集和测试语音参数以及一个AVI操作演示视频压缩包仅2.85MB体量紧凑、结构清晰便于按模块查阅。运行环境建议为MATLAB 2021a及以上版本各脚本模块划分清晰便于二次开发与算法调试。目前已有330人学习下载适合作为课程设计、毕业设计或技术预研的参考。通过这份资源读者可深入理解GMM在说话人识别中的建模思路掌握从语音预处理到模型训练、结果输出的完整代码结构并参照录屏快速复现实验。1. 听声辨人为什么说话人识别要用GMM在会议录音里区分是谁在说话在门禁系统里判断语音是否来自授权用户这类任务都属于说话人身份识别。人耳能分辨音色而机器需要把音色转成可计算的向量。高斯混合模型GMM用多个高斯分量的加权和去拟合每个说话人的声学特征分布不需要文本对齐训练和识别都直接高效。这套matlab仿真资源包含了从MFCC特征提取、GMM训练到身份判决的完整代码并附带了操作演示视频。它尤其适合作为说话人识别的入门baseline、课程设计或后续i-vector系统的对比基准。运行环境建议matlab2021a及以上执行时注意直接运行训练和识别两个主脚本不要单独运行子函数。2. MFCC特征提取先搞清楚喂给GMM的是什么2.1 为什么不能拿原始波形让GMM学习语音信号是典型的非平稳信号直接对整段波形做概率建模没有意义。工程上的标准做法是分帧把一段语音切成10到30毫秒的短片段每个片段近似平稳再从中提取频谱特征。说话人识别里最常用的特征是MFCC它的提取链路是预加重→分帧→加窗→FFT→梅尔三角滤波→取对数→DCT。代码包里enframe.m、rfft.m、melbankm.m、melcepst.m、frq2mel.m、mel2frq.m、rdct.m共同完成这条链路。了解每一步在做什么后面调参数才不会盲人摸象。2.2 分帧与加窗enframe.m的帧长和帧移分帧要同时考虑帧长和帧移。帧长大则频率分辨率高但时间分辨率低帧长太小则FFT不稳定。说话人识别里的经验值是帧长25ms、帧移10ms。假如采样率fs16000Hz那么一帧就是400个采样点。enframe.m负责把单通道语音信号切成帧矩阵frameLen floor(0.025 * fs); frameShift floor(0.010 * fs); frames enframe(x, frameLen, frameShift);这段代码把x切成每行一帧的矩阵frames。frameLen是每帧点数frameShift是帧移点数。注意不同版本的enframe对末尾不足一帧的处理方式不一样有的直接丢弃有的补零所以不要用floor((length(x)-frameLen)/frameShift)1去硬套行数。加窗通常在分帧之后进行使用汉明窗把每帧两端压向零减少FFT的频谱泄漏。2.3 从时域到频域rfft.m与对称性加窗之后对每一帧做FFT。语音是实信号FFT结果共轭对称正频率和负频率的信息重复所以rfft.m只计算非负频率部分把400点FFT变成201点幅度谱。这样做的好处是减少后续梅尔滤波器组的乘法计算量。典型调用形式如下w hamming(frameLen, periodic); magSpec rfft(frames .* w);frames .* w将每帧与窗函数相乘rfft按列计算。magSpec的列对应帧行对应频率索引。如果直接使用matlab自带的fft需要取一半再求模rfft把这个过程封装好了。在16kHz采样率下频率分辨率等于fs/NN为FFT点数如果FFT点数等于帧长则每个频率bin约62.5Hz。2.4 梅尔滤波器组与频率刻度映射人对音高的感知不是线性的低频区域比高频区域更敏感。梅尔刻度把线性频率映射到近似人耳感知频率轴。frq2mel.m和mel2frq.m完成这两种刻度的互转。melbankm.m构造一组在梅尔轴上等间隔的三角滤波器作用在幅度谱上提取每个子带的能量bank melbankm(24, frameLen, fs, 0, 0.5, t); melEnergy bank * magSpec;第一个参数是滤波器数量一般取24或40第二个参数是FFT点数必须与rfft输出行数匹配第三个是采样率0和0.5表示归一化频率上下限0.5对应fs/2t表示返回三角滤波器矩阵。melEnergy每一帧得到24个通道的能量值。滤波器数量不是越大越好太大相邻滤波器相关性变强太小则丢说话人的个性频谱细节。2.5 对数压缩与DCTmelcepst.m封装梅尔子带能量通常动态范围很大取对数后更符合人耳响度感知也压缩了动态范围。DCT的作用是把对数能量去相关得到更紧凑的倒谱系数。rdct.m是独立的DCT实现结果与matlab自带dct一致。melcepst.m把分帧、加窗、FFT、梅尔滤波、对数、DCT串成一条命令mfcc melcepst(x, fs, E, 24, 12, frameLen, frameShift);输出mfcc是帧数×13的矩阵前12列是MFCC系数最后一列是对数帧能量。E代表输出能量项如果不需要可以改成N。24是滤波器个数12是倒谱维度。使用melcepst之后就不再需要手动调enframe、melbankm、rdct了但理解底层做什么是改参数的前提。这些特征将直接作为GMM的训练输入。下面的表格列出了特征提取阶段各文件的角色方便后续阅读训练代码时快速对照文件功能关键输入enframe.m分帧语音序列、帧长、帧移rfft.m实信号幅度谱加窗后帧矩阵melbankm.m梅尔三角滤波器组滤波器个数、FFT点数、采样率frq2mel.m / mel2frq.m频率与梅尔值互转线性频率 / 梅尔频率rdct.mDCT变换对数能量序列melcepst.mMFCC顶层封装语音、fs、滤波器个数、倒谱阶数实际项目中我踩过一个很隐蔽的坑所有特征提取函数都要求传入一致的fs但录制的音频可能来自不同设备有的采样率标注是16k实际是44.1k重采样的跑出的MFCC在相同说话人的两次录音上分布差异很大。训练前应该统一重采样到同一个采样率再进melcepst。3. GMM建模与EM训练每个说话人对应一个概率密度3.1 从聚类到软聚类GMM比VQ好在哪里最朴素的说话人识别做法是把特征聚类成若干码本识别时算每个码本的量化误差。但硬划分丢掉了帧属于某个类别的不确定性。GMM更进一步用K个高斯分量的加权和表示特征分布权重描述分量占比均值描述分布中心协方差描述散布形状。参数λ{w_i, μ_i, Σ_i}目标是让模型在训练特征上获得最大似然。单高斯假设特征服从正态分布显然不够语音的发音风格、语速、情绪变化使特征空间呈现多种模式K个分量正好对应这些模式。K越大模型表达能力越强但需要更多训练数据支撑具体怎么选会在第五章展开。3.2 初始化gmm_init.m先给模型一个合理起点EM算法是一个迭代爬山过程初始点不好容易陷入差的局部最优。gmm_init.m的作用是用简单聚类算法为每个分量确定初始参数。最常用的做法是K-means把该说话人的特征帧分成M个簇簇内样本均值作为μ_i簇内样本协方差作为Σ_i簇内样本比例作为w_i。M 8; gmm gmm_init(train_feat, M);train_feat是当前说话人的特征矩阵行是帧列是特征维数。返回的gmm是结构体通常包含weights、means、covs三个字段。weights是1×Mmeans是M×Dcovs用1×M的cell数组存每个cell为D×D协方差矩阵。后面的gmm_em和calcpost都按这个格式取参数。如果某个簇的样本数太少初始化时会把该簇中心重置为整体均值避免训练中出现空分量。3.3 E步calcpost.m计算帧与分量的归属强度EM的E步是计算每一帧属于每个高斯分量的后验概率γ_{t,i}。第t帧x_t对第i个分量的后验正比于w_i N(x_t; μ_i, Σ_i)。calcpost.m负责这一步输入训练特征和当前模型参数输出归属矩阵post calcpost(train_feat, gmm.weights, gmm.means, gmm.covs);post是T×M矩阵T为帧数M为分量数。如果calcpost返回未归一化的分量贡献则每行不需要和为1如果返回严格后验则每一行和为1。训练脚本里会按对应的语义做归一化或直接使用。实现这类计算时为了避免exp下溢常见做法是先算对数高斯密度再用log-sum-exp技巧归一化。如果你自己改写calcpost务必保持数值稳定否则训练到一半会突然出现Inf导致整个模型报废。3.4 M步与迭代控制gmm_em.m更新模型参数M步用E步得到的归属强度重新估计参数权重是归属强度的均值均值是归属强度加权的样本均值协方差是归属强度加权的样本散布。gmm_em.m把E步和M步包在一个循环里直到对数似然增量小于阈值或者达到最大迭代次数maxIter 50; tol 1e-3; gmm gmm_em(train_feat, gmm, maxIter, tol);在每次E步后计算完整对数似然LL当(LL_new - LL_old) / abs(LL_old) tol时认为收敛并提前停止。如果跑满maxIter仍未收敛常见原因是特征维数高、混合度大或初始化不好。M步更新协方差时还有一个隐藏细节如果样本落在某个分量的支撑区域太小协方差会逐渐趋于奇异矩阵。成熟实现会在协方差对角线加一个很小的floor值比如1e-4。gmm_em里没有显式加的话训练后很容易在calcpost阶段出现NaN需要自己补上。K-means与GMM EM的对比能帮助理解为什么选GMM维度K-meansGMM EM归属方式硬划分软划分输出码本中心权重 均值 协方差优化目标量化误差最小对数似然最大对异常帧敏感有协方差建模容忍度略高3.5 训练多个说话人模型与保存每个说话人独立训练一个GMM然后统一保存到speaker.mat。speaker.mat里通常是一个cell数组或结构体数组第i个元素对应第i个说话人的模型。训练循环的示意代码如下for spk 1:numSpeaker feats extractFeatures(spk); % 提取该说话人全部训练语音的MFCC speaker.model{spk} gmm_em(feats, gmm_init(feats, M), 50); end save(speaker.mat, speaker, fs, M);保存时最好连带保存采样率和混合度M因为识别阶段必须使用完全相同的特征提取参数。如果训练时滤波器个数是24识别时却改成32模型维度和特征维度就对不上错误很难排查。4. 训练与识别全流程复现跑通Runme_train和Runme_recog4.1 资源包文件与各自角色压缩包里的文件分布很清晰第一类是与MFCC相关的底层函数第二类是GMM训练相关函数第三类是数据和主脚本。先看下来避免运行时分不清主次文件角色Runme_train.m训练主脚本加载tra_data.mat并生成speaker.matRunme_recog.m识别主脚本加载rec_data.mat和speaker.mattra_data.mat训练语音数据rec_data.mat测试语音数据speaker.mat训练好的GMM模型集合gmm_init.m / gmm_em.m / calcpost.mGMM初始化、EM训练、后验计算melcepst.m / enframe.m / rfft.m / melbankm.m / rdct.m / frq2mel.m / mel2frq.m特征提取操作录像0019.avi从解压到跑出结果的完整操作演示运行顺序是先用Runme_train.m训练再用Runme_recog.m识别。不要直接去运行gmm_init或melcepst这些子函数它们只是被主脚本调用的工具。4.2 训练阶段逻辑Runme_train.m做了什么训练主脚本的基本逻辑是读入tra_data.mat按说话人编号分组对每个人的语音特征做GMM训练最后统一保存。主干代码大致如下变量名以你load之后看到的实际字段为准load(tra_data.mat); % 内含训练语音、采样率、说话人标签 for spk 1:numSpeaker idx find(trainLabel spk); featList []; for k 1:length(idx) cep melcepst(trainAudio{idx(k)}, fs, E, 24, 12); featList [featList; cep]; % 拼接成单一大特征矩阵 end gmm gmm_init(featList, M); gmm gmm_em(featList, gmm, 50); speaker{spk} gmm; end save(speaker.mat, speaker, fs, M);这里每一轮循环处理一个说话人的所有训练音频把每段语音的MFCC纵向拼接得到一个大的特征矩阵再统一训练一个GMM。注意拼接前应该对每段语音单独做端点检测或静音剔除否则长段静音的帧会把模型参数拉偏导致识别时对安静环境过于敏感。如果tra_data.mat里已经存好了特征而非波形则可以跳过melcepst这一步直接使用。4.3 识别阶段逻辑Runme_recog.m如何判决识别脚本加载speaker.mat和rec_data.mat计算测试语音在每个说话人GMM下的得分取最大得分对应的说话人作为结果。经典的打分方式是平均对数似然这样可以消除测试音频长度不同带来的偏差。资源包里的Runme_recog.m主干逻辑类似load(speaker.mat); load(rec_data.mat); rec_feat melcepst(recAudio, fs, E, 24, 12); scores zeros(1, length(speaker)); for s 1:length(speaker) compLh calcpost(rec_feat, speaker{s}.weights, ... speaker{s}.means, speaker{s}.covs); frameLh sum(log(compLh 1e-10), 2); % 对分量求和得到每帧似然 scores(s) mean(frameLh); % 平均对数似然 end [~, result] max(scores); fprintf(识别说话人编号%d\n, result);calcpost的返回值是语音帧在各高斯分量下的贡献值。对每个分量取对数后求和就得到一帧在该GMM下的对数似然再对所有帧取平均使得得分与测试语音长度无关。如果calcpost在包内已经做了归一化这段代码依然能保持相对比较的一致性因为所有说话人模型用的是同一套计算逻辑。最终max拿到的result就是判定的说话人身份。4.4 运行前检查清单与高频坑位第一次跑这套仿真建议按下面顺序操作确认matlab版本是2021a或更高老版本对部分函数支持有差异。把压缩包解压到一个纯英文路径中文路径或空格路径会导致load读取失败。在matlab左侧的当前文件夹窗口切换到工程所在目录注意不是右上角搜索路径。打开Runme_train.m按F5运行等训练过程结束后再打开Runme_recog.m运行。如果控制台报“未定义函数或变量enframe”说明当前文件夹没有切换到位或者解压后子文件夹没有被matlab识别。运行前可以先用命令行执行load(tra_data.mat)查看变量名对照我上面的示意代码做调整。操作录像0019.avi演示了这个过程视频里会看到workspace中出现了speaker变量说明模型已经生成。识别结束后如果打印的说话人编号与测试数据真实标签一致就说明整套流程跑通了。如果结果不对优先检查训练与识别使用的滤波器数量、倒谱维数是否一致以及当前文件夹路径是否带有中文。5. 进阶调参与鲁棒性验证让仿真从“能跑”变成“可靠”5.1 混合分量数M的选择交叉验证替代拍脑袋M是GMM说话人识别里最敏感的超参数。M太小模型欠拟合M太大在有限的训练数据上过拟合。常见做法是遍历一组M值比如2、4、8、16、32在独立验证集上计算平均对数似然选择验证集得分最高且训练集得分没有骤降的M。示例代码如下for M 2:2:32 gmm gmm_init(feat_train, M); gmm gmm_em(feat_train, gmm, 50); compLh calcpost(feat_val, gmm.weights, gmm.means, gmm.covs); llVal mean(sum(log(compLh 1e-10), 2)); scores(M) llVal; end [~, bestM] max(scores);注意交叉验证要保证训练集和验证集来自不同录音而不是同一段语音的前后半段否则会有严重的信息泄漏。如果训练数据只有几十秒M取16到32往往已经足够更大的M会带来计算负担且收益很小。5.2 协方差退化问题给gmm_em做一点加固训练中经常遇到某个高斯分量的协方差矩阵接近奇异导致calcpost里出现Inf或NaN。我一般会在gmm_em的M步末尾加一个对角矩阵下限floorVal 1e-4 * eye(D); for i 1:M gmm.covs{i} gmm.covs{i} floorVal; end这个操作等价于给每个协方差的特征值加一个正下界防止矩阵奇异也避免log(det(sigma))变成负无穷。代价是模型对精细分布的刻画略失真但换来了数值稳定性。如果你发现训练好的模型在识别时得分全是-Inf优先检查这行有没有加。5.3 特征增强倒谱均值减与差分特征ova语音经过不同麦克风或信道MFCC的整体均值会发生偏移而说话人的个性信息更多体现在相对变化里。倒谱均值减是消除这种全局偏移的常用手段mfcc melcepst(x, fs, E, 24, 12); mfcc mfcc - mean(mfcc); % CMS delta [zeros(1, size(mfcc,2)); diff(mfcc)]; feat [mfcc, delta]; % 拼接一阶差分去掉均值后特征对信道更鲁棒加一阶差分可以保留动态信息。不过特征维度从13变成26后GMM的参数数量平方级增长需要相应增大M或改用对角协方差约束来降低训练难度。在资源包里把Runme_train和Runme_recog里的melcepst输出替换成feat并把gmm_init和gmm_em内部对协方差的处理改为对角矩阵即可。5.4 验证实验设计留出法得到可靠识别率要评价这套仿真到底能达到什么水平不能只看一两次识别结果。正确做法是将每个说话人的录音按70%训练、30%测试进行留出法划分保证测试语音和训练语音不是同一句然后统计识别正确率。评估脚本的主干逻辑如下for trial 1:5 trainIdx, testIdx splitByUtterance(spk, 0.7); trainModel trainGMM(trainIdx); pred recognize(testIdx, trainModel); acc(trial) mean(pred trueLabel); end fprintf(平均识别正确率%.2f%%\n, 100*mean(acc));如果训练人数只有两三个人正确率可能虚高这时要关注混淆矩阵里哪些说话人相互频繁误判。GMM对音色极度相似的说话人容易混淆这种场景下需要优先检查特征是否包含足够的说话人个性信息并考虑增加混合度或换用更长的语音段做测试。调参的一条实用技巧是在gmm_em里把协方差floor从1e-4调小到1e-6可以保留更多细节但数值稳定性会明显变差每次训练完记得打印一次对数似然序列确认其持续上升而不是中途变成NaN。本文还有配套的精品资源点击获取