简介这份番茄叶子缺陷图像分类数据集面向从事图像分类、农业病害识别与深度学习实践的开发者与研究者提供约3000张已标注的番茄叶片图像覆盖细菌斑点、早疫病、健康、Septoria_spot等7个类别可直接作为分类网络输入省去自行采集与清洗的环节。资源包共2000个文件以1998张jpg图像为主体另含1个py可视化脚本与1个json类别说明文件压缩包约161.45MB并已按训练集、测试集划分各类别图片分目录存放便于直接构建数据加载流程。运行包内show脚本即可快速预览样本分布与图像质量json文件则给出具体类别映射方便核对标签。目前已有63人学习下载适合用于分类模型训练、迁移学习对比实验及数据增强策略验证也能为分割网络改进提供配套素材。1. 番茄叶子缺陷图像分类数据集从拿到手到跑出第一个可用模型番茄叶片病害识别是农业视觉里最典型的细粒度分类任务之一。你手里如果有一份约 3,000 张、已经标注好的番茄叶子缺陷图像分类数据集第一反应往往是「数据量不大直接上模型就行」。但真正动手就会发现3,000 张在图像分类里属于小样本区间类别不均衡、背景干扰、叶片重叠、光照差异这些问题会集中爆发。这份数据集的价值不在于「有多少张」而在于它把标注这件事替你做了——省掉的是最耗时的环节留下的是模型选型、增强策略和验证方法这些真正决定成败的部分。这篇文章面向两类人一类是刚拿到数据集、想快速跑通 baseline 的新手另一类是已经做过几个分类项目、想搞清楚小样本农业图像到底该怎么调参的熟手。下面从数据检查、模型选型、训练配置到避坑一步步拆开讲。2. 拿到数据集先别急着训练三件事决定后面顺不顺2.1 目录结构与类别分布的自检脚本标注好的分类数据集通常按类别分文件夹存放但不同来源的打包方式差异很大。有的把训练集和验证集已经切好有的全部混在一起有的类别名带空格或中文。第一步不是写模型而是写一个自检脚本把目录结构、类别数量、每类样本数、图片尺寸分布全部打印出来。import os from pathlib import Path from collections import Counter from PIL import Image DATA_ROOT Path(./tomato_leaf_defect) # 改成你的实际路径 # 1. 遍历所有类别文件夹 class_dirs sorted([d for d in DATA_ROOT.iterdir() if d.is_dir()]) print(f类别数量: {len(class_dirs)}) # 2. 统计每类图片数和尺寸 size_counter Counter() for cls_dir in class_dirs: imgs list(cls_dir.glob(*.jpg)) list(cls_dir.glob(*.png)) print(f {cls_dir.name}: {len(imgs)} 张) for img_path in imgs[:20]: # 每类抽样20张看尺寸 with Image.open(img_path) as im: size_counter[im.size] 1 print(\n尺寸分布(抽样):) for size, cnt in size_counter.most_common(10): print(f {size}: {cnt})这段脚本做三件事确认类别文件夹是否被正确识别、统计每类样本量、抽样检查图片尺寸是否统一。参数上DATA_ROOT指向数据集根目录imgs[:20]控制抽样数量数据量大时可以调小。如果发现某类只有几十张、另一类有几百张那类别不均衡就是后面必须处理的问题。如果尺寸五花八门说明预处理阶段必须统一 resize不能直接喂给模型。2.2 类别不均衡与脏数据的快速判断3,000 张听起来够用但如果分成 8 到 10 个类别平均每类只有 300 到 375 张实际分布往往更极端。常见情况是健康叶片样本远多于病害样本因为健康叶子好拍、病害叶子需要特定条件。这种不均衡会让模型倾向于预测多数类准确率看起来不错但少数类召回率极低。判断方法很简单把每类样本数画成柱状图或者直接看自检脚本的输出。如果最大类与最小类的比例超过 5:1就需要在训练时用加权损失或者重采样。另一个容易被忽略的是脏数据——标注错误的图片、模糊到无法辨认的图片、混入的其他植物叶片。快速筛查可以用一个已经预训练好的模型跑一遍推理把预测置信度极低的样本挑出来人工复查。这一步不需要标注工具直接用 torchvision 加载一个 ImageNet 预训练模型即可。import torch from torchvision import models, transforms from PIL import Image # 加载预训练模型做粗筛 model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) model.eval() preprocess 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]), ]) def check_image(img_path): img Image.open(img_path).convert(RGB) tensor preprocess(img).unsqueeze(0) with torch.no_grad(): output model(tensor) prob torch.softmax(output, dim1).max().item() return prob # 置信度低于0.3的图片大概率是脏数据或异常样本 low_conf [] for img_path in DATA_ROOT.rglob(*.jpg): p check_image(img_path) if p 0.3: low_conf.append((str(img_path), round(p, 3))) print(f低置信度样本数: {len(low_conf)}) for path, p in low_conf[:20]: print(f {path} - {p})这里用 ImageNet 预训练模型做粗筛逻辑是正常番茄叶片图片在 ImageNet 模型下也会有相对明确的预测而模糊、纯色、非植物图片的预测分布会非常分散最大置信度偏低。阈值 0.3 是经验值可以按实际情况调整。挑出来的样本需要人工看一眼确认是标注错误还是图片本身有问题。2.3 训练集/验证集切分的两个硬约束切分不是随机分就完事。农业图像有一个特点同一片叶子可能被拍了多张或者同一株植物的不同叶片在视觉上高度相似。如果随机切分训练集和验证集里会出现几乎一样的图片验证准确率虚高实际部署时翻车。硬约束有两个第一按拍摄批次或植株编号分组切分同一组只出现在训练集或验证集第二验证集比例不低于 15%且每类都要有样本。如果数据集没有提供分组信息一个折中做法是对图片做感知哈希去重把相似度极高的图片分到同一组再切分。下面是一个基于 imagehash 的去重分组示例import imagehash from PIL import Image from collections import defaultdict # 计算每张图的感知哈希相似图片归为一组 hash_groups defaultdict(list) for img_path in DATA_ROOT.rglob(*.jpg): with Image.open(img_path) as im: h str(imagehash.phash(im)) hash_groups[h].append(str(img_path)) # 按组切分保证同组图片不跨集 groups list(hash_groups.values()) print(f去重后组数: {len(groups)}) print(f原始图片数: {sum(len(g) for g in groups)})感知哈希会把视觉上几乎一样的图片映射到相同或相近的哈希值。分组后按组切分能有效避免数据泄漏。参数上imagehash.phash的哈希长度默认 8对轻微裁剪和亮度变化不敏感适合这种场景。3. 模型选型小样本农业图像该用 CNN 还是 Transformer3.1 从 ResNet 到 ConvNeXt 的精度与成本对比3,000 张图片属于小样本模型选型的核心矛盾是容量太小的模型欠拟合容量太大的模型过拟合。传统做法是用 ResNet50 加预训练权重在农业图像上微调。但最近两年的趋势是 ConvNeXt 和 Swin Transformer 这类新架构在细粒度分类上表现更好代价是显存和训练时间增加。实际选型时我一般会跑一个快速对比用相同的数据增强和训练轮数分别试 ResNet50、EfficientNet-B0、ConvNeXt-Tiny 三个模型看验证集准确率和单轮训练时间。下面是一个简化的对比脚本框架import torch import torch.nn as nn from torchvision import models import time def build_model(name, num_classes): if name resnet50: model models.resnet50(weightsmodels.ResNet50_Weights.DEFAULT) model.fc nn.Linear(model.fc.in_features, num_classes) elif name efficientnet_b0: model models.efficientnet_b0(weightsmodels.EfficientNet_B0_Weights.DEFAULT) model.classifier[1] nn.Linear(model.classifier[1].in_features, num_classes) elif name convnext_tiny: model models.convnext_tiny(weightsmodels.ConvNeXt_Tiny_Weights.DEFAULT) model.classifier[2] nn.Linear(model.classifier[2].in_features, num_classes) return model # 简单测一轮前向反向的时间 for name in [resnet50, efficientnet_b0, convnext_tiny]: model build_model(name, num_classes8).cuda() dummy torch.randn(16, 3, 224, 224).cuda() labels torch.randint(0, 8, (16,)).cuda() criterion nn.CrossEntropyLoss() optimizer torch.optim.AdamW(model.parameters(), lr1e-4) torch.cuda.synchronize() start time.time() for _ in range(5): optimizer.zero_grad() out model(dummy) loss criterion(out, labels) loss.backward() optimizer.step() torch.cuda.synchronize() print(f{name}: 5步耗时 {time.time()-start:.2f}s)这段代码只测训练速度不测精度。精度需要完整跑几个 epoch 才能看出来。参数上batch size 设为 16 是为了在单卡上快速对比实际训练时可以根据显存调整。lr1e-4是微调的常用起点ConvNeXt 这类模型对学习率更敏感可能需要更低。从经验看3,000 张图片下 ResNet50 和 EfficientNet-B0 的最终精度差距通常在 1 到 3 个百分点ConvNeXt-Tiny 可能再高 1 到 2 个点但训练时间翻倍。如果部署环境是边缘设备EfficientNet-B0 的性价比最高如果追求精度且算力充足ConvNeXt-Tiny 值得试。3.2 预训练权重怎么选ImageNet 还是农业领域预训练ImageNet 预训练权重是默认选择但农业图像和 ImageNet 的分布差异不小。如果能在公开的农业数据集上做二次预训练效果会更好。常见做法是先用 PlantVillage 或类似数据集训练一个基础模型再在番茄叶子缺陷数据集上微调。如果没有领域预训练权重ImageNet 权重加更强的数据增强也能凑合。微调策略上小样本场景不建议冻结太多层。我一般会解冻全部层但用较小的学习率1e-4 到 5e-5同时用余弦退火调度。如果发现过拟合再考虑冻结前面的层。下面是一个微调配置的示例from torch.optim.lr_scheduler import CosineAnnealingLR model build_model(convnext_tiny, num_classes8).cuda() criterion nn.CrossEntropyLoss(label_smoothing0.1) # 标签平滑缓解过拟合 optimizer torch.optim.AdamW(model.parameters(), lr5e-5, weight_decay0.05) scheduler CosineAnnealingLR(optimizer, T_max30) # 训练循环骨架 for epoch in range(30): model.train() for imgs, labels in train_loader: imgs, labels imgs.cuda(), labels.cuda() optimizer.zero_grad() out model(imgs) loss criterion(out, labels) loss.backward() optimizer.step() scheduler.step() # 每个epoch后在验证集上评估label_smoothing0.1是防止模型对训练标签过度自信在小样本上很有效。weight_decay0.05是 AdamW 的常用值比 SGD 时代的 1e-4 大很多。T_max30对应总 epoch 数余弦退火会让学习率从 5e-5 平滑降到接近 0。3.3 数据增强哪些增强对叶片图像真正有用通用增强里随机裁剪、水平翻转、颜色抖动是标配。但叶片图像有几个特殊点第一旋转不变性很重要因为叶片在自然场景中方向随机所以随机旋转 90 度甚至任意角度都合理第二颜色抖动要谨慎因为病害的颜色变化本身就是分类依据过度抖动会破坏信号第三CutMix 和 MixUp 在小样本上通常有正收益但会延长收敛时间。我一般会用的增强组合是RandomResizedCrop(224, scale(0.6, 1.0))、RandomHorizontalFlip、RandomVerticalFlip、RandomRotation(90)、ColorJitter(0.2, 0.2, 0.2, 0.05)。CutMix 在训练后期开启概率 0.5。下面是对应的 torchvision 配置from torchvision import transforms train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.6, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomVerticalFlip(), transforms.RandomRotation(90), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2, hue0.05), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_transform 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]), ])scale(0.6, 1.0)控制裁剪区域占原图的比例太小会丢失病害细节太大则增强效果弱。RandomRotation(90)是 90 度以内的任意角度旋转不是只转 90 度。hue0.05比默认的 0.5 小很多就是为了保护颜色信号。4. 训练配置与调参让 3,000 张图片发挥最大价值4.1 学习率、batch size 与优化器的组合逻辑小样本训练最怕的是学习率太大导致震荡或者太小导致欠拟合。经验规则是微调时学习率比从头训练小一个数量级AdamW 比 SGD 更适合小样本因为自适应学习率对参数更新更稳定。batch size 受显存限制但太小会让 BatchNorm 统计不稳定太大则泛化变差。3,000 张图片下batch size 32 或 64 是合理区间。如果显存不够可以用梯度累积模拟大 batch。下面是一个梯度累积的示例ACCUM_STEPS 4 # 模拟 batch_size 16 * 4 64 optimizer.zero_grad() for i, (imgs, labels) in enumerate(train_loader): imgs, labels imgs.cuda(), labels.cuda() out model(imgs) loss criterion(out, labels) / ACCUM_STEPS loss.backward() if (i 1) % ACCUM_STEPS 0: optimizer.step() optimizer.zero_grad()注意 loss 要除以累积步数否则梯度会放大。这种写法在单卡小显存上很实用代价是训练速度略慢。4.2 早停与模型保存别让过拟合吃掉你的时间小样本训练通常在 20 到 40 个 epoch 内收敛之后验证集准确率会下降。早停策略是监控验证集准确率如果连续 8 个 epoch 没有提升就停止保存验证集准确率最高的模型权重。下面是一个早停类的实现class EarlyStopping: def __init__(self, patience8, min_delta0.001): self.patience patience self.min_delta min_delta self.counter 0 self.best_acc 0.0 self.best_state None def step(self, val_acc, model): if val_acc self.best_acc self.min_delta: self.best_acc val_acc self.best_state {k: v.cpu().clone() for k, v in model.state_dict().items()} self.counter 0 else: self.counter 1 return self.counter self.patience # 使用方式 early_stop EarlyStopping(patience8) for epoch in range(50): train_one_epoch() val_acc evaluate() if early_stop.step(val_acc, model): print(f早停于 epoch {epoch}, 最佳验证准确率 {early_stop.best_acc:.4f}) break model.load_state_dict(early_stop.best_state)patience8是经验值数据量越小可以设得越小。min_delta0.001是提升阈值小于这个值不算提升。保存最佳权重而不是最后一个 epoch 的权重是小样本训练的基本纪律。4.3 类别不均衡的损失函数选择如果自检时发现类别比例超过 5:1交叉熵损失需要加权重。权重可以按类别样本数的倒数计算归一化后传给nn.CrossEntropyLoss的weight参数。另一种做法是用 Focal Loss它对难样本的关注更多但需要调两个超参数不如加权交叉熵直接。import numpy as np # 假设 class_counts 是每类样本数列表 class_counts [400, 350, 300, 250, 200, 150, 100, 50] weights 1.0 / np.array(class_counts, dtypenp.float32) weights weights / weights.sum() * len(weights) # 归一化 weights torch.tensor(weights).cuda() criterion nn.CrossEntropyLoss(weightweights, label_smoothing0.1)归一化让权重均值为 1避免整体损失尺度变化太大。label_smoothing和加权可以同时用不冲突。如果加权后少数类召回率仍然低可以考虑对少数类做过采样但要注意过采样会加剧过拟合需要配合更强的增强。5. 避坑与排查番茄叶片分类里最容易翻车的五个地方5.1 验证集准确率很高但实际部署一塌糊涂现象训练时验证集准确率 95% 以上但拿新拍的叶片图片测试准确率掉到 60% 以下。原因通常是数据泄漏或域偏移。数据泄漏来自同一叶片的多张图片被分到训练集和验证集模型记住了叶片纹理而不是病害特征。域偏移来自训练图片和实际拍摄条件差异大比如训练集是实验室均匀光照实际是田间复杂光照。解决方法是按植株或拍摄批次分组切分并在训练时加入更强的光照和背景增强。如果已经切分完才发现可以用感知哈希去重后重新切分。5.2 模型只学会预测多数类现象训练损失正常下降但验证集上少数类召回率接近 0混淆矩阵显示所有少数类都被预测成多数类。原因是类别不均衡加上交叉熵损失没有加权。解决方法是先确认类别分布然后加类别权重或改用 Focal Loss。另一个容易忽略的点是验证集也要按类别分层采样否则验证指标本身就有偏。5.3 图片尺寸不统一导致训练报错或精度下降现象DataLoader 报错说 tensor 尺寸不一致或者训练能跑但精度异常低。原因是数据集中混有不同分辨率的图片而 transform 里只做了 ToTensor 没有 Resize。解决方法是统一在 transform 里加 Resize 或 RandomResizedCrop确保输出尺寸一致。如果图片长宽比差异很大CenterCrop 会裁掉病害区域这时应该用 Resize 到固定尺寸而不是裁剪。5.4 学习率设太大导致 loss 震荡不收敛现象训练前几个 epoch loss 剧烈震荡甚至变成 NaN。原因是微调时学习率用了从头训练的量级比如 0.01 或 0.1。解决方法是把学习率降到 1e-4 到 5e-5并用 warmup 让前几个 epoch 学习率从很小线性增加到目标值。如果已经出现 NaN检查是否有除零或 log(0) 的操作同时降低学习率重跑。5.5 数据增强过度导致模型学不到有效特征现象训练准确率一直上不去验证准确率也低模型欠拟合。原因是增强太强比如颜色抖动幅度过大、随机裁剪比例太小导致病害特征被破坏。解决方法是逐步降低增强强度先只用翻转和轻微旋转跑一个 baseline确认模型能拟合后再逐步加增强。颜色相关的增强要特别谨慎因为病害分类很依赖颜色。6. 进阶技巧用混淆矩阵和 Grad-CAM 定位模型的真实短板训练完一个模型只是开始真正有价值的是知道它在哪里出错、为什么出错。混淆矩阵能告诉你哪些类别容易混Grad-CAM 能告诉你模型在看图片的哪个区域。这两个工具配合使用能帮你决定下一步是补数据、调增强还是换模型。先看混淆矩阵的生成。用 sklearn 的confusion_matrix和 seaborn 的热力图几行代码就能画出来import seaborn as sns import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix def plot_confusion(model, val_loader, class_names): model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in val_loader: imgs imgs.cuda() out model(imgs) preds out.argmax(dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) plt.figure(figsize(10, 8)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelsclass_names, yticklabelsclass_names) plt.xlabel(Predicted) plt.ylabel(True) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150) plt.show()看混淆矩阵时重点关注两件事对角线以外的数值集中在哪两个类别之间以及少数类的召回率。如果两个类别互相混淆严重说明它们的视觉特征太接近可能需要更细粒度的标注或者针对性的数据增强。如果少数类召回率低回到第 4 章检查类别权重。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]] # ConvNeXt 的最后一层 cam GradCAM(modelmodel, target_layerstarget_layers) # 取一张验证集图片 img_tensor, label next(iter(val_loader)) input_tensor img_tensor[0:1].cuda() grayscale_cam cam(input_tensorinput_tensor, targetsNone) grayscale_cam grayscale_cam[0, :] # 叠加到原图 rgb_img img_tensor[0].permute(1, 2, 0).numpy() rgb_img (rgb_img - rgb_img.min()) / (rgb_img.max() - rgb_img.min()) visualization show_cam_on_image(rgb_img, grayscale_cam, use_rgbTrue) plt.imshow(visualization) plt.axis(off) plt.savefig(gradcam.png, dpi150)target_layers的选择取决于模型结构ResNet 用layer4[-1]ConvNeXt 用features[-1]EfficientNet 用features[-1]。Grad-CAM 的热力图如果集中在叶片病斑区域说明模型学到了正确特征如果集中在背景或边缘说明需要检查数据增强是否引入了背景偏差。我自己的习惯是每训练完一个模型先看混淆矩阵再看 10 张 Grad-CAM 图。如果混淆矩阵显示某两类互相混就专门去翻这两类的图片看是不是标注标准不一致如果 Grad-CAM 显示模型在看背景就回去检查数据增强里的随机裁剪是不是裁得太狠或者数据集里是不是有大量背景相似的图片。这个流程走下来通常能在不增加数据的情况下把验证集准确率再提 2 到 5 个百分点。希望帮到你。本文还有配套的精品资源点击获取