简介:这份资源是面向计算机相关专业学生与深度学习入门者的农作物病虫害识别检测系统完整项目,基于卷积神经网络实现图像分类与检测,可作为高分毕业设计、课程设计或期末大作业的实战参考。压缩包共56个文件,约88.3MB,包含Python源码、Jupyter Notebook实验文件、数据集、运行说明文档及部署配置,其中ipynb覆盖ResNet50、VGG16、VGG19、DenseNet121、PyTorch、TensorFlow、Keras、Fastai等多种模型实现,另有Flask服务端脚本、Dockerfile与云部署指南,便于对比不同框架的建模流程。项目已获98人学习,代码经导师指导并评审通过,结构完整、注释清晰,小白也能按说明跑通。读者可从中掌握数据预处理、模型训练调参、评估指标分析到Web端部署的完整链路,并借鉴多模型对比与工程化目录组织思路,快速完成自己的识别类项目。
1. 农作物病虫害识别毕设:一份能跑通的 CNN 项目到底长什么样
带过几届毕设之后,我总结出一个规律:真正卡住学生的从来不是「CNN 是什么」,而是「拿到一份源码,怎么让它在我这台破笔记本上跑起来」。这份基于深度学习卷积神经网络的农作物病虫害识别检测系统,恰好就是冲着这个痛点来的——它不是一个只丢给你一个.ipynb就完事的半成品,而是把 Flask Web 界面、多框架训练脚本、Docker 部署、云平台部署指南全都打包进去了。
项目本身解决的是一个很具体的场景:给定一张叶片照片,判断它属于哪种病害(比如苹果黑星病、番茄早疫病、马铃薯晚疫病等),并给出置信度。技术栈是 Python + 卷积神经网络,训练侧提供了 ResNet50、DenseNet121、VGG16、VGG19 等多套骨干网络的 notebook,推理侧用 Flask 起了一个可交互的 Web 页面。适合谁?计算机相关专业正在做毕设的学生、需要交课程设计大作业的人,以及想拿一个完整 CNN 项目练手但不想从零搭架子的人。评审分 99 分这个信息说明它的完成度是被导师认可过的,代码结构、文档、部署说明都齐。
2. 先看清项目骨架:目录结构与技术选型逻辑
2.1 从文件树反推这套系统怎么分层
拿到一个压缩包,我习惯先不急着装环境,而是把目录结构过一遍。这份项目的顶层结构大致是这样的:
├── app/ # Flask 应用主体 │ ├── server.py # 后端入口,加载模型并处理请求 │ ├── view/ # 前端页面模板 │ ├── models/ # 模型权重存放位置 │ └── static/ # CSS/JS/图片等静态资源 ├── notebook/ # 各框架训练脚本 │ ├── Plant_Disease_RESNET50.ipynb │ ├── Plant_Detect_PyTorch.ipynb │ ├── Plant_Disease_Detection_TensorFlow.ipynb │ ├── Plant_Disease_Detection_Fastai.ipynb │ ├── Plant_Disease_Detection_Keras.ipynb │ ├── plant_disease_detector.ipynb │ ├── Plant_Disease_DenseNet121.ipynb │ ├── Plant_Disease_VGG19.ipynb │ └── Plant_Disease_VGG16.ipynb ├── deployment_guide/ # 部署文档 │ ├── local_flask/ │ ├── aws_deployment.md │ └── gcp_deployment.md ├── Dockerfile ├── requirements.txt ├── app.yaml └── README.md这个结构透露了几个关键信息。第一,训练和推理是分离的——notebook 负责训练出权重文件,app 负责加载权重做在线推理。第二,多框架并存不是炫技,而是给你选择权:如果你学校机房只装了 TensorFlow,就用 TensorFlow 那套;如果你自己电脑上 PyTorch 更熟,就切 PyTorch。第三,deployment_guide 里同时给了本地 Flask、AWS、GCP 三条路径,说明作者考虑过「答辩时老师让你现场演示」这个场景。
2.2 为什么选 ResNet50 作为主力骨干网络
项目里提供了 VGG16、VGG19、DenseNet121、ResNet50 四种骨干网络的训练脚本,但 README 和默认配置指向的是 ResNet50。这不是随便选的。
VGG 系列结构简单、参数量大,VGG16 大约 1.38 亿参数,VGG19 更多。在植物病害这种类别数不算特别多(通常几十类)的任务上,VGG 容易过拟合,而且训练慢。DenseNet121 参数量少、特征复用强,但密集连接的结构在显存受限时容易爆。ResNet50 大约 2500 万参数,残差连接解决了深层网络的梯度消失问题,在 ImageNet 上的预训练权重也最容易拿到,迁移学习效果好。
我一般会这样跟学生解释:如果你显卡只有 4GB 显存,优先用 ResNet50 做迁移学习,冻结前面的卷积层,只训练最后的全连接层,这样 batch size 能开到 16 甚至 32。如果你非要用 VGG,那就把输入尺寸从 224×224 降到 128×128,但精度会掉几个点。
2.3 数据集的组织方式与类别划分
项目的数据集按照标准图像分类格式组织,每个类别一个文件夹:
dataset/ ├── train/ │ ├── Apple___Apple_scab/ │ ├── Apple___Black_rot/ │ ├── Tomato___Early_blight/ │ └── ... ├── valid/ │ └── (同上类别结构) └── test/ └── (同上类别结构)这种类别名/图片的结构是ImageDataGenerator和torchvision.datasets.ImageFolder都能直接吃的格式。类别命名用了植物名___病害名的三下划线分隔,这是 PlantVillage 数据集的标准命名习惯。如果你要换成自己的数据,只要保持这个目录结构,改一下类别数就行。
提示:替换数据集时,务必保证 train/valid/test 三个子集的类别文件夹名称完全一致,否则标签映射会错位,训练出来的模型预测结果会全部偏移。
3. 环境搭建与训练脚本实操:从零跑通第一个 notebook
3.1 依赖安装与 Python 环境配置
项目根目录的requirements.txt是环境配置的起点。我建议不要直接pip install -r requirements.txt一把梭,而是先看清楚里面有什么。典型的依赖包括:
tensorflow>=2.4.0 torch>=1.8.0 torchvision>=0.9.0 fastai>=2.3.0 flask>=2.0.0 numpy pandas matplotlib Pillow gunicorn这里有个坑:TensorFlow 和 PyTorch 同时装在一个环境里,版本冲突的概率不低。我的做法是给训练和推理分别建虚拟环境。训练用哪个框架就建哪个环境,推理环境只装 Flask + 对应框架的推理依赖。
# 创建训练环境(以 PyTorch 为例) python -m venv venv_train source venv_train/bin/activate # Windows 用 venv_train\Scripts\activate # 安装 PyTorch(根据你的 CUDA 版本选择) pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装其他依赖 pip install numpy pandas matplotlib Pillow jupyter这段命令的逻辑是:先隔离环境,再装框架,最后装辅助库。--index-url指定 PyTorch 官方源是为了避免从 PyPI 拉到 CPU 版本。如果你没有 NVIDIA 显卡,把cu118换成cpu即可。参数说明:cu118对应 CUDA 11.8,这是目前比较稳的版本,太新的 CUDA 12.x 在某些旧驱动上会翻车。
3.2 用 ResNet50 notebook 完成一次完整训练
打开Plant_Disease_RESNET50.ipynb,核心训练逻辑大概是这样:
import torch import torch.nn as nn import torchvision.models as models from torchvision import transforms, datasets from torch.utils.data import DataLoader # 1. 数据增强与预处理 train_transform = transforms.Compose([ transforms.Resize((224, 224)), # ResNet50 标准输入尺寸 transforms.RandomHorizontalFlip(), # 随机水平翻转,增加泛化 transforms.RandomRotation(15), # 随机旋转 ±15 度 transforms.ToTensor(), transforms.Normalize( # ImageNet 预训练权重的均值方差 mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] ) ]) valid_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] ) ]) # 2. 加载数据集 train_dataset = datasets.ImageFolder('dataset/train', transform=train_transform) valid_dataset = datasets.ImageFolder('dataset/valid', transform=valid_transform) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4) valid_loader = DataLoader(valid_dataset, batch_size=32, shuffle=False, num_workers=4) # 3. 加载预训练 ResNet50 并替换分类头 model = models.resnet50(pretrained=True) num_classes = len(train_dataset.classes) # 根据实际类别数自动确定 model.fc = nn.Linear(model.fc.in_features, num_classes) # 4. 冻结前面的卷积层,只训练全连接层 for param in model.parameters(): param.requires_grad = False for param in model.fc.parameters(): param.requires_grad = True # 5. 定义损失函数和优化器 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.fc.parameters(), lr=1e-3) # 6. 训练循环 epochs = 10 for epoch in range(epochs): 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() # 验证阶段 model.eval() correct = 0 total = 0 with torch.no_grad(): for images, labels in valid_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() print(f'Epoch {epoch+1}/{epochs}, Loss: {running_loss/len(train_loader):.4f}, ' f'Val Acc: {100*correct/total:.2f}%') # 7. 保存模型权重 torch.save(model.state_dict(), 'app/models/resnet50_plant.pth')这段代码有几个关键决策点需要解释。第一,transforms.Normalize用的均值和方差是 ImageNet 的统计值,因为 ResNet50 的预训练权重是在 ImageNet 上训的,保持输入分布一致才能最大化迁移学习效果。第二,冻结卷积层只训全连接层,这是小数据集上的标准操作——植物病害数据集通常几万张图,从头训容易过拟合。第三,batch_size=32是 4GB 显存下的安全值,如果你显存更小,降到 16 或 8。第四,num_workers=4在 Windows 上有时会报错,改成 0 即可。
训练完成后,权重文件保存到app/models/目录下,Flask 应用启动时会从这里加载。
3.3 训练过程中的参数调优与监控
跑通第一个 epoch 只是开始,真正决定毕设分数的是精度。我一般会盯这几个指标:
| 参数 | 推荐值 | 调整逻辑 |
|---|---|---|
| 学习率 | 1e-3(冻结阶段)→ 1e-4(微调阶段) | 冻结时用大学习率快速收敛,解冻后用小学习率精细调整 |
| batch_size | 16~32 | 显存够就往上加,但别超过 64,小数据集上大 batch 反而掉点 |
| epochs | 10~20 | 看验证集准确率曲线,连续 3 个 epoch 不涨就可以停 |
| 数据增强 | 翻转+旋转+颜色抖动 | 颜色抖动对叶片病害识别特别有用,因为光照条件变化大 |
如果你发现训练集准确率 99% 但验证集只有 70%,那就是过拟合了。解决办法:加 Dropout、加 L2 正则、增加数据增强强度、或者干脆减少全连接层的神经元数量。
4. Flask 推理服务与部署:让模型真正能被访问
4.1 server.py 的推理流程拆解
app/server.py是整个系统的在线推理入口。它的核心逻辑是:接收上传的图片 → 预处理 → 送入模型 → 返回预测类别和置信度。简化后的关键代码如下:
from flask import Flask, request, jsonify, render_template import torch from torchvision import transforms from PIL import Image import io app = Flask(__name__) # 启动时加载模型(只加载一次) device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = torch.load('models/resnet50_plant.pth', map_location=device) model.eval() # 类别名称列表,顺序必须与训练时一致 class_names = ['Apple___Apple_scab', 'Apple___Black_rot', ...] preprocess = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) @app.route('/') def index(): return render_template('index.html') @app.route('/predict', methods=['POST']) def predict(): if 'file' not in request.files: return jsonify({'error': 'no file uploaded'}), 400 file = request.files['file'] img = Image.open(io.BytesIO(file.read())).convert('RGB') input_tensor = preprocess(img).unsqueeze(0).to(device) with torch.no_grad(): outputs = model(input_tensor) probabilities = torch.nn.functional.softmax(outputs, dim=1) confidence, predicted = torch.max(probabilities, 1) return jsonify({ 'class': class_names[predicted.item()], 'confidence': round(confidence.item() * 100, 2) }) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False)几个容易翻车的点:class_names的顺序必须和训练时ImageFolder自动排序的顺序完全一致,否则预测结果会张冠李戴。model.eval()不能省,否则 BatchNorm 层会用训练模式的统计量,推理结果不稳定。torch.no_grad()包裹推理过程,减少显存占用。
4.2 本地启动与接口测试
环境装好后,启动服务只需要一行:
cd app python server.py浏览器打开http://localhost:5000就能看到上传页面。如果你想用命令行测试接口:
curl -X POST -F "file=@test_leaf.jpg" http://localhost:5000/predict返回的 JSON 类似{"class": "Tomato___Early_blight", "confidence": 94.37}。如果返回 500 错误,先看控制台有没有报KeyError或RuntimeError,大概率是类别名对不上或者模型权重路径写错了。
4.3 Docker 与云平台部署的取舍
项目提供了 Dockerfile 和 AWS/GCP 部署文档。Docker 方式适合你需要在不同机器上迁移演示的场景:
FROM python:3.8-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 5000 CMD ["gunicorn", "-b", "0.0.0.0:5000", "server:app"]构建和运行:
docker build -t plant-disease . docker run -p 5000:5000 plant-disease用 gunicorn 替代 Flask 自带的开发服务器,是因为开发服务器不支持并发,答辩时如果多个老师同时访问会卡死。-b 0.0.0.0:5000让服务监听所有网卡,方便局域网内其他设备访问。
至于 AWS 和 GCP 的部署文档,我的建议是:毕设答辩用本地 Flask 或 Docker 就够了,云部署更适合你想把项目写进简历、展示工程能力的时候再折腾。云平台的坑在于安全组配置和模型文件上传,文档里虽然有步骤,但不同账号的界面有差异,照着做也可能卡住。
5. 避坑与常见问题排查
5.1 模型加载报错:KeyError 与 size mismatch
现象:启动 Flask 时抛出RuntimeError: Error(s) in loading state_dict for ResNet: Missing key(s) in state_dict或size mismatch for fc.weight。
原因:训练时保存的是model.state_dict(),但加载时用了torch.load()直接加载整个模型对象,或者类别数和训练时不一致导致全连接层维度对不上。
解决:统一用state_dict方式保存和加载。加载前先实例化模型结构,再model.load_state_dict(torch.load('xxx.pth'))。如果类别数变了,重新训练最后一层。
5.2 预测结果全部偏向同一类
现象:不管上传什么图片,模型都返回同一个类别,置信度还很高。
原因:最常见的是class_names列表顺序和训练时ImageFolder.classes的顺序不一致。ImageFolder按文件夹名称的字母序排列,如果你手动写的列表顺序不同,映射就错了。
解决:在训练脚本里打印train_dataset.classes,把输出直接复制到server.py的class_names里。或者把类别列表存成 JSON 文件,训练和推理都从同一个文件读。
5.3 CUDA out of memory
现象:训练到一半报RuntimeError: CUDA out of memory。
原因:batch_size 太大,或者没有及时释放中间变量。
解决:先把 batch_size 减半试试。如果还不行,在训练循环里加torch.cuda.empty_cache(),或者把num_workers调小。另外检查是不是在验证阶段忘了加torch.no_grad(),导致计算图一直累积。
5.4 Flask 上传大图后请求超时
现象:上传手机拍的高清叶片照片(几 MB),页面一直转圈最后 502。
原因:Flask 默认没有限制上传大小,但预处理时把大图直接 resize 到 224×224 之前,PIL 解码大图会消耗大量内存和时间。
解决:在server.py里加app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024限制 5MB,同时在预处理里先做一次缩略图裁剪,再送进 transform。
5.5 notebook 里的路径在本地跑不通
现象:notebook 里写的是/content/dataset/train,这是 Google Colab 的路径,本地跑就找不到文件。
原因:作者在 Colab 上调试的,路径没改成相对路径。
解决:把所有绝对路径改成相对于项目根目录的路径,比如dataset/train。或者在 notebook 开头加一段os.chdir()切换到项目根目录。
6. 把毕设变成能讲清楚的作品:验证方法与进阶技巧
跑通代码只是及格线,答辩时老师真正会问的是「你怎么知道这个模型是有效的」。我一般会让学生做三件事来验证。
第一,画混淆矩阵。用验证集跑一遍预测,用sklearn.metrics.confusion_matrix生成矩阵,看看哪些类别容易被混淆。比如番茄早疫病和番茄晚疫病在叶片上的视觉差异很小,模型分错很正常,但你要能解释为什么。
第二,做一组消融对比。用同一个数据集分别训 ResNet50、VGG16、DenseNet121,记录验证集准确率和训练时间。表格一摆,选 ResNet50 的理由就立住了。我见过太多毕设只跑了一个模型,老师问「为什么不用别的」就答不上来。
第三,测试真实场景图片。从网上找几张不在数据集里的叶片照片,或者自己用手机拍几张,看看模型能不能给出合理预测。这一步经常翻车——数据集里的图片背景干净、光照均匀,真实照片背景杂乱,模型可能直接懵掉。如果翻车了,这反而是个加分项:你可以说「当前模型的局限在于训练数据分布单一,后续可以通过增加真实场景数据做域适应」。
# 混淆矩阵与分类报告 from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt model.eval() all_preds = [] all_labels = [] with torch.no_grad(): for images, labels in valid_loader: images = images.to(device) outputs = model(images) _, predicted = torch.max(outputs, 1) all_preds.extend(predicted.cpu().numpy()) all_labels.extend(labels.numpy()) cm = confusion_matrix(all_labels, all_preds) plt.figure(figsize=(12, 10)) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=class_names, yticklabels=class_names) plt.xlabel('Predicted') plt.ylabel('True') plt.title('Confusion Matrix') plt.savefig('confusion_matrix.png', dpi=150, bbox_inches='tight') print(classification_report(all_labels, all_preds, target_names=class_names))这段代码输出的分类报告里,重点看f1-score而不是accuracy。如果某个类别的 f1 低于 0.7,说明模型在这个类别上既容易漏检又容易误检,答辩时主动提出来并给出改进思路,比被老师问住要好。
还有一个进阶技巧:把模型导出成 ONNX 格式,这样推理时不再依赖 PyTorch,部署体积能小很多,启动速度也快。对于毕设演示来说,这意味着你可以在没有装 PyTorch 的机器上跑推理,只要装onnxruntime就行。
# 导出 ONNX dummy_input = torch.randn(1, 3, 224, 224).to(device) torch.onnx.export(model, dummy_input, 'plant_disease_resnet50.onnx', input_names=['input'], output_names=['output'], dynamic_axes={'input': {0: 'batch_size'}, 'output': {0: 'batch_size'}})导出后用onnxruntime加载验证一遍,确保输出和 PyTorch 一致。这一步做完,你的毕设就不只是一个「能跑的 notebook」,而是一个有部署方案、有性能对比、有边界分析的完整作品。
从那以后我每次带学生做毕设,都要求他们在答辩前至少跑一遍混淆矩阵和真实场景测试,因为这两步能暴露 90% 的问题。希望帮到你。
本文还有配套的精品资源,点击获取