资讯动态

自动驾驶感知实战:YOLOv8在KITTI数据集上的目标检测训练与优化

发布时间:2026/10/21 21:08:18 来源:尧图企业网站定制
做自动驾驶感知的同学应该都遇到过这个需求车辆、行人、红绿灯这三类目标基本就是城市道路的“铁三角”也是摄像头方案的必修课。前两年大家可能还在用Faster R-CNN或SSD硬撑或者用YOLOv5做原型验证但到了实际工程落地阶段检测速度、小目标召回、类别均衡这几个问题会轮流折磨你。我最近在模拟项目X里完整做了一遍基于KITTI数据集的YOLOv8训练与优化覆盖车辆、行人、红绿灯三类目标的检测从数据标注格式转换到模型调优再到推理部署踩了不少坑也沉淀了一些可复用的经验。这篇就把整个流程和关键细节掰开揉碎讲清楚尤其适合准备入门自动驾驶感知、或者想把手头检测模型换到YOLOv8上的开发者参考。KITTI数据集虽然上线有些年头了但它至今仍是学术界和工业界做道路目标检测最常用的真实场景数据之一道路环境、光照变化、目标遮挡和小目标密度都相当贴近实际。YOLOv8相比之前版本改动很大anchor-free检测头、C2f特征提取模块、Task-Aligned标签分配策略一套组合下来在COCO上的表现确实比v5强一截而且工程化程度高训练和部署都很顺畅。这篇项目的核心就是把两者结合起来走通一条从原始KITTI标注到YOLOv8训练格式的完整链路针对红绿灯这类小目标做专项优化最后给出在嵌入式设备上的部署建议和实测数据。1. 项目整体设计与技术选型1.1 自动驾驶目标检测的三大难点先聊一下为什么这个任务不是“拿个训练集跑个模型”那么简单。自动驾驶场景里的目标检测有三个很现实的问题第一是目标尺度跨度极大图像里一辆公交车可能占几百像素而一个远处的红绿灯可能只有十几个像素小目标召回率很容易拖后腿。第二是类别不均衡KITTI里车辆类别的标注数量远多于行人和红绿灯模型天然偏向学“车”行人和红绿灯的召回率就容易被牺牲。第三是遮挡和截断真实道路场景里行人被车辆挡住、车辆被路牌挡住太常见了这对模型的特征提取能力和NMS参数都很敏感。这三个问题在KITTI上都存在在真实业务里只会更严重。所以我在设计这个项目时没有单纯追求“训练个模型跑出mAP多高”而是把重点放在类别怎么映射、小目标怎么做专项优化、类别不均衡怎么缓解、以及训练策略怎么配合。这些才是实战里真正花时间的地方。1.2 为什么选YOLOv8而不是YOLOv5或其他方案选型这件事我纠结过一段时间。YOLOv5生态成熟资料多但它的anchor-based检测头在红绿灯这种极端小目标上并不占优而且标签分配策略相对粗糙。YOLOv8最大的区别是换成了anchor-free的Decoupled Head分类和回归分支解耦配合Task-Aligned Assigner做标签分配。实际测试下来同等模型体积下v8在KITTI这种场景上的收敛速度和最终精度都比v5好一些尤其是行人这类中等尺寸目标。另外还有一个重要考量YOLOv8的代码结构非常清爽配置文件和数据接口很规范后续做P2小目标检测头、改损失函数权重、接TensorRT导出都更方便。对于项目周期有限的团队来说这种工程友好度比“理论上限更高但调起来费劲”的方案划算得多。当然如果你要追求极致性能YOLOv8不是唯一选择但在“快速落地方案并获得不错效果”这件事上它目前是最均衡的。1.3 KITTI数据集的取舍分析KITTI数据集包含7481张训练图像和7581张测试图像图像分辨率约1242×375内容来自德国卡尔斯鲁厄的真实道路采集。训练图像带完整标注测试图像只有图像不公开标注所以常规做法是再把训练集按比例切出一部分来当验证集比如我这次是6144张训练、1337张验证大概8:2。KITTI原生标注的类别有Car、Van、Truck、Pedestrian、Person_sitting、Cyclist、Tram、Misc、DontCare九类这里有几个坑要注意KITTI里没有现成的红绿灯类别这是很多新手第一次跑这个任务时容易懵的地方。我在项目里的处理方式是在原始KITTI图像基础上用标注工具对包含交通信号灯的区域做了补充标注形成一个traffic_light类别同时把语义相近的原始类别做了合并映射。具体映射关系后面详细说。2. 数据集预处理从KITTI原生格式到YOLO训练格式2.1 KITTI标注体系与YOLO格式的差异KITTI的标注文件是纯文本格式每行代表一个目标字段按顺序是类别、截断率、遮挡状态、观测角度、2D检测框坐标x_left, y_top, x_right, y_bottom、3D尺寸height, width, length、3D中心位置和绕Y轴旋转角。对于2D目标检测任务来说我们只关心前五个字段也就是类别和2D框的四个像素坐标。YOLO系列要求的标签格式完全不同每行是“类别id x_center y_center width height”其中x_center、y_center、width、height都是归一化到[0,1]的数值坐标系以图像左上角为原点。也就是说坐标转换的核心就是把KITTI的左上角右下角表示法转换YOLO的中心点宽高表示法同时除以图像的宽和高做归一化。2.2 类别重映射与样本统计我之前说过KITTI原生类别很杂如果直接拿九类去做训练小类别样本量太少根本学不动。所以第一步是类别映射这也是整个数据预处理里最关键的一步。我采用的映射策略是Car、Van、Truck合并为car回归到“机动车”语义Tram轨道电车样本太少直接剔除也有做法是归入car但我实测剔除后更稳Pedestrian、Person_sitting合并为pedestrianCyclist保留为cyclistMisc、DontCare直接剔除补标的红绿灯单独作为traffic_light映射完成后我统计了一下样本量级car大约28000个目标pedestrian大约6000个cyclist大约1600个traffic_light补标后只有800个左右。很明显traffic_light的样本量是四大类别里最少的骑行者也不多这两个类别后面注定是优化重点。2.3 标注格式转换脚本详解转换脚本其实不复杂核心逻辑就是读取KITTI文本行解析类别和坐标做映射和归一化写入新的txt文件。但有几个细节必须处理否则训练时会出现莫名其妙的问题。第一是越界处理。KITTI的部分标注框会超出图像边界我先做了clamp操作把坐标限制在图像范围内同时过滤掉clamp后宽度或高度小于3像素的框这种框在resize之后基本就看不见了留着只会给损失函数增加噪声。第二是空标签文件。有些图像里确实没有任何目标对应的txt文件是空文件这是正常的但检查代码时容易误以为数据处理出错了。我在脚本里对这种情况做了兼容。第三是文件命名对齐。KITTI图像文件一般是000000.png这种形式转换后的标签txt文件必须和图像文件同名且放在对应目录下。这个看起来简单但实际项目中因为大小写不一致、路径拼接错误导致标签和图像错位的情况非常常见。这里顺手贴一下我用的转换核心逻辑基于常见实践封装import os import numpy as np from PIL import Image KITTI_CLASSES { Car: 0, Van: 0, Truck: 0, Pedestrian: 1, Person_sitting: 1, Cyclist: 2, } def convert_kitti_label(txt_path, img_width, img_height): boxes [] with open(txt_path, r) as f: lines f.readlines() for line in lines: parts line.strip().split() if len(parts) 5: continue cls_str parts[0] if cls_str not in KITTI_CLASSES: continue x_left, y_top, x_right, y_bottom map(float, parts[4:8]) x_left max(0, min(x_left, img_width - 1)) x_right max(0, min(x_right, img_width - 1)) y_top max(0, min(y_top, img_height - 1)) y_bottom max(0, min(y_bottom, img_height - 1)) if (x_right - x_left) 3 or (y_bottom - y_top) 3: continue x_center ((x_left x_right) / 2) / img_width y_center ((y_top y_bottom) / 2) / img_height w (x_right - x_left) / img_width h (y_bottom - y_top) / img_height boxes.append([KITTI_CLASSES[cls_str], x_center, y_center, w, h]) return boxes这里要强调一点补标的traffic_light类别id是3这段代码里我没有合并到KITTI_CLASSES里因为补标数据如果用的是KITTI原生标注格式需要单独解析并且不能走“不在映射表就跳过”的逻辑。我的做法是补标时直接把标签输出成YOLO格式从源头上绕开了二次转换的问题。如果你也准备补标红绿灯强烈建议直接用支持YOLO格式导出的标注工具省去后面一堆麻烦。2.4 红绿灯类别的特殊处理红绿灯这个类别在整个项目里是一个特殊存在因为它有两个特性目标极小、颜色语义重要。在图像里一个红绿灯往往只有20×30像素甚至更小而YOLOv8在640分辨率下的特征图最小stride是32理论上一个目标在高层特征图上可能只剩1个点很容易漏检。针对这一点我在补标时选择了标注灯体外框含灯罩而不是整个交通灯杆。如果标注整个灯杆和灯板目标尺寸会变大但语义会变得模糊——模型会把“灯杆”当成特征而不同场景的灯杆形态差异很大泛化能力反而变差。标注灯体外框还有一个额外好处后续如果要扩展红绿灯颜色状态分类基于灯体外框做ROI裁剪会非常方便。另外红绿灯样本的分布很不均匀。有的图像里三个灯全亮有的只有一个黄灯在闪如果只看数量不看场景多样性模型很容易见过某一类红色灯特别多而绿色灯特别少导致颜色预测偏置。我的做法是标注时尽量覆盖不同角度、不同光照、红黄绿三种状态都有的样本宁可数量少一些也要保证状态分布均匀。3. 模型配置与全流程训练实操3.1 环境准备与基础验证训练环境这块其实没什么玄学PyTorch版本和CUDA版本对应上就行。我这次用的是PyTorch 2.x和Ultralytics的YOLOv8官方库代码拉下来后先拿官方COCO预训练权重跑一个最小demo确认环境没问题再开始正式训练。有一个小建议先在COCO预训练权重上做fine-tune而不是随机初始化训练。虽然KITTI的域和COCO差别不小但COCO里包含大量车辆、行人、骑行者这些特征对KITTI任务依然有效。实测下来COCO预训练权重做初始化能让收敛速度提升30%以上最终精度也更好。红绿灯这个类别COCO里没有但模型可以通过其他类别的特征迁移来加速学习。3.2 数据集配置文件编写YOLOv8的数据集配置是一个yaml文件指定路径和类别列表。有几个值得注意的细节train: ./datasets/KITTI/images/train val: ./datasets/KITTI/images/val nc: 4 names: [car, pedestrian, cyclist, traffic_light]这里有个容易踩的坑路径写相对路径时标准训练命令里的数据路径是相对于你执行命令的目录解析的。我建议直接用绝对路径或者把yaml文件放在项目根目录下统一管理否则换了机器或者换了目录之后报错“image not found”的概率很高。类别顺序一旦定下来就不要改因为它直接映射到标签txt里的类别id。如果你训练到一半想加一个类别就必须重新生成所有标签别想着“在线改一下names就行”这个坑我踩过一次改完类别顺序后模型训练仍然收敛但推理结果全乱了。3.3 模型结构与超参数初始化模型我选的是YOLOv8s不是最轻的nano也不是最重的large。原因很简单nano在红绿灯上的能力太弱large在单卡训练和小目标优化迭代时太慢。v8s在参数量、速度和精度之间比较平衡先拿它跑通全流程后面再根据瓶颈决定是轻量化还是增量化。训练超参数我初始配置如下这是我在类似项目上反复试过比较稳的一套起点epochs: 150 batch: 16 imgsz: 640 optimizer: AdamW lr0: 0.0005 weight_decay: 0.0005 warmup_epochs: 3 close_mosaic: 10这个参数组合有一个值得解释的地方lr0设置在0.0005而不是默认的0.01。YOLOv8默认优化器是SGD对应的默认学习率是0.01但如果切换成AdamW学习率还保持0.01就会明显震荡。我实际测下来AdamW配0.0005在KITTI这个数据量上收敛平稳loss曲线没有明显反弹。3.4 训练执行与监控解读启动训练后最需要关注的是三个lossbox_loss、cls_loss、dfl_loss。不要只看总loss分开看才能定位问题。比如cls_loss降得慢说明类别特征学得不好box_loss震荡说明回归分支学习率偏高。训练过程中我还会盯一个关键指标验证集P和R。P高R低说明模型“宁缺毋滥”漏检多P低R高说明误检多。对于自动驾驶场景漏检比误检严重得多所以我会优先保证Recall再通过调NMS的参数去拉Precision。这个原则在后续优化里会反复用到。训练日志里还有一个容易被忽视的信息每个epoch最后几行会输出每个类别的AP。不要只看mAP一定要关注traffic_light的单独AP因为mAP会被car这类大样本类别拉平红绿灯的真实表现必须看单项指标。4. 检测精度深度优化从77%到89%的迭代记录4.1 数据增强策略的加法实验第一个优化方向是数据增强。YOLOv8默认开启Mosaic、MixUp、HSV变换、随机翻转等一系列增强这些增强在COCO这类通用场景上表现很好但在KITTI这种小目标多的场景上Mosaic反而可能帮倒忙。原因很简单Mosaic会把四张图拼在一起再随机裁剪红绿灯这种本身就极小的目标经过Mosaic裁剪后很可能直接消失等于给模型提供了大量“该目标不存在”的错误监督信号。我的处理方式是分阶段调整前中期保留Mosaic但把概率从默认的1.0降到0.5让模型不至于完全失去上下文信息同时在最后10个epoch用close_mosaic参数强制关闭Mosaic让模型在训练后期专门适配原始尺度的目标分布。这一步做完traffic_light的AP50直接涨了5个百分点是整轮优化里单点收益最大的一次改动。HSV增强我适当调高了饱和度和色调的幅度因为红绿灯颜色在不同光照条件下的变化很大白天和黄昏的颜色差异肉眼可见。增强幅度加大会让模型对颜色扰动更鲁棒但也不能调太大否则红色灯会被增强成紫色反而引入了错误语义。4.2 小目标与远距离目标的专项优化小目标专项优化有两条路线同时走提升输入分辨率和增加P2检测头。提升分辨率是最直接的手段。我将训练分辨率从640提升到960代价是训练时间增加了约60%但检测器在小目标上的召回率提升效果立竿见影。对于部署阶段推理分辨率保持训练分辨率或略低于训练分辨率但至少不低于640否则红绿灯的漏检率会明显回升。增加P2检测头是另一个有效手段YOLOv8原始结构从P3到P5stride分别是8、16、32而P2意味着在stride为4的特征图上额外增加一个检测头。P2特征图分辨率高包含更多小目标的细节信息非常适合红绿灯检测。但代价是计算量增加明显我在实际项目中是用yolov8s加了P2头速度还能接受。对于性能敏感的设备建议先跑通pipeline再权衡。有一个配套技巧值得提在模型yaml配置中开启P2后需要同步调整anchor-free目标的分配阈值因为更高分辨率的特征图会对目标中心点离得远的样本更敏感需要更宽松的匹配距离。如果不调P2头很容易学不到东西。4.3 类别不均衡的损失函数调整traffic_light样本量只有800个左右这是整个项目里最棘手的类别不均衡问题。YOLOv8默认的BCE分类损失没有显式的类别权重小样本类别很容易被大样本类别淹没。我的做法有两层。第一层是采样层面对包含红绿灯的图像在数据加载时做了轻微的过采样让traffic_light样本在每次epoch中出现的频率更高。这个可以通过自定义Dataset实现代价是训练时间略微增加。第二层是损失权重层面我在cls_loss上针对traffic_light类别单独加了1.5倍的权重系数让模型对这个类别的错分付出更高代价。注意这里不能把权重加得过大否则模型会偏向红绿灯而牺牲掉车辆和行人的精度。我记得调参过程中有过一次权重放到2.5之后traffic_light的AP确实上去了但car的AP掉了近2个百分点后来又调回1.5才平衡过来。类别不均衡还有一剂猛药是Focal Loss思想但YOLOv8内部已经有类似的机制在起作用自己实现一个Focal Loss替换默认分类损失需要不少改动建议在基础调优做完之后还有余力再尝试。4.4 训练策略细节Warmup、EMA与余弦退火训练策略对结果的提升是“润物细无声”的不会像数据增强那样出现几个点的大跳变但对最终精度的上限有直接影响。Warmup阶段YOLOv8默认是3个epoch在这个阶段学习率从很小逐步爬升到设定值。我试过把warmup拉长到5个epoch对红绿灯这种小目标类别有一点好处因为小目标特征在初期非常不稳定模型需要一个更平缓的启动过程。但拉太长会浪费训练时间整体收益不太划算最终我还是保留3个epoch。EMA指数移动平均是YOLOv8默认开启的机制它会让模型参数取历史平均而不是当前值相当于给模型加了一层平滑正则化。EMA的衰减系数默认是0.9999一般不用动但要注意它和最后阶段的评估一致性。在关闭Mosaic和MixUp的后期阶段EMA的权重直接影响最终模型的表现如果发现后期验证指标有小幅震荡把衰减系数调到0.9998往往能压下来。余弦退火学习率调度也是默认配置这个策略前期下降快、后期缓慢逼近最低点比较适合KITTI这种中等规模数据集。我的经验是学习率整个训练周期的末尾会降到初始值的1%左右这时候模型已经在细节上收敛再继续训练提升空间不大。150个epoch在KITTI上基本够用继续延长到200个epoch收益有限。5. 实战中的坑与排查手册5.1 标注转换踩坑实录标注转换这个环节看着简单实际是最耗时间的部分。我遇到过一个诡异的bug转换完的标签里所有car类别的x_center都集中在一块区域但可视化之后发现框根本没有对齐。排查了半天才发现是KITTI原标注文件里第4和第5个字段不是x_left和y_top而是观测角度alpha不同版本的KITTI标注格式有细微差异解析时字段位置搞错了。另外KITTI标注文件里的坐标是像素坐标但KITTI原始图像在公开下载时有的会带有裁剪区域裁剪之后图像宽高变了坐标范围对不上。这种情况需要先读取实际图像尺寸再基于真实尺寸做归一化不能直接拿固定的1242×375去算。我的脚本里每次转换都会用PIL读取图像真实尺寸简单可靠。5.2 训练不收敛问题定位训练不收敛最典型的表现是loss在某个值附近震荡就是不降或者降到一定程度开始反弹。常见原因不外乎三个学习率过高、标签格式错误、类别id错位。学习率过高导致的loss震荡可以通过把AdamW的lr0从0.0005降到0.0002验证一般两个epoch就能看到变化。标签格式错误的表现比较隐蔽——loss能降但mAP一直为0这时候要检查标签txt里是否存在归一化后大于1的数值或者类别id超出nc范围。我建议在训练前写一个几十行的小脚本随机抽取几张训练图把标签画上去人工检查一遍这一步能省下一整晚的排查时间。类别id错位的问题通常发生在你改过类别映射表之后旧标签和新模型配置对不上。这种问题在验证集上表现成某个类别的recall一直为0而且只影响特定类别。5.3 mAP与直观效果不一致的原因训练结束后经常会遇到一个困惑验证集mAP看着还行但拿视频去跑效果总觉得不对。这里面的原因通常是验证集和训练集同源KITTI本身的场景有一定单一性都是同一辆车采集的道路模型看到了太多相似场景对域外场景的泛化能力其实有限。解决这个问题最朴素的方法是交叉验证也就是拿一部分KITTI训练数据做训练另一部分做验证但最终还是要拿到真实道路视频上测试。我在项目中做了一步aB面测试用KITTI训练得到的模型直接去跑了一段自己录制的城市道路视频发现红绿灯的漏检率明显比KITTI验证集上高。这个落差不是模型不好而是数据域的天然差异不必过度焦虑但要有心理预期。5.4 部署推理性能优化建议训练完成之后一般要导出部署格式我的首选是ONNX加TensorRT。YOLOv8官方库的导出接口已经封装好了直接用torch.onnx导出然后通过TensorRT做FP16量化。在嵌入式设备上FP16相比FP32几乎没有精度损失但推理速度能提升80%左右。如果你在嵌入式设备上跑还有一个优化点值得关注NMS部分在TensorRT上会有点耗时。可以考虑把检测头的topk数量从默认的300降到100因为自动架势场景每帧的目标数量通常远小于300减少候选框可以显著降低NMS的耗时。实测这一步对帧率提升的帮助甚至比模型量化还明显。另外视频流测试时如果发现检测框在连续帧之间跳动建议在输出端加一个轻量级跟踪器做平滑比如ByteTrack。它不增加太多计算开销但能让检测框的稳定性看起来好很多这在做演示或实车部署时体验差异很大。最后分享一点心得整个流程走下来有个体会挺深YOLOv8在KITTI这类中等规模数据集上的训练流程已经相当成熟真正决定项目成败的往往不是模型结构本身而是数据处理的颗粒度和小目标的专项优化。KITTI的mAP只能算一个基准线实际场景中红绿灯的漏检才是最大的对手建议你在自己的数据上多花时间做补标和清洗。最后再分享一个实用技巧训练和验证的数据集划分最好按“采集序列”切分而不是按图像切分避免同一段视频的连续帧同时出现在训练集和验证集里否则验证指标会虚高得让你产生错觉。这个细节决定了你后续调优的方向是否靠谱。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑