Attention Is All You Need—transformer详解

发布时间 2023-04-25 09:51:36作者: 快乐的拉格朗日

Attention Is All You Need 论文

transformer代码

以下大部分是根据论文理解进行的总结和概括,如有疑问,欢迎交流~

transformer仅仅使用注意力机制,没有使用任何的卷积或者RNN结构。

传统RNN结构的当前隐层状态\(h_t\)需要当前时刻的输入以及上一时刻的隐状态输入\(h_{t-1}\),受到这一固有特性的影响,遇上长序列时效率会非常低,因为受到内存大小的限制,不可能无限制增大batch的大小,无法并行处理序列。

由于翻译前后句子长短不一定是一致的,机器翻译任务一般采用编码-解码结构来解决。无论输入的句子有多长,都将句子编码成一个固定大小的向量,再对该向量进行解码以完成机器翻译任务。这样由于中间编码向量的大小是固定的,解码的效果显然会随着输入的句子的增长而下降,注意力机制就是为了缓解这种现象而被提出的。

输入输出

word embedding

不能直接将输入的话(一串字符)输入到transformer,因为它并不认识,所以需要将文字转换成计算机认识的数学表示。文字到字向量转换。用到的方法有Word2Vec。字嵌入得到的尺寸是 \((batch\_size, seq\_length,d_{model})\),嵌入维度是由Word2Vec决定。

论文中包括两个输入,原始输入的数据,以及上一时刻及之前输出的结果序列。将这两个信息利用可以学习的嵌入方法转换成\(d_{model}\) 维度的向量。在论文中,两个输入的线性变换使用了相同的权重矩阵,并且将嵌入结果都乘以\(\sqrt d_{model}\)进行缩放。

positional embedding

输入是经过词嵌入后的向量。

由于网络中,不包含RNN和卷积网络,为了充分利用token的相对或者绝对的位置信息,所以需要将位置信息注入到嵌入向量中。

为了让字嵌入和位置嵌入进行相加,位置嵌入和字嵌入的维度相同。有很多位置嵌入的方法,可以学习的或者固定的提取方法。

文中采用了在不同频率使用sin和cos进行提取的方法。采用sin-cos的方法是为了让模型能够轻松学到通过相对位置来获取注意力信息。如果使用固定的位置编码,对于任何的偏移量,位置编码为\(PE_{pos+k}\),那么任何表示都是\(PE_{pos}\)的线性表示,这里可以理解为k为一个可正可负的值。

文中同时也测试了可以学习的位置嵌入方法,但是发现二者性能差不多。但采用sin-cos还有一个好处就是可以让模型去轻松处理比在训练过程中遇到的更长的序列。

\[PE_{(pos,2i)}=sin(pos/10000^{2i/d_{model}}) \\ PE_{(pos,2i+1)}=cos(pos/10000^{2i/d_{model}}) \]

pos是位置序号,取值范围是[0,mxalen],maxlen>=T。i是维度序号,取值范围是 \((0,d_{model})\)\(d_{model}\)表示字嵌入的维度。偶数位和奇数位分别采用sin,cos进行编码。

总体结构

transformer全部由Attention机制组成。transformer的Encoder-Decoder结构如下所示,其编码器和解码器均有6层相同的结构组成。最后一层编码器的输出是每一层解码器的输入。实际预测时,会根据编码器的结果及上一次预测结果的编码,输出序列的下一个结果。
image

将上述的6层编解码结构拆解,只看其中的一个encoder-decoder,如下图所示

image

上图左侧是一个encoder,右侧是一个decoder。transformer就是有6个左侧结构和6个右侧结构组成。

Encoder

编码器有六个相同的层组成,每个层包括两个子层,第一个子层是多头注意力机制,第二个子层是一个简单的前馈神经网络。这两个子层都使用残差连接,并且在其后面都有一个layer归一化。每一个子层可用下式表述:

\(y = layernorm(x+sublayer(x))\)

为了方便残差连接,所有的子层输出维度为512。

编码器包含很多个自注意力层,每个自注意力层的Q,K,V均来自于上一层的输出。编码器的每一个位置可以注意到之前编码器前一层的所有位置。这里我理解,由于权重计算是KQ,计算出的注意力可以作用到V的每一个位置,因此每一层都会注意到之前一层的所有位置的。

decoder

解码器也是由6个相同的层组成,除了和encoder两个一样的子层以外,解码器增加了一个多头注意力机制。和encoder一样,每一个子层都使用了残差连接,并添加了layernorm。

新添加子层的输入中,query是来自于解码器上一层的输出;key和value是来自于编码器的输出。这可以让解码器的每个位置注意到输入序列的所有位置。这里我的理解是这样的:利用Q,K获取注意力信息,也就是就能利用上一时刻的输出结果的编码与输入序列获取下一时刻输出所需要的注意力权重。假设是中译英的翻译任务,这里的输入序列就是中文的编码信息,上一时刻的输出结果就是上一时刻及之前输出的英文序列结果。这样在预测当前输出结果时,就能利用编码器的所有信息,能够注意到所有位置的信息获得一个输出。

解码器预测时的输入是上一时刻及之前的网络输出结果。但是在训练的时候是将整个输出结果输进去的。由于解码器中的自注意力层允许解码器中的每一个位置都关注到解码器的所有位置。为了防止信息左流以保持自回归的特性,我们通过屏蔽掉所有的对应着非法连接的softmax输入实现。

这里信息左流的解释:

为了保持自回归的性质,要保持从左往右的顺序 (这种情况下,不能利用要预测的未来来推断过去)。 这里将当前token以后的进行mask (即将注意力得分加上-inf,将其变成无穷小,使其注意力系数极小接近于无) [exp(-inf) = 0]。GAT也是这样做的,只不过mask的是非邻居结点 (避免信息泄露,从而让模型学不好)。

Attention

attention函数可以被描述为将query和一系列键值对映射到输出的过程。在这里QKV都是向量。输出是V的加权之和,这里的权重是由Q和V计算出来的。

scale dot-product attention

Q(query),K(key),V(value)可以理解为带有时序信息的向量。

文中采用的注意力机制除了缩放系数\(\sqrt d_k\)的不同,其他与常用的点积注意力机制相同,所以本文的点积注意力加上了缩放两个字,起名缩放点积注意力机制。

在两种常用的注意力机制中,文章解释了为什么用点积注意力机制,而不是加型注意力机制。

加型注意力机制采用单个隐藏层的前向神经网络计算注意力系数。然而这两种方式理论上复杂度是一样的。但是点积注意力计算速度更快和空间利用更高效,因为可以使用高度优化的矩阵乘法代码进行计算。

缩放点积注意力的输入包括维度为\(d_k\)的Q和K,维度为\(d_v\)的V。首先计算Q和K的点积,然后除以K的维数的开方。并且进行softmax变换,来获得权重向量。
image

\[Attention(Q,K,V)=softmax({QK^T\over \sqrt d_k})V \]

\(d_k\)进行开方的原因:

对于较小的\(d_k\),加型和点积两种注意力机制的表现差不多,但是在不加缩放系数的情况下,加型要优于点积注意力机制。文中猜测对于比较大的\(d_k\),其点积增长的也会很大,会让softmax函数逼近梯度很小的区域,为了抵消这种效果,对\(d_k\)进行开方缩放。

multi-head attention

\(MultiHead(Q,K,V)=Concat(head_1,head_2,...,head_h)W^O\)

其中\(W^O \in R^{hd_v*d}\),\(head_i \in R^{seq*d_v}\)

缩放点积注意力是多头注意力的一部分。在多头注意力机制中,总体思路是先进行线性映射,然后进行多头注意力的计算,再进行concat和线性映射。

文中提及先对Q,K,V进行h次线性映射,这样效果更好。这h次分别将Q,K,V的维度从d转换成\(d_k,d_k,d_v\),经过h次线性映射后变成了h份“多头”。

多头有什么好处呢?多头可以允许模型联合处理不同位置(head)的不同表示子空间中的信息。如果使用单头注意力,那么计算中的平均操作会阻碍模型学习到这种信息。

\(head_i=Attention(QW_i^Q,KW_i^K,VW_i^V)\)

假设模型输入的向量的维度为d,经过h次线性映射后,每一个head的维度为\(d_k=d_v={d \over h}\)

关于多头在这里有两种思考路径:

思考一:

假设\(Q \in R^{seq*d}\)\(W_i^Q \in R^{d*d_k}\),这样二者相乘消去了中间的d。所以\(QW_i^Q,KW^K,VW^V\)共进行h次,也就是进行h次的缩放点积注意力运算,每次获取一个单头注意力\(head_i\)

思考二:

假设\(W^Q \in R^{d*d}\),通过线性映射\(QW^Q\),维度由(seq,d)变成了(seq,d)。也就是中间权重的维度为(d,d)。然后对变换后的Q',K',V'按照维度进行划分,划分成h份。然后再分别进行点积运算。

总结:

其实根据点乘的原理,上述两种思考的结果是一致的,每个多头注意力用的是不一样的可以学习的权重。一开始权重矩阵分开计算的方式和先点乘完后再分开其实是一致的。

假设h=2,d/2=n,那么将有两次\(QW_i^Q\),这两次的计算结果的维度是两个(seq,n),合并起来就是(seq,2*n)等价于(seq,d)。

所以上述两种思路是等价的。

不过如果按照计算机计算的方便性,应该是先将其进行点乘,然后再分开,这样计算效率会更高。

每个head进行注意力计算后,输出 \((seq,d_k)\) 的矩阵。共有h个,其维度为\((h,seq,d_k)\)。将这h个多头注意力进行concat,其维度变成\((seq,h*d)\) 等价于 \((seq,d)\),然后此矩阵进行后续的计算。可以发现在经过注意力机制前后,其向量的维度是不变的。

在代码中,也是采用第二种思路进行代码编写。

由于降低了维度,所以多头注意力的计算量与单头计算量大致相当。

所以,多头注意力的关键点就是将线性变换后的Q,K,V按照维度划分成了h份,每一份进行注意力的计算,计算h次,多头注意力可以联系不同位置表示子空间的信息。

多头注意力中的线性变换

在两个线性变换中增加一个RELU激活函数,其变换公式如下:

\(FFN(x)=max(0,xW_1+b_1)W_2+b_2\)

输出

在输出中,每个时间步输出都使用了可以学习的线性变换,并且采用softmax输出下一个输出token的概率。

输出线性层的作用:通过对上一步进行线性变换得到指定维度的输出,也就是转换维度,转换后的维度对应着输出类别的个数。如果是翻译任务,对应着文字字典的大小。

正则化手段

每一个子层(缩放点积注意力)的softmax输出之后,与V进行计算之前,采用了dropout。同时在编解码器对位置嵌入和词嵌入之和后面添加了dropout。

维度变化

  • 词嵌入和位置嵌入:

    输出:(batch_size,seq,dimension)

  • 缩放点积注意力:

    输入 (batch_size,seq,dimension)

    \(W^Q,W^K,W^V\)(dimension,dimension)

    输出:(batch_size,seq,dimension)

  • 多头注意力

    线性映射将维度分为h份后,Q,K,V的的维度变成:(batch_size,seq,h,dimension/h)

    拿出一组head进行说明,\(Q_i\)\(K_i\)的转置点积: (seq,dimension/h) (dimension/h,seq),输出的维度为: (seq,seq)

    然后沿着最后一个维度做softmax,使每一行的和为1。拿出第一行来说,表示的是第一个字与其他几个字的注意力状态,即权重。也就是第一个字与其他几个字的相关程度。

    然后利用注意力矩阵给\(V_i\)加权。 (seq,seq)(seq,dimension/h) 相乘,输出 (seq,dimension/h)

    因为有h个head,concat之后的维度为 (batch_size,seq,dimension)

  • 做残差:

    \(x_{embeding}+Attention(Q,K,V)\)

    输出维度为(batch_size,seq,dimension)

  • 层归一化。

    层归一化把神经网络中的隐藏层归一化为正态分布,也就是i.i.d独立同分布,以起到加快训练速度,加速收敛的作用。

  • 前馈神经网络:

  • 输入:(batch_size,seq,dimension),权重(batch_size,dimension,d)(batch_size,d,dimension)

  • 输出仍然是:(batch_size,seq,demsion)

优缺点分析

  • 每层的计算复杂度更低

    大多数情况下,序列长度小于表示维度d,这时self-attention会更快。可以并行计算

  • self-attention只需要操作一次,但是RNN需要操作O(n)

  • 解决长序列依赖问题

    由于RNN固有特性,需要一步一步计算,在遇到长序列,需要从第一个序列计算到第n个序列。CNN需要增大感受野。而self-attention只需要计算一次。当序列n过长的时候可以通过窗口限制长度。

  • 模型更具有可解释性。通过对结果分析,其学到了一些语义和语法信息。

参考

https://mp.weixin.qq.com/s/MfV2QU-C3bj1tQuEllWchg

https://mp.weixin.qq.com/s/H7hPPFW2g93bE_Z4XhZWbg