卷积:把「位置无关」写进结构里
这一章讲三件事: 卷积这个操作到底是什么(它能手算);为什么它天生就带第 07 章缺的那两个性质;以及怎么把「卷积 + 池化(把一小块区域压成一个数,降低分辨率)」堆成网络,赢下鸟与飞机。 位置:全书视觉线的枢纽;第 13 章的 3D 版、第 15 章的分割,全是这一章的直系后代。
1. 把上一章的两个要求翻译成数学
上一章结尾,我们对理想结构提了两个要求1:
- 局部性:判断一个像素属于什么图案,只需要看它附近的像素;远处是树是云不重要。
- 平移不变性:同一个图案,出现在图的左上角和右下角,该被同样认出来。
先试试在旧框架里硬凑:全连接就是「每个输出 = 全部输入的加权和」。要局部性,就把远处像素的权重强制设零;要平移不变,就让「位置 (x,y) 看 (x+1,y)」的权重和「位置 (x′,y′) 看 (x′+1,y′)」的权重永远相等——训练时要同步更新这些绑在一起的权重1。理论上可行,工程上是噩梦:稀疏、捆绑、自定义更新规则。
卷积就是把这两件事一次性做对的现成操作。 而且它还附带第三个礼物:参数量和图像尺寸脱钩——参数只取决于那个小权重块的大小1。
2. 卷积是什么:手算一遍就会了
卷积核(kernel)就是一小块权重,比如 3×3。卷积(convolution)就是:把这块小权重扣在图的每个 3×3 邻域上,对应位置相乘再求和,得到一个输出像素;滑到下一个位置,再算一个2。
用书里那个极简例子手算。输入是 4×4 的单通道图,中间有个十字;卷积核是:
kernel = [[0, 1, 0],
[1, 1, 1],
[0, 1, 0]] ← 一个「十字检测器」
输入(局部) 核扣在左上角 3×3 上:
0 1 0 0 0×0 + 1×1 + 0×0
1 1 1 0 1×0 + 1×1 + 1×1
0 1 0 0 0×0 + 1×1 + 0×0
0 0 0 0 = 5 → 输出左上角是 5
图说:这个 5 的含义是「这个位置和十字图案的吻合度」。
核滑完整个图,就得到一张「哪里像十字」的热度图。
三个性质立刻从这副样子里长出来2:
- 局部性:输出一个像素只看输入的 3×3 邻域;
- 平移不变:同一组权重滑遍全图——「十字」在哪个位置都能点亮;
- 参数少:9 个数管整张图,不管图是 32×32 还是 4000×3000。
一个精确的对照值得记住:卷积等价于一个「绝大多数权重为零、其余权重彼此捆绑相等」的全连接层2。所以它不是新物种,是全连接加了两条正确的约束。约束不是限制表达能力,是把「不用学的东西」从参数里删掉了。
一个术语提醒:严格数学意义的卷积会把核翻转;PyTorch 的不翻——较真地说它算「互相关」,但全行业都这么叫2。
3. 两个手工核:模糊与找边
网络里的核是随机初始化、被反向传播学出来的。但先把权重手工设成已知值,能立刻看清卷积在干什么3:
| 手工核 | 效果 | 为什么 |
|---|---|---|
| 全部 1/9 | 图像变模糊 | 每个输出像素 = 邻域 9 个的平均,相邻输出变得相似 |
[[-1,0,1],[-1,0,1],[-1,0,1]] | 竖直边缘变亮 | 输出 = 右三列之和 − 左三列之和;左右亮度突变处数值大,均匀处为零 |
第二个核手算一个:对像素 (2,2),输出 = i₁₃−i₁₁ + i₂₃−i₂₁ + i₃₃−i₃₁——右边三格减左边三格3。
历史上,计算机视觉(让机器处理和理解图像的这个领域)专家的手艺就是设计这样的滤波器组合;深度学习的回答是:别设计了,让核从数据里学——卷积网络训练的目标就是「学出一组组核,让它们把图变成对任务有用的特征图」3。
4. 边角、padding 与「看得更远」
两个工程细节,一次说清。
边界。 3×3 核扣在角上时,左上没邻居。默认做法是不算——输出每边小一圈(32→30)。要保住尺寸,就在图外圈一圈假像素(padding=1,补零),输出回到 32×324。
这里先记一笔账,第 09 章会来兑现。那一章会出现一种连法,叫残差连接:把一块网络的输入原封不动加到它的输出上,给信号留一条不经过这块计算的近路。既然要把前后两个张量逐位置相加,两者的尺寸就必须一模一样——32×32 加 30×30 是加不起来的。于是「padding 保住尺寸」从一个可选的细节,变成了那种结构的硬性前提。
看得更远。 3×3 的核只能看 3 个像素宽的东 西;鸟有翅膀,跨好几像素,怎么办?用大核?32×32 的核就退化回全连接了。正解是池化 + 堆叠5:
- 池化(pooling):把图缩小。最常用的是最大池化(max pooling):每 2×2 取最大值,图减半。「最大」的用意:卷积+激活的输出在「检测到特征」的位置数值高,取最大保证强响应活下来5。
- 堆叠:第一层卷积看 3×3;池化减半后,第二层卷积的 3×3 作用在半尺寸图上,折回原图就是 6×6——层数越深,每个输出像素「看得到」的输入区域越大。这个区域叫感受野(receptive field)。
书里算了一笔精确的账:conv(3×3) → maxpool(2×2) → conv(3×3),第二个卷积输出的一个像素,受输入 8×8 区域影响5。小核 + 池化 + 堆叠 = 用小参数看大图案,这是卷积网络的底层循环。
5. 主走查:鸟图穿过整个网络
现在把零件装成整网,跟同一只鸟图走完一遍(第 07 章那只)6:
输入 [1, 3, 32, 32] ← 1 张图、3 通道、32×32
Conv2d(3→16, 3×3, padding=1) → [1, 16, 32, 32] 16 组「特征热度图」
Tanh → 形状不变 过敏感区弯折
MaxPool2d(2) → [1, 16, 16, 16] 缩一半
Conv2d(16→8, 3×3, padding=1) → [1, 8, 16, 16] 在「特征」上再找特征
Tanh → MaxPool2d(2) → [1, 8, 8, 8]
view(拉直) → [1, 512]
Linear(512→32) → Tanh → Linear(32→2) → [1, 2] 两个 logits
图说:前半段把「图」变成「特征」,后半段把「特征」变成「答案」。
通道数一路减(16→8)、图尺寸一路缩(32→16→8),是分类网的典型收束。
先数参数:[432, 16, 1152, 8, 16384, 32, 64, 2],合计 180906。对照第 07 章:全连接 374 万。砍掉 99% 的参数,来自两处:卷积层参数只跟核大小和通道数走(432=16×3×3×3+16);第一层 Linear 的输入从 3072 缩到了 512。
训练照旧(第 05 章那台机器,CrossEntropyLoss、SGD、100 轮、批量 64),结果7:
训练集准确率 95%
验证集准确率 90% ← 全连接网是 81%;错误率 (100−90)/(100−81) ≈ 几乎减半
书里把这一步的意义写成了一句加粗的话:模型结构比参数数量重要7。374 万参数输给 1.8 万参数,不是参数输了,是「结构里没有位置」输给了「结构里写死了局部性和平移不变」。
6. 两件后勤事:存模型、上 GPU
网络类定型了,两件天天要用的后勤8:
- 存模型:
torch.save(model.state_dict(), path)只存参数(一本「名字→张量」的字典),不存结构;读回来要先造一个同样的 Net 实例再load_state_dict。参数和结构分家存,意味着改类定义后旧存档就废了。 - 上 GPU:
model.to(device)把模型所有参数搬进显存(注意:Module.to是原地改,Tensor.to是返回新张量——这对不对称是个常见坑);数据在训练循环里逐批.to(device)。先搬模型、后建优化器,否则优化器拿着的是 CPU 上的旧参数对象8。
7. 作者的判断与证据
| 说法 | 性质 |
|---|---|
| 卷积 = 稀疏 + 权重共享的全连接 | 有据,书里拿一张 4×4 的图把两种写法逐格对齐画了出来2 |
| conv+pool+conv 的感受野 = 8×8 | 有据,逐层倒推(第二层 3×3 ← 池化前 6×6 ← 输入 8×8)5 |
| 18090 参数、95%/90% | 有据,可复现运行67 |
| 「最大池化保留强特征,代价是丢掉四分之三数据」 | 设计动机解释;池化各家有争议(平均池化失宠、strided conv 有潜力未普及),书里照实列了几种替代方案5 |
| 「很多核训完其实没用」 | 书里引「彩票假设」(Frankle & Carbin 2019)做的注——是研究假说,不是定论9 |
8. 边界与局限
- 本章全是 2D 图像;视频、CT 走
Conv3d,音频走Conv1d,思想不变。 - 池化的「丢掉 3/4 数据」是真代价;现代很多架构用带步长的卷积代替池化,书里点了但没展开5。
- 90% 的验证准确率对 Jane 够用,对医学不够;第 12–15 章会看到,真正的项目瓶颈常常不在模型。
- 卷积解决「挪位置」,不解决「没见过的东西」:第 09 章结尾 Fred 猫的故事等着。
9. 可带走的
- 卷积 = 小权重块滑过全图、逐位置做同一个加权求和;一次操作买到局部性、平移不变、参数与图像尺寸脱钩。
- 卷积核是被学的,不是被设计的;手工核(模糊、边缘)只是演示工具。
- padding=1 保尺寸,默认不补会每边小 kernel//2。
- 池化缩图、堆叠扩感受野:两层 3×3 加一个 2×2 池化,感受野就到 8×8。
- 分类网的典型形状:通道先升后降、分辨率一路缩、最后拉直进线性层。
- 18090 打赢 374 万:结构 > 参数量。 约束写进结构,等于把「不用学的东西」从参数里删掉。
state_dict只存参数不存结构;Module.to原地、Tensor.to返回新;先搬模型再建优化器。
10. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 两个要求与硬凑的麻烦 | ch8 | text/16-ch08-8-using-convolutions-to-generalize.txt:48(搜「immediate neighbors」) · :63(搜「tied weights」) |
| 卷积定义与手算 | ch8 | text/16-ch08-8-using-convolutions-to-generalize.txt:126(搜「scalar product」) · :90(搜「cross-correlations」) |
| 三大收益 | ch8 | text/16-ch08-8-using-convolutions-to-generalize.txt:140(搜「Local operations」) · :146(搜「size of the convolution kernel」) |
| 等价于稀疏+共享的全连接 | ch8 | text/16-ch08-8-using-convolutions-to-generalize.txt:169(搜「sparse」) · :158(搜「THESE ARE EQUAL」) |
| 手工核:模糊与边缘 | ch8 | text/16-ch08-8-using-convolutions-to-generalize.txt:350(搜「1.0 / 9.0」) · :393(搜「i13 - i11」) |
| 滤波器设计是旧手艺 | ch8 | text/16-ch08-8-using-convolutions-to-generalize.txt:405(搜「combination of filters」) |
| padding | ch8 | text/16-ch08-8-using-convolutions-to-generalize.txt:265(搜「Padding the boundary」) · :275(搜「ghost pix」) |
| 池化三种方案 | ch8 | text/16-ch08-8-using-convolutions-to-generalize.txt:488(搜「Average the four pixels」) |
| 感受野 8×8 | ch8 | text/16-ch08-8-using-convolutions-to-generalize.txt:577(搜「receptive field of 8 × 8」) |
| 整网结构与参数 18090 | ch8 | text/16-ch08-8-using-convolutions-to-generalize.txt:201(搜「Conv2d(3, 16」) · :646(搜「18090」) |
| 95%/90% 与「结构>参数」 | ch8 | text/16-ch08-8-using-convolutions-to-generalize.txt:1043(搜「Accuracy train: 0.95」) · :1057(搜「architecture matters more than parameter count」) |
| Sequential 缺 reshape 报错 | ch8 | text/16-ch08-8-using-convolutions-to-generalize.txt:671(搜「mat1 and mat2」) |
| 存模型与 GPU | ch8 | text/16-ch08-8-using-convolutions-to-generalize.txt:1067(搜「state_dict」) · :1095(搜「modified in place」) · :1114(搜「Optimizer after moving the parameters」) |
| 彩票假设注 | ch8 | text/16-ch08-8-using-convolutions-to-generalize.txt:188(搜「lottery ticket hypothesis」) |