Nnet3配置中的上下文和块大小
简介
本页讨论了nnet3配置中关于解码和训练的块大小以及左右上下文的某些术语。这将有助于理解一些脚本。目前,从脚本角度来看,没有任何关于nnet3的"概述"文档,所以这是nnet3唯一的文档。
基础
如果您已阅读了"nnet3"配置的以前的文档,您会发现"nnet3"不仅支持简单的前馈DNN,还可以实现在网络内层进行时间拼接(帧拼接)的时延神经网络(TDNN)以及带有recurrent(循环)拓扑的RNN、LSTM、BLSTM等。所以nnet3有时间轴的概念。下面我们确定一些术语。
左右上下文
只有TDNN才需要左右上下文,LSTM/RNN不需要左右上下文
只有TDNN才需要右下文,LSTM/RNN不需要右下文;TDNN、LSTM、RNN均需要左上文
假设我们需要让神经网络计算特定时间索引时的输出;比如时间t=154。如果该神经网络在内部进行了帧的拼接(或任何其他与"t"索引相关的内容),在这种情况下,如果没有给出当前帧的一定范围左右上下帧,则可能无法计算当前帧的输出。例如,如果没有看到t = 150到t = 157这个范围内的帧,则可能无法计算输出。在这种情况下(忽略细节),我们会说网络的左上下文为4、右上下文为3。上下文的实际计算有点复杂,因为它必须考虑到特殊情况,例如"t"值为奇数或偶数时。
循环拓扑,除了上述"所需"左右上下文外,在训练或解码时,它还需要"额外的"上下文。RNN会利用到超出"所需"上下文的上下文。在脚本中,通常会看到名为extra-left-context和extra-right-context的变量,这意味着"除了需要的内容之外,我们将提供的上下文的数量"。
在某些情况下,左上下文和右上下文意味着添加到chunk中的总的左上下文和总的右上下文,即
左上下文=模型左上下文+额外左上下文
右上下文=模型右上下文+额外右上下文
因此,在某些情况下,您需要搞清楚一个变量指的是模型的左右上下文还是数据块的左右上下文。
在Kaldi5.0及更早版本中,数据块中的左右上下文不受块大小在开头或结尾的影响;在最后我们用第一或最后一帧的副本填充输入。这意味着对于循环拓扑,我们可能会用很多帧(最多40个左右)来填充语句的开始或结束。这没有意义而且很奇怪。在版本5.1和更高版本中,您可以指定extra-left-context-initial和extra-right-context-final,允许话语的开始/结束具有不同的上下文量。如果您指定这些值,通常将它们都指定为0(即没有额外的上下文)。但是,为了与旧版本兼容,它们通常默认为-1(意思是复制默认的左上方和右上方)。
Chunk大小
Chunk的大小是我们在训练或解码中每个数据块所含(输出)帧的数量。在get_egs.sh和train_dnn.py脚本中,chunk-size的也被称为frames-per-eg(在某些上下文中,这与块大小不同;见下文)。在解码中,我们把它称为frame-per-chunk。
对于非RNN、非chain模型、非TDNN
example, /eg-/, 样例,样本,可用其IPA音标"eg"来作为其缩写;在传统的语音识别中,一个example,即一帧以及标签的二元组(frame, label)。
egs,多个eg,多个样本。
对于不使用上下文帧以及任何时间信息的DNN,如以Sigmoid、Tanh、ReLu为神经元的DNN。chunk与egs等同。
对于非RNN、非chain模型、TDNN
对于使用交叉熵目标函数训练的前馈网络或TDNN等非常简单的网络类型,我们在帧级别上打乱整个数据集,并且我们一次只训练一帧。为了在并行化训练时顺序地进行I/O,需要在帧级别上对数据进行预随机化。然而,当训练TDNN时,每帧都需要左右各10帧的上文和下文,并写入到磁盘中,就必须知道某一帧的左右上下文具体是哪些帧,并记录。
然而,不使用chunk,即以普通的方法生成训练样本时,所需的数据量可能会变成原来的20倍:
8帧,总共需要160帧的左右上下文
[-10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[-9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,11]
[-8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ,12]
[-7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
[-6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
[-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
[-4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
[-3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
为了解决这个问题,将
- 某个时间范围内的帧(大小由frame-per-eg控制,默认为8)
- 对应的标签
- 左上下文
- 右上下文
组合为一个块,即chunk,使得这些帧能共享左右上下文帧:
8帧,总共需要20帧的左右上下文
chunk与egs的区别:
chunk:[-10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
eg:[0, 1, 2, 3, 4, 5, 6, 7]
即:chunk是显式包含左右上下文帧的eg
这样,chunk-width为8时,存储的数据大小为原来的1/8;若chunk-width为20时,存储的数据大小为原来的1/20
当训练模型时,将以chunk为单位。
若以帧为单位进行LSTM的训练,一整句话的所有帧能不断地在神经网络中进行前向传播;
在TDNN训练中,chunk包含frame_per_egs个有效输出帧,以及显式包含所需的左右上下文帧。设定大于1的frame_per_egs能减少硬盘空间消耗,但不影响训练结果。
作用:
- 提供所需的左右上下文帧;
- 减少磁盘空间消耗;
TDNN解码时,输入第一帧时,需要10帧左上文和10帧右下文。而第一帧是没有左上文的,因此,在输入层处对第一帧拷贝10次,作为其左上文;而10帧的右下文还没到来,因此无法输出第一帧的输出,需要等到第11帧到来时,才能输出第1帧,等到第12帧到来时,输出第2帧。
因此,时延为11*0.01=0.11秒,且只与右下文有关。
输出到最后一帧时,没有右下文了,将最后一帧拷贝10次,作为其右下文
不需要chunk,直接对整个语句进行解码输出。
left-context-max = max(left-contexti)
right-context-max = max(right-contexti)s
RNN的chunk-size
在RNN(LSTM、BLSTM等)模型或"Chain"模型中,总是训练相当大的chunk(通常在40到150帧的范围内),即chunk大小。当解码时,通常在相当大的chunk(frames-per-chunk=30、50或100)上评估神经网络。对于RNN,尽可能确保在训练时的chunk-size或frames-per-chunk、extra-left-context、extra-right-context与解码时的大致相同,以得到最优结果(尽管有时时解码中的上下文值稍大一些较好)。人们可能会认为在解码时更长的上下文能得到更好的结果,但是并不总是这样(然而,请参见下面的looped decoding,其中提到一个更好的方法)。
在RNN训练中,若不使用chunk,则以语句为单位进行训练,但是,过长的语句会导致训练过慢,且梯度沿时间传播容易衰减甚至消失。因此,RNN的chunk用于解决训练过慢和梯度衰减的问题。将语句分为若干个chunk,每个chunk根据left-context恢复对上一个chunk的记忆;每个chunk共享一段历史信息。
作用:
- 加速训练;
- 防止梯度衰减;
块大小与frame-subsampling-factor的关系
当对输出使用了帧降采样(如chain模型),块大小仍然以"t"的倍数进行测量,我们确保chunk-size是frame-subsampling factor的倍数。比如,chunk-size为90,frame-subsampling-factor为3,那么对大小为90帧的chunk估计30个的输出(例如t = 0,t = 3 ... t = 87)。
可变的chunk大小
在Kaldi 5.1或更高版本中,有时候在训练中使用可变chunk大小。因为当块相当大时,会出现由于一个语句的帧数不是块的整数倍而导致帧的丢失。这时,可以将块大小指定为以逗号分隔的列表(例如150,120,90,75),并且生成训练示例的命令可以创建任何这些大小的块。指定的第一个块大小称为主块大小,并且对于任何给定的话语都是"特殊的",最多允许指定两个非主块大小;剩余的块必须是主块大小。这种限制更容易得到给定长度的文件的最佳分割,并使得生成偏向于具有特定长度的块。
Minibatch大小
nnet3-merge-egs将各个训练样本合并到包含许多不同样本的minibatch中(每个原始样本获得不同的'n'索引)。 minibatch-size是minibatch的大小,指的是:将多个样本的帧以及label组合为一个样本,即eg的数量;
或对于RNN或TDNN,sequences,即chunk;进行组合的数量(例如,minibatch-size = 128)。
minibatch是以chunk为单位,一个chunk即一个样本。
minibatch是以帧为单位,如64。
假设chunk-width=20,那么一个minibatch将横跨3.2个chunk。短句的时长一般为3秒~4秒,设为3秒,设帧移为10ms,则1秒包含1000/10=100帧,一个短句包含300帧,如果minibatch=64,那么一句话被切分为4个minibatch=64*4=256帧,尾部的44帧被丢弃。
因此,minibatch常常也可变。
这种情况下,若minibatch=64,32
这句话就被切分为5个minibatch:64+64+64+64+32=288,尾部只有12帧被丢弃。
当块大小可变时(如果我们设置了extra-left-context-initial和extra-right-context-final,考虑到话语开头/结尾的上下文可能不同),需要确保minibatch中只包含"类似"的样本;即某个样本的上下文可用另一个样本表示,以减小开销。
在Kaldi版本5.1及更高版本中,nnet3-merge-egs仅将相同结构的chunk(即相同的块大小和相同的左右上下文)合并在一起。它持续从输入中读取chunk,直到样本数达到minibatch-size。在5.1之前的Kaldi版本中,通常丢弃那些无法凑足一个minibatch的样本,现在,多种不同的chunk大小就不会丢弃太多数据)。
--egs.chunk-width
egs(examples)中每个chunk包含的帧数。注意:如果将值翻一番,则应将"--trainer.samples-per-iter"值折半。
--trainer.samples-per-iter
每个ark(archive,档案)中包含的egs(examples,样本)数。每个eg(example,样本)包含'chunk_width'个帧。
chunk_width=20、samples_per_iter=20000时;相当于训练普通DNN时每次迭代使用20*20000=400000帧。
可变的Minibatch大小
从Kaldi 5.1及更高版本开始,--minibatch-size的参数可以是一个更通用的字符串,允许用户指定可变而非固定的的minibatch大小。 例如,可以指定--minibatch-size=64,128,这样,对于每种类型的样本,首先以128进行切分并输出,直至输入的末尾;若末尾剩下样本数>=64,再以64为大小进行切分并输出。 --minibatch-size也支持数字范围,如 --minibatch-size=1:64表示首先以64帧为单位进行切分,然后将所有剩余的样本组合为一个minibatch。 还可以为不同大小的样本指定不同的规则(不带参数运行nnet3-merge-egs以获取详细信息);这可使得GPU内存占用不容易溢出。
循环解码(Looped decoding)
可修改"xconfig"配置文件中LSTM组件的decay-time参数,以使用循环解码,如: "decay-time= 20"。这似乎不会降低WER,并且消除了普通解码和循环解码之间的差异(即,它使得网络能够容忍比训练中看到的更长的上下文)。
脚本steps/nnet3/decode_looped.sh(Kaldi 5.1以上)仅接收两个与块或上下文相关的参数:frames-per-chunk(仅影响速度/延迟权衡,而非解码结果),以及extra-left-context-initial,该参数应与训练时(在最新脚本中通常为零)相同。
在撰写本文时,尚未实现支持循环解码的online2-wav-nnet3-latgen-faster;这是我们后续将要实现的。