最近在整理一个做了一段时间的道路交通多目标检测项目,也就是标题里那个“_1”版本——基于RetinaNet的道路交通多目标检测与识别系统,目前已经完成第一版,能在真实道路视频流里同时识别车辆、行人、非机动车和交通设施等多类目标,并且在工程落地层面跑通了从数据准备到推理部署的完整链路。这篇文章想把选型逻辑、原理拆解、训练调参和踩坑全过程写清楚,给正在做类似目标检测项目的朋友一份可参考的“成长记录”。这类项目的目标很明确:给定一段道路监控或车载摄像头画面,系统需要同时输出画面里所有目标的类别和位置框,而且要在车流密集、行人穿插、目标尺度差异极大的情况下尽量不丢框、不错框。RetinaNet在这个场景里算是一个非常稳妥的起点,尤其是它处理“小目标”和“前景背景样本不平衡”这两件事的思路,刚好对应道路交通场景里最常见的两个痛点。下面从最初选型开始讲。1. 为什么选RetinaNet做道路交通多目标检测——选型时的一笔账先交代一下背景。第一版的目标不是追求某个比赛级别的精度,而是要在可控的开发周期内搭出一个真正可运行、可评估、可继续迭代的多目标检测基线。既然方向锁定在“道路交通”这个场景,一开始就要把场景特点拆清楚:目标类别多且大小悬殊、密集遮挡频繁、背景复杂且占像素比例极大。这几个特点直接决定了模型选型的方向。当时摆在我面前的无非是几类主流方案:两阶段检测器(Faster R-CNN系列)、单阶段检测器(SSD、YOLO系列)、以及基于关键点或Transformer的新方案。我最终选了RetinaNet,核心原因有三条。第一,RetinaNet是单阶段模型,推理速度快,工程上更容易满足道路场景的实时性要求。道路交通检测往往要部署在边缘设备或车载工控机上,算力预算有限,两阶段模型精度虽好但速度压力很大。第二,RetinaNet的骨干网络采用特征金字塔结构,能拿到从高分辨率浅层到低分辨率深层的多尺度特征图。道路场景里远处行人可能只有几十个像素,而近处公交车可能占满大半个画面,这种多尺度目标同时出现的情况,恰好是特征金字塔擅长处理的。第三,它的Focal Loss专门针对“前景样本和背景样本数量极端不平衡”的问题。道路交通画面里,绝大多数锚框都是没有目标的背景区域,如果拿普通的交叉熵去训练,模型会花大量精力去学“背景”,学出来的检测器对稀疏的目标极度不敏感。Focal Loss给“容易分类的背景样本”不断降权,让模型把注意力集中到那些难的、有目标的前景样本上。顺手做个横向对比表,方便后来者选型:模型类型精度速度小目标表现道路交通场景适配度Faster R-CNN高较慢较好适合离线分析,实时性弱SSD中快较弱小目标易漏检,密集场景一般YOLO系列中高很快中等,依赖版本需要额外做样本平衡处理RetinaNet高快好精度/速度平衡,适合第一版落地第一版选择RetinaNet,本质上是想先把“能不能稳定检测出东西”这个基本问题答好,后续再根据实际数据表现替换骨干或换更强的检测头。2. RetinaNet的骨架与两个子网络——锚点、金字塔、Focal Loss选型定了之后,就要真正理解这个模型每个模块在干什么。RetinaNet本身结构并不复杂,但它把骨干网、特征金字塔、锚点匹配、分类子网络、回归子网络和Focal Loss串成了一条完整流水线。任何一环理解不到位,后面的训练都会出问题。2.1 骨干网络与特征金字塔:多尺度特征是怎么来的RetinaNet通常用ResNet系列作为骨干网络,我第一版用的是ResNet50,主要是考虑显存和训练速度的平衡。图像输入后会经过ResNet逐层下采样,产生不同分辨率的特征图。但仅靠骨干网络的最后一层特征图做检测,对小的交通目标很不友好,因为深层特征图语义信息强但空间分辨率低,小目标在图上只剩几个像素,很难定位。RetinaNet没有直接拿骨干网络输出做检测,而是接了一个特征金字塔网络,用自顶向下和横向连接的方式把深层语义信息和浅层细节信息融合,生成P3到P7这几层特征图。P3对应原始图像的1/8分辨率,适合检测小目标;越往后特征图分辨率越低,对应越大的目标。特征图层下采样倍数适合检测的目标尺度实际对应场景P38小目标远处行人、车辆尾部P416中小目标路口中间的非机动车P532中目标近处轿车、公交车P664大目标画面主体车辆P7128超大目标极少出现,主要用于大目标兜底当时做目标尺度统计时发现,项目里三分之一的目标真实框面积小于32×32像素,这类目标如果只在P5以上层做检测,基本必漏。RetinaNet把P3拉进检测范围,算是解决了这个基础问题。2.2 锚点机制与正负样本匹配RetinaNet在每个特征图位置预设一组锚点框。我用的配置是每个位置3个尺度、3个长宽比,共9个锚点。不同特征图层锚点的基础尺寸不同,比如P3层锚点最小、P7层锚点最大,这样每层只负责对应尺度的目标。训练时,锚点与真实目标框做IoU匹配,IoU大于0.5的锚点被视为正样本,IoU小于0.4的锚点被当作负样本,处于中间地带的锚点直接忽略。这里有一个常被忽略的细节:正负样本匹配策略对道路交通场景非常敏感。比如两个行人挨得很近,某个锚点和行人A的IoU是0.45,和行人B的IoU也是0.45,按默认规则它会被直接忽略。但如果行人B比较远,这个框其实代表了一个“模糊但存在”的目标。第一版我没有做复杂处理,只按默认规则来,结果发现检测器对密集行人的召回率偏低,后面才在数据增强和后处理上补救。2.3 分类子网络与回归子网络RetinaNet在每个特征图后分出两个平行子网络,一个是分类分支,输出每个锚点对应每个类别的概率;另一个是回归分支,输出锚点和真实目标框之间的四个偏移量。这两个子网络都是全卷积结构,参数在所有锚点间共享,这样既控制了参数量,也让训练更容易收敛。回归分支输出的是归一化偏移值,分别是锚点中心坐标和宽高的对数偏移。训练回归分支时用的是Smooth L1损失,相比L2损失它对离群点更不敏感,不容易出现某个难样本把梯度带偏的情况。2.4 Focal Loss:为什么能压住“易分类背景”Focal Loss可以理解为在标准交叉熵上乘了一个调制系数。标准交叉熵里,即使一个背景样本被正确分类且置信度很高,它依然会产生一个不小的损失值,大量高置信度背景样本的损失累加起来,会压过少量前景样本的正确梯度。Focal Loss的形式是,当某个样本的分类概率越高、也就是“越好分”的时候,它的损失会被乘以一个接近0的权重。反之,难分样本保留较大的损失贡献。配合类别平衡系数,可以在训练过程中动态把注意力导向真正困难的前景样本。道路交通画面里背景锚点数量往往是前景锚点上万倍,没有Focal Loss,模型训练到最后大概率变成“什么框都输出低置信度”的保守模式。用了Focal Loss之后,一个直观感受是训练前几个epoch里分类损失下降更快,后面则剩下难例在拉动损失曲线。3. 数据标注的类别体系与增强方案——决定模型上限的环节模型选得再好,数据准备不到位也是白搭。道路交通场景的数据比一般目标检测数据集更麻烦,因为类别之间有大量相似外观和遮挡关系。第一版在数据上投入的时间几乎占整个项目的一半,这部分经历我觉得最值得写。3.1 类别体系设计:少而清晰,别贪多第一版我只保留了七个类别:行人、自行车、摩托车、小汽车、公交车、卡车、交通标志。这是一个刻意做减法的结果。最初添加过“电动车”“三轮车”“行人推车”这些细分类别,但标注一致性和模型精度都很难保证,因为不同标注员对“电动车”和“摩托车”的边界判断差异很大,模型学出来的特征也混在一起。七个类别里,真正容易混的是“自行车”和“摩托车”,在像素较小或者被遮挡时,两者外观确实像。第一版的处理方式是允许这一对误判存在,先不在类别体系里强行细分,后面再通过增加训练样本来改善。3.2 标注格式与数据划分数据标注格式我统一转成COCO风格JSON,因为RetinaNet的常见训练框架对COCO格式支持最成熟。标注结构大概是:{ "images": [ {"id": 1, "file_name": "img_0001.jpg", "width": 1920, "height": 1080} ], "annotations": [ {"id": 1, "image_id": 1, "category_id": 2, "bbox": [x, y, w, h], "area": 30456, "iscrowd": 0} ], "categories": [ {"id": 1, "name": "pedestrian"}, {"id": 2, "name": "car"} ] }如果你的原始数据是VOC格式的XML,建议写个脚本统一转成COCO JSON,一次性把字段对齐,避免训练时再来回折腾。训练集和验证集划分要特别注意不能按视频帧随机切。我把同一场景的连续帧全部归到同一集合里,避免模型“背”下视频帧的相似背景,从而在验证集上刷出虚高指标。3.3 数据增强:别增强过度,也不要漏掉关键变换道路交通数据天然存在光照变化、天气变化、运动模糊等挑战,但训练数据往往只来自几个晴天时段,这时候增强就是必需品。我用的是随机翻转、多尺度缩放、颜色抖动、随机裁剪组合。多尺度缩放效果最明显。第一版输入尺寸在600到1000像素之间随机变化,等效于让模型在不同距离下观察目标,小目标检测的鲁棒性明显提升。有一个容易踩的坑是随机裁剪。在做目标检测的随机裁剪时,如果裁剪框和真实框重叠不合理,会把大量真实框切成残片,甚至把目标一半裁出画面外。这个我单独用工具检查过增强后的可视化结果,凡是把遮挡率提得太高的增强策略,最终训练出来的模型漏检率都会上升,所以一切增强都要以“目标仍然能认出是什么东西”为前提。3.4 类别样本数量不平衡的补救类别样本数量不平衡是道路交通数据里必然出现的问题。小汽车样本可能有上万,交通标志只有几百,如果不做处理,交通标志这个类别即使被标出来,模型也会倾向于把它忽略。我采用的策略是在采样器里给低频类别更高的采样权重,同时用最简单可靠的的Copy-Paste增强:把样本数量少的类别目标按真实尺寸粘贴到道路背景图上,保证粘贴位置符合透视关系,比如交通标志往路边放、行人往人行道放。这种增强虽然土,但对小样本类别的召回率提升非常直观。4. 训练策略、超参与损失跟踪——怎么把模型“喂”到位数据准备好了,接下来是整个项目里最磨人的阶段:训练。RetinaNet的训练过程比想象中更依赖对损失函数的拆解,你不能只看一个总损失值就判断模型好坏,而是要分开观察分类损失、回归损失以及各类别AP的变化。4.1 优化器与初始超参我用的是带动量的SGD,初始学习率设在0.01,配合warmup。warmup的前500步学习率从0.001线性升到0.01,这个操作非常关键。RetinaNet在训练初期锚点还没适应目标分布,如果直接给大学习率,容易出现分类损失直接发散、边界框回归方向跑偏的情况。批大小设为8,受限于单卡显存。如果想增大批大小,可以把输入分辨率调低或者改用ResNet50的小变体。第一版我没有用分布式训练,单卡训练完整个流程大概要跑接近两天,属于可接受范围。4.2 训练过程中的损失观察训练时,我在每个epoch记录四类信息:总损失、分类损失、bbox回归损失、正样本数量。很多框架默认只打印总损失,但我建议一定要手动记录正样本数量。最终我的经验是,正常收敛的情况下,分类损失应该呈现快速下降后平缓减小的曲线,回归损失则相对平缓。如果分类损失下降缓慢,大概率是正样本占比过低;如果回归损失振荡,大概率是某些困难样本的损失权重太大。下面是我用来跟踪训练状态的一段示意代码,你需要根据自己的数据结构调整:# 训练日志跟踪示意,记录每个epoch的关键指标 def log_training_metrics(epoch, cls_loss, reg_loss, num_pos): avg_cls = cls_loss / num_pos avg_reg = reg_loss / num_pos print(f"epoch={epoch} " f"cls_loss={avg_cls:.4f} " f"reg_loss={avg_reg:.4f} " f"pos_samples={num_pos}")4.3 遇到损失异常怎么办第一版训练中遇到过两次典型的异常。第一次是训练到第10个epoch时,回归损失突然出现一个巨大尖峰,之后整个模型输出的框位置全部飘掉。排查发现是某个训练样本的标注框超了图像边界,导致回归目标计算出异常值。处理方式是清洗数据时统一把超出边界的框裁回图像范围,面积太小的直接删掉。第二次是损失整体不降。检查发现Focal Loss的γ参数设成了0,等于退化成了普通交叉熵,模型在大量背景锚点的压力下完全学不动。把γ调回2.0之后,损失曲线在几十步内就有了明显反应。如果你也遇到“损失不降”的问题,第一件事不是调学习率,而是确认Loss实现里有没有应用Focal Loss调制系数,再看正负样本比例是否严重失衡,最后才考虑模型结构。4.4 学习率调度和停止时机第一版采用余弦退火学习率,总共训练了20个epoch。从实际效果看,验证集mAP在使用数据增强后,大约在第8个epoch基本稳定,第16到第20个epoch有小幅提升。如果再往后训练,就会见到验证集指标开始震荡,那是过拟合的信号。5. 从“检测出框”到“系统可用”——推理、后处理与评估训练完成只是第一步,检测系统真正能不能用,要看推理阶段的执行效率和后处理策略。道路交通场景里,车辆和行人往往密集,NMS参数的一点差异直接影响最终输出的可用性。5.1 NMS参数调优RetinaNet输出的原始框数量非常多,必须经过置信度筛选和NMS去重。第一版里我把置信度阈值设在0.4,NMS的IoU阈值设在0.5。这个组合对稀疏道路场景效果不错,但在一个公交站附近的小型拥堵场景里,检测器会输出大量重叠框,NMS的IoU阈值太严就会把同一个目标的多个框全部留下,导致重复计数。后来我把NMS IoU阈值提高到0.6,重复框明显减少,代价是某些紧密挨着的行人可能被合并成一个框。这类参数没有通用最优值,最好按场景单独验证一遍。我的调法是把几种典型道路截图各取20张,在0.4到0.7之间逐档测试,每档看输出框数量和实际目标数量的匹配程度。5.2 推理速度与模型简化第一版最初直接使用训练框架的原始PyTorch模型推理,输入1920×1080图像时,单帧在GPU上约耗时80毫秒,大约是12FPS,对多目标检测系统来说偏慢。之后做了几件简化:把输入分辨率统一缩放到1280×720,同时把P6和P7层的前向计算从完整特征图改为只在候选区域密集时启用,相当于一种“动态层跳过”的思路。这样在空旷路段能把平均帧率提升到22FPS左右,而密集场景仍然保持全量计算,保证精度不塌。如果你需要更进一步的加速,可以把模型导出为ONNX,再用推理引擎做FP16或INT8量化。不过要注意的是,量化后小目标类别(比如远处行人)的召回率往往会下降,必须用验证集去复测各类别的AP,而不是只看整体mAP。5.3 评价指标怎么用多目标检测不能只盯一个mAP。道路交通场景里,我要同时看三类指标:mAP@0.5表示宽松定位条件下的整体检测能力;mAP@0.75表示精确框位置的检测能力,这个对交通轨迹分析很重要;各类别的AP则用来发现某个被“欺负”的类别。我第一版在验证集上的mAP@0.5约为0.78,但其中交通标志类别只有0.51,小汽车类别达到0.88。如果只报一个mAP,这个差距就被掩盖了,后续优化方向也会跑偏。建议每一位做同类系统的朋友,在评估时把每个类别的AP打印出来,做成表格。6. 第一版踩过的坑和可以复用的经验最后这部分,我按“问题—原因—解决”的方式把第一版遇到的主要坑整理成一张表,再补一些个人心得。现象根因解决方案小目标漏检严重特征金字塔低层特征参与检测不足,或小目标样本太少启用P3层检测,增加远处目标样本,多尺度训练行人和非机动车误检类别外观相似,标注边界不统一统一标注规则,增加难例样本,适当合并相似类别分类损失不降Focal Loss参数无效或正样本过少校验γ、α参数,检查正样本锚点匹配比例回归损失尖峰标注框出界或异常目标数据清洗,框回归目标做截断推理速度慢输入分辨率过高,全层特征图都参与计算动态跳层,缩放输入分辨率,模型导出量化密集场景重复框NMS阈值设置过严调整NMS IoU阈值,引入更精准的框回归目标个人心得方面,有几个点我觉得比技术本身更值得记住。模型第一版不需要追求一次性把所有问题解决,先把“能稳定检测出常见大目标”这件事做扎实,再逐步加难例、加类别,迭代节奏会健康很多。数据处理的时间和训练时间不是浪费,交通场景的数据集中注意力放在光照、遮挡和多尺度这三件事上,能覆盖大部分模型后续的大坑。做类似系统时,一定把自动化的评估脚本提前写好,每次改完参数,立刻跑同一套验证集,用指标对比而不是肉眼感觉来判断好坏。肉眼感觉在少数几张图上特别容易被骗,只有整个验证集的统计数字是诚实的。第一版项目到了这个阶段,已经能连续稳定处理城市道路视频流中的常见目标。下一步我自己的计划是先把交通标志类别的样本数量补上来,再针对夜间和雨天场景做一次专门的数据采集,然后升级骨干网络验证精度的提升幅度。如果你也在做类似的检测系统,希望这篇文章能让你少走一点弯路,尤其是把数据、损失、评估这三件事想清楚,整个项目推进起来会顺畅很多。
最近在整理一个做了一段时间的道路交通多目标检测项目,也就是标题里那个“_1”版本——基于RetinaNet的道路交通多目标检测与识别系统,目前已经完成第一版,能在真实道路视频流里同时识别车辆、行人、非机动车和交通设施等多类目标,并且在工程落地层面跑通了从数据准备到推理部署的完整链路。这篇文章想把选型逻辑、原理拆解、训练调参和踩坑全过程写清楚,给正在做类似目标检测项目的朋友一份可参考的“成长记录”。这类项目的目标很明确:给定一段道路监控或车载摄像头画面,系统需要同时输出画面里所有目标的类别和位置框,而且要在车流密集、行人穿插、目标尺度差异极大的情况下尽量不丢框、不错框。RetinaNet在这个场景里算是一个非常稳妥的起点,尤其是它处理“小目标”和“前景背景样本不平衡”这两件事的思路,刚好对应道路交通场景里最常见的两个痛点。下面从最初选型开始讲。1. 为什么选RetinaNet做道路交通多目标检测——选型时的一笔账先交代一下背景。第一版的目标不是追求某个比赛级别的精度,而是要在可控的开发周期内搭出一个真正可运行、可评估、可继续迭代的多目标检测基线。既然方向锁定在“道路交通”这个场景,一开始就要把场景特点拆清楚:目标类别多且大小悬殊、密集遮挡频繁、背景复杂且占像素比例极大。这几个特点直接决定了模型选型的方向。当时摆在我面前的无非是几类主流方案:两阶段检测器(Faster R-CNN系列)、单阶段检测器(SSD、YOLO系列)、以及基于关键点或Transformer的新方案。我最终选了RetinaNet,核心原因有三条。第一,RetinaNet是单阶段模型,推理速度快,工程上更容易满足道路场景的实时性要求。道路交通检测往往要部署在边缘设备或车载工控机上,算力预算有限,两阶段模型精度虽好但速度压力很大。第二,RetinaNet的骨干网络采用特征金字塔结构,能拿到从高分辨率浅层到低分辨率深层的多尺度特征图。道路场景里远处行人可能只有几十个像素,而近处公交车可能占满大半个画面,这种多尺度目标同时出现的情况,恰好是特征金字塔擅长处理的。第三,它的Focal Loss专门针对“前景样本和背景样本数量极端不平衡”的问题。道路交通画面里,绝大多数锚框都是没有目标的背景区域,如果拿普通的交叉熵去训练,模型会花大量精力去学“背景”,学出来的检测器对稀疏的目标极度不敏感。Focal Loss给“容易分类的背景样本”不断降权,让模型把注意力集中到那些难的、有目标的前景样本上。顺手做个横向对比表,方便后来者选型:模型类型精度速度小目标表现道路交通场景适配度Faster R-CNN高较慢较好适合离线分析,实时性弱SSD中快较弱小目标易漏检,密集场景一般YOLO系列中高很快中等,依赖版本需要额外做样本平衡处理RetinaNet高快好精度/速度平衡,适合第一版落地第一版选择RetinaNet,本质上是想先把“能不能稳定检测出东西”这个基本问题答好,后续再根据实际数据表现替换骨干或换更强的检测头。2. RetinaNet的骨架与两个子网络——锚点、金字塔、Focal Loss选型定了之后,就要真正理解这个模型每个模块在干什么。RetinaNet本身结构并不复杂,但它把骨干网、特征金字塔、锚点匹配、分类子网络、回归子网络和Focal Loss串成了一条完整流水线。任何一环理解不到位,后面的训练都会出问题。2.1 骨干网络与特征金字塔:多尺度特征是怎么来的RetinaNet通常用ResNet系列作为骨干网络,我第一版用的是ResNet50,主要是考虑显存和训练速度的平衡。图像输入后会经过ResNet逐层下采样,产生不同分辨率的特征图。但仅靠骨干网络的最后一层特征图做检测,对小的交通目标很不友好,因为深层特征图语义信息强但空间分辨率低,小目标在图上只剩几个像素,很难定位。RetinaNet没有直接拿骨干网络输出做检测,而是接了一个特征金字塔网络,用自顶向下和横向连接的方式把深层语义信息和浅层细节信息融合,生成P3到P7这几层特征图。P3对应原始图像的1/8分辨率,适合检测小目标;越往后特征图分辨率越低,对应越大的目标。特征图层下采样倍数适合检测的目标尺度实际对应场景P38小目标远处行人、车辆尾部P416中小目标路口中间的非机动车P532中目标近处轿车、公交车P664大目标画面主体车辆P7128超大目标极少出现,主要用于大目标兜底当时做目标尺度统计时发现,项目里三分之一的目标真实框面积小于32×32像素,这类目标如果只在P5以上层做检测,基本必漏。RetinaNet把P3拉进检测范围,算是解决了这个基础问题。2.2 锚点机制与正负样本匹配RetinaNet在每个特征图位置预设一组锚点框。我用的配置是每个位置3个尺度、3个长宽比,共9个锚点。不同特征图层锚点的基础尺寸不同,比如P3层锚点最小、P7层锚点最大,这样每层只负责对应尺度的目标。训练时,锚点与真实目标框做IoU匹配,IoU大于0.5的锚点被视为正样本,IoU小于0.4的锚点被当作负样本,处于中间地带的锚点直接忽略。这里有一个常被忽略的细节:正负样本匹配策略对道路交通场景非常敏感。比如两个行人挨得很近,某个锚点和行人A的IoU是0.45,和行人B的IoU也是0.45,按默认规则它会被直接忽略。但如果行人B比较远,这个框其实代表了一个“模糊但存在”的目标。第一版我没有做复杂处理,只按默认规则来,结果发现检测器对密集行人的召回率偏低,后面才在数据增强和后处理上补救。2.3 分类子网络与回归子网络RetinaNet在每个特征图后分出两个平行子网络,一个是分类分支,输出每个锚点对应每个类别的概率;另一个是回归分支,输出锚点和真实目标框之间的四个偏移量。这两个子网络都是全卷积结构,参数在所有锚点间共享,这样既控制了参数量,也让训练更容易收敛。回归分支输出的是归一化偏移值,分别是锚点中心坐标和宽高的对数偏移。训练回归分支时用的是Smooth L1损失,相比L2损失它对离群点更不敏感,不容易出现某个难样本把梯度带偏的情况。2.4 Focal Loss:为什么能压住“易分类背景”Focal Loss可以理解为在标准交叉熵上乘了一个调制系数。标准交叉熵里,即使一个背景样本被正确分类且置信度很高,它依然会产生一个不小的损失值,大量高置信度背景样本的损失累加起来,会压过少量前景样本的正确梯度。Focal Loss的形式是,当某个样本的分类概率越高、也就是“越好分”的时候,它的损失会被乘以一个接近0的权重。反之,难分样本保留较大的损失贡献。配合类别平衡系数,可以在训练过程中动态把注意力导向真正困难的前景样本。道路交通画面里背景锚点数量往往是前景锚点上万倍,没有Focal Loss,模型训练到最后大概率变成“什么框都输出低置信度”的保守模式。用了Focal Loss之后,一个直观感受是训练前几个epoch里分类损失下降更快,后面则剩下难例在拉动损失曲线。3. 数据标注的类别体系与增强方案——决定模型上限的环节模型选得再好,数据准备不到位也是白搭。道路交通场景的数据比一般目标检测数据集更麻烦,因为类别之间有大量相似外观和遮挡关系。第一版在数据上投入的时间几乎占整个项目的一半,这部分经历我觉得最值得写。3.1 类别体系设计:少而清晰,别贪多第一版我只保留了七个类别:行人、自行车、摩托车、小汽车、公交车、卡车、交通标志。这是一个刻意做减法的结果。最初添加过“电动车”“三轮车”“行人推车”这些细分类别,但标注一致性和模型精度都很难保证,因为不同标注员对“电动车”和“摩托车”的边界判断差异很大,模型学出来的特征也混在一起。七个类别里,真正容易混的是“自行车”和“摩托车”,在像素较小或者被遮挡时,两者外观确实像。第一版的处理方式是允许这一对误判存在,先不在类别体系里强行细分,后面再通过增加训练样本来改善。3.2 标注格式与数据划分数据标注格式我统一转成COCO风格JSON,因为RetinaNet的常见训练框架对COCO格式支持最成熟。标注结构大概是:{ "images": [ {"id": 1, "file_name": "img_0001.jpg", "width": 1920, "height": 1080} ], "annotations": [ {"id": 1, "image_id": 1, "category_id": 2, "bbox": [x, y, w, h], "area": 30456, "iscrowd": 0} ], "categories": [ {"id": 1, "name": "pedestrian"}, {"id": 2, "name": "car"} ] }如果你的原始数据是VOC格式的XML,建议写个脚本统一转成COCO JSON,一次性把字段对齐,避免训练时再来回折腾。训练集和验证集划分要特别注意不能按视频帧随机切。我把同一场景的连续帧全部归到同一集合里,避免模型“背”下视频帧的相似背景,从而在验证集上刷出虚高指标。3.3 数据增强:别增强过度,也不要漏掉关键变换道路交通数据天然存在光照变化、天气变化、运动模糊等挑战,但训练数据往往只来自几个晴天时段,这时候增强就是必需品。我用的是随机翻转、多尺度缩放、颜色抖动、随机裁剪组合。多尺度缩放效果最明显。第一版输入尺寸在600到1000像素之间随机变化,等效于让模型在不同距离下观察目标,小目标检测的鲁棒性明显提升。有一个容易踩的坑是随机裁剪。在做目标检测的随机裁剪时,如果裁剪框和真实框重叠不合理,会把大量真实框切成残片,甚至把目标一半裁出画面外。这个我单独用工具检查过增强后的可视化结果,凡是把遮挡率提得太高的增强策略,最终训练出来的模型漏检率都会上升,所以一切增强都要以“目标仍然能认出是什么东西”为前提。3.4 类别样本数量不平衡的补救类别样本数量不平衡是道路交通数据里必然出现的问题。小汽车样本可能有上万,交通标志只有几百,如果不做处理,交通标志这个类别即使被标出来,模型也会倾向于把它忽略。我采用的策略是在采样器里给低频类别更高的采样权重,同时用最简单可靠的的Copy-Paste增强:把样本数量少的类别目标按真实尺寸粘贴到道路背景图上,保证粘贴位置符合透视关系,比如交通标志往路边放、行人往人行道放。这种增强虽然土,但对小样本类别的召回率提升非常直观。4. 训练策略、超参与损失跟踪——怎么把模型“喂”到位数据准备好了,接下来是整个项目里最磨人的阶段:训练。RetinaNet的训练过程比想象中更依赖对损失函数的拆解,你不能只看一个总损失值就判断模型好坏,而是要分开观察分类损失、回归损失以及各类别AP的变化。4.1 优化器与初始超参我用的是带动量的SGD,初始学习率设在0.01,配合warmup。warmup的前500步学习率从0.001线性升到0.01,这个操作非常关键。RetinaNet在训练初期锚点还没适应目标分布,如果直接给大学习率,容易出现分类损失直接发散、边界框回归方向跑偏的情况。批大小设为8,受限于单卡显存。如果想增大批大小,可以把输入分辨率调低或者改用ResNet50的小变体。第一版我没有用分布式训练,单卡训练完整个流程大概要跑接近两天,属于可接受范围。4.2 训练过程中的损失观察训练时,我在每个epoch记录四类信息:总损失、分类损失、bbox回归损失、正样本数量。很多框架默认只打印总损失,但我建议一定要手动记录正样本数量。最终我的经验是,正常收敛的情况下,分类损失应该呈现快速下降后平缓减小的曲线,回归损失则相对平缓。如果分类损失下降缓慢,大概率是正样本占比过低;如果回归损失振荡,大概率是某些困难样本的损失权重太大。下面是我用来跟踪训练状态的一段示意代码,你需要根据自己的数据结构调整:# 训练日志跟踪示意,记录每个epoch的关键指标 def log_training_metrics(epoch, cls_loss, reg_loss, num_pos): avg_cls = cls_loss / num_pos avg_reg = reg_loss / num_pos print(f"epoch={epoch} " f"cls_loss={avg_cls:.4f} " f"reg_loss={avg_reg:.4f} " f"pos_samples={num_pos}")4.3 遇到损失异常怎么办第一版训练中遇到过两次典型的异常。第一次是训练到第10个epoch时,回归损失突然出现一个巨大尖峰,之后整个模型输出的框位置全部飘掉。排查发现是某个训练样本的标注框超了图像边界,导致回归目标计算出异常值。处理方式是清洗数据时统一把超出边界的框裁回图像范围,面积太小的直接删掉。第二次是损失整体不降。检查发现Focal Loss的γ参数设成了0,等于退化成了普通交叉熵,模型在大量背景锚点的压力下完全学不动。把γ调回2.0之后,损失曲线在几十步内就有了明显反应。如果你也遇到“损失不降”的问题,第一件事不是调学习率,而是确认Loss实现里有没有应用Focal Loss调制系数,再看正负样本比例是否严重失衡,最后才考虑模型结构。4.4 学习率调度和停止时机第一版采用余弦退火学习率,总共训练了20个epoch。从实际效果看,验证集mAP在使用数据增强后,大约在第8个epoch基本稳定,第16到第20个epoch有小幅提升。如果再往后训练,就会见到验证集指标开始震荡,那是过拟合的信号。5. 从“检测出框”到“系统可用”——推理、后处理与评估训练完成只是第一步,检测系统真正能不能用,要看推理阶段的执行效率和后处理策略。道路交通场景里,车辆和行人往往密集,NMS参数的一点差异直接影响最终输出的可用性。5.1 NMS参数调优RetinaNet输出的原始框数量非常多,必须经过置信度筛选和NMS去重。第一版里我把置信度阈值设在0.4,NMS的IoU阈值设在0.5。这个组合对稀疏道路场景效果不错,但在一个公交站附近的小型拥堵场景里,检测器会输出大量重叠框,NMS的IoU阈值太严就会把同一个目标的多个框全部留下,导致重复计数。后来我把NMS IoU阈值提高到0.6,重复框明显减少,代价是某些紧密挨着的行人可能被合并成一个框。这类参数没有通用最优值,最好按场景单独验证一遍。我的调法是把几种典型道路截图各取20张,在0.4到0.7之间逐档测试,每档看输出框数量和实际目标数量的匹配程度。5.2 推理速度与模型简化第一版最初直接使用训练框架的原始PyTorch模型推理,输入1920×1080图像时,单帧在GPU上约耗时80毫秒,大约是12FPS,对多目标检测系统来说偏慢。之后做了几件简化:把输入分辨率统一缩放到1280×720,同时把P6和P7层的前向计算从完整特征图改为只在候选区域密集时启用,相当于一种“动态层跳过”的思路。这样在空旷路段能把平均帧率提升到22FPS左右,而密集场景仍然保持全量计算,保证精度不塌。如果你需要更进一步的加速,可以把模型导出为ONNX,再用推理引擎做FP16或INT8量化。不过要注意的是,量化后小目标类别(比如远处行人)的召回率往往会下降,必须用验证集去复测各类别的AP,而不是只看整体mAP。5.3 评价指标怎么用多目标检测不能只盯一个mAP。道路交通场景里,我要同时看三类指标:mAP@0.5表示宽松定位条件下的整体检测能力;mAP@0.75表示精确框位置的检测能力,这个对交通轨迹分析很重要;各类别的AP则用来发现某个被“欺负”的类别。我第一版在验证集上的mAP@0.5约为0.78,但其中交通标志类别只有0.51,小汽车类别达到0.88。如果只报一个mAP,这个差距就被掩盖了,后续优化方向也会跑偏。建议每一位做同类系统的朋友,在评估时把每个类别的AP打印出来,做成表格。6. 第一版踩过的坑和可以复用的经验最后这部分,我按“问题—原因—解决”的方式把第一版遇到的主要坑整理成一张表,再补一些个人心得。现象根因解决方案小目标漏检严重特征金字塔低层特征参与检测不足,或小目标样本太少启用P3层检测,增加远处目标样本,多尺度训练行人和非机动车误检类别外观相似,标注边界不统一统一标注规则,增加难例样本,适当合并相似类别分类损失不降Focal Loss参数无效或正样本过少校验γ、α参数,检查正样本锚点匹配比例回归损失尖峰标注框出界或异常目标数据清洗,框回归目标做截断推理速度慢输入分辨率过高,全层特征图都参与计算动态跳层,缩放输入分辨率,模型导出量化密集场景重复框NMS阈值设置过严调整NMS IoU阈值,引入更精准的框回归目标个人心得方面,有几个点我觉得比技术本身更值得记住。模型第一版不需要追求一次性把所有问题解决,先把“能稳定检测出常见大目标”这件事做扎实,再逐步加难例、加类别,迭代节奏会健康很多。数据处理的时间和训练时间不是浪费,交通场景的数据集中注意力放在光照、遮挡和多尺度这三件事上,能覆盖大部分模型后续的大坑。做类似系统时,一定把自动化的评估脚本提前写好,每次改完参数,立刻跑同一套验证集,用指标对比而不是肉眼感觉来判断好坏。肉眼感觉在少数几张图上特别容易被骗,只有整个验证集的统计数字是诚实的。第一版项目到了这个阶段,已经能连续稳定处理城市道路视频流中的常见目标。下一步我自己的计划是先把交通标志类别的样本数量补上来,再针对夜间和雨天场景做一次专门的数据采集,然后升级骨干网络验证精度的提升幅度。如果你也在做类似的检测系统,希望这篇文章能让你少走一点弯路,尤其是把数据、损失、评估这三件事想清楚,整个项目推进起来会顺畅很多。