简介:面向疲劳驾驶与分心行为检测的嗜睡数据集,涵盖头部下垂、唤醒、昏昏欲睡等典型状态,适合目标检测算法学习者与开发者快速上手。压缩包共2000个文件,以XML标注文件为主,并配套YOLO格式TXT标签,整体大小172.36MB;数据集已预先划分好训练、验证和测试集,内置data.yaml配置文件,可无缝适配YOLOv5、YOLOv7、YOLOv8、YOLOv9、YOLOv10及YOLO11等主流算法。标签覆盖头部下垂、唤醒、昏昏欲睡、分心、吸烟、打哈欠、电话共七类行为,同时提供yolo(txt)与voc(xml)两种标注格式,便于不同训练框架直接调用。目前已有91人学习下载,数据可直接复用,无需二次整理。该资源省去手动标注环节,拿到后即可开展训练与验证;初学者还能对照两种标签格式,理解yolo归一化坐标与voc绝对坐标的差异,非常适合作为驾驶员状态监测、安全驾驶预警等场景的算法验证数据集。
1. 嗜睡行为数据集不是解压即用:8979 张图背后的 7 类标签关系
解压这个 zip 之后,先别急着跑yolo train。这 8979 张图像带标签的嗜睡数据集,标注里包含头部下垂、唤醒、昏昏欲睡、分心、吸烟、打哈欠、电话 7 个行为类别,正好覆盖 DMS 驾驶员监控系统里最难处理的疲劳行为和分心行为组合。它的定位很明确:让你不用从零收集数据、不用请人标框,就能把一个多类别行为检测的底座搭起来。适合两类人——一类是要做车载 DMS、疲劳驾驶预警产品验证的工程师;另一类是拿 YOLO 做毕业设计或竞赛、需要真实带标签数据的学生。但这不是一个解压就能直接训出好模型的包,标签之间的关系、类别数量差异、连续帧图像的分组方式,都会在训练中变成实实在在的坑。下面按我实际处理这类行为数据的流程,一步步拆开。
2. 把 7 类嗜睡标签转成 YOLO 格式:类别编号、坐标归一化与目录规划
2.1 先定类别表:唤醒类是负样本锚点,不能删
拿到数据集第一件事不是看图像,而是把标注里的类别名称列出来,写成一份固定的类别映射表。这个数据集里的 7 个标签,我一般会按下表编号:
| 编号 | 类别名 | 对应标签 | 训练中的作用 |
|---|---|---|---|
| 0 | head_down | 头部下垂 | 疲劳正样本,与 drowsy 常共现 |
| 1 | awake | 唤醒 | 正常驾驶基线,负样本锚点 |
| 2 | drowsy | 昏昏欲睡 | 核心疲劳状态 |
| 3 | distracted | 分心 | 目光偏离前方、操作中控 |
| 4 | smoking | 吸烟 | 分心子类,单独检出 |
| 5 | yawning | 打哈欠 | 疲劳先兆 |
| 6 | phone | 电话 | 分心子类,单独检出 |
注意「唤醒」这一类,很多人会把它当成废话类删掉,这是第一个要避开的坑。DMS 是一个开机就持续运行的检测任务,摄像头里大部分时间是正常驾驶状态。如果训练集里没有「唤醒」这个类,模型就缺少背景锚点,会把正常驾驶时的低头看仪表、倾斜身体、自然转头全部误判成疲劳或分心。这个类的存在不是冗余,而是给其他 6 个正类划了一条决策边界。到第 5 章的避坑部分,我会专门讲删掉它之后会发生什么。
还有一个语义重叠问题需要提前处理:分心、吸烟、电话三个标签在场景上有交叉。一个人打电话时视线往往也偏离前方,但数据集里把它标成了 phone 而不是 distracted。我的约定是:只要画面里有手机,就归 phone;有烟支,就归 smoking;二者都没有但视线离开前方,归 distracted。转换脚本里按这个优先级判断,避免同一帧在多个类里重复造框。
2.2 转换脚本:从源标注到每图一个同名 txt
YOLO 系列训练的标签格式不是 json 也不是 csv,而是每张图像对应一个同名 txt,每行写一条记录:类别编号 中心点x 中心点y 框宽 框高,坐标全部用图像宽高归一化到 0~1 之间。绝大多数压缩包里的原始标注都不是这种格式,所以第一步永远是写转换脚本。下面是我处理行为类数据集的常用模板,其中load_source_annotations按你手里实际标注格式(json、csv、xml 都行)替换读取逻辑:
import os from PIL import Image # 源标注演示结构:anno[文件名] = [ (类别名, x_min, y_min, x_max, y_max), ... ] # 实际项目中常见的是 json / csv / xml,读取部分按你的源标注格式替换 anno = load_source_annotations("/path/to/raw_annotations.json") # 7 个类别按固定顺序编号,顺序一旦定下来就别改,训练和推理共用这一份 CLS_MAP = { "head_down": 0, # 头部下垂 "awake": 1, # 唤醒 / 正常驾驶,保留作负样本锚点 "drowsy": 2, # 昏昏欲睡 "distracted": 3, # 分心,目光离开前方或操作中控 "smoking": 4, # 吸烟 "yawning": 5, # 打哈欠 "phone": 6 # 使用电话 } def convert_to_yolo(image_dir, output_dir): for img_name, boxes in anno.items(): img_path = os.path.join(image_dir, img_name) w, h = Image.open(img_path).size txt_path = os.path.join(output_dir, os.path.splitext(img_name)[0] + ".txt") lines = [] for cls_name, x1, y1, x2, y2 in boxes: if cls_name not in CLS_MAP: continue # 未映射的标签直接跳过,避免污染 # 防止出现负坐标或越界坐标,先夹紧 x1 = max(0, min(x1, w - 1)) y1 = max(0, min(y1, h - 1)) x2 = max(0, min(x2, w)) y2 = max(0, min(y2, h)) if x2 <= x1 or y2 <= y1: continue # 宽高非正的框是无效框,丢弃 cx = ((x1 + x2) / 2) / w cy = ((y1 + y2) / 2) / h bw = (x2 - x1) / w bh = (y2 - y1) / h lines.append(f"{CLS_MAP[cls_name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") with open(txt_path, "w") as f: f.write("\n".join(lines)) print(f"转换完成,共生成 {len(anno)} 个 txt")这段脚本里有三个细节决定了后面训练能不能顺利跑起来。
第一,坐标归一化用的是中心点/宽高,不是左上右下,YOLO 的标签格式要求的就是中心点加宽高,四个值必须在 0~1 之间。第二,夹紧和丢弃逻辑不是多余的——行为数据集中经常出现标注框略微越界、或者框退化成一个点的情况,这些框如果不清理,训练时 loss 会先爆掉。第三,类别编号顺序一旦定下来,训练和推理必须共用同一个CLS_MAP,不要中途调整顺序,否则你后面做模型部署时预测结果和类别对不上,这种问题排查起来最费时间。
2.3 目录结构与第一轮校验:先跑一遍文件完整性
转换完标签后,目录结构按 YOLO 惯例来组织,一个data根目录下分images和labels,各自再分train、val。这种结构是之后 data.yaml 能直接引用的标准布局,也是主流开源训练平台默认识别的格式。
data/ ├── images/ │ ├── train/ # ~8000 张 │ └── val/ # ~900 张 ├── labels/ │ ├── train/ # 与 images/train 同名同前缀的 txt │ └── val/ └── data.yaml # 训练配置划分比例上,8979 张图的规模,我一般按 90/10 划分,val 里保证每个类别至少有几十个框,否则验证集的 mAP 波动会大到没法看。划分之后必须做一轮完整性校验:遍历 labels 目录下每个 txt,检查里面每一行的类别编号是否都在 0~6 之间、四个坐标值是否都在 0~1 之间、每张图是否都能找到对应 txt。这一步可以用一个十几行的 Python 脚本完成,但很多初次接触行为数据集的人会跳过它,结果训练到一半才报错,浪费几个小时。校验脚本我一般会在转换脚本里顺手加上,跑完直接打印异常样本列表。到这一步数据格式就齐了,接着进入训练前的数据体检。
3. 训练前给 8979 张图体检:类别不均衡、空图和连续帧泄漏
3.1 统计各类别框数:吸烟、电话这类少数类要心里有数
行为检测数据集和通用物体检测数据集最大的不同,是类别分布极其不均衡。通用数据集里的猫、狗、车数量比例通常在一个数量级内,但驾驶行为数据里,「唤醒」这种正常状态的框可能占一半以上,而「吸烟」「电话」这类短时行为的框可能只有前者的十分之一。直接拿原始分布去训练,模型会把多数类学得很好,少数类几乎全漏。所以在划分数据集之前,必须先跑一遍统计脚本,做到心里有数。
import glob from collections import Counter LABEL_DIR = "labels/train" # 先只看训练集,val 单独看 cls_counter = Counter() box_per_img = [] for txt in glob.glob(f"{LABEL_DIR}/*.txt"): rows = open(txt).read().strip().splitlines() if not rows or rows == [""]: box_per_img.append(0) continue box_per_img.append(len(rows)) for row in rows: cls_counter[int(row.split()[0])] += 1 print("各类别框数:", dict(cls_counter)) print("空标注图数量:", box_per_img.count(0)) print("总图数:", len(box_per_img)) print("单图平均框数:", round(sum(box_per_img) / len(box_per_img), 2))这段统计能一次性暴露三个问题:哪些类是少数类、有多少图是空的、每张图平均有几个框。空标注图不是脏数据,它们对应的就是「唤醒」状态下的正常驾驶画面,是负样本,绝对不能删除。但要注意的是,它们会让模型更偏向预测「无目标」,所以在训练配置里要么提高正类权重,要么在采样时控制空图占比。单图平均框数也能侧面反映该数据集的难度——平均框数超过 2,说明一张图里经常同时出现头和手等多个目标,这个信息后面排查混淆矩阵时会用到。
3.2 按视频片段分组划分 train/val,别用随机划分
行为数据集大多是从连续视频里抽帧标注的,同一段视频相邻帧的背景、角度、光照几乎完全一样。如果直接用random_split按图随机划分,同一个视频片段的帧会同时出现在训练集和验证集里。这种情况下验证集的 mAP 会虚高,因为模型相当于做过原题——它记住了背景而不是学到了行为特征。等你部署到车上面对新的摄像头视角,精度会断崖式下跌。
正确处理方式是按视频片段或受试者编号分组。文件名通常带片段前缀,比如cam01_000123.jpg、subject02_000456.jpg,取前缀作为分组键:
import os, random from collections import defaultdict group2imgs = defaultdict(list) for img in os.listdir("images"): group = img.split("_")[0] # 按视频片段/受试者分组 group2imgs[group].append(img) random.seed(2024) train_imgs, val_imgs = [], [] for group, imgs in group2imgs.items(): random.shuffle(imgs) split = int(len(imgs) * 0.9) # 组内 90/10 train_imgs.extend(imgs[:split]) val_imgs.extend(imgs[split:])组内划分保证了同一个片段最多只有 10% 的帧进验证集,且其他 90% 的训练帧虽然背景相似但不会和验证帧逐帧重复。对于 8979 张图的规模,这样划分后 val 里每个类别依然能保留足够的框数。切分完成后把train_imgs、val_imgs分别硬链接到images/train、images/val,再同步把对应 txt 复制到 labels 目录。这一步是防止数据泄漏最关键的一环,也是很多训练平台默认随机划分做不到的。
3.3 一张图多个行为框的处理:不拆类,但要看共生关系
行为检测还有一个通用检测里不常见的问题:同一个人的同一时刻可能出现多个行为标签。典型场景是驾驶员打哈欠的同时头部下垂、眼皮半闭,这时候同一张图里会出现 yawning 框和 head_down 框,甚至 drowsy 框。目标检测本身就是多框多类的,这些框不用拆,模型会同时学习「打哈欠」和「头部下垂」两个独立的视觉特征。但你要注意它们之间的共生关系——如果 yawning 和 drowsy 在训练集里高度共现,模型可能学到的是「有哈欠就有嗜睡」的关联,而非真正区分两个类别的边界。后面观察混淆矩阵时,这两类之间的错误匹配也会明显偏高。这不是数据问题,而是行为本身的强相关性,标注工具无法给一个行为框同时打两个标签,只能按主标签打。我的做法是在训练时接受这种共生,但在评估指标上不看单一类的 AP,而是看疲劳类(head_down、drowsy、yawning)的合并召回率,只要三个类里有任一检出就算疲劳命中。这样更贴近真实 DMS 的使用逻辑。
4. 用 YOLOv8 跑训练:预训练权重、损失函数与 120 轮迭代策略
4.1 data.yaml 与预训练模型:9000 张图规模别从零训练
类别和目录都整理好后,训练配置就相对省事了。先写 data.yaml,让训练工具能定位到图像和标签:
# data.yaml path: /data/drowsiness_detection train: images/train val: images/val nc: 7 names: ["head_down", "awake", "drowsy", "distracted", "smoking", "yawning", "phone"]8979 张图带标签的规模说大不大、说小不小,但从零初始化训练一个检测头基本是浪费算力。行为数据集的共性特点是背景相对单一(都是驾驶舱视角),但目标的姿态变化大,所以前几层特征用预训练模型是绝对划算的。我一般会下载 YOLOv8m 的预训练权重作为起点,而不是用yolov8n——行为框通常偏大、偏清晰,模型的容量主要花在区分不同行为类别上,n 的骨干网络在少量数据下很难把吸烟和正常喝水这类细粒度差异分开。如果你手里显存紧张,把 imgsz 降到 480 比换小模型更有效。预训练模型这块选择上,v8 的官方权重在 COCO 上学到的通用目标表征,对头、手、手机、烟支这些目标都有不错的迁移效果。
4.2 训练命令与 6 个必调超参数
训练命令本身不长,关键是每个参数的行为数据含义。下面是针对 8979 张图、7 类行为数据的一套完整命令:
yolo detect train \ model=yolov8m.pt \ data=data.yaml \ imgsz=640 \ batch=16 \ epochs=120 \ patience=20 \ workers=8 \ device=0 \ project=runs/dms \ name=drowsy_v8m几个参数按我的习惯逐个说。
imgsz=640是性价比最高的分辨率。DMS 摄像头画面里驾驶员头部区域通常占整幅图像的 15% 以上,目标大、细节不算苛刻,640 足够捕捉头部姿态和嘴部状态。如果你发现打哈欠和小幅头部下垂漏检多,再把 imgsz 提到 960 或 1280,但训练时间会接近翻倍,建议先用 640 跑通流程。
batch=16在 12GB 显存上配合 yolov8m 刚好合适,24GB 显存可以上 32。batch 太小(小于 8)会让 BN 层的统计量不稳定,行为数据里的空图和稠密图差异大,这个问题更容易被放大。
epochs=120配patience=20是行为检测的稳妥组合。数据集只有 9000 张图,不算大,120 轮足够模型收敛,patience 20 意味着 20 轮内验证集没有提升就提前停,避免后期过拟合到训练集的背景细节上。
另外两个值得关注的参数是optimizer和mosaic。迁移学习场景下我用 AdamW 起步,前 50 轮保持冻结 backbone,后 70 轮解冻全部层微调。mosaic 增强在通用检测里效果很好,但行为检测里它会拼出半个人脸或断裂的手部,我的经验是把 mosaic 概率从默认的 1.0 降到 0.5,让模型不至于过度依赖拼出来的假样本。
4.3 类别不平衡怎么传给训练:cls_pw 与损失函数选择
YOLOv8 默认的损失函数是三个分支的组合:分类用 BCEWithLogitsLoss,回归用 CIoU,外加 DFL 分布损失。这套组合在大多数检测任务里都不用动,行为数据集的麻烦不在损失函数形式,而在类别权重。
上一章统计里大概率会发现吸烟、电话、头部下垂这几个类别的框数明显少于唤醒和昏昏欲睡。如果直接训,模型会对少数类的损失贡献不够敏感。YOLOv8 训练参数里可以通过cls_pw传入每个类的正样本权重,我一般按频率倒数平方根计算:
import numpy as np freq = np.array([ 420, # head_down 框数,示例值,以你的统计为准 3100, # awake 框数,示例值 2100, # drowsy 框数,示例值 560, # distracted 框数,示例值 180, # smoking 框数,示例值 780, # yawning 框数,示例值 220 # phone 框数,示例值 ], dtype=float) weights = 1.0 / np.sqrt(freq) weights = weights / weights.mean() # 归一到均值 1 print(weights.round(3))输出结果像[0.62, 0.22, 0.27, 0.53, 0.96, 0.46, 0.87]这样的数组,直接拼在训练命令的cls_pw参数后面。用平方根而不是直接用倒数,是为了避免把权重拉得过大——像 smoking 这种样本特别少的类,权重一旦超过 3,模型会开始把喝水、吃零食的普通抬手动作全部误判成吸烟,因为漏报的惩罚太重了。权重的作用是保底,让少数类在损失里有一定的存在感,而不是压过所有其他类别。
训练过程中我建议每 20 轮存一次 checkpoint,而不是只存最后的权重。后面要做难例挖掘(第 6 章内容),需要拿不同训练阶段的模型对同一批数据进行预测,对比哪些误检是模型中期有、后期消失的——这能帮你判断是数据量不够还是类别边界真的模糊。
5. 训练嗜睡数据集最容易翻车的 5 个坑:从 BN 崩溃到混淆矩阵失真
5.1 BN 崩溃、loss 变成 nan,val 精度全程为 0
现象:训练刚开始几轮 loss 还能正常下降,到第 5 轮左右突然出现 inf,之后 mAP 一直是 0,日志里 batch 统计的输入图像也都正常。
原因:绝大多数情况出在标签上。嗜睡行为数据集的标注框里存在两种脏数据:一是坐标越界——标注框的右下角超出了图像宽高,或者在归一化后出现了负值;二是「退化框」——比如某一帧驾驶员低头幅度极大,标注框压成了一个薄片,宽或高接近于 0,导致回归分支的 loss 爆炸。这两种脏框在训练初期会被模型当成真实目标去拟合,梯度瞬间失控。
解决:在转换脚本阶段就做夹紧和过滤,这是我第 2 章脚本里x2 <= x1 or y2 <= y1那段逻辑存在的意义。如果你已经训练到一半才发现,别想着靠调低学习率补救,直接回退到转换脚本,把异常框清理后再重新训练。这个坑是所有行为数据集的第一个拦路虎,训练前跑一遍完整性校验的成本远低于白跑几十轮。
5.2 把「唤醒」类删了,换来一整屏疲劳误报
现象:只保留头部下垂、昏昏欲睡、分心等 6 个行为类训练,验证集 mAP 看着不错,但拿到一段正常驾驶的视频里跑,每几秒就报一次疲劳,驾驶员不过是低头看了一眼仪表盘或者调了一下后视镜。
原因:漏了「唤醒」这个负样本锚点类。没有唤醒类的数据,模型内部就没有「以上都不算」的空间,它只能在 6 个行为类里硬选一个最接近的——而头部下垂类在姿态上跟低头看仪表盘几乎无法区分。你缺少的是一种用来压制误报的负样本,而不是模型能力问题。
解决:训练集里保留全部 7 个类,尤其是唤醒类不能删。更进一步的建议是给唤醒类单独设置一个过滤逻辑:推理时把唤醒类的置信度阈值提高到 0.7,其余行为类用默认的 0.25——这样既能保证正常状态不被误报,又不会因为唤醒类抢占了行为类的概率空间而影响真正的疲劳检出。
5.3 混淆矩阵总和永远对不上,看起来像数据错乱
现象:训练结束后打开 confusion_matrix 图,矩阵里每个格子都有数字,但把所有数字加起来发现远远大于训练样本总数,甚至大好几倍,于是怀疑数据集或训练工具出了问题。
原因:目标检测的混淆矩阵本来就不是按「样本数」算的。YOLO 是先把每个 GT 框和预测框做 IoU 匹配,然后每个预测框都可能进入矩阵的一个格子,同一个 GT 可能被多个预测框命中。再加上行为数据里一张图平均有 1.5~2 个框,头和手同时出现,预测框的数量天然比 GT 多。矩阵总和虚高是正常现象,不是 bug。
解决:别用混淆矩阵核对数据量。你想知道训练集规模,回到第 3 章的统计脚本里去数框。想评估模型到底漏了多少,去看每类的 recall 和针对性的 PR 曲线。混淆矩阵只用来观察类别之间的混淆模式——尤其是 smoking 和 phone 之间、drowsy 和 yawning 之间的错位,这是行为类别的语义相似性造成的,属于预期内现象,不符合预期才需要排查数据。
5.4 连续帧泄漏:val mAP 0.9 以上,上路测试一塌糊涂
现象:训练结果极好,val mAP 超过 0.9,每个类的 AP 都稳定。但把模型部署到一套全新的摄像头画面上,检测率跌了 30 个点以上,头部下垂经常漏检。
原因:这是最典型的随机划分翻车现场。8979 张图如果按文件名随机打乱划分,同一段视频的连续帧会出现在训练集和验证集里。模型训练时见过这些帧的背景纹理和驾驶员衣着,验证时等于做原题,mAP 虚高。但真实场景里的汽车内饰、座椅角度、光线条件和数据集完全不同,模型立刻露馅。
解决:回到第 3.2 节的按片段分组划分逻辑。划分完成后,我习惯做一个抽查——随机从 val 里取 30 张图,去看它们的文件名前缀,确认每个片段在 val 里只占很小比例。这个检查便宜且有效,比任何训练指标都更能说明数据划分是否干净。
5.5 吸烟类被喂成「乱枪打鸟」:recall 高但 precision 惨不忍睹
现象:训练结束后打印分类报告,smoking 类 recall 有 0.7,但 precision 只有 0.2,预测出的吸烟框一大半是驾驶员在喝水、吃东西、甚至手靠近嘴边的普通动作。
原因:smoking 类样本太少,模型无法学到一个紧致的「烟支」特征表示,只能把凡是「手在嘴边附近」的框都判成吸烟。YOLOv8 的检测头在类别不平衡时会倾向于扩大正例范围来降低损失,于是 precision 被牺牲掉了。
解决:在两个层面下手。数据层面,用难例挖掘补充训练——拿当前模型去预测行车视频,把置信度在 0.3~0.6 之间的边缘框全部导出成小图,人工筛选出真正的吸烟动作回填到训练集。参数层面,把 smoking 类的cls_pw权重从 0.96 调到 1.2 左右,再用一个轻微的下置信度阈值过滤,比如推理时 smoking 类阈值从 0.25 提到 0.35。这一条治标,难例挖掘才是治本的路子,具体操作在第 6 章展开。
6. 把边缘样本捞回来再训一轮:置信度筛图与难例补充
训练完第一轮,别急着下车验证。针对吸烟、电话这类样本少的类别,我有一个固定动作:跑一遍置信度筛查,把模型犹豫过的样本捞回来重新标注。
具体做法是拿训练好的 best.pt 对一组新的行车视频片段做预测,输出每帧的检测置信度,然后按下表分档处理:
| 置信度区间 | 模型状态 | 处理动作 |
|---|---|---|
| 0.6 以上 | 高置信,分类基本可靠 | 不处理,直接作为有效检测 |
| 0.3~0.6 | 边缘预测,模型在两类之间摇摆 | 导出小图,人工复核后补标,回填训练集 |
| 0.25 以下 | 模型认为无目标 | 人工抽查这些帧里是否存在漏检的短时行为 |
回填流程很简单,用预测脚本先把边缘帧输出成带名字的图片列表:
yolo predict model=runs/dms/drowsy_v8m/weights/best.pt \ source=raw_clips/ \ save_txt=True \ conf=0.25 \ iou=0.5把输出的 txt 和图片交给标注工具,重点检查边缘区间里那些被模型标成 smoking 但实际是喝水、被标成 drowsy 但实际是头部微小晃动的帧。人工确认后把修正结果合并回训练集,再微调一轮。我一般会重复这个流程两轮,第一轮解决少数类的 precision 问题,第二轮解决疲劳类的漏检问题。这种做法不是玄学,它的原理是让模型在它最模糊的决策边界附近获得更多真实样本——比无脑加相同数量的随机帧有效得多。
回填完成后,再跑一次同样的预测,你会发现边缘置信度区间的帧数明显减少,这说明类别边界被压实了,而不是模型整体的置信度虚高。到了这一步,这个 8979 张图的数据集才算真正被榨干价值。拿到这类行为数据集,我的第一习惯永远是先统计、再划分、最后才开训——顺序反了,后面的每一轮迭代都在还数据债。希望帮到你。
本文还有配套的精品资源,点击获取