ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python农作物病虫害识别分类项目:从数据集到部署的完整实践

Python农作物病虫害识别分类项目:从数据集到部署的完整实践 简介基于Python的农作物病虫害识别分类项目面向计算机相关专业毕业生和有实战练习需求的学习者尤其适合用于毕业设计、课程设计、期末大作业等场景。压缩包共96个文件以76个.py源文件为核心配有15个.pyc预编译模块、2个zip资源包、2个txt训练日志与说明、1个md使用文档整体25.09MB体量紧凑便于分类查看和学习。已有128人学习下载。项目经严格调试下载即可运行包含数据加载、模型训练、验证与预测的完整流程并整合ResNet、EfficientNet、Swin Transformer等主流网络实现附有模型权重和训练记录便于参考算法细节、替换骨干网络、调整超参数以做对比实验。数据集已随包提供无需额外寻找使用说明写清运行环境和步骤适合快速上手并继续扩展直接支撑毕业论文撰写与答辩演示。1. 农作物病虫害识别分类项目拿到这份Python源码先别急着跑从网上下载一份“基于python的农作物病虫害识别分类项目源码数据集使用说明.zip”解压后第一感觉通常是内容不少文件夹里既有train.py、predict.py这类Python脚本又有一堆按病害类别分好的叶片图片。大多数人习惯性双击运行train.py结果在数据集路径、依赖版本、权重加载这三关轮番翻车。这个项目本质上是一条很标准的数据集→训练→推理流水线把农作物叶片图像整理成分类数据集用CNN模型输出这是哪种病虫害。真正值得花时间的不是跑通脚本而是把数据目录、训练参数、推理阈值这三块按自己的数据重做一遍。这篇文章围绕这条流水线展开适合要做毕设、课程设计或农业信息化落地的Python开发者。新手能从里面找到可以直接抄的脚本老手也能看到训练和部署时的边界条件。2. 数据准备从数据集到可训练的目录结构2.1 拿到数据集先别急着标注花三分钟统计类别和数量这类项目附带的数据集最常见的形态是一个大类文件夹下面按病害名称分目录例如data/raw/Tomato___Early_blight/、data/raw/Tomato___Late_blight/里面直接是jpg或png图片。有些数据集下载下来文件名是随机哈希串图片格式混着大写后缀还有极少量的空目录和损坏图片。直接把这些目录扔进ImageFolder之前先写一个扫描脚本把家底摸清楚这一步能省掉后面大量莫名其妙的报错。import os from collections import Counter dataset_root data/raw exts (.jpg, .jpeg, .png, .bmp) counter Counter() for class_name in sorted(os.listdir(dataset_root)): class_dir os.path.join(dataset_root, class_name) if not os.path.isdir(class_dir): continue images [f for f in os.listdir(class_dir) if f.lower().endswith(exts)] counter[class_name] len(images) for class_name, count in counter.items(): print(f{class_name}: {count}) print(total:, sum(counter.values()), classes:, len(counter))这段脚本做的事很简单遍历data/raw下的每个子目录把扩展名规范化为小写后做过滤统计每个类别的图片数量。输出结果里如果发现某个类别只剩几张图或者类别总数和说明里对不上就要在划分前先补齐数据而不是带着残缺数据直接训练。这里有一个新手常见误用用os.listdir直接数所有文件把子目录和隐藏文件也算进去导致统计偏大。统计结果还能当“常识校准”用。比如番茄病害数据集通常有10个左右类别如果扫描出来只有7个先怀疑下载解压是不是不完整而不是急着跑训练。顺带可以用PIL把每张图真正打开一次过滤掉损坏文件这个操作虽然慢但对后续训练稳定性帮助很大。2.2 划分训练集和验证集按类别抽样而不是全局乱切划分训练集和验证集是数据集准备里最容易被忽视的一步。常见做法是随机切8:2但如果直接对整个文件列表做random.shuffle再切样本量小的类很可能在验证集里出现0张或只有1张的情况验证指标就会剧烈抖动。按类别分别划分是更稳妥的方式保证每个类在训练集和验证集里都有且比例接近。import os import random import shutil random.seed(42) src_root data/raw dst_root data/split val_ratio 0.2 for class_name in os.listdir(src_root): class_dir os.path.join(src_root, class_name) if not os.path.isdir(class_dir): continue images [f for f in os.listdir(class_dir) if f.lower().endswith((.jpg, .jpeg, .png))] random.shuffle(images) val_count int(len(images) * val_ratio) val_dir os.path.join(dst_root, val, class_name) train_dir os.path.join(dst_root, train, class_name) os.makedirs(val_dir, exist_okTrue) os.makedirs(train_dir, exist_okTrue) for img in images[:val_count]: shutil.copy(os.path.join(class_dir, img), os.path.join(val_dir, img)) for img in images[val_count:]: shutil.copy(os.path.join(class_dir, img), os.path.join(train_dir, img))代码逻辑先用random.seed(42)固定随机种子保证同一份数据在不同机器上划分结果一致然后对每个类单独shuffle再用前20%做验证集。这里刻意用复制而不是移动原图原始数据集还能留一份做对比实验。val_ratio一般取0.15到0.2。样本量在几千张的时候取0.2如果数据集达到几万张0.1就够。类别的目录名不要改它会被PyTorch的ImageFolder直接当作标签用任何改名都会让训练代码里的类别索引错位。划分完后还要做一个校验动作遍历train和val两个目录确认每个类在两边的数量都不为0。这一步用几行代码就能实现但能拦住后面大部分“训练正常但验证曲线诡异”的问题。2.3 数据增强旋转翻转可以颜色抖动别太狠划分好之后训练时还要做数据增强。病虫害识别的核心特征是叶片上的病斑纹理比如早疫病的同心轮纹、晚疫病的褐色坏死斑这些属于形状和纹理特征因此随机旋转、随机翻转、随机裁切都安全。颜色抖动要克制把亮度、对比度、色相调得太狠病斑颜色偏移模型反而学不到真实的病斑特征。验证集和测试集绝不能加增强只用中心裁剪加归一化否则验证结果会虚高。from torchvision import transforms train_transform transforms.Compose([ transforms.RandomRotation(degrees15), transforms.RandomHorizontalFlip(p0.5), transforms.RandomResizedCrop(size224, scale(0.8, 1.0)), transforms.ColorJitter(brightness0.1, contrast0.1, saturation0.1, hue0.02), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform 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.8到1.0适合病害叶片这种目标占满全图的场景取太小会把病斑裁掉ColorJitter的hue只给0.02色相抖动稍大一点叶片的正常绿色和病斑的褐色就会互相污染。Normalize的均值和方差是ImageNet的统计值迁移学习任务里直接沿用不要自己重算否则预训练权重等于白加载。还有一个容易翻车的细节训练和推理必须用同一套归一化参数。我看到过有人训练时用上面这套ImageNet参数推理时用了别的均值和方差结果得分整体偏低还反过来怀疑模型训练有问题。这类问题虽然不复杂但排查起来很耗时间养成“transform只定义一次训练推理共用”的习惯就能避开。3. 模型选择与训练脚本迁移学习是主流路线3.1 为什么常见项目都选迁移学习而不是从头搭CNN病虫害分类数据集规模通常在几千到两万张之间这个量级从零训练一个VGG或ResNet效果通常不好。原因很直观CNN低层要学边缘、纹理这些通用特征需要海量图片喂而ImageNet预训练权重已经把通用特征学完了。迁移学习的做法是把预训练模型的前面层冻结只训练最后的分类层和少量微调层少量样本就能收敛。这也是这类“源码数据集”项目普遍采用的路线。网络结构参数量适合场景ResNet18约11M通用分类CPU推理勉强可接受精度性价比高ResNet50约25M需要更高精度有GPU训练MobileNetV3约5M部署到嵌入式设备或低算力环境EfficientNet视版本而定追求极致精度调参成本偏高在病虫害识别场景里我一般先用ResNet18跑通全流程。理由有三点第一torchvision里一行就能加载预训练权重省掉自己设计网络的调试时间第二参数量少CPU训练也能在合理时间内完成一轮第三做迁移学习时最后一层替换成本最低只改一个fc就能适配任意类别数。MobileNetV3适合后面做部署时再换第一版跑通流程不必纠结。这里还牵出一个更实际的问题源码包里的时间戳和当前时间往往隔了一两年网上随便搜到的python教程和源码包大多是ResNet时代的写法而当前torchvision版本已经把pretrainedTrue参数改成weightsResNet18_Weights.DEFAULT了。对着旧代码报错时先意识到这是接口版本差异不要硬去翻源码。3.2 训练脚本核心写法冻结backbone先只练分类头import torch import torch.nn as nn from torchvision import models num_classes 15 model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) for param in model.parameters(): param.requires_grad False in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) optimizer torch.optim.Adam(model.fc.parameters(), lr1e-3) criterion nn.CrossEntropyLoss()代码逻辑加载预训练权重后遍历全部参数把requires_grad设为False这样反向传播不会更新backbone然后取出原始fc层的输入维度替换成输出维度等于类别数的新分类头。优化器只接收fc.parameters()所以训练时只更新分类头这一层整个流程的记忆占用也很小。这里有一个常见分歧要不要解冻最后几个残差块一起微调。我的建议是先用“只训分类头”跑通全流程等确认数据和代码链路没问题后再解冻最后两层以1e-5学习率微调。一上来就全量解冻训练曲线容易震荡排错时很难分清是数据问题还是微调策略问题。参数名建议值说明lr1e-3只训练分类头时用1e-3解冻backbone后降到1e-5batch_size16或32取决于显存ResNet18配合batch32在6GB显卡上能跑epochs20数据少时20轮足够主要看验证loss是否还在降weight_decay1e-4防止分类头过拟合如果机器没有GPUbatch_size降到8或16CPU训练也能在可接受的时间内跑完一轮只是总训练时长会拉长到几小时甚至一夜。这类项目本身对实时性要求不高训练慢一点可以接受但推理阶段必须保证单张图片秒级出结果。3.3 训练主循环把checkpoint和早停写进去很多源码包里的训练脚本只有一个最朴素的for循环跑完指定epoch就结束连模型都不保存。实战里一定要加两样东西按验证loss保存的最优checkpoint以及连续几轮不下降就提前停下来的早停逻辑。best_val_loss float(inf) patience 5 bad_epochs 0 for epoch in range(epochs): model.train() train_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() train_loss loss.item() val_loss 0.0 model.eval() with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) val_loss loss.item() if val_loss best_val_loss: best_val_loss val_loss bad_epochs 0 torch.save({ model_state: model.state_dict(), num_classes: num_classes, }, best_model.pth) else: bad_epochs 1 if bad_epochs patience: print(early stop at epoch, epoch) break逻辑说明每个epoch先train再evaleval阶段用torch.no_grad()关掉梯度计算节省显存。只有验证loss创新低时才保存checkpoint连续5轮没突破就提前终止。保存的对象用dict而不是只保存权重带上num_classes等元信息后面推理脚本加载时能做兼容判断。这里有一个值得养成的习惯保存checkpoint时同时打印当前epoch的train_loss和val_loss。如果两个loss差距越拉越大说明过拟合在加重如果两者都纹丝不动说明学习率可能过小。观察这两个数字的变化比盯准确率更能反映训练状态因为它能提前暴露出数据或参数的问题。依赖环境安装如果卡在torch版本上按“torch版本对应torchvision版本”去查对照表即可这类问题通常和python版本、pip源都有关系换源重装往往能解决。4. 推理部署阶段单张识别与批量识别4.1 单张图片识别的完整流程eval模式一定要开训练完成后第一步是做单张图片的推理验证。很多源码包里predict.py写得很简单加载权重后直接forward但漏掉了一个关键点模型推理前没有调用model.eval()。这行代码决定BatchNorm层用的是训练统计量还是运行统计量忘掉它的后果是单张结果和验证结果差一大截属于最常见的玄学翻车点。import torch from PIL import Image from torchvision import transforms, models def load_model(model_path, num_classes, device): model models.resnet18() model.fc torch.nn.Linear(model.fc.in_features, num_classes) checkpoint torch.load(model_path, map_locationdevice) model.load_state_dict(checkpoint[model_state]) model.to(device) model.eval() return model def predict(model, image_path, transform, device): image Image.open(image_path).convert(RGB) tensor transform(image).unsqueeze(0).to(device) with torch.no_grad(): probs torch.softmax(model(tensor), dim1) top_prob, top_idx torch.topk(probs, 3, dim1) return top_idx[0].tolist(), top_prob[0].tolist()说明加载模型时要先构造一个和训练时结构一致的resnet18再替换fc层这样load_state_dict才能对上。训练时如果模型用DataParallel包过权重里会多出module.前缀加载时会报unexpected key处理方式在下一章避坑里展开。Image.open之后强制convert(RGB)把灰度图和RGBA图统一成三通道防止输入维度不对。topk返回的是概率最大的前3个索引和对应概率。单张识别时打印这三项比只打印Top-1更能看出模型是不是在几个相似类别之间犹豫例如早疫病和晚疫病早期症状确实相近Top-2也是早疫病或晚疫病就说明模型学到的特征没跑偏。4.2 批量识别导出CSV把置信度一起输出实际使用场景里很少一张一张看图更多是给一个文件夹的叶片照片做批量识别。批量脚本除了输出预测类别还要输出置信度最好加上Top-3。原因是Top-1置信度低时把Top-3的结果列出来农技人员肉眼复核时可以参考第二名是不是同一类病的相近变种。低于阈值的样本单独标记为“无法识别”别硬给一个类别。import csv import os from tqdm import tqdm results [] threshold 0.5 for file_name in tqdm(os.listdir(image_dir)): image_path os.path.join(image_dir, file_name) if not file_name.lower().endswith((.jpg, .jpeg, .png)): continue try: indices, probs predict(model, image_path, val_transform, device) label class_names[indices[0]] if probs[0] threshold else 无法识别 results.append([file_name, label, round(probs[0], 4), class_names[indices[1]], round(probs[1], 4)]) except Exception as e: results.append([file_name, 读取失败, 0, str(e), ]) with open(predict_results.csv, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([文件名, 预测类别, Top1置信度, Top2类别, Top2置信度]) writer.writerows(results)这段代码值得拆开讲几个点。用tqdm加进度条是因为批量识别几百张图时模型虽然在推理但看起来像卡死进度条能直观看到处理到哪一张。CSV写入用utf-8-sig编码Windows下Excel打开不会乱码这个细节常被忽略。异常捕获保证一张损坏图片不会中断整个批量任务失败的图片单独标记出来后面统一补拍或人工复核。threshold0.5只是初始值具体取多少应该在验证集上统计后确定。把阈值定太高会把大量低置信度的正确样本拒识定太低误报会混进结果。实际操作里先跑一遍验证集看每个类别的置信度分布再决定阈值而不是拍脑袋定一个0.5。4.3 阈值怎么定在验证集上统计最低正确置信度定阈值这件事常见做法是拿验证集推理一遍把每个类别的正确预测置信度分布拉出来取每个类在验证集上的最低正确置信度作为该类的阈值下限。比如某个类正确样本的最低置信度是0.62把全局阈值定成0.7就会把这一类全杀光如果只盯着整体准确率把阈值定到0.3又会有大量错分混进来。所以阈值应该是一个按类别区分的列表而不是一个全局值。统计方法是加载验证集循环predict记录label、预测类别、置信度分桶统计。有了这张表生产端的拒识逻辑就清晰了低于阈值的样本不进入自动决策转人工复核。这是从“能跑通demo”到“敢上线”的分水岭农业场景里误报会让农户对系统失去信任拒识转人工比硬给一个错误结论更能保住可靠性。4.4 把推理封装成一个小工具界面命令行脚本对开发者自己够用但要交给农技站或农户使用就费劲了。常见做法是用Streamlit包一层最简页面上传图片、显示预测类别和置信度、展示Top-3的候选结果。实现上只是把predict()函数接到上传控件后面核心逻辑不变界面部分几十行代码就能完成。这类“完整代码界面”的组合在课程设计和实际项目里都很受欢迎因为展示效果好落地阻力小。我一般建议先做批量CSV验证结果可靠再花半天时间包界面顺序不要反过来。5. 病虫害识别项目避坑指南五个高频翻车现场5.1 现象路径带中文图片一张都读不出来训练脚本运行报FileNotFoundError或者PIL打开图片返回None检查路径确实存在。原因在Windows下部分OpenCV版本和旧版torchvision对中文路径的编码处理有问题PIL相对好一点但也可能碰到。解决方法是把数据集放到纯英文路径下例如C:\crop_data代码里统一用pathlib.Path构造路径。如果你的数据已经在中文目录里最省事的办法是复制一份到英文路径而不是和编码问题较劲。python入门阶段遇到这类问题最容易劝退人先配好vscode python环境再动项目能少走很多弯路。5.2 现象训练集准验证集突然掉到接近0训练集准确率很高验证集准确率却忽高忽低或者某几个类别永远分错。打印分类报告发现预测结果整体偏移一个索引。原因多半是手改过目录名或者用os.listdir拿到的是乱序目录而模型训练时用的class_to_idx和推理脚本里的类别列表不一致两边顺序差了一位。解决方法是训练脚本开头打印class_to_idx与推理脚本里的class_names逐项比对两边的类别列表必须来自同一个来源最好统一由sorted(os.listdir(train_dir))生成。目录名一旦定下来项目进行中尽量不要再改否则旧checkpoint全部作废这件事在动手前想清楚免得后面没有后悔药。5.3 现象loss一直不降甚至直接nan训练输出loss一直是1500这种大数值或者几轮后突然变成nan。最常见原因是输入图片没归一化像素值在0到255区间直接送入网络数值范围过大导致梯度爆炸其次是学习率太大尤其是只用fc参数训练时1e-3还能接受一旦解冻backbone就要降到1e-5。解决方法是检查transform里是否同时包含ToTensor和Normalize确认后把lr降到1e-4再跑。如果loss从第一个epoch就是nan优先怀疑数据里有全黑或全白的损坏图片把异常文件从数据目录里移除。还有一种隐蔽情况某些图片是四通道PNG没有做convert(RGB)网络输入维度对不上导致前向传播报错。5.4 现象换机器加载权重报unexpected key在A机器训练好的best_model.pth拿到B机器上加载时报Missing key或Unexpected key模型参数总对不上。原因有两个方向A机器用DataParallel包过模型权重键名多出module.前缀或者A机器的torchvision版本新权重结构有细微差异。解决方法是加载前打印model.state_dict().keys()和checkpoint里的keys做对比不一致的话把module.前缀剥掉再load。更省心的做法是训练时只保存model.state_dict()不保存整只model对象跨机器的兼容性最好。版本号这种东西训练时记录到训练日志里换机器时一眼就能看出差异。5.5 现象模型只会报那两三个常见病验证集总体准确率有九十多但打开混淆矩阵大部分类别没被正确召回所有样本都被分到样本量最大的那两三个类里。原因是类别不均衡。这类病虫害数据集普遍存在这个问题健康叶片和某几种常见病占大头稀有病害只有几十张样本模型学不到稀有类的特征分类头就偏向高频类。解决方法是先按第2章的统计脚本确认每个类的样本数再选择class_weight加权或WeightedRandomSampler。前者是给loss里样本少的类加权后者是让每个batch里稀有类的出现概率更高。这两种方法都可以救回稀有类但不代表能完全弥补数据量的绝对不足样本只有几十张的类优先补数据才是正路。6. 诊断类别不均衡加权Loss与自适应阈值6.1 两行代码换成加权损失在5.5的基础上最直接的改法是计算每个类样本频率的倒数把它乘进CrossEntropyLoss。比如第i类有200张第j类有2000张那么第i类的权重约为第j类的10倍。这样做损失函数里稀有类的每个样本贡献更高分类头被迫把注意力分给稀有类。import torch.nn as nn import torch class_counts torch.tensor([1200, 200, 300, 1500, 80], dtypetorch.float) weights 1.0 / class_counts weights weights / weights.sum() * len(class_counts) criterion nn.CrossEntropyLoss(weightweights.to(device))这里weights做了一步归一化用1.0/class_counts算出每个类的逆频率再除以总和乘以类别数让权重均值回到1附近避免权重绝对值太大导致训练早期loss爆炸。如果你的数据集类别数很多先看统计结果再决定是否全局加权样本少于几十张的类单靠加权loss救不回来属于结构性数据缺陷。另一种常见方案是WeightedRandomSampler它作用在数据加载层面让每个batch的类别分布相对均衡。两种方法可以组合sampler负责采样loss里的weight负责梯度加权互不冲突。6.2 按类别设置自适应阈值加权训练后稀有类的置信度分布会上移但整体仍低于高频类。推理阶段不要用一个全局threshold而是在验证集上按类统计置信度分位数取5%分位数作为拒绝阈值。含义是这一类里95%的历史正确样本置信度高于这个值低于它时模型给出的结果大概率不可信。实现方式很简单批量推理时记录每个类所有正确样本的置信度排序后取分位点存成一个字典推理时按预测类别查表。这个参数是判断一个病虫害识别项目能不能真正交给农技人员使用的关键。模型训练得再好如果在低置信度时硬输出一个类别就是在制造错误答案加了按类别拒识之后错误样本会被拦截转人工这比调accuracy指标实在得多。这类“源码数据集使用说明”项目拿回来后最忌讳的就是直接跑训练。我现在养成的习惯是每拿到一个新项目第一件事永远是统计数据量和打印class_to_idx这两个动作看起来不起眼但实际上能避开后面一半的排错时间跑训练前先用一个小数据集验证链路通不通再上全量数据保存权重时永远带上类别数和transform参数。黑匣子一样的网络结构可以慢慢研究数据链路必须先确认无误这个顺序错不了希望帮到你。本文还有配套的精品资源点击获取
返回列表