大抵是去年底吧,收到了几个公众号读者的信息,希望能写几篇介绍下Attention以及Transformer相关的算法的文章,当时的我也是满口答应了,但是确实最后耽误到了现在也没有写。
前一阵打算写这方面的文章,不过发现一个问题,就是如果要介绍Transformer,则必须先介绍Self Attention,亦必须介绍下Attention,以及Encoder-Decoder框架,以及GRU、LSTM、RNN和CNN,所以开始漫长的写作之旅。
终于在这个五一假期完成了最后的四篇文章(总共八篇,从开始写到现在历经两个月左右的时间),也算是兑现了自己的承诺,俗话说的好,言必行,行必果吧。尤其是对于我这么一个爱好脸面的人,就更得兑现了。最近有一些感悟,很多事情有了很多不同的看法和理解。以前读《易经》觉得自己读懂了一些,现在看来压根是一点都没懂,而且还偏离的厉害,有些事情可能得重新去审视,有些念头需要重新去整理了。
其实吧,有时候不想写这些文章,这种大基本功的文章,可以说是投入产出比极低的。人们往往追求酷炫的技术,沉醉于貌似的理解中,但是对于这种大基本功缺视而不见,殊不知所有的酷炫如果没有大基本功都是空中楼阁。而且能够构建出这些酷炫技术的人,一定是基本功十分扎实的人,对于算法同学来说,不仅仅止于算法,还需要扎实深刻的架构知识。
好了,书归正传,截止目前,已经完成几篇文章的输出,罗列如下,组织方式都是按照学习的依赖进行构建的,喜欢的同学可以关于《秃顶的码农》公众号,选择机器学习算法系列进行学习。
终于把这个系列写到了最后一文,五一五天假期的第四天,嗯,怎么说呢!感觉自己过的很充实。在这里炫耀下,昨天买了个mac的触摸板,比起鼠标来,还真是好用,配合上我的大显示器、机器键盘,妥妥的提升生产力,我想我还能写几百篇 哈哈。
在前一篇文章中,我们讨论了注意力——现代深度学习模型中普遍存在的方法。注意力是一个有助于提高神经机器翻译应用程序性能的机制(并行化)。在这篇文章中,我们将讨论Transformer —— 利用注意力来提高训练速度的模型。Transformer在特定任务中优于谷歌神经机器翻译模型。其中最大的提升在于Transformer架构适合于并行化。事实上,谷歌Cloud建议使用Transformer作为参考模型来使用他们的云TPU产品。让我们把这个模型拆开来看看它是如何运作的。
Transformer是在Attention is all you need这篇论文中提出的,并且引起了业界的广泛的关注,仿佛你要不提下、不知道Transformer你就不是做算法的,可见其强大的影响力。
以语言机器翻译模型为例,从整体的角度来看在机器翻译过程中,流程主要是输入一种语言的一个句子,然后输出另一种语言的相应的译文。
前面的章节中,我们已经介绍过Encoder-Decoder框架,他主要包含量大组件:
通过编码组件与解码组件完成整个框架的搭建,进行模型的训练与预测。
下面我看一个例子,编码组件是一堆编码器(有六个编码器,一个叠一个——数字6没有什么神奇的,你肯定可以尝试其他的安排)。解码组件是具有相同数字的解码器的排列。
编码器在结构上都是相同的(但它们不共享权重)。每一个都被分成两个子层:
解码器有两层:
现在我们已经看到了模型的主要组成部分,让我们开始研究各种矢量/张量,以及它们如何在这些组成部分之间流动,从而将训练模型的输入转换为输出。
与一般的NLP应用程序一样,我们首先使用嵌入算法将每个输入单词转换为一个向量。
嵌入只发生在最底部的编码器中。所有编码器的共同抽象是,它们接收一个大小为512的向量列表——在底部的编码器中,这个词是“嵌入”,但在其他编码器中,它将是直接下面的编码器的输出。
在输入序列中嵌入单词后,每个单词都流经编码器的两层。
在这里,我们开始看到Transformer的一个关键属性,即每个位置的单词在编码器中都通过自己的路径流动。在自我注意层中,这些路径之间存在依赖关系。然而,前馈层没有这些依赖关系,因此各种路径可以在流经前馈层时并行执行。
正如我们已经提到的,编码器接收一个向量列表作为输入。它处理这个向量,将这些向量传递到“自我关注”层,然后进入前馈神经网络,然后将输出向上发送到下一个编码器,编码器可以是多层的。
上一篇文章已经介绍过Self Attention,这里从另外的角度进行下阐述。其实注意力机制在现实生活中是大量存在的,而且人类社会也在一直使用。让我们提炼一下它是如何工作的。
假设下面的句子是我们想要翻译的输入句子:
“动物没有过马路,因为它太累了。”
这个句子中的“它”指的是什么?它指的是街道还是动物?这对人类来说是一个简单的问题,但对算法来说就不那么简单了。
当使用自我注意力机制的时候,模型处理“它”这个词时,自我注意力让它将“它”与“动物”联系起来。
当模型处理每个单词(输入序列中的每个位置)时,自我注意允许它查看输入序列中的其他位置,以寻找有助于对该单词进行更好编码的线索。
如果你对RNN很熟悉,想想如何维护一个隐藏状态让RNN将它之前处理过的单词/向量的表示与当前正在处理的单词/向量结合起来。自我关注是Transformer用来将其他相关词汇的“理解”融入到我们当前处理的词汇中,同样可以达到这个目标,同时由于其算法的特点可以无视距离,并且实现并行化的操作,提升计算性能。
本节我们再复习下如何计算Self-Attention,本节不做过细的介绍,一律使用矩阵计算。
计算自我注意力的第一步是根据编码器的每个输入向量创建三个向量(在本例中,在训练过程中通过三个参数矩阵计算得到)。
这些新的向量比嵌入向量的维度要小。它们的维数为64,而嵌入向量和编码器输入输出向量的维数为512。向量的维度可以由大家自行定义,这里使用64。
什么是“查询”、“键”和“值”向量?
它们是对计算和思考注意力有用的抽象概念。一旦你继续阅读下面是如何计算注意力的,你就会差不多知道所有你需要知道的关于每个向量所起的作用。
计算自我注意力的第二步是计算分数。假设我们在计算这个例子中第一个单词“Thinking”的自我注意力。我们需要将输入句子中的每个单词与这个单词进行比较。分数决定了当我们在某个位置编码一个单词时,将多少注意力放在输入句子的其他部分。
分数是通过查询向量与我们打分的单词的关键向量的点积来计算的。如果我们处理位置1的单词的自我注意,第一个分数就是q1和k1的点积。第二个分数是q1和k2的点积。
第三步和第四步是将分数除以8(论文中使用的关键向量维数的平方根- 64),据说这样可以拥有更稳定的梯度。然后通过softmax操作传递结果。Softmax将这些分数归一化,使它们都是正的,加起来等于1。
这个softmax分数决定了在这个位置每个单词被歧途单词影响的程度。显然,在这个位置的单词将拥有最高的softmax分数,但有时关注与当前单词相关的另一个单词是非常有用的。
第五步是将每个值向量乘以softmax得分(准备将它们相加)。这里的直觉是保留我们想要关注的单词的值,并淹没不相关的单词(例如,通过将它们乘以很小的数字,如0.001),然后进行按元素相加,生成最终的表征向量。
第六步是对加权值向量求和。这在这个位置产生了自我注意层的输出(对于第一个单词)。
第一步是计算Query、Key和Value矩阵。我们通过将我们的嵌入投射到一个矩阵X中,并将其乘以我们训练过的权重矩阵(WQ, WK, WV)来实现。
最后可以将第二步到第六步压缩到一个公式中,以计算自我注意层的输出。
本文进一步细化了自我注意层,增加了“多头”注意力机制。这从两个方面提高了注意力层的性能:
它扩展了模型关注不同位置的能力。是的,在上面的例子中,z1包含了一些其他的编码,但是它可以被实际的单词本身支配。如果我们在翻译像“The animal didn 't cross The street because It was too tired”这样的句子,我们会想知道“It”指的是哪个词,这是很有用的。
它给了注意层多个“表示子空间”。正如我们接下来将看到的,对于多头部注意,我们不仅有一个,而且有多个查询/键/值权重矩阵集(Transformer使用8个注意头部,因此我们最终为每个编码器/解码器提供8个注意头部集)。每个集合都是随机初始化的。然后,经过训练,每个集合用于将输入嵌入(或来自较低编码器/解码器的向量)投影到不同的表示子空间,通过不同的子空间挖掘更多的相互之间的关系。
如果我们做同样的自我注意计算,只是用不同的权重矩阵进行8次不同的计算,我们就得到了8个不同的Z矩阵。
这给我们留下了一点挑战。前馈层期望的不是8个矩阵——它期望的是单个矩阵(每个单词对应一个向量)。所以我们需要一种方法把这8个压缩成一个矩阵。
怎么做呢?我们将这些矩阵连接然后将它们乘以一个附加的权重矩阵WO。
我们将整体的流程合在一个图中,如下所示。
正如我们目前所描述的,该模型缺少的一件事是解释输入序列中单词的顺序。Self—Attention无视距离,无视空间,但是位置信息还是非常重要的,下面我们看看如何解决这个问题。
为了解决这个问题,转换器在每个输入嵌入中添加一个向量。这些向量遵循模型学习的特定模式,这有助于模型确定每个单词的位置,或序列中不同单词之间的距离。这里的直觉是,将这些值添加到嵌入中,一旦它们被投射到Q/K/V向量中,在点积注意期间,就可以在嵌入向量之间提供有意义的距离。
如果我们假设嵌入的维度是4,实际的位置编码应该是这样的:
这个模式会是怎样的呢?
在下面的图中,每一行对应于一个向量的位置编码。所以第一行就是我们在输入序列中嵌入第一个单词时要加上的向量。每行包含512个值—每个值都在1到-1之间。我们用颜色标记了它们,这样图案就清晰可见了。
位置编码公式在Transformer的论文中描述(第3.5节)。您可以在get_timing_signal_1d()https://github.com/tensorflow/tensor2tensor/blob/23bd23b9830059fbc349381b70d9429b5c40a139/tensor2tensor/layers/common_attention.py中看到用于生成位置编码的代码。这不是位置编码的唯一可行方法。然而,它的优势在于能够缩放到序列中看不见的长度(例如,如果我们训练过的模型被要求翻译一个比我们训练集中的任何句子都长的句子)。
2020年7月更新:上面显示的位置编码来自Transformer的transformer2transformer实现。本文所示的方法略有不同,它不是直接串联,而是交织两个信号。下图显示了它的样子。下面是生成它的代码:https://github.com/jalammar/jalammar.github.io/blob/master/notebookes/transformer/transformer_positional_encoding_graph.ipynb
介绍到这里,大家可能有个疑问,那就是Transformer的Loss是如何计算的?下面就一起探讨下:
假设我们正在训练我们的模型,这是我们训练阶段的第一步,我们正在用一个简单的例子来训练它——把“merci”翻译成“thanks”。
这意味着,我们希望输出是一个概率分布,表示“谢谢”这个词。但由于这个模型还没有经过训练,所以目前不太可能发生这种情况。
如何比较两个概率分布?我们只是简单地用一个减去另一个。要了解更多细节,请看交叉熵和Kullback-Leibler散度。
但请注意,这是一个过于简化的示例。更现实地说,我们会使用比一个单词更长的句子。例如-输入:“je suis étudiant”,期望输出:“i am a student”。这实际上意味着,我们希望我们的模型连续输出概率分布,其中:
在一个足够大的数据集上训练模型足够长的时间后,可能生成的概率分布如下:
现在,由于该模型每次产生一个输出,我们可以假设该模型从该概率分布中选择了概率最高的单词,而丢弃了其余的。这是一种方法(称为贪婪解码)。另一种方法是,抓住最前面的两个单词(例如,‘I’和‘a’),然后在下一步中,运行模型两次:一次假设第一个输出位置是单词‘I’,另一次假设第一个输出位置是单词‘a’,并且考虑到位置#1和#2,保留产生较少错误的版本。我们对2号和3号位置重复这个动作。这个方法被称为“beam search”,在我们的例子中,beam_size为2(这意味着在任何时候,内存中都保留着两个部分假设。还有top_beams,这两个都是可以试验的超参数。
介绍:杜宝坤,互联网行业从业者,十五年老兵。精通搜广推架构与算法,并且从0到1带领团队构建了京东的联邦学习解决方案9N-FL,同时主导了联邦学习框架与联邦开门红业务。
个人比较喜欢学习新东西,乐于钻研技术。基于从全链路思考与决策技术规划的考量,研究的领域比较多,从工程架构、大数据到机器学习算法与算法框架、隐私计算均有涉及。欢迎喜欢技术的同学和我交流,邮箱:[email protected]
自己撰写博客已经很长一段时间了,由于个人涉猎的技术领域比较多,所以对高并发与高性能、分布式、传统机器学习算法与框架、深度学习算法与框架、密码安全、隐私计算、联邦学习、大数据等都有涉及。主导过多个大项目包括零售的联邦学习,社区做过多次分享,另外自己坚持写原创博客,多篇文章有过万的阅读。公众号秃顶的码农大家可以按照话题进行连续阅读,里面的章节我都做过按照学习路线的排序,话题就是公众号里面下面的标红的这个,大家点击去就可以看本话题下的多篇文章了,比如下图(话题分为:一、隐私计算 二、联邦学习 三、机器学习框架 四、机器学习算法 五、高性能计算 六、广告算法 七、程序人生),知乎号同理关注专利即可。
一切有为法,如梦幻泡影,如露亦如电,应作如是观。