
简介面向驾驶员分心检测与图像分类任务这份已标注数据集的资源说明提及约22,000张样本涵盖安全驾驶、打电话、喝水、与乘客交谈等10类动作具体类别与标签映射见随包json标注文件。压缩包为7z格式共2000个文件主体为1998张jpg图像另含1个json标注文件与1个python可视化脚本整包约649MB。数据已划分训练集与测试集并按类别分目录存放便于直接用于分类模型训练、验证和评估运行show脚本可可视化图像与对应标签还能快速核对各类别样本数量与分布为后续数据增强和模型调优提供参考。该数据集适合图像分类网络改进和计算机视觉项目实战目前已有69人学习可为驾驶行为分析、智能座舱安全预警等应用提供已标注数据支撑适合研究者、学生及项目开发者使用。1. 驾驶员分心检测图像分类数据集22,000 张已标注图像解决的是“标签从哪来”很多团队第一次把驾驶员分心检测模型装上车时最先暴露出来的问题不是网络结构选得不对而是“正常”和“分心”的边界没定义清楚驾驶员伸手调后视镜系统报了分心司机低头看仪表盘系统没反应。要训练一个能用的图像分类模型先决条件是一批数量够大、标注口径一致的驾驶员行为图像。这套约 22,000 张已标注数据的驾驶员分心检测图像分类数据集做的就是这件事输入一张驾驶舱摄像头拍到的画面输出驾驶员当前属于正常驾驶还是某类分心行为。它替你省掉的是从录数据、清洗到定标注规范的两到三周时间适合正在做车队管理、智能座舱 DMS 的算法工程师也适合刚接触图像分类、想拿真实工业数据练手的初学者。2. 数据组织与标注规格拿到 22,000 张图后先做的三件事2.1 为什么分心检测适合用图像分类而不是目标检测先说任务形态。驾驶员分心检测的数据来自固定安装的驾驶舱摄像头机位基本不动驾驶员的脸、手、方向盘始终处在差不多的画面位置。这时候判断“这个人是不是在打电话”“是不是在喝水”本质是一个整体状态的判断不是“在画面哪个位置找到手机”的定位问题。图像分类的标注成本明显更低每张图只需要一个类别标签而目标检测要对画面里的手机、水杯、手分别画框标注一张图的时间会被放大几倍。22,000 张图如果走检测标注工期和一致性都很难控制。我一般会先把这类任务定成图像分类类别设计成“正常驾驶、打电话、发消息、喝水、化妆、与乘客互动”等行为一张图一个主标签。如果后续想回答“手具体在哪里”再把分类结果作为初筛对少数被分到分心类的图做目标检测定位算力成本也更划算。这并不等于检测方案没有用。像“危险区域手部姿态分析”这类要求细粒度位置的任务检测比分类合适。但以 22,000 张图这个规模分类模型更容易快速形成可用基线迭代反馈也更快。2.2 目录结构与标签字段接手数据后先做三件事拿到数据集第一件事不是开训练而是先把目录结构和标签读进来。常见做法是 train/val/test 三个目录每个类别一个子目录子目录名就是类名标签信息以 CSV 或 JSON 形式集中存放。核心字段至少包括 image_id、class_id、class_name如果同时带了 driver_id 或 clip_id这类字段要格外珍惜后面做数据划分时它们比类别标签还关键。先执行一条命令把目录结构列出来看每个类别的样本量DATA_ROOT/path/to/distraction_dataset find $DATA_ROOT -maxdepth 2 -type d | sort输出一般长这样具体类目以数据集标签文件为准train/c0_normal/ train/c1_calling/ train/c2_texting/ train/c3_drinking/ train/c4_reaching_behind/ ...接着用 Python 统计每个类别的样本数同时确认标签文件里有没有缺失或重复的 image_idimport pandas as pd ann pd.read_csv(annotations.csv) print(ann[class_name].value_counts()) print(重复 image_id, ann[image_id].duplicated().sum()) print(空标签行数, ann[class_name].isna().sum())这段代码的作用是快速暴露三类问题类别极端不均衡、文件名重复、漏标。22,000 张图如果均分成 10 类每类约 2,000 张但真实采集数据通常不会这么均匀——尾部类别喝水、化妆、伸手取物可能只有几百张头部类别正常驾驶可能有五六千张。尽早知道这个分布后面才能决定是用类别加权损失还是过采样。2.3 质量审计的四个检查点统计只能发现数字问题图像本身的质量还需要肉眼抽检。我不会每张都看按每个类别抽 10% 到 15%重点关注四个检查点检查点看什么不处理的后果类别分布尾部类别样本是否少于 500 张尾部类 F1 很难上 0.8清晰度与遮挡逆光、暗光、方向盘遮挡下的图是否可辨认模型学会“见光死”相似类别边界打电话和玩手机、喝水和吃东西是否真能区分标注标准漂移loss 难收敛时间相关性同一视频片段的多帧是否分散在不同集合验证集虚高线上翻车抽检方法很土但有效把每个类别的图拼成一张网格图一眼扫过去就能发现同类样本里混着多少“看着就不像这一类”的图。import matplotlib.pyplot as plt from PIL import Image import glob, random paths sorted(glob.glob(f{DATA_ROOT}/train/c3_drinking/*.jpg)) sample_paths random.sample(paths, min(20, len(paths))) fig, axes plt.subplots(4, 5, figsize(15, 12)) for ax, p in zip(axes.ravel(), sample_paths): img Image.open(p) ax.imshow(img) ax.axis(off) plt.savefig(quality_check_drinking.jpg, dpi100)这台脚本把某个类别的样本集中展示几秒钟就能判断这一类内部的干净程度。坏样本不用急着删除非整张图完全标错更推荐的做法是记下来训练时单独观察模型对它们的预测——它们往往就是最终混淆矩阵里那几对难分行为。2.4 标签字段说明书没有它划分和审计都无从谈起标签文件里除了 class_id 和 class_name还有几个字段决定了你能做多严格的评估。我经手过的分心数据集中一份合格标签说明书至少包含这些字段字段典型值为什么需要image_idimg_0042.jpg与图像文件一一对应的唯一索引class_id / class_name3 / drinking训练目标分类任务的 ydriver_idD007按人划分验证对陌生司机的泛化性clip_idT02_shot08按视频片段划分防止时序泄漏splittrain/val/test确认官方划分避免同一张图跨集合重复出现有些数据集的标签只有两列文件名和类别没有 driver_id、clip_id。这种情况下至少要按文件名前缀分组前缀相同、只有序号不同的基本是同一段视频连续帧划分时把它们绑在一起。另外如果标签文件里出现 gaze_direction、hand_position 这类属性字段别随手丢了它们之后做细粒度行为分析时是现成的辅助监督信号。3. 跑通 ResNet50 基线分组划分、训练脚本与关键参数3.1 数据划分的协议按司机分组别按帧随机切图像分类任务里最常见的翻车姿势是按图像随机分 train/val。对驾驶员分心检测来说随机切分几乎必然导致数据泄漏同一个司机、同一段视频里的连续帧外观和背景完全一样一半进了训练集一半进了验证集。模型记住的是这套车的内饰而不是“分心行为长什么样”验证集精度轻松到 98%上了新车就露馅。正确做法是按司机或视频片段分组划分。数据里一般会有 driver_id 或 clip_id 字段以它为分组键做 GroupShuffleSplitfrom sklearn.model_selection import GroupShuffleSplit gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(gss.split(images, labels, groupsdriver_ids)) train_imgs [images[i] for i in train_idx] val_imgs [images[i] for i in val_idx]这段代码的原理是groups 参数把同一司机的所有图片捆绑成一个整体划分时要么全部进训练集要么全部进验证集保证验证集里出现的司机在训练时一个也没见过。random_state 固定下来后续换模型或调参时才方便横向比较。如果数据里没有司机或视频片段字段至少要保证同一视频的连续帧进同一个集合。判断方法很简单看相邻 frame 的文件名如果前缀相同、只有序号不同就按前缀分组。3.2 训练脚本ResNet50 预训练权重跑出第一个可对比的基线第一版基线我建议用 PyTorch torchvision 的 ResNet50理由是它在 ImageNet 上预训练充分单卡即可训练参数组合也最容易找参考。写脚本时三个点要留意替换最后的全连接层、图像增强别上来就水平翻转第 5 章细说、优化器用 SGD 就足够稳定。import torch from torch import nn from torchvision import models, transforms num_classes 10 # 以数据集的 class_id 数量为准 model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V1) model.fc nn.Linear(model.fc.in_features, num_classes) # 替换分类头 train_tf transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomResizedCrop(224, scale(0.7, 1.0)), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.1), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) val_tf transforms.Compose([ transforms.Resize((256, 256)), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ]) optimizer torch.optim.SGD(model.parameters(), lr1e-3, momentum0.9, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max60) criterion nn.CrossEntropyLoss()训练循环是常规写法不再整段贴。需要重点理解的是上面几个参数的含义scale(0.7, 1.0) 的 RandomResizedCrop强制模型关注驾驶员行为的主体区域而不是整张图的静态背景。比例下界太低会裁掉手部关键信息0.7 是我跑过比较稳的下界。ColorJitter 幅度要小驾驶舱画面本身光照变化较大但亮度扰动过强会让肤色失真反而削弱对行为的判断。SGD 的 lr1e-3 是预训练权重微调的安全起点。如果从头训练建议降到 3e-4 以下。weight_decay1e-4 对类别数量不大的分类任务足够过大的 weight decay 会把尾部类别的决策边界压缩得过紧。CosineAnnealingLR 配 T_max60前 30 个 epoch 保持相对高的学习率快速拟合后 30 个 epoch 平滑收敛比固定学习率更容易在 60 epoch 内压低验证 loss。3.3 三个直接影响收敛的参数经验第一batch size 建议 64。驾驶舱图像背景差异小batch 太大容易过早收敛到“背景识别”batch 太小8 或 16在 22,000 张规模下每个 epoch 权重更新次数太多训练时间拉长收益却不明显。显存不够就把分辨率先降到 160而不是强行把 batch 降到 8。第二分类头初始化不要随机。新加的 fc 层是随机初始化的前几个 epoch 会输出接近均匀分布的概率拖慢收敛。常见做法是把它初始化成数据集的类别先验把 bias 设为 log(class_count / total_count)。with torch.no_grad(): counts torch.tensor(class_counts, dtypetorch.float) model.fc.bias.copy_(torch.log(counts / counts.sum())) # 权重保持默认随机初始化bias 带先验即可这样网络一开始就知道从“常见类别”出发尾部类别虽然数量少也不会完全被淹没在 softmax 归一化里。第三epoch 数别贪多。60 epoch 已经够判断一个方案行不行如果到 40 epoch 时验证集的宏平均 F1 还在 0.85 以下问题大概率不在训练时长而在数据划分或标注口径上。先查泄漏再调模型顺序不能反。3.4 训练中要记录的三样东西除了 loss 曲线我习惯每 10 个 epoch 存一次验证集概率输出和混淆矩阵。只看 loss 走到后期会产生一种“玄学”错觉——下降变慢了但不知道瓶颈在哪。把每个类别的 recall 单独打出来能看到正常驾驶已经 0.97而喝水只有 0.55后面所有调试目标就变成“把喝水提高 10 个点”而不是笼统地“再训几轮看看”。4. 评估指标怎么定宏平均 F1、TPR 与不容错过的混淆矩阵4.1 主指标用宏平均 F1辅助看加权 F1很多分类项目只报 accuracy在分心检测里这个数字会骗人。22,000 张图中“正常驾驶”通常占一半以上模型把所有图都判成正常accuracy 也能有 60% 以上但分心行为一条都没抓到。正确做法是用 F1而且要用宏平均 F1 做主指标。宏平均把每个类别的 F1 先算出来再取平均头部类和尾部类权重一致加权平均按样本数加权会被“正常驾驶”这种大类别主导。分心检测业务上关心的恰恰是尾部类喝水、化妆、伸手取物出现频率低但一旦漏掉风险比漏掉一次打电话更高。我一般同时打印两个值主看宏平均 F1加权平均 F1 作为参考如果两者差距超过 0.1说明类别不均衡已经到了必须干预的程度。from sklearn.metrics import f1_score y_true ... # 验证集真实标签 y_pred ... # 模型预测标签 print(macro F1: %.4f % f1_score(y_true, y_pred, averagemacro)) print(weighted F1: %.4f % f1_score(y_true, y_pred, averageweighted))4.2 关心 TPR低误报率分心检测的落地指标单看 F1 还是会漏掉一个重要问题误报率。分心检测系统安装在车里每一次误报都在消耗司机对系统的信任。误报太频繁司机要么关掉系统要么直接无视报警整条产品线就废了。所以业务上真正要看的指标是在误报率不超过 1% 或 5% 的前提下分心行为能召回多少。这个指标叫 TPR低FPR用 ROC 曲线计算。按优先级排序最理想的状态是正常驾驶类别的误报率低于 1%同时主要分心类别打电话、玩手机的召回率达到 90% 以上。from sklearn.metrics import roc_curve fpr, tpr, thresholds roc_curve(y_true_onehot.ravel(), y_score.ravel()) for target_fpr in [0.01, 0.05]: idx (fpr target_fpr).sum() - 1 print(fTPR FPR{target_fpr:.2f}: {tpr[idx]:.4f})用 One-vs-Rest 方式把每个类别都算一遍重点关注两个数正常驾驶类别的 FPR 是否压得住最难的分心类别尾部类的 TPR 是否起得来。如果正常驾驶的误报率压到 1% 后分心类别的 TPR 只有 60%与其调网络不如先查 3.1 节的划分是否泄漏。4.3 混淆矩阵里两类不能忍的错误混淆矩阵是排查阶段最直接的诊断工具。分心检测里有两类错误需要优先处理分心被识别成正常以及正常被识别成分心。前者是漏报事故风险最高后者是误报直接伤害用户体验。此外还有一类容易被忽略的“串类”打电话被识别成玩手机。这类错误在业务上可以接受一个宽容度都是分心但如果你发现自己标注“发消息”和“打电话”的行为特征太接近需要回过头检查标注标准是否统一。from sklearn.metrics import confusion_matrix cm confusion_matrix(y_true, y_pred) for i, cls in enumerate(class_names): recall cm[i, i] / cm[i].sum() leak_to_normal cm[i, normal_id] / cm[i].sum() print(f{cls}: recall{recall:.3f}, 漏判为正常{leak_to_normal:.3f})这段代码会输出每个类别的召回率以及该类被漏成“正常”的比例。看到某个分心类别 recall 低于 0.7下一步就打开该类别的预测错例看模型是栽在遮挡、光照还是连续帧背景上。这个检查比调学习率更值得先做。4.4 决策阈值别默认用 0.5图像分类的默认输出是概率常规做法是取 argmax。分心检测这个场景不太适合正常驾驶类占比高模型对它的输出概率天然偏高一旦新环境光照变化正常类的置信度波动会直接把分心类挤出 argmax。常见做法是给每个类别单独调阈值正常驾驶类要“高门槛”只有非常确信才判分心高风险分心类要“低门槛”宁可误报不可漏报。调阈值不需要重新训练拿验证集的概率输出扫描一遍即可import numpy as np best_thresholds {} for cls in class_names: best_f1, best_thr 0, 0.5 for thr in np.arange(0.3, 0.8, 0.01): pred (proba[:, cls] thr).astype(int) macro_f1 f1_score(y_true_onehot[:, cls], pred, averagebinary) if macro_f1 best_f1: best_f1, best_thr macro_f1, thr best_thresholds[cls] best_thr不同类别用不同阈值会让整体准确率下降 1 到 2 个点但误报和漏报的分布会明显更符合业务预期。这个取舍值得做。5. 训练与评估中的 5 个常见问题泄漏、翻转和类别失衡逐个排查下面这五条是按出现频率排序的血泪经验每条都按“现象、原因、解决”的顺序写排查时可以直接对着找。5.1 现象验证集 accuracy 98%现场一用就误报——司机与背景泄漏这是我在这个任务上第一次翻车时遇到的状况。训练脚本跑完验证集 accuracy 0.98团队成员都很兴奋上车后却频繁误报坐进没见过的车模型完全失灵。原因是数据划分按图随机切同一个司机的图像同时出现在训练集和验证集模型记住了特定座椅、方向盘和内饰而不是分心行为。这类问题不会出现在 loss 曲线上只能通过按司机分组重划分来验证见 3.1。5.2 现象loss 在后半程震荡尾部类别 recall 忽高忽低——类别不平衡22,000 张图看起来不少但拆到尾部类别后可能只剩 300 到 500 张。直接用 CrossEntropyLoss 训练尾部类别在 60 epoch 的后半段会出现训练 loss 震荡、验证集 recall 在 0.3 到 0.8 之间跳的表现。原因是每个 batch 里尾部类样本太少梯度信号不稳定。解决先给损失函数加权或者把类别少的样本过采样复制进训练集权重系数按“总样本数 / (类别数 * 类别样本数)”计算。如果加了权重后尾部类 recall 上来了而头部类掉得不多就把约束放在 2 倍以内别让模型直接丢掉常见类。5.3 现象水平翻转增强后验证集掉点——语义翻转被忽略做图像分类默认会加 RandomHorizontalFlip但驾驶员行为分类里这个增强是危险的左右翻转后右手打电话变成左手打电话左手扶方向盘变成右手扶方向盘。模型必须额外学会“左右手不重要”否则它会把左右手信息当成判别特征验证集掉 1 到 2 个点而且很难解释。解决如果数据集原始采集条件已经包含左右两种场景就不做水平翻转非要做只对“正常驾驶”这类对称行为启用或把翻转概率降到 0.1 以下。5.4 现象把同一段视频的连续帧同时放进 train 和 val——时序数据泄漏司机分组解决了不同司机之间的泄漏但同一段视频内部的连续帧仍然可能被分开。连续帧之间只有轻微动作差异几乎同一个外观如果训练集和验证集各有一半验证分数的虚高会再次出现。本质是时序相关。解决按 clip_id 而非 driver_id 做分组划分数据没有 clip_id 时按文件名前缀分组保证同一视频片段完整进入一个集合。5.5 现象灰色样本导致标注标准漂移——边界不清的行为怎么处理观看后视镜、转头观察侧方和东张西望在图像上非常接近两个标注员给出的标签可能不同。标注标准一旦漂移模型学到的边界就会混乱同一类的特征方差变大。解决这类灰色样本不删单独归入“ambiguous”目录或在标签里标注为低置信度训练完成后再看这群样本被模型分成哪一类如果稳定地偏向某类再用确定性标注规则对这类样本做二次标注。这样做能避免用自己的猜测覆盖数据集原始标注口径。6. 小样本与增量评估1-shot/5-shot 与 ViT 分类头的调整技巧6.1 1-shot / 5-shot 评估怎么做数据集的价值不只在于训练一个完整模型也在于它适合当“探针数据集”用。后续遇到新的分心类别比如“戴耳机”“看副驾屏幕”时不必立刻收集上千张新图先在已有的 22,000 张数据上训练一个特征提取模型再对每个新类别只抽 1 到 5 张样本做小样本评估。这一步能快速判断新类别与现有类别是否可分。按标准的小样本协议每个类别从训练数据里抽 1 个1-shot或 5 个5-shot样本其余样本当测试集。注意划分时仍然要按司机分组否则小样本评估也会被泄漏污染。最省事的实现是用现成的特征提取器from sklearn.linear_model import LogisticRegression train_feats encoder(train_imgs) # 预训练模型提取的特征 clf LogisticRegression(max_iter1000) clf.fit(train_feats, train_labels) # 每类只喂 1 或 5 个样本逻辑回归在冻结特征上做线性分类基本能反映特征的可分性。1-shot 的 acc 在 0.8 以上说明新旧类别特征差距明显低于 0.6则要考虑采集更多新类别数据。6.2 用 ViT 评估时分类头要不要调整很多团队拿着现成的 ViT 模型直接评估新数据集最常见的错误是只改了分类头的输出数却忘了调整预训练权重的适配策略。第一个结论是“分类头必须改”ViT 预训练模型是在 ImageNet 上训出来的分类头是 1000 类全连接新类别数不一样必须换成新的 Linear 层。第二个结论是“头该怎么调要看数据量”数据量每类少于 100 张时冻结 backbone只训练分类头线性探测学习率 1e-3迭代 30 个 epoch 足够每类 100 到 500 张时解冻最后两三个 transformer block 和分类头学习率降到 1e-4backbone 层学习率再乘 0.1数据量充足时再全量微调。新分类头随机初始化backbone 用预训练权重这是一条可靠的经验线能省掉反复试错的时间。我一般会先跑线性探测如果它已经到 0.85 以上宏平均 F1就不急着解冻只有在线性探测明显不够时才解冻层数并且每解冻一层就在验证集上复核一次。这个“先头后身”的顺序帮我少走了几次弯路也让我对数据本身的表达能力更有底模型表现不好时先怀疑数据划分和标注口径再怀疑网络容量。希望帮到你。本文还有配套的精品资源点击获取