简介: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 到 thchs30
kaldi-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、spk2utt
GMM-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-frequency=8000 --num-mels=23 --num-ceps=13 --snip-edges=truenum-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 → tri3
4.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-context=3 --right-context=3" \ data/train data/lang exp/tri1_ali exp/tri2splice-opts决定拼帧数,左右各 3 帧共 7 帧是 Kaldi 里比较常用的配置。LDA 的目标是让投影后同类状态的距离更近,不同状态的距离更远,但它本质是线性变换,对说话人变化无能为力,所以 tri2 之后通常还要接 SAT。LDA-MLLT 阶段如果发现Accuracy提升不明显,常见原因是训练数据里某些说话人录音条件差异过大,干扰了类内散度矩阵的估计,此时更适合提前进入 SAT,而不是继续增加迭代轮数。
4.4 SAT:fMLLR 自适应训练与三音素收尾
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-scale=0.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_duration=1.0): dur = float(subprocess.check_output( ["soxi", "-D", src_wav], text=True).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}" ], check=True)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,这套归档习惯会让你省掉大量来回校准数据的时间。
本文还有配套的精品资源,点击获取