下载
https://github.com/kaldi-asr/kaldi.git
cd kaldi
tools/extras/check_dependencies.sh
注意:根据提示安装相关依赖工具
cd tools
make openfst
cd tools
make cub
cd tools
make sph2pipe
cd tools
extras/install_irstlm.sh
extras/install_srilm.sh
extras/install_kaldi_lm.sh
其中安装SRILM时有两点需要注意:
第一,SRILM用于商业用途不是免费的,需要到SRILM网站注册、接收许可协议,并需要命名为srilm.tgz,放到tools文件夹下
第二,STILM的安装依赖lbfgs库,这个库的安装方法是
cd tools
extras/install_liblbfgs.sh
OpenBLAS/MKL
kaldi的最新版本已经选用MKL作为默认的矩阵运算库,如果需要手工安装OpenBLAS或者MKL,方法如下
cd tools
extras/install openblas.sh
cd src
./configure --help # 查看相关配置
# 如果编译目的实在夫妻上搭建训练环境,推荐使用编译方式
./configure --share
make #单线程编译
make -j 4 # 多线程编译
# 如果只有cpu运算,则需要在配置时加入如下选项
./configure --share --use-cuda=no
# 如果ARMv8交叉编译,则使用如下编译方式,前提是armv-8-rpi3-linux-gnueabihf工具链是可用的,同时要求OpenFst和ATLAS使用armv8-rpi3-linux-gnueabihf工具链编译并安装到/opt/cross/armv8hf
./configure --static --fst-root=/opt/cross/armv8hf --atlas-root=/opt/cross/armv8hf -host=armv8-rpi3-linux-gnueabihf
# 如果为ARM架构的Android编译,则需要加上--android-includes这个选项,因为Android NDK提供的工具链可能没有吧C++的stdlib头文件加入交叉编译路径中
./configure --static --openblas-root=/opt/cross/arm-linux-androideabi --fst-root=/opt/cross/arm-linux-androideabi --fst-version=1.4.1 --android-incdir=/opt/cross/arm-linux-androideabi/sysroot/usr/include --host=arm-linux-androideabi
运行配置工具会在src文件夹在生成kaldi.mk文件,这个文件在编译过程中会被各个子目录的编译文件引用。
# 如果kaldi代码做了修改,则可以使用如下选项来确定代码能够运行:
make test # 运行测试代码
make valgrind # 运行测试代码,检查内存泄漏
make cudavalgrinda # 运行GPU矩阵和测试代码,检查内存泄漏
make clean
make depend
make
kaldi并没有提供类型make install 的方式把所有的编译结果复制到同一个指定地点,编译结束之后,生成的可执行文件都存放在各自的代码目录下,如:bin、featbin,可以在环境变量PATH中添加这些目录以方便调用Kaldi工具
这个Perl脚本的作用是多任务地执行某个程序,这是一个非常方便的工具,可以独立于kaldi使用
utils/run.pl JOB=1:8 ./tmp/log.JOB.text echo "this is the job JOB"
四个标准文件
wav.scp
utt2spk
spk2utt
text
训练集和测试集所在路径-data/test_yesno和data/train_yesno
test_yesno
spk2utt
记录说话人说的每个ID
说话人id->语音id
global 0_0_0_0_1_1_1_1 0_0_0_1_0_0_0_1 0_0_0_1_0_1_1_0 0_0_1_0_0_0_1_0 0_0_1_0_0_1_1_0 0_0_1_0_0_1_1_1 0_0_1_0_1_0_0_0 0_0_1_0_1_0_0_1 0_0_1_0_1_0_1_1 0_0_1_1_0_0_0_1 0_0_1_1_0_1_0_0 0_0_1_1_0_1_1_0 0_0_1_1_0_1_1_1 0_0_1_1_1_0_0_0 0_0_1_1_1_0_0_1 0_0_1_1_1_1_0_0 0_0_1_1_1_1_1_0 0_1_0_0_0_1_0_0 0_1_0_0_0_1_1_0 0_1_0_0_1_0_1_0 0_1_0_0_1_0_1_1 0_1_0_1_0_0_0_0 0_1_0_1_1_0_1_0 0_1_0_1_1_1_0_0 0_1_1_0_0_1_1_0 0_1_1_0_0_1_1_1 0_1_1_1_0_0_0_0 0_1_1_1_0_0_1_0 0_1_1_1_0_1_0_1 0_1_1_1_1_0_1_0 0_1_1_1_1_1_1_1
text
记录每个ID的文本内容
语音id->语音的内容
0_0_0_0_1_1_1_1 NO NO NO NO YES YES YES YES
0_0_0_1_0_0_0_1 NO NO NO YES NO NO NO YES
0_0_0_1_0_1_1_0 NO NO NO YES NO YES YES NO
utt2spk
记录每个ID的说话人信息
语音id -> 说话人id
0_0_0_0_1_1_1_1 global
0_0_0_1_0_0_0_1 global
0_0_0_1_0_1_1_0 global
wav.scp
记录每个ID的音频文件路径
语音id -> 语音id所对应的文件路径
0_0_0_0_1_1_1_1 waves_yesno/0_0_0_0_1_1_1_1.wav
0_0_0_1_0_0_0_1 waves_yesno/0_0_0_1_0_0_0_1.wav
0_0_0_1_0_1_1_0 waves_yesno/0_0_0_1_0_1_1_0.wav
train_yesno
生成的这两个目录使用的是Kaldi的标准数据文件夹格式,每个句子都没有指定了一个唯一的id
作用
列表表单 用于索引存储于磁盘或内存中的文件
在Kaldi通用脚本中,这类表单默认以.scp
为扩展名,但对于Kaldi可执行程序来说并没有扩展名的限制
file1_index /path/to/file1
file2_index /path/to/file2
可以是磁盘中的物理地址
也可以是以管道形式的内存地址
file1_index gunzip -c /path/to/file1.gz |
file2_index gunzip -c /path/to/file2.gz |
从管道文件和偏移定位符可以看出,文件定位符定义的“文件”,本质是上一个存储地址,这个地址可能是一个外部磁盘的物理地址,也可能是管道指向的内存地址,还可能是从一个磁盘文件中的某个字节开始的地址。
无论哪种形式,列表表单的元素一定是“文件”
存档表单用于存储数据,数据可以是文本数据,也可以是二进制数据
这类表单通常默认以.ark为扩展名,但没有严格限制
存档表单没有行的概念,存档表单的元素直接没有间隔符,对于文本类型的存档文件来说,需要保证每个元素都以换行符结尾
text_index1 this is first text\text_index2 this is second text\n
索引以每个字符对于的ASCII值存储,然后是一个空格,接下来是“\0B”,这个标志位是区别文本和二进制内容 的重要标识
紧接着是二进制的表单元素,直至下一个索引
可以通过内容本身判断这个元素占用的空间大小,这个信息保存在一段文件头中
binary_index1 \0Bbinary_index2 \0B
读声明符和谐声明符定义了可执行程序处理输入表单文件和输出表单文件的方式,他们都是有两部分组成
这两部分都冒号组合在一起
他们可以接受的表单文件名如下:
# 参数1:
# 读声明符
# 表单属性: scp:
# 表单文件名: path/file1
# 参数2:写声明符 ark,t:path/utt2dur
cmd scp:path/file1 ark,t:path/utt2dur
表单类型:标识符为scp或ark,这个属性定义了输出表单文件的类型
scp是列表表单
ark是存档表单
同时输出一个存档表单和一个列表表单,必须ark在前scp在后
ark,scp:/path/archiver.ark,/path/archive.scp
二进制模式:标识符为b,表示将输出表单保存为二进制文件,只对输出存档表单生效
文本模式:标识符为t,表示输出的表单保存为文本文件,只对输出存档表单生效
刷新模式:标识符为f,表示刷新,标识符为nf,表示不刷新,用于确定在每次写操作后是否刷新数据流,默认是刷新
宽容模式:标识符为p,只对输出列表生效。在同时输出存档表单和列表表单时,如果表单的某个元素对应的存档内容无法获取,那么在列表表单中直接跳过这个元素,不提示错误
表单类型:标识符为scp或ark,输入表单文件的类型,无法在输入时同时定义一个存档表单和列表表单,只能输入一个表单文件,当同时输入多个表单时,可以通过多个读声明符实现
单次访问:标识符为o,标识符no为多次访问,告知可执行程序在读入表单中每个索引值出现一次,不会出现多个元素使用同一个索引的情况
有序表单:标识符为s,告知可执行程序元素的索引是有序的,ns是无序的
有序访问:标识符是cs或ncs,字面含义与有序表单属性的含义类似。这个属性的含义是,告知可执行程序表单中的元素将被顺序访问
二进制模式:标识符为b,表示将输出表单保存为二进制文件,只对输出存档表单生效
文本模式:标识符为t,表示输出的表单保存为文本文件,只对输出存档表单生效
刷新模式:标识符为f,表示刷新,标识符为nf,表示不刷新,用于确定在每次写操作后是否刷新数据流,默认是刷新
宽容模式:标识符为p,只对输出列表生效。在同时输出存档表单和列表表单时,如果表单的某个元素对应的存档内容无法获取,那么在列表表单中直接跳过这个元素,不提示错误
可以把命令输出到管道,通过管道作为表单文件
# scp echo 'utt1 data/103-1240-0000.wav |' 读声明符
# echo 'utt1 data/103-1240-0000.wav' 输出一个表单
# 表单组成: "scp:[磁盘路径、标准输入-、管道符号|、磁盘路径夹偏移定位符]"
# 表单组成: "ark:[磁盘路径、标准输入-、管道符号|、磁盘路径夹偏移定位符]"
wav-to-duration "scp:echo 'utt1 data/103-1240-0000.wav' |" ark,t:-
多个读入文件,和多个输出文件,读入文件只能是单个类型的表单,输出可以是多种类型的表单
# 读声明符1 "ark:compute-mfcc scp:wav1.scp ark:- |",
# 读声明符2 "ark:compute-pitch scp:wav2.scp ark:- |"
# 写声明符:输输出多个文件feats.ark,feats.scp:ark,scp:feats.ark,feats.scp
paste-feats "ark:compute-mfcc scp:wav1.scp ark:- |" "ark:compute-pitch scp:wav2.scp ark:- |" ark,scp:feats.ark,feats.scp
给出了声学模型训练数据的描述,其中文本标注是以词为单位的
说话人映射表单
文件名为:utt2spk、spk2utt
存放的是文本内容,一个句子到说话的映射,以及说话人到句子的映射
103-1240-0000 103-1240
103-1240-0001 103-1240
103-1240-0002 103-1240
103-1240-0003 103-1240
...
标注文本表单
切分信息表单
切分信息表单文件名为:segments
kaldi处理的数据是以句子为单位,如果音频文件没有按句切分,就需要将音频中的每一句的起止时间记录在segments文件中。
103-1240-0000 103-1240 2.81 6.41
103-1240-0001 103-1240 9.74 12.62
103-1240-0003 103-1240 15.27 24.23
...
VTLN相关系数表单
句子时长表单
在kaldi的数据文件夹中常见的表单内容,其中需要自行准备,保存wav.scp、text和utt2spk,其它的文件都可以通过kaldi通用脚本生成
脚本名称 | 功能简介 |
---|---|
combine-data.sh | 将多个数据文件夹合并为一个,并合并对应的表单 |
combine_short_segments.sh | 合并原来文件夹的短句,创建一个新的数据文件夹 |
copy_data_dir.sh | 复制原文件夹,创建一个新的数据文件夹,可以指定说话人或句子的前缀。后缀,复制一部分数据 |
extract_wav_segments_data_dir.sh | 利用原文件夹中的分段信息,切分音频文件,并保存为一个新的 数据文件夹 |
fix_data_dir.sh | 为原文件夹保留一个备份,删除没有同时出现在多个表单中的句子,并修正排序 |
get_frame_shift.sh | 获取数据文件夹的帧移信息,打印到屏幕 |
get_num_frames.sh | 获取数据文件夹的总帧移信息,打印到屏幕 |
get_segments_for_data.sh | 获取音频时长信息,转为segments文件 |
get_utt2dur.sh | 获取音频时长信息,生成 utt2dur 文件 |
limit feature dim.sh | 根据原数据文件夾中的 feats. scp,取其部分维度的声学特征,保存到新创建的数据文件夹中 |
modify_speaker _info.sh | 修改原数据文件夹中的说话人索引,构造“伪说话人”,保存到新创建的数据文件夹中 |
perturb_ data_ dir _speed.sh | 为原数据文件夹创建一个速度扰动的副本 |
perturb data dir volume.sh | 修改数据文件夹中的 wav.scp 文件,添加音量扰动效果 |
remove_ dup_utts.sh | 刪除原数据文件夹中文本内容重复超过指定次数的句子,保存到新创建的数据文件夹中 |
resample data dir.sh | 修改数据文件夹中的 wav.scp 文件,修改音频采样率 |
shift feats.sh | 根据原数据文件夹中的 feats.scp 进行特征偏移,保存到新创建的数据文件夹中 |
split data.sh | 将数据文件夹分成指定数目的多个子集,保存在原数据文件夹中以 split 开头的目录下 |
subsegment data dir.sh | 根据一个额外提供的切分信息文件,将原数据文件夹重新切分,创建一个重切分的数据文件夹 |
subset data dir.sh | 根据指定的方法,创建一个原数据文件夹的子集,保存为新创建的数据文件夹 |
validate data dir.sh | 检查给定数据文件夹的内容,包括排序是否正确、 元素索引是否对应等 |
在开始训练声学模型之前,需要定义发音词典、音素集和HMM的结构
在进行音素上下文聚类的时候,还可以通过制定聚类问题的方式融入先验知识。
包括了发音词典与音素集,一般保存文件名为:dict
在下载数据阶段,还下载了预先整理好的发音词典和语言模型,以及语言模型的训练数据,
用于生成L.fst,,发音词典的fst:四个文件
lexiconp.txt、nonsilence_phones.txt、optional_silence.txt、silence_phones.txt
# 生成dict文件夹
# lexiconp.txt 概率音素词典
# lexicon.txt 音素词典
# lexicon_words.txt 音素词典
# nonsilence_phones.txt 非静音音素
# optional_silence.txt 可选音素 sil
# silence_phones.txt 静音音素 sil
local/prepare_dict.sh
lexicon.txt
SIL
!SIL SIL 表示静音,其发音是静音音素
SPN 表示噪声和集外词,其发音都是SPN
SPN
YES Y
NO N
给出了YES、NO和这三个单词的音素序列,其中、是一个特殊单词,表示静音
lexicon_nosil.txt
和lexicon.txt文件相同,只是去掉了
YES Y
NO N
phones.txt
给出了音素集
SIL
Y
N
silence_phones.txt
所有可以用来表示无效语音内容的音素
SIL
SPN # 表示有声音但是无法识别的声音片段
optional_silence.txt
通过词典文件夹,生成语言文件夹,L.fst
L_disambig.fst # 增加消歧之后的发音词典生成的FST
L.fst # 增加消歧之前的发音词典生成的FST
oov.int # 集外词
oov.txt # 集外词
phones # 定义了关于音素的各种属性,音素上下文无关、聚类时共享根节点
phones.txt # 音素索引
topo # HMM拓扑结构
words.txt # 词索引
phones.txt和words.txt,分别定义了音素索引和词索引
集外词:无法被识别的
静音词、噪声词
!SIL SIL 表示静音,其发音是静音音素
SPN 表示噪声和集外词,其发音都是SPN
SPN
数据文件夹生成后,就可以根据其中的文本信息,以及事先准备好的发音词典等文件,生成语言模型文件夹
# 生成L.fst
utils/prepare_lang.sh --position-dependent-phones false data/local/dict “<SIL>” data/local/lang data/lang
通过语料text,每句话的标注文本文件,生成语言模型,即3-ngram
task.arpabo
是语音模型
可以通过第三方工具和语料直接得到
\data\
ngram 1=4
\1-grams:
-1 NO
-1 YES
-99
-1
准备文件
text
BAC009S0002W0122 而 对 楼市 成交 抑制 作用 最 大 的 限 购
BAC009S0002W0123 也 成为 地方 政府 的 眼中 钉
BAC009S0002W0124 自 六月 底 呼和浩特 市 率先 宣布 取消 限 购 后
BAC009S0002W0125 各地 政府 便 纷纷 跟进
BAC009S0002W0126 仅 一 个 多 月 的 时间 里
BAC009S0002W0127 除了 北京 上海 广州 深圳 四 个 一 线 城市 和 三亚 之外
BAC009S0002W0128 四十六 个 限 购 城市 当中
BAC009S0002W0129 四十一 个 已 正式 取消 或 变相 放松 了 限 购
BAC009S0002W0130 财政 金融 政策 紧随 其后 而来
BAC009S0002W0131 显示 出 了 极 强 的 威力
BAC009S0002W0132 放松 了 与 自 往 需求 密切 相关 的 房贷 政策
BAC009S0002W0133 其中 包括 对 拥有 一 套住 房 并 已 结清 相应 购房 贷款 的 家庭
BAC009S0002W0134 为 改善 居住 条件 再次 申请 贷款 购买 普通 商品 住房
BAC009S0002W0135 银行 业金 融机 构 执行 首套 房贷 款 政策
...
lexicon.txt
SIL sil
sil
啊 aa a1
啊 aa a2
啊 aa a4
啊 aa a5
啊啊啊 aa a2 aa a2 aa a2
啊啊啊 aa a5 aa a5 aa a5
阿 aa a1
阿 ee e1
阿尔 aa a1 ee er3
阿根廷 aa a1 g en1 t ing2
阿九 aa a1 j iu3
阿克 aa a1 k e4
阿拉伯数字 aa a1 l a1 b o2 sh u4 z iy4
阿拉法特 aa a1 l a1 f a3 t e4
阿拉木图 aa a1 l a1 m u4 t u2
阿婆 aa a1 p o2
...
脚本
# 生成LM
local/prepare_lm.sh
声学分的固有分,即下一个单词出现的概率
通过L.fst和G.fst可以合成LG.fst,音素到词的fst,即输入是音素,输出是词的wfst——加权有限状态机
音素与音素之间也有概率转移,lexconp.txt文件
词与词之间也有概率转移
概率转移即使加权
事实上,我们人类的听觉器是通过频域而不是波形来辨别声音的,把声音进行短时傅里叶变换(STFT),就得到了声音的频谱。因此我们以帧为单位,依据听觉感知机理,按需调整声音片段频谱中各个成分的幅值,并将其参数化,得到适合表示语音信号特性的向量,这就是声学特征(Acoustic Feature)
把波形分成若干离散的帧,整个波形可以看做是一个矩阵。
波形被分为了很多帧,每一帧都用一个12维的向量表示,色块的颜色深浅表示向量值的大小。
梅尔频率倒谱系数(MFCCs)是最常见的声学特征
compute-mfcc-feats # 提取mfcc的脚本
FilterBank也叫FBank,是不做DCT的MFCCs,保留了特征维间的相关性,再用卷积神经网络作为声学模型时,通常选用FBank作为特征
compute-fbank-feats # 提取fbank的脚本
PLP特征提取字线性预测系数(Linear Prediction Coefficient,LPC)
compute-plp-feats # 提取plp的脚本
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v6DfXaoB-1648518966054)(assets/v2-1150511699482f0b4b2bd255bcd024f2_r.png)]
这是训练声学模型的前提,特征提取需要读取配置文件,默认的配置文件路径是当前调用路径下的conf/mfcc.conf,也可以通过–mfcc-config选项来指定
for x in train_yesno test_yesno;do
# mfcc 提取音频特征
steps/make_mfcc.sh --nj 1 data/$x exp/make_mfcc/$x mfcc
steps/compute_cmvn_stats.sh data/$x exp/make_mfcc/$x mfcc
utils/fix_data_dir.sh data/$x
done
特征提取的输出就是声学特征表单和用于保存声学特征的二进制文档
生成cmvn(Cepstral Mean and Variance Normalization,CMVN)
steps/compute_cmvn_stats.sh data/$x exp/make_mfcc/$x mfcc
该表单的元素以说话人为索引,每个方括号内是其对应的倒谱均值方差归一化系数,一个均值归一化,一个是方差归一化,以使得模型的输入特征趋于正态分布,这对于与说话人无关的声学模型建模非常重要。
copy-matrix ark:mfcc/cmvn_train.ark ark,t:- # 查看cmvn 倒谱均值方差归一化
特征提取完成之后,可以通过数据文件夹中的声学特征表单feats.scp和倒谱均值方差归一化系数表单cmvn.scp获取归一化的特征。在训练声学模型时,通常还要对特征做更多的扩展,例如kaldi的单音子模型训练,在谱归一化的基础上做了差分系数(Delta)扩展
mfcc->cmvn->delta
无监督特征变换:差分(Delta)、拼帧(Splicing) 和归一化(Normalize)
差分:即在一定的窗长内,计算前后帧的差分特征,补充到当前特征上。
src/featbin/add-deltas scp:data/train/feats.ark \
ark,scp:data/ train/feats_delta.ark,data/train/feats_delta.scp
拼帧:即在一定的窗长内,将前后若干帧拼接成一帧特征
sec/featbin/splice-feats scp:data/feats.ark \
ark,scp:data/feats_splice.ark,data/teats_splice.scp
归一化:通常被称为倒谱均值方差归一化,使其符合正太分布。
#估计CMVN系数
src/featbin/compute-cmvn0stats scp:data/train/feats.ark \
ark,scp:data/train/cmvn.ark,data/train/cmvn.scp
# 应用CMVN进行特征变换
src/featbin/apply-cmvn scp:data/train/cmvn.scp scp:data/train/train/feats.ark \
ark,scp:data/train/feats_cmvn.ark,data/trian/feats_cmvn.scp
有监督特征变换:有监督特征变换借助标注信息,估计一组变换系数,增强输入特征的表征能力,有助于提升声学模型的建模能力。
在语音识别中特征变换矩阵的估计方法主要分为两大类,线性判别分析(LDA)和最大似然线性变换(MLLT)。
LDA的目的是通过变换来减少同类特征间的方差,增加不同类特征之间 方差,这里的类指的是声学模型的状态。
是一类变换技术的统称,
均值最大线性自然回归(MeanMLLR),方差最大线性自然回归(VarMLLR),针对模型参数进行变换
报错半绑定协方差(STC),和特征最大似然线性回归(FMLLR),针对特征进行变换的技术
steps/train_lda_mllt.sh
steps/train_sat.sh
在中文语音识别中还常用基频
脚本名 | 作用 | 配置文件(conf文件夹下) |
---|---|---|
make_mfcc.sh | 提取mfcc加基频特征 | mfcc.conf |
make_mfcc_pitch.sh | 提取mfcc加基频特征 | mfcc.conf pitch.conf |
make_mfcc_pitch_online.sh | 提取mfcc加在线基频特征 | mfcc.conf,pitch_online.conf |
make_fbank.sh | 提取fbank特征 | fbank.conf |
make_fbank_pitch.sh | 提取fbank加基频特征 | fbank.conf,pitch.conf |
make_plp.sh | 提取plp特征 | plp.conf |
make_plp_pitch.sh | 提取plt加基频特征 | plp.conf,pitch.conf |
在训练时候的特征 和 预测时候的特征是有偏差的,采用GMM-HMM的声学模型,没有NN-HMM的模型泛化能力强。
做好了前面的各项准备工作,就可以开始训练声学模型(Acoustic Model,AM)
基本的模型结构:使用高斯混合模型(GMM)描述单因子(Monophone)发音转台的概率分布函数(PDF)的HMM模型
一个声学模型就是一组HMM,一个HMM的参数是有初始概率,转移概率,观察概率三部分构成。
对于语音识别框架中的声学模型的每一个HMM,都应当定义该HMM中有多少个状态,以及各个状态起始的马尔科夫链的初始化概率,个状态间的转移概率以及每个状态的概率分布函数。
根据声学模型,可以计算某一帧声学特征在某一个状态上的声学分(AM score)
指的是该 帧声学特征 对于该 状态的 对数观察概率, 或者成为对数似然值(log-likelihood):
A m S c o r e ( t , i ) = l o g P ( o t ∣ s i ) AmScore(t,i) = logP(o_t|s_i) AmScore(t,i)=logP(ot∣si)
在上式子中,是第t帧语音声学特征 o t o_t ot在状态 s i s_i si上的声学分
用于GMM建模观察概率分布的函数如下:
l o g P ( o t ∣ s i ) = l o g ( ∑ m = 1 M c i , m e x p ( − 1 2 ( o t − u i , m ) T ( ∑ i , m − 1 ) ( o t − u i , m ) ) ( 2 π ) D 2 ∣ ∑ i , m ∣ 1 2 ) logP(o_t|s_i)=log(\sum^{M}_{m=1}\frac{c_i,_mexp(-\frac{1}{2}(o_t-u_i,_m)^T(\sum^{-1}_{i,m})(o_t-u_i,_m))}{(2\pi)^{\frac{D}{2}}|\sum{}_{i,m}|^{\frac{1}{2}}}) logP(ot∣si)=log(∑m=1M(2π)2D∣∑i,m∣21ci,mexp(−21(ot−ui,m)T(∑i,m−1)(ot−ui,m)))
一个GMM-HMM模型存储的主要参数为各状态和高斯分类的 u i , m 、 ρ i , m u_{i,m}、\rho_{i,m} ui,m、ρi,m和 c i , m c_{i,m} ci,m。
gmm-copy --binary=false final.mdl final.mdl.txt
识别的过程就是语音的特征的序列特征取匹配一个状态图,搜索最优路径。
状态图中有无数条路径,每条路径代表一种可能的识别结果,且都有一个分数,该分数表示语音和该识别结果的匹配程度。
判断两条路径的优劣就是比较这两条路径的的分数,分数高的路径更有,即高分路径上的识别结果和声音更匹配。
这个基础模型的每个状态只有一个高斯分类,在后续的训练过程中,会进行单高斯分量到混合多高斯分量的分裂。
# HMM topo结构
# 声学特征维数
# 初始化声学模型
gmm-init-mono topo 39 mono.mdl mono.tree
获取帧级别的标注,通过下面的工具
compile-train-graphs # 输出一个状态图
gmm-align # 内部调用了FasterDecoder,解码器来完成对齐
gmm-align-compiled # 对训练数据进行反复对齐
transition模型存储于kaldi声学模型的头部
<TransitionModel>
<TopologyEntry>
# 第一部分
</TopologyEntry>
<Triples>
# 第二部分
<音素索引,HMM状态索引,PDF索引>
</Triples>
</TransitionModel>
transition-state对这些状态从0开始编号,
这样就得到了transition-index,把(transition-state,transition-index)作为一个二元组并从1开始编号,该编号就被称为transition-id
$ show-transitions phones.txt mono.mdl
Transition-state 1:phone = a hmm-state=0 pdf=0
Transition-id=1 p=0.75 [self-loop]
Transition-id=2 p=0.25 [0>1]
Transition-state 2:phone = a hmm-state=1 pdf=1
Transition-id=3 p=0.75 [self-loop]
Transition-id=4 p=0.25 [1>2]
Transition-state 3:phone = a hmm-state=2 pdf=2
Transition-id=5 p=0.75 [self-loop]
Transition-id=6 p=0.25 [2>3]
Transition-state 4:phone = a hmm-state=0 pdf=3
Transition-id=7 p=0.75 [self-loop]
Transition-id=8 p=0.25 [0>1]
Transition-state 5:phone = b hmm-state=1 pdf=4
Transition-id=9 p=0.75 [self-loop]
Transition-id=10 p=0.25 [1>2]
Transition-state 6:phone = a hmm-state=2 pdf=5
Transition-id=11 p=0.75 [self-loop]
Transition-id=12 p=0.25 [2>3]
transition-state:可以理解为是fst图的状态节点
transition-id:可以理解为fst的弧
相比 transition-id, pdfid 似乎是表示 HMM 状态更直观的方式,为什么 Kaldi要定义这样烦琐的编号方式呢?这是考虑到 paf-id 不能唯一地映射成音素,而transition id 可以。如果直接使用 paf-id 构建状态图,固然可以正常解码并得到 pdf-id序列作为状态级解码结果,但难以从解码结果中得知各个pdf-id 对应哪个音素,也就无法得到音素级的识别结果了,因此 Kaldi 使用 transition-id 表示对齐的结果。
声学模型训练需要对齐结果,而对齐过程又需要声学模型,这看起来是一个鸡生蛋蛋生鸡的问题
Kaldi采取了一种更加简单粗暴的方式进行首次对齐,即直接把训练样本按该句的状态个数平均分段,认为每段对应相应的状态
align-equal-compiled # 对齐结果
对齐结果作为gmm-acc-stats-ali的输入。
# 输入一个初始模型:gmm-init-mono得到、
# 训练数据、
# 对齐结果
# 输出用于GMM模型参数更新的ACC文件
gmm-acc-stats-ali 1.mdl scp:train.scp ark:1.ali 1.acc
acc文件存储了GMM在EM训练中所需要的统计量。
生成ACC文件后,可以使用gmm-est工具来更新GMM模型参数
gmm-est
每次模型参数的迭代都需要成对使用这两个工具
gmm-acc-stats-ali
gmm-est
单音子作为建模单元的语音识别模型机器训练,在实际使用中,单音子模型过于简单,往往不能达到最好识别性能。
Content Dependent Acoustic Model
描述的是一个音素模型实例取决于实例中心音素、左相邻音素和右相邻音素,共三个音素。
和HMM三状态要区分清楚,一个音素模型实例内部有三个HMM状态组成,在概念上不同的HMM状态用来分别捕捉该音素发音时启动、平滑、衰落等动态变化。
无论是单音子还是三音子,通常使用三状态HMM结构来建模
单音子模型到三音子模型的扩展,虽然解决了语言学中协同发音等上下文的问题。但也带来了另一个问题,模型参数数据“爆炸”。
将所有的三音子模型放到一起进行相似性聚类,发音相似的三音子被聚类到同一个模型,共享参数,通过人为控制聚类算法最终的类的个数,可以有效的减少整个系统中实际的模型个数,同时又兼顾解决了单音子假设无效的问题。
具体实现:通过决策树算法,将所有需要建模的三音子的HMM状态放到决策树的根节点中,作为基类。
和单音子训练流程一样,训练之后用生成的模型对训练数据重新进行对齐,作为后续系统的基础
三音子训练模型的脚本功能又train_deltas.sh完成
steps/train_deltas.sh <num-leaves叶子数量> <tot-gauss高斯数量> <data-dir训练数据> <lang-dir语言词典等资源> <alignment-dir单音子模型产生的对齐文件> <exp-dir生成训练的的三音子模型>
steps/train_deltas.sh 2000 10000 data/train data/lang exp/mono_ali exp/tri
问题集:通过yes、no的形式进行提问
语音识别的过程是在解码空间中衡量和评估所有的路径,将打分最高的路径代表的识别结果作为最终的识别结果。传统的最大似然训练是使正确路径的分数尽可能高,而区分性训练,则着眼于加大这些路径直接的打分差异,不仅要使正确路径的分数尽可能的高,还要使错误路径,尤其是易混淆路径的分数尽可能的低,这就是区分性训练的核心思想。
N元文法语言模型:ARPA
词与词之间的跳转,权重是语言模型
# 对APRA格式的语言模型文件解压后,直接输入到arpa2fst程序中,就得到目标G.fst
gunzip -c n.arpa.gz | arpa2fst --disambig-symbol=#0 \
--read-symbol-table=words.txt - G.fst
单音子与词之间的跳转,权重是音素词典概率
prepare_lang.sh
Compose
生成LG.fst
音素到单词的转录机
得到C之后,将C和LG复合,就得到了CLG。CLG把音素上下文序列转录为单词序列
fstmakecontextfst ilabels.sym <LG.fst> CLG.fst
实际上,并不是任意单音子的组合都是有意义的,在kaldi的实现中,并不去真正地构建完整的C,而是根据LG一边动态构建局部C,一边和LG复合,避免不必要地生成C的全部状态和跳转。
C的输入标签:是状态
输出标签是:音素对应的id
权重是:左侧音素和右侧音素
在生成从HMM状态到单词的转录机,之前需要有 从上下文音素到单词的转录机。
首先把HMM模型的拓扑结构以及转移概率构成的WFST,这个WFST习惯上被简称为H
输入标签是HMM状态号
输入出标签是C中的ilabel
跳转权重是转移概率
# 构建H的工具
make-h-transducer
## 构造G
arpa2fst --natural-base=false lm.arpa |\
fstprint | esp2disambig.pl | s2eps.pl |\
fstcompile -isymbols=map_word --osymbols=map_word \
--keep_isymbols=false --keep_osymbols=false |\
fstrmepsilon > G.fst
## 构造L
make_lexicon_fst.pl lexicon_disambig 0.5 sil | \
fstcompile --isymbols=map_phone --psymbols=map_word \
--keep_isymbols=false --keep_osymbols=false |\
fstarcsort --sort_type=olabel > L.fst
## 构造LG = L * G
fsttablecompose L.fst G.fst | fstdeterminizestar --use-log=true | \
fstminimizeencoded | fstpushspecial > LG.fst
## 动态奶生成C,并组合到LG,得到CLG
fstcomposecontext --context-size=3 --central-position=1 \
--read-disambig-syms=list_disambig \
--write-disambig-syms=ilabels_disambig \
ilabels LG.fst > CLG.fst
## 构造H
make-h-transducer --disambig-syms-out=tid_disambig \
ilabels tree final.mdl >H.fst
## 最终得到HCLG
fsttablecompose H.fst CLG.fst | \ # 复合
fstdeterminizestar --use-log=true | \ # 确定化
fstrmsymbols tid_disambig | fstrmepslocal | fstminimizeencoded | \ # 移除消歧符 最小化
add-self-loops --self-loop-scale=0.1 --reorder=true \ # 增加自跳转
model_final.mdl > HCLG.fst
解码部分
构建了HCLG后,我们希望在图中找到一条最优路径,该路径上输出标签所代表的的HMM状态在待识别语音上的代价要尽可能的低。这条路径上取出静音音素后的 输出标签就是单词级别的识别结果,这个过程就是解码。
通常建立一个$T \times X 矩 阵 , 矩阵, 矩阵,T 为 帧 数 , 为帧数, 为帧数,S$为HMM状态总数,对声学特征按帧遍历,对于每一帧的每个状态,把前一帧各个状态的累计代价和当前帧状态下的代价累加,选择使当前帧代价最低的前置状态作为当前路径的前置状态。现实中,并不需要始终存储整个矩阵信息,而只保留当前帧及上一帧信息即可
有时候我们也希望找到最优的多条路径,每条路径都对应一个识别结果,这个识别结果的列表被称为最优N个
令牌传递算:该算法的基本思路就是把令牌进行传递。这里所说的令牌实际上是历史路径的记录,对每个令牌,都可以读取或回溯出全部的历史路径信息。令牌上还存储该路径的累计代价,用于评估该路径的优劣。
代价越低路径越优
每个状态只保留一个令牌的方法,可以大幅度减少计算量,但令牌的数量仍然会快速增长,因此需要采用其他方法进一步限制解码器的计算量。
常见的方法是制定一套规则,比如全局最多令牌个数,当前令牌个数和最优令牌的最大差分等一系列条件,每传递指定的帧数,就把不满足这些条件的令牌删除,称为剪枝(Prune)
当Decode()函数执行完毕后,解码的主体流程实际上就已经结束了,接下来需要执行一些步骤来取出识别结果。
simpledecode解码器提供了一个函数:ReachedFinal(),用于检测是否解码到最后一帧。
通常来说如果模型训练较好,解码时都可以到达最后一帧。
如果声学模型或语言模型和待测音频不匹配,则有可能所有的令牌在传递过程中都被剪掉,这时,就无法解码到最后一帧了。出现这种情况时,就是可以尝试设置更大的beam值
如果还是无法解码到最后,就需要分析声音,考虑重新训练声学模型和语言模型了。
src/gmmbin/gmm-decode-simple GMM模型 HCLG解码图 声学特征 输出单词级解码结果
声学模型:exp/tri1/final.mdl
状态图:exp/tri1/graph/HCLG.fst
声学特征:data/test.feats.scp
但需要对声学特征进行CMVN以及Delta处理
apply-cmvn --utt2spk=ark:utt2spk scp:cmvn.scp\
scp:feats.scp ark:- | add-deltas ark:- ark:feats_cmvn_delta.ark
以上就是解码所需要的全部输入,可以使用gmm-decode-simple工具解码
gmm-decode-simple final.mdl hclg.fst ark:feats_cmvn_delta.ark ark,t:result.txt
识别结果保存在result.txt文件中
解码的更常见做法不是只输出一个最佳路径,而是输出一个词网格(word Lattice)。词网格没有一个统一的定义,在Kaldi中,词网格被定义为一个特殊的WFST,该WFST的每个跳转的权重有两个值构成,不是一个标准WFST的一个值。这两个值分别代表声学分数和语言分数,和HCLG一样,词网格的输入标签和输出标签分别是transition-id和word-id
词格:包含了最佳路径也包含了其它可能路径
LatticeDecoder
lattice-to-nbest #
lattice-best-path # 得到文本方式表示的最佳路径单词序列
在构建HCLG时,如果语言模型非常大,则会构建出很大的G.fst,而HCLG.fst 的大小有事G.fst的若干倍,以至于HCLG。fst达到无法载入。
所以通过会采用语言模型裁剪等方法来控制HCLG的规模
ngram-count -prune # 参数提供了裁剪功能
裁剪后的语言模型或多或少会减少损失识别率。基于WFST的解码方法对这个问题的解决策略是使用一个较小的语言模型来构造G,进而构造G,进而构造HCLG。使用这个HCLG解码后,对得到的词格的语言模型使用大的语言模型进行修正,这样就在内存有限的情况下较好的利用大语言模型的信息。
语言分和HMM转移概率、多音字特定发音概率混在一起共同够了固有分
语言模型重打分调整的知识语言分,因此需要首先想办法去掉原固有分中的旧语言模型分数,然后应用新的语言模型分数
# 去掉旧语言模型分数
lattice-lmrescore --lm-scale=-.10 ark:in.lats G_old.fst ark:nolm.lats
# 应用新的语言模型分数
lattice-lmrescore --lm-scale=1.0 ark:nolm.lats G_new.fst ark:out.lats
构建大语言模型,无需构建HCLG,只需要构建G,使用arpa-to-const-arpa工具把ARPA文件转成CONST ARPA
arpa-to-const-arpa --bos-symbol=$bos \
--eos-symbol=$eos --unk0symbol-$unk \
lm.arpa G.carpa
和G 不同,CONSTARPA 是一种树结构,可以快速第查找到某一个单词的语言分,而不需要构建庞大的WFST,构建CONST ARPA后,就可以使用lattice-lmrescore-const-arpa工具进行重打分,他可以支持非常巨大的语言模型
# 去掉旧语言模型分数
lattice-lmrescore --lm-scale=-.10 ark:in.lats G_old.fst ark:nolm.lats
# 用CARPA应用新的语言模型分数
lattice-lmresocre-const-arpa --lm-scale=1.0 ark:nolm.lats \
G.carpa ark:out.lats
# 准备dict
. ./path.sh
# lexicon.txt文件夹
echo ">>>lexicon.txt "
res_dir=study
dict_dir=study/dict
mkdir -p $dict_dir
# 准备文件lexicon.txt
cp $res_dir/lexicon.txt $dict_dir
cat $dict_dir/lexicon.txt | awk '{ for(n=2;n<=NF;n++){ phones[$n] = 1; }} END{for (p in phones) print p;}'| \
perl -e 'while(<>){ chomp($_); $phone = $_; next if ($phone eq "sil");
m:^([^\d]+)(\d*)$: || die "Bad phone $_"; $q{$1} .= "$phone "; }
foreach $l (values %q) {print "$l\n";}
' | sort -k1 > $dict_dir/nonsilence_phones.txt || exit 1;
echo sil > $dict_dir/silence_phones.txt
echo sil > $dict_dir/optional_silence.txt
# No "extra questions" in the input to this setup, as we don't
# have stress or tone
cat $dict_dir/silence_phones.txt| awk '{printf("%s ", $1);} END{printf "\n";}' > $dict_dir/extra_questions.txt || exit 1;
cat $dict_dir/nonsilence_phones.txt | perl -e 'while(<>){ foreach $p (split(" ", $_)) {
$p =~ m:^([^\d]+)(\d*)$: || die "Bad phone $_"; $q{$2} .= "$p "; } } foreach $l (values %q) {print "$l\n";}' \
>> $dict_dir/extra_questions.txt || exit 1;
echo ">>>字典准备完成 "
# 准备数据
echo ">>>准备wav数据,生成 "
aishell_audio_dir=$res_dir/wav
aishell_text=$res_dir/text/aishell_transcript_v0.8.txt
# 前期数据
data=$res_dir/data
mkdir -p $data
# find wav audio file for train, dev and test resp.
find $aishell_audio_dir -iname "*.wav" > $data/wav.flist
n=`cat $data/wav.flist | wc -l`
[ $n -ne 141925 ] && \
echo Warning: expected 141925 data data files, found $n
dir=$data
# Transcriptions preparation
echo Preparing $dir transcriptions
sed -e 's/\.wav//' $dir/wav.flist | awk -F '/' '{print $NF}' > $dir/utt.list
sed -e 's/\.wav//' $dir/wav.flist | awk -F '/' '{i=NF-1;printf("%s %s\n",$NF,$i)}' > $dir/utt2spk_all
paste -d' ' $dir/utt.list $dir/wav.flist > $dir/wav.scp_all
utils/filter_scp.pl -f 1 $dir/utt.list $aishell_text > $dir/transcripts.txt
awk '{print $1}' $dir/transcripts.txt > $dir/utt.list
utils/filter_scp.pl -f 1 $dir/utt.list $dir/utt2spk_all | sort -u > $dir/utt2spk
utils/filter_scp.pl -f 1 $dir/utt.list $dir/wav.scp_all | sort -u > $dir/wav.scp
sort -u $dir/transcripts.txt > $dir/text
utils/utt2spk_to_spk2utt.pl $dir/utt2spk > $dir/spk2utt
# kaldi_file标准文件目录
kaldi_file=$data/kaldi_file
mkdir -p $kaldi_file
for f in spk2utt utt2spk wav.scp text; do
cp $data/$f $kaldi_file/$f || exit 1;
done
echo ">>>准备spk2utt utt2spk wav.scp text数据,生成kaldi_file标准文件格式 "
lang=${res_dir}/lang
# 生成L.fst
utils/prepare_lang.sh --position-dependent-phones false $dict_dir "" $res_dir/lang_tmp $lang || exit 1;
echo ">>>spk2utt utt2spk wav.scp text数据准备完成 "
echo ">>>准备语言模型,LM "
echo `pwd`
# LM training
study/train_code/aishell_train_lms.sh || exit 1;
echo ">>>关键的一步,开始生成G.fst>>>"
G=${res_dir}/G
# 生成G.fst
utils/format_lm.sh ${res_dir}/lang ${res_dir}/lm/3gram-mincount/lm_unpruned.gz \
${dict_dir}/lexicon.txt $G || exit 1;
echo ">>>恭喜,生成G.fst完成 "
# 生成声学模型,H.fst
echo ">>>关键的一步,声学模型,开始生成H.fst "
echo ">>>提取音频特征MFCC "
train_cmd=run.pl
mfccdir= ${dict_dir}/mfcc
exp=${res_dir}/exp
steps/make_mfcc_pitch.sh --cmd "$train_cmd" --nj 8 $kaldi_file $exp/make_mfcc $mfccdir || exit 1;
echo ">>>提取完成"
steps/compute_cmvn_stats.sh $kaldi_file exp/make_mfcc $mfccdir || exit 1;
utils/fix_data_dir.sh $kaldi_file || exit 1;
echo ">>>CMVN完成"
echo ">>>开始训练单音素"
# steps/train_mono.sh --cmd "run.pl" --nj 8 data/train data/lang exp/mono
steps/train_mono.sh --cmd "$train_cmd" --nj 8 $kaldi_file $lang $exp/mono || exit 1;
echo ">>>恭喜,生成H.fst完成"
# 生成HCLG.fst
# Monophone decoding
# 合成HCLG
# # 解码
utils/mkgraph.sh $G $exp/mono $exp/mono/graph || exit 1;
steps/decode.sh --cmd "run.pl" --config conf/decode.config --nj 8 \
$exp/mono/graph $kaldi_file $exp/mono/decode
为了捕捉发音单元的变换,通常将单音子(MonoPhone)扩展为上下文相关的三音子(Triphone),其副作用是模型参数急剧扩大,导致数据系数,训练效率降低,为了解决这个问题,建模过程引入了基于聚类方法的上下文决策树,以期在建模精度和数据量之间达到平滑。基于决策树的声学模型中,决策树的叶子节点的观察概率分布用GMM拟合,即似然度。在NN-HMM框架中,使用神经网络的输出表示每个叶子节点的分类概率,即后验概率。为了不影响声学模型训练和识别过程中的得分幅值,将后验概率除以对应叶子节点的先验概率,得到似然度。因此NN-HMM中 的NN是发音状态分类模型,输入是声学特征,输出是分类概率。
我们前面介绍过,语音识别是一个封闭词表的任务,通常来说一旦构建就词表就以固定。但实际应用中总会出现各种各样的新词汇,有时我们还需要删除词表中的一些完全无用的垃圾词。name,我们想对词表进行增补或者删除时,是否需要重新构建整个系统呢?
为了回答这个问题,这里需要明确一个概念:语音识别系统训练过程中的词表(词典)与解码时的词表可以完全独立的。
在Kaldi的很多方法中只涉及一个词典,因此体现不明显,但开发者需要了解一下
训练词典:
解码词典:
其作用在于覆盖实际应用可能出现的所有词汇
一方面,当面对狭窄的应用领域时,其词表可能比声学模型训练阶段的词表少很多
另一方面,当面对专业词的应用时,其中也可以包含许多训练阶段中没有出现的词汇
因此,我们在应用阶段对词表进行变更时,无关训练,只需变更解码词典,并对解码空间进行离线重构。具体来说,在Kaldi中的HCLG的WFST框架下,整体的解码空间为HCLG,对于词表的变更,我们只需要参数Kaldi中的HCLG的相关流程,将其中的L及G进行更新,并与原声学模型搭配即可。
构建G的方法1
echo "》》》关键的一步,开始生成G.fst===================================="
G=${res_dir}/G
# 生成G.fst
utils/format_lm.sh ${res_dir}/lang ${res_dir}/lm/3gram-mincount/lm_unpruned.gz \
${dict_dir}/lexicon.txt $G || exit 1;
echo "》》》恭喜,生成G.fst完成=========================================="
构建G的方法2
## 构造G
arpa2fst --natural-base=false lm.arpa |\
fstprint | esp2disambig.pl | s2eps.pl |\
fstcompile -isymbols=map_word --osymbols=map_word \
--keep_isymbols=false --keep_osymbols=false |\
fstrmepsilon > G.fst
## 构造L
make_lexicon_fst.pl lexicon_disambig 0.5 sil | \
fstcompile --isymbols=map_phone --psymbols=map_word \
--keep_isymbols=false --keep_osymbols=false |\
fstarcsort --sort_type=olabel > L.fst
## 构造LG = L * G
fsttablecompose L.fst G.fst | fstdeterminizestar --use-log=true | \
fstminimizeencoded | fstpushspecial > LG.fst
## 动态生成C,并组合到LG,得到CLG
fstcomposecontext --context-size=3 --central-position=1 \
--read-disambig-syms=list_disambig \
--write-disambig-syms=ilabels_disambig \
ilabels LG.fst > CLG.fst
make-h-transducer --disambig-syms-out=tid_disambig \
ilabels tree final.mdl >H.fst
## 最终得到HCLG
fsttablecompose H.fst CLG.fst | \ # 复合
fstdeterminizestar --use-log=true | \ # 确定化
fstrmsymbols tid_disambig | fstrmepslocal | fstminimizeencoded | \ # 移除消歧符 最小化
add-self-loops --self-loop-scale=0.1 --reorder=true \ # 增加自跳转
model_final.mdl > HCLG.fst
再有L和G的基础上
# 生成HCLG.fst
# Monophone decoding
utils/mkgraph.sh $G $exp/mono $exp/mono/graph || exit 1;
需要HCLG.fst
# 参数
# 解码配置文件
# HCLG所在目录
# 生成解码的文件识别的结果文本txt放在
# 这个目录:..exp/tri1/decode_test/scoring_kaldi/penalty_1.0
steps/decode.sh --cmd "run.pl" --config conf/decode.config --nj 8 \
$exp/mono/graph $kaldi_file $exp/mono/decode
# 内部使用的是gmm-latgen-faster解码器
在Librispeech示例中使用的是 faster-rnnlm方案,重打分的脚本是steps/rnnlmrescore.sh.这个脚本使用了RNN LM 和N元文法LM混合的重打分方案,其中 RNN LM的语言分计算由脚本utils/rnnlm_compute_scores.sh完成,并使计算出的分数修改词格。
# 去掉旧语言模型分数
lattice-lmrescore --lm-scale=-.10 ark:in.lats G_old.fst ark:nolm.lats
# 应用新的语言模型分数
lattice-lmrescore --lm-scale=1.0 ark:nolm.lats G_new.fst ark:out.lats
构建大语言模型,无需构建HCLG,只需要构建G,使用arpa-to-const-arpa工具把ARPA文件转成CONST ARPA
arpa-to-const-arpa --bos-symbol=$bos \
--eos-symbol=$eos --unk0symbol-$unk \
lm.arpa G.carpa
和G 不同,CONSTARPA 是一种树结构,可以快速第查找到某一个单词的语言分,而不需要构建庞大的WFST,构建CONST ARPA后,就可以使用lattice-lmrescore-const-arpa工具进行重打分,他可以支持非常巨大的语言模型
# 去掉旧语言模型分数
lattice-lmrescore --lm-scale=-.10 ark:in.lats G_old.fst ark:nolm.lats
# 用CARPA应用新的语言模型分数
lattice-lmresocre-const-arpa --lm-scale=1.0 ark:nolm.lats \
G.carpa ark:out.lats