大豆叶片病害数据集构建与PyTorch图像分类训练实战

📅 2026/8/27 22:39:42
大豆叶片病害数据集构建与PyTorch图像分类训练实战
简介图像分类是计算机视觉最基础也最常用的任务之一而数据集的质量直接决定了模型的上限。在农业AI领域真实场景下的病害识别往往受限于样本数量少、背景复杂、类别相似度高等问题。构建一个规范、可用的领域数据集需要从采集规范、标注流程到清洗策略进行全流程设计。同时借助迁移学习和数据增强技术可以在中等规模样本下训练出泛化能力较强的模型。本文以大豆叶片病害为切入点系统介绍一个包含3600张图像、覆盖5类常见病害与健康叶片的数据集构建过程并基于PyTorch框架对比ResNet、EfficientNet和MobileNetV3在细粒度分类任务上的表现为农业视觉落地提供可复用的工程实践思路。1. 数据集概述这3600张图到底能做什么1.1 大豆叶片病害识别的实际需求做农业视觉方向的项目最头疼的往往不是算法选型而是数据。我最初想找一个能直接用于训练的大豆叶片病害分类数据集翻遍了各类公开平台要么是国外数据集里只有少数几类病害、图片数量少得可怜要么是图片风格高度统一、全是实验室白背景下拍的拿来做研究凑合但想落地到田间场景就很别扭。当时我自己的项目卡在一个很现实的问题上农户拍一张病叶照片传上来系统需要判断这是什么病害或者至少给出方向性提示。想在移动端跑得动、识别得准就得有贴合真实拍摄条件的数据来训练和验证。于是花了大约三周时间从田间采集到整理标注整理出一份约3600张、已标注的大豆叶片病害图像分类数据集。这份数据集能做的事很明确训练图像分类模型识别大豆叶片常见的几类病害包括健康叶片作为对照类别。验证和对比主流分类算法在细粒度病害识别上的表现比如ResNet、EfficientNet、MobileNetV3这些。给做农业AI、智慧植保、毕业设计或者算法竞赛的同学当实验数据。简单说它的定位是“到手就能直接上训练流程”的数据集不需要你再花大量时间做标注清洗。我后面的内容会把它从数据构建到模型训练验证的过程完整拆开讲包括每类图片数量、采集标注时踩过的坑以及训练时容易翻车的几个问题。1.2 类别体系、数据规模与使用场景这份数据集一共包含6个类别覆盖了田间最常见的大豆叶部病害外加一个健康对照类。类别中文名称图片数量healthy健康叶片600张soybean_rust大豆锈病600张frogeye_leaf_spot灰斑病蛙眼病600张downy_mildew霜霉病600张bacterial_blight细菌性斑疹病600张anthracnose炭疽病600张每类600张加起来正好3600张。把各类数量做均匀最大的好处就是训练初期不用太担心类别不平衡带来的准确率虚高问题尤其对新手来说可以先把分类模型跑通再去考虑复杂的样本均衡策略。选择这几类病害一是因为它们在大豆种植区确实高发二是因为它们的叶片症状在视觉上有一定相似度比如灰斑病和细菌性斑疹病早期都是小斑点训练出来的模型如果能区分这些“容易混淆”的类别实际应用价值会高很多。数据集主要覆盖两种使用场景科研实验验证新提出的分类网络、注意力模块、数据增强策略在农业细粒度图像上的效果。工程落地用迁移学习在较小数据量下训练出推理速度快的模型部署到小程序或服务端。我后来用ResNet34做了一轮基础实验测试集准确率能到92%左右说明这个量级的数据配合合理的训练技巧已经能支撑实际项目验证了。2. 图像采集与标注全流程2.1 采集方案设备、环境与拍摄规范数据集的原始图片来自两部分一部分是在试验田里拍的另一部分是盆栽大豆在实验室内补拍的。这个组合比较关键因为田间照片背景杂、光线不受控、叶片姿态自然而实验室照片背景干净、光照均匀两者混合训练模型在真实场景下不容易“水土不服”。拍摄设备我用了两部手机主摄4800万像素和一台微单相机分辨率都至少在800x800以上。有人会问手机拍的够用吗实测下来只要光照和距离控制好手机拍的照片做224x224输入完全够用而且更贴近用户真实上传图片的画质。具体拍摄时我定了几个硬性规范叶片平铺在纯色背景上拍优先用白板、灰布或黑色植绒布方便后期如果要做分割或检测背景也容易抠。尽量用自然漫射光避开正午强光避免叶片高光反光掩盖病斑纹理。手机镜头和叶片所在平面保持平行也就是俯拍90度角减少透视畸变。每片叶子拍1到3张稍微变换角度和位置保证同一片叶子的特征不会被某一种姿态“固化”。采集过程中真的会出现大量废片。最常见的问题是对焦对在了背景上、叶片反光过大、以及手抖导致的模糊。这些图片肉眼看着好像还行但模型训练时会引入噪声后面清洗阶段需要重点处理。2.2 标注方案分类标签的规范与质检流程图像分类数据集的“标注”和检测、分割不一样不需要画框或抠轮廓只需要给每张图片打一个类别标签。最直接的做法就是按类别建立文件夹把图片放进去文件夹名就是标签。我在标注前先定了一套命名和归档规范类别名统一用英文小写加下划线比如soybean_rust、frogeye_leaf_spot避免中文路径带来的各种麻烦。文件名格式统一为“类别名_编号.jpg”例如soybean_rust_001.jpg方便后续追溯和数据管理。每张图只打一个主导类别标签。遇到一张叶片上同时出现两种病害的情况归类到症状更明显的那个类别并在备注Excel里记录“mixed”标记方便后续做多标签扩展。标注过程用了一个比较笨但可靠的方式第一步由一个人完成全部初标第二步由另一个有植保背景的人抽检20%第三步我把抽检中不确定的样本统一再过一遍。不要小看这个流程农业病害的视觉判断有时真的存在主观差异尤其像灰斑病和细菌性斑疹病早期症状容易混淆单人标注的失误率可能达到5%以上。质检环节还发现一个有意思的现象有经验的植保人员判断叶片病害时不光看斑点的形态还会看斑点的分布位置和叶片整体颜色变化。这类经验信息很难直接写进标注规则里但能帮我们确定哪些“边缘样本”该保留、哪些该删。比如某张图叶片已经大面积枯黄坏死病斑特征完全看不清这种就算专家也难判断留在数据集里只会增加噪声直接删除。2.3 数据清洗与基础预处理数据清洗的工作量比想象中大。用脚本批量做了三件事计算所有图片的清晰度拉普拉斯方差把方差低于阈值的模糊图片挑出来人工确认。用感知哈希算法做相似度去重同一片叶子的多张连拍只保留清晰度和构图最好的一张。抽样检查每张图片的类别标签是否和文件名一致避免归档时放错文件夹。清洗前后对比原始采集量接近4200张最终保留3600张清洗率约14%这个比例在自采数据集里算是正常的。清洗后统一做了基础格式转换全部转为JPG格式压缩质量设为95体积和清晰度平衡。长边统一缩放到1024像素以内短边不做强制限制保留原始宽高比。EXIF信息里的GPS和拍摄时间建议移除避免发布时泄露采集地点等元信息。这里有个细节值得说下不要把所有图片都统一resize成224x224再存盘。因为训练时通常还会做随机裁剪和缩放如果你在存盘阶段就把分辨率压死数据增强的随机性会大打折扣。存1024以内的原图比例图训练时再实时变换效果会好很多。3. 数据组织与训练集构建3.1 目录结构设计整理好的数据集我采用了PyTorch的ImageFolder标准目录结构这是最通用、最省事的组织方式。你拿到数据后不用额外写自定义Dataset类torchvision的datasets.ImageFolder直接就能读。dataset/ ├── train/ │ ├── healthy/ # 健康叶片 │ ├── soybean_rust/ # 大豆锈病 │ ├── frogeye_leaf_spot/ # 灰斑病 │ ├── downy_mildew/ # 霜霉病 │ ├── bacterial_blight/ # 细菌性斑疹病 │ └── anthracnose/ # 炭疽病 ├── val/ │ └── (同train的子目录结构) └── test/ └── (同train的子目录结构)所有图片按照“主目录/类别名/图片文件名.jpg”的路径存放。这里的类别名同时也承担标签编码的任务ImageFolder会自动按文件夹名的字母顺序分配从0开始的类别索引所以文件夹命名一定要规范统一不然后面训练代码里类别对应关系会乱掉。除了图片目录建议同时维护一份labels.csv记录文件名、类别名、类别编码、来源田间/实验室、备注等信息。这份文件在你以后想做数据分析、可视化样本分布或者转成其他格式时非常有用。别省这一步我后来多次想重新统计数据分布都靠这个CSV直接出结果。3.2 训练集、验证集、测试集分层划分划分比例我用的是8:1:1即训练集2880张、验证集360张、测试集360张。关键在于必须按类别分层抽样而不是把所有图片混在一起随机切分。直接随机切分的话有可能出现某一类病害在训练集里特别多、在验证集里特别少的情况导致验证集评估结果波动很大。分层抽样的实现很简单from sklearn.model_selection import train_test_split import pandas as pd df pd.read_csv(labels.csv) train_df, temp_df train_test_split( df, test_size0.2, stratifydf[label], random_state42 ) val_df, test_df train_test_split( temp_df, test_size0.5, stratifytemp_df[label], random_state42 ) print(train_df[label].value_counts()) print(val_df[label].value_counts()) print(test_df[label].value_counts())stratify参数指定按label列做分层保证每个子集里各类别占比基本一致。test_size0.2表示先分出20%作为临时集再把这20%等分成验证集和测试集各占10%。划分完要检查一下不同集合之间不要有重复图片。自采数据里同一片叶子的多个角度照片在划分时要确保整组进同一个集合否则会出现“数据泄漏”——测试集里出现了训练集相似度极高的图片测试分数虚高。那个“图片编号是同一片叶子的多张”之类的情况我会在划分前先按叶片编号分组再把整组切到同一集合。3.3 数据增强策略与实施3600张图说多不多训练一个深度分类网络如果不加数据增强很容易过拟合。我用的是torchvision.transforms训练集和验证集/测试集分别用不同的pipelinefrom torchvision import transforms train_transforms transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transforms transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])有些细节要提醒一下RandomResizedCrop的scale参数设的是(0.7, 1.0)而不是默认的(0.08, 1.0)。因为默认值裁剪比例太狠会把一些本来就很小的病斑区域直接裁掉模型学不到关键特征。RandomRotation只用了15度。叶片图像旋转太大会让背景占比过高还会把部分叶片边缘旋出画面。ColorJitter的三个参数都控制在0.2强度不高但有效。病害叶片本身颜色信息就很重要调过头会让“黄化”和“正常绿”的边界被抹掉反而干扰分类。验证集和测试集不用随机增强只做Resize和CenterCrop保证每次评估结果可复现、可比对。我用上面的增强策略把3600张图的训练样本量等效放大了20倍以上后续实验证明效果明显验证集准确率比不加增强时高了接近4个百分点。4. 模型训练实战与效果对比4.1 模型选型ResNet、EfficientNet、MobileNetV3我拿这个数据集测试了三个有代表性的模型覆盖了从精度优先到效率优先的不同场景模型参数量输入尺寸特点ResNet34约21M224x224结构经典、训练稳定适合作为baselineEfficientNet-B0约5M224x224精度和效率均衡NAS搜索出的结构MobileNetV3-Large约5M224x224移动端部署友好推理速度快选择这三个不是随意定的。ResNet不需要解释是图像分类绕不开的经典结构用来验证数据集质量最可靠。EfficientNet-B0在同样输入尺寸下参数量不到ResNet34的四分之一但精度往往不输适合验证“小模型能不能打”。MobileNetV3-Large代表了移动端推理场景的实际需求因为这类农业识别功能最终大概率要部署在手机小程序或低算力设备上。三个模型都采用ImageNet预训练权重做迁移学习也就是只替换最后一层全连接分类头把输出维度改成6然后对整个网络做全量微调。很多人会问为什么不冻结前面层只训练分类头我的经验是当你的数据和ImageNet有一定分布差异比如农业病害叶片和自然图像整体风格差别明显时全量微调通常能获得更好的效果代价只是训练时间稍长。数据量在3000张级别时全量微调并不会过拟合只要配合好正则化手段。4.2 关键训练参数配置训练参数我调过几轮最终一套稳定配置如下优化器Adam学习率1e-4weight_decay1e-4。损失函数CrossEntropyLoss。batch_size32如果显卡显存不够可以降到16。学习率调度余弦退火CosineAnnealingLRT_max设为训练总轮数。训练轮数50个epoch每个epoch大约90个batch整体训练时间很短。随机种子固定为42保证实验可复现。选Adam而不是SGD是因为在迁移学习场景下Adam的收敛更稳对学习率不那么敏感对新手更友好。weight_decay虽然设了但值不大主要起轻微的正则作用防止后期过拟合。余弦退火的好处是前期以较大步长快速逼近最优区域后期用小步长精细收敛效果比固定学习率或StepLR更平滑。一个踩过的坑是初始学习率不能直接沿用默认的1e-3。预训练模型的特征提取层已经有了不错的特征表示初始学习率太大会把预训练权重“冲坏”导致前期损失震荡。降到1e-4以后整个训练过程就稳定多了。4.3 基于PyTorch的训练源码数据读取直接用ImageFolder DataLoaderfrom torch.utils.data import DataLoader from torchvision import datasets train_dataset datasets.ImageFolder(dataset/train, transformtrain_transforms) val_dataset datasets.ImageFolder(dataset/val, transformval_transforms) test_dataset datasets.ImageFolder(dataset/test, transformval_transforms) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) test_loader DataLoader(test_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue)训练循环主体import torch import torch.nn as nn import torch.optim as optim from torchvision import models from torch.optim.lr_scheduler import CosineAnnealingLR device torch.device(cuda if torch.cuda.is_available() else cpu) model models.resnet34(weightsmodels.ResNet34_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, 6) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-4, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max50)训练和验证循环best_val_acc 0 for epoch in range(50): model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) model.eval() correct 0 total 0 val_loss 0.0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) val_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() val_acc correct / total print(fEpoch {epoch1:02d} | Train Loss: {running_loss/len(train_dataset):.4f} f| Val Loss: {val_loss/len(val_dataset):.4f} | Val Acc: {val_acc:.4f}) if val_acc best_val_acc: best_val_acc val_acc torch.save(model.state_dict(), best_model_resnet34.pth) scheduler.step()训练结束后在测试集上评估model.load_state_dict(torch.load(best_model_resnet34.pth)) model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in test_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() print(fTest Accuracy: {correct/total:.4f})训练过程中我习惯每轮都打印训练损失、验证损失和验证准确率。如果发现训练损失持续下降但验证损失上升说明开始过拟合需要提前停止或增强正则如果验证准确率在某个值附近来回震荡往往说明学习率策略需要调整。4.4 实验结果与指标解读三个模型在相同数据划分下的测试集结果模型Top-1准确率平均精确率平均召回率F1分数ResNet3492.2%92.3%92.1%92.2%EfficientNet-B091.7%91.8%91.5%91.6%MobileNetV3-Large89.4%89.6%89.2%89.4%ResNet34相对EfficientNet-B0在准确率上只有0.5个百分点的优势但参数量是后者的4倍多这说明如果算力有限或需要部署到边缘设备EfficientNet-B0是更划算的选择。MobileNetV3-Large虽然准确率最低但它在CPU上的推理速度相比ResNet34能快3倍左右实际落地时这个差距往往比那3个百分点更重要。进一步看了混淆矩阵发现最容易被混淆的是灰斑病frogeye_leaf_spot和细菌性斑疹病bacterial_blight。这两个病的早期症状都是叶片上出现小斑点颜色和形态接近人工判断都有一定难度模型分不清也算合理。想改善这个问题可以增加这类难分类别的样本比例或者从图像上提取更多纹理细节特征。测试集准确率92%这个数字单独看还不错但如果直接用这份模型去识别田间自然生长的叶片效果会打折扣。原因在于测试集和训练集有相同的采集分布而田间拍摄的光照、角度、背景变化远远超过训练集覆盖的范围。这也是为什么我反复强调自采数据要尽量包含多样化的采集环境。5. 常见问题与排查笔记5.1 过拟合怎么压用3600张图训练ResNet34第三四个epoch开始训练损失和验证损失就分道扬镳了训练损失降到0.1以下但验证损失还在0.5以上这是典型的过拟合信号。我试下来最有效的组合是数据增强强度拉满前面说的那一套尤其不能少RandomResizedCrop和ColorJitter。weight_decay从0加到1e-4对Adam来说这个值已经能起到约束作用。把全连接层加一层Dropout概率设为0.3对全量微调有帮助。采用Early Stopping验证准确率连续10个epoch不提升就停止训练。有人会问用更大的模型是不是反而不容易过拟合实际在这个数据规模下ResNet50比ResNet34更容易过拟合模型容量大了但数据量没跟上拟合噪声的能力也更强了。所以不要盲目换大模型。5.2 类别不平衡与损失函数调整虽然我这套数据各类别数量完全均等但这是理想情况。如果你以后要扩展数据集不可避免会遇到某些类别图片难采集、数量偏少的问题。比如霜霉病在干旱年份可能整个试验田都找不到几片典型病叶。遇到类别不平衡我的建议从简单到复杂排序最简单的做法是按类别加权CrossEntropyLoss少数类的loss权重调高。直接用sklearn的compute_class_weight可以算出权重代码不超过十行。对少数类做过采样就是在DataLoader里把少数类的样本多重复几次本质上和加权loss效果类似。用Focal Loss它会对难分类样本给予更高关注对天然易混淆的病害类别有一定帮助。但Focal Loss的超参数需要重新调不建议一上来就用。权重过大会导致少数类过拟合严重权重过小等于没调一般从官方计算出的class_weight起步效果不行再手动上下微调。5.3 标注噪声与数据质量风险标注错误是分类问题里最隐蔽的陷阱。一个类别的图片被错误标成了另一个类别模型会被强行“教错”而且很难通过训练曲线发现。我这次质检过程中发现大约有1%到2%的图片存在标注争议主要集中在两类早期病斑的区分上。处理标注噪声的办法是与其把争议样本强行标一个类别不如直接删掉。删除少量边界模糊的样本对模型训练的影响可以忽略不计但能避免错误标签干扰特征学习。还有一个技巧是训练结束后把模型预测错误且confidence很高的样本列出来人工复查这一步经常能发现隐藏的标注错误。5.4 实用问题速查表问题表现解决思路训练损失不下降损失在初始值附近波动检查数据标签是否错乱尝试把学习率调大一个量级验证准确率震荡每轮结果忽高忽低降低学习率、加大batch_size、检查验证集是否太小模型过拟合训练损失低但验证损失高加强数据增强、加正则化、Early Stopping测试集分数虚高可信度存疑检查训练/测试是否包含相似或重复图片特定类别混淆严重混淆矩阵中两个类别互分不清增加该类样本量、加入更多该类别特有特征相关的增强同图出现在训练和验证集结果异常高按图片原始分组划分数据而不是按单张图划分6. 从分类到更深的扩展方向6.1 继续扩数据病害部位、严重程度与多生长阶段当前数据集每张图只标了一个类别标签。实际上大豆病害的发生常常伴随多病害复合侵染同一株甚至同一片叶子上可能同时出现多种病斑。如果后续想提升应用价值可以在现有基础上做两个方向的扩展增加“严重程度”维度比如把灰斑病按病斑面积占比分为轻、中、重三级。这实际上把6分类变成了更多类的细粒度分类模型难度会明显上升但输出的信息对农户决策更有指导意义。增加不同生长阶段的叶片样本。大豆苗期、开花期、结荚期的叶片颜色、厚度、质地差异都很明显同一病害在不同时期的症状表现也不完全一样。现有数据集大部分拍摄于生长中后期模型对早期叶片可能识别不佳。扩数据的优先顺序我建议先补“易混淆类”的样本量再补“不同生长阶段”最后考虑加严重程度标注。每一步都可以复用现有的数据组织规范和训练流程。6.2 从图像分类到目标检测与实例分割分类任务只能告诉我们“这张叶片有什么病”但实际场景中农户拍的照片往往是整株大豆或者几片叶子叠在一起这时候分类模型就不够用了。如果希望模型能自动定位到叶片上的病斑区域就需要把数据集升级为检测或分割格式。从分类数据转检测/分割有两条路重新标注把每张图中的单个叶片和病斑区域画上边界框或分割掩码。这类标注工作量大但获取的信息量也最大。先做叶片检测再对检测出的叶片区域做病害分类这种两阶段方案可以复用现有的分类数据训练第二阶段的分类器只需要额外标注叶片级别的检测框。我自己更倾向于两阶段方案因为在农业AI落地中先定位叶片、再识别病害逻辑清晰且每一阶段都可以单独调优。如果以后想做病斑分割那就要用LabelMe或EISeg这类工具重新标注了工作量按小时计算都是少的。6.3 部署落地与移动端模型需求数据集的最终价值要体现在实际应用里。以我做的项目为例目标是在微信小程序里让农户拍叶片照片返回病害类型和防治建议。这就涉及到模型部署的约束模型体积不能太大、推理时间不能太长、手机端不能过热。从当前数据集训练的模型来看MobileNetV3-Large在CPU上单张推理约30到50毫秒体积约14MB属于能接受的范围。如果要进一步压缩可以试试知识蒸馏——用ResNet34这个强模型当教师训练MobileNetV3或更小的模型当学生往往能在几乎不损失精度的情况下把模型做得更小。部署时可以先用ONNX导出模型再用ONNX Runtime或TensorRT Lite做推理优化。整个过程不复杂真正要注意的是模型输入预处理必须和训练时保持一致特别是归一化的mean和std很多部署翻车都翻在这里。7. 写在最后的个人体会做完这份数据集的标注和训练验证我最大的感受是一个数据集的价值不在于它的图片有多精美而在于它是否真实覆盖了你想解决的应用场景。实验室里拍得整整齐齐的叶片图片训练出来的模型到田间一测就露馅反而是那些带着泥土、光线不太均匀的照片能让模型学到更鲁棒的特征。整理数据这件事看起来很琐碎但确实是整个项目里性价比最高的一步。一套规范的目录结构、一份完整的labels.csv、一次严格的分层划分能帮你在后续模型迭代中省下大量时间。如果你计划自己采集农业图像数据我建议从第一天就把拍摄规范、命名规范、质检流程定下来宁可前期多花两天也别后面返工重来。另外关于模型选型再啰嗦一句不要一上来就追最新的网络结构先拿ResNet18或ResNet34把baseline跑通确认数据没问题再逐步换模型、调参数。很多所谓“模型不行”的问题最后查出来都是数据划分或预处理的小错误。这个数据集拿回去以后建议也按这个流程走一遍稳扎稳打比什么都重要。本文还有配套的精品资源点击获取