简介:《多任务框架-YOLOv11同时实现检测+分割+计数的联合训练方案》是一份面向计算机视觉开发者、算法工程师及科研人员的PDF技术文档,定位在让读者理解如何基于YOLOv11构建检测、分割与计数三大任务的联合训练框架,解决多任务场景下模型重复建设、算力消耗高等问题。文档共48页,为单文件PDF,压缩包大小约2.2MB,支持目录章节跳转与阅读器大纲定位,内容结构完整、排版正常。内容从YOLOv11网络结构与性能优势讲起,依次展开多任务框架设计、任务融合策略和联合训练方案,并覆盖数据集准备、模型训练优化、实验结果对比以及智能交通、工业质检、安防监控、医疗影像等应用案例,适合用于系统学习与方案设计参考。目前已有123人浏览学习,适合正在研究多任务学习或希望拓展YOLO系列用法的读者查阅。
1. YOLOv11多任务框架:一次前向同时输出检测框、分割掩膜与目标计数的选型前提
一条产线质检任务往往要同时回答三个问题:缺陷在哪个位置、缺陷覆盖哪些像素、这一批一共有几个缺陷。分开部署检测、分割和计数三个模型,意味着三次前向计算、三份权重驻留显存,而且检测框和分割掩膜经常对不齐——两个模型的特征表达各自独立,同一目标在检测坐标系和分割坐标系里可能偏移几个像素。YOLOv11多任务框架解决的是这种结构性问题:共享特征提取层只做一次计算,检测、分割、计数三个任务分支从同一组特征图分别解码,输出的空间位置天然一致。这套方案把YOLOv11的Backbone和Neck改造成共享层,挂上三个任务分支并用组合损失联合训练,适合智慧交通、安防监控、工业质检和农业监测场景,尤其是需要把多个单任务模型合并成单模型部署、对推理延迟和显存占用敏感的团队。
2. YOLOv11网络结构与多任务分支的架构拆解
2.1 从YOLOv8到YOLOv11:主干网络与检测头的关键变化
YOLOv11在主干网络上最明显的改动是用C3k2模块替换了YOLOv8的C2f模块。C3k2的思路是让特征沿两条路径传播,一条路径经过多个Bottleneck做深层语义提取,另一条路径通过shortcut直连,最后在通道维度拼接。相比C2f,C3k2的Bottleneck数量可以按模型规模灵活调节,梯度回传路径更短,浅层特征在反向传播时不容易被深层梯度淹没。这对多任务框架很关键:分割和计数分支对纹理、边缘这类浅层特征依赖较强,如果主干网络在加深过程中丢失浅层梯度,分割掩膜的边界质量和计数分支的密度估计都会受影响。
颈部网络仍然保持PAN-FPN结构,输出P3、P4、P5三个尺度的特征图,stride分别为8、16、32。PAN结构兼顾了自顶向下的语义传递和自底向上的定位细节传递。多任务场景下,P3提供给分割分支做高分辨率上采样,P5提供给计数分支做全局语义聚合,P3/P4/P5同时送给检测分支做多尺度预测。这个结构天然适配"检测+分割+计数"的三任务组合,基本不需要为多任务做额外的特征金字塔改造。
检测头方面,方案继续沿用解耦头设计:分类和回归分开两个卷积分支输出。解耦头的价值在于分类任务和回归任务的梯度不会互相干扰——分类分支关注类别可分性,回归分支关注边界框的IoU精度,这两个优化目标在早期训练阶段经常冲突,分开之后收敛更稳定。YOLOv11引入的动态锚框机制让候选框的初始尺寸跟数据集自适应对齐,不再固定使用COCO预设锚框,对特定场景数据集的收敛速度有明显提升。
2.2 共享特征提取层的选型:为什么复用Backbone和Neck而不是另起炉灶
多任务框架的第一步是确定共享层。方案文档选择直接复用YOLOv11的Backbone加Neck作为共享特征提取层,理由可以拆成三点来看。
第一,预训练权重直接可用。YOLOv11官方发布的COCO预训练权重覆盖n/s/m/l/x多个模型规模,加载后检测分支可以直接用继承的参数初始化,分割和计数分支从头训练。相比从零训练一个多任务模型,这种初始化方式让检测分支在第一个epoch就有可用的检测能力,分割和计数分支只需要在已有特征表达上做适配,收敛速度快很多。
第二,特征金字塔本身就是多尺度共享的产物。PAN-FPN输出的P3到P5特征图已经做了跨层融合,高层语义信息和低层定位信息在每一层特征里都有分布。三个分支拿到的是同一组特征,天然共享同一套表达能力。如果是两个独立模型,检测模型和分割模型的中间特征可能差异很大,这种"同源输入"的一致性在联合训练时能明显减少任务间的特征偏移。
第三,梯度共享的尺度问题需要提前处理。三个分支回传到共享层的梯度密度差异很大:检测损失只在正样本锚框位置回传梯度,分割损失是逐像素回传,梯度个数可能差出一个数量级。如果分割分支权重过大,共享层会被分割任务主导,检测的小目标召回率会下降。常见的做法有两种,要么降低分割损失的固定权重,要么给分割分支的梯度加一个缩放系数,第4章会给出实际可用的参数配置。
2.3 三个任务分支的结构设计与PyTorch实现
三个分支在共享特征图之后各自独立,以下是参照YOLOv11检测头设计思路实现的多任务分支代码:
import torch import torch.nn as nn import torch.nn.functional as F class DetectionBranch(nn.Module): """检测分支:从P3/P4/P5三个尺度的特征图预测框、类别和置信度""" def __init__(self, in_channels_list, num_classes, num_anchors=3): super().__init__() self.num_classes = num_classes self.num_anchors = num_anchors # 每个尺度独立使用一个解耦头 self.classifiers = nn.ModuleList([ nn.Conv2d(c, num_classes * num_anchors, 1) for c in in_channels_list ]) self.regressors = nn.ModuleList([ nn.Conv2d(c, 4 * num_anchors, 1) for c in in_channels_list ]) self.confidences = nn.ModuleList([ nn.Conv2d(c, num_anchors, 1) for c in in_channels_list ]) def forward(self, features): # features 为 [P3, P4, P5],长度与 in_channels_list 一致 outputs = [] for i, f in enumerate(features): cls = self.classifiers[i](f) reg = self.regressors[i](f) conf = self.confidences[i](f) outputs.append((cls, reg, conf)) return outputs class SegmentationBranch(nn.Module): """分割分支:基于P3特征图做上采样和逐像素分类,输出原图分辨率分割结果""" def __init__(self, in_channels, num_classes): super().__init__() self.conv1 = nn.Conv2d(in_channels, 128, 3, padding=1) self.conv2 = nn.Conv2d(128, 64, 3, padding=1) self.head = nn.Conv2d(64, num_classes, 1) def forward(self, x): # x 为 P3 特征图,stride=8,空间分辨率是输入的1/8 x = F.relu(self.conv1(x)) x = F.relu(self.conv2(x)) x = self.head(x) # 8倍上采样回到原图分辨率 x = F.interpolate(x, scale_factor=8, mode="bilinear", align_corners=True) return x class CountingBranch(nn.Module): """计数分支:全局平均池化后回归目标总数""" def __init__(self, in_channels): super().__init__() self.pool = nn.AdaptiveAvgPool2d((1, 1)) self.fc = nn.Linear(in_channels, 1) def forward(self, x): # x 为 P5 特征图,stride=32,包含最丰富的全局语义信息 x = self.pool(x) # (B, C, 1, 1) x = x.squeeze(-1).squeeze(-1) # (B, C) x = self.fc(x) # (B, 1) return x.squeeze(-1) # (B,)这段代码里有几个参数值得展开说明。in_channels_list对应P3、P4、P5三个尺度的通道数,在YOLOv11s里分别是128、256、512,检测头在每个尺度上独立预测,小目标的梯度主要来自P3层。分割分支选P3而不是P4,因为P3的空间分辨率是输入的1/8,上采样8倍回原图,比从P4做16倍上采样的边界精细度好很多,代价是分割头的计算量更大。计数分支吃P5的全局信息,AdaptiveAvgPool2d把整张特征图压缩成一个通道维向量,全连接层输出一个标量,这种方式和检测结果解耦,属于独立回归路线,本章开头提到的"检测框累加计数"是另一条路线,两者的取舍放在第3章讲。
三个分支的输入输出对比可以整理成一张表:
| 分支 | 输入特征 | 输出形状 | 说明 |
|---|---|---|---|
| 检测 | P3、P4、P5 | 每尺度 (B, anchors, 5+num_classes) | 分类、回归、置信度三头输出 |
| 分割 | P3(stride 8) | (B, num_classes, H, W) | 逐像素类别概率,上采样8倍 |
| 计数 | P5(stride 32) | (B,) | 目标总数的回归值,标量输出 |
2.4 梯度协调的三个可选方案
共享层收到三份梯度的场景下,除降低分割损失权重外,还有两种工程上常用的梯度协调手段。
第一种是GradNorm思路,在每次反向传播前计算三个分支损失对共享层输出的梯度范数,按梯度范数的比例反向调整任务权重,让三个分支的梯度量级保持在同一个尺度。实现上需要额外保存共享层输出的梯度,代码复杂度较高,适合三个任务重要性相近的场景。
第二种更简单,直接对分割分支的梯度做固定缩放,比如把分割梯度的反向系数设为0.5。这个操作的原理是分割分支的梯度密度远高于检测分支,人为压低它的梯度贡献,给检测分支留出足够的优化空间。方案文档里没有明确指定用哪种方式,实际训练时如果发现检测mAP上不去,可以先从梯度缩放试起,改动量最小。
3. 检测、分割与计数的特征融合与多任务损失函数组合
3.1 检测框引导分割:前景损失掩膜
检测和分割的信息交互是多任务框架的核心收益之一。检测分支输出粗粒度的边界框,分割分支输出细粒度的掩膜,两者在共享特征图上天然有空间对应关系。方案里采用的融合机制是双向的:检测框引导分割训练,分割掩膜反哺检测框修正。
检测框引导分割的做法是在分割损失计算时构造一个前景损失掩膜。分割任务里背景像素占比往往超过90%,如果整张图都参与交叉熵计算,模型会偏向预测背景,小目标的掩膜区域经常被直接吞掉。用检测分支输出的框(训练时用GT框,推理时用预测框)把非目标区域的前景损失屏蔽掉,让分割分支只关注检测框内部及其邻域的像素,目标的边界细节能得到明显保留。核心代码实现如下:
def foreground_mask_loss(seg_logits, seg_target, det_boxes, sigma=2): """ det_boxes: (B, N, 4) 检测框坐标,格式为 (x1, y1, x2, y2) sigma: 检测框向外扩张的像素范围,用于包含目标边缘的上下文 """ B, C, H, W = seg_logits.shape # 逐像素交叉熵,保留每个位置的损失值用于后续mask loss = F.cross_entropy(seg_logits, seg_target, reduction="none") det_mask = torch.zeros(B, H, W, device=seg_logits.device) for i in range(B): for box in det_boxes[i]: if box.sum() == 0: continue x1, y1, x2, y2 = box.long() # 检测框向外扩 sigma 像素,覆盖边缘过渡区域 x1 = max(0, x1 - sigma) y1 = max(0, y1 - sigma) x2 = min(W, x2 + sigma) y2 = min(H, y2 + sigma) det_mask[i, y1:y2, x1:x2] = 1.0 # 只在前景区域累加损失,避免背景像素主导梯度 masked_loss = (loss * det_mask).sum() / det_mask.sum().clamp(min=1) return masked_loss代码里的sigma=2值得单独说明。检测框是矩形切割,目标的边缘像素很容易落在框外,对外扩2个像素后再做掩膜,分割分支能学到目标边缘和背景的过渡带,掩膜的边界质量会有可见提升。如果检测框特别紧贴目标,sigma可以加到4到5,但过大的sigma会把其他目标的像素也包含进来,反而引入噪声。训练阶段这里用的GT检测框,推理阶段换成预测框,输入格式统一为xyxy,注意YOLO系列默认输出xywh,转格式时不要在数据管线的中间环节混用坐标系。
3.2 分割掩膜反哺检测框修正:连通域与回归辅助
反向的信息流动是分割掩膜对检测框的细化。分割输出的掩膜比检测框更贴合目标形状,利用掩膜可以直接修正框的边界。典型做法是计算每个类别的掩膜连通域,对每个连通域求质心和外接矩形,再和检测框计算IoU。IoU超过0.8的框直接用掩膜外接矩形替换,IoU在0.5到0.8之间保留原框但把边界向掩膜方向微调几个像素,IoU低于0.5的框标记为可疑候选,交给NMS阶段决定去留。
这种修正逻辑在推理阶段是一个后处理步骤,挂在检测分支输出之后、NMS之前,不会增加训练负担。在训练阶段如果想引入这部分约束,可以把每个检测框与对应掩膜外接矩形的距离差作为一项辅助回归损失,以很小的权重加进检测损失里,让检测分支在训练时就能从分割分支学到目标边界形状的先验。
3.3 计数分支的两条路线:独立回归与检测结果累加
目标计数在方案里可以走两条路线,它们的适用场景不同,不能直接替换。
第一条路线是独立回归,对应第2章的CountingBranch结构。模型直接从P5特征图回归目标总数,不依赖检测分支的输出。这种方式的优点是实现简单、推理开销低,适合目标密集但形态差异较小的场景,比如细胞计数、谷物颗粒统计。缺点是模型只能输出一个总数,无法回答"哪些目标被数进去了"这类可解释性问题,一旦计数误差偏大,很难定位是漏检还是重复计数导致的。
第二条路线是基于检测框累加。检测分支输出的所有框经过NMS去重后直接累加,得到目标数量。这种方式的好处是计数结果天然可解释,每个被计数的目标都有一个检测框。方案里提到一个实用技巧:把置信度阈值适当调低(从默认的0.25降到0.1),累加后再用分割掩膜的连通域数量做一次校验。如果检测框数量与掩膜连通域数量差异超过某个阈值,说明存在漏检或误检,可以按连通域数量修正最终结果。路线二在训练时不需要额外的计数损失,检测分支越准计数越准。
两条路线的对比:
| 对比维度 | 独立回归 | 检测框累加 |
|---|---|---|
| 训练依赖 | 需要计数标注或密度图 | 仅需检测框标注 |
| 输出可解释性 | 差,只有标量 | 好,每个目标有框 |
| 密集场景准确率 | 较高 | 受NMS和漏检影响 |
| 额外损失函数 | 需要MSE/密度图损失 | 不需要 |
3.4 多任务损失函数的组合与自适应权重调整
多任务损失函数的形式是所有损失项的加权和,完整的计算逻辑可以看下面这个函数:
def multi_task_loss(det_outputs, seg_logits, count_pred, det_targets, seg_target, count_target, lambdas=(1.0, 0.5, 0.1)): """ det_outputs: 检测分支输出列表,每个元素是 (cls, reg, conf) seg_logits: 分割分支logits,形状 (B, C, H, W) count_pred: 计数分支输出,形状 (B,) lambdas: (lambda_det, lambda_seg, lambda_count),默认检测为主 """ lambda_det, lambda_seg, lambda_count = lambdas # 检测损失:分类交叉熵 + 框回归IoU损失 + 置信度BCE det_loss = 0.0 for cls, reg, conf in det_outputs: cls_loss = F.cross_entropy(cls, det_targets["cls"]) reg_loss = 1.0 - torch.diag(box_iou(reg, det_targets["box"])).mean() conf_loss = F.binary_cross_entropy_with_logits( conf, det_targets["conf"]) det_loss += cls_loss + reg_loss + conf_loss det_loss /= len(det_outputs) # 分割损失:前景掩膜交叉熵,见3.1节 seg_loss = foreground_mask_loss(seg_logits, seg_target, det_targets["box"]) # 计数损失:平滑L1,对离群值比MSE更鲁棒 count_loss = F.smooth_l1_loss(count_pred, count_target) total = (lambda_det * det_loss + lambda_seg * seg_loss + lambda_count * count_loss) return total损失权重的设置直接决定训练行为。lambda_seg过高会共享层被分割梯度主导,lambda_count过高会让特征提取偏向全局语义而损失局部细节。方案里的参考权重组合可以按场景选择:
| lambda_det | lambda_seg | lambda_count | 适用场景 |
|---|---|---|---|
| 1.0 | 0.5 | 0.1 | 检测为主,分割和计数辅助质量评估 |
| 1.0 | 1.0 | 0.05 | 检测分割并重,如自动驾驶实例感知 |
| 0.5 | 1.0 | 0.1 | 分割为主,如缺陷区域精细测量 |
| 1.0 | 0.3 | 0.5 | 计数敏感场景,如密集人群统计 |
更进阶的做法是让权重在训练过程中自适应变化。一个可行方案是记录三个损失各自最近5个epoch的下降幅度,下降快的任务权重适当降低,下降慢的任务权重提高,把三者的优化进度拉齐。这个逻辑在每个epoch结束时更新一次权重,不增加训练开销,在任务难度差异大的组合里比固定权重稳定。
4. 多任务联合训练的数据准备与工程实现
4.1 数据集格式与标注工具的选择
联合训练需要一份同时包含检测框、分割掩膜和计数标注的数据集。三个任务的标注格式不同,工程上要统一到同一个数据容器里。检测用COCO的bbox字段,分割用segmentation字段存储多边形坐标或RLE编码,计数则有两种处理方式:一是直接存一个count字段,二是从分割掩膜里统计目标实例数生成密度图。
COCO格式对三任务比较友好,因为它的annotation结构天然同时支持bbox和segmentation两个字段,而count的值可以直接从segmentation里统计得到。Pascal VOC格式只有bndbox,Cityscapes的精细标注包含polygon但缺少统一的计数标签。选择数据集时需要确认标注是否同时覆盖框和掩膜两个维度,只提供框标注的数据集需要额外补充掩膜标注才能训练分割分支。目前公开数据集里,COCO的stuff类别和Cityscapes的实例标注可以直接用于多任务训练,其他场景数据通常需要用标注工具重新标注。
标注工具方面,LabelMe适合画边界多边形,CVAT支持同时标注框、多边形和标签属性,X-AnyLabeling适合做预标注后人工修正。工程上的建议是先用YOLOv11的检测模型做预标注生成初始框,再人工修正并补充掩膜,比从零开始画多边形效率高一个量级。
4.2 数据增强与标注的同步变换
多任务训练的数据增强比单任务复杂,核心问题是增强操作要同时作用于图像、检测框和分割掩膜三份数据,而且变换公式必须完全一致。YOLO系列常用的Mosaic增强在分割任务上会放大标注工作量——四张图拼成一张图,边界处的掩膜会被裁切,对应的分割标签也要做同样的拼接和裁切。
def mosaic_transform(images, boxes, masks, size=640): """ images: 4张图像的列表 boxes: 每张图对应的检测框,xyxy格式 masks: 每张图对应的分割掩膜 将4张图按2x2拼接并缩放到size """ canvas_img = torch.zeros(3, size, size) canvas_box = [] canvas_mask = torch.zeros(size, size, dtype=torch.long) # 每个子图的中心偏移由随机裁剪参数决定 for i, (img, box, mask) in enumerate(zip(images, boxes, masks)): # 按Mosaic的位置分配每个子图到画布的四分之一区域 # 子图的缩放比例和偏移需要同时应用到box和mask cx, cy = i % 2 * size // 2, i // 2 * size // 2 # 实际工程中此处使用仿射矩阵统一处理img/box/mask # 伪代码省略了具体的仿射实现,核心是同一个矩阵作用于三者 canvas_box.append(transform_box(box, affine_matrix)) canvas_mask[cy:cy+size//2, cx:cx+size//2] = \ transform_mask(mask, affine_matrix) return canvas_img, torch.cat(canvas_box), canvas_mask这段代码的核心是Affine变换矩阵同一套作用于所有标注对象。前两句伪代码注释里提到的affine_matrix是拼接时从原图到画布的缩放和平移矩阵,检测框的四个角点、掩膜的每个像素以及图像本身的像素坐标,全部乘同一个矩阵,才能保证增强之后的框和掩膜仍然贴合目标。常见的坑是图像用cv2.warpAffine做变换、掩膜却用了不同的插值方式,导致掩膜边缘偏移一两个像素,在分割指标上就会掉零点几个mIoU。
4.3 训练循环与超参数配置
联合训练的循环在标准检测训练基础上多了分割和计数两个损失的反向传播。配置上需要注意的项包括优化器、学习率调度、batch size和损失权重。下面给出训练循环的骨架代码:
from torch.cuda.amp import GradScaler, autocast def train_epoch(model, dataloader, optimizer, scaler, lambdas): model.train() total_loss = 0.0 for batch in dataloader: images = batch["images"].cuda() det_targets = batch["det_targets"] seg_target = batch["seg_target"].cuda() count_target = batch["count_target"].cuda() with autocast(): # 混合精度前向 det_out, seg_logits, count_pred = model(images) loss = multi_task_loss( det_out, seg_logits, count_pred, det_targets, seg_target, count_target, lambdas ) optimizer.zero_grad() scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() total_loss += loss.item() return total_loss / len(dataloader)混合精度在这里几乎是必须的:分割分支的输出是整张特征图逐像素分类,显存占用比检测分支大很多,不开AMP时一个batch只能放4张640×640的图,开AMP可以放到8张。训练策略参考配置如下:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 优化器 | AdamW,lr 1e-3 | YOLOv11的C3k2模块对AdamW的适配性好 |
| 学习率调度 | COCO预训练+10 epoch warmup+cosine衰减 | 先固定主干只训分支,再整体微调 |
| Batch size | 64(AMP下分8卡或梯度累积) | 依赖显存,分割分支是瓶颈 |
| 梯度累积 | 2~4步 | 小batch场景稳定BN统计量 |
| 分割损失权重 | 参考第3.4节表格 | 从低权重开始,稳定后逐步提高 |
4.4 显存管理与模型保存恢复
分割分支的显存瓶颈主要在最后的上采样层。一个实用技巧是让分割分支在1/4分辨率上计算损失,再上采样回原图做可视化,可以把显存占用降低到原来的四分之一左右,代价是损失函数对边界像素的约束稍微弱化。另一个技巧是梯度检查点,对分割头里的每个卷积层做重新计算,用时间换显存,适合单卡训练。
模型保存需要同时存储模型权重、优化器状态和当前epoch的学习率。多任务模型还有一个额外要求:保存当前epoch三个任务的损失值,方便恢复训练时判断权重设置是否合理。断点续训时建议把loss的滑动平均值也写入checkpoint,否则恢复训练后学习率重启容易导致损失震荡。
5. 推理结果保存、小目标优化与部署验证
5.1 预测后保存检测框、掩膜与计数结果
推理完成后需要把三个分支的输出序列化到磁盘。YOLOv11原生的model.predict只保存检测框和类别,分割掩膜和计数结果需要额外处理。推荐的做法是生成一个统一的JSON文件和叠加掩膜的PNG图片:
import json import numpy as np from PIL import Image def save_multitask_results(orig_img, det_boxes, det_scores, seg_mask, count, save_path): """ det_boxes: (N, 4) 检测框xyxy seg_mask: (H, W) 类别索引掩膜 count: 目标数量 保存检测框JSON和叠加掩膜的可视化图 """ result = { "image_size": orig_img.shape[:2], "detections": [ {"box": box.tolist(), "score": float(score)} for box, score in zip(det_boxes, det_scores) ], "segmentation": seg_mask.tolist(), # 可换成RLE编码压缩 "count": int(count), "count_source": "detection_accumulate" # 记录计数来源 } with open(f"{save_path}.json", "w") as f: json.dump(result, f) # 掩膜叠加:不同类别用不同颜色,便于人工核验 color_mask = np.zeros((*seg_mask.shape, 3), dtype=np.uint8) for cls_id in np.unique(seg_mask): color_mask[seg_mask == cls_id] = class_colors[cls_id] overlay = cv2.addWeighted(orig_img, 0.6, color_mask, 0.4, 0) Image.fromarray(overlay).save(f"{save_path}_mask.png")segmentation字段用tolist()保存的JSON体积会很大,一张1080p的图掩膜JSON可能超过20MB,部署时建议换成RLE编码或直接保存PNG索引图,JSON里只保存类别和面积这两个统计量。count_source字段记录计数结果来自哪个分支——如果来自检测框累加,配合检测框列表可以反推每个目标的计数依据;如果来自独立回归分支,那么计数值和检测框之间是松散耦合的,调试时需要分开排查。
5.2 小目标优化:输入分辨率、自注意力机制与CARAFE算子
小目标检测是YOLOv11多任务框架里最先暴露短板的地方,因为分割分支的上采样虽然保留了细节,但共享特征层的P3特征图对极小目标的信息量本身有限。三个实践中有效的优化方向可以优先尝试。
第一个方向是提高输入分辨率。训练和推理时把输入从640提升到960甚至1280,P3特征图的空间分辨率同步提高,小目标在特征图上占的像素数量变多,检测和分割的召回率都会提升。代价是推理速度下降约30%,显存占用翻倍,需要结合部署平台的算力评估。
第二个方向是在Backbone里插入自注意力机制。方案提到的SE模块是对通道维做全局注意力,而更进一步的思路是在C3k2模块内部加一个多头自注意力层,让特征提取时能建模远距离像素的依赖关系。这个改动对小目标密集的场景有效,因为自注意力的感受野覆盖全局,小目标可以从周围的上下文中获得补充信息。工程上建议只在最深一层的C3k2后加注意力层,计算开销可控。
第三个方向是替换上采样算子为CARAFE。双线性插值的上采样感受野只有2×2,无法利用全局上下文生成上采样核,导致分割掩膜边缘模糊。CARAFE通过学习的方式预测每个位置的上采样核,对物体边缘的保持效果好很多。替换位置在分割分支的上采样层和Neck的自顶向下路径中,改动量小,收益在mIoU上通常能提升0.5到1个百分点。
5.3 验证步骤与指标基线
验证多任务模型是否真正收敛,建议按下面的顺序来。
把三个任务的指标拆开逐一确认:检测看mAP50和mAP50-95,分割看mIoU,计数看MAE和MSE。先在验证集上跑一轮,记录三个指标;再分别单独训练三个单任务模型作为baseline,多任务模型在三个指标上都达到baseline的95%以上算及格,某个任务超过baseline则说明任务间的信息交互产生了正收益。
重点检查小目标的独立指标,按面积区间拆分mAP,小于32×32像素的目标单独统计。如果多任务模型的小目标mAP明显低于单任务检测模型,优先降低分割分支的损失权重,观察一个完整训练周期后小目标的召回率变化。最后做一次端到端的推理延迟测试,记录一次前向计算出三个分支结果的完整耗时,确认多任务框架相比三个独立模型的总延迟降低到原来的50%以下;日志里三个任务的损失曲线都稳定下降、验证集指标不再大幅波动时,说明检测、分割和计数在同一个特征空间里的梯度博弈已经进入稳定状态,模型可以进入部署环节。
本文还有配套的精品资源,点击获取