背景

Gemfield之前雲遊四方的時候,遇到一些名勝古蹟古文碑銘之類的,總是不禁會想——如果有個智慧眼鏡掃描到這類古文然後將其翻譯為現代白話文就好了。比如:

驃騎將軍逾居延至祁連山,捕首虜甚多。|o-o| 驃騎將軍越過居延澤,到達祁連山,捕獲了很多敵人。

吾乃Gemfield。|o-o| 我是這啥蠻夷文字。

子貢問曰:“富而無驕,貧而無諂,何如?”|o-o| 子貢問孔子說:“富有而不驕縱,貧窮而不諂媚,這樣的人怎麼樣?”。

|o-o| 是Gemfield戴的眼睛。沒錯,這就是一個經典的語言翻譯任務,也是NLP任務中常見的問題之一。NLP,目的是使機器像人那樣理解人類的語言,目前已經誕生了如下常見的研究領域:

文字的分類,比如輸入一段新聞,然後識別是社會新聞、體育新聞、政治新聞等;

語言翻譯,就像上面gemfield戴的|o-o|具備的功能;

語音識別(語音到文字);

命名實體識別(NER),識別一個句子中特定的名字實體;

文字情感分析,識別一段文字的態度、情緒、諷刺、困惑、懷疑等人類情感;

自然語言生成;

自動摘要;

文字校對;

問答系統/聊天系統。

Word Embedding

在NLP任務中,網路輸入在大多數情況下都是文字……的某種表示形式。這種表示形式具體是什麼呢?最直截了當的就是使用utf8編碼或者onehot編碼。但這樣的話,就只是冷冰冰的數字了,這個字的含義並沒有得到表示。什麼是字的含義?比如你見過“紅色蘋果”,也見過“藍色天空”,雖然沒見過“紅色天空”,但也能大致想象到這個場景,這就是文字的含義。我們需要找到一種方法,讓機器也能弄懂這樣的邏輯。

我們直接略過大段歷史,進入Word Embedding的世界。我們將一個字對映到N維的向量空間(比如128維)。我們並不知道每個維度代表什麼,因為每個維度上的值是靠模型自己來學習的。如果我們有10000個字,每個字用128維向量來表示,那麼10000x128的空間就能儲存下所有這些字的語義。為了舉例方便,我們來簡化下詞典,不要10000個字這麼多,假設就只有“吾乃gemfield”3個單詞——總共就3個字,每個字用4維向量來表示,也就是3x4,我們使用nn。Embedding(3, 4) 來表示:

import

torch

import

torch。nn

as

nn

word_dict

=

{

“吾”

0

“乃”

1

“gemfield”

2

}

embedding

=

nn

Embedding

3

4

lookup_tensor

=

torch

tensor

([

word_dict

“gemfield”

]],

dtype

=

torch

long

#查詢第0個向量,也就是gemfield這個單詞的embedding,4維

gemfield_embed

=

embedding

lookup_tensor

print

gemfield_embed

上面的程式碼中,我們透過索引2找到gemfield這個單詞的embedding(4維向量),列印結果如下:

gemfield@homepod:~$ python gemfield。py

tensor([[-1。5256, -0。7502, -0。6540, -1。6095]], grad_fn=

列印的這個4維向量就是“gemfield”這個token的embedding,只是這4個數字有什麼含義呢?沒有……至少目前沒有。要給它意義,就需要設計一個網路,再針對該網路設計出某種任務,讓embedding作為網路的輸入,然後訓練獲得在該任務上具備含義的embedding。我們常用以下兩種方式來進行embedding的訓練:

Skip-Gram

CBOW

Skip-Gram

方式是透過設定一個視窗來從語料庫中獲取訓練樣本,比如設定視窗大小為2,則對於“富而無驕,貧而無諂,何如”這個語料,我們可以獲取如下訓練樣本:

富 - 而;富 - 無;

而 - 富;而 - 無; 而 - 驕;

無 - 富;無 - 而; 無 - 驕;無 -, ;

……

CBOW

是continous bag of words的縮寫:連續詞袋模型(這裡提到的“詞袋”,其表達方式是在自然語言處理和資訊檢索IR下被簡化的表達模型,torch。nn模組下有nn。EmbeddingBag類與此對應。詞袋模型下,一段文字可以用一個裝著這些詞的袋子來表示,這種表示方式不考慮文法以及詞的順序)則是透過上下文來預測一個字。比如,給定一句話:“富而無驕,貧而無諂,何如”,我們把中間的字掩掉:“富而無驕,貧而[。。。]諂,何如”,被掩掉的字作為target。訓練的目的是讓模型預測出被掩掉的字大機率是“無“,稍微小一點的機率是“不”,等等,而預測出“又”、“CivilNet”這些字的機率則無限接近0。

如此以來,同樣的語料數量情況下,Skip-Gram方式獲得的訓練樣本要遠多於CBOW。再加上Skip-Gram生成訓練集的方式,使得其更適用於語料數量小、有生僻字的情況;而反過來,CBOW在頻繁出現的字上會表現出略微好的準確性。

此外,還有一個

N-Gram

方式,就是把一句話中前N個詞作為sample,第N+1個詞作為target。Gemfield來舉個例子:

syszux_yuliao = “”“晉太元中,武陵人捕魚為業。緣溪行,忘路之遠近。忽逢桃花林,夾岸數百步,中無雜樹,芳草鮮美,落英繽紛,漁人甚異之。復前行,欲窮其林。林盡水源,便得一山,山有小口,彷彿若有光。便舍船,從口入。初極狹,才通人。復行數十步,豁然開朗。土地平曠,屋舍儼然,有良田美池桑竹之屬。阡陌交通,雞犬相聞。其中往來種作,男女衣著,悉如外人。黃髮垂髫,並怡然自樂。見漁人,乃大驚,問所從來。具答之。便要還家,設酒殺雞作食。村中聞有此人,鹹來問訊。自雲先世避秦時亂,率妻子邑人來此絕境,不復出焉,遂與外人間隔。問今是何世,乃不知有漢,無論魏晉。此人一一為具言所聞,皆嘆惋。餘人各復延至其家,皆出酒食。停數日,辭去。此中人語云:“不足為外人道也。”既出,得其船,便扶向路,處處志之。及郡下,詣太守,說如此。太守即遣人隨其往,尋向所志,遂迷,不復得路。南陽劉子驥,高尚士也,聞之,欣然規往。未果,尋病終,後遂無問津者。”“”。strip()

train_dataset = [([syszux_yuliao[i], syszux_yuliao[i + 1], syszux_yuliao[i + 2]], syszux_yuliao[i + 3]) for i in range(len(syszux_yuliao) - 3)]

這樣一個簡單的訓練集就出來了,形式如下:

[([‘晉’, ‘太’, ‘元’], ‘中’), ([‘太’, ‘元’, ‘中’], ‘,’), ([‘元’, ‘中’, ‘,’], ‘武’)……]

可以看出來,這裡的N為3,根據前3個字預測第4個字。

為了用上Embedding,我們來設計個小的網路——NGramCivilNet:

#vocab_size 為詞典大小,這裡桃花源記一共213個字

#embedding_dim 設計為10

#sample_size 設計為3, target size為1

class NGramCivilNet(nn。Module):

def __init__(self, vocab_size, embedding_dim, sample_size):

super(NGramCivilNet, self)。__init__()

self。embeddings = nn。Embedding(vocab_size, embedding_dim)

self。linear1 = nn。Linear(sample_size * embedding_dim, 128)

self。linear2 = nn。Linear(128, vocab_size)

def forward(self, inputs):

embeds = self。embeddings(inputs)。view((1, -1))

out = F。relu(self。linear1(embeds))

out = self。linear2(out)

log_probs = F。log_softmax(out, dim=1)

return log_probs

在這個迷你型網路中,我們設計input的shape為torch。Size([3]),那麼對應的embedding就應該是[3, 10],我們reshape為[1, 30],送給Linear層。因為輸出是log_softmax已經加上log了,所以網路的損失函式就使用NLLLoss:

loss_function = nn。NLLLoss()

model = NGramCivilNet(213, 10, 3)

optimizer = optim。SGD(model。parameters(), lr=0。001)

完整訓練程式碼(只有40行程式碼)參考:

https://

github。com/CivilNet/Gem

field/blob/master/src/python/pytorch/embedding/simple_embedding。py

訓練完成後,我們可以列印一個字的Embedding,比如“山”字的Embedding:

print(model。embeddings。weight[word_dict[“山”]])

假設這個語料庫有上億,那“山”的embedding就很有意義了,可以固化下來直接用作其它任務的輸入。總的說來,詞嵌入(word embedding)是目前發展出來的最能有效編碼、最能代表一個詞的語義的技術,是NLP任務的基礎。假設經過有效的訓練,上文中提到過的“紅”和“紫”的embedding向量距離將很接近,而“藍色天空”向量 加上“紅色”和“藍色”向量的差,將能表示出“紅色天空”的語義。

Transformer

NLP任務的一大特點就是處理的是sequence,在transformer之前,已經有RNN和LSTM在這個領域各領風騷數年了,直到後來基於self-attention模組的transformer。我們已經知曉transformer的結構大概長什麼樣子了:

PyTorch的Transformer

將上述的transformer結構層層剝開:

在第一層,

transformer = embedding + 位置編碼(Positional Encoding) + encoder + decoder

在第二層,

encoder = 多個EncoderLayer = 多個(Multi-Head-Attention + LayerNorm + Residual連線 + FeedForwardNet)

decoder = 多個DecoderLayer = 多個(Masked Multi-Head-Attention + encoder-decoder Multi-Head-Attention + LayerNorm + Residual連線 + FeedForwardNet)

。其中,Layer Normalization和BatchNorm不同,BatchNorm是在一個batch裡所有sample的同一個維度上計算mean std,而LayerNorm不考慮batch,是在一個sample的不同維度上計算mean std;

在第三層,

Multi-Head-Attention = linear + scaled dot-product attention + concat + linear

在第四層,

scaled dot-product attention = MatMul + Scale + Mask + SoftMax + MatMul

總的說來,訓練集中蘊含的所有資訊,經過訓練,最終會提煉並落盤到Embedding、FeedForwardNet、Multi-Head-Attention的引數中。請允許gemfield把上述每個部分對應到PyTorch的模組上,然後逐一說來。

1,embedding

和傳統的序列模型類似,transformer中使用了可學習的embeddings來將輸入tokens轉化為維度大小為dmodel的向量:

#ntoken為輸入的字典數量, d_model為embedding大小

class Embeddings(nn。Module):

def __init__(self, d_model, ntoken):

super(Embeddings, self)。__init__()

self。embedding = nn。Embedding(ntoken, d_model)

self。d_model = d_model

def forward(self, x):

# x為[seq_len, batch_num]大小的輸入token矩陣,經此轉換後維度變為[seq_len, batch_num, d_model]

return self。embedding(x) * math。sqrt(self。d_model)

以“吾乃Gemfield”為例,這3個token經過embedding後,就會變成3x512的矩陣(假設dmodel為512)。在embedding層,我們還要將embedding的輸出乘以

\sqrt{dmodel}

self。embedding(x) * math。sqrt(self。d_model)

這個Embedding模組放在哪裡了呢?有兩處地方:encoder棧和decoder棧的最底層。在上述transformer架構圖中你也看到了,其中encoder最底層的地方我們使用Embedding(其實就是個look-up table)把輸入token序列(或矩陣)中的每個token轉換為dmodel維度的向量——這個很好理解,那麼decoder棧最底層的Embedding呢?當在訓練過程中,標籤(label或者target)經過Embedding進入Decoder;而在測試過程中,起始符會經過Embedding率先進入decoder(在第一個字元的預測中)。

2,位置編碼(PositionalEncoding)

既然transformer中沒有rnn和卷積,那麼如何利用輸入sequence的順序資訊呢? 為此transformer引入了位置編碼模組(PositionalEncoding),作用於encoder和decoder棧中embedding(位於encoder棧和decoder棧的底部)的輸出。在Transformer中,原作使用了sine和cosine函式,且位置編碼的輸出維度和embedding的維度是一樣的,這樣兩者可以被加起來,也就是:

輸入 = 輸入 + PE

PyTorch的Transformer

上述公式中,pos是位置索引(比如[seq_len, batch_num]中的0到seq_len,序列長度,比如“吾乃Gemfield”的seq_len為3)、i是維度索引(比如0到d_model)、dmodel是embedding的大小。從上面的公式也可以看出,positional encoding的每個維度上都有一個對應的正(餘)弦曲線。假設embedding的大小d_model是64,則對應著32個正弦曲線和32個餘弦曲線,且每一個正弦/餘弦的之間最顯著的區別就是變化週期的不同,維度越在後面的正弦曲線變化週期越慢。如此以來,

輸入 = 輸入 + PE

之後,輸入就被加上了獨特的和位置密切相關的“紋理”資訊 。PE程式碼實現如下:

class PositionalEncoding(nn。Module):

def __init__(self, d_model, dropout=0。1, max_len=5000):

super(PositionalEncoding, self)。__init__()

self。dropout = nn。Dropout(p=dropout)

pe = torch。zeros(max_len, d_model)

position = torch。arange(0, max_len, dtype=torch。float)。unsqueeze(1)

div_term = torch。exp(torch。arange(0, d_model, 2)。float() * (-math。log(10000。0) / d_model))

pe[:, 0::2] = torch。sin(position * div_term)

pe[:, 1::2] = torch。cos(position * div_term)

pe = pe。unsqueeze(0)。transpose(0, 1)

self。register_buffer(‘pe’, pe)

def forward(self, x):

x = x + self。pe[:x。size(0), :]

return self。dropout(x)

上述程式碼中,torch。exp(torch。arange(0, d_model, 2)。float() * (-math。log(10000。0) / d_model)) 實現了公式中的

10000^{2i/dmodel}

3,encoder

transformer中的encoder棧是由6個相同的encoder layer組成的,每個encoder layer是由2個sub-layer組成的,這2個sub-layer分別是:

多頭注意力模組(MultiheadAttention,相當於encoder自己的intra-attention):

#d_model為embedding size,nhead在論文中為8

MultiheadAttention(d_model, nhead)

全連線層:

#dim_feedforward的值為全連線層的維度,論文中為2048

self。linear1 = Linear(d_model, dim_feedforward)

self。linear2 = Linear(dim_feedforward, d_model)

EncoderLayer還在多頭注意力模組和全連線層這兩個sublayer上各引入了一個殘差連線(末尾再加上LayerNorm):

LayerNorm(x + sublayer(x) )

。encoder layer在PyTorch中被封裝為了TransformerEncoderLayer類:

class TransformerEncoderLayer(Module):

def __init__(self, d_model, nhead, dim_feedforward=2048, dropout=0。1, activation=“relu”,layer_norm_eps=1e-5):

super(TransformerEncoderLayer, self)。__init__()

self。self_attn = MultiheadAttention(d_model, nhead, dropout=dropout)

# Implementation of Feedforward model

self。linear1 = Linear(d_model, dim_feedforward)

self。dropout = Dropout(dropout)

self。linear2 = Linear(dim_feedforward, d_model)

self。norm_first = norm_first

self。norm1 = LayerNorm(d_model, eps=layer_norm_eps)

self。norm2 = LayerNorm(d_model, eps=layer_norm_eps)

self。dropout1 = Dropout(dropout)

self。dropout2 = Dropout(dropout)

self。activation = _get_activation_fn(activation)

def forward(self, src: Tensor, src_mask: Optional[Tensor] = None) -> Tensor:

#Multi head self attention

src2 = self。self_attn(src, src, src, attn_mask=src_mask)[0]

#第一個殘差連線

src = src + self。dropout1(src2)

# 追加layer norm

src = self。norm1(src)

# 全連線層

src2 = self。linear2(self。dropout(self。activation(self。linear1(src))))

# 第二個殘差連線

src = src + self。dropout2(src2)

# 追加layer norm

src = self。norm2(src)

return src

在TransformerEncoderLayer的forward邏輯中,可以看到很典型的多頭注意力 + 全連線層 + 殘差連線 + LayerNorm。對於一個EncoderLayer來說,輸入是一個sequence,輸出是一個相同維度的sequence。而在PyTorch的transformer實現中,整個encoder棧被封裝為了TransformerEncoder類:

class TransformerEncoder(Module):

def __init__(self, encoder_layer, num_layers):

super(TransformerEncoder, self)。__init__()

#使用ModuleList封裝了6個相同的encoder layer

self。layers = ModuleList([copy。deepcopy(encoder_layer) for _ in range(num_layers)])

def forward(self, src: Tensor, mask: Optional[Tensor] = None) -> Tensor:

output = src

for mod in self。layers:

output = mod(output, src_mask=mask)

return output

可以很明顯的看到多個encoder layer就是順序迴圈執行的。也就是說,對於整個encoder棧,同樣也是輸入一個sequence,輸出是一個同樣維度的sequence。輸出的sequence最後會作為decoder中encoder-decoder MultiheadAttention模組的輸入,這個輸入還有另外一個名字:memory。

4,decoder

transformer中的decoder棧是由6個相同的decoder layer組成的,和encoder layer由2個sub-layer組成不同,每個decoder layer是由3個sub-layer組成的,分別是:

帶掩碼的多頭注意力模組(相當於decoder自己的intra-attention,也就是Q = K = V):

#d_model為embedding size,nhead在論文中為8

MultiheadAttention(d_model, nhead)

注意,同樣是intra-attention,但和encoder不同,decoder中這裡前向的時候需要攜帶mask引數,所以在架構圖中這裡是Masked Multi-Head Attention模組。Mask的作用是防止後面的token參與前面token的注意力計算,也就是位置i的token的注意力計算只能使用位置0到位置i的token,因為decoder的使命之一就是用來預測下一個token,在訓練的時候不遮蔽掉就相當於模擬考試作弊呀。

encoder-decoder多頭注意力模組(

encoder-decoder attention

,K、V來自encoder輸出,Q來自decoder輸入):

#d_model為embedding size,nhead在論文中為8

MultiheadAttention(d_model, nhead)

等等,K、V來自encoder輸出是什麼意思?Gemfield後文會詳細解釋。

全連線層:

#dim_feedforward的值為全連線層的維度,論文中為2048

self。linear1 = Linear(d_model, dim_feedforward)

self。linear2 = Linear(dim_feedforward, d_model)

三個sub-layer就是這樣,整體都似曾相識。在PyTorch的transformer實現中,decoder layer被封裝為了TransformerDecoderLayer類:

class TransformerDecoderLayer(Module):

def __init__(self, d_model, nhead, dim_feedforward=2048, dropout=0。1, activation=“relu”,layer_norm_eps=1e-5) -> None:

super(TransformerDecoderLayer, self)。__init__()

self。self_attn = MultiheadAttention(d_model, nhead, dropout=dropout, batch_first=batch_first)

self。multihead_attn = MultiheadAttention(d_model, nhead, dropout=dropout, batch_first=batch_first)

# Implementation of Feedforward model

self。linear1 = Linear(d_model, dim_feedforward)

self。dropout = Dropout(dropout)

self。linear2 = Linear(dim_feedforward, d_model)

self。norm_first = norm_first

self。norm1 = LayerNorm(d_model, eps=layer_norm_eps)

self。norm2 = LayerNorm(d_model, eps=layer_norm_eps)

self。norm3 = LayerNorm(d_model, eps=layer_norm_eps)

self。dropout1 = Dropout(dropout)

self。dropout2 = Dropout(dropout)

self。dropout3 = Dropout(dropout)

self。activation = _get_activation_fn(activation)

def forward(self, tgt: Tensor, memory: Tensor, tgt_mask: Optional[Tensor] = None, memory_mask: Optional[Tensor] = None,

tgt_key_padding_mask: Optional[Tensor] = None, memory_key_padding_mask: Optional[Tensor] = None) -> Tensor:

# 第1個multi head self attention,也是masked MultiheadAttention

tgt2 = self。self_attn(tgt, tgt, tgt, attn_mask=tgt_mask,key_padding_mask=tgt_key_padding_mask)[0]

# 第1個殘差連線

tgt = tgt + self。dropout1(tgt2)

# 追加LayerNorm

tgt = self。norm1(tgt)

# 第2個multi head self attention,也就是encoder-decoder attention

tgt2 = self。multihead_attn(tgt, memory, memory, attn_mask=memory_mask,key_padding_mask=memory_key_padding_mask)[0]

# 第2個殘差連線

tgt = tgt + self。dropout2(tgt2)

# 追加LayerNorm

tgt = self。norm2(tgt)

# 全連線層

tgt2 = self。linear2(self。dropout(self。activation(self。linear1(tgt))))

# 第3個殘差連線

tgt = tgt + self。dropout3(tgt2)

# 追加LayerNorm

tgt = self。norm3(tgt)

return tgt

TransformerDecoderLayer中第一個Multi-Head Attention在下圖中被標註為了Masked Multi-Head Attention,這個Masked的含義前文已經說明。

PyTorch的Transformer

TransformerDecoderLayer中第二個Multi-Head Attention就不帶Masked關鍵字了,但它有一些自己的特點,三個輸入有兩個來自Encoder(k、v),這個具體的含義在下文會詳細解釋。decoder棧則被封裝為了TransformerDecoder類:

class TransformerDecoder(Module):

def __init__(self, decoder_layer, num_layers):

super(TransformerDecoder, self)。__init__()

self。layers = ModuleList([copy。deepcopy(decoder_layer) for _ in range(num_layers)])

def forward(self, tgt: Tensor, memory: Tensor, tgt_mask: Optional[Tensor] = None,memory_mask: Optional[Tensor] = None, tgt_key_padding_mask: Optional[Tensor] = None,memory_key_padding_mask: Optional[Tensor] = None) -> Tensor:

output = tgt

for mod in self。layers:

output = mod(output, memory, tgt_mask=tgt_mask, memory_mask=memory_mask, tgt_key_padding_mask=tgt_key_padding_mask, memory_key_padding_mask=memory_key_padding_mask)

return output

和encoder類似,decoder類也是多個decoder layer順序迴圈執行。注意在測試的時候,decoder的輸入剛開始只有起始符,只能預測出下一個token後再拼接起來迴圈往復,直到遇到最大長度或者是結束符才停止——這稱之為greedy decode。

5,MultiheadAttention

在這一小節,我們主要說下MultiheadAttention模組的核心——self attention。Gemfield使用前文的例子來說下self-attention機制。假設我們輸入的序列是[子貢問曰:“富而無驕,貧而無諂,何如?”],一共20個token(17個獨一無二的token,先不考慮起始結束符),那麼輸入就是20個embedding,我們記為e1_1、e1_2、e1_3、……e1_20。以e1_1為例,我們需要得到e1_1……e1_20分別對e1_1的影響力(attention score):

matrix_q * e1_1 = q1;

matrix_k * e1_1 = k1;

matrix_v * e1_1 = v1;

e1_2、……、e1_20 同理,得到q2、……、q20、k2、……、k20、v2、……、v20;

q1 dot_product k1、q1 dot_product k2、……、q1 dot_product k20分別得到a1_1、……、a1_20,這就是其它token對第一個token的影響力或者相關性(attention score);也就是說,token1和token2之間的相關性等於q1 * k2。

a1_1 * v1 + a1_2 * v2 + …… + + a1_20 * v20 得到 e2_1,也就是第一個token的向量經過上述self-attention計算得到第二代的第一個token對應的向量。可以看到,這就是一個加權和計算,加權和計算相當於把其它token的資訊按照重要與否的程度注入到了當前的token,也就是第二代中每個token都或多或少的包含了第一代中所有token的資訊;

上述所有的步驟描述瞭如何計算 e2_1,而e2_2、……、e2_20的計算同理;

上述所有的步驟描述瞭如何計算第二代token的向量,第三、四、五。。。代同理;

上述所有的步驟共享同一個matrix_q、matrix_k、matrix_v,後文我們用Wq、Wk、Wv代替。

還有一個值得注意的事項是,1、……、20的這些值,都是可以同時計算得到的。在矩陣運算中,我們只需要:

Q = Wq * E

K = Wk * E

V = Wv * E

A = K。t() * Q

E(下一代) = V * softmax(A)

現在的問題是:

為什麼會有Q、K、V這套機制?換言之,這個attention函式為什麼被設計成query、key、value的形式並且還能起作用?首先這些個術語來自於檢索系統(谷歌起家的領域),以知乎搜尋為例,query代表使用者在搜尋框中輸入的內容(當前token的q)、keys代表文章的標題/標籤詞等(各個token的k)、values代表匹配度最好的文章——比如gemfield專欄中的文章(各個token的v)。這只是說明了這些術語的來歷,而這些術語也能硬套在attention上:把一個query向量和一堆key-value(向量對)對映到一個output向量上。怎麼對映?output向量由value的加權和計算得出。那麼各個value的加權係數又怎麼來的?由query和key進行dot product計算得到的。如下圖所示:

PyTorch的Transformer

Gemfield:Scaled Dot-Product Attention

如果這個加權係數被限制為one-hot向量,那就是“知乎上搜索gemfield專欄文章”這個例子了。

為了計算其它token對某一個token的影響力,我們引入了Wq、Wk、Wv來將token embedding 線性變化對映到另外一個空間。為什麼要引入Wq、Wk、Wv?我們簡化來講,一個token embedding是相對固定的(訓練完成後,或者是來自上游任務),但是在不同的上下文中,一個token要表達出不一樣的含義。我們引入這三個可學習的Wq、Wk、Wv,就可以更好更靈活的把embedding對映到更合適的空間,可以做到更好的提取特徵表達含義。

為什麼q1 dot_product k1-20 得到的是其它token對第一個token的影響力?兩個向量的點積,比如a * b,其值為

a \cdot b = \left| a \right| \left| b \right| cos\theta

,這個

\theta

就是兩個向量的夾角,可以看到,夾角越小,向量餘弦距離越近,點積就越大。道理反過來講,兩個向量點積越大,說明其餘弦距離越近,向量就越相似。

MultiheadAttention

PyTorch的Transformer實現中,多頭注意力機制被封裝成了MultiheadAttention類。這個類我們關心兩個函式:建構函式和forward函式。

1,建構函式

1.1,TransformerEncoderLayer和TransformerDecoderLayer中構造MultiheadAttention例項

建構函式的常用引數是:

embed_dim:embedding的大小;

num_heads:論文中為8;

batch_first:預設為False,輸入輸出的tensor shape為(seq_len, batch, feature);如果為True,那麼輸入輸出的tensor shape為(batch, seq_len, feature)。

比如在Encoder、Decoder的3個MultiheadAttention例項中,MultiheadAttention的例項化方式都為

MultiheadAttention(d_model, nhead, batch_first=False)

#d_model為512,nhead為8

#encoder self attention

self。self_attn = MultiheadAttention(d_model, nhead, batch_first=False)

#decoder的masked self attention

self。self_attn = MultiheadAttention(d_model, nhead, batch_first=False)

#decoder中的encoder-decoder attention

self。multihead_attn = MultiheadAttention(d_model, nhead, batch_first=False)

1.2,MultiheadAttention構造時初始化了什麼

初始化了三個成員變數:

self。in_proj_weight = Parameter(torch。empty((3 * embed_dim, embed_dim)))

self。in_proj_bias = Parameter(torch。empty(3 * embed_dim))

self。out_proj = nn。Linear(embed_dim, embed_dim, bias=bias)

下文細說。

2,forward函式

MultiheadAttention例項在推理的時候要傳入什麼引數呢?我們知道,實際使用PyTorch Transformer類的時候,我們的呼叫棧是Transformer ——> TransformerEncoder/TransformerDecoder ——> TransformerEncoderLayer/TransformerDecoderLayer ——> MultiheadAttention,因此我們可以換種方式來問:

從資料集傳入Transformer模組的入參是什麼呢?

從外層的Transformer模組傳到裡面的TransformerEncoder和TransformerDecoder例項時,入參是什麼呢?

TransformerEncoder例項傳到TransformerEncoderLayer例項時,入參是什麼呢?TransformerDecoder例項傳到TransformerDecoderLayer例項時,入參是什麼呢?

從TransformerEncoderLayer例項傳到MultiheadAttention例項時,入參是什麼呢?從TransformerDecoderLayer例項傳到MultiheadAttention例項時,入參是什麼呢?

我們以下面這條資料集為例:

#假設有128條這樣的資料組成1個batch

驃騎將軍逾居延至祁連山,捕首虜甚多。|o-o| 驃騎將軍越過居延澤,到達祁連山,捕獲了很多敵人。

2.1,從資料集到transformer入口

從資料集過來的是token list,所以sample就可能是[70,30,2021,719,7030。。。],長度為18;target同理,假設長度為24。但是這種情況下沒有考慮batch,假設batch為128的話,那麼這個sample的size(先不考慮起始結束符)就是(128, 18),target的size為(128, 24);你肯定會問了,一個batch中的每個sample長度不一樣呀?沒關係,照著最長的padding。所以假設“驃騎將軍逾居延至祁連山,捕首虜甚多。”就是該batch中最長的句子,那麼這個sample batch送過來就是(128, 18)了;你肯定又會問了,padding的那些符號本沒有意義,那這些符號參與attention不是在干擾注意力嘛?沒關係,把padding的部分用padding mask遮蔽掉,不讓這些位置參與注意力計算;你肯定又會問了,你這裡提到的padding mask和前文說到的masked MultiheadAttention的mask是一回事嘛?不是,padding mask的作用這裡已經解釋了。masked MultiheadAttention的mask前文也解釋了,是防止decoder模擬考試作弊的,後文稱之為attn_mask或者position mask(和位置相關嘛)。總結下來就是,key_padding_mask是用來防止sequence中的PAD符號(補齊區域)被注意力顧及到,而attn_mask是用來防止sequence中特定位置被注意力顧及到,如果在推理時要預測sequence的下一個token(比如transformer的decoder),那麼在訓練時就要使用該mask(為三角形狀)來模擬推理時情況——防止“未來的、未知的token”影響當前的token。

好了,現在“驃騎將軍逾居延至祁連山,捕首虜甚多。”已經變成token matrix了。即將送往transformer,但是,一同送往transformer的引數還需要padding_mask和attn_mask,這兩個mask怎麼獲得呢?具體到這個智慧眼鏡任務中,就是encoder的padding_mask和decoder的padding_mask + attn_mask 怎麼得到呢?padding_mask就是哪裡pad了對應的mask矩陣中就置為True,attn_mask是一個下三角矩陣(這樣可以mask掉未來資訊對當前token的影響):

# 得到attn mask

def

generate_square_subsequent_mask

sz

):

return

torch

triu

torch

full

((

sz

sz

),

float

‘-inf’

)),

diagonal

=

1

還要注意一點,mask都是遮蔽某個位置影響當前token的,使命一樣,所以最後這兩個mask要合併。所以在智慧眼鏡任務中,encoder並不需要attn_mask,所以attn_mask就初始化為一個全是0的矩陣,如下所示:

import torch

def generate_square_subsequent_mask(sz):

return torch。triu(torch。full((sz, sz), float(‘-inf’)), diagonal=1)

def getPosAndPaddingMask(src, tgt, pad_idx=1):

src_seq_len = src。shape[0]

tgt_seq_len = tgt。shape[0]

tgt_mask = generate_square_subsequent_mask(tgt_seq_len)

src_mask = torch。zeros(src_seq_len, src_seq_len)。type(torch。bool)

src_padding_mask = (src == pad_idx)。transpose(0, 1)

tgt_padding_mask = (tgt == pad_idx)。transpose(0, 1)

return src_mask, tgt_mask, src_padding_mask, tgt_padding_mask

最後,這些mask的size分別為:

encoder的attn_mask:(18, 18)

encoder的padding_mask: (128, 18)

decoder的attn_mask: (24, 24)

decoder的padding_mask:(128, 24)

2.2,從transformer入口到TransformerEncoder和TransformerDecoder

在從transformer入口到TransformerEncoder和TransformerDecoder的過程中,還需要經過embedding和位置編碼模組。

類似“驃騎將軍逾居延至祁連山,捕首虜甚多。”這樣的128個樣本不是已經轉換為大小為(128, 18)的token矩陣了嘛,而對應的target(標籤)也差不多是(128, 24)大小的token矩陣。由於transformer實現預設不是batch first的,再加上 pad_sequence的實現也不是batch first的,所以實現上到transformer入口的時候,sample和target的維度已經分別變成了(18,128)和(24, 128)。 現在,它們雙雙來到embedding。

embedding的計算就是個查表操作,sample和target的維度(18,128)和(24, 128)經過embedding後(假設dmodel為512),維度於是變為(18,128, 512)和(24, 128, 512),疊加上位置編碼後維度不變。

現在,我們手頭有:

sample:(18,128, 512)

target:(24, 128, 512)

encoder的attn_mask:(18, 18)

encoder的padding_mask:(128, 18)

decoder的attn_mask:(24, 24)

decoder的padding_mask:(128, 24)

在Transformer類的forward中,有:

memory = self。encoder(src, mask=src_mask, src_key_padding_mask=src_key_padding_mask)

output = self。decoder(tgt, memory, tgt_mask,tgt_key_padding_mask,memory_key_padding_mask)

這裡面涉及到的引數有:

src,就是sample:(18,128, 512)

src_mask,就是encoder的attn_mask:(18, 18)

src_key_padding_mask,就是encoder的padding_mask:(128, 18)

tgt,就是target:(24, 128, 512)

memory,就是encoder的輸出,和輸入一樣,為(18,128, 512);

tgt_mask,就是decoder的attn_mask:(24, 24)

tgt_key_padding_mask,就是decoder的padding_mask:(128, 24)

memory_key_padding_mask,就是encoder的padding_mask:(128, 18)

2.3,從TransformerEncoder、TransformerDecoder到TransformerEncoderLayer、TransformerDecoderLayer

在TransformerEncoder的前向中,就是迴圈執行多個TransformerEncoderLayer:

output = src

for mod in self。layers:

output = mod(output, src_mask, src_key_padding_mask)

引數由上層透傳而來,不再贅述。在TransformerDecoder的前向中,就是迴圈執行多個TransformerDecoderLayer:

output = tgt

for mod in self。layers:

output = mod(output, memory, tgt_mask, tgt_key_padding_mask, memory_key_padding_mask)

引數由上層透傳而來,不再贅述。

2.4,從TransformerEncoderLayer、TransformerDecoderLayer到MultiheadAttention

在TransformerEncoderLayer的前向中,呼叫MultiheadAttention的地方:

self。self_attn(src, src, src, attn_mask=src_mask, key_padding_mask=src_key_padding_mask)[0]

我們有:

src,就是sample:(18,128, 512)

attn_mask,就是src_mask,就是encoder的attn_mask:(18, 18)

key_padding_mask,就是src_key_padding_mask,就是encoder的padding_mask:(128, 18)

在TransformerDecoderLayer的前向中,呼叫MultiheadAttention的地方:

#masked self-attention

self。self_attn(tgt, tgt, tgt, attn_mask=tgt_mask, key_padding_mask=tgt_key_padding_mask)[0]

#encoder-decoder attention

#memory為encoder的輸出

self。multihead_attn(tgt, memory, memory, attn_mask=memory_mask, key_padding_mask=memory_key_padding_mask)[0]

我們有:

tgt,就是target:(24, 128, 512)

memory,就是encoder的輸出,和輸入一樣,為(18,128, 512);

tgt_mask,就是decoder的attn_mask:(24, 24)

tgt_key_padding_mask,就是decoder的padding_mask:(128, 24)

memory_key_padding_mask,就是encoder的padding_mask:(128, 18)

memory_mask為None。

2.5 歡迎來到MultiheadAttention的推理

2.5.1 MultiheadAttention forward函式的輸入輸出

MultiheadAttention的forward函式的輸入引數有:

query:shape為(L, N, E),其中L是sequence的長度(在self-attention中就是輸入sequence的長度,L=S;在encoder-decoder attention中就是decoder輸入的sequence的長度),N是batch size,E是embedding的大小;以上文的驃騎將軍舉例,在encoder的self-attention中就是(18,128, 512),在decoder的masked self-attention中是(24, 128, 512),在decoder的encoder-decoder attention中,它來自前面masked self-attention的輸出,也是(24, 128, 512);

key:shape為(S,N,E),其中S是source sequence的長度(在self-attention中就是輸入sequence的長度,L=S;在encoder-decoder attention中就是encoder輸出的sequence的長度),N和E同上;以上文的驃騎將軍舉例,在encoder的self-attention中就是(18,128, 512),在decoder的masked self-attention中就是(24, 128, 512),在decoder的encoder-decoder attention中就是(18,128, 512)——來自encoder的輸出;

value:shape為(S,N,E),S、N、E同上;以上文的驃騎將軍舉例,在encoder的self-attention中就是(18,128, 512),在decoder的masked self-attention中就是(24, 128, 512),在decoder的encoder-decoder attention中就是(18,128, 512)——來自encoder的輸出;

key_padding_mask:shape為(N, S),N和S同上;當在EncoderLayer中時,這個是src_key_padding_mask,以上文的驃騎將軍舉例,這個size為(128, 18);當在DecoderLayer的masked self-attention中時,這個是tgt_key_padding_mask,比如(128, 24);在DecoderLayer的encoder-decoder attention中時,這個是memory_key_padding_mask(也就是src_key_padding_mask),以上文的驃騎將軍舉例,這個size為(128, 18)。這幾個padding mask([src/tgt/memory]_key_padding_mask)指示的位置應該被注意力計算忽略掉。具體來說,如果傳參了ByteTensor型別, 非零值指示的位置將被忽略(不參與注意力計算),而零值指示的位置將繼續參與注意力計算;如果傳參的是BoolTensor型別,True指示的位置將被忽略(不參與注意力計算),而False指示的位置將繼續參與注意力計算;

attn_mask:可以是2D mask和3D mask。當為2D mask的時候,shape為(L,S),在self-attention中,L=S;當為3D mask的時候,shape為(N*num_heads, L, S)。當在EncoderLayer中時,這個是src_mask,以驃騎將軍舉例就是(18, 18);當在DecoderLayer的masked self-attention中時,這個是tgt_mask,以驃騎將軍舉例就是(24, 24);在DecoderLayer的encoder-decoder attention中時,這個預設是None,如果要傳參的話大小應該是(24,18)。[src/tgt/memory]_mask確保了只有未被mask的區域才會參加注意力計算:如果該引數是ByteTensor,非零值指示的位置不參與注意力計算,零指示的位置將繼續參與;如果是BoolTensor,True指示的位置不參與注意力計算,False指示的位置將繼續參與;如果提供的是FloatTensor ,將會被直接加到attention weight上(比如-inf加上去後,attention weight相應的位置也變成-inf,然後softmax之後就變為0了,也就是不參與注意力計算)。

MultiheadAttention的forward函式的返回值有兩個:

attn_output:shape為(L, N, E),含義同上。這個是attention計算的輸出;

attn_output_weights:shape為(N, L, S),含義同上。這是attention的權重。

返回值為含有上述兩個元素的tuple,一般只取第一個,所有會有取[0]操作。

2.5.2 MultiheadAttention forward函式如何被呼叫

EncoderLayer中呼叫該forward函式的方式為:

#取[0]是因為forward返回一個tuple,第1個元素為attention的輸出

x = self_attn(src, src, src, attn_mask=src_mask, key_padding_mask=src_key_padding_mask)[0]

可見query、key、value均為encoder的同一個輸入sequence,以驃騎將軍為例,大小為(18,128, 512)。

在DecoderLayer的masked self-attention例項上,呼叫forward函式的方式為:

x = self_attn(tgt, tgt, tgt, attn_mask=tgt_mask, key_padding_mask=tgt_key_padding_mask)[0]

可見query、key、value均為decoder的同一個輸入sequence,以驃騎將軍為例,大小為(24,128, 512)。

在Decoder的encoder-decoder attention例項上,呼叫forward函式的方式為:

x = attn(tgt, memory, memory, attn_mask=memory_mask,key_padding_mask=memory_key_padding_mask)[0]

query為DecoderLayer中masked self-attention的輸出,而key、value來自memory,也就是encoder的輸出。

2.6,注意力的真正計算

MultiheadAttention的forward函式中並沒有實現真正的計算,真正的計算過程是實現在了F。multi_head_attention_forward函式中:

attn_output, attn_output_weights = F。multi_head_attention_forward(

query, key, value,

self。embed_dim, self。num_heads,

self。in_proj_weight, self。in_proj_bias,

self。out_proj。weight, self。out_proj。bias,

key_padding_mask=key_padding_mask, attn_mask=attn_mask)

其中,self。in_proj_weight, self。in_proj_bias來自如下的定義(這正是MultiheadAttention例項化的時候初始化的權重):

self。in_proj_weight = Parameter(torch。empty((3 * embed_dim, embed_dim)))

self。in_proj_bias = Parameter(torch。empty(3 * embed_dim))

首先可以看到這兩個是要參與訓練的引數,其次它們的shape為(E*3, E)。為啥是E*3呢(以512維的embedding size為例,就是1536)?因為把query、key、value的線性變換矩陣Wq、Wk、Wv放到了一個parameter裡——我們稱之為packed weights。

實參self。out_proj。weight, self。out_proj。bias傳遞給F。multi_head_attention_forward的out_proj_weight、out_proj_bias,而self。out_proj。weight, self。out_proj。bias自身又來自MultiheadAttention例項化時候的初始化:

self。out_proj = nn。Linear(embed_dim, embed_dim, bias=bias)

也就是in_features和out_features的大小都是embed_dim(比如512)。所以我們知道了,一個MultiheadAttention例項中要訓練的引數有:

self。in_proj_weight,使用packed weights的形式一次性包裝了Wq、Wk、Wv;引數數量512 * 3 *512 = 786432;6個EncoderLayer + 6個DecoderLayer 就是18個MultiheadAttention例項,總共 14155776個引數,float32型別表示的話就是大約54M;

self。in_proj_bias,使用packed weights的形式一次性包裝了Wq、Wk、Wv對應的bias;引數數量512 * 3 = 1536;

self。out_proj的引數數量為 512 * 512 + 512 = 262656,相當於一個Wq/Wk/Wv;

2.6.1,F.multi_head_attention_forward的引數檢查

在F。multi_head_attention_forward這個函式中,要對輸入引數做一些檢查,這些檢查也蘊含了一些特別的意義:

query的shape為(L, N, E),而該函式的第4個傳參為self。embed_dim,看出來了吧,query的shape的第3個數字E的值和self。embed_dim必須要相同,這是自然而然的;

key的shape必須和value的shape相同;在self-attention模組中,query = key = value,而在encoder-decoder attention模組中,query來自decoder的輸入,key = value 來自encoder的輸出(也就是memory);

attn_mask引數,承載position mask的資訊,來自於[src/tgt/memory]_mask,必須是BoolTensor/FloatTensor/ByteTensor(ByteTensor會被轉換為BoolTensor)型別;意義前文已經講過;

key_padding_mask是ByteTensor或者BoolTensor,如果傳入的是ByteTensor,則會被轉換為BoolTensor;shape必須為(batch_size, src_len);

2.6.2,F.multi_head_attention_forward的attention結構

Gemfield從論文《Attention Is All You Need》中直接把Multi-Head Attention的流程圖抄過來,如下所示:

PyTorch的Transformer

可以看到,Q、K、V過來之後要先進行線性變換(這就是self。in_proj_weight起作用的地方,這變數名起的真好:輸入對映),然後再送給Scaled Dot-Product Attention模組,這個模組的輸出最後再送給一個Linear模組(這就是self。out_proj起作用的地方,這變數名起的不錯:輸出對映)。看來核心就是Scaled Dot-Product Attention模組了,Gemfield再從論文中把Scaled Dot-Product Attention的流程圖抄過來:

PyTorch的Transformer

可見經過線性變換的Q、K連續經過MatMul ——> Scale ——> Mask ——> SoftMax計算,然後和V進行MatMul進行計算——下面我們就按圖索驥。

2.6.3,F.multi_head_attention_forward的attention計算

下面是真正的計算,F。multi_head_attention_forward的輸入是query、key、value、self。in_proj_weight, self。in_proj_bias、self。out_proj,從輸入開始說起,計算如下:

q, k, v = _in_projection_packed(query, key, value, in_proj_weight, in_proj_bias)

2.6.3.1,_in_projection_packed函式的使用

這個函式的輸入是:query, key, value 和in_proj_weight, in_proj_bias,輸出是:q, k, v。這個輸入輸出表明瞭一個事實:我們要把query、key、value線性變換到q、k、v並且這個線性變換矩陣的引數是參與訓練的。也就是說,我們使用packed weights將query、key、value線性變換為了為q、k、v,並且輸入的query、key、value的shape和輸出的q、k、v的shape相同。

內容也很簡單,我們知道,在transformer中一共用到了三種attention,其中encoder和decoder的masked self-attention(也就是query和key、value相同的時候)在使用

_in_projection_packed

函式時候的邏輯為:

#query和key相同的時候

q, k, v = linear(q, w, b)。chunk(3, dim=-1)

torch。nn。functional。linear函式接收3個引數:input、weight、bias

它們的shape為:

Input: (N, *, in_features),N是batch size, * 代表了額外維度的大小。在這個上下文中,具體的shape為(L, N, E);

Weight: (out_features, in_features),比如上文提到了weight的shape為(E*3, E),就說明out_features是E*3,所以……

Bias: (out_features),

Output: (N, *, out_features),out_features是E*3,所以我們需要在最後一個維度上進行chunk(3)操作,把一個(N, *, E*3)輸出變為三個(N, *, E)。在這個上下文中,就是三個(L, N, E)或者三個(S, N, E),因為L = S嘛。

而在decoder的encoder-decoder attention(也就是query和key、value不同的時候),我們的邏輯為:

w_q, w_kv = w。split([E, E * 2])

b_q, b_kv = b。split([E, E * 2])

q, k, v = (linear(q, w_q, b_q),) + linear(k, w_kv, b_kv)。chunk(2, dim=-1)

可以看到,由於query和key/value(memory)不同,我們讓query用一個weight和bias,而key、value用一個weight和bias;所以輸出就是:

q是(L, N, E),k/v是(S, N , E)。

2.6.3.2,reshape q, k, v 且確保batch first

q = q。contiguous()。view(tgt_len, bsz * num_heads, head_dim)。transpose(0, 1)

k = k。contiguous()。view(-1, bsz * num_heads, head_dim)。transpose(0, 1)

v = v。contiguous()。view(-1, bsz * num_heads, head_dim)。transpose(0, 1)

也就是說,如果head是1的話,那麼q、k、v的shape會保持不變,僅僅transpose了L/S和N的維度,也就是使得batch first;如果是多個head的話,則batch維度從batch_size上升到batch_size * nhead,而embedding維度從E下降到E//nhead,然後再經過transpose變成batch first。

2.6.3.3,合併key_padding_mask和attn_mask

前文說過,key_padding_mask和attn_mask的動機不一樣,但使命是一樣的。所以在這一步要將padding mask的使命注入到position mask上。經過這一步合併後,key_padding_mask的影響力就注入到attn_mask上了,key_padding_mask自身至此消亡。也就是key_padding_mask和attn_mask融合後變成了新的attn_mask。

既然討論到key_padding_mask和attn_mask的合併,不妨先看看這兩者到此時的shape:

key_padding_mask:(batch_size, src_len),也就是(N, S);

attn_mask:2D的時候為(L, S),然後在這一步之前透過unsqueeze(0)操作將shape變為了(1, L, S);而3D的時候為(N*num_heads, L, S);

為了融合,key_padding_mask必須reshape成attn_mask的形狀。這個reshape是透過如下的操作完成的:

# key_padding_mask為(batch_size, src_len)或者(N, S)

key_padding_mask

。view(batch_size, 1, 1, src_len)

。expand(-1, num_heads, -1, -1)

。reshape(batch_size * num_heads, 1, src_len)

這裡的expand API呼叫中,-1表示在這個維度上不變,也就是在第0、2、3維度上保持不變,而在第1個維度上從1 擴充套件到num_heads。怎麼擴充套件呢?複製。所以假設key_padding_mask的shape初始為為(2, 3),也就是:

#初始

[ [False, False, True], [False, False, True] ]

#view後

[ [[[False, False, True]]], [[[False, False, True]]] ]

#expand後,假設num_heads是2,所以shape變為(2, 2, 1, 3)

[ [ [[False, False, True]], [[False, False, True]] ],

[ [ [False, False, True]], [[False, False, True]] ] ]

#reshape為(batch_size * num_heads, 1, src_len),也就是(4,1,3)

[[False, False, True]],

[[False, False, True]],

[[False, False, True]],

[[False, False, True]]]

至此,shape為(1, L, S)或者(N*num_heads, L, S)的attn_mask 就要和 shape為(batch_size * num_heads, 1, src_len)的key_padding_mask融合了:

如果attn_mask為BoolTensor(或者ByteTensor)的話,則attn_mask = attn_mask。logical_or(key_padding_mask);

如果attn_mask為FloatTensor的話,則attn_mask = attn_mask。masked_fill(key_padding_mask, float(“-inf”));

不管是logical_or還是masked_fill,attn_mask和key_padding_mask在第0和1個維度上的大小不一致,預示著將在第0和1個維度上產生broadcast操作,也就是說,新的attn_mask的shape為

(batch_size * num_heads, L, S)

。而且,FloatTensor的masked_fill的使用(對應BoolTensor的或操作)意味著:

FloatTensor中的-inf對應著BoolTensor的True

接下來,如果新attn_mask為bool型別的話,還需要轉換為FloatTensor:

new_attn_mask = torch。zeros_like(attn_mask, dtype=torch。float)

new_attn_mask。masked_fill_(attn_mask, float(“-inf”))

attn_mask = new_attn_mask

直接將attn_mask中True對應的位置改成了FloatTensor中的-inf。這個負無窮是什麼含義呢?既然我們已經有了線性變換後的q、k、v,也有了準備好的mask,那麼我們就要進行scaled dot product操作了。而在這個操作中,-inf的含義將得到展現。

2.6.3.4,scaled dot product

scaled dot product的計算是透過如下API完成的:

attn_output, attn_output_weights = _scaled_dot_product_attention(q, k, v, attn_mask)

這個函式計算q、k、v這3個Tensor的scaled dot product。輸入q、k、v、attn_mask的shape分別為:

q:大小為(batch_size * nhead, L, E/nhead),nhead在論文中為8,以上文的驃騎將軍為例,在EncoderLayer的self-attention模組中時,這裡是(1024, 18, 64);在DecoderLayer的self-attention、encoder-decoder attention模組中時,這裡是(1024, 24, 64);

k:大小為(batch_size * nhead, S, E/nhead),以上文的驃騎將軍為例,在EncoderLayer的self-attention模組中時,這裡是(1024, 18, 64);在DecoderLayer的self-attention模組中時,這裡是(1024, 24, 64);在DecoderLayer的encoder-decoder attention模組中時,這裡是(1024, 18, 64);

v:大小為(batch_size * nhead, S, E/nhead),以上文的驃騎將軍為例,在EncoderLayer的self-attention模組中時,這裡是(1024, 18, 64);在DecoderLayer的self-attention模組中時,這裡是(1024, 24, 64);在DecoderLayer的encoder-decoder attention模組中時,這裡是(1024, 18, 64);

attn_mask:大小為(batch_size * nhead, L, S)或者(batch_size * nhead, 1, S);以上文的驃騎將軍為例,在EncoderLayer的self-attention模組中時,這裡是(1024, 18, 18);在DecoderLayer的self-attention模組中時,這裡是(1024, 24, 24);在DecoderLayer的encoder-decoder attention模組中時,這裡是(1024, 1, 18)。

輸出為attention values和attention weights,shape分別為:

attention values:大小為(batch_size * nhead, L, E/nhead),以上文的驃騎將軍為例,在EncoderLayer的self-attention模組中時,這裡是(1024, 18, 64);在DecoderLayer的self-attention模組、encoder-decoder attention模組中時,這裡是(1024, 24, 64);

attention weights:(batch_size * nhead, L, S),以上文的驃騎將軍為例,在EncoderLayer的self-attention模組中時,這裡是(1024, 18, 18);在DecoderLayer的self-attention模組中時,這裡是(1024, 24, 24);在DecoderLayer的encoder-decoder attention模組中時,這裡是(1024, 24, 18)。

scaled dot product計算步驟如下:

PyTorch的Transformer

q = q / math。sqrt(E);

k = k。transpose(-2, -1);把k的shape從(batch_size * nhead, S, E/nhead) 變為 (batch_size * nhead, E/nhead,S),這是為了下一步dot product時輸入引數的維度匹配;

attention_weights = torch。bmm(q, k);bmm函式不會做broadcast操作,因此q、k引數的shape(batch_size * nhead, L, E/nhead) 、 (batch_size * nhead, E/nhead, S)中的batch_size必須一致,torch。bmm輸出的attention_weights的shape為(batch_size*nhead, L, S);

attention_weights += attn_mask;attn_mask中-inf標記的地方相加之後在attention_weights中也是-inf;

attention_weights = softmax(attn, dim=-1);dim=-1表明是在S維度上做softmax,並且-inf的地方經過softmax後變為了0,也就是這個地方不產生加權影響,這正是mask的作用;

attention_values = torch。bmm(attention_weights, v);又是一個dot product操作,attention_weights的shape為(batch_size * nhead, L, S)、v的shape為(batch_size * nhead, S, E/nhead) ,輸出的attention_values的shape為(batch_size *nhead, L, E/nhead)——

這正好是q的shape

如此以來,我們就得到了attention values和attention weights的值。

2.6.3.5,attention values和attention weights的後處理

attention values首先從batch first轉換為之前的非batch first,然後從多head變為之前的(L, N, E),然後經過輸出線性變換矩陣(正是前文提到的名字起的不錯的self。out_proj)進行線性變換,輸出仍為(L, N, E),程式碼如下:

attention_values = attention_values。transpose(0, 1)。contiguous()。view(tgt_len, batch_size, embed_dim)

attention_values = linear(attention_values, out_proj_weight, out_proj_bias)

而attention weights的shape為(batch_size * nhead, L, S),然後reshape為(batch_size, num_heads, L, S),然後在num_heads的維度上

相加取平均繼而消除掉head這個維度

,shape變成了(batch_size, L, S),程式碼如下:

attention_weights = attention_weights。view(batch_size, num_heads, tgt_len, src_len)

attention_weights = attention_weights。sum(dim=1) / num_heads

但是我們在TransformEncoderLyaer和TransformDecoderLayer中並沒有使用attention_weights的值(只使用了attention_values)。

翻譯任務的演進

|o-o| 這個智慧眼鏡的演算法部分現已開源:

目前這只是個實踐PyTorch transformer的初級專案。那麼如果持續投入資源的話,且網路結構限定transformer的話,那麼演進路徑應該是什麼呢?Gemfield覺得大概得從以下方面著筆:

使用大量的古文資料集,訓練古文單字的詞向量;

使用大量的現代文資料集,訓練現代文單字的詞向量;

使用更多的古今翻譯資料集,訓練完整的transformer;

教育部或者哪個部門應該出面牽頭組建全球最大的中文NLP資料集,既能產生權威的資料集品牌,又避免了各個公司重複投資產生資源浪費。