ARTICLE DETAIL

资讯详情

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

改进YOLOv5实现烟叶病害小目标检测的实战指南

改进YOLOv5实现烟叶病害小目标检测的实战指南 简介一份介绍基于改进YOLOv5烟叶病害检测系统的PPT幻灯片面向计算机视觉、深度学习及农业智能化方向的开发者与研究者。内容覆盖烟草病害防治背景、传统诊断方法的不足、嵌套式YOLOv5识别方案、环境搭建、数据集采集与标注、训练效果以及系统整合并补充了YOLO改进思路如优化函数、激活函数、损失函数、网络结构修改可作为汇报展示或项目方案参考。压缩包内为单个pptx文件大小10.34MB共1个文件页数编排完整按主题分节便于直接演示或二次编辑。已有3118人学习该资源适合用于毕业设计、课题汇报或工业检测项目的前期调研与PPT底稿。1. 改进YOLOv5做烟叶病害检测不是换新模型而是把旧模型改到极致很多做农业视觉的团队一上来就问现在都 YOLOv8、YOLOv9 了谁还碰 YOLOv5但真正落到烟叶病害检测这个场景你会发现数据量几千张、病害病斑小、田间光线杂乱新模型不一定更好训反而部署链路、后处理、找资料的成本全压上来。我这边的结论是把 YOLOv5 改到适配小目标和细粒度病斑比换模型收益更稳也更容易向评审和同行解释清楚。这篇文章按我一直用的落地顺序讲基线怎么选、数据集怎么做、改进点怎么加、训练有哪些坑、最后怎么验证和部署。2. 为什么基线选 YOLOv5s结构、选型与最小跑通环境2.1 YOLOv5s 的网络结构与可改进位置YOLOv5s 的结构算是一张老图了但很多人只记得“它有 backbone、neck、head”并不清楚每一层到底改哪里收益大。我习惯把它分成三块看模块组成对烟叶病害检测的作用建议改进位置BackboneFocus / Conv C3 SPPF提取叶片纹理、病斑边缘特征在主干尾部插入注意力NeckPANet 自顶向下 自底向上融合高层语义与低层细节增加浅层小目标分支Head三个尺度的耦合检测头输出 80x80、40x40、20x20 预测改进损失函数或 NMS这里有个关键点烟叶病斑的物理尺寸往往只有整张叶片的 1% 到 5%在 640x640 输入下对应的目标框可能只有 8x8 到 20x20 像素。YOLOv5s 默认最小的检测层是 80x80 下采样 8 倍对小目标只能算“勉强够用”。所以绝大多数改进方案都围绕两件事让网络更关注病斑区域以及给网络一个更小的检测尺度。选择 YOLOv5s 而不是 YOLOv5m/l 的另一个现实理由烟草行业的现场设备大多是普通工控机或边缘盒子没有 A100。YOLOv5s 在 1080Ti 上能做到 60 到 70 FPS改进后掉到 40 FPS 也还能接受如果直接上 yolov5l改进完基本就告别实时了。后面大家做 PPT 或者演示系统时速度指标是最容易出彩的这也是基线选小的原因。2.2 Python 环境配置与最小跑通命令环境配置看起来没什么技术含量但“yolov5 环境配置”却是大多数新人第一个翻车点。我一般建议用 conda不要直接在全局 Python 里装因为你可能同时有多个检测项目依赖会互相打架。conda create -n smoke_yolo python3.9 conda activate smoke_yolo pip install torch1.13.1 torchvision0.14.1 --index-url https://download.pytorch.org/whl/cu117 git clone https://github.com/ultralytics/yolov5.git cd yolov5 pip install -r requirements.txt参数说明Python 3.9 是 YOLOv5 官方仓库兼容性最稳的版本之一torch 1.13.1 对应 CUDA 11.7如果你的显卡驱动只支持 CUDA 11.8 或 12.1可以把版本换成 torch 2.0.1 cu118。需要说明的是如果你在纯 CPU 环境把 index-url 后缀去掉只装 torch 的 CPU 版本即可但训练时间会非常痛苦。装完之后先别急着训练第一步是跑一次官方推理确认环境没问题python detect.py --source data/images/bus.jpg --weights yolov5s.pt --conf-thres 0.25逻辑说明detect.py会加载预训练权重、读取图片、推理并保存标注结果到runs/detect目录。如果这一步能在 3 分钟内跑通说明 PyTorch、opencv、pillow 这些基础依赖都正常。--conf-thres 0.25是置信度阈值低于这个值的预测会直接过滤掉做烟叶病害检测时我建议先保持 0.25 看原始输出不要一上来就调高否则后期评估时你会误以为模型漏检很多。2.3 用预训练权重先确定“痛点基线”很多人拿到 YOLOv5s 权重直接就--epochs 300开始训练烟叶数据这是不对的。我在拿到任何新数据集时第一步一定是拿官方yolov5s.pt和随机初始化的模型各训 50 个 epoch把两者对比。这样能回答一个问题烟叶病害这种小目标场景到底是从 COCO 预训练权重迁移学得快还是从零开始学更快。实测下来烟叶叶片纹理和自然图像有一定共性用预训练权重做迁移学习前期收敛明显快通常 30 个 epoch 能达到随机初始化模型 80 个 epoch 的水平。但有一个隐藏优点很多人没注意预训练权重自带的 anchor 是基于 COCO 目标统计的烟叶病斑普遍更小所以后面一定要重算 anchor具体操作我会在第四节讲。另外YOLOv5 的 weight 文件头部存储了模型结构映射如果你的代码版本和权重版本不一致会报错“KeyError: model.24.m.0.weight 不存在”这不是模型坏了而是你用 v5.0 代码加载了 v6.0 权重。把这个也列入环境检查项。3. 自建烟叶病害数据集采集标注到 YOLO 格式的转换脚本3.1 病害类别定义与样本采集要点烟叶病害检测的数据集很少有大厂开源大部分课题组或项目组都得自己采集。常见类别我这边按生产实际建议定义四类黑胫病、赤星病、野火病、靶斑病。注意一个原则宁可少而准不要多而乱。如果你把“虫害”“日灼伤”也混进来类别数量增加会让小目标判别难度直线上升最终每一类的 AP 都会掉。采集时有两个容易被忽略的细节第一烟叶病斑经常在叶片背面更明显一定要正反面都拍第二田间的叶片是弯曲、重叠的不要只拍平铺在桌子上的照片否则模型学到的只是干净的“标本特征”一来现场就废。建议每个类别至少 300 张原始图像四类就是 1200 张再通过裁剪、翻转、光照调整扩到 3000 张以上。标注工具我常用 labelImg 或 X-AnyLabeling。如果你是一个人做建议用 X-AnyLabeling 的自动辅助标注可以先用 YOLOv5s 权重预标注一遍再人工修正能省不少时间。但这步有个坑自动标注出来的框经常贴着病斑边缘比人工框小一圈如果直接拿去训练会让模型倾向预测更小的框后面对 mAP 和召回都不利。所以自动标注之后必须把明显过紧的框手动外扩 2 到 3 个像素。3.2 VOC 转 YOLO 格式转换脚本与四个边界坑标注导出格式我建议统一用 VOC xml因为它结构完整方便之后做数据清洗。但 YOLOv5 训练只认 txt 格式的 YOLO 标签所以要写一个转换脚本。我把之前用过的脚本精简后贴出来import os import xml.etree.ElementTree as ET classes [black_shank, frog_eye, wildfire, target_spot] def convert_label(xml_path, out_dir): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in classes: continue box obj.find(bndbox) x1 float(box.find(xmin).text) y1 float(box.find(ymin).text) x2 float(box.find(xmax).text) y2 float(box.find(ymax).text) # 边界保护裁剪到图像范围内避免越界 x1 max(0, min(x1, img_w - 1)) x2 max(0, min(x2, img_w - 1)) y1 max(0, min(y1, img_h - 1)) y2 max(0, min(y2, img_h - 1)) if x2 - x1 1 or y2 - y1 1: continue cx (x1 x2) / 2 / img_w cy (y1 y2) / 2 / img_h bw (x2 - x1) / img_w bh (y2 - y1) / img_h lines.append(f{classes.index(name)} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) out_name os.path.basename(xml_path).replace(.xml, .txt) with open(os.path.join(out_dir, out_name), w) as f: f.write(\n.join(lines)) if __name__ __main__: os.makedirs(labels, exist_okTrue) for xml_file in os.listdir(annotations): if xml_file.endswith(.xml): convert_label(os.path.join(annotations, xml_file), labels)逻辑说明先读取 xml 里的图像宽高再把每个目标的 bndbox 转成中心点坐标与宽高的归一化值。YOLO 格式要求类别索引是整数且从 0 开始所以classes.index(name)这一步必须保证 class 顺序和训练配置一致否则模型会学到错位的标签。参数说明x1 max(0, min(x1, img_w - 1))这行常被省略但如果某个框刚好标注到图像边缘外一两个像素不裁剪会导致归一化坐标大于 1训练时部分损失函数会计算异常表现是 loss 下降正常但 mAP 一直在低位徘徊。转换完成之后要做一次反向检查把生成的 txt 重新画到图上和原标注对比。这一步我通常会写一个十个样本的抽查脚本不再贴代码但建议你至少在四个类别里各人工看一眼别有左图右框错位的情况。3.3 训练集划分与数据增强参数YOLOv5 官方按 8:1:1 划分 train/val/test但我做烟叶病害时会调整到 7.5:1.5:1因为烟叶小目标很多验证集太小会让 mAP 波动像心跳一样一个 epoch 涨 2 个点下一个 epoch 又掉 3 个点根本无法判断改进好坏。划分脚本建议这样写import os import random random.seed(42) images [f for f in os.listdir(images) if f.endswith(.jpg)] random.shuffle(images) n_train int(len(images) * 0.75) n_val int(len(images) * 0.15) with open(train.txt, w) as f: for img in images[:n_train]: f.write(os.path.abspath(os.path.join(images, img)) \n) with open(val.txt, w) as f: for img in images[n_train:n_train n_val]: f.write(os.path.abspath(os.path.join(images, img)) \n) with open(test.txt, w) as f: for img in images[n_train n_val:]: f.write(os.path.abspath(os.path.join(images, img)) \n)划分时注意一个实操细节同一株烟叶的不同角度照片要放进同一个集合不能让一张在 train、一张在 val。否则验证集相当于作弊模型实际上已经见过同株叶片mAP 虚高四五个点等你到现场拍新照片就现原形。数据增强方面YOLOv5 默认的mosaic1.0在最后 10 个 epoch 会自动关闭这个细节很多人不知道。另外烟叶病害图片的 HSV 增强建议把hsv_h调低到 0.01因为烟草叶片颜色是生产判断的重要依据色调扰动太厉害会改变病斑与健康组织之间的颜色对比关系模型会学到错误的色彩先验。4. 三个值得动手的改进方向注意力、小目标检测层与损失函数4.1 在 Backbone 尾部插入 CBAM 注意力烟叶病害检测的核心难点是叶片本身纹理复杂病斑与健康区域的边界有时并不清晰网络很容易把叶脉阴影当成病斑。一种代价小、见效快的方式是在 backbone 尾部加一个 CBAM 注意力模块让网络同时关注“哪里是病斑”和“什么颜色/纹理像病斑”。在models/common.py末尾追加模块定义import torch import torch.nn as nn class CBAM(nn.Module): def __init__(self, c1, reduction16): super().__init__() self.channel_attention nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(c1, c1 // reduction, 1, biasFalse), nn.ReLU(inplaceTrue), nn.Conv2d(c1 // reduction, c1, 1, biasFalse), nn.Sigmoid() ) self.spatial_attention nn.Sequential( nn.Conv2d(2, 1, kernel_size7, padding3, biasFalse), nn.Sigmoid() ) def forward(self, x): ch_att self.channel_attention(x) x x * ch_att avg_out torch.mean(x, dim1, keepdimTrue) max_out, _ torch.max(x, dim1, keepdimTrue) spatial torch.cat([avg_out, max_out], dim1) spatial_att self.spatial_attention(spatial) return x * spatial_att逻辑说明CBAM 分两步做注意力。通道注意力会对每个特征通道算一个权重让网络知道病斑这种颜色突变主要在哪些通道里响应空间注意力则用平均池化和最大池化把特征压成一张“显著性图”让网络聚焦到真正的病斑区域而不是叶片阴影。参数说明reduction16是通道压缩比。如果改成 8注意力表达能力更强但参数量和计算量也更大烟叶病害这种小目标场景我常用 16够用且不容易过拟合。插入位置我建议在 backbone 的最后一个 C3 输出之后、SPPF 之前具体是修改models/yolov5s.yaml中的 backbone 段找到最后一个C3后追加一行backbone: # 省略前面的定义 - [-1, 1, Conv, [1024, 3, 2]] - [-1, 1, C3, [1024, False]] - [-1, 1, CBAM, [1024]] # 新增 - [-1, 1, SPPF, [1024, 5]]这里要特别提醒common.py里没有的模块名称不能直接写进 yaml否则训练时会报AttributeError: module models.common has no attribute CBAM。加完模块必须同步改models/yolo.py的 parse_model 函数在if m in {...}的集合里补上CBAM。4.2 增加 P2 小目标检测层烟叶病斑大部分属于小目标默认的检测层最小 stride 是 8对应输入 640 时特征图只有 80x80病斑在这个尺度上可能只有一两个像素。一个直接有效的手段是增加一个 stride 为 4 的 P2 检测层让网络在 160x160 的特征图上对小目标做预测。修改yolov5s.yaml中的 head 部分在高分辨率特征图旁增加一个检测头head: # 原有 P3 检测层之前的浅层特征开始 - [-1, 1, Conv, [128, 3, 2]] - [[-1, 6], 1, Concat, [1]] - [-1, 1, C3, [128, False]] # 新增加 P2 输出将上一层上采样到 stride4 与浅层特征拼接 - [-1, 1, nn.Upsample, [None, 2, nearest]] - [[-1, 2], 1, Concat, [1]] - [-1, 1, C3, [128, False]] # 这里接一个输出通道为 128 的检测层 - [[-1, 10], 1, Detect, [nc, anchors]]逻辑说明nn.Upsample会把 40x40 的特征图上采样到 80x80再和主干里更早期的 80x80 特征拼接。这样检测头能看到更细的纹理和边缘信息对 10x10 像素的小病斑更友好。参数说明nc是你的类别数我在四类病害时设为 4anchors需要为 P2 层单独设计一组更小的先验框比如[4, 6, 5, 9, 8, 14]。这里有个经验新增检测层后原来三层 anchor 不够用建议训练前单独跑一次python utils/autoanchor.py --data smoke.yaml重新聚类否则 P2 层会因为没有匹配的先验框而大量报背景。后面第五节我会单独讲这个坑。4.3 用 Wise-IoU 替代 CIoU 损失YOLOv5 默认使用 CIoU 作为边框回归损失但它在小目标上有个问题当预测框和真实框重叠面积很小时宽高比惩罚项会过度放大导致梯度方向不稳定。烟叶病斑这种“小且密集”的目标常常出现几个病斑离得很近CIoU 会让模型为了拉低损失而把预测框压得过小。我目前常用的做法是把损失切换为 Wise-IoU v1它通过动态分配梯度权重让低质量样本的负面影响变小。修改方式是在utils/loss.py中找到class ComputeLoss里的bbox_iou调用替换成如下简化实现def wiou_loss(pred, target, weightNone, scale1.0): inter (pred target).sum(dim(-2, -1)) union (pred | target).sum(dim(-2, -1)) iou inter / union.clamp(min1e-6) # WiseIoU v1使用动态非单调聚焦系数 iou_mean iou.detach().mean() gradient_weight scale * (iou.detach() / iou_mean.clamp(min1e-6)) loss (1 - iou) * gradient_weight return loss.mean()逻辑说明Wise-IoU 的核心是给每个样本的动态权重gradient_weight。当一批样本整体质量高时它会降低高质量样本的权重让训练更关注中低质量样本当整体质量低时则会削弱异常样本的影响避免一张模糊病斑图把整个模型带偏。参数说明scale是尺度因子一般取 0.1如果 mAP 波动大可以调大到 0.3。这里的实现只保留了训练用梯度权重部分实际 YOLOv5 的 loss 还会组合分类损失和置信度损失修改时要保留原有加权比例不要为了追求双位数 mAP 提升而牺牲稳定性。另外NMS 后处理也可以顺手调一下烟叶病害密集场景中默认 NMS 阈值iou-t0.45会保留较多重叠框我建议改成 0.35减少同一病斑的重复检测。这属于“后处理”环节最容易改、也最容易被忽略的一个参数。4.4 改进后的训练 yaml 与启动命令汇总上面的改动训练配置里最关键的是这几个参数# smoke.yaml train: ./train.txt val: ./val.txt test: ./test.txt nc: 4 names: [black_shank, frog_eye, wildfire, target_spot]启动训练命令我一般这样写python train.py --data smoke.yaml --weights yolov5s.pt --cfg models/yolov5s_smoke.yaml --batch-size 16 --epochs 150 --img 640 --patience 15 --hyp data/hyps/hyp.scratch-low.yaml逻辑说明--cfg传入修改后的网络结构文件--hyp指定超参数。如果你改了 backbone 或 head不要把官方预训练权重直接硬塞进去因为最后一层 Detect 的输出通道数量变了官方权重不匹配会报错。我一般就是按“原权重 新结构”直接启动YOLOv5 会自动剔除不匹配的层让剩余层继续迁移学习。参数说明--patience 15表示连续 15 个 epoch 验证指标没有更好就早停。烟叶数据量不大150 epoch 通常在第 80 个左右收敛不需要跑满 300 epoch。--batch-size 16是在 12GB 显存上加 P2 层后比较稳的选择如果显存小优先减小--img到 480而不是调小 batch因为小目标检测比较依赖输入分辨率。5. 训练烟叶病害模型的常用坑现象、原因与排查顺序5.1 训练正常但验证集 mAP 一直是 0现象loss 在正常下降但每轮验证时 mAP0.5 始终是 0一张病斑都没检测出来。原因绝大多数情况是 label 的类别索引和smoke.yaml里的names顺序不一致。比如标注脚本里classes.index(black_shank)返回 2但训练配置里第 0 类才是 black_shank模型学了四个epoch就发现类别错乱。另一个常见原因是标注 txt 里出现空格或全角符号解析后目标坐标全变成负值网络把所有目标都当背景。解决先写一段检查脚本读取训练集中的 txt 和对应图片把框画回去人工看。另外直接看train_batch0.jpg和val_batch0.jpg两张图如果框的位置明显偏移说明转换或划分这一步出了问题不要继续训练。5.2 loss 曲线下降但验证 AP 忽高忽低现象训练 loss 平滑下降但验证集每一轮 mAP 波动超过 5 个点根本无法判断改进效果。原因烟叶病害数据本身病斑数量少、分布不均而验证集又太小。每轮验证的随机增强不同导致少量大目标被遮挡或模糊mAP 就大幅跳水。另一个容易被忽视的原因是 P2 层新增后正负样本比例严重失衡验证时大量小目标被当背景。解决把验证集比例从 10% 提到 15% 或 20%同时关闭验证时的 mosaic 增强YOLOv5 在验证阶段不会开 mosaic但确认一下--rect参数没被误开。另外对 P2 层单独跑一次autoanchoranchor 不匹配会让正样本分配极其不稳定AP 自然上下乱跳。5.3 训练后期出现 NaN loss现象训练到第 80 到 100 个 epochloss 突然打印成 nan之后模型权重全乱。原因最常见是学习率过大导致梯度过冲尤其是加了 Wise-IoU 后梯度权重动态变化默认的lr00.01在某些 batch 上会把损失推爆。另一种是 ground truth 里出现畸形的零面积框算 IoU 时除以 0。解决先在hyp.scratch-low.yaml里把lr0从 0.01 改到 0.005warmup_epochs加到 5。如果数据有零面积框用脚本清洗。还可以开启梯度裁剪在 train.py 的scaler.scale(loss).backward()之后增加一行torch.utils.clip_grad_norm_(model.parameters(), 10)防止梯度爆炸。5.4 加了注意力或 P2 层后效果反而不如原版现象按常规思路增加 CBAM 和 P2 层训练 150 个 epoch 后 mAP 反而比原版 YOLOv5s 低 2 到 3 个点。原因不要盲目堆模块。烟叶数据量不大CBAM 在通道数为 1024 的深层特征上引入了大量额外参数很容易过拟合。P2 层虽然对小目标友好但会让正样本数量激增且小目标 anchor 与小病斑的 IoU 匹配阈值过紧大量真实目标匹配不到 anchor反而被当成背景。解决优先做消融实验先只加 P2 层不动 loss再只换 Wise-IoU不动结构。每改一处就训练 100 个epoch 并对比验证集。如果加了 CBAM 后 class 0 的 AP 涨了但整体下降考虑把reduction从 16 调到 32增加压缩比来防止过拟合。5.5 改完网络后训练时间翻倍、显存不够现象原本 12GB 显存能跑 batch 16加了 P2 层和 CBAM 后 batch 8 都 OOM。原因P2 层的特征图分辨率是 160x160比 P3 层大了 4 倍显存占用CBAM 的空间注意力在 160x160 上也有额外开销。这是改进 YOLOv5 最实际的成本。解决优先在训练时把输入从 640 降为 512这会直接砍掉 P2 层近 36% 的显存。如果还想保输入分辨率就把 P2 层的卷积通道从 128 降到 64代价是 P2 的分层特征表达弱一些但通常还能保持 90% 的收益。另一个做法是开启 AMP 混合精度YOLOv5 默认用--amp确认device是 0 而不是 CPU。6. 从验证到部署用混淆矩阵和 ONNX 导出检验改进效果训练结束后先别急着部署我一般先跑一遍完整验证把confusion_matrix.png和PR_curve.png调出来看python val.py --data smoke.yaml --weights runs/train/exp/weights/best.pt --conf-thres 0.25 --iou-thres 0.45 --task val --img 640参数说明--task val只做验证不训练区分于--task test。--conf-thres 0.25和--iou-thres 0.45是部署时要复现的阈值必须和最终推理脚本保持一致否则你验证时报告的指标和现场效果对不上评审一问就露馅。看混淆矩阵时重点不是对角线而是“真实黑胫病被预测成野火病”那一格如果这类错分比例超过 10%说明两类病斑的颜色纹理太接近需要回看数据里有没有大量标注错误。确认效果满意后下一个步骤是导出 ONNX。这一步能同时检验网络是否有只适配 PyTorch 的层也方便现场服务用 OpenCV 或 ONNX Runtime 推理python export.py --weights runs/train/exp/weights/best.pt --include onnx --opset 12 --img 640大部分模块导出没问题但如果你用了自定义的 CBAMexport.py里会有一段torch.onnx.export的 trace 过程遇到adaptive_avg_pool2d一般能直接转换如果报Unsupported operator多半是空间注意力里torch.max(x, dim1)导出的维度处理问题可以在export.py中把这段改为torch.amax(x, dim1, keepdimTrue)。部署到边缘设备上时我的一个教训是不要一上来就上 TensorRT先把 ONNX 模型放到 ONNX Runtime 里跑通确认图像预处理和后处理和训练时完全一致。我见过有人把 BGR 转 RGB 的顺序弄反结果现场识别率暴跌到 20%我一个一个排查最后发现是cv2.cvtColor(img, cv2.COLOR_BGR2RGB)放在了letterbox之前导致缩放时颜色通道错位。另一个部署技巧如果现场只有 CPU可以导出 FP16 量化的 ONNX往往能带来 1.5 到 2 倍提速mAP 损失不到 1 个点。命令行加--half即可。如果目标是 Jetson 或树莓派建议再导出--include engine用 TensorRT 跑但前提是设备的 TensorRT 版本和本机一致否则会报plugin does not satisfy version这类问题。我做烟叶病害检测项目时最深的体会是很多人把精力花在刷 mAP 上却从不看混淆矩阵也不实际部署验证。结果做出来的模型在验证集上好看到了现场面对自然光、露水、尘土马上崩盘。现在我每完成一次改进都强制自己导出 ONNX 并在目标设备上跑 30 张实地照片再决定要不要继续加模块。这个习惯帮我提前排除过很多问题也希望帮到你。本文还有配套的精品资源点击获取
返回列表