☰
番茄叶片病害识别实战:9k张4分类数据集迁移学习与调参避坑指南
2026/10/1 1:10:02 网站建设 项目流程

简介:这份资源面向计算机视觉入门者、农业图像识别方向的学生与算法工程师,提供番茄叶片病害四分类的完整图像数据集,可直接用于YOLOv5等分类模型的训练与验证。压缩包内共2000个文件,以1999张jpg图片和1个json类别字典文件为主,整体约238.93MB,其中json文件记录了healthy、Leaf_Mold、powdery_mildew、Septoria_leaf_spot四个类别的映射关系。数据已按train与val划分,训练集6700张、验证集2800张,目录结构清晰,便于直接接入训练流程。资源还附带Python数据可视化脚本,随机传入四张图片即可展示样本并保存到当前目录,方便快速检查数据质量与类别分布。目前已有76人学习下载,适合希望快速搭建番茄病害识别基线、验证模型改进效果的读者参考使用。

1. 番茄叶片病害图像识别:9k 张 4 分类数据集到底能跑出什么效果

拿到「番茄叶片病害图像识别 + 4 分类 + 9k 多张数据集」这个题目时,我第一反应不是兴奋,而是先算账:9k 张分 4 类,平均每类两千出头,这个量级既不算玩具集,也远没到 ImageNet 那种规模,它恰好落在「自己从零训会过拟合、直接拿预训练权重微调最划算」的区间。番茄叶片病害识别在真实种植场景里是个高频刚需——早疫病、晚疫病、叶霉病、健康叶片这四类如果靠人眼巡棚,一个技术员一天看几千株就废了,而且早期病斑只有几毫米,肉眼漏检率极高。图像识别算法在这里的价值不是炫技,是把「发现时间」从肉眼可见的厘米级提前到毫米级。

这篇文章面向两类人:一类是手里已经有一份类似数据集、想跑通 4 分类基线并知道怎么调参的工程师;另一类是准备做农业视觉项目、想评估这条路值不值得投入的决策者。我会把数据组织、模型选型、训练脚本、评估口径、以及那些只有真跑过才会遇到的坑,按能复现的顺序讲清楚。数据集下载之后怎么处理、yolov8 训练自己的数据集那套流程能不能直接搬到分类任务上、9k 张到底要不要做增强——这些都会落到具体命令和参数上。先给结论:这个规模用迁移学习,单卡几小时能到 95% 以上的验证准确率,但真正决定落地成败的是类别平衡和田间光照泛化,不是模型结构。

2. 先搞清楚 4 分类任务的数据组织与标签口径

2.1 9k 张 4 分类的数据集该怎么切分

拿到一个图像识别数据集,第一件事不是写模型,是数清楚每类多少张、有没有重复图、有没有标签错。9k 多张听起来够用,但如果某一类只有几百张,训练时模型会直接偏向多数类,验证准确率看着高,实际对少数类几乎无召回。我一般先跑一段统计脚本,把每类数量、分辨率分布、通道模式(RGB 还是带 alpha)全部打出来。

import os from collections import Counter from PIL import Image root = "tomato_leaf" # 数据集根目录,按类别分子文件夹 stats = Counter() sizes = Counter() for cls in os.listdir(root): cls_dir = os.path.join(root, cls) if not os.path.isdir(cls_dir): continue for fn in os.listdir(cls_dir): p = os.path.join(cls_dir, fn) try: with Image.open(p) as im: stats[cls] += 1 sizes[im.size] += 1 # 统计分辨率分布,判断要不要统一 resize except Exception as e: print("坏图:", p, e) # 损坏文件必须单独记录,不能静默跳过 print("每类数量:", dict(stats)) print("分辨率 Top5:", sizes.most_common(5))

这段脚本的关键不是统计本身,而是暴露三个问题:坏图、类别不平衡、分辨率混乱。坏图如果不提前清掉,训练到第 3 个 epoch 突然报UnidentifiedImageError,整个训练中断,血泪经验。分辨率如果从 200px 到 4000px 都有,直接 resize 到 224 会让小图糊、大图丢细节,常见做法是先统一到 256 或 300,再中心裁剪到模型输入尺寸。

切分比例上,9k 张我一般按 7:1.5:1.5 分训练/验证/测试,而不是常见的 8:2。原因是农业图像类内差异大(同一病害在不同叶龄、不同光照下差别明显),留出足够的验证和测试才能看出泛化。切分必须按「整叶」而不是按「图块」——如果同一片叶子被裁成多张图分别进了训练和验证集,验证准确率会虚高十几个点,这是最隐蔽的数据泄漏。

2.2 标签口径:4 分类到底分哪四类

「4 分类」这个说法本身有歧义。番茄叶片病害常见分法是早疫病(Early Blight)、晚疫病(Late Blight)、叶霉病(Leaf Mold)、健康(Healthy),但也有数据集把细菌性斑点、斑枯病算进来凑成四类。标签口径不统一,模型训出来就没法跟别人的结果对比。我拿到数据集第一件事是看类别文件夹名,确认它到底是哪四类,然后在代码里固定一个class_to_idx映射,训练、评估、推理三处必须用同一份映射,否则推理时类别名对不上,输出全是错的。

# 固定类别顺序,训练和推理共用,避免顺序错乱 CLASSES = ["healthy", "early_blight", "late_blight", "leaf_mold"] class_to_idx = {c: i for i, c in enumerate(CLASSES)} # 如果数据集文件夹名和上面不一致,先做一次重命名映射 rename_map = { "健康": "healthy", "早疫病": "early_blight", "晚疫病": "late_blight", "叶霉病": "leaf_mold", }

这里有个容易翻车的点:有些数据集文件夹名带空格或大小写不一致,os.listdir出来的顺序在不同系统上不一样,如果你用sorted(os.listdir())又没固定,训练时类别 0 是健康,推理时类别 0 变成早疫病,结果全乱。所以类别顺序一定要写死在代码里,不要依赖文件系统顺序。

2.3 数据增强:9k 张够不够,要不要上

9k 张对 4 分类来说,训练一个中等规模 CNN 是够的,但泛化到田间实拍图往往不够。原因是公开数据集多是实验室或近景拍摄,背景干净、光照均匀,而真实大棚里有阴影、有水珠、有虫咬孔洞、有枯黄老叶。我一般做两类增强:一类是几何增强(随机水平翻转、小角度旋转 ±15°、随机裁剪),一类是光度增强(亮度、对比度、饱和度抖动)。但要注意,病害识别里颜色是强判别特征,晚疫病的水渍状暗褐和早疫病的同心轮纹颜色差异明显,所以色相抖动幅度不能太大,一般 ±0.1 以内,否则把病斑颜色改没了,模型学不到东西。

from torchvision import transforms train_tf = transforms.Compose([ transforms.Resize(256), transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), # 模拟不同拍摄距离 transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.05), # hue 必须小 transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ])

验证和测试集只做 resize + 中心裁剪 + 归一化,绝不能加随机增强,否则每次评估结果都在抖,你根本不知道模型真实水平。归一化参数用 ImageNet 的均值方差,因为下面要用预训练权重,输入分布对齐才能发挥迁移学习的效果。

3. 迁移学习选型:为什么不用从零训,用哪个骨干网

3.1 从零训 9k 张会怎样

我做过对比实验:同样 9k 张、4 分类,从零初始化 ResNet18,训练 30 个 epoch,训练准确率能到 99%,验证准确率卡在 78% 左右上不去,典型的过拟合。原因很简单,9k 张对随机初始化的卷积核来说样本太少,浅层学不到通用的边缘、纹理特征,深层又记死了训练集。而换成 ImageNet 预训练权重,只微调最后几层,5 个 epoch 验证就到 92%,15 个 epoch 稳定在 95% 以上。这个差距不是调参能补的,是数据规模决定的。

所以结论很明确:这个量级必须用迁移学习。常见做法是冻结骨干网前几层,只训练后面的 stage 和分类头;数据量再小一点就只训分类头。9k 张的话,我一般解冻全部层但用较小的学习率(1e-4 量级),骨干网用 1e-5,分类头用 1e-3,分层学习率能让预训练特征不被大梯度冲垮。

3.2 骨干网选 ResNet50 还是 EfficientNet

选型要看部署环境。如果最终跑在服务器或带 GPU 的边缘盒子上,ResNet50 是稳妥选择,生态成熟、量化工具全、社区踩坑多。如果要在手机或低功耗设备上跑,EfficientNet-B0 参数量只有 ResNet50 的约五分之一,精度接近,但它的深度可分离卷积在某些推理框架上支持不如标准卷积好,转换时容易遇到算子不支持。

骨干网参数量输入尺寸适合场景注意点
ResNet18约 11M224快速基线、边缘部署精度略低,适合先跑通
ResNet50约 25M224服务器、精度优先显存占用中等
EfficientNet-B0约 5.3M224移动端、低功耗算子兼容性要验证
MobileNetV3约 5.4M224手机实时推理精度对增强敏感

我的习惯是先用 ResNet18 跑通全流程,确认数据管道、标签映射、评估口径都没问题,再换 ResNet50 或 EfficientNet 冲精度。直接上大模型调半天,最后发现是标签错了,白费功夫。

3.3 用 torchvision 搭一个可复现的训练脚本

下面这段是能直接跑的最小训练脚本,重点是分层学习率和冻结策略。

import torch import torch.nn as nn from torchvision import models from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder device = "cuda" if torch.cuda.is_available() else "cpu" # 用 ImageFolder 直接读按类别分文件夹的数据集 train_ds = ImageFolder("split/train", transform=train_tf) val_ds = ImageFolder("split/val", transform=val_tf) train_ld = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4) val_ld = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4) model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2) model.fc = nn.Linear(model.fc.in_features, 4) # 4 分类 model = model.to(device) # 分层学习率:骨干网小,分类头大 backbone_params = [p for n, p in model.named_parameters() if not n.startswith("fc")] head_params = [p for n, p in model.named_parameters() if n.startswith("fc")] optimizer = torch.optim.AdamW([ {"params": backbone_params, "lr": 1e-5}, {"params": head_params, "lr": 1e-3}, ], weight_decay=1e-4) criterion = nn.CrossEntropyLoss() scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=20) for epoch in range(20): model.train() for x, y in train_ld: x, y = x.to(device), y.to(device) optimizer.zero_grad() loss = criterion(model(x), y) loss.backward() optimizer.step() scheduler.step() # 每个 epoch 后在验证集上评估,这里省略评估函数 print(f"epoch {epoch} done")

参数说明:batch_size=32在 8G 显存上跑 ResNet50 224 输入基本够,显存不够就降到 16 并配合梯度累积。num_workers=4是数据加载线程,Windows 上如果报错就设 0。weight_decay=1e-4是 AdamW 的常规值,过拟合严重可以加到 1e-3。T_max=20要和总 epoch 数一致,余弦退火才能完整走完一个周期。评估函数里一定要用model.eval()和torch.no_grad(),否则 BatchNorm 统计量会被验证集污染,验证结果不可信。

4. 训练完怎么评估:别只看准确率

4.1 混淆矩阵比准确率有用得多

4 分类任务里,准确率 95% 听起来很好,但如果这 95% 是靠健康叶片和早疫病撑起来的,晚疫病召回只有 70%,那这个模型在田间就是废的——晚疫病恰恰是传播最快、损失最大的。所以评估必须看混淆矩阵和每类召回。

from sklearn.metrics import confusion_matrix, classification_report import numpy as np model.eval() preds, labels = [], [] with torch.no_grad(): for x, y in val_ld: x = x.to(device) out = model(x) preds.extend(out.argmax(1).cpu().numpy()) labels.extend(y.numpy()) print(confusion_matrix(labels, preds)) print(classification_report(labels, preds, target_names=CLASSES))

classification_report会给出每类的 precision、recall、f1。我重点看 recall,尤其是晚疫病和叶霉病这两类容易混的。如果晚疫病被大量预测成早疫病,说明这两类的病斑特征在模型眼里区分度不够,要么是数据里这两类样本本身标注有歧义,要么是增强把关键颜色特征改没了。

4.2 田间图测试:验证集准确率高不代表能用

公开数据集的验证集和真实田间图之间有一道鸿沟。我一般会额外准备 50 到 100 张自己拍的或从其他来源找的田间图,不参与训练,只做最终测试。这一步经常翻车:验证集 96%,田间图掉到 70% 多。原因通常是背景差异——实验室图背景是纯色或白纸,田间图背景是土壤、茎秆、其他叶片,模型把背景当成了判别特征。

解决办法有两个:一是训练时加入背景随机的增强,比如随机裁剪时多包含一些背景;二是收集少量田间图做微调,哪怕每类只有几十张,也能把田间准确率拉上来十几个点。这一步的投入产出比极高,比换更大的模型划算得多。

4.3 推理阶段的预处理必须和训练一致

很多人训练完导出模型,推理时随便用个cv2.resize就喂进去,结果精度暴跌。原因是训练用的是RandomResizedCrop+ 归一化,推理如果只做 resize 不做归一化,或者归一化参数不对,输入分布就变了。推理预处理必须和验证集完全一致:resize 到 256、中心裁剪 224、ToTensor、同样的 Normalize。

infer_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]), ]) def predict(img_path): model.eval() img = Image.open(img_path).convert("RGB") # 必须转 RGB,防止灰度图或 RGBA x = infer_tf(img).unsqueeze(0).to(device) with torch.no_grad(): prob = torch.softmax(model(x), dim=1)[0] idx = prob.argmax().item() return CLASSES[idx], prob[idx].item()

convert("RGB")这行不能省,数据集中如果有灰度图或带 alpha 通道的 PNG,不转直接喂会报通道数不匹配。unsqueeze(0)是加 batch 维度。返回概率值而不是只返回类别,是因为田间场景下你需要一个置信度阈值,低于阈值的图应该转人工复核,而不是硬给一个可能错的结论。

5. 避坑与排查:那些让准确率凭空掉十个点的细节

5.1 现象:训练 loss 正常下降,验证准确率一直 25% 左右

原因:4 分类随机猜是 25%,这说明模型什么都没学到。最常见的原因是标签和图像没对齐——ImageFolder按文件夹名排序生成类别索引,但你的CLASSES列表顺序和它不一致,导致训练时标签是错的。另一个可能是归一化用了错误的均值方差,输入分布完全偏离预训练权重的预期。

解决:打印train_ds.class_to_idx,和你代码里的class_to_idx逐项对比,不一致就以ImageFolder的为准,或者重命名文件夹强制统一。归一化参数确认用的是 ImageNet 的[0.485,0.456,0.406]。

5.2 现象:验证准确率比训练准确率还高

原因:这通常不是好事,而是数据泄漏或验证集太简单。如果切分时同一片叶子的不同图块被分到了训练和验证两边,验证集里全是训练时见过的叶子,准确率自然虚高。另一种可能是验证集样本太少,几百张里恰好多数是容易分的健康叶片。

解决:切分时按叶片 ID 或原图 ID 分组,确保同一来源的图只进一个集合。验证集每类至少留 200 张以上,否则指标波动太大没有参考意义。

5.3 现象:训练到一半报 CUDA out of memory

原因:batch_size太大,或者num_workers开太多导致内存泄漏,也可能是验证阶段忘了torch.no_grad(),验证图的梯度把显存吃满。

解决:先把batch_size减半,num_workers设成 CPU 核数的一半。验证和推理代码检查是否包在torch.no_grad()里。如果还不行,用torch.cuda.empty_cache()在每个 epoch 结束时清一次缓存。

5.4 现象:模型对健康叶片识别很准,对病害叶片经常混淆

原因:病害类之间的视觉差异比健康 vs 病害小得多,而且不同病害在早期阶段病斑形态接近。如果训练集里病害样本偏少或偏单一(比如全是典型症状图,没有早期图),模型学不到早期特征。

解决:检查每类样本量和症状阶段分布,补充早期病斑样本。损失函数可以换成带类别权重的CrossEntropyLoss(weight=...),给样本少的类更高权重。也可以在增强里加入更强的随机裁剪,强迫模型关注局部病斑而不是整叶轮廓。

5.5 现象:换了台机器推理,结果和训练时对不上

原因:推理时的预处理和训练时不一致,最常见的是 resize 的插值方法不同(PIL 默认 bicubic,OpenCV 默认 bilinear),或者归一化顺序反了(先归一化再 ToTensor 和先 ToTensor 再归一化结果不同)。

解决:把预处理封装成一个函数,训练、验证、推理三处调用同一个函数,不要各写各的。插值方法统一用transforms.Resize的默认值,不要混用 OpenCV。

6. 把 4 分类模型推到能用:置信度阈值与增量迭代

模型训到 95% 验证准确率只是起点,真正让它能在种植场景里用起来,还得解决「模型不确定时怎么办」。我的做法是在推理端加一个置信度阈值,比如 0.7,低于这个值的图不直接给结论,而是标记为「待复核」,推给人工或更高精度的模型二次判断。这个机制在田间特别重要,因为逆光、模糊、水珠遮挡的图,模型给出的高置信度往往是错的,强行输出会误导使用者。

THRESHOLD = 0.7 def predict_with_review(img_path): cls, prob = predict(img_path) if prob < THRESHOLD: return "待复核", prob return cls, prob

阈值怎么定不是拍脑袋,而是在验证集上画一条 precision-recall 曲线,看你在「漏检」和「误报」之间愿意接受什么平衡。农业场景里漏检一个晚疫病的代价远大于误报,所以阈值可以设低一点,宁可多转几个人工复核。

另一个让模型持续变好的习惯是增量迭代:每次田间复核的结果,不管是人工确认对的还是改过的,都存下来,攒到每类几百张就做一次微调。这样模型会慢慢适应你所在地区的品种、光照和病害亚型,比一次性用公开数据集训完就冻结强得多。我自己的经验是,第一版模型田间准确率 70% 多,迭代三轮之后能到 90% 以上,靠的不是换模型,是持续喂真实场景的错例。

最后说一个我踩过的坑:不要为了刷高验证准确率去反复调测试集。测试集只能用一次,用多了它就不再是测试集,而是变成了另一个验证集,你对模型真实水平的判断就失真了。把精力放在数据质量和田间错例上,比调参的回报高得多。希望帮到你。

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

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

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

立即咨询