在机器学习系列里写到了第十六篇,总算要聊卷积神经网络(CNN)了。前面我们花了不少篇幅介绍全连接网络、反向传播、梯度下降这些基础,说实话,经常有朋友问:为什么处理图像不用前面学的全连接网络?答案其实很直接——全连接网络在图像任务上根本“卷不动”,参数量大到离谱,而且它天然忽略了像素之间的空间结构。这篇我根据自己的学习和带项目踩坑的经验,把CNN的第一部分拆开揉碎讲清楚:它到底解决了什么问题、卷积层池化层是怎么设计的、特征图尺寸怎么算、经典网络是怎么一步步演化的。这篇面向的是学过基础神经网络、想深入图像方向的读者,看完你应该能彻底理解CNN的骨架结构,并且能自己动手搭一个简单的卷积网络。
1. 从全连接网络的“死穴”说起
1.1 一张图拉平后发生了什么
我们先看一个最简单的场景:假设输入是一张 224×224 的彩色图片,三个通道(RGB)。如果按全连接网络的思路,第一步就是把图片“拉平”成一维向量,那这个向量有多长?224×224×3,算一下是 150528 维。
紧接着问题就来了。假设我们第一个隐藏层设置 1024 个神经元,那这一层的权重参数有多少?150528×1024,约等于 1.54 亿。这还只是第一层。如果网络再深一点,参数会膨胀到完全没法训练的地步:显存不够、训练速度极慢,而且数据量根本不足以支撑这么多参数,结果就是严重过拟合。
当然,我们可以通过调小隐藏层神经元数量来缓解参数规模,但还有另一个更本质的问题:把一个二维图像拉成一维向量,像素之间的空间结构被彻底破坏了。要知道,图像里相邻像素往往高度相关,比如一只猫的耳朵轮廓、毛发的纹理,局部特征是有完整结构的。拉平之后,原本“上下左右”的邻接关系全没了,网络只能去学习一堆毫无顺序感的原始特征,效率极低。
1.2 CNN 应对图像任务的三个核心设计
卷积神经网络之所以适合图像,本质上是在网络结构里内置了三个先验假设:局部连接、参数共享、平移不变性。这三个概念初看有点抽象,我用一个自己常用的类比来解释:你可以把卷积核想象成一个“局部放大镜”,它在整张图上滑动,每个位置只看一个小窗口;而这个放大镜本身是同一个,不管滑到哪里,用的都是同一套参数。
这也就是为什么 CNN 参数少:局部连接让每个神经元只连输入的一小块区域,参数共享让同一套卷积核在整张图上复用,平移不变性则意味着猫在图的左边还是右边,卷积网络都能用同一组特征检测器识别出来。这三个设计凑在一起,既控制了参数量,又保留了空间结构,效果远超全连接网络。
所以,CNN 不是凭空发明的新东西,它更像是针对“图像到底是什么样的数据”所做的一系列合理妥协和设计。
2. 卷积层核心拆解
2.1 卷积运算到底在算什么
卷积层是 CNN 的核心,但很多初学朋友一听到“卷积”两个字就发怵,总觉得是高数里的连续积分。其实在神经网络里,卷积就是一次简单的“逐元素乘法再求和”。
具体来说,假设输入是一张 5×5 的灰度图,我们用一个 3×3 的卷积核(也叫滤波器)去扫描它。卷积核覆盖左上角 3×3 区域时,把对应位置的像素值和卷积核里的权重相乘,然后把 9 个乘积加起来,得到一个数值,这个值就是输出特征图左上角第一个位置的像素。接着卷积核向右滑动一个步长,重复同样的计算,扫完整个输入后就得到一张新的“特征图”。
这个过程本质上就是用一组权重去检测某种局部模式。比如某个卷积核的权重恰好能检测水平边缘、垂直边缘或者某种纹理,它输出的大数值就表示“这个位置存在我感兴趣的特征”。
这里有个容易忽略的点:我们不是只用一组卷积核,而是用多组。比如第一层用 32 组卷积核,每组 3×3,那么输出就是一个有 32 个通道的特征图。你可以理解为每通道是在回答“图中哪些位置存在模式 A”,第二通道回答“哪些位置存在模式 B”,以此类推。这 32 个特征图叠加在一起,构成了对原始图像的更丰富的抽象表示。
2.2 步长、填充和特征图尺寸计算
实操中,调卷积层必然要面对三个超参数:卷积核大小(kernel size)、步长(stride)、填充(padding)。很多人在这里容易记混,我这里给一个万年公式: 输出特征图尺寸 = (W - K + 2P) / S + 1,其中 W 是输入尺寸,K 是卷积核尺寸,P 是填充的圈数,S 是步长。
我建议不要死记公式,而是理解公式怎么来的。卷积核在图上能滑动的起始位置只有 W-K+1 个,再加填充相当于两边各扩大了 P 个像素,所以滑动范围变成 W-K+2P,每一步走 S,能走几步就取整加 1。
举个例子:输入 32×32,卷积核 3×3,步长 1,不填充。代入公式:(32-3)/1+1 = 30,输出是 30×30。注意尺寸变小了 2。如果我们希望输出和输入一样大,怎么办?那就设置 padding=1,因为 (32-3+2)/1+1 = 32,尺寸不变。
这里有个实用经验:在搭建比较深的网络时,我多数情况下会保持卷积层输出尺寸不变(用 padding 来凑),把尺寸的变化留给池化层去控制。这样后续通道数和数据流向会非常清晰,不容易算错。步长大于 1 的卷积可以用于下采样,但从我自己的使用习惯看,能用池化就别用大步长,后面我会解释原因。
2.3 卷积核越大越好吗
很多第一次搭网络的朋友上来就问:卷积核是不是越大越好?毕竟看得区域大,提取的特征应该更丰富吧?
理论上大卷积核感受野更大,但实际经验告诉我,3×3 卷积核才是工程上的“主力军”。为什么?核心原因是:两个 3×3 卷积叠加后的感受野和一个 5×5 卷积一样大,但参数量只有它的 18/25。更妙的是,中间多了一次非线性激活,网络的表达能力反而更强。
我举个例子帮你算一下:假设输入通道是 C,输出通道是 C。一个 5×5 卷积的参数是 25C²,两个 3×3 卷积的参数是 2×9C²=18C²。省下 28% 的参数,还多了一层非线性变换,这买卖怎么算都不亏。现代网络里大量使用 3×3 甚至 1×1 卷积,就是在参数量、感受野和表达能力之间反复权衡后的结果。
当然,第一层有时候会例外,比如有些人喜欢用 7×7 的大卷积核覆盖原始图像,这样可以更快地用较大的感受野处理原始像素信息。不过现在很多新模型也把 7×7 拆成几个小卷积来用,所以大卷积核的使用场景在逐渐减少。
2.4 单通道与多通道输入的卷积怎么算
上面举例时假设的是灰度图(单通道输入)。但实际操作中,输入经常是三通道彩色图,或者上一层输出是多通道特征图。这时候卷积是怎么处理的?这是很多初学者最懵的点。
核心规则:卷积核的通道数必须与输入的通道数一致。也就是说,如果一个卷积层输入有 64 个通道,那这一层的每组卷积核也是 64 个通道,但每个卷积核通道是 3×3 这样的二维尺寸。每组卷积核对输入的所有通道做“逐通道相乘相加”后,再把所有通道的结果累加,最终输出一个“单通道”的特征图。
如果这一层有 128 组卷积核,那输出就有 128 个通道。所以“卷积核的数量决定输出的通道数”这句话要刻在脑子里,它决定了整个网络的通道数走向。
做了这么多年图像项目,我发现通道数理解透了,后续看 ResNet、DenseNet 这类复杂网络结构会轻松很多,因为它们本质上就是在一层层地调整通道数。
3. 从统计角度理解参数共享与局部感受野
3.1 感受野的含义
感受野这个词听着高深,其实就是:输出特征图上的某个像素,对应回原始输入图像上的一块区域,这块区域的大小就是感受野。
实际使用中,每经过一个 3×3 卷积,感受野就扩大一点。比如输入层直接接一个 3×3 卷积,输出像素的感受野就是 3×3。再接一个 3×3 卷积,第二个卷积层的输出像素感受野就变成 5×5(因为第一个卷积层输出上相邻 3×3 个像素,对应回原始图像是 5×5 区域)。
这个概念的工程意义在于:如果我们要检测的是“大物体”,比如一整张人脸,那浅层网络这时候覆盖不够,必须通过堆叠卷积层来扩大感受野,后面的层才能看到足够大的上下文。这也是为什么图像分类网络通常都很深,浅了看不全局。
有个地方必须提醒大家:感受野的扩这是连续膨胀的,但在早期层,特征图尺寸可能还比较大,计算量比较可观,所以设计网络时要在“够不够深”和“跑不跑得动”之间做权衡。
3.2 感受野递推:手工计算一次
这里我直接给一个常用的感受野递推公式,方便你搭网络时手动估算: R_l = R_{l+1} + (K_l - 1) × stride_1 × stride_2 × … × stride_{l-1}
看着有点绕,我举个例子。假设网络结构:输入 -> 卷积(3×3, stride=1) -> 卷积(3×3, stride=1) -> 池化(2×2, stride=2) -> 卷积(3×3, stride=1)。
从最后一层往前推。倒数第一层卷积核大小为 3,它前面没有层,所以它的感受野就是 3×3(相对倒数第二层输出)。继续往前,卷积核还是 3,因为中间隔着池化(stride=2),所以往前推时,感受野在该池化层对应的步长要乘 2,那就是 3 + (3-1)×1×2 = 7(相对于再前面一层的输出)。再往前推一层卷积,步长因子不变,感受野变成 7 + (3-1)×1×2 = 11。最终相对原始输入的感受野没过早变化的话,大概在 11×11 左右。
算这个有什么用?当你做分割任务或者小目标检测时,如果感受野太小,模型根本看不到足够大的范围,效果会非常差。我建议拿到一个新网络结构时,先花两分钟把各层感受野算一遍,看看是否覆盖了你关注的目标大小。
3.3 参数共享为什么不会导致“学不到特征”
一个常被提到的问题:参数共享是不是太粗暴了?同一个卷积核用在整个图上,不同位置的相同特征它都能识别,但不同位置的“语境”不是不一样吗?
这个问题问得很到位。参数共享确实限制了每个卷积核的表达能力,但网络通过“多个卷积核”来弥补这一点。也就是说,虽然每个卷积核是全局共享的,但不同的卷积核会关注不同类型、不同频率的特征,组合起来就够用了。
这就好比一群人分工看整幅图:一个人专门看横线,另一个人专门看竖线,还有人看颜色渐变,虽然有分工但也合作,最终拼接出来就是完整的理解。参数共享带来的好处是巨大的:是它让 CNN 不需要每个位置都学习一套独立权重,从而实现了平移不变性。
4. 池化层:特征图的“降维压缩器”
4.1 池化的本质与两种常用方式
池化层在 CNN 早期结构中几乎是标配,它的核心作用一句话:在保留主要特征的前提下,对特征图做下采样。
最常用的是最大池化(Max Pooling)和平均池化(Average Pooling)。以 2×2、步长 2 的最大池化为例,它会将特征图分成不重叠的 2×2 小块,每块取最大值。这样特征图的高和宽都减半,但通道数不变。
我自己的经验是:分类任务里最大池化更常用,因为最大值往往代表“这块区域有没有出现某个特征”——比如这里有没有一个边缘,有没有一个角点。平均池化则更平滑,常用于网络的最后阶段,比如把最后一层特征图全局平均池化成一个向量,再接分类器。
4.2 池化到底有没有被淘汰
可能有人问:现在很多新网络不是不用池化了吗?比如 ResNet 里有步长为 2 的卷积做下采样。这话没错,甚至有不少观点认为池化不是必需的,用 stride 卷积也能达到空间降维的目的。
但我个人的看法是:池化在 CNN 的设计学习阶段依然重要。它简单、直观、无参数,而且是理解“空间缩放”这个概念的最佳教具。很多轻量级网络、前端模型仍然在使用池化做下采样。此外,池化还可以提供一定程度的平移不变性——就算目标在图像里挪了几个像素,池化后的特征图大概率还是相似的,这对后续分类是有好处的。
不过有一种场景我坚决不用池化:图片本身很小(比如 28×28),这时候连续池化几次,特征图直接缩到 1×1 了,空间信息完全丢失。遇到这种情况,宁可用步长 1 的卷积加 padding 维持尺寸,只在特定阶段下采样。
5. 经典 CNN 结构范例与数据流分析
5.1 从 LeNet 到 ResNet 的演进逻辑
要想真正掌握 CNN,只看理论不够,我建议你把几个里程碑模型的演变捋一遍。
最初的 LeNet 是给手写数字识别设计的,结构很简单:卷积 -> 池化 -> 卷积 -> 池化 -> 全连接 -> 输出。它确立了“卷积提取特征,池化降采样,全连接做分类”的基本范式。
后来的 AlexNet 在 LeNet 基础上把网络做得更深更宽,并且引入了 ReLU 激活,解决了梯度消失问题,训练速度也快了很多。再往后 VGG 证明了一个规律:把网络做深,用小卷积核堆叠,效果会稳定提升。而 GoogLeNet 则从“宽度”出发,在同一层里用不同尺寸的卷积核并行,增加网络的多样性。
到了 ResNet,最大的贡献是“残差连接”。当网络层数太深时,梯度在反向传播中会逐渐消失,前几层几乎学不到东西。ResNet 在结构上加了“捷径连接”,让信息可以直接跨层流动,梯度也能更顺畅地传到浅层。这个设计让训练几百层的网络变得可行。
我自己带新人的时候,经常建议按这个顺序去逐步看代码、改结构,会比直接上手复杂网络扎实得多。
5.2 手写一个微型 CNN 并算清各层维度
这里我给一个经典的手写数字识别例子(MNIST 数据集,输入 28×28×1)。结构如下:
- 卷积层:核 5×5,输出通道 6,padding=0,stride=1。输出尺寸:(28-5)/1+1=24,所以输出为 24×24×6。
- 池化层:2×2,stride=2。输出尺寸 12×12×6。
- 卷积层:核 5×5,输出通道 16,padding=0,stride=1。输出尺寸:(12-5)/1+1=8,所以输出为 8×8×16。
- 池化层:2×2,stride=2。输出尺寸 4×4×16。
- 全连接层:先把特征图拉平,4×4×16=256 维,再接全连接输出到 120,最后再输出到 10 个类别。
你有没有注意到卷积层的通道数变化:1 -> 6 -> 16,特征图从 28×28 一路缩到 4×4。这个“空间变小、通道变多”的模式,几乎是 CNN 的通用范式:网络前半段在提取越来越抽象的特征,通道数不断增加,同时空间维度不断压缩,直到最后变成一列特征向量用来做分类。
这段设计看起来很简单,但当年我初学时最大的困惑就是“为什么全连接层输入是 256 而不是其他数”。答案就一个:把上一层特征图拉平后的长度。所以建议你现在就拿张纸,把上面 4 个步骤的维度演算一遍,比看任何资料都管用。
5.3 经典结构参数对比
为了让你对常见 CNN 结构的参数规模有点感觉,我做个小表:
| 结构 | 年代 | 主要特点 | 大致参数量 | 关键贡献 |
|---|---|---|---|---|
| LeNet-5 | 1998 | 卷积+池化+全连接 | 约 6 万 | 确立 CNN 基本范式 |
| AlexNet | 2012 | 网络加深,ReLU | 约 6000 万 | GPU 训练,大幅提升性能 |
| VGG-16 | 2014 | 统一用 3×3 卷积 | 约 1.38 亿 | 证明深度的重要性 |
| GoogLeNet | 2014 | 多分支并行卷积 | 约 700 万 | 高效利用计算资源 |
| ResNet-50 | 2015 | 残差连接 | 约 2500 万 | 解决深层网络退化问题 |
我特别想说说 GoogLeNet。它的参数量只有 VGG 的零头,但准确率却更高,原因就是它在结构上减少了大量冗余参数。这给我们一个启示:网络效果好坏,不全看参数量大小,结构设计是否高效有时更关键。
6. 训练 CNN 的常见问题与踩坑经验
6.1 特征图尺寸对不上
这是我看到新房最容易踩的第一个坑。FCN 或者 Transformer 里一旦尺寸不匹配,直接报错倒还好,问题是很多框架里卷积操作即使尺寸不对也只是在边界上截断或者给个警告,导致模型能跑到一半,训练结果却莫名其妙。
我的建议是:在动手搭网络之前,先用纸笔把每一层的输出尺寸算出来,尤其注意 padding 和 stride 的组合。如果模型里有多分支结构(比如 GoogLeNet、ResNet 的捷径分支),还要保证相加或拼接的维度一致。实际操作中,我习惯在网络定义代码旁注释每一层的输出形状,一旦报错能立刻定位。
6.2 小数据集上硬套复杂网络
很多人一开始就上 ResNet-152,结果在自己的小数据集上反而比简单网络效果差。原因很简单:复杂模型的容量太大,数据量喂不饱,最终严重过拟合——训练集准确率很高,验证集一塌糊涂。
我自己的经验是:当数据集只有几千张图时,优先考虑小模型(比如自定义的 4~5 层卷积网络),配合数据增强(随机裁剪、翻转、颜色抖动),先把基线效果跑出来。如果效果还可以,再逐步加大模型的复杂度。千万不要盲目迷信参数多就是好。
6.3 卷积核数量怎么定才合理
很多朋友问:第一层到底用多少组卷积核?这个问题没有标准答案,但有经验规律:通常从 16、32、64 起步,逐层翻倍,最多到 512 或 1024 就差不多了(更大只在大规模任务里才用到)。
翻倍的逻辑是:浅层提取的是边缘、纹理这种相对简单的特征,需要的通道数少;深层要组合出更复杂的语义模式,需要更多通道来承载不同的组合方式。如果你发现模型在某个阶段欠拟合(训练集准确率也上不去),可以考虑先加通道数,而不是先加深层数。
6.4 千万别忘记归一化层
这里要特别提一下批量归一化(Batch Normalization,简称 BN),它在 CNN 里的地位几乎和卷积层本身一样重要。BN 做的事情:把每个批次内的特征分布拉回到均值 0、方差 1,然后再用可学习的缩放和平移参数去恢复表达能力。
实际效果是:训练大幅加速,对初始化不那么敏感,还能起到一定的正则化作用。我的习惯是,在每个卷积层之后、激活函数之前加一个 BN。除非有特别强烈的理由,否则我不会省略它。
不过要注意:BN 在 batch size 比较小的时候效果会变差,因为统计量计算不稳定。如果因为显存限制 batch size 只能设到 2 或 4,我往往会考虑换用其他归一化方式,比如 Layer Normalization 或 Group Normalization。
7. 实用技巧:用数据增强和数据流可视化帮自己排错
每天面对一堆抽象的数字,看不见摸不着,很容易在调参时陷入盲目状态。我自己的两个小习惯,极大地提升了调试效率。
第一个是数据增强。图像分类里,随机水平翻转、随机裁剪、随机亮度和对比度调整基本是必选项。这不仅是为了扩充数据量,更重要的是让网络学到“不看位置、不看颜色、不看方向”的鲁棒特征。增强要在合理的范围内,比如翻转后物体语义不能变,不然反而干扰学习。
第二个是特征图可视化。训练时每隔几步,把某一层部分卷积核输入对应的输出特征图截图保存下来。浅层的特征图通常是边缘、颜色块,深层的特征图往往会呈现出具有语义含义的激活模式。如果你的可视化图在训练很久后依然是一团噪音,那几乎可以断定网络出了问题,而不是“还在收敛中”。
标准做法是:选一张验证集图片,前向传播时把需要的特征图捞出来,转成灰度或伪彩色图保存。在 PyTorch 里注册一个 forward hook 就行,不需要改动网络代码主体;在 TensorFlow 里的做法是通过中间层输出张量。熟练之后,这个操作只需要几分钟搞定,但能省下大量盲调的无效时间。
8. 写在最后:动手、画图、算维度
这篇把 CNN 的核心骨架讲了一遍:局部感受野、参数共享、卷积与池化、特征图维度计算,以及经典网络结构这条演进线。信息量确实不小,但如果你只记住一件事,我希望是这句话:搭 CNN 一定要动手算维度和画特征图尺寸,而不是像堆积木一样随手堆。
我自己第一次搭 CNN 时,亲手把 LeNet 的每一层输出尺寸在纸上完整算了一遍,之后再看任何网络结构都觉得心里有底。那些看起来复杂的 ResNet 分支、DenseNet 拼接,归根到底就是卷积操作、通道数和特征图尺寸在变化而已。
下一篇我准备接着讲卷积神经网络的两个更深的话题:转置卷积(反卷积)如何把特征图“变大”,以及空洞卷积如何在不增加参数的前提下扩大感受野。那部分在做图像分割和生成模型时是绕不开的。这篇就先到这里,如果你在自己叠加网络时遇到了维度对不上的报错,建议先按我第二节的公式把尺寸从头捋一遍,多数问题都是那一步算错了引发的。