3. 注意力机制:Q、K、V 与缩放点积
这是什么:注意力(Attention) = 给句子里每个词装一个"聚光灯",用三个矩阵把词向量改造成 Q(查询)、K(键)、V(值) 三种角色,再互相打分、按分取值。
为什么用:第 2 章把话变成了向量,但这些向量各管各的,"它"不知道自己指的是"猫"还是"鱼"。注意力让每个词吸收整个句子的语境,这是 Transformer 的灵魂。
如何用:本章用 NumPy 手搓一遍"打分 → softmax → 加权求和"三步曲,亲眼看到每个词是如何分配关注度的。
三种角色:查询、键、值
一个记忆检索的类比最直观:
- Q(Query,查询):我这个词想找什么?——"它"想找"单数名词"
- K(Key,键):我这个词贴的什么标签?——"猫"贴着"单数名词"的标签
- V(Value,值):对上号之后,我实际能提供的内容
三者都由词向量乘各自权重矩阵变出来。真实模型里 W_Q / W_K / W_V 是训练出来的,我们这里拿随机的顶替,重点看数据怎么流:
第一步:打分——缩放点积
让每个词的 Q 和所有词的 K 各算一次点积,就得到一张"谁该看谁几分"的分数表。分数除以 √d(d 是向量维度)再进 softmax,这一步叫缩放点积注意力(Scaled Dot-Product Attention):
import numpy as np
rng = np.random.default_rng(42) # 固定随机种子:保证你跑出的数字和我一样
T, d = 3, 4 # 句子 3 个词,每个词的向量 4 维
X = rng.normal(size=(T, d)) # 3×4 词向量矩阵(随机模拟)
W_Q = rng.normal(size=(d, d)) # 示意权重:真实模型是训练出来的
W_K = rng.normal(size=(d, d))
W_V = rng.normal(size=(d, d))
Q, K, V = X @ W_Q, X @ W_K, X @ W_V
scores = Q @ K.T / np.sqrt(d) # Q 逐个与所有 K 点积打分,再除 √d 缩放
weights = np.exp(scores) / np.exp(scores).sum(axis=1, keepdims=True) # softmax 变百分比
print(np.round(weights, 3)) # 每行是词对句中每个词的关注度,长相加为 1
print(weights.sum(axis=1))
你会看到:
[[0.419 0.217 0.365]
[0.01 0.955 0.034]
[0.396 0.21 0.394]]
[1. 1. 1.]
| 代码 | 作用 |
|---|---|
rng = np.random.default_rng(42) |
固定种子:换一个数字,整张关注度表就换一种长相 |
X @ W_Q |
矩阵乘法:把"词向量"改造成"查询向量",@ 是 NumPy 的矩阵乘法 |
Q @ K.T / np.sqrt(d) |
3×3 分数表;除 √d 防止大维度时点积偏大、softmax "偏科"到个别词上 |
np.exp(scores) / ...sum(axis=1, keepdims=True) |
softmax:把一行分数变成加起来等于 1 的百分比 |
np.round(weights, 3) |
关注度表:第 2 行 0.955 表示第 2 个词几乎只盯着第 2 个词看 |
第二步:加权求和——按待遇领内容
拿到关注度表后,每个词的输出 = 关注度 × 各词的 V 做加权平均。对方关注度越高,它的 V 内容就吸进来越多:
out = weights @ V # 3×3 关注度表 × 3×4 值矩阵 = 每个词吸收语境后的新向量
print(out.shape)
你会看到:
(3, 4)
记忆口诀:Q 生疑问,K 贴标签;点积打分除根号,softmax 摊百分,按分领 V 领内容。
动手改
- 把
default_rng(42)换成其他种子,看关注度表怎么"换性格" - 把
/ np.sqrt(d)删掉再跑,观察 softmax 输出是不是变得极化(更接近非 0 即 1) - 构造一个
Q全等于K的极端例子,验证每个词最关注的正是它自己
下一章:多头注意力与残差,给一层装上完整配件 🐸