作者NGK编辑部

本文将介绍如何基于新一代 Kaldi 框架快速搭建一个服务端的 ASR 系统,包括数据准备、模型训练测试、服务端部署运行。

更多内容建议参考:

  • k2[1]
  • icefall[2]
  • lhotse[3]
  • sherpa[4]

前言

距离新一代 Kaldi 开源框架的正式发布已经有一段时间了。截至目前,框架基本的四梁八柱都已经立起来了。那么,如何用它快速搭建一个 ASR 系统呢?

阅读过前面几期公众文的读者可能都知道新一代 Kaldi 框架主要包含了四个不同的子项目:k2、icefall、lhotse、sherpa。其中,k2是核心算法库;icefall是数据集训练测试示例脚本;lhotse是语音数据处理工具集;sherpa是服务端框架,四个子项目共同构成了新一代 Kaldi 框架。

另一方面,截至目前,新一代 Kaldi 框架在很多公开数据集上都获得了很有竞争力的识别结果,在 WenetSpeech 和 GigaSpeech 上甚至都获得了 SOTA 的性能。

看到这,相信很多小伙伴都已经摩拳擦掌、跃跃欲试了。那么本文的目标就是试图贯通新一代 Kaldi 的四个子项目,为快速搭建一个服务端的 ASR 系统提供一个简易的教程。希望看完本文的小伙伴都能顺利搭建出自己的 ASR 系统。

三步搭建 ASR 服务端系统

本文主要介绍如何从原始数据下载处理、模型训练测试、到得到一个服务端 ASR 系统的过程,根据功能,分为三步:

  • 数据准备和处理
  • 模型训练和测试
  • 服务端部署演示

本文介绍的 ASR 系统是基于 RNN-T 框架且不涉及外加的语言模型。所以,本文将不涉及 WFST 等语言模型的内容,如后期有需要,会在后面的文章中另行讲述。

为了更加形象、具体地描述这个过程,本文以构建一个基于 WenetSpeech 数据集训练的pruned transducer stateless2[5]recipe 为例,希望尽可能为读者详细地描述这一过程,也希望读者在本文的基础上能够无障碍地迁移到其他数据集的处理、训练和部署使用上去。

本文描述的过程和展示的代码更多的是为了描述功能,而非详细的实现过程。详细的实现代码请读者自行参考egs/wenetspeech/ASR[6]。

Note: 使用者应该事先安装好k2、icefall、lhotse、sherpa。

第一步:数据准备和处理

对于数据准备和处理部分,所有的运行指令都集成在文件prepare.sh[7]中,主要的作用可以总结为两个:准备音频文件并进行特征提取、构建语言建模文件。

准备音频文件并进行特征提取

(注:在这里我们也用了 musan 数据集对训练数据进行增广,具体的可以参考prepare.sh[8]中对 musan 处理和使用的相关指令,这里不针对介绍。)

下载并解压数据

为了统一文件名,这里将数据包文件名变为 WenetSpeech, 其中 audio 包含了所有训练和测试的音频数据


WenetSpeech.json中包含了音频文件路径和相关的监督信息,我们可以查看WenetSpeech.json文件,部分信息如下所示:


(注:WenetSpeech 中文数据集中包含了 S,M,L 三个不同规模的训练数据集)


利用 lhotse 生成 manifests

关于 lhotse 是如何将原始数据处理成jsonl.gz格式文件的,这里可以参考文件wenet_speech.py[9], 其主要功能是生成recordings和supervisions的jsonl.gz格式文件


这里,可用vim对recordings和supervisions的jsonl.gz文件进行查看, 其中:

wenetspeech_recordings_S.jsonl.gz:


wenetspeech_supervisions_S.jsonl.gz:


由上面两幅图可知,recordings用于描述音频文件信息,包含了音频样本的 id、具体路径、通道、采样率、子样本数和时长等。supervisions用于记录监督信息,包含了音频样本对应的 id、起始时间、时长、通道、文本和语言类型等。

接下来,我们将对音频数据提取特征。

计算、提取和贮存音频特征

首先,对数据进行预处理,包括对文本进行标准化和对音频进行时域上的增广,可参考文件preprocess_wenetspeech.py[10]。


其次,将数据集切片并对每个切片数据集进行特征提取。可参考文件compute_fbank_wenetspeech_splits.py[11]。

(注:这里的切片是为了可以开启多个进程同时对大规模数据集进行特征提取,提高效率。如果数据集比较小,对数据进行切片处理不是必须的。)


最后,待提取完每个切片数据集的特征后,将所有切片数据集的特征数据合并成一个总的特征数据集:


至此,我们基本完成了音频文件的准备和特征提取。接下来,我们将构建语言建模文件。

构建语言建模文件

在RNN-T模型框架中,我们实际需要的用于训练和测试的建模文件有tokens.txt、words.txt和Linv.pt。我们按照如下步骤构建语言建模文件:

规范化文本并生成 text

在这一步骤中,规范文本的函数文件可参考text2token.py[12]。


text的形式如下:


分词并生成 words.txt

这里我们用jieba对中文句子进行分词,可参考文件text2segments.py[13]。


text_words_segmentation的形式如下:


words_no_ids.txt的形式如下:


words.txt的形式如下:


生成 tokens.txt 和 lexicon.txt

这里生成tokens.txt和 lexicon.txt 的函数文件可参考prepare_char.py[14]。


tokens.txt的形式如下:


lexicon.txt的形式如下:


至此,第一步全部完成。对于不同数据集来说,其基本思路也是类似的。在数据准备和处理阶段,我们主要做两件事情:准备音频文件并进行特征提取、构建语言建模文件。

这里我们使用的范例是中文汉语,建模单元是字。在英文数据中,我们一般用 BPE 作为建模单元,具体的可参考egs/librispeech/ASR/prepare.sh[15]。

第二步:模型训练和测试

在完成第一步的基础上,我们可以进入到第二步,即模型的训练和测试了。这里,我们根据操作流程和功能,将第二步划分为更加具体的几步:文件准备、数据加载、模型训练、解码测试。

文件准备

首先,创建 pruned_transducer_stateless2 的文件夹。


其次,我们需要准备数据读取、模型、训练、测试、模型导出等脚本文件。在这里,我们在egs/librispeech/ASR/pruned_transducer_stateless2[16]的基础上创建我们需要的文件。

对于公共的脚本文件(即不需要修改的文件),我们可以用软链接直接复制过来,如:


其他相同文件的操作类似。另外,读者也可以使用自己的模型,替换本框架内提供的模型文件即可。

对于不同的脚本文件(即因为数据集或者语言不同而需要修改的文件),我们先从egs/librispeech/ASR/pruned_transducer_stateless2中复制过来,然后再进行小范围的修改,如:


在本示例中,我们需要对train.py中的数据读取、graph_compiler(图编译器)及 vocab_size 的获取等部分进行修改,如(截取部分代码,便于读者直观认识):

数据读取:


graph_compiler:


vocab_size 的获取:


更加详细的修改后的 train.py 可参考egs/wenetspeech/ASR/pruned_transducer_stateless2/train.py[17]。其他 decode.py、pretrained.py、export.py 等需要修改的文件也可以参照上述进行类似的修改和调整。

(注:在准备文件时,应该遵循相同的文件不重复造轮子、不同的文件尽量小改、缺少的文件自己造的原则。icefall 中大多数函数和功能文件在很多数据集上都进行了测试和验证,都是可以直接迁移使用的。)

数据加载

实际上,对于数据加载这一步,也可以视为文件准备的一部分,即修改文件asr_datamodule.py[18],但是考虑到不同数据集的 asr_datamodule.py 都不一样,所以这里单独拿出来讲述。

首先,这里以egs/librispeech/ASR/pruned_transducer_stateless2/asr_datamodule.py[19]为基础,在这个上面进行修改:


其次,修改函数类的名称,如这里将LibriSpeechAsrDataModule修改为WenetSpeechAsrDataModule,并读取第一步中生成的jsonl.gz格式的训练测试文件。本示例中,第一步生成了data/fbank/cuts_L.jsonl.gz,我们用load_manifest_lazy读取它:


其他的训练测试集的jsonl.gz文件读取和上述类似。另外,对于train_dataloaders、valid_dataloaders和test_dataloaders等几个函数基本是不需要修改的,如有需要,调整其中的具体参数即可。

最后,调整修改后的asr_datamodule.py和train.py联合调试,把WenetSpeechAsrDataModule导入到train.py,运行它,如果在数据读取和加载过程中不报错,那么数据加载部分就完成了。

另外,在数据加载的过程中,我们也有必要对数据样本的时长进行统计,并过滤一些过短、过长且占比极小的样本,这样可以使我们的训练过程更加稳定。

在本示例中,我们对 WenetSpeech 的样本进行了时长统计(L 数据集太大,这里没有对它进行统计),具体的可参考display_manifest_statistics.py[20],统计的部分结果如下:


根据上面的统计结果,我们在train.py中设置了样本的最大时长为 15.0 seconds:


模型训练

在完成相关必要文件准备和数据加载成功的基础上,我们可以开始进行模型的训练了。

在训练之前,我们需要根据训练数据的规模和我们的算力条件(比如 GPU 显卡的型号、GPU 显卡的数量、每个卡的显存大小等)去调整相关的参数。

这里,我们将主要介绍几个比较关键的参数,其中,world-size表示并行计算的 GPU 数量,max-duration表示每个 batch 中所有音频样本的最大时长之和,num-epochs表示训练的 epochs 数,valid-interval表示在验证集上计算 loss 的 iterations 间隔,model-warm-step表示模型热启动的 iterations 数,use-fp16表示是否用16位的浮点数进行训练等,其他参数可以参考train.py[21]具体的参数解释和说明。

在这个示例中,我们用 WenetSpeech 中L subset训练集来进行训练,并综合考虑该数据集的规模和我们的算力条件,训练参数设置和运行指令如下(没出现的参数表示使用默认的参数值):


到这里,如果能看到训练过程中的loss记录的输出,则说明训练已经成功开始了。

另外,如果在训练过程中,出现了Out of Memory的报错信息导致训练中止,可以尝试使用更小一些的max-duration值。如果还有其他的报错导致训练中止,一方面希望读者可以灵活地根据实际情况修改或调整某些参数,另一方面,读者可以在相关讨论群或者在icefall 上通过issues和pull request等形式进行反馈。

如果程序在中途中止训练,我们也不必从头开始训练,可以通过加载保存的某个epoch-X.pt或checkpoint-X.pt模型文件(包含了模型参数、采样器和学习率等参数)继续训练,如加载 epoch-3.pt 的模型文件继续训练:


这样即使程序中断了,我们也不用从零开始训练模型。

另外,我们也不用从第一个batch进行迭代训练,因为采样器中保存了迭代的 batch 数,我们可以设置参数--start-batch xxx, 使得我们可以从某一个 epoch 的某个 batch 处开始训练,这大大节省了训练时间和计算资源,尤其是在训练大规模数据集时。

在 icefall 中,还有更多类似这样人性化的训练设置,等待大家去发现和使用。

当训练完毕以后,我们可以得到相关的训练log文件和tensorboard损失记录,可以在终端使用如下指令:


如在使用上述指令之后,我们可以在终端看到如下信息:


将上述显示的tensorboard记录查看网址复制到本地浏览器的网址栏中即可查看。如在本示例中,我们将 https://tensorboard.dev/experiment/wM4ZUNtASRavJx79EOYYcg/ 复制到本地浏览器的网址栏中,损失函数的 tensorboard 记录如下:


(PS: 读者可从上图发现,笔者在训练 WenetSpeech L subset 时,也因为某些原因中断了训练,但是,icefall 中人性化的接续训练操作让笔者避免了从零开始训练,并且前后两个训练阶段的loss和learning rate曲线还连接地如此完美。)

解码测试

当模型训练完毕,我们就可以进行解码测试了。

在运行解码测试的指令之前,我们依然需要对decode.py进行如文件准备过程中对train.py相似位置的修改和调整,这里将不具体讲述,修改后的文件可参考decode.py[22]。

这里为了在测试过程中更快速地加载数据,我们将测试数据导出为webdataset要求的形式(注:这一步不是必须的,如果测试过程中速度比较快,这一步可以省略),操作如下:


同时,在asr_datamodule.py中修改test_dataloader函数,修改如下(注:这一步不是必须的,如果测试过程中速度比较快,这一步可以省略):


待修改完毕,联合调试 decode.py 和 asr_datamodule.py, 解码过程能正常加载数据即可。

在进行解码测试时,icefall 为我们提供了四种解码方式:greedy_search、beam_search、modified_beam_search和fast_beam_search,更为具体实现方式,可参考文件beam_search.py[23]。

这里,因为建模单元的数量非常多(5500+),导致解码速度非常慢,所以,笔者不建议使用 beam_search 的解码方式。

在本示例中,如果使用 greedy_search 进行解码,我们的解码指令如下 ( 关于如何使用其他的解码方式,读者可以自行参考 decode.py):


运行上述指令进行解码,在终端将会展示如下内容(部分):

............
2022-06-30 16:58:17,232 INFO [decode.py:487] About to create model
2022-06-30 16:58:17,759 INFO [decode.py:508] averaging ['pruned_transducer_stateless2/exp/epoch-9.pt','pruned_transducer_stateless2/exp/epoch-10.pt']
............
2022-06-30 16:58:42,260 INFO [decode.py:393] batch 0/?, cuts processed until now is 104
2022-06-30 16:59:41,290 INFO [decode.py:393] batch 100/?, cuts processed until now is 13200
2022-06-30 17:00:35,961 INFO [decode.py:393] batch 200/?, cuts processed until now is 27146
2022-06-30 17:00:38,370 INFO [decode.py:410] The transcripts are storedinpruned_transducer_stateless2/exp/greedy_search/recogs-DEV-greedy_search-epoch-10-avg-2-context-2-max-sym-per-frame-1.txt
2022-06-30 17:00:39,129 INFO [utils.py:410] [DEV-greedy_search] %WER 7.80% [51556 / 660996, 6272 ins, 18888 del, 26396 sub ]
2022-06-30 17:00:41,084 INFO [decode.py:423] Wrote detailed error stats to pruned_transducer_stateless2/exp/greedy_search/errs-DEV-greedy_search-epoch-10-avg-2-context-2-max-sym-per-frame-1.txt
2022-06-30 17:00:41,092 INFO [decode.py:440]
For DEV, WER of different settings are:
greedy_search   7.8     bestforDEV
............

这里,读者可能还有一个疑问,如何选取合适的epoch和avg参数,以保证平均模型的性能最佳呢?这里我们通过遍历所有的 epoch 和 avg 组合来搜索最好的平均模型,可以使用如下指令得到所有可能的平均模型的性能,然后进行找到最好的解码结果所对应的平均模型的 epoch 和 avg 即可,如:


以上方法仅供读者参考,读者可根据自己的实际情况进行修改和调整。目前,icefall 也提供了一种新的平均模型参数的方法,性能更好,这里将不作细述,有兴趣可以参考文件decode.py[24]中的参数--use-averaged-model。

至此,解码测试就完成了。使用者也可以通过查看egs/pruned_transducer_stateless2/exp/greedy_search中recogs-*.txt、errs-*.txt和wer-*.txt等文件,看看每个样本的具体解码结果和最终解码性能。

本示例中,笔者的训练模型和测试结果可以参考icefall_asr_wenetspeech_pruned_transducer_stateless2[25],读者可以在icefall_asr_wenetspeech_pruned_transducer_stateless2_colab_demo[26]上直接运行和测试提供的模型,这些仅供读者参考。

第三步:服务端部署演示

在顺利完成第一步和第二步之后,我们就可以得到训练模型和测试结果了。

接下来,笔者将讲述如何利用 sherpa 框架把训练得到的模型部署到服务端,笔者强烈建议读者参考和阅读sherpa使用文档[27],该框架还在不断地更新和优化中,感兴趣的读者可以保持关注并参与到开发中来。

本示例中,我们用的 sherpa 版本为sherpa-for-wenetspeech-pruned-rnnt2[28]。

为了将整个过程描述地更加清晰,笔者同样将第三步细分为以下几步:将训练好的模型编译为 TorchScript 代码、服务器终端运行、本地 web 端测试使用。

将训练好的模型编译为 TorchScript 代码

这里,我们使用torch.jit.script对模型进行编译,使得nn.Module形式的模型在生产环境下变得可用,具体的代码实现可参考文件export.py[29],操作指令如下:


运行上述指令,我们可以在egs/wenetspeech/ASR/pruned_transducer_stateless2/exp中得到一个cpu_jit.pt的文件,这是我们在 sherpa 框架里将要使用的模型文件。

服务器终端运行

本示例中,我们的模型是中文非流式的,所以我们选择非流式模式来运行指令,同时,我们需要选择在上述步骤中生成的cpu_jit.pt和tokens.txt:


注:在上述指令的参数中,port 为6006,这里的端口也不是固定的,读者可以根据自己的实际情况进行修改,如6007等。但是,修改本端口的同时,必须要在sherpa/bin/web/js中对offline_record.js和streaming_record.js中的端口进行同步修改,以保证 web 的数据和 server 的数据可以互通。

与此同时,我们还需要在服务器终端另开一个窗口开启 web 网页端服务,指令如下:


本地 web 端测试使用

在服务器端运行相关功能的调用指令后,为了有更好的 ASR 交互体验,我们还需要将服务器端的 web 网页端服务进行本地化,所以使用 ssh 来连接本地端口和服务器上的端口:


接下来,我们可以在本地浏览器的网址栏输入:localhost:6008,我们将可以看到如下页面:


我们选择Offline-Record,并打开麦克风,即可录音识别了。笔者的一个识别结果如下图所示:


到这里,从数据准备和处理、模型训练和测试、服务端部署演示等三步就基本完成了。

新一代 Kaldi 语音识别开源框架还在快速地迭代和发展之中,本文所展示的只是其中极少的一部分内容,笔者在本文中也只是粗浅地概述了它的部分使用流程,更多详细具体的细节,希望读者能够自己去探索和发现。

总结

在本文中,笔者试图以 WenetSpeech 的 pruned transducer stateless2 recipe 构建、训练、部署的全流程为线索,贯通 k2、icefall、lhotse、sherpa四个独立子项目, 将新一代 Kaldi 框架的数据准备和处理、模型训练和测试、服务端部署演示等流程一体化地全景展示出来,形成一个简易的教程,希望能够更好地帮助读者认识和使用新一代 Kaldi 语音识别开源框架,真正做到上手即用。

参考资料

[1]

k2:https://github.com/k2-fsa/k2

[2]

icefall:https://github.com/k2-fsa/icefall

[3]

lhotse:https://github.com/lhotse-speech/lhotse

[4]

sherpa:https://github.com/k2-fsa/sherpa

[5]

pruned transducer stateless2 recipe:https://github.com/k2-fsa/icefall/tree/master/egs/wenetspeech/ASR

[6]

pruned transducer stateless2 recipe:https://github.com/k2-fsa/icefall/tree/master/egs/wenetspeech/ASR

[7]

prepare.sh:https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/prepare.sh

[8]

prepare.sh:https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/prepare.sh

[9]

wenet_speech.py:https://github.com/lhotse-speech/lhotse/blob/master/lhotse/recipes/wenet_speech.py

[10]

preprocess_wenetspeech.py:https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/local/preprocess_wenetspeech.py

[11]

compute_fbank_wenetspeech_splits.py:https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/local/compute_fbank_wenetspeech_splits.py

[12]

text2token.py:https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/local/text2token.py

[13]

text2segments.py:https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/local/text2segments.py

[14]

prepare_char.py:https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/local/prepare_char.py

[15]

egs/librispeech/ASR/prepare.sh:https://github.com/k2-fsa/icefall/tree/master/egs/librispeech/ASR

[16]

egs/librispeech/ASR/pruned_transducer_stateless2:https://github.com/k2-fsa/icefall/tree/master/egs/librispeech/ASR/pruned_transducer_stateless2

[17]

egs/wenetspeech/ASR/pruned_transducer_stateless2/train.py:https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/pruned_transducer_stateless2/train.py

[18]

asr_datamodule.py:https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/pruned_transducer_stateless2/asr_datamodule.py

[19]

egs/librispeech/ASR/pruned_transducer_stateless2/asr_datamodule.py:https://github.com/k2-fsa/icefall/blob/master/egs/librispeech/ASR/pruned_transducer_stateless2/asr_datamodule.py

[20]

display_manifest_statistics.py:https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/local/display_manifest_statistics.py,

[21]

train.py:https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/pruned_transducer_stateless2/train.py

[22]

decode.py:https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/pruned_transducer_stateless2/decode.py

[23]

beam_search.py:https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/pruned_transducer_stateless2/train.py

[24]

decode.py:https://github.com/k2-fsa/icefall/blob/master/egs/librispeech/ASR/pruned_transducer_stateless5/train.py

[25]

icefall_asr_wenetspeech_pruned_transducer_stateless2:https://huggingface.co/luomingshuang/icefall_asr_wenetspeech_pruned_transducer_stateless2

[26]

icefall_asr_wenetspeech_pruned_transducer_stateless2_colab_demo:https://colab.research.google.com/drive/1EV4e1CHa1GZgEF-bZgizqI9RyFFehIiN?usp=sharing

[27]

sherpa使用文档:https://k2-fsa.github.io/sherpa/

[28]

sherpa-for-wenetspeech-pruned-rnnt2:https://github.com/k2-fsa/sherpa/tree/9da5b0779ad6758bf3150e1267399fafcdef4c67

[29]

export.py:https://github.com/k2-fsa/icefall/blob/master/egs/wenetspeech/ASR/pruned_transducer_stateless2/export.py