写在前面
最近又重温了一下沐神的论文精读系列,发现实在是干货满满,可以说是全程无废话了,每句话都具有价值,但是经常看了一边过后会忘记一些内容,再去回过头来翻看视频就需要花费一些时间来定位到你想要看的位置,所以便萌生了在看视频的时候把沐神说的话以及画的图给记录下来,方便后续自己进行回顾。正好现在许多人也在找实习,希望对大家有所帮助。
本文全程为我一个字一个字手敲,尽量在保证正确的情况下还原沐神的语气,同时优化了一些句子的说法,但是难免还是会有记录上的错误,也希望大家可以指出。
视频链接:https://www.bilibili.com/video/BV1pu411o7BE/?spm_id_from=333.337.search-card.all.click&vd_source=b240310dffc2035fb9fa31343f4794fe
摘要
并行化更好、全靠注意力机制。把所有的循环层全部换成了multi-headed self-attention。一开始是在机器翻译这个小任务上做的,后面被BERT、GPT给带火出圈了,也用在了cv上。
Introduction
RNN
RNN的特点是什么,同时也是他的缺点是什么。在RNN里面,给你一个序列的话,他的计算是把这个序列从左往右移一步一步往前做。假设你的序列是一个句子的话,他就是一个一个词的看。
对第t个词。他会计算一个输出叫h_t,也叫它的隐藏状态。然后它的h_t是由前面一个词的隐藏状态h_t-1和当前第t个词本身决定的。这样他就可以把前面学到的历史信息呢,通过h_t-1放到当下,然后和当前的词做一些计算,然后得到输出,这也是RNN如何能够有效处理时序信息的一个关键之所在,他把之前的信息全部放在隐藏状态里面,然后一个一个放下去。
但他的问题也来自于这里,第一个就是它是时序,就是一步一步计算的过程,它比较难以并行。你在算第t个词的时候,算h_t的那个出处的时候呢,你必须要保证前面那个词的h_t-1输入完成了。假设你的句子有100个词的话,你得时许地算100步。导致你在时许上无法并行,现在在主流的GPU和那些加速器(TPU),大家都是成千上万个线程,你无法在这个上面并行的话导致你的并行度比较低,使得你在计算上性能比较差。第二个也是这个原因,你得历史信息是一步一步的往后传递的,如果你的时序比较长的话,那么你在很早期的那些时序信息啊,在后面的时候可能会丢掉,如果你不想丢掉的话,那你可能要h_t要比较大。但是这个问题就是如果你做比较大的h_t,你在每一个时间步都得把它存下来,导致你的内存开销是比较大的。
attention在RNN上的应用
在这篇文章之前,attention已经被成功地用在编码器和解码器里面了,它主要是用在怎么样把编码器的东西很有效地传给解码器,主要是用在这一块。就是说你跟RNN是一起使用的。
Background
第一段是说如何使用卷积神经网络来替换掉你的循环神经网络,使得减少你的时序地计算,他提到了一些工作。他又说这些工作主要就是说,用卷积神经网络对于比较长的序列难以建模,这是因为我们知道卷积做计算的时候,每一次他去看一个比较小地一个窗口,比如说看一个3*3的一个像素块,如果你两个像素隔得比较远的话呢,你需要用很多层卷积一层一层上去,才能够最后把这两个隔得远的像素给你融合起来。但是他说如果使用Transformer里面的注意力机制的话,每一次都能看到所有的像素,所以我一层就能够把整个序列给你看到,相对来说就没有这个问题。但是他又提到说卷积它的一个比较好的地方就是说,可以做多个输出通道。一个输出通道可以认为是它可以去识别不一样的模式,所以他说我也想要这样子的多输出通道的效果,所以他提出了一个叫做Multi-Head Attention,可以模拟卷积神经网络多输出通道的一个效果。
接下来第二段他讲的是自注意力机制(self-attention),其实这个是Transformer里面一个关键性的点呢,但是他说这个工作其实之前已经有人提出来了,并不是我这个工作的创新,我这个地方需要给大家说明白一下。
另外他又提到了一个叫做memory network得东西,这个在17年得时候也算是一个研究的重点吧。(不知道的话可以跳过)
在我们的best knowlegde里面啊,我们的Transformer是第一个只依赖于自注意力来做这种encoder到decoder的架构的模型。
这就是相关工作的章节,关键是你要讲清楚跟你论文相关的那些论文是谁,跟你的联系是什么以及说你跟他们的区别是什么。
模型架构
这些序列模型里面,现在比较好的是一个叫做编码器和解码器的架构。
他解释了什么是编码器解码器。对于编码器来讲呢,它会将一个输入啊,就是一个长为n的一个(x1, … , xn)的东西,假设你有一个句子,有n个词,那么第xt就表示你的第t个词。他说这个序列呢编码器会把它表示成一个也是长为n,但是其中每一个zt【z = (z1, … , zn)】啊,它对应的是xt的一个向量的表示,假设你是一个句子的话呢,那么zt就表示你第t个词的一个向量的表示,这就是你的编码器的输出,就是这样一些原始的输入呢变成机器学习可以理解的一系列的向量。
那对解码器来讲,我会拿到编码器的输出,然后他会生成一个长为m的序列(y1, … , ym),首先注意到n和m是不一样长的,可以一样可以不一样。比如说你英文句子翻译中文句子的话,那么两个句子很有可能是不一样长的。他跟编码器的一个大的不一样是说,在解码器里面,你的这个词是一个一个生成的。因为对编码器来讲,你很有可能是一次性能看全整个句子,就是说做翻译的时候,我可以把整个英语的句子给你,但是你在解码的时候呢只能一个一个的生成,这个东西叫做一个自回归(auto-regressive)的模型。在这个里面啊,你的输入又是你的输出。具体来看就是在最开始我给定的Z,那么你要去生成第一个输出,叫做y1,在拿到y1之后,我就可以去生成我的y2,然后一般来说你要去生成yt的话,你可以把之前所有的y1到yt-1全部拿到,也就是说你在翻译的时候你是一个词一个词地往外蹦,所以就是说你在过去时刻的输出也会作为你当前时刻的输入,所以这个叫做自回归。
然后他又很简单地来了一句说Transformer是使用了一个编码器解码器的架构,具体来说他是将一些自注意力和point-wise,fully connected layers,然后把一个一个堆在一起的。
左边编码器,右边解码器
Input:这是编码器的输入,比如说你中文翻英文的话,那么这就是你中文的句子
Outputs:这是你解码器的输入,在解码器在做预测的时候是没有输入的,实际上他就是解码器在之前时刻的一些输出作为输入在这个地方,所以这个地方写的是一个output啊。他说shifted right就是一个一个往后往右移。
编码器
编码器是用一个n=6个的完全一样的层(layer),每个layer里面会有两个sub-layer:
第一个sub-layer叫做multi-head self-attention
第二个sub-layer是simple,position-wise fully connected feed-forward network(其实说白了就是一个MLP,但是他为了显得fancy一点呢,就把名字搞的特别长)
对每一个子层他用了一个残差连接,最后再使用一个叫做layer normalization的东西,公式如下:
为了残差连接的简单起见,保证维度相同,不用做投影,我就把每一个层它的输出的维度变成512,也就是说你对每一个词啊,你不管在哪一层,我都做了是512的这个长度的表示。这和我们之前的CNN是不一样的,或者我们之前做MLP的时候经常会把维度啊要么是往下减,要么CNN的话是空间维度往下减,但是channel维度往上拉,但是这个地方呢其实它就是固定长度来表示,使得这个模型相对来说是比较简单的,然后调参也就调一个参就行了,另外一个参就是你要复制多少块(n)。所以这个简单设计影响到后面一系列网络啊,他说BERT啊怎么样GPT怎么样,实际上也就是这两个超参数是可以调的,你就要多少层,然后每一层里面的那个维度有多大,也就是这两个参数。
LayerNorm
LayerNorm也是因为Transformer这篇文章被大家广为知道的,接下来我们通过跟batchNorm来对比来解释一下什么是LayerNorm以及说为什么我们在这些变长的应用里面不适用BatchNorm
我们考虑一个最简单的二维输入的情况啊,二维输入的话我就是输入是一个矩阵,然后呢我的每一行是一个样本,我的每一列是我的特征。batchNorm干的事情就是说每一次啊,我去把我的每一个列(就是每一个特征)把它在一个小mini-batch里面它的均值变成0方差变成1。
怎么把一个向量变成均值为0方差为1呢?
就是你把它的这个向量本身的均值减掉,然后再除以它的方差就行了。这个地方你算均值的时候呢是在每一个小批量里面啊,就这条向量里面算出它的均值,算出它的方差。
这个是在训练的时候你可以做小批量,在预测的时候呢你会把一个全局的一个均值给算出来,这个你认为是以整个数据扫一遍之后呢,在所有数据上那些平均的那个均值方差存起来,在预测的时候再使用。
当然呢,batchnorm还会去学一个λ和一个γ出来,就是说我可以把这个向量通过学习可以放成一个任意方差为某个值,均值为某个值的东西。
layernorm和batchnorm在很多时候几乎是一样的啊,除了他做的方法有点不一样之外。如果同样是我这一个二维输入的话呢,layernorm干的事情就是对每个样本啊他做normalization而不是对每个特征做了,就之前我们是把每一个列它的均值变0方差变1,现在是我把每一个行变成均值为0方差为1,这个行就表示的是一个样本。所以你可以认为这个LayerNorm就是整个把数据转置一下放到batchnorm里面出来的结果再转置回去一下,基本上可以得到自己的东西了。这个是当你的输入是二维的时候最简单的情况。
但是在我们的transformer里面或者说正常的RNN里面,他的输入是一个三维的东西,因为他输入的是一个序列的样本,就是每一个样本其实是里面有很多个元素对吧,他是一个序列,你给一个句子里面有n个词,所以每个词有个向量的话,还有一个batch的话,那么就是个3D的东西,画出来就是下面这个东西。
如果在Transformer里面的话seq的长就是n,那么feature就是d,d在刚刚我们设成了512。那么如果你还是用batchnorm的话就是取一个特征,然后把它的每个样本里面所有的元素啊,这个序列的元素啊以及它的整个batch全部搞出来,把它的均值变成0方差变成1,就是说我切一下切一块出来,把它拉成一个向量,然后跟之前做一样的运算。
如果是layernorm的话就是对每个样本这么切一下(用黄色来表示),横着切一下。就这两种切法不一样。但说切法不一样,它是会带来不一样的结果。具体来说为什么layernorm用的多一点,一个原因是说在时序的这些序列模型里面,你的每个样本的长度可能会发生变化。这里的主要的问题其实是在算均值和方差上面,对于batchnorm来说,我算均值的时候其实是通过画阴影的部分来算的,你会发现如果你的样本长度变化比较大的时候,你每次做小批量的时候,你算出来的均值方差它的抖动相对来说是比较大的,而且这个另外一个问题是说,因为我们记得我们在做预测的时候,我们要把这个全局的均值和方差记录下来,那么这个全局的均值方差,如果碰到一个新的预测样本,如果特别特别长怎么办,我碰到一个那么那么长的东西,那么我是不是在训练的时候没见过伸出去那么多,那么我在之前算的均值和方差很有可能是不那么好用的,但反过来讲对layernorm相对来说没有太多这个问题线,是因为它是每个样本自己来算我的均值和方差,我也不需要存下一个全局的一个均值方差,因为这个东西是对每个样本来做的,所以相对来说你不管样本是长还是短,反正我算均值是在你自己里面算的啊,这样子的话相对来说它稳定一些。这也是layernorm大家去看那篇文章的时候他是给大家这么解释的,但实际上来说我们知道一个很好用的一个东西呢,原文写的东西可能和之后大家的理解是不一样的,在之后又有一篇文章来解释为什么layernorm有效,更多是从一个对梯度啊,对于输入的那些normalization,然后呢提升它的常数来解释的。
解码器
解码器跟编码器是一个很像的东西,首先它跟编码器一样是由n等于6个同样的层构成的,每个层里面呢跟编码器一样有两个一样的子层。但是不一样的在于是说,解码器里面用了一个第三个子层,它同样是一个多头的注意力机制,它说跟编码器一样我们同样的用了残差连接我们用了layernorm,另外一个是我们知道在解码器的时候他做的是一个自回归,也就是说你当前的输出的输入集是上面一些时刻的输出,意味着是说你在做预测的时候你当然不能看到之后的那些时刻的输出。但是我们知道在注意力机制里面,他每一次能看到整个完整的输入,所以这个地方我我们要避免这个情况发生。也就是说在解码器训练的时候,在预测第t个时刻的输出的时候你不应该看到t时刻以后的那些输入。它的做法是通过一个带掩码(masked)的注意力机制。
模型的input是输入的句子,decoder的output在做预测的时候 是没有输入的。Shifted right 指的是 decoder 在之前时刻的一些输出,作为此时的输入。一个一个往右移。
Attention
注意力函数呢是一个将一个query和一些key-value对映射成一个输出的函数,这里面所有的query啊key,value和output它都是一些向量。具体来说你的output是你的value的一个加权和,所以就导致说你的输出的维度和你的value的维度是一样的。另外一个是说这个权重是怎么来的呢,对于每一个value的权重啊,它是这个value对应的key和你这个查询这个query的相似度算来的,这个相似度,或者叫做compatibility function,不同的注意力机制有不同的算法,如果我们画一个简单示意图,可以长成这样子,如下。
假设我有三个value和三个对应的key,假设我们现在给一个query(黄色),这个query呢跟第一第二个key比较近,那么你的输出呢就是这三个v的相加,但是第一第二个key的权重会比较大一点,第三个key的权重会比较小一点,因为这个权重是等价于你的query和你对应的key的那个相似度。同样道理,我假设再给你一个query(绿色),但是他是跟最后那一个key比较像的话,那么这样子你再去算它的value的时候呢,就会发现他对后面的权重会比较高一点,中间权重也还不错,最后的权重是比较小一点,就会得到一个新的输出。虽然你的key,value并没有变,但是随着你query的改变,因为权重的分配不一样,导致你的输出会有不一样,这就是注意力机制。
Scaled Dot-Product Attention
因为不同的相似函数导致不一样的注意力的版本,所以接下来这一章就讲的是Transformer自己用到的这一个注意力是什么样子计算的。它取的名字叫做scaled dot-product attention,虽然名字比较长啊,但实际上是最简单的注意力机制了。他说我这个里面呢我的query和key它的长度(维度)是等长的,都等于dk,因为你可以不等长,不等长是有别的办法算的,然后它的value是dv(维度),当然你的输出也一样的是dv了,它说我具体计算的是说我对我每一个query和我的key呀做内积,然后把它作为相似度,你也可以认为两个向量做内积的事儿,如果这两个向量的long是一样的话,那么你的内积的值越大(就是它的余弦值啊),那么就表示这两个向量的相似度就越高,如果你的内积比如0了,那就等于是这两个向量正交的,就是没有相似度。然后算出来之后啊它再除以根号dk,就是你这个向量的长度,然后再用一个softmax来得到你的权重,因为你给一个query,假设给n个key-value pair的话,那么就会算出n个值对吧,因为你这个query会跟每个key做内积,算出来之后再放进softmax就会得到n个非负的而且加起来和等于1的一个权重,对于权重我们觉得当然是非负啊,加起来等于1就是比较好的权重,然后我们把这些权重作用在我们的value上面就会得到我们的输出了。当然在实际中我们不能一个一个这么做运算,算起来比较慢,所以它下面给了一个在实际中的时候我们应该怎么样算的。
他说我的query啊可以写成一个矩阵,就是我其实可能不止一个query,我有n个query,那我们画出来就是一个假设Q是这个地方,然后有n行,你的维度是等于dk的。同样道理你的k啊也是一个同样的东西,但你的可能会长一点或者短一点都没关系。就是你的query的个数和你的key,value的个数可能是不一样的,但是它的长度一定是一样的,这样子我才能做内积。
然后给定这两个矩阵啊,我把它一乘就会得到一个n*m的一个东西对吧,所以这个东西里面它的每一行啊,就这个蓝色的线就是一个query对所有key的那一个内积值,然后我们再除以这个根号dk,再做softmax。所谓的softmax就是对每一行做softmax,然后行与行之间是独立的,这样子就会得到我的权重,然后再乘以我的v,我的v是有一个叫m行的,然后它的列数是dv的一个矩阵,这两个矩阵一乘的话就会得到一个长为n乘以dv的一个东西对吧,那么这个地方每一行啊,他就是我们要的一个输出了。
所以这里你可以看到是说对于一组key-value对啊和你n个query的话,我可以通过两次矩阵乘法来把整个计算做掉。这些query啊key啊value啊在实际中对应的就是我的序列,所以这样导致说我基本上可以并行的计算里面每个元素,因为矩阵乘法是一个非常好并行的东西。
接下来一段他说我提出来的注意力机制啊跟别的区别是什么样子。他说一般有两种比较常见的注意力机制,一种叫做加型的注意力机制(additive attention),它可以处理你的query和你的key不等长(维度不同)的情况,另外一个叫做点积的注意力机制(dot-product / multi-plicative attention)。他说点积的注意力呢跟我的其实是一样的,除了我这里除了一个这个根号dk之外。所以你可以看到它的名字就是scaled,就是除了那个东西,然后是点积注意力机制。
接下来他说这两种注意力机制啊其实都差不多,但是他选用的是点乘,这是因为这个实现起来比较简单,而且会比较高效,因为这就是两次矩阵乘法就能算好。当然你需要解释一下你为什么不直接用最简单的点乘注意力,你为什么要这里要除一个根号dk。他说呀,当你的dk不是很大的时候,其实你除不除都没关系,但是当你的dk比较大的时候,也就是说两个向量啊它的长度(维度大)比较长的时候,那么你做点积的时候,这些值呢可能就会比较大,当然也可能是比较小了。当你的值相对来说比较大的时候呢,你之间的相对的那些差距啊就会变大,就导致说你值最大的那一个值做出来softmax的时候就会更加靠近于1,剩下那些值呢就会更加靠近于0,就是你的值就会更加像两端靠拢,当你出现这样子的情况的时候,你算梯度的时候你会发现梯度比较小,因为softmax最后的结果是什么,最后的结果就是我希望我的预测值啊置信的地方尽量靠近1,不置信的地方尽量靠近0,这样子的时候我说我的收敛就差不多了,这时候你的梯度就会变得比较小,那你就会跑不动,所以他说啊我们在Transformer里面一般用的dk比较大,之前说过是512,所以除以一个根号dk是一个不错的选择。整个注意力的计算呢,他在上面有张图给大家画了出来。
可以看到这里面你要有两个矩阵,一个是query,一个是key做矩阵乘法,然后再除以根号dk,然后再做softmax,做出来结果最后跟你的值的那个矩阵做矩阵乘法就会得到你的输出了,这个是通过计算图来展示你这个是怎么做的。另外一个我们要讲到是怎么样做mask,mask主要是为了避免你在第t时间的时候看到以后时间的东西。具体来说,我们假设我们的query和key是等长的,他们长度都为n,而且在时间上是能对应起来的。对于第t时间刻的qt啊,这是我的query,那么我在做计算的时候,我应该只是看k1一直到kt-1,而不应该去看了kt和他之后的东西,因为kt在当前时刻还没有。但是我们知道在注意力机制的时候,其实你会看到所有,你qt会跟所有k里面的东西全部做运算,就是kt一直算算算到kn,那这个时候怎么办,就是说我们发现其实你算还是可以算的,就是说你把这些值全部给你算出来,然后呢在算出来之后,我们只要保证说在计算权重的时候,就是算输出的时候呢,我们不要用到后面的一些东西就行了,具体来说他就在你这个地方呢加了一个mask。mask的意思是说对于qt和kt和他之后的计算那些值,我给你换成一个非常大的负数比如说1e-10。那么这一个那么大的负数在进入softmax做指数的时候他就会变成0,所以导致softmax之后出来的这些东西啊,他的他对应的那些权重都会变成0,而只会前面这些值出效果(绿框框部分)。这样子的话我在算我的output的时候,我只用了v对应的v1一直到vt-1的结果,而后面的东西我没有看,所以这个mask效果是在我训练的时候,我让你t个时刻的query只看我对应的前面那一些的key-value pair,使得我在做预测的时候,我跟现在这个是能够一一对应上的。
Multi-Head Attention
在讲完注意力机制的计算之后呢,我们来看一下multi-head是在干什么事情,我们首先还是回到我们的文字那部分啊。他这里说与其我做一个单个的注意力函数。不如说我把整个query呀,key,value呀投影到一个低维,投影h次,然后再做h次的注意力函数,然后每一个函数的输出我把它并在一起,然后再投影来会得到我的最终的输出,他说我们在图2给大家演示这个效果。
然后他说这个是我们原始的value啊key啊query啊,然后在这个地方呢我们进入一个线性层,线性层就是把你投影的比较低的维度,然后再做一个scaled dot-product attention,就是左边这个东西啦,全部放进来,然后我们这里做h次,会得到h个输出,我们再把自己这些向量啊全部合并在一起,最后做一次线性的投影,会回到我们的multi-head attention。
所以为什么要做多头注意力机制呢?
如果我们回过头来看这个dot-product的注意力的话,你会发现里面没有什么可以学的参数,你的距离函数啊就是你的内积。但有时候我为了识别不一样的那些模式啊我希望你可能有一些不一样的计算相似度的方法。如果你用的是加性attention(这里没有提到的)的话,那里面其实还是有一个权重你来学的,你也许可以学到这些东西。他说那我不用那个,那我用这个的话,我的一个做法是我先让你投影到一个低维,这个投影的w是可以学的,也就是说我给你h次机会,希望你能学到不一样的投影的方法,使得在那个投影进去的那个度量空间里面能够去匹配不同模式他需要的一些相似函数,然后最后把这些东西回来,最后再做一次投影,所以跟我们之前说到的有点点像在卷积网络里面你有多个输出通道的感觉。
然后我们看一下这个东西的具体的公式是怎么算的,你会发现在Multi-Head的情况下,你还是以前的Q、K、V,但是你的输出啊已经是你不同的头的那一个输出的做concat起来,再投影到一个WO里面的。对每一个头啊,他就是把你的Q、K、V,然后通过一个不同的可以学习的WQ、WK、WV投影到一个低维上面,再做我们之前提到过的注意力函数,然后再出来就行了。
在实际上来说他用的h是等于8的,就是用8个头,而且我们知道你的注意力的时候因为有残差连接的存在,使得你的输入和输出的维度至少是一样的。所以它的做法是说你投影的时候,它投影的就是你的输出的维度啊除以h。因为我们之前我的输出维度是512,所以除以8之后呢,就是每一次我们把它投影到一个64维的一个维度,然后在上面算你的注意力函数,然后再并起来再投影回来。虽然这个地方你看到是非常多的小矩阵的乘法,实际上你在实现的时候也可以通过一次的矩阵乘法来实现。(这个可以作为一个练习题,大家想一下怎么去实现他)
Applications of Attention in our Model
再讲完多头注意力是如何实现了之后,在3.2章的最后个小节里面讲的是在Transformer这个模型里面是如何使用注意力的,他这里讲了三种使用的情况啊。我们最简单的方法是回到我们之前那个架构图看一看它到底是怎么被用的。
我们回到我们的架构图,我们看到的是黄色这个东西表示的是注意力的层,这个地方一共有三种不一样的注意力层,然后我们分别来看一下每一个注意力层它的输入和输出分别是什么。这个其实是对应的是刚刚我们那一小节里面的三段话。
第一个注意力层
首先我们看一下我们的编码器的注意力是在干什么事情,我们知道编码器的输入啊,假设你的句子长度是n的话呢,它的输入其实是一个n个长为d的向量啊,假设我们的pn大小设成1了。我们把它画出来就是每一个输入它的词对应的是一个长为d的向量,然后我们这里一共有n个这样子的东西,然后我们来看一下注意力层啊他有三个输入。他分别表示的是key、value和query。然后这个地方是你一根线过来然后它复制成了三下,意思就是说同样一个东西,我既作为key也作为value也作为query,所以这个东西叫做自注意力机制,就是说你的key、value和query其实就是一个东西,就是自己本身。然后我们知道这个地方我们输入了n个query,那么每个query我会拿到一个输出,那么意味着我会有n个输出,而且这个输出和value因为长度是一样的话,那么我的输出的维度其实也是那个d,就是意味着我的输入和输出的大小其实是一个东西,我们把它画出来(下面蓝色)。
其实就是你的输也是跟它长度一样长为n的一个东西,对于每个query,我会计算一个这样子的输出,因为我们知道这个输出啊,其实就是你的value的一个加权和,权重呢是来自于query和key的一些东西,但他本身是一个东西,那么久意味着说它的这个东西呢,实际上本身就是你的输入的一个加权的一个和,然后这个绿色线代表权重的话。因为这个权重其实本身就是这一个向量(红框框里面涂绿的向量)跟每一个输入的别的向量计算相似度,那么它跟自己算肯定是最大的,就是说你这根线肯定是最粗的。假设这根线跟你右边这个向量也相似度比较高的话,那么这个权重也会稍微高一点。假设我们不考虑多头啊和有投影的情况,你的输出呢其实就是你的输入的一个加权和,你的权重来自于你自己本身跟各个向量之间的一个相似度。但如果我们说过有多头的话,因为有投影,其实我们在这个地方会学习h个不一样的距离空间出来,使得你出来的东西当然是会有一点点不一样了,这个就是第一个注意力层是如何用的。
第二个注意力层
然后你看到解码器啊,解码器是一回事,他这个地方是一样的,一个东西过来,然后复制成了三次,然后解码器的输入也是一样的,只是长度可能变成了一个长为m的样子,然后你的维度其实也是一样的,所以它跟编码器是一样的自注意力,唯一不一样的是这里有个masked这个东西。我们之前有解释过在解码器的时候啊,比如说你算这一个query它对应的输出的时候呢,他是不应该看后面那些东西,所以意味着是说在解码器的时候,你的后面的这些东西要设成0,我们用黄色的线表示一下。
第三个注意力层
就是图中最上面的那个注意力层,这个地方你看到他不再是自注意力了,而是,你的key和你的value来自于你的编码器的输出,然后你的query呢是来自于你解码器下一个attention的输入,我们知道你的编码器最后一层的输出就是n个长为d(维度)的向量(红色框框),那么你的解码器的masked attention啊,就最下面那个attentino它的输出是m个也是长为d的向量(绿色框框)。
这里你的编码器的输出作为value和key进来,然后你的解码器下一层的输出呢作为query进来,意味着是说对解码器的每一个输出呢作为query我要算一个我要的输出,假设用蓝色表示的话,那么你的输出我们知道是来自于value的一个加权和,那么就是来自于编码器它的输出的加权和(画过来就是图中的蓝色线),这个权重它的粗细程度啊就是取决于我这个query(绿框框)跟这个东西(红框框)的相似度,假设相似度比较高的话那么权重就会大一点。那意味着就是说在这个attention干的事情其实就是去有效的把你的编码器里面的一些输出啊根据我想要的东西给它拎出来。举个具体的例子,假设你是在做英文翻译中文,我假设第一个词是hello,第二个词是world的话,那么你的中文他就是第一个当然是”你“对吧,”你好“,所以你会知道说在算”好“的时候,如果它作为query的时候,那么呢去看hello的这个向量应该是会相近一点,给它一个比较大的权重,但是”world“这个是后面的次相关,我发现到”world“这个词跟我这个query相关度没那么高,在计算你的相似度的时候,那么就是说在算”好“的时候呢,我会给它一个比较大的权重在hello上面,但是我在后面如果还有”你好世界“的话,如果是个”世“的话,那么在这个query的时候,我在去算它的输出这个东西的时候呢,它那么就会给第二个向量给一个比较大的一个权重出来。意味着是说根据你在解码器的时候你的输入的不一样,那么我会去根据你的当前的那一个向量去在编码器的输出里面去挑我感兴趣的东西,也就是你注意到你感兴趣的东西,那些跟你不那么感兴趣的东西你就可以忽略掉它,这个也是说attention是如何在编码器和解码器之间传递信息的时候起到的一个作用。
Position-wise Feed-Forward Networks
他其实就是一个fully connected feed-forward network,他就是一个MLP了,但是他不一样的是说他是applied to each position seperately and identically。
position是什么东西呢,就是你输入的那一个序列啊,不是有很多很多个词吗,每个词他就是一个点,他就是那一个position,那他就是把一个MLP对每一个词作用一次,然后对每个词作用的是同样一个MLP,所以这个就是point wise的意思。他说白了就是MLP,只是作用在最后一个维度。
具体来看一下他是怎么写的:
括号里面就是一个线性层,max(…)就是一个Relu的激活层,然后再有一个线性层。我们知道在我们的注意力层它的输入啊,就每一个query它对应的那一个输出他是长为512,那么这个x呢就是一个512的一个向量,他说W1我会把512投影成2048这个维度,就等于是我把它的维度扩大了四倍,因为最后你有一个残差连接,你还得投影回去,所以W2呢又把2048投影回了512,所以这个公式2说白了就是一个单隐藏层的MLP,然后中间隐藏层把你的输入扩大四倍,最后输出的时候也回到你输入的大小。说白了就是你用pytorch来实现的话它其实就是把两个线性层放在一起,你都不需要改任何参数,因为pytorch当你的输入是一个3d的时候,它默认就是在最后一个维度做计算。为了更好地理解,我们用图把他跟你的attention这个东西给大家画一下,以及说它跟我们之前的RNN它的区别在什么地方。
我们这里还是考虑一个最简单的情况,就是没有残差连接,也没有LayerNorm,然后你的attention呢也是一个单头然后没有投影。我们知道我们的输入啊就是一个长为n的一些向量(下面蓝色框框),再进入attention之后(红色框框),我们就会得到同样长度的一些输出(上方的蓝色框框)。最简单情况的attention就是对你的输入做一个加权的和,然后加权和之后我们进入我们的MLP,就是那个point wise的MLP。虽然我们花了几个MLP但是其实就一个,每一个红色的方块之间的权重是一样的,然后每个MLP对每一个输入的点呢做运算会得到一个输出,最后就得到了整个Transformer块的一个输出是这样子,虽然它的输入和输出大小都是一样的。
所以这个地方你看到的是说,attention起的作用是什么东西,他就是把整个序列里面的信息抓取出来,做一次汇聚aggregation,所以这个东西已经就有了我序列中感兴趣的东西,信息已经抓取出来了,以至于我在做投影啊,在做MLP的时候,映射成我更想要的那个语义空间的时候,因为这个东西已经含有了我的序列信息,所以每个MLP只要再对每个点独立做就行了。因为这个地方序列信息已经被汇聚完成,所以这个地方是可以分开做的。也就是整个Transformer是如何抽取序列信息然后把这些信息加工成我最后要的那个语义空间那个向量的过程。
作为对比,我们看一下RNN是怎么做的,我们知道RNN的输入跟你是一样啊,就是一些向量。然后对于第一个点呢,说白了你也就是做一个线性层,我们做一个最简单的就是一个没有隐藏层的MLP就是一个纯线性的层。第一个点就是直接做出去就完事了,对于下一个点,我是怎么样利用我的序列信息的呢,我还是用之前这个MLP(它的权重跟之前是一样的),但是呢我的时序信息啊(用绿色表示),它就是把这个东西它的上一个时刻的输出放回来,作为跟输入一起并入进去,这样子我就完成了我信息的一个传递。绿色的线表示的是之前的信息啊,蓝色的线表示的是我当前的信息,这样子我会得到一个当前的一个输出。历史信息就是上一次的那个输出作为历史信息进来然后得到我当前的一个输出。
所以可以看到是说RNN呢跟Transformer是一样的,都是用一个线性层或者说一个MLP来做一个语义空间的一个转换,但是不一样的是你如何传递序列的信息,RNN是把上一个时刻的信息输出传入下一个时候做输入。但是在Transformer里面他是通过一个attention层,然后再全局的去拉到整个序列里面信息,然后再用MLP做语义的转换,这个是两个模式之间的区别。但是他的关注点都是在你怎么去有效的去使用你的序列的信息。
Embeddings and Softmax
embedding大家都知道,因为我的输入是一个个的词,或者一个叫词源,叫token,那我需要把它映射成一个向量,embedding就是说给任何一个词,我学习一个长为d(d_model)的一个向量来表示它,当然这个地方d你可以认为是512了,他这里是说你的编码器要一个embedding,你的解码器的输入也要有个embedding。这货在你的softmax前面那个线性啊也需要一个embedding,他说我这三个是一样的权重,这样子我训练起来会简单一点,另外一个有意思的是说他把权重啊乘了一个根号d(d_model),d就是512。为什么做这个事情是因为你在学embedding的时候呢,多多少少会把每一个向量啊它的LToLlong啊学成相对来说比较小的,比如说学成1吧,就不管你的维度多大的话,最后你的值都会等于1,那就是说你的维度一大呢你学的一些权重值就会变小,但是你之后我要加上这个东西(Positional Encoding),加这个东西的时候它不会随着你的长度变长了他把你的long固定住,所以它乘了根号d之后使得加上Positional Encoding的时候在一个scale上大家都差不多,就是它做了一个hat。
Positional Encoding
为什么有这个东西,是因为你发现attention这个东西是不会有时序信息的,你想一想你的输出是什么东西,你的输出是你的value的一个加权和,你这个权重,是query和key的之间的那个距离,它跟你的序列信息是无关的。就我根本就不会去看你那个key、value里面那些对啊在你序列里面的哪些地方,所以意味着说我给你一句话,我把顺序任何打乱之后,我attention出来结果都是一样的,顺序会变,但是值不会变。这个当然是有问题的对吧,在处理时序数据的时候,我给你一句话,假设我把里面的词给你完全打乱,把么你语义肯定会发生变化,但是你的attention不会处理这个情况,所以我需要把时序信息加进来。RNN是怎么加呢,RNN是说上一个时刻的输出作为下一个时刻的输入来传递我的历史的信息,所以他本来就是一个时序的一个东西,但是attention不是。他的做法是说我在我的输入里面加入时序信息,就是说你这一个词啊,他在一个位置i,我把i这个位置这个数字12345啊加到你的输入里面,所以这个东西叫做positional encoding。具体计算公式如下所示:
大概的思路是,在计算机里面我们怎么表示一个数字,假设我用一个32位的整数来表示数字的话,那就是用32个bit,每个bit上面有不同的值来表示012345678,你可以认为就是说我一个数字是用一个长为32的一个向量来表示的。现在,我一个词在嵌入层会表示成一个长为512的向量,同样我用一个长为512的向量来表示一个数字,表示你这个位置012345678,具体那些值是怎么算出来的,是用周期不一样的sin和cos函数的值来算出来的,所以导致说我任何一个值可以用一个长为512的一个向量来表示它,然后呢这个长为512的记录了时序信息的一个东西啊跟你的嵌入层相加就会完成了我把时序信息加进我的数据的一个做法。
如果我们回到之前的架构图的话(figure1的模型图),你可以看到我的输入进来,进入embedding层之后,那么对每个词都会拿到那个向量,长为512的一个向量,然后positional encoding就是你这个词在我这个句子中的位置告诉它,它返回给你一个长为512的一个向量表示这个位置,然后把这两个加起来就行了。这个东西因为是cos和sin的一个函数,他是在正1负1之间抖动的,所以这个东西(input embedding)乘了一个根号d,使得每个数字呢也是差不多的正1到负1之间这个数值区间里面。然后进去之后那么我就完成了在输入里面加入信息,这是因为我后面整个这一块(attention的块)啊是顺序不变,就是说我的输入的序列不管我怎么打乱我的顺序,进去之后,我的输出那些值是不变的,最多是我的顺序发生了相应的变化,所以他就把顺序信息直接在数据里面那个值给加进去了。
Why Self-Attention
这章是大概解释一下为什么要用自注意力。它主要说的是相对于你用循环层或者卷积层的时候,我用自注意力有多么好。整个这一段话它解释的其实就是这一个表。(如下)
他说我们比较了四种不一样的层:
自注意力
循环层
卷积层
它构造出来一个受限的自注意力
他有三列作比较:
第一列是说我的计算复杂度当然是越低越好
第二个是说我的顺序的计算越少越好。顺序的计算就是说你下一步计算必须要等前面多少步计算完成,在算一个Layer的时候你越不要等那么你的并行度就越高
最后一个是说一个信息从一个数据点走到另外一个数据点要走多远,这也是越短越好
我们分别来看一下每一个层它代表的数值是什么意思,首先我们来看自注意力层,n这个地方是你序列的长度,d是你向量的长度,我们知道整个自注意力的话其实说白了就是几个矩阵做运算,其中一个矩阵是你的query的矩阵乘以你的key的矩阵。你的query矩阵是有n行,你的n个query,然后你的列数是d,就是你的维度是d。然后你的key也是一样的也是n*d,所以两个矩阵一乘的话,那么算法复杂度就是n^2*d,另外当然还有哦一些别的矩阵运算,但是它的复杂度都是一样的。然后你的Sequential Operation呢因为你就是那么几个矩阵乘法,矩阵里面它可以认为是并行度比较高的,所以这个地方是一个O(1)的。最大的长度是说你从一个点的信息想跳到另外一个点要走多少步。我们知道在attention里面,就是一个query可以跟所有的key去做运算,而且你的输出啊是你所有value的一个加权和,所以就是说任何query跟任何一个很远的一个key-value pair我只要一次就能过来,所以呢这个长度是比较短的。
然后看一下循环层,循环层呢是我们知道就是如果你的序列是乘了n的话,他就一个一个做运算,每个里面呢它的主要的计算就是一个n*n的一个矩阵,就是一个dense layer,然后再乘以你一个长为d的一个输入,所以是n*d^2。然后你对比一下这两个东西啊,是有一定区别的,就真的取决于你是n大还是d大。实际上来说这个地方你的d是512,你的n呢也差不多是几百的样子吧,n和d这两个差不多。但是在循环的时候呢,因为你是要一步一步做运算,当前时间刻的那个词啊需要等待前面那个东西去完成,所以导致你是一个长为n的一个序列化的操作,在并行上是比较吃亏的。另外一个是说你最初点的那个历史信息啊需要到最后那一个点的话需要走过n步才能过去,所以它这个地方的最长是O(n)。所以大家会批评RNN说你对特别长的序列的时候做的不够好,因为你的信息一开始走啊走啊走,就走丢了,而不像attention一样,就可以直接一步就能过去。
再看一下卷积,卷积在序列上具体的做法是他用一个ed的卷积,所以它的kernel啊他就是个k,n是你的长度,然后d呢就是你的输入的通道数和输出的通道数吧。k一般也不大,k一般就3啊5啊几,所以这个东西你也可以认为是常数,所以导致说卷积的复杂度和RNN的复杂度其实是差不多的。但是卷积的好处是说你就是一个卷积操作就完成了,里面的并行度很高,所以卷积呢做起来通常比RNN要快一点,另外一个是说卷积每一次一个点是由一个长为k的一个窗口来看的,所以它一次一个信息在k距离内是能够一次就能传递,如果你超过了k的话它要传递信息的话,它要通过多层,就一层一层上去,但是他是log的一个操作,所以这个东西也不亏。
最后一个东西他是说让我做注意力的时候啊,我的query啊只跟我最近的r个邻居去做运算,这样子的话我就不用去算n平方这个的东西了对吧,但它的问题是说,这样子的话有两个比较长的远的一个点啊需要走几步才能过来。
一般来说在实际上来说呢,我们用attention主要是关心说啊特别长的序列你真的能够把整个信息揉的比较好一点,所以在实际过程中啊,第四个感觉上用的不是那么多啊,大家都是用最原始的版本,不用太做受限了。
所以基本上可以看起来是说就实际中啊,就是当你的序列的长度啊和你的整个模型的宽度啊差不多的时候,而且大家深度都一样的话,基本上这三个模型的算法复杂度都是差不多的,当然是说你的attention和卷积相对来说计算会好一点,另外一个是说attention在信息的糅合性上会好一点。所以你看上去是说用了self-attention之后是不是你的感觉上对长数据处理更好,而且可能算的也不快就不会慢对吧,但实际上其实也不是这样子的啊。实际上是说attention对你整个模型的假设做了更少,导致说你需要更多的数据和更大的模型才能训练出来跟你RNN和CNN同样的效果,所以导致现在基于Transformer的模型呢,都是特别大特别贵。
实验
Training Data and Batching
第五章是讲你训练的一些设置是怎么样的,首先他讲我的训练数据集和我的batching怎么做的。
他用了两个任务,一个是英语翻德语,他用的是标准的WMT 2014的数据,他这个里面有4.5万个句子的对,他说他用的是byte-pair encoding,就是bpe,大概的思想就是说你不管是英语还是德语啊,其实一个词里面有很多种变化,就是什么加ing啊,加es呀,但是你如果直接把每一个词做成一个token的话呢,你会导致你的字典里面的东西会比较多,而且一个动词可能有几种变化形式,你做成不一样的词的时候,它们之间的区别模型是不知道了。bpe相对来说就是把你那些词根给你提出来,这样好处是说它可以把整个字典降的比较小,他这个地方用的是37000个token的一个字典,而且他是在英语和德语之间是共享的,就是说我们不再为英语构造一个字典,不再为德语构造一个字典,这样的好处是说我整个编码器和解码器的一个embedding就可以用一个东西了,而且整个模型变得更加简单,也就是他之前所谓的我的编码器解码器那个embedding它是共享权重的。另外一个英语到法语的话,他用了一个更大的数据集。
Hardware and Schedule
在接下来的硬件和schedule部分,他说我训练使用了八个P100的GPU,这个是比较有意思的,就是说在几年前啊,google的工作还是大量使用GPU的,但是这个之后啊基本上就很少见了,因为在这个之后google让内部的员工尽量使用tpu,限制你们使用gpu,这篇文章多多少少也是推动这个进展。这是因为Transformer里面我们知道基本就是一些比较大的矩阵做乘法,tpu这种东西啊就是特别适合做大的矩阵乘法,以至于Google说你们其实挪到tpu也没有太多问题,可能性能还更好一些,他说我们的base模型呢是用的一个小一点的参数,他每一个batch训练的时间是0.4秒,然后我们一共训练了10万步,一共就是在我的八个gpu上训练了12个小时。他说我大的模型,这样一个batch训练需要1秒钟,然后他一共训练了30万步啊,最后是一台机器3.5天。
Optimizer
在训练器上面他们用的是Adam,这是他们的参数。
β2我觉得可能不是最常用的,β2我记得应该是0.99还是0.999,所以他选了一个稍微少一点的值,它的学习率是用这个公式3算出来的,有意思的是他的学习率是根据你的模型的那个宽度的-0.5次方,就是说当你的模型越宽的时候,就是你学的那些向量越长的时候,你的学习率要低一点。另外一个是他有一个warmup,就是从一个小的值慢慢地爬到一个高的值,爬到之后呢,再根据你的步数啊,按照0.5次方衰减,最后他说我的warmup是4000。
有意思的是基本可以看到说这个地方没有东西可以调的,就是你的学习率几乎是不用调的,取决于第一Adam对学习率确实不那么敏感,第二个是说他这个地方也是把整个模型宽度和schedule考虑进来了,所以他的学习率是不用调的。
Regularization
在5.4节中讲的他使用的正则化,他用了三个正则化:
Residual Dropout:说白了就是说对每一个子层,子层就是包括了你的多头的注意力层和你之后的MLP。在每个层的输出上,在他进入残差连接之前和在进入LayerNorm之前,它使用了一个dropout,他的dropout率是0.1。也就是说把这些输出的10%的那些元素只乘0.1,剩下的那些只能乘以1.1。另外一个他在输入加上你的词嵌入再加上你的positional encoding的时候呢在它上面也用了一个dropout,也就是把10%的元素乘了一个。有意思的是说你基本看到对每一个带权重的层啊他在输出上都使用的dropout,用的是比较狠的,虽然这个dropout率并不是特别高,但是他使用了大量的dropout的层来对他的模型做正则化。
Label Smoothing:然后另外一个他使用的是一个label smoothing,这个技术是在inception v3让大家官方的知道啊,意思是说我们用softmax去学一个东西的时候,我的标号是正确的是1,错误的是0,就是我用对于争取的那一个label的softmax的值去逼近于1,但我们知道softmax是很难逼近于1的,因为它里面是一个指数,它是一个很soft的东西,就是说它需要你的输出接近无限大的时候才能逼近于1,这个使得训练比较难。一般的做法是说你不要搞成那么特别难的0和1,你可以把那个1的值往下降一点,比如说降成0.9啊,但这个地方它降的比较狠,它是降成了0.1,就是说对于正确的那个词,我只需要我的softmax的输出是到0.1就行了,叫置信度是0.1就行了,不需要做的很高。剩下的那些值呢就可以使0.9除以你的字典的大小。他说这里会损失你的perplexity,perplexity是你的log lost做指数,基本上可以认为是你的模型不确信度。因为你这个地方让你学说我正确答案我也只要给个10%是对的就行了,所以当然你的不确信度会增加,所以你这个值会变高。但是他说我的模型会不那么确信,会提升我的精度和我的BLUE的分数,因为精度和BLUE的分数才是我们关心的重点,所以这个地方他说觉得说没关系。
接下来我们看第二个表,第二个表表示的是不同的超参数之间的一些对比,我们可以看一下有哪些超参数啊。
N:你要堆多少层
d_model:你这个模型的宽度,就是一个token进来要表示成一个多长的向量
dff:表示的是你那MLP中间那个隐藏层的输出的大小
h:注意力层头的个数
dk、dv:分别是你一个头里面那个key和那个value的维度
P_drop:dropout
银瀑寺隆ls:最后lable smoothing的时候,你这个要学的label的真实值是等于多少
train steps:你要训练多少个batch
虽然这个模型看上去比较复杂,但是也没有太多东西可以调,这个设计上来说让后面的人更加方便一点,比如说bert它其实就是把这个架构拉过去,然后把N、d、dff这几个参数改了一下。GPT也基本就是copy过去,然后稍微调一下就行了,让后面的人的工作变得简单了很多。
最后还有一个表是说在另一个任务上表现也挺好的。
但是最新的一些结果表明,attention只是在Transformer里面起到一个作用,它的主要作用是把整个序列的信息给大家聚合起来,但是后面的MLP啊,以及你的残差连接是缺一不少了,如果你把这些东西去掉的话,attention基本上什么东西都训练不出来,所以说你这个模型attention也不是说你只需要它就行了。
第二个是说attention根本就不会去对你这个数据的那个顺序做建模,他为什么能够打赢RNN呢,RNN能够显式的建模这个序列信息,理论上应该比你的MLP效果更好,现在大家觉得他使用了一个更广泛的归纳偏置,使得他能处理一些更一般化的信息,这也是为什么说attention并没有做任何空间上的一些假设,它也能够跟CNN甚至是比CNN取得更好的一些结果。但是它的代价是说因为它的假设更加一般呢,所以他对数据里面抓取信息的能力变差了,以至于说你需要使用更多的数据,使用更大的模型才能训练出你要的效果,这也是为什么现在Transformer模型一个比一个大训练一个比一个贵。