
简介常规茶叶叶片病害图像分类数据集面向计算机视觉方向的学生、农业病害识别研究者及图像分类入门者解决茶叶常见叶部病害样本分散、标注数据集不易获取的问题适合作为图像分类课程作业、毕业设计或算法验证的基准数据。数据覆盖褐枯病、灰枯萎病、红点病等5类已标注病害约4000张图像并划分好训练集、验证集和测试集可直接用于CNN等分类网络的训练与效果评估也能降低数据预处理门槛。压缩包共2000个文件以jpg图片为主另含1个Python可视化脚本和1个json标注文件整体约21.68MB体积小巧、结构清晰方便本地快速部署。目前已有105人学习下载。配套的show脚本可帮助快速预览各类别样本json文件提供具体类别映射三类数据分目录存放省去手动划分耗时适合初学者快速搭建基线也便于在此基础上尝试分类网络改进与优化实验。此外清晰的数据组织方式还有助于复现实验结果为后续在不同产区、不同光照条件下扩展病害识别研究提供可靠基础。1. 常规茶叶叶片病害图像分类数据集4,000张已标注图像带来的真实起点常规茶叶叶片病害图像分类数据集核心就一句话约4,000张已经标注好的茶叶叶片病害图片。拿到它之后第一个判断不应该是“能不能训”而是“这4,000张能撑起什么样的图像分类任务”。有过中小规模图像分类经验的人都知道配合预训练权重做迁移学习这种体量下验证集准确率跑到90%以上并不难难的是类别划分、标注一致性和验证集划分都经得起推敲。这篇笔记适合两类人给学生作业、竞赛或植保落地项目找图像数据的从业者以及想快速验证自己图像分类算法基线、又不想从零采数据的研究人员。我会按“先检查数据、再搭训练管线、后可视化验证”的顺序讲尽量少谈玄学多给具体参数和踩坑记录。2. 拆数据集的三个检查脚本目录结构、类别统计与标注噪声预处理拿到任何图像分类数据集我的习惯都是先忍住不开训花半小时把数据彻底拆一遍。茶叶叶片病害数据尤其需要这一步田间拍摄的图片往往存在过曝、叶片反光、背景杂乱、同类病斑表现不一致等问题这些问题不提前发现后面所有训练和调参都是在给脏数据打工。2.1 先确认标注格式类别文件夹与CSV两种常见组织方式解压之后的第一件事是确认标注是“目录即标签”还是“文件名映射标签”。常规茶叶叶片病害图像分类数据集最常见的组织方式是ImageNet式目录data/train/茶饼病/*.jpg、data/val/健康叶/*.jpg另一种是图片平铺在images/目录下由train.csv记录文件名与类别。前者对PyTorch的ImageFolder最友好后者更利于做细粒度的标签清洗。先把目录结构看清再写代码能省下半天返工时间。unzip -q tea_leaf_disease.zip -d tea_leaf_disease cd tea_leaf_disease find . -maxdepth 2 -type d | sort | head -30-maxdepth 2是为了只看类别层不把每张图片都打印出来刷屏head -30截断输出防止类别目录过多时终端滚动。看到类似./train/茶饼病、./val/健康叶的结构后面直接使用torchvision.datasets.ImageFolder零额外代码如果看到的是./images加train.csv就需要写一个自定义Dataset从CSV读路径和标签。这一步的判断决定了后续所有数据加载代码的写法值得在项目开始时多花五分钟确认。2.2 清点图像数量、尺寸与损坏文件训练前必须过的一道关4,000张数据不算多但每张图的尺寸分布往往被忽略。茶叶叶片照片可能由手机拍摄也可能由田间固定相机拍摄短边从720像素到4,000像素都有可能。如果病斑本身只占叶片的一小块统一缩放到224像素会让模型学到“看整片叶子”而不是“看病斑”。所以我会在训练前统计每个类别的数量、宽高范围同时检查解压过程中产生的损坏文件。import os import cv2 import csv from collections import defaultdict data_root tea_leaf_disease stats defaultdict(lambda: {count: 0, bad: 0, w: [], h: []}) for root, _, files in os.walk(data_root): label os.path.basename(root) if not files: continue for name in files: if not name.lower().endswith((.jpg, .jpeg, .png, .bmp)): continue path os.path.join(root, name) stats[label][count] 1 img cv2.imread(path) if img is None: stats[label][bad] 1 continue h, w img.shape[:2] stats[label][w].append(w) stats[label][h].append(h) with open(dataset_stats.csv, w, newline) as f: writer csv.writer(f) writer.writerow([label, count, bad, min_w, max_w, min_h, max_h]) for label, s in sorted(stats.items()): writer.writerow([label, s[count], s[bad], min(s[w]), max(s[w]), min(s[h]), max(s[h])])逻辑说明os.walk按三层目录结构遍历label取当前目录名所以这个脚本要求数据组织为“类别目录直接包含图片”如果中间还隔了一层train/valid就要把os.path.basename(root)换成上层目录名。cv2.imread读取失败返回None以此判断损坏文件。输出CSV后重点看两个指标一是有没有类别图像数特别少二是宽高范围是否跨度过大。对损坏文件建议移动到backup目录而不是直接删除防止误删后没有后悔药。2.3 类别不均衡怎么处理先算不平衡度再决定损失函数茶叶叶片病害数据里健康叶和常见病炭疽病、茶饼病、茶轮斑病样本通常充足某些稀有病害可能只有几十张。4,000张规模下不处理不均衡模型很容易在验证集上收敛成“全猜健康叶”的平庸解。我一般用一个可复用的判断标准最大类与最小类的样本数之比超过10倍就让采样器或损失函数介入3到10倍之间优先用加权损失而不是过采样因为过采样会放大少数类里本来就存在的标注噪声。import pandas as pd from torch.utils.data import WeightedRandomSampler df pd.read_csv(dataset_stats.csv) counts df.set_index(label)[count] imbalance_ratio counts.max() / counts.min() print(fmax/min ratio: {imbalance_ratio:.1f}) if imbalance_ratio 10: weights 1.0 / counts print(sample weights:, weights.to_dict()) # sampler WeightedRandomSampler(weightsweights, num_sampleslen(dataset), replacementTrue)逻辑说明weights按类别取倒数样本越少的类别被采样到的概率越高等效于对少数类做“软过采样”。replacementTrue允许同一张图在一个epoch里被多次取出这是过采样效果的实现方式。参数说明如果比例在3到10倍之间我通常不做采样干预保持天然分布训练评估时用macro-F1衡量因为过度平衡会让样本极少的类过拟合得很快。这个决策要在训练开始前定下来而不是等loss曲线不对劲了再回头改。3. 用迁移学习在4,000张茶叶病害图像上搭分类管线模型与DataLoader选型数据检查做完才进入训练管线的搭建。对4,000张这个量级模型选型的核心不是“哪个最新”而是“哪个在收敛速度和过拟合控制之间最平衡”。这一章给出我常用的模型选择逻辑、DataLoader配置和一套可直接复用的训练骨架。3.1 为什么在4,000张规模下选预训练CNN而不是“最新”的ViT最新的图像分类模型如ViT、Swin Transformer在ImageNet上表现确实亮眼但在4,000张的小规模数据上直接微调效果往往不如预训练CNN。原因很直接Transformer靠大量数据学归纳偏置数据量骤减后需要更强正则和更多训练轮次才能收敛投入产出比很低。常规做法是用torchvision提供的ImageNet预训练CNN权重做迁移学习。模型参数量4,000张下的特点适用场景ResNet18约11M收敛快过拟合可控默认首选ResNet34约21M精度略高训练更慢数据质量较高时EfficientNet_B0约5.3M算力友好细节保持好硬件资源有限MobileNetV3_Large约5.4M精度略低推理快后续要部署到移动端在茶叶叶片病害图像分类任务上我的默认起点是ResNet18。它不算先进但在这个数据规模下参数量和拟合能力刚好匹配单卡训练快复现成本低。如果你发现某个病害类别总学不好再升级到ResNet34或EfficientNet_B0通常比直接上Transformer更有效。3.2 DataLoader与数据增强参数让有限样本安全膨胀数据增强是4,000张数据集训练成败的分水岭。茶叶病害图像有个特点病斑纹理集中在叶片局部与背景、叶脉的区分度依赖细节。增强太弱会过拟合增强太猛会把病斑特征裁掉。from torchvision import transforms as T train_tf T.Compose([ T.RandomResizedCrop(224, scale(0.5, 1.0), ratio(0.75, 1.33)), T.RandomHorizontalFlip(p0.5), T.RandomRotation(degrees30, fill(255, 255, 255), interpolationT.InterpolationMode.BILINEAR), T.ColorJitter(brightness0.3, contrast0.3, saturation0.3), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_tf T.Compose([ T.Resize(256), T.CenterCrop(224), T.ToTensor(), T.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])参数说明里有三个关键选择。scale(0.5, 1.0)而不是PyTorch默认的(0.08, 1.0)因为茶叶病斑是局部特征裁到0.08的尺度很可能只截到背景或叶缘病斑信息直接丢失。RandomRotation的fill(255, 255, 255)把旋转产生的空白填成白色叶片图像背景多为白纸或浅色膜填白比填黑的语义更接近真实分布。ColorJitter三个通道都设0.3模拟田间光照变化如果原始图像已经过曝严重可以调低到0.2。验证集不做随机增强只用Resize(256) CenterCrop(224)保证评估稳定。DataLoader里我通常设batch_size32、num_workers4、pin_memoryTrue显存不够时先降batch而不是降分辨率。3.3 训练骨架代码冻结Backbone与解冻微调的节奏迁移学习最常见的翻车方式是一开始就全量微调随机初始化的分类头在反向传播时产生大梯度把ImageNet预训练特征破坏掉。我在4,000张规模下习惯两阶段训练先冻结backbone训练分类头再解冻全量微调。import torch import torch.nn as nn from torchvision import models model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, 8) # 类别数按实际数据修改 # 阶段一冻结backbone只训练fc for p in model.parameters(): p.requires_grad False for p in model.fc.parameters(): p.requires_grad True optimizer torch.optim.AdamW(model.fc.parameters(), lr1e-3, weight_decay1e-4) for epoch in range(5): train_one_epoch(model, train_loader, optimizer, criterion) # 阶段二解冻全部参数低学习率微调 for p in model.parameters(): p.requires_grad True optimizer torch.optim.AdamW(model.parameters(), lr3e-5, weight_decay1e-4)逻辑说明阶段一的5个epoch相当于让新分类头在ImageNet特征上“暖身”此时backbone参数不动梯度不会反向破坏预训练特征。阶段二切换到3e-5这个低学习率是迁移学习里的常用值backbone已经收敛学习率过大会让权重更新幅度过大导致loss在收敛点附近震荡。为什么不用SGD在小数据集上SGD配合momentum也完全可以但AdamW对学习率的敏感度低不用花大量时间调momentum和lr decay策略对于“先跑通再调优”的节奏更友好。等baseline稳定后想追求更高精度再换SGDmomentum不迟。4. 茶叶病害图像分类训练参数五个搞定过拟合的关键设定模型骨架搭好后训练参数的设定决定了这个模型是“记住4,000张图”还是“学会看病斑”。这一章不讲玄学只讲我在茶叶这类中小规模图像分类任务上反复验证过的五组参数。4.1 学习率调度与warmup小数据集最怕冷启动小数据集的训练早期非常脆弱。warmup阶段用低学习率起步让优化器统计量稳定下来再进入正常学习率否则前几个epoch loss可能剧烈震荡尤其是阶段二解冻backbone时。from torch.optim.lr_scheduler import LinearLR, CosineAnnealingLR, SequentialLR warmup LinearLR(optimizer, start_factor0.05, total_iters5) cosine CosineAnnealingLR(optimizer, T_max40, eta_min1e-6) scheduler SequentialLR( optimizer, schedulers[warmup, cosine], milestones[5] )参数说明start_factor0.05表示warmup第一个epoch只用最终学习率的5%total_iters5对应5个epoch的warmup长度T_max40是余弦退火的总长度我这里按阶段二的训练轮数设eta_min1e-6防止学习率衰减到0导致后期完全不动。4.2 标签平滑与MixUp给4,000张的标注噪声留容错空间茶叶叶片病害的标注存在天然的模糊地带同一病害的不同发病阶段病斑形态差异很大早期病斑和晚期病斑的边界可能只是颜色深浅不同。这种标注噪声下模型如果对硬标签过分自信会把噪声样本也牢牢记住。标签平滑直接把硬标签变成软标签正确类别概率乘(1 - label_smoothing)其余概率均匀分给所有类别。我在这个任务上默认label_smoothing0.1类别数在8到10类时这个值不会让损失值失真。MixUp是对输入空间做插值mixup_alpha 0.2 lam np.random.beta(mixup_alpha, mixup_alpha) index torch.randperm(batch_size).to(device) mixed_x lam * x (1 - lam) * x[index] loss lam * criterion(logits, y) (1 - lam) * criterion(logits, y[index])逻辑说明每次迭代随机取batch内另一张图的索引按lam比例混合两张输入损失也按相同比例混合。mixup_alpha0.2的beta分布形状比较偏0和1生成的lam要么接近1要么接近0混血程度不会过于激进。对茶叶病害图MixUp的额外好处是让模型学会在病斑与健康叶特征之间平滑插值而不是死记硬背某个病斑的精确位置。4.3 早停与最优模型选择用macro-F1做守门员小数据集训练后期验证集准确率可能在一个高点附近反复横跳。保存模型不能只看总准确率因为总量大的“健康叶”类会掩盖少数病害类别的糟糕表现。我用macro-F1作为保存模型的守门指标同时设置patience早停防止无效训练。# 每个epoch结束时 if macro_f1 best_f1: best_f1 macro_f1 torch.save({ model: model.state_dict(), optimizer: optimizer.state_dict(), epoch: epoch, macro_f1: macro_f1, }, best_model.pt) # 连续10个epoch macro_f1不上升就停 if epoch - best_epoch 10: print(early stop at epoch, epoch) break下面是我在4,000张茶叶叶片病害数据上常用的训练参数表可以直接抄去当起点再根据实际loss曲线调整参数推荐值说明输入分辨率224病斑小则用384优先保证病斑细节batch_size32显存不足优先降batch分类头学习率1e-3阶段一使用全量微调学习率3e-5阶段二使用weight_decay1e-4L2正则label_smoothing0.1应对标注噪声mixup_alpha0.2输入空间正则warmup_epochs5线性warmuppatience10早停轮数保存指标macro-F1不被大类主导5. 茶叶叶片病害图像分类踩坑记录标注噪声、分辨率与验证集划分的四类问题模型跑起来只是开始真正的血泪经验都在数据层面。下面四条坑是我在中小规模图像分类任务上反复遇到的每一条都按“现象、原因、解决”的顺序写清楚。5.1 同一种病斑被标成两类标注标准不统一的后果现象训练时某一对类别的准确率始终在50%附近震荡预测结果在这两类之间随机横跳。原因标注人员把同一病害的不同发病阶段当成两个不同类别或者把两种外观极相似的病害混标。茶叶病害里茶饼病和茶叶白星病早期病斑都呈白色小点如果不做标注规范这两类就成了解不开的结。解决训练前先做一次标签清洗把易混淆类别的样本并排贴出来人工复核更高效的做法是用预训练模型提取特征做t-SNE降维重叠严重的类别优先合并或细分。5.2 叶片反光与缩放过猛病害纹理被吃掉的根源现象训练loss正常下降但验证集里反光叶片上的病斑总是漏检。原因叶片表面的蜡质层在强光下形成高光区域把病斑纹理直接淹没再加上RandomResizedCrop裁剪尺度太大病斑被裁到画面之外。解决数据增强里把ColorJitter的brightness从0.3降到0.1同时在RandomResizedCrop的scale下限设0.5如果原图分辨率足够直接把输入分辨率提到384让模型有更多像素去看病斑纹理。5.3 验证集指标虚高按拍摄批次与地块划分数据现象验证集准确率0.95模型一拿到现场新拍的照片就垮小病害类别几乎全错。原因数据集按随机比例切分train/val时同一片叶子、同一批拍摄条件下的多张照片被同时分进训练集和验证集相当于验证集泄漏了训练集的拍摄环境信息。解决按拍摄批次或叶片个体分组划分数据保证同一个叶片的多个视角只出现在训练集或验证集一侧。代码上可以把每个样本的group_id传入GroupKFold这是比随机划分更诚实的评估方式。5.4 类别不平衡总准确率高但某一病害全猜错现象整体准确率85%但炭疽病的F1不到0.4。原因健康叶样本占绝大多数模型把一切不确定的样本都倾向预测成健康叶总准确率被大类抬高小类的错误被隐藏。解决评估指标改成balanced accuracy或macro-F1训练时对少数类启用WeightedRandomSampler或改用Focal Loss数据侧可以针对少数类做同一叶片的多角度、多尺度扩图增加特征多样性。6. 训练之后先别急着交付混淆矩阵与CAM可视化验证模型学到了什么最后一个习惯也是我踩过坑之后强制自己养成的模型训练完先画混淆矩阵和CAM热力图再谈部署和交付。总准确率只是一个数字它无法告诉你模型到底在看叶片上的病斑还是在看叶片边缘的锯齿、背景里的土壤颜色。混淆矩阵能快速暴露系统性错误。用sklearn.metrics.confusion_matrix和matplotlib画出来按行归一化后每一行代表真实类别的召回情况。如果某个病斑类别大量被预测成健康叶说明这个类别的特征没有被充分学到如果两类病害互相混淆说明它们视觉上确实接近应该回头去看标注。import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay cm confusion_matrix(y_true, y_pred, labelsclass_names) disp ConfusionMatrixDisplay(confusion_matrixcm, display_labelsclass_names) disp.plot(cmapBlues) plt.xticks(rotation45) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150)CAM可视化用来回答“模型看哪里”。torchcam库封装了常用的CAM变体也可以用Grad-CAM手动实现取模型最后一个卷积层的梯度做全局平均池化得到每个通道的权重再与特征图加权求和并上采样回原图尺寸。把热力图叠在原图上正常情况是病斑区域发亮如果高亮区域集中在叶片反光、叶脉或背景上说明模型学到的特征不对即使准确率好看也是假象。我现在的习惯是每次训练结束固定生成四张可视化总体混淆矩阵、每一类的样本召回图、两张典型的CAM热力图叠加图、以及一张错误样本的九宫格拼图。这套流程跑完才敢说这个模型真的能用于田间场景。以前我也跳过这些步骤直接看准确率后来发现准确率会骗人可视化不会。希望帮到你。本文还有配套的精品资源点击获取