三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

深度学习入门第五关:Global Average Pooling 到底是什么?为什么能把 [8,128,8,8] 变成 [8,128]?

深度学习入门第五关: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×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×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

从这里开始,我们会从:

看懂一个网络层

逐渐进入:

看懂一个网络为什么这样设计。

从入门,到入神。

← 返回列表