
简介这份番茄叶子缺陷图像分类数据集面向从事图像分类、农业病害识别与深度学习实践的开发者与研究者提供可直接投入训练与验证的标注数据帮助解决番茄叶片病害分类任务中样本获取与预处理成本高的问题。资源包共约2000个文件以1998张jpg图像为主体另含1个py脚本与1个json标注文件压缩包大小约161.45MB图像按训练集与测试集分别存放同类数据归入同一目录json文件记录细菌斑点、早疫病、健康、Septoria_spot等7个类别的具体划分信息py脚本可用于数据集可视化预览。目前已有63人学习下载。读者可借助该数据集快速搭建分类网络输入管线验证模型在真实叶片图像上的表现并参考配套的图像分类与分割改进思路及计算机视觉完整项目内容完成从数据加载、类别统计到训练评估的完整流程适合作为课程设计、竞赛练习或算法对比实验的基础数据。1. 番茄叶子缺陷图像分类数据集3000 张已标注样本能跑出什么结果去年帮一个做设施农业的朋友处理大棚巡检数据他拿手机拍了两个月番茄叶片攒了四千多张图问我能不能直接训个模型识别早疫病和晚疫病。我打开一看光照忽明忽暗、背景里全是滴灌管和地膜标注只有文件夹名。这件事让我意识到番茄叶子缺陷图像分类数据集这类资源真正的价值不在图片数量而在于「已标注」三个字背后省掉的清洗成本。约 3000 张已标注数据如果类别均衡、标注规范足够从零训出一个可用的分类基线也能做迁移学习的小样本验证。它适合两类人一是想入门图像分类但被数据准备卡住的开发者二是做智慧农业、植物表型分析、边缘端病害识别预研的工程师。下面我按「拿到数据集先看什么、怎么切分、怎么训、怎么避坑」的顺序把这条链路走一遍。2. 拿到番茄叶子缺陷图像分类数据集先做三件事查类别、看分布、验标注很多人拿到数据集第一反应是直接ImageFolder加载开训结果训到一半发现某类只有几十张或者标注文件里混进了非叶片图。番茄叶片缺陷分类的类别通常围绕早疫病、晚疫病、叶霉病、健康叶这几类展开但不同来源的数据集类别命名和粒度差异很大。先花半小时做体检比后面调三天参划算。2.1 用脚本统计类别分布与图像尺寸第一步不是写模型是写一个统计脚本。把目录结构、每类数量、图像分辨率、通道模式全部打出来。这一步能暴露三个问题类别是否均衡、是否存在损坏文件、尺寸是否统一。import os from pathlib import Path from PIL import Image from collections import defaultdict root Path(./tomato_leaf_defect) # 数据集根目录按类别分子文件夹 stats defaultdict(list) corrupt [] for cls_dir in sorted(root.iterdir()): if not cls_dir.is_dir(): continue for img_path in cls_dir.glob(*): if img_path.suffix.lower() not in {.jpg, .jpeg, .png, .bmp}: continue try: with Image.open(img_path) as im: im.verify() # 校验文件完整性 with Image.open(img_path) as im: stats[cls_dir.name].append(im.size) except Exception as e: corrupt.append((str(img_path), str(e))) for cls, sizes in stats.items(): w_set {s[0] for s in sizes} h_set {s[1] for s in sizes} print(f{cls}: {len(sizes)} 张, 宽度种类{len(w_set)}, 高度种类{len(h_set)}, 示例{sizes[0]}) print(f\n损坏文件数: {len(corrupt)}) for p, e in corrupt[:10]: print(p, e)这段代码的逻辑是遍历每个类别文件夹用verify()做完整性校验再记录尺寸。参数说明root指向数据集根目录要求是「一类一文件夹」的结构suffix过滤非图像文件避免.DS_Store或标注文本被误读。跑完后重点看两件事各类数量是否差距超过 3 倍尺寸种类是否超过 5 种。如果某类只有一两百张后面必须用加权采样或数据增强补如果尺寸五花八门统一 resize 到 224 或 256 是常规做法。2.2 抽样目检标注质量与背景干扰统计只能看数量标注对不对必须肉眼抽检。从每类随机抽 20 张拼成网格图看。番茄叶片数据最常见的标注问题是把健康叶误标成早期病斑、把多片叶混在一起标成单类、背景里地膜反光被当成病斑。我一般会写个拼图脚本一次性看完。import random import matplotlib.pyplot as plt from PIL import Image from pathlib import Path root Path(./tomato_leaf_defect) classes [d.name for d in root.iterdir() if d.is_dir()] fig, axes plt.subplots(len(classes), 8, figsize(16, 2 * len(classes))) for i, cls in enumerate(classes): imgs list((root / cls).glob(*.jpg)) list((root / cls).glob(*.png)) samples random.sample(imgs, min(8, len(imgs))) for j, p in enumerate(samples): axes[i][j].imshow(Image.open(p)) axes[i][j].axis(off) axes[i][0].set_ylabel(cls, fontsize9) plt.tight_layout() plt.savefig(sample_grid.png, dpi120)逻辑说明每类抽 8 张横向排列行标签是类别名。参数说明random.sample保证不重复抽样min(8, len(imgs))防止某类样本不足时报错。看拼图时重点判断同一类内部是否形态一致、有没有明显不属于该类的图混入。如果发现某类里混了别的病害要么手动剔除要么在训练时当作噪声容忍——但类别少的时候建议剔除3000 张的规模经不起太多脏数据。2.3 划分训练验证测试集时别按随机切随机切分在图像分类里有个隐蔽的坑同一片叶子连拍的多张图可能被分到训练集和验证集导致验证指标虚高。番茄叶片数据如果是连续拍摄的相邻帧高度相似。稳妥做法是按「拍摄批次」或「叶片编号」分组切分没有分组信息时至少用固定随机种子并检查重复图。import hashlib from pathlib import Path from collections import defaultdict def file_hash(p, block8192): h hashlib.md5() with open(p, rb) as f: while chunk : f.read(block): h.update(chunk) return h.hexdigest() root Path(./tomato_leaf_defect) hash_map defaultdict(list) for p in root.rglob(*): if p.suffix.lower() in {.jpg, .png, .jpeg}: hash_map[file_hash(p)].append(str(p)) dups {k: v for k, v in hash_map.items() if len(v) 1} print(f重复图片组数: {len(dups)}) for k, v in list(dups.items())[:5]: print(v)逻辑说明用 MD5 对文件内容做哈希内容完全相同的图会落到同一组。参数说明block8192是分块读取大小大图也不会吃满内存。如果重复组很多说明数据集里有大量连拍或增强过的副本切分时必须整组归到同一侧否则验证集就是「背答案」。这一步做完再按 7:1.5:1.5 划分训练集用于拟合验证集调参测试集只在最后跑一次。3. 用迁移学习在 3000 张番茄叶片上训出可用模型从基线到调参3000 张图在图像分类里属于小样本从零训 ResNet 基本会过拟合。常见做法是拿 ImageNet 预训练权重做迁移冻结主干先训分类头再解冻部分层微调。这一章给出可复现的训练脚本和关键参数模型选型上EfficientNet-B0 或 ResNet18 在边缘端部署友好精度也够用。3.1 构建 DataLoader增强策略与类别权重番茄叶片图像的增强不能照搬通用配方。颜色抖动要克制因为病斑颜色是重要特征随机裁剪可以但别裁掉病斑区域水平翻转安全垂直翻转对叶片语义影响不大但也能用。类别不均衡时用加权采样。import torch from torch.utils.data import DataLoader, WeightedRandomSampler from torchvision import datasets, transforms from collections import Counter train_tf transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.7, 1.0)), # 保留主体避免裁掉病斑 transforms.RandomHorizontalFlip(p0.5), transforms.RandomVerticalFlip(p0.2), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.1, hue0.02), # 颜色抖动要轻 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_tf transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_ds datasets.ImageFolder(./split/train, transformtrain_tf) val_ds datasets.ImageFolder(./split/val, transformval_tf) targets [s[1] for s in train_ds.samples] counts Counter(targets) class_weights {c: 1.0 / n for c, n in counts.items()} sample_weights [class_weights[t] for t in targets] sampler WeightedRandomSampler(sample_weights, num_sampleslen(sample_weights), replacementTrue) train_loader DataLoader(train_ds, batch_size32, samplersampler, num_workers4, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) print(类别分布:, counts)逻辑说明训练增强包含缩放裁剪、翻转、轻量颜色抖动验证集只做确定性的 resize 和中心裁剪。参数说明scale(0.7, 1.0)控制裁剪面积下限太低会切掉病斑ColorJitter的hue0.02很小因为色相变化会破坏病斑颜色特征WeightedRandomSampler让少样本类别被采到的概率提高replacementTrue表示有放回采样。batch_size32在 8GB 显存上跑 224 输入比较稳显存小就降到 16。3.2 迁移学习训练循环冻结、解冻与学习率设置训练分两阶段先冻结主干只训分类头让随机初始化的头收敛再解冻最后几个 block 做小学习率微调。这样比一上来全量微调稳定也不容易把预训练特征打乱。import torch.nn as nn import torch.optim as optim from torchvision import models device torch.device(cuda if torch.cuda.is_available() else cpu) num_classes len(train_ds.classes) model models.efficientnet_b0(weightsmodels.EfficientNet_B0_Weights.IMAGENET1K_V1) for param in model.parameters(): param.requires_grad False # 第一阶段冻结全部主干 model.classifier[1] nn.Linear(model.classifier[1].in_features, num_classes) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.classifier.parameters(), lr1e-3, weight_decay1e-4) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max10) def run_epoch(loader, trainTrue): model.train() if train else model.eval() total_loss, correct, total 0.0, 0, 0 with torch.set_grad_enabled(train): for x, y in loader: x, y x.to(device), y.to(device) out model(x) loss criterion(out, y) if train: optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() * x.size(0) correct (out.argmax(1) y).sum().item() total x.size(0) return total_loss / total, correct / total for epoch in range(10): tr_loss, tr_acc run_epoch(train_loader, True) va_loss, va_acc run_epoch(val_loader, False) scheduler.step() print(fEpoch {epoch1}: train_loss{tr_loss:.4f} train_acc{tr_acc:.4f} val_loss{va_loss:.4f} val_acc{va_acc:.4f}) # 第二阶段解冻最后两个 block 微调 for name, param in model.named_parameters(): if features.7 in name or features.8 in name: param.requires_grad True optimizer optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr1e-4, weight_decay1e-4) for epoch in range(10): tr_loss, tr_acc run_epoch(train_loader, True) va_loss, va_acc run_epoch(val_loader, False) print(fFinetune Epoch {epoch1}: train_acc{tr_acc:.4f} val_acc{va_acc:.4f})逻辑说明第一阶段只更新classifier学习率 1e-3第二阶段解冻 EfficientNet 的最后两个 stage学习率降到 1e-4。参数说明AdamW的weight_decay1e-4抑制过拟合CosineAnnealingLR的T_max10对应第一阶段轮数解冻层用features.7、features.8是 EfficientNet-B0 的深层 block浅层特征通用性强不必动。如果验证准确率在第一阶段就冲到很高但训练准确率低说明数据太简单或验证集泄漏回头查切分。3.3 评估不只看准确率混淆矩阵与单类召回番茄病害分类里把「早疫病」误判成「健康叶」比反过来代价大得多因为漏检会导致整株传染。所以评估必须看每类召回率不能只看总体准确率。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(classification_report(all_labels, all_preds, target_namesval_ds.classes, digits4)) print(混淆矩阵:) print(confusion_matrix(all_labels, all_preds))逻辑说明classification_report输出每类的 precision、recall、f1。参数说明target_names用val_ds.classes保证类别名对应digits4方便看小差异。重点看召回率最低的那一类通常是样本最少或标注最乱的类。如果某类召回低于 0.7优先补该类数据或单独调该类阈值而不是盲目加轮数。4. 番茄叶片缺陷分类的避坑清单五条血泪经验这一章记录我在植物病害分类项目里真实翻过的车每条按现象、原因、解决写。番茄叶片数据集的坑和通用图像分类不完全一样背景干扰和标注粒度是重灾区。4.1 验证集准确率 99% 但上线就废现象本地验证准确率冲到 0.99部署到手机端拍照识别健康叶被大量判成病害。原因数据集里的图多是近距离、均匀光照的样本而实际拍摄有阴影、反光、远距离小目标域差异巨大。另外切分时连拍图泄漏验证集等于训练集的近邻。解决按文件哈希去重后再切分训练时加入模拟真实场景的增强比如随机遮挡、亮度大幅变化、运动模糊上线前用真实手机拍一批图做测试集别信实验室指标。4.2 颜色抖动开太大病斑特征被抹掉现象训练损失下降正常但模型对早疫病和晚疫病的区分能力很差混淆矩阵里两类互相误判。原因早疫病和晚疫病的差异部分体现在病斑颜色和边缘色泽上ColorJitter的hue或saturation开大后这些细微差异被随机化模型学不到判别特征。解决把hue控制在 0.02 以内saturation不超过 0.2如果类别间主要靠颜色区分考虑在 HSV 空间做固定变换而不是随机抖动或者干脆去掉颜色增强。4.3 类别文件夹命名带空格和中文ImageFolder 报错现象datasets.ImageFolder加载时报FileNotFoundError或类别名乱码。原因ImageFolder 按文件夹名生成类别索引中文或空格在某些系统编码下会出问题且类别顺序按字典序容易和预期不一致。解决统一改成英文小写加下划线比如early_blight、late_blight、leaf_mold、healthy加载后打印train_ds.classes确认顺序保存模型时把类别列表一起存推理时按同一顺序解码。4.4 训练集和验证集归一化参数不一致现象验证损失比训练损失高一大截且验证准确率波动剧烈。原因训练用了Normalize验证忘了加或者两边的 mean/std 写的不一样。番茄叶片图像如果自己统计了数据集均值训练和验证必须用同一组。解决把 transform 定义成函数统一管理训练和验证共用同一个Normalize如果要用数据集自身均值先在训练集上统计一次写死到配置里验证和测试都复用。4.5 显存不够就无脑降 batch size忘了同步调学习率现象从 batch 32 降到 8 后训练变得极不稳定损失震荡不收敛。原因学习率是按大 batch 设的batch 变小后梯度噪声增大等效学习率偏高。解决batch 减半时学习率大致减半或者用梯度累积模拟大 batch。比如batch_size8配accumulate_steps4等效 batch 32学习率保持 1e-3。另外开pin_memoryTrue和num_workers4能缓解数据加载瓶颈但别把 workers 开太大IO 跟不上反而慢。5. 把 3000 张番茄叶片模型推到边缘设备量化、导出与现场验证训完模型只是半程番茄大棚里真正要用起来得把模型塞进边缘设备或手机。3000 张数据训出的 EfficientNet-B0 参数量约 5MFP32 权重 20MB 左右量化到 INT8 能压到 5MB 上下推理延迟在树莓派 4 上大约几百毫秒手机端更快。这一章给一个从 PyTorch 到 ONNX 再到量化的具体路径以及现场验证的笨办法。5.1 导出 ONNX 并做动态量化先导出 ONNX再用 ONNX Runtime 做动态量化。动态量化对分类模型友好不需要校准数据集权重和激活都量化。import torch import onnx import onnxruntime as ort from onnxruntime.quantization import quantize_dynamic, QuantType model.eval() dummy torch.randn(1, 3, 224, 224).to(device) torch.onnx.export( model, dummy, tomato_effb0.onnx, input_names[input], output_names[logits], dynamic_axes{input: {0: batch}, logits: {0: batch}}, opset_version12, ) quantize_dynamic( tomato_effb0.onnx, tomato_effb0_int8.onnx, weight_typeQuantType.QInt8, ) sess ort.InferenceSession(tomato_effb0_int8.onnx) import numpy as np test_input np.random.randn(1, 3, 224, 224).astype(np.float32) out sess.run(None, {input: test_input}) print(INT8 输出形状:, out[0].shape)逻辑说明torch.onnx.export把模型转成 ONNX 图dynamic_axes让 batch 维可变方便部署时按需调整。参数说明opset_version12兼容性较好quantize_dynamic的weight_typeQuantType.QInt8表示权重用 8 位整型动态量化不需要额外校准数据适合快速验证。导出后务必用同一张图对比 PyTorch 和 ONNX 的输出最大绝对误差在 1e-3 量级算正常超过 1e-2 要查算子兼容性。5.2 现场验证用「三档置信度」而不是单阈值实验室里 argmax 就完事现场不能这么干。番茄叶片识别错一类的代价不同我一般设三档置信度高于 0.85 直接给结论0.6 到 0.85 提示「疑似建议复拍」低于 0.6 直接返回「无法判断请重新拍摄」。这样能大幅降低误报带来的信任损耗。def predict_with_confidence(sess, img_tensor, classes, high0.85, low0.6): logits sess.run(None, {input: img_tensor})[0][0] exp np.exp(logits - logits.max()) probs exp / exp.sum() idx int(probs.argmax()) conf float(probs[idx]) if conf high: return classes[idx], conf, 确定 elif conf low: return classes[idx], conf, 疑似建议复拍 else: return unknown, conf, 无法判断逻辑说明手动做 softmax 避免依赖框架三档阈值把「不确定」显式暴露出来。参数说明high0.85、low0.6是经验值病害漏检代价高就把low提到 0.7宁可多让用户复拍。类别列表classes必须和训练时train_ds.classes顺序一致建议存成 JSON 随模型一起发。5.3 一个我常犯的错误拿验证集当测试集反复调最后说个习惯问题。我早期做植物分类时习惯性用验证集调阈值、选模型、试增强调了十几轮后验证集已经间接参与训练指标不可信。后来强制自己切分时留一份测试集锁在单独目录训练期间绝不碰只在最终交付前跑一次。如果测试集结果比验证集掉超过 5 个点说明调参过拟合了验证集得回头简化模型或补数据。3000 张的番茄叶片数据集不算大这个纪律尤其重要。希望帮到你。本文还有配套的精品资源点击获取