简介:这份资源面向具备一定Python基础、希望上手农作物病虫害识别分类的开发者与学习者,提供从数据到部署的完整项目实践方案。包内共61个文件,以9个ipynb实验笔记、10个md说明文档、15张png与8张jpg示例图为主,另含py脚本、js/css前端资源、yml/yaml部署配置、pkl模型文件及Dockerfile等,压缩包约88.66MB,覆盖训练、评估与上线各环节。项目围绕图像预处理、特征提取与模型训练展开,包含ResNet50、VGG16、VGG19、DenseNet121等预训练网络,并给出TensorFlow、Keras、PyTorch、Fastai多框架实现,配套Flask后端与静态页面,可完成实时识别交互。已有306人学习下载,适合作为课程设计、毕业设计或竞赛项目的参考模板,帮助读者快速理解病虫害分类的完整流程与工程结构。
1. 农作物病虫害识别分类项目:一份 Python 源码加数据集到底能跑出什么
田里叶子刚出现黄褐色斑点时,种植户往往分不清是早疫病还是叶霉病,打错药不仅白花钱,还会让病菌产生抗性。农作物病虫害识别分类要解决的就是这件事:拍一张叶片照片,让模型告诉你它得了什么病、属于哪一类。这个标题里的「基于 Python 的农作物病虫害识别分类项目源码+数据集+说明文档.zip」,本质是一套可复现的迁移学习工程包——用 Python 把预训练卷积网络微调到植物叶片数据集上,输出分类结果。它适合三类人:想入门深度学习但缺完整案例的学生、需要给农业场景做原型验证的开发者、以及手里有自己田间数据想套用现成流程的从业者。热词里的 python 安装、python 教程、yolov8 训练自己的数据集,其实都指向同一个诉求:拿到能跑通的代码和数据,而不是又一篇原理科普。下面我按「数据长什么样、模型怎么搭、训练怎么调、坑在哪」把这条路走一遍。
2. 先看清数据集:病虫害图像的分类体系与预处理
拿到一个病虫害识别项目,第一件事不是急着跑模型,而是把数据集翻个底朝天。图像分类任务里,数据决定了模型上限,网络结构只是逼近这个上限。农作物病害数据集通常按「作物种类 + 病害名称」组织目录,比如Tomato___Early_blight、Potato___Late_blight这种命名,下划线分隔作物和病害,这种命名方式直接对应 ImageFolder 的类别读取逻辑。
2.1 目录结构与类别分布怎么读
常见做法是把数据整理成每个类别一个文件夹,文件夹名就是标签。用 Python 扫一遍目录,能立刻看出类别是否均衡、有没有脏数据。
import os from collections import Counter data_dir = "plant_disease/train" class_counts = {} for cls in sorted(os.listdir(data_dir)): cls_path = os.path.join(data_dir, cls) if os.path.isdir(cls_path): # 只统计常见图片后缀,避免把 .DS_Store 之类算进去 imgs = [f for f in os.listdir(cls_path) if f.lower().endswith((".jpg", ".jpeg", ".png"))] class_counts[cls] = len(imgs) for k, v in class_counts.items(): print(f"{k}: {v}") total = sum(class_counts.values()) print("总样本数:", total) print("类别数:", len(class_counts)) # 找出最少的类别,判断是否需要重采样 minority = min(class_counts, key=class_counts.get) print("最少类别:", minority, class_counts[minority])这段代码做三件事:遍历训练目录、按类别统计图片数量、找出样本最少的类别。参数上,data_dir指向训练集根目录,后缀过滤是为了防止系统隐藏文件干扰计数。逻辑说明:如果最多类别和最少类别差距超过 5 倍,训练时模型会偏向多数类,这时候要么对少数类做数据增强,要么用加权损失。这一步不做,后面准确率虚高但少数病害全预测错,是血泪经验。
2.2 图像尺寸、归一化与数据增强参数
病虫害图像有个特点:病斑在叶片上的位置随机,大小不一。所以预处理不能只做中心裁剪,得用随机裁剪加翻转。常见做法是统一缩放到 224×224,配合 ImageNet 的均值和标准差做归一化,因为后面要用预训练权重。
from torchvision import transforms train_tf = transforms.Compose([ transforms.Resize(256), # 先缩到 256,保留更多细节 transforms.RandomResizedCrop(224), # 随机裁剪到 224,增强位置鲁棒性 transforms.RandomHorizontalFlip(), # 叶片左右翻转不改变病害类别 transforms.RandomRotation(15), # 小角度旋转,模拟拍摄角度差异 transforms.ColorJitter(0.2, 0.2, 0.2), # 亮度对比度扰动,抗光照变化 transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) val_tf = 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(224)的缩放范围默认 0.08 到 1.0,意味着可能裁到病斑局部,这对小病斑识别有帮助。RandomRotation(15)角度别开太大,超过 30 度叶片背景会出现黑边,反而引入噪声。ColorJitter三个 0.2 分别控制亮度、对比度、饱和度,数值再大可能把病斑颜色改得不像病。验证集必须用确定性的 CenterCrop,否则每次评估结果都在抖,没法判断模型是否真的在进步。
提示:如果数据集里同一片叶子拍了多张,务必按叶片或按地块划分训练验证集,不能随机按图片划分,否则验证集里出现训练集同叶片的照片,准确率会虚高十几个点。
3. 模型选型与迁移学习:为什么不用从零训练
病虫害数据集规模通常在几千到几万张,这个量级从零训练卷积网络必然过拟合。迁移学习是这里的标准答案:拿在 ImageNet 上训练好的骨干网络,换掉最后的分类头,用较小学习率微调。热词里出现的 imagenet1k 数据集下载、yolov8 训练自己的数据集,背后都是同一个思路——预训练权重是免费的先验知识。
3.1 骨干网络对比与选择依据
| 网络 | 参数量 | 输入尺寸 | 适合场景 | 注意事项 |
|---|---|---|---|---|
| ResNet18 | 约 11M | 224 | 数据量小、要快速验证 | 层数浅,细粒度病害可能欠拟合 |
| ResNet50 | 约 25M | 224 | 类别多、病斑细微 | 显存占用翻倍,小数据集易过拟合 |
| EfficientNet-B0 | 约 5M | 224 | 移动端部署、算力有限 | 输入分辨率敏感,别随意改 |
| MobileNetV3 | 约 2.5M | 224 | 边缘设备实时推理 | 精度略低,适合做基线 |
我一般先用 ResNet18 跑通全流程,确认数据管道没问题,再换 ResNet50 看精度能涨多少。如果涨幅不到 2 个点,说明瓶颈在数据质量而不是模型容量,换更大网络是浪费算力。
3.2 替换分类头与冻结策略
import torch import torch.nn as nn from torchvision import models def build_model(num_classes, backbone="resnet18", freeze_backbone=True): if backbone == "resnet18": model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT) in_features = model.fc.in_features model.fc = nn.Linear(in_features, num_classes) # 替换分类头 elif backbone == "resnet50": model = models.resnet50(weights=models.ResNet50_Weights.DEFAULT) in_features = model.fc.in_features model.fc = nn.Linear(in_features, num_classes) if freeze_backbone: # 冻结除 fc 外的所有参数,先只训练分类头 for name, param in model.named_parameters(): if "fc" not in name: param.requires_grad = False return model model = build_model(num_classes=38, backbone="resnet18", freeze_backbone=True) trainable = sum(p.numel() for p in model.parameters() if p.requires_grad) print("可训练参数量:", trainable)逻辑说明:weights=...DEFAULT加载预训练权重,model.fc是 ResNet 的全连接层,替换成自己类别数的线性层。冻结骨干网络后,只有分类头参与训练,这样前几个 epoch 不会因为随机初始化的分类头产生大梯度而破坏预训练特征。参数上,num_classes必须等于数据集类别数,多一个少一个都会在算损失时报维度错误。等分类头收敛后,再解冻后面几个 stage 做微调,学习率要调小到 1e-4 量级。
3.3 分阶段解冻的训练循环
import torch.optim as optim from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder train_ds = ImageFolder("plant_disease/train", transform=train_tf) val_ds = ImageFolder("plant_disease/val", transform=val_tf) train_loader = DataLoader(train_ds, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_ds, batch_size=32, shuffle=False, num_workers=4) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) criterion = nn.CrossEntropyLoss() # 第一阶段:只训练分类头 optimizer = optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-3) for epoch in range(5): 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() print(f"stage1 epoch {epoch} loss {loss.item():.4f}") # 第二阶段:解冻 layer4 和 fc,小学习率微调 for name, param in model.named_parameters(): if "layer4" in name or "fc" in name: param.requires_grad = True optimizer = optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-4) for epoch in range(10): 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() print(f"stage2 epoch {epoch} loss {loss.item():.4f}")参数说明:第一阶段学习率 1e-3,因为分类头是随机初始化的,需要较大步长。第二阶段降到 1e-4,避免把预训练特征冲掉。batch_size=32在 8G 显存下跑 ResNet18 比较稳,显存不够就降到 16 并同步调小学习率。num_workers=4是数据加载线程数,Windows 上如果报错就设成 0。这套两阶段流程比一上来就全网络微调更稳,尤其在小数据集上。
4. 训练、评估与推理:把模型跑成能用的工具
训练完不等于能用,得看混淆矩阵、看单类召回,还要把模型导出成推理脚本。很多项目源码只给训练代码,不给推理入口,拿到手不知道怎么预测一张新图,这是常见缺陷。
4.1 评估指标与混淆矩阵
from sklearn.metrics import classification_report, confusion_matrix import numpy as np model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for imgs, labels in val_loader: imgs = imgs.to(device) preds = model(imgs).argmax(dim=1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) print(classification_report(all_labels, all_preds, target_names=val_ds.classes, digits=3)) cm = confusion_matrix(all_labels, all_preds) # 找出被误判最多的类别对 for i in range(len(cm)): for j in range(len(cm)): if i != j and cm[i][j] > 5: print(f"{val_ds.classes[i]} 误判为 {val_ds.classes[j]}: {cm[i][j]} 次")逻辑说明:classification_report给出每类的精确率、召回率、F1,重点看召回率低的类别。混淆矩阵里非对角线的大数值就是主要错误来源。参数上target_names直接取val_ds.classes,顺序和标签索引一致。如果某两类互相误判严重,通常是它们病斑形态接近,需要针对性补充这两类的区分性样本,而不是盲目加数据。
4.2 单张图片推理脚本
from PIL import Image import torch.nn.functional as F def predict(image_path, model, class_names, device): model.eval() img = Image.open(image_path).convert("RGB") # 强制三通道,防灰度图报错 tensor = val_tf(img).unsqueeze(0).to(device) # 增加 batch 维度 with torch.no_grad(): logits = model(tensor) probs = F.softmax(logits, dim=1)[0] top_prob, top_idx = probs.max(0) return class_names[top_idx.item()], top_prob.item() label, prob = predict("test_leaf.jpg", model, val_ds.classes, device) print(f"预测: {label}, 置信度: {prob:.3f}")参数说明:convert("RGB")很关键,手机拍的叶片图可能是 RGBA 或灰度,不转三通道会在归一化时报错。unsqueeze(0)把单张图变成[1, 3, 224, 224],因为模型要求有 batch 维度。softmax把 logits 转成概率,置信度低于 0.6 时建议提示用户重新拍摄,而不是硬给一个结果。
注意:推理时的预处理必须和验证集完全一致,包括 Resize 尺寸和 Normalize 参数。训练用 256 缩放、推理用 224 直接缩放,精度会掉好几个点,这种不一致是排查半天才发现的典型翻车点。
5. 避坑与排查:病虫害识别项目最容易翻车的五个地方
这一章按「现象 → 原因 → 解决」写,都是我在实际项目里踩过的。
现象一:训练准确率 99%,验证准确率 60% 都不到。原因:训练集和验证集按图片随机划分,同一片叶子的多张照片被分到两边,模型记住了叶子背景而不是病斑。解决:按叶片 ID 或拍摄批次划分,确保同一来源的图片只出现在一个集合里。如果数据集没给叶片 ID,就按文件名前缀分组划分。
现象二:模型把所有类别都预测成样本最多的那一类。原因:类别极度不均衡,交叉熵损失被多数类主导。解决:用加权交叉熵,权重取类别频率的倒数;或者对少数类做过采样,配合强增强。代码上给nn.CrossEntropyLoss(weight=class_weights)传入按类别数计算的权重张量。
现象三:验证集 loss 突然变成 nan。原因:学习率太大,或者某张图片损坏导致输入出现异常值。解决:先把学习率降一个数量级;再用脚本遍历数据集,用 PIL 打开每张图,捕获异常并打印文件名,把损坏图片剔除。数据集中混入零字节文件是常见情况。
现象四:推理时置信度普遍偏低,都在 0.3 左右。原因:推理预处理和训练不一致,最常见的是归一化均值标准差写错,或者忘了除以 255。解决:把训练时的 transform 对象直接复用到推理,不要手写归一化。检查ToTensor()是否在Normalize之前。
现象五:换一台机器跑,报显存不足或 num_workers 报错。原因:batch_size 和 num_workers 是跟机器绑定的,Windows 上多进程数据加载容易出问题。解决:把 batch_size 和 num_workers 提成命令行参数或配置文件,Windows 下 num_workers 设 0,Linux 下再调大。别把机器相关参数硬编码在代码里。
6. 从能跑到好用:提升小样本病害识别的两个技巧
模型跑通只是起点,真正决定这个项目值不值得投入的,是它在少见病害上的表现。我一般会做两件事。
第一件是类别平衡采样加焦点损失的组合。焦点损失通过调制因子降低易分类样本的权重,让模型把注意力放在难分的少数类上。实现上把交叉熵换成带 gamma 参数的焦点损失,gamma 取 2 比较常用,再配合按类别倒数采样的 WeightedRandomSampler,少数类的召回率通常能提十来个点。参数别乱调,gamma 超过 3 容易训练不稳定。
第二件是用测试时增强做推理。对同一张待预测图片做多次轻微变换(水平翻转、小角度旋转),把多次预测概率平均。这个技巧不增加训练成本,只增加推理时间,在病斑边界模糊的样本上效果明显。
| 技巧 | 改动位置 | 预期收益 | 代价 |
|---|---|---|---|
| 焦点损失 | 损失函数 | 少数类召回提升 | 需调 gamma |
| 平衡采样 | DataLoader | 缓解类别偏斜 | 训练轮次需增加 |
| 测试时增强 | 推理脚本 | 置信度更稳 | 推理耗时翻倍 |
验证这些改动是否有效,不能只看总体准确率,要盯住混淆矩阵里少数类的召回变化。我习惯每次改动只动一个变量,跑完对比分类报告,确认涨的是目标类别再保留。这套流程走下来,一个病虫害识别项目才算从「能跑」变成「敢用」。希望帮到你。
本文还有配套的精品资源,点击获取