☰
Kaldi语音识别实战:从环境搭建到跑通中文ASR系统
2026/10/4 8:00:28 网站建设 项目流程

最近一直在折腾语音识别的东西,本来想直接用现成的在线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-HMM

Kaldi经典的声学模型训练路径是:单音素GMM(mono)→ 三音素GMM(tri1)→ LDA-MLLT(tri2)→ SAT(tri3)→ DNN-HMM(tri4或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.fst;HMM状态 → 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 第一步:编译Kaldi

Kaldi的安装在它的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/tri1

2500 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做解码评估。到tri3(SAT)阶段,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 CXX=g++-11 make -j8

4.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-dev
run.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永远体会不到的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询