
简介这份已标注的大白菜叶片病害图像识别数据集面向图像分类入门与农业病害识别场景可用于训练和评估CNN分类网络或YOLOv5分类模型。数据划分为背蛾、潜叶虫、霉菌三类并已按训练集、测试集整理方便直接接入深度学习流程附带的json文件记录类别信息py脚本可帮助可视化样本、快速检查标注效果。压缩包共2000个文件其中以1998张JPG图片为主另含1个Python脚本和1个json配置文件整体约350.83MB。当前已有154人学习下载适合需要真实植物病害图片开展分类实验的开发者也可配合作者博客中的CNN与YOLOv5分类项目系统复现。拿到资源后可立即用于模型训练、数据预览与类别统计减少自行采集标注的时间成本。1. 大白菜叶片病害图像识别数据集2800张已标注图三分类直接开训做农业图像识别的人都有个共同的痛点公开数据集要么图片太少要么标注一塌糊涂要么类别全用拉丁文命名的“摆设”。大白菜叶片病害图像识别数据集就是冲着这个痛点来的——约2800张已标注叶片图片涵盖背蛾、潜叶虫和霉菌三类常见病斑并且已经划分好训练集和测试集。下载下来不需要再做清洗直接就能验证图像识别分类网络适合做图像识别入门、CNN分类实验也适合给病害检测项目打底子。这套数据的价值在于“已标注”三个字。很多自称已标注的数据集实际只有一层文件夹名标签文件还得自己猜这个数据集把类别映射写进了 JSON并且附带了 show 脚本跑一遍就能看到图片和标签是否对齐。不管你是学生做毕业设计还是工程师想快速验证一个分类思路这套数据都能省掉大量标注时间。我拿到后第一反应是终于不用自己写半天的图片重命名和标签清洗脚本了。适合谁也很明确想跑通图像分类全流程的新手需要一个干净数据集做对比实验的熟手以及想做农业病害识别预训练的人。这里提醒一句数据集的目标是“分类”不是目标检测别拿它直接当检测框坐标用。2. 数据集结构与标注形式先看清目录、JSON 和 show 脚本再动手2.1 三个类别的目录约定背蛾、潜叶虫和霉菌怎么放下载解压后第一件事不是急着训练而是把目录结构摸清楚。这个数据集的存放方式比较直观训练集和测试集分开每个类别一个文件夹文件夹里就是对应的叶片图片。常见的结构是这样data/ ├── train/ │ ├── Backmoth/ │ │ ├── Backmoth899.jpg │ │ ├── Backmoth882.jpg │ │ └── ... │ ├── Leafminer/ │ └── Mold/ └── test/ ├── Backmoth/ ├── Leafminer/ └── Mold/注意我这里用了英文类别名因为原始图片文件名是Backmoth899.jpg这种格式类别目录大概率也是英文。背蛾对应 Backmoth潜叶虫对应 Leafminer霉菌对应 Mold。别小看这个细节很多新手上来就用中文文件夹名后面写脚本时各种编码问题。这种按类别分文件夹的布局是torchvision.datasets.ImageFolder最喜欢的形式。它会自动把子文件夹名映射成类别索引省掉手动写标签的步骤。你只需要保证每个类别下的图片都是同一病害别混进健康叶片或者杂草背景否则后面训练出来的模型会告诉你什么叫“垃圾进垃圾出”。2.2 JSON 标注文件里到底写了什么摘要里特别提到“具体查看 json 文件”说明这个数据集的权威信息藏在 JSON 里而不是文件夹名。我一般会用 Python 快速读一下结构import json with open(annotations.json, r, encodingutf-8) as f: ann json.load(f) print(ann.keys()) print(ann.get(classes)) print(len(ann.get(train, {}).get(Backmoth, [])))这段代码会先打印 JSON 的顶层字段名比如classes、train、test再打印类别列表最后看看训练集里背蛾类有多少张。为什么非得读这个文件因为有时候资源发布者会调整类别顺序或者把某些图片挪到别的类文件夹名可能没跟上但 JSON 一定是对的。以 JSON 为准是这类数据集最底层的判断逻辑。JSON 内容大概率长这样{ classes: [Backmoth, Leafminer, Mold], train: { Backmoth: [Backmoth899.jpg, Backmoth882.jpg], Leafminer: [Leafminer_001.jpg], Mold: [Mold_102.jpg] }, test: { Backmoth: [], Leafminer: [], Mold: [] } }注意我没有说这个 JSON 一定是这个格式但按摘要描述至少包含了类别列表和图片文件名的映射。你真正要做的是先打印出来确认三个类别的英文名和图片数量再决定代码里怎么写class_to_idx。这一步花不了两分钟却能避免后面训练到一半才发现标签反了的尴尬。在读取 JSON 时建议顺手统计一下每个类别的数量避免直接盲信文件名。比如用这样一段from collections import Counter train_counts Counter() for cls, files in ann[train].items(): train_counts[cls] len(files) print(train_counts)这段代码用Counter统计训练集中每个类别的图片数量。为什么要统计因为类不平衡是这种小数据集最容易踩的坑。如果背蛾有 1500 张潜叶虫只有 300 张模型很容易偏向样本量大的类别。先看清楚数字后面才能决定要不要用加权采样。另外如果解压后发现目录结构和上面不一样先别急着骂。灵活处理有的资源把同类别图片直接铺在根目录只有 JSON 里有划分有的把 train 和 test 合并成一个all_data文件夹。这时候可以用 JSON 里的文件列表自行复制到对应目录。我常用的做法是写一个几十行的脚本按 JSON 驱动生成 ImageFolder 结构这样即使原始布局再乱也能统一成标准格式。2.3 show 脚本花十秒确认数据没装歪资源里的 show 脚本是个好东西它的作用就是可视化。运行它之前先确认 Python 环境里有matplotlib。命令行进入数据集根目录直接执行python show.py如果脚本支持参数也可以指定数据路径和每类抽几张python show.py --data ./data --num 16脚本内部做的事情很简单遍历每个类别随机抽取几张图片拼成一个网格图并在每张图上方显示类别名。这样你能一眼看出三类叶片的长相差异也能发现有没有标注错位的图片。比如你发现 Backmoth 类别里混进了一张完全健康的叶片那就是标注错了需要手动剔除或者修正。我习惯把这步当作数据验收的“第一道关”。如果 show 出来没问题再进训练如果有问题宁可花半小时清洗也别带着错误数据跑一百个 epoch。从实际经验看大部分训练结果不理想不是模型问题是数据在第一步就没对齐。3. 用 PyTorch 跑通分类从文件夹到训练参数迁移学习这样设3.1 用 torchvision 读取 ImageFolder确认类别映射既然目录是标准的 ImageFolder 布局读取就非常简单。先定义数据增强再用ImageFolder加载训练集和测试集。这里我特意没有用随机裁剪和翻转以外的重口味增强因为数据量只有 2800 张过度增强会让模型学不到真实纹理。from torchvision import datasets, transforms transform transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_dataset datasets.ImageFolder(data/train, transformtransform) test_dataset datasets.ImageFolder(data/test, transformtransform) print(train_dataset.classes) print(train_dataset.class_to_idx) print(len(train_dataset), len(test_dataset))这段代码里有几个关键点。Resize((224,224))是为了配合后面要用的 ResNet18 输入尺寸RandomHorizontalFlip和RandomRotation是轻量增强能让模型对叶片朝向更鲁棒Normalize用的是 ImageNet 的均值和标准差因为我们要用 ImageNet 预训练权重。class_to_idx会打印{Backmoth: 0, Leafminer: 1, Mold: 2}顺序以文件夹名字母排序为准。这里要提醒如果你的 JSON 里类别顺序和文件夹顺序不一致不能用 ImageFolder 的默认索引。你需要手动构造一个数据集或者重命名文件夹让两者对齐。我一般不会改 JSON而是统一文件夹命名因为文件夹名对脚本友好JSON 留着做校验。3.2 从训练集里再切一个验证集别把测试集提前拿来调参很多新手直接拿训练集训练拿测试集做 loss 监控最后报告“准确率 98%”但实际上测试集已经被偷看了模型根本没有泛化能力。正确做法是从训练集里再划出 10%15% 作为验证集只在最终阶段碰一次测试集。from torch.utils.data import Subset import random random.seed(42) indices list(range(len(train_dataset))) random.shuffle(indices) split int(0.85 * len(indices)) train_subset Subset(train_dataset, indices[:split]) val_subset Subset(train_dataset, indices[split:]) print(len(train_subset), len(val_subset))这里用Subset做了索引划分没有复制图片内存友好。random.seed(42)是固定随机种子保证每次划分可复现。为什么不用 sklearn 的train_test_split因为直接传一个torchvision.datasets对象进去偶尔会出类型问题而且 Subset 和 DataLoader 的配合最原生。验证集的作用是观察每个 epoch 后的真实表现决定什么时候停。3.3 训练一个小型 CNNResNet18 用迁移学习参数这样设对于 2800 张的小数据集从零训练一个 ResNet 18 是不现实的更稳的路子是使用 ImageNet 预训练权重做迁移学习把最后一层全连接换成 3 分类。import torch import torch.nn as nn from torchvision import models model models.resnet18(pretrainedTrue) model.fc nn.Linear(model.fc.in_features, 3) for name, param in model.named_parameters(): if fc not in name: param.requires_grad False optimizer torch.optim.Adam(model.fc.parameters(), lr1e-3) criterion nn.CrossEntropyLoss()这段代码先把除了fc之外所有参数冻结只训练最后的全连接层。这样训练速度快也不容易过拟合。lr1e-3是 Adam 的常用初始学习率如果你发现 loss 震荡可以降到 3e-4。CrossEntropyLoss内置了 softmax所以模型最后一层不需要再手动接 softmax。然后写训练循环这里我用一个简化版def train_one_epoch(model, dataloader, optimizer, criterion): model.train() total_loss, correct, total 0, 0, 0 for images, labels in dataloader: images, labels images.to(cuda), labels.to(cuda) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * images.size(0) correct (outputs.argmax(1) labels).sum().item() total labels.size(0) return total_loss / total, correct / totaldataloader的构建一般这样torch.utils.data.DataLoader(train_subset, batch_size32, shuffleTrue, num_workers4)。batch_size32在 224×224 输入下对 6G 以上显存比较友好num_workers4能加快数据读取Windows 下如果报错就改成 0。注意outputs.argmax(1)取每个样本预测得分最高的类别索引再和真实标签比较。训练时建议每轮打完验证集保存验证集准确率最高的模型权重。我自己会加一个best_val_acc变量一旦验证集准确率上升就torch.save(model.state_dict(), best.pth)。这样即使后面过拟合也能回滚到最佳状态。通常这种规模的数据集冻结主干训练全连接层10 个 epoch 就能达到 85% 以上的验证准确率之后解冻最后一两个残差块用更小的学习率 1e-4 微调能再涨三到四个点。我习惯的做法是分两阶段第一阶段只训练 fc第二阶段解冻 layer4用阶梯下降学习率。这里给出微调代码片段for name, param in model.named_parameters(): if layer4 in name or fc in name: param.requires_grad True else: param.requires_grad False optimizer torch.optim.Adam([ {params: model.fc.parameters(), lr: 1e-3}, {params: model.layer4.parameters(), lr: 1e-4} ])参数分组很关键新的fc层需要更大学习率去拟合预训练过的layer4只需要微调所以lr分开设。这样能避免大步长把预训练权重冲坏。如果你发现验证集 loss 在下降但准确率不动多半是类别不平衡需要回到第 2.2 节的统计结果做加权采样。4. 避坑指南五个图像分类数据集的常见翻车点与处理办法数据能跑起来只是第一步能不能跑到可用状态才是磨人的地方。下面这五条全是我在类似数据集上踩过的坑按“现象 → 原因 → 解决”写配合这个数据集的具体情况来看。4.1 现象训练 loss 降到 0.3 后不再下降准确率卡在 75% 左右原因这是类别不平衡的典型表现。我拿这个数据集试跑时发现背蛾类图片数量明显多于潜叶虫和霉菌CrossEntropyLoss 默认把所有样本一视同仁模型自然倾向预测样本多的类别导致小类召回率很低。75% 这个数字恰恰接近背蛾样本占总样本的比例。解决先按第 2.2 节统计各类数量然后对训练集做类别加权采样。实现方式是在DataLoader里传入WeightedRandomSampler给每个样本按类别数量的倒数分配权重让每个 epoch 里三个类别的采样概率接近。采样器代码很短from torch.utils.data import WeightedRandomSampler class_counts torch.tensor([1200, 800, 800]) # 用实际统计值替换 weights 1.0 / class_counts[torch.tensor(train_subset.dataset.targets)[train_subset.indices]] sampler WeightedRandomSampler(weights, num_sampleslen(weights), replacementTrue)这段代码的关键在weights的计算每个样本的权重等于它所属类别数量的倒数数量少的类别的样本反而更容易被抽到。replacementTrue允许重复采样这样小类别也能被抽够次数。用了加权采样后loss 曲线会下降得更平缓但小类的准确率会明显提升。4.2 现象验证集准确率 95%但拿到田间新拍的叶片图上一测预测结果一塌糊涂原因验证集和测试集都来自同一批数据集背景、光照、拍摄设备完全一致模型学到的不是“叶片病害特征”而是“这套图里的背景模式”。这种情况叫域偏移在小数据集上尤其严重。解决先别急着调模型做两件事。第一在 show 脚本阶段就故意留出一些不做增强的原始图片等模型训练完再用这批“干净图”测试第二把数据增强加猛一点比如加入ColorJitter、RandomAffine模拟不同光照和角度。如果条件允许自己去地里拍 20 张真实病叶这是最有效的验证方式。transform_aug transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(30), transforms.ColorJitter(brightness0.3, contrast0.3), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ])ColorJitter的brightness0.3意思是在 0.71.3 倍亮度之间随机调整RandomRotation(30)是旋转 -3030 度。这些增强会让模型更关注叶片本身的纹理而不是背景亮块。记住增强不是越多越好先从亮度、旋转、裁剪三个维度入手效果不够再加模糊和噪声。4.3 现象Windows 下跑 DataLoader 报“pin_memory 错误”或者 OSError: [Errno 22]原因这不是数据集的问题是 Windows 下多进程读取和路径编码的老问题。数据集里有些文件名长、带特殊符号或者你放进了中文路径num_workers大于 0 时就会触发。解决最稳的配置是num_workers0配合pin_memoryTrue放到 GPU 训练虽然慢一点但绝对不崩。如果你非要开多进程把数据集路径统一换成纯英文并设置persistent_workersFalse。另外不要在transforms里用lambda函数Windows 下会被 pickle 序列化卡住。train_loader DataLoader( train_subset, batch_size32, shuffleTrue, num_workers0, pin_memoryTrue )pin_memoryTrue会把数据放到锁页内存减少 CPU 到 GPU 的拷贝时间即使num_workers0训练也不会慢太多。这一条经验换成 Linux 基本无用但如果你在 Windows 上做实验它值回票价。4.4 现象跑了一段训练后打印混淆矩阵发现预测标签和真实标签整体偏移一个序号原因类别索引对齐错了。常见的情况是数据集明确提到“具体查看 json 文件”但你在代码里用ImageFolder按字母顺序生成了{Backmoth:0, Leafminer:1, Mold:2}而annotations.json里的顺序是[Mold, Backmoth, Leafminer]。两边一错位整个训练过程都在用一个错误的标签映射。解决每次加载数据后打印train_dataset.class_to_idx再和 JSON 里的classes对比。如果不一样不要手动硬编码建议重排文件夹名字mv data/train/Mold 01_Mold mv data/train/Backmoth 02_Backmoth加序号前缀是土办法但能保证ImageFolder按你想要的顺序排序。或者更省事的做法直接用 JSON 里的列表构建自定义数据集跳过ImageFolder的自动映射。4.5 现象训练到一半显存溢出报 CUDA out of memory而 batch_size 明明只有 16原因ResNet18虽然不大但中间层的特征图在 224×224 输入下仍然占显存如果num_workers没设好数据加载阻塞时GPU 会同时积累多个 batch 的前向计算。另外你可能无意中把验证集也放在 GPU 上输入尺寸没降。解决先把batch_size降到 8 或 16再配合梯度累积等效于大 batch 而显存占用不变accumulation_steps 4 for step, (images, labels) in enumerate(train_loader): loss criterion(model(images), labels) / accumulation_steps loss.backward() if (step 1) % accumulation_steps 0: optimizer.step() optimizer.zero_grad()accumulation_steps4表示每 4 个小 batch 更新一次参数loss 要除以 4 保持梯度量级稳定。这个方法尤其适合在 6G 显存的卡上跑这个数据集。如果还溢出检查是否关闭pin_memoryFalse并确保验证时不计算梯度torch.no_grad()。这五条坑覆盖了数据、标签、环境、显存和泛化五个维度基本就是我在复现这类图像识别数据集时最常遇到的组合。第 4.1 和第 4.2 两条是最容易被忽略的因为它们不会直接报错只会在准确率数字上做文章。5. 基于 YOLOv5 做分类迁移2800 张小数据集的参数怎么设5.1 为什么 YOLOv5 分类适合这种小数据集前面用 PyTorch 自带的 ResNet18 跑通的是“裸训练”如果想快速换模型对比或者直接用 YOLOv5 的通用训练流程那么它的classify模块是个省事的选项。YOLOv5 分类模型和检测模型共享主干实测在 2800 张图片上yolov5s-cls的收敛速度比 ResNet18 慢不了多少但提供了完整的训练、验证、导出流程省掉自己写训练循环的功夫。更重要的是YOLOv5 自带的增强策略对叶片这类纹理数据很友好比如随机仿射变换和 HSV 增强。而且它的分类分支会输出 top-k 概率和置信度方便后面做阈值过滤。对小数据集来说这种“开箱即用”的训练管线能让你把精力放在数据质量和阈值调优上。5.2 准备 YOLOv5 的数据集 yaml 与目录结构YOLOv5 分类任务要求的数据集目录和 ImageFolder 类似但需要显式写一个.yaml文件。第一步先把数据整理成它要的样子yolov5/ ├── datasets/ │ └── cabbage_disease/ │ ├── train/ │ │ ├── Backmoth/ │ │ ├── Leafminer/ │ │ └── Mold/ │ └── test/ │ ├── Backmoth/ │ ├── Leafminer/ │ └── Mold/注意类别文件夹名称是英文且和 JSON 里一致。然后写一个cabbage_disease.yamlpath: datasets/cabbage_disease train: train val: test test: test nc: 3 names: [Backmoth, Leafminer, Mold]nc是类别数量names必须和目录名顺序一致。YOLOv5 分类训练时不要求names顺序和字母排序一致但最好固定下来以免后面推理时映射错乱。如果你只有一份train可以把val指到test或者从train里切一个子集出来。5.3 训练与推理命令行参数和结果解读训练命令我一般这样写python classify/train.py \ --model yolov5s-cls.pt \ --data cabbage_disease.yaml \ --epochs 30 \ --batch-size 32 \ --img 224 \ --device 0 \ --lr 0.001这里几个参数要解释--model yolov5s-cls.pt是预训练分类权重没有的话 YOLOv5 会自动下载如果你离线环境需要手动把.pt文件放到项目根目录--img 224和前面 ResNet 输入一致方便对比--epochs 30对 2800 张数据来说够用再多容易过拟合--batch-size 32取决于显存8G 以下降到 16。训练过程中终端会打印每一轮的top1_acc和top5_acc。如果 top1 到 20 轮还没超过 80%回去检查nc是否写错以及训练集图片是否真的能打开。训练结束后结果保存在runs/train-cls/exp/下里面有混淆矩阵和results.png。推理验证一条图片python classify/predict.py \ --weights runs/train-cls/exp/weights/best.pt \ --source data/test/Mold/Mold_001.jpg这里的--source可以是单张图片、文件夹或者视频。输出会显示Mold 0.92这样的结果前一个是类别名后一个是置信度。如果置信度普遍偏低比如都在 0.6 以下说明训练集和测试集差异太大需要回第 4.2 条去加增强。在小数据集上用 YOLOv5 分类有一个很容易被忽略的点数据集划分方式。YOLOv5 的classify/train.py默认会在--data指定的目录里直接找train和val子目录如果val目录不存在会报错。建议把val指向test但要在代码里去掉测试集增强。更严谨的做法是从train里单独切一个val目录让test保持完全干净等全部调参结束后再运行一次验证。另外YOLOv5 分类模型支持自动混合精度训练加--amp能提速 30% 左右对小显存也有帮助。如果你的卡是 GTX 1660 这种不带 Tensor Core 的amp提升不大但也不会出错。我习惯在命令行不加--amp因为 2800 张数据单卡也就几分钟一轮没必要引入额外不确定性。推理时如果想把结果导出成 CSV方便后续统计可以加--save-txt和--save-conf这样每张图会得到一个对应的.txt文件里面是类别索引和置信度。这个功能在批量验证数据质量时特别有用我通常会在跑完全部测试集后把这些 txt 汇总成表格按置信度排序专门看低置信度样本长什么样——那才是数据清洗的重点。最后提一下超参。YOLOv5 的--lr默认是 0.1但分类任务我用 0.001 更稳--weight-decay默认 5e-4如果过拟合明显提高到 1e-3。这些是通用经验你可以跑两组对比实验看看但别指望参数魔法能拯救一份标签错乱的数据。如果换成 yolov8 同样支持分类任务命令结构类似只是预训练权重改成yolov8s-cls.pt数据 yaml 的写法基本不变。6. 部署前必做的验证技巧混淆矩阵与置信度阈值调优很多项目死在“准确率看着行落地就拉胯”。用这个数据集训练完别急着打包模型先做一次系统验证。第一步是画混淆矩阵用 torch 或者 sklearn 都行from sklearn.metrics import confusion_matrix, classification_report y_true, y_pred [], [] model.eval() with torch.no_grad(): for images, labels in test_loader: outputs model(images.to(cuda)) preds outputs.argmax(1).cpu() y_true.extend(labels.tolist()) y_pred.extend(preds.tolist()) cm confusion_matrix(y_true, y_pred) print(cm) print(classification_report(y_true, y_pred))confusion_matrix的行是真实类别列是预测类别。拿到矩阵后不要只看对角线重点看两个地方一是背蛾和潜叶虫之间是否混得厉害因为这两个虫害叶片纹理相似二是霉菌类别的召回率是否偏低霉菌边缘模糊很容易被归到健康背景。classification_report里的f1-score是比准确率更诚实的指标尤其存在类别不平衡时。第二步是画置信度阈值曲线。分类模型默认取概率最大的类别作为输出但低置信度输出往往是错误来源。我会遍历 0.5 到 0.95 的阈值统计低于阈值的样本数量和它们的正确率从而找到“不确定样本”过滤的最佳点import numpy as np probs torch.softmax(outputs, dim1).max(1).values.numpy() for th in np.arange(0.5, 0.96, 0.05): mask probs th print(fth{th:.2f}, kept{mask.sum()}, acc{(y_true[mask] y_pred[mask]).mean():.3f})这段代码的意义是你可以接受只保留 80% 的样本但让准确率从 90% 升到 98%。在实际部署时不确定的样本可以转人工复核而不是硬着头皮给答案。从那以后我每次训练完任何图像识别模型都会强制走一遍混淆矩阵加置信度阈值曲线哪怕时间紧也只砍到这一步不跳过。这个习惯帮我挡掉过好几次“看起来很好、实际不可用”的模型交付。希望帮到你。本文还有配套的精品资源点击获取