ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Kaldi GMM-HMM训练实战:决策树绑定与强制对齐调优指南

Kaldi GMM-HMM训练实战:决策树绑定与强制对齐调优指南 简介Kaldi是由Daniel Povey团队维护的开源语音识别工具包在学术与工业界应用广泛。这份kaldi-master.zip源码压缩包适合语音识别入门者、算法工程师及研究人员深入学习GMM-HMM与HMM-GMM模型的落地实现。资源包约15.52MB文件总数与类型明细暂未提供但主体为Kaldi主分支源代码包含数据预处理、特征提取、模型训练与解码等模块源码可直接用于编译和二次开发。已有288人学习/下载。结合资源描述读者可沿着MFCC提取、EM算法参数估计、Viterbi解码这条主线理解GMM如何刻画语音特征的静态分布、HMM如何建模时序动态同时可关注balanceecd_yeth82所代表的均衡交叉熵等优化策略并延伸到SGMM、DNN-HMM等扩展模型。整份源码对搭建完整语音识别实验环境、复现经典识别流程、以及对比不同建模方案均有实际参考价值。1. 拿到 kaldi-master 自带脚本就学 GMM-HMM能省下两三周弯路打开 kaldi-master 源码包时大多数人的第一反应是翻 egs 下某个 run.sh看到 train_mono、train_deltas 一长串就把 GMM-HMM 当成“旧时代的跑腿工具”跳过去。实际它一直没有退场神经网络声学模型常用它做强制对齐TDNN 训练前也用它的对齐结果当音素级 seed更不用说想快速验证一批数据质量时GMM-HMM 是成本最低的建模方案。真正让人卡住的不是命令本身而是 HMM 状态、GMM 发射、决策树绑定这三样东西对不齐调参等于盲调。下面直接绕着 kaldi-master 里的工具链把 GMM-HMM 训练主线拆到命令和参数一级顺手处理训练数据不均衡时 GMM 会被带偏的问题最后用一个强制对齐的反向验证技巧判断模型到底学到没有。2. GMM-HMM 建模单元在 Kaldi 里的正交拆解GMM-HMM 这个复合名词里有一套清晰的职责划分HMM 负责把语音切进有限状态GMM 负责给每个状态观测到的特征打分。Kaldi 没有重新发明这两个模型它的核心贡献在决策树绑定上把 HMM 状态空间压到可训练的量级。不把这个关系弄干净后面所有脚本输出都会变成一堆没有上下文意义的数字。2.1 状态拓扑文件决定每个音素有哪几个私有状态每个音素的起始状态数写在data/lang/topo里。默认非静音音素是 3 个状态每个状态允许自跳和跳到下一个状态静音通常也多给几个状态。triphone 调参会频繁命中silprob这类静音相关参数但你首先应该看 topo 里静音节点的设计。如果数据里静音片段很多GMM 会把不少混合分量花在“不讲话的高斯”上真正语音状态的参数更新就会被稀释。常见做法是把静音状态的转移概率稍微调低或者放到最后再分裂。不要为了模仿论文在 topo 里随意增加状态状态数一变决策树叶子数跟着膨胀模型立刻变脆。cat data/lang/topo tree-info exp/tri1/tree | head -10topo 文件里每个 phone 块由Topology标签包裹TopologyEntry中的PdfClass可以允许同一状态对应多个 pdf-class。tree-info输出的num-pdfs和num-leaves如果不一致说明树里有空叶子常见原因是训练数据里缺少某些上下文组合这时优先补数据或增大混叠概率而不是去改 topo。2.2 GMM 发射概率维数、对角协方差与混合数一个 pdf-id 对应的输出概率是 M 个高斯分量的加权和公式写成P(o|s)Σ w_i N(o; μ_i, Σ_i)。Kaldi 默认用对角协方差矩阵避免满矩阵在高维特征下求逆不稳定也大幅减少参数量。脚本里的--totgauss和--max-gaussians控制 GMM 总分量数的上下界。三音素阶段从 2000 到 10000 个高斯起步是比较稳的区间超过 8 千小时数据时再往上加收益就很低了。比混合数更影响收敛的是特征拼接。--delta-order2会把 13 维 MFCC 扩展成 39 维让 GMM 感知动态变化拼接帧数太多相邻特征高度相关模型会去拟合帧间噪声。参数常用默认作用失败典型表现--totgauss1000~20000总高斯分量数训练日志中numgauss长期不变--max-gaussians自动单个叶子高斯上限内存占用陡增但准确率不涨--delta-order2动态特征阶数特征相关度过高模型发散--min-split-obs500~1000分裂节点最小观测数叶子数开始时正常中期停滞这里有个常见误用只调大--totgauss不同步检查--max-gaussians结果总高斯数被叶子数限制住根本分不上去。如果tree-info显示 num-leaves 是 3000totgauss 设 3000那每个叶子平均只有一个高斯说话人差异根本没有建模空间至少要设到叶子数的 3 到 5 倍。2.3 决策树状态绑定pdf-id 和 HMM-state 不是一回事tree文件把 HMM 状态按音素上下文问了若干语言学问题最后落到决策树叶子每个叶子对应一个 pdf-idKaldi 训练 log 里出现的数字大多是 pdf-id不是音素编号。转移概率归属在 HMM-state 层GMM 参数归属在 pdf-id 层这层映射关系决定了“调错目标”很容易发生。比如某段 log 说某个 pdf 的 acc 掉得很厉害其实该查的是那个 pdf 对应的音素上下文而不是孤立看某个音素。数据量不足时为了压低--min-split-obs反复分裂叶子会变成“某几个噪音帧专用高斯”acc 上升但真实识别不涨。遇到这种情况先看tree-info的num-roots和num-leaves比例如果叶子数远多于训练语料里的上下文组合数就把--min-split-obs拉大让稀有上下文合并到相近音素上去。3. 用 kaldi-master 里最稳的命令段跑通 GMM-HMM 训练egs 下的 run.sh 把训练和识别串成一条龙但做实验时我习惯拆成小段每段能单独重跑排错时不会被整条流水线的状态码骗过去。下面从语言目录准备开始到 triphone 训练完每一步都依赖上一步的数据格式。3.1 语言目录里的 topo 和词典是如何约束训练过程的GMM-HMM 训练必须有一个data/lang目录里面包含phones.txt、words.txt、L.fst、topo。它由词典生成命令通常是utils/prepare_lang.sh --position-dependent-phones true \ data/local/dict UNK data/local/lang data/lang--position-dependent-phones true会让每个音素按词首、词中、词尾拆分出不同 phone 条目三音素上下文的左上下文和右上下文才能落在不同 HMM 状态上。data/local/dict里必须有lexicon.txt和phones.txt前者是一行一个字词对应一串音素后者提供 silence 作为SIL。如果 lexcion 里某个词出现两个相同音素连写而 topo 按默认 3 状态建模会出现连续相同音素无法正确分配状态的问题常见解决办法是换用word-position-dependent phones或增加静音词间模型这不是特征层能补回来的。3.2 数据目录三件套 wav.scp/text/utt2spk 必须同序Kaldi 不关心目录名关心的是wav.scp、text、utt2spk三个文件能不能通过utils/validate_data_dir.sh。wav.scp提供音频路径text提供标注utt2spk提供 utterance 和说话人映射。三者的 utterance id 必须完全对应顺序可以不同但 id 不能有半点差池。mkdir -p data/train cat data/train/wav.scp EOF utt001 /audio/corpus/001.wav utt002 /audio/corpus/002.wav EOF cat data/train/text EOF utt001 你 好 世 界 utt002 今 天 天 气 不 错 EOF cat data/train/utt2spk EOF utt001 speaker_a utt002 speaker_b EOF utils/utt2spk_to_spk2utt.pl data/train/utt2spk data/train/spk2utt utils/validate_data_dir.sh --no-feats data/traintext 里的中文要按词典里的分字或分词结果用空格隔开不能直接黏在一起。wav.scp 的路径建议用绝对路径相对路径在 Kaldi 里偶尔会因sort和后续 steps 修改 cwd 而出问题。如果 validate 报错先查三件套里有没有空行或 BOM 头sed -i s/\r$//清一下再跑。3.3 特征提取与 CMVN把不同录音设备的数据拉到同一分布MFCC 默认输出 13 维加 delta 后变 39 维。Kaldi 不会自动做归一化必须显式计算 CMVN否则不同录音设备或不同人之间的均值差异会把 GMM 的高斯均值拉偏。steps/make_mfcc.sh --nj 8 --mfcc-config conf/mfcc.conf data/train exp/make_mfcc data/train/mfcc steps/compute_cmvn_stats.sh data/train exp/make_mfcc data/train/mfcc utils/fix_data_dir.sh data/train--nj是并行 job 数不要大过物理核数conf/mfcc.conf里常用--sample-frequency16000 --num-mel-bins40 --num-ceps13。compute_cmvn_stats会生成cmvn.scp训练脚本会自动读。音频采样率和 conf 不一致时make_mfcc 可能自动 resample也可能直接报 length mismatch所以要看exp/make_mfcc/log里的 WARNING不能只看 exit code。3.4 从 monophone 到 triphone三行命令按顺序执行monophone 是上下文无关模型用来产生第一版对齐triphone 在这个对齐上继续做上下文建模。steps/train_mono.sh --nj 8 --cmd $train_cmd data/train data/lang exp/mono steps/align_si.sh --nj 8 --cmd $train_cmd data/train data/lang exp/mono exp/mono_ali steps/train_deltas.sh --cmd $train_cmd 2000 10000 data/train data/lang exp/mono_ali exp/tri1train_deltas.sh后面紧跟的两个数字分别是num-leaves和tot-gauss。经验比例落在 1:3 到 1:10。第一次跑 2000 leaves、10000 gauss 足够后面再按tree-info输出决定是否加量。对齐脚本的--beam 10 --retry-beam 40属于搜索宽度不要和训练 iter 搞混beam 太小会让正确状态进不了候选最终表现为 acc 上不去而不是报错。3.5 先别加五音素读 acc 和 log 判断是否已收敛训练脚本结束会打印对齐帧准确率但这是和上一次迭代比出来的中间值。真正该看的是exp/tri1/log/下 acc 文件的最后几次差值。如果第 30 到 35 次迭代 acc 变化低于 0.1%说明三音素已经吃到当前特征的上限。此时加五音素通常没有回报更划算的是做 LDA 或 fMLLRsteps/train_lda_mllt.sh会把原始 MFCC 投影到区分性更强的子空间再用align_fmllr.sh做说话人自适应。很多实验里LDAMLLT 带来的提升比盲目把三音素换成五音素大得多。4. 参数调优与数据平衡防止不均衡数据把 GMM 估计带偏kaldi-master 的脚本默认数据分布接近自然语料但实际项目里不均衡才是常态。需要警惕的不均衡不是说话人数而是落到每个 pdf-id 上的观测帧数。GMM 靠统计更新均值帧数不足的高斯会漂移或退化最后整个状态被静音吃掉。4.1 不均衡的源头在 HMM 状态而不是声学事件一个音素出现得少它对应的几个 HMM 状态能拿到的高斯分量就少。比如某个方言数据里 z 声母只有 20 句另一个音素却有 2000 句决策树很可能把 z 绑到根节点附近。查分布最直接的办法是把已经训练好的对齐导出成音素级计数ali-to-phones exp/tri1/final.mdl ark:gunzip -c exp/tri1/ali.1.gz | ark,t:- | \ awk {print $2} | sort | uniq -c | sort -n | head -20输出里出现大量只有几十帧的 pdf 或 phone就要先补这个音素所在上下文的语料。补数据的方向不是随便加几小时语音而是找包含该音素且出现在多种左右上下文里的句子。如果没有采集条件再做数据侧增强。4.2 数据侧平衡降采样、速度扰动和合并子集过度代表的那一类可以用 Kaldi 自带的 filter 工具降采样。utils/filter_scp.pl按第二列过滤 feats.scp可以快速抽一个子集utils/filter_scp.pl -f 2 (head -5000 data/train/feats.scp) data/train/feats.scp \ data/train_subset/feats.scp utils/fix_data_dir.sh data/train_subset少数类不要简单复制原音频复制会让 GMM 在完全相同的帧上反复更新方差被低估。更稳妥的是用速度扰动生成新的特征把少数类语料以 0.9、1.0、1.1 倍速分别提一次 MFCC等于让同一句话提供三种声学变化。Kaldi 里可用utils/data/perturb_data_dir_speed.sh它会同时改 wav.scp 和 utt2spk然后重新跑 make_mfcc。最后用utils/data/combine_data.sh data/train_balanced data/train_speaker_a data/train_speaker_b合并再重新训练。4.3 训练脚本里那排“顺位锁”参数怎么设GMM-HMM 的几个训练参数不像神经网络学习率那么显眼但作用直接。--boost-silence默认 1.0调大能补偿静音帧对似然的压制但超过 2.0 时非静音状态会被漏吃log 里开始出现大片空对齐。--beam和--retry-beam控制维特比搜索宽度训练和对齐脚本里用同一对值。数据量增长后优先调--num-iters而不是 beam迭代数超过 50 基本不会再有明显提升。参数默认经验范围作用--boost-silence1.01.0~1.5提高静音转移概率--beam1010~15对齐搜索宽度--retry-beam4040~80失败重试搜索宽度--num-iters4040~50最大迭代次数数据不平衡时最容易被忽略的是--min-split-obs。少数类状态如果老在分裂边缘就把这个值调大让它们合并到更大的簇里再用更多数据加持。5. 用强制对齐结果反向验证 GMM-HMM 学到了什么训练完三音素模型先别急着接任意解码器。最省时间的验证方式是用同一份模型对测试集做强制对齐把帧级路径转成音素序列直接从错误形态反推问题。steps/align_si.sh --nj 4 data/test data/lang exp/tri1 exp/tri1_test_ali ali-to-phones --ctm-output exp/tri1_test_ali/final.mdl \ ark:gunzip -c exp/tri1_test_ali/ali.1.gz | exp/tri1_test_ali/phones.ctm head -20 exp/tri1_test_ali/phones.ctmphones.ctm里每行是说话人、utterance id、起始时间、时长和 phone 编号。用data/lang/phones.txt把编号映射回文本可以清楚看到“静音把词尾 p 吃掉”或“en 被对齐成 n”这类问题。如果静音出现在词中间多半是词典缺词或音素序列分错如果所有词尾都缺同一个音素多半是数据标注不统一。更细的对齐可视化用show-alignmentsshow-alignments exp/tri1_test_ali/final.mdl data/lang/phones.txt \ exp/tri1_test_ali/ali.1.gz | head -80从输出里能直接看到每个 HMM 状态跳转是否按 1-2-3 的路径走状态跃迁还会不会出现反向回跳。只要看到大量从状态 3 跳回状态 1 的序列说明 pdf-id 之间的区分度不够GMM 在共享相似特征。这个检查做完再决定是补数据还是上 LDA 就清楚了不用先跑完整解码工程。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进