Attention Is All You Need
Transformer
文章链接:
想要了解Transformer最好先了解一下seq2seq架构和Attention原理,感兴趣的可以点击以下链接:
Transformer中抛弃了传统的CNN和RNN,整个网络结构完全是由Attention机制组成。更准确地讲,Transformer由且仅由self-Attenion和Feed Forward Neural Network组成。
一个基于Transformer的可训练的神经网络可以通过堆叠Transformer的形式进行搭建,作者的实验是通过搭建编码器和解码器各6层,总共12层的Encoder-Decoder,并在机器翻译中取得了BLEU值得新高。
作者采用Attention机制的原因是考虑到RNN(或者LSTM,GRU等)的计算限制为是顺序的,也就是说RNN相关算法只能从左向右依次计算或者从右向左依次计算,这种机制带来了两个问题:
- 时间片 t 时刻的计算依赖 t-1 时刻的计算结果,这样限制了模型的并行能力;
- 顺序计算的过程中信息会丢失,尽管LSTM等门机制的结构一定程度上缓解了长期依赖的问题,但是对于特别长期的依赖现象,LSTM依旧无能为力。
Transformer的提出解决了上面两个问题,首先它使用了Attention机制,将序列中的任意两个位置之间的距离是缩小为一个常量;其次它不是类似RNN的顺序结构,因此具有更好的并行性,符合现有的GPU框架。
模型整体架构
上图就是谷歌提出的transformer 架构,这其中左半部分是 Encoder 右半部分是 Decoder。
注意图的左右两边都有“ N× ”符号,这代表方框中的模块总共有N个,每个模块的结构都是相同的(不共享权重),上一个模块的输出会作为下一个模块的输入,上图只画了其中的一层。假如N=2,那么结构如下图所示:
文中设定N=6,也就是说Encoder和Decoder部分都有6层,如下图所示:
如果还没有理解,那么请看下图:
下面介绍每一层的内部结构:
-
Encoder
Encoder模块有N=6个一样的layers,每一层包含了两个sub-layers。第一个sub-layer 是多头注意力层(multi-head attention layer) ,接着是一个简单的全连接层(前馈神经网络层)。
还有一个残差连接 (residual connection),在这个基础上, 还有一个layer norm。多头注意力层会在下文详细介绍。
-
Decoder
Decoder模块同样有6个一样的layers,但是这里的layer 和Encoder中的不一样, 这里的layer 包含了三个sub-layers:self-attention layer、 encoder-decoder attention layer 和全连接层(前馈神经网络层)。
前两个sub-layer 都是基于multi-head attention layer,但第二个sub-layer的输入同第一个有所不同,它的输入一部分来自于上一层的输出,另一部分来自于Encoder的输出结果,这样可以用来帮助解码器关注输入句子的相关部分。
第一个multi-head attention layer还加入了masking操作, masking 的作用就是防止在训练的时候使用未来的输出的单词。 比如训练时, 第一个单词不能参考第二个单词的生成结果。 Masking就会把这个信息变成0, 用来保证预测位置 i 的信息只能基于 i 前面的输出。
Multi-head attention
Self-Attention
Self-Attention工作原理
首先我们了解下它的工作原理。假设下面这句话是我们想翻译的:
“The animal didn’t cross the street because it was too tired”
句子中“it”指的是什么?指street还是说animal呢?对人来说很简单的问题,对机器却很复杂。当模型处理单词“it”时,self-attention 就可以使它指代“animal”。当模型处理每个单词时(输入序列中每个位置),self-attention使得它可以查看输入序列的其他位置以便于更好的编码该单词。
如果你熟悉RNN,考虑一下如何维护隐藏层状态来更好的结合已经处理的先前的单词/向量与目前正在处理的单词/向量。Transformer使用self-attention来将其他相关单词的“理解”融入到目前正在处理的单词。
例如上图中,在第5层时,我们就知道 it 大概指的是 animal 了。
Self-Attention算法细节
Step1
第一步,为编码器的每个输入单词创建三个向量,即 Query vector, Key vector, Value vector。
这些向量通过单词 embedding 和三个矩阵相乘得到,请注意,这些新向量的尺寸小于嵌入向量。它们的维数为64,而嵌入和编码器输入/输出向量的维数为512.它们不必更小,这是一种架构选择,可以使多头注意力计算(大多数)不变。
将x1乘以WQ得到 Query 向量 q1,同理得到 Key 向量和 Value 向量,这三个向量对 attention 的计算有很重要的作用。
Step2
第二步,是计算一个得分。
假设我们要计算一个例子中第一个单词 “Thinking” 的 self-attention,就需要根据这个单词,对输入句子的每个单词进行评分,这个分数决定了对其他单词放置多少关注度。
分数的计算方法是:
例如我们正在考虑 Thinking 这个词,就用它的 q1 去乘以每个位置的 ki。
Step3&4
第三步和第四步,是将得分加以处理再传递给 softmax。
第三步是将得分除以 8(sqrt{d_{k}})(因为论文中使用的 key 向量的维度是64,8是它的平方根,这个sqrt{d_{k}}并不是唯一值,经验所得),这样可以有更稳定的梯度。
第四步将第三步分结果传递给 softmax,Softmax 将分数标准化,这样加起来和为 1。这个 softmax 分数决定了每个单词在该位置的表达程度。很明显,这个位置上的单词将具有最高的softmax分数,但有时候注意与当前单词相关的另一个单词也是有用的。
Step5
第五步是将每个Value向量乘以softmax后的得分得到新的Value向量。这里实际上的意义在于保存对当前词的关注度不变的情况下,降低对不相关词的关注(通过乘以 0.001 这样小的数字,来淹没不相关的单词)。
Step6
将第五步得到的value向量加权求和:
这就是第一个单词的 self-attention 的输出。
整体计算公式为:
得到的向量接下来要输入到前馈神经网络,在实际实现中用矩阵乘法的形式完成,下面来介绍Self-Attention的矩阵计算。
Self-Attention的矩阵计算
第一步 是计算Query, Key, Value矩阵。通过将词嵌入整合到矩阵X中,并将其乘以我们训练过的权重矩阵(WQ,WK,WV)来实现。
X矩阵中的每一行对应于输入句子中的一个单词。 我们看到的X每一行的方框数实际上是词嵌入的维度,图中所示的和论文中是有差距的。
X:图中的4个方框论文中为512个。
q / k / v向量:图中的3个方框论文中为64个。
最后 ,由于我们在处理矩阵,我们可以将步骤2到步骤6合并为一个公式来计算self-attention层的输出。
multi-headed 机制
论文中还增加一种称为 multi-headed 注意力机制,可以提升注意力层的性能,它使得模型可以关注不同位置。
虽然在上面的例子中,z1 包含了一点其他位置的编码,但当前位置的单词还是占主要作用, 当我们想知道“The animal didn’t cross the street because it was too tired” 中 it 的含义时,这时就需要关注到其他位置。
这个机制为注意层提供了多个“表示子空间”。下面我们将具体介绍:
经过 multi-headed , 我们会得到和 heads 数目一样多的 Query / Key / Value 权重矩阵组,论文中用了8个,那么每个encoder/decoder我们都会得到 8 个集合。这些集合中的每个矩阵都是随机初始化的,经过训练之后,每个集合会将input embeddings 投影到不同的表示子空间中。如下图所示(图中是两个头的例子):
简单来说,就是定义 8 组权重矩阵,每个单词会做 8 次上面的 self-attention 的计算,这样每个单词会得到 8 个不同的加权求和 z。如下图所示:
但在 feed-forward 处只能接收一个矩阵,所以需要将这八个压缩成一个矩阵,方法就是先将8个z矩阵连接起来,然后乘一个额外的权重矩阵WO。如下图所示:
这样multi-headed self attention的全部内容就介绍完了。之前可能都是一些过程的图解,现在我将这些过程连接在一起,用一个整体的框图来表示一下计算的过程,希望可以加深理解。
位置编码
截止目前为止,我们介绍的Transformer模型并没有捕捉顺序序列的能力,也就是说无论句子的结构怎么打乱,Transformer都会得到类似的结果。换句话说,Transformer只是一个功能更强大的词袋模型而已。
为了解决这个问题,论文中在编码词向量时引入了位置编码(Position Embedding)的特征。具体地说,位置编码会在词向量中加入了单词的位置信息,这样Transformer就能确定每个单词的位置,或者序列中不同单词之间的距离。
那么怎么编码这个位置信息呢?常见的模式有:
- 通过训练学习;
- 自己设计编码规则。
试验后发现两种选择的结果是相似的,所以采用了第2种方法,优点是不需要训练参数,而且即使在训练集中没有出现过的句子长度上也能用。那么这个位置编码该是什么样子呢?通常位置编码是一个和词向量等长的特征向量,这样便于和词向量进行求和操作。公式如下:
在上式中:
- pos 指的是这个 word 在这个句子中的位置
- i指的是 embedding 维度。比如选择 d_model=512,那么i就从1数到512
这种方法的好处为:
任意位置的PE(pos+k)都可以被 PE(pos) 的函数线性表示,三角函数特性:
假如input embedding 的维度为4,那么实际的positional encodings如下所示:
Encoder部分的输入为:
下图是20个单词的 positional encoding,每行代表一个单词的位置编码,因此第一行就是输入序列中第一个单词的词嵌入向量,每行包含 512 个值, 每个值介于 -1 和 1 之间,这里我们进行了涂色,使模式可见。
当然这并不是位置编码的唯一方法,只是这个方法能够扩展到看不见的序列长度,例如当我们要翻译一个句子,这个句子的长度比我们训练集中的任何一个句子都长时。
残差连接
接下来介绍每个self-attention层的左右连接情况,我们称这个为:layer-normalization 步骤。如下图所示:
再进一步探索其内部计算方式,我们可以将上面图层可视化为下图:
Decoder
上面介绍的是Encoder中的结构,下面我们看一下 Decoder 和Encoder有哪些区别:
相同点:
- Encoder有的Decoder都有。
不同点:
- Decoder比 Encoder 多了一个 Masked Multi-head attention,其实它是在Encoder中Multi-head attention的基础上做了微小调整,即在 self attention 的 softmax 步骤之前,将未来的位置设置为 -inf 来屏蔽这些位置,这样做是为了self attention 层只能关注输出序列中靠前的位置,因为还没有输出的单词是不能拿来预测当前单词的。
- Decoder中的Multi-head attention和Encoder中的Multi-head attention,输入不同。Encoder中所有的输入都来自于上一层的输出,而Decoder中只有Queries 矩阵来自于上一层的输出,Keys 和 Values 矩阵都是从Encoder的输出中得到。
- Decoder最后要经过 Linear 和 softmax 输出概率。
Decoder整体流程:
线性层和 softmax 层
Decoder的输出是浮点数的向量列表。我们是如何将其变成一个单词的呢?这就是最终的线性层和softmax层所做的工作。
线性层就是一个很简单的全连接神经网络,将解码器输出的向量映射成一个更长的向量,这个向量称为logits向量。例如我们有 10000 个无重复的单词(模型的“输出词汇表”,这是从训练集中学到的),那么最后输出的向量就有一万维。
每个位置上的值代表了相应单词的得分。softmax 层将这个分数转换为了概率(全部为正数,加起来为1)。我们选取最高概率所对应的索引,然后通过这个索引找到对应的单词作为输出。
下图是从Decoder的输出开始到最终softmax的输出。一步一步的图解:
简述训练过程
现在我们已经讲解了transformer的训练全过程了,让我们回顾一下。
在训练期间,未经训练的模型将通过如上的流程一步一步计算。因为我们是在对标记的训练数据集进行训练(机器翻译可以看做双语平行语料),那么我们可以将模型输出与实际的正确答案相比较,来进行反向传播。
为了更好的理解这部分内容,我们假设我们输出的词汇只有(“a”,“am”,“i”,“thanks”,“student”和“”(“句末”的缩写))。
在我们开始训练之前,我们模型的输出词汇是在预处理阶段创建的。
一旦我们定义了输出的词汇表,那么我们就可以使用相同宽度的向量来表示词汇表中的每个单词。称为one-hot编码。例如,我们可以使用下面向量来表示单词“am”:
下面我们再讨论一下模型的损失函数,我们优化的指标,引导一个训练有素且令人惊讶的精确模型。
假设我们正在训练一个模型,比如将“merci”翻译成“谢谢”。这意味着我们希望模型计算后的输出为“谢谢”,但由于这种模式还没有接受过训练,所以这种情况不太可能发生。
这是因为模型的参数(权重)都是随机初始化的,因此(未经训练的)模型对每个单词产生的概率分布是具有无限可能的,但是我们可以通过实际输出和期望输出进行比较,然后利用反向传播算法调整所有模型的权重,使得最终输出更接近期望输出。
那么如何比较算法预测值与真实期望值呢?
实际上,我们对其做一个简单的减法即可。你也可以了解交叉熵和Kullback-Leibler散度来掌握这种差值的判断方式。
但是,需要注意的是,这只是一个很简单的demo,真实情况下,我们需要输出一个更长的句子,例如。输入:“je suis étudiant”和预期输出:“I am a student”。这样的句子输入,意味着我们的模型能够连续的输出概率分布。其中:
- 每个概率分布由宽度为vocab_size的向量表示(在我们的示例中vocab_size为6,但实际上可能为3,000或10,000维度)
- 第一概率分布在与单词“i”相关联的单元处具有最高概率
- 第二概率分布在与单词“am”相关联的单元格中具有最高概率
- 依此类推,直到第五个输出分布表示’ '符号,意味着预测结束。
上图为:输入:“je suis étudiant”和预期输出:“I am a student”的期望预测概率分布情况。
在算法模型中,虽然不能达到期望的结果,但是训练足够长时间之后,我们的算法模型能够有如下图所示的概率分布情况:
现在,因为模型一次生成一个输出,我们可以理解为这个模型从该概率分布(softmax)矢量中选择了具有最高概率的单词并丢弃了其余的单词。