
2.4 一个简单的示例
本节将展示一个语音识别的示例:YesNo。这个示例的功能很有限,只能识别Yes和No两个单词。示例虽然简单,却“麻雀虽小,五脏俱全”。读者通过学习这个示例,可以了解创建语音识别系统的基本流程。当理解了这个示例后,读者将会发现,自己借助Kaldi也能够搭建一个简单的语音识别系统。
2.4.1 运行run.sh
这个示例无需修改就可以直接运行,包括数据的下载和整理、模型的训练、识别率的测试。所有脚本都在目录egs/yesno下。
首先我们来看一下这个目录的结构:

可以看到,这个示例由若干Shell脚本、Perl脚本和一些文本文件构成。看到这么多文件,读者可能会不知从何入手。其实,Kaldi的所有示例,无论由多少个文件构成,都是以run.sh为入口的。各示例中的其他脚本和可执行程序,都是被run.sh直接或间接调用的。所以,直接执行run.sh就可以运行这个示例了。

我们暂且不理会这个示例背后的原理,先看看执行结果。如果Kaldi被正确安装,那么运行run.sh后,屏幕上首先输出的信息是:

上面的信息很容易理解,脚本从OpenSLR网站下载了一个名为waves_yesno.tar.gz的压缩包,这个压缩包就是这个示例所用的音频数据。
OpenSLR是Kaldi社区建立的一个用于存储语音和语言资源的网站,网站上提供了大量英语、汉语、西班牙语等语料,可以免费下载,可用于训练语音识别、语音合成、说话人识别等模型。
接下来屏幕显示了许多信息,这些信息对于不熟悉语音识别的读者来说很难理解。读者如果看不懂这些信息,可以暂时不用理会。
这个示例的数据集规模非常小,在普通硬件配置的计算机上,大约一两分钟,整个脚本就运行完毕了。
输出信息的最后一行是:

这就是测试结果了:WER为0.00。也就是说,总共测试了232个词,全部识别正确。
2.4.2 脚本解析
本节将解析刚才运行过的run.sh,帮助读者理解这个脚本所做的事情。
1)脚本的前两行设置了train_cmd和decode_cmd两个变量:

这两个变量在后面会用到,比如后面的:

以及

Kaldi的很多脚本,比如这个示例中要用到的steps/train_mono.sh和steps/decode.sh,都允许设置cmd参数。在本例中,cmd参数被设置成了utils/run.pl。
utils/run.pl这个Perl脚本的作用是多任务地执行某个程序。这是一个非常方便的工具,是可以独立于Kaldi之外使用的。这里用一个示例展示其用法:

上面的命令同时执行了8个echo命令,并把屏幕显示输出分别写入/tmp/log.[1-8].txt这8个文本文件中。我们打开其中一个文件看一下:

可以看到,各个进程被分别执行,并将输出信息写入了不同的日志文件中。
Kaldi工具包中提供了utils/run.pl、utils/queue.pl和utils/slurm.pl作为cmd的可选工具,它们的命令行接口相同,任务所需的内存大小等选项也相同,不同之处在于run.pl在本地并行地执行命令,而queue.pl和slurm.pl把命令提交到计算集群上执行。
执行任务分发的Perl脚本名及其选项拼接在一起,作为cmd参数传入Kaldi的脚本中,然后Kaldi脚本使用cmd参数传入的Perl脚本来并行地执行程序。如果需要,读者也可以编写自己的任务分发脚本作为cmd的参数。
2)设置cmd参数后,脚本从OpenSLR网站下载数据并解压。
waves_yesno.tar.gz压缩包被解压后,除一个README文件外,就是很多WAV文件了。通常来说,用于训练语音识别模型的数据,除音频外,还需要有音频对应的文本。这个数据集由于情况简单,只包含YES和NO两个单词,因此这个数据集的提供者直接把文本标注写到了文件名中,用1代表YES,用0代表NO。比如,1_0_1_0_1_0_0_1.wav这个文件,其对应的文本就是:

接下来,需要对数据进行整理。数据整理有两个目的,其一是把数据规范成Kaldi规定的数据文件夹格式,其二是划分训练集和测试集。run.sh中整理数据的脚本是:

执行这行脚本后,将生成data/train_yesno目录和data/test_yesno目录,分别作为这个示例的训练集和测试集。两个目录的结构完全相同:

生成的这两个目录使用的是Kaldi的标准数据文件夹格式,我们查看一下这些文件的前几行:

每个句子都被指定了一个唯一的ID。wav.scp文件记录每个ID的音频文件路径,text文件记录每个ID的文本内容,spk2utt文件和utt2spk文件记录每个ID的说话人信息,本例中统一为global。
3)除下载数据外,还有一些资源需要手动准备。在这个示例中,这些资源已经由贡献者准备好了,在input路径下。
首先是发音词典lexicon.txt:

lexicon.txt文件给出了YES、NO和<SIL>这三个单词的音素序列,其中<SIL>是一个特殊单词,表示静音。这里由于任务简单,每个单词都只用一个音素表示。lexicon_nosil.txt文件和lexicon.txt文件的内容相同,只是去掉了<SIL>行。
phones.txt文件给出了这个示例的音素集:

其实phones.txt文件也可以从lexicon.txt文件中将所有音素去重得到。
task.arpabo是语言模型。本例中的语言模型不必训练,直接手工书写即可:

上面的语言模型定义了识别空间:只可能是Yes和No这两个单词,并且这两个单词出现的概率相同。关于语言模型的知识将在本书第5章中详细介绍。
4)数据文件夹生成后,就可以根据其中的文本信息,以及事先准备好的发音词典等文件,生成语言文件夹了。脚本如下:

前两行脚本读取input的资源文件,生成data/lang目录。这个目录是Kaldi标准的语言文件夹,存储了待识别语言的单词集、音素集等信息。第三行脚本把语言模型构建成图的形式,其细节将在本书第5章中介绍。
5)接下来是定义声学特征,这是训练声学模型的前提,脚本如下:

脚本执行完毕后,train_yesno目录和test_yesno目录下将分别生成feats.scp文件,里面记录了每个ID的声学特征存储位置。
6)下面是声学模型训练和测试阶段。由于这个示例的任务比较简单,因此只需训练最简单的声学模型,脚本如下:

脚本执行完毕后,声学模型被存储在exp/mono0a目录下。至此,模型训练完毕,进入测试识别阶段。识别的过程也被称作解码,解码前需要构建状态图:

本书将在第5章中详细讲解为何需要构建状态图及构建状态图的原理。构建状态图完毕后,调用Kaldi的解码器解码:

现在识别结果已经输出到exp/mono0a/decode_test_yes下面了。我们看一下识别结果:

这里我们只查看了exp/mono0a/decode_test_yes下的scoring_kaldi/penalty_0.0/10.txt文件。实际上,这个脚本输出了很多类似的识别结果文件,这些文件的区别是使用了不同的解码参数,其WER有微小的差异。
run.sh运行的最后,是寻找最好的解码器调参结果并输出:

最终找到了最好的结果:scoring_kaldi/penalty_0.0/7.txt,WER为0.0%。
以上是对YesNo这个示例较顶层的介绍。YesNo示例是一个很好的用来入门的示例,但其声学模型训练过于简单,只训练了单音素的GMM模型,同时这个示例的发音词典的设置也不具备一般性。
从第3章起,本书主要使用Librispeech作为示例,这个示例是一个通用英文识别任务,使用近千小时的训练数据,是一个可以真正使用的语音识别系统。第3章~第6章将通过Librispeech示例,详细地介绍语音识别系统的模型训练及解码的流程与原理。有了YesNo示例作为基础,相信读者能够更容易地理解其他更复杂的示例流程及其背后的原理。