2 乘 2 池化把 8 乘 8 压成 4 乘 4,元素个数只剩 25%。 最大池化留每块最强的那个响应,平均池化留每块的平均强度。 它换来的是「挪一点也认得出」:平移 1 像素,原图差 14,池化后只差 4。 但这个范围只有一个窗口——平移 2 像素,差值又涨到 7。
上一节的卷积让尺寸从 8 降到 6,通道从 1 升到 4。 6 乘 6 乘 4 是 144 个数,接全连接还是太多。 池化就是再压一次。 它和卷积最大的区别是:池化没有参数。 它不学任何东西,规则是写死的「每块取最大」或「每块求平均」。 所以它不增加模型容量,只改变信息的组织方式。
一、规则只有一个字:块
把特征图切成不重叠的 2 乘 2 块,每块 4 个数变 1 个。 8 乘 8 变 4 乘 4,每边减半,总数变成四分之一。
| 原图 2 乘 2 一块 | 最大池化 | 平均池化 |
|---|---|---|
| 0 1 / 0 0 | 1 | 0.25 |
| 1 1 / 1 1 | 1 | 1.00 |
| 1 0 / 1 1 | 1 | 0.75 |
| 0 0 / 0 0 | 0 | 0.00 |
| 最大池化只关心「这块里最强的是多少」,不关心有几个强。 | ||
| 所以一块里有一个 1 和四个 1,对它来说一样。 | ||
| 这就是它换来平移不变的机制:只要最强的那个还在块里,输出就不变。 |
二、降尺寸的账
8 乘 8 等于 64 个数,池化后 4 乘 4 等于 16 个,剩 25%。 再池化一次就是 2 乘 2,只剩 4 个。 这对后面的全连接层影响是按平方级的: 输入从 64 降到 16,接 10 类的权重从 640 降到 160。 如果特征图是 32 乘 32,池化三次之后是 4 乘 4,差 64 倍。 所以池化是控制参数量的主要手段之一。 今天有些网络干脆不用池化,改用「步长 2 的卷积」来降采样——因为卷积至少还能学,而池化不能学。
三、平移不变,但是有范围的
把图整体横向平移,比较「原图的差」和「池化后的差」:
| 平移 | 原图差 | 最大池化后差 | 平均池化后差 |
|---|---|---|---|
| 1 像素 | 14 | 4 | 3.50 |
| 2 像素 | 19 | 7 | 4.75 |
| 3 像素 | 18 | 6 | 4.50 |
| 原图差一直在 14 到 19 之间:同一幅图错开几格,逐像素比就一定有这么多不一样。 | |||
| 池化之后落到 4 到 7,小了一个量级。 | |||
| 为什么不是零?因为块的内容真的变了。 | |||
| 平移 1 像素时,一个块里的 4 个数换掉了一两个,最大值可能还是原来那个; | |||
| 平移 2 像素时,整块的内容全换了,最大值也就变了。 | |||
| 所以「平移不变」的准确说法是:一个窗口之内的移动,输出基本不变;超过窗口就失效。 | |||
| 这句话很重要,很多「为什么我的模型对位移不鲁棒」的问题都出在这里。 |
四、三种池化
最大池化:留最强响应。 最常用,语义也最好解释——「这块区域里出现过这个特征」。 平均池化:留平均强度。 对噪声更平滑,今天多出现在网络末端,或者用来把特征图压成一个数。 全局平均池化:整张图求一个均值,直接得到 1 个数。 它的作用是替代全连接层:一个通道一个数,有多少通道就有多少个数。 这样参数量直接归零,也是今天主流分类网络的标准结尾。
五、池化的反向传播
池化没有参数,但它要「把梯度传回去」。 最大池化的规则是:梯度只回到当时被选中的那个位置,其它位置是零。
前向:m = max(a, b, c, d)
反向:dm 只传给取到最大值的那个位置
其余三个位置的梯度是 0
所以实现时要记下「当时谁最大」,这就是那个 mask。 忘了记就没法反传,这是手写 CNN 时最常见的 bug 之一。 平均池化简单些:梯度平均分给 4 个位置,每个拿四分之一。
六、动手 Demo:每块只留一个数,换来小范围平移不变
这段代码把一张 8 乘 8 的数字图做 2 乘 2 最大池化和平均池化,再把图整体平移 1、2、3 像素, 比较「原图的差」和「池化后的差」。 重点看三处。第一段 8 乘 8 变 4 乘 4 的两张结果;第二段剩 25%;第三段 14/19/18 对 4/7/6。
# -*- coding: utf-8 -*-
import unicodedata
import numpy as np
def wd(s):
return sum(2 if unicodedata.east_asian_width(c) in ('W', 'F') else 1 for c in s)
def pad(s, n):
return s + ' ' * max(0, n - wd(s))
def one_digit(k, size=8):
SEG = {3: 'abgcd', 8: 'abcdefg'}
POS = {'a': (1, 2, 5), 'b': (2, 5, 3), 'c': (5, 5, 3), 'd': (7, 2, 4),
'e': (5, 1, 3), 'f': (2, 1, 3), 'g': (4, 2, 4)}
img = np.zeros((size, size))
for sg in SEG[k]:
r, c0, ln = POS[sg]
if sg in ('b', 'c'):
img[r:r + ln, c0] = 1.0
else:
img[r, c0:c0 + ln] = 1.0
return img
def pool(img, s=2, mode='max'):
h, w = img.shape[0] // s, img.shape[1] // s
out = np.zeros((h, w))
for i in range(h):
for j in range(w):
blk = img[i * s:(i + 1) * s, j * s:(j + 1) * s]
out[i, j] = blk.max() if mode == 'max' else blk.mean()
return out
def shift(img, d):
"""整幅图横向平移 d 个像素,空出来的位置补 0。"""
out = np.zeros_like(img)
if d > 0:
out[:, d:] = img[:, :-d]
else:
out[:, :d] = img[:, -d:]
return out
def show(mat, fmt='%.0f'):
for row in mat:
print(' ' + ' '.join(fmt % v for v in row))
img = one_digit(3)
print('[1] 池化就是「每块只留一个数」')
print(' 2 乘 2 的窗口、步长 2,把 8 乘 8 切成 16 块,每块 4 个像素变 1 个。')
print('')
print(' 原图(数字 3):')
show(img)
print('')
print(' 2 乘 2 最大池化 -> 4 乘 4:')
show(pool(img, 2, 'max'))
print(' 2 乘 2 平均池化 -> 4 乘 4:')
show(pool(img, 2, 'avg'), '%.2f')
print('')
print('[2] 元素个数掉到多少')
print(' 8 乘 8 = %d 个 -> 4 乘 4 = %d 个,剩 %.1f%%'
% (64, 16, 100.0 * 16 / 64))
print(' 再池化一次就是 2 乘 2,只剩 4 个位置。')
print(' 后面的全连接层输入从 64 降到 16,权重数量按平方级缩小。')
print('')
print('[3] 换来的是「挪一点也认得出」')
for d in (1, 2, 3):
moved = shift(img, d)
d_raw = float(np.abs(img - moved).sum())
d_max = float(np.abs(pool(img) - pool(moved)).sum())
d_avg = float(np.abs(pool(img, 2, 'avg') - pool(moved, 2, 'avg')).sum())
print(' 平移 %d 像素:原图差 %.0f,最大池化后差 %.0f,平均池化后差 %.2f'
% (d, d_raw, d_max, d_avg))
print('')
print(' 原图差在 14 到 19 之间:同一幅图错开几格,逐像素比就一定有这么多不一样。')
print(' 池化之后落到 4 到 7:一个窗口之内挪动,块里的最大值往往不变。')
print(' 平移 2 像素比 1 像素更大——正好跨过一整个窗口,块的内容换掉了。')
print(' 所以「平移不变」是有范围的:窗口之内有效,超过窗口就失效。')
print('')
print('[4] 三种池化的选择')
print(' 最大池化:留最强的那一个响应,最常用,也最贴近「这块出现过某个特征」。')
print(' 平均池化:留平均强度,对噪声更平滑,今天多出现在网络的最后一层。')
print(' 全局平均池化:整张特征图求一个均值,直接压成 1 个数,省掉全连接层。')
print(' 这张图做全局平均池化,结果是 %.4f(亮像素占的比例)。' % img.mean())
实跑输出:
--------------------------------------------------------------------
池化层:每块只留一个数,换来小范围平移不变
--------------------------------------------------------------------
[1] 池化就是「每块只留一个数」
2 乘 2 的窗口、步长 2,把 8 乘 8 切成 16 块,每块 4 个像素变 1 个。
原图(数字 3):
0 0 0 0 0 0 0 0
0 0 1 1 1 1 1 0
0 0 0 0 0 1 0 0
0 0 0 0 0 1 0 0
0 0 1 1 1 1 0 0
0 0 0 0 0 1 0 0
0 0 0 0 0 1 0 0
0 0 1 1 1 1 0 0
2 乘 2 最大池化 -> 4 乘 4:
0 1 1 1
0 0 1 0
0 1 1 0
0 1 1 0
2 乘 2 平均池化 -> 4 乘 4:
0.00 0.50 0.50 0.25
0.00 0.00 0.50 0.00
0.00 0.50 0.75 0.00
0.00 0.50 0.75 0.00
[2] 元素个数掉到多少
8 乘 8 = 64 个 -> 4 乘 4 = 16 个,剩 25.0%
再池化一次就是 2 乘 2,只剩 4 个位置。
后面的全连接层输入从 64 降到 16,权重数量按平方级缩小。
[3] 换来的是「挪一点也认得出」
平移 1 像素:原图差 14,最大池化后差 4,平均池化后差 3.50
平移 2 像素:原图差 19,最大池化后差 7,平均池化后差 4.75
平移 3 像素:原图差 18,最大池化后差 6,平均池化后差 4.50
原图差在 14 到 19 之间:同一幅图错开几格,逐像素比就一定有这么多不一样。
池化之后落到 4 到 7:一个窗口之内挪动,块里的最大值往往不变。
平移 2 像素比 1 像素更大——正好跨过一整个窗口,块的内容换掉了。
所以「平移不变」是有范围的:窗口之内有效,超过窗口就失效。
[4] 三种池化的选择
最大池化:留最强的那一个响应,最常用,也最贴近「这块出现过某个特征」。
平均池化:留平均强度,对噪声更平滑,今天多出现在网络的最后一层。
全局平均池化:整张特征图求一个均值,直接压成 1 个数,省掉全连接层。
这张图做全局平均池化,结果是 0.2656(亮像素占的比例)。
第一段两张输出图放一起看,能看出最大池化和平均池化的性格差别。
最大池化把中间那些 0.5、0.25 全抹成了 0 和 1,信息更「硬」;
平均池化保留了灰度,信息更「软」。
第二段那个 25% 要记住:池化是每边减半,总数是四分之一。
这个比例决定了后面全连接层的规模,是搭网络时最先算的数。
第三段是这一节的核心。
原图差 14 到 19,池化后 4 到 7——降了一个量级,但没有归零。
「没有归零」才是对的,因为平移确实改变了内容。
注意平移 2 像素的差(7)比平移 1 像素(4)大。
这不是噪声,是池化窗口的周期:平移刚好一个窗口,块的内容整体换掉。
所以池化的不变性是「窗口内」的,不是「任意」的。
代码里 pool 用了 img.shape[0] // s,所以不能整除的部分会被丢掉。
这是池化的一个副作用:8 乘 8 池化两次正好,7 乘 7 就会丢一行一列。
所以真实网络里的尺寸通常都是精心设计成 2 的幂。
30 秒实验
- 把池化窗口从 2 改成 4,重跑。8 乘 8 直接变 2 乘 2,只剩 4 个数。 平移 1、2、3 像素的差会更小,但信息也丢得更多——这是同一件事的两面。
- 把
shift换成「循环平移」(np.roll),重跑。原图差会变小,因为移出去的像素又绕回来了。 真实数据不会循环,所以用补零更接近实际。 - 把最大池化换成平均池化,重跑第三段。平均池化的差更小(3.50 对 4)。 因为它对块内所有位置一视同仁,单个位置变化的影响被摊薄了。
常见误区
- 「池化能让模型完全不受平移影响」。 只能在一个窗口之内。实测平移 1 像素差 4,平移 2 像素差 7。 超过窗口大小,池化就无能为力了。
- 「池化层也有参数,只是比较少」。 池化一个参数都没有。 它不增加模型容量,所以也不可能「学出更好的池化方式」。
- 「最大池化一定比平均池化好」。 最大池化适合「有没有这个特征」,平均池化适合「这个特征有多强」。 分类任务多用最大,网络末端多用平均。
- 「用了池化就不用管尺寸了」。 池化会把不能整除的部分丢掉。 7 乘 7 池化 2 乘 2 之后是 3 乘 3,最后一行一列直接消失。
自测三问(点开对答案)
1. 2 乘 2 池化把 8 乘 8 变成多大?元素剩多少? —— 变成 4 乘 4,元素从 64 个剩到 16 个,也就是 25%。再池化一次是 2 乘 2,只剩 4 个。 后面的全连接层输入会按这个比例缩小。 2. 池化的「平移不变」有多大范围? —— 只有一个池化窗口之内。实测平移 1 像素,池化后的差从 14 降到 4; 平移 2 像素(正好一个窗口),差又涨到 7。 所以准确的说是「小范围平移不变」,不是「完全不受平移影响」。 3. 最大池化的反向传播怎么传梯度? —— 只传给「当时取到最大值」的那个位置,其余三个位置是零。 所以前向时必须记下谁最大(mask),忘了记就没法反传。平均池化则把梯度平均分给块内每个位置。卷积找特征、池化降尺寸,剩下的问题是怎么把「一堆特征图」变成一个答案。 这就是全连接层要做的事,也是整个网络里参数最多的地方。
- ☐ (本节完,下一节 3.4 全连接层)