图解Transformer

前言

      Attention这种机制最开始应用于机器翻译的任务中,并且取得了巨大的成就,因而在最近的深度学习模型中受到了大量的关注。在在这个基础上,我们提出一种完全基于Attention机制来加速深度学习训练过程的算法模型-Transformer。事实证明Transformer结构在特定任务上已经优于了谷歌的神经网络机器翻译模型。但是,Transformer最大的优势在于其在并行化处理上做出的贡献。谷歌也在利用Transformer的并行化方式来营销自己的云TPU。所以,现在让我们一步一步剖析Transformer的神秘面纱,让我看看他是怎么一步一步训练的。

     Transformer在Goole的一篇论文Attention is All You Need被提出,为了方便实现调用Transformer Google还开源了一个第三方库,基于TensorFlow的Tensor2Tensor,一个NLP的社区研究者贡献了一个Torch版本的支持:guide annotating the paper with PyTorch implementation。这里,我想用一些方便理解的方式来一步一步解释Transformer的训练过程,这样即便你没有很深的深度学习知识你也能大概明白其中的原理。

A High-Level Look

       我们先把Transformer想象成一个黑匣子,在机器翻译的领域中,这个黑匣子的功能就是输入一种语言然后将它翻译成其他语言。如下图:

掀起The Transformer的盖头,我们看到在这个黑匣子由2个部分组成,一个Encoders和一个Decoders。

我们再对这个黑匣子进一步的剖析,发现每个Encoders中分别由6个Encoder组成(论文中是这样配置的)。而每个Decoders中同样也是由6个Decoder组成。

对于Encoders中的每一个Encoder,他们结构都是相同的,但是并不会共享权值。每层Encoder有2个部分组成,如下图: 

每个Encoder的输入首先会通过一个self-attention层,通过self-attention层帮助Endcoder在编码单词的过程中查看输入序列中的其他单词。如果你不清楚这里在说什么,不用着急,之后我们会详细介绍self-attention的。

Self-attention的输出会被传入一个全连接的前馈神经网络,每个encoder的前馈神经网络参数个数都是相同的,但是他们的作用是独立的。

每个Decoder也同样具有这样的层级结构,但是在这之间有一个Attention层,帮助Decoder专注于与输入句子中对应的那个单词(类似与seq2seq models的结构)

Bringing The Tensors Into The Picture

      在上一节,我们介绍了Transformer的网络结构。现在我们以图示的方式来研究Transformer模型中各种张量/向量,观察从输入到输出的过程中这些数据在各个网络结构中的流动。

      首先还是NLP的常规做法,先做一个词嵌入:什么是文本的词嵌入?

      我们将每个单词编码为一个512维度的向量,我们用上面这张简短的图形来表示这些向量。词嵌入的过程只发生在最底层的Encoder。但是对于所有的Encoder来说,你都可以按下图来理解。输入(一个向量的列表,每个向量的维度为512维,在最底层Encoder作用是词嵌入,其他层就是其前一层的output)。另外这个列表的大小和词向量维度的大小都是可以设置的超参数。一般情况下,它是我们训练数据集中最长的句子的长度。

     上图其实介绍到了一个Transformer的关键点。你注意观察,在每个单词进入Self-Attention层后都会有一个对应的输出。Self-Attention层中的输入和输出是存在依赖关系的,而前馈层则没有依赖,所以在前馈层,我们可以用到并行化来提升速率。

    下面我用一个简短的句子作为例子,来一步一步推导transformer每个子层的数据流动过程。

Now We’re Encoding!

       正如之前所说,Transformer中的每个Encoder接收一个512维度的向量的列表作为输入,然后将这些向量传递到‘self-attention’层,self-attention层产生一个等量512维向量列表,然后进入前馈神经网络,前馈神经网络的输出也为一个512维度的列表,然后将输出向上传递到下一个encoder。

     如上图所示,每个位置的单词首先会经过一个self attention层,然后每个单词都通过一个独立的前馈神经网络(这些神经网络结构完全相同)。

Self-Attention at a High Level

      Self attention这个单词看起来好像每个人都知道是什么意思,但实质上他是算法领域中新出的概念,你可以通过阅读:Attention is All You Need 来理解self attention的原理。

      假设下面的句子就是我们需要翻译的输入句:

The animal didn't cross the street because it was too tired

      这句话中的"it"指的是什么?它指的是“animal”还是“street”?对于人来说,这其实是一个很简单的问题,但是对于一个算法来说,处理这个问题其实并不容易。self attention的出现就是为了解决这个问题,通过self attention,我们能将“it”与“animal”联系起来。

      当模型处理单词的时候,self attention层可以通过当前单词去查看其输入序列中的其他单词,以此来寻找编码这个单词更好的线索。

      如果你熟悉RNNs,那么你可以回想一下,RNN是怎么处理先前单词(向量)与当前单词(向量)的关系的?RNN是怎么计算他的hidden state的。self-attention正是transformer中设计的一种通过其上下文来理解当前词的一种办法。你会很容易发现...相较于RNNs,transformer具有更好的并行性。

如上图,是我们第五层Encoder针对单词'it'的图示,可以发现,我们的Encoder在编码单词‘it’时,部分注意力机制集中在了‘animl’上,这部分的注意力会通过权值传递的方式影响到'it'的编码。

更多细节可以查看 Tensor2Tensor notebook 

Self-Attention in Detail

    这一节我们先介绍如何用向量的方式来计算self attention,然后再来看看它是如何使用矩阵来实现的。

     计算self attention的第一步是从每个Encoder的输入向量上创建3个向量(在这个情况下,对每个单词做词嵌入)。所以,对于每个单词,我们创建一个Query向量,一个Key向量和一个Value向量。这些向量是通过词嵌入乘以我们训练过程中创建的3个训练矩阵而产生的。

     注意这些新向量的维度比嵌入向量小。我们知道嵌入向量的维度为512,而这里的新向量的维度只有64维。新向量并不是必须小一些,这是网络架构上的选择使得Multi-Headed Attention(大部分)的计算不变。

我们将X_{1}乘以W^{Q}的权重矩阵得到新向量q_{1}q_{1}既是“query”的向量。同理,最终我们可以对输入句子的每个单词创建“query”,
“key”,“value”的新向量表示形式。

对了..“query”,“key”,“value”是什么向量呢?有什么用呢?

这些向量的概念是很抽象,但是它确实有助于计算注意力。不过先不用纠结去理解它,后面的的内容,会帮助你理解的。

      计算self attention的第二步是计算得分。以上图为例,假设我们在计算第一个单词“thinking”的self attention。我们需要根据这个单词对输入句子的每个单词进行评分。当我们在某个位置编码单词时,分数决定了对输入句子的其他单词的关照程度。

       通过将query向量和key向量点击来对相应的单词打分。所以,如果我们处理开始位置的的self attention,则第一个分数为q_{1}k_{1}的点积,第二个分数为q_{2}k_{2}的点积。如下图

第三步和第四步的计算,是将第二部的得分除以8\sqrt{d_{k}})(论文中使用key向量的维度是64维,其平方根=8,这样可以使得训练过程中具有更稳定的提取。这个\sqrt{d_{k}}并不是唯一值,经验所得)。然后再将得到的输出通过softmax函数标准化,使得最后的列表和为1。

这个softmax的分数决定了当前单词在每个句子中每个单词位置的表示程度。很明显,当前单词对应句子中此单词所在位置的softmax的分数最高,但是,有时候attention机制也能关注到此单词外的其他单词,这很有用。

第五步是将每个Value向量乘以softmax后的得分。这里实际上的意义在于保存对当前词的关注度不变的情况下,降低对不相关词的关注。

第六步是 累加加权值的向量。 这会在此位置产生self-attention层的输出(对于第一个单词)。

总结self-attention的计算过程,(单词级别)就是得到一个我们可以放到前馈神经网络的矢量。 然而在实际的实现过程中,该计算会以矩阵的形式完成,以便更快地处理。下面我们来看看Self-Attention的矩阵计算方式。

Matrix Calculation of Self-Attention

第一步是去计算Query,Key和Value矩阵。我们将词嵌入转化成矩阵X中,并将其乘以我们训练的权值矩阵(W^{Q},W^{K},W^{V}

X矩阵中的每一行对应于输入句子中的一个单词。 我们看到的X每一行的方框数实际上是词嵌入的维度,图中所示的和论文中是有差距的。X(图中的4个方框论文中为512个)和q / k / v向量(图中的3个方框论文中为64个)

最后,由于我们正在处理矩阵,我们可以在一个公式中浓缩前面步骤2到6来计算self attention层的输出。

 

The Beast With Many Heads

本文通过使用“Multi-headed”的机制来进一步完善self attention层。“Multi-headed”主要通过下面2中方式改善了attention层的性能:

1. 它拓展了模型关注不同位置的能力。在上面例子中可以看出,The animal didn't cross the street because it was too tired,我们的attention机制计算出“it”指代的为“animal”,这在对语言的理解过程中是很有用的。

2.它为attention层提供了多个“representation subspaces”。由下图可以看到,在self attention中,我们有多个个Query / Key / Value权重矩阵(Transformer使用8个attention heads)。这些集合中的每个矩阵都是随机初始化生成的。然后通过训练,用于将词嵌入(或者来自较低Encoder/Decoder的矢量)投影到不同的“representation subspaces(表示子空间)”中。

    通过multi-headed attention,我们为每个“header”都独立维护一套Q/K/V的权值矩阵。然后我们还是如之前单词级别的计算过程一样处理这些数据。

    如果对上面的例子做同样的self attention计算,而因为我们有8头attention,所以我们会在八个时间点去计算这些不同的权值矩阵,但最后结束时,我们会得到8个不同的Z矩阵。如下图:

    瞧瞧,这会给我们后续工作造成什么问题?

    我们知道在self-attention后面紧跟着的是前馈神经网络,而前馈神经网络接受的是单个矩阵向量,而不是8个矩阵。所以我们需要一种办法,把这8个矩阵压缩成一个矩阵

    我们怎么做? 

   我们将这8个矩阵连接在一起然后再与一个矩阵W^{O}相乘。步骤如下图所示:

这样multi-headed self attention的全部内容就介绍完了。之前可能都是一些过程的图解,现在我将这些过程连接在一起,用一个整体的框图来表示一下计算的过程,希望可以加深理解。

      现在我们已经触及了attentionheader,让我们重新审视我们之前的例子,看看例句中的“it”这个单词在不同的attention header情况下会有怎样不同的关注点。

      如图:当我们对“it”这个词进行编码时,一个注意力的焦点主要集中在“animal”上,而另一个注意力集中在“tired” 

但是,如果我们将所有注意力添加到图片中,那么事情可能更难理解:

 

Representing The Order of The Sequence Using Positional Encoding

# 使用位置编码表示序列的顺序

 

妈的 不撸了...明天再撸

下面是谷歌翻译的结果 将就看=.=

 

我们到目前为止描述的模型中缺少的一件事是一种考虑输入序列中单词顺序的方法。

为了解决这个问题,变换器为每个输入嵌入添加了一个向量。这些向量遵循模型学习的特定模式,这有助于确定每个单词的位置,或者序列中不同单词之间的距离。这里的直觉是,将这些值添加到嵌入中,一旦它们被投影到Q / K / V向量中并且在点积注意期间,就在嵌入向量之间提供有意义的距离。

 


为了让模型了解单词的顺序,我们添加位置编码向量 - 其值遵循特定模式。

 

如果我们假设嵌入的维度为4,那么实际的位置编码将如下所示:


玩具嵌入大小为4的位置编码的真实示例

 

这种模式可能是什么样的?

在下图中,每行对应矢量的位置编码。因此第一行将是我们添加到输入序列中第一个字的嵌入的向量。每行包含512个值 - 每个值的值介于1和-1之间。我们对它们进行了颜色编码,使图案可见。


嵌入大小为512(列)的20个字(行)的位置编码的真实示例。您可以看到它在中心位置分成两半。这是因为左半部分的值由一个函数(使用正弦)生成,而右半部分由另一个函数(使用余弦)生成。然后将它们连接起来以形成每个位置编码矢量。

位置编码的公式在论文(3.5节)中描述。您可以在中查看用于生成位置编码的代码get_timing_signal_1d()。这不是位置编码的唯一可能方法。然而,它具有能够扩展到看不见的序列长度的优点(例如,如果我们训练的模型被要求翻译的句子比我们训练集中的任何句子更长)。

剩余物

在继续之前我们需要提到的编码器架构中的一个细节是每个编码器中的每个子层(自注意,ffnn)在其周围具有残余连接,然后是层规范化步骤。

 

如果我们要将矢量和与自我关注相关的图层规范操作可视化,它将如下所示:

 

这也适用于解码器的子层。如果我们想到2个堆叠编码器和解码器的变压器,它看起来像这样:

解码器方面

现在我们已经涵盖了编码器方面的大多数概念,我们基本上知道解码器的组件如何工作。但是让我们来看看它们如何协同工作。

编码器通过处理输入序列开始。然后将顶部编码器的输出变换成一组关注矢量K和V.这些将由每个解码器在其“编码器 - 解码器关注”层中使用,这有助于解码器关注输入序列中的适当位置:


完成编码阶段后,我们开始解码阶段。解码阶段中的每个步骤输出来自输出序列的元素(在这种情况下为英语翻译句子)。

以下步骤重复此过程,直至特殊 到达符号表示变压器解码器已完成其输出。每个步骤的输出在下一个时间步骤中被馈送到底部解码器,并且解码器像编码器一样冒泡它们的解码结果。就像我们对编码器输入所做的那样,我们在这些解码器输入中嵌入并添加位置编码,以指示每个字的位置。

解码器中的自关注层以与编码器中的自注意层略有不同的方式操作:

在解码器中,仅允许自我关注层关注输出序列中的较早位置。这是通过-inf在自我关注计算中的softmax步骤之前屏蔽未来位置(将它们设置为)来完成的。

“编码器 - 解码器注意”层的工作方式与多头自注意一样,只是它从下面的层创建其查询矩阵,并从编码器堆栈的输出中获取键和值矩阵。

最终线性和Softmax层

解码器堆栈输出浮点数向量。我们如何将其变成一个单词?这是最终线性层的工作,其后是Softmax层。

线性层是一个简单的完全连接的神经网络,它将由解码器堆栈产生的向量投影到一个更大,更大的向量中,称为对数向量。

让我们假设我们的模型知道从训练数据集中学到的10,000个独特的英语单词(我们的模型的“输出词汇表”)。这将使logits矢量10,000个细胞宽 - 每个细胞对应于一个唯一单词的得分。这就是我们如何解释模型的输出,然后是线性层。

然后softmax层将这些分数转换为概率(所有正数,都加起来为1.0)。选择具有最高概率的单元,并且将与其相关联的单词作为该时间步的输出。

 

 
该图从底部开始,产生的矢量作为解码器堆栈的输出。然后它变成输出字。

 

回顾培训

现在我们已经通过训练有素的变压器覆盖了整个前进过程,看一下培训模型的直觉是有用的。

在训练期间,未经训练的模型将通过完全相同的前进传球。但由于我们正在对标记的训练数据集进行训练,因此我们可以将其输出与实际正确的输出进行比较。

为了想象这一点,让我们假设我们的输出词汇只包含六个单词(“a”,“am”,“i”,“thanks”,“student”和“<eos>”(“句末”的缩写)) 。


在我们开始训练之前,我们模型的输出词汇是在预处理阶段创建的。

一旦我们定义了输出词汇表,我们就可以使用相同宽度的向量来表示词汇表中的每个单词。这也称为单热编码。例如,我们可以使用以下向量指示单词“am”:


示例:我们的输出词汇表的单热编码

在回顾一下之后,让我们讨论一下模型的损失函数 - 我们在训练阶段优化的指标,以引导一个训练有素且令人惊讶的精确模型。

损失函数

假设我们正在训练我们的模型。说这是我们在训练阶段的第一步,我们正在通过一个简单的例子进行训练 - 将“merci”翻译成“谢谢”。

这意味着,我们希望输出是指示“谢谢”一词的概率分布。但由于这种模式还没有接受过训练,所以这种情况不太可能发生。


由于模型的参数(权重)都是随机初始化的,因此(未经训练的)模型产生具有每个单元/单词的任意值的概率分布。我们可以将它与实际输出进行比较,然后使用反向传播调整所有模型的权重,使输出更接近所需的输出。

 

你如何比较两个概率分布?我们简单地从另一个中减去一个。有关更多详细信息,请查看 交叉熵Kullback-Leibler散度

但请注意,这是一个过于简单的例子。更现实的是,我们将使用长于一个单词的句子。例如 - 输入:“jesuisétudiant”和预期输出:“我是学生”。这实际意味着,我们希望我们的模型能够连续输出概率分布,其中:

  • 每个概率分布由宽度为vocab_size的向量表示(在我们的玩具示例中为6,但更实际地是3,000或10,000的数字)
  • 第一概率分布在与单词“i”相关联的单元处具有最高概率
  • 第二概率分布在与单词“am”相关联的单元格中具有最高概率
  • 依此类推,直到第五个输出分布表示' <end of sentence>'符号,其中还有一个与10,000元素词汇表相关联的单元格。


我们将在一个样本句子的训练示例中训练我们的模型的目标概率分布。

 

在足够大的数据集上训练模型足够的时间之后,我们希望产生的概率分布看起来像这样:


希望通过培训,该模型将输出我们期望的正确翻译。当然,这个短语是否是训练数据集的一部分并不是真正的指示(参见:交叉验证)。请注意,即使不太可能是该时间步的输出,每个位置都会获得一点概率 - 这是softmax的一个非常有用的属性,有助于训练过程。

现在,因为模型一次生成一个输出,我们可以假设模型从该概率分布中选择具有最高概率的单词并丢弃其余的单词。这是一种方法(称为贪婪解码)。另一种方法是坚持,比如前两个词(例如,'我'和'a'),然后在下一步中,运行模型两次:一旦假设第一个输出位置是单词'I',另一次假设第一个输出位置是单词'me',并且考虑到#1和#2位置保留的任何版本产生的错误都较少。我们重复这个位置#2和#3 ......等等。这种方法称为“波束搜索”,在我们的例子中,beam_size是两个(因为我们在计算位置#1和#2的波束后比较了结果),和top_beams也是两个(因为我们保留了两个单词)。这些都是您可以试验的超参数。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章