YOLOv8 Detect层输出维度修改全指南:从源码原理到部署实战
2026/9/15 15:41:22 网站建设 项目流程

我接触YOLOv8也快两年了,中间改过不少次它的Detect层输出维度。这东西说简单确实简单,改一个数字就完事;但说复杂也真复杂,改完之后loss不收敛、导出报错、部署维度对不上,每一个坑我都踩过。所以这次干脆把Detect层的输出维度改动从头到尾捋一遍,从源码级原理到实操改法,再到部署端的注意事项,一次说透。

先说结论:Detect层是整个YOLOv8模型最后出口,所有backbone和neck的特征都要汇到这一层,最终转成预测结果。你改动输出维度,本质是在改这个“出口的形状”。如果只是做分类数变化,那确实改一行配置就行;但如果你想改输出结构本身,比如加关键点分支或者旋转框参数,那就得动手改head.py的Detect类了。

接下来我按自己的理解,从Detect层的核心机制讲起,再一步步拆开输出维度的计算逻辑,最后给几个实战改动案例和问题排查。

1. 先把Detect层的结构彻底拆开

1.1 Detect层在YOLOv8里到底干了什么

YOLOv8的Detect层,放在整个模型的最后,接收来自neck的P3、P4、P5三层特征图。这三层特征图分别对应输入图像的三个不同尺度,比如输入640x640时,P3是80x80,P4是40x40,P5是20x20。Detect层就是对这些特征图做最终预测,输出目标的类别、位置和置信度。

要理解输出维度的来源,得看Detect类的核心代码结构。Ultralytics官方源码在ultralytics/nn/modules/head.py里面,关键部分是forward_inference这两个函数:

class Detect(nn.Module): def __init__(self, nc=80, ch=()): super().__init__() self.nc = nc self.nl = len(ch) self.reg_max = 16 self.no = nc + self.reg_max * 4 self.stride = torch.zeros(self.nl) c2, c3 = max((16, ch[0] // 4, self.reg_max * 4)), max(ch[0], min(self.nc, 100)) self.cv2 = nn.ModuleList( nn.Sequential(Conv(x, c2, 3), Conv(c2, c2, 3), nn.Conv2d(c2, 4 * self.reg_max, 1)) for x in ch ) self.cv3 = nn.ModuleList( nn.Sequential(Conv(x, c3, 3), Conv(c3, c3, 3), nn.Conv2d(c3, self.nc, 1)) for x in ch ) self.dfl = DFL(self.reg_max) if self.reg_max > 1 else nn.Identity()

这段代码把输出分成了两个分支:

  • cv2分支:负责预测边界框的回归参数,输出通道是4 * reg_max,也就是4个距离值,每个距离值用reg_max个bin表示。默认reg_max=16,所以这一路通道数是64。
  • cv3分支:负责预测类别分数,输出通道数是nc,也就是类别数。默认COCO是80。

所以Detect层最终每个位置上的输出通道是nc + 4 * reg_max,也就是80 + 64 = 144。这就是大家经常看到的YOLOv8输出张量里的144这个数字的来历。

1.2 训练状态和推理状态完全不是一回事

很多新手在改Detect层的时候,最容易困惑的就是:为什么明明改了输出维度,但打印模型结构的时候看到的输出shape和自己想的不一样?这是因为Detect层在训练状态和推理状态走的是完全不同的逻辑。

forward函数的实现:

def forward(self, x): shape = x[0].shape for i in range(self.nl): x[i] = torch.cat((self.cv2[i](x[i]), self.cv3[i](x[i])), 1) if self.training: return x elif self.dynamic or self.shape != shape: self.anchors, self.strides = (x.transpose(0, 1) for x in make_anchors(x, self.stride, 0.5)) self.shape = shape x_cat = torch.cat([xi.view(shape[0], self.no, -1) for xi in x], 2) box, cls = x_cat.split((self.reg_max * 4, self.nc), 1) dbox = self.decode_bboxes(self.dfl(box), self.anchors.unsqueeze(0)) * self.strides y = torch.cat((dbox, cls.sigmoid()), 1) return y

关键区别就在这里:训练时返回的是一个list,里面每个元素是某一层特征图的原始输出,shape是(batch, 64 + nc, h, w),这个结果直接喂给loss函数做计算。推理时,才会把三个尺度的结果全部拼接到一起,变成(batch, 4 + nc, total_anchors)这样的格式,然后做DFL解码和距离转坐标。

这个区别直接影响了你在改输出维度时的判断。比如你改了nc之后,发现训练能跑但导出onnx后output shape不对,多半就是因为没理解这条路径差异。

1.3 DFL结构才是输出维度的隐藏主角

说到detect层的输出维度,绝大多数人第一反应是加类别数或者减类别数,但很少人注意到reg_max这个参数。在我们做自定义改动的时候,这个参数往往才是真正需要动的那个。

reg_max是Distribution Focal Loss里的回归区间数,YOLOv8默认取16,意味着边界框的每条边用16个概率值来表示。DFL做的就是把这16个值做一个softmax加权求和,得到一个精确的偏移量:

class DFL(nn.Module): def __init__(self, c1=16): super().__init__() self.conv = nn.Conv2d(c1, 1, 1, bias=False).requires_grad_(False) x = torch.arange(c1, dtype=torch.float) self.conv.weight.data[:] = nn.Parameter(x.view(1, c1, 1, 1)) self.c1 = c1 def forward(self, x): b, c, a = x.shape return self.conv(x.view(b, 4, self.c1, a).transpose(2, 1).softmax(1)).view(b, 4, a)

这个DFL本身不需要训练参数,它的conv权重是固定好的0到15的序列值。所以如果你想把reg_max改成别的值,比如想压缩模型尺寸改成8,输出维度就会变成nc + 4 * 8 = n c + 32,这个改动会直接影响检测精度和模型大小,而且改动逻辑完全和类别数无关。

我自己的经验是:改reg_max最难受的地方不是Detect层本身,而是后续的loss计算和anchor生成。YOLOv8的loss里用到了Dist2BboxLoss,它把DFL输出的4个距离通过dist2bbox函数转成真实的xyxy坐标时,会依赖这个reg_max值。所以只改Detect层的输出维度、不考虑配套的decode逻辑,训练时经常会遇到loss为nan或者收敛极慢的情况。

2. 输出维度的完整计算链路

2.1 从输入到输出的Shape变化全过程

要彻底掌握输出维度改动,得先把整条计算链路捋清楚。以输入一张640x640x3的图片,训练COCO检测模型为例,输出维度的演变过程是这样的:

输入图片: (1, 3, 640, 640) 经过backbone和neck后,得到三路特征: - P3: (1, 256, 80, 80) - P4: (1, 512, 40, 40) - P5: (1, 1024, 20, 20) 进入Detect层: - cv2分支输出回归参数: (1, 64, hi, wi) - cv3分支输出类别分数: (1, 80, hi, wi) - concat后: (1, 144, hi, wi) 推理时拼接三个尺度的anchor: - 80x80: 6400个anchor - 40x40: 1600个anchor - 20x20: 400个anchor - 总数: 8400个anchor 最终输出shape: (1, 84, 8400)

注意这个最终输出两个维度的含义:84 = 4 + 80,分别是4个坐标值(xyxy)和80个类别分数;8400是所有anchor数量总和。这就是很多人在用ONNX导出或者部署到RK3588这类设备上时看到的(1, 84, 8400)的来历。

这里我特意强调一下,做部署端适配的时候,84这个数字和8400这个数字是不能写死的。8400和输入分辨率挂钩,改了输入大小它会变;84和nc挂钩,改了类别数它会变。所以当你在RK3588上做推理程序时,应该从模型的输入输出结构动态解析这两个维度,而不是写死。

2.2 n c、reg_max和输出维度三者的数学关系

输出维度 = 4 + nc,这里的4是解码后的坐标维度,因为在推理时DFL解码和距离转坐标已经提前完成了。但这个4和reg_max之间是有转换关系的:

  • 训练时的原始输出维度 = nc + 4 * reg_max
  • 推理时的最终输出维度 = 4 + nc

这里的转换关系很重要,我见过很多人在看模型结构时,发现Detect层输出的第一个维度是144(默认情况,训练时)或者84(推理时),然后就开始混乱了。其实两者的关系就是:推理时把4 * reg_max这一块通过DFL和距离解码变成了4个坐标值。

再展开说,YOLOv8的边界框回归方式从YOLOv5的anchor-based变成了anchor-free + DFL。YOLOv5的输出格式是(batch, 5 + nc, num_anchors),其中5是x、y、w、h、conf。YOLOv8去掉了objectness分支,变成了(batch, 4 + nc, num_anchors),因为DFL机制认为当前的分类分支已经隐含了目标存在的置信度信息,没必要再单独搞一个置信度打分。

这个改动在输出维度上的体现就是:YOLOv8比YOLOv5少了一个维度。我们在改动输出维度的时候,要时刻记得这个设计理念,不要尝试把objectness加回去,那样会让整个head结构偏离YOLOv8的设计初衷。

2.3 Detect层两个关键函数:make_anchors和dist2bbox

Detect层改动之后要想正常工作,还需要理解两个绑定的函数:make_anchorsdist2bbox。它们通常不是写在Detect类里面的,而是在训练loss或推理脚本里调用,但它们对输出维度的影响是决定性的。

make_anchors负责根据特征图的尺寸生成anchor点,每个特征图位置生成一个anchor。它返回的是归一化到特征图坐标系的中心点坐标。比如80x80的特征图,就生成80x80=6400个anchor点,每个点有x和y两个坐标,所以shape是(6400, 2)

dist2bbox则负责把DFL解码得到的4个距离值,加上anchor的中心点坐标,转换回预测框的xyxy格式。这个函数处理的是距离和坐标之间的数学关系:

def dist2bbox(distance, anchor_points, xywh=True, dim=-1): lt, rb = distance.chunk(2, dim) x1y1 = anchor_points - lt x2y2 = anchor_points + rb if xywh: c_xy = (x1y1 + x2y2) / 2 wh = x2y2 - x1y1 return torch.cat((c_xy, wh), dim) return torch.cat((x1y1, x2y2), dim)

这段代码的逻辑值得花几分钟看看:anchors表示当前特征图位置的中心坐标,lt表示左上角距离中心点的偏移距离,rb表示右下角距离中心点的偏移距离。所以anchor - lt得到左上角坐标,anchor + rb得到右下角坐标,两者合并就是xyxy格式的框。

理解了这两个函数,改动输出维度时的思路才能清晰。比如你要把输出坐标维度改成xywh格式,就不能只改head输出的张量形状,还要改dist2bbox的输出策略,以及后处理时读取坐标的方式。

3. 实操:如何正确修改Detect层输出维度

3.1 改类别数:最基础但不简单的改动

最经典的需求就是把官方预训练的80类模型改成自己的n类模型。这个过程看起来就是在yaml配置文件里把nc改成你的类别数,但在实际改动中,你还需要同步处理好训练数据、anchor策略、以及最后的识别效果验证。

以我实际把YOLOv8改成5类检测的经验为例:

ultralytics/cfg/models/v8/yolov8.yaml里面,核心内容是这样的:

nc: 80 scales: n: [0.33, 0.25, 2.0] s: [0.33, 0.50, 2.0] m: [0.67, 0.75, 1.5] l: [1.00, 1.00, 1.0] x: [1.00, 1.25, 1.0] backbone: - [-1, 1, Conv, [64, 3, 2]] ... head: - [-1, 1, nn.Upsample, [None, 2, "nearest"]] ... - [-1, 1, Detect, [nc]]

如果只是改类别数,把nc改成5就够了。但我的建议是不要只改yaml,最好在训练前打印一下模型的输出shape确认改动生效了:

from ultralytics import YOLO model = YOLO("yolov8n.yaml") model.train(data="my_dataset.yaml", epochs=100, imgsz=640)

如果训练过程中报出和维度相关的错误,通常都不是nc本身没改,而是数据集的labels文件里class id超出了新的nc范围。比如你把nc改成了5,但有一个标注文件的类别id写的是6,训练就会直接崩。

另外改nc之后建议用预训练权重做迁移学习。官方给的yolov8n.pt是基于COCO 80类训练的,它的检测头nc=80。如果你直接用新nc=5的模型结构去加载预训练权重,Ultralytics会自动跳过shape不匹配的Detect层参数,剩下的backbone和neck参数正常加载。实际操作下来,我发现这样训练比随机初始化收敛快非常多,因为backbone提取通用特征的能力已经被充分训练过了。我第一次改类别数的时候没经验,直接从零训练,跑了100个epoch效果还是稀烂,后来改成加载预训练权重做迁移,50个epoch就达到了之前的水平。

3.2 修改输出坐标维度:从xyxy到xywh的实战改造

再举一个实际项目里经常遇到的需求:有些部署框架或者上位机程序要求模型直接输出xywh格式的框信息,而YOLOv8默认输出xyxy格式。这时候就需要改Detect层的_inference逻辑。

dist2bbox函数里有个xywh参数,默认是True。但在Detect类的_inference里,调用方式是dist2bbox(dfl(box), self.anchors.unsqueeze(0), xywh=False),也就是说YOLOv8推理时默认输出xyxy格式。

如果你想输出xywh格式,改动其实很简单,把xywh=False改成xywh=True

y = torch.cat((dbox, cls.sigmoid()), 1) # dbox现在是(cx, cy, w, h)而不是(x1, y1, x2, y2)

但这里有个隐蔽的坑:改了推理输出格式之后,decode_bboxes的内部逻辑也要跟着调整,因为decode_bboxes调用dist2bbox时传的参数要和最终输出格式保持一致。如果你只改外层、不改内层,两个函数计算的框会不一致,但程序不会报错,这个问题排查起来极其恶心。

我实际改造时,是把Detect类的逻辑改成了这样:

class Detect(nn.Module): def __init__(self, nc=80, ch=(), xywh=False): super().__init__() self.xywh = xywh ... def forward(self, x): ... if self.training: return x # 推理时通过 self.xywh 控制输出格式 dbox = self.decode_bboxes(self.dfl(box), self.anchors.unsqueeze(0)) * self.strides y = torch.cat((dbox, cls.sigmoid()), 1) return y def decode_bboxes(self, bboxes, anchors): return dist2bbox(bboxes, anchors, xywh=self.xywh)

这样改完之后,你实例化模型时通过xywh=True就能控制输出为xywh格式,不用动内部逻辑。很多嵌入式部署或者上位机开发场景,拿到的是xywh格式的框可以直接用OpenCV的rectangle画框,不用再写坐标转换,省了不少事。

3.3 新增分支:修改Detect层以输出关键点检测结果

这是比改坐标格式复杂很多的改动。以YOLOv8-pose为例,它在Detect层的基础上增加了一个关键点分支,所以最终输出维度变成了4 + nc + 2 * kpt + kpt_conf。我当时第一次看yolov8-pose的head结构时,一度以为是自己把输出维度理解错了,后来仔细翻了源码才明白,它其实是在Detect基础上扩展了cv4分支。

YOLOv8-pose的Detect是独立的一个Pose类,核心结构如下:

class Pose(nn.Module): def __init__(self, nc=80, kpt_shape=(17, 3), ch=()): super().__init__() self.kpt_shape = kpt_shape self.nk = kpt_shape[0] # 关键点数量 self.kpt_dim = kpt_shape[1] # 每个关键点维度,一般是3,代表x、y、可见性 c4 = max(ch[0] // 4, self.nk * self.kpt_dim) self.cv4 = nn.ModuleList( nn.Sequential(Conv(x, c4, 3), Conv(c4, c4, 3), nn.Conv2d(c4, self.nk * self.kpt_dim, 1)) for x in ch )

关键点分支通过一个单独的cv4序列输出,通道数是nk * kpt_dim,默认17类的COCO关键点就是51个通道(17点 * 3维,其中3是x坐标、y坐标、可见性)。

推理时,Pose类concat了三个分支:

x[i] = torch.cat((self.cv2[i](x[i]), self.cv3[i](x[i]), self.cv4[i](x[i])), 1)

所以最终单个尺度的输出通道是4 * reg_max + nc + nk * kpt_dim

如果你想在自己的自定义任务里加一个别的分支,完全可以参考Pose类的写法,在Detect基础上加一个cvN系列。比如想加segmentation分支,就加一个输出通道为num_masks的卷积序列。加完别忘了把训练时的loss函数同步修改,不然新增分支没有对应的loss监督信号,训练时根本学不出有用的东西。

3.4 修改reg_max压缩输出维度的操作

最后说一个相对少见的改动:压缩reg_max,把每个框的回归输出维度从4*16=64降到更小,这样模型头部参数变少,推理速度能提升一些。

我在GTX 1660 Ti上做过对比实验,把reg_max从16改成8,类目不变的条件下,Detect层输出通道从144降到(8*4+80)=112,模型体积有小幅缩小,推理速度在batch=1、640分辨率下大概提升了8%-12%,但mAP50下降了大概1.5个百分点。这个trade-off对小模型应用场景还是值得考虑的。

具体改法,一行代码的事,直接把self.reg_max = 16改成self.reg_max = 8。但真正麻烦的是配套逻辑,你要把DFL初始化、输出维度计算的地方全部同步改:

# 修改后的Detect __init__ self.reg_max = 8 self.no = nc + self.reg_max * 4 # 这里自动变成 nc+32

然后DFL、make_anchorsloss.py里的集成回归相关逻辑全部都会通过self.reg_max自适应调整,因为源码本身是用reg_max变量来推导维度的,所以你只要保证reg_max这个值在前后端保持同步就行。我用下来感觉最需要注意的是,改完reg_max后如果加载预训练权重,Detect层的所有参数都会因为shape不匹配被跳过,整个head要从头训练。要是训练数据不够多,精度可能还不如原版大reg_max效果好,所以这个操作需要结合实际数据集量级来评估。

4. 改动输出维度时,训练和部署的连锁反应

4.1 loss.py里藏着输出维度的隐性依赖

改了Detect层输出维度,训练阶段报错最多的地方往往不是head本身,而是loss函数。YOLOv8的loss在ultralytics/utils/loss.py里,它的BboxLoss类和v8DetectionLoss类都直接依赖Detect层输出的通道数。

比如v8DetectionLoss的初始化:

class v8DetectionLoss: def __init__(self, model): device = next(model.parameters()).device h = model.args m = model.model[-1] # Detect() module self.bce = nn.BCEWithLogitsLoss(reduction="none") self.hyp = h self.stride = m.stride self.nc = m.nc self.no = m.nc + m.reg_max * 4 self.reg_max = m.reg_max self.device = device ...

注意这一行:self.no = m.nc + m.reg_max * 4,它是直接读取Detect层里的属性来初始化loss的。这也就是为什么只要你正确修改了Detect类里的nc或者reg_max,loss这边基本会自动适配。但如果你不是通过源码配置改的,而是直接在Detect类外面手动改了什么输出维度,又没同步改这里的no属性,那训练就一定会报shape mismatch

我自己遇到过这样的情况:为了做一个多任务模型,我在Detect层concat的时候额外拼了一个语义分割分支的输出,训练时loss计算完全正常,但eval的时候模型输出维度比之前宽了,程序直接崩了。排查了半天,发现是v8DetectionLoss里对输出切分的代码:

pred_distri, pred_scores = torch.cat([xi.view(shape[0], self.no, -1) for xi in x], 2).split( (self.reg_max * 4, self.nc), 1 )

这里用的是split((self.reg_max * 4, self.nc), 1),它假设Detect原始输出只包含回归+分类两部分。如果你在Detect里加了第三个分支,数据格式跟loss里预期的对不上,自然会出问题。所以对于复杂改动,loss部分也得跟着加逻辑,不能指望它自动适配。

4.2 导出ONNX和部署端的维度适配经验

训练完模型之后,很多人会在导出onnx这个环节卡住。YOLOv8官方导出命令非常简洁,一行搞定:

yolo export model=yolov8n.pt format=onnx opset=11

这里有个和输出维度强相关的细节:导出onnx时,模型会走推理态,不再经过loss逻辑,这时的输出shape就是(batch, 4 + nc, 8400)。如果你在自定义数据集上训练,nc发生了改变,导出的onnx第二个维度就会同步变成4 + 你的类别数

我遇到过最典型的情况是:训练阶段自己改了Detect层,训练损失曲线看上去正常,保存的pt文件也能正常预测,但导出成onnx之后维度就错了。原因主要是自定义改动没有走官方支持的修改路径,导致pt里保存的Detect层状态和onnx导出时代码走的分支不一致。

如果按官方的方式在yaml里调nc,是不会有这个问题的。但如果自己做了一些自定义分支改动,导出前必须先跑一遍推理模式检查输出shape是否符合预期:

import torch from ultralytics import YOLO model = YOLO("runs/train/exp/weights/best.pt") model.eval() dummy = torch.zeros(1, 3, 640, 640) with torch.no_grad(): output = model.model(dummy) print(output.shape) # 应该是 (1, 4 + nc, 8400)

如果输出维度和期待值不一致,千万别急着导出,先在pt层面排查清楚。部署端很多问题都是因为这个环节没检查到位,导致RK3588或者Jetson上跑的时候shape硬编码不对,推理直接报错或者返回空结果。

4.3 用Netron彻底看懂输出维度的变化

谁也不能保证自己记性一直在线,特别是模型结构改动多了之后,输出维度到底是多少,靠人脑记忆根本不靠谱。我的习惯是每次改动完输出维度之后,都会导出onnx然后拖进Netron看一眼模型结构。

Netron打开onnx文件后,找到最后的输出节点,直接就能看到输出的维度标注。比如YOLOv8n原版显示的是[batch, 84, 8400],如果你把nc改成了5,最后一个输出节点就会显示[batch, 9, 8400]。这里9 = 4 + 5,一眼就能确认改动是否正确。

在Netron里还有个好用的功能,点开Detect节点前面的卷积层,能具体看到每个分支的输出通道数。如果改了reg_max,你能从卷积层的权重维度上看到变化,这个对排查输出维度异常非常有帮助。我习惯在导出onnx之后,先拖进Netron确认一下,再进行下一步的部署工作,这已经形成了一个固定习惯。很多人嫌麻烦跳过这一步,结果最后在部署端反复调shape,浪费的时间比看一眼Netron多得多。

5. 常见问题排查与避坑记录

5.1 训练时报shape mismatch的常规检查流程

改动输出维度后,训练阶段最崩溃的就是task_aligned_assigner或者loss计算时报shape mismatch。这类报错通常长这样:

RuntimeError: The size of tensor a (5) must match the size of tensor b (80) at non-singleton dimension 0

出现这种错的时候,先别急着怀疑代码写错了,按下面这个顺序排查:

  • 第一,确认yaml配置文件里的nc写的是不是你目标类别数。
  • 第二,打印模型head部分的输出shape,确认Detect层实际输出的通道数是多少。
  • 第三,检查数据集label文件里的类别id值,有没有超过nc-1的范围。
  • 第四,如果用了预训练权重,确认加载时是否因为shape不匹配自动跳过了Detect层参数。
  • 第五,检查hpy参数里的loss权重配置,看有没有硬编码某个类别数量。

我遇到的最常见情况是第二个:Detect层实际输出已经是(batch, 9, 8400)了,但训练代码里还按某个历史缓存的shape去切分数据,新旧两套维度混在一起。这种问题难在它不是必现的,有时候跑几个step才崩,特别磨人。

5.2 后处理换维度踩过的坑

Detect层输出维度的变化,不只是在模型内部有影响,后处理逻辑里的非极大值抑制NMS处理也必须同步调整。YOLOv8源码自带的NMS在ultralytics/nn/autobackend.pyultralytics/utils/ops.py里都有涉及,它假设输入格式是(batch, 4 + nc, num_anchors)

比如你在Detect层输出里加了一个extra分支,导致输出变成(batch, 4 + nc + extra, num_anchors),后处理的NMS函数就会把extra这部分当成类别分数来处理,结果越弄越乱。

所以改动Detect层输出维度之后,一定要同步排查下游的对输出维度有硬编码假设的地方。我整理了一个自查清单,每次改动后滤一遍:

改动点需要同步调整的地方
改nc数据集label、后处理类别过滤逻辑、可视化标签映射
改reg_maxDetect类、loss里no计算、DFL初始化、导出后处理距离解码
改输出坐标格式dist2bbox参数、NMS输入坐标顺序、可视化函数、部署端IOU计算
新增分支Detect类的cvN分支、loss新增对应损失项、后处理拆分逻辑、部署端解析逻辑
压缩输出通道模型yaml缩放因子、输出通道计算、部署端内存估计

5.3 经验心得:小目标检测场景下输出维度的取舍

最后分享一个实际项目中的经验。之前做过一个安全帽佩戴检测项目,摄像头的俯视角能看到大量小目标,每个人头加上安全帽特征,在640x640的输入下往往只占十几个像素。一开始用默认的YOLOv8l(reg_max=16),检测率不够理想。后来尝试压缩reg_max到8来加速推理,结果发现小目标检测能力明显下降,漏检率高出不少。

复盘之后想明白了原因:reg_max=16意味着DFL用16个离散值来表示边界框的每条边,分布更细腻;压缩到8之后,偏移量的表达精度下降,对于小目标来说几个像素的偏移误差就足以让检测框偏移目标。所以如果做小目标检测,没必要为了省一点算力去动reg_max,得不偿失。

但如果做的是大目标检测,比如传送带上的包裹分拣,目标占据画面大部分区域,回归精度要求没那么高,那压缩reg_max就是个不错的提速手段。这也提醒我们:Detect层输出维度的改动,从来不只是数字大小的问题,它本质上是在精度、速度、复杂度之间做权衡。动手之前想清楚自己真正要什么,才能选对改法。

实际训练中还有一个容易被忽略的细节:修改输出维度之后,训练初期的anchor匹配策略也会受影响,学习率、epoch的设定最好重新评估。我一般会把初始学习率降低一点,让head部分自适应新维度的过程平稳一些,等loss过了前几十个step的波动期之后再加回去。这虽说是训练策略上的微调,但对于刚改动完输出维度、模型还没完全稳定下来的阶段,帮助还是很大的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询