Transformer:GPT背后的"造腦工程"全解析(含手搓過程)
Transformer 是人工智能領域的革命性架構,通過自注意力機制讓模型像人類一樣"全局理解"上下文關系。它摒棄傳統循環結構,采用并行計算實現高效訓練,配合位置編碼破解序列的時空密碼,在機器翻譯、文本生成等任務中實現質的飛躍。GPT、BERT等頂尖模型均基于Transformer,其多頭注意力設計如同給AI裝上"多核大腦",可同時捕捉詞語間的語法、語義、指代等多維關系,成為通向通用人工智能的重要基石。
一、從"人工智障"到"智能涌現":Transformer的降維打擊
震撼對比實驗:
使用相同訓練數據(維基百科+圖書語料)
- RNN模型:“巴黎是法國的首都,位于__” → “塞納河畔”(正確率68%)
- Transformer:“巴黎是法國的首都,位于__” → “北部法蘭西島大區”(正確率92%)
傳統模型三大痛點:
- 梯度消失:長距離依賴難以捕捉(如"雖然…但是…"結構)
- 計算低效:無法并行處理序列數據
- 記憶瓶頸:固定長度上下文窗口
?
二、位置編碼
transfomer的其他結構均在之前文章有過涉及,這里著重講一下位置編碼。
由于Transformer模型不使?循環神經?絡,因此?法從序列中學習到位置信息。為了解決這個問題,需要為輸?序列添加位置編碼,將每個詞的位置信息加?詞向量中。
通過位置編碼加入每一個token的位置信息
圖中的類似于太極圖的那個符號其實是“正弦”符號。正弦位置編碼使?不同頻率的正弦和余弦函數對每個位置進?編碼。編碼后,每個位置都會得到?個固定的位置編碼,與詞向量拼接或相加后,可以作為模型的輸?。
正弦位置編碼具有平滑性和保留相對位置信息等優點,因此在原始的Transformer論?中被采?。當然,也有其他位置編碼?法,如可學習的位置編碼,它將位置信息作為模型參數進?學習。
三、分部手搓Transformer核心組件
這個逐步拆解的過程是從中?到兩邊、從左到右進?的。也就是從中?組件到外圍延展,從編碼器到解碼器延展,然后把它們組合成Transformer類。
以下是代碼的關鍵組件。
(1)多頭?注意?:通過ScaledDotProductAttention
類實現縮放點積注意?機制,然后通過MultiHeadAttention
類實現多頭?注意?機制。
(2)逐位置前饋?絡:通過PoswiseFeedForwardNet
類實現逐位置前饋?絡。
(3)正弦位置編碼表:通過get_sin_code_table
函數?成正弦位置編碼表。
(4)填充掩碼:通過get_attn_pad_mask
函數為填充令牌?成注意?掩碼,避免注意?機制關注??的信息。
(5)編碼器層:通過EncoderLayer
類定義編碼器的單層。
(6)編碼器:通過Encoder
類定義Transformer
完整的編碼器部分。
(7)后續掩碼:通過get_attn_subsequent_mask
函數為后續令牌(當前位置后?的信息)?成注意?掩碼,避免解碼器中的注意?機制“偷窺”未來的?標數據。
(8)解碼器層:通過DecoderLayer
類定義解碼器的單層。
(9)解碼器:通過Decoder
類定義Transformer
完整的解碼器部分。
(10)Transformer
類:此類將編碼器和解碼器整合為完整的Transformer
模型。
3.1 多頭自注意力(包含殘差連接和歸一化)
多頭自注意力的結構如下:

這?我們有兩個?組件:ScaledDotProductAttention
(縮放點積注意?)類和MultiHeadAttention
(多頭?注意?)類。它們在Transformer架構中負責實現?注意?機制。
其中,ScaledDotProductAttention
類是構成MultiHeadAttention
類的組件元素,也就是說,在多頭?注意?中的每?個頭,都使?縮放點積注意?來實現。
import numpy as np # 導入 numpy 庫
import torch # 導入 torch 庫
import torch.nn as nn # 導入 torch.nn 庫
d_k = 64 # K(=Q) 維度
d_v = 64 # V 維度
# 定義縮放點積注意力類
class ScaledDotProductAttention(nn.Module):def __init__(self):super(ScaledDotProductAttention, self).__init__() def forward(self, Q, K, V, attn_mask):#------------------------- 維度信息 -------------------------------- # Q K V [batch_size, n_heads, len_q/k/v, dim_q=k/v] (dim_q=dim_k)# attn_mask [batch_size, n_heads, len_q, len_k]#----------------------------------------------------------------# 計算注意力分數(原始權重)[batch_size,n_heads,len_q,len_k]scores = torch.matmul(Q, K.transpose(-1, -2)) / np.sqrt(d_k) #------------------------- 維度信息 -------------------------------- # scores [batch_size, n_heads, len_q, len_k]#----------------------------------------------------------------- # 使用注意力掩碼,將 attn_mask 中值為 1 的位置的權重替換為極小值#------------------------- 維度信息 -------------------------------- # attn_mask [batch_size, n_heads, len_q, len_k], 形狀和 scores 相同#----------------------------------------------------------------- scores.masked_fill_(attn_mask, -1e9) # 對注意力分數進行 softmax 歸一化weights = nn.Softmax(dim=-1)(scores) #------------------------- 維度信息 -------------------------------- # weights [batch_size, n_heads, len_q, len_k], 形狀和 scores 相同#----------------------------------------------------------------- # 計算上下文向量(也就是注意力的輸出), 是上下文信息的緊湊表示context = torch.matmul(weights, V) #------------------------- 維度信息 -------------------------------- # context [batch_size, n_heads, len_q, dim_v]#----------------------------------------------------------------- return context, weights # 返回上下文向量和注意力分數
整個過程配合代碼如下圖所示:

前向傳播(forward):
def forward(self, Q, K, V, attn_mask):
Q
: 查詢向量 (query),形狀是[batch_size, n_heads, len_q, dim_q]
。K
: 鍵向量 (key),形狀是[batch_size, n_heads, len_k, dim_k]
。V
: 值向量 (value),形狀是[batch_size, n_heads, len_v, dim_v]
。attn_mask
: 注意力掩碼 (mask),形狀是[batch_size, n_heads, len_q, len_k]
。用于在計算注意力時屏蔽某些位置(例如在解碼器中,避免未來位置被看到)。
?
應用掩碼:
scores.masked_fill_(attn_mask, -1e9)
- 掩碼應用:
attn_mask
具有和scores
相同的形狀([batch_size, n_heads, len_q, len_k]
)。將attn_mask
中為1
的位置替換為一個非常小的值-1e9
,這些小值在后續的softmax
操作中會被“屏蔽”,即變為 0,避免這些位置的注意力權重被關注。
即1是需要忽略的部分,0是不需要忽略的部分。
attn_mask生成方式通常取決于以下幾個因素:
- 解碼器中的未來掩碼(用于防止信息泄漏)
在 Transformer 的解碼器中,我們需要確保模型只能看到當前時刻及之前的詞,而不能看到未來時刻的詞。例如,當前時刻的第 t
個位置的查詢 Q[t]
應該只依賴于前 t
個位置的鍵 K
和對應的值 V
。這樣做的目的是防止在訓練時未來信息泄漏。
例如,如果序列長度是 4
,attn_mask
應該是一個上三角矩陣,表示模型不能看到未來時刻的內容。具體來說,attn_mask
會是一個形狀為 [batch_size, n_heads, len_q, len_k]
的矩陣,其中 attn_mask[i, j, p, q] = 1
表示第 i
個樣本、第 j
個頭、第 p
個查詢位置和第 q
個鍵位置之間的注意力需要被屏蔽。
舉個例子,假設 attn_mask
為:
[1, 0, 0, 0] # 第 0 個位置只能看到自己
[1, 1, 0, 0] # 第 1 個位置可以看到自己和第 0 個位置
[1, 1, 1, 0] # 第 2 個位置可以看到自己和前兩個位置
[1, 1, 1, 1] # 第 3 個位置可以看到自己和前三個位置
這樣,對于 attn_mask
中的每個位置為 1
的部分,scores
中對應的位置會被屏蔽(設為極小的值 -1e9
),從而避免模型在生成時刻 t
的預測時“看到”未來的信息。
- 填充(Padding)掩碼(用于忽略填充位置)
在處理變長輸入序列時,序列中的某些位置可能是填充符(<PAD>
),這些填充符并不包含實際的信息,因此我們希望忽略它們對注意力計算的影響。為了避免填充符影響模型的注意力計算,我們會將填充符對應的位置的 attn_mask
設置為 1
(表示屏蔽這些位置)。
假設輸入序列是:
[1, 2, 3, 0, 0] # 1, 2, 3 是實際內容,0 是填充符
對應的 attn_mask
可以是:
[0, 0, 0, 1, 1] # 填充符位置被標記為 1,表示要屏蔽
這樣,attn_mask
中為 1
的位置就會在計算注意力時被屏蔽,確保填充符不會影響計算。
- 其他任務相關掩碼
有時,attn_mask
也可以根據特定任務的需求自定義。例如,某些任務可能要求在計算注意力時忽略特定的區域,或者僅在特定的部分計算注意力。這種情況通常是通過任務外部的邏輯生成掩碼。
?
下?定義多頭?注意?另?個?組件,多頭?注意?類(這?同時包含殘差連接和層歸?化操作)
# 定義多頭自注意力類
d_embedding = 512 # Embedding 的維度
n_heads = 8 # Multi-Head Attention 中頭的個數
batch_size = 3 # 每一批的數據大小
class MultiHeadAttention(nn.Module):def __init__(self):super(MultiHeadAttention, self).__init__()self.W_Q = nn.Linear(d_embedding, d_k * n_heads) # Q的線性變換層self.W_K = nn.Linear(d_embedding, d_k * n_heads) # K的線性變換層self.W_V = nn.Linear(d_embedding, d_v * n_heads) # V的線性變換層self.linear = nn.Linear(n_heads * d_v, d_embedding)self.layer_norm = nn.LayerNorm(d_embedding)def forward(self, Q, K, V, attn_mask): #------------------------- 維度信息 -------------------------------- # Q K V [batch_size, len_q/k/v, embedding_dim] #----------------------------------------------------------------- residual, batch_size = Q, Q.size(0) # 保留殘差連接# 將輸入進行線性變換和重塑,以便后續處理q_s = self.W_Q(Q).view(batch_size, -1, n_heads, d_k).transpose(1,2) k_s = self.W_K(K).view(batch_size, -1, n_heads, d_k).transpose(1,2)v_s = self.W_V(V).view(batch_size, -1, n_heads, d_v).transpose(1,2)#------------------------- 維度信息 -------------------------------- # q_s k_s v_s: [batch_size, n_heads, len_q/k/v, d_q=k/v]#----------------------------------------------------------------- # 將注意力掩碼復制到多頭 attn_mask: [batch_size, n_heads, len_q, len_k]attn_mask = attn_mask.unsqueeze(1).repeat(1, n_heads, 1, 1)#------------------------- 維度信息 -------------------------------- # attn_mask [batch_size, n_heads, len_q, len_k]#----------------------------------------------------------------- # 使用縮放點積注意力計算上下文和注意力權重context, weights = ScaledDotProductAttention()(q_s, k_s, v_s, attn_mask)#------------------------- 維度信息 -------------------------------- # context [batch_size, n_heads, len_q, dim_v]# weights [batch_size, n_heads, len_q, len_k]#----------------------------------------------------------------- # 通過調整維度將多個頭的上下文向量連接在一起context = context.transpose(1, 2).contiguous().view(batch_size, -1, n_heads * d_v) #------------------------- 維度信息 -------------------------------- # context [batch_size, len_q, n_heads * dim_v]#----------------------------------------------------------------- # 用一個線性層把連接后的多頭自注意力結果轉換,原始地嵌入維度output = self.linear(context) #------------------------- 維度信息 -------------------------------- # output [batch_size, len_q, embedding_dim]#----------------------------------------------------------------- # 與輸入 (Q) 進行殘差鏈接,并進行層歸一化后輸出output = self.layer_norm(output + residual)#------------------------- 維度信息 -------------------------------- # output [batch_size, len_q, embedding_dim]#----------------------------------------------------------------- return output, weights # 返回層歸一化的輸出和注意力權重
將輸?進?線性變換和重塑,就是為了形成多個頭
?
3.2 逐位置前饋網絡(包含殘差連接和層歸一化)
前饋神經?絡(Feed-Forward Network)我們都了解,是?個包含全連接層的神經絡。這種?絡在計算過程中是按照從輸?到輸出的?向進?前饋傳播的。
但是這個“Position- wise”如何理解?
在這?,“Poswise”或“Position-wise”是指這個前饋神經?絡獨?地作?在輸?序列的每個位置(即token)上,也就是對?注意?機制處理后的結果上的各個位置進?獨?處理,?不是把?注意?結果展平之后,以?個?的?維張量的形式整體輸?前饋?絡。這意味著對于序列中的每個位置,我們都在該位置應?相同的神經?絡,做相同的處理,并且不會受到其他位置的影響。因此,逐位置操作保持了輸?序列的原始順序
所以?論是多頭?注意?組件,還是前饋神經?絡組件,都嚴格地保證“隊形”,不打亂、不整合、不循環,?這種對序列位置信息的完整保持和并?處理,正是Transformer的核?思路。
# 定義逐位置前饋網絡類
class PoswiseFeedForwardNet(nn.Module):def __init__(self, d_ff=2048):super(PoswiseFeedForwardNet, self).__init__()# 定義一維卷積層 1,用于將輸入映射到更高維度self.conv1 = nn.Conv1d(in_channels=d_embedding, out_channels=d_ff, kernel_size=1)# 定義一維卷積層 2,用于將輸入映射回原始維度self.conv2 = nn.Conv1d(in_channels=d_ff, out_channels=d_embedding, kernel_size=1)# 定義層歸一化self.layer_norm = nn.LayerNorm(d_embedding)def forward(self, inputs): #------------------------- 維度信息 -------------------------------- # inputs [batch_size, len_q, embedding_dim]#---------------------------------------------------------------- residual = inputs # 保留殘差連接 # 在卷積層 1 后使用 ReLU 激活函數 output = nn.ReLU()(self.conv1(inputs.transpose(1, 2))) #------------------------- 維度信息 -------------------------------- # output [batch_size, d_ff, len_q]#----------------------------------------------------------------# 使用卷積層 2 進行降維 output = self.conv2(output).transpose(1, 2) #------------------------- 維度信息 -------------------------------- # output [batch_size, len_q, embedding_dim]#----------------------------------------------------------------# 與輸入進行殘差鏈接,并進行層歸一化output = self.layer_norm(output + residual) #------------------------- 維度信息 -------------------------------- # output [batch_size, len_q, embedding_dim]#----------------------------------------------------------------return output # 返回加入殘差連接后層歸一化的結果
PoswiseFeedForwardNet類實現了逐位置前饋?絡,?于處理Transformer中?注意?機制的輸出。其中包含兩個?維卷積層,它們?個負責將輸?映射到更?維度,?個再把它映射回原始維度。在兩個卷積層之間,使?了ReLU
函數。
在這?,??維卷積層代替了論?中的全連接層(線性層)來實現前饋神經?絡。其原因是全連接層不共享權重,??維卷積層在各個位置上共享權重,所以能夠減少?絡參數的數量。
?維卷積層的?作原理是將卷積核(也稱為過濾器或特征映射)沿輸?序列的?個維度滑動(如下圖所示),并在每個位置進?點積操作。在這種情況下,我們使???為1的卷積核。這樣,卷積操作實際上只會在輸?序列的?個位置進?計算,因此它能夠獨?地處理輸?序列中的每個位置。

在PoswiseFeedForwardNet類中,?先通過使?conv1的多個卷積核將輸?序列映射到更?的維度(程序中是2048維,這是?個可調節的超參數),并應?ReLU函數。
接著,conv2將映射后的序列降維到原始維度。這個過程在輸?序列的每個位置上都是獨?完成的,因為?維卷積層會在每個位置進?逐點操作。所以,逐位置前饋神經?絡能夠在每個位置上分別應?相同的運算,從?捕捉輸?序列中各個位置的信息。
逐位置前饋神經網絡有下??個作?。
(1)增強模型的表達能?。FFN為模型提供了更強?的表達能?,使其能夠捕捉輸?序列中更復雜的模式。通過逐位置前饋神經?絡和?注意?機制的組合,Transformer可以學習到不同位置之間的?距離依賴關系。
(2)信息融合。==FFN可以將?注意?機制輸出的信息進?融合。==每個位置上的信息在經過FFN后,都會得到?個新表示。這個新表示可以看作原始信息在經過?定程度的?線性變換之后的結果。
(3)層間傳遞。在Transformer中,逐位置前饋神經?絡將在每個編碼器和解碼器層中使?。
這樣可以確保每?層的輸出都經過了FFN的處理,從?在多層次上捕捉到序列中的特征。多頭?注意?層和逐位置前饋神經?絡層是編碼器層結構中的兩個主要組件,不過,在開始構建編碼器層之前,還要再定義兩個輔助性的組件。第?個是位置編碼表,第?個是?成填充注意?掩碼的函數。
?
3.3 正弦編碼表
Transformer模型的并?結構導致它不是按位置順序來處理序列的,但是在處理序列尤其是注意?計算的過程中,仍需要位置信息來幫助捕捉序列中的順序關系。為了解決這個問題,需要向輸?序列中添加位置編碼。
Tansformer的原始論?中使?的是正弦位置編碼。它的計算公式如下:
P E ( p o s , 2 i ) = sin ? ( p o s 1000 0 2 i / d ) PE(\mathrm{pos},2i)=\sin\left(\frac{\mathrm{pos}}{10000^{2i/d}}\right) PE(pos,2i)=sin(100002i/dpos?)
P E ( p o s , 2 i + 1 ) = cos ? ( p o s 1000 0 2 i l d ) PE(\mathrm{pos},2i+1)=\cos\left(\frac{\mathrm{pos}}{10000^{2ild}}\right) PE(pos,2i+1)=cos(100002ildpos?)
這種位置編碼?式具有周期性和連續性的特點,可以讓模型學會捕捉位置之間的相對關系和全
局關系。這個公式可以?于計算位置嵌?向量中每個維度的?度值。
■ pos:單詞/標記在句?中的位置,從0到seq_len-1。
■ d:單詞/標記嵌?向量的維度embedding_dim。
■ i:嵌?向量中的每個維度,從0到 d 2 ? 1 \frac{d}{2}-1 2d??1
# 生成正弦位置編碼表的函數,用于在 Transformer 中引入位置信息
def get_sin_enc_table(n_position, embedding_dim):#------------------------- 維度信息 --------------------------------# n_position: 輸入序列的最大長度# embedding_dim: 詞嵌入向量的維度#----------------------------------------------------------------- # 根據位置和維度信息,初始化正弦位置編碼表sinusoid_table = np.zeros((n_position, embedding_dim)) # 遍歷所有位置和維度,計算角度值for pos_i in range(n_position):for hid_j in range(embedding_dim):angle = pos_i / np.power(10000, 2 * (hid_j // 2) / embedding_dim)sinusoid_table[pos_i, hid_j] = angle # 計算正弦和余弦值sinusoid_table[:, 0::2] = np.sin(sinusoid_table[:, 0::2]) # dim 2i 偶數維sinusoid_table[:, 1::2] = np.cos(sinusoid_table[:, 1::2]) # dim 2i+1 奇數維 #------------------------- 維度信息 --------------------------------# sinusoid_table 的維度是 [n_position, embedding_dim]#---------------------------------------------------------------- return torch.FloatTensor(sinusoid_table) # 返回正弦位置編碼表
事實上,使?1、2、3、4等?然數序列作為位置編碼確實可以為序列中的不同位置提供區分性。然?,這種?法可能在某些??不如正弦和余弦函數?成的位置嵌?向量有效。
當我們使??然數序列作為位置編碼時,這些編碼是線性的。這意味著相鄰位置之間的差異在整個序列中保持恒定。然?,在許多任務中,不同位置之間的關系可能更復雜,可能需要?種能夠捕捉到這種復雜關系的編碼?法。
正弦和余弦函數?成的位置嵌?向量具有周期性和正交性,因此可以產?在各個尺度上都有區分性的位置嵌?。這使得模型可以更容易地學習到序列中不同位置之間的關系,特別是在捕捉?距離依賴關系時可能表現得更好。
所以,雖然使??然數序列(1、2、3、4等)作為位置編碼可以做?定的區分,但正弦和余弦函數?成的位置嵌?向量在捕捉序列中更復雜的位置關系??更具優勢。
?
3.4 填充掩碼
在NLP任務中,輸?序列的?度通常是不固定的。為了能夠同時處理多個序列,我們需要將這些序列填充到相同的?度,將不等?的序列補充到等?,這樣才能將它們整合成同?個批次進?訓練。
通常使??個特殊的標記(如,編碼后這個token的值通常是0)來表示填充部分。
然?,這些填充符號并沒有實際的含義,所以我們希望模型在計算注意?時忽略它們。因此,在編碼器的輸?部分,我們使?了填充位的注意?掩碼機制(如下?圖所示)。這個掩碼機制的作?是在注意?計算的時候把??的信息屏蔽,防?模型在計算注意?權重時關注到填充位。

# 定義填充注意力掩碼函數
def get_attn_pad_mask(seq_q, seq_k):#------------------------- 維度信息 --------------------------------# seq_q 的維度是 [batch_size, len_q]# seq_k 的維度是 [batch_size, len_k]#-----------------------------------------------------------------batch_size, len_q = seq_q.size()batch_size, len_k = seq_k.size()# 生成布爾類型張量pad_attn_mask = seq_k.data.eq(0).unsqueeze(1) # <PAD>token 的編碼值為 0#------------------------- 維度信息 --------------------------------# pad_attn_mask 的維度是 [batch_size,1,len_k]#-----------------------------------------------------------------# 變形為與注意力分數相同形狀的張量 pad_attn_mask = pad_attn_mask.expand(batch_size, len_q, len_k)#------------------------- 維度信息 --------------------------------# pad_attn_mask 的維度是 [batch_size,len_q,len_k]#-----------------------------------------------------------------return pad_attn_mask
我們為填充的?本序列創建?個與其形狀相同的?維矩陣,稱為填充掩碼矩陣。填充掩碼矩陣的?的是在注意?計算中屏蔽填充位置的影響。屏蔽流程如下。
(1)根據輸??本序列創建?個與其形狀相同的?維矩陣。對于原始?本中的每個單詞,矩陣中對應位置填充0;對于填充的符號,矩陣中對應位置填充1。
(2)為了將填充部分的權重降?接近負?窮,我們可以先將填充掩碼矩陣中的1替換為?個?常?的負數(例如-1e9),再將處理后的填充掩碼矩陣與注意?分數矩陣進?元素相加。這樣,有意義的token加了0,值保持不變,?填充部分加了?窮?值,在注意?分數矩陣中的權重就會變得?常?。
(3)對注意?分數矩陣應?softmax函數進?歸?化。由于填充部分的權重接近負?窮,softmax函數會使其歸?化后的權重接近于0。這樣,模型在計算注意?時就能夠忽略填充部分的信息,專注于序列中實際包含的有效內容。
?
3.5 編碼器層
有了多頭?注意?和逐位置前饋?絡這兩個主要組件,以及正弦位置編碼表和填充掩碼這兩個輔助函數后,現在我們終于可以搭建編碼器層這個核?組件了。
# 定義編碼器層類
class EncoderLayer(nn.Module):def __init__(self):super(EncoderLayer, self).__init__() self.enc_self_attn = MultiHeadAttention() # 多頭自注意力層 self.pos_ffn = PoswiseFeedForwardNet() # 位置前饋神經網絡層def forward(self, enc_inputs, enc_self_attn_mask):#------------------------- 維度信息 --------------------------------# enc_inputs 的維度是 [batch_size, seq_len, embedding_dim]# enc_self_attn_mask 的維度是 [batch_size, seq_len, seq_len]#-----------------------------------------------------------------# 將相同的 Q,K,V 輸入多頭自注意力層 , 返回的 attn_weights 增加了頭數 enc_outputs, attn_weights = self.enc_self_attn(enc_inputs, enc_inputs,enc_inputs, enc_self_attn_mask)#------------------------- 維度信息 --------------------------------# enc_outputs 的維度是 [batch_size, seq_len, embedding_dim] # attn_weights 的維度是 [batch_size, n_heads, seq_len, seq_len] # 將多頭自注意力 outputs 輸入位置前饋神經網絡層enc_outputs = self.pos_ffn(enc_outputs) # 維度與 enc_inputs 相同#------------------------- 維度信息 --------------------------------# enc_outputs 的維度是 [batch_size, seq_len, embedding_dim] #-----------------------------------------------------------------return enc_outputs, attn_weights # 返回編碼器輸出和每層編碼器注意力權重
這個類將多頭自注意力層和位置前饋神經網絡層組合在一起,并完成前向傳播的計算。
EncoderLayer
類將 多頭自注意力機制 和 位置前饋神經網絡 結合,完成了 Transformer 編碼器層的基本結構。具體過程是:
- 輸入經過多頭自注意力層,計算查詢與鍵的注意力權重,并生成上下文向量。
- 將上下文向量輸入到位置前饋神經網絡中,得到最終的編碼器輸出。
- 返回編碼器輸出和每層的注意力權重。
這種結構是 Transformer 編碼器的核心部分,支持在輸入序列中捕捉遠距離依賴并進行非線性變換。

?
3.6 編碼器
# 定義編碼器類
n_layers = 6 # 設置 Encoder 的層數
class Encoder(nn.Module):def __init__(self, corpus):super(Encoder, self).__init__() self.src_emb = nn.Embedding(len(corpus.src_vocab), d_embedding) # 詞嵌入層self.pos_emb = nn.Embedding.from_pretrained( \get_sin_enc_table(corpus.src_len+1, d_embedding), freeze=True) # 位置嵌入層self.layers = nn.ModuleList(EncoderLayer() for _ in range(n_layers))# 編碼器層數def forward(self, enc_inputs): #------------------------- 維度信息 --------------------------------# enc_inputs 的維度是 [batch_size, source_len]#-----------------------------------------------------------------# 創建一個從 1 到 source_len 的位置索引序列pos_indices = torch.arange(1, enc_inputs.size(1) + 1).unsqueeze(0).to(enc_inputs)#------------------------- 維度信息 --------------------------------# pos_indices 的維度是 [1, source_len]#----------------------------------------------------------------- # 對輸入進行詞嵌入和位置嵌入相加 [batch_size, source_len,embedding_dim]enc_outputs = self.src_emb(enc_inputs) + self.pos_emb(pos_indices)#------------------------- 維度信息 --------------------------------# enc_outputs 的維度是 [batch_size, seq_len, embedding_dim]#-----------------------------------------------------------------# 生成自注意力掩碼enc_self_attn_mask = get_attn_pad_mask(enc_inputs, enc_inputs) #------------------------- 維度信息 --------------------------------# enc_self_attn_mask 的維度是 [batch_size, len_q, len_k] #----------------------------------------------------------------- enc_self_attn_weights = [] # 初始化 enc_self_attn_weights# 通過編碼器層 [batch_size, seq_len, embedding_dim]for layer in self.layers: enc_outputs, enc_self_attn_weight = layer(enc_outputs, enc_self_attn_mask)enc_self_attn_weights.append(enc_self_attn_weight)#------------------------- 維度信息 --------------------------------# enc_outputs 的維度是 [batch_size, seq_len, embedding_dim] 維度與 enc_inputs 相同# enc_self_attn_weights 是一個列表,每個元素的維度是 [batch_size, n_heads, seq_len, seq_len] #-----------------------------------------------------------------return enc_outputs, enc_self_attn_weights # 返回編碼器輸出和編碼器注意力權重
這個編碼器類實現了Transformer模型中的編碼器部分,包括詞嵌?、位置嵌?和多個編碼器層。通過這個編碼器,可以處理輸?序列,并從中提取深層次的特征表示。這些特征表示可以直接應?于后續的任務,如序列到序列的?成任務(如機器翻譯)或者分類任務(如情感分析)等。
BERT模型就只包含Transformer模型中的編碼器部分,因此它很適合為各種NLP下游任務提供有?的特征表示。
編碼器的定義?此結束,下?我們進?解碼器組件。不過,在開始構建解碼器層之前,也有?個?組件需要說明,它就是?成后續注意?掩碼的函數。
?
3.7 后續掩碼
在?然語?處理中,尤其是Seq2Seq任務中,我們需要為解碼器提供正確的輸?,對于已經?成的部分,我們要讓解碼器看到序列是否正確,然后?正確的信息(Ground Truth)來預測下?個詞。但是與此同時,為了確保模型不會提前獲取未來的信息,我們?需要在注意?計算中遮住當前位置后?的信息(Subsequent Positions)。
所以,對序列中的第?個位置,我們需要遮住后?所有的詞;?對后?的詞,需要遮住的詞會逐漸減少(如下圖所示)。?如把“咖哥 喜歡 ?冰”這句話輸?解碼器,當對“咖哥”計算注意?時,解碼器不可以看到“喜歡”“?冰”這兩個詞。當對“喜歡”計算注意?時,解碼器可以看到“咖哥”,不能看到“?冰”,因為它正是需要根據“咖哥”和“喜歡”這個上下?,來猜測咖哥喜歡誰。當對最后?個詞"?冰"計算注意?的時候,前兩個詞就不是秘密了。

# 生成后續注意力掩碼的函數,用于在多頭自注意力計算中忽略未來信息
def get_attn_subsequent_mask(seq):#------------------------- 維度信息 --------------------------------# seq 的維度是 [batch_size, seq_len(Q)=seq_len(K)]#-----------------------------------------------------------------# 獲取輸入序列的形狀attn_shape = [seq.size(0), seq.size(1), seq.size(1)] #------------------------- 維度信息 --------------------------------# attn_shape 是一個一維張量 [batch_size, seq_len(Q), seq_len(K)]#-----------------------------------------------------------------# 使用 numpy 創建一個上三角矩陣(triu = triangle upper)subsequent_mask = np.triu(np.ones(attn_shape), k=1)#------------------------- 維度信息 --------------------------------# subsequent_mask 的維度是 [batch_size, seq_len(Q), seq_len(K)]#-----------------------------------------------------------------# 將 numpy 數組轉換為 PyTorch 張量,并將數據類型設置為 byte(布爾值)subsequent_mask = torch.from_numpy(subsequent_mask).byte()#------------------------- 維度信息 --------------------------------# 返回的 subsequent_mask 的維度是 [batch_size, seq_len(Q), seq_len(K)]#-----------------------------------------------------------------return subsequent_mask # 返回后續位置的注意力掩碼
此段代碼最終生成的是注意力掩碼,根據上圖第一行為例,因為咖哥只能看到自己來推測下面的詞,所以先寫出咖哥對整個句子的權重,在人為將看不到的地方取消關注(也就是替換成Zero weight),從上到下一行是一個時間的步長。
?
3.8 解碼器層
# 定義解碼器層類
class DecoderLayer(nn.Module):def __init__(self):super(DecoderLayer, self).__init__() self.dec_self_attn = MultiHeadAttention() # 多頭自注意力層 self.dec_enc_attn = MultiHeadAttention() # 多頭自注意力層,連接編碼器和解碼器 self.pos_ffn = PoswiseFeedForwardNet() # 位置前饋神經網絡層def forward(self, dec_inputs, enc_outputs, dec_self_attn_mask, dec_enc_attn_mask):#------------------------- 維度信息 --------------------------------# dec_inputs 的維度是 [batch_size, target_len, embedding_dim]# enc_outputs 的維度是 [batch_size, source_len, embedding_dim]# dec_self_attn_mask 的維度是 [batch_size, target_len, target_len]# dec_enc_attn_mask 的維度是 [batch_size, target_len, source_len]#----------------------------------------------------------------- # 將相同的 Q,K,V 輸入多頭自注意力層dec_outputs, dec_self_attn = self.dec_self_attn(dec_inputs, dec_inputs, dec_inputs, dec_self_attn_mask)#------------------------- 維度信息 --------------------------------# dec_outputs 的維度是 [batch_size, target_len, embedding_dim]# dec_self_attn 的維度是 [batch_size, n_heads, target_len, target_len]#----------------------------------------------------------------- # 將解碼器輸出和編碼器輸出輸入多頭自注意力層dec_outputs, dec_enc_attn = self.dec_enc_attn(dec_outputs, enc_outputs, enc_outputs, dec_enc_attn_mask)#------------------------- 維度信息 --------------------------------# dec_outputs 的維度是 [batch_size, target_len, embedding_dim]# dec_enc_attn 的維度是 [batch_size, n_heads, target_len, source_len]#----------------------------------------------------------------- # 輸入位置前饋神經網絡層dec_outputs = self.pos_ffn(dec_outputs)#------------------------- 維度信息 --------------------------------# dec_outputs 的維度是 [batch_size, target_len, embedding_dim]# dec_self_attn 的維度是 [batch_size, n_heads, target_len, target_len]# dec_enc_attn 的維度是 [batch_size, n_heads, target_len, source_len] #-----------------------------------------------------------------# 返回解碼器層輸出,每層的自注意力和解 - 編碼器注意力權重return dec_outputs, dec_self_attn, dec_enc_attn
定義了一個標準的解碼器層,通過三個主要步驟處理輸入:
- 自注意力:通過多頭自注意力機制理解目標語言的上下文。
- 編碼器-解碼器注意力:通過與編碼器的輸出進行交互,理解目標語言與源語言的關系。
- 前饋神經網絡:對解碼器輸出進行進一步的轉換和處理。
這些步驟共同作用,使得解碼器能夠生成目標語言的翻譯或輸出。

?
3.9 解碼器

解碼器類的實現代碼如下:
# 定義解碼器類
n_layers = 6 # 設置 Decoder 的層數
class Decoder(nn.Module):def __init__(self, corpus):super(Decoder, self).__init__()self.tgt_emb = nn.Embedding(len(corpus.tgt_vocab), d_embedding) # 詞嵌入層self.pos_emb = nn.Embedding.from_pretrained( \get_sin_enc_table(corpus.tgt_len+1, d_embedding), freeze=True) # 位置嵌入層 self.layers = nn.ModuleList([DecoderLayer() for _ in range(n_layers)]) # 疊加多層def forward(self, dec_inputs, enc_inputs, enc_outputs): #------------------------- 維度信息 --------------------------------# dec_inputs 的維度是 [batch_size, target_len]# enc_inputs 的維度是 [batch_size, source_len]# enc_outputs 的維度是 [batch_size, source_len, embedding_dim]#----------------------------------------------------------------- # 創建一個從 1 到 source_len 的位置索引序列pos_indices = torch.arange(1, dec_inputs.size(1) + 1).unsqueeze(0).to(dec_inputs)#------------------------- 維度信息 --------------------------------# pos_indices 的維度是 [1, target_len]#----------------------------------------------------------------- # 對輸入進行詞嵌入和位置嵌入相加dec_outputs = self.tgt_emb(dec_inputs) + self.pos_emb(pos_indices)#------------------------- 維度信息 --------------------------------# dec_outputs 的維度是 [batch_size, target_len, embedding_dim]#----------------------------------------------------------------- # 生成解碼器自注意力掩碼和解碼器 - 編碼器注意力掩碼dec_self_attn_pad_mask = get_attn_pad_mask(dec_inputs, dec_inputs) # 填充位掩碼dec_self_attn_subsequent_mask = get_attn_subsequent_mask(dec_inputs) # 后續位掩碼dec_self_attn_mask = torch.gt((dec_self_attn_pad_mask \+ dec_self_attn_subsequent_mask), 0) dec_enc_attn_mask = get_attn_pad_mask(dec_inputs, enc_inputs) # 解碼器 - 編碼器掩碼#------------------------- 維度信息 -------------------------------- # dec_self_attn_pad_mask 的維度是 [batch_size, target_len, target_len]# dec_self_attn_subsequent_mask 的維度是 [batch_size, target_len, target_len]# dec_self_attn_mask 的維度是 [batch_size, target_len, target_len]# dec_enc_attn_mask 的維度是 [batch_size, target_len, source_len]#----------------------------------------------------------------- dec_self_attns, dec_enc_attns = [], [] # 初始化 dec_self_attns, dec_enc_attns# 通過解碼器層 [batch_size, seq_len, embedding_dim]for layer in self.layers:dec_outputs, dec_self_attn, dec_enc_attn = layer(dec_outputs, enc_outputs, dec_self_attn_mask, dec_enc_attn_mask)dec_self_attns.append(dec_self_attn)dec_enc_attns.append(dec_enc_attn)#------------------------- 維度信息 --------------------------------# dec_outputs 的維度是 [batch_size, target_len, embedding_dim]# dec_self_attns 是一個列表,每個元素的維度是 [batch_size, n_heads, target_len, target_len]# dec_enc_attns 是一個列表,每個元素的維度是 [batch_size, n_heads, target_len, source_len]#----------------------------------------------------------------- # 返回解碼器輸出,解碼器自注意力和解碼器 - 編碼器注意力權重 return dec_outputs, dec_self_attns, dec_enc_attns
1.詞嵌入:輸入目標語言的詞索引,并結合位置編碼來生成解碼器的輸入。
2.掩碼計算:生成自注意力掩碼和解碼器-編碼器掩碼,確保模型不會使用未來信息或填充位置的信息。
3.多層解碼器:通過多層解碼器來處理輸入,生成目標語言的最終表示。
4.返回結果:解碼器的輸出和每一層的注意力權重。
?
3.10 transfomer類
# 定義 Transformer 模型
class Transformer(nn.Module):def __init__(self, corpus):super(Transformer, self).__init__() self.encoder = Encoder(corpus) # 初始化編碼器實例 self.decoder = Decoder(corpus) # 初始化解碼器實例# 定義線性投影層,將解碼器輸出轉換為目標詞匯表大小的概率分布self.projection = nn.Linear(d_embedding, len(corpus.tgt_vocab), bias=False)def forward(self, enc_inputs, dec_inputs):#------------------------- 維度信息 --------------------------------# enc_inputs 的維度是 [batch_size, source_seq_len]# dec_inputs 的維度是 [batch_size, target_seq_len]#----------------------------------------------------------------- # 將輸入傳遞給編碼器,并獲取編碼器輸出和自注意力權重 enc_outputs, enc_self_attns = self.encoder(enc_inputs)#------------------------- 維度信息 --------------------------------# enc_outputs 的維度是 [batch_size, source_len, embedding_dim]# enc_self_attns 是一個列表,每個元素的維度是 [batch_size, n_heads, src_seq_len, src_seq_len] #----------------------------------------------------------------- # 將編碼器輸出、解碼器輸入和編碼器輸入傳遞給解碼器# 獲取解碼器輸出、解碼器自注意力權重和編碼器 - 解碼器注意力權重 dec_outputs, dec_self_attns, dec_enc_attns = self.decoder(dec_inputs, enc_inputs, enc_outputs)#------------------------- 維度信息 --------------------------------# dec_outputs 的維度是 [batch_size, target_len, embedding_dim]# dec_self_attns 是一個列表,每個元素的維度是 [batch_size, n_heads, tgt_seq_len, src_seq_len]# dec_enc_attns 是一個列表,每個元素的維度是 [batch_size, n_heads, tgt_seq_len, src_seq_len] #----------------------------------------------------------------- # 將解碼器輸出傳遞給投影層,生成目標詞匯表大小的概率分布dec_logits = self.projection(dec_outputs) #------------------------- 維度信息 --------------------------------# dec_logits 的維度是 [batch_size, tgt_seq_len, tgt_vocab_size]#-----------------------------------------------------------------# 返回邏輯值 ( 原始預測結果 ), 編碼器自注意力權重,解碼器自注意力權重,解 - 編碼器注意力權重return dec_logits, enc_self_attns, dec_self_attns, dec_enc_attns
?先初始化編碼器、解碼器和投影層。在forward?法中,將源序列輸?傳遞給編碼器,獲取編碼器輸出和?注意?權重。然后將編碼器輸出、解碼器輸?和編碼器輸?傳遞給解碼器,獲取解碼器輸出、解碼器?注意?權重和編碼器-解碼器注意?權重。最后,將解碼器輸出傳遞給投影層,?成?標詞匯表??的概率分布。
這個概率分布將被?于計算損失和評估模型的性能。
?
四、舉一個栗子跑跑
4.1 數據準備
先準備幾個中英翻譯例句
sentences = [['咖哥 喜歡 小冰', 'KaGe likes XiaoBing'],['我 愛 學習 人工智能', 'I love studying AI'],['深度學習 改變 世界', ' DL changed the world'],['自然語言處理 很 強大', 'NLP is powerful'],['神經網絡 非常 復雜', 'Neural-networks are complex'] ]
然后,創建TranslationCorpus類,?于讀?中英翻譯語料,并?成字典和模型可以讀取的數據批次。
from collections import Counter # 導入 Counter 類
# 定義 TranslationCorpus 類
class TranslationCorpus:def __init__(self, sentences):self.sentences = sentences# 計算源語言和目標語言的最大句子長度,并分別加 1 和 2 以容納填充符和特殊符號self.src_len = max(len(sentence[0].split()) for sentence in sentences) + 1self.tgt_len = max(len(sentence[1].split()) for sentence in sentences) + 2# 創建源語言和目標語言的詞匯表self.src_vocab, self.tgt_vocab = self.create_vocabularies()# 創建索引到單詞的映射self.src_idx2word = {v: k for k, v in self.src_vocab.items()}self.tgt_idx2word = {v: k for k, v in self.tgt_vocab.items()}# 定義創建詞匯表的函數def create_vocabularies(self):# 統計源語言和目標語言的單詞頻率src_counter = Counter(word for sentence in self.sentences for word in sentence[0].split())tgt_counter = Counter(word for sentence in self.sentences for word in sentence[1].split()) # 創建源語言和目標語言的詞匯表,并為每個單詞分配一個唯一的索引src_vocab = {'<pad>': 0, **{word: i+1 for i, word in enumerate(src_counter)}}tgt_vocab = {'<pad>': 0, '<sos>': 1, '<eos>': 2, **{word: i+3 for i, word in enumerate(tgt_counter)}} return src_vocab, tgt_vocab# 定義創建批次數據的函數def make_batch(self, batch_size, test_batch=False):input_batch, output_batch, target_batch = [], [], []# 隨機選擇句子索引sentence_indices = torch.randperm(len(self.sentences))[:batch_size]for index in sentence_indices:src_sentence, tgt_sentence = self.sentences[index]# 將源語言和目標語言的句子轉換為索引序列src_seq = [self.src_vocab[word] for word in src_sentence.split()]tgt_seq = [self.tgt_vocab['<sos>']] + [self.tgt_vocab[word] \for word in tgt_sentence.split()] + [self.tgt_vocab['<eos>']] # 對源語言和目標語言的序列進行填充src_seq += [self.src_vocab['<pad>']] * (self.src_len - len(src_seq))tgt_seq += [self.tgt_vocab['<pad>']] * (self.tgt_len - len(tgt_seq)) # 將處理好的序列添加到批次中input_batch.append(src_seq)output_batch.append([self.tgt_vocab['<sos>']] + ([self.tgt_vocab['<pad>']] * \(self.tgt_len - 2)) if test_batch else tgt_seq[:-1])target_batch.append(tgt_seq[1:]) # 將批次轉換為 LongTensor 類型input_batch = torch.LongTensor(input_batch)output_batch = torch.LongTensor(output_batch)target_batch = torch.LongTensor(target_batch) return input_batch, output_batch, target_batch
# 創建語料庫類實例
corpus = TranslationCorpus(sentences)
4.2 訓練Transfomer模型
import torch # 導入 torch
import torch.optim as optim # 導入優化器
model = Transformer(corpus) # 創建模型實例
criterion = nn.CrossEntropyLoss() # 損失函數
optimizer = optim.Adam(model.parameters(), lr=0.0001) # 優化器
epochs = 5 # 訓練輪次
for epoch in range(epochs): # 訓練 100 輪optimizer.zero_grad() # 梯度清零enc_inputs, dec_inputs, target_batch = corpus.make_batch(batch_size) # 創建訓練數據 outputs, _, _, _ = model(enc_inputs, dec_inputs) # 獲取模型輸出 loss = criterion(outputs.view(-1, len(corpus.tgt_vocab)), target_batch.view(-1)) # 計算損失if (epoch + 1) % 1 == 0: # 打印損失print(f"Epoch: {epoch + 1:04d} cost = {loss:.6f}")loss.backward()# 反向傳播 optimizer.step()# 更新參數
訓練100輪之后,損失會減?到?個較?的值。
?
4.3 測試Transfomer模型
# 創建一個大小為 1 的批次,目標語言序列 dec_inputs 在測試階段,僅包含句子開始符號 <sos>
enc_inputs, dec_inputs, target_batch = corpus.make_batch(batch_size=1,test_batch=True)
print("編碼器輸入 :", enc_inputs) # 打印編碼器輸入
print("解碼器輸入 :", dec_inputs) # 打印解碼器輸入
print("目標數據 :", target_batch) # 打印目標數據
predict, enc_self_attns, dec_self_attns, dec_enc_attns = model(enc_inputs, dec_inputs) # 用模型進行翻譯
predict = predict.view(-1, len(corpus.tgt_vocab)) # 將預測結果維度重塑
predict = predict.data.max(1, keepdim=True)[1] # 找到每個位置概率最大的詞匯的索引
# 解碼預測的輸出,將所預測的目標句子中的索引轉換為單詞
translated_sentence = [corpus.tgt_idx2word[idx.item()] for idx in predict.squeeze()]
# 將輸入的源語言句子中的索引轉換為單詞
input_sentence = ' '.join([corpus.src_idx2word[idx.item()] for idx in enc_inputs[0]])
print(input_sentence, '->', translated_sentence) # 打印原始句子和翻譯后的句子
編碼器輸入 : tensor([[11, 12, 13, 0, 0]])解碼器輸入 : tensor([[1, 0, 0, 0, 0]])目標數據 : tensor([[14, 15, 16, 2, 0]])自然語言處理 很 強大 <pad> <pad> -> ['NLP', 'NLP', 'NLP', 'NLP', 'NLP']
這個Transformer能訓練,能?。不過,其輸出結果并不理想,模型只成功翻譯了?個單詞“NLP”,之后就不斷重復這個詞。
對于這樣簡單的數據集,在設計和選擇模型時,應該優先考慮簡單的模型,像Transformer這樣?較復雜的模型并不?定效果更好。
這次測試效果不理想的真正原因和模型的簡單或者復雜?關,主要是因為此處我們并沒有利?解碼器的?回歸機制進?逐位置(即逐詞、逐令牌、逐元素或逐時間步)的?成式輸出。