kaldi环境配置
下载
https://github.com/kaldi-asr/kaldi.git
安装编译依赖库
cd kaldi
tools/extras/check_dependencies.sh
注意:根据提示安装相关依赖工具
安装第三方工具
- OpenFst:
- kaldi使用FST作为状态图的表现形式,期待吗依赖OpenFst中定义的FST结构及一些基本操作,因此OpenFst对于Kaldi的编译是不可或缺的,安装方法如下
- 需要g++ 11
cd tools
make openfst
- cub:
- cub是NVIDIA官方提供的CUDA核函数开发库,是目前Kaldi编译的必选工具,安装方法如下
cd tools
make cub
- Sph2pipe:
- 这个工具是用来对SPH音频格式进行转换的,使用LDC数据的示例都要用到这个工具
cd tools
make sph2pipe
- ITSTLM/SRILM/Kaldi_lm:
- 这是三个不同的语言模型工具,不同的示例使用不同的语音模型工具
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
-
编译kaldi
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工具
配置并行环境
脚本工具
utils/run.pl
这个Perl脚本的作用是多任务地执行某个程序,这是一个非常方便的工具,可以独立于kaldi使用
utils/run.pl JOB=1:8 ./tmp/log.JOB.text echo "this is the job JOB"
kaldi流程
准备数据
四个标准文件
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
- spk2utt
- text
- utt2spk
- wav.scp
生成的这两个目录使用的是Kaldi的标准数据文件夹格式,每个句子都没有指定了一个唯一的id
kaldi输入输出机制
表单
- 经过local文件夹中的预处理脚本的处理,原始数据文件被处理成kaldi的标准格式——表单(table)
- 表单的本质是若干元素的集合,每个元素有一个索引
- 索引必须是一个不包含空格的非空字符串
- 而元素的类型取决于创建表单时的定义
- 例如:摇窗机一个音频表单,那么元素的内容就是音频文件名:aduio1 /音频/audio1.wav
- audio1 就是索引,后面的路径就是表单元素
- 在kaldi中,所有的数据文件都是以表单形式存储的,比如文本、音频特征、特征变换矩阵
- 表单可以存储在磁盘上,也可以存储在内存中[以管道的形式]
- 表单有两种
- 列表(Script-file) 表单
- 存档(Archive)表单
- 一套特有的输入输出机制
列表表单
-
作用
-
列表表单 用于索引存储于磁盘或内存中的文件
-
在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 |
- 上面的示例中,第一个空格之后的内容表示wav格式的音频文件的压缩包酱油gunzip进行解压并传输到内存管道中
- 而kaldi的可执行文件将从管道中读取解压之后的文件内容并执行后续操作
- 这样做可以节省磁盘空间
-
偏移定位符
- 如果文件定位符执行的是二进制的kaldi存档文件,则还可以增加偏移定位符
- 用于指向该二进制文件中从某一个字节开始的内容
- 扩展偏移定位符:
- 通过切片操作指定读取的行和列的范围
-
-
-
从管道文件和偏移定位符可以看出,文件定位符定义的“文件”,本质是上一个存储地址,这个地址可能是一个外部磁盘的物理地址,也可能是管道指向的内存地址,还可能是从一个磁盘文件中的某个字节开始的地址。
-
无论哪种形式,列表表单的元素一定是“文件”
存档表单
-
存档表单用于存储数据,数据可以是文本数据,也可以是二进制数据
-
这类表单通常默认以.ark为扩展名,但没有严格限制
-
存档表单没有行的概念,存档表单的元素直接没有间隔符,对于文本类型的存档文件来说,需要保证每个元素都以换行符结尾
-
text_index1 this is first text\text_index2 this is second text\n
-
二进制类型存档表单中
索引以每个字符对于的ASCII值存储,然后是一个空格,接下来是“\0B”,这个标志位是区别文本和二进制内容 的重要标识
紧接着是二进制的表单元素,直至下一个索引
可以通过内容本身判断这个元素占用的空间大小,这个信息保存在一段文件头中
binary_index1 \0B<header><content>binary_index2 \0B<header><content>
<header>中可以包含特征的帧数,维度,声学特征类型,占用字节数和释放压缩等信息
读写声明符
-
读声明符和谐声明符定义了可执行程序处理输入表单文件和输出表单文件的方式,他们都是有两部分组成
-
表单属性(specifier option)
- scp:列表表单
- ark,t ; ark :存档表单
-
表单文件名 ( xfilename)
- path/file1
-
-
这两部分都冒号组合在一起
-
他们可以接受的表单文件名如下:
- 磁盘路径
- 对于读声明符,指定一个存在于磁盘的文件路径
- 对于写声明符,制定一个希望输出的文件路径
- 标准输入
- 对于读声明符和写声明符,如果指定 “-” 为表单文件名,则意味着要从标准输入获取文件内容,或者将输出打印到标准输入
- 管道符号
- 如果在某个可执行程序后边加上管道符号,则意味着要将输出送入管道,由管道后边的可执行程序接收
- 如果在某个可执行程序前面加上管道符号,则意味着要从管道中获取输入
- 磁盘路径夹偏移定位符
- 这种方式只能用于读声明符,用户告知可执行程序从文件的某个字节开始读取
- 磁盘路径
-
# 参数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
数据文件
给出了声学模型训练数据的描述,其中文本标注是以词为单位的
列表类数据表单
-
句子音频表
- 句子音频表单的文件名为wav.scp
- 表单元素为音频文件或者音频处理工具输出的管道,每个元素可以表示一个切分后的句子,也可以表示包含多个句子的为切分整段音频
- 例如:说话人1录制的一段阅读段落
- 这种未切分的,为分段的音频表单需要配合切分表单Segments使用
-
声学特征表单
- 声学特征表单的文件名为feats.scp
- 表单元素保存的是声学特征,每个元素表示一个句子。
-
普特征归一化表单
- 文件名称:cmvn.scp
- 通过声学特征处理脚本提取的谱归一化系数文件,其归一化可以以句子为单位,也可以以说话人为单位
-
VAD信息表单
- vad.scp
- 表单元素为用Kaldi的compute-vad工具提取的vad信息文件。
- 这个表单有提取vad的通用脚本生成的,以句子为单位
存档类型数据表单
-
说话人映射表单
-
文件名为:utt2spk、spk2utt
-
存放的是文本内容,一个句子到说话的映射,以及说话人到句子的映射
-
103-1240-0000 103-1240 103-1240-0001 103-1240 103-1240-0002 103-1240 103-1240-0003 103-1240 ...
-
-
标注文本表单
- 标注文本表单的文件名:text
- 其内容是每一句音频的标注内容,通常保存为一个文本类型的存档表单
- 该文件保存的应当是文本归一化之后的内容,所谓的归一化,就是保证文本中的词都在发音字典和语言模型的此表中,而未出现的词都将被当做未知词。对于英语,通常要将所有字母统一成大写和小写。对于中文,最基本的要求是完成文本分词。
-
切分信息表单
-
切分信息表单文件名为: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相关系数表单
- VTLN是一种说话人自适应技术
- 在Kaldi的数据文件中,有三个文本类型的存档文件与此相关,分别是:
- 说话人性别映射(spk2gender) 索引是说话人,内容是性别标识f:女性,m男性
- 说人话卷曲因子映射(spk2warp) 索引是说话人,内容是卷曲因子,用一个0.5~1.5的浮点数表示,
- 句子卷曲映射(utt2warp) 索引是句子,内容与spk2warp内容相同
-
句子时长表单
- 文件名为:utt2dur,表单可以由一个通用脚本生成,
- 句子为索引,内容是每个句子的时长,以秒为单位
数据文件夹处理脚本
在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 | 检查给定数据文件夹的内容,包括排序是否正确、 元素索引是否对应等 |
表单索引一致性
- 表单索引分为三类:句子、音频、说话人
- 音频索引
- 的作用是定位数据集中的音频文件,音频wav.scp一定是以音频为索引的。在kaldi的帮助文件中,音频索引被称为Recording identifier。这个索引对应的是一个录音文件,如果这个录音文件已经被切分为句子,则音频索引等同于句子索引。
- 句子索引
- 在kaldi的帮助文件中被称为Utterance identifier,它定义了kaldi处理的数据的基本单元。大部分表单时以句子为索引的,其中最重要的就是text、utt2spk和feats.scp.在完成声学特征提取之后,音频索引就不再被使用了,这个声学模型训练过程都是使用上述三个表单完成的,因此这些表单的索引需要保持一一致
- 说话人索引
- 这个索引并不一定对应一个真正的录音人,事实上,在kaldi的语音识别示例中,大部分都没有使用录音人作为说话人。
- 以说话人为索引的 表单包括spk2utt和cmvn.scp
- 音频索引
- 说话人信息在自适应声学建模中使用,用来增强识别系统对不同说话人的适应能力,例如倒谱归一化(CMVN)。对CMVN系数估计和使用,kaldi的可执行程序有两种模式,一种是每句估计一套归一化系数,另一种是一个说话人使用一套归一化系数。在官方给出的训练脚本中,cmvn.scp默认安装spk2utt给出的映射统计每个说话人的归一化系数。
语言模型相关文件
在开始训练声学模型之前,需要定义发音词典、音素集和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 SIL 表示静音,其发音是静音音素 <UNK> SPN 表示噪声和集外词,其发音都是SPN <SPOKEN_NOISE> SPN YES Y NO N
给出了YES、NO和<SIL>这三个单词的音素序列,其中、<SIL>是一个特殊单词,表示静音
-
lexicon_nosil.txt
-
和lexicon.txt文件相同,只是去掉了<SIL>行
-
YES Y NO N
-
-
phones.txt
-
给出了音素集
-
SIL Y N
-
-
silence_phones.txt
-
所有可以用来表示无效语音内容的音素
-
SIL SPN # 表示有声音但是无法识别的声音片段
-
-
optional_silence.txt
- 用于填充词间静音的音素,选择用SIL这个音素表示词间静音。
生成语言文件夹
通过词典文件夹,生成语言文件夹,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 表示静音,其发音是静音音素 <UNK> SPN 表示噪声和集外词,其发音都是SPN <SPOKEN_NOISE> 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 <s> -1 </s>
-
通过语言模型生成G.fst
准备文件
text
BAC009S0002W0122 而 对 楼市 成交 抑制 作用 最 大 的 限 购
BAC009S0002W0123 也 成为 地方 政府 的 眼中 钉
BAC009S0002W0124 自 六月 底 呼和浩特 市 率先 宣布 取消 限 购 后
BAC009S0002W0125 各地 政府 便 纷纷 跟进
BAC009S0002W0126 仅 一 个 多 月 的 时间 里
BAC009S0002W0127 除了 北京 上海 广州 深圳 四 个 一 线 城市 和 三亚 之外
BAC009S0002W0128 四十六 个 限 购 城市 当中
BAC009S0002W0129 四十一 个 已 正式 取消 或 变相 放松 了 限 购
BAC009S0002W0130 财政 金融 政策 紧随 其后 而来
BAC009S0002W0131 显示 出 了 极 强 的 威力
BAC009S0002W0132 放松 了 与 自 往 需求 密切 相关 的 房贷 政策
BAC009S0002W0133 其中 包括 对 拥有 一 套住 房 并 已 结清 相应 购房 贷款 的 家庭
BAC009S0002W0134 为 改善 居住 条件 再次 申请 贷款 购买 普通 商品 住房
BAC009S0002W0135 银行 业金 融机 构 执行 首套 房贷 款 政策
...
lexicon.txt
SIL sil
<SPOKEN_NOISE> 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
该表单的元素以说话人为索引,每个方括号内是其对应的倒谱均值方差归一化系数,一个均值归一化,一个是方差归一化,以使得模型的输入特征趋于正态分布,这对于与说话人无关的声学模型建模非常重要。
查看CMVN
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
LDA的目的是通过变换来减少同类特征间的方差,增加不同类特征之间 方差,这里的类指的是声学模型的状态。
MLLT
是一类变换技术的统称,
均值最大线性自然回归(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中有多少个状态,以及各个状态起始的马尔科夫链的初始化概率,个状态间的转移概率以及每个状态的概率分布函数。
- 初始概率
- 一般零初始化概率恒为1
- 转移概率
- 预设为固定值,不再训练中更新转移概率
- 观察概率
- 声学模型包含的信息主要是状态定义和个状态的观察概率分布
- 如果用混合高斯模型对观察概率分布建模,那么就是GMM-HMM模型
- 如果使用神经网络模型对观察概率分布建模,那么就是NN-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建模
用于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
将声学模型用于语音识别
识别的过程就是语音的特征的序列特征取匹配一个状态图,搜索最优路径。
状态图中有无数条路径,每条路径代表一种可能的识别结果,且都有一个分数,该分数表示语音和该识别结果的匹配程度。
判断标准
判断两条路径的优劣就是比较这两条路径的的分数,分数高的路径更有,即高分路径上的识别结果和声音更匹配。
- 分数
- 声学分
- 声学分则是在识别过程中根据声学模型和待识别语音匹配关系动态计算的,声学模型在语音识别过程中的最主要的就是计算声学分。
- 图固有分(Graph score)
- 图固有分主要来源于语言模型概率,同时来源于发音词典的多音词选择概率和HMM模型的转移概率。
- 这些概率在状态图构建过程中就固定在了状态图中,和待识别的语音无关,因此我们称它为图固有分
- 声学分
模型初始化
这个基础模型的每个状态只有一个高斯分类,在后续的训练过程中,会进行单高斯分量到混合多高斯分量的分裂。
# HMM topo结构
# 声学特征维数
# 初始化声学模型
gmm-init-mono topo 39 mono.mdl mono.tree
对齐
获取帧级别的标注,通过下面的工具
compile-train-graphs # 输出一个状态图
gmm-align # 内部调用了FasterDecoder,解码器来完成对齐
gmm-align-compiled # 对训练数据进行反复对齐
transition模型
transition模型存储于kaldi声学模型的头部
<TransitionModel>
<TopologyEntry>
# 第一部分
</TopologyEntry>
<Triples>
# 第二部分
<音素索引,HMM状态索引,PDF索引>
</Triples>
</TransitionModel>
查看transition-state
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的原因
相比 transition-id, pdfid 似乎是表示 HMM 状态更直观的方式,为什么 Kaldi要定义这样烦琐的编号方式呢?这是考虑到 paf-id 不能唯一地映射成音素,而transition id 可以。如果直接使用 paf-id 构建状态图,固然可以正常解码并得到 pdf-id序列作为状态级解码结果,但难以从解码结果中得知各个pdf-id 对应哪个音素,也就无法得到音素级的识别结果了,因此 Kaldi 使用 transition-id 表示对齐的结果。
GMM模型迭代
声学模型训练需要对齐结果,而对齐过程又需要声学模型,这看起来是一个鸡生蛋蛋生鸡的问题
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文件
acc文件存储了GMM在EM训练中所需要的统计量。
生成ACC文件后,可以使用gmm-est工具来更新GMM模型参数
gmm-est
每次模型参数的迭代都需要成对使用这两个工具
gmm-acc-stats-ali
gmm-est
三音子模型训练
单音子作为建模单元的语音识别模型机器训练,在实际使用中,单音子模型过于简单,往往不能达到最好识别性能。
上下文相关的声学模型
Content Dependent Acoustic Model
三音子
描述的是一个音素模型实例取决于实例中心音素、左相邻音素和右相邻音素,共三个音素。
和HMM三状态要区分清楚,一个音素模型实例内部有三个HMM状态组成,在概念上不同的HMM状态用来分别捕捉该音素发音时启动、平滑、衰落等动态变化。
无论是单音子还是三音子,通常使用三状态HMM结构来建模
三音子聚类裁剪
单音子模型到三音子模型的扩展,虽然解决了语言学中协同发音等上下文的问题。但也带来了另一个问题,模型参数数据“爆炸”。
解决办法
将所有的三音子模型放到一起进行相似性聚类,发音相似的三音子被聚类到同一个模型,共享参数,通过人为控制聚类算法最终的类的个数,可以有效的减少整个系统中实际的模型个数,同时又兼顾解决了单音子假设无效的问题。
具体实现:通过决策树算法,将所有需要建模的三音子的HMM状态放到决策树的根节点中,作为基类。
Kaldi中的三音子模型训练流程
和单音子训练流程一样,训练之后用生成的模型对训练数据重新进行对齐,作为后续系统的基础
三音子训练模型的脚本功能又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
从语言模型构建G
词图
词与词之间的跳转,权重是语言模型
# 对APRA格式的语言模型文件解压后,直接输入到arpa2fst程序中,就得到目标G.fst
gunzip -c n.arpa.gz | arpa2fst --disambig-symbol=#0 \
--read-symbol-table=words.txt - G.fst
从发音词典构建L
音素图
单音子与词之间的跳转,权重是音素词典概率
prepare_lang.sh
WFST的复合运算
Compose
生成LG.fst
音素到单词的转录机
LG图对上下文展开
得到C之后,将C和LG复合,就得到了CLG。CLG把音素上下文序列转录为单词序列
fstmakecontextfst ilabels.sym <LG.fst> CLG.fst
实际上,并不是任意单音子的组合都是有意义的,在kaldi的实现中,并不去真正地构建完整的C,而是根据LG一边动态构建局部C,一边和LG复合,避免不必要地生成C的全部状态和跳转。
C的输入标签:是状态
输出标签是:音素对应的id
权重是:左侧音素和右侧音素
用WFST表示HMM拓扑结构
在生成从HMM状态到单词的转录机,之前需要有 从上下文音素到单词的转录机。
首先把HMM模型的拓扑结构以及转移概率构成的WFST,这个WFST习惯上被简称为H
输入标签是HMM状态号
输入出标签是C中的ilabel
跳转权重是转移概率
# 构建H的工具
make-h-transducer
kaldi构建HCLG的主要流程为
## 构造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-best
有时候我们也希望找到最优的多条路径,每条路径都对应一个识别结果,这个识别结果的列表被称为最优N个
Token
令牌传递算:该算法的基本思路就是把令牌进行传递。这里所说的令牌实际上是历史路径的记录,对每个令牌,都可以读取或回溯出全部的历史路径信息。令牌上还存储该路径的累计代价,用于评估该路径的优劣。
代价越低路径越优
剪枝
每个状态只保留一个令牌的方法,可以大幅度减少计算量,但令牌的数量仍然会快速增长,因此需要采用其他方法进一步限制解码器的计算量。
常见的方法是制定一套规则,比如全局最多令牌个数,当前令牌个数和最优令牌的最大差分等一系列条件,每传递指定的帧数,就把不满足这些条件的令牌删除,称为剪枝(Prune)
控制剪枝能力beam
当Decode()函数执行完毕后,解码的主体流程实际上就已经结束了,接下来需要执行一些步骤来取出识别结果。
simpledecode解码器提供了一个函数:ReachedFinal(),用于检测是否解码到最后一帧。
通常来说如果模型训练较好,解码时都可以到达最后一帧。
使用beam的情况
如果声学模型或语言模型和待测音频不匹配,则有可能所有的令牌在传递过程中都被剪掉,这时,就无法解码到最后一帧了。出现这种情况时,就是可以尝试设置更大的beam值
beam值越大,剪枝能力越弱
如果还是无法解码到最后,就需要分析声音,考虑重新训练声学模型和语言模型了。
Simp0leDecoder
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
- 特点:
- 所有解码分数或负代价大于某阈值的输出标签(单词)序列,都可以在词网格中找到对应的路径
- 词网格中每条路径的分数和输入标签序列都能在HCLG中找到对应的路径
- 对于任意输出标签序列,最多只能在词网格中找到一条路径
词格:包含了最佳路径也包含了其它可能路径
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 "<SPOKEN_NOISE>" $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
语音识别系统评价
评价指标
- 英文词错率 WER
- 计算方法
- 将识别结果错误词的累计个数除以标注中的总词数,结果表示为一个百分数。
- 对错误词有以下三种定义
- 插入错误(Insertion)
- 删除错误(Deletion)
- 替换错误(Substitiute)
- 计算方法
- 中文字错率 CER
- 正确率ACC来评价
- 测试句子的正确识别次数和全部标注文本词数
深度学习声学模型建模技术
基于神经网络的声学模型
为了捕捉发音单元的变换,通常将单音子(MonoPhone)扩展为上下文相关的三音子(Triphone),其副作用是模型参数急剧扩大,导致数据系数,训练效率降低,为了解决这个问题,建模过程引入了基于聚类方法的上下文决策树,以期在建模精度和数据量之间达到平滑。基于决策树的声学模型中,决策树的叶子节点的观察概率分布用GMM拟合,即似然度。在NN-HMM框架中,使用神经网络的输出表示每个叶子节点的分类概率,即后验概率。为了不影响声学模型训练和识别过程中的得分幅值,将后验概率除以对应叶子节点的先验概率,得到似然度。因此NN-HMM中 的NN是发音状态分类模型,输入是声学特征,输出是分类概率。
词表的扩展
背景
我们前面介绍过,语音识别是一个封闭词表的任务,通常来说一旦构建就词表就以固定。但实际应用中总会出现各种各样的新词汇,有时我们还需要删除词表中的一些完全无用的垃圾词。name,我们想对词表进行增补或者删除时,是否需要重新构建整个系统呢?
为了回答这个问题,这里需要明确一个概念:语音识别系统训练过程中的词表(词典)与解码时的词表可以完全独立的。
在Kaldi的很多方法中只涉及一个词典,因此体现不明显,但开发者需要了解一下
-
训练词典:
- 其作用在于覆盖训练文本中出现的词汇,一旦将训练数据的文本转换为声学建模单元(入音素、音节等),接下来的声学模型训练就与词典无关了。
-
解码词典:
-
其作用在于覆盖实际应用可能出现的所有词汇
-
一方面,当面对狭窄的应用领域时,其词表可能比声学模型训练阶段的词表少很多
-
另一方面,当面对专业词的应用时,其中也可以包含许多训练阶段中没有出现的词汇
-
解决方法
因此,我们在应用阶段对词表进行变更时,无关训练,只需变更解码词典,并对解码空间进行离线重构。具体来说,在Kaldi中的HCLG的WFST框架下,整体的解码空间为HCLG,对于词表的变更,我们只需要参数Kaldi中的HCLG的相关流程,将其中的L及G进行更新,并与原声学模型搭配即可。
构建HCLG
构建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
## 构造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
构建CLG
## 构造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
## 最终得到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
再有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