图解Transformer

本文翻译自The Illustrated Transformer,外加自己的理解与想法。

个人感觉原文作者在介绍decoder部分时讲的并不是很清楚,不妨将文章多读几遍,最好还是能够结合代码能方便于理解。

在前一篇文章中介绍了注意力(Attention)—一种在现代深度学习中无处不在的方法。注意力有助于提高神经机器翻译应用的性能。在这篇文章中我们将学习Transformer—一种使用注意力来提高训练速度的模型。Transformer在某些特定的任务中优于Google的神经机器翻译模型(Neural Machine Translation model), 然而Transformer最大的优点来自于他对并行化的贡献,Google也建议使用Transformer作为参考模型来使用他们的 Cloud TPU。那么就让我们拆开Transformer,看看它是如何工作的吧。

Transformer在 Attention is All You Need这篇论文中被提出,Tensorflow版本的Transformer也包含在Tensor2Tensor中, 哈佛大学自然语言处理小组也写了篇使用Pytorch来实现和解释Transformer. 在这篇文章中我将一些事情简化并一点一点的介绍Transformer的概念以便于让那些基础较薄弱的读者能够理解。

模型的高层实现

我们先将模型看成一个黑盒,在机器翻译应用中我们向黑盒中输入一种语言的句子,黑盒会输出另一种语言的句子。

将Transformer解开一层以后,我们可以看见Transformer由编码器组件(encoding component)和解码器组件(decoding component)构成,编码器组件与解码器组件相连。

编码器组件就是由一堆的编码器构成(论文中说编码器组件由六个编码器组成,六也没有什么特殊的含义,你可以根据实验调整这个数字),同样的解码器组件也是由一堆解码器构成的。

编码器的结构都是相同的,但是每个编码器都有自己的参数且并不共享,每一个编码器都可以解成下图的两个子部分:

编码器的输入首先经过自注意力层(self-attention layer ),这个自注意力层能够让编码器编码句子中某个单词时能够”注意”到句子中的其他单词,自注意力层的细节稍后再讲解。

自注意力层的输出送给一个前馈神经网络(feed-forward neural network), 这个前馈神经网络在每一个编码器中的结构都是完全相同的。

解码器同样拥有这两个层,但是在这两个层之间多了一个编码-解码注意力层(Encoder-Decoder Attention), 这个层使得解码器可以专注于在输入句子中相关的部分(跟Seq2seq模型中的注意力是一个作用)。

模型的运行

我们已经学习了模型的主要组成部分,现在让我们看一下各种向量/张量,看看它们是怎么输入到Transformer中并得到输出的。

与一般的NLP应用一样我们现将输入的单词用word embedding算法转换成一个向量。

这种嵌入(embedding)仅存在于编码器的最底层,每一个编码器都获得到一组512维的向量,在最底层的编码器这组向量就是word embedding,在其余的编码器中这组向量上一个编码器的输出, 一共有多少组向量呢?这是一个超参数,一般来时它等于要输入的最长的那个句子的长度。

在将单词转换成向量后,每一个向量就通过编码器的两层:

这里我们要学习Transformer中的关键属性:在编码器中句子的每一个单词都有自己的路径,在自注意力层的这些路径存在依赖关系,但在前馈神经网络中这些路径不存在依赖关系,因此可以在流经前馈神经网络时各个路径能够并行操作。

接下来,我们将示例切换为更短的句子,我们将查看编码器的每个子层中究竟发生了什么。

进入编码器

正如我们之前提到的,一个编码器接受一组向量作为输入,编码器将这组向量送入自注意力层,再将自注意力层的输出送进前馈神经网络,最后将前馈神经网络的输出送进下一个编码器。

自注意力的高层实现

虽然”自注意力”这个词我说了很多遍搞得好像这是一个人尽皆知的概念,但其实并不是这样,我是读了Attention is all You need这篇论文才知道这个概念,让我们看一看自注意力是怎么工作的吧!

假设以下句子是我们要翻译句子:

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

这句话中的’it’指的是啥呢?指的是‘street’还是’animal’? 这个问题对人来说非常简单,但是对于机器来说就不是那么容易了。

当模型在处理”it”时,自注意力就赋予模型模型将“it”与“animal”相联系的能力。

当模型在处理每一个单词时, 自注意力允许模型查看句子中的其他单词然后对这个单词进行更好的编码。

如果你很熟悉循环神经网络, 想象一下循环神经网络是如何通过隐藏状态将它以前处理过的单词/向量与当前处理过的单词/向量的相结合。自注意力是Transformer用来将其他相关单词的“理解”放入我们正在处理的单词中。

你可以用一下 Tensor2Tensor notebook ,你可以在其中加载Transformer模型,并使用此交互式可视化对其进行检查。

自注意力的细节

让我们首先看看如何用向量计算自注意力,然后看看如何用矩阵实现它。

计算自注意力的第一步就是要用每一个要输入到编码器的向量(每一个单词embedding后的向量)创建三个向量,也就是对于每个单词我们创建一个Query向量,一个Key向量和一个Value向量, 这三个向量是用单词向量和三个矩阵相乘得到的,这三个矩阵是我们要训练的参数。

注意到生成这三个向量的维度(64维)比单词向量的维度(512维)要小,但是多头(multiheaded)处理之后维度又恢复到512维,后面会介绍多头这一概念。

query, key,value又是啥?

它们是对计算和思考注意力有用的抽象。请继续阅读下面的注意力计算方法,就可以理解这些向量所担任的角色。

计算自注意力的第二步就是要计算出一个分数。比如我们要计算第一个词”Thinking”的自注意力,我们需要根据这个词对输入句子的每个单词进行评分。当我们在某个位置编码一个单词时,分数决定了这个单词要在输入句子的其他单词上集中多少注意力。

这个分数是由这个单词的query向量和所有单词的key向量点乘的得到的,所以当我们处理第一个单词时第一个分数就是q1点乘k1,第二个分数就是q1点乘k2。

第三步就是把分数除以8(key向量维度的平方根,目的是为了让梯度更稳定一些,具体来说就是防止值太大导致softmax函数处于近似水平状态,影响梯度下降,当然你也可以用其他值),第四步就是再把分数送入到softmax,softmax将分数标准化(经过softmax所有值加起来和为1)。

Softmax后分数决定了每个单词在这个位置上的表达量。很显然在这个单词自己拥有最高的softmax分数,但有时能够注意与当前单词相关的另一个单词,这是非常有用的。

第五步是将每一个value向量都乘以对应的softmax分数,这一步从直觉上来看就是要保持相关单词的值并减少不相关单词的值。

第六步是对加权后的value求和(Z1 = V1 + V2),然后就计算出第一个单词在句子中的自注意力。

这就是自注意力的计算过程。最终的向量就是我们要送到前馈网络的向量,然而在实际的应用中都是用矩阵来计算的而不是向量,因为矩阵可以更快所以然我们看看用矩阵是怎么做计算的。

自注意力的矩阵运算

第一步是要计算Query,Key和Value矩阵,我们把词向量打包成一个向量X, 然后与我们要训练的权重矩阵相乘(WQ, WK, WV)。

最后,我们要处理这些矩阵,我们可以将六步合成一步,直接使用一个公式来计算自注意力层的输出。

多头野兽

这篇论文通过引入多头注意力( “multi-headed” attention)机制更加细化或是说完善了了自注意力层,这可以从两个方面提高性能:

1.它提高了模型注意不同位置的能力,上面的例子Z1它只包含一点点其他单词的信息,但是这个单词本身却可能处于支配地位,如果我们翻译像是”The animal didn’t cross the street because it was too tired”这样一句话,要是知道“it”指的是什么就好了。

2.它赋予注意力层许多的表征子空间,正如我们下面将要看到的,使用了多头注意力我们就会有很多组Query/Key/Value矩阵(Transformer有八个头,所以每个编码器/解码器会有八组Query/Key/Value矩阵),每一组矩阵都是随机初始化的,经过训练之后这八组矩阵可以讲输入词向量转成八种不同的表征。

综合这两条来说就是,多头注意力能够找到一个单词在空间中的不同表示,进而在编码这个单词的时候能够注意到更多有用的其他单词。

我们做八次不同的计算,就能得到八个Z矩阵。

这八个矩阵非常棘手因为前馈神经网络一次只能处理一个矩阵,所以我们要将这八个矩阵压缩成一个矩阵。

怎么做呢?我们先将这些矩阵连接起来然后把它乘以矩阵W0

多头注意力差不多讲完了,我知道各种矩阵可能让你眼花缭乱,我把它们放在一张图里,这样就能看的清了。

让我们重新回顾一下以前的例子,看看在我们的示例句中,当我们编码“it”这个词时,两个注意力头的注意力集中在哪里:

如果我们看所有注意力头的注意力,额,事情就变得难已解释了。

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

到目前为止我们还有一样东西没有介绍那就是模型如何了解寻列序中单词的顺序。

为了解决这个问题Transformer在每一个输入向量上加上一个向量,这些向量遵循模型学习的特定模式,这有助于确定每个单词的位置,或者序列中不同单词之间的距离。将这些值添加到嵌入向量中,一旦嵌入向量被投影到q/k/v向量中,以及在点乘注意力期间,就可以在嵌入向量之间提供有意义的距离。

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

这个特定的模式是什么样子呢?

在下图中,每一行对应于向量的位置编码。所以第一行就是我们要与输入序列中第一个单词嵌入向量相加的向量。每一行有512个值,每个值都在-1和1之间,我们将其可视化展示:

在论文中的3.5节给出了生成编码向量的公式,具体的代码实现在get_timing_signal_1d()中给出,这不是位置编码的唯一可能方法。但是,它的优点是能够根据看不见的序列长度进行缩放,换句话说就是对任意长的句子我都能进行位置编码(例如,如果我们的训练模型被要求翻译的句子比我们训练集中的任何句子都长)。

残差网络

在继续之前,我们需要提到的编码器体系结构中的一个细节,每个编码器中的每个子层(self-attention,ffnn)都有一个残差连接,然后是一个层规范化(layer-normalization)步骤。

将其可视化的展示出来:

解码器中的子层同样如此,让我们画一个有两个编码器和两个解码器的Transformer:

进入解码器

在我们已经介绍了编码器方面的大部分概念,我们基本上也知道解码器的组件是如何工作的。但让我们看看他们是如何合作的。

编码器通过处理输入序列启动。然后将顶部编码器的输出转换为一组注意向量K和V。每个解码器将在其“编码器-解码器-注意”层( “encoder-decoder attention” layer)中使用这些注意向量,这有助于解码器将注意力集中在输入序列中的适当位置:

以下步骤重复此过程,直到达到表示Transformer解码器完成输出的特殊符号出现。每一步的输出在下一个时间步又被送入底部解码器,就像我们对编码器输入所做的那样,我们将位置编码嵌入并添加到这些解码器输入中,以指示每个字的位置。

解码器中的自注意力层的工作方式与编码器中的稍有不同:

在解码器中自注意力层仅允许观察输出序列的早些位置, 这是通过在softmax之前掩盖未来位置来实现的(设置成=inf)。

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

最后一层: Linear and Softmax layer

解码器输出一个浮点型的向量,我们该怎么把它转换成一个单词?这就是最后一层网络的工作啦!

线性层就是全连接神经网络将解码器产生的向量投射到一个更大的向量(称为逻辑向量)中的网络。

假设模型的从训练数据中学习到的输出词汇一共有10,000个,那么就使逻辑向量有10,000维,每一个维度都代表了一个单词的分数。

然后softmax层将分数转换成概率(都是正数且和为1),拥有最大值的那个维度被选中将这个维度对应的单词输出。

训练要点

现在我们已经学习了整个Transformer的前向传播过程,这对我们学习训练过程也相当有帮助。

在训练时模型遵循同样的前向传播,我们将它的输出与正确输出作对比。

为了可视化的展示,让我们假设我们的输出词汇仅包含六个单词(“a”, “am”, “i”, “thanks”, “student”, and “” ( ‘end of sentence’的缩写))。

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

下面,让我们来讨论一下模型的损失函数——我在训阶段优化的度量标准,以得到一个经过训练的非常精确的模型。

损失函数

假设我们正在训练我们的模型。假设这是我们在训练阶段的第一步,我们正在用一个简单的例子来训练它——将“meric”翻译成“thanks”。

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

两个概率分布如何比较?我们把两者相减就可以了,具体的细节cross-entropyKullback–Leibler divergence这两篇文章查看。

但是注意我们这里用的是一个过度简单的例子,说的更具体些我们需要输出一个句子而不是一个单词,所以我们需要模型依次输出概率分布:

  • 每一个概率分布的宽度都是vocab_size(在我们的例子中是6,真实情况可能是10,000或3,000)
  • 第一个概率分布最大值与单词”i”对应
  • 第二个概率分布最大值与单词”am”对应
  • 以此类推最后一个概率分布最大值与单词”“对应

在足够大的数据集上训练之后我们希望我们的模型可以达到下图的效果:

现在,因为模型一次生成一个输出,所以我们可以假设模型从概率分布中选择概率最高的单词,然后丢弃其余的单词。这是一种方法(称为贪婪解码)。另一种方法是抓住前两个词(例如“i”和“me”),然后在下一步中运行模型两次:一次假设第一个输出位置是“i”,另一次假设第一个输出位置是“me”,考虑位置1和2产生的误差较小的留下。我们对位置2和3…等重复此操作。此方法称为“波束搜索”(beam search),在我们的示例中,波束大小为2(因为我们在计算位置1和2的波束后比较了结果),并且顶波束也是2(因为我们保留了两个单词)。这两个都是你可以实验的超参数。

生活不易,求打赏~