1. 预训练模型在CIFAR-10上的迁移学习实践
在计算机视觉领域,预训练模型的迁移学习已经成为解决小规模数据集问题的标准方法。CIFAR-10作为经典的32x32小尺寸图像分类基准,为我们提供了验证不同预训练模型效果的理想平台。这次我尝试了ResNet18架构,并深入探索了其内部结构和训练策略。
1.1 项目环境与数据准备
首先需要搭建完整的PyTorch实验环境。我推荐使用conda创建独立的Python环境:
bash复制conda create -n pytorch_env python=3.8
conda activate pytorch_env
pip install torch torchvision matplotlib
数据预处理是影响模型性能的关键因素。对于CIFAR-10这种小尺寸图像,我设计了以下增强策略:
python复制train_transform = transforms.Compose([
transforms.RandomCrop(32, padding=4), # 随机裁剪保留边缘信息
transforms.RandomHorizontalFlip(), # 水平翻转增加数据多样性
transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1), # 颜色扰动
transforms.RandomRotation(15), # 小幅旋转增强
transforms.ToTensor(),
transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)) # CIFAR-10专用归一化
])
注意:测试集只需进行简单的归一化,不应包含任何数据增强操作,否则会干扰模型评估的准确性。
1.2 ResNet18模型架构解析
ResNet的核心创新在于残差连接(Residual Connection),解决了深层网络梯度消失的问题。通过PyTorch的torchvision.models可以方便地加载预训练权重:
python复制from torchvision.models import resnet18
model = resnet18(pretrained=True)
print(model) # 查看完整模型结构
关键改进点在于修改最后的全连接层,适配CIFAR-10的10分类任务:
python复制in_features = model.fc.in_features
model.fc = nn.Linear(in_features, 10) # 替换最后的分类层
残差块的结构可以通过以下代码深入观察:
python复制# 查看第一个残差块的结构
print(model.layer1[0])
"""
BasicBlock(
(conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
"""
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 迁移学习策略与训练技巧
2.1 分阶段训练策略
直接微调所有层在小数据集上容易导致过拟合。我采用分阶段训练策略:
- 冻结阶段(前5个epoch):仅训练最后的全连接层
- 微调阶段:解冻所有层,使用更小的学习率精细调整
实现代码如下:
python复制def freeze_model(model, freeze=True):
for name, param in model.named_parameters():
if 'fc' not in name: # 非全连接层
param.requires_grad = not freeze
return model
2.2 优化器与学习率调度
Adam优化器结合ReduceLROnPlateau调度器能有效平衡收敛速度和最终性能:
python复制optimizer = optim.Adam([
{'params': [p for n,p in model.named_parameters() if 'fc' in n], 'lr': 1e-3},
{'params': [p for n,p in model.named_parameters() if 'fc' not in n], 'lr': 1e-4}
], weight_decay=1e-4)
scheduler = optim.lr_scheduler.ReduceLROnPlateau(
optimizer, mode='min', factor=0.5, patience=2, verbose=True
)
实操技巧:初始阶段全连接层使用较大学习率(1e-3),卷积层使用较小学习率(1e-4),避免破坏预训练获得的良好特征。
2.3 训练过程监控
完善的训练监控需要记录多个指标:
python复制def train(...):
train_loss_history = []
test_loss_history = []
train_acc_history = []
test_acc_history = []
all_iter_losses = []
for epoch in range(epochs):
# 训练循环
for batch_idx, (data, target) in enumerate(train_loader):
# 前向传播、反向传播
...
all_iter_losses.append(loss.item())
# 测试集评估
model.eval()
with torch.no_grad():
for data, target in test_loader:
...
# 记录指标
train_loss_history.append(epoch_train_loss)
test_loss_history.append(epoch_test_loss)
train_acc_history.append(epoch_train_acc)
test_acc_history.append(epoch_test_acc)
3. 实验结果与分析
3.1 训练曲线解读
通过matplotlib可以绘制两种关键曲线:
- Iteration级损失曲线:反映模型在每个batch上的即时表现
python复制plt.plot(indices, losses, 'b-', alpha=0.7)
plt.xlabel('Iteration')
plt.ylabel('Loss')
- Epoch级指标曲线:展示整体训练趋势
python复制plt.subplot(1,2,1)
plt.plot(epochs, train_acc, 'b-', label='Train')
plt.plot(epochs, test_acc, 'r-', label='Test')
plt.subplot(1,2,2)
plt.plot(epochs, train_loss, 'b-', label='Train')
plt.plot(epochs, test_loss, 'r-', label='Test')
典型现象分析:
- 训练初期测试准确率快速上升,表明模型在快速学习通用特征
- 约10个epoch后进入平台期,需要降低学习率继续优化
- 理想情况下训练和测试曲线应保持接近,过大差距预示过拟合
3.2 残差连接的作用验证
为验证残差连接的效果,我对比了普通CNN和ResNet的训练过程:
| 模型类型 | 测试准确率 | 训练稳定性 |
|---|---|---|
| Plain CNN | 78.2% | 容易出现梯度消失 |
| ResNet18 | 92.6% | 训练曲线平滑 |
残差连接使得:
- 梯度可以直接回传到浅层
- 网络可以设计得更深
- 训练过程更加稳定
4. 常见问题与解决方案
4.1 过拟合问题
症状:训练准确率持续上升但测试准确率停滞
解决方案:
- 增加数据增强强度
- 提前停止训练(Early Stopping)
- 增加权重衰减系数
- 减少可训练参数(延长冻结期)
4.2 训练不收敛
可能原因:
- 学习率设置不当
- 梯度爆炸/消失
- 数据预处理错误
排查步骤:
python复制# 检查数据范围
print("输入数据范围:", torch.min(data), torch.max(data))
# 检查梯度
for name, param in model.named_parameters():
if param.grad is not None:
print(f"{name}梯度均值:", torch.mean(param.grad).item())
4.3 显存不足
小批量训练技巧:
python复制# 梯度累积
accum_steps = 4
optimizer.zero_grad()
for i, (data, target) in enumerate(train_loader):
output = model(data)
loss = criterion(output, target)/accum_steps
loss.backward()
if (i+1)%accum_steps == 0:
optimizer.step()
optimizer.zero_grad()
5. 模型部署与推理优化
训练完成后,可以通过以下方式优化推理速度:
- 模型量化:
python复制quantized_model = torch.quantization.quantize_dynamic(
model, {nn.Linear}, dtype=torch.qint8
)
- TorchScript导出:
python复制traced_script = torch.jit.trace(model, torch.rand(1,3,32,32).to(device))
traced_script.save('resnet18_cifar10.pt')
- ONNX转换:
python复制torch.onnx.export(model, dummy_input, "model.onnx",
input_names=["input"], output_names=["output"])
在实际部署中发现,量化后的模型大小减少约4倍,推理速度提升2-3倍,而准确率仅下降约1%。
6. 扩展实验与对比研究
6.1 不同预训练模型对比
在相同条件下测试了多种模型:
| 模型 | 参数量 | 测试准确率 | 训练时间(epoch) |
|---|---|---|---|
| ResNet18 | 11M | 92.6% | 40 |
| ResNet50 | 25M | 93.1% | 50 |
| EfficientNet-B0 | 5.3M | 91.8% | 35 |
| MobileNetV2 | 3.4M | 90.2% | 30 |
选择建议:
- 高精度场景:ResNet50
- 边缘设备:MobileNetV2
- 平衡型:ResNet18
6.2 消融实验
验证各技术组件的贡献:
| 配置 | 测试准确率 |
|---|---|
| 基础训练 | 85.3% |
| +数据增强 | 88.7% |
| +分阶段训练 | 90.5% |
| +学习率调度 | 92.6% |
实验表明,合理的训练策略比单纯增加模型复杂度更有效。
