图解Transformer

图解Transformer

Transformer模型是在论文《Attention is All You Need》中提出的。它的TensorFlow实现作为Tensor2Tensor包的一部分是可用的。哈佛大学的自然语言处理小组创建了一个指南,用PyTorch实现对论文进行了注释。在这篇文章中,我们将尝试简化一些内容,并逐个介绍概念,希望能让没有深入知识的人更容易理解。

A High-Level Look

让我们从将模型视为一个单一的黑盒子开始。在机器翻译应用中,它将接受一种语言的句子,并输出其在另一种语言的翻译。
在这里插入图片描述
打开这个擎天柱(Optimus Prime,这里比喻为复杂事物的核心部分)的外壳,我们看到了一个编码组件、一个解码组件以及它们之间的连接。
在这里插入图片描述

编码组件是由一系列编码器堆叠而成的(论文中将六个编码器堆叠在一起——数字六没有特别的意义,完全可以通过实验来探索其他的排列方式)。解码组件也是由相同数量的解码器堆叠而成的。
在这里插入图片描述

编码器在结构上都是相同的(尽管它们不共享权重)。每一个编码器都由两个子层组成:
在这里插入图片描述

编码器的输入首先流经一个自注意力层——这是一个帮助编码器在对特定单词进行编码时查看输入句子中其他单词的层。
自注意力层的输出接着被送入一个前馈神经网络。完全相同的前馈网络独立地应用于每个位置。

解码器也有这两个层,但在它们之间是一个注意力层,它帮助解码器聚焦于输入句子中的相关部分(类似于注意力在序列到序列模型中的作用)。
在这里插入图片描述

Bringing The Tensors Into The Picture(将张量引入到图像中)

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

正如在自然语言处理(NLP)应用中通常的情况一样,我们首先使用嵌入算法将每个输入单词转换为向量。


每个单词都被嵌入到一个大小为512的向量中。我们将用这些简单的框来表示这些向量
嵌入只在最底层的编码器中发生。所有编码器共有的抽象是它们接收到一个大小为512的向量列表——在底层编码器中,这些将是单词嵌入,但在其他编码器中,将是直接位于其下方的编码器的输出。这个列表的大小是一个我们可以设置的超参数——基本上它将是我们训练数据集中最长句子的长度。

在将输入序列中的单词进行嵌入后,它们每个都会依次流经编码器的两个层。

在这里插入图片描述

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

Now We’re Encoding!

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

在这里插入图片描述
每个位置的单词都会经过一个自注意力过程。然后,它们各自通过一个前馈神经网络——完全相同的网络,每个向量独立地流经它

Self-Attention at a High Level

不要被我随意使用“自注意力”这个词所迷惑,好像这是一个每个人都应该熟悉的概念。直到我阅读了《Attention is All You Need》这篇论文之前,我个人从未遇到过这个概念。让我们来提炼一下它的工作原理。

假设下面的句子是我们想要翻译的输入句子:”The animal didn’t cross the street because it was too tired”

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

当模型处理单词“它”时,自注意力允许将其与“动物”关联起来。

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

如果你熟悉RNN(循环神经网络),可以想象一下,维持一个隐藏状态如何允许RNN将其已处理过的前一个单词/向量的表示与当前正在处理的单词结合起来。自注意力是Transformer用来将对其他相关单词的“理解”融入我们当前正在处理的单词的方法。

在这里插入图片描述
当我们在编码器#5(堆栈中的顶层编码器)中编码单词"it"时,注意力机制的一部分专注于"The Animal"(这只动物),并将其中一部分表示融入到"it"的编码中。

Self-Attention in Detail

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

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

请注意,这些新向量的维度比嵌入向量小。它们的维度是64,而嵌入和编码器输入/输出向量的维度是512。它们不必非得是更小的,这是一种架构选择,使得多头注意力(multiheaded attention)的计算(大多数情况下)是恒定的。
在这里插入图片描述将x1乘以WQ权重矩阵产生q1,即与该单词相关联的“查询”向量。我们最终为输入句子中的每个单词创建了一个“查询”、“键”和“值”的投影。

注解

  1. 查询(Query)向量:
  • 查询向量代表了当前处理的单词或位置的“兴趣点”,即模型在生成当前位置的输出时应该关注输入序列中的哪些部分。在自注意力机制中,查询向量用于与键向量进行匹配,以计算注意力分数。
  1. 键(Key)向量:
  • 键向量与查询向量一起工作,用于确定输入序列中每个单词对当前查询点的相关性。注意力分数是通过计算查询向量和键向量之间的点积得到的,这个分数表明了输入序列中每个单词对当前查询点的重要性。
  1. 值(Value)向量:
  • 值向量包含了输入序列中每个单词的实际内容信息,它将被用来生成输出,但是只有在通过注意力分数加权之后。在自注意力机制中,所有的值向量都会根据对应的注意力分数进行加权求和,以生成最终的输出表示。
    在Transformer模型中,这些向量是通过将输入嵌入与三个不同的可学习的权重矩阵(WQ、WK、WV)相乘来生成的。这样,每个输入单词都会产生一个查询向量、一个键向量和一个值向量,它们将被用于后续的注意力计算。
    多头注意力(Multi-head Attention)是一种特殊的自注意力机制,它将这个过程复制多次(通常是多头),每个头使用不同的权重矩阵集合,然后将得到的所有头的输出拼接起来,再次进行线性变换,以增加模型的表达能力

想象一下你在图书馆(输入句子)中寻找一本书(输出单词)。在这个过程中:

  1. 查询(Query):
  • 查询就像是你的搜索请求。当你告诉图书管理员你想要什么类型的书时,你实际上是在提交一个查询请求。在自注意力机制中,查询向量帮助模型确定它在生成句子的某个单词时应该关注输入句子的哪些部分。
  1. 键(Key):
  • 键就像是图书馆中每本书的索引编号。当你在图书馆中寻找书籍时,你需要根据索引编号来找到你想要的书。在自注意力机制中,键向量用于与查询向量进行匹配,以确定输入句子中每个单词与当前查询的相关性。
  1. 值(Value):
  • 值就像是书的内容。一旦你通过索引编号找到了书,你实际上感兴趣的是书的内容,而不是索引编号本身。在自注意力机制中,值向量包含了输入单词的实际信息,这些信息将被用来生成最终的输出,但在使用之前,它们会根据查询和键的匹配程度进行加权。
    在自注意力机制中,模型会创建这些查询、键和值向量,然后使用它们来决定输入句子的每个部分对生成输出单词的贡献有多大。这样,模型就可以更好地理解单词之间的关系,并生成更准确的翻译或响应。

它们是用于计算和思考注意力的抽象概念。一旦你继续阅读下面如何计算注意力的内容,你将几乎了解这些向量各自扮演的角色所需的全部信息。

计算自注意力的第二步是计算一个分数。假设我们正在计算这个例子中第一个单词“Thinking”的自注意力。我们需要对输入句子中的每个单词进行评分,与这个词进行比较。这个分数决定了在对某个位置的单词进行编码时,要对输入句子的其他部分给予多少关注。

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

在这里插入图片描述

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

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

每一个val得到的softmax分数类似这个val所占的比重(所有softmax分数加一起为1),计算的softmax分数之前需要对Score进行缩放,主要是为了防止点乘得到的值过大,softmax太小趋于0导致反向传播梯度消失。

第六步是将加权的值向量加起来。这产生了自注意力层在该位置的输出(对于第一个单词)。

加权的值相加是每一个val乘上其对应的权重然后相加得到最终的结果

在这里插入图片描述

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

Matrix Calculation of Self-Attention

第一步是计算查询(Query)、键(Key)和值(Value)矩阵。我们通过将嵌入打包成一个矩阵X,然后将其乘以我们已经训练过的权重矩阵(WQ、WK、WV)来实现这一点。

WQ、WK、WV是可学习的参数

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

最后,由于我们处理的是矩阵,我们可以将步骤二到步骤六合并为一个公式,来计算自注意力层的输出。
在这里插入图片描述

自注意力计算的矩阵形式是将整个过程——从计算查询(Query)、键(Key)和值(Value)的点积,到应用softmax函数,再到加权求和值(Value)向量——用矩阵运算来高效地完成。这样做的好处是可以利用矩阵运算的并行性,从而加快计算速度。

在矩阵形式的自注意力计算中,首先使用嵌入矩阵X和权重矩阵WQ、WK、WV来生成Q、K、V矩阵。然后,使用这些矩阵来计算注意力分数和权重,最后计算加权的值向量的和,得到自注意力层的输出。

具体的数学表达式通常是这样的:

  1. 计算查询和键的点积,然后应用softmax函数来获得注意力权重。
  1. 将注意力权重与值矩阵相乘,得到加权的值向量。
  2. 将这些加权的值向量求和,得到最终的自注意力层输出。

在实际操作中,这些步骤通常是高度优化的,以利用GPU等硬件的并行处理能力。在深度学习框架中,如TensorFlow或PyTorch,这些操作通常是内置的,可以直接使用。

The Beast With Many Heads

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

  1. 它扩展了模型关注不同位置的能力。是的,在上述示例中,z1包含了其他编码的一小部分,但它可能由实际的单词本身主导。如果我们正在翻译一个句子,如“The animal didn’t cross the street because it was too tired”(这只动物没有过马路,因为它太累了),知道“it”指的是哪个词将非常有用。
  2. 它为注意力层提供了多个“表示子空间”。正如我们接下来将看到的,通过多头注意力,我们不仅有一组,而是有多组查询/键/值权重矩阵(Transformer使用八个注意力头,因此我们最终在每个编码器/解码器中都有八组)。这些集合中的每一个都是随机初始化的。然后,在训练后,每组都用于将输入嵌入(或来自较低编码器/解码器的向量)投影到不同的表示子空间中。

在这里插入图片描述

如果我们按照上面描述的方法,使用不同的权重矩阵进行八次相同的自注意力计算,我们最终会得到八个不同的Z矩阵。

在这里插入图片描述

这给我们带来了一点挑战。前馈层不是在等待八个矩阵——它期望的是一个单一的矩阵(每个单词一个向量)。所以我们需要一种方法将这八个矩阵合并成一个单一的矩阵。

我们怎么做呢?我们先将这些矩阵进行拼接,然后再将它们乘以一个额外的权重矩阵WO。
在这里插入图片描述

多头注意力除了包含多个Wq,Wk,Wv以外,还有一个Wo用于把每一个注意力的输出进行合并的

这就是多头自注意力的全部内容了。我意识到这里有相当多的矩阵。让我试着将它们全部放在一个视觉图中,这样我们就可以一次性查看它们。

在这里插入图片描述

现在我们已经涉及到了注意力头,让我们重新审视之前的例子,看看在编码我们示例句子中的单词“it”时,不同的注意力头都聚焦在哪里:

在这里插入图片描述在编码单词“it”时,一个注意力头主要关注“the animal”(这只动物),而另一个则关注“tired”(累了)——从某种意义上说,模型对单词“it”的表示融合了“animal”和“tired”的表示。

然而,如果我们将所有的注意力头都添加到图中,事情可能会更难解释:
在这里插入图片描述

Representing The Order of The Sequence Using Positional Encoding

到目前为止,我们描述的模型中缺少的一点是,模型需要一种方法来考虑输入序列中单词的顺序。

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

在这里插入图片描述

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

如果我们假设嵌入的维度为4,那么实际的位置编码将如下所示:
在这里插入图片描述一个使用玩具示例嵌入大小为4的真实位置编码的例子。

位置编码器实现:位置编码器并不是简单的1,2,3,4的数值信息,而是一个d维的向量。d维通常与词嵌入的维度相同。在n个输入的序列中每一个位置都会有一个d维的位置向量。

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

在下图中,每一行对应一个位置编码的向量。因此,第一行将是我们添加到输入序列中第一个单词嵌入的向量。每一行包含512个值——每个值介于1和-1之间。我们已经用颜色编码它们,以便可以看到模式。
在这里插入图片描述

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

The Residuals

在继续之前,我们需要提到编码器架构中的一个细节,那就是编码器中的每个子层(自注意力层、前馈神经网络)都有一个残差连接,并且后面跟着一个层归一化步骤。

在这里插入图片描述

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

在这里插入图片描述

每个输入向量首先经过自注意力层处理,然后每个输出向量会添加一个从相应的输入向量来的残差连接。接下来,这个合并的输出将进入层归一化步骤,以确保训练过程中的稳定性和效率。在可视化中,你可能会看到每个步骤的向量表示,以及层归一化如何调整这些向量的分布。

这同样适用于解码器的子层。如果我们考虑一个由2个堆叠的编码器和解码器组成的变压器,它可能看起来像这样:
在这里插入图片描述

The Decoder Side

既然我们已经讨论了编码器方面的大部分概念,我们基本上也了解了解码器的组成部分是如何工作的。但让我们来看看它们是如何协同工作的。

编码器开始处理输入序列。最顶层编码器的输出随后被转换成一组注意力向量K(Key)和V(Value)。这些向量将被每个解码器在其“编码器-解码器注意力”层中使用,该层帮助解码器聚焦于输入序列中的适当位置:

解码器中的交叉注意力的k和v是来自编码器的k和v的

在这里插入图片描述

接下来的步骤重复该过程,直到达到一个特殊符号,表明变压器解码器已完成其输出。每一步的输出都会在下一个时间步骤传递给底部的解码器,解码器就像编码器一样逐层向上传递它们的解码结果。就像我们对编码器输入所做的那样,我们也对这些解码器输入进行嵌入并添加位置编码,以指示每个单词的位置。

在这里插入图片描述

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

在解码器中,自注意力层只允许关注输出序列中较早的位置。这是通过在自注意力计算的softmax步骤之前,通过掩蔽未来位置(将它们设置为负无穷大)来实现的。

“编码器-解码器注意力”层的工作方式与多头自注意力类似,不同之处在于它从下面的层创建其查询(Queries)矩阵,并从编码器堆栈的输出中获取键(Keys)和值(Values)矩阵

The Final Linear and Softmax Layer

解码器堆栈输出一个浮点向量。我们如何将这个向量转换成一个单词呢?这是最后一个线性层的工作,该层后跟一个Softmax层。

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

假设我们的模型从其训练数据集中学习了10,000个独特的英文单词(我们模型的“输出词汇”)。这将使logits向量的宽度为10,000个单元——每个单元对应一个独特单词的分数。这就是我们如何解释经过线性层后的模型输出。

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

在这里插入图片描述这个图形从底部开始,以解码器堆栈输出的向量为起点。然后,这个向量被转换成一个输出单词。

Recap Of Training

既然我们已经讨论了经过训练的变压器的整个前向传播过程,现在了解一下训练模型的直觉将是有益的。

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

为了形象化这一点,假设我们的输出词汇只包含六个单词(“a”,“am”,“i”,“thanks”,“student”,和“”(代表‘句末’))。
在这里插入图片描述我们模型的输出词汇表是在我们开始训练之前的预处理阶段创建的。

一旦我们定义了我们的输出词汇表,我们可以使用相同宽度的向量来表示词汇表中的每个单词。这也被称为独热编码(one-hot encoding)。例如,我们可以使用以下向量来表示单词“am”:
在这里插入图片描述示例:我们输出词汇表的一位有效编码

在这次回顾之后,让我们讨论一下模型的损失函数——这是我们在训练阶段进行优化的指标,以期得到一个训练有素且希望是惊人准确的模型。

The Loss Function

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

这意味着,我们希望输出是一个概率分布,指示单词“thanks”。但由于这个模型尚未训练,目前不太可能发生这种情况。

在这里插入图片描述

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

如何比较两个概率分布?我们简单地从一个中减去另一个。有关更多细节,请查看交叉熵和Kullback-Leibler散度。

但请注意,这是一个过于简化的例子。更现实地,我们将使用超过一个单词的句子。例如——输入:“je suis étudiant”和预期输出:“i am a student”。这实际上意味着,我们希望我们的模型能够连续输出概率分布,其中:
1.- 每个概率分布由一个宽度为词汇表大小(在我们的例子中是6,但实际上可能是30,000或50,000等数字)的向量表示。
2.- 第一个概率分布在与单词“i”相关联的单元格中具有最高的概率。
3.- 第二个概率分布在与单词“am”相关联的单元格中具有最高的概率。
4.- 以此类推,直到第五个输出分布指示“句子结束”的符号,它也从10,000个元素的词汇表中与之相关联的单元格中具有最高的概率。

在这里插入图片描述

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

在足够大的数据集上对模型进行足够时间的训练后,我们希望产生的的概率分布看起来会像这样:

在这里插入图片描述

当然,在训练过程中,我们希望模型能够输出我们期望的正确翻译。如果这个短语是训练数据集的一部分,那么这并不是一个真正的指示(参见:交叉验证)。请注意,即使某个位置作为该时间步输出的可能性很小,它仍然会获得一定的概率——这是softmax的一个非常有用的特性,有助于训练过程。

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

翻译自(https://jalammar.github.io/illustrated-transformer/)

  • 26
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值