本次分享的论文是鼎鼎有名的 a t t e n t i o n i s a l l y o u n e e d attention\ is\ all\ you\ need attention is all you need,论文链接attention is all you need,其参考的 t e n s o r f l o w tensorflow tensorflow 实现代码tensorflow代码实现。
自己水平有限,在读这篇论文和实现代码时,感觉比较吃力,花了两三天才搞懂了一些,在此总结下。
废话不多说,直接带着代码看论文介绍的网络结构。
下面总结是以论文实验 机器翻译来说的。
我们分部分来看:
建议可以先看看 台大教授李宏毅关于transformer的课程:https://www.bilibili.com/video/av56239558?from=search&seid=17256210640645614178
Stage1 Encode Input
和普遍的做法一样,对文本输入做 w o r d e m b e d d i n g word\ embedding word embedding 操作,
embedding_encoder = tf.get_variable("embedding_encoder", [Config.data.source_vocab_size, Config.model.model_dim], self.dtype)(注意这里的model_dim)
embedding_inputs = embedding_encoder
上面其实就是做个输入文本的 e m b e d d i n g embedding embedding矩阵而已。
模型里已经剔除了 R N N RNN RNN, C N N CNN CNN,如何体现输入文本的先后关系呢?而这种序列的先后关系对模型有着至关重要的作用,于是论文中提出了 P o s i t i o n E n c o d i n g Position\ Encoding Position Encoding 骚操作~,论文是这样做的:
P
E
(
p
o
s
,
2
i
)
=
s
i
n
(
p
o
s
/
1000
0
2
i
/
d
m
o
d
e
l
)
(
偶
数
位
置
处
)
PE(pos,2i) = sin(pos/10000^{2i/d_{model}})(偶数位置处)
PE(pos,2i)=sin(pos/100002i/dmodel)(偶数位置处)
P
E
(
p
o
s
,
2
i
+
1
)
=
c
o
s
(
p
o
s
/
1000
0
2
i
/
d
m
o
d
e
l
)
(
奇
数
位
置
处
)
PE(pos,2i+1) = cos(pos/10000^{2i/d_{model}})(奇数位置处)
PE(pos,2i+1)=cos(pos/100002i/dmodel)(奇数位置处)
这里的 d m o d e l d_{model} dmodel指的是上面 w o r d e m b e d d i n g word\ embedding word embedding 的维度, p o s pos pos就是当前的词在整个句子中的位置,例如第一个词还是第二个词等, i i i 就是遍历 d m o d e l d_{model} dmodel时的值,在代码中是这样做的:
def positional_encoding(dim, sentence_length, dtype=tf.float32):
encoded_vec = np.array([pos/np.power(10000, 2*i/dim) for pos in range(sentence_length) for i in range(dim)])#对每个位置处都产生一个维度为dim的向量。
encoded_vec[::2] = np.sin(encoded_vec[::2])#偶数位置处
encoded_vec[1::2] = np.cos(encoded_vec[1::2])#奇数位置处
return tf.convert_to_tensor(encoded_vec.reshape([sentence_length, dim]), dtype=dtype)
#Positional Encoding
with tf.variable_scope("positional-encoding"):
positional_encoded = positional_encoding(Config.model.model_dim, Config.data.max_seq_length, dtype=self.dtype)
上面生成的 p o s i t i o n a l _ e n c o d e d positional\_encoded positional_encoded其实就是位置信息的 e m b e d d i n g embedding embedding 矩阵。论文中提到这样做的原因,就是希望模型能很容易的学到相对先后的位置信息。
# Add
position_inputs = tf.tile(tf.range(0, Config.data.max_seq_length), [self.batch_size])#将range(0, max_seq_length)列表复制batch_size次,生成shape为[batch_size, max_seq_length]的张量。
position_inputs = tf.reshape(position_inputs,[self.batch_size, Config.data.max_seq_length]) # batch_size x [0, 1, 2, ..., n]#未经过embedding的位置输入信息。
好了 输 入 文 本 和 位 置 信 息 的 e m b e d d i n g 输入文本和位置信息的embedding 输入文本和位置信息的embedding矩阵都做好了,该 l o o k _ u p look\_up look_up 了。
encoded_inputs = tf.add(tf.nn.embedding_lookup(embedding_inputs, inputs), tf.nn.embedding_lookup(positional_encoded, position_inputs))
这与输入文本信息就结合的其位置信息了,作为 e n c o d e r encoder encoder的整体输入,这部分对应的上面那张图的** s t a g e 1 stage1 stage1**部分,这部分的操作就是如下:
p o s i t i o n a l e n c o d i n g s d d e d ⊕ e m b e d d e d i n p u t s positional\ encodings\ dded ⊕ embedded\ inputs positional encodings dded⊕embedded inputs
d e c o d e _ i n p u t e m b e d d i n g decode\_input\ embedding decode_input embedding同理,就不再赘述了。
Stage2 Multi Head Attention
multi head attention 绝对是transform里面的 一个重点和优点,正是有了这个机制,transform才能有这么好的效果。
当时论文读到这里有点懵逼,什么叫 m u l t i h e a d multi\ head multi head?再仔细看看论文吧?
由上图我们可以看出 m u l t i h e a d a t t e n t i o n multi\ head\ attention multi head attention 有三个相同的输入,不妨分别记为 Q 、 K 、 V Q、K、V Q、K、V,其实就是上面的 E n c o d e I n p u t Encode\ Input Encode Input其 s h a p e shape shape均为 [ b a t c h _ s i z e , m a x _ s e q _ l e n g t h , d i m ] [batch\_size, max\_seq\_length, dim] [batch_size,max_seq_length,dim]。论文中提到对三个输入做 n u m _ h e a d num\_head num_head次不同的线性映射,即为:
def _linear_projection(self, q, k, v):
q = tf.layers.dense(q, self.linear_key_dim, use_bias=False)
k = tf.layers.dense(k, self.linear_key_dim, use_bias=False)
v = tf.layers.dense(v, self.linear_value_dim, use_bias=False)
return q, k, v
上述代码就是做线性映射,其中 l i n e a r _ k e y _ d i m 、 l i n e a r _ v a l u e _ d i m linear\_key\_dim、linear\_value\_dim linear_key_dim、linear_value_dim就是映射的 u n i t s units units个数。这里面相当于把 n u m _ h e a d num\_head num_head次的线性映射一起做了,后面需要把每一个 h e a d head head映射结果分割开,故需要保证 l i n e a r _ k e y _ d i m linear\_key\_dim linear_key_dim 和 l i n e a r _ v a l u e _ d i m linear\_value\_dim linear_value_dim 能整除 n u m _ h e a d num\_head num_head。 经过线性映射后生成的 q 、 k 、 v q、k、v q、k、v 的 s h a p e shape shape分别为 [ b a t c h _ s i z e , m a x _ s e q _ l e n g t h , l i n e a r _ k e y _ d i m ] , [ b a t c h _ s i z e , m a x _ s e q _ l e n g t h , l i n e a r _ k e y _ d i m ] , [ b a t c h _ s i z e , m a x _ s e q _ l e n g t h , l i n e a r _ v a l u e _ d i m ] [batch\_size, max\_seq\_length, linear\_key\_dim], [batch\_size, max\_seq\_length, linear\_key\_dim], [batch\_size, max\_seq\_length, linear\_value\_dim] [batch_size,max_seq_length,linear_key_dim],[batch_size,max_seq_length,linear_key_dim],[batch_size,max_seq_length,linear_value_dim]
然后按 n u m _ h e a d s num\_heads num_heads分割开来得:(这里分割开,相当于初始个num_head 不同的权重,下面将会做num_head 次不同的attention操作。理解这非常重要)
def _split_heads(self, q, k, v):
def split_last_dimension_then_transpose(tensor, num_heads, dim):
┆ t_shape = tensor.get_shape().as_list()
┆ tensor = tf.reshape(tensor, [-1] + t_shape[1:-1] + [num_heads, dim // num_heads])
┆ return tf.transpose(tensor, [0, 2, 1, 3]) # [batch_size, num_heads, max_seq_len, dim]
qs = split_last_dimension_then_transpose(q, self.num_heads, self.linear_key_dim)
ks = split_last_dimension_then_transpose(k, self.num_heads, self.linear_key_dim)
vs = split_last_dimension_then_transpose(v, self.num_heads, self.linear_value_dim)
return qs, ks, vs
论文提到,这时生成的 q s 、 k s 、 v s qs、ks、vs qs、ks、vs可以并行的放入到 a t t e n t i o n _ f u n c t i o n attention\_function attention_function 中,那么这个 a t t e n t i o n _ f u n c t i o n attention\_function attention_function 是个什么样的结构呢?
上图所示的结构在论文中被称为
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
Scaled Dot_Product Attention,其
a
t
t
e
n
t
i
o
n
attention
attention值的计算公式如下:
A
t
t
e
n
t
i
o
n
(
Q
,
K
,
V
)
=
s
o
f
t
m
a
x
(
Q
K
T
d
k
)
V
Attention(Q,K,V)=softmax\left ( \frac{QK^{T}}{\sqrt{d_k}} \right )V
Attention(Q,K,V)=softmax(dkQKT)V
由上面可知,其公式中的
Q
、
K
、
V
Q、K、V
Q、K、V 分别对应的是
q
s
、
k
s
、
v
s
qs、ks、vs
qs、ks、vs,其实都是
E
n
c
o
d
e
r
I
n
p
u
t
Encoder\ Input
Encoder Input,只是做了不同的线性映射,其中
q
s
、
k
s
qs、ks
qs、ks 的维度相同。我们可以这样理解
Q
K
T
QK^T
QKT操作,假设
Q
Q
Q为
s
h
a
p
e
shape
shape为
[
m
a
x
_
s
e
q
_
l
e
n
g
t
h
,
d
i
m
]
[max\_seq\_length, dim]
[max_seq_length,dim]的矩阵,
V
V
V为
s
h
a
p
e
shape
shape相同,那么经过
Q
K
T
QK^T
QKT操作以后,变成了
s
h
a
p
e
shape
shape为
[
m
a
x
_
s
e
q
_
l
e
n
g
t
h
,
m
a
x
_
s
e
q
_
l
e
n
g
t
h
]
[max\_seq\_length, max\_seq\_length]
[max_seq_length,max_seq_length]的矩阵,怎样理解这个生成的矩阵呢?其实就是做了个
s
e
l
f
_
a
t
t
e
n
t
i
o
n
self\_attention
self_attention操作,即是当前句子中每个词和其他词做个乘积形成的矩阵,以得到每个词的权重,以便学习当前应该
f
o
u
c
s
foucs
foucs到哪个词。那么为什么要除以
d
k
\sqrt{d_k}
dk呢?论文中说到,当两个矩阵做
d
o
t
p
r
o
d
u
c
t
dot\ product
dot product时,可能会变得很大(试想一下,两个矩阵相互独立,且均值为0,方差为1,那么经过矩阵相乘以后,均值还为0,方差变成
d
k
d_k
dk),经过
s
o
f
t
m
a
x
softmax
softmax后,梯度可能会变得很小,为了抵消这种效果,再除以
d
k
\sqrt{d_k}
dk。其代码如下:
def _scaled_dot_product(self, qs, ks, vs):
key_dim_per_head = self.linear_key_dim // self.num_heads
o1 = tf.matmul(qs, ks, transpose_b=True)
o2 = o1 / (key_dim_per_head**0.5)
if self.masked:
┆ diag_vals = tf.ones_like(o2[0, 0, :, :]) # (batch_size, num_heads, query_dim, key_dim)
┆ tril = tf.contrib.linalg.LinearOperatorTriL(diag_vals).to_dense() # (q_dim, k_dim)
┆ masks = tf.tile(tf.reshape(tril, [1, 1] + tril.get_shape().as_list()),
┆ ┆ ┆ ┆ ┆ [tf.shape(o2)[0], tf.shape(o2)[1], 1, 1])
┆ paddings = tf.ones_like(masks) * -1e9
┆ o2 = tf.where(tf.equal(masks, 0), paddings, o2)
o3 = tf.nn.softmax(o2)
return o3
好了,过了 s e l f _ a t t e n t i o n self\_attention self_attention后:
由上图可知,再过 c o n c a t concat concat操作:(不同的head关注的点可能不一样,这里concat操作,相当于把num_head次不同的attention结果集成在一起,理解这非常重要)
def _concat_heads(self, outputs):
def transpose_then_concat_last_two_dimenstion(tensor):
┆ tensor = tf.transpose(tensor, [0, 2, 1, 3]) # [batch_size, max_seq_len, num_heads, dim]
┆ t_shape = tensor.get_shape().as_list()
┆ num_heads, dim = t_shape[-2:]
┆ return tf.reshape(tensor, [-1] + t_shape[1:-2] + [num_heads * dim])
return transpose_then_concat_last_two_dimenstion(outputs)
论文中提到,这样做后,再过一层线性映射。
output = tf.layers.dense(output, self.model_dim)
故整个 M u l t i H e a d A t t e n t i o n Multi\ Head\ Attention Multi Head Attention 操作如下:
def multi_head(self, q, k, v):
q, k, v = self._linear_projection(q, k, v)
qs, ks, vs = self._split_heads(q, k, v)
outputs = self._scaled_dot_product(qs, ks, vs)
output = self._concat_heads(outputs)
output = tf.layers.dense(output, self.model_dim)
return tf.nn.dropout(output, 1.0 - self.dropout)
然后在做个 r e s N e t resNet resNet和 l a y e r N o r m a l i z a t i o n layerNormalization layerNormalization:
def _add_and_norm(self, x, sub_layer_x, num=0):
with tf.variable_scope(f"add-and-norm-{num}"):
┆ return tf.contrib.layers.layer_norm(tf.add(x, sub_layer_x)) # with Residual connection
这里面的
s
u
b
_
l
a
y
e
r
_
x
sub\_layer\_x
sub_layer_x 就是上面
m
u
t
l
i
h
e
a
d
mutli\ head
mutli head的输出,
x
x
x就是
e
n
c
o
d
e
r
_
i
n
p
u
t
encoder\_input
encoder_input。
矩阵并行化计算过程:
上面就是 S t a g e 2 Stage2 Stage2 的整个过程。
Stage3 Feed Forward
这一步就比较简单了,就是做两层的 f u l l y _ c o n n e c t i o n fully\_connection fully_connection 而已,只不过内层的 f u l l y _ c o n n e c t i o n fully\_connection fully_connection 会过 r e l u relu relu 激活。
F F N ( x ) = m a x ( 0 , x W 1 + b 1 ) W 2 + b 2 FFN(x) = max(0, xW_1 + b_1)W_2 + b_2 FFN(x)=max(0,xW1+b1)W2+b2
别问我为啥 m a x max max
同理再过 r e N e t 、 n o r m a l i z a t i o n reNet、normalization reNet、normalization。
D e c o d e Decode Decode部分和上面差不多,只不过在 D e c o d e I n p u t Decode\ Input Decode Input 的 S t a g e 1 Stage1 Stage1部分,做 s e l f a t t e n t i o n self\ attention self attention时,我们不能使当前词的后面的词对当前词产生影响,因为在当前我们实际是不知道后面应该有哪些词的,只不过在 t r a i n train train的时候可以批量的训练,但是在 d e c o d e decode decode的时候是不知道的。那么该怎么消除后面词对当前词的影响呢?
在 s e l f a t t e n t i o n self\ attention self attention时,会得到 a t t e n t i o n attention attention矩阵,我们只需要保留该矩阵的下三角部分即可,然后再做 s o f t m a x softmax softmax,既可消除后面词对当前词的影响。
def _scaled_dot_product(self, qs, ks, vs):
key_dim_per_head = self.linear_key_dim // self.num_heads
o1 = tf.matmul(qs, ks, transpose_b=True)
o2 = o1 / (key_dim_per_head**0.5)
if self.masked:
┆ diag_vals = tf.ones_like(o2[0, 0, :, :]) # (batch_size, num_heads, query_dim, key_dim)
┆ tril = tf.contrib.linalg.LinearOperatorTriL(diag_vals).to_dense() # (q_dim, k_dim)
┆ masks = tf.tile(tf.reshape(tril, [1, 1] + tril.get_shape().as_list()),
┆ ┆ ┆ ┆ ┆ [tf.shape(o2)[0], tf.shape(o2)[1], 1, 1])
┆ paddings = tf.ones_like(masks) * -1e9
┆ o2 = tf.where(tf.equal(masks, 0), paddings, o2)
o3 = tf.nn.softmax(o2)
return o3
剩余部分的 S t a g e 4 、 S t a g e 5 Stage4、Stage5 Stage4、Stage5与上面类似,就不再赘述了。
整个论文的过程可以用如下动画解释:
个人看法
- 该论文摈弃了 R N N 、 C N N RNN、CNN RNN、CNN 等作为基本的模型,而是单纯的采用 A t t e n t i o n Attention Attention结构,使得计算并行性大大提高。
- 没想到 a t t e n t i o n attention attention也可以单独的作为神经网络的一层,甚至可以看作对 i n p u t input input的 r e p e s e n t a t i o n repesentation repesentation。
- Transformer的多头注意力机制能从不同角度对每个对象的重要性进行评价,从而能更好的学习输入对象中存在的各种关系并对其进行表征。
不同的head, attention的注意力不一样