Appearance
注意力机制
这是整个 Transformer 的心脏。理解了这一页,80% 的困惑就解决了。我们会从「一个查询如何检索信息」讲起,一步步推导到 scaled dot-product attention,再落地成能跑的代码。
1. 从一个检索比喻开始
注意力机制本质上是一次可微分的软检索(soft lookup)。回想你怎么查字典:
你手里有一个"查询"(Query): 想找"猫"的意思
字典里有很多"键"(Key): 每个词条的索引
每个键对应一份"值"(Value): 词条的真正解释
结果 = 找到最匹配的键, 取回它对应的值注意力做的是同样的事,只是两点不同:
- 不是只取最匹配的一个,而是按匹配程度「加权平均」所有值(软的)。
- 「匹配程度」是算出来的,且能被训练。
于是有了三个主角:
| 角色 | 符号 | 含义 |
|---|---|---|
| 查询 | Query | 「我现在想了解什么」 |
| 键 | Key | 「我是什么,可供别人匹配」 |
| 值 | Value | 「真正被取走的内容」 |
2. Q、K、V 从哪来:三块线性变换
Q/K/V 不是额外数据,而是同一批输入向量分别乘上三个可学习矩阵得到的:
输入向量序列 X (形状 [n, d_model])
│
├── × W_Q ──► Q (查询)
├── × W_K ──► K (键)
└── × W_V ──► V (值)python
Q = X @ W_Q # W_Q: [d_model, d_k]
K = X @ W_K
V = X @ W_V为什么要拆成三份?因为「一个词作为查询时想找什么」和「它作为被查对象时是什么」是两回事。三个矩阵让模型学会:用什么特征去提问、用什么特征去应答、应答时递交什么内容。
3. 计算注意力:四步走
对每个位置的 Query,去和所有位置的 Key 比一比,再据此加权汇总 Value:
第 1 步 打分: score = Q · Kᵀ (每个 Q 和每个 K 的点积 = 相关性)
第 2 步 缩放: score = score / √d_k (防止点积过大)
第 3 步 归一化: weights = softmax(score) (变成和为 1 的权重)
第 4 步 加权求和: output = weights · V (按权重混合所有 V)合成一个公式,就是大名鼎鼎的:
[ \text{Attention}(Q,K,V) = \text{softmax}!\left(\frac{QK^{T}}{\sqrt{d_k}}\right)V ]
图示:单个 Query 关注全场
K₁ K₂ K₃ K₄
│ │ │ │
Q₂ ·───────────┼──●──┼─────┼──●──┼── 点积打分
(第2个词) 0.1 0.9 0.2 0.7 ← 未归一化
│ │ │ │
softmax ↓ 归一化成权重
0.05 0.45 0.15 0.35 (和=1)
│ │ │ │
输出₂ = 0.05·V₁ + 0.45·V₂ + 0.15·V₃ + 0.35·V₄整句话所有位置同时这样算,就得到注意力输出矩阵。
4. 为什么要除以 √d_k(缩放)
点积是逐维相乘再相加。维度 d_k 越大,点积的方差越大,数值容易跑到很大区间:
维度大 → Q·K 值大 → softmax 进入"饱和区"
→ 某个位置权重≈1,其余≈0(梯度几乎为0)→ 学不动
除以 √d_k 把数值拉回合理范围,softmax 分布更"柔和",梯度健康一句话:缩放是为了让 softmax 待在梯度友好的区域,这也是名字里 "Scaled" 的由来。
5. 自注意力 vs 普通注意力
「自」注意力(Self-Attention)指 Q、K、V 全部来自同一个序列——句子自己关注自己:
Self-Attention: Q,K,V 都来自输入 X → 建模句内词与词的关系
Cross-Attention: Q 来自解码器, K,V 来自编码器 → 翻译时"目标句"看"源句"Cross-Attention 会在 Encoder与Decoder 里详细出现。
6. 完整可运行实现(纯 PyTorch)
python
import torch
import torch.nn.functional as F
import math
def scaled_dot_product_attention(Q, K, V, mask=None):
d_k = Q.size(-1)
# 1) 打分 2) 缩放
scores = Q @ K.transpose(-2, -1) / math.sqrt(d_k) # [..., n, n]
# 3) 掩码(可选): 把不允许关注的位置设为 -inf, softmax 后权重≈0
if mask is not None:
scores = scores.masked_fill(mask == 0, float("-inf"))
# 4) 归一化 + 加权求和
weights = F.softmax(scores, dim=-1)
return weights @ V, weights
# 小例子: 3 个词, 每个词 4 维
torch.manual_seed(0)
X = torch.randn(3, 4)
W_Q, W_K, W_V = (torch.randn(4, 4) for _ in range(3))
Q, K, V = X @ W_Q, X @ W_K, X @ W_V
out, attn = scaled_dot_product_attention(Q, K, V)
print("注意力权重(每行和为1):\n", attn)
print("输出形状:", out.shape) # [3, 4]attn[i][j] 就表示「第 i 个词对第 j 个词的关注度」,它是一个 n×n 的矩阵——这正是 O(n²) 复杂度的来源。
7. 因果掩码:让模型学会「只看过去」
语言模型要预测下一个词,就必须保证算某个位置时不能偷看它之后的词(否则是「作弊」,训练/推理不一致)。办法是在注意力矩阵上加一张下三角掩码:
允许关注=1, 禁止=0 (causal mask)
位置1 位置2 位置3 位置4
位置1 [ 1 0 0 0 ] ← 第1个词只能看自己
位置2 [ 1 1 0 0 ]
位置3 [ 1 1 1 0 ]
位置4 [ 1 1 1 1 ] ← 第4个词能看1~4python
n = 4
mask = torch.tril(torch.ones(n, n)) # 下三角为 1
out, attn = scaled_dot_product_attention(Q, K, V, mask=mask)
# 效果: attn 的上三角全为 0,每个位置只从过去+自己聚合信息- Encoder 的自注意力:通常不加因果掩码(BERT 甚至双向可见)。
- Decoder / GPT 的自注意力:必须加因果掩码。
这条区别正是 BERT 与 GPT 分道扬镳的地方,详见 GPT架构。
8. 一图总结全流程
输入 X (含词义+位置)
│
┌────┼────┐
×W_Q ×W_K ×W_V ← 生成 Q K V
Q K V
\ | /
\ | /
Q·Kᵀ /√d_k ← 打分 + 缩放
│
(+ 因果掩码) ← 只看过去(decoder)
│
softmax ← 变权重(每行和为1)
│
· V ← 加权聚合
│
输出 Z ← 每个位置都融合了全句(或过去)的信息9. 常见疑问快答
- Q/K/V 维度要和 d_model 一样吗? 单头时输出是 d_model;多头时会拆成 h 份,见下一页。
- 注意力能替代 FFN 吗? 不能。注意力负责「混合位置信息」,FFN 负责「在每个位置做非线性加工」,二者互补。见 前馈网络与残差连接。
- 为什么要 softmax 不直接用点积? softmax 把分数变成归一化权重,既可解释(比例),又保证加权求和稳定、梯度友好。
小结
- 注意力 = 可微分的软检索:用 Q 去问,用 K 匹配,从 V 取信息。
- 核心公式:
softmax(QKᵀ/√d_k)·V;÷√d_k 为防 softmax 饱和。 - Self:Q/K/V 同源;Cross:Q 与 K/V 来自不同序列。
- 因果掩码让 decoder 只能看过去,是 GPT 能自回归生成的关键。
下一步
单头注意力只能从一个角度看关系。下一页让模型「同时戴多副透镜」 → 多头注意力。