简介:农作物病虫害识别系统是一份基于机器学习(Python)的完整毕业设计项目,适合计算机、人工智能等相关专业学生用于课程设计、论文实验或期末项目,也适合作为图像分类入门的工程范例。资源共477个文件,压缩包约82MB,主要包含9个Python源码、150个GIF演示图、147个HTML页面、99个JavaScript与17个CSS样式文件,以及模型权重(pth)、SQLite数据库、TXT说明文档等。前端基于LayUI搭建操作界面,后端实现模型推理与数据存储,代码结构清晰,便于直接运行、调试和二次开发。所有源码经本地编译验证可正常运行,项目评审分达95分以上,内容经过助教审定,难度适中,无需过多调参即可复现。数据与模型权重均已打包,配合说明文档可快速理解从数据处理、模型训练到系统部署的完整链路。目前已有288人学习下载,适合希望快速掌握图像分类、特征提取、模型部署全流程,并需要一份可运行参考实现的初学者。
1. 农产品叶片拍一张照片,怎么判断得的是哪种病
在大田里巡检是植保最费人的环节。过去的做法是请植保专家到现场看叶片、查病斑,或者翻开图鉴逐条比对。无论是稻瘟病、玉米大斑病还是番茄晚疫病,症状相近、发病阶段不同,往往同一个病斑在不同光照下会误判。这个项目做的事情,就是把这些经验换成机器学习分类器:输入一张叶片图像,输出它是健康还是染病,以及具体是哪种病害。从标题形态看,数据、源码、说明文档齐全,属于典型的“完整交付”的工程包,而不是一个只有训练脚本的算法片段。
这类项目在智能农业、植保站自动预警、手机端农技工具等场景里是很有代表性的落地课题。对从业者来说,本周要解决的问题很具体:先弄懂项目里的数据长什么样,再在 Python 环境下把训练和推理跑通,最后理解模型评估结果为什么不能只看准确率。下面按一套完整的机器学习应用流程,从数据、模型、训练到部署逐个拆解,每一步都会给出可直接抄用的命令和代码。
2. 数据准备与预处理:先弄清目录结构、标签和增强方式
2.1 解压后先看数据目录,不要急着跑训练脚本
拿到*.zip之后,第一件事不是去读 README,而是把目录树列出来,搞清楚图片怎么组织、标签从哪里来。常见做法是train/和val/按类别分文件夹,文件夹名就是病害名,例如Tomato___Late_blight、Tomato___healthy。另一类是把所有图片放一个目录,旁边给一份labels.csv,里面写着filename,label两列。两种方式对后面的DataLoader写法影响很大。
unzip crop_disease.zip -d crop_disease find crop_disease -maxdepth 2 -type d | head -20解压后用find只列出两级目录,是为了快速确认数据集采用的是哪种组织方式。如果看到train/class_name/xx.jpg结构,就可以直接用torchvision.datasets.ImageFolder加载;如果看到大量散落的图片加一个 CSV,就需要自定义Dataset。很多源码包里两种格式都存在,训练脚本里一般会同时兼容,但你在跑通之前必须看清当前用的是哪一套。
2.2 图像读取、尺寸统一与归一化的标准写法
不管原始图片是手机直出的 3000x4000,还是植保无人机拍的 TIFF,进入模型之前都要统一尺寸和通道。绝大多数病虫害识别项目使用 ResNet 系列为骨干网络,默认输入是224x224的 RGB 三通道图像。这一步要做三件事:缩放、中心裁剪或随机裁剪、归一化到[0,1]区间并减去 ImageNet 均值。
import torch from torchvision import transforms train_transforms = transforms.Compose([ transforms.RandomResizedCrop(224, scale=(0.7, 1.0)), transforms.RandomHorizontalFlip(), 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]) ]) val_transforms = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(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)控制裁剪面积占原图的比例,值越小模型见过的局部细节越多,适合作物叶片这种目标占画面较大的场景;RandomRotation(15)表示最多旋转正负 15 度,超过这个范围叶片边缘容易出现黑色填充区域,引入无意义的边界噪声。验证集不做随机增强,只做等比缩放和中心裁剪,确保评估结果稳定可对比。均值方差用 ImageNet 的统计值,是因为后面加载预训练权重时,要求输入分布和预训练阶段一致,换别的数值会直接拉低迁移学习的效果。
2.3 数据增强的适用边界与参数建议
数据增强不是越多越好。病虫害识别的难点在于病斑位置、大小、颜色深浅变化很大,所以增强的优先级是:颜色抖动 > 旋转 > 裁剪 > 平移。颜色抖动很关键,因为田间不同光照条件下,同一病害的叶片颜色差异很大;而平移类增强对叶片这种中心型目标帮助不大,翻过头反而会把病斑移出画面。
| 增强方式 | 典型参数 | 适用场景 | 不适用场景 |
|---|---|---|---|
| RandomResizedCrop | scale=(0.6, 1.0) | 病斑只占叶片局部时 | 病斑布满全叶、需要整体特征时 |
| ColorJitter | brightness=0.2, contrast=0.2 | 田间光照复杂、早晚色温不同 | 数据集本身颜色已经很统一 |
| RandomRotation | 10~20 度 | 叶片朝向不固定 | 病斑具有明确方向敏感性的任务 |
| RandomErasing | p=0.3 | 模拟叶片遮挡 | 小目标识别,容易把病斑抹掉 |
提示:源码包里如果提供了
augment.py或datasets.py,先看里面的增强参数,如果 padded 之后出现大面积黑边,说明旋转角度设得太大了。
3. 模型搭建:从零训练 CNN 不如迁移学习
3.1 病虫害识别为什么首选迁移学习
农作物病害识别在机器学习任务里属于细粒度图像分类:不同病害之间病斑颜色接近,区别可能在纹理和分布形态上,比如炭疽病的黑色小点与疮痂病的凸起病斑肉眼很难分辨。从零训练一个深度 CNN,像是让模型从像素开始同时学习颜色、纹理、形状和分类边界,在小数据集上几乎必然会过拟合。常见的做法是使用在 ImageNet 上预训练的分类骨干网络,冻结浅层特征,只微调靠近输出的高层特征。浅层学到的是边缘、纹理、色块基底,这些在自然图像和叶片图像上是通用的;需要重新学的是“这些纹理组合起来意味着什么病”。
3.2 用 PyTorch 加载预训练 ResNet 并替换分类头
import torch.nn as nn from torchvision import models def build_model(num_classes, model_name='resnet18', freeze_backbone=False): if model_name == 'resnet18': model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) in_features = model.fc.in_features model.fc = nn.Sequential( nn.Dropout(0.3), nn.Linear(in_features, 256), nn.ReLU(inplace=True), nn.Linear(256, num_classes) ) elif model_name == 'resnet50': model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2) in_features = model.fc.in_features model.fc = nn.Linear(in_features, num_classes) if freeze_backbone: for name, param in model.named_parameters(): if not name.startswith('fc.'): param.requires_grad = False return modelin_features是从原模型的fc层读出来的,ResNet18 是 512,ResNet50 是 2048,直接写死虽然能跑,但换骨干网络时容易出错。替换后的fc保留了两层全连接加 Dropout,比单层线性层多了一点非线性拟合能力,适合病斑特征和类别之间不是严格线性关系的场景。
freeze_backbone参数控制是否冻结骨干网络。数据集小于一万张时建议冻结,只训练分类头,训练速度快且不容易过拟合;数据集足够大时解冻全部参数,用较小的学习率整体微调,效果更好。如果想继续加深,可以直接把model_name换成'resnet34'或'efficientnet_b0',torchvision.models下的写法基本一致。
3.3 源码阅读技巧:快速定位模型和入口
拿到源码包后,不要从头到尾逐行读 Python 文件。先ls看一眼文件列表,如果存在train.py、predict.py、model.py、config.py这类命名,整个套路就清楚了。训练入口在train.py,模型定义在model.py,超参数通常在config.py顶部集中以字典或简单变量形式声明。先打开config.py看看数据集路径、批量大小、学习率和 epoch 数,再回去看train.py的循环,比盲目按顺序读要高效得多。
grep -n "torchvision\|resnet\|vgg\|efficientnet" model.py | head -20上面这行命令用来确认项目用的模型来源。如果是torchvision.models下的标准模型,按官方 API 调用即可;如果看到model.py里自己定义了class DenseNet(nn.Module),说明是手写复现,要重点检查卷积层的 stride、padding 和最后的全连接输出维度与类别数是否匹配。很多源码包跑不通,不是环境问题,而是最后一个分类层的输出节点数和训练数据的类别数不一致,报错信息会显示size mismatch。
4. 训练循环、评估指标与调参实战
4.1 损失函数与优化器配置的取舍
病虫害分类是标准的多分类任务,损失函数用交叉熵,这个基本没有争议。真正影响收敛的是优化器和学习率策略。常见做法是先用 Adam 快速跑 5 到 10 个 epoch 观察损失下降趋势,再切到带动量的 SGD 做精细调优。亚当更适合初始阶段是因为它对学习率不敏感,对病虫害这种类别间特征相似的任务不容易发散;但 Adam 在后期容易出现泛化性不如 SGD 的情况,所以最终模型往往用 SGD 微调几轮收尾。
| 优化器 | 推荐初始学习率 | 关键参数 | 适用阶段 |
|---|---|---|---|
| Adam | 1e-3 ~ 3e-3 | betas=(0.9, 0.99) | 前期快速收敛、调参初期 |
| SGD | 1e-2 ~ 3e-2 | momentum=0.9, weight_decay=5e-4 | 后期精调、追求泛化 |
| AdamW | 1e-4 ~ 5e-4 | weight_decay=5e-4~1e-2 | 带 L2 正则的 Adam 变体 |
学习率直接照搬论文里的数值很容易崩。Adam 在ResNet18 + 病虫害叶片这个任务上,0.001 是一个安全的起步值;如果 loss 前几个 step 就变成 NaN,把学习率降到 1e-4 重新试。SGD 需要更大的初始学习率,配合CosineAnnealingLR或StepLR按 epoch 衰减,效果才会稳定。
4.2 最小可运行的训练循环与 checkpoint 保存
import torch from torch.utils.data import DataLoader from torchvision import datasets train_dataset = datasets.ImageFolder('data/train', transform=train_transforms) train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4, pin_memory=True) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=7, gamma=0.1) model.train() for epoch in range(20): running_loss = 0.0 for images, labels in train_loader: images, labels = images.to('cuda'), labels.to('cuda') outputs = model(images) loss = criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() running_loss += loss.item() * images.size(0) scheduler.step() avg_loss = running_loss / len(train_dataset) print(f'Epoch [{epoch+1}/20] Loss: {avg_loss:.4f}') torch.save({'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'loss': avg_loss}, f'checkpoints/epoch_{epoch+1}.pth')batch_size=32在 224x224 输入下,ResNet18 显存占用在 3 GB 左右,4 GB 显存的旧卡也能跑。scheduler.step()放在每一轮 epoch 末尾,配合StepLR(step_size=7, gamma=0.1)表示每 7 轮学习率乘 0.1,前 7 轮以 1e-3 快速搜索,后段用 1e-4 精细收敛。梯度清零放在loss.backward()之前是 PyTorch 固定顺序,写反了会导致梯度累加,优化器每轮用上一轮遗留的梯度更新,loss 跳动但数值不下降。
保存 checkpoint 时不要只存state_dict(),把 epoch、优化器状态和 loss 一起存进字典。这样中途停了可以从第 N 轮恢复训练,不需要从头跑起;后续做模型对比时,也能看出哪个 epoch 的 checkpoint 泛化最好。
4.3 评估指标:准确率会骗人,重点看混淆矩阵
当某类样本特别多时,“全预测成健康叶片”也能拿到 90% 的准确率,这在病虫害场景里很常见,因为田间健康叶片本来就比病叶多。所以评估阶段不能只看测试集准确率,要按类别把召回率列出来。
from sklearn.metrics import classification_report, confusion_matrix def evaluate(model, val_loader, class_names): model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for images, labels in val_loader: images = images.to('cuda') outputs = model(images) _, preds = torch.max(outputs, 1) all_preds.extend(preds.cpu().tolist()) all_labels.extend(labels.tolist()) print(classification_report(all_labels, all_preds, target_names=class_names, digits=3)) return confusion_matrix(all_labels, all_preds)classification_report返回每个类别的精确率、召回率和 F1。在病虫害识别里,最怕的是把染病叶子判成健康,因为这会直接延误防治窗口期。所以当拿到输出结果时,先看“健康叶片”这一行的召回率,如果低于 95%,说明很多病叶被漏过了;再看每两种病害之间是否互相混淆,比如把“早疫病”错判成“晚疫病”,对应混淆矩阵里某个非对角元素偏高,这种错误的影响比误判成健康小,但同样需要关注。
4.4 训练中最常见的 3 个坑
第一个坑是过拟合。典型表现是训练集 loss 降得很低、验证集准确率停滞甚至下滑。解决思路按优先级排序:降低模型容量(ResNet50 换回 ResNet18)、把 Dropout 加到 0.5、增强数据里的 RandomErasing。病虫害叶片背景复杂,模型很容易记住背景里的土壤纹理而不是叶片上的病斑。如果验证集 loss 在某一轮之后开始反弹,就是过拟合的明确信号,应当立刻停止训练,回退到验证集指标最好的那个 checkpoint。
第二个坑是学习率过小导致 loss 几乎不动。把训练日志拉出来,如果前 3 个 epoch loss 降幅小于 15%,直接对学习率乘 10 再试。病虫害识别里特征差异精细,学习率太小会让模型收敛到很低效的局部点。
第三个坑是显存不足。报错CUDA out of memory时,不要急着换卡,先检查是否有人在跑别的任务占着显存,然后按顺序调整:batch_size 从 32 降到 16 或 8,图片输入尺寸保持 224 不变,关闭pin_memory。如果num_workers=4导致内存被吃满,改成 2。多数情况下降 batch size 就能解决,且对最终准确率影响很小。
5. 本地复现:Python 环境配置、推理脚本与可视化验证
5.1 环境搭建:一台新机器怎么把这套源码跑起来
拿到源码包后最常见的问题不是模型写错,而是 Python 环境版本不对。先确认本机 Python 版本,推荐 3.9 或 3.10,太老的版本装不上新版 PyTorch,太新的版本有时遇到 CUDA 组件不兼容。
python -m venv crop_env source crop_env/bin/activate pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install numpy matplotlib scikit-learn pillow tqdm两个主要安装步骤拆开执行是有意的:第一行先装 PyTorch 核心库,第二行再装外围科学计算库。如果一次性把 requirements.txt 里的全部内容直接装,一旦某个依赖包版本冲突,排查起来非常困难。源码包里的说明文档如果写了环境依赖,以里面的requirements.txt为准;没写的话,上面这套环境组合可以覆盖大多数基于 PyTorch 的病虫害识别项目。
5.2 写一个按图片路径批量推理的脚本
import torch from PIL import Image from torchvision import transforms def predict_image(model, image_path, class_names, device='cuda'): model.eval() image = Image.open(image_path).convert('RGB') tensor = val_transforms(image).unsqueeze(0).to(device) with torch.no_grad(): outputs = model(tensor) probs = torch.softmax(outputs, dim=1) top_prob, top_idx = torch.max(probs, 1) label = class_names[top_idx.item()] confidence = top_prob.item() return label, confidence这个函数里面有两个容易忽略的细节。一是Image.open()之后必须接.convert('RGB'),否则遇到少数的灰度图或带 alpha 通道的 PNG 图,模型因为输入只有 1 通道直接报错。二是torch.max(probs, 1)返回的是分值和索引,索引需要.item()取出 Python 整型才能访问类别名列表。批量测试时,循环这个函数并记录每张图的预测结果,输出成 CSV 保存,方便和人工标注对比。
5.3 用 Gradio 快速搭一个 Web 演示界面
训练完成的模型做成命令行脚本,对于验证算法已经够了,但要给植保站同事或非技术人员演示,一个网页上传入口远好过让他们敲命令。Gradio 只需要几行代码就可以把推理函数包成 Web 界面。
import gradio as gr def classify(image): label, confidence = predict_image(model, image, class_names) return f"{label} (置信度 {confidence:.2%})" app = gr.Interface( fn=classify, inputs=gr.Image(type='pil', image_mode='RGB'), outputs=gr.Label(num_top_classes=3), title='农作物病虫害识别演示', examples=['samples/tomato_late_blight.jpg', 'samples/healthy.jpg'] ) app.launch(server_name='0.0.0.0', server_port=7860)gr.Image(type='pil')表示上传的图片以 PIL 格式传给推理函数,正好和前面的predict_image衔接;gr.Label(num_top_classes=3)会同时展示概率最高的前三个类别,这一步很关键,因为很多病害的第二候选类别概率只比第一低几个百分点,遇到这种情况说明模型在两类之间拿不准,需要人工复核。examples参数把示例图片展示在界面上,用户不用先拍照就能点开试用。
5.4 提高复现性的一个具体技巧:固定随机种子再测一次
源码包说明文档里如果写了“测试准确率 92%”,你复现出来只有 88%,不一定是模型坏了。PyTorch 里的随机性来源很多:数据加载时的 shuffle 顺序、Dropout 随机失活、ResNet 里的 BatchNorm 行为。要复现论文或文档里的指标,把训练脚本开头的随机种子固定下来。
import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False这里比较隐蔽的是cudnn.benchmark = False。默认情况下 PyTorch 会针对当前输入尺寸自动选择最优卷积算法,这个选择过程在不同机器上结果不一致;关闭 benchmark 后卷积计算固定为确定性算法,代价是推理速度略慢 5% 到 10%,但换来的是同样的数据和同样的代码,在不同机器上跑出完全一致的结果,这对验收一个带说明文档的源码项目很有价值。除了模型权重外,训练过程的随机性修复能让你判断“文档里 92% 的准确率”到底是模型真的学到了有效特征,还是运气好碰到了一个特定的数据划分。
本文还有配套的精品资源,点击获取