一、為什么需要 Mask?
在此,先思考一個問題,為什么需要 mask?
在 NLP 中,一個最常見的問題便是輸入序列長度不等,通常需要進行 PAD 操作,通常在較短的序列后面填充 0,雖然 RNN 等模型可以處理不定長輸入,但在實踐中,需要對 input 做 batchsize,轉(zhuǎn)換成固定的 tensor。
PAD 案例:
如下是兩句英文,先將文本轉(zhuǎn)換成數(shù)字
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)
對文本進行數(shù)字編碼
[[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 結(jié)果
tensor([[3, 3],
[1, 4],
[7, 5],
[0, 6],
[0, 7]])
以句子 ”He likes cats“ 的 PAD 結(jié)果舉例:[3, 1, 7, 0, 0],PAD 操作會引起以下幾個問題。
1. mean-pooling 的問題
如上述案例所示,對于矩陣:
對 s1 進行 mean-pooling:
進行 pad 之后:
對 進行 mean-pooling:
對比 和
發(fā)現(xiàn):pad 操作影響 mean-pooling。
2. max-pooling 的問題
對于矩陣 s1: ,PAD 之后:
分別對 和
進行 max-pooling:
對比 和
發(fā)現(xiàn):pad 操作影響 max-pooling。
3. attention 的問題
通常在 Attention 計算中最后一步是使用 softmax 進行歸一化操作,將數(shù)值轉(zhuǎn)換成概率。但如果直接對 PAD 之后的向量進行 softmax,那么 PAD 的部分也會分攤一部分概率,這就導致有意義的部分 (非 PAD 部分) 概率之和小于等于 1。
二、Mask 為解決 PAD 問題順應而生
Mask 是相對于 PAD 而產(chǎn)生的技術,具備告訴模型一個向量有多長的功效。Mask 矩陣有如下特點:
- Mask 矩陣是與 PAD 之后的矩陣具有相同的 shape。
- 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 問題
2. 解決 max-pooling 問題
在進行 max-pooling 時,只需要將 pad 的部分的值足夠小即可,可以將 mask 矩陣中的值為 0 的位置替換的足夠小 ( 如: ,則不會影響 max-pooling 計算。
3. 解決 Attention 問題
該問題的解決方式跟 max-pooling 一樣,就是將 pad 的部分足夠小,使得 的值非常接近于 0,以至于忽略。
二、常見的 Mask 有哪些?
有了之前的鋪墊,你應該明白 Mask 因何而產(chǎn)生,有什么作用,而在 NLP 任務中,因為功能不同,Mask 也會不同。
常見的 Mask 有兩種,Padding-mask,用于處理不定長輸入,也即是上面講的第一種,另一種則是 seqence-mask,為了防止未來信息不被泄露。接下來將詳細講解這兩種 Mask。
padding mask - 處理輸入不定長
在 NLP 中,一個常見的問題是輸入序列長度不等,一般來說我們會對一個 batch 內(nèi)的句子進行 PAD,通常值為 0。但在前面我們也講過,PAD 為 0 會引起很多問題,影響最后的結(jié)果,因此,Mask 矩陣為解決 PAD 問題而產(chǎn)生。
舉個例子:
case 1: I like cats.
case 2: He does not like cats.
假設默認的 seq_len 是5,一般會對 case 1 做 pad 處理,變成
[1, 1, 1, 0, 1]
在上述例子數(shù)字編碼后,開始做 embedding,而 pad 也會有 embedding 向量,但 pad 本身沒有實際意義,參與訓練可能還是有害的。
因此,有必要維護一個 mask tensor 來記錄哪些是真實的 value,上述例子的兩個 mask 如下:
1 1 1 0 0
1 1 1 1 1
后續(xù)再梯度傳播中,mask 起到了過濾的作用,在 pytorch 中,有參數(shù)可以設置:
nn.Embedding(vocab_size, embed_dim,padding_idx=0)
sequence mask - 防止未來信息泄露
在語言模型中,常常需要從上一個詞預測下一個詞,sequence mask 是為了使得 decoder 不能看見未來的信息。也就是對于一個序列,在 time_step 為 t 的時刻,我們的解碼輸出應該只能依賴于 t 時刻之前的輸出,而不能依賴 t 之后的輸出。因此我們需要想一個辦法,把 t 之后的信息給隱藏起來。
那么具體怎么做呢?也很簡單:產(chǎn)生一個上三角矩陣,上三角的值全為 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有一張效果圖:

值得注意的是,本來 mask 只需要二維的矩陣即可,但是考慮到我們的輸入序列都是批量的,所以我們要把原本二維的矩陣擴張成 3 維的張量。上面的代碼可以看出,我們已經(jīng)進行了處理。
如果覺得文章對您有幫助,歡迎點贊,轉(zhuǎn)發(fā),關注。
