恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
PyTorch迁移学习实战:ResNet50实现IP102害虫细粒度图像分类
首页
资讯中心
/
PyTorch迁移学习实战:ResNet50实现IP102害虫细粒度图像分类
PyTorch迁移学习实战:ResNet50实现IP102害虫细粒度图像分类
发布时间:2026/8/26 10:36:49
简介图像分类是计算机视觉中最基础也最核心的任务之一但在农业植保场景中对田间害虫进行细粒度识别远比普通物体分类更具挑战。面对类间差异小、背景干扰大、样本分布不均等现实问题从零训练卷积神经网络往往难以收敛。迁移学习为此提供了高效路径借助在ImageNet上预训练的ResNet50模型我们能够快速提取通用视觉特征再针对特定任务进行微调大幅降低训练成本并提升分类精度。本文以IP102害虫数据集为例系统讲解数据整理、数据增强、模型构建、两阶段微调、损失函数设计与评估分析等关键环节并给出可直接复现的PyTorch代码与调参经验。无论你是刚接触深度学习的新手还是正在搭建图像分类baseline的工程师这套从数据到部署的完整方案都值得参考。 最近在做一个农业领域的图像识别项目核心任务是对田间害虫做细粒度分类数据集用的公开的IP102框架当然选了Pytorch而且直接加载了在ImageNet上预训练好的ResNet50当特征提取器。整套流程跑下来从环境搭建、数据装载、模型微调到最后的评估报告踩了不少坑也沉淀了一套比较完整的落地路径。这篇就把整个项目从零到一的思路、代码和调参心得全部拆开讲适合正在入门或想快速搭建图像分类baseline的同学参考。先说这个项目能解决什么问题。害虫识别在农业植保里属于典型的细粒度图像分类任务难点在于类间差异小比如棉铃虫和烟青虫长得几乎一样、类内差异大同一种害虫的不同龄期、姿态差异明显、再加上田间拍摄的背景杂乱、光照不定用简单CNN从零训练很难收敛。IP102数据集覆盖了102类常见害虫共7.5万张左右图片规模适中是学术界比较常用的害虫分类benchmark。用Pytorch配合预训练模型做迁移学习能大幅降低训练成本同时把分类准确率拉到一个可用的水平。如果你只想要一套“能跑通、能出指标、能继续扩展”的完整方案这篇会直接给你可复现的代码和每一步的关键取舍。要是你刚开始学Pytorch里面涉及的Dataset、Dataloader、迁移学习、评估指标这些基础概念我也会用比较通俗的方式讲清楚保证你能跟上。1. 项目整体设计与思路拆解1.1 为什么选IP102数据集IP102是北京邮电大学团队发布的昆虫识别数据集专门为农业害虫识别任务设计包含102个类别训练集约65000张验证集7500张测试集7500张图片分布非常接近真实田间场景。这个数据集有几个非常鲜明的特点直接决定了你在技术方案上的选择。第一个特点是类别极度不平衡。有些害虫的图片超过千张有些类别只有一百多张如果直接用原始分布训练模型会严重偏向样本量大的类。第二个特点是背景干扰大很多图片里害虫本身只占很小一块叶片纹理、土壤颜色、光影变化都会干扰分类器。第三个特点是类间相似度高比如同属夜蛾科的几种害虫从外观上普通人根本分不出来这对模型的细粒度特征提取能力要求很高。选择IP102还有一个工程师视角的理由它足够“脏”。真实项目里拿到的数据往往比学术数据集更乱IP102的不平衡、模糊标签和复杂背景反而更像真实业务场景训练出来的模型泛化能力更有参考价值。如果你只是想要一个“干净”的高准确率模型那用PlantVillage那种病害叶子数据集会更省心但IP102能让你提前感受工业级任务的痛苦。1.2 为什么必须用预训练模型做迁移学习这个项目里“包含预训练模型”不是可选项而是刚需。102类害虫分类输入图片224x224如果用ResNet50从随机初始化开始训练在IP102这种量级的数据集上收敛速度慢到让人怀疑人生最终准确率大概率也到不了50%。原因很简单CNN的低层卷积核负责提取边缘、纹理、颜色块等通用特征这些特征在ImageNet这种千万级数据集上已经学得非常好了完全没有必要丢弃。迁移学习的核心思路是“站在巨人肩膀上”把ImageNet预训练权重作为初始参数让模型先具备良好的特征提取能力然后只针对最后分类层做重点训练。具体来说ResNet50的最后一层全连接是1000维ImageNet类别数我们把这一层替换成102维输出然后对整个网络用小学习率微调。这样既保留了低层通用特征又让高层特征针对害虫数据重新适配。从训练成本上看迁移学习也优势明显。一张消费级显卡比如RTX 3060配合预训练模型很多情况下20个epoch左右就能看到不错的效果从头训练的话不仅epoch数要翻好几倍还容易陷入局部最优。所以我的建议很简单任何图像分类项目只要不是做学术消融实验一律先跑通预训练模型再谈其他。2. 环境准备与数据装载2.1 Pytorch环境搭建和CUDA踩坑记录Pytorch的安装不算难但容易在版本匹配上翻车。我用的组合是Python 3.10 Pytorch 2.1.2 CUDA 12.1 cuDNN 8.9整体非常稳。如果你用的是PyTorch 2.6以上版本有一个细节必须注意torch.load()默认的weights_only参数变成了True如果加载的权重存了优化器状态或自定义类会直接报错。这时候需要显式设置weights_onlyFalse但要注意这会有反序列化安全风险工程上建议只加载官方发布或可信来源的权重文件。命令行安装建议直接用官方给出的命令先确认自己的CUDA版本nvidia-smi # 假设显示CUDA Version: 12.1 pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121之前遇到过装了CPU版本还在沾沾自喜的情况验证GPU是否可用的方法import torch print(torch.cuda.is_available()) # True print(torch.cuda.get_device_name(0)) # NVIDIA GeForce RTX 30602.2 IP102数据集的下载和目录整理IP102数据集网上有很多下载渠道下载完之后是原始图片加一个包含标签映射关系的文本文件。建议先把数据整理成ImageFolder标准结构这样可以直接使用torchvision.datasets.ImageFolder加载省去写自定义Dataset的麻烦。整理成以下结构IP102/ ├── train/ │ ├── class_001/ │ │ ├── xxx.jpg │ │ └── ... │ ├── class_002/ │ └── ... ├── val/ │ └── ... └── test/ └── ...这里有个关键细节IP102的标签文件里类别名称是英文害虫名比如Acythosiphon pisum需要建立一个从0到101的整数索引映射。数据整理这一步虽然枯燥但直接决定了后面能不能省心。我在一个脚本里同时完成“读取标签文件、复制图片到目标目录、生成类别名称JSON”一步到位。标签映射的代码示意import json from pathlib import Path classes sorted(os.listdir(IP102/train)) idx_to_class {i: cls for i, cls in enumerate(classes)} class_to_idx {cls: i for i, cls in enumerate(classes)} with open(class_mapping.json, w) as f: json.dump({idx_to_class: idx_to_class, class_to_idx: class_to_idx}, f, indent2)2.3 Dataloader配置与数据增强策略数据增强是这次项目里效果提升最明显的环节之一。我采用的是组合策略训练时用RandomResizedCrop随机裁剪加缩放、RandomHorizontalFlip随机水平翻转、ColorJitter轻微调整亮度饱和度、最后Normalize标准化验证和测试阶段只做ResizeCenterCrop不做随机增强。from torchvision import transforms train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])关于RandomResizedCrop里scale参数的选择默认是(0.08, 1.0)也就是裁剪区域可以小到原图的8%这对IP102这种目标占比大的图片来说可能造成严重的信息丢失。实战中调成(0.6, 1.0)更合适保证裁剪后害虫主体基本保留。这一处微调我测试下来准确率大概有1-2个百分点的提升。Dataloader的两个关键参数batch_size视显存定我的一开始设64结果RTX 3060 12GB跑ResNet50刚好卡在OOM边缘最终降到48num_workers设为4加载速度明显提升。对了Windows系统下如果遇到DataLoader多进程报错可以把num_workers设成0或者把Dataloader相关代码放进if __name__ __main__:里。3. 模型构建与训练核心代码实现3.1 从torchvision加载ResNet50预训练模型模型加载这部分Pytorch的torchvision.models已经封装得非常好关键点在于选择正确的权重版本和正确替换分类头。现在版本的torchvision推荐用weights参数指定权重枚举类型而不是老式的pretrainedTrue。import torchvision.models as models model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) # 替换最后一层全连接 in_features model.fc.in_features model.fc nn.Linear(in_features, 102)这里有个初学者容易忽略的细节ResNet50_Weights.IMAGENET1K_V2比IMAGENET1K_V1准确率更高V2版本的top-1准确率约80.86%V1约76.13%直接使用V2作为起始点会让微调的起点更高。另外model.fc.in_features可以自动识别2048维输入不需要手写硬编码。训练时还可以选择一种升级策略先冻结backbone参数只训练新增的fc层等fc层收敛后再解冻全部参数做全量微调。这种两阶段训练的好处是防止随机初始化的fc层在前期产生过大的梯度破坏预训练权重。不过实际操作中如果学习率设置得够小小于1e-3直接全量微调也是可以的很多情况下效果差不多但更省事。3.2 损失函数与优化器选择分类任务最常见的选择就是交叉熵损失nn.CrossEntropyLoss。但IP102数据集类别不平衡严重直接把交叉熵用在原始分布上模型会偏向样本量大的类别。这里有两种处理策略我在项目里都试过。第一种是给损失函数加类别权重。用sklearn计算各类别样本数量的倒数并归一化然后传给CrossEntropyLossimport torch.nn as nn class_counts [...] class_weights 1.0 / torch.tensor(class_counts, dtypetorch.float) class_weights class_weights / class_weights.sum() * len(class_counts) criterion nn.CrossEntropyLoss(weightclass_weights.cuda())第二种是使用带label smoothing的交叉熵。label smoothing的作用是防止模型对训练标签过于自信相当于给标签概率做了一点“软化”增强泛化能力。Pytorch里的实现也很简单nn.CrossEntropyLoss(label_smoothing0.1)一行搞定。我最终采用的是“类别权重 label smoothing 0.1”的组合效果比单独使用任何一种都好。优化器我选择AdamW配合余弦退火学习率调度。AdamW相比Adam在权重衰减的处理上更规范能够有效防止过拟合。学习率初始设为3e-4冻结backbone阶段或者1e-4全量微调阶段配合CosineAnnealingLR在训练后段自动降低学习率让模型参数在损失面底部稳定收敛。权重衰减设1e-4或5e-5都行不建议设太大否则预训练权重会被过度“遗忘”。3.3 完整训练循环框架这里给一套可以直接复用的精简训练循环只列关键部分。模型放GPU、设置混合精度、记录每个epoch的指标这是训练脚本的基本框架。from torch.cuda.amp import autocast, GradScaler device torch.device(cuda if torch.cuda.is_available() else cpu) epochs 50 batch_size 48 train_loader torch.utils.data.DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue, num_workers4, pin_memoryTrue) val_loader torch.utils.data.DataLoader(val_dataset, batch_sizebatch_size, shuffleFalse, num_workers4, pin_memoryTrue) model model.to(device) criterion nn.CrossEntropyLoss(label_smoothing0.1).to(device) optimizer torch.optim.AdamW(model.parameters(), lr3e-4, weight_decay5e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxepochs) scaler GradScaler() for epoch in range(epochs): model.train() total_loss, correct, total 0.0, 0, 0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() with autocast(): outputs model(images) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() total_loss loss.item() * images.size(0) _, preds torch.max(outputs, 1) correct torch.sum(preds labels.data).item() total labels.size(0) train_acc correct / total val_acc validate(model, val_loader) # 保存最佳模型 if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_model.pth) scheduler.step()这里重点说下混合精度训练。Pytorch的GradScalerautocast在ResNet50这类网络上能带来约30%-40%的训练加速而且显存占用更低几乎不损失精度。条件允许的情况下建议无脑开启。注意混合精度模式下如果出现loss是NaN的情况可以先检查是否有异常大的学习率并把scaler.scale(loss).backward()后的scaler.unscale_(optimizer)语句加上方便定位梯度中的异常值。3.4 冻结backbone的两阶段微调我在项目中实际采用了两阶段训练效果比直接全量微调要稳定。第一阶段冻结所有卷积层只训练新加的fc层学习率可以稍微大一点1e-3跑5个epoch把分类层调整到合理区间。第二阶段解冻全部参数学习率降到3e-4完整训练50个epoch。冻结参数的核心代码# 冻结backbone for param in model.parameters(): param.requires_grad False # 只让fc层可训练 for param in model.fc.parameters(): param.requires_grad True # 训练5个epoch后再解冻 for param in model.parameters(): param.requires_grad True这个方法的原理是预训练模型的卷积层已经能提取到丰富的视觉特征但新增的fc层是随机初始化的如果一开始就让全模型一起反向传播fc层的梯度尺度较大可能对预训练权重造成破坏性的更新。先让fc层“站稳脚跟”再用小学习率微调整体参数两条路径都比较稳。不过有个细节提醒一下如果用同一个AdamW优化器管理了全部参数并且冻结backbone后发现fc的梯度更新正常、backbone没有梯度这属于正常现象。解冻之后最好重新创建优化器或者调用optimizer.param_groups重新设置学习率避免旧状态干扰。4. 评估指标与结果分析4.1 不只盯着准确率多维度评估体系项目在IP102测试集上的Top-1准确率达到了68.3%Top-5准确率89.6%整体效果已经超过了官方论文报告的baseline约62%。但只报一个准确率数字显然不够农业场景里的评估必须要更细致因为不同类别的识别难度差异极大。我额外计算了三个维度的指标精确率Precision、召回率Recall和F1分数并对每个类别单独统计。在102个类别里有些类别比如玉米螟的Precision高但Recall低说明模型倾向于只把最有把握的样本预测为该类漏检了更多有些类别比如棉铃虫相反Recall高但Precision低说明模型把大量相似害虫误判成了它。综合分析这些指标才能真正知道模型在哪些类上可用、哪些类上不可用。4.2 混淆矩阵和错误分析混淆矩阵是细粒度分类任务里最值得看的图。在102x102的矩阵中我重点观察了非对角线上的高频错误点。结果发现主要的混淆模式集中在以下几种情况同科害虫之间的混淆比如夜蛾科内部的几个物种、不同龄期的同种害虫被分到不同类、背景中其他昆虫的干扰。一个有趣但符合预期的发现是模型对于“绿色且细长”形态的害虫比如蚜虫、叶蝉容易互相混淆因为这些类在颜色、纹理上高度相似单纯靠视觉特征很难区分。如果要进一步提升精度可能需要引入更细粒度的局部区域特征或者增加害虫背板纹理等强判别性特征的监督信号。4.3 混淆矩阵评估代码评估阶段我写了这样一个函数可以输出每个类别的分类报告并保存混淆矩阵的图片from sklearn.metrics import classification_report, confusion_matrix import seaborn as sns import matplotlib.pyplot as plt all_preds [] all_labels [] model.eval() with torch.no_grad(): for images, labels in test_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) print(classification_report(all_labels, all_preds, target_namesidx_to_class.values())) cm confusion_matrix(all_labels, all_preds) plt.figure(figsize(40, 40)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelsidx_to_class.values(), yticklabelsidx_to_class.values()) plt.savefig(confusion_matrix.png, dpi100)5. 常见问题与排查技巧实录5.1 训练loss降不下去或直接NaN这是最常遇到的问题之一。loss为NaN大概率是学习率设置过高导致梯度爆炸尤其是使用预训练模型时fc层随机初始化后的梯度会非常大。解决方式是先把学习率降到1e-4级别或者采用两阶段微调先让fc层稳定再解冻全模型。还有一个容易被忽视的原因数据里有损坏的图片文件。在处理IP102这种从网上下载的大规模数据集时偶尔会有几张损坏的JPEG或者零字节文件Pytorch的ImageFolder加载到它们时会直接抛错或者输出NaN。建议在数据整理阶段用PIL.Image.open()逐张校验把无法打开的文件移到一个单独目录或者用Image.open(img_path).verify()方法快速过滤。5.2 验证集准确率波动很大怎么办如果你的训练loss在逐步下降但验证准确率每两个epoch波动超过5个百分点说明当前学习率对模型来说还是偏大或者batch_size过小导致batch内部的样本分布不稳定。最直接的解法是降低学习率把AdamW初始学习率从3e-4降到1e-4试试。另外可以检查是不是数据增强太强导致验证集分布和训练集分布差异过大减弱ColorJitter的强度或者改用RandomResizedCrop里较大的scale值。5.3 类别严重的模型偏置如果不使用类别权重IP102上模型很容易对所有样本都预测为样本量最大的那几个类。解决办法就是我在3.2节中提到的类别权重交叉熵或者使用Focal Loss。Focal Loss在样本不平衡的细粒度分类任务里表现很好它的核心思想是降低易分类样本的权重让模型更关注难分类的样本。写一个简化版的Focal Loss并不复杂import torch import torch.nn.functional as F class FocalLoss(nn.Module): def __init__(self, gamma2.0, alphaNone): super(FocalLoss, self).__init__() self.gamma gamma self.alpha alpha def forward(self, logits, targets): ce_loss F.cross_entropy(logits, targets, reductionnone) pt torch.exp(-ce_loss) focal_loss (1 - pt) ** self.gamma * ce_loss if self.alpha is not None: focal_loss focal_loss * self.alpha[targets] return focal_loss.mean()实际使用中我是先用类别权重交叉熵训练好一个baseline再用Focal Loss继续精调效果比从头就用Focal Loss更好。原因可能是Focal Loss的梯度形态比较特殊直接用容易收敛得比较慢。5.4 显存不足OOM的排查ResNet50输入224x224batch_size48在RTX 3060 12GB上训练时刚好能在混合精度下跑通。如果你显存不够最简单的调整是减小batch_size但同时适当调低学习率保持稳定性也可以把torch.utils.data.DataLoader里的pin_memory改为False这能节省一点额外的显存占用。还有一个办法是用torch.utils.checkpoint做梯度检查点用计算换显存但对于ResNet50这种中等规模网络不必非要用这个。5.5 PyTorch 2.6及以后版本加载模型报错这个在上文提到过非常值得单独再列一次。如果你从网上下载别人训练好的权重或者加载自己之前的checkpoint在PyTorch 2.6环境中遇到了类似“Weights only load failed”的报错基本就是weights_only默认值的锅。我的建议是在torch.load时明确设置为weights_onlyFalse因为老版本的checkpoint普遍包含优化器状态等附加信息。但这里必须提醒安全风险不要加载不信任的pth文件一旦文件被恶意构造可能在反序列化时执行任意代码。如果你的环境是PyTorch 2.6以下则不需要考虑这个问题。打算长期维护项目的同学建议还是保持Pytorch版本相对固定不要手滑升级大版本。6. 扩展方向与部署建议项目跑到了68.3%的Top-1准确率后在真正落地到田间场景之前还可以从几个方向继续改进模型结构。第一个方向是换更强的骨干网络比如Swin Transformer或EfficientNet-B5在小数据集上迁移学习的表现往往优于ResNet但代价是推理速度更慢。第二个方向是引入目标检测前置模块先用YOLO之类模型把害虫从背景中单独裁出来再做分类这对解决“目标占比小、背景干扰大”的问题非常有帮助。第三个方向是模型集成或知识蒸馏用多个模型集成得到伪标签来训练一个轻量化的学生模型兼顾精度和部署效率。部署方面如果是边缘设备比如Jetson系列需要注意安装对应JetPack版本的Pytorch并尽量使用TensorRT或者ONNX Runtime加速推理。一个经典的部署路径是Pytorch训练得到权重转成ONNX再利用ONNX Runtime或者TensorRT做推理优化。转ONNX时需要注意固定输入尺寸、关闭动态轴并对torchvision里的一些预处理操作做等价转换否则部署后推理结果可能和离线结果不一致。如果在Jetson设备上部署硬件环境配置和Pytorch的Python版本、CUDA版本都需要匹配具体的安装方式不同的JetPack版本差异较大这里就不展开说了核心思路是先去设备上确认系统自身带的是哪个Python和CUDA版本再去找对应的Pytorch的wheel包不要盲目装最新版。最后还要提一个贴近实战的小经验做这类农业识别项目时不要只盯着测试集的准确率最好留一部分图片做实际的野外样例测试因为IP102测试集的分布与真实田间环境仍然有差异。我们最后在实际植保站拍摄的图片上做了一轮抽查发现真实场景的accuracy比测试集低5-6个百分点这属于正常现象。如果你的项目要交付给甲方使用把这一条提前写在报告里能省掉后续很多争议。本文还有配套的精品资源点击获取