简介:常规茶叶叶片病害图像分类数据集,主要面向图像分类初学者、农业AI研究人员及茶叶植保相关开发者,可用于茶叶褐枯病、灰枯萎病、红点病等5类常见叶部病害的识别模型训练与算法验证。数据集约4000张已标注图片,按训练集、验证集、测试集分别存放,各文件夹内为同类别图像,json文件提供具体类别映射,便于直接开展模型训练与评估。压缩包共2000个文件,以1998张jpg图像为主体,另含1个py可视化脚本与1个json标签文件,整体大小21.68MB,结构清晰且占用空间小,适合快速下载使用。资源内附带show.py可视化脚本,能够快速预览各类样本并核对标注质量与类别分布。目前已有105人学习浏览,适合作为图像分类实战、CNN网络改进实验以及农业病害检测项目的基础数据集。
1. 常规茶叶叶片病害图像分类数据集:4,000张已标注图片到底够不够用
做茶叶病害识别的人,多半卡在同一个地方:不是算法不会写,而是手里没有一份像样的、已标注的图像分类数据集。你自己去茶园拍,拍完还要请植保专家逐张打标签,时间成本高到离谱。这份“常规茶叶叶片病害图像分类数据集【已标注,约4,000张数据】”解决的就是这个痛点——把常见的茶叶病害图片按类别整理好,直接拿来训练图像分类模型。但我也要泼一盆冷水:4,000张对于深度学习图像分类来说,属于“小数据集”,它够不够用,完全取决于你怎么划分数据、怎么做增强、怎么调训练参数。本文就按实战路径,从数据检查、模型训练到避坑,把这条路走通。
先说结论:4,000张已标注图片,如果类别在5到8类之间,每类大约500到800张,配合迁移学习和数据增强,足以训练出一个在茶园场景下可用的分类器;但如果你一上来就自己写一个ResNet从零训练,那大概率过拟合。接下来我会按照“数据集检查 → 训练基线 → 数据增强 → 评估优化”的顺序,带你把每一步踩实。
2. 拆开这份数据集:类别构成、标注格式与图像质量检查
拿到任何数据集,第一件事不是训练,而是先看清它的“长相”。图像分类数据集的标注一般有两种形式:一种是文件夹名即类别名,图片按类别归入不同文件夹;另一种是单独的CSV/JSON文件,记录每张图片的文件名和标签。这份茶叶叶片病害数据集以类别文件夹组织最为常见,标注信息通常内嵌在路径里。先去根目录看一眼,别急着写训练脚本。
2.1 先看目录结构和标注文件,别急着训练
常见做法是解压后先执行下面的命令,把目录层级列出来。我一般会用tree或find,在Windows上则是dir /s。先确认顶层有几个文件夹,每个文件夹里有多少张图。
# 查看数据集目录层级,限定两层深度,避免刷屏 tree -L 2 /path/to/tea_leaf_dataset/ # 统计每个类别文件夹下的图片数量 for dir in /path/to/tea_leaf_dataset/*/; do echo "$(basename "$dir"): $(find "$dir" -type f | wc -l)" done这个命令的逻辑很简单:外层循环遍历每个子目录,basename取出类别名,find统计该目录下所有文件数量。如果你看到某类只有几十张,而其他类有上千张,那说明样本不均衡,后面训练时要给少数类更高的权重。另外,注意图片格式是不是统一,常见的是.jpg,偶尔夹杂.png和.bmp,这会影响读取效率。
还有一种情况是数据集提供了train.txt/val.txt这类清单文件。这时要检查两件事:文件路径是否相对路径,以及路径分隔符是/还是\——在Windows上解压后直接跑Linux训练脚本,经常因为这个翻车。建议先用head -5 train.txt看一眼格式,再用wc -l确认行数。
2.2 用脚本统计类别分布和图像尺寸
光看文件夹数量不够,还得知道每个类别到底有多少张、图像分辨率是不是统一。分辨率差异太大会给后面的随机裁剪带来麻烦。我习惯写个小Python脚本,用PIL读出每张图的宽高,顺便统计类别分布。
import os from PIL import Image from collections import Counter root = '/path/to/tea_leaf_dataset' sizes = Counter() labels = Counter() for label in os.listdir(root): label_dir = os.path.join(root, label) if not os.path.isdir(label_dir): continue for fname in os.listdir(label_dir): if not fname.lower().endswith(('.jpg', '.png', '.jpeg')): continue labels[label] += 1 with Image.open(os.path.join(label_dir, fname)) as img: sizes[(img.width, img.height)] += 1 print('类别分布:', labels) print('尺寸分布(前10):', sizes.most_common(10))这里用Counter统计,效率比手动字典高。labels会输出每个类别的图片数,sizes输出最常见的10种分辨率。如果发现分辨率只有两种,比如 600x600 和 1200x1200,那可能是采集相机焦距不同导致,训练时统一缩放即可;如果出现几十种奇葩尺寸,说明数据集没做预处理,后续要写统一的resize逻辑。
2.3 图像质量检查:模糊、过曝、重复样本一眼识别
很多人忽略这一步,直到训练出的模型在真实茶园里识别率崩了,才回头找原因。茶叶叶片病害图像里,最常见的质量问题是:失焦模糊、叶片反光过曝、以及同一片叶子被重复拍摄多次被当成多个样本。重复样本会导致训练集和验证集“串通”,让验证准确率虚高,实际应用却惨不忍睹。
检查模糊和过曝可以用OpenCV直接算指标:
import cv2 import numpy as np import os def check_image_quality(path): img = cv2.imread(path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 拉普拉斯方差:值越小越模糊 laplacian_var = cv2.Laplacian(gray, cv2.CV_64F).var() # 过曝比例:像素值 > 250 的比例 overexposed = np.mean(gray > 250) return laplacian_var, overexposed # 遍历所有图片,把可疑样本打印出来 for root, dirs, files in os.walk('/path/to/tea_leaf_dataset'): for f in files: if f.endswith('.jpg'): p = os.path.join(root, f) lv, oe = check_image_quality(p) if lv < 30 or oe > 0.3: print(f'{p}: blur_var={lv:.1f}, overexposed_ratio={oe:.2f}')拉普拉斯方差低于30的图像,人眼可能看着还行,但卷积神经网络能提取的边缘信息已经很少。过曝比例超过0.3,意味着叶片高光区域几乎全白,病斑纹理全丢了。这类样本要么直接删掉,要么在训练时用色彩抖动增强来弥补。重复样本可以通过计算感知哈希来检测,但这不是当前重点,先记住:统计完类别分布和图像质量后,再进入训练阶段才算稳妥。
3. 用这份数据集训练图像分类模型:从ResNet到最新的图像分类模型
数据集检查完之后,就可以搭建训练流程了。图像分类算法目前的主流选择依然是卷积神经网络和视觉Transformer(ViT),但在4,000张这种规模下,ViT容易过拟合,ResNet系列反而是最稳的起点。这里我说的“最新的图像分类模型”并不是让你直接上Swin Transformer,而是建议你用“最新的训练技巧”去微调一个ResNet,比如EMA、Mixup、Cosine Schedule。先跑通一个基线再说。
3.1 准备数据集:按ImageNet风格整理文件夹
PyTorch的torchvision.datasets.ImageFolder可以直接读取按类别分文件夹的数据集,前提是目录结构必须是root/class_name/image.jpg。刚才你已经确认过这份数据基本符合这个结构,所以只需把路径指过去。
如果你拿到的标注是CSV,那么需要先转换为ImageFolder风格。这里给一个转换脚本模板:
import pandas as pd import shutil import os # 假设csv列名: filename, label df = pd.read_csv('annotations.csv') for _, row in df.iterrows(): src = os.path.join('raw_images', row['filename']) dst_dir = os.path.join('dataset', row['label']) os.makedirs(dst_dir, exist_ok=True) shutil.copy(src, os.path.join(dst_dir, row['filename']))这段代码按标签建子目录,再把图片复制过去。注意shutil.copy是复制,如果硬盘空间紧张可以改成os.rename,但前提是原图不再需要保留。转换完成后,用ImageFolder加载时,它会自动根据文件夹名生成类别索引,不需要你手动维护字典。
3.2 训练基线模型:ResNet18命令与参数
我习惯先用ResNet18跑一个快速基线,看数据本身能学到什么程度。以下是一个极简但完整的PyTorch训练脚本骨架,包括数据划分、归一化、训练和验证循环。
import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms, models from torch.utils.data import DataLoader, random_split # 数据增强和归一化,ImageNet统计量 transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) dataset = datasets.ImageFolder('/path/to/tea_leaf_dataset', transform=transform) # 按8:2划分训练和验证集 train_size = int(0.8 * len(dataset)) val_size = len(dataset) - train_size train_set, val_set = random_split(dataset, [train_size, val_size]) train_loader = DataLoader(train_set, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_set, batch_size=32, shuffle=False, num_workers=4) model = models.resnet18(pretrained=True) # 替换最后一层为实际类别数 num_classes = len(dataset.classes) model.fc = nn.Linear(model.fc.in_features, num_classes) device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model.to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-4) for epoch in range(30): model.train() running_loss = 0.0 for inputs, labels in train_loader: inputs, labels = inputs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * inputs.size(0) # 验证 model.eval() correct = 0 total = 0 with torch.no_grad(): for inputs, labels in val_loader: inputs, labels = inputs.to(device), labels.to(device) outputs = model(inputs) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() print(f'Epoch {epoch+1}, Loss: {running_loss/len(train_set):.4f}, Val Acc: {correct/total:.4f}')这里有几个参数值得说明:lr=1e-4是针对迁移学习比较保守的选择,因为预训练权重已经很好,学习率太大会把原来学到的特征破坏掉;batch_size=32在4,000张数据量下足够稳定,显存不够就降到16;num_workers=4看CPU核心数,Windows上建议设为0,否则会报错。ResNet18的pretrained=True会下载在ImageNet上预训练的权重,第一次运行需要联网。
3.3 用迁移学习微调,而不是从零训练
很多人以为“用自己的数据集训练”就是要从随机初始化开始,这是大误区。4,000张图片根本养不起一个深层网络,正确做法是加载ImageNet预训练权重,然后微调。上面代码里我们已经用了pretrained=True,但还有一个改进点:可以冻结前面的卷积层,只训最后一层,先把线性分类器训到收敛,再解冻所有层用更小的学习率微调。
# 先冻结所有层 for param in model.parameters(): param.requires_grad = False # 只让最后一层可训练 for param in model.fc.parameters(): param.requires_grad = True # 第一轮训练只优化fc层 optimizer = optim.Adam(model.fc.parameters(), lr=1e-3) # 训练10轮后,解冻全部层 for param in model.parameters(): param.requires_grad = True optimizer = optim.Adam(model.parameters(), lr=1e-4)这个两阶段策略在数据量少时特别有效。先用较大的学习率让分类器适应茶叶特征,再用小学习率微调整个网络,能避免一开始就破坏预训练特征。我在实际项目中,这个操作通常能让验证准确率提升3到5个百分点。注意两个阶段的epoch分配:冻结阶段5到10轮足够,解冻阶段可以跑20到30轮,并配合余弦退火学习率。
4. 标注质量避坑:4,000张数据里的常见标注问题与排查方法
小数据集最怕的不是模型不好,而是标注里有坑。很多公开数据集号称“已标注”,但实际用起来你会发现各种问题。以下是这个数据集场景里最常见的4个坑,按“现象 → 原因 → 解决”的方式说清楚。
4.1 现象:验证集准确率虚高,但实际测试很差
如果你发现训练时验证准确率轻松超过99%,但拿到茶园里拍几张真实照片一试,识别结果一塌糊涂,那大概率是数据划分出问题了。最常见原因是图片排序导致同一个植株或同一批采集的叶片全部分到了同一个split里——因为random_split默认是随机划分,但如果数据集本身按文件名排序,而且连续几百张都是一个时间点拍的,随机划分也可能把相关样本拆到两边,造成信息泄漏。
解决方法是按照文件名或采集时间做分组划分。比如文件名前缀可能是date_field_001.jpg,那就用前缀作为分组键,确保同一个日期或同一块田里的样本不会同时出现在训练集和验证集。
import os from collections import defaultdict from sklearn.model_selection import GroupShuffleSplit # 假设文件名格式: 20240501_tea_leaf_001.jpg groups = [] files = [] for label in os.listdir(root): label_dir = os.path.join(root, label) for fname in os.listdir(label_dir): date = fname.split('_')[0] # 按日期分组 groups.append(date) files.append(os.path.join(label_dir, fname)) gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, val_idx = next(gss.split(files, groups=groups))这样划分后,同一日期采集的所有照片都在同一侧,模型看到的验证样本与训练样本在时间上分离,准确率才是真实水平。虽然这么做会让验证准确率下降一点,但心里有底。
4.2 现象:少数类别准确率特别低,甚至被完全吞掉
茶叶病害数据集中,健康叶片往往占大头,某种罕见病害可能只有100来张。训练时模型为了降低总体loss,会偏向多数类,少数类几乎不学。这时候你看整体准确率可能还行,但每一类的混淆矩阵里,罕见类几乎全被预测成健康叶片。
解决方式有两步:第一步,计算类别权重,在损失函数中给少数类加权;第二步,针对少数类做更强的数据增强。具体在PyTorch里这样实现:
from sklearn.utils.class_weight import compute_class_weight import numpy as np # 假设有train_labels列表 class_weights = compute_class_weight('balanced', classes=np.unique(train_labels), y=train_labels) class_weights = torch.tensor(class_weights, dtype=torch.float).to(device) criterion = nn.CrossEntropyLoss(weight=class_weights)compute_class_weight会自动把少数类权重调高,比如某个类只有100张,另一个类有1000张,那前者的权重是后者的10倍。注意,加权重后学习率可能要调低一点,否则少数类梯度太大,容易震荡。另外,如果你用了Mixup增强,建议谨慎加权重,因为Mixup本身就会混合标签,权重相互影响。
4.3 现象:标注文件里的类别名和图片内容对不上
这是公开数据集最常见的“玄学”问题。你打开一个叫leaf_blight的文件夹,里面可能混着几张锈病或者虫害咬痕的图。原因很简单:标注工作量大,标注员偶尔手滑,或者图片本身就具有迷惑性。如果你不做清洗,模型会把两个不同病害当成同一类,或者被噪声样本带偏。
排查方法很直接:每类随机抽9张图,拼成一张网格图,人眼快速浏览。我用一个简单脚本生成网格图:
import matplotlib.pyplot as plt from PIL import Image import os, random def show_grid(class_dir, grid_size=9): files = [os.path.join(class_dir, f) for f in os.listdir(class_dir) if f.endswith(('.jpg', '.png'))] sample = random.sample(files, min(grid_size, len(files))) fig, axes = plt.subplots(3, 3, figsize=(9, 9)) for ax, img_path in zip(axes.flatten(), sample): ax.imshow(Image.open(img_path)) ax.axis('off') plt.show() # 对每个类别目录调用 show_grid for class_name in os.listdir(root): show_grid(os.path.join(root, class_name))一旦发现某张图与其他同类差异过大,直接删掉或者移动到废弃文件夹。4,000张数据删掉二三十张噪声,换来模型精度提升,非常划算。不要舍不得删,那些图留着只会让决策边界更混乱。
5. 数据增强与训练策略:让4,000张数据发挥出更大价值
在数据量有限的情况下,数据增强不是锦上添花,而是必需品。但增强策略要结合茶叶叶片特点来设计,不能搬一套ImageNet的增强就打发了。比如叶片病害识别,病斑的形状、颜色是关键特征,色彩抖动幅度不能太大,否则病斑颜色变了,模型就学错了。
5.1 增强策略:随机裁剪、翻转、色彩抖动
我常用的增强组合如下。随机裁剪模拟拍摄时不同距离和角度,左右翻转模拟叶片正反面,色彩抖动模拟不同光照下叶色变化。注意不要用太强的旋转,因为茶叶病害图一般有固定的叶片朝向,超过30度的旋转会引入不真实的视角。
import torchvision.transforms as transforms train_transform = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale=(0.6, 1.0)), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.05), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])这里的RandomResizedCrop的scale参数下限设到0.6,意味着裁剪区域最小是原图的60%,这样能保留完整叶片的主要病斑区域,不会裁剪到只剩下叶脉。hue色相调整设0.05,变化非常小,因为茶叶病害的颜色色调是重要判别特征,调太多会让模型学习到不存在的色相变化。训练时,验证集不能加这些随机增强,只能使用Resize和Normalize。
5.2 训练参数建议:学习率、batch size、epoch
在小数据集上训练,参数设定有默认经验值,但你需要根据loss曲线微调。首先是学习率,迁移学习建议1e-4到3e-4,如果从头训练要1e-3起步。其次batch size,4,000张数据32到64都合适,但batch size越大,梯度估计越稳定,可学习率要相应降低。epoch方面,冻结层训练阶段5到8轮,解冻后20到30轮,总共不超过40轮。
另外强烈建议加入学习率调度器,使用余弦退火比固定学习率收敛更好。这里给一个带余弦退火的PyTorch写法:
from torch.optim.lr_scheduler import CosineAnnealingLR optimizer = optim.Adam(model.parameters(), lr=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=30, eta_min=1e-6) for epoch in range(30): # 训练代码... scheduler.step()T_max设为总epoch数,eta_min是最小学习率。这种调度方式会在训练后期把学习率降到非常低,有助于模型落入更平缓的损失谷底,泛化性能更好。很多开源代码里已经默认用CosineAnnealingLR,你不需要自己纠结什么时候手动调学习率。
5.3 如何评估模型:混淆矩阵和单类精确率召回率
只看准确率会掩盖问题。尤其是不均衡的数据集,整体准确率99%可能只是把所有样本都预测成多数类。正确评估方式是看每个类别的精确率(Precision)和召回率(Recall),以及混淆矩阵。
from sklearn.metrics import classification_report, confusion_matrix import numpy as np all_preds = [] all_labels = [] with torch.no_grad(): model.eval() for inputs, labels in val_loader: inputs, labels = inputs.to(device), labels.to(device) outputs = model(inputs) _, preds = torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) print(classification_report(all_labels, all_preds, target_names=dataset.classes)) print(confusion_matrix(all_labels, all_preds))classification_report会输出每一类的精确率、召回率和F1分数,这是判断模型是否真正学会识别每一种病害的关键。如果某一类召回率只有0.4,说明这类病害大量被漏判,回去检查训练样本和增强策略是否有偏差。混淆矩阵能直观看到哪两类容易互相混淆,比如茶饼病和炭疽病症状接近,模型可能会搞混,这时你就要考虑是否给这两个类别增加更多有区分度的训练样本,或者结合叶背面的特征图片。
6. 进阶用法:用交叉验证替代固定划分,并导出ONNX部署
4,000张数据的小样本场景,固定的一次性划分结果受运气影响很大。你可能这次划分验证集准确率是92%,换个随机种子变成88%,根本不知道模型的真实水平。我建议用5折交叉验证来更可靠地评估模型,同时把训练好的模型导出成ONNX,方便部署到手机或边缘设备。
6.1 用交叉验证替代固定划分
交叉验证的做法是把数据集分成5份,轮流取4份训练、1份验证,最后综合5次的平均准确率和标准差。以下是用KFold实现的关键代码:
from sklearn.model_selection import KFold kf = KFold(n_splits=5, shuffle=True, random_state=42) fold_accs = [] for fold, (train_idx, val_idx) in enumerate(kf.split(dataset)): train_subset = torch.utils.data.Subset(dataset, train_idx) val_subset = torch.utils.data.Subset(dataset, val_idx) # 这里复用前面定义的train_transform和val_transform # 重新训练并记录验证准确率 acc = train_one_fold(train_subset, val_subset) fold_accs.append(acc) print(f'Fold {fold+1}: {acc:.4f}') print(f'平均准确率: {np.mean(fold_accs):.4f} ± {np.std(fold_accs):.4f}')注意KFold默认的shuffle=True能避免顺序偏差,但如果你担心信息泄漏,可以换成GroupKFold,配合4.1节里的按日期分组逻辑。交叉验证的代价是训练时间变成5倍,但4,000张数据量小,ResNet18每折30轮用GPU也就几分钟,值得做。
6.2 模型压缩和导出ONNX
训练好的PyTorch模型想要部署到生产环境,一般先转成ONNX格式,再交给TensorRT或OpenVINO推理。导出ONNX很简单,但有坑:模型必须处于eval模式,并且输入张量尺寸要固定。
import torch.onnx model.eval() dummy_input = torch.randn(1, 3, 224, 224).to(device) torch.onnx.export( model, dummy_input, 'tea_leaf_model.onnx', input_names=['input'], output_names=['output'], dynamic_axes={'input': {0: 'batch_size'}, 'output': {0: 'batch_size'}}, opset_version=11 )dynamic_axes允许在推理时动态改变batch大小,方便服务器端批量推理。opset_version=11是兼容性比较高的设置,某些新算子可能需要更高的opset,但对ResNet来说11足够。导出后,建议用onnxruntime加载并和PyTorch输出对比,确保数值误差在可接受范围内。
6.3 我的习惯和教训
我做过几个植物病害识别的小项目,最大的教训就是:不要迷信准确率,一定要在真实场景里拍几十张新照片来测试。模型在验证集上可能95%,但到实际茶园里,因为背景杂乱、光照不均、叶片背面没拍到,准确率可能直接掉到80%以下。所以我现在的习惯是,每次训练完都会挑3到5张没有出现在数据集里的实拍照片,单独跑一次推理,看看概率分布是否合理。如果模型对某个类别的置信度总是徘徊在0.5以下,说明特征没学好,我会回到数据增强这一步,而不是继续调学习率。
还有一个参数容易忽略:推理时的预处理必须和训练时的验证集完全一致,包括Resize方式、归一化均值和标准差。我见过有人训练用Resize((224,224)),部署时直接读了原图没resize,结果自然是错的。把这些环节都跑通后,这份4,000张的茶叶病害数据集才算真正被吃透了——从数据检查到模型训练,再到导出部署,每一步都留痕,换到其他叶片病害数据集上,你也能复用这套流水线。希望帮到你。
本文还有配套的精品资源,点击获取