自然语言处理(NLP)-模型常用技巧:Mask【Padding Mask、Subsequent Mask】

对于NLP中 Mask 的作用(ps:padding mask 和 Subsequent mask不是官方命名):

  • Padding mask:处理非定长序列,区分padding和非padding部分,如在RNN等模型和Attention机制中的应用等
  • Subsequent mask:防止标签泄露,如:Transformer decoder中的mask矩阵,BERT中的[Mask]位,XLNet中的mask矩阵等

一、Padding Mask【处理非定长序列】

在NLP中,文本一般是不定长的,所以在进行 batch训练之前,要先进行长度的统一,过长的句子可以通过truncating 截断到固定的长度,过短的句子可以通过 padding 增加到固定的长度,但是 padding 对应的字符只是为了统一长度,并没有实际的价值,因此希望在之后的计算中屏蔽它们,这时候就需要 Mask。

在这里插入图片描述
上图为中文场景下,一个 batch=5 的,以字为单位的输入矩阵(也可以在分词后以词为单位)和 mask 矩阵,左图已经将文本 padding 到统一长度了,右图中的1表示有效字,0代表无效字。

1、RNN中的Mask

对于RNN等模型,本身是可以直接处理不定长数据的,因此它不需要提前告知 sequence length,如下是pytorch下的LSTM定义:

nn.LSTM(input_size, hidden_size, *args, **kwargs)

但是在实践中,为了 batch 训练,一般会把不定长的序列 padding 到相同长度,再用 mask 去区分非 padding 部分和 padding 部分。

区分的目的是使得RNN只作用到它实际长度的句子,而不会处理无用的 padding 部分,这样RNN的输出和隐状态都会是对应句子实际的最后一位。另外,对于token级别的任务,也可以通过mask去忽略 padding 部分对应的loss。

不过,在 pytorch 中,对 mask 的具体实现形式不是mask矩阵,而是通过一个句子长度列表来实现的,但本质一样。

RNN在处理类似变长的句子序列的时候,我们就可以配套使用以下函数来避免padding对句子表示的影响:

  • torch.nn.utils.rnn.pack_padded_sequence()
  • torch.nn.utils.rnn.pad_packed_sequence()

1.1 为什么RNN需要处理变长输入

假设我们有情感分析的例子,对每句话进行一个感情级别的分类,主体流程大概是下图所示:

在这里插入图片描述
思路比较简单,但是当我们进行batch个训练数据一起计算的时候,我们会遇到多个训练样例长度不同的情况,这样我们就会很自然的进行padding,将短句子padding为跟最长的句子一样。

比如向下图这样:
在这里插入图片描述
但是这会有一个问题,什么问题呢?比如上图,句子“Yes”只有一个单词,但是padding了5的pad符号,这样会导致LSTM对它的表示通过了非常多无用的字符,这样得到的句子表示就会有误差,更直观的如下图:
在这里插入图片描述
那么我们正确的做法应该是怎么样呢?

这就引出pytorch中RNN需要处理变长输入的需求了。在上面这个例子,我们想要得到的表示仅仅是LSTM过完单词"Yes"之后的表示,而不是通过了多个无用的“Pad”得到的表示:如下图:

在这里插入图片描述

1.2 pytorch中RNN如何处理变长padding

主要是用函数torch.nn.utils.rnn.pack_padded_sequence()以及torch.nn.utils.rnn.pad_packed_sequence()来进行的,分别来看看这两个函数的用法。

这里的pack,理解成压紧比较好。 将一个 填充过的变长序列 压紧。(填充时候,会有冗余,所以压紧一下)

输入的形状可以是(T×B×* )。T是最长序列长度,B是batch size,代表任意维度(可以是0)。如果batch_first=True的话,那么相应的 input size 就是 (B×T×)。

Variable中保存的序列,应该按序列长度的长短排序,长的在前,短的在后(特别注意需要进行排序)。即input[:,0]代表的是最长的序列,input[:, B-1]保存的是最短的序列。

参数说明:

input (Variable) – 变长序列 被填充后的 batch

lengths (list[int]) – Variable 中 每个序列的长度。(知道了每个序列的长度,才能知道每个序列处理到多长停止)

batch_first (bool, optional) – 如果是True,input的形状应该是BTsize。

返回值:

一个PackedSequence 对象。一个PackedSequence表示如下所示:

在这里插入图片描述
具体代码如下:

embed_input_x_packed = pack_padded_sequence(embed_input_x, sentence_lens, batch_first=True)
encoder_outputs_packed, (h_last, c_last) = self.lstm(embed_input_x_packed)

其中:sentence_lens 表示的是这个batch中每一个句子的实际长度。

此时,返回的h_last和c_last就是剔除padding字符后的hidden state和cell state,都是Variable类型的。代表的意思如下(各个句子的表示,lstm只会作用到它实际长度的句子,而不是通过无用的padding字符,下图用红色的打钩来表示):
在这里插入图片描述
但是返回的output是PackedSequence类型的,可以使用:

encoder_outputs, _ = pad_packed_sequence(encoder_outputs_packed, batch_first=True)

将encoderoutputs在转换为Variable类型,得到的_代表各个句子的长度。

2、Attention中的Mask

在 Attention 机制中,同样需要忽略 padding 部分的影响,这里以transformer encoder中的self-attention为例:

self-attention中,Q和K在点积之后,需要先经过mask再进行softmax,因此,对于要屏蔽的部分,mask之后的输出需要为负无穷,这样softmax之后输出才为0。
在这里插入图片描述

def attention(query, key, value, mask=None, dropout=None):
    "Compute 'Scaled Dot Product Attention'"
    d_k = query.size(-1)
    scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
    if mask is not None:
        scores = scores.masked_fill(mask == 0, -1e9) # mask步骤,用 -1e9 代表负无穷
    p_attn = F.softmax(scores, dim = -1)
    if dropout is not None:
        p_attn = dropout(p_attn)
    return torch.matmul(p_attn, value), p_attn

二、Subsequent mask【防止标签泄露】

在语言模型中,常常需要从上一个词预测下一个词,但如果要在LM中应用 self attention 或者是同时使用上下文的信息,要想不泄露要预测的标签信息,就需要 mask 来“遮盖”它。不同的mask方式,也对应了一篇篇的paper,这里选取典型的几个。

1、Transformer中的Mask

Transformer 是包括 Encoder和 Decoder的,

  • Encoder中 self-attention 只需要 padding mask,
  • Decoder 不仅需要 padding mask,还需要防止标签泄露,即在 t 时刻不能看到 t 时刻之后的信息,因此在上述 padding mask的基础上,还要加上 Subsequent mask。

Subsequent mask 一般是通过生成一个上三角为0的矩阵来实现的,上三角区域对应要mask的部分。

在Transformer 的 Decoder中,先不考虑 padding mask,一个包括四个词的句子[A,B,C,D]在计算了相似度scores之后,得到下面第一幅图,将scores的上三角区域mask掉,即替换为负无穷,再做softmax得到第三幅图。这样,比如输入 B 在self-attention之后,也只和A,B有关,而与后序信息无关。

因为在softmax之后的加权平均中: B’ = 0.48A+0.52B,而 C,D 对 B’不做贡献。
在这里插入图片描述
实际应用中,Decoder 需要结合 padding mask 和 Subsequent mask,下面在pytorch框架下以一个很简化的例子展示 Transformer 中 的两种 Mask。

import torch


def padding_mask(sentence, pad_idx):
    mask = (sentence != pad_idx).int().unsqueeze(-2)  # [B, 1, L]
    return mask


def subsequent_mask(sentence):
    batch_size, seq_len = sentence.size()
    mask = 1 - torch.triu(torch.ones((seq_len, seq_len), dtype=torch.uint8), diagonal=1)
    mask = mask.unsqueeze(0).expand(batch_size, -1, -1)  # [B, L, L]
    return mask


def test():
    # 以最简化的形式测试Transformer的两种mask
    sentence = torch.LongTensor([[1, 2, 5, 8, 3, 0]])  # batch_size=1, seq_len=3,padding_idx=0
    embedding = torch.nn.Embedding(num_embeddings=50000, embedding_dim=300, padding_idx=0)
    query = embedding(sentence)
    key = embedding(sentence)

    scores = torch.matmul(query, key.transpose(-2, -1))
    print("\nscores = \n", scores)

    mask_p = padding_mask(sentence, 0)
    mask_s = subsequent_mask(sentence)
    print("mask_p = \n", mask_p)
    print("mask_s = \n", mask_s)

    mask_encoder = mask_p
    mask_decoder = mask_p & mask_s  # 结合 padding mask 和 Subsequent mask
    print("mask_encoder = \n", mask_encoder)
    print("mask_decoder = \n", mask_decoder)

    scores_encoder = scores.masked_fill(mask_encoder == 0, -1e9)  # 对于scores,在mask==0的位置填充-1e9
    scores_decoder = scores.masked_fill(mask_decoder == 0, -1e9)  # 对于scores,在mask==0的位置填充-1e9

    print("scores_encoder = \n", scores_encoder)
    print("scores_decoder = \n", scores_decoder)

test()

对应的各mask值为:

scores = 
 tensor([[[256.9339,  -4.1667,  21.3342,  -9.0958,   2.4808,   0.0000],
         [ -4.1667, 285.8431, -20.5250, -10.3684,  18.9549,   0.0000],
         [ 21.3342, -20.5250, 291.5568,  18.6109,  15.0059,   0.0000],
         [ -9.0958, -10.3684,  18.6109, 279.6891, -20.5765,   0.0000],
         [  2.4808,  18.9549,  15.0059, -20.5765, 287.6636,   0.0000],
         [  0.0000,   0.0000,   0.0000,   0.0000,   0.0000,   0.0000]]],
       grad_fn=<UnsafeViewBackward>)
mask_p = 
 tensor([[[1, 1, 1, 1, 1, 0]]], dtype=torch.int32)
mask_s = 
 tensor([[[1, 0, 0, 0, 0, 0],
         [1, 1, 0, 0, 0, 0],
         [1, 1, 1, 0, 0, 0],
         [1, 1, 1, 1, 0, 0],
         [1, 1, 1, 1, 1, 0],
         [1, 1, 1, 1, 1, 1]]], dtype=torch.uint8)
mask_encoder = 
 tensor([[[1, 1, 1, 1, 1, 0]]], dtype=torch.int32)
mask_decoder = 
 tensor([[[1, 0, 0, 0, 0, 0],
         [1, 1, 0, 0, 0, 0],
         [1, 1, 1, 0, 0, 0],
         [1, 1, 1, 1, 0, 0],
         [1, 1, 1, 1, 1, 0],
         [1, 1, 1, 1, 1, 0]]], dtype=torch.int32)
scores_encoder = 
 tensor([[[ 2.5693e+02, -4.1667e+00,  2.1334e+01, -9.0958e+00,  2.4808e+00,
          -1.0000e+09],
         [-4.1667e+00,  2.8584e+02, -2.0525e+01, -1.0368e+01,  1.8955e+01,
          -1.0000e+09],
         [ 2.1334e+01, -2.0525e+01,  2.9156e+02,  1.8611e+01,  1.5006e+01,
          -1.0000e+09],
         [-9.0958e+00, -1.0368e+01,  1.8611e+01,  2.7969e+02, -2.0577e+01,
          -1.0000e+09],
         [ 2.4808e+00,  1.8955e+01,  1.5006e+01, -2.0577e+01,  2.8766e+02,
          -1.0000e+09],
         [ 0.0000e+00,  0.0000e+00,  0.0000e+00,  0.0000e+00,  0.0000e+00,
          -1.0000e+09]]], grad_fn=<MaskedFillBackward0>)
scores_decoder = 
 tensor([[[ 2.5693e+02, -1.0000e+09, -1.0000e+09, -1.0000e+09, -1.0000e+09,
          -1.0000e+09],
         [-4.1667e+00,  2.8584e+02, -1.0000e+09, -1.0000e+09, -1.0000e+09,
          -1.0000e+09],
         [ 2.1334e+01, -2.0525e+01,  2.9156e+02, -1.0000e+09, -1.0000e+09,
          -1.0000e+09],
         [-9.0958e+00, -1.0368e+01,  1.8611e+01,  2.7969e+02, -1.0000e+09,
          -1.0000e+09],
         [ 2.4808e+00,  1.8955e+01,  1.5006e+01, -2.0577e+01,  2.8766e+02,
          -1.0000e+09],
         [ 0.0000e+00,  0.0000e+00,  0.0000e+00,  0.0000e+00,  0.0000e+00,
          -1.0000e+09]]], grad_fn=<MaskedFillBackward0>)


============================== 1 passed in 0.80s ==============================

Process finished with exit code 0

可以看到

  • mask_decoder 的第6列为0 ,对应padding mask;
  • mask_decoder 上三角为0,对应Subsequent mask;

对于Decoder,在batch训练时会同时需要padding mask和Subsequent mask。

在测试时为单例,仅需要加上Subsequent mask,这样可以固定住每一步生成的词,让前面生成的词在self-attention时不会包含后面词的信息,这样也使测试和训练保持了一致。

不过,在测试过程中,预测生成的句子长度逐步增加,因此每一步都会要生成新的Subsequent mask矩阵的,是维度逐步增加的下三角方阵。

2、BERT中的Mask

BERT实际上是Transformer的Encoder,为了在语言模型的训练中,使用上下文信息又不泄露标签信息,采用了Masked LM,简单来说就是随机的选择序列的部分token用 [Mask] 标记代替。
在这里插入图片描述
BERT之后,也有不少在Mask的范围和形式上做文章的,比如:ERNIE,但大同小异,不多赘述。

3、XLNet中的Mask

XLNet的Mask操作就非常的巧(nan)妙(dong)。XLNet通过Permutation Language Modeling实现了不在输入中加[Mask]标记,同样可以利用上下文信息,关键的操作就是下面所示的 Attention Mask 机制。

在这里插入图片描述

但并不是那么好理解,要理解XLNet中的Mask,一定要先看张俊林老师的:XLNet:运行机制及和Bert的异同比较,再来看下面的内容。上图也是引自该文,这里再引用一句我认为非常关键的一段话:

在Transformer内部,通过Attention掩码,从X的输入单词里面,也就是Ti的上文和下文单词中,随机选择i-1个,放到Ti的上文位置中,把其它单词的输入通过Attention掩码隐藏掉,于是就能够达成我们期望的目标(当然这个所谓放到Ti的上文位置,只是一种形象的说法,其实在内部,就是通过Attention Mask,把其它没有被选到的单词Mask掉,不让它们在预测单词Ti的时候发生作用,如此而已。看着就类似于把这些被选中的单词放到了上文Context_before的位置了)

对于排列序列:3->2->4->1,通过 Attention Mask,在 self-attention 的加权平均计算中,以上图中的 E 2 ′ E_{2^{'}} E2 为例:

self-attention 计算之后Content stream中的 E 2 ′ = a 2 E 2 + a 3 E 3 E_{2^{'}}=a_2E_2+a_3E_3 E2=a2E2+a3E3,其中 E 2 E_2 E2 表示第2个词对应的向量,其他同理。这样在 E 2 ′ E_{2^{'}} E2 中就看到了它的下文 E 3 E_3 E3,就好像是把 E 3 E_3 E3 放到了它的上文位置一样,但实际顺序并没有改变。

4、UniLM中的Mask

UniLM (Unified Language Model),从它的名字就足以见它的野心。而它也确实仅用Mask,就让BERT可以同时构建双向语言模型,单向语言模型和seq2seq语言模型。

其实,上面三种LM的差异,就在于训练时能利用哪些信息,具体实现上,UniLM就通过Mask来控制信息的利用,语言模型的学习上,和BERT一样,还是通过完形填空,预测被mask位去学习。下图很形象的展示了上述三种LM:
在这里插入图片描述
上图的中间展示的就是不同LM的Self-Attention Mask 矩阵(没有考虑 padding mask,仅有Subsequent mask,假设被mask位为0):

双向语言模型:和BERT一样,可以是一句或两句话,全1的mask矩阵,即可以看到上下文所有信息

单向语言模型:仅一句话,从左到右预测的话是上三角为0的mask矩阵,仅能看到上文

seq2seq语言模型:需要两句话,第一句话可以看到本身所有信息,但看不到第二句话任何信息;第二句话可以看到第一句话所有信息,本身只能看到上文。所以它的mask矩阵包括四个部分,从左到右,从上到下分别是:全0,全1,全0,上三角为1。

上面三种LM中,最巧妙地是seq2seq语言模型,在不改变BERT原有结构的情况下,做到了类seq2seq的形式,充分利用第一句的信息,去帮助第二句的预测,可以很好地提升BERT在NLG方面的效果,论文中的实验效果也验证了这点。




参考资料:
pytorch中如何处理RNN输入变长序列padding
NLP 中的Mask全解
XLNet:运行机制及和Bert的异同比较
XLNet 中神奇的 Attention Mask

Guess you like

Origin blog.csdn.net/u013250861/article/details/120950371