Transformer
Motivation
在自然语言处理等处理序列信息的场景中,常用的模型是包含Encoder和Decoder两部分的递归神经网络(RNN),有时还会加入注意力机制(attention mechanism)。但是这种网络天然的序列式或者说串行式处理数据的方法(序列后部的计算结果依赖于序列之前的计算),使得计算非常难以并行,即限制的了GPU的运算,又加大了对内存大小的要求。
如果能完全摒弃递归或者卷积的计算,那么网络的可并行程度会大大增加。而Transformer正是基于这个动机,设计了一种基于注意力机制的序列处理网络结构。这个模型不仅并行度高,大大缩减了模型训练时间,而且其中使用的多头注意力机制(multi-head attention mechanism)还能更好地切合序列模型的应用场景,使模型有能力模拟距离较长的输入间的依赖关系。
Structure
上图显示了Transformer的整体结构,可以看到它遵循了主流的Encoder-Decoder模式,但是这两个部分的具体网络结构却和常见的序列模型不大相同。接下来我会分小块详细阐述我的理解。
Encoder block
Encoder由N个一样的block组成。每个block大致分为两个部分,第一个部分是一个有residual connection的multi-head self-attention module。
输入序列经过embedding和positional encoding后,会被输入Encoder的第一个block。 (后面会提到这两个部分)
之所以叫做multi-head是因为它其实是基于多个并列的参数矩阵的结构,每个矩阵使用点乘的方式来模拟词与词之间不同的关联性。这个设计的初衷是,使用单个的Attention层难以完全捕捉到输入的各个方面的相关性。例如机器翻译句子‘“最终正义彻底战胜了邪恶”,一方面两个主体“正义”和“邪恶”有很强的关联性,另一方面”正义”是“战胜”这个词的主语,也理应把这两个词连在一起理解。使用多个并列的Attention模块可以很好地解决这个问题。
scaled dot-product
并列的每个Head都是一个Scaled Dot-Product Attention模块,结构如下:
对于Encoder而言,Q, K, V对应的都是输入序列。假设一个长度为t的输入序列,每个时间点的feature维度为k,通常会把N个这样的序列叠在一起处理,组成一个维度为 [N, t, k] 的三维矩阵。
def Scaled Dot-Product Attention(Q, K, V):
# mat_re的维度是[N, t, t]。代表序列中词与词之间的关系,可以类似成是一个协方差矩阵。
mat_re = Q * np.transpose(K) / math.qrt(K.shape[-1])
# 有了词与词之间的关联性以后,把它作为权重, 用权重和的方式,基于序列所有其他词的特征及他们的关联性,计算/更新每个词的特征。
# 在第三个维度上做softmax。即,N个序列的每个词都拥有一个概率分布,显示它和同序列中其他词的相关性
# 用每个词的概率分布作为权重,把同序列中其他词的各维feature按相应权重结合起来,作为这个词新的features,feature的维度与输入时的一致。out维度为[N, t, k]
out = tf.softmax(mat_re, axis=2) * V
return out
通过以上代码可以明显看出,它不是用递归神经网络的方式处理输入,而是直接通过矩阵乘法计算同序列中,词与词之间的相互关系,以此关系的远近做为权重,重新计算每个词的各维features的值。
multi-head attention
下图为原论文中的多头注意力机制(multi-head attention mechanism)的结构。可以看出,它是由多个并列的Scaled Dot-Product Attention组成的。V(Value), K(Key) 和Q(Query)会经过各自独立的简单线性变换,然后进入相应的Scaled Dot-Product Attention。(每个Scaled Dot-Product Attention会对应三个独立的linear transform matrix)
h个Scaled Dot-Product Attention模块的输出会连接起来,经过线性变换后,输入到下一个block。
def AttentionHead(Q, K, V, feature_dim):
# 做线性转换,转换后的维度统一为feature_dim
q_dim = Q.shape[-1]
Q = LinearTransform(Q, q_dim, feature_dim)
k_dim = K.shape[-1]
K = LinearTransform(K, k_dim, feature_dim)
v_dim = V.shape[-1]
V = LinearTransform(V, v_dim, feature_dim)
# 用之前实现的Scaled Dot-Product Attention来完成相关性计算和features更新
# 输出维度为[N, t, k]
out = Scaled Dot-Product Attention(Q, K, V)
return out
以上是单头(Single-Head)模块,Multi-Head Attention仅需在基础上稍加组装。如下:
def MultiheadAttention(Q, K, V, feature_dim, model_dim, n_head):
# 生成一系列的单头attention输出,并把它们在feature维度(最后一维)连接起来
multi_attentions = [np.concatenate(multi_attentions, AttentionHead(Q, K, V, feature_dim), axis=-1) for i in range(n_head)]
# 通过线性变换把feature维度转变回最初输入的维度, out维度为 [N, t, k]
out = LinearTransform(x, feature_dim*n_head, model_dim)
return out
至此,我们已经把transformer的关键组成部分解释了,下面来从更高的模块角度解释它的结构。
the overall Encoder Block
有了前文对Mlti-Attention机制的了解,Encoder包含的block结构其实已经很容易理解了。它主要由两部分组成:Mlti-Attention子模块和一个简单的正向神经子网络(序列内的不同位置间共享,但不同layer或者说block拥有的position-wise feed-forward network不同)。这两个子模块分别都加上了一个residual连接,并且在输出到下一个子模块前使用了layer normalization。
def Encode_block(x):
# 每个encoder模块的三个输入都是一样的
# x维度[N, t, k]
# atten 维度[N, t, k]
atten = MultiheadAttention(x, x, x, feature_dim, model_dim, n_head)
# residual connection 加上 layer normalization
x = x + layer_norm(atten)
# 普通的feed forward 子模块
# 注意,此处的feed forward对序列每个位置而言都一样,即每一个词位置(t维度)都共用一个feed forward network。
#当然,每个block中的position-wise feed-forward network有所不同。
ff_out = position_wise_feed_forward(x)
# out维度[N, t, k]
out = x + layer_norm(ff_out)
return out
position-wise feed-forward network
对于某一个block里的feedforward网络,它会在每一个word对应的feature纬度上做transform。这个sequence的不同word上,使用相同的feedforward网络,对每个word的feature做变换。
例如,现在有N个sequence,每个sequence拥有t个word,每个word的纬度是k维:[N,t, k]。那么feedforward网络会在第三维上进行连接,weight matrix的维度是[k, dims]和[dims, k]。
position-wise 也指它在position纬度上卷积。
Decoder block
可以看到Decoder block和Encoder block的结构很相似,都使用了Mlti-Attention子模块和一个同位置共享的的正向神经子网络来组成。但因为功能不同,Decoder有以下几个特别点:
- Masked Multi-Head Attention
在训练transformer时,我们只能给Decoder输入当前word之前的信息,不能把需要predict的label(current word)也给它。因此,Decoder的输入序列需要根据当前需要predict的词的位置来进行mask。当前待predict的位置和其之后对应的attention均需要被mask为0。 - the input of middle Multi-Head Attention
中间的Mlti-Attention子模块用于结合Encoder的信息。它的三个输入:Key, Value, Query分别对应encoder output, encoder output 和decoder前一个Multi-Head模块的输出。可以理解为是用encoder学到的信息组成了一个信息库,而decoder学到的关于outputs的信息作为查询,来获取下一个时间点的预测。
positional encoding
以上的章节已经涵盖了transformer的核心结构,但是为了让这个结构更加适合序列应用场景,transformer有一个特别的设计叫做positional encoding。这个设计的初衷是,序列中元素的顺序会使序列有不同的含义,因此模型在模拟使应该利用元素间的顺序。例如,“蛇吃了人”和“人吃了蛇”这两句话有非常不同的含义。但目前为止提到的结构模块都无视了元素间的相对顺序(Multi-Head Attention直接计算每个元素与其他元素间的关系,而position-wise feed-forward network只与元素单独的位置有关)。为了模拟元素间的相对顺序对序列含义的影响,transformer在输入数据经过embedding后,添加了positional encoding这个步骤。
positional encoding的公式如下:
P
E
(
p
o
s
,
2
i
)
=
sin
(
p
o
s
/
100
0
2
i
/
d
m
o
d
e
l
)
PE(pos, 2i) = \sin(pos / 1000^{2i/d_{model}})
PE(pos,2i)=sin(pos/10002i/dmodel)
P
E
(
p
o
s
,
2
i
+
1
)
=
sin
(
p
o
s
/
100
0
(
2
i
+
1
)
/
d
m
o
d
e
l
)
PE(pos, 2i+1) = \sin(pos / 1000^{(2i+1)/d_{model}})
PE(pos,2i+1)=sin(pos/1000(2i+1)/dmodel)
pos代表了元素在序列中的位置,而i是代表了元素的feature的各个维度。因为三角函数是周期的,以此它可以体现元素与元素之间的相对位置关系。
summary
https://mlexplained.com/2017/12/29/attention-is-all-you-need-explained/
basic : https://medium.com/inside-machine-learning/what-is-a-transformer-d07dd1fbec04
attention+en-decoder: https://nlp.stanford.edu/pubs/emnlp15_attn.pdf
http://jalammar.github.io/illustrated-transformer/
https://github.com/tensorflow/nmt
https://skymind.ai/wiki/attention-mechanism-memory-network