【深度学习小课堂】| torch | 给张量“瘦身”与“扩容”:深度拆解 PyTorch 中 squeeze 与 squeeze 的维度魔法,squeeze与squeeze 到底会不会导致信息丢失?
1. 维度升降的本质:为什么需要“1”?
在 PyTorch 的张量世界里,形状(Shape)决定了一切。我们经常会遇到维度为 1 的情况,比如 (Batch, Channel, 1, 1)。这些大小为 1 的维度虽然不增加任何数据量,但它们是对齐张量、匹配函数输入以及触发广播机制的关键。
unsqueeze(升维):在指定位置插入一个大小为 1 的新维度。squeeze(降维):移除所有(或指定位置)大小为 1 的维度。
2. unsqueeze:无中生有的“扩容术”
unsqueeze 用于在张量的特定位置增加一个维度。这在处理单样本推理或需要手动对齐维度时非常有用。
代码演示与分析
import torch
# 构造一个 1 维张量 (向量),长度为 3
x = torch.tensor([1, 2, 3])
print(f"原始形状: {x.shape}") # torch.Size([3])
# 在第 0 维增加一个维度
# 常用于给单张图片增加 Batch 维度: [C, H, W] -> [1, C, H, W]
x_0 = x.unsqueeze(0)
print(f"unsqueeze(0) 后: {x_0.shape}") # torch.Size([1, 3])
# 在第 1 维增加一个维度
x_1 = x.unsqueeze(1)
print(f"unsqueeze(1) 后: {x_1.shape}") # torch.Size([3, 1])
核心价值:触发广播机制
如果你有一个形状为 (3)(3)(3) 的偏置项,想加到形状为 (3,10)(3, 10)(3,10) 的特征图上,直接相加会报错。通过 unsqueeze(1) 将偏置变为 (3,1)(3, 1)(3,1),PyTorch 就能自动将其“广播”成 (3,10)(3, 10)(3,10) 完成计算。
3. squeeze:精准打击的“瘦身术”
当一个维度的大小为 1 时,它往往是多余的。squeeze 的任务就是剔除这些多余的“空维度”。
代码演示与分析
# 构造一个形状为 (1, 5, 1, 1) 的特征图 (类似池化后的输出)
y = torch.randn(1, 5, 1, 1)
# 默认模式:移除所有大小为 1 的维度
y_all = y.squeeze()
print(f"squeeze() 全部后: {y_all.shape}") # torch.Size([5])
# 指定模式:只移除指定的维度
# 如果该维度大小不是 1,则操作无效 (保持原样)
y_dim1 = y.squeeze(1)
print(f"尝试对 dim=1 squeeze 后: {y_dim1.shape}") # torch.Size([1, 5, 1, 1])
y_dim0 = y.squeeze(0)
print(f"对 dim=0 squeeze 后: {y_dim0.shape}") # torch.Size([5, 1, 1])
4. 实战对比:以全局平均池化为例
在深度学习研究中,这种操作极其常见。
- 场景:输入 [B,C,H,W][B, C, H, W][B,C,H,W] 经过 nn.AdaptiveAvgPool2d(1)。
- 结果:输出形状为 [B,C,1,1][B, C, 1, 1][B,C,1,1]。
- 痛点:如果你后续要接入全连接层(FC),FC 层期望的输入是 [B,C][B, C][B,C]。
解决方案对比:
y = output.squeeze():简单粗暴,但有风险。如果 Batch Size 恰好也是 1,它会把 维度也压掉,导致模型报错。y = output.squeeze(-1).squeeze(-1):最安全的做法,精准移除末尾的两个 1 像素维度。y = output.view(B, -1):另一种常用手段。
总结:维度体操的黄金法则
| 函数 | 操作方向 | 核心要求 | 典型应用场景 |
|---|---|---|---|
unsqueeze | 升维 ( 维) | 需指定 dim 位置 | 单样本增加 Batch 维度、对齐维度以触发广播 |
squeeze | 降维 ( 维) | 目标维度大小必须为 1 | 池化后降维、移除冗余维度、特征送入全连接层 |
5. squeeze与squeeze操作维度为1的张量,到底会不会导致信息丢失?
5.1 深度理解:维度为 1 到底是什么?
我们可以通过一个公式来理解张量的元素总量:Total Elements=d1×d2×⋯×dn\text{Total Elements} = d_1 \times d_2 \times \dots \times d_nTotal Elements=d1×d2×⋯×dn
假设你有一个张量,形状是 (1,5,1,1)(1, 5, 1, 1)(1,5,1,1):
- 它的总元素个数是:1×5×1×1=51 \times 5 \times 1 \times 1 = 51×5×1×1=5 个。
- 如果你把它 squeeze 变成 (5)(5)(5):
- 它的总元素个数依然是:555 个。
关键点: 维度为 1 就像是一个单间。虽然这个维度存在,但它里面只住了一个“房客”。当你移除这个维度时,数据(房客)并没有消失,只是它不再住在那个单间里了,而是直接暴露在更高一级的维度中。
5.2 类比:地址与容器
为了通俗理解,我们可以用快递包装来做类比:
- 场景 A (4维张量): 你买了一根笔,被套了四个盒子。地址写着:中国(1) -> 上海市(5) -> 某街道(1) -> 某小区(1)。
- 场景 B (1维张量): 你直接拿到了这根笔。地址写着:上海市(5)。
在这两个场景中,笔(数据) 始终是那一根,没有任何零件丢失。区别仅仅在于你为了找到这根笔,需要拆开多少层包装(维度)。在代码中,squeeze的过程就是把那些“只装了一个东西”的冗余包装拆掉。
5.3 为什么直接移除不会丢失信息?
在计算机内存中,张量其实是一排连续存放的数字。所谓的维度(Shape),只是我们给这排数字加上的“索引规则”。
-
原始数据: [1.2, 3.4, 5.6, 7.8, 9.0]
-
形状 (1, 5, 1, 1): 你告诉计算机,要通过四个坐标才能找到一个数,比如 data[0][2][0][0] 对应 5.6。
-
形状 (5): 你告诉计算机,直接通过一个坐标就能找到,比如 data[2] 对应 5.6。
数据本身在内存里的位置和数值完全没变,变的是你访问它的方式。所以,信息是 100% 安全的。
5.4 既然不丢信息,为什么还要留着“1”?
既然维度 1 看起来这么累赘,为什么网络层(比如池化层)非要产出 (B, C, 1, 1) 这种形状呢?这主要是为了广播机制(Broadcasting)。
如果你想让一个权重向量乘回原始特征图:
- 特征图 (原始数据): (B,C,H,W)(B, C, H, W)(B,C,H,W) —— 这是一个 4 维张量。
- 权重向量 (池化后): 如果是 (C)(C)(C),PyTorch 会不知道这 CCC 个数该往哪对齐。
- 带 1 的权重: 如果是 (1,C,1,1)(1, C, 1, 1)(1,C,1,1),PyTorch 就会秒懂:“哦!要把这 CCC 个数沿着 HHH 和 WWW 维度复制填充,直到铺满整个特征图。”
5. 总结
- 维度为 1 的位置有元素吗? 有,且只有一个。
- 移除会丢失信息吗? 不会。数值、精度、顺序完全不变。
- 什么时候移除? 当你需要把数据送入那些“嫌弃”多余维度的层(比如全连接层 Linear),或者你想简化索引时。
- 什么时候保留? 当你需要进行张量之间的算术运算(加减乘除),且需要维度对齐时。
你可以把 squeeze 想象成脱掉外壳。壳子没了,但核心的内容物始终都在。
在深度学习中,张量总元素个数是不变的,改变的只是我们看待它的视角。掌握 squeeze 和 unsqueeze,本质上是掌握了与 PyTorch 自动广播机制对话的语言。
更多推荐
所有评论(0)