☰
花生叶片缺陷图像分类:770张已标注数据训练与调优实战
2026/10/5 5:27:50 网站建设 项目流程

简介:本资源为花生叶片缺陷图像分类数据集,面向从事图像分类、农业病害识别及深度学习模型改进的开发者与研究者,可用于训练和评估分类网络。数据已完成预处理,可直接作为分类模型输入,共划分3个类别:疾病叶片、死掉的叶片与健康叶片,具体类别信息可查看包内json文件。压缩包共780个文件,以777张jpg图像为主体,另含1个py脚本、1个png与1个json标注文件,整体约23.18MB,并已划分训练集与测试集,各类图片分目录存放,便于直接读取。资源中附带show脚本,可快速可视化数据集分布与样本效果。目前已有110人学习下载。读者可获得一套开箱即用的叶片缺陷分类数据,用于分类网络训练、对比实验与改进验证,同时结合配套的图像分类与分割改进、计算机视觉完整项目内容,快速搭建实验流程并复现结果。

1. 花生叶片缺陷图像分类:约 770 张已标注数据能跑出什么结果

拿到「花生叶片缺陷图像分类数据集【已标注,约770张数据】」这个标题,多数人第一反应是:770 张够不够训一个能用的分类模型?我的判断是——够,但前提是你别把它当成 ImageNet 那种量级的玩具来对待。花生叶片缺陷识别属于典型的农业细粒度视觉任务,田间场景下病斑、虫害、缺素症状在颜色和纹理上高度相似,770 张的规模意味着你必须把每一张图的价值榨干,而不是简单丢进ImageFolder跑一遍resnet50就完事。

这个数据集适合三类人:一是做智慧农业、植保巡检方向,需要快速验证一个叶片病害分类 baseline 的工程师;二是手里有类似小样本农业数据集、想找一套可复现训练流程的算法同学;三是想拿真实场景数据练手图像分类算法(从数据清洗到部署)的从业者。它解决的核心问题是:在标注数据有限、类别间视觉差异细微的条件下,如何用迁移学习加数据增强把分类精度推到可用水平。下面我按自己实际跑这类数据集的顺序,把选型、训练、调参和踩坑讲清楚。

2. 先搞清楚数据长什么样:类别分布、图像质量与划分策略

2.1 约 770 张已标注数据的典型结构与检查方法

农业图像分类数据集通常按类别分文件夹存放,目录结构大概率是peanut_leaf_defect/类别名/图片文件。在写任何训练代码之前,我一般先跑一段统计脚本,把类别数、每类样本量、图像尺寸和通道模式摸清楚。这一步不做,后面划分训练集时很容易出现某类验证集只有两三张的尴尬局面。

import os from PIL import Image from collections import Counter root = "peanut_leaf_defect" # 数据集根目录,按实际路径替换 class_counts = {} size_stats = [] for cls in sorted(os.listdir(root)): cls_dir = os.path.join(root, cls) if not os.path.isdir(cls_dir): continue imgs = [f for f in os.listdir(cls_dir) if f.lower().endswith((".jpg", ".jpeg", ".png", ".bmp"))] class_counts[cls] = len(imgs) for f in imgs[:20]: # 每类抽样20张看尺寸,避免全量IO过慢 with Image.open(os.path.join(cls_dir, f)) as im: size_stats.append((im.size, im.mode)) print("类别分布:", class_counts) print("总样本数:", sum(class_counts.values())) print("尺寸/模式抽样:", Counter(size_stats).most_common(5))

这段脚本做三件事:统计每类图片数量、汇总总样本量、抽样查看图像尺寸和色彩模式。参数上,root指向解压后的数据集目录;抽样数量20可以按需调整,数据量小的时候直接全量统计也行。输出里如果发现某类只有三四十张、另一类有两百多张,那类别不平衡就是你必须先处理的问题,而不是等到训练完看混淆矩阵才后悔。尺寸抽样则决定了你后面Resize到 224 还是 256——如果原图普遍只有 100 多像素,强行放大到 448 只会引入插值噪声。

2.2 小样本下训练/验证/测试怎么切才不翻车

770 张数据,如果按 8:1:1 切,测试集只有 77 张,单类可能不到 10 张,评估结果的方差会大到让你怀疑人生。我一般对这类小数据集采用分层抽样(stratified split),并且把验证集比例提到 15%~20%,测试集保留 10% 但明确它只用于最终一次评估,不参与任何调参决策。

import shutil from sklearn.model_selection import train_test_split def split_dataset(root, out_root, val_ratio=0.15, test_ratio=0.10, seed=42): for cls in sorted(os.listdir(root)): cls_dir = os.path.join(root, cls) if not os.path.isdir(cls_dir): continue imgs = [f for f in os.listdir(cls_dir) if f.lower().endswith((".jpg", ".jpeg", ".png", ".bmp"))] # 先切出测试集,再从剩余里切验证集,保证比例稳定 train_val, test = train_test_split(imgs, test_size=test_ratio, random_state=seed) train, val = train_test_split(train_val, test_size=val_ratio / (1 - test_ratio), random_state=seed) for subset, files in [("train", train), ("val", val), ("test", test)]: dst = os.path.join(out_root, subset, cls) os.makedirs(dst, exist_ok=True) for f in files: shutil.copy(os.path.join(cls_dir, f), os.path.join(dst, f)) split_dataset("peanut_leaf_defect", "peanut_split")

关键点在test_size的换算:先按test_ratio切出测试集,剩下的再按val_ratio/(1-test_ratio)切验证集,这样最终验证集占总量的比例才等于你设定的val_ratio。seed固定住,保证每次划分一致,否则你调参时验证集变了,指标波动根本分不清是模型改了还是数据换了。分层抽样由train_test_split在每类内部独立执行来近似实现,类别极不平衡时可以考虑stratify参数,但这里按类循环已经天然分层。

注意:划分完一定要再跑一次统计,确认每个子集里每类都至少有 5 张以上,否则验证指标没有参考意义。

3. 迁移学习选型与训练:从 ResNet 到轻量模型的取舍

3.1 为什么小样本农业图像优先用预训练 backbone

770 张图从零训练一个 CNN,几乎必然过拟合。花生叶片缺陷的判别依据主要是病斑的颜色、形状和纹理分布,这些低层和中层特征在 ImageNet 预训练模型里已经学得很扎实,你只需要微调高层语义部分。常见做法是冻结 backbone 的前若干层,只训练最后的分类头和部分高层 block。选型上,resnet18或resnet50是最稳的 baseline,efficientnet_b0在参数量和精度之间平衡得不错,如果最终要部署到边缘设备,mobilenet_v3_small值得一试。我一般先用resnet18跑通流程拿到基准精度,再决定要不要换更大的模型。

3.2 用 PyTorch 跑通第一个训练循环

下面这段代码是一个最小可用的训练脚本,包含数据增强、迁移学习、学习率调度和验证。数据增强对 770 张的数据集来说是刚需,RandomResizedCrop、RandomHorizontalFlip、ColorJitter这三个基本够用,农业图像里垂直翻转通常不合理(叶片朝向有语义),所以不加RandomVerticalFlip。

import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms, models train_tf = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) val_tf = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) data_dir = "peanut_split" train_ds = datasets.ImageFolder(f"{data_dir}/train", train_tf) val_ds = datasets.ImageFolder(f"{data_dir}/val", val_tf) train_loader = DataLoader(train_ds, batch_size=16, shuffle=True, num_workers=2) val_loader = DataLoader(val_ds, batch_size=16, shuffle=False, num_workers=2) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) for p in model.parameters(): # 先冻结全部,再解冻高层 p.requires_grad = False model.fc = nn.Linear(model.fc.in_features, len(train_ds.classes)) for p in model.layer4.parameters(): # 解冻最后一个stage p.requires_grad = True model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.AdamW( filter(lambda p: p.requires_grad, model.parameters()), lr=1e-3, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30) for epoch in range(30): model.train() for x, y in train_loader: x, y = x.to(device), y.to(device) optimizer.zero_grad() loss = criterion(model(x), y) loss.backward() optimizer.step() scheduler.step() # 验证 model.eval() correct = total = 0 with torch.no_grad(): for x, y in val_loader: x, y = x.to(device), y.to(device) pred = model(x).argmax(1) correct += (pred == y).sum().item() total += y.size(0) print(f"epoch {epoch+1}, val_acc={correct/total:.4f}")

逻辑上分四块:数据增强与加载、模型改造、优化器与调度、训练验证循环。参数说明几个关键点:batch_size=16是 770 张数据下的稳妥选择,显存够可以提到 32;lr=1e-3配合AdamW适合只训练分类头和解冻层,如果你解冻更多层,学习率要降到1e-4量级;T_max=30对应总 epoch 数,余弦退火让学习率平滑衰减。layer4是 ResNet 的最后一个残差 stage,解冻它能在不引入过多参数的情况下提升对病斑语义的适应能力。跑完 30 个 epoch,如果验证集准确率还在震荡,先别急着加 epoch,去看数据增强是不是过强了。

3.3 类别不平衡与增强策略的参数怎么定

如果统计阶段发现类别不平衡,CrossEntropyLoss可以传weight参数,按类别样本数的倒数来设。另一个有效手段是WeightedRandomSampler,让每个 batch 里各类别出现概率接近。数据增强的强度需要根据验证集表现来调:RandomResizedCrop的scale下限从 0.7 降到 0.5 会增强尺度不变性,但病斑被裁掉的风险也变大,我一般先保持 0.7,过拟合明显时再降。ColorJitter的幅度不宜过大,否则会把病斑颜色特征搅乱,brightness 和 contrast 各 0.2 是经验值。

提示:训练前把model.fc换成新层后,确认filter(lambda p: p.requires_grad, ...)里确实包含了新层参数,否则分类头根本不会被更新。

4. 评估与排查:小数据集上指标为什么会骗你

4.1 混淆矩阵比准确率更能暴露问题

770 张数据训出来的模型,准确率 85% 听起来还行,但如果某一类召回率只有 40%,实际部署就是灾难。评估阶段必须看混淆矩阵和每类的 precision/recall。用sklearn的classification_report配合confusion_matrix是最快的方式。

from sklearn.metrics import classification_report, confusion_matrix import numpy as np model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for x, y in val_loader: x = x.to(device) preds = model(x).argmax(1).cpu().numpy() all_preds.extend(preds) all_labels.extend(y.numpy()) print(confusion_matrix(all_labels, all_preds)) print(classification_report(all_labels, all_preds, target_names=val_ds.classes, digits=3))

classification_report会给出每类的 precision、recall、f1-score 和支持样本数。重点看支持样本数少但 recall 也低的类,那通常是模型没学到该类的判别特征,需要针对性补充数据或调整增强。混淆矩阵里如果两类互相误判严重,说明它们在视觉上确实接近,可以考虑引入更细粒度的注意力机制,或者干脆合并这两类再评估业务上是否可接受。

4.2 验证集准确率虚高的三个来源

第一个来源是数据泄漏:如果同一片叶子的多张照片被分到了训练集和验证集,验证指标会虚高。农业数据集采集时往往对同一病叶拍多张,划分时必须按「叶片个体」而不是按「图片」来切,否则模型记住的是这片叶子的背景而不是病斑特征。第二个来源是验证集太小,77 张里随机波动几个样本就能让准确率跳 5 个百分点,解决办法是交叉验证取平均。第三个来源是测试集被反复用于调参,调着调着测试集就变成了验证集,最终报告的数字没有意义。我的习惯是测试集只在最后跑一次,跑完就封存。

5. 避坑与常见问题:770 张数据训练时的真实翻车记录

现象:训练 loss 正常下降,验证准确率始终在 25% 左右不动。原因通常是类别数对不上——model.fc的输出维度用了默认的 1000,而数据集只有几类,或者ImageFolder读到的类别顺序和标签映射错位。解决方法是打印train_ds.classes和len(train_ds.classes),确认nn.Linear的输出维度与之一致,同时检查数据集目录下有没有混入非类别文件夹(比如__MACOSX或隐藏目录),它们会被ImageFolder当成一个额外类别。

现象:验证集准确率比训练集还高。在小数据集上这不一定是好事,常见原因是训练时用了强增强而验证时没有,导致训练难度被人为抬高;另一个原因是验证集样本太少且恰好容易分。解决办法是先把增强调弱一档再看,如果仍然验证高于训练,检查验证集是否混入了训练集图片(用文件哈希去重)。

现象:模型对某一类几乎全部预测错误。原因可能是该类样本量过少,也可能是该类图像在颜色或背景上与其他类差异过大,模型学到了背景捷径。解决办法是先做类别加权,再用WeightedRandomSampler平衡采样,同时检查该类图片是否有明显的采集偏差(比如全部来自同一地块、同一光照条件)。

现象:换了随机种子后准确率波动超过 10 个百分点。这是小数据集的典型特征,说明模型对数据划分敏感。解决办法是改用 5 折交叉验证报告平均指标和标准差,而不是只报一次划分的结果。如果标准差很大,说明数据量确实不够支撑稳定结论,需要考虑补充数据或使用更强的预训练模型。

现象:推理时单张图片预测结果和验证时不一致。原因通常是推理时的预处理和验证时不一致,比如忘了Normalize、Resize的插值方式不同、或者 PIL 读进来是灰度图而训练时是 RGB。解决办法是把验证集的 transform 单独抽成一个函数,推理时复用同一个函数,不要手写一套新的预处理。

6. 把 770 张用到极致:交叉验证、模型集成与推理加速

小数据集的进阶玩法不是换更大的模型,而是把现有数据的信息量榨到极限。我一般会做两件事:5 折交叉验证和 checkpoint 集成。5 折交叉验证把数据分成 5 份,每次用 4 份训练、1 份验证,最终报告 5 次的平均准确率和标准差。这样做的好处是每个样本都有机会出现在验证集中,指标更可靠,同时你得到了 5 个在不同数据子集上训练的模型。把这 5 个模型的预测概率平均,就是最简单的集成,通常能比单模型提升 2~4 个百分点,而且几乎不增加推理成本(如果串行推理)或只增加少量成本(如果并行)。

# 假设已有5折的模型 checkpoint 列表 models_list = [] for fold in range(5): m = models.resnet18(weights=None) m.fc = nn.Linear(m.fc.in_features, num_classes) m.load_state_dict(torch.load(f"fold{fold}_best.pth", map_location=device)) m.eval().to(device) models_list.append(m) def ensemble_predict(x): probs = torch.zeros(x.size(0), num_classes, device=device) with torch.no_grad(): for m in models_list: probs += torch.softmax(m(x), dim=1) return probs.argmax(1)

集成推理时把所有模型的 softmax 概率相加再取 argmax,比投票法更平滑。如果部署环境对延迟敏感,可以把 5 个模型蒸馏成一个学生模型,但蒸馏需要额外的训练轮次,770 张数据下蒸馏效果不一定稳定,我一般优先用集成。

另一个实用技巧是测试时增强(TTA):对同一张测试图做水平翻转和中心裁剪,分别推理后平均概率。TTA 在小数据集上通常能再涨 1~2 个百分点,代价是推理时间翻倍。如果业务允许,值得加上。

最后说一个我自己的习惯:每次跑完实验,把配置文件、数据划分文件、随机种子和最终指标一起存档。小数据集实验的可复现性特别脆弱,隔两周回来你根本记不清当时用的是哪个增强强度、哪个学习率。这个习惯帮我省了无数次重跑的时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询