简介本资源为常规茶叶叶片病害图像分类数据集面向从事农业图像识别、深度学习分类任务的学生与算法工程师可用于训练和评估CNN分类模型。数据集已标注共划分5个类别包括褐枯病、灰枯萎病、红点病等具体类别信息可查看包内json文件。资源已按训练集、验证集、测试集划分各类别图片分别存放便于直接加载训练同时提供show脚本可快速可视化样本分布与图像内容。压缩包共2000个文件以1998张jpg图像为主另含1个py脚本和1个json标注文件整体约21.68MB结构清晰、开箱即用。目前已有104人学习下载适合作为茶叶病害分类项目的基准数据也可结合CNN网络改进思路进行模型对比实验帮助读者快速完成数据加载、类别核对与训练验证流程。1. 茶叶叶片病害分类数据集4000 张已标注图像能直接跑通什么拿到一个图像分类数据集第一反应不该是「有多少张」而是「标注结构长什么样、能不能直接喂给训练脚本」。这份常规茶叶叶片病害图像分类数据集约 4000 张已标注图像分 5 个类别——褐枯病、灰枯萎病、红点病等具体类别名以随包的 json 文件为准。它已经把训练集、验证集、测试集按同一类别分目录存放还带了一个可视化脚本省掉了自己写划分逻辑的功夫。适合谁用一是想快速验证 CNN 分类网络改进效果的人二是做农业图像识别、需要一份干净多分类数据做 baseline 的从业者三是教学场景里要演示「数据加载到训练」完整链路的。不适合谁想直接拿来做目标检测的这份是分类标注不是边界框标注别硬套 YOLO 那套流程。下面从目录结构、加载、训练、避坑到进阶一层层拆开讲。2. 数据集目录结构与标注格式先看清 json 再动手2.1 目录组织与类别映射这类分类数据集常见的组织方式是train/val/test三个根目录每个根目录下再按类别名建子文件夹图片直接放在对应类别文件夹里。这种结构的好处是torchvision.datasets.ImageFolder能直接读不需要额外写索引文件。但这份资源额外带了 json 文件说明类别名和编号的映射关系可能存在 json 里而不是单纯靠文件夹名。先做一件事把 json 读出来确认类别数和类别名。很多翻车现场就是「以为有 5 类结果 json 里写了 6 类最后一类只有 3 张图」训练时 loss 直接 NaN。import json import os # 假设 json 文件在数据集根目录名为 classes.json json_path ./tea_dataset/classes.json with open(json_path, r, encodingutf-8) as f: class_info json.load(f) print(类别总数:, len(class_info)) for idx, name in class_info.items(): print(f 编号 {idx} - 类别 {name})逻辑说明这段代码只做一件事——把类别映射打印出来。参数上encodingutf-8必须加中文类别名在 Windows 下默认编码容易乱码。如果 json 结构不是{编号: 名称}而是{classes: [...]}把取值那行改成class_info[classes]即可。跑完这一步你心里就有底了到底几类、每类叫什么。2.2 统计每类样本数识别长尾类别不均衡是农业病害数据集的常态。褐枯病可能拍了 1500 张红点病只有 300 张。不先统计就开训模型会偏向多数类验证集准确率看着高实际对少数类几乎没识别能力。import os from collections import Counter root ./tea_dataset/train counter Counter() for cls_name in os.listdir(root): cls_dir os.path.join(root, cls_name) if os.path.isdir(cls_dir): imgs [f for f in os.listdir(cls_dir) if f.lower().endswith((.jpg, .png, .jpeg))] counter[cls_name] len(imgs) for cls_name, num in counter.most_common(): print(f{cls_name}: {num} 张) total sum(counter.values()) print(训练集合计:, total)逻辑说明os.listdir遍历类别文件夹endswith过滤出图片文件避免把.DS_Store或缩略图缓存算进去。most_common()按数量降序排列一眼就能看出哪个类是长尾。如果最多类和最少类差距超过 5 倍训练时就得考虑加权采样或数据增强补偿这个后面第 4 章会展开。2.3 用自带 show 脚本做可视化抽检资源里带了 show 脚本直接跑之前先确认它依赖什么。常见做法是 matplotlib 读几张图拼成网格。如果脚本报错大概率是路径写死或者缺少Pillow。我一般会先手动抽检 20 张确认图片没有损坏、没有标注错位。# 先看 show 脚本依赖 head -30 show.py # 常见依赖缺失时补装 pip install matplotlib pillow numpy # 运行可视化 python show.py逻辑说明head -30先看脚本头部导入和路径配置避免直接跑报一堆错不知道从哪查。如果脚本里路径是绝对路径改成相对路径再跑。可视化不是为了好看是为了确认「褐枯病的图确实是褐枯病」标注质量决定后面所有训练的上限。3. 从 ImageFolder 到 DataLoader把 4000 张图喂进 CNN3.1 构建 Dataset 与划分校验虽然数据集已经分好 train/val/test但还是要校验一遍三个集合的类别是否一致。常见坑是验证集里少了一个类别训练时模型学了 5 类验证时只有 4 类算准确率直接报错。from torchvision import datasets, transforms data_dir ./tea_dataset train_dir os.path.join(data_dir, train) val_dir os.path.join(data_dir, val) test_dir os.path.join(data_dir, test) # 基础变换统一尺寸 转张量 base_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), ]) train_ds datasets.ImageFolder(train_dir, transformbase_tf) val_ds datasets.ImageFolder(val_dir, transformbase_tf) test_ds datasets.ImageFolder(test_dir, transformbase_tf) print(训练集类别:, train_ds.classes) print(验证集类别:, val_ds.classes) print(测试集类别:, test_ds.classes) print(类别是否一致:, train_ds.classes val_ds.classes test_ds.classes)逻辑说明ImageFolder会自动按子文件夹名排序生成classes列表三个集合的classes必须完全相同。Resize((224, 224))是 ImageNet 预训练模型的标配输入尺寸如果你用别的骨干网络按它的要求改。ToTensor()把像素从 0-255 归一化到 0-1这是后续 Normalize 的前提。3.2 数据增强与归一化参数茶叶叶片图像的光照、角度、背景差异大增强是必须的。但增强不能乱加比如随机裁剪可能把病斑裁掉颜色抖动过度会让褐枯病和红点病颜色特征混淆。train_tf transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.8, 1.0)), 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((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])逻辑说明RandomResizedCrop的scale(0.8, 1.0)控制裁剪面积比例下限别低于 0.7否则病斑容易被裁没。RandomRotation(15)限制在 15 度内茶叶叶片方向性没那么强但旋转太大可能引入不真实样本。Normalize的均值和标准差是 ImageNet 统计值用预训练权重时必须对齐否则特征分布偏移收敛变慢。验证集只用 Resize Normalize不做随机增强保证评估可复现。3.3 DataLoader 批大小与线程设置4000 张图不算大但批大小和线程数设错训练速度差一倍。from torch.utils.data import DataLoader train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) test_loader DataLoader(test_ds, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue)逻辑说明batch_size32是 224 尺寸下的稳妥值显存 8G 以上可以试 64。shuffleTrue只在训练集开验证和测试必须关否则评估结果每次不一样。num_workers4在 Linux 下通常够用Windows 下如果报错就改成 0 先跑通。pin_memoryTrue在 GPU 训练时能加速数据搬运CPU 训练可以关掉。4. 训练配置与常见问题排查血泪经验都在这里4.1 学习率与优化器选择分类任务用预训练骨干时学习率别设大。常见做法是主干网络用 1e-4分类头用 1e-3分参数组设置。import torch import torch.nn as nn from torchvision import models model models.resnet50(pretrainedTrue) num_classes len(train_ds.classes) model.fc nn.Linear(model.fc.in_features, num_classes) # 分参数组主干小学习率分类头大学习率 backbone_params [p for n, p in model.named_parameters() if fc not in n] head_params [p for n, p in model.named_parameters() if fc in n] optimizer torch.optim.AdamW([ {params: backbone_params, lr: 1e-4}, {params: head_params, lr: 1e-3}, ], weight_decay1e-4) criterion nn.CrossEntropyLoss()逻辑说明pretrainedTrue加载 ImageNet 权重小数据集上这是提点最快的手段。AdamW比Adam多了正确的权重衰减实现分类任务上更稳。weight_decay1e-4抑制过拟合数据量 4000 张不算大正则化不能省。CrossEntropyLoss是多分类标配如果类别不均衡严重加weight参数传类别权重。4.2 训练循环与验证指标训练循环要记录训练 loss 和验证准确率别只看 loss 下降就以为没问题。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) correct (outputs.argmax(1) labels).sum().item() total imgs.size(0) return total_loss / total, correct / total torch.no_grad() def evaluate(model, loader, device): model.eval() correct, total 0, 0 for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) outputs model(imgs) correct (outputs.argmax(1) labels).sum().item() total imgs.size(0) return correct / total逻辑说明model.train()和model.eval()必须成对出现影响 BatchNorm 和 Dropout 行为。torch.no_grad()在验证时关闭梯度省显存也提速。loss.item() * imgs.size(0)是按样本数加权平均避免最后一个 batch 不满时 loss 计算偏差。验证准确率才是选模型的依据训练 loss 低不代表泛化好。4.3 避坑与常见问题排查现象一训练 loss 一直不降准确率卡在 20% 左右。原因类别数对不上或者标签编码错位。json 里写 5 类但文件夹有 6 个ImageFolder按文件夹名排序生成标签和 json 映射不一致。 解决打印train_ds.classes和 json 的类别列表逐项比对确保顺序和数量完全一致。不一致就重命名文件夹或重写 json。现象二验证准确率比训练准确率高很多。原因验证集太小或者验证集和训练集有重叠图片。4000 张按 7:2:1 划分验证集只有 800 张波动大正常但高太多就是数据泄漏。 解决用图片 MD5 去重检查 train 和 val 是否有相同文件。常见做法是划分前先对所有图片做哈希按哈希划分而不是按文件名。现象三训练几个 epoch 后 loss 突然变 NaN。原因学习率太大或者某张图片损坏导致梯度爆炸。 解决先把学习率降 10 倍试。如果还 NaN在 Dataset 的__getitem__里加 try-except把读不出来的图片路径打出来直接删掉或修复。现象四GPU 显存够但利用率很低训练慢。原因num_workers设太小或者数据增强在 CPU 上成了瓶颈。 解决把num_workers加到 8 试同时用pin_memoryTrue。如果还慢检查是不是每张图都做了耗时的颜色变换适当简化增强。现象五测试集准确率远低于验证集。原因测试集分布和训练验证不一致比如测试集图片来自不同拍摄设备或不同光照条件。 解决这不是代码问题是数据问题。要么补充测试集同分布的样本要么在训练时加入更强的光照和颜色增强提升模型鲁棒性。5. 进阶技巧用混淆矩阵和 t-SNE 验证模型到底学到了什么准确率只是一个数字5 分类任务里模型可能把红点病全预测成褐枯病但另外三类全对准确率照样 80%。要真正判断模型可用性得看混淆矩阵和特征分布。import numpy as np from sklearn.metrics import confusion_matrix, classification_report import matplotlib.pyplot as plt import seaborn as sns torch.no_grad() def get_all_preds(model, loader, device): model.eval() all_preds, all_labels [], [] for imgs, labels in loader: imgs imgs.to(device) outputs model(imgs) preds outputs.argmax(1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) return np.array(all_preds), np.array(all_labels) preds, labels get_all_preds(model, test_loader, device) cm confusion_matrix(labels, preds) plt.figure(figsize(8, 6)) sns.heatmap(cm, annotTrue, fmtd, xticklabelstest_ds.classes, yticklabelstest_ds.classes) plt.xlabel(预测类别) plt.ylabel(真实类别) plt.title(测试集混淆矩阵) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150) print(classification_report(labels, preds, target_namestest_ds.classes))逻辑说明confusion_matrix的行是真实标签列是预测标签对角线是正确分类。如果某一列特别亮说明模型偏向预测那个类。classification_report给出每个类的精确率、召回率和 F1比整体准确率更有参考价值。sns.heatmap的annotTrue把数字标在格子里fmtd保证显示整数。混淆矩阵能告诉你「哪个类被混淆了」但不知道「为什么混淆」。这时候用 t-SNE 把倒数第二层的特征降到二维看分布。from sklearn.manifold import TSNE torch.no_grad() def extract_features(model, loader, device): model.eval() # 去掉最后的全连接层取池化后的特征 feature_extractor torch.nn.Sequential( *list(model.children())[:-1] ).to(device) feats, labs [], [] for imgs, labels in loader: imgs imgs.to(device) out feature_extractor(imgs).squeeze(-1).squeeze(-1) feats.append(out.cpu().numpy()) labs.extend(labels.numpy()) return np.concatenate(feats), np.array(labs) feats, labs extract_features(model, test_loader, device) tsne TSNE(n_components2, perplexity30, random_state42) feats_2d tsne.fit_transform(feats) plt.figure(figsize(8, 6)) for i, cls_name in enumerate(test_ds.classes): mask labs i plt.scatter(feats_2d[mask, 0], feats_2d[mask, 1], labelcls_name, alpha0.6, s10) plt.legend() plt.title(测试集特征 t-SNE 分布) plt.tight_layout() plt.savefig(tsne.png, dpi150)逻辑说明model.children()[:-1]去掉 ResNet 最后的全连接层保留全局池化输出得到每张图的特征向量。perplexity30是 t-SNE 的常用值数据量 800 左右时 30 到 50 都合理。random_state42保证每次跑图一致方便对比不同模型。如果 t-SNE 图上某一类散得到处都是说明模型没学到该类别的判别特征要么数据不够要么增强过度把特征破坏了。我自己的习惯是每次训完一个模型混淆矩阵和 t-SNE 必须各跑一遍不看这两个图不敢说模型能用。有一次准确率 92% 看着挺好混淆矩阵一出来发现红点病召回率只有 0.6全被预测成褐枯病后来补了 200 张红点病样本才拉回来。从那以后我每次拿到分类数据集都强制先跑一遍类别统计和可视化抽检再开始训。希望帮到你。本文还有配套的精品资源点击获取