[翻译]图解Transformer
Posted Alex_996
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[翻译]图解Transformer相关的知识,希望对你有一定的参考价值。
在上一篇文章中,我们了解了注意力机制——这是现代深度学习模型中普遍使用的方法,是一个有助于提高神经网络机器翻译性能的概念。在这篇文章中,我们将研究Transformer——一个使用注意力机制提高训练速度的模型。Transformer 在特定任务中的表现优于谷歌神经网络机器翻译模型,然而最大的好处在于 Transformer 是如何实现并行的。事实上,Google Cloud 建议使用 Transformer 作为参考模型来使用他们的 TPU 产品,所以让我们试着把模型拆开,看看它是如何运作的。
Transformer 是在论文 Attention is All You Need 中提出的,它的 TensorFlow 实现也作为 Tensor2Tensor 包的一部分提供,哈佛的 NLP 小组也创建了一个指南,用 PyTorch 实现对论文进行注释。在这篇文章中,我们将尝试将模型简化,并一一介绍这些概念,让还没有深入了解Transformer的人更容易理解。
概览
让我们首先将模型视为一个黑盒子,在机器翻译任务中,以一种语言输入一个句子,然后用另一种语言输出它的翻译。
打开这个黑盒子,我们会看到一个编码组件、一个解码组件以及它们之间的连接。
编码组件是一堆编码器(论文中将6个编码器堆叠在一起——数字 6 并没有什么神奇之处,当然可以尝试其他排列方式),解码组件是一堆相同数量的解码器。
编码器在结构上都是相同的(但它们不共享权重),每一个都分为两个子层:
编码器的输入首先经过自注意力层——该层在对特定单词进行编码时可以查看输入句子中的其它单词。我们将在后面的文章中仔细研究自注意力层。
自注意力层的输出被发送到前馈神经网络,对于输入句子中的每个位置独立应用完全相同的前馈网络。
解码器也具有这两个层,但在它们之间是一个注意力层,帮助解码器也可以关注输入句子的相关部分(类似于 seq2seq 模型中的注意力)。
引入张量演示
现在我们已经了解了模型的主要组件,让我们开始研究各种向量/张量以及它们如何在这些组件之间流动以训练模型的输入转化为输出。
与 NLP 应用程序中的一般情况一样,我们首先使用嵌入算法将每个输入单词转换为向量。
每个单词都转化为一个大小为 512 的向量中,我们在这用简单的框来表示这些向量。
词向量转换仅发生在最开始的编码器中,所有编码器共有的输入是它们接收一个大小为 512 的向量列表——在底部的编码器中是词嵌入,但在其它编码器中是下方的编码器输出。这个列表的大小是我们可以设置的超参数——基本上是我们训练数据集中最长句子的长度。
在这里,我们开始看到 Transformer 的一个关键属性,那就是每个位置的单词在编码器中都有自己的路径,在自注意力层中,这些路径之间存在依赖关系,然而前馈层没有这些依赖关系,因此各种路径可以在流过前馈层的同时并行执行。
接下来,我们将把这个例子转换成一个更短的句子,我们将看看编码器的每个子层发生了什么。
编码器
正如我们已经提到的,编码器接收向量列表作为输入,将这些向量传递给自注意层来处理这,然后传递给前馈神经网络,最后将输出向上发送到下一个编码器。
每个位置的单词都会经过一个自注意力过程,然后它们每个都通过一个完全相同的前馈神经网络。
自注意力层
不用纠结于自注意力层到底是什么,在阅读论文 Attention is All You Need 之前,我也从没接触过这个概念,所以接下来就让我们展示一下它是如何工作的。
假设以下句子是我们要翻译的输入句子:The animal didn't cross the street because it was too tired.
这句话中的“it”指的是什么?是街道还是动物?这对人类来说是一个简单的问题,但对算法来说却不是那么简单。当模型处理“it”这个词时,自注意力机制就会把“it”和“animal”联系起来。
当模型处理每个单词(输入序列中的每个位置)时,自注意力允许它查看输入序列中其它位置的单词,这样可以更好地编码该单词的含义。
如果您熟悉 RNN,类似于 RNN 中保存隐藏状态,将其已处理的先前单词/向量的表示与当前正在处理的单词/向量结合起来。自注意力机制就是 Transformer 用来将其它相关单词的“理解”融入当前正在处理的单词的方法。
在第5个编码器(堆栈中的顶部编码器)中对单词“it”进行编码时,部分注意力机制专注于“The Animal”,并将其表示的一部分提供到“it”的编码中。
让我们先看看如何使用向量计算自注意力,然后继续看看它是如何使用矩阵实现的。
自注意力分数的计算
- 向量形式
计算自注意力的第一步是从每个编码器的输入向量中创建三个向量(在这种情况下,可以直接使用词向量)。因此,对于每个单词,我们创建一个Query向量、一个Key向量和一个Value向量,这些向量是通过将词向量乘以我们在训练过程中训练的三个矩阵来创建的。
为什么需要三个向量?
将 x1 乘以 WQ 权重矩阵产生 q1,即与该词相关的Query向量。最终为输入句子中的每个单词创建了Query、Key和Value三个向量。
什么是Query、Key和Value向量?
它们是用于计算和思考注意力的抽象概念,一旦你继续阅读下面的注意力是如何计算的,你就会知道每个向量的作用。
自注意力的第二步是计算一个分数,假设我们正在计算本例中第一个单词“Thinking”的自注意力分数,就需要根据这个词对输入句子的每个词进行评分。当我们在某个位置对单词进行编码时,分数决定了对输入句子其它部分的关注程度。
分数是通过Query向量与我们正在打分的相应单词的Key向量的点积来计算的。因此,如果我们正在处理位置 #1 中单词的自注意力,第一个分数将是 q1 和 k1 的点积,第二个分数是 q1 和 k2 的点积。
第三步和第四步是将分数除以 8(论文中使用的是Key向量的维度的平方根,这样具有更稳定的梯度,这里可能还有其他可能的值,但默认值是8),然后通过 softmax 操作传递结果,Softmax 将分数归一化,让它们都是正数并且加起来为 1。
为什么要除以8?
怎么就让梯度更加稳定了?
这个 softmax 分数决定了每个单词在这个位置上的表达量,显然,这个位置原本对应的单词将具有最高的 softmax 分数,但有时关注与当前单词相关的另一个单词也很有用。
第五步是将每个Value三个向量乘以 softmax 分数(准备将它们相加)。这个操作会保持我们想要关注的单词的值不变,并降低不相关单词的权重(例如,将它们乘以 0.001 之类的小数字)。
第六步是对加权值向量求和,这会在这个位置产生自注意力层的输出(对于第一个词)。
自注意力计算到此结束,结果向量就是我们可以发送到前馈神经网络的向量。
- 矩阵形式
在实际实现中,这种计算是以矩阵形式进行的,以便更快地处理。既然我们已经看到了单词级别的计算,那么让我们再来看看矩阵形式。
第一步是计算Query、Key和Value三个矩阵。通过将Embedding打包到矩阵 X 中,并将其乘以我们训练的权重矩阵(WQ、WK、WV)来做到这一点。
X 矩阵中的每一行对应于输入句子中的一个单词。我们再次看到词向量(512,或图中 4 个框)和 q/k/v 向量(64,或图中 3 个框)大小的差异。
最后,由于我们处理的是矩阵,我们可以将步骤 2 到 6 合并到一个公式中来计算自注意力层的输出。
矩阵形式的自注意力分数计算
多头自注意力机制
论文通过添加一种称为“多头”注意力的机制进一步完善了自注意力层,它通过两种方式提高了自注意力层的性能:
- 它扩展了模型关注不同位置的能力。在上面的示例中,z1 包含一点其它编码,但它可能由实际单词本身主导。如果我们翻译
The animal didn’t cross the street because it was too tired
这样的句子会很有用,因为我们会想知道“它”指的是哪个词。 - 它为注意力层提供了多个“表示子空间”。正如我们接下来将看到的,通过多头注意力,有多组Query/Key/Value权重矩阵(Transformer 使用八个注意力头,因此每个编码器/解码器最终有八个集合) 。这些集合中的每一个都是随机初始化的,训练之后,每个集合用于将输入词向量(或来自较低编码器/解码器的向量)投影到不同的表示子空间中。
通过多头注意力,我们为每个头维护单独的 Q/K/V 权重矩阵,从而产生不同的 Q/K/V 矩阵。正如我们之前所做的那样,我们将 X 乘以 WQ/WK/WV 矩阵以产生 Q/K/V 矩阵。
如果我们进行与上述相同的自注意力计算,只是使用不同的权重矩阵进行八次不同的计算,我们最终会得到八个不同的 Z 矩阵。
这给我们带来了一些挑战。前馈层不需要八个矩阵——它需要一个矩阵(每个单词的向量)。所以我们需要一种方法将这八个浓缩成一个矩阵。
以下就是多头自注意力的全部内容:
既然我们已经接触了注意力头,让我们重新回顾之前的例子,看看我们在例句中编码单词“it”时不同注意力头的焦点在哪里:
当我们对“it”这个词进行编码时,一个注意力头最关注“the animal”,而另一个注意力头则专注于“tired”——在某种意义上,模型对“it”这个词的表示包含了一些“the animal”和“tired”的表示。
但是,如果我们将所有注意力都添加到图片中,事情可能会更难解释:
多头不会过平滑么?
使用位置编码表示序列的顺序
到此为止,模型中还缺少一个至关重要的内容——输入序列中单词的顺序。
为了解决这个问题,Transformer为每个输入向量添加了一个位置向量。这些向量遵循模型学习的特定模式,有助于确定每个单词的位置,或序列中不同单词之间的距离。一旦将这些值投影到 Q/K/V 向量中以及在点积注意力期间,将这些值添加到Embedding中会在向量之间提供有意义的距离。
为了让模型了解单词的顺序,我们添加了位置编码向量——其值遵循特定的模式。
如果我们假设嵌入的维度为 4,那么实际的位置编码将如下所示:
这种模式可能是什么样子?
在下图中,每一行对应一个向量的位置编码。所以第一行将是我们添加到输入序列中第一个单词嵌入的向量。每行包含 512 个值——每个值都介于 1 和 -1 之间,对它们进行颜色编码后可见下图。
位置向量大小为 512(列)的 20 个单词(行)的位置编码的真实示例。您可以看到它在中间被分成两半,这是因为左半部分的值是由一个函数(使用正弦)生成的,而右半部分是由另一个函数(使用余弦)生成的,然后将它们连接起来形成每个位置编码向量。
为什么要用正弦函数和余弦函数编码?
论文中描述了位置编码的公式(第 3.5 节),您可以在 get_timing_signal_1d() 中查看生成位置编码的代码。这不是位置编码的唯一可能方法。然而,它具有能够扩展到看不见的序列长度的优势(例如,如果我们训练的模型被要求翻译比我们训练集中的任何一个句子更长的句子)。
上面显示的位置编码来自 Transformer 的 Tranformer2Transformer 实现。论文中展示的方法略有不同,它不是直接连接,而是将两个信号交织在一起。下图显示了它的图像。这是生成它的代码:
残差连接
在继续之前我们需要提到的编码器架构中的一个细节是,每个编码器中的每个子层在其周围都有一个残差连接,然后是一个层归一化步骤。
如果我们要可视化与 self attention 相关的向量和 layer-norm 操作,它看起来像这样:
这也适用于解码器的子层,如果我们想一个由 2 个堆叠编码器和解码器组成的 Transformer,它看起来像这样:
解码器
现在我们已经涵盖了编码器方面的大部分概念,我们基本上知道解码器的组件是如何工作的,但是让我们来看看它们是如何协同工作的。
编码器首先处理输入序列,然后将顶部编码器的输出转换为一组注意向量 K 和 V。
这些向量将在每个解码器“编码器-解码器注意力”层中使用,有助于解码器注意输入序列中的适当位置:
完成编码阶段后,开始解码阶段。解码阶段的每一步都从输出序列(本例中的英文翻译句子)中输出一个元素。
以下步骤重复该过程,直到出现一个特殊符号,表明转换器解码器已完成其输出。每一步的输出在下一个时间步被馈送到底部的解码器,解码器就像编码器一样冒泡它们的解码结果。就像我们对编码器输入所做的那样,我们将位置编码嵌入并添加到这些解码器输入中,以指示每个单词的位置。
解码器中的自注意力层的操作方式与编码器中的方式略有不同:
在解码器中,自注意力层只允许关注输出序列中较早的位置。这是通过在 self-attention 计算中的 softmax 步骤之前屏蔽未来位置(将它们设置为 -inf)来完成的。
编码器解码器注意力层的工作方式与多头自注意力相似,不同之处在于它从其下方的层创建其Query矩阵,并从编码器堆栈的输出中获取 Keys 和 Values 矩阵。
最终的 Linear 层和 Softmax 层
解码器堆栈输出一个浮点向量。我们如何把它变成一个词?这是最后一个线性层的工作,后面是一个 Softmax 层。
线性层是一个简单的全连接神经网络,它将解码器堆栈产生的向量投影到一个更大的向量中,称为 logits 向量。
假设我们的模型知道从训练数据集中学习到的 10,000 个独特的英语单词(我们模型的“输出词汇”)。这将使 logits 向量有 10,000 个单元格宽——每个单元格对应一个唯一单词的分数。这就是我们如何解释模型的输出,然后是线性层。
然后,softmax 层将这些分数转化为概率(全部为正,全部加起来为 1.0)。选择概率最高的单元格,并生成与其关联的单词作为该时间步的输出。
该图从底部开始,生成的向量作为解码器堆栈的输出。然后它变成一个输出字。
训练回顾
现在我们已经通过一个训练有素的 Transformer 介绍了整个前向传递过程,对于理解整个训练模型会很有用。
在训练期间,未经训练的模型将通过完全相同的前向传递,但是由于我们是在一个带标签的训练数据集上训练它,可以将它的输出与实际正确的输出进行比较。
我们模型的输出词汇表是在我们开始训练之前的预处理阶段创建的。
一旦我们定义了我们的输出词汇表,我们就可以使用一个相同宽度的向量来表示我们词汇表中的每个单词。这也称为 one-hot 编码。因此,例如,我们可以使用以下向量表示单词“am”:
在这个回顾之后,让我们讨论模型的损失函数——我们在训练阶段正在优化的指标,以生成一个经过训练的、希望非常准确的模型。
损失函数
假设这是我们在训练阶段的第一步,我们正在通过一个简单的例子来训练它——将“merci”翻译成“thanks”。
这意味着,我们希望输出是一个概率分布,表示“thanks”这个词。但由于这个模型还没有经过训练,所以这还不太可能发生。
由于模型的参数(权重)都是随机初始化的,因此(未经训练的)模型会为每个单元格/单词生成具有任意值的概率分布。我们可以将其与实际输出进行比较,然后使用反向传播调整所有模型的权重,以使输出更接近所需的输出。
你如何比较两个概率分布?我们只是从另一个中减去一个。有关更多详细信息,请查看交叉熵和 Kullback-Leibler 散度。
但请注意,这是一个过于简单的示例,更现实的是,我们会使用一个比一个词长的句子。例如——输入:“je suis étudiant”,预期输出:“i am a student”。这真正意味着,我们希望我们的模型能够连续输出概率分布,其中:
- 每个概率分布都由一个宽度为 vocab_size 的向量表示(在我们的玩具示例中为 6,但更实际的是一个数字,例如 30,000 或 50,000)
- 第一个概率分布在与单词“i”相关的单元格中具有最高概率
- 第二个概率分布在与单词“am”相关的单元格中具有最高概率
- 依此类推,直到第五个输出分布指示
<end of sentence>
符号,它也有一个来自 10,000 个元素词汇表的单元格与之关联。
我们将在训练示例中针对一个样本句子训练模型的目标概率分布。
在足够大的数据集上训练模型足够的时间后,我们希望生成的概率分布如下所示:
希望在训练后,模型会输出我们期望的正确翻译。当然,如果这个短语是训练数据集的一部分,这并没有真正的效果(参见:交叉验证)。请注意,每个位置都有一点概率,即使它不太可能是那个时间步的输出——这是 softmax 的一个非常有用的属性,有助于训练过程。
现在,因为模型一次产生一个输出,我们可以假设模型正在从该概率分布中选择具有最高概率的单词并丢弃其余的单词。这是一种方法(称为贪婪解码)。另一种方法是保留前两个单词(例如,“I”和“a”),然后在下一步中运行模型两次:假设第一个输出位置是单词’I’,另一次假设第一个输出位置是单词’a’,并且考虑到位置#1和#2,无论哪个版本产生的错误更少。我们对#2 和#3 等位置重复此操作。这种方法称为“beam search”,在我们的示例中,beam_size 为 2(意味着在任何时候,两个部分假设(未完成的翻译)都保存在内存中),top_beams 也是 2(意味着我们将返回两个翻译)。这些都是您可以试验的超参数。
总结与展望
我希望你觉得这是一篇有用的文章,可以开始用 Transformer 的主要概念打破僵局。如果您想更深入,我建议您执行以下步骤:
- 阅读 Attention Is All You Need 论文、Transformer 博客文章(Transformer: A Novel Neural Network Architecture for Language Understanding)和 Tensor2Tensor 文档。
- 观看 Łukasz Kaiser 的演讲,了解模型及其细节
- 使用 Tensor2Tensor 库提供的 Jupyter Notebook
- 探索 Tensor2Tensor 库
原文链接:The Illustrated Transformer
在下才疏学浅,翻译多有不周,还请各位读者多多指正。
以上是关于[翻译]图解Transformer的主要内容,如果未能解决你的问题,请参考以下文章