小赵带你读论文系列-Attention is all your need

之前看视频记过一点Transformer的笔记和代码Transformer及其变种Transformer的理解与实现

但是对于原论文一直没有看过,只是分别从粗浅的概念上和实现上进行了理解,下面将对原文中的一些概念进行详述。


Position Encoding的解释Position Encoding1Position Encoding2这两篇博文讲的比较详细。具体就是在Embedding前将其位置信息表示为一个和Embedding大小相同的Vector,然后Embedding的结果和其进行相加,这样就可以表示其相对位置的信息。代码可见Transformer的理解与实现部分。

缩放点积注意力机制:其实Q,K,V只是X embedding后的三种表示,Q和K做Attention算出score,再softmax为attention(权重),然后再和V做weighted sum就得到context(这个context是value里面的值,暂时不用理解)。至于为什么要除K的dim,原文是这么解释的:

假设 Q 和 K 的均值为0,方差为1。它们的矩阵乘积将有均值为0,方差为 dk。因此,dk 的平方根被用于缩放(而非其他数值),因为,Q 和 K 的矩阵乘积的均值本应该为 0,方差本应该为1,这样会获得一个更平缓的 softmax。

 注意这个Mask是可以选择的,Mask的实现机制其实就是给Q*K后的对应矩阵的每一个值加入一个超大的负值(用于遮挡一个序列中的后续标记),使其在softmax后attention weights接近于0。

OK,到这里大家有可能有点印象了,这里要记住这个东西返回两个值,一个是attention weights,还有一个是对应的outputs(也就是value里面根据attention weights选出来的值)。有可能大家对q,k,v的内在还是不了解,我们来看个例子。

np.set_printoptions(suppress=True)

temp_k = tf.constant([[10,0,0],
                      [0,10,0],
                      [0,0,10],
                      [0,0,10]], dtype=tf.float32)  # (4, 3)

temp_v = tf.constant([[   1,0],
                      [  10,0],
                      [ 100,5],
                      [1000,6]], dtype=tf.float32)  # (4, 2)

# 这条 `请求(query)符合第二个`主键(key)`,也就是temp_k的第二行
# 因此返回了第二个`数值(value)`。
temp_q = tf.constant([[0, 10, 0]], dtype=tf.float32)  # (1, 3)
print_out(temp_q, temp_k, temp_v)
Attention weights are:
tf.Tensor([[0. 1. 0. 0.]], shape=(1, 4), dtype=float32)
Output is:
tf.Tensor([[10.  0.]], shape=(1, 2), dtype=float32)
# 这条请求符合重复出现的主键(第三第四个),也就是第三四行
# 因此,对所有的相关数值取了平均。
temp_q = tf.constant([[0, 0, 10]], dtype=tf.float32)  # (1, 3)
print_out(temp_q, temp_k, temp_v)
Attention weights are:
tf.Tensor([[0.  0.  0.5 0.5]], shape=(1, 4), dtype=float32)
Output is:
tf.Tensor([[550.    5.5]], shape=(1, 2), dtype=float32) 

还有一个复杂的例子如下

# 这条请求符合第一和第二条主键,特别注意这种情况
# 因此,对它们的数值去了平均。
temp_q = tf.constant([[10, 10, 0]], dtype=tf.float32)  # (1, 3)
print_out(temp_q, temp_k, temp_v)
Attention weights are:
tf.Tensor([[0.5 0.5 0.  0. ]], shape=(1, 4), dtype=float32)
Output is:
tf.Tensor([[5.5 0. ]], shape=(1, 2), dtype=float32)

看到了嘛,这个q其实就是k里面的某一行或者多行,然后根据这个行数去对应的v里面找值做average.

多头注意力机制:首先注意原文中的dmodel是embedding的维度数,该数量一定要是head的数目的倍数。其实单头已经理解了,多头也就好理解了,原文阐述如下:

Q、K、和 V 被拆分到了多个头,而非单个的注意力头,因为多头允许模型共同注意来自不同表示空间的不同位置的信息。在分拆后,每个头部的维度减少,因此总的计算成本与有着全部维度的单个注意力头相同。

具体在实现上是怎么做到呢,其实也很简单,但是要注意维度数目的变化(超级易错),先将Q,K,V在dim(512)维度上进行拆分,然后分别attention,然后再将attention的结果加起来即可。

我们隐藏具体实现细节,仅关注一下输入输出维度:这里有同学可能要问了,为什么attn最后输出居然是两个60,你可以回到点积注意力那里看一下,其实输出的维度是和k和v的维度是有直接关系的。(或者不妨理解为8个feature下,60个单词互相之间的关联关系)

temp_mha = MultiHeadAttention(d_model=512, num_heads=8)
y = tf.random.uniform((1, 60, 512))  # (batch_size, encoder_sequence, d_model)
out, attn = temp_mha(y, k=y, q=y, mask=None)
out.shape, attn.shape
(TensorShape([1, 60, 512]), TensorShape([1, 8, 60, 60]))

 Encoder Layer部分:具体可以参照Seq-to-Seq实战,但有一点我老是忽略,大家看一下下图:居然attention的outputs输出以后和原来直接embedding的结果加起来才算是下一个input,在每一个子块都是。(原文中点窥视的第一层dff不用管他,我觉得换成别的也行,只要最后输出是dmodel就行了。)

Decoder Layer部分:一定要注意Decoder中的Q,K,V是什么,这里有点不好记,描述如下:

多头注意力(用填充遮挡)。V(数值)和 K(主键)接收编码器输出作为输入。Q(请求)接收遮挡的多头注意力子层的输出。(这里Q的dim是另一门语言的embedding!!!)

还有一点重要的就在于这个填充!!!因为你不能拿未来的decoder结果来推测现在的值。这里比较难理解,所以代码镇楼!

class DecoderLayer(tf.keras.layers.Layer):
  def __init__(self, d_model, num_heads, dff, rate=0.1):
    super(DecoderLayer, self).__init__()

    self.mha1 = MultiHeadAttention(d_model, num_heads)
    self.mha2 = MultiHeadAttention(d_model, num_heads)

    self.ffn = point_wise_feed_forward_network(d_model, dff)
 
    self.layernorm1 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
    self.layernorm2 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
    self.layernorm3 = tf.keras.layers.LayerNormalization(epsilon=1e-6)
    
    self.dropout1 = tf.keras.layers.Dropout(rate)
    self.dropout2 = tf.keras.layers.Dropout(rate)
    self.dropout3 = tf.keras.layers.Dropout(rate)
    
    
  def call(self, x, enc_output, training, 
           look_ahead_mask, padding_mask):
    # 这个call是当函数使用
    # enc_output.shape == (batch_size, input_seq_len, d_model)

    attn1, attn_weights_block1 = self.mha1(x, x, x, look_ahead_mask)  # (batch_size, target_seq_len, d_model)
    attn1 = self.dropout1(attn1, training=training) # test无需drop
    out1 = self.layernorm1(attn1 + x)
    
    attn2, attn_weights_block2 = self.mha2(
        enc_output, enc_output, out1, padding_mask)  # (batch_size, target_seq_len, d_model)
    attn2 = self.dropout2(attn2, training=training)
    out2 = self.layernorm2(attn2 + out1)  # (batch_size, target_seq_len, d_model)
    
    ffn_output = self.ffn(out2)  # (batch_size, target_seq_len, d_model)
    ffn_output = self.dropout3(ffn_output, training=training)
    out3 = self.layernorm3(ffn_output + out2)  # (batch_size, target_seq_len, d_model)
    
    return out3, attn_weights_block1, attn_weights_block2

损失函数:这里的Padding搞了好久都没有理解这mask是怎么运作的,后来发现好像是这个意思,最终的输出是一个snetence_len*vocab的矩阵,但是后面Padding的部分显然不能算在Loss里面。比如I Love Python 0 0 0,这三个0的Loss是不能计算的,所以原文中使用mask将其置为0,实现如下:

def loss_function(real, pred):
  mask = tf.math.logical_not(tf.math.equal(real, 0))
  loss_ = loss_object(real, pred)

  mask = tf.cast(mask, dtype=loss_.dtype)
  loss_ *= mask
  
  return tf.reduce_mean(loss_)

训练和预测:训练的时候使用已有句子的开头当作起始值进行生成,然后计算取中间部分计算Loss,如下图;预测的时候起始值为目标语料库的起始Token。

其他一些小知识点:原文中出现,但是没有做过多解释的部分

Beam Search: Beam Search

Word Piece: Word Piece

Word Embedding: Word Embedding

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Data_Designer

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值