CIFAR-10图像分类避坑指南:用PyTorch复现VGG-16时,我踩过的那些坑
CIFAR-10图像分类避坑指南:用PyTorch复现VGG-16时,我踩过的那些坑
第一次在CIFAR-10数据集上复现VGG-16时,我本以为照着论文和教程就能轻松实现90%以上的准确率。但现实给了我一记响亮的耳光——从数据预处理到模型训练,几乎每个环节都藏着意想不到的陷阱。这篇文章不会给你展示最终完美的代码,而是带你重走我踩过的那些坑,分享那些让我抓狂又恍然大悟的瞬间。
1. 数据预处理:你以为的增强可能是在帮倒忙
数据增强是提升模型泛化能力的利器,但用错了反而会让准确率不升反降。我最开始照搬ImageNet的那套增强策略,结果在CIFAR-10上栽了大跟头。
1.1 尺寸调整的陷阱
CIFAR-10的图片只有32x32像素,而VGG-16原本是为224x224设计的。直接套用会导致:
# 错误示范:直接使用大尺寸的padding transforms.Pad(224) # 这会引入大量无效信息正确的做法是保持小尺寸增强:
transform_train = transforms.Compose([ transforms.RandomCrop(32, padding=4), # 小幅随机裁剪 transforms.RandomHorizontalFlip(), # 水平翻转 transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.247, 0.243, 0.261)) # CIFAR专用均值方差 ])1.2 颜色增强的误区
我尝试过以下增强组合,结果准确率下降了3%:
# 错误组合: transforms.ColorJitter(brightness=0.5, contrast=0.5, saturation=0.5, hue=0.5), transforms.RandomGrayscale(p=0.5)后来发现,对于CIFAR-10这种低分辨率数据集,简单的水平翻转+小幅裁剪反而是最有效的。下表对比了不同增强策略的效果:
| 增强组合 | 测试准确率 | 训练时间 |
|---|---|---|
| 无增强 | 89.2% | 2.1小时 |
| 过度增强 | 86.5% | 2.8小时 |
| 适度增强 | 91.3% | 2.3小时 |
提示:CIFAR-10的图片已经很小,过度增强会破坏原有特征。建议先用最小增强集,再逐步测试其他方法。
2. 模型结构调整:别让VGG在CIFAR上"水土不服"
VGG-16原本是为ImageNet设计的,直接移植到CIFAR-10会出现几个典型问题。
2.1 通道数的玄学
原版VGG第一层是64通道,但在CIFAR-10上我发现:
- 64通道:准确率89.7%
- 96通道:准确率90.9%
- 128通道:准确率89.1%
# 修改后的通道配置 vgg_config = [96, 96, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M']2.2 全连接层的过拟合陷阱
原版VGG有三个全连接层,这在CIFAR-10上简直是过拟合的温床。我的解决方案:
- 减少中间层维度(4096→1024)
- 调整Dropout率(0.5→0.4)
- 添加BatchNorm
self.classifier = nn.Sequential( nn.Linear(512, 1024), nn.BatchNorm1d(1024), nn.ReLU(inplace=True), nn.Dropout(0.4), nn.Linear(1024, 10) )3. 训练过程中的那些"坑"
3.1 Batch Size的平衡术
最开始我设batch_size=4,结果:
- 训练时间:8小时/epoch
- 准确率:87.3%
调整到batch_size=128后:
- 训练时间:35分钟/epoch
- 准确率:90.1%
但batch_size也不是越大越好,超过256后准确率开始下降。下表是我的测试数据:
| Batch Size | 训练时间/epoch | 最终准确率 | GPU显存占用 |
|---|---|---|---|
| 4 | 8小时 | 87.3% | 2GB |
| 32 | 1.5小时 | 89.8% | 5GB |
| 128 | 35分钟 | 90.1% | 9GB |
| 256 | 25分钟 | 89.5% | 爆显存 |
3.2 优化器的选择困境
我对比了三种优化器的表现:
# SGD表现最好 optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=5e-4) # Adam初期收敛快但后期波动大 optimizer = optim.Adam(model.parameters(), lr=0.001) # RMSprop居中 optimizer = optim.RMSprop(model.parameters(), lr=0.01, alpha=0.99)实际训练曲线显示:
- Adam在前5个epoch领先
- 10个epoch后SGD反超
- 最终SGD比Adam高1.5%准确率
3.3 学习率调整的艺术
我尝试了三种调度策略:
# 等间隔调整(最终采用) scheduler = StepLR(optimizer, step_size=5, gamma=0.5) # 余弦退火 scheduler = CosineAnnealingLR(optimizer, T_max=10) # 预热+余弦 scheduler = CosineAnnealingWarmRestarts(optimizer, T_0=5, T_mult=1)注意:VGG对学习率非常敏感,建议初始值不要超过0.01,并在验证集准确率停滞时手动调整。
4. 那些容易被忽视的细节
4.1 权重初始化的影响
不使用初始化时,模型有时会完全学不动。我对比了几种方法:
# He初始化效果最好 for m in model.modules(): if isinstance(m, nn.Conv2d): nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu') elif isinstance(m, nn.BatchNorm2d): nn.init.constant_(m.weight, 1) nn.init.constant_(m.bias, 0)4.2 早停策略的误用
我最初设定了严格的早停规则(连续3次不提升就停止),结果:
- 错过了后期学习率调整带来的提升
- 最佳模型出现在第25个epoch,而早停在18epoch就触发了
改进后的策略:
# 更宽松的早停条件 if best_acc < current_acc: best_acc = current_acc patience = 0 torch.save(model.state_dict(), 'best.pth') else: patience += 1 if patience >= 10: # 放宽到10次 break4.3 梯度裁剪的意外收获
在训练后期添加梯度裁剪后,准确率提升了0.8%:
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=2.0)这个技巧特别适合当batch_size较大时,能有效防止梯度爆炸。
