简介:一套面向图像分类任务的中文标注数据集,专门收集马铃薯叶片病害图像,涵盖早疫病、晚疫病及健康叶片三个类别,并已按训练集与测试集划分,同类图片统一存放在独立文件夹中,便于深度学习项目直接使用。压缩包共2000个文件,其中1998张JPG图片、1个Python可视化脚本和1个JSON分类标识文件,整体大小38MB。可视化脚本可随机展示样本与对应标签,JSON文件详细记录类别名称及映射关系,方便建模前的数据检查与类别对照。图片文件名自带早期、健康等关键词,可快速筛选。对于从事农业病害识别、计算机视觉分类研究的开发者或学生,这套已标注的基础数据能直接用于CNN等模型的训练与效果对比,省去自行采集和标注的时间。资源目前已有143人学习,适合作为算法验证、课题实验及模型迭代的支撑数据。
1. 马铃薯叶片病害图像分类数据集:2100张已标注图能做什么
做农业图像分类的人常卡在一个尴尬位置:公开数据集要么是ImageNet那种泛化巨兽,要么是几百张自己拍的原始照片,还得自己标。这个马铃薯叶片病害图像分类数据集,约2100张、已标注,放在图像分类任务里属于偏小但能用的规模。比CIFAR-10的每类6000张少一个量级,但比自建数据集的几百张多出可操作空间。它的价值不是提供多少新知识,而是把"从零标注到能训练"这段最磨人的链路砍掉了,让你直接进入迁移学习微调阶段。适合做农业病害识别验证、深度学习课程设计、或者想跑通一整套图像分类训练流程的开发者。2100张图容错率不高,但方向对了,能改出一套可用的基线模型。
2. 读懂这份数据集:类别、标注与文件组织
拿到数据集先别急着训练,先做三件事:看类别名、看标注字段、看划分逻辑。图像分类数据集和检测数据集不一样,标注不是画框,而是给每张图贴一个类别标签。这个数据集的交付形态通常有两种,一种是把不同病害的叶片图直接按文件夹归类,另一种是附带一个CSV或JSON文件记录每一张图片的路径与标签。这两种都叫"已标注",但处理代码完全不同。
2.1 类别设计:病害种类与健康叶片的平衡
马铃薯叶片常见病害包括早疫病、晚疫病、环腐病、病毒病等,健康叶片一般单独成一个类别。以约2100张的总量来看,常见设计是5到6个类别,每类大约350到420张。这个分布是"能用"的下限——太少类别不平衡会非常难训,太多每类的样本又会稀释到不足以让模型学出判别特征。
先列出类别清单。在PyTorch里最直观的方式是直接用文件夹名当标签:
from torchvision import datasets, transforms # 按文件夹名自动生成标签,子文件夹名就是类别名 dataset = datasets.ImageFolder( root="potato_leaf_dataset/train", transform=transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), ]) ) print(dataset.classes) # ['Early_Blight', 'Late_Blight', 'Healthy', ...] print(dataset.class_to_idx) # {'Early_Blight': 0, 'Late_Blight': 1, ...}ImageFolder会扫描根目录下的所有子文件夹,把每个子文件夹当作一个类别。这里的关键是你拿到数据后先打印classes看一眼顺序,后面所有评估、导出、可视化都要对着这个索引来,顺序错位会直接导致报告里的标签全反了。
2.2 标注格式:文件夹分类与CSV/JSON标签的常见写法
如果数据以CSV交付,常见格式是两列:filename,label,文件名指向实际图片路径,label是类别字符串。你自己写加载器时要处理路径拼接和字符编码问题,特别是Windows环境下的中文字符串路径。这里建议把路径统一转成英文,后面能省掉大量解码报错。
import pandas as pd from PIL import Image df = pd.read_csv("potato_leaf_dataset/labels.csv") # 假设列名为 file_path, label file_paths = df["file_path"].tolist() labels = df["label"].tolist() class_to_idx = {cls: i for i, cls in enumerate(sorted(set(labels)))} y = [class_to_idx[label] for label in labels] # 读取与校验:破损图片提前过滤,否则训练到一半崩溃 valid_images = [] valid_labels = [] for path, label in zip(file_paths, y): try: img = Image.open(path) img.verify() valid_images.append(path) valid_labels.append(label) except Exception: print(f"损坏图片已跳过: {path}")img.verify()不加载完整图像数据,只校验文件头,速度远快于open().load()。训练前跑一遍,提前把损坏图踢出去,能避免中途读取报错导致数小时训练白跑。
2.3 划分训练/验证/测试集:为什么不能随机打乱一次
约2100张的规模决定了划分策略直接影响最终分数。常见做法是训练70%、验证15%、测试15%。但有一个细节坑:同一株马铃薯可能拍了多张叶片图,这些图在视觉上非常相似,如果随机打乱,同源的图会同时进训练集和验证集,模型在验证集上表现虚高,这就是数据泄露。
import random from collections import defaultdict # 按类别分层采样,避免某一类在图里全挤进训练集或验证集 random.seed(42) train_ratio, val_ratio = 0.7, 0.15 train_files, val_files, test_files = [], [], [] for cls in sorted(set(df["label"])): cls_paths = df[df["label"] == cls]["file_path"].tolist() random.shuffle(cls_paths) n_train = int(len(cls_paths) * train_ratio) n_val = int(len(cls_paths) * val_ratio) train_files.extend(cls_paths[:n_train]) val_files.extend(cls_paths[n_train:n_train + n_val]) test_files.extend(cls_paths[n_train + n_val:])分层采样后,每个类别在三个集合里的比例和全集一致,不会出现验证集里某类只有十几张的情况。固定随机种子保证复现性。如果你拿到的数据集已经自带了划分文件,我用下来建议直接信它的划分,不自己重分——因为划分后可能有人为筛选,别打乱原作者的设计。以下是这套数据集常见文件结构的参照,具体到手的包可能略有差异:
| 内容 | 常见结构 | 说明 |
|---|---|---|
| 训练图像目录 | train/类别名/*.jpg | 每类一个子目录 |
| 验证图像目录 | val/类别名/*.jpg | 与训练同命名规则 |
| 标签索引文件 | class_names.txt 或 JSON | 类别与数字ID映射 |
| 全量图及标注 | 部分包只给一个根目录加CSV | 需自行划分 |
3. 用迁移学习跑通第一个分类模型:ResNet是最稳的起点
面对2100张图,最忌直接从头训练一个深层卷积网络。图像分类模型动辄参数量百万级,这个规模的数据会让模型严重过拟合。可靠做法是加载在ImageNet上预训练过的骨干网络,冻结前几层,只微调后面的分类层。这也是当前图像分类算法里的主流套路。
3.1 为什么从ResNet-18或ResNet-50开始
ResNet系列在中小数据集上的表现非常稳定。ResNet-18参数量小、训练快,适合先摸清数据质量;ResNet-50更准但更吃显存。Vision Transformer在小数据集上反而不如卷积网络稳,除非你有足够数据做大规模增强。我一般先用ResNet-18跑通全流程,确认数据没有问题再换ResNet-50冲分数。
关键点是:加载预训练权重时要把最后一层全连接层换掉,因为ImageNet是1000类,而这里是几个病害类。
import torchvision.models as models import torch.nn as nn # 加载预训练ResNet-18,把输出维度改为类别数 num_classes = len(class_to_idx) # 通常是5或6 model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) model.fc = nn.Linear(model.fc.in_features, num_classes) # 冻结前四层,只训练后面的层,这是小数据集防过拟合的关键 for name, param in model.named_parameters(): if name.startswith("layer4") or name.startswith("fc"): param.requires_grad = True else: param.requires_grad = Falserequires_grad=False的层在前向传播中照常计算但不会再更新权重。这里冻结到layer3之前,只让layer4和全连接层参与训练,参数更新量大幅减少。如果你的图像风格和ImageNet差异很大(比如是显微镜下的孢子图),冻结层会阻碍模型提取新特征,这时可以少冻结一些,只冻结conv1和layer1。
3.2 最小可跑通的训练脚本
下面是去掉日志和可视化后的核心训练代码。batch size选16,因为2100张图不算多,batch太大容易让梯度方向不够精细;学习率用1e-4而不是默认的1e-3,预训练模型微调时学习率要降一段,否则容易一步跨过最优区域。
import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import transforms, datasets # 数据增强:翻转与归一化按ImageNet的mean/std做 train_transform = transforms.Compose([ 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]), ]) train_dataset = datasets.ImageFolder("potato_leaf_dataset/train", transform=train_transform) train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True, num_workers=4) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-4) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) 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) print(f"Epoch {epoch+1}/{30}, Loss: {running_loss/len(train_loader.dataset):.4f}")训练循环本身没有玄学,真正决定结果的是增强策略和学习率。scale=(0.8, 1.0)避免裁剪出太小的叶片碎片,马铃薯病斑通常分布在叶片边缘,裁剪太狠会把病斑切掉。num_workers=4在Windows上偶尔会报多进程错误,如果遇到,改成num_workers=0最省事。
3.3 四个必调参数:batch size、学习率、冻结层、epoch数
这几个参数量级小,调起来很快,记录下常用的经验值:
| 参数 | 常见取值区间 | 调整信号 |
|---|---|---|
| batch size | 8~32 | 显存不够就减半,精度波动大就调大 |
| 学习率 | 5e-5~1e-3 | 训练loss不降就降学习率 |
| 冻结层数 | 冻结一半到只冻结stem | 验证集不涨就解冻更多层 |
| epoch数 | 20~50 | 观察验证集是否过拟合,不是固定值 |
epoch数不是越大越好。我见过很多人在第15个epoch已经到最优验证精度,却硬跑完40个epoch,模型开始背训练集细节。正确做法是每轮算验证集准确率,连续5轮不涨就早停,后面会专门讲。
4. 把准确率从85%提到95%:数据增强与评估细节
基础模型通常能到85%左右,再往上走就要靠增强和指标细读了。叶片病害有个特殊性——病斑在颜色和纹理上和健康组织有明显差异,模型很容易就学会看全局颜色分布而不是局部病斑。这会导致在拍摄环境相似的测试集上表现不错,一换环境就掉点严重。
4.1 病害图像特有的增强操作:亮度、色调抖动与随机旋转
马铃薯叶片的背景常常是土壤、温室环境或实验室桌面。如果只做水平翻转,模型会学到"背景特征"这种和病害无关的捷径。推荐加入颜色扰动来对抗这个:
train_transform = transforms.Compose([ transforms.RandomRotation(degrees=30), transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.3, hue=0.05), transforms.RandomAffine(degrees=0, translate=(0.05, 0.05)), transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ])hue=0.05要克制,色相偏移过大会让叶片变蓝变紫,破坏真实的病斑颜色信号。ColorJitter的brightness和contrast设0.3,是为了模拟不同光照条件下拍摄的叶片——早上露水反光和阴天暗光都会改变图像亮度。随机裁剪配合Resize,等效于让模型看到叶片的局部区域,逼它关注病斑纹理而不是整张图的平均颜色。
4.2 学习率调度与早停:不迷信固定epoch
到后期验证集损失开始回升时,降低学习率可以让模型走进更平坦的极小值。常见做法是余弦退火或者在验证集连续不升时手动打折:
from torch.optim.lr_scheduler import ReduceLROnPlateau scheduler = ReduceLROnPlateau(optimizer, mode="max", factor=0.5, patience=3, min_lr=1e-6) # 在验证循环里调用,val_acc为当前验证集准确率 scheduler.step(val_acc)factor=0.5表示每次触发把学习率减半。patience=3表示连续3个epoch验证集准确率没有创新高才降。这套策略配合早停,实测能省掉约一半的无用训练时间。触发早停时保存模型权重的代码要在训练循环里带上:
if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), "best_model.pth")只保存重参数文件,不保存整个模型对象,因为换环境加载时类定义可能对不上。
4.3 评估指标:准确率不够,还要看召回率
2100张数据集的分类评估只看整体准确率会骗人。假设健康叶片占样本的60%,模型全猜健康也能有60%准确率,但对早疫病的召回是0。必须按类别看precision、recall、F1。
from sklearn.metrics import classification_report with torch.no_grad(): model.eval() all_preds = [] all_labels = [] for images, labels in val_loader: images = images.to(device) preds = model(images).argmax(dim=1).cpu() all_preds.extend(preds.tolist()) all_labels.extend(labels.tolist()) print(classification_report(all_labels, all_preds, target_names=class_to_idx.keys()))classification_report输出的每一行是一个病种的精确率、召回率和F1。你要重点盯召回率——如果某类别召回率明显低于其他类别,说明模型经常把它认成别类。病种漏检到生产环境里就是病害扩散的代价,宁可误报不能漏。
5. 马铃薯叶片病害分类最常见的五个坑:现象、原因与对策
这部分是血泪经验。同样的坑我在多个数据集上翻过车,列出来供参考。
5.1 现象:训练loss下降,验证集精度却纹丝不动
这多半是数据泄露或划分不均。检查验证集里每个类别的样本数,如果某一类在验证集里不足20张,评估结果方差会特别大。另一个根源是同源图片——同一棵植株拍了不同角度,模型见过训练集的正面,在验证集看到同一株的背面也能认出来,但这不是真正的泛化能力。
解决:重做分层采样,手动按植株来源分组后再划分。如果数据集没有提供植株ID,那就寄希望于拍摄时不是连拍,否则这个泄露基本无解。
5.2 现象:早疫病和晚疫病互相误判
这两类病斑在早期阶段视觉上确实像,都是叶片上出现褐色不规则区域。模型分不清是正常的。原因有两个:样本量不够大、病斑特征未被强调。
解决:对易混类别做差异化增强。早疫病病斑通常有同心环纹,晚疫病病斑边缘较模糊且伴随褪绿。用RandomResizedCrop时把scale下限调大,至少要0.5,保证裁剪区域包含完整病斑。同时给易混类别更高的类别权重:
weights = torch.tensor([1.0, 2.0, 1.0, 1.0], device=device) # 给晚疫病提高权重 criterion = nn.CrossEntropyLoss(weight=weights)改完之后混淆矩阵里这两个类别的互认数量会下降,代价是整体准确率可能略降,因为模型变得保守。
5.3 现象:健康叶片被误判为病害
这个反直觉的情况通常发生在健康叶片占比少、且背景相似的数据集里。模型学到的是"叶片不平整=有病",而健康叶片的叶脉纹理本身就不平整。
解决:检查你的增强里有没有加RandomGrayscale(p=0.1)。灰度化会削弱颜色信息,强迫模型学纹理和形状。如果加了还误判,降低训练集里健康叶片的增强强度,保留更多原始颜色分布,让模型能区分"叶片本色"和"病斑变色"。
5.4 现象:训练代码在Windows上跑着跑着报DataLoader错误
Windows下num_workers>0会有概率触发子进程中断。这不是代码逻辑错,是PyTorch在Windows上的多进程机制和Unix不同。现象是训练中途无征兆崩溃,或者卡死不动。
解决:先把num_workers设为0,确认能跑通。想提速就换Linux环境,如果必须在Windows上跑,加一段保护逻辑:
if __name__ == "__main__": # 主训练逻辑全部包进来 train_loop()文件名含中文也会引发类似报错,提前将所有图像重命名为英文字母加数字,最省事。
5.5 现象:训练曲线震荡剧烈,loss像锯齿
最常见原因是batch size偏小加学习率偏大。2100张图让数据方差本身就大,每个batch的分布差异明显。学习率1e-3在预训练模型上几乎必然震荡。
解决:把学习率降到5e-5,batch size提到32。如果显存有限,梯度累积也可以:
accumulation_steps = 2 # 每跑2个batch更新一次参数,等效于batch size翻倍 loss = loss / accumulation_steps loss.backward() if (step + 1) % accumulation_steps == 0: optimizer.step() optimizer.zero_grad()注意loss要除以累积步数,否则梯度会被放大到原batch size的2倍,反而更不稳。
6. 验证模型学到了什么:混淆矩阵与Grad-CAM定位依据
能跑出90%以上的准确率只是第一步,交付时要回答一个问题:模型到底按什么特征做出了判断?特别是农业生产方会关心模型是不是靠叶片边缘锯齿来分类——那会导致真实场景里掉链子。用混淆矩阵和Grad-CAM验证,是检验模型学到的是不是"病斑"的关键。
6.1 混淆矩阵:定位易混对,决定补数据方向
混淆矩阵比准确率实在得多。在不平衡数据集中,它能直接暴露哪两个类别在互相争夺边界。我一般按行归一化,因为原始计数在类别不平衡时会掩盖比率问题:
from sklearn.metrics import confusion_matrix import matplotlib.pyplot as plt cm = confusion_matrix(all_labels, all_preds) cm_normalized = cm.astype("float") / cm.sum(axis=1, keepdims=True) # 打印错得最多的类别对 for i, row in enumerate(cm_normalized): for j, val in enumerate(row): if i != j and val > 0.1: print(f"{class_names[i]} -> {class_names[j]}: {val:.1%}")输出中的高比例组合就是你要去补数据或调损失权重的类别对。如果是早疫病和晚疫病互认,说明病斑形态差异不足,优先加两类各自的典型图片;如果是健康叶片被误判为早疫病,问题多半在背景,需要改善拍摄角度。
6.2 Grad-CAM:看模型注意力是否落在病斑上
Grad-CAM能生成热力图,标识模型分类时看的是图像哪个区域。如果热力图集中在叶片边缘或背景,那这个模型不可靠。实现时用torchvision自带支持的钩子方式:
from torchvision.models.feature_extraction import create_feature_extractor # 提取ResNet最后一层卷积的输出 model.eval() feature_extractor = create_feature_extractor( model, return_nodes={"layer4": "feature"} ) input_tensor = preprocess_image(img).unsqueeze(0).to(device) with torch.enable_grad(): features = feature_extractor(model, input_tensor)["feature"] output = model.fc(features.flatten(1)) pred_class = output.argmax(dim=1) class_score = output[0, pred_class] class_score.backward()得到features的梯度后,对每个通道做全局平均池化得到权重,加权求和后过ReLU,就能得到和输入图尺寸接近的热力图。我不贴完整绘图代码,关键是观察结果:正常的热力图应该是一团集中在叶片中央病斑区的亮色。如果热力图散落在背景上,优先怀疑背景过拟合,把背景裁剪掉或换一批不同背景的图片重训。
拿被误判的图片挨个过一遍Grad-CAM,是唯一能确认模型“没走捷径”的办法。这个习惯帮我筛掉过很多看起来Acc很高、实际上学了奇怪特征的模型。训练一个分类器不难,难的是证明它在真实地里能用。如果你也在做类似的小规模病害分类,先跑通基线、再做混淆矩阵和Grad-CAM验证,再考虑打磨准确率,这个顺序值回票价。希望帮到你。
本文还有配套的精品资源,点击获取