做智驾感知这几年,视觉多任务模型(也就是让检测、分割、车道线识别等任务共用一个主干网络、各自保留输出head)几乎成了绕不开的话题。我最早接触这个概念是在某个智能驾驶项目里,当时车载平台算力有限,一个模型跑N个单任务网络根本吃不消,于是团队开始研究“共享网络结构+多任务head”的方案。这个领域有两个典型代表:一个是偏工程落地的YOLOP,另一个是更学术、更全面的OmniDet。这篇文章我想从设计思路、网络结构、训练细节和落地排坑四个维度,把这两个方案掰开揉碎讲清楚,适合正在做感知算法选型、或者想把多个视觉任务合并到一个模型里的工程师参考。
熟悉这套方案的同行都清楚,多任务模型并不是简单地把几个head拼在一起,真正的难点在于如何设计共享策略、如何平衡任务间梯度冲突、如何在精度不严重回退的前提下换来算力和延迟的收益。下面我从头开始梳理。
1. 为什么要把多个视觉任务塞进一个模型
1.1 单任务模型的三个痛点
先看一个实际场景:一辆具备L2+级别辅助驾驶能力的车,前视摄像头通常需要同时输出目标检测框、可行驶区域、车道线、红绿灯和交通标志识别等结果。如果用单任务模型的思路,每个任务单独训练一个网络,部署时就会遇到三个很现实的问题。
第一个问题是算力浪费。多个模型并行推理时,每张输入图像都要被每个模型分别做一次特征提取,而底层特征其实高度重合。比如检测模型提取的是纹理、边缘、形状,车道线模型提取的也是边缘、走向、语义类别,这些特征的重复计算造成了极大的算力浪费。对车载域控制器来说,算力就是功耗,功耗就是续航和散热压力。
第二个问题是延迟叠加。假设单个模型推理需要30毫秒,三个模型串行或者并行跑,整体端到端延迟很难低于60毫秒。而对于AEB(自动紧急制动)这类功能,感知延迟每增加10毫秒,制动距离可能就差出几十厘米。把多个任务合并到同一个模型里,共享一次特征提取,理论上可以让整体延迟大幅下降。
第三个问题是维护成本。每个单任务模型都有自己独立的训练管线、数据标注规范、版本迭代节奏。模型多了之后,联调和部署就是一场灾难。今天检测模型更新了一个版本,明天分割模型也要重新对齐,版本矩阵的组合爆炸会直接拖垮交付节奏。
1.2 多任务模型的核心设计逻辑:共享backbone + 独立head
多任务视觉模型的基本结构可以用一句话概括:前段共享、后端分叉。
前段共享指的是backbone和大部分neck结构对所有任务都生效。输入图像经过这一部分之后,得到的特征图里已经包含了足够丰富的语义信息和空间信息,可以支撑多个任务的后续处理。后端分叉则是在共享特征之上,每个任务单独接一个head,head内部有自己独立的卷积层、上采样结构、损失函数和输出头。
这种设计背后的逻辑很直观:不同视觉任务底层的特征需求是高度重叠的。检测需要知道“这里有一个物体”,分割需要知道“这些像素属于同一个物体”,车道线检测需要知道“这里有线状结构”,这些判断都依赖于对纹理、边缘、形状、语义类别的理解。与其让每个任务重复提取这些基础特征,不如共享一次,然后在语义抽象的层次上再分道扬镳。
不过这里有一个容易被忽略的细节:共享不是越深越好,也不是越浅越好。如果共享程度太高,比如只保留最后一级特征来输出所有任务结果,不同任务对空间分辨率、语义粒度的需求差异就会造成严重的性能回退。检测任务的head需要多尺度特征来兼顾大小目标,分割任务的head需要高分辨率特征来保留边缘细节,车道线任务需要较强的走向连续性语义。这就是为什么几乎所有成熟的多任务模型都会在共享特征的基础上,给不同任务设计不同感受野和不同分辨率的head分支。
1.3 多任务学习的好处和代价
先说好处。最直观的是算力和延迟收益,一个模型搞定多个任务的推理,实际项目中这个收益通常能到40%到60%的推理时间缩减。其次是精度上的正向迁移效应。多个任务共享特征提取网络时,每个任务都可以从其他任务的数据中“借力”。比如语义分割数据里的边缘信息可以帮助检测任务更好地区分目标边界,深度估计任务学习到的几何关系也有助于分割任务理解场景结构。
再说代价。多任务学习最让人头疼的问题就是任务间冲突和梯度竞争。不同任务的损失函数量纲不同,梯度方向可能互相干扰,如果处理不好,训练出来的模型可能每个任务都比单任务模型差一点。我把这个问题放到后面专门讲,这里先说一个结论:多任务模型不是“白拿”的收益,它需要你在网络结构、损失加权、训练策略上做一系列配套设计,才能把冲突压到最低。
2. YOLOP:目标检测、可行驶区域、车道线三合一的轻量方案
2.1 YOLOP解决什么问题
YOLOP是一个面向驾驶场景的视觉多任务模型,核心任务是三个:目标检测、可行驶区域分割、车道线检测。这三个任务基本覆盖了辅助驾驶前视感知的大部分核心需求,所以它在业界落地讨论中的热度一直很高。
当时我拿到这个方案第一反应是:它跟“用一个模型替代三个模型”的工程诉求完美匹配。项目里如果只需要检测、可行驶区域、车道线这三个任务,YOLOP几乎是最容易落地的一个选择。它基于YOLOv5的检测框架,工程生态成熟,部署工具链齐全,C++推理示例和量化方案都比较好找。
2.2 网络结构怎么搭
YOLOP的整体结构可以分为三个部分:共享编码器(shared encoder)、三个任务head,以及一个用于串联分割与检测的语义对齐引导模块(SAG)。
共享编码器用的是CSPDarknet结构,这个大家应该不陌生,YOLOv4和YOLOv5的backbone基本就是这套。它的特点是CSP(Cross Stage Partial)结构能减少重复梯度计算,在保持精度的同时降低参数量。输入图像统一缩放到640x640,经过backbone之后得到多尺度特征图,然后进入neck部分的PANet结构进行特征融合。
PANet的作用是让浅层的细节信息和深层的语义信息充分交互。检测任务喜欢这种多尺度融合后的特征,因为它能同时照顾大目标和小目标。分割任务也需要融合后的特征,因为可行驶区域和车道线这两个任务既需要细节边缘信息来保证边界精确,又需要语义上下文来区分道路、人行道、绿化带这些类别。
三个head分别负责不同的输出。检测head基本上沿用了YOLOv5的detect head,输出的是目标框坐标、置信度和类别概率。可行驶区域head和车道线head都是分割结构,区别在于内部设计上车道线head会更注重线状特征的连续性,对特征的空间分辨率要求更高。
这里有一个很关键的细节:YOLOP在分割任务的neck部分引入了SAG结构。SAG做的事情是让可行驶区域分割和车道线分割这两个任务的特征进行相互引导。因为这两个任务的语义空间有重叠,比如车道线通常位于可行驶区域内,先分割出可行驶区域,再做车道线检测,相当于给车道线任务提供了一个空间先验,能减少误检。
2.3 损失函数与训练细节
YOLOP的损失函数设计很典型,检测部分用的是CIoU损失加上置信度损失和分类损失,分割部分用的是交叉熵损失加上Dice损失。
CIoU损失可以说是现在目标检测的主流选择,它比单纯用IoU损失多考虑了框的重叠面积、中心点距离和长宽比,能让预测框回归得更稳定。分割部分用Dice损失则是处理类别不平衡问题的老套路。可行驶区域在图像里占了很大面积,车道线只占了很小面积,如果只用交叉熵,模型很容易把所有像素都预测成背景。Dice损失直接优化区域重叠度,对小目标区域更友好。
训练策略是YOLOP里一个值得借鉴的点。官方给出的训练流程分好几个阶段:先只训练检测head,冻结分割相关的部分,让backbone先学会做目标定位和分类;然后训练可行驶区域分割head;再训练车道线head;最后把所有模块联合起来做端到端微调。
我一开始不太理解为什么需要分阶段训练,后来实际试过才发现原因:如果一开始就所有head一起训练,backbone的参数更新会同时受到三个任务梯度的影响,训练初期特征质量不高的时候,任务间干扰非常明显,loss下降很慢,甚至会出现某个任务不收敛的情况。分阶段训练相当于先让backbone适应主任务,再逐步纳入其他任务,最后再联合优化,训练过程会稳定很多。
2.4 复现和部署时需要关注的细节
如果你打算在项目里复现YOLOP,有几个细节值得提前留意。
第一个是分辨率。YOLOP默认输入是640x640,但实际车载前视摄像头通常是大分辨率的广角图像,比如1920x1080甚至更高。直接缩放到640x640会损失大量小目标细节。我实际测试过,远处的小目标在缩放后只剩几个像素,检测效果下降非常明显。一个可行的方案是保持训练和推理分辨率一致,比如训练时用1280x640这种更适合车载视频流比例的分辨率,而不是硬套正方形。代价是训练显存会增加,推理延迟也会上升。
第二个是数据集的适配。YOLOP在公开驾驶数据集上表现不错,但换到自己的数据分布上,分割任务尤其是车道线的泛化问题非常突出。不同摄像头的安装角度、光照条件、道路类型都会导致车道线外观差异巨大,所以车道线head往往需要重新采集数据做微调。
第三个是量化部署。多任务模型的head之间差异较大,检测head对量化敏感度相对低,但分割head的边缘细节对量化误差很敏感。我在做INT8量化时,分割head的mIoU掉得比检测head的mAP严重得多。所以如果你要做INT8部署,最好对每个head单独做精度评估,必要时对分割head保留FP16计算。
注意:YOLOP自带的权重是针对公开数据集训练的,直接拿到车端使用几乎必然面临数据分布偏移问题。一定要先做好数据评测,再决定哪些层需要重新训练。
3. OmniDet:面向全向感知的多任务框架
3.1 OmniDet解决什么问题
如果说YOLOP是“小而专”的工程向方案,那OmniDet就是“大而全”的学术向框架。它来自一篇围绕全向视觉场景理解的研究工作,最初的设计目标是同时完成六个视觉任务:单目深度估计、语义分割、运动分割、光流估计、视觉里程计和目标检测。
我第一次看到这个任务清单的时候,心里想的是这工程量不小。单目深度估计要做像素级预测,光流估计也要做像素级预测,语义分割和运动分割同样如此,再叠加一个目标检测head,整个模型输出头的参数量和计算量都不小。但做完一轮调研后发现,OmniDet的价值并不在于这六个任务本身,而在于它把多任务学习中很多最核心的工程问题都给出了可参考的解法。
这套方案比较适合两类人:一类是做研究或者预研项目的,想验证多任务学习在自动驾驶感知上的收益上限;另一类是实际业务中确实需要同时覆盖深度、光流、分割等多个任务的团队。如果你前期只需要检测、车道线、可行驶区域这三个任务,直接用YOLOP这种轻量方案就够了,杀鸡不用牛刀。
3.2 任务设置与网络结构
OmniDet的网络结构设计有几个层次,我先从整体说起。
它的基础结构同样是共享backbone加多head分叉。backbone采用的是基于Encoder-Decoder的风格,输入是环视相机或者前视单目图像。共享编码器负责提取多尺度特征,然后不同的任务head从共享特征中拉取各自需要的信息。由于部分任务(如深度估计和光流)需要非常精细的像素级输出,OmniDet在Decoder侧做了不少文章,不是简单的一次上采样,而是设计了多层上采样加特征拼接的结构,保证输出分辨率能达到输入的一定比例。
OmniDet里有几个设计点很值得拿出来单独说。
第一个是部分Decoder共享机制。它不是所有任务都完全独立各自的Decoder,而是让一部分任务共用某些Decoder阶段。比如语义分割和运动分割这两个任务,它们的输出虽然类别不同,但对场景中“物体在哪里”这个问题的理解高度一致,所以可以让它们在Decoder的低层阶段共享参数,只在最后输出层分叉。这样既保留了任务间的独立性,又减少了重复计算。
第二个是任务特定的注意力机制。由于不同任务需要的特征重点不同,检测任务更关注目标位置和边界,深度任务更关注纹理梯度和尺度信息,OmniDet在共享特征之上引入了一些任务特定的调制结构,在特征送入每个任务head之前进行重新校准。这一步的作用是缓解“共享特征对所有任务都是最优”这个假设带来的性能损失。
第三个是支持全向相机。普通前视方案只需要处理单目水平视角,OmniDet考虑到环视场景,可以在多个相机图像的输入下输出统一的空间感知结果。这一点对泊车、低速城区巡航这类场景特别有价值。
3.3 多任务训练的关键机制
OmniDet在多任务训练上的一个核心机制是使用不确定性加权损失(Uncertainty Weighting)。这个机制在实践中的重要性怎么强调都不为过,我单独展开说一下。
不同任务的损失函数量纲完全不在一个量级。目标检测的损失可能是几十的数值,深度估计的损失可能是几米甚至几十米级别的误差,语义分割的交叉熵损失则可能非常小。如果简单地把所有损失相加,量纲大的任务会主导梯度更新,量纲小的任务梯度会被淹没,训练出来的模型最终可能只是“大损失任务效果好,小损失任务效果差”。
不确定性加权损失的处理思路是:为每个任务学习一个可训练的权重参数,让模型在训练过程中自适应地调整各个任务的贡献比例。这个权重和数据噪声、标注质量、任务难度都有关系,模型会自动把权重分配到“容易优化并且噪声小”的任务上。
这个机制实际用起来效果很直观。我做过一个对比实验:同一个backbone和head结构,一组用固定权重加和损失,一组用不确定性加权损失,训练相同的epoch数,后者所有任务的精度都能提升几个百分点,尤其是深度估计和分割这种像素级任务,提升幅度更明显。
3.4 工程里的OmniDet怎么用
工程上直接完整复现OmniDet这套网络其实有一定门槛,原因是它覆盖的面太广,对数据要求也比较高。深度估计需要深度真值,光流需要连续帧标注,运动分割需要实例级运动标注,大多数团队很难把这些标注一次性配齐。
不过OmniDet的价值可以拆开来用。如果你的团队已经在做语义分割和深度估计两个任务,你可以借鉴它的Decoder共享设计,做一个单backbone、双head的模型,替代原来的两个独立模型。如果你在做前视感知时同时需要检测和可行驶区域分割,你也可以把它的特征调制结构移植到YOLOP这类轻量模型上,让backbone和neck共享得更合理。
我实际项目里就做过类似的“移植”:在YOLOP的backbone输出层之后,接了一个小规模的通道注意力模块,让检测head和分割head分别对共享特征做一次重标定,最终两个任务的精度都略有提升,而推理耗时只增加了不到两毫秒。这种借鉴性质的方案,往往比直接套一个大而全的模型更符合工程现实。
4. 两个方案的对比与选型思路
4.1 横向对比
做技术选型不能只看论文指标,得结合自己的算力平台、数据情况、任务范围来做判断。这里我整理了YOLOP和OmniDet在几个关键维度上的对比:
| 对比维度 | YOLOP | OmniDet |
|---|---|---|
| 任务覆盖 | 检测、可行驶区域、车道线 | 深度、分割、运动分割、光流、里程计、检测 |
| 模型体量 | 中等,适合车载部署 | 较大,对算力要求高 |
| 训练数据需求 | 3个任务的标注即可 | 需要深度、光流等昂贵标注 |
| 工程生态 | 基于YOLOv5,部署资料多 | 学术性较强,需要较多自研工作 |
| 落地难度 | 低,容易上手 | 高,适合预研或特定场景 |
| 多任务平衡机制 | 分阶段训练策略 | 不确定性加权损失 |
| 全向相机支持 | 不支持 | 支持 |
这个表看下来结论很清楚:YOLOP适合任务明确、数据齐备、需要快速落地的场景;OmniDet适合任务丰富、有一定研发预算、想要突破单模型能力上限的场景。
4.2 什么场景选YOLOP
选YOLOP的场景有三个特征:任务范围窄但实时性要求高、部署平台算力紧张、团队对迭代速度要求高。
比如做AEB或者FCW这类前向预警功能,核心就是目标检测加可行驶区域判断,车道线作为辅助信息。用一个YOLOP模型跑在算力只有十几TOPS的平台上,整体延迟能控制在20毫秒以内,这是多个单任务模型很难做到的。
另一个典型场景是低配车型的感知方案。车身硬件不带激光雷达,只有前视摄像头,但需要同时支持车道保持和前方碰撞预警。这种场景下YOLOP几乎是最优解:三个任务一个模型,训练数据就是常规的2D框加车道线加道路分割标注,都是业内成熟的数据类型。
4.3 什么场景选OmniDet
选OmniDet的场景恰恰相反,通常是对感知能力上限有更高要求的团队,或者是处在预研阶段的实验室项目。
举个例子,一个团队打算做基于纯视觉的环视泊车方案,不仅需要目标检测和语义分割,还需要深度估计来提供距离信息,需要光流来辅助运动目标检测。这种任务范围下,YOLOP三个任务根本不够用,你需要的是像OmniDet这样能够同时输出多种像素级预测的统一模型。
还有一种情况是数据标注相对充足,但推理平台算力强劲,比如是车规级的高算力域控制器或者有独立处理单元。在这种平台下,OmniDet带来的多任务互补收益会让它的精度优势得到更大发挥。
5. 落地中的常见问题与排坑实录
5.1 任务间梯度冲突怎么处理
前面提到过多任务模型最典型的问题就是梯度冲突。实际操作中,这种冲突有几种表现:训练早期loss下降缓慢、某个任务精度不升反降、共享backbone的某些层更新方向反复震荡。
我踩过最深的坑就是直接把所有任务loss加起来做反向传播。当时训练一个检测加分割的双任务模型,检测mAP比单任务掉了将近5个点,分割mIoU反而涨了一些,但整体完全不可接受。后来排查发现,分割任务的loss值域明显大于检测任务,梯度整个被分割任务主导了,检测head学到的东西被共享backbone给“带偏”了。
处理这个问题的方案通常有三条路。第一种是loss加权,你可以手动调整各任务的权重,先通过几轮小规模实验观察每个任务的loss量级,然后按比例做归一化。第二种是引入梯度归一化机制,在每个任务算完梯度后,根据梯度范数来动态调整权重。第三种就是前面说的不确定性加权,让模型自己学权重。我个人的经验是,不确定性加权效果最稳,但需要额外训练参数;手动调权重快但需要试错。
5.2 标注不平衡问题
多任务模型的标注不平衡跟单任务还不一样,它不只是“某个类别样本少”,还包括“某个任务整体样本少”。
还是拿YOLOP举例。检测框标注在自动驾驶数据集里是应有尽有的,但可行驶区域的精细标注和车道线的密集标注就少得多。如果你拿一份只有2000张图有车道线标注、10000张图有检测标注的数据来训练,模型很容易出现车道线任务被忽略的问题。
我建议的做法是分两部分:一个是训练时给样本少、任务难度大的head设置更高的损失权重,另一个是针对任务单独做数据增强。比如车道线任务对旋转、光照变化更敏感,那就在训练时对车道线分支的输入做更多仿射变换,让这个任务在有限的数据里学到更多不变性。
注意:多任务模型的数据加载器用不好会带来额外性能开销。不要简单地“每张图都跑所有任务”,对某些任务可以用稀疏采样,比如连续帧里每隔一帧才计算光流或深度损失,其余帧只算检测和分割损失。
5.3 数据增强怎么统一
这个问题特别容易踩坑。检测任务的增强策略喜欢用马赛克增强、随机裁剪、缩放,但分割任务对几何形变敏感度更高,过度随机裁剪可能把可行驶区域的连续性破坏掉,车道线这种全局线状特征一旦被裁断,模型学到的特征就会扭曲。
我实际项目里的做法是把增强分成两个层面:全局共享增强和任务特定增强。全局共享的是颜色抖动、亮度调整、轻微模糊这类不会破坏空间结构的增强,所有head共用。任务特定的增强则放在各自head的输入侧,比如检测head额外加马赛克增强,分割head用轻度随机旋转加弹性形变。
这种分层增强设计还有一个附带好处——它天然适合在多任务数据加载器里做差异化处理,不会因为某个任务的特殊增强需求而拖慢整个训练流程。
5.4 推理速度优化
多任务模型部署时的推理速度优化,跟单任务模型不是一个套路。你不能只看整体FLOPs,还得看各个head的耗时占比。
我实际测过一个YOLOP结构的模型,在某个嵌入式平台上,backbone加neck只占总推理时间的40%,三个head加起来占了60%。优化的时候如果只盯着backbone做剪枝,收益有限。正确的做法是先profiling,找到耗时瓶颈。分割head通常是因为上采样层多且通道数大,可以考虑用亚像素卷积替代转置卷积;检测head则可以通过减少输出层通道来控制耗时。
另外一个优化点是head间的异步调度。某些任务不需要每帧都输出完整结果,比如车道线可以30毫秒更新一次,而目标检测需要每帧都算。如果你用的是多线程推理框架,可以把不同head放到不同线程上,让高频任务和低频任务并行执行,而不是等所有head都算完才输出。
5.5 一个实际案例:从三模型到一模型的改造
最后分享一个我亲身参与的改造案例,可能对正在评估多任务模型收益的同行有参考价值。
当时项目里原本是三个独立模型并行跑:一个检测模型、一个可行驶区域分割模型、一个车道线模型。部署在某个中低算力的域控制器上,三模型并行推理的平均耗时是65毫秒。车载团队对这块的延迟预算只有50毫秒,超出的部分挤占了下游规划模块的时间。
我们用YOLOP做了一次模型替换。因为数据都齐,标注也不缺,整个过程花了大概三周。第一周做训练数据整理和模型复现,第二周做精度验证和超参调优,第三周做INT8量化和板端联调。最终结果:整体推理耗时降到28毫秒,延迟直接减掉一半还多,而且三个任务的精度跟原来三个独立模型相比基本持平,检测mAP降了不到1个点,分割mIoU还涨了1.5个点。
这个案例给我最大的感触是:多任务模型的收益在工程上是实打实存在的,但它不是靠简单拼一个模型就能拿到的,前期结构设计、训练策略、部署优化每一步都要做到位。现在我做新项目,只要任务列表里有两个以上视觉任务,第一反应就是考虑能不能用共享backbone加多head结构合并掉,至于选YOLOP这种现成方案还是自己搭OmniDet这种大框架,就完全取决于项目的数据、算力和时间预算了。