卷积神经网络 (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 其他重要网络
| 网络 | 年份 | 核心创新 | 参数量 |
|---|---|---|---|
| Inception | 2014 | 多尺度卷积并行 | 6.8M |
| ResNet | 2015 | 残差连接 | 25.6M (ResNet-50) |
| DenseNet | 2017 | 密集连接,特征复用 | 8.0M |
| MobileNet | 2017 | 深度可分离卷积 | 4.2M |
| EfficientNet | 2019 | 复合缩放 (深度/宽度/分辨率) | 5.3M (B0) |
| ConvNeXt | 2022 | 纯 CNN 追赶 Transformer | 89M |
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 | 单阶段 | 中 | 高 | 均衡 |
| DETR | Transformer | 中 | 高 | 端到端简洁 |
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 的发展脉络:
- LeNet → 证明 CNN 可行
- AlexNet → ReLU + GPU + 大数据推动深度学习爆发
- VGG → 小卷积核堆叠,深度即力量
- ResNet → 残差连接解决退化,网络可达 1000+ 层
- EfficientNet → 复合缩放,效率最优
- 自动化设计 → NAS、AutoML 搜索最优架构
工程实践要点:
- 数据增强比调参更重要,AutoAugment/Mixup/CutMix 显著提效
- 迁移学习是绝大多数场景的首选,ImageNet 预训练权重是宝贵资源
- 批归一化加速收敛,残差连接让深层网络可训练
- 检测任务按实时性需求选 YOLO(快)或 Faster R-CNN(准)
- 分割用 U-Net 系列或 Mask R-CNN
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。