transformer详解

Transformer

1.总体框架

先来一张名图镇楼,哈哈哈。

神经模型就是一个黑盒,一个函数,如图,经过这个黑盒函数后,完成了不同语言的翻译。

Transformer主要结构包括两部分,编码层和解码层,也就是中间的黑盒可以进一步细化化下图。

编码层:6个编码器的堆叠,每个编码器结构相同,包括两部分, self-attention和feed forward,但不共享权重。

解码层:6个解码器的堆叠

注意:

1.编码层的输出输入到每一个解码器

2.“6”数字可以变化调整。

和Attention模型一样,Transformer模型中也采用了 encoer-decoder 架构。但其结构相比于Attention更加复杂,论文中encoder层由6个encoder堆叠在一起,decoder层也一样。

对于encoder,包含两层,一个self-attention层和一个前馈神经网络,self-attention能帮助当前节点不仅仅只关注当前的词,从而能获取到上下文的语义。decoder也包含encoder提到的两层网络,但是在这两层中间还有一层attention层,帮助当前节点获取到当前需要关注的重点内容。

现在我们知道了模型的主要组件,接下来我们看下模型的内部细节。首先,模型需要对输入的数据进行一个embedding操作,(也可以理解为类似w2c的操作),enmbedding结束之后,输入到encoder层,self-attention处理完数据后把数据送给前馈神经网络,前馈神经网络的计算可以并行,得到的输出会输入到下一个encoder。

2 Encoder

学习神经网络模型的核心就是抓住数据流,即数据的形式和维度,一般来说,输入都是矩阵/向量,这时就要重点关注经过每个模块后数据的维度变化。神经网络模型输入的形式是矩阵/向量,而现实中的数据是图片或句子或声音等模拟信号,所以第一步重要的就是把这些模拟信号转化成数字信号。在 CV 中,我们通常将输入图片转换为4维(batch, channel, height, weight)张量来表示。那么NLP中 如何将句子/单词编码数字化呢?

2.1 词嵌入与位置编码(positional encoding)

2.1.1 独热编码One-Hot Encoding

在 NLP 中,可以将输入单词用 One-Hot 形式编码成序列向量。向量长度是预定义的词汇表(想象一本词典)中拥有的单词量,向量在这一维中的值只有一个位置是1,其余都是0,1对应的位置就是词汇表中表示这个单词的地方。

例如词汇表中有5个词,第3个词表示“你好”这个词,那么该词对应的 one-hot 编码即为 00100(3个位置为1,其余为0

2.1.2 词向量嵌入Word Embedding

One-Hot 的形式看上去很简洁,也挺美,但劣势在于它很稀疏,而且还可能很。比如词汇表如果有 10k 个词,那么一个词向量的长度就需要达到 10k,而其中却仅有一个位置是1,其余全是0,太“浪费”!

更重要的是,这种方式无法体现出词与词之间的关系比如 “爱” 和 “喜欢” 这两个词,它们的意思是相近的,但基于 one-hot 编码后的结果取决于它们在词汇表中的位置,无法体现出它们之间的关系。

因此,我们需要另一种词的表示方法,能够体现词与词之间的关系,这种方法即 Word Embedding

那么应该如何设计这种方法呢?最方便的途径是设计一个可学习的权重矩阵 W(这个矩阵是致密的)将词向量与这个矩阵进行点乘,即得到新的表示结果,使得意思相近的词有相近的表示结果

假设 “爱” 和 “喜欢” 这两个词经过 one-hot 后分别表示为 10000 (1*5)和 00001,权重矩阵设计如下(5*3):

[ w00,w01,w02  w10, w11, w12
  w20, w21, w22
  w30, w31,w32
  w40, w41,w42 ]

那么两个词点乘后的结果分别是 [w00, w01, w02] 和 [w40, w41, w42],在网络学习过程中(这两个词后面通常都是接主语,如“你”,“他”等,或者在翻译场景,它们被翻译的目标意思也相近,它们要学习的目标一致或相近),权重矩阵的参数会不断进行更新,从而使得 [w00, w01, w02] [w40, w41, w42] 的值越来越接近

其实,可以将这种方式看作是一个 lookup table对于每个 word,进行 word embedding 就相当于一个lookup操作,在表中查出一个对应结果

另一方面,对于以上这个例子,我们还把向量的维度从5维压缩到了3维。因此,word embedding 还可以起到降维的效果。一个2x5的矩阵,乘上一个5x3的矩阵,变成一个2x3的矩阵。

A∗B=C

   在上述公式中,一个10个元素的A矩阵变成C中6个元素的矩阵,直观上大小缩小了近一半。

假设一个100Wx10W的矩阵,乘上一个10Wx20的矩阵,可以把它降到100Wx20的矩阵,降低10w/20=5000倍。

总结:在某种程度上,Embedding层实现了降维的作用,降维的原理是根据矩阵乘法。

Embedding就是用一个低维稠密的向量表示一个对象,这里的对象可以是一个词(Word2vec),也可以是一个物品(Item2vec),亦或是网络关系中的节点(Graph Embedding)。Embedding向量能够表达对象的某些特征,两个向量之间的距离反映了对象之间的相似性。简单的说,Embedding就是把一个东西映射到一个向量X。如果这个东西很像,那么得到的向量x1和x2的欧式距离很小。

在 Pytorch 框架下,可以使用 torch.nn.Embedding来实现 word embedding:

class Embeddings(nn.Module):
  def __init__(self, d_model, vocab):      
super(Embeddings, self).__init__()       
self.lut = nn.Embedding(vocab, d_model)        
self.d_model = d_model    
def forward(self, x):       
return self.lut(x) * math.sqrt(self.d_model)

其中,vocab 代表词汇表中的单词量,one-hot 编码后词向量的长度就是这个值;d_model代表权重矩阵的列数,通常为512,就是要将词向量的维度从 vocab 编码到 d_model

2.1.3位置编码(positional encoding)       

那么是不是词嵌入就完成的单词的向量化呢?还差了一步。因为这只是编码了一个个独立的单词,但是我们还需要知道每个单词在输入句子中的位置和顺序,这样便可以进一步掌握单词之间的内在关系。一种让模型感知顺序的方法是对每个词加一小块它在句子中的位置信息,我们把这"一小块信息"叫做位置编码。

字词的位置和顺序是所有语言的基本组成部分。它们定义了语法从而定义了句子的实际语义。循环神经网络(RNNs)自带了对字词顺序的考虑,它们通过顺序地逐字解析一条句子,这样就把字词的顺序集成到了RNNs的主干当中。

比如“小红喜欢小白”和“小白喜欢小红”两句,因为词语的位置不同,则表达的意思是不同的。

但是Transformer架构抛弃了循环机制,选择了多头自注意力机制。它避免了RNNs循环的方法,使得训练时间大大加速。并且理论上,它可以捕获句子中更长距离的依赖(self-attention)。但也是因为transformer所有的输入都是一起的,不会像RNN输入一个处理一个,所以其需要位置编码。

脑子冒出来的第一种想法是对每个时间戳赋一个[0,1]的值,其中0代表第一个字,1代表最后一个时间戳。你能想到这样会导致什么样的问题吗?其中一个它会产生的问题是,指定了这样的范围,你不知道里面总共表示了多少个词。换句话说,不同句子之间,时间戳的增量没有一致的意义

另一个想法是线性地赋予每个时间戳一个数字。就是说,第一个字给它个"1",第二个字给它个"2",以此类推。这个方法的问题是不仅这个值可能会变得很大,而且我们的模型可能会遇到比训练里更长的句子。再更进一步说,我们的模型可能会从来没看过某个特定长度的样本,从而损伤了模型的泛化能力

理想地说,位置编码要满足以下这些标准:

  • 唯一性:它应该对每个时间戳(句子当中字词的位置)输出一个唯一的编码
  • 一致性:不同长度的句子当中,任意两个时间戳的距离应该要一致。(前提这两个时间戳相对距离一样)
  • 有界性:我们的模型应该在不付出任何努力的条件下泛化到更长的句子。它的编码值应该要有限(有界)。
  • 确定性:它必须是确定性的。

那么具体该怎么做?我们通常容易想到两种方式:

  • 通过网络来学习;
  • 预定义一个函数,通过函数计算出位置信息;

Transformer 的作者对以上两种方式都做了探究,发现最终效果相当,于是采用了第2种方式,从而减少模型参数量,同时还能适应即使在训练集中没有出现过的句子长度。

论文中的计算方法如下

其中,pos是指当前词在句子中的位置(比如一个sentence当中有10个词,pos=0~9),d_model是词向量维度,i是指向量中每个值的index(这里的向量维度是1*512,和词嵌入向量d_model相同,也是为了后续位置向量和词嵌入向量可以直接相加, 512维时,  i=0~255)。

可以看出,在偶数位置,使用正弦编码,在奇数位置,使用余弦编码。最终输出的是一个max_len*d_model的张量,max_len代表句子中的单词数量,每一行代表单词的通过上述公式计算得到的位置向量。

以三个单词每个词向量维度为4为例计算得到:

这种计算方式使得最终输出的每一列都对应一个正弦曲线。

图中横坐标代表pos,颜色代表i,

为何使用三角函数呢?

由于三角函数的性质: sin(a+b) = sin(a)cos(b) + cos(a)sin(b)、 cos(a+b) = cos(a)cos(b) - sin(a)sin(b),于是,对于位置 pos+k 处的信息,可以由 pos 位置计算得到,作者认为这样可以让模型更容易地学习到位置信息。

为何使用这种方式编码能够代表不同位置信息呢?

由公式可知,每一维i都对应不同周期的正余弦曲线:i=0时是周期为2π的sin函数,i=1时是周期为2π的cos函数。对于不同的两个位置pos1和pos2(不同行),若它们在某一维i(同一列)上有相同的编码值,则说明这两个位置的差值等于该维所在曲线的周期,即|pos1-pos2|=T1 。而对于另一个维度j(j=/i),由于Tj=/Ti ,因此 pos1和pos2在这个维度j上的编码值就不会相等,对于其它任意k{0,1,2…d-1},k=/i,也是如此。综上可知,这种编码方式保证了不同位置在所有维度上不会被编码到完全一样的值,从而使每个位置都获得独一无二的编码。

那是如何表示单词之间相互距离的呢?

2.14.最终输入

如果我们的嵌入维度为4,那么实际上的位置编码就如下图所示:

最后把这个Positional Encodingembedding的值相加,作为输入送到下一层

但值得注意的是,位置编码仅在最下面一层编码器上操作,后续堆叠的编码器不再做此操作。

2.2 自注意力机制(Self-attention)

为什么要自注意力机制?为了获取一个单词和句子中其它单词的关系(相关性)。

“The animal didn't cross the street because it was too tired ”

比如句子中的“it”指的是什么?

2.2.1 注意力机制
注意力机制的理解

在Attention诞生之前,已经有CNN和RNN及其变体模型了,那为什么还要引入attention机制?主要有两个方面的原因,如下:

(1)计算能力的限制:当要记住很多“信息“,模型就要变得更复杂,然而目前计算能力依然是限制神经网络发展的瓶颈。

(2)优化算法的限制:LSTM只能在一定程度上缓解RNN中的长距离依赖问题,且信息“记忆”能力并不高。

注意力机制其实是源自于人对于外部信息的处理能力。由于人每一时刻接受的信息都是无比的庞大且复杂,远远超过人脑的处理能力,因此人在处理信息的时候,会将注意力放在需要关注的信息上,对于其他无关的外部信息进行过滤,这种处理方式被称为注意力机制。

1.1非自主提示和自主提示

针对于注意力机制的引起方式,可以分为两类,

非自主提示:指的是由于物体本身的特征十分突出引起的注意力倾向,源自于物体本身。

自主提示:指的是经过先验知识的介入下,对具有先验权重的物体引起的注意力倾向,源自于一种主观倾向。

举例说明如下:

当我们第一眼看到上图时,我们便会首先将注意力集中到兔子身上。这是因为,整张图中兔子的特征十分的突出,让人一眼就关注到兔子身上。这种引起注意力的方式便是非自主提示。在看到兔子之后,我们便想兔子在干嘛,从而我们就会关注兔子的行为。此时兔子在吃草,这时我们便把注意力集中在兔子周边的草上。这种引起注意力机制的方式便是自主提示,其中"兔子在干嘛"则是我们主观意识。

另外,为加深理解,再引用一下动手学深度学习中的例子解释:

此时我们面前有五个物体,分别是报纸,论文,咖啡,笔记本和书。首先,我们会关注在咖啡身上,因为只有咖啡是红色,而其他物体是黑白。那么红色的咖啡由于其显眼的特征,就成了注意力机制的非自主提示。

喝完咖啡后,十分精神,想看本书。此时,通过"想看书"这种意识,我们将注意力放到了书上。这种通过主观意识引起注意力的方式称为自主提示。

注意力机制的设计

根据自主提示和非自主提示来设计注意力机制。

首先考虑简单情况,即只考虑非自主提示的话,只需要对所有物体的特征信息(非自主提示)进行简单的全连接层,甚至是无参数的平均汇聚层或者最大汇聚层,就可以提取出需要感兴趣的物体。

下图是平均汇聚方法的示例图,最后结果是所有物体向量的平均加权和。

而如果考虑自主提示的话,我们就需要设计一种通过查询(Query),键(Key)和值(Value) 来实现注意力机制的方法。其中Query指的是自主提示,即主观意识的特征向量Key指的是非自主提示,即物体的突出特征信息向量Value则是代表物体本身的特征向量

注意力机制是通过Query与Key的注意力汇聚(指的是QueryKey的相关性进行建模,实现池化筛选或者分配权重),实现Value的注意力权重分配,生成最终的输出结果。如下图所示:

另外,还有一种理解方式。我们可以将查询,键和值理解为一种软寻址(Soft Addressing)

Value可以看作存储器存储的内容,Key看作是存储器的地址。当Key==Query时,则取出Key地址对应存储器中的Value值,这被称为硬寻址。

而软寻址则是通过计算Key和Query的相似度来进行寻址,这种方法不只是获取一个Key地址中存储器的Value值,而是获取所有的存储器中的Value值的加权和 。至于每个Value的权重(重要程度),是通过Key和Query相似度计算得到的,最终的输出是所有Value值和其权重的加权和。如下图所示:

注意力机制模型

从本质上理解,Attention是从大量信息中筛选出少量重要信息,并聚焦到这些重要信息上,忽略大多不重要的信息。权重越大越聚焦于其对应的Value值上,即权重代表了信息的重要性,而Value是其对应的信息。

至于Attention机制的具体计算过程,如果对目前大多数方法进行抽象的话,可以将其归纳为两个过程:

第一个过程是根据QueryKey计算权重系数第二个过程根据权重系数对Value进行加权求和

而第一个过程又可以细分为两个阶段:第一个阶段根据Query和Key计算两者的相似性或者相关性;第二个阶段对第一阶段的原始分值进行归一化处理;这样,可以将Attention的计算过程抽象为如图展示的三个阶段。

在第一个阶段,可以引入不同的函数和计算机制,根据Query和某个 Keyi ,计算两者的相似性或者相关性,第一阶段产生的分值根据具体产生的方法不同其数值取值范围也不一样。最常见的方法包括:求两者的向量点积、求两者的向量Cosine相似性或者通过再引入额外的神经网络来求值,即如下方式:

点积:SimilarityQuery,Keyi=QueryKeyi

Cosine相似性:SimilarityQuery,Keyi=Query∙KeyiQueryKeyi

MLP网络:SimilarityQuery,Keyi=MLP(Query,Keyi)

第二阶段引入类似SoftMax的计算方式对第一阶段的得分进行数值转换,一方面可以进行归一化,将原始计算分值整理成所有元素权重之和为1的概率分布;另一方面也可以通过SoftMax的内在机制更加突出重要元素的权重。即一般采用如下公式计算:

αi=SoftmaxSimilarityi=eSimilarityij=1LxeSimilarityi

第二阶段的计算结果 αi即为 Valuei对应的权重系数,然后进行加权求和即可得到Attention数值:

AttentionQuery,Source=i=1LxαiValuei

通过如上三个阶段的计算,即可求出针对Query的Attention数值,目前绝大多数具体的注意力机制计算方法都符合上述的三阶段抽象计算过程。

2.2.2 Self-attention自注意力机制

自注意力机制和注意力机制的区别就在于,注意力机制的查询和键是不同来源的,例如,在Encoder-Decoder模型中,键是Encoder中的元素,而查询是Decoder中的元素。在中译英模型中,查询是中文单词特征,而键则是英文单词特征。而自注意力机制的查询和键则都是来自于同一组的元素,例如,在Encoder-Decoder模型中,查询和键都是Encoder中的元素,即查询和键都是中文特征,相互之间做注意力汇聚。也可以理解为同一句话中的词元或者同一张图像中不同的patch,这都是一组元素内部相互做注意力机制,因此,自注意力机制(self-attention)也被称为内部注意力机制(intra-attention

自注意力机制是注意力机制的变体,其减少了对外部信息的依赖,更擅长捕捉数据或特征的内部相关性。

自注意力机制在文本中的应用,主要是通过计算单词间的互相影响,来解决长距离依赖问题。

优点:可以建立全局的依赖关系,扩大图像的感受野。相比于CNN,其感受野更大,可以获取更多上下文信息。

缺点:自注意力机制是通过筛选重要信息,过滤不重要信息实现的,这就导致其有效信息的抓取能力会比CNN小一些。这所以这样是因为自注意力机制相比CNN,无法利用图像本身具有的尺度,平移不变性,以及图像的特征局部性(图片上相邻的区域有相似的特征,即同一物体的信息往往都集中在局部)这些先验知识,只能通过大量数据进行学习。这就导致自注意力机制只有在大数据的基础上才能有效地建立准确的全局关系,而在小数据的情况下,其效果不如CNN。

2.2.3 Self-attention计算过程

自注意力机制的计算过程:

1.将输入单词转化成嵌入向量;

2.根据嵌入向量得到q,k,v三个向量;

3.为每个向量计算一个score:score =q . k ;

4.为了梯度的稳定,Transformer使用了score归一化,即除以矩阵列维度的开方(论文中是64的开方8) ;

5.对score施以softmax激活函数;

6.softmax点乘Value值v,得到加权的每个输入向量的评分v;

7.相加之后得到最终的输出结果z :z= v。

接下来我们详细看一下self-attention,其思想和attention类似,但是self-attention是Transformer用来将其他相关单词的“理解”转换成我们正在处理的单词的一种思路,我们看个例子:

The animal didn't cross the street because it was too tired

这里的it到底代表的是animal还是street呢,对于我们来说能很简单的判断出来,但是对于机器来说,是很难判断的,self-attention就能够让机器把it和animal联系起来,接下来我们看下详细的处理过程。

Step1:首先,self-attention会根据编码器的输入向量(每个单词)计算出三个新的向量,分别称为Query、Key、Value,这三个向量是分别用embedding输入向量1*512与三个矩阵相乘得到的结果,三个矩阵是随机初始化的,其值在BP的过程中会一直进行更新,维度为51264

如下图:X1的维度是(1,512),WK的维度是(512,64),q1的维度是(1,64)

计算公式为:

q1=X1WQ

k1=X1WK

v1=X1WV

得到的这三个向量的维度是64,低于embedding维度512,但是它们的维度不一定非要更小。这里取64的值更多是为了使得后面拼接多头注意力输出的Z值矩阵维度相同(64*8=512)。如果只是单头注意力的话,维度也可以设为512。

Step2计算self-attention的分数值,该分数值决定了当我们在某个位置encode一个词时,对输入句子的其他部分的关注程度。这个分数值的计算方法是Query与Key做点乘。

SimilarityQuery,Keyi=QueryKeyi

以下图为例,首先我们需要针对Thinking这个词,计算出其他词对于该词的一个分数值,首先是针对于自己本身即q1·k1,然后是针对于第二个词即q1·k2

Step3接下来,把点乘的结果除以一个常数然后把得到的结果做一个softmax的计算。这里我们除以8,这个值一般是采用上文提到的矩阵的列维度的开方即64的开方8,当然也可以选择其他的值。最终得到的结果即是每个词对于当前位置的词的相关性大小,当然,当前位置的词相关性肯定会很大,这些值均为正且和为1.

这种通过 query 和 key 的相似性程度来确定 value 的权重分布的方法被称为scaled dot-product attention。其实scaled dot-Product attention就是我们常用的使用点积进行相似度计算的attention,只是多除了一个(为K的维度)起到调节作用,使得内积不至于太大。

Step4下一步就是把Value和softmax得到的值进行相乘,并相加,得到的结果即是self-attetion在当前节点的值。

以有4个单词的句子为例(注意一下这里的):

        在计算信息值的时候,是并行计算的,如下。

在实际的应用场景,为了提高计算速度,我们采用的是矩阵的方式,直接计算出Query, Key, Value的矩阵,然后把embedding的值与三个矩阵直接相乘,把得到的新矩阵Q与K相乘,乘以一个常数,做softmax操作,最后乘上V矩阵

最后,用矩阵形式将上述几个步骤统一到下面这个公式里面,即:

2.2.4 多头自注意力机制Multi-head attention

单一注意力机制,只会建立一种查询和键的依赖关系。而我们常希望可以基于相同的注意力汇聚方法学习到不同的依赖关系,然后将这些依赖关系组合起来,实现捕获序列内各种范围的依赖关系。

例如,机器翻译任务,以 " I like fishing because it can relax my mind " 要翻译为 " 我喜欢钓鱼,因为可以放松心灵 " 为例,我们以"放松"为Query,对英文句子中每个单词的Key进行注意力汇聚,结果获取"放松"和"relax"的依赖关系。这是单头注意力的结果。

如果我们进行多次注意力汇聚,则可能捕获"放松"和"fishing", "I"等单词的依赖关系。这样,我们将多个结果进行融合就可以得到更为全面,复杂的依赖关系,这对于深度学习下游任务,例如目标检测,语义分割等都具有很大帮助。这就是本章节要介绍的多头注意力汇聚方法。

为了让注意力更好的发挥性能,作者提出了多头注意力的思想,其实就是将每个querykeyvalue分出来多个分支,有多少个分支就叫多少头(是一个矩阵分成多块,还是多个矩阵),就是说不仅仅只初始化一组QKV的矩阵,而是初始化多组,tranformer是使用了8组,所以最后得到的结果是8个矩阵。对Q, K, V求多次不同的注意力计算,得到多个不同的output,再把这些不同的output拼接起来得到最终的output。

论文通过添加一种称为“多头”注意力的机制,进一步细化了自注意力层。这通过两种方式提高了注意力层的性能:

它扩展了模型注意于不同位置的能力。在上面的例子中,z1包含了每个单词的部分编码,但它可能被自身所支配。如果我们翻译一句话,比如“这只动物没有过马路是因为它太累了”,那么知道“它”指的是哪个词会很有用。

它为注意力层提供了多个“表示子空间”。正如我们接下来将看到的,对于多头注意力,我们不仅有一组,而且有多组查询/键/值权重矩阵(Transformer使用八个注意力头,所以我们最终为每个编码器/解码器使用八组)。这些集合中的每一个都是随机初始化的。然后,在训练之后,使用每个集合将输入嵌入(或来自较低编码器/解码器的向量)投影到不同的表示子空间中。

主要思想就是在于:希望不同注意力的output可以从不同层面(representation subspace)考虑关联性而得到最终的输出。

做8次不同的自注意力层计算,会得到8个Z值矩阵,

这给我们留下了一个小的挑战,前馈神经网络没法输入8个矩阵呀,这该怎么办呢?所以我们需要一种方式,把8个矩阵降为1个,首先,我们把8个矩阵连在一起,这样会得到一个大的矩阵,再随机初始化一个大矩阵和这个组合好的矩阵相乘,最后得到一个最终的矩阵。也就意味着最初求的QKV矩阵也分别是8个。

这就是multi-headed attention的全部流程了,把所有的矩阵放到一张图内看一下总体的流程。

既然我们已经谈到了注意力头,让我们重新审视之前的例子,看看当我们在例句中对单词“it”进行编码时,不同的注意力头会集中在哪里(这里不同颜色代表attention不同头的结果,颜色越深attention值越大):

然而,如果我们把所有的注意力都放在画面上,事情可能会更难解读:

对于使用自注意力机制的原因,论文中提到主要从三个方面考虑(每一层的复杂度,是否可以并行,长距离依赖学习),并给出了和RNN,CNN计算复杂度的比较。可以看到,如果输入序列n(句子中单词数量)小于表示维度d的话,每一层的时间复杂度self-attention是比较有优势的。当n比较大时,作者也给出了一种解决方案self-attention(restricted)即每个词不是和所有词计算attention,而是只与限制的r个词去计算attention。在并行方面,多头attention和CNN一样不依赖于前一时刻的计算,可以很好的并行,优于RNN。在长距离依赖上,由于self-attention是每个词和所有词都要计算attention,所以不管他们中间有多长距离,最大的路径长度也都只是1,可以捕获长距离依赖关系

2.3 残差

在继续之前,我们需要提到编码器架构中的一个细节,即每个编码器中的每个子层(自注意力层)周围都有一个残差连接,然后是层规范化步骤。

用向量可视化的话,如下图:

其中Add代表了Residual Connection,是为了解决多层神经网络训练困难的问题,通过将一部分的前一层的信息无差的传递到下一层,可以有效的提升模型性能——因为对于有些层,我们并不确定其效果是不是正向的。加了残差连接之后,我们相当于将上一层的信息兵分两路,一部分通过我们的层进行变换,另一部分直接传入下一层,再将这两部分的结果进行相加作为下一层的输出。这样的话,其实可以达到这样的效果:我们通过残差连接之后,就算再不济也至少可以保留上一层的信息,这是一个非常巧妙的思路。

这一方法之前在图像处理结构如ResNet等中常常用到。

2.4 层归一化Layer normalization

  Normalization有很多种,但是它们都有一个共同的目的,那就是把输入转化成均值为0方差为1的数据。我们在把数据送入激活函数之前进行normalization(归一化),得到的值大部分会落入非线性函数的线性区,导数远离导数饱和区,避免了梯度消失和梯度爆炸,这样来加速训练收敛过程。归一化技术就是让每一层的分布稳定下来,让后面的层能在前面层的基础上“安心学习”。

2.4.1 batch normalization

batch normalization是对一批样本同一纬度特征做归一化。如下图我们想根据这个batch中的三种特征(身高、体重、年龄)数据进行预测性别,首先我们进行归一化处理,如果是Batch normalization操作则是对每一列特征进行归一化,如下图求一列身高的平均值。

BN特点:强行将数据转为均值为0,方差为1的正态分布,使得数据分布一致,并且避免梯度消失。而梯度变大意味着学习收敛速度快,能够提高训练速度。

2.4.2 layer normalization

而layer normalization是对单个样本的所有维度特征做归一化。如下表中,如果是Layer normalization则是对每一行(该条数据)的所有特征数据求均值。

2.4.3Transformer为什么用Layer Normalization

从操作上看:BN是对同一个batch内的所有数据的同一个特征数据进行操作;而LN是对同一个样本进行操作。

从特征维度上看:BN中,特征维度数=均值or方差的个数;LN中,一个batch中有batch_size个均值和方差

如在NLP中上图的C、N、H,W含义:

N:N句话,即batchsize;

C:一句话的长度,即seqlen;

H,W:词向量维度embedding dim。

BN不适合RNN、transformer等序列网络,不适合文本长度不定和batchsize较小的情况,适合于CV中的CNN等网络;

LN适合用于NLP中的RNNtransformer等网络,因为sequence的长度可能是不一致的

栗子:如果把一批文本组成一个batchBN就是对每句话的第一个词进行操作,BN针对每个位置进行缩放就不符合NLP的规律了。而LN则是对一句话的所有词进行操作。

2.5前馈神经网络

FeedForward的输入是什么呢?是Multi-Head Attention的输出做了残差连接和Norm之后得数据,然后FeedForward做了两次线性线性变换一次ReLU激活函数(代码中还有一次dropout),为的是更加深入的提取特征。

前馈全连接层(feed-forward linear layer)基本上就是一堆神经元,每个神经元都与其他神经元相连接。

请看下图,其中a、b、c和d是神经元。这些神经元包含了一些 input(即一些我们想要理解的数据(像素值(pixels)、词嵌入(word embeddings)等))。它们与编号为1的神经元相连。每两个神经元之间的连接都有不同的连接权重值(connection strength)。例如,a-1是0.1,b-1是0.2,等等。

实际上,左列中的所有神经元都与右列中的所有神经元相连。但是为了清晰起见,我没有在图像中展示全部的连接,你需要了解这一情况。就像图中有a-1一样,还应该有a-2、b-2、c-2、d-3等。两个神经元之间的每个连接都有不同的“连接权重”。

尽管全连接层(Fully connected layers)的使用非常广泛,但也存在一个很大的缺点——它们是线性层(linear layers),只能进行线性变换和线性计算。全连接层可以进行加法和乘法运算,但无法以创造性的方式转换输入(input)。有时候,仅仅增加计算量是不够的,需要以完全不同的思考方式来解决问题。

激活函数能够帮助我们进行非线性变换(non-linear transformation)。例如,将一个数字列表[1, 4, -3, 5.6]转换为概率分布,就是Softmax激活函数的作用。该激活函数能够将这些数字转换为[8.29268754e-03, 1.66563082e-01, 1.51885870e-04, 8.24992345e-01]这样的输出。这5个数字相加等于1。虽然这些数字看起来有些混乱,但 e-03 表示第一个数字(8)在小数点后3个零开始(例如0.00,然后是82926。实际上该数字是0.00829268754)。这个Softmax激活函数将整数转换为01之间的浮点数,转换后的浮点数仍然保持了原始整数之间的相对大小关系。这种保持相对大小关系的特性在统计学中非常有用。

还有其他类型的激活函数,其中最常用的之一是ReLU(修正线性单元)。这是一种非常简单(同时也非常有用)的激活函数,它能够将任何负数转化为0,而非负数保持不变。非常简单且实用。如果我将列表[1, -3, 2]输入ReLU函数,会得到[1, 0, 2]。

所以FeedForward的作用是:通过线性变换,先将数据映射到高纬度的空间再映射到低纬度的空间,提取了更深层次的特征。如图:

FeedForward的计算公式如下:

FFNx=ReLu0,xW1+b1W2+b2

2.6 Decode模块

如上已经介绍完毕一个编码器的结构,论文中将6个相同的编码器堆叠在一起就构成了编码模块。如下图是2个编码器堆叠在一起的形式。

3 Decoder层

Decoder部分其实和encoder部分大同小异,不过在最下面额外多了一个masked mutil-head attetion。在解码器中,自注意层仅被允许关注输出序列中前面的位置。这是通过在自注意力计算中的softmax步骤之前屏蔽未来位置(将其设置为-inf)来完成的。

Mask机制经常被用于NLP任务中,按照作用总体来说可以分成两类:

  1. 用于处理非定长序列padding mask(非官方命名);
  2. 用于防止标签泄露sequence mask(非官方命名)。

Transformer中同时用到了这两种Mask机制。

3.1 Mask

mask 表示掩码,它对某些值进行掩盖,使其在参数更新时不产生效果。Transformer 模型里面涉及两种 mask,分别是 padding  mask 和 sequence mask。

其中,padding mask 在所有的 scaled dot-product attention 里面都需要用到,而 sequence mask 只有在 decoder 的 self-attention 里面用到。

padding mask

在NLP任务中,文本通常是不定长的,计算attention score会出现偏差,所以在输入一个样本长短不一的batch(这里的batch指有多个句子)到网络前,要对batch中的样本进行truncating截断/padding补齐操作,以便能形成一个张量的形式输入网络,如下图所示。

具体的做法是,对于一个长度不足的样本,往往采用特殊字符"<PAD>"进行padding,把这些位置的值加上一个非常大的负数(负无穷),这样的话,经过 softmax,这些位置的概率就会接近0!在训练时利用Mask矩阵将补全的位置给mask。Mask矩阵中可以用1表示有效字,0代表无效字(也可以用True/False)。值为 false 的地方就是我们要进行处理的地方,也就是说输出矩阵和Mask矩阵为false的对应位置处补一些无穷小(负无穷)的值。

分为如下两步:

1padding(补齐)操作在batch输入网络前完成,同步生成padding mask矩阵(用于记录后续输出矩阵需要mask的位置);

2)根据padding mask矩阵,QK在点积之后,需要先经过mask,将输出矩阵和Mask矩阵为false的对应位置处补一些无穷小(负无穷)的值,再进行softmax.

def attention(query, key, value, mask=None, dropout=None):

      "Compute 'Scaled Dot Product Attention'"

      d_k = query.size(-1)

      scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)

      if mask is not None:

          scores = scores.masked_fill(mask == 0, -1e9) # mask步骤,用 -1e9 代表负无穷

      p_attn = F.softmax(scores, dim = -1)

      if dropout is not None:

          p_attn = dropout(p_attn)

      return torch.matmul(p_attn, value), p_attn
sequence mask

sequence mask有各种各样的形式和设计,最常见的应用场景是在需要一个词预测下一个词的时候,如果self attention 或者是其他同时使用上下文信息的机制,会导致模型提前看到待预测的内容,这显然不行,所以为了不泄露要预测的标签信息,就需要 mask 遮盖它。

如下图所示,这也是TransformerDecoderMasked Multi-Head self-attention使用的Mask机制

除了在decoder部分加入mask防止标签泄露以外,还有模型利用这种填空机制帮助模型学的更好,比如说BERT和ERNIE模型中利用到的Masked LM(MLM)。(注意:BERT模型只有TransformerEncoder层,是可以学习上下文信息的

文章前面也提到,sequence mask 是为了使得 decoder 不能看见未来的信息。也就是对于一个序列,在 time_step 为 t 的时刻,我们的解码输出应该只能依赖于 t 时刻之前的输出,而不能依赖 t 之后的输出。因此我们需要想一个办法,把 t 之后的信息给隐藏起来。

那么具体怎么做呢?也很简单:产生一个上三角矩阵,上三角的值全为0。把这个矩阵作用在每一个序列上,就可以达到我们的目的。

  • 对于 decoder 的 self-attention,里面使用到的 scaled dot-product attention,同时需要padding mask 和 sequence mask 作为 attn_mask,具体实现就是两个mask相加作为attn_mask
  • 其他情况,attn_mask 一律等于 padding mask。

3.2 Decoder

 “编码器-解码器-注意力”层的工作原理与多头自注意力类似,只是它从下面的层创建查询矩阵,并从编码器堆栈的输出中获取键和值矩阵。编码器通过处理输入序列,然后将顶部编码器的输出转换为一组注意向量kv

每个解码器将在其“encoder-decoder attention”层中使用这些注意向量,这有助于解码器将注意力集中在输入序列中的适当位置。

解码阶段的每个步骤从输出序列(本例中为英语翻译句)输出一个元素
以下步骤重复此过程,一直到达到表示解码器已完成输出的符号。每一步的输出在下一个时间步被送入底部解码器,解码器像就像我们对编码器输入所做操作那样,我们将位置编码嵌入并添加到这些解码器输入中,以表示每个字的位置。

3.3 输出层linear 和softmax

解码器堆栈输出一个浮点向量。我们如何将其转化为一个词?这是最后一个线性层的工作,后面是Softmax层。,假如我们的词典是1w个词,那最终softmax会输入1w个词的概率,概率值最大的对应的词就是我们最终的结果。
         线性层是一个简单的全连接神经网络,它将解码器堆栈产生的向量投影到一个更大的向量中,称为logits向量。

让我们假设我们的模型知道10000个独特的英语单词(我们模型的“输出词汇表”),这些单词是从训练数据集中学习的。这将使logits矢量宽度为10000个单元格——每个单元格对应一个唯一单词的分数。这就是我们解释线性层后面的模型输出的方式。

softmax层然后将这些分数转换为概率(全部为正,加起来为1.0)。选择具有最高概率的单元,并产生与其相关联的字作为该时间步长的输出。

4 模型训练

现在我们已经通过一个经过训练的Transformer介绍了整个前向传递过程,最后看看模型是如何训练的。

在训练过程中,未经训练的模型会经历完全相同的正向传递。但由于我们在标记的训练数据集上训练它,我们可以将其输出与实际的正确输出进行比较。

为了形象化这一点,假设输出词汇表只包含六个单词(“a”、“am”、“i”、“thanks”、“student”和“<eos>”(“句末”的缩写))。

一旦我们定义了输出词汇表,我们就可以使用相同宽度的向量来指示词汇表中的每个单词。这也被称为一个热编码。例如,我们可以使用以下向量来表示单词“am”: 注意这里的独热编码是监督量,实际上是和decoder层最后softmax输出的预测概率向量作比较的。

示例:我们的输出词汇表的一个热门编码

再讨论一下模型的损失函数——我们在训练阶段优

假设我们正在训练我们的模型。假设这是我们在培训阶段的第一步,我们正在以一个简单的例子进行培训——将“merci”翻译成“谢谢”。

这意味着,我们希望输出是一个概率分布,表示单词“谢谢”。但由于这个模型还没有经过训练,目前还不太可能实现。

由于模型的参数(权重)都是随机初始化的,(未经训练的)模型为每个单元/单词产生具有任意值的概率分布。我们可以将其与实际输出进行比较,然后使用反向传播调整模型的所有权重,使输出更接近所需输出。

你如何比较两种概率分布?我们只是从另一个中减去一个。

但请注意,这是一个过于简单化的例子。更现实地说,我们将使用比一个单词更长的句子。例如,输入:“je suisétudant”,预期输出:“我是一名学生”。这真正意味着,我们希望我们的模型连续输出概率分布,其中:

每个概率分布由宽度为vocab_size的矢量表示(在我们的示例中为6,但更实际的是一个类似30000或50000的数字)

第一个概率分布在与单词“i”相关联的单元处具有最高概率

第二个概率分布在与单词“am”相关联的单元格处具有最高概率

依此类推,直到第五个输出分布指示“<句子结束>”符号,该符号也有一个与10000元素词汇表中的单元格相关联。

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

在足够大的数据集上训练模型足够长的时间后,我们希望产生的概率分布如下所示:

希望通过培训,该模型能够输出我们期望的正确翻译。当然,这并不能真正表明这个短语是否是训练数据集的一部分(请参阅:交叉验证)。请注意,每个位置都有一点概率,即使它不太可能是该时间步长的输出——这是softmax的一个非常有用的特性,有助于训练过程。

现在,因为模型一次产生一个输出,我们可以假设模型从概率分布中选择概率最高的单词,然后扔掉其余的。这是一种方法(称为贪婪解码)。另一种方法是抓住前两个单词(例如“I”和“a”),然后在下一步中运行模型两次:一次假设第一个输出位置是单词“I”,另一次假设第二个输出位置为单词“a”,无论哪个版本在考虑位置#1和#2的情况下都会产生较小的错误。我们对位置#2和#3重复此操作…等。这种方法被称为“波束搜索”,在我们的例子中,beam_size是两个(意味着在任何时候,两个部分假设(未完成的翻译)都保存在内存中),而top_beams也是两个(这意味着我们将返回两个翻译)。这两个超参数都可以进行实验。

参考资料

The Illustrated Transformer – Jay Alammar – Visualizing machine learning one concept at a time.

李宏毅transformer视频课程

Transformer模型详解(图解最完整版) - 知乎

Transformer 修炼之道(一)、Input Embedding - 知乎

【Transformer系列】深入浅出理解Embedding(词嵌入)_transformer embedding-CSDN博客

Transformer:注意力机制(attention)和自注意力机制(self-attention)的学习总结_注意力机制和自注意力机制-CSDN博客

10.1. 注意力提示 — 动手学深度学习 2.0.0 documentation

Attention注意力机制与self-attention自注意力机制 - 知乎

【深度学习】batch normalization和layer normalization区别_layer normalization和batch normalization-CSDN博客

https://zhuanlan.zhihu.com/p/665269977

  • 31
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Transformer 是一种用于自然语言处理任务的深度学习模型,它是由 Google 在 2017 年提出的。相比于传统的循环神经网络和卷积神经网络,Transformer 采用了全新的 Encoder-Decoder 架构,使用了自注意力机制(Self-Attention Mechanism)来处理输入序列,从而在机器翻译、文本摘要、对话生成等任务上取得了很好的效果。 Transformer 本质上是一个 Encoder-Decoder 架构,其中 Encoder 和 Decoder 都是由多个相同的层堆叠而成。每个层都包含了两个子层,分别是 Multi-Head Attention 和 Feed Forward Neural Network。其中 Multi-Head Attention 是 Transformer 的核心,它使用了自注意力机制来计算输入序列中每个词与其他词之间的关系,从而更好地捕捉上下文信息。 Transformer 的训练过程分为两个阶段:第一阶段是无监督的预训练,使用大量的无标注数据来训练模型;第二阶段是有监督的微调,使用有标注的数据来微调模型,使其适应特定的任务。 下面是一个简单的 Transformer 模型的实现代码: ```python import torch import torch.nn as nn import torch.nn.functional as F class Transformer(nn.Module): def __init__(self, input_dim, hidden_dim, output_dim, num_layers, num_heads, dropout): super().__init__() self.embedding = nn.Embedding(input_dim, hidden_dim) self.encoder_layers = nn.ModuleList([EncoderLayer(hidden_dim, num_heads, dropout) for _ in range(num_layers)]) self.decoder_layers = nn.ModuleList([DecoderLayer(hidden_dim, num_heads, dropout) for _ in range(num_layers)]) self.fc_out = nn.Linear(hidden_dim, output_dim) self.dropout = nn.Dropout(dropout) self.scale = torch.sqrt(torch.FloatTensor([hidden_dim])).to(device) def forward(self, src, trg, src_mask, trg_mask): src_len, batch_size = src.shape trg_len, batch_size = trg.shape src_pos = torch.arange(0, src_len).unsqueeze(1).repeat(1, batch_size).to(device) trg_pos = torch.arange(0, trg_len).unsqueeze(1).repeat(1, batch_size).to(device) src = self.dropout((self.embedding(src) * self.scale) + src_pos) trg = self.dropout((self.embedding(trg) * self.scale) + trg_pos) for layer in self.encoder_layers: src = layer(src, src_mask) for layer in self.decoder_layers: trg = layer(trg, src, trg_mask, src_mask) output = self.fc_out(trg) return output ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值