news 2026/9/19 1:32:08

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
YOLOv5小目标检测头改造:P2头与anchor调优实战

1. 小目标怎么就漏了:先把检测头这件事讲透

做目标检测时间久一点的人,大概都有过这种体验:模型在验证集上mAP看着还行,一放到实际场景里,那些远处的小车、监控画面里的行人、工业质检里的细小划痕、无人机视角下的光伏板热斑,要么框不出来,要么框得歪歪扭扭。更气人的是,你把置信度阈值往下调,它又能冒出来几个,但误检也跟着涨。这个现象背后,八成不是你的数据不够多,也不是训练轮数不够,而是检测头本身的分辨率不够用了。这篇就围绕 YOLOv5 的小目标检测头改造来讲,把为什么加、怎么加、加完怎么调、加完会不会掉点这几件事一次说清楚,适合已经跑通过 YOLOv5 训练、想在小目标上再抠一抠精度的人。

先把话说在前头:加检测头不是万能药,它本质上是拿计算量和推理速度去换小目标的召回。你得先判断自己手里的漏检到底是不是分辨率问题。我见过太多人一上来就改网络,结果真正的病根是标注框太小被过滤掉了、图片被过度缩放、或者anchor尺寸和自家数据完全对不上。所以在动手改结构之前,先做一件事——把漏检的图单独存出来看,量一下这些目标的像素尺寸。如果大量目标在原图里都小于32x32像素(COCO里 small object 的定义),那这篇内容对你就很有用;如果你的目标普遍是中等尺寸,加头带来的收益可能还不够抵掉速度损失。

1.1 检测头到底在干什么

很多人对"检测头"这个词是模糊的,觉得它就是个输出层。其实检测头做的事情可以拆成三件:第一,在特征图的每一个空间位置上,预测若干个先验框(anchor)的偏移量;第二,判断这个位置有没有目标、是什么类别;第三,把这些预测还原回原图坐标。YOLOv5 用的是一种叫解耦头之前的一体式设计,最近几个版本里 Detect 层会输出三个张量,分别对应 box 回归、objectness/类别、以及 DFL 的分布参数。

关键的约束在第一步和第二步之间的那个"空间位置"上。特征图有多大,模型就只能在多少个格子上做预测。拿 640x640 的输入举例,P3 层的特征图是 80x80,stride 是 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 时特征图stride
P2160 x 160320 x 3204
P380 x 80160 x 1608
P440 x 4080 x 8016
P520 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 到 P5:

head: [[-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 头,参数量大概会多出零点几 M,FLOPs 涨得更明显。在桌面 GPU 上可能只是每帧多一两毫秒,但在 Jetson 这类边缘设备上,可能就是能不能跑到实时的问题。部署到 TensorRT 的时候,P2 那层大特征图对显存带宽压力不小,如果帧率卡死,可以考虑只保留 P2 头做两阶段筛选,或者用输入缩放配合。我的建议是:上线前一定在目标硬件上实测帧率,别拿 3090 的成绩去估边缘设备。

6. 效果对比与消融:怎么判断这刀加得值

改造完总得有个说法。判断标准不能只看总 mAP,小目标的收益往往被大中目标的高分掩盖,要看 AP_small 这个分项。如果数据集没有 COCO 那种按尺度分档的评测,可以自己在验证集里按目标像素面积分桶,单独统计小目标桶的召回和精度。

6.1 指标怎么看

mAP@0.5 是最常用的,但对小目标不够敏感。我一般同时看三个数:整体 mAP、小目标子集 mAP、以及固定误检率下的召回。前两个看趋势,第三个看实际业务能不能用。很多项目最后卡的不是精度,是误检率,加头之后小目标召回涨了,但如果误检也涨,业务上未必接受,得根据场景权衡。

6.2 一组对比参考

下面是我在类似水果瑕疵检测任务上的经验数据,仅作趋势参考,你的绝对值肯定不一样:

配置整体 mAP@0.5小目标 mAP单帧耗时(相对)
yolov5s 原版0.860.411.0x
+P2 头,未调 anchor0.840.441.28x
+P2 头,重聚类 anchor0.880.531.28x
+P2 头,anchor+增强调优0.890.571.30x

能看出来,光加头不调 anchor 甚至整体掉点,把 anchor 和增强补上之后小目标才真正起飞。这也印证了前面反复说的一句话:结构只是基础,配套调优才是收益来源。

6.3 什么情况下别加

如果你的目标本来就不小,或者部署硬件算力锁死,老老实实用输入分辨率提升或者更强的主干更划算。加 P2 头适合的是"小目标确实多、漏检代价大、算力有余量"这三个条件同时满足的场景。我个人的体会是,先试提高输入尺寸,再试数据增强,最后才动结构,这个顺序能帮你用最小代价拿到大部分收益。

最后分享一个小技巧:把加了 P2 头和没加 P2 头的两个模型在同一批困难样本上跑一遍,把两者预测框画到同一张图上对比。哪些目标只有新模型检出来了、哪些被检歪了,一眼就清楚。这种可视化比盯着 mAP 数字看有用得多,也能帮你判断下一步该调哪里。这个内容后续还可以往轻量化方向扩展,比如用深度可分离卷积替换 P2 头的标准卷积,在保持小目标精度的同时把速度损失压回去,感兴趣的话可以自己动手试试。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 1:29:20

Ubuntu 22.04.4 配置 UE5.3.2 开发环境实战指南

1. 为什么在Ubuntu上配UE5开发环境不是“折腾”,而是刚需最近三个月,我帮六位做独立游戏的同行朋友远程搭过UE5开发环境,其中四位明确说:“之前在Windows上用得挺顺,但一换到Ubuntu就卡在编译环节,要么Clan…

作者头像 李华
网站建设 2026/9/19 1:28:29

ESP32-P4 USB Device模式开发实战:从读卡器到工业网关

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 1:28:22

/fast 报 requires native?Claude Code 在 TaoToken 通道下先 claude install

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 1:25:00

React核心机制深入:render函数、虚拟DOM与Fiber架构

1. 组件到底是怎么跑起来的:render函数的那些事先说一个很多初学React的同学都会卡住的问题:为什么我们的组件每次都返回一个新的render函数?这个问题其实不是React特有的,而是React整个运行机制的基石。我自己带过不少新人&#…

作者头像 李华