从入门到入神|PyTorch 基础系列 05
上一篇我们讲了 Flatten:
[8,128,8,8]
↓
Flatten
↓
[8,8192]
但在很多 CNN 中,还会看到这样的代码:
nn.AdaptiveAvgPool2d((1, 1))
然后 Tensor 变成:
[8,128,8,8]
↓
[8,128,1,1]
↓
[8,128]
第一次看到这里,很容易产生几个问题:
GAP 到底是什么?
(1,1)是什么意思?
为什么8×8可以直接变成1×1?
为什么 128 个 Channel 没有变化?
GAP 和普通 Pooling、Flatten 又有什么区别?
这一关,我们只围绕一个核心问题:
Global Average Pooling 到底对 Tensor 做了什么?
一、先看完整代码
还是和前几篇一样,我们先把代码完整跑起来,再一点一点看里面发生了什么。
import torch
import torch.nn as nn
# ==========================================
# 1. 构造输入 Tensor
# ==========================================
x = torch.randn(8, 128, 8, 8)
print("原始 Tensor:")
print(x.shape)
# ==========================================
# 2. Global Average Pooling
# ==========================================
gap = nn.AdaptiveAvgPool2d((1, 1))
x = gap(x)
print("\nGAP 后:")
print(x.shape)
# ==========================================
# 3. Flatten
# ==========================================
x = torch.flatten(x, 1)
print("\nFlatten 后:")
print(x.shape)
运行结果:
原始 Tensor:
torch.Size([8, 128, 8, 8])
GAP 后:
torch.Size([8, 128, 1, 1])
Flatten 后:
torch.Size([8, 128])
所以整段代码实际上只完成了两个变化:
[8,128,8,8]
↓ GAP
[8,128,1,1]
↓ Flatten
[8,128]
PyTorch 中,AdaptiveAvgPool2d((1,1)) 会把每个输入平面的空间输出尺寸变成 1×1,同时保持输入和输出的通道数量一致。
接下来,我们只需要弄明白:
为什么 GAP 能把
8×8变成1×1?
二、GAP 到底是什么?结合代码一起看
先看这一行:
gap = nn.AdaptiveAvgPool2d((1, 1))
这里最重要的是:
(1, 1)
它表示我们希望最终每张 Feature Map 的空间尺寸变成:
1 × 1
比如原来一个 Channel 是:
8 × 8
经过:
nn.AdaptiveAvgPool2d((1, 1))
以后变成:
1 × 1
所以:
8×8
↓
1×1
但问题来了:
原来
8×8有 64 个数,现在只剩 1 个数,这个数是怎么来的?
答案就是:
把这一整个 Channel 中的所有空间位置求平均。
先拿一个小一点的 Feature Map 举例
为了方便计算,假设某一个 Channel 是一张 4×4 Feature Map:
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 16
这张 Feature Map 一共有:
4 × 4 = 16
个数。
Global Average Pooling 会把这 16 个数全部加起来,再除以 16:
(1 + 2 + 3 + ... + 16) / 16
结果是:
8.5
于是:
4×4 Feature Map
↓
Global Average Pooling
↓
1×1
↓
8.5
这就是 Global Average Pooling 名字里的 Global:
不是只看一个局部区域,而是对整张 Feature Map 的空间区域求平均。
三、真实的 8×8 又是怎么计算的?
回到我们的代码:
x = torch.randn(8, 128, 8, 8)
其中:
8 = Batch
128 = Channel
8 = Height
8 = Width
先只看一个样本中的一个 Channel。
它是一张:
8 × 8
的 Feature Map。
里面一共有:
8 × 8 = 64
个特征值。
GAP 做的事情就是:
64 个特征值
↓
全部求和
↓
除以 64
↓
得到 1 个平均值
因此:
8×8
↓
1×1
所以千万不要把它理解成:
用了一个 2×2 Pooling
↓
8×8 直接变成 1×1
不是这样的。
这里是整个 8×8 空间区域共同产生一个平均值。
四、那 128 个 Channel 怎么办?
现在再回来看完整 Tensor:
[8,128,8,8]
对于其中一个样本来说,它不是只有一张 Feature Map,而是有:
128 张 Feature Map
每一张大小都是:
8×8
GAP 会分别处理它们:
Channel 1:
8×8 → 1×1
Channel 2:
8×8 → 1×1
Channel 3:
8×8 → 1×1
...
Channel 128:
8×8 → 1×1
所以原本:
128 张 8×8 Feature Map
变成:
128 张 1×1 Feature Map
最终 Shape:
[8,128,8,8]
↓
GAP
↓
[8,128,1,1]
这也解释了为什么:
Channel:128 → 128
没有变化。
因为 GAP 并没有把不同 Channel 混在一起,而是每个 Channel 分别对自己的 H、W 求平均。PyTorch 官方文档也说明 AdaptiveAvgPool2d 的输出特征数量与输入平面数量保持一致。
一句话记住:
GAP 压缩的是 H 和 W,不是 Channel。
五、为什么代码最后又变成了 [8,128]?
我们的代码还有一句:
x = torch.flatten(x, 1)
GAP 后:
[8,128,1,1]
经过 Flatten:
[8,128]
因为:
128 × 1 × 1 = 128
所以完整过程一定要分成两步来看:
[8,128,8,8]
↓ AdaptiveAvgPool2d((1,1))
[8,128,1,1]
↓ torch.flatten(x,1)
[8,128]
上一篇我们已经讲过,torch.flatten(x,1) 会从第 1 维开始展开,因此 Batch 维度仍然保留。
所以:
GAP 本身得到的是
[8,128,1,1],不是[8,128]。
最后变成 [8,128],是因为后面又执行了一次 Flatten。
六、GAP 和普通 Average Pooling 有什么区别?
上一篇我们讲 Pooling 时,见过:
nn.AvgPool2d(
kernel_size=2,
stride=2
)
如果输入 Feature Map 是:
8×8
那么这种 2×2 Average Pooling 会得到:
8×8
↓
4×4
因为它每次只处理一个局部 2×2 区域。AvgPool2d 本身就是按照指定的 pooling window 对输入进行平均池化。
而 GAP 是:
8×8
↓
1×1
所以可以先这样理解:
普通 AvgPool2d(2,2)
局部区域求平均
8×8 → 4×4
而:
Global Average Pooling
整张 Feature Map 求平均
8×8 → 1×1
这里真正重要的不是名字,而是:
平均的范围不一样。
七、GAP 和 Flatten 到底有什么区别?
这也是最容易混淆的地方。
假设输入完全一样:
[8,128,8,8]
使用 Flatten
[8,128,8,8]
↓
Flatten
↓
[8,8192]
因为:
128 × 8 × 8 = 8192
它把所有位置的特征值全部展开。
使用 GAP
[8,128,8,8]
↓
GAP
↓
[8,128,1,1]
↓
Flatten
↓
[8,128]
GAP 会把每一个 Channel 中原来的:
64 个空间位置
压缩成:
1 个平均值
所以两条路线可以直接对比:
Flatten:
[8,128,8,8]
↓
[8,8192]
保留所有空间位置的特征值
GAP:
[8,128,8,8]
↓
[8,128]
每个 Channel 最终只保留一个平均值
这就是它们最大的区别。
八、为什么很多 CNN 会使用 GAP?
现在这个问题就比较容易理解了。
如果直接 Flatten:
[8,128,8,8]
↓
[8,8192]
后面的全连接层需要处理:
8192 个特征
而经过 GAP:
[8,128,8,8]
↓
[8,128]
后面只需要处理:
128 个特征
所以 GAP 本身并不是“拥有更少参数”。
实际上:
Flatten:没有可学习参数
GAP:也没有可学习参数
真正变化的是:
送入后续 Linear 层的特征维度大幅减小。
因此,使用 GAP 后,后面的全连接层通常可以使用更少的参数。
九、GAP 会不会丢失信息?
会。
而且这里要准确理解它到底丢了什么。
原来一张 Feature Map 是:
8×8
网络还能知道:
哪个位置响应强
哪个位置响应弱
特征出现在左边还是右边
特征出现在上面还是下面
但经过 GAP:
8×8
↓
1×1
所有位置最终只剩一个平均值。
所以 GAP 更关注:
这个 Channel 整体响应有多强。
而不再显式保留:
这个响应具体出现在哪里。
因此:
GAP 会明显压缩空间位置信息。
这也是为什么 GAP 并不是任何地方都可以随便使用。
十、把这一关真正串起来
现在再来看最开始的代码:
x = torch.randn(8, 128, 8, 8)
gap = nn.AdaptiveAvgPool2d((1, 1))
x = gap(x)
x = torch.flatten(x, 1)
你应该已经能够把每一步翻译出来。
第一步:
[8,128,8,8]
表示:
8 个样本,每个样本有 128 张
8×8Feature Map。
第二步:
gap = nn.AdaptiveAvgPool2d((1, 1))
表示:
把每一张 Feature Map 的空间尺寸压缩成
1×1。
所以:
[8,128,8,8]
↓
[8,128,1,1]
第三步:
torch.flatten(x, 1)
把:
128 × 1 × 1
展开成:
128
于是:
[8,128,1,1]
↓
[8,128]
最终完整数据流:
[8,128,8,8]
↓
Global Average Pooling
↓
[8,128,1,1]
↓
Flatten
↓
[8,128]
总结
这一关真正需要记住的,其实只有四句话。
第一:
GAP 会分别对每一个 Channel 的整个 H×W 空间区域求平均。
第二:
8×8
↓
GAP
↓
1×1
不是局部 2×2 Pooling,而是整张 Feature Map 共同得到一个平均值。
第三:
GAP 通常不会改变 Channel:
[8,128,8,8]
↓
[8,128,1,1]
第四:
GAP 后再 Flatten:
[8,128,1,1]
↓
[8,128]
所以最终:
[8,128,8,8]
↓
GAP
↓
[8,128]
如果现在再看到:
nn.AdaptiveAvgPool2d((1, 1))
你应该不再只是知道:
“这是一个池化层。”
而是能够真正说清楚:
它把每个 Channel 的整张 Feature Map 求平均,让 H、W 压缩成
1×1,同时保持 Channel 数量不变。
下一关:Residual Connection 到底是什么?
下一篇我们开始进入真正的网络结构:
深度学习入门第六关:为什么
x + F(x)可以直接相加?Residual 到底在“残差”什么?
下一关会重点解决:
x + F(x) 到底是什么意思、为什么两个 Tensor 不能随便相加、为什么 64 Channel 和 128 Channel 不能直接 Add,以及为什么 Shortcut 中经常出现 1×1 Conv。
从这里开始,我们会从:
看懂一个网络层
逐渐进入:
看懂一个网络为什么这样设计。
从入门,到入神。

308


被折叠的 条评论
为什么被折叠?



