2017 年,Google 在论文 Attention is All you need 中提出了 Transformer 模型,其使用 Self-Attention 结构取代了在 NLP 任务中常用的 RNN 网络结构。
RNN、LSTM 和 GRU 网络已在序列模型、语言建模、机器翻译等领域中取得不错的效果。循环结构 (recurrent) 的语言模型和编码器 - 解码器体系结构取得了不错的进展。但是,RNN 固有的顺序属性阻碍了训练样本间的并行化,对于长序列,内存限制将阻碍对训练样本的批量处理。
Transformer是一种避免循环 (recurrent) 的模型结构,完全基于注意力机制对输入输出的全局依赖关系进行建模。因为对依赖的建模完全依赖于注意力机制,Transformer 使用的注意力机制被称为自注意力(self-attention)。
优势主要有:
1.突破了 RNN 模型不能并行计算的限制,可以充分利用GPU资源。
2.可以直接计算每个词之间的相关性,不需要通过隐藏层进行传递。
2.自注意力可以产生更具可解释性的模型。我们可以从模型中检查注意力分布。各个注意头 (attention head) 可以学会执行不同的任务。
其存在的不足有:
1.局部特征捕捉能力不足。粗暴的抛弃RNN和CNN虽然非常新颖,但是它也使模型丧失了捕捉局部特征的能力,RNN + CNN + Transformer的结合可能会带来更好的效果。
2.位置信息编码存在问题。Transformer失去的位置信息其实在NLP中非常重要,而论文中在特征向量中加入Position Embedding也只是一个权宜之计,并没有改变Transformer结构上的固有缺陷。
3.可能出现顶层梯度消失。Transformer 模型实际上是由一些残差模块与层归一化模块组合而成。目前最常见的 Transformer 模型都使用了LN,即层归一化模块位于两个残差模块之间。因此,最终的输出层与之前的 Transformer 层都没有直连通路,梯度流会被层归一化模块阻断。
下图为Transformer的总体架构:
1.1 Transformer概览
Transformer最初是应用到机器翻译任务中,我们可以先将 Transformer 模型视为一个黑盒。如图 1-1-1 所示。在机器翻译任务中,将一种语言的句子作为输入,然后将其翻译成另一种语言的句子作为输出。
图1-1-1 Transformer黑盒模型用于机器翻译
Transformer 本质上是一个 Encoder-Decoder 架构。因此中间部分的 Transformer 可以分为两个部分:编码组件和解码组件。如图 1-1-2 所示:
图1-1-2 Transformer的编码器-解码器架构模式
其中,编码组件由多层编码器(Encoder)组成(在论文中作者使用了 6 层编码器,在实际使用过程中可以尝试其他层数)。解码组件也是由相同层数的解码器(Decoder)组成(在论文也使用了 6 层)。如图 1-1-3所示:
图1-1-3 6层编码器和6层解码器组成的Transformer
使用不同的权重参数。
图1-1-4 编码器的子层:自注意层和FFN
编码器的输入会先流入 Self-Attention 层。它可以让编码器在对特定词进行编码时使用输入句子中的其他词的信息(可以理解为:当我们翻译一个词时,不仅只关注当前的词,而且还会关注其他词的信息)。然后,Self-Attention 层的输出会流入前馈网络。
解码器也有编码器中这两层,但是它们之间还有一个注意力层(即 Encoder-Decoder Attention),其用来帮助解码器关注输入句子的相关部分(类似于 seq2seq 模型中的注意力)。
图1-1-5 解码器的三个子层
1.2 张量
现在我们已经了解了模型的主要组成部分,让我们开始研究各种向量/张量,以及它们在这些组成部分之间是如何流动的,从而将输入经过已训练的模型转换为输出。
和通常的 NLP 任务一样,首先,我们使用词嵌入算法(Embedding)将每个词转换为一个词向量。在 Transformer 原论文中,词嵌入向量的维度是 512。
图1-2-1 每个词被嵌入到大小为512维度的向量中,由小方格表示
基本上这个参数就是训练数据集中最长句子的长度。
对输入序列完成嵌入操作后,每个词都会流经编码器的两层。
图1-2-2 词嵌入向量通过编码器的两个子层
接下来,我们将换一个更短的句子作为示例,来说明在编码器的每个子层中发生了什么。
上面我们提到,编码器会接收一个向量作为输入。编码器首先将这些向量传递到 Self-Attention 层,然后传递到前馈网络,最后将输出传递到下一个编码器。
图1-2-3 词嵌入向量逐步通过子层
1.3 Self-Attention自注意力
1.3.1 Self-Attention概览
首先我们通过一个例子,来对 Self-Attention 有一个直观的认识。假如,我们要翻译下面这个句子:
The animal didn’t cross the street because it was too tired
这个句子中的 it 指的是什么?是指 animal 还是 street ?对人来说,这是一个简单的问题,但是算法来说却不那么简单。
当模型在处理 it 时,Self-Attention 机制使其能够将 it 和 animal 关联起来。
当模型处理每个词(输入序列中的每个位置)时,Self-Attention 机制使得模型不仅能够关注当前位置的词,而且能够关注句子中其他位置的词,从而可以更好地编码这个词。
如果你熟悉 RNN,想想如何维护隐状态,使 RNN 将已处理的先前词/向量的表示与当前正在处理的词/向量进行合并。Transformer 使用 Self-Attention 机制将其他词的理解融入到当前词中。
图 1-3-1 最后一层编码器(#5)中it词的编码信息
可以看到,在对it编码时,有一部分注意力集中在"The animal"上,并将它们的部分信息融入到"it"的编码中。
1.3.2 Self-Attention机制
下面我们来看一下 Self-Attention 的具体机制。其基本结构如图 1-3-2 所示:
图1-3-2 Scaled Dot-Product Attention(缩放点积注意力)
对于 Self Attention 来讲,Q(Query),K(Key)和 V(Value)三个矩阵均来自同一输入,并按照以下步骤计算:
- 首先计算 Q 和 K 之间的点积,为了防止其结果过大,会除以
- ,其中
- 为Key向量的维度。
- 然后利用softmax操作将其结果归一化为概率分布,再乘以矩阵V就得到权重求和的表示。
整个计算过程可以表示为:
关于Q、K、V矩阵的理解:
V是一个值序列,序列中的每个值在一开始都看做是互相独立的,信息量有限。而想要让每个值包含更多的信息,一个办法就是让每个值去融合该序列中其他值的信息。融合方式中,最差的方法是取平均(mean pool),会有大量实际不相关的信息被融入; 更好的方式是按照该值和其他值的相关度加权求和。
那么问题来了,怎么看V中每一个值和其他值的相关度,并将相关度量化成权重数值(用于最终的加权求和)?
这个时候,Q和K来了,Q是询问的值(对应到V中的某个值),K是被询问的值序列,Q问K,你的哪个值跟我最像?Q挨个问一遍K中的值,每个值和Q比较后得到一个和Q的相似程度,组成一个相似度序列。相似度序列归一化后变成一个和为1序列,可以看成是一个权重。这个权重就是V序列中对应的那个值和其他值的相关度的权重量化。
概括: Q查K以提供给V中每个值和其他值之间加权求和的权重。
为了更好的理解 Self-Attention,下面我们通过具体的例子进行详细说明。
1.3.3 Self-Attention机制详解
下面通过一个例子,让我们看一下如何使用向量计算 Self-Attention。计算 Self-Attention 的步骤如下:
第 1 步:对编码器的每个输入向量(在本例中,即每个词的词向量)创建三个向量:Query 向量、Key 向量和 Value 向量。它们是通过词向量分别和 3 个矩阵相乘得到的,这 3 个矩阵通过训练获得。
请注意,这些向量的维数小于词向量的维数。新向量的维数为 64,而 Embedding 和编码器输入/输出向量的维数为 512。新向量不一定非要更小,这是为了使多头注意力计算保持一致的结构性选择。
图1-3-3 词向量求得其对应的q,k,v向量
可以看到,词向量通过与矩阵
,
,
分别相乘得到其对应的q,k,v向量,最终会为输入句子中的每个词创建一个 Query,一个 Key 和一个 Value 向量。
什么是 Query,Key 和 Value 向量?
它们是一种抽象,对于注意力的计算和思考非常有用。继续阅读下面的注意力计算过程,你将了解这些向量所扮演的角色。
第 2 步:计算注意力分数。假设我们正在计算这个例子中第一个词 “Thinking” 的自注意力。我们需要根据 “Thinking” 这个词,对句子中的每个词都计算一个分数。这些分数决定了我们在编码 “Thinking” 这个词时,需要对句子中其他位置的每个词放置多少的注意力。
这些分数,是通过计算 “Thinking” 的 Query 向量和需要评分的词的 Key 向量的点积得到的。如果我们计算句子中第一个位置词的注意力分数,则第一个分数是
和
的点积,第二个分数事
和
的点积。通过向量点积可以表征词向量间的相似性。最终结果就是与“Thinking”相关性较大的词向量点积数值较大。
图1-3-4 计算第一个位置词的注意力分数
第 3 步:将每个分数除以
,
是 Key 向量的维度。目的是在反向传播时,求梯度更加稳定。实际上,也可以除以其他数。
第 4 步:将这些分数进行 Softmax 操作。Softmax 将分数进行归一化处理,使得它们都为正数并且和为 1,如图1-3-5所示。
图1-3-5 对位置词的注意力分数进行处理
这些 Softmax 分数决定了在编码当前位置的词时,对所有位置的词分别有多少的注意力。很明显,当前位置的词汇有最高的分数,但有时注意一下与当前位置的词相关的词是很有用的。
第 5 步:将每个 Softmax 分数分别与每个 Value 向量相乘。这种做法背后的直觉理解是:对于分数高的位置,相乘后的值就越大,我们把更多的注意力放在它们身上;对于分数低的位置,相乘后的值就越小,这些位置的词可能是相关性不大,我们就可以忽略这些位置的词。
第 6 步:将加权 Value 向量(即上一步求得的向量)求和。这样就得到了自注意力层在这个位置的输出。
图1-3-6 当前位置词的注意力计算结果
这样就完成了自注意力的计算。生成的向量会输入到前馈网络中。但是在实际实现中,此计算是以矩阵形式进行,以便实现更快的处理速度。下面我们来看看如何使用矩阵计算。
1.3.4 使用矩阵计算Self-Attention
第 1 步:计算 Query,Key 和 Value 矩阵。首先,将所有词向量放到一个矩阵X中,然后分别和 3 个训练过的权重矩阵(
,
,
)相乘,得到Q、K、V矩阵。
图1-3-7 对输入向量矩阵X求Q、K、V矩阵
X中的每一行,表示输入句子中的每一个词的词向量(长度为 512,在图中为 4 个方框),本例中有两个词,所以矩阵维度为2x512。矩阵 Q,K 和 V
第 2 步:计算自注意力。由于这里使用了矩阵进行计算,可以将前面的第 2 步到第 6 步压缩为一步。
图1-3-8 求自注意力得分
1.4 多头注意力机制 Multi-head Attention
在 Transformer 论文中,通过添加一种多头注意力机制,进一步完善了自注意力层。具体做法:首先,通过 h h h 个不同的线性变换对 Query、Key 和 Value 进行映射;然后,将不同的 Attention 拼接起来;最后,再进行一次线性变换。基本结构如图1-3-9所示:
图1-4-1 多头注意力基本结构
每一组注意力用于将输入映射到不同的子表示空间,这使得模型可以在不同子表示空间中关注不同的位置。整个计算过程可表示为:
其中
,
,
,
,在原论文中指定 h = 8(即使用 8 个注意力头),
,
,Q,K,V可以理解为输入的词向量矩阵X。X矩阵乘以
,
,
矩阵得到 Query、Key 和 Value 矩阵。
图1-4-2 多头注意力计算各自的Q,K,V矩阵
Z矩阵。
图1-4-3 8个注意力头计算得到8个Z矩阵
接下来就有点麻烦了。因为前馈神经网络层接收的是 1 个矩阵(每个词的词向量),而不是上面的 8 个矩阵。因此,我们需要一种方法将这 8 个矩阵整合为一个矩阵。具体方法如下:
- 将8个矩阵{
- ,...,
- }拼接起来。
- 将拼接矩阵与权重矩阵
- 相乘。
- 得到最终的矩阵Z,这个矩阵包含了所有注意力头的信息。这个矩阵会输入到 FFN 层。
图1-4-4 拼接各个注意力头计算得到的矩阵并与权重矩阵相乘
这差不多就是多头注意力的全部内容了。下面将所有内容放到一张图中,以便我们可以统一查看。
图1-4-5 多注意力头计算Z矩阵的全过程
input sentence -> word embedding -> 各个头根据权重矩阵计算矩
,
,
-> 计算
-> 合并
并与权重矩阵
相乘获得最终注意力层输出Z。
现在让我们重新回顾一下前面的例子,看看在对示例句中的“it”进行编码时,不同的注意力头关注的位置分别在哪:
图1-4-6 不同注意力头关注的情况
当我们对“it”进行编码时,一个注意力头关注“The animal”,另一个注意力头关注“tired”。从某种意义上来说,模型对“it”的表示,融入了“animal”和“tired”的部分表达。
Multi-head Attention 的本质是,在参数总量保持不变的情况下,将同样的 Query,Key,Value 映射到原来的高维空间的不同子空间中进行 Attention 的计算,在最后一步再合并不同子空间中的 Attention 信息。这样降低了计算每个 head 的 Attention 时每个向量的维度,在某种意义上防止了过拟合;由于 Attention 在不同子空间中有不同的分布,Multi-head Attention 实际上是寻找了序列之间不同角度的关联关系,并在最后拼接这一步骤中,将不同子空间中捕获到的关联关系再综合起来。
关于参数总量保持不变:
这里的参数主要是 W_Q/W_K/W_V,假设注意力层的输入维度是 512(即 x 的维度),输出维度 512。
(1)单头注意力参数:因为输出维度是 512,则 Q/K/V 的维度是 512,那么 W_Q/W_K/W_V 的维度都是 512*512,即参数总量 3*512*512。
(2)多头注意力参数:假设头数为 8,则 Q/K/V 的维度是 512 / 8 = 64,每个 W_Q/W_K/W_V 的维度都是 512*64,即单个注意力头的参数总量为 3*512*64,那么8个注意力头的总参数量是 3*512*64*8。
综合以上,我们可以发现,在单头和多头中,W_Q/W_K/W_V 的参数总量都是 3*512*512。区别点在于:在每个头中,W_Q/W_K/W_V 的维度不一样,但是总数是一样的。
1.5 位置前馈网络Position-wise Feed-Forward Networks
位置前馈网络就是一个全连接前馈网络,每个位置的词都单独经过这个完全相同的前馈神经网络。其由两个线性变换组成,即两个全连接层组成。两个全连接层之间有一个 ReLU 激活函数。可以表示为:
FFN(x)=max(0,
x+
)
+
不共享参数。整个前馈网络的输入和输出维度都是
=512,第一个全连接层的输出和第二个全连接层的输入维度为
=2048。
1.6 残差链接和层归一化
编码器结构中有一个需要注意的细节:每个编码器的每个子层(Self-Attention 层和 FFN 层)都有一个残差连接,再执行一个层标准化操作,整个计算过程可以表示为:
sub_layer_output=LayerNormal(x+SubLayer(x))
图1-6-1 编码器中的残差连接和层归一化
其中Add代表了Residual Connection,是为了解决多层神经网络训练困难的问题,通过将一部分的前一层的信息无差的传递到下一层,可以有效的提升模型性能——因为对于有些层,我们并不确定其效果是不是正向的。加了残差连接之后,我们相当于将上一层的信息兵分两路,一部分通过我们的层进行变换,另一部分直接传入下一层,再将这两部分的结果进行相加作为下一层的输出。这样的话,其实可以达到这样的效果:我们通过残差连接之后,就算再不济也至少可以保留上一层的信息,这是一个非常巧妙的思路。
而Norm则代表了Layer Normalization,通过对层的激活值的归一化,可以加速模型的训练过程,使其更快的收敛。
将向量和自注意力层的层标准化操作可视化,如下图所示:
图1-6-2 编码器中的残差连接和层归一化可视化
上面的操作也适用于解码器的子层。假设一个 Transformer 是由 2 层编码器和 2 层解码器组成,其如下图所示:
图1-6-3 编码器解码器的残差连接和层归一化
为了方便进行残差连接,编码器和解码器中的所有子层和嵌入层的输出维度需要保持一致,在 Transformer 论文中
=512。
1.7 位置编码
到目前为止,我们所描述的模型中缺少一个东西:表示序列中词顺序的方法。为了解决这个问题,Transformer 模型为每个输入的词嵌入向量添加一个向量。这些向量遵循模型学习的特定模式,有助于模型确定每个词的位置,或序列中不同词之间的距离。
图1-7-1 使用positional encoding表示位置信息
如果我们假设词嵌入向量的维度是 4,那么实际的位置编码如下:
那么位置编码向量到底遵循什么模式?其具体的数学公式如下:
1.8 编码器
现在我们已经介绍了编码器的大部分概念,我们也了解了解码器的组件的原理。现在让我们看下编码器和解码器是如何协同工作的。
通过上面的介绍,我们已经了解第一个编码器的输入是一个序列,最后一个编码器的输出是一组注意力向量 Key 和 Value。这些向量将在每个解码器的 Encoder-Decoder Attention 层被使用,这有助于解码器把注意力集中在输入序列的合适位置。
图1-8-1 编码器与解码器协同工作
在完成了编码阶段后,我们开始解码阶段。解码阶段的每个时间步都输出一个元素。
接下来会重复这个过程,直到输出一个结束符,表示 Transformer 解码器已完成其输出。每一步的输出都会在下一个时间步输入到下面的第一个解码器,解码器像编码器一样将解码结果显示出来。就像我们处理编码器输入一样,我们也为解码器的输入加上位置编码,来指示每个词的位置。
Encoder-Decoder Attention 层的工作原理和多头自注意力机制类似。不同之处是:Encoder-Decoder Attention 层使用前一层的输出构造 Query 矩阵,而 Key 和 Value 矩阵来自于编码器栈的输出。