Skip to content

注意力机制

这是整个 Transformer 的心脏。理解了这一页,80% 的困惑就解决了。我们会从「一个查询如何检索信息」讲起,一步步推导到 scaled dot-product attention,再落地成能跑的代码。

1. 从一个检索比喻开始

注意力机制本质上是一次可微分的软检索(soft lookup)。回想你怎么查字典:

你手里有一个"查询"(Query): 想找"猫"的意思
字典里有很多"键"(Key):    每个词条的索引
每个键对应一份"值"(Value): 词条的真正解释

结果 = 找到最匹配的键, 取回它对应的值

注意力做的是同样的事,只是两点不同:

  1. 不是只取最匹配的一个,而是按匹配程度「加权平均」所有值(软的)。
  2. 「匹配程度」是算出来的,且能被训练

于是有了三个主角:

角色符号含义
查询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~4
python
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 能自回归生成的关键。

下一步

单头注意力只能从一个角度看关系。下一页让模型「同时戴多副透镜」 → 多头注意力