拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Kaldi GMM-HMM语音识别训练全流程指南

简介Kaldi-master压缩包大小为15.52MB是一份围绕GMM-HMM与HMM-GMM语音识别模型的开源工具代码适合语音识别入门者、算法工程师以及需要研究Kaldi框架的开发者。压缩包当前收录文件总数为0文件类型明细暂未提供不过从源码组织方式可以直观理解Kaldi中语音特征提取、模型参数估计、解码等核心流程。已有288人学习浏览。资源内容重点讨论了GMM-HMM与HMM-GMM两种模型的区别及适用场景介绍了平衡交叉熵等优化策略在防止过拟合和应对类别不平衡时的作用同时延伸到SGMM、DNN-HMM等进阶模型方便读者沿着语音识别技术发展脉络做对照学习。通过这套资料可以系统梳理基于Kaldi的模型训练与解码思路掌握数据处理和特征构建的常用手法为实验环境搭建、算法调试及二次开发提供明确参考。1. 先说说为什么这个“老项目”还没过时刚接触语音识别的人很多会直奔端到端框架觉得 GMM-HMM 是上古产物。但如果手头拿到的是kaldi-master.zip无论你想跑通第一个示例还是复现经典基线第一站永远是 GMM-HMM。原因很简单Kaldi 的整个数据准备、特征提取、决策树绑定和 I-vector 流水线都建立在 GMM-HMM 之上它不只是一个模型更是一套工程坐标系。能在 Kaldi 里把单音素、三音素、SAT 训练跑明白的人去看今天各种端到端方案里的数据对齐、语速增强、说话人自适应都会有“原来这里是从那儿来的”的感觉。这篇文章就顺着kaldi-master.zip解压之后最常用的路径把 GMM-HMM 在 Kaldi 里的数据准备、特征计算、训练命令和排错要点串起来让新手能跟着复现也让有经验的人能对参数边界和常见误用有更准确的判断。2. Kaldi-master 的编译与数据准备GMM-HMM 训练的地基2.1 解压后第一步tool 依赖与 OpenFst拿到kaldi-master.zip先不要急着./configure。Kaldi 的构建分为 tools 和 src 两段编译前需要先把第三方依赖编译好否则后面跑 egs 示例时utils/下的脚本会各种找不到fst命令。常见做法是先安装系统级依赖比如automake autoconf libtool zlib1g-dev sox然后进入 tools 目录执行cd kaldi-master/tools/ extras/check_dependencies.sh # 检查缺失的依赖, 缺什么装什么 make -j 8 # 编译 OpenFst 等第三方工具check_dependencies.sh会明确告诉你在当前系统上还缺哪些包比手动逐个试要省事。OpenFst 编译完成后tools/openfst/下会有bin/目录包含fstcompile、fstarcsort、fsttablecompose等命令后续构成 HCLG 解码图时全部依赖这些二进制。注意make -j的并发数不要超过机器核数太多否则容易出现偶发的编译报错在不清楚依赖关系时少用-j反而是更稳定的做法。2.2 选对示例脚本从 yesno 到 thchs30kaldi-master/egs/下每个子目录代表一个语料库的完整实验脚本结构上大同小异。第一次接触时最推荐跑egs/yesno/s5它只有 60 条左右的人名语音回答 yes/no数据量小五分钟就能跑完整条 GMM-HMM 训练链。跑通之后再看egs/thchs30/s5或egs/wsj/s5它们的脚本结构基本一致只是数据量、语言模型复杂度和特征维度有所差异。启动一个示例实验前需要先确认两处一是path.sh里的KALDI_ROOT是否指向你解压的kaldi-master根目录二是run.sh里数据集的绝对路径是否正确。很多人在这一步把KALDI_ROOT指错后面所有脚本都会报找不到utils/的错误排查时却以为是训练参数写错了。2.3 数据目录四件套wav.scp、text、utt2spk、spk2uttGMM-HMM 的训练不认原始 wav 文件而是认data/train下的元数据文本由这些文本驱动特征提取。准备训练集时最关键的是四个文件data/train/ ├── wav.scp # 每行: utt_id /绝对路径/xxx.wav ├── text # 每行: utt_id 对应的发音文本(按词典分词) ├── utt2spk # 每行: utt_id speaker_id └── spk2utt # 每行: speaker_id 后跟该说话人的所有 utt_idwav.scp是 Kaldi 特有的格式第一列是句子 ID第二列可以是 wav 路径也可以是管道命令比如sph2pipe -f wav input.sph |这种设计让 Kaldi 能够直接对接非 wav 格式的原始音频。text文件不需要加标点但要严格按照词典里的词形来写数字和缩写如果不在词典中会导致后面构造 FST 时报错。在准备spk2utt时可以直接用 Kaldi 提供的工具生成不需要手写循环utils/utt2spk_to_spk2utt.pl data/train/utt2spk data/train/spk2utt utils/fix_data_dir.sh data/trainfix_data_dir.sh是一个值得养成的习惯它会检查四个文件之间的 ID 是否对齐并自动排序去重。因为spk2utt的顺序会影响后续说话人数统计建议每次增删数据后都跑一遍这个脚本再开始特征提取。3. MFCC 特征与 GMM-HMM 声学建模的输入约束3.1 为什么 GMM-HMM 需要的是 MFCC 而不是原始波形GMM-HMM 的观测概率建立在特定特征向量上而不是直接作用在采样点上。steps/make_mfcc.sh是 Kaldi 里最标准的特征提取脚本默认输出 13 维 MFCC之后通过add-deltas扩展为 39 维静态 一阶差分 二阶差分。这 39 维特征被 GMM 的对角协方差矩阵建模时计算量远小于全协方差模型这是 Kaldi 在传统基线里做得比较务实的设计——对角协方差配合 delta 拼接既保留时序信息又避免协方差矩阵求逆的开销。在低资源场景下MFCC 的--snip-edges参数需要特别留意。默认配置会在分段边界裁掉部分帧避免窗函数越界。对于短音频比如yesno那种 2 秒到 4 秒的句子snip-edges的影响不大但如果做命令词识别一个词可能只有 0.3 秒裁掉边缘帧相当于丢掉有效信息这时需要在conf/mfcc.conf里把特征保持完整# conf/mfcc.conf --sample-frequency8000 --num-mels23 --num-ceps13 --snip-edgestruenum-mels决定滤波器组数量Kaldi 默认 23在 8k 采样率下足够覆盖语音主要能量所在频段num-ceps是保留的倒谱系数数量13 是经验值。如果做远场或麦克风阵列场景num-mels可以提高到 40但 GMM-HMM 基线通常不追求极端参数保持默认更容易复现论文结果。3.2 说话人无关到说话人自适应fMLLR 在特征层的作用Kaldi 的 SAT 训练speaker adaptive training是三音素模型里比较典型的一步它并不修改 GMM-HMM 的拓扑结构而是在特征层面做线性变换。steps/train_sat.sh会先训练一个模型然后用它估计每个说话人的 fMLLR 变换矩阵再用变换后的特征重新训练模型。这个流程在代码上体现为两个关键脚本steps/train_sat.sh --cmd $train_cmd --nj 8 \ data/train data/lang data/exp/tri2 exp/tri3 steps/align_fmllr.sh --cmd $train_cmd --nj 8 \ data/train data/lang exp/tri3 exp/tri3_alitrain_sat.sh内部的第一步实际上会强制进行对齐然后估计变换。如果在训练日志里看到WARNING: posteriors are degenerate说明某些音素的帧数太少GMM 后验出现了极端分布这时要做的是检查数据时长是否足够而不是去调迭代次数。fMLLR 变换是 GMM-HMM 时代对抗说话人差异的经典手段它的思路也延续到了端到端模型的特征增强上。3.3 特征的坑能量维、CMVN 与静音段另一个容易被忽略但直接影响识别率的地方是 CMVN倒谱均值方差归一化。Kaldi 里compute-cmvn-stats计算的是整句话的统计量apply-cmvn在特征提取时逐帧去均值并缩放方差。对于长音频整句 CMVN 可以正常工作但如果一个文件里有很长的静音段CMVN 会被拉偏导致语音段特征压缩过度这时建议使用滑动窗口 CMVN窗口设置成 3 秒左右比较合理。GMM-HMM 对静音段的建模是显式的data/lang/phones里包含SIL和SP, 分别对应长静音和句间短停顿。在训练中如果静音帧被错误分配到某个说话人fMLLR 变换会被污染所以数据准备时做 VAD 修剪steps/segmentation或silence-weighted特征在强噪声场景下比在安静场景下更重要。判断是否需要 VAD最简单的办法是看训练日志里每个音素的帧数分布如果SIL占总量超过 20%就可以考虑剪掉一部分静音再重新提取特征。4. Kaldi 里 GMM-HMM 训练的四级跳mono → tri1 → tri2 → tri34.1 单音素模型初始化与 EM 迭代steps/train_mono.sh是 Kaldi 的起点它训练一个上下文无关的单音素 GMM-HMM。命令一般长这样steps/train_mono.sh --cmd $train_cmd --nj 8 \ data/train data/lang exp/mono--nj是并行 job 数需要根据机器核数和音频数量来定数据只有几十条时--nj 8没有意义反而增加调度开销。train_mono.sh内部会先计算初始对齐然后进行 40 轮 EM 迭代前 20 轮高斯分量还比较少后 20 轮逐渐加入更多分裂。日志里的Likes数值会逐步上升如果看到连续几轮 Likelihood 下降通常是数据出了脏帧或者初始对齐失败。单音素模型的高斯数量由--totgauss控制默认是 1000。在极小数据集上 1000 个高斯很容易过拟合我一般会降到 400 左右。判断依据很直接每个高斯对应多少帧如果平均不足 50 帧模型方差会退化后面做三音素扩展时基线就不稳。4.2 三音素决策树训练 deltas 模型单音素模型跑完就要做三音素建模。三音素的问题是组合爆炸所以 Kaldi 用决策树做状态绑定。steps/train_deltas.sh是第一个三音素模型steps/train_deltas.sh --cmd $train_cmd --nj 8 \ data/train data/lang exp/mono_ali exp/tri1这个过程里会用到cluster-phones和build-tree前者对音素按发音属性聚类后者根据音素上下文和问题集生成决策树。data/lang/phones/下的roots.txt定义了哪些音素共享树根初学者经常在这里出错——比如把共享静音和真实音素放在同一个根里训练几十轮后才发现所有静音状态被绑定到了一起。多数常见库的脚本已经写好了这部分不需要改动。train_deltas.sh默认--totgauss 2000、迭代 35 轮。这里有个原则三音素模型的一个状态绑定到决策树的叶子节点叶子数等于模型中的物理状态数。如果你增加了数据量不能只调totgauss还需要看tree文件里的叶子数量和final.mdl中的拓扑是否匹配。日志文件里的#pdfs就是叶子节点数一般 tri1 阶段 2000 个高斯对应 1000 多叶子节点是正常的。4.3 LDA-MLLT从“音素区分”到“特征变换”tri2 开始脱离纯 delta 特征改用 LDA 和 MLLT。steps/train_lda_mllt.sh的做法是先用 tri1 的对齐结果拼接邻接帧一般左右各 4 帧共 9 帧的 MFCC 拼接成一个高维向量再在这个高维空间里做 LDA 降维到 40 维最后再做 MLLT 全局线性变换steps/train_lda_mllt.sh --cmd $train_cmd --nj 8 \ --splice-opts --left-context3 --right-context3 \ data/train data/lang exp/tri1_ali exp/tri2splice-opts决定拼帧数左右各 3 帧共 7 帧是 Kaldi 里比较常用的配置。LDA 的目标是让投影后同类状态的距离更近不同状态的距离更远但它本质是线性变换对说话人变化无能为力所以 tri2 之后通常还要接 SAT。LDA-MLLT 阶段如果发现Accuracy提升不明显常见原因是训练数据里某些说话人录音条件差异过大干扰了类内散度矩阵的估计此时更适合提前进入 SAT而不是继续增加迭代轮数。4.4 SATfMLLR 自适应训练与三音素收尾tri3 采用train_sat.sh它先由 tri2 对齐再估计 fMLLR 变换然后在这个变换后的特征空间里训练新的模型。由于每个说话人都要估计一个变换矩阵所以数据集里说话人数不能太少否则变换矩阵容易过拟合到某个人的长期特征上。如果只有两三个说话人SAT 的效果通常不明显甚至不如直接用 tri2。训练完成后常见做法是再训练一个tri3_cleaned或tri4版本思路是先做语料清洗把对齐分数低的句子过滤掉再重新训练。steps/cleanup/clean_and_segment_data.sh可以自动做这件事但清洗比例要谨慎极端情况下会删掉 30% 的数据这时候宁可不清洗而保留数据量。4.5 训练失败时的快速诊断方法训练 GMM-HMM 时最常见的报错是gmm-align-compiled: could not open file或fsttablecompose返回非零退出码。前者通常是data/lang目录下的L.fst或G.fst缺失后者多半是arpa2fst生成的语言模型里出现了词典之外的词对应的oov符号没有正确处理。日志文件exp/tri1/log/train.1.log里的信息最有用关注三个点每轮迭代的Likes是否单调上升、#Gauss是否按预期增长、Overall准确率是否大于 0。如果出现了NaN那就是特征里有分母为零的帧检查 MFCC 配置里的--num-ceps是否大于--num-mels这是低级但常见的错误。5. 解码与调参让 GMM-HMM 真正“开口说话”5.1 解码前的语言模型与 HCLG 构图训练完成不等于能识别还需要把声学模型、词典和语言模型合成 HCLG 解码图。Kaldi 标准的构图命令是utils/format_lm.sh data/lang data/local/lm/lm.arpa \ data/local/dict/lexicon.txt data/lang_test || exit 1 utils/mkgraph.sh data/lang_test exp/tri3 exp/tri3/graphlm.arpa可以是外部训练的 n-gram 语言模型也可以用lm_build.py手机器生成。这里新手最常见的错误是直接拿训练用data/lang去mkgraph不单独建立lang_test导致测试时词典和训练不一致解码图上出现空路径。所以format_lm.sh这一步千万别省。mkgraph.sh本质上是在做有限状态转换器的复合、确定化、最小化。如果数据很小但构图耗时很长或者内存占用异常高多半是语言模型的回退结构不合理模型阶数太高可以先检查arpa文件里的 n-gram 数量。5.2 解码参数beam、acoustic scale 与静音惩罚Kaldi 的steps/decode.sh是整个流程里最值得手动调的脚本。它内部的默认参数对普通话识别往往不是最优需要根据场景微调steps/decode.sh --cmd $decode_cmd --nj 8 \ --beam 15.0 --lattice-beam 8.0 --acwt 0.0833 \ exp/tri3/graph data/test exp/tri3/decode_test--acwt是声学权重acoustic weight决定了语言模型和声学模型之间的相对权重。0.0833 对应 1/12这是 Kaldi 里比较常用的经验值。如果识别结果过度偏向短词、语音输入被截断可以把acwt调小到 0.06 左右试一下如果解码结果出现大量音近但语义不连贯的错误则把acwt往 0.1 方向调大。--beam是剪枝宽度16 到 20 是常见范围太小的值在噪声环境或语速不配合时会剪掉正确路径。静音惩罚silence penalty在 GMM-HMM 解码中容易被忽略它在mkgraph.sh的--silence-weight参数里控制。对命令词识别场景正确的做法是提高静音罚分让模型更倾向于输出非静音状态而对长句听写静音罚分太高会把停顿咬碎出现“我要 喝水”这种结果。5.3 从 lattice 到最终文本后处理与置信度解码的原始输出是 lattice 格式的图我们需要把最优路径提取出来才能得到文本。Kaldi 提供了一套完整工具lattice-best-path --acoustic-scale0.0833 \ ark:gunzip -c exp/tri3/decode_test/lat.1.gz | \ ark,t:exp/tri3/decode_test/best_path.1.ark utils/int2sym.pl -f 2- data/lang_test/words.txt \ exp/tri3/decode_test/best_path.1.ark result.txtlattice-best-path在给定声学权重的条件下找最优路径输出的是词索引序列int2sym.pl再把这些整数索引映射回文本符号。如果要对实时识别流做截断常见做法是先用时间戳约束做 VAD再对每个语音段解码最后按置信度阈值过滤结果。这里可以记一个比较实用的参数--word-ins-penalty它控制解码时对词的插入惩罚在高误识别场景比如命令词里适当提高词插入惩罚能显著减少多词错误。5.4 识别结果不理想的排查顺序当识别率不达标时不要一上来就调解码参数按下面顺序排查先检查 test 集的wav.scp的采样率和训练集是否一致用soxi查一下再检查text里是否有词典外的词用utils/validate_dict_dir.pl验证然后看 CMVN 统计量是否跨整条音频计算、测试集的cmvn.scp是否缺失最后才去看模型迭代轮数是否足够、解码 beam 是否太小。6. 数据均衡与 GMM-HMM 的边界一份可落地的补录策略yesno这类小数据集有个典型问题数据不均衡。如果 60 条样本里 yes 占 45 条、no 占 15 条GMM-HMM 训练出来的模型会明显偏向高频类解码时即使输入是 no也容易在置信度很接近的情况下输出 yes。Kaldi 本身不提供重采样器但这个操作要放在特征提取之前而且不要直接复制同一份 wav 到数据目录否则会造成训练集和验证集的隐式重复。常见的做法是数据级增强 过采样的组合。下面这段 Python 脚本演示了如何做等时长重采样同时避免直接复制原始音频import random import subprocess def oversample(src_wav, dst_wav, min_duration1.0): dur float(subprocess.check_output( [soxi, -D, src_wav], textTrue).strip()) if dur min_duration: return # 时长足够不处理 # 对短音频做变速增强变成 0.9x 和 1.1x 两种版本 for rate in [0.9, 1.1]: out dst_wav.replace(.wav, f_r{rate}.wav) subprocess.run([ sox, src_wav, out, tempo, f{rate}, pitch, f{rate*100:.0f} ], checkTrue)tempo保持音高、只改变语速pitch再做一次小幅音高搬移两者组合出来的增强样本比直接加白噪声更适合 GMM-HMM。增强完成后重新生成wav.scp、utt2spk和text把新样本的 utt_id 追加进去。这一步要在特征提取之前做否则需要重新删除data/mfcc和data/cmvn再重跑。数据均衡之后的训练可以把原来的 mono 模型删除重新训练也可以用--init-model exp/mono/final.mdl做热启动。在数据量增加不多但样本类别变均衡的情况下热启动能让模型快速收敛而且能保持之前的对齐结果。不过GMM-HMM 对数据均衡的敏感度其实低于端到端模型——它每个高斯分量独立建模类别偏斜的影响通过初始化阶段的高斯分配被部分消化所以遇到识别率偏低时先检查是不是数据时长分布的问题再决定要不要重采样。判断方法很简单训练日志里各音素的帧数占比如果yes对应音素的帧数是no的三倍以上再做均衡也不迟。最后提一个在工作流上的建议kaldi-master.zip解压后的目录是自带版本管理的每次改动脚本或数据前先在egs/你的项目/s5下新建实验编号目录比如exp/mono_v2不要去覆盖exp/mono。GMM-HMM 实验的复现重点不在于某一次的训练结果而在于对齐、特征、解码图的版本对应关系。把每个阶段的final.mdl、tree、lat.gz归档清楚比追求一次跑出最好成绩重要得多。后续如果迁移到 k2 或 wenet这套归档习惯会让你省掉大量来回校准数据的时间。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门