做智能驾驶舱监控的朋友应该都有同样的体会:想找一份能直接开训的驾驶员行为检测数据集,难度比想象中大得多。市面上零散能搜到一些公开的驾驶员行为数据集,但要么数量太少,要么标注格式五花八门,拿到手先花两三天清洗转换,还没开始训练就耗掉了大半精力。我自己在做舱内DMS相关项目时,把收集到的原始图像整理成了一套22600张图的YOLO格式驾驶员行为检测数据集,类别覆盖正常驾驶、手持电话、低头操作手机、喝水饮食、疲劳状态和视线偏离六种常见行为。这篇文章就以这套数据集为主线,把整理思路、目录结构、标注细节、YOLO训练参数、踩坑记录、评估方法到部署优化完整讲一遍,适合想快速上手驾驶员行为检测的算法工程师,也适合正在准备相关毕业设计或竞赛的入门玩家直接抄作业。
1. 为什么我要整理这套驾驶员行为检测数据集
1.1 舱内监控的需求变化与公开数据的短板
驾驶员行为检测在智能驾驶里解决的是一个很具体的问题:车内的摄像头除了看路,还要看人。驾驶员是否在打瞌睡,是不是低头看手机,有没有单手扶着方向盘打电话,这些行为直接关系到行车风险等级,也是疲劳驾驶预警、分心驾驶提醒等功能的核心输入。
这几年智能驾驶行业对舱内监控的需求增长非常快,很多团队想上手做这件事,第一反应就是上网找现成的驾驶员行为数据集。但按照我的实际找数据经历,公开数据集的短板非常明显,归纳起来就是三个字:少、乱、窄。
“少”指的是数量。公开的驾驶员行为数据集很多只有几千张图,部分甚至只有几百张,还是按视频抽帧抽出来的,去重之后有效样本再打折。放到YOLO这类数据需求上不封顶的模型里,往往没跑几个epoch就过拟合了,验证集指标好看,一换上真实车内场景立刻露馅。
“乱”指格式。有的是VOC XML,有的是COCO JSON,还有自定义CSV表格。每种格式对应一种解析脚本,转换过程中稍不注意坐标缩放就出错。尤其是我这种整天在不同框架之间横跳的人,最烦的就是数据格式来回折腾。
“窄”指场景覆盖。很多公开数据是在固定实验室环境拍的,机位固定、光照固定、驾驶员就那么几个。真实驾驶场景里的逆光、夜间红外补光、车窗起雾、驾驶员戴墨镜、摄像头装在A柱下方等复杂情况,公开数据集基本覆盖不到。模型训练完拿到实际路测里,泛化能力几乎要打对折。
1.2 这套数据集的定位与整理思路
我整理这22600张驾驶员行为检测数据的初衷很简单:与其到处求人,不如自己攒一套工作流顺手、场景相对丰富、格式统一的数据集。整套数据的整理遵循了几个原则。
第一是格式统一为YOLO标准标签。所有图像对应同名学生txt文件,坐标全部归一化,直接放YOLOv5、YOLOv8里就能开训,免去格式转换环节。第二是类别按业务预警逻辑划分,而不是按“数据集好看”来划。第三是预留了明确的训练集、验证集、测试集划分,大家拿到手不用再自己费劲切分,可以直接横向对比各家模型的水平。
这套数据我按81%比例划分训练集约18100张,验证集2260张,测试集2240张。图像统一保存为jpg,原图保留较高分辨率,训练时再缩放到YOLO输入尺寸。后面所有经验教训,都是从这套数据上一次次跑出来的。
2. 数据集的构成与标注格式详细拆解
2.1 类别设计与图像来源
类别设计是整份数据集最核心的决策点,我前后调整了好几轮。市面上有些公开数据把打电话又细分成手持和免提,有些把玩手机和打电话合并,还有些单独列了化妆、抽烟、和乘客交谈等类别。类别划分越细,标注成本越高,模型学习难度也越大,但业务侧能得到更细的风控粒度。
我最终按照产品预警需求定下来六个类别,每个类别背后的业务含义都不一样。这张表可以直观看到六个类别的定义和覆盖行为:
| 类别ID | 行为名称 | 具体定义 |
|---|---|---|
| 0 | safe_driving | 正常驾驶,双手在方向盘上或处于合理驾驶位置 |
| 1 | hand_phone | 手持电话接听或拨打,单手脱离方向盘 |
| 2 | texting | 低头查看、打字操作手机,视线明显离开道路 |
| 3 | drink | 喝水、进食等手持饮料或食物动作 |
| 4 | fatigue | 闭眼、打哈欠、持续性瞌睡等疲劳状态 |
| 5 | looking_away | 视线离开前方,左顾右盼或转头回看 |
把“手持电话”和“低头操作手机”分开,是因为两种行为的风险等级差很多。hand_phone的驾驶员大多还偶尔看着路,texting基本是全程低头盯着屏幕,刹车反应时间明显变差。后面接预警策略时,这两种行为会触发不同等级的告警,合并成一个类会让产品侧没法精确做差异化处理。同样,drink和safe_driving之间的边界也很值得推敲,喝水这个动作持续时间短,但如果驾驶员正在喝一口水的同时突然遇到前车急刹,反应速度同样受影响。
图像来源方面,我刻意让样本覆盖多组变量,这是决定模型能否上车的关键。驾驶员个体覆盖不同性别、体型和非裔、亚裔、欧裔等不同族裔长相;摄像头安装位置包括仪表台、后视镜区域、A柱下方三种常见视角;时间段覆盖白天强光、傍晚逆光、夜间近红外补光;天气条件包括晴天、下雨、车窗起雾。这些变量在我整理数据时被当作硬性采样条件,而不是随机碰运气。
2.2 目录结构与YOLO标签格式说明
整个数据集严格遵循YOLO的目录规范,图像和标签分开放,但子目录结构保持一致。我最终采用的目录组织方式如下:
driver_behavior_dataset/ ├── images/ │ ├── train/ # 18100张 │ ├── val/ # 2260张 │ └── test/ # 2240张 ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ ├── data.yaml └── README.mdYOLO标签格式是每行一个目标框,五个字段依次是“类别ID 归一化中心点X 归一化中心点Y 归一化宽度 归一化高度”。拿一个真实标注文件举例:
0 0.5120 0.4180 0.2840 0.3720 2 0.5310 0.6620 0.1810 0.2130第一行表示safe_driving类别,边界框中心位于图像宽度方向的51.2%、高度方向的41.8%处,目标框宽度占整张图像宽度的28.4%,高度占37.2%。第二行是texting类别,模型读到这个文件会自动和同名图像建立对应关系。一张图里出现多少个目标,txt里就有多少行,顺序没有严格要求,但标注时保持“重要目标靠前”是个好习惯。
图像和标签文件的命名对应是整个数据集最容易出错的地方。YOLO官方逻辑是,images/train/day_00123.jpg对应labels/train/day_00123.txt,名称必须完全一致,包括大小写和扩展名。如果名字对不上,训练时这张图等于没有标签,存在目录里也是废数据。我从一开始就在命名环节做了脚本检查,确保每张图像都有对应的标签文件,反过来也一样。
data.yaml是训练时唯一的数据地图,路径写错会让整个训练进程直接报错。我的配置文件长这样:
train: /absolute/path/to/driver_behavior_dataset/images/train val: /absolute/path/to/driver_behavior_dataset/images/val test: /absolute/path/to/driver_behavior_dataset/images/test nc: 6 names: 0: safe_driving 1: hand_phone 2: texting 3: drink 4: fatigue 5: looking_away新手经常在路径上栽跟头。train和val如果写成相对路径,训练环境的工作目录一变就报“No such file or directory”。我的建议是直接写成绝对路径,或者用脚本根据当前目录动态拼接,宁可多写几行,也别让路径成为训练的绊脚石。
2.3 类别样本分布与不平衡处理思路
类别不平衡是驾驶员行为数据集的通病,因为正常驾驶的画面天然占大头。如果采集时不对少数类做针对性补采,训练结果会是灾难性的:模型学到的最优策略就是不管看到什么图像都往safe_driving上预测,因为这样准确率也能有六成以上,但业务完全没法用。
我的数据里各类目标框数量大致分布如下:
| 类别 | 目标框数量(约) | 占比 |
|---|---|---|
| safe_driving | 9800 | 31.8% |
| hand_phone | 5600 | 18.2% |
| texting | 6500 | 21.1% |
| drink | 3600 | 11.7% |
| fatigue | 2700 | 8.8% |
| looking_away | 2600 | 8.4% |
目标框总量约30800个,safe_driving占比还是最高,但其他五类合起来接近七成,模型不会一边倒。做类别平衡的时候我没有简单地把少数类复制粘贴,而是补采了不同场景下的真实样本。如果只是复制同一张图的标注,模型学到的其实是重复特征,泛化没有任何提升。
如果后续你想扩展类别,比如加入“抽烟”或者“与乘客交谈”,建议遵循同样的比例思路去补样本。新增类别样本量最好不要低于总量5%,低于这个线模型很容易把它当背景噪声忽略掉。
3. 基于YOLO的训练流程与关键参数选择
3.1 环境准备与训练命令
我用YOLOv8做示例,因为它的训练、验证、导出一体化程度高,对新手足够友好,给生产级项目用也够稳。环境准备没有太多花活,PyTorch 2.x配套CUDA环境即可,然后安装ultralytics库:
pip install ultralytics安装完成后用一行命令验证环境是否正常:
yolo predict model=yolov8n.pt source=https://ultralytics.com/images/bus.jpg能看到检测结果输出,说明安装成功。接下来把data.yaml里的绝对路径改成你本机实际路径,启动训练:
yolo detect train data=/path/to/driver_behavior_dataset/data.yaml \ model=yolov8m.pt \ epochs=150 \ batch=16 \ imgsz=640 \ device=0 \ patience=20模型规模我选yolov8m而不是s或n,核心原因是精度和速度的综合平衡。驾驶员行为检测要跑在车载设备上,算力不是无限充裕的。yolov8n推理最快但mAP掉得比较明显,尤其是在fatigue这种小目标类别上;s模型略好,但遇到逆光场景的鲁棒性不如m。m模型在主流消费级显卡上训练速度可控,部署到Jetson Orin等级别的边缘设备也能跑到30帧以上,是最稳妥的一个档位。
batch size的选择也值得展开讲。batch大小受显存约束,同时对BN层稳定性影响很大。我在24GB显存上跑yolov8m用了batch=16,如果只有12GB显存,建议直接降到8。一个常见的新手操作是在batch报OOM之后把imgsz从640降到320来省显存。这个操作极其不推荐,因为320分辨率下驾驶员手部、手机这类小目标的信息几乎丢光,模型精度会出现断崖式下跌。正确做法是保持imgsz=640不动,按显存余量降batch,或者干脆换轻量模型。
imgsz=640是YOLO默认输入尺寸,也是权衡选项。原图虽然接近1080P,但训练时YOLO会用letterbox等比缩放并填充灰边到640x640,目标框同步缩放。有人总想用更高分辨率训练来获得极致精度,但对驾驶员行为检测来说,640通常够了,因为关键目标不是远处的小车,而是画面中占比已经不小的驾驶员头部和手部。唯独一种情况我建议升到960:你的摄像头视角特别广,驾驶员在画面里占比很小,手机屏幕只有十几个像素,这时高分辨率训练对小目标召回有明显帮助。
3.2 预训练权重、增强策略与早停机制
训练启动时加载的预训练权重同样关键。COCO预训练权重yolov8m.pt已经让模型学会了很多通用的纹理、边缘、形状特征,这些底层视觉能力可以直接迁移到驾驶员行为检测。从零训练也不是不行,但22600张图对于从零训练来说还是偏少,模型需要更多epoch才能达到同等精度,还更容易收敛到次优解。我的习惯是直接用官方COCO权重启动,让迁移学习帮我们省掉前几十个epoch的底层特征摸索。
增强策略方面,YOLOv8默认开启Mosaic增强,把四张图拼成一张喂给模型,相当于间接扩大了训练样本的上下文组合。在驾驶员行为检测场景,Mosaic的收益很实在,因为它让模型见到更多样的背景组合。但有一个隐患:如果大量样本的目标框本来就贴近图像边缘,四图拼接会加剧边界框截断,导致模型学到残缺目标。这种情况建议把训练前10个epoch的mosaic关掉,等模型学出稳定的基础特征后再开启,或者把mosaic概率调低。
超参数中初始学习率0.01、动量0.937、权重衰减0.0005是YOLOv8的默认值,在多数目标检测任务里表现稳定,没有必要一开始就大改。真正值得关注的是早停参数,我设置patience=20,意思是验证集指标如果连续20个epoch没有提升,训练自动终止。这个机制能省去大量无效训练时间,同时也能在一定程度上防止后段过拟合。
训练结束后,runs/detect/train目录下会生成weights/best.pt和weights/last.pt两个权重文件。best.pt是验证集表现最好的权重,生产环境一律以它为准。last.pt是最新epoch的状态,主要用来断点续训,直接拿去推理反而不合适。
4. 训练过程中的三大拦路虎与排查记录
命令行里敲一条训练指令很容易,但实际跑起来几乎每个人都会遇到几个让人头疼的问题。下面这几个坑是我在训练这套数据时真实踩过的,每一个我都尽量还原了排查链路,希望能帮你缩短定位问题的时间。
4.1 标签坐标越界引发的NaN损失
第一次训练这套数据,跑到第8个epoch时loss曲线突然变成红色,控制台日志里开始出现NaN。正常情况下损失值应该平稳下降,出现NaN基本可以把怀疑范围锁定在数据和数值计算这两块。
我的排查链路是这样一步步走的:先怀疑学习率太大,把初始学习率从0.01降到0.001重新训练,结果问题照旧,说明学习率不是根因。接着怀疑模型实现有bug,拿官方COCO数据跑了一个epoch做对照实验,loss走势完全正常,说明模型和代码没问题,嫌疑集中在数据上。逐个扫描标签文件之后终于找到两个问题:有一张图的标注框宽度归一化值写成了1.32,明显超出图像范围;还有几个文件的类别ID写成6,而类别总数是6,合法ID应该从0到5,越界类别会让损失计算彻底乱套。
修复方法很简单,写一个几十行的脚本扫描所有标签文件,检查坐标是否落在0到1区间、类别ID是否小于类别总数,把异常行直接剔除。自从那次之后,“标签合法性检查”已经被我列入了任何数据集开训前的强制执行清单。
import os from pathlib import Path label_root = Path('labels/train') NUM_CLASSES = 6 for txt_path in label_root.glob('*.txt'): lines = txt_path.read_text().strip().splitlines() for idx, line in enumerate(lines): parts = line.split() if len(parts) != 5: print(f'{txt_path.name} 第{idx + 1}行字段数异常: {line}') continue cls_id = int(parts[0]) coords = [float(x) for x in parts[1:]] if cls_id < 0 or cls_id >= NUM_CLASSES: print(f'{txt_path.name} 类别越界: {cls_id}') if any(x < 0 or x > 1 for x in coords): print(f'{txt_path.name} 坐标越界: {coords}')比写检查脚本更重要的习惯是可视化。训练前随机抽上几百张图像,把标注框画到原图上肉眼检查一遍,确认框的位置、大小、类别都符合直觉。这一步看起来原始,但发现问题的效率比训练50个epoch看指标再回头猜要高出好几个量级。
4.2 疲劳类别指标上不去的根因
训练中期我遇到一个更加隐蔽的问题:fatigue类别的mAP始终比其他类别低将近10个百分点,训练集上看着正常,一到验证集recall就掉到0.5附近,怎么调参数都不见起色。
初始判断指向类别不平衡,因为fatigue目标框只占总量8.8%。但补采了一些样本之后,指标并没有同步涨上去,这让我意识到问题另有根源。后来我把fatigue类别的所有图像专门抽出来看了一遍,才发现两个现象:夜间红外补光图像里,驾驶员眼睛细节非常弱,闭眼和正常睁眼在画面上几乎没差别;同时疲劳状态下头部可能会有较大幅度的低垂,部分标注框只有20x20像素,在640分辨率下已经属于小目标范畴。弱纹理加小目标,模型根本找不到可学习的特征,样本再多也是白搭。
我用了三步组合拳才把这个问题压下来。第一步,重新定义fatigue的标注规则,只有闭眼状态明显、脸部区域在画面中占比足够且纹理清晰时才允许标注,模糊到人眼都难以确认的直接丢弃,宁缺毋滥。第二步,训练时对fatigue样本做针对性过采样,让少数类别每个epoch参与次数更多。第三步,把整体训练分辨率从640提高到960,专门强化小目标特征学习。三步执行完,fatigue类别的mAP从0.52涨到0.71,虽然还是低于全类别均值,但已经进入可用范围。
4.3 过拟合还是数据划分不合理?别急着调模型
训练到120个epoch时,我注意到训练集mAP已经摸到0.94,验证集只有0.83,当下第一反应是过拟合,马上加dropout、调权重衰减、提前早停,结果验证集指标纹丝不动。后来静下心把训练曲线和数据集分布放在一起对比,才意识到这根本不是过拟合,而是数据划分本身出了问题。
验证集里相当比例的图像来自夜间红外场景,摄像头视角也和训练集主流样本不同,光照分布整体更暗。模型在训练集里没怎么见过这种视角和亮度分布,验证集指标偏低是正常的分布外表现,说明模型的泛化边界还不够宽。解决思路不是防过拟合,而是从数据侧入手:一方面在训练集里补充夜间和不同视角样本,另一方面调整划分策略,按“采集场景”分组而不是纯随机划分。同一个场景的图像尽量不跨训练集和验证集,这样验证集数值才反映真实泛化能力,而不是因为训练集和验证集装了同一场景的图像而虚高。
这个案例的教训是:看到训练验证差距大,先别急着动模型结构,回头检查一下数据划分逻辑,往往收获更大。
5. 评估指标解读:别只看mAP一个数
训练完模型,扫一眼mAP就下结论,是我见过最普遍的操作误区。在驾驶员行为检测场景,mAP只是及格线,业务真正关心的是漏报率和误报率,而这两件事单看mAP根本看不出来。
5.1 mAP、Recall与混淆矩阵在驾驶员检测里的特殊含义
YOLOv8验证输出会打印mAP@50、mAP@50-95、precision、recall四组数。mAP@50是IoU阈值0.5下的平均精度,mAP@50-95是0.5到0.95每隔0.05取一个IoU阈值再求平均。驾驶员行为检测的目标框本身不需要像素级精准,框稍微偏一点都不影响判断行为类型,所以我建议重点看mAP@50,它更真实地反映“目标有没有被检出来”这个核心问题。
更关键的是precision和recall的组合。驾驶员行为检测的漏检和误检代价严重不对称:漏掉一个疲劳状态,可能导致重大事故;误报一次喝水动作,顶多是中控台叮一声。因此在这个场景里,我宁可让模型偏激进一些,多误报也不能漏报,尤其是fatigue和texting这两个类别的recall,必须单独盯住。
混淆矩阵是评估环节最容易被人忽略但价值最高的工具。YOLOv8训练完成后会自动在runs/detect/train下生成混淆矩阵图,横轴真实类别,纵轴预测类别,对角线越亮越好。通过混淆矩阵可以一眼看出类别间的混淆模式。我在这套数据上看到过texting被误判成hand_phone、looking_away被误判成safe_driving的典型模式。前者是危险行为被降级处理,后者是分心状态被完全漏掉,两类都是业务侧不能接受的错误。这些风险和类别细节光看mAP无论如何也发现不了。
5.2 验证脚本与实测指标参考
自己复现验证指标的命令很直接:
yolo detect val data=/path/to/driver_behavior_dataset/data.yaml \ model=/path/to/best.pt \ batch=16跑完会在当前目录生成results.csv和confusion_matrix.png等文件。我自己的习惯是每次验证后把results.csv按日期重命名留存,这样不同模型、不同时间点的改动效果都有横向可比性。回头复盘时能清楚地看到哪次改动有效、哪次改动是白忙活,而不是靠记忆猜。
实测来看,yolov8m在这套数据上训练到收敛,mAP@50能稳定在0.90到0.93之间,mAP@50-95在0.70到0.75左右,整体recall在0.88上下。这个水平满足多数舱内预警产品的早期要求。如果某些类别指标明显低于均值,回到第4节的三个坑里检查一遍,多数问题都能对号入座。
6. 部署到智能驾驶场景的实用经验
验证集指标好看只是第一步,模型真正上车跑起来,才会遇到各种工程细节问题。部署环节是算法和工程之间差距最明显的分水岭。
6.1 模型导出与TensorRT加速
训练好的best.pt是PyTorch格式,直接拿去推理效率太低,转成ONNX再优化成TensorRT引擎才是车载部署的标准路径。ONNX导出一条命令:
yolo export model=/path/to/best.pt format=onnx opset=12导出之后建议用onnxruntime自带的工具做一次输入输出shape校验,确认没有导出错误。转TensorRT时我习惯开启FP16精度,精度损失通常能控制在1%以内,推理速度接近翻倍。实测在Jetson Orin Nano设备上,yolov8m输入640x640跑FP16,单帧推理大约25到35毫秒,已经能满足实时检测需求。如果设备算力更紧张,可以把模型蒸馏到s档,或者用imgsz=480版本重新训练,而不是直接拿640模型硬改推理尺寸。直接改推理尺寸会让目标框精度明显下降,远不如用低分辨率重新训练一版来得稳。
6.2 推理端分辨率选择与增强策略的延续
部署阶段的输入分辨率需要重新权衡一次。如果边缘设备算力有限,降低推理分辨率是最直接的加速手段,但一定要让训练和推理的输入尺寸保持一致,或者至少处于同一档位。我见过一个团队用640训练、416推理,结果小目标漏检率直接翻倍,后来让他们用416重新训练了一版,问题立刻缓解。
训练阶段的色彩增强策略不能到推理阶段就扔掉。驾驶场景光照变化剧烈,白天进隧道、夜间对向远光、黄昏逆光,都会给模型推理带来额外压力。训练时如果设置了hsv_h、hsv_s、hsv_v这类色彩扰动参数,模型在训练中就已经“见过”类似的光照变化,推理遇到真实环境变化时表现会稳定很多。这也是为什么我反复建议大家不要为了省时间关掉这些默认增强。
6.3 难例回流驱动的持续迭代
模型部署之后不是终点,而是数据迭代的起点。我现在坚持的流程是:每次实际运行中采集到误报或漏报片段,先截帧保存,再定期交给标注人员补标,丢回训练集做增量训练。这套流程坚持两到三个月后,模型在夜间逆光场景的漏报率出现了肉眼可见的下降,比频繁更换模型结构迭代的效果明显得多。
夹带一点个人体会:驾驶员行为检测这个方向,模型结构的选择空间其实不大,大家用的都是YOLO系列或者类似的目标检测框架,真正拉开差距的永远是数据质量和迭代节奏。整理好一套格式统一、场景丰富、类别合理的数据集,再配上规范的训练和评估流程,这个项目就已经成功了一大半。剩下的,就是在一轮又一轮的踩坑、修复、复测中把细节磨扎实。