前言
前面我们介绍了【NLP】图解从RNN到seq2seq+Attention,里面提及了 Attention
,本篇就是来具体看看 Attention
的来源 Attention Is All You Need (Transformer
)。
自从 Attention
机制在提出之后,加入 Attention
的 Seq2Seq
模型在各个任务上都有了提升,所以现在的 seq2seq
模型指的都是结合 rnn
和 attention
的模型。
- 传统的基于
RNN
的Seq2Seq
模型难以处理长序列的句子,无法实现并行,并且面临对齐的问题
之后,研究人员也使用了多种方式来尝试解决以上问题,但是依旧会存在一些问题,而 Transformer
抛弃了之前模型和RNN结合的固有模式,将其替换为 Attention
,解决了长序列问题以及平行处理的问题,同时又可以引入 Multi-head
来关注序列的不同信息。下面我们就来看看它到底为何如此强大。
Self-Attention
transformer
其实就是 Seq2Seq model with "Self-Attention"
,特别的地方就是在 Seq2Seq
中大量使用了 Self-Attention
通常我们要解决一个 sequence
问题时,我们最常想得到的就是使用 RNN
及其变种的架构,例如下图:
- 上图中的红色块中使用了双向的RNN,我们在输出每个 y y y的时候,都会将 x x x统统看一遍,如果使用简单的RNN的话,那么在输出 x 4 x_4 x4的时候,会将 x 1 , . . . x 4 x_1,...x_4 x1,...x4都看一篇,输出 x 3 x_3 x3的时候会将 x 1 , . . x 3 x_1,..x_3 x1,..x3看一篇。
RNN经常被用在输入是有序列的情况,但是RNN不容易被平行化,假设我们要计算 y 4 y_4 y4,在简单的RNN中需要依次看 x 1 , x 2 , x 3 , x 4 x_1,x_2,x_3,x_4 x1,x2,x3,x4后才能算出来。那么该怎么办呢?
- 接下来就有人提出将CNN拿来取代RNN的想法,如下图:
虽然CNN能够做到像RNN一样输出一个sequence输出一个sequence
- 但是CNN只考虑局部信息,不像RNN一样考虑整个序列
- 而CNN也不是说不能够考虑更长的信息,只需要叠加更多CNN即可(上层的就会考虑更长的信息,如图中上层CNN对第一层的前三个输出进行计算,而这三个已经包含了 x 1 , x 2 , . . . , x 4 x_1,x_2,...,x_4 x1,x2,...,x4的信息)
- 此外CNN的好处在于,我们无需等前面一个计算的结果来计算后一个,对于每层完全可以平行处理
那么有没有其他方法来解决以上两个问题呢?先看图:
这就是 Self-Attention
,它做的事情就是想取代原来RNN做的事情,而且可以解决RNN不能平行处理的问题。
在
Self-Attention
出来后,原本可以用RNN做的是,都可以用Self-Attention
来替换
Self-Attention
第一次出现在一篇名为:Attention is all you need
的paper中(相信大家都知道,我就不给链接了),既然知道了 Self-Attention
是干什么的,自然就会想 Self-Attention
是怎么做到的,不急,继续往下看:
第一步
- 计算 q , k , v q,k,v q,k,v
a i = W x i a^i = Wx^i ai=Wxi
- q:query(用来匹配其他的)
q i = W q a i q^i=W^qa^i qi=Wqai - k:key(用来被匹配的)
k i = W k a i k^i=W^ka^i ki=Wkai - v:value(用来抽取出来的)
v i = W v a i v^i=W^va^i vi=Wvai
现在我们每一个input都有 q , k , v q,k,v q,k,v,接下来要做的事就是:
第二步
- 拿每个query(
q
q
q)去对每个key(
k
k
k)做
Attention
,Attention
的方式有多种(输入两个向量输出一个分数,判断两个向量有多匹配)
- α 1 i = q 1 ⋅ k i d \alpha_1^i=\frac{q^1\cdot k^i}{\sqrt{d}} α1i=dq1⋅ki
其中 d d d 是 q q q 和 k k k 的维度,这里不好理解的是为什么要除以 d \sqrt{d} d,一个直观的理解是:
- 如果 q q q 和 k k k 的维度比较大,那么 q 1 ⋅ k i q^1\cdot k^i q1⋅ki的时候里面的每个值相加的项就越多,值就比较大
第三步
- 接下来,就是做一次softmaxt
- α ^ 1 i = e x p ( α 1 i ) ∑ j e x p ( α 1 j ) \hat\alpha_1^i=\frac{exp(\alpha_1^i)}{\sum_j exp(\alpha_1^j)} α^1i=∑jexp(α1j)exp(α1i)
第四步
- 接着,我们用每个 α ^ 1 i \hat\alpha_1^i α^1i 去和对应的 v v v 相乘,然后相加:
- b 1 = ∑ i α ^ 1 i v i b^1 = \sum_i\hat{\alpha}_1^iv^i b1=i∑α^1ivi
我们可以发现,在计算 b 1 b^1 b1 的时候,要使用到 v 1 , . . . , v 4 v^1,...,v^4 v1,...,v4,也就是看到了所有的输入,如果我们只需要局部的,那么只需要将无需关注的部分的 α ^ \hat{\alpha} α^ 设为0即可
以上我们计算了 b 1 b^1 b1 ,其实我们在计算 b 1 b^1 b1 的时候,也可以同时计算 b 2 b^2 b2
根据同样地方式我们可以计算出
b
3
,
b
4
b^3,b^4
b3,b4,如果我们不关心怎么计算的(或者没看到上面的)那就暂时记住,我们的输入经过一个 Self-Attention
后得到了
b
1
,
b
2
,
b
3
,
b
4
b^1,b^2,b^3,b^4
b1,b2,b3,b4,如下图:
平行化处理(矩阵方式)
- 计算 Q , K , V Q,K,V Q,K,V
- 计算 A ^ \hat{A} A^
- 计算 O O O
整体上再看一遍
我们可以发现,
Self-Attention
内部就是做一系列的矩阵运算,而矩阵运算很容易用 GPU 加速
Multi-head Self-Attention
(多头自注意力)
以两个头为例:
那么这个有什么作用呢?
- 不同的head可能关注的点事不一样的
这里是以两个head为例,实际过程中,head的数目是一个参数,需要调的
图解Transformer
具备了上面的知识,对理解 Transformer
就轻而易举了
整体结构
这里我们依然以机器翻译为例:
我们先将整个模型视为黑盒,比如在机器翻译中,接收一种语言的句子作为输入,然后将其翻译成其他语言输出。
细看下,其中由编码组件、解码组件和它们之间的连接层组成。
编码组件是六层编码器首位相连堆砌而成,解码组件也是六层解码器堆成的。
编码器是完全结构相同的,但是并不共享参数,每一个编码器都可以拆解成以下两个字部分。
- 编码器的输入首先流过一个
self-attention
层,当它编码某个词时,该层帮助编码器能够看到输入序列中的其他单词 self-attention
的输出流向一个前向网络,每个输入位置对应的前向网络是独立互不干扰的。
解码器同样也有这些子层,但是在两个子层间增加了
attention层
,该层有助于解码器能够关注到输入句子的相关部分,与seq2seq model
的Attention
作用相似。
输入
现在,我们解析下模型最主要的组件,从向量/Tensor开始,然后是它们如何流经各个组件们并输出的。正如NLP应用的常见例子,先将输入单词使用embedding algorithm
转成向量。
每个词映射到512维向量上,此处用box表示向量,只显示4维
- 词的向量化仅仅发生在最底层的编码器的输入时,这样每个编码器的都会接收到一个list(每个元素都是512维的词向量),只不过其他编码器的输入是前个编码器的输出。
list的尺寸是可以设置的超参,通常是训练集的最长句子的长度。
- 在对输入序列做词的向量化之后,它们流经编码器的如下两个子层。
- 这里能看到
Transformer
的一个关键特性,每个位置的词仅仅流过它自己的编码器路径。但是在self-attention
层中,这些路径两两之间是相互依赖的(看完前部分内容的应该就知道了)- 而前向网络层则没有这些依赖性
- 这些路径在流经前向网络时可以并行执行
Encoder阶段
正如之前所提,编码器接收向量的list作输入。然后将其送入self-attention处理,再之后送入前向网络,最后将输入传入下一个编码器。
每个位置的词向量被送入
self-attention
模块,然后是前向网络(对每个向量都是完全相同的网络结构)
不要被 self-attention
这个词迷惑了(个人理解:就是加权,权重大的多关注,权重小的少关注),下面我们逐步分解下它是如何工作的:
以下面这句话为例,作为我们想要翻译的输入语句
The animal didn’t cross the street because it was too tired
。句子中it
指的是什么呢?“it"指的是"street” 还是“animal”?对人来说很简单的问题,但是对算法而言并不简单。
当模型处理单词“it”时,self-attention
允许将“it”和“animal”联系起来。当模型处理每个位置的词时,self-attention
允许模型看到句子的其他位置信息作辅助线索来更好地编码当前词。如果你对RNN熟悉,就能想到RNN的隐状态是如何允许之前的词向量来解释合成当前词的解释向量。
Transformer
使用self-attention
来将相关词的理解编码到当前词中。
Self-Attention细节
一般计算
前面我们已经介绍了 Self-Attention
这里我们带入到 Transformer
再来巩固一遍。
我们先看下如何计算 self-attention
的向量,再看下如何以矩阵方式计算。
- 第一步,根据编码器的输入向量,生成三个向量,比如,对每个词向量,生成query-vec, key-vec, value-vec,生成方法为分别乘以三个矩阵,这些矩阵在训练过程中需要学习。
注意:不是每个词向量独享3个matrix,而是所有输入共享3个转换矩阵;权重矩阵是基于输入位置的转换矩阵;有个可以尝试的点,如果每个词独享一个转换矩阵,会不会效果更厉害呢?
注意到这些新向量的维度比输入词向量的维度要小(512–>64),并不是必须要小的,是为了让 Multi-head attention
的计算更稳定。
所谓的 query/key/value-vec
是什么?
这种提取对计算和思考attention
是有益的,当读完下面attention
是如何计算的之后,你将对这些向量的角色有更清晰的了解。
- 第二步,计算attention就是计算一个分值。对“Thinking Matchines”这句话,对“Thinking”(pos#1)计算attention 分值。我们需要计算每个词与“Thinking”的评估分,这个分决定着编码“Thinking”时(某个固定位置时),每个输入词需要集中多少关注度。
这个分,通过“Thing”对应query-vector与所有词的key-vec依次做点积得到。所以当我们处理位置#1时,第一个分值是 q 1 q_1 q1 和 k 1 k_1 k1 的点积,第二个分值是 q 1 q_1 q1 和 k 2 k_2 k2 的点积。
第三步和第四步,除以8( = d k e y =\sqrt{d_{key}} =dkey),这样梯度会更稳定。然后加上softmax操作,归一化分值使得全为正数且加和为1。
softmax
分值决定着在这个位置,每个词的表达程度(关注度)。很明显,这个位置的词应该有最高的归一化分数,但大部分时候总是有助于关注该词的相关的词。
- 第五步,将
softmax
分值与value-vec
按位相乘。保留关注词的value
值,削弱非相关词的value
值。 - 第六步,将所有加权向量加和,产生该位置的
self-attention
的输出结果。
上述就是self-attention
的计算过程,生成的向量流入前向网络。在实际应用中,上述计算是以速度更快的矩阵形式进行的。下面我们看下在单词级别的矩阵计算。
矩阵计算
第一步,计算query/key/value matrix
,将所有输入词向量合并成输入矩阵
X
X
X,并且将其分别乘以权重矩阵
W
q
,
W
k
,
W
v
W_q,W_k,W_v
Wq,Wk,Wv
输入矩阵X的每一行表示输入句子的一个词向量
最后,鉴于我们使用矩阵处理,将步骤2~6合并成一个计算self-attention层输出的公式。
矩阵形式的
self-attention
计算
多头机制
论文进一步增加了 multi-headed
的机制到 self-attention
上,在如下两个方面提高了 attention
层的效果:
- 多头机制扩展了模型集中于不同位置的能力。在上面的例子中,z1只包含了其他词的很少信息,仅由实际自己词决定。在其他情况下,比如翻译
The animal didn’t cross the street because it was too tired
时,我们想知道单词"it"指的是什么。 - 多头机制赋予attention多种子表达方式。像下面的例子所示,在多头下有多组query/key/value-matrix,而非仅仅一组(论文中使用8-heads)。每一组都是随机初始化,经过训练之后,输入向量可以被映射到不同的子表达空间中。
每个head都有一组Q/K/V matrix
如果我们计算 multi-headed self-attention
的,分别有八组不同的 Q/K/V matrix
,我们得到八个不同的矩阵。
这会带来点麻烦,前向网络并不能接收八个矩阵,而是希望输入是一个矩阵,所以要有种方式处理下八个矩阵合并成一个矩阵。
上述就是多头自注意机制的内容,我认为还仅是一部分矩阵,下面尝试着将它们放到一个图上可视化如下。
现在加入 attention heads
之后,重新看下当编码“it”时,哪些 attention head
会被集中。
编码"it"时,一个
attention head
集中于"the animal",另一个head集中于“tired”,某种意义上讲,模型对“it”的表达合成了的“animal”和“tired”两者
如果我们将所有的 attention heads
都放入到图中,就很难直观地解释了。
加入位置编码
截止到目前为止,我们还没有讨论如何理解输入语句中词的顺序。
为解决词序的利用问题,Transformer
新增了一个向量对每个词,这些向量遵循模型学习的指定模式,来决定词的位置,或者序列中不同词的举例。对其理解,增加这些值来提供词向量间的距离,当其映射到 Q/K/V
向量以及点乘的 attention
时。
为了能够给模型提供词序的信息,新增位置emb向量,每个向量值都遵循指定模式
如果假设位置向量有4维,实际的位置向量将如下所示:
一个只有4维的位置向量表示例子
所谓的指定模式是什么样的呢?
在下图中,每一行表示一个位置的pos-emb,所以第一行是我们将要加到句子第一个词向量上的vector。每个行有512值,每个值范围在[-1,1],我们将要涂色以便于能够将模式可视化。
一个真实的例子有20个词,每个词512维。可以观察中间显著的分隔,那是因为左侧是用sin函数生成,右侧是用cos函数生成。
残差
编码器结构中值得提出注意的一个细节是,在每个子层中(slef-attention+ffnn
),都有残差连接,并且紧跟着 layer-normalization
。
如果我们可视化向量和layer-norm
操作,将如下所示:
在解码器中也是如此,假设两层编码器+两层解码器组成Transformer,其结构如下:
Decoder阶段
现在我们已经了解了编码器侧的大部分概念,也基本了解了解码器的工作方式,下面看下他们是如何共同工作的。
编码器从输入序列的处理开始,最后的编码器的输出被转换为K和V,它俩被每个解码器的 encoder-decoder atttention
层来使用,帮助解码器集中于输入序列的合适位置。
在编码之后,是解码过程;解码的每一步输出一个元素作输出序列
下面的步骤一直重复直到一个特殊符号出现表示解码器完成了翻译输出。每一步的输出被喂到下一个解码器中。正如编码器的输入所做的处理,对解码器的输入增加位置向量。
-
在解码器中的
self attention
层与编码器中的稍有不同,在解码器中,self-attention 层仅仅允许关注早于当前输出的位置。在softmax之前,通过遮挡未来位置(将它们设置为-inf)来实现。 -
Encoder-Decoder Attention
层工作方式跟multi-headed self-attention
是一样的,除了一点,它从前层获取输出转成query矩阵,接收最后层编码器的key和value矩阵做key和value矩阵。
最后的线性和Softmax层
解码器最后输出浮点向量,如何将它转成词?这是最后的线性层和softmax层的主要工作。
线性层是个简单的全连接层,将解码器的最后输出映射到一个非常大的logits向量上。假设模型已知有1万个单词(输出的词表)从训练集中学习得到。那么,logits向量就有1万维,每个值表示是某个词的可能倾向值。
softmax层将这些分数转换成概率值(都是正值,且加和为1),最高值对应的维上的词就是这一步的输出单词。
回顾训练
现在我们已经了解了一个训练完毕的Transformer的前向过程,顺道看下训练的概念也是非常有用的。
在训练时,模型将经历上述的前向过程,当我们在标记训练集上训练时,可以对比预测输出与实际输出。为了可视化,假设输出一共只有6个单词(“a”, “am”, “i”, “thanks”, “student”, “”)
模型的词表是在训练之前的预处理中生成的
一旦定义了词表,我们就能够构造一个同维度的向量来表示每个单词,比如one-hot编码,下面举例编码“am”。
举例采用one-hot编码输出词表
下面让我们讨论下模型的loss损失,在训练过程中用来优化的指标,指导学习得到一个非常准确的模型。
损失函数
我们用一个简单的例子来示范训练,比如翻译“merci”为“thanks”。那意味着输出的概率分布指向单词“thanks”,但是由于模型未训练是随机初始化的,不太可能就是期望的输出。
由于模型参数是随机初始化的,未训练的模型输出随机值。我们可以对比真实输出,然后利用误差后传调整模型权重,使得输出更接近与真实输出
如何对比两个概率分布呢?简单采用 cross-entropy或者Kullback-Leibler divergence中的一种。
鉴于这是个极其简单的例子,更真实的情况是,使用一个句子作为输入。比如,输入是“je suis étudiant”,期望输出是“i am a student”。在这个例子下,我们期望模型输出连续的概率分布满足如下条件:
- 每个概率分布都与词表同维度。
- 第一个概率分布对“i”具有最高的预测概率值。
- 第二个概率分布对“am”具有最高的预测概率值。
- 一直到第五个输出指向"<eos>"标记。
在足够大的训练集上训练足够时间之后,我们期望产生的概率分布如下所示:
训练好之后,模型的输出是我们期望的翻译。当然,这并不意味着这一过程是来自训练集。注意,每个位置都能有值,即便与输出近乎无关,这也是softmax对训练有帮助的地方。
现在,因为模型每步只产生一组输出,假设模型选择最高概率,扔掉其他的部分,这是种产生预测结果的方法,叫做greedy 解码。另外一种方法是beam search,每一步仅保留最头部高概率的两个输出,根据这俩输出再预测下一步,再保留头部高概率的两个输出,重复直到预测结束。top_beams是超参可试验调整。
总结
优点:
- 每层计算复杂度降低
- 计算可以被并行化
- CNN需要增加卷积层数来扩大视野,RNN需要从1到n逐个进行计算,而self-attention只需要一步矩阵计算就可以
self-attention
模型更可解释,attention
结果的分布表明了该模型学习到了一些语法和语义信息
缺点:
- 实践上:有些
RNN
轻易可以解决的问题transformer
没做到,比如复制string,或者推理时碰到的sequence
长度比训练时更长(因为碰到了没见过的position embedding
) - 理论上:
transformers
非computationally universal
(图灵完备)
Transformer是第一个用纯attention搭建的模型,不仅计算速度更快,在翻译任务上也获得了更好的结果。
参考链接: