Kaldi中的Chain模型

Chain模型的训练流程

链式模型的训练过程是MMI的无网格的版本,从音素级解码图生成HMM,对其使用前向后向算法,获得分母状态后验,通过类似的方式计算分子状态后验,但限于对应于转录的序列。

对于神经网络的每个输出索引(即对于每个pdf-id),我们计算(分子占有概率 - 分母占用概率)的导数,并将它们在网络中反向传播。

分母FST

对于计算中的分母部分,我们对HMM进行前向-后向计算。实际上,由于我们把它表示为一个有限状态接受器,标签(pdf-id)与弧而不是状态相关联,所以在正常的公式中分母FST并不是真正的HMM。不过可以简单地将它看作是HMM,因为我们使用前向-后向算法来获得后验。在代码和脚本中,我们将其称为"分母FST"。

分母FST所需的音素语言模型

构建分母FST的第一个阶段是创建一个音素语言模型。这种语言模型是从训练数据的音素对齐中学习得到的。这是一个不平滑的语言模型,这意味着我们不能将它简化为低阶n-gram。然而,在构建时,一些语言模型状态被完全删除,所以,连接到这些状态的转移,将被连接到低阶n-gram的状态。我们避免进行平滑的原因是为了减少在声学上下文扩展和编译后的图中将出现的弧的数量。

   

我们的设定是估计一个4-gram语言模型,并且永远不会修剪低于3-gram的语言模型状态(所以我们始终保持至少两个音素的历史记录)。在无修剪3-gram规则规定的状态数量之上,我们可以保留指定数量(例如2000)的4-gram语言模型状态(其余的都以相应的3-gram状态表示),我们保留的是那些可最大化训练数据的似然的状态。估计所有概率,使训练数据的似然最大。不修剪3-gram的原因是3-gram的任何稀疏度将倾向于最小化编译后图的大小。需要注意的是,如果我们的音素LM只是一个简单的音素回路(即一个unigram),由于语音上下文的影响,它将会扩展到triphones,但它会有所有可能的3-grams中的弧。所以我们从使用未修剪的3-gram模型中获得的任何稀疏度都是有好处的。从经验来说,一个不平滑的3-gram LM可能是一个最小FST;并且修剪一些trigrams,同时增加编译的FST的大小,导致很少或没有WER的改进(至少在3倍速度扰动扩展的300小时数据上是这样的;不过在较少的数据上可能有帮助)。

   

Switchboard的配置中,我们尝试了各种复杂度(57)的音素语言模型;我们选择的配置(4-gram,除了2000个状态以外的所有状态都修剪为3-gram)的音素-LM复杂度大概是6。更低复杂度的音素LM总会导致训练模型更好的WER;对于常规(基于字的)MMI训练,语言模型的中等强度似乎最有效。

分母FST的编译

上一节中描述的音素语言模型被扩展为FST,"pdf-id"作为弧,通常,在Kaldi解码流程中(除了没有涉及到词汇),解码图的编译过程中(参见解码图创建流程(测试 )),最后我们将transition-ids转换为pdf-id。区别在于我们如何最小化图的大小。 正常的流程包括确定化和最小化。 我们无法通过使用消歧符号、determinizationminimization或它们的变形来减少图的大小。相反,为了对图进行最小化,我们重复使用如下步骤3次:pushminimizereverse; pushminimize reverspush"指weight-pushing; 'reverse'是指将弧线的指向反转,并交换初始状态和最终状态。

   

初始、最终概率和'归一化FST'

上面提到的图的创建过程自然地给了我们一个初始状态和每个状态的最终概率;但这些不是我们在前向-后向中使用的。原因是这些概率适用于话语边界,但是我们训练一个固定长度(例如1.5秒)的分割的话语块。将HMM的初始状态和最终状态限制在这些任意选择的切割点是不合适的。相反,我们使用从"运行HMM"导出的初始概率进行固定次数的迭代,并对概率进行平均;最终概率等于1.0。我们这么做是有理由的,但现在没有时间解释。在分母词图的前向-后向过程中,我们将这些初始和最终概率应用于初始和最终帧作为计算的一部分。然而,我们也写出一个具有这些初始和最终概率的分母FST的版本,我们称之为"归一化FST"。 (初始概率使用ε弧进行仿真,因为FST不支持初始概率)。这种"归一化FST"将增加分子FST中的概率,稍后将描述这种增加概率的方式。

   

分子FST

作为我们准备训练过程的一部分,我们为每个话语产生一个叫做"分子FST"的东西。分子FST编码监督转录物,并且还编码该转录物的比对(即,其强制与从基线系统获得的参考对准相似),但是分子FST允许与转录有部分差异,这种差异用"摆动空间"来描述。默认情况下,在lattice对齐中,我们允许一个音素过早或延迟0.05秒。与对齐信息的整合很重要,因为我们并不训练整句话,而是对分割开的固定长度的一段进行训练(对于基于GPU的训练而言这是很重要的),也就是说,需要根据对齐,将话语分成多个部分。

   

作为训练数据的特定发音,我们不用强制使用训练数据的特定发音,而是使用lattice格式的对齐(由lattice解码流程生成,steps/align_fmllr_lat.sh),作为训练数据发音的替代,使用对应于一段语句的图作为解码图。这会产生beam范围内最高得分的所有发音的对齐。

固定长度的chunkmini-batches

   

为了使用小批量数据进行训练,我们将我们所有的语音分解成固定长度的语音块(在我们当前的脚本中长度为1.5秒)。丢弃短于此长度的语音;长于此长度的语音,被分割成多个块(块之间可能有较短的重叠或间隙)。请注意,我们的声学模型通常需要用到声学语境的左侧或右侧的帧,因此,需要在语音块分割完后添加上下文(left-context, right-context)。

   

minibatch通常设定为2的幂,并且受到GPU内存的限制。我们的许多示例脚本讲minibatch设定为128 GPU内存的最大单次消耗是前向后向计算中的alpha概率。例如,chunk1.5秒,经过3次降采样之后,我们有50time steps。在Switchboard配置中,分母FST30000个状态。我们使用单精度浮点来表示alpha,使用的内存为(128 * 50 * 30000 * 4/ 10 ^ 9 = 0.768G

   

这并不会耗尽所有的GPU内存,但还有其他的地方需要消耗内存,例如我们在内存中保留nnet输出的两个副本,这将消耗大量的内存,如果设定分母FST的状态为10000个而非30000,它将为nnet输出的副本留出足够的内存。

使用帧偏移的数据进行训练

在神经网络训练中,我们已经可以通过对数据进行扰动的方法来人为地增加我们训练的数据量。标准的nnet3训练示例脚本中使用0.9,1.01.0的因子对原始音频进行时间扭曲,以得到3倍的数据。这是chain模型的模型配置。如果基线做了速度扰动,chain模型也需要这么做,反之亦然。然而,有一种额外的方法可以让我们对数据进行扩充,即对帧进行偏移。这些模型的输出帧速率是常规帧速率的三分之一(可配置),这意味着我们只计算时间t3的倍数的输出,因此我们可以通过对训练样本偏移0帧,1帧和2帧来生成不同版本的训练数据。这可以在训练脚本中自动完成,并且在我们从磁盘读取训练样本时就已经完成,程序nnet3-chain-copy-egs具有由脚本设置的-frame-shift选项。这会影响epoch的数量。如果用户要求,例如4epochs,那么我们实际上训练了12epochs;我们只是在3种不同版本的数据中进行。选项-frame-shift = t选项实际上是将输入帧进行t个移位并将输出的帧进行偏移,偏移位数为接近t3的倍数(n%3==0, n~t)。 (这个倍数可能不是3,而是一个名为-frame-subsampling-factor的配置变量)。

训练时GPU的问题

chain的计算包括分子FST和分母HMM上的前向后向计算。分子FST的计算是非常快的。而分母前向后向计算需要花费相当多的时间,因为分母FST中可能会有很多的弧(例如默认配置的Switchboard中有200,000个弧和30,000个状态)。所花费的时间几乎与计算神经网络所需的时间一样多。

   

为了进一步加速前向后向算法的计算,需要对前向后退计算进行简化(像pruned Viterbi一样,但只计算后验概率)。为了进行加速,我们必须将很大一部分的状态修剪掉,因为我们需要弥补修剪会带来的内存损失。在我们目前的实现中,我们非常小心,确保一组GPU线程都处理相同的HMM状态和时间,只是从不同的块(我们在代码中称为这些不同的"序列");并且我们确保与这些不同序列相对应的存储器位置在存储器中彼此相邻,因此GPU可以进行"整合存储器访问"。通过状态级修剪,由于不同序列的内存访问将不再"同步",所以我们将失去这一优势。尽管如此,仍然可以获得修剪版本的前向后退算法。

   

为了速度,我们不会在分母图的alpha-beta计算中使用对数值。为了将所有的数值保持在一个合适的范围内,我们将每一帧的所有的声学概率(即指数形式的nnet输出)进行累积,选择一个"任意值",以确保我们的α分数保持在一个好的范围内。我们将其称为"任意值",因为算法的设计使得我们可以在此处选择任何值,并且在数学上仍然是正确的。我们将一个HMM状态指定为"特殊状态",该"任意常数"选取为特殊状态前一帧的α的倒数。这使得特殊状态的alpha值接近于1。作为"特殊状态",我们选择在HMM的有限分布中具有高概率的状态,并且可以访问HMM的大部分状态。

   

Chain模型的解码

chain模型的解码过程与常规的基于nnet3的模型完全相同,实际上,它们使用相同的脚本(steps/nnet3/decode.sh)。不过,还是有一些配置差异:

首先,构建chain的图时,使用更简单的拓扑结构;但这不需要用户的特定操作,因为图的构建脚本所需的拓扑结构是通过'final.mdl'中提取的,该'final.mdl'chain训练脚本生成的,模型中包含有正确的拓扑。

默认情况下,编译图时,我们将"self-loop-scale"设定为0.1。这影响编译程序对自环上的转换概率的处理方式(通常会更好)。然而,对于chain模型来说,由于它的训练方式,我们需要使用与训练时完全相同的概率缩放系数,为简单起见,我们设置为1.0。因此在utils/mkgraph.sh脚本中将选项-self-loop-scale设定为1.0

在这些模型中没有必要"以先验进行分割"。所以在.mdl文件中,我们根本不设置先验向量。当未设定先验向量时,解码器会忽略这个步骤。

通常在解码中使用的默认声学权重(0.1)是不恰当的 - 对于chain模型,最佳声学权重接近于1。因此,我们脚本steps/nnet3/decode.sh中将选项-acwt默认设定为1.0

评分脚本对语言模型进行打分(搜索)时,语言模型权重只能设定为整数,在一些实例脚本的设置中,最佳的语言模型权重介于1015之间,当权重接近于1时,效果不好。(注意:可以将语言模型权重视为声学模型权重的倒数)。为了解决"语言模型权重只能为整数"这个问题而不改变评分脚本(这评分脚本是特定于数据库的),我们向脚本steps/nnet3/decode.sh中加入了一个新的选项-post-decode-acwt 10.0,在生成lattice之前,它将声学概率缩小10倍。此后,最佳语言模型权重大约为10

   

只要不修改声学模型权重,默认的decode beamlattice beam对于'chain'模型来说是合适的。然而,一旦使用-acwt 1.0选项,可能不会明显加速解码,您可以通过使用稍微更小的decode beam来获得更快的解码。通过在Switchboard中缩小光束,我们可以将解码时间从实时的大约1.5倍降低到实时的大约0.5倍,精度只降低0.2%左右(这是神经网络在CPU上的结果;在GPU上应该会更快)。Dan指出:以上我的实验得出的最好的结果,实际上的解码速度可能会超过了上述结果。请记住,这是在大功率的最新的服务器机器(单线程)上运行的结果。

   

您可能会注意到,在当前的示例脚本中,我们使用了iVectors。我们这样做只是因为他们通常会对性能有一点帮助,并且,因为我们在基线中也使用了iVectors,为了对chain与基线进行对比(控制变量),我们在chain模型中也使用了iVectorsiVectorschain模型没有固有的联系,使用chain模型也不一定需要使用iVectors。其实我们想摆脱iVectors的使用(见下文)。

Chain模型的下一步工作(未来与展望)

(注意:此列表生成于20151213日,但可能已过期)。我们需要做的事情(我们想帮助的)是:

在各种语料库中提供示例脚本(并调整它们),看看示例脚本中定义的模型的性能是否与语料(语料的特性:语种、方言、场景、语料量等)有关。

创建并调整训练脚本的LSTMBLSTM版本。(这可能涉及到一些学习率schedules和配置)。

找出如何加快前向-后向的计算。(例如进行状态层面的修剪,或优化当前的Kaldi内核或数据结构)。

   

Dan应该去做的,一个更长远的计划是,为chain模型提供在线解码。实际上chain的在线解码与nnet3的在线解码没有什么不同,因为chain模型类似与普通的nnet3模型。有一点必须要完成的,就是决定是否继续支持iVector,弃用iVector可以大大地简化训练流程,并使得模型更加鲁棒。我们希望LSTMs能够代替iVector,因为LSTMs可以对声学语境进行良好的学习,所以iVector自适应将不再有用,可能会被弃用。我们还有一些其他想法,将模型自适应加入到神经网络中,而不再使用iVectors进行自适应。这将需要一些实验。

   

原文地址:https://www.cnblogs.com/JarvanWang/p/7499589.html