简介本资源是一个面向自动驾驶场景的YOLOv8多任务统一模型实现聚焦目标检测、可行驶区域Freespace分割与车道线分割三大核心任务适用于算法工程师、智能驾驶研发人员及计算机视觉进阶学习者。项目采用轻量级设计将三类任务集成于单模型中并创新提出结构简洁、通用性强的卷积式分割头配合统一损失函数显著降低多任务定制开发成本兼顾实时性与部署友好性。压缩包共1313个文件涵盖859个Python源码含训练/推理/数据预处理脚本、171份Markdown文档含环境配置、模型说明与使用指南、43个YAML配置文件定义网络结构与训练超参、136个pyc缓存文件及少量Dockerfile支持Jetson/CPU/ARM64多平台、.pth模型权重、.ipynb示例笔记等整体28.54MB结构清晰、开箱即用。已有1344人学习下载提供完整训练流程、跨任务协同优化思路与轻量化部署参考是研究多任务学习与自动驾驶感知融合的实用技术方案。 跑过自动驾驶感知项目的人应该都懂光有目标检测远远不够。检测框能告诉你“这里有一辆车”但它不会告诉你“这条路能不能走”更不会告诉你“车道线在哪里”。在车载或者机器人场景下这三样东西经常是同时需要的目标检测负责找障碍物可行驶区域分割负责找安全空间车道线分割负责找行驶约束。之前我的做法是三个模型分开跑检测一个模型、分割一个模型结果就是推理延迟直接翻倍嵌入式板卡上根本跑不动。后来把YOLOv8改造成单模型多任务结构一次前向同时输出检测框、可行驶区域mask和车道线mask延迟一下子降了下来。这篇文章就专门讲讲这套多任务方案的完整落地过程从网络改造、数据处理到训练调参都给出能直接抄作业的做法。1. 多任务感知方案的整体设计思路1.1 单模型多任务 vs 多个单任务模型怎么选先说结论如果设备资源充足、任务之间没有强关联那分开跑三个模型确实更省心每个模型各司其职、独立调优出了问题也好定位。但实际部署到车载Jetson、RK3588这类设备上算力就那么一点三个模型轮流跑一遍单帧推理时间一加起来直接就没法满足实时性了。我最初在Xavier上测试过这种方案YOLOv8m检测大概25毫秒一个轻量级分割网络大概35毫秒再加一个车道线网络30毫秒三轮下来接近100毫秒也就是10FPS出头。这还没算前后处理的时间跑起来车机界面都会卡顿。所以对我来说多任务单模型几乎是唯一合理的选项。另一个更关键的原因是特征共享带来的好处。可行驶区域和车道线的判断本质上都依赖对场景结构、道路边界的理解这些信息在backbone的前几层就已经被提取出来了。单模型意味着三路任务共享同一份特征提取结果检测任务学到的“物体在哪”的语义信息能直接帮助分割任务理解“哪些区域被物体占据”反过来分割任务学到的几何结构信息也能帮助检测任务排除一些误检。这个特征互补效应在数据量不足的时候尤其明显。当然单模型多任务不是没有代价最主要的就是训练复杂度上去了三个任务的loss要一起优化梯度之间可能互相冲突。这个在后面“损失函数设计与权重平衡”那一节会详细展开。1.2 YOLOv8的网络结构回顾与改造切入点YOLOv8的骨干网络走的是C2f结构相比之前的C3模块C2f通过split操作让梯度流更加丰富在同样计算量下特征表达能力更强。颈部网络沿用FPNPAN的结构自顶向下传递语义信息自底向上传回空间细节这个部分非常成熟改造时完全可以在保留原结构的前提下接出多路分支。YOLOv8官方发布的权重分为detect、segment、pose、obb四种任务类型但官方没有直接提供detectsegment同时输出的多任务版本。我们要做的就是在这个基础上自己搭一个多任务框架。改造的核心思路很简单backbone和neck部分完全复用原版YOLOv8的C2f和SPPF在neck输出的三个尺度特征图之后把网络拆成两个独立的head分别做检测和分割。检测头就是原版的Decoupled Head分类分支和回归分支分开分割头则需要重新设计因为可行驶区域和车道线都是像素级输出需要把特征图上采样到原图分辨率。打个比方来说backbone就像是一个公司的公共后勤部门负责给所有项目组提供基础资料neck是中层管理把不同尺度的信息整合到一起到head这一层各个项目组开始各自干活了——检测组负责画框分割组负责涂区域互不干扰但共享后勤资源。这个设计思路决定了整个改造的代码结构。1.3 Head分支数量与摆放哪些层必须分开很多人一开始容易走弯路以为三个任务可以共享的东西越多越好最后把检测和分割共用了整个neck甚至部分head结果训练出来一顿稀烂。我踩过这个坑之后总结出一个经验backbone和neck可以放心共享head必须每个任务一套独立参数。原因不难理解。目标检测的输出是“在某个位置是否存在某个物体”本质上是稀疏的、基于anchor point的分类问题而分割输出是“每个像素属于什么类别”是稠密的像素级分类。两个任务的损失函数形态差异很大如果让它们共享最后几层卷积层训练时就会互相拉扯一个任务想让特征更加判别性另一个任务想让特征保持空间细节最后两边的效果都不好。具体到我的实现里neck输出的P3、P4、P5三层特征图尺寸分别是80x80、40x40、20x20以640x640输入为例会做一个split操作一路送入检测头另一路送入分割头。我测试过把分割头放在哪一层效果更好结论是放在P3这一层就够了因为可行驶区域和车道线都属于较大的目标P3层80x80的分辨率已经能保留足够细节不需要额外的高分辨率特征层这样计算量也能省不少。2. 数据集准备与标注格式转换2.1 数据集选型BDD100K为什么不二之选多任务模型需要的数据集必须同时包含检测框标注、可行驶区域标注和车道线标注。我搜了一圈最合适的还是伯克利发布的BDD100K自动驾驶数据集它包含10万张图片每张图都有完整的2D检测框标注其中约7万张有可行驶区域和车道线的语义标注。这个数据量对训练多任务模型来说完全够用。除了BDD100K还有一些备选方案。Cityscapes数据集本身也带车道线标注但它的检测框标注做得比较粗更多是实例级别的不同语义类别直接拿来训练YOLO格式的检测头有点费劲。ApolloScape的标注精度很高但数据量偏小更适合做评测而不是训练。如果你想只做demo验证也可以从BDD100K里抽几千张图先用着效果足够说明问题。BDD100K的类别体系也需要提前想清楚。它的检测类别有10类bus、light、sign、person、bike、truck、motor、car、train、rider。可行驶区域分成两类直接可行驶区域和备选可行驶区域还有人会把drivable和alternative drivable合并成一个通用可行驶区域。车道线分类更细按颜色、样式分了十几个类别。实际项目中我建议做简化车道线统一作为前景二值分割可行驶区域也合并成单类这样能有效降低训练难度。2.2 把BDD100K的JSON转成YOLO能吃的格式原始BDD100K的标注是JSON格式每张图对应一个JSON文件里面保存了所有的检测框坐标和多边形轮廓点。检测部分转YOLO格式很简单就是把两个角点坐标归一化存成class_id cx cy w h的txt文件每行一个目标。分割部分的转换要麻烦一些。因为YOLOv8自带的segment训练格式要求每个分割目标用多边形点集表示但可行驶区域和车道线不一定都是规则的闭合多边形尤其是车道线往往是很细的折线。我在实际处理时走了另一条路不转成YOLOv8的segment格式而是直接生成像素级mask用自定义的加载器来读取。具体做法是先在原图上画好三通道的二值mask——通道0是背景通道1是可行驶区域通道2是车道线然后把mask缩放到与输入图像相同的尺寸或者跟标签缩放比例一致在训练时随机做同样的数据增强操作。这里有一个细节值得提醒多边形和bbox在数据增强比如旋转翻转时如果转的是多边形顶点可能会出现顶点顺序错乱导致区域扭曲如果转的是像素级mask只需要对整张mask做同样的仿射变换就行问题会少很多。我第一次做转换的时候图省事直接存多边形等跑数据增强以后分割标签就变花了后来老老实实改成mask方案训练过程一下子就稳定了。转换代码的核心逻辑是先用imgaug或者自己写的仿射变换函数把图片和mask同步增强然后直接从mask中提取像素索引参与损失计算。这部分如果有现成的数据加载器强烈建议把mask作为整体来加载不要拆成多个目标能省掉很多麻烦。2.3 数据可视化校验标注里的坑在做完格式转换之后千万不可省略的一步是可视化校验。我一般会把原图、检测框、可行驶区域mask、车道线mask叠在一张图上打印出来随机抽个几十张逐张看。这一步能发现很多问题。比如BDD100K里有些图像是雨天、夜间、逆光场景检测框会漏标注一些模糊的行人有些可行驶区域多边形边界跟路牙贴合不紧往外扩了几个像素还有的车道线标注断断续续白色虚线目标在mask里变成了一段一段的点直接影响后面分割Loss的计算。我处理这些脏数据的策略是线下修正一批特别离谱的标注而不是在训练时做太多处理。因为多任务学习本身对标注噪声就比单任务敏感一个错误的分割mask会拉偏整个分割头的权重大半天。如果人力不够可以采用一个折中办法——对标注置信度低的样本直接丢弃。反正BDD100K数据量大丢几个极端样本对模型整体影响可以忽略不计。3. Head分支设计与损失函数计算3.1 检测头维持YOLOv8原版结构检测头这块我没做太多改动直接用YOLOv8原版的Decoupled Head结构每个尺度一个输出包含两条分支一条算分类损失用BCE一条算框回归损失用的是CIoU加DFL组合。之所以坚持用原版检测头是因为YOLOv8官方已经在COCO上花了大量算力调优包括正负样本分配策略TaskAlignedAssigner和anchor-free的decoupled head都是经过工业验证的。多任务改造已经够复杂了检测部分尽量保持稳定一是减少变量二是可以放心加载原版预训练权重让检测任务有个很不错的起点。唯一需要适配的地方是类别数和数据集。BDD100K有10个检测类别需要在数据集配置文件的nc参数里改。加载权重时如果检测头输出维度跟预训练不匹配Ultralytics框架会自动跳过不匹配的层所以不用担心加载报错后续把整个网络训练好以后这部分参数自然会收敛。3.2 分割头两种可选实现方式分割头我试过两种方案各有优劣这里把思路都列出来。第一种方案是复用YOLOv8-seg的ProtoMask方式。YOLOv8-seg的做法是backbone输出后用一个额外的分支生成一组原型maskProto同时检测头对每个目标输出一组mask系数最后通过系数线性组合原型来生成每个实例的mask。这种方案的好处是只增加很少计算量因为原型mask数量通常只有32个。但它本质上适合实例分割对于可行驶区域和车道线这种像素级语义分割任务来说直接用原型mask做逐像素分类表达力不太够。第二种方案是给分割任务单独加一个FCN风格的head。具体结构是从neck的P3特征图接进来经过一个1x1卷积把通道数压缩到64然后连续做两个3x3卷积最后用一个1x1卷积输出num_seg_classes个通道这里的通道数就是可行驶区域和车道线两类。输出特征图直接按像素算BCE或者Dice loss。我最后用的是第二种方案。虽然它比ProtoMask方案多一些计算量但语义分割表达更直接训练更稳定而且实现起来非常简单跟检测头完全解耦。如果你想控制参数量可以把分割头的hidden dim从64降到32精度损失很小。3.3 损失权重怎么定经验赋权不确定性加权多任务训练最核心的调参点就是怎么平衡三个loss。常见的做法有两种一种是根据经验手动设置权重另一种是用Kendall在2018年提出的基于同方差不确定性自动学习权重。我两种都试了说下实际感受。手动设置权重的方式很简单直接设loss loss_det alpha * loss_segalpha一开始取0.5左右。如果发现分割任务loss降不下去就把alpha调大一点如果检测框效果变差就调小一点。具体数值没有标准答案跟数据集、学习率都有关系。基于不确定性的自动加权方式理论上更优雅它通过给每个任务学习一个噪声参数来自动调整loss的比例。但我在实际训练时发现这个权重值在训练早期波动很大容易跟学习率衰减发生耦合反而把整个训练搞得不稳定。所以回到工程实践上我更建议用固定经验权重 warmup的方案前10个epoch让detection loss为主把学习率先拉起来从第10个epoch开始把segmentation loss权重加进来这样两个任务能平稳过渡到joint optimization的状态。另外还有一个细节可行驶区域这类大面积类别在mask里占比很高如果直接算BCE loss模型会倾向于把背景也预测成可行驶区域。所以我在分割loss里加了Dice loss因为Dice对样本不均衡没那么敏感。最后用的分割loss是0.4 * BCE 0.6 * Dice这个比例在BDD100K上实测效果不错。4. 基于Ultralytics YOLOv8源码的改造实战4.1 环境搭建与源码结构分析先说一下我部署训练的环境Ubuntu 20.04NVIDIA RTX 3090 24GBPyTorch 2.0.1CUDA 11.8ultralytics版本8.0.x。如果你用的是GTX 1660 Ti这类16系显卡显存只有6GB建议直接用yolov8n作为baselinebatch size调成4否则会爆显存。改造工作的入口是Ultralytics源码里的ultralytics/nn/tasks.py模型的搭建逻辑都集中在这里。核心是BaseModel类它负责根据yaml配置文件初始化网络结构、前向传播、loss计算。我们要做的就是写一个子类在init里同时创建检测头和分割头在forward里让三个任务共享backbone前向过程然后分别计算loss。从源码结构上看DetectionModel类的forward会走_predict_once方法经过backbone、neck、head三步。我们可以在这个基础上做改动方式是在parse_model搭好backbone和neck后额外初始化一个分割网络的卷积序列。这样改动的面比较小不需要动底层的BaseModel太多逻辑。4.2 修改模型定义与推理逻辑核心代码思路如下。因为Ultralytics每个版本API有点差异大家用的时候注意对照自己下载的版本:import torch import torch.nn as nn from ultralytics.nn.tasks import DetectionModel from ultralytics.nn.modules import Conv, C2f, Detect class MultiTaskModel(DetectionModel): def __init__(self, cfgyolov8m.yaml, ch3, ncNone, seg_channels2, verboseTrue): # 这里先让父类初始化检测相关的结构 super().__init__(cfgcfg, chch, ncnc, verboseverbose) # 拿到neck输出的某个特征层尺寸这里假设从P3层接入分割头 in_channels 256 # 需要根据模型尺寸调整yolov8m的P3层通常是256通道 self.seg_head nn.Sequential( Conv(in_channels, 64, 3, 1), Conv(64, 64, 3, 1), nn.Conv2d(64, seg_channels, 1) ) def forward(self, x, *args, **kwargs): # 复用父类前向逻辑拿到backboneneck的特征 y self._predict_once(x) # y 是Detect head的输出但我们需要从neck取中间特征接分割头 # 为了简化这里直接复用_predict_once的内部实现拆出neck的P3层 ...注意上面代码是示意实际接入分割头的P3特征图需要修改_predict_once方法在里面把neck的中间层结果保存下来。你也可以不继承DetectionModel而是直接改tasks.py里前向函数把分割分支的输入通过self.model[-2]那层之前的特征拿过来。建议自己先写一段小脚本打印每一层的shape确认清楚再动手。推理阶段同样要扩展检测结果照常输出分割结果要额外做softmax后取argmax或者用sigmoid做单类阈值分割。最后可视化时我会生成一张三通道的叠加图目标检测框画在原图可行驶区域半透明绿色覆盖车道线用红色细线描出来这样一眼就能看出模型的整体感知效果。4.3 训练命令与实验记录训练时我直接在Ultralytics的CLI基础上扩展先写一个自定义dataset的yaml文件描述图片路径和mask路径然后运行训练脚本。一个简化的训练流程如下yolo train \ modelmultitask_yolov8m.yaml \ databdd100k_multitask.yaml \ pretrainedyolov8m.pt \ imgsz640 \ epochs200 \ batch16 \ lr00.01 \ lrf0.01 \ optimizerSGD \ ampTrue因为Ultralytics原生不支持多任务切割mask的loss所以我实际是在源码里注册了一个CustomTrainer把分割loss加到传统的检测loss后面。整体loss打印出来长这样epoch 200: loss_box0.62, loss_cls0.35, loss_dfl1.02, loss_seg0.28, mAP500.74, mIoU_seg0.68训练过程中我比较关注的几个指标是Detection的mAP50和mAP50-95、分割的mIoU和车道线IoU。如果分割IoU长期低于检测mAP说明loss权重需要调如果检测mAP掉得很厉害说明分割分支梯度干扰太大。另外我每个epoch都会保存一次checkpoint方便回溯。整个训练在3090上跑了大约18个小时。对比单任务模型多任务模型在检测mAP上略微下降约1到2个百分点但分割任务从零开始学能到接近单任务分割模型的效果整体收益对我来说完全值得。5. 训练与推理过程中的常见问题排查5.1 多任务收敛失衡loss一高一低怎么办这是多任务训练遇到最多的问题。表现为检测loss已经降得比较低但分割loss还在高位原地踏步或者反过来。我遇到这个问题的第一次以为是网络结构有问题排查了很久没找到bug最后才发现是因为两个任务初始loss尺度差异太大默认学习率下分割分支根本学不动。解决方法是分别统计两个loss在初始几个epoch的量级比如检测loss可能刚开始是4.0分割loss是0.3差了一个数量级。这时候需要把分割loss的权重放大到2到3倍让梯度更新的绝对量能和检测任务匹配上。不要只盯着alpha这个数字关键是看两个loss各自的梯度范数。还有一个很实用的技巧给分割分支单独设置一个更高的初始学习率。因为在共享backbone的情况下分割分支属于全新初始化的模块而检测分支已经加载了预训练权重它们的收敛速度天然不同步。我通常用param_groups设置分组学习率检测head用1x分割head用10x效果立竿见影。5.2 显存不够参数调优与优化技巧显存不够是多任务改造绕不开的坎。分割分支虽然参数量不大但训练时要保存整张mask的梯度这额外开销其实不小。25G显存的3090跑yolov8m加分割头batch 16没问题但如果换成Jetson Orin这类小显存设备就得认真调参了。几个亲测有效的降显存手段第一分割分支的输出层精度不需要太高用ampTrue混合精度训练能把显存降下来接近一半。第二减少分割head的通道数比如从64降到32对精度影响很小但显存直接少了四分之一。第三gradient_accumulation可以解决显存不足还想用大batch的问题用optimizer.step()间隔积累梯度。如果以上方法都试过还是爆显存那就只能把输入分辨率降到512x512了。这个操作对检测任务影响不大但车道线细长目标的分割质量会明显变差所以这会是一个权衡取舍。我自己最后是700x384、batch 8跑的兼顾了速度和精度。5.3 车道线细长目标分割断裂的处理车道线在图像里通常只占几个像素宽是典型的长尾小目标。用FCN head做分割时最容易出现的问题是预测出的车道线在弯道处或者远处断成好几截。这个问题不能完全靠模型解决后处理十分关键。我的做法是加一个简单的Viz化后处理先用形态学闭运算把断裂的车道线连接起来再用skeletonize或递归生长法提取中心线最后用曲线拟合把点连成线。这个流程虽然朴素但效果明显。不过后处理只是补救根源还是数据增强要跟上。我在训练时加入了对分割目标特别友好的增强策略小幅度的透视变换和随机旋转让模型学会处理不同角度的弯道状车道线另外还加入随机遮挡和模糊模拟雨天反光、前车遮挡等场景增强模型的鲁棒性。加了这两块增强之后车道线的分割连续性明显好了一个档次。5.4 推理性能优化从TensorRT到NPU部署模型训好之后总得上车跑部署才是终极考验。我试过两种部署路线。第一种是TensorRT加速把PyTorch模型导出成ONNX再转成TensorRT engine。这一步遇到的主要坑是自定义分割head在ONNX导出时可能因为使用了某些不支持的操作导致转换失败所以网络结构里尽量别用过于自定义的层改用标准卷积和激活函数转换就基本顺畅。第二种路线是NPU平台比如RK3588。这类芯片跑YOLOv8需要走RKNN工具链过程相对繁琐但也能跑通。我把多任务模型导出成ONNX后单独拆分出检测输出和分割输出两个节点NPU上检测的推理延迟大概12到15毫秒分割因为要做全图上采样耗时稍高一些。整体一帧60毫秒内能搞定基本满足轻度实时的应用场景。跟单模型部署相比多任务模型最大的好处就是省去了三个模型的排队时间在算力紧张的设备上才能感受到这种差距有多重要。5.5 分类别统计别忘了给每个任务单独评估多任务模型最忌讳只看一个综合指标就下结论。我在每个epoch结束时会分别计算检测的mAP、可行驶区域的IoU和车道线的IoU并且把这三项指标分开记录曲线。因为很多时候某个epoch检测mAP升了但车道线IoU掉了这时候就需要去看是哪部分数据在起作用很有可能是过拟合到了某些训练样本上。还有一点很重要不要用检测数据增强的随机裁剪来同时处理分割mask。因为它们对边界处理的精度要求不同目标检测对bbox中心点偏移不敏感但分割mask对像素位置极度敏感。随机裁剪如果裁掉了部分车道线训练出来的模型对长车道线场景就非常不稳定。这一点在导入BDD100K这类道路数据集时尤其需要注意。最后说点个人经验这套方案从动手改造到跑出可用结果前前后后花了两周时间。最大的体会是多任务模型省下来的推理时间全靠训练阶段和数据处理阶段加倍还回去想要效果好数据清洗和loss权重设计比调网络结构重要得多。如果你想在三五天内快速出一个demo直接把我上面的默认参数铺开用就行数据量不用太大一万张BDD100K的子集配合yolov8n效果已经能在测试集上看到明显轮廓。等到后面真正要上线部署了再认真做类别细分、损失权重调优和量化剪枝也不迟。另外一个小建议训练之前先把单任务的检测权重在BDD100K上微调一圈再拿来做多任务初始权重这样检测分支能给分割分支提供语义更好的共享特征整个训练过程会顺畅不少。本文还有配套的精品资源点击获取