10.1 先建立直觉:注意力 = 自动化的"查资料"

上一课说了 Transformer 的核心是注意力。这一课把它彻底讲透。

先做个类比:注意力就像图书馆查资料。

你要写一篇关于"北京"的论文(这是你的"查询")。
你跑到图书馆,看每本书的标题(这是"键"),
觉得哪本相关,就翻开内容记笔记(这是"值")。

最终你写出的论文,其实是:
   每本书的相关度 × 每本书内容 的加权融合

对应到模型里:

图书馆类比模型里的名字含义
你要查的问题Q(Query 查询)当前词"想找什么"
每本书的标题K(Key 键)其他词"能提供什么"
书里的内容V(Value 值)其他词"实际的信息"

一句话总结注意力:拿我的 Q,去和所有词的 K 做匹配,算出每个词的相关度,再按相关度加权取 V。


10.2 Q/K/V 是从哪来的?

不是模型里"天然就有"三个向量。它们是由同一个输入向量,乘以三个不同的权重矩阵得到的:

输入向量 x(第 3 个词 "首都")

x · W_Q = Q(这个"首都"想找什么信息)
x · W_K = K(这个"首都"能提供什么信息)
x · W_V = V(这个"首都"的实际内容)
import numpy as np

np.random.seed(42)

d_model = 64          # 向量维度
x = np.random.randn(d_model)  # 某个词的输入向量

W_Q = np.random.randn(d_model, d_model) * 0.1
W_K = np.random.randn(d_model, d_model) * 0.1
W_V = np.random.randn(d_model, d_model) * 0.1

Q = x @ W_Q
K = x @ W_K
V = x @ W_V

print(f"输入 x: {x.shape}")    # (64,)
print(f"Q: {Q.shape}")         # (64,) —— 同一个词,三种身份
print(f"K: {K.shape}")         # (64,)
print(f"V: {V.shape}")         # (64,)

这三个矩阵 W_Q、W_K、W_V 就是模型的参数,是在训练中"学出来"的。 训练得越好,模型越知道"什么词该关注什么词"。


10.3 注意力分数的计算:一步步来

设一句话有 4 个词:["我", "爱", "北京", "天安门"],每个词向量 4 维(简化):

第 1 步:算 Q 和所有 K 的点积(相关性打分)

以"北京"的 Q 为例:
  得分(北京, 我)     = Q北京 · K我     = 0.6
  得分(北京, 爱)     = Q北京 · K爱     = 0.4
  得分(北京, 北京)   = Q北京 · K北京   = 0.9  (和自己最相关)
  得分(北京, 天安门) = Q北京 · K天安门 = 0.8  (地理位置相关)

第 2 步:除以缩放因子 √d

d = 向量维度(这里 4,真实模型 64~128 不等)
每个分数 ÷ √d:

  0.6/2 = 0.30
  0.4/2 = 0.20
  0.9/2 = 0.45
  0.8/2 = 0.40

为什么除以 √d?如果向量维度很大,点积结果会很大,导致 softmax 之后"最相关的词拿走几乎全部权重",其他词啥也分不到。除一下,让分数分布更温和。(11.4 细讲)

第 3 步:softmax 归一化成权重

softmax([0.30, 0.20, 0.45, 0.40]) ≈ [0.25, 0.20, 0.30, 0.25]

现在这些数加起来 = 1,可以当"权重"用了

第 4 步:按权重加权求和 V

输出(北京) = 0.25×V我 + 0.20×V爱 + 0.30×V北京 + 0.25×V天安门

这个加权后的向量,就是"北京"在看过全句之后的新表示。 它既保留了"北京"自己的信息,又融入了"我爱北京天安门"这个上下文。

完整公式(给想记的读者)

Attention(Q, K, V) = softmax( QKᵀ / √d ) V
import numpy as np

def softmax(x):
    e = np.exp(x - np.max(x))
    return e / e.sum()

def attention(Q, K, V):
    d = K.shape[-1]
    scores = Q @ K.T / np.sqrt(d)   # 1. 点积打分  2. 缩放
    weights = softmax(scores)       # 3. 归一化
    return weights @ V              # 4. 加权求和

# 4 个词,每个 4 维
X = np.random.randn(4, 4)
W_Q = np.random.randn(4, 4) * 0.1
W_K = np.random.randn(4, 4) * 0.1
W_V = np.random.randn(4, 4) * 0.1

Q, K, V = X @ W_Q, X @ W_K, X @ W_V
output = attention(Q, K, V)

print(f"输出形状: {output.shape}")  # (4, 4) 每个词都看过全句了

10.4 为什么要除以 √d?

这是大家最容易忽略的细节。原因:

  1. 点积的规模随维度增长:两个 d 维向量点积,期望值大约 √d
  2. 如果不缩放,softmax 输入数值会很大
  3. softmax 对很大数值敏感:会把最大的分数"放大到接近 1",其他归 0
  4. 结果:每个词只盯着最相关的 1 个词,梯度也容易消失,学不动

类比:两个队比赛,如果得分普遍是 5 分制,你除以 √d 就是"统一成 1 分制",让差距不会一下子把弱者打死。


10.5 Multi-Head:为什么要有"多头"?

单头的局限

一组 Q/K/V 只能捕捉一种关注模式。比如它学会了"关注地理位置相关的词",就没精力学"关注句法关系"。

多头 = 多组"眼镜"

把 64 维分成 8 个头,每个头管 8 维:

头 1:关注"代词指代谁"(它 → 小明)
头 2:关注"动词的宾语"(吃 → 苹果)
头 3:关注"数字与单位"(3 → 个)
头 4:关注"否定词的作用"(不 → 后面整句反转)
...

8 个头的结果拼接起来 → 一个"看得更全"的表示
# 多头注意力(简化示意)
def multi_head_attention(X, num_heads=8, d_model=64):
    head_dim = d_model // num_heads
    heads = []
    for h in range(num_heads):
        # 每个头有自己的 Q/K/V 权重(这里用同一个权重示意)
        W_Qh = np.random.randn(d_model, head_dim) * 0.1
        W_Kh = np.random.randn(d_model, head_dim) * 0.1
        W_Vh = np.random.randn(d_model, head_dim) * 0.1

        Q = X @ W_Qh
        K = X @ W_Kh
        V = X @ W_Vh
        heads.append(attention(Q, K, V))

    return np.concatenate(heads, axis=-1)  # 拼回完整维度

类比:一个人看问题只有一个视角;8 个专家从 8 个角度(句法、指代、语义、数量……)分别看,最后汇总讨论,结论自然更全面。


10.6 注意力的代价:O(n²)

注意力很强大,但也有代价:计算量随句子长度平方增长

句子长度 n = 100   → 计算量 ∝ 100² = 1 万
句子长度 n = 10000 → 计算量 ∝ 10000² = 1 亿

长度增加 100 倍,计算量增加 1 万倍!

这就是为什么"长上下文"很贵、很慢。围绕它有一堆优化技术:

  • Flash Attention:优化显存访问,不改变算法但快得多
  • 稀疏注意力:每个词只看附近词,不看全句
  • GQA / MLA:减少 KV 缓存(第 14 课会讲)

记住:只要提到"模型上下文变长为什么难、为什么贵",答案里一定有 O(n²)。


10.7 注意力可视化:模型在"看"什么?

研究者经常把注意力权重画成热力图,非常直观:

             我    爱    北京   天安门
  我       [0.15, 0.45, 0.20, 0.20]
  爱       [0.30, 0.15, 0.35, 0.20]
  北京     [0.10, 0.20, 0.45, 0.25]   ← "北京"最关注"北京"(自指)
  天安门   [0.10, 0.15, 0.30, 0.45]

行 = 当前词,列 = 它关注的词。深色 = 权重高 = 关系强。 你会看到模型确实学出了合理的语法/语义关系。

更多推荐