☰
番茄叶子缺陷图像分类实战:3,000张已标注数据集从训练到部署
2026/10/5 8:58:45 网站建设 项目流程

简介:这份番茄叶子缺陷图像分类数据集面向从事图像分类、农业病害识别与深度学习实践的开发者与研究者,提供可直接投入训练与验证的标注数据,帮助解决番茄叶片病害分类任务中数据获取与预处理成本高的问题。资源包共约2000个文件,以1998张jpg图像为主体,另含1个py可视化脚本与1个json标注文件,压缩包约161.45MB,图像已按训练集、测试集划分并归入同一类别目录,json文件记录细菌斑点、早疫病、健康、Septoria_spot等7个类别的具体信息,运行show脚本即可快速浏览样本分布与图像质量。目前已有63人学习下载。读者可借此获得一套结构清晰、开箱即用的分类数据,用于搭建基线模型、验证网络改进效果或开展迁移学习实验,同时结合配套脚本快速完成数据检查与可视化,降低从零整理数据的时间成本。

1. 番茄叶子缺陷图像分类数据集:3,000 张已标注样本能跑出什么结果

棚里番茄叶片刚出现褐色斑点时,人眼判断往往要等两三天才敢确认是早疫还是叶霉。等确认完,病斑已经扩散到第三层叶片。这类场景下,一个约 3,000 张、已标注的番茄叶子缺陷图像分类数据集,价值不在于数据量有多大,而在于它把“叶片长什么样算哪种缺陷”这件事固定成了可训练的标签体系。它适合三类人:想入门图像分类但苦于没有干净标注数据的算法新手;需要快速验证某个 backbone 或增强策略是否有效的工程师;以及做农业视觉产品、要拿一个小规模闭环先跑通采集到部署链路的团队。3,000 张不算多,但已标注意味着你能把精力放在模型和流程上,而不是先花两周清洗标签。下面按“数据怎么读、模型怎么选、训练怎么调、坑在哪”一路拆开。

2. 番茄叶子缺陷数据集的结构拆解与加载方式

拿到一个已标注的图像分类数据集,第一件事不是急着写模型,而是把目录结构、类别分布和图像规格摸清楚。番茄叶子缺陷常见类别包括早疫病、晚疫病、叶霉病、健康叶片等,不同来源的类别命名和数量会有差异。约 3,000 张的规模,如果按 8:1:1 划分,训练集约 2,400 张,验证和测试各约 300 张。这个量级下,类别是否均衡直接决定你要不要做重采样或加权损失。

2.1 先确认目录是 ImageFolder 还是 CSV 索引

已标注数据集最常见的两种组织方式:一种是按类别分文件夹,每类一个目录,直接用torchvision.datasets.ImageFolder读;另一种是图像放一起,用 CSV 记录文件名和标签。前者省事,后者灵活。先跑一段脚本统计每类数量和图像尺寸,别跳过这一步。

import os from collections import Counter from PIL import Image root = "tomato_leaf_defect/train" counter = Counter() sizes = Counter() for cls in sorted(os.listdir(root)): cls_dir = os.path.join(root, cls) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): if not fname.lower().endswith((".jpg", ".jpeg", ".png")): continue counter[cls] += 1 with Image.open(os.path.join(cls_dir, fname)) as im: sizes[im.size] += 1 print("类别分布:", dict(counter)) print("尺寸分布 top5:", sizes.most_common(5))

这段脚本做两件事:统计每个类别的样本数,以及统计图像分辨率分布。逻辑很直白,但输出信息决定后续决策。如果某一类只有 200 张而另一类有 800 张,直接训练会让模型偏向多数类;如果尺寸分布很散,比如既有 256×256 又有 1024×1024,统一 resize 到 224 或 256 时要注意长宽比,别把病斑拉变形。

参数上,root指向训练集根目录,类别文件夹名就是标签名。sizes用Counter统计元组,能快速看出是否需要统一预处理。常见做法是训练和验证分别统计,确认两边类别比例一致,避免验证集里某一类几乎没样本。

2.2 用 DataLoader 搭一个可复现的输入管道

确认结构后,把加载管道固定下来。图像分类的输入管道包括 resize、归一化、增强三部分。番茄叶片图像的特点是背景相对单一、病斑区域对比度有时偏低,所以增强策略要克制,别一上来就 RandAugment 拉满。

import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms train_tf = transforms.Compose([ transforms.Resize((256, 256)), # 统一尺寸,保留病斑形状 transforms.RandomHorizontalFlip(p=0.5), # 叶片左右翻转不改变类别 transforms.RandomRotation(15), # 小角度旋转,模拟拍摄姿态 transforms.ColorJitter(brightness=0.2, contrast=0.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((256, 256)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) train_ds = datasets.ImageFolder("tomato_leaf_defect/train", transform=train_tf) val_ds = datasets.ImageFolder("tomato_leaf_defect/val", transform=val_tf) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4, pin_memory=True) print("类别到索引:", train_ds.class_to_idx)

这里几个参数值得说清楚。Resize((256, 256))而不是 224,是因为后续可能用 256 输入再随机裁剪到 224,给增强留空间;如果直接 224 也行,但别混用。RandomRotation(15)控制在 15 度以内,番茄叶片方向性不强,但病斑分布有位置特征,转太多会引入噪声。ColorJitter的 brightness 和 contrast 各 0.2,模拟棚内光照变化,再大就可能把浅色病斑洗掉。Normalize用 ImageNet 均值方差,这是迁移学习的常规做法,如果你从零训练,可以改成数据集自身统计值。

num_workers=4和pin_memory=True是吞吐优化,GPU 训练时能减少数据加载瓶颈。class_to_idx打印出来要记下,后面推理时索引对应关系不能错。验证集不做增强,只 resize 和归一化,保证评估可复现。

2.3 类别不均衡时先算权重再决定要不要重采样

如果 2.1 的统计显示类别差距超过 3:1,优先用加权损失,而不是马上过采样。过采样少数类容易导致过拟合,尤其是 3,000 张这个量级。加权损失实现简单,效果稳定。

import numpy as np from torch import nn counts = [counter[cls] for cls in train_ds.classes] total = sum(counts) weights = [total / (len(counts) * c) for c in counts] class_weights = torch.tensor(weights, dtype=torch.float32) criterion = nn.CrossEntropyLoss(weight=class_weights) print("类别权重:", dict(zip(train_ds.classes, [round(w, 3) for w in weights])))

权重公式是总数 / (类别数 × 该类样本数),少数类权重大,多数类权重小。CrossEntropyLoss的weight参数会在计算 loss 时按类别加权。注意权重张量要放到和模型相同的设备上,训练循环里别忘了class_weights.to(device)。如果加权后验证集少数类召回仍然很低,再考虑配合 WeightedRandomSampler 做温和重采样,但两者叠加要小心,可能让训练不稳定。

3. 从 ResNet 到 ViT:3,000 张规模下模型怎么选

3,000 张已标注图像在图像分类里属于小规模。这个量级下,模型选型的核心矛盾是:容量太大容易过拟合,容量太小欠拟合。最新的图像分类模型层出不穷,但小数据集上并不是越新越好。下面把选型逻辑和训练代码一起讲清楚。

3.1 小数据集优先迁移学习,backbone 从 ResNet 和 EfficientNet 试起

从零训练一个 CNN 在 3,000 张上几乎必然过拟合。常见做法是用 ImageNet 预训练权重做迁移学习,冻结前几层,只训练分类头和后面几个 stage。ResNet50 和 EfficientNet-B0 是两个稳妥起点:ResNet50 结构成熟、社区示例多;EfficientNet-B0 参数少、推理快,适合后续部署到边缘设备。

import torchvision.models as models import torch.nn as nn def build_model(num_classes, arch="resnet50", pretrained=True): if arch == "resnet50": model = models.resnet50(weights=models.ResNet50_Weights.DEFAULT if pretrained else None) in_features = model.fc.in_features model.fc = nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) ) elif arch == "efficientnet_b0": model = models.efficientnet_b0(weights=models.EfficientNet_B0_Weights.DEFAULT if pretrained else None) in_features = model.classifier[1].in_features model.classifier = nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, num_classes) ) else: raise ValueError(f"不支持的架构: {arch}") return model num_classes = len(train_ds.classes) model = build_model(num_classes, arch="resnet50") print(model.fc if hasattr(model, "fc") else model.classifier)

这段代码把分类头替换成 Dropout + Linear。Dropout 0.3 是小数据集的常规正则,别设太高,否则欠拟合。pretrained=True加载 ImageNet 权重,这是迁移学习的关键。注意weights参数在新版 torchvision 里替代了旧的pretrained布尔值,如果你环境版本较老,用pretrained=True也行,但要确认 API 兼容。

选 ResNet50 还是 EfficientNet-B0,可以两个都跑一轮对比验证集准确率和 F1。3,000 张规模下,EfficientNet-B0 往往在更少参数下达到接近甚至更好的效果,但 ResNet50 的预训练特征更“通用”,在病斑这种纹理特征上有时更稳。别只看准确率,类别不均衡时看宏平均 F1。

3.2 冻结策略与分层学习率:别让预训练权重被冲垮

迁移学习不是简单加载权重就完事。如果所有层都用同一个学习率,预训练学到的底层特征会被快速覆盖,小数据集上表现为训练 loss 下降但验证 loss 上升。常见做法是分层设置学习率:底层小学习率,分类头大学习率。

def set_layer_lr(model, base_lr=1e-4, head_lr=1e-3): params = [] head_params = [] for name, param in model.named_parameters(): if "fc" in name or "classifier" in name: head_params.append(param) else: params.append(param) optimizer = torch.optim.AdamW([ {"params": params, "lr": base_lr}, {"params": head_params, "lr": head_lr}, ], weight_decay=1e-4) return optimizer optimizer = set_layer_lr(model, base_lr=1e-4, head_lr=1e-3)

这里用 AdamW,weight_decay 1e-4。底层学习率 1e-4,分类头 1e-3,差一个数量级。如果显存够,也可以先冻结 backbone 只训练分类头 3 到 5 个 epoch,再解冻全部用更小的学习率微调。冻结策略没有绝对优劣,取决于你的验证集表现。判断标准很简单:如果解冻后验证集指标掉头向下,说明学习率太大或解冻太早。

3.3 训练循环里必须记录的四个指标

训练循环不只是跑 loss,要记录能帮你判断过拟合和欠拟合的指标。至少记录:训练 loss、验证 loss、验证准确率、验证宏平均 F1。前两个看拟合状态,后两个看类别均衡下的真实效果。

from sklearn.metrics import f1_score import torch device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) criterion = criterion.to(device) def run_epoch(model, loader, criterion, optimizer=None): is_train = optimizer is not None model.train() if is_train else model.eval() total_loss, preds_all, labels_all = 0.0, [], [] with torch.set_grad_enabled(is_train): for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) outputs = model(imgs) loss = criterion(outputs, labels) if is_train: optimizer.zero_grad() loss.backward() optimizer.step() total_loss += loss.item() * imgs.size(0) preds_all.extend(outputs.argmax(1).cpu().numpy()) labels_all.extend(labels.cpu().numpy()) avg_loss = total_loss / len(loader.dataset) acc = (np.array(preds_all) == np.array(labels_all)).mean() f1 = f1_score(labels_all, preds_all, average="macro") return avg_loss, acc, f1 for epoch in range(20): tr_loss, tr_acc, tr_f1 = run_epoch(model, train_loader, criterion, optimizer) va_loss, va_acc, va_f1 = run_epoch(model, val_loader, criterion) print(f"Epoch {epoch+1:02d} | train loss {tr_loss:.4f} acc {tr_acc:.4f} f1 {tr_f1:.4f} " f"| val loss {va_loss:.4f} acc {va_acc:.4f} f1 {va_f1:.4f}")

run_epoch通过optimizer是否为 None 区分训练和验证,避免写两套循环。f1_score用 macro 平均,每个类别权重相同,能暴露少数类被忽略的问题。如果验证 loss 连续 3 个 epoch 不降,就可以停或降学习率。20 个 epoch 是起点,小数据集通常 15 到 30 个 epoch 就能看出趋势。

4. 番茄叶片缺陷分类的避坑与排查清单

这一章是我自己在类似农业图像分类任务里踩过的坑,按“现象 → 原因 → 解决”写。番茄叶片数据集规模不大,很多问题不是模型结构引起的,而是数据和流程细节。

4.1 验证集准确率很高但实际推理全错

现象:训练完验证集准确率 95% 以上,拿棚里新拍的照片推理,结果几乎全错。原因通常是训练集和验证集来自同一批采集条件,背景、光照、拍摄角度高度相似,模型学到了背景特征而不是病斑特征。解决:划分验证集时按采集批次或拍摄日期划分,而不是随机划分。如果数据集没有批次信息,至少做一次跨背景测试,把不同背景的图片单独拿出来评估。增强策略里加入随机裁剪和轻微背景变化,逼模型关注叶片区域。

4.2 少数类召回率始终为 0

现象:宏平均 F1 很低,打印混淆矩阵发现某个类别全部预测成另一类。原因可能是该类样本太少,或者该类与另一类视觉差异极小,比如早期病斑和健康叶片边缘。解决:先确认标注是否一致,抽 20 张该类图片人工复核。如果标注没问题,提高该类损失权重,或者用 WeightedRandomSampler 温和过采样。还可以用混淆矩阵定位具体被误判成哪一类,针对性做类别间增强,比如对这两类做更强的颜色抖动。

4.3 训练 loss 震荡不收敛

现象:训练 loss 时高时低,验证指标不升。原因常见有三个:学习率太大、batch size 太小、数据增强太强。解决:先把增强关掉只留 resize 和归一化,跑一轮看 loss 是否平稳。如果平稳,逐项加回增强,定位是哪个增强引起。学习率从 1e-4 降到 1e-5 试。batch size 如果只有 8 或 16,考虑梯度累积,累积 2 到 4 步等效增大 batch。

4.4 图像尺寸不统一导致 resize 后病斑变形

现象:数据集中既有横拍又有竖拍,统一 resize 到正方形后病斑被拉长。原因是非等比缩放。解决:先做短边 resize 再中心裁剪,或者用 padding 补成正方形。transforms.Resize(256)只写一个整数时,短边缩到 256,长边等比缩放,再CenterCrop(224)或RandomCrop(224),能保留长宽比。如果病斑靠近边缘,中心裁剪可能裁掉,这时用 padding 更稳。

4.5 推理时类别索引对不上

现象:模型输出概率最高的索引,映射回类别名时错位。原因:训练时class_to_idx的顺序和推理时手动写的类别列表不一致。解决:把train_ds.class_to_idx保存成 JSON,推理时加载同一个映射文件。别在推理代码里手写类别顺序,这是血泪经验。保存模型时连同类别映射一起存,或者单独存一个label_map.json。

5. 把 3,000 张数据集的验证做扎实:混淆矩阵与推理脚本

最后一章落到一个具体技巧:怎么用混淆矩阵和单张推理脚本,把 3,000 张数据集的验证做扎实。很多人训练完只看一个准确率数字,但农业缺陷分类里,错分代价不一样,把健康判成病害和把病害判成健康,后果完全不同。混淆矩阵能告诉你错在哪。

先跑验证集混淆矩阵:

from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for imgs, labels in val_loader: imgs = imgs.to(device) outputs = model(imgs) all_preds.extend(outputs.argmax(1).cpu().numpy()) all_labels.extend(labels.numpy()) cm = confusion_matrix(all_labels, all_preds) print(classification_report(all_labels, all_preds, target_names=train_ds.classes, digits=4)) plt.figure(figsize=(8, 6)) sns.heatmap(cm, annot=True, fmt="d", cmap="Blues", xticklabels=train_ds.classes, yticklabels=train_ds.classes) plt.xlabel("预测") plt.ylabel("真实") plt.tight_layout() plt.savefig("confusion_matrix.png", dpi=150)

classification_report给出每个类别的精确率、召回率和 F1,confusion_matrix给出具体错分方向。重点看两件事:健康叶片有多少被误判成病害,以及哪两类之间互相误判最多。如果健康被误判成病害的比例高,说明模型对健康叶片的特征学得不够,可以补充健康叶片样本或调整阈值。

再写一个单张推理脚本,方便拿新照片快速验证:

from PIL import Image import json import torch.nn.functional as F def predict(image_path, model, transform, class_to_idx, device): model.eval() img = Image.open(image_path).convert("RGB") tensor = transform(img).unsqueeze(0).to(device) with torch.no_grad(): logits = model(tensor) probs = F.softmax(logits, dim=1)[0] idx_to_class = {v: k for k, v in class_to_idx.items()} top_probs, top_idxs = probs.topk(3) results = [(idx_to_class[i.item()], round(p.item(), 4)) for p, i in zip(top_probs, top_idxs)] return results with open("label_map.json", "r", encoding="utf-8") as f: class_to_idx = json.load(f) test_tf = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ]) print(predict("test_leaf.jpg", model, test_tf, class_to_idx, device))

这个脚本输出 top3 类别和概率,比只看一个结果更有判断空间。如果 top1 概率只有 0.4,top2 有 0.35,说明模型不确定,这种样本值得人工复核。label_map.json就是训练时保存的class_to_idx,保证索引一致。推理预处理必须和验证集一致,别用训练增强那套。

一个习惯:每次训练完,先看混淆矩阵,再看 top3 推理结果,最后才看准确率。准确率会骗人,混淆矩阵不会。3,000 张的数据集不大,但把验证做扎实,比盲目换更大的模型有用得多。希望帮到你。

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

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

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

立即咨询