上一周我把某园区车辆管理项目里的算法链路整体翻新了一遍,核心工作就是把“车牌识别+车辆检测”升级成“多品牌车型的识别与检测”。模型最终落到了YOLOv8-P6上,跑通了从标注规范、训练调参到TensorRT部署的完整流程。多品牌车型识别这个任务,一句话描述就是:给定一张监控画面,框出每辆车的车身位置,同时输出它的品牌、车系和年款信息。听着和普通目标检测差不多,真正做起来才发现细粒度分类、小目标召回、数据均衡这些问题全挤在一起。这篇文章把我围绕YOLOv8-P6的实现思路、踩坑记录和优化手段完整梳理一遍,给正在做车辆识别或类似细粒度检测任务的同行一个可参考的复现路径。
1. 项目需求拆解与模型选型思路
1.1 任务本质:检测和细粒度分类的叠加
多品牌车型识别不是单纯的“找车”。如果是“找车”,用通用检测模型训一个单类别vehicle就结束了。但这里要输出“某德系品牌-A系列-2019款”这样的细粒度信息,本质上是目标检测和图像分类的叠加任务。检测分支负责定位车辆位置,分类分支负责区分同一辆车在不同年款、不同角度下的细微差异。这两个任务在同一个模型里同时完成,对特征表达的要求比普通检测高不少。
为什么不做成“检测+分类”的两阶段方案?两阶段方案的思路是先检测车辆框,再对每个框内的图像做品牌车型分类。这样做的好处是分类器可以单独优化,而且能利用检测框裁剪后的高分辨率细节。我之前在另一个项目里就是这么干的:先用检测模型出框,再用轻量分类网络做细分类。精度不差,但落地时有两个问题:一是链路长,检测加分类两个模型串行,延迟叠加;二是两个模型各自维护训练数据、迭代版本,工程复杂度翻倍。在数据规模有限、又要求实时性的场景下,一体化检测分类模型更划算。YOLOv8-P6天然支持多类别输出,直接让每个类别代表“品牌+车系+年款”的叶子节点,检测的同时完成分类,一条链路走到底。
这里要特别提一点:一体化模型对类别体系设计很敏感。如果把每个年款都当平铺类别,类别数会迅速膨胀,数据覆盖率跟不上,分类准确率就会崩。所以类别体系必须做成树状结构,训练时用叶子节点,推理时通过树结构聚合出品牌和车系置信度。这个我在第二章详细展开。
1.2 为什么选用YOLOv8-P6而不是标准YOLOv8
标准YOLOv8用的是P5结构,输出P3、P4、P5三个尺度的特征图,默认输入分辨率640。对通用目标检测来说,这个配置在速度和精度之间平衡得很好。但在车辆识别场景下有个明显短板:监控画面里的车辆往往不大,尤其俯拍、远视角场景,一辆车在整张图中可能只占不到5%,车标、格栅纹理这些细粒度判别特征在特征图上只剩几个像素。小目标召回上不来,品牌分类就更不用谈。
YOLOv8-P6在backbone后面多了一次下采样,输出变成P3、P4、P5、P6四个尺度,并习惯配合1280的输入分辨率使用。P6特征图分辨率更低、感受野更大,主要负责大目标的回归质量;而由于输入分辨率翻倍,P3、P4层上的小目标特征占比明显提升,车标和格栅区域的细节保留得更完整。从结果看,我同一批数据下对比过标准YOLOv8 640输入和YOLOv8-P6 1280输入,验证集mAP50-95从大约76%提升到82%以上,增量主要来自中小尺寸车辆的召回和分类正确率。代价是训练显存和推理耗时明显增加,这个账要在部署环节算清楚。
1.3 方案对比:一张表看清差异
| 方案 | 输入分辨率 | 输出特征尺度 | 小目标表现 | 显存占用 | 推理速度 | 适用场景 |
|---|---|---|---|---|---|---|
| 标准YOLOv8 | 640 | P3/P4/P5 | 中 | 低 | 快 | 通用检测、实时性优先 |
| YOLOv8-P6 | 1280 | P3/P4/P5/P6 | 强 | 高 | 中 | 车辆细分类、小目标密集场景 |
| 两阶段检测+分类 | 640+224 | 视模型而定 | 中高 | 中 | 慢 | 数据充足、精度优先 |
这个表不是绝对的,具体还要看显卡型号和部署平台。如果只有一张中端显卡且要求实时,P6的1280输入可能扛不住,这时候可以退一步:训练时用1280,推理时用动态分辨率降到1024或960,平衡效果和速度。我后面在部署部分会细说这个操作。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据集构建与标签策略
2.1 类别体系:品牌-车系-年款三级树
训练数据怎么组织,直接决定模型能不能收敛。我的做法是先维护一张类别树:一级是品牌,二级是车系,三级是年款或代际。例如“品牌A1-车系X1-2019款”“品牌A1-车系X2-2021款”。训练时只把叶子节点作为类别标签,模型输出的也是叶子类别。推理端拿到叶子类别概率后,沿树向上聚合:某个车系下所有年款的概率加起来,就是该车系的置信度;品牌层同理。
为什么不用平铺类别?因为车辆数据天然存在长尾效应。热门车系样本可能上千张,冷门年款可能只有几十张。平铺类别时,模型对冷门类别的学习严重不足,而且类别之间的混淆信息也无法利用。树状结构加上聚合策略,至少能在车系和品牌层兜底:即使某个具体年款识别错了,品牌和车系大概率还是对的。这对实际业务很有价值,很多场景只需要精确到车系级别。
数据集来源上,我混合了公开车辆数据集和自采监控画面。公开集负责提供场景多样性,自采数据负责贴近实际部署环境。注意公开集和自采数据的标注规范必须完全统一,否则模型会学到标注风格的差异,而不是真正的车型差异。
2.2 标注规范:车身紧包围框与属性标签
标注规范是我这次项目里最想强调的部分。同一个车型,不同标注员的框法不同,模型就很容易学歪。我最后定的标准是:目标出现完整车头或车尾时,标注“整个可见车身的紧包围框”,即从保险杠最外沿到车身侧面最外沿,包含后视镜但不包含明显伸出的天线或拖钩。对只有部分车身的遮挡场景,标注可见部分的紧包围框,并打上遮挡属性标签。有了遮挡属性,训练时可以做分档处理,推理时也能辅助判断置信度。
品牌和车型属性的标注要和框严格绑定。一个框对应一个叶子节点标签,不要出现“一辆车同时标两个品牌”这类歧义。车头车尾要不要分开标?我建议统一作为同一个类别,因为后续推理场景你无法保证车辆一定正对或背对摄像头。但数据配比上尽量让车头、车尾、侧面三个视角都覆盖,避免模型只认识车头。
还有一个容易忽略的细节:验证集划分不能按“图片随机切分”直接来。同一个停车场、同一辆车会出现在多张连续帧中,如果按图片随机分,训练集和验证集会出现同一辆车的不同帧,验证指标会很虚。我当时的做法是先对车辆轨迹做聚类,保证同一辆车的所有帧只落在同一个集合里。这一步非常关键,否则你看到的mAP是“记住车”而不是“认识车”的结果。
2.3 数据增强策略与难例挖掘
增强策略直接沿用YOLOv8默认的Mosaic、MixUp、HSV扰动、随机翻转。车辆检测有个特性:水平翻转基本安全,因为车身对称,车标和格栅不依赖左右朝向。但如果你的分类规则里有“驾驶位在左还是在右”这类方向性信息,就不要翻转。HSV扰动要适度,过度会把车身颜色这个重要特征洗掉。某些品牌车型的颜色分布很固定,比如白色车身占了70%,模型可能会偷懒用颜色判断品牌,而不是用格栅和车灯造型。所以色彩增强要保留主要的颜色分布差异,但不能强到让模型失去对真实颜色的敏感性。
难例挖掘是提升分类精度的核心手段。我在训练中期会做一次混淆矩阵分析,找出“品牌A1”和“品牌B2”之间互相认错的成对组合,把这些样本重采样,按2倍到3倍权重参与后续训练。实际操作中可以单独挑出这些难例图片,重复进入数据加载器,比简单加大全局epoch更有效。另外,夜间、逆光、雨雾天气的样本要在训练集里强制覆盖。否则部署到现场,白天精度还能看,一到晚上就露馅。
3. YOLOv8-P6训练配置与关键参数
3.1 模型配置:从内置yaml到自定义改造
直接用ultralytics内置的yolov8-p6.yaml是个稳妥起点。它的nc默认是80,需要改成你的叶子类别总数。我这里构建完类别树后,叶子节点一共是213个类别。训练前用auto anchor让程序重新聚类当前数据集的最佳anchor尺寸。P6模型有4个输出尺度,anchor数量比P5多,自动聚类这一步不能省。
yaml配置里我改动较多的是这几个地方:nc设为叶子类别数,model.scale参数保持默认。如果你用的GPU显存有限,可以考虑把输入分辨率参数从1280降到1024,但这会牺牲小目标表现。也可以把backbone的宽度因子和深度因子从默认的0.33/0.33上调到0.5/0.5,相当于扩大模型容量。我实测过,对细粒度分类任务,模型宽一点比深一点更有效。宽模型能保留更多通道维度上的细节特征,适合分辨车标和格栅的微差。代价是计算量上涨,需要测试平台能否承受。
3.2 训练超参:优化器、学习率与损失配置
训练超参我基于默认配置做了一套组合,实测收敛稳定:
- 优化器:SGD,momentum=0.937,weight_decay=0.0005。SGD在目标检测任务上比AdamW稳,尤其训练后期不容易在极小值附近震荡。
- 初始学习率:lr0=0.01,配合cosine衰减。跑了300个epoch,最终lr降到初始值的1/100以下。
- warmup:前3个epoch做warmup,从0.1倍lr逐步升到0.01,避免早期梯度爆炸。
- label smoothing:0.05。细粒度分类任务用少量标签平滑能提升泛化,但不要设太大,太大了会让各类别概率趋同,反而模糊了相似车型的边界。
- close_mosaic=10:最后10个epoch关闭Mosaic增强,只用原始图片和轻度增强做精调。这一步很重要,Mosaic生成的合成图有大量拼接痕,一直用到底会让模型对真实分布的适应性变差。
损失函数沿用默认的CIOU、BCE和DFL组合。P6模型下box_loss的收敛曲线会比P5平缓,这是正常的,不必焦虑。真正需要重点盯的是cls_loss:如果cls_loss长时间不降,去查混淆矩阵,大概率是某些相似车系在打架,而不是模型结构问题。
3.3 训练过程监控与恢复策略
训练过程中我每隔10个epoch保存一次权重,同时记录验证集mAP50和mAP50-95。mAP50-95对车辆检测来说更诚实,因为车辆目标大小差异大,单看mAP50容易虚高。另外,我用EMA(指数滑动平均)对权重做平滑,推理时加载EMA权重,效果比用原始权重稳定不少。AMP混合精度训练默认开启,但要注意:如果训练日志里出现分类损失的NaN,先不要急着降学习率,优先检查是不是AMP的数值稳定性问题。我在某个版本的依赖环境下确实遇到过,最后把分类头单独切到FP32计算,问题就消失了。
训练中断恢复也很常见。ultralytics支持resume参数直接从上次保存的权重继续训练。这里有个经验:恢复训练后先跑20个iter观察loss曲线是否和中断前衔接,如果loss突然跳高,说明batch size或随机种子变动导致数据顺序变化,一般跑几十个iter后会回落,不影响最终结果。
4. 推理优化与落地部署经验
4.1 从PyTorch模型到TensorRT
训练完成后需要把PyTorch模型导出成TensorRT engine,才能在推理阶段拿到足够的性能。先转ONNX,再转engine,FP16精度。命令大概是这样:
bash复制yolo export model=best.pt format=onnx opset=12 imgsz=1280
trtexec --onnx=best.onnx --saveEngine=best_fp16.engine --fp16 --minShapes=images:1x3x1280x1280 --optShapes=images:4x3x1280x1280 --maxShapes=images:8x3x1280x1280
这里的动态shape是重点。训练时固定1280,但推理时不一定总是1280,某些机位视野近,车辆占比大,用1024甚至960完全够,速度快不少。设置min/opt/max三个档位,运行时按实际输入尺寸动态选择。P6模型在FP16下偶尔会出现分类logits的数值抖动,如果发现同一辆车连续帧的品牌置信度不稳定,把分类头留在FP32就能解决,代价是速度略降,通常可以接受。
嵌入设备上跑P6会比较吃力。我在某边缘盒子上实测过:Jetson Orin NX级别才能跑到实时,性能低一级的设备建议退回到P5模型配640输入。这个权衡不做不行,P6不是万能的,设备算力不够就是不够。
4.2 后处理细节:NMS参数与分类结果平滑
NMS阈值我调了几轮,最终用iou=0.45。车辆框大且密集时,比如停车场大角度俯拍,相邻车辆边界重叠多,阈值太松会合并掉两辆车,阈值太紧会出现同一辆车多个框。如果你发现高置信度重复框多,可以试试开启agnostic NMS,让所有类别一起做抑制,对车辆场景效果不错。但要注意,如果一辆车旁边有行人,agnostic模式可能把行人框误伤,这个看业务是否需要同时检测其他目标。
分类结果的时间平滑是我强烈建议加的一层。视频流场景下,单帧分类结果会有抖动,可能这一帧是“品牌A1-车系X1”,下一帧变成“品牌A1-车系X2”,肉眼看起来就是页面上的识别结果在跳。我在检测框跟踪ID的基础上,对每个ID维持一个长度为5帧的分类置信度历史,做加权平均。只有连续3帧以上都判定为另一个类别时才切换分类结果。这个机制在门禁和停车管理场景里非常实用,能大幅减少误报。
4.3 端到端性能实测与并发设计
最终部署方案里,我把检测和分类合并的YOLOv8-P6模型跑在单张中高端GPU上,FPS能达到35到40帧每秒。这个算力已经能满足常规园区卡口。如果接多路视频流,建议用batch推理:把多路的待检测帧攒成batch一次过模型,而不是每路单独推理。batch size为4时的总吞吐量大约是单帧推理的四路并发,同时也能规避显存碎片。
CPU上就不要指望P6了,纯CPU推理1280输入一帧要好几秒,只适合离线批量处理。如果是离线分析历史录像,那无所谓,反而可以把输入分辨率提到1536,进一步刷新小目标表现。在线实时和离线高精度,本质上是同一套模型的两个端点,用动态分辨率都能兼顾。
5. 常见问题排查与避坑实录
5.1 问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 小目标漏检多 | 输入分辨率不够 / 验证集划分泄漏 | 用P6+1280训练,检查验证集是否同车泄漏 |
| A品牌误判为B品牌 | 车系相似,难例样本不足 | 从混淆矩阵找重灾区,难例重采样 |
| 夜间识别率骤降 | 训练集夜间样本少,过曝干扰 | 补充夜间、逆光、强光样本,做曝光增强 |
| 训练loss震荡 | 学习率过高 / AMP数值不稳定 | 降低lr0,或将分类头保留FP32 |
| 同一辆车多个框 | NMS阈值过松或过紧 | 调节iou阈值,测试agnostic NMS |
| 部署速度不达标 | P6模型过重,输入过大 | 用动态分辨率降到1024,或换P5模型 |
| 多个年款互相混 | 叶子类别样本不均衡 | 树状聚合,按品牌/车系输出并兜底 |
| 视频分类结果跳动 | 单帧分类抖动 | 按跟踪ID做5帧置信度平滑 |
5.2 印象最深的三个坑
第一个坑是“验证集虚高”。项目初期我按图片随机切分训练集和验证集,mAP50-95一度到85%以上,我当时还挺高兴,结果上现场测试发现同一条路上拍的车,识别表现远不如验证集。后来复盘才意识到:同一辆车的连续帧被同时分进了训练集和验证集,模型是在“认车”而不是“认车型”。重构数据集划分方式,按照车辆轨迹聚类后再切分,真实指标立刻掉到82%左右,但这个数据是可信的。这件事给我一个教训:细粒度识别任务里,数据划分的严谨程度直接决定实验结论是否有效。
第二个坑是德系某品牌和同集团子品牌之间的大面积混淆。两者的前脸格栅设计语言高度相似,只看局部特征很容易认错。我一开始没意识到问题有多严重,直到看混淆矩阵才发现这两个品牌互误率超过了25%。解决方式分两步:第一步,在数据层面把这两个品牌的车头侧面样本做了3倍重采样;第二步,在模型层面给它们增加专门的细粒度损失权重。最终互误率降到8%左右。相似品牌之间的区分,纯粹靠堆样本,模型其实也能学好,但你得先给模型机会多看到这些难例。
第三个坑是P6模型把显存吃得死死的。第一次训练1280分辨率,batch size设为8,直接显存溢出。后来把batch降到4,用梯度累积模拟8的等效batch size。要注意的是,梯度累积和真实batch size在BatchNorm统计上并不完全等价,YOLOv8有跨迭代BN的机制,所以影响不大。如果还溢出,把Mosaic关闭后训练段的内存占用会低不少。总之P6不是“开箱即用”的配置,显存规划要在训练启动前就算好。
5.3 关于数据闭环的一点建议
这个项目做完后我感触最深的一点是:模型架构再先进,也比不上数据反馈闭环的长期价值。项目上线后,每天会有大量误判样本沉淀在日志里。与其反复调超参,不如每周固定把误判样本捞出来,人工复核后回流到训练集。连续迭代两三个月后,识别准确率可能比单次训练调参提升更明显。这也是为什么我建议把推理阶段的分类置信度、检测框坐标都完整落日志,没有这些历史数据,后面想做增量训练也无从下手。
回到标题本身,“多品牌车型识别与检测”这个方向,YOLOv8-P6确实是一个高效且合理的基座模型。它用更大的输入分辨率和更深的特征金字塔,换来了细粒度车型识别最需要的小目标表现。如果你也正在做类似项目,建议按这篇文章给的路径来:先把类别树和数据划分做扎实,再用P6模型去训练,最后在部署端配合动态分辨率和分类结果平滑。数据、模型、部署三段各留出迭代空间,这个项目就算打通了。
