从入门到入神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 Map1 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]这也解释了为什么Channel128 → 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_size2, stride2 )如果输入 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网络还能知道哪个位置响应强 哪个位置响应弱 特征出现在左边还是右边 特征出现在上面还是下面但经过 GAP8×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。从这里开始我们会从看懂一个网络层逐渐进入看懂一个网络为什么这样设计。从入门到入神。