前沿

注意力(attention)--是一个在深度学习模型中普遍使用的方法。注意力是一个帮助改善神经机器翻译应用性能的概念。在这篇博客中,我会著眼于Transformer--该模型运用注意力来提高训练这些模型的速度。Transformer在一些特定任务中性能表现超过Google Neural Machine。然而,最大的益处在于Transformer并行化运算。事实上,Google Cloud的建议是使用Transformer作为参考模型来使用他们的Cloud TPU产品。因此让我们尝试拆解模型来看看它是怎么工作的。

Transformer在Attention is All You Need这篇论文中被提出。它的一个tensorflow版本实现可以在Tensor2Tensor得到。哈佛大学的NLP小组写了pytorch的实现guide annotating the paper with PyTorh implementation.在这篇博客中,我们尝试使一些东西简单化,逐一介绍概念,以便在没有深入了解主题的情况下让人们更容易理解。

从整体看

我们可以把这些模块看成单个黑盒。在机器翻译应用中,输入是一种语言的句子,输出翻译成另一种语言。

抛开其它的一切,我们看到了一个编码组件,一个解码组件以及他们之间的连接。

编码部分是编码器的堆叠(在这篇论文中堆叠了6个,在彼此之上一个一个互相叠加-数字6并没有什么奇特之处,你也可以在实验中尝试其他的数字)。解码部分是相同数目解码器的堆叠。

编码器在结构上完全相同(当然他们不共享权重)。每一个编码器分解成两个子层:

编码器的输入首先经过一个自注意力层-该层帮助编码器著眼于输入句子中其他的单词,因为它对特定单词编码。我们会在后面的部分详细介绍自注意力(self-attention)。

自注意力层的输出进入前向神经网路(feed-forward neural network)。完全相同的前馈网路独立应用于每个位置。

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

将张量图形化表示

既然我们已经看过了模型的主要部分,下面我们来看一下不同的矢量/张量以及它们是怎么在不同部分之间流动,来将训练模型的输入转换为输出。

与NLP应用中的情况一样,我们先利用embedding algorithm演算法,把每一个输入单词变成一个矢量。

单词嵌入只在最低端的编码器。它们接收每个大小为512的向量表-在最底部的编码器会进行单词嵌入,但是在其他编码器中,它是直接将位于下方的编码器输出。向量表的大小是一个我们可以设置的超参数,该参数是训练集中最长句子的长度。

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

在此,我们看到了Transformer的一个关键特征,即每个位置中的单词在编码器中流经自己的路径。在自注意力层这些路径存在依赖关系。前馈网路层不存在这些依赖关系,因此,各种路径可以在流过前馈层的同时并行执行。

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

编码器

就像我们上面提到的那样,一个编码器接收一系列矢量作为输入。它通过将这些向量传递到自注意力层来处理列表,然后进入前馈神经网路,接著把这些输出送到下一个编码器。

每一个位置处的单词都经过自编码过程。然后,它们各自通过一个前馈神经网路-完全相同的网路,每个向量分别流过它。

自注意力

不要因为我提出一个「自注意力」词语而被迷惑,这是每个人都应该熟悉的概念。我自己从来没有碰到过这个概念直到阅读了Attention is All You Need 这篇论文。让我们探究一下它是如何工作的。

下面的句子是一个我们想要翻译的输入语句:

「The animal didnt cross the street because it was too tired」

it在句子中指代什么?是指street还是animal?对人类来说这是一个简单的问题,但是对演算法来说不简单。

当模型处理单词「it」时,自注意力允许it和animal联系起来。

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

如果你对RNN熟悉,请考虑如何保持隐藏状态允许RNN将其已处理的先前单词/向量的表示与其正在处理的当前单词/向量合并。自注意力是Transformer用于将其他相关单词的「理解」 bake到我们当前正在处理的单词中。

当我们在编码器中编码单词it时(最上面的编码器),一部分的注意力机制正在关注「The Animal」,然后其将一部分注意力影响到「it」的编码中。

检查Tensor2Tensor notebook确保你可以载入Transformer模型,并使用此互动式可视化对其进行检查。

详解自注意力

首先看一下如何利用向量计算自注意力,然后继续看它是如何用矩阵实现的。

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

注意到这些新的向量在维度上小于嵌入向量。它们的维度是64,而单词嵌入和编码器输入输出向量是512维。这个维度不必要变小,这是一种架构选择,可以使多头注意力的计算量保持不变。

将x1乘以权重矩阵WQ会产生q1,query向量和与输入对应的那个词有关,我们最终为输入句子中每个单词创建query,key,value投影。

query,key和value向量是什么?

它们是摘要信息,有助于计算和思考注意力。继续阅读以下计算自注意力的方法。你几乎可以了解这些向量所扮演的角色。

第二步,计算自注意力就是计算得分(score)。假设我们正在计算例子中的第一个单词「Thinking」的自注意力。我们需要根据这个单词对输入句子的每个单词进行评分。这个分数决定当我们在某个位置编码单词时,应该对输入句子中的其他部分放置多大的焦点。

通过将query向量和相应单词的key向量点积来计算得分。因此,如果我们处理位置1处的单词的自注意力,第一个分数就是q1和k1的点积。第二个分数就是q1和k2的点积。

第三步和第四步,把这个分数除以8(在论文中,key向量维度64的平方根=8,如果是64,这会导致更平滑的梯度,因为当取值较大时,softmax的梯度比较小。所以它求了平方根。这个值可以是其他可能的值,64只是一个默认值),然后把结果进行softmax操作。softmax归一化这些分数,保证它们都是正数并且和为1.

该softmax分数决定每个单词在该位置受关注的程度。很明显,这个位置上的单词将具有最高的softmax分数,但有时候关注与当前单词相关的另一个单词是有用的。

第五步,每一个value向量乘以softmax分数(为了求和做准备)。这里的直觉是,保持我们关注单词的原样,淹没了无关的单词(例如,将它们乘以0.001之类的微小数字)。

第六步,value向量加权求和。这会在此位置(对于第一个单词)产生自注意力层的输出。

这就是自注意力计算的结论。得到的向量是我们可以发送到前馈神经网路的向量。在实际的实现中,为了快速计算,采用矩阵乘法来计算。因此,下面我们看一下矩阵是怎么运算的。

自注意力中的矩阵运算

第一步,计算Query,Key和Value矩阵。我们通过把多个embedding向量整合成一个矩阵X,然后将矩阵X乘以训练过的权重矩阵(WQ,WK,WV).

矩阵X中的每一行对应于输入句子中的一个单词。图中的4个方块在实际中表示512维,q/k/v向量是64维。

最终,由于我们用矩阵处理,我们可以在一个公式中把6个过程压缩为2个来计算自注意力层的输出。

多头注意力(multi-headed)

这篇论文通过增加一种称为「多头注意力」机制,进一步完善了自注意力层。这在两方面提高了注意力层的性能:

  1. 它增大了模型聚焦不同位置的能力。当然在上面的例子中,z1包含了一些其他词的编码,但它可以由实际的单词本身支配。当我们翻译句子「The animal didnt cross the street because it was too tired」,我们想知道it指代什么?
  2. 它提供注意力层多重的「表示子空间」(representation subspaces)。正如我们将在下面看到的一样,用多头注意力,我们有多组Query/Key/Value权重矩阵(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"

如果我们把所有注意力头加在图片上,那么看起来可能更难理解:

用Positional Encoding表示序列的顺序

正如我们目前所描述的,该模型缺少统计输入序列中单词顺序的方式。 要解决这个问题,transformer为每一个输入嵌入增加了一个向量。这些向量遵循模型学习的特定模式,这有助于确定每个单词的位置,或者序列中不同单词之间的距离。这里的直觉是,将这些value添加到嵌入中,一旦它们被投影到Q/K/V向量中并且在点积注意力之间,就在嵌入向量之间提供有意义的距离。

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

所谓的特定模式是什么? 在接下来的图片中,每一行对应一个向量的位置编码。因此,第一行就是输入序列中的第一个单词的嵌入向量。每一行包含512个值--每个值的取值介于1和-1之间。我们对它们进行了颜色编码,使图案可见。

一个真实位置编码的例子,用512(列)维的嵌入编码20(行)个单词。你可以看到它在中心位置分成两半。那是因为左半部分用一个sin函数生成的数值,右半部分是另一个函数(cos)生成的数值。然后他们拼接一块形成每一个位置编码向量。

位置编码的公式在论文中3.5节描述。你可以在get_ timing_ signal_ld()看到位置编码的源码。不只有一种可能的位置编码方法。然而,它具有能够扩展到看不见的序列长度的优点。(例如,如果让训练过的模型翻译一个比我们在训练集中更长的句子)

残差连接

编码器结构中,我们需要提到的一个细节是每一个编码器的每一个子层(self-attention,ffnn)在其周围具有残差连接,然后是层标准化(layer-normalization)步骤。

如果我们将向量和与自注意相关的层标准化可视化,它将如下所示:

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

解码器端

现在我们已经涵盖了编码器方面的大多数概念,我们基本上知道了解码器的组件如何工作。现在我们看一下它们是如何一起工作的。

编码器通过处理输入序列开始。顶端编码器的输出转换为注意力向量K和V的集合。这些将由每个解码器在其「编码器-解码器注意力」层中使用,这有助于解码器关注输入序列中的适当位置:

完成编码阶段,我们开始进入解码阶段。解码阶段中的每个步骤从输出序列输出一个元素。

接下来的步骤重复这个过程,直到一个特殊的符号到达表明transformer解码器完成了输出。每一步的输出在下一个时间点被喂到底部的解码器,解码器就像编码器一样往上流动冒出解码结果。就像我们对编码器输入所做的那样,我们在这些解码器输入端嵌入并增加位置编码,来指示每个单词的位置。

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

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

「编码器--解码器注意力」层像多头自注意力一样工作,除了它从下面的层创建它的Queries矩阵,和从编码器输出中获取Keys和Value矩阵。

线性层和softmax层

解码器输出浮点型向量。我们怎么把它转化成一个单词?这是最终线性层的工作,其后是softmax层。

线性层是一个简单的全连接神经网路,把解码器的输出向量投影到一个更大、更大的logits向量。

我们假设我们的模型知道10000个独特的单词(模型的输出辞汇表),这些单词从训练集中学习得到。这将使logits向量10000个cell宽--每个cell对应于一个唯一单词的得分。这就是我们如何解释模型的输出,然后是线性层。

softmax层把这些得分转换成概率(都是正数,和为1)。概率最高的cell被选中,并且与其关联的单词将作为此时间步的输出。

该图从底部开始,产生的矢量作为解码器的输出。然后它变成了一个输出单词。

训练过程的扼要重述

既然我们通过训练过的Transformer涵盖了整个前向过程,看一下训练模型的直觉是有用的。

在训练过程中,一个未训练过的模型会经历同样的前向过程。但是因为我们在一个标注的训练集训练它,我们可以比较模型的输出和实际正确的输出。

为了可视化这个过程,假设我们输出辞汇只包含6个单词(a,am,I,thanks,student,和end of sentence的缩写)

模型的输出辞汇在预处理阶段被创建,这在我们开始训练之前。

一旦我们定义好输出辞汇,我们可以用一个同样宽度的向量指示我们辞汇表中的每一个单词。这也被称为one-hot编码。因此,举个例子,我们可以用下面的向量指示单词am:

下面接著讨论一下模型的损失函数--在训练过程中我们优化的准则,来训练出训练有素、令人惊奇的精确模型。

损失函数

假设我们正在训练我们的模型。假设我们现在处在训练阶段的第一步,我们在一个简单的例子上训练它--把「merci」翻译成「thanks」。

这意味著,我们想输出一个概率分布来指示单词「thanks」。但是因为这个模型还没有被训练,这还不太可能发生。

因为模型的参数(权重)随机初始化,(未训练的)模型为每一个cell/word产生一个任意值的概率分布。我们可以与真实的输出比较,然后利用反向传播法调整模型的权重,来保证输出更接近期望的输出。

我们怎么比较两个概率分布呢?我们简单的用一个分布减去另一个。更多细节请看cross-entorpy和Kullback-Leibler divergence

需要注意的是,这是一个过度简化的例子。更真实地,我们可以用一个长于一个单词的句子。例如-输入「je suis étudiant」期望输出是「i am a student」。这实际意味著,我们希望我们的模型能够连续输出概率分布,其中:

  • 每一个概率分布用一个宽度为vocab_size(在下面的例子中为6,真实的例子中为3000或10000)的向量来表示。
  • 第一个概率分布在与单词「i」相关联的单元处具有最高概率
  • 第二个概率分布在与单词「am」相关联的单元处具有最高概率
  • 直到第5个输出概率指示符号,即使在10000个元素的单词表也有一个单元与它对应。

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

在一个足够大的数据集上训练足够的时间之后,我们希望产生的概率分布会是这样:

希望经过训练,模型会输出我们期望的正确的翻译。当然,这个短语是否是训练数据集的一部分没有真正的指示(见cross validation)。请注意,即使不太可能是该时间步的输出,每个位置都会获得一点概率--这是softmax函数非常有用的特性,这有利于训练过程。

现在,因为模型一次产生一个输出,我们假设模型从那些概率分布中选择具有最大概率分布的单词,并扔掉其余的部分。这是处理的一种方式(被称为greedy decoding)。另一种处理的方式是假设保留两个具有最高概率的单词(假设单词「I」和「a」),下一步,运行模型两次:一次假设第一个输出位置是单词「I」,另一次假设第一个输出单词是「me」,考虑位置#1和位置#2,保留产生最少误差的版本。我们在位置#2和位置#3重复同样的操作,以此类推。这种方法称为「beam search」,在我们的例子中,beam_size是2(因为我们在计算位置#1和位置#2的beams后比较了结果),并且top_beams也是2(因为我们保留了两个单词)。这些都是你可以试验的超参数。

参考

翻译自The Illustrated Transformer


推荐阅读:
相关文章