Transformer由论文《Attention is All You Need》提出,是一种用于自然语言处理(NLP)和其他序列到序列(sequence-to-sequence)任务的深度学习模型架构,在自然语言处理领域获得了巨大的成功,在这个系列的文章中,我会将模型拆解,并逐一介绍里面的核心概念。
Transformer模型
Transformer结构如图所示,属于encode-decode架构,左边是编码器,右边是解码器。它们都由N个Transformer Block组成。编码器的输出作为解码器多头注意力的Key和Value,Query来自解码器输入经过掩码多头注意力后的结果。
主要涉及以下几个模块:
- 嵌入表示(Input/Output Embedding) 将每个标记(token)转换为对应的向量表示。
- 位置编码(Positional Encoding) 由于没有时序信息,需要额外加入位置编码。
- 多头注意力(Multi-Head Attention) 利用注意力机制对输入进行交互,得到具有上下文信息的表示。根据其所处的位置有不同的变种:邻接解码器嵌入位置是掩码多头注意力,特点是当前位置只能注意本身以及之前位置的信息;掩码多头注意力紧接的多头注意力特点是Key和Value来自编码器的输出,而Query来自底层的输出,目的是在计算输出时考虑输入信息。
- 层归一化(Layer Normalization) 作用于Transformer块内部子层的输出表示上,对表示序列进行层归一化。
- 残差连接(Residual connection) 作用于Transformer块内部子层输入和输出上,对其输入和输出进行求和。
- 位置感知前馈网络(Position-wise Feedforward Network) 通过多层全连接网络对表示序列进行非线性变换,提升模型的表达能力。
下面依次介绍各模块的功能和关键部分的实现。
嵌入表示
对于一段输入的文本序列,一个常识是,我们首先应该把它进行分词并进行Word Embedding工作,这有两点原因:计算机无法直接处理一个单词或者一个汉字,需要把一个token转化成计算机可以识别的向量;Transformer对文本的建模是基于细粒度的。
Embedding(嵌入表示)就是用一个低维稠密的向量表示一个对象,这里的对象指的是句子中的一个token(词)。Embedding向量能够表达对象的语义特征,两个向量之间的距离反映了对象之间的相似性。如果这两个token很像,那么得到的两个向量之间的距离应该很小。
经过Word Embedding层后,我们就拿到了文本初始的token级别的表示,需要注意的是,bert对于英文的分词是以词为单位,对中文的分词则是以汉字为单位。
位置编码
经过Word Embedding后,我们获得了词与词之间关系的表达形式,但是,由于Transformer不是基于类似RNN循环的方式,而是一次为所有的token进行建模,这就导致enbedding中不包含token在句子中的位置信息,因此要将位置信息加进去,结合了这种方式的词嵌入就是Position Embedding了。
为了实现这一点,我们通常容易想到两种方式:(1)通过网络来学习;(2)预定义一个函数,通过函数计算出位置信息。Transformer的作者对以上两种方式都做了探究,发现最终效果相当,于是采用了第2种方式,从而减少模型参数量,同时还能适应即使在训练集中没有出现过的句子长度。公式为:
P
E
(
p
o
s
,
2
i
)
=
s
i
n
(
p
o
s
1000
0
2
i
/
d
)
PE(pos,2i)=sin(\frac{pos}{10000^{2i/d}})
PE(pos,2i)=sin(100002i/dpos)
P
E
(
p
o
s
,
2
i
+
1
)
=
c
o
s
(
p
o
s
1000
0
2
i
/
d
)
PE(pos,2i+1)=cos(\frac{pos}{10000^{2i/d}})
PE(pos,2i+1)=cos(100002i/dpos) 其中pos表示标记所在的位置;i代表维度,即位置编码的每个维度对应一个波长不同的正弦或余弦波,波长从
2
π
2π
2π到
10000
⋅
2
π
10000 ⋅ 2π
10000⋅2π成等比数列;d表示位置编码的最大维度,和词嵌入的维度相同。
为何使用这种方式编码能够代表不同位置信息呢?由公式可知,每一维 i 都对应不同周期的正余弦曲线:
i
=
0
i=0
i=0时是周期为
2
π
2π
2π的
s
i
n
sin
sin函数,
i
=
1
i=1
i=1时是周期为
2
π
2π
2π的
c
o
s
cos
cos函数…对于不同的两个位置
p
o
s
1
pos1
pos1和
p
o
s
2
pos2
pos2,若它们在某一维上有相同的编码值,则说明这两个位置的差值等于该维所在曲线的周期,即
∣
p
o
s
1
−
p
o
s
2
∣
=
T
i
|pos1-pos2|=T_i
∣pos1−pos2∣=Ti。而对于另一个维度
j
(
j
≠
i
)
j(j\neq i)
j(j=i),由于
T
i
≠
T
j
T_i\neq T_j
Ti=Tj,因此
p
o
s
1
pos1
pos1和
p
o
s
2
pos2
pos2在这个维度
j
j
j上的编码值就不会相等,对于其它任意
k
∈
{
1
,
2
,
.
.
.
,
d
−
1
}
;
k
≠
i
k\in \{1,2,...,d-1\};k\neq i
k∈{1,2,...,d−1};k=i也是如此。这种生成位置编码的方式保证了不同位置在所有维度上不会被编码到完全一样的值,从而使每个位置都获得独一无二的编码。Position Embedding的内积表示了位置的相对距离。内积的结果是对称的,所以没有方向信息,参考下图:
最后得到的位置编码需要和标记的词嵌入向量进行相加。pytorch中给了positional_encodings.torch_encodings自动的实现位置编码,我们给一个使用示例。
from positional_encodings.torch_encodings import PositionalEncoding1D
def get_pos_encode(token_fea):
fea_dim = token_fea.shape[2]
p_1d_feature = PositionalEncoding1D(token_fea)
positional_fea = p_1d_image(token_fea).cuda()
return positional_fea
# 生成位置编码,加入到enbedding中
positional_enbedding = get_pos_encode(token_fea)
token_fea = token_fea + positional_enbedding
下一篇,我将介绍Transformer中的多头注意机制。