一句话总结:Q(Query)是“我在找什么”,K(Key)是“我挂着什么标签”,V(Value)是“我能贡献什么内容”。Q 和 K 的点积给出学出来的匹配分,再用归一化权重混合允许读取的 V。


10.1 本章目标

上一章我们建立了 Attention 的几何直觉:点积同时看方向和长度,可以拿来做学出来的 Query-Key 匹配分。

但还有几个关键问题没回答:

  • Q、K、V 是什么意思?
  • 它们是怎么从输入生成的?
  • 整个 Attention 的维度变化是怎样的?

这一章,我们来完整追踪 Attention 的计算过程,把每一步的维度变化都搞清楚。


10.2 输入数据的形状

10.2.1 理解输入维度

输入张量 X 的 batch、序列长度和模型维度

在实际训练中,我们会批量处理多个句子。输入数据的形状是:

X: [batch_size, seq_length, d_model]

以图中为例:

  • batch_size = 4:同时处理 4 个句子(批次大小)
  • seq_length = 16:这个批次把每个句子整理成 16 个 token 位置(序列长度)
  • d_model = 512:每个 token 用 512 维向量表示(模型维度)

10.2.2 具体例子

图中展示了 4 个句子:

1. 小沈阳江西演唱会邀请了,
2. 明星刀郎的歌火遍大江南北
3. 2002年的第一场雪比2001年来得更
4. LLM张老师的粉丝全是正能量

在这个教学例子里,每个句子会通过截断或 padding 整理成 16 个 token 位置,每个位置用 512 维向量表示。

所以总的输入形状是 [4, 16, 512]:

  • 4 个句子
  • 16 个位置
  • 512 维向量

10.2.3 三个维度的含义

维度英文中文含义
batch_size批次批次大小同时处理多少个句子
seq_length序列长度序列长度这个批次里每个句子有多少个 token 位置
d_model模型维度学习维度每个 token 的向量维度

10.3 生成 Q、K、V

10.3.1 核心思想

同一个输入 X 经过三个不同投影生成 Q、K、V

Q、K、V 都是从同一个输入 X 生成的,通过三个不同的权重矩阵:

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

这三个权重矩阵(W_Q、W_K、W_V)是可学习的参数,会在训练过程中不断调整。

10.3.2 维度计算

合并投影的 Q、K、V 拆成多个 head

以生成 Q 为例:

X:  [4, 16, 512]  (batch_size, seq_length, d_model)
W_Q:[512, 512]    (d_model, h × d_k)
Q:  [4, 16, 512]  (batch_size, seq_length, h × d_k)

矩阵乘法的维度规则:[..., A, B] @ [B, C] = [..., A, C]

所以:[4, 16, 512] @ [512, 512] = [4, 16, 512]

这个常见例子取 h × d_k = h × d_v = d_model = 512,所以合并后的 Q、K、V 恰好和 X 同形状。这不是硬性规则:同一个 head 里的 Q、K 必须共享 d_k 才能做点积,V 可以使用不同的 d_v。

10.3.3 为什么需要三个不同的矩阵?

你可能会问:既然形状都一样,为什么要用三个不同的矩阵?

答案是:Q、K、V 承担不同的角色。

  • Q(Query,查询):代表"我在找什么信息"
  • K(Key,键):代表"我有什么信息可以被找到"
  • V(Value,值):代表"如果被找到,我提供什么内容"

通过学习不同的 W_Q、W_K、W_V,模型可以把同一个输入变成三种视角。Q 和 K 会进入彼此兼容的逐头空间来打分,V 则携带最后要混合进输出的特征。

10.3.4 一个直观的类比

想象一个图书馆检索系统:

角色类比作用
Query (Q)读者的搜索词"我想找关于机器学习的书"
Key (K)每本书的索引标签"机器学习, Python, 入门"
Value (V)书的实际内容整本书的内容

当你搜索时:

  1. 用你的 Query 和每本书的 Key 比较
  2. 匹配分更高的书得到更大的归一化权重
  3. 按这些权重混合各本书的 Value(内容)

10.4 第一次矩阵乘法:Q @ K^T

10.4.1 计算匹配分数矩阵

QK 转置点积经过缩放、Mask 和 Softmax 变成权重

合并投影拆成多个 head 以后,下一步是在每个 head 内计算 Query-Key 匹配分:

Q @ K^T

注意:K 需要转置(K^T),因为我们要让 Q 的每一行和 K 的每一行做点积。

10.4.2 维度变化

Q:      [4, 4, 16, 128]  (batch, heads, seq, d_k)
K^T:    [4, 4, 128, 16]  (batch, heads, d_k, seq)
scores: [4, 4, 16, 16]   (batch, heads, query_seq, key_seq)

这里取 h = 4,所以 d_k = d_model / h = 512 / 4 = 128。head 到底怎么拆,下一章详细讲;这一章先把 head 这一维写出来,保证每一步形状都对得上。

10.4.3 结果的含义

结果是一个 [4, 4, 16, 16] 的张量:

  • 4 个句子
  • 每个句子有 4 个 head
  • 每个 head 有一个 16×16 的原始分数矩阵
  • 位置 (i,j) 是第 i 个 Query 和第 j 个 Key 的点积

图中下方用“猫爱吃鱼”做了一个手工简化示例。它只演示形状和点积,不是从真实模型里测出来的:

       猫  爱  吃  鱼
猫    [            ]
爱    [            ]
吃    [            ]
鱼    [            ]

每个位置的值 = 对应 Q 行向量和 K 列向量的点积。真实 head 不一定对角线更大;高分也不等于“语义相关性的真值”。还要经过缩放、mask 和逐行 Softmax,它才会变成允许位置上的混合权重。


10.5 Scale:为什么除以 √d_k

10.5.1 缩放操作

匹配分除以根号 d_k 后再进入 Mask 和 Softmax

Q @ K^T 的结果需要缩放:

scaled_scores = (Q @ K^T) / √d_k

除以 √d_k 不是为了把数值硬塞进 [-1, 1],而是让不同 head 宽度下的典型分数尺度大致可比。

10.5.2 为什么要缩放?

问题:当 d_k 变大时,未缩放点积的典型幅度也会变大。

点积 = Σ(q_i × k_i)  # 128 个数相乘再相加

如果 Q、K 各维彼此独立、均值为 0、方差为 1,那么点积的方差约为 d_k,标准差约为 √d_k。

后果:数值太大会让 Softmax 变得极端。

Softmax([100, 1, 2]) ≈ [1.0, 0.0, 0.0]  # 极端分布
Softmax([1.0, 0.1, 0.2]) ≈ [0.539, 0.219, 0.242]  # 没那么饱和

Softmax 饱和时,大多数导数会变得很小,优化更困难。

解决方案:除以 √d_k,让上面理想化条件下的分数标准差回到 1 左右。

点积 / √128 ≈ 点积 / 11.3

10.5.3 公式中的位置

Attention(Q,K,V)=softmax(QKTdk+M)V\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}} + M\right) V

这里的 √d_k 就是 Scale,M 代表在 Softmax 前加入的 causal、padding 等 mask。


10.6 Mask:防止"偷看"未来

10.6.1 为什么需要 Mask?

Causal Mask 把未来 Key 的分数设为负无穷

在 GPT 这样的自回归模型中,预测下一个词时不能看到未来的词。

比如预测"小沈阳江西演唱会邀请了____",模型不能看到答案。

但是 Q @ K 会先计算所有位置对的原始分数,包括未来的位置!其他架构还可能需要 padding 或结构性 mask。

10.6.2 Mask 的实现

解决方案:用一个三角形 Mask 把未来的位置"遮住"。

原始注意力矩阵:          Mask 后:
[0.3, 0.2, 0.1, 0.4]     [0.3, -∞,  -∞,  -∞ ]
[0.2, 0.5, 0.2, 0.1]  →  [0.2, 0.5, -∞,  -∞ ]
[0.1, 0.3, 0.4, 0.2]     [0.1, 0.3, 0.4, -∞ ]
[0.2, 0.1, 0.3, 0.4]     [0.2, 0.1, 0.3, 0.4]

把右上角(未来的位置)设为负无穷(-inf)。

10.6.3 为什么用 -inf?

因为 Softmax 会把 -inf 变成 0:

Softmax([0.3, -∞, -∞, -∞]) = [1.0, 0.0, 0.0, 0.0]

这样,经过 Softmax 后,未来位置的注意力权重就变成 0 了——模型"看不到"未来。

10.6.4 图中的示例

图中沿着同一行展示了 Mask 前后的变化:Scale 后是 [0.32, 1.87, 0.94, 0.72],加入 causal mask 后,未来位置变成 -∞。下方三角矩阵再把规则画完整:第 i 个 Query 只能读取第 i 个位置和它左边的位置。


10.7 Softmax:把分数变成权重

10.7.1 转换过程

逐行 Softmax 把允许位置的分数变成混合权重

Mask 之后,对每一行应用 Softmax:

Softmax 前:[0.32, 1.87, 0.94, -inf]
Softmax 后:[0.132, 0.622, 0.246, 0.000]

10.7.2 Softmax 的作用

  1. 归一化:每一行的和变成 1
  2. 保持排序:分数越大,权重越大;权重比由指数决定
  3. 处理 -inf:-inf 变成 0

10.7.3 图中的观察

看图中的数据:

  • 第 0 行("小"):[1.00000, 0.00000, 0.00000, ...]
    • 只能看到自己,所以自己的权重是 100%
  • 第 1 行("沈"):比如 [0.32, 0.68, 0.00, ...]
    • 可以看到"小"和"沈",但具体权重由学出来的分数决定
  • 后面的行虽然可选位置更多,某个 head 仍然可能把大部分权重集中到一两个位置

10.7.4 这就是"注意力权重"

Softmax 的输出就是注意力权重矩阵——它是混合允许读取的 Value 时用的逐行系数,不是某个 token “语义相关”的概率。


10.8 第二次矩阵乘法:Attention @ V

10.8.1 加权求和

Attention 权重对允许读取的 V 行做加权求和

有了注意力权重,下一步是用它对 V 进行加权求和:

Output = Attention_Weights @ V

10.8.2 维度变化

Attention_Weights: [4, 4, 16, 16]  (batch, heads, ctx_len, ctx_len)
V:                 [4, 4, 16, 128] (batch, heads, key_seq, d_v)
head_output:       [4, 4, 16, 128] (batch, heads, query_seq, d_v)

注意:这里显示的是 Multi-Head 的情况,有 4 个 head。

10.8.3 这一步在做什么?

每个位置的输出 = 所有允许读取的 V 行的加权和,权重来自逐行 Softmax。

output[i] = Σ(attention_weight[i,j] × V[j])

如果第 i 个 token 对第 j 个 token 的注意力是 0.7,对第 k 个 token 的注意力是 0.3,那么:

output[i] = 0.7 × V[j] + 0.3 × V[k]

输出是"根据注意力重新组合"的向量表示。


10.9 Attention 输出的含义

10.9.1 输出维度

多头 Attention 输出经过拼接、W_O 和 residual 路径

经过一个 head 的 Attention_Weights @ V,每个位置得到一个逐头输出:

head_output: [batch_size, ctx_length, d_v] = [4, 16, 128]

4 个 head 的结果会拼回 [4, 16, 512],再经过 W_O 投影回 d_model。下一章会把这一步完全展开。

10.9.2 输出的语义

这个输出有什么特别之处?

它融合了上下文信息!

在第一层,X 从 token 和位置信息出发;到了更深的 Block,X 本身已经带着上下文。每个 Attention head 都会再加入一份来自允许读取的 V 行的加权混合。

10.9.3 图中的说明

更准确的路径是:多个 head 的输出拼接后经过 W_O,Attention 分支再和 residual stream 相加,然后才继续进入后面的子层。它更新的是当前层的隐藏状态,不会直接改写 embedding 参数表。


10.10 完整的 Attention 计算流程

10.10.1 流程图

Scaled Dot-Product Attention 的完整维度流程

让我们把整个流程串起来:

步骤1:生成合并后的 Q, K, V
        Q = X @ W_Q    [4, 16, 512]
        K = X @ W_K    [4, 16, 512]
        V = X @ W_V    [4, 16, 512]
              ↓
        reshape 成 4 个 head;本例 d_k = d_v = 128
        Q, K, V: [4, 4, 16, 128]
              ↓
步骤2:逐 head 计算匹配分
        scores = Q @ K^T    [4, 4, 16, 16]
              ↓
步骤3:缩放
        scores = scores / √d_k    [4, 4, 16, 16]
              ↓
步骤4:按需要加 mask M(causal / padding 等)
        scores = scores + M    [4, 4, 16, 16]
              ↓
步骤5:Softmax
        weights = softmax(scores)    [4, 4, 16, 16]
              ↓
步骤6:逐 head 加权求和
        head_output = weights @ V    [4, 4, 16, 128]
              ↓
        拼接 heads: [4, 16, 512]
        output = concat @ W_O: [4, 16, 512]

10.10.2 PyTorch 代码

# 代码示例
import torch
import torch.nn.functional as F

def attention(Q, K, V, allowed_mask=None):
    """
    计算 Scaled Dot-Product Attention

    Args:
        Q: [..., query_len, d_k]
        K: [..., key_len, d_k]
        V: [..., key_len, d_v]
        allowed_mask: 可广播到 [..., query_len, key_len] 的 bool 张量;
                      True 表示允许读取

    Returns:
        output: [..., query_len, d_v]
        attention_weights: [..., query_len, key_len]
    """
    d_k = Q.size(-1)

    # 步骤2: Q @ K^T
    scores = torch.matmul(Q, K.transpose(-2, -1))

    # 步骤3: Scale
    scores = scores / (d_k ** 0.5)

    # 步骤4: Mask
    if allowed_mask is not None:
        # 每个 Query 行必须至少保留一个可读的 Key。
        scores = scores.masked_fill(~allowed_mask, float('-inf'))

    # 步骤5: Softmax
    attention_weights = F.softmax(scores, dim=-1)

    # 步骤6: 加权求和
    output = torch.matmul(attention_weights, V)

    return output, attention_weights

10.11 Q、K、V 的深层理解

10.11.1 三者的关系

角色生成方式作用参与的计算
QX @ W_Q表示“我在找什么”Q @ K^T
KX @ W_K表示“我挂着什么标签”Q @ K^T
VX @ W_V表示“我能贡献什么内容”weights @ V

10.11.2 为什么 K 和 V 要分开?

你可能会问:K 和 V 都来自同一个输入,为什么要用两个不同的矩阵?

答案:解耦"匹配"和"提取"。

  • K 负责"被匹配":决定哪些位置应该被关注
  • V 负责"被提取":决定关注后提取什么信息

这给了模型更大的灵活性:负责决定路线的特征,可以和沿着路线传递的特征不同。

10.11.3 一个例子

考虑这个手工例句:“皮影艺人收起了驴皮影人。”

当处理“收起”时:

  • Q("收起") 可能在找“谁做了这个动作”
  • K("皮影艺人") 的表示可能包含“我是动作的发出者”
  • V("皮影艺人") 的表示可能携带这个人物在当前层里的特征

这是某个训练好的 head 可能学会的模式,不代表每个模型、每个 head 都按这套语法角色编码。


10.12 本章总结

10.12.1 核心概念

概念形状含义
X[batch, seq, d_model]输入向量
W_Q / W_K[d_model, h × d_k]可学习的 Query / Key 投影
W_V[d_model, h × d_v]可学习的 Value 投影
Q、K[batch, h, seq, d_k]逐 head 的 Query / Key
V[batch, h, seq, d_v]逐 head 的 Value
Scores[batch, h, query_seq, key_seq]原始匹配分数
Weights与 Scores 同形状mask + Softmax 后的逐行混合权重
Head output[batch, h, query_seq, d_v]每个 head 对 V 的加权混合
Block 输出[batch, seq, d_model]拼接各 head,再经过 W_O

10.12.2 计算流程

X → [W_Q, W_K, W_V] → 合并 Q, K, V → 拆成 heads
              ↓
        Q @ K^T (匹配分数)
              ↓
        / √d_k (缩放)
              ↓
        + M (按需要做 causal / padding mask)
              ↓
        Softmax (归一化)
              ↓
        @ V (逐 head 加权求和)
              ↓
        拼接 heads → W_O → Output

10.12.3 核心认知

Q、K、V 是 Attention 的三个主角。Q 发问,K 挂标签,它们的点积给出学出来的匹配分;缩放、mask 和 Softmax 把每行变成路由权重,再用这些权重混合 V。最后把各个 head 合并,经 W_O 回到 residual stream。


本章交付物

学完这一章,你应该能够:

  • 说出 Q、K、V 各自的含义
  • 解释它们是如何从输入 X 生成的
  • 追踪 Attention 计算过程中的维度变化
  • 理解 Mask 的作用(防止看到未来)
  • 解释为什么要 Scale(除以 √d_k)

下一章预告

这一章我们把 QKV 的整条路径走完了,并且把 head 这一维提前写出来,保证形状从头到尾一致。

但实际的 Transformer 使用的是 Multi-Head Attention(多头注意力)——把 Attention 分成多个"头",每个头关注不同的信息。

为什么要这样做?多个头是怎么工作的?下一章,我们来揭开 Multi-Head Attention 的神秘面纱!

引用本文 / Cite
Zhang, Wayland (2026). 第 10 章:QKV 到底是什么 - Attention 的三个主角. In Transformer 架构:从直觉到实现. https://waylandz.com/llm-transformer-book/%E7%AC%AC10%E7%AB%A0-QKV%E5%88%B0%E5%BA%95%E6%98%AF%E4%BB%80%E4%B9%88-Attention%E7%9A%84%E4%B8%89%E4%B8%AA%E4%B8%BB%E8%A7%92/
@incollection{zhang2026transformer_10_-QKV_-Attention,
  author = {Zhang, Wayland},
  title = {第 10 章:QKV 到底是什么 - Attention 的三个主角},
  booktitle = {Transformer 架构:从直觉到实现},
  year = {2026},
  url = {https://waylandz.com/llm-transformer-book/%E7%AC%AC10%E7%AB%A0-QKV%E5%88%B0%E5%BA%95%E6%98%AF%E4%BB%80%E4%B9%88-Attention%E7%9A%84%E4%B8%89%E4%B8%AA%E4%B8%BB%E8%A7%92/}
}