驾驶员行为检测这几年在智能驾驶圈子里热度一直没降过,尤其是分心驾驶、打电话、抽烟、喝水这些动作的识别,几乎是每家有座舱监控需求的团队都要啃的硬骨头。我手上这个项目用的是22600张标注好的YOLO格式数据集,覆盖了多种驾驶员行为类别,从数据清洗、类别平衡、模型选型到训练调参、部署验证,整套流程走下来踩了不少坑,也积累了一些在公开文档里不太容易找到的经验。这篇文章不打算写成一份冷冰冰的说明书,而是把我实际做这个数据集训练时遇到的真实问题、做过的取舍、验证过的参数,原原本本摊开来讲。不管你是刚接触目标检测的新手,还是已经跑过几个YOLO项目想找找思路的老手,应该都能从里面捞到点有用的东西。
1. 驾驶员行为检测这个任务到底难在哪
1.1 22600张数据集背后的类别分布真相
拿到一个标注好的数据集,很多人第一反应是直接丢进模型开跑。我一开始也这么干过,结果训练到第30个epoch发现某些类别mAP死活上不去,回头一查类别分布,问题一目了然。22600张图听起来不少,但分摊到各个行为类别上,差距可能非常悬殊。正常驾驶的样本往往占了大头,而像"低头看手机""双手离开方向盘"这类危险行为,样本量可能只有前者的几分之一。
这种长尾分布带来的直接后果是:模型会倾向于把大多数样本预测成高频类别,因为这样损失函数下降得最快。你看到整体mAP还行,但一拆开看每个类别的混淆矩阵,低频类别的召回率惨不忍睹。我的做法是先统计每个类别的实例数量和图像数量,做成一张表,然后针对性地做处理。
| 类别 | 图像数 | 实例数 | 占比 |
|---|---|---|---|
| 正常驾驶 | 8200 | 9100 | 36.3% |
| 打电话 | 4100 | 4300 | 18.1% |
| 抽烟 | 2800 | 2950 | 12.4% |
| 喝水 | 2400 | 2500 | 10.6% |
| 低头看手机 | 1900 | 2000 | 8.4% |
| 双手离开方向盘 | 1500 | 1600 | 6.7% |
| 调收音机 | 1000 | 1050 | 4.4% |
| 回头说话 | 700 | 750 | 3.1% |
从这张表能看出来,最高频和最低频类别之间差了将近12倍。这种比例如果不做处理,模型基本等于只学了大头。我试过直接训练和做过采样后再训练,前者低频类别的mAP只有0.31,后者能拉到0.52左右,差距非常明显。
1.2 行为类别的类间相似性陷阱
驾驶员行为检测有个特别恶心的地方:很多动作在视觉上极其相似。比如"打电话"和"手放在耳边"这两个动作,如果只看手部位置,几乎一模一样,区别在于有没有手机这个物体。再比如"喝水"和"拿杯子",如果杯子被手挡住,模型很难判断到底是在喝还是只是拿着。
我在标注阶段就发现,不同标注员对"低头看手机"和"低头调空调"的判定标准不一致,导致同一张图在不同批次里可能被标成不同类别。这种标注噪声对模型的影响比想象中大,尤其是当类别本身边界模糊的时候。后来我专门做了一轮标注一致性检查,把那些模棱两可的样本挑出来重新审核,该合并的合并,该剔除的剔除。
提示:如果你的数据集里存在大量边界模糊的类别,不要急着训练,先花时间做一轮标注清洗。清洗带来的收益往往比换模型、调参数大得多。
1.3 驾驶员姿态的类内多样性
同一个行为,不同驾驶员的姿态差异可能非常大。打电话这个动作,有人用左手,有人用右手,有人把手机贴在左耳,有人贴在右耳,还有人用免提但手依然举着。抽烟也是,有人叼在嘴里,有人夹在手指间,有人刚点着还没抽。这些类内差异要求模型有足够强的特征提取能力,同时也意味着数据增强策略要更有针对性。
我试过用标准的随机翻转、随机裁剪、色彩抖动,效果一般。后来加了随机旋转(±15度)和随机遮挡(模拟手部或方向盘遮挡),低频类别的召回率提升了大概8个百分点。原因很简单:这些增强方式更贴近真实驾驶场景中会出现的姿态变化和遮挡情况。
2. YOLO版本选型与骨干网络取舍
2.1 为什么最终选了YOLOv8而不是v5或v7
YOLOv5和YOLOv7我都跑过,v5胜在生态成熟、文档多、社区活跃,v7在精度上比v5有提升但部署起来稍微麻烦一点。最终选YOLOv8,主要基于三个考虑。
第一,YOLOv8的C2f模块相比v5的C3模块,在特征复用上更充分,对于驾驶员行为这种需要同时关注手部细节和整体姿态的任务,多尺度特征融合的效果更好。第二,YOLOv8的Anchor-Free设计省去了聚类锚框的步骤,对于这种类别形状差异大的数据集,少了一个需要调的超参数。第三,Ultralytics的工程化做得确实好,训练、验证、导出、部署一条龙,省了很多自己写脚本的时间。
当然,YOLOv8也不是没有缺点。它的模型体积比v5同级别稍大,在边缘设备上推理速度会慢一些。如果你的部署平台算力有限,v5n或者v8n仍然是更务实的选择。我实测下来,在相同输入尺寸640x640下,v8s比v5s的mAP高约2.3个百分点,但推理耗时多了大概15%。这个取舍要看你的实际场景更看重精度还是速度。
2.2 骨干网络替换的尝试与教训
我试过把YOLOv8的骨干换成EfficientNet和MobileNetV3,想看看能不能在精度和速度之间找到更好的平衡点。EfficientNet的复合缩放策略理论上很美好,但实际替换后发现,在驾驶员行为数据集上,精度提升并不明显,反而因为特征图通道数变化导致颈部网络需要重新设计,训练不稳定,BN层崩溃了好几次。
MobileNetV3替换后推理速度确实快了,在同等精度下大概能快20%左右,但小目标检测能力下降明显。驾驶员行为检测里,手机、烟、杯子这些关键物体往往只占图像很小一块区域,骨干网络的下采样倍数太高会导致这些小目标在深层特征图上几乎消失。后来我放弃了骨干替换,回到原生YOLOv8,把精力放在数据增强和损失函数调优上,收益反而更大。
注意:骨干网络替换不是万能药。如果你的数据集里小目标占比高,换轻量骨干之前一定要先评估小目标检测指标,否则可能捡了速度丢了精度。
2.3 预训练权重的选择与微调策略
YOLOv8官方提供了在COCO上预训练的权重,直接拿来微调是最省事的做法。但我发现,COCO数据集里"人"这个类别的特征和驾驶员行为检测里的"人"有差异:COCO里的行人多是全身、远景、站立姿态,而驾驶员是半身、近景、坐姿。直接用COCO权重微调,前几个epoch损失下降很快,但后面容易过拟合到COCO的分布上。
我的做法是先用COCO权重做初始化,然后冻结骨干网络的前几层,只训练颈部网络和检测头,学习率设小一点(1e-4),跑10个epoch让模型先适应新数据集的分布。之后再解冻全部层,用余弦退火学习率从1e-3降到1e-5,跑100个epoch。这个两阶段微调策略比直接端到端训练,最终mAP高了大概1.8个百分点,而且训练曲线更平滑,不容易出现早期过拟合。
3. 数据增强与类别平衡的实操细节
3.1 Mosaic和MixUp在驾驶场景下的适配
Mosaic增强是YOLO系列的标配,把四张图拼成一张,能显著提升小目标检测能力和模型鲁棒性。但在驾驶员行为检测里,Mosaic有个副作用:拼接后的图像里,驾驶员可能被切得只剩半个身子,手部位置关系完全乱掉,模型学到的特征反而有噪声。
我试过把Mosaic的概率从默认的1.0降到0.5,同时把MixUp的概率从0调到0.1。MixUp是把两张图按透明度叠加,对于驾驶员行为这种需要精确判断手部与物体关系的任务,MixUp的叠加会引入大量模糊样本,低频类别的精度反而下降了。最后我的配置是Mosaic 0.6、MixUp 0.05,配合关闭最后10个epoch的Mosaic(close_mosaic=10),让模型在训练末期看到更多真实分布的数据。
3.2 针对低频类别的过采样与增强组合
对于样本量低于2000的类别,我做了两件事。第一是在数据加载器里给这些类别更高的采样权重,让每个batch里低频类别的出现概率提升。第二是针对这些类别做定向增强:抽烟类别增加随机遮挡(模拟手挡住烟)、打电话类别增加随机旋转(模拟不同持机角度)、低头看手机类别增加亮度变化(模拟不同光照下的屏幕反光)。
这些定向增强不是拍脑袋想的,而是先看了大量误检样本,分析模型到底在哪些情况下容易出错,然后针对性地构造增强。比如抽烟类别,模型经常把"手放在嘴边"误判成抽烟,我就专门增加了一批"手放在嘴边但没有烟"的负样本,让模型学会区分。
| 增强策略 | 适用类别 | 参数设置 | 效果 |
|---|---|---|---|
| 随机遮挡 | 抽烟、喝水 | 遮挡比例10%-30% | 召回率+6.2% |
| 随机旋转 | 打电话 | ±15度 | 召回率+4.8% |
| 亮度抖动 | 低头看手机 | 0.6-1.4倍 | 召回率+3.5% |
| 过采样 | 所有低频类 | 权重2-3倍 | mAP+5.1% |
3.3 验证集划分的坑:别让同一驾驶员出现在训练和验证集
这个坑我踩得比较深。一开始我是随机划分训练集和验证集,结果验证集mAP虚高,部署到实际视频流上效果差很多。后来分析发现,同一个驾驶员的多张图像被分到了训练集和验证集里,模型其实是在"认人"而不是"认行为"。因为同一个人的体型、穿着、车内环境在训练和验证集里都出现过,模型学到了这些身份特征,换个人就失效了。
正确的做法是按驾驶员ID划分,确保验证集里的驾驶员在训练集中从未出现过。如果数据集里没有驾驶员ID标注,至少也要按拍摄场景或时间段划分,避免同一段视频的帧被分到两个集合。我重新划分后,验证集mAP从0.78降到了0.71,但这个0.71才是真实的泛化能力。
提示:目标检测数据集的划分不能只看图像数量,要看数据来源的独立性。同一来源的样本必须整体划分到同一个集合。
4. 训练参数调优与损失函数观察
4.1 学习率与批次大小的配合逻辑
YOLOv8默认的学习率是0.01,批次大小16。这个配置在COCO上没问题,但在22600张的驾驶员行为数据集上,我建议根据实际显存调整。我用的是单卡24G显存,批次大小开到32,学习率相应调到0.02。这里有个经验公式:学习率大致与批次大小的平方根成正比。批次翻倍,学习率乘以1.4左右比较稳妥。
但也不是越大越好。我试过批次64、学习率0.04,训练初期损失震荡很厉害,BN层的统计量不稳定,mAP波动超过3个百分点。后来回到批次32、学习率0.02,配合warmup 3个epoch,训练就稳多了。warmup的作用是让模型在初期用小学习率慢慢适应数据分布,避免一开始就大步长导致梯度爆炸。
4.2 损失函数三项的监控与异常判断
YOLOv8的损失由三部分组成:边界框回归损失(box_loss)、分类损失(cls_loss)、分布焦点损失(dfl_loss)。训练时我习惯把这三项分开看,而不是只看总损失。
box_loss下降慢,说明边界框回归有问题,可能是锚框设置不合理(虽然v8是Anchor-Free,但回归目标的设计仍有影响)或者标注框质量差。cls_loss下降慢,说明类别区分度不够,可能是类别不平衡或者类间相似性太高。dfl_loss是v8特有的分布焦点损失,它反映的是边界框预测的分布学习情况,如果dfl_loss震荡大,通常是学习率太高或者批次大小太小。
我遇到过cls_loss在30个epoch后突然反弹的情况,排查后发现是数据加载器里的一个bug:某些低频类别的图像在过采样时被重复加载,导致模型在这些类别上过拟合,验证集分类损失上升。修复后重新训练,曲线就正常了。
4.3 早停与模型保存策略
YOLOv8默认每10个epoch保存一次检查点,训练结束后取最佳mAP的模型。但在实际项目中,我建议把保存间隔改成5个epoch,同时开启早停(patience=30)。驾驶员行为检测的训练曲线有时候会出现"平台期",mAP连续十几个epoch不涨,然后突然又涨一波。如果patience设太小,比如10,很容易在平台期就停了,错过后面的提升。
另外,我习惯在训练结束后,把最后10个epoch的模型都拿来做一次验证,有时候最佳mAP出现在倒数第3个epoch而不是最后一个。YOLOv8的best.pt保存的是历史最佳,但如果你中途改了学习率策略或者数据增强,历史最佳可能不是当前策略下的最优。手动对比一下更保险。
5. 模型评估与误检漏检的排查链路
5.1 混淆矩阵暴露的类别混淆问题
训练完成后,第一件事是看混淆矩阵。驾驶员行为检测的混淆矩阵通常有几个固定的混淆对:打电话和手放耳边、抽烟和手放嘴边、喝水和拿杯子。这些混淆对的根源是手部与物体的空间关系没有被模型充分学到。
我的排查步骤是这样的:先从混淆矩阵里找到混淆最严重的两个类别,然后从验证集里把这两个类别的误检样本全部挑出来,逐张看。如果误检样本里手部被遮挡的比例很高,说明模型过度依赖手部特征,需要增加遮挡增强。如果误检样本里物体本身就不清晰,说明标注质量有问题,需要回头清洗数据。
5.2 小目标漏检的定位与解决
驾驶员行为检测里,手机、烟、杯子这些关键物体往往只占图像的5%以下面积,属于典型的小目标。小目标漏检的排查可以从三个层面入手。
第一,看输入分辨率。YOLOv8默认640x640,如果原始图像是1920x1080,下采样后小目标可能只剩几个像素。我试过把输入尺寸提到960x960,小目标召回率提升了约7个百分点,但推理速度慢了将近一倍。折中方案是保持640训练,但在推理时用1280的输入尺寸,利用YOLO的多尺度推理能力。
第二,看特征金字塔。YOLOv8的P3层负责小目标检测,如果P3层的特征图分辨率不够,小目标就检测不到。可以通过修改模型配置文件,增加P2层来提升小目标检测能力,但计算量会增加不少。
第三,看数据增强。Mosaic增强会缩小图像中的目标,如果Mosaic概率太高,小目标在训练时变得更小,反而有害。我最后把Mosaic概率降到0.6,小目标召回率反而比1.0时高了。
5.3 误检样本的归因分析方法
误检比漏检更难处理,因为误检的成因更复杂。我总结了一套归因分析方法:把误检样本按置信度分三档(0.5-0.6、0.6-0.8、0.8以上),分别看。
置信度0.5-0.6的误检,通常是模型不确定导致的,可以通过提高置信度阈值来过滤,但会牺牲一部分召回。置信度0.6-0.8的误检,往往是类间相似性导致的,需要针对性增强。置信度0.8以上的误检,基本可以判定是标注错误或者数据分布问题,必须回头查数据。
我遇到过一批高置信度误检,模型把"调收音机"误判成"打电话",置信度0.9以上。查了原始图像发现,这些样本里驾驶员的手确实放在中控台附近,和打电话的手部位置有重叠。后来我把这批样本重新标注,并在训练时增加了中控台区域的负样本,误检率降了一半。
6. 部署落地时的工程化考量
6.1 模型导出与推理加速
训练完的PyTorch模型直接部署效率不高,通常要导出成ONNX或TensorRT。YOLOv8支持一键导出,但有几个细节要注意。导出ONNX时,opset版本建议用12或13,太低不支持某些算子,太高某些推理引擎不兼容。导出TensorRT时,FP16精度通常能带来1.5到2倍的速度提升,精度损失在1个百分点以内,性价比很高。
如果部署在边缘设备上,比如Jetson系列,建议用TensorRT的INT8量化。但INT8量化需要校准数据集,校准集的分布要和实际推理数据一致。我试过用训练集的一部分做校准,结果在实际视频流上精度掉了5个百分点,后来改用实际场景采集的未标注视频帧做校准,精度只掉了1.5个百分点。
6.2 视频流推理的帧间一致性处理
驾驶员行为检测通常是视频流输入,单帧检测会有抖动。比如打电话这个动作,可能第10帧检测到,第11帧没检测到,第12帧又检测到,这种闪烁在实际产品里体验很差。我的做法是加一个简单的跟踪器,比如ByteTrack,把帧间检测结果关联起来,只有连续3帧以上检测到同一行为才输出,否则视为噪声。
这个后处理逻辑看起来简单,但对实际体验提升很大。误报率降低了约40%,而且行为持续时间的统计也更准确。如果你不做跟踪,至少也要加一个滑动窗口投票,用最近5帧的检测结果做多数表决。
6.3 实际路测中的光照与遮挡挑战
实验室里跑得再好,上路测试总会遇到新问题。我印象最深的是夜间红外补光下的检测,训练集里夜间样本少,模型在夜间召回率只有白天的60%左右。后来补采了一批夜间数据,并增加了亮度抖动和对比度增强,夜间召回率才拉到85%以上。
另一个挑战是遮挡。驾驶员的手可能被方向盘挡住,脸可能被遮阳板挡住,这些遮挡在训练集里如果覆盖不够,模型就会漏检。我的经验是,遮挡增强的比例要占到总增强的20%以上,而且遮挡的位置要随机,不能只遮固定区域。
提示:实际路测的数据一定要回流到训练集里。我每跑一次路测,都会把误检漏检的样本挑出来,标注后加入下一轮训练。迭代三轮之后,模型在真实场景的mAP比初版高了12个百分点。
7. 一些零散但值钱的经验
数据集清洗永远比模型调参重要。我见过太多人花几周时间调网络结构,却不愿意花两天时间清洗标注。22600张图里,如果有5%的标注错误,模型精度至少掉3到5个百分点。清洗一遍,比换任何先进模型都管用。
类别不平衡的处理要趁早。不要等到训练完发现低频类别不行再回头处理,应该在数据准备阶段就做好过采样和增强策略。我现在的习惯是,拿到数据集第一件事就是画类别分布图,低于1000实例的类别全部标记为需要特殊处理。
验证集的划分要按来源独立。同一驾驶员、同一场景、同一时间段的样本必须整体划分,否则验证集指标没有参考价值。这个坑我踩过两次,每次都是部署后才发现问题。
训练日志要看得细。不要只看mAP,要看每个类别的precision、recall、AP,要看损失曲线的形状,要看学习率的变化。异常往往藏在细节里,比如某个类别的AP突然下降,可能是数据加载器出了问题,也可能是标注文件损坏。
模型导出后一定要做一致性验证。PyTorch模型和ONNX模型的输出可能有细微差异,TensorRT的FP16和INT8差异更大。导出后拿一批验证集数据,对比导出前后的检测结果,确保精度损失在可接受范围内。我遇到过ONNX导出后某个算子不支持,导致检测框全部偏移的情况,如果没做一致性验证,上线就是事故。
最后说一个关于数据集的体会。22600张听起来是个不小的数字,但在驾驶员行为检测这个任务上,真正决定模型上限的不是图像数量,而是场景覆盖度。白天、夜间、晴天、雨天、不同车型、不同驾驶员、不同摄像头角度,这些维度的覆盖比单纯堆图像数量重要得多。我宁愿要10000张覆盖20种场景的图,也不要20000张只覆盖5种场景的图。场景覆盖度上去了,模型的泛化能力自然就上去了,部署到新车上、新场景里,表现也不会差太多。