ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

番茄叶子缺陷图像分类实战:3,000张已标注数据集从训练到部署

番茄叶子缺陷图像分类实战:3,000张已标注数据集从训练到部署 简介这份番茄叶子缺陷图像分类数据集面向从事图像分类、农业病害识别与深度学习实践的开发者与研究者提供可直接投入训练与验证的标注数据帮助解决番茄叶片病害分类任务中数据获取与预处理成本高的问题。资源包共约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(p0.5), # 叶片左右翻转不改变类别 transforms.RandomRotation(15), # 小角度旋转模拟拍摄姿态 transforms.ColorJitter(brightness0.2, contrast0.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, transformtrain_tf) val_ds datasets.ImageFolder(tomato_leaf_defect/val, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) print(类别到索引:, train_ds.class_to_idx)这里几个参数值得说清楚。Resize((256, 256))而不是 224是因为后续可能用 256 输入再随机裁剪到 224给增强留空间如果直接 224 也行但别混用。RandomRotation(15)控制在 15 度以内番茄叶片方向性不强但病斑分布有位置特征转太多会引入噪声。ColorJitter的 brightness 和 contrast 各 0.2模拟棚内光照变化再大就可能把浅色病斑洗掉。Normalize用 ImageNet 均值方差这是迁移学习的常规做法如果你从零训练可以改成数据集自身统计值。num_workers4和pin_memoryTrue是吞吐优化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, dtypetorch.float32) criterion nn.CrossEntropyLoss(weightclass_weights) print(类别权重:, dict(zip(train_ds.classes, [round(w, 3) for w in weights])))权重公式是总数 / (类别数 × 该类样本数)少数类权重大多数类权重小。CrossEntropyLoss的weight参数会在计算 loss 时按类别加权。注意权重张量要放到和模型相同的设备上训练循环里别忘了class_weights.to(device)。如果加权后验证集少数类召回仍然很低再考虑配合 WeightedRandomSampler 做温和重采样但两者叠加要小心可能让训练不稳定。3. 从 ResNet 到 ViT3,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, archresnet50, pretrainedTrue): if arch resnet50: model models.resnet50(weightsmodels.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(weightsmodels.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, archresnet50) print(model.fc if hasattr(model, fc) else model.classifier)这段代码把分类头替换成 Dropout Linear。Dropout 0.3 是小数据集的常规正则别设太高否则欠拟合。pretrainedTrue加载 ImageNet 权重这是迁移学习的关键。注意weights参数在新版 torchvision 里替代了旧的pretrained布尔值如果你环境版本较老用pretrainedTrue也行但要确认 API 兼容。选 ResNet50 还是 EfficientNet-B0可以两个都跑一轮对比验证集准确率和 F1。3,000 张规模下EfficientNet-B0 往往在更少参数下达到接近甚至更好的效果但 ResNet50 的预训练特征更“通用”在病斑这种纹理特征上有时更稳。别只看准确率类别不均衡时看宏平均 F1。3.2 冻结策略与分层学习率别让预训练权重被冲垮迁移学习不是简单加载权重就完事。如果所有层都用同一个学习率预训练学到的底层特征会被快速覆盖小数据集上表现为训练 loss 下降但验证 loss 上升。常见做法是分层设置学习率底层小学习率分类头大学习率。def set_layer_lr(model, base_lr1e-4, head_lr1e-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_decay1e-4) return optimizer optimizer set_layer_lr(model, base_lr1e-4, head_lr1e-3)这里用 AdamWweight_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, optimizerNone): 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, averagemacro) 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(fEpoch {epoch1: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_namestrain_ds.classes, digits4)) plt.figure(figsize(8, 6)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelstrain_ds.classes, yticklabelstrain_ds.classes) plt.xlabel(预测) plt.ylabel(真实) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150)classification_report给出每个类别的精确率、召回率和 F1confusion_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, dim1)[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, encodingutf-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.4top2 有 0.35说明模型不确定这种样本值得人工复核。label_map.json就是训练时保存的class_to_idx保证索引一致。推理预处理必须和验证集一致别用训练增强那套。一个习惯每次训练完先看混淆矩阵再看 top3 推理结果最后才看准确率。准确率会骗人混淆矩阵不会。3,000 张的数据集不大但把验证做扎实比盲目换更大的模型有用得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表