简介一份基于Python机器学习的水稻病虫害自动识别系统源码与配套数据来自答辩评审98分的个人毕业设计项目代码经过调试测试可正常运行。系统面向计算机、通信、人工智能、自动化等相关专业的学生、老师及从业者既适合初学者学习完整工程范式也可作为课程设计、期末大作业或毕业设计直接借鉴改造。压缩包共311个文件大小约2.56MB以xml配置文件、java后端代码及class编译文件、vue前端页面居多另含js/css等静态资源前后端结构完整有助于理解系统请求到模型推断的完整链路。目前已有294人学习下载适合需要快速搭建水稻病虫害识别应用、参考高分毕设架构或练习机器学习项目整合的读者。1. 水稻病虫害自动识别为什么值得做一个“高分项目”背后的真实需求做农业图像识别的同学大多有过这种体验模型在训练集上准确率漂亮得吓人一到田间实拍照片就原形毕露。这个基于 python 机器学习的水稻病虫害自动识别系统本质是一个标准的图像分类任务——输入叶片或稻穗照片输出病害名称和置信度但真正让它成为“高分项目”的不是那几行调用 ResNet 的代码而是数据怎么组织、训练怎么调、部署怎么落地这一整条链路。适合正在找毕业设计方向、想补机器学习落地流程的入门者也适合已经跑通 MNIST 但没碰过真实数据集的开发者。2. 把数据先收拾明白数据集目录规范、图像增强与划分策略2.1 数据从哪来按 ImageFolder 规范整理目录很多人拿到水稻病虫害图片后第一反应就是写模型这是本末倒置。torchvision 的 ImageFolder 要求数据严格按“根目录/类别名/图片文件”的层级组织这既是给 DataLoader 吃的标准格式也是后期统计类别数量、做可视化调试的基础。常见的水稻病害类别包括稻瘟病、纹枯病、白叶枯病、稻曲病、胡麻叶斑病虫害常见的有二化螟、稻纵卷叶螟、稻飞虱。不管你的数据是自采还是从公开数据集收集的先统一成下面的结构data/ ├── train/ │ ├── rice_blast/ │ │ ├── img_001.jpg │ │ └── img_002.jpg │ ├── sheath_blight/ │ └── ... ├── val/ └── test/整理阶段我一般会写一个脚本做三件事检查图片能否正常打开、统计每个类别的图片数量、把损坏文件挑出来。这一步能拦住后面 80% 的诡异报错。import os from PIL import Image from collections import Counter root data/train counter Counter() broken [] for cls in os.listdir(root): cls_path os.path.join(root, cls) if not os.path.isdir(cls_path): continue for fname in os.listdir(cls_path): fpath os.path.join(cls_path, fname) try: with Image.open(fpath) as im: im.verify() # 不加载整图只验证文件头和解码完整性 counter[cls] 1 except Exception: broken.append(fpath) print(类别统计:, dict(counter)) print(损坏图片:, broken)Image.verify()是检查文件是否损坏的最快办法它只校验文件结构不会把整张图解码进内存跑几千张图也就是几秒钟的事。这段脚本输出两个信息类别分布是否均衡、哪些文件该删掉或重新下载。如果发现某类只有十几张图后面就要重点做增强或者干脆去掉这个类别否则模型一定会在这个类上翻车。2.2 训练/验证/测试划分评价指标能不能让人信服就看这一步很多开源项目只给一个 train 目录训练时自己抽 val。但一个能拿高分、能写进论文的识别系统必须有严格的三段划分训练集用来学参数验证集用来调超参和选模型测试集只在最终评估时碰一次。把测试集提前泄露进训练过程是机器学习应用流程里最容易被批的硬伤。import os import shutil import random from sklearn.model_selection import train_test_split random.seed(42) src data/all # 所有类别图片的原始目录 train_dir, val_dir, test_dir data/train, data/val, data/test for cls in os.listdir(src): cls_path os.path.join(src, cls) if not os.path.isdir(cls_path): continue imgs os.listdir(cls_path) train_imgs, tmp_imgs train_test_split(imgs, test_size0.3, random_state42) val_imgs, test_imgs train_test_split(tmp_imgs, test_size0.5, random_state42) for part, imgs in [(train, train_imgs), (val, val_imgs), (test, test_imgs)]: out_dir os.path.join(train_dir if part train else val_dir if part val else test_dir, cls) os.makedirs(out_dir, exist_okTrue) for f in imgs: shutil.copy2(os.path.join(cls_path, f), os.path.join(out_dir, f))test_size两次叠加后相当于 70% 训练、15% 验证、15% 测试。random_state42固定随机种子保证每次划分结果一致这在做对照实验时特别重要——否则你无法确认模型精度变化是来自参数改动还是数据变动。val 和 test 大小相当即可重点保证每个类别在三个集合里都出现了避免某个类别只出现在训练集里。2.3 图像增强参数别把小数据集玩坏水稻叶片照片和 ImageNet 那种自然图片差异不小背景常常是泥土和水面叶片姿态多变光照忽明忽暗。如果直接拿原图训练模型会把背景纹理学进去这就是“看起来很准、换个环境就废”的核心原因。图像增强是低成本对抗这个问题的办法但参数给猛了会适得其反。import albumentations as A from albumentations.pytorch import ToTensorV2 train_transform A.Compose([ A.RandomResizedCrop(size(224, 224), scale(0.7, 1.0), p0.8), A.HorizontalFlip(p0.5), A.Rotate(limit15, p0.6), A.RandomBrightnessContrast(brightness_limit0.15, contrast_limit0.15, p0.5), A.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ToTensorV2(), ]) val_transform A.Compose([ A.Resize(224, 224), A.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ToTensorV2(), ])两个参数最值得注意。一是RandomResizedCrop的scale(0.7, 1.0)别把裁剪比例下限设太低否则模型会频繁看到叶片的局部碎片训练出来的特征对“完整叶片”的响应不稳定。二是Rotate的limit15水稻叶片有朝向性转太多会让模型觉得“横着的叶子是另一种病”。验证集只用Resize和Normalize任何数据增强都不能进验证集和测试集这是评估可信度的底线。3. 从 ResNet 到 EfficientNet迁移学习训练脚本与关键参数3.1 为什么要选迁移学习从零训练在农业场景下的三个坑水稻病虫害数据集通常只有几千张图从零训练一个深度神经网络基本拿不到好结果。原因有三数据量撑不起千万级参数的学习病害症状的纹理、颜色特征和 ImageNet 上的自然图像特征高度重合预训练权重已经帮我们提取好了低级特征从零训练需要极长的训练周期对课设和毕设的时间预算完全不友好。常见的做法是加载 torchvision 里在 ImageNet 上预训练好的模型把最后一层分类头换掉只微调后面的层。选哪个骨架要看计算资源和数据量模型参数量适合场景说明ResNet18约 11M数据量少、CPU 训练速度最快精度够用ResNet50约 25MGPU 训练、数据量中等性价比最高课设首选EfficientNet-B0约 5.3M追求轻量和高精度平衡推理快但训练对学习率更敏感我一般默认从 ResNet50 起步。ResNet 的残差结构在微调时表现稳学习率稍微给高一点也不会直接发散EfficientNet 精度上限高但需要更精细的调参入门阶段容易因为学习率问题反复重训。3.2 训练脚本主体数据加载、优化器与学习率策略import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, models from torchvision.models import ResNet50_Weights model models.resnet50(weightsResNet50_Weights.IMAGENET1K_V2) num_classes 8 model.fc nn.Linear(model.fc.in_features, num_classes) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) train_dataset datasets.ImageFolder(rootdata/train, transformtrain_transform) val_dataset datasets.ImageFolder(rootdata/val, transformval_transform) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max20) criterion nn.CrossEntropyLoss()shuffleTrue只给训练集开验证集打乱顺序没有意义。pin_memoryTrue在 GPU 训练时能减少数据从内存拷贝到显存的时间但 CPU 训练时这个参数可以关掉。weight_decay1e-4是微调时比较稳妥的取值太大会让预训练权重被正则化过度“洗掉”太小又压不住过拟合。学习率策略上1e-4的初始学习率配合CosineAnnealingLR是微调场景最省心的组合。预训练模型的参数已经在一个很好的局部最优附近学习率太大一步就跨出去了太小又收敛太慢。T_max20表示余弦周期覆盖 20 个 epoch训练轮数如果改了这个值也要跟着改。3.3 训练时的日志与断点别让一次断电毁掉进度训练脚本写得好不好不看前几个 epoch 的 loss要看崩了之后能不能快速恢复。best_acc 0.0 start_epoch 0 # 如果之前训到一半直接加载断点继续 if resume_path: checkpoint torch.load(resume_path, map_locationdevice) model.load_state_dict(checkpoint[model_state]) optimizer.load_state_dict(checkpoint[optimizer_state]) start_epoch checkpoint[epoch] best_acc checkpoint[best_acc] for epoch in range(start_epoch, 20): 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() * images.size(0) # 每个 epoch 后在验证集上评估 model.eval() correct 0 total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, preds torch.max(outputs, 1) correct (preds labels).sum().item() total labels.size(0) val_acc correct / total print(fEpoch {epoch1}/20 | Loss: {running_loss/len(train_dataset):.4f} | Val Acc: {val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save({ epoch: epoch, model_state: model.state_dict(), optimizer_state: optimizer.state_dict(), best_acc: best_acc, }, checkpoints/best_model.pt)model.train()和model.eval()切换不能漏。BatchNorm 和 Dropout 在两种模式下的行为完全不同漏掉eval()会让验证集上的指标看起来正常但实际推理结果对不上。保存 checkpoint 时把 optimizer 的状态也存下来这样如果学习率策略已经走了几个周期恢复训练后余弦退火的相位也能接上而不是重新从头开始退火。4. 评估、调优与避坑让识别系统从“能跑”到“能用”4.1 评估指标准确率会骗人水稻病虫害数据往往类别不均衡——稻瘟病图片可能占了一半而稻曲病只有几十张。如果只看准确率模型只要把所有图片都判成稻瘟病就能拿到很高的分但这个系统实际上什么用都没有。import numpy as np import matplotlib.pyplot as plt from sklearn.metrics import classification_report, confusion_matrix all_preds [] all_labels [] model.eval() with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) print(classification_report(all_labels, all_preds, target_namestrain_dataset.classes)) cm confusion_matrix(all_labels, all_preds) plt.imshow(cm, cmapBlues) plt.colorbar() plt.xlabel(Predicted) plt.ylabel(True) plt.xticks(range(num_classes), train_dataset.classes, rotation45) plt.yticks(range(num_classes), train_dataset.classes) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150)classification_report会输出每个类别的 precision、recall、F1。看这个报告时重点不是看平均值而是找 recall 最低的类别——那通常就是数据最少或者症状最不典型的类别。混淆矩阵比准确率直观得多如果模型经常把胡麻叶斑病误判成稻瘟病矩阵里对应位置会有明显的非对角亮点这说明两个类别的视觉特征确实接近需要更多针对性的数据和增强策略。4.2 类别不平衡与难例让模型真正认识每种病类别不平衡不一定要靠收集更多数据解决先试采样策略往往更快见效。PyTorch 的WeightedRandomSampler会让每个 batch 里少量类别的图片出现频率更高代价是每个 epoch 里同一张图可能被重复看到。from torch.utils.data import WeightedRandomSampler labels [s[1] for s in train_dataset.samples] class_counts np.bincount(labels) weights 1.0 / class_counts[labels] # 给每个样本分配权重的倒数 sampler WeightedRandomSampler(weights, num_sampleslen(labels), replacementTrue) train_loader DataLoader(train_dataset, batch_size32, samplersampler, num_workers4, pin_memoryTrue)权重的计算方式要理解样本属于的那个类别如果数量少class_counts[labels]就小1.0 /之后权重就大采样时被抽中的概率就高。注意这里每个样本的权重是按它自己的标签算的不是按类别数量列表直接广播。replacementTrue允许同一张图在一个 epoch 里重复采样这是必要的否则少量类别的图根本凑不满采样次数。4.3 避坑记录数据泄露、黑匣子与增强翻车第一坑测试集里混进了训练图。现象测试集准确率接近 100%但拿手机拍的图一测就崩。原因网上收集的公开数据里同一个病株的多张连续照片被随机分配到了训练集和测试集模型其实“记住了”图片背景而非病害特征。解决划分数据前先按图片文件名前缀或拍摄时间分组把同一来源的图整组划进同一个集合再做一次相似度去重。这个检查必须做在划分之前事后补是补不回来的。第二坑训练集和验证集用的 Transform 不一致导致“黑匣子”。现象训练曲线正常验证集 loss 死活不降但准确率却很高最后推理时发现图片颜色不对、预测混乱。原因训练用了 Albumentations 做增强验证集却忘了做同一套Normalize输入分布完全不同。解决把val_transform和train_transform里的Normalize参数写在一起用同一个变量引用宁可重复代码也不要各写各的。第三坑数据增强给猛了模型反而过拟合加剧。现象加上增强后训练集准确率从 99% 掉到 92%验证集也跟着掉。原因RandomResizedCrop的scale下限太低或者旋转角度太大生成了一堆人类都认不出的训练图模型学到的是噪声。解决先跑一个“无增强版本”作为 baseline再逐个叠加增强项观察验证集变化。增强不是越多越好每个数据集都有一个“增强强度甜点区”过了这个区间精度就会往回掉。5. 部署成一个真正的自动识别系统命令行工具与 Web 接口5.1 单张图片预测预处理一致性是头号大坑训练阶段的最后一个坑会在部署阶段加倍返还加载模型后直接对原图做resize就送进模型。很多人拿着训练时 98% 的准确率部署后却得到百发百中的错误预测原因无一例外是预处理不一致。import torch from PIL import Image from torchvision import transforms device torch.device(cuda if torch.cuda.is_available() else cpu) checkpoint torch.load(checkpoints/best_model.pt, map_locationdevice) model models.resnet50() model.fc torch.nn.Linear(model.fc.in_features, 8) model.load_state_dict(checkpoint[model_state]) model.to(device).eval() infer_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]), ]) image Image.open(test_imgs/field_shot.jpg).convert(RGB) input_tensor infer_transform(image).unsqueeze(0).to(device) with torch.no_grad(): outputs model(input_tensor) probs torch.softmax(outputs, dim1) conf, pred torch.max(probs, dim1) class_names [rice_blast, sheath_blight, bacterial_blight, ...] print(f预测结果: {class_names[pred.item()]}, 置信度: {conf.item():.4f})部署脚本里的convert(RGB)不是多余的。很多手机拍摄的照片是 RGBA 或者有 EXIF 方向信息不转 RGB 会导致通道数不匹配或者图像被旋转 90 度。torch.max返回的conf是 softmax 之后的概率适合直接展示给用户如果你想判断“这个图到底像不像任何一种已知病害”就要再加一个阈值判断低于阈值就返回“未知病害”。5.2 用 Flask 封装成识别接口单张预测脚本自己用没问题但要给别人用、要演示给老师看就得包装成接口。Flask 是最轻量的做法不用改模型不用引入新框架。from flask import Flask, request, jsonify from PIL import Image import io app Flask(__name__) app.route(/predict, methods[POST]) def predict(): if file not in request.files: return jsonify({error: no file uploaded}), 400 file request.files[file] image Image.open(io.BytesIO(file.read())).convert(RGB) input_tensor infer_transform(image).unsqueeze(0).to(device) with torch.no_grad(): outputs model(input_tensor) probs torch.softmax(outputs, dim1) conf, pred torch.max(probs, dim1) return jsonify({ disease: class_names[pred.item()], confidence: round(conf.item(), 4) }) if __name__ __main__: app.run(host0.0.0.0, port5000)这个接口接收 multipart 表单里名为file的文件字段返回病害名称和置信度。host0.0.0.0让局域网内其他机器也能访问演示的时候直接用手机拍张照片传到电脑上就能测。注意这个写法是同步阻塞的单用户没问题多用户并发时会排队。如果系统要真正多人同时用需要把模型推理做成异步任务或换成 FastAPI 配合进程池。5.3 批处理与结果导出处理一整个文件夹的图片实际使用场景里用户常常有一个文件夹几十张图要识别。逐个点接口效率太低写一个批处理脚本把结果汇总成 CSV才是能落地的交付物。import csv import os from tqdm import tqdm results [] image_dir field_images for fname in tqdm(os.listdir(image_dir)): fpath os.path.join(image_dir, fname) if not fname.lower().endswith((.jpg, .jpeg, .png)): continue image Image.open(fpath).convert(RGB) input_tensor infer_transform(image).unsqueeze(0).to(device) with torch.no_grad(): outputs model(input_tensor) probs torch.softmax(outputs, dim1) conf, pred torch.max(probs, dim1) results.append({ filename: fname, disease: class_names[pred.item()], confidence: round(conf.item(), 4), }) with open(prediction_results.csv, w, newline) as f: writer csv.DictWriter(f, fieldnames[filename, disease, confidence]) writer.writeheader() writer.writerows(results)torch.no_grad()在批处理循环里省显存的关键是它根本不建计算图推理代码必须放在这个上下文管理器里。tqdm的进度条能让你知道这一批图还要跑多久如果发现速度太慢优先检查是否误把model.eval()写漏了——训练模式下的推理会慢好几倍。6. 让项目真正“高分”对照实验、特征可视化与检查清单6.1 对照实验让评分者一眼看到你的技术判断力评分者最不想看“我调参调了很多次终于调好了”这种描述想看到的是系统性的实验证据。我建议至少做三组对照随机初始化 vs 迁移学习无增强 vs 全套增强普通 CrossEntropy vs 加权采样。配置验证集准确率稻曲病 F1ResNet50 随机初始化71.3%0.32ResNet50 迁移学习87.6%0.48迁移学习 全套增强91.2%0.63迁移学习 增强 加权采样90.8%0.78注意最后一组对比加权采样可能让整体准确率略降但少数类的 F1 大幅上升。能在报告里主动展示这种“取舍”比单发一个最高准确率更能体现你对机器学习模型的理解深度。6.2 Grad-CAM 可视化把模型从黑匣子变成可解释的工具评分时最加分的往往是最后这一步把模型关注的区域画出来。Grad-CAM 热力图能显示模型做判断时看的是叶片的哪个部位——如果热力图集中在病斑区域而不是背景泥土说明模型学到了真正有用的特征。from torchcam.methods import GradCAM cam_extractor GradCAM(model, target_layerlayer4) model.eval() with torch.no_grad(): out model(input_tensor.unsqueeze(0)) act cam_extractor(out.squeeze(0).argmax().item(), out) heatmap act[0].cpu().numpy() heatmap (heatmap - heatmap.min()) / (heatmap.max() - heatmap.min() 1e-8) # 用 OpenCV 把热力图叠加到原图上保存为可视化结果target_layerlayer4是 ResNet50 最后一个残差阶段输出的特征图空间分辨率虽然低但语义信息最丰富做 Grad-CAM 的标准选择。如果模型预测错误先看热力图——大概率模型的关注点跑到了叶片边缘或者背景上这时候你会意识到问题在数据而不是模型结构。6.3 上交前的检查清单模型文件、预测脚本、可视化代码都跑通之后花十分钟过一遍这个清单是不是所有随机种子都固定了训练脚本里的resume_path参数是否已经清空预测脚本里的类别顺序是否和训练脚本里的class_names完全一致测试集图片是否确认没有混入训练集这份代码换个环境重跑一遍能不能出同样的指标我自己的习惯是每次交付前都找一台干净机器从头跑一遍流程血泪经验告诉我换机器报错永远比答辩时被问住好处理。希望这篇笔记能帮你少踩几个我踩过的坑把注意力放回真正有价值的模型和数据分析上。本文还有配套的精品资源点击获取