Skip to content

CNN:卷积与视觉任务 ​

核心:用「小的、共享的、滑动」的卷积核,替代全连接,从而利用图像的空间结构。

为什么全连接不行 ​

一张 224×224×3 的图展平是 150,528 维。接一个 1000 维全连接层 = 1.5 亿参数,且丧失了空间结构(把相邻像素打散了)。

CNN 的三个先验(归纳偏置):

先验含义实现
局部性相邻像素相关卷积核只看小窗口
平移不变性猫在左上或右下都是猫卷积核在整图滑动,权重共享
层次性边缘→纹理→部件→物体多层堆叠 + 池化降采样

卷积到底在算什么 ​

一个 3×3 的卷积核在图上滑动,每个位置做一次「点乘再求和」,输出一个特征图。

python
import torch.nn as nn

conv = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, padding=1, stride=1)
x = torch.randn(8, 3, 224, 224)
print(conv(x).shape)      # (8, 64, 224, 224)

输出尺寸公式(必背) ​

$$H_{out} = \left\lfloor \frac{H_{in} + 2P - K}{S} \right\rfloor + 1$$

参数含义常用
K (kernel)核大小3(配 padding=1 保持尺寸)
S (stride)步长1(保尺寸),2(下采样一半)
P (padding)边缘填充K//2 保持尺寸

快速记忆:K=3, P=1, S=1 → 尺寸不变;S=2 → 尺寸减半。

一个卷积块的标配结构 ​

Conv → BatchNorm → ReLU → (可选 MaxPool)
python
def conv_block(cin, cout, stride=1):
    return nn.Sequential(
        nn.Conv2d(cin, cout, 3, stride, padding=1, bias=False),
        nn.BatchNorm2d(cout),          # 有 BN 就不要 bias
        nn.ReLU(inplace=True),
    )
组件作用注意
BatchNorm稳定训练、加速收敛小 batch(<8)时改用 GroupNorm / LayerNorm
ReLU非线性inplace=True 省显存
MaxPool降采样、扩感受野现代网络更多用 stride=2 卷积替代
1×1 卷积跨通道融合、升降维极省算力,ResNet/Inception 常用

从零写一个 CNN ​

python
import torch, torch.nn as nn

class SmallCNN(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.features = nn.Sequential(
            conv_block(3, 32), conv_block(32, 64), nn.MaxPool2d(2),   # 32→16
            conv_block(64, 128), conv_block(128, 128), nn.MaxPool2d(2), # 16→8
            conv_block(128, 256), nn.AdaptiveAvgPool2d(1),            # 任意尺寸→1
        )
        self.classifier = nn.Sequential(
            nn.Flatten(), nn.Dropout(0.3), nn.Linear(256, num_classes))

    def forward(self, x):
        return self.classifier(self.features(x))

print(SmallCNN())

关键技巧:AdaptiveAvgPool2d(1) 把任意 H×W 压成 1×1,这样网络能接受任意输入尺寸,也让分类头参数量与输入尺寸无关。

迁移学习:实际项目的正确起点 ​

从零训练几乎永远不是最优选择。 用 ImageNet 预训练权重,只训分类头。

python
from torchvision import models

def build_model(num_classes, freeze_backbone=True):
    model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1)
    if freeze_backbone:
        for p in model.parameters():
            p.requires_grad = False
    model.fc = nn.Linear(model.fc.in_features, num_classes)   # 换头
    return model.to(device)

# 阶段一:只训头(lr=1e-3, 5 epochs)
# 阶段二:解冻全部,小 lr 微调(lr=1e-4 ~ 1e-5, 再加 5 epochs)
数据量策略
< 1k冻结主干,只训头 + 强增强
1k–50k冻结→解冻两阶段微调
> 100k可全参数训练,或从零训

预处理必须与预训练一致

用 ImageNet 权重就要用 ImageNet 的归一化: Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225]) 用错的话,精度会莫名低 10 个点,且很难查出原因。

ResNet:为什么残差能训深 ​

$$y = F(x) + x$$

  • 梯度可以直接沿 +x 这条捷径回流,缓解梯度消失
  • 学「残差」比学「完整映射」更容易(接近 0 比接近恒等简单)
python
class Residual(nn.Module):
    def __init__(self, c):
        super().__init__()
        self.block = nn.Sequential(conv_block(c, c), conv_block(c, c))
    def forward(self, x):
        return x + self.block(x)          # 尺寸必须一致

这是理解 Transformer 残差连接的同一个东西。 记住这个结构,L4 会反复用到。

视觉任务全景 ​

任务输出代表模型
图像分类一个类别ResNet、EfficientNet、ConvNeXt、ViT
目标检测框 + 类别YOLO 系列、Faster R-CNN、DETR
语义分割每像素类别U-Net、DeepLab、SegFormer
实例分割每个实例的掩码Mask R-CNN、SAM
关键点坐标HRNet、MediaPipe
图文匹配跨模态向量CLIP

现代趋势

ViT(Vision Transformer)在很多任务上已超过 CNN,但它需要更多数据或更强的增强。小数据集上,CNN(尤其 ConvNeXt / EfficientNet)依然更划算。 无论哪种,都用预训练权重。

数据增强:视觉任务的免费午餐 ​

python
import torchvision.transforms as T

train_tf = T.Compose([
    T.RandomResizedCrop(224, scale=(0.6, 1.0)),
    T.RandomHorizontalFlip(),
    T.TrivialAugmentWide(),          # 自动增强,省去手调
    T.ToTensor(),
    T.Normalize([0.485,0.456,0.406], [0.229,0.224,0.225]),
    T.RandomErasing(p=0.25),         # 随机遮挡,抗遮挡
])

val_tf = T.Compose([
    T.Resize(256), T.CenterCrop(224), T.ToTensor(),
    T.Normalize([0.485,0.456,0.406], [0.229,0.224,0.225]),
])
增强适用慎用
水平翻转通用文字、有方向性的医学影像
随机裁剪缩放通用—
色彩抖动通用颜色是判别特征时(如红绿灯)
旋转遥感、显微自然图像(会变形)
Mixup / CutMix大数据、追求 SOTA小数据可能反而变差

排查:模型效果不好怎么查 ​

步骤检查
1用 1 个 batch 能否过拟合到 100%?(见 训练)
2可视化 16 张训练样本的增强结果 —— 增强是否破坏语义
3类别是否失衡?看混淆矩阵
4归一化是否与预训练一致?
5学习率是否过大(看 loss 是否先降后爆)
6是否忘了 model.train()/eval()
7验证集是否太小导致指标抖动

通过标准 ​

下一步 ​

→ 序列建模:RNN / LSTM 到注意力

AI 学习路线 · 从入门到专家