zl程序教程

您现在的位置是:首页 >  后端

当前栏目

【Transformer】图解 Transformer

图解 transformer
2023-09-14 09:14:47 时间

🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎

📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃

🎁欢迎各位→点赞👍 + 收藏⭐ + 留言📝

📣系列专栏 - 机器学习【ML】 自然语言处理【NLP】 深度学习【DL】

🖍foreword

✔说明⇢本人讲解主要包括Python、机器学习(ML)、深度学习(DL)、自然语言处理(NLP)等内容。

如果你对这个系列感兴趣的话,可以关注订阅哟👋


文章目录

高级外观

将张量带入图片

现在我们正在编码

高层次的 Self-Attention

Self-Attention 的细节

Self-Attention的矩阵计算

有许多个头的野兽

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

残差

解码器端

最后的 Linear 和 Softmax 层

Training回顾

损失函数


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

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

高级外观

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

打开擎天柱,我们看到一个编码组件、一个解码组件以及它们之间的连接。

编码组件是一堆编码器(论文将其中的6个堆叠在一起——数字 6 没有什么神奇的,人们肯定可以尝试其他安排)。解码组件是一堆相同数量的解码器。

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

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

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

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

将张量带入图片

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

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

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

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

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

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

接下来,我们将把示例转换成一个较短的句子,然后看看编码器的每个子层都发生了什么。

现在我们正在编码

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

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

高层次的 Self-Attention

不要被我抛出的“self-attention”这个词所愚弄,好像每个人都应该熟悉这个概念一样。在阅读 Attention is All You Need 论文之前,我个人从未接触过这个概念。让我们提炼一下它是如何工作的。

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

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

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

当模型处理“it”这个词时,self-attention 允许它把“it”和“animal”联系起来。

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

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

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

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

Self-Attention 的细节

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

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

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

将x1乘以WQ权重矩阵产生q1,即与该词关联的“query”向量。我们最终创建了输入句子中每个单词的“query”、“key”和“value”投影。

“query”、“key”和“value”向量是什么?

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

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

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

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

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

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

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

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

Self-Attention的矩阵计算

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

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

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

矩阵形式的self-attention计算

有许多个头的野兽

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

  1. 它扩展了模型关注不同位置的能力。是的,在上面的示例中,z1 包含了所有其他编码的一点点,但它可能主要由实际单词本身决定。如果我们要翻译像“The animal didn't cross the street because it was too tired”这样的句子,知道“it”指的是哪个词会很有用。
  2. 它为注意力层提供了多个“表示子空间”。正如我们接下来将看到的,对于多头注意力,我们不仅有一个,而且有多组查询/键/值权重矩阵(Transformer 使用8个注意力头,所以我们最终为每个编码器/解码器得到8组) . 这些集合中的每一个都是随机初始化的。然后,在训练之后,每个集合用于将输入嵌入(或来自较低编码器/解码器的向量)投影到不同的表示子空间中。

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

如果我们进行与上面概述的相同的自注意力计算,只需8次不同的权重矩阵,我们最终会得到8个不同的 Z 矩阵

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

我们该怎么做?我们连接这些矩阵,然后将它们乘以一个额外的权重矩阵 WO。

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

现在我们已经谈到了注意力头,让我们重新审视我们之前的例子,看看当我们在示例句子中对单词“it”进行编码时,不同的注意力头集中在什么地方:

当我们对“it”这个词进行编码时,一个注意力头最关注“the animal”,而另一个注意力头则关注“tired”——从某种意义上说,模型对“it”这个词的表示在某些表示中被烘烤“animal”和“tired”。

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

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

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

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

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

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

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

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

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

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

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

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

残差

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

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

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

解码器端

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

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

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

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

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

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

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

最后的 Linear 和 Softmax 层

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

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

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

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

该图从底部开始,生成的向量作为解码器堆栈的输出。然后它变成一个输出字。

Training回顾

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

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

为了形象化这一点,让我们假设我们的输出词汇表只包含六个词(“a”、“am”、“i”、“thanks”、“student”和“<eos>”(“end of sentence”的缩写)) .

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

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

示例:输出词汇表的一次性编码

在回顾之后,让我们讨论一下模型的损失函数——我们在训练阶段优化的指标,以生成一个训练有素且有望达到惊人准确度的模型。

损失函数

假设我们正在训练我们的模型。假设这是我们在训练阶段的第一步,我们正在用一个简单的例子训练它——将“merci”翻译成“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 为二(意味着在任何时候,两个部分假设(未完成的翻译)都保存在内存中),top_beams 也是两个(意味着我们将返回两个翻译)。这些都是您可以试验的超参数。