前阵子做智慧环卫方向的项目核心需求其实就一句话从路口和垃圾中转站的监控画面里把垃圾车从一堆货车、客车、面包车里精确地分出来。实验做了一圈之后最终落地的是标题里这套组合——Grid R-CNN ResNet-101 FPN GN head用 COCO 预训练权重按2x训练策略做微调。整条链路从数据标注、配置改写、训练收敛到部署加速都走通了验证集 mAP0.5 稳定在 88% 上下。这篇算是一次完整复盘把每一步的选择逻辑和实际踩到的坑都写出来适合正在做特种车辆检测、或者想在 mmdetection 里把 Grid R-CNN 跑通的人参考。1. 需求拆解垃圾车检测为什么不能直接抄 COCO 的 truck 类1.1 应用场景与真实痛点当时接到的项目场景是这样的在一批固定视角的路口监控和环卫站进出口相机上需要自动识别经过的环卫作业车辆尤其是垃圾运输车用来做清运频次统计、作业轨迹回放和违规倾倒预警。这类需求看起来就是普通的目标检测但有个很微妙的地方——我们不是要数车流而是要把垃圾车这个细分类从所有卡车里捞出来。垃圾车的形态本身就很杂。后压缩式、侧装式、钩臂式车体颜色从白到绿到橙黄都有和普通厢式货车、渣土车、集装箱卡车在远看轮廓上高度相似。如果只是在通用模型上把truck这个类拉出来用几乎没办法区分哪辆是环卫垃圾车、哪辆是普通社会货车。实际抽测第一版通用模型时误检率到了没法看的地步路口一棵树的阴影、一辆深色面包车都能被当成货车框出来垃圾车本身的召回倒还行问题是下游系统没法基于这种框做业务判断。1.2 为什么不拿通用模型直接凑合很多人会问COCO 不是有 truck 类吗直接微调不就行了我一开始也是这么想的后来发现这条路有两个绕不开的问题。第一个问题是 COCO 的标注口径。truck 类把所有卡车混在一起半挂、集装箱、厢式货车全都算垃圾车的数量在训练集里占比极低模型对这种子类的判别力自然不足。第二个问题是通用模型对具体场景的先验分布完全不了解。固定机位、特定角度、特定光照下的监控画面和 COCO 里五花八门的网络图片是两个分布直接泛化过来边界框飘、置信度上下乱跳都是常态。所以方案定为拿 COCO 预训练的网格检测模型做初始化用自己的垃圾车数据集做微调。这本质上不是从零训练也不是拿来就用而是在一个已经很会看通用物体的模型基础上教会它区分垃圾车和看起来像垃圾车的车。1.3 模型选型的评估标准选模型之前我先定了三条硬指标单类检测 mAP0.5 不低于 85%部署形态是服务器上的 GPU 视频流服务单帧推理控制在 200ms 以内训练和复现要稳不能靠某个实验室的特殊 trick。候选方案当时有 Faster R-CNN、Cascade R-CNN、Grid R-CNN、FCOS 和 YOLOv5 这一批最终选了 Grid R-CNN理由后面细说但核心就一句它在两阶段框架里把定位精度往上推了一档而且 mmdetection 里有现成的 R101 FPN GN head 2x 配置改数据路径就能训省去大量调框架的时间。2. Grid R-CNN 的定位原理网格点在回归之外多干了一件事2.1 从 Faster R-CNN 的短板说起两阶段模型的定位流程大家都很熟RPN 先提候选框RoIHead 对每个候选框做一次分类和回归。这里的回归通常只预测四个值——中心点偏移和宽高偏移本质是在把框往正确的方向挪一挪。这个设计对大多数目标够用但对边界敏感的细分类任务有个隐患回归对遮挡、截断、大姿态变化的目标容易跑偏。垃圾车在监控里经常被截断车头和车厢颜色反差又大如果只用四个偏移量去拟合框经常往里缩一点或者往外飘一点——单独看还行但要拿这个框去关联车牌、判断车辆是否驶入指定区域误差就放大了。2.2 网格点回归到底怎么工作Grid R-CNN 的核心思路是在回归之外再预测目标内部一批网格点的位置用这些点反过来约束最终框。默认配置是 3×3 的九个点它们在目标上的位置相对固定——车头中线、车尾中线、车侧边缘这些关键位置。训练时用真实框切出网格点做监督推理时把网格点坐标和回归框做融合最终得到比纯回归更贴边的结果。我的理解里这东西可以类比成搭帐篷回归框相当于先大概圈出帐篷的占地范围九个网格点就是九根固定桩桩位置对了帐篷布的边界自然就绷准了。对于垃圾车这种车体规整、边角清晰的刚性目标网格点能卡在车厢和车头的几何特征上定位质量比纯回归稳定得多。2.3 和几个主流候选方案对比选型时我列了一张对比表直接对着需求逐项打分方案定位机制精度潜力训练/部署复杂度我当时权衡Faster R-CNN四参数回归中低定位细度不够我的下游用Cascade R-CNN多级回归高中精度高但推理更慢收益不明确Grid R-CNN回归 9 点网格高中最终选择FCOS / ATSS逐像素预测中高低边框质量不如网格约束YOLOv5单阶段 anchor中低快但遮挡和细分类一般实际训练之后的体感是Grid R-CNN 在框贴不贴边这件事上确实比 Faster R-CNN 好一截尤其在目标占画面比例较大的路口机位效果差距能直接量化到 mAP0.75 上。代价是多了一个网格头的计算量和推理时的网格融合逻辑这个后面部署章节再具体讲。3. 配置逐个解读R101、FPN、GN-head、2x 都不是白给的3.1 主干网络与特征金字塔这个配置里每一项后缀都有明确作用不是拍脑袋组合出来的。先看 R101 和 FPN。ResNet-101 比 ResNet-50 深一截对垃圾车 vs 普通卡车这种细分类来说深层特征能捕捉到更多外观细节比如车斗结构、液压臂、车厢后盖的形态差异。代价是训练和推理都变慢但对服务器部署来说可以接受。FPN 是特征金字塔它解决的问题是尺度垃圾车在监控里可能占满半幅画面也可能只有几十像素FPN 把不同深度的特征图拉成多尺度输出让 RPN 和 RoI 提取层都能在不同尺度上找到对应特征。提示如果数据里小目标特别多优先保证输入分辨率而不是盲目加深网络。垃圾车这类中大型车辆R101 的收益主要体现在细分类上。3.2 GN head 解决的是训练稳定性问题GN 是 Group Normalization也就是分组归一化。它和 BatchNorm 最大的区别是BN 依赖 batch 内的统计量而 GN 把通道分成若干组在组内做归一化完全不依赖 batch size。检测模型的 head 部分天然不适合 BN。因为 R-CNN 系列是对采样出来的 RoI 做运算一个 batch 里的 RoI 数量受正负样本比例影响统计量抖动很大而且训练时如果只有一两张卡、每卡 2 张图BN 的均值和方差几乎不可用。GN 按通道分组做归一化单张图也能算训练过程稳得多。Grid R-CNN 的网格头里全是卷积分支通道数不低GN 在这里几乎是标配。3.3 2x 训练策略的含义2x 是 mmdetection 里的训练节奏约定总共训 24 个 epoch在第 16 和 22 个 epoch 把学习率乘以 0.1。相比 1x 的 12 轮2x 给了模型更长的收敛时间和更充分的学习率衰减过程。在 COCO 这种十万级图片的数据集上2x 是标准配置。但在自建的小数据集上就要留个心眼几千张图训 24 轮大概率会过拟合。我的实际做法是先用 2x 跑一遍观察验证集曲线的拐点如果第 14 轮之后训练损失还在降、验证 mAP 开始横盘就说明后面的轮次基本在浪费时间。3.4 具体配置文件与训练命令我用的 mmdetection 版本是 2.x 系列配置文件直接基于模型库里的grid_rcnn_r101_fpn_gn-head_2x_coco改只动了数据路径和类别数。下面是核心部分_base_ [ ../_base_/models/grid_rcnn_r101_fpn_gn-head_2x_coco.py, ../_base_/datasets/coco_detection.py, ../_base_/schedules/schedule_2x.py, ../_base_/default_runtime.py ] model dict( pretrainedtorchvision://resnet101, roi_headdict( bbox_headdict( num_classes1, loss_clsdict(typeCrossEntropyLoss, use_sigmoidFalse, loss_weight1.0), loss_bboxdict(typeSmoothL1Loss, beta1.0, loss_weight1.0)), ), test_cfgdict( rcnndict( score_thr0.05, nmsdict(typenms, iou_threshold0.5), max_per_img100, griddict(use_gridTrue, grid_points9)))) dataset_type CocoDataset data_root data/garbage_truck/ classes (garbage_truck, ) data dict( traindict( typedataset_type, ann_filedata_root annotations/train.json, img_prefixdata_root train/, classesclasses), valdict( typedataset_type, ann_filedata_root annotations/val.json, img_prefixdata_root val/, classesclasses), testdict( typedataset_type, ann_filedata_root annotations/val.json, img_prefixdata_root val/, classesclasses))注意一点bbox_head 的num_classes改成 1 之后加载预训练权重时必须把分类层的参数剥掉这个坑我放到训练章节详细说。训练命令很简单python tools/train.py configs/garbage/grid_rcnn_r101_fpn_gn-head_2x_garbage.py \ --work-dir work_dirs/garbage_grid/如果只有单卡学习率建议从默认的 0.002 线性缩放。默认 0.002 是八卡、每卡两张图的设置单卡配 2 张图时我一般直接降到 0.00025或者不做缩放但把 batch 拉大否则 loss 在前几百步会抖得厉害。4. 数据集构建垃圾车数据怎么标、怎么筛、怎么扩4.1 数据来源与采样策略数据来源主要是两类固定机位的监控录像和随车记录仪画面。固定机位视角稳定但点位少、场景单调随车画面场景丰富但目标角度变化大、难例也多。我的策略是两者混着用比例大约 7:3。采样时最容易犯的错误是相邻帧大量重复。连续视频按每秒抽几帧抽出来的两千张图里可能有 60% 是几乎一样的画面模型等于在反复看同一批车验证集又恰好是同一机位的其他时段导致验证指标虚高。后来我用感知哈希对帧做了去重再按机位分组划分训练集和验证集确保同一个机位不会同时出现在两边。另外专门收集了一批难例环卫清扫车、洒水车、城管执法车、各类厢式货车以及在树荫、雨天、夜间条件下的普通车辆。这批难例不参与训练也可以单独放一个文件夹每次评估的时候混进验证集专门看模型在它该拒绝的东西上表现如何。4.2 标注规范与 COCO 格式转换类别设计这块我纠结过一阵要不要把垃圾车按子类型拆成三类比如后压缩式、侧装式、钩臂式。后来验证下来发现子类型之间外观差异大样本数量又不均拆类反而拉低整体精度最后统一成一个garbage_truck类子类型差异留给模型自己学习。标注规范里有一条要提前约定好目标被遮挡时人工只标可见部分还是按可推断的完整轮廓标我最终定的是完整轮廓因为下游要做轨迹还原和区域判断需要的是整车位置。转换 COCO 格式时我建议注解框直接从多边形算包围盒不要手动拉矩形手拉矩形很容易带进背景网格头的监督信号会被污染。标注完成之后我做了两轮质检第一轮看框和目标的贴合度第二轮专门查漏标——把模型初检结果叠到图片上人工看哪些目标没被框出来。第二轮通常能捡回 2%-5% 的漏标这对指标的影响比换模型结构还大。4.3 数据统计对超参数的影响标注完了先别急着训做一次数据统计能省很多弯路。我统计下来垃圾车目标在 1333×800 输入下的边长大多落在 64~512 像素宽高比集中在 1.2~2.2和我们用的默认 anchor 配置基本匹配所以不用额外调 anchor。如果统计发现大量目标小于 32 像素那就要考虑提高输入分辨率或者多尺度训练而不是硬调 anchor 尺度。单类别检测还有一个隐性收益RPN 阶段的目标/背景判断几乎不会出现类别不平衡因为垃圾车是有明确外观的一类不是那种可能是也可能不是的模糊类。主要的风险反而是类内差异大、难例不够这要靠持续收集失败样本来修正。5. 训练实施与调优过程5.1 环境与预训练权重环境没什么特别的PyTorch 1.8 左右、对应版本的 CUDA、mmdetection 2.x按官方文档装就行。预训练权重从模型库下载grid_rcnn_r101_fpn_gn-head_2x_coco的 checkpoint这个权重在 COCO 上有大概 41 的 box AP是一个已经收敛得很好的初始化点。5.2 加载预训练模型的正确姿势这里有个容易踩的坑。COCO checkpoint 的分类层是全类别 80 类加背景一共 81 个类而我们要训的是 1 个类加背景只有 2 个类。直接load_from会让分类层的 shape 对不上轻则警告跳层重则直接加载失败。正确做法是加载前先把分类层参数剥掉import torch ckpt torch.load(checkpoints/grid_rcnn_r101_fpn_gn-head_2x_coco.pth) state ckpt[state_dict] for k in list(state.keys()): if roi_head.bbox_head.fc_cls in k: state.pop(k) torch.save({state_dict: state}, checkpoints/grid_garbage_pretrained.pth)为什么要动fc_cls而不动fc_reg因为回归分支的输出是固定的四个坐标量和类别数无关可以直接复用分类分支的输出维度跟类别数绑定类别数量变了就必须丢弃等模型自己重新学。RPN 头里的分类输出是背景和前景二分类和类别数也没关系可以放心保留。我把这个处理写进训练脚本里后续不管换多少次数据集这一套逻辑都是通用的。5.3 训练曲线与超参调整训练过程比预期顺。前几个 epochRPN 的损失和回归损失下降很快第 8 轮左右验证 mAP0.5 就能到 78% 前后第 16 轮学习率第一次衰减之后指标明显再跳一档最终 24 轮跑到 88% 上下。中间我做过几轮小调整。第一次是想加速收敛把pos_fraction从 0.25 降到 0.2目的是减少正样本数量防止单类别场景下 RoIHead 正样本过多导致训练偏向。实际效果是过拟合确实轻了一点验证集和训练集的差距缩小了但 mAP 本身没有明显变化。后来又把学习率从 0.002 降到 0.001 试了一轮发现收敛更稳最终精度基本持平所以我没把它留在最终配置里。还有一个值得记录的观察GN head 在这个任务里从没出现过 NaN 或者 loss 跳变这比我在其他 BN head 项目里的体验好太多。如果你在小 batch 训练时遇到 head 部分 loss 不稳定优先检查是不是 BN 统计量在作怪。5.4 验证结果与测试时增强最终验证集的结果大概是这样我自己的数据上的数字仅供参考不同机位分布差异会很大指标数值mAP0.588.4mAP0.7574.6mAP0.5:0.9558.3AR10091.9小目标 AP64px61.8中目标 AP64~256px88.5大目标 AP256px91.2mAP0.75 能到 74 是让我比较惊喜的这个指标对框的贴合度非常敏感间接说明网格定位在起作用。测试时增强方面我试了水平翻转加多尺度缩放 0.8 和 1.2 共三种尺度mAP0.5 能再涨 0.8 到 1 个点但推理耗时翻了将近三倍部署阶段果断没上。6. 实测中的失败案例漏检和误检都在哪里6.1 夜间与低对比度训练集里夜间样本约占 15%但夜间漏检率还是明显高于白天。最容易漏的场景是路灯背光下深灰色车厢和道路混在一起车灯强光过曝让车头轮廓消失。针对这类样本我把夜间段的视频单独抽了一个月的数据补进训练集同时做了一个对比实验在预处理里加了 CLAHE 对比度增强结果夜间 AP 提升了约 3 个点白天 AP 几乎不受影响。6.2 相似车型的误检误检大头不是背景而是看起来像垃圾车的车。打扫得很干净的环卫清扫车、白色厢式货车、深色的公交车在特定角度下都有可能拿到超过 0.5 的置信度。这个问题的解法不是调阈值阈值调高了只会把真正的垃圾车也一起砍掉。我的对策是持续收集误检样本尤其是那些模型给出高置信度的难例混入训练集重训。两轮之后打扫车和普通厢式货车的误检明显下降。另外在工程层面做了兜底通过车辆运行轨迹和作业区域做规则过滤检测框必须出现在环卫作业路段或者垃圾站进出口周边才会进入业务判定流程相当于给模型加了一层业务先验。6.3 小目标与遮挡远处的垃圾车只有几十像素的时候漏检率随之上升。提升输入分辨率是最直接的手段我从 1333×800 提到 1600×1200 试过小目标 AP 从 61.8 涨到 67 左右但推理时间多了将近 40%最后因为延迟指标服务端还是退回 1333×800。遮挡场景则是两辆垃圾车并排时 NMS 容易把后车吞掉我试验过把 NMS 的 iou_threshold 从 0.5 调到 0.45多辆车近距离并排的场景召回有所改善单场景又没有明显变差是一个性价比很高的小改动。7. 部署加速的过程记录7.1 网格预测在部署时的麻烦训练收敛之后部署阶段遇到一个文档里讲得很少的问题Grid R-CNN 推理时的网格融合逻辑不太好导出。网格头预测出九个点的分布之后还需要一个解码步骤把网格点坐标和回归框做迭代融合这部分在 ONNX 导出时容易碰到不支持的算子。如果你也卡在这最省事的方案是把test_cfg.rcnn.grid.use_grid改成False让模型在推理时只走回归分支不再做网格融合。代价是定位精度略微下降。我在测试集上对比过关掉网格融合 mAP0.5 只掉了 0.6 个点mAP0.75 掉了 1.2 个点左右。对于纯部署场景这个精度损失完全能接受换取的是导出和推理链路的极大简化。7.2 加速方案与实测耗时我先后试过三条路径原生 PyTorch fp32、ONNX Runtime、TensorRT FP16。在消费级显卡上的实测单帧耗时如下推理方式单帧耗时PyTorch fp32开网格约 180msPyTorch fp32关网格约 165msONNX Runtime约 140msTensorRT FP16关网格约 40msTRT 的 FP16 加速非常可观四倍多的收益让线上服务的吞吐彻底不是问题。导出命令基本是标准的python tools/deployment/pytorch2onnx.py \ configs/garbage/grid_rcnn_r101_fpn_gn-head_2x_garbage.py \ work_dirs/garbage_grid/latest.pth \ --input-img demo.jpg \ --shape 1333 800拿到 ONNX 之后再用 TensorRT 做精度序列化注意 mmdetection 导出的模型里预处理部分的均值方差是内嵌的部署端不要再重复做一次归一化。7.3 工程化注意事项部署阶段还有个经常被忽略的点输入尺寸。mmdetection 的 pipeline 是按比例缩放并 pad 到 1333×800如果把预处理换成简单缩放到固定尺寸宽高比变了检测框坐标换算也会跟着错。我在接入视频流时把 resize 逻辑改成了和训练时一致的等比缩放加灰色填充并且统一了坐标还原公式才把线上检测框和画面中实际车辆对齐。帧间稳定性方面我没有上跟踪模型而是对检测框做了轻量级的时序平滑连续多帧里同一辆车通过 IOU 关联的置信度取滑动平均只有连续两帧及以上都被检测到才输出稳定的车辆事件。这一层在工程上基本把单帧漏检造成的业务抖动压下去了代价几乎为零。8. 复盘哪些地方我重来一遍会改8.1 数据投入的优先级如果重来一次我会把更多时间放到数据上而不是模型结构上。回头看最终精度提升贡献最大的是两件事难例样本的持续补充和标注的二次质检这两块的投入产出比远高于调学习率或者换骨干网络。建议一开始就把持续收集失败样本、定期重训纳入流程而不是等到上线被退回才补。8.2 训练策略上的简化我自己的数据量只有几千张24 轮训练的方案偏奢侈了。实际从验证曲线上看第 16 轮学习率衰减之后模型的提升已经很小后面六个 epoch 基本在烧 GPU。换个角度想如果当时直接用 1x 也就是 12 轮辅以学习率衰减和早停能省下接近一半的训练时间最终 mAP 大概只差 0.5 个点以内。数据量小的场景不必迷信模型仓库里的默认配置。8.3 模型选择的后见之明Grid R-CNN 在这个项目里是站得住脚的精度达标训练稳定代码链路完整。但目前看来它的主要成本集中在部署环节的网格解码算子。如果你的部署环境对自定义算子很不友好Cascade R-CNN 或 FCOS 这类导出更顺的方案可能是更稳妥的选择。反过来如果你和我一样做精细定位的单类检测、又有标准 GPU 服务Grid R-CNN 依然是值得先试的那一个。最后再分享一个实操细节我在评估use_gridTrue和use_gridFalse的差距时意外发现纯回归分支的表现比预期好很多这说明标注质量已经把框的上限拉得很高。以后再做类似单类特种车辆检测我会优先把标注质量、难例数量和机位分布做好模型结构反而不需要太纠结。这套流程里写的排查思路和代码都是可以直接拿走的但具体的精度数字换一批数据就会有变化——指标可以参考踩坑的经验才比较稳定。