Tensor 变形:先学会读 shape,再写 reshape
🎯 先猜一猜(下注解锁)
把 [B,C,H,W] 变成 [B,H×W,C] 时,最危险的不是 API 名字,而是元素顺序。reshape 只会重新分组,不会自动把 C 挪到最后。
先判断:为什么通常要先 permute,再 reshape?
先补的知识
- Tensor 的 shape 不是一串数字,而是每个轴的语义。
- 图像特征常见布局是 [B, C, H, W]:B 是样本数,C 是通道,H/W 是空间网格。
- Transformer 更喜欢序列布局:[B, S, C],其中 S 可以是 H * W。
图解原理
把一张 H×W 的小棋盘摊平成一条路线:每个格子变成一个 token,格子里带着 C 个通道特征。
同一张图有两个通道,像两张叠在一起的 2×3 透明胶片。
现在每个空间位置都带着一个 2 维小向量。
Transformer 看到的是 6 个 token,每个 token 有 2 个特征。
你不需要先背 permute(0, 2, 3, 1)。先问自己:我要把 C 从第 2 个轴挪到最后,再把 H 和 W 合成序列长度。
| 玩具例子 | Notebook 变量 | 检查点 |
|---|---|---|
[b,t,f] | x: [B,C,H,W] | 先说清每个轴代表什么 |
permute | [B,H,W,C] | C 必须来到最后 |
reshape | [B,H*W,C] | 只合并 H 和 W |
| 现象 | 优先检查 |
|---|---|
| shape 变成 [B,C,H×W] | C 没有移动到最后,permute 顺序不对。 |
| shape 正确但 allclose 失败 | 元素顺序被打乱;重新画出第一个 token 应来自哪个像素。 |
| reshape 报不连续 | 理解 reshape/contiguous 的关系,或用 einops 明确表达轴语义。 |
ViT patch、图像 token、Attention head 拆分和 KV Cache 都在做同一类事情:重新组织轴,但不能破坏“哪个数属于哪个 token”的语义。
语法热身:换轴和合并维度怎么写
下面用一个“时间序列”小例子练语法,不使用 notebook 里的图像 shape。
# x: [batch, time, feature]
x = torch.randn(2, 4, 3)
# 把 feature 放到 time 前面: [batch, feature, time]
y = x.permute(0, 2, 1)
# 把 batch 和 time 合并: [batch * time, feature]
z = x.reshape(2 * 4, 3)
# einops 写法:名字就是维度含义
z2 = rearrange(x, "b t f -> (b t) f")
读法:permute 是“重新排列轴”,reshape 是“重新分组元素”,einops 是“把变形规则写成句子”。
举一反三:每次变形前写一行 shape 合同
# 输入合同: x [B,C,H,W]
# 目标合同: tokens [B,H*W,C]
# 数量守恒: C*H*W == (H*W)*C
tokens = x.permute(0, 2, 3, 1).reshape(B, H * W, C)
assert tokens.shape == (B, H * W, C)shape 合同只能保证外形;最后仍要用一个小张量或 allclose 检查元素顺序。
巩固一下
如果 x 的 shape 是 [2, 3, 4, 5],转成图像 token 序列后 shape 应该是多少?
回到 notebook 的作业
这里不直接写答案。你已经拿到足够输入,最后用 notebook 的 TODO 做举一反三。