Non-Autoregressive Sequence Generation
李宏毅老师2020新课深度学习与人类语言处理课程主页:
http://speech.ee.ntu.edu.tw/~tlkagk/courses_DLHLP20.html
视频链接地址:
https://www.bilibili.com/video/BV1RE411g7rQ
图片均截自课程PPT、且已得到李宏毅老师的许可:)
考虑到部分英文术语的不易理解性,因此笔记尽可能在标题后加中文辅助理解,虽然这样看起来会乱一些但更好读者理解,以及文章内部较少使用英文术语或者即使用英文也会加中文注释
深度学习与人类语言处理 P23 系列文章目录
- Non-Autoregressive Sequence Generation
- 前言
- I Overview 非自回归模型引入与问题
- II Models & Methods 非自回归翻译模型与方法
- 2.1 Vanilla NAT :Fertility 数字预测
- 2.2 Sequence-level knowledge distillation 句子级的知识蒸馏
- 2.3 Noisy Parallel Decoding
- 2.4 Evolution of NAT 非自回归翻译模型的演化
- 2.5 NAT :Iterative Refinement 反复修改的非自回归翻译模型
- 2.6 Mask-Predict 带有BERT的NAT
- 2.7 Insertion 插入法
- 2.8 KERMIT 端到端的插入法模型
- 2.9 Levenshtein Transformer 插入删除法的模型
- 2.10 CTC-based 基于CTC的非自回归模型 Imputer
前言
上篇中(P22)我们讲解了条件生成,并主要从三个方面进行的讲解:
Generation 怎么产生一个有结构的东西
Attention 产生结构的辅助
Tips for Generation 一些生成任务的技巧
(注意,上篇内容是老师之前的课程,为了视频和课程的一致性,因此笔者还是写了分享,大家可以复习使用)
而在本篇中,我们将进入 非自回归模型 的序列生成,将讲述有关问题、实现方法、和模型结构。
I Overview 非自回归模型引入与问题
1.1 Autoregressive 自回归模型
对于机器翻译、图片字幕生成都是Conditional Sequence Generation条件生成,一般解决这类问题的方法都是使用类似上图的自回归模型。
具体而言,如果我们使用的是RNN模型,将输入部分一一输入到Encoder中,再将Encoder的输出丢给Decoder部分,由Decoder再一一生成翻译的字,在Decode每一个字的时候都会依赖前面的所有信息。但这样的模型都会由这样的问题,那就是假如我们今天像翻译的句子很长,那么花的时间就和Decode的长度成正比。
那在我们有了Transformer模型之后,我们不需要逐一输入,在Encode的时候是可以并行加快的,可是在Decode的时候还是会遇到一样的问题,逐字输出,花费时间和Decode的长度成正比。
1.2 Non-autoregressive 非自回归模型
既然Transformer模型可以在Encoder端平行运算,直接输入整个句子,那我们能不能让Transformer在Decoder端一样一口气吐出来整个句子,该怎么实现呢?
有这样一种想法,如上图,首先预测Decoder端的长度,然后再直接给Decoder端position embedding位置编码向量作为输入,这样就可以在一个timestamp时间点上输出整个句子了。
但像上面所想的这样的模型其实会有一个很大的问题:multi-modality problem 多模态问题
1.3 multi-modality problem 多模态问题
如果我们直接使用不加任何限制的非自回归模型,如1.2中所使用的模型,我们将会遇到这样的问题,多模态问题。不像自回归模型,每一步输出都是参照前一步的输出得到,而非自回归模型输出的每一部分都是各自独立的、没有任何依存性关系。
举例而言,假如我们输入的是 “Hello”,如上图,Decoder端可能会在第一个位置上得到 ”哈“和“你”这两个字的概率都非常大,在第二个位置上“啰”和“好”概率大,这样的话模型很有可能输出“你啰”或“哈好”这样的翻译,类似于这样的问题就叫做多模态问题。
对于非自回归模型,同样,对于图片也会有这样的问题,如果仅仅最小化输出图片和目标图片的L2距离,我们最后会得到数据集中相关图片的平均值组成的模糊图片。
那对于上述这样的多模态问题,该怎么解决呢?我们将从模型角度讲解解决方法。
II Models & Methods 非自回归翻译模型与方法
2.1 Vanilla NAT :Fertility 数字预测
请注意 NAT : Non-Autoregressive Translation 非自回归翻译
这就是第一篇使用非自回归模型做机器翻译的论文。
第一个方法是fertility,我们在Encoder端对每一个token都预测一个数字,这个数字代表这一个token在Decoder端对应几个字,如上图,比如“Hello”在Encoder端预测数字为2,那么在Decoder端就要重复“Hello”两次输入。这些数字的总和也就决定了Decoder最终生成的句子长度。
那为什么我们要对每一个Encoder输入的每个token进行数值的预测呢?因为这个数字相当于模型在做句子级别的规划。
举例,如上图,假如我们的“Hello, world!”预测的数字是2 1 2 1,那么Decoder端的输入就是“Hello Hello , world world !”,就可能预测出“你好,世界!”。同样,如果预测的数字是1 1 2 1,那么Decoder端就可能预测出“嗨,世界!”。
那我们要怎么训练这个数字的预测呢?有两种方法:
- 第一种方法是,可以直接使用外部的aligner分配器,如上图右侧,这种分配器可以告知我们输入部分对应输出部分的哪些位置,也就知道了每个输入词汇应该预测的数值。
- 第二种方法是,使用自回归模型中的attention得到的权重值来分配每个输入部分与输出部分的对应关系,这样也就知道了输入词汇对应的数字。
但是假如我们都是从外部得到的答案来训练这个数字预测的话,和最后模型要输出最终的句子的目标是不一致的,所以我们在模型Encoder端可以很好预测数字下,会进行Fine-tune微调参数,将Reinforcement learning强化学习的loss加在数字预测上。
2.2 Sequence-level knowledge distillation 句子级的知识蒸馏
刚刚讲的Fertility数字预测是第一招,第二招是句子级的知识蒸馏。
对于 Knowledge Distillation 知识蒸馏,比如,对某一任务在我们已经有了一个很好的大模型,我们想要再训练一个小模型也能达到同样好的效果,这时我们只需将小模型的输入同样丢给大模型,训练目标就是最小化小模型与大模型的输出。
此时,这里的大模型也就是老师,就是自回归模型。小模型相当于学生,就是我们要得到的非自回归模型。但后面做的有点不一样,我们不是要非自回归模型去学自回归模型的输出,而是通过自回归模型用greedy decode(如不懂greedy decode请参见本系列笔记P4中的Attend)的方法去重新预测数据集,然后将这个贪心解码得到的答案当作非自回归模型的正确答案。这样得到的答案的每一部分是有一定关系的,也就是在“你”后面的“啰”的概率是小于“好”的概率的。
2.3 Noisy Parallel Decoding
第三招就是 Noisy Parallel Decoding (NPD),这种方法是,在训练非自回归模型的时候,根据不同的数字预测可以得到不同的Decode结果,再将这些句子交给自回归模型来打分数,也就是预测在同样的输入情况下,生成这种句子的概率,最终选一个最好的句子当作答案。
那这种又交给自回归模型进行打分,不是又会花费与句子长度成正比的时间么?此时,我们只需使用Teacher Forcing的方法强制输入即可,这样就不再是句子长度正比的时间复杂度了。
2.4 Evolution of NAT 非自回归翻译模型的演化
自非自回归模型的出现后,在机器翻译任务上得到很多不同的模型,如上图,为五种经典的非自回归翻译模型
- Vanilla NAT:通过 Fertility 数字预测来解决多模态问题,2.1已讲。
- Iterative Refinement:与Vanilla NAT类似,只不过在Decoder部分输出一句话后,再将这句话拉到Decoder的输入再生成,多次重复修正刚刚的错误。(2.5)
- Insertion-based:这种方法很像 Iterative Refinement 的方法,不过它重复的方式是在每两个字间插入字。这样做解决了 原本的 Iterative Refinement 的句子长度固定的问题,在第一个timestamp得到句子长度后之后无论重复生成多少次句子长度都保持不变了。但这种方法会慢一点,如上图的生成[ABCDEFG]的过程。
- Insert+Delete:进阶版Insertion-based ,如果插入的字是错误的,那我们就不能修改了。因此加了Delete方法,判断字是否应该被删掉。
- CTC-based:
2.5 NAT :Iterative Refinement 反复修改的非自回归翻译模型
就模型而言,在第一次Decode时,和Vanilla NAT没有区别,都是先预测生成的句子长度,然后输给Decode等长的X也就是
X
′
X'
X′得到
Y
0
Y_0
Y0。只不过还有第二次Decode,此时给Decoder_2的输入将会是
Y
l
−
1
Y_{l-1}
Yl−1,训练目标是最小化生成的
Y
l
与
Y
l
−
1
Y_l与Y_{l-1}
Yl与Yl−1的差别。这里的
Y
l
−
1
Y_{l-1}
Yl−1可以是上一步的
Y
0
Y_0
Y0,此外,我们也可以把正确答案
Y
l
−
1
Y_{l-1}
Yl−1加一些噪音,如上图的Corruption Process三种方法。多次Decode最终输出多次修改的结果。
实验结果:和第一篇的Vanilla NAT效果差不多:(
2.6 Mask-Predict 带有BERT的NAT
这个模型就是把BERT当作Decoder来用,在t=0时,也就是开始时,通过Encoder端的”[CLS]“预测得到应该翻译句的长度,如上图为6,我们就把**[MASK] 复制**六次丢到 BERT里面,此时就会得到第一版的翻译。
这个翻译可能是比较差的,没有关系,我们再把这个翻译拉到BERT的输入端,并选择其中哪些字在刚刚生成时几率是比较低的,进行MASK,让BERT来预测这些MASK的字,并得到第二版的翻译,此时的翻译就已经有了一定的关系性。因为在BERT预测MASK时,它是会考虑整个Encoder端,和MASK字左右的字的。
那我们究竟该MASK掉多少个字呢?具体计算公式见上图的n,通俗而言,就是最开始会MASK多一点,后来MASK的字就逐渐变少。
实验结果:其实蛮列害的,大的模型甚至可以接近自回归模型的分数。
2.7 Insertion 插入法
插入法指的是,对于残缺的句子,每两个字间要预测要插入什么,如果没有插入就预测[end]。
具体而言,如上图,我们有6个token,就会有6个向量表示,将相邻的向量接在一起,将这个接在一起的向量拿来预测要插入的字。
那我们该怎么训练上述这样的插入模型呢?
对于构建训练资料,如上图,假设我们要生成的是
y
1
至
y
1
0
y_1 至y_10
y1至y10这10个tokens:
首先,将这10个tokens打乱
其次,随机选择前k个tokens,比如选前5个tokens如上图的红框框
之后,将未被选择的tokens隐藏掉
最后,还原原始tokens顺序,将隐藏掉的作为需要插入的部分,选择的作为初始的输入
经过上述构建方法,就会得到下图的训练过程。
注意,这里有个插入的技巧,通过给不同插入部分不同权重loss,来使得模型每次插入时更倾向于先插入中间部分,这样的插入方式就会越接近平衡二叉树的方式,插入的步骤也就会减少。
在模型学会平衡二叉树的loss方式后,每次插入时都会优先插入中间部分,具体示例如上图。
实验结果:接近自回归模型。
2.8 KERMIT 端到端的插入法模型
与上面的插入法不同的是,KERMIT将Encoder和Decoder合在同一个模型里。
这样做有什么好处呢?如上图,假如我们训练好了英译中,同样的输入格式改为英文残缺,这样就可以做到中译英,甚至中英互补互译。
其实,这样也相当于将训练数据集扩大了两倍。
甚至,KERMIT可以做所有BERT能做的,它也可以作为一个预训练模型使用,而且GLUE效果和BERT相当,且优于BERT的是,它可以做生成。
后来,又有了 Multilingual KERMIT :多语种训练的KERMIT。有兴趣的可阅读论文,这个模型的效果真的很出色。
2.9 Levenshtein Transformer 插入删除法的模型
同样,都会先对输入句Encode,在Encoder结束后,会分成三个部分,如上图,delete部分判断这个字要不要被删掉。insert部分会判断每个token间要插入多少个字,即放多少个[PLH]占位符。token部分将insert得到的结果丢到Decoder去生成。
那我们该怎么训练这个模型呢?Imitation Learning 模仿学习
首先,我们会制造一些需要删除的句子
y
d
e
l
y_{del}
ydel,比如在正确的句子里加一些随机token。
同时,我们也要制造一些需要插入的句子
y
i
n
s
y_{ins}
yins,比如把一些完整的句子中一部分删除掉。
具体示例如上图,制造三种训练数据。
2.10 CTC-based 基于CTC的非自回归模型 Imputer
其实CTC模型本身就是一个很好的非自回归模型,即使它应用方向是语音辨识。且在语音辨识(具体可参考P5)方向没有多模态对应问题,一句语音往往就只有一个正确答案。
但CTC有几个缺点,效果输给seq2seq的LAS(P4)模型,而且不能反复重复修改输出。如果已经生成错误的字,之后也不能再修改。
Imputer模型就可以完善上述缺点,模型结构很相似。不过在Encoder输入端加入了等长的占位符,在t=0初始时,占位符全部为[M]
之后,在下一时刻t=1时,就会更新这些占位符,将预测得到的替换原来的占位符。注意,这里的"_"是CTC中的分隔符无法再修改的。
在这个模型,还有这样一个训练技巧,就是加入了Decode Block,假设我们设置的块长是3,那么模型就会将输入分成多个3tokens组成的部分,如上图的划分。在Decode时,每一次Decode都会解码这一个块中的一个token,这样只需Decode三次,就完成了训练。
同样这样的CTC想法也可以应用在文字生成上,将输入的文字序列经过Encode后得到的向量一分为二,这样就相当于得到了一定的很长的语音讯号,之后再用CTC的方式输出文字。