☰
农作物病虫害识别系统实战:从迁移学习到模型部署
2026/10/1 23:25:45 网站建设 项目流程

简介:基于机器学习实现的农作物病虫害识别系统完整项目包,面向高校毕业设计、机器学习课程实践及农业信息化方向开发者。资源包含可运行的Python源码、训练数据、模型权重与说明文档,评审分数达95分以上,难度适中,适合作为完整课设或毕设参考。包体共477个文件,约82MB,涵盖前端展示页面(html/css/js)、Python后端与算法脚本(py)、图片及动图示例(jpg/png/gif)、说明文本(txt/json)以及模型文件(pth)和SQLite数据库,目录结构清晰,便于按模块检索和二次开发,前端基于Layui框架构建,交互直观。目前已有288人浏览学习。通过该资源可掌握从数据准备、模型训练到系统部署的完整流程,可直接运行验证识别效果,并借助说明文档快速理解项目架构与核心代码,为论文撰写和功能扩展提供支撑。

1. 这个压缩包到底能帮你省多少时间:识别系统的组成与适用场景

植保站的小王把手机凑近一片发黄的玉米叶拍照,几秒钟后屏幕上跳出“玉米大斑病,置信度0.92”——这个结果的背后,是一套用机器学习和Python训练出来的图像识别模型。标题里这个zip包,把源码、数据和说明文档三样东西打包在一起,对应的正是做这类系统从准备数据集、训练模型到写推理脚本的完整闭环。它适合三类人:想复现课题的在校生、接农业信息化项目的外包团队,以及想给自家果园做病害识别的技术型种植户。先说结论:这套系统能不能在真实农田里用住,取决于数据质量与部署策略,而不只是模型的准确率高低。

2. 从图像到病害标签:数据组织、迁移学习选型与基线检查

2.1 为什么这类系统都喜欢用迁移学习

农作物病虫害识别系统在机器学习算法层面属于细粒度图像分类任务,类别之间差异很小,比如稻瘟病的“叶瘟”和“穗颈瘟”在视觉上就差一个病斑位置。如果从零搭建卷积网络,需要大量同类样本才能学到稳定的纹理特征,而绝大多数开源数据集单类样本只有几百到几千张,直接训练很容易过拟合。另一个容易被忽视的问题是训练时间,从零训练一个ResNet50在单卡上可能要跑到十几个小时,而微调预训练权重通常几十分钟就能看到明显收敛。

一个更省事的方案是迁移学习:先加载ImageNet上训练好的模型权重,把最后的全连接分类头换成病虫害的类别数,重新训练。常见做法是把ResNet50、EfficientNet-B0或MobileNetV3作为backbone,前面几层冻结,后面几层连同新分类头一起微调。ResNet50的优点是结构成熟、资料多,任何报错都能搜到解法;EfficientNet-B0则在同等精度下参数更少,适合后期部署到只能用小模型的环境。如果压缩包说明文档里推荐了某个backbone,我一般会优先沿用,而不是自己换结构,因为源码里的数据增强、学习率调度基本都是按默认backbone调过的。

这里有个关键点要提醒:迁移学习不是把整个网络直接拿来跑推理。预训练模型学到的是通用边缘、纹理和形状特征,而病虫害的特征往往集中在病斑的颜色和分布上,因此训练时要把输入图像统一到224×224,并对训练集做适度的随机裁剪、旋转和色彩抖动,让模型学会关注病斑本身而不是叶片位置。这个预处理策略会直接影响后面所有环节,所以拿到压缩包后先读说明文档里的数据加载部分,确认归一化参数和图像尺寸,不要自己另起一套。

2.2 数据目录怎么摆:train/valid/test与标签文件

拿到源码后先不急着跑训练,先看说明文档里写的目录结构。绝大多数这类系统会采用train/valid/test三个文件夹按类分目录的布局,例如:

data/ ├── train/ │ ├── blight/ # 疫病 │ ├── rust/ # 锈病 │ └── healthy/ # 健康 ├── valid/ │ ├── blight/ │ ├── rust/ │ └── healthy/ └── test/ ├── blight/ ├── rust/ └── healthy/

每个病害类别对应一个文件夹,图片文件名一般没有特殊含义,不要依赖文件名解析标签。训练脚本会遍历文件夹名作为类别标签,所以文件夹命名必须全英文、不要带空格,避免Windows和Linux路径解析差异。如果压缩包里的目录结构和你自己的数据不一致,常见做法是把数据整理成这种规范布局,再在训练脚本里改一下--data参数即可,不要为了迁就脚本去重命名几万张图片。

除了文件夹式布局,也有项目把标签写在CSV或JSON里。常见做法是准备一个labels.json,内容大致是:

{ "class_names": ["healthy", "blight", "rust"], "num_classes": 3, "input_size": 224, "pretrained": true }

这里有个常见的坑:valid与train不能有重叠图片。如果数据集是从某个大目录里用随机函数划分的,务必要检查是否有人把同一张图同时放进了train和valid。否则验证集准确率会虚高,部署后完全不是那个效果。我拿到一个压缩包时,会用文件名的MD5值扫一遍train和valid集合,发现重复立刻剔除,这是一个成本极低但能避免后期大量返工的动作。

2.3 第一个可运行的检查脚本:统计类别分布与图片尺寸

开始训练之前,我习惯先跑一个几分钟的基线检查脚本,确认每个类别样本量是不是严重不均衡、图片是否有损坏。如果某类只有几十张,训练时就容易过拟合;如果图片尺寸差异很大,数据加载时的resize过程也会影响效果。这个步骤对应机器学习应用流程里的数据质量评估,跳过去直接开训,后面多半要回来补课。

import os from collections import Counter from PIL import Image data_root = "./data/train" cls_counts = Counter() widths, heights = [], [] for cls_name in os.listdir(data_root): cls_path = os.path.join(data_root, cls_name) if not os.path.isdir(cls_path): continue for img_name in os.listdir(cls_path): img_path = os.path.join(cls_path, img_name) cls_counts[cls_name] += 1 # 统计类别样本数 with Image.open(img_path) as im: widths.append(im.width) # 记录图片宽度 heights.append(im.height) # 记录图片高度 print("每类数量:", dict(cls_counts)) print("图片宽度范围: min=%d max=%d" % (min(widths), max(widths))) print("图片高度范围: min=%d max=%d" % (min(heights), max(heights)))

这段代码逐类统计文件夹里的图片数量,并读取每张图片的宽高。Counter用来累加类别样本数,PIL的Image.open不会立刻加载整张图,只在进入with块后读取头部信息,所以即使数据量大也不会吃太多内存。跑出来的结果如果某个类别数量是其他类的四分之一,就要考虑在训练时做类别加权采样,而不是直接开训。

这一步也能提前暴露损坏图片的问题。如果PIL在打开某张图时报OSError: image file is truncated,说明数据集里有不完整图片,应该在预处理脚本里统一过滤掉,而不是等训练到一半才翻车。很多源码包会忽略这种细节,你需要在数据准备阶段自己兜底。

3. 把源码跑起来:训练命令、超参与混淆矩阵评估

3.1 最小训练命令:一条命令跑通训练流程

这类系统的训练脚本通常可以通过命令行参数调整模型和训练配置。我一般会先按默认参数跑一个小epoch版本,确认流程能走通,再挂机做完整训练。最小命令类似:

python train.py \ --model resnet50 \ --data ./data \ --epochs 30 \ --batch-size 32 \ --lr 1e-4 \ --input-size 224 \ --pretrained True

--model指定backbone,--data指向数据根目录,--epochs是训练轮数,--batch-size是每步送入GPU的图片数,--lr是初始学习率,--input-size是送入模型的图片分辨率,--pretrained表示是否加载ImageNet权重。第一次验证流程时,可以把epochs临时改小到3,看它能否顺利完成一个完整周期;等日志正常了再恢复成完整数值。

常见做法是训练脚本会自动把数据按train/valid目录读取,并在每个epoch结束时计算验证集准确率,保存验证集准确率最高的权重为best_model.pt。如果源码里没有这一步,你需要自己写一个回调:每个epoch结束后把model.state_dict()存下来。这是因为后续做评估和导出重训成本很高,有一个“后悔药”能节省大量调试时间。我自己的习惯是每5个epoch存一个带epoch编号的权重文件,防止训练中断后只能从头开始。

3.2 三个必调超参:学习率、batch size与输入分辨率

第一是学习率。迁移学习场景下初始学习率设在1e-4到1e-3之间比较稳妥。如果从头训练,学习率可以大一些,但这里基本都是加载预训练权重,学习率过大会一下冲坏前面层学好的通用特征。更稳的做法是用学习率预热:前5个epoch从1e-5线性升到1e-4,之后再用余弦退火慢慢降。这个技巧在病虫害识别里特别有效,因为病斑特征和ImageNet的自然图像分布差异不大也不小,需要一个温和的适应过程。

注意:显存不足时不要只把batch size减半,优先检查输入尺寸是否过高,其次是开启梯度累积。

第二是batch size。显存不够时很多人会直接把batch size调小,但要明白这会影响BatchNorm层的统计量。比如从32降到8,模型每个step看到的样本变少,BN统计的均值和方差波动变大,可能让训练不稳定。如果显存确实只有6G,建议优先把输入尺寸从224降到192,而不是把batch size压到8以下;或者干脆开启梯度累积,每4个batch累积一次梯度再更新参数,等效于batch size为32的效果。

第三是输入分辨率。作物病害不少是叶片上的小斑点,224×224下几个像素的病斑可能直接被池化层忽略。如果数据清晰度够,把input-size调到256或384能明显提升小病斑召回率,但代价是显存占用和推理时间增大。在部署到手机之前,先在服务器上用高分辨率训练,再蒸馏或量化到小模型,是这几年的主流做法。

# 使用余弦退火与预热时,很多开源脚本支持这两个开关 python train.py --model efficientnet_b0 \ --lr 1e-4 --warmup-epochs 5 --cosine True

3.3 用混淆矩阵看它有没有“真会”:评估脚本

训练日志里的验证准确率在很多细粒度任务上并不可靠。假设三个类别分别占60%、20%、20%,模型把所有图片都判成第一类也能得到60%的准确率,误导性很强。因此训练结束后我都会跑一个混淆矩阵。

import torch from torch.utils.data import DataLoader from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay import matplotlib.pyplot as plt model.eval() # 切换到评估模式 y_true, y_pred = [], [] with torch.no_grad(): # 推理时不计算梯度 for images, labels in val_loader: outputs = model(images) _, preds = torch.max(outputs, 1) y_true.extend(labels.numpy()) y_pred.extend(preds.numpy()) cm = confusion_matrix(y_true, y_pred) ConfusionMatrixDisplay(cm, display_labels=class_names).plot() plt.savefig("confusion_matrix.png")

这段代码把验证集所有样本的预测结果和真实标签收集起来,交给sklearn生成混淆矩阵。对角线数值高说明每个类别都学得比较均衡;某一行其他位置有大量值,就说明这个类经常被错认到别的位置。比如“稻曲病”频繁被识别成“稻瘟病”,多半是两者病斑颜色和纹理相似,这时要看训练样本里这两类的数量是否相差过大,或者是否需要采集更多难例样本。

评估这一步不要只看准确率,还要关注每类召回率。农业场景里漏检比误报更严重:漏了会让病害继续扩散,误报了顶多是喷药多花一点成本。所以后面的阈值调整和人工抽检,都建立在这张混淆矩阵的基础上。

4. 训练翻车与田间失效:五个常见问题排查记录

4.1 损失函数不降反升:先看学习率,别急着换模型

现象:训练到第5个epoch,训练损失还在0.7上下震荡,甚至逐步走高;验证准确率始终在20%左右,和随机猜测差不多。

原因:最初级的原因通常是learning rate过大,模型在损失曲面两侧来回跳跃;也有可能是数据预处理没有做归一化,输入像素值范围不对。迁移学习用得越多,越容易忽略“加载预训练权重后要配套对应的归一化参数”这件事。

解决:先打印一次前向传播的输入张量均值和方差,确认图像数据已经按ImageNet的mean=[0.485,0.456,0.406]和std=[0.229,0.224,0.225]做了标准化。然后把学习率降到1e-5,跑5个epoch看看损失曲线是否下降。如果下降了,再逐步把学习率升回1e-4。我踩过的最大翻车是把学习率设成1e-2,第一轮损失直接爆炸,浪费了大半天。源码包里的默认参数如果没跑过,先小学习率探路,别一上来就按论文抄。

4.2 验证集90%田间就失效:背景过拟合与分布偏移

现象:验证集准确率有90%以上,把模型拿到田间拍的照片上一测,大量误报。

原因:很多数据集是在实验室环境拍的叶片照片,背景是统一白板或黑布;而田间拍摄会带泥土、杂草、光照不均匀和露珠。模型学到的是“前景纹理+背景颜色”的组合特征,而不是单纯病斑特征。这就是典型的数据分布偏移。

解决:训练数据里加入田间环境增强:随机改变亮度、对比度、添加噪声、随机遮挡,甚至把背景换成绿色或泥土色块。更直接的做法是把采集的田间照片按8:2切分,只把20%放进训练集,剩下80%全放进测试集来评估真实泛化能力。如果项目源码的数据集里没有这类场景,你需要自己补拍或用公开的自然场景病害数据做外部测试。这个问题在压缩包自带数据上往往看不出来,因为开源数据多半也是实验室拍摄的。

4.3 常见病样本多、稀有病害总被漏:类别不平衡

现象:总数上训练准确率85%,主要来自“健康”和“稻瘟病”两个大类;样本数量少的“胡麻斑病”召回率只有12%。

原因:训练集本身严重不均衡,模型通过优先学会多数类来降低整体损失,少数类别很少被正确预测。

解决:三条路结合使用。第一,做类别加权采样,每个batch里少数类的样本被抽中的概率更高;第二,使用Focal Loss,让模型聚焦在难分类样本上;第三,对少数类做更强的数据增强,比如随机旋转30度、水平翻转、色彩抖动,把已有的几十张扩充出更多变换。注意不要对少数类做拼接伪造样本,容易让模型学到不真实的病斑位置。

class_weights = torch.tensor([1.0, 1.0, 5.0]) # 给少数类更高权重 criterion = torch.nn.CrossEntropyLoss(weight=class_weights)

4.4 标签错标导致模型学歪:标注噪声的排查办法

现象:某个类别训练损失一直降不下去,混淆矩阵里它总被错认到相邻类别。

原因:数据集的标注可能是人工逐张标注的,难免有误标。比如一张同时感染两种病害的叶片,被判给了其中一种;或者早期病斑不明显,标注者直接标成了健康。

解决:把该类别里模型预测错误且置信度最高的前20张图片单独拉出来,人工复核。很多源码包叫这个“hard sample extraction”。如果发现其中几张确实标注错误,就把标签改正,而不是通过调整模型去硬拟合错误数据。农业数据里误标很难完全避免,但至少要把高置信度错误样本筛一遍,因为它们对训练损失的影响最大。

4.5 显存溢出与训练中断:梯度累积与断点续训

现象:训练到第10个epoch时CUDA out of memory,或者服务器重启,之前的训练全部白费。

原因:batch size太大、输入尺寸太高、多卡配置不当都会导致显存溢出;没有保存checkpoint,中断后只能从头再来。

解决:显存溢出时先尝试开启梯度累积,示例代码如下。

optimizer.zero_grad() loss = loss / accumulation_steps loss.backward() if (step + 1) % accumulation_steps == 0: optimizer.step() optimizer.zero_grad()

同时训练脚本里要每5个epoch保存一次checkpoint,保存内容包括model.state_dict()、optimizer.state_dict()、当前epoch和best_acc。恢复时加载这些信息继续训练,而不是重新初始化优化器。我自己通常保存last_epoch.pt和best_model.pt两个文件,前者用于断点续训,后者用于最终验证和导出。没有这个习惯的话,长训练每挂一次都是煎熬。

5. 从训练机到田间手机:ONNX导出、推理脚本与设备取舍

5.1 把PyTorch模型导出成ONNX并做精度校验

模型训练完,部署时一般不会直接把PyTorch模型搬到生产环境。更常见做法是导出成ONNX,再用ONNX Runtime加载推理。ONNX格式不依赖PyTorch运行时,也能在CPU上获得不错的加速效果。导出前先加载训练阶段保存的best_model.pt,把模型设置为eval模式,然后构造一个batch为1、尺寸为224×224的随机张量作为动态输入。

import torch model.load_state_dict(torch.load("best_model.pt", map_location="cpu")) model.eval() 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"}, "output": {0: "batch"}}, opset_version=13, )

这段代码里,dynamic_axes让batch维度可变,这样同一个onnx文件既能跑单张图片,也能在服务端一次处理多张图片;opset_version设为13兼容性较好。导出之后必须做一次精度校验:用同一张测试图片分别在PyTorch和ONNX Runtime里跑一遍,比较输出的概率分布是否一致。常见差异源于模型里有自定义op或导出时用了非标准的动态维度,遇到这种情况先检查opset版本,再检查模型里是否有torch.nn.functional.interpolate等对对齐敏感的算子。

5.2 写一个十分钟能上线的推理脚本:预处理、概率输出与置信度过滤

实际田间识别场景里,用户上传的图片可能是一个手持照片、扫描件甚至放大模糊图。推理脚本里预处理逻辑必须和训练时保持一致,否则再好的模型也会失灵。这里给出一个基于ONNX Runtime的推理函数。

import numpy as np import onnxruntime as ort from PIL import Image sess = ort.InferenceSession("crop_disease.onnx") input_name = sess.get_inputs()[0].name def predict(img_path, class_names, top_k=3, threshold=0.5): img = Image.open(img_path).convert("RGB").resize((224, 224)) arr = np.array(img).astype(np.float32) / 255.0 arr = (arr - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225] arr = arr.transpose(2, 0, 1)[None, ...] logits = sess.run(None, {input_name: arr})[0] exp_logits = np.exp(logits - logits.max(axis=1, keepdims=True)) probs = exp_logits / np.sum(exp_logits, axis=1, keepdims=True) top_idx = np.argsort(-probs, axis=1)[0, :top_k] results = [(class_names[i], float(probs[0, i])) for i in top_idx] return [r for r in results if r[1] >= threshold]

这段代码的核心是保持预处理与训练一致:缩放、归一化、通道重排、增加batch维度。probs用softmax从logits换算成概率,threshold=0.5表示低于50%的预测直接过滤,不返回给用户。农业场景下置信度阈值一般设在0.5到0.7之间,过低会大量误报,过高会漏掉真实病害。最终输出里带上类别名和概率值,前端拿到后可以决定是展示“疑似”还是“确诊”。

5.3 服务器和手机端的取舍:量化、尺寸与帧率

如果系统最终部署在服务器上,有GPU就继续保持float32精度,并让ONNX Runtime跑GPU EP;没有GPU,纯CPU推理一张224×224图片大概在100到300毫秒,农业识别场景并不要求实时视频流,所以还能接受。如果要做成手机App拍照识别的形式,就要考虑量化。

MobileNetV3这类小网络在int8量化后模型体积能从10MB降到3MB以下,推理速度提升两倍左右。但量化可能会让原本就弱的小病斑特征进一步受损。常见的做法是先用高分辨率大模型在服务器端蒸馏一个小模型,或者先做int8量化,再用一组难例回传数据做校准集。无论哪种方式,量化后的模型都要在田间图片上重新测一次混淆矩阵,不能只看量化前后的准确率差值。

这里有一个我对很多团队的提醒:移动端部署不只是换推理框架。手机镜头拍出来的叶片图片光照和角度差异极大,应用层最好先做一次图像质量判断,比如模糊度超过阈值就提示用户重拍。否则模型面对再多的预处理也是白搭。

6. 用人工抽样与田间反馈校准你的识别系统

真正让这套系统在农田里被信任的,不是Kaggle式的准确率榜单,而是让植保员拿起手机拍真实叶片,看他是否认可模型的结论。我常用的验证方法是一张人工抽检表:取200张模型置信度分布不同的图片,找懂病害的农艺师逐张给标签,不用重训,先看模型预测和人工判断在哪些类别上频繁不一致。重点关注“不确诊”的请求比例——如果模型在欠曝或逆光图片上总是给出0.3以下的置信度,那就要考虑调整输入图像增强策略,而不是调分类阈值。

另一个小习惯能省不少后期维护成本:把每次识别结果连同原图按“识别时间、模型版本、置信度、用户是否反馈错误”存下来。这些数据是后续微调和量化校准的重要素材。很多团队训练完就丢进文件夹,等到田间翻车再重新采集,时间已经过去一个生长季。

我在做类似系统时摔过最重的一跤,是把训练好的模型直接接到Web后端,没有给前端的“识别结果”留人工纠错入口。结果用户反馈了几十条错误样本,却只能在聊天记录里翻。后来在返回结果里加了一个“这个结果不对”的按钮,每周导出一批反馈重新标注,模型的田间准确率才慢慢拉起来。

如果这个压缩包只帮你跑通了一套训练流水线,它至少证明了这条路走得通;但真正的价值点在于如何把本地训练、田间采集和人工反馈循环起来。把这个闭环建立起来,你的识别系统才算真正落地。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询