简介:面向计算机视觉与农业智能化应用场景的叶片病害图像分类数据集,覆盖玉米、马铃薯、水稻、甘蔗、小麦5种作物共17个病害类别,全部图像已完成标注并划分好训练集与测试集,适合训练CNN分类模型、验证迁移学习效果或用于课程设计与论文实验。资源包共2000个文件,压缩后约280.36MB,其中包含1998张jpg图片、1个Python可视化脚本和1个json类别配置文件。运行show脚本即可快速浏览数据分布,json文件可查看各类别名称与划分规则,省去手动整理和标注成本,已有208人学习下载。使用这份数据可直接开展图像分类任务,配合作者分享的CNN网络改进专栏,可进一步对比不同网络结构在复杂叶片病害识别上的精度与泛化性能,对做智慧农业方向研究或深度学习实战进阶都有实用价值。
1. 五作物叶片病害数据集:约 13,000 张已标注图像能解决什么问题
做农业视觉项目的人应该都有同感:找齐一套像样的叶片病害图像分类数据集,比调模型本身更磨人。这个数据集覆盖玉米、马铃薯、水稻、甘蔗、小麦五种大田作物,合计约 13,000 张已标注图像,每张图都带明确的健康或病害类别标签,解压后就能直接喂给图像分类模型训练,不用自己爬图、清洗、打标。对经常要快速验证分类思路的人来说,它最大的价值在于省掉两周以上的数据准备时间。它适合正在做作物病害识别、农业图像分类、或者需要一份现成 benchmark 数据集来验证网络结构选型的同学。
2. 数据集目录与标注体系:先看清文件结构再动手
拿到压缩包后第一件事不是急着训练,而是把目录结构和标注方式看清楚。图像分类数据集的标注方式常见的有两种:一种是 ImageNet 风格的文件夹组织,类别名直接当文件夹名;另一种是 CSV 或 JSON 映射表。这个数据集按常见的做法整理成了 train、val、test 三个子集,子集内部按类别分文件夹,同时附一份 label_mapping.csv 记录类别名到数字索引的映射。这样无论你想用 ImageFolder 直接加载,还是自己写 Dataset 类做精细控制,都能快速切换,不用二次加工。
2.1 目录结构与文件组织
解压后的顶层目录结构大概是这样的:
leaf_disease_dataset/ ├── train/ │ ├── Maize_Healthy/ │ ├── Maize_Leaf_Spot/ │ ├── Maize_Rust/ │ ├── Potato_Healthy/ │ ├── Potato_Early_Blight/ │ ├── Potato_Late_Blight/ │ ├── Rice_Healthy/ │ ├── Rice_Blast/ │ ├── Rice_Bacterial_Leaf_Blight/ │ ├── Sugarcane_Healthy/ │ ├── Sugarcane_Red_Rot/ │ ├── Wheat_Healthy/ │ ├── Wheat_Strip_Rust/ │ └── Wheat_Leaf_Rust/ ├── val/ │ └── (与 train 相同的类别文件夹) ├── test/ │ └── (与 train 相同的类别文件夹) ├── label_mapping.csv └── README.txttrain、val、test 三个目录里的子文件夹是完全一致的,类别数量相同、命名相同,后两个目录分别用于验证和最终测试。这里有个细节值得注意:val 和 test 的用途不同,val 是用来在训练中做早停、调学习率、选 epoch 的,而 test 必须留到最后才碰一次,这样最终评估指标才可信。很多人图省事只用 train 加 val,把 test 合并进 val,等提交结果或写报告的时候手里就没有干净的数据了。
label_mapping.csv 里存的是文件夹名到数字标签的映射,内容类似下面这样:
ClassName,LabelIndex Maize_Healthy,0 Maize_Leaf_Spot,1 Maize_Rust,2 Potato_Healthy,3 Potato_Early_Blight,4 Potato_Late_Blight,5 Rice_Healthy,6 Rice_Blast,7 Rice_Bacterial_Leaf_Blight,8 Sugarcane_Healthy,9 Sugarcane_Red_Rot,10 Wheat_Healthy,11 Wheat_Strip_Rust,12 Wheat_Leaf_Rust,13如果要用 PyTorch 的 ImageFolder 加载,这个文件可以不用管,因为 ImageFolder 会自动按字母序生成类别索引;但如果你要对齐自己的标签体系,或者想用 TensorFlow 的 flow_from_directory,那份 CSV 就是唯一的映射标准。我的习惯是先读一遍 CSV,确认类别顺序和 ImageFolder 的 class_to_idx 一致,然后再进入训练环节。
2.2 类别标签体系与划分比例
五种作物各自覆盖的病害类别不同,整理下来大致是这些:玉米三类,马铃薯三类,水稻三类,甘蔗两类,小麦三类,加上每个作物的健康叶片类别,总计 14 个类别。每类图像数量并不完全均衡,主粮作物比如玉米和小麦的样本会偏多一些,甘蔗这种经济作物的健康样本会在两个左右类别里被拉掉一部分,这点在做评估时要尤其留意,准确率这个单一指标很容易被样本多的类别拉高。
划分比例上,train 约占 70%,val 占 15%,test 占 15%。这个比例对迁移学习来说是够用的:每个类别在训练集里大约能分到 130 到 200 张图像,配上预训练权重做微调,已经能训出可用的模型。如果你打算把五个作物合并成一个 14 类的大分类器,上述划分保持不变;如果你想按作物单独训五个模型,需要自己按子目录重新聚合,两种路线我在第 3 章都会给出对应做法。
3. 用 ResNet50 做迁移学习训练:数据增强、超参数与全流程代码
数据集准备好只完成了第一步,重头戏在训练环节。对 13,000 张这个规模,从零训练一个深度卷积网络并不明智,参数量跟不上样本量,泛化必然出问题。常规做法是加载 ImageNet 预训练权重做迁移学习,把最后的全连接层换成自己的类别数,然后以较小的学习率微调。ResNet50 在这个场景下是性价比很高的选择,残差结构在农业图像这种背景复杂、目标尺度多变的场景里表现稳定,训练显存占用也适中,微调时单卡 11GB 左右就能跑起来。
3.1 数据加载与增强策略
数据加载直接用 torchvision 的 ImageFolder,增强策略是决定模型能不能泛化的关键。农业图像最典型的干扰是拍摄角度不一、光照变化、叶片姿态不同,所以增强里旋转、翻转和颜色抖动必须加:
from torchvision import datasets, transforms from torch.utils.data import DataLoader transform_train = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) transform_eval = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_dataset = datasets.ImageFolder("leaf_disease_dataset/train", transform=transform_train) val_dataset = datasets.ImageFolder("leaf_disease_dataset/val", transform=transform_eval) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4, pin_memory=True)先 Resize 到 256,再 RandomCrop 到 224,等价于给模型提供了小幅平移和缩放扰动,比直接 Resize 到 224 效果更好。ColorJitter 的三个参数值得解释一下:brightness=0.3 表示亮度在正负 30% 范围内随机变化,contrast 和 saturation 同理。农业图像里早晚光照色温差异很大,颜色抖动太弱模型容易把背景色调错当成病害特征,太强又会把病斑原本的颜色特征洗掉,0.2 到 0.3 之间是比较稳的范围。
验证集上不要用任何随机增强,只做 Resize 和 Normalize,否则验证指标会带噪声,早停判断就不准了。Normalize 用的 mean 和 std 是 ImageNet 的统计值,因为迁移学习加载的是 ImageNet 预训练权重,输入分布必须和预训练时保持一致。
3.2 迁移学习训练参数配置
模型部分把 ResNet50 的最后的 fc 层替换成 14 类输出。这里有两种微调策略可选:一种是把 backbone 完全冻结只训练 fc 层,速度快但效果有限;另一种是全参数微调,backbone 用较小的学习率,新加的 fc 层用较大学习率。第二种更常用,我直接给出完整实现:
import torch import torch.nn as nn from torchvision import models model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2) num_classes = len(train_dataset.classes) model.fc = nn.Linear(model.fc.in_features, num_classes) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.SGD(model.parameters(), lr=1e-3, momentum=0.9, weight_decay=1e-4) fc_params = model.fc.parameters() backbone_params = [p for name, p in model.named_parameters() if not name.startswith("fc.")] optimizer = torch.optim.SGD([ {"params": backbone_params, "lr": 1e-4}, {"params": fc_params, "lr": 1e-3} ], momentum=0.9, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1) best_val_acc = 0.0 for epoch in range(30): model.train() running_loss = 0.0 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() running_loss += loss.item() * images.size(0) 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) _, preds = torch.max(outputs, 1) correct += (preds == labels).sum().item() total += labels.size(0) val_acc = correct / total print(f"Epoch {epoch+1}: loss={running_loss/len(train_dataset):.4f}, " f"val_acc={val_acc:.4f}") if val_acc > best_val_acc: best_val_acc = val_acc torch.save(model.state_dict(), "best_model.pth") scheduler.step()超参数选择基于 13,000 张图像和 14 类划分的场景,整理成表格更好对照修改:
| 参数 | 取值 | 说明 |
|---|---|---|
| 输入尺寸 | 224×224 | ResNet50 默认输入,配合预训练权重 |
| batch_size | 32 | 单卡 11GB 显存可跑,显存小就降到 16 |
| backbone 学习率 | 1e-4 | 微调旧特征,太高会破坏预训练知识 |
| fc 层学习率 | 1e-3 | 新分类头从零学,稍高收敛更快 |
| weight_decay | 1e-4 | 缓解过拟合,五作物分类任务够用 |
| scheduler | StepLR, step=10, gamma=0.1 | 每 10 个 epoch 学习率降为原来的 1/10 |
| 总 epoch | 30 | 配合早停,实际 15-20 轮左右收敛 |
SGD 加 momentum 在这种中等规模数据集上比 Adam 更容易收敛到更平坦的极小值,验证准确率通常高 1-2 个百分点。weight_decay 用 1e-4 而不是 Adam 常用的 1e-5,因为叶片病害图像纹理细节多,模型很容易记住病斑周围的纹理噪声,适当的 L2 约束能压住这一倾向。
代码里保存模型只认 val_acc 最高的那一轮,而不是最后一轮的权重。训练后期损失下降变慢,验证准确率可能已经开始回落,直接用最后一轮权重往往比最佳权重低 3% 甚至更多。这个习惯我在很多项目里都吃过亏,所以现在不管任务大小都是边训边存。
4. 模型评估:混淆矩阵、分类报告与逐类别阈值设定
训练结束只是拿到了模型权重,真正能说明问题的是评估环节。农业图像分类里最容易被忽视的恰恰是对结果的分析:总准确率上去了,不代表每个类别都可靠。五作物 14 类中,有些类别视觉特征高度相似,比如马铃薯早疫病和晚疫病,早期症状都是叶片上出现褐色斑点,不做混淆矩阵分析根本发现不了模型在哪些地方翻车。
4.1 用混淆矩阵定位易混类别
在测试集上跑一遍完整推理,收集所有预测结果和真实标签,然后用 sklearn 直接算混淆矩阵:
from sklearn.metrics import confusion_matrix, classification_report import numpy as np model.load_state_dict(torch.load("best_model.pth", map_location=device)) model.eval() all_labels = [] all_preds = [] with torch.no_grad(): for images, labels in test_loader: images = images.to(device) outputs = model(images) _, preds = torch.max(outputs, 1) all_labels.extend(labels.cpu().numpy()) all_preds.extend(preds.cpu().numpy()) cm = confusion_matrix(all_labels, all_preds) print("混淆矩阵,行=真实类别,列=预测类别") print(np.array2string(cm, max_line_width=120))混淆矩阵的行代表真实类别,列代表预测类别,对角线越集中说明分类越可靠。拿到矩阵后不要只扫一眼对角线,要重点看非对角线上的大数字:如果马铃薯早疫病被大量预测成晚疫病,说明这两个类在特征空间里距离太近,单靠视觉信息很难区分。这时候的补救措施不是继续加训练轮次,而是回到数据层面检查这两个类别的样本质量,或者考虑把两个难以区分的类别合并成一个“马铃薯疫病类”,语义上虽然粗一些,但对田间用药决策来说可能已经够用。
另外一个值得看的维度是每类的样本量在混淆矩阵里的占比。用 np.sum(cm, axis=1) 算出每行总数,再用对角线除以行总数得到每类召回率,这样才能发现样本量小的类别是不是被样本量大的类别压住了。
4.2 分类报告与逐类别指标解读
classification_report 输出的指标更直观,这里给一个测试集上的示例格式:
| 类别 | precision | recall | f1-score | 测试样本数 |
|---|---|---|---|---|
| Maize_Healthy | 0.96 | 0.97 | 0.96 | 128 |
| Maize_Leaf_Spot | 0.91 | 0.88 | 0.89 | 121 |
| Maize_Rust | 0.94 | 0.95 | 0.94 | 117 |
| Potato_Early_Blight | 0.82 | 0.79 | 0.80 | 96 |
| Potato_Late_Blight | 0.84 | 0.81 | 0.82 | 102 |
| Rice_Blast | 0.89 | 0.92 | 0.90 | 110 |
| Wheat_Leaf_Rust | 0.90 | 0.87 | 0.88 | 105 |
注意 precision、recall、f1-score 这三列的差异。precision 低说明预测为该类别的图像里混了很多其他类,recall 低说明该类别的图像被漏判了很多。如果某个类别两者都低,检查该类别的训练样本是否存在大量背景干扰。
此时还应该做一件事:调整分类阈值。默认情况下模型预测取 softmax 输出的最大值,但对样本不平衡的类别,可以给每一类设定独立的置信度阈值。比如马铃薯早疫病的可靠样本只有 96 张,模型对它的平均置信度天然偏低,把阈值从默认的 top-1 改成“置信度超过 0.5 才输出该类别,否则标记为不确定”,虽然会降低硬准确率,但能显著减少误判给另一个病害类别的风险。这个操作在需要下发到田间做辅助诊断的场景里非常有价值,宁可让模型说“我看不准”,也不要让它给出一个高置信度的错误答案。
5. 常见问题与排查:五条能救场的踩坑记录
这一部分我把实际做这类数据集时遇到的五个高频问题整理出来,每个都按现象、原因、解决的顺序写。这五条每一句都是真实翻车换来的,建议先对照自己的流程检查一遍再开始训练。
5.1 数据侧:来自同一植株的叶片同时进了训练集和验证集
现象:训练时验证准确率一路飙到 0.98,你满怀信心地把模型部署到田间,发现实际识别准确率连 0.7 都不到。
原因:数据划分时没有考虑图像来源。同一个植株的不同叶片,或者同一块地里同一天拍的照片,光照、土壤背景、叶片姿态几乎一致,如果它们随机分布到训练集和验证集,模型等于直接背答案,验证指标是假的。这个现象在农业数据集里太常见了,因为采集时通常是对着同一植株拍很多张。解决:划分时必须按植株 ID 或者拍摄批次作为分组单位来分层划分,保证同一个源的图像要么全部进训练集,要么全部进验证集。
5.2 数据侧:类别不平衡让总准确率变成障眼法
现象:整体准确率 0.93,看起来不错,但看混淆矩阵发现甘蔗赤腐病的召回率只有 0.55,三分之一样本被分到了健康类。
原因:甘蔗类别总数少,训练时模型对它的学习不充分,而甘蔗健康样本多,只要把所有样本都预测成健康,总准确率也能维持在一个好看的数值上。解决:第一个手段是统计每个类别的样本数,对样本少的类别在 DataLoader 里做加权采样,让模型每个 epoch 看到各类的次数相对均衡;第二个手段是配合第 6 章的 Focal Loss 使用,它能让模型把注意力放在难分类的少数类别上。
5.3 数据侧:标注噪声集中在早期病害和健康叶片的边界上
现象:验证集上健康类别的 precision 只有 0.85,随机抽 20 张预测为健康的图像,发现里面有几张其实是病害早期症状。
原因:叶片病害早期症状本身就淡,标注员容易出现前后标准不一致,同样一张有少量斑点叶片,一个人标健康,另一个人标病害初期。这是人工标注数据集的通病,不是代码问题。解决:训练前用置信学习工具自动筛查可疑样本,比如先用模型预测所有训练集样本,找出模型置信度高但标注不同的图像,再让有经验的人复核这些边界样本。实际操作中这类边界修复通常能带来 2-4 个百分点的 f1 提升。
5.4 训练侧:验证损失先降后升,模型记住的是背景而不是病斑
现象:训练 loss 持续下降,验证 loss 在某个 epoch 后反弹,验证准确率停滞在 0.85 左右上不去。
原因:训练轮次过多导致过拟合,模型开始记忆叶片背景里的土壤纹理、拍摄环境等与病害无关的特征。这是 13,000 张规模数据集最容易踩的坑,标标准准的过拟合信号。解决:先确认第 3 章代码里的 best_val_acc 保存逻辑在工作,确保拿到的是最佳轮次权重而非最后一轮;其次增加数据增强强度,比如给 ColorJitter 的 brightness 调高到 0.4,让背景颜色变化更剧烈;最后把 weight_decay 从 1e-4 调到 5e-4 再试一轮。如果验证准确率还在 0.85 附近不动,大概率不是过拟合问题,而是某些类别本身难以区分,回到 4.1 看混淆矩阵找问题类别。
5.5 训练侧:学习率设置不当导致损失震荡
现象:训练 loss 在 1.2 附近反复横跳,怎么都降不下去,验证准确率在 0.6 到 0.7 之间剧烈波动。
原因:backbone 的迁移学习初始学习率开太高了。预训练权重已经学得很充分,对它们用 1e-3 的学习率相当于在已经收敛的平面上乱跳,SGD 的动量还会放大这种震荡。解决:把 backbone 参数的学习率降到 5e-5 或 1e-5,fc 层保持 1e-3 不变,同时给优化器加一个 warm-up:前 2 个 epoch 学习率从 0 线性升到目标值。在 torchvision 里实现 warm-up 需要自己做调度器包装,但效果是立竿见影的,特别是对甘蔗这类样本少的作物,训练曲线会立刻稳下来。
6. 进阶技巧:Focal Loss、测试时增强与注意力可视化
训练收敛之后,如果你希望模型质量再上一个台阶,常规的加数据、调参已经接近天花板,这时候该考虑的是三个针对性技巧。
第一个是 Focal Loss。4.2 里提过,类别不平衡和难例并存时,CrossEntropyLoss 对已分类正确的简单样本贡献了太多梯度,Focal Loss 通过调制因子压低简单样本的权重,把梯度让给难分类样本:
class FocalLoss(nn.Module): def __init__(self, alpha=0.25, gamma=2.0): super().__init__() self.alpha = alpha self.gamma = gamma def forward(self, inputs, targets): ce_loss = nn.functional.cross_entropy(inputs, targets, reduction="none") pt = torch.exp(-ce_loss) loss = self.alpha * (1 - pt) ** self.gamma * ce_loss return loss.mean()alpha 控制正负样本权重比例,gamma 控制难易样本的调制强度,gamma 越大对难样本的注意力越强,一般取 2.0 起步。这个损失在甘蔗赤腐病这种少数类上通常能拉高 5 个百分点以上的召回率,但要注意 gamma 过大会让模型对噪声标签更敏感,配合 5.3 的置信学习清洗后使用更稳。
第二个技巧是测试时增强,即推理时把同一张图做多次变换后取平均预测。常见做法是把水平翻转后的预测结果和原图预测结果做平均,代码实现很简单:
def predict_tta(model, image): output = model(image.unsqueeze(0).to(device)) flipped = torch.flip(image, dims=[2]) output += model(flipped.unsqueeze(0).to(device)) return torch.softmax(output, dim=1).mean(dim=0)TTA 对验证集准确率的提升通常只有 0.5-1%,但换来的是单张图像预测的稳定性提升,这对农业现场采集到的模糊图像很有实际意义。
第三个是注意力可视化。用 Grad-CAM 输出模型聚焦区域,贴到原始叶片图上,能直观确认模型看的是病斑而不是叶片边缘或背景土壤。我在做这个五作物数据集时检查过一次,发现部分小麦条锈的预测是靠着叶片边缘纹理判出来的,而不是锈病孢子堆区域。定位到这个问题后,我在增强里增加了 RandomErasing 和 Cutout,强制模型不能只依赖某一小块局部特征。
从那以后,每次做完一轮训练,我都会强制走一遍混淆矩阵、类别阈值、Grad-CAM 三个步骤再定稿权重,这个习惯替我省掉了至少三次实测返工。需要这份数据集跑通整个流程的话,按标题搜完整名称就能找到下载地址,解压后先按第 2 章把目录结构对一遍,再对照第 3 章的参数开训,有问题随时回来对第 5 章的清单排查。希望帮到你。
本文还有配套的精品资源,点击获取