“链”模型是一种使用nnet3实现的DNN-HMM模型,它在很多方面都和传统的模型不同。你可以将它们看作声学模型空间中的不同设计点。
在哪里可以找到“链”模型的脚本
目前用于'链'模型的最佳脚本可以在egs / swbd / s5c中的Switchboard设置中找到; 脚本local / chain / run_tdnn_2o.sh是当前最好的脚本。目前可在官方github存储库(https://github.com/kaldi-asr/kaldi.git)的“chain”分支中找到它,它最终将合并到主服务器。
该脚本使用TDNN作为神经网络(我们一直在使用TDNN进行开发,因为它们更容易调整LSTM),并提供比基线TDNN更好的WER WER(文字差错率):11.4%,而最佳TDNN基线为12.1% (在eval2000的仅配电板部分)。
链模型在DNN的输出端使用(当前)3倍降速的帧率。DNN的输入特征是原始帧速率为每秒100帧; 这是有道理的,因为我们当前使用的所有神经网络(LSTM,TDNN)都有某种经常性的连接或拼接,即它们不是纯粹的前馈网络。
与通常模型的不同之处在于用于训练它的目标函数:我们使用正确音素序列的对数概率作为目标函数,而不是帧级目标。训练过程在原理上与MMI训练非常相似,其中我们计算分子和分母的“占领概率”,在导数计算中使用两者之间的差异。没有必要将DNN输出归一化为每帧的总和为1 - 这样的归一化没有什么作用。
由于帧速率降低(每30 ms一帧),我们需要使用修改后的HMM拓扑。我们希望HMM在一个转换中是可遍历的(而不是模型的3个转换与正常帧速率相对)。当前偏好的拓扑具有只能出现一次的状态,然后另一种状态可以出现零次或者多次。使用与基于GMM的模型相同的过程获得状态聚类,尽管具有不同的拓扑(我们将比对转换为新的拓扑和帧速率)。
“链”模型的训练程序
链模型的训练过程是MMI的无格点版本,其中分母状态后验是通过由音素级解码图形成的HMM上的向前-向后算法获得的,并且分子状态后验是通过类似的向前-向后算法来获取的,但限于对应于转录本的序列。
对于神经网络的每个输出索引(即每个pdf-id),我们计算(分子占用概率 - 分母占用概率)形式的导数, 并将它们传播回网络。
分母FST
对于计算的分母部分,我们在HMM上做向前-向后。实际上,因为我们将它表示为有限状态接受器,所以标签(pdf-id)与弧而不是状态相关联,因此在通常的公式中它并不是真正的HMM,但是我们更容易将其视为HMM,因为我们使用前向前-向后算法来获得后验。在代码和脚本中,我们将其称为“分母FST”。
构建分母FST的第一个阶段是创建音素语言模型。该语言模型是从训练数据音素对齐中学习的。这是一种不平滑的语言模型,这意味着我们永远不会退回到低阶n-gram。但是,某些语言模型状态会被完全修剪,因此转换到这些状态会改为低阶n-gram状态。我们避免平滑的原因是减少语音环境扩展后编译图中将出现的弧数。
我们选定的配置是用来估算一个4-gram语言模型和不修剪低于trigram的LM状态(因此我们始终保持至少2-phone历史记录)。除了由不修剪三元组规则决定的状态数量之外,我们还有一个可指定数量(例如2000)的4-gram语言模型状态,这些状态将被保留(所有其余的都用相应的三元组状态标识),我们选择保留的那些是以最大化训练数据可能性的方式确定的。估计所有概率以最大化训练数据可能性。不修剪三元组的原因是任何允许三元组的稀疏性将倾向于最小化编译图形的尺寸。请注意,如果我们的音素LM只是一个简单的音素循环(例如:一个unigram),由于语音的临近序列效应,它会扩展为三音素,但它会有所有可能的三元组的弧。因此,使用未修剪的三元模型得到的任何稀疏性都是一个奖励。根据经验,未平滑的trigram LM可扩展到尽可能小的FST; 并修剪一些trigrams,虽然它增加了编译的FST的大小,导致很少或没有WER改善(至少300小时的数据扩展3倍速度扰动;在较少的数据可能有帮助)。
在Switchboard设置中,我们尝试的各种模型的phone-LM复杂度在5到7之间; 我们所选配置的phone-LM复杂度(4-gram,除了2000个状态以外都被修剪为trigram)大约是6个。并不是因为较低的phone-LM复杂度总是使训练系统有更好的WER; 对于传统的(基于单词的)MMI训练,一个中间强度的语言模型似乎效果最好。
将上一节中描述的音素语言模型扩展为FST,其中把'pdf-id'作为弧,在这个过程中反映了正常Kaldi解码中的解码图编译过程(参见解码图创建配方(测试时间) )),除了没有涉及词典,最后我们将transition-id转换为pdf-id。
一个区别在于我们如何最小化图的大小。正常的配方包括确定化和最小化。我们无法使用此过程减少图形的大小,或者使用消歧符号来减小图形的大小。相反,我们的图形最小化过程可以紧凑地描述如下:“重复3次:推动,最小化,反向;推动,最小化反转。”。'推'指的是推重; “反向”是指弧的方向反转,并交换初始和最终状态。
上面提到的图形创建过程自然地给出了一个初始状态,以及每个状态的最终概率; 但这些不是我们在向前-向后中使用的那些。原因是这些概率适用于话语边界,但我们训练分裂的是固定长度(例如1.5秒)的话语块。在这些任意选择的切割点上限制HMM的初始状态和最终状态是不合适的。相反,我们使用从“running the HMM”得到的初始概率进行固定次数的迭代并对概率求平均值; 每个状态的最终概率等于1.0。我们有理由这样做,但现在没有时间解释它。在分母向前-向后过程中,我们将这些初始和最终概率应用于初始和最终帧作为计算的一部分。但是,我们还写出了具有这些初始和最终概率的分母FST版本,我们将其称为“归一化FST”。(使用epsilon弧模拟初始概率,因为FST不支持初始概率)。这种“标准化FST”将用于以稍后我们将描述的方式向分子FST添加概率。
作为我们准备训练过程的一部分,我们为每个话语生成一种称为“分子FST”(“numerator FST ”)的东西。分子FST编码了监督转录本,并且还编码该转录本的对齐(即,它强制与从基线系统获得的参考对齐相似),但是它允许稍微的“摆动空间”与该参考不同。默认情况下,我们允许音素在格子对齐中分别在其开始和结束位置之前或之后0.05秒发生。合并对齐信息非常重要,因为我们不是对整个话语进行训练,而是对分裂的固定长度的话语进行训练(这对于基于GPU的训练很重要):如果我们将话语分成几部分知道转录本对齐的地方。
我们使用特定于话语的图解作为解码图,通过格子生成解码过程生成训练数据的替代发音的网格,而不是强制执行训练数据的特定发音。这将生成的最佳得分发音内的所有发音对齐。
如上所述,我们训练固定大小的话语片段(例如长度为1.5秒)。这要求我们将分子FST分成固定大小的碎片。这并不难,因为分子FST(记住,编码时间对齐(time-alignment)信息)自然具有我们可以识别具有特定帧索引的任何FST状态的结构。注意:在我们进行此拆分的阶段,分子FST中没有损失 - 它只是被视为编码路径上的约束 - 因此我们不必决定如何分割路径上的损失。
上图(分母FST的汇编)提到了如何计算分母FST的初始和最终概率,以及我们如何在“归一化FST”( 'normalization FST')中对这些概率进行编码。我们用这个“归一化FST”组成分子FST的分割部分,以确保分母FST的损失反映在分子FST中。这确保了目标函数永远不会是正的(这使得它们更容易解释),并且还防止分子FST可能包含分母FST不允许的状态序列的可能性,这原则上可以允许目标函数在不受约束的条件下增加。这钟情况可能发生的原因是音素LM缺乏平滑,并且从1-best对齐估计,因此格子可能包含有在训练中看不到的音素n-gram序列。
偶尔(但很少)会发生这种归一化过程产生空FST的情况:这种情况可能发生在当格子包含用于训练音素语言模型的1-best对齐中不存在的三音素时,并且没有任何可选路径的格可以弥补由此产生的“失败”路径。这是有可能发生的,因为1-bes和格子产生对齐选择了单词的不同发音。这些话语都被丢弃了。
分子FST是加权接受器,其中标签对应于pdf-id加1。我们不能使用pdf-id,因为它们可能为零; 并且零是由OpenFST专门处理的(如epsilon)。当我们形成小批量时,我们实际上将它们放在一起以形成更长的FST,而不是存储一系列单独的分子FST; 这使我们能够对小批量中的所有话语进行单向向前-向后,直接计算总的分子对数概率。(这不是一个重要的特性,它只是一个软件细节,我们在这里解释,以免产生混淆)。
为了训练小批量,我们将我们的话语分成固定长度的语音块(在我们当前的脚本中长度为1.5秒)。比这更短的话语被丢弃; 那些更长的,被分成块,块之间有重叠,或块之间有小间隙。请注意,我们的声学模型通常需要左或右框架用于声学环境; 我们对其进行了补充,但这是一个单独的问题; 在确定块之后添加环境。
我们的小批量大小通常是2的幂,它可能受GPU内存考虑因素的限制。我们的许多示例脚本每个小批量使用128个块。GPU内存的最大单个使用者是向前-向后计算中的alpha概率。例如,对于1.5秒的块,我们在3倍分段抽样后有50个时间步长。在我们的Switchboard设置中,典型的分母FST中有30,000个状态。我们对alpha使用单精度浮点数,因此以千兆字节为单位的内存为(128 * 50 * 30000 * 4)/ 10 ^ 9 = 0.768G。
这不会耗尽所有GPU内存,但还有其他内存来源,例如我们在内存中保留两个nnet输出副本,这根据配置需要相当多的内存 -- 例如,将30000替换为10000,它将为您提供在合理配置中用于一个nnet输出副本的内存量。
在神经网络训练中,我们已经有了生成扰动数据的方法来人为地增加我们训练的数据量。我们的标准nnet3神经网络训练示例脚本通过0.9, 1.0和1.0的因子对原始音频进行时间扭曲,以创建3倍的增强数据。这与'链'模型正交,我们这样做(或不这样做)就像我们对基线一样。但是,有一种额外的方法可以通过移动帧来增加链模型的数据。这些模型的输出帧速率是常规帧速率的三分之一(当然是可以配置的),这意味着我们只评估nnet输出t
值为3的倍数,因此我们可以通过将训练样例移动0,1和2帧来生成不同版本的训练数据。这是在训练脚本中自动完成的,当我们从磁盘读取训练样例时,它“即时”完成 - 程序nnet3-chain-copy-egs
有一个由脚本设置的选项–frame-shift(帧-移位)
。这会影响解释epochs数量。如果用户请求例如4个epochs,那么我们实际上训练了12个epochs; 我们只是对3个不同移位版本的数据这样做。选项–frame-shift=t
选项实际上做的是将输入帧t
移位并将输出帧移动最接近的3到t
。(通常它可能不是3,它是一个名为–frame-subsampling-factor
的配置变量)。
特定于 “链”计算的计算部分是分子FST上的向前-向后和分母HMM上的向前-向后。分子部分非常快。向前-向后分母需要相当多的时间,因为分母FST中可能存在很多弧(例如,典型的Switchboard设置中有200,000个弧和30,000个状态)。所花费的时间几乎与计算的神经网络部分所花费的时间一样多。我们非常小心地确保内存的位置。
进一步加快这一步骤的下一步可能是实现向前-向后计算的修剪版本(如修剪的Viterbi,但计算后验)。为了获得加速,我们必须削减很高比例的状态,因为我们需要弥补修剪带来的内存局部性损失。在我们当前的实现中,我们仔细地确保一组GPU线程都处理相同的HMM状态和时间,只是来自不同的块(我们在代码中调用这些不同的'序列'); 并且我们确保对应于这些不同序列的内存为止在内存中彼此相邻,因此GPU可以进行“整合内存访问”。使用状态级修剪,因为不同序列的内存访问将不再“同步”,我们会失去这个优势。但是,获得向前-向后算法的修剪版本仍然是可行的。
对于速度,我们不在分母图的alpha-beta计算中使用对数值。为了将所有数值保持在合适的范围内,我们将每一帧上的所有声学概率(指数化nnet输出)乘以选定的“任意值”,以确保我们的alpha分数保持在良好的范围内。我们称之为“任意值”,因为算法的设计使得我们可以在这里选择任何值,并且它在数学仍然上是正确的。我们将一个HMM状态指定为“特殊状态”,并且选择“任意常数”是前一帧上该特殊状态的alpha的倒数。这使特殊状态的alpha值保持接近1。作为“特殊状态”,我们选择一个在HMM限制分布中具有高概率的状态,并且它可以进入大部分的HMM状态。
使用'链'模型的解码过程与基于常规nnet3神经网络的模型完全相同,实际上它们使用相同的脚本(steps / nnet3 / decode.sh)。但是也有一些配置差异:
–self-loop-scale 1.0
到utils/mkgraph.sh
脚本。.mdl
文件中设置先验矢量;我们确保解码器在没有设置先验的前提下,只省略除法。。–acwt 1.0
选项给脚本。steps/nnet3/decode.sh
。–post-decode-acwt 10.0
给脚本steps/nnet3/decode.sh
,它在转储格子之前将声学概率缩放10。在此之后,最佳语言模型比例将在10左右,如果您不了解此问题,可能会有点混乱,但是对于评分脚本的设置方式来说是很方便的。一旦使用该–acwt 1.0
选项,默认解码和点阵光束适用于“链”模型而无需修改。但是,它们不会显示完全可能的加速,并且您可以通过使用稍微紧一点的光束来获得更快的解码。通过在Switchboard设置中收紧光束,我们能够将解码时间从实时的1.5倍降低到实时的约0.5倍,精度降低仅约0.2%(这是通过CPU的神经网络评估;在GPU上本来会更快)。Dan提供的注意事项:在我写这篇文章的时候,这是我最好的回忆; 实际上,退化退化可能不止于此。请记住,这是在高性能的现代服务器机器(单线程)上。
您可能会注意到当前的示例脚本中 我们使用的是iVectors。我们这样做只是因为它们通常有所帮助而且因为我们正在使用它们比较基线设置,。“链”模型没有内在联系,也没有使用它们的基本要求。