chapter 04 / cnn-vision · 预计学习时间 120 分钟
把第 3 章的 MLP 直接用在图像上试试:一张 224×224 的 RGB 图展平后是 150,528 维。第一个隐藏层哪怕只要 1000 个单元,参数就是 1.5 亿——只是一层。更糟的是结构性的浪费:
解决思路是本章的灵魂概念:归纳偏置(inductive bias)——把我们对数据的先验知识直接焊进网络结构,让参数不必浪费在重新发现「图像是平移规律的、局部相关的」这种常识上。
卷积层用两条结构约束实现这个先验:
$$ (I * K)(i, j) = \sum_{m}\sum_{n} I(i+m,\, j+n)\, K(m, n) $$一个小窗口(卷积核,如 3×3 的 9 个权重)在图像上滑动,每个位置算一次加权和。两条约束的含义:
关键认知:卷积核的 9 个数字就是可学习的权重 $\theta$。训练前它是随机噪声,训练后它「长成」边缘检测器、纹理检测器……没人手工设计它们——这正是与传统图像处理(人工设计 Sobel 算子)的分水岭。下面你将亲手扮演一次「训练好的卷积核」,体会一个核能看见什么、看不见什么。
必做实验:① Sobel-X 与 Sobel-Y 对比——看竖直边缘和水平边缘如何被分别「点亮」(方块的四条边只亮两条);② 渐变带的沉默——平滑变化的区域边缘检测输出接近 0,卷积检测的是「突变」不是「亮度」;③ 把视图切到 ReLU——这就是真实 CNN 里下一层收到的东西:负响应被砍掉,只留「检测到了」的信号;④ 手动改数字——把 9 个权重清零只留中心为 1(恒等核),再随机乱填,体会「未训练的核」输出的是无意义噪声。
中间的 3×3 就是卷积核的 9 个权重,直接编辑数字,或用预设。右侧 feature map 用磷光绿表示激活强度(它是神经元的兴奋程度,不是照片)。
$N$ 输入尺寸、$F$ 核尺寸、$P$ 补零圈数(padding)、$S$ 步幅(stride)。两个常用配置背下来:3×3 核 + $P{=}1$ + $S{=}1$ → 尺寸不变;$S{=}2$ → 尺寸减半(现代架构常用它取代池化做下采样)。
一个核只能找一种模式,所以每层并行学几十上百个核。输入 $C_{in}$ 通道时,每个核实际是 $F \times F \times C_{in}$ 的小立方体,输出堆成 $C_{out}$ 个 feature map。参数量 $= F^2 C_{in} C_{out} + C_{out}$。RGB 图第一层 64 核:3×3×3×64+64 = 1792。
2×2 最大池化把尺寸减半:保留「这附近有没有检测到」、扔掉「精确在哪」——花小代价买局部平移不变性。更重要的副产品是感受野的扩张:堆叠 3×3 卷积,第二层的每个神经元间接看到 5×5、第三层 7×7……配合下采样,深层神经元的感受野覆盖大半张图。层级特征由此涌现:浅层学边缘 → 中层把边缘拼成纹理和部件(眼睛、轮子)→ 深层把部件拼成物体。第 1 章视频里 3B1B 画的「层层抽象」假想图,在 CNN 里是真实可视化的(Zeiler & Fergus 2014)。
| 架构(年) | 层数 | 顿悟 |
|---|---|---|
| LeNet-5(1998) | 5 | 卷积+池化+全连接的基本配方成立,读了美国 10% 的支票 |
| AlexNet(2012) | 8 | 同样的配方 × GPU × 大数据就能引爆;ReLU、Dropout 加持(序章讲过这一刻) |
| VGG(2014) | 19 | 架构可以极简:从头到尾只用 3×3,深度即正义 |
| GoogLeNet(2014) | 22 | 并联多尺度分支(Inception),1×1 卷积做通道压缩省算力 |
| ResNet(2015) | 152 | 残差连接打破深度墙,错误率 3.6% 首超人类(5.1%)——详见下节 |
VGG 之后大家自然地继续堆深,然后撞墙了:56 层的普通网络训练误差反而比 20 层高。注意不是过拟合(训练误差都变差了),是优化失败——理论上 56 层至少能把多出的 36 层学成恒等映射来追平 20 层,但梯度下降找不到这个解。深度的收益被优化的困难吃掉了。
何恺明的解法是给优化「修路」。普通块要求层直接学目标映射 $H(x)$;残差块改为学修正量 $F(x) = H(x) - x$,输出:
$$ y = F(x) + x $$两个立竿见影的效果:
x = x + attn(norm(x))),GPT 本质上是一摞残差块。「LLM 是 ResNet 的精神后裔」并不夸张。配方的另一半是 BatchNorm(第 3 章已推导):卷积后、激活前,conv→BN→ReLU 三件套。2020 年的 Vision Transformer 提出了挑衅性的问题:卷积的归纳偏置是必需品还是拐杖?做法暴力而优雅:把图像切成 16×16 的 patch,每个 patch 展平后线性投影成一个向量——把 patch 当作「视觉单词」,喂给标准 Transformer(第 6 章的主角),让自注意力自己学习 patch 之间的关系。
结果完美呼应第 1 章的偏差-方差框架:中小数据集上 ViT 输给 CNN(没有平移先验,方差大);数据量到亿级后 ViT 反超——数据足够时,模型能自己学出比人工先验更好的「先验」,硬编码的偏置反而成了天花板。这也是《苦涩的教训》(附录信息源里 Sutton 那篇两页雄文)的又一次应验:通用方法 + 更多算力数据,长期总是赢过人工先验。2026 年的格局:纯视觉任务 CNN(含 ConvNeXt)与 ViT 并存,但多模态 LLM 的视觉编码器几乎清一色 ViT 系——因为它和语言塔共享同一种架构语言。
import torch, torch.nn as nn
class ResidualBlock(nn.Module):
"""conv→BN→ReLU→conv→BN,再加上 shortcut,最后一起 ReLU"""
def __init__(self, ch_in, ch_out, stride=1):
super().__init__()
self.conv1 = nn.Conv2d(ch_in, ch_out, 3, stride, padding=1, bias=False)
self.bn1 = nn.BatchNorm2d(ch_out) # BN 自带偏置,conv 可省 bias
self.conv2 = nn.Conv2d(ch_out, ch_out, 3, 1, padding=1, bias=False)
self.bn2 = nn.BatchNorm2d(ch_out)
# 尺寸/通道变化时,捷径用 1×1 卷积对齐形状,否则恒等
self.shortcut = nn.Identity()
if stride != 1 or ch_in != ch_out:
self.shortcut = nn.Sequential(
nn.Conv2d(ch_in, ch_out, 1, stride, bias=False),
nn.BatchNorm2d(ch_out))
def forward(self, x):
out = torch.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
return torch.relu(out + self.shortcut(x)) # ← y = F(x) + x,全章的灵魂在这一行
# 验证输出尺寸公式:O = (N + 2P - F)/S + 1
x = torch.randn(1, 64, 56, 56)
print(ResidualBlock(64, 64)(x).shape) # → (1, 64, 56, 56) 尺寸不变
print(ResidualBlock(64, 128, stride=2)(x).shape) # → (1, 128, 28, 28) 减半且升通道