Attention is all you need 论文阅读报告及代码详解

文章简介

这篇文章是Google公司谷歌大脑部门的一篇关于使用注意力机制(Attention Mechanism)来进行翻译的模型的研究,这篇文章最终发表在NIPS 2017上。整个模型还是基于 encode-decode的构架,但是attention不再和CNN以及RNN一起使用,而是单独使用。这个框架的名字叫做Transformer

论文链接

Self-attention Model

自注意力模型是进两年来的热门模型,这个模型的想法挺有意思的。

attention 可以进行如下描述,一般为Query(Q)和Key(K)与Value(V)的对应的映射,其中key-value一一对应,在NLP的任务当中,通常 $K=V$, 整个attention 的结构可以看做下图。
图片1

  1. 模型通过比较Q和K的相似度,得到权重,通常的相似度计算方法如下
    $$f(Q,K_i)=Q^TK_i$$ 点乘
    $$f(Q,K_i)=Q^TWK_i$$ 权重
    $$f(Q,K_i)=W[Q,K_i]$$ 拼接权重
    $$f(Q,K_i)=v^T\text{tanh}(W Q+U K_i)$$ 感知器
    权重的计算方法如下:
    $$\alpha_i = softmax(f(Q,K))=\frac{\text{exp}(f(Q,K_i))}{\sum_j\text{exp}(f(Q,K_i))}$$
  2. 根据上一步计算的权重,对value值进行加权计算,通常为加权求和,得到attention向量
    $$AttentionValue(Q,K,V) = \sum_i{\alpha_i V_i}$$
  3. self-attention 模型就是自己对自己求attention,即$Q=K=V$, 但是论文中对其attention权重做了缩放,除去了$K$的纬度$\sqrt{d_k}$,得到
    $$Attention(Q,K,V)=\text{softmax}(\frac{QK^T}{\sqrt{d_k}})V.$$

图片2:论文时用的attention方法

Encoder

编码器由6个完全相同的层组成,每一个层有两个子层,一个是multi-headed attention层,另一个是一个position-wise的全连接层。在每一层中应用残差网络和正则化层的方法。残差网络就是将输入也链接到输出,使得训练更加有效。
图片3:编码器

Multi-headed Attention

论文提出了Multi-headed Attention,旨在对Q,K,V,在不同的空间的特征进行提取,其思想也比较简单,就是多做几次attention,其中每次attention的特征不共享

$$\text{MultiHead}(Q,K,V) = \text{Concat}(\text{head}_i,\cdots,\text{head}_h)$$

其中每一个$\text{head}_i$计算方法如下
$$\text{head}_i = \text{Attention}(Q W^Q_i,K W^K_i,V W^V_i)$$

论文中使用了8个heads

图片4:编码器

Position Encoding

论文输入词向量时,不仅输入了词向量的序列,也考虑了词语之间的顺序和位置。

$$PE(pos,2i)=sin(\frac{pos}{10000^{2i/d_m}}) \\
PE(pos,2i+1)=cos(\frac{pos}{10000^{2i/d_m}})$$
其中,$d_m$表示输出的维数。
这样表示的原因是,对于$PE_{pos+k}$的词向量,可以是$PE_{pos}$向量的线性组合表示,这里利用了公式:
$$\sin(\alpha+\beta)=\sin\alpha \cos\beta + \cos\alpha \sin\beta \\
\cos(\alpha+\beta)=\cos\alpha \cos\beta - \sin\alpha \sin\beta$$

最后得到的位置向量,可以和原词向量拼接,也可以和词向量相加。

Layer Normalization

Layer NormalizationBatch Normalization 想法相似,均为降低模型的复杂程度,缓解过拟合,加快训练效率。

Decoder

解码器也是由6个完全一样的层组成,每一个网络层由编码器中的两个子层组成外,还多了一个子层,是将编码器中的多头注意力加入到解码过程中。同时,使用了Masking层,对解码到当前位置i时,对解码器输入i-1之前的信息保留,对i之后的信息进行屏蔽
图片5:解码器

Tricks

论文和代码实现,使用了一下小技巧:

  1. 使用了大量的dropout以增强模型的泛化能力。drop rate选择为0.1
  2. 论文实验使用了8个GPU。

Experiment

Code 分析

代码为 Kyubyong的版本,源代码地址:点我

encoder 部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
with tf.variable_scope("encoder"):
## Embedding 词向量的计算
self.enc = embedding(self.x,
vocab_size=len(de2idx),
num_units=hp.hidden_units,
scale=True,
scope="enc_embed")

## Positional Encoding 加入位置向量
self.enc += positional_encoding(self.x,
num_units=hp.hidden_units,
zero_pad=False,
scale=False,
scope="enc_pe")


## Dropout 对输入向量进行dropout处理,减少过拟合
self.enc = tf.layers.dropout(self.enc,
rate=hp.dropout_rate,
training=tf.convert_to_tensor(is_training))

## Blocks,若干个相同的block
for i in range(hp.num_blocks):
with tf.variable_scope("num_blocks_{}".format(i)):
### Multihead Attention,多头注意力机制
self.enc = multihead_attention(queries=self.enc,
keys=self.enc,
num_units=hp.hidden_units,
num_heads=hp.num_heads,
dropout_rate=hp.dropout_rate,
is_training=is_training,
causality=False)

### Feed Forward,接上一个全连接层。
self.enc = feedforward(self.enc, num_units=[4*hp.hidden_units, hp.hidden_units])

multihead_attention部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def multihead_attention(queries,
keys,
num_units=None,
num_heads=8,
dropout_rate=0,
is_training=True,
causality=False,
scope="multihead_attention",
reuse=None):
'''Applies multihead attention.

Args
queries: A 3d tensor with shape of [N, T_q, C_q].
keys: A 3d tensor with shape of [N, T_k, C_k].
num_units: A scalar. Attention size.
dropout_rate: A floating point number.
is_training: Boolean. Controller of mechanism for dropout.
causality: Boolean. If true, units that reference the future are masked.
num_heads: An int. Number of heads.
scope: Optional scope for `variable_scope`.
reuse: Boolean, whether to reuse the weights of a previous layer
by the same name.

Returns
A 3d tensor with shape of (N, T_q, C)
'''
with tf.variable_scope(scope, reuse=reuse):
# Set the fall back option for num_units
if num_units is None:
num_units = queries.get_shape().as_list[-1]

# Linear projections
Q = tf.layers.dense(queries, num_units, activation=tf.nn.relu) # (N, T_q, C)
K = tf.layers.dense(keys, num_units, activation=tf.nn.relu) # (N, T_k, C)
V = tf.layers.dense(keys, num_units, activation=tf.nn.relu) # (N, T_k, C)

# Split and concat
Q_ = tf.concat(tf.split(Q, num_heads, axis=2), axis=0) # (h*N, T_q, C/h)
K_ = tf.concat(tf.split(K, num_heads, axis=2), axis=0) # (h*N, T_k, C/h)
V_ = tf.concat(tf.split(V, num_heads, axis=2), axis=0) # (h*N, T_k, C/h)

# Multiplication
outputs = tf.matmul(Q_, tf.transpose(K_, [0, 2, 1])) # (h*N, T_q, T_k)

# Scale
outputs = outputs / (K_.get_shape().as_list()[-1] ** 0.5)

# Key Masking
key_masks = tf.sign(tf.abs(tf.reduce_sum(keys, axis=-1))) # (N, T_k)
key_masks = tf.tile(key_masks, [num_heads, 1]) # (h*N, T_k)
key_masks = tf.tile(tf.expand_dims(key_masks, 1), [1, tf.shape(queries)[1], 1]) # (h*N, T_q, T_k)

paddings = tf.ones_like(outputs)*(-2**32+1)
outputs = tf.where(tf.equal(key_masks, 0), paddings, outputs) # (h*N, T_q, T_k)

# Causality = Future blinding
if causality:
diag_vals = tf.ones_like(outputs[0, :, :]) # (T_q, T_k)
tril = tf.contrib.linalg.LinearOperatorTriL(diag_vals).to_dense() # (T_q, T_k)
masks = tf.tile(tf.expand_dims(tril, 0), [tf.shape(outputs)[0], 1, 1]) # (h*N, T_q, T_k)

paddings = tf.ones_like(masks)*(-2**32+1)
outputs = tf.where(tf.equal(masks, 0), paddings, outputs) # (h*N, T_q, T_k)

# Activation
outputs = tf.nn.softmax(outputs) # (h*N, T_q, T_k)

# Query Masking
query_masks = tf.sign(tf.abs(tf.reduce_sum(queries, axis=-1))) # (N, T_q)
query_masks = tf.tile(query_masks, [num_heads, 1]) # (h*N, T_q)
query_masks = tf.tile(tf.expand_dims(query_masks, -1), [1, 1, tf.shape(keys)[1]]) # (h*N, T_q, T_k)
outputs *= query_masks # broadcasting. (N, T_q, C)

# Dropouts
outputs = tf.layers.dropout(outputs, rate=dropout_rate, training=tf.convert_to_tensor(is_training))

# Weighted sum
outputs = tf.matmul(outputs, V_) # ( h*N, T_q, C/h)

# Restore shape
outputs = tf.concat(tf.split(outputs, num_heads, axis=0), axis=2 ) # (N, T_q, C)

# Residual connection
outputs += queries

# Normalize
outputs = normalize(outputs) # (N, T_q, C)

return outputs

Decoder 部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# Decoder
with tf.variable_scope("decoder"):
## Embedding
self.dec = embedding(self.decoder_inputs,
vocab_size=len(en2idx),
num_units=hp.hidden_units,
scale=True,
scope="dec_embed")

## Positional Encoding
self.dec += positional_encoding(self.decoder_inputs,
vocab_size=hp.maxlen,
num_units=hp.hidden_units,
zero_pad=False,
scale=False,
scope="dec_pe")

## Dropout
self.dec = tf.layers.dropout(self.dec,
rate=hp.dropout_rate,
training=tf.convert_to_tensor(is_training))

## Blocks
for i in range(hp.num_blocks):
with tf.variable_scope("num_blocks_{}".format(i)):
## Multihead Attention ( self-attention) ,自注意力机制
self.dec = multihead_attention(queries=self.dec,
keys=self.dec,
num_units=hp.hidden_units,
num_heads=hp.num_heads,
dropout_rate=hp.dropout_rate,
is_training=is_training,
causality=True,
scope="self_attention")

## Multihead Attention ( vanilla attention),和编码器的输出结果做注意力机制
self.dec = multihead_attention(queries=self.dec,
keys=self.enc,
num_units=hp.hidden_units,
num_heads=hp.num_heads,
dropout_rate=hp.dropout_rate,
is_training=is_training,
causality=False,
scope="vanilla_attention")

## Feed Forward
self.dec = feedforward(self.dec, num_units=[4*hp.hidden_units, hp.hidden_units])

总结

论文一经提出就受到了巨大的关注,说明Google在该领域的领头羊作用,且2017~2018年出现大量的self-attention文章,说明其有效作用。

该论文工程性极强,可以看到论文中含有很多的tricks,这些往往影响到最后模型的效果,所以这篇文章还是很值得学习的。

文章作者: Victor Zhang
文章链接: http://cupdish.com/2018/03/28/attention-is-all-you-need/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Cupdish.com