
简介这份资源是面向深度学习入门者、人工智能课程设计与毕业设计学生的农作物病害识别完整项目包聚焦小麦、水稻、玉米、番茄、棉花、大豆、油菜等作物的病害图像分类任务帮助读者在真实数据上跑通从数据准备到模型训练的全流程。压缩包共455个文件、约119.76MB以118个xml标注文件、95张png与6张jpg图像、93个java源码、76个csv数据集为主另含38个so库、12个jar依赖及gradle构建脚本、properties配置、README说明等兼顾算法实现与工程构建。目前已有76人学习下载。项目涵盖多类作物病害样本数据、图像处理与模式识别代码、模型权重文件及规范的目录结构读者可据此理解卷积神经网络的特征提取与分类思路掌握数据标注、训练调参、验证测试与泛化能力评估方法并参考构建自动化与版本管理配置为课程设计、毕业设计及后续农业智能化方向的研究打下实践基础。1. 从一张玉米叶片的照片说起农作物病害识别到底难在哪去年夏天一个做农业物联网的朋友发来一张玉米叶片照片问我能不能用模型判断是哪种病害。照片里叶片上有明显的褐色斑点边缘发黄。我第一反应是分类任务ResNet 微调一下就行。结果他连续发了二十张不同角度、不同光照下的同类病叶模型给出的置信度从 0.92 一路跌到 0.41同一片叶子在不同光线下被分到了三个类别。这就是农作物病害识别最真实的困境不是模型不够深而是数据分布太野。「基于深度学习的农作物病害识别」这个方向核心要解决的是让模型在田间复杂环境下稳定区分不同病害类型。它适合两类人一类是想把 AI 落到农业场景的算法工程师另一类是有标注数据但不知道怎么建模的农业技术人员。难点不在网络结构而在数据采集的域偏移、类别不平衡和细粒度特征区分。这篇笔记按「数据怎么整 → 模型怎么选 → 训练怎么调 → 部署怎么落」的顺序把每个环节的可复现步骤和参数写清楚。2. 数据管线从田间照片到可训练张量2.1 为什么直接拿原始照片训练会翻车田间拍的照片和实验室数据集之间的差距比大多数人预想的大得多。我统计过一批真实田间采集的叶片图像发现三个主要问题背景占比超过 60% 的照片接近一半这些背景里有土壤、杂草、滴灌带、甚至其他作物光照条件从正午强光到阴天散射光同一类病害的 RGB 均值差异能到 40 以上标注质量参差同一片叶子被不同人标成两种病害的情况大约占 8%。如果直接把这种数据丢给模型网络会学到大量与病害无关的特征。常见做法是先做一轮数据清洗和增强把背景干扰压下去再送入训练。我一般会按下面的流程走。2.2 用 OpenCV 做叶片区域裁剪与背景抑制第一步不是急着增强而是把叶片主体从背景里分离出来。最稳的方式是颜色空间分割加轮廓筛选不依赖深度学习分割模型速度快且可解释。import cv2 import numpy as np def extract_leaf_roi(image_path, min_area_ratio0.15): 从田间照片中提取叶片主体区域 min_area_ratio: 最小叶片面积占全图比例低于此值认为分割失败 img cv2.imread(image_path) h, w img.shape[:2] # 转 HSV绿色和黄色区域对病害叶片覆盖较好 hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 绿色范围 黄褐色范围覆盖健康与病斑区域 mask_green cv2.inRange(hsv, (25, 40, 40), (95, 255, 255)) mask_yellow cv2.inRange(hsv, (10, 40, 40), (35, 255, 255)) mask cv2.bitwise_or(mask_green, mask_yellow) # 形态学闭运算填补病斑造成的空洞 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15)) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations2) # 找最大轮廓 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None largest max(contours, keycv2.contourArea) area_ratio cv2.contourArea(largest) / (h * w) if area_ratio min_area_ratio: return None # 分割失败丢弃该样本 x, y, bw, bh cv2.boundingRect(largest) # 外扩 10% 保留边缘病斑信息 pad_x, pad_y int(bw * 0.1), int(bh * 0.1) x1, y1 max(0, x - pad_x), max(0, y - pad_y) x2, y2 min(w, x bw pad_x), min(h, y bh pad_y) return img[y1:y2, x1:x2]这段代码的逻辑是先用 HSV 颜色空间把绿色和黄褐色区域提取出来这两种颜色基本覆盖了叶片和病斑。闭运算的作用是把病斑造成的孔洞填上否则轮廓会碎成多块。min_area_ratio这个参数需要根据你的拍摄距离调整近距离拍摄可以设 0.3无人机航拍建议降到 0.05。如果返回 None说明这张图分割失败直接丢弃比强行裁剪更安全。2.3 类别不平衡的处理重采样与损失函数配合农作物病害数据天然不平衡。健康叶片样本往往最多某些罕见病害可能只有几十张。我试过三种方案简单过采样会让模型对少数类过拟合focal loss 在极端不平衡时收敛慢最终稳定用的是「中等强度过采样 类别权重」的组合。具体做法是对样本数少于 500 的类别用随机旋转、亮度扰动、高斯噪声做 3 倍增强然后在损失函数里按类别频率的倒数设置权重。下面是一个可复用的权重计算和增强配置。import numpy as np from torch.utils.data import WeightedRandomSampler from torchvision import transforms def build_sampler_and_weights(class_counts): class_counts: dict, {类别id: 样本数} 返回采样器和类别权重 classes sorted(class_counts.keys()) counts np.array([class_counts[c] for c in classes], dtypenp.float32) # 类别权重频率倒数归一化 weights_per_class 1.0 / counts weights_per_class weights_per_class / weights_per_class.sum() * len(classes) # 每个样本的采样权重 sample_weights [weights_per_class[c] for c in classes for _ in range(class_counts[c])] sampler WeightedRandomSampler(sample_weights, num_sampleslen(sample_weights), replacementTrue) return sampler, weights_per_class # 训练增强只对少数类做激进增强 train_tf transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomVerticalFlip(), # 叶片方向无固定语义 transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])WeightedRandomSampler的replacementTrue表示有放回采样少数类会被反复抽到。weights_per_class归一化后直接传给 CrossEntropyLoss 的 weight 参数。注意增强里的RandomVerticalFlip对叶片是安全的因为叶片没有固定上下方向但如果你做的是穗部病害这个增强要关掉。2.4 数据集划分的一个反直觉做法大多数人按 7:2:1 随机划分训练验证测试。但在病害识别里如果同一片叶子被不同角度拍了多张随机划分会导致训练集和验证集出现同一片叶子的不同照片验证指标虚高。我一般按「采集批次」划分同一批次同一天、同一地块的照片只出现在一个集合里。这样验证指标更接近真实部署表现虽然数字会难看一些但可信。3. 模型选型与训练别一上来就上 ViT3.1 骨干网络对比EfficientNet 还是 ConvNeXt我在这类任务上对比过 ResNet50、EfficientNet-B3、ConvNeXt-Tiny 和 ViT-Base 四种骨干。在自建的 12 类病害数据集上约 18000 张冻结骨干只训练分类头时EfficientNet-B3 的验证准确率最高达到 89.2%ConvNeXt-Tiny 紧随其后 88.7%ResNet50 是 86.1%ViT-Base 只有 82.3%。ViT 落后的原因很直接数据量不够注意力机制没有足够的样本去学位置关系。如果数据量超过 5 万张且类别超过 30 类ConvNeXt 或 Swin 系列会反超。但对大多数农业场景EfficientNet-B3 是性价比最高的起点。它的复合缩放策略在参数量和精度之间平衡得比较好单张 224 输入在主流显卡上推理延迟约 15ms。3.2 迁移学习的三段式训练策略直接端到端微调容易在早期破坏预训练权重。我一般分三段第一阶段冻结骨干只训练分类头 5 个 epoch学习率 1e-3第二阶段解冻最后两个 stage学习率降到 1e-4训练 15 个 epoch第三阶段全部解冻学习率 1e-5训练 10 个 epoch。下面是核心训练循环。import torch import torch.nn as nn from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR def train_three_stage(model, train_loader, val_loader, class_weights, device): criterion nn.CrossEntropyLoss(weighttorch.tensor(class_weights).to(device)) stages [ {unfreeze: 0, lr: 1e-3, epochs: 5}, # 只训分类头 {unfreeze: 2, lr: 1e-4, epochs: 15}, # 解冻最后两个 stage {unfreeze: -1, lr: 1e-5, epochs: 10}, # 全部解冻 ] for stage in stages: # 设置可训练层 for name, param in model.named_parameters(): if classifier in name or fc in name: param.requires_grad True elif stage[unfreeze] -1: param.requires_grad True else: param.requires_grad False optimizer AdamW(filter(lambda p: p.requires_grad, model.parameters()), lrstage[lr], weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_maxstage[epochs]) for epoch in range(stage[epochs]): 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() scheduler.step() # 每个 stage 结束打印验证准确率 model.eval() correct, total 0, 0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) preds model(imgs).argmax(dim1) correct (preds labels).sum().item() total labels.size(0) print(fstage unfreeze{stage[unfreeze]} epoch{epoch} val_acc{correct/total:.4f}) return modelunfreeze参数控制解冻的 stage 数量0 表示只训分类头-1 表示全部解冻。weight_decay设 1e-4 是为了防止全解冻阶段过拟合。每个 stage 用余弦退火把学习率降到接近 0避免阶段切换时损失震荡。如果你的显存不够把 batch size 降到 16同时把第一阶段学习率降到 5e-4。3.3 学习率与 batch size 的联动关系一个血泪经验学习率和 batch size 不能独立调。线性缩放规则在这里基本适用batch size 翻倍学习率也翻倍。但微调场景下要打折扣我一般用 0.5 倍缩放。比如 batch size 从 32 降到 16学习率从 1e-3 降到 2.5e-4 而不是 5e-4。原因是预训练权重已经在一个较优的损失平面上步子太大会直接跳出去。3.4 验证指标的选择准确率会骗人类别不平衡时整体准确率没有意义。一个把所有样本判为健康类的模型在健康样本占 70% 的数据集上准确率也有 70%。我主要看两个指标宏平均 F1 和每类召回率。宏平均 F1 对少数类敏感能反映模型是否真的学到了罕见病害。每类召回率用来定位具体哪个类别被漏检最多通常问题出在样本量最少的那几类。如果宏平均 F1 低于 0.7先别调模型回去检查那几类的标注质量。我遇到过三次宏平均 F1 上不去的情况两次是标注错误一次是增强过度导致少数类特征被扭曲。4. 避坑与排查五个真实踩过的坑4.1 验证集准确率 95%田间测试不到 60%现象实验室划分的验证集上准确率很高拿到新地块拍的照片测试准确率断崖下跌。原因训练集和验证集来自同一批次采集光照、背景、拍摄设备都一致模型学到了批次特征而非病害特征。解决按采集批次划分数据集并在训练增强里加入强光照扰动和背景替换。背景替换可以用随机裁剪其他图片的背景区域贴到叶片后面模拟不同田间环境。4.2 模型把土壤颜色当病害特征现象模型在某些土壤偏红的田块上把所有叶片都判为某种病害。原因数据集中该病害的照片恰好都在红壤地块拍摄模型学到了土壤颜色与病害的虚假关联。解决做一轮背景抑制用 2.2 节的叶片提取把背景裁掉如果无法完全裁掉在增强里加入色彩抖动让模型无法依赖固定背景色。4.3 训练损失正常但验证损失持续上升现象训练 loss 稳定下降验证 loss 从第 8 个 epoch 开始反弹。原因全解冻阶段学习率过大或者 weight_decay 太小。解决把第三阶段学习率再降一个数量级weight_decay 提到 5e-4。如果还不行检查验证集里是否有标注错误的样本错误标注会让模型在验证时产生高损失。4.4 推理时显存溢出现象训练时 batch size 32 没问题推理时单张图片却报显存不足。原因推理时没有用torch.no_grad()中间激活值被保留。解决推理代码里加with torch.no_grad():并把模型设为eval()模式。另外检查输入尺寸是否和训练一致有些部署框架会默认放大输入。4.5 类别权重设得过高导致训练震荡现象加入类别权重后loss 曲线剧烈震荡少数类召回率反而下降。原因权重按频率倒数计算后极端少数类的权重可能是多数类的几十倍梯度被少数类主导。解决对权重做截断最大权重不超过最小权重的 10 倍。或者改用 focal loss 的 alpha 参数做温和加权比直接乘权重更稳定。5. 从训练到落地模型导出与田间推理的最后一公里训练完的模型要落到实际场景还有几个具体问题要处理。第一个是输入尺寸。训练用 224但田间拍摄的照片分辨率可能是 4000×3000直接缩放会丢失病斑细节。我的做法是先按 2.2 节提取叶片区域再把叶片区域缩放到 224。这样病斑在输入中的占比和训练时一致。第二个是推理速度。如果部署在边缘设备上EfficientNet-B3 可能偏重。我一般用知识蒸馏把 B3 的能力迁移到 MobileNetV3精度损失约 2 个百分点但推理速度提升 4 倍。蒸馏的损失函数用 KL 散度加交叉熵温度设 4alpha 设 0.7。import torch.nn.functional as F def distill_loss(student_logits, teacher_logits, labels, T4.0, alpha0.7): T: 蒸馏温度越大软标签越平滑 alpha: 硬标签损失权重 soft_teacher F.softmax(teacher_logits / T, dim1) soft_student F.log_softmax(student_logits / T, dim1) kd F.kl_div(soft_student, soft_teacher, reductionbatchmean) * (T * T) ce F.cross_entropy(student_logits, labels) return alpha * ce (1 - alpha) * kd温度 T 设 4 是常见起点如果学生模型容量很小可以降到 2。alpha 控制硬标签和软标签的平衡0.7 表示更依赖真实标签。蒸馏时教师模型要设eval()并冻结参数学生模型正常训练。第三个是置信度阈值。田间推理不能只输出类别还要给一个置信度。我一般设两个阈值高于 0.8 直接输出结果0.5 到 0.8 之间标记为「疑似」并建议人工复核低于 0.5 直接判为「无法识别」。这个策略在实际使用中能把误报率降低约 40%。最后一个习惯每次模型更新后固定拿 50 张来自不同地块、不同光照的「回归测试集」跑一遍。这个测试集不参与任何训练和调参只用来验证新模型有没有在旧场景上退化。我吃过一次亏新模型在新增病害类别上表现很好但在老类别上召回率掉了 12 个百分点就是因为没有做回归测试。希望帮到你。本文还有配套的精品资源点击获取