ARTICLE DETAIL

资讯详情

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

YOLOv5小目标检测头改造:P2头与anchor调优实战

YOLOv5小目标检测头改造:P2头与anchor调优实战 1. 小目标怎么就漏了先把检测头这件事讲透做目标检测时间久一点的人大概都有过这种体验模型在验证集上mAP看着还行一放到实际场景里那些远处的小车、监控画面里的行人、工业质检里的细小划痕、无人机视角下的光伏板热斑要么框不出来要么框得歪歪扭扭。更气人的是你把置信度阈值往下调它又能冒出来几个但误检也跟着涨。这个现象背后八成不是你的数据不够多也不是训练轮数不够而是检测头本身的分辨率不够用了。这篇就围绕 YOLOv5 的小目标检测头改造来讲把为什么加、怎么加、加完怎么调、加完会不会掉点这几件事一次说清楚适合已经跑通过 YOLOv5 训练、想在小目标上再抠一抠精度的人。先把话说在前头加检测头不是万能药它本质上是拿计算量和推理速度去换小目标的召回。你得先判断自己手里的漏检到底是不是分辨率问题。我见过太多人一上来就改网络结果真正的病根是标注框太小被过滤掉了、图片被过度缩放、或者anchor尺寸和自家数据完全对不上。所以在动手改结构之前先做一件事——把漏检的图单独存出来看量一下这些目标的像素尺寸。如果大量目标在原图里都小于32x32像素COCO里 small object 的定义那这篇内容对你就很有用如果你的目标普遍是中等尺寸加头带来的收益可能还不够抵掉速度损失。1.1 检测头到底在干什么很多人对检测头这个词是模糊的觉得它就是个输出层。其实检测头做的事情可以拆成三件第一在特征图的每一个空间位置上预测若干个先验框anchor的偏移量第二判断这个位置有没有目标、是什么类别第三把这些预测还原回原图坐标。YOLOv5 用的是一种叫解耦头之前的一体式设计最近几个版本里 Detect 层会输出三个张量分别对应 box 回归、objectness/类别、以及 DFL 的分布参数。关键的约束在第一步和第二步之间的那个空间位置上。特征图有多大模型就只能在多少个格子上做预测。拿 640x640 的输入举例P3 层的特征图是 80x80stride 是 8意味着原图上每 8 个像素才有一个预测点。一个 16x16 像素的小目标落在这张特征图上可能只占两个格子模型要在这两个格子上同时完成定位和分类本身就很勉强。说白了检测头不是看不见小目标是它看的密度不够格子太粗小目标在两个格子之间就被平均掉了。1.2 YOLOv5三级头的能力边界YOLOv5 默认挂三个检测头分别接在 P3/8、P4/16、P5/32 上。stride 越小特征图越细对小目标越友好。P3 已经是三者中最细的了stride 8负责的最小 anchor 大概是 10x13 这种量级。问题在于一旦你的目标普遍小于这个尺度P3 也顶不住。我做水果分拣那会儿就有切身体会整幅图里果子挺大但果柄上的小黑点、表面霉斑这种瑕疵像素只有十几个P3 层根本抓不住模型只能靠纹理特征瞎猜召回率惨不忍睹。后来实测下来真正让这类小目标起死回生的就是把检测密度再加一档——在 P2/4 上加第四个头stride 变成 4特征图从 80x80 变成 160x160格子数量翻了四倍小目标终于有多几个落点了。但这里有个容易被忽略的代价P2 层是浅层特征感受野小语义信息弱。它擅长的是这是什么形状不擅长这是什么类别。所以加 P2 头不是简单复制一层卷积就完事还得靠 neck 把深层的语义信息往上传否则小目标定位准了、分类却错了。这也是为什么改造的重点其实在 neck 的路径设计上而不只是那一个 Detect。1.3 什么场景值得加第四个头不是所有项目都值得加。我一般用下面这张表快速判断主要看目标像素分布和业务对召回的要求判断维度建议加 P2 头不建议加 P2 头目标平均像素尺寸小于 32x32 占多数普遍大于 64x64漏检代价漏一个就出事故/丢订单漏检可接受抓大放小推理算力有 GPU 余量能接受降速端侧部署帧率吃紧图像分辨率高分辨率大图目标被摊薄输入本身很小数据集标注质量小目标标注完整小目标标注缺失严重提示如果你的小目标标注本身就漏标严重加头只会把漏标的那些目标也检出来反而拉低 precision。改造前先花半天时间抽查标注这一步比改网络重要。2. 加头之前anchor与stride必须先算清楚结构改错了还能重来anchor 没算对就是白改。我见过最典型的翻车辛辛苦苦把 P2 头加上去anchor 直接照搬了 P3 的尺寸结果新头预测的框全部偏大训练 loss 震荡最后还不如不加。为什么因为 stride 从 8 变成了 4同样的 anchor 在更细的特征图上对应的实际尺寸就小了一半anchor 和真实框严重不匹配正样本匹配质量一塌糊涂。2.1 stride与特征图的对应关系复算其实 stride 的计算特别简单就是把输入尺寸除以特征图尺寸。但很多人从来没自己算过直接抄配置抄错了也不知道。我们把常见输入的对应关系列出来方便你对照检查检测头输入 640 时特征图输入 1280 时特征图strideP2160 x 160320 x 3204P380 x 80160 x 1608P440 x 4080 x 8016P520 x 2040 x 4032从表里能看出输入翻倍特征图翻倍每个格子对应的原图像素不变所以小目标在高分辨率输入下相对更占格。这也是为什么在小目标场景里把 imgsz 从 640 提到 1024 或 1280往往比改结构见效更快、风险更低。当然代价是显存和推理时间成倍上升这个后面细说。2.2 新增P2头的anchor从哪来P2 头负责的是最小的一档目标anchor 自然也要跟着变小。YOLOv5 官方在 yolov5s-p2 这个配置里给出了参考值P2 的 anchor 大概是 5x6、8x14、15x11 这种量级三个 anchor 覆盖了比 P3 更小的尺度区间。你自己改的时候没必要硬抄最好基于自家数据重新聚类但官方值可以作为初始化的兜底。这里要强调一个细节anchor 是相对原图像素定义的不是相对特征图。很多人搞混把 5 写成了特征图上的 5 个格子那实际对应原图就是 5 乘 stride 4 等于 20 像素差了一个数量级。配 anchor 的时候脑子里要始终装着这是原图尺度。2.3 用k-means重新聚类自家数据集YOLOv5 自带 autoanchor 功能训练时会自动检查 anchor 是否适配不适配会重新聚类并打印建议。你也可以单独跑。核心逻辑就是把所有训练标注框的宽高读出来用 k-means 聚成 9 或 12 类几个头乘每头几个 anchor然后用遗传算法做变异优化最终输出一组 IoU 最优的 anchor。聚类的时候有几个坑要注意。第一标注特别小比如宽高小于 2 像素的框要过滤掉它们本身就是噪声会把聚类结果带偏。第二如果你的数据里既有超小目标又有超大目标k-means 会倾向于中间尺度这时候可以手动把聚类后的 anchor 按尺度排序把最小的几个分给 P2最大的分给 P5。第三改完 anchor 一定要重新初始化训练不要在不匹配的 anchor 上接着训那样前面学的全废。3. 改模型配置从yaml到Detect层的完整改造真正动手改了你会发现 YOLOv5 的结构配置全在一个 yaml 文件里改起来其实比想象中清爽。但它的坑在于每层的索引都是隐式的全靠列表顺序一旦你在中间插一层后面所有层的引用编号都得跟着改。这一节我把逐行改造的过程写清楚照着抄基本不会错。3.1 复制一份yolov5s-p2.yaml不要直接改原文件复制一份出来命名成 yolov5s-p2.yaml放到 models 目录下。然后确认你的训练脚本可以通过 --cfg models/yolov5s-p2.yaml 指定它。这么做的好处是出问题了能随时切回原配置对比也能做消融实验时随时切换不用来回改文件。3.2 backbone引出P2特征标准 YOLOv5s 的 backbone 里P2/4 其实是存在的只是没被 neck 引出来用。你看前两层第一个 Conv stride 2 得到 P1/2第二个 Conv stride 2 得到 P2/4紧接着才是 C3。要做的是在 P2/4 这层之后再加一个 C3 模块把特征强化一下再引出去否则浅层特征直接用效果不稳定。官方 yolov5s-p2 的做法是在第二个 Conv 后面接一个 C3形成 0-P1/2、1-P2/4、2-C3新增、3-P3/8 这样的结构。加这一层的理由是浅层特征虽然分辨率高但通道数少、感受野小直接拿去 Concat 语义太弱加个 C3 能既保持分辨率又提一点抽象能力实测对小目标分类准确率有正向帮助。3.3 neck的Concat索引逐行核对这是最容易翻车的地方。因为 backbone 多了一层后面所有层号都往后挪了一位neck 里引用的 backbone 层号也要跟着改否则 Concat 会对齐到错误的特征图要么通道不匹配直接报错要么尺寸对不上训练时崩。改完一定要打印模型结构确认。下面是我实际用的 head 部分配置Detect 挂在 21、24、27、30 四层上分别对应 P2 到 P5head: [[-1, 1, Conv, [512, 1, 1]], # 10 [-1, 1, nn.Upsample, [None, 2, nearest]], [[-1, 6], 1, Concat, [1]], # 12 cat backbone P4 [-1, 3, C3, [512, False]], # 13 [-1, 1, Conv, [256, 1, 1]], # 14 [-1, 1, nn.Upsample, [None, 2, nearest]], [[-1, 4], 1, Concat, [1]], # 16 cat backbone P3 [-1, 3, C3, [256, False]], # 17 P3/8-small [-1, 1, Conv, [128, 1, 1]], # 18 [-1, 1, nn.Upsample, [None, 2, nearest]], [[-1, 2], 1, Concat, [1]], # 20 cat backbone P2 [-1, 3, C3, [128, False]], # 21 P2/4-xsmall [-1, 1, Conv, [128, 3, 2]], # 22 [[-1, 17], 1, Concat, [1]], # 23 cat head P3 [-1, 3, C3, [256, False]], # 24 P3/8-small [-1, 1, Conv, [256, 3, 2]], # 25 [[-1, 13], 1, Concat, [1]], # 26 cat head P4 [-1, 3, C3, [512, False]], # 27 P4/16-medium [-1, 1, Conv, [512, 3, 2]], # 28 [[-1, 9], 1, Concat, [1]], # 29 cat head P5 [-1, 3, C3, [1024, False]], # 30 P5/32-large [[21, 24, 27, 30], 1, Detect, [nc, anchors]], # Detect(P2,P3,P4,P5) ]注意看第 20 层的 Concat它引用的是第 2 层新增的 C3而不是原来的第 1 层。如果你漏改了这里模型要么报通道数不对要么悄悄对齐到 P1/2 上训练能跑但效果奇差。这种能跑但不对的问题最难查所以改完 yaml 之后用脚本把模型 summary 打印出来逐层核对输出形状是不是 160、80、40、20 这个序列。3.4 Detect层anchor列表与nc参数Detect 层的参数里nc 是类别数anchors 是四组 anchor。加了 P2 之后 anchor 从三组变四组每组三个一共 12 个。官方配置里四组锚框按尺度从小到大排列P2 那组是最小的。你要做的是把之前聚类得到的 12 个 anchor 按尺度升序填进去保证最小的三个对应 P2。这里顺序填错等于 anchor 和 stride 全错配训练会非常不稳定。3.5 验证模型结构是否搭对改完别急着上数据跑先用随机张量做一次前向。确认三件事一是 Detect 输出确实是四组而不是三组二是每组特征图尺寸符合 stride 关系三是参数量和官方 p2 版本量级接近。参数量如果有明显偏差比如多了几百万说明某处通道数写错了比如 C3 的通道或 Conv 的输入输出没对齐。4. 训练调参让小目标头真正起作用结构改对只是拿到了入场券训练策略跟不上新头照样学不好。这里我踩过的坑最多逐条讲。4.1 显存与batch size的现实取舍加了 P2 头中间特征图最大到 160x160、通道 128 甚至更多激活内存直接涨一大截。同样的输入尺寸和 batch size显存占用可能涨 30% 到 50%。我的建议是先降 batch别急着降输入尺寸因为小目标本来就依赖分辨率。实在显存不够可以用梯度累积模拟大 batch或者开 amp 混合精度省下来的显存比降分辨率划算。如果还是爆再考虑把输入从 640 降到 512但要做好精度回退的心理准备。4.2 学习率和warmup的调整新增的层是随机初始化的和预训练权重不在一个成熟度上。直接拿原学习率开训新层容易震荡甚至把整个网络带偏。我的做法是把 warmup 轮数适当延长让新增层先慢慢追上来同时可以考虑对 backbone 冻结几轮只训 neck 和 head等 loss 稳住了再解冻全量微调。这个策略在小数据集上尤其管用能明显减少前几轮的 loss 尖峰。4.3 数据增强对小目标的影响这里要特别提醒一点Mosaic 和 MixUp 这类增强对小目标是把双刃剑。Mosaic 把四张图拼一起实际上做了缩小原本就不大的目标变得更小如果 P2 头不够强反而增加学习难度。但反过来Mosaic 也让模型见到更多小目标的组合泛化更好。我的经验是加头初期可以先把 mosaic 的概率调低一点等新头学得像样了再调回默认值。Copy-Paste 增强对小目标特别有效把标注好的小目标裁剪出来贴到其他图上能显著增加小目标样本数量性价比很高。4.4 损失权重与正样本匹配YOLOv5 的损失里box、cls、dfl 三部分的权重是可以调的。加 P2 头之后小目标的正样本数变多了如果 cls 权重不变分类分支可能学得慢。我在具体项目里试过把 box 损失权重适当加大配合标签分配的宽严参数调整让小目标更容易被分到正样本。但要注意别调过头不然小目标的框会过拟合中大型目标开始漂。这块没有万能值建议用消融实验一格一格试记录每次的 mAP_small 变化。注意改损失权重比改结构更敏感一次只动一个参数动完至少跑够 50 个 epoch 再判断不然你看到的只是随机波动。5. 常见问题与排查实录底下这些是我和朋友在实际项目里反复遇到的问题整理成速查表遇到先对号入座。现象大概率原因处理方向训练直接报通道数不匹配Concat 引用的层号没跟着改核对 yaml 层索引能跑但 mAP 奇低anchor 与 stride 错配重新聚类 anchor新头完全不收敛新层未进 warmup学习率过大延长 warmup冻结 backbone小目标召回涨了但误检暴涨正样本分配过松置信度阈值低收紧匹配提高 conf训练 loss 正常验证掉点过拟合小目标样本不足加 copy-paste增强正则推理速度断崖式下降P2 头计算量大输入又高权衡输入尺寸与头数5.1 训练报错速查通道不匹配最常见基本就是 yaml 索引问题照着上面 3.3 逐行核对。另一个高频错误是 anchor 数量对不上四组头必须 12 个 anchor写成 9 个会直接报错。还有人把 nc 忘了改改成自己的类别数后忘了同步 anchor结果训练集类别数对但检测框乱飞。5.2 加了头反而掉点怎么办先别慌这种情况多半不是结构问题而是训练策略没跟上。我一般按这个顺序排查先看是不是新头把整体 loss 带高了导致老的头也学不好如果是就冻结 backbone 再训再看小目标相关的数据是不是本身就少anchor 聚类是不是偏向了大目标最后才怀疑结构。实测里八成加头掉点都能靠重训和调 anchor 解决真正需要回退结构的很少。5.3 推理速度和部署侧的代价加 P2 头参数量大概会多出零点几 MFLOPs 涨得更明显。在桌面 GPU 上可能只是每帧多一两毫秒但在 Jetson 这类边缘设备上可能就是能不能跑到实时的问题。部署到 TensorRT 的时候P2 那层大特征图对显存带宽压力不小如果帧率卡死可以考虑只保留 P2 头做两阶段筛选或者用输入缩放配合。我的建议是上线前一定在目标硬件上实测帧率别拿 3090 的成绩去估边缘设备。6. 效果对比与消融怎么判断这刀加得值改造完总得有个说法。判断标准不能只看总 mAP小目标的收益往往被大中目标的高分掩盖要看 AP_small 这个分项。如果数据集没有 COCO 那种按尺度分档的评测可以自己在验证集里按目标像素面积分桶单独统计小目标桶的召回和精度。6.1 指标怎么看mAP0.5 是最常用的但对小目标不够敏感。我一般同时看三个数整体 mAP、小目标子集 mAP、以及固定误检率下的召回。前两个看趋势第三个看实际业务能不能用。很多项目最后卡的不是精度是误检率加头之后小目标召回涨了但如果误检也涨业务上未必接受得根据场景权衡。6.2 一组对比参考下面是我在类似水果瑕疵检测任务上的经验数据仅作趋势参考你的绝对值肯定不一样配置整体 mAP0.5小目标 mAP单帧耗时相对yolov5s 原版0.860.411.0xP2 头未调 anchor0.840.441.28xP2 头重聚类 anchor0.880.531.28xP2 头anchor增强调优0.890.571.30x能看出来光加头不调 anchor 甚至整体掉点把 anchor 和增强补上之后小目标才真正起飞。这也印证了前面反复说的一句话结构只是基础配套调优才是收益来源。6.3 什么情况下别加如果你的目标本来就不小或者部署硬件算力锁死老老实实用输入分辨率提升或者更强的主干更划算。加 P2 头适合的是小目标确实多、漏检代价大、算力有余量这三个条件同时满足的场景。我个人的体会是先试提高输入尺寸再试数据增强最后才动结构这个顺序能帮你用最小代价拿到大部分收益。最后分享一个小技巧把加了 P2 头和没加 P2 头的两个模型在同一批困难样本上跑一遍把两者预测框画到同一张图上对比。哪些目标只有新模型检出来了、哪些被检歪了一眼就清楚。这种可视化比盯着 mAP 数字看有用得多也能帮你判断下一步该调哪里。这个内容后续还可以往轻量化方向扩展比如用深度可分离卷积替换 P2 头的标准卷积在保持小目标精度的同时把速度损失压回去感兴趣的话可以自己动手试试。
返回列表