早在2019年Daniel Povey就开始向外界透露他要开发Next-gen Kaldi,如今已经过去两年多了,那么Next-gen Kaldi 到底是什么呢?本文中小编试图通过这几年Daniel的对外演讲内容和日常工作中接触到的信息对Next-gen Kaldi的诞生与发展做一个梳理,希望能给读者大致讲清楚到底Next-gen Kaldi是什么。

(注:本期内容有点东西,适用于大伙儿拿着笔记本就着咖啡和茶来查阅)

Why Next-gen Kaldi

首先,为什么要开发Next-gen Kaldi? 归根结底的原因就两个,一是端到端模型迅速发展起来,神经网络变得越来越复杂,而且更新迭代速度很快。众所周知Kaldi中神经网络是自成一套从头用C++/CUDA实现的,修改网络结构既复杂又不经济。第二,Pytorch、Tensorflow等通用的深度学习工具包逐渐成熟起来,重复造轮子已无必要,而应该更多地关注语音识别任务本身。总之,Kaldi工具集已经没办法满足语音识别的进一步研发需求,所以Next-gen Kaldi的构想就诞生了。
大部分人对Next-gen Kaldi的认识始于k2,大家多多少少都听过k2中实现了可微分的WFST,但其实到目前为止,Next-gen Kaldi经过了大致三个阶段的发展历程。

Kaldi+ (2019.10 - 2020.5)

前面讲了Kaldi在神经网络方面已经有些不合时宜,但Kaldi里面还有很多已被证明行之有效的方法,比如LF-MMI, 比如基于Openfst的解码方法,还有那一整套完备的流程及众多的recipes。所以最初的想法是弃其糟粕留其精华,搞一个Kaldi+, 简单说就是用Pytorch这样的通用深度学习框架来替换Kaldi里面的网络建模部分,如下图所示。具体的做法是将原来Kaldi中的C++/CUDA代码用pybind11封装成python接口,训练的时候将Pytorch网络模型的输出通过python接口送到Kaldi里面,Kaldi负责计算损失函数并将梯度回传给Pytorch。这一部分的工作在Kaldi官方仓库的pybind11分支:
  • https://github.com/kaldi-asr/kaldi/tree/pybind11
大部分的工作已经完成,已经是一个可用的原型系统,有兴趣的同学可以参考Aishell recipe 的s10,里面有完整的流程。

Next-gen Kaldi (2020.5 - 2021.10)

Kaldi+的构想在各路大佬的贡献下,很快就搞出了一个能用的原型,但Daniel很快就感觉这不是他意想中的Next-gen Kaldi,在原来的基础上修修补补太不酷了,而且Kaldi的代码仓库本就已经非常庞大复杂,再加入一堆python的wrapper只会雪上加霜,让Kaldi更加难以入手和维护。于是2020年4月份Daniel陆续在k2仓库提交了一些notes,全新的Next-gen Kaldi拉开了序幕,这些notes现在归档到文档目录下(有兴趣的同学可以读读以了解k2的构想):
  • https://github.com/k2-fsa/k2/tree/master/docs/notes
虽然Next-gen Kaldi以k2而让大家熟知,但其实Next-gen Kaldi包含了三个子项目(k2, lhotse, Icefall), 最近又添加了一个python的服务端框架sherpa:
  • https://github.com/k2-fsa/sherpa
小编科普时间:
这三个项目的命名,k2是世界第二高峰乔戈里峰的别称,其中也包含了Kaldi 2的含义,一语双关;Lhotse是世界第四高峰洛子峰的英文名;Icefall可直译为冰瀑,覆盖于高山之上。另,团队新加的python服务端框架取名sherpa,即夏洛巴人,是喜马拉雅山麓下住着的一个少数民族,以攀登高山及帮助别人攀登高山为业。
这三个项目的关系和分工也很简单明了,k2是核心算法库,最主要就是可微分的有限状态自动机,以及损失函数,另外还有一些需要C++/CUDA实现的辅助工具;Lhotse负责语音数据处理;Icefall就是recipe,里面是神经网络定义以及训练和解码的流程。
Lhotse是一个独立的数据处理工具集,用来做特征提取,数据增广等音频数据处理的工作,里面还包含了sampler以支持动态batch size的功能。当然,Lhotse的设计初衷是为所有机器学习任务提供统一高效的音频数据处理工具,不仅仅是用在语音识别,语音分离、说话人识别等其他语音任务一样可以适用。Lhotse在ASR中的运用,在Icefall中已经有很好的样例,如果对其他的功能和实现细节有兴趣可以关注项目动态:
  • https://github.com/lhotse-speech/lhotse,此处将不赘述。

Why k2

大家最感兴趣的无疑是k2这个项目,Next-gen Kaldi就是以k2而让大家熟知。但为什么要有k2,它到底是什么,很多没跟过项目的估计都一头雾水。简单来说,k2研发的其中一个重要目标就是要打造一个全新的Kaldi+。上面有介绍,Kaldi+是为了结合Kaldi 和 Pytorch的优点,在端到端模型中使用LF-MMI。而LF-MMI的计算需要WFST的参与,k2的目标就是在摒弃原Kaldi仓库(内含openfst)下实现这样一套流程。为了达到这个目标,需要重新造一个WFST的轮子,这就是k2。当然,k2并不是openfst的复制,造轮子的目的不是为了造而造,是因为的确没有这样的轮子,而且也没办法在旧轮子上改造。所以,k2中包含了很多技术创新及优雅的实现,为了让WFST能参与进训练,k2实现了WFST的可微分,为了能高效训练和解码,k2让WFST跑在了GPU上,等等。有兴趣的同学可以从如下链接了解k2的核心概念,
  • https://k2-fsa.github.io/k2/core_concepts/index.html
更好的方式是读一读k2的代码:
  • https://github.com/k2-fsa/k2

k2如何工作?

在理解可导的FSA之前,我们先看一个最简单的Pytorch计算图,他表示的是用CE损失函数训练z = w * x + b 这样一个模型。众所周知,在Pytorch中只需将想训练的向量设置requires_grad属性,那么在调用loss.backward()的时候就会自动计算其梯度。无论这个向量距离loss多远,只要他在计算图中并且有路径连接loss,那梯度都会一层层的沿着计算图传递回来。可导的FSA扮演的角色就是下图中的一个圆圈(更多的时候就是CE这个圆圈),所以只要我们有办法把梯度传递回去,就实现了FSA的可导。

怎么理解可导的FSA

FSA的可导,说的是FSA的边上的浮点类型的数据可导,比如每条边上的score,在上面我们提到k2和Pytorch无缝衔接,Pytorch的tensor就可以作为FSA的输入。如下图所示,我们在1的地方给FSA边设置的分数,并让他可导;在2的地方我们运行了一个简单的FSA算法(最短路径)并且在最短路径上使用Tropical Semiring计算了路径的总分数;对tot_scores调用backward()后,我们可以在3中看到scores的梯度,说明梯度已经通过FSA传递回了最初的Tensor,这就实现了FSA的可导。这个梯度的意思非常明了,最短路径为0->1->3->4,对最终tot_scores有贡献的是第1,3,5三条边,而且贡献相等。这个例子相对简单,最短路径就是单一路径,所以梯度都是1和0。如果计算tot_scores 的图更加复杂,并使用log semiring来计算分数,梯度就会各异了,大家可以自己尝试。

如何使用可导FSA训练模型

理解了可导的FSA的概念,那使用可导FSA来训练模型的思路就很简单了,首先,我们怎么用上FSA,第二我们怎么把梯度传回神经网络。具体的数据流如下图所示,首先是两个图的构建ctc_L 和 DenseFsa,ctc_L图表示能输出当前transcript的所有可能路径(即计算ctc loss的所有alignments),由transcript经由L和ctc topolopy求交集而来;DenseFsa是一种特殊的FSA,他的每个state都有相同的边数(vocab-size + 1),可直接从nnet_output构建。将ctc_L和DenseFsa求交集,就得到了带权值的Lattice,该Lattice上求得的log-semiring total scores 就等于ctc loss中前向后向算法求得的分数。由于k2中FSA的求交集算法是可导的,所以能够把梯度传回神经网络,由此完成一次完整的迭代。
这里只是做一个简单介绍,关于其中各个部分的含义以及各种图怎么构造,后面小编还会给出更详细的解释,关注我们的公众号,期待后续的推送。

Icefall 从何入手?

Icefall是Next-gen Kaldi的示例脚本库,里面包含了使用Pytorch、k2和Lhotse 来构建ASR系统的流程。初来者一看Icefall的仓库可能会觉得有点乱,无从下手,其实Icefall组织代码的方法非常简洁,代码仓库看似庞大,其实目前就两部分内容,一部分是公用辅助函数和类,比如词典、构图、日志、多卡训练等相关的代码,它放在最外层的icefall目录下(如下图左);另一部分是各个数据集的recipe,它放在egs目录下。对于每个数据集,里面都有若干文件夹,比如conformer_ctc、transducer_stateless等,每个文件夹都是独立的recipe,相互之间互不影响。比如你想尝试transducer_stateless 这个模型,你只需要关注这个文件夹里面的代码,运行里面的train.py 和 decode.py就行。这样组织的目的主要是为了规避复杂的配置文件,使代码和模型的配置更加清晰,当然,目前正在快速迭代中,所以代码中不乏冗余,不免使初接触者感到繁乱,稳定之后会做些清理工作。另外要说的就是prepare.sh这个文件,这是Icefall中仅用的shell 脚本,主要是用来准备训练数据和解码所需的图,运行训练与解码之前需先运行该文件准备好必要数据。
所以,想要尝试icefall里的模型特别简单,首先选定一个数据集(如LibriSpeech),
进入egs/librispeech/ASR, 运行 prepare.sh;然后选择想尝试的模型
(如conformer_ctc),运行 python conformer_ctc/train.py, 最后训练完成后,运行 python/conformer_ctc/decode.py。训练和解码脚本会有众多参数,可从代码和文档了解,或者直接在脚本后面加 --help 查看。我们为这个流程写了非常详尽的文档,
可登入如下链接查看:
  • https://icefall.readthedocs.io/en/latest/recipes/librispeech/conformer_ctc.html
其他的数据集和recipe套路相似,亦可照此施行。
最后,强烈建议各位尝试我们最新的RNN-T模型,最新的代码位于:
  • egs/librispeech/ASR/pruned_transducer_stateless4
里面包含了Daniel 改进过的Conformer模型,目前在Librispeech、Gigaspeech、WenetSpeech均已取得SOTA结果。有问题欢迎在github提Issue讨论。

Switch to RNN-T(2021.10 ~ now)

我们在上面的段落中已提及开发可微分WFST的目的之一就是想在端到端模型中继续使用LF-MMI,所以我们选择了CTC+LF-MMI的建模方式。为此我们已经取得了优异的成果,大家可参看:
  • https://github.com/k2fsa/icefall/tree/master/egs/librispeech/ASR/conformer_ctc

  • https://github.com/k2fsa/icefall/tree/master/egs/librispeech/ASR/conformer_mmi

但最终结果仍不如意想的那么好。加之,conformer encoder + transformer decoder的模型并非天然支持实时的识别,为了做到实时识别就必须牺牲部分准确率,transformer decoder的解码中包含自回归也难以做到并行。经过调研之后,我们在2021年冬天决定将研究重点聚焦到RNN-T类模型,因为RNN-T类模型天然支持实时的识别,而且Google、Facebook等大公司均有投入,是一个有潜力的方向。当然,这并不意味着放弃k2,也同样欢迎大家在CTC+LF-MMI上继续前进,事实上,conformer_ctc recipe已经是个支持流式识别并且可用GPU进行解码的系统。另外,k2代码库也将继续在RNN-T模型的高效解码中扮演重要作用。
在决定转向RNN-T类模型之后,我们旋即在快速训练、高效解码、提升性能三方面开展研究,目前均已取得阶段性的成果。在训练方面我们提出并实现了Pruned RNN-T Loss,能在使用少得多内存的情况下取得数倍的速度提升;在解码方面,我们在GPU上实现FSA-based的RNN-T并发解码;另外,我们提出了包括大模型蒸馏等多种提升RNN-T模型性能的方法。

Pruned RNN-T Loss For fast training

Pruned RNN-T Loss的想法非常的简单,通常我们计算RNN-T Loss时需要在T 和 S两个维度上遍历所有的路径(如下左图),但依据常识音频与标注文字应当是单调对应的,真正有效的路径只有左下角至右上角这条对角线上的窄带,如下右图所示,Pruned RNN-T Loss的目标就是要Pruned掉左上角和右下角这部分无效的计算。
具体的实现可由下图来阐明,首先我们会用一个简单的联合网络来求得transducer lattice(上图左)上每个点的梯度(由于简单联合网络只是encoder和decoder的简单相加,所以可以有高效的办法求得这个梯度),然后根据这个梯度来求得剪裁的边界(即对于每一帧在S方向上要保留哪几个点)。获得剪裁边界后,我们将encoder和decoder的输出进行剪裁,然后将剪裁后的矩阵送入非线性的联合网络中,最终计算出损失函数。
所以实际上我们会使用不同的联合网络计算两次loss,如上图中的rnnt_loss_simple 和 rnnt_loss_pruned。虽然看似多进行了一次计算,但由于计算中不涉及(N,T,S,V)四维大矩阵的分配和计算,我们仍然可以取得更高的计算效率,对比结果如下表所示,可以看出相比torchaudio 的rnnt loss, k2 pruned loss 在使用五分之一内存的情况下依然取得超过10倍的速度提升。
Benchmark的详情可参见:
  • https://github.com/csukuangfj/transducer-loss-benchmarking
当然,想法看似简单,但内中细节却很多,在此没法一一讲解清楚,而实现又更加重要,想了解Pruned RNN-T loss 的具体实现可以阅读如下代码:
  • https://github.com/danpovey/fast_rnnt
  • https://github.com/k2-fsa/k2/blob/master/k2/python/k2/rnnt_loss.py
两份代码一样,这个loss已包含在k2仓库中,fast_rnnt 是独立出来的版本。这部分工作近期会有论文释出,敬请关注。

FSA-Based fast RNN-T Decoding

上述章节中提到我们在GPU上实现了FSA-based的高并发解码方法,这个解码方法的实现得益于我们在RNN-T模型上做的两个小创新。第一,我们的decoder网络是一个无状态的网络,从(https://ieeexplore.ieee.org/document/9054419/)修改而来,我们在embedding层后面加了一个一维卷积以扮演N-gram语言模型的作用。第二,我们在解码阶段严格限制每一帧输出一个符号。这使得我们解码过程不存在自回归,而且在每一帧上也不会有循环存在。具体的实现细节文辞上难以尽述,想了解其中细节可以参看Daniel的proposal:
  • https://github.com/k2-fsa/k2/issues/885
最终实现跟这个有些差异;k2中的实现
  • https://github.com/k2-fsa/k2/pull/926
Icefall中的运用:
  • https://github.com/k2-fsa/icefall/pull/250
  • https://github.com/k2fsa/icefall/pull/277

    这部分的成果近期也会有论文释出,敬请关注。

提升模型性能

我们目前的模型结构仍是业界常用的transformer-transducer模式,encoder是一个conformer模型,decoder是上述提到的embedding + Conv1D的无状态模块。我们在这个基础模型上业已取得很好的效果,在librispeech test-clean数据集上使用greedy search就能到2.6%左右的词错率,这和conformer ctc + n-gram rescoring + transformer rescoring取得的结果相当,然而解码却简单快速得多。

想要了解这部分的内容可参看:
  • https://github.com/k2fsa/icefall/tree/master/egs/librispeech/ASR/pruned_transducer_stateless
为了得到更好的准确率和更快的收敛速度,我们又在以下三方面做了很多工作,一个是对Conformer 模型做了大量改进,使得训练更稳定,收敛速度更快;第二个是引入额外的数据来训练RNN-T模型;第三是使用大模型蒸馏的迁移学习方法。

Reworked model

Daniel Povey对Conformer模型做了众多的改进,包含移除BatchNorm,替换LayerNorm,为每个模块加上可学习的权重以将参数限制在一个较小的范围之内,使用新的优化器等。这一部分的内容Daniel已在2022年北京智源大会上分享,大家可以关注我们的公众号,不久后将会放出PPT和演讲视频。
这部分的内容涉及技术细节较多,我们还会在后续的文稿中单独分享,有兴趣的同学可以参看:
  • https://github.com/k2fsa/icefall/tree/master/egs/librispeech/ASR/pruned_transducer_stateless2
    该模型的结果相较原来的模型略好,但是它只需要训练一半的epoch数就可以达到相同的结果,大大降低了训练时间。

Multi-datasets training

多数据源训练的想法来自于不同的数据集可能来自不同的domain,他们的声学特性应该相似,但语言模型各异,如果杂糅在一起使用并不一定能提高特定domain的准确率,而如果只取额外数据的声学部分,则能提高Encoder的建模能力。对于RNN-T类模型,使用多种数据训练模型非常容易,如下图所示,我们使用Librispeech和GigaSpeech共同来训练模型,两个数据集共用Encoder,而分别拥有自己的Decoder和Joiner。方法看起来特别直观,想尝试这个设置的同学可参看:

  • https://github.com/k2fsa/icefall/tree/master/egs/librispeech/ASR/pruned_transducer_stateless3
使用该实验设置,当我们把giga_prob设置成0.9的时候,Librispeech test-clean上的词错率可以降至2.0%,test-other上的词错率可以降至4.63%。

VQ based transfer learning

无监督大模型近年来受到大家追捧,但训练成本过高,实际的产线上使用也不现实,于是大家想到了使用大模型来指导小模型训练的方法。常规的使用方法一般是在训练过程中提取大模型生成的Embeding与小模型生成的Embedding,然后计算其距离以此作为辅助的损失函数,如下图左所示。这样的使用方法尚有不足,在训练小模型时无法抛开大模型(需要提取Teacher Embedding),导致训练时需要大量机器资源并且速度慢。我们提出了一种量化方案(Vector Quantization, VQ),将Teacher Embedding 量化成几个4bits或者8bits的整数(称codebook index,CI),如下图右所示,这大大降低了Teacher Embedding的存储空间,使大模型中的“信息”能够提取出来。训练时可以完全抛开大模型,只需要使用提取的codebook index,让小模型训练的overhead 降至最小。
在该设置下,使用Hubert作为teacher相较没有teacher的模型在Librispeech 数据集上test-clean 和 test-other的词错率从 2.67% & 6.68% 降至 2.30% & 5.57%。VQ部分的代码可参看:
  • https://github.com/k2-fsa/multi_quantization
使用VQ进行训练的示例脚本可参看:
  • https://github.com/k2fsa/icefall/tree/master/egs/librispeech/ASR/pruned_transducer_stateless6
    此部分工作近期也会有论文释出,敬请期待

在开源数据集上的性能

目前,我们使用k2在多个公开的流行数据集上进行了实验,获得了非常有竞争力的识别结果,甚至在一些数据集上获得了SOTA。对于每一个数据集和使用的相关模型,我们都提供了训练和测试的具体参数、训练和测试过程的记录文件和预训练模型,具体的信息可查看:
  • https://github.com/k2-fsa/icefall/tree/master/egs下表展示了目前我们在部分数据集上获得的相应结果:

什么是Next-gen Kaldi?

那么从Kaldi+到k2再到RNN-T,到底什么是Next-gen Kaldi呢?我想Next-gen Kaldi是多元的而非单一的,是进取的而非自满的,是开放的而非保守的。虽然Next-gen Kaldi以可微分有限状态自动机k2而名声在外,但Next-gen Kaldi团队和Daniel本人一直持非常开放的态度,并不会被局限在一个单一技术里面。Daniel已经在多种场合阐述过,Next-gen Kaldi的初衷是打造一个高性能、高效率、可产品化落地的ASR系统,这也是Kaldi当初的梦想。时至今日技术虽然有了演进和发展,但开源语音的精神一直在传承,那就是让智能语音惠及更多的公司、惠及更多的人们。从Kaldi+到k2再到RNN-T就是我们的探索历程,我们走过的每一步都留下了坚实的脚印。所以,Next-gen Kaldi并不只是一个单一的技术,它也包含了构建优异、高效、普惠语音识别系统这样一种愿景和目标。这种愿景目前建构在可微分有限状态自动机k2和RNN-T上,后面可能还会有更多的载体,让我们一起努力,共同浇灌,让Next-gen Kaldi茁壮成长。
欢迎关注、尝试Next-gen Kaldi项目,更期待大家的贡献,它们分别是:


核心算法库k2:

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

语音数据处理工具集Lhotse:

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

数据集示例脚本Icefall:

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

服务端框架sherpa:

https://github.com/k2-fsa/sherpa
(笔耕不辍,文不短意很长,我们下期再会。别找小编,小编到家了。)
福利时间
私信小编说说看
你是从什么时候开始使用Kaldi的
或者
和Kaldi的相关故事
小编将随机抽3位粉丝
送新一代Kaldi相关主题T恤
本文出品:新一代Kaldi-NGK编辑部
撰文:蛋哥的pkufool
继续阅读
阅读原文