ResNet 图像分类:残差网络与迁移学习
ResNet 图像分类入门:从残差学习到迁移实战
ResNet(残差网络)是图像分类史上里程碑式的架构。它不仅以惊人的深度(152层)赢得了 ILSVRC 2015 冠军,更通过简单优雅的残差连接解决了深层网络难以训练的核心痛点。本教程将带你从零理解 ResNet 的原理,并基于预训练模型快速落地图像分类任务——即使你只有基础的 Python 和深度学习知识。
1. 为什么需要 ResNet?深度网络的退化问题
直觉上,网络越深,能提取的特征层次越丰富,性能应该越好。但实验发现:在适当深度的模型上继续堆叠更多层,训练误差和测试误差不降反升——这不是过拟合(因为训练误差也变差了),而是网络退化(Degradation)。
深层网络难以训练的根本原因在于梯度消失/爆炸的加剧以及恒等映射的困难。ResNet 的核心思想是:如果额外增加的层只需实现恒等映射(即输出等于输入),那么深层网络的表现至少不会比浅层更差。基于此,它设计出残差块,让网络去学习残差,而不是直接学习完整映射。
2. 残差块:让网络学习“扰动”
一个标准的残差块包含两条路径:
- 主路径:卷积层 → 批量归一化(BN)→ 激活(ReLU)→ 卷积层 → BN
- 捷径(Shortcut):直接将输入跳跃连接到输出
最后将两条路径的结果相加,再经过 ReLU 激活。数学表示为:
[ y = \mathcal{F}(x, {W_i}) + x ]
这里 (\mathcal{F}) 就是残差函数(需要学习的部分)。当 (\mathcal{F}(x)=0) 时,残差块退化为恒等映射。网络只需要学习输出与输入的微小差异,这让深层网络的优化变得极其稳定。
当输入输出维度不匹配时(例如经过下采样),捷径需要通过 1×1 卷积调整维度,并可能改变步长:
[ y = \mathcal{F}(x, {W_i}) + W_s x ]
3. ResNet 经典架构一览
ResNet 家族的主要变体按层数划分:18、34、50、101、152。其中 ResNet-50 因其在精度和计算效率之间的出色平衡而成为工业界和学术界最常用的骨干网络。
所有变体遵循统一的五阶段结构:
| 阶段 | 输出尺寸 | ResNet-50 结构 |
|---|---|---|
| conv1 | 112×112 | 7×7 卷积,步长 2,64 通道 |
| conv2_x | 56×56 | 3 个瓶颈残差块(通道数 256) |
| conv3_x | 28×28 | 4 个瓶颈残差块(通道数 512) |
| conv4_x | 14×14 | 6 个瓶颈残差块(通道数 1024) |
| conv5_x | 7×7 | 3 个瓶颈残差块(通道数 2048) |
| 分类层 | 1×1 | 全局平均池化 + 全连接 + softmax |
**瓶颈块(Bottleneck)**专为深层网络设计(ResNet-50 及以上),使用了三层卷积:1×1 降维 → 3×3 卷积 → 1×1 升维。这种设计显著减少了参数数量和计算量,同时保持了表达能力。
ResNet-18/34 则采用两层 3×3 卷积的基本残差块,适用于更轻量级的任务。
4. 迁移学习:站在巨人的肩膀上
从头训练一个 ResNet 需要海量数据(如 ImageNet 的百万级图像)和大量计算资源。迁移学习允许我们利用在 ImageNet 上预训练好的模型,将其强大的特征提取能力迁移到自己的小样本任务上。
两种常见策略:
- 特征提取:冻结卷积基(除最后几层外),仅训练新添加的分类层。
- 微调(Fine-tuning):解冻部分高层卷积块,以很小的学习率与分类层一起训练,使特征更贴合目标数据。
对于大多数自定义图像分类任务,微调预训练 ResNet-50 通常能以数百张图片取得优异效果。
5. 实战:用 Keras 实现 ResNet-50 迁移学习
我们将使用 TensorFlow / Keras 框架,快速构建一个猫狗二分类器。请确保已安装 tensorflow。
5.1 数据准备
假设图像按 train/ 和 validation/ 目录组织,每类一个子文件夹。使用 ImageDataGenerator 进行数据增强和归一化:
from tensorflow.keras.preprocessing.image import ImageDataGenerator
train_datagen = ImageDataGenerator(
rescale=1./255,
rotation_range=20,
width_shift_range=0.2,
height_shift_range=0.2,
horizontal_flip=True
)
train_generator = train_datagen.flow_from_directory(
'data/train',
target_size=(224, 224),
batch_size=32,
class_mode='binary'
)
val_datagen = ImageDataGenerator(rescale=1./255)
validation_generator = val_datagen.flow_from_directory(
'data/validation',
target_size=(224, 224),
batch_size=32,
class_mode='binary'
)
5.2 构建模型
加载不包含顶层的 ResNet-50 预训练权重,添加全局平均池化和新的分类层:
from tensorflow.keras.applications import ResNet50
from tensorflow.keras import layers, models
base_model = ResNet50(weights='imagenet', include_top=False, input_shape=(224, 224, 3))
# 冻结基础模型的所有层
base_model.trainable = False
model = models.Sequential([
base_model,
layers.GlobalAveragePooling2D(),
layers.Dense(256, activation='relu'),
layers.Dropout(0.5),
layers.Dense(1, activation='sigmoid') # 二分类
])
model.compile(optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy'])
model.summary()
5.3 训练模型
history = model.fit(
train_generator,
steps_per_epoch=train_generator.samples // 32,
epochs=10,
validation_data=validation_generator,
validation_steps=validation_generator.samples // 32
)
经过数轮训练后,验证准确率通常能达到 95% 以上(取决于数据量)。
5.4 进阶微调
如果希望进一步提升性能,可以解冻 conv5_x 之后的层,以极低的学习率继续训练:
# 解冻 base_model 的最后三个残差块
base_model.trainable = True
for layer in base_model.layers[:143]: # 根据实际情况调整
layer.trainable = False
model.compile(optimizer=tf.keras.optimizers.Adam(1e-5), # 极低学习率
loss='binary_crossentropy',
metrics=['accuracy'])
history_fine = model.fit(
train_generator,
steps_per_epoch=train_generator.samples // 32,
epochs=5,
validation_data=validation_generator,
validation_steps=validation_generator.samples // 32
)
6. 用 PyTorch 实现 ResNet-50 迁移学习
对于 PyTorch 用户,以下是等价的袖珍实现:
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import models, transforms, datasets
# 设备配置
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# 数据预处理
data_transforms = {
'train': transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
'val': transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
]),
}
image_datasets = {x: datasets.ImageFolder(f'data/{x}', data_transforms[x])
for x in ['train', 'val']}
dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=32, shuffle=True)
for x in ['train', 'val']}
# 加载预训练 ResNet-50 并修改最后全连接层
model = models.resnet50(pretrained=True)
num_ftrs = model.fc.in_features
model.fc = nn.Linear(num_ftrs, 1) # 二分类输出 1 个logit
model = model.to(device)
criterion = nn.BCEWithLogitsLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-4)
# 训练代码略(典型循环:forward, loss, backward, step)
使用 PyTorch 时,通常默认微调全部参数,但也可以选择性冻结卷积层。
7. 关键技巧与常见问题
- 输入尺寸:ResNet 系列通常期望 224×224 的输入。不要随意修改,否则预训练权重的语义会被破坏。
- 归一化:必须使用与训练 ImageNet 时相同的均值和标准差(Keras 的
preprocess_input或 PyTorch 的归一化参数)。 - 学习率:微调时学习率应为初始训练的 1/10 或更低,避免破坏预训练特征。
- 类别不平衡:使用
class_weight参数或在损失函数中加权。 - 避免过拟合:数据增强、Dropout、早停(EarlyStopping)是标配。
8. 总结与进阶方向
ResNet 通过残差连接让训练数百层网络成为可能,其预训练模型更是迁移学习的利器。你现在可以:
- 替换自己的数据集,复制上述代码即可运行分类任务。
- 将 ResNet 作为特征提取器,结合 SVM 或 XGBoost 等传统分类器。
- 探索其他变体:ResNeXt(分组卷积)、ResNet-RS(改进训练策略)、Wide ResNet(增加宽度)。
- 将 ResNet 延伸到目标检测(Faster R-CNN 骨干)或语义分割(DeepLab 骨干)。
残差思想已成为现代深度网络设计的 DNA,掌握了它,你就拥有了打开复杂视觉任务之门的钥匙。