理论学习:卷积为什么能看见图像
第 3 章最后,模型测试 98.20%,180 张错图里 9→4 有 13 张、7→2 有 10 张。这一章用同一份 MNIST 重训了一个 784→64→10 的 MLP:干净测试 96.04%,把整幅图右移 3 个像素后掉到 45.99%。人眼几乎看不出区别,网络却像换了一套输入。
问题不在优化,而在输入被拉平的那一刻。

<!-- 配图:三张卡片——100,480 个参数、784 维拉平、平移后平均 71.72%;底部对比全连接与卷积的看图方式 -->
一、全连接的三个致命问题
把 28×28 展平成 784 维,看起来只是换了个容器,实际丢了三样东西:参数效率、邻接关系、位置复用。
1. 参数爆炸
一层 784→128 的全连接:
784 × 128 + 128 = 100,480输入换成 224×224,同一层变成 224×224×128 + 128 = 6,422,656。参数量随输入面积平方增长,和图像内容无关。
同样做 1→32 通道、3×3 核的卷积:
(1 × 3 × 3 + 1) × 32 = 32028×28 和 224×224 都是 320 个参数。两者相差 314 倍。
224×224 的 RGB 输入更夸张:全连接 3→64 是 3×224×224×64+64 = 9,633,856,3→64 的 3×3 卷积是 (3×3×3+1)×64 = 1,792,差 5,376 倍。
2. 空间被拉平
Flatten 之后,第 i 个输出只看 x[i],像素的上下左右变成了权重矩阵里的下标。权重可以记住「第 300 个像素通常是亮的」,但记不住「它左边那个像素也亮」。
实测:取一张数字 7,整体右移 2 个像素,784 个像素里有 200 个数值发生变化,与原图的余弦相似度只有 0.4536,L2 距离 24.7288。网络收到的不是「同一个 7 挪了位置」,而是「四分之一的输入变了」。
MLP 因此只能靠数据增强把每个位置都见一遍。没见过的位移,就按错误的权重算。
3. 平移不共享
全连接层给每个输入位置一套独立权重。左上角的 w[0] 只对左上角像素有效,图往右挪一格,这组权重就对不上了。
同一个训练预算(3 轮、50,000 张、Adam 1e−3、batch 128)下,把测试集整幅平移再评估:
| 水平平移 | MLP 784→64→10 | CNN 两层卷积 + 池化 |
|---|---|---|
| −3 px | 53.94% | 85.78% |
| −2 px | 75.37% | 94.84% |
| −1 px | 92.72% | 97.85% |
| 0(干净) | 96.04% | 98.52% |
| +1 px | 91.19% | 98.16% |
| +2 px | 71.13% | 94.91% |
| +3 px | 45.99% | 84.47% |

<!-- 配图:MLP 与 CNN 的水平平移曲线,右侧三张卡片给出干净 96.04/98.52、平均 71.72/92.67、最大下降 24.32/13.74 -->
|d|≥1 的 6 个位置平均,MLP 71.72%,CNN 92.67%。垂直方向更差:MLP 在 −3 px 只剩 39.48%,+3 px 剩 36.21%;CNN 分别是 73.37% 和 66.86%。
注意 CNN 的参数还更少:20,490 对 50,890,少 59.7%。
但卷积不是白送的。同一台机器、4 线程、3 轮 50,000 张:MLP 训练 0.312 秒,CNN 16.775 秒,慢 53.7 倍。计算量也确实更大——MLP 每图 50,816 次乘加,CNN 每图 1,031,744 次,20.3 倍。时间差比计算量差还大,多出来的是卷积实现的调度开销,第 4.3 篇会把它拆到 0.518 毫秒。
所以真正的问题不是「卷积更快」,而是「卷积把参数花在了正确的地方」。
二、局部连接与参数共享
卷积只做两件事,两件都针对上面的病。

<!-- 配图:左图 36 条连线全部指向一个输出,右图只连 3×3 窗口的 9 条;底部给出 784→9 与 1.15% -->
第一件是局部连接:每个输出只看一个 3×3 窗口,9 个输入,而不是全部 784 个。28×28 时,连接数从 784 降到 9,每个输出只用到 1.15% 的输入。
第二件是参数共享:同一个 3×3 核在 784 个输出位置重复使用。

<!-- 配图:三个不同位置的窗口都指向同一个 2×2 核;右侧 288 个权重、加 32 个偏置共 320,复用 784 次 -->
写成公式:
y[o, i, j] = Σ_c Σ_u Σ_v x[c, i+u, j+v] × w[o, c, u, v] + b[o]w 只由 (o, c, u, v) 索引,没有 (i, j)。图挪到哪个位置,用的都是同一组 w。32 个 3×3 核一共 32×9=288 个权重,加 32 个偏置,320 个参数,与输入尺寸无关。
| 结构 | 输入 | 参数量 |
|---|---|---|
| 全连接 784→128 | 28×28 | 100,480 |
| 全连接 501,760→128 | 224×224 | 6,422,656 |
| 卷积 1→32,3×3 | 28×28 | 320 |
| 卷积 1→32,3×3 | 224×224 | 320 |
| 全连接 150,528→64 | 3×224×224 | 9,633,856 |
| 卷积 3→64,3×3 | 3×224×224 | 1,792 |

<!-- 配图:28×28 与 224×224 两组对数柱状图,全连接 100,480 / 6,422,656 对卷积 320;底部注明乘加量反过来是 2.25 倍 -->
设计取舍:卷积层还是全连接层
| 方案 | 收益 | 代价 |
|---|---|---|
| A:局部连接 + 参数共享 | 320 个参数,平移平均 92.67%,参数少 59.7% | 单个输出只看 9 个输入,感受野只有 3×3,必须堆叠;边界被零稀释;训练 16.775 秒,是 MLP 的 53.7 倍 |
| B:全连接 | 一次就能看到整张图,实现简单 | 100,480 个参数随面积增长;平移平均 71.72%;每个位置都要单独学 |
这一章选 A,但不是无条件选 A。如果任务里的目标位置固定、尺寸固定,全连接的「一次看全图」反而省事;只要目标可能出现在任意位置,共享权重就是必须的。
踩坑记录
- 现象:看到 320 对 100,480,判断卷积比全连接快 314 倍;实测同一层的乘加量是 225,792 对 100,352,卷积反而多 2.25 倍。
- 原因:参数共享减少的是「权重个数」,不是「运算次数」。输出有 784 个位置,每个位置都要做
32×9=288次乘加,合计 225,792;全连接每个输出只做 784 次,128 个输出合计 100,352。 - 修复:做算力预算时分开记两个数——参数量看显存与过拟合,MAC 看延迟。真要提速,靠的是把卷积改写成矩阵乘(im2col),第 4.3 篇把 0.7916 秒压到 0.007945 秒。
三、一次卷积在算什么
先把一个窗口拆开。

<!-- 配图:5×5 输入左上角 3×3 窗口与垂直边缘核逐元素相乘,9 个乘积排成一行,求和得 15 -->
输入 5×5、核 3×3、步长 1,左上角窗口的 9 个乘积是:
0×−1 + 1×0 + 5×1
0×−1 + 2×0 + 5×1
0×−1 + 1×0 + 5×1
= 0 + 0 + 5 + 0 + 0 + 5 + 0 + 0 + 5 = 159 次乘法、8 次加法、1 次偏置。9 个乘积里只有 3 个非零,但卷积不会跳过另外 6 个:位置固定,运算次数也固定。核继续向右滑 2 格、向下滑 2 格,得到完整的 3×3 输出:
[[15, 0, −15],
[15, 0, −15],
[15, 0, −15]]竖直亮条经过这个「左负右正」的核,中间响应 15,两侧 0,右边 −15。它检测的是竖直边缘,而不是「亮条在第几列」。
输出尺寸
四个量决定输出大小:输入 H、核 K、补零 P、步长 S,再加上膨胀 D。

<!-- 配图:顶部公式;四张卡片分别演示 valid 5→3、same 5→5、stride 2 5→2、dilation 2 7→3 -->
有效核 = D(K−1) + 1
O = ⌊ (H + 2P − 有效核) / S ⌋ + 1代入几组真实配置:
| H | K | S | P | D | 输出 | 说明 |
|---|---|---|---|---|---|---|
| 28 | 3 | 1 | 1 | 1 | 28 | 默认 same |
| 28 | 3 | 1 | 0 | 1 | 26 | valid,每层缩 2 |
| 28 | 3 | 2 | 0 | 1 | 13 | stride 2 减半 |
| 28 | 5 | 1 | 2 | 1 | 28 | 5×5 也 same |
| 32 | 5 | 2 | 2 | 1 | 16 | stride 2 + pad 2 |
| 28 | 3 | 1 | 0 | 2 | 24 | dilation 2 |
| 32 | 3 | 2 | 1 | 2 | 15 | 两个都用 |
| 10 | 4 | 1 | 1 | 1 | 9 | 偶数核,same 少一格 |
| 224 | 7 | 2 | 3 | 1 | 112 | AlexNet 首层 |
最后两行值得注意。32→15 不是 16:有效核是 5,(32+2−5)/2+1=15。10→9 是偶数核的经典坑:K=4 要让输出仍是 10,两侧各补 1 不够,必须一侧补 2、另一侧补 1。
padding 在补什么

<!-- 配图:5×5 原图补一圈变 7×7,3×3 核走 5 步回到 5×5;下方统计零的占比与边界参与次数 -->
5×5 补一圈变 7×7,其中 24 个零,占 48.98%。28×28 补一圈变 30×30,116 个零,占 12.89%。
补零不是为了「多算」,是为了让边缘像素也能被核的中心覆盖。代价是边界响应被零稀释:3×3 valid 时,角点只参与 1 个窗口,边点 2 个,内部点 4 个。层数一多,边缘信息越来越弱。
stride 与 dilation

<!-- 配图:7×7 上 stride 1 输出 5×5、stride 2 输出 3×3、dilation 2 跨度 5×5 但仍只取 9 个点 -->
stride=2:每两个位置取一次,7×7 变 3×3,输出位置少 75%。省计算,但会漏掉落在采样间隙的细节。dilation=2:核内元素间隔 1 格,3×3 核的有效跨度是 5×5,但只取 9 个点。感受野变大,参数量不变,代价是网格状空洞——相邻像素可能一次都没被采到。
两者可以同时用,32→15 就是 s=2, d=2, p=1 的组合。
四、感受野与堆叠
一个 3×3 核只看 9 个像素。堆三层,最后一层的输出能看到多少?

<!-- 配图:9×9 → 7×7 → 5×5 → 3×3 四层网格,中心 7×7 高亮;下方公式与 27 个权重 -->
r_out = r_in + (K−1) × j_in
j_out = j_in × Sr 是感受野边长,j 是相邻输出在输入上的间距(jump)。三层 3×3、stride 1:
第 1 层:r = 1 + 2×1 = 3
第 2 层:r = 3 + 2×1 = 5
第 3 层:r = 5 + 2×1 = 7不是 3+3+3=9,而是 3→5→7。每层只增加 K−1,但 jump 会随 stride 变大。

<!-- 配图:左侧有效感受野公式与 27/49 的对比;右侧表格列出 3 层 3×3、1 层 7×7、conv-pool-conv、4 层 3×3 的 RF 与参数 -->
| 结构 | 感受野 | 参数量(单通道) | 说明 |
|---|---|---|---|
| 3 层 3×3 | 7×7 | 27 | 3 次激活 |
| 1 层 7×7 | 7×7 | 49 | 参数多 81.5%,少两层非线性 |
| conv3 → pool2 → conv3 | 8×8 | 27 | pool 把 jump 变成 2 |
| 4 层 3×3 | 9×9 | 36 | 每层 +2 |
conv3 → pool2 → conv3 的推导:r=3, j=1 → pool 后 r=3+1=4, j=2 → 再过 3×3,r=4+2×2=8。池化没有参数,却让后面的每层卷积都多涨 2 格。
设计取舍:堆 3×3 还是一次 7×7
| 方案 | 收益 | 代价 |
|---|---|---|
| A:3 层 3×3 | 同样 7×7 感受野,27 个参数,中间夹 3 次非线性 | 3 层串行,前向/反向都多两轮;中间激活要占显存;边界稀释累计 3 次 |
| B:1 层 7×7 | 一层到位,调度简单 | 49 个参数,多 81.5%;只有 1 次非线性,表达能力弱 |
VGG 选 A 的理由不是省参数,是多出来的非线性。两个 3×3 堆叠已经能表达 5×5 的感受野,但中间多了一次 ReLU,能拟合的组合更多。代价是深度换来的训练难度,这正是第 5 章要处理的问题。
特征层级是堆出来的

<!-- 配图:三个阶段——边缘(RF 3×3)、笔画(RF 7×7)、数字(RF 11×11),中间用箭头连接 -->
第 1 层的 3×3 核只能看到一小段亮暗变化,学到的是边缘。把边缘图再卷一次,3 层后感受野 7×7,开始组合出拐角和笔画。5 层后 11×11,局部笔画能拼成数字的一部分。
「CNN 第一层学边缘」不是设计出来的,是局部连接 + 堆叠的必然结果:核只有 3×3,不学边缘就没有别的可学。
五、1×1 卷积与等变性
1×1 卷积不是「没用」

<!-- 配图:H×W×64 经 1×1×64 变成 H×W×32,右侧参数 2,080、乘加 6.42M -->
1×1 核的空间窗口只有 1 个像素,但它跨全部通道:
b[o, i, j] = Σ_c a[c, i, j] × w[o, c] + b[o]每个输出都是同一位置上 64 个通道的加权和。参数量 64×32+32 = 2,080,在 56×56 特征图上乘加 32×56×56×64 = 6,422,528 次。
它的用途是改通道数:3×3 卷积前先用 1×1 把 64 通道降到 32,3×3 的乘加量直接减半;两个 3×3 之间插一个 1×1,可以跨通道混合信息。它不扩大感受野,也不找边缘。
等变不等于不变

<!-- 配图:输入右移一格,卷积特征图也右移一格;底部池化的 15.82% 与精确平移副本 -->
卷积是等变的:输入平移 1 格,特征图也平移 1 格,值不变。因为同一组权重在每个位置都用了一遍。
但分类需要的是不变:数字挪了位置,标签仍是 7。这一步由池化做。2×2 max pool 把 2×2 格子压成一个值,平移 1 px 时 15.82% 的池化格子变化,平均绝对变化 0.1852;平移 2 px(正好一个格子)时,池化图是原图的精确平移副本,最大差 0。
池化只对「步长整数倍」的平移给不变性。挪 1 px 时,池化图仍会变,剩下的鲁棒性靠数据增强和后续层补。平均池化更平滑,平移 1 px 时有 22.96% 的格子变化,比 max pool 的 15.82% 还大——平滑不等于不变。
六、这一章没解决的问题
局部连接、参数共享、堆叠,解决了「看哪里」和「同一模式出现在不同位置怎么办」。它们没有解决「能堆多深」。
把 3×3 卷积一层层加上去,感受野确实在涨,参数量也还可控,但训练误差会在某个深度之后反而变大——不是过拟合,是优化不动。20 层的普通卷积网络,训练误差可能比 19 层更高;这不是本章的猜想,而是第 5 章要拿数据回答的第一个问题。
如果卷积层本身没问题,为什么加深会退化?ResNet 又是用什么办法把网络堆到 152 层还保持可训练?
自检
合上笔记,试着回答:
- 全连接的三个问题分别是什么?各用本章哪个数字支撑?
- 320 个参数为什么对应 225,792 次乘加?这两个数分别该在什么预算里看?
28×28、k=3、s=2、p=1的输出是多少?d=2时有效核多大?- 为什么
10×10的图用k=4, p=1只能得到9×9? - 3 层 3×3 的感受野为什么是 7 而不是 9?pool 把 jump 变成 2 后,RF 怎么算?
- 卷积的等变性和池化的不变性差在哪?平移 1 px 时 max pool 为什么还会变?