马铃薯叶片病害图像分类数据集:2100张标注样本的加载、划分与训练避坑指南 简介本资源为马铃薯叶片病害图像分类数据集面向从事农业图像识别、深度学习入门与CNN分类实践的学生及研究人员可用于早疫病、晚疫病与健康叶片的自动识别任务。数据集共约2100张已标注图像按三类划分训练集与测试集各类图片分目录存放并附有json标注文件与可视化show脚本方便快速查看样本分布与类别对应关系。压缩包内共2000个文件以1998张jpg图像为主另有1个Python脚本和1个json文件整体约38MB体积轻便易于本地加载与迁移。目前已有144人学习下载适合作为图像分类课程作业、毕业设计或网络改进实验的基准数据读者可据此完成数据加载、模型训练与效果对比并参考作者主页的CNN改进与目标检测项目进一步拓展。1. 马铃薯叶片病害图像分类数据集2100 张已标注样本怎么用起来拿到一个农业病害数据集第一反应往往不是「能不能跑通」而是「标注到底靠不靠谱、类别平不平衡、能不能直接喂给 CNN」。这份马铃薯叶片病害图像分类数据集约 2100 张 JPG 图片分早疫病、晚疫病、健康叶片三类训练集和测试集已经切好每类图片放在各自目录下还附带一个可视化脚本。文件名里能看到RS_Early.B和RS_HL这样的标记说明标注信息直接编码在文件名中而不是靠单独的 XML 或 COCO JSON 去索引。对做图像分类算法验证、小样本学习 baseline、农业视觉落地预研的人来说这种「开箱即用」的结构省掉了最耗时的清洗环节。它适合谁适合想快速验证 ResNet、ViT 或自研分类头的人也适合拿它做数据增强、类别不平衡处理的练手项目。但别急着torchvision.datasets.ImageFolder一把梭先搞清楚它的目录约定和标注逻辑后面能少踩很多坑。2. 数据集结构与标注逻辑从文件名到 ImageFolder 的映射2.1 目录划分与类别编码这份资源的核心结构并不复杂但有几个细节决定了你能不能直接套用标准加载器。训练集和测试集是分开的各自内部按类别建子目录。常见做法是dataset/ ├── train/ │ ├── Early_Blight/ │ ├── Healthy/ │ └── Late_Blight/ └── test/ ├── Early_Blight/ ├── Healthy/ └── Late_Blight/但原始文件名里出现的是RS_Early.B和RS_HL这说明作者在整理时可能用了缩写。Early.B对应早疫病Early BlightHL对应健康叶片Healthy Leaf晚疫病大概率是Late.B或类似标记。如果你拿到的压缩包解压后目录名不是标准英文而是这些缩写就需要先做一次重命名映射。ImageFolder依赖目录名作为类别标签目录名混乱会直接导致标签错位。我一般会先跑一段脚本统计每个子目录的文件数和文件名模式确认没有混类。下面这段代码就是干这个的import os from collections import Counter root ./dataset for split in [train, test]: split_path os.path.join(root, split) if not os.path.exists(split_path): print(f[跳过] {split_path} 不存在) continue for cls in sorted(os.listdir(split_path)): cls_path os.path.join(split_path, cls) if not os.path.isdir(cls_path): continue files [f for f in os.listdir(cls_path) if f.lower().endswith((.jpg, .jpeg, .png))] # 提取文件名中的标记段观察命名规律 marks Counter() for f in files: parts f.replace(.jpg, ).replace(.JPG, ).split(___) if len(parts) 1: marks[parts[-1].split()[0]] 1 print(f{split}/{cls}: {len(files)} 张, 标记分布: {marks.most_common(3)})这段代码的逻辑是遍历 train 和 test 下的每个类别目录统计图片数量同时把文件名按___分割取最后一段的第一个词作为标记。这样你能快速看出RS_Early.B、RS_HL这些标记是否和目录名一致。如果某个目录下混入了其他标记的文件说明标注有交叉需要手动清理。参数上root指向解压后的根目录split列表按实际划分调整。跑完这一步你对数据集的「干净程度」就有底了。2.2 标注文件与 JSON 的读取摘要里提到「具体查看 json 文件」这意味着除了目录结构可能还有一个 JSON 文件记录了类别映射或划分索引。常见做法是classes.json或dataset_info.json里面存{0: Early_Blight, 1: Healthy, 2: Late_Blight}这样的字典。如果你直接按目录名训练类别顺序可能和 JSON 不一致导致推理时标签对不上。我一般会先读 JSON用它的映射关系去校验目录名import json with open(./dataset/classes.json, r, encodingutf-8) as f: class_map json.load(f) print(JSON 类别映射:, class_map) # 反向映射目录名 - 索引 name_to_idx {v: int(k) for k, v in class_map.items()} print(目录名到索引:, name_to_idx)如果 JSON 里的键是字符串数字记得转 int。有些数据集会把训练集和测试集的划分也写进 JSON比如{train: [...], test: [...]}这时候就不能单纯靠目录了得按 JSON 里的文件列表来加载。判断方法很简单看 JSON 里有没有文件路径列表。有就以 JSON 为准没有就以目录为准。两者冲突时优先信 JSON因为它是作者显式声明的标注。2.3 可视化脚本 show 的使用与改造资源里带了一个 show 脚本大概率是用来预览图片和标签的。常见实现是随机抽样几张图用 matplotlib 显示标题写类别名。直接运行之前先确认它的路径参数是否指向你的解压目录。我见过不少 show 脚本硬编码了作者本机的绝对路径跑起来直接报FileNotFoundError。改造思路是把它改成接受命令行参数import argparse import random import matplotlib.pyplot as plt from PIL import Image import os parser argparse.ArgumentParser() parser.add_argument(--root, default./dataset/train, help训练集根目录) parser.add_argument(--n, typeint, default6, help每类显示张数) args parser.parse_args() classes [d for d in os.listdir(args.root) if os.path.isdir(os.path.join(args.root, d))] fig, axes plt.subplots(len(classes), args.n, figsize(args.n * 2, len(classes) * 2)) for i, cls in enumerate(sorted(classes)): cls_path os.path.join(args.root, cls) files [f for f in os.listdir(cls_path) if f.lower().endswith((.jpg, .png))] samples random.sample(files, min(args.n, len(files))) for j, fname in enumerate(samples): img Image.open(os.path.join(cls_path, fname)) ax axes[i][j] if len(classes) 1 else axes[j] ax.imshow(img) ax.set_title(cls, fontsize8) ax.axis(off) plt.tight_layout() plt.show()这段代码按类别逐行展示每类随机抽n张。--root参数让你不用改代码就能切换 train 或 test。跑一遍可视化你能直观看到早疫病的同心轮纹、晚疫病的水渍状边缘、健康叶片的均匀绿色这对后面判断模型是否学到了正确特征很关键。如果发现某类图片明显偏暗或偏亮说明拍摄条件不一致可能需要做直方图均衡化。3. 训练集与测试集划分怎么切才不泄漏、怎么加载才不翻车3.1 划分比例与类别平衡检查摘要说「划分了训练集、测试集」但没给具体比例。常见做法是 8:2 或 7:3。你需要自己统计一下每类的训练和测试数量看是否接近这个比例。如果某类测试集只有个位数评估结果波动会很大。下面这段代码输出每类的训练/测试数量和比例import os root ./dataset for cls in sorted(os.listdir(os.path.join(root, train))): train_dir os.path.join(root, train, cls) test_dir os.path.join(root, test, cls) if not os.path.isdir(train_dir): continue n_train len([f for f in os.listdir(train_dir) if f.lower().endswith((.jpg, .png))]) n_test len([f for f in os.listdir(test_dir) if f.lower().endswith((.jpg, .png))]) if os.path.isdir(test_dir) else 0 total n_train n_test ratio n_train / total if total 0 else 0 print(f{cls}: 训练 {n_train}, 测试 {n_test}, 训练占比 {ratio:.2%})如果发现某类训练占比明显偏离整体比例比如整体 80% 但某类只有 60%说明划分时没有做分层抽样。这时候你有两个选择要么接受现状在训练时用WeightedRandomSampler做类别加权要么重新按类别分层切分。我一般会先看测试集是否够用如果测试集每类都超过 50 张就不折腾了直接加权训练。3.2 用 ImageFolder 加载与 transform 参数确认目录结构没问题后加载就简单了。torchvision.datasets.ImageFolder会自动按目录名排序生成类别索引所以你要确保目录名排序后和你的预期一致。比如Early_Blight、Healthy、Late_Blight按字母序是 Early、Healthy、Late正好对应 0、1、2。如果目录名是中文或缩写排序可能乱建议先重命名。from torchvision import datasets, transforms from torch.utils.data import DataLoader train_tf transforms.Compose([ transforms.Resize((224, 224)), # 统一尺寸ViT 和 ResNet 常用 transforms.RandomHorizontalFlip(p0.5), # 叶片左右翻转不改变类别 transforms.RandomRotation(15), # 小角度旋转模拟拍摄角度变化 transforms.ColorJitter(brightness0.2, contrast0.2), # 光照鲁棒性 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_tf 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_ds datasets.ImageFolder(./dataset/train, transformtrain_tf) test_ds datasets.ImageFolder(./dataset/test, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4) test_loader DataLoader(test_ds, batch_size32, shuffleFalse, num_workers4) print(类别索引:, train_ds.class_to_idx) print(训练集大小:, len(train_ds), 测试集大小:, len(test_ds))参数说明Resize((224, 224))是 ImageNet 预训练模型的标配输入如果你用 ViT通常也是 224。RandomHorizontalFlip对叶片病害是安全的因为病斑位置左右翻转后语义不变。RandomRotation(15)别开太大超过 30 度可能把叶片转出画面。ColorJitter的 brightness 和 contrast 控制在 0.2 以内太大可能把病斑颜色改得不像病。Normalize的均值和方差用 ImageNet 的即可除非你有大量数据重新统计。num_workers在 Windows 上如果报错改成 0。3.3 小样本场景下的采样策略热搜里有人关心 1-shot、5-shot 小样本分类。这份数据集每类约 700 张不算小样本但你可以主动构造小样本实验从每类训练集中只抽 1 张或 5 张测试集不变。常见做法是用Subset和自定义采样器import random from torch.utils.data import Subset def build_few_shot(dataset, n_shot5): targets [y for _, y in dataset.samples] indices [] for cls in sorted(set(targets)): cls_indices [i for i, t in enumerate(targets) if t cls] indices.extend(random.sample(cls_indices, min(n_shot, len(cls_indices)))) return Subset(dataset, indices) few_shot_ds build_few_shot(train_ds, n_shot5) few_shot_loader DataLoader(few_shot_ds, batch_size4, shuffleTrue) print(5-shot 训练集大小:, len(few_shot_ds))这段代码按类别各抽n_shot张构造一个子集。注意dataset.samples是ImageFolder的属性返回(path, label)列表。用Subset包装后DataLoader依然能正常工作。做 1-shot 时把n_shot改成 1 即可。这种构造方式适合快速验证原型网络或 MAML 类算法但别指望在 5 张图上训出高精度它的意义是评估模型的快速适应能力。4. 避坑与排查标注、路径、显存这三处最容易翻车4.1 文件名标记与目录类别不一致现象训练时准确率卡在 33% 左右跟随机猜差不多。原因部分图片的文件名标记是RS_HL但被放进了Early_Blight目录标签错了。解决跑 2.1 节的统计脚本把标记和目录名做交叉表发现不一致的手动移走或重新标注。如果数量大写脚本按文件名标记自动归位。4.2 训练集和测试集出现同一张图现象测试集准确率异常高达到 99%但换一批新图就崩。原因划分时按文件名随机切同一张原图的不同增强版本或连拍图被分到了训练和测试两边。解决用感知哈希或简单的文件 MD5 去重确保同一来源的图片只出现在一个 split 里。常见做法是先用imghash库算哈希再按哈希分组切分。4.3 显存不足与 batch size 的玄学现象batch size 设 32 直接 OOM设 16 又报CUDA out of memory。原因图片分辨率没统一部分原图是 4000x3000Resize之前就占满了显存。解决在DataLoader里加transforms.Resize之前先做一次transforms.Resize(256)的短边缩放或者用ImageFolder的loader参数自定义 PIL 读取时先缩略。另外num_workers调大也会增加内存占用不是越大越好。4.4 JSON 类别顺序与 ImageFolder 不一致现象推理时模型输出索引 0你以为是早疫病结果 JSON 里 0 是健康。原因ImageFolder按目录名字母序生成索引JSON 按作者手写顺序。解决训练前打印train_ds.class_to_idx和 JSON 的映射逐项对比。不一致就以class_to_idx为准重新生成推理时的标签映射表。4.5 show 脚本路径硬编码导致找不到图片现象运行 show 脚本报FileNotFoundError路径是D:\xxx\dataset。原因作者本机路径没改。解决按 2.3 节改成argparse接收根目录或者直接把脚本里的路径替换成你的解压路径。如果脚本用了os.getcwd()确保你在正确的目录下运行。5. 从 ResNet 到 ViT分类头调整与验证技巧5.1 用预训练 ResNet50 跑一个 baseline先别急着上 ViT用 ResNet50 跑通流程确认数据加载和标签没问题。下面这段代码冻结骨干只训分类头适合快速验证import torch import torch.nn as nn from torchvision import models device torch.device(cuda if torch.cuda.is_available() else cpu) model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) for param in model.parameters(): param.requires_grad False model.fc nn.Linear(model.fc.in_features, 3) # 三类 model model.to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.fc.parameters(), lr1e-3) for epoch in range(10): model.train() for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() loss criterion(model(imgs), labels) loss.backward() optimizer.step() print(fEpoch {epoch1} done)参数说明weightsIMAGENET1K_V2是 torchvision 新版接口旧版用pretrainedTrue。model.fc.in_features是 2048换成 3 类。冻结骨干后只训fc10 个 epoch 通常能到 90% 以上。如果显存够解冻最后两个 stage 微调效果更好。5.2 ViT 分类头要不要调整热搜里有人问「用 ViT 评估时分类头用调整吗」。答案是如果你用torchvision.models.vit_b_16(weights...)它的heads默认是 1000 类必须换成 3 类。ViT 的分类头是一个Linear层输入维度 768。调整方式和 ResNet 类似from torchvision.models import vit_b_16, ViT_B_16_Weights vit vit_b_16(weightsViT_B_16_Weights.IMAGENET1K_V1) vit.heads.head nn.Linear(vit.heads.head.in_features, 3) vit vit.to(device)注意 ViT 对数据量敏感2100 张图直接从头训容易过拟合。常见做法是冻结 patch embedding 和前几个 transformer block只训最后几层和分类头。另外 ViT 的输入必须是 224x224Resize不能少。5.3 验证时看混淆矩阵而不是只看准确率准确率会被类别不平衡掩盖。跑完测试集后用sklearn输出混淆矩阵from sklearn.metrics import confusion_matrix, classification_report import numpy as np model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in test_loader: imgs imgs.to(device) preds model(imgs).argmax(dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) print(confusion_matrix(all_labels, all_preds)) print(classification_report(all_labels, all_preds, target_nameslist(train_ds.class_to_idx.keys())))如果早疫病和晚疫病互相误判多说明模型没学到区分性特征可能需要更强的数据增强或换更高分辨率输入。健康叶片如果被误判为病害检查是否有背景干扰。5.4 一个习惯先过一遍 show 脚本再开训从那以后我每次拿到新数据集都强制先跑一遍可视化脚本随机看 20 张图确认标注和图像内容对得上。这一步花不了五分钟但能避免后面几小时的无效训练。希望帮到你。本文还有配套的精品资源点击获取