☰
YOLO驾驶员行为检测实战:22600张数据集训练与调优全流程
2026/10/1 4:03:29 网站建设 项目流程

驾驶员行为检测这几年在智能座舱和商用车队管理里越来越热,但真正动手做项目的人都知道,最卡脖子的往往不是模型结构,而是数据。你要检测抽烟、打电话、喝水、双手离方向盘、扭头张望这些动作,公开数据集要么类别对不上,要么场景太单一,要么标注质量堪忧。我最近拿到一份22600张的YOLO格式驾驶员行为检测数据集,从整理、清洗到训练跑通完整走了一遍,中间踩的坑比想象中多。这篇就把整个流程拆开讲清楚:这份数据集到底包含什么、YOLO格式的目录该怎么组织、类别不平衡怎么处理、训练参数怎么调、以及那些只有真正跑过一遍才会知道的细节。不管你是刚接触目标检测的新手,还是已经做过几个YOLO项目想切入智能驾驶场景的老手,应该都能从里面找到能直接用的东西。

1. 先搞清楚这份数据集到底装了什么

拿到一个数据集,最忌讳的就是直接丢进训练脚本开跑。我见过太多人这么干,结果训练到一半发现类别对不上、图片损坏、标注越界,白白浪费几个小时甚至一整天的GPU时间。所以在动手之前,先把数据集的"家底"摸清楚,这一步花二十分钟,后面能省你半天。

1.1 22600张图片的构成与场景分布

这份数据集总共22600张图片,全部是驾驶员行为相关的场景。从实际翻看的情况来看,拍摄视角以车内固定摄像头为主,大致可以分为几类:正对驾驶员的面部视角、从副驾或A柱斜向拍摄的侧视角、以及少量从车顶向下俯拍的视角。这种多视角的构成其实挺重要,因为实际部署时摄像头安装位置千差万别,如果训练数据只有单一视角,模型换个安装位置就废了。

场景光照条件覆盖得还算全面,白天自然光、夜间红外补光、隧道明暗交替、逆光强曝光这些都有涉及。我特意统计了一下,夜间和弱光场景大概占三成左右,这个比例对于驾驶员行为检测来说是合理的,毕竟夜间疲劳驾驶和违规行为反而更高发。图片分辨率不统一,有1920×1080的,也有1280×720的,还有一部分是640×480的,这个后面预处理的时候要统一处理。

从行为类别来看,覆盖了驾驶场景下最常见的几类动作:正常驾驶、打电话、抽烟、喝水/进食、双手离开方向盘、扭头与乘客交谈、低头看手机、打哈欠/疲劳。类别数量不算特别多,但都是实际业务里真正需要检测的高频行为,没有那种凑数的冷门类别。

1.2 YOLO格式的目录结构与标注规范

这份数据集是标准的YOLO格式,目录结构大概是这样的:

dataset/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yaml

图片和标签文件一一对应,文件名相同,只是扩展名不同(图片是.jpg,标签是.txt)。每个标签文件里,每一行代表一个目标,格式是:

class_id x_center y_center width height

这里有个新手特别容易搞混的点:x_center、y_center、width、height这四个值全部是归一化到0到1之间的相对值,不是像素值。也就是说,x_center = 目标中心点x坐标 / 图片宽度。我刚开始做检测的时候,就因为把像素值直接写进去,训练了半天loss不降,排查了好久才发现是标注格式的问题。

data.yaml文件里定义了类别数量和类别名称,大概长这样:

train: ./images/train val: ./images/val test: ./images/test nc: 8 names: ['normal', 'phone', 'smoke', 'drink', 'hands_off', 'turn_head', 'look_down', 'fatigue']

提示:拿到数据集第一件事就是打开data.yaml确认nc和names,然后随机抽十几个标签文件,对照图片看看标注框位置对不对。这一步能帮你提前发现标注错位、类别编号错乱等致命问题。

1.3 类别分布与不平衡情况摸底

类别不平衡是目标检测里的老问题,驾驶员行为检测尤其明显。正常驾驶的样本数量远远多于抽烟、打电话这些违规行为,这是符合现实的,但对训练很不友好。我实际统计了一下这份数据集的类别分布,大致情况如下:

类别大致占比说明
normal(正常驾驶)约35%数量最多,容易导致模型偏向
phone(打电话)约15%高频违规行为
smoke(抽烟)约12%中等频率
drink(喝水进食)约10%中等频率
hands_off(双手离盘)约8%较难检测
turn_head(扭头)约8%姿态变化大
look_down(低头)约7%与疲劳有重叠
fatigue(疲劳打哈欠)约5%数量最少

这个分布意味着,如果你不做任何处理直接训练,模型会倾向于把大部分目标预测成normal,因为这样"蒙对"的概率最高。后面我会专门讲怎么处理这个问题。

2. 训练之前必须做的数据清洗与预处理

很多人觉得数据清洗是脏活累活,能跳过就跳过。但我可以负责任地说,在驾驶员行为检测这个场景里,数据清洗带来的收益往往比换一个更先进的模型结构还大。原因很简单:车内场景的干扰因素太多了,方向盘遮挡、安全带横穿、后视镜反光、乘客入镜,这些都会让标注变得模糊,而模糊的标注会直接教坏模型。

2.1 标注越界与零面积框的批量排查

YOLO格式要求所有坐标都在0到1之间,但实际标注过程中,标注员手一抖就可能标出越界的框。更隐蔽的是零面积框,就是width或height等于0的情况,这种框在训练时会直接导致loss计算出NaN。我写了一个脚本批量排查这两类问题:

import os import glob def check_labels(label_dir): issues = [] for txt_file in glob.glob(os.path.join(label_dir, '*.txt')): with open(txt_file, 'r') as f: lines = f.readlines() for i, line in enumerate(lines): parts = line.strip().split() if len(parts) != 5: issues.append((txt_file, i, 'field_count', line.strip())) continue cls, x, y, w, h = map(float, parts) if not (0 <= x <= 1 and 0 <= y <= 1): issues.append((txt_file, i, 'center_out', line.strip())) if w <= 0 or h <= 0: issues.append((txt_file, i, 'zero_area', line.strip())) if w > 1 or h > 1: issues.append((txt_file, i, 'size_out', line.strip())) return issues issues = check_labels('./labels/train') print(f'共发现 {len(issues)} 处问题') for item in issues[:20]: print(item)

跑完这个脚本,我在这份数据集里发现了大概一百多处问题,主要集中在零面积框和中心点越界。处理方式很简单:零面积框直接删除该行,中心点越界的把坐标裁剪回0到1范围。但要注意,如果一个标签文件里所有行都被删光了,那这个文件对应的图片也要从训练集里剔除,否则会出现"有图无标签"的情况,某些训练框架会报错。

2.2 重复图片与近似重复的识别

数据集里存在重复图片是很常见的,尤其是从视频抽帧生成的数据集。重复图片会导致训练集和验证集之间发生数据泄漏,让验证指标虚高,实际部署时性能大打折扣。识别完全重复的图片可以用MD5哈希,但近似重复(比如同一帧稍微裁剪了一下)就需要用感知哈希或者特征相似度。

我一般用imagehash库做感知哈希,阈值设在5左右:

from PIL import Image import imagehash import glob def find_duplicates(img_dir, threshold=5): hashes = {} duplicates = [] for img_path in glob.glob(f'{img_dir}/*.jpg'): img = Image.open(img_path) h = imagehash.phash(img) for existing_hash, existing_path in hashes.items(): if abs(h - existing_hash) <= threshold: duplicates.append((img_path, existing_path)) break else: hashes[h] = img_path return duplicates

这份数据集我跑下来发现近似重复的比例不算高,大概2%左右,主要集中在连续帧抽样的部分。处理策略是保留其中一张,其余的从训练集移除。但这里有个细节:如果重复图片分别落在训练集和验证集里,一定要确保移除后两个集合没有交集,否则验证结果不可信。

2.3 图像尺寸统一与letterbox处理

前面提到这份数据集图片分辨率不统一,训练前需要统一尺寸。YOLO系列通常用640×640作为输入尺寸,但直接resize会改变宽高比,导致目标变形。正确做法是letterbox:保持宽高比缩放,然后用灰色填充到目标尺寸。

这里有个容易忽略的点:letterbox之后,标注框的坐标也要跟着变换。如果你用的是Ultralytics的YOLOv8,它的数据加载器会自动处理letterbox和坐标变换,你不需要手动改标签。但如果你自己写数据加载器,就必须同步更新坐标,否则框会错位。

我实测下来,对于驾驶员行为检测,640×640的输入尺寸基本够用,因为驾驶员在画面里占的比例通常比较大。但如果你的摄像头安装位置比较远,驾驶员在画面里很小,那可能需要提到960甚至1280。这个要根据实际场景来定,不能一概而论。

3. 用YOLOv8跑通第一版基线模型

数据清洗完之后,先别急着调参优化,第一步是跑通一个基线模型,确认整个流程没问题。基线模型不需要多高的精度,它的作用是给你一个参照点,后面所有的改进都要跟它对比。

3.1 环境搭建与依赖版本选择

YOLOv8用Ultralytics的官方库是最省事的,安装就一行命令:

pip install ultralytics

但这里有个坑:Ultralytics的版本更新非常快,不同版本之间的API和默认参数可能有差异。我建议锁定一个稳定版本,比如8.0.x系列,避免今天跑通的代码明天就报错。另外,PyTorch的版本要和CUDA版本匹配,这个不用多说,装之前先nvidia-smi看一下驱动支持的CUDA版本。

我用的环境是Python 3.10 + PyTorch 2.0 + CUDA 11.8 + Ultralytics 8.0.196,这套组合实测比较稳。如果你用的是更新的50系显卡,可能需要PyTorch 2.7以上才支持,这个要提前确认。

3.2 data.yaml配置与路径陷阱

data.yaml看起来简单,但路径配置是最容易出问题的地方。Ultralytics支持相对路径和绝对路径,相对路径是相对于你运行训练命令时的工作目录,不是相对于data.yaml文件本身。这个细节坑过很多人。

我的建议是直接用绝对路径,省得来回折腾:

train: /home/user/dataset/images/train val: /home/user/dataset/images/val test: /home/user/dataset/images/test nc: 8 names: ['normal', 'phone', 'smoke', 'drink', 'hands_off', 'turn_head', 'look_down', 'fatigue']

还有一个细节:names列表的顺序必须和标签文件里的class_id严格对应。比如class_id为0的必须是normal,如果顺序写反了,模型学出来的类别就全乱了。这个错误很隐蔽,因为训练loss看起来正常,但推理结果完全不对。

3.3 基线训练命令与关键参数解读

基线训练命令其实很简单:

yolo detect train \ data=data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ device=0 \ project=runs/driver \ name=baseline

这里几个参数值得展开说。model选yolov8n.pt是因为它最小最快,适合先跑通流程。等流程验证没问题了,再换yolov8s或yolov8m提升精度。epochs设100是起步值,实际要看loss曲线什么时候收敛。batch=16是显存和训练稳定性的折中,显存够的话可以往上加,但要注意学习率也要相应调整。

注意:第一次训练建议先用小规模数据(比如从训练集里抽2000张)跑10个epoch,确认整个流程没问题再上全量数据。全量数据跑一次动辄几个小时,流程有问题的话时间全浪费了。

3.4 训练过程监控与loss曲线判读

训练启动后,重点看几个指标:box_loss、cls_loss、dfl_loss,以及验证集上的mAP50和mAP50-95。box_loss反映定位精度,cls_loss反映分类精度,dfl_loss是YOLOv8特有的分布焦点损失,影响框的回归质量。

正常情况下,三个loss都应该稳步下降,然后趋于平缓。如果box_loss下降但cls_loss不降,说明定位学得还行但分类有问题,可能是类别不平衡或者标注类别有误。如果loss震荡剧烈,通常是学习率太大或者batch太小。

我跑基线的时候,前20个epoch loss下降很快,30到60个epoch进入平台期,60之后基本没什么变化。最终mAP50大概在0.82左右,mAP50-95在0.55左右。这个成绩作为基线是合格的,但离实际部署还有距离,尤其是hands_off和fatigue这两个类别的召回率偏低。

4. 类别不平衡与难例的针对性处理

基线跑通之后,就要开始解决具体问题了。这份数据集最突出的问题就是类别不平衡,normal类别太多,fatigue类别太少,导致模型对少数类的检测能力很弱。另外,hands_off和look_down这两个类别因为姿态变化大、与相邻类别界限模糊,也是难啃的骨头。

4.1 过采样、欠采样与focal loss的取舍

处理类别不平衡有三条路:过采样少数类、欠采样多数类、改损失函数。我三种都试过,说说实际效果。

过采样就是把fatigue和look_down这些少数类的图片复制多份,让它们在训练集里的比例提上来。好处是简单直接,坏处是容易过拟合,因为同一张图反复出现,模型会记住而不是学会。我的做法是配合数据增强一起用,每次过采样的时候随机施加不同的增强,这样每份"副本"其实都不一样。

欠采样就是随机丢弃一部分normal类别的图片。这个方法的副作用是浪费数据,而且normal类别里其实有很多边界情况(比如手放在方向盘上但没完全握住),丢掉可惜。我不太推荐。

focal loss是我最终采用的方法。它通过降低易分类样本的权重,让模型更关注难分类的样本。YOLOv8默认用的是BCE loss,改成focal loss需要修改源码或者用支持自定义loss的版本。实测下来,focal loss对fatigue类别的召回率提升明显,从基线的0.61提到了0.74。

4.2 针对hands_off与look_down的标注复核

这两个类别的问题不完全在数据量,而在标注一致性。hands_off的定义是"双手离开方向盘",但实际标注中,单手离开算不算?手搭在方向盘上但没握算不算?这些边界情况如果标注标准不统一,模型就学不明白。

我的做法是抽了500张这两个类别的图片,逐张复核标注,把模糊的、有歧义的挑出来重新标。这个过程很枯燥,但效果立竿见影。复核之后重新训练,hands_off的mAP50从0.71提到了0.83。

look_down的问题是和fatigue有重叠,低头看手机和低头打瞌睡在视觉上很像。解决办法是在标注时严格区分:看手机必须有手机入镜,打瞌睡必须有闭眼或打哈欠的特征。如果两者都不明显,就归到normal里,宁可漏标也不要错标。

4.3 数据增强策略的定制化调整

YOLOv8默认的数据增强包括mosaic、mixup、随机翻转、HSV调整等。这些通用增强对驾驶员行为检测不一定都合适。比如随机翻转,水平翻转后"左手打电话"变成"右手打电话",如果实际场景里两种都常见那没问题,但如果你的应用只关心"是否打电话"这个二分类,翻转反而增加了不必要的多样性。

我针对这个场景做了几处调整:关闭上下翻转(驾驶员不会倒着开车),降低mosaic的概率(mosaic会把四张图拼在一起,车内场景拼出来很违和),增强HSV的饱和度扰动(应对不同光照条件)。这些调整都是基于对场景的理解,没有放之四海皆准的答案。

5. 模型选型与推理部署的实测对比

训练出模型只是第一步,真正落地还要考虑推理速度、模型大小、部署环境。这份数据集训练出来的模型最终要跑在车机或者边缘设备上,所以模型选型不能只看精度。

5.1 YOLOv8n/s/m在驾驶员行为检测上的精度速度权衡

我把三个尺度的模型都训了一遍,在同一台机器上测了推理速度,结果如下:

模型mAP50mAP50-95参数量GPU推理(ms)CPU推理(ms)
YOLOv8n0.820.553.2M6.885
YOLOv8s0.870.6111.2M12.4210
YOLOv8m0.890.6425.9M24.1480

从数据看,YOLOv8s是性价比最高的选择,精度比n高了5个点,速度还能接受。m的精度提升有限但速度慢了一倍,除非你的硬件很充裕,否则不太划算。如果部署在纯CPU设备上,那只能选n,而且还要做量化加速。

5.2 ONNX导出与TensorRT加速的踩坑记录

Ultralytics支持一键导出ONNX:

yolo export model=runs/driver/baseline/weights/best.pt format=onnx opset=12

但导出之后用onnxruntime推理,经常遇到输出维度对不上的问题。这是因为YOLOv8的ONNX输出格式和YOLOv5不一样,v8的输出是[1, 84, 8400]这种形状,需要自己写后处理解码。我建议直接用Ultralytics的推理接口,或者参考官方提供的后处理代码,不要自己瞎写。

TensorRT加速在NVIDIA设备上效果很明显,FP16量化后速度能提升2到3倍,精度损失在1个点以内。但TensorRT的版本兼容性很坑,引擎文件不能跨版本使用,换一台机器就要重新构建。如果部署环境固定,TensorRT是首选;如果环境多变,ONNX更省心。

5.3 实际车载场景下的误报与漏报分析

实验室指标好看不代表实际好用。我把模型接到实际车载视频上跑了一遍,发现几个典型问题。

误报最多的是把"调整后视镜"误判成"扭头",因为两个动作的头部姿态确实很像。解决办法是加入时序信息,单帧判断不了就看连续几帧的动作趋势。漏报最多的是"低头看手机",因为手机屏幕小、反光强,有时候模型根本看不到手机。这个只能靠提高输入分辨率或者专门训练一个手机检测的子模型来辅助。

还有一个容易被忽略的问题:驾驶员戴墨镜时,疲劳检测基本失效,因为看不到眼睛。这种情况下只能依赖打哈欠的嘴部特征,或者干脆把疲劳检测降级为"长时间无动作"的辅助判断。

6. 从数据集到落地项目的几点经验

做完这个项目,有几个体会特别深,都是文档里不会写、只有真正跑过一遍才知道的东西。

第一,数据集的类别定义比数量重要得多。22600张听起来不少,但如果类别边界模糊,再多数据也训不出好模型。我花在复核标注上的时间,比调参的时间还多,但我觉得值。

第二,不要迷信大模型。在驾驶员行为检测这个相对封闭的场景里,YOLOv8s已经能到0.87的mAP50,实际业务够用了。与其花时间堆模型规模,不如把数据质量和后处理逻辑做好。

第三,时序信息是绕不开的。单帧检测永远解决不了"扭头"和"调整后视镜"这种动作相似的问题,必须引入连续帧的分析。我后来加了一个简单的滑动窗口投票机制,误报率直接降了一半。

第四,部署环境决定技术选型。如果你的目标平台是车机,那模型大小和推理速度的优先级要高于精度;如果是云端分析,那可以放开手脚用大模型。这个顺序不能搞反。

最后说一个具体的技巧:训练的时候把验证集按场景分组,比如白天一组、夜间一组、逆光一组,分别看指标。这样你能清楚知道模型在哪种场景下弱,针对性补数据。我一开始只看总体mAP,觉得0.87挺好了,分组一看才发现夜间场景只有0.72,后来专门补了夜间数据才提上来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询