一、為什麼需要 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 矩陣有如下特點:
- 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 問題
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有一張效果圖:
值得注意的是,本來 mask 只需要二維的矩陣即可,但是考慮到我們的輸入序列都是批量的,所以我們要把原本二維的矩陣擴張成 3 維的張量。上面的代碼可以看出,我們已經進行了處理。
如果覺得文章對您有幫助,歡迎點贊,轉發,關注。
閱讀更多 隨時學丫 的文章