變壓器(Transformer)

變壓器(Transformer)

圖 1。

在前面的章節中,我們介紹了主要的神經網路架構,例如卷積神經網路(CNN)和遞迴神經網路(RNN)。讓我們回顧一下他們的利弊:

CNN易於在層上並行化,但不能很好地捕獲可變長度順序依賴關係。

RNN能夠捕獲遠端、可變長度的順序資訊,但無法在序列內並行化。

為了結合CNN和RNN的優勢,Vaswani 使用注意力機制設計了一種新穎的架構。這種稱為Transformer的結構透過注意捕獲重複序列來實現並行化,同時對序列中每個位置進行編碼。結果,Transformer成為了一個相容模型,訓練時間大大縮短。

Transformer也基於編碼器-解碼器結構。但是,Transformer與前者的不同之處在於,將seq2seq中的迴圈層替換為多頭注意層,透過位置編碼合併位置資訊,並使用層歸一化。我們在圖1中並排比較了Transformer和seq2seq。

總體而言,這兩個模型彼此相似:源序列嵌入被輸入到N個重複的塊中。然後將最後一塊的輸出用作解碼器的注意特徵。將目標序列嵌入類似地輸入到解碼器中的N個重複塊中,並透過將具有詞彙量的密集層應用於最後一個塊的輸出來獲得最終輸出。

另一方面,Transformer與seq2seq的注意力模型不同之處在於:

變壓器塊:seq2seq中的迴圈層被變壓器塊代替。 對於編碼器,該塊包含一個多頭注意層和兩個位置前饋網路的網路層。對於解碼器,另一個多頭注意層用於獲取編碼器狀態。

加法和歸一化層:多頭注意層或位置前饋網路的輸入和輸出均由兩個“加法和歸一化層”處理,該層包含殘差結構和歸一化層。

位置編碼:由於自我注意力層無法區分序列中的特徵順序,因此使用位置編碼層將序列資訊新增到每個序列特徵中。

1。 多頭注意力

變壓器(Transformer)

圖 2。

在討論多頭注意力層之前,讓我們快速表達一下自我注意結構。 自我注意模型是普通的注意模型,其查詢、鍵和值從順序輸入的每個項中完全相同地複製。 正如我們在圖2中所說明的,對於每個輸入項,自我注意輸出相同長度的順序輸出。 與迴圈層相比,可以平行計算自我注意層的輸出項,因此易於獲得高效的實現。

變壓器(Transformer)

圖 3。

變壓器(Transformer)

from d2l import mxnet as d2limport mathfrom mxnet import autograd, np, npxfrom mxnet。gluon import nnnpx。set_np()def transpose_qkv(X, num_heads): # input X shape: (batch_size, seq_len, num_hiddens) # output X shape: (batch_size * num_heads, seq_len, num_hiddens / num_heads) X = X。reshape(X。shape[0], X。shape[1], num_heads, -1) X = X。transpose(0, 2, 1, 3) output = X。reshape(-1, X。shape[2], X。shape[3]) return outputdef transpose_output(X, num_heads): X = X。reshape(-1, num_heads, X。shape[1], X。shape[2]) X = X。transpose(0, 2, 1, 3) return X。reshape(X。shape[0], X。shape[1], -1)class MultiHeadAttention(nn。Block): def __init__(self, num_hiddens, num_heads, dropout, use_bias=False, **kwargs): super(MultiHeadAttention, self)。__init__(**kwargs) self。num_heads = num_heads self。attention = d2l。DotProductAttention(dropout) self。W_q = nn。Dense(num_hiddens, use_bias=use_bias, flatten=False) self。W_k = nn。Dense(num_hiddens, use_bias=use_bias, flatten=False) self。W_v = nn。Dense(num_hiddens, use_bias=use_bias, flatten=False) self。W_o = nn。Dense(num_hiddens, use_bias=use_bias, flatten=False) def forward(self, query, key, value, valid_len): query = transpose_qkv(self。W_q(query), self。num_heads) key = transpose_qkv(self。W_k(key), self。num_heads) value = transpose_qkv(self。W_v(value), self。num_heads) output = self。attention(query, key, value, valid_len) output_concat = transpose_output(output, self。num_heads) return self。W_o(output_concat)

2。 位置前饋網路

變壓器模組中的另一個關鍵元件稱為位置前饋網路(FFN)。 它接受具有形狀為 ( 批處理大小 X 序列長度 X 特徵大小) 的3維輸入。 位置FFN由應用於最後一個維度的兩個全連線層組成。 由於序列中的每個位置項都使用相同的兩個全連線層,因此我們將其稱為位置感知的。 實際上,這等效於應用兩個1×1卷積層。

下面,PositionWiseFFN顯示瞭如何使用兩個分別具有隱藏大小ffn_num_hiddens和pw_num_outputs的全連線層來實現位置FFN。

class PositionWiseFFN(nn。Block): def __init__(self, ffn_num_hiddens, pw_num_outputs, **kwargs): super(PositionWiseFFN, self)。__init__(**kwargs) self。dense1 = nn。Dense(ffn_num_hiddens, flatten=False, activation=‘relu’) self。dense2 = nn。Dense(pw_num_outputs, flatten=False) def forward(self, X): return self。dense2(self。dense1(X))

與多頭注意類似,按位置前饋網路將僅更改輸入的最後維度大小-特徵維度。 另外,如果輸入序列中的兩項相同,則相應的輸出也將相同。

除了Transformer塊中的上述兩個元件外,該塊中的“ add and norm”還起著平穩連線其他層的輸入和輸出的關鍵作用。 為了說明,我們在多頭注意力層和位置FFN網路之後添加了一個包含殘差結構的層和一個層歸一化。 層歸一化與批處理歸一化相似。 一個區別是,沿著最後一個維度(例如X。mean(axis=-1))而不是第一個批次維度(例如X。mean(axis=0))計算層歸一化的平均值和方差。 層歸一化可以防止層中的值範圍變化太大,從而可以更快地進行訓練並具有更好的泛化能力。

MXNet在nn塊中實現了LayerNorm和BatchNorm。 讓我們將它們都呼叫,並在下面的示例中看到區別。

變壓器(Transformer)

現在,讓我們一起實現連線塊AddNorm。 AddNorm接受兩個輸入 和 。 我們可以將X視為殘差網路中的原始輸入,將 視為多頭注意力層或位置FFN網路的輸出。 另外,我們在應用dropout進行正則化。

class AddNorm(nn。Block): def __init__(self, dropout, **kwargs): super(AddNorm, self)。__init__(**kwargs) self。dropout = nn。Dropout(dropout) self。ln = nn。LayerNorm() def forward(self, X, Y): return self。ln(self。dropout(Y) + X)

4. 位置編碼

與迴圈層不同,多頭注意層和位置前饋網路均獨立計算序列中各項的輸出。 此功能使我們能夠並行化計算,但無法為給定序列建模序列資訊。 為了更好地捕獲順序資訊,變壓器模型使用位置編碼來維護輸入序列的位置資訊。

位置 是一個二維矩陣,其中 表示句子中的順序, 表示沿嵌入向量位置。 這樣就可以使用以下等式維護原始序列中的每個值:

變壓器(Transformer)

class PositionalEncoding(nn。Block): def __init__(self, num_hiddens, dropout, max_len=1000): super(PositionalEncoding, self)。__init__() self。dropout = nn。Dropout(dropout) # Create a long enough P self。P = np。zeros((1, max_len, num_hiddens)) X = np。arange(0, max_len)。reshape(-1, 1) / np。power( 10000, np。arange(0, num_hiddens, 2) / num_hiddens) self。P[:, :, 0::2] = np。sin(X) self。P[:, :, 1::2] = np。cos(X) def forward(self, X): X = X + self。P[:, :X。shape[1], :]。as_in_ctx(X。ctx) return self。dropout(X)

5。 解碼器

變壓器解碼器塊看起來與變壓器編碼器塊相似。 但是,除了兩個子層(多頭注意層和位置編碼網路)之外,解碼器塊還包含第三子層,該子層將多頭注意應用於編碼器堆疊的輸出。 與編碼器塊類似,解碼器塊採用“加法和歸一化”,即殘差連線和層歸一化來連線每個子層。

變壓器(Transformer)

圖 5。

變壓器(Transformer)

在訓練期間,查詢t的輸出可以觀察到所有先前的鍵值對。 它導致與預測不同的行為。 因此,在預測期間,我們可以透過為第 個查詢指定有效長度來消除不必要的資訊。