手都敲麻了,一文解析Transformer模型的基本原理与Pytorch代码实现。
仅用于站内搜索,没有排版格式,具体信息请跳转上方微信公众号内链接
我们根据论文的结构图,先了解基本原理,再一步一步使用Pytorch实现这个Transformer模型。
在开始之前,如果你是深度学习初学者,我自己在25年3月有给粉丝整理过一份非常详细的入门学习路线思维导图,学习顺序、怎么学、看什么、要注意什么都写的非常清楚,因网盘分享一段时间就会和谐,如果你需要的话可以直接扫码添加我的助理让她无偿及时发送给你最新网盘链接。
为了避免单个微信添加频繁,我准备了两个微信,大家随意添加一个即可。
1. Transformer架构
首先看一下transformer的结构图:
解释一下这个结构图。首先,Transformer模型也是使用经典的encoer-decoder架构,由encoder和decoder两部分组成。
-上图的左半边用Nx框出来的,就是我们的encoder的一层。encoder一共有6层这样的结构。
-上图的右半边用Nx框出来的,就是我们的decoder的一层。decoder一共有6层这样的结构。
-输入序列经过wordembedding和positionalencoding相加后,输入到encoder。
-输出序列经过wordembedding和positionalencoding相加后,输入到decoder。
-最后,decoder输出的结果,经过一个线性层,然后计算softmax。
-wordembedding和positionalencoding我后面会解释。我们首先详细地分析一下encoder和decoder的每一层是怎么样的。
2. Encoder
encoder由6层相同的层组成,每一层分别由两部分组成:
第一部分是一个multi-headself-attentionmechanism
第二部分是一个position-wisefeed-forwardnetwork,是一个全连接层
两个部分,都有一个残差连接(residualconnection),然后接着一个LayerNormalization。
如果你是一个新手,你可能会问:
multi-headself-attention是什么呢?
参差结构是什么呢?
LayerNormalization又是什么?
这些问题我们在后面会一一解答。
3. Decoder
和encoder类似,decoder由6个相同的层组成,每一个层包括以下3个部分:
第一个部分是multi-headself-attentionmechanism
第二部分是multi-headcontext-attentionmechanism
第三部分是一个position-wisefeed-forwardnetwork
还是和encoder类似,上面三个部分的每一个部分,都有一个残差连接,后接一个LayerNormalization。
4. Attention机制
在讲清楚各种attention之前,我们得先把attention机制说清楚。
通俗来说,attention是指,对于某个时刻的输出y,它在输入x上各个部分的注意力。这个注意力实际上可以理解为权重。
attention机制也可以分成很多种,一问有一张比较全面的表格:
为什么这种attention叫做additiveattention呢?很简单,对于输入序列隐状态和输出序列的隐状态,它的处理方式很简单,直接合并,变成。
但是我们的transformer模型使用的不是这种attention机制,使用的是另一种,叫做乘性注意力(multiplicativeattention)。
那么这种乘性注意力机制是怎么样的呢?从上表中的公式也可以看出来:两个隐状态进行点积!
所谓self-attention实际上就是,输出序列就是输入序列!因此,计算自己的attention得分,就叫做self-attention!
context-attention一词并不是本人原创,有些文章或者代码会这样描述,我觉得挺形象的,所以在此沿用这个称呼。其他文章可能会有其他名称,但是不要紧,我们抓住了重点即可,那就是两个不同序列之间的attention,与self-attention相区别。
不管是self-attention还是context-attention,它们计算attention分数的时候,可以选择很多方式,比如上面表中提到的:
additiveattention
local-base
general
dot-product
scaleddot-product
那么我们的Transformer模型,采用的是哪种呢?答案是:scaleddot-productattention。
论文Attentionisallyouneed里面对于attention机制的描述是这样的:
这句话描述得很清楚了。翻译过来就是:通过确定Q和K之间的相似程度来选择V!
用公式来描述更加清晰:
scaleddot-productattention和dot-productattention唯一的区别就是,scaleddot-productattention有一个缩放因子。
上面公式中的表示的是K的维度,在论文里面,默认是64。
那么为什么需要加上这个缩放因子呢?论文里给出了解释:对于很大的时候,点积得到的结果维度很大,使得结果处于softmax函数梯度很小的区域。
我们知道,梯度很小的情况,这对反向传播不利。为了克服这个负面影响,除以一个缩放因子,可以一定程度上减缓这种情况。
为什么是呢?论文没有进一步说明。个人觉得你可以使用其他缩放因子,看看模型效果有没有提升。
论文也提供了一张很清晰的结构图,供大家参考:
首先说明一下我们的K、Q、V是什么:
在encoder-decoderattention中,Q来自于decoder的上一层的输出,K和V来自于encoder的输出,K和V是一样的。
Q、K、V三者的维度一样,即。
下面是multi-headattention的结构图:
值得注意的是,加入不同位置的表示子空间的信息。上面所说的分成份是在维度上面进行切分的。因此,进入到scaleddot-productattention的实际上等于未进入之前的。
Multi-headattention允许模型
Multi-headattention的公式如下:
其中,
论文里面,,。所以在scaleddot-productattention里面的
相信大家已经理清楚了multi-headattention,那么我们来实现它吧。代码如下:
上面的代码终于出现了Residualconnection和Layernormalization,我们现在来解释它们。
11. Residualconnection是什么?
残差连接其实很简单!给你看一张示意图你就明白了:
假设网络中某个层对输入x作用后的输出是,那么增加residualconnection之后,就变成了:
这个+x操作就是一个shortcut。
那么残差结构有什么好处呢?显而易见:因为增加了一项,那么该层网络对x求偏导的时候,多了一个常数项!所以在反向传播过程中,梯度连乘,也不会造成梯度消失!
所以,代码实现residualconnection很非常简单:
defresidual(sublayer_fn,x):returnsublayer_fn(x)+x
文章开始的transformer架构图中的Add&Norm中的Add也就是指的这个shortcut。
至此,residualconnection的问题理清楚了。更多关于残差网络的介绍可以看文末的参考文献。
12. Layernormalization是什么?
Normalization有很多种,但是它们都有一个共同的目的,那就是把输入转化成均值为0方差为1的数据。我们在把数据送入激活函数之前进行normalization(归一化),因为我们不希望输入数据落在激活函数的饱和区。
说到normalization,那就肯定得提到BatchNormalization。BN在CNN等地方用得很多。
BN的主要思想就是:在每一层的每一批数据上进行归一化。
我们可能会对输入数据进行归一化,但是经过该网络层的作用后,我们的的数据已经不再是归一化的了。随着这种情况的发展,数据的偏差越来越大,我的反向传播需要考虑到这些大的偏差,这就迫使我们只能使用较小的学习率来防止梯度消失或者梯度爆炸。
BN的具体做法就是对每一小批数据,在批这个方向上做归一化。如下图所示:
可以看到,右半边求均值是沿着数据批量N的方向进行的!
Batchnormalization的计算公式如下:
具体的实现可以查看上图的链接文章。
说完Batchnormalization,就该说说咱们今天的主角Layernormalization。
那么什么是Layernormalization呢?:它也是归一化数据的一种方式,不过LN是在每一个样本上计算均值和方差,而不是BN那种在批方向计算均值和方差!
下面是LN的示意图:
和上面的BN示意图一比较就可以看出二者的区别啦!
下面看一下LN的公式,也BN十分相似:
什么是paddingmask呢?回想一下,我们的每个批次输入序列长度是不一样的!也就是说,我们要对输入序列进行对齐!具体来说,就是给在较短的序列后面填充0。因为这些填充的位置,其实是没什么意义的,所以我们的attention机制不应该把注意力放在这些位置上,所以我们需要进行一些处理。
具体的做法是,把这些位置的值加上一个非常大的负数(可以是负无穷),这样的话,经过softmax,这些位置的概率就会接近0!
哈佛大学的文章TheAnnotatedTransformer有一张效果图:
其他情况,attn_mask一律等于paddingmask。
至此,mask相关的问题解决了。
17. Positionalencoding是什么?
好了,终于要解释位置编码了,那就是文字开始的结构图提到的Positionalencoding。
就目前而言,我们的Transformer架构似乎少了点什么东西。没错,就是它对序列的顺序没有约束!我们知道序列的顺序是一个很重要的信息,如果缺失了这个信息,可能我们的结果就是:所有词语都对了,但是无法组成有意义的语句!
为了解决这个问题。论文提出了Positionalencoding。这是啥?一句话概括就是:对序列中的词语出现的位置进行编码!如果对位置进行编码,那么我们的模型就可以捕捉顺序信息!
那么具体怎么做呢?论文的实现很有意思,使用正余弦函数。公式如下:
其中,pos是指词语在序列中的位置。可以看出,在偶数位置,使用正弦编码,在奇数位置,使用余弦编码。
上面公式中的是模型的维度,论文默认是512。
这个编码公式的意思就是:给定词语的位置,我们可以把它编码成维的向量!也就是说,位置编码的每一个维度对应正弦曲线,波长构成了从到的等比序列。
上面的位置编码是绝对位置编码。但是词语的相对位置也非常重要。这就是论文为什么要使用三角函数的原因!
正弦函数能够表达相对位置信息。,主要数学依据是以下两个公式:
上面的公式说明,对于词汇之间的位置偏移k,可以表示成和的组合形式,这就是表达相对位置的能力!
以上就是E的所有秘密。说完了positionalencoding,那么我们还有一个与之处于同一地位的wordembedding。
Wordembedding大家都很熟悉了,它是对序列中的词汇的编码,把每一个词汇编码成维的向量!看到没有,Postionalencoding是对词汇的位置编码,wordembedding是对词汇本身编码!
所以,我更喜欢positionalencoding的另外一个名字Positionalembedding!
如果你想获取更详细的关于wordembedding的信息,可以看我的另外一个文章word2vec的笔记和实现。
20. Position-wiseFeed-Forwardnetwork是什么?
这就是一个全连接网络,包含两个线性变换和一个非线性函数(实际上就是ReLU)。公式如下:
这个线性变换在不同的位置都表现地一样,并且在不同的层之间使用不同的参数。
论文提到,这个公式还可以用两个核大小为1的一维卷积来解释,卷积的输入输出都是,中间层的维度是。
实现如下:
21. Transformer的实现
至此,所有的细节都已经解释完了,现在来完成我们Transformer模型的代码。首先,我们需要实现6层的encoder和decoder。
encoder代码实现如下:
通过文章前面的分析,代码不需要更多解释了。同样的,我们的decoder代码如下:
最后,我们把encoder和decoder组成Transformer模型!
代码如下:
至此,Transformer模型已经实现了!