【Transformer】图解 Transformer

原题:The Illustrated Transformer
原文:HTML
作者:Jay Alammar



前一篇文章中,我们研究了注意力——现代深度学习模型中一种普遍存在的方法。注意力是一个帮助提高神经机器翻译应用程序性能的概念。在这篇文章中,我们将看看Transformer——一个利用注意力来提高这些模型训练速度的模型。Transformer 在特定任务中的表现优于谷歌神经机器翻译模型。然而,最大的好处来自于Transformer如何进行并行化。事实上,谷歌云推荐使用Transformer作为参考模型来使用他们的Cloud TPU产品。所以让我们试着把这个模型分解开来,看看它是如何工作的。

Transformer 是在论文 Attention is All You Need 中提出的。它的张量流实现是Tensor2Tensor 的一部分。哈佛的NLP小组用PyTorch实现创建了一个注释论文的指南。在这篇文章中,我们将试图把事情简单,并逐一地介绍相关概念,希望能让没有深入了解相关知识的人更容易理解。


A High-Level Look

让我们从将模型视为单个黑盒开始。在机器翻译应用程序中,它会使用一种语言的句子,并输出另一种语言的翻译。

【Transformer】图解 Transformer
弹出Optimus Prime优势,我们看到了编码组件,解码组件以及它们之间的连接。

【Transformer】图解 Transformer
编码组件是一堆编码器(论文中将6个编码器堆叠在一起,6并非固定的,可以尝试其它数量)。解码组件是相同数量的解码器的堆栈。

【Transformer】图解 Transformer
编码器的结构都是相同的,但它们不共享权重。每一层都分为两个子层:
【Transformer】图解 Transformer
编码器的输入首先流经一个自我关注层(self-attention layer)——该层帮助编码器在编码特定单词时查看输入句子中的其他单词。我们将在后面的文章中详细介绍。

自关注层的输出被输入到前馈神经网络。完全相同的前馈网络独立地应用于每个位置。

解码器具有这两个层,但是在它们之间有一个注意力层,帮助解码器聚焦于输入句子的相关部分(类似于seq2seq模型中的注意力)

【Transformer】图解 Transformer

Bringing The Tensors Into The Picture

现在我们已经看到了模型的主要组件,让我们开始看看各种向量/张量,以及它们如何在这些组件之间流动,以将训练好的模型的输入转化为输出。

正如在一般的自然语言处理应用中的情况一样,我们首先使用嵌入算法(embedding algorithm)将每个输入单词转换成一个向量。
【Transformer】图解 Transformer
Each word is embedded into a vector of size 512. We’ll represent those vectors with these simple boxes.

嵌入(embedding)只应用在最底层的编码器中。所有编码器共有的抽象(abstraction)是,它们接收每个大小为512的向量列表(list)——在底部编码器中的输入是词嵌入(word embeddings),但在其他编码器的输入中,是前一个编码器输出。这个列表的大小是我们可以设置的超参数——基本上是训练数据集中最长句子的长度。

在将单词嵌入到我们的输入序列中之后,每个单词都流经编码器的两层。

在这里,我们开始看到Transformer的一个关键属性,即每个位置的单词都流经编码器中自己的路径。自我注意层中的这些路径之间存在依赖性。但是,前馈层不具有这些依赖性,因此可以在流过前馈层的同时并行执行各种路径。

接下来,我们将示例切换到较短的句子,然后看一下编码器每个子层中发生的情况。

Now We’re Encoding!

正如我们已经提到的,编码器接收向量列表作为输入。它通过将这些向量传递到自我注意层(self-attention layer),然后传递到前馈神经网络,然后将输出到下一个编码器来处理此列表。
【Transformer】图解 Transformer
The word at each position passes through a self-attention process. Then, they each pass through a feed-forward neural network – the exact same network with each vector flowing through it separately.

Self-Attention at a High Level

说下面的句子是我们要翻译的输入句子:

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

这句话中的 “it” 指的是什么?是指街道还是动物?对人类来说,这是一个简单的问题,但对算法而言却不那么简单。

当模型处理 “it” 一词时,自我关注使它可以将 “it”“animal” 相关联。

在模型处理每个单词(输入序列中的每个位置)时,自我关注使其能够查看输入序列中的其他位置以寻找线索,从而有助于更好地对该单词进行编码。

如果你熟悉RNN,考虑一下如何通过保持隐藏状态(hidden state)来使RNN将其已处理的先前单词/向量的表示形式与当前正在处理的单词/向量进行合并(incorporate)。自我注意是Transformer用来将其他相关单词的 “understanding” 融入当前正在处理的单词的方法。
【Transformer】图解 Transformer
As we are encoding the word “it” in encoder #5 (the top encoder in the stack), part of the attention mechanism was focusing on “The Animal”, and baked a part of its representation into the encoding of “it”.

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

Self-Attention in Detail

首先,让我们看一下如何使用向量计算自我注意力,然后再着眼于如何实际实现-使用矩阵。

计算自我注意(self-attention)的第一步(first step)是从每个编码器的输入向量中创建三个向量(在这种情况下,是每个单词的嵌入(embedding))。因此,对于每个单词,我们创建一个查询向量(Query vector)、一个关键字向量(Key vector)和一个值向量(Value vector)。这些向量是通过将嵌入乘以在训练过程中训练的三个矩阵而创建的。

请注意,这些新向量的维数小于嵌入向量的维数。它们的维数是64,而嵌入和编码器输入/输出向量的维数是512。它们不必更小,这是一个架构选择,以使多头注意力(multiheaded attention)的计算大部分恒定。

【Transformer】图解 Transformer
X 1 X_1 X1 乘以 W Q W^Q WQ 权重矩阵产生 q 1 q_1 q1,即与该单词相关联的 “query” vector。我们最终创建了输入句子中每个单词的 “query”,“key”,“value” projection。

What are the “query”, “key”, and “value” vectors?

它们是对计算和思考注意力有用的抽象概念。一旦你开始阅读下面的注意力是如何计算的,你就会知道这些向量中的每一个所扮演的角色。

计算自我关注(self-attention)的第二步是计算分数。假设我们在计算这个例子中第一个词 “Thinking” 的自我关注度。我们需要对照这个单词给输入句子的每个单词打分。分数决定了当我们在某个位置编码一个单词时,应该把多少注意力放在输入句子的其他部分。

分数是通过将查询向量(query vector)与我们正在评分的相应单词的关键向量( key vector)的点积来计算的。因此,如果我们在位置#1处理单词的自我注意,第一个分数将是q1和k1的点积。第二个分数是q1和k2的点积。

【Transformer】图解 Transformer
第三步和第四步是将分数除以8(论文中使用的关键向量(key vectors)维数的平方根——64)。这导致更稳定的梯度。这里可能有其他可能的值,但这是默认值,然后通过softmax操作传递结果。Softmax将分数归一化,使它们都为正,加起来为1。

【Transformer】图解 Transformer
这个softmax分数决定了每个单词在这个位置上的表达量(expressed)。显然,这个位置的单词将具有最高的softmax分数,但有时关注与当前单词相关的另一个单词是有用的。

第五步是将每个值向量(value vector)乘以softmax分数,为求和做准备。这里的直觉是保持我们想要关注的单词(words)的值不变,并弱化(drown-out)不相关的单词(例如,将它们乘以像0.001这样的小数字)。

第六步是对加权值向量求和。这将在这个位置产生自我关注层(self-attention layer)的输出(对于第一个单词)。

【Transformer】图解 Transformer
自我注意计算到此结束。得到的向量是我们可以发送到前馈神经网络的向量。然而,在实际实现中,这种计算是以矩阵形式进行的,以便更快地处理。现在让我们来看一下,我们已经在单词层面上看到了计算的直觉。

Matrix Calculation of Self-Attention

第一步是计算 Query, Key, Value 矩阵。为此,我们将嵌入内容打包到矩阵 X X X 中,然后将其乘以训练过的权重矩阵 ( W Q , W K , W V ) (W^Q,W^K,W^V) (WQWKWV)

【Transformer】图解 Transformer
Every row in the X matrix corresponds to a word in the input sentence. We again see the difference in size of the embedding vector (512, or 4 boxes in the figure), and the q/k/v vectors (64, or 3 boxes in the figure)

最后,由于我们要处理矩阵,因此我们可以将步骤2至6压缩为一个公式,以计算自我注意层的输出。

【Transformer】图解 Transformer
The self-attention calculation in matrix form

The Beast With Many Heads

论文中通过添加一种称为“multi-headed”注意力的机制,进一步完善了自我注意力层。这样可以通过两种方式提高关注层(attention layer)的性能:

它扩展了模型专注于不同位置的能力。是的,在上面的示例中, z 1 z_1 z1 包含所有其他编码的一点点,但是它可能由实际单词本身决定。如果我们要翻译这样的句子 “The animal didn’t cross the street because it was too tired”,那么我们会想知道 “it” 指的是什么。

【Transformer】图解 Transformer
With multi-headed attention, we maintain separate Q/K/V weight matrices for each head resulting in different Q/K/V matrices. As we did before, we multiply X by the WQ/WK/WV matrices to produce Q/K/V matrices.

如果执行上述的相同的自注意力计算,则仅在8个不同的时间使用不同的权重矩阵,最终将得到8个不同的 Z Z Z 矩阵。

【Transformer】图解 Transformer
这给我们带来了一些挑战。前馈层不希望有8个矩阵,而是需要单个矩阵(每个单词一个向量)。因此,我们需要一种将这8个压缩为单个矩阵的方法。

该怎么做?合并(concat)矩阵,然后将它们乘以权重矩阵 W O W^O WO

这就是多头自我注意力(multi-headed self-attention)。这包括很多矩阵,将它们全部放在一个图中,以便对比查看它们。

【Transformer】图解 Transformer
现在,我们已经涉及到 attention heads,让我们从前面重新回顾一下示例,看看在示例句中对“it”一词进行编码时,不同的 attention heads 所关注的位置:

【Transformer】图解 Transformer
As we encode the word “it”, one attention head is focusing most on “the animal”, while another is focusing on “tired” – in a sense, the model’s representation of the word “it” bakes in some of the representation of both “animal” and “tired”.

但是,如果将所有 attention head 添加到图片中,则可能难以解释:
【Transformer】图解 Transformer

Representing The Order of The Sequence Using Positional Encoding

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

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

【Transformer】图解 Transformer
To give the model a sense of the order of the words, we add positional encoding vectors – the values of which follow a specific pattern.

如果我们假设嵌入的维数为4,则实际的位置编码应如下所示:
【Transformer】图解 Transformer
A real example of positional encoding with a toy embedding size of 4

这种模式是什么样的?

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

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

本文(第3.5节)描述了位置编码的公式。你可以在get_timing_signal_1d()中看到生成位置编码的代码。这不是唯一可能的位置编码方法。然而,它的优点是能够缩放到看不见的序列长度(例如,如果训练的模型被要求翻译比我们训练集中的任何一个句子都长的句子)。

July 2020 Update:上面显示的位置编码来自 Transformer 的 Tranformer2Transformer 实现。论文中显示的方法略有不同,它没有直接连接(concatenate),而是将两个信号交织( interweaves)在一起,下图显示了这种情况。 这里是生成它的代码。

【Transformer】图解 Transformer

The Residuals

需要注意编码器架构的一个细节,每个编码器中的每个子层(self-attention, ffnn)周围都有一个残差连接(residual connection),后面是 layer-normalization 步骤。

【Transformer】图解 Transformer
如果我们将向量和与自我关注相关的 layer-norm 操作可视化,它看起来像这样:
【Transformer】图解 Transformer
解码器的子层也是如此。如果我们考虑一个由两个堆叠的编码器和解码器组成的 Transformer,它看起来像这样:

【Transformer】图解 Transformer

The Decoder Side

既然我们已经涵盖了编码器方面的大多数概念,我们基本上也知道解码器的组件是如何工作的。让我们来看看他们是如何合作的。

编码器从处理输入序列开始。顶部编码器的输出随后被转换成一组关注向量(attention vectors) K K K V V V。这些被每个解码器在其“编码器-解码器关注”层中使用,这有助于解码器关注输入序列中的适当位置:
【Transformer】图解 Transformer
完成编码阶段后,开始解码阶段。解码阶段的每一步都从输出序列中输出一个元素(在这种情况下是英语翻译句子)。

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

【Transformer】图解 Transformer
解码器中的自我关注层(self attention layers)的工作方式与编码器中的略有不同:

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

Encoder-Decoder Attention Layer 就像多头自我注意(multiheaded self-attention)一样工作,只是它从它下面的层创建它的查询矩阵(Queries matrix),并从编码器堆栈的输出中获取键矩阵( Keys matrix)和值矩阵(Values matrix)。

The Final Linear and Softmax Layer

解码器堆栈输出一个浮点向量,如何把它变成一个单词?这是最后的线性层和Softmax层的工作。

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

假设我们的模型知道从其训练数据集中学习的10,000个不同的英语单词(我们模型的“output vocabulary”)。这将使logits向量的宽度变为10,000个单元——每个单元对应一个单词得分。这就是解释模型的输出以及线性层的方式。

然后,softmax层将那些分数转换为概率(全部为正,全部相加为1.0)。选择具有最高概率的单元,并且与此时间相关的单词将作为该时间步的输出。

【Transformer】图解 Transformer
This figure starts from the bottom with the vector produced as the output of the decoder stack. It is then turned into an output word.

Recap Of Training

现在,我们已经通过训练有素的Transformer涵盖了整个前向传播过程,这对于了解训练模型的直觉很有用。

在训练过程中,未经训练的模型将经过完全相同的前向传播路径。但是由于是在有标记的训练数据集上进行训练,因此可以将其输出与实际的输出进行比较。

为了直观地说明这一点,我们假设输出词汇表仅包含六个单词:“a”,“am”,“i”,“thanks”,“student”,“<eos>”(“end of sentence”的缩写) 。

【Transformer】图解 Transformer
The output vocabulary of our model is created in the preprocessing phase before we even begin training.

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

【Transformer】图解 Transformer
回顾之后,让我们讨论模型的损失函数——训练阶段优化的指标,可以得出经过训练的,希望是惊人的准确模型。

The Loss Function

假设正在训练模型。这是训练阶段的第一步,我们将以一个简单的示例对其进行训练——将“merci”转换为“thanks”。

这意味着我们希望输出是一个表示单词“thanks”的概率分布。但是,由于尚未对该模型进行训练,因此目前不太可能发生。
【Transformer】图解 Transformer
由于模型的参数(权重)都是随机初始化的,因此(未经训练的)模型会针对每个单元格/单词生成具有任意值的概率分布。我们可以将其与实际输出进行比较,然后使用反向传播调整所有模型的权重,以使输出更接近所需的输出。

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

但是请注意,这是一个过于简化的示例。实际上,我们将使用一个单词多于一个单词的句子。例如,输入:“je suis étudiant”,预期输出:“i am a student”。这实际上意味着我们希望我们的模型连续输出概率分布,其中:

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

【Transformer】图解 Transformer
我们将在训练示例中针对一个样本句子针对目标概率分布进行训练。

在足够大的数据集上训练模型足够的时间后,我们希望产生的概率分布如下所示:
【Transformer】图解 Transformer
希望经过训练,该模型将输出我们期望的正确翻译。当然,这并不是该短语是否属于训练数据集的真正迹象(请参阅:交叉验证)。请注意,即使不太可能是该时间步长的输出,每个位置也会有概率得分——这是softmax的一个非常有用的属性,可以帮助训练过程。

现在,由于该模型一次生成一个输出,因此我们可以假设该模型正在从该概率分布中选择具有最高概率的单词,然后丢弃其余单词。这是做到这一点的一种方法(称为贪婪解码(greedy decoding))。做到这一点的另一种方法是,坚持前两个单词(例如,“I” 和 “a”),然后在下一步中运行模型两次:一次假设第一个输出位置为单词 “I”,另一个时间假设第一个输出位置是单词 “a”,考虑到位置#1和#2,保留产生的错误更少的情况。对位置#2和#3也重复这一过程。此方法称为波束搜索(beam search),在我们的示例中,beam_size=2(意味着始终保留两部分假设(未完成的翻译)),并且 top_beams=2(意味着将返回两个翻译)。这些都是超参数。


Go Forth And Transform

我希望你已经找到了一个有用的地方来开始打破与Transformer的主要概念的僵局。如果你想深入了解,我建议你采取以下步骤:

Follow-up works【Paper】: