Transformer 中self-attention以及mask操作的原理以及代码解析

笔者最近在梳理自然语言预训练模型的有关内容。在看到Bert的时候,突然发现Bert之后的预训练模型都与Transformer结构有关。该结构的一个为人所知的重点是self-attention,但是其另外一个重点mask操作却被人了解的很少,笔者借鉴了其他博主的优质内容,加上自己的理解整理了一下,希望从原理以及代码的角度来学习一下这两个知识点。

**

self-attention

**
Self-attention可以说是整个Transformer模型的核心思想。本章关于Self-attention的公式图片均来源于Vaswani 的文章attention is all you need以及博主adam-liu图解Transformer,或者本人手绘.该篇博客在Self-attention的讲述上非常精彩,建议初学者去看一下其原博客内容。

self-attention的出现是为了摆脱循环神经网络不能并行计算的缺点而提出的,它的设计模式可以通过当前单词去查看其输入序列中的其他单词,以此来寻找编码这个单词更好的线索。
在学习Self-Attention的过程中,首先学习的是一个attention的普遍形式(文章中称之为Scaled Dot-Product Attention),看过Attention is all your need 文章的同学肯定知道其计算其计算方式就是通过构造三个矩阵Q,K,V来计算Scaled Dot-Product Attention矩阵,具体的计算流程以及以及计算公式如下图所示:
在这里插入图片描述
图1 Scaled Dot-Product Attention


图2 attention的计算方式

Vaswani文章第一次对attention提出了一个归纳化的公式。在NMT领域当中,我们对比传统attention的计算方式,很容易看出以上Q是来源于Encoder端的隐藏层状态,而K,V是来源于Decoder端的隐藏曾状态。
而Transformer使用的Self-attention。顾名思义即句子中的每一个词都要和该句子当中的所有词进行一个attention计算,目的是学习句子内部的词依赖关系,获取词的内部结构。因此,从这个特点当中我们可以其实也能推测出Self-attention计算的信息来源都是来源于其句子本身。
故在上述attention的计算公式当中,可以看出self-attention即Q=K=V。下面用Transformer的部分代码(Tensorflow)来进一步的了解Q,K,V在self-attention中的构成(以Encoder端为例)。

以下是train.py的部分代码
1 with tf.variable_scope("num_blocks_{}".format(i)):
2     self.enc = multihead_attention(queries=self.enc,
3                                    keys=self.enc,
4                                    num_units=hp.hidden_units,
5                                    num_heads=hp.num_heads,
6                                    dropout_rate=hp.dropout_rate,
7                                    is_traing=is_traing,
8                                    causality=False)
以下是module.py的部分代码
1 def multihead_attention(queries,
2                         keys,
3                         num_units=None,
4                         num_heads=8,
5                         dropout_rate=0,
6                         is_training=True,
7                         causality=False,
8                         scope='multihead_attention',
9                         reuse=None):
10                        
11    with tf.variable_scope(scope,reuse=reuse):
12        if num_units is None:
13            num_units = queries.get_shape().as_list[-1]
14
15        Q = tf.layers.dense(queries,num_units,activation=tf.nn.relu)
16        K = tf.layers.dense(keys,num_units,activation=tf.nn.relu)
17        V = tf.layers.dense(keys,num_units,activation=tf.nn.relu)

train.py中的 self.enc代表的就是 encoder端经过word_embedding以及position_embedding之后的句子信息,之后用X来代替
根据其传入 multihead_attention 函数中的参数来看,在机器翻译领域当中,Transformer当中的queries以及Keys都是其输入信息x。
而在module.py文件当中,我们从矩阵Q,K,V的计算公式中我们可以发现:
Q是将queries输入进一个节点数为num_units的前馈神经网络之后得到的矩阵
而K,V则是将keys输入进一个节点数为num_units的前馈神经网络之后得到的矩阵。
结合所有的信息,在机器翻译领域当中,Q,K,V的所有来源就是encoder端的输入X。即可以看成Q=K=V
具体计算过程如下图所示:
在这里插入图片描述
图3K,Q,V的计算过程

从图中我们可发现,当中使用了三个权重矩阵(WQ,WK,WV)来右乘输入信息矩阵X获得K,Q,V。之后我们便可以使用上述的attention公式来计算self-attention的矩阵。
在这里也顺便提一下muilti_head的概念,Multi_head self_attention的意思就是重复以上过程多次,论文当中是重复8次,即8个Head,使用多套(WQ,WK,WV)矩阵(只要在初始化的时候多稍微变一下,很容易获得多套权重矩阵)。获得多套(Q,K,V)矩阵,然后进行attention计算时便能获得多个self_attention矩阵。
self-attention之后紧接着的步骤是前馈神经网络,而前馈神经网络接受的是单个的矩阵向量,而不是多个矩阵,因此需要把计算得到的多个self-attention矩阵采用某种方式进行合并。因此文章中给出的思路是将这多个连接在一起(可能是简单就是好,No free lunch原则)再和一个矩阵W0相乘。步骤如下图:在这里插入图片描述
图四multi_head_attention的合并过程

综合上述说法,multi_layer_self-attention的整体计算流程如下图所示:
在这里插入图片描述
图5multi_head_self_attention的整体计算流程

self-attention在神经机器翻译实际的操作设计当中,不仅仅是由上面self-attention计算公式那般设计,其中还要加入Mask操作。
其中在Encoder端和Decoder端都需要使用的Mask操作,称之为PADDING MASK。
而仅仅在Decoder段使用的Mask操作,则被称之为Sequence MASK。下面将结合代码分别对其进行介绍。

PADDING MASK

我们在训练的过程中,自然语言数据往往都是以Batch的形式输入进的模型,而一个batch中的每一句话不能保证长度都是一样的,所以需要使用PADDING的方式将所有的句子都补全到最长的长度,比如拿0进行填充,但是我们知道这种用0填充的位置的信息是完全没有意义的,因此我们希望这个位置不参与后期的反向传播过程。以此避免最后影响模型自身的效果,因此提出了在训练时将补全的位置给Mask掉的做法。而在Self-attention的计算当中,我们自然也不希望有效词的注意力集中在这些没有意义的位置上,因此使用了PADDING MASK的方式.
PADDING MASK在attention的计算过程中处于softmax之前(图1中的opt表示optional即该层可加可不加,若是不想使用PADDING MASK操作,则直接Softmax就完事了),通过PADDING MASK的操作,使得补全位置上的值成为一个非常大的负数(可以是负无穷),这样的话,经过Softmax层的时候,这些位置上的概率就是0。以此操作就相当于把补全位置的无用信息给遮蔽掉了(Mask掉了)。具体操作见如下代码(请和注释一起食用):

def multihead_attention(queries,
                        keys,
                        num_units=None,
                        num_heads=8,
                        dropout_rate=0,
                        is_traing=True,
                        causality=False,
                        scope='multihead_attention',
                        reuse=None):

    with tf.variable_scope(scope,reuse=reuse):
        if num_units is None:
            num_units = queries.get_shape().as_list[-1]
        # Q,K,V的shape均为(batch_size,max_length,hidden_size)
        Q = tf.layers.dense(queries,num_units,activation=tf.nn.relu)
        K = tf.layers.dense(keys,num_units,activation=tf.nn.relu)
        V = tf.layers.dense(keys,num_units,activation=tf.nn.relu)
		
		#通过tf.split将Q,K,按照最后一维切分成num_heads份,然后按第一维度进行拼接,
		#以此达到“多头的效果”,此时的Q_就相当于num_heads个Q的拼接,其余同理。
        Q_ = tf.concat(tf.split(Q,num_heads,axis=2),axis=0)
        K_ = tf.concat(tf.split(K,num_heads,axis=2),axis=0)
        V_ = tf.concat(tf.split(V,num_heads,axis=2),axis=0)
    	#这便是图2公式中SoftMax部分的内容    
        A = tf.transpose(K_,[0,2,1])
        outputs = tf.matmul(Q_,tf.transpose(K_,[0,2,1]))
        outputs = outputs / (K_.get_shape().as_list()[-1]**0.5)
		
		#以下是PADDING MASK的过程 不管是在Encoder的计算中还是Decoder的计算当中都会使用。
		
		#这个是整个PADDING MASK的核心知识点,当输入的自然语言句子转换成embedding向量的时候.
		#若embedding的时候,将用0填充的位置(PADDING)进行embedding的时候全部使用0来代替:
		#			举个例子:若一句自然语言用0填充之后的表示方式为[1,2,0,0],若embedding_size选择为2
		#						vocab_size为3.我们的lookup_table则初始化为如下的一个3*3的矩阵(第一行必须全部为0):
		#													[[0,0,0],
		#													[-0.12,0.21,0.32],
		#													[0.14,0.24,-0.32]]
		#						那么经过tf.nn.embedding_lookup()之后 原自然语言句子就被embedding变成了如下形式	
		#										[1,2,0,0]->[[-0.12,0.21,0.32],
		#													[0.14,0.24,-0.32],
		#													[0,0,0],
		#													[0,0,0]]
		# 				这样的话,通过如下的这几步的操作,便能将与key_masks为0的位置(即填充的位置)一样的output的位置变成一个特别小的一个负数.
		#			这样,经过后期的Softmax的时候,便能将该填充位置的输出变成0,以此来防止因为填充位置的无用信息影响模型的效果
		#			如果在最开始的embedding的同时没有使用0元素进行遮盖(即lookup_table矩阵第一行不为0,而是一些别的随机数)
		#			那么PADDING_MASK将不起作用.
        key_masks = tf.sign(tf.abs(tf.reduce_sum(keys, axis=-1)))
      	
      	#接下来的两步操作只是为了能够让key_masks的维度能够和outputs匹配
        key_masks = tf.tile(key_masks, [num_heads, 1])
        key_masks = tf.tile(tf.expand_dims(key_masks, 1), [1, tf.shape(queries)[1], 1])

		# paddings里面的值都是非常小的负数,当key_masks矩阵的某个变量和0相等的时候,将同样位置上的outputs的值变成一个特别小的数.
        paddings = tf.ones_like(outputs)*(-2**32+1)
        outputs = tf.where(tf.equal(key_masks, 0), paddings, outputs)
		outputs = tf.nn.softmax(outputs)
        query_masks = tf.sign(tf.abs(tf.reduce_sum(queries,axis=-1)))
        query_masks = tf.tile(query_masks,[num_heads,1])
        query_masks = tf.tile(tf.expand_dims(query_masks,-1),[1,1,tf.shape(keys)[1]])
        outputs *= query_masks

在进行softmax之后,源码当中又加入了一个query_mask.其实这里我是有点没太懂的,在这儿搬运一下其他博主说的话,若是有能理解的同学可以留言:原因在于若不是self-attention的情况下,query和key的第二维可能不同(两者句子长度不同)可能会出现一个填充,一个没有填充的状况出现,因此需要分情况讨论.但是keys_mask和query_mask都是一样的目的.

以上便是PADDING_MASK的理论解释以及在实际中的运用方法.

Sequence MASK

Sequence Mask只在Decoder端进行,目的是为了使得decoder不能看见未来的信息.也就是对于一个序列中的第i个token,解码的时候只能够依靠i时刻之前(包括i)的的输出,而不能依赖于i时刻之后的输出.因此我们要采取一个遮盖的方法(Mask)使得其在计算self-attention的时候只用i个时刻之前的token进行计算,因为Decoder是用来做预测的,而在训练预测能力的时候,我们不能够"提前看答案",因此要将未来的信息给遮盖住.效果如下图所示:
在这里插入图片描述
图6 mask之后的self-attention矩阵图

图中灰色部分即为mask的部分.实际操作的代码如图所示,再次强调,sequence mask 操作只发生在decoder阶段,而且对于self-attenion而言,这是一个必要的操作,不像之前的PADDING_MASK可以选择跳过:

			#	这遮盖的方法和之前的PADDING_MASK的操作方法基本一致,不一样的只是masks矩阵中的元素不一样,masks矩阵是一个
			#下三角矩阵,即对角线以及对角线一下是1,对角线以上全为0.之后同样将outputs位置对于Masks矩阵为0的位置上的元素统统
			#替换成一个很小的负数,这样之后在经过softmax的时候就能够将mask部分的self-attention的计算值变成0.以此来达到遮盖
			#未来信息的目的
            diag_vals = tf.ones_like(outputs[0, :, :])
            tril = tf.linalg.LinearOperatorLowerTriangular(diag_vals).to_dense()
            masks = tf.tile(tf.expand_dims(tril, 0), [tf.shape(outputs)[0], 1, 1])
            paddings = tf.ones_like(masks)*(-2**32+1)
            outputs = tf.where(tf.equal(masks, 0), paddings, outputs)

如果有能够来讨论交流的小伙伴欢迎在下面留言!!!好好学习天天向上!

  • 74
    点赞
  • 175
    收藏
    觉得还不错? 一键收藏
  • 16
    评论
以下是使用PyTorch实现TransformerSelf-Attention的示例代码: ## Self-Attention ```python import torch import torch.nn as nn class SelfAttention(nn.Module): def __init__(self, embed_size, heads): super(SelfAttention, self).__init__() self.embed_size = embed_size self.heads = heads self.head_dim = embed_size // heads assert (self.head_dim * heads == embed_size), "Embed size needs to be divisible by heads" self.values = nn.Linear(self.head_dim, self.head_dim, bias=False) self.keys = nn.Linear(self.head_dim, self.head_dim, bias=False) self.queries = nn.Linear(self.head_dim, self.head_dim, bias=False) self.fc_out = nn.Linear(heads * self.head_dim, embed_size) def forward(self, values, keys, queries, mask): # Get number of training examples N = queries.shape[0] value_len, key_len, query_len = values.shape[1], keys.shape[1], queries.shape[1] # Split embedding into self.heads pieces values = values.reshape(N, value_len, self.heads, self.head_dim) keys = keys.reshape(N, key_len, self.heads, self.head_dim) queries = queries.reshape(N, query_len, self.heads, self.head_dim) # Transpose to get dimensions batch_size * self.heads * seq_len * self.head_dim values = values.permute(0, 2, 1, 3) keys = keys.permute(0, 2, 1, 3) queries = queries.permute(0, 2, 1, 3) # Calculate energy energy = torch.matmul(queries, keys.permute(0, 1, 3, 2)) if mask is not None: energy = energy.masked_fill(mask == 0, float("-1e20")) # Apply softmax to get attention scores attention = torch.softmax(energy / (self.embed_size ** (1/2)), dim=-1) # Multiply attention scores with values out = torch.matmul(attention, values) # Concatenate and linearly transform output out = out.permute(0, 2, 1, 3).reshape(N, query_len, self.heads * self.head_dim) out = self.fc_out(out) return out ``` ## Transformer ```python import torch import torch.nn as nn from torch.nn.modules.activation import MultiheadAttention class TransformerBlock(nn.Module): def __init__(self, embed_size, heads, dropout, forward_expansion): super(TransformerBlock, self).__init__() self.attention = MultiheadAttention(embed_dim=embed_size, num_heads=heads) self.norm1 = nn.LayerNorm(embed_size) self.norm2 = nn.LayerNorm(embed_size) self.feed_forward = nn.Sequential( nn.Linear(embed_size, forward_expansion * embed_size), nn.ReLU(), nn.Linear(forward_expansion * embed_size, embed_size) ) self.dropout = nn.Dropout(dropout) def forward(self, value, key, query, mask): attention_output, _ = self.attention(query, key, value, attn_mask=mask) x = self.dropout(self.norm1(attention_output + query)) forward_output = self.feed_forward(x) out = self.dropout(self.norm2(forward_output + x)) return out class Encoder(nn.Module): def __init__(self, src_vocab_size, embed_size, num_layers, heads, device, forward_expansion, dropout, max_length): super(Encoder, self).__init__() self.embed_size = embed_size self.device = device self.word_embedding = nn.Embedding(src_vocab_size, embed_size) self.position_embedding = nn.Embedding(max_length, embed_size) self.layers = nn.ModuleList([ TransformerBlock(embed_size, heads, dropout, forward_expansion) for _ in range(num_layers) ]) self.dropout = nn.Dropout(dropout) def forward(self, x, mask): N, seq_length = x.shape positions = torch.arange(0, seq_length).expand(N, seq_length).to(self.device) out = self.dropout(self.word_embedding(x) + self.position_embedding(positions)) for layer in self.layers: out = layer(out, out, out, mask) return out class DecoderBlock(nn.Module): def __init__(self, embed_size, heads, forward_expansion, dropout, device): super(DecoderBlock, self).__init__() self.norm = nn.LayerNorm(embed_size) self.attention = MultiheadAttention(embed_size, heads) self.transformer_block = TransformerBlock(embed_size, heads, dropout, forward_expansion) self.dropout = nn.Dropout(dropout) def forward(self, x, value, key, src_mask, trg_mask): attention_output, _ = self.attention(x, x, x, attn_mask=trg_mask) query = self.dropout(self.norm(attention_output + x)) out = self.transformer_block(value, key, query, src_mask) return out class Decoder(nn.Module): def __init__(self, trg_vocab_size, embed_size, num_layers, heads, forward_expansion, dropout, device, max_length): super(Decoder, self).__init__() self.embed_size = embed_size self.device = device self.word_embedding = nn.Embedding(trg_vocab_size, embed_size) self.position_embedding = nn.Embedding(max_length, embed_size) self.layers = nn.ModuleList([ DecoderBlock(embed_size, heads, forward_expansion, dropout, device) for _ in range(num_layers) ]) self.fc_out = nn.Linear(embed_size, trg_vocab_size) self.dropout = nn.Dropout(dropout) def forward(self, x, enc_out, src_mask, trg_mask): N, seq_length = x.shape positions = torch.arange(0, seq_length).expand(N, seq_length).to(self.device) x = self.dropout(self.word_embedding(x) + self.position_embedding(positions)) for layer in self.layers: x = layer(x, enc_out, enc_out, src_mask, trg_mask) out = self.fc_out(x) return out ``` 这些代码可以用于实现TransformerSelf-Attention模型。但这只是示例,你需要根据你的数据和任务来调整这些代码的各种超参数和结构。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值