本站内容由 AI 生成,可能存在错误。如发现问题,欢迎到 GitHub Issues 反馈。

QKV 的数据结构与直觉

QKV 的数据结构与直觉

更新于 2026-04-23

简介:为什么需要 QKV

在上一篇文章中,我们看到 Self-Attention 的核心公式:

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

这个公式中出现了三个矩阵:Q(Query)、K(Key)、V(Value)。它们是从哪里来的?为什么需要三个不同的矩阵,而不是直接用输入本身?

答案是:通过三组不同的线性投影,让同一个输入在不同的子空间中扮演不同的角色。这使得模型能够灵活地学习”问什么”、“用什么来匹配”和”提取什么信息”这三个本质不同的功能。

QKV 投影总览X输入 embeddingQ = X·W_Q我在找什么?K = X·W_K我包含什么?V = X·W_V我携带什么信息?Attention注意力计算核心思想:同一个输入,三种不同角色

直觉理解:图书馆检索类比

想象你走进一座图书馆,想查找关于”Transformer 架构”的资料:

角色类比说明
Query(查询)你脑中的问题”我想了解 Transformer 架构” — 你带着这个问题去检索
Key(键)书籍的标签/索引卡每本书都有关键词标签:“深度学习”、“注意力机制”、“NLP”等
Value(值)书的实际内容标签匹配后,你真正读到的是书中的文字内容

检索过程如下:

  1. Query 与 Key 比较:你的问题(Query)和每本书的标签(Key)做匹配,计算”相关性分数”
  2. Softmax 归一化:将分数转化为概率分布 — 最相关的书获得最高权重
  3. 加权提取 Value:根据相关性权重,从各本书的内容(Value)中提取信息

关键点在于:标签(Key)和内容(Value)是同一本书的不同表征。标签用来快速匹配,内容才是你真正需要的信息。同理,在 Attention 中,Key 用于计算注意力权重,Value 用于聚合信息 — 它们来自同一个输入,但通过不同的投影承担不同的功能。

图书馆检索类比Query (Q)读者的需求🔍Key (K)书的标签indexValue (V)书的内容contentQ·K^T匹配度softmax选择权重× V取出内容

如果不做投影,而是直接用输入 XX 同时充当 Q、K、V,那么模型只能计算”自身与自身的相似度”,表达能力极为有限。三组独立的投影赋予了模型更强的灵活性。

QKV 维度流向输入权重矩阵输出X[B, S, 512]×W_Q[512, 512]Q[B, S, 512]X[B, S, 512]×W_K[512, 64]K[B, S, 64](GQA 共享)X[B, S, 512]×W_V[512, 64]V[B, S, 64](GQA 共享)矩阵乘法维度匹配:[B, S, 512] × [512, d] → [B, S, d]

数学定义:线性投影公式和维度

输入

设输入序列经过 Embedding 后的矩阵为 XX

XRS×HX \in \mathbb{R}^{S \times H}

其中:

  • SS 是序列长度(token 数量)
  • HH 是隐藏层维度(hidden size)
X:(S=seq_len,H=hidden)

权重矩阵

三组可学习的权重矩阵:

WQRH×dk,WKRH×dk,WVRH×dvW_Q \in \mathbb{R}^{H \times d_k}, \quad W_K \in \mathbb{R}^{H \times d_k}, \quad W_V \in \mathbb{R}^{H \times d_v}

其中 dkd_k(也常写作 dheadd_{\text{head}})是每个注意力头的维度。在标准 Transformer 中,通常 dk=dvd_k = d_v

线性投影

Q、K、V 的计算非常简单 — 就是矩阵乘法:

Q=XWQRS×dkQ = X W_Q \in \mathbb{R}^{S \times d_k} K=XWKRS×dkK = X W_K \in \mathbb{R}^{S \times d_k} V=XWVRS×dvV = X W_V \in \mathbb{R}^{S \times d_v}

每个 token 的 HH 维隐藏向量被投影(线性变换)到 dkd_k 维的新空间。这就是所谓的”线性投影”(linear projection)。

为什么叫”投影”? 从几何角度看,乘以权重矩阵相当于将高维空间中的向量投射到一个低维子空间上。不同的权重矩阵定义了不同的子空间 — Query 空间、Key 空间、Value 空间。

Q 空间
"我在找什么"
垫子
K 空间
"我能提供什么"
垫子
V 空间
"我的实际内容"
垫子
垫子

分步可视化

下面用一个小例子(S=4S=4, H=6H=6, dk=3d_k=3)演示 QKV 线性投影的完整过程:

输入矩阵 X

输入矩阵 X 的形状为 (S=4, H=6)。每一行代表一个 token 的隐藏表示向量。

X ∈ ℝ^(4×6)
h₁
h₂
h₃
h₄
h₅
h₆
t₁
0.05
0.11
0.42
0.03
0.89
0.59
t₂
0.63
0.06
0.25
0.44
0.56
0.76
t₃
0.32
0.72
0.77
0.19
0.43
0.64
t₄
0.93
0.80
0.29
0.82
0.80
0.08
(4, 6)

张量形状追踪

从输入到 Q、K、V 的每一步维度变化:

步骤操作形状说明
1输入 XX(S,H)(S, H)每行是一个 token 的隐藏向量
2权重矩阵 WQW_Q(H,dk)(H, d_k)可学习参数
3Q=XWQQ = X W_Q(S,dk)(S, d_k)矩阵乘法:(S,H)×(H,dk)(S,dk)(S, H) \times (H, d_k) \to (S, d_k)
4权重矩阵 WKW_K(H,dk)(H, d_k)另一组可学习参数
5K=XWKK = X W_K(S,dk)(S, d_k)(S,H)×(H,dk)(S,dk)(S, H) \times (H, d_k) \to (S, d_k)
6权重矩阵 WVW_V(H,dv)(H, d_v)第三组可学习参数
7V=XWVV = X W_V(S,dv)(S, d_v)(S,H)×(H,dv)(S,dv)(S, H) \times (H, d_v) \to (S, d_v)

加上 batch 维度时,完整的张量形状为:

输入 X:(B=batch,S=seq_len,H=hidden)

经过投影后:

Q, K, V:(B=batch,S=seq_len,d_k=head_dim)

以 GPT-2 Small 为例(H=768H=768, h=12h=12 个头):

参数说明
HH768隐藏维度
hh12注意力头数
dkd_k64每头维度 = 768/12768 / 12

投影前每个 token 是 768 维向量,投影后变成 64 维的 Q/K/V 向量(单个头内)。

多头场景下的 QKV

在 Multi-Head Attention 中,隐藏维度被拆分为 hh 个头:

dk=Hhd_k = \frac{H}{h}

为什么令 dk×h=Hd_k \times h = H 这不是数学上的必然要求,而是一个工程设计选择:

  • 参数量守恒:合并后的权重矩阵恰好为 RH×H\mathbb{R}^{H \times H},参数量与单头 Attention 的一个线性层相同,不会因为引入多头而增加参数
  • 实现高效:一次矩阵乘 (B,S,H)×(H,H)(B,S,H)(B, S, H) \times (H, H) \to (B, S, H) 后,只需 reshape 即可拆出所有头。如果每个头的 dkd_k 不同,就无法使用 reshape,需要按不等长度切分,效率更低
  • 残差连接整齐:Attention 输出需要加回输入(维度 HH)。每个头的输出维度为 dvd_vhh 个头拼接后为 h×dvh \times d_v。当 dv=H/hd_v = H/h 时,拼接维度恰好为 HH,输出投影 WORH×HW_O \in \mathbb{R}^{H \times H} 保持方阵形式。即使拼接维度不等于 HH,也可以用不同大小的 WOW_O 投影回去,但不如这样简洁

值得注意的是,后续的 GQA(Grouped Query Attention)MQA(Multi-Query Attention) 就放松了”Q、K、V 必须有相同头数”这一假设 — MQA 让所有 Q 头共享同一组 K/V,GQA 则按组共享。不过它们的 Q 投影仍然满足 dk×h=Hd_k \times h = H,改变的是 K/V 的头数以减少 KV Cache 大小。

实际实现中,通常不会真的对每个头单独做矩阵乘法。而是用一个大的权重矩阵一次性投影出所有头的结果,然后 reshape:

WQallRH×HW_Q^{\text{all}} \in \mathbb{R}^{H \times H} Qall=XWQallRS×HQ^{\text{all}} = X W_Q^{\text{all}} \in \mathbb{R}^{S \times H}

然后将 QallQ^{\text{all}}(S,H)(S, H) reshape 为 (S,h,dk)(S, h, d_k),再转置为 (h,S,dk)(h, S, d_k),这样每个头就有了自己的 Query 矩阵。

(B,S,H)WQ(B,S,H)reshape(B,S,h,dk)transpose(B,h,S,dk)(B, S, H) \xrightarrow{W_Q} (B, S, H) \xrightarrow{\text{reshape}} (B, S, h, d_k) \xrightarrow{\text{transpose}} (B, h, S, d_k)
(S, H) 原始投影结果

投影后的矩阵 (S, H) = (4, 8)所有元素未区分 head,统一颜色。

Q ∈ (4, 8)
t₁
0,0
0,1
0,2
0,3
0,4
0,5
0,6
0,7
t₂
1,0
1,1
1,2
1,3
1,4
1,5
1,6
1,7
t₃
2,0
2,1
2,2
2,3
2,4
2,5
2,6
2,7
t₄
3,0
3,1
3,2
3,3
3,4
3,5
3,6
3,7

PyTorch 引申:reshape 与 transpose 的底层机制

PyTorch 张量在底层存储三个元数据:data_ptr(内存起始地址)、shape(各维度大小)、stride(沿各维度移动一步跳过的元素数)。访问 tensor[i, j, k, l] 时,实际偏移量为 i×stride[0] + j×stride[1] + k×stride[2] + l×stride[3]

以 shape (1, 4, 2, 3) 为例,连续存储时 stride 为 (24, 6, 3, 1)

  • reshape:只修改 shape 元数据,stride 随之调整,底层内存不动,不复制数据
  • transpose(1, 2):交换 dim1 和 dim2 的 size 和 stride,shape 变为 (1, 2, 4, 3),stride 变为 (24, 3, 6, 1) — 同一块内存,不同的遍历顺序

如何判断 non-contiguous? 连续张量的 stride 必须满足 stride[i] = stride[i+1] × size[i+1](从右到左递增)。transpose 后 stride (24, 3, 6, 1)6 ≠ 1×3,不满足条件,PyTorch 标记为 non-contiguous。后续若调用矩阵乘法等需要连续内存的操作,需要先调用 .contiguous() 按新维度顺序重新排列数据到一块新内存中 — 这时才发生真正的数据拷贝。

Multi-Head Attention 的完整机制将在后续文章中详细展开。这里只需理解:QKV 投影是 Multi-Head 的基础步骤

总结

本文介绍了 Transformer 中 Q、K、V 的核心概念:

  1. 直觉:Query 是”问题”,Key 是”标签”,Value 是”内容” — 类似图书馆检索
  2. 数学本质:三组独立的线性投影 Q=XWQQ = XW_Q, K=XWKK = XW_K, V=XWVV = XW_V
  3. 维度变化:输入 (S,H)(S, H) 经过投影变为 (S,dk)(S, d_k)
  4. 为什么需要三组:不同的投影让同一输入在不同子空间扮演不同角色,赋予模型更强的表达能力
  5. 多头扩展:实际中一次投影完成所有头的 QKV,再通过 reshape 拆分

下一步,我们将进入 Attention 计算详解,看看 Q、K、V 如何通过 QKTQK^T、softmax 和加权求和完成信息聚合。