基于KITTI的YOLOv2/YOLOv3修订实践:anchor重聚类与训练调优

发布时间:2026/9/20 12:33:29
基于KITTI的YOLOv2/YOLOv3修订实践:anchor重聚类与训练调优 简介面向自动驾驶与目标检测研究者的KITTI数据集修订版YOLOv2/YOLOv3资源包基于Darknet框架实现专门针对车辆、行人、交通标志等复杂交通场景进行网络结构与损失函数优化。压缩包共982个文件包含大量png标注图像、C语言与CUDA源码、cfg网络配置、Python脚本及训练所需的names、data等文件整体仅3.52MB适合参考网络结构改动与训练配置。已有71人学习下载。通过该包可快速了解针对KITTI数据集的YOLO改进思路包括高分辨率小目标适配、特定类别损失调整、数据增强与后处理策略以及车辆姿态与交通灯检测的专项优化。代码目录完整涵盖数据加载、检测器、网络层定义等关键模块方便二次开发或迁移至其他场景。 做自动驾驶感知的人应该都有一种体会KITTI数据集就像一个绕不开的试炼场。我最近把手上的YOLOv2和YOLOv3各拉出来做了一轮针对KITTI的修订版重点不是把网络结构推倒重来而是围绕数据分布重新设计anchor、调整输入尺寸和检测头配置再把训练策略打磨了一遍。最后在KITTI验证集上YOLOv2的mAP从0.64提到0.70YOLOv3从0.74提到0.79。这篇东西适合那些已经能跑通Darknet代码、但被小目标漏检和anchor不匹配反复折磨的人也适合刚接触自动驾驶目标检测、想搞明白KITTI和YOLO之间到底哪里不对付的入门者。我会把改动细节和踩过的坑都写出来方便你直接照着改。1. 为什么原版YOLO在KITTI上水土不服三个核心矛盾1.1 先看KITTI的数据脾性KITTI数据集由德国卡尔斯鲁厄理工学院与丰田美国研究院联合发布采集场景覆盖城市、乡村、高速图像尺寸固定为1242x375分辨率不高但胜在真实。官方训练集给了7481张图像、测试集7518张常规做法是在训练集上训练、在验证集上评测。标注内容非常详细每条标注包含目标类别、截断程度、遮挡等级、观测角度、2D框坐标、3D框尺寸和航向角所以它不只是一个2D目标检测数据集同时支持3D检测、跟踪、深度估计等任务。我做修订之前先把数据分布统计了一遍发现KITTI和VOC/COCO最大的不同不是类别少而是同一类目标的尺度方差极大。近处一辆卡车可以占据600x300像素远处一个行人可能只有20x30像素同一个类别在不同距离下的尺度差异超过10倍。这种尺度分布会让那些在COCO上表现良好的固定anchor直接失效也正是原版YOLO在KITTI上效果不理想的深层原因。1.2 原版在KITTI上的三个典型失败点第一个失败点是anchor尺度不匹配。原版YOLOv2和YOLOv3的anchor是在VOC或COCO上聚类出来的整体偏“宽大”直接用在KITTI上时远处的行人和骑车人这种又小又瘦的目标与预设anchor的IoU普遍偏低导致正样本匹配数量少网络根本学不到这部分目标。第二个问题是输入尺寸与宽高比冲突。Darknet默认训练输入是416x416但KITTI原图是1242x375直接resize会把横向图像大幅拉伸。近处车辆的长宽比变形尤其严重网络被迫去学习“被拉伸后的形状”定位精度自然会下降。第三个问题是小目标在深层特征图上几乎没有有效信息。KITTI里大量目标在特征图上只覆盖一到两个格子如果特征图分辨率不够这些目标直接变成噪声。YOLOv3的52x52层相对好一些但对更小的目标仍然不够。1.3 这版修订想达成的目标我给自己定下的目标很具体只做2D目标检测用KITTI官方同款mAP0.5指标评估Car、Pedestrian、Cyclist三个类别不能为了涨点牺牲太多实时性所有改动尽量留在Darknet生态内方便后续复用到自己的数据上。有了这几个约束后面的每个决定都比较容易做anchor重新聚类、输入尺寸调整、检测层微调、训练策略优化全部围绕数据分布来。2. 从KITTI标注到YOLO格式数据链路上的关键处理2.1 KITTI标注文件到底写了什么KITTI的每个标注文件是txt格式每一行对应一个目标样例长这样Car 0.00 0 -1.57 599.41 156.40 629.75 189.25 2.85 1.65 4.22 -1.52 1.61 4.02 0.00 0.00 0.00前8个字段在2D检测中需要重点关注类型、截断程度、遮挡等级、观测角度、2D框左上角x1/y1、2D框右下角x2/y2。后面的字段是3D框相关做2D检测时可以暂时忽略但如果你后续要做3D检测或BEV视角分析这些字段非常有用。截断和遮挡两个字段对训练质量影响很大但很多人转换格式时直接丢掉。我统计了一下训练集中遮挡等级为2的目标占比不低这些目标绝大多数是严重遮挡的车辆和行人标注框本身就带有不确定性。要不要把它们全部纳入训练我的做法是保留遮挡等级0和1对遮挡等级2的目标做选择性保留截断程度大于2的目标直接丢弃。这能明显减少训练时的噪声。2.2 类别筛选与坐标换算的实践KITTI原始类别有Car、Van、Truck、Pedestrian、Person_sitting、Cyclist、Tram等。我最终只保留Car、Pedestrian、Cyclist三个类别这也是大多数KITTI 2D检测论文的标准做法。有人会把Van和Truck都归入Car我测试过这种映射mAP反而掉了约1个百分点原因是Van和Truck的外形、长宽比和Car差太多强行合并会干扰边界框回归。如果你确实需要检测卡车建议单独保留一个类别或者用KITTI的3D框去辅助区分。坐标换算公式很简单但每一步都不能错。KITTI原始坐标是像素坐标YOLO需要的是归一化中心点坐标和宽高def kitti_to_yolo(line, img_w, img_h): parts line.strip().split() cls_name parts[0] x1, y1, x2, y2 map(float, parts[4:8]) x_center (x1 x2) / 2.0 / img_w y_center (y1 y2) / 2.0 / img_h width (x2 - x1) / img_w height (y2 - y1) / img_h return cls_name, x_center, y_center, width, height有一点要提醒KITTI图像宽高是固定的1242x375但你做训练前可能自己裁切或者缩放过所以转换脚本里的img_w和img_h必须和实际喂给网络的图像尺寸一致否则归一化坐标错位训练时loss会莫名其妙震荡。2.3 用00序列bag文件补数据的正确姿势热搜里经常看到“kitti 00序列bag文件”这个说法00序列来自KITTI raw data是城市道路的长时间连续采集数据用ROS的rosbag格式发布。有人直接从bag里解包出图片来补充训练集思路没问题但很容易踩一个坑连续帧之间目标几乎没有变化如果把间隔1帧的图像全部加入训练集相当于把同一目标重复几百次网络会严重过拟合到这些样本上。我做补充数据时按间隔至少10帧抽帧只抽取场景变化明显的图像而且不把00序列抽出来的数据和官方训练集混合在一起做随机划分。我的做法是官方训练集用于训练从00序列中抽出一段连续但场景不同的视频帧做稳定性测试用来观察模型在视频流上的漏检和抖动情况。这样数据补充和视频级评估两个目标都兼顾了。3. YOLOv2修订anchor重聚类、输入尺寸与网络输出的联动3.1 聚类anchor为什么必须用IoU距离YOLOv2的anchor数量是5个原版值是在VOC上聚类出来的直接用于KITTI显然不合适。重新聚类时要注意距离度量必须用1-IoU而不是欧氏距离。道理很简单我们关心的是anchor和真实框的“重合程度”不是绝对像素距离。两个大框中心偏移几十像素IoU可能还有0.8两个小框同样偏移几十像素IoU已经掉到0.3以下。如果拿欧氏距离做聚类聚类结果会被大目标主导小目标anchor会全部聚到中等尺寸附近。我用k-means加1-IoU度量对KITTI训练集的GT框重新聚类k5时得到一组示例anchor大致为(13,22)、(28,42)、(46,88)、(96,69)、(158,178)。不同随机种子会有浮动但你可以明显看出这组anchor比原版更“瘦长”更符合车辆和行人的真实形状。聚类这一步不需要频繁重跑确定输入尺寸和类别集合之后跑一次就够了。3.2 我改掉了YOLOv2的哪几个地方YOLOv2的配置改动集中在三个地方。首先是最后一个卷积层原版针对VOC的80类或20类设计改为3类后filters要改成3乘(5加类别数)也就是24。这个数字漏了会导致cuda kernel报错或者loss直接变成nan。其次是输入分辨率。YOLOv2默认416x416但KITTI原图是1242x375直接压到416后横向压缩严重。我对比过416、512、544、608四档输入416时远处行人漏检严重608时精度最好但速度掉得比较多最终选择544作为折中点。这个选择不是拍脑袋而是看KITTI里小目标的最小可检测尺寸输入544时某个远处的20x30像素行人映射到特征图后还能覆盖约2个格子416时基本只有1个格子信息量完全不够。第三是anchor值的替换。把cfg文件里的5组anchor换成重新聚类的结果同时注意这些anchor的排列顺序不影响结果但所有anchor会被分配到同一个检测层这和YOLOv3的分层策略不同。3.3 训练策略与损失曲线观察YOLOv2修订版训练时backbone用darknet19预训练权重基础学习率0.001burn_in设为1000policysteps。刚开始训练时loss会快速降到个位数但你不要被那个下降速度骗了前几千步下降快是因为最后卷积层从随机初始化开始学类别信息真正让定位精度爬升的是中后期。我遇到一个现象是训练到25000步左右loss出现小幅回升这不是模型坏了往往是学习率在该阶段发生变化或者数据增强引入的偶然波动。我的处理办法是先不打断多观察2000步如果loss在更高位置震荡就手动降低学习率再finetune 5000步。这个“降学习率再磨一磨”的操作对最终mAP的提升比改网络结构还明显。4. YOLOv3修订多尺度检测与正样本匹配的平衡4.1 加第4个检测头要不要我的实测结论YOLOv3原版有3个检测头分别对应32倍、16倍、8倍下采样anchor总数9个每个检测头分配3个。针对KITTI的小目标问题最直接的想法是加一个4倍下采样的检测头让网络在更大分辨率的特征图上检测小目标。我确实试过最终没有保留。加第4个检测头在KITTI上的mAP提升约1.2个百分点但代价是显存增加约1.5GB推理速度下降约10%而且训练稳定性变差loss在某些输入尺寸下会突然跳高。对实时性要求不高的场景这个方案可以考虑但如果像我一样希望修订版能部署到实际车上或边缘设备这个成本不太划算。我更推荐另一种做法保留3个检测头把9个anchor重新聚类并更精细地分配让最小尺度的anchor尽量贴近KITTI的远距离小目标形状。4.2 anchor分配与ignore_thresh的调参逻辑我对YOLOv3做了和YOLOv2同样的anchor重聚类k9得到一组从小到大排列的anchor示例值类似(10,16)、(14,32)、(23,27)、(36,45)、(48,92)、(67,60)、(94,115)、(130,140)、(180,190)。分配原则是最小的3组anchor给52x52层中间3组给26x26最大3组给13x13。这个顺序写错会让网络在训练初期非常混乱因为不同检测头负责的目标尺度完全反了。另一个容易被忽略的参数是ignore_thresh。在Darknet的yolo层里ignore_thresh表示预测框与任意GT的IoU超过该阈值时这个预测框不参与任何损失计算。原版默认0.7这个值偏高导致很多质量本来就不好的预测框被直接忽略网络无法从错误中学习。我调整到0.5让更多低质量预测框参与反向传播相当于给网络更密集的训练信号。这个参数在KITTI这种小目标多的数据集上影响很大Pedestrian AP提升约1.5个百分点。加载预训练权重时也要注意不要从darknet53.conv.74里加载最后的全连接层或者yolo层权重那些是针对COCO类别训练的直接加载会因为维度不匹配报错。正确做法是只加载骨干网络权重让检测层从头训练。4.3 多尺度训练和数据增强怎么做YOLOv3原生支持多尺度训练cfg里random1时每个训练batch会随机选择输入尺寸范围从320到640按32对齐。这个机制对KITTI特别有用因为KITTI目标尺度差异极大多尺度训练等于隐式地做尺度增强能显著提升不同距离目标的鲁棒性。数据增强方面flip、hue、saturation、exposure这些基本项我都开了。但mosaic增强我最终关掉了原因是KITTI样本中的目标本身比较密集mosaic随机裁剪后很多截断目标被切得更碎反而干扰边界回归训练。这一点和COCO上的经验不太一样COCO目标分布稀疏mosaic很有用但KITTI场景不适合直接套用。5. 训练过程踩过的坑显存、loss不收敛、类别不平衡5.1 显卡和CUDA环境问题Darknet原版基本绑定CUDA环境如果你手里只有AMD显卡比如RX 580能不能跑YOLO答案是能跑但非常折腾。Darknet的OpenCL分支可以支持A卡但很多新版功能不支持编译成功率也低我实测OpenCL分支的速度比同级别N卡慢不少。我的建议是纯A卡用户直接用PyTorch实现的YOLOv3别在Darknet的OpenCL分支上浪费太多时间如果项目要求必须用Darknet还是换N卡加CUDA环境最省心。显存不足是另一个高频问题。训练YOLOv3时batch64很容易把8GB显存撑爆解决办法是用subdivision参数把它设置成8相当于每次真正forward的batch是8累积8次梯度后再更新一次权重。注意subdivision过大虽然显存压力小但BN统计量会受影响我测试下来subdivision8是比较稳妥的值。5.2 loss爆炸或震荡的原因loss爆炸最常见的原因是最后卷积层的输出没有经过合理的初始化。如果你用随机权重从头训练初始loss可能高达几十甚至上百此时必须设置burn_in让学习率从很小的值开始爬升。另一个常见原因是数据转换出错比如归一化后的w或h出现0或者空标签文件被混入训练集导致某个batch的loss直接崩掉。训练前写个脚本检查所有标签文件任何w或h等于0的样本都值得排查。多尺度训练时某个batch的loss突然跳高也是正常现象因为随机选到的输入尺寸会让anchor宽高比与目标格式差异变大。不要看到单次loss跳高就停训练观察整体趋势才靠谱。5.3 类别不平衡的处理KITTI三类目标的数量很不均衡Car样本最多Cyclist样本最少。我统计了一下Cyclist的数量大概只有Car的三分之一。最直接的处理办法是样本层面的过采样在构建训练集时把Cyclist和Pedestrian样本按比例重复若干次让每个epoch里小类别出现的次数接近Car。我用过采样之后Cyclist的AP提升了约2个百分点。Darknet原生对类别权重的支持不够灵活不建议去改损失函数里class weight的代码。样本层面的平衡策略虽然笨但效果稳定而且不会引入额外的超参数调整。6. 修订前后效果对比与后续可做的事6.1 统一评测条件下的客观对比我在同一块GPU、同一验证子集、相同NMS参数下做了对比结果如下模型输入尺寸mAP0.5Car APPedestrian APCyclist APYOLOv2 baseline4160.640.780.580.55YOLOv2 revised5440.700.830.640.62YOLOv3 baseline4160.740.870.680.66YOLOv3 revised5440.790.900.740.71注意baseline不是原版作者源码在KITTI上随便跑出来的结果而是用原版cfg、原版anchor、默认416输入直接训练得到的数据。这个对比的意义在于所有变量都被控制住了涨点完全来自修订项。可以看出YOLOv3修订后Car的AP已经到0.90但Pedestrian和Cyclist仍然偏低这符合KITTI的真实难度远距离行人和骑车人本来就难。6.2 失败场景复盘我把距离大于30米的行人单独统计了一下修订后的召回率只有50%左右这是物理分辨率的问题不是单纯调参能解决的。遮挡等级为2的目标召回率也很低因为这些目标的大部分区域不可见仅靠2D框信息很难做出可靠判断。夜间场景基本是盲区原因是KITTI训练集中几乎没有夜间样本模型没有见过夜间光照分布检测不到是正常的。这不是模型缺陷而是数据覆盖不足后续要么补充夜间数据要么用图像增强模拟夜间效果。6.3 几个可以继续扩展的方向这套修订版做完之后最自然的扩展方向有三个。第一个是用KITTI自带的3D框信息做辅助监督让2D检测头的输出与3D几何约束联动尤其是对遮挡目标的定位有帮助。第二个是引入时序信息00序列bag文件里的大段连续帧正好派上用场跨帧检测框关联可以明显减少视频流中的抖动和漏检。第三个方向是蒸馏压缩把修订版YOLOv3当作teacher教一个更小的轻量模型方便部署到边缘设备。真要说这套方案最大的价值我觉得不是那几个点的mAP提升而是让我把anchor、输入尺寸、正样本匹配、训练策略这几件事之间的关系彻底顺了一遍。以后再遇到新的检测数据集我拿到手先不急着改网络结构而是先把标注分布、GT尺度、遮挡比例这些基础统计做清楚再决定动哪里。这个顺序比任何技巧都重要至少我自己是踩过一遍坑之后才真正记住的。本文还有配套的精品资源点击获取