7.2 Attention Is All You Need:Transformer 架構全解(2017)#
一、歷史背景:一次徹底的減法#
2017 年 6 月,Google 的 Vaswani 等 8 位作者發表《Attention Is All You Need》。論文標題就是宣言:把 Seq2Seq 中保留循環結構、用注意力打補丁的做法徹底反轉——丟掉循環與卷積,只留注意力。這不是改良,是減法革命。
捨棄 RNN 的動機在 6.2 已經明朗:串行計算無法利用 GPU 平行度; \(O(n)\) 的信息路徑限制長程依賴。Transformer 的承諾同樣直白:訓練時間縮短一個數量級,翻譯品質創 SOTA(BLEU 28.4)。歷史證明其野心遠超翻譯:BERT(2018)、GPT(2018)、ViT(2020)、擴散模型的骨幹、直至今天的大型語言模型——Transformer 是深度學習統一化的載體,正如馮紐曼架構之於計算機。
timeline
title Transformer 誕生前夜的積木(2014-2017)
2014 : 軟尋址(記憶網路)/ Bahdanau 跨序列注意力
2015 : ResNet 殘差連接 / Luong 點積注意力
2016 : 層歸一化 / 序列內自注意力驗證
2017 : 縮放點積 + 多頭 + 位置編碼 → Transformer二、整體架構#
flowchart TB
IN["源序列 embedding + 位置編碼"] --> E1["編碼器 ×N<br>多頭自注意力 → 殘差&層歸一<br>FFN → 殘差&層歸一"]
E1 --> E2["K, V 供給解碼器"]
OUT["目標序列 embedding + 位置編碼"] --> D1["掩碼多頭自注意力<br>→ 殘差&層歸一"]
E2 --> D2["交叉注意力(Q 來自解碼器)<br>→ 殘差&層歸一"]
D1 --> D2
D2 --> D3["FFN → 殘差&層歸一"]
D3 --> SOFT["Linear + Softmax → 下一詞機率"]編碼器: \(N=6\) 個相同層堆疊,每層兩個子層(多頭自注意力 + 逐位置前饋網路),每個子層套殘差連接與層歸一化: \(\text{LayerNorm}(x + \text{Sublayer}(x))\) 。
解碼器:每層三個子層,自注意力加因果掩碼(防止看到未來),再加一個交叉注意力(Q 來自解碼器、K/V 來自編碼器輸出)——這正是 Bahdanau 注意力的直系後代。
三、核心組件的數學#
3.1 縮放點積注意力與多頭#
單頭注意力(7.1 已推導): \(\text{Attention}(Q, K, V) = \text{softmax}\!\big(\tfrac{QK^\top}{\sqrt{d_k}}\big)V\) 。
多頭:把 \(d_{model}=512\) 維投影到 \(h=8\) 個 \(d_k=d_v=64\) 的子空間,各自做注意力再拼接:
計算量與單頭 \(d_{model}\) 相同,但允許模型在不同的表示子空間關注不同的關係模式(句法關係、指代關係、位置近鄰……)。事後研究(如 Clark et al. 2019 的 BERT 注意力分析)證實確實有頭專門做語法依存、指代消解——「多頭 = 並行的關係偵測器」得到經驗支持。
3.2 位置編碼:為無序的注意力注入順序#
自注意力是置換等變的——打亂輸入順序,輸出同樣打亂,模型本身不知道「誰在誰前面」。循環結構原本免費提供順序,捨棄 RNN 後必須外掛順序信息。Transformer 用正弦位置編碼:
巧妙之處: \(PE_{pos+k}\) 可以表示為 \(PE_{pos}\) 的線性變換(正弦的相位移動等價於旋轉矩陣),模型因此易於學習相對位置關係;不同頻率組合可表達任意長度的位置模式。後續演化:可學習絕對位置(BERT/GPT)、相對位置編碼(Transformer-XL)、旋轉位置編碼 RoPE(2021,今日 LLM 主流)——位置表示至今仍是活躍的研究線,印證「捨棄循環的代價由位置編碼償還」。
3.3 逐位置前饋網路與殘差-歸一化骨架#
每層的 FFN 對每個位置獨立做兩層變換(中間維度 \(d_{ff}=2048\) ,先擴後縮):
分工理解:注意力負責「跨位置混合信息」,FFN 負責「逐位置的非線性加工」。FFN 參數量占全模型的約三分之二,近年研究(幾何解釋、記憶存儲)顯示它是知識存儲的主要場所。
殘差 + LayerNorm(4.2)在此的角色:注意力與 FFN 都是重參數化操作,殘差保證恆等通路(梯度高速公路,5.2),LayerNorm 穩定各層激活分布——Transformer 是 ResNet 的「門控-歸一化精神」在注意力世界的再生。
3.4 為何可並行:複雜度的三方對比#
| 每層計算複雜度 | 串行步數 | 長程路徑長度 | |
|---|---|---|---|
| RNN | \(O(n \cdot d^2)\) | \(O(n)\) | \(O(n)\) |
| CNN(kernel k) | \(O(k \cdot n \cdot d^2)\) | \(O(1)\) | \(O(\log_k n)\) |
| 自注意力 | \(O(n^2 \cdot d)\) | \(O(1)\) | \(O(1)\) |
自注意力以 \(O(n^2)\) 的平方代價換取 \(O(1)\) 串行步數與 \(O(1)\) 路徑長度。2017 年 \(n \sim 100\) ,平方代價微不足道,並行收益巨大;當 \(n\) 到達百萬詞(2026 的長上下文), \(O(n^2)\) 成為主要瓶頸,催生稀疏/線性注意力等研究(7.1 思考題 4)。架構的優劣永遠相對於硬體與問題規模——這是貫穿本書的方法論教訓。
四、程式碼實作:完整可運行的 Transformer#
4.1 從零實作編碼器層#
import torch, torch.nn as nn, math
class MultiHeadAttention(nn.Module):
def __init__(self, d_model=512, n_heads=8):
super().__init__()
assert d_model % n_heads == 0
self.d_k = d_model // n_heads
self.h = n_heads
self.qkv = nn.Linear(d_model, 3 * d_model) # Q,K,V 合一投影
self.out = nn.Linear(d_model, d_model)
def forward(self, x, mask=None):
B, T, D = x.shape
q, k, v = self.qkv(x).chunk(3, dim=-1) # (B, T, D) each
# 拆頭:(B, h, T, d_k)
q = q.view(B, T, self.h, self.d_k).transpose(1, 2)
k = k.view(B, T, self.h, self.d_k).transpose(1, 2)
v = v.view(B, T, self.h, self.d_k).transpose(1, 2)
scores = q @ k.transpose(-2, -1) / math.sqrt(self.d_k) # (B,h,T,T)
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
attn = torch.softmax(scores, dim=-1)
out = (attn @ v).transpose(1, 2).contiguous().view(B, T, D)
return self.out(out) # (B, T, D)
class EncoderLayer(nn.Module):
def __init__(self, d_model=512, n_heads=8, d_ff=2048, p=0.1):
super().__init__()
self.attn = MultiHeadAttention(d_model, n_heads)
self.ffn = nn.Sequential(nn.Linear(d_model, d_ff), nn.ReLU(),
nn.Linear(d_ff, d_model))
self.n1, self.n2 = nn.LayerNorm(d_model), nn.LayerNorm(d_model)
self.drop = nn.Dropout(p)
def forward(self, x, mask=None):
x = self.n1(x + self.drop(self.attn(x, mask))) # 殘差 + LN
x = self.n2(x + self.drop(self.ffn(x)))
return x
# 測試:並行性 —— 全序列一次前向,無時間迴圈
layer = EncoderLayer()
x = torch.randn(4, 50, 512) # batch=4, 句長=50
print(layer(x).shape) # (4, 50, 512),50 步同時處理!
4.2 位置編碼的性質驗證#
import torch, math
def positional_encoding(T, d_model):
pos = torch.arange(T).unsqueeze(1)
i = torch.arange(0, d_model, 2)
div = torch.exp(-i * math.log(10000) / d_model)
pe = torch.zeros(T, d_model)
pe[:, 0::2] = torch.sin(pos * div)
pe[:, 1::2] = torch.cos(pos * div)
return pe
pe = positional_encoding(100, 64)
# 性質 1:相對位置的線性變換性(內積只依賴相對距離)
dot = pe @ pe.T
print("任意兩行內積僅依賴 pos_i - pos_j:",
torch.allclose(dot[0, 5], dot[10, 15], atol=1e-4)) # True
# 性質 2:置換等變性 —— 沒有位置編碼的注意力不知道順序
x = torch.randn(1, 5, 8)
att = nn.MultiheadAttention(8, 2, batch_first=True)
out1, _ = att(x, x, x)
perm = torch.randperm(5)
out2, _ = att(x[:, perm], x[:, perm], x[:, perm])
print("打亂順序後輸出同樣打亂(置換等變):",
torch.allclose(out1[:, perm], out2, atol=1e-5)) # True
4.3 完整小型 Transformer 做翻譯任務#
import torch, torch.nn as nn, math
class TinyTransformer(nn.Module):
def __init__(self, src_v, tgt_v, d=128, h=4, layers=2):
super().__init__()
self.d = d
self.se, self.te = nn.Embedding(src_v, d), nn.Embedding(tgt_v, d)
enc = [EncoderLayer(d, h, 4*d) for _ in range(layers)]
self.enc = nn.ModuleList(enc)
self.dec_attn = nn.ModuleList(MultiHeadAttention(d, h) for _ in range(layers))
self.dec_ffn = nn.ModuleList(nn.Sequential(nn.Linear(d, 4*d), nn.ReLU(),
nn.Linear(4*d, d)) for _ in range(layers))
self.n = nn.ModuleList(nn.LayerNorm(d) for _ in range(4*layers))
self.out = nn.Linear(d, tgt_v)
def causal_mask(self, T):
return torch.tril(torch.ones(T, T)).bool()
def forward(self, src, tgt):
x = self.se(src) * math.sqrt(self.d) + positional_encoding(src.size(1), self.d)
for i, l in enumerate(self.enc): # 編碼器(無掩碼)
x = self.n[4*i](x + l.attn(x)); x = self.n[4*i+1](x + l.ffn(x))
y = self.te(tgt) * math.sqrt(self.d) + positional_encoding(tgt.size(1), self.d)
m = self.causal_mask(tgt.size(1)).to(y.device)
for i in range(len(self.enc)): # 解碼器(掩碼 + 交叉)
y = self.n[4*i+2](y + self.dec_attn[i](y, m.unsqueeze(0).unsqueeze(0)))
cross = MultiHeadAttention.attend(self.dec_attn[i], x, x, y) # Q=y, KV=x
y = y + cross
y = self.n[4*i+3](y + self.dec_ffn[i](y))
return self.out(y)
# MultiHeadAttention 需支援 attend(Q,K,V);完整實作見本書配套代碼庫
# 訓練流程與 6.2 的 Seq2Seq 相同(teacher forcing + 交叉熵),
# 但現在整條序列一個前向 —— GPU 平行度拉滿
(註:教學版簡化了部分細節;工業實作請用 nn.Transformer 或 HuggingFace。要點已全部呈現:多頭、掩碼、交叉注意力、殘差-LN、位置編碼。)
五、Transformer 為何統一了一切#
2017 年後的五年裡,Transformer 吞併了各模態,原因值得總結:
- 序列的通用性:圖像切成 patch(ViT 2020)、語音切成幀、蛋白質切成殘基——任何離散單元序列都能吃;模態之別被化約為「tokenizer 之別」。
- 規模的友好性: \(O(1)\) 串行深度 + 純矩陣乘法,完美匹配 GPU/TPU;訓練算力隨硬體線性擴展(第 8、9 章)。
- 殘差流的可加性:所有信息沿殘差通道累加,層與層之間近似「讀-改-寫」,使可解釋性(殘差流分解)與擴展規律(9.2)的研究有了穩定對象。
- 訓練範式的承載力:自監督(下一詞預測、掩碼重建)+ Transformer + 大算力,構成預訓練範式的三腳架(9.2)。
同時誠實記錄其代價: \(O(n^2)\) 上下文成本、自回歸推理的串行性(生成時無法並行)、以及 KV cache 的顯存壓力。這些「Transformer 稅」正是 Mamba、線性注意力等後繼者的出發點——架構演化永不停歇。
六、本節重點#
- Transformer(2017)以「捨棄循環與卷積、只留注意力」的減法革命,同時解決 RNN 的串行瓶頸(訓練並行化)與長程路徑問題( \(O(1)\) 路徑),BLEU 28.4 創 SOTA。
- 四大組件各司其職:多頭自注意力(跨位置信息混合 + 子空間關係並行偵測)、FFN(逐位置非線性加工與知識存儲)、位置編碼(補償置換等變性、以正弦結構表達相對位置)、殘差+LayerNorm(梯度高速公路與訓練穩定)。
- 複雜度三方對比:自注意力以 \(O(n^2 \cdot d)\) 計算換 \(O(1)\) 串行步數與路徑長度;架構優劣相對於硬體與規模, \(n^2\) 在長上下文時代反轉為新瓶頸。
- 掩碼自注意力(因果性)+ 交叉注意力(Q 來解碼器、KV 來編碼器)是 Bahdanau 思想的完整繼承與重構。
- Transformer 成為通用架構的四個原因:模態無關(一切皆序列)、硬體友好(純矩陣乘)、殘差流可加(利於研究與擴展)、承載預訓練範式——它是深度學習的馮紐曼架構。
七、思考問題#
- 自注意力是置換等變的。請證明:對任意置換矩陣 \(P\) , \(\text{Attn}(PX, PX, PX) = P\,\text{Attn}(X, X, X)\) 。這個性質為何使位置編碼成為必需?
- 多頭注意力的參數量與單頭相同(皆為 \(4 d^2\) ),為何多頭通常更好?若把 \(h\) 增大到 \(d_k=1\) 會發生什麼?
- 因果掩碼使解碼器第 \(t\) 步只能看到前 \(t\) 個詞。請分析這一訓練-推論一致性的設計,以及「訓練時並行看全部、推論時串行生成」的不對稱對推理成本的影響。
- 論文標題宣稱 Attention Is All You Need。請反駁:FFN、殘差、LayerNorm、位置編碼缺一不可。你認為哪個組件最「可有可無」?(提示:近年的簡化研究,如去掉 LayerNorm 位置、簡化注意力。)
- KV cache 使自回歸生成的顯存隨上下文線性增長。請從「注意力 = 鍵值資料庫」的視角,分析 Mamba 等狀態空間模型如何把 KV cache 壓縮為固定大小的狀態,以及這一壓縮的資訊論代價。