05. 卷积神经网络 CNN

CNN 卷积神经网络全解析:卷积/池化/全连接、经典网络 LeNet 到 EfficientNet、目标检测基础、数据增强与迁移学习

卷积神经网络 (CNN) 是计算机视觉领域的核心架构,通过局部连接和权重共享高效提取图像特征。本文从卷积操作出发,梳理经典网络演进,覆盖目标检测基础与迁移学习。

1. 卷积操作

1.1 二维卷积

卷积核在输入特征图上滑动,计算点积:

输出尺寸 = ⌊(输入尺寸 - 核尺寸 + 2×填充) / 步幅⌋ + 1
import torch
import torch.nn as nn

# 输入: (batch_size, channels, height, width)
x = torch.randn(1, 3, 32, 32)  # 1 张 3 通道 32x32 图像

# 卷积层: in=3, out=16, kernel=3x3, padding=1, stride=1
conv = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, 
                 padding=1, stride=1)
out = conv(x)
print(out.shape)  # torch.Size([1, 16, 32, 32])

# 卷积层参数
print(f"卷积核: {conv.weight.shape}")  # [16, 3, 3, 3] (out, in, kH, kW)
print(f"参数量: {sum(p.numel() for p in conv.parameters())}")
# 16*3*3*3 + 16 = 448  (权重 + 偏置)

1.2 感受野

感受野表示输出特征图上某位置对应输入图像的区域大小。

# 计算感受野
def receptive_field(kernel_sizes, strides):
    """计算连续卷积层的感受野"""
    rf = 1
    for k, s in zip(reversed(kernel_sizes), reversed(strides)):
        rf = rf * s + (k - s)
    return rf

# 3 层 3x3 卷积,stride=1
print(receptive_field([3, 3, 3], [1, 1, 1]))  # 7
# 等价于 1 层 7x7 卷积,但参数量: 3*3^2 vs 1*7^2  (27 vs 49)

用多层小卷积核替代大卷积核:参数更少、非线性更多、感受野相同。

1.3 空洞卷积 (Dilated Convolution)

在不增加参数的情况下扩大感受野,用于语义分割。

# dilation=2,3x3 卷积核有 5x5 的感受野
dilated_conv = nn.Conv2d(3, 16, kernel_size=3, padding=2, dilation=2)
out = dilated_conv(x)  # 32x32 保持尺寸

1.4 转置卷积 (Transposed Conv)

用于上采样(如分割中的解码器、生成对抗网络)。

# 上采样: 16x16 → 32x32
conv_t = nn.ConvTranspose2d(16, 3, kernel_size=4, stride=2, padding=1)
x_small = torch.randn(1, 16, 16, 16)
out = conv_t(x_small)
print(out.shape)  # [1, 3, 32, 32]

2. 池化与归一化

2.1 池化层

# 最大池化:保留最显著特征
maxpool = nn.MaxPool2d(kernel_size=2, stride=2)
x = torch.randn(1, 16, 32, 32)
out = maxpool(x)
print(out.shape)  # [1, 16, 16, 16]

# 平均池化:保留背景信息
avgpool = nn.AvgPool2d(kernel_size=2, stride=2)

# 全局平均池化 (GAP):替代全连接层,减少参数
# 应用于分类末层,HxW → 1x1,每个通道一个值
gap = nn.AdaptiveAvgPool2d((1, 1))
out = gap(x)      # [1, 16, 1, 1]
out = out.view(1, -1)  # [1, 16]

2.2 批归一化 (BatchNorm)

# 卷积层后接 BatchNorm → ReLU
self.conv_block = nn.Sequential(
    nn.Conv2d(64, 128, 3, padding=1),
    nn.BatchNorm2d(128),   # 通道维归一化
    nn.ReLU(inplace=True)
)

注意:BatchNorm 对 batch size 敏感,batch size 较小时(如 1-2)用 GroupNorm 替代。

3. 经典网络演进

3.1 LeNet-5 (1998)

首个成功的 CNN,手写数字识别。

class LeNet(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(1, 6, kernel_size=5),   # 28x28
            nn.Tanh(),
            nn.AvgPool2d(kernel_size=2),       # 14x14
            nn.Conv2d(6, 16, kernel_size=5),   # 10x10
            nn.Tanh(),
            nn.AvgPool2d(kernel_size=2),       # 5x5
        )
        self.classifier = nn.Sequential(
            nn.Linear(16*5*5, 120),
            nn.Tanh(),
            nn.Linear(120, 84),
            nn.Tanh(),
            nn.Linear(84, num_classes)
        )
    
    def forward(self, x):
        x = self.features(x)
        x = x.view(x.size(0), -1)
        x = self.classifier(x)
        return x

3.2 AlexNet (2012)

深度学习爆发的里程碑:ReLU、Dropout、GPU 并行。

Conv1(96, 11x11) → MaxPool → Conv2(256, 5x5) → MaxPool
→ Conv3(384, 3x3) → Conv4(384, 3x3) → Conv5(256, 3x3)
→ MaxPool → FC(4096) → FC(4096) → FC(1000)

3.3 VGGNet (2014)

核心思想:用多个 3x3 小卷积替代大卷积核。

class VGG16(nn.Module):
    def __init__(self, num_classes=1000):
        super().__init__()
        self.features = nn.Sequential(
            # Block 1
            nn.Conv2d(3, 64, 3, padding=1), nn.ReLU(),
            nn.Conv2d(64, 64, 3, padding=1), nn.ReLU(),
            nn.MaxPool2d(2),
            # Block 2
            nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(),
            nn.Conv2d(128, 128, 3, padding=1), nn.ReLU(),
            nn.MaxPool2d(2),
            # Block 3 (3 conv)
            nn.Conv2d(128, 256, 3, padding=1), nn.ReLU(),
            nn.Conv2d(256, 256, 3, padding=1), nn.ReLU(),
            nn.Conv2d(256, 256, 3, padding=1), nn.ReLU(),
            nn.MaxPool2d(2),
            # Block 4 (3 conv)
            nn.Conv2d(256, 512, 3, padding=1), nn.ReLU(),
            nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(),
            nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(),
            nn.MaxPool2d(2),
            # Block 5 (3 conv)
            nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(),
            nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(),
            nn.Conv2d(512, 512, 3, padding=1), nn.ReLU(),
            nn.MaxPool2d(2),
        )
        self.avgpool = nn.AdaptiveAvgPool2d((7, 7))
        self.classifier = nn.Sequential(
            nn.Linear(512*7*7, 4096), nn.ReLU(), nn.Dropout(0.5),
            nn.Linear(4096, 4096), nn.ReLU(), nn.Dropout(0.5),
            nn.Linear(4096, num_classes)
        )

3.4 ResNet (2015):残差连接

核心突破:残差块解决深层网络退化问题。

class BasicBlock(nn.Module):
    expansion = 1
    
    def __init__(self, in_channels, out_channels, stride=1):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, 3, 
                               stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.conv2 = nn.Conv2d(out_channels, out_channels, 3, 
                               padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU(inplace=True)
        
        # shortcut 连接
        self.shortcut = nn.Sequential()
        if stride != 1 or in_channels != out_channels:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, out_channels, 1, 
                         stride=stride, bias=False),
                nn.BatchNorm2d(out_channels)
            )
    
    def forward(self, x):
        out = self.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        out += self.shortcut(x)  # 残差连接:F(x) + x
        out = self.relu(out)
        return out

# Bottleneck 块(ResNet-50/101/152)
class Bottleneck(nn.Module):
    expansion = 4
    
    def __init__(self, in_channels, out_channels, stride=1):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, 1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.conv2 = nn.Conv2d(out_channels, out_channels, 3, 
                               stride=stride, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.conv3 = nn.Conv2d(out_channels, out_channels * 4, 1, bias=False)
        self.bn3 = nn.BatchNorm2d(out_channels * 4)
        self.relu = nn.ReLU(inplace=True)
        
        self.shortcut = nn.Sequential()
        if stride != 1 or in_channels != out_channels * 4:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, out_channels * 4, 1, 
                         stride=stride, bias=False),
                nn.BatchNorm2d(out_channels * 4)
            )
    
    def forward(self, x):
        out = self.relu(self.bn1(self.conv1(x)))
        out = self.relu(self.bn2(self.conv2(out)))
        out = self.bn3(self.conv3(out))
        out += self.shortcut(x)
        out = self.relu(out)
        return out

3.5 其他重要网络

网络年份核心创新参数量
Inception2014多尺度卷积并行6.8M
ResNet2015残差连接25.6M (ResNet-50)
DenseNet2017密集连接,特征复用8.0M
MobileNet2017深度可分离卷积4.2M
EfficientNet2019复合缩放 (深度/宽度/分辨率)5.3M (B0)
ConvNeXt2022纯 CNN 追赶 Transformer89M

3.6 EfficientNet 复合缩放

from torchvision.models import efficientnet_b0

model = efficientnet_b0(pretrained=True)
model.classifier[1] = nn.Linear(model.classifier[1].in_features, num_classes)

# 复合缩放公式
# depth: d = α^φ, width: w = β^φ, resolution: r = γ^φ
# φ 是用户指定的缩放系数

4. 目标检测基础

4.1 检测任务类型

任务输出应用
目标检测边界框 (bbox) + 类别自动驾驶、安防
语义分割像素级类别医学影像、自动驾驶
实例分割像素级类别 + 实例区分机器人视觉
全景分割语义 + 实例场景理解

4.2 两阶段检测器 (Faster R-CNN)

图像 → Backbone (ResNet) → RPN (候选区域) → ROI Pooling → 分类 + 回归
from torchvision.models.detection import fasterrcnn_resnet50_fpn

model = fasterrcnn_resnet50_fpn(pretrained=True)
model.roi_heads.box_predictor.cls_score = nn.Linear(
    model.roi_heads.box_predictor.cls_score.in_features, num_classes)
model.roi_heads.box_predictor.bbox_pred = nn.Linear(
    model.roi_heads.box_predictor.bbox_pred.in_features, num_classes * 4)

4.3 单阶段检测器 (YOLO/SSD)

from torchvision.models.detection import ssd300_vgg16

# SSD:多尺度特征图上直接预测
model = ssd300_vgg16(pretrained=True)
检测器类型速度精度适用
Faster R-CNN两阶段高精度需求
YOLOv5/v8单阶段极快中高实时检测
RetinaNet单阶段均衡
DETRTransformer端到端简洁

5. 数据增强

5.1 基础变换

from torchvision import transforms

train_transform = transforms.Compose([
    transforms.RandomResizedCrop(224, scale=(0.8, 1.0)),
    transforms.RandomHorizontalFlip(p=0.5),
    transforms.RandomRotation(degrees=15),
    transforms.ColorJitter(brightness=0.2, contrast=0.2, 
                          saturation=0.2, hue=0.1),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406],
                        std=[0.229, 0.224, 0.225])
])

test_transform = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406],
                        std=[0.229, 0.224, 0.225])
])

5.2 高级增强

# AutoAugment: 学习最优增强策略
from torchvision.transforms import AutoAugment, AutoAugmentPolicy

transform = transforms.Compose([
    AutoAugment(policy=AutoAugmentPolicy.IMAGENET),
    transforms.ToTensor(),
    transforms.Normalize(...)
])

# Mixup: 图像与标签按比例混合
# x = λ*x1 + (1-λ)*x2, y = λ*y1 + (1-λ)*y2
# CutMix: 将一张图的区域替换为另一张图,标签按面积比例混合

6. 迁移学习

6.1 微调策略

import torchvision.models as models

# 加载预训练模型
model = models.resnet50(pretrained=True)

# 策略 1:冻结特征提取器,只训练分类头
for param in model.parameters():
    param.requires_grad = False

model.fc = nn.Linear(model.fc.in_features, num_classes)

# 策略 2:全部微调(数据充足时)
model = models.resnet50(pretrained=True)
model.fc = nn.Linear(model.fc.in_features, num_classes)
# 所有参数都 requires_grad=True(默认)

# 策略 3:分层微调(推荐)
# 前面层学习通用特征(边缘、纹理),冻结
# 后面层学习特定特征(零件、形状),微调
for name, param in model.named_parameters():
    if 'layer1' in name or 'layer2' in name:
        param.requires_grad = False
    else:
        param.requires_grad = True

6.2 不同场景的微调策略

数据量与预训练数据相似度策略
很少冻结卷积层,训练分类头
很少更难,可能需要数据增强或找相似域的预训练模型
中等只冻结前几层
中等全微调,较小学习率
很多任意从头训练或全微调

7. 语义分割基础

7.1 U-Net 架构

编码器-解码器 + Skip Connection,医学影像分割经典。

输入 → [Conv/Pool]x4 → 瓶颈 → [UpConv/Concat/Conv]x4 → 输出
                    ↓___________________________↑ Skip Connections
import segmentation_models_pytorch as smp

# 使用 smp 库快速构建
model = smp.Unet(
    encoder_name="resnet50",      # 编码器
    encoder_weights="imagenet",   # 预训练权重
    in_channels=3,
    classes=1,                    # 输出类别数
    activation="sigmoid"          # 二分类分割
)

8. 完整训练示例

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import models, datasets, transforms
from torch.utils.data import DataLoader

# 数据
transform = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
train_dataset = datasets.ImageFolder('data/train', transform=transform)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)

# 模型:ResNet18 微调
model = models.resnet18(pretrained=True)
model.fc = nn.Linear(model.fc.in_features, len(train_dataset.classes))
model = model.cuda()

# 分层学习率
optimizer = optim.Adam([
    {'params': model.fc.parameters(), 'lr': 1e-3},
    {'params': [p for n, p in model.named_parameters() 
                if 'fc' not in n], 'lr': 1e-4}
])

# 损失
weights = torch.tensor([1.0, 2.0]).cuda()  # 类别不平衡
criterion = nn.CrossEntropyLoss(weight=weights)

# 训练
for epoch in range(20):
    model.train()
    for images, labels in train_loader:
        images, labels = images.cuda(), labels.cuda()
        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

总结

CNN 的发展脉络:

  1. LeNet → 证明 CNN 可行
  2. AlexNet → ReLU + GPU + 大数据推动深度学习爆发
  3. VGG → 小卷积核堆叠,深度即力量
  4. ResNet → 残差连接解决退化,网络可达 1000+ 层
  5. EfficientNet → 复合缩放,效率最优
  6. 自动化设计 → NAS、AutoML 搜索最优架构

工程实践要点:

  • 数据增强比调参更重要,AutoAugment/Mixup/CutMix 显著提效
  • 迁移学习是绝大多数场景的首选,ImageNet 预训练权重是宝贵资源
  • 批归一化加速收敛,残差连接让深层网络可训练
  • 检测任务按实时性需求选 YOLO(快)或 Faster R-CNN(准)
  • 分割用 U-Net 系列或 Mask R-CNN

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「ai-ml」更多文章

  1. 13. 大语言模型应用开发
  2. 12. MLOps 与实验管理
  3. 11. 模型部署与推理优化