
简介这份资源是一套面向图像分类入门及农业视觉应用学习者的马铃薯叶片病害分类数据集适合用于训练 CNN 分类模型或验证改进网络效果。数据集中包含约 2,100 张已标注图片覆盖早疫病、晚疫病和健康叶子三个类别且已划分好训练集与测试集并附带 JSON 标注文件便于直接开展模型训练与评估。压缩包共 2,000 个文件主要由 1,998 张 JPG 图像、1 个 Python 显示脚本和 1 个 JSON 配置文件组成整体大小约 38MB结构简洁便于下载后快速上手。目前已有 144 人学习下载。除数据本身外资源内还提供了可视化脚本运行后即可直观查看各类别样本分布结合作者主页中关于 CNN 分类网络改进的系列文章使用者可基于该数据集开展网络结构调整、精度对比实验是一份兼具数据支撑与扩展学习路径的实用资源。1. 2100张标注图能撑起一个可用的马铃薯病害分类器吗先说反直觉的结论2100张标注图不算多靠它从零训练一个深度卷积网络必翻车但借助ImageNet预训练权重做迁移学习配合一套针对叶片影像设计的数据增强足以在早疫病、晚疫病和健康叶片的分类任务上跑到95%以上的验证准确率。这里说的“可用”是指能进到真实田间部署验证的那种而不是只在测试集上自嗨。这个标题指向的是“已标注”的马铃薯叶片病害图像分类数据集约2100张。它解决的是很多农业视觉项目最痛苦的第一公里数据从哪来、标签怎么定义、怎么统一目录格式。你要是做过PlantDoc、PlantVillage这类公开数据集就知道下载下来的图分辨率参差不齐、标注噪声不少而一个按ImageFolder组织好的小数据集反而能让baseline在三小时内跑出来。这篇笔记把数据集结构拆开、搭好训练流程、把坑列出来适合第一次做农业图像分类的工程师也适合想用CVAT或LabelImg自建数据集的团队作对照。2. 先拆数据集病害类别、目录结构与首个加载脚本2.1 一张图一个标签分类数据集长什么样图像分类任务里数据集最通用的组织方式就是ImageFolder根目录下面每个文件夹一个类别文件夹里的每张图都属于这个类。拿到数据集先执行一条命令tree -L 2 -d .如果返回结果是这样的data/ ├── train/ │ ├── Early_blight/ │ └── Late_blight/ │ └── healthy/ ├── val/ └── test/说明数据已经按训练集/验证集/测试集拆分好了直接用torchvision就能加载。如果只有一个总目录加一个CSV标注文件那需要先重排成ImageFolder这步十分钟就能搞定。这里有个容易忽视的细节验证集和测试集的区别不要搞混。验证集是训练过程中用来调学习率、挑epoch的模型会间接“见过”它测试集只许碰一次。这个小数据集既然标了约2100张那么常见拆分比例是train占70%80%val和test各占15%左右。实际做的时候别急着合并重拆保持发布者的划分结果才能和别人对比。2.2 用torchvision在10行内跑通数据加载不管后续用什么模型第一步都是把图从硬盘里读出来转成能进网络的张量。torchvision的datasets.ImageFolder加上transforms就能做到from torchvision import datasets, transforms from torch.utils.data import DataLoader # 训练集与验证集先用同一套基础变换后面再优化 train_transform transforms.Compose([ transforms.Resize((256, 256)), # 先把短边拉到一个固定尺寸 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_data datasets.ImageFolder(data/train, transformtrain_transform) val_data datasets.ImageFolder(data/val, transformtrain_transform) train_loader DataLoader(train_data, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_data, batch_size32, shuffleFalse, num_workers4) print(train_data.classes) print(len(train_data), len(val_data))这段代码的逻辑有三层第一ImageFolder会扫描子目录名生成类别索引classes按字母序排第二Resize把输入统一到256乘256为的是后续模型输入尺寸一致避免到前向传播时才报维度错误第三Normalize用的均值方差是ImageNet的标准值因为后面要加载ImageNet预训练权重。参数上batch_size32在8GB显存上跑ResNet18勉强够用如果你显卡只有4GB就降到16。num_workers在Windows上容易报错设成0最稳Linux下4到8都可以。注意这里的Resize只是基线后面会换掉。2.3 标注质量初检统计类别分布和分辨率分布从外部拿到的数据集第一件事不是训练而是用代码做个体检。常见问题有两类类别不平衡以及图片分辨率离散程度过高。一个脚本就能看出来from PIL import Image from collections import Counter import os root data/train exts (.jpg, .jpeg, .png) size_counter Counter() class_counter Counter() for label in os.listdir(root): class_dir os.path.join(root, label) if not os.path.isdir(class_dir): continue for name in os.listdir(class_dir): if not name.lower().endswith(exts): continue img Image.open(os.path.join(class_dir, name)) size_counter[(img.width, img.height)] 1 class_counter[label] 1 print(class_counter) print(size_counter.most_common(10))这个脚本跑完后重点看两个指标。第一个是每个类别的样本数有没有超过1倍的差距比如健康叶600张、早疫病200张这就会让模型偏向样本多的类别后面得用加权采样或WeightedLoss处理。第二个是分辨率分布如果很多图是几百像素起步个别图超过2000像素你得在训练时用RandomResizedCrop统一裁剪而不是粗暴Resize否则模型会过度依赖叶片占据画面的比例。标注层面也要扫一眼把目录名当成标签是一种默认约定建议抽查几十张图确认没有错放。曾有同事拿到一批标注数据发现某个文件夹里混入了不少背景图模型收敛之后对“地面”特别敏感这样的标注噪声后面再清理成本就高了。3. 训练一个能用的baselineEfficientNet微调与关键参数3.1 为什么不自己设计卷积网络2100张图自己搭一个VGG或ResNet的变体从零初始化训练几乎必然陷入过拟合训练准确率冲到98%验证准确率卡在75%。这不是代码问题是小数据大参数空间的统计必然。农业病害图像分类的主流做法是加载ImageNet预训练权重做微调而不是从零训练。EfficientNet系列是个稳妥起点。它用神经架构搜索定了一组复合缩放系数在同样FLOPs下比ResNet精度更高。V2还修复了训练早期显存占用过大的问题。对这个尺寸的数据集我一般用EfficientNet-B0或B1既能吃下256输入8GB显存也跑得动。如果你手里只有CPU或者老显卡ResNet18也可以但最终精度大约低两个点。3.2 完整训练脚本从冻结到解冻把整个训练流程拆成两个阶段先冻结backbone只训练分类头让新加的全连接层先适应这个小数据集的类别分布再解冻backbone用更低的学习率微调。这比一上来就全量微调稳定得多能避免初始loss过大导致训练震荡。import torch import torch.nn as nn from torch.optim import AdamW from torchvision import models model models.efficientnet_b0(weightsmodels.EfficientNet_B0_Weights.DEFAULT) num_classes 3 model.classifier[1] nn.Linear(model.classifier[1].in_features, num_classes) # 冻结 backbone只训练 classifier for param in model.features.parameters(): param.requires_grad False for param in model.classifier.parameters(): param.requires_grad True optimizer AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3) criterion nn.CrossEntropyLoss() # 阶段一只训分类头 5 epoch for epoch in range(5): for images, labels in train_loader: outputs model(images) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() # 解冻 backbone低学习率微调 for param in model.parameters(): param.requires_grad True optimizer AdamW(model.parameters(), lr1e-4, weight_decay1e-4) # 阶段二全量微调 15 epoch这个循环体实际跑项目和下面等价 for epoch in range(15): model.train() for images, labels in train_loader: images, labels images.to(device), labels.to(device) loss criterion(model(images), labels) optimizer.zero_grad() loss.backward() optimizer.step()这里参数要解释清楚。阶段一用1e-3的学习率因为新分类头的随机初始化需要较大步长去拟合类别中心。解冻后的模型必须降到1e-4而且我习惯加上weight_decay1e-4防止全量微调时backbone权重偏离预训练太远。AdamW替代原始的Adam它对weight decay的处理更规范能有效缓解过拟合。注意两个坑其一model.classifier[1]是EfficientNet的最后一层不同版本的torchvision写法可能不同改成model.classifier[-1]更保险其二冻结阶段一定要确认哪些参数的requires_grad是False否则AdamW会把所有参数的梯度都算一遍冻结形同虚设。3.3 超参数怎么调先定尺寸、批次、学习率超参数是新手最容易“玄学化”的东西。实际上有一个稳定的调法先固定输入尺寸再选batch最后用余弦退火去调学习率峰值。输入尺寸方面马铃薯叶片病害纹理比较细微晚疫病病斑边缘不规则太小容易丢失纹理太大显存扛不住。256是一个甜点值224略糊384能涨点精度但训练时间约多一倍。批次大小直接影响BatchNorm的统计量太小的batch在微调阶段会让模型抖动建议显存允许下尽量32起步。学习率是个坑。迁移学习场景下直接用ImageNet image size训练好的权重学习率过大会把预训练特征破坏掉。我见过太多人带着1e-2的初始lr冲进去训练几轮loss降到零但验证集乱跳这就是典型的学习率翻车。python train.py \ --model efficientnet_b0 \ --image_size 256 \ --batch_size 32 \ --lr_phase1 1e-3 \ --lr_phase2 1e-4 \ --epochs_phase1 5 \ --epochs_phase2 15建议把上述配置写成一个shell脚本方便复现不同实验。一个可用的打印方式是在每轮结束输出train loss、val loss和当前最佳准确率然后按val loss选模型别只看train loss。约2100张图在单张RTX 3060上256输入跑20个epoch大约需要20分钟速度足够你多尝试几组学习率。4. 把2100张“变”成更多数据增强与类别不均衡处理4.1 针对叶片病害的增强组合数据增强是这个小规模数据集能work的核心环节。通用做法是随机翻转加颜色扰动但叶片病害分类有个特殊性病斑的颜色和纹理是关键判据强度过高的ColorJitter会把病斑的色调漂没反而伤害精度。我常用的组合是RandomResizedCrop模拟不同拍摄距离HorizontalFlip加VerticalFlip模拟叶片朝向Rotate(30)模拟倾斜摆放ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.05)做轻微颜色变化。其中hue一定要设小马铃薯晚疫病的病斑呈褐色色相偏移太大会让模型学到错误相关性。from torchvision import transforms train_transform transforms.Compose([ transforms.RandomResizedCrop(size(256, 256), scale(0.7, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomVerticalFlip(p0.5), transforms.RandomRotation(degrees30), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.05), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])RandomResizedCrop里的scale(0.7, 1.0)意思是裁剪面积是原图的70%到100%这模拟了镜头拉近拉远的效果。注意不要用Resize替代它Resize会把整张图塞进固定尺寸让模型对叶片占比过度敏感。验证集保持Resize(256)加CenterCrop(256)不要做随机增强这样每次验证结果才可比。4.2 用Albumentations封装训练流程torchvision的变换在实际项目里够用但对叶片病害想要更强效果Albumentations提供了更丰富的变换比如RandomBrightnessContrast和GaussNoise而且因为底层是OpenCV速度比PIL快一两倍。习惯做法是用albumentations定义好变换后包一层成torch的Dataset接口。import albumentations as A from albumentations.pytorch import ToTensorV2 train_aug A.Compose([ A.RandomResizedCrop(height256, width256, scale(0.7, 1.0)), A.HorizontalFlip(p0.5), A.VerticalFlip(p0.5), A.Rotate(limit30), A.RandomBrightnessContrast(brightness_limit0.2, contrast_limit0.2, p0.5), A.GaussNoise(var_limit(10.0, 50.0), p0.2), A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ToTensorV2(), ]) class LeafDataset: def __init__(self, paths, labels, transformNone): self.paths paths self.labels labels self.transform transform def __len__(self): return len(self.paths) def __getitem__(self, idx): img A.load(self.paths[idx]) # 读取为 BGR ndarray if self.transform is not None: transformed self.transform(imageimg) img transformed[image] return img, self.labels[idx]这里A.load是albumentations自带的图像读取函数返回的是BGR ndarray内部变换会自动处理。GaussNoise加噪可以提升模型对田间噪点的鲁棒性但p0.2是合理的上限太高会让纹理细节失真。要注意ImageFolder虽然省事但生成代码用自定义Dataset是更通用的方案这样后面想换成CSV标注、或从数据库读路径都不用改训练主循环。4.3 类别不均衡先从loss层面处理如果一个类别明显少于其他两类不要让模型自己去硬扛。三种办法由轻到重weight_decay加大到1e-3、给损失函数加类别权重、以及用WeightedRandomSampler重采样。class_weights torch.tensor([1.0, 2.0, 1.0]) # 按类别样本比的倒数估算 criterion nn.CrossEntropyLoss(weightclass_weights)简单场景下构建好class_weights传给CrossEntropyLoss就够。它的含义是少样本类别的loss乘以对应权重让梯度更大迫使模型更关注这些类。如果这样训练后少样本类别的召回率还不行再上WeightedRandomSampler。注意重采样会让同一个epoch内重复看到少样本但同时也加剧了对重复样本的过拟合所以SAMPLER配合一点DropOut效果更好。5. 避坑指南验证准确率虚高和真实场景失效的4个常见问题5.1 验证集98%下地实测掉到60%现象本地验证集效果好得惊人Field测试却一塌糊涂。原因最大嫌疑人有两个。第一是数据泄漏如果训练集和验证集来自同一批采集、同一光线下网络其实记住了光线环境而不是病害纹理。第二是拍摄分布不一致数据集里都是正面俯拍的完整叶片你下地拍的时候镜头里的叶片是倾斜的、带露水的、有遮挡的分布一变模型就发懵。解决拿到这个标注数据集后先按采集批次或图像来源做分组划分验证集不要按文件名随机划分。这一点在公开数据集里尤其普遍——发布者往往用随机拆分几千张图来自几十个园区同一个园区的图可能同时出现在训练和验证里。5.2 模型学到了叶片以外的背景现象错误分析时发现模型把某些环境背景和类别关联起来了。比如在水泥地上拍的叶片都判成早疫病。原因标注的人不会特别注意拍摄背景而背景在视觉上比病斑特征大得多CNN天然会捡软柿子捏。解决数据增强里加RandomResizedCrop比例增大强制模型看局部纹理更有效的是做一次背景替换增强——把叶片抠出来随机贴在纯色背景上。虽然多了预处理工作量但能让特征注意力离开背景。诊断方法也简单把一张正常叶片图裁成左下角和右下角两块分别预测看预测结果是否一致不一致就说明模型注意力飘了。5.3 训练到第20个epoch验证loss回升但准确率还在涨现象验证集准确率一直在涨Training loss也在降但验证loss反弹了。原因这是选择模型的经典陷阱。准确率是离散指标最后几个epoch只要少判错一张图就跳动一个点它掩盖了概率输出的退化——模型开始对正确答案过度自信对错误答案也过度自信这会让概率校准变差后续做置信度阈值或主动学习时很难用。解决盯val loss而不是val accuracy来存模型。代码里加一行if val_loss best_loss: best_loss val_loss torch.save(model.state_dict(), best_leaf_model.pt)同时配合ReduceLROnPlateauval loss连续三个epoch不降就把学习率衰减到原来的0.1这样做十二个epoch就稳得住了。别省这个回调它是防“过拟合反弹”的后悔药。5.4 batch太小BatchNorm开始抽风现象显存不够把batch压到8结果训练loss剧烈震荡收敛极慢。原因BatchNorm在小batch下统计量噪声太大而EfficientNet的BatchNorm层又特别多。模型在训练和验证时用的是两套统计量训练batch太小会让验证时统计量漂移。解决不要用BatchNorm系列的模型换成没有全局统计依赖的变体。一个立竿见影的做法是把efficientnet_b0换成efficientnet_b0的GN变体或者干脆用ResNet18加GroupNorm但改动不小。更省事的是用混合精度训练在不动batch的前提下节省显存scaler torch.cuda.amp.GradScaler() with torch.autocast(device_typecuda, dtypetorch.float16): outputs model(images) loss criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()想省事的话最简单的办法是按原图resize到256后缓存成tensor存内存里batch8时也可以硬跑只是吃内存。这个在小数据集上特别有效2100张图也就几百MB。6. 最后一章训练结束后做一次可信度自检并导出部署模型训练完成不等于结束。在真正去写技术报告或部署之前我习惯按下面三步做一次完整自检确保模型不是黑匣子别人接手也敢用。第一步五折交叉验证。把train和val合并按类别分层拆五折每折训20个epoch记录每折在验证集上的准确率和召回率。平均后如果标准差大于两个百分点说明模型对数据划分敏感要么是某个类别样本太少要么是图像采集分布不均匀。这时回去检查标注质量比继续调参数划算得多。第二步看混淆矩阵和每类别的UARunweighted average recall。对于马铃薯叶片病害这个任务漏判晚疫病的代价远高于把早疫病误判成健康叶片所以绝不能只看总准确率。如果晚疫病召回率低于90%优先做类内数据收集或按5.3的方式重调类别权重。第三步导出ONNX并做一次推理单测。很多部署环境跑不了PyTorch转ONNX是通用做法model.eval() dummy torch.randn(1, 3, 256, 256).to(device) torch.onnx.export(model, dummy, leaf_model.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}})导出时注意dynamic_axes打开batch维度这样边缘设备上可以一次跑多张。然后用onnxruntime加载并和PyTorch输出做逐元素对比误差在1e-4以内才罢休。作为一线做农业视觉的人我最深的感受是这类小规模已标注数据集的价值不在“开箱即用”而在于它帮你把数据管线、训练基线和评估流程一次性跑顺。踩过验证集泄漏的坑之后我每个项目都强制要求按来源分组划分数据踩过背景学习的坑之后增强配置里永远加局部裁剪。这个数据集约2100张你按本文流程走一遍收获的不仅是一个模型而是整套可复用到下一个病害分类任务的方法希望帮到你。本文还有配套的精品资源点击获取