ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

YOLOv11多任务联合训练:检测、分割、计数一次搞定 简介面向目标检测与多任务学习研究者的YOLOv11联合训练方案文档系统讲解在同一个框架下同时完成检测、分割与计数任务的方法。内容从YOLOv11网络结构、多任务学习原理出发覆盖特征共享、任务分支设计、多任务损失函数、联合训练流程、数据集标注与预处理、模型优化及实验对比并给出智能交通、安防监控、工业质检等落地场景。文档共1个PDF文件整体约2.2MB48页内容支持目录跳转与大纲快速定位文字、图表显示完整清晰。已有123人学习下载适合有一定深度学习基础、希望提升多任务建模效率的开发者阅读。读者可借此快速建立检测-分割-计数一体化方案的全局认知减少自行梳理资料的时间直接用于方案设计、技术调研或课程学习参考。1. YOLOv11 多任务框架为什么检测、分割、计数要绑在一起训做过视觉落地的都知道实际项目里几乎没有只跑一个任务的场景。智能交通要同时框车辆、分割车道、数车流工业质检要圈缺陷、切缺陷区域、统计缺陷个数安防要检测人员、分割画面、清点人数。以前的做法是分别训三个模型检测用YOLO分割用U-Net计数再挂个回归头最后用胶水代码拼在一起。这套路看着省事实际维护成本高得离谱三个模型三个推理时延特征各提各的小目标漏检了分割跟着错计数更是直接拉垮。这份48页的《多任务框架-YOLOv11同时实现检测分割计数的联合训练方案》解决的正是这个问题——用YOLOv11做共享特征底座挂检测、分割、计数三个任务分支一次前向推理同时输出三类结果。方案从网络结构、损失函数、数据集准备一路写到训练优化和实验结果适合准备做多任务落地的工程师也适合拿YOLOv11做毕设但不想只跑个检测的在校学生。下面我把方案里的关键设计和可复现的细节拆开讲。2. YOLOv11 网络结构落地拆解共享特征层与三分支怎么搭2.1 为什么拿 YOLOv11 当多任务底座多任务框架的底座选型直接决定上限。方案里选YOLOv11当共享特征提取层核心原因是它的单阶段结构天然适合做“一鱼多吃”。YOLO系列从v1开始就把检测当回归问题做一次前向扫描直接输出边界框和类别速度快是刻在基因里的。到了v11骨干网络换成轻量卷积模块加注意力机制的组合既控制住了计算量又能自适应调整特征通道的权重这对后续同时喂给三个任务分支很关键——检测要的是边界和类别分割要的是像素级语义计数要的是整体的密度响应底层特征必须足够通用不能被某一类任务带偏。另一个选型理由是YOLOv11的多尺度融合策略和检测头设计。方案里明确提到跨阶段连接Cross-Stage Connection浅层特征和深层特征之间信息能直接传递小目标和大目标都有对应的特征层可用。检测头用了解耦头加动态锚框分类和回归分开优化锚框大小比例还能随输入自适应。这几项改进合在一起让YOLOv11在COCO这类基准上的精度能压过前代不少而精度恰恰是计数任务的上限——检测漏一个计数就少一个。2.2 三分支结构共享特征之后的各干各活整体架构不复杂输入图像先进共享特征提取层得到一组多尺度特征图然后分三条路走。检测分支复用YOLOv11的解耦头设计输出边界框、类别概率和置信度分割分支接全卷积结构上采样到原图尺寸逐像素分类计数分支最轻量全局池化后接一个全连接层直接回归目标数量。方案里给了一段计数分支的示例代码写法很典型class CountingBranch(nn.Module): def __init__(self, in_channels): super(CountingBranch, self).__init__() self.pooling nn.AdaptiveAvgPool2d((1, 1)) self.fc nn.Linear(in_channels, 1) def forward(self, x): # x: 共享特征层输出的特征图, shape [B, C, H, W] x self.pooling(x) # 全局平均池化, 压成 [B, C, 1, 1] x x.view(x.size(0), -1) # 展平成 [B, C] x self.fc(x) # 回归出一个标量, 即预测数量 return x这段代码逻辑上是把整张特征图的空间信息压缩成一个全局描述子再映射成数量。池化用AdaptiveAvgPool2d好处是不管输入特征图是多大尺寸输出都是 [B, C, 1, 1]不用手工算池化核大小。全连接层不加激活函数因为计数是回归任务输出要能取任意实数值。实际项目里有个常见改动如果目标密度差异很大有人会把最后的Linear层改成输出两个值再取指数让预测值始终为正训练初期更稳。2.3 分割分支与检测分支的实现细节分割分支方案里用的是类FCN结构逐层上采样恢复分辨率。关键点是最后的上采样倍数要和特征提取层的下采样倍数对齐。假如共享特征层把输入缩到了1/4分割分支就要把特征图上采样4倍回原尺寸。用 bilinear 插值做上采样看起来简单但细节位置会丢想要更好的边界可以换成转置卷积或后续再接一个轻量的解码器。检测分支我建议直接沿用Ultralytics那一套YOLOv11实现不用自己从零写。解耦头的思路是把分类和回归从同一个特征图上分开各自用独立的卷积输出避免两个任务互相干扰。动态锚框意味着每个位置不固定锚框尺寸而是根据特征图内容预测偏移量。自己复现这部分工作量不小落地时直接用开源实现改后端分支更现实。2.4 多任务损失函数三个损失怎么加权方案里的总损失写得清楚L ω_d * L_d ω_s * L_s ω_c * L_c其中 L_d 是检测损失分类交叉熵 边界框回归 置信度L_s 是分割交叉熵L_c 是计数均方误差。三个 ω 分别是任务权重。乍看很简单但这里藏着多任务训练最大的坑——梯度尺度不匹配。检测的回归损失可能是几十的量级分割交叉熵是几百计数MSE又是几千直接相加的话损失最大的任务会主导整个训练。我一般的做法是先各任务单独训几步看初始损失的量级然后按反比设权重。比如检测损失初始在2.5左右分割在1.8左右计数在150左右那就把权重设成类似 0.4、0.5、0.01 这样的比例让三个损失相加后没有哪个任务绝对主导。之后还可以用不确定性加权或者动态权重自适应调整但那是后话先把静态权重调明白再谈进阶。3. 联合训练方案设计损失权重、优化器与训练循环实现3.1 数据集准备三套标签的同步处理多任务训练对数据标注的要求比单任务高一个量级。同一张图既要检测框又要分割掩膜还要一个计数标签。方案里列举了COCO、Pascal VOC、Cityscapes这几个公开数据集其中COCO是唯一同时覆盖检测和分割标注的但要拿来做计数还得自己从标注里统计每张图的目标数量。Cityscapes适合自动驾驶场景但目标类别偏道路语义。实际项目里多数情况是自有数据用Labelme这类工具画多边形既能导出检测框多边形外接矩形也能导出分割掩膜计数标签直接数标注对象个数就行。这里有个非常容易被忽略的点数据增强必须同步处理。随机翻转、旋转、裁剪这些操作对检测框、分割掩膜和计数标签的变换逻辑不一样。翻转之后检测框坐标要跟着变掩膜要跟着翻转但计数标签不变随机裁剪可能把一部分目标裁出画面那计数标签也得重新数。方案里提到的“标注数据的同步处理”就是这个意思。def transform(image, boxes, masks, count, flipFalse): # 统一的预处理入口, 三套标签一起变 if flip: image image[:, ::-1].copy() # 水平翻转图像 # 检测框: x坐标翻转, 用 (width - x_center) 换算 boxes[:, [0, 2]] image.shape[1] - boxes[:, [2, 0]] # 分割掩膜: 同样水平翻转 masks masks[:, :, ::-1].copy() # 计数标签: 翻转不影响数量, 保持不变 return image, boxes, masks, count这段代码的要点在翻转逻辑那一行。检测框存的如果是 [x_center, y_center, w, h]水平翻转后 x_center 要变成 图像宽度减去原来的 x_center而不是简单取负。掩膜翻转用的是 numpy 的切片反向。计数标签为什么不用改因为整张图翻转后目标数量不变但如果做的是随机裁剪那裁剪框外的目标会被去掉计数标签就必须用掩膜重新统计。3.2 优化器与学习率策略联合训练三个任务优化器的选择直接关系收敛质量。方案里提到优化器选择、学习率调整、批次大小和迭代次数。常见做法是用 SGD 加动量momentum 0.937weight_decay 0.0005这是YOLO系列一贯的配置泛化性能比Adam好一些。Adam训练前期收敛快但后期精度容易卡住检测分割这种像素级任务尤其明显。学习率一般用 warmup cosine decay。warmup 阶段通常前3个epoch从很低的初始学习率线性升到目标值避免训练初期梯度方向不稳把共享特征层带偏。cosine decay 让学习率在后期平滑下降比阶梯式下降更容易收敛到好的局部最优。如果三个任务收敛节奏不一致——比如检测已经收敛但分割还在掉——可以单独给分割分支设高一点的学习率给共享层设低一点这就是分层学习率策略。3.3 训练循环实现一次迭代的完整结构for images, boxes, masks, counts in train_loader: images images.to(device) boxes [b.to(device) for b in boxes] # list, 因每张图目标数不同 masks masks.to(device) # [B, H, W], 像素级标签 counts counts.to(device).float() # [B], 每张图目标数 # 共享特征层前向 features shared_backbone(images) # 三个分支各自前向 det_cls, det_reg, det_conf detection_branch(features) seg_pred segmentation_branch(features) count_pred counting_branch(features) # 三个损失 loss_det det_loss(det_cls, det_reg, det_conf, boxes, masks) loss_seg seg_loss(seg_pred, masks) loss_cnt count_loss(count_pred, counts) # 加权合成 loss 0.4 * loss_det 0.5 * loss_seg 0.01 * loss_cnt loss.backward() optimizer.step() optimizer.zero_grad()代码里的详细结构说明几点。检测分支的标签boxes是变长的每张图目标数量不一样所以要按list逐张算损失或者padding到同一长度后加mask。分割的标签masks和计数标签counts都是整图级别的可以直接用tensor。损失权重0.4、0.5、0.01不是拍脑袋是参照前面说的“按损失量级反比”原则。训练时要把三个loss单独打印出来监控如果loss_cnt一直比loss_det大两个数量级那训练就废了。3.4 验证流程mAP、mIoU 与 MAE 三张表联合训练的效果不能只看一张图。检测任务看mAP平均精度均值COCO标准下是求IoU从0.5到0.95的平均分割看mIoU所有类别IoU的平均计数看MSE和MAE。方案里第八章的实验设置就是按这个思路展开的——不同模型做mAP对比、mIoU对比、MSE对比。验证时最省时间的做法是每N个epoch存一次checkpoint跑一遍完整验证集把三个指标记到一个CSV里。理性状态是三个指标同步上升但实际中经常出现检测涨、计数跌的情况这时候要回头调损失权重而不是盲目加训练轮数。4. 检测、分割与计数的任务融合信息交互与权重调整4.1 检测结果引导分割先框住再抠细节方案里专门有一节讲“检测结果对分割的引导”。这个机制在工程上有非常实际的价值直接做全图分割计算量大还容易把背景上的相似纹理误判成目标但先用检测框把目标区域圈出来分割分支只需要在框内做像素分类推理量降了准确率反而升了。训练阶段可以做一个辅助监督——检测分支输出的框对应位置的掩膜loss权重加大框外的loss权重减小。这样模型会优先把框内的分割做得准而不是浪费学习能力在无关背景上。4.2 分割结果细化检测边界回归的后悔药反过来分割分支输出的像素级掩膜也能帮检测头修正边界框。检测框是矩形的遇到倾斜或不规则目标框的边必然包含大量背景。但分割掩膜给出的轮廓精确得多。方案里的做法是把分割结果和检测回归特征拼接在一起让回归头在预测边界时参考掩膜信息。实际实现时不一定要做特征拼接更轻量的方案是推理阶段后处理检测框四个边向掩膜方向收缩直到掩膜的边界能明显提升IoU。4.3 计数任务的真实依赖检测与分割共同支撑计数分支最简单但也最依赖前两个任务。方案里提到“检测与分割结果对计数的支持”这是计数准确率的天花板。如果直接让计数分支回归总数那它就是个黑匣子训完也不知道数错在哪。工程上我更喜欢把计数做成检测结果的统计——检测分支输出所有目标后直接len()一下分割分支把连通域数一遍两种方法交叉验证。如果检测漏检严重计数就会偏少如果分割把一块大区域误切成两半计数就会偏多。所以联合训练时计数loss的本质作用不是让计数分支自己学出魔法而是反向约束检测和分割分支把目标找全。4.4 自适应权重调整动态调ω的两种方式静态权重调起来费劲因为训练中三个任务的收敛速度不一样。方案里提到自适应权重调整和融合策略优化。常见做法有两种。第一种是Gradient Normalization梯度归一化每个任务算完梯度后按梯度范数调整权重让各任务梯度尺度接近。第二种是Uncertainty Weighting不确定性加权把损失乘上一个可学习的方差参数方差大的任务自动降权。我实际试下来不确定加权实现简单、效果稳推荐先用它。log_vars torch.zeros(3, requires_gradTrue) # 三个任务的对数方差 def multi_task_loss(loss_det, loss_seg, loss_cnt): # 用可学习方差做自动加权, 方差大的任务自动降低权重 precision_det torch.exp(-log_vars[0]) precision_seg torch.exp(-log_vars[1]) precision_cnt torch.exp(-log_vars[2]) loss (precision_det * loss_det log_vars[0] precision_seg * loss_seg log_vars[1] precision_cnt * loss_cnt log_vars[2]) return loss这段代码原理要讲清楚log_vars是三个可学习参数初始时为0对应方差为1。训练时它自己会调整——如果某个任务一直很难学它的方差值会变大precision变小对应损失权重自动下降。后面加的log_vars项是正则防止方差变成负无穷。这种做法的好处是不用手调三个权重缺点是引入了额外的超参数初始值不过初始为0一般都能正常工作。5. 训练常见问题与避坑五次翻车记录与解决方式5.1 分割分支loss不降检测正常涨现象联合训练跑了几十个epochmAP稳步上升但mIoU一直在低位徘徊几乎不动。 原因我遇到过的情况是分割分支的学习率太低或者分割标签中目标类别像素占比太小前景背景比例严重失衡交叉熵被大量背景像素带偏。检测任务靠bounding box能学起来但分割是像素级分类类别不平衡的影响更致命。 解决先用单独的分割预训练把分割分支初始化到位再进联合训练或者在loss里给前景像素加权重把目标类别的交叉熵权重放大25倍。我通常用Focal Loss替代普通交叉熵对易分的背景像素自动降权。5.2 计数loss爆炸模型直接变NaN现象训练没几个steploss变成NaNloss.backward()直接报错。 原因计数分支的输出是线性层的标量回归如果真实目标数量很大比如一张拥挤街道图里有几百人MSE loss会非常可观。gradient clipping没开的时候梯度更新一步就可能把共享层的权重打飞。 解决两种手段一起用。一是给优化器加grad_clipPyTorch里写clip_grad_norm_(model.parameters(), max_norm10)二是给计数标签做归一化把counts除以当前batch的最大值或者除以训练集的最大数量让标签落在0~1区间。从那以后我每次跑多任务都先确认各任务loss的量级差异不超过两个数量级超过就先归一化再训练。5.3 推理时分割mask尺寸对不上现象训练一切正常部署的时候发现分割分支输出的mask尺寸和输入图不一样可视化直接花屏。 原因训练时统一resize到640x640但推理时输入了1920x1080的原图。分割分支里的Upsample用的是固定的scale_factor4输入尺寸变了输出尺寸也跟着变。检测分支因为anchor机制能自适应所以问题不明显。 解决上采样层换成interpolate 指定size或者推理时不用原图直接喂而是先resize到训练尺寸再推理。现在Ultralytics版本的推理管线会自动resize但自己搭框架时必须处理这一层。5.4 三个任务分支互相拖累性能都不如单任务现象分别训三个单任务模型每个都比联合训练的对应任务强联合训练变成负优化。 原因任务相关性没有想象中那么高或者共享层容量不够三个任务抢特征表示。检测要纹理和边缘分割要语义和边界计数要看密度分布同时塞进一个特征提取层可能谁也喂不饱。 解决先做相关性验证——把三个任务两两组合训练检测分割、分割计数、检测计数看两两组合时的效果。如果某对组合明显掉点就在那两个分支之间加独立特征层不要让它们直接共享同一个高维输出。方案里提到的“任务间相互促进”不是天然发生的得靠结构设计保障。5.5 数据增强不同步导致标签错位现象训练loss正常收敛但验证的时候mAP很低可视化发现检测框歪了掩膜错位。 原因图像翻转和裁剪了但检测框坐标或掩膜没跟着变。最常见的是用了第三方数据增强库图像自动翻转但boxes参数没传进去或者传了但没开flip。 解决数据增强管线只用一套图像和所有标签必须过同一个transform函数。我习惯把检测框、掩膜、计数标签打包成一个dict整个dict传进transform增强逻辑统一处理。6. 从方案到工程小目标优化与推理验证技巧6.1 小目标优化把共享特征层喂得更细多任务场景里最实用的一个技巧是调整共享特征层的输出尺度。YOLOv11的高效原因是下采样倍数够大计算量小但小目标经过那么多层下采样后特征图上的像素可能只剩一两个检测和分割都拿不到细节。方案里有一节专门讨论小目标优化常见做法是给共享层增加一个更高分辨率的特征输出分支不参与融合直接喂给分割和检测做辅助。代价是显存占用上去一截但换来的小目标mAP提升通常很值得。6.2 推理结果的保存与可视化验证训练完第一步不是直接部署而是把验证集结果保存下来人工过一遍。保存推理结果要用统一的输出格式检测框、分割掩膜、计数数值一起叠画到原图上避免一张张单独看后处理逻辑。import cv2 def draw_results(image, boxes, mask, count): overlay image.copy() # mask: [H, W] 0/1, 用颜色填充目标区域 mask_color np.zeros_like(image) mask_color[mask 0] (0, 180, 255) image cv2.addWeighted(overlay, 0.6, mask_color, 0.4, 0) # boxes: [N, 4], 画矩形框 for box in boxes: x1, y1, x2, y2 box.astype(int) cv2.rectangle(image, (x1, y1), (x2, y2), (0, 255, 0), 2) # 左上角写计数结果 cv2.putText(image, fcount: {count}, (30, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (255, 255, 255), 2) return image保存之前要先统一坐标类型。检测框如果来自模型输出可能归一化在0~1之间掩膜也是逻辑张量直接画图之前务必先乘以原图宽高。count值如果是回归出来的浮点数可视化时先用round取整。我习惯每个epoch结束都跑一遍验证集可视化专挑那些检测、分割、计数结果不一致的图看——框少了一个但掩膜还在说明分割分支和检测分支没对齐数出来的数和框数对不上说明计数分支脱离检测独立发挥了。6.3 部署前的最后一道确认联合训练的模型部署时最稳妥的方式是验证时同时输出三个分支的置信度分数确认它们在相同输入下的一致性。我这些年被多任务模型坑得最多的不是精度而是两个不同的后处理读同一份输出读出来的结果对不上。从那以后我每次写完推理脚本都用同一张图跑一遍可视化再写一个脚本统计三个任务结果的一致性——检测框数量、掩膜连通域数量、计数分支输出数量三者必须匹配。全部匹配了才敢打包交付。这份方案从原理到实验都很完整真正落地时最重要的还是那句话三个任务绑在一起不是为了省一张显卡的钱而是让检测的分寸感、分割的精细度和计数的宏观统计互相兜底。把这套框架跑通你大概率会对多任务学习有新的体会。方案文档里各个章节跳转和目录都整理好了边看边搭最方便。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进