transformer的模型架构如下:
从图中可以看出,transformer是一个seq2seq结构的模型,是一种比较适合做文本生成之类任务的模型。在tensorflow的官方教程中就给出了使用transformer进行葡萄牙语到英语的翻译。
首先来简单介绍一下seq2seq。在seq2seq结构的模型一般会分成encoder和decoder两部分。在以往的模型中,encoder或者decoder都会使用lstm或者gru之类的序列模型来对输入或者输出的数据进行编码和解码。比如,下图中,输入为
this is a lovely dog.
经过encoder编码器编码成一个表征输入特征的一个向量context vector之后,再将这个向量输入到解码器decoder中进行解码,在序列模型的解码器中输出对应的翻译结果
这是一只可爱的小狗。
而transformer也是采用类似的seq2seq结构,但不同的是,transformer在编码器和解码器中使用的不是lstm或者gru这样的序列模型,而是使用self-attention。
既然transformer中使用了self-attention,那么我们再简单介绍一下注意力机制,在上面给出的简单的翻译例子中,输入的this is a lovely dog.
和输出的这是一只可爱的小狗。
这两句话之间的词的位置都是一一对应的。但是,在实际任务中,输入句子和输出句子之间的词的位置并不都是一一对应。比如,在英语中的he is smart very much.
翻译过来是他非常聪明。
,这里的词的位置就不是一一对应了。那么,在解码器进行翻译输出的时候,他怎么知道当前需要翻译的词应该对应输入句子中的哪一个词的呢?即,解码器怎么知道它应该把它的注意力放在输入句子中的哪些词上,才能给出更加准确合理的译文。那么,这里就需要使用到注意力机制了。
正如上面所言,注意力机制的目的就是让解码器知道自己在进行解码的时候需要把更多注意力放在输入的哪些词上,以给出更准确的输出。注意力机制的结构如下:
其对应的公式为
e i j = f ( s i − 1 , h j ) e_{ij}=f(s_{i-1}, hj) eij=f(si−1,hj)
α i j = e x p ( e i j ) / ( ∑ k = 1 T x e x p ( e i k ) ) \alpha_{ij}={exp(e_{ij})}/({\sum^{T_x}_{k=1}{exp(e_{ik})}}) αij=exp(eij)/(∑k=1Txexp(eik))
c i = ∑ j = 1 T x α i j h j c_i=\sum^{T_x}_{j=1}{\alpha_{ij}h_j} ci=∑j=1Txαijhj
其中 f f f表示的是一个前向神经网络。
以上是Bahdanau等设计的一种方式,若对其他方式感兴趣的朋友可以去网上查找。本文是对transformer进行总结,对attention就不进行深入介绍了。
而在transformer中使用的是self-attention。顾名思义,self-attention能够对输入句子本身进行注意力权重对训练。比如,当输入一个句子之后,句子之中对每一个token能够获取到其与其他token之间对关系。比如
上图中,句子The animal didn't cross the street becouse it was too tired
经过tokenize之后,得到[The_, animal_, didn_, '_, t_, cross_, the_, street_, becouse_, it_, was_, too_, tire, d_]
这样若干个token,然后输入到self-attention。进过训练,从上图可以看出,it_
跟The animal
, tired
关系权重比较大,因为it
指代的是The animal
,且it
很tired
,说明这里的self-attention很好的学习到了it
的指代内容,以及it
的感受。因此,self-attention能够很好的刻画句子之间token与token之间的关系,并且能够很好的提取句子的特征信息。因此,transformer通过self-attentin能够更好的提取句子的信息。
下面是我对self-attention与word2vec,attention之间的区别的简单看法:
这个苹果真好吃
和苹果手机真好用
这两句中的苹果
,在word2vec中都是只有一个相同的向量表示,而在self-attention中则会根据上下文的不同给出不同的向量表示。上一节对transformer的整体架构进行介绍,接下来本节将对transformer中从输入到输出的各个部分进行介绍,主要有以下几个部分:
在encoder的输入部分,是将输入句子进行分词之后,对每一个分词后对token通过一个矩阵映射到一个向量(或者可以理解为接一个全连接层,训练权重),然后将每一个token对应对向量输入到encoder模块。
而我们通过对第一节中应该可以了解到,encoder中对self-attention是将一个句子中的各个token进行并行计算权重,没有无法获取句子中词的先后顺序。但在一个句子中,词的顺序确实非常重要的,因此,我们输入到encoder模块的除了句子各token的向量,还有各token的位置信息,即positional encoding。
由于positional encoding是跟输入的词向量一起作为模型的输入,因此,positional encoding应该跟输入的词向量的维度保持一直。假设输入的词向量数据是256个词向量,每个词向量的长度是128,那么positional encoding的维度也应该是(256,128),即positional encoding可以看作以一个矩阵的形式输入。那么,对于这个矩阵中的每一个位置的元素值的大小,可以用如下公式得到
P E ( p o s , 2 i ) = s i n ( p o s / 1000 0 2 i / d m o d e l ) PE_{(pos, 2i)}=sin(pos/10000^{2i/d_{model}}) PE(pos,2i)=sin(pos/100002i/dmodel)
P E ( p o s , 2 i + 1 ) = c o s ( p o s / 1000 0 2 i / d m o d e l ) PE_{(pos, 2i+1)}=cos(pos/10000^{2i/d_{model}}) PE(pos,2i+1)=cos(pos/100002i/dmodel)
其中,pos表示positional encoding中的第几个向量, 2 i 2i 2i或者 2 i + 1 2i+1 2i+1表示在该向量中的位置。
将向量输入到encoder,encoder中首先接收到数据的是Multi-head attention,即多头注意力。实际上,这里所说的多头,就是将前文中所说的self-attention复制多份,将输入的向量分别输入到这些self-attentioin,然后从这多个self-attention中得到不同的向量。
还是以上面的The animal didn't cross the street becouse it was too tired
为例,上文中给出的是单个self-attention得到的注意力结果,而对于多头注意力的结果如下图
从图中可以看到,我们可以得到各种不同的注意力结果。我们可以理解为,通过多头注意力机制,让模型能够从多种不同的角度去获取句子token之间的关联关系,也就是能够学到更加丰富的句子的特征信息。
在multi-head attention之后接对是一个layer normaliza。与layer normalization相对应的是Batch normalization。关于这两者之间联系与区别,以及为什么nlp这边会比较倾向于使用layer normalization而不是Batch normalization,可以参考《NLP中 batch normalization与 layer normalization》。下面是我阅读该篇文章的简单总结:
- Batch normalization是对每个批次中的多个样本之间的相同位置的特征进行归一化;
- layer normalization是对同一个样本中的不同特征进行归一化;
- 对于一些结构化特征场景下,layer normalization看起来不太合理,但是对于nlp场景下layer normalization就比较合理。因为在nlp场景下,对每一个样本的多个token 向量进行layer normalization,实际上就是对token向量进行归一化。
在layer normalization之后,接的是一个feed forward。这里就是一个正常的全连接层,就不过多介绍了。
另外,还有一个地方需要留意的是,不论是在encoder部分还是在decoder部分,我们都可以看到有残差结构。这里的设计这种残差结构的目的,应该是为了保证在训练的过程中,梯度能够更好的传递下去。
前面2.1.1~2.1.4小节已经大致介绍了一下encoder中各个部分了。然后,对于2.1.2中所讲的multi-head attention我们这里对其内部结构再进行更深入的介绍。multi-head attention,多头注意力,顾名思义,就是多个self-attention,这里的self-attention也叫scaled dot-production attention。
关于attention的各种形式的理解,大家可以参考《attention各种形式总结》。下面一段是引用该文中关于attention的介绍。
Attention函数的本质可以被描述为一个查询(query)到一系列(键key-值value)对的映射。
在计算attention时主要分为三步:
- 第一步是将query和每个key进行相似度计算得到权重,常用的相似度函数有点积,拼接,感知机等;
- 第二步一般是使用一个softmax函数对这些权重进行归一化;
- 最后将权重和相应的键值value进行加权求和得到最后的attention。
目前在NLP研究中,key和value常常都是同一个,即key=value。
sacled dot-product attention也是按照上述所说的流程进行计算的。首先我们来看一下multi-head attention和scaled dot-product attention的结构。
从上图中可以看到,multi-head attention是由多个scaled dot-product attention组成的。上面右图中给出了scaled dot-product attention的结构明细。通过这个结构明细能够看出,其流程基本与上面所说的是一致的。为了更好的了解这个过程中的细节,我们下面通过推导一个case来进行进一步了解。
如下图,假设输入为Thinking
和Machine
这两个词。Thinking
的词向量为X1,Machine
的词向量为X2,而每个词的Q(query), K(key), V(value)这三个向量都是从各自词向量分别与矩阵 W Q , W K , W V W^Q, W^K, W^V WQ,WK,WV相乘得到的。
q 1 = X 1 W Q q1 = X_1W^Q q1=X1WQ
k 1 = X 1 W K k1 = X_1W^K k1=X1WK
v 1 = X 1 W V v1 = X_1W^V v1=X1WV
q 2 = X 2 W Q q2 = X_2W^Q q2=X2WQ
k 2 = X 2 W K k2 = X_2W^K k2=X2WK
v 2 = X 2 W V v2 = X_2W^V v2=X2WV
其中的 W Q , W K , W V W^Q, W^K, W^V WQ,WK,WV都是需要进行训练得到的。
在得到来各自的q,k, v向量之后,就到了走attention计算的流程了。我们这里以计算Thinking
为例。
首先,将q1分别与k1,k2点积得到两个分数score,然后将这个分数除以 d k \sqrt{d_k} dk进行scaled(这里的 d k d_k dk表示词向量的维度,之所以除以这个数的作用是避免将softmax函数push到梯度很小的区域,具体可以参考《transformer中的attention为什么scaled?》),然后进行softmax,得到softmax向量之后再与各value相乘求和,得到Thinking
的进行scaled dot-product attention之后的向量。Machine
的计算也是类似。
以矩阵形式来说明如下:
首先通过矩阵相乘得到Q,K,V向量,然后按照下面给出的方式进行计算attention向量Z。
以公式形式展示为
A t t e n t i o n ( Q , K , V ) = s o f t m a x ( Q K T d k ) V Attention(Q, K, V) = softmax(\frac{QK^T}{\sqrt{d_k}})V Attention(Q,K,V)=softmax(dkQKT)V
至此,transformer的encoder各部分已经介绍得差不多了。
在decoder的输入部分,会使用shifted right的方法进行数据。还是以上面的将this is a lovely dog.
翻译为这是一只可爱的小狗。
为例。在模型训练的时候,是将[this
, is
, a
, lovely
, dog
, .
]作为encoder的输入,将[这
, 是
, 一只
, 可爱的
, 小狗
, 。
]作为decoder的输入。
但是在真是预测的时候,当我们预测出了第一个词的时候,此时其实我们是不知道后面是什么词的,顶多就只能知道当前已经预测出了这
这个词,甚至有可能预测出的第一个词不是这
。因此,我们在训练的时候,decoder的输入部分也不能让模型看到后面还未预测的词,比如,当我们要预测是
这个词的时候,输入的就只能是前面这
这个词。那么当我们要预测第一个词这
的时候,输入的是啥呢?一般我们会使用一个特殊字符
来表示一句话的开始,即,在预测第一个词的时候,decoder的输入就是
。那么,相应的在最后一个词后面也会有表示结束的特殊字符
。
在decoder的输入其实是这样的:[
, 这
, 是
, 一只
, 可爱的
, 小狗
, 。
,
]。相当于是将期望的输入向右移动来一下,所以叫做 shifted right。
通过shifted right来让decoder的输入不会看到未来不该看的词(除了shifted right之外,还需要masked方法,这个2.2.2中介绍),但是前面也说了,在实际预测的时候,其实我们无法保证前面预测出来的词就是正确的,那么我们为啥在训练的时候可以将前面正确的词作为输入的(比如训练中预测是
的时候将这
作为输入)?那是因为我们这里使用了teacher forcing的方法来训练。举一个不是很恰当的例子,面对一道有多个小问的大题时,虽然我们在考试的时候无法保证前面的小问能够做对,但是如果我们在平时练习的时候,如果有老师在旁边当我们每做一道小问的时候进行及时的纠正,可以帮助我们快速学习,掌握知识。通过teacher forcing可以帮助模型快速学习和收敛。
在前面的transformer的架构图中我们可以看到,在decoder部分的每个模块中,会有两个multi-head attention,一个是masked multi-head attention,一个是跟encoder连接的multi-head attention。我们这里先来介绍一下masked multi-head attention。
前面2.2.1中讲了通过shifted right来让decoder的输入不会看到未来不该看的词,但是光是这样是不够的。我们在前面介绍了scaled dot-product attention,可以看出,scaled dot-product attention是一个并行输入,并且输入的句子长度需要固定的。比方说,在我们设置的scaled dot-product attention的输入长度是128,但是实际输入的句子就只有10个词的长度,这个时候,剩下的118个长度,就需要使用
来进行填充。
而在decoder的输入中,但我们预测是
的时候,并不是输入[
, 这
,
,
,
, …]的方式来输入。输入的还是[
, 这
, 是
, 一只
, 可爱的
, 小狗
, 。
,
,
,
,
, …],只是将后面的[是
, 一只
, 可爱的
, 小狗
, 。
,
]给mask了,即在后面的计算multi-head attention的时候,将不能看的数据的mask,所以叫masked multi-head attention。
然后讲一下decoder中的第二个multi-head attention,这个multi-head attention是连接encoder和decoder的。
我们在前面介绍scaled dot-product attention中介绍了其中会有Q, K, V这三个输入,而在这个multi-head attention中,使用的K和V,就是从encoder的输出得来的,而Q还是使用它前面的masked multi-head attention得到,如下如所示。其他的在multi-head attention中的计算,跟前面介绍的一样。
然后这个multi-head attention之后接一个feed forward,与前面encoder相同。至此,decoder模块的各部分也介绍的差不多了。
在decoder输出之后,接的是一个linear层和softmax,计算出每次预测是词表中的哪个词。这个也就不做过多介绍了。
至此,transformer从输入到输出的各部分就已经介绍完了。
由于本人还是菜鸟,文中若有不对之处,还望不吝赐教