Kaldi语音识别模型微调实战:参数调优与避坑指南
语音识别这行当里Kaldi 算是老牌劲旅了虽然这两年端到端方案风头很盛但工业界大量落地的声学模型依然跑在 Kaldi 这套框架上。我接触过的项目里十有七八都是拿开源预训练模型做微调而不是从零训起——毕竟不是谁都有几千小时的标注数据和几十张卡。微调这件事听起来简单就是把模型拿过来接着训但实际操作里坑特别多学习率设大了模型直接崩设小了半个月不收敛数据准备格式不对训练脚本跑一半报错退出更别提那些隐藏的参数配置文档里压根没写清楚。这篇内容就是把我自己踩过的坑、试出来的参数、以及一套可复现的微调流程整理出来适合已经跑通过 Kaldi 基础流程、想用自己的数据优化模型的中阶使用者也适合刚接手语音项目、需要快速出效果的工程师参考。1. 微调之前必须想清楚的几件事1.1 为什么要微调而不是从头训练从头训练一个语音识别声学模型需要的资源远超大多数人的想象。以常用的 TDNN 或 Chain 模型为例至少需要几百到上千小时的标注语音训练周期按周计算单次实验的算力成本相当可观。而微调的核心逻辑是预训练模型已经在大规模通用语料上学到了音素、发音、声学特征的通用表示你只需要用自己的领域数据去调整模型对特定口音、特定词汇、特定录音条件的适应能力。打个比方预训练模型像一个学过标准普通话的人现在你要让他听懂带方言口音的普通话不需要重新教他说话只需要让他多听这类口音适应一下。这个适应过程就是微调。微调的优势很明显数据需求小几十小时甚至几小时就能见效训练时间短几轮迭代就能看到词错误率下降算力要求低单卡甚至 CPU 都能跑。但代价是如果你的目标领域和预训练数据差异极大比如预训练是朗读新闻你要识别的是嘈杂环境下的电话录音微调的效果会打折扣这时候可能需要考虑领域自适应或者重新训练部分层。1.2 什么情况下微调会失效我见过不少人微调之后效果反而变差常见原因有这么几个。第一是数据量太少且分布单一比如只有两三个人的录音模型会过拟合到这几个人的音色上换个人识别率暴跌。第二是标注质量差语音识别的标注必须是精确到帧级别的文本对齐如果文本有错字或者漏字模型学到的就是错误映射。第三是学习率设置不当这个后面会详细讲。第四是预训练模型和你的采样率、特征参数不匹配比如预训练用的是 16k 采样、40 维 MFCC你用的是 8k 采样、13 维 MFCC那微调基本等于重新训练。注意微调前一定要确认预训练模型的特征提取配置和你自己的数据一致包括采样率、帧长、帧移、特征类型和维度。不一致的话要么改数据要么换模型。1.3 微调策略的选择全部微调还是部分微调Kaldi 的模型通常由多个层组成底层负责提取通用声学特征高层负责具体的音素分类。微调时有两种策略一种是全部层都参与训练另一种是冻结底层只训练高层。全部微调适合数据量相对充足比如 50 小时以上、领域差异较大的场景。部分微调适合数据量少几小时到十几小时、领域差异较小的场景。实际操作中我一般会先尝试全部微调如果发现过拟合严重再考虑冻结底层。冻结底层的操作在 Kaldi 里不是直接支持的需要手动修改训练脚本把底层的学习率设为 0 或者从训练列表中排除。这个后面会给出具体做法。2. 数据准备微调的地基2.1 数据格式与目录结构Kaldi 的数据准备有一套固定的格式微调时同样要遵守。核心文件包括wav.scp、text、utt2spk、spk2utt、segments可选、utt2dur可选。这些文件的具体含义和生成方式在 Kaldi 官方文档里有详细说明这里只强调几个微调时容易出错的点。wav.scp的格式是utterance_id path_to_wav注意路径必须是绝对路径或者相对于执行目录的正确相对路径。我遇到过因为路径写错导致特征提取阶段全部失败的情况排查了半天才发现是相对路径的问题。text文件是标注文本必须和wav.scp中的 utterance_id 一一对应。文本中不能有特殊字符标点符号一般要去掉数字要转写成文字形式。如果文本里有英文单词建议统一大小写。utt2spk是 utterance 到说话人的映射微调时这个文件很重要因为要保证训练集和验证集的说话人不重叠否则验证结果会虚高。spk2utt可以由utt2spk通过utils/utt2spk_to_spk2utt.pl脚本生成。2.2 数据清洗与筛选微调的数据量通常不大所以每一条数据都很珍贵但也不能什么数据都往里放。我一般会做以下几轮筛选。第一轮是时长筛选剔除过短小于 0.5 秒和过长大于 20 秒的 utterance。过短的可能是噪音或者截断过长的可能是静音段太多都会影响训练效果。第二轮是文本筛选检查文本中是否有异常字符、空文本、纯数字等。可以用简单的脚本统计文本长度分布把明显异常的挑出来人工检查。第三轮是音频质量筛选用工具检测音频的信噪比、是否有削波、是否有长时间静音。Kaldi 自带的compute-vad或者第三方工具都可以做这件事。第四轮是说话人筛选如果某个说话人的数据特别多其他说话人特别少会导致模型偏向这个说话人。可以考虑对数据多的说话人进行下采样或者对数据少的说话人进行上采样。2.3 特征提取的参数对齐微调时特征提取的参数必须和预训练模型一致。如果你用的是预训练模型自带的特征提取脚本直接复用即可。如果需要自己提取要确认以下参数参数常见值说明sample-frequency16000采样率必须和音频一致frame-length25帧长单位毫秒frame-shift10帧移单位毫秒num-mel-bins40Mel 滤波器组数量num-ceps40MFCC 倒谱系数维度use-energyfalse是否使用能量特征这些参数在conf/mfcc.conf和conf/plp.conf中配置。如果预训练模型用的是 PLP 特征你也要用 PLP如果用的是 MFCC就用 MFCC。混用会导致模型完全无法收敛。提示不确定预训练模型用什么特征时去看它的conf目录和run.sh脚本里面会有明确记录。3. 模型加载与训练配置3.1 定位预训练模型的关键文件Kaldi 的预训练模型通常以final.mdl或final.raw的形式提供同时还有tree、phones.txt、cmvn.scp等辅助文件。微调时需要加载这些文件所以必须确保它们完整且版本匹配。final.mdl是声学模型文件包含了网络结构和参数。tree是决策树文件定义了音素到 HMM 状态的映射。phones.txt是音素列表。cmvn.scp是倒谱均值和方差归一化参数用于特征归一化。如果预训练模型是 Chain 模型还会有final.raw和den.fst等文件。Chain 模型的微调和普通 TDNN 略有不同后面会单独说明。3.2 修改训练脚本的关键参数Kaldi 的训练脚本通常是steps/nnet3/train_tdnn.sh或steps/nnet3/chain/train.py。微调时需要修改的参数主要有以下几个。--num-epochs控制训练轮数。微调时不需要太多轮一般 5 到 15 轮就够了。太多会过拟合太少效果不明显。--initial-effective-lrate和--final-effective-lrate控制学习率。微调的学习率要比从头训练小一个数量级左右。比如从头训练用 0.001微调可以用 0.0001 到 0.0005。--num-jobs-initial和--num-jobs-final控制并行任务数。根据你的 GPU 数量调整单卡就设为 1。--trainer.optimization.num-jobs类似控制优化器的并行度。--egs.opts控制训练样本的生成方式微调时一般保持默认即可。--feat.cmvn-opts控制特征归一化如果预训练模型有 cmvn 文件这里要指向它。3.3 学习率与批大小的配合学习率和批大小是微调中最关键的两个超参数。批大小决定了每次参数更新的梯度估计质量学习率决定了更新的步长。一般来说批大小越大梯度估计越稳定可以用稍大的学习率。批大小越小梯度噪声越大学习率要相应减小。Kaldi 默认的批大小是 128 或 256微调时可以保持这个值或者根据显存情况适当调整。学习率的具体设置我一般这样操作先用一个较小的学习率比如 0.0001跑一两轮观察损失下降情况。如果损失下降很慢可以适当提高如果损失震荡或者上升就要降低。Kaldi 支持学习率衰减可以设置初始学习率和最终学习率让训练过程中自动调整。注意微调时不要用太大的学习率否则会破坏预训练模型已经学到的参数导致效果比微调前还差。这个坑我踩过不止一次。4. 实操流程从零到微调完成4.1 环境准备与依赖检查假设你已经有一个可用的 Kaldi 环境并且跑通过yesno或timit这样的示例。如果没有先去 Kaldi 官方仓库按照INSTALL文件安装这里不展开。检查环境是否满足微调需求# 检查 Kaldi 是否在 PATH 中 which steps/nnet3/train_tdnn.sh # 检查 CUDA 是否可用如果用 GPU nvidia-smi # 检查预训练模型文件是否完整 ls -lh exp/pretrained_model/final.mdl ls -lh exp/pretrained_model/tree ls -lh exp/pretrained_model/phones.txt如果这些文件都存在环境基本没问题。4.2 数据准备完整流程假设你的数据放在/data/mydata目录下音频是 wav 格式标注是每行utterance_id transcript的文本文件。第一步生成wav.scp# 假设音频文件按说话人分目录存放 find /data/mydata/wav -name *.wav | awk -F/ {print $(NF-1)_$NF $0} | sed s/\.wav// data/train/wav.scp第二步生成text# 假设标注文件是 text.txt格式为 utterance_id transcript cp /data/mydata/text.txt data/train/text第三步生成utt2spk# 从 utterance_id 中提取说话人 ID假设 utterance_id 格式为 spk_utt awk {print $1 $1} data/train/text | sed s/_.* / / data/train/utt2spk第四步生成spk2uttutils/utt2spk_to_spk2utt.pl data/train/utt2spk data/train/spk2utt第五步检查数据一致性utils/validate_data_dir.sh --no-feats data/train如果这一步报错根据提示修正。常见错误包括 utterance_id 不匹配、文件路径不存在、文本为空等。4.3 特征提取与归一化数据准备完成后提取特征steps/make_mfcc.sh --nj 4 --cmd run.pl data/train exp/make_mfcc/train mfcc steps/compute_cmvn_stats.sh data/train exp/make_mfcc/train mfcc如果预训练模型用的是 PLP 特征把make_mfcc.sh换成make_plp.sh。特征提取完成后检查特征维度是否和预训练模型一致feat-to-dim scp:data/train/feats.scp -如果输出是 40而预训练模型期望的是 40那就没问题。如果不一致需要调整特征提取参数。4.4 模型初始化与训练启动微调时模型的初始化不是随机初始化而是加载预训练模型的参数。Kaldi 的train_tdnn.sh脚本支持通过--nnet3-affix或者直接指定--pretrained-model来加载预训练模型。具体做法是在训练脚本的--egs.opts中加入--egs.chunk-width等参数并在--trainer.opts中指定--trainer.input-model为预训练模型的路径。一个典型的微调命令如下steps/nnet3/train_tdnn.sh \ --cmd run.pl \ --stage 0 \ --num-epochs 10 \ --initial-effective-lrate 0.0005 \ --final-effective-lrate 0.00005 \ --num-jobs-initial 1 \ --num-jobs-final 1 \ --pretrained-model exp/pretrained_model/final.mdl \ data/train data/lang exp/tri3_ali exp/finetune这里exp/tri3_ali是对齐目录微调时需要有对齐信息。如果没有现成的对齐可以用预训练模型对训练数据做一次强制对齐。4.5 训练过程监控与日志解读训练启动后日志会输出到exp/finetune/log目录下。关键要看的指标是train_loss和valid_loss如果有验证集。正常情况下train_loss应该稳步下降valid_loss也下降但可能波动。如果train_loss下降但valid_loss上升说明过拟合了需要减少训练轮数或者增加正则化。Kaldi 还会输出progress信息包括当前轮数、学习率、处理速度等。如果处理速度突然变慢可能是数据读取瓶颈或者 GPU 显存不足。提示训练过程中可以随时用tail -f exp/finetune/log/train.1.log查看实时日志。5. 微调后的模型评估与调优5.1 解码与词错误率计算训练完成后用final.mdl进行解码计算词错误率WER。解码脚本通常是steps/nnet3/decode.sh。steps/nnet3/decode.sh \ --nj 4 \ --cmd run.pl \ --acwt 1.0 \ --post-decode-acwt 10.0 \ exp/tri3/graph data/test exp/finetune/decode_test解码完成后用local/score.sh或者steps/score_kaldi.sh计算 WERsteps/score_kaldi.sh data/test exp/tri3/graph exp/finetune/decode_testWER 的结果会输出在exp/finetune/decode_test/wer_*文件中。对比微调前的 WER如果下降了说明微调有效如果上升了需要分析原因。5.2 效果不达预期的排查思路微调后效果不好常见原因和排查方法如下表现象可能原因排查方法WER 比微调前还高学习率太大降低学习率重新训练WER 下降很少数据量不足或领域差异小增加数据或检查数据质量训练损失不下降特征不匹配或对齐错误检查特征维度和对齐文件验证集 WER 波动大验证集太小或分布不均扩大验证集或重新划分解码时报错图与模型不匹配用微调后的模型重新生成图我遇到最多的情况是学习率太大导致模型崩掉表现是训练损失先下降后突然上升然后一直震荡。这时候只能降低学习率重新来。5.3 迭代微调与多轮优化微调不是一次就能到位的通常需要多轮迭代。我的做法是第一轮用较小的学习率跑 5 轮看效果如果有效但不够好第二轮加载第一轮的模型用更小的学习率再跑几轮如果无效分析是数据问题还是参数问题。多轮微调时要注意每轮都要保存模型方便回滚。Kaldi 默认会保存每轮的模型但为了节省空间可以只保留最好的几个。另外可以尝试不同的微调策略比如先冻结底层训练高层再解冻全部层微调。这种渐进式微调有时候效果更好。6. 常见问题与避坑指南6.1 数据准备阶段的典型错误错误一utterance_id 重复。如果两个不同的音频文件用了相同的 utterance_idKaldi 会报错或者覆盖。生成wav.scp时要确保 id 唯一可以用文件名加路径哈希的方式生成。错误二文本编码问题。中文文本要用 UTF-8 编码如果文件是 GBK 编码Kaldi 读取时会乱码。可以用iconv -f GBK -t UTF-8转换。错误三音频格式不统一。有的音频是 16k 采样有的是 8k有的单声道有的立体声。Kaldi 要求统一格式可以用sox批量转换。错误四路径中有空格或特殊字符。Kaldi 的脚本对路径中的空格处理不好尽量用下划线代替。6.2 训练阶段的常见报错与解决报错No such file or directory: exp/pretrained_model/final.mdl。检查预训练模型路径是否正确文件是否存在。报错Feature dimension mismatch。特征维度和模型期望的不一致检查feat-to-dim的输出和模型的input-dim。报错Alignment file not found。对齐文件缺失需要用预训练模型重新做对齐。报错CUDA out of memory。显存不足减小批大小或者用 CPU 训练。报错Training diverged。训练发散降低学习率或者增加梯度裁剪。6.3 微调效果不佳的调优技巧如果微调后 WER 下降不明显可以尝试以下技巧。第一增加数据增强。对训练数据进行速度扰动、音量扰动、加噪等操作可以扩充数据量提升模型鲁棒性。Kaldi 支持--egs.opts中的--trainer.perturb参数。第二调整网络结构。如果数据量足够可以尝试增加网络层数或者隐藏单元数。如果数据量少反而要减小网络规模避免过拟合。第三使用更好的对齐。对齐质量直接影响微调效果可以用微调后的模型重新对齐再微调一次迭代几次。第四尝试不同的损失函数。Kaldi 支持交叉熵、LF-MMI 等多种损失函数Chain 模型用的是 LF-MMI普通模型可以用交叉熵。不同损失函数对微调效果有影响。提示微调是一个实验驱动的过程没有一套参数适合所有场景。建议做好实验记录每次只改一个变量方便对比。6.4 模型保存与版本管理微调过程中会产生多个模型文件建议做好版本管理。我的做法是每次实验建一个独立的exp目录目录名包含日期和关键参数比如exp/finetune_20240101_lr0.0005。这样方便回溯和对比。模型文件比较大如果磁盘空间有限可以只保留final.mdl和最好的几个中间模型其他的删除。但删除前要确认不再需要。另外建议把训练脚本和配置文件也保存下来方便复现。Kaldi 的run.sh脚本通常包含了完整的训练流程可以在此基础上修改。7. 进阶话题Chain 模型与端到端微调7.1 Chain 模型微调的特殊之处Chain 模型是 Kaldi 中性能较好的一种声学模型它使用 LF-MMI 损失函数和特殊的网络结构。微调 Chain 模型时除了前面提到的参数还要注意以下几点。Chain 模型的训练需要den.fst文件这个文件定义了分母图。微调时如果换领域分母图可能需要重新生成。生成方法是用微调数据重新构建解码图然后用nnet3-chain-get-egs生成训练样本。Chain 模型的学习率通常比普通 TDNN 更小因为 LF-MMI 损失对学习率更敏感。我一般用 0.0001 到 0.0002 的初始学习率。Chain 模型的输出层是final.raw而不是final.mdl加载时要注意文件类型。7.2 端到端模型的微调思路虽然 Kaldi 以传统混合模型为主但也支持一些端到端方案比如基于 CTC 的模型。端到端模型的微调和传统模型类似但有一些区别。端到端模型通常不需要对齐信息直接输入特征和文本即可训练。微调时学习率要更小因为端到端模型的参数更多更容易过拟合。端到端模型的评估通常用 CER字符错误率而不是 WER特别是中文场景。Kaldi 的端到端支持不如其他框架成熟如果要做端到端微调可能需要参考 Kaldi 的egs目录下的示例或者考虑其他框架。7.3 跨领域微调的经验分享跨领域微调是指预训练模型和微调数据的领域差异较大比如预训练是朗读语音微调是电话客服语音。这种情况下微调的难度更大需要更多技巧。首先数据量要足够。跨领域微调至少需要几十小时的数据否则模型很难适应新领域。其次学习率要更小训练轮数要更多。因为模型需要调整的参数更多需要更细致的优化。再次可以考虑多阶段微调。先用大量通用数据微调一轮再用领域数据微调一轮逐步过渡。最后评估时要分场景评估。比如电话客服场景要分别评估安静环境和嘈杂环境的 WER看模型在哪些场景下表现好哪些场景下需要进一步优化。8. 工具与脚本推荐8.1 数据准备辅助脚本Kaldi 自带了很多数据准备脚本在utils/和steps/data/目录下。常用的有utils/validate_data_dir.sh检查数据完整性utils/fix_data_dir.sh修复数据格式问题steps/data/perturb_data_dir_speed_3way.sh做速度扰动数据增强。我自己也写了一些小脚本比如批量转换音频格式的、批量检查文本编码的、统计说话人分布的。这些脚本不复杂但能省不少时间。8.2 训练监控与可视化工具Kaldi 的训练日志是纯文本的看起来不方便。可以用grep和awk提取关键指标画成曲线图。我一般用 Python 的 matplotlib 画损失曲线和学习率曲线直观判断训练状态。另外Kaldi 有nnet3-info和nnet3-am-info工具可以查看模型结构和参数统计帮助分析模型。8.3 模型对比与选择微调会产生多个模型如何选择最好的一个我的做法是在验证集上计算 WER选 WER 最低的模型。如果没有验证集可以在测试集上评估但要注意不要过拟合测试集。除了 WER还要看模型的泛化能力。有的模型在测试集上 WER 低但在实际应用中表现不稳定可能是过拟合了。这时候要结合多个测试集评估。最后模型大小和推理速度也是考虑因素。如果部署环境资源有限可能要牺牲一点 WER 换取更小的模型。9. 我个人在实际操作中的几点体会微调这件事说到底是数据和参数的博弈。数据质量决定上限参数调优决定能否逼近上限。我见过太多人把精力花在调参上却忽略了数据清洗和标注质量结果怎么调都效果不好。另一个体会是微调不要贪多。很多人想一次微调就达到完美效果结果参数调来调去反而把预训练模型的能力破坏了。我的建议是小步快跑每次只改一个变量跑几轮看效果有效就继续无效就回滚。还有一点做好实验记录。微调过程中会尝试很多组参数如果不记录很容易忘记哪组效果好、哪组不好。我一般用表格记录每次实验的数据量、学习率、轮数、WER方便对比。最后分享一个小技巧如果微调后效果不理想可以试试用微调后的模型对训练数据重新对齐然后再微调一次。这个迭代过程有时候能带来意外的提升。原理是第一次微调后模型对齐更准了第二次微调时训练样本质量更高形成正反馈。我试过几次WER 能再降几个百分点。