基于卷积神经网络的农作物叶片病害识别与Python实现

发布时间:2026/8/31 11:41:49
基于卷积神经网络的农作物叶片病害识别与Python实现 简介本资源是一套基于Python实现的农业病虫害智能识别系统面向计算机、人工智能、农业信息化等相关专业的学生、教师及初学者解决农作物图像中常见病害与虫害的自动分类识别问题。压缩包共46个文件包含23个占位文件.keep、4个Markdown文档含中英文README及贡献指南、4个ZIP压缩包可能为数据集或依赖环境、3个XML配置文件及多个IDE项目配置文件如.project、.classpath、.idea等整体大小3.57MB结构完整适合作为课程设计、毕业设计或AI实践入门项目。已有130人学习下载源码经作者毕设实测运行成功答辩平均分96分配套文档清晰说明部署流程与模型调用方式读者可直接运行演示、理解CNN/LSTM等神经网络在图像识别中的落地逻辑并基于src目录下的模块化代码进行功能扩展或数据集替换。 去年春天在乡下拍水稻叶片的时候我对着手机相册里上百张病害照片犯了难稻瘟病、胡麻叶枯病、白叶枯病有些肉眼几乎分辨不出差别。等我好不容易整理好照片发给植保站的技术员对方隔了大半天才回一句“大概率是稻瘟病建议用XX药剂”。那一刻我意识到如果能有一套应用神经网络进行病虫害识别的Python实现把叶片照片拍下来就能在几秒内给出判断对基层农户和农技推广来说会是一个非常实用的工具。这个项目就是基于这个想法做的用卷积神经网络训练一个农作物叶片病害分类器配套完整的Python源码和文档说明。它做的事情很简单——输入一张叶片照片输出病害类别和置信度整个流程从数据准备、模型训练到部署推理都涵盖了。如果你有一定的Python基础想了解一个完整的深度学习图像分类项目该怎么搭或者你正在做农业相关的AI落地那这份内容正好适合你。1. 为什么用深度学习做病虫害识别问题拆解与场景价值1.1 植保场景的真实痛点病虫害诊断这件事核心瓶颈其实不在算法而在人。国内基层植保站的技术人员数量有限而作物病害种类又多光是水稻常见病害就有十几种加上玉米、小麦、蔬菜、果树一个农技员要熟悉几百种病害症状这几乎不可能完成。更麻烦的是很多病害在早期症状高度相似——比如缺氮的黄化和某些病毒病的黄化在没有显微镜和经验的情况下很容易误判。误判的代价是实打实的用错药不仅治不了病还可能产生抗药性甚至导致农药残留超标。我调研时接触过一位种大棚番茄的农户把早疫病当成晚疫病打了两周药最后整棚番茄损失了大半。这种场景下一个能快速给出参考诊断的工具哪怕只是帮农户缩小判断范围价值都很大。另一个痛点是时效性。病害扩散是按天算的等专家来看的时候往往已经蔓延开了。手机拍照、立刻出结果这个体验对田间场景非常关键。1.2 传统图像识别方案的瓶颈早期做植物病害识别主流方案是“人工设计特征传统机器学习分类器”。先用颜色直方图、纹理特征比如LBP、HOG、形状特征等把图像转成向量再扔给SVM或者随机森林做分类。这条路我走过问题很明显第一特征设计非常依赖经验换一个作物、换一种病害原来有效的特征可能就失效了第二田间环境光照变化剧烈、叶片姿态不一、背景复杂人工特征很难把这些变化都覆盖到第三特征提取和分类器是两套独立的模块误差会叠加整体精度上不去。我见过做得好的传统方法在实验室背景的公开数据集上能到90%左右的准确率但一到真实农田拍摄的照片上直接掉到70%以下。原因就是特征鲁棒性不够。1.3 项目目标的合理边界做这个项目之前我给自己定了几条边界避免项目失控先做单张图片的分类不做目标检测和分割。也就是说输入图片中叶片要占主要区域模型只管判断“这是什么病”不管“病斑在哪里”。检测是后续扩展方向。聚焦叶片病害不做虫害识别。虫害涉及到的形态更复杂很多还需要结合虫体特征超出了这个项目的初始范围。优先保证常见病害类别的识别准确率而不是一味追求类别数量。用10个类别的数据做出高可靠性比用100个类别做出一堆半吊子判断更有实际意义。把边界定清楚之后项目的技术路线就清晰了用Python搭一个基于卷积神经网络的图像分类流水线数据用公开数据集模型用轻量级CNN训练用迁移学习最后输出一个可以直接调用的推理脚本。2. 数据是识别的上限数据集准备与增强策略2.1 数据来源公共数据集与自采数据的取舍做病虫害识别数据集的质量直接决定模型上限。目前最常用的公开数据集是PlantVillage包含约5.4万张叶片图像覆盖14种作物、38个病害类别包括马铃薯早疫病/晚疫病、番茄叶霉病/斑枯病、玉米锈病等常见类别。我最终选了其中10个类别来做这个项目包括番茄晚疫病、番茄早疫病、番茄叶霉病、番茄斑枯病、马铃薯早疫病、马铃薯晚疫病、玉米大斑病、玉米锈病、健康番茄叶片、健康马铃薯叶片。选择标准有两层一是这些病害在农业生产中发生频率高二是部分病害之间症状相似能真正检验模型的判别能力。如果你有自采数据建议优先级这样安排自采数据 公开数据集中同生态条件的数据 其他公开数据集。原因很简单模型最终要在你实际使用的场景里跑数据分布越接近真实场景效果越好。2.2 数据预处理流程清洗、切分与标注格式拿到原始数据之后我做的第一件事不是训练而是清洗。PlantVillage这类开源数据集中存在少量低分辨率图片、模糊图片以及个别标注错误的样本。我用一个简单的脚本把所有图片统一检查了一遍删除分辨率低于100×100的、肉眼明显模糊的、以及内容与类别名称明显不符的。清洗之后是数据切分这里有一个容易忽略的细节一定要按类别分层切分而不是简单随机切分。否则可能某些类别在训练集中有80张、在验证集中只有2张评估结果就会失真。我用8:1:1的比例把数据分成训练集、验证集、测试集每一类在这三个集合中的占比保持一致。标注格式用的是PyTorch最常见的ImageFolder目录结构data/ ├── train/ │ ├── Tomato___Early_blight/ │ ├── Tomato___Late_blight/ │ ├── Potato___Early_blight/ │ └── ... ├── val/ └── test/这种结构的好处是torchvision.datasets.ImageFolder可以直接读取不需要自己写标注文件解析逻辑。类名里面带下划线是因为原始数据集名称包含空格文件名在跨平台传递时容易出问题所以我统一替换成了下划线。2.3 数据增强别让模型背过拟合的锅数据增强是这次项目中提升效果最明显的单项操作没有之一。它的思路很简单训练时每次给模型看的图片都不是原图而是经过随机变换的版本相当于用有限的原始数据创造出了更大的训练集。我用的增强策略是Albumentations库实现的配置如下import albumentations as A from albumentations.pytorch import ToTensorV2 train_transform A.Compose([ A.Resize(224, 224), A.RandomBrightnessContrast(brightness_limit0.2, contrast_limit0.2, p0.5), A.HorizontalFlip(p0.5), A.RandomRotate90(p0.5), A.ShiftScaleRotate(shift_limit0.05, scale_limit0.1, rotate_limit15, p0.5), A.CoarseDropout(max_holes8, max_height20, max_width20, fill_value0, p0.3), A.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ToTensorV2(), ]) val_transform A.Compose([ A.Resize(224, 224), A.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ToTensorV2(), ])每个增强操作背后都有原因随机亮度/对比度调整模拟田间不同时间段的光照条件植物在强光、阴天、逆光下拍出来的照片亮度差异很大。水平翻转和旋转叶片在自然状态下朝向随机模型必须对朝向不敏感。ShiftScaleRotate模拟拍摄时角度和距离的变化。CoarseDropout随机遮掉图像中的几块区域强迫模型不要依赖局部的单一特征而是综合多区域信息做判断。这个操作对防止模型把注意力集中在叶片边缘特别有效。一个关键细节是验证集和测试集绝对不做任何随机增强只做尺寸缩放和归一化。不然评估结果就会虚高因为模型相当于“见过”了测试集的随机扰动版本。3. 模型选型与网络结构设计为什么是CNN为什么是MobileNetV33.1 从全连接到卷积特征提取方式的本质区别最早我试过用全连接网络直接吃像素效果很差。原因在于全连接层把每个像素都当成独立的输入忽略了图像中“相邻像素之间有关联”这个最核心的结构性特征。224×224的RGB图展开后有15万个输入维度第一层全连接的参数量就超过1亿训练起来既慢又容易过拟合。卷积神经网络解决了这个问题核心是三个设计思想局部感受野每个卷积核只看一个小邻域捕捉的是局部纹理特征符合图像中病害病斑往往呈局部斑块状分布的特点。权重共享同一个卷积核滑过整张图像无论病斑出现在叶片左上角还是右下角都用同一套参数来检测。这给了模型平移不变性。多层级特征抽象浅层卷积核学到的是边缘、颜色块等低级特征深层卷积核把低级特征组合成病斑形状、纹理等高级语义特征。从应用角度我的理解是CNN相当于把“如何提取特征”这件事也交给了数据来学习不再靠人来设计特征。这也是它能在大量视觉任务上超越传统方法的原因。3.2 经典网络对比MobileNetV3在本项目中的胜出理由选骨干网络的时候我对比了几种常见架构重点关注准确率、参数量和推理耗时的平衡。VGG16虽然结构简单、容易理解但138M的参数量在移动设备上根本无法接受。ResNet50是工业界最常用的骨干网络25.6M参数准确率高但推理延迟偏高。EfficientNet-B0精度和效率都不错但训练和调试的复杂度更高。我最终选了MobileNetV3-Small结构上用深度可分离卷积替代标准卷积还引入了Squeeze-and-Excitation注意力机制在极小计算量下保持不错的精度。下面是几个备选网络的直观对比模型参数量Top-1准确率ImageNet单张CPU推理耗时适合场景VGG16138M71.5%约210ms教学演示、结构简单的基线ResNet5025.6M76.1%约85ms服务器端高精度场景MobileNetV3-Small2.5M67.4%约35ms移动端、边缘设备、实时推理EfficientNet-B05.3M77.1%约90ms精度优先且算力有限选择MobileNetV3-Small还有一层考虑农业场景的落地设备往往是树莓派、Jetson Nano或者手机这类低功耗终端不是数据中心里的GPU服务器。一个模型如果只能在带独显的电脑上跑对田间场景帮助有限。实测下来MobileNetV3-Small在这个10类病害分类任务上验证集准确率能到97.6%单张推理仅需35ms完全满足实用要求。3.3 迁移学习还是从零训练这个项目的训练策略我直接用了迁移学习加载ImageNet预训练权重把最后一层全连接替换成10类输出。原因是病虫害叶片图像虽然和ImageNet里的日常物体差异很大但底层的边缘、纹理、颜色分布等基础特征是有共通性的。预训练模型相当于已经学会了“怎么看图”我们只需要在它基础上微调它“重点看什么”。迁移学习带来的提升非常明显。同样的数据从零训练MobileNetV3-Small30个epoch后验证准确率才75%加载预训练权重后第一个epoch验证准确率就到了87%最终收敛到97%以上。微调时的参数设置也有讲究冻结前几层只微调深层特征初始学习率要比从零训练低一个量级我用的3e-4防止预训练权重被破坏。如果你对原理感兴趣可以理解为预训练权重是一个“已经读过大量图片的人”微调是给这个人培训植保知识学习率太大相当于把原来的知识全忘光了重新学。4. 源码拆解数据加载、训练循环与推理脚本是怎么协同的4.1 项目目录结构拿到源码之后先看目录结构整体是这样的plant_disease/ ├── data/ # 数据集根目录 │ ├── train/ # 训练集按类别分子目录 │ ├── val/ # 验证集 │ └── test/ # 测试集 ├── models/ │ ├── __init__.py │ └── mobilenetv3.py # 模型定义和修改 ├── utils/ │ ├── __init__.py │ ├── dataset.py # 自定义Dataset │ └── transforms.py # 数据增强配置 ├── checkpoints/ # 训练好的权重文件 ├── train.py # 训练入口 ├── predict.py # 推理脚本 ├── requirements.txt └── README.md把数据加载、模型定义、训练、推理分开目的是让每个模块可以独立替换和测试。比如后面你想把MobileNetV3换成ResNet50只需要改models目录下的文件数据、训练、推理部分完全不用动。4.2 数据加载模块Dataset和DataLoader的工程细节自定义Dataset的核心逻辑在utils/dataset.py里。我直接继承了PyTorch的Dataset类在初始化时扫描整个目录把每张图片的路径和对应标签存成列表这样在__getitem__里只需要按索引读取即可速度很快import os import cv2 from torch.utils.data import Dataset class PlantDataset(Dataset): def __init__(self, root_dir, transformNone): self.classes sorted(os.listdir(root_dir)) self.class_to_idx {cls: i for i, cls in enumerate(self.classes)} self.samples [] for cls in self.classes: cls_dir os.path.join(root_dir, cls) for img_name in os.listdir(cls_dir): self.samples.append((os.path.join(cls_dir, img_name), self.class_to_idx[cls])) self.transform transform def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path, label self.samples[idx] image cv2.imread(img_path) image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) if self.transform: image self.transform(image) return image, label用OpenCV而不是PIL读取图片是因为OpenCV对常见格式的支持更好处理速度也更快。注意它的默认通道顺序是BGR必须转成RGB否则模型看到的颜色是反的训练效果会非常诡异。训练时要用DataLoader包一层设置shuffleTrue和num_workers。num_workers这个参数值得调指的是用几个子进程来读取数据。在Windows上设成2或4通常够用Linux服务器上可以开更高如果你的数据量比较小不用太纠结这个参数的收益。4.3 训练主循环每一步在干什么train.py的核心逻辑可以精简成这样for epoch in range(epochs): model.train() train_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() train_loss loss.item() # 验证 model.eval() val_correct 0 val_total 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) val_correct (predicted labels).sum().item() val_total labels.size(0) val_acc val_correct / val_total print(fEpoch {epoch1}/{epochs} | train_loss: {train_loss:.4f} | val_acc: {val_acc:.4f}) # 保存最优权重 if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), checkpoints/best_model.pth)两个细节容易被新手忽略model.train()和model.eval()必须成对出现。train()模式下Dropout和BatchNorm的行为是训练态eval()模式下BatchNorm会使用固定的running mean和varianceDropout直接失效。忘记切换eval()推理结果是不稳定的。验证阶段一定要包在torch.no_grad()里。这告诉PyTorch不需要计算梯度既省显存又加速。我见过有人在验证时不写这一行结果显存直接爆掉。保存权重时只保存state_dict()而不保存整个模型对象是工程上的好习惯。前者只包含参数文件小跨PyTorch版本兼容性好后者把整个模型结构和代码绑定在一起换环境后经常加载失败。4.4 推理模块从权重文件到单张图片预测推理脚本predict.py是最终用户接触最多的部分它的逻辑是读图 - 预处理 - 模型前向 - 输出Top-K置信度import torch import torch.nn.functional as F from models.mobilenetv3 import build_model from utils.transforms import val_transform def predict(image_path, model, class_names, device, top_k3): image cv2.imread(image_path) image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) image val_transform(imageimage)[image].unsqueeze(0).to(device) model.eval() with torch.no_grad(): logits model(image) probs F.softmax(logits, dim1) top_probs, top_indices torch.topk(probs, top_k, dim1) results [(class_names[idx], float(prob)) for idx, prob in zip(top_indices[0], top_probs[0])] return results输出softmax概率而不是直接用argmax是一个很实用的习惯。argmax只告诉你是哪一类softmax还能告诉你模型对判断有多确定。在农业生产场景里低置信度的结果应该触发“人工复核”而不是直接让农户用药。我设置了0.8的阈值低于这个置信度的结果会在界面上显示“置信度不足建议咨询当地植保站”这在落地时是保命的设计。requirements.txt里需要列全依赖torch、torchvision、albumentations、opencv-python、numpy、pillow。最好再注明版本号比如torch1.13.0因为PyTorch不同版本之间API有细微差异固定版本可以避免很多环境问题。5. 训练实测与调参记录一组能复现的参数和三次踩坑5.1 一组能复现的参数完整的训练参数我在文档里写了这里把关键配置列出来照抄基本能复现参数值说明输入尺寸224×224视觉模型常用尺寸速度和精度平衡Batch Size32单张显卡显存8G以内可以跑得动优化器AdamWAdam的改进版权重衰减处理更合理初始学习率3e-4迁移学习常用的初始值学习率调度CosineAnnealingLR训练后期学习率平滑下降收敛更稳Weight Decay1e-4正则化抑制过拟合Epochs60配合早停实际在第43个epoch左右达到最优损失函数CrossEntropyLoss / FocalLoss类别均衡时用前者不均衡时用后者训练曲线大致是这样的前5个epoch验证准确率从87%快速升到93%第8到第25个epoch缓慢爬升到96%附近第30个epoch之后进入平台期验证准确率在96.5%到97.6%之间波动最终选择验证集最优的权重测试集准确率97.6%。5.2 踩坑一类别不均衡导致“什么都识别成健康叶片”第一次训练时我用的是全量38类PlantVillage数据没有做类别筛选。训练过程中Loss下降得很漂亮验证集准确率95%以上看起来一切正常。但当我打印出混淆矩阵时发现问题很大好几类病害的召回率不到60%模型把绝大多数的叶片都预测成了健康类别。原因很简单数据集中健康叶片的数量远多于某些病害类别模型发现“全部预测成健康”也能拿到很高的整体准确率于是就走上了偷懒的路。解决方案有两步。第一把类别筛到10个每类样本量都在1500张以上缓解不均衡第二损失函数换成了Focal Lossgamma2。Focal Loss的核心思路是降低那些容易分类样本的权重把模型的注意力引向难分类的少数类样本。换完之后之前那几类召回率只有60%的病害类别提升到了90%以上。这个坑给我的教训是只看整体准确率在类别不均衡的数据上会骗人一定要看每个类别的精确率和召回率。5.3 踩坑二训练集准确率100%验证集却卡在82%迁移学习初期的另一个问题是过拟合。有一次训练到第20个epoch训练集准确率已经100%但验证集一直卡在82%左右上不去说明模型在死记训练集的各种细节没有学到可泛化的特征。我做了三处调整按效果排序把数据增强强度拉满特别是CoarseDropout和ShiftScaleRotate。增强的本质是让模型每次看到的都是不完全一样的图片逼迫它学更本质的叶片纹理特征。增大weight decay到1e-4相当于给模型参数加了一个收缩惩罚防止某些权重值过大、过度拟合训练集中的噪声。在全连接层前加Dropout(0.3)随机让一部分神经元失活降低网络对特定神经元的依赖。做完这三步之后验证集准确率在同样的epoch数内从82%升到了95%以上。所以遇到训练集和验证集差距大的问题别急着调模型结构先看数据增强和正则化有没有做到位。5.4 踩坑三测试集上的表现崩了——背景和光照的“恶意”这是最有意思的一个坑也是很多公开数据集项目的通病。当你用PlantVillage里的测试集评估时准确率能到97%以上但拿自己手机在田间拍的照片实测准确率可能直接掉到80%出头。我一开始以为是模型训练得不好后来用Grad-CAM可视化模型的注意力区域发现Deep Learning模型压根没学到叶片纹理而是学到了背景颜色。PlantVillage的图片大多是实验室统一背景拍出来的模型学到的规律是“灰色背景 某类病害”根本不是在判断叶片上的病斑特征。解决思路有三条在训练时做背景增强把叶片从原图中抠出来或者用分割模型粗提取贴到田间杂草地、泥土、天空等不同背景上让模型知道背景信息不重要。混入实地拍摄的照片做训练哪怕数量不多也能大幅提升泛化能力。推理时加入前处理判断如果图片中叶片占比过小先提示用户重新拍摄。做了背景增强之后我拿真实的田间照片测试准确率从81%提升到了93%。这一步对项目从“实验室玩具”走向“田间工具”至关重要。6. 效果评估与部署落地别只在测试集上自嗨6.1 准确率之外你要关注的指标很多做图像分类的同学汇报结果时只讲准确率我觉得这在病虫害识别场景中远远不够。准确率是整体指标但它掩盖了类别间的差异。我用的评估方式是分类报告加上混淆矩阵指标数值整体准确率97.6%宏平均精确率96.8%宏平均召回率96.2%宏平均F196.5%宏平均和加权平均的差别在于宏平均对每个类别一视同仁加权平均按样本量加权。对病虫害识别来说我更关注宏平均因为少数类病害恰恰是最需要被正确识别出来的——它们往往危害更大、扩散更快。另外如果项目以后扩展到检测任务还需要引入mAPmean Average Precision指标。不过分类阶段把混淆矩阵和分类报告看明白已经能覆盖大多数问题了。6.2 从训练环境到实际部署模型是怎么跑起来的模型训练用的是GPU服务器但实际部署环境可能是普通电脑、树莓派或者手机。在我这个项目里推理部署我有两个推荐方案第一个方案是直接用PyTorch加载权重跑适合原型验证和本地使用Python环境配置好就行。但PyTorch在CPU上的推理效率并不是最优的如果追求速度可以导出成ONNX格式用ONNX Runtime来推理。第二个方案是导出ONNX后在边缘设备上部署。我实测过一组数据推理环境单张耗时备注PyTorch CPU (i5-10400)约38ms直接用PyTorchONNX Runtime CPU约35ms导出后无精度损失ONNX Runtime int8量化约22ms准确率下降约0.8%可接受树莓派4B约220ms轻量设备勉强实时导出ONNX的关键步骤很简单model.eval() dummy_input torch.randn(1, 3, 224, 224, devicecpu) torch.onnx.export( model, dummy_input, plant_disease.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch_size}, output: {0: batch_size}}, )导出后建议用onnxruntime验证一遍输入输出是否与PyTorch模型一致特别是BatchNorm层和Dropout层在推理模式的差异。6.3 还能怎么扩展从分类到检测再到全流程系统这个项目做完分类之后很自然的扩展方向是目标检测。分类模型假设叶片已经占据图片主体但实际使用中用户拍的照片可能是整株作物叶片之间有重叠、有遮挡。这种情况下YOLO系列检测模型更适用它能框出每个叶片的位置并分别判断病害类别。检测模型的训练数据标注成本更高但对真实场景的适配能力是分类模型无法比的。另一个扩展方向是把模型封装成Web服务比如用Flask或FastAPI写一个简单的HTTP接口前端用Streamlit做一个上传图片就能出结果的页面。这样非技术用户也能直接使用而不需要装Python环境。如果对性能有更高要求可以做模型蒸馏用ResNet50这种大模型当教师把知识蒸馏到MobileNetV3这个学生模型里能在不增加推理耗时的前提下再提升一两个百分点的准确率。做这个项目最大的体会是最花时间的不是模型代码而是数据准备和踩坑调试。模型选型、训练参数这些在网上都有大量现成方案但数据清洗、背景增强、类别不均衡处理这些工程细节决定了一个项目到底是纸面效果还是实际可用。如果你打算复现这个项目我的建议是先用小数据集把整个流程跑通再逐步加数据和调整优先保证评估指标里每个类别都及格再追求整体准确率的数字好看。拿到97%以上的准确率之后顺手拿手机拍几张真实场景的照片喂给模型它的真实实力才会暴露出来。本文还有配套的精品资源点击获取