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 | 验证集是否太小导致指标抖动 |