文章目录
- 一、循环神经网络
- 1.1RNN模型
- 1.1.1RNN模型简介
- 1.1.2RNN基本结构
- 1.1.3权重共享机制
- 1.1.4RNN局限性:长期依赖问题与梯度消失
- 1.2LSTM模型
- 1.2.1LSTM核心思想
- 1.2.2遗忘门
- 1.2.3输入门
- 1.2.4更新细胞状态
- 1.2.5输出门
- 1.2.6参数更新
- 二、Seq2Seq机制
- 2.1RNN结构的局限性
- 2.2编码器-解码器结构
- 2.3总结
- 三、Self-Attention
- 注意力机制案例1
- 注意力机制案例2
- 3.1CNN与RNN的局限
- 3.2Self-Attention
- 3.2.1Self-Attention原理
- 3.2.2代码实现
- 3.3Multi-Head Attention
- 3.3.1Multi-Head Attention原理
- 3.3.2代码实现
- 3.4Positional Encoding
- 3.5Maked Attention
- 3.6Self-Attention的应用
- 三、Transformer
- 3.1Encoder Block
- 补充知识:Batch Normalization与Layer Normalization
- 3.2Decoder Block
- 3.3Output Block
- 3.4代码实现
论文:Attention is All You Need
论文链接:Attention is All You Need
一、循环神经网络
1.1RNN模型
1.1.1RNN模型简介
RNN(Recurrent Neural Network)是一类用于处理序列数据的神经网络。常见的序列数据包括时间序列数据(不同时间点上收集到的数据,反映了某一事物、现象等随时间的变化状态或程度)、文本序列数据等,这些序列数据有这公共的特点,即后面的数据跟前面的数据有关系。RNN是神经网络中的一种,能够挖掘数据中的时序信息以及语义信息,对具有序列特性的数据较为有效,而类似的神经网络模型如CNN,并不能很好处理这类数据(CNN对输入图像数据的处理是独立的)。
【例子】将昨天,我吃了苹果翻译为英文。
若模型不能考虑文字之间的序列关系,则会进行逐词翻译得到yesterday I eat apple。事实上,在翻译吃时应当考虑之前输入过昨天这一信息,从而翻译为yesterday I ate apple。可见,序列数据中数与数据之间的内在联系对于模型的输出有着直接的影响,为更准确地捕捉和利用这些联系,提出了循环神经网络(RNN)。
1.1.2RNN基本结构
与传统的神经网络相比,循环神经网络(RNN)的特殊之处在于其结构中增加了循环部分。这个循环部分允许信息在网络的层级之间进行循环,使得网络能够处理序列数据,并且能够利用之前的信息来影响后续的处理和输出。

在传统神经网络中,输入信息会通过权重矩阵和激活函数进行处理得到最终输出结果。而RNN在生成当前输出时不仅考虑当前的输入信息,还会考虑上一时间步(非时间序列数据即为上一次输入的数据)传递的信息,即为图中的
w
w
w方块。这就使得RNN能够在考虑上一时间步数据的情况下处理当前输入的数据,从而更有效地处理序列数据。
将模型按照时间步展开:

- x t x_t xt: t t t时刻输入模型的数据。
- U U U: x t x_t xt的权重矩阵。
- s t s_{t} st:当前时刻模型的状态(实际是获取的特征信息,可理解为记忆体),由上一时刻模型的状态 s t − 1 s_{t-1} st−1与当前时刻的输入信息 x t x_t xt共同组成。
- W W W:上一时刻模型状态 s t − 1 s_{t-1} st−1的权重。
- O t O_t Ot: t t t时刻模型的输出。
- V V V:模型输出的权重。
从此得到模型的状态公式与输出公式:


其中,
f
、
g
f、g
f、g表示不同的激活函数。总体流程如下:
- t t t时刻,模型接受输入数据 x t x_t xt和 t − 1 t-1 t−1时刻的隐藏层状态,分别经过权重矩阵 U 、 W U、W U、W的线性变换(两矩阵输出维度一致),两部分结果被结合并通过一个激活函数来生成当前时刻 t t t的隐藏层状态 s t s_t st。
- 隐藏层状态 s t s_t st经过权重矩阵 V V V与激活函数 g g g的计算得到 t t t时刻隐藏层的输出 O t O_t Ot。
- 以此类推,隐藏层状态 s t s_t st会被复制并传递到下一个时间步。
1.1.3权重共享机制
在全连接神经网络中,每个输入数据特征都有自己的权重参数。在卷积神经网络中,卷积层 的卷积核就是其权重,卷积核在特征图上进行滑动,不断和特征图中的数值进行计算,因此一个特征图共享了一组权重。而在RNN中,不同时间步参与计算的权重
U
、
V
、
W
U、V、W
U、V、W都是共享的:

因此循环神经网络的网络参数数要少很多,这大大减少了计算机的计算量,也不会容易过拟合。
1.1.4RNN局限性:长期依赖问题与梯度消失
以预测the clouds are in the sky最后的词sky为例,在这样的场景中,相关的信息(the clouds are in the)和预测的词(sky)位置之间的间隔是非常小的,RNN可以学会使用先前的信息。但是当尝试预测I grew up in France...(中间一大段其他信息)I speak fluent French最后的词French时,由于相关信息(I grew up in France)和当前预测位置(I speak fluent French)之间的间隔非常大,而RNN会在训练过程中逐渐丧失学习到如此远信息的能力(在实际训练过程中,可能一开始就会遗漏重要信息)。如:

即,RNN会受到短时记忆的影响。如果一条序列足够长,那它们将很难将信息从较早的时间步传送到后面的时间步(权重是共享的,
X
0
、
X
1
X_0、X_1
X0、X1的输入信息会在传递过程中被不断弱化)。这一问题对模型的影响表现在:
- 梯度消失问题:RNN可能从一开始就会遗漏重要信息,而在反向传播(通过不断缩小误差来更新参数,从而不断去拟合真实函数曲线)时,因为权重值的更新方式为: 新的权值 = 旧权值 − 学习率 ∗ 梯度 新的权值 = 旧权值 - 学习率*梯度 新的权值=旧权值−学习率∗梯度,而梯度会随着传播到较早时间步时变得非常小,此时获得小梯度更新的层会停止学习,导致模型精度下降,即RNN只具有短期记忆。
- 梯度爆炸问题:函数求导导致。
理论推导见:
梯度消失与爆炸理论推导
RNN存在的问题
1.2LSTM模型
1.2.1LSTM核心思想
LSTM的核心思想是细胞状态,并使用
C
i
C_i
Ci来传输细胞状态:

细胞状态类似于传送带,直接在整个链上运行,只有一些少量的线性交互。LSTM就是依靠这条传输带来保存之前经过筛选的有用信息,并利用这些信息参与当前运算,从而使得当前时刻的输出是由之前筛选后的信息和当前的输入信息综合影响输出的。相比较RNN神经网络一股脑的将之前的所有信息都作为输入有着本质的改进。并且,LSTM有通过精心设计的称作为“门”的结构来去除或者增加信息到细胞状态的能力。
1.2.2遗忘门
遗忘门用于决定细胞状态会丢失的信息,其基本结构如下:

遗忘门会读取上一个(时间步)细胞的输出
h
t
−
1
h{t-1}
ht−1和当前的输入
x
t
x_t
xt并将二者作线性变换作为sigmoid激活函数的输入,得到向量
f
t
f_t
ft,该向量每一维度的值均在
[
0
,
1
]
[0,1]
[0,1]之间(1表示完全保留,0表示完全忘记),最后与上一细胞状态
C
t
−
1
C_{t-1}
Ct−1相乘。可使用下图来查看具体计算过程:

其中,
[
h
t
−
1
,
X
t
]
[h_{t-1},X_t]
[ht−1,Xt]表示将向量
h
t
−
1
h_{t-1}
ht−1与
X
t
X_t
Xt进行拼接再运算,然后计算参数矩阵$
W
f
W_f
Wf和连接后的新向量的乘积,再将这个乘积的结果和偏置
b
f
b_f
bf求和,然后经过sigmoid激活函数进行函数映射,得到向量
f
t
f_t
ft,该向量每一个元素都是在0-1之间。注意,这里的参数
W
f
W_f
Wf需要通过反向传播进从训练数据中学习。
1.2.3输入门
输入门控制着新输入信息的流入程度。它通过使用sigmoid激活函数将当前输入与之前的记忆状态进行组合,得到一个介于0和1之间的值。接下来,通过使用另一个tanh激活函数,将当前输入与记忆状态的组合作为新的记忆候选值。总之,遗忘门用于决定上一细胞状态、当前输入应被遗忘的部分,而输入门用于决定上一细胞状态、当前输入应被记住的部分。结构如下:

其中,
i
t
i_t
it的功能等同于遗忘门(参数矩阵不同),而
C
t
~
\tilde{C_t}
Ct~是将上一时刻细胞状态的输出与当前时刻的输入作线性变换并输入
t
a
n
h
tanh
tanh函数后得到,该向量每一维度的值均在
[
−
1
,
1
]
[-1,1]
[−1,1],用于更新细胞状态:

1.2.4更新细胞状态
通过更新 C t − 1 C_{t-1} Ct−1的值来更新细胞状态。
- 将细胞旧状态 C t − 1 C_{t-1} Ct−1的值与 f t f_t ft相乘,用于决定需要忘记的内容。
- 将更新后的细胞状态加上 i t ∗ C t ~ i_t*\tilde{C_t} it∗Ct~(新的细胞状态更新值)得到最新细胞状态。

1.2.5输出门
LSTM系统的输出由两部分组成:

将上一时间步细胞的输出及当前的输入通过sigmoid激活函数得到每一维度均在
[
0
,
1
]
[0,1]
[0,1]之间的向量
o
t
o_t
ot,由该向量决定当前细胞状态
C
t
C_t
Ct有哪些部分需要作为
h
t
h_t
ht进行输出。可注意到,此处的
h
t
h_t
ht有两个输出方向,一份是作为LSTM的输出,还有一份是作为下一个时刻的输入。
1.2.6参数更新
通过上面的分析可以知道LSTM一共有4个参数矩阵分别为 W f 、 W i 、 W C 、 W o W_f、W_i、W_C 、W_o Wf、Wi、WC、Wo,这些参数矩阵可以利用梯度下降法更新参数更新,从而最终得到一个LSTM模型来帮忙我们完成对应场景中的任务。
二、Seq2Seq机制
Seq2Seq(Sequence to Sequence,输入一个序列、输出另一个序列),是一种用于处理序列数据的神经网络模型,特别适用于如机器翻译、语音识别等需要将一个序列转换为另一个序列的任务。这种模型由两部分核心组件构成:编码器(Encoder)和解码器(Decoder),故也称为编码器-解码器(Encoder-Decoder)结构
2.1RNN结构的局限性
在处理序列数据方面,RNN有以下几种常见结构:
【N to N结构】处理输入和输出序列等长的任务,如:
- 词性标注。

【1 to N结构】处理输入长度为1,输出长度为N的任务,可分为两种:输入只输入到第一个记忆单元、输入到所有时间步的记忆单元。常见有以下任务:
- 用图像生成文字,如输入一张图片,输出一段图片描述性的文字;
- 输入音乐类别,生成对应的音乐。


【N to 1结构】处理输入长度为N,输出长度为1的任务,如:
- 序列分类任务,如给定一段文本或语音序列,归到各个类(情感分类,主题分类等)

这些RNN结构有较大的局限性,如,在机器翻译任务中输入和输出数据的长度并不对等,是N to M类型,此时就无法仅靠RNN实现。由此提出了Seq2Seq模型。
2.2编码器-解码器结构
Seq2Seq模型由编码器(Encoder)和解码器(Deconder)两部分组成,共同完成输入序列到输出序列的转换过程。
- 编码器结构(Encoder):负责将输入序列转换为固定长度的上下文向量。
编码器结构(Encoder)将输入序列中的每个元素(比如单词或者音素)转换成一个高维向量表示,这个过程可以看作是将原始序列的信息压缩到一个固定长度或可变长度的向量中,称之为上下文向量(Context Vector)或者编码状态(Encoded State)。这一过程一般使用循环神经网络RNN、长短期记忆网络LSTM或门控循环单元GRU来实现,这些网络结构能够很好地处理序列数据中的时间依赖性。
- 解码器结构(Deconder):基于编码器产生的上下文向量,生成目标序列。
解码器结构(Deconder)同样使用循环神经网络(RNN)或其变体(如LSTM、GRU)来实现生成过程。在每个时间步,解码器根据上一个时间步的输出、当前的隐藏状态和上下文向量来生成当前时间步的输出,即通过逐步生成输出序列中的每个元素,最终完成整个序列的生成任务。
下图是一个简单的利用编码器-解码器结构完成英语翻译为法语的过程:

其中,<SOS>作为开始标记,<EOS>作为结束标记。
2.3总结
Seq2Seq模型通过端到端的训练方式,将输入序列和目标序列直接关联起来,避免了传统方法中繁琐的特征工程和手工设计的对齐步骤。这使得模型能够自动学习从输入到输出的映射关系,提高了序列转换任务的性能和效率。

对于Seq2Seq模型,目标函数(loss function)通常是最小化输出序列与真实序列间的某种距离,这通常是通过最大化给定输入序列情况下的输出序列的条件概率来实现的。即给定输入序列的条件下,输出序列出现的概率。这个概率可以分解为各个时间步的概率的乘积:

其中,
Y
=
(
y
1
,
y
2
,
.
.
.
y
n
)
Y=(y_1,y_2,...y_n)
Y=(y1,y2,...yn)是输出序列,
X
=
(
x
1
,
x
2
,
.
.
.
,
x
m
)
X=(x_1,x_2,...,x_m)
X=(x1,x2,...,xm)是输入序列,
θ
θ
θ是模型参数,
P
(
y
i
∣
y
1
,
y
2
,
.
.
.
y
i
−
1
,
X
,
θ
)
P(y_i|y_1,y_2,...y_{i-1},X,θ)
P(yi∣y1,y2,...yi−1,X,θ)表示在给定模型参数下,根据输入序列
X
X
X生成了
Y
=
(
y
1
,
y
2
,
.
.
.
y
i
−
1
Y=(y_1,y_2,...y_{i-1}
Y=(y1,y2,...yi−1序列后,生成下一个数据
y
i
y_i
yi的条件概率。为了优化这个目标函数,模型在训练过程中会使用如最大似然估计等方法来调整其参数,确保能够为给定的输入序列生成最有可能的正确输出序列。
三、Self-Attention
注意力机制案例1
输入以下数据:

比如要预测57的腰围对应的体重是多少。从表中可见,56对应43、58对应48,故取均值进行估计:
(
43
+
48
)
/
2
=
0.5
∗
43
+
0.5
∗
48
=
45.5
(43+48)/2=0.5*43+0.5*48=45.5
(43+48)/2=0.5∗43+0.5∗48=45.5
但问题在于,这一计算方法并未用到给出的所有数据。假设用
α
(
q
,
k
i
)
α(q,k_i)
α(q,ki)表示
q
q
q(腰围,此处为57)和
k
i
k_i
ki(体重,此处
k
1
=
51
,
k
2
=
56
,
k
3
=
58
k_1=51,k_2=56,k_3=58
k1=51,k2=56,k3=58)对应的注意力权重,则体重预测值
f
(
q
)
f(q)
f(q)为

事实上,
(
43
+
48
)
/
2
=
0.5
∗
43
+
0.5
∗
48
=
45.5
(43+48)/2=0.5*43+0.5*48=45.5
(43+48)/2=0.5∗43+0.5∗48=45.5也是用到了注意力权重的思想,只是
k
2
=
56
,
k
3
=
58
k_2=56,k_3=58
k2=56,k3=58的注意力权重均占一半(0.5),而
k
1
=
51
k_1=51
k1=51对应权重为0,这是并不准确的。
引入公式计算各注意力权重分数
a
(
q
,
k
i
)
a(q,k_i)
a(q,ki):

内部使用的是欧式距离,外部套上
s
o
f
t
m
a
x
(
)
softmax()
softmax()将其归一化(使所有权重之和为1)。得到结果:

以上数据都是一维数据,将问题扩展到二维上。用(腰围,胸围)预测(体重,身高):

此时也不再使用欧式距离来计算注意力分数:


从而将公式简化为(除以了
d
k
\sqrt{d_k}
dk,避免点积结果过大,使得Softmax梯度消失):

在Transformer模型中往往会对输入序列X乘以Q、K、V矩阵作变化,相当于获取了不同的特征信息。
注意力机制案例2
假设有一个词性标注(POS Tags)的任务,例如:输入I saw a saw(我看到了一个锯子)这句话,目标是将每个单词的词性标注出来,最终输出为N, V, DET, N(名词、动词、定冠词、名词)。

这句话中,第一个saw为动词,第二个saw(锯子)为名词。为区分二者,要求模型能够在看到一个向量(单词)时,要同时考虑其上下文向量(单词),并且,要能判断出上下文中每一个元素应该考虑多少。例如,对于第一个saw,要更多的关注I,而第二个saw,就应该多关注a。此时,就要Attention机制来提取这种关系:如果一个任务的输入是一个Sequence(向量序列),而且各向量之间有一定关系,那么就要利用Attention机制来提取这种关系。

在Attention机制中,每个输入的向量都会和其他向量计算一个相关性分数,然后基于该分数,输出包含上下文信息的新向量。如上图所示,向量
a
1
a^1
a1需要与所有向量(包括自己)计算相关性分数
α
1
,
1
,
α
1
,
2
,
α
1
,
3
,
α
1
,
4
α_{1,1},α_{1,2},α_{1,3},α_{1,4}
α1,1,α1,2,α1,3,α1,4(不同注意力机制有不同的计算方式),分数值越高表示两个向量的相关度越高。在计算完
α
1
,
i
α_{1,i}
α1,i后,即可由此求出包含
a
1
a^1
a1及其相关上下文信息的新向量
b
1
b^1
b1,设相关性分数
α
1
,
1
=
5
,
α
1
,
2
=
2
,
α
1
,
3
=
1
,
α
1
,
4
=
2
α_{1,1}=5,α_{1,2}=2,α_{1,3}=1,α_{1,4}=2
α1,1=5,α1,2=2,α1,3=1,α1,4=2,则
b
1
b^1
b1可使用加权求和的方式计算:

但也有两个问题:
- 1. α α α分数之和不为1,使得序列 ( α 1 , 1 ∗ a 1 , α 1 , 2 ∗ a 2 , α 1 , 3 ∗ a 3 , α 1 , 4 ∗ a 4 ) (α_{1,1}*a^1,α_{1,2}*a^2,α_{1,3}*a^3,α_{1,4}*a^4) (α1,1∗a1,α1,2∗a2,α1,3∗a3,α1,4∗a4)比起原始序列 ( a 1 , a 2 , a 3 , a 4 ) (a^1,a^2,a^3,a^4) (a1,a2,a3,a4)整体放大,较大的数值不利于模型的训练,可将这些分数通过softmax()进行处理。
- 2.直接用输入向量 a i a^i ai去乘的话,拟合能力不够好。可以将 a i a^i ai乘以某个矩阵(参数可训练)生成 v i v^i vi,用 v i v^i vi乘以 α α α分数。
【1.如何计算相关性分数】
两个向量相乘(做内积),公式为:
a
⋅
b
=
∣
a
∣
∣
b
∣
c
o
s
θ
a⋅b=∣a∣∣b∣cosθ
a⋅b=∣a∣∣b∣cosθ , 通过公式可以很容易得出结论:
- 两个向量夹角越小(越接近),其内积越大,相关性越高。
- 两个向量夹角越大,相关性越差,如果夹角为90°,两向量垂直,内积为0,无相关性。
故可考虑使用向量内积来计算
a
1
a^1
a1与
a
2
a^2
a2之间的相关性,即
α
1
,
2
=
a
1
⋅
a
2
α_{1,2}=a^1⋅a^2
α1,2=a1⋅a2。但此时两个saw向量的夹角为0,相关性最高,但实际上二者语义、词性完全不同。为此,Self-Attention提出了矩阵
W
q
W^q
Wq和矩阵
W
k
W^k
Wk:
- W q W^q Wq负责对“主角”进行线性变化,将其变换为 q q q,称为 q u e r y query query。
- W k W^k Wk负责对“配角”进行线性变化,将其变换为 k k k,称为 k e y key key。
之后即可计算相关分数
α
1
,
2
α_{1,2}
α1,2:

从而得到总体的计算流程:

就可计算得到
a
1
a^1
a1与其他向量的相关程度分数
α
1
,
1
,
α
1
,
2
,
α
1
,
3
,
α
1
,
4
α_{1,1},α_{1,2},α_{1,3},α_{1,4}
α1,1,α1,2,α1,3,α1,4。
【2.归一化相关程度分数】
得到相关程度分数
α
1
,
1
,
α
1
,
2
,
α
1
,
3
,
α
1
,
4
α_{1,1},α_{1,2},α_{1,3},α_{1,4}
α1,1,α1,2,α1,3,α1,4后还需进行归一化处理:

【3.生成包含上下文信息的新向量】
若直接将
a
a
a与
α
′
α'
α′进行加权求和,泛化性不够好,所以需要对
a
a
a进行线性变换(通过矩阵
W
v
W^v
Wv实现)得到向量
v
v
v,
W
v
W^v
Wv也是可训练参数。

加权求和得到
b
b
b:

所有上下文向量
b
i
b^i
bi的计算:

【4.总结】
总之,Attention的工作在于,对于输入向量序列
I
=
(
a
1
,
a
2
,
a
3
,
a
4
)
I=(a^1,a^2,a^3,a^4)
I=(a1,a2,a3,a4),Attention机制可将其转化为另一组包含了上下文信息的向量序列
O
=
(
b
1
,
b
2
,
b
3
,
b
4
)
O=(b^1,b^2,b^3,b^4)
O=(b1,b2,b3,b4)。流程如下:
- 1.求出查询向量 q i q^i qi: q i = W q ⋅ a i q^i=W^q⋅a^i qi=Wq⋅ai。
- 2.求出键向量 k j k^j kj: q j = W k ⋅ a j q^j=W^k⋅a^j qj=Wk⋅aj。
- 3.求出相关度分数 ( α i , 1 , α i , 2 , . . . , α i , n ) (α_{i,1},α_{i,2},...,α_{i,n}) (αi,1,αi,2,...,αi,n)并归一化得到 α ′ i , 1 , α ′ i , 2 , . . . , α ′ i , n α{'}_{i,1},α{'}_{i,2},...,α{'}_{i,n} α′i,1,α′i,2,...,α′i,n: α ′ i , 1 , α ′ i , 2 , . . . , α ′ i , n = S o f t m a x ( α i , 1 , α i , 2 , . . . , α i , n ) α{'}_{i,1},α{'}_{i,2},...,α{'}_{i,n}=Softmax(α_{i,1},α_{i,2},...,α_{i,n}) α′i,1,α′i,2,...,α′i,n=Softmax(αi,1,αi,2,...,αi,n)。
- 4.求出包含上下文信息的向量 b i b^i bi: b i = ∑ j α i , i ′ ⋅ v j b^i=\sum_{j}α'_{i,i}⋅v^j bi=∑jαi,i′⋅vj
其中, W q , W k , W v W^q,W^k,W^v Wq,Wk,Wv都是可训练参数。
3.1CNN与RNN的局限
Transformer是一种Sequence to Sequence (Seq2Seq) 模型,特别之处在于它大量用到了 注意力机制。
【RNN处理Sequence】
RNN善于处理Sequence数据,常见的有Single-Directional RNN(单向循环神经网络)、Bi-directional RNN(双向循环神经网络)。向RNN模型中输入一串Sequence数据,其会输出另一串Sequence数据:

- Single-Directional RNN(单向循环神经网络):输出 b 3 b^3 b3时,需要已获取过 a 1 − a 3 a^1-a^3 a1−a3的数据。
- Bi-directional RNN(双向循环神经网络):输出 b i , i ∈ { 1 , 2 , 3 , 4 } b^i,i∈\{1,2,3,4\} bi,i∈{1,2,3,4}时,需要已获取过 a 1 − a 4 a^1-a^4 a1−a4的数据。
RNN的问题在于很难平行化(Hard to parallel)。例如,对于Single-Directional RNN(单向循环神经网络)要输出 b 3 b^3 b3时,就必须已获取过 a 1 − a 3 a^1-a^3 a1−a3的数据,这样有先后的串行步骤使得RNN的运算难以平行化,也使得RNN 和 LSTM 等网络难以充分发挥GPU的加速优势。
【CNN处理Sequence】
使用一组CNN代替RNN处理Sequence数据(使用橘色三角形表示一个卷积核大小为3x3的卷积层,卷积结果使用橘色圆点表示):

虽然能做到输入一个Sequence后输出另一个Sequence,但问题在于CNN的感受野有限,上图中每一个CNN最多考虑三个数据的内容,而不像RNN一样可以考虑所有的数据。事实上,可通过叠加CNN、扩大感受野来解决长距离依赖(Long Term Dependecy)问题:

例如,第二层的CNN可以第一层CNN的输出作为输入,此时就能考虑到6个输入数据的内容(而非9个)。也就是说,只要堆叠足够多层的CNN,就能看到相当长时期/长距离的信息。除此之外,也可使用膨胀卷积/空洞卷积等方式扩大感受野。
此外,CNN计算可并行化(同一层卷积核的运算可同时运行)的特点使其能利用GPU加速模型的训练。
【Self-Attention Layer】
Self-Attention提出的目的在于取代RNN,结构如下:

Self-Attention Layer的输入、输出和RNN模型相同(均为Sequence),且每一个输出
b
i
b^i
bi均考虑了整个输入Sequence。其优势在于每一个
b
i
b^i
bi的计算都可通过并行化计算得到。
3.2Self-Attention
3.2.1Self-Attention原理
Attention is all you need的含义为,不需要CNN、RNN,只要Attention机制即可。假设输入Sequence长度为2,包含数据
x
1
、
x
2
x_1、x_2
x1、x2:

- 1.通过Input Embedding运算(图中用映射 f ( x ) f(x) f(x)表示)映射为 a 1 、 a 2 a_1、a_2 a1、a2。
- 2.将
a
1
、
a
2
a_1、a_2
a1、a2分别与变换矩阵
W
q
、
W
k
、
W
v
W_q、W_k、W_v
Wq、Wk、Wv运算得到对应的
q
i
、
k
i
、
v
i
q^i、k^i、v^i
qi、ki、vi,其中,矩阵
W
q
、
W
k
、
W
v
W_q、W_k、W_v
Wq、Wk、Wv是共享、可训练的参数。
- q q q:query,用于匹配key。
- k k k:key,用于被query匹配。
- v v v:value,表从 a i a^i ai中提取的信息。
- q q q与 k k k的计算过程可理解为计算两者的相关性,相关性越大对应 v v v的权重也就越大。
假设
a
1
=
(
1
,
1
)
,
a
2
=
(
1
,
0
)
,
W
q
=
(
1
1
0
1
)
a_1=(1,1),a_2=(1,0),W_q=\begin{pmatrix} 1 & 1 \\ 0 & 1 \\ \end{pmatrix}
a1=(1,1),a2=(1,0),Wq=(1011),则:

这一运算是可并行化的,得到
(
q
1
q
2
)
=
(
1
2
1
1
)
\begin{pmatrix} q^1 \\ q^2 \end{pmatrix}=\begin{pmatrix} 1 & 2 \\ 1 & 1 \end{pmatrix}
(q1q2)=(1121),即为矩阵
Q
Q
Q。同理可得矩阵
K
=
(
k
1
k
2
)
、
V
=
(
v
1
v
2
)
K=\begin{pmatrix} k^1 \\ k^2 \end{pmatrix}、V=\begin{pmatrix} v^1 \\ v^2 \end{pmatrix}
K=(k1k2)、V=(v1v2)。
- 3.用每一个 q i q^i qi与每一个 k i k^i ki做attention运算(不同attention机制下的运算不同,在Self-attention中使用的是Scaled Dot-Product Attention算法),即,将 q i q^i qi与 k i k^i ki点乘并除以 d \sqrt{d} d得到对应的 α α α分数(相当于代表向量 q i q^i qi与向量 k i k^i ki的匹配程度)。其中, d d d表示 q i q^i qi与 k i k^i ki的向量维度,除法运算是因为维度越高,点乘后的数值越大,会导致通过softmax后梯度变的很小,故使用除法进行放缩。
例如,计算
α
1
,
i
α_{1,i}
α1,i:

使用矩阵进行表示:

将矩阵经过
s
o
f
t
m
a
x
(
)
softmax()
softmax()运算得到
α
^
1
,
1
、
α
^
1
,
2
、
α
^
2
,
1
、
α
^
2
,
2
\hat{α}_{1,1}、\hat{α}_{1,2}、\hat{α}_{2,1}、\hat{α}_{2,2}
α^1,1、α^1,2、α^2,1、α^2,2:

此时即完成了
A
t
t
e
n
t
i
o
n
(
Q
、
K
、
V
)
Attention(Q、K、V)
Attention(Q、K、V)运算中的
s
o
f
t
m
a
x
(
Q
K
T
/
d
k
)
softmax(QK^T/\sqrt{d_k})
softmax(QKT/dk)部分。
- 4.将 v v v与对应的 α α α加权求和,得到最终的输出 S e q u e n c e ( b 1 , b 2 ) Sequence(b_1,b_2) Sequence(b1,b2)。

矩阵表示:

可见,
b
1
b_1
b1与
b
2
b_2
b2的计算同时考虑了
a
1
a_1
a1与
a
2
a_2
a2(可通过设置
α
^
\hat{α}
α^的值是否为0来决定
b
i
b_i
bi是否获取
a
i
a_i
ai的信息)。并且,
b
1
b_1
b1与
b
2
b_2
b2的计算是可并行的。且有:
- 若要考虑局部信息:只需学习出相应的 α ^ i , 1 = 0 \hat{α}_{i,1}=0 α^i,1=0, b 1 b^1 b1就不会带有对应分支的信息。
- 若要考虑全局信息:需要学习出所有的 α ^ i , 1 ≠ 0 \hat{α}_{i,1}≠0 α^i,1=0, b 1 b^1 b1就会带有所有分支的信息。
由计算可知,Self-Attention可达到与RNN相同的效果,除此之外其还能并行地得到输出Sequence中的每个向量:

设:
- I I I:输入Sequence。
- W q 、 W k 、 W v W^q、W^k、W^v Wq、Wk、Wv:query参数矩阵、key参数矩阵、value参数矩阵。
-
Q
、
K
、
V
Q、K、V
Q、K、V:query矩阵、key矩阵、value矩阵。
- Q = I ∗ W q = [ q 1 , q 2 , q 3 , q 4 ] Q=I*W^q=[q^1,q^2,q^3,q^4] Q=I∗Wq=[q1,q2,q3,q4]。
- K = I ∗ W k = [ k 1 , k 2 , k 3 , k 4 ] K=I*W^k=[k^1,k^2,k^3,k^4] K=I∗Wk=[k1,k2,k3,k4]。
- V = I ∗ W v = [ v 1 , v 2 , v 3 , v 4 ] V=I*W^v=[v^1,v^2,v^3,v^4] V=I∗Wv=[v1,v2,v3,v4]。
计算过程如下:

将每个
k
i
k^i
ki的转置与
q
i
q^i
qi作内积运算得到标量
α
^
i
,
j
\hat{α}_{i,j}
α^i,j并对其取
S
o
f
t
m
a
x
Softmax
Softmax运算得到标量
α
^
i
,
j
\hat{α}_{i,j}
α^i,j。之后将
α
^
i
,
j
\hat{α}_{i,j}
α^i,j与对应
v
i
v^i
vi加权求和得到
b
i
b^i
bi。整体过程用矩阵表示为:
A
=
K
T
⋅
Q
A
^
=
S
o
f
t
m
a
x
(
A
)
O
=
V
⋅
A
^
A=K^T·Q \hat{A}=Softmax(A) O=V·\hat{A}
A=KT⋅QA^=Softmax(A)O=V⋅A^

GPU可加速矩阵运算,从而在性能上优于RNN。代码实现:
3.2.2代码实现
例子:

将向量进行堆叠,即为矩阵运算,这也是可并行计算的原理:

以下使用代码实现Self-Attention,公式为:

标记维度:

- n n n:input_num,输入向量的数量,如,一句话包含20个单词,则该值为20。
- d k d_k dk:dimension of K,Q和K矩阵的行维度(超参数,需要自己调,一般和输入向量维度 d d d一致即可),该值决定了线性层的宽度。
- d v d_v dv:dimension of V,V矩阵的行维度,该值为输出向量的维度(超参数,需要自己调,一般取值和输入向量维度 d d d保持一致)。
- d d d:input_vector_dim,输入向量的维度,例如将单词编码为了10维的向量,则该值为10。
其中,矩阵
Q
、
K
、
V
Q、K、V
Q、K、V由矩阵
W
q
、
W
k
、
W
v
W^q、W^k、W^v
Wq、Wk、Wv和输入向量
I
I
I计算而来,Pytorch中一般般使用torch.nn.Linear()来表示需要训练的矩阵参数(此时会被加入自动求导当中,而普通的矩阵并不会)。
class SelfAttention(nn.Module):
def __init__(self, input_vector_dim: int, dim_k=None, dim_v=None):
"""
初始化SelfAttention,包含如下关键参数:
input_vector_dim: 输入向量的维度,对应上述公式中的d,例如你将单词编码为了10维的向量,则该值为10
dim_k: 矩阵W^k和W^q的维度
dim_v: 输出向量的维度,即b的维度,例如,经过Attention后的输出向量b,如果你想让他的维度为15,则该值为15,若不填,则取input_vector_dim
"""
super(SelfAttention, self).__init__()
self.input_vector_dim = input_vector_dim
# 如果 dim_k 和 dim_v 为 None,则取输入向量的维度
if dim_k is None:
dim_k = input_vector_dim
if dim_v is None:
dim_v = input_vector_dim
"""
实际写代码时,常用线性层来表示需要训练的矩阵,方便反向传播和参数更新
"""
self.W_q = nn.Linear(input_vector_dim, dim_k, bias=False)
self.W_k = nn.Linear(input_vector_dim, dim_k, bias=False)
self.W_v = nn.Linear(input_vector_dim, dim_v, bias=False)
# 这个是根号下d_k
self._norm_fact = 1 / np.sqrt(dim_k)
def forward(self, x):
"""
进行前向传播:
x: 输入向量,size为(batch_size, input_num, input_vector_dim)
"""
# 通过W_q, W_k, W_v矩阵计算出,Q,K,V
# Q,K,V矩阵的size为 (batch_size, input_num, output_vector_dim)
Q = self.W_q(x)
K = self.W_k(x)
V = self.W_v(x)
# permute用于变换矩阵的size中对应元素的位置,
# 即,将K的size由(batch_size, input_num, output_vector_dim),变为(batch_size, output_vector_dim,input_num)
# 0,1,2 代表各个元素的下标,即变换前,batch_size所在的位置是0,input_num所在的位置是1
K_T = K.permute(0, 2, 1)
# bmm是batch matrix-matrix product,即对一批矩阵进行矩阵相乘
# bmm详情参见:https://pytorch.org/docs/stable/generated/torch.bmm.html
atten = nn.Softmax(dim=-1)(torch.bmm(Q, K_T) * self._norm_fact)
# 最后再乘以 V
output = torch.bmm(atten, V)
return output
&emps;定义50个向量序列为一批( N = 50 N=50 N=50),输入向量维度为3,一批中包含5个向量。将其输入到Attention层中,编码为5个四维的向量:
model = SelfAttention(3, 5, 4)
model(torch.Tensor(50,5,3)).size()
输出:
torch.Size([50, 5, 4])
3.3Multi-Head Attention
3.3.1Multi-Head Attention原理
- MultiHead的head不管有几个,参数量都是一样的。并不是head多,参数就多。
- 当MultiHead的head为1时,并不等价于Self Attetnion,MultiHead Attention和Self Attention是不一样的东西。
- MultiHead Attention中注意力分数 α α α的计算方式与Self Attention公式相同。
- 除了矩阵 W q , W k , W v W^q, W^k, W^v Wq,Wk,Wv外,还要额外定义矩阵 W o W^o Wo。
Self-Attention中计算
A
t
t
e
n
t
i
o
n
(
Q
,
K
,
V
)
Attention(Q,K,V)
Attention(Q,K,V)的方式为
S
c
a
l
e
d
D
o
t
−
P
r
o
d
u
c
t
A
t
t
e
n
t
i
o
n
Scaled Dot-Product Attention
ScaledDot−ProductAttention:

对于Multi-Head Attention,其大部分逻辑和Self Attention是一致的,但求出Q、K、V矩阵后发生了改变。对于Self-Attention,其求出Q、K、V矩阵后直接运算即可:

而Multi-Head Attention在带入公式前会将Q、K、V矩阵拆分为多个头。此处假设
h
e
a
d
=
4
head=4
head=4:

之后的计算也是将所有的
Q
i
Q_i
Qi分别与
K
i
T
K^T_i
KiT进行运算,并与
V
i
V_i
Vi加权求和,求出包含上下文信息的新向量序列:

但这样拆开来计算的Attention使用Concat进行合并效果并不太好,所以最后需要再采用一个额外的
W
o
W^o
Wo矩阵,对Attention再进行一次线性变换,如图所示:

以下则为李宏毅B站视频中给出的
h
e
a
d
=
2
head=2
head=2的例子:

之后再令
q
i
,
1
q^{i,1}
qi,1与
k
i
,
1
k^{i,1}
ki,1作Attention运算再与
v
i
,
1
v^{i,1}
vi,1相乘、
q
i
,
1
q^{i,1}
qi,1与
k
j
,
1
k^{j,1}
kj,1作Attention运算再与
v
j
,
1
v^{j,1}
vj,1相乘,将二者运算结果求和得到
b
i
,
1
b^{i,1}
bi,1。同理可得
b
i
,
2
b^{i,2}
bi,2:

将
b
i
,
1
b^{i,1}
bi,1与
b
i
,
2
b^{i,2}
bi,2Concat,再通过一个Transformation矩阵调整维度从而得到
b
i
b^i
bi

多头注意力机制在论文中表示为:

其中
S
c
a
l
e
d
D
o
t
−
P
r
o
d
u
c
t
A
t
t
e
n
t
i
o
n
Scaled Dot-Product Attention
ScaledDot−ProductAttention即为Self-Attention的运算。
3.3.2代码实现
论文中给出的Multi-head attention机制的优势在于,允许模型共同关注来自不同位置、表示不同子空间的信息。
def attention(query, key, value):
"""
计算Attention的结果。
这里其实传入的是Q,K,V,而Q,K,V的计算是放在模型中的,请参考后续的MultiHeadedAttention类。
这里的Q,K,V有两种Shape,如果是Self-Attention,Shape为(batch, 词数, d_model),
例如(1, 7, 128),即batch_size为1,一句7个单词,每个单词128维
但如果是Multi-Head Attention,则Shape为(batch, head数, 词数,d_model/head数),
例如(1, 8, 7, 16),即Batch_size为1,8个head,一句7个单词,128/8=16。
这样其实也能看出来,所谓的MultiHead其实就是将128拆开了。
在Transformer中,由于使用的是MultiHead Attention,所以Q,K,V的Shape只会是第二种。
"""
# 获取d_model的值。之所以这样可以获取,是因为query和输入的shape相同,
# 若为Self-Attention,则最后一维都是词向量的维度,也就是d_model的值。
# 若为MultiHead Attention,则最后一维是 d_model / h,h为head数
d_k = query.size(-1)
# 执行QK^T / √d_k
scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
# 执行公式中的Softmax
# 这里的p_attn是一个方阵
# 若是Self Attention,则shape为(batch, 词数, 次数),例如(1, 7, 7)
# 若是MultiHead Attention,则shape为(batch, head数, 词数,词数)
p_attn = scores.softmax(dim=-1)
# 最后再乘以 V。
# 对于Self Attention来说,结果Shape为(batch, 词数, d_model),这也就是最终的结果了。
# 但对于MultiHead Attention来说,结果Shape为(batch, head数, 词数,d_model/head数)
# 而这不是最终结果,后续还要将head合并,变为(batch, 词数, d_model)。不过这是MultiHeadAttention
# 该做的事情。
return torch.matmul(p_attn, value)
class MultiHeadedAttention(nn.Module):
def __init__(self, h, d_model):
"""
h: head的数量
"""
super(MultiHeadedAttention, self).__init__()
assert d_model % h == 0
# We assume d_v always equals d_k
self.d_k = d_model // h
self.h = h
# 定义W^q, W^k, W^v和W^o矩阵。
# 如果你不知道为什么用nn.Linear定义矩阵,可以参考该文章:
# https://blog.csdn.net/zhaohongfei_358/article/details/122797190
self.linears = [
nn.Linear(d_model, d_model),
nn.Linear(d_model, d_model),
nn.Linear(d_model, d_model),
nn.Linear(d_model, d_model),
]
def forward(self, x):
# 获取Batch Size
nbatches = x.size(0)
"""
1. 求出Q, K, V,这里是求MultiHead的Q,K,V,所以Shape为(batch, head数, 词数,d_model/head数)
1.1 首先,通过定义的W^q,W^k,W^v求出SelfAttention的Q,K,V,此时Q,K,V的Shape为(batch, 词数, d_model)
对应代码为 `linear(x)`
1.2 分成多头,即将Shape由(batch, 词数, d_model)变为(batch, 词数, head数,d_model/head数)。
对应代码为 `view(nbatches, -1, self.h, self.d_k)`
1.3 最终交换“词数”和“head数”这两个维度,将head数放在前面,最终shape变为(batch, head数, 词数,d_model/head数)。
对应代码为 `transpose(1, 2)`
"""
query, key, value = [
linear(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2)
for linear, x in zip(self.linears, (x, x, x))
]
"""
2. 求出Q,K,V后,通过attention函数计算出Attention结果,
这里x的shape为(batch, head数, 词数,d_model/head数)
self.attn的shape为(batch, head数, 词数,词数)
"""
x = attention(
query, key, value
)
"""
3. 将多个head再合并起来,即将x的shape由(batch, head数, 词数,d_model/head数)
再变为 (batch, 词数,d_model)
3.1 首先,交换“head数”和“词数”,这两个维度,结果为(batch, 词数, head数, d_model/head数)
对应代码为:`x.transpose(1, 2).contiguous()`
3.2 然后将“head数”和“d_model/head数”这两个维度合并,结果为(batch, 词数,d_model)
"""
x = (
x.transpose(1, 2)
.contiguous()
.view(nbatches, -1, self.h * self.d_k)
)
# 最终通过W^o矩阵再执行一次线性变换,得到最终结果。
return self.linears[-1](x)
测试:
# 定义8个head,词向量维度为512
model = MultiHeadedAttention(8, 512)
# 传入一个batch_size为2, 7个单词,每个单词为512维度
x = torch.rand(2, 7, 512)
# 输出Attention后的结果
print(model(x).size())
得到:
torch.Size([2, 7, 512])
3.4Positional Encoding
从上文Self-Attention、Multi-Head Attention的计算可见,输入序列Sequence中嵌入向量的顺序并不重要,即在计算中没有考虑到位置信息。即使将输入序列
(
a
1
,
a
2
,
a
3
)
(a_1,a_2,a_3)
(a1,a2,a3)改为
(
a
1
,
a
3
,
a
2
)
(a_1,a_3,a_2)
(a1,a3,a2),仍是继续做Attention运算、求加权和,对输出Sequence中的
b
1
b_1
b1并不会产生影响(
b
2
、
b
3
b_2、b_3
b2、b3则交换位置)。
使用代码模拟这一问题(Head=1的MultiheadAttention即为Self-Attention):
import torch
import torch.nn as nn
m = nn.MultiheadAttention(embed_dim=2, num_heads=1)
t1 = [[[1., 2.], #a1
[2., 3.], #a2
[3., 4.]]] #a3
t2 = [[[1., 2.], #a1
[3., 4.], #a3
[2., 3.]]] #a2
#交换a2、a3的顺序相当于交换了矩阵中(q2,k2,v2)与(q3,k3,v3)的位置,这对b1并无影响
q, k, v = torch.as_tensor(t1), torch.as_tensor(t1), torch.as_tensor(t1)
print("result1: \n", m(q, k, v))
q, k, v = torch.as_tensor(t2), torch.as_tensor(t2), torch.as_tensor(t2)
print("result2: \n", m(q, k, v))
为使得模型可学习输入序列中嵌入向量的位置信息,论文中引入了位置编码Positional Encoding。在将输入序列元素
x
i
x^i
xi映射为嵌入向量
a
i
a^i
ai后还应再加上位置编码
e
i
e^i
ei(同为向量,形状同
a
i
a^i
ai,代表了嵌入向量的位置信息):

在论文中,
e
i
e^i
ei有两种计算方式:
- 1.固定的位置编码,由
sine and cosine functions计算得到,并非可训练参数。 - 2.非固定的位置编码,可通过训练进行优化。
在消融实验中二者性能差不多,而在ViT中使用的是非固定的、可训练的位置编码。
对于为何将嵌入向量
a
i
a^i
ai与位置编码
e
i
e^i
ei直接相加而不做
c
o
n
c
a
t
concat
concat操作,有如下解释:假设使用one-hot向量
p
i
p^i
pi表示输入数据
x
i
x^i
xi的位置信息:

其中,
p
i
p^i
pi是第
i
i
i维为1,其他维是0的列向量。若直接将
x
i
x^i
xi与
p
i
p^i
pi拼接并乘以矩阵
W
W
W(将
x
i
x^i
xiEmbedding为嵌入向量),事实上可将矩阵
W
W
W看作是矩阵
W
I
W^I
WI与矩阵
W
P
W^P
WP的拼接,该乘法运算可化为:

则
x
i
x^i
xiEmbedding为嵌入向量,而
W
P
W^P
WP与向量
p
i
p^i
pi相乘得到位置编码
e
i
e^i
ei,而
W
I
W^I
WI与向量
x
i
x^i
xi相乘得到嵌入向量
a
i
a^i
ai。可见,位置编码
e
i
e^i
ei与输入序列中的嵌入向量
a
i
a^i
ai直接相加的结果等同于,输入序列向量
x
i
x^i
xi直接拼接表示位置的独热位置编码
p
i
p^i
pi再做线性映射得到嵌入向量。
矩阵
W
P
W^P
WP实际是手工设计的:

总结:在Transformer模型中除了需要使用嵌入向量来表示输入序列的内容供模型学习,还需要使用位置编码来表示嵌入向量在序列中的位置信息。
3.5Maked Attention
Transformer中的Decoder中有一个Masked MultiHead Attentionm,若用一句话来概况加上mask掩码的作用,就是:防止网络看到不该看到的内容。Attention公式的矩阵表示如下:

若
(
v
1
,
v
2
,
.
.
.
,
v
n
)
(v_1,v_2,...,v_n)
(v1,v2,...,vn)对应
(
机
,
器
,
学
,
习
,
真
,
好
,
玩
)
(机,器,学,习,真,好,玩)
(机,器,学,习,真,好,玩),则
(
o
1
,
o
2
,
.
.
.
,
o
n
)
(o_1,o_2,...,o_n)
(o1,o2,...,on)对应
(
机
′
,
器
′
,
学
′
,
习
′
,
真
′
,
好
′
,
玩
′
)
(机',器',学',习',真',好',玩')
(机′,器′,学′,习′,真′,好′,玩′)。其中,
机
′
机'
机′包含
v
1
v_1
v1到
v
n
v_n
vn的所有注意力信息。而计算
机
′
机'
机′时,
(
机
,
器
,
学
,
习
,
真
,
好
,
玩
)
(机,器,学,习,真,好,玩)
(机,器,学,习,真,好,玩)这些字对应的注意力分数即为注意力权重矩阵
A
′
A'
A′的第一行
(
α
1
,
1
′
,
α
2
,
1
′
,
α
3
,
1
′
,
.
.
.
)
(α'_{1,1},α'_{2,1},α'_{3,1},...)
(α1,1′,α2,1′,α3,1′,...)。
假设输入"Machine learning is fun"让Transomer模型进行翻译,首先会将这句话输入到编码器模块中,其会输出一个名为Memory的Tensor:

之后我们会将该Memory作为Decoder的一个输入,使用Decoder预测。Decoder并不是一下子就能把“机器学习真好玩”说出来,而是一个字一个字输出,如图所示:

之后继续调用解码器,这次传入的是Memory和解码器上一时间步的输出"机",此时解码器输出"器":

依次类推,直到解码器最后输出<eos>结束翻译任务:

可见,对于Decoder来说是一个字一个字预测的,所以假设我们Decoder的输入是“机器学习”时,“习”字只能看到前面的“机器学”三个字,所以此时对于“习”字只有“机器学习”四个字的注意力信息。但是,例如最后一步传的是“<bos>机器学习真好玩”,还是不能让“习”字看到后面“真好玩”三个字,所以要使用mask将其盖住,这又是为什么呢?原因是:如果让“习”看到了后面的字,那么“习”字的编码就会发生变化。但事实上,“习"字的输出不应由后面的内容决定,其只应取决于"机器学”。从编码的角度进行分析:
一开始我们只传入了“机”(忽略bos),此时使用attention机制,将“机”字编码为了
[
0.13
,
0.73
,
.
.
.
]
[ 0.13 , 0.73 , . . . ]
[0.13,0.73,...]。
第二次,我们传入了“机器”,此时使用attention机制,如果我们不将“器”字盖住的话,那“机”字的编码就会发生变化,它就不再是是
[
0.13
,
0.73
,
.
.
.
]
[ 0.13 , 0.73 , . . . ]
[0.13,0.73,...]了,也许就变成了
[
0.95
,
0.81
,
.
.
.
]
[ 0.95 , 0.81 , . . . ]
[0.95,0.81,...]。
这就会导致第一次“机”字的编码是
[
0.13
,
0.73
,
.
.
.
]
[0.13,0.73,...]
[0.13,0.73,...],第二次却变成了
[
0.95
,
0.81
,
.
.
.
]
[0.95,0.81,...]
[0.95,0.81,...],这样就可能会让网络有问题。所以我们为了不让“机”字的编码产生变化,所以我们要使用mask,掩盖住“机”字后面的字,也就是即使他能attention后面的字,也不让他attention。
【掩码的实现】
可通过修改
A
n
×
n
′
A'_{n×n}
An×n′的值来实现掩码。假设一开始只有变量
v
1
v_1
v1,则输出向量:

当有变量
v
1
,
v
2
v_1,v_2
v1,v2时,则有输出向量:

若不对
A
2
×
2
′
A'_{2×2}
A2×2′进行掩码操作,则输出序列中
o
1
o_1
o1的值就会发生改变(只有
v
1
v_1
v1时,
o
1
=
α
1
,
1
′
∗
v
1
o_1=α'_{1,1}*v_1
o1=α1,1′∗v1;有
v
1
,
v
2
v_1,v_2
v1,v2时,
o
1
=
α
1
,
1
′
∗
v
1
+
α
2
,
1
′
∗
v
2
o_1=α'_{1,1}*v_1+α'_{2,1}*v_2
o1=α1,1′∗v1+α2,1′∗v2)。此时就需要对
α
2
,
1
′
α'_{2,1}
α2,1′实施掩码操作,保证
o
1
o_1
o1不会被
v
2
v_2
v2所影响:

以此类推,计算
o
n
o_n
on时有:

而在源码当中,mask掩码使用的是负无穷
1
e
−
9
1e-9
1e−9:
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
p_attn = scores.softmax(dim=-1)
这是因为源码是在softmax之前进行掩码,所以才是负无穷,因为将负无穷softmax后就会变成0了。
3.6Self-Attention的应用
一般的Sequence to Sequence模型会使用RNN在Encoder、Decode中,其中,Encoder可将
(
x
1
,
x
2
,
.
.
.
,
x
4
)
(x^1,x^2,...,x^4)
(x1,x2,...,x4)转换为
(
h
1
,
h
2
,
h
3
,
h
4
)
(h^1,h^2,h^3,h^4)
(h1,h2,h3,h4),可用Self-Attention进行替代:

而Decode中的RNN将
(
c
1
,
c
2
,
c
3
)
(c^1,c^2,c^3)
(c1,c2,c3)转换为输出序列
(
o
1
,
o
2
,
o
3
)
(o^1,o^2,o^3)
(o1,o2,o3),这也可使用Self-Attention实现:

三、Transformer
下图是一个最基本的Transformer结构(
N
=
6
N=6
N=6):

左侧是编码器模块(Encoder Block,用于处理输入序列),右侧是解码器模块(Decoder Block,用于生成输出序列),这使得Transformer适用于处理Seq to Seq任务,如文本翻译。其中,编码器中包含一个多头注意力机制(每个头可学习不同的注意权重,更好捕捉不同类型的关系),解码器中包含两个多头注意力机制,多头注意力机制上方的Add & Norm层:
Add:表示残差连接,用于防止网络退化。Norm:表示 Layer Normalization(层归一化),用于将每一层的激活值归一化。
而 F e e d F o r w a r d Feed Forward FeedForward表示前馈神经网络(全连接神经网络)。
3.1Encoder Block

输入序列通过Input Embedding变为嵌入向量序列并加上位置编码(Positional Encoding)后,进入Multi-Head Attention:

- 1.Multi-Head Attention由多层Self-Attention组成,其完成Seq2Seq任务,将输入序列 ( a 1 , a 2 , a 3 , a 4 ) (a_1,a_2,a_3,a_4) (a1,a2,a3,a4)转换为序列 ( b 1 , b 2 , b 3 , b 4 ) (b_1,b_2,b_3,b_4) (b1,b2,b3,b4)。

- 2.进入Add & Norm层。Add表示残差连接,其将Multi-Head Attention的输入序列 ( a 1 , a 2 , a 3 , a 4 ) (a_1,a_2,a_3,a_4) (a1,a2,a3,a4)与输出序列 ( b 1 , b 2 , b 3 , b 4 ) (b_1,b_2,b_3,b_4) (b1,b2,b3,b4)相加,之后输入Layer Norm进行规范化(Layer Norm一般应用在RNN当中,Transformer正式类似于RNN的模型)(Layer Norm比Batch Norm更符合对文本处理的直觉)。
补充知识:Batch Normalization与Layer Normalization
Normalization:规范化或标准化,就是把输入数据X在输送给神经元之前先对其进行平移和伸缩变换,将X的分布规范化成在固定区间范围的标准分布。这是因为神经网络的Block大部分都是矩阵运算,一个向量经过矩阵运算后值会越来越大,为了网络的稳定性,我们需要及时把值拉回正态分布。
- Batch Normalization:对一个Batch Size样本内的每个特征分别做归一化。
- Layer Normalization:分别对每个样本的所有特征做归一化。
【1.Batch Normalization】
假设想根据下图的batch数据中的三种特征(身高、体重、年龄)数据进行预测性别,进行Batch Normalization归一化处理时会对每一列特征进行归一化,如下图求一列身高的平均值。

Batch Normalization会将数据转为均值为0,方差为1的正态分布,使得数据分布一致,并且避免梯度消失。在模型训练中,设输入数据维度为
[
N
,
H
,
W
,
C
]
[N,H,W,C]
[N,H,W,C],将
H
H
H与
W
W
W进行合并得到输入数据的矩形表示形式:

Batch Norm会在通道维度进行归一化,得到C个统计量u,δ(均值、标准差)。即,在C的每个维度上对[N, H, W]计算其均值、方差,用于该维度上的归一化操作。
通俗地说,假设有N本书,每本书有C页,每页可容纳HxW个字符,Batch Norm就是页为单位:假设每本书都为C页,首先计算N本书中第1页的字符【N, H, W】均值方差,得到统计量
u
1
、
δ
1
u_1、δ_1
u1、δ1,然后对N本书的第一页利用该统计量对第一页的元素进行归一化操作,剩下的C-1页同理。
【2.Layer Normalization】
Batch Normalization以Batch为单位计算统计量,而Layer Normalization以样本为单位计算统计量,因此最后会得到N个
u
,
δ
u,δ
u,δ。即,在N的每个维度上对[H,W,C]计算其均值、方差,用于该维度上的归一化操作。
通俗地说,假设有N本书,每本书有C页,每页可容纳HxW个字符,Layer Norm就是以本为单位:首先计算第一本书中的所有字符【H, W, C】均值方差,得到统计量
u
1
,
δ
1
u_1,δ_1
u1,δ1,然后利用该统计量对第一本数进行归一化操作,剩下的N-1本书同理。
- 3.将ADD & Norm的结果输入到Feed Forward(全连接神经网络)处理,并再进行一次ADD & Norm操作。
3.2Decoder Block

解码器模块(Decoder Block)的输入包括两部分:
- 编码器模块的输出。
- 上一个时间步解码器模块的预测结果加上位置编码,再作为当前时间步解码器模块的输入。
以将"我爱中国"翻译为英文为例,先将其转换为嵌入向量序列并输入到编码器模块。之后编码器的执行步骤为:
- 步骤一:
- 来自上一时间步解码器的输出:上一时间步解码器并未输出,故此时输入起始符
<s>+位置编码。 - 编码器的输出:(我爱中国)Encoder Embedding
- 最终输出:预测单词"I"。
- 来自上一时间步解码器的输出:上一时间步解码器并未输出,故此时输入起始符
- 步骤二:
- 来自上一时间步解码器的输出:起始符
<s>+“I”+位置编码。 - 编码器的输出:(我爱中国)Encoder Embedding
- 最终输出:预测单词"Love"。
- 来自上一时间步解码器的输出:起始符
- 步骤三:
- 来自上一时间步解码器的输出:起始符
<s>+“I”+“Love”+位置编码。 - 编码器的输出:(我爱中国)Encoder Embedding
- 最终输出:预测单词"China"。
- 来自上一时间步解码器的输出:起始符

Decoder Block模块中增加了Masked Multi-Head Self-attention机制,使得注意力只关注已产生的 Sequence 而不含未产生的部分(这样解码器第
i
i
i位的预测不会受到原编码器输出中,
i
+
1
、
i
+
2
i+1、i+2
i+1、i+2等位置的信息影响)。此外,从解码器的执行流程可见,编码器是可并行计算的(能一次性全部Encoding出来),而解码器类似于RNN,需要一步一步Decoding出预测结果(因为需要上一时间步解码器的输出作为当前时间步解码器的输入)。
3.3Output Block

将解码器模块的输出经过一次线性变换(全连接神经网络),再使用softmax得到输出的概率分布,最后通过词典输出概率最大的对应的单词作为模型的预测输出。
3.4代码实现
【Scaled Dot-Product Attention运算的实现】
class ScaledDotProductAttention(nn.Module):
''' Scaled Dot-Product Attention '''
def __init__(self, temperature, attn_dropout=0.1):
super().__init__()
self.temperature = temperature
self.dropout = nn.Dropout(attn_dropout)
def forward(self, q, k, v, mask=None):
#计算Q·K^T
attn = torch.matmul(q / self.temperature, k.transpose(2, 3))
#判断是否需要加上掩码
if mask is not None:
attn = attn.masked_fill(mask == 0, -1e9) # Mask
#计算Softmax(QK^T) + Dropout
attn = self.dropout(F.softmax(attn, dim=-1))
#得到A(Q,K,V)
output = torch.matmul(attn, v)
#返回向量序列、attention矩阵
return output, attn
【位置编码的代码实现】

class PositionalEncoding(nn.Module):
def __init__(self, d_hid, n_position=200):
super(PositionalEncoding, self).__init__()
# Not a parameter
self.register_buffer('pos_table', self._get_sinusoid_encoding_table(n_position, d_hid))
def _get_sinusoid_encoding_table(self, n_position, d_hid):
''' Sinusoid position encoding table '''
# TODO: make it with torch instead of numpy
def get_position_angle_vec(position):
return [position / np.power(10000, 2 * (hid_j // 2) / d_hid) for hid_j in range(d_hid)]
sinusoid_table = np.array([get_position_angle_vec(pos_i) for pos_i in range(n_position)])
sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2]) # dim 2i
sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2]) # dim 2i+1
return torch.FloatTensor(sinusoid_table).unsqueeze(0) # (1,N,d)
def forward(self, x):
# x(B, N, d)
return x + self.pos_table[:, :x.size(1)].clone().detach()
【多头注意力机制代码实现】
class MultiHeadAttention(nn.Module):
''' Multi-Head Attention module '''
def __init__(self, n_head, d_model, d_k, d_v, dropout=0.1):
super().__init__()
self.n_head = n_head
self.d_k = d_k
self.d_v = d_v
self.w_qs = nn.Linear(d_model, n_head * d_k, bias=False)
self.w_ks = nn.Linear(d_model, n_head * d_k, bias=False)
self.w_vs = nn.Linear(d_model, n_head * d_v, bias=False)
self.fc = nn.Linear(n_head * d_v, d_model, bias=False)
self.attention = ScaledDotProductAttention(temperature=d_k ** 0.5)
self.dropout = nn.Dropout(dropout)
self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
def forward(self, q, k, v, mask=None):
d_k, d_v, n_head = self.d_k, self.d_v, self.n_head
sz_b, len_q, len_k, len_v = q.size(0), q.size(1), k.size(1), v.size(1)
residual = q
# Pass through the pre-attention projection: b x lq x (n*dv)
# Separate different heads: b x lq x n x dv
q = self.w_qs(q).view(sz_b, len_q, n_head, d_k)
k = self.w_ks(k).view(sz_b, len_k, n_head, d_k)
v = self.w_vs(v).view(sz_b, len_v, n_head, d_v)
# Transpose for attention dot product: b x n x lq x dv
q, k, v = q.transpose(1, 2), k.transpose(1, 2), v.transpose(1, 2)
if mask is not None:
mask = mask.unsqueeze(1) # For head axis broadcasting.
q, attn = self.attention(q, k, v, mask=mask)
#q (sz_b,n_head,N=len_q,d_k)
#k (sz_b,n_head,N=len_k,d_k)
#v (sz_b,n_head,N=len_v,d_v)
# Transpose to move the head dimension back: b x lq x n x dv
# Combine the last two dimensions to concatenate all the heads together: b x lq x (n*dv)
q = q.transpose(1, 2).contiguous().view(sz_b, len_q, -1)
#q (sz_b, len_q, n_head, N * d_k)
# 最终的输出矩阵 Z
q = self.dropout(self.fc(q))
# Add & Norm 层
q += residual
q = self.layer_norm(q)
return q, attn
【Feed Forward Network代码实现】
class PositionwiseFeedForward(nn.Module):
''' A two-feed-forward-layer module '''
def __init__(self, d_in, d_hid, dropout=0.1):
super().__init__()
self.w_1 = nn.Linear(d_in, d_hid) # position-wise
self.w_2 = nn.Linear(d_hid, d_in) # position-wise
self.layer_norm = nn.LayerNorm(d_in, eps=1e-6)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
residual = x
# 两层 FCs + Dropout
x = self.w_2(F.relu(self.w_1(x)))
x = self.dropout(x)
# Add & Norm 层
x += residual
x = self.layer_norm(x)
return x
【Encoder Block】
class EncoderLayer(nn.Module):
''' Compose with two layers '''
def __init__(self, d_model, d_inner, n_head, d_k, d_v, dropout=0.1):
super(EncoderLayer, self).__init__()
# MHA + Add & Norm
self.slf_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
# FFN + Add & Norm
self.pos_ffn = PositionwiseFeedForward(d_model, d_inner, dropout=dropout)
def forward(self, enc_input, slf_attn_mask=None):
enc_output, enc_slf_attn = self.slf_attn(enc_input, enc_input, enc_input, mask=slf_attn_mask)
enc_output = self.pos_ffn(enc_output)
return enc_output, enc_slf_attn
【Decoder Block】
class DecoderLayer(nn.Module):
''' Compose with three layers '''
def __init__(self, d_model, d_inner, n_head, d_k, d_v, dropout=0.1):
super(DecoderLayer, self).__init__()
self.slf_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
self.enc_attn = MultiHeadAttention(n_head, d_model, d_k, d_v, dropout=dropout)
self.pos_ffn = PositionwiseFeedForward(d_model, d_inner, dropout=dropout)
def forward(self, dec_input, enc_output, slf_attn_mask=None, dec_enc_attn_mask=None):
# MMHA + Add & Norm
dec_output, dec_slf_attn = self.slf_attn(dec_input, dec_input, dec_input, mask=slf_attn_mask)
# MHA + Add & Norm
dec_output, dec_enc_attn = self.enc_attn(dec_output, enc_output, enc_output, mask=dec_enc_attn_mask)
# FFN + Add & Norm
dec_output = self.pos_ffn(dec_output)
return dec_output, dec_slf_attn, dec_enc_attn
【编码器】
class Encoder(nn.Module):
''' A encoder model with self attention mechanism. '''
def __init__(
self, n_src_vocab, d_word_vec, n_layers, n_head, d_k, d_v,
d_model, d_inner, pad_idx, dropout=0.1, n_position=200):
super().__init__()
self.src_word_emb = nn.Embedding(n_src_vocab, d_word_vec, padding_idx=pad_idx)
self.position_enc = PositionalEncoding(d_word_vec, n_position=n_position)
self.dropout = nn.Dropout(p=dropout)
self.layer_stack = nn.ModuleList([
EncoderLayer(d_model, d_inner, n_head, d_k, d_v, dropout=dropout)
for _ in range(n_layers)])
self.layer_norm = nn.LayerNorm(d_model, eps=1e-6)
def forward(self, src_seq, src_mask, return_attns=False):
enc_slf_attn_list = []
# -- Forward --
# Input Embedding + Position Embedding + Dropout + Norm
enc_output = self.dropout(self.position_enc(self.src_word_emb(src_seq)))
enc_output = self.layer_norm(enc_output)
# N × Encoder Block
for enc_layer in self.layer_stack:
enc_output, enc_slf_attn = enc_layer(enc_output, slf_attn_mask=src_mask)
enc_slf_attn_list += [enc_slf_attn] if return_attns else []
if return_attns:
return enc_output, enc_slf_attn_list
return enc_output,
【解码器】
class Decoder(nn.Module):
''' A decoder model with self attention mechanism. '''
def forward(self, trg_seq, trg_mask, enc_output, src_mask, return_attns=False):
dec_slf_attn_list, dec_enc_attn_list = [], []
# -- Forward --
dec_output = self.dropout(self.position_enc(self.trg_word_emb(trg_seq)))
dec_output = self.layer_norm(dec_output)
for dec_layer in self.layer_stack:
dec_output, dec_slf_attn, dec_enc_attn = dec_layer(
dec_output, enc_output, slf_attn_mask=trg_mask, dec_enc_attn_mask=src_mask)
dec_slf_attn_list += [dec_slf_attn] if return_attns else []
dec_enc_attn_list += [dec_enc_attn] if return_attns else []
if return_attns:
return dec_output, dec_slf_attn_list, dec_enc_attn_list
return dec_output,
【Transformer】
class Transformer(nn.Module):
''' A sequence to sequence model with attention mechanism. '''
def __init__(
self, n_src_vocab, n_trg_vocab, src_pad_idx, trg_pad_idx,
d_word_vec=512, d_model=512, d_inner=2048,
n_layers=6, n_head=8, d_k=64, d_v=64, dropout=0.1, n_position=200,
trg_emb_prj_weight_sharing=True, emb_src_trg_weight_sharing=True):
super().__init__()
self.src_pad_idx, self.trg_pad_idx = src_pad_idx, trg_pad_idx
self.encoder = Encoder(
n_src_vocab=n_src_vocab, n_position=n_position,
d_word_vec=d_word_vec, d_model=d_model, d_inner=d_inner,
n_layers=n_layers, n_head=n_head, d_k=d_k, d_v=d_v,
pad_idx=src_pad_idx, dropout=dropout)
self.decoder = Decoder(
n_trg_vocab=n_trg_vocab, n_position=n_position,
d_word_vec=d_word_vec, d_model=d_model, d_inner=d_inner,
n_layers=n_layers, n_head=n_head, d_k=d_k, d_v=d_v,
pad_idx=trg_pad_idx, dropout=dropout)
self.trg_word_prj = nn.Linear(d_model, n_trg_vocab, bias=False)
for p in self.parameters():
if p.dim() > 1:
nn.init.xavier_uniform_(p)
assert d_model == d_word_vec, \
'To facilitate the residual connections, \
the dimensions of all module outputs shall be the same.'
self.x_logit_scale = 1.
if trg_emb_prj_weight_sharing:
# Share the weight between target word embedding & last dense layer
self.trg_word_prj.weight = self.decoder.trg_word_emb.weight
self.x_logit_scale = (d_model ** -0.5)
if emb_src_trg_weight_sharing:
self.encoder.src_word_emb.weight = self.decoder.trg_word_emb.weight
def forward(self, src_seq, trg_seq):
# source mask:用于产生 Encoder 的 mask,它是一列 Bool 值,负责把标点 mask 掉
src_mask = get_pad_mask(src_seq, self.src_pad_idx)
# target mask:用于产生 Decoder 的 mask。它是一个矩阵,如图 24 中的 mask 所示,功能已在上文介绍
trg_mask = get_pad_mask(trg_seq, self.trg_pad_idx) & get_subsequent_mask(trg_seq)
enc_output, *_ = self.encoder(src_seq, src_mask)
dec_output, *_ = self.decoder(trg_seq, trg_mask, enc_output, src_mask)
seq_logit = self.trg_word_prj(dec_output) * self.x_logit_scale
return seq_logit.view(-1, seq_logit.size(2))
【生成掩模】
def get_pad_mask(seq, pad_idx):
return (seq != pad_idx).unsqueeze(-2)
def get_subsequent_mask(seq):
''' For masking out the subsequent info. '''
sz_b, len_s = seq.size()
subsequent_mask = (1 - torch.triu(
torch.ones((1, len_s, len_s), device=seq.device), diagonal=1)).bool()
return subsequent_mask



















