
简介基于机器学习实现的农作物病虫害识别系统完整项目包面向高校毕业设计、机器学习课程实践及农业信息化方向开发者。资源包含可运行的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 True3.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_labelsclass_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(weightclass_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_locationcpu)) 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_version13, )这段代码里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_k3, threshold0.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(axis1, keepdimsTrue)) probs exp_logits / np.sum(exp_logits, axis1, keepdimsTrue) top_idx np.argsort(-probs, axis1)[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换算成概率threshold0.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后端没有给前端的“识别结果”留人工纠错入口。结果用户反馈了几十条错误样本却只能在聊天记录里翻。后来在返回结果里加了一个“这个结果不对”的按钮每周导出一批反馈重新标注模型的田间准确率才慢慢拉起来。如果这个压缩包只帮你跑通了一套训练流水线它至少证明了这条路走得通但真正的价值点在于如何把本地训练、田间采集和人工反馈循环起来。把这个闭环建立起来你的识别系统才算真正落地。希望帮到你。本文还有配套的精品资源点击获取