论文分享-->Attention is all you need

本次分享的论文是鼎鼎有名的 attention is all you need ,论文链接attention is all you need,其参考的 tensorflow 实现代码tensorflow代码实现

自己水平有限,在读这篇论文和实现代码时,感觉比较吃力,花了两三天才搞懂了一些,在此总结下。

废话不多说,直接带着代码看论文介绍的网络结构。

下面总结是以论文实验 机器翻译来说的。

这里写图片描述

我们分部分来看:

这里写图片描述

Stage1 Encode Input

和普遍的做法一样,对文本输入做 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

上面其实就是做个输入文本的 embedding 矩阵而已。

模型里已经剔除了 RNN CNN ,如何体现输入文本的先后关系呢?而这种序列的先后关系对模型有着至关重要的作用,于是论文中提出了 Position Encoding 骚操作~,论文是这样做的:

PE(pos,2i)=sin(pos/100002i/dmodel)

PE(pos,2i+1)=cos(pos/100002i/dmodel)

这里的 dmodel 指的是上面 word embedding 的维度, pos 就是当前的词在整个句子中的位置,例如第一个词还是第二个词等, i 就是遍历 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)

上面生成的 positional_encoded 其实就是位置信息的 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的位置输入信息。

好了 embedding 矩阵都做好了,该 look_up 了。

encoded_inputs = tf.add(tf.nn.embedding_lookup(embedding_inputs, inputs),  tf.nn.embedding_lookup(positional_encoded, position_inputs))

这与输入文本信息就结合的其位置信息了,作为 encoder 的整体输入,这部分对应的上面那张图的 stage1 部分,这部分的操作就是如下:

positional encodings ddedembedded inputs

decode_input embedding 同理,就不再赘述了。

Stage2 Multi Head Attention

当时论文读到这里有点懵逼,什么叫 multi head ?再仔细看看论文吧?

这里写图片描述

由上图我们可以看出 multi head attention 有三个相同的输入,不妨分别记为 QKV ,其实就是上面的 Encode Input shape 均为 [batch_size,max_seq_length,dim] 。论文中提到对三个输入做 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

上述代码就是做线性映射,其中 linear_key_dimlinear_value_dim 就是映射的 units 个数。这里面相当于把 num_head 次的线性映射一起做了,后面需要把每一个 head 映射结果分割开,故需要保证 linear_key_dim linear_value_dim 能整除 num_head 。 经过线性映射后生成的 qkv shape 分别为 [batch_size,max_seq_length,linear_key_dim],[batch_size,max_seq_length,linear_key_dim],[batch_size,max_seq_length,linear_value_dim]

然后按 numheads 分割开来得:

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

论文提到,这时生成的 qsksvs 可以并行的放入到 attention_function 中,那么这个 attention_function 是个什么样的结构呢?

这里写图片描述

上图所示的结构在论文中被称为 Scaled Dot_Product Attention ,其 attention 值的计算公式如下:

Attention(Q,K,V)=softmax(QKTdk)V

由上面可知,其公式中的 QK 分别对应的是 qsksvs ,其实都是 Encoder Input ,只是做了不同的线性映射,其中 qsks 的维度相同。我们可以这样理解 QKT 操作,假设 Q shape [max_seq_length,dim] 的矩阵, V shape 相同,那么经过 QKT 操作以后,变成了 shape [max_seq_length,max_seq_length] 的矩阵,怎样理解这个生成的矩阵呢?其实就是做了个 self_attention 操作,即是当前句子中每个词和其他词做个乘积形成的矩阵,以得到每个词的权重,以便学习当前应该 foucs 到哪个词。那么为什么要除以 dk 呢?论文中说到,当两个矩阵做 dot product 时,可能会变得很大(试想一下,两个矩阵相互独立,且均值为0,方差为1,那么经过矩阵相乘以后,均值还为0,方差变成 dk ),经过 softmax 后,梯度可能会变得很小,为了抵消这种效果,再除以 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

好了,过了 self_attention 后:

这里写图片描述

由上图可知,再过 concat 操作:

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)

故整个 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)

然后在做个 resNet 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

这里面的 sub_layer_x 就是上面 mutli head 的输出, x 就是 encoder_input

上面就是 Stage2 的整个过程。

Stage3 Feed Forward

这一步就比较简单了,就是做两层的 fully_connection 而已,只不过内层的 fully_connection 会过 relu 激活。

FFN(x)=max(0,xW1+b1)W2+b2

别问我为啥 max

同理再过 reNetnormalization

Decode 部分和上面差不多,只不过在 Decode Input Stage1 部分,做 self attention 时,我们不能使当前词的后面的词对当前词产生影响,因为在当前我们实际是不知道后面应该有哪些词的,只不过在 train 的时候可以批量的训练,但是在 decode 的时候是不知道的。那么该怎么消除后面词对当前词的影响呢?

self attention 时,会得到 attention 矩阵,我们只需要保留该矩阵的下三角部分即可,然后再做 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

剩余部分的 Stage4Stage5 与上面类似,就不再赘述了。

这里写图片描述

整个论文的过程可以用如下动画解释:

这里写图片描述

个人看法

  1. 该论文摈弃了 RNNCNN 等作为基本的模型,而是单纯的采用 Attention 结构,使得计算并行性大大提高。
  2. 没想到 attention 也可以单独的作为神经网络的一层,甚至可以看作对 input repesentation

猜你喜欢

转载自blog.csdn.net/mr_tyting/article/details/80148557