本文主要用于记录谷歌发表于2017年的一篇论文(引用量接近上千)。该论文提出的Transformer模型也是近年来被广泛应用的。本笔记主要为方便初学者快速入门,以及自我回顾。。。
论文链接:https://arxiv.org/pdf/1706.03762.pdf
Github: https://github.com/tensorflow/tensor2tensor
官方教程链接:https://www.tensorflow.org/tutorials/text/transformer#encoder_and_decoder
基本目录如下:
- 摘要
- 核心思想
- 总结
------------------第一菇 - 摘要------------------
1.1 论文摘要
现今几乎所有主流的翻译模型都是建立在复杂循环或卷积seq2seq框架基础上的,而其中表现最好的模型是基于注意力机制实现的。本论文提出了一种新的神经网络结构,Transformer,仅仅依赖于注意力机制,摈弃了传统循环或卷积网络。这个新的网络结构,刷爆了各大翻译任务,同时创造了多项新的记录(英-德的翻译任务,相比之前的最好记录提高了2个BLEU值)。而且,该模型的训练耗时短,并且对大数据或者有限数据集均有良好表现。
------------------第二菇 - 核心思想------------------
2.1 论文模型结构
作者在开头还是吐槽了很多传统模型的弊端,之后又吹说自己的Transformer模型有多牛x,完美避开了那些复杂的神经网络模型,只用了注意力机制,不仅大大加快了模型的训练速度,还保证了质量哈哈~(补:现今2019年下半年来看,还真的是这样,主流的NLP预训练模型底层基本都用Transformer作为特征提取层了)
在继续往下看之前,还请大家思考一个问题:
现有的RNN体系(包括后面衍生的Attention机制)为什么无法满足现阶段语言模型的需求???
我个人的理解主要有两点:
1)现有体系无法解决Long-term Dependency 问题(该问题即可以简单理解为无法捕获相距较远的词之间的关联关系)。其本质原因还是RNN体系的梯度消失/爆炸问题,尽管LSTM在一定程度上能缓解该问题,然而并未能完全避免。
2)计算量的问题。该问题其实也是所有时序模型的通病,因为时序模型必须是串行的,可能在预测阶段还能接受这个时效性,但是在训练阶段,与能并行计算的CNN相比,时效性表现的就不尽如人意了。
因此,也是基于以上两点的考虑,论文在背景介绍里抛出了一个貌似是新的概念self-attention(我还专门去搜了一下相关文献,搜到一篇IBM的,我也写了论文笔记,大家可以参考)。话不多说,我就带着大家一起来看看这个Transformer到底是个什么东西。(多图预警!)
首先直接看原论文的模型架构图肯定是一脸懵逼,这都啥玩意?反正我第一眼看过去都是个新概念。。。想必很多初学者也是跟我一样,所以我们还是先从更高的视角来解析Transformer模型(在读模型结构的时候,大家也要细心去思考,这个模型到底是如何解决掉上面我提到的RNN体系的缺陷的)。我从网上盗了几张模型架构图【1】,方便大家理解。
2.1.1 Transformer架构在哪里?(图片来源)
这张图的结构就非常清晰,就说明了一件事情!谷歌团队真的就只用了Transformer来做整个特征提取的orz!大家没有看错,这不是简化图,这就是整个模型的全部,只有一个Transformer😁!
2.1.2 Transformer里面是什么?(图片来源)
又是一张结构非常清晰的图,说明了2个事情。第一件事,Transformer结构的基本组成仍旧是seq2seq那一套(参考我另一篇笔记)。第二件事,每一个ENCODERS和DECODERS部分都由6(原论文的N=6)个小的并且相同的ENCODER和DECODER组成。这几个部分的叠加其实很有意思,大家可以想一想,之前的RNN体系虽说是深度学习模型,但其“深度”我们都是从时间的纬度来考量的,但这里,是真的多个特征提取层的叠加,是真正的纵向意义上的深度模型😄~
2.1.3 ENCODER和DECODER内部结构是什么?(图片来源)
每一个ENCODER内部又分为两层,第一层为self-attention(主要是用来捕获时序类的特征),第二层为feed-forward层(常规的特征转换层,通过非线性的变化来转换特征吧)。DECODER层与ENCODER层相似,但是中间多了一层Attention,其功能原理与普通的RNN体系的注意力机制相似(参考我另一篇笔记)
2.1.4 self-attention是如何运行的?(图片来源)
在原论文里,作者很霸气的丢出了一个公式,
要直接去理解这个公式,对矩阵运算不是非常熟练的同学可能有点困难,因此,我们还是先拆分去考量一个单词向量的计算过程,再回过头来理解这个矩阵的运算。如下图,是进行运算的第一步,
对于每一个单词向量(Embedding后的),我们都会计算出3个不同的向量,名为query, keys, values。这些向量都是我们通过模型参数计算得出来的。相比于词向量的纬度(论文中为512),这些向量的纬度会小一点(为64),当然这些向量的纬度肯定都是超参数,是可以调整的。至于这三个向量的具体含义,只能让大家从后续的计算过程中自行体会来,我也会穿插讲一下自己的理解。
self-attention的第二步是对每一个词进行打分(dot products of the query with all keys)其实就是把当前位置q的向量与所有位置的k向量进行点积相乘(该步我的理解其实就是去考量当前位置上的单词与每一个位置上的单词的一种关联关系的程度),然后再除以(原论文是8,据说是可以让训练收敛的更平稳),最后再做一层softmax操作,每一个词就会得到一个对当前位置的打分,显然,当前词应该会对当前位置有最高的权重。(总感觉看到了一丝rnn的意思,考虑所有的输入序列,对当前预测结果的影响)
self-attention的最后一步,也是很简单了,就是把权重与每一个位置的v向量加权求和,最后得到的z向量就是我们要送入到下一层前馈神经网络的。上述过程的计算示意图如下,
至此,每一个词向量的计算过程已经描述清楚了,矩阵的运算想必也是很好理解了。首先,我们计算Q,K,V三个矩阵,再根据上述的运算过程,简化为矩阵的运算就如下图,
至此,对原论文中的公式就剖析完毕了(当然也是整个self-attention)的核心。当然大家可以思考一下,这种self-attention的结构设计是如何解决RNN体系的Long-term Dependency问题的?
我个人的理解主要有两点:
1)参考计算公式,我们会发现考量当前词与其他词关系的时候,不论距离远近,我们都会赋予相同的权重进行计算(Q*K),以此来考量当前词,与其他所有词的关联关系~而RNN体系随着梯度消失的问题,距离越近,相互关系捕获的越好,但是距离越远反而难以捕获了!因此,这就是self-attention的神奇之处~(但真的那么神奇吗?其实是需要打一个问号的,因为超过一段距离的单词之间其实是真的没有什么关系了,但这边还是会与近距离的单词同等对待~)
2)但是随之而来的就是计算量和存储的问题,因为我们要计算所有词的关联关系,大家可以想象一下那个矩阵得有多大了~所有后面有人提出了transformer-xl的升级版(有兴趣的可以去了解)
2.1.5 Multi-Head attention是如何运行的?(图片来源)
这里Multi-Head其实没有那么神秘,简单说就是把上述的过程,重复进行几次(原论文中取值为8)最后再把结果连接起来。而重复进行的运算中,唯一不同的就是初始Q,K,V矩阵的生成,他们分别由不同的参数矩阵计算得出,示意图如下,
我个人对这样处理的理解主要有两点:
1)扩大了模型的视野,让模型在计算当前位置信息时,能关注到更多其他不同位置的信息。(若是单一模型,很可能永远被当前词所决定)。
2)增加了模型的语意表达能力,因为所有的Q,K,V三个矩阵的生成都是互不干扰的,可能会有更多语意层面的表达(每一组都能捕获到不一样对信息哈,且可以并行化处理~)。
当生成完多个输出矩阵以后,我们会拼接所有的结果,然后与一个权重矩阵相乘(随模型训练的),得到一个最终的self-attention层的输出,因此,总结一下,self-attention的整个计算过程如下示意图,
看到这里,想必各位对整套attention的从理论层面上已经有所了解了,我们再来看一下官方给的源码教程及说明是怎么写的,看完之后可能会有不同的理解~
Multi-head attention
class MultiHeadAttention(tf.keras.layers.Layer):
def __init__(self, d_model, num_heads):
super(MultiHeadAttention, self).__init__()
self.num_heads = num_heads
self.d_model = d_model
assert d_model % self.num_heads == 0
self.depth = d_model // self.num_heads
self.wq = tf.keras.layers.Dense(d_model)
self.wk = tf.keras.layers.Dense(d_model)
self.wv = tf.keras.layers.Dense(d_model)
self.dense = tf.keras.layers.Dense(d_model)
def split_heads(self, x, batch_size):
"""Split the last dimension into (num_heads, depth).
Transpose the result such that the shape is (batch_size, num_heads, seq_len, depth)
"""
x = tf.reshape(x, (batch_size, -1, self.num_heads, self.depth))
return tf.transpose(x, perm=[0, 2, 1, 3])
def call(self, v, k, q, mask):
batch_size = tf.shape(q)[0]
q = self.wq(q) # (batch_size, seq_len, d_model)
k = self.wk(k) # (batch_size, seq_len, d_model)
v = self.wv(v) # (batch_size, seq_len, d_model)
# 多头的划分,个人觉得有2种理解:
# 1. 因为先经过了一层线性变化,故而拆分多头后的每一个头都是句子的一种表达方式。
# 2. 拆分多头后的每一个头都是句子的某一个局部信息的特征(本人倾向这个)
q = self.split_heads(q, batch_size) # (batch_size, num_heads, seq_len_q, depth)
k = self.split_heads(k, batch_size) # (batch_size, num_heads, seq_len_k, depth)
v = self.split_heads(v, batch_size) # (batch_size, num_heads, seq_len_v, depth)
# scaled_attention.shape == (batch_size, num_heads, seq_len_q, depth)
# attention_weights.shape == (batch_size, num_heads, seq_len_q, seq_len_k)
scaled_attention, attention_weights = scaled_dot_product_attention(
q, k, v, mask)
scaled_attention = tf.transpose(scaled_attention, perm=[0, 2, 1, 3]) # (batch_size, seq_len_q, num_heads, depth)
concat_attention = tf.reshape(scaled_attention,
(batch_size, -1, self.d_model)) # (batch_size, seq_len_q, d_model)
output = self.dense(concat_attention) # (batch_size, seq_len_q, d_model)
return output, attention_weights
Multi-head attention consists of four parts:
- Linear layers and split into heads.
- Scaled dot-product attention.
- Concatenation of heads.
- Final linear layer.
Each multi-head attention block gets three inputs; Q (query), K (key), V (value). These are put through linear (Dense) layers and split up into multiple heads.
The scaled_dot_product_attention
defined above is applied to each head (broadcasted for efficiency). An appropriate mask must be used in the attention step. The attention output for each head is then concatenated (using tf.transpose
, and tf.reshape
) and put through a final Dense
layer.
Instead of one single attention head, Q, K, and V are split into multiple heads because it allows the model to jointly attend to information at different positions from different representational spaces. After the split each head has a reduced dimensionality, so the total computation cost is the same as a single head attention with full dimensionality.
2.1.6 什么是Positional Encoding? (图片来源)
Positional Encoding的引入是为了考量输入序列中的顺序问题,他的作用就是为模型注入一些当前词的绝对位置或相对位置信息(告诉模型每一个词的位置信息)。该向量是由特定的模式生成的,论文中也有公式(大概是用正弦或余弦计算得出的),然后将该向量与词向量想加,构造出整个模型的输入向量。(貌似代码的实现与论文中的公式有出入,有兴趣的同学可以深入调研一下,我其实也没太想明白这块的输入具体在什么地方起到作用)。依旧再放一张图,方便大家的理解,
2.1.7 Residuals和Layer-Normalization的引入(图片来源)
每一个Encoder内部,每一层都会有一个残参连接,并且带有一个层-归一化操作。这倒没什么好展开讲的,直接上图(一个Encoder的内部结构图),一目了然,
2.1.8 encoder-decoder attention(图片来源)
Decoder的结构跟Encoder其实差不多,只不过,Decoder多了一层注意力机制(是真正的注意力机制orz,跟seq2seq那一套的注意力机制类似,不熟的参考我上一篇论文笔记)。我们来做一个对应关系,其实大家应该就懂了,seq2seq中的输出隐状态,其实就是这边上一层的Q矩阵输出,而源输入的隐状态,就对应这边Encoder出来的K,V矩阵。原理几乎是一模一样的,下面上一张图,大家就能理解了,
不过还是要再多说一句,Decoder与Encoder的结构还是略微的不同,主要体现在Decoder层的self-attention层会有一个mask,会把当前位置之后的所有值都置为(-inf)意为他们对当前预测词不应该起作用。
说到这里,除了最后一层,最常规的softmax层,来预测当前输出词的最大概率,其他的模型结构应该是梳理清晰了,这个时候再回头看,原论文中的图,应该就清晰多了。
2.2 Transformer架构的好处
原论文中,独辟蹊径的开了一章,就叫"Why Self-Attention"。重点讨论了一下自己的这一套self-attention在提取特征上与RNN, CNN的对照。主要分三个层面的讨论,1)每一层的空间复杂度。2)并行计算的可能性。3)解决长时依赖问题的最长路径。具体的对照大家可以看论文原文。这里我就聊一下自己的理解。其实本质上来看,整一套Transformer的架构并没有标题取的那么神乎其神(Attention is All You Need)哈哈,让我一度以为,注意力框架真的能自成一套,但其实本质还是绕不过特征提取的阶段。
再贴一个官方的优劣论证,
This general architecture has a number of advantages:
- It make no assumptions about the temporal/spatial relationships across the data. This is ideal for processing a set of objects (for example, StarCraft units).
- Layer outputs can be calculated in parallel, instead of a series like an RNN.
- Distant items can affect each other's output without passing through many RNN-steps, or convolution layers (see Scene Memory Transformer for example).
- It can learn long-range dependencies. This is a challenge in many sequence tasks.
The downsides of this architecture are:
- For a time-series, the output for a time-step is calculated from the entire history instead of only the inputs and current hidden-state. This may be less efficient.
- If the input does have a temporal/spatial relationship, like text, some positional encoding must be added or the model will effectively see a bag of words.
2.3 论文实验结果分析
论文作者把自己提出的整套框架实现了一遍,这里就不具体展现了。有兴趣的读者可以自行研读。值得一提的是,谷歌有开源的tensor2tensor,有空还是可以读一遍源码,或者工业界的小伙伴,可以学一波应用。
------------------第三菇 - 总结------------------
3.1 总结
到这里,整篇论文的核心思想及其创新点已经说清楚了。本论文主要集中在于阐述Transformer架构,并且解释了自己为什么要使用这一套架构的原因(坊间谣言,为了对标FB的convseq2seq)。
简单总结一下本文就是先罗列了一下该论文的摘要,再具体介绍了一下Transformer架构,主要是盗用了很多一个外国小哥博客的图(他的可视化Transformer,真的能让人快速入门,感恩),最后也谈了一点自己对Transformer架构的理解,总的来说,谷歌这篇还是划时代的产物。希望大家读完本文后能进一步加深对该论文的理解。有说的不对的地方也请大家指出,多多交流,大家一起进步~😁
参考文献:
【1】https://jalammar.github.io/illustrated-transformer/
附录:
有助于理解的自问自答
1. 假如encoder的shape为(batch_e, seq_e, word_e),decoder的shape为(batch_d, seq_d, word_d)。
则batch_e = batch_d、seq_e != seq_d、word_e = word_d
2. 在多头Attention中,word_e和num_heads、depth的关系?
word_e = num_heads * depth
3. encoder、decoder的输入为句子,句子长度的限制大小是多少?
不影响参数量,影响计算过程中的显存和速度,要小于position_embedding的size。
4. 假如encoder的shape为(3, 4, 8),decoder的shape为(3, 4, 8),输出的目标语言单词集合大小为8000,那么decoder的输出shape是?
(3, 4, 8000)
5. 假如encoder的shape为(3, 4, 8),decoder的shape为(3, 4, 8),padding_mask和look_ahead_mask的形状
(3, 1, 4)、(3, 4, 4)