当前位置: 首页 > news >正文

Yolo中的检测头

目录

一、检测头是什么

二、检测头的 “一行代码” 是什么意思?

三、检测头的输入

四、检测头的核心任务

4.1. 成员与构造参数

4.2. 中间通道的设定

4.3. 回归头cv2(每层一套)

4.4. 分类头cv3(每层一套)

4.5. DFL(Distribution Focal Loss)解码器

4.6. One-to-one 蒸馏/对齐(可选)

4.7. 举例说明

五、流程捋顺


在上一章中,我们学习了yolo的特征融合,并扩展到了多模态,现在我们要学习最后一块,检测头。

基于Yolo的图像识别中的特征融合-CSDN博客

一、检测头是什么

        在 Backbone 和 Neck 提取出三种尺度的特征后(P3,P4,P5),检测头负责:

把每个尺度的特征图映射成:
     (类别预测 + 位置预测 + 分布式回归)。

最终生成我们看到的检测框。

        Backbone/Neck 做的是“特征加工”。检测头做的是“框的计算”和“结果输出”。

二、检测头的 “一行代码” 是什么意思?

不知道代码怎么找参考:

笔记:对yolov8网络代码的学习_yolov8s 如何获取yolov8s.yaml配置-CSDN博客

基于Yolo的图像识别中的特征融合-CSDN博客

..\ultralytics\nn\modules\head.py

YAML 中通常是这一行:

[[P3, P4, P5], 1, Detect, [nc]]

或者 YOLOv11-RGBT 中:

[[26, 29, 32, 42, 45, 48], 1, DetectAux, [nc]]

含义很简单:

把这些多尺度特征输入检测头,生成最终检测结果。

但内部逻辑远远不止一句话。

三、检测头的输入

YOLOv8/YOLOv11 都使用三个(或更多)尺度:

FeatureShape(通常)
P3(B, 256, 80, 80)
P4(B, 512, 40, 40)
P5(B,1024,20,20)

每个尺度包含不同数量的语义:

  • P3 专注于小目标

  • P4 中目标

  • P5 大目标

检测头需要从这些特征图中为每个网格 cell 预测:

  1. 边界框

  2. 目标置信度

  3. 类别概率

  4. (YOLOv8/11)DFL 边界分布

四、检测头的核心任务

        结合Yolo官方给出的Detect组件进行分析:

4.1. 成员与构造参数
class Detect(nn.Module):dynamic = False      # 动态重建网格(导出相关)export  = Falseformat  = Noneend2end = Falsemax_det = 300shape   = Noneanchors = torch.empty(0)strides = torch.empty(0)legacy  = False      # 兼容旧版头(v3/v5/v8/v9)xyxy    = False      # 输出坐标格式(训练多用 xywh,推理通常转 xyxy)
def __init__(self, nc: int = 80, ch: tuple = ()):super().__init__()self.nc = nc                  # 类别数self.nl = len(ch)             # 检测层数(通常 3 个:P3/P4/P5)self.reg_max = 16             # DFL 的 bins(=每个边的离散分布长度)self.no = nc + self.reg_max * 4   # 每个点的输出通道:4*reg_max(回归分布) + nc(分类)self.stride = torch.zeros(self.nl)    # 各层步幅,build 时自动填
4.2. 中间通道的设定
c2 = max((16, ch[0] // 4, self.reg_max * 4))
c3 = max(ch[0], min(self.nc, 100))
  • 回归:保证足够容量去拟合 4*reg_max = 64 的分布输出。

  • 分类:给到不低于输入通道的中间维度(并封顶 100),兼顾表示力与轻量化。

4.3. 回归头cv2(每层一套)
self.cv2 = nn.ModuleList(nn.Sequential(Conv(x, c2, 3),          # 3×3Conv(c2, c2, 3),         # 3×3nn.Conv2d(c2, 4 * self.reg_max, 1)  # → 4*16=64 通道(DFL)) for x in ch
)

其中 c2 的关键作用是:

把 backbone/neck 原始特征映射到回归友好的语义空间

4.4. 分类头cv3(每层一套)

如果 legacy=True:用两层标准 Conv + 1×1 输出。

否则(默认、与你代码一致):Depthwise 轻量头(DWConv + 1×1 的两段堆叠)再接 1×1 输出到 nc。

self.cv3 = nn.ModuleList(nn.Sequential(nn.Sequential(DWConv(x,  x,  3), Conv(x,  c3, 1)),nn.Sequential(DWConv(c3, c3, 3), Conv(c3, c3, 1)),nn.Conv2d(c3, self.nc, 1),) for x in ch
)
4.5. DFL(Distribution Focal Loss)解码器
self.dfl = DFL(self.reg_max) if self.reg_max > 1 else nn.Identity()

训练/推理时把 64 通道的离散分布解成 4 个连续距离(l,t,r,b 或左/上/右/下偏移)

4.6. One-to-one 蒸馏/对齐(可选)
if self.end2end:self.one2one_cv2 = copy.deepcopy(self.cv2)self.one2one_cv3 = copy.deepcopy(self.cv3)
4.7. 举例说明

        我们来举例演示Detect是如何构造的,以官方yaml为例:

  - [[16, 19, 22], 1, Detect, [nc]] # Detect(P3, P4, P5)

        明确这条代码的含义:接收16,19,22层的输出,重复一次,调用Detect,传参类别数。

1) 先解析出 ch(各输入尺度的通道数)

        收集第 16、19、22 层的通道,形成元组:

ch = (channels_of_layer_16, channels_of_layer_19, channels_of_layer_22)
# 常见是 (256, 512, 1024)

2) 用 ncch 去初始化 Detect 

        实例化:             

head = Detect(nc=nc, ch=(256, 512, 1024))  # 以常见配置举例

3) Detect 内部如何用到 ch

   self.nl = len(ch) → 检测层数 = 3(P3/P4/P5)

        按 for x in ch 分别给每个尺度建一套回归头分类头    ​​​​

# 回归(DFL)头:x→c2→c2→(4*reg_max)
self.cv2 = ModuleList(Sequential(Conv(x, c2, 3), Conv(c2, c2, 3), Conv1x1(c2, 4*reg_max)) for x in ch
)
# 分类头(轻量DW):x→c3→c3→nc
self.cv3 = ModuleList(Sequential( DWConv+Conv(x→c3), DWConv+Conv(c3→c3), Conv1x1(c3→nc) ) for x in ch
)

  c2,c3的定义:

c2 = max(16, ch[0] // 4, 4*reg_max)   # 回归分支中间通道,常见等于 64
c3 = max(ch[0], min(nc, 100))         # 分类分支中间通道,常见等于 ch[0]

前向(训练与推理)的输入/输出是什么?

x = [x16, x19, x22]  # 分别对应 P3, P4, P5
# 形状类似:
# x16: (B, 256, 80, 80)
# x19: (B, 512, 40, 40)
# x22: (B, 1024, 20, 20)

训练时(不做解码,直接监督 DFL 与分类):

对每个尺度:

reg = cv2[i](x[i])  # (B, 4*reg_max, H_i, W_i)  # 64 通道(reg_max=16)
cls = cv3[i](x[i])  # (B, nc,        H_i, W_i)
out_i = cat([reg, cls], dim=1)  # (B, 64+nc, H_i, W_i)

返回列表:

[out_P3, out_P4, out_P5] 

给损失函数,损失里会对 64 通道做 DFL 分布监督,对 nc 通道做分类监督

推理时(拼接 → DFL 解码 → 网格/步幅解码):

展平并拼接三个尺度:

x_cat = cat([out_i.view(B, 64+nc, -1) for out_i in outs], dim=2)  # (B, 64+nc, N), N=Σ(H_i*W_i)

切分回归/分类:

box_distr = x_cat[:, :4*reg_max, :]   # (B, 64, N)
cls_logit = x_cat[:, 4*reg_max:, :]   # (B, nc, N) → sigmoid

DFL 把 64 通道分布解成 4 个连续距离:

box = self.dfl(box_distr)  # (B, 4, N)

结合网格中心与 stride 解到像素坐标(anchor-free):

centers, strides = make_anchors(...)
boxes = dist2bbox(centers, box) * strides   # (B, 4, N) → xywh/xyxy

转置为常用输出:

boxes -> (B, N, 4)
scores -> (B, N, nc)

五、流程捋顺

一句话总括:

YOLO =(预处理)→ Backbone 抽语义 → Neck 多尺度(可多模态)融合 → Detect 头:解耦回归(DFL) + 分类 → DFL 解码 + 网格/步幅还原 → 分数阈值 → NMS → 框。

为什么 Detect 头要先把框进行编码(DFL 分布),然后在推理时再解码(DFL→连续偏移→stride 网格还原)?这种先编码再解码的意义是什么?

YOLO 头部采用“编码 → 解码”的方式,是为了让训练阶段在分布空间学习更稳定的梯度与边界不确定性,而推理阶段则将学到的离散边界分布通过期望值解码为连续坐标,实现低噪声、高精度的边界预测。这种方式在多模态场景中尤其适合处理模态间对齐误差与弱边界问题。

分数阈值如何得出?

YOLOv5 输出三个分支:

objectness 作为“目标存在概率”,减少 FP。

分支输出意义
回归 head4个数box
objectness1个通道点是否包含目标
分类 headnc个通道类别概率
score = sigmoid(obj) * sigmoid(class[k])

YOLOv8(含 v11)采用了解耦头:

  • 回归 → 4×reg_max(DFL)

  • 分类 → nc 通道

分类分支不再输出 objectness

final score = 分类概率 = sigmoid(cls[k])

scores = cls_prob.max(dim=2)  # 每个点的最高类别概率

在多类别场景下,对于每个预测框:

  • 取所有类别 cls_prob 的最大值作为分数

  • 对应的类别索引作为 class_id

模型输出分类概率 = “每个点有某种类别的置信度”

人为设定 score_threshold

conf_thres = 0.25

因此:YOLO 的最终检测分数由分类分支的 sigmoid 输出决定,是对每个位置的类别置信度;在 YOLOv8/11 中不再使用单独的 objectness 通道。推理时根据 score ≥ threshold 过滤候选,配合 NMS 得到最终框。阈值由经验与任务需求确定,而模型输出的概率决定框的可信度排序。

模型预测出了 score(分类概率),score 阈值是怎么一步一步“筛选出最终框”的?score 是怎么跟 box 对应的?最后是怎么从一堆点变成一组框?

1. Detect 头输出了什么?

YOLOv8/11(默认流程)输出三件东西:

1)box_distr(4×16 的分布)

→ 通过 DFL 解码成 (l,t,r,b) 连续偏移量

2)cls_logit(nc 通道的 logits)

→ sigmoid → 分类概率 cls_prob

3)每个点都有一个 中心点坐标(由 grid 决定)

也就是说,在特征图上的 每个点都对应一个候选框

例如总共 8400 个点:

P3: 80×80 = 6400
P4: 40×40 = 1600
P5: 20×20 = 400
-------------------
总共 N = 6400 + 1600 + 400 = 8400 个潜在框

2.每个点产生一个候选框

对每个点执行:

(步骤 1)分类概率

cls_prob = sigmoid(cls_logit)  # (nc,)
score = max(cls_prob)          # 单个分数
class_id = argmax(cls_prob)    # 类别ID

(步骤2)坐标解码

把(l,t,r,b)通过:

x1 = cx - l*stride
y1 = cy - t*stride
x2 = cx + r*stride
y2 = cy + b*stride

得到候选框:

box = [x1, y1, x2, y2]

注意:每个点都对应一个候选框。
每个候选框都有一个 score 和一个 class_id。

符号全称解释
lleft从当前特征点中心往“左边界”方向的距离
ttop从当前特征点中心往“上边界”方向的距离
rright从当前特征点中心往“右边界”方向的距离
bbottom从当前特征点中心往“下边界”方向的距离

关键点:特征点中心 ≠ 目标中心

YOLO 的 anchor-free 预测是以 特征图上的每一个点(cell center) 为中心,而不是以目标中心为中心。

也就是说:

每个特征点都会产生一个框,框是相对于“该特征点的中心”预测的,而不是相对于物体中心。

3. 分数阈值:筛掉垃圾框(关键步骤!)

比如设定:

conf_thres = 0.25

对每个点的 score 判断:

if score >= 0.25:保留
else:丢弃

经过阈值筛选:

  • 原来是 8400 个候选框

  • 保留可能只有 ~300~1000 个(取决于场景)

  • 这一步就像“预过滤”,只保留可信度高的框让 NMS 处理。

4. 阈值后的框如何变成最终框?(NMS)

剩下的框会进入 NMS

1)按照 score 从大到小排序

2)从最大 score 框开始

3)过滤与其 IoU 过大的框

(比如 IoU > 0.5 则认为重叠太多,是重复检测)

4)保留不重叠的框

5)继续直到所有框处理完

最终得到:

少量高质量的、不重复的框。

5. 所以整个“分数阈值到框”的链条是

1)每个特征点预测

  • 一个框(通过 DFL 解码)

  • 一个 score(分类最高概率)

2)score 阈值筛选

  • score < 阈值 → 丢弃
  • score >= 阈值 → 保留

3)保留下来的框进入 NMS(去重)

4)NMS 输出最终框列表

[x1,y1,x2,y2, score, class_id]

流程如图所示:

http://www.dtcms.com/a/598957.html

相关文章:

  • 张家界建设局网站电话号码模板建站费用
  • 走进Linux的世界:进程状态
  • 毕设做网站需要买域名么网站建设方案之目标
  • 交叉熵损失函数(Cross-Entropy Loss)个人理解
  • 结对编程:提升编程效率与团队协作的最佳实践 | 如何通过结对编程实现高效协作和代码质量提升
  • 缓存优化(SpringCache、XXL-JOB)
  • 网站建设长期待摊费用个人网站的留言板怎么做
  • 优惠劵网站怎么做srm系统
  • Hugging Face Gated 模型下载全攻略:解决 401/403 和访问受限问题
  • 建筑行业网站模板ajax实现wordpress导航栏
  • 网站建设服务 杭州甜品店网页模板html
  • 状态机的实现方法--C语言版本
  • 网站做app开发有梦商城公司网站
  • 网站开发系统毕业综合实践报告电子版个人简历模板
  • 线代强化NO5|矩阵的运算法则|分块矩阵|逆矩阵|伴随矩阵|初等矩阵
  • 最新域名网站查询网站背景大小
  • 服装网站建设发展状况wordpress数据库访问慢
  • 大同市住房城乡建设网站扬州网站建设 天维
  • nat123做网站 查封编写网站的软件
  • 天津房地产网站建设福建联美建设集团有限公司网站
  • 简述网站建设有哪些步骤有什么网站可以做推广
  • C语言进阶:位操作
  • 建站网站苏州wordpress架设系统
  • wordpress短代码返回html石家庄网站seo优化
  • python合适做网站吗网站建设与维护面试
  • 什么是Hinge损失函数
  • 网站设计的趋势百度双站和响应式网站的区别
  • usrsctp之cookie
  • CC防护:抵御应用层攻击的精确防线
  • 如何自己制作链接内容泰安网站建设优化