谈起自然语言,就不得不说到现在大火的bert以及openai gpt-2,但是在理解这些模型之前,我觉得首先应该了解transformer,因本人水平有限,在看了transformer的论文之后也一知半解,在分享今天的知识之前,我们先简单了解一下seq2seq
首先要说到seq2seq的发展历史,从单纯的RNN-RNN到LSTM-LSTM,再到BiLSTM-BiLSTM或者BiGRU-BiGRU,首先说到RNN的缺陷会导致梯度消失,LSTM-LSTM改进之后,解决了梯度消失的问题,后面发展是加了双向的BiLSTM-BiLSTM或者BiGRU-BiGRU
Ray Mooney,一个非常著名的计算机语义学家抱怨的说到,you cann't cram the meaning of whole %$& sentence into a single %$&* vector! 因为每句话长度不一,你不能让我把大量的信息塞进固定的向量中,这样让学习变得太困难了。
atttention后面开始产生,Transformer是第一个完全依赖于self-attention来计算其输入和输出表示的转换模型,而不使用序列对齐的RNNs或卷积。
传统的双向RNN模型图
今天翻译一篇非常优秀的详解transformer文章,感谢作者的详细讲解,本人翻译水平有限(英文水平好的可以直接看原文),文章篇幅较长,请耐心观看
https://jalammar.github.io/illustrated-transformer/
阐述transformer
在前一篇文章中,我们研究了Attention——现代深度学习模型中普遍存在的一种方法。Attention是一个有助于提高神经机器翻译应用程序性能的概念。在这篇文章中,我们将看看Transformer——一个使用Attention来提高这些模型训练速度的模型。在特定的任务中,Transformer的性能优于谷歌神经机翻译模型。然而,最大的好处来自于Transformer如何使自己适合并行化。事实上,谷歌云推荐使用Transformer作为参考模型来使用他们的Cloud-TPU产品。让我们试着把这个模型拆开看看它是如何运作的。
在Attention is all you need论文中提出的Transformer。它的一个TensorFlow实现可以作为Tensor2Tensor包的一部分。哈佛大学的NLP小组创建了一个使用PyTorch实现注释该论文的指南。在这篇文章中,我们将尝试把事情简单化一点,并逐一介绍概念,希望能够使那些对主题没有深入了解的人更容易理解。
让我们首先将模型看作一个单独的黑盒子。在机器翻译应用程序中,它将使用一种语言的一个句子,然后输出另一种语言的翻译。
打开the transformer,我们看到一个编码组件,一个解码组件,以及它们之间的连接。
编码组件是一堆编码器(论文将其中的6个堆叠在一起——数字6没有什么神奇之处,人们肯定可以尝试其他排列方式)。解码组件是一组相同数量的解码器。
编码器在结构上都是相同的(但它们不共享权重)。每一个都被分成两个子层:
编码器的输入首先通过一个self-attention层——这一层帮助编码器在编码特定单词时查看输入句子中的其他单词。我们将在稍后的文章中进一步研究self-attention。
将self-attention的输出反馈给前馈神经网络。完全相同的前馈网络独立地应用于每个位置。
解码器具有这两个层,但它们之间是一个 attention层,帮助解码器将注意力集中到输入语句的相关部分(类似于seq2seq模型中的注意)。
现在我们已经看到了模型的主要组件,让我们开始看看各种向量/张量,以及它们如何在这些组件之间通过,从而将经过训练的模型的输入转换为输出。
与一般的NLP应用程序一样,我们首先使用embedding algorithm将每个输入字转换为向量。
每个单词都嵌入到大小为512的向量中。我们用这些简单的盒子表示这些向量。
word embedding只发生在最下面的编码器。所有编码器共有的是,它们接收一个大小为512的向量列表——在底部的编码器中是word embedding的输入,但在其他编码器中,它是直接下面的编码器的输出作为输入。这个列表的大小是我们可以设置的超参数——基本上它是我们的训练数据集中最长的句子的长度。
在我们的输入序列中嵌入单词之后,每个单词都需通过编码器的两层。
在这里,我们开始看到transformer的一个关键属性,在编码器中,word的每个位置通过自己的路径,在self-attention层中,这些路径之间存在依赖关系。然而,前馈层没有这些依赖关系,因此可以在通过前馈层时并行执行各种路径。
接下来,我们将把示例转换成一个更短的句子,并查看在编码器的每个子层中发生了什么。
正如我们已经提到的,编码器接收一个向量列表作为输入。它通过将这些向量传递到一个“self-attention”层来处理这个列表,然后传递到一个前馈神经网络,然后将输出向上发送到下一个编码器。
每个位置上的单词都经过一个self-attention的过程。然后,它们各自通过一个前馈神经网络——完全相同的网 络,每个向量分别流经它。
别被我说的“Self-Attention”这个词骗了,好像这是一个每个人都应该熟悉的概念。我个人从来没有遇到过这个概念,直到阅读。the Attention is All You Need的论文,让我们来提炼一下它是如何工作的。
假设下面的句子是我们要翻译的输入句:
“The animal didn't cross the street because it was too tired”
这个句子中的it指的是什么?它指的是街道还是动物?这对人类来说是一个简单的问题,但对算法来说就没那么简单了。
当模型在处理“它”这个词时,自我注意使它能够把“它”和“动物”联系起来。
当模型处理每个单词(输入序列中的每个位置)时,self - attention允许它查看输入序列中的其他位置,以寻找有助于更好地编码这个单词的线索。
如果您熟悉RNNs,请考虑如何维护一个隐藏状态,使RNN能够将它处理过的前一个单词/向量的表示形式与它正在处理的当前单词/向量相结合。self - attention是Transformer用来将其他相关词汇的“理解”转化为我们当前正在处理的词汇的方法。
当我们在编码器#5(堆栈中的顶部编码器)中对“it”进行编码时,self - attention机制的一部分集中在“动物”上,并将其表示形式的一部分融入到“it”的编码中。
确信查阅 Tensor2Tensor notebook,在那里您可以加载一个Transformer模型,并使用这个交互式可视化检查它。
让我们先来看看如何使用向量来计算self-attention,然后再来看看它是如何实现的——使用矩阵。
计算自我注意的第一步是从每个编码器的输入向量(在本例中,是每个单词的embedding)创建三个向量。因此,对于每个单词,我们创建一个Query向量、一个Key向量和一个Value向量。这些向量是通过将embedding乘以我们在训练过程中训练的三个矩阵得到的。
注意,这些新向量的维数比embedding向量小。其维数为64,而embedding和编码器的输入/输出向量维数为512。它们不必更小,这是一种架构选择,可以使多头注意力的计算(大部分)保持不变。
x1乘以WQ权重矩阵得到q1,即与该单词相关的“Query”向量。我们最终为输入语句中的每个单词创建一个“Query”、一个“Key”和一个“Value”投影。
什么是“Query”、“Key”和“Value”向量?
它们是对计算和思考attention很有用的抽象概念。一旦你开始阅读下面的attention是如何计算的,你就会知道你需要知道的关于这些向量所扮演的角色。计算 self-attention的第二步是计算分数。假设我们在计算这个例子中第一个单词的self-attention,“Thinking”。我们需要用这个单词给输入句子中的每个单词打分。当我们在某个位置编码一个单词时,分数决定了我们要把多少注意力放在输入句子的其他部分。
评分是通过Query向量与我们评分的单词的Key向量的点积来计算的。如果我们处理位置1的单词的self-attention,第一个分数就是q1和k1的点积。第二个分数是q1和k2的点积。
第三步和第四步是将分数除以8(论文中使用的关键向量的维数的平方根- 64)。这导致了更稳定的梯度。这里可能有其他可能的值,但这是默认值),然后通过softmax操作传递结果。Softmax将这些分数标准化,使它们都是正的,加起来等于1。
这个softmax分数决定了每个单词在这个位置的表达量。很明显,这个位置的单词将拥有最高的softmax分数,但有时关注与当前单词相关的另一个单词是有用的。
第五步是将每个Value向量乘以softmax分数(准备对它们求和)。这里的直觉是保持我们想要关注的单词的值不变,去掉无关单词(例如,将它们乘以0.001这样的小数字)。
第六步是对加权Value向量求和。这将在此位置生成self-attention层的输出(对于第一个单词)。
这就是self-attention计算的结论。得到的向量是一个我们可以发送到前馈神经网络的向量。然而,在实际实现中,为了加快处理速度,这种计算是以矩阵的形式进行的。现在我们来看看这个我们已经看到了计算的直观感觉。
第一步是计算Query”、“Key”和“Value”矩阵。我们将embedding打包到矩阵X中,并将其乘以我们训练过的权重矩阵(WQ, WK, WV)。
X矩阵中的每一行对应于输入句子中的一个单词。我们再次看到嵌入向量(图中512个或4个框)和q/k/v向量(图中64个或3个框)大小的差异
最后,由于我们在处理矩阵,我们可以将步骤2到步骤6浓缩成一个公式来计算self-attention层的输出。
self-attention矩阵形式的计算
本文进一步细化了自我注意层,增加了一个称为 “multi-headed” attention的机制。这在两个方面提高了attention层的性能:
1. 它扩展了模型聚焦于不同位置的能力。是的,在上面的例子中,z1包含所有其他编码的一小部分,但是它可以由实际单词本身控制。如果我们翻译一个句子,比如“The animal didn 't cross The street because It was too tired”,我们想知道“It”指的是哪个单词,这将会很有用。
2. 它赋予attention层多个“表示子空间”。接下来我们将看到,对于multi-headed attention,我们不仅有一个,而且有多个Query/Key/Value权重矩阵集(transformer使用8个注意头,因此我们最终为每个编码器/解码器提供8个注意头集)。每个集合都是随机初始化的。然后,经过训练,每个集合用于将输入embeddings(或来自较低编码器/解码器的向量)投影到不同的表示子空间中。
在多头 attention的情况下,我们对每个头保持独立的Q/K/V权重矩阵,从而得到不同的Q/K/V矩阵。和之前一样,我们用X乘以WQ/WK/WV矩阵得到Q/K/V矩阵。
如果我们做我们上面列出的同样的 self-attention计算,只是8次不同的加权矩阵,我们得到8个不同的Z矩阵
这给我们带来了一些挑战。前馈层不需要8个矩阵——它只需要一个矩阵(每个单词对应一个向量)。所以我们需要一种方法把这8个压缩成一个矩阵。
我们怎么做呢?我们把这些矩阵简化,然后用一个额外的权值矩阵WO乘以它们。
这就是multi-headed self-attention,我知道有很当多的矩阵。让我试着把它们都放在一个视觉上这样我们就能在一个地方看到它们
既然我们已经接触到了attention头部,让我们回顾一下之前的例子,看看当我们在例句中编码单词“it”时,不同的注意力头部集中在哪里:
当我们编码“it”这个词时,一个注意力集中在“the animal”上,而另一个则集中在“tired”上——从某种意义上说,模型对“它”这个词的表达,在“the animal”和“tired”的一些表达中都有体现。
然而,如果我们把所有的attention头部都放在图片上,事情就会变得更难解释了:
到目前为止,我们所描述的模型缺少的一件事是解释输入序列中单词顺序的方法。
为了解决这个问题,transformer向每个输入的embedding添加一个向量。这些向量遵循模型学习的特定模式,这有助于确定每个单词的位置,或序列中不同单词之间的距离。这里的直觉是,将这些值添加到embedding中,在embedding向量被投影到Q/K/V向量和点积attention期间,它们之间提供了有意义的距离。
为了让模型了解单词的顺序,我们添加了位置编码向量——其值遵循特定的模式。
如果我们假设embedding的维数是4,那么实际的位置编码应该是这样的:
一个实例关于位置编码embedding大小为4
这种模式会是什么样的呢?
在下面的图中,每一行对应一个向量的位置编码。所以第一行就是我们要添加到输入序列中第一个单词的embedding中的向量。每一行包含512个值——每个值的值介于1和-1之间。我们用不同的颜色来表示它们,这样图案就可以看到了。
这是一个实际的位置编码示例,用于embedding大小为512(列)的20个单词(行)。你可以看到它从中间一分为二。这是因为左半部分的值由一个函数(使用正弦)生成,而右半部分由另一个函数(使用余弦)生成。然后将它们连接起来,形成每个位置编码向量。
本文(第3.5节)描述了位置编码的公式。您可以在get_timing_signal_1d()中看到用于生成位置编码的代码。这不是唯一可能的位置编码方法。然而,它的优势在于能够扩展到不可见的序列长度(例如,如果我们的训练模型被要求翻译一个比训练集中任何一个句子都长的句子)。
在继续之前,我们需要在编码器体系结构中提到的一个细节是,每个编码器中的每个子层(self-attention, ffnn)周围都有一个残差连接,然后是layer-normalization步骤。
如果我们将向量和与self-attention相关的layer-norm运算形象化,它会是这样的:
这也适用于解码器的子层。如果我们考虑一个由两个堆叠的编码器和解码器组成的Transformer,它应该是这样的:
既然我们已经涵盖了编码器方面的大部分概念,我们基本上也知道解码器的组件是如何工作的。但让我们来看看它们是如何协同工作的。
编码器首先处理输入序列。然后将顶层编码器的输出转换为一组attention向量K和v,分别由其“encoder-decoder attention”层中的各个解码器使用,帮助解码器在输入序列中聚焦于适当的位置:
在完成编码阶段后,我们开始解码阶段。解码阶段的每一步都从输出序列中输出一个元素(本例中为英语翻译句)。
以下步骤重复此过程,直到到达一个特殊符号,表示 transformer解码器已完成其输出。每一步的输出在下一个时间步中被提供给底层解码器,解码器就像编码器一样弹出解码结果。就像我们处理编码器输入一样,我们嵌入并添加位置编码到那些解码器输入中来表示每个单词的位置。
解码器中的self attention层的工作方式与编码器中的略有不同:
在解码器中,self attention层只允许处理输出序列中较早的位置。这是通过在self attention计算的softmax步骤之前屏蔽将来的位置(将它们设置为-inf)来实现的。
“Encoder-Decoder Attention”层的工作原理与multiheaded self-attention类似,只是它从其下一层创建Queries矩阵,并从编码器堆栈的输出中获取 Keys和Values矩阵。
解码器堆栈输出浮点数向量。我们怎么把它变成一个单词呢?这是最后一个线性层的工作,然后是一个Softmax层。
线性层是一个简单的全连接的神经网络,它将解码堆栈产生的向量投射到一个更大的向量上,称为logits向量。
让我们假设我们的模型知道10,000个独特的英语单词(我们的模型的“输出词汇”),这些单词是从它的训练数据集中学到的。这将使logits向量宽10,000个单元格——每个单元格对应一个惟一单词的得分。这就是我们如何解释线性层之后的模型输出。
然后softmax层将这些分数转换为概率(所有为正,所有加起来等于1.0)。选择概率最高的单元格,并生成与之关联的单词作为这个时间步骤的输出。
这个图从底部开始,向量作为解码器堆栈的输出。然后将其转换为输出字。
既然我们已经介绍了通过一个经过训练的Transformer的整个前向传递过程,那么了解一下训练模型的直观感受是很有用的。
在训练期间,一个未经训练的模型将通过完全确定相同的向前。但是,由于我们在标记的训练数据集中训练它,所以我们可以将它的输出与实际正确的输出进行比较。
为了形象化,我们假设输出词汇表只包含6个单词(“a”、“am”、“i”、“thanks”、“student”和“
我们的模型的输出词汇表是在我们开始训练之前的预处理阶段创建的。
一旦定义了输出词汇表,就可以使用相同宽度的向量来表示词汇表中的每个单词。这也称为one-hot编码。例如,我们可以用以下向量表示单词“am”:
示例:输出词汇表的one-hot编码
在这篇综述之后,让我们来讨论模型的损失函数——我们在训练阶段优化的度量标准,以得到一个经过训练的、希望非常准确的模型。
假设我们正在训练我们的模型。假设这是我们在训练阶段的第一步,我们正在用一个简单的例子进行训练——将“merci”翻译成“thanks”。
这意味着,我们希望输出是一个表示“谢谢”的概率分布。但由于这个模型还没有经过训练,目前还不太可能实现。
由于模型的参数(权重)都是随机初始化的,因此(未经训练的)模型为每个单元格/单词生成具有任意值的概率分布。我们可以将它与实际输出进行比较,然后使用反向传播调整模型的所有权重,使输出更接近所需的输出。
如何比较两个概率分布?我们只是简单地把一个减去另一个。要了解更多细节,请查看交叉熵和Kullback-Leibler散度。
但请注意,这是一个过于简化的示例。更实际地说,我们将使用一个比一个单词更长的句子。例如,输入:“je suis etudiant”,期望输出:“i am a student”。这实际上意味着,我们希望我们的模型连续输出概率分布,其中:
我们将在一个示例句的训练示例中针对目标概率分布训练我们的模型。
在足够大的数据集上对模型进行足够长的时间的训练后,我们希望生成的概率分布是这样的:
希望通过训练,模型能够输出我们期望的正确翻译。当然,如果这个短语是训练数据集的一部分,那么它就没有真正的指示意义(参见:交叉验证)。注意每个位置都有一点概率即使它不太可能是那个时间步长的输出——这是softmax的一个非常有用的属性,它有助于训练过程。
现在,因为这个模型一次产生一个输出,我们可以假设这个模型从这个概率分布中选择了概率最高的单词然后扔掉了剩下的。这是一种方法(称为贪婪解码)。另一个方法是坚持说,前两个单词(比如“i”和“a”),然后在下一步中,运行模型两次:一次假设第一个输出位置是“i”这个词,而另一个假设第一个输出位置是‘me’这个词,和哪个版本产生更少的错误考虑# 1和# 2保存位置。我们对位置#2和位置#3重复同样的做法,等等。这种方法称为“beam search”,在我们的示例中,beam_size是2(因为我们在计算了位置#1和#2的beams之后比较了结果),top_beam也是2(因为我们保留了两个单词)。这两个都是可以进行实验的超参数。
I hope you’ve found this a useful place to start to break the ice with the major concepts of the Transformer. If you want to go deeper, I’d suggest these next steps:
Follow-up works:
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