基于CNN的农作物病虫害识别系统:从ResNet50训练到Flask部署
发布时间:2026/10/2 14:51:42来源:尧图网络
简介面向计算机专业毕业设计与实战学习者这是一套基于深度学习卷积神经网络的农作物病虫害识别检测系统完整项目。项目经导师指导并获评审98分源码均通过本地编译与严格调试覆盖数据预处理、模型训练、评估与部署全流程可直接运行。压缩包共56个文件整体约88.27MB包含Python源码、9个Notebook模型实现ResNet50、VGG系列、DenseNet121以及PyTorch、TensorFlow、Keras、Fastai等主流框架、图片数据集、环境依赖说明与Docker部署配置等。多种框架实现便于横向对比数据集中包含PNG、JPG等格式样本可供训练测试目录结构清晰适合逐步研读。目前已有349人学习下载。读者可获得可运行的完整项目代码、多框架实现笔记、数据样例、部署指导既能作为毕业设计的高分参考也有助于深入理解CNN在农作物病虫害识别中的特征提取与模型优化思路具备较强的实用价值。1. 这套毕设源码包了什么CNN 病虫害识别不是玩具项目每年毕设季都有人拿着「深度学习卷积神经网络」的题目来问我第一句永远是「老师会不会一看就知道我是调包的」。我的回答是把调包调出可复现、可展示、可答辩的完整闭环本身就是本事。这套农作物病虫害识别检测系统就是典型的能拿高分的项目——它不是一个训练到 98% 准确率的 Notebook 就完事而是把训练、验证、Web 展示、部署串成了一条线。源码里同时给了 PyTorch、TensorFlow、Keras、Fastai 四套实现还有 ResNet50、DenseNet121、VGG16、VGG19 四个骨干网络的 Notebook外加 Flask 服务端和 Dockerfile适合正在做毕设的计算机专业学生也适合想完整走一遍 CNN 分类实战的初学者。下文我按自己拆包复现的顺序从模型选型讲到部署踩坑。2. 为什么拿 ResNet50 当主力从网络结构到框架选型的取舍2.1 卷积神经网络抓的是纹理不是语义作物病虫害识别本质上是一个图像分类问题。叶片上的病斑、霉层、枯黄区域在图像里表现为局部的纹理和颜色异常这和「判断图片里有没有一只猫」这种需要全局语义的任务不一样它更依赖局部特征的层次组合。卷积神经网络正好擅长这个浅层卷积核负责边缘和颜色块中层组合出斑点和纹理深层才逐渐对应到「这像是晚疫病还是早疫病」这种抽象特征。我在复现项目里的 ResNet50 Notebook 时第一步不是急着跑训练而是先看它对单张叶片图做了什么。把中间层的特征图打印出来你会发现前面几层激活的基本是叶脉走向和病斑边缘几乎不关心叶片在画面里的位置。这就是 CNN 处理病虫害的天然优势病斑可大可小、可左可右卷积的平移等变性让模型不至于因为病斑位置偏移就误判。传统的 SVM 加 HOG 特征也能做但遇到背景杂乱、光照不均的田间照片手工特征就顶不住了而 CNN 靠数据增强能把这些变化学进去。理解了这个前提你就能明白为什么这套项目里所有 Notebook 都用的标准套路ImageFolder 加载、Resize 到 224、ImageNet 预训练权重、迁移学习。这不是偷懒是病虫害识别这类中量级数据集的通行做法——自己从头训练一个深度网络数据量根本喂不饱。2.2 四个骨干网络的取舍ResNet50、DenseNet121、VGG16、VGG19项目里放了四套骨干网络的实现我全部跑过一遍给你交个底。骨干网络参数量级单卡训练速度显存占用在这个项目里的表现VGG16约 1.38 亿慢高收敛稳定但训练耗时最长VGG19约 1.44 亿更慢更高比 VGG16 提升极有限ResNet50约 2550 万快中准确率高收敛快推荐主力DenseNet121约 800 万快低参数少但训练初期 loss 下降略慢VGG 系列的结构极其规整就是卷积层堆叠加全连接理解起来最直观适合第一次跑通流程时用。但它的全连接层占了绝大部分参数对显存和训练时间都不友好。我在 1080Ti 上跑 VGG16一个 epoch 比 ResNet50 慢将近一倍而准确率并没有明显优势。ResNet50 的残差结构解决了网络加深时的退化问题跳跃连接让梯度能直接流回浅层所以 50 层还能稳定收敛。DenseNet121 更进一步每层都连接前序所有层的输出特征复用率高参数量小但实现复杂度和显存访问开销换来的是「省参数不省显存」。如果你只有一块 4GB 的卡DenseNet121 反而比 ResNet50 更容易爆显存因为它要保存大量中间特征图。我的判断是默认跑 ResNet50 的 PyTorch Notebook这是效率和效果最平衡的选择。VGG 留着做对照实验答辩时「我对比了 VGG16、ResNet50、DenseNet121 三组模型」这句话本身就是加分项。2.3 requirements.txt 里的依赖版本是第一个隐形坑拆包后第一件事永远是看 requirements.txt。这个项目里 torch 和 torchvision 的版本绑定关系是典型的坑torch 1.x 对应的 torchvision 编译版本不能乱升否则加载预训练权重时会直接报AttributeError或者 CUDA 算子不匹配。常见做法是用 pip 安装时锁定大版本pip install torch1.13.1 torchvision0.14.1 --index-url https://download.pytorch.org/whl/cu117提示CUDA 版本要和你的显卡驱动匹配。先跑nvidia-smi看最高支持的 CUDA 版本再选对应的 PyTorch 轮子能少折腾一晚上。Notebook 里如果用了models.resnet50(pretrainedTrue)旧版 torchvision 的写法是pretrainedTrue新版改成了weightsResNet50_Weights.IMAGENET1K_V1。这个差异在 Keras 和 TensorFlow 版本里同样存在报错信息会直接告诉你 API 变了不要慌按提示改就行。3. 把数据集跑起来九套 Notebook 复制哪一份、参数怎么调3.1 数据集目录ImageFolder 的隐式标签这套资源自带数据集目录按作物病害分类组织。PyTorch 的ImageFolder会直接把子目录名当作类别标签所以目录结构决定了训练能不能跑起来。标准结构是这样的dataset/ ├── train/ │ ├── Tomato___Late_blight/ │ ├── Tomato___healthy/ │ └── Potato___Early_blight/ └── valid/ ├── Tomato___Late_blight/ └── ...加载代码只有几行from torchvision import datasets, transforms train_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomRotation(15), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_data datasets.ImageFolder(dataset/train, transformtrain_tf) valid_data datasets.ImageFolder(dataset/valid, transformvalid_tf)ImageFolder按字母序给类别生成索引classes列表就是标签映射。这里有个细节目录名里不要带中文和空格。我在 Windows 上试过一次目录名带中文结果ImageFolder读取时编码直接乱了训练出来的模型无论如何都是随机猜测。把目录名改成规范的英文类名是最省事的做法。验证集一般只做 Resize、ToTensor、Normalize不做随机旋转和翻转因为验证集要模拟真实推理时的输入分布。3.2 数据增强让模型学病斑而不是背图像病虫害识别最容易翻车的点是模型把「背景」当成了「特征」。如果你的训练图全是白色背景的实验室照片模型可能学到的是「白底 健康」这种假规律。所以 Notebook 里的数据增强参数值得逐个看一遍。RandomRotation(15)是让图像在 ±15 度内随机旋转对应田间拍摄时叶片朝向不固定的情况。ColorJitter(brightness0.2, contrast0.2)模拟光照变化——田间照片的光照差异远比实验室大。这两项加进去之后训练集的有效分布会宽很多验证集准确率和真实场景表现会更接近。Normalize用的是 ImageNet 的均值和方差因为迁移学习加载的预训练权重是在 ImageNet 上练出来的输入分布必须和预训练时一致。有个新手常犯的错用了预训练权重却忘了 Normalize结果训练 loss 一直降不下去还以为是学习率的问题。Normalize 这一步不是玄学是硬性要求。3.3 迁移学习冻结主干只练分类头ResNet50 的 Notebook 里核心训练代码是这个套路import torchvision.models as models import torch.nn as nn model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1) for param in model.parameters(): param.requires_grad False num_classes len(train_data.classes) model.fc nn.Linear(model.fc.in_features, num_classes)requires_grad False把整个骨架冻住只留最后新换的全连接层可训练。这样做的理由很实在ImageNet 预训练权重已经学会了通用的纹理和边缘特征叶片病斑的纹理检测本质上是在复用这些底层能力没有必要也没数据去重新训练整个 50 层。只训练最后一层参数量从两千多万降到几十万训练速度极快也不容易过拟合。model.fc.in_features是 ResNet50 最后池化层输出的维度固定是 2048。如果你换 DenseNet121这个值变成 1024VGG16 是 4096。不要写死数字用in_features动态取是通用写法。训练超参我给一个经过验证的起点参数推荐值说明batch_size32 或 644GB 显存用 328GB 以上用 64优化器Adamlr1e-3只训练全连接层时 1e-3 足够冻结训练 epoch10~15观察 loss 是否进入平台期解冻后 lr1e-4~5e-5解冻主干后学习率必须调小完整训练 epoch25~40配合早停防止过拟合冻结阶段用 1e-3解冻后降到 1e-4这是最常用的两段式微调。直接全程用大学习率解冻训练预训练权重很快就毁了loss 会崩。3.4 训练日志怎么读loss 不掉就是有问题Notebook 训练时会打印每个 epoch 的train_loss、valid_loss和valid_acc。我复现时的判断标准放这里valid_loss持续下降、valid_acc稳步上升这是健康曲线。train_loss一直在降但valid_loss拐头上升说明过拟合开始应该早停并加大数据增强强度。train_loss和valid_loss都在 2.0 以上纹丝不动先查 Normalize 和标签是否有问题不要急着调学习率。有一次我跑到第 8 个 epoch 时valid_acc卡在 80% 不动train_loss还在降。最后发现是验证集里有一个类别的图片尺寸异常Resize 之后出现黑边模型学到的是黑边特征。把那张图删掉重跑准确率直接跳到 92%。数据里的一颗老鼠屎比模型结构问题更能毁掉训练。4. 从 Notebook 到 Web 系统Flask 推理链路与两次部署4.1 Flask 推理链路上传图片到返回类别项目里的server.py和app/目录组成了一个完整的 Flask 服务。核心逻辑是把训练好的模型权重加载进内存接收前端上传的图片预处理后推理返回类别名和置信度。骨架代码长这样from flask import Flask, request, jsonify from PIL import Image import torch import torchvision.transforms as transforms app Flask(__name__) model load_model() def preprocess(image_file): img Image.open(image_file).convert(RGB) 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]) ]) return tf(img) app.route(/predict, methods[POST]) def predict(): file request.files[image] img preprocess(file) with torch.no_grad(): out model(img.unsqueeze(0)) idx out.argmax(dim1).item() conf out.softmax(dim1).max().item() return jsonify({class: model.classes[idx], confidence: round(conf, 4)})推理和训练有两个关键区别。第一img.unsqueeze(0)是把单张图扩成 batch 维度因为模型输入要求[batch, 3, 224, 224]单张图是[3, 224, 224]差一个维度。第二torch.no_grad()在推理时关掉梯度计算不仅省显存速度也会快一截。加载模型后一定要调model.eval()否则 BatchNorm 层的行为还是训练模式同样的图两次推理结果都可能不一样。views文件里通常还会配一个首页路由渲染static目录下的前端页面。前端用 FormData 上传图片后端返回 JSON前端展示识别结果。这个链路不复杂但它是毕设答辩时演示的核心——面试官和老师看你现场传一张带病斑的叶片图系统 1 秒内给出「番茄晚疫病 94.2%」这个演示效果比 training loss 曲线直观得多。4.2 本地跑起来local_flask 的三步项目里local_flask目录和deployment_guide是配套的。本地复现按三步走第一步确认模型权重文件存在。训练好的权重一般输出为.pth文件要确认路径和server.py里load_model()写的一致。最常见的问题是 Notebook 训练完没保存权重或者保存到了checkpoints/但 Flask 在models/里找。第二步启动 Flask 开发服务器python server.py默认监听127.0.0.1:5000。本地测试用curl比打开浏览器更快curl -X POST -F imagetest_leaf.jpg http://127.0.0.1:5000/predict返回{class: Tomato___Late_blight, confidence: 0.942}就说明链路通了。如果返回 500先去 Flask 控制台看堆栈——多半是权重路径不对或者输入图片不是 RGB 三通道。第三步把host0.0.0.0加上让局域网内其他设备能访问。这一步在毕设演示时很实用用手机访问电脑的 IP直接拍照识别比在电脑上翻文件演示效果好一个档次。4.3 部署到云Dockerfile、app.yaml 和两份部署手册的差别项目里同时有Dockerfile、app.yaml、aws_deployment.md和gcp_deployment.md这在国内毕设项目里很少见。我去读了 aws 和 gcp 两份手册核心差异在于平台对应用的假设不同。Dockerfile 是通用的一步内容很常规FROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 8080 CMD [gunicorn, server:app, -b, 0.0.0.0:8080]用 gunicorn 而不是 Flask 自带的开发服务器是因为 Flask 内置服务器是单进程单线程压不住并发请求gunicorn 多 worker 才能支撑演示时的多人访问。server:app的意思是导入server.py里的app对象所以server.py里必须有app Flask(__name__)这一行。AWS 部署手册走的是 EC2 路线启动一台 Ubuntu 实例安装 Docker把镜像跑起来然后在安全组里放开 8080 端口。GCP 的app.yaml走的是 App Engine 标准环境配置了entrypoint和runtime它不需要自己管 DockerGoogle 那边自动编排。差异本质是AWS 给你一台机器自己折腾GCP 给你一个平台只管推代码。毕设场景我建议本地 Flask 演示就够了。部署到云端的意义在于「系统可访问性」——如果你的答辩要远程连线或者想给系统做一个公网访问的展示链接再考虑部署。否则为了部署多花三天时间调环境性价比不高。5. 避坑训练不收敛、显存溢出、部署 404 的高频问题5.1 训练 loss 不降反升准确率一直在 20% 左右徘徊现象训练 10 个 epochtrain_loss稳定在 2.5 上下不降验证准确率接近随机猜测。原因最常见的是两种。一是输入图片没有做Normalize预训练权重完全无法适配输入分布二是标签和图片不对应ImageFolder按字母序生成标签如果你的类别名改了但模型分类头维度没同步改训练就是在对空气输出。解决先检查 transforms 里有没有Normalize以及均值方差是否为 ImageNet 标准值。然后打印train_data.class_to_idx人工核对几对「图片路径-标签索引」的对应关系确认不是张冠李戴。我排查过不下十次训练异常九成是这两个原因。5.2 CUDA out of memory一跑训练就爆显存现象batch_size 设 64加载模型一进训练循环直接报CUDA out of memory。原因ResNet50 中间特征图占显存的大头VGG16 的全连接层更夸张。8GB 显存的卡跑 64 的 batch 本身就勉强。解决三个手段按顺序试。第一batch_size 降到 32 或 16第二把DataLoader的num_workers调大让 CPU 预取数据减少 GPU 等待时间第三换 DenseNet121它的显存占用曲线比 ResNet50 平缓得多。另外检查是不是同时开了多个 Notebook 在占显存跑nvidia-smi看一眼该杀就杀。5.3 Flask 页面能打开但上传图片后一直转圈不返回现象前端页面正常显示上传图片后请求发出去浏览器一直 loading后端控制台也没有报错。原因这个坑我在项目里踩过——server.py里的模型加载放在了 Flask 路由外面但模型权重是非常大的文件每次请求进来 Flask 开发服务器的 reloader 会重新加载一遍模型单线程处理请求时阻塞住了。解决把模型加载逻辑放到模块顶层只加载一次启动时加use_reloaderFalse。如果请求还是卡在predict函数里加日志打印定位是卡在前处理还是推理阶段。5.4 torch.load 在 Windows 下加载路径报错现象Notebook 里训练的模型换一台电脑加载时报ModuleNotFoundError或者pickle反序列化失败。原因PyTorch 的权重文件本质上是用 pickle 序列化的如果训练环境用的类定义在加载环境里不存在就会反序列化失败。另外 Windows 下路径分隔符和 Linux 不同硬编码的/checkpoints/model.pth风格路径在 Windows 上会失效。解决用torch.load(path, map_locationcpu)先加载到 CPU 再放到 GPU避免 CUDA 版本不一致的报错。代码里永远用os.path.join拼接路径不要手写分隔符。如果换了环境报类找不到检查是不是用了自定义的网络结构是的话要把模型定义代码放在同一个模块里。5.5 部署到云后静态页面 404接口却正常现象Docker 跑起来之后/predict接口用 curl 能通但浏览器访问首页返回 404。原因Flask 的static_folder和template_folder默认相对路径是相对app所在的包。如果server.py在项目根目录而static/在app/子目录下默认路径就找不到。解决显式指定静态目录app Flask(__name__, static_folderapp/static, template_folderapp/templates)Docker 里还得确认COPY . .把所有静态文件打进了镜像用docker exec -it 容器名 ls /app/app/static查一眼文件在不在。404 里的九成问题都是「文件没进镜像」和「路径不对」二选一。6. 进阶验证混淆矩阵查漏诊与 Docker 一键复现6.1 用混淆矩阵做漏诊检查准确率是个会骗人的指标。如果你的测试集里健康叶片占 60%模型全猜健康也有 60% 准确率但真正得病的叶片它一个都没认出来——这在农业场景是致命的漏诊。所以我复现任何分类项目都会在训练结束后立即跑一个全量验证集的混淆矩阵from sklearn.metrics import classification_report, confusion_matrix import seaborn as sns import matplotlib.pyplot as plt model.eval() y_true, y_pred [], [] for images, labels in valid_loader: with torch.no_grad(): outputs model(images) _, preds torch.max(outputs, 1) y_true.extend(labels.numpy()) y_pred.extend(preds.numpy()) print(classification_report(y_true, y_pred, target_namesmodel.classes)) cm confusion_matrix(y_true, y_pred) plt.figure(figsize(12, 10)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelsmodel.classes, yticklabelsmodel.classes) plt.show()怎么看这张图先找对角线之外数值大的格子那代表两个类别互相混淆。比如「番茄早疫病」和「番茄晚疫病」在视觉上确实相近——都是叶片上长斑只是斑的颜色和分布不同。如果这两个类互相误判严重解决手段是补数据或对这两类单独加更强的增强而不是盲目调模型结构。classification_report里的 F1-score 也要逐类看单看总体准确率会漏掉低召回率的类。6.2 换自己数据集时只改三个地方很多人的毕设不是作物病虫害而是皮肤病变、木材缺陷、表面瑕疵这类相近的图像分类题。从这套项目迁移过去只需要改三个地方num_classes改成自己的类别数transforms 的Resize尺寸按模型要求保持 224目录名替换成自己的类别英文名。其余代码完全不用动。这就是 ResNet50 通用特征提取器的好处——换领域不换骨架。6.3 Docker 一键复现的验证习惯最后说一个我养成的习惯训练结束后至少用 Docker 完整走一遍 build 和 run确认真实环境里能跑通。docker build -t plant-disease . docker run -d -p 8080:8080 plant-disease curl -X POST -F imagetest_leaf.jpg http://localhost:8080/predict这一步能验证三件事requirements.txt 有没有漏掉的依赖、模型权重有没有被 Docker 的.dockerignore排除掉、预训练权重加载在干净环境里是否依赖了某个本地路径。我从这套项目里学到的最实在一条任何模型改动、任何环境迁移之后先跑一遍全量验证集的混淆矩阵再跑一遍 Docker 复现确认「模型效果」和「环境一致性」两个维度都没问题才算真正完成。从那以后我每次训练完都强制走这两个步骤宁可多花半小时也不在答辩现场翻车。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网