
简介这是一套面向计算机相关专业学生与项目实战学习者的农作物病虫害识别分类完整项目包可直接用于毕业设计、课程设计或期末大作业。项目以Python实现涵盖数据加载、模型构建、训练验证等核心环节代码结构清晰适合具备一定深度学习基础、希望快速上手图像分类实战的读者。压缩包共96个文件以76个py源码文件为主辅以15个pyc编译文件、2个zip子包、2个txt日志与1个md说明文档整体约25.1MB源码与数据集、使用说明一并提供下载后无需额外配置即可运行。项目已通过严格调试包含dataloader、models、config等模块涉及resnet、efficientnet、swin_transformer等多种网络结构便于读者理解分类模型的组织方式与训练流程。目前已有2167人学习下载可作为图像识别方向毕设的可靠参考也能帮助学习者掌握从数据到模型落地的完整思路。1. 农作物病虫害识别项目从一张叶片照片到可用模型的完整路径田里发现叶片上出现褐色斑点拍照后想立刻知道是哪种病害、该用什么药这个需求催生了大量基于 Python 的农作物病虫害识别分类项目。这类项目通常打包成「源码 数据集 使用说明」的形式核心任务是用卷积神经网络对叶片图像做多分类输出病害类别。它适合两类人一类是想入门深度学习但不想从零造轮子的开发者另一类是需要快速验证识别方案是否可行的农业技术人员。真正决定项目能不能跑起来的不是模型多先进而是数据集质量、环境版本匹配和训练参数这三件事。下面按「先跑通、再调优、后避坑」的顺序拆开讲。2. 数据集与模型选型为什么 MobileNet 比 ResNet 更适合这个场景2.1 农作物病害数据集的典型结构与类别分布常见的农作物病虫害数据集如 PlantVillage 风格的叶片图像集一般按「作物种类 / 病害名称」组织目录每个子目录下存放该类别的叶片照片。典型规模在 2 万到 5 万张之间类别数在 10 到 40 之间图像分辨率从 256×256 到 512×512 不等。目录结构大致如下dataset/ ├── train/ │ ├── Tomato___Early_blight/ │ ├── Tomato___Late_blight/ │ ├── Tomato___healthy/ │ ├── Potato___Early_blight/ │ └── ... ├── val/ │ └── (同上结构) └── test/ └── (同上结构)这种结构可以直接被torchvision.datasets.ImageFolder或tf.keras.utils.image_dataset_from_directory读取不需要额外写解析脚本。需要重点检查的是类别是否均衡——如果某个病害只有几十张图而健康叶片有几千张训练出来的模型会严重偏向多数类。常见做法是先统计每个类别的样本数对少于 200 张的类别做数据增强或过采样。2.2 选 MobileNetV3 而不是 ResNet50 的三个理由在这个任务上我一般会优先选 MobileNetV3-Small 或 MobileNetV3-Large而不是 ResNet50。原因有三点第一叶片病害的判别特征主要是颜色斑块和纹理不需要 ResNet 那么深的层次去提取抽象语义第二MobileNet 的参数量在 2M 到 5M 之间训练时显存占用低一张 8GB 显卡就能跑 batch size 32第三推理速度快部署到边缘设备或手机端时延迟可控。如果数据集类别超过 30 类且类间差异细微可以换用 EfficientNet-B0它在精度和参数量之间平衡得更好。迁移学习是标配做法加载 ImageNet 预训练权重冻结前面的特征提取层只训练最后的分类头。等分类头收敛后再解冻最后几个 block 做微调。这样在几千张图的训练集上也能拿到 90% 以上的验证准确率。2.3 用 torchvision 构建数据加载管线的完整代码import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader # 训练集增强随机裁剪、翻转、颜色抖动 train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomVerticalFlip(), 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_transform 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]) ]) train_dataset datasets.ImageFolder(dataset/train, transformtrain_transform) val_dataset datasets.ImageFolder(dataset/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) print(f类别数: {len(train_dataset.classes)}) print(f训练样本: {len(train_dataset)}, 验证样本: {len(val_dataset)})这段代码的关键点在于训练集用了RandomResizedCrop和颜色抖动模拟田间拍摄时角度和光照的变化验证集不做增强保证评估结果稳定。Normalize的均值和标准差必须用 ImageNet 的统计值因为后面加载的是 ImageNet 预训练权重。num_workers设为 4 是经验值设太大在 Windows 上容易出问题设太小则数据加载成为瓶颈。pin_memoryTrue在 GPU 训练时能加快数据传输。3. 训练脚本与参数配置让模型在 30 轮内收敛3.1 迁移学习训练循环的完整实现import torch.nn as nn import torch.optim as optim from torchvision import models device torch.device(cuda if torch.cuda.is_available() else cpu) # 加载预训练 MobileNetV3 model models.mobilenet_v3_small(weightsmodels.MobileNet_V3_Small_Weights.IMAGENET1K_V1) # 冻结特征提取层 for param in model.features.parameters(): param.requires_grad False # 替换分类头输出类别数 num_classes len(train_dataset.classes) model.classifier[3] nn.Linear(model.classifier[3].in_features, num_classes) model model.to(device) # 损失函数与优化器 criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.classifier.parameters(), lr1e-3, weight_decay1e-4) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) best_acc 0.0 for epoch in range(30): 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() # 验证阶段 model.eval() correct, total 0, 0 with torch.no_grad(): for images, labels in val_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() acc correct / total scheduler.step() print(fEpoch {epoch1}/30, Loss: {running_loss/len(train_loader):.4f}, Val Acc: {acc:.4f}) if acc best_acc: best_acc acc torch.save(model.state_dict(), best_model.pth) print(f 已保存最佳模型 (acc{acc:.4f}))逻辑说明先冻结model.features的所有参数只训练分类头这样前 10 轮左右分类头就能收敛到一个不错的水平。优化器用 AdamW 而不是 SGD是因为分类头参数少AdamW 的自适应学习率收敛更快。CosineAnnealingLR让学习率按余弦曲线下降避免后期震荡。每轮验证后保存最佳模型防止过拟合后反而丢失最优权重。参数说明lr1e-3是分类头训练的常用初始学习率weight_decay1e-4做 L2 正则T_max30与总轮数一致。如果验证准确率在 15 轮后不再提升可以把model.features最后两个 block 解冻用lr1e-4做微调。3.2 微调阶段的参数调整与解冻策略当分类头收敛后通常验证准确率在 85% 到 92% 之间可以解冻部分特征层做微调。具体做法是# 解冻最后两个 block for param in model.features[-2:].parameters(): param.requires_grad True # 重新设置优化器学习率降低 10 倍 optimizer optim.AdamW([ {params: model.features[-2:].parameters(), lr: 1e-4}, {params: model.classifier.parameters(), lr: 1e-4} ], weight_decay1e-4)微调阶段一般再跑 10 到 15 轮验证准确率能再提升 2 到 5 个百分点。注意解冻太多层会导致过拟合尤其是训练集小于 5000 张时解冻最后 1 个 block 就够了。3.3 训练过程中的监控指标与早停判断除了准确率还要看混淆矩阵和每类的 F1 分数。有些病害类别长得像模型容易混淆比如番茄早疫病和晚疫病在早期症状上很接近。如果发现某两个类别互相误判严重可以针对性补充这两类的训练样本或者在损失函数里给这两个类别更高的权重。早停的判断标准如果验证损失连续 5 轮不下降就停止训练。不要只看准确率因为准确率可能在过拟合后仍然微涨但验证损失已经开始上升。4. 避坑与排查训练不收敛、显存溢出、类别不均衡的实战处理4.1 损失不下降准确率卡在随机水平现象训练 10 轮后 loss 仍在 2.3 左右对应 10 类随机猜测验证准确率约 10%。原因最常见的是标签没对齐。ImageFolder按目录名排序生成类别索引如果训练集和验证集的目录顺序不一致标签就会错位。另一个原因是学习率太大分类头直接震荡。解决打印train_dataset.classes和val_dataset.classes确认两者完全一致。如果学习率是 1e-2 以上降到 1e-3 或 1e-4 再试。4.2 CUDA out of memory 的三种降显存方法现象训练开始几秒后报RuntimeError: CUDA out of memory。原因batch size 太大或者图像分辨率太高或者没有用torch.no_grad()包住验证阶段。解决第一把 batch size 从 32 降到 16 或 8第二把输入分辨率从 224 降到 192第三确认验证阶段用了with torch.no_grad():。如果还不够用torch.cuda.amp做混合精度训练显存占用能再降 30% 左右。4.3 某个类别准确率极低甚至为 0现象整体准确率 90%但某个病害类别的召回率只有 5%。原因该类样本太少模型直接把它归到多数类。或者该类图像质量差比如全是模糊的或过曝的。解决先统计每个类别的样本数对少于 200 张的类别做离线增强旋转、裁剪、加噪声扩充到 500 张以上。如果样本本身质量差考虑从数据集中剔除或重新采集。4.4 验证准确率远高于测试准确率现象验证集 95%测试集只有 70%。原因验证集和训练集来自同一批数据分布一致测试集可能来自不同时间、不同设备拍摄存在域偏移。解决这是最容易被忽略的坑。如果项目要求实际部署必须用独立采集的测试集评估。常见做法是把测试集按拍摄时间或地点划分而不是随机划分。如果测试集准确率确实低需要在训练时加入更强的数据增强或者用测试集做一次微调。4.5 模型保存后加载报错现象torch.save(model.state_dict(), best_model.pth)保存后加载时提示缺少 key 或 shape 不匹配。原因保存时模型结构是mobilenet_v3_small且分类头被替换过加载时如果重新构建的模型分类头输出类别数不一致就会报错。解决加载前先用相同的代码构建模型结构确保num_classes一致再调用model.load_state_dict(torch.load(best_model.pth))。如果只是推理用model.eval()切换到评估模式。5. 从训练到推理用 ONNX 导出与单张图片预测的完整链路训练完模型只是第一步真正要用起来还得能对单张叶片照片做预测。我一般会把 PyTorch 模型导出成 ONNX这样部署时不受 Python 环境限制推理速度也更快。import torch import onnx import onnxruntime as ort import numpy as np from PIL import Image from torchvision import transforms # 加载训练好的模型 model models.mobilenet_v3_small(weightsNone) model.classifier[3] nn.Linear(model.classifier[3].in_features, num_classes) model.load_state_dict(torch.load(best_model.pth)) model.eval() # 导出 ONNX dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export(model, dummy_input, crop_disease.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}}, opset_version11) # 用 ONNX Runtime 推理 session ort.InferenceSession(crop_disease.onnx) transform 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]) ]) img Image.open(test_leaf.jpg).convert(RGB) input_tensor transform(img).unsqueeze(0).numpy() outputs session.run(None, {input: input_tensor}) predicted_class np.argmax(outputs[0]) confidence np.softmax(outputs[0])[0][predicted_class] print(f预测类别: {train_dataset.classes[predicted_class]}, 置信度: {confidence:.4f})导出 ONNX 时opset_version11兼容性最好dynamic_axes让 batch 维度可变方便批量推理。推理时注意预处理必须和验证集完全一致否则准确率会掉。如果置信度低于 0.6建议输出「不确定」而不是强行给一个类别这在农业场景里比误判更安全。一个实用技巧把类别名称和对应的防治建议做成一个 JSON 映射表推理后直接返回病害名称和推荐用药这样整个链路就完整了。我自己的习惯是每次训练完先跑 20 张测试图人工核对一遍再部署这一步能拦住大部分低级错误。希望帮到你。本文还有配套的精品资源点击获取