ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

大豆叶片病害图像分类数据集:PyTorch加载、ResNet迁移学习训练与避坑指南

大豆叶片病害图像分类数据集:PyTorch加载、ResNet迁移学习训练与避坑指南 简介这份大豆叶片病害图像分类数据集面向计算机视觉入门与农业病害识别研究者提供约6000张已标注的叶片图片覆盖Diabrotica speciosa、健康、Caterpillar共三类已按训练集与测试集划分并附json标注文件同时提供show脚本可一键可视化样本与标签便于快速了解数据分布。资源包共2000个文件以jpg图片为主体1998张另含1个py脚本和1个json文件整体约808MB结构清晰适合直接用于图像分类模型训练、迁移学习或论文实验。目前已有135人学习下载对于需要标准病害分类数据集的开发者来说可省去自行采集标注的繁琐环节结合配套可视化脚本能帮助读者在统一数据格式下快速搭建分类流程专注网络设计与调参优化。1. 大豆叶片病害图像分类数据集从目录结构到训练避坑做农业图像分类的人应该都有体会公开数据集里病害类图片不难找但标注干净的、类别分布清晰的、拿过来就能直接训练的不多。这份大豆叶片病害图像分类数据集约 6000 张已标注图片三个类别Diabrotica speciosa、健康、Caterpillar已经划分好训练集和测试集存放结构就是同一类图片放同一个文件夹用 torchvision 的 ImageFolder 就能直接读不需要自己写复杂的解析逻辑。适合正在做植物病害分类、想快速跑通一个 baseline 模型、或者需要一份标注质量靠谱的数据来验证图像分类网络改进效果的从业者。数据量不算大但对单类识别任务来说配合预训练模型和合适的数据增强足够支撑到你判断模型有没有学出有效特征。2. 数据集结构与标注细节先看清 JSON 里藏了什么2.1 目录组织方式为什么说它对新手很友好先说目录。拿到数据集解压之后你会看到训练集和测试集两大部分各自下面按类别建了子文件夹。以摘要描述中提到的三个类别为例训练集里大概会是这样的组织train/ healthy/ healthy (1).jpg healthy (2).jpg ... Diabrotica_speciosa/ ... Caterpillar/ ... test/ healthy/ ... Diabrotica_speciosa/ ... Caterpillar/ ...这个结构的价值在于主流深度学习框架里读取分类数据的标准接口比如 torchvision.datasets.ImageFolder直接接收这种「按类分文件夹」的格式。ImageFolder 会自动为每个子文件夹生成类别索引你不需要自己写 CSV 解析或手动打标签。这点在实际项目中很关键——很多数据集的标注信息散落在 XML 或 txt 文件里得先写脚本解析才能用而这个数据集帮你省掉了这一步。2.2 JSON 文件里的类别映射与标注逻辑摘要里特别提到了一个 json 文件类别具体信息要看这个文件。常见做法是它记录了类别名称和对应标签索引的映射关系。我一般会先执行下面这段代码把类别映射打印出来确认import json with open(class_indices.json, r, encodingutf-8) as f: class_indices json.load(f) print(class_indices) # 常见输出格式{0: Diabrotica_speciosa, 1: healthy, 2: Caterpillar}注意这里class_indices的键是字符串类型的索引不是整数。如果你后续要用这个映射做预测结果的可视化记得转换类型int(label_id)再取类别名。另外一个容易忽略的点是训练集和测试集的类别顺序可能不一致如果 json 只在训练集目录下而你的测试集加载逻辑依赖 json 里的索引关系加载前先确认两者用的是同一份映射表。2.3 show 脚本训练前先做一次可视化验证摘要里提到「如果想可视化数据集可以运行资源中的 show 脚本」。这一步强烈建议做。很多新手拿到数据集直接开训练结果模型训练完才发现图片有损坏、类别张冠李戴、或者某些样本的标注本身有问题。可视化验证能提前暴露这些问题。show 脚本的功能本质上是随机抽一批图片展示并打印类别标签。如果没有附带这个脚本用下面这段也能达到同样的效果import matplotlib.pyplot as plt from torchvision import datasets, transforms # 先定义基础预处理 transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor() ]) # 加载训练集 train_dataset datasets.ImageFolder(roottrain/, transformtransform) # 随机抽 9 张出来可视化 fig, axes plt.subplots(3, 3, figsize(9, 9)) for i in range(9): img, label train_dataset[i] ax axes[i // 3][i % 3] ax.imshow(img.permute(1, 2, 0)) ax.set_title(train_dataset.classes[label]) ax.axis(off) plt.tight_layout() plt.show()这段脚本先通过 ImageFolder 加载数据集对象然后按索引取样本train_dataset[i]返回的元组第一个元素是 Tensor 图像第二个是类别索引。用train_dataset.classes[label]把索引映射回类别名。做这一步时我一般会关注三件事图片尺寸是否一致、有无全黑或严重过曝的样本、类别名和图片内容是否吻合。如果发现某张图明显不是叶片或者类别对不上尽早从数据集里剔除别指望模型能自动忽略这些脏数据。3. 从加载到训练ResNet18 baseline 的参数选择3.1 数据预处理与图像分辨率设置6000 张图片、3 个类别单类平均 2000 张。这个数据量下模型初始化方式比网络结构深度更影响最终性能。我的习惯是用在 ImageNet 上预训练过的 ResNet18 或 ResNet50冻结 backbone 的前几层只微调后面的层。数据增强策略上因为叶片病害识别的关键特征是纹理和局部病斑翻转、旋转这类全局增强可以用但不要加太强的颜色抖动否则会让模型对病斑颜色的敏感度下降。预处理流程参考如下from torchvision import transforms train_transform transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(15), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])注意训练集用 Resize 256 再 RandomCrop 224验证集直接 Resize 224这个差异是刻意的。RandomCrop 相当于在裁剪时引入了平移扰动等于免费的数据增强。Normalize 用的参数是 ImageNet 统计数据因为我们要加载预训练权重输入分布必须跟预训练时保持一致。如果你用自己训练的模型这些统计值需要从数据集里重新计算。3.2 训练参数怎么设batch size、学习率与 epoch 的经验值针对 6000 张这种量级直接给一组我验证过能收敛的参数组合参数推荐值说明batch size32显存 8G 以上可以用 64但 32 更稳初始学习率1e-4用了预训练权重学习率大了容易破坏已有特征权重衰减1e-4抑制过拟合这个数据量上有效优化器AdamW比 Adam 的权重衰减实现更规范训练轮数30~5015 轮后关注验证集没提升就提前停学习率调整StepLRstep10gamma0.1或者用 CosineAnnealing差别不大训练循环的核心代码框架import torch import torch.nn as nn from torchvision import models device torch.device(cuda if torch.cuda.is_available() else cpu) model models.resnet18(pretrainedTrue) # 把最后的全连接层替换成 3 分类 model.fc nn.Linear(model.fc.in_features, 3) model model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) for epoch in range(30): model.train() for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() # 每个 epoch 结束后在验证集上评估 model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() print(fEpoch {epoch}: val_acc {100.0 * correct / total:.2f}%)model.fc.in_features是 ResNet18 全连接层输入维度512替换成输出 3 维的线性层即可。CrossEntropyLoss内部已经包含 Softmax所以模型输出不需要再单独过 Softmax直接送进 Loss 就行。验证阶段的torch.max(outputs, 1)返回每行最大值和对应索引索引就是预测类别。3.3 评估指标只看准确率不够三分类问题准确率高不代表模型真实可用。建议打印每个类别的 precision、recall、F1 值。在这份数据集里重点看 Diabrotica speciosa 和 Caterpillar 这两个类——它们都属于虫害类叶片表观特征可能有重叠比如都有啃食痕迹模型容易混淆。from sklearn.metrics import classification_report, confusion_matrix y_true, y_pred [], [] model.eval() with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) y_true.extend(labels.cpu().numpy()) y_pred.extend(predicted.cpu().numpy()) print(classification_report(y_true, y_pred, target_namestrain_dataset.classes)) print(confusion_matrix(y_true, y_pred))classification_report输出每个类别对应的精确率、召回率和 F1confusion_matrix能直接看到哪两个类互相误判最多。如果 healthy 被误判成 Caterpillar 的比例很高说明模型把叶片上某些正常纹理当成了虫害特征这时候需要考虑增加健康样本的多样性或者调小颜色增强的幅度。4. 避坑指南训练大豆叶片分类的三个常见问题4.1 类别不平衡导致模型偏向多数类现象训练集里三个类别的图片数量差距明显。摘要中只给出了 healthy 类的大量文件名举例但实际分布可能并非完全均匀。如果 Diabrotica speciosa 只有 800 张而 healthy 有 2500 张最终模型在 healthy 上准确率高另外两个类偏低。原因CrossEntropyLoss 默认对所有类别一视同仁模型会倾向于预测样本量大的那个类因为这样总损失最小。解决给 Loss 加上类别权重样本数少的类权重调高。计算权重的方式import torch import torch.nn as nn # 假设拿到每个类别的样本数 class_counts torch.tensor([800, 2500, 1700], dtypetorch.float32) weights class_counts.sum() / (class_counts * 3) loss_fn nn.CrossEntropyLoss(weightweights.to(device))4.2 用预训练模型时学习率设太大导致特征被破坏现象训练 5 轮后 loss 降到很低但验证集准确率一直在 60% 左右上不去甚至出现回退。原因预训练模型在 ImageNet 上学到的底层特征边缘、纹理、颜色过渡是很稳定的通用特征。学习率设成 1e-3 甚至更高这些底层特征会被大幅调整模型对叶片这种特定域的适应性反而变差。这是迁移学习中典型的「灾难性遗忘」。解决把学习率降到 1e-4 或更小也可以采用分层设置——backbone 用低学习率新加的 fc 层用高学习率backbone_params list(model.parameters())[:-2] # hacky但能说明思路 fc_params list(model.fc.parameters()) optimizer torch.optim.AdamW([ {params: backbone_params, lr: 1e-5}, {params: fc_params, lr: 1e-3} ], weight_decay1e-4)更正规的做法是用model.parameters()配合param_groups按 layer 名称分组上面的写法是对 fc 层特殊处理的最简版。4.3 可视化时图片显示成蓝绿色调以为数据损坏现象运行 show 脚本后图片颜色很奇怪整体偏蓝或偏紫和正常叶片颜色不像。原因这大概率不是数据问题是transforms.ToTensor()把像素值从 [0,255] 归一化到了 [0,1]送进模型前又做了 Normalize但 matplotlib 的imshow期望的输入范围是 [0,1] 且通道顺序是 RGB。你展示的是 Tensor 数据如果之前做过 Normalize像素值已经不在 [0,1] 范围颜色就会失真。解决可视化的时候不要走完整的预处理链路只做 Resize 和 ToTensorvis_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor() ])或者如果已经 normalize 了手动反归一化inv_normalize transforms.Normalize( mean[-0.485/0.229, -0.456/0.224, -0.406/0.225], std[1/0.229, 1/0.224, 1/0.225] ) img inv_normalize(img)5. 迁移学习的两个进阶技巧冻结层调整与 CAM 可视化5.1 怎么决定冻结多少层数据量为 6000 张、类别数只有 3训练 ResNet50 全量参数的问题是模型容量太大而数据不足容易过拟合。经验法则是先训练 fc 层看效果再解冻并微调最后一个 stagelayer4。在训练脚本里可以加一个「解冻开关」# 初始阶段只让 fc 层参与训练 for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True # 训练 10 轮后解冻 layer4 参与微调 def unfreeze_layer4(): for name, param in model.named_parameters(): if name.startswith(layer4): param.requires_grad True「先用小学习率训练新层再解冻深层继续微调」这个流程比一步到位训全量更可控因为每轮只调整一部分参数梯度更新不会产生大幅度波动。5.2 用 Grad-CAM 检查模型到底在看哪里训练完成后如果你想知道模型是依据叶片上的病斑位置做的判断还是抓到了背景中的无关特征Grad-CAM 类激活图可视化是最直观的工具。截取 a tiny 实现class GradCAM: def __init__(self, model, target_layer): self.model model self.target_layer target_layer self.gradients None self.activations None target_layer.register_forward_hook(self.save_activation) target_layer.register_full_backward_hook(self.save_gradient) def save_activation(self, module, input, output): self.activations output.detach() def save_gradient(self, module, grad_input, grad_output): self.gradients grad_output[0].detach() def generate(self, input_tensor): output self.model(input_tensor) self.model.zero_grad() # 取最高概率类别的分数做反传 output[0, output.argmax()].backward() weights self.gradients.mean(dim(2, 3), keepdimTrue) cam (weights * self.activations).sum(dim1, keepdimTrue) cam torch.relu(cam) cam torch.nn.functional.interpolate( cam, size(224, 224), modebilinear, align_cornersFalse) return cam如果你的模型对某张 Diabrotica speciosa 的图片判断正确但激活图高亮区域集中在叶片边缘而不是啃食痕迹附近说明模型学习到的特征不够鲁棒换到新拍摄条件下可能会翻车。届时的调整手段不是改网络结构而是检查训练集里是否有太多背景为纯色或纯白区域的图片适当增加复杂背景的样本。5.3 最终部署前的检查习惯模型训练完一定要做一次完整的推理链路检查读一张训练集之外的图片、走预处理、推理、打印置信度分布。我的习惯是写一个单图推理脚本输出每个类别的置信度分数观察模型对相近类别的置信度差距。如果 healthy 和 Caterpillar 的置信度只差 0.03说明判别边界很模糊部署到实际环境里肯定有风险。从那以后我每次拿到新的分类数据集都会强制走一遍可视化验证、构造类别权重、冻结预训练参数三层检查后才开始正式调参。这套流程在多个农业图像分类项目里都帮我避免了无效训练希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进