简介:这套基于高斯混合模型(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。假如采样率fs=16000Hz,那么一帧就是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/N,N为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/2;'t'表示返回三角滤波器矩阵。melEnergy每一帧得到24个通道的能量值。滤波器数量不是越大越好,太大相邻滤波器相关性变强,太小则丢说话人的个性频谱细节。
2.5 对数压缩与DCT:melcepst.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.m | DCT变换 | 对数能量序列 |
| melcepst.m | MFCC顶层封装 | 语音、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×M,means是M×D,covs用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-means | GMM 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_recog
4.1 资源包文件与各自角色
压缩包里的文件分布很清晰,第一类是与MFCC相关的底层函数,第二类是GMM训练相关函数,第三类是数据和主脚本。先看下来避免运行时分不清主次:
| 文件 | 角色 |
|---|---|
| Runme_train.m | 训练主脚本,加载tra_data.mat并生成speaker.mat |
| Runme_recog.m | 识别主脚本,加载rec_data.mat和speaker.mat |
| tra_data.mat | 训练语音数据 |
| rec_data.mat | 测试语音数据 |
| speaker.mat | 训练好的GMM模型集合 |
| gmm_init.m / gmm_em.m / calcpost.m | GMM初始化、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。
本文还有配套的精品资源,点击获取