☰
甘蔗病害图像分类数据集:19000张标注样本的预处理与模型训练实战
2026/10/11 14:05:11 网站建设 项目流程

简介:本资源为一份面向计算机视觉学习者的甘蔗植物病害图像分类数据集,适用于图像分类模型训练、算法对比与课程实践等场景,尤其适合正在做农业病害识别或深度学习入门项目的人群。数据集共标注约19,000张图像,涵盖红腐病、锈病、健康、枯萎病等6个类别,具体类别名称可查看包内json文件;数据已按训练集与测试集划分,同类图片分别存放,便于直接接入主流分类网络。压缩包为7z格式,内含2000个文件,其中1998个jpg图像、1个py脚本与1个json标注文件,整体约424.78MB,运行show脚本即可快速可视化样本分布与图像内容。目前已有199人学习下载。借助该数据集,读者可完成从数据加载、类别统计到模型训练与评估的完整流程,并结合作者提供的图像分类网络改进与计算机视觉项目系列内容,进一步理解数据增强、类别不平衡处理与模型调优思路,适合作为毕设、竞赛或论文实验的基础数据支撑。

1. 甘蔗病害图像分类数据集:19000 张已标注样本能解决什么

甘蔗叶片上出现一条细长的黄白条纹,到底是缺素、药害,还是白条病?这个判断在田间往往靠经验,但经验没法批量复制。我拿到这份约 19000 张的甘蔗植物病害图像分类数据集时,第一反应不是"数据量够大",而是"标注粒度到底能不能支撑分类模型收敛"。翻了一遍目录结构,它按病害类别分文件夹存放,每张图对应一个类别标签,属于典型的单标签图像分类数据集,不是目标检测那种带边界框的格式。这意味着你不需要额外写解析脚本去处理 XML 或 JSON 标注文件,直接把文件夹名当类别名就能喂给训练框架。

适合谁用?如果你在做农业视觉相关的图像分类算法验证,或者想拿一个真实场景的数据集跑通从数据加载到模型部署的完整链路,这份资源省掉了最耗时的采集和标注环节。19000 张的体量不算大,但足够训练一个中等规模的分类网络,也够做迁移学习后的微调。不适合的场景也要说清楚:它做不了目标检测,也做不了语义分割,因为标注只到图像级别,没有像素级或框级别的信息。

2. 数据组织与预处理:从文件夹到 TensorFlow/PyTorch 可读格式

2.1 先搞清楚目录结构和类别分布

拿到数据集后别急着写训练脚本,先花十分钟做两件事:确认目录层级、统计每个类别的样本数。常见做法是用pathlib遍历一遍,把类别名和对应文件数打印出来。这一步能帮你发现两个坑:一是某些类别样本极少,直接训练会导致严重的不平衡;二是文件名里可能混入了非图像文件,比如.DS_Store或缩略图缓存。

from pathlib import Path from collections import Counter data_root = Path("./sugarcane_disease_dataset") # 假设目录结构为 data_root/类别名/图片文件 class_counts = {} for class_dir in sorted(data_root.iterdir()): if class_dir.is_dir(): imgs = [f for f in class_dir.iterdir() if f.suffix.lower() in ('.jpg', '.jpeg', '.png', '.bmp')] class_counts[class_dir.name] = len(imgs) for name, cnt in class_counts.items(): print(f"{name}: {cnt}") print("总类别数:", len(class_counts)) print("总图片数:", sum(class_counts.values()))

这段代码的逻辑很直白:遍历根目录下的每个子文件夹,把后缀名在允许列表里的文件计数。参数上唯一需要改的是data_root,指向你解压后的实际路径。如果打印出来的总图片数和简介里的 19000 对不上,先检查是不是有嵌套目录没被遍历到,或者图片格式不在列表里。我一般会再加一行print(Counter(class_counts.values()))看分布,如果最大类和最小类差了一个数量级,后面训练时就得考虑加权采样或数据增强。

2.2 划分训练集、验证集、测试集

图像分类数据集最常见的翻车点不是模型结构,而是划分方式。如果你按文件夹随机抽 80% 做训练、10% 验证、10% 测试,看起来没问题,但如果同一片田、同一株甘蔗拍了多张照片,随机划分会导致训练集和验证集出现高度相似的样本,验证准确率虚高。这个数据集没有提供田块或植株 ID,所以只能按类别分层抽样,尽量保证每个类别的比例一致。

import random import shutil from pathlib import Path random.seed(42) data_root = Path("./sugarcane_disease_dataset") output_root = Path("./split_dataset") train_ratio, val_ratio = 0.8, 0.1 for class_dir in sorted(data_root.iterdir()): if not class_dir.is_dir(): continue imgs = [f for f in class_dir.iterdir() if f.suffix.lower() in ('.jpg', '.jpeg', '.png', '.bmp')] random.shuffle(imgs) n = len(imgs) n_train = int(n * train_ratio) n_val = int(n * val_ratio) splits = { "train": imgs[:n_train], "val": imgs[n_train:n_train + n_val], "test": imgs[n_train + n_val:] } for split_name, files in splits.items(): target = output_root / split_name / class_dir.name target.mkdir(parents=True, exist_ok=True) for f in files: shutil.copy2(f, target / f.name)

这里用shutil.copy2而不是move,是为了保留原始数据集不动,方便后面反复实验。random.seed(42)保证每次划分结果一致,团队协作时不会因为划分不同导致指标对不上。参数上train_ratio和val_ratio可以按需调整,但测试集至少留 10%,否则评估结果波动太大。如果某个类别样本数少于 50,建议直接合并到相近类别,或者用过采样补到 50 以上再划分。

2.3 用ImageDataGenerator或Dataset做在线增强

19000 张图不算多,尤其是某些病害类别可能只有几百张。在线增强是必选项,不是可选项。TensorFlow 的ImageDataGenerator和 PyTorch 的torchvision.transforms都能做,区别在于前者可以在 CPU 上异步加载,后者配合DataLoader的num_workers更灵活。我一般用 PyTorch,因为调试起来更直观。

from torchvision import transforms from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder train_tf = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(15), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=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, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_ds = ImageFolder("./split_dataset/train", transform=train_tf) val_ds = ImageFolder("./split_dataset/val", transform=val_tf) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4, pin_memory=True)

RandomResizedCrop的scale参数控制裁剪区域占原图的比例,设成 0.7 到 1.0 是为了模拟田间拍摄时植株远近不同的情况。ColorJitter的四个参数分别对应亮度、对比度、饱和度和色调,数值别设太大,否则叶片颜色失真,模型可能把颜色当成主要判别特征。Normalize用的 ImageNet 均值方差,如果你从零训练而不是用预训练权重,可以改成数据集自身的统计值,但用预训练权重时保持一致更重要。

3. 模型选型与训练:从 ResNet 到 EfficientNet 的取舍

3.1 为什么我不建议一上来就上 ViT

最新的图像分类模型里,Vision Transformer 和 ConvNeXt 确实在 ImageNet 上刷了很高的分,但那是 128 万张训练图加大量算力堆出来的。19000 张甘蔗病害图,类别数可能只有个位数到十几种,直接上 ViT 大概率过拟合。更务实的路线是:先用 ResNet-50 或 EfficientNet-B0 跑一个基线,看验证集准确率和混淆矩阵,再决定要不要换更大的模型。

ResNet-50 的优势是结构成熟、预训练权重好找、训练稳定。EfficientNet-B0 参数量更小,推理速度快,适合后面部署到边缘设备。如果你手头有 GPU,两个都跑一遍对比一下,成本不高。我一般会先跑 ResNet-50 五个 epoch,看 loss 下降是否正常,如果 loss 震荡厉害,先调学习率和 batch size,别急着换模型。

import torch import torch.nn as nn from torchvision import models device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2) num_classes = len(train_ds.classes) model.fc = nn.Linear(model.fc.in_features, num_classes) model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=20)

weights参数指定用 ImageNet 预训练权重,这是小数据集训练的关键。model.fc替换成新的全连接层,输出维度等于类别数。优化器用AdamW而不是SGD,是因为小数据集上 AdamW 收敛更快,但如果你追求极致精度,SGD 加动量在调好学习率后往往泛化更好。CosineAnnealingLR让学习率按余弦曲线下降,T_max设成总 epoch 数,避免后期学习率过低导致欠拟合。

3.2 训练循环里必须记录的东西

很多人训练时只看 loss 和 accuracy,结果模型不收敛时完全不知道从哪查。我习惯在每个 epoch 结束后记录:训练 loss、验证 loss、验证准确率、学习率、以及混淆矩阵。混淆矩阵能直接告诉你哪些类别容易被搞混,比如白条病和缺素症在叶片上的视觉特征可能很接近,模型分不清是正常的,这时候要么加数据,要么在损失函数里给这些类别更高权重。

from sklearn.metrics import confusion_matrix import numpy as np def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, correct, total = 0, 0, 0 for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() * imgs.size(0) preds = outputs.argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) return total_loss / total, correct / total def evaluate(model, loader, criterion, device): model.eval() total_loss, correct, total = 0, 0, 0 all_preds, all_labels = [], [] with torch.no_grad(): for imgs, labels in loader: imgs, labels = imgs.to(device), labels.to(device) outputs = model(imgs) loss = criterion(outputs, labels) total_loss += loss.item() * imgs.size(0) preds = outputs.argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) cm = confusion_matrix(all_labels, all_preds) return total_loss / total, correct / total, cm

train_one_epoch里loss.item() * imgs.size(0)是为了按样本数加权平均,避免最后一个 batch 大小不同导致 loss 偏差。evaluate里用torch.no_grad()关闭梯度计算,节省显存。混淆矩阵用 sklearn 的confusion_matrix生成,行是真实标签,列是预测标签,对角线上的数字越大越好。如果某个类别的召回率特别低,优先检查这个类别的样本是不是被其他类别的特征覆盖了。

3.3 学习率找不准就先跑 LR range test

学习率是训练里最玄学的参数之一。我见过太多人用 1e-3 跑 ResNet 结果 loss 直接炸掉,也见过用 1e-6 跑一百个 epoch 还没收敛的。比较靠谱的做法是跑一次 LR range test:从极小学习率开始,每个 batch 后指数增长,记录 loss 随学习率的变化曲线,找 loss 下降最快的那段对应的学习率。

import math def lr_range_test(model, loader, criterion, device, start_lr=1e-7, end_lr=1e-1, num_iter=200): optimizer = torch.optim.AdamW(model.parameters(), lr=start_lr) lr_scheduler = torch.optim.lr_scheduler.ExponentialLR( optimizer, gamma=(end_lr / start_lr) ** (1 / num_iter)) model.train() lrs, losses = [], [] best_loss = float('inf') for i, (imgs, labels) in enumerate(loader): if i >= num_iter: break imgs, labels = imgs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(imgs) loss = criterion(outputs, labels) loss.backward() optimizer.step() lr_scheduler.step() lrs.append(optimizer.param_groups[0]['lr']) losses.append(loss.item()) if loss.item() < best_loss: best_loss = loss.item() if loss.item() > 4 * best_loss: break return lrs, losses

跑完之后把lrs和losses画出来,找 loss 最低点对应的学习率,然后除以 10 作为训练时的初始学习率。这个技巧在换模型或换数据集时特别省时间,比盲目试参数靠谱得多。注意num_iter别设太大,200 个 batch 足够看出趋势,再多就是浪费算力。

4. 避坑与排查:标注数据集的五个血泪教训

4.1 类别文件夹名带空格或中文,导致ImageFolder读取出错

现象:用ImageFolder加载时抛出FileNotFoundError或者类别数为 0。原因:文件夹名里有空格、中文或特殊字符,某些版本的 torchvision 在解析路径时处理不当。解决:先把文件夹名统一改成英文小写加下划线,比如white_stripe、deficiency,改完再跑一遍目录统计脚本确认。

4.2 图片损坏或截断,训练中途报PIL.UnidentifiedImageError

现象:训练到某个 batch 突然崩溃,报错指向某张图片无法识别。原因:数据集里混入了下载不完整或格式损坏的图片。解决:在预处理阶段加一道校验,用PIL.Image.open逐个打开并verify(),把失败的图片路径打印出来,手动删除或替换。

from PIL import Image import warnings bad_files = [] for img_path in data_root.rglob("*"): if img_path.suffix.lower() in ('.jpg', '.jpeg', '.png', '.bmp'): try: with Image.open(img_path) as im: im.verify() except Exception as e: bad_files.append((str(img_path), str(e))) warnings.warn(f"损坏图片: {img_path}, 错误: {e}") print(f"共发现 {len(bad_files)} 张损坏图片")

4.3 验证集准确率远高于测试集,划分方式有问题

现象:验证集准确率 95%,测试集只有 70%。原因:划分时没有按类别分层,或者同一植株的多张照片被分到了不同集合。解决:改用分层抽样,确保每个集合的类别比例一致;如果数据集有田块或时间信息,按田块划分而不是随机划分。

4.4 训练 loss 不下降,检查标签是否和文件夹名对得上

现象:loss 一直在 2.3 左右震荡,准确率等于随机猜。原因:ImageFolder按文件夹名排序生成类别索引,如果你手动改了文件夹名但没重新生成映射,标签和图片可能错位。解决:打印train_ds.class_to_idx确认映射关系,再随机抽几张图连同标签一起可视化,肉眼确认。

4.5 显存不够,batch size 降了还是 OOM

现象:把 batch size 从 32 降到 8 还是爆显存。原因:图片分辨率太高,或者num_workers设太大导致内存泄漏。解决:先把图片统一 resize 到 256 或 224,再检查num_workers是否超过 CPU 核心数;如果还不行,用混合精度训练torch.cuda.amp能省一半显存。

5. 进阶技巧:用混淆矩阵反推数据质量问题

训练完一个基线模型后,别只看准确率就收工。混淆矩阵是排查数据质量问题的最好工具。我一般会把混淆矩阵归一化后打印出来,重点看两类错误:一是某个类别被大量误判为另一个类别,说明这两个类别的视觉特征重叠严重,可能需要合并或补充区分性样本;二是某个类别的召回率极低,说明这个类别的样本可能标注有误,或者数量太少被模型忽略了。

import seaborn as sns import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix def plot_confusion_matrix(cm, class_names, normalize=True): if normalize: cm = cm.astype('float') / cm.sum(axis=1, keepdims=True) plt.figure(figsize=(10, 8)) sns.heatmap(cm, annot=True, fmt='.2f' if normalize else 'd', cmap='Blues', xticklabels=class_names, yticklabels=class_names) plt.xlabel('Predicted') plt.ylabel('True') plt.title('Confusion Matrix') plt.tight_layout() plt.savefig('confusion_matrix.png', dpi=150) plt.show()

归一化后的混淆矩阵,对角线上的值就是每个类别的召回率。如果某个类别的召回率低于 0.6,先别急着调模型,回去看这个类别的图片是不是有标注错误。我遇到过一种情况:某个病害类别的文件夹里混进了十几张健康叶片的图,模型怎么训都学不会,因为标签本身就是矛盾的。把这几张图挑出来重新标注后,召回率直接从 0.55 涨到 0.82。

另一个技巧是用t-SNE或UMAP把倒数第二层的特征降维可视化。如果两个类别的特征点在二维平面上完全混在一起,说明模型提取的特征不足以区分它们,这时候要么换更强的 backbone,要么在数据层面找区分性更强的样本。这个分析比单纯看准确率有用得多,因为它能告诉你模型到底学到了什么。

从那以后我每次拿到新的分类数据集,都强制走一遍「目录统计 → 损坏校验 → 分层划分 → 基线训练 → 混淆矩阵分析」这五步,少一步后面都可能翻车。希望帮到你。

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

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

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

立即咨询