图解Transformer译文(illustrated-transformer)
图解 Transformer
原作者:Jay Alammar
原博客地址:http://jalammar.github.io/illustrated-transformer
Discussions: Hacker News (65 points, 4 comments), Reddit r/MachineLearning (29 points, 3 comments)
Translations: Arabic, Chinese (Simplified) 1, Chinese (Simplified) 2, French 1, French 2, Italian, Japanese, Korean, Persian, Russian, Spanish 1, Spanish 2, Vietnamese
Watch: MIT’s Deep Learning State of the Art lecture referencing this post
Featured in courses at Stanford, Harvard, MIT, Princeton, CMU and others
在上一篇文章中,我们讨论了注意力(Attention) —— 一个在现代深度学习模型广泛使用的方法。注意力是有助于提升神经机器翻译应用程序性能的一个概念性的东西。在这篇文章中,我们将观察 Transformer —— 一个使用注意力机制加速模型训练的模型\(^{[1]}\)。 Transformer 在一些特定任务上表现优于谷歌神经机器学习翻译模型。然而,它最大的优势来自于 Transformer 如何实现并行化。事实上 Google Cloud 建议使用 Transformer 作为参考模型来使用他们的 Cloud TPU 产品。所以让我们尝试分离这个模型看看它是如何工作的。
Transformer 在论文 Attention is All You Need 中提出。 TensorFlow 的实现只是作为 Tensor2Tensor 包的一部分提供。Harvard 的 NLP (Natural Language Processing)组创建了 guide annotating the paper with PyTorch implementation 。这篇文章,我们将尝试简化某些东西并逐步地引入概念,希望这能使没有相应学科背景的人更容易理解。
概览全局
让我们先把模型看作单个的黑匣子。在机器翻译应用程序中,它接收一种语言的一个句子,然后在另一边输出它的翻译。
打开潘多拉魔盒,我们可以看到一个编码组件(encoding component),一个解码组件(decoding component)以及他们之间的连接。
编码组件(encoding component)是一堆编码器(encoders)组成的栈(stack)(论文中堆叠了六个—— 六这个数字没有什么魔幻的,你绝对可以使用其他的排列方式实验)。解码器组件(decoding component)也是一堆相同数量的编码器(decoders)组成的栈(stack)。
编码器(encoders)在结构上都是相同的(但他们都不共享权重(weights))。每一个编码器都被分离为了两个子层。
编码器(encoder)的第一个输入首先流经自注意力层(self-attention layer)—— 该层在编码器(encoder)编码一个特殊单词时帮助编码器“查看”输入语句中的其他单词。
自注意力层(self-attention layer)的输出被喂给前向反馈神经网络(feed-forward neural network)。完全相同的前向反馈神经网络独立地应用于每一个单词的位置。
解码器(decoder)有相同的层,但是在他们之间是有助于解码器聚焦于输入语句相关部分的注意力层(attention layer)(类似在seq2seq 模型中注意力做的那样)
把张量(Tensors)带进图片中
现在我们已经看到了 Transformer 模型的主要组件,让我们开始看看各种向量(vectors)/张量(tensors),看看他们为了把训练的模型转换为输出都是怎么流动的。
与一般的 NLP 应用程序一样,我们从使用嵌入算法(embedding algorithm)把每一个输入的单词都转换成向量开始。
每一个单词都被嵌入(embedded in)大小为 512 的向量。我们将使用这些简单的盒子展示这些向量
embedding\(^{[2]}\) 仅仅发生在最低层的编码器(encoder)中。所有的编码器共同的抽象机制是他们都接收一个 512 大小的向量列表—— 在低层编码其中这会是词嵌入(word embeddings),但是在其他编码器中,当前编码器的输入是正下方编码器的输出。这个向量列表的长度是我们可以设置的超参数——它基本上是我们训练数据集中最长句子的长度了。
在嵌入(embedding)我们输入序列中的单词后,他们每一个都会流经编码器的这两个层。
这里我们看到 Transformer 的一个关键属性,每个位置的单词都流经编码器中自己的路径。在自注意力层(self-attention layer)这些路径之间存在相互依赖关系。前向反馈层(feed-forward layer)没有这些依赖关系,因此当他们流经前向反馈层时,各种路径可以并行执行。
下面,我们将把例子切换为比较短的句子,我们将看到在编码器中的每个子层中到底发生了什么。
现在我们正在编码!
正如我们已经提到的,一个编码器接收一个向量列表作为输入。它通过传递这些向量列表到‘自注意力’层来处理这些列表,然后到一个前向反馈神经网络,然后把输出向上发送到下一个编码器。
每个位置的单词传递到自注意力进程。然后,每一个都会通过前向反馈神经网络 —— 每个向量都分别流经相同的网络
认识自注意力机制
不要被我抛出的“自注意力(self-attention)”这个词给吓唬到,好像就是每个人都应该对这个概念很熟悉一样。实际上我从来没有接触过这个概念直到我读到 Attention is All You Need 这篇论文。让我们提炼一下它是如何工作的。
假设以下是我们想要翻译的输入语句:
“The animal didn't cross the street because it was too tired
”
“it”这个词在这个句子中指的是什么?它指的是街道还是动物?这对人类来说是一个简单的问题,但是对于算法来说可不是哦。
当这个模型正在处理单词“it”,自注意力允许“it”关联上“animal”。
当模型处理每个单词(输入序列的每个位置)时,为得到能帮助这个单词更好地编码的线索,自注意力(self-attention)允许“查看”输入序列的其他位置。
如果你对 RNNs(Recurrent Neural Network)比较熟悉的话,思考一下维护一个隐藏的状态怎样做才能允许一个 RNN 合并先前处理过的单词/向量和正在处理的单词/向量。自注意力(self-attention)是 Transformer 用来把其他相关单词的“理解”融入到我们当前正在处理的单词中的一种方法。
当我们正在编码器#5 中编码“it”这个单词时(栈(stack)中最顶层的编码器),部分的注意力机制是聚焦于“The Animal”并把它的表示融入到“it”的编码中。
务必检查在Tensor2Tensor notebook中能够加载上图的 Transformer 模型并且能够使用可视化交互的方式实验它。
深入自注意力机制
让我们看看怎样使用向量计算自注意力,然后看看它实际是如何实现的 —— 使用矩阵。
计算自注意力的第一步是从编码器每个输入向量中创建三个向量(在这种情况下,是每个单词的嵌入向量(embedding))。所以对于每一个单词,我们创建一个 Query 向量,一个 Key 向量和一个 Value 向量。通过把embedding和在训练期间我们训练的三个矩阵相乘获取这些向量。
注意,这些新的向量在维数(dimension)上要比 embedding 向量更小。他们的维数是 64,然而 embedding 和编码器输入/输出向量有 512 维。他们不必更小,这是架构上的选择,目的是为了让多头注意力的计算(大部分)保持不变。
\(q_{1}=x_{1}*W^{Q}\), \(q_{2}=x_{2}*W^{Q}\), \(k_{1}=x_{1}*W^{K}\),借助上图以此类推,其中可以看出这些向量和这些单词有一定的联系。我们最终会为句子中的每个单词创建一个“query”,“key”和“value”向量。
什么是“query”,“key”和“value”向量?
他们是对于计算和思考注意力极为有用的抽象。一旦你开始阅读下面注意力是怎样计算的,你就会知道你需要了解的这些向量每一个都扮演着什么角色。
计算自注意力的第二步是计算一个分数(score)。假设我们正在计算这个例子中的第一个单词的自注意力,“Thinking”。我们需要为输入句子的每一个单词评分。这个分数决定了当我们在一个确定的位置编码这一个单词时,我们需要对输入语句的其他部分给予多少关注。
query vector和我们正在评分的各个单词的key vector做点积计算出分数。所以如果我们正在处理处在位置#1的单词的自注意力时,q1和k2的点积就会是第二个分数。
第三和第四步是把这些分数除以8(在本文中使用的是这些key向量维度的平方根 —— 64。因为这样会有更稳定的梯度。也可以使用其他可能的值,但本文中使用默认的值),获得的结果流经softmax层。Softmax归一化会让这些分数维持正数且全部加起来为1。
流经softmax层的分数决定了每个单词在这个位置的表达量\(^{[3]}\)。如图Thinking这个单词在这个位置拥有更高的softmax分数,但是有时关注和这个单词相关联的其他单词也是很有用的。
第五步是把每个单词的softmax分数乘以每个value vector(准备把它们相加)。直觉上我们应该保持我们想要关注的单词不变并淹没掉不相关的单词(比如说把它们乘上0.001)。
第六步是把所有加权重的value vectors加和。在这个位置产生了自注意力的输出(在这里是输出第一个单词)。
自我注意计算到此结束。我们可以把最后的结果向量单独发送到前向神经网络。在实际的实现中,为了更快的处理这些过程,这些计算会以矩阵方式进行。所以,现在让我们看一下,我们已经有了在单词层面上计算的第一感觉。
自注意力的矩阵计算
第一步是计算Query,Key和Value矩阵。我们把embeddings打包成一个矩阵X,然后乘上我们训练的权重矩阵(WQ,WK,WV)。
在X矩阵中的每一行对应输入句子中的一个单词。我们也看到了embedding vector(512或者图中X的一行4列方框)和q/k/v向量(64或图中q/k/v的一行三列方框)的大小的差异。
最后,当我们处理矩阵的时候,我们把第二到第六步压缩为一个公式去计算自注意力层的输出。
以矩阵方式做自注意力计算
多头注意力
这篇论文添加“多头”注意力机制进一步定义自注意力层。这在两方面提高了注意力层的性能:
-
它扩展了模型关注不同位置的能力,是的,在上面的例子中,z1包含了一点其他编码,但它会被实际单词本身控制。如果我们翻译像“The animal didn't cross the street because it was too tired”这样的句子,有助于知道单词“it”到底指什么。
-
它为注意力层提供了多个“表示子空间”。我们下面将看到,使用多头注意力,我们不仅仅有一个,而是乘上多个Query/Key/Value(QKV)权重矩阵的集合( Transformer 使用八个注意力头,所以我们为每个编码器/解码器提供了八组QKV集合)。每一个集合都被随机初始化。然后,在训练之后,每组集合用于把input embeddings(或者说来自低层编码器/解码器的向量(vectors))映射到一个不同的表示子空间中。
使用多头注意力,我们为每个头维护分离的Q/K/V权重矩阵产生了不同的Q/K/V矩阵。正如我们在之前做的,X乘以WQ/WK/WV获得Q/K/V矩阵。
如果我们进行与上述完全相同的自注意力计算,仅需要用不同的权重矩阵进行八次不同的计算,我们就可以得到八个不同的Z矩阵。
这给我们留下了一点挑战。前向反馈层并不期望得到八个矩阵 —— 它只期望得到一个矩阵(每个单词对应的那个向量)。所以我们需要一种方式去把这八个矩阵压缩成一个矩阵。
我们怎么做到?我们把这些矩阵连接起来然后用额外的权重矩阵WO乘上它们。
这几乎就是多头自注意力机制的全部内容。我意识到,这是相当多的矩阵。让我尝试把他们放到一张图中以便我们能够在一个地方纵观全局。
现在我们已经触及到了注意力头,让我们回顾一下我们之间的例子看看在我们的例句中编码单词“it”时不同的注意力头都会关注哪部分。
当我们对“it”这个单词进行编码时,一个注意力头会更多把“it”和“the animal”相关联,然而其他注意力会关注“tired” —— 从某种意义上来讲,模型对单词“it”的表示方式与“animal”和“tired”的一些表示方式一致。
如果我们把所有的注意力都放在下面这一张图片上,事情可能会更难解释:
使用位置编码表示序列的顺序
迄今为止我们已经描述过的模型中还缺少解释在输入序列中单词顺序的方法。
为了解释这点, Transformer 为每一个input embedding添加一个向量。这些向量遵循模型学习的特定模式,这种模式帮助它决定每个单词的位置,或者在序列中不同单词的距离。直觉上一旦它们被映射到Q/K/V向量且在使用点积(dot-product)计算注意力期间,把这些值(每个单词在序列中的位置值)添加到embeddings中可以提供embedding vectors之间有意义的距离数据。
为了让模型知道每个单词都有自己在输入序列中的位置,我们添加了位置编码向量(positional encoding vectors) —— 这些值遵循一个特定的模式。
假设embedding有4个维度,实际的位置编码会像下图中那样:
小巧的4维embedding的位置编码的真实案例
这个模式是什么样子的?
在下图中,每一行对应一个向量的位置编码。所以第一行是我们添加的输入序列第一个单词的embedding的向量。每一行包含512个值 —— 每个都有一个1到-1之间的值。我们已经对它进行了颜色编码可视化了这个模式。
20个单词(行)的大小为512的embedding(列)的位置编码的真实案例。你能看到它看起来是从中间一分为二。这是因为左半边的值使用一个函数(使用正弦)生成,右半部分使用另一个函数(使用余弦)生成。然后把它们连接起来形成每个位置向量(positional encoding vectors)。
在论文3.5节描述了位置编码的公式。你能看到在 get_timing_signal_id()
中生成位置编码的代码。这不是位置编码唯一的生成方式。然而它的优势是能扩展到序列长度不可见的序列上(比如我们训练的模型被要求翻译一个比我们训练集中任何一条语句都要长的长语句的时候)。
2020年7月更新:以上展示的位置编码来自于Transformer的Tensor2Tensor实现。在论文中展示的方法有一点不同,因为他不是直接连接,而是把两个信号交织在一起。下图展示了它的样子。这里是生成它的代码:
残差
在继续前进之前,我们需要提到编码器架构中的一个细节,它就是在每个编码器中每个子层(自注意力,ffnn)周围都有一个残差连接,这个残差连接遵循归一化层(layer-normalization)这一步骤。
如果我们可视化这些向量和自注意力相关的归一化层(layer-norm)操作,它像下图这样:
这也适用于解码器的子层。如果我们考虑由2个堆叠的编码器和解码器组成的Transformer,它看起来如下图所示:
解码器
现在我们已经介绍了编码器端(encoder side)的大多数概念,我们也基本知道了解码器的组件是如何工作的。但是接下来让我们看看它们是如何协同工作的。
解码器以处理输入序列开始。然后顶部编码器的输出被转成一系列注意力向量K和V。“编码器-解码器注意力”层的每个解码器使用这些向量来帮助解码器将注意力集中在输入序列中的适当位置。
在完成编码阶段之后,我们开始解码阶段。解码阶段的每个步骤都从输出序列中输出一个元素(在这种情况下是英语翻译句子)。
接下来重复这个步骤直到到达一个特殊的标记表明the transformer decoder已经完成了它的输出。每一次解码的最终输出会喂给下一次解码(next time step)的最低层解码器\(^{[4]}\),然后解码器会像编码器那样放大它们的解码结果。我们也会像对待编码器的输入一样给每个解码器输入嵌入、添加位置编码来表明每个单词的位置。
在解码器操作的自注意力层相比编码器里的有一点不同:
在解码器中,自注意力层仅被允许关注输出序列中较早的位置。这是通过在自注意力计算的softmax步骤之前屏蔽未来位置(把它们设置为-inf)来做到的。
“编码器-解码器注意力”层仅仅像多头自注意力机制那样工作,只是它从它下面的层创建了它的Queries matrix并且从编码器堆栈的输出中获取Keys matrix和Values matrix。
最终层:线性和Softmax层
解码器堆栈输出浮点数向量。我们怎么把它转换回一个单词?这是线性层要做的工作,后面是Softmax层。
线性层是一个简单的全连接神经网络。他把解码器栈产生的向量映射到一个更大的向量,这个更大的向量被称作 logits vector 。
假设我们的模型知道10,000个来自于它训练数据集的唯一的英语单词(我们模型的“输出词汇”)。这会让 logits vector 高达10,000个单元格宽度 —— 每个单元格对应一个唯一单词的分数。这就是我们解释线性层之后的模型的输出的方式。
softmax层之后会把这些分数转换成概率(全部为正数,加起来为1.0)。选择高概率的单元格,并生成与其相关联的字作为这次(time step)的输出。
这张图从底部的向量开始,这个向量是解码栈输出的向量。然后被转化为一个输出单词。
总结
现在我们已经通过一个训练好的Transformer介绍了整个前向传递流程,它会对你训练模型的直觉起到一定促进作用。
在训练期间,一个没有被训练的模型会经过一个完全相同的前向传递流程。但是一旦我们在一个被标记的训练数据集上训练它,我们就能把它的输出同真实且正确的输出进行比较。
为了可视化它,假设我们的输出词汇仅仅包含六个单词("a", "am", "i", "thanks", "student"和"<eos>
"(语句结束标识符))。
我们模型的输出词汇表是在我们训练之前的预处理阶段被创建的。
一旦我们定义了我们的输出词汇,我们就能使用相同宽度的向量表示我们的词汇表中的每个单词。这也被称作 one-hot encoding
。所以举例来讲,我们使用以下的向量表示单词“am”:
例子:我们的输出词汇表的one-hot encoding
回顾本片,然后让我们讨论一下模型的损失函数 —— 我们在训练阶段优化的指标,帮助建立一个经过训练的且希望非常准确的模型。
损失函数
假设我们正在训练我们的模型,假设在我们训练阶段的第一步,我们用一个简单的例子来训练,比如把“merci”翻译成“thanks”。
这意味着,我们想要输出一个表示单词“thanks”的概率分布。但是因为这个模型现在还没有训练,所以这还不太可能会发生。
因为模型参数(权重)全部被随机初始化,所以模型(未训练)为每一个单元格/单词生成一个具有任意值的概率分布。我们把它和真实输出比较,然后使用反向传播调整模型的所有权重使其输出更接近预期输出。
你怎么比较两个概率分布?我们简单地从一个概率分布中减去另一个。有关细节,请查看交叉熵(cross-entropy)和Kullback-Leibler 散度(Kullback-Leibler divergence)。
但是注意,这是一个过于简化的例子。实际上,我们应使用长于一个单词的句子,举个例子 —— 输入:“je suis étudiant”和预期输出:“i am a student”。这意味着,我们想要我们的模型连续输出一个概率分布:
- 每个概率分布都表示为宽度为vocab_size的向量(在我们的例子中是6,但是实际上向量宽度应该为30,000或者50,000)
- 第一个概率分布对关联单词“i”的单元格有更高的概率
- 第二个概率分布对关联单词“am”的单元格有更高的概率
- 以此类推,知道第五个输出分布表明
<end of sentence>
标识,它也有从10,000格元素词汇表中关联它的对应单元格。
我们再次用一个样本语句训练的模型的目标概率分布
在使用足够大的数据集和足够时间去训练模型后,我们希望生成的概率分布像下面一样:
希望通过训练,模型会输出我们期望的正确的翻译。当然,这并不能表明这个短语是否是训练数据集的一部分(参看:交叉验证(cross validation))。注意每个位置都得到了一点可能性,甚至它不像这次(time step)的输出 —— 这是softmax一个非常有用的属性,它可以促进训练进程。
现在,因为模型一次生成一个输出,我们假设模型正在通过概率分布的最高概率选择单词并且丢弃其他单词。这是一种方式去做(被称作贪婪解码(greedy decoding))。另外一种方法是抓住前两个单词(例如“I”, “a”),然后在下一步中,运行模型两次:一次假设第一个输出位置是单词“I”,另一次假设第一次输出位置是单词“a”,考虑到位置#1和#2保持不变,所以无论那个版本都会产生较小的错误。对于位置#2和#3等等...我们重复这样的操作。这个方法称为“bean search”,在我们的例子中,beam_size是2(意味着在任何时候,两个部分的假设(未完成的翻译)都被保存在内存中),top_beams是2(意味着我们将返回两个翻译)。这两个超参数都可以进行实验。
向着Transformer继续前进
我希望你能从这里发现有用的信息帮助你快速启程,同时能融化你对Transformer的困惑的冰山一角。如果你想要深入了解,我建议这些:
- 阅读论文 《Attention Is All You Need》 ,Transformer博客 Transformer: A Novel Neural Network Architecture for Language Understanding 和 Tensor2Tensor announcement。
- 观看 Lukasz Kaiser的演讲 阅览模型和它的细节
- 动手尝试 Jupyter Notebook provided as part of the Tensor2Tensor repo。
- 浏览 Tensor2Tensor repo。
Follow-up works:
- Depthwise Separable Convolutions for Neural Machine Translation
- One Model To Learn Them All
- Discrete Autoencoders for Sequence Models
- Generating Wikipedia by Summarizing Long Sequences
- Image Transformer
- Training Tips for the Transformer Model
- Self-Attention with Relative Position Representations
- Fast Decoding in Sequence Models using Discrete Latent Variables
- Adafactor: Adaptive Learning Rates with Sublinear Memory Cost
Acknowledgements
Thanks to Illia Polosukhin, Jakob Uszkoreit, Llion Jones , Lukasz Kaiser, Niki Parmar, and Noam Shazeer for providing feedback on earlier versions of this post.
Please hit me up on Twitter for any corrections or feedback.
Written on June 27, 2018
译者注
[1] Transformer也可以被看作一种模型体系结构,再大点说,《模型架构》
[2] 把文本映射为向量,这个过程被称为embeddings,该做法旨在便于做“相关性”计算
[3] 即每个单词在这个位置出现的概率
[4] step表示单个步骤,比如编码器栈中流经一个编码器为一个step;time step 表示整个流程作为单个步骤,比如流经整个编码器栈就是一个time step