ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于机器学习的农作物病虫害识别系统:从数据预处理到模型部署完整指南

基于机器学习的农作物病虫害识别系统:从数据预处理到模型部署完整指南 简介基于机器学习实现的农作物病虫害识别系统是一套完整的毕业设计项目面向需要完成毕业设计、期末大作业或课程设计的学生。项目包含全部源码和配套数据集代码注释详细新手也能快速看懂据称是个人多次打磨的高分项目导师认可度高下载后简单部署即可运行。压缩包共四百七十七个文件约八十二兆字节涵盖源码文件、网页前端文件结构、样式、脚本、模型权重文件、多种图像样本以及数据库文件等各类型文件分工明确目录结构清晰便于按模块学习、调试和二次开发。系统遵循数据预处理、标注、特征提取、模型训练与评估、系统实施、实时识别的标准流程源码中均有对应实现适合在答辩演示时快速展示效果。目前已有六百六十四人学习下载对于想快速完成高质量结课项目并理解机器学习实践流程的初学者具有不错的参考价值。1. 这是把“种地”和“算法”绑在一起的毕设农作物病虫害识别到底在做什么每年毕业设计选题季总有一批学生会卡在同一个问题上既要让导师觉得工作量够又要让自己能在三个月内做完还得保证答辩时能现场跑出效果。农作物病虫害识别系统恰好卡在这个平衡点上——它有明确的任务边界给一张叶子照片判断是哪种病有现成的公开数据集有不只一种能落地的算法路线而且结果可以可视化展现做成网页、小程序或者桌面界面都不违和。它的本质是图像分类任务但套上了农业应用的外壳后工作量感知立刻不一样。这个方向适合谁适合那些有一定 Python 基础、想避开纯算法创新、又不想做“管理系统”那种看上去没什么技术含量的学生。它也适合想快速验证一个完整机器学习流程的人从数据准备、模型训练、评估到部署一条链路全部覆盖。下面我会按我自己做这类项目的习惯把从零到答辩演示的整条路径讲清楚包括参数怎么调、哪些地方会莫名其妙翻车、以及怎么做才不至于让毕设变成“调包侠”现场。2. 先想清楚用哪条技术路线传统机器学习还是深度学习坦白说标题里写的是“机器学习”但你在实际做的时候会发现病虫害识别这类图像任务纯传统机器学习路线特征工程 分类器能出效果但天花板很低。别急着选路先搞明白两种路线的边界再决定你的系统主体用什么。2.1 传统机器学习路线手工特征 SVM/随机森林的组合早年没有深度学习铺路的时候作物病害识别是这么做的把叶片图像转成颜色特征、纹理特征比如 GLCM 灰度共生矩阵、形状特征然后用 SVM 或随机森林做分类。特征是人设计的模型负责在这些特征上找决策边界。这套流程在数据量小、类别少、背景干净的情况下是能跑的而且可解释性好——你很清楚模型是“看着”哪些特征下判断的。但它的脆弱性也很明显叶片在田间的照片背景杂乱、光照不均、叶子互相遮挡手工特征很难扛住这些干扰。换一批拍摄条件不同的图片准确率会掉得很明显。所以除非你的毕设题目明确要求“不可以使用深度学习”或者你的数据集只有几百张图且背景被预处理得很干净否则我不建议把传统路线当主线。它更适合放进论文里当“基线对比”——用朴素方法跑一个结果再让深度学习方法把这个结果打下去这正好能体现工作量。2.2 深度学习路线预训练模型微调才是实践中的默认选型真实项目中大家默认的做法是用卷积神经网络而且基本不会从零训练——用 ImageNet 上预训练好的权当做初始化然后在自己的病虫害数据集上微调。这背后的逻辑很简单病虫害叶片图像里有很多底层视觉结构叶脉、颜色过渡、斑点纹理和 ImageNet 里自然图像共享把预训练模型已经学会的底层特征迁移过来你只需要让模型去适应“叶片上的异常斑点”这种特殊的高层模式。ResNet 系列和 EfficientNet 系列是这类任务里最常见的两个选择ResNet18 轻量不容易过拟合EfficientNet 在同参数量下精度通常更好但调参更敏感。有些同学会纠结用 PyTorch 还是 TensorFlow我的建议是 PyTorch倒不是因为它一定更好而是因为毕业设计阶段你大概率要读不少开源代码做参考PyTorch 生态里的图像分类例子最多、最容易被看懂遇到报错也更容易搜到答案。框架本身对你的最终评分几乎不影响能跑通、能说清楚原理才是重点。2.3 数据层面的选型公开数据集目录结构决定训练代码样式病虫害识别的公开数据集通常按“作物_病害名”组织目录比如 tomato___leaf_mold、potato___early_blight 这种命名。这种结构天然适配 PyTorch 的torchvision.datasets.ImageFolder它会自动把子目录名当作类别标签不需要手写 CSV 映射。这是你做数据准备时的关键前提即便你打算自己补充爬取图片也尽量保持这种目录结构后面所有代码都会简单一大截。这里需要先定一个底线数据集的划分策略、类别数、每类的图片数量决定了你的模型容量和训练轮次。常见做法是全部数据按 8:1:1 切训练集、验证集、测试集并且要保证划分是按类别分层的而不是随机乱切。下面进入具体操作。3. 环境搭建与数据处理把图片变成模型能吃的样子写代码之前先把环境整理干净。这个步骤看似基础但我在帮人调试时见过太多“跑不起来”最终都栽在环境问题上。按下面的顺序来可以少花很多冤枉时间。3.1 创建独立的 Python 环境并安装依赖包不要直接在系统 Python 里装包这是最容易产生玄学问题的地方。用 conda 或 venv 建一个干净环境Python 版本推荐 3.9 或 3.10。然后安装必要依赖PyTorch 的安装命令要按你自己的 CUDA 版本去官网生成不要在命令里直接抄我写的默认版本号。下面以 CPU 为例给出最小依赖集GPU 环境把 torch 那行换成你对应的版本即可。conda create -n crop_disease python3.10 -y conda activate crop_disease pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu pip install opencv-python pillow numpy matplotlib scikit-learn tqdm参数说明--index-url指定了 PyTorch 官方 CPU 版 wheel 源这个参数不加的话默认源有时候会装到不兼容的版本组合比如 torch 和 torchvision 版本对不上这在 PyTorch 里是相当经典的翻车点。注意 torch 和 torchvision 必须配套两者版本不匹配时常见的报错是ModuleNotFoundError: No module named torchvision或者导入时就抛 C 扩展错误。建议装完先跑一步验证import torch, torchvision; print(torch.__version__, torchvision.__version__)。opencv 用来做图像尺寸适配和数据增强scikit-learn 用来生成混淆矩阵和分类报告tqdm 用来显示训练进度条——这些不是可选装饰后面对应环节都会用到。3.2 用 ImageFolder 加载数据集并做标签映射数据集准备好之后第一个代码块就是加载它。这里有个重要的心理建设ImageFolder是按文件夹名排序后生成类别索引的不是按你心里想的顺序。所以病名和索引的对应关系一定要在训练前打印出来确认一遍否则后面分析结果时容易张冠李戴。import torch from torchvision import datasets, transforms data_dir ./dataset # 数据集根目录内含 train / val / test 三个子目录 transform_train transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(degrees15), 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]), ]) transform_eval transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) train_set datasets.ImageFolder(rootf{data_dir}/train, transformtransform_train) val_set datasets.ImageFolder(rootf{data_dir}/val, transformtransform_eval) print(类别映射:, train_set.class_to_idx)逻辑说明这段代码定义了两套预处理流程训练集用带随机增强的版本验证集用纯标准化版本。数据增强的作用是让模型在有限的图片上“看到”更多变化——左右翻转、小幅旋转、调整亮度和饱和度这些都是让模型不那么容易过拟合的手段。但增强幅度不要过大旋转 15 度对于叶片这种本身有朝向性的物体已经够用了转多了反而会让模型学到错误的方向模式。参数说明Resize((224, 224))是所有预训练模型的默认输入尺寸。换用 EfficientNet 时建议把尺寸提到 240 或 260这个模型的缩放系数在更大分辨率下表现更好具体看你是哪个版本efficientnet-b0 用 240 比较稳妥。Normalize的 mean 和 std 用的是 ImageNet 预训练时的统计值微调时不要改成自己数据集的统计值因为预训练权重就是基于这套标准化学习的改了等于把模型的“输入习惯”破坏了。class_to_idx打印出来的字典你要截图存进论文附录答辩时可以直接展示这个映射来证明你的数据流程是完整的。3.3 写一个通用的训练循环先从 5 个 epoch 开始验证链路通不通一上来就全量训练十几个 epoch一旦发现代码有报错时间全都浪费在等待上。我一般的做法是先跑 5 个 epoch 只用一个批次的数据确认前向传播、反向传播、验证流程整条链路都通了再放开跑全量。这样可以把“代码逻辑错误”和“训练效果不好”两类问题隔离开排查效率高很多。import torch.nn as nn import torch.optim as optim from torchvision import models from torch.utils.data import DataLoader device torch.device(cuda if torch.cuda.is_available() else cpu) model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, len(train_set.classes)) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.SGD(model.parameters(), lr0.001, momentum0.9, weight_decay1e-4) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) train_loader DataLoader(train_set, batch_size32, shuffleTrue, num_workers2) val_loader DataLoader(val_set, batch_size32, shuffleFalse, num_workers2) for epoch in range(5): 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() * images.size(0) epoch_loss running_loss / len(train_set) print(fEpoch {epoch1} loss: {epoch_loss:.4f})参数说明与选型理由分类头改法model.fc nn.Linear(model.fc.in_features, num_classes)这是 ResNet18 的标准改法。如果你的预训练模型是 EfficientNet要改的是model.classifier[1]两者位置不一样这点写代码时容易搞混。优化器选 SGD 而不是 Adam是有意为之。Adam 前期收敛速度快但泛化性在部分任务上略逊于调好学习率的 SGD毕设里你要的是“验证集准确率高”的评价指标SGD 加余弦退火调度在图像分类微调上是更稳的组合。学习率 0.001 是微调预训练模型的常见起点如果你的数据集很小每类少于 200 张把 lr 降到 0.0005 更安全。weight_decay1e-4是给权重加 L2 正则防止模型在某些病害类别上过于自信。CosineAnnealingLR配合T_max30意味着学习率会在 30 个 epoch 内从初始值余弦下降到接近 0。如果你后续修改了 epoch 总数T_max要同步调整否则学习率曲线会在训练中途发生跳变。跑完这 5 个 epoch如果 loss 在明显下降哪怕只从 1.8 掉到 1.2说明链路是好的可以进入完整训练。4. 完整训练流程与评估准确率只是起点混淆矩阵才是重点链路通了之后就可以放开训练了。这一阶段有两个核心任务一是把训练完整跑完并保存最优模型二是做多维度评估因为导师答辩时大概率会问“你的系统到底哪些类容易搞混”你不能只说一个总体准确率。4.1 训练时把每轮验证集准确率记录下来并保存最优权重训练循环本身不复杂但一定要在验证集上实时评估并且保留“在验证集上表现最好的那一版模型”而不是最后一轮的模型。这在图像分类里是常规操作但很多初学者图省事只保存了最后一步结果后面想回退都找不到后悔药。import copy best_acc 0.0 best_model_wts copy.deepcopy(model.state_dict()) num_epochs 30 for epoch in range(num_epochs): model.train() for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() loss criterion(model(images), labels) loss.backward() optimizer.step() scheduler.step() model.eval() correct 0 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) total labels.size(0) correct (predicted labels).sum().item() val_acc correct / total if val_acc best_acc: best_acc val_acc best_model_wts copy.deepcopy(model.state_dict()) model.load_state_dict(best_model_wts) torch.save(model.state_dict(), best_model.pth) print(fBest validation acc: {best_acc:.4f})逻辑说明用copy.deepcopy保存权重是因为 PyTorch 的state_dict是可变对象直接赋值会在后续更新中被覆盖掉。验证阶段必须用torch.no_grad()包裹否则验证过程会记录梯度图吃满显存而且没有任何必要。模型切换model.eval()还会同步关闭 BatchNorm 的统计更新和 Dropout 的随机失活用训练模式去推理是预测阶段最常见的错误之一输出的结果会不稳定。参数说明30 个 epoch 是在公开数据集PlantVillage 这类规模上比较合理的一个配置。如果你的数据集更小比如每类只有 100 张左右10~15 个 epoch 就够了再往后基本就是在过拟合训练集。保存文件名里加上准确率或时间戳比如best_model_92.3.pth不要只叫best_model.pth。这个习惯能让你在多次训练后不会搞混模型尤其是当你调整了参数重新训练时文件名里的数字是唯一的判断依据。4.2 评估阶段用混淆矩阵定位“谁和谁长得像”准确率只告诉你整体好坏而混淆矩阵能告诉你哪些病在模型眼里长得像。这一点对做毕设特别重要因为你需要对结果给出“合理的解释”——比如模型把晚疫病和早疫病搞混是因为这两种病在叶片上的症状本来就相似颜色都是褐色斑块区别只在于斑块分布和边缘形态这个解释能体现你对任务本身的理解。import numpy as np import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, classification_report from sklearn.metrics import ConfusionMatrixDisplay model.eval() all_preds [] all_labels [] with torch.no_grad(): for images, labels in val_loader: images images.to(device) outputs model(images) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) print(classification_report(all_labels, all_preds, target_namestrain_set.classes, digits4)) disp ConfusionMatrixDisplay(confusion_matrixcm, display_labelstrain_set.classes) fig, ax plt.subplots(figsize(12, 10)) disp.plot(axax, xticks_rotation90) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi200)图表生成出来之后先别急着贴进论文。你有两件事要做第一把classification_report里的每个类别单独看找出 F1-score 最低的两三个类对应回原始图片从数据角度想原因——是样本太少、背景杂乱、还是症状本身相似。第二随机挑一些预测错误的图片做一个 misclassification 表格一张图三列原图 / 真实标签 / 预测标签。这个表格放到论文实验章节非常有说服力比任何文字描述都直观。4.3 类别不均衡时怎么处理重采样比换损失函数更省心病虫害数据集的类别分布通常很不均衡有些病常见、图片多有些病罕见、只有几十张。如果直接训练模型会对图片多的类别严重偏科。常见做法有两个一是对图片少的类别做过采样WeightedRandomSampler让每个 batch 里小类别样本出现的概率提高二是设置类别权重到损失函数里CrossEntropyLoss(weight...)让小类别的预测错误得到更大的惩罚。我一般倾向于先用过采样因为它不改损失函数的数学形式调参不会被干扰。具体操作是统计每类样本数、算出每个样本被抽中的概率与类别样本数成反比再传给 DataLoader。权重算一次就行写在数据准备环节里不要在训练循环里重复计算。5. 病虫害识别系统避坑与排查5 个高频踩坑点做这类识别系统90% 的时间会花在“明明感觉没问题但就是跑不对”的调试里。我把自己和找我帮忙的人踩得最多的坑整理成清单每一条都按现象、原因、解决的思路写清楚。5.1 训练时图像尺寸不一致导致崩溃现象第一个 epoch 跑到一半突然报错类似Expected input batch_size (32) to match target batch_size (16)或者干脆是 size mismatch。原因数据集里混进了一些不规则图片Resize之后还是怪异的尺寸或者某些图是 RGBA 四通道模型只接受 RGB 三通道。解决在ImageFolder加载前先写一个数据清洗脚本把所有图片统一转换为 RGB、统一尺寸并剔除损坏文件。from PIL import Image import os for root, _, files in os.walk(./dataset): for f in files: if not f.lower().endswith((.jpg, .jpeg, .png)): continue path os.path.join(root, f) try: img Image.open(path).convert(RGB) img.save(path) except Exception as e: print(f损坏文件: {path}, 已删除) os.remove(path)这步要放进“数据处理”而不是“模型训练”部分因为这类脏数据会在你训练的几乎任何阶段随机炸一下没有固定规律特别费时间。建议拿到任何数据集都先跑一遍这个清理解本。5.2 验证集效果很好但测试集掉分明显现象验证准确率 92%但你额外准备的一组测试图片比如自己从网上找的跑出来只有 60%。原因验证集和训练集来自同一个数据集的不同划分背景、拍摄设备、光照条件都太像了模型实际上没有学会“识别病害”而是学会了识别“这个数据集的拍摄风格”。解决想办法引入外部图片做测试或者至少做一次数据拆分时按拍摄批次分组保证同一个来源的图片不会同时出现在训练和验证里。这个坑在毕设里影响不算大但如果你在论文里写“系统鲁棒性验证”导师一眼就能看穿验证集和测试集同源的问题。5.3 训练 loss 不降反升或震荡剧烈现象loss 在 1.5 上下反复弹跳甚至越跑越高。原因最常见的是学习率偏大SGD 在 batch size 较小的时候对学习率非常敏感其次是 BatchNorm 在 batch size 为 1 或 2 时统计失真导致训练不稳定。解决先把学习率降到 0.0001如果还震荡就把 batch size 提到 32 以上。如果你的显卡一次装不下 32 张 224x224 的图用梯度累积——每 8 个小 batch 做一次参数更新等效 batch size 就是 8×实际 batch size。5.4 预测阶段打开单张图片总是报错现象模型训练没问题考试的时候写预测脚本却报RuntimeError: expected input to have 3 channels, but got 4 channels或尺寸错误。原因预测代码里没有做和训练时一致的预处理尤其是漏了ToTensor()或者忘了转 RGB。解决把预测脚本里的 transform 和训练时的 eval transform 写得一字不差最好是直接把同一个 transform 对象复用过去不要复制粘贴后手改。这条是最容易被忽视但也是最容易避免的坑。5.5 保存加载模型后精度骤降现象torch.load加载后跑出来的准确率和训练时差一大截。原因加载到的权重和当前模型结构不对齐常见于改了model.fc之后没有加载预训练的分类头参数或者保存时用的是state_dict、加载时却用torch.load(model_path)直接整体载入。解决保存和加载都要明确成对使用建议用下面的标准写法。# 保存 torch.save(model.state_dict(), best_model.pth) # 加载 model models.resnet18(weightsNone) model.fc nn.Linear(model.fc.in_features, num_classes) model.load_state_dict(torch.load(best_model.pth, map_locationcpu))这里有几个细节weightsNone避免加载预训练参数后再被覆盖map_locationcpu可以让一台没有 GPU 的机器也能加载训练好的参数加载之后模型结构和保存时必须完全一致包括num_classes否则报错或者静默错位。所谓静默错位是最难受的——代码不报错但参数张量对不上结果全是垃圾输出。6. 把成果做成能现场演示的系统从模型权重到可用界面的快速路径训练跑通只是完成了 60%剩下 40% 的价值在于“能不能把这个系统展示给别人用”。毕设答辩时的要求往往很具体现场跑通、界面能交互、你能说清楚背后的原理。常见的做法是做成一个本地 Web 应用让人在浏览器里上传一张叶子照片服务端调用模型推理返回病害类别和置信度。这里我用 Flask 做最小实现避免牵扯前端框架的复杂度。先写好推理函数把预处理和模型加载封装成独立模块。这个函数要保证只接受图片路径返回类别名和置信度列表。接口越简单后续接 Web、接 GUI、接命令行都方便。import torch from PIL import Image from torchvision import transforms import torch.nn as nn from torchvision import models device torch.device(cuda if torch.cuda.is_available() else cpu) class_names [apple_healthy, apple_scab, tomato_healthy, tomato_leaf_mold] # 实际请替换为 train_set.classes 生成的真实列表 transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) model models.resnet18(weightsNone) model.fc nn.Linear(model.fc.in_features, len(class_names)) model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) model.to(device) model.eval() def predict(image_path, topk3): img Image.open(image_path).convert(RGB) img transform(img).unsqueeze(0).to(device) with torch.no_grad(): outputs model(img) probs torch.softmax(outputs, dim1) top_probs, top_indices torch.topk(probs, topk) results [] for prob, idx in zip(top_probs[0], top_indices[0]): results.append((class_names[idx], round(prob.item(), 4))) return results这个函数里比较关键的是torch.softmax和torch.topk模型最终的输出是 logits不会自动带概率语义softmax 转成概率后topk就能取出前三个候选项。答辩演示时展示“模型给出了三个可能性主预测是 0.93 的番茄叶霉病次预测是 0.05 的早疫病”——这一句比单纯说“准确率 94%”更有说服力因为你实际上展示了模型在不确定情况下的行为而所有真实系统都会面临不确定性。Flask 的 Web 层用最小代码实现核心逻辑是接收上传文件调 predict 函数渲染结果。注意一个细节上传文件的原始文件名不能信任需要重命名成随机字符串再保存避免路径穿越和脏文件名干扰。界面不需要花哨一个文件选择器加一个按钮就够用样式可以在 CSS 里简单处理重点在功能闭环。一个更进阶的验证技巧找到一批“模型预测正确但置信度很低”的图片在论文里专门列一节分析这些边界样本。你要解释清楚为什么置信度低——是光照异常、叶子重叠、还是出现了数据集里没见过的症状表现。这节内容在答辩时价值极高因为它说明你不只是把模型跑通了而是真的在理解模型的行为边界。最后说一下我自己的习惯每次训练完都会把这类边界样本单独放一个文件夹连同模型权重一起保存。之后不管是换模型结构、调数据增强还是换损失函数都拿同一批样本回来对比看改动的效果到底是变好了还是变坏了。这比只看验证集准确率可靠得多也让你在跟导师讨论时有实实在在的依据。带数据、带权重、带失败案例的毕设才经得起追问。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进