Attention is all you need
🍀

Attention is all you need

Tags
Research
AI
Published
Jan 20, 2021
 
🐋
首先,这篇文章是谷歌大脑在2017年底发表的,发表出来后就引起了很多人的关注。
它提出transfore是一个seq2seq模型.,基于transformer衍生出来的预训练模型BERT现在已经取得了大范围的应用和扩展。 在自然语言处理的榜单里, 机器的成绩已经超人类表现, 这很大程度要归功于基于transformer的BERT预训练模型.

背景

transformer这个模型主要是应用于自然语言处理领域的。那么说到自然语言处理, 语言模型, 命名实体识别, 机器翻译这些概念, 可能很多人先想到的LSTM等循环神经网络, 但目前其实,LSTM起码在自然语言处理领域已经过时了,
Transformer比LSTM拥有更多的优势。
首先是效果更好,这篇文章是通过一个机器翻译任务对模型的性能进行测试,结果就是transformer在效果上比循环神经网络更好。
并且,transformer这个模型还具有很高的可并行性,所以训练的时间也更少。

🐋理解transformer这个模型不需要有很多深度学习和数学基础, 我们可以用简单的语言和通过可视化的方法去理解。
前面说过,BERT是基于transformer的预训练模型,transformer应用的方式主要就是先进行预训练语言模型, 然后把预训练的模型适配给下游, 以完成各种不同的任务, 如分类, 生成, 标记等等,预训练模型非常重要, 预训练的模型的性能直接影响下游任务的性能。

0.Transformer模型的直观认识

我们首先来直观地认识一下这个模型。
transformerLSTM的最大区别, 就是LSTM的训练是迭代的, 是一个接一个字的来, 当前这个字过完LSTM单元, 才可以进下一个字,
而transformer的训练是并行的, 就是所有字是全部同时训练的, 这样就大大加快了计算效率
那既然它是并行训练的,怎么才能让transformer获取字先后顺序或者说是时间序列关系呢?
transformer使用了位置嵌入(positional \ encoding)来理解语言的顺序, 使用自注意力机制全连接层来进行计算, 这些下面再详细讲解.

🐋
transformer模型主要分为两大部分, 分别是左边的编码器和右边的解码器
编码器负责把自然语言序列映射成为隐藏层, 隐藏层就是含有自然语言序列的数学表达.
然后解码器把隐藏层再映射为自然语言序列, 从而使我们可以解决各种问题, 如情感分类, 命名实体识别, 机器翻译等等,
下面我们举个例子来说明模型的整体流程。
假设我们执行一个语言翻译的任务,将英文翻译成中文
notion image
  1. 输入自然语言序列到编码器: Why do we work?
  1. 编码器处理序列,输出到隐藏层, 再输入到解码器;
  1. 当解码器输入<start>符号,代表开始
  1. 得到第一个字"为";
  1. 将得到的第一个字"为"落下来再向解码器中输入;
  1. 得到第二个字"什";
  1. 将得到的第二字再落下来,
    1. 直到解码器输出<end>(终止符), 即序列生成完成.

🐋
我们今天主要讲一下编码器,因为解码器的结构和编码器是类似的
BERT预训练模型只用到了编码器的部分,也就是把自然语言序列映射为隐藏层的数学表达的过程。再把它分配给各种各样五花八门的任务。
其实,我们只用编码器就能够完成一些自然语言处理中比较主流的任务, 如情感分类, 语义关系分析, 命名实体识别等
notion image
用X来代表一个自然语言序列,它的维度是batch size * sequence length,batch size 可以理解为句子的个数,sequence length 指的是句子的长度
我们输入X,然后经过这个编码器得到一个Xhidden,就是隐藏层

🐋

1.Input embedding

Input Embedding这一步的操作就是从这个字向量表里找到对应的字的数学表达。
notion image
比如,这是一个字向量表,这个字向量表的维度是vocab_size * embedding_size
vocab_size 指的就是我们的字数,比如字典里有1万个字,那么我们的模型就知道这1万个字,一个字就是一条,都对应了一个embedding_size,每,代表一条数学表达。
然后每一个字的数学表达的维度就是embedding dimension,经过这一步X的维度就变成了batch size * sequence length * embedding dimension,通常把这个维度叫做嵌入的维度,也就是字向量的维度

🐋

2.Positional encoding

下一步就是positional encoding,positional encoding叫做位置嵌入/位置编码
由于transformer模型没有循环神经网络的迭代操作, 所以我们必须提供每个字的位置信息给transformer, 才能识别出语言中的顺序关系.
位置嵌入的维度为[max \ sequence \ length, \ embedding \ dimension], 其实位置嵌入的第二个维度和字向量的第二个维度是一样的,所以,我们之后计算出位置嵌入来,可以直接和字向量元素相加就。
max \ sequence \ length属于超参数, 指的是限定的最大单个句长.
这个地方注意, 我们一般以字为单位训练transformer模型, 也就是说我们不用分词了,
那么到底如何给模型提供位置信息呢?
在这里论文中使用了sine和cosine函数的线性变换来提供给模型位置信息: PE_{(pos,2i)} = sin(pos / 10000^{2i/d_{\text{model}}}) \\\quad PE_{(pos,2i+1)} = cos(pos / 10000^{2i/d_{\text{model}}})
上式中pos就是position,指的是句中字的位置, 比如一个句子有10个字组成,pos就是从0-9的数字。取值范围是[0, \ max \ sequence \ length),
i指的是embedding dimension词向量的维度, 取值范围是[0, \ embedding \ dimension), 比如字向量的维度是256,那么i的取值范围是0-255.
dmodel是总的字向量的维度也就是说256.
sin和cos公式, 也就是对应着embedding \ dimension维度的一组奇数偶数的序号的维度,
偶数是sin,奇数是cos,从而产生不同的周期性变化
这是在jupyter 中画的图
这里定义了最大的句长是100
第二个维度是hidden dimension,其实就是上面说的embedding dimension,指的是字向量的维度,在embedding dimension的第0个序号,0是偶数,所以使用sin函数进行一个线性变换,1是奇数,所以使用cos进行线性变换。
我们可以发现一种现象,这个周期变化,随着embedding dimension序号的增加会变得越来越,这张热图,浅色的地方接近于1,深色的地方接近于0,在0这个地方是大小大小大小,在1这个地方是大小大小,也就是变化越来越慢。位置嵌入在embedding \ dimension维度上随着维度序号增大, 周期变化会越来越慢, 而产生一种包含位置信息的纹理。这种交错会产生一种信息,让模型可以得到句子之间的序列关系。
可以从这个图中看一下周期的变化,分别是在embedding dimension上的第一个第二个第三个维度的周期。这种组合可以在时间的维度,产生一些独特的不重复的纹理信息,可以让模型理解时间序列的关系。

🐋
第3部分是注意力机制

3.self attention mechanism 自注意力机制

谷歌大脑发布的这篇论文,名字是attention is all you need,所以multi-head attention这一部分非常重要。经常就是把这一部分叫做注意力机制。在这里叫做self attention mechanism,也就是自注意力机制。下面介绍一下为什么叫做自注意力机制。
notion image
先回顾一下刚才讲到的词向量和位置嵌入。
现在有一个自然语言学列,维度是batch size * seqlen
之后查表,经过embedding Lookup和位置嵌入相加,就得到了XEmbedding,它的维度是batch size * seq Len * embedding dimension,这个矩阵就是拿出一个句子,它的一个形状。行是指的每个字,列是每个字向量的维度。
之后,对这个字向量的矩阵进行一个线性变换,这里分为3个权重,进行3次线性变换。
分配的三个权重分别是Wq,Wk,Wv,他们的维度都是embedding dimension * embedding dimension
线性映射后,形成3个矩阵。分别是Q,K,V,这三个矩阵的维度,和之前字向量矩阵的维度是一样的。
之后对Q,K,V三个矩阵进行分割。准备进行多头注意力机制,也就是multi head attention,为什么叫多头呢?因为要把每个矩阵分割,分割成多个头。
num of heads,是一个超参数,在这里头的个数是3,这里需要注意,嵌入的维度embedding dimension必须整除head size
我们把embedding dimension分成了h份,也就是头的个数
分割后,Q,K,V的维度为[batch size, sequence length, h ,embedding dimension / h]
之后为了方便计算,我们把Q,K,V中的sequence length, h进行一下转置,转置后Q,K,V的维度为[batch size, h ,sequence length, embedding dimension/h

🐋
我们拿出其中的一个head作为演示
notion image
这个是这一步的公式
比如,这个head的维度是6*3, 6是他的sequence length,3是head size,head size就相当于分割之后的embedding dimension
左边这个矩阵是Q,右边的矩阵是K的转置,然后我们求这两个矩阵的点积。
这里为什么要去做一个点积,这与点积的几何意义有关。
点积的公式是:ab = abcos𝜽
两个向量离的越近,夹角越小,他们的点积就越大。也就是说,如果这两个词比较相似,那么他们的词向量的点积就会比较大,如果他们完全不一样,点积就是负的。
生成的这个矩阵就叫注意力矩阵,这是一个方形的矩阵。每一行表示当前的这一个字,和这一句话所有字的关系。这个注意力矩阵的对角线,是这个字和它本身的相关程度。
看到这个公式中,把注意力矩阵除以\sqrt{d_k},,这篇论文在脚注里给出了这样操作的原因。这样是为了把注意力矩阵缩放成标准正态分布,因此可以获得更好的梯度。
下一步,沿列的维度做softmax归一化。因为每一行是当前这个字和其他字的关联程度,我们想让这种关联程度的和为1,去形成一个概率分布,所以做的就是softmax。做

🐋
notion image
我们要用这个归一化之后的注意力矩阵给V加权,
V是我们把得到的字向量进行线性变化得到的第3个矩阵,现在还完全没有动过,刚刚只是用Q和K求出了一个注意力矩阵,
注意力矩阵中的每一行代表着一个字和其他字的相关性,V这一行是当前这个字的数学表达,用注意力矩阵给它加权就是,让所有信息融入到当前这个字里面,这样就相当于把这一句话中所有字的全部信息都融入到了当前这个字里面。我们在进行selfattention后,V的维度并没有变化。
总之,经过selfattention后,我们的目的就达到了,我们的目的就是让这个字含有所有信息
我们来看一下加权之后的矩阵是什么样子。
这里的head是12。第一个字是CLS最后一个是SEP,可以理解为是开始和结束的符号
第一个字为,和什么这两个字比较相关,什和么非常相关,所以这些区域就比较亮。不同的层数和head数,会产生不同的attention矩阵

🐋
然后需要注意非常重要的一点,在我们做的时候,通常是一次进行多句话的计算,也就是形成一个mini batch,mini batch中含有多个句子,每个句子的长度是不一样的,一般是按照最大的句长进行padding,也就是把那些比较短的句子填充成和最大句子一样的长度,这样才可以进行矩阵计算,我们一般是用0来填充,
但这时在进行softmax时会产生问题
notion image
画阴影的地方都是padding的区域,这些地方全部填充为0,白色的区域是实际有效的区域
这样,做softmax的时候就产生了问题
 
e^0=1,本来是0的地方做完softmax就不是0了,就会参与之后的计算。
这时就需要做一个mask让这些无效区域不参与运算, 我们一般给无效区域加一个很大的负数的偏置, 也就是:
经过上式的masking,无效区域经过softmax计算之后还几乎为0, 这样就避免了无效区域参与计算.

🐋
第4部分是残差连接和层归一化。

4. Layer Normalization和残差连接.

Add指的是残差连接,norm指的是normilization

残差连接

我们在上一步得到了经过注意力矩阵加权之后的V, 也就是Attention(Q, \ K, \ V), 我们对它进行一下转置, 使其和X_{embedding}的维度一致, 也就是[batch \ size, \ sequence \ length, \ embedding \ dimension], 然后把他们加起来做残差连接,
X_{embedding} + Attention(Q, \ K, \ V)
这一步的目的是,因为transformer的结构是比较深的,我们通常会使用很多个模块,所以容易出现梯度消失,我们用残差连接,这样梯度反向传播的时候,从上面往下传,梯度可以直接传到前面来,避免了梯度消失的情况
在之后的运算里, 每经过一个模块的运算, 都要把运算之前的值和运算之后的值元素相加, 从而得到残差连接, 训练的时候可以使梯度直接走捷径跨层反传到最初始层,避免了梯度消失的情况。对
对每一个子层都进行这样的操作
X + SubLayer(X)

LayerNorm:

Layer Normalization的作用是把神经网络中隐藏层归一为标准正态分布, 以起到加快训练速度, 加速收敛: LayerNorm(x)=\alpha \odot \frac{x_{ij}-\mu_{i}} {\sqrt{\sigma^{2}_{i}+\epsilon}} + \beta 用每一行每一个元素减去这行的均值, 再除以这行的标准差, 从而得到归一化后的数值, \epsilon是为了防止除0;
这样会使它损失一些指向长度的信息,就要有2个参数\alpha,\beta
这两个参数是可学习的,用来弥补在归一化过程中损失的信息。通常初始化\alpha为全1,\beta为全0,\alpha,\beta的维度,和x的维度是一样的,用\alpha元素相乘归一化的x词矩阵,再加上\beta,这个符号表示元素相乘而不是点积

🐋

5.feed forward

feed forward这个地方就是一个线性变换,和Q,KV是差不多的操作,后面的add,norm和前面是一样的操作。

🐋

transformer encoder整体

我们现在来梳理一下transformer编码器的所有计算流程
1). 字向量与位置编码: X = EmbeddingLookup(X) + PositionalEncoding X \in \mathbb{R}^{batch \ size \ * \ seq. \ len. \ * \ embed. \ dim.}
2). 自注意力机制: Q = Linear(X) = XW_{Q} K = Linear(X) = XW_{K} V = Linear(X) = XW_{V} X_{attention} = SelfAttention(Q, \ K, \ V)
3). 残差连接与Layer \ Normalization X_{attention} = X + X_{attention} X_{attention} = LayerNorm(X_{attention})
4). Feedforward
下面进行transformer \ block结构图中的第4部分, 也就是FeedForward, 其实就是两层线性映射并用激活函数激活, 比如说ReLU: X_{hidden} = Activate(Linear(Linear(X_{attention})))
第一次线性变化一般是把维度扩展到比较大的维度,第二次变换再压缩,这样使前面和后面元素相加,形成残差连接,残差连接必须让前面和后面位数相同,之后进行一个激活。
5). 重复3).: X_{hidden} = X_{attention} + X_{hidden} X_{hidden} = LayerNorm(X_{hidden}) X_{hidden} \in \mathbb{R}^{batch \ size \ * \ seq. \ len. \ * \ embed. \ dim.}
之后得到了隐藏层,维度和字向量是一样的。
transfromer的编码器可以是n个这样的模块,也就是说这个步骤可以重复n次,

🐋
小结: 我们到现在位置已经讲完了transformer的编码器的部分, 了解到了transformer是怎样获得自然语言的位置信息的, 注意力机制是怎样的。