ARTICLE DETAIL

资讯详情

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

西瓜叶片病害识别数据集:5700张标注图像与CNN分类实战

西瓜叶片病害识别数据集:5700张标注图像与CNN分类实战 简介这份西瓜病害识别图像分类数据集面向从事图像分类实践的学生与算法工程师尤其适合需要真实农业场景数据来验证CNN模型、开展迁移学习或数据增强实验的读者。数据集共标注约5,700张西瓜叶片图像划分为花叶病毒、健康、炭疽病和霜霉病四类并已按训练集与测试集分别存放同类图片归入同一目录便于直接接入主流深度学习框架。压缩包内共2000个文件以1998张jpg图像为主体另附1个json标注文件和1个Python可视化脚本整体约156.94MB运行show脚本即可快速浏览样本分布与类别情况。目前已有59人学习下载。借助该数据集读者可完成从数据加载、类别平衡分析到分类网络训练与评估的完整流程并配合作者主页的CNN改进系列内容进一步对比不同网络结构在病害识别任务上的表现。1. 西瓜叶片病斑识别数据集5700 张已标注图像能直接跑通什么拿到一个农业图像分类数据集第一反应往往不是能不能用而是标注到底靠不靠谱。这份西瓜病害识别数据集给了一个相对省心的起点约 5700 张叶片图像已经按 4 个类别分好——花叶病毒、健康、炭疽病、霜霉病训练集和测试集各自独立存放同一类图片归在同一个目录下。类别定义不是靠猜资源里附了 json 文件类别名和索引的对应关系以它为准。它解决的是从零采集和标注这段最耗时的环节。做过田间图像的人都知道西瓜叶片病害的类间差异有时候非常细炭疽病的病斑是近圆形凹陷霜霉病是叶背灰紫色霉层花叶病毒则表现为黄绿相间的花斑健康叶片在强光下也可能出现反光斑。自己拍、自己标光是把这四类分清楚就要反复核对。这份数据把这一步前置了适合两类人一是想快速验证 CNN 分类网络改进效果的算法工程师二是做农业病害识别课程设计或毕设的学生。资源里还带了一个 show 脚本用来可视化数据集先看再训比盲跑强。2. 数据组织与标注格式先搞清楚目录结构和 json 怎么读2.1 目录布局与文件命名规律从项目正文给出的文件名能看出一些线索IMG_4110_zoomed.jpg、IMG_1980_shifted.jpg、IMG_4113_brightened.jpg、IMG_1984_shifted.jpg。命名里带了原始编号加后缀后缀有zoomed、shifted、brightened三种。这说明数据在采集后做过增强处理——缩放、平移、亮度调整属于常见的离线数据扩充手段。对分类任务来说这种增强能提升模型对尺度、位置和光照的鲁棒性但也要注意如果增强后的图同时出现在训练集和测试集就会造成数据泄漏测试精度会虚高。常见做法是训练集和测试集按原始图像编号划分同一张原图的不同增强版本只进其中一个集合。拿到数据后第一件事不是写模型而是核对这一点。可以用下面这段脚本统计每个类别下文件名前缀的重复情况import os from collections import defaultdict # 假设数据根目录结构为 data/train/类别名/ 和 data/test/类别名/ root data for split in [train, test]: split_dir os.path.join(root, split) if not os.path.isdir(split_dir): continue print(f {split} ) for cls in sorted(os.listdir(split_dir)): cls_dir os.path.join(split_dir, cls) if not os.path.isdir(cls_dir): continue # 提取文件名中 IMG_数字 部分作为原始编号 prefixes defaultdict(int) for fname in os.listdir(cls_dir): if not fname.lower().endswith((.jpg, .jpeg, .png)): continue key fname.split(_)[1] if _ in fname else fname prefixes[key] 1 dup {k: v for k, v in prefixes.items() if v 1} print(f{cls}: 共 {sum(prefixes.values())} 张, 原始编号 {len(prefixes)} 个, 多版本编号 {len(dup)} 个)这段代码的逻辑是按_切分文件名取第二段作为原始编号统计同一编号下有多少张图。如果某个编号在训练集里出现 3 次原图 zoomed brightened说明增强版本集中在一个集合内这是正常的如果同一个编号在 train 和 test 里都出现就要手动剔除。参数上root改成你解压后的实际路径即可脚本不依赖任何第三方库。2.2 json 类别映射与标签读取资源说明里明确写了具体查看 json 文件这意味着类别索引不是按目录名字母序硬编码的。很多分类项目翻车就翻在这里训练时用ImageFolder默认按文件夹名排序生成class_to_idx推理时又按另一个顺序解析 json结果花叶病毒被当成健康。正确做法是先读 json建立自己的映射表再和目录名对齐。import json import os # 读取资源中的 json 文件确认类别定义 with open(data/class_info.json, r, encodingutf-8) as f: info json.load(f) # json 结构常见为 {0: 花叶病毒, 1: 健康, ...} 或 {classes: [...]} if classes in info: classes info[classes] else: classes [info[str(i)] for i in range(len(info))] print(json 中的类别顺序:, classes) # 与目录名核对 train_dir data/train dirs sorted(os.listdir(train_dir)) print(目录中的类别顺序:, dirs) # 建立统一映射后续训练和推理都用它 class_to_idx {name: idx for idx, name in enumerate(classes)} print(统一映射:, class_to_idx)逻辑说明先兼容两种常见 json 结构再打印出来和目录名对比。如果两者顺序不一致以 json 为准在 Dataset 里用class_to_idx手动映射不要依赖ImageFolder的默认行为。参数上json 路径和训练目录按实际解压位置改。这一步花两分钟能省掉后面调半天精度上不去的玄学时间。3. 用 CNN 跑通训练从 DataLoader 到第一个 baseline3.1 构建 Dataset 与数据增强策略数据本身已经做过离线增强在线增强就要克制。我的习惯是训练集只加随机水平翻转和轻微颜色抖动验证/测试集不做增强只做 resize 和归一化。原因是离线增强已经引入了缩放、平移、亮度变化再叠加过强的在线增强分布会偏得厉害尤其霜霉病那种依赖颜色纹理的类别容易掉点。import torch from torch.utils.data import Dataset, DataLoader from torchvision import transforms from PIL import Image import os class WatermelonDataset(Dataset): def __init__(self, root, class_to_idx, transformNone): self.samples [] self.transform transform self.class_to_idx class_to_idx for cls_name in os.listdir(root): cls_dir os.path.join(root, cls_name) if not os.path.isdir(cls_dir): continue label class_to_idx[cls_name] for fname in os.listdir(cls_dir): if fname.lower().endswith((.jpg, .jpeg, .png)): self.samples.append((os.path.join(cls_dir, fname), label)) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] img Image.open(path).convert(RGB) if self.transform: img self.transform(img) return img, label # 训练集轻量在线增强 train_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.1, contrast0.1), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) # 测试集只 resize 归一化 test_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_set WatermelonDataset(data/train, class_to_idx, train_tf) test_set WatermelonDataset(data/test, class_to_idx, test_tf) train_loader DataLoader(train_set, batch_size32, shuffleTrue, num_workers4) test_loader DataLoader(test_set, batch_size32, shuffleFalse, num_workers4) print(f训练集 {len(train_set)} 张, 测试集 {len(test_set)} 张)参数说明Resize((224, 224))是 ImageNet 预训练模型的通用输入尺寸如果你用自己改的网络按网络第一层期望的尺寸改。batch_size32在 8GB 显存上跑 ResNet50 级别模型比较稳显存小就降到 16。num_workers4在 Windows 上如果报错就改成 0这是老问题了。归一化的 mean/std 用 ImageNet 统计值因为后面大概率要加载预训练权重。3.2 加载预训练模型并替换分类头4 分类任务数据量 5700 张从头训容易过拟合常见做法是加载 ImageNet 预训练权重把最后的全连接层换成 4 输出。资源摘要里提到CNN 分类网络改进说明作者本身也在做网络结构层面的调整baseline 先用标准 ResNet 跑通再谈改进。import torch.nn as nn from torchvision import models def build_model(num_classes4, pretrainedTrue): # 以 ResNet50 为例也可换成 EfficientNet、ConvNeXt 等 weights models.ResNet50_Weights.IMAGENET1K_V2 if pretrained else None model models.resnet50(weightsweights) in_features model.fc.in_features # 替换分类头输出类别数与 json 一致 model.fc nn.Linear(in_features, num_classes) return model device torch.device(cuda if torch.cuda.is_available() else cpu) model build_model(num_classeslen(classes)).to(device) criterion nn.CrossEntropyLoss() # 分类头用大学习率主干用小学习率是迁移学习的常规操作 optimizer torch.optim.AdamW([ {params: model.fc.parameters(), lr: 1e-3}, {params: [p for n, p in model.named_parameters() if not n.startswith(fc)], lr: 1e-4}, ], weight_decay1e-4)逻辑说明ResNet50_Weights.IMAGENET1K_V2是 torchvision 新版接口老版本用pretrainedTrue。分组学习率是迁移学习的常规操作分类头随机初始化需要大学习率快速收敛主干已经学过通用特征小学习率微调即可。weight_decay加一点正则对 5700 张这种规模的数据有帮助。3.3 训练循环与验证指标训练循环本身不复杂关键是每个 epoch 后在测试集上算准确率和混淆矩阵。农业病害识别里把炭疽病误判成霜霉病的代价比把健康误判成花叶病毒高因为前者意味着用错药。所以别只看总准确率要看每一类的召回。from sklearn.metrics import classification_report, confusion_matrix import numpy as np def evaluate(model, loader, device): model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in loader: imgs imgs.to(device) outputs model(imgs) preds outputs.argmax(dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) return np.array(all_labels), np.array(all_preds) for epoch in range(20): model.train() running_loss 0.0 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() running_loss loss.item() * imgs.size(0) avg_loss running_loss / len(train_set) y_true, y_pred evaluate(model, test_loader, device) acc (y_true y_pred).mean() print(fEpoch {epoch1}: loss{avg_loss:.4f}, test_acc{acc:.4f}) # 训练结束后打印详细报告 print(classification_report(y_true, y_pred, target_namesclasses)) print(confusion_matrix(y_true, y_pred))参数说明epoch设 20 是保守值配合早停可以更少。classification_report会输出每一类的 precision、recall、f1重点看霜霉病和炭疽病的 recall 是否明显低于健康类。如果某一类 recall 低于 0.8优先怀疑该类样本量偏少或标注有歧义而不是急着换网络。4. 可视化与数据检查show 脚本之外还要看什么4.1 用 show 脚本快速过一遍数据资源里带了 show 脚本直接跑能可视化数据集。这一步别跳过因为标注问题肉眼一看就出来。常见做法是随机抽每个类别若干张拼成网格观察是否有明显错标、重复、或者非叶片图像混入。import matplotlib.pyplot as plt import random def show_grid(dataset, classes, n4): fig, axes plt.subplots(len(classes), n, figsize(n*3, len(classes)*3)) for i, cls in enumerate(classes): idxs [j for j, (_, label) in enumerate(dataset.samples) if label i] pick random.sample(idxs, min(n, len(idxs))) for j, idx in enumerate(pick): path, _ dataset.samples[idx] img Image.open(path).convert(RGB) axes[i][j].imshow(img) axes[i][j].set_title(cls, fontsize9) axes[i][j].axis(off) plt.tight_layout() plt.savefig(dataset_preview.png, dpi120) print(已保存 dataset_preview.png) show_grid(train_set, classes)逻辑说明按类别各抽 4 张拼成 4 行 4 列的网格。重点看三件事同一类别内是否混入了其他病害、健康类里是否有明显病斑、文件名后缀对应的增强图是否看起来自然。如果发现某类里混了十几张明显不对的图手动移走比让模型硬学更划算。4.2 类别分布与图像尺寸统计5700 张分 4 类平均每类 1400 张左右但实际分布可能不均。类别不平衡会直接影响召回先统计再决定要不要加权采样。from collections import Counter labels [label for _, label in train_set.samples] counter Counter(labels) for i, cls in enumerate(classes): print(f{cls}: {counter[i]} 张 ({counter[i]/len(labels)*100:.1f}%)) # 统计图像尺寸分布 sizes Counter() for path, _ in train_set.samples[:500]: # 抽样 500 张即可 with Image.open(path) as im: sizes[im.size] 1 print(尺寸分布 top5:, sizes.most_common(5))如果某一类占比低于 15%训练时给CrossEntropyLoss加weight参数或者用WeightedRandomSampler。尺寸统计是为了确认 resize 不会把病斑压得太小——如果原图普遍是 4000 像素级别直接 resize 到 224 会丢失细节常见做法是先中心裁剪病斑区域再 resize或者用更大输入尺寸配合下采样。5. 避坑与排查标注数据集的五个血泪经验5.1 训练集和测试集出现同一原图的增强版本现象测试准确率异常高轻松到 99%但换一批新图推理就崩。原因zoomed、shifted、brightened版本被随机划分到了两个集合模型在测试集上看到的是训练时见过的原图变体。解决按原始编号划分同一编号的所有增强版本只进一个集合。用 2.1 节的脚本先查重复编号发现跨集合重复就手动归并。5.2 json 类别顺序与目录名不一致现象训练时 loss 正常下降但混淆矩阵里所有预测都偏向某一类或者类别名和实际内容对不上。原因ImageFolder按目录名字母序生成标签json 里是另一套顺序两者错位。解决统一用 json 的类别顺序建class_to_idxDataset 里手动映射训练和推理共用同一个映射表。别偷懒用ImageFolder的class_to_idx。5.3 离线增强过强导致颜色特征失真现象霜霉病和健康类混淆严重模型学到的更像是亮度而不是病斑纹理。原因brightened版本把亮度拉得过高叶背灰紫色霉层被冲淡和健康叶片的差异变小。解决检查brightened样本的亮度分布如果明显偏离正常拍摄条件训练时把这部分样本降权或剔除。在线增强只保留水平翻转和轻微抖动。5.4 图像尺寸过大直接 resize 丢失病斑细节现象炭疽病的圆形凹陷病斑在 224 输入下几乎看不见该类召回率长期低于 0.7。原因原图可能是手机拍摄的高分辨率图病斑只占画面一小块直接 resize 后病斑被压缩到几个像素。解决先统计病斑区域占比如果普遍偏小改用中心裁剪或随机裁剪到病斑附近再 resize或者把输入尺寸提到 384。代价是显存和训练时间增加。5.5 测试集样本量过少导致指标波动大现象每个 epoch 测试准确率跳动好几个百分点不知道哪个才是真实水平。原因测试集如果只有几百张单次评估的方差很大。解决用 K 折交叉验证或者至少保证测试集每类不少于 200 张。如果资源里的测试集偏小从训练集里按原始编号划出一部分做验证集训练集相应减少别让测试集反复参与调参。6. 进阶技巧用混淆矩阵反推标注质量与类别边界跑通 baseline 之后真正有价值的信息在混淆矩阵里。5700 张 4 分类的数据集总准确率到 90% 以上不难难的是把炭疽病和霜霉病的互相误判压下去。我的习惯是训练结束后固定做三件事导出混淆矩阵、挑出置信度低的样本、回看这些样本的原图。import torch.nn.functional as F def find_hard_samples(model, dataset, device, top_k20): model.eval() records [] with torch.no_grad(): for idx in range(len(dataset)): img, label dataset[idx] logits model(img.unsqueeze(0).to(device)) prob F.softmax(logits, dim1).cpu().squeeze() pred prob.argmax().item() conf prob[pred].item() # 记录预测错误或置信度低的样本 if pred ! label or conf 0.6: records.append((idx, label, pred, conf)) records.sort(keylambda x: x[3]) return records[:top_k] hard find_hard_samples(model, test_set, device) for idx, label, pred, conf in hard: path, _ test_set.samples[idx] print(f{path} | 真实:{classes[label]} 预测:{classes[pred]} 置信度:{conf:.3f})逻辑说明遍历测试集把预测错误或置信度低于 0.6 的样本挑出来按置信度升序排列。这些就是模型的黑匣子时刻。参数top_k20控制输出数量conf 0.6是经验阈值可按实际分布调整。拿到这批难样本后逐张打开看。通常会发现三类问题一是标注本身有歧义比如早期炭疽病和健康叶片边缘发黄容易混二是图像质量差逆光或模糊导致病斑不可辨三是增强版本引入的伪影比如shifted后叶片移出画面模型只能靠背景猜。前两类考虑重新标注或剔除第三类考虑在数据加载时过滤掉边缘空白过多的图。另一个技巧是对混淆矩阵做归一化看每一类的误判流向。如果炭疽病有 15% 被判成霜霉病而反方向只有 3%说明这两类的特征空间有重叠但炭疽病的样本多样性不够。常见做法是对炭疽病单独做更强的在线增强或者引入注意力机制让网络聚焦病斑区域。资源摘要里提到的 CNN 分类网络改进方向之一就是在主干后加通道注意力或空间注意力这类改动在病斑细粒度分类上通常有 1 到 3 个点的提升。最后说一个我自己的习惯每次拿到新的分类数据集先不碰模型花半小时把类别分布、尺寸分布、重复编号、难样本预览这四件事过一遍。这半小时的后悔药比后面调参两天都值。这份西瓜病害数据集的结构已经算清晰json 和 show 脚本都给了剩下的就是把上面这些检查项走一遍再决定用哪个网络、怎么增强。希望帮到你。本文还有配套的精品资源点击获取
返回列表