📕 第5章 卷积神经网络
📄 一句话概括

卷积就是「核在图上滑,每停一次算一个内积」。3×3 的核,一次参与 9 个权重。 不补零、步幅 1 时,输出边长 = 输入边长 − 核边长 + 1。5×5 配 3×3 得 3×3。 实测一条横线探测器:横线落在窗口中间时输出 +6,落不在时 −3。核就是一个图案匹配器。

📝 本节和前面几节的关系

5.1 讲了为什么要有卷积,这一节讲卷积怎么算。 5.5 的补零、5.6 的步幅,都是在改这一节那个尺寸公式。 内积在第 1 章 1.4 节算过一次(结果是 −5.0),这里是最直接的应用。

一、内积:对应位置相乘再相加

两个同样长的向量,对应位置相乘、再全部加起来。3×3 的核和内积就是九个乘积的和。 它的几何含义是「方向有多一致」: 内积大说明两个向量方向接近,内积为零说明正交,为负说明方向相反。 所以卷积的输出可以读成「这一小块输入和这个核有多像」。这句话是理解后面所有内容的钥匙。

二、核在图上滑

从左上角开始,停一次算一次,往右移一格,到头回左边下一行。 每个输出点的计算完全独立,所以能并行。 3×3 的核在 5×5 的图上能停 3×3 等于 9 次,得到 3×3 的输出。 「完全独立」这件事,是 GPU 在卷积上特别快的原因之一。

三、输出尺寸

不补零、步幅 1 时,输出边长 = 输入边长 − 核边长 + 1。 直观理解:核的左上角能放的位置,横向从第 0 列到第 H−F 列,一共 H−F+1 个。 后面这个式子会加两个修正项:补零加 2p、步幅除以 s。 完整版是 floor((H + 2p − F)/s) + 1,5.6 节会用。

四、互相关与卷积

数学上的卷积要先翻转核(f 星号 g 里那个翻转),深度学习不翻。 所以严格说,深度学习里的「卷积层」做的是互相关。这个名字是历史遗留。 后果只有一条:不需要记翻转,但看论文里的公式时要注意作者的约定。 实际写代码时,几乎所有框架都按不翻转实现。

五、核在找什么

实测:横线探测器在横线正好落在窗口中间时给 +6,落不在时给 −3。 一个核就是一个图案匹配器。训练做的事,就是让这 9 个数自己变成有用的图案。 5.12 节会看到:真实网络第一层的核,学出来确实很像边缘、 颜色斑块这类东西——因为数据和结构逼着它只能学成那样。

六、动手 Demo:一个 5×5 的图,一个 3×3 的核

输入是一张 5×5 的图,只有第 3 行是亮的。核是「中间一行 +2、上下两行各 −1」,一条横线探测器。 把 9 次停靠的内积全部打出来。

import numpy as np

x = np.zeros((5, 5))
x[2, :] = 1.0                     # 第 3 行是一条横线
k = np.array([-1.0, -1.0, -1.0,
              2.0,  2.0,  2.0,
             -1.0, -1.0, -1.0]).reshape(3, 3)

for i in range(3):
    for j in range(3):
        patch = x[i:i + 3, j:j + 3]
        v = float((patch * k).sum())
        print("停 (%d,%d)  内积 %+.0f" % (i, j, v))

实跑输出:

--------------------------------------------------------------------
卷积运算:一个 5×5 的图,一个 3×3 的核
--------------------------------------------------------------------

  输入(5×5,第 3 行是一条横线):
      0.0   0.0   0.0   0.0   0.0
      0.0   0.0   0.0   0.0   0.0
      1.0   1.0   1.0   1.0   1.0
      0.0   0.0   0.0   0.0   0.0
      0.0   0.0   0.0   0.0   0.0
  核(3×3,中间一行 +2、上下两行各 -1,这是一条横线探测器):
     -1.0  -1.0  -1.0
      2.0   2.0   2.0
     -1.0  -1.0  -1.0

  核在图上滑过,每停一次算一个内积:对应位置相乘,再把这九个数全部加起来。

    停 (0,0)  取到 +0 +0 +0 +0 +0 +0 +1 +1 +1  内积 -3
    停 (0,1)  取到 +0 +0 +0 +0 +0 +0 +1 +1 +1  内积 -3
    停 (0,2)  取到 +0 +0 +0 +0 +0 +0 +1 +1 +1  内积 -3
    停 (1,0)  取到 +0 +0 +0 +1 +1 +1 +0 +0 +0  内积 +6
    停 (1,1)  取到 +0 +0 +0 +1 +1 +1 +0 +0 +0  内积 +6
    停 (1,2)  取到 +0 +0 +0 +1 +1 +1 +0 +0 +0  内积 +6
    停 (2,0)  取到 +1 +1 +1 +0 +0 +0 +0 +0 +0  内积 -3
    停 (2,1)  取到 +1 +1 +1 +0 +0 +0 +0 +0 +0  内积 -3
    停 (2,2)  取到 +1 +1 +1 +0 +0 +0 +0 +0 +0  内积 -3

  输出(3×3):
     -3.0  -3.0  -3.0
      6.0   6.0   6.0
     -3.0  -3.0  -3.0

  读法:**横线正好落在中间那一行时,输出是 +6;落不在时就 −3。**
  三列的数一模一样,因为横线是整条贯通的。
  这就是「核在找它想要的图案」:图案对上,内积大;对不上,内积小甚至为负。
  **5-2 节讲卷积运算,要建立的第一个直觉就是这个。**

  尺寸怎么来的:5 − 3 + 1 = 3。
  **核放不进图里的位置就不算,所以每滑一步,输出就比输入小 (核边长 − 1)。**

  还有一处容易搞混:深度学习里的「卷积」其实是数学上的互相关,核没有翻转。
  **书里也照这个约定,所以不用管翻转。**

核的构造说明它在找什么:它希望自己中间那一行对着亮的、上下两行对着暗的。正好是一条横线的样子。 9 次内积里,第 0 行和第 2 行都是 −3,中间一行 +6。横线只在「核的中行正好压住它」时得高分。 输出 3×3 三列一模一样,因为横线是整条贯通的——核在水平方向滑动时,看到的图案没有变化。 尺寸 5 − 3 + 1 = 3,和公式对上。 这行算式以后每写一层都要用,写错一次,后面所有层的形状都会连锁错。 这个例子里核是人工指定的。 训练的时候这 9 个数是从数据里学出来的, 学出来的东西往往和这个横线探测器很像——因为「找横线」确实是第一层该干的事。

30 秒实验

  1. 把核换成竖线探测器(左右两列 −1、中间一列 +2),输出会变成什么样?
  2. 如果横线在第 1 行而不是第 3 行,输出矩阵会怎么变?
  3. 把核里的 +2 改成 +1,输出的数值会变成原来的几分之几?

常见误区

  1. 以为卷积在做「模糊」或「锐化」。那只是特定核的效果。核里是什么数,它就是什么算子。
  2. 把输出尺寸算成「减核边长」。是「减核边长加一」。 差一个 1,会让后面所有层的形状全错,而且报错信息通常看不出根因。
  3. 以为核越大越好。大核参数多、计算多。5.4 节会看到三层 3×3 比一层 7×7 更划算。
自测三问(点开对答案) 1. 卷积的每一步在算什么? —— 一小块输入和核的内积:对应位置相乘,再把结果全部加起来。 所以它衡量的是「这一小块和核有多像」。 2. 5×5 输入配 3×3 核、不补零、步幅 1,输出多大? —— 3×3。公式是 H − F + 1,也就是 5 − 3 + 1 = 3。 3. 深度学习里的卷积和数学上的卷积差在哪? —— 不翻转核,实际做的是互相关。名字沿用「卷积」,但实现里没有那个翻转。

单个二维核只能处理单通道的图。真实的图有 RGB 三个通道,一层还会有几十个核。下一节把它们拼起来。

相关笔记