基于PyTorch的农作物病虫害识别:迁移学习与图像分类实战
简介农作物病虫害识别系统是一份基于机器学习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(brightness0.2, contrast0.2, saturation0.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 数据增强的适用边界与参数建议数据增强不是越多越好。病虫害识别的难点在于病斑位置、大小、颜色深浅变化很大所以增强的优先级是颜色抖动 旋转 裁剪 平移。颜色抖动很关键因为田间不同光照条件下同一病害的叶片颜色差异很大而平移类增强对叶片这种中心型目标帮助不大翻过头反而会把病斑移出画面。增强方式典型参数适用场景不适用场景RandomResizedCropscale(0.6, 1.0)病斑只占叶片局部时病斑布满全叶、需要整体特征时ColorJitterbrightness0.2, contrast0.2田间光照复杂、早晚色温不同数据集本身颜色已经很统一RandomRotation10~20 度叶片朝向不固定病斑具有明确方向敏感性的任务RandomErasingp0.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_nameresnet18, freeze_backboneFalse): if model_name resnet18: model models.resnet18(weightsmodels.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(inplaceTrue), nn.Linear(256, num_classes) ) elif model_name resnet50: model models.resnet50(weightsmodels.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 是 512ResNet50 是 2048直接写死虽然能跑但换骨干网络时容易出错。替换后的fc保留了两层全连接加 Dropout比单层线性层多了一点非线性拟合能力适合病斑特征和类别之间不是严格线性关系的场景。freeze_backbone参数控制是否冻结骨干网络。数据集小于一万张时建议冻结只训练分类头训练速度快且不容易过拟合数据集足够大时解冻全部参数用较小的学习率整体微调效果更好。如果想继续加深可以直接把model_name换成resnet34或efficientnet_b0torchvision.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 微调几轮收尾。优化器推荐初始学习率关键参数适用阶段Adam1e-3 ~ 3e-3betas(0.9, 0.99)前期快速收敛、调参初期SGD1e-2 ~ 3e-2momentum0.9, weight_decay5e-4后期精调、追求泛化AdamW1e-4 ~ 5e-4weight_decay5e-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, transformtrain_transforms) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size7, gamma0.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(fEpoch [{epoch1}/20] Loss: {avg_loss:.4f}) torch.save({epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), loss: avg_loss}, fcheckpoints/epoch_{epoch1}.pth)batch_size32在 224x224 输入下ResNet18 显存占用在 3 GB 左右4 GB 显存的旧卡也能跑。scheduler.step()放在每一轮 epoch 末尾配合StepLR(step_size7, gamma0.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_namesclass_names, digits3)) 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_workers4导致内存被吃满改成 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, devicecuda): 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, dim1) 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( fnclassify, inputsgr.Image(typepil, image_modeRGB), outputsgr.Label(num_top_classes3), title农作物病虫害识别演示, examples[samples/tomato_late_blight.jpg, samples/healthy.jpg] ) app.launch(server_name0.0.0.0, server_port7860)gr.Image(typepil)表示上传的图片以 PIL 格式传给推理函数正好和前面的predict_image衔接gr.Label(num_top_classes3)会同时展示概率最高的前三个类别这一步很关键因为很多病害的第二候选类别概率只比第一低几个百分点遇到这种情况说明模型在两类之间拿不准需要人工复核。examples参数把示例图片展示在界面上用户不用先拍照就能点开试用。5.4 提高复现性的一个具体技巧固定随机种子再测一次源码包说明文档里如果写了“测试准确率 92%”你复现出来只有 88%不一定是模型坏了。PyTorch 里的随机性来源很多数据加载时的 shuffle 顺序、Dropout 随机失活、ResNet 里的 BatchNorm 行为。要复现论文或文档里的指标把训练脚本开头的随机种子固定下来。import random import numpy as np import torch def set_seed(seed42): 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% 的准确率”到底是模型真的学到了有效特征还是运气好碰到了一个特定的数据划分。本文还有配套的精品资源点击获取