探秘Transformer系列之(4)--- 編碼器 & 解碼器
探秘Transformer系列之(4)--- 編碼器 & 解碼器
0x00 摘要
對于機器翻譯,Transformer完整的前向計算過程如下圖所示(與總體架構那章的流程圖相比較,此處重點突出了編碼器和解碼器內部以及之間的關聯)。圖左側是編碼器棧,右側是解碼器棧,這兩個構成了 Transformer 的“軀干”。具體流程如下。
- 將輸入序列轉換為嵌入矩陣,再加上位置編碼(表示每個單詞的位置)之后構成word embedding,然后把word embedding輸入解碼器。此步驟對應下圖的標號1。
- 編碼器接收輸入序列的word embedding并生成相關隱向量。編碼器是并行處理,因此只會進行一次前向傳播。編碼器棧內部通過自注意力機制完成了對源序列的特征的提取,得到了源序列內部元素之間的彼此相關性,保留了高維度潛藏的邏輯信息。或者說,自注意力負責基于其全部輸入向量來建模每個輸出均可以借鑒的隱向量。此步驟對應下圖的標號2。
- 與編碼器不同,解碼器會循環執行,直到輸出所有結果。解碼器以
標記和編碼器的輸出作為起點,生成輸出序列的下一個 token。就像我們對編碼器的輸入所做的那樣,我們會生成嵌入并添加位置編碼來傳給那些解碼器。此步驟對應下圖的標號3。 - 解碼器棧內部通過掩碼自注意力機制完成了對目標序列特征的提取,得到了目標序列內部元素之間的彼此相關性,保留了高維度潛藏的邏輯信息。或者說,掩碼自注意力負責基于解碼器的輸入向量來建模每個解碼器的輸出向量。
- 自注意力機制只能提煉解構本序列的關聯性特征,因此編碼器棧和解碼器棧之間通過交叉注意力在編碼器和解碼器之間傳遞信息,完成彼此的聯系,確保了對特征進行非對稱的壓縮和還原。或者說,交叉注意力層則負責基于編碼器的所有輸出隱向量來進一步建模每個解碼器的輸出向量。
- 使用一個線性層來生成 logits。此步驟對應下圖的標號4。
- 應用一個 softmax 層來生成概率,最終依據某種策略輸出一個最可能的單詞。此步驟對應下圖的標號5。
- 解碼器使用解碼器的新輸出token和先前生成的 token 作為輸入序列來生成輸出序列的下一個 token。此步驟對應下圖的標號6。
- 重復步驟 3-6循環(對于步驟3,每次輸入是變化的)來對下一個時刻的輸出進行解碼預測,直到生成 EOS 標記表示解碼結束或者達到指定長度后停止。
這個解碼過程其實就是標準的seq2seq流程。因此,注意力機制是Transformer 的“靈魂”,Transformer 實際上是通過三重注意力機制建立起了序列內部以及序列之間的全局聯系。

本章依然用機器翻譯來分析說明。
0x01 編碼器
編碼器的輸入是word embedding序列,這是一個低階語義向量序列。編碼器的目標就是對這個低階語義向量序列進行特征提取、轉換,并且最終映射到一個新的語義空間,從而得到一個高階語義向量序列。因為編碼器使用了注意力機制,所以這個高階語義向量序列具有更加豐富和完整的語義,也是上下文感知的。這個高階語義向量序列將被后續的解碼器使用并生成最終輸出序列。而且,編碼器是為每一個待預測詞都生成一個上下文向量。為何要在每一步針對每一個待預測詞都生成一個新的上下文向量?我們可以通過例子來解答。
- 中文:我吃了一個蘋果,然后吃了一個香蕉。
- 英文:I ate an apple and then a banana。
如果逐字翻譯,翻譯到”我吃了一個“時候,得到的英文應該是”I ate a“?還是”I ate an”?這就需要依據后面的“蘋果”來判斷。所以,翻譯“蘋果”之后,需要依據“蘋果”才能確定是"a"還是“an”,進而更新之前”一個“這個詞對于的上下文向量。
1.1 結構
Transformer的編碼器模塊結構如下圖紫色方框所示。編碼器是由多個相同的EncoderLayer(編碼器層,即下圖的黃色部分)堆疊而成的。Transformer論文原圖中只畫了一個EncoderLayer,然后旁邊寫了個 Nx,這個Nx就表示編碼器是由幾個EncoderLayer堆疊而成。論文中設置N = 6。

每個EncoderLayer由以下模塊組成:
- 多頭自注意力機制(Multi-Head Self-Attention/MHA),其特點如下:
- MHA是對輸入序列自身進行的注意力計算,用于獲取輸入序列中不同單詞之間的相關性。
- 編碼器的輸入被轉換(即輸入經過嵌入層和位置編碼后,分別與Query、Key 和 Value 的參數矩陣相乘)后,作為Query、Key 和 Value這三個參數傳遞給MHA,即QKV均來自一個序列。
- MHA可以使網絡在進行預測時對輸入句子的不同位置的分配不同的注意力。多頭注意力意味著模型有多組不同的注意力參數,每組都會輸出一個依據注意力權重來加權求和的向量,這些向量會被合并成最終的向量進行輸出。
- 第一個殘差連接(Residual Connection)。殘差連接使注意力機制中產生的新數據和最開始輸入的原始數據合并在一起,這個合并其實就是簡單的加法,這樣可以避免深度神經網絡中的梯度消失問題。殘差連接對應上圖中的“Add”。
- 第一個Layer Normalization(層歸一化)。該模塊可以讓數據更穩定,便于后續處理。具體對應圖上的“Norm”,其會對層的輸入進行歸一化處理,使得其均值為0,方差為1。
- FFN(Feed-Forward Networks/前饋神經網絡)。這個模塊由兩個線性變換組成,中間夾有一個ReLU激活函數。它對每個位置的詞向量獨立地進行變換。這個層對經過注意力處理后的向量進一步進行處理和優化,產生一個新的表示。FFN輸出是一個更具抽象性、更豐富的上下文表示,可以增加模型的非線性表示能力。
- 第二個殘差連接,作用同第一個殘差連接。
- 第二個Layer Normalization,作用同第一個Layer Normalization。
或者簡化來看,編碼器模塊由一系列相同層構成,每個層分為兩個重要子模塊:MHA和FFN。每個重要子模塊周圍有一個殘差連接,并且每個重要子模塊的輸出都會經過Layer Normalization。
1.2 輸入和輸出
因為編碼器是層疊的棧結構,因此不同EncoderLayer的輸入輸出不盡相同。
編碼器棧第一個EncoderLayer的輸入是單詞的Embedding加上位置編碼,即圖上的Input Embedding和Positional Encoding相加之后的結果,我們稱之為Word Embedding(詞向量)。加上位置編碼的原因是由于Transformer模型沒有循環或卷積操作,為了讓模型能夠利用詞的順序信息,需要在輸入嵌入層中加入位置編碼。因為多個EncoderLayer是串聯在一起,所以棧的其它EncoderLayer的輸入是上一個EncoderLayer的輸出。
經過多層計算之后,最后一個EncoderLayer的輸出就是編碼器的輸出(編碼器和解碼器之間的隱狀態)。該輸出會送入解碼器堆棧中的每一個DecoderLayer中。通常在代碼實現中把這個輸出叫做memory。編碼器的輸出就是對原始輸入的高階抽象表達,是在更高維的向量空間中的表示。
輸入的維度一般是[batch_size, seq_len, embedding_dim]。為了方便殘差連接,每一個EncoderLayer輸出的矩陣維度與輸入完全一致。其形狀也是[batch_size, seq_len, embedding_dim]。這樣的設計也確保了模型在多個編碼器層之間能夠有效地傳遞和處理信息,同時也為更復雜的計算和解碼階段做好了準備。
1.3 流程
我們繼續細化編碼流程。一個Transformer編碼塊做的事情如下圖所示。圖中分為兩部分,既包括編碼器,也包括其輸入(為了更好的說明,把處理輸入部分也涵蓋進來)。
- 上面部分( #2)就是Encoder模塊里的一個獨立的EncoderLayer。這個模塊想要做的事情就是想把輸入X轉換為另外一個向量R,這兩個向量的維度是一樣的。然后向量R作為上一層的輸入,會一層層往上傳。
- 下面部分(#1)的是兩個單詞的embedding處理部分,即EncoderLayer的輸入處理部分。

我們對上圖流程分析如下。
- 第一步會用token embedding和位置編碼來生成word embedding,對應圖上圓形標號1。
- 第二步是自注意力機制,對應圖上圓形標號2,具體操作是\(softmax(QK^T)V\)。所有的輸入向量共同參與了這個過程,也就是說,X1和X2通過某種信息交換和雜糅,分別得到了中間變量Z1和Z2。自注意力機制就是句子中每個單詞看看其它單詞對自己的影響力有多大,本單詞更應該關注在哪些單詞上。在輸入狀態下,X1和X2互相不知道對方的信息,但因為在自注意力機制中發生了信息交換,所以Z1和Z2各自都有從X1和X2得來的信息。
- 第三步是殘差連接和層歸一化,對應圖上圓形標號3。其具體操作是\(Norm(x+Sublayer(x))\)。
- 第四步是FFN層,對應圖上圓形標號4。對應操作是\(max(0, xW_1 + b_1)W_2 + b_2\),即兩層線性映射并且中間使用激活函數來激活。因為FFN是割裂開的,所以Z1和Z2各自獨立通過全連接神經網絡,得到了R1和R2。
- 第五步是第二個殘差連接和層歸一化,對應圖上圓形標號5。其具體操作是\(Norm(x+Sublayer(x))\)。
至此,一個EncoderLayer就執行完畢,其輸出可以作為下一個EncoderLayer的輸入,然后重復2~5步驟,直至N個EncoderLayer都處理完畢。我們也可以看到,每個輸出項的計算和其他項的計算是獨立的,即每一層的EncoderLayer都對輸入序列的所有位置同時進行操作,而不是像RNN那樣逐個位置處理,這是Transformer模型高效并行處理的關鍵。
1.4 張量形狀變化
我們來看看編碼過程中的張量形狀變化。編碼器的輸入是待推理的句子序列X: [batch_size, seq_len, d_model]。
注意:如果考慮到限制最大長度,則每次應該是[batch_size, max_seq_len, d_model],此處進行了簡化。
編碼器內部數據轉換時候的張量形狀變化如下表所示,對于輸入X的Input Embedding張量來說,其形狀在編碼器內部始終保持不變,具體如下。
- 輸入層中,在做embedding操作時,張量形狀發生變化;在和位置編碼相加時,張量形狀保持不變。
- 在編碼器層內部的流轉過程中,張量形狀保持不變。
- 在編碼器內部,即多個編碼器層之間交互的過程中,張量形狀保持不變。
下表給出了詳細的操作和張量形狀。
| 視角 | 操作 | 操作結果張量的形狀 |
|---|---|---|
| 輸入層 | X(token index) | [batch_size, seq_len] |
| 輸入層 | X = embedding(X) | [batch_size, seq_len, d_model] |
| 輸入層 | X = X + PE | [batch_size, seq_len, d_model] |
| 編碼器層內部 | X = MHA(X) | [batch_size, seq_len, d_model] |
| 編碼器層內部 | X = X + MHA(X) | [batch_size, seq_len, d_model] |
| 編碼器層內部 | X = LayerNorm(X) | [batch_size, seq_len, d_model] |
| 編碼器層內部 | X = FFN(X) | [batch_size, seq_len, d_model] |
| 編碼器層 | X = EncoderLayer(X) | [batch_size, seq_len, d_model] |
| 編碼器 | X = Encoder(X) = 6 x EncoderLayer(X) | [batch_size, seq_len, d_model] |
1.6 實現
我們接下來看看哈佛源碼中編碼器的實現。
Encoder
Encoder類是編碼器的實現,它的forward()函數返回的就是編碼之后的向量。
# 使用Encoder類來實現編碼器,它繼承了nn.Module類
class Encoder(nn.Module):
"Core encoder is a stack of N layers"
# Encoder的核心部分是N個EncoderLayer堆疊而成的棧
def __init__(self, layer, N):
"""
初始化方法接受兩個參數,分別是:
layer: 要堆疊的編碼器層,對應下面的EncoderLayer類
N: 堆疊多少次,即EncoderLayer的數量
"""
super(Encoder, self).__init__() # 調用父類nn.Module的初始化方法
# 使用clone()函數將layer克隆N份,并將這些層放在self.layers中
self.layers = clones(layer, N)
# 創建一個LayerNorm層,并賦值給self.norm,這是“Add & Norm”中的“Norm”部分
self.norm = LayerNorm(layer.size)
def forward(self, x, mask):
"Pass the input (and mask) through each layer in turn."
"""
前向傳播函數接受兩個參數,分別是:
x: 輸入數據,即經過Embedding處理和添加位置編碼后的輸入。形狀為(batch_size, seq_len,embedding_dim)
mask:掩碼
"""
# 使用EncoderLayer對輸入x進行逐層處理,每次都會得到一個新的x,然后將x作為下一層的輸入
# 此循環的過程相當于輸出的x經過了N個編碼器層的逐步處理
for layer in self.layers: # 遍歷self.layers中的每一個編碼器層
x = layer(x, mask) # 將x和mask傳遞給當前編碼器層,編碼器層進行運算,并將輸出結果賦值給x
return self.norm(x) # 對最終的輸出x應用層歸一化,并將結果返回
其中的clone函數的代碼為
def clones(module, N):
"Produce N identical layers."
return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])
EncoderLayer
EncoderLayer類是編碼器層的實現,作為編碼器的組成單元, 每個EncoderLayer完成一次對輸入的特征提取過程, 即編碼過程。
class EncoderLayer(nn.Module):
"Encoder is made up of self-attn and feed forward (defined below)"
def __init__(self, size, self_attn, feed_forward, dropout):
"""
初始化函數接受如下參數:
size: 對應d_model,即word embedding維度的大小,也是編碼層的大小
self_attn: 多頭自注意力模塊的實例化對象
feed_forward: FFN層的實例化對象
dropout: 置0比率
"""
super(EncoderLayer, self).__init__() # 調用父類nn.Module的構造函數
self.self_attn = self_attn # 設置成員變量
self.feed_forward = feed_forward # 設置成員變量
# 創建兩個具有相同參數的SublayerConnection實例,一個用于自注意力,一個用于FFN
self.sublayer = clones(SublayerConnection(size, dropout), 2)
self.size = size
def forward(self, x, mask):
"Follow Figure 1 (left) for connections."
"""
對應論文中圖1左側Encoder的部分
前向函數的參數如下:
x: 源語句的嵌入向量或者前一個編碼器的輸出
mask: 掩碼
"""
# 順序運行兩個函數:self_attn(),self.sublayer[0]()
# 1. 對輸入x進行自注意力操作
# 2. 將自注意力結果傳遞給第一個SublayerConnection實例,SublayerConnection實例內部會做殘差連接和層歸一化
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
# 用上面計算結果來順序運行兩個函數:self.feed_forward()和self.sublayer[1]
# 1. FFN進行運算
# 2. 將FFN計算結果傳遞給第一個SublayerConnection實例,SublayerConnection實例內部會做殘差連接和層歸一化
return self.sublayer[1](x, self.feed_forward)
我們對代碼中提到的SublayerConnection做下簡要說明,后續文章會進行詳述。從論文圖上看,不管是自注意力模塊還是FFN,它們都會先做自己的業務邏輯,然后做殘差連接和層歸一化,也會加入Dropout。因為有些邏輯可以復用,因此哈佛代碼把他們封裝在SublayerConnection類中。SublayerConnection類在其內部會構造 LayerNorm 和 Dropout的實例,自注意力或FFN還是放在EncoderLayer 中構造,然后在前向傳播時候由EncoderLayer 傳給SublayerConnection。解碼器也是按照類似方式調用SublayerConnection。
但是SublayerConnection的實現和論文略有不同。
- 原始論文的實現機制是:\(LayerNorm(x+ Sublayer(x))\)。
- SublayerConnection則是:\(x+LayerNorm(Sublayer(x))\)。
研究人員把Transformer論文中的實現叫做Post LN,因為是最后做LayerNorm,把SublayerConnection的實現方式叫做Pre LN。兩種方式各有優劣,我們會在后續文章中進行分析。
0x02 解碼器
首先要提前說明下,解碼器的輸入有兩個:編碼器產生的隱狀態和解碼器之前預測的輸出結果。解碼器會基于這兩個輸入來預測下一個輸出token。網上對編碼器和解碼器的關系有一個比較恰當的通俗比喻,作者依據自己的思考對該比喻做進一步調節:
- 輸入序列是一個需要組裝的玩具。
- 編碼器是售貨員,售貨員對該玩具的各個組件進行研究,編碼器的輸出結果(隱狀態)就是玩具的組裝說明書,里面說明了玩具每個組件的用法(需要怎么和其它組件相配合)。
- 解碼器就是購買者。如果想把玩具組裝好,購買者就需要在組裝說明書中查詢每個零件的說明,然后依據說明書的描述找到最相似的零件(注意力匹配)進行組裝。在組裝過程中的玩具就是解碼器在之前步驟中的預測輸出結果。因為組裝需要一面查詢組裝說明書,一面查看在組裝過程中的玩具。所以解碼器有兩個輸入:組裝說明書和組裝過程中的玩具。
- 購買者最終輸出一個組裝好的玩具。
2.1 結構
解碼器的結構如下圖所示,也是由多個解碼器層組成。在解碼器中,子層堆疊的目的是逐層細化和優化生成詞匯的表示,使得模型能夠生成更準確、更符合上下文的目標詞。每個子層都有不同的功能和作用。

每個解碼器層包括三個重要子模塊:掩碼多頭注意力(Masked Multi-Head Attention),交叉注意力和FFN。每個子模塊的輸出會傳遞到下一個子模塊,進一步豐富和優化生成序列的表示。與編碼器類似,每個重要子模塊周圍都有一個殘差連接,并且每個重要子模塊的輸出都會經過層歸一化。這三個子模塊的作用如下:
- 掩碼多頭注意力。這是輸入序列對自身的注意力計算,每個解碼步驟中,掩碼自注意力層會通過自注意力機制計算輸出序列中,當前詞與已生成詞的關系。其細節如下。
- 解碼器的輸入之一(解碼器之前預測輸出結果的拼接)被傳遞給所有三個參數,Query、Key和 Value,即QKV均來自一個序列。
- 掩碼多頭注意力模塊會對“解碼器之前預測的輸出結果”這個序列進行編碼,此編碼執行類似編碼器中全局自注意力層的工作。
- 掩碼多頭注意力與全局自注意力的不同之處在于在對序列中位置的處理上(掩碼操作),我們馬上會進行分析。
- 該層輸出一個新的表示,是結合了已生成部分序列的信息的一個上下文向量,它包含當前生成的詞與已生成詞之間的上下文依賴關系。
- 交叉注意力。該模塊將源序列和目標序列進行對齊,是解碼器和編碼器之間的橋梁,這個子層的目的是讓解碼器結合編碼器的輸出,也就是在解碼期間參考源句子的上下文信息。通過這一層,解碼器可以對源句子的每個 token 進行注意力計算,確保解碼時能夠參考源句子的結構和語義。交叉注意力是編碼器和解碼器的第二個不同之處。其細節如下。
- 不帶掩碼。
- 交叉注意力的輸入來源有兩處:編碼器的輸出和掩碼多頭注意力的輸出。K、V矩陣來自編碼器的輸出,Q來自掩碼多頭注意力的輸出。作者之所以這樣設計,是在模仿傳統編碼器-解碼器模型的解碼過程,可以綜合考慮目標序列與源序列的內容。
- 這個層的輸出結合了編碼器輸出的上下文信息以及解碼器當前步驟生成的目標詞的表示,進一步優化了目標詞的表示。輸出是一個包含源句子信息的上下文表示(融合了源句子的結構和目標序列的部分結構)。
- FFN,作用同編碼器的FFN。
解碼器的掩碼多頭自注意力與編碼器的多頭自注意力不同之處的終極原因在于訓練和推理的不同,即訓練和推理在每個時間步的輸入和操作的區別。
- 訓練過程中每個時間步的輸入是全部目標序列,在Encoder的多頭自注意力中,每個位置都可以自由地注意序列中的所有其他位置。這意味著計算注意力分數時,并沒有位置上的限制。這種設置是因為在編碼階段,我們假定有完整的輸入序列,并且每個詞都可以依賴于上下文中的任何其他詞來獲得其表示。
- 推理過程中每個時間步的輸入是直到當前時間步所產生的整個輸出序列,推理的本質也是串行自回歸的。或者說,解碼器的本質就是自回歸的。
為了并行操作,人們使用了teacher forcing機制(需要結合掩碼機制),這樣可以讓解碼器同時對同一序列中的多個token進行解碼和預測。因為解碼器現在的輸入是整個目標句子,而當預測某個位置的輸出時,我們希望單詞只能看到它以及它之前的的單詞,不希望注意力在預測某個詞時候就能關注到其后面的單詞,那樣模型有可能利用已經存在的未來詞來輔助當前詞的生成,就是“作弊”了。為了讓前面的token不能觀察到后面token的信息,所以使用了掩碼技術。掩碼的作用是確保解碼器只能關注到它之前已經生成的詞,而不能看到未來的詞。掩碼邏輯是為了訓練來特殊打造。
另外,雖然推理時候所有輸入都是已知輸入,沒有真正的“偷看未來詞”的可能性,不需要掩碼,但是為了保持與訓練時的計算一致,推理時也保留了此處代碼和模型結構。這樣使得推理時的行為與訓練時完全匹配,避免了訓練與推理之間的行為差異。而且,雖然未來詞還沒有生成,掩碼自注意力機制依然會起到限制作用,確保解碼器在每個步驟只關注已經生成的上下文,而不會假設未來的信息存在。后續章節會對掩碼做詳細闡釋。
2.2 輸入和輸出
解碼器也是層疊的棧結構,因此不同解碼器層的輸入輸出不盡相同。
- 第一個解碼器層的輸入有兩個:
- 解碼器之前預測輸出結果的拼接,即前一時刻解碼器的輸入 + 前一時刻解碼器的輸出(預測結果)。另外還要加上Shfited Right操作。
- 編碼器的輸出,即第一個解碼器層中交叉注意力的K、V均來自編碼器的輸出(編碼器堆棧中最后一個編碼器的輸出)。
- 后續解碼器層的輸入有兩個:
- 前一個解碼器層的輸出。
- 編碼器的輸出,即后續每一個解碼器層中交叉注意力的K、V均來自編碼器的同一個輸出(編碼器堆棧中最后一個編碼器的輸出)。
解碼器最終會輸出一個實數向量,傳給架構圖中最上方的線性層,由最終的線性變換和softmax層最終轉換為概率分布,借此預測下一個單詞。前面提到,編碼器的輸出就是對原始輸入在更高維的向量空間中的表示。解碼器就是在高維空間內對向量進行操作,找到按照注意力分數匹配的高階向量。后續會經過generator等模塊把高維向量轉換回人類可以理解的低維向量。

2.3 流程
解碼器的流程需要區分訓練階段和推理階段,因為這兩個階段的流程不盡相同。
訓練
注:解碼器在訓練時采用Teacher Forcing模式,會向解碼器輸入整個目標序列,可以并行預測目標序列的所有單詞。我們會在后續章節對Teacher Forcing進行詳細分析。
假如我們的訓練任務得到了如下文本對:
- 中文:我愛你。
- 英文:I love you。
模型采用如下方式調用,其中batch.src是"我愛你”,batch.tgt是"I love you“。
out = model.forward(batch.src, batch.tgt, batch.src_mask, batch.tgt_mask)
編碼器接受"我愛你”作為輸入,“我愛你”首先被tokenizer處理成token,然后轉換成一組向量進行后續處理,得到輸出memory。
memory = encode(self, src, src_mask)
解碼器有兩種輸入:編碼器的輸出(對應下面代碼的memory)和英文句子“I love you”(對應下面代碼的tgt)。“I love you”經過tokenizer處理成token,然后轉換成一組向量。解碼器會把該組向量和編碼器的輸出結合起來,生成最終的翻譯輸出。
decode(self, memory, src_mask, tgt, tgt_mask)
我們要對解碼器的輸入再做一下說明。
- tgt:英文句子“I love you”是訓練集的真值標簽,需要結合掩碼并且整體右移(Shifted Right)一位,最終得到是 shifted and masked ground truth。右移的原因解釋如下:解碼器在T-1時刻會預測T時刻的輸出,所以需要在輸入句子的最開始添加起始符
,從而整個句子將整體右移一位,這樣就把每個token的標簽設置為它之后的token,方便預測第一個Token(在Teacher forcing模式下,也同時方便預測后續的token)。比如原始輸入是“I love you”,右移之后變成“ I love you”,這樣我們就可以通過起始符 預測“I”,也就是通過起始符 l來預測實際的第一個輸出。否則無法預測第一個輸出。

- src_mask和tgt_mask是源序列和目標序列的掩碼。前面已經介紹過,加入掩碼的原因是要隱藏未來信息。因為解碼器的輸入是整個目標句子,而當預測某個位置的輸出時,我們希望單詞只能它以及它之前的的單詞,不希望注意力在預測某個詞時候就能關注到其后面的單詞,那樣就是“作弊”了。因此需要借助掩碼把后面單詞的信息隱藏掉,這樣才能在訓練時候模擬實際推理的效果。
具體訓練時候的輸入輸出如下圖,因為是并行訓練,所以解碼器的輸入之一是"I love you",在假設全部預測正確的情況下,輸出也是"I love you",此輸出是一次性全部輸出。

推理
解碼器在推理時的工作流程相對簡單多了,所以不再需要掩碼。此時,解碼器采用的是自回歸模式,也就是這次的輸出會加到上次的輸入后面,作為下一次的輸入,這樣每次解碼都會利用前面已經解碼輸出的所有單詞嵌入信息。因此,推理任務不用真實目標序列來指導生成過程,只使用了中文句子“我愛你”。即,Encoder的輸入不變,而Decoder的輸入會是之前Decoder輸出的組合(也包括Encoder的輸出)。
具體如下圖所示,模型將從特殊的起始序列標記

具體代碼流程如下。
def inference_test():
test_model = make_model(11, 11, 2)
test_model.eval()
src = torch.LongTensor([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]])
src_mask = torch.ones(1, 1, 10)
memory = test_model.encode(src, src_mask)
ys = torch.zeros(1, 1).type_as(src)
for i in range(9):
out = test_model.decode(
memory, src_mask, ys, subsequent_mask(ys.size(1)).type_as(src.data)
)
prob = test_model.generator(out[:, -1])
_, next_word = torch.max(prob, dim=1)
next_word = next_word.data[0]
ys = torch.cat(
[ys, torch.empty(1, 1).type_as(src.data).fill_(next_word)], dim=1
)
print("Example Untrained Model Prediction:", ys)
test_model.decode()對應如下代碼。
class EncoderDecoder(nn.Module):
def decode(self, memory, src_mask, tgt, tgt_mask):
return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)
編碼器會逐步調用編碼器層的邏輯。
class Decoder(nn.Module):
def forward(self, x, memory, src_mask, tgt_mask):
for layer in self.layers:
x = layer(x, memory, src_mask, tgt_mask)
return self.norm(x)
最終編碼器層會調用業務邏輯,即注意力計算、殘差連接等。
class DecoderLayer(nn.Module):
def forward(self, x, memory, src_mask, tgt_mask):
"Follow Figure 1 (right) for connections."
m = memory
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask))
x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, src_mask))
return self.sublayer[2](x, self.feed_forward)
2.4 張量形狀變化
我們來看看解碼過程中的張量形狀變化(為了更好的說明,我們把解碼器的輸入和輸出都一起納入)。解碼器的輸入是一個長度變化的張量Y:[batch_size, seq_len, d_model],初始時,這個張量中,每個矩陣只有1行,即開始字符的編碼。
注意:如果考慮到限制最大長度,則每次應該是[batch_size, max_seq_len, d_model]。
對于輸入Y的Input Embedding張量來說,其形狀在解碼器內部始終保持不變,具體如下。
- 輸入層中,在做embedding操作時,張量形狀發生變化;在和位置編碼相加時,張量形狀保持不變。
- 在解碼器層內部的流轉過程中,張量形狀保持不變。
- 在解碼器內部,即多個解碼器層交互過程中,張量形狀保持不變。
- 進入到輸出層之后,張量形狀開始變化。
注意,在單次推理過程中,張量不變,但是每次推理之后,矩陣增加一行,seq_len加1。
| 視角 | 操作 | 操作結果張量的形狀 |
|---|---|---|
| 輸入層 | Y(token index) | [batch_size, seq_len] |
| 輸入層 | Y = embedding(Y) | [batch_size, seq_len, d_model] |
| 輸入層 | Y = Y + PE | [batch_size, seq_len, d_model] |
| 解碼器層內部 | Y = Masked-MHA(Y) | [batch_size, seq_len, d_model] |
| 解碼器層內部 | Y = LayerNorm(Y + Masked-MHA(Y)) | [batch_size, seq_len, d_model] |
| 解碼器層內部 | Y = Cross-MHA(Y, M, M) | [batch_size, seq_len, d_model] |
| 解碼器層內部 | Y = LayerNorm(Y + Cross-MHA(Y, M, M)) | [batch_size, seq_len, d_model] |
| 解碼器層內部 | Y = FFN(Y) | [batch_size, seq_len, d_model] |
| 解碼器層內部 | Y = LayerNorm(Y + FFN(Y)) | [batch_size, seq_len, d_model] |
| 解碼器層 | Y = DecoderLayer(Y) | [batch_size, seq_len, d_model] |
| 解碼器 | Y = Decoder(Y) = N x DecoderLayer(Y) | [batch_size, seq_len, d_model] |
| 輸出層 | logits = Linear(Y) | [batch_size, seq_len, d_voc] |
| 輸出層 | prob = softmax(logits) | [batch_size, seq_len, d_voc] |
2.5 實現
Decoder
Decoder類是解碼器的實現,是 N 個解碼層堆疊的棧。編碼器會將自己輸出的隱向量編碼矩陣C傳遞給解碼器,這些隱向量可以幫助解碼器知道它應該更加關注輸入序列哪些位置。解碼器的每個解碼層都會使用同一個隱向量編碼矩陣C,這些隱向量將被每個解碼層用于自身的Encoder-Decoder交叉注意力模塊,
Decoder類依次會根據當前翻譯過的第i個單詞,翻譯下一個單詞(i+1)。在解碼過程中,翻譯到第i+1單詞時候需要通過Mask操作遮蓋住(i+1)之后的單詞。Decoder類的代碼具體如下。
class Decoder(nn.Module):
"Generic N layer decoder with masking."
def __init__(self, layer, N):
"""
初始化函數有兩個參數。layer對應下面的DecoderLayer,是要堆疊的解碼器層;N是解碼器層的個數
"""
super(Decoder, self).__init__()
self.layers = clones(layer, N) # 使用clones()函數克隆了N個DecoderLayer,然后保存在layers這個列表中
self.norm = LayerNorm(layer.size) # 層歸一化的實例
def forward(self, x, memory, src_mask, tgt_mask):
"""
前向傳播函數有四個參數:
x: 目標數據的嵌入表示,x的形狀是(batch_size, seq_len, d_model),在預測時,x的詞數會不斷增加,比如第一次是(1,1,512),第二次是(1,2,512),以此類推
memory: 編碼器的輸出
src_mask: 源序列的掩碼
tgt_mask: 目標序列的掩碼
"""
# 實現多個編碼層堆疊起來的效果,并完成整個前向傳播過程
for layer in self.layers: # 讓x逐次在每個解碼器層流通,進行處理
x = layer(x, memory, src_mask, tgt_mask)
# 對多個編碼層的輸出結果進行層歸一化并返回最終的結果
return self.norm(x)
DecoderLayer
DecoderLayer類是解碼器層的實現。作為解碼器的組成單元,每個解碼器層根據給定的輸入向目標方向進行特征提取操作,即實施解碼過程。DecoderLayer和EncoderLayer的內部非常相似,區別EncoderLayer只有一個多頭自注意力模塊,而DecoderLayer有兩個多頭自注意力模塊,從代碼上看,就是比EncoderLayer多了一個src_attn成員變量。self_attn和src_attn的實現完全一樣,只不過使用的Query,Key 和 Value 的輸入不同。
DecoderLayer主要成員變量如下:
- size:詞嵌入的維度大小,即解碼器層的尺寸。
- self_attn: 掩碼多頭自注意力模塊,負責對解碼器之前的輸出(即當前的輸入)做自注意力計算。該模塊需要Q=K=V。Self-Attention 的 Query,Key 和 Value 都是來自下層輸入或者是原始輸入。
- src_attn:交叉注意力模塊,負責對編碼器的輸出和解碼器之前的輸出做交叉注意力計算,但是Q!=K=V。Query 來自 self-attn 的輸出,Key 和 Value則是編碼器最后一層的輸出(代碼中是memory變量)。
- feed_forward:FFN模塊。
- sublayer:應用了殘差連接和層歸一化。
- drop:置零比率。
上述這些成員變量通過參數傳給初始化函數。
class DecoderLayer(nn.Module): # 繼承自PyTorch的nn.Module類
"Decoder is made of self-attn, src-attn, and feed forward (defined below)"
def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
super(DecoderLayer, self).__init__()
self.size = size
self.self_attn = self_attn
self.src_attn = src_attn
self.feed_forward = feed_forward
# 創建三個SublayerConnection類實例,分別對應self_attn,src_attn和feed_forward
self.sublayer = clones(SublayerConnection(size, dropout), 3)
def forward(self, x, memory, src_mask, tgt_mask):
"Follow Figure 1 (right) for connections."
"""
前向傳播函數有四個參數:
x: 目標數據的嵌入表示,x的形狀是(batch_size, seq_len, d_model),在預測時,x的詞數會不斷增加,比如第一次是(1,1,512),第二次是(1,2,512),以此類推。x可能是上一層的輸出或者是整個解碼器的輸出
memory: 編碼器的輸出
src_mask: 源序列的掩碼
tgt_mask: 目標序列的掩碼
"""
m = memory # 將memory表示成m方便之后使用
# 第一個子層執行掩碼多頭自注意力計算。相當于順序運行了兩個函數:self_attn()和self.sublayer[0]()。這里的Q、K、V都是x。tgt_mask的作用是防止預測時看到未來的單詞。
x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask))
# 第二個子層執行交叉注意力操作,此時Q是輸入x,K和V是編碼器輸出m。src_mask在此處的作用是遮擋填充符號,避免無意義的計算,提升模型效果和訓練速度。此刻需要注意的是,兩個注意力計算的mask參數不同,上一個是tgt_mask,此處是src_mask
x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, src_mask))
# 第三個子層是FFN,經過它的處理后就可返回結果
return self.sublayer[2](x, self.feed_forward)
0x03 交叉注意力深入
前面已經簡單介紹過注意力的分類,此處結合代碼對交叉注意力再進行深入分析。
3.1 分類
Transformer 中,注意力被用在三個地方,Encoder中的Multi-Head Attention;Decoder中的Masked Multi-Head Attention;Encoder和Decoder交互部分的Multi-Head Attention。注意力層(Self-attention 層及 Encoder-Decoder-attention 層)以三個參數的形式接受其輸入:查詢(Query)、鍵(Key)和值(Value)。我們接下來就分析下,看看Transformer中各個注意力模塊的Q、K、V到底是怎么來的。
| 名稱 | 位置 | Q | K/V |
|---|---|---|---|
| 多頭自注意力 | 編碼器 | QKV均來自同一個序列 | QKV均來自同一個序列 |
| 掩碼多頭自注意力 | 解碼器 | QKV均來自同一個序列 | QKV均來自同一個序列 |
| 交叉注意力 | 解碼器 | 掩碼多頭自注意力的輸出 | 編碼器輸出被用做V和K。 |
三個注意力模塊的作用如下:
- 多頭自注意力:是輸入序列對自身的注意力計算,允許每個位置自由地注意到整個序列,這樣可以獲取輸入句子中不同單詞之間的相關性。此處可以說是Transformer最大的創新。
- 掩碼多頭自注意力:是輸入序列對自身的注意力計算,用于獲取輸出句子(已經翻譯好的部分)中不同單詞之間的相關性。同時通過序列掩碼來限制注意范圍,以保持自回歸屬性,確保生成過程的正確性。這種設計是Transformer模型能夠并行訓練的關鍵所在。
- 交叉注意力:是目標序列對輸入序列的注意力計算。這種設計是Transformer模型能夠有效處理類似文本翻譯任務的關鍵所在。
或者說, 注意力機制是尋找sequence內部(self-attention)或者sequence之間(cross-attention)不同位置上的相似性或者相關性。
3.2 業務邏輯
我們接下來看看交叉注意力的業務邏輯。交叉注意力計算的是每個源序列單詞與每個目標序列單詞之間的相互作用或者相似度,是在輸入序列和輸出序列之間進行對齊。
seq2seq場景存在一種因果關系,該因果性體現在上下文(即包括輸入序列,也包括輸出序列)上,因為顯然源文本中每個位置的字符應該和目標翻譯文本各位置字符存在一定的對照關系,因此源文本每個位置的token對于當下要預測的token應該有不一樣的影響(權重分配)。我們以機器翻譯任務為例,解碼器的目標是在給定某源語言序列時產生正確的目標語言輸出序列。為了實現這一點,解碼器需要:
- 學習到歷史譯文的所有信息。只有知道歷史生成的內容,才有了正確輸出下一個token的基礎。
- 學習到于源文本中與當前輸出的 token 相關的部分。這其實蘊含了解碼器需要對編碼器輸出的所有信息都有所了解。只有解碼器的每個位置都能夠獲取輸入序列中的所有位置的信息,才能通過學習生成正確的輸出 token。
但是,掩碼自注意力只能保證解碼器學習到歷史譯文的內容。還需要一個方式來學習到源文本信息,以及把源文本和歷史譯文融合起來。因此,人們提出了交叉注意力來完成這個功能,這也是因果關系的一種體現。“編碼器-解碼器交叉注意力”從兩個來源獲得輸入,這兩個來源分別來自不同的范疇,因此交叉注意力可以理解為是自注意力的雙塔實踐。
- Q是譯文,來自于解碼器的輸出。因為Q是來自解碼器的掩碼自注意力,所以此時天然已經獲取了歷史譯文的內容。
- K和V是原文,來自于編碼器的輸出,已經持有了輸入序列(比如:“我喜歡蘋果”)的信息。
交叉注意力機制可以讓解碼器和編碼器進行交互,確保了解碼器可以"詢問"編碼器有關輸入序列的信息,可以聚焦于源語言句子中的不同部分。或者說,編碼器輸出的隱向量本質是聚合了輸入序列信息的一個數據庫(V),而解碼器的每一個輸入token本質是一條查詢語句(Q),負責查詢數據庫中與之最相似的(最需要注意的)token。最終 \(QK^T\) 這個矩陣的每一行都表示decoder的一個輸入token對隱向量中所有token的注意力。例如,在進行“我喜歡蘋果”到“I love apple”的翻譯時,解碼器可能會詢問編碼器:“根據你對‘我’的理解,接下來我應該輸出什么?”。通過這種方式,解碼器可以更準確地預測目標序列的下一個詞。這種"一問一答"的描述是形象的,它說明了解碼器是如何利用編碼器的信息來確定最合適的輸出的。
3.3 業務流程
然后我們再看交叉注意力是如何在編碼器和解碼器之間起作用的。
解碼器在對當前時刻進行解碼輸出時,都會將當前時刻之前所有的預測結果作為輸入來對下一個時刻的輸出進行預測。假設現在需要將"我吃了一個蘋果翻譯成英語"I ate an apple",目前解碼器已經輸出了“I ate”兩個單詞,接下來需要對下一時刻的輸出"an"進行預測,那么整個過程就可以通過下圖來進行表示,圖上藍色部分是解碼器,左邊的小藍框是解碼器中的掩碼多頭自注意力模塊,右面大藍框是編碼器的交叉注意力模塊,左下方紅色虛線框是編碼器。

具體流程說明如下:
- 左上角的矩陣(對應序號1)是解碼器中掩碼自注意力機制對輸入
" I ate"這3個詞編碼后的結果。 - 左下角(對應序號2)是編碼器對輸入`"我吃了一個蘋果"編碼后的結果,這就是編碼器最終輸出的從多角度集自身與其他各個字關系的矩陣,記為memory。
- 因為上述兩個矩陣分別都做了自注意力轉換,所以每個矩陣中的每一個向量都包含了本序列其它位置上的編碼信息。
- 把序號1看作是Q(對應序號3),把序號2看作是K和V(對應序號4和5)。
- 接下來,Q與K計算得到了一個注意力分數矩陣(對應序號6),矩陣的每一行就表示在對memory(圖中的V)中的每一位置進行解碼時,應該如何對注意力進行分配。
- 進行掩碼操作(對應序號7)。
- 進行softmax操作(對應序號8)得到注意力權重A。此權重可以看做是Q(待解碼向量)在K(本質上也就是memory)中查詢memory中各個位置與Q有關的信息。
- 再將注意力權重A與V進行線性組合便得到了交叉注意力模塊的輸出向量(對應序號9)。此時這個輸出向量可以看作是考慮了memory中各個位置編碼信息的輸出向量,也就是說它包含了在解碼當前時刻時應該將注意力放在memory中哪些位置上的信息。
3.4 代碼邏輯
我們再從代碼邏輯進行梳理。

具體流程解讀如下。
- run_epoch()函數調用模型的前向傳播函數,即EncoderDecoder類的forward()函數(對應圖上序號1),此時Batch類會提供數據和掩碼(batch.src, batch.tgt, batch.src_mask, batch.tgt_mas)給模型進行前向傳播。
- EncoderDecoder類會通過encode()函數執行編碼功能。
- encode()函數利用src, src_mask調用Encoder類的forward()函數(對應圖上序號2)。src里面是單詞的索引,encode()函數中會調用self.src_embed(src)來生成src的word embedding。
- Encoder.forward()函數又會調用EncoderLayer.forward()函數(對應圖上序號3),此處是編碼器的編碼功能。
- EncoderLayer.forward()函數最終調用到MultiHeadedAttention.forward()函數(對應圖上序號4),此處是編碼器層的編碼功能。
- MultiHeadedAttention.forward()函數最終調用到attention()函數完成注意力計算功能(對應圖上序號5)。
- encode()函數會返回memory,memory會被用作參數傳遞給decode()函數。
- EncoderDecoder類會通過decode()函數執行解碼功能。
- decode()函數利用memory,src_mask, tgt, tgt_mask來調用到Decoder.forward()函數(對應圖上序號6)。tgt里面是單詞的索引,decode()函數會調用self.tgt_embed(tgt)來生成tgt的word embeddng。
- Decoder.forward()函數會調用DecoderLayer.forward()函數(對應圖上序號7),此處是解碼器的解碼功能的實現。
- DecoderLayer.forward()函數首先會調用MultiHeadedAttention.forward()函數。因為是使用self.self_attn(x, x, x, tgt_mask)來調用(對應圖上序號8),x是tgt,所以這是掩碼多頭自注意力。
- DecoderLayer.forward()函數其次會調用MultiHeadedAttention.forward()函數。因為是使用self.src_attn(x, m, m, src_mask)來調用(對應圖上序號10),x是tgt,所以這是交叉注意力。
- 掩碼多頭自注意力的計算會使用tgt,tgt_mask來調用attenion()函數(對應圖上序號9)。
- 交叉注意力的計算會使用tgt,memoy, memory, src_mask來調用attention()函數(對應圖上序號11)。
其實這塊與上文 Encoder 中 的 Multi-Head Attention 具體實現細節上完全相同。
我們總結下,交叉注意力機制使模型能夠在生成輸出序列的每一步都考慮到輸入序列的全部信息,從而捕捉輸入和輸出之間的復雜依賴關系,在各種seq2seq任務中實現卓越的性能。
0x04 Decoder Only
Transformer的架構并非一成不變,它可以表現為僅編碼器(Encoder Only)、僅解碼器(Decoder Only)或經典的編碼器-解碼器模型。每種架構變體都針對特定的學習目標和任務進行了定制。
4.1 分類
Transformer架構最初作為機器翻譯任務的編碼器-解碼器模型引入。在此架構中,編碼器將整個源語言句子作為輸入,并將其通過多個Transformer編碼器塊,提取輸入句子的高級特征。然后,這些提取的特征被一個接一個地饋送到解碼器中,解碼器基于來自編碼器的源語言特征以及解碼器之前生成的tokens來生成目標語言中的tokens。在隨后的工作中,研究人員也引入僅編碼器和僅解碼器的架構,分別從原始編碼器-解碼器架構中僅取編碼器和解碼器組件,如圖所示。
- (a)是僅包含編碼器的模型,并行執行所有token的推理。
- (b)是僅包含解碼器的模型,以自回歸方式進行推理。
- (c)是包含編碼器-解碼器的模型,,它使用編碼序列的輸出作為交叉注意力模塊的輸入。

下圖給出了現代LLM的進化樹,追溯了近年來語言模型的發展,并突出了一些最著名的模型。同一分支上的模型關系更密切。基于Transformer的模型以非灰色顯示:
-
藍色分支 代表Decoder-Only 模型。隨著時間的推移,越來越多的 Decoder-Only 模型被推出。
-
粉色分支 代表Encoder-Only 模型。這些模型主要用于編碼和表示輸入序列。
-
綠色分支 代表 Encoder-Decoder 模型。結合了前兩者的特點,既能夠編碼輸入序列,又能生成輸出序列。
模型在時間線上的垂直位置表示它們的發布日期。開源模型用實心方塊表示,而閉源模型用空心方塊表示。右下角的堆疊條形圖顯示了來自不同公司和機構的模型數量。

從進化樹中,我們得出與本篇相關的以下觀察結果:
- 僅解碼器模型逐漸主導了LLM的發展。在LLM開發的早期階段,僅解碼器模型不如僅編碼器和編碼器-解碼器模型流行。然而,在2021年之后,隨著改變游戲規則的LLM-GPT-3的引入,僅解碼器模型經歷了顯著的繁榮。與此同時,在BERT帶來的最初爆炸性增長之后,僅編碼器的模型逐漸開始逐漸消失。
- 編碼器-解碼器模型仍然很有前景,因為這種類型的架構仍在積極探索中,其中大多數都是開源的。谷歌對開源編碼器-解碼器架構做出了重大貢獻。然而,僅解碼器模型的靈活性和多功能性似乎使谷歌對這一方向的堅持不那么有希望。
4.2 Decoder Only
Decoder-Only 模型只使用標準 Transformer 的 Decoder 部分,但稍作改動,典型差異是少了編碼器解碼器注意層,即在 Decoder-Only 模型不需要接收編碼器的信息輸入。Decoder-Only 模型沒有顯式的編碼器模塊,不顯式區分“理解”和“生成”階段。模型在自注意力機制中隱式完成對用戶輸入的分析、理解和建模,同時為生成任務提供基礎。
前文提到,在BERT帶來的最初爆炸性增長之后,僅編碼器的模型逐漸開始逐漸消失。因此目前只剩Encoder-Decoder模型和Decoder only模型。然而, Decoder Only模型也可以細分為Causal Decoder架構和Prefix Decoder架構。因此,現有LLM的主流架構在事實上大致可分為三種主要類型,即編碼器-解碼器、因果解碼器和前綴解碼器。下圖給出了三種主流架構中注意力模式的比較。在這里,藍色、綠色、黃色和灰色圓角矩形分別表示前綴token之間的注意力、前綴和目標token之間的注意力、目標token間的注意力和掩碼注意力。

我們接下來分析兩種Decoder Only架構。
- Causal Decoder(因果解碼器)架構。因果解碼器架構結合了單向注意力掩碼,以確保每個輸入token只能關注過去的token和自身。輸入和輸出token通過解碼器以相同的方式進行處理。作為該架構的代表性語言模型,GPT系列模型是基于因果解碼器架構開發的。
- Prefix Decoder(前綴解碼器)架構。前綴解碼器架構(也稱為非因果解碼器])修改了因果解碼器的掩蔽機制,以實現對前綴token的雙向關注和僅對生成的token的單向關注。這樣,與編碼器-解碼器架構一樣,前綴解碼器可以對前綴序列進行雙向編碼,并逐一自回歸預測輸出token,其在編碼和解碼過程中共享相同的參數。與其從頭開始進行預訓練,一個實用的建議是不斷訓練因果解碼器,然后將它們轉換為前綴解碼器以加速收斂,基于前綴解碼器的現有代表性LLM包括GLM130B和U-PaLM。
4.3 架構選擇
許多研究都對僅解碼器架構和編碼器-解碼器架構的性能進行了研究,但在有足夠的訓練和模型規模的情況下,確實沒有確鑿證據證明一種架構在最終性能上優于另一種架構。在知乎上有一個知名的帖子:[為什么現在的LLM都是Decoder only的架構?](為什么現在的LLM都是Decoder only的架構?)。各路大神都有很精彩的見解。不完全總結如下:
注:下面的decoder only主要指Causal Decoder(因果解碼器)架構。
- 適合生成任務。
- Decoder-Only 模型的任務適配性更好。“純生成”任務(如對話、續寫)沒有明確的“輸入”和“輸出”分界,引入Encoder會顯得多余。
- Decoder-Only 模型更適合生成任務。很多實際應用更關注生成的連貫性和語義豐富性,而不是對輸入的復雜理解。
- 泛化性能更好。
- 蘇神提出了”注意力滿秩問題“。即Decoder-only架構的Attention矩陣一定是滿秩的,這代表更強的表達能力,而雙向注意力反而會變得不足。
- 在純解碼器Decoder-Only架構中,由于因果掩碼(防止模型看到未來的標記Token),注意力矩陣被限制為下三角形式,理論上可以保持其全秩狀態:對角線上的每個元素(代表自注意力)都有助于使行列式為正(只有 Softmax 才能得到正結果)。全秩意味著理論上更強的表達能力。
- 另外兩種生成式架構都引入了雙向注意力,因此無法保證其注意力矩陣的全秩狀態。直覺上這是有道理的。雙向注意力是一把雙刃劍:它能加快學習過程,但也會破壞模型學習生成所必需的更深層預測模式。你可以把它想象成學習如何寫作:填空比逐字逐句地寫出整篇文章更容易,但這是一種不太有效的練習方式。不過,經過大量訓練后,這兩種方法都能達到學習如何寫作的目的。
- Encoder-Decoder模型因為可以看到雙向,雖然預測時候有優勢,但是訓練時候降低了學習難度,導致上限不高。而Decoder-Only 模型在模型足夠大、數據足夠多時,其學習通用表征的上限更高。
- 論文"What Language Model Architecture and Pretraining Objective Work Best for Zero-Shot Generalization" 比較了各種架構和預訓練方法的組合。他們發現:
- Decoder-Only 模型在沒有任何tuning數據的情況下、zero-shot表現最好。我們的實驗表明,在純粹的自監督預訓練后,根據自回歸語言建模目標訓練的純因果解碼器模型表現出最強的零樣本泛化能力。
- 而encoder-decoder則需要在一定量的標注數據上做multitask finetuning才能激發最佳性能。然而,在實驗中,對具有非因果可見性的輸入來說,先使用基于掩碼語言建模目標訓練,然后進行多任務微調的模型性能最好。
- Decoder-Only 模型是Casual attention,具備隱式的位置編碼功能,可以打破Transformer的位置不變性。而雙向attention的模型如果不帶位置編碼,則對語序區分能力較弱。
- 從提示詞中進行上下文學習。在使用 LLM 時,我們可以采用提示詞工程方法,例如提供少量實例來幫助 LLM 理解上下文或任務。在論文“Why Can GPT Learn In-Context? Language Models Secretly Perform Gradient Descent as Meta-Optimizers”中,研究人員用數學方法證明,這種上下文信息可以被視為具有與梯度下降類似的效果,可以更新零樣本的注意力權重。如果我們把提示詞看作是給注意力權重引入梯度,那么我們或許可以期待它對Decoder-Only模型產生更直接的效果,因為它在用于生成任務之前不需要先轉化為中間語境的特征表示。從邏輯上講,它應該仍然適用于Encoder-Decoder架構,但這需要對編碼器進行仔細調整,使其達到最佳性能,而這可能比較困難。
- 蘇神提出了”注意力滿秩問題“。即Decoder-only架構的Attention矩陣一定是滿秩的,這代表更強的表達能力,而雙向注意力反而會變得不足。
- 高效性。
- Decoder-Only 模型在同一個模塊中處理輸入序列與輸出序列,避免了模型結構的復雜化。根據奧卡姆剃刀原理:如果你有兩個相互競爭的觀點來解釋同一現象,你應該選擇更簡單的觀點。我們應該更傾向于只使用解碼器的模型結構。
- Decoder-Only 模型在推理過程只需一次向前傳播,而不是 Encoder 和 Decoder 分別進行向前傳播,推理效率更高。
- Decoder-Only 模型支持一直復用KV Cache,對多輪對話更友好。而其它兩種架構難以做到。在純解碼器模型(Decoder-Only)中,先前Token的鍵(K)和值(V)矩陣可以在解碼過程中重復用于后面的標記Token。由于每個位置只關注之前的Token(由于因果注意力機制),因此這些標記Token的 K 和 V 矩陣保持不變。這種緩存機制避免了為已經處理過的標記Token重新計算 K 和 V 矩陣,從而提高了效率,有利于在自回歸模型(如 GPT)的推理過程中加快生成速度并降低計算成本。
- 利用Scale Up。Encoder-Decoder 架構網絡不是均勻對稱的(不是線性而是有大量的分叉),導致數據依賴關系復雜,難以并行優化。而Decoder-Only 架構沒有此問題。
- 訓練數據效率高,訓練成本低。
- Decoder-Only 模型的訓練目標是預測下一個 Token(Next Token Prediction),這是大規模預訓練任務的核心目標。這種目標與網絡架構直接對齊,能高效利用海量的非結構化文本數據。
- Causal Decoder 模型因其強大的零樣本泛化能力而表現出色,這與當前的慣例--在大規模語料庫上進行自我監督學習十分契合。
- 而Encoder-Decoder 模型需要額外設計輸入輸出配對的數據。要實現 Encoder-Decoder結構的最大潛力,我們需要對標注數據進行多任務微調(基本上就是指令微調),這可能會非常昂貴,尤其是對于大型模型而言。
0xFF 參考
解剖Transformer 第二部分:你會用注意力機制組裝出一個Transformer嗎? 大方
A Learning Algorithm for Continually Running Fully Recurrent Neural Networks
Scheduled Sampling for Sequence Prediction with Recurrent Neural Networks
Professor Forcing: A New Algorithm for Training Recurrent Networks, 2016. Section 10.2.1, Teacher Forcing and Networks with Output Recurrence, Deep Learning, Ian Goodfellow, Yoshua Bengio, Aaron Courville, 2016.
為什么現在的LLM都是Decoder-only的架構? 蘇劍林
Transformer 三大變體之Decoder-Only模型詳解 浪子 [牛山AI公園]
Transformer系列:圖文詳解Decoder解碼器原理 xiaogp
Transformer中的解碼器詳解 浪子 牛山AI公園
Transformer中的編碼器詳解 浪子 牛山AI公園
Decoder-only的LLM為什么需要位置編碼? 蘇劍林
為什么大多數LLM只使用Decoder-Only結構? AI算法之道
Transformers基本原理—Decoder如何進行解碼? Python伊甸園
Why Can GPT Learn In-Context? Language Models Secretly Perform Gradient Descent as Meta-Optimizers. Retrieved from https://arxiv.org/abs/2212.10559 Dai, D., Sun, Y., Dong, L., Hao, Y., Sui, Z., & Wei, F. (2022).
浙公網安備 33010602011771號