前几天细读GPT的paper,里面使用的基础模型和BERT一样都是Transformer,区别就在于GPT用的是单向Transformer,而BERT使用的是双向Transformer。之前对Transformer认识也比较敷衍,这几天就专门看了相关的paper、文章和代码,重新认识一下Transformer。
首先,Transformer来自论文Attention Is All You Need,后面笔记很多参考文章The Illustrated Transformer,同时加了一些自己的理解。
1. 从上到下认识Transformer
首先我们把Transformer看做一个黑盒,以机器翻译示例为例,Transformer是输入就是待翻译的句子,输出就是翻译后的句子。

然后我们再一步一步打开这个潘多拉魔盒:

看到没有,这是一个经典的Encoder-Decoder模型,只是Encoder部分不是一个单独的Encoder,而是6个Encoder的堆栈,Decoder部分也是一样,至于为什么是6个,可能是实验效果最好。把Encoders、Decoders部分打开,就是下面这个样子:

在Encoders部分中每个encoder的结构都是一样的,只是不共享weights。每个encoder打开后,如下图结构:

每个Encoder包括一个Self-Attention层和feed-farward NN层。Encoder的输入先进入Self-Attention层(暂时先不关心Self-Attention细节,后面会介绍),Self-Attention层的输出作为feed-farward NN层的输入。
对于feed-farward NN层,是由多个feed-farward network组成的,每word对应一个,而且是互相独立的。
针对每个Decoder如下图,也是有Self-Attention层和feed-farward NN层组成,只是在两个中间加入了一个Encoder-Decoder Attention层,我的理解是对每个Encoder给予不同的注意力,用于表现在输入句子中应该更看着哪一部分。

2. 深入细节
现在开始深入模型的细节,从tensor级别来看看,这些输入向量在模型内部是如何流动和变型,到输出,来认识如何训练一个语言模型。
和一般NLP任务一样,一个句子中每个word用embedding word的形式表示,即每个word是一个向量。
如果embedding word的长度是512,那么每个word就是1*512的向量,下图简化表示每个词:

那么对于最底层的encoder输入就是一个向量的list,其中每个向量的长度是512,至于list的长度作为模型的一个超参数,一般情况去训练数据中最长句子的word个数,假设最长的句子有N个词,那么这个list就是N*512的向量。而对于其他encoder的输入就是它前面一层encoder的输出。

这里提到了Transformer一个重要的特性:并行化,原因就在于list经过self-attention层输出后也是一个向量的list,而且每个向量进入各自的feed-forward layer,而且每个feed-forward 都是独立,所以可以并行运算。
下面就以一个简单的示例具体看看在encoder里每个子层究竟做了什么。

-Self-Attention
如果我们要翻译这句话“The animal didn't cross the street because it was too tired”。句子中的it是指的是animal还是street呢,Self-Attention就是为了把animal和it联系起来,也就是说Self-Attention是Transformer去了解句子中词之间的关系。

究竟Self-Attention是如何做到这一点的呢,下面一步一步来看计算细节。
第一步——参数:每个encoder的输入就是每个词的word embedding,然后要根据这个输入对每个词创建3个向量,分别是:Query vector, Key vector, Value vector。那么这个三个向量是怎么产生的呢,这三个向量分别是通过输入得word embedding向量乘以三个矩阵产生的,而这三个矩阵就是我们要训练的。

Query vector, Key vector, Value vector的长度要小于word embedding,word embedding和每个encoder的输入/输出向量长度是512,而Query vector, Key vector, Value vector是64。其实他们不是一定要求要小,只是为了运算更方便。那么这三个向量是怎么帮助计算词之间的注意力的呢,我们继续往下看。
第二步——计算得分:以计算例子中第一个词“Thinking”的self-attention得分为例,我们需要计算输入句子中每个个词对“Thinking”的self-attention得分,这个分数表示了当我们在解析句子特定位置的词时,输入句中其他词对这个词的关注程度。
如果我们要计算第一个位置词的self-attention得分,先计算q1*k1,再计算q1*k2。

第三步,把self-attention得分除以key vector长度的开方,在论文中key vector的长度是64,所以这个除以8。为什么要这么做,论文给的解释是可以得到更稳定的梯度,我的理解是给K的长度一定的惩罚项。
第四步,做一个softmax操作进行归一化,这个归一化的值保证了所有的self-attention得分都为正,而且<=1。
在这里,开始这几个概念开始把我弄得比较晕,现在开始梳理清楚了,首先是词和位置。Query vector表征的是位置向量,Key vector表征的是词向量,根据softmax score的计算可以表征的是词对当前位置的关注程度。根据这个定义,当然通常这个位置上词有最高的softmax score,但同时对表示其他词和当前词的关联程度的表示也是非常有用。

第五步,针对每个词对位置#1的softmax score乘以词#1的value vector得到权重。同时乘以一个很小的数(比如0.001),这样保留需要关注的词,同时丢弃那些不相关的词。
第六步,把每个词针对位置#1的权重相加,就得到位置#1self-attention层的输出值。

上面的计算向量值被送到feed-forward neural层计算。在实际计算中,为了加快计算速度,这些计算是以矩阵的形式进行的。所以下面我们来以矩阵的视角看看是如何计算的。
Self-Attention的矩阵运算
第一步,计算Query、Key、Value矩阵。我们的输入X:word embedding矩阵,然后分别乘以我们要训练的三个权重矩阵WQ、WK、WV。

然后对前面针对self-attention向量计算,压缩成矩阵运算就是:

“multi-headed” attention
上面计算看似OK了?不,论文作者还加入了“multi-headed”机制,我们先来看什么是multi-headed,再看它带来的性能提升。
简单来说,前文讲到的attention属于一个head,但还不够,我们还要多加几个head。如下图所示,有W0Q、W0K、W0V计算出Q0、K0、W0,这算1个head,同时还有W1Q、W1K、W1V计算出Q1、K1、W1,这算第2个head。

按照上图所示,论文中用了8个不同的矩阵(即8个不同的WQ、WK、WV),即8个head,得到了8个不同的Z矩阵。

好了,这儿又带来一个问题,对于下一步的feed-forward层的输入只期望是一个矩阵,而不是8个,那么如何是的8个矩阵变成1个呢?论文中的方法是将8个矩阵按行链接成一个,然后乘以一个额外的权重矩阵WO,得到最终的输出矩阵Z。

至于为什么multi-head能提升性能,我个人理解就是类似学习器的组合,通过不同的随机初始化矩阵训练,捕获不同的特征,从不同的侧面表征词之间的关联,以组合的形式得到更强大的学习器。下面用一张图来表示前面描述的过程:

下面再来看看multi-head的效果,回顾一下之间的示例,不同位置的词对“it”的关注程度:

Positional Encoding
到目前为止,该模型还没有描述词之间的顺序关系,也就是如果将一个句子打乱其中的位置,也应该获得相同的注意力,为了解决这个问题,论文加入了自定义位置编码,位置编码为word embedding长度相同的特征向量,然后和word embedding进行求和操作。

论文中给出的编码公式:

具体代码的实现参考get_timing_signal_1d()。关于这部分的理解还不是很清楚,后面理清了再补充。
The Residuals
在继续之前,关于self-attention层还有一个细节需要提到就是residuals,如果熟悉CNN,对ResNet了解的,一定对residuals不陌生,residuals的关键点就是skip connection。在Transformer里,在每一个encoder的子层(也就是self-attention,FFNN )有一个residual connection, 输入和输出有一个add和归一化操作。

以向量的视觉来看就是:

同样,在decoder的子层里也有这样的操作,如果我们把Transformer看做2层的encoders和decoders,将是下面这样:

Decoder
下面重点来看Decoder这方是如何工作的。
最顶层的encoder转化成了注意力向量集合K、V。这作为decoder的输入,下面的gif以机器翻译为例,展示了decoder如何工作。


最后的linear+softmax层
decoder栈最后输出的是一个浮点数的向量,如何把它转化为word,这就是最后linear和softmax层的作用。
假设目前词典的大小是10000个,那么linear层就是decoder栈的输出到10000节点的全连接,每个节点就是表示一个词,最后跟上softmax层,表示到目标词的概率到大小,选择概率最大的词作为输出。

training and loss function
和RNN一样,以机器翻译为例,training的y值就是翻译后句子的one-hot编码,loss function一般采用cross-entropy或者Kullback-Leibler divergence。


一般有,先取位置1概率最大的值,然后在位置1取值的基础上取位置2的概率最大值,依次往后推。为了捕获更准确或者说更广泛的取值,可以采取beam-search。beam-search中有个参数B,假设B取3,表示在位置1取概率排前3的词,然后在这三个词的基础上选取位置2概率最高的三个值,往后依次类推。
好了,到此为止,我们对transformer有了一个全面的了解,更多细节参考:
1. 论文Attention Is All You Need
2. 代码实现http://nlp.seas.harvard.edu/2018/04/03/attention.html
参考
网友评论