【NLP】图解变压器(transformer)

一、说明

        在这篇文章中,我们将看看 The Transformer——一个利用注意力来提高这些模型训练速度的模型。转换器在特定任务中优于谷歌神经机器翻译模型。然而,最大的好处来自变压器如何适应并行化。事实上,谷歌云建议使用The Transformer作为参考模型来使用他们的Cloud TPU产品。因此,让我们尝试将模型分解并查看其功能。

二、最外层的外观

        让我们首先将模型视为单个黑盒。在机器翻译应用程序中,它将采用一种语言的句子,然后输出另一种语言的翻译。

        打开引擎的顶盖,我们看到一个编码组件,一个解码组件,以及它们之间的联系。

        编码组件是一堆编码器(纸张将其中六个堆叠在一起 - 数字六没有什么神奇之处,人们绝对可以尝试其他安排)。解码组件是一堆相同编号的解码器。

        编码器在结构上都是相同的(但它们不共享权重)。每个子层都分为两个子层:

        编码器的输入首先流经自我注意层 - 该层可帮助编码器在编码特定单词时查看输入句子中的其他单词。我们将在帖子后面仔细研究自我关注。

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

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

三、将张量带入图片

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

与一般的NLP应用程序一样,我们首先使用嵌入算法将每个输入词转换为向量。


每个单词都嵌入到大小为 512 的向量中。我们将用这些简单的框来表示这些向量。

嵌入仅发生在最底部的编码器中。所有编码器的共同抽象是它们接收一个大小为 512 的向量列表 – 在底部编码器中,这将是单词嵌入,但在其他编码器中,它将是编码器的输出正下方。这个列表的大小是我们可以设置的超参数——基本上它是我们训练数据集中最长句子的长度。

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

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

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

四、现在我们正在编码!

        正如我们已经提到的,编码器接收一个向量列表作为输入。它通过将这些向量传递到“自我注意”层来处理此列表,然后传递到前馈神经网络,然后将输出向上发送到下一个编码器。


每个位置的单词都经过一个自我注意的过程。然后,它们各自通过一个前馈神经网络——完全相同的网络,每个向量分别流经它。

五、高水平的自我关注

        不要被我抛出“自我关注”这个词所迷惑,就像这是一个每个人都应该熟悉的概念一样。我个人从未遇到过这个概念,直到阅读了“注意力就是你需要的一切”论文。让我们提炼一下它是如何工作的。

        假设以下句子是我们想要翻译的输入句子:

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

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

        当模型处理单词“it”时,自我注意允许它将“it”与“animal”相关联。

        当模型处理每个单词(输入序列中的每个位置)时,自我注意允许它查看输入序列中的其他位置,以寻找有助于更好地编码该单词的线索。

        如果您熟悉 RNN,请考虑保持隐藏状态如何允许 RNN 将其先前处理的单词/向量的表示与当前正在处理的单词/向量合并。自我注意是变形金刚用来将对其他相关单词的“理解”烘焙到我们目前正在处理的单词中的方法。


当我们在编码器 #5(堆栈中的顶部编码器)中对单词“it”进行编码时,注意力机制的一部分专注于“动物”,并将其表示的一部分烘焙到“it”的编码中。

        请务必查看 Tensor2Tensor 笔记本,您可以在其中加载转换器模型,并使用此交互式可视化对其进行检查。

六、细节上的自我关注

        让我们首先看看如何使用向量计算自我注意,然后继续看看它实际上是如何使用矩阵实现的。

        计算自我注意的第一步是从编码器的每个输入向量创建三个向量(在本例中为每个单词的嵌入)。因此,对于每个单词,我们创建一个查询向量、一个键向量和一个值向量。这些向量是通过将嵌入乘以我们在训练过程中训练的三个矩阵来创建的。

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


将 x1 乘以 WQ 权重矩阵得到 q1,即与该单词关联的“查询”向量。我们最终为输入句子中的每个单词创建一个“查询”、“键”和“值”投影。


 

什么是“查询”、“键”和“值”向量?

它们是对计算和思考注意力有用的抽象概念。一旦你继续阅读下面的注意力是如何计算的,你就会知道几乎所有你需要知道的关于这些向量所扮演的角色。

计算自我注意力的第二步是计算分数。假设我们正在计算此示例中第一个单词“思考”的自我注意。我们需要根据该单词对输入句子的每个单词进行评分。分数决定了当我们在某个位置对单词进行编码时,对输入句子的其他部分的关注程度。

分数的计算方法是将查询向量的点积与我们评分的相应单词的关键向量相提并论。因此,如果我们处理位置 #1 中单词的自我注意,第一个分数将是 q1 和 k1 的点积。第二个分数是 q1 和 k2 的点积。

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

这个softmax分数决定了每个单词在这个位置的表达量。显然,这个位置的单词将具有最高的softmax分数,但有时关注与当前单词相关的另一个单词很有用。

第五步是将每个值向量乘以softmax分数(准备将它们相加)。这里的直觉是保持我们想要关注的单词的值不变,并淹没不相关的单词(例如,将它们乘以像 0.001 这样的小数字)。

第六步是总结加权值向量。这会在此位置(对于第一个单词)产生自我注意层的输出。

        自我注意力计算到此结束。生成的向量是我们可以发送到前馈神经网络的向量。然而,在实际实现中,这种计算是以矩阵形式完成的,以便更快地处理。因此,现在让我们看看,我们已经看到了单词级别的计算直觉。

七、自我注意的矩阵计算

        第一步是计算查询、键和值矩阵。为此,我们将嵌入打包到矩阵X中,并将其乘以我们训练的权重矩阵(WQ,WK,WV)。


X 矩阵中的每一行对应于输入句子中的一个单词。我们再次看到嵌入向量(图中 512 或 4 个框)和 q/k/v 向量(图中 64 个或 3 个框)的大小差异

最后,由于我们正在处理矩阵,我们可以将步骤 2 到 6 压缩在一个公式中,以计算自我注意层的输出。


矩阵形式的自我注意计算

八、多头兽

        该论文通过添加一种称为“多头”注意的机制进一步完善了自我注意层。这通过两种方式提高了注意力层的性能:

  1. 它扩展了模型专注于不同位置的能力。是的,在上面的例子中,z1 包含一点其他编码,但它可能由实际单词本身主导。如果我们翻译一个句子,比如“动物没有过马路,因为它太累了”,知道“它”指的是哪个词会很有用。

  2. 它为注意力层提供了多个“表示子空间”。正如我们接下来将看到的,对于多头注意力,我们不仅有一组,而是多组查询/键/值权重矩阵(转换器使用八个注意力头,所以我们最终为每个编码器/解码器有八组)。这些集合中的每一个都是随机初始化的。然后,在训练后,每个集合用于将输入嵌入(或来自较低编码器/解码器的向量)投影到不同的表示子空间中。


通过多头注意,我们为每个头部维护单独的 Q/K/V 权重矩阵,从而产生不同的 Q/K/V 矩阵。和以前一样,我们将 X 乘以 WQ/WK/WV 矩阵以产生 Q/K/V 矩阵。


如果我们做上面概述的相同的自我注意计算,只有八次不同的时间使用不同的权重矩阵,我们最终得到八个不同的 Z 矩阵

这给我们带来了一些挑战。前馈层不需要八个矩阵 - 它期望单个矩阵(每个单词的向量)。因此,我们需要一种方法将这八个压缩成一个矩阵。

我们如何做到这一点?我们将矩阵连接起来,然后将它们乘以一个额外的权重矩阵 WO。

这几乎就是多头自我关注的全部内容。我意识到这是相当多的矩阵。让我尝试将它们全部放在一个视觉对象中,以便我们可以在一个地方查看它们

现在我们已经触及了注意力头,让我们重新审视一下之前的例子,看看当我们在例句中编码单词“it”时,不同的注意力头集中在哪里:


当我们对“它”这个词进行编码时,一个注意力头最关注“动物”,而另一个注意力集中在“疲惫”上——从某种意义上说,模型对“它”这个词的表示融入了“动物”和“疲惫”的一些表示。

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

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

        到目前为止,模型中缺少的一件事是解释输入序列中单词顺序的方法。

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

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

        如果我们假设嵌入的维数为 4,则实际的位置编码如下所示:


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

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

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


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

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

        2020 年 2 月更新:上面显示的位置编码来自转换器的 Tensor<>Tensor 实现。论文中显示的方法略有不同,因为它不直接连接,而是交织两个信号。下图显示了它的外观。下面是生成它的代码

十、残余物(残差)

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

        如果我们要可视化与自我注意相关的向量和层范数运算,它看起来像这样:

        这也适用于解码器的子层。如果我们要考虑一个由 2 个堆叠编码器和解码器的 Transformer 组成,它看起来像这样:

十一、解码器端

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

        编码器首先处理输入序列。然后将顶部编码器的输出转换为一组注意力向量 K 和 V。每个解码器在其“编码器-解码器注意”层中使用这些,这有助于解码器专注于输入序列中的适当位置:

完成编码阶段后,我们开始解码阶段。解码阶段的每个步骤都会从输出序列中输出一个元素(在本例中为英语翻译句子)。

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

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

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

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

十二、最终的线性层和软最大值层

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

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

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

然后,softmax层将这些分数转换为概率(全部为正,加起来为1.0)。选择概率最高的像元,并生成与之关联的单词作为此时间步长的输出。


该图从底部开始,作为解码器堆栈输出生成的矢量。然后将其转换为输出字。

十三、 训练回顾

        现在我们已经通过经过训练的转换器涵盖了整个正向传递过程,那么浏览一下训练模型的直觉会很有用。

        在训练期间,未经训练的模型将经历完全相同的正向传递。但是由于我们在标记的训练数据集上训练它,我们可以将其输出与实际正确的输出进行比较。

        为了形象化这一点,让我们假设我们的输出词汇表只包含六个单词(“a”、“am”、“i”、“谢谢”、“学生”和“<eos>”(“句子结尾”的缩写))。


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

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


示例:输出词汇表的独热编码

在此回顾之后,让我们讨论模型的损失函数 - 我们在训练阶段优化的指标,以导致一个经过训练且有望令人惊讶的准确模型。

十四、损失函数

        假设我们正在训练我们的模型。假设这是我们在培训阶段的第一步,我们正在通过一个简单的例子来训练它 - 将“怜悯”翻译成“谢谢”。

        这意味着,我们希望输出是一个概率分布,指示单词“谢谢”。但由于这个模型还没有经过训练,所以现在不太可能发生。


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

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

但请注意,这是一个过于简化的示例。更现实地说,我们将使用一个比一个单词更长的句子。例如 – 输入:“je suis étudiant”和预期输出:“我是学生”。这真正意味着,我们希望我们的模型连续输出概率分布,其中:

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


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

        在足够大的数据集上训练模型足够长的时间后,我们希望生成的概率分布如下所示:


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

        现在,由于模型一次生成一个输出,因此我们可以假设模型正在从该概率分布中选择概率最高的单词,并丢弃其余的单词。这是一种方法(称为贪婪解码)。另一种方法是保留,比如说,前两个单词(例如,“I”和“a”),然后在下一步中,运行模型两次:一次假设第一个输出位置是单词“I”,另一次假设第一个输出位置是单词“a”,考虑到位置 #1 和 #2,哪个版本产生的错误较小,则保留。我们对位置 #2 和 #3 重复此操作...等。这种方法称为“波束搜索”,在我们的示例中,beam_size是两个(意味着始终将两个部分假设(未完成的翻译)保存在内存中),top_beams也是两个(意味着我们将返回两个翻译)。这些都是您可以试验的超参数。

十五、转型

        我希望你已经发现这是一个有用的地方,开始打破变形金刚的主要概念。如果你想更深入,我建议这些后续步骤:

参考和后续工作:

猜你喜欢

转载自blog.csdn.net/gongdiwudu/article/details/131896915