简介:这份大豆叶片病害图像分类数据集面向计算机视觉入门与农业病害识别研究者,提供约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', encoding='utf-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(root='train/', transform=transform) # 随机抽 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(p=0.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 size | 32 | 显存 8G 以上可以用 64,但 32 更稳 |
| 初始学习率 | 1e-4 | 用了预训练权重,学习率大了容易破坏已有特征 |
| 权重衰减 | 1e-4 | 抑制过拟合,这个数据量上有效 |
| 优化器 | AdamW | 比 Adam 的权重衰减实现更规范 |
| 训练轮数 | 30~50 | 15 轮后关注验证集,没提升就提前停 |
| 学习率调整 | StepLR,step=10,gamma=0.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(pretrained=True) # 把最后的全连接层替换成 3 分类 model.fc = nn.Linear(model.fc.in_features, 3) model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-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(f'Epoch {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_names=train_dataset.classes)) print(confusion_matrix(y_true, y_pred))classification_report输出每个类别对应的精确率、召回率和 F1,confusion_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], dtype=torch.float32) weights = class_counts.sum() / (class_counts * 3) loss_fn = nn.CrossEntropyLoss(weight=weights.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_decay=1e-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 和 ToTensor:
vis_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 层看效果,再解冻并微调最后一个 stage(layer4)。在训练脚本里可以加一个「解冻开关」:
# 初始阶段只让 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), keepdim=True) cam = (weights * self.activations).sum(dim=1, keepdim=True) cam = torch.relu(cam) cam = torch.nn.functional.interpolate( cam, size=(224, 224), mode='bilinear', align_corners=False) return cam如果你的模型对某张 Diabrotica speciosa 的图片判断正确,但激活图高亮区域集中在叶片边缘而不是啃食痕迹附近,说明模型学习到的特征不够鲁棒,换到新拍摄条件下可能会翻车。届时的调整手段不是改网络结构,而是检查训练集里是否有太多背景为纯色或纯白区域的图片,适当增加复杂背景的样本。
5.3 最终部署前的检查习惯
模型训练完一定要做一次完整的推理链路检查:读一张训练集之外的图片、走预处理、推理、打印置信度分布。我的习惯是写一个单图推理脚本,输出每个类别的置信度分数,观察模型对相近类别的置信度差距。如果 healthy 和 Caterpillar 的置信度只差 0.03,说明判别边界很模糊,部署到实际环境里肯定有风险。从那以后我每次拿到新的分类数据集,都会强制走一遍可视化验证、构造类别权重、冻结预训练参数三层检查后,才开始正式调参。这套流程在多个农业图像分类项目里都帮我避免了无效训练,希望帮到你。
本文还有配套的精品资源,点击获取