NLP预训练家族 | Transformer-XL及其进化XLNet

2021-08-20 17:12:36 浏览数 (1)

作者 | 周俊贤 整理 | NewBeeNLP

最近又重新读了Transformer-XL和XLNet的论文和代码,又有很多新的感悟。其中,要想搞懂XLNet的同学一定要首先明白Transofrmer-XL,因为XLNet是基于Transformer-XL进行改进的。

  • tips:Transformer-XL投稿是被ICLR 2019拒稿的,作者基于Transformer-XL改进提出了XLNet,被NeurIPS 2019接收

Transformer-XL

论文全称及连接:《Transformer-xl: Attentive language models beyond a fixed-length context[1]》

项目地址:https://github.com/kimiyoung/transformer-xl

Vanilla Transformer语言模型

Transformer结构第一次提出来是用于机器翻译任务,提出后自然有个想法:「Transformer结构能不能用于训练语言模型?」 答案是肯定的。RNN、LSTM等网络结构训练语言模型的最大缺点在于容易梯度消失,难以学习长期依赖,但是Transformer没有合格缺点,因为它没有沿时间线进行横向传播的过程,这样来说,Transformer很适合训练语言模型。

怎么训练?很简单,举个例子,假如现在我们有一句话包含100个token,在预测第一个token的时候,我们把其它的99个token进行attention mask掉,只输入一个开始标记,让模型预测第一个token;在预测第二个token的时候,把剩下的98个token attention mask掉,只让模型看见开始标记和第一个token;在预测第三个token的时候,把剩下的97个token attention mask掉,只让模型看见开始标记和前两个token。。。损失函数为真实token和预测token的交叉熵,这样模型不断学习,不断更新参数,就训练得到一个语言模型了。

想想这样有个什么问题?问题就是真实的文本可没有只包含100个token这么少。举个例子,假设我现在手头有个文章是包含4000个token的,那我就要把这4000个token都作为输入,原理上可以这么做,但实际上,输入的token很多的情况下,中间层attention的计算资源会爆炸。

Vanilla Transformer的做法是「切片」,如把4000个token切成8段,每段包含500个token,再对每段进行独立的训练,也即在第二段训练的时候,是看不到第一段的信息的,这样导致的问题就是「上下文碎片问题(context fragmentation problem)」,由于切片长度限制,模型并不能学习到足够常的依赖信息,这样会大大损害学习出来的语言模型的性能。

在评估时,为了能利用训练时的最长上下文信息,是每个时间步向右移动一个token,导致的结构是评估耗费的计算资源很大。

改进点

「改进点一:循环机制」

在计算每个segment的时候,通过缓存上一个segment的信息,把前面segment的信息也考虑进正向传播过程中(上一个segment的信息虽然参与正向传播,但上一个segment是不会参与反向传播的)。具体的看,下面的式子,

begin{aligned} &hat{mathbf{h}}_{tau 1}^{n-1}=left[mathrm{SG}left(mathbf{h}_{tau}^{n-1}right) circ mathbf{h}_{tau 1}^{n-1}right] \ &mathbf{q}_{tau 1}^{n}, mathbf{k}_{tau 1}^{n}, mathbf{v}_{tau 1}^{n}=mathbf{h}_{tau 1}^{n-1} mathbf{W}_{q}^{top}, widetilde{mathbf{h}}_{tau 1}^{n-1} mathbf{W}_{k}^{top}, widetilde{mathbf{h}}_{tau 1}^{n-1} mathbf{W}_{v}^{top} \ &mathbf{h}_{tau 1}^{n}=text { Transformer-Layer }left(mathbf{q}_{tau 1}^{n}, mathbf{k}_{tau 1}^{n}, mathbf{v}_{tau 1}^{n}right) end{aligned}

这里的

h^{n-1}_{tau}

指的是Transformer第n-1层的第

tau

个segmetn的Encoder的输出,把它与Transformer第n层的第

tau 1

的encoder的输出沿着时间轴拼接在一起,SG表示stop-gradient,即前一个segmetn的信息只参与正向传播,并不会用到反向传。另外,在计算查询矩阵q时,只会用当前segmetn来计算得到;只有在计算键值向量时,才会用到拼接的segmetn向量。

这样的做法,能大大缓解上下文碎片的问题,举个例子,假设Transformer的encoder一共有4层,每个segment为500个token。根据循环机制的原理,第4层的第

tau

个segmetn的输出不仅考虑了第三层encoder的第

tau

个encoder的输出,也考虑了第

tau-1

个encdoer的输出,而第

tau-1

个encdoer的输出,不仅考虑了第二层encoder的第

tau-1

个encdoer的输出,也考虑了第

tau-2

个encdoer的输出,也即,上下文的最大依赖长度是线性增加的,如O(N*L),如这里所说的例子,虽然,一个segmetn是500个token,但其实在最后输出时,足足考虑了4 * 500 = 2000个token这么多!上下文碎片的问题也就自然得到了大大的缓解了。

另外,在评估时,由于采用了循环机制,我们就不必每次只向右移动一步了,而且可以采用同训练时候差不多的片段机制,从而大大提高了评估效率。

「改进点二:相对位置编码」

上面所说的循环机制还有个问题待解决,就是位置编码,我们知道,原生的Transformer使用的是绝对位置编码,但绝对位置编码跟循环机制结合会导致一个问题,举个例子,我们在计算第

tau

的输出时,不仅考虑了第

tau

个片段的输入,还考虑了第

tau-1

个片段的输入,假设我们采用绝对位置编码,那第

tau

个片段和

tau-1

个片段的第1个token的位置编码是一样的,但这是明显的不合理的。因此,作者提出了一种相对位置编码的思想。

具体的,原生的绝对位置编码在计算attention时,如下式所示,

begin{aligned} mathbf{A}_{i, j}^{mathrm{abs}} &=underbrace{mathbf{E}_{x_{i}}^{top} mathbf{W}_{q}^{top} mathbf{W}_{k} mathbf{E}_{x_{j}}}_{(a)} underbrace{mathbf{E}_{x_{i}}^{top} mathbf{W}_{q}^{top} mathbf{W}_{k} mathbf{U}_{j}}_{(b)} \ & underbrace{mathbf{U}_{i}^{top} mathbf{W}_{q}^{top} mathbf{W}_{k} mathbf{E}_{x_{j}}}_{(c)} underbrace{mathbf{U}_{i}^{top} mathbf{W}_{q}^{top} mathbf{W}_{k} mathbf{U}_{j}}_{(d)} . end{aligned}

采用相对位置编码,

begin{aligned} mathbf{A}_{i, j}^{mathrm{rel}} &=underbrace{mathbf{E}_{x_{i}}^{top} mathbf{W}_{q}^{top} mathbf{W}_{k, E} mathbf{E}_{x_{j}}}_{(a)} underbrace{mathbf{E}_{x_{i}}^{top} mathbf{W}_{q}^{top} mathbf{W}_{k, R} mathbf{R}_{i-j}}_{(b)} \ & underbrace{u^{top} mathbf{W}_{k, E} mathbf{E}_{x_{j}}}_{(c)} underbrace{v^{top} mathbf{W}_{k, R} mathbf{R}_{i-j}}_{(d)} end{aligned}
  • 将绝对位置embedding
U_j

用相对位置

R_{i-j}

代替,注意,这里的

R

是用原生Transformer的三角函数编码模式,是不需要学习的;

u epsilon R^d

去代替

U^T_iW^T_q

,理由是由于相对编码的思想,无论query在哪个位置,他们对于自身的位置编码信息应该都是一样的,同理,用

v epsilon R^d

去代替

U^T_iW^T_q

W_k

分解成

W_{k,E}

W_{k,R}

,分别表示基于内容的健向量和基于位置的健向量。

通常这样的变换,上面的每一项都有其相应的意义

  • (a)项为基于内容的寻址;
  • (b)项为基于内容的位置偏置;
  • (c)项为全局的内容偏置;
  • (d)为全局的位置偏置。

把循环机制和相对位置编码的信息合并后,Transformer-XL的最终形态

begin{aligned} widetilde{mathbf{h}}_{tau}^{n-1}=&left[mathrm{SG}left(mathbf{m}_{tau}^{n-1}right) circ mathbf{h}_{tau}^{n-1}right] \ mathbf{q}_{tau}^{n}, mathbf{k}_{tau}^{n}, mathbf{v}_{tau}^{n}=& mathbf{h}_{tau}^{n-1} mathbf{W}_{q}^{n top}, widetilde{mathbf{h}}_{tau}^{n-1} mathbf{W}_{k, E}^{n} top, widetilde{mathbf{h}}_{tau}^{n-1} mathbf{W}_{v}^{n top} \ mathbf{A}_{tau, i, j}^{n}=& mathbf{q}_{tau, i}^{n} mathbf{k}_{tau, j}^{n} mathbf{q}_{tau, i}^{n}{ }^{top} mathbf{W}_{k, R}^{n} mathbf{R}_{i-j} \ & u^{top} mathbf{k}_{tau, j} v^{top} mathbf{W}_{k, R}^{n} mathbf{R}_{i-j} \ mathbf{a}_{tau}^{n}=& operatorname{Masked}-operatorname{Softmax}left(mathbf{A}_{tau}^{n}right) mathbf{v}_{tau}^{n} \ mathbf{o}_{tau}^{n}=& operatorname{LayerNorm}left(operatorname{Linear}left(mathbf{a}_{tau}^{n}right) mathbf{h}_{tau}^{n-1}right) \ mathbf{h}_{tau}^{n}=& text { Positionwise-Feed-Forward }left(mathbf{o}_{tau}^{n}right) end{aligned}
Transformer-XL不足及与BERT的对比

Transformer-XL这篇论文为什么没有被ICLR接受,我认为主要是因为并没有与当前一些基于Transformer的模型,如BERT等对比,并没有在具体的NLP任务如分类、QA等应用进行实践。论文里只是简单提了Transformer-XL在文本生成(由于Transformer-XL是语言模型,所以用于文本生成很自然)、无监督特征学习等都有前景,并没有给出在某些具体的GLUE等的表现,所以略显单薄。

不少人都说Transformer-XL能有效解决BERT的长度限制问题,确实,Transformer-XL是不限制文本长度的,它的预训练模式是语言模型的训练目标,通过循环机制和相对编码,可以接受无限长的输入文本。应用到下游任务也很简单,如文本分类可以用最后一个token的输出再接一些前连接层来做分类,序列标注任务也可以用每个token的输出再接一些网络。

为什么BERT是有长度限制?因为在预训练的时候,就把输入长度限制在512,BERT会把1~512位置映射到一个position embedding,没有512以上的position embedding。我们当然也可以重头预训练一个最大长度为1000的BERT,但会很耗资源。

Transformer-XL输入是没有position embedding的,相对位置信息是加在每个层encoder的attention计算中。除此之外,Transformer-XL预训练是只利用了单项信息,BERT是利用了双向的上下文编码,所以可以期待对于短文本,Transformer-XL是打不过BERT的,长文本的话还有一点可能。

XLNet

论文全称及连接:《XLNet: Generalized Autoregressive Pretraining for Language Understanding[2]》

项目地址:https://github.com/huggingface/transformers/blob/master/src/transformers/models/xlnet/modeling_xlnet.py

XLNet的源码我没有看作者发的tensorflow版本,而是看的huggingface的pytorch版本。

AR & AE

「自回归语言模型(Autoregressive LM)」

  • 如Transformer-XL,GPT等等,任务为根据上文内容预测下一个单词,预训练任务为单项的语言模型任务
  • 优点:可以完成生成类的NLP任务
  • 缺点:只能利用单向信息
max _{theta} log p_{theta}(mathbf{x})=sum_{t=1}^{T} log p_{theta}left(x_{t} mid mathbf{x}_{<t}right)=sum_{t=1}^{T} log frac{exp left(h_{theta}left(mathbf{x}_{1: t-1}right)^{top} eleft(x_{t}right)right)}{sum_{x^{prime}} exp left(h_{theta}left(mathbf{x}_{1: t-1}right)^{top} eleft(x^{prime}right)right)}

「自编码语言模型(Autoencoder LM)」

  • 如BERT,根据上下文单词来预测被【MASK】掉的单词(噪声);
  • 优点:能利用到双向信息;
  • 缺点:预训练阶段和Fine-tuning阶段不一样,预训练时输入有【MASK】,但在应用到具体下游任务时,输入是没有【MASK】的。
max _{theta} log p_{theta}(overline{mathbf{x}} mid hat{mathbf{x}}) approx sum_{t=1}^{T} m_{t} log p_{theta}left(x_{t} mid hat{mathbf{x}}right)=sum_{t=1}^{T} m_{t} log frac{exp left(H_{theta}(hat{mathbf{x}})_{t}^{top} eleft(x_{t}right)right)}{sum_{x^{prime}} exp left(H_{theta}(hat{mathbf{x}})_{t}^{top} eleft(x^{prime}right)right)}
Permutation Language Modeling

XLNET的终极目的就是结合自回归语言模型和自编码语言模型的优点,提出了排列语言模型。具体的,看下图

假设我们现在要预测第3个token。正常的语言模型的序列是1 -> 2 -> 3 -> 4,我们对其做「排列组合」,排列方式应该有4!=24种,上图举例了其中的4种,如第一种3 -> 2 -> 4 -> 1,这样排列的话,3是看不到2和4和1的,只能看到上一个segment的输入(这里可以先不不用理,下面会说,XLNet利用了Transformer-XL的思想,所以会有上一个segment作为mem的概念),通过排列,在预测token3的时候,总会有一些排列能考虑到1、2、4,穷举排列,一起训练,这样的话,模型能将3上下文的信息都能学到。

注意的是,实际在预训练时,并非真的排列,如2 -> 4 -> 3 -> 1,并不是排成这个序列,而是利用attention mask的思想来近似作用这种排列,就是把token1 mask掉,因为按照这个排列,token3应该是看不到token1的,利用这种attention mask的思想来近似实现排列。

双流自注意力

根据上面的思想,又引出了一个问题,举个例子I love New York这四个token,现在有两个序列1-> 2 -> 3 ->4和1-> 2 -> 4 ->3,现在假设已经知道前面两个token是I love,要预测下一个token,很明显,在两个序列中,下一个token为New的概率都是一样的,这是非常不合理的,因为下一个token的位置是不一样的,并没有把位置信息考虑进去。因此,作者提出了一种双流自注意力。

  • 当token用于预测后面的字符,这时候,它应该能看到自己的内容信息和位置信息;
  • 当token用于预测自己到底是哪个字符,这时候,它应该只能看到自己的位置信息。

借此,提出两种自注意流

  • The content representaion
h_{varTheta}(x_{z{le}t})

  • The query representaion
g_{varTheta}(x_{z{le}t},z_t)

加入位置作为预测信息后,预测下一个token的概率变成

p_{theta}left(X_{z_{t}}=x mid mathbf{x}_{z_{<t}}right)=frac{exp left(e(x)^{top} g_{theta}left(mathbf{x}_{mathbf{z}_{<t}}, z_{t}right)right)}{sum_{x^{prime}} exp left(eleft(x^{prime}right)^{top} g_{theta}left(mathbf{x}_{mathbf{z}_{<t}}, z_{t}right)right)}

初始化第一层

g_i^{(0)}=w

,其中

w

为一个可训练的向量,

h_i^{(0)}=e(x_i)

为相应的词嵌入。两种Attention在接下来每层的构造

begin{aligned} &left.g_{z_{t}}^{(m)} leftarrow text { Attention }left(mathrm{Q}=g_{z_{t}}^{(m-1)}, mathrm{KV}=mathbf{h}_{mathrm{z}<mathrm{t}}^{(m-1)} ; thetaright), quad text { (query stream: use } z_{t} text { but cannot see } x_{z_{t}}right)\ &left.h_{z_{t}}^{(m)} leftarrow text { Attention }left(mathrm{Q}=h_{z_{t}}^{(m-1)}, mathrm{KV}=mathbf{h}_{mathrm{z} leq t}^{(m-1)} ; thetaright), quad text { (content stream: use both } z_{t} text { and } x_{z_{mathrm{t}}}right) text { . } end{aligned}

在fine tuning应用于下游任务时,把

g

挪走,只保留

h

与Transformer-XL的结合

这里就是把Transformer-XL的循环机制和相对位置编码引进XLNet中,值得注意的是,Permutation只争对当前的segment,上一个segment的排序是原文本的顺序排列。

多个Segments的输入

像BERT之类的模型,输入是可以是一对文本,这样的预训练方式可以适应输入为不止一个句子的下游任务。XLNet采用了一个相对Segment编码的方式来解决输入多个句子的问题。分别用两个segment编码

s_{ij}=s_

s_{ij}=s_-

,其中

s_

s_-

是每层endoer可以学习的参数,把他们也加进去attention的计算中,

a_{ij}=(q_i b)^Ts_{ij}

,这里

q_i

为查询向量,

b

为可学习的编制,当两个token不是来源于同一句时,

s_{ij}

=

s_-

,当来自同一句时,

s_{ij}=s_

想想这样做的好处?好处就是XLNet不再限制输入的为一对句子对,而是可以超过两个以上句子作为输入,因为XLNet不像BERT一样,输入时的segment embedding只能为0或1再映射到一个向量加入到input中,XLNet不是在输入层映射到0和1,而是在每一层encoder通过引入segmetn编码,来处理多个句子输入的问题。

实验结果

值得注意的是,XLNet相对于BERT使用了更多的训练预料,由于是晚于BERT出来的模型,自然效果是比BERT要好的,与BERT 的比较

与RoBERTa的比较

0 人点赞