Transformer论文精读

摘要

Transfomer 仅仅依赖了注意力机制,没有依赖循环或者是卷积神经网络。

结论

第一个仅仅使用注意力机制来做序列转录的模型。

导言

当前最实用的模型是 RNN,包括了 LSTM、GRU。主流模型有两种,一种是语言模型,另一种是基于 encoder-decoder 的架构模型。在 RNN 中,对于一个序列,对这个序列的处理方法是从这个序列的左边开始,一步一步往右边做。那么对于一个句子,对于第 t 个词,会记录他的一个隐藏状态 ht,这个 ht 是由当前的词 t 以及前一个词的隐藏状态 h(t-1) 决定的。这样子就可以获取当前词之前的信息并且根据当前词做出计算得到输出。所以 RNN 可以有效处理时序信息的一个关键点,将之前的信息放在隐藏状态里。同时缺点就是无法并行处理,每一个词都依赖上一个词的隐藏状态,并且当时序比较长的时候,隐藏状态在后面的位置所表达的信息量可能比较大,或者是为了减少大小需要丢弃部分前面的信息。

基于 Attention 以后可以做纯并行,完全依赖 Attention 来处理输入和输出之间的全局依赖关系。

背景

使用 CNN 对长序列难以建模,需要多层才能建立长距离序列之间的关联,CNN 的另一个特点就是可以做多个输出通道,一个输出通道可以认为是它可以去识别不一样的模式。

使用注意力机制可以只用一层就获取所有序列之间的位置关系。同时注意力机制也想要 CNN 的可以做多个输出通道的特点,然后就提出了 Muti-Head Attention 多头注意力机制来模拟 CNN 多输出通道的一个效果。

Transformer 是第一个只依赖于自注意力机制来做 encoder-decoder 架构的模型。

模型架构

序列模型中最常用的就是 encoder-decoder 架构。对于 encoder 来说,它会将输入序列(x1, ... ,xn)映射到连续表示序列(z1, ... ,zn),也就是将原始输入转变成机器学习可以理解的一个向量序列,比如有一个句子,有 n 个词,xt 表示第 t 个词,encoder 就会把这个句子表示为一个长度为 n ,但是每一个 zt 都是 xt 的一个向量的表示。encoder 的输入就是原始输入序列,输出一个相同长度的向量。对于 decoder,首先会拿到 encoder 的输出,然后生成一个长度为 m 的序列,m 可以与 n 不一样大小,例如中英做翻译。在 decoder 中和 encoder 中的一个不一样就是,decoder 通常是一个词一个词生成的,因为对于 encoder 来说,获取输入能够看到整个句子,但是 decoder 是一个词一个词生成的,这叫做自回归,auto-regressivet 的一个模型。对于 decoder,给定一个长度为 n 的序列 z,输出一个长度为 m 的序列 y,对于序列 y,y1 在作为输出的同时,也会作为 y2 的输入,同理,yn 之前的所有输出(y1 到 y(n-1)),也都会成为 yn 的输入,这就叫做自回归。

Transformer 使用了 encoder-decoder 架构,具体来说它是将一些自注意力、 point-wise、fully connected layers 堆在一起的一个架构。

输入是原始输入,encoder 会对 input 做一个 embedding,然后做 positional encoding,接下来就是 N 个 Transformer block。Transformer block 里首先是一个 Multi-Head Attention,然后就是一个前馈神经网络(Feed Forward),本质上也就是一个MLP ,通过残差连接,再有一些 normalization 构成了一个完整的 block。

对于 encoder 的输出,会作为 decoder 的输入,但是是插入到 decoder 中的结构中。但是对于 decoder 来说,相对于上面描述的 Transformer block 多了一个叫作 Masked 的一个多头注意力机制。然后 decoder 也是会有 N 个 Transformer block。然后输出输入到输出层(linear),然后做一个 softmax 就会得到一个最终的输出。

encoder

N 为 6,也就是 6 个完全一样的 layers(也就是上文描述的 Transformer block)。每个 layer 里有两个 sub-layers。第一个 sub-layer 叫作 Multi-Head self-attention,第二个 sub-layer 叫作 simple 的 MLP。对于每一个子层都使用残差连接,最后再使用一个叫作 layer normalization 的东西。公式写出来长这样LayerNorm(x+Sublayer(x)),输入 x 进来,先进入一个子层,然后通过残差连接,输入 x 和 sub-layer 加在一起,最后进入 LayerNorm。为了简单起见,残差连接需要你的输入和输出是一样的大小,所以将每一个层的输出维度变成 512,也就是对每一个词,不管在哪一个层,都做了 512 长度的表示。

LayerNorm 和 BatchNorm 相比。以二维输入举例,输入一个矩阵,每一行是样本,每一列是特征。有:

X = [[x1_1, x1_2, x1_3],
     [x2_1, x2_2, x2_3],
     [x3_1, x3_2, x3_3]]

对于 BatchNorm 会依次计算第 1 个特征(列) x1_1, x2_1, x3_1 的均值和方差;第 2 个特征(列)x1_2, x2_2, x3_2 的均值和方差;第 3 个特征(列) x1_3, x2_3, x3_3 的均值和方差。然后,用这些均值和方差对每个特征列进行归一化,得到归一化后的输出。

而 LayerNorm 则是依次计算第 1 个样本(行) x1_1, x1_2, x1_3 的均值和方差;第 2 个样本(行) x2_1, x2_2, x2_3 的均值和方差;对第 3 个样本(行) x3_1, x3_2, x3_3 的均值和方差。然后,用这些均值和方差对每个样本的特征进行归一化,得到归一化后的输出。

对于三维输入来说行是样本,列是序列的长度,称为 sequence(transformer 称之为 n)。每一个 sequence 就是对每一个词来说,有自己的向量,称之为 feature 也就是特征(transformer 称之为 d ,设置成了 512 ),则有:

X = [
      [[x1_1, x1_2], [x1_3, x1_4], [x1_5, x1_6]],
      [[x2_1, x2_2], [x2_3, x2_4], [x2_5, x2_6]],
      [[x3_1, x3_2], [x3_3, x3_4], [x3_5, x3_6]]
    ]

BatchNorm 会对每个特征(即每列数据)跨整个 batch 来计算均值和方差。它的归一化是跨样本的。例如:

  • 对第 1 个特征(列)进行归一化:计算 x1_1, x2_1, x3_1,然后再用均值和方差对每个样本的第 1 个词进行归一化。
  • 对第 2 个特征(列)进行归一化:计算 x1_2, x2_2, x3_2,然后再用均值和方差对每个样本的第 1 个词的第二个特征进行归一化。

LayerNorm 会对每个样本的每个序列进行归一化(即跨每个样本的所有特征),例如:

  • 对样本 1 中的第 1 个词 [x1_1, x1_2],计算均值和方差,并进行归一化。
  • 对样本 1 中的第 2 个词 [x1_3, x1_4],计算均值和方差,并进行归一化。
  • 对样本 1 中的第 3 个词 [x1_5, x1_6],计算均值和方差,并进行归一化。

BatchNorm:跨样本和序列位置(批量维度)对每个特征进行归一化。LayerNorm:对每个样本独立地,对每个序列(或词)内的所有特征进行归一化。

为什么 Transformer 会使用 LayerNorm?一个原因是说,对于时序的模型来说,每一个样本的长度 seq 可能发生变化,BatchNorm 在这里计算出来的抖动较大,LayerNorm 则没有这个问题,相对稳定一些。

decoder

同样的是 N 为 6,也就是 N 个完全一样的 layers,每个 layer 里有三个 sub-layers(encoder 只有两个),多出来的这个 sub-layer 也是 Multi-Head self-attention。另一点就是 decoder 做了一个自回归,在 t 时刻不应该看到 t 时刻以后的输出,具体做法是通过一个带掩码的注意力机制来实现,也就是上文模型架构中提到的叫作 Masked 的一个多头注意力机制,用来保证 t 时刻看不到 t 时刻以后的输入,从而保证训练和预测的时候行为是一致的。

注意力层

注意力函数将一个 query 和一些 key-value 对,映射成输出 output 的一个函数。具体来说,output 是 value 的一个加权和,也就意味着输出的维度和 value 的维度是相同的。对于每一个 value 的权重,是根据 value 对应的 key 和 query 的相似度计算而来,这个相似度 compatibility function ,不同的注意力机制有不同的算法。

例如有位置为 1.00, 2.00, 3.00 的三种 k-v 对。现在有一个 query A 在位置 1.23 进行计算, 那么 A 由于离 1.00 较近,则位置为 1.00 的 key 权重最大,位置为 2.00 的 key 权重则适中,位置为 3.00 的 key 权重最小,计算出 output。若现在还有个 query B 在 位置 2.89 进行计算,则 3.00 的 key 权重最大,2.00 的 key 权重适中,1.00 的 key 权重最小。

Transformer 使用的注意力机制 Scaled Dot-Product Attention。这里面的 query 和 key 都是等长的(实际上也可以不等长,但是计算方法可能会更复杂),维度都等于 dk。 value 的长度也就是维度为 dv,那么意味着 output 的维度也是 dv。具体计算方法就是对每一个 query 和 key 做内积(点积)来计算注意力权重,用来衡量每个query 和所有 key 之间的相似度。内积的值也就是余弦值越大,表示这两个向量的相似度越高。计算出来以后除以根号dk也就是这个向量的长度。然后用 softmax 来得到权重。比如一个 query 给 n 个k-v 就会计算出 n 个值,因为这个 query 就会跟这 n 个 key 做内积,算出来之后放进 softmax 就会得到 n 个非负并且加起来和等于一的权重。然后将这些权重作用到 value 上就会得到输出了。但是实际上不能一个一个这样做,因为做起来比较慢。实际计算中,可以把 query 写成一个矩阵(不止一个 query),n 行,维度为 dk;那么 key 也是一个矩阵,行数为 m 行(也就是 query 的数量和 key 数量可能会不一样),但是维度与 query 相同为 dk。对于 query 的矩阵和 key 的矩阵,将他们相乘,得到一个 n 行 m 列的矩阵,这个矩阵里的每一个行就是一个 query 对所有 key 的内积值。然后再除以根号 dk 后做 softmax(对每一行做 softmax,每一行都是独立的),得到权重。最后用权重乘以 V(V 是有 m 行,列数是 dv 的一个矩阵),得到一个 n 行,维度为 dv 的矩阵,这里的每一行就是我们所需要的 output。实际上用矩阵乘法来做就可以很好的做并行操作,来提高计算速度。

addtitive attention 加性的注意力机制,可以处理 query 和 key 不等长的情况; dot-product(multi-plicative) attention 点积注意力机制和上述的计算方法类似,但是没有将 query 矩阵和 key 矩阵相乘后除以根号 dk 这个步骤。为什么要除以根号 dk 呢?因为当 dk 不是很大的时候除不除都没太大关系,但是当 dk 比较大,也就是两个向量的长度比较长的时候,那么做点积的时候值可能会比较大或者比较小小,当值比较大的时候,那些相对的差距就会变大,导致最大的那个值做出来以后,softmax 后会更加靠近于 1,其他值就会接近于 0,值就会更加向两边靠拢,那么计算梯度的时候会发现梯度比较小(因为 softmax 希望置信的地方靠近 1,不置信的地方靠近 0,这样就表示计算的差不多了,这样就表示收敛的差不多了),导致跑不动,所以这里 dk 取 512,除以一个根号 dk 就是一个不错的选择。

如何做 Mask 呢?也就是做掩码,避免 t 时刻看到 t 时刻以后的信息。首先假设 query 和 key 是等长的为 n,并且时间上能够对应起来的。对于 t 时刻的 query qt,那么在做计算的时候,只应该看到 k1 一直到 k(t-1),不应该看到 kt 以及 kt 以后的东西,因为 kt 在当前时刻还没有。计算过程还是一样的,但是只需要保证在计算权重的时候,也就是算输出的时候,不要用到后面的一些东西就行了,具体做法就是对于 qt 和 kt 和他之后的一些值,换成一些非常大的负数,例如 -1 的 10 次方。这样这些值在进入 sfoftmax 做计算的时候他就会变为 0。

接下来关注 Multi-Head Attention。与其做一个单个的注意力函数,不如把整个 query 和 k-v 对投影到低维,投影 h 次,然后再做 h 次的注意力函数,然后把每一个函数的 output 并在一起,然后投影回来得到最终的输出。多头注意力机制可以识别不同的模式,具体的做法就是将原始的 key, value, query 进入到线形层,投影到比较低的维度,然后做 h 次 scaled Dot-Product Attention,得到 h 个输出,将所有输出向量合并到一起,最后做一次线形的投影,回到 Multi-Head Attention。先投影到低维,这个投影的 w 是可以学的,也就是说给 h 次机会,能够学到不同的投影方法,使得在投影进去的度量空间里,能够匹配不同模式需要的相似函数,最后再投影回来。

Multi-Head Attention 具体公式如下(这里的 h 取的 8,也就是 8 个头。在使用注意力函数的时候因为有残差连接的存在,所以输入和输出的维度是一样,所以投影的时候,维度就是输出的维度除以 h ,输出维度是 512,除以 h 就是 64维,也就是每次投影到一个 64 维的维度下,然后在上面计算注意力函数,最后再投影回来):

# 将不同的头的输出做 Concat 起来,然后投影到W_O里面
MultiHead(Q,K,V) = Concat(head_1,...,head_h)W_O
# 通过一个不同的可以学习 的WQ,WK,WV 投影到 dv 上面,再做注意力函数得到输出。
where head_i = Attention(QW_Q_i,KW_K_i,VW_V_i) # 多个小矩阵乘法可以合并成一次矩阵乘法来实现

Transformer 是如何使用注意力机制的

三种使用情况:第一个是编码器的注意力机制;第二个是解码器的注意力机制;第三个是 k-v 来自编码器的输出,query 来自解码器下一个 attention 的输出的这个注意力机制。

编码器的注意力层:若输入的句子长度为 n,那么其实际的输入是 n 个长为 d 的向量(若 pn 大小设置为1)。输入有三个输入也就是 key,value,query;这三个输入复制了三份,同样一个东西既作为 key,也作为value也作为 query,这就是自注意力机制,key,value,query 是一个东西那就是自己本身。输出也是 n 个长为 d 的向量,实际上是 value 的一个加权和,权重来自于 query 和 key 的信息。若不考虑多头和有投影大情况下,输出其实就是输入的一个加权和,权重来自于本身跟各个向量之间的相似度;但是有多头和投影的情况下,则会学习 h 个不一样的距离空间出来,使得输出会和前半句话不同。

解码器的注意力层:唯一与编码器注意力机制不同的是有 Masked,在 t 时刻以后的权重都是0。

第三个:不再是自注意力了,key 和 value 来自于编码器的输出,query 则来自于解码器下一个 注意力层的输入。意味着在这里的注意力层实际上是把编码器中的一些输入,根据 query 来找出来(做好了权重的情况下)。根据解码器输入 query 的不一样,在编码器的输出中挑选注意力层感兴趣的东西。

Position-wise Feed-Forward Networks

输入的序列有多个词,每个词看成一个点(一个 position),把一个 MLP 对每个词作用一次,并且 MLP 只作用在最后一个维度,这就是 position-wise 的意思。

公式如下:

# xW_1+b_1 表示第一个线形层
# max(0,...) 是一个 relu 的激活层
# 然后整体来看 W_2+b_2 又是一个线形层
FFN(x) = max(0,xW_1+b_1)W_2+b_2

注意力层的 query 对应的输出维度为 512,也就是 x 是一个512 维度的向量,W_1 会把 512 维投影到 2048 维,然后W_2会把2048维投影回 512 维。实际上 FFN 也就是一个单隐藏层MLP,中间隐藏层把输入扩大 4 倍,最后输出的时候回到输入的大小。使用 pytorch 实现实际上就是把两个线形层放在一起(因为 pytorch 当输入是一个 3d 的时候,默认在最后一个维度做计算)。

Embeddings and Softmax

输入的是一个一个的词,也叫作词源,也可以叫作 token,要将其映射成一个向量。embedding 就是对于任何一个词,学习一个长为 d (512维度)的一个向量来表示它。编码器输入要一个 embedding,解码器的输入也要一个 embedding,softmax 前的线形层也需要一个 embedding,这三个 embedding 是一样的权重(为了简单训练),另外一点就是把权重乘了一个根号 d,也就是根号下 512。

Positional Encoding

由于注意力机制不会有时序信息(输出是一个 value 的加权和),权重是 query 和 key 之间的距离,与序列的信息无关。所以需要一个位置编码来把时序信息进行添加。

具体公式如下:

PE(pos,2_i)   = sin(pos/10000^(2_i/d))
PE(pos,2_i+1) = cos(pos/10000^(2_i/d))

也就是用 d = 512 维度的一个向量来表示位置信息(具体的值用上面的公式计算),然后直接于 embedding 进行相加,也就是意味着把位置信息加到了 token 中,可以进行训练和计算了。