☰
基于深度学习的农作物病虫害识别系统:Python源码与数据集实战
2026/10/1 19:04:28 网站建设 项目流程

简介:这份资源是面向计算机相关专业学生与深度学习入门者的农作物病虫害识别检测系统完整项目,基于卷积神经网络实现图像分类与检测,可作为毕业设计、课程设计或期末大作业的实战参考。包内共56个文件,涵盖9个ipynb实验笔记、2个py核心脚本、15个png与4个jpg效果图、10个md说明文档,以及yml、html、css、pkl模型文件、Dockerfile等部署配置,压缩包约88.3MB,目录按app、models、notebook、static等模块划分,结构清晰。项目包含ResNet50、DenseNet121、VGG16、VGG19等多种网络实现,并附TensorFlow、PyTorch、Keras、Fastai多框架版本,方便对比学习。已有98人学习下载,适合希望快速跑通完整流程、理解模型训练与Web端部署的读者参考借鉴。

1. 农作物病虫害识别系统:从数据集到可运行 Python 源码的完整落地路径

田里出现一片发黄的叶片,农户拍张照上传,系统在 1 秒内返回“玉米大斑病,置信度 0.93,建议喷施苯醚甲环唑”——这就是基于深度学习卷积神经网络的农作物病虫害识别检测系统要干的事。它本质是一个图像分类(或检测)任务:输入作物叶片照片,输出病虫害类别。整套东西通常包含三块:Python 源码、运行说明、标注好的数据集。适合谁?正在做高分毕设的本科生、想入门深度学习算法又不想只跑 MNIST 的开发者、以及需要快速搭一个农业 AI demo 的工程师。热搜里“深度学习”“卷积神经网络”“python”“源码”“数据集”这几个词,恰好对应了这套系统的五个核心环节:环境、模型、训练、推理、部署。下面按我实际搭过一遍的顺序,把每一步拆开讲清楚,包括参数怎么设、坑在哪。

2. 数据集怎么选、怎么洗:决定模型上限的不是网络结构

2.1 公开数据集与自建数据集的取舍

农作物病虫害领域最常被引用的公开数据集是 PlantVillage,包含约 5 万多张叶片图像,覆盖 38 个类别(作物-病害组合),背景多为单一颜色,标注质量高。但它的致命问题是:真实田间照片有复杂背景、光照变化、遮挡,直接拿 PlantVillage 训出来的模型下地就翻车。我的做法是:用 PlantVillage 做预训练或基线,再用自己拍的 200~500 张田间图做微调。如果毕设时间紧,至少要在运行说明里写清楚“本模型适用于单一背景叶片图”,别让答辩老师拿一张田间图把你问住。

自建数据集时,类别命名建议用“作物_病害”格式,比如corn_blight、rice_blast,避免中文路径。每类至少 200 张,少于这个数就得靠数据增强硬撑。图像统一缩放到 224×224 或 256×256,格式统一为 JPG,文件名不要带空格。

2.2 数据清洗与划分的实操脚本

拿到原始图片后,第一步不是直接训练,而是去重、去模糊、检查标签。下面这段脚本做三件事:删除损坏图片、按 8:1:1 划分训练/验证/测试集、生成类别索引文件。

import os import shutil import random from PIL import Image from tqdm import tqdm RAW_DIR = "raw_data" # 原始数据,每个子文件夹是一个类别 OUT_DIR = "dataset" # 输出目录 SPLIT = (0.8, 0.1, 0.1) # 训练/验证/测试比例 def is_valid_image(path): try: img = Image.open(path) img.verify() # 校验文件完整性 return True except Exception: return False classes = sorted(os.listdir(RAW_DIR)) for cls in classes: cls_dir = os.path.join(RAW_DIR, cls) if not os.path.isdir(cls_dir): continue imgs = [f for f in os.listdir(cls_dir) if f.lower().endswith((".jpg", ".jpeg", ".png"))] imgs = [f for f in imgs if is_valid_image(os.path.join(cls_dir, f))] random.shuffle(imgs) n = len(imgs) n_train = int(n * SPLIT[0]) n_val = int(n * SPLIT[1]) splits = { "train": imgs[:n_train], "val": imgs[n_train:n_train + n_val], "test": imgs[n_train + n_val:] } for split, files in splits.items(): dst = os.path.join(OUT_DIR, split, cls) os.makedirs(dst, exist_ok=True) for f in tqdm(files, desc=f"{cls}-{split}"): shutil.copy(os.path.join(cls_dir, f), os.path.join(dst, f)) print("划分完成")

逻辑说明:is_valid_image用 PIL 的verify()过滤掉下载不全或格式损坏的图,这一步能避免训练时突然报UnidentifiedImageError。random.shuffle前建议固定随机种子(random.seed(42)),保证每次划分一致,方便复现。参数上,SPLIT比例不是死的:如果某类样本少于 300,验证集可以压到 0.05,把更多数据留给训练。输出目录结构直接兼容torchvision.datasets.ImageFolder,后面写 Dataset 时不用再改。

注意:划分必须在“去重”之后做。我见过有人先划分再发现训练集和测试集有重复图,测试准确率虚高到 99%,答辩时被当场戳穿。

2.3 数据增强:别把叶片转成“外星生物”

增强策略要符合田间实际。随机水平翻转、±15° 旋转、亮度对比度微调是安全的;但垂直翻转对叶片图像通常不合理(自然界叶片不会倒挂),大角度旋转会引入黑边。颜色抖动(ColorJitter)幅度别超过 0.3,否则病斑颜色失真,模型学到的特征就偏了。

from torchvision import transforms train_tf = transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), # 模拟不同拍摄距离 transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(15), 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]) # ImageNet 统计量 ]) val_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])

RandomResizedCrop的scale=(0.7,1.0)是关键:它让模型见过“叶片只占画面一部分”的情况,提升对真实拍摄的鲁棒性。归一化用的均值和方差是 ImageNet 的,只要你是用预训练权重微调,就必须保持一致,否则预训练学到的分布对不上,收敛会变慢甚至不收敛。

3. 卷积神经网络选型与训练:ResNet18 够用,别一上来就上 ViT

3.1 为什么农作物病虫害识别首选 ResNet 系列

卷积神经网络处理图像的核心是局部感受野和权值共享,这对叶片病斑这种局部纹理特征非常对路。选型上,我的建议很直接:毕设或中小规模数据集(几千到几万张),ResNet18 或 ResNet50 微调就是性价比最高的方案。ResNet 的残差连接解决了深层网络退化问题,18 层在 224×224 输入下参数量约 1100 万,单张 1080Ti 或 3060 就能训。ViT 这类 Transformer 结构需要更大数据量才能发挥优势,5 万张以下的数据集上,ResNet 通常更稳。

如果标题里强调“检测”而非“分类”,那要用 YOLO 系列(如 YOLOv8)做目标检测,输出病斑位置框。但多数毕设的“识别检测系统”实际做的是分类,运行说明里要写清楚,别让评审误解。

3.2 用迁移学习微调 ResNet18 的完整训练脚本

下面这段代码是可直接跑的最小训练闭环:加载预训练权重、替换最后一层、冻结部分层、训练、保存最优模型。

import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, models, transforms from torchvision.models import ResNet18_Weights DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") BATCH_SIZE = 32 EPOCHS = 30 LR = 1e-3 NUM_CLASSES = 10 # 改成你的类别数 train_ds = datasets.ImageFolder("dataset/train", transform=train_tf) val_ds = datasets.ImageFolder("dataset/val", transform=val_tf) train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=4) model = models.resnet18(weights=ResNet18_Weights.IMAGENET1K_V1) for param in model.parameters(): param.requires_grad = False # 先冻结全部 model.fc = nn.Linear(model.fc.in_features, NUM_CLASSES) # 替换分类头 criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.fc.parameters(), lr=LR) # 只训分类头 scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1) model = model.to(DEVICE) best_acc = 0.0 for epoch in range(EPOCHS): model.train() for imgs, labels in train_loader: imgs, labels = imgs.to(DEVICE), labels.to(DEVICE) optimizer.zero_grad() loss = criterion(model(imgs), labels) loss.backward() optimizer.step() model.eval() correct, total = 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels = imgs.to(DEVICE), labels.to(DEVICE) preds = model(imgs).argmax(dim=1) correct += (preds == labels).sum().item() total += labels.size(0) acc = correct / total scheduler.step() if acc > best_acc: best_acc = acc torch.save(model.state_dict(), "best_resnet18.pth") print(f"Epoch {epoch+1}/{EPOCHS} val_acc={acc:.4f} best={best_acc:.4f}")

逻辑说明:先冻结主干只训分类头,是为了让随机初始化的fc层不把预训练特征带偏;训 10 个 epoch 后如果验证准确率停滞,可以解冻layer4一起微调,学习率降到 1e-4。StepLR每 10 轮降一次学习率,防止后期震荡。保存的是state_dict而非整个模型,加载时更灵活。

参数说明:BATCH_SIZE=32在 8GB 显存下安全,显存不够就降到 16 并同步调小学习率到 5e-4。num_workers=4在 Windows 上如果报错就改成 0。NUM_CLASSES必须和你的类别数严格一致,改错会直接维度不匹配报错。

3.3 训练过程要看哪些曲线、怎么判断过拟合

训练时至少盯三条线:训练 loss、验证 loss、验证准确率。正常情况训练 loss 平稳下降,验证 loss 先降后平。如果训练 loss 一直降、验证 loss 开始上升,就是过拟合,处理顺序是:先加数据增强,再加 Dropout(nn.Dropout(0.5)放在 fc 前),最后才考虑减模型容量。如果两条 loss 都不降,先查学习率是不是太大(loss 震荡)或太小(几乎不动),再查数据标签有没有错。我一般会在训练脚本里每轮打印学习率,确认 scheduler 生效。

4. 推理与系统封装:把模型变成能点的界面

4.1 单张图片推理与置信度输出

训练完只是拿到权重,用户要的是“上传图片→出结果”。下面是最小推理函数,返回类别名和置信度。

import torch import torch.nn.functional as F from PIL import Image from torchvision import models, transforms CLASS_NAMES = ["corn_blight", "rice_blast", "healthy", "..."] # 与训练顺序一致 infer_tf = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) def predict(image_path, model_path="best_resnet18.pth"): model = models.resnet18(weights=None) model.fc = torch.nn.Linear(model.fc.in_features, len(CLASS_NAMES)) model.load_state_dict(torch.load(model_path, map_location="cpu")) model.eval() img = Image.open(image_path).convert("RGB") tensor = infer_tf(img).unsqueeze(0) with torch.no_grad(): probs = F.softmax(model(tensor), dim=1)[0] idx = probs.argmax().item() return CLASS_NAMES[idx], probs[idx].item()

关键点:CLASS_NAMES的顺序必须和ImageFolder按文件夹名排序后的顺序一致,否则结果全错。map_location="cpu"保证在没有 GPU 的机器上也能加载。softmax后的置信度低于 0.6 时,建议在界面上提示“不确定,请重新拍摄”,而不是硬给一个类别。

4.2 用 Gradio 快速搭一个可演示界面

毕设演示不需要写前端,Gradio 三行代码出网页界面,支持拖拽上传。

import gradio as gr def classify(img): label, conf = predict(img) return f"{label} ({conf:.2%})" demo = gr.Interface( fn=classify, inputs=gr.Image(type="filepath"), outputs="text", title="农作物病虫害识别系统" ) demo.launch(server_name="0.0.0.0", server_port=7860)

server_name="0.0.0.0"让局域网内其他设备也能访问,答辩时用手机拍一张就能现场演示。如果运行说明里要求“离线可用”,Gradio 默认会尝试联网加载前端资源,需要提前把静态文件本地化,或者改用 PyQt5 写桌面端。

4.3 运行说明该写什么才算合格

一份能让人复现的运行说明,至少包含:Python 版本(建议 3.8~3.10)、依赖安装命令(pip install -r requirements.txt)、数据集放置路径、训练命令、推理命令、常见报错。requirements.txt 里要锁版本,比如torch==2.0.1、torchvision==0.15.2,不锁版本是复现失败的头号原因。我见过有人写“安装 PyTorch 即可”,结果对方装了最新版,API 变了直接跑不起来。

5. 避坑与排查:这五个坑我替你踩过了

5.1 现象:训练准确率 99%,测试准确率 40%

原因:训练集和测试集有重复图片,或者同一张图的不同增强版本被分到了两个集合。解决:划分前用图片哈希(如 MD5)去重,确保同一原图只出现在一个集合。

5.2 现象:loss 变成 NaN

原因:学习率过大,或者输入数据没有归一化,像素值 0~255 直接进网络。解决:确认ToTensor()后接Normalize,学习率从 1e-3 起调,出现 NaN 就降到 1e-4。

5.3 现象:验证集准确率远高于训练集

原因:验证集太小(比如只有 20 张),或者验证集类别分布和训练集差异大。解决:验证集至少每类 20 张,且用分层抽样保证类别比例一致。

5.4 现象:推理时类别名对不上

原因:CLASS_NAMES手写顺序和ImageFolder的class_to_idx不一致。解决:训练后打印train_ds.classes并保存成 json,推理时读取同一个文件。

5.5 现象:换台机器就报CUDA out of memory

原因:batch size 按大显存设的,小显存机器扛不住。解决:在代码里加自动降级逻辑,捕获 OOM 异常后把 batch size 减半重试,或者直接在运行说明里写明最低显存要求。

6. 把准确率再抬 3 个点的三个技巧

第一个技巧是测试时增强(TTA)。推理时对同一张图做原图、水平翻转、轻微缩放三个版本,分别预测后取平均概率。这个操作不重训模型,通常能涨 1~2 个点,代价是推理时间变三倍。代码上就是把infer_tf跑三次,probs累加后除以 3。

第二个技巧是类别不平衡处理。如果健康叶片样本远多于病害样本,用加权交叉熵:weight = 1 / 每类样本数,归一化后传给nn.CrossEntropyLoss(weight=weight)。这比简单过采样更稳,不会让模型反复看同一张图。

第三个技巧是混淆矩阵驱动优化。训练完在测试集上画混淆矩阵,找出最容易被混淆的两个类别(比如两种叶斑病),针对这两类单独补数据或调整增强策略。我一般会把这个矩阵直接放进毕设论文,比只报一个总体准确率有说服力得多。

最后说个习惯:每次改完超参数,把配置写进一个config.yaml,连同权重文件一起存档。我吃过亏,隔了两周想复现最好的那次结果,忘了当时学习率是 5e-4 还是 1e-3,只能重训。模型训练没有后悔药,配置留档就是给自己留退路。希望帮到你。

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

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

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

立即咨询