2、卷积神经网络(CNN)基础回顾:卷积核、感受野、池化、经典架构(ResNet、MobileNet)
好,咱们正式开始。这一章我打算带大家快速过一遍CNN的核心概念。别担心,不是那种枯燥的教科书式复习。我会结合自己踩过的坑,把卷积核、感受野、池化这几个老朋友重新认识一下。最后再看看ResNet和MobileNet这两个经典架构,它们的设计思路对后续理解“卷积+自注意力”融合非常有帮助。
2.1 卷积核:特征提取的“小窗口”
卷积核,说白了就是一个可学习的滤波器。它像个滑动窗口,在输入图像上扫来扫去,提取局部特征。
核心参数:
- 大小:常见3x3、5x7。我习惯用3x3堆叠,效果往往比单个大核好。
- 步长:每次滑动的像素数。步长=2时,特征图尺寸直接减半。
- 填充:保持边界信息,常用“same”填充让输出尺寸不变。
我的经验:刚开始做图像分类时,我总喜欢用5x5的大核,觉得感受野大。后来发现,3x3堆叠两层,感受野一样大,参数量却少很多。嗯,这就是VGG给我的启发。
举个简单的例子,一个3x3卷积,输入通道3,输出通道64:
import torch.nn as nn
conv_layer = nn.Conv2d(
in_channels=3,
out_channels=64,
kernel_size=3,
stride=1,
padding=1
)
# 参数量 = 3 * 64 * 3 * 3 + 64 = 1792
小技巧:实际项目中,我建议先用小卷积核(3x3)做基线,再根据任务复杂度调整。别一上来就上大核,容易过拟合。
2.2 感受野:网络看到了多大范围?
感受野,就是特征图上每个像素对应到原始图像上的区域大小。你想想看,浅层网络感受野小,只能看到局部纹理;深层网络感受野大,能捕捉全局语义。
计算公式:
RF_{l} = RF_{l-1} + (kernel_size - 1) * stride_{l-1}
举个例子,一个3x3卷积堆叠3层,步长均为1:
- 第1层:RF = 3
- 第2层:RF = 3 + (3-1)*1 = 5
- 第3层:RF = 5 + (3-1)*1 = 7
注意:我曾经犯过一个错误——在检测小目标时用了太多下采样,导致感受野过大,小目标特征被“淹没”。后来我调整了空洞卷积,才把感受野控制住。
这里我画了一张图,帮你直观理解感受野的叠加过程:
2.3 池化:降维与特征压缩
池化层,说白了就是“浓缩精华”。它没有可学习参数,但作用很大:
- 最大池化:取窗口内最大值,保留最显著特征。我常用于分类任务。
- 平均池化:取窗口内平均值,保留整体信息。适合分割任务。
- 全局平均池化:整个特征图取平均,直接替代全连接层。ResNet就用它。
避坑指南:我曾经在轻量级网络里用最大池化,结果丢失了大量细节信息。后来换成步长为2的卷积来做下采样,效果好了不少。你想想看,卷积至少还能学,池化是固定的,灵活性差一些。
池化操作示例:
import torch.nn.functional as F
# 2x2最大池化,步长2
x = torch.randn(1, 64, 32, 32)
out = F.max_pool2d(x, kernel_size=2, stride=2)
print(out.shape) # torch.Size([1, 64, 16, 16])
2.4 经典架构:ResNet与MobileNet
这两个架构,我建议你重点理解。它们代表了两种不同的设计哲学。
2.4.1 ResNet:残差连接解决退化
ResNet的核心思想很简单:让网络学习残差 F(x) = H(x) - x,而不是直接学习 H(x)。
为什么有效?
- 解决了深层网络的退化问题(不是过拟合,是训练误差反而升高)
- 梯度可以直通浅层,缓解梯度消失
- 可以训练上百层的网络
一个基本的残差块:
class BasicBlock(nn.Module):
def __init__(self, in_channels, out_channels, stride=1):
super().__init__()
self.conv1 = nn.Conv2d(in_channels, out_channels, 3, stride, 1)
self.bn1 = nn.BatchNorm2d(out_channels)
self.conv2 = nn.Conv2d(out_channels, out_channels, 3, 1, 1)
self.bn2 = nn.BatchNorm2d(out_channels)
# 捷径连接
self.shortcut = nn.Sequential()
if stride != 1 or in_channels != out_channels:
self.shortcut = nn.Sequential(
nn.Conv2d(in_channels, out_channels, 1, stride),
nn.BatchNorm2d(out_channels)
)
def forward(self, x):
out = F.relu(self.bn1(self.conv1(x)))
out = self.bn2(self.conv2(out))
out += self.shortcut(x) # 残差连接
return F.relu(out)
我的习惯:实际项目中,我通常用ResNet-50作为基线。如果计算资源有限,就换成ResNet-18。记住,残差连接是“保底”的,即使新加的层学不到东西,至少不会比浅层差。
2.4.2 MobileNet:深度可分离卷积
MobileNet的目标是轻量级。它用深度可分离卷积替代标准卷积,参数量大幅下降。
标准卷积 vs 深度可分离卷积:
| 类型 | 参数量 | 计算量 |
|---|---|---|
| 标准卷积 (3x3, 输入64, 输出128) | 64×128×3×3 = 73,728 | 高 |
| 深度可分离卷积 | 64×3×3 + 64×128×1×1 = 8,768 | 约1/8 |
深度可分离卷积分两步:
- 深度卷积:每个通道单独卷积,不跨通道
- 逐点卷积:1x1卷积,融合通道信息
注意:MobileNet虽然轻量,但精度往往不如ResNet。我建议在移动端或实时场景用MobileNet,服务器端还是用ResNet更稳妥。有一次我在嵌入式设备上硬跑ResNet-50,帧率只有5fps,换成MobileNet-V2直接飙到30fps。
深度可分离卷积实现:
class DepthwiseSeparableConv(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size=3):
super().__init__()
self.depthwise = nn.Conv2d(
in_channels, in_channels, kernel_size,
padding=kernel_size//2, groups=in_channels
)
self.pointwise = nn.Conv2d(in_channels, out_channels, 1)
def forward(self, x):
return self.pointwise(self.depthwise(x))
2.5 本章小结
好了,这一章的内容就这些。我帮你梳理一下重点:
- 卷积核:3x3是黄金尺寸,堆叠比单用大核更高效
- 感受野:理解它的计算方式,避免小目标检测翻车
- 池化:能用卷积下采样就别用池化,除非你追求极致速度
- ResNet:残差连接是“保底”神器,训练深层网络必备
- MobileNet:深度可分离卷积是轻量化的核心,参数量直接降一个数量级
这些基础概念,后面讲“卷积+自注意力融合”时会反复用到。尤其是ResNet的残差思想和MobileNet的深度可分离卷积,很多融合方案都借鉴了它们的设计。嗯,先消化这些,下一章我们聊聊自注意力机制。