The Illustrated Transformer

Alammer老师

原文地址:

The Illustrated Transformer – Jay Alammar – Visualizing machine learning one concept at a time. (jalammar.github.io)​jalammar.github.io/illustrated-transformer/

初探Transformer

首先我们从一个黑箱的角度观察模型。在一个机器翻译模型中,输入某一语种的一句话就会输出它另一种语言的翻译版本。

中间酷似擎天柱的引擎盖的图案就是我们的黑箱模型

打开擎天柱的引擎盖,我们能够看到一个编码组件,一个解码组件,当然他们之间有着某种联系。

编码组件是一堆编码器(本文只显示了其中六个堆叠在一起——为什么是六个呢?并没有什么神奇之处,当然可以尝试其他数字排列方式)。解码组件是一堆相同数量的解码器。

所有编码器有着相同的结构(但是它们不进行权值共享),它们都由两个次级结构层组成:

编码器的输入首先流经self-attention层——该层帮助编码器在对特定单词进行编码时关注单词所在的输入句子中的其他单词。我们将在后面的文章中仔细研究Self-Attention的机制。

self-attention层的输出被馈送到前馈神经网络。每个位置的前馈网络拥有完全相同结构,但其参数是完全独立的。

解码器同样具有这两个层,但在它们之间是一个E-D Attention层,它会帮助解码器专注于输入句子的相关部分(类似于seq2seq模型中的self-attention)。

引入张量

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

与NLP(自然语言处理)实际应用中的情况一样,我们首先使用嵌入算法将每个输入单词转换为向量(也叫做“嵌入”)。

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

只有最底部的编码器接受嵌入作为输入。 所有编码器都将接收一个大小为 512 的向量列表——但在底部的编码器中,这个向量列表是由具体的单词生成的嵌入;在其他编码器中,这个输入是位于下方的编码器的输出 . 这个列表的大小是我们可以设置的超参数——一般来说它是我们训练数据集中最长句子的长度。

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

在这里,我们开始看到 Transformer 的一个关键属性:每个位置的单词在编码器中流过属于自己的路径。 在self-attention层中,这些路径之间存在依赖关系。 然而,前馈层没有这些依赖关系,因此各种路径可以在流过前馈层的同时并行执行(这可以节省大量时间)。

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

现在开始编码!

正如我们已经提到的,编码器接收向量列表作为输入。 它通过将这些单词向量(嵌入)传递给self-attention层来处理这个列表,然后传递给前馈神经网络,最后将输出向上发送到下一个编码器。

每个位置的单词都会经过一个self-attention层。 然后,它们每个都通过一个前馈神经网络——每个前馈神经网络完全相同

深究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”联系起来。

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

如果您熟悉 RNN,思考一下RNN是如何保留隐藏状态,从而将其已处理的先前单词/向量的表示与当前正在处理的单词/向量结合起来。Self-attention是 Transformer 用来将其他相关单词的“理解”融入我们当前正在处理的单词的方法。

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

Self-attention机制相关细节

让我们先看看如何使用向量计算self-attention,然后继续看看它是如何使用矩阵实际实现的。

计算self-attention的第一步是从每个编码器的输入向量中创建三个向量(在这种情况下,每个编码器的输入向量是由每个词生成的嵌入)。因此,对于每个单词嵌入,我们创建一个Query向量、一个Key向量和一个Value向量。 这三个向量是通过将嵌入乘以我们在训练过程中训练的三个矩阵来创建的(对应下图的WQ,WK和WV)。

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

将 x1 乘以 WQ 权重矩阵产生 q1,即该词的Query向量。 我们最终为输入句子中的每个单词创建了一个Query、一个Key和一个Value映射。

什么是Query、Key和Value向量?

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

计算self-attention的第二步是计算一个分数。 假设我们正在计算本例中第一个单词“Thinking”的self-attention,我们需要根据这个词对输入句子中所含的每个词进行评分。当我们在某个位置对单词进行编码时,这个分数决定了对输入句子其他单词的关注程度(理解成寻找当前单词与其他单词间是否能够建立联系)。

分数是通过计算Query向量与我们正在评分的单词的Key向量的点积来实现的。 因此,如果我们正在处理位置1中单词“Thinking”的self-attention,第一个分数将是 q1 和 k1 的点积。 第二个分数是 q1 和 k2 的点积。

第三步和第四步是将分数除以 8(论文中使用的关键向量的维度——64的平方根,这会使梯度更稳定,当然可以用其他可能的值,但8是默认值),然后通过Softmax激活函数操作传递结果。Softmax可以将分数归一化,结果都是正数并且加起来为1。

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

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

第六步是对加权值向量求和。这会在这个位置产生Self-Attention层的输出(对于第一个词)

Self-Attention的计算到此结束,计算结果是我们可以发送到前馈神经网络的向量。当然,在实际实现中,这种计算是以矩阵形式进行的,以便更快地处理。接下来我们将看到具体到单词的计算过程。

Self-Attention的矩阵计算

第一步是计算Query, Key, and Value矩阵。 我们通过将嵌入打包到矩阵 X 中,并将其乘以我们训练的权重矩阵(WQ、WK、WV)来做到。

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

最后,由于我们处理的是矩阵,我们可以将步骤 2 到 6 合并到一个公式中来计算self-attention层的输出。

self-attention的矩阵计算

“九头蛇”

通过添加一种称为“多头”注意力的机制,多头机制通过两方面进一步完善了self-attention层:

  1. 多头机制扩展了模型关注不同位置的能力。在上面的示例中,z1是每组Softmax分数的加权之和,但z1的值很可能由当前位置的单词本身主导。如果我们翻译“The animal didn't cross the street because it was too tired”这样的句子会很有用,但我们也会想知道“它”指的是哪个词。
  2. 多头机制为self-attention层提供了多个“表示子空间”。正如我们接下来将看到的,在多头机制下,我们的Query/Key/Value矩阵不是只有一组,而是多组(Transformer 使用了八个attention head,所以我们最终每个编码器/解码器都有八个Query/Key/Value矩阵集合).这些集合中的每一个都是随机初始化的。然后,在训练之后,每个集合用于将输入的嵌入(或来自较低层编码器/解码器的向量)投影到不同的表示子空间中。

通过多头机制,每个头中的Q/K/V权重矩阵不进行权值共享,从而产生不同的Q/K/V矩阵。 正如我们之前所做的那样,我们将X乘以WQ/WK/WV 矩阵以产生 Q/K/V 矩阵

如果我们进行与上述相同的self-attention计算,我们会发现只是使用不同的权重矩阵进行八次相同过程的计算,我们最终会得到八个不同的Z矩阵。

这给我们带来了一些挑战,因为前馈层不需要八个矩阵——它只需要一个矩阵(根据每个单词产生的向量)。所以我们需要一种方法将多头机制生成的这八个矩阵浓缩成一个矩阵。

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

以上就是多头自注意力的全部内容。 我意识到我们提到了相当多的矩阵,让我尝试将它们全部放在一张图中,以便我们可以在同一个地方查看它们

既然我们已经了解多头机制,让我们重新审视我们之前的例子,看看当我们在例句中编码单词“it”时不同的attention head集中在哪里:

当我们对“it”这个词进行编码时,其中一个attention head最关注“动物”(如图中橙色所示),而另一个注意力头则专注于“疲倦”——在某种意义上,模型对“it”这个词的表示包含了一些表示 “动物”和“疲倦”这两个词的信息。

然而,如果我们将8个attention head都考虑在内,事情貌似更难去解释了。

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

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

为了解决这个问题,transformer为每个输入单词产生的嵌入添加了一个向量。 这些向量遵循模型学习的特定模式,这有助于模型确定输入序列中每个单词的位置或序列中不同单词之间的距离。 这样做的目的是,一旦将这些值投影到 Q/K/V 向量中以及在通过点积计算attention期间,这些值添加到嵌入中会在嵌入向量之间提供有意义的距离信息。

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

若我们假设每个单词生成的嵌入维度为4,则事实上赋予它们的位置编码会长这样:

嵌入大小为 4 的位置编码

这会使嵌入看上去发生什么变化呢?

在下图中,每一行对应一个向量的位置编码。 所以第一行将是我们添加到输入序列中第一个单词嵌入的向量。每个向量包含 512 个值——每个值都介于 1 和 -1 之间。 我们对它们进行了颜色编码,因此图案会变得更加清晰易懂。

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

在transformer的论文中描述了位置编码的公式(第 3.5 节)。您可以在以下链接中查看生成位置编码的代码。

https://github.com/tensorflow/tensor2tensor/blob/23bd23b9830059fbc349381b70d9429b5c40a139/tensor2tensor/layers/common_attention.py​github.com/tensorflow/tensor2tensor/blob/23bd23b9830059fbc349381b70d9429b5c40a139/tensor2tensor/layers/common_attention.py

这不是生成位置编码的唯一方法。 然而,它具有能够在不知道输入序列具体长度的情况下进行拓展编码的能力(例如,我们训练的模型被要求翻译一个比我们训练集中的任何一个句子更长的句子)。

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

https://github.com/jalammar/jalammar.github.io/blob/master/notebookes/transformer/transformer_positional_encoding_graph.ipynb​github.com/jalammar/jalammar.github.io/blob/master/notebookes/transformer/transformer_positional_encoding_graph.ipynb

残差结构

在继续讨论之前,我们需要提到的编码器架构中的一个细节是,每个编码器中的每个子层(self-attention,ffnn)在其周围都有一个残差结构,然后是一个层归一化步骤。

如果我们要更清晰地描述与 self attention 相关的向量和 layer-norm 操作,它看起来像这样:

这也适用于解码器的子层。 如果我们尝试去描述一个由 2 个堆叠而成的编码器和解码器组成的 Transformer,它看起来像这样:

终于,我们开始探究解码器

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

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

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

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

解码器中的self-attention层的操作方式与编码器中的方式略有不同:

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

“Encoder-Decoder Attention”层的工作方式与多头自注意力相似,不同之处在于它从其下方的层创建其查询Query矩阵,并从编码器堆叠的输出中获取 Keys 和 Values 矩阵。

最终的线性和 Softmax 层

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

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

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

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

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

模型训练回顾

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

在训练期间,未经训练的模型将通过完全相同的前向路径传递。 但是由于我们是在一个带标签的训练数据集上训练它,因此我们可以将它的输出与实际正确的输出进行比较。

为了更加形象地描述,假设我们的输出词汇表只包含六个单词(“a”、“am”、“i”、“thanks”、“student”和“<eos>”(“end of sentence”的缩写))。

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

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

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

损失函数

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

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

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

如何比较两个概率分布?我们只是从另一个中减去一个。有关更多详细信息,请自行查看交叉熵函数的定义和 Kullback-Leibler散度相关的知识。

但请注意,这是一个过于简单的示例。更现实的是,我们会使用一个含有多个单词的句子。例如——输入:“je suis étudiant”,预期输出:“我是学生”。这真正意味着,我们希望我们的模型能够连续输出概率分布,其中:

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

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

将一个模型训练在一个大型数据集中训练足够长时间后,我们会希望它输出一个如下图所示的概率分布:

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

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值