4、分头操作:view与transpose实现维度重塑
好,咱们接着往下走。上一节我们把 Q、K、V 都准备好了,维度都是 (batch, seq, d_model)。但多头注意力机制要求我们把它们拆成多个头,对吧?
说白了,就是把一个大的特征空间,切成 h 份,每份负责不同的子空间。我刚开始接触这个操作时,觉得不就是 reshape 一下嘛,有啥难的?结果第一次跑模型就报错了,维度对不上。嗯,这里面的门道还真不少。
4.1 为什么要分头?
先想一个问题:为什么要把一个注意力头拆成多个?
我个人理解是:单一的头容易「偏科」。它可能只关注了句法关系,忽略了语义信息。多头机制让每个头关注不同的特征子空间,最后拼起来,信息更丰富。
举个例子,你让一个团队做项目,有人负责技术,有人负责沟通,有人负责进度。每个角色各司其职,最后汇总成果。多头注意力也是这个道理。
核心公式回顾:
每个头的维度:d_k = d_model / h
比如 d_model=512,h=8,那么 d_k=64。每个头只处理 64 维的特征。
4.2 从 (batch, seq, d_model) 到 (batch, h, seq, d_k)
目标很明确:把 (batch, seq, d_model) 变成 (batch, h, seq, d_k)。
为什么要变成这个形状?因为后续我们要在 seq 维度上做注意力计算,而 h 维度是独立的头,互不干扰。
我习惯分两步走:
- 先 view 成 (batch, seq, h, d_k) —— 把 d_model 拆成 h 份
- 再 transpose 成 (batch, h, seq, d_k) —— 把 h 维度提到 seq 前面
你想想看,为什么不能一步到位?因为 view 操作只能改变形状,不能交换维度顺序。而我们需要把 h 放到 seq 前面,这样才能方便后续的批量计算。
4.3 代码实现:view + transpose
直接上代码,我习惯用 PyTorch 来演示:
import torch
batch_size = 2
seq_len = 10
d_model = 512
h = 8
d_k = d_model // h # 64
# 模拟 Q 矩阵
Q = torch.randn(batch_size, seq_len, d_model)
# 第一步:view 成 (batch, seq, h, d_k)
Q_view = Q.view(batch_size, seq_len, h, d_k)
print("view 后形状:", Q_view.shape) # (2, 10, 8, 64)
# 第二步:transpose 成 (batch, h, seq, d_k)
Q_trans = Q_view.transpose(1, 2)
print("transpose 后形状:", Q_trans.shape) # (2, 8, 10, 64)
避坑指南:
我曾经犯过一个低级错误——直接用 view(batch_size, h, seq_len, d_k)。结果呢?数据全乱了!因为 view 是按内存顺序填充的,它不会自动帮你把 h 维度提取出来。必须先 view 成 (batch, seq, h, d_k),再 transpose。
4.4 view 和 transpose 的区别
这两个操作经常被混淆,我简单总结一下:
| 操作 | 作用 | 是否改变内存布局 | 是否可逆 |
|---|---|---|---|
| view | 重塑形状,不改变元素顺序 | 否(只是改变视图) | 是(只要总元素数不变) |
| transpose | 交换两个维度 | 是(内存布局会变) | 是(再 transpose 回来) |
举个例子你就明白了:
# 假设有一个 2x3 的矩阵
x = torch.tensor([[1, 2, 3],
[4, 5, 6]])
# view 只是改变形状
x_view = x.view(3, 2)
print(x_view)
# 输出:
# tensor([[1, 2],
# [3, 4],
# [5, 6]])
# transpose 交换维度
x_trans = x.transpose(0, 1)
print(x_trans)
# 输出:
# tensor([[1, 4],
# [2, 5],
# [3, 6]])
看到了吗?view 是按行顺序填充的,而 transpose 是真正的行列交换。在多头注意力中,我们必须用 transpose 来把 h 维度提前。
4.5 完整的分头操作流程
把 Q、K、V 都做同样的处理:
def split_heads(x, h):
"""
将 (batch, seq, d_model) 拆分为 (batch, h, seq, d_k)
"""
batch_size, seq_len, d_model = x.shape
d_k = d_model // h
# 先 view 再 transpose
x = x.view(batch_size, seq_len, h, d_k)
x = x.transpose(1, 2)
return x
# 测试
Q = torch.randn(2, 10, 512)
K = torch.randn(2, 10, 512)
V = torch.randn(2, 10, 512)
Q_heads = split_heads(Q, 8)
K_heads = split_heads(K, 8)
V_heads = split_heads(V, 8)
print("Q heads shape:", Q_heads.shape) # (2, 8, 10, 64)
print("K heads shape:", K_heads.shape) # (2, 8, 10, 64)
print("V heads shape:", V_heads.shape) # (2, 8, 10, 64)
注意事项:
- d_model 必须能被 h 整除,否则会报错。我建议在代码里加个断言:
assert d_model % h == 0 - transpose 操作会改变内存布局,后续如果要用 view 恢复形状,需要先调用
contiguous() - 不同框架的 transpose 函数名可能不同,PyTorch 是
transpose,TensorFlow 是tf.transpose
4.6 为什么不能直接用 reshape?
有同学可能会问:用 reshape 不行吗?
嗯,这个问题我当年也纠结过。reshape 和 view 在大多数情况下效果一样,但 reshape 更「智能」——如果张量不连续,它会自动拷贝一份连续的内存。而 view 要求张量必须是连续的,否则会报错。
我个人习惯用 view,因为它更「透明」。如果报错了,说明我的操作有问题,需要检查。用 reshape 可能会掩盖一些潜在的错误。
不过话说回来,如果你用 reshape,代码也能跑通:
# 用 reshape 替代 view
Q_reshaped = Q.reshape(batch_size, seq_len, h, d_k).transpose(1, 2)
效果是一样的。但记住:reshape 不是万能的,它不能替代 transpose 来交换维度。
4.7 小结
分头操作其实就两行代码:
view(batch, seq, h, d_k)—— 把 d_model 切成 h 份transpose(1, 2)—— 把 h 维度提到 seq 前面
这两步缺一不可,顺序也不能错。我刚开始学的时候,总是记混了顺序,后来写了个口诀:「先 view 拆开,再 transpose 换位」。你试试看,是不是好记多了?
下一节,我们会用这些分好头的 Q、K、V 来做真正的注意力计算。准备好了吗?