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

注意:我曾经犯过一个错误——在检测小目标时用了太多下采样,导致感受野过大,小目标特征被“淹没”。后来我调整了空洞卷积,才把感受野控制住。

这里我画了一张图,帮你直观理解感受野的叠加过程:

输入图像 32x32 Conv1 RF=3 Conv2 RF=5

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

深度可分离卷积分两步:

  1. 深度卷积:每个通道单独卷积,不跨通道
  2. 逐点卷积: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的深度可分离卷积,很多融合方案都借鉴了它们的设计。嗯,先消化这些,下一章我们聊聊自注意力机制。

可可精品资料库