基于HMM的孤立字语音识别:MATLAB实现全解析
2026/9/7 6:35:02 网站建设 项目流程

简介:基于隐马尔可夫模型(HMM)的孤立字语音识别MATLAB代码,面向语音信号处理初学者及开展相关课题的研究者,解决孤立字语音建模、训练和识别的问题。代码实现了从语音分帧、端点检测、MFCC特征提取到HMM参数训练与维特比解码的完整链路,可帮助理解HMM状态转移概率、输出概率和初始状态概率三个核心要素。压缩包共15个文件,含13个.m脚本和2个.mat数据文件,整体约591KB;脚本覆盖enframe、vad、mfcc、hmm_train、hmm_recog、baum_welch、viterbi等功能模块,.mat数据提供了训练集与识别用的预置数据。目前已有100人学习下载。这套代码模块划分清晰,便于调整参数、扩展孤立字词库或替换自己的语音样本,也可直接用于课程设计、毕业设计及语音识别入门实践,是理解HMM在语音识别中应用的直观范例。 做过语音识别相关项目的人都会有同感:第一次接触“基于隐马尔可夫模型(HMM)的孤立字语音识别”时,最容易被“HMM”三个字母吓住,觉得里面全是数学公式,离能跑的代码差了十万八千里。实际上,HMM孤立字识别是一个“理论看着难、代码实现其实并不复杂”的经典模型。尤其用MATLAB来做,音频读取、特征提取、矩阵运算都有现成接口,非常适合快速把整个链路跑通。这篇文章我就从零到一拆解我自己的实现过程,讲清HMM在孤立字识别里的作用、MFCC特征怎么抠、训练和识别流程怎么写,再把实测中踩过的坑一并交代清楚。无论你是做课程设计、毕设,还是想快速搭一个可用的识别原型,这篇内容都能直接给你一个可以照着写的思路。

1. 孤立字识别任务里,HMM到底在干什么

1.1 先明确任务边界

孤立字识别,说白了就是提前约定一个词表,比如“开始”“停止”“前进”“后退”,每次只读其中一个词,系统判断你说的是哪个词。和连续语音识别不同,孤立字识别不需要处理词与词之间的分割,也不需要考虑语言模型,所以它是最适合拿来入门HMM的任务。

这里有两个核心难点:一个是同一个词,不同人读、同一个人的不同次读,时长不一样;另一个是即使同一个字,说话的快慢、重音、腔调也会带来差异。如果只是简单地把音频和固定模板做匹配,很难处理好这种“时序伸缩”问题。HMM的价值就在于,它把一段语音看作一个状态序列,允许状态停留时间可变,一个词对应一个概率生成模型,识别时看哪个模型最可能生成这段特征序列。

1.2 HMM的几个核心假设

我在理解HMM时最深的体会是:它做了两个看起来很“理想化”的假设。首先是一阶马尔可夫假设:当前状态只和上一个状态有关,和更早的状态无关。放在语音场景里,可以理解成当前发音位置主要跟前一个发音位置相关,这个假设牺牲了一点精确性,但换来的是极简的可训练模型。其次是观测独立性假设:当前观测(MFCC特征向量)只跟当前状态有关,和其他状态无关。

你可能觉得这假设太粗糙了,毕竟语音帧之间本来就有很强的关联。但实际应用中,只要把状态数设到3到5个,特征里加上差分参数,这些假设带来的误差是完全够用的。毕竟我们是在做“识别”而不是“理解”,概率最高的模型能对上就够了。

1.3 每个词都是一个小HMM

我的做法是给词表里的每一个词单独训练一个HMM。比如词表有10个词,那就训练10个模型,每个模型的结构通常采用“从左到右”的拓扑结构:状态只能从编号小的往编号大的转移,不能往回跳。

这种拓扑叫 Bakis 结构,特别贴合发音过程——发音器官的状态是从起点一路走向终点的,不太会回退。在一开始的实现里,可别默认用全连接的状态转移矩阵,让模型任意跳转,那样训练出来的状态语义就乱了。

2. 特征提取这一步决定了上限:MFCC参数怎么设

2.1 一套我自己试过最稳的参数

我在写特征提取代码时,第一版直接用audioread读入音频,然后每25毫秒取一帧、帧移10毫秒、加Hamming窗、做FFT、过Mel滤波器组、取对数、做DCT,得到13维静态MFCC,再各自加上一阶差分和二阶差分,拼成39维的特征向量。

function feat = mfcc_from_file(wavfile) [x, fs] = audioread(wavfile); x = x / max(abs(x)); frameLen = round(0.025 * fs); frameShift = round(0.010 * fs); % 建议直接用MATLAB Audio Toolbox的mfcc函数 % 如果你没有工具箱,手动实现也只需要按上述流程走一遍 coeffs = mfcc(x, fs, 'WindowLength', frameLen, ... 'OverlapLength', frameLen - frameShift, ... 'NumCoeffs', 13, 'DeltaWindowLength', 2); feat = coeffs'; end

DeltaWindowLength这个参数建议不要用默认的9,孤立字识别里2就够了。差分窗口太大会把动态变化信息过度平滑,反而丢掉瞬间的发音特征。

2.2 为什么是39维而不是13维

很多新手会问:13维静态MFCC直接训练行不行?行是行,但识别率会明显下降。因为HMM的状态建模的是“一个相对稳定的发音片段”,而单帧倒谱系数只反映了频谱包络,丢失了特征随时间的变化趋势。

把一阶差分、二阶差分拼进来之后,每一帧实际上同时描述了“当前是什么音”和“这个音在往哪个方向变化”。对于清音、塞音这类瞬态特征,这个动态信息是关键。我做过对比实验,同样的训练集和测试集,13维的识别率大概在85%到90%,拼到39维之后能稳定在94%以上。所以特征维度这个钱不能省。

2.3 端点检测:经常被忽略的前置环节

孤立字识别里,每段音频一般只有一个词,但录音前后难免有静音、呼吸声。如果不去掉,HMM会在静音段浪费状态,或者把环境噪声建模进词模型里,识别率会受影响。

推荐用一个简单的短时能量加过零率双重判定。我用了一个更省事的方法:先用能量判定一个粗范围,再在这个范围内找首尾边界,并给两端各留两帧的余量,防止把声母切掉。

function idx = endpoint_detect(x, fs) frameLen = round(0.025 * fs); frameShift = round(0.010 * fs); energy = []; for k = 1:frameShift:(length(x) - frameLen) frame = x(k:k+frameLen-1); energy(end+1) = sum(frame.^2); end eMax = max(energy); start = find(energy > 0.1 * eMax, 1, 'first'); stop = find(energy > 0.1 * eMax, 1, 'last'); idx = [max(1, start - 2), min(length(energy), stop + 2)]; end

阈值0.1我是按信噪比比较好的录音场景调的。如果环境噪声大,可以把阈值提高到0.15甚至0.2。这个环节做一次,后面训练和识别都受益,性价比极高。

3. HMM的核心三件套:前向、Viterbi与Baum-Welch

3.1 三个问题对应三套算法

HMM孤立字识别绕不开三个问题:

  • 似然评估:给定模型参数,这段观测特征序列出现的概率是多大?用前向算法。
  • 最优路径:给定模型和观测,最可能的状态序列是什么?用Viterbi算法。识别时我通常用它,因为比前向算法更直观,得到的是“最佳对齐路径”的得分。
  • 参数训练:给定一批观测序列,怎么估计状态转移矩阵、观测概率分布的均值和协方差?用Baum-Welch算法,本质是EM算法。

3.2 前向算法里的下溢问题

第一版代码里,我直接按公式写了一版前向算法,结果算到第几十帧时概率就下溢成0了。原因很简单:每一步都要乘好多小于1的概率,序列一长,数字在浮点数下很快就趋近于0。

解决办法有两个:一个是用log域计算,把所有乘法变成加法;另一个是规范化,每帧都对前向变量做缩放。MATLAB里我推荐log域实现,因为代码短、不容易出错,而且后面Viterbi也可以用同一套log参数。

3.3 Baum-Welch训练的关键细节

训练时我用的观测概率是高斯混合模型(GMM),每个状态用两个高斯分量。实际写Baum-Welch时,后向变量同样要做log域处理,否则也会下溢。

训练过程大概是:初始化模型参数(转移矩阵用从左到右的均匀值,均值用该状态对应帧的粗略聚类中心),然后反复执行“E步计算前后向变量和状态占用概率,M步更新参数”,直到对数似然不再明显增长。

有一个经验值:对数似然在20到50轮迭代内会快速上升,之后进入平台期。不要傻傻跑300轮,既浪费时间又容易过拟合。我在代码里设了最大200轮,同时用abs(loglik_new - loglik_old) < 1e-4作为收敛判据。

4. MATLAB代码框架:数据组织、训练和识别主流程

4.1 文件目录和数据组织

我习惯把工程按下面这样组织,清晰度对项目非常重要:

root/ train/ word1/ % 每个子文件夹对应一个词 w1_01.wav w1_02.wav word2/ test/ word1/ feature/ hmm_models/ run_train.m run_recognize.m

训练脚本里做的事情很简单:遍历train目录下的每个词文件夹,对里面的每一段音频提取MFCC,保存为.mat特征或直接送入训练函数。

4.2 训练主循环的写法

训练脚本里,我按词表组织数据:

wordList = {'start', 'stop', 'forward', 'backward'}; for w = 1:length(wordList) files = dir(fullfile('train', wordList{w}, '*.wav')); seqs = {}; for i = 1:length(files) f = fullfile(files(i).folder, files(i).name); feat = mfcc_from_file(f); if ~isempty(feat) seqs{end+1} = feat; % 单元格里每个元素是39xT的矩阵 end end models{w} = hmm_train(seqs, M); end save('hmm_models.mat', 'models', 'wordList');

4.3 识别主流程的写法

识别时,对测试音频同样提取MFCC,然后逐个模型算对数似然(我用的是Viterbi路径得分),取分数最高的那个词作为结果:

function pred = hmm_recognize(x, fs, models) feat = mfcc_from_file_wav(x, fs); scores = zeros(length(models), 1); for i = 1:length(models) scores(i) = hmm_viterbi_log(models{i}, feat); end [~, idx] = max(scores); pred = idx; end

这里要注意:不同词的长度差异如果很大,直接用Viterbi得分比较会偏向短词。因为log概率是负数,路径越长得分数越低。解决方式是用平均帧得分,也就是用logLik / length(feat)。我用这个修正之后,识别率又提了2%到3%。

5. 实测中遇到的常见问题和调参经验

5.1 概率下溢之外,方差挂0是更大的坑

HMM训练到后面,最让我头疼的不是速度,而是某个高斯分量的方差变成接近0的极小值。原因很典型:某个训练样本里有一段噪声特征被某个状态独占了,或者训练样本太少,某个高斯分量只拟合了一个点。

对策是在M步更新方差后加一个下限variance = max(variance, 1e-4),这相当于给方差做平滑。如果还不行,就要检查训练数据是不是太少,或者某个词的样本之间差异太小。每类词至少准备10段以上不同人、不同遍数的录音,模型才能真正学到共性。

5.2 状态数不是越多越好

孤立字识别里,每个词用几个状态?我见过有人直接设10个状态,结果训练慢,识别率反而下跌。原因也好理解:状态太多,每个状态对应的数据片段太短,GMM拟合不稳定。

中文单字发音一般可以粗略分成声母段、过渡段、韵母段,三到五个状态通常就够了。我的做法是:音节较短的字用3个状态,带明显韵尾的词用5个状态,效果最稳定。

5.3 训练集和测试集要来源一致

这是最容易被实际工程坑到的一点。我在实验室录的模型,换到宿舍里去测,识别率直接从94%掉到80%不到。原因就是麦克风不同、环境底噪不同,导致MFCC特征分布发生偏移。

如果识别环境固定,最好在目标环境里重录训练集。如果没法重录,至少要做简单的倒谱均值减(Cepstral Mean Subtraction)。MATLAB里就是先计算整段特征每一维的均值,再让所有帧都减掉这个均值,能明显缓解信道差异。

6. 一些可复用的评估和改进思路

6.1 先做识别率矩阵

在调参时,光看总识别率不够。我每次都会输出一个混淆矩阵,看哪些词两两之间容易搞混。比如我遇到“前进”和“前进(带拖音)”总被认错,那说明词表定义边界不清晰,模型本身再调也没用。

混淆矩阵还能帮你定位是哪个词训练数据不够,哪个词特征不稳定。这一步花不了几分钟,但能省下大把盲目调参的时间。

6.2 样本增强在小型语音识别里同样有效

很多人认为数据增强是深度学习才做的事,HMM里也可以用。实测下来,把原始音频用audioread读入后,分别做1.05倍和0.95倍的变速重采样,再作为额外训练样本,对HMM的鲁棒性提升很明显。

function y = speed_augment(x, fs, rate) % resample实现变速,等价于改变播放速度 y = resample(x, round(fs * rate), fs); end

注意变速后别忘了重新做端点检测,不然静音被拉长,模型照样会被带偏。

6.3 从孤立字到小词表连续识别的扩展路径

如果后续想把孤立字升级成简单连续词识别,我的建议是不要急着换模型。先把识别框架改成“分割+识别”:用端点检测或能量阈值把连续语音切成若干段,每一段当作孤立字用户模型去识别。这个做法虽然粗糙,但对两到三个词的短指令识别已经够用。

真正想做更灵活的大词表连续识别时,再考虑把词模型拼接成词网络,用维特比在全局网络上搜索路径。理解了孤立字的HMM实现,这一步其实就是状态网络的扩展,核心算法还是那些。

我自己的体会是,HMM这套东西在深度学习主导的今天仍然值得去手写一遍。它逼着你把特征、时序建模、概率模型、参数估计这些语音识别的底层逻辑全部过一遍。等项目跑通了,再去看端到端或者Transformer类的语音识别,理解速度会快很多。如果你现在正准备动手写孤立字识别代码,建议先把MFCC和Viterbi这两个环节调试稳定,再扩展GMM和Baum-Welch,这条路是最稳妥的。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询