1. 项目概述
ResNet残差网络作为计算机视觉领域的里程碑式架构,在图像分类任务中展现出卓越性能。本次实战将基于PyTorch框架,利用ResNet预训练模型实现食物图像分类任务。不同于从零训练,迁移学习能大幅降低计算成本,即使在小样本数据集上也能获得理想效果。
食物分类作为计算机视觉的经典应用场景,在餐饮推荐、营养分析、智能厨房等领域具有广泛价值。传统方法需要人工设计特征,而深度学习方法能自动学习食物图像的层次化特征表示。ResNet凭借其独特的残差连接结构,有效解决了深层网络梯度消失问题,成为当前图像分类任务的首选架构之一。
2. 核心原理解析
2.1 ResNet架构精要
ResNet的核心创新在于残差块(Residual Block)设计。传统卷积神经网络随着深度增加会出现梯度消失问题,而ResNet通过引入跨层连接(Shortcut Connection)实现了恒等映射。具体实现包含两种基本模块:
基础残差块(BasicBlock):
- 两个3×3卷积层堆叠
- 每层后接BatchNorm和ReLU激活
- 输入输出维度相同时直接相加
瓶颈残差块(Bottleneck):
- 1×1卷积降维 → 3×3卷积 → 1×1卷积升维
- 计算量更小适合深层网络
- 典型用于ResNet50及以上版本
数学表达为:y = F(x, {Wi}) + x,其中F表示残差函数。当需要调整维度时,通过1×1卷积实现维度匹配:y = F(x, {Wi}) + Wsx。
2.2 迁移学习机制
迁移学习的有效性基于以下假设:
- 底层特征通用性:网络前几层学习的边缘、纹理等基础特征具有跨领域通用性
- 高层特征特异性:全连接层等高层网络学到的语义特征需要针对新任务调整
实践中有三种微调策略:
- 特征提取器:冻结所有卷积层,仅训练新添加的分类层
- 部分微调:解冻最后若干卷积层进行微调
- 完整微调:解冻全部网络层(需较大数据集)
3. 环境配置与数据准备
3.1 开发环境搭建
推荐使用Python 3.8+和PyTorch 1.10+环境:
conda create -n food_cls python=3.8 conda activate food_cls pip install torch torchvision torchaudio pip install pandas matplotlib tqdm3.2 数据集处理
使用Food-101数据集(包含101类食物,每类1000张图片)的子集:
数据目录结构:
food_dataset/ ├── train/ │ ├── pizza/ │ ├── sushi/ │ └── ... └── val/ ├── pizza/ ├── sushi/ └── ...数据增强策略:
from torchvision import transforms train_transform = transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) val_transform = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ])
4. 模型构建与训练
4.1 模型加载与改造
import torchvision.models as models def build_model(num_classes, pretrained=True): model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1) # 冻结所有卷积层 for param in model.parameters(): param.requires_grad = False # 替换最后一层全连接 in_features = model.fc.in_features model.fc = nn.Sequential( nn.Linear(in_features, 512), nn.ReLU(), nn.Dropout(0.5), nn.Linear(512, num_classes) ) return model4.2 训练流程实现
def train_model(model, dataloaders, criterion, optimizer, num_epochs=25): best_acc = 0.0 for epoch in range(num_epochs): for phase in ['train', 'val']: if phase == 'train': model.train() else: model.eval() running_loss = 0.0 running_corrects = 0 for inputs, labels in dataloaders[phase]: inputs = inputs.to(device) labels = labels.to(device) optimizer.zero_grad() with torch.set_grad_enabled(phase == 'train'): outputs = model(inputs) _, preds = torch.max(outputs, 1) loss = criterion(outputs, labels) if phase == 'train': loss.backward() optimizer.step() running_loss += loss.item() * inputs.size(0) running_corrects += torch.sum(preds == labels.data) epoch_loss = running_loss / len(dataloaders[phase].dataset) epoch_acc = running_corrects.double() / len(dataloaders[phase].dataset) print(f'{phase} Loss: {epoch_loss:.4f} Acc: {epoch_acc:.4f}') if phase == 'val' and epoch_acc > best_acc: best_acc = epoch_acc torch.save(model.state_dict(), 'best_model.pth') return model5. 优化技巧与调参经验
5.1 学习率策略
采用分层学习率设置:
- 新添加层:较高学习率(如0.001)
- 深层卷积层:中等学习率(如0.0001)
- 浅层卷积层:较低学习率(如0.00001)
使用余弦退火调度器:
from torch.optim.lr_scheduler import CosineAnnealingLR optimizer = torch.optim.Adam([ {'params': model.conv1.parameters(), 'lr': 1e-5}, {'params': model.layer1.parameters(), 'lr': 1e-5}, {'params': model.layer2.parameters(), 'lr': 1e-4}, {'params': model.layer3.parameters(), 'lr': 1e-4}, {'params': model.layer4.parameters(), 'lr': 1e-3}, {'params': model.fc.parameters(), 'lr': 1e-3} ], weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=num_epochs)5.2 类别不平衡处理
对于食物数据集中常见的类别不平衡问题:
- 过采样少数类
- 损失函数加权:
class_counts = [...] # 各类别样本数 class_weights = 1. / torch.tensor(class_counts, dtype=torch.float) criterion = nn.CrossEntropyLoss(weight=class_weights.to(device)) - 使用Focal Loss:
class FocalLoss(nn.Module): def __init__(self, alpha=1, gamma=2): super(FocalLoss, self).__init__() self.alpha = alpha self.gamma = gamma def forward(self, inputs, targets): BCE_loss = F.cross_entropy(inputs, targets, reduction='none') pt = torch.exp(-BCE_loss) F_loss = self.alpha * (1-pt)**self.gamma * BCE_loss return torch.mean(F_loss)
6. 模型评估与可视化
6.1 评估指标
除准确率外还应关注:
- 混淆矩阵:识别易混淆类别(如不同种类面条)
- 查准率/查全率:对关键类别单独评估
- ROC曲线:评估模型在不同阈值下的表现
6.2 特征可视化
使用Grad-CAM可视化关注区域:
from torchcam.methods import GradCAM cam_extractor = GradCAM(model, target_layer='layer4') with torch.no_grad(): out = model(input_tensor) activation_map = cam_extractor(out.squeeze(0).argmax().item(), out)7. 部署优化方案
7.1 模型轻量化
知识蒸馏:
- 使用ResNet152作为教师模型
- 蒸馏到ResNet18学生模型
量化压缩:
model = torch.quantization.quantize_dynamic( model, {nn.Linear}, dtype=torch.qint8 )
7.2 部署方式
ONNX导出:
torch.onnx.export(model, dummy_input, "food_cls.onnx", input_names=["input"], output_names=["output"], dynamic_axes={'input': {0: 'batch'}, 'output': {0: 'batch'}})TensorRT加速:
trtexec --onnx=food_cls.onnx --saveEngine=food_cls.engine --fp16
8. 常见问题排查
8.1 训练问题
验证准确率始终为0:
- 检查数据加载顺序是否被打乱
- 验证标签是否与模型输出维度匹配
- 检查是否错误冻结了所有层
损失值震荡剧烈:
- 降低学习率
- 增大batch size
- 添加梯度裁剪
8.2 部署问题
ONNX导出失败:
- 检查模型是否包含不支持的操作
- 尝试固定输入尺寸导出
- 更新PyTorch和ONNX版本
推理速度慢:
- 启用半精度推理
- 使用更高效的前处理库(如OpenCV)
- 优化数据加载流水线
9. 进阶优化方向
多模态融合:
- 结合菜品名称文本特征
- 引入营养成分数据
细粒度分类:
- 实现同一类别的不同变体区分
- 引入注意力机制
增量学习:
- 支持新菜品类别的持续学习
- 防止旧类别遗忘
实际部署中发现,合理的数据增强比模型结构调整带来的提升更显著。对于食物分类任务,颜色抖动和随机裁剪的组合效果最佳,这符合食物图像对颜色敏感的特性。另一个实用技巧是在全连接层前添加256维的瓶颈层,既能降低过拟合风险,又能减少参数量。