一、为什么需要 Mask?

在此,先思考一个问题,为什么需要 mask?

在 NLP 中,一个最常见的问题便是输入序列长度不等,通常需要进行 PAD 操作,通常在较短的序列后面填充 0,虽然 RNN 等模型可以处理不定长输入,但在实践中,需要对 input 做 batchsize,转换成固定的 tensor。

PAD 案例:

如下是两句英文,先将文本转换成数字

s1 = 'He likes cats'
s2 = 'He does not like cats'
s = s1.split(' ') + s2.split(' ')
word_to_id = dict(zip(s, range(len(s))))
id_to_word = dict((k,v) for v,k in word_to_id.items())
# {'He': 3, 'likes': 1, 'cats': 7, 'does': 4, 'not': 5, 'like': 6}
# {3: 'He', 1: 'likes', 7: 'cats', 4: 'does', 5: 'not', 6: 'like'}
s1_vector = [word_to_id[x] for x in s1.split(' ')]
s2_vector = [word_to_id[x] for x in s2.split(' ')]
sentBatch = [s1_vector, s2_vector]
print(sentBatch)

对文本进行数字编码

[[3, 1, 7], [3, 4, 5, 6, 7]]

对如上两个 vector 进行 pad 处理。

from torch.nn.utils.rnn import pad_sequence

a = torch.tensor(s1_vector)

b = torch.tensor(s2_vector)

pad = pad_sequence([a, b])

print(pad)

PAD 结果

tensor([[3, 3],

[1, 4],

[7, 5],

[0, 6],

[0, 7]])

以句子 ”He likes cats“ 的 PAD 结果举例:[3, 1, 7, 0, 0],PAD 操作会引起以下几个问题。

1. mean-pooling 的问题

如上述案例所示,对于矩阵:s1 = [3, 1, 7]

对 s1 进行 mean-pooling:mean_{s1}=(3+1+7)/3=3.667

进行 pad 之后:pad_{s1}=[3,1,7,0,0]

对 pad_ {s1} 进行 mean-pooling:pad_{s1}=(3+1+7+0+0)/10=1.1

对比 mean_ {s1} 和 pad_{s1} 发现:pad 操作影响 mean-pooling。

2. max-pooling 的问题

对于矩阵 s1: s1 = [-3, -1, -7],PAD 之后:pad_{s1}= [-3,-1,-7,0,0]

分别对 s1 和 pad_ {s1} 进行 max-pooling:max{s1}=-1,max{pad_{s1}}=0

对比 mean_ {s1} 和 pad_{s1} 发现:pad 操作影响 max-pooling。

3. attention 的问题

通常在 Attention 计算中最后一步是使用 softmax 进行归一化操作,将数值转换成概率。但如果直接对 PAD 之后的向量进行 softmax,那么 PAD 的部分也会分摊一部分概率,这就导致有意义的部分 (非 PAD 部分) 概率之和小于等于 1。

二、Mask 为解决 PAD 问题顺应而生

Mask 是相对于 PAD 而产生的技术,具备告诉模型一个向量有多长的功效。Mask 矩阵有如下特点:

  1. Mask 矩阵是与 PAD 之后的矩阵具有相同的 shape。
  2. mask 矩阵只有 1 和 0两个值,如果值为 1 表示 PAD 矩阵中该位置的值有意义,值为 0 则表示对应 PAD 矩阵中该位置的值无意义。

在第一部分中两个矩阵的 mask 矩阵如下所示:

mask_s1 = [1, 1, 1, 0, 0]
mask_s2 = [1, 1, 1, 1, 1]
mask = a.ne(torch.tensor(paddingIdx)).byte()
print(mask)
>>> tensor([[1, 1],
[1, 1],
[1, 1],
[0, 1],
[0, 1]], dtype=torch.uint8)

1. 解决 mean-pooling 问题

mean_s1=sum(pad_{s1}*m)/sum(m)

2. 解决 max-pooling 问题

在进行 max-pooling 时,只需要将 pad 的部分的值足够小即可,可以将 mask 矩阵中的值为 0 的位置替换的足够小 ( 如: 甚至负无穷,则不会影响 max-pooling 计算。

max_b=max(pad_b-(1-m)*10^{-10})

3. 解决 Attention 问题

该问题的解决方式跟 max-pooling 一样,就是将 pad 的部分足够小,使得 的值非常接近于 0,以至于忽略。

softmax(x)=softmax(x-(1-m)*10^{10})

二、常见的 Mask 有哪些?

有了之前的铺垫,你应该明白 Mask 因何而产生,有什么作用,而在 NLP 任务中,因为功能不同,Mask 也会不同。

常见的 Mask 有两种,Padding-mask,用于处理不定长输入,也即是上面讲的第一种,另一种则是 seqence-mask,为了防止未来信息不被泄露。接下来将详细讲解这两种 Mask。

padding mask - 处理输入不定长

在 NLP 中,一个常见的问题是输入序列长度不等,一般来说我们会对一个 batch 内的句子进行 PAD,通常值为 0。但在前面我们也讲过,PAD 为 0 会引起很多问题,影响最后的结果,因此,Mask 矩阵为解决 PAD 问题而产生。

举个例子:

case 1: I like cats.

case 2: He does not like cats.

假设默认的 seq_len 是5,一般会对 case 1 做 pad 处理,变成

[1, 1, 1, 0, 1]

在上述例子数字编码后,开始做 embedding,而 pad 也会有 embedding 向量,但 pad 本身没有实际意义,参与训练可能还是有害的。

因此,有必要维护一个 mask tensor 来记录哪些是真实的 value,上述例子的两个 mask 如下:

1 1 1 0 0

1 1 1 1 1

后续再梯度传播中,mask 起到了过滤的作用,在 pytorch 中,有参数可以设置:

nn.Embedding(vocab_size, embed_dim,padding_idx=0)

sequence mask - 防止未来信息泄露

在语言模型中,常常需要从上一个词预测下一个词,sequence mask 是为了使得 decoder 不能看见未来的信息。也就是对于一个序列,在 time_step 为 t 的时刻,我们的解码输出应该只能依赖于 t 时刻之前的输出,而不能依赖 t 之后的输出。因此我们需要想一个办法,把 t 之后的信息给隐藏起来。

那么具体怎么做呢?也很简单:产生一个上三角矩阵,上三角的值全为 1,下三角的值全为 0,对角线也是 0。把这个矩阵作用在每一个序列上,就可以达到我们的目的啦。

一个常见的 trick 就是生成一个 mask 对角矩阵,如[1]:

def sequence_mask(seq):
batch_size, seq_len = seq.size()
mask = 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

哈佛大学的文章The Annotated Transformer有一张效果图:


Vector Autosar MemMap组件配置_数字编码


值得注意的是,本来 mask 只需要二维的矩阵即可,但是考虑到我们的输入序列都是批量的,所以我们要把原本二维的矩阵扩张成 3 维的张量。上面的代码可以看出,我们已经进行了处理。