简介:这是一套面向计算机相关专业学生与项目实战学习者的农作物病虫害识别分类完整项目包,可直接用于毕业设计、课程设计或期末大作业。项目以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(brightness=0.2, contrast=0.2, saturation=0.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', transform=train_transform) val_dataset = datasets.ImageFolder('dataset/val', transform=val_transform) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4, pin_memory=True) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4, pin_memory=True) print(f"类别数: {len(train_dataset.classes)}") print(f"训练样本: {len(train_dataset)}, 验证样本: {len(val_dataset)}")这段代码的关键点在于:训练集用了RandomResizedCrop和颜色抖动,模拟田间拍摄时角度和光照的变化;验证集不做增强,保证评估结果稳定。Normalize的均值和标准差必须用 ImageNet 的统计值,因为后面加载的是 ImageNet 预训练权重。num_workers设为 4 是经验值,设太大在 Windows 上容易出问题,设太小则数据加载成为瓶颈。pin_memory=True在 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(weights=models.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(), lr=1e-3, weight_decay=1e-4) scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=30) 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(f"Epoch {epoch+1}/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让学习率按余弦曲线下降,避免后期震荡。每轮验证后保存最佳模型,防止过拟合后反而丢失最优权重。
参数说明:lr=1e-3是分类头训练的常用初始学习率;weight_decay=1e-4做 L2 正则;T_max=30与总轮数一致。如果验证准确率在 15 轮后不再提升,可以把model.features最后两个 block 解冻,用lr=1e-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_decay=1e-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(weights=None) 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_version=11) # 用 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_version=11兼容性最好,dynamic_axes让 batch 维度可变,方便批量推理。推理时注意预处理必须和验证集完全一致,否则准确率会掉。如果置信度低于 0.6,建议输出「不确定」而不是强行给一个类别,这在农业场景里比误判更安全。
一个实用技巧:把类别名称和对应的防治建议做成一个 JSON 映射表,推理后直接返回病害名称和推荐用药,这样整个链路就完整了。我自己的习惯是每次训练完先跑 20 张测试图,人工核对一遍再部署,这一步能拦住大部分低级错误。希望帮到你。
本文还有配套的精品资源,点击获取