ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于NAS-FCOS的拥挤路段车辆检测系统实践

基于NAS-FCOS的拥挤路段车辆检测系统实践

1. 为什么拥挤路段的车辆检测总是翻车

做智慧交通项目的人应该都有同感:单独拍一辆车、视野空旷的路口,模型跑得漂漂亮亮,mAP高得吓人,一上真实拥堵路口就原形毕露——近处的大巴车把后面轿车挡掉一半,左转待转区十几辆车挤在一起,远处还有一片模糊的车尾在红绿灯下排成长队。这个时候,你手里的经典检测器大概率会犯三种毛病:漏检、误检、同一辆车出好几个框。

我最早用的是基于锚点的两阶段检测方案,在拥挤场景下anchor尺寸和长宽比怎么调都不对。后来换到无锚点路线,才算把方向走对了。这篇要分享的是我基于NAS-FCOS这条路子做的一套完整系统:R50-Caffe-FPN-NASHead-GN-Head,专门针对拥挤路段车流量统计和违章抓拍的车辆检测模型。标题里那一长串后缀不是随便堆的,每一个缩写都对应一个具体的工程决策,后文我会逐个拆开讲。

这套系统能做什么?简单说就是:给一段监控视频输入,它能实时把画面里每辆车框出来,就算车跟车挨得只剩一条缝、车头车尾混在一起、光照从逆光突然切换成顺光,框的稳定性和类别判定都不会大幅垮掉。适合三类人来参考:一是做智慧交通算法落地的工程师,二是想从有锚点检测器迁移到无锚点路线的研究者,三是在 Caffe 框架上做老模型升级、但不想换 TensorFlow 或 PyTorch 的技术团队。

先说个结论:拥挤路段场景,真正决定模型上限的不是 backbone 多强,而是 neck 和 head 对“重叠目标”和“小目标”的响应方式。NAS-FCOS 恰好在这两个环节做了很对路的改进。

2. NAS-FCOS 的技术拆解:它到底改了哪些东西

2.1 FCOS 为什么适合拥挤场景

FCOS 属于无锚点检测器,它的核心思想是“让每个像素直接预测它属于哪个目标的哪一部分”,而不是像传统方法那样在图上铺一堆预设框再去匹配。这个设计天然对拥挤场景友好,原因是:锚点框方案在密集目标下会产生大量重复匹配,而且交并比过高的锚点框在训练时会被直接判定为负样本,导致真正有遮挡的车辆反而学不到特征。

FCOS 还做了个很关键的事情:对每个输出特征图上的位置,只负责预测距离该位置最近的框。当你站在路侧高点去看拥堵的车流时,车身之间的遮挡是必然的,但每个像素距离“可见的那辆车中心”不一定都很远,这样即使另一半车被挡了,只要这部分可见的车身像素足够多,模型仍然能给出一个完整的回归框。

另外一个容易被忽略的点是 FCOS 不需要锚点长宽比先验。拥挤车流里,公交车、SUV、轿车、电三轮(如果是混合交通),长宽比五花八门,锚点框方案在前期设计上就很难照顾周全。

2.2 NAS 搜索出来的 Head 到底搜了什么

NAS-FCOS 的核心贡献是把原来 FCOS 的检测头替换成了通过神经架构搜索找到的结构。很多人一看到 NAS 就觉得“搜出来的网络不可控、落地难”,实际用在 FCOS 这个场景里比想象中要收敛得多。

NAS 在这里搜索的空间不是整个 backbone,而是集中在 head 部分:在 P3、P4、P5、P6、P7 这五层特征层之间,搜索到底哪些层之间需要跨层连接,每一层内部应该堆多少个卷积,卷积核大小用 3x3 还是混合比例。换句话说,把原来“每一层都独立做分类和回归”的并联结构,换成了一个更复杂的多路径连接结构。

从实测效果看,NAS 搜出来的结构相当于在 FPN 各层之间加了几条“跳到中层再接回来”的捷径。比如 P5 层的结果不只是从 P4 上采样来的,它还会额外拿一份从 P3 那边送过来的更细节的信息。这相当于给高层特征图补充了下层的空间细节,对拥挤路段的近距离车辆和远距离排队车辆同时有效。

2.3 GN-Head 为什么替换 BN

NAS-FCOS 原版结构在 head 里用的归一化方式在不同开源复现里并不统一,有的用 BN,有的用 GN。我在这个项目里把所有检测头的归一化全部换成了 GroupNorm,这背后有个很实际的原因:拥挤路段监控画面的分辨率通常很高,喂进网络的输入尺寸相应也会拉大,显卡显存根本撑不住足够大的 batch size。

在 batch size 只有 2 或者 4 的情况下,BatchNorm 的均值方差估计会非常不稳。换到 GroupNorm 之后,归一化完全不依赖 batch 里的其他样本,每组特征内部自己归一化,训练稳定性好很多。这一点对于那些准备用单卡或者双卡跑训练的同学尤其关键。

2.4 ResNet-50 backbone 够不够用

这个项目用的是 ResNet-50,相比 ResNet-101 甚至更重的骨干,在精度上的损失其实可控,但在速度和显存占用上优势明显。拥堵路段的车辆检测并不需要识别细到车标级别的差异,主要分类是轿车、SUV、公交车、卡车这几类,加上 VID 这类车辆重识别模型的辅助,五十层的特征提取能力完全够用。

如果未来想要进一步提升,可以在保留整套 head 的前提下把 backbone 替换成具有更强多尺度能力的版本,甚至可以在三个输出层中设置不同的膨胀系数,让感受野对齐不同距离的车辆尺寸。这部分我会在文末的扩展思考里再梳理。

3. 数据准备和训练细节:这里藏着 80% 的坑

3.1 拥挤场景数据要做哪些特殊处理

模型训练用到的数据,尤其是道路监控视角的数据,和公开数据集 COCO 那类自然图像差异很大。车辆普遍处于小角度俯视状态,而且密集排列,车尾特征和车顶特征占了很大比重。我对数据做了三类处理:

第一,裁切增强。道路监控的原始分辨率通常是 1920x1080,甚至有些摄像头输出 2560x1440。直接整图缩放会造成远处车辆信息急剧丢失。我在训练时采用了随机裁切策略,裁出来的区域缩放到固定输入尺寸,这样让模型在不同距离的车辆上都能学到特征。

第二,亮度与对比度扰动。拥堵路段的画面受天气变化影响极大,同一组摄像头,清晨逆光、正午强光、傍晚阴影、晚上路灯,色温能差出十万八千里。在做数据增强时,我在 HSV 空间加了比较大幅度的扰动,尤其是 V 通道的变化范围调到常规增强的 1.5 倍左右。这一步对夜间车灯过曝导致的车身细节丢失,有明显改善。

第三,针对遮挡的遮挡模拟。简单来说,就是在训练过程中用随机矩形块遮住车辆的一部分,强迫模型露出少一半车身也能识别出来,这样模型对拥挤场景里大量存在的部分遮挡不会过于敏感。结合上面提到的像素级预测机制,效果比单纯加数据要可靠得多。

3.2 Caffe 环境下模型结构复现的几个关键点

NAS-FCOS 原版开源是以 PyTorch 实现的。但这个项目所在的推理链路,早期是在 Caffe 框架下跑的:整条链路基于 Caffe 的检测模型做过大量适配,包括前处理、后处理和硬件加速库,全部是围绕 Caffe 模型格式构建的。所以我当时的任务是把 PyTorch 的 NAS-FCOS 结构用 Caffe 复现,并对齐参数。这里有几个很重要的细节,值得记下来:

  • Caffe 不支持 PyTorch 里常用的某些动态操作,NAS 搜出来的 Head 结构在表达时,要把多路径连接全部重新写成固定拓扑的 DAG,然后在 prototxt 里直接定义成类似残差结构的并联层。
  • GroupNorm 在 Caffe 里没有原生层,我用的是自定义插件,把输入特征按通道分组后依次做 mean、var、normalize 和 scale/shift。建议把这个层单独提取出来做单元测试,否则后面定位问题会很痛苦。
  • FPN 的上采样操作,PyTorch 里默认双线性插值,Caffe 里要对应换成 Deconvolution + 带固定双线性核的初始化方式,保证像素对齐行为一致。这步不对齐,输入端稍微改个尺寸,输出特征就全是错位的。

3.3 训练超参与损失函数配置

训练时输入尺寸我设定为 896x896,batch size 8,使用多卡训练,优化器为带动量的 SGD,动量 0.9,初始学习率 0.01,并使用 warmup 策略。学习率调度选择了 cosine 衰减,相比 step 下降,在检测任务上后期收敛得更平滑,不容易出现在某个 LR 台阶附近损失震荡的情况。

损失函数部分,分类分支用的是 Focal Loss,因为拥挤场景负样本(背景)数量远大于正样本(车辆),Focal Loss 可以在训练过程中自动压低易分类负样本的权重,让模型把注意力集中在那些被遮挡、模糊、难以分类的车上。回归分支用的是 GIoU Loss,它对小目标回归的宽容度比标准 Smooth L1 更好。中心度分支还是保留 FCOS 原始的 BCE Loss。

梯度裁剪也一直开着,梯度 norm 限制在 0.5。虽然 GN 对 batch 不敏感,但在训练初期仍然可能出现个别层梯度过大的问题,裁剪后整体稳定很多。

3.4 类不平衡问题怎么处理

训练集中九成是轿车的情况下,模型对公交车和卡车会产生明显的掉点。这个问题在人群拥堵场景中特别明显。解决思路有两个层面:一是对训练数据中的少样本类别做过采样,保证每个批次里公交车、卡车这类样本的出现频率维持在合理比例;二是对 Focal Loss 的 alpha 参数按类别统计频率做自适应调整,让低频类别获得更高的损失权重。

实际跑下来的结果,各类别的 AP 差距从原来的 12 个百分点缩小到 5 个百分点以内。如果你手头数据的类别比例更极端,建议把 alpha 参数根据当前样本分布动态计算,不要手工硬调。

4. 训练过程的踩坑记录:最狠的坑不在模型结构

4.1 损失降不下去的排查过程

第一次把模型结构搭起来开跑的时候,前三个 epoch 损失下降得很顺利,到第四个 epoch 开始震荡,分类分支的 loss 怎么都降不到预期水平。检查了数据增强、学习率,都没发现问题。后来逐层打印特征图数值,发现在 P6、P7 这两层,经过多次卷积之后特征数值分布出现了明显的偏移,大量值集中在零附近,但方差又异常大。

问题出在 GroupNorm 的初始化和 conv 层的初始化配合上。我当时对 head 里新增的卷积层沿用了 PyTorch 默认的 Kaiming 初始化,但是 GroupNorm 的 scale 初始值设成 1,在深层特征上会出现前几个批次统计量还没稳定导致的输出饱和。把 GroupNorm 的 scale 初始值改成 0.1 之后,这个问题就消失了。这是一个非常小众的坑,说不上有什么精妙原理,但很值得记进你的工程检查清单。

4.2 多卡训练时 GN 和 BN 的显存差异

这里有一个可复现的规律:使用 BN 时,即便显存能撑下更大的 batch size,增大 batch 对最终精度也没有正向作用,因为监控图片之间场景差异太大,统计量难以在 batch 内对齐;换成 GN 后,把 batch size 从 4 提到 8 反而有约 0.8 个点的提升,因为数据多样化更有助于模型泛化。

所以在显存规划上,我建议采用“GN + batch size = 8 或 16”的搭配,而不是“BN + batch size = 32 或 64”。同样的显存预算下,GN 还能预留出空间给更长的训练序列和更大的输入分辨率。

4.3 EMA 对稳定性的提升

检测模型训练在 180 个 epoch 之后,权重在最优位置附近反复横跳,导致验证集上 AP 整体在 86.5% 到 87.6% 之间波动。后来加入了指数移动平均,衰减参数设为 0.9998,验证时用平均权重而不是迭代权重,最终 AP 稳定在 87.8% 左右。

EMA 在拥挤场景下尤其好用,因为它能让优化轨迹更接近“整体最优区间”的中心。这里给一个经验值:如果训练集的标注噪声较大,EMA 的 decay 可以适当设到 0.9999,平滑效果更明显;如果数据比较干净,0.999 就够了,设得太高反而延缓了对新分布的响应。

4.4 DeepLayer 梯度的监控习惯

现在每次跑训练,我都会在关键层加一组梯度统计量的日志输出,包括梯度二范数和梯度最大值。这一步能让你在训练早期就从曲线形态上判断层与层之间是否存在梯度消失或爆炸,不用等 300 个 epoch 跑完才发现模型废了。特别是替换了 GroupNorm 插件之后,这个监控几乎成了刚需。

5. 推理阶段的性能优化:把模型塞进实时链路

5.1 检测头裁剪和分支合并

NAS 搜索出来的 Head 结构,如果在推理时不做处理,耗时是原始 FCOS 的 1.3 倍左右,在边缘计算盒子上帧率会被压到非常尴尬的水平。我做的优化分两步。

第一步是通道剪枝。对分类分支和回归分支中贡献度低的卷积通道做剪枝,保留响应值统计显著的前 90% 通道。这一步不改变计算图,只是把矩阵变窄,推理实测加速 8%,精度损失可以忽略。第二步是分支合并。把 head 最后几层中分类和回归共用的部分合并成一个共享卷积,只在最终输出时才分叉。因为两个任务在低层特征上高度相关,这样合并不但能提速,还能提升回归稳定性,算是意外收获。

5.2 NMS 参数在拥挤场景下的重新调节

拥挤场景中的后处理也需要单独调。我用了 Soft-NMS,并且把 IoU 阈值调高到 0.65,这样密集排列的车辆不会被别的框的置信度连带压掉。分类置信度阈值设置在 0.35,比常规场景低一些,因为遮挡环境下真实目标的置信度本身偏低。如果你用的是普通 NMS,在极端拥堵下很容易出现同一辆车的多个框互相抑制导致误删,非常不推荐。

5.3 量化与硬件部署

模型最终部署用的推理硬件支持 INT8 量化,所以我把模型的卷积层权重做了一轮校准量化。这一步对检测框位置的影响非常小,但对分类分数影响明显:量化前分类分数偏低的那部分目标,量化后可能直接低于阈值而漏检。解决办法是把分类分支的 0.35 阈值在量化后下调到 0.3,同时保持 NMS 阈值不变,这样能在保持低误检的前提下挽回一部分漏检。

如果你用的是 GPU 部署环境,完全可以使用 TensorRT 做 FP16 推理,收益比 INT8 更稳妥。FP16 在这里没有任何精度问题,而且避免了量化校准的额外工作。

6. 实测效果和性能对比

整个系统在真实道路监控画面上的测试结果,可以用一组数据来说明。测试集选了三个不同拥堵等级的路口:轻度、中度和重度。轻度路口是正常车流量,车辆之间保持至少一个车身的间距;中度路口出现在早晚高峰,排队前进但车辆之间间隔很小;重度路口则直接是十字路口被堵死,多车并行加混入非机动车。

场景召回率精确率FPS(边缘设备)
轻度拥堵93.2%96.1%27
中度拥堵90.4%94.8%27
重度拥堵84.7%91.6%27

在重度拥堵场景中,召回率下降主要来源于严重遮挡导致的车身可视面积过小,以及夜间大灯过曝造成的纹理丢失。法务上的违章抓拍会对这部分场景做二次甄别确认,确保不会因漏检或误检造成处罚差错。

和之前基于有锚点检测器的方案对比,漏检率在重度拥堵场景下下降了大概 20%,误检率变化不大。这些数据不一定对所有硬件都成立,但方向上对参考价值很明确。

7. 一些可以继续延伸的方向

这个系统目前还能做很多横向扩展。一个方向是跨摄像头车辆追踪,先把单镜头下的检测结果按时间维度做跟踪,然后通过车辆外观特征在多摄像头之间做关联匹配。这个用网络训练时提取的中层语义特征做重识别,效果比单纯用颜色直方图稳定得多,鲁棒性更接近量产需求。

另一个方向是行为识别,比如靠车辆轨迹特征判断违规变道、加塞等动作。但行为识别对检测框的稳定性要求极高,小抖动都会被放大成不规律的轨迹抖动,所以在此基础上需要先把跟踪模块加上,再去做轨迹平滑和动作分类。

还有一个我认为很有潜力的方向是用更轻量的骨干网络替代 ResNet-50,比如基于结构重参数化思想的轻量模块。拥堵路段车辆检测的实时性要求往往比精度更迫切,在保证 GN-Head 检测结构不变的前提下换一个更轻的 backbone,能进一步把推理帧率推到 30 帧以上,而精度损失控制在可接受范围内。

最后分享一个小的实战体会:拥挤路段车辆检测这类任务,在模型结构上的差距很容易被数据质量和后处理细节弥补掉一部分;反过来,结构再先进,训练数据里遮挡样本和极端光照样本不足,模型活不过回灌真实场景的那一天。所以与其一上来就追求更复杂的 backbone,不如先把数据分布吃透,再在 head 和训练策略上做文章。NAS-FCOS 这条路子引入的搜索空间和归一化改造,正好把这两个关键方向都兼顾了,这也是我把整套方案沉淀下来分享的主要原因。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进