图解变压器(Transformer)

2023-12-21 12:04:50

一、说明


????????观看:麻省理工学院的深度学习最先进讲座引用了这篇文章

????????在上一篇文章中,我们研究了注意力——现代深度学习模型中普遍存在的方法。注意力是一个有助于提高神经机器翻译应用程序性能的概念。在这篇文章中,我们将研究Transformer——一种利用注意力来提高模型训练速度的模型。Transformer 在特定任务中的表现优于 Google 神经机器翻译模型。然而,最大的好处来自 Transformer 如何实现并行化。事实上,Google Cloud 建议使用 Transformer 作为参考模型来使用其Cloud TPU产品。因此,让我们尝试分解该模型并看看它是如何运作的。

????????Transformer 是在Attention is All You Need论文中提出的。它的 TensorFlow 实现作为Tensor2Tensor包的一部分提供。哈佛大学的 NLP 小组创建了一个指南,用 PyTorch 实现对论文进行注释。在这篇文章中,我们将尝试稍微简化一些事情,并一一介绍这些概念,希望能让那些对主题没有深入了解的人更容易理解。

????????2020 更新:我创建了一个“叙述变压器”视频,这是一个更温和的主题方法:

二、变压器环节

2.1 高级外观

????????让我们首先将该模型视为一个黑匣子。在机器翻译应用程序中,它将采用一种语言的句子,并以另一种语言输出其翻译。

????????打开擎天柱的优点(Optimus Prime),我们看到一个编码组件、一个解码组件以及它们之间的连接。

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

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

????????编码器的输入首先流经自注意力层,该层帮助编码器在对特定单词进行编码时查看输入句子中的其他单词。我们将在本文后面仔细研究自我注意力。

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

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

2.2 将张量带入图片中

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

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


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

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

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

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

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

2.3 现在我们正在编码!

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


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

2.4 高水平的自我关注

????????不要被我乱用“自我注意力”这个词所愚弄,好像这是每个人都应该熟悉的概念。在阅读《注意力就是你所需要的一切》论文之前,我个人从未遇到过这个概念。让我们提炼一下它是如何工作的。

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

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

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

????????当模型处理“it”这个词时,自注意力使其能够将“it”与“动物”联系起来。

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

????????如果您熟悉 RNN,请考虑维护隐藏状态如何允许 RNN 将其先前处理过的单词/向量的表示与当前正在处理的单词/向量合并起来。自注意力是 Transformer 用来将其他相关单词的“理解”融入到我们当前正在处理的单词中的方法。


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

????????请务必查看Tensor2Tensor 笔记本,您可以在其中加载 Transformer 模型,并使用此交互式可视化检查它。

2.5 自注意力细节

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

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

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


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


?

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

????????它们是对于计算和思考注意力有用的抽象。一旦您继续阅读下面的注意力是如何计算的,您就会几乎了解每个向量所扮演的角色。

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

????????分数是通过查询向量我们要评分的各个单词的键向量的点积来计算的。因此,如果我们处理位置#1中单词的自注意力,第一个分数将是q1k1的点积。第二个分数是q1k2的点积。

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

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

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

????????第六是对加权值向量求和。这会在该位置(第一个单词)产生自注意力层的输出。

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

2.6 Self-Attention的矩阵计算

第一步是计算查询、键和值矩阵。我们通过将嵌入打包到矩阵X中,并将其乘以我们训练过的权重矩阵(WQWKWV)来做到这一点。


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

最后,由于我们处理的是矩阵,因此我们可以将第二步到第六步压缩为一个公式来计算自注意力层的输出。


矩阵形式的self-attention计算

三、多头兽

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

  1. 它扩展了模型关注不同位置的能力。是的,在上面的示例中,z1 包含一些其他编码,但它可能由实际单词本身主导。如果我们翻译一个句子,比如“The Animal did not cross the street because it was tooert”,那么知道“it”指的是哪个单词会很有用。

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


通过多头注意力,我们为每个头维护单独的 Q/K/V 权重矩阵,从而产生不同的 Q/K/V 矩阵。正如我们之前所做的那样,我们将 X 乘以 WQ/WK/WV 矩阵以生成 Q/K/V 矩阵。


????????如果我们进行与上面概述相同的自注意力计算,只是使用不同的权重矩阵进行八次不同的时间,我们最终会得到八个不同的 Z 矩阵

????????这给我们带来了一些挑战。前馈层不需要八个矩阵——它需要一个矩阵(每个单词一个向量)。所以我们需要一种方法将这八个压缩成一个矩阵。

????????我们该怎么做呢?我们将矩阵连接起来,然后将它们乘以一个附加的权重矩阵 WO。

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

????????现在我们已经触及了注意力头,让我们回顾一下之前的示例,看看当我们在示例句子中对单词“it”进行编码时,不同的注意力头聚焦在哪里:


当我们对“it”这个词进行编码时,一个注意力头主要关注“动物”,而另一个注意力头则关注“累”——从某种意义上说,模型对“it”这个词的表示会烘焙一些表示“动物”和“累”。

????????然而,如果我们将所有注意力头添加到图片中,事情可能会更难以解释:

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

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

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


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

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


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

这个模式可能是什么样子?

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


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

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

????????2020 年 7 月更新:?上面显示的位置编码来自 Transformer 的 Tensor2Tensor 实现。论文中所示的方法略有不同,它不是直接连接,而是将两个信号交织在一起。下图显示了它的样子。这是生成它的代码

3.2 处理残差

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

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

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

3.3 解码器端

????????现在我们已经涵盖了编码器方面的大部分概念,我们基本上也知道了解码器的组件是如何工作的。但让我们看看它们是如何协同工作的。

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


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

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

????????解码器中的自关注层的运行方式与编码器中的运行方式略有不同:

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

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

四、最后的线性和 Softmax 层

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

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

????????假设我们的模型知道从训练数据集中学习的 10,000 个独特的英语单词(我们模型的“输出词汇”)。这将使 logits 向量有 10,000 个单元格宽——每个单元格对应一个唯一单词的分数。这就是我们解释线性层模型输出的方式。

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


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

4.1 培训回顾

现在我们已经通过经过训练的 Transformer 介绍了整个前向传递过程,了解一下训练模型的直觉会很有用。

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

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


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

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


示例:输出词汇的 one-hot 编码

????????在回顾之后,让我们讨论模型的损失函数——我们在训练阶段优化的指标,以形成经过训练的、希望非常准确的模型。

4.2? 损失函数

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

????????这意味着我们希望输出是表示“谢谢”一词的概率分布。但由于该模型尚未经过训练,因此目前还不太可能发生。


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

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

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

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


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

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


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

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

文章来源:https://blog.csdn.net/gongdiwudu/article/details/135124136
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。