卷积网络
这一章开始,我们给神经网络"换零件"。前面几章学的全连接网络——每个输入连到每个神经元——处理图像时既笨重又浪费。本章要介绍卷积网络(CNN),它是现代计算机视觉的基石:人脸解锁、自动驾驶、拍照识物,背后都有它。学完这一章,你会明白为什么图像不能用全连接网络,卷积核怎么像"小探照灯"一样在图像上滑动,以及填充、步长、池化、通道这些名词到底是什么意思。
为什么图像不能用全连接网络?
图像处理对全连接网络来说,有三个绕不开的大麻烦。第一,参数多到爆炸。一张普通的 224×224 彩色照片有 224×224×3 ≈ 15 万个像素值,也就是 15 万个输入。全连接层里每个输入都要连到每个隐藏单元,权重数大约是输入的平方量级——15 万的平方是 220 亿,而这还只是第一层!要喂饱这么多参数,数据量和算力都是天文数字。
第二,它无视"相邻"关系。图像里挨着的像素高度相关:一片蓝天是连成一片的蓝。可全连接网络把所有输入一视同仁,根本不知道哪两个像素靠得近。有人做过实验:把像素按同样顺序随机打乱,全连接网络照样能训练,性能几乎不变——说明它压根没利用空间结构,只是在硬背像素位置。
第三,它没有平移不变性。把一棵树的照片往左平移几个像素,它还是一棵树,理想情况下网络应该认出"还是那棵树"。这叫平移不变性,写成公式就是 $f[t[x]] = f[x]$,其中 $t$ 是平移变换。这个式子的意思是:先平移再识别,和直接识别,结果应该一样。可全连接网络里每个像素都有专属权重,图一平移输入就全变了,它不得不在每个位置重新学一遍"树长什么样"。
卷积核:滑动的"小探照灯"
卷积网络的核心构件叫卷积核(也叫滤波器)。先从一维说起,把直觉建立起来。假设输入是一串数 $x_1, x_2, \ldots, x_D$,核里有三个权重 $\omega_1, \omega_2, \omega_3$。对每个位置 $i$,输出是这样算的:
$$z_i = \omega_1 x_{i-1} + \omega_2 x_i + \omega_3 x_{i+1}$$这个式子的意思是:把当前位置和左右两个邻居,分别乘上三个权重再相加。这个"加权求和"会在每个位置重复做,而且权重始终是同一组,这叫做权重共享。
卷积核就像一盏"三格宽的探照灯",贴着这排数字从左扫到右,每照亮三个数,就按同一张配方算出一个加权和。配方永远不变,动的只是灯的位置。扫完一遍,你得到一串新数字,每个数字都记录着"这个局部模式"在那里出现的强度。
到了图像上,核变成二维的 3×3 小方块,有 9 个权重 $\omega_{mn}$。它像探照灯一样在图像上从上到下、从左到右滑动:每停一个位置,把灯下的 3×3 共 9 个像素和核的 9 个权重逐位相乘再求和,加上偏置 $\beta$,再过一道激活函数 $a[\cdot]$,得到输出 $h_{ij}$:
$$h_{ij} = a\left[\beta + \sum_{m=1}^{3}\sum_{n=1}^{3} \omega_{mn}\, x_{i+m-2,\, j+n-2}\right]$$这个式子看着吓人,其实还是一句话:输出图上每个点,都是原图对应位置 3×3 小块的加权和,再加点非线性。核的取值不同,功能就不同——有的核"亮"在竖直边缘上,有的核"亮"在水平边缘上。核里到底填什么数字,不用人设计,训练时自动学出来。
import numpy as np
def conv2d(image, kernel, pad=1):
"""手写 3x3 卷积:滑动窗口加权求和(零填充)"""
H, W = image.shape
K = kernel.shape[0]
padded = np.pad(image, pad) # 四周补零 = padding
out = np.zeros((H, W))
for i in range(H):
for j in range(W):
patch = padded[i:i+K, j:j+K] # 探照灯照到的 3x3 小块
out[i, j] = (patch * kernel).sum() # 加权求和
return out
# 一张 8x8 图:左半暗、右半亮
image = np.zeros((8, 8))
image[:, 4:] = 1.0
# 竖直边缘检测核(Sobel 风格)
sobel_x = np.array([[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]])
print(conv2d(image, sobel_x)) # 竖直接缝附近输出最大 → 边缘被"点亮"
这段代码就是手写的"探照灯"。在真正的框架里一行就够,比如 PyTorch 的 nn.Conv2d(3, 16, kernel_size=3, padding=1),表示"输入 3 个通道、输出 16 个通道、3×3 核、零填充"。
填充与步长:边缘怎么办、步子迈多大
滑动过程中有两个实际问题。第一个:边缘怎么办? 探照灯滑到最左上角时,灯下只有 4 个真像素,其余 5 个位置悬空。最常用的解法是零填充:在图像四周补一圈 0,假装外面有像素,这样输出和输入一样大。也可以反过来用"有效"卷积——悬空的地方干脆不算,代价是输出会变小一圈。
第二个:步子迈多大? 默认每滑 1 个像素算一次(步长为 1)。你也可以一步滑 2 个像素(步长为 2),输出大约减半——边扫边压缩。步长越大,图缩得越快,但细节丢得越多。核的大小也能调:核越大,一次看得越远,但参数也越多。
多个核=多个通道,池化=压缩尺寸
一个核只能检测一种特征。想同时看竖直边缘、水平边缘、角点、纹理,就并行放多个核:每个核滑完整张图,生成一张特征图,也叫一个通道。于是第一个卷积层之后,图像变成了好多张特征图叠在一起,通道数等于核的个数。
如果输入本身有 $C_i$ 个通道,核也跟着变厚,尺寸变成 $C_i \times 3 \times 3$:算每个输出点时,要把所有输入通道、所有核位置一起加权求和。要生成 $C_o$ 个输出通道,权重总数就是 $C_i \times C_o \times 3 \times 3$。这个式子告诉我们:权重只跟核和通道数有关,跟图像大小无关——图再大参数也不涨,这正是卷积网络能处理大图的底气。
特征图太大时还要池化。最常用的是最大池化:把特征图分成一个个 2×2 的小块,每块只保留最大值。图立刻缩小一半,但"最强烈的响应"被保留了下来。
池化就像把照片缩成马赛克:每个 2×2 的格子只留下最亮的那颗像素。图变小了,但"哪里有什么、有多强"的信息基本保住。另一种平均池化则是取格子的平均值,思路一样。
池化还有个隐藏好处——对小幅平移更不敏感:物体挪动一个像素,2×2 格子里的最大值往往不变,输出也就基本不动。这正是前面说的平移不变性,虽然只是"部分"的。
层层组合:从边缘到物体部件
单个卷积层只能看到 3×3 的局部,但把卷积层堆起来,网络就"越看越远":第一层每个单元直接看原图的 3×3;第二层的单元综合第一层相邻 3 个位置,等于间接看到了原图 5×5;第三层再往上叠,看到 7×7……这个"间接可见范围"叫感受野。层数越深,感受野越大。
于是 CNN 自然地形成一条特征阶梯:浅层检测边缘、色块,中层拼出纹理、角点,深层组合成眼睛、轮子这样的部件,再往上就是"猫""车"这些整体概念。所有规则都不用人工设计——训练时梯度下降自动调好每一层"该看什么"。
参数差距有多大?书里在 MNIST-1D 数据上做了对比:一个三层卷积网络只要 2,050 个参数,同样规模的全连接网络却要 59,065 个,差了近 29 倍。两者都能把训练数据拟合得很好,但卷积网络测试误差约 17%,全连接却高达约 40%。原因就在于卷积结构自带先验知识:它认定"特征在任何位置都长一样",这个约束就像给模型一本交通规则,让它在没见过的数据上不容易跑偏。