基于HMM的孤立字语音识别MATLAB实战:MFCC特征与Viterbi解码 简介这份资源提供基于隐马尔可夫模型HMM的孤立字语音识别MATLAB实现面向具备一定信号处理与统计学习基础的高年级本科生、研究生及语音识别入门开发者用于理解并复现HMM在孤立字识别中的完整流程。压缩包共15个文件以13个.m脚本和2个.mat数据文件为主整体约591KB脚本覆盖语音预处理、端点检测、特征提取、参数训练与识别推断等环节数据文件则用于训练与识别实验。已有100人学习下载。读者可据此搭建从训练集构建、模型初始化、参数重估到维特比解码的完整实验链路结合MFCC特征提取与聚类初始化等模块深入掌握状态转移概率、输出概率与初始状态概率的估计方法并在此基础上开展识别准确率对比与算法改进实验。1. 基于隐马尔可夫模型HMM的孤立字语音识别从一段“听不清”的录音说起一段 0.8 秒的录音背景有空调底噪说话人带一点方言口音让你判断这是“开”还是“关”。人耳几乎瞬间完成但把它交给代码问题立刻变得具体音频怎么切、特征取什么、模型怎么建、识别结果为什么总是偏向某一个词。孤立字语音识别就是在这个尺度上做文章——一次只认一个词不处理连续语句也不做大规模语言模型纠错。基于隐马尔可夫模型HMM的孤立字语音识别 MATLAB 代码正是很多实验室和课程设计里最经典的入门路径用 MFCC 提特征用 HMM 做时序建模用 Viterbi 解码出最可能的词。它适合两类人一是想真正搞懂语音识别底层时序建模的开发者二是需要在 MATLAB 环境里快速搭一套可解释、可调参、可复现的孤立字识别流程的工程师。这篇文章不讲空泛概念只讲怎么把这条链路跑通、参数怎么设、哪里容易翻车。2. 为什么孤立字识别仍然值得用 HMM 做一遍原理与选型理由2.1 孤立字任务的边界与 HMM 的天然匹配孤立字识别Isolated Word Recognition的核心约束是每个语音片段只包含一个词词与词之间有明确静音段。这个约束把问题简化成了“给一段特征序列从固定词表里选一个最可能的词”。HMM 之所以适合是因为语音信号的本质是短时平稳加时序演化在 10~30 毫秒的尺度上频谱特性近似不变但跨帧来看同一个词的发音又存在时长和音素过渡的变化。HMM 用状态转移描述这种时序演化用观测概率描述每帧特征与状态的匹配程度天然处理“同一词不同语速、不同发音”的弹性对齐问题。常见做法是每个词训练一个独立的 HMM词表有多大就建多少个模型。识别时把测试语音的特征序列分别送进每个模型用 Viterbi 算法计算该模型下产生这条序列的最大概率取概率最大的词作为结果。这里不需要语言模型也不需要解码网络整个流程在 MATLAB 里用几十行核心代码就能表达清楚。对于想理解“时序建模到底在做什么”的人来说这条路径比直接上端到端深度网络更有教学和调试价值。2.2 特征选择MFCC 仍然是孤立字场景的稳妥起点在 MATLAB 里做孤立字识别特征侧最常见的选择是 MFCCMel 频率倒谱系数。原因很直接它把频谱包络和基频细节做了分离保留了与音素内容更相关的包络信息同时做了倒谱域的去相关。对于孤立字这种短时、干净、词表有限的场景MFCC 加一阶差分已经能提供足够区分度。具体参数上我一般会这样设采样率 16 kHz预加重系数 0.97帧长 25 ms400 点帧移 10 ms160 点Hamming 窗Mel 滤波器组取 24~26 个倒谱系数取 12~13 维再加一维对数能量最后拼接一阶差分。这样每帧得到 26~28 维特征。帧移不要小于 10 ms否则相邻帧相关性太强HMM 的状态转移会变得“黏”帧长不要超过 30 ms否则短时平稳假设开始失效。2.3 HMM 拓扑与状态数从左到右模型怎么定孤立字 HMM 通常采用从左到右无跳转或允许自环的拓扑。每个词一个模型模型内部状态数一般取 5~8 个。状态太少无法覆盖一个词内部的音素过渡状态太多训练数据不够时每个状态分不到足够帧参数估计会严重过拟合。我的经验是单音节词取 5 个状态双音节词取 6~8 个状态三音节及以上取 8~10 个状态。每个状态用混合高斯输出混合数从 1 开始试数据量充足再增加到 2~4。混合数超过 4 在孤立字小词表场景下收益很小反而容易在训练集上过拟合。转移矩阵初始化时自环概率给大一点比如 0.6~0.7进入下一状态的概率给 0.3~0.4最后状态只保留自环。这样训练初期模型倾向于“停留”随着 Baum-Welch 迭代再自动调整。如果一开始就把转移概率均匀分布训练收敛会慢很多甚至出现状态“跳跃”导致对齐混乱。3. 在 MATLAB 里把 HMM 孤立字识别跑通数据准备、训练与解码3.1 数据组织与静音段切除假设你手头有一批孤立字录音每个词有若干遍重复。目录结构建议按词名分文件夹每个文件夹下放该词的多个 wav 文件。采样率统一到 16 kHz单声道16 bit。如果原始录音采样率不一致先用resample统一不要直接改采样率参数去凑。静音切除是第一个容易翻车的地方。孤立字录音前后通常有几百毫秒静音如果不切掉HMM 会花大量状态去建模静音词本身的区分性反而被稀释。常见做法是用短时能量和过零率做双门限端点检测。下面这段代码给出一个可复现的端点检测实现function [speech, fs] trim_silence(filename, frame_len, frame_shift, energy_th, zcr_th) % 读取音频并做端点检测返回切除静音后的语音段 [speech, fs] audioread(filename); speech speech(:,1); % 强制单声道 speech speech / max(abs(speech)); % 幅度归一化避免能量门限受录音增益影响 frame_len round(frame_len * fs); % 帧长转采样点 frame_shift round(frame_shift * fs); % 帧移转采样点 num_frames floor((length(speech) - frame_len) / frame_shift) 1; energy zeros(num_frames, 1); zcr zeros(num_frames, 1); for i 1:num_frames idx (i-1)*frame_shift 1; frame speech(idx : idx frame_len - 1); energy(i) sum(frame.^2); zcr(i) sum(abs(diff(sign(frame)))) / (2 * frame_len); end energy energy / max(energy); % 能量归一化到 0~1 zcr zcr / max(zcr); % 双门限能量高于高门限或过零率高于门限的帧视为语音 is_speech (energy energy_th) | (zcr zcr_th); start_idx find(is_speech, 1, first); end_idx find(is_speech, 1, last); if isempty(start_idx) warning(未检测到语音段返回原始信号); return; end % 前后各留 50 ms 余量避免切掉词头词尾的弱音素 margin round(0.05 * fs); start_sample max(1, (start_idx-1)*frame_shift 1 - margin); end_sample min(length(speech), (end_idx-1)*frame_shift frame_len margin); speech speech(start_sample : end_sample); end这段代码的逻辑是先做幅度归一化让能量门限不受录音音量影响然后逐帧计算短时能量和过零率用“或”逻辑合并两个判据避免清音段能量低但过零率高时被误判为静音最后前后各留 50 ms 余量防止词首爆破音或词尾鼻音被切掉。参数上frame_len取 0.025 秒frame_shift取 0.01 秒energy_th从 0.1 开始试zcr_th从 0.2 开始试。如果发现切出来的片段太短优先降低energy_th如果噪声被切进来优先提高zcr_th。3.2 MFCC 特征提取与差分拼接端点检测之后对每个语音段提取 MFCC。MATLAB 没有内置的 MFCC 函数需要自己写 Mel 滤波器组和 DCT。下面给出核心实现function feat extract_mfcc(speech, fs, num_mel, num_cep) % 提取 MFCC 及一阶差分返回 (num_frames x (2*num_cep1)) 的特征矩阵 frame_len round(0.025 * fs); frame_shift round(0.01 * fs); pre_emph 0.97; speech filter([1 -pre_emph], 1, speech); % 预加重提升高频 num_frames floor((length(speech) - frame_len) / frame_shift) 1; win hamming(frame_len); mel_filters mel_filterbank(fs, frame_len, num_mel); % 自定义 Mel 滤波器组 mfcc zeros(num_frames, num_cep); log_energy zeros(num_frames, 1); for i 1:num_frames idx (i-1)*frame_shift 1; frame speech(idx : idx frame_len - 1) .* win; spec abs(fft(frame, frame_len)); spec spec(1 : frame_len/2 1); mel_energy mel_filters * (spec.^2); mel_energy(mel_energy 1e-10) 1e-10; % 防止 log(0) log_mel log(mel_energy); mfcc(i,:) dct(log_mel); mfcc(i,1) []; % 去掉第 0 阶它主要反映能量 log_energy(i) log(sum(frame.^2) 1e-10); end % 取前 num_cep 维拼接对数能量 mfcc mfcc(:, 1:num_cep-1); feat [mfcc, log_energy]; % 一阶差分 delta diff(feat, 1, 1); delta [delta(1,:); delta]; % 补齐帧数 feat [feat, delta]; end这里的关键参数是num_mel和num_cep。num_mel取 24~26num_cep取 12~13。Mel 滤波器组在低频密集、高频稀疏符合人耳听觉特性。DCT 之后去掉第 0 阶是因为它主要反映帧能量而能量已经单独作为一维拼进来了。一阶差分描述特征随时间的变化对区分发音相近的词很重要。注意差分后要补齐第一帧否则特征矩阵行数会少一帧后续 HMM 训练时容易和标签对不上。3.3 用 MATLAB 的 hmmtrain 训练每个词的 HMMMATLAB 统计与机器学习工具箱提供了hmmtrain和hmmdecode可以直接用于离散或高斯观测的 HMM。对于连续 MFCC 特征需要设置Symbols为特征矩阵并指定输出分布为高斯混合。下面是一个训练单个词模型的示例function hmm train_word_hmm(features, num_states, num_mix) % features: cell 数组每个元素是一个语音段的特征矩阵 (T x D) % 返回训练好的 HMM 结构体 % 初始化转移矩阵自环 0.7右移 0.3 trans diag(0.7 * ones(num_states,1)) diag(0.3 * ones(num_states-1,1), 1); trans(end,end) 1.0; % 最后状态只自环 trans(1,1) 0.7; % 第一状态自环 % 初始化输出用所有特征做全局均值和方差 all_feat vertcat(features{:}); mu mean(all_feat); sigma cov(all_feat); % 构造初始 HMM hmm struct(); hmm.nstates num_states; hmm.trans trans; hmm.mu repmat(mu, num_states, 1); hmm.sigma repmat(sigma, 1, 1, num_states); hmm.mix ones(num_states, num_mix) / num_mix; % 混合权重均匀 % 用 Baum-Welch 迭代训练 max_iter 20; tol 1e-4; for iter 1:max_iter [hmm, loglik] hmmtrain(features, hmm, MaxIterations, 1, ... Tolerance, tol, Verbose, false); if iter 1 abs(loglik - prev_loglik) tol break; end prev_loglik loglik; end end这段代码的核心是转移矩阵初始化。自环概率给 0.7右移给 0.3最后状态只自环这样模型在训练初期倾向于在某个状态停留随着迭代再调整。hmmtrain的MaxIterations设为 1外层再循环是为了手动控制收敛判断和日志输出。实际使用时如果发现某个词的模型在训练集上对数似然一直不上升优先检查特征里有没有 NaN 或 Inf其次检查端点检测是否把静音切得太干净导致某些状态分不到帧。3.4 Viterbi 解码与识别结果输出识别阶段对每个测试语音提取同样的 MFCC 特征然后分别计算它在每个词模型下的对数似然取最大值对应的词function [best_word, scores] recognize(feat, models, word_list) % feat: 测试语音特征矩阵 (T x D) % models: cell 数组每个元素是一个训练好的 HMM % word_list: cell 数组词名列表 num_words length(models); scores zeros(num_words, 1); for i 1:num_words [~, loglik] hmmdecode(feat, models{i}, Symbols, feat); scores(i) loglik; end [max_score, idx] max(scores); best_word word_list{idx}; end这里hmmdecode返回的是该模型下观测序列的对数似然。注意Symbols参数要传入特征矩阵且训练和测试的特征维度必须一致。如果测试时发现所有词的得分都差不多说明模型区分性不够优先增加训练样本数或降低状态数如果某个词总是被误认成另一个词先看这两个词的 MFCC 特征在倒谱域是否真的接近再考虑增加差分维度或调整 Mel 滤波器组。4. 避坑与排查孤立字 HMM 识别里最容易翻车的 5 个点4.1 端点检测切太狠词头词尾被吃掉现象训练出来的模型在测试集上识别率很低但把测试语音手动切好再识别就正常。原因端点检测的 energy_th 设得太高或者 margin 留得太小导致词首的爆破音、词尾的鼻音被切掉。解决把 energy_th 降低 0.05~0.1margin 从 50 ms 增加到 80~100 ms。更稳妥的做法是先用一段已知的静音录音统计噪声能量再据此设定门限而不是拍脑袋给固定值。4.2 特征维度不一致导致 hmmtrain 报错现象训练时提示“观测序列维度不匹配”或“Symbols 大小不一致”。原因不同语音段提取的 MFCC 帧数不同是正常的但特征维度必须相同。常见错误是某段语音太短DCT 之后取前 num_cep-1 维时越界或者差分补齐时行列搞反。解决在提取特征后统一检查size(feat,2)是否等于预期维度如果不等打印该文件的帧数和采样点数定位是端点检测切太短还是 DCT 参数写错。4.3 状态数给太多训练集上完美、测试集上崩盘现象训练时对数似然很高但测试识别率只有 50% 左右。原因每个词只有几遍录音状态数却给了 10 个以上每个状态分到的帧数不足高斯参数估计严重过拟合。解决单音节词状态数降到 5双音节降到 6~8混合数从 1 开始。如果词表里词之间发音差异大优先增加训练遍数而不是增加状态数。4.4 转移矩阵初始化太均匀Baum-Welch 收敛慢现象训练迭代 20 次后对数似然几乎没变化模型参数和初始值差不多。原因转移矩阵初始化时自环和右移概率接近均匀模型没有形成“从左到右”的偏好。解决自环概率给 0.6~0.7右移给 0.3~0.4最后状态只自环。如果还是收敛慢把MaxIterations增加到 30~40同时观察对数似然曲线只要还在上升就不要提前停。4.5 测试语音和训练语音的录音条件不一致现象训练集识别率 90%测试集只有 60%且误识别没有明显规律。原因训练和测试用了不同的麦克风、不同的采样率或不同的环境噪声。解决统一采样率到 16 kHz统一做幅度归一化测试时尽量用和训练相同的录音设备。如果条件受限在特征侧加倒谱均值归一化CMN即对每个语音段的 MFCC 减去该段的均值能部分抵消通道差异。5. 把识别率再往上推一点CMN、多遍平均与混淆矩阵排查走到这里一套基于 HMM 的孤立字识别流程已经能跑通。但如果你的词表里有几个发音接近的词比如“四”和“十”、“开”和“关”识别率可能卡在 80% 上下。这时候别急着换模型先把特征侧和后处理侧能做的做满。第一个技巧是倒谱均值归一化CMN。实现很简单对每个语音段的 MFCC 矩阵按列减去该段的均值。这一步能抵消不同录音通道带来的频谱倾斜。代码上就是在extract_mfcc返回之前加一行feat feat - mean(feat, 1);。注意要同时对静态系数和差分系数做或者只对静态系数做、差分重新计算两种做法我都试过差别不大关键是训练和测试要一致。第二个技巧是多遍平均。孤立字录音通常每个词有 3~5 遍训练时不要只用一个模型去拟合所有遍而是对每个词训练多个 HMM比如每遍一个识别时取所有模型得分的最大值或平均值。最大值对异常遍更鲁棒平均值对噪声更平滑。我的习惯是训练遍数少于 5 时用最大值大于等于 5 时用平均值。第三个技巧是用混淆矩阵定位问题。不要只看总体识别率把测试结果整理成混淆矩阵看哪些词对之间互相误认。如果“四”和“十”互相误认先看它们的 MFCC 特征在第二、三维上是否重叠严重如果重叠考虑增加 Mel 滤波器组数量到 28~30或者把帧长从 25 ms 降到 20 ms提高时间分辨率。如果混淆矩阵显示某个词被误认成多个其他词那更可能是该词的训练样本太少或端点检测不稳定优先补数据。最后一个习惯每次改完参数固定随机种子重新跑一遍完整训练和测试把识别率、混淆矩阵、每个词的对数似然得分都存下来。孤立字 HMM 的调参空间不大但参数之间的相互影响很微妙没有记录的话很容易陷入“改了 A 好了改了 B 又坏了”的循环。我自己的做法是建一个简单的 CSV每行记录一次实验的参数组合和识别率跑上十几轮之后哪个参数在哪个区间最稳一目了然。希望帮到你。本文还有配套的精品资源点击获取