📗 第3章 卷积神经网络
📄 一句话概括

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 秒实验

  1. 把池化窗口从 2 改成 4,重跑。8 乘 8 直接变 2 乘 2,只剩 4 个数。 平移 1、2、3 像素的差会更小,但信息也丢得更多——这是同一件事的两面。
  2. 把 shift 换成「循环平移」(np.roll),重跑。原图差会变小,因为移出去的像素又绕回来了。 真实数据不会循环,所以用补零更接近实际。
  3. 把最大池化换成平均池化,重跑第三段。平均池化的差更小(3.50 对 4)。 因为它对块内所有位置一视同仁,单个位置变化的影响被摊薄了。

常见误区

  1. 「池化能让模型完全不受平移影响」。 只能在一个窗口之内。实测平移 1 像素差 4,平移 2 像素差 7。 超过窗口大小,池化就无能为力了。
  2. 「池化层也有参数,只是比较少」。 池化一个参数都没有。 它不增加模型容量,所以也不可能「学出更好的池化方式」。
  3. 「最大池化一定比平均池化好」。 最大池化适合「有没有这个特征」,平均池化适合「这个特征有多强」。 分类任务多用最大,网络末端多用平均。
  4. 「用了池化就不用管尺寸了」。 池化会把不能整除的部分丢掉。 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),忘了记就没法反传。平均池化则把梯度平均分给块内每个位置。

卷积找特征、池化降尺寸,剩下的问题是怎么把「一堆特征图」变成一个答案。 这就是全连接层要做的事,也是整个网络里参数最多的地方。

相关笔记