驾驶员行为检测这两年成了智能座舱和车队安全管理里绕不开的一个话题。不管是做DMS(Driver Monitoring System)的算法团队,还是给商用车队做安全预警的方案商,手里没有一批标注质量过硬的驾驶员行为数据,模型基本没法落地。这次我拿到的是一个22600张规模的YOLO格式驾驶员行为检测数据集,标签覆盖了打电话、抽烟、喝水、低头、双手离方向盘等常见分心动作。下面我把从数据检查、类别梳理、训练配置到调优排错的完整链路拆开讲一遍,尤其是那些只有真正跑过这类数据集才会遇到的坑,比如类别极度不均衡、小目标漏检、混淆矩阵对不上号这些问题,都会给出我自己的处理办法。
1. 拿到数据集先别急着训练:22600张背后的分布真相
很多人拿到一个两万多张的数据集,第一反应是直接丢进YOLO开跑,觉得数据量够大,效果差不到哪去。我踩过这个坑,结果训出来的模型在验证集上mAP看着还行,一上车载摄像头实测就崩。问题不在模型,在于你根本没搞清楚这批数据是怎么分布的。
1.1 驾驶员行为检测的类别体系到底该怎么定
驾驶员行为检测和通用目标检测最大的区别在于,它的类别定义直接决定了模型能不能用。通用COCO那套80类里没有"打电话""抽烟"这种语义,你得自己定。这批数据集我梳理下来,核心类别大致是这么几类:
- 正常驾驶:双手握方向盘、目视前方,这是基准类,样本量通常最大
- 打电话:单手或双手持手机贴耳,注意要区分"手持电话"和"手持其他物品"
- 抽烟:手部持烟靠近嘴部,这个类别的难点在于烟体本身很小,主要靠手部姿态判断
- 喝水/进食:手持瓶装水或食物靠近面部
- 低头/分神:头部明显下俯,视线离开前方路面
- 双手离开方向盘:这个类别和上面几类存在重叠,标注时要想清楚优先级
这里有个关键决策:类别之间是否互斥。比如一个人一边打电话一边低头,你标哪个?我的做法是定义优先级——危险动作优先于姿态类。打电话、抽烟这类明确危险行为优先级最高,低头、离手这类姿态类作为补充。如果不定义清楚,标注就会乱,模型学出来的边界也是模糊的。
提示:类别数不是越多越好。我见过有人把驾驶员行为拆成20多类,结果每类样本只有几百张,训出来的模型每类都半吊子。22600张的规模,控制在6到10个核心类是比较合理的。
1.2 用脚本快速摸清每类样本量和尺寸分布
在动手训练前,我习惯先写个脚本统计标注文件。YOLO格式的标签是每行class_id x_center y_center width height,全部归一化到0到1。统计逻辑很简单,遍历所有txt文件,累计每个类别的框数量,同时记录宽高的分布。
import os from collections import defaultdict label_dir = "labels/train" class_count = defaultdict(int) size_list = [] for fname in os.listdir(label_dir): if not fname.endswith(".txt"): continue with open(os.path.join(label_dir, fname)) as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue cid = int(parts[0]) w, h = float(parts[3]), float(parts[4]) class_count[cid] += 1 size_list.append((w, h)) for cid in sorted(class_count): print(f"class {cid}: {class_count[cid]} boxes") # 统计小目标占比(宽高都小于0.05的框) small = sum(1 for w, h in size_list if w < 0.05 and h < 0.05) print(f"small objects ratio: {small / len(size_list):.2%}")跑完这个脚本,你大概率会发现两个问题:一是类别严重不均衡,正常驾驶的框可能是打电话的十倍;二是小目标占比不低,尤其是抽烟、手机这类目标,归一化宽高经常在0.03到0.08之间。这两个发现直接决定了你后面的训练策略——不均衡要用重采样或focal loss,小目标要调anchor或者上更高分辨率的输入。
1.3 数据清洗:那些会让模型学歪的脏标注
两万多张的数据集,标注质量参差不齐是常态。我在检查时遇到过几类典型脏数据:框超出图像边界的、宽高为0的、同一张图里同一个目标被标了两次的、类别ID超出定义范围的。这些不清理掉,训练时loss会莫名其妙地跳,甚至直接NaN。
清理逻辑我一般这么写:先过滤掉宽高小于某个阈值(比如0.005)的框,再检查坐标是否在0到1之间,超出就裁剪回去。重复框的处理稍微麻烦点,用IoU判断,同一个类别IoU大于0.9的就认为是重复,保留一个。这些操作看着琐碎,但能省掉后面一大堆调参的功夫。
2. YOLO版本选型:为什么我在这类数据集上不盲目追新
数据集准备好了,接下来是选YOLO版本。现在网上热词里yolov8、yolov5、yolo3、mamba yolo、efficient head yolo一大堆,新手很容易被带偏,觉得版本越新越好。我的观点是:驾驶员行为检测这个场景,稳定和可复现比追新重要得多。
2.1 YOLOv5、v8、v11在驾驶员行为场景下的实测差异
我在同一批数据上对比过几个版本,结论如下表:
| 版本 | 训练速度 | mAP@0.5 | 小目标表现 | 部署友好度 | 我的评价 |
|---|---|---|---|---|---|
| YOLOv5s | 快 | 0.86 | 一般 | 极好 | 基线首选,生态最全 |
| YOLOv8s | 中 | 0.89 | 较好 | 好 | anchor-free省心 |
| YOLOv11s | 中 | 0.90 | 好 | 好 | 新项目可用 |
| YOLOv3 | 慢 | 0.81 | 差 | 一般 | 除非老项目兼容,否则不推荐 |
YOLOv5的优势在于资料多、踩坑记录全,遇到问题基本都能搜到答案。YOLOv8和v11是anchor-free的,省去了调anchor的麻烦,对小目标也更友好一些。但如果你团队里有人对v5的代码结构更熟,那就用v5,别为了新而新。我见过一个团队硬上最新版本,结果训练崩了没人会调,白白耽误两周。
2.2 anchor-based和anchor-free在驾驶员行为检测上的取舍
驾驶员行为检测的目标有个特点:尺度跨度大。方向盘占的画面比例可能到0.4,而一支烟可能只有0.02。anchor-based的方法需要你根据数据集的框分布去聚类anchor,如果anchor设得不好,小目标直接漏检。
我的做法是:如果用YOLOv5,先用k-means在训练集上重新聚类anchor。YOLOv5自带的anchor是针对COCO的,直接拿来用在这类数据集上不一定合适。聚类时把框的宽高提取出来,聚成9组,替换掉配置文件里的anchor。如果用YOLOv8/v11,anchor-free省了这一步,但要注意它的正样本匹配策略对小目标是否友好,必要时调整reg_max参数。
2.3 预训练权重的选择:别用错了backbone
热词里"yolo预训练模型下载"是个高频搜索,说明很多人卡在这一步。我的建议是:用官方在COCO上预训练的权重做初始化,但要注意输入分辨率匹配。如果你打算用640的输入,就用640预训练的权重;如果要用1280的高分辨率来抓小目标,最好找对应分辨率的权重,或者至少让预训练时的分辨率接近。
另外,如果你要做的是红外或者夜间场景的驾驶员检测,COCO预训练的权重帮助会打折扣,因为色彩分布差异大。这种情况下可以考虑先用大量无标注的驾驶场景图像做自监督预训练,再微调。不过这条路成本高,一般项目用COCO权重加数据增强就够了。
3. 训练配置:从输入分辨率到损失函数的每一个决策
配置这一块是最容易出问题的地方,也是新手和老手差距最大的地方。我把关键参数一个个拆开讲,每个都告诉你为什么这么设。
3.1 输入分辨率:640还是1280,这是个成本问题
驾驶员行为检测里,抽烟、打电话这些动作的关键信息集中在手部和面部的小区域。用640的输入,一个占画面0.03的烟体,映射到特征图上可能就剩几个像素,模型根本学不到。我实测下来,1280的输入对小目标的召回提升明显,mAP@0.5能涨3到5个点。
但代价是显存和速度。1280输入的显存占用大约是640的四倍,推理速度也慢不少。如果你的部署平台是车机或者边缘设备,算力有限,那就得权衡。我的折中方案是:训练用1280,推理时如果算力不够,用TensorRT做量化,INT8量化后速度能回来不少,精度损失控制在1个点以内。
3.2 batch size和显存的平衡:别硬撑
batch size的设置原则很简单:在显存不爆的前提下尽量大。但驾驶员行为检测的数据集类别不均衡,batch太小会导致每个batch里某些类别一个样本都没有,BN层的统计量会偏。我一般建议单卡batch至少16,多卡的话用--batch-size配合梯度累积。
如果你显存不够,可以用梯度累积模拟大batch。比如想要等效batch 64但只能放下16,就设accumulate=4。注意梯度累积时BN的统计还是按实际batch算的,所以如果实际batch太小(比如小于8),考虑换成GroupNorm或者SyncBN。
3.3 损失函数里那些容易忽略的细节
YOLO的损失一般由三部分组成:框回归损失、置信度损失、分类损失。驾驶员行为检测里,分类损失要特别关注,因为类别不均衡。正常驾驶的样本远多于危险行为,如果不处理,模型会倾向于把所有框都预测成正常驾驶。
我的处理办法有两个:一是用focal loss替代交叉熵,让模型更关注难分类的样本;二是在数据加载时做重采样,对稀有类别过采样。YOLOv5/v8默认用的是BCEWithLogitsLoss,你可以在loss.py里改成focal loss的实现。另外,如果发现训练中BN崩溃(热词里"yolo训练中bn崩溃"是个高频问题),大概率是batch太小或者学习率太大,先把学习率降一个数量级试试。
3.4 数据增强:哪些增强对驾驶员行为检测有效
数据增强不是越多越好,得看场景。驾驶员行为检测的图像通常来自固定视角的车内摄像头,所以几何变换要谨慎。水平翻转可以用,因为驾驶员左右手动作对称;但垂直翻转绝对不能用,倒过来的驾驶员不是真实场景。
我常用的增强组合是:Mosaic(YOLO自带,提升小目标效果明显)、随机缩放、色彩抖动(模拟不同光照)、轻微旋转(正负10度以内)。Mosaic在训练后期建议关掉,因为它会让图像边缘出现不自然的拼接,影响最终精度。YOLOv5/v8都有--close-mosaic参数,设成最后10个epoch关闭。
4. 训练过程中的排错:混淆矩阵对不上、loss不降、漏检
训练跑起来不代表就万事大吉了,真正的功夫在排错上。这一节我把自己遇到过的几个典型问题和解法完整复盘一遍。
4.1 混淆矩阵总合不唯一的排查链路
热词里"yolo混淆矩阵总合不唯一"这个问题我遇到过。现象是验证结束后,混淆矩阵里每一行的和跟实际样本数对不上。排查下来,根因通常是这几个:
第一,验证时用了增强。如果你在验证阶段还开着Mosaic或者随机缩放,同一个目标可能被检测多次,混淆矩阵自然对不上。验证时一定要关掉所有随机增强,用--augment=False。
第二,NMS的IoU阈值设置不当。NMS阈值太高,重叠的框没被抑制,同一个目标被算成多个预测;阈值太低,相邻目标被误抑制。驾驶员行为检测里,手部和手机经常挨得很近,NMS阈值我一般设0.5到0.6之间,需要根据实际数据调。
第三,类别ID映射错误。如果你的数据集类别ID不是从0连续编号的,而配置文件里写的是连续编号,映射就会错位。检查data.yaml里的names列表和标签文件里的class_id是否一一对应。
排查顺序我建议是:先确认验证无增强,再检查NMS阈值,最后核对类别映射。九成的问题出在前两步。
4.2 loss震荡不收敛的三种常见原因
loss震荡是训练中最常见的问题,我总结了三类原因:
- 学习率太大:这是最常见的。YOLO默认学习率0.01,但如果你换了数据集或者改了batch size,这个值不一定合适。我的经验是从0.001开始试,用余弦退火调度,观察前几个epoch的loss曲线。
- 数据标注噪声大:如果数据集里有大量错误标注,模型会在这些样本上反复震荡。回到第1节的数据清洗,把脏数据清掉。
- 类别不均衡导致的梯度冲突:稀有类别的梯度被多数类淹没,loss会在某个值附近来回跳。用focal loss或者重采样缓解。
判断方法很简单:如果loss在前几个epoch快速下降然后开始震荡,多半是学习率问题;如果从一开始就震荡,检查数据和配置。
4.3 小目标漏检:从特征图到anchor的逐层排查
抽烟、手机这类小目标漏检,是驾驶员行为检测最头疼的问题。我的排查思路是从后往前:
先看特征图。把模型中间层的特征图可视化出来,看看小目标在P3(stride 8)这一层还有没有响应。如果P3上都没响应,说明backbone下采样太狠,小目标信息丢了,这时候要么用更高分辨率输入,要么在backbone里加浅层特征。
再看anchor匹配。如果是anchor-based的模型,检查小目标的框有没有匹配到合适的anchor。用k-means重新聚类anchor,专门为小目标增加几个小尺寸的anchor。
最后看正样本分配。YOLOv8用的是TaskAlignedAssigner,对小目标的正样本分配可能不够。可以调整topk参数,或者换用更宽松的分配策略。
我实测下来,输入分辨率从640提到1280,小目标召回能提升10个点以上,这是最有效的手段。其次是重新聚类anchor,能再提升3到5个点。
5. 模型评估与部署:mAP之外的指标才决定能不能上车
模型训完,mAP看着不错,但这不代表能上车。驾驶员行为检测是安全相关的应用,漏检一个打电话的动作可能意味着一次事故。所以评估指标要看得更细。
5.1 除了mAP,你还要看每类的召回和误报
mAP是个综合指标,会掩盖单类的短板。我评估时一定会把每个类别的precision和recall单独拉出来看。驾驶员行为检测里,危险行为类别的召回率比精度更重要。宁可多报几个误报,也不能漏掉一个打电话的。
具体做法是在验证脚本里输出每类的PR曲线,重点关注打电话、抽烟、喝水这几类的recall。如果某类recall低于0.8,就得针对性处理——要么补数据,要么调该类别的损失权重。
另外,误报率也要关注。如果正常驾驶被大量误报成危险行为,用户体验会很差,系统会被司机直接关掉。我一般把正常驾驶类的precision控制在0.95以上。
5.2 部署时的量化和加速:精度和速度的平衡
部署到车机或者边缘设备,绕不开量化和加速。我常用的路径是:PyTorch模型导出ONNX,再用TensorRT做INT8量化。量化时要注意,校准集要覆盖各种光照和场景,否则量化后的精度会掉得厉害。
INT8量化后,模型体积能缩小到原来的四分之一,推理速度提升2到3倍。精度损失一般在1到2个mAP点,可以接受。如果对精度要求极高,可以用FP16,速度提升没那么大但精度几乎无损。
部署脚本这块,热词里"一键部署脚本yolo最新版本"是个高频需求。我的建议是别迷信一键脚本,自己把导出、量化、推理的每一步搞清楚,出了问题才知道怎么调。一键脚本省的是前期时间,但后期排错成本更高。
5.3 实际车载场景下的域偏移问题
实验室训出来的模型,上车后效果打折扣,这是域偏移。原因可能是摄像头型号不同、安装角度不同、光照条件不同。解决办法有两个:一是在训练数据里尽量覆盖多种摄像头和光照,二是部署后收集实际场景的数据做增量训练。
我一般会留一个在线难例挖掘的机制:部署后把模型置信度低或者误报的样本回传,人工标注后加入训练集,定期重新训练。这样模型能持续适应实际场景,效果会越来越稳。
6. 几个只有跑过这类数据集才知道的经验
最后分享几个我在实际项目里踩出来的经验,都是文档里不会写的。
第一,别忽略正常驾驶类。很多人把精力全放在危险行为上,结果正常驾驶类训得一塌糊涂,误报率飙升。正常驾驶类的样本质量和数量同样重要,它是整个系统的基准。
第二,标注一致性比标注精度更重要。同一个动作,不同标注员的框可能差几个像素,这没关系。但如果一个人标"打电话"另一个人标"手持物品",模型就学废了。标注前一定要统一标准,最好做一轮交叉验证。
第三,训练日志要留全。我习惯把每次训练的配置、loss曲线、评估结果都存档。调参是个反复的过程,没有日志你根本记不住上次改了什么、效果如何。用TensorBoard或者WandB都行,关键是坚持记录。
第四,小目标检测别只盯着模型改。数据层面能解决的,别急着改网络结构。提高输入分辨率、补充小目标样本、重新聚类anchor,这三招能解决大部分小目标问题,比改网络结构见效快得多。
第五,验证集要能代表实际场景。如果你的验证集全是白天清晰图像,而实际部署在夜间,那验证指标再高也没用。验证集的分布要尽量贴近部署环境,必要时按场景分层采样。
这套流程我在几个驾驶员行为检测项目里反复用过,22600张的数据集规模,从数据清洗到部署上线,大概两到三周能跑通一轮。关键不在于用了多新的模型,而在于每一步的决策都有依据,出了问题能定位到具体环节。数据集是死的,怎么用活它才是功夫。