
简介这份资源面向从事图像分类、深度学习入门与植物识别研究的开发者与学习者提供一套已完成划分的大型34类植物叶片图像分类数据集可直接用于模型训练与算法验证省去自行采集与清洗数据的成本。压缩包共约2000个文件以1998张jpeg图像为主另含1个py可视化脚本与1个json类别字典整体约508.58MB采用7z格式打包。数据按文件夹组织data目录下分为train与test两部分训练集27,346张、测试集6,654张覆盖苹果、葡萄、猕猴桃等34个植物叶片类别可直接用ImageFolder加载无需额外处理也能作为yolov5的分类数据集使用。随包提供的可视化脚本无需修改即可运行随机传入4张图片便能展示效果并保存到当前目录方便快速核验数据质量。目前已有179人学习下载适合需要现成分类数据开展实验、课程作业或模型对比的读者。1. 拿到一个 34 类植物叶片数据集先别急着喂给模型植物叶片图像分类这件事很多人第一次接触是在课程作业或者一个跨平台识别 Demo 里。你手里拿到的这份「大型 34 植物叶片图像分类数据集已做数据集划分」本质上解决的是一个非常具体的痛点你不需要再自己写脚本按比例切分训练集、验证集和测试集也不用担心类别不均衡导致某几类叶片被模型彻底忽略。它把 34 个植物类别、已经划分好的目录结构直接摆在你面前你打开就能跑。但这里有个反直觉的结论数据集划分好了不代表你就能直接拿到高准确率。叶片图像有几个天然难点——背景杂乱土壤、手指、阴影、类间差异小同科植物叶片形状接近、类内差异大同一株植物不同角度、不同光照。如果你上来就ImageFolder一把梭跑完发现验证集准确率卡在 60% 上下大概率不是模型不行而是预处理和增强策略没跟上。这篇文章面向的是已经拿到这份数据集、准备做图像分类落地的人我会从目录结构检查、数据增强、模型选型、训练参数到避坑一步步拆开讲。2. 先摸清目录结构34 类叶片数据集的划分逻辑与加载方式2.1 已划分数据集常见的三种目录形态「已做数据集划分」这句话在不同来源里含义不一样。我见过的大多数植物叶片数据集划分后的目录结构逃不出下面三种第一种是train/val/test三个平级目录每个目录下再按类别分子目录。这是最标准的ImageFolder友好结构PyTorch 和 TensorFlow 都能直接读。第二种是train/val两个目录测试集单独放在test或者干脆没有测试集只有val充当验证。这种情况你需要自己从train里再切一小部分出来做测试或者把val当测试用但要注意这样就没有独立的验证集来调早停。第三种是扁平结构所有图片放在一个目录划分信息写在train.txt、val.txt、test.txt三个文本文件里每行是图片路径 类别索引。这种结构在学术界常见但用ImageFolder读不了需要自定义Dataset。我一般拿到数据集先跑一段脚本把目录树和类别数打出来确认没有隐藏文件、没有空目录、没有类别名带空格或中文的情况。下面这段代码就是干这个的import os from pathlib import Path def inspect_dataset(root): root Path(root) # 打印一级目录 print(一级目录:, [p.name for p in root.iterdir() if p.is_dir()]) for split in [train, val, test]: split_dir root / split if not split_dir.exists(): print(f{split}: 不存在) continue classes sorted([d.name for d in split_dir.iterdir() if d.is_dir()]) # 统计每个类别的图片数量 counts {} for c in classes: exts {.jpg, .jpeg, .png, .bmp, .tif, .tiff} n sum(1 for f in (split_dir / c).iterdir() if f.suffix.lower() in exts) counts[c] n total sum(counts.values()) print(f{split}: {len(classes)} 类, {total} 张) # 打印最少和最多的类别检查是否严重不均衡 if counts: min_c min(counts, keycounts.get) max_c max(counts, keycounts.get) print(f 最少: {min_c}({counts[min_c]}), 最多: {max_c}({counts[max_c]})) inspect_dataset(./plant_leaf_34)这段脚本的逻辑很直白先看一级目录有哪些再分别统计train/val/test下的类别数和图片总数最后把最少和最多的类别打出来。参数说明root换成你实际解压后的路径exts集合里我加了.tif因为部分植物叶片数据集用 TIFF 存高分辨率图如果你确定只有 JPG可以删掉。跑完如果发现某个 split 下类别数不是 34或者某个类别只有个位数图片那就要警惕了——要么是解压不完整要么是原始数据本身就不均衡。2.2 用 ImageFolder 加载并验证类别映射确认目录结构没问题后加载就很简单了。但这里有个细节ImageFolder的类别索引是按文件夹名字母序排的不是按数字顺序。如果你后面要输出混淆矩阵或者做类别名映射必须把class_to_idx存下来。from torchvision import datasets, transforms # 基础变换只做 resize 和 tensor 转换用于验证集和测试集 base_tf transforms.Compose([ transforms.Resize((224, 224)), # 统一到 224适配大多数 CNN transforms.ToTensor(), ]) train_ds datasets.ImageFolder(./plant_leaf_34/train, transformbase_tf) val_ds datasets.ImageFolder(./plant_leaf_34/val, transformbase_tf) test_ds datasets.ImageFolder(./plant_leaf_34/test, transformbase_tf) print(类别数:, len(train_ds.classes)) print(类别到索引:, train_ds.class_to_idx) print(训练集样本数:, len(train_ds))逻辑说明这里故意没有在train_ds上加数据增强因为增强要单独写一个 transform避免验证集和测试集也被随机翻转、裁剪。参数说明Resize((224, 224))是 ImageNet 预训练模型的标配输入尺寸如果你用 EfficientNet-B0 可以改成 224用 B3 改成 300用 ViT-B/16 也是 224。class_to_idx一定要打印出来存成 JSON后面推理时类别名对不上准确率再高也没法解释。注意如果val_ds.classes和train_ds.classes顺序不一致说明某个 split 下缺少了某个类别的文件夹。这种情况ImageFolder不会报错但训练时标签会错位必须手动对齐。3. 叶片图像的数据增强别把叶子转没了3.1 为什么叶片分类的增强策略和通用图像不一样通用图像分类的增强套路是随机裁剪、随机翻转、颜色抖动、旋转。但叶片图像有几个特殊性旋转不变性很强叶子转 90 度还是叶子颜色是重要判别特征病斑颜色、叶脉颜色背景干扰大土壤、手指、阴影。如果你直接套用RandomResizedCrop加ColorJitter可能会把叶片的关键区域裁掉或者把病斑的颜色抖没了。我一般会这样配from torchvision import transforms train_tf transforms.Compose([ transforms.Resize((256, 256)), # 先放大一点给随机裁剪留空间 transforms.RandomResizedCrop(224, scale(0.7, 1.0)), # 裁剪比例别太狠 transforms.RandomHorizontalFlip(p0.5), transforms.RandomVerticalFlip(p0.3), # 叶片上下翻转也合理 transforms.RandomRotation(degrees30), # 旋转 30 度以内 transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.1, hue0.02), # hue 要小 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])参数说明scale(0.7, 1.0)表示随机裁剪面积占原图的 70% 到 100%比默认的(0.08, 1.0)保守很多避免把整片叶子裁成一小块。RandomVerticalFlip(p0.3)概率给低一点因为有些叶片上下翻转后叶脉方向不自然但完全不给又浪费了旋转不变性。hue0.02是关键——色相抖动太大会把绿色叶子抖成黄色模型会学到错误的颜色特征。Normalize用的是 ImageNet 的均值和方差如果你从零训练不用预训练权重可以改成数据集自己的统计值。3.2 验证集和测试集为什么不能加增强验证集和测试集的 transform 必须和推理时一致ResizeCenterCropToTensorNormalize。如果你在验证集上加了RandomHorizontalFlip每次评估结果都会波动早停的阈值就没法设。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]), ])逻辑说明Resize(256)再CenterCrop(224)是 ImageNet 评估的标准做法先缩放到短边 256再从中心裁 224。这样和训练时的RandomResizedCrop(224)在尺度上大致对齐。如果你发现验证集准确率比训练集低很多先检查这两个 transform 是不是一个用了增强一个没用。提示如果数据集里图片尺寸差异很大有的 500x500有的 2000x3000建议先统一缩放到一个中间尺寸再存一份否则每次Resize都在 CPU 上做训练速度会被拖慢。4. 模型选型与训练参数从 ResNet 到 EfficientNet 的取舍4.1 34 类叶片分类选哪个骨干网络34 类不算多但叶片图像的类间差异小需要模型有较强的细粒度特征提取能力。我一般会按下面这个顺序试模型参数量输入尺寸适用场景注意事项ResNet-5025M224基线稳定容易过拟合需要强增强EfficientNet-B05.3M224数据量中等学习率要调小EfficientNet-B312M300数据量较大显存占用高ViT-B/1686M224数据量很大小数据集上不如 CNNConvNeXt-Tiny28M224替代 ResNet训练慢但精度高如果训练集每类只有几十张优先用 EfficientNet-B0 加预训练权重冻结前面几层只训分类头。如果每类有几百张ResNet-50 或 ConvNeXt-Tiny 都可以。ViT 在 34 类叶片上我不推荐除非你有上万张图否则注意力机制学不到细粒度特征。import torch.nn as nn from torchvision import models def build_model(num_classes34, backboneefficientnet_b0, pretrainedTrue): if backbone resnet50: model models.resnet50(weightsmodels.ResNet50_Weights.DEFAULT if pretrained else None) in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) elif backbone efficientnet_b0: model models.efficientnet_b0(weightsmodels.EfficientNet_B0_Weights.DEFAULT if pretrained else None) in_features model.classifier[1].in_features model.classifier[1] nn.Linear(in_features, num_classes) else: raise ValueError(f不支持的骨干: {backbone}) return model model build_model(num_classes34, backboneefficientnet_b0) print(model.classifier if hasattr(model, classifier) else model.fc)逻辑说明替换分类头是迁移学习的基本操作in_features是骨干网络最后一层的输入维度换成num_classes后输出维度就和你的类别数对齐了。参数说明pretrainedTrue会加载 ImageNet 预训练权重如果数据集很小这一步能显著提升收敛速度。backbone参数控制用哪个网络方便你后面做对比实验。4.2 学习率、批次大小和早停怎么设训练参数这块我踩过的坑比模型选型还多。下面这套配置在 34 类叶片数据集上比较稳import torch from torch.utils.data import DataLoader from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR # 数据集 train_ds datasets.ImageFolder(./plant_leaf_34/train, transformtrain_tf) val_ds datasets.ImageFolder(./plant_leaf_34/val, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) # 模型、优化器、调度器 device torch.device(cuda if torch.cuda.is_available() else cpu) model build_model(num_classes34).to(device) optimizer AdamW(model.parameters(), lr3e-4, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max30) criterion nn.CrossEntropyLoss(label_smoothing0.1) # 早停 best_acc 0.0 patience 7 counter 0 for epoch in range(30): model.train() for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() loss criterion(model(imgs), labels) loss.backward() optimizer.step() scheduler.step() # 验证 model.eval() correct total 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) preds model(imgs).argmax(dim1) correct (preds labels).sum().item() total labels.size(0) acc correct / total print(fEpoch {epoch1}: val_acc{acc:.4f}) if acc best_acc: best_acc acc counter 0 torch.save(model.state_dict(), best_leaf_model.pth) else: counter 1 if counter patience: print(早停触发) break参数说明lr3e-4是 AdamW 在预训练模型微调时的常用值如果你从零训练可以调到 1e-3。weight_decay1e-4控制权重衰减防止过拟合。label_smoothing0.1对叶片这种类间差异小的任务很有用能缓解模型对某一类的过度自信。patience7表示验证集准确率连续 7 个 epoch 不提升就停这个值可以根据你的总 epoch 数调整一般设总轮数的 1/4 到 1/3。注意CosineAnnealingLR的T_max要设成你计划训练的总 epoch 数如果中途早停了学习率还没降到最低影响不大但如果T_max设小了学习率提前降到接近 0后面就学不动了。5. 避坑与排查叶片分类训练中最容易翻车的 5 个点5.1 验证集准确率比训练集高很多现象训练到第 5 个 epoch训练集准确率 70%验证集准确率 85%。原因训练集用了强增强验证集只做了 resize 和 center crop增强后的图片更难所以训练集准确率反而低。这不是 bug是正常现象。但如果验证集准确率一直比训练集高 10 个点以上就要检查是不是验证集太小或者验证集和训练集有重叠。解决先确认train_ds和val_ds的图片路径没有交集。如果验证集只有几十张准确率波动会很大建议从训练集里再切一部分出来做验证保证验证集每类至少 10 张。5.2 某一类准确率特别低其他类都正常现象34 类里 33 类 F1 都在 0.9 以上只有某一类 F1 是 0.3。原因要么这一类样本太少要么这一类和其他类视觉上太像。叶片数据集里同科植物经常出现这种情况。解决先看混淆矩阵确认这一类被误判成了哪一类。如果是样本少用WeightedRandomSampler给少样本类加权。如果是类间相似考虑加一个细粒度分类头或者用 ArcFace 损失增大类间距离。from torch.utils.data import WeightedRandomSampler import numpy as np # 统计每个类别的样本数 targets [label for _, label in train_ds.samples] class_counts np.bincount(targets) class_weights 1.0 / class_counts sample_weights class_weights[targets] sampler WeightedRandomSampler(weightssample_weights, num_sampleslen(sample_weights), replacementTrue) train_loader DataLoader(train_ds, batch_size32, samplersampler, num_workers4, pin_memoryTrue)参数说明class_weights是类别频率的倒数样本越少的类权重越大。replacementTrue表示有放回采样保证每个 batch 里少样本类也能出现。用了 sampler 之后shuffle必须设为 False否则会冲突。5.3 训练 loss 震荡不下降现象loss 在 2.0 到 3.5 之间来回跳准确率不涨。原因学习率太大或者 batch size 太小导致梯度噪声大。叶片数据集如果图片背景杂乱梯度本身就不稳定。解决先把学习率降到 1e-4 试 5 个 epoch如果 loss 开始稳定下降说明之前学习率大了。如果还不行把 batch size 从 32 降到 16同时学习率再降一半。另外检查Normalize的均值和方差是不是和预训练模型匹配不匹配也会导致训练不稳定。5.4 测试集准确率远低于验证集现象验证集 92%测试集只有 75%。原因验证集和测试集的分布不一致。可能是划分时没有做分层抽样测试集里某些类别的图片光照、角度和训练集差异大。解决重新检查划分脚本确保每个 split 里每个类别的比例一致。如果数据集已经划分好了没法改就在训练时加入更强的光照增强ColorJitter的 brightness 和 contrast 调大让模型对光照变化更鲁棒。5.5 推理时类别名对不上现象模型输出 34 维向量但不知道第 0 维对应哪个植物。原因ImageFolder的class_to_idx没有保存或者保存了但推理时没加载。解决训练完立刻把class_to_idx存成 JSON推理时加载同一个 JSON。import json # 训练时保存 with open(class_to_idx.json, w, encodingutf-8) as f: json.dump(train_ds.class_to_idx, f, ensure_asciiFalse, indent2) # 推理时加载 with open(class_to_idx.json, r, encodingutf-8) as f: class_to_idx json.load(f) idx_to_class {v: k for k, v in class_to_idx.items()}逻辑说明class_to_idx是文件夹名到索引的映射idx_to_class反过来。推理时用idx_to_class[pred]就能拿到植物类别名。这个 JSON 文件要和模型权重一起保存否则换台机器跑推理就抓瞎了。6. 进阶技巧用混淆矩阵和 TTA 把准确率再抬 2 个点训练完模型、跑完测试集很多人就停了。但如果你想把 34 类叶片分类的准确率再往上抬一抬有两个成本很低的手段混淆矩阵分析和测试时增强TTA。先说混淆矩阵。它不只是看个热闹关键是帮你定位「哪两类在互相误判」。我一般会这样画import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay import numpy as np model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in test_loader: imgs imgs.to(device) preds model(imgs).argmax(dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) disp ConfusionMatrixDisplay(confusion_matrixcm, display_labelslist(idx_to_class.values())) fig, ax plt.subplots(figsize(14, 14)) disp.plot(axax, xticks_rotation90, cmapBlues, colorbarFalse) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150)逻辑说明confusion_matrix的行是真实标签列是预测标签对角线是正确分类。参数说明xticks_rotation90让类别名竖着排34 个类别横着排会挤成一团。cmapBlues颜色越深表示数量越多。跑完这张图你会看到某几个类别之间有一条明显的非对角线亮带那就是需要重点处理的类对。找到易混淆的类对之后有两个处理方向。一是针对性补数据如果这两类在训练集里各只有 50 张想办法补到 100 张准确率提升比调模型明显。二是加一个二分类器先让 34 类模型输出概率如果 top-2 概率差距小于 0.2就送进一个专门区分这两类的二分类模型。这个方案在叶片分类里很实用因为同科植物叶片确实长得像强行让一个 34 类模型分开不如让一个二分类模型专门学它们的差异。再说 TTA。测试时增强的思路是对同一张测试图做多次不同的变换比如原图、水平翻转、不同裁剪分别推理后把概率平均。这样能抵消单次推理的随机性通常能涨 1 到 2 个点。def tta_predict(model, img_tensor, n_aug5): 对单张图做 n_aug 次增强推理返回平均概率 model.eval() probs [] # 原图 with torch.no_grad(): probs.append(torch.softmax(model(img_tensor.unsqueeze(0).to(device)), dim1)) # 水平翻转 with torch.no_grad(): probs.append(torch.softmax(model(torch.flip(img_tensor, dims[2]).unsqueeze(0).to(device)), dim1)) # 垂直翻转 with torch.no_grad(): probs.append(torch.softmax(model(torch.flip(img_tensor, dims[1]).unsqueeze(0).to(device)), dim1)) # 不同亮度 for scale in [0.9, 1.1]: aug torch.clamp(img_tensor * scale, 0, 1) with torch.no_grad(): probs.append(torch.softmax(model(aug.unsqueeze(0).to(device)), dim1)) return torch.stack(probs).mean(dim0)逻辑说明torch.flip(img_tensor, dims[2])是水平翻转dims[1]是垂直翻转因为 tensor 的维度是(C, H, W)。亮度缩放用torch.clamp限制在 0 到 1 之间。最后torch.stack(probs).mean(dim0)把 5 次推理的概率平均。参数说明n_aug5是增强次数次数越多越稳但推理越慢一般 5 到 8 次就够了。TTA 只在测试阶段用训练和验证阶段不要用否则评估结果会虚高。最后说一个我自己的习惯每次训练完先把混淆矩阵和 TTA 后的准确率一起看。如果 TTA 涨了不到 0.5 个点说明模型本身已经比较稳不用再折腾如果 TTA 涨了 2 个点以上说明模型对翻转、亮度这些变换很敏感训练时的增强策略还有优化空间。这个判断比单纯看准确率数字有用得多。希望帮到你。本文还有配套的精品资源点击获取