YOLOv8多任务模型实战:目标检测+可行驶区域分割+车道线分割 简介多任务学习是当前计算机视觉落地的重要方向尤其在自动驾驶场景中目标检测、语义分割与车道线检测往往需要协同工作。传统独立部署多个模型不仅资源开销大工程维护成本也高。YOLOv8凭借清晰的结构和高效的C2f特性成为构建多任务感知模型的热门底座。本文从多任务模型的基本原理出发讲解如何在YOLOv8-seg基础上改造出三头结构实现一次推理同时输出障碍物框、可行驶区域mask和车道线mask。内容涵盖共享backbone与neck的设计思路、数据标注统一与增强同步的工程细节、损失函数组合与权重调节方法以及ONNX/TensorRT部署时的优化要点。针对实际训练中常见的数据对齐、mask不收敛、车道线断裂等问题也总结了有效的排查技巧。适合从事ADAS、自动驾驶视觉或多任务感知开发的工程师参考帮助理解多任务模型从训练到落地的完整链路。 最近一直在折腾一个YOLOv8多任务项目核心是把目标检测、可行驶区域分割和车道线分割三个任务塞进同一个模型里跑。项目做完之后感触挺深这里把整个落地过程、模型改造思路、训练调参和部署环节完整梳理一遍希望能给同样在做自动驾驶视觉方案的朋友省点时间。这个项目要解决的核心问题很明确一套感知模型一次推理同时输出障碍物框、可行驶区域mask和车道线mask。相比单独部署三个模型共享backbone之后计算量降低明显而且任务之间有天然的信息互补。适合正在做ADAS、园区无人车、或者在嵌入式设备上做多任务感知的团队参考。我最终的方案是在Ultralytics YOLOv8-seg的基础上改造成三头结构保留了原生检测头新增了可行驶区域分割头和车道线分割头共用backbone和neck训练时三个损失一起反传。整个过程走下来我认为最有价值的不是模型结构本身而是数据对齐、任务权重调节和部署时多输出兼容这些工程细节。1. 项目背景为什么要把三个任务塞进一个模型1.1 三合一路线的真实需求在真实的路测环境里感知系统并不是孤立地做目标检测。车要决定能不能继续往前走必须知道前方道路的边界在哪要完成变道或者跟车必须清楚车道线的位置和曲率。以前常见的做法是检测、分割、车道线各跑一个模型三个模型独立输出最后在决策层做融合。这样做有一个很现实的问题设备资源扛不住。车载工控机或者Jetson这类边缘设备跑一个yolov8s大约占几十毫秒如果同时跑三个模型显存占用翻倍整体帧率会被压到很低。更麻烦的是三个模型各自维护训练数据、版本迭代和推理流程工程维护成本直线上升。多任务模型的价值就在于共享backbone提取通用特征一个网络同时输出三个任务的预测结果。目标检测需要的纹理信息、可行驶区域需要的边缘信息、车道线需要的几何信息其实在浅层特征里是高度重叠的。把它们放在一起训练任务之间还能互相补充——比如检测到远处停着的车会对可行驶区域的判断有正向帮助。1.2 为什么选YOLOv8做底座YOLOv8已经是目前工程落地比较成熟的目标检测框架但真正让我决定以它为基础做多任务改造的有几个具体原因。首先是Ultralytics的代码结构非常清晰backbone、neck、head之间的边界很明确。改动head不需要动到整个训练链路这对二次开发极其重要。其次是YOLOv8-seg的出现意味着官方已经验证了在同一个模型上叠加分割头是可行的。虽然官方分割头做的是实例分割思路和语义分割不完全一样但底层的特征复用逻辑是相通的。另外YOLOv8的anchor-free检测头在推理时省掉了大量后处理逻辑C2f结构在GPU上效率很高训练和推理速度都让人满意。实测下来同样的NVIDIA GTX 1660 TiYOLOv8s比之前用过的YOLOv5s在耗时上没有什么劣势精度还有一定提升。拿来做多任务改造的底座省心。2. 数据准备统一标注、统一格式、统一加载2.1 三任务的数据从哪来这个项目我用的是BDD100K数据集。选择它主要是因为BDD100K一张图里同时带了目标检测框、可行驶区域polygon和车道线polyline三类标注天然适合多任务训练不用拼接多个数据集。如果你的项目不能用开源数据需要自采数据那建议在标注阶段就同时标注三份检测框、可行驶区域多边形、车道线折线。这样后面转格式的时候一次搞定比分开标注省很多事。BDD100K的数据格式是这样的检测标注json文件每帧图像下有name、labels每个label包含category和box2d坐标。可行驶区域标注category为drivable area坐标是poly2d点集。车道线标注category为lane坐标同样是poly2d但语义上是一条折线。2.2 转换为训练所需的统一格式问题在于YOLO系列训练时检测和分割的标签格式完全不一样。检测是txt文本每行四个归一化坐标加一个类别ID分割如果是YOLOv8-seg格式则需要多边形坐标。我实际用的方案是检测标签走YOLO txt另外两个分割任务走全图mask。也就是说每张训练图对应三份标签信息det_xxx.txtYOLO检测格式一行一个目标。drivable_xxx.png可行驶区域mask前景为白色背景为黑色。lane_xxx.png车道线mask前景为白色背景为黑色。为什么分割不用YOLOv8-seg的多边形标签因为可行驶区域和车道线本质上是语义分割问题不是实例分割。我只需要知道像素属于可行驶区域还是背景不需要区分这是第几个可行驶区域。全图mask直接做逐像素分类实现简单训练也稳定。转换过程中最需要注意的是坐标系的归一化。BDD100K的json里poly2d是像素坐标需要除以图片宽高得到归一化坐标。mask生成时则要反算回像素坐标注意不要出现坐标超出图像边界的问题。2.3 自定义Dataset同时加载三种标签Ultralytics默认的Dataset只能加载检测标签带分割的YOLOv8-segDataset也只能读多边形标签不满足多任务需求。我写了一个自定义Dataset来同时读图、检测txt和两张mask。这个Dataset类只改标签读取部分不需要重新实现数据增强。因为Ultralytics在Dataset里会调用增强模块我需要让增强模块同时处理好检测框和两张mask。实际使用中我建议直接用Albumentations做增强而不是用Ultralytics内置的增强逻辑。原因是多任务场景下检测框、可行驶区域mask、车道线mask必须在一次增强操作中保持空间同步。Albumentations支持同时传bbox和mask翻转、缩放、裁剪之后框和mask会保持一致这个特性对多任务训练来说实在太重要了。import albumentations as A transform A.Compose([ A.RandomScale(scale_limit0.3), A.HorizontalFlip(p0.5), A.RandomBrightnessContrast(p0.3), A.RandomGamma(p0.3), ], bbox_paramsA.BboxParams( formatyolo, label_fields[class_labels] ))用Albumentations的时候注意如果图片标注里没有目标但可行驶区域mask是有效的这种情况下bbox列表为空需要单独处理不能因为检测框为空就跳过整张图的增强。2.4 数据增强必须保持空间一致性多任务训练的数据增强是很容易出问题的地方。单独跑检测的时候增强随便写问题不大但加了mask之后就要小心了。常见的坑有两个。第一个是resize的时候mask和原图必须使用相同的插值方式原图用双线性mask不能用双线性应该用最近邻插值否则mask边缘会出现模糊过渡训练时造成损失震荡。第二个是随机裁剪时如果裁掉了部分可行驶区域那么mask也要同步裁剪。手动实现这些操作容易出错所以我最后完全切到了Albumentations它的compose机制会把bbox和mask统一处理不用自己写对齐逻辑。旋转增强也要慎用。对于目标检测旋转15度以内问题不大但可行驶区域和车道线对旋转很敏感超过10度就容易把车道线转成不符合物理规律的方向。我最终只用了水平翻转和轻微旋转重度的旋转增强没有开。3. 模型改造YOLOv8如何长出三个头3.1 整体架构选择Ultralytics官方没有直接提供“检测可行驶区域车道线”三合一的多任务模型所以这一步需要自己动手改。我在YOLOv8-seg的基础上做了调整而不是从零搭网络。整体结构是这样的backboneCSPDarknet承担特征提取任务加载YOLOv8预训练权重。neckPAN-FPN融合多尺度特征。head拆成三个输出分支。检测头沿用YOLOv8原生Detect输出4个检测特征图分别对应不同尺度。可行驶区域分割头从neck最后一个特征层出发上采样到原图分辨率输出1通道mask。车道线分割头结构和可行驶区域分割头类似输出1通道mask。共享部分完全一样三个分支在neck之后再分叉各输出各的预测。这样backbone和neck的参数是三任务共享的推理时只计算一次这是多任务效率的核心。3.2 分割头的具体实现YOLOv8-seg原生分割头输出的是每个目标的实例mask设计思路是检测到目标之后在目标框内预测mask。可行驶区域分割和车道线分割不适合走这个路线因为可行驶区域不一定有“目标框”很多场景下贯穿整幅图实例分割的方式会把道路切成一块一块。所以我直接重新写了一个语义分割头这个头从backbone后端的P5特征层接收输入因为P5层分辨率低但语义信息强适合提取道路和车道线这类全局结构信息。分割头基本结构若干个C2f模块做特征细化一次双线性上采样到输入尺寸的1/4一层卷积将通道数压缩到1再上采样到原图尺寸输出1通道的logits这样设计参数不会太多不会拖累整体的推理速度。我实际改完后模型总参数量大约比yolov8s多了15%但推理速度只慢了大约12%。可行驶区域头输出1通道车道线头输出1通道两个头分开的好处是训练时方便分别调loss权重。如果你后续想扩展更多的分割类别比如区分可行驶区域和不可行驶区域把输出通道改成对应类别数就行。3.3 三头模型的核心代码示意下面是我改完之后的模型主干逻辑结构上做了简化方便理解class MultiTaskYOLOv8(nn.Module): def __init__(self, nc_det8, nc_mask1, ch(64, 128, 256)): super().__init__() self.model YOLOv8SEG(nc_det) # 复用官方backboneneck # 分割特征层处理 self.drivable_head nn.Sequential( nn.Conv2d(256, 128, 3, padding1), nn.BatchNorm2d(128), nn.SiLU(), nn.Conv2d(128, nc_mask, 1), nn.Upsample(scale_factor4, modebilinear, align_cornersFalse) ) self.lane_head nn.Sequential( nn.Conv2d(256, 128, 3, padding1), nn.BatchNorm2d(128), nn.SiLU(), nn.Conv2d(128, nc_mask, 1), nn.Upsample(scale_factor4, modebilinear, align_cornersFalse) ) def forward(self, x): det_out, seg_out, feat self.model(x) # feat是neck输出的P5层特征 drivable self.drivable_head(feat) lane self.lane_head(feat) return det_out, drivable, lane注意两点一是分割头的输入特征图尺寸要和mask的输出尺寸对应好避免上采样倍数算错二是训练时要拿模型输出的logits直接算损失判断的时候再套sigmoid不要在模型里过早做sigmoid。3.4 损失函数组合与权重三头模型的损失函数是三部分之和L_total L_det λ1 * L_drivable λ2 * L_lane检测部分沿用YOLOv8的默认组合包括box损失和分类损失性能已经很成熟不折腾。可行驶区域和车道线两个分割头我用了BCE和Dice的组合损失。原因很简单单用BCE在类别极度不平衡的时候容易训练不稳车道线在画面中可能只占几个像素BCE会让网络倾向于把所有位置预测为背景Dice损失对前景占比不敏感能强制网络关注前景结构。车道线这边还有一个细分情况如果做的是实例级车道线比如同时输出多条车道线各自的位置建议用polyline回归头或离散化点集分类头单纯的语义分割mask在后处理时的车道线聚类会比较麻烦。但如果像我这个项目一样只需要“哪些像素属于车道线”那语义mask就够用了。实际权重值我一开始设λ11.0、λ21.0训练几个epoch后发现可行驶区域损失明显偏大车道线损失偏小后面做了调整。4. 训练配置与调优要点4.1 从预训练权重起步多任务模型最忌讳的就是从头开始训练。backbone和neck有非常成熟的YOLOv8预训练权重直接加载可以省下大量训练时间也能让loss从一开始就快速下降。我用的加载策略是加载yolov8s-seg.pt权重。模型结构改了之后权重文件里没有新加的分割头参数所以加载时忽略分割头部分。新初始化的两个分割头学习率可以适当调大一点让它们尽快追上backbone的收敛速度。Ultralytics加载自定义模型权重时经常会遇到键不匹配的问题这里不要嫌麻烦直接在load_state_dict的时候加一个strictFalse然后把不匹配的键名过滤掉剩下的都是共享的backbone和neck权重。4.2 训练参数的实际配置我的硬件环境是单张NVIDIA RTX 3060 12G显存训练集用了BDD100K的子集大约5000张图batch size设16输入分辨率640x640。说一下配置思路不一定适用所有场景但可以当参考。优化器SGD初始lr0.02配合warmup 3个epoch总训练100个epoch。学习率策略余弦退火。混合精度开启显存占用明显降低训练速度提升约30%。数据加载num_workers8pin_memoryTrue。为什么选SGD而不是AdamWYOLO系列的训练习惯里SGD配上合适的lr schedule在检测任务上通常收敛更稳。分割头这边虽然BCE和Dice对优化器不太挑剔但整体模型还是用SGD比较符合YOLO的一贯风格。如果你的数据量非常大可以考虑AdamW收敛更快。batch size我用16是因为12G显存刚好能装下这个规模。更大的batch理论上更稳但显存不够梯度累积可以解决一部分问题实际效果不如直接加大batch来得好。4.3 损失权重怎么调多任务训练里最玄学也最重要的就是loss权重的设置。权重设得不对模型会偏向那个loss大的任务其他任务训着训着就退化。一个可用判断方法在训练初期观察三个loss的数量级。如果检测loss在0.05附近可行驶区域loss在0.3附近车道线loss在0.5附近直接相加的话分割任务会吃掉大部分梯度。我的做法是先不改权重训练5个epoch记录三个loss的初始值和下降趋势然后按比例调整。初始值差距大就用大的loss权重拉平。但注意不要单纯追求三个loss数值相等任务的重要程度不同检测稳定性优先时可以把L_det权重维持1.0分割头的总梯度占比控制在30%以内。最终我选的是λ10.5λ20.8效果比λ1.0时稳定不少。4.4 训练过程怎么监控训练时不要只看total loss要把三个任务的loss曲线分开可视化。Ultralytics在训练过程中会把box_loss、cls_loss、dfl_loss分别显示但第二个和第三个分割loss需要自己加日志。我给项目加了一个周期性的评测逻辑每训练10个epoch在验证集上跑一次分割预测把预测mask可视化保存下来。肉眼观察车道线和可行驶区域的连续性比盯着loss曲线有用得多。很多时候loss在降但预测mask边缘一团糟这种问题只能靠可视化发现。训练loss曲线方面最关键的信号是Dice loss正常情况下应该从接近1的位置缓慢下降到0.1以下。如果你发现Dice loss从一开始就很低比如0.1以下大概率是出现了mask全黑或全白的问题需要赶紧检查标签加载。5. 评估与部署5.1 三个任务分别怎么测多任务模型的评估不能一概而论三个头各自用各自的指标。目标检测mAP50和mAP50-95。用COCO或自定义检测集的标准评估脚本。可行驶区域mIoU。计算预测mask和GT mask的像素交并比背景和前景分别算然后取前景的IoU为主要指标。车道线像素IoU和F1。如果后续做车道线聚类还要计算TP/FP/FN看车道线的连通性和连续性。我实际在验证集上跑的结果检测mAP50在0.82左右可行驶区域mIoU在0.86左右车道线IoU在0.67左右车道线的IoU偏低主要受远处小目标影响近处车道线的效果是可以接受的。评估过程中要特别注意尺寸对齐。模型输出的mask是640x640验证集的GT可能是原始分辨率计算IoU之前一定要先resize到同一个尺寸。这个坑我踩过很多次尺寸没对齐的时候IoU会虚高误导调参方向。5.2 导出ONNX和TensorRT训练完的项目最终要部署到边缘设备导出环节不能马虎。用Ultralytics导出ONNX很简单通常一条命令就能生成。但多任务模型的导出有两种方式需要提前想好把三个头合并成一个模型导出后输出三个张量。分开导出三个模型但这样部署时要跑三次推理完全失去了多任务的意义。我采用的是方案一。导出时打开dynamic_axes让batch维度是动态的方便部署时按需调整batch size。导出ONNX之后用onnxruntime测试一下输出形状是不是符合预期然后导入TensorRT做fp16推理。yolo export modelbest.pt formatonnx dynamicTrue opset13 trtexec --onnxbest.onnx --saveEnginebest_fp16.engine --fp16TensorRT的优化效果非常明显在RTX 3060上原始PyTorch推理耗时大约35ms转成TensorRT fp16之后降到18ms左右而且这个速度是三个任务全部完成的总耗时。5.3 边缘设备部署的注意点设备端的部署我只说几个关键经验细枝末节不展开。第一输入分辨率不要盲目追求高。640x640在大多数场景够用如果车道线远处识别效果不好可以尝试768x768但推理耗时上涨明显。我实测640调到768mIoU提升大概1.5%但推理时间增加了25%这个性价比要评估好。第二尽量用INT8量化但一定要先做校准数据集。可行驶区域mask对量化误差比较敏感不做校准的话mask边缘会变得很脏。我的经验是先跑fp16确认精度无损再考虑INT8。第三两个分割头的输出后处理要放在目标检测NMS流程之外避免NMS把mask信息干扰掉。mask的分辨率和输入图一致直接在设备端用GPU算子做阈值分割即可不要拷贝回CPU再处理那样延迟会飙升。6. 常见问题与排查技巧实录问题现象可能原因解决办法训练时loss为NaN学习率过高、输入数据有异常值降低初始lr检查标签是否有越界坐标可行驶区域mask全黑分割头权重初始化不合理、mask标签读取失败检查Dataset的mask路径确认加载后数据非空车道线断裂严重车道线的类别极度不平衡BCE权重过大增加Dice loss权重、后处理加形态学闭运算目标检测精度掉点多任务抢占梯度导致检测头收敛慢适当调大检测loss权重、冻结backbone前几层先训检测头分割头边缘锯齿感明显上采样使用双线性但输出分辨率太低输出分辨率改到原图1/4以上后处理加滤波平滑训练mAP正常但推理效果差部署时预处理和后处理不一致检查推理时是否做了与训练相同的归一化和尺寸对齐GPU显存溢出batch过大、输入分辨率过高减半batch、开梯度累积、降低输入分辨率6.1 数据对齐问题多任务最隐蔽的坑我整个项目过程中踩过最大的坑其实是数据对齐。检测标签、可行驶区域mask、车道线mask三份数据只要有一份在加载时没和当前图片对齐模型就一定会学歪。怎么排查简单粗暴的办法是写一个可视化脚本把检测框、可行驶区域mask、车道线mask同时画到原图上肉眼检查几十张图。如果检测框和车道线位置有偏移或者mask边缘明显和图像内容对不上那一定是数据加载或增强环节出了问题。这个脚本值得多花时间写因为它能省掉后面无数个训练排错的夜晚。6.2 分割头不收敛先检查sigmoid分割头不收敛绝大多数时候不是网络结构的问题而是训练收敛异常的细节。我遇到过一次两个分割头的loss一直卡在0.7左右不动排查半天发现是模型输出和loss计算之间加了一次多余的sigmoid。sigmoid是一个单调函数如果模型输出是logitsloss函数里已经带了sigmoid那再在外面包一层会让梯度变小甚至消失。检查这类问题直接打印模型输出值的范围如果输出值一直在0到1之间大概率是多套了一层sigmoid。6.3 车道线后处理形态学操作能治断裂车道线分割模型的输出即使训练得很不错原始mask也经常出现断裂和孔洞。推理阶段后处理需要加一道形态学闭运算先膨胀再腐蚀几乎能把断线重新连接起来。我实际用的核大小是5x5遍历两遍。如果车道线断裂很严重可以适当增大核或增加迭代次数但注意别把两条相邻车道线连成一条。OpenCV里就是两行代码的事kernel cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) lane_mask cv2.morphologyEx(lane_mask, cv2.MORPH_CLOSE, kernel, iterations2)这个操作在GPU上无法直接用OpenCV做部署到TensorRT时需要在后处理里单独实现或者把闭运算转成卷积实现。6.4 类别不均衡针对车道线单独调权车道线在整个画面中占的像素比例通常只有1%到3%这是Chicken-and-egg问题越难学的东西越容易被模型忽视越被忽视就越学不会。一个有效的补充方案是在损失函数里给车道线mask加一个类别权重系数。对前景像素赋予更高的weight让BCE loss能够注意到这些稀疏但重要的像素。pos_weight torch.tensor([5.0]).to(device) # 车道线前景权重 criterion nn.BCEWithLogitsLoss(pos_weightpos_weight)pos_weight设多少需要根据数据集实际比例来调我的数据里前景占比大约1.5%pos_weight设5比较合适。如果设得太大模型会把背景大量误判为车道线IoU反而下降。最后再分享一个我的经验多任务模型的项目最后总会在某个意想不到的地方出问题。我之前把三个任务的数据都准备好了模型结构也改好了满怀期待地跑了一轮训练结果发现可行驶区域分割出的形状都是斜的检查了半天才发现是训练时用了一个只对检测框做的随机旋转增强mask没有跟着转。这个教训让我后来坚定了一件事多任务训练里数据增强的同步性优先级高于一切。另外一个建议是如果你也准备走YOLOv8多任务这条路第一版模型不用追求一次到位。先跑通一个最小可用的版本比如检测只保留几类、分割mask只区分粗略区域验证整套pipeline是通的再逐步扩类别、加样本。不要一上来就把所有行类全加上否则数据、loss、部署哪一环出问题都不好定位。这个项目后续如果想继续扩展我打算在车道线分割头后面加一个拟合模块把车道线mask转成三次曲线参数这样输出给控制层的数据会更干净。希望这篇记录能给正在做多任务感知的朋友一些参考少走一点弯路。本文还有配套的精品资源点击获取