深度学习入门第五关:Global Average Pooling 到底是什么?为什么能把 [8,128,8,8] 变成 [8,128]?
从入门到入神|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×4Feature 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×8GAP 会分别处理它们:
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×2Average 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×2Pooling,而是整张 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。
从这里开始,我们会从:
看懂一个网络层
逐渐进入:
看懂一个网络为什么这样设计。
从入门,到入神。
