很多人学CNN,最绕不开的就是“卷积核”和“通道”这两个概念。我第一次接触时也踩过不少坑:卷积核到底怎么“扫”过图片?通道数越多是不是一定越好?为什么一个3×3卷积就能提取边缘、纹理甚至人脸部件?这些问题不搞清楚,后面调参、改网络结构基本全靠试,试了也不知道为什么有用。这篇内容我就把卷积核和通道彻底讲透,从原理到实操,再到常见误区和可以复现的动手实验,给刚入门的同学一条能直接“抄作业”的路径。
1. 从全连接到卷积:图像处理为什么绕不开CNN
1.1 全连接网络面对图像有三个“不匹配”
先说一个很多人问过我的问题:图像处理为啥不用前馈神经网络?直接用全连接层,把图片像素一个个拉平不就行了?
理论上行,实际上很蠢。假设输入是一张224×224的RGB图片,拉平之后就是224×224×3=150528个数。你用全连接网络,第一层想提取64个特征,那么这个全连接层的参数量就是150528×64,算下来是963万,光这一层就快一千万个参数。到这还没完,这些参数里每一个都对应一个特定位置的特定像素,模型完全没有“像素挨得近就更相关”这个概念。
这带来三个致命问题。
第一,参数爆炸。一层就接近千万参数,到了深层网络几乎没法训练,显存、算力、过拟合全部爆表。
第二,局部性被破坏。图像里一个像素通常只和周围小范围内的像素有意义地相关,比如一个圆润的物体边缘、一段纹理,都只涉及局部区域。全连接层却强制让你把远处完全不相关的像素也一起算,等于在学一堆没用的关系。
第三,没有平移不变性。同一个物体,在图片里往左挪了3个像素,全连接网络的内部表达就完全变了,网络必须重新学一遍。可现实里猫出现在照片的左边还是右边,它就是同一只猫,网络却认不出来。
1.2 卷积怎么用局部连接解决参数爆炸
卷积网络的思路就很直接:我只让每个神经元连接输入的一个小局部区域,比如一个3×3或者5×5的窗口,这个窗口就叫“感受野”。一个像素的激活值,只由它附近一个方块内的像素决定,离得远的根本不参与计算。
这样一来,参数量一下子降下来。还是那个例子,第一层想提取64个特征,用3×3卷积,它的weight形状是64×3×3×3,注意最后那个3是输入通道数RGB,总共才1728个参数,加上bias是1792个。跟全连接的963万没法比。
同样的信息量,用卷积只要千分之一左右的参数,因为卷积天然假设了“局部性”,而这个假设在图像上是成立的。它不是不用这个关系,而是把它内置到了网络结构里。
1.3 权值共享与平移不变性:一张特征图对应一种检测器
卷积还有一个隐藏Buff,叫权值共享。我用一个3×3卷积核去扫描整张图,从左到右、从上到下,用的是同一组权重。什么意思?就是这个核在图片任何位置都干同一件事:检测它所代表的那个特征。
这时候你再看“平移不变性”就顺了。一个检测边缘的核,不管边缘在图片左上角还是右下角,它扫过去都能激活,网络不需要为每个位置单独学一套权重。
更关键的是,一个卷积核只生成一张特征图,这张特征图上的每一个像素,都代表“这个位置有没有这个特征”。这引出了CNN里特别重要的一条主线:卷积核决定“检测什么特征”,通道决定“一共检测多少个特征”。这两件事合起来,才有了深度网络“底层学边缘、中层学纹理、高层学部件和语义”的能力。
2. 卷积核是怎么工作的一台“特征探测器”
2.1 卷积运算的本质:滑动窗口做点积
先别管那些花哨的数学符号,卷积核在图像上的运算就是五个字:滑动窗口点积。
你拿一个3×3的核,盖在图像的某个3×3区域上,对应位置相乘,再加起来,得到输出特征图上的一个像素值。然后窗口向右滑动一个步长,继续算,直到扫完整张图。把这个过程想象成用放大镜一格一格看照片,每一格算出一个“这个区域和这个核像不像”的分数,分数越高,说明这片区域越像核想要检测的特征。
这里有个细节值得提醒:卷积核的参数,包括9个权重和1个偏置,偏置会在最后加上去。所以一个3×3核一共有10个可学习参数,很多人算参数量时容易把bias漏掉。
输出特征图的尺寸也有一套公式,输入是H×W,核大小是k,填充是p,步长是s,那么:
H_out = (H + 2p - k) / s + 1
W_out同理。这套公式我建议你手推一遍,哪怕闭着眼睛都能写出来,后面搭网络时反复要用到。
2.2 经典手写核:边缘、模糊、锐化
可能有人会觉得卷积核是深度学习“学”出来的,但事实上卷积运算是图像处理几十年的老手艺了。在深度学习火起来之前,图像处理工程师就是手写这些核来做特征的。
举个例子,Sobel边缘检测核长这样:
Gx = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]]它检测的是竖直方向的边缘。原理也很简单:左边一列权重是负的,右边一列是正的,中间是0。如果图像某区域左边暗右边亮,卷积结果就是正值,越大说明垂直方向的亮度变化越剧烈,也就是一条竖着的边缘。
水平方向边缘检测核Gy,就是把Gx转置一下:
Gy = [[-1, -2, -1], [ 0, 0, 0], [ 1, 2, 1]]还有高斯模糊核:
Gaussian = 1/16 × [[1, 2, 1], [2, 4, 2], [1, 2, 1]]它的权重是中间大、四周小,等于把中心像素和周围像素做加权平均,让变化锐利的亮度“糊”在一起,就起到了模糊的效果。
锐化核则相反:
Sharpen = [[ 0, -1, 0], [-1, 5, -1], [ 0, -1, 0]]它让中心像素和周围像素的差异更突出,视觉上边缘更锐利。
我建议你自己试一次:随便找一张灰度图,用上面的核做一次卷积,输出会非常直观地告诉你“卷积核到底在干嘛”。PyTorch里只需要定义好权重,然后调torch.nn.functional.conv2d就能跑。
2.3 学习到的卷积核:训练到底在调什么
手动设计核确实强大,但缺点是你得知道要检测什么特征,才写得出对应的核。真实世界的特征太复杂了,边缘可以手写,那鼻子、眼睛、车轮、羽毛呢?
这就是深度学习的核心突破了:不再由人指定核的权重,而是把权重当作随机初始化的参数,让反向传播根据损失函数的梯度去自动调整。训练的过程,本质上就是不断修改卷积核里的每一个数字,让它们输出的特征图对最终的分类、检测或者分割任务最有帮助。
你如果去看一个训练好的卷积网络第一层卷积核,常常能发现它们长得很像Gabor滤波器,就是一组方向不同、频率不同的条纹和边缘检测器。这不是谁规定的,是网络自己在图像数据集上学出来的规律。到中间层,核的响应开始组合成纹理、角点;到高层,一个通道可能就对应“有没有轮子”这种语义概念了。
2.4 核大小怎么选:感受野的堆叠逻辑
卷积核的大小,3×3、5×5、7×7该选哪个?如果你刚起步,我的建议是默认3×3,理由藏在“感受野的堆叠”里。
两层3×3卷积堆叠,感受野等效于5×5;三层3×3堆叠,感受野等效于7×7。但是参数呢?两层3×3的参数量是2×9=18,一个5×5核的参数是25;三层3×3的参数是27,一个7×7核的参数是49。也就是说,用多个小核替代一个大核,感受野一样大,参数量反而更少。
另外,小核堆叠还有两个隐形好处:中间多了一层ReLU激活,给网络增加了非线性表达能力;每层卷积都在前一层基础上重新组合特征,信息处理得更细。VGG当年就是靠这个思路把网络做深的,后来ResNet、DenseNet也延续了3×3为主的策略。
当然,遇到输入分辨率特别高、需要大范围上下文的情况,5×5或7×7仍然有它的用武之地,但入门阶段先别纠结,3×3足够应付绝大多数场景。
3. 通道:CNN内部的“信息分层暗号”
3.1 输入通道:RGB只是起点
通道这个词,刚开始接触时最容易把人绕晕。先说输入通道,这个比较好理解。一张彩色图片是三个通道叠加起来的:R、G、B。每个通道都是一张和图片同尺寸的灰度图,分别记录红色、绿色、蓝色的亮度值。
但到了卷积网络内部,通道的含义就变了。第一层的输入是RGB三通道,经过一次卷积之后,输出不再是三个通道,而是你设定的卷积核个数那么多个通道。比如你用64个卷积核做卷积,输出就是64个通道的特征图。
这64个通道每个都代表一个特征类型,不是颜色了,而是像“竖直边缘”“水平边缘”“角点”这种东西。所以你可以把通道理解成“这台网络一共用了多少个探头在同时扫描图片”,每一个探头专门负责检测一种特征。
3.2 输出通道与卷积核数量的关系
这里有个非常核心的公式关系:一个卷积核对应一个输出通道。你用了多少个卷积核,输出就有多少个通道,两者数量永远相等。
我见过很多初学者搭网络时搞混一件事:卷积核的形状到底是什么。在PyTorch里,一个卷积层的weight形状是:
(C_out, C_in, k, k)意思是C_out个卷积核,每个核的尺寸是C_in×k×k。注意,每个卷积核不是只有k×k,而是C_in×k×k。因为每个输出通道要“看”输入的所有通道。假如上一层输入是128通道,这一层有256个3×3卷积核,那么每个卷积核都要处理128个输入通道的信息,它的完整尺寸是128×3×3。
这个细节决定了很多东西。一个输出通道是输入所有通道信息的综合反应,而不是只看某一个颜色通道或者某一个特征通道。这也是为什么后面1×1卷积能做通道维度上的信息交互。
3.3 通道数为什么要逐层翻倍
你去翻经典网络结构,会发现一个规律:通道数通常是逐层翻倍的,比如64→128→256→512,同时特征图的宽高逐层减半。
这种设计不是随便拍的,背后有信息守恒的意思在里面。特征图被下采样,空间分辨率降低,意味着空间细节在减少。如果通道数不增加,网络能保存的信息容量就缩水了。增加通道数,相当于把压缩掉的空间信息转换到更多的特征维度里,让网络有机会用“更多种类特征”来描述这个物体。
你可以把特征图想象成一张照片,通道就是不同的滤镜视角。空间尺寸变小是照片变模糊,通道变多是滤镜种类变多。两者搭配起来,信息量才能保持住。同时也因为通道数增加,模型容量变大,高层语义特征的表达能力才跟得上。
3.4 通道维度的参数量公式:1994个参数怎么推出来的
我直接给你一个通用公式,计算任意卷积层的参数数量:
参数量 = C_out × C_in × k × k如果要加偏置,还要加上C_out个偏置参数。
拿一个具体例子来算。输入RGB三通道,第一层卷积用64个3×3核,那么:
参数量 = 64 × 3 × 3 × 3 + 64 = 1792那三个3,第一个3是输出通道数64?不是,重新看公式:C_out=64,C_in=3,k=3,k=3,所以是64×3×3×3=1728,再加64个bias,一共1792。
如果上一层输出是256通道,这一层用512个3×3核,那么一个卷积核的尺寸是256×3×3=2304个数,整个卷积层参数量是512×2304=1179648,加bias是1180160,约118万。你看,参数量很快就是百万级别了,这是通道数从256到512带来的直接代价。
搞懂这个公式后,你再去看网络结构,心里会特别有底。别人说“这个层很重”,你能立刻算出它到底重在哪里;别人说“用1×1卷积降维”,你也能立刻意识到它把C_in从256降到了64,参数量直接变成四分之一。
4. 通道注意力机制:SE模块为什么有效
4.1 信息冗余:很多特征图是“凑数的”
按上一节的说法,通道越多能保存的信息越多,但有一个很现实的问题:并不是每个通道对最终任务都同等重要。训练一个大型网络后,你会发现大量特征图响应很弱,很多通道输出的激活值几乎全是0,或者和其他通道高度相似,根本是冗余的。
这类通道占着参数量,消耗着计算资源,但对最终结果没什么贡献。这就是“通道注意力”要解决的问题:让网络自己学会,把注意力集中在更重要的通道上,削弱不重要的通道。
打个比方,这就跟混音师面对几十条音轨一样,人声轨道要拉高,某个录音环境噪音很重的轨道就要拉低。你不用全删,但要知道哪些轨道重要,哪些轨道不重要。
4.2 SENet三段式:压缩、激活、重标定
SENet(Squeeze-and-Excitation Networks)是通道注意力机制里最具代表性的工作,它用三个步骤实现通道的重标定。
第一步是Squeeze(压缩)。对每个通道的特征图做全局平均池化,把一个H×W的二维特征图压缩成一个数字。这个数字表示的是“这个通道在整个空间范围内综合激活的强度”。
第二步是Excitation(激活)。用一个两层的全连接网络处理这组数字。第一层把C个通道压缩到C/r,这里r是一个缩减比例,常见取16,作用是减少参数量;接一个ReLU;第二层再恢复到C个通道,接Sigmoid,把输出压缩到0到1之间。
第三步是Reweight(重标定)。把Sigmoid输出的C个0到1之间的权重,分别乘到原来的C个通道上。权重接近1的通道被保留,权重接近0的通道被压制。
这个机制妙在,它先通过全局平均池化看到整个特征图的统计信息,再用一个很小的神经网络学习通道之间的依赖关系,最后给通道“打分”,告诉网络“接下来你要多注意哪几个通道”。这就是热词里常说的SE模块。
PyTorch里实现非常简洁,核心就是这么一段:
class SEBlock(nn.Module): def __init__(self, channels, reduction=16): super().__init__() self.squeeze = nn.AdaptiveAvgPool2d(1) self.excitation = nn.Sequential( nn.Linear(channels, channels // reduction), nn.ReLU(inplace=True), nn.Linear(channels // reduction, channels), nn.Sigmoid() ) def forward(self, x): b, c, h, w = x.size() y = self.squeeze(x).view(b, c) y = self.excitation(y).view(b, c, 1, 1) return x * y4.3 用它之前先做的实验:打印特征图均值对比
SE模块直接在网络里插入通常有效,但如果你看过代码还是一头雾水,我建议你做一个低成本实验。
取一个训练好的网络,随便扔进去一张图片,把中间某一层输出的特征图打印出来,每个通道先算一个全局平均池化的数值。你会发现它们分布很散,有的通道平均值接近1,有的几乎为0。这背后就是通道重要性的差异。
接下来你手动把平均值最高的5个通道保留,把平均值最低的5个通道直接置0,然后再跑一次最终分类。对比一下精度变化。通常情况下,把低均值通道置0对结果的影响很小,但如果把高均值通道置0,分类结果会立刻恶化。这个实验能让你直观体会到,SENet的“注意力”到底在注意什么。
我建议在诸如CIFAR-10、ImageNet这类小型数据集上用一层带SE的ResNet做这个实验,不需要完整训练,找一个预训练模型就够看了。
5. 1×1卷积与深度可分离卷积:通道维度的轻量化手段
5.1 1×1卷积:通道间的一次“混合调配”
1×1卷积乍看很奇怪:核只有1×1,根本不看周围像素,那它做的是什么?
它做的纯粹是通道维度上的线性组合。输入有多少个通道,1×1卷积核就有多少个数。比如输入128通道,用32个1×1卷积核去卷积,输出就是32个通道。这里的计算逻辑是:每个输出通道=输入128个通道的加权和,权重是1×1卷积核里那128个数。
很多经典结构里都用它做通道降维。ResNet的Bottleneck设计就是先用一个1×1把256通道降到64,省掉后面3×3卷积的计算量,再用一个1×1把64通道升回256。这一升一降,参数量大幅下降,信息瓶颈还能迫使特征更紧凑。
它另一个用途是跨通道信息融合。因为普通卷积里不同通道的信息是在计算时各自独立处理后再叠加的,1×1卷积等于在通道层面做了一次线性混合,增加模型的表达力。NIN(Network in Network)和Inception系列都把这一点用到了极致。
5.2 深度可分离卷积:把空间卷积和通道混合拆开
如果1×1卷积是通道维度的“混合”,那么深度可分离卷积就是把“空间卷积”和“通道混合”两个步骤完全拆开。
它分两步走。第一步是Depthwise卷积,每个输入通道配一个独立的k×k卷积核,自己跟自己卷积,通道之间互不干扰。第二步是Pointwise卷积,也就是1×1卷积,把所有通道的信息混合起来。
这样做最直接的好处是计算量大幅下降。标准卷积的计算量是:
C_out × C_in × k × k × H_out × W_out深度可分离卷积的计算量是:
C_in × k × k × H_out × W_out + C_out × C_in × 1 × 1 × H_out × W_out以3×3卷积为例,深度可分离卷积的计算量大约是标准卷积的1/8到1/9,这里的猜测是9倍差拉满之后,再叠加1×1卷积做通道混合的成本,所以实际省下大约8到9倍计算量。
MobileNet系列就是因为这个结构能在手机上实时跑。我做移动端模型优化时,经常先把主干网络里的标准卷积替换成深度可分离卷积,模型体积和时延肉眼可见地降下来,精度掉得不多。它是我个人最推荐的轻量化手段之一。
5.3 计算量对比:一张表说清楚省在哪里
拿一个具体的数字来对比,假设输入是112×112×64的特征图,输出通道是128,用3×3标准卷积:
- 标准卷积计算量:128×64×3×3×112×112 ≈ 9.26亿次乘加
- 深度可分离卷积:Depthwise部分是64×3×3×112×112 ≈ 723万次,Pointwise部分是128×64×1×1×112×112 ≈ 1.03亿次
- 合计约1.1亿次,约为标准卷积的1/8.4
| 类型 | 乘加次数 | 相对标准卷积 |
|---|---|---|
| 标准3×3卷积 | 约9.26亿 | 1.0倍 |
| 深度可分离卷积 | 约1.10亿 | 约0.12倍 |
这张表我复推过很多次,结论稳定。所以你在看MobileNet结构时,会发现它大量使用1×1卷积,因为Pointwise部分占了深度可分离卷积的大头,降维到更小的通道数能进一步压低计算量,这就是MobileNet把最后一层1×1卷积做到很宽的底层逻辑。
6. 复盘一个真实调试案例和它的几点避坑心得
6.1 案例:为什么把通道数翻倍后训练变慢了
之前我做一个小型分类任务,原始网络第一层32通道,效果一般。我按惯性把32改成64,想着容量大一点总不会错。结果训练时间直接翻倍,而且精度几乎没提升。
排查后发现,问题不在通道数本身,在于我把所有层的通道都翻倍了,从第一层到最后一层。每层计算量都翻倍,加起来训练时间就爆炸了。更关键的是,任务本身只有10个类别,32通道的特征容量已经完全够用,再加64通道只是在“背”更多的训练集细节,反而更容易过拟合。
后来的做法是只在网络浅层加了通道数,深层保持不动,再配合一点数据增强,精度才真正涨上去。这就是我想说的第一个心得:通道数不是一个越大越好的旋钮,它是一个和任务复杂度、数据量匹配的变量,调它的前提是理解瓶颈在哪一层。
6.2 新手最容易混的三个概念
如果只挑三个最值得澄清的概念,我认为是通道数与卷积核数量的关系、卷积核尺寸和步长的区别、感受野和卷积核大小的区别。
通道数与卷积核数量是同一个意思,这句话听起来啰嗦,但很多人写代码时还是会搞混。你在PyTorch里定义nn.Conv2d(in_channels, out_channels, kernel_size),这个out_channels就是卷积核数量,也是输出通道数。
卷积核尺寸和步长是两个完全不同的参数。核尺寸决定每次“看多大区域”,步长决定“看完一跳多远”。很多人把步长调到2,还以为输出尺寸不变,结果特征图尺寸减半,然后百思不得其解。
感受野是一个卷积层最终能看到的输入范围,它不等于卷积核大小。多个卷积层堆叠后,感受野会叠加扩大,这一点我在前面核大小选择那节已经讲过。新手容易把“第5层的3×3卷积”直接理解成“只看5层之前的3×3区域”,这就完全错了。
6.3 我留下的小习惯:先可视化,再调结构
最后分享一个我很受用的习惯:调任何CNN结构之前,先把中间层的特征图可视化出来看看。
做法很简单,拿一张测试图片,注册一下某个卷积层输出的forward hook,把特征图保存下来,然后按通道画成网格图。你很快就会发现,低层特征图里都是边缘、颜色、纹理的响应,高层特征图越来越抽象,某些通道只在特定物体出现时激活。这一步比看任何准确率曲线都更能建立直觉。
我自己频繁做这个实验后,对卷积核和通道的理解深刻了很多:卷积核决定“怎么扫”,通道决定“扫多少个不同东西”。理解到这一层后再看SE、1×1卷积、深度可分离卷积这些设计,你就能自动把它们放到合适的位置。
如果你感兴趣,建议今晚就用PyTorch加载一个预训练ResNet,打印第一层64个卷积核出来看看。那些像条纹一样的图案,就是你深度学习旅程里最直观的“第一印象”。