从零讲透Transformer和注意力机制
Transformer 从零讲透:自注意力到底在算什么
副标题:不背公式、不绕弯子,用「舞台聚光灯」和「工位同事」两个比喻,把自注意力、多头、位置编码、残差、LayerNorm、FFN 一次看明白——附调库版 & 面试手搓版代码
你写模型、调 GPT、刷 BERT 时,一定听过一句话:「Transformer 是大模型的基石」。但真要你讲清楚「自注意力(Self-Attention)到底在算什么」,是不是卡在了那行公式上:
A t t e n t i o n ( Q , K , V ) = s o f t m a x ( Q K T d k ) V Attention(Q, K, V) = softmax\left(\frac{QK^T}{\sqrt{d_k}}\right)V Attention(Q,K,V)=softmax(dkQKT)V
别慌,这几乎是所有人的必经之痛。前面我们讲过 Tensor(第 43 篇)、nn.Module(第 50 篇)、优化器与正则化(第 52~57 篇)——那些是「零件」。而 Transformer,是把这些零件组装成一种全新信息流动方式的范式。
这一篇,我们就把这套机制彻底拆开。我会用两个贯穿全程的比喻——舞台聚光灯和工位同事——串起所有概念。
总览:先记住两句话,全文就通了
你大概听说过:Transformer 是 BERT、GPT、ChatGPT 的共同骨架。它凭什么这么强?一句话——
👉 它让一句话里「任意两个词」都能直接对话,而且所有词是同时算、可以并行的。
为了让你后面不迷路,先把整篇的「贯穿比喻」立起来:
💡 比喻 A · 舞台聚光灯:一句话是一支队伍上台。每个词头顶有一束聚光灯,灯光照向谁,谁就被「重点关注」。自注意力,就是大家一起决定:我的灯该打在谁身上。
💡 比喻 B · 工位同事:每个词是办公室里一个同事。开会时,每个人同时向全场发问、也听全场回答。谁的问题和你对口,你就多听谁的——这就是「加权融合」。
文章里每个图解,都是把上面这两个比喻「画」出来给你看。下面正式开始。
一、为什么是 Transformer:RNN 的两个老毛病
在 Transformer 之前,处理「一句话 / 一段序列」的主力是 RNN(循环神经网络)。它像一条流水线:看完第 1 个词,才看第 2 个,再看第 3 个……时间步必须 串行。
🕳️ 毛病① 算得慢(不能并行):第 2 个词必须等第 1 个词算完。句子越长,排队越久,GPU 的并行算力被白白浪费。
🕳️ 毛病② 记性差(长程依赖丢失):信息要从第 1 个词一路「传递」到第 100 个词,每传一步都打点折扣,传到远处就梯度消失、记不住开头了。所以 RNN 很难理解「一句话开头和结尾的呼应」。
Transformer 的解法是「换思路」:不让词排队,而是让所有词同时坐在一屋子里,两两直接对话。于是:
| 维度 | RNN(旧) | Transformer(新) |
|---|---|---|
| 计算方式 | 串行,必须等上一时间步 | 并行,所有位置同时算 |
| 长程依赖 | 远处易梯度消失,记不住 | 任意两位置直连,一步可达 |
| 复杂度 | O(n),随长度线性 | O(n²),随长度平方(吃显存) |
| 位置信息 | 天然按顺序输入 | 无序,需额外加位置编码 |
| 现状 | 渐被取代 | GPT / BERT / 大模型基石 |

💡 一句话记忆:Transformer 用「平方的算力」换「并行 + 长程直连」,在长序列上完胜 RNN。序列特别长时 O(n²) 成为瓶颈 → 催生 FlashAttention、稀疏注意力等优化。
二、自注意力:一句话的「聚光灯」
核心公式就一行,但每个符号都能翻译成大白话:
A t t e n t i o n ( Q , K , V ) = s o f t m a x ( Q K T d k ) ⏟ 该关注谁 ⋅ V Attention(Q, K, V) = \underbrace{softmax\left(\frac{QK^T}{\sqrt{d_k}}\right)}_{\text{该关注谁}} \cdot V Attention(Q,K,V)=该关注谁 softmax(dkQKT)⋅V
拆成两步就懂:
- 第一步 · 打分:每个词拿自己的「提问」去和别人的「标签」比对,算出该关注谁多少(就是中间那个 Q K T QK^T QKT)。
- 第二步 · 融合:把分数变成权重(每行加起来 = 1),再按权重把大家的「内容」搅和在一起(最后那个 ⋅ V \cdot V ⋅V)。

💡 聚光灯版解释:每个词头顶一束灯(Query),灯指向谁,谁就被照亮、被「吸收」进自己的理解里。灯越亮 = 关注越多。上面这张图里,气泡越大 = 被关注越多。

⚠️ 此刻只是「算出该关注谁」,还没真正改变任何词。 改变发生在第二步融合。
三、Q / K / V:每个词的「三张角色卡」
Q、K、V 都来自同一个输入,只是各乘了一个不同的线性矩阵:
KaTeX parse error: Expected 'EOF', got '#' at position 40: …_Q \quad \text{#̲ Query:我想找什么样的上…

💡 工位同事版:开会时,每个人都同时拿着三张卡——
- Q(提问卡):我现在想搞懂什么?
- K(名牌卡):我擅长回答哪类问题?
- V(资料卡):我被问到时,掏出什么真东西给你?
💡 为什么要拆三份:「用来匹配的特征」和「用来传递的内容」往往不同。比如代词「它」的 Q 想找一个「动物名词」,而它真正要搬运的 V 是那个名词的具体含义——拆开 K 和 V,让「找人」和「给料」两件事解耦,表达力更强。
四、缩放 √d_k:给「吵闹的打分」降噪
为什么点积要除以 d k \sqrt{d_k} dk?一句话:防止分数方差爆炸,Softmax 变「独热」导致梯度消失。
Q 和 K 每个维度里的值,都是随机初始化、近似独立的小数字。两个 d k d_k dk 维向量做点积,相当于把 d k d_k dk 个这样的小乘积加起来:
s c o r e = q ⋅ k = ∣ q ∣ ⋅ ∣ k ∣ ⋅ cos θ score = q \cdot k = |q| \cdot |k| \cdot \cos\theta score=q⋅k=∣q∣⋅∣k∣⋅cosθ
维度 d k d_k dk 越高,点积的方差越大(≈ d k d_k dk 倍)。不处理的话,分数会极端到 ±很大,送进 Softmax 后几乎变成「只选一个、其余全 0」的 one-hot,梯度直接消失,模型学不动。
💡 除以 √d_k 做了什么:把方差从 ≈ d k d_k dk 拉回到 ≈1,分数分布变得「温和」,Softmax 才会给多个词都分一点注意力,梯度也健康。这也就是名字 Scaled Dot-Product Attention(缩放点积注意力) 的由来。
💡 面试高频追问:「为什么除 √d_k 而不是别的?」→ 因为要让缩放后点积的方差稳定在 1,√d_k 是方差开根号恰好抵消的那一档。答到「方差稳定」四个字就对了。
五、注意力矩阵:整句的「关注全景」
把每个词当 Query 打一遍分,排成矩阵,再按行做 Softmax:
A = s o f t m a x ( Q K T d k ) , s o f t m a x ( z ) j = e z j / τ ∑ k e z k / τ A = softmax\left(\frac{QK^T}{\sqrt{d_k}}\right), \quad softmax(z)_j = \frac{e^{z_j/\tau}}{\sum_k e^{z_k/\tau}} A=softmax(dkQKT),softmax(z)j=∑kezk/τezj/τ
以「猫 坐 在 垫子 上」为例,注意力矩阵长这样(每行和为 1,数值越大 = 越关注):
猫 坐 在 垫子 上 ← Key(被看)
猫 [ 0.50][0.10][0.10][0.20][0.10]
坐 [ 0.30][0.40][0.10][0.10][0.10]
在 [ 0.10][0.10][0.30][0.30][0.20]
垫子 [ 0.40][0.10][0.10][0.30][0.10]
上 [ 0.20][0.10][0.10][0.40][0.20]
↑ 每个词当一次 Query(在看)

💡 聚光灯全景版:这就是「谁在看谁」的全局地图。亮带通常对应语法/指代关系,比如「它→猫」「动词→主语」。
💡 温度 τ 的作用:τ→0,权重集中到最相关的那个词(硬选择);τ→∞,权重趋于均匀(平均一切)。训练时 τ 固定为 1(已含在 √d_k 里);推理采样时调它能控制「专注 vs 发散」。
六、多头注意力:多组聚光灯,各看一面
把 d d d 维切成 h h h 个低维子空间,每个头独立做一遍注意力,结果拼接后用 W O W_O WO 融合:
h e a d t = A t t e n t i o n ( X W Q t , X W K t , X W V t ) M u l t i H e a d = C o n c a t ( h e a d 1 , … , h e a d h ) ⋅ W O \begin{aligned} head_t &= Attention(XW_Q^t, XW_K^t, XW_V^t)\\ MultiHead &= Concat(head_1, \dots, head_h) \cdot W_O \end{aligned} headtMultiHead=Attention(XWQt,XWKt,XWVt)=Concat(head1,…,headh)⋅WO

💡 多组聚光灯版:一个头像一盏聚光灯,只能照一个角度。多头 = 同时打 h 盏不同颜色的灯:有的专照「主谓一致」,有的专照「代词指代」,有的专照「相邻距离」。最后 W O W_O WO 把各盏灯的发现重新混回完整表示。
💡 为什么不是一个大头:单一空间里,「语法对齐」和「语义相关」会互相干扰、挤在同一组夹角里。拆成多头,让每种关系拥有独立子空间、互不打架——实践里这显著提升表达力。
七、位置编码:给每个词贴「座位号」
正弦编码给每个维度安排一个不同频率:低维转得慢(管粗略位置),高维转得快(管精细位置)——像钟表的时针/分针/粗细分针叠加,组合出独一无二的位置指纹。
KaTeX parse error: Expected 'EOF', got '#' at position 180: …i) \quad \text{#̲ 把座位号加到词向量上} \e…
💡 座位号版:注意力是「对称」的——它只看词与词的内容关系,天然分不清先后。位置编码就是给每个人发一张座位牌,贴进向量里,模型这才知道「谁坐在第几个」。
💡 为什么是 sin/cos:三角函数有界(不会随句子变长而爆炸)、且 P E ( p o s + k ) PE(pos+k) PE(pos+k) 可由 P E ( p o s ) PE(pos) PE(pos) 线性表示,模型容易学会「相对距离」。现代大模型常用 RoPE:把向量旋转一个正比于位置的角度,于是「相对位置」变成「夹角差」——恰好能被下一步的点积感知。
八、Encoder Block 四件套(堆叠 N 层)
一个 Block 由四部分组成。理解这四件套,就理解了 90% 的 Transformer:
- 多头自注意力:让词与词充分对话(第 2~6 节)。
- 残差连接 + LayerNorm:把注意力的结果「小步」加回原值,并归一化(下一节)。
- 前馈网络 FFN:每个词独立过一个非线性小网络(第 10 节)。
- 残差连接 + LayerNorm:再把 FFN 的结果小步加回。
一个 Block 的数据流(Post-LN 写法):
x = x + Attention(LayerNorm(x)) # 子层1:注意力 + 残差
x = x + FFN(LayerNorm(x)) # 子层2:前馈 + 残差

💡 乐高版:把「注意力」和「FFN」想成两块功能不同的乐高,每块外面都裹一层「残差+归一化」的保护壳。把这块乐高重复堆 N 次,就是完整的 Transformer 编码器。
💡 Pre-LN vs Post-LN:上面是原论文的 Post-LN(先注意力后 Norm)。现代大模型多用 Pre-LN(
x + Attn(LayerNorm(x))顺序不变,但 Norm 在子层里面),训练更稳、更深也不易崩。
九、残差 & LayerNorm:让深层网络稳如老狗
残差连接:注意力和 FFN 都不替换输入,而是作为「增量 Δ」加回去:
x ← x + Δ x \leftarrow x + \Delta x←x+Δ
💡 便利贴版:残差就像在原件上贴便利贴补充,而不是把整页重写。信息沿一条「残差流」主干一路累加,每层的修改都很小。
💡 为什么不可或缺:没有残差,每层要从零重建表示,几十层叠加会让梯度在反向传播中指数衰减(梯度消失),根本训不动。有了 x + Δ x+\Delta x+Δ,梯度有一条「高速公路」直达浅层——这是深层网络可行的关键。

LayerNorm:对每个向量减均值、除标准差,再用可学习的 γ、β 微调:
L N ( x ) = γ ⋅ x − μ σ + β , μ , σ 为该向量自身的均值/标准差 LN(x) = \gamma \cdot \frac{x - \mu}{\sigma} + \beta, \quad \mu,\sigma \text{ 为该向量自身的均值/标准差} LN(x)=γ⋅σx−μ+β,μ,σ 为该向量自身的均值/标准差
💡 统一格式版:LayerNorm 抹掉「长度/尺度」,只保留「方向」。把所有点推到半径固定的球面上,后续点积比较只看角度,不会被某个模长过大的点带偏。
💡 深层含义:正因为 LayerNorm 不断归一化长度,Transformer 里真正承载语义的是向量的方向而非长度——这也解释了为什么分析模型常用余弦相似度。
十、前馈网络 FFN:把空间「折弯」出非线性
结构是「升维 → 激活 → 降维」,中间层通常是 4 倍宽:
F F N ( x ) = W 2 ⋅ G E L U ( W 1 x + b 1 ) + b 2 , W 1 : d → 4 d , W 2 : 4 d → d FFN(x) = W_2 \cdot GELU(W_1 x + b_1) + b_2, \quad W_1: d\rightarrow 4d,\; W_2: 4d\rightarrow d FFN(x)=W2⋅GELU(W1x+b1)+b2,W1:d→4d,W2:4d→d
💡 折纸版:线性层只能旋转拉伸(保持「直」),唯有激活函数能把空间折弯。每个 ReLU/GELU 神经元像在空间压一道折痕,许多折痕叠加,就能把原本线性不可分的点云揉成可分的形状。
💡 另一种读法(键值记忆):升维后的每个神经元像一个「探测器」,命中就往输出写入一段对应的「知识向量」。研究表明 FFN 正是模型存储事实的地方(「巴黎—法国首都」这类知识就编码在这里),它占了模型约 2/3 的参数量。
十一、堆叠 N 层 → 从混沌到语义
浅层多处理表层/语法关系,深层逐步形成抽象、任务相关的语义。最后取末位 token 的向量,和词表每个词向量做点积,选出最对齐的:
KaTeX parse error: Expected 'EOF', got '#' at position 59: …^T \quad \text{#̲ 又是一次夹角比较!}\\ P…
💡 首尾呼应:输出(Unembedding)又回到了点积——把末位点的方向和词表里每个词向量比夹角,最对齐的方向就是预测的下一个词。从输入到输出,模型自始至终只在反复比较和调整「方向」。
💡 这就是生成:选出一个词后,接到句尾,整流程再跑一遍预测下下个词——如此自回归,逐字写出整段回答。
十二、代码实战:调库版 + 面试手搓版
① 调库版:把 MNIST 图片当序列做分类
我们把 28×28 的图看作「28 个时间步、每步 28 维特征」的序列,喂给 Transformer 编码器。真正的重点不是这堆代码,而是后面那张「张量尺寸变化表」——它告诉你数据每一步变成什么样。
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
# ===== 一、构建模型:继承 nn.Module,实现两个方法 =====
class TransformerModel(nn.Module):
def __init__(self):
super().__init__()
self.embed = nn.Linear(28, 64) # 每步28维 → 64维(d_model)
self.pos = nn.Parameter(torch.randn(1, 28, 64)) # 可学习的位置编码
layer = nn.TransformerEncoderLayer(
d_model=64, nhead=4, # 4个注意力头
dim_feedforward=128, # FFN 隐藏维度
batch_first=True) # 形状 [批, 序列, 特征]
self.encoder = nn.TransformerEncoder(layer, num_layers=2) # 叠2层
self.fc = nn.Linear(64, 10) # 全连接: 64 → 10类
def forward(self, x):
x = self.embed(x) + self.pos # 投影 + 加位置编码 [批,28,64]
x = self.encoder(x) # 过编码器
x = x.mean(dim=1) # 序列维平均(池化) [批,64]
return self.fc(x) # 得到 10 类得分
# ===== 二、准备数据 / 设备 / 训练循环(套路固定)=====
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = TransformerModel().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)
transform = transforms.ToTensor()
train_set = datasets.MNIST('./data', train=True, download=True, transform=transform)
train_loader = DataLoader(train_set, batch_size=64, shuffle=True)
for epoch in range(5):
for images, labels in train_loader:
images = images.squeeze(1).to(device) # [批,1,28,28]→[批,28,28]
labels = labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
optimizer.zero_grad(); loss.backward(); optimizer.step()
print(f'Epoch {epoch+1}, Loss: {loss.item():.4f}')
张量尺寸变化追踪(务必看懂):
| 阶段 | 形状 |
|---|---|
| 输入序列 | [批, 28, 28] |
| 投影 + 位置编码 | [批, 28, 64] |
| Encoder 输出 | [批, 28, 64] |
| 序列平均池化 | [批, 64] |
| 全连接 | [批, 10] |
② 面试手搓版:缩放点积注意力(必背核心 5 行)
面试说「手搓 Transformer」= 不准用 nn.TransformerEncoder,要自己写出注意力的矩阵运算。重点是默写下面这几行。
import torch.nn.functional as F
def attention(Q, K, V, mask=None):
# Q,K,V: [批, 头, 序列, d_k]
d_k = Q.size(-1)
scores = Q @ K.transpose(-2, -1) / d_k ** 0.5 # ← 缩放点积
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9) # 挡住的位置填 -∞
attn = F.softmax(scores, dim=-1) # 每行归一化
return attn @ V # 加权求和
🕳️ 面试高频追问预案:
- 为什么除 √d_k → 防点积方差随维度变大、Softmax 梯度消失。
- 多头的意义 → 不同头关注不同子空间(语法/语义/位置),信息更丰富。
- Encoder vs Decoder → Decoder 多一层带 causal mask 的自注意力 + 交叉注意力。
- 手搓 vs 调库 → 手搓 = 自己写 QKᵀ/√d_k 那 5 行;调库 = 写应用。
训练四连(固定套路,所有模型通用)
optimizer.zero_grad() # 1. 清零
loss.backward() # 2. 反传
optimizer.step() # 3. 更新
💡 口诀:清零 → 反传 → 更新。无论 CNN / RNN / LSTM / Transformer,这套训练循环完全一样。
核心要点小结
- 自注意力 = 聚光灯:每个词同时向全场发问、按相关度融合别人的内容( Q K T / d k → s o f t m a x → ⋅ V QK^T/\sqrt{d_k} \rightarrow softmax \rightarrow \cdot V QKT/dk→softmax→⋅V)。
- Q/K/V 三张卡:提问卡、名牌卡、资料卡,由同一输入乘三个矩阵得来,「找人」与「给料」解耦。
- ÷√d_k 是降噪:把点积方差拉回 1,防止 Softmax 变独热、梯度消失。
- 多头 = 多组聚光灯:切分子空间,各看一种关系(语法/指代/距离),再融合。
- 位置编码 = 座位号:注意力本身不分先后,必须额外注入顺序信息。
- Encoder 四件套:多头注意力 → 残差+LN → FFN → 残差+LN,叠 N 层。
- 残差 = 贴便利贴: x ← x + Δ x \leftarrow x + \Delta x←x+Δ,只走小步、不推倒重来,深层才训得动。
- LayerNorm = 统一格式:抹掉长度只留方向,把点拉回球面,稳定尺度。
- FFN = 折纸:激活函数把空间折弯,制造非线性,约占 2/3 参数、存事实。
- 它取代了 RNN:用 O(n²) 算力换「并行 + 长程直连」,是 BERT/GPT 的基石。
动手思考题
- 把「猫坐在垫子上」改成「垫子在猫坐上」,注意力矩阵会怎么变?位置编码在这里起了什么作用?
- 如果去掉 √d_k 缩放,d_k=512 时点积分数可能多大?Softmax 后权重会变成什么样?模型为什么学不动?
- 为什么多头用「切分子空间」而不是「用一个更大的头」?结合「语法对齐 vs 语义相关互相干扰」想一想。
- 假设只有 1 层、没有残差,深层网络会遇到什么具体问题?用「梯度高速公路」的比喻解释。
- 在评论区贴出你第一次看注意力公式时的困惑,我们一起排雷 💬
📚 关于本系列
本文是「AI 学习路线 · 阶段四:PyTorch 深度学习基础」系列中的一篇。完整系列覆盖从 Tensor 基础到模型诊断的全流程,每篇都配有可交互演示与高清配图。
🗺️ 查看完整 AI 学习路线(从 0 到进阶, 持续更新)
👉 bestsdz.xyz觉得有帮助的话,欢迎去博客点个收藏 ⭐,你的支持是我持续输出的最大动力!
更多推荐







所有评论(0)