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 维度是独立的头,互不干扰。

我习惯分两步走:

  1. 先 view 成 (batch, seq, h, d_k) —— 把 d_model 拆成 h 份
  2. 再 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 小结

分头操作其实就两行代码:

  1. view(batch, seq, h, d_k) —— 把 d_model 切成 h 份
  2. transpose(1, 2) —— 把 h 维度提到 seq 前面

这两步缺一不可,顺序也不能错。我刚开始学的时候,总是记混了顺序,后来写了个口诀:「先 view 拆开,再 transpose 换位」。你试试看,是不是好记多了?

下一节,我们会用这些分好头的 Q、K、V 来做真正的注意力计算。准备好了吗?