简介:这份资源面向语音处理与模式识别方向的学习者,提供一套基于高斯混合模型(GMM)的说话人识别MATLAB实现,适合已具备一定信号处理基础、希望动手实践话者识别流程的高年级本科生或研究生。压缩包共15个文件,以12个.m脚本和3个.mat数据文件为主,整体约2.68MB,脚本覆盖GMM初始化、EM迭代训练、后验概率计算、MFCC特征提取与识别匹配等环节,数据文件则用于存放训练与识别阶段的语音特征。目前已有816人学习下载。读者可借助train.m与recog.m完整走通从特征提取、模型训练到说话人判定的链路,理解E步计算后验概率、M步更新均值方差与混合系数的原理,并在此基础上调整参数、替换特征以提升识别准确率与鲁棒性。
1. 从 calcpost 到说话人识别:GMM 这套老方案为什么还值得动手跑一遍
如果你手上有一个基于高斯混合模型(GMM)的说话人识别matlab.zip,解压后看到calcpost、gmm 训练、话者识别这几个文件夹或函数名,那基本可以确定:这是一套用 MATLAB 实现的、基于高斯混合模型的文本无关说话人识别代码。它的核心逻辑不复杂——对每个说话人的语音提取 MFCC 特征,用多高斯分量去拟合这个人的特征分布,识别时算一段测试语音在每个说话人 GMM 下的对数似然,谁高判给谁。calcpost通常就是计算后验概率或对数似然的那一步。
这套东西放到今天不算新,深度学习做声纹早就把它按在地上摩擦。但它依然是理解说话人识别最干净的入口:参数量小、训练快、每一步都能在 MATLAB 里打断点看数值。适合两类人——想搞懂 GMM-UBM 这套经典框架到底怎么落地的新手,以及需要在嵌入式或小样本场景下快速搭一个能跑的话者识别基线的工程师。下面我按“特征怎么提、模型怎么训、calcpost 怎么算、坑在哪”把整条链路拆开讲清楚,你照着能复现。
2. 特征前端:MFCC 提取与 GMM 的输入对齐
2.1 为什么 GMM 吃的是 MFCC 而不是原始波形
GMM 是一个对特征向量做概率密度估计的模型,它假设每一帧语音的特征向量服从若干个高斯分布的加权混合。原始波形的维度是采样点数,帧与帧之间高度相关,直接丢给 GMM 既不稳也不收敛。所以标准做法是先做分帧、加窗、FFT,再走 Mel 滤波器组、取对数、做 DCT,得到 13 维左右的 MFCC,通常再加一阶、二阶差分凑成 39 维。
这里有个容易忽略的点:GMM 本身不含时序建模能力,它把每一帧当成独立同分布样本。所以 MFCC 里的差分特征其实是在帮它补一点动态信息。如果你只提 13 维静态 MFCC,识别率通常会掉几个点,这不是玄学,是差分补了帧间变化。
2.2 用 MATLAB 提 MFCC 的最小代码
MATLAB 从 R2018b 起有mfcc函数,但很多老代码包用的是自己写的melbankm+dct。两种都能用,关键是参数要统一。下面给一段我常用的提取流程,采样率按 16kHz、帧长 25ms、帧移 10ms 设。
fs = 16000; % 采样率,说话人识别常用 16k frameLen = 400; % 25ms * 16000 = 400 点 frameShift = 160; % 10ms * 16000 = 160 点 nfft = 512; % FFT 点数,取 2 的幂 numCep = 13; % 倒谱系数个数 numFilters = 26; % Mel 滤波器个数 % 预加重,补偿高频衰减 x = filter([1 -0.97], 1, x); % 分帧 frames = buffer(x, frameLen, frameLen - frameShift, 'nodelay'); frames = frames .* hamming(frameLen); % 加汉明窗 % FFT 与功率谱 spec = abs(fft(frames, nfft)).^2; spec = spec(1:nfft/2+1, :); % Mel 滤波器组 melFb = melFilterBank(fs, nfft, numFilters); melEner = melFb * spec; melEner(melEner < eps) = eps; % 防止 log(0) % 取对数做 DCT logMel = log(melEner); cep = dct(logMel); mfcc = cep(2:numCep+1, :); % 丢掉第 0 维能量逻辑说明:预加重系数 0.97 是行业默认,别乱改;buffer的第三参数保证帧移正确;melFilterBank需要你自己实现或从代码包里拿,它决定 Mel 刻度上的三角滤波器中心频率。参数说明:numCep取 13 是经典值,取 20 也能跑但会引入更多噪声维度;numFilters一般取 24 到 26,太少频率分辨率不够,太多相邻滤波器重叠过大。
2.3 倒谱均值归一化:不做这一步识别率会飘
MFCC 提完后必须做倒谱均值归一化(CMN),也就是把每条语音的每一维 MFCC 减去该维的均值。原因是不同录音设备、不同信道的频率响应会在倒谱域引入一个加性偏置,CMN 能把这个偏置去掉。很多代码包里calcpost之前会有一个cmn或mean_norm调用,别把它当可选步骤。
mfcc = mfcc - mean(mfcc, 2); % 按行减均值,每维独立归一如果你还想要更好的信道鲁棒性,可以再加倒谱均值方差归一化(CMVN),但方差归一化在短语音上会放大噪声,测试语音少于 3 秒时慎用。
3. GMM 训练:从特征到说话人模型
3.1 GMM 的三个参数到底在拟合什么
一个 GMM 由三组参数描述:混合权重w、均值向量mu、协方差矩阵Sigma。权重表示每个高斯分量被选中的先验概率,均值和协方差描述该分量在特征空间里的位置和形状。训练就是给定一堆 MFCC 帧,用 EM 算法迭代调整这三组参数,让数据在这些高斯混合下的似然最大。
说话人识别里通常用对角协方差,不用全协方差。原因很实际:全协方差参数量是D*D,39 维就是 1521 个参数每个分量,数据不够直接过拟合;对角协方差只有 39 个,训练稳、算得快,实测识别率差距很小。
3.2 用 MATLAB 训练一个说话人 GMM
MATLAB 的 Statistics and Machine Learning Toolbox 里有fitgmdist,可以直接训。但很多老代码包用的是自己写的 EM,因为要控制初始化。下面给一段基于fitgmdist的最小训练代码,同时说明自己写 EM 时要注意什么。
function gmm = trainGMM(feat, nMix) % feat: D x N,D 是特征维数,N 是帧数 % nMix: 高斯分量个数,常用 16 / 32 / 64 D = size(feat, 1); feat = feat'; % fitgmdist 要求样本按行排列 % 用 kmeans 初始化均值,避免 EM 陷入局部最优 [idx, C] = kmeans(feat, nMix, 'MaxIter', 200, ... 'Replicates', 3, 'EmptyAction', 'singleton'); % 构造初始协方差:对角,用全局方差 Sigma = zeros(D, D, nMix); globalVar = var(feat, 0, 1); for k = 1:nMix Sigma(:,:,k) = diag(globalVar); end % 初始权重按簇大小比例 w = histcounts(idx, 1:nMix+1) / numel(idx); init = struct('mu', C, 'Sigma', Sigma, 'ComponentProportion', w); % 训练,RegularizationValue 防止协方差奇异 gmm = fitgmdist(feat, nMix, 'Start', init, ... 'CovarianceType', 'diagonal', ... 'RegularizationValue', 1e-4, ... 'MaxIter', 300, 'TolFun', 1e-6); end逻辑说明:kmeans初始化比随机初始化稳得多,Replicates设 3 是让 kmeans 自己跑三次取最优。RegularizationValue是关键参数,它给协方差对角线加一个小量,防止某个分量只分到一两个样本导致协方差接近零、似然爆炸。参数说明:nMix取 16 适合短语音或数据少的情况,32 到 64 适合每人有几分钟以上语音;MaxIter300 通常够收敛,TolFun1e-6 是似然变化阈值。
3.3 自己写 EM 时最容易翻车的两处
如果你用的代码包里是手写 EM,重点看两个地方。第一是 E 步算责任度gamma时有没有做下溢保护,标准做法是先算对数似然再用 log-sum-exp 归一化,直接算exp在 39 维下几乎必然下溢成 0。第二是 M 步更新协方差后有没有检查正定性,对角协方差只要保证每个元素大于零即可,但手写代码经常忘了加eps。
% E 步的对数域写法,避免下溢 logProb = zeros(N, K); for k = 1:K diff = feat - mu(:,k)'; logProb(:,k) = -0.5*sum((diff.^2)./varVec(:,k)', 2) ... -0.5*sum(log(varVec(:,k))) ... + log(w(k)); end logNorm = logsumexp(logProb, 2); gamma = exp(logProb - logNorm); % 责任度,每行和为 1这段是calcpost类函数的核心,识别阶段算的也是类似的东西,只是把训练时的责任度换成了整段语音的对数似然求和。
4. calcpost 与识别打分:一段语音怎么判给某个人
4.1 calcpost 在识别阶段到底算什么
calcpost这个名字直译是“计算后验”,在说话人识别里通常做两件事:一是算测试语音每一帧在每个说话人 GMM 下的对数似然,二是把这些帧级似然累加或平均,得到整段语音的得分。严格的后验概率需要贝叶斯公式加先验,但说话人识别里先验通常设均匀,所以直接比对数似然就行。
function score = calcpost(gmmList, feat) % gmmList: 训练好的 GMM 数组 % feat: D x N 测试特征 % score: 1 x S,每个说话人的对数似然总分 S = numel(gmmList); score = zeros(1, S); feat = feat'; for s = 1:S % 逐帧算对数似然,再求和 ll = sum(log(pdf(gmmList{s}, feat))); score(s) = ll; end end逻辑说明:pdf返回的是概率密度,数值可能极小,先取log再求和是标准做法,避免连乘下溢。参数说明:这里用的是总分,如果测试语音长度差异大,应该改成平均分ll/N,否则长语音天然占优。很多代码包在calcpost里还会做似然归一化,减掉一个 UBM 的得分,这就是 GMM-UBM 框架。
4.2 GMM-UBM:为什么直接训每个说话人的 GMM 不够
如果每个说话人只有几十秒语音,直接训 64 分量 GMM 会过拟合,识别率很难看。标准解法是先用大量不同说话人的语音训一个通用背景模型(UBM),然后用每个说话人的数据对 UBM 做 MAP 自适应,只微调均值和权重。这样每个说话人的模型都从同一个 UBM 出发,参数空间对齐,小数据下稳得多。
function gmm = adaptUBM(ubm, feat, r) % ubm: 通用背景模型 % feat: 目标说话人特征,D x N % r: 自适应系数,控制新数据权重 feat = feat'; % 用 UBM 算责任度 gamma = posterior(ubm, feat); % MAP 更新均值 mu_new = ubm.mu; for k = 1:ubm.NumComponents Nk = sum(gamma(:,k)); if Nk > 0 alpha = Nk / (Nk + r); mu_new(k,:) = alpha * (gamma(:,k)' * feat / Nk) ... + (1 - alpha) * ubm.mu(k,:); end end gmm = ubm; gmm.mu = mu_new; end逻辑说明:r是自适应系数,典型取 10 到 16,它控制目标说话人数据对 UBM 均值的修改幅度。Nk是第 k 个分量分到的帧数,帧数越多alpha越接近 1,越信任新数据。参数说明:只更新均值通常就够,更新协方差需要更多数据,更新权重在短语音下容易不稳。
4.3 打分阶段的归一化:别忽略 trial 归一化
实际做识别时,测试语音和注册语音可能来自不同信道,得分会有系统性偏移。常见做法是做得分归一化,比如 Z-norm 或 T-norm:用一批背景说话人的语音算得分均值和方差,把测试得分标准化。这一步在代码包里可能藏在calcpost后面,如果识别率不理想,先检查有没有做归一化。
5. 避坑与排查:跑 GMM 说话人识别最常见的 5 个翻车点
5.1 识别率接近随机:先查特征和标签是否对齐
现象:训练完测试,准确率跟瞎猜差不多。原因:最常见的是特征矩阵和说话人标签错位,比如calcpost里循环说话人时索引从 0 开始但 MATLAB 从 1 开始,或者特征提取时把帧数当成了样本数。解决:在训练前打印每个说话人的特征维度,确认size(feat,2)是帧数、size(feat,1)是维数,标签数量等于说话人数。
5.2 训练时似然变成 NaN:协方差奇异或 log(0)
现象:EM 迭代几轮后似然变 NaN 或 -Inf。原因:某个高斯分量只分到极少帧,协方差趋近零,log出 -Inf;或者 Mel 能量里有零,log(0)出 -Inf。解决:Mel 能量加eps,协方差加RegularizationValue,E 步用对数域 log-sum-exp。这三处是血泪经验,缺一个都可能翻车。
5.3 测试语音长短不一导致得分不可比
现象:长语音总是被判给某个说话人。原因:calcpost用的是对数似然总和,帧数越多总和越大。解决:改成平均对数似然,或者做长度归一化。如果代码包里是总和,自己改成ll/N再比。
5.4 换一台机器识别率掉一半:采样率或预加重不一致
现象:训练和测试在同一台机器上正常,换设备就崩。原因:训练语音 16kHz,测试语音 8kHz 没重采样;或者预加重系数一边 0.97 一边 0.95。解决:在特征提取入口强制统一采样率,预加重系数写死在配置里,别两处各写一个。
5.5 nMix 设太大导致训练极慢且过拟合
现象:nMix设 128 或 256,训练几小时不收敛,测试还不如 32。原因:数据量不够支撑这么多分量,EM 在局部最优附近震荡。解决:每人语音少于 1 分钟时nMix不超过 32,1 到 3 分钟用 64,更多再考虑 128。先跑 16 看基线,再往上加。
6. 把 GMM 当基线:验证方法与一个提分技巧
跑通之后怎么确认这套 GMM 是真的在工作,而不是碰巧?我一般做三件事。第一,画每个说话人 GMM 的对数似然随迭代次数的曲线,正常应该单调上升然后趋平,如果震荡说明nMix太大或初始化不好。第二,做留一交叉验证:每次留一个说话人的一段语音做测试,其余做训练,看识别率是否稳定。第三,拿一段明显不属于任何注册说话人的语音做开集测试,看得分是否普遍偏低,这能验证模型没有过度自信。
一个实测有效的提分技巧是得分融合。GMM 的得分和 MFCC 差分特征的得分往往互补,你可以分别用静态 MFCC 和 MFCC+差分训两个 GMM,识别时把两组对数似然加权相加。权重不用调太细,0.5 比 0.5 通常就比单用好。下面是一个简单的融合打分示例。
function score = fusedScore(gmmStatic, gmmDelta, featStatic, featDelta) % 两组特征分别打分后加权融合 s1 = calcpost(gmmStatic, featStatic); s2 = calcpost(gmmDelta, featDelta); % 各自做零均值单位方差归一化,避免量纲差异 s1 = (s1 - mean(s1)) / std(s1); s2 = (s2 - mean(s2)) / std(s2); score = 0.5 * s1 + 0.5 * s2; end逻辑说明:先各自归一化再融合,是因为两组得分的数值范围可能差很多,直接相加会被其中一组主导。参数说明:权重 0.5 是起点,如果静态特征明显更好可以调到 0.6/0.4,但别调得太激进,融合的收益本来就有限。
还有一个边界要知道:GMM 是文本无关的,它不关心你说的是什么内容,只关心特征分布。所以它天然适合固定短语以外的场景,但也因此对信道和噪声敏感。如果你的场景是远场或强噪声,GMM 基线可能只有六七十分,这时候该上 i-vector 或深度嵌入,但 GMM 仍然是你验证前端特征是否提对的参照物。
我自己跑这类代码包的习惯是:先不改任何参数跑一遍,记下基线识别率;然后只动nMix和r两个参数,各跑三组,看趋势;最后再动特征。这样出问题能定位到是哪一层。希望帮到你。
本文还有配套的精品资源,点击获取