ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

PyTorch实战:基于ResNet的食物图像分类与迁移学习

PyTorch实战:基于ResNet的食物图像分类与迁移学习

1. 项目概述

ResNet残差网络作为计算机视觉领域的里程碑式架构,在图像分类任务中展现出卓越性能。本次实战将基于PyTorch框架,利用ResNet预训练模型实现食物图像分类任务。不同于从零训练,迁移学习能大幅降低计算成本,即使在小样本数据集上也能获得理想效果。

食物分类作为计算机视觉的经典应用场景,在餐饮推荐、营养分析、智能厨房等领域具有广泛价值。传统方法需要人工设计特征,而深度学习方法能自动学习食物图像的层次化特征表示。ResNet凭借其独特的残差连接结构,有效解决了深层网络梯度消失问题,成为当前图像分类任务的首选架构之一。

2. 核心原理解析

2.1 ResNet架构精要

ResNet的核心创新在于残差块(Residual Block)设计。传统卷积神经网络随着深度增加会出现梯度消失问题,而ResNet通过引入跨层连接(Shortcut Connection)实现了恒等映射。具体实现包含两种基本模块:

  1. 基础残差块(BasicBlock):

    • 两个3×3卷积层堆叠
    • 每层后接BatchNorm和ReLU激活
    • 输入输出维度相同时直接相加
  2. 瓶颈残差块(Bottleneck):

    • 1×1卷积降维 → 3×3卷积 → 1×1卷积升维
    • 计算量更小适合深层网络
    • 典型用于ResNet50及以上版本

数学表达为:y = F(x, {Wi}) + x,其中F表示残差函数。当需要调整维度时,通过1×1卷积实现维度匹配:y = F(x, {Wi}) + Wsx。

2.2 迁移学习机制

迁移学习的有效性基于以下假设:

  1. 底层特征通用性:网络前几层学习的边缘、纹理等基础特征具有跨领域通用性
  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 tqdm

3.2 数据集处理

使用Food-101数据集(包含101类食物,每类1000张图片)的子集:

  1. 数据目录结构:

    food_dataset/ ├── train/ │ ├── pizza/ │ ├── sushi/ │ └── ... └── val/ ├── pizza/ ├── sushi/ └── ...
  2. 数据增强策略:

    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 model

4.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 model

5. 优化技巧与调参经验

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 类别不平衡处理

对于食物数据集中常见的类别不平衡问题:

  1. 过采样少数类
  2. 损失函数加权:
    class_counts = [...] # 各类别样本数 class_weights = 1. / torch.tensor(class_counts, dtype=torch.float) criterion = nn.CrossEntropyLoss(weight=class_weights.to(device))
  3. 使用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 模型轻量化

  1. 知识蒸馏:

    • 使用ResNet152作为教师模型
    • 蒸馏到ResNet18学生模型
  2. 量化压缩:

    model = torch.quantization.quantize_dynamic( model, {nn.Linear}, dtype=torch.qint8 )

7.2 部署方式

  1. ONNX导出:

    torch.onnx.export(model, dummy_input, "food_cls.onnx", input_names=["input"], output_names=["output"], dynamic_axes={'input': {0: 'batch'}, 'output': {0: 'batch'}})
  2. TensorRT加速:

    trtexec --onnx=food_cls.onnx --saveEngine=food_cls.engine --fp16

8. 常见问题排查

8.1 训练问题

  1. 验证准确率始终为0:

    • 检查数据加载顺序是否被打乱
    • 验证标签是否与模型输出维度匹配
    • 检查是否错误冻结了所有层
  2. 损失值震荡剧烈:

    • 降低学习率
    • 增大batch size
    • 添加梯度裁剪

8.2 部署问题

  1. ONNX导出失败:

    • 检查模型是否包含不支持的操作
    • 尝试固定输入尺寸导出
    • 更新PyTorch和ONNX版本
  2. 推理速度慢:

    • 启用半精度推理
    • 使用更高效的前处理库(如OpenCV)
    • 优化数据加载流水线

9. 进阶优化方向

  1. 多模态融合:

    • 结合菜品名称文本特征
    • 引入营养成分数据
  2. 细粒度分类:

    • 实现同一类别的不同变体区分
    • 引入注意力机制
  3. 增量学习:

    • 支持新菜品类别的持续学习
    • 防止旧类别遗忘

实际部署中发现,合理的数据增强比模型结构调整带来的提升更显著。对于食物分类任务,颜色抖动和随机裁剪的组合效果最佳,这符合食物图像对颜色敏感的特性。另一个实用技巧是在全连接层前添加256维的瓶颈层,既能降低过拟合风险,又能减少参数量。

返回列表