目录
简介
自从2014年seq2seq被提出以来,encoder和decoder框架一直被广泛用于各类生成任务,其中最有代表性的就是机器翻译。而encoder和decoder所使用的基础模型也从最开始的RNN(LSTM), 逐步引入attention机制,到google提出Transformer, 完全利用self-attention来取代RNN。我们会在本文中阐述之前基于RNN的seq2seq,与Transformer的比较,并详细解读 Transformer的原理,之后我们会结合
Transformer的官方代码,详细分析其具体实现。
为什么用Transformer
seq2seq中encoder和decoder,其本质就是将一个序列映射为另一个序列。我们可以选择不同的模型来实现这样的序列映射过程。假设我们的输入序列是 a 0 , a 1 , . . . , a n {a_0,a_1,...,a_n} a0,a1,...,an, 我们的输出序列是 b 0 , b 1 , … , b n {b_0, b_1, …, b_n} b0,b1,…,bn。
RNN
当我们选择使用RNN(各种不同的单向或者双向RNN)的时候,RNN的每个时间步的输出,不仅与其当前输入有关,也与其他时间步的输入有关,而且每个输出都需要在其之前的输出已经计算完成之后才能开始计算。这种方法的痛点在于序列的生成不能并行计算,另外就是由于RNN的记忆特性,对于long dependency的效果不够好。
CNN
对于RNN的序列生成不能并行计算的问题,我们可以使用CNN来解决。我们可以使用CNN将不同的连续输入映射成输出向量,即通过卷积核可以将某个时间步附近的输入映射成对应时间步的输出
但是由于卷积核的大小有限,这样做有一个问题在于每一个时间步的输出向量只能捕捉到该时间步附近的输入向量的信息,而不是全局的输入向量的信息,为了解决这个问题,我们可以用多层的CNN来学习全局输入信息
Transformer
我们可以看到对于序列生成来说,RNN的问题在于不能并行计算,而CNN的问题在于学习全局信息的时候需要的层数过深。针对这些问题,google创造性的提出了只用attention来实现序列生成,并解决了以上这些问题。在介绍transformer之前,我们首先来介绍一下attention机制
Attention
attention机制由三个部分组成,一个query,一组keys和一组values。其中query是单个向量,keys和values都是向量集合,attention的作用就是将query向量映射成一个新的向量,这个新的向量是由values这个集合中所有的向量加权得到的,而values集合中每个向量的加权系数是由query与keys集合中对应的向量计算得到的。这些加权系数的计算有多种不同的方式,比如dot-product, concatenation与perceptron等,而且所有的加权系数最后需要经过一个softmax函数,使得所有的加权系数之和为0.
下面两页截图引用自MSRA的Nan Duan博士在微软工程院所做的分享
下面是一个有关attention的例子
Transformer的图示
在对attention机制有了基本了解之后,下面将介绍attention在Transformer中的应用。以下这些图示来自于台湾的李宏毅博士在YouTube上对于Transformer的介绍。
在Transformer的attention中,每个时间步的输入都通过一个矩阵映射成对应的query, key 和value
然后我们计算query对每一个key的attention score,注意transformer中使用的是dot-product, 由于query和key向量的维度会影响attention score(维度越大,dot-product的值越容易更大),因此将这些attention score除以 d 1 / 2 d^{1/2} d1/2
之后我们将query对所有keys的attention score做softmax,得到归一化的加权系数。这里要着重强调的是,由于softmax的特性,如果我们在做softmax之前对某个attention score上加上负无穷,那么经过softmax之后其对应的加权系数为0,也就是说values中的对应向量在最后的加权平均中不起作用。这种机制在代码实现的时候被用来处理需要被mask的向量,比如一个batch中的不同样本,其输入的长度是不一致的,为了方便计算我们会将所有的样本padding到相同的length,那么在经过attention模块的时候,我们就可以将对应的padding位置的attention score在经过softmax之前加上负无穷。
在得到了query相对于keys中各个向量的加权系数之后,我们就可以将values中的向量进行加权平均,得到该query对应时间步的输出
对其他的query进行同样的操作
对于上述的每一个时间步的操作,我们将一种数学化的方式将这些操作合并成一个完整的矩阵操作
query,keys和values以及输入序列的矩阵表示
为了与图示保持一致,我们假设输入序列的长度为4,可以定义输入序列为
I = [ a 1 , a 2 , a 3 , a 4 ] I=[a^1,a^2,a^3,a^4] I=[a1,a2,a3,a4]
定义所有时间步的query为
Q = [ q 1 , q 2 , q 3 , q 4 ] Q = [q^1,q^2,q^3, q^4] Q=[q1,q2,q3,q4]
定义所有时间步的key为
K = [ k 1 , k 2 , k 3 , k 4 ] K=[k^1, k^2,k^3, k^4]