Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

卷积网络与视觉表示

把一张 8×88\times8 的灰度图展开,就得到 64 个数字。上一章的全连接网络已经能接收它们。那为什么还需要卷积网络?

因为图像中的位置有关系。相邻像素可能组成一条边,一条边平移几格后仍是一条边。全连接层可以从数据学习这些关系,卷积层则把“局部观察、重复使用同一套规则”的想法直接写进结构。

1. 拿一个小窗口,在图上移动

先看一张很简单的图:每行都是 [0,0,1,1,1][0,0,1,1,1],一共五行。左侧暗、右侧亮,中间有一条竖直边界。

用下面的 3×33\times3 窗口去观察它:

K=[−101−101−101] K= \begin{bmatrix} -1&0&1\\ -1&0&1\\ -1&0&1 \end{bmatrix}

这个窗口叫卷积核。放到图像左上角,把覆盖的像素与核对应相乘,再全部相加,得到 3。向右移动一格,结果仍是 3;再移动一格,窗口里左右一样亮,结果变成 0。

图 1:核在左右亮度有差异的位置响应较大。这里的核是手工指定的,训练卷积网络时通常由数据学习。

对单输入、单输出通道,步长为 1、没有填充时:

Yi,j=∑u=0kh−1∑v=0kw−1Ku,vXi+u,j+v+b Y_{i,j}=\sum_{u=0}^{k_h-1} \sum_{v=0}^{k_w-1} K_{u,v}X_{i+u,j+v}+b

XX 是输入,YY 是输出特征图,kh,kwk_h,k_w 是核的高与宽,bb 是偏置。每个输出位置只看输入的一小块。

严格说,这个不翻转核的滑动点积在数学上叫互相关。深度学习框架通常沿用“卷积”的名称;本章实现与常用 Conv2d 接口一致。核可以学习时,是否翻转不改变这类结构能够表达的核集合,但对一个手工指定的核,计算约定要一致。

2. 同一组参数,为什么能在不同位置使用

如果每个位置都各学一套边缘规则,参数会很多,也可能只在见过的位置有效。卷积把同一套核用于所有位置,这叫参数共享。

图像平移时,在合适的边界条件下,步长为 1 的卷积响应也会相应平移,这叫平移等变。它不是“平移以后输出完全不变”:边缘从左边移动到右边,响应的位置也应移动。

如果希望分类结果对小幅移动较稳定,可以继续组合池化、空间汇总和数据增强。填充方式、下采样及边界裁剪都会影响这种性质,不能把“使用了卷积”直接等同于任意平移不变。

这一结构带入了一种先验:局部模式值得关注,相似模式在不同位置可能有相似作用。它适合很多视觉问题,却并非所有任务都适合。比如页面标题的位置本身可能有意义,完全忽略位置反而会丢掉信息。

3. 通道、步长与输出大小

灰度图可以看作一个通道;彩色图通常有三个颜色通道。一个输出通道的核会同时覆盖所有输入通道,将它们的贡献相加。多个输出通道可以学习不同模式。

若有 CinC_{\mathrm{in}} 个输入通道、CoutC_{\mathrm{out}} 个输出通道,核为 kh×kwk_h\times k_w,普通卷积层的参数数为:

Cout(Cinkhkw+1) C_{\mathrm{out}} \bigl(C_{\mathrm{in}}k_hk_w+1\bigr)

最后的 1 对应每个输出通道的偏置。参数数不随图像位置数量直接增加,但在更大图上移动同一个核,计算量仍会增加。

步长决定每次移动几格。填充在图像周围补上额外位置,例如零值,使边缘也能参与计算,或维持输出大小。

不考虑膨胀卷积时,一个方向的大小为:

Nout=⌊N+2P−KS⌋+1 N_{\mathrm{out}} =\left\lfloor\frac{N+2P-K}{S}\right\rfloor+1

NN 是输入大小,PP 是每侧填充量,KK 是核大小,SS 是步长;向下取整表示只保留完整窗口。本例 N=5,K=3,P=0,S=1N=5,K=3,P=0,S=1,所以输出为 3。若填充 1、步长仍为 1,就能保持大小 5。

PyTorch 图像批次通常采用 (B,C,H,W)(B,C,H,W):批量、通道、高、宽。不要把彩色通道误放到最后一维,却仍按这个约定调用卷积。

4. 池化与逐层扩大观察范围

最大池化从一个局部窗口中取最大值,例如 2×22\times2 窗口、步长 2,将大致相邻的响应合成一个。它没有需要学习的权重,常用于缩小空间尺寸。

池化会丢失精确位置。一条细线可能在下采样后消失,几个不同图案也可能得到相同汇总结果。因此,缩小特征图既节省计算,也是一种信息取舍。

连续叠加卷积,会让后面的单元间接看见更大的输入区域,这个范围叫感受野。两个步长 1 的 3×33\times3 卷积,在不考虑边界时,可使后层一个位置依赖原图的 5×55\times5 区域。

早层常能学习局部边缘和纹理,后层则可能组合更复杂的模式。但不要把每个通道都机械命名为某个物体部件;具体表示要通过实验检查。

5. 残差连接:保留一条直接通路

网络越深,不代表越容易训练。除了继续改变初始化和优化器,还可以改变信息走过的路径。

残差块写成:

y=x+F(x) \boldsymbol y=\boldsymbol x+F(\boldsymbol x)

一条分支学习变换 FF,另一条直接传递输入。网络可以先把 FF 调得较小,让这一块接近恒等变换,再逐步学习需要的修正。

图 2:相加要求两条分支形状兼容。若通道数或空间大小改变,直接分支也需要相应投影或下采样。

在一维情况下,导数为 dy/dx=1+F′(x)dy/dx=1+F'(x)。多维时对应单位矩阵与变换的雅可比矩阵相加。直接通路有助于信号与梯度传播,但不保证永不消失或爆炸,因为两项可能抵消,其他层也仍会影响结果。

残差不是把旧输出保存下来、训练结束后再平均;它是每次前向计算中的结构,反向传播也会沿两条路径累计梯度。

6. 从窗口计算到可运行代码

示例脚本只使用标准库,演示一个通道、无填充的卷积与池化:

python3 src/examples/convolution.py
"""第 15 章:二维滑动点积、池化与残差;只用标准库。"""


def correlate(image, kernel, stride=1):
    height, width = len(image), len(image[0])
    kh, kw = len(kernel), len(kernel[0])
    return [[sum(image[row+i][col+j]*kernel[i][j]
                 for i in range(kh) for j in range(kw))
             for col in range(0, width-kw+1, stride)]
            for row in range(0, height-kh+1, stride)]


def max_pool(image, size=2, stride=2):
    return [[max(image[row+i][col+j] for i in range(size) for j in range(size))
             for col in range(0, len(image[0])-size+1, stride)]
            for row in range(0, len(image)-size+1, stride)]


def example():
    image = [[0, 0, 1, 1, 1] for _ in range(5)]
    kernel = [[-1, 0, 1] for _ in range(3)]
    return image, kernel, correlate(image, kernel)


def main():
    image, kernel, output = example()
    print("原图的卷积输出:")
    for row in output:
        print(row)
    shifted = [[0] + row[:-1] for row in image]
    print("右移一格后,输出第一行:" + str(correlate(shifted, kernel)[0]))
    print("2×2 最大池化:" + str(max_pool(output)))
    values = [-2, 1]
    print("残差 x+ReLU(x):" + str([x+max(0, x) for x in values]))
    print(f"1 个输入通道、2 个输出通道、3×3 核的参数数:{2*(1*3*3+1)}")


if __name__ == "__main__":
    main()

输出为:

原图的卷积输出:
[3, 3, 0]
[3, 3, 0]
[3, 3, 0]
右移一格后,输出第一行:[0, 3, 3]
2×2 最大池化:[[3]]
残差 x+ReLU(x):[-2, 2]
1 个输入通道、2 个输出通道、3×3 核的参数数:20

右移后的有效区域也发生了边界变化,因此不要把这两个小矩阵读成对任意大小、任意填充都成立的完整平移证明。它展示的是同一个局部模式移动后,响应位置跟着变化。

残差例子中的 F(x)=ReLU⁡(x)F(x)=\operatorname{ReLU}(x) 是为了手算指定的。输入 −2 能沿直接分支保留下来,输入 1 则得到 2。真实残差块中的 FF 通常包含可学习层,块末是否再加激活函数,也会影响最终输出。

第十六章的篇末实践会训练一个真正的图像分类器:卷积提取特征,池化缩小空间,再由全连接层输出十个数字类别的分数。这里先把一次卷积究竟算了什么看清楚。

7. 自测与小实验

  1. 本例核放在全为 1 的区域,为什么响应为 0?
  2. 一个核在图中滑动十个位置,会增加十套参数吗?
  3. 平移等变与平移不变有什么不同?
  4. 输入大小 8、核大小 3、每侧填充 1、步长 2,输出大小是多少?
  5. 输入通道 3、输出通道 8、核为 3×33\times3,带偏置时有多少参数?
  6. 为什么残差相加前需要检查形状?
  7. 最大池化会保留窗口内所有像素的位置吗?
展开参考答案
  1. 每行 −1、0、1 的贡献相消,核检测的是左右差异。
  2. 不会,同一套核被重复使用;计算次数会增加。
  3. 等变是输入变化时输出对应变化;不变是输出保持相同。
  4. 为 ⌊(8+2−3)/2⌋+1=4\lfloor(8+2-3)/2\rfloor+1=4。
  5. 为 8(3×3×3+1)=2248(3\times3\times3+1)=224。
  6. 逐项相加需要对应位置有一致含义,通常要求形状相同或经过明确设计的变换。
  7. 不会,它只保留最大值,丢掉了大量细节和精确位置信息。

选做:将卷积核转置,观察它更关注什么方向。再把图中竖直边界改成水平边界,检查猜测。不要只看总响应大小,也看响应发生在哪些位置。

下一章序列建模与表示学习将把“局部空间关系”换成“随时间到来的信息”。残差网络的原始工作是 He 等人的 Deep Residual Learning for Image Recognition;接口细节见 PyTorch Conv2d 文档。