上一章《超参怎么选》给了「该加数据还是加模型」的判据;可图片这条路,光加模型会撞上参数爆炸, 猫挪一格还得重新认一遍。CNN 的办法是只刻一块 3×3 的小印章,拿它盖满整张图。
MLP 看图的办法,是给每个像素单独拉一根线。 假设它已经学会在左上角认出一个「十字」:那一块的几根线权重大,其余都是 0。 现在把十字往右挪,看它还认不认得。
互动 · 同一个十字,挪一挪位置
十字还是那个十字,可全连接神经元的权重只长在虚线框里。东西一离开那个框,它就看不见了; 想在每个位置都认得,就得在每个位置各学一遍。
另一个问题是参数:一张 224×224 的彩图是 15 万个数字(长 × 宽 × 红绿蓝 3 个颜色),第一层只放 256 个神经元, 就要 3,850 万个参数(第 3 节可以拖着看)。右边那根不动的条,就是这一章的答案:一块 3×3 的小印章。
上一节说的小印章,正式名字叫卷积核(filter / kernel):一块 3×3 的小数字板。把它盖在图像的某个 3×3 区域上, 对应位置相乘再求和,得到一个数——输出的一个像素就是这么来的。 然后窗口往右挪一格,再来一次……滑完整张图,就得到一张特征图。
核心大图 · 卷积到底在算什么
输入是 8×8 的灰度图(0=黑,255=白),3×3 的核滑过以后输出是 6×6 —— 每滑一次就少一圈。 为了看清楚,输出的灰度取的是加权和的绝对值(否则负值会全黑,只能看到一半的边缘)。
把鼠标移到公式里的 I、K、S、i、j、m、n 上,会显示它怎么念、在上面那张图上是哪一块;Σ 的说明在下面那张表里。鼠标不方便就直接读表。
| 符号 | 怎么念 | 一句话 | 在图上是哪一块 |
|---|---|---|---|
I | 输入图像 | 一张二维数字表,每格一个像素 | 演示里左边那张 8×8 |
K | 卷积核(filter) | 一块很小的数字板,值是学出来的,没人设计它 | 标着「卷积核」的那块 3×3,彩色的是非零项 |
S | 特征图(feature map) | 滑完一遍的输出,存的是「这里的边缘有多强」 | 右边那张 6×6,鼠标移上去窗口会跳过去 |
i, j | 输出上的位置 | 「第 i 行第 j 列的输出点」,窗口每次只算一个 | 6×6 里光标指的那一格 |
m, n | 核内部的位置 | 「核里第 m 行第 n 列的数」,双重求和就是跑完这九个格 | 3×3 核里正在被读到的那个格子 |
Σ | 西格玛,把所有都加起来 | 3×3 一共九项,一个不漏地加起来 | 演示里那行「逐项相乘」的九个结果相加那一步 |
✱ | 卷积运算 | 把窗口盖上去、逐项相乘再求和这一个动作 | 演示里中间那一步「相乘求和 →」 |
严格说,这里算的叫互相关(不翻核)。深度学习里的核是学出来的,翻不翻只是把学到的 9 个数倒个序,所以大家直接叫它卷积,PyTorch 的 Conv2d 算的也是这个。
🎯 类比 想象你拿着一块 3×3 的镂空模板在照片上盖印,模板上写着"左边减、右边加"。 盖在平坦的地方得到 0(什么都没有);盖在明暗交界处,得到的数很大 —— 于是"边缘"就被这个模板挑出来了。
卷积核里的数字是学出来的,没人手工设计!我们只规定"用 3×3 的窗口去滑",里面那 9 个数字由
第 1 节那两个问题(
拖一下滑块,看看同一张图用两种方式处理,参数量差了多少倍:
互动 · MLP 全连接 vs CNN 卷积
MLP 那个数字跟图像面积成正比(边长的平方),CNN 那个数字永远是 1792。
换成分辨率更高的图,MLP 直接崩,CNN 纹丝不动。
两个数怎么来的:MLP 是「边长 × 边长 × 3 个颜色」个输入,每个都连到 256 个神经元,再加 256 个偏置;
CNN 是 64 块 3×3×3 的核,每块 27 个数加 1 个偏置,(27 + 1) × 64 = 1,792。
点下面这个按钮,把图片整体右移两格,然后观察右边的特征图发生了什么:
互动 · 图像挪位置,特征图跟着挪
现在两边是对齐的。点按钮试试。
特征图上的数字一模一样,只是整体平移了:不管猫在图片的哪个角落,同一组数字都会出现,只是出现的位置跟着挪。 这叫等变——输出跟着输入一起挪。它还不等于「不变」(不管在哪答案都一样):卷积本身只做到「跟着挪」, 要靠后面把位置收起来——第 5 节的 2×2 池化只能在同一个窗口内容忍一格左右的挪动, 现代 CNN 在末尾再把整张特征图取一次平均(全局平均池化),位置才彻底不影响结果。
同一张输入图,卷积核里的数字变一变,输出的特征图就完全是另一回事。 点下面的按钮,看同一张 8×8 图被不同的"眼睛"看成了什么。
互动 · 卷积核画廊
| 卷积核 | 里面的数字(3×3) | 它"看见"的东西 |
|---|---|---|
| 垂直边缘 | −1 0 1 / −2 0 2 / −1 0 1 | 竖直方向的明暗分界线 |
| 水平边缘 | −1 −2 −1 / 0 0 0 / 1 2 1 | 水平方向的明暗分界线 |
| 模糊 | 9 个格子全是 1/9 | 把周围平均一下 → 噪点消失,细节变糊 |
| 锐化 | 中间 5,四邻 −1 | 把"自己和邻居的差异"放大 → 边缘更硬 |
| 浮雕 | −2 −1 0 / −1 1 1 / 0 1 2 | 给出方向性的立体感 |
| 原样复制 | 中间 1,其余 0 | 什么都不做(这是卷积的"单位元":乘进去什么都不改变的那个数) |
上面这些核是人类手工设计的经典例子,用来帮你理解"卷积能干什么"。 真实网络里的卷积核是训练出来的,长得跟这些不完全一样,但浅层的核往往确实很像边缘/纹理检测器。 想看自己训出来的核长什么样:把第一层的每个 3×3 核当成一张小图打印出来就行——上面画廊里显示的那个 3×3 方阵就是这么摆的,表里每一行也是一个核按行写开。
特征图太大了,计算吃不消。池化的做法很暴力:每 2×2 的一小块里,只留最大的那个数。 面积直接砍到 1/4,信息留下最"亮"的部分。
互动 · 2×2 池化
6×6 输入 → 2×2 窗口、步长 2(窗口每次挪 2 格)→ 3×3 输出。参数 0 个,完全没有要学的东西。
假设这 2×2 里最大的那个数是"这里有一条边"。不管这条边在窗口里偏左一像素还是偏右一像素, 最大值都一样。所以池化提供了一点"容忍误差"的能力——在同一个池化窗口之内,轻微位移、轻微变形都不影响结果。
那被丢掉的 3/4 信息呢?确实丢了。池化赌的是「最强的响应出现在哪一块附近」比精确位置更重要——后面接全连接做判断时,需要的正是这个。
一层卷积只能看 3×3。但把卷积 + 池化反复叠,
🎯 类比 像拼乐高:边缘是小砖块 → 角、弧线是小组件 → 眼睛、耳朵是模块 → 猫脸是成品。 层数越深,能表达的"零件"越复杂。"深度学习"里的"深"就在这。
互动 · 立方体里的小立方体:一个核怎么吃掉所有通道
前面几节都在讲零件。这一节把它们接起来串成一条路:卷积 → 池化 → 展平 → MLP。 下面第一层卷积的输出和输入一样大(都 32×32):四周各补了一圈 0(填充),所以尺寸不变;第 M 节细讲。
互动 · 完整前向过程逐步播放
空间尺寸一路变小(224 → 112 → 56 → 28 → 14 → 7),通道数一路变多(3 → 64 → 128 → 256 → 512)。 通道就是每个位置上的那一串数:彩图有红绿蓝 3 个通道,卷积层里每个核产出 1 个通道。 翻译成人话:图片越来越小,但每个位置携带的"语义信息"越来越丰富。 最后那 7×7×512 已经不再像图片,而是一组"关于整张图的高层描述"。
CNN 和 MLP 的关系:一张图看懂
左边是这一章讲的卷积部分,右边是 《MLP 多层感知机》那一章的全连接部分(数字取自 VGG-16)。 它们不是两种模型,而是同一个模型的两段。 VGG-16 是 2014 年一个经典的 16 层卷积网络,最后在 ImageNet(约 120 万张图、1000 类的图像数据集)上分 1000 类——和上面演示的 4 类是两个不同的例子。
分界点就是那个 Flatten(展平)。
展平之前:数据还是三维的(高 × 宽 × 通道),卷积核在上面滑,保留空间结构,
所以能"看出哪里有什么"。
展平之后:变成一根长长的向量。每个数还固定对应原来的某个位置,
但「谁和谁挨着」这层关系没了:把所有像素按同一个固定顺序打乱,MLP 学起来一样好,CNN 会明显变差。
所以最后那几层全连接只负责拿特征下判断;这也是
MLP 单独处理图片效果差的原因:它从第一层起就没用上「谁和谁挨着」。
🎯 类比 CNN 像一个流水线上的质检团队:一层一层地把"原料"加工成"半成品",
每一站都在丢掉位置细节、提炼语义。
到了 Flatten 这一步,相当于把一块立体雕塑碾成了一张清单——
"有 512 个数值,分别是……"。
MLP 就是拿到这张清单的决策者:它不需要知道雕塑长什么样,
只看这 512 个数就能拍板"这是猫"。
第 2 节看着小窗口一格一格滑过去。这一节只回答一个问题: 它一共能盖几个位置?答案就是下面这条式子。N 是输入边长,k 是核大小,p 是补的圈数,s 是步长——拖四个滑块,输出格子跟着变。
互动 · 输入多大、窗口多大、跳几步,决定输出多大
把「步长」从 1 调到 2:绿点立刻只剩 1/4——跳着走,能盖的位置就少了,输出变小。 再把「填充」先拖到 0(输出缩一圈),再拖回 1(又长回来)。搭网络时算每一层输出多大,用的就是这一行。
前面几节讲的都是标准卷积。它有一个通病:感受野和计算量是绑死的, 想看得更远就得堆更多层,想更省就得把图缩小。 这一节把人们绕开这个绑定的五种办法排开(第一行「标准卷积」是基线)。
| 变体 | 它改了什么 | 一行写法 | 什么时候用 |
|---|---|---|---|
| 标准卷积 | 基线:3×3 窗口,逐位相乘再相加 | Conv2d(cin, cout, 3) | 默认。绝大多数位置都用它 |
| 1×1 卷积 | 窗口缩到一个像素,只看通道方向 | Conv2d(cin, cout, 1) | 改通道数、跨通道混合。GoogLeNet 用它先把通道压下来,再做 3×3(GoogLeNet 是 2014 年的经典网络,后面《经典 CNN 家族》里讲) |
| 空洞卷积 | 把窗口「撑开」:采样点之间留空,不动分辨率就扩大感受野 | Conv2d(..., dilation=2) | 语义分割。一次盖 5×5 的范围,却只花 3×3 的参数量 |
| 深度可分离卷积 | 拆成「逐通道各卷各的」+「1×1 混合」两步 | Conv2d(c, c, 3, groups=c) + Conv2d(c, cout, 1) |
移动端。计算量和参数量省 8~9 倍,精度只掉一点 |
| 转置卷积 | 把卷积反过来:小图变大图 | ConvTranspose2d(...) | 上采样。分割的 decoder、生成模型都靠它 |
| 可变形卷积 | 那 9 个采样点的位置也变成可学的 | 学一个 offset 再采样 | 目标形状多变时(文字、行人)。代价是实现复杂 |
这张表只讲「卷积这个操作怎么变」。那些整机的经典网络
(AlexNet / VGG / ResNet)是另一件事,在 《经典 CNN 家族》那一章。
后面两行是特定任务的工具。
写法里的 cin / cout 是输入 / 输出通道数,groups 是把通道分成几组各卷各的,offset 是采样点额外学出来的位置偏移。
因为它干的不是「看邻居」,是「把通道重新组合一遍」。 3×3 卷积在每个位置把 9 个空间邻居乘起来,1×1 把同一位置的所有通道乘起来。 想改通道数又不想动空间尺寸,就只有它。
卷积之所以省参数,是因为它假设了两件事:一次只看一小块(局部性), 并且让同一块核走遍全图(权值共享)。权值共享带来一个性质:东西挪到哪,响应就跟着挪到哪(平移等变)。 这两条在图像上几乎总是对的,所以它赢了二十年。但也正是它们,划出了它的边界。
2026 年的一次实测
2026 年一篇视网膜疾病筛查的基准,把 12 个架构分成 4 族比了一遍: 简单的二分类上大家都不错,难的多标签任务上 attention 系和混合架构更强。 这只是一个医学影像任务,但方向和这几年的公开榜单一致:纯 CNN 没有被淘汰,只是不再是默认答案。
| 它不成立的地方 | 什么时候真的会痛 | 怎么办 |
|---|---|---|
| 全局长程依赖要靠堆层数硬看 | 图像里相隔很远的两个部分要发生关系 | 换成带注意力(attention)的骨干(骨干=网络里负责提取特征的主体部分)。CNN 要几十层才够的距离,attention 一层就到 |
| 它假设了平移等变 | 位置本身携带信息:医学影像、文档版面、有坐标意义的场景 | 显式加位置信息(坐标通道、位置编码),别指望卷积自己留着 |
| 标准的 2D 卷积只吃规则网格 像素排成规整方格的那种数据 |
文本序列、图结构(→ 《图神经网络》)、点云、任意变长的输入,都不是二维网格 | 文本要用一维卷积(窗口只沿一个方向滑),图和点云要专门设计的变体(图卷积、点卷积),或换 attention。Transformer(全靠注意力搭起来的模型,后面章节讲)能在文本、图像、语音上通用,这是一个重要原因 |
| 它的归纳偏置在数据多时会变成束缚 归纳偏置=结构里事先认定的规律,比如「附近的像素更相关」 |
数据量已经大到不需要「先验帮忙」了 | 数据少时卷积的先验(就是上面说的归纳偏置)是帮忙的,数据多时它限制上限。这也是 ViT(把图片切成小块、用注意力处理的模型,后面章节讲)要在大数据上才赢的原因 |
| 它在端侧不是最省的 | 要在手机上实时跑 | 换成第 8 节的深度可分离卷积,或者整机换成 MobileNet 系(为手机设计的轻量网络) |
怎么判断手上的任务会不会踩到这些边界?问一句:把图里的东西挪个位置,答案该不该变——该变(比如「这块阴影在左肺还是右肺」),就别指望卷积自己保住位置。
看两件事:数据量(小数据就用 CNN,它的先验能帮你省样本) 和有没有现成的预训练权重(有就用 ViT / hybrid,它们在大数据上更强)。 两边都占的话,混合架构最稳。
这一章站在 ② 没有免费午餐 这条线上:CNN 先对图像下了两个判断(挨得近的更相关、同一块核走遍全图), 再拿「只看局部 + 权值共享」换来省参数;权值共享还带来一个性质——东西挪到哪,响应跟着挪。
回到第 2 节那张卷积演示。把鼠标在右边 6×6 输出图上从左划到右—— 窗口一路跟过去,每一格都是同一块 3×3 核算出来的。
那个瞬间你看到的就是「假设被焊进结构」:同一套 9 个数字,不管滑到哪儿都在找同一种东西。 再回第 1 节把十字挪三格:全连接那根条掉到 0,小印章那根一动不动。
| 假设 | 代码里的体现 | 什么时候它会失效 |
|---|---|---|
| 局部性 附近像素更相关 |
3×3 的窗口只看相邻九个格子 | 图像里相隔很远的两块需要互相参照时(比如"这张脸和那片天空配不配") |
| 平移等变性 猫挪了位置,响应跟着挪 |
同一块核滑遍全图,权重共享 | 位置本身就是信息时("这块阴影在左肺还是右肺"):卷积只是让响应跟着挪,后面的池化加全局平均会把位置收掉,所以最后只知道「有」、不知道「在哪」 |
| 层次性 边缘→纹理→部件→物体 |
一层层堆叠,每层 |
只需要一层就能判断的任务里,深堆叠纯属浪费 |
| 暗线 | 这一章的回答 |
|---|---|
| A 信息流动 | 输入是 (高, 宽, 通道) 的三维张量,卷积一直保留空间结构,直到 Flatten 把它压平——那一步不可逆。 |
| B 什么被牺牲了 | 牺牲了「任意两个位置可以直接相连」的自由;换来的是参数不再随图片尺寸增长——一个 3×3 核只有 9 个数,第 3 节那层 64 个核、3 个输入通道合起来是 (27 + 1) × 64 = 1792 个(那个滑块量的就是它)。 |
| C 参数账本 | 一块 3×3 核就 9 个参数,图多大都不变;换成同样大小的全连接层,8×8 输入就要 64 × 36 = 2304 个。 |
| D 跑在什么上 | |
| E 它假设了什么 | 见上面那张表:CNN 厉害,是因为它猜对了两个关于图像的假设(局部性、权值共享→平移等变)。这是这一章最该被记住的事。 |
| F 违背了哪个直觉 | "看得越多越准"是错的:一层 3×3 看到的信息少得可怜,堆几十层后最上面的神经元却能看到整张图——感受野不是一层给的,是叠出来的(下一章 《感受野与下采样》)。 |
🎯 前后钩子 它接住了前面的什么:上一章 《超参怎么选》说损失高就加模型; 可图片上直接加全连接会参数爆炸——CNN 的局部连接与权值共享正是为它准备的。
它给下一章留了什么:卷积核只有 3×3,看不到大东西。 那"整张图"是怎么被看到的?《感受野与下采样》。
CNN = 一个 3×3 的小印章盖满全图。局部连接省参数,权值共享抗平移,层层堆叠后从边缘长出猫脸。
它的局限是
上面讲的都是「够用」的版本。想往下挖,这里有三个入口—— 它们是给想再往前走一步的读者准备的。