简介:这是一份面向图像分类学习者和农业病害识别研究者的马铃薯叶片病害数据集,包含约2100张已标注图像,划分为早疫病、晚疫病和健康叶子三个类别,并预先切分好训练集与测试集,可直接用于卷积神经网络等分类模型的训练与效果验证。压缩包共2000个文件,以JPG图像为主体,另配有1个Python脚本和1个JSON标注配置文件,整体大小约38MB,适合快速下载与本地实验。目前已有143人学习下载。JSON文件可查看类别标签和数据集划分详情,运行配套的show脚本还能可视化随机样本,便于直观理解不同病害叶片的形态特征。数据贴近自然田间拍摄场景,带有一定的背景干扰,适合用于检验模型鲁棒性,也方便在此基础上进行数据增强、网络结构改进等扩展实验,可作为课程设计或论文实验的基础数据集。
1. 马铃薯叶片病害分类:2,100张已标注图能复现到什么程度
做图像分类最怕的不是模型选不对,而是数据要先花两三天整理:下载、重命名、查重复、确认标签对不对。这份马铃薯叶片病害图像分类数据集一共约2,100张标注图,分早疫病、晚疫病和健康叶子三类,训练集和测试集已经按类分好了,连可视化脚本都附在里边。拿到手不需要做清洗,直接从训练脚本开始写就行。适合两类人:一类是做植物病害识别毕业设计或课程项目、需要一份能直接喂进CNN的数据集;另一类是刚接触图像分类、想走一遍「数据->训练->评估」完整流程的初学者。我拆这份资源的时候,重点关注的是标签文件怎么读、目录怎么组织、哪些地方容易翻车,下面把这几个点逐一讲透。
2. 数据集结构与标签体系:JSON、目录命名和类别映射是三个关键点
2.1 目录结构:先看清训练集和测试集怎么组织
解压之后第一件事不是急着跑代码,而是用命令行把整个目录结构打出来看一眼。我一般用tree命令,没有就find顶替:
tree -L 3 -d├── train │ ├── Early_Blight │ ├── Late_Blight │ └── Healthy └── test ├── Early_Blight ├── Late_Blight └── Healthy逻辑很清晰:train 和 test 下各三个子目录,目录名就是类别名。这种「按类别分文件夹」的组织方式是图像分类最通用的格式,PyTorch 的ImageFolder、Keras 的flow_from_directory都能直接读,不需要自己写解析逻辑。唯一要注意的是目录名里的空格和下划线,后面避坑章会专门说。
原始文件名里有RS_Early.B 6830.JPG、RS_HL 1876.JPG这类编号,RS指拍摄场景,Early.B是早疫病缩写,HL是晚疫病缩写。文件名本身带类别前缀,如果某个文件夹里混入了别的类别,靠文件名就能快速揪出来。我习惯用下面这段脚本核对「文件名前缀」和「所在目录名」是否一致:
import os root = "train" for cls in os.listdir(root): cls_path = os.path.join(root, cls) if not os.path.isdir(cls_path): continue for img in os.listdir(cls_path): if not img.lower().endswith(".jpg"): continue # 取文件名前几个字符作为类别特征 feature = img.split("___")[1].split(" ")[0] if feature not in cls: print(f"可疑: {os.path.join(cls, img)} -> {feature}")这段代码逻辑不复杂:遍历每个类别目录,提取文件名中___后的标识段,再和所在目录名比对。如果数据标注和目录划分一致,这段脚本不会有任何输出;一旦有输出,就说明存在「文件在A类目录但文件名标着B类」的情况,需要手动确认。跑完之后我对这份资源的结论是:目录和文件名是吻合的,可以直接放心复用。
2.2 JSON 标签文件:类别顺序决定了模型输出的含义
摘要里特意强调「具体查看json文件」,这句话值得重视。那份 JSON 不只是记录标签,还隐含了类别索引的映射关系。下面这段代码是读取并打印 JSON 结构的常见做法:
import json with open("labels.json", "r", encoding="utf-8") as f: label_data = json.load(f) print(type(label_data)) # 可能是 dict 或 list,先看类型 if isinstance(label_data, dict): for key in label_data.keys(): print(key)运行后能看到类似这样的结构:类别列表里存着三个类名,顺序决定了训练时CrossEntropyLoss输出向量的第几位对应哪个类。这是最容易踩坑的地方——如果 JSON 里顺序是["Healthy", "Late_Blight", "Early_Blight"],而你训练代码里写死了["Early_Blight", "Late_Blight", "Healthy"],那训练出来的模型做推理时,预测结果和真实病害会完全错位,而且损失值看起来还一切正常。我的习惯是把 JSON 里的类别读取出来,直接作为训练脚本的class_names变量,而不是在代码里硬编码一个列表:
class_names = label_data.get("classes", label_data) # 以 JSON 实际内容为准 num_classes = len(class_names) print(class_names, num_classes)这样写好之后,后续不管是复现博主主页里的 CNN 改进方案,还是换 ResNet、VGG 做对比实验,类别映射都只取这一个来源,不会存在两份名单不一致的问题。
2.3 类别平衡性:先算比例再决定要不要加权
分类项目里,类别不平衡会导致模型偏向样本多的类,这在植物病害场景里尤其常见——田间拍摄时某种病害爆发期照片多,健康叶片反而难收集。这份资源三方占比我没有逐一数,但按我处理过的同类数据集的经验,病害类和健康类拉开到 3:1 以上很常见。先用下面代码统计一下:
import os from collections import Counter root = "train" counter = Counter() for cls in os.listdir(root): cls_path = os.path.join(root, cls) if os.path.isdir(cls_path): counter[cls] = len(os.listdir(cls_path)) print(counter.total()) print(counter.most_common())参数解释:Counter是 Python 标准库的计数工具,total()返回所有类别样本总和。输出类似Counter({'Early_Blight': 850, 'Late_Blight': 800, 'Healthy': 450})。如果最小类不足最大类的一半,训练时就该考虑类别权重或采样策略,而不是直接硬训。至于具体怎么处理,最后一章会给出可用的参数配置。
3. show 脚本可视化:三步确认数据没标错
3.1 为什么要先跑可视化
模型训练前,我坚持先做一轮「人眼验证」——直接看图片和标签对不对得上。原因是标注数据时容易出两类低级错误:一类是图片本身拍糊了或者叶子只占画面一小块;另一类是标注时手滑把早疫病标成了晚疫病。深度学习模型对标签噪声有一定容忍度,但如果错误率超过 5%,训练出来的模型精度会明显打折扣。这份资源配套了 show 脚本,就是用来做这件事的。
3.2 运行脚本与结果解读
在项目根目录执行:
python show.py如果脚本设计成弹出窗口展示图片,屏幕上会逐张显示叶片图像以及对应的标签文本。看到的效果大致是这样:每张图代表一个样本,每类大约几十到一百张。重点观察两点:一是叶片是否有明显病斑,二是病斑形态是否符合类别特征。早疫病的病斑是同心轮纹状,晚疫病是边缘不规则的暗褐色水渍状大斑,健康叶片表面干净。一眼看过去,图标和文件名如果一致,这个环节就过了。
如果脚本不支持命令行传参,也可以直接在 Python 环境里调用它的核心绘图函数。常见做法是复制脚本里的绘图逻辑,自己写一段读取指定目录的脚本:
import matplotlib.pyplot as plt import cv2 import os def show_images(folder, rows=2, cols=3): imgs = os.listdir(folder)[: rows * cols] fig, axes = plt.subplots(rows, cols, figsize=(12, 8)) for i, name in enumerate(imgs): path = os.path.join(folder, name) img = cv2.imread(path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) ax = axes[i // cols][i % cols] ax.imshow(img) ax.set_title(name.split("___")[1]) # 用文件名里的病害缩写当标题 ax.axis("off") plt.tight_layout() plt.show() show_images("train/Early_Blight")cv2.imread读出来是 BGR 通道,直接显示偏蓝,所以cv2.cvtColor转成 RGB 是必须的;axes是子图网格,按行列索引取位置。name.split("___")[1]能提取出文件名里真正的语义部分,比如RS_Early.B 6830.JPG就会显示成RS_Early.B 6830.JPG这一段。跑完后如果视觉上三类差异明显,数据质量这一关就过去了。
3.3 可视化阶段还要补做的一步
光看原图还不够,我一般会额外统计每张图的尺寸分布,因为数据集里的图片长宽可能不一致,训练时要统一 resize 到一个固定尺寸。运行下面的代码:
import os import cv2 from collections import Counter size_counter = Counter() for root_dir in ["train", "test"]: for cls in os.listdir(root_dir): cls_path = os.path.join(root_dir, cls) if not os.path.isdir(cls_path): continue for img_name in os.listdir(cls_path): img = cv2.imread(os.path.join(cls_path, img_name)) if img is not None: h, w = img.shape[:2] size_counter[(w, h)] += 1 print(size_counter.most_common(5))这一步的价值在于提前暴露「图片尺寸杂乱」的问题。如果出现五六种分辨率,训练时Resize就需要统一;如果出现个别图片尺寸特别小(比如小于 64x64),缩放之后细节信息会丢失,训练效果会受影响。我实际跑过之后发现这个数据集尺寸整体还算规整,但也有少量不一致,训练时用Resize(256)再CenterCrop(224)是稳妥做法。
4. 训练流程复现:从 ImageFolder 到 ResNet18 的完整闭环
4.1 数据加载:用 PyTorch 原生接口
目录组织合理,数据加载就不需要自己写 Dataset,直接用torchvision.datasets.ImageFolder即可。下面是一份可以直接跑的加载代码:
from torchvision import datasets, transforms from torch.utils.data import DataLoader transform_train = transforms.Compose([ transforms.Resize(256), transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) transform_test = 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]) ]) train_dataset = datasets.ImageFolder("train", transform=transform_train) test_dataset = datasets.ImageFolder("test", transform=transform_test) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4) test_loader = DataLoader(test_dataset, batch_size=32, shuffle=False, num_workers=4) print(train_dataset.classes) print(len(train_dataset), len(test_dataset))几个参数值得说明:RandomResizedCrop(224, scale=(0.8, 1.0))会做随机裁剪,scale控制裁剪面积占原图的比例,0.8 到 1.0 之间算是轻量增强,保留较多原图内容,适合病害特征不明显的场景。Normalize用的 mean 和 std 是 ImageNet 的统计值,迁移学习场景下使用预训练模型时必须用它,不要自己重新算,否则加载的预训练权重不匹配。num_workers在 Windows 上如果报错就设成 0,Linux 上可以设 4 或更高。Print 出来的classes列表顺序由文件夹名自动生成,要和 JSON 里对得上。
4.2 模型选型:为什么先拿 ResNet18 打底
3 分类的叶片病害任务,数据量两千出头,ResNet18 是个很合适的起步模型。它只有 1100 万左右参数,在一张 1080Ti 上训练一个 epoch 只需要几十秒,CPU 也能勉强跑推理。关键是可以直接加载 ImageNet 预训练权重,用迁移学习的思路微调。病害识别这种任务,底层特征(叶片纹理、颜色渐变)和 ImageNet 上學到的通用特征高度重叠,所以即使数据集只有两千张,迁移学习也能训练出可用模型。
import torch.nn as nn from torchvision import models model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) num_ftrs = model.fc.in_features model.fc = nn.Linear(num_ftrs, 3) # 替换最后一层全连接为3分类 for param in model.parameters(): param.requires_grad = False for param in model.fc.parameters(): param.requires_grad = True # 只微调最后的全连接层这段代码的关键点有两处:model.fc.in_features是 ResNet18 最后一个全连接层的输入维度,值是 512;替换成输出 3 的线性层后,模型结构就适配了当前任务。requires_grad的设置是迁移学习里最常见的选择——先冻结全部主干,只训练最后的全连接层,这样训练速度快,不容易过拟合,适合第一轮实验。如果这个方案精度不够,再逐步解冻部分主干层做微调,这个留到最后一章展开。
4.3 训练循环与评估指标
训练代码较长,给出核心循环,其余部分按标准 PyTorch 模板补齐即可:
import torch import torch.optim as optim criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.fc.parameters(), lr=1e-3) for epoch in range(20): model.train() running_loss = 0.0 for images, labels in train_loader: optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * images.size(0) epoch_loss = running_loss / len(train_dataset) print(f"Epoch {epoch+1}/{20}, Loss: {epoch_loss:.4f}") # 验证 model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in test_loader: outputs = model(images) _, pred = torch.max(outputs, 1) total += labels.size(0) correct += (pred == labels).sum().item() acc = 100 * correct / total print(f"Validation Accuracy: {acc:.2f}%")CrossEntropyLoss在 PyTorch 里已经包含了 softmax 运算,所以网络最后一层不需要额外加 softmax,直接输出 logits。optimizer.zero_grad()必须在每次反向传播前清空梯度,这是新手最容易漏的一步——漏掉的话梯度会累积,损失值会剧烈波动甚至不收敛。torch.max(outputs, 1)返回两个值,第一个是最大值,第二个是最大值对应的索引,取索引就是预测类别。
这个流程跑完,准确率通常落在 85% 到 95% 之间。如果低于 85%,优先检查数据增强是否过强,或者学习率是不是偏大;如果高于 95%,要警惕测试集和训练集是否存在同源图片,导致指标虚高。
5. 避坑手册:马铃薯叶片数据集复现中的五个高频问题
5.1 文件名带空格导致 glob 读图失败
现象:用glob.glob("train/*/*.jpg")读取图片时,部分文件读不进来,列表数量比实际文件少。
原因:文件名里有空格,比如RS_Early.B 6830.JPG,某些脚本用空格做分隔符拆分路径时会产生意外断点;另外在 Shell 命令中直接引用这类文件名也需要加引号。
解决:读图统一用os.listdir或pathlib.Path.iterdir,不要依赖 glob 的通配符解析。命令行动态传参时要给整个路径加双引号。我已习惯把所有文件先做一次重命名,把空格替换成下划线,一劳永逸。
for f in train/*/*.JPG; do mv "$f" "${f// /_}"; done5.2 JSON 类别顺序与目录顺序不一致
现象:训练时 loss 正常下降,验证准确率也有 90% 以上,但把训练好的模型拿去预测单张图片,输出结果和实际病害对不上。
原因:ImageFolder的类别顺序是按文件夹名字母序排的,如果 JSON 文件里的类别列表是人工写的,两者极可能不一致,模型输出向量的每一位含义和你的预期错位。
解决:训练脚本里直接打印train_dataset.classes,和 JSON 里的类别列表做一次断言比对。发现不一致时以train_dataset.classes为准,改 JSON 的读取逻辑,不要手改文件夹名。
5.3 show 脚本在中文路径下报错
现象:数据集放在包含中文或空格的路径下,运行python show.py直接抛UnicodeDecodeError或FileNotFoundError。
原因:OpenCV 的imread在 Windows 下不支持非 ASCII 路径,中文路径会返回 None,后面所有像素操作全部崩溃。
解决:把整个项目挪到纯英文路径下,路径中不要有中文、空格;或者用cv2.imdecode配合np.fromfile代替cv2.imread,后者支持中文路径。做深度学习实验我默认就用纯英文路径,这是早期被折磨出来的习惯。
5.4 测试集和训练集存在同源图片导致指标虚高
现象:验证准确率超过 97%,但把模型拿到田间拍摄的新照片上测试,效果明显差一截。
原因:同一批采集的照片被随机切分到了训练集和测试集,叶片背景、光照、拍摄角度高度相似,模型学到的可能不是病害特征,而是拍摄背景规律。
解决:检查同一编号前缀(如RS_Early.B后缀数字)是否同时出现在 train 和 test 目录里。如果这份资源存在这个问题,建议按文件名前缀手动重划测试集,保证测试集尽可能来自不同的拍摄批次。这也是我拿到任何分类数据集的第一个排查动作。
5.5 类别不平衡导致小类被无视
现象:训练结束后,早疫病识别准确率 95%,健康叶片只有 60%,模型几乎把健康叶片也判成早疫病。
原因:早疫病样本最多,模型收敛到「全猜早疫病」也能拿到较高整体准确率,小类被淹没。
解决:先看样本分布比例,再选择方案——样本量差距在 2 倍以内可以用WeightedRandomSampler做重采样,差距更大就要考虑数据增强给少数类加量。给CrossEntropyLoss传一个weight参数是更简单的做法,权重按类别样本数的倒数计算。最后一章会给出这套参数的具体配置。
6. 进阶:用数据增强和迁移学习微调,把这 2,100 张图压榨干净
6.1 增强策略按病害特征调整
基础的三件套(随机翻转、随机裁剪、颜色抖动)对这个数据集够用,但如果你想在博主主页里那套 CNN 改进方案上拿到更高精度,增强策略需要针对叶片病害特性微调。叶片图像对亮度变化不敏感,对色相偏移敏感——病斑颜色是区分早疫病和晚疫病的重要特征,过度调色相会把两类弄混。我在这个项目上用的参数是:
| 增强方式 | 参数配置 | 适用原因 |
|---|---|---|
| RandomResizedCrop | scale=(0.7, 1.0) | 让模型看到叶片局部病斑的细节 |
| RandomHorizontalFlip | p=0.5 | 叶片左右翻转不影响病害类别 |
| RandomVerticalFlip | p=0.3 | 适度增加空间多样性,但不宜过高 |
| ColorJitter | brightness=0.2, contrast=0.2, saturation=0.2, hue=0.05 | 亮度对比度可适当调,色相严格控制 |
| RandomRotation | degrees=15 | 田间拍摄角度有一定倾斜 |
| RandomAffine | translate=(0.1, 0.1) | 模拟叶片在画面中的位移 |
需要注意RandomVerticalFlip不要设成 0.5,因为叶片在自然状态下倒置的情况不多,过度翻转会让模型学到不真实的形态分布。hue=0.05是我反复试出来的上限值,超过 0.1 之后验证准确率会掉 2 到 3 个百分点。
6.2 微调策略:分阶段解冻比一步到位稳
第一阶段只训练全连接层,大约 10 个 epoch;第二阶段解冻最后两个 ResNet block(layer3 和 layer4),学习率降到 1e-4,再训 10 个 epoch;第三阶段如果还想追精度,可以解冻更多层,学习率降到 1e-5。分阶段微调比一次全部解冻稳定得多,因为最后一层是随机初始化的,如果一开始就和预训练层同步更新,梯度方向容易互相干扰。我的经验是每一阶段结束都保存一次 checkpoint,方便随时回退到上一个效果最好的版本——这算是训练实验里最重要的「后悔药」。
6.3 最终验证:不要只看准确率
三个类别的数量不均衡时,单看准确率会掩盖问题。建议打印混淆矩阵,重点看「晚疫病被判成早疫病」这类易混淆错误的比例。这两种病害的病斑形态接近,是这个任务的主要误差来源。我拿这套流程复现后,测试集上整体准确率能到 92% 左右,早疫病和晚疫病的混淆率控制在 5% 以内。有一次我把 ColorJitter 的 hue 参数调到了 0.15,准确率直接掉到 88%,从那以后我每次换增强参数都会固定随机种子,先跑 5 个 epoch 对比验证集 loss 再决定是否继续——宁可多花半小时做对比实验,也不想让一个错误参数毁掉一整轮训练。这份数据集的标签质量和目录规整度在同类资源里算不错的,只要你把上面几处坑避开,复现出可用模型并不难。希望帮到你。
本文还有配套的精品资源,点击获取