本文翻译自Jay Alammar 的博客文章 The Illustrated Transformer , 旨在通过可视化方式深入浅出地解释 Transformer 模型的工作原理。该文章详细剖析了 Transformer 的编码器和解码器结构,以及自注意力机制的核心概念。 其中,自注意力机制允许模型在处理序列中的每个词时,关注序列中的其他词,从而更好地理解上下文关系。文章还介绍了多头注意力、位置编码、残差连接和层归一化等关键技术。 此外,还讨论了 Transformer 的训练过程,包括损失函数和解码方法。 最后,文章还推荐了一系列深入学习 Transformer 的资源和相关研究。
一、从整体来理解 Transformer
首先,我们先将模型视为一个黑盒子。在机器翻译应用中,它会接收一种语言的句子,然后输出另一种语言的翻译。
揭开这个Optimus Prime般的神奇构造,我们会看到一个编码组件、一个解码组件以及它们之间的连接。
编码组件是一堆编码器(这里以6个编码器举例,当然可以尝试其他排列方式)。解码组件是一堆相同数量的解码器。
编码器的结构完全相同(但它们不共享权重)。每个编码器分为两个子层:
编码器的输入首先流经自注意力层(self-attention) - 该层可帮助编码器在编码特定单词时查看输入句子中的其他单词。我们将在后面的文章中更深入地讨论自注意力。
自注意力层的输出被馈送到前馈神经网络(Feed Forward)。完全相同的前馈网络独立应用于每个位置。
解码器也具有这两个层,但它们之间有一个注意层,可帮助解码器关注输入句子的相关部分(类似于seq2seq 模型中的注意力)。
二、将张量引入到图中
现在我们已经了解了模型的主要组成部分,让我们开始看看各种向量/张量以及它们如何在这些组成部分之间流动,从而将训练模型的输入转化为输出。
与一般 NLP 应用的情况一样,我们首先使用嵌入算法将每个输入词转换为向量
嵌入仅发生在最底层的编码器中。所有编码器的共同抽象是它们接收一个向量列表,每个向量的大小为 512 – 在底层编码器中,这将是单词嵌入,但在其他编码器中,它将是直接位于下方的编码器的输出。此列表的大小是我们可以设置的超参数 – 基本上它将是我们训练数据集中最长句子的长度。
将单词嵌入到我们的输入序列之后,每个单词都会流经编码器的两层中的每一层。
这里我们开始看到 Transformer 的一个关键特性,即每个位置上的单词在编码器中流经自己的路径。在自注意力层中,这些路径之间存在依赖关系。然而,前馈层没有这些依赖关系,因此各种路径可以在流经前馈层时并行执行。
接下来,我们将示例换成更短的句子,并看看编码器的每个子层中发生的情况。
三、现在我们开始编码!
正如我们已经提到的,编码器接收向量列表作为输入。它通过将这些向量传递到“自我注意”层,然后传递到前馈神经网络来处理此列表,然后将输出向上发送到下一个编码器。
四、高级自我注意力
不要被我随意使用“自注意力”这个词所欺骗,好像这是一个每个人都应该熟悉的概念。我个人在阅读《注意力就是你所需要的一切》这篇论文之前从未接触过这个概念。让我们提炼一下它的工作原理。
假设以下句子是我们要翻译的输入句子:
“ The animal didn't cross the street because it was too tired”
这句话中的“它”指的是什么?它指的是街道还是动物?对于人类来说,这是一个简单的问题,但对于算法来说却不那么简单。
当模型处理it
这个词时,自我注意力让模型将it
与animal
联系起来。
当模型处理每个单词(输入序列中的每个位置)时,自我注意力机制允许它查看输入序列中的其他位置以寻找有助于更好地编码该单词的线索。
如果您熟悉 RNN,请考虑如何通过保持隐藏状态让 RNN 将其已处理的先前单词/向量的表示与当前正在处理的单词/向量结合起来。自注意力是 Transformer 用于将对其他相关单词的“理解”融入我们当前正在处理的单词的方法。
当我们在编码器#5(堆栈中的顶部编码器)中对单词it
进行编码时,注意力机制的一部分集中在“The Animal”上,并将其部分表示融入到“it”的编码中。
请务必查看Tensor2Tensor 笔记本,您可以在其中加载 Transformer 模型,并使用此交互式可视化来检查它。
五、自注意力机制详解
我们先来看看如何用向量来计算自我注意力,然后再看看它是如何实际实现的——使用矩阵。
计算自注意力的第一步是从编码器的每个输入向量(在本例中为每个单词的嵌入)创建三个向量。因此,对于每个单词,我们创建Query 向量,Key 向量,Value 向量。这些向量是通过将嵌入乘以我们在训练过程中训练的三个矩阵而创建的。
请注意,这些新向量的维度小于嵌入向量。它们的维度为 64,而嵌入和编码器输入/输出向量的维度为 512。它们不一定比嵌入向量小,这是一种架构选择,可使多头注意力的计算(大部分)保持恒定。
六、什么是Query 向量,Key 向量,Value 向量?
它们是计算和思考注意力的抽象概念。一旦你继续阅读下面如何计算注意力,你就会知道这些向量所起的作用。
计算自注意力的第二步是计算分数。假设我们正在计算本例中第一个单词“Thinking”的自注意力。我们需要根据这个词对输入句子的每个单词进行评分。分数决定了我们在某个位置编码单词时对输入句子其他部分的关注程度。
得分是通过对查询向量与我们要评分的相应单词的键向量进行点积计算得出的。举例来说,如果我们要处理1号位置中的单词的自注意力,则第一个得分将是q1和k1的点积。第二个得分将是q1和k2的点积。
第三步和第四步是将分数除以 8(论文中使用的关键向量维度的平方根 - 64。这会导致更稳定的梯度。这里可能还有其他可能的值,但这是默认值),然后将结果传递给 softmax 运算。Softmax 对分数进行归一化,使它们都为正数并且加起来为 1。
这个 softmax 分数决定了每个单词在这个位置的表达程度。显然,这个位置的单词将具有最高的 softmax 分数,但有时关注与当前单词相关的另一个单词也很有用。
第五步是将每个值向量乘以 softmax 分数(准备将它们相加)。这里的直觉是保持我们想要关注的单词的值不变,并淹没不相关的单词(例如,通过将它们乘以 0.001 这样的小数字)。
第六步是将加权值向量相加。这将产生该位置(第一个单词)的自注意力层的输出。
这就是自注意力计算的结论。我们可以将得到的向量发送到前馈神经网络。然而,在实际实现中,为了加快处理速度,这个计算是以矩阵形式进行的。既然我们已经了解了单词级计算的直观原理,那么现在让我们来看一下。
七、Self-Attention的矩阵计算
第一步是计算查询、键和值矩阵。我们将嵌入打包到矩阵X中,并将其乘以我们训练过的权重矩阵(WQ、WK、WV)。
最后,由于我们处理的是矩阵,我们可以将第二步到第六步浓缩为一个公式来计算自注意力层的输出。
八、多头注意力机制(multi-head attention)
论文进一步完善了自注意力层,增加了一种称为“多头”注意力的机制。这从两个方面提高了注意力层的性能:
-
1.它扩展了模型关注不同位置的能力。是的,在上面的例子中,z1 包含了其他所有编码的一小部分,但它可能由实际单词本身主导。如果我们要翻译
The animal didn’t cross the street because it was too tired
这样的句子,那么知道it
指的是哪个词会很有用。 -
2.它为注意层提供了多个“表示子空间”。正如我们接下来将看到的,使用多头注意,我们不仅有一组,而且有多组查询/键/值权重矩阵(Transformer 使用八个注意头,因此我们最终为每个编码器/解码器设置了八组)。这些集合中的每一个都是随机初始化的。然后,在训练之后,每个集合用于将输入嵌入(或来自较低编码器/解码器的向量)投影到不同的表示子空间中。
如果我们进行上述相同的自注意力计算,只需使用不同的权重矩阵进行 8 次不同的计算,我们最终会得到 8 个不同的 Z 矩阵
这给我们带来了一些挑战。前馈层并不期望八个矩阵——它期望一个矩阵(每个单词一个向量)。所以我们需要一种方法将这八个矩阵压缩成一个矩阵。
我们该怎么做呢?我们将矩阵连接起来,然后将它们乘以附加权重矩阵 WO。
这就是多头自注意力的全部内容。我意识到,这有相当多的矩阵。让我试着把它们都放在一个视觉图中,这样我们就可以在一个地方看到它们
既然我们已经接触到了注意力头,让我们重新回顾之前的例子,看看当我们在例句中编码单词it
时,不同的注意力头关注的焦点在哪里:
当我们对it
这个词进行编码时,一个注意力头主要关注the animal
,而另一个注意力头则关注tired
——从某种意义上说,模型对it
这个词的表示融合了animal
和tired
的一些表示。
然而,如果我们把所有的注意力头都添加到图片中,事情就会变得更加难以解释:
九、使用位置编码表示序列的顺序
到目前为止,我们所描述的模型缺少一件事,那就是解释输入序列中单词顺序的方法。
为了解决这个问题,Transformer 模型对每个输入的向量都添加了一个向量。这些向量遵循模型学习到的特定模式,有助于确定每个单词的位置,或者句子中不同单词之间的距离。这种做法背后的直觉是:将这些表示位置的向量添加到词向量中,得到了新的向量,这些新向量映射到 Q/K/V,然后计算点积得到 attention 时,可以提供有意义的信息。
如果我们假设嵌入的维数为 4,则实际的位置编码将如下所示:
这种模式可能是什么样的?
在下图中,每一行对应一个向量的位置编码。因此,第一行将是我们要添加到输入序列中第一个单词的嵌入的向量。每行包含 512 个值 - 每个值介于 1 和 -1 之间。我们对它们进行了颜色编码,以便模式清晰可见。
2020 年 7 月更新: 上面显示的位置编码来自 Transformer 的 Tensor2Tensor 实现。论文中展示的方法略有不同,它不直接连接,而是交织两个信号。下图显示了它的样子。以下是生成它的代码:
十、残差
在我们继续讲解之前,编码器结构中有一个需要注意的细节是:编码器的每个子层(Self Attention 层和 FFNN)都有一个残差连接和层标准化(layer-normalization)。
如果我们将与自我注意力相关的向量和层规范操作可视化,它将看起来像这样:
这也适用于解码器的子层。如果我们考虑一个由 2 个堆叠的编码器和解码器组成的 Transformer,它看起来会像这样:
十一、解码器端
现在我们已经介绍了编码器方面的大部分概念,我们基本上也知道了解码器组件的工作原理。但让我们看看它们是如何协同工作的。
编码器首先处理输入序列。然后,顶部编码器的输出被转换为一组注意向量 K 和 V。每个解码器将在其“编码器-解码器注意”层中使用它们,这有助于解码器将注意力集中在输入序列中的适当位置:
编码阶段结束后,我们开始解码阶段。解码阶段的每一步都会从输出序列中输出一个元素(在本例中是英语翻译句子)。 以下步骤重复该过程,直到出现特殊符号到达时,表示 Transformer 解码器已完成输出。每个步骤的输出都会在下一个时间步骤中馈送到底部解码器,解码器会像编码器一样将其解码结果冒泡。就像我们对编码器输入所做的那样,我们嵌入并添加位置编码到这些解码器输入中,以指示每个单词的位置。
解码器中的自注意力层的操作方式与编码器中的自注意力层略有不同:
在解码器中,自注意力层仅允许关注输出序列中的较早位置。这是通过-inf在自注意力计算中的 softmax 步骤之前屏蔽未来位置(将其设置为)来实现的。
“编码器-解码器注意”层的工作原理与多头自注意类似,不同之处在于它从其下方的层创建查询矩阵,并从编码器堆栈的输出中获取键和值矩阵。
十二、最后的线性层和 Softmax 层
解码器堆栈输出一个浮点向量。我们如何将其转换成一个单词?这是最后一个线性层的工作,后面是 Softmax 层。
线性层是一个简单的完全连接的神经网络,它将解码器堆栈产生的向量投影到一个更大的向量中,称为 logits 向量。
假设我们的模型知道从训练数据集中学习到的 10,000 个独特的英语单词(我们模型的“输出词汇表”)。这将使 logits 向量宽度达到 10,000 个单元格 - 每个单元格对应一个独特单词的分数。这就是我们解释线性层之后的模型输出的方式。
然后,softmax 层将这些分数转换为概率(所有分数均为正数,总和为 1.0)。选择概率最高的单元格,并生成与其关联的单词作为此时间步骤的输出。
十三、训练回顾
现在我们已经通过训练好的 Transformer 介绍了整个前向传递过程,了解一下训练模型的直觉会很有用。
在训练过程中,未经训练的模型会经历完全相同的前向传递。但由于我们是在标记的训练数据集上进行训练,因此我们可以将其输出与实际正确的输出进行比较。
为了形象化地说明这一点,我们假设我们的输出词汇只包含六个单词(a
、am
、i
、thanks
、student
和<eos>
(end of sentence
的缩写))。
我们的模型的输出词汇是在我们开始训练之前的预处理阶段创建的。
一旦我们定义了输出词汇表,我们就可以使用相同宽度的向量来表示词汇表中的每个单词。这也称为独热编码。例如,我们可以使用以下向量表示单词am
:
在回顾之后,让我们讨论一下模型的损失函数——我们在训练阶段优化的指标,以得到一个训练有素且希望非常准确的模型。
十四、损失函数
假设我们正在训练模型。假设这是我们训练阶段的第一步,我们正在用一个简单的例子来训练它——将merci
翻译成thanks
。
这意味着,我们希望输出是一个概率分布,表示单词“谢谢”。但由于这个模型尚未训练,所以目前不太可能实现。
由于模型的参数(权重)都是随机初始化的,因此(未经训练的)模型会为每个单元/单词生成一个具有任意值的概率分布。我们可以将其与实际输出进行比较,然后使用反向传播调整所有模型的权重,使输出更接近期望输出。
如何比较两个概率分布?我们只需将一个从另一个中减去即可。有关更多详细信息,请查看 交叉熵和Kullback-Leibler散度 。
但请注意,这是一个过于简单的例子。更现实的是,我们将使用一个长度超过一个单词的句子。例如 - 输入:“je suis étudiant”和预期输出:“i am a student”。这实际上意味着,我们希望我们的模型连续输出概率分布,其中:
- 每个概率分布由宽度为 vocab_size 的向量表示(在我们的玩具示例中为 6,但更现实的数字是 30,000 或 50,000)
- 第一个概率分布在与单词“i”相关的单元格中具有最高概率
- 第二个概率分布在与单词“am”相关的单元格处具有最高概率
- 依此类推,直到第五个输出分布指示“
”符号,该符号在 10,000 个元素词汇表中也有一个与之关联的单元格。
我们将在一个示例句子的训练示例中针对目标概率分布来训练我们的模型。
在足够大的数据集上对模型进行足够时间的训练后,我们希望产生的概率分布如下所示:
现在,由于模型每次只产生一个输出,我们可以假设模型从概率分布中选择概率最高的单词,并丢弃其余单词。这是一种方法(称为贪婪解码greedy decoding)。 另一种方法是保留前两个单词(例如“I”和“a”),然后在下一步中运行模型两次:一次假设第一个输出位置是单词“I”,另一次假设第一个输出位置是单词“a”,并且保留考虑位置 #1 和 #2 时产生较少错误的版本。我们对位置 #2 和 #3 重复此操作……等等。这种方法称为“集束搜索”,在我们的示例中,beam_size 为 2(意味着在任何时候,内存中都保留两个部分假设(未完成的翻译)),top_beams 也是 2(意味着我们将返回两个翻译)。这些都是您可以尝试的超参数。
我希望您发现这是一个有用的起点,可以开始了解 Transformer 的主要概念。如果您想深入了解,我建议您采取以下步骤:
- 阅读《注意力就是你所需要的一切》论文、Transformer 博客文章(Transformer:一种用于语言理解的新型神经网络架构)和Tensor2Tensor 公告。
- 观看Łukasz Kaiser 的演讲,详细了解该模型及其细节
- 使用 Tensor2Tensor repo 提供的 Jupyter Notebook进行操作
- 探索Tensor2Tensor repo。
关注我获取更多资讯

