ARTICLE DETAIL

资讯详情

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

YOLOv11多任务联合训练实战:检测+分割+计数一次搞定

YOLOv11多任务联合训练实战:检测+分割+计数一次搞定 简介面向计算机视觉开发者与深度学习研究者的完整技术参考文档围绕YOLOv11构建检测、分割、计数多任务联合训练框架从网络结构创新、特征共享、损失函数设计到数据集处理与实验评估均有系统讲解并附智能交通、工业质检、安防监控等典型落地案例适合需要提升实时视觉任务效率的读者。资源为单个PDF文件共48页包体约2.2MB支持目录跳转与大纲定位便于章节式查阅。已有123人学习。文档内容覆盖共享特征提取层、任务分支设计、多任务损失组合、训练策略与调优方法并对mAP、mIoU、MAE等指标进行对比分析可直接作为多任务方案设计与实验规划的参考。1. 一份 YOLOv11 多任务方案为什么值得你放下手上的三段式流水线做工业质检或者车流统计的人多半经历过这样的组合拳先跑一个 YOLOv11 检测目标再切一个语义分割分支抠边缘最后写一段计数逻辑对着检测框数数。三个模型串起来又慢又难维护而把这几个任务塞进一个多任务框架让同一个模型同时输出检测框、分割掩膜和计数结果省显存、省延迟特征还能互相补。这篇笔记就是把“检测 分割 计数”的联合训练方案按工程落地的思路拆开讲网络怎么改、损失怎么配、参数怎么调、坑在哪。适合正在做自动易拉罐计数、硬币检测、零件缺陷定位这类目标检测和实例分割落地的工程师也适合想把实验室模型变成一条能跑的推理链的人。2. YOLOv11 网络结构选型一个 backbone 分出三个头到底动了哪些地方2.1 先拆任务计数不是第四个分类头它是两条实现路线的取舍多任务里最容易聊崩的点就是“计数”。很多人把计数理解成模型多输出一个数字于是直接在设计里加了一个回归头结果训练出来和检测头互相打架。我通常先把计数拆成两条路线再决定要不要加头。第一条是检测后计数NMS 之后按类别做统计或者对检测框中心点做虚拟线穿越计数。它的优点是模型不需要额外学任何东西检测框的数量本身就是计数结果完全可解释缺点是密集遮挡场景下检测召回率一掉计数必然偏少而且框重叠时容易重复计数。第二条是密度图回归在模型里额外挂一个输出通道输出一张单通道密度图每个目标中心附近铺一个高斯峰推理时对整张密度图求和就是总数。它天然适合人群、细胞这类高密度场景对遮挡更鲁棒但代价是丢失了单个目标的坐标信息没法回答“第几个目标是什么”。在联合训练方案里我一般这样取舍做零件、硬币、易拉罐这类刚体计数用检测后计数模型结构不加额外的头做细胞、人群这类非刚体密集计数就在分割分支的同一层特征上再接一个极小的密度图头。这两种都算“同时实现检测 分割 计数”但网络改法和损失配比完全不同先想清楚场景再动手。2.2 在 YOLOv11 上挂分割头和计数头的常见改法C3k2、C2PSA、anchor-free 三个层面YOLOv11 相比 v8主干里的 C3k2 模块替换了 C3内部用更小的卷积核分组堆叠整体计算量降了特征表达能力反而够用C2PSA 是一个带自注意力结构的模块通常放在主干最深层用来提升对大目标的全局感知。这两个改动对于多任务来说是友好的——分割和计数都依赖边界细节而 C3k2 的分组卷积让浅层特征更碎反而容易保留边缘信息所以不需要额外加深主干。检测 分割的挂法延续 YOLO 系列成熟做法neck 的 PAN 结构在 P3、P4、P5 三层输出不同尺度特征检测头沿用 anchor-free DFL 的 Detect 头分割头则从 P3 层引出两条支路一条生成 mask prototypes另一条生成 mask coefficients两者做矩阵乘法再上采样得到实例掩膜。很多人以为 YOLOv11 的分割头是直接在检测头上加卷积实际上是“原型 系数”的轻量解码mask 分辨率一般固定在 160×160不是贴着输入图全分辨率解码。计数头如果要加我不会让它直接从 backbone 引出来而是从分割特征再往下采一层。原因很现实分割支路的特征已经带边缘和边界信息密度图本质上是对“目标中心是否存在”的回归用边界特征来监督中心点梯度会互相拉扯。常见做法是把计数头设计成一个三层小卷积1×1 降通道、3×3 提取密度特征、1×1 输出单通道密度图输出尺度是输入的 1/8比如 640×640 输入对应 80×80 密度图。2.3 最小改动的模型定义示例从配置文件到前向逻辑如果你是在 ultralytics 这类框架上做二次开发我的经验是不要直接大改 Detect而是先仿照 seg 版本的 YAML 结构新增一个多任务头。下面是一个可参考的配置文件骨架注释里写了每个头的作用。# multi_task_yolov11.yaml 的关键片段 backbone: - [-1, 1, Conv, [64, 3, 2]] - [-1, 1, C3k2, [128, False, 0.25]] # ... 中间层省略保持 YOLOv11 默认网络结构 head: - [-1, 1, SPPF, [1024, 5]] # P5 层 - [-1, 1, C2PSA, [1024, False]] # 全局注意力增强 # 上面是共享主干部分 - [-1, 1, Detect, [nc]] # 检测头box cls - [-1, 1, SegHead, [proto_dim, mask_res]] # 分割头prototype coefficient - [-1, 1, CountHead, [1, 80]] # 计数头单通道密度图这个 YAML 里的关键参数是 proto_dim 和 mask_res。proto_dim 是 mask prototype 的数量常见取值是 32数量越大分割边缘越细但显存和延迟跟着涨mask_res 是 mask 输出分辨率默认 160。CountHead 里的 1 是输出通道数80 是密度图分辨率对应输入 640 时下采样 8 倍。配置改完之后前向逻辑也要跟着调整。一个最小可跑的 forward 改动长这样def forward_once(self, x): # 共享 backbone neck features self.backbone(x) neck_out self.neck(features) # 三个尺度的小特征图 # 检测头返回 (boxes, class_scores, dfl_vector) det_out self.detect(neck_out) # 分割头prototypes 经过全图卷积coefficients 由检测框 ROI 内特征生成 mask_protos self.seg_head.get_prototypes(neck_out) mask_coeffs self.seg_head.get_coefficients(neck_out, det_out) masks torch.einsum(bchw,bnc-bnhw, mask_protos, mask_coeffs) # 计数头从分割特征层再降采样输出密度图 density self.count_head(neck_out[0]) # 取 P3 层 return det_out, masks, density注意这里的分割头实现沿用了“只对检测框内部生成 mask”的思路coefficients 不是对全图每个像素算的而是从检测框对应的 ROI 特征里提取这样能省掉近一半计算量。计数头我特意取了 P3 层而不是 P5 层因为 P3 分辨率更高小目标的中心点在小尺度特征图上可能已经被压没了。这个改法的代价是 P3 层特征被三个头共用反向传播时梯度叠加所以后面损失函数设计里要控制计数头的梯度强度。3. 数据与损失设计三条标注流怎么喂给同一个模型3.1 数据格式一张图里的 box、polygon、密度图如何组织联合训练的第一个拦路虎是数据格式。检测只要矩形框分割要 polygon 或灰度掩膜计数要么要目标中心点要么直接要密度图三套标注各自存在不同文件里是没法喂给一个 DataLoader 的。我的做法是在数据预处理阶段统一转成三类信息并且把耗时操作预计算落盘避免训练时每次迭代都现场生成。这里给一个参考的 Dataset 实现核心是__getitem__同时返回三类标签def __getitem__(self, idx): ann self.annotations[idx] # 1. 检测标签直接从 JSON 的 bbox 字段读 boxes torch.tensor(ann[boxes], dtypetorch.float32) # 2. 分割标签polygon 点集转成二值化实例掩膜 # 注意这里输出的是 [num_instances, H, W] 的三维张量 masks polygons_to_mask(ann[polygons], self.img_h, self.img_w) # 3. 计数标签目标中心点列表转密度图 # 预计算后存成 npy训练时直接加载 density np.load(ann[density_path]) image load_image(ann[image_path]) return image, boxes, masks, density这里有几个容易被忽略的点。polygons 转 mask 时如果一张图里有 200 个实例直接生成 [200, 640, 640] 的 bool 张量会占 80MB三个头一起反向传播时内存压力很大我一般会改成把 mask 统一缩放到 160×160和分割头的输出分辨率对齐。density_path 是预计算好的密度图文件生成密度图时 sigma 很关键——sigma 太大峰跟峰连成一片模型学成“糊一片也能蒙对总数”sigma 太小又变成全零。按目标半径映射到 1/8 特征图上sigma 取 1 像素比较稳。3.2 损失函数组合检测用 CIoU分割用 DiceBCE计数用什么多任务损失设计上我的原则是“每个头沿用各自任务最成熟的损失不做创新”因为联合训练本身的变量已经够多损失函数再标新立异会直接无法定位问题。检测头沿用 box 的 CIoU、分类的 BCE分割头在 YOLO 系里默认是 mask 的 BCE但我实测加入 Dice 项能明显改善边缘区域和前景占比小的样本否则一张图只有一个大目标时BCE 的梯度容易被背景淹没。计数头看路线密度图回归用 MSE检测后计数则完全不参与损失。def multi_task_loss(pred, target, alphas): # 检测分支框回归的 CIoU 分类的 BCE和单任务 YOLO 保持一致 loss_box ciou_loss(pred[boxes], target[boxes]) loss_cls bce_loss(pred[cls_logits], target[cls_labels]) # 分割分支Dice 用于拉高前景区域梯度BCE 稳定全图收敛 pred_masks pred[masks] # [B, N, 160, 160] gt_masks target[masks] # [B, N, 160, 160] loss_dice dice_loss(pred_masks, gt_masks, eps1e-6) loss_mask bce_loss(pred_masks, gt_masks) # 计数分支密度图 MSE注意这时不对背景区域做加权 loss_cnt mse_loss(pred[density], target[density]) # 加权组合 return (alphas[box] * loss_box alphas[cls] * loss_cls alphas[seg] * (loss_dice loss_mask) alphas[cnt] * loss_cnt)这里三个参数必须说清楚。第一dice_loss 里的 eps 取 1e-6 不是随便填的当一张图的 gt mask 全为零比如背景帧时没有 eps 直接除零得到 NaN整个训练崩掉。第二分割损失的权重 alphas[seg] 千万别给大YOLOv8-seg 里默认只有 0.05 左右原因是 mask 分支每个样本的损失数值本身就比检测的 CIoU 大一个量级权重再大就把 box 梯度淹了。第三计数权重 alphas[cnt] 是三个里最难调的给 0.01 起跳往下试很多训练翻车都是因为密度图 MSE 的值域和检测损失不在一个量级。3.3 损失平衡策略先“各学各的”再“联合微调”直接三头同时开训经验上结果基本都是检测 AP 尚可、分割一般、计数完全没学起来或者反过来。原因不是模型能力不够而是不同任务的损失收敛速度差异极大分类任务几个 epoch 就能学得差不多密度图回归要学几十个 epoch 才稳定。我常用的做法是分两阶段。第一阶段把计数头的损失权重设为 0只训检测 分割让共享的 backbone 先把“什么是目标”学会第二阶段再把计数头打开前 10 个 epoch 用很小的固定权重后面切到自适应。自适应不用太复杂直接上不确定性加权uncertainty weighting就行# 把各任务的损失权重换成可学习的 sigma 参数 log_sigma_det nn.Parameter(torch.zeros(1)) log_sigma_seg nn.Parameter(torch.zeros(1)) log_sigma_cnt nn.Parameter(torch.zeros(1)) # 训练时这样组合注意要加上 log(sigma) 项防止退化解 uncertain_loss ( 0.5 / torch.exp(log_sigma_det) ** 2 * loss_det 0.5 / torch.exp(log_sigma_seg) ** 2 * loss_seg 0.5 / torch.exp(log_sigma_cnt) ** 2 * loss_cnt log_sigma_det log_sigma_seg log_sigma_cnt )这个做法的原理是每个任务的 sigma 越大说明模型对这个任务的“置信度”越低对应损失权重自动变小而 log(sigma) 项保证了 sigma 不会无限增大导致这个支路直接躺平。我踩过的坑是刚开始把三个 sigma 全部初始化为 0结果前几十个 iter 内不确定性损失被 log 项主导整体损失不降反升训练看起来像没在学。正确做法是三个 sigma 都初始化为 0.1让第一次权重更新幅度别太大。4. 训练参数与配置分辨率、batch、epoch 和那些必须调的数值4.1 参数总表一次说清六个最关键的训练配置很多人在多任务训练里反复试参其实大部分参数有默认值可循。我把跑检测 分割 计数联合训练时最关键的配置整理成一张表这些数值是我在不同数据集上试过相对稳妥的起点不是某个版本的官方默认值。参数推荐起始值调整方向理由输入分辨率640×640小目标密集场景提到 1024分割和密度图都吃分辨率640 下 160×160 的 mask 已经不够细batch size16显存不足就 8 梯度累积三个头同时反向传播batch 16 的峰值显存约等于单任务 batch 32epoch300计数头后开就加到 400密度图收敛慢150 epoch 内基本看不到计数 MAE 明显下降初始学习率0.01batch 减半时同步减半配合 warmup 3 epoch 使用避免头几个 iter 损失爆炸学习率调度Cosine数据量小1k 张用 StepCosine 的尾部低学习率对分割边缘精细度帮助明显多尺度训练0.51.5算力充足再开 0.32.0多尺度对计数头是双刃剑密度图的高斯 sigma 是按固定分辨率生成的这里重点展开前三项。分辨率 640 是 YOLO 系的常见起点但一旦任务里混入小目标计数比如细胞、远处车辆640 下的 P3 特征图上小目标可能只有 4×4 像素分割头要在这个尺度上生成边界掩膜信息量不够所以我建议直接上 1024 并用 mosaic 增强兜底。batch size 18 附近经常有个显存拐点16 刚好能塞进 24G 卡8 的话训练稳定性会查很多。epoch 这块检测任务 300 epoch 基本收敛但计数头的密度图回归更吃长尾训练我的习惯是看验证集 MAE 曲线连续 20 个 epoch 不降就提前停。4.2 训练顺序与多尺度先分割后计数还是先计数后分割训练顺序上我的结论是先分割后计数。原因是分割任务和检测共享的语义特征最多都在学“目标在哪、目标边界长什么样”而计数头学的是“目标中心点的分布”和检测的位置回归存在天然冲突。如果第一阶段的预训练让 backbone 充分适应了边界特征第二阶段开计数头时模型不会为了压低密度图 MSE 而把边界特征全部重新揉碎。具体操作是第一阶段用第 3.2 节里 alphas[cnt]0 的配置训 150 epoch第二阶段切到 alphas[cnt]0.01 训剩余 150 epoch。第二阶段前 10 个 epoch 我还会把检测头的学习率乘 0.1让计数头先适应特征检测头只做微调等计数 loss 开始正常下降再拉回来。多尺度训练这里要单独提醒计数头的密度图标注是按原始分辨率生成的训练时如果图像被随机缩放到 0.75 倍密度图的高斯峰也跟着缩小但 sigma 像素值没变等于标签和模型输出不匹配。常见做法是数据增强时对图像和密度图同步做 scalesigma 也乘缩放系数更稳妥的做法是多尺度增强只作用在检测和分割分支计数头固定 640 输入。4.3 显存不够怎么办梯度累积和 mask 分辨率妥协三头联合训练最现实的问题是显存。单跑 YOLOv11 检测可能 8G 就够挂了分割头显存直接翻倍再挂密度图头即使在 24G 卡上也要精打细算。我的实用组合是batch 16 换成 batch 8 梯度累积 2 步效果上接近 batch 16 但峰值显存降 40%。梯度累积要注意 BN 统计量的问题如果你的框架支持 sync_bn 就开不支持就确保第一个子 batch 的样本分布别太偏。还有一个容易被忽视的大头是 mask 分支的中间张量。mask_protos 是 [B, 32, 160, 160] 的 float 张量一个 batch 16 就是 55MB 起步而这是每个 iter 都要存在的。常见妥协方案是把 mask 分辨率从 160 降到 80分割精度损失有限但显存直接减到四分之一更激进的做法是在 mask 的 loss 计算里只取检测框内部区域背景区域直接切掉这样既省显存又让模型更关注目标实例内部。最后一个技巧是开 AMP自动混合精度。分割和计数头的算子大部分是卷积和逐点运算对 FP16 很友好我的经验是 AMP 下分割 mAP 掉 0.5 以内但训练速度提升接近一倍。如果发现 AMP 下计数头 loss 出现 NaN优先怀疑密度图 MSE 的累加精度把计数头的 loss 强制切回 FP32 计算即可。5. 避坑与排查联合训练里最容易翻车的五个地方5.1 分割分支 loss 变成 NaN训练中断现象训练到几百个 iterloss 突然变成 NaN然后模型权重全部废掉。看日志发现是 seg_loss 先爆不是 det_loss。原因最常见的是 batch 里出现了一张 mask 标注全为零的图比如背景帧dice_loss 公式里分母是预测值加真值的和全零真值会触发除零。另一个隐蔽原因是 density map 生成时 sigma 设太小整数坐标取整后中心点像素接近零MSE 的反向传播梯度异常放大。解决dice loss 里统一加 eps1e-6 兜底并且在 DataLoader 里过滤掉完全没有实例的样本不让它们进训练。密度图生成时改用浮点中心点坐标做高斯写入不要直接 round 成整数配合 AMP 时把计数损失单独切成 FP32 计算。5.2 检测精度掉点而计数精度上涨任务在打架现象第二阶段开计数头之后验证集上计数 MAE 从 15 降到 8但检测 mAP50 从 0.92 掉到 0.87分割 mAP 也跟着掉 0.02。看起来模型“偏科”了。原因密度图回归的梯度虽然量级小但它是逐像素监督每个像素都有梯度信号而检测的 CIoU 只在 anchor 匹配到的位置上回传。计数头积累的梯度在 backbone 里占比过高把特征往“密度分布”方向推牺牲了位置回归的精度。解决第一阶段保证计数头完全关闭第二阶段开启计数头时把计数权重降到 0.01 以下同时观察 det_loss 和 cnt_loss 的数值比。我一般要求 cnt_loss 的数值不超过 det_loss 的十分之一。如果任务持续打架就把计数头从 backbone 的深层特征改接到独立分支让它的梯度不影响检测头共享的那部分网络结构。5.3 小目标密集场景漏检计数跟着少一半现象训练曲线正常但验证时对密集小目标比如细胞群、远处车流的计数输出总数只有真实值的 60%而且漏掉的目标基本是最小尺寸那部分。原因YOLOv11 的检测头默认在 P3、P4、P5 三层特征上输出P3 层下采样 8 倍640 输入时最小能检测的目标约 8×8 像素。比这更小的目标压根不会进入匹配阶段。分割头受 mask 分辨率 160×160 限制两个挨得近的小目标在 160 分辨率下直接融成一个掩膜。解决把输入分辨率提到 1024或者在网络结构里增加 P2 输出层P2 是下采样 4 倍小目标特征保留度明显提升。更简单的是检查密度图生成的 sigma如果 sigma 取 1 而目标中心距离只有 3 像素高斯峰会混在一起导致计数头学习时“两个目标看成一个峰”这属于标签问题靠改模型没用。5.4 mask 标注缺失导致训练崩坏现象真实项目里分割标注很贵经常出现一张图只有检测框没有 polygon mask。直接补零训练分割分支把没有 mask 的图当成全背景学输出一片空白。原因DataLoader 里对缺失的 mask 统一补了全零张量分割头学到的是“看到这种特征就输出零”。而检测框明明框住了目标这种正负样本矛盾信号让 backbone 无所适从。解决在 Dataset 里给每个样本增加一个has_mask标志分割损失只在 has_maskTrue 的样本上计算其他样本只回传检测和计数的梯度。注意这里不能用全零 mask 参与 dice 计算因为 dice loss 对于全零目标的行为是不确定的要么跳过要么把 mask 损失权重在该样本上置零。这个坑在混合标注的数据集里几乎必踩属于我每次都要写进代码注释里的血泪经验。5.5 训练正常推理速度却不如三个分开跑的模型现象模型训练完验证时单张图推理要 80ms而原来的检测 20ms 分割 40ms 计数脚本 5ms 加起来才 65ms多任务反而更慢。原因分割头的 mask 解码部分用了einsum做 prototype 和 coefficient 的批量矩阵乘法在 CPU 上非常慢计数头的卷积虽然小但因输入分辨率高也占到不少时间。如果部署时不需要分割很多人的惯性错误是仍然跑完整网络只取检测结果。解决推理时根据任务动态裁剪头部。只做检测就把 seg_head 和 count_head 的 forward 都跳过需要计数但不需要掩膜时跳过 seg_head 的解码步骤。更细一步把 mask 解码的 einsum 改成矩阵乘法实现在 GPU 上能快 20% 以上。另外常见做法是导出 ONNX 时给三个头分别指定动态输出这样部署端可以按需加载而不是一次跑全量。6. 验证与进阶mAP、分割 IoU 和计数 MAE 怎么放一起看再谈部署技巧联合训练的价值要分别验证才有意义只看总 loss 下降毫无说服力。我的评估逻辑是分三块检测用 mAP50 和 mAP50-95分割用 mask 的 IoU 或 mask mAP计数用 MAE 和 MSE。注意这里的验证 batch 必须是 1因为密度图回归对 batch 内统计量的依赖非常强你不是在测训练行为而是在测推理行为。def evaluate(model, dataloader): det_ap evaluate_detection(model, dataloader) # 走标准 COCO 评估逻辑 seg_iou evaluate_mask_iou(model, dataloader) # 预测 mask 与 gt mask 的 IoU mae_sum 0.0 for imgs, _, _, density_gt in dataloader: _, _, density_pred model(imgs) # 密度图求和得到总数注意要乘上输入缩放系数 pred_cnt density_pred.sum(dim(2, 3)) gt_cnt density_gt.sum(dim(2, 3)) mae_sum abs(pred_cnt - gt_cnt).sum().item() cnt_mae mae_sum / len(dataloader.dataset) return det_ap, seg_iou, cnt_mae这个验证脚本的核心是计数指标的归一化。density_pred 是模型直接输出的密度值它的求和结果理论上等于目标数但如果训练时做过下采样或缩放这里要乘回对应的空间尺度系数否则 MAE 会整体偏小给你一个虚假的好结果。另一种作法是直接在 count head 输出端加一个全局平均池化回归头验证起来更省事但那个数字只能告诉你“数对不对”告诉你不了目标分布在哪。部署层面我目前的习惯是先导出不带计数头的 ONNX 模型跑检测 分割计数分支在部署端用检测结果的中心点做阈值统计实现。因为密度图头虽然能并行算但在 Jetson Nano 这类边缘设备上多一个卷积层和多一段后处理延迟开销远比 PC 上明显。如果你确实要在 orin 上部署完整三分支模型优先保证计数头的输入分辨率降到 1/16 而不是 1/8MAE 通常只受影响 2% 左右延迟能省掉近三成。这个方案本身适合的场景我从经验上总结单类目标、背景干净、计数对单目标准确率要求不高的情况把密度图头和检测头联合训练是划算的但如果目标是多类且每类都要单独计数检测后计数永远是更稳的选择别为了多任务而把网络结构复杂化。另外保存推理结果时建议把密度图也一起存成 npy后期换计数阈值或改统计口径时不用重新跑推理。希望这篇起步级的笔记能帮你在做 YOLOv11 多任务联合训练时少走弯路。本文还有配套的精品资源点击获取
返回列表