整体框架:
编码器译码器结构,编码器部分由6个encoder堆叠而成,译码器部分由6个decoder堆叠而成。每个encoder结构都是相同的(但是它们的参数不共享),由多头自注意力层以及全连接层构成。decoder结构与encoder类似,只是比后者多出了一个encoder-decoder attention层,用于获取编码器部分输出的全局(全句)信息。
编码器之前:
Input Embedding: 将输入的tokens初始化为维度固定(transformer设置dim=512)的词向量。
Positional Encoding: 位置编码。因为transformer摒弃了传统RNN类似的顺序输入结构,损失了位置信息,因此需要手动加入位置信息。
给定了一个长为 T T T的文本,首先我们可以考虑最简单的位置编码,就是计数,即使用 P E = p o s = 0 , 1 , 2 , . . . , T − 1 PE=pos=0,1,2,...,T-1 PE=pos=0,1,2,...,T−1作为文本中每个字的位置编码。当然这样的瑕疵非常明显,这个序列是没有上界的。设想一段很长的(比如含有500个字的)文本,最后一个字的位置编码非常大,这是很不合适的:1. 它比第一个字的编码大太多,和字嵌入合并以后难免会出现特征在数值上的倾斜;2. 它比一般的字嵌入的数值要大,难免会抢了字嵌入的「风头」,对模型可能有一定的干扰。
从这里,我们知道位置编码最好具有一定的值域范围,这样就有了第二个版本:使用文本长度对每个位置作归一化,得到 P E = p o s / ( T − 1 ) PE=pos/(T-1) PE=pos/(T−1)。这样固然使得所有位置编码都落入 ( 0 , 1 ) (0,1) (0,1)区间,但是问题也是显著的:不同长度文本的位置编码步长是不同的,在较短的文本中紧紧相邻的两个字的位置编码差异,会和长文本中相邻数个字的两个字的位置编码差异一致。这显然是不合适的,我们关注的位置信息,最核心的就是相对次序关系,尤其是上下文中的次序关系,如果使用这种方法,那么在长文本中相对次序关系会被「稀释」。
最后,我们考虑一种有界的周期性函数:
P E ( p o s ) = s i n ( p o s α ) PE(pos)=sin(\frac{pos}{\alpha }) PE(pos)=sin(αpos)
其中
α
{\alpha }
α用来调节位置编码函数的周期,当
α
{\alpha }
α比较大时,周期比较长,相邻字的位置编码之间的差异比较小。
这样的做法还是有一些简陋,周期函数的引入是为了复用位置编码函数的值域,但是这种
Z
→
[
−
1
,
1
]
Z\rightarrow [-1,1]
Z→[−1,1]的映射,还是太单调:如果
α
{\alpha }
α比较大,相邻字符之间的位置差异体现得不明显;如果
α
{\alpha }
α比较小,在长文本中还是可能会有一些不同位置的字符的编码一样,这是因为
[
1
−
,
1
]
[1-,1]
[1−,1]空间的表现范围有限。此时,我们注意到每一个tokens嵌入的维度dim=512,那么我们自然可以使用一个dim维的向量来表示某个位置的编码——
[
−
1
,
1
]
d
i
m
[-1,1]^{dim}
[−1,1]dim的表示范围要远大于
[
−
1
,
1
]
[-1,1]
[−1,1]。
显然,在不同维度上应该用不同的函数操纵位置编码,这样高维的表示空间才有意义。可以为位置编码的每一维赋予不同的
α
{\alpha }
α;甚至在一些维度将
s
i
n
sin
sin替换为
c
o
s
cos
cos,一种构造方法就是论文中的方法了:
其中
i
=
0
,
1
,
.
.
.
,
(
d
i
m
−
1
)
/
2
i=0,1,...,(dim-1)/2
i=0,1,...,(dim−1)/2,
p
o
s
=
0
,
1
,
.
.
.
,
pos=0,1,...,
pos=0,1,...,max_seq_len-1。这里交替使用
s
i
n
/
c
o
s
sin/cos
sin/cos只是为了使编码更「丰富」,在哪些维度上使用
s
i
n
sin
sin,哪些使用
c
o
s
cos
cos,不是很重要。
自此,该位置编码函数可以唯一地表示各个位置的编码,除此之外,它还实现了相对位置的建模:相隔 k 个词的两个位置 pos 和 pos+k 的位置编码是由 k 的位置编码定义的一个线性变换,推导公式如下:
用到了三角函数的以下性质:
s
i
n
(
α
+
β
)
=
s
i
n
α
c
o
s
β
+
c
o
s
α
s
i
n
β
sin(\alpha +\beta )=sin\alpha cos\beta +cos\alpha sin\beta
sin(α+β)=sinαcosβ+cosαsinβ
c
o
s
(
α
+
β
)
=
c
o
s
α
c
o
s
β
−
s
i
n
α
s
i
n
β
cos(\alpha +\beta )=cos\alpha cos\beta -sin\alpha sin\beta
cos(α+β)=cosαcosβ−sinαsinβ
引用自:如何理解Transformer论文中的positional encoding,和三角函数有什么关系?LinT&小莲子的回答
多头自注意力层(Multi-Head self-Attention):
Transformer使用了一种比较直观的方式来实现自注意机制:每个输入词向量都分别赋予三个向量:Q(Query)、K(Key)、V(Value)。使用每个词向量的Q分别与所有词向量的K做点积,获取该词对输入句子中所有词的评分,评分越高说明两个词之间的关系越大。将所有的评分除以
d
k
\sqrt{d_{k}}
dk(
d
k
d_{k}
dk是向量K的维度)进行缩放后再使用softmax标准化,获得各个词的权重分数。最后对各个词的V向量加权求和,得到当前词的输出,完整过程如下图所示:
需要注意的是,实际输入的句子长度有时会短于模型规定的最大输入长度(max_seq_len),这时我们需要在句子后面补零再输入。显然,这些补零的部分不是我们要关注的对象,因此,在将计算出的
Q
K
T
QK^T
QKT评分输入softmax之前,要将相应补零位置的评分置为
−
∝
-\propto
−∝,这样在softmax之后,相应位置的权重得分即可变为0,实现了将补零位置的信息忽略的目的。
上述的Q、K、V向量如何得到?
每个self-attention层都会有三个矩阵:
W
Q
、
W
K
、
W
V
W^Q、W^K、W^V
WQ、WK、WV,这三个矩阵是可学习的,他们的维度分别为
[
d
i
m
,
d
q
]
、
[
d
i
m
,
d
k
]
、
[
d
i
m
,
d
v
]
[dim,d_q]、[dim,d_k]、[dim,d_v]
[dim,dq]、[dim,dk]、[dim,dv],其中dim即输入词向量的维度,
d
q
=
d
k
d_q=d_k
dq=dk,transformer中
d
v
d_v
dv可以自己设置,因为后面多头拼接后可以再转换到维度dim,以便于实现残差连接,而bert中直接将
d
v
d_v
dv设置为dim/head_num,这样可以将各头得到的输出拼接后直接与输入相加,而无需转换。
充分利用矩阵的性质,可以一次性完成上述提到的所有运算(amazing!):
其中X矩阵每行就是一个词向量,Z矩阵就是经过self-attention层得到的新词向量。
前面提到的多头的意思其实就是使用多组
W
Q
、
W
K
、
W
V
W^Q、W^K、W^V
WQ、WK、WV矩阵分别对输入执行同样的操作,然后再将得到的结果拼接起来。这样,一个完整的Multi-Head self-Attention过程就可以用下图展示出来了:
其中,bert不需要
W
O
W^O
WO矩阵,而transformer需要使用它来进行维度变换:
Add&Normalization:
Add就是残差连接,这里的Normalization使用的是Layer Normalization,而不是Batch Normalization。因为如果我们将一批文本组成一个batch,那么BN的操作方向是,对每句话的第一个词进行操作。但语言文本的复杂性是很高的,任何一个词都有可能放在初始位置,且词序可能并不影响我们对句子的理解。而BN是针对每个位置进行缩放,这不符合NLP的规律。
而LN则是针对一句话进行缩放的,且LN一般用在第三维度,如[batchsize, seq_len, dims]中的dims,一般为词向量的维度,或者是RNN的输出维度等等,这一维度各个特征的量纲应该相同。因此也不会遇到上面因为特征的量纲不同而导致的缩放问题。如下图所示:
引用自:NLP中 batch normalization与 layer normalization(秩法策士)
前馈全连接层(Feed Forward):
一个隐藏层数为1的全连接层,使用ReLU作为激活函数:
编码器部分The End:
依次前馈通过6个结构相同的encoder,得到最终的输出,它的维度为[batchsize,max_seq_len,dim],自此,编码器部分的任务就完成了。
现在是解码时间:
从论文给出的框架图可以看出,decoder和encoder结构相差不大,只是多出了一个encoder-decoder attention层。该层同样有
W
Q
、
W
K
、
W
V
W^Q、W^K、W^V
WQ、WK、WV三个矩阵,但是它们所服务的对象不一样了。注意到我们现在是要关注当前解码单词与编码器各个位置之间的关系,因此用于计算的Q向量应该是解码器的输入的,而K向量和V向量应该是编码器的最终的输出的。
除此之外,decoder的self-attention层也需要做出一点小改变:
在decoder的self-attention层中,我们仅允许每个词关注输入语言序列中的前序位置。所以我们要将计算得到的后续位置的分数score设置为
−
∝
-\propto
−∝(和前面对补零位置的操作一样),以忽略后面还没有预测到的词,防止模型“作弊”。
Linear&SoftMax:
译码器最后面的Linear和sofmax层负责将最后输出的向量转化为目标词。Linear层是一个简单的全连接层负责把编码器的输出向量转化为logits向量,该向量和词汇表长度一致,logits向量中每个元素代表输出该位置对应词汇表中词的分数,再通过sofmax层将分数转化为概率,再取最大概率对应的词为当前的预测词。