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

从这里开始,我们会从:

看懂一个网络层

逐渐进入:

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

从入门,到入神。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值