多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Kaldi语音识别实战:从环境搭建到跑通中文ASR系统

Kaldi语音识别实战:从环境搭建到跑通中文ASR系统 最近一直在折腾语音识别的东西本来想直接用现成的在线API但看了一圈要么受限于网络和并发要么就是定制性不够。翻来覆去最后还是绕回到Kaldi上。这东西虽然老但确实是绕不开的基础设施GitHub上九千多star不是白来的学术界和工业界的很多系统底层都是它。我把自己从零开始啃Kaldi的笔记整理了一下从环境搭建到跑通第一个中文识别系统再到那些网上不常写清楚的坑希望给正在入坑的朋友省点时间。这篇笔记适合两类人一类是刚接触ASR、想系统了解语音识别链路的学生或工程师另一类是已经在用其他工具比如Whisper、讯飞SDK但想深入底层原理、自己做定制训练的开发者。Kaldi的文档写得实在是让人头大我踩了不少坑才把整个流程摸顺这里尽量用大白话把关键环节讲清楚。1. 为什么现在还要学Kaldi先搞清楚工具选型先说个网上经常吵的问题都有Whisper、都有那么多在线SDK了为什么还要学Kaldi这种“老古董”1.1 Kaldi的定位不是拿来即用的产品而是研究平台Kaldi不是一个装好就能用的“语音识别软件”它更像是一套完整的语音识别研究工具箱。它给你提供的是特征提取、声学模型训练、解码器、语言模型工具链以及一套设计得相当精巧的脚本体系。你需要自己去跑训练流程、调参数甚至改代码。这跟调用讯飞或者百度的在线语音识别API完全是两码事。在线API方便但你是黑盒识别错了你不知道为什么错想针对特定场景优化也无从下手。最近看到有人在问“asr语音转文字有ubuntu的sdk包吗”其实这就是两种思路的差别——你要的是一个能离线跑的、自己能控制的识别系统不仅仅是调用云端接口。Kaldi给的就是这种自主性它支持完全离线部署不依赖外网对数据隐私敏感的场景特别重要。1.2 Kaldi、Whisper、在线SDK怎么选我做了个对比表方便你判断自己到底该学哪个方案定制性部署方式上手难度适合场景Kaldi高可改底层模型和训练流程可离线、可嵌入式高需要定制声学模型、离线部署、学术研究Whisper中可以微调但生态较重可离线需较大内存中通用场景、直接使用预训练模型在线SDK低纯黑盒必须联网低快速原型、对效果要求不苛刻所以结论很简单如果你想快速做个demo别碰Kaldi直接用在线SDK但如果你想认真做语音识别理解声学模型怎么训练、解码图怎么构建Kaldi是绕不过去的一课。最近有人在讨论“llamacpp 部署asr”说明端侧部署ASR成了新趋势而Kaldi训练出来的模型是可以转成端侧推理格式的这个技术链路Kaldi依然有价值。2. Kaldi核心概念扫盲这些不搞懂后面寸步难行我在刚开始学Kaldi的时候最大的困惑就是那么多目录、那么多脚本到底先看哪个其实核心只需要弄明白四件事数据怎么组织、特征怎么提、模型怎么训练、解码图怎么建。2.1 数据准备的核心scp、ark与Kaldi的数据目录Kaldi里最基础的数据单元不是单个文件而是一个数据目录data dir。一个典型的数据目录长这样data/train/ ├── wav.scp # 音频文件列表 ├── text # 标注文本 ├── utt2spk # 每条语音对应说话人 ├── spk2utt # 每个说话人对应哪些语音 └── ...这里wav.scp是这个系统的灵魂它的格式是utterance_id_001 /home/user/audio/001.wav utterance_id_002 /home/user/audio/002.wav注意Kaldi默认不直接读取音频文件而是通过wav.scp里的命令去读取比如你可以写成utterance_id_001 sox /home/user/audio/001.wav -t wav - rate 16k |这样就能在读取的同时重采样。这个设计非常实用因为Kaldi要求所有音频统一为16kHz采样率、16bit单声道的wav格式。我之前拿一批48kHz的录音直接跑特征提取那一步各种报错后来才意识到必须先转格式。text文件是标注文本格式是“发音编号 文本内容”比如utterance_id_001 今天天气怎么样这三个文件wav.scp、text、utt2spk是最关键的其他文件可以通过脚本生成。我在实际跑数据的时候发现很多人忽略了utt2spk的重要性如果你不提供这个文件后面算说话人相关的特征如CMVN的时候就会出问题。2.2 特征提取MFCC和Fbank为什么女声识别率比男声低特征提取是ASR的第一步也是网上热搜里“女声语音识别为什么比男声更低”这个问题的根源所在。Kaldi最常用的特征有两种MFCC梅尔频率倒谱系数和Fbank滤波器组特征。MFCC是经典特征经过DCT去相关适合GMM-HMM模型Fbank保留了更多信息适合DNN模型。特征提取的命令长这样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第一个命令生成MFCC特征第二个命令计算倒谱均值方差归一化CMVN参数。CMVN这东西很重要它能消除信道和说话人差异带来的影响如果不做这步识别率会明显下降。至于女声识别率比男声低这个问题挺有意思。本质上不是因为女声“更难识别”而是特征的分布差异女声的基频普遍比男声高谐波间隔更大导致在梅尔滤波器组Mel Filterbank的某个频段内女声携带的判别性信息相对分散。更直接的原因是训练数据不均衡——很多开源数据集男声占比天然偏高模型见过的男声更多自然在男声上表现更好。解决方式通常是数据增强或者平衡训练集Kaldi提供了speed perturbation和volume perturbation等数据增强工具# speed perturbation: 0.9x, 1.0x, 1.1x steps/data/augment_data_dir.sh --utt-suffix sp --ngroups 3 data/train data/train_sp这个操作非常简单直接把每句话加速减速各生成一份数据量立刻翻三倍。我跑了之后发现WER词错误率能下降差不多一个点性价比很高。2.3 声学模型从GMM-HMM到DNN-HMMKaldi经典的声学模型训练路径是单音素GMMmono→ 三音素GMMtri1→ LDA-MLLTtri2→ SATtri3→ DNN-HMMtri4或nnet3。一句话解释这几种模型的区别单音素模型假设每个音素是独立的不考虑上下文。这只是一个起点用来做对齐alignment生成后续训练需要的帧级别标签。三音素模型考虑一个音素的前后文context比如“a”在“b_a_c”这样一个上下文里建模成独立的“triphone state”。这是GMM时代的主流方法。LDA-MLLT对特征做线性判别分析和全局半绑定变换相当于对特征空间做一次整体旋转让类别更好分。SAT说话人自适应训练为每个说话人估计一个变换矩阵把不同说话人的特征“掰”到同一个空间。DNN-HMM用深度神经网络替换GMM来建模观测概率输入通常是拼接了前后文的Fbank特征。整个训练链路是一环扣一环的前一个模型的输出alignments是后一个模型的输入标注。所以你不能跳步必须从mono开始老老实实跑。这个过程我第一次跑的时候觉得简直是浪费时间后来才明白这叫“课程学习”curriculum learning先简单后复杂每一步都在为下一步提供更好的对齐质量。2.4 WFST解码图语言模型如何参与识别解码图是Kaldi里最难理解的部分没有之一。简单说解码图就是把声学模型、词典和语言模型三者编译成一张大的加权有限状态转换器WFST解码时在上面做搜索找到最可能的词序列。Kaldi里构建解码图的标准命令utils/format_lm.sh data/lang data/local/lm/threegram.arpa.gz data/local/dict/lexicon.txt data/lang_test理一下流程就是文本 → 语言模型n-gram→ G.fst词典 → L.fst音素上下文 → C.fstHMM状态 → H.fst。最后把HCLG四张图复合在一起得到一张大图。有个经典的眼花缭乱的命令steps/decode.sh --nj 4 --cmd run.pl exp/tri3/graph data/test exp/tri3/decode_test这个decode.sh会加载exp/tri3/graph里的HCLG.fst对测试集的每个音频做解码输出的就是识别结果。一开始我完全不理解为什么解码图要搞这么复杂后来总算悟了WFST把所有约束音素序列合法、单词拼写合法、词序列符合语言模型概率统一成了一个带权重的图搜索问题这样解码的时候不用运行时逐个检查约束全部预编译好解码速度才能快。这是Kaldi能在CPU上做到实时率RTF小于1的关键。3. 从零跑通一个中文识别系统完整实操记录光讲概念没有用我带你把一个真实的中文普通话识别系统完整跑一遍。这里我用的是开源的thchs30数据集是清华发布的中文30小时语音库体量小、适合学习把整个流程跑通大概需要半天到一天的时间。3.1 第一步编译KaldiKaldi的安装在它的GitHub仓库里有文档但有几个前置依赖是文档里写得不清楚的。我用的环境是Ubuntu 22.04依赖检查这一步尤其重要sudo apt-get update sudo apt-get install -y git make automake autoconf libtool g zlib1g-dev \ liblapack-dev libblas-dev libfst-dev python3 python3-pip sox重点说两个坑一个是libfst-dev。这是OpenFst库Kaldi依赖它来做WFST操作。Ubuntu 22.04的软件源里带了OpenFst 1.7.x版本跟Kaldi的兼容性还可以但如果你用的是其他发行版或者自己编译的OpenFst版本不一致会导致编译到latbin或fstbin相关工具时报错。另一个是BLAS/LAPACK库Kaldi既支持用系统自带的ATLAS也支持用OpenBLAS建议直接装OpenBLAS训练速度会快一些。装完依赖之后Kaldi的编译就相对顺利了git clone https://github.com/kaldi-asr/kaldi.git cd kaldi/tools make -j8 cd ../src ./configure --shared make depend -j8 make -j8./configure --shared这步建议加上--shared参数这样生成的库是动态链接的后续编译自己的工具时不用反复重新编译Kaldi源码。编译时间取决于机器配置8核机器大概半小时左右。编译完成后验证一下能否进入src/bin下运行某个工具比如./src/bin/compute-mfcc-feats --help能打印帮助信息就说明编译成功了。3.2 第二步下载thchs30数据并准备Kaldi格式thchs30的官方下载地址在openslr.org上也可以用Kaldi仓库里自带的脚本下载cd egs/thchs30/s5 local/download_and_untar.sh your_path https://www.openslr.org/resources/18 data_thchs30.tgz下载完之后把数据软链接到thchs30的s5目录下ln -s /path/to/data_thchs30 data_thchs30 ln -s /path/to/resource data/resource然后跑数据准备脚本local/thchs-30_data_prep.sh /path/to/data_thchs30 /path/to/data/resource这个脚本会生成data/local/data目录里面是整理好的wav列表、文本等中间文件然后会自动构造出data/train、data/dev、data/test三个Kaldi数据目录。你可以用utils/validate_data_dir.sh来验证数据目录格式是否正确utils/validate_data_dir.sh --no-feats data/train注意现在还没有特征文件所以要用--no-feats参数跳过特征验证。如果脚本输出#Data validation succeeded恭喜数据准备好了。3.3 第三步训练单音素模型和构建解码图这是第一个核心训练步骤steps/train_mono.sh --nj 8 --cmd run.pl data/train data/lang exp/mono参数解释一下--nj 8并行任务数。建议不超过CPU核数太大反而会因为进程切换降低效率。--cmd run.pl本地并行。如果是在集群上跑可以改成queue.pl但我们本地学习就老老实实用run.pl。data/train训练数据目录。data/lang语言目录包含词典、音素映射等是准备数据的时候自动生成的。exp/mono输出目录。单音素模型训练完后需要先构建解码图然后才能解码和评估# 构建基于单音素模型的语言模型解码图 utils/mkgraph.sh data/lang_test exp/mono exp/mono/graph # 解码测试集 steps/decode.sh --nj 8 --cmd run.pl exp/mono/graph data/test exp/mono/decode_test解码完之后评估词错误率grep WER exp/mono/decode_test/wer_*thchs30在单音素模型下的WER大概在60%左右不要被这个吓到这只是baseline。我记得我第一次看到这个数字时怀疑自己装了假的Kaldi后来才知道单音素模型本来就不强只是用来做对齐的。继续往下训练数字会一路降下来。3.4 第四步三音素模型、LDA-MLLT和SAT接下来按部就班跑三音素模型# 基于mono对齐做三音素训练 steps/align_si.sh --nj 8 --cmd run.pl data/train data/lang exp/mono exp/mono_ali steps/train_deltas.sh --cmd run.pl 2500 20000 data/train data/lang exp/mono_ali exp/tri12500 20000分别对应三音素模型的决策树叶子节点数和GMM总高斯数。这个参数选择会直接影响模型容量和训练速度。thchs30数据量小2500个叶子节点、2万高斯已经够用数据量大时通常按经验公式缩放比如每小时的语音约100个叶子节点。然后继续LDA-MLLT和SAT# tri2: LDA-MLLT steps/align_si.sh --nj 8 --cmd run.pl data/train data/lang exp/tri1 exp/tri1_ali steps/train_lda_mllt.sh --cmd run.pl 2500 20000 data/train data/lang exp/tri1_ali exp/tri2 # tri3: SAT steps/align_fmllr.sh --nj 8 --cmd run.pl data/train data/lang exp/tri2 exp/tri2_ali steps/train_sat.sh --cmd run.pl 2500 20000 data/train data/lang exp/tri2_ali exp/tri3每一轮训练完都要重新对齐和解码因为模型在变好新模型能给出更好的对齐结果而更好的对齐结果又能用于训练新模型。这个“训练-对齐-再训练-再对齐”的迭代过程是Kaldi的经典套路。跑完之后同样用utils/mkgraph.sh和steps/decode.sh做解码评估。到tri3SAT阶段thchs30测试集的WER应该能降到25%左右。3.5 第五步引入DNN-HMM如果是四五年以前接下来就是跑nnet2或者nnet3的DNN训练了。现在Kaldi官方主推的是nnet3 chain模型训练脚本是steps/nnet3/chain/train.py。但chain模型的配置脚本比较复杂涉及biphone双音素语言模型和不同的特征输入初学者容易被绕晕。这里我建议先跑简单的nnet3 DNN不涉及chain那一套复杂的配置# 先用tri3的对齐做DNN训练的数据准备 steps/align_fmllr.sh --nj 8 --cmd run.pl data/train data/lang exp/tri3 exp/tri3_ali # 用train_dnn.sh训练这是nnet3中经典的脚本 steps/nnet3/train_dnn.sh --cmd run.pl --num-epochs 3 \ --hidden-layers 4 --hidden-dim 1024 \ data/train data/lang exp/tri3_ali exp/tri3_ali exp/tri4_dnn简单说一下参数--hidden-layers 4 --hidden-dim 1024意味着4个隐藏层、每层1024个节点。对于30小时的数据这个规模不能算大但跑起来也不慢。--num-epochs 3表示训练3轮这个值可以根据效果调整。如果你想看看训练过程中损失的变化可以看exp/tri4_dnn/log/train_log这个文件。正常情况下每轮结束后损失应该下降如果第二轮到第三轮没有明显变化说明模型已经收敛可以停止训练。DNN解码和GMM时代有个显著差异DNN模型的输入特征一般用的是40维fbank而不是39维MFCC所以训练完DNN之后要重新提特征。我们需要走一步特殊的解码流程# 为DNN提fbank特征 steps/make_fbank.sh --nj 8 --cmd run.pl data/train exp/make_fbank/train fbank steps/compute_cmvn_stats.sh data/train exp/make_fbank/train fbank然后解码时用--nj 8指定并行数最后同样grep WER看结果。跑到这一步thchs30的WER能到20%左右如果加上speed perturbation等数据增强能进一步降到16%左右。对于一个30小时的小数据集这个结果还算可以接受。3.6 部署延伸怎么把Kaldi模型用到实际产品里跑完模型之后自然要考虑部署问题。Kaldi原生训练出来的模型格式是final.mdl这个格式没办法直接在Android、iOS或者嵌入式设备上跑。实际部署方案主要有三种第一种是Kaldi自带的在线解码服务比如online2-wav-nnet3-latgen-faster这个工具可以把模型跑在服务器上支持流式识别。大家搜的“asr语音转文字有ubuntu的sdk包吗”本质就是这种方案——Kaldi在Ubuntu服务器上部署成服务对外提供接口自己做SDK封装。第二种是Kaldi到端侧的转换。比如把final.mdl转成OpenFst格栅再转成其他格式或者用kaldi-native-fbank这类库在端侧做特征提娶然后把模型导出成ONNX或TensorFlow Lite格式。我曾试过把Kaldi的DNN模型导出到端侧跑用的是kaldi2onnx这类工具最后在树莓派上跑起来是OK的。最近大家讨论“llamacpp 部署asr”其实也是这个方向——把ASR模型做端侧/本地化部署绕开云端。第三种是换引擎跑Kaldi模型。有一些开源项目可以直接加载Kaldi模型做解码比如Vosk就是基于Kaldi的轻量级语音识别工具包它把Kaldi模型封装成了友好的Python/Android/iOS API。如果你在嵌入式设备比如ESP32上做语音识别Vosk有专门的轻量模型或者直接接入讯飞这类SDK。从Kaldi学到的东西在迁移到这些引擎时是通用的。4. 那些年我踩过的坑Kaldi运行中的典型问题这可能是整个笔记里含金量最高的部分。网上Kaldi教程不少但很少有人会把报错和排查过程写清楚。我把自己踩过的、以及身边朋友踩过的坑整理了一下。4.1 编译期的坑报错信息configure: error: Python not found原因Kaldi编译时需要Python开发头文件不只是Python解释器。解决方案sudo apt-get install python3-dev报错信息g: error: unrecognized command line option原因GCC版本太老或太新Kaldi源码可能存在兼容性问题。Ubuntu 22.04自带的GCC 11是可以正常编译的如果你用了更新的GCC 13某些第三方库可能不支持。解决方案是换用系统默认版本sudo apt-get install g-11 cd src make clean CXXg-11 make -j84.2 数据准备期的坑报错信息ERROR: Could not find file data/train/text原因脚本在跑数据准备时没有生成text文件。大部分情况是你的音频文件名和标注文本中的ID对不上。排查方法utils/validate_data_dir.sh data/train如果提示找不到text先看看data/local/data里有没有文本文件再确认下local/thchs-30_data_prep.sh里的路径是否指向正确。报错信息ERROR (compute-mfcc-feats): Read error on MFCC file原因音频文件解码失败可能采样率不是16kHz或格式不是wav。排查用soxi命令看一下音频信息soxi audio/001.wav采样率不是16000的话需要重采样。Kaldi在读取wav.scp时如果指定了sox命令可以在读取时重采样我之前用过一个写法utterance_id_001 sox /path/to/001.wav -t wav - rate 16000 |注意最后那个竖线表示输出到标准输出Kaldi会从管道读取。4.3 训练期的坑报错信息ERROR: Could not get lattice from latgen-faster-mapped原因解码时模型类型和输入特征维度不匹配。最常见的是用fbank特征去decode一个GMM模型而GMM模型用的是MFCC特征。解决方案务必为每个模型使用对应的特征。报错信息No space left on device原因Kaldi训练过程中会产生大量中间文件尤其是对齐和解码的lattice数据。建议提前准备足够空间thchs30全流程至少需要20GB或者训练过程中定期清理exp/*/decode_*目录里的lattice文件。报错信息Not enough memory原因并行任务数--nj开得太大或者模型参数设置太大。解决减少--nj同时检查模型参数量。我自己的经验是thchs30不建议超过8个并行任务。4.4 常见问题速查表问题可能原因解决方法编译报错缺少zlib未安装zlib开发包sudo apt-get install zlib1g-devrun.pl任务失败脚本路径不对检查是否source了path.sh解码WER过高模型未对齐或特征不匹配按流程重新对齐检查特征类型CMVN文件丢失未执行compute_cmvn_stats.sh重新生成CMVN统计文件数据目录验证失败文件格式错误或ID不一致用utils/validate_data_dir.sh定位问题GPU训练很慢batch size太小增大--num-jobs-final或提升--minibatch-size我最想提醒的就是一定要source path.sh。Kaldi的脚本全部依赖path.sh设置PATH环境变量如果你不source脚本会找不到Kaldi的工具报各种莫名其妙的错误。进入egs/thchs30/s5目录后的第一件事就是source path.sh如果重启终端后忘了source那后面所有脚本都会用系统里残留的旧工具产生的结果不可复现。最好的习惯是把source写进.bashrc里或者在每次跑实验前强制检查一下which run.pl的路径是否指向Kaldi目录。5. 一个旧框架为何还能有生命力踩完了坑回头说点掏心窝的话。Kaldi的学习曲线确实陡峭文档也不友好但它教会我的那些东西——WFST解码原理、GMM/DNN声学模型的演进逻辑、特征工程对识别效果的影响——在任何一个现代ASR工具里都能复用。正是因为它“框架感”强而不只是“API友好”逼着你把每一层原理都搞明白之后再去用Whisper或者Vosk你才看得懂它们内部在做什么。从时间投入来看完整跑通thchs30这个流程一个有一定Linux基础的人大概需要一周。这周的时间花得非常值因为你会发现以后再接触任何ASR任务心里都有一张完整的地图数据怎么处理、特征怎么提、模型怎么训、解码怎么布。当然我也要说Kaldi毕竟发展了十多年代码风格老旧、模块之间耦合度高、部分文档过时新项目想直接用它搭生产系统确实要慎重。但作为一个学习工具、一个理解ASR原理的入口它依然是最好的选择之一。如果你还在犹豫要不要学Kaldi我的建议是先去把thchs30跑通一遍不用追求多低的WER跑通流程本身就是最大的收获。跑完之后你会发现语音识别没有想象中那么神秘它就是一环扣一环的特征处理、模型训练和搜索解码每一个环节你都能动手控制——而这种“控制感”恰恰是使用在线SDK永远体会不到的。
返回列表