☰
西红柿病害图像分类数据集:3.2万张标注图与11类识别实战
2026/10/5 0:48:13 网站建设 项目流程

简介:这份西红柿病害图像分类数据集面向从事农业视觉识别、深度学习课程实践与CNN分类网络改进的开发者与研究者,覆盖11类常见番茄病害,包括Bacterial_spot、powdery_mildew、Early_blight等,类别明细可查阅包内json文件。资源已按训练集与验证集划分,同类图片集中存放,便于直接接入主流分类框架训练与评估。压缩包共约2000个文件,以1998张jpg图像为主,另含1个py脚本与1个json标注文件,整体约739.33MB,其中show脚本可用于快速可视化样本分布与图像质量。目前已有38人学习下载。读者可借此完成数据加载、类别统计、模型训练与效果对比,并配合作者主页的CNN分类网络改进系列内容,将数据集用于骨干网络替换、注意力模块消融等实验,快速搭建可复现的番茄病害识别流程。

1. 三万张西红柿病害图:这个数据集到底能帮你解决什么

你手头有一批西红柿叶片照片,想训练一个能分辨早疫病、晚疫病、叶霉病的分类模型,但卡在第一步——没有标注好的数据。自己拍、自己标,三千张就能耗掉两周,更别说三万张。这个标题指向的,正是一个已经标注完成、约 32,000 张的西红柿病害图像分类数据集,覆盖 11 种病害类别。它解决的不是模型结构问题,而是「数据从哪来、标签怎么对齐、类别怎么分」这三件最耗人力的事。适合谁用?做农业 AI 落地的算法工程师、想跑通图像分类全流程的学生、以及需要快速验证病害识别方案的团队。你拿到它之后,真正要花心思的地方在于:类别映射对不对、划分比例合不合理、增强策略会不会把病斑特征抹掉。下面按「先看清数据长什么样,再动手跑通,最后避开翻车点」的顺序拆开讲。

2. 先搞懂 11 类西红柿病害的标注逻辑与目录结构

2.1 类别体系与标注粒度决定了模型上限

拿到一个图像分类数据集,第一件事不是急着写 DataLoader,而是把类别清单和每类样本量拉出来看。11 种西红柿病害通常涵盖:早疫病(Early Blight)、晚疫病(Late Blight)、叶霉病(Leaf Mold)、细菌性斑点病(Bacterial Spot)、靶斑病(Target Spot)、 mosaic virus(花叶病毒)、黄化曲叶病毒(Yellow Leaf Curl Virus)、蜘蛛螨(Spider Mites)、健康叶片等。不同数据集的类别命名可能是英文、拼音或编号,你必须先做一次映射,否则训练出来的 label 和实际病害对不上,模型再准也没法用。

标注粒度上,图像分类数据集一般是一图一标签,即整张叶片图对应一个类别。但西红柿病害有个坑:一片叶子上可能同时出现两种病斑。如果数据集是单标签体系,这类样本要么被归到主导病害,要么被剔除。你需要抽样几十张图,用肉眼确认标注是否与图像内容一致。常见做法是随机抽 5% 的样本,按类别分层抽样,逐张核对。如果发现某类错标率超过 5%,就要考虑清洗或重新标注。

样本量分布同样关键。32,000 张分到 11 类,平均每类约 2,900 张,但实际往往不均衡。健康叶片可能占 4,000 张,而某些罕见病害只有 800 张。这种长尾分布会直接导致模型偏向多数类。你需要在训练前统计每类数量,对少于 1,500 张的类别做过采样或强增强。下面这段脚本用来统计类别分布并生成映射表:

import os from collections import Counter from pathlib import Path # 假设数据集按类别文件夹组织:dataset/train/类别名/图片 data_root = Path("dataset/train") class_counts = Counter() class_names = sorted([d.name for d in data_root.iterdir() if d.is_dir()]) for cls in class_names: imgs = list((data_root / cls).glob("*.*")) class_counts[cls] = len(imgs) # 打印类别与数量,并生成 类别名->编号 映射 label_map = {name: idx for idx, name in enumerate(class_names)} print("类别映射:", label_map) for name, cnt in class_counts.items(): print(f"{name}: {cnt} 张")

这段代码的逻辑很直接:遍历训练集下每个类别文件夹,统计图片数量,同时按字母序生成从 0 开始的整数标签。参数上,data_root要改成你实际存放训练集的路径;如果数据集已经划分好 train/val/test,就对三个子集分别跑一遍,确认划分后各类比例是否一致。注意,glob("*.*")会匹配所有带扩展名的文件,如果文件夹里有非图片文件(如.DS_Store),需要加后缀过滤,比如*.jpg、*.png。

2.2 目录组织方式与划分比例怎么定

常见的数据集目录有两种:一种是train/val/test已经分好,每个子集下再按类别分文件夹;另一种是全部图片放在一个目录,标签在 CSV 里。标题说「已标注」,大概率是第一种。你需要确认划分比例。农业图像数据集常见做法是 7:2:1 或 8:1:1。如果原始数据只给了训练集,你得自己切分。切分时不能随机打乱,因为同一片叶子的多张照片可能被分到训练和验证集,造成数据泄漏。正确做法是按「叶片 ID」或「拍摄批次」分组切分。

如果你拿到的数据集已经分好 train/val/test,先检查三个子集的类别分布是否一致。比如训练集里晚疫病占 15%,验证集里只占 5%,那验证结果就会失真。下面这段代码用来检查各子集类别比例:

import pandas as pd from pathlib import Path splits = ["train", "val", "test"] rows = [] for split in splits: root = Path(f"dataset/{split}") for cls_dir in root.iterdir(): if cls_dir.is_dir(): cnt = len(list(cls_dir.glob("*.jpg"))) rows.append({"split": split, "class": cls_dir.name, "count": cnt}) df = pd.DataFrame(rows) pivot = df.pivot(index="class", columns="split", values="count").fillna(0) pivot["train_ratio"] = pivot["train"] / pivot.sum(axis=1) print(pivot)

逻辑说明:把三个子集的类别数量拉成一张透视表,并计算每类在训练集中的占比。参数上,*.jpg要按实际图片格式调整。如果发现某类在验证集或测试集中数量为 0,说明切分时没有按类别分层,需要重新用sklearn.model_selection.train_test_split的stratify参数切分。

提示:切分前先把所有图片路径和标签读进 DataFrame,用groupby按类别分层抽样,避免某一类在验证集里消失。

3. 用 PyTorch 跑通 11 类病害分类的最小训练流程

3.1 数据增强与预处理:别把病斑特征增强没了

西红柿病害识别的关键特征是小面积病斑的颜色、纹理和边缘。如果你直接套用 ImageNet 的增强策略,比如随机裁剪到 224×224、颜色抖动幅度过大,病斑可能被裁掉或颜色失真。我一般会这样配:训练集用RandomResizedCrop(224, scale=(0.7, 1.0)),保留至少 70% 的叶片区域;颜色抖动只调亮度和对比度,幅度控制在 0.2 以内;再加水平翻转和轻微旋转(±15 度)。验证集和测试集只做Resize(256)+CenterCrop(224),不做随机增强。

下面是一个可复用的 Dataset 和 Transform 配置:

import torch from torch.utils.data import Dataset, DataLoader from torchvision import transforms from PIL import Image from pathlib import Path class TomatoDataset(Dataset): def __init__(self, root, transform=None): self.root = Path(root) self.transform = transform self.samples = [] self.classes = sorted([d.name for d in self.root.iterdir() if d.is_dir()]) self.class_to_idx = {c: i for i, c in enumerate(self.classes)} for cls in self.classes: for img_path in (self.root / cls).glob("*.jpg"): self.samples.append((img_path, self.class_to_idx[cls])) def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path, label = self.samples[idx] img = Image.open(img_path).convert("RGB") if self.transform: img = self.transform(img) return img, label train_tf = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), transforms.RandomHorizontalFlip(), 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), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_ds = TomatoDataset("dataset/train", transform=train_tf) val_ds = TomatoDataset("dataset/val", transform=val_tf) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4)

逻辑说明:TomatoDataset按类别文件夹读取图片,自动生成class_to_idx。训练变换里scale=(0.7, 1.0)保证裁剪后仍保留大部分叶片;ColorJitter只调亮度和对比度,不调饱和度和色调,避免病斑颜色偏移。参数上,batch_size=32适合 8GB 显存;如果显存不够,降到 16 并配合梯度累积。num_workers按 CPU 核数设置,Windows 下建议设为 0 避免多进程报错。

3.2 模型选择与训练循环:从 ResNet 到 EfficientNet 的取舍

11 类分类任务,数据量 3 万张,不算小。ResNet-50 是稳妥的基线,但如果你想要更高精度且显存有限,EfficientNet-B0 或 B1 更划算。我一般先用 ResNet-18 跑一个快速基线,确认数据管道没问题,再换 EfficientNet-B3 做正式训练。优化器用 AdamW,学习率 3e-4,权重衰减 1e-4,余弦退火调度。损失函数用带标签平滑的交叉熵,平滑系数 0.1,能缓解标注噪声。

下面是一个最小训练循环:

import torch.nn as nn import torch.optim as optim from torchvision.models import efficientnet_b0, EfficientNet_B0_Weights device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = efficientnet_b0(weights=EfficientNet_B0_Weights.IMAGENET1K_V1) model.classifier[1] = nn.Linear(model.classifier[1].in_features, 11) model = model.to(device) criterion = nn.CrossEntropyLoss(label_smoothing=0.1) optimizer = optim.AdamW(model.parameters(), lr=3e-4, weight_decay=1e-4) scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30) for epoch in range(30): model.train() for imgs, labels in train_loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() scheduler.step() # 每个 epoch 后在验证集上算准确率 model.eval() correct, total = 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels = imgs.to(device), labels.to(device) outputs = model(imgs) preds = outputs.argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) print(f"Epoch {epoch+1}, Val Acc: {correct/total:.4f}")

逻辑说明:加载 ImageNet 预训练的 EfficientNet-B0,把最后一层全连接改成 11 类输出。label_smoothing=0.1防止模型对标注噪声过拟合。学习率 3e-4 配合余弦退火,30 个 epoch 足够收敛。参数上,如果你用 ResNet-50,学习率可以降到 1e-4;如果 batch size 调到 64,学习率可以线性放大到 6e-4。验证时只做argmax算准确率,但类别不均衡时准确率会骗人,建议同时输出每类的 precision 和 recall。

注意:如果验证集准确率在 5 个 epoch 内不升反降,先检查学习率是不是太大,或者数据增强是不是太狠。把ColorJitter关掉再跑一次,如果准确率立刻上升,说明增强把病斑特征破坏了。

4. 避坑:三万张西红柿病害数据训练时最容易翻车的 5 个点

4.1 现象:训练准确率 99%,验证准确率 60% 不到

原因:同一片叶子的多张照片被分到了训练集和验证集,模型记住了叶片背景而不是病斑特征。西红柿病害数据集里,同一株植物可能拍了几十张,如果按图片随机切分,必然泄漏。

解决:按「叶片 ID」或「拍摄时间+位置」分组切分。如果数据集没有提供分组信息,用图片文件名里的前缀做分组,比如leaf001_1.jpg、leaf001_2.jpg归到同一组,整组进训练集或验证集。用GroupShuffleSplit代替train_test_split。

4.2 现象:模型把健康叶片全预测成某一种病害

原因:健康叶片样本太少,或者健康叶片的特征和某种病害早期症状太像。32,000 张里健康叶片可能只有 1,000 张,模型为了降低整体损失,干脆把不确定的样本都归到多数类。

解决:对健康叶片做过采样,或者用WeightedRandomSampler给少数类更高采样权重。损失函数改用 Focal Loss,让模型关注难分类样本。同时检查健康叶片的标注是否混入了早期病叶。

4.3 现象:训练 loss 震荡剧烈,几个 epoch 后突然变成 NaN

原因:学习率太大,或者数据里有损坏图片导致梯度爆炸。西红柿病害数据集可能包含手机拍摄的 HEIC 格式转 JPG 的图片,色彩空间异常。

解决:先把学习率降到 1e-4 跑 3 个 epoch,如果 loss 稳定再逐步调高。在 Dataset 的__getitem__里加 try-except,跳过无法打开的图片,并打印路径。用torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)裁剪梯度。

4.4 现象:验证集准确率很高,但测试集一塌糊涂

原因:验证集和测试集的拍摄条件不同。比如验证集是实验室均匀光照,测试集是田间自然光。数据集的 train/val/test 划分可能按时间切分,导致分布偏移。

解决:先确认三个子集的来源是否一致。如果测试集是独立采集的,需要在训练时加入更强的光照和背景增强,比如RandomApply([transforms.GaussianBlur(3)])和随机调整 gamma。同时用测试集做一次零样本评估,看模型在无微调情况下的表现。

4.5 现象:某类病害的 recall 始终为 0

原因:该类样本在训练集中少于 200 张,模型根本没学到特征。或者类别名映射时,该类被合并到了其他类。

解决:统计每类数量,少于 500 张的类别要么补充数据,要么在评估时单独看。检查class_to_idx映射,确认没有两个类别指向同一个编号。如果数据集本身不均衡,考虑用分层采样或类别权重。

5. 进阶:用混淆矩阵和 Grad-CAM 验证模型到底看的是不是病斑

训练完模型,准确率只是一个数字。你真正需要知道的是:模型有没有在看病斑。我一般会做两件事:画混淆矩阵,找出最容易混的类别对;用 Grad-CAM 热力图看模型关注区域。如果热力图高亮的是叶片边缘或背景,说明模型走了捷径。

混淆矩阵用sklearn.metrics.confusion_matrix和seaborn.heatmap就能画。重点看哪两类互相错分最多。比如早疫病和靶斑病在早期症状上都是褐色斑点,模型容易混。这时候你需要回到数据层面,看这两类的标注边界是否清晰,或者考虑加一个二阶段分类器专门区分这两类。

Grad-CAM 的实现用pytorch-grad-cam库最省事:

from pytorch_grad_cam import GradCAM from pytorch_grad_cam.utils.image import show_cam_on_image import numpy as np target_layers = [model.features[-1]] cam = GradCAM(model=model, target_layers=target_layers) # 取一张验证集图片 img_tensor, label = val_ds[0] input_tensor = img_tensor.unsqueeze(0).to(device) grayscale_cam = cam(input_tensor=input_tensor, targets=None) grayscale_cam = grayscale_cam[0, :] # 叠加到原图 rgb_img = img_tensor.permute(1, 2, 0).cpu().numpy() rgb_img = (rgb_img - rgb_img.min()) / (rgb_img.max() - rgb_img.min()) visualization = show_cam_on_image(rgb_img, grayscale_cam, use_rgb=True)

逻辑说明:target_layers选 EfficientNet 最后一个卷积块,targets=None表示用预测类别作为目标。生成的grayscale_cam是 0 到 1 的热力图,叠加到原图上就能看到模型关注区域。参数上,如果热力图太分散,把target_layers换成更浅的层;如果太局部,换成更深的层。我一般会抽 20 张验证集图片批量生成热力图,人工看一遍。如果超过三成图片的高亮区域不在病斑上,这个模型就不能上线。

还有一个实用技巧:把验证集中模型预测错但置信度很高的样本单独拉出来看。这些样本往往是标注错误或者类别定义模糊。我习惯用pandas把image_path、true_label、pred_label、confidence存成 CSV,按置信度降序排,人工复核前 50 张。十有八九能找出十几张标错的。把这些样本修正后重新训练,验证准确率通常能涨 2 到 3 个百分点。

最后说个血泪教训:别在训练中途频繁改数据增强。我曾经在一个西红柿病害项目里,看到验证准确率波动就调增强参数,结果模型始终不稳定。后来固定增强策略,只调学习率和 batch size,反而顺利收敛。数据管道一旦确定,就把它当成黑匣子,别老去动它。希望帮到你。

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

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

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

立即咨询