ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

驾驶员分心检测图像分类数据集实战:PyTorch训练与避坑指南

驾驶员分心检测图像分类数据集实战:PyTorch训练与避坑指南 简介面向驾驶员状态监测、辅助驾驶与图像分类任务的研究者及开发者该数据集提供约2.2万张已标注图片覆盖安全驾驶、打电话、喝水、与乘客交谈等10类常见分心行为标签细节可在配套json文件中查看。数据已按训练集、测试集划分同类图片集中存放便于直接开展模型训练与评估资源内附show.py可视化脚本可快速抽查标注质量。压缩包共2000个文件主体为1998张jpg图像另含1个py脚本和1个json标签文件整体约649.16MB适合用于图像分类网络改进实验或作为毕业设计、课程设计的数据基础。当前已有69人学习下载配套的图像分类网络改进与计算机视觉完整项目专栏可进一步帮助读者完成从数据预处理到模型落地的一整套流程。1. 驾驶员分心检测22,000 张已标注图像分类数据集的真实用法做驾驶员分心检测的都知道这就是一道图像分类题输入一张驾驶舱内的图像输出当前驾驶员是在安全驾驶还是打电话、喝水、与乘客交谈。但真把模型放到车载设备上时低光、抖动、遮挡一叠加很多在测试集上刷到 95% 以上的模型都会原形毕露。这份 22,000 张、10 个类别、自带训练集/测试集划分和 JSON 标签文件的数据集恰好把这道题从“玄学调参”拉回到“可复现实验”每类行为单独建目录标签已在文件名之外固定资源里还附带可视化脚本。适合手里有图像分类基础、想快速验证算法改进效果并处理真实误报的工程向读者新手照着能跑通全流程熟手可直接拿它做迁移学习基线。2. 数据集内部结构目录组织、JSON 类别映射与训练/测试划分逻辑这份数据集让我觉得省事的地方在于它没有走“一个总文件夹 一份 CSV 标注”的路线而是把标签直接写进了目录结构。每一类行为一个子文件夹文件夹名就是类别名训练集与测试集各自独立存放。这种组织方式对工程落地非常友好用 PyTorch 的 ImageFolder 可以直接加载省掉不少易错的自定义解析代码。2.1 解压后的目录结构类别子目录与两份数据划分拿到资源后我习惯先花两分钟把目录结构审一遍确认是下面这种布局再继续避免训练到一半才发现路径不对。. ├── labels.json # 类别索引映射文件 ├── show.py # 可视化脚本 ├── train/ │ ├── safe_driving/ # 安全驾驶样本 │ ├── calling/ # 打电话样本 │ ├── drinking/ # 喝水样本 │ └── ... # 其余 7 个类别子目录 └── test/ ├── safe_driving/ ├── calling/ ├── drinking/ └── ...上面的类别目录名是按摘要描述推断的示例解压后请以 JSON 文件里的实际类别名为准。每个子目录里放的是该类的全部 jpg 图片文件名形如 img_23155.jpg、img_3489.jpg这些编号是原始采集时的帧序号跨度很大说明图片是从不同时间、不同驾驶员状态的视频里抽帧得到的。文件名本身不携带标签信息标签完全取决于它所在目录这一点后面加载数据时一定要记住。2.2 JSON 类别映射两种常见结构以及转换方法JSON 文件是整份资源的“字典”类别名和数字索引的对应关系都从这里读取。我拆过很多带 JSON 的数据集这类标签文件最常见的是两种写法差别不大但处理方式不同。第一种是把类别索引映射写成字典key 是字符串形式的数字value 是类别名形如 {0: safe_driving, 1: calling}。第二种是直接放一个类别名列表。如果是第二种训练前需要自己补一步转换import json with open(labels.json, r, encodingutf-8) as f: data json.load(f) # 列表写法转成 {index: class_name} if isinstance(data, list): class_to_idx {name: i for i, name in enumerate(data)} idx_to_class {i: name for i, name in enumerate(data)} else: # 字典写法注意把字符串 key 转成 int class_to_idx {name: int(idx) for idx, name in data.items()} idx_to_class {int(idx): name for idx, name in data.items()} print(idx_to_class)这里的关键点是类型的统一。JSON 里数字索引的 key 通常是字符串而模型输出和 PyTorch 的 label 是 int不在同一类型上做匹配后面打印混淆矩阵时会出现“索引对不上”的怪问题。这段代码把两种常见结构都兼容了值得直接抄进项目里。2.3 训练/测试分开存放的工程意义很多数据集给的是一个总文件夹加一份标注 CSV加载前得自己按比例拆分拆分时还要小心随机种子不一致导致训练集和测试集互相混入。这份资源直接省掉了这一步train/ 与 test/ 下类别子目录一一对应加载时不关心文件级标注对齐问题。但我还是会多做一个校验确保两边类别目录完全一致。目录顺序一旦不一致ImageFolder 会在两边各按字母序生成类别索引顺序错位后测试集的一切指标都会失真import os train_classes sorted(os.listdir(train)) test_classes sorted(os.listdir(test)) assert train_classes test_classes, 训练集与测试集类别目录不一致 print(类别目录一致共, len(train_classes), 类)另外拿到数据后建议先统计每类图片数量确认与 22,000 张的总体规模对得上同时看类别分布是否均衡。这一步直接决定后面要不要做类别加权或重采样是紧接着就要面对的问题。3. 跑通一次分类实验PyTorch 加载、训练脚本与超参设定数据集本身不能直接产生价值跑通一条 baseline 管线才算把它用起来。这里我按自己的工程习惯用 PyTorch 写一个最小可复现的分类流程ImageFolder 加载、预训练 ResNet 微调、注意力改进位点预留。配套博客里讲的图像分类网络改进核心思路也是在这一基座上做替换我先把最稳的版本给出来。3.1 用 ImageFolder 加载数据读图、标签索引和 DataLoader 一步到位因为目录结构天然兼容 ImageFolder加载极其简单但有一个细节要注意训练和测试的 transform 不能完全一样。测试集不应该做随机裁剪和翻转否则结果不稳定。from torch.utils.data import DataLoader from torchvision import datasets, transforms transform_train transforms.Compose([ transforms.Resize((256, 256)), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) transform_test transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) train_ds datasets.ImageFolder(train, transformtransform_train) test_ds datasets.ImageFolder(test, transformtransform_test) train_loader DataLoader(train_ds, batch_size64, shuffleTrue, num_workers4) test_loader DataLoader(test_ds, batch_size64, shuffleFalse, num_workers4)RandomCrop 缩放到 256 再随机裁 224是在保留更多上下文和增强多样性之间的折中。ColorJitter 对驾驶舱场景尤其重要因为真实光照从白天到隧道变化很大。Normalize 用的是 ImageNet 的均值和方差预训练模型前处理必须这样保持一致。3.2 最小训练脚本冻结骨干训分类头再全量微调ResNet50 的预训练权重在这类驾驶行为图像上依然好用我的做法是分两阶段先冻结骨干网络只更新分类头把随机初始化的 10 类分类器训起来等 loss 降到平台期再解冻骨干做全量微调。这样做比一开始就全量训练更容易收敛也符合博客里改进实验的常规节奏。import torch import torch.nn as nn from torchvision import models device torch.device(cuda if torch.cuda.is_available() else cpu) model models.resnet50(weightsmodels.ResNet50_Weights.IMAGENET1K_V2) in_features model.fc.in_features # 把最后的 1000 类分类头换成 10 类 model.fc nn.Sequential( nn.Dropout(0.2), nn.Linear(in_features, 10), ) model model.to(device) # 第一阶段冻结骨干只跑分类头 for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True optimizer torch.optim.AdamW(model.fc.parameters(), lr1e-3, weight_decay1e-4) loss_fn nn.CrossEntropyLoss()等分类头收敛几个 epoch 后再解冻骨干把学习率降到 3e-5全量参数一起微调。这个两阶段策略在标注量不算特别大的数据集上很稳能明显减少过拟合。配套博客里讲的改进主流方向是在残差块后插入 SE 或 CBAM 这样的注意力模块把通道权重重新标定后再进下一层。这个是后话先把 baseline 跑出来再动主干。3.3 超参设定学习率、batch size 与 epoch 的实用取值我直接给出在这类驾驶舱数据集上验证过比较稳的起点读者可根据显存大小调整。参数我的设定值说明输入尺寸224x224ResNet 系列的标准尺寸batch size64显存不够时降到 32初始学习率1e-3只训练分类头时可用全量微调学习率3e-5解冻骨干后必须调小优化器AdamWweight_decay 设 1e-4训练轮数15~30以验证集 loss 不再下降为准学习率调度CosineAnnealingLRT_max 设为剩余训练轮数batch size 从 64 降到 32 时我习惯把学习率同步减半这不是严格的线性缩放公式但能避免小 batch 下梯度噪声过大导致训练不稳。epoch 数量不要死板重点看验证集准确率是否还在涨连续 3 个 epoch 不涨就可以停了。4. 可视化与数据体检show 脚本的正确打开方式别直接跳过资源里自带 show 脚本很多人觉得“可视化而已跳过不影响训练”。我的血泪经验是数据可视化的价值不在看效果在于发现标注问题。一张标注错得离谱的图片能让训练过程白白消耗几十个 epoch。所以拿到资源的第一件事应该是把 show 脚本跑起来把数据“看一遍”。4.1 show 脚本的常见实现把每类抽样拼成网格资源里的 show 脚本通常就是一个用 matplotlib 把图片拼成网格的 Python 文件运行时只需确保路径变量指向数据集目录python show.py如果脚本里写死了路径或者你想自己控制抽样张数直接改用下面这段通用版本import matplotlib.pyplot as plt from PIL import Image import os, random root train classes sorted(os.listdir(root)) sample_per_class 4 fig, axes plt.subplots(len(classes), sample_per_class, figsize(12, len(classes) * 3)) for row, cls in enumerate(classes): cls_dir os.path.join(root, cls) imgs random.sample(os.listdir(cls_dir), sample_per_class) for col, img in enumerate(imgs): ax axes[row][col] ax.imshow(Image.open(os.path.join(cls_dir, img))) ax.axis(off) if col 0: ax.set_ylabel(cls, rotation0, labelpad60, fontsize10) plt.tight_layout() plt.show()这段代码会从每个类别里随机抽 4 张图按“行是类别、列是样本”的方式排成一个大网格。重点在 axes 的标签设置第一列左侧显示类别名这样一眼就能看出每类数据的整体面貌。random.sample 保证每次运行看到的样本不同适合多轮抽查。4.2 用可视化做数据体检错标、光照和类别歧义跑通可视化后我会带着三个问题去看图。第一是错标某个类别里混入了明显无关的图像第二是光照和角度差异比如夜间驾驶样本占比是否足够因为车载摄像头最常见的干扰就是低光环境第三是类别之间的边界歧义尤其是“喝水”和“打电话”这类动作当驾驶员举着饮料瓶贴近耳边时不同标注员可能给出不同标签。这一步看起来是纯人工劳动但它决定的恰恰是模型上限。数据标注质量差的分类任务后面所有模型改进都是在错误标注的噪声里找规律结果只能越调越偏。如果发现某一类样本数量明显偏少先记录下类别分布在第 5 章的处理方案里会给出解决办法。4.3 用可视化结果反向验证数据集划分还有一个小技巧可视化时把 train/ 和 test/ 分开各跑一遍对比同一个类别的图像内容。如果发现测试集里出现与训练集几乎相同的连拍帧就说明划分时可能存在“帧泄漏”即同一段视频的连续帧被同时分到了训练集和测试集。这种情况在抽帧数据里偶发后续评估出的准确率会偏乐观我在下一章会具体展开。5. 训练与评估避坑五个值得写进笔记的排查记录这一章记录的是我自己在这类驾驶行为分类数据上踩过的五个坑每条都是真实翻车现场按“现象 → 原因 → 解决”整理希望能帮你少走弯路。5.1 类别索引错位训练正确但混淆矩阵对不上号码现象训练过程 loss 正常下降准确率也不错但打印混淆矩阵时发现“打电话”和“喝水”两个标签好像互换了类别名叫不上号。原因PyTorch 的 ImageFolder 会按文件夹名的字母序自动生成类别索引比如 calling 排第 0、drinking 排第 1而 JSON 文件里的编号可能是按采集顺序设计的两者恰好错开。模型本身没有错错的是我们把自己预设的编号套到了模型输出上。解决打印出 ImageFolder 的 classes 属性与 JSON 文件里的索引逐一比对。print(train_ds.classes) # 例如 [calling, drinking, ...] print(idx_to_class) # 例如 {0: safe_driving, ...}如果顺序不一致统一以 train_ds.classes 为准把 idx_to_class 重新映射一遍。宁可在训练前多花两分钟做这一步也别在出结果后对着混淆矩阵猜半天。5.2 批次内图像尺寸不一致DataLoader 直接报错现象使用自定义 Dataset 时训练跑到第几个 batch 突然报错。类似“Expected input batch_size to match target”或 stack 维度不一致。原因每张 jpg 的原图尺寸不同如果 transform 里只写了 ToTensor 而没写 ResizeDataLoader 在把一批图拼成一个 tensor 时会因为高度宽度不一致而失败。ImageFolder 场景不多见但一旦换成自定义读取就很容易踩中。解决transform 里强制统一尺寸最保险的是 Resize((224, 224))。如果某些实验需要保持原始宽高比就在 collate 里做 padding。def collate_pad(batch): images, labels zip(*batch) max_h max(img.shape[1] for img in images) max_w max(img.shape[2] for img in images) padded torch.zeros(len(images), 3, max_h, max_w) for i, img in enumerate(images): padded[i, :, :img.shape[1], :img.shape[2]] img return padded, torch.tensor(labels)5.3 部分图片解码异常训练中段崩溃现象训练跑到第 27 个 epoch 时 DataLoader 抛异常提示图片读取失败且每次崩溃位置不完全一样。原因数据集中存在少量损坏或不完整的 jpg 文件PIL 在读取时抛异常而 DataLoader 的多进程加载让报错时机变得随机看起来就像玄学崩溃。解决进入训练前先跑一遍全量过滤把无法正常解码的文件找出来。from PIL import Image import os bad_files [] for split in [train, test]: for cls in sorted(os.listdir(split)): cls_dir os.path.join(split, cls) for fname in os.listdir(cls_dir): fpath os.path.join(cls_dir, fname) try: with Image.open(fpath) as img: img.verify() except Exception: bad_files.append(fpath) print(损坏图片数量:, len(bad_files)) for fp in bad_files: print(fp)找出坏图后建议移出数据集目录而不是直接删除方便回溯。这步过滤通常只需几分钟但能避免训练到一半眼睁睁看着进程崩掉。5.4 连续帧泄漏进测试集准确率高得假现象测试集准确率 96%觉得模型已经完美但部署到实际车辆上发现误报率完全不是那么回事。原因驾驶舱视频采集是连续帧同一段视频里相邻几帧画面基本相同。如果抽帧后没有按视频片段划分数据集而是把所有帧混合后随机分到 train/ 和 test/那么训练集中某帧的“近亲”几乎必然出现在测试集中。模型记住的是这张脸和这个背景而不是“分心”这个行为模式。解决先做帧间相似度检查找出那些几乎重复的图像。一个快速做法是对所有图片计算感知哈希阈值内判为近重复帧再从训练集或测试集中剔除重复项。import imagehash from PIL import Image def dedup_by_hash(folder, threshold5): seen {} dupes [] for root, _, files in os.walk(folder): for f in files: path os.path.join(root, f) h imagehash.phash(Image.open(path)) for prev_h, prev_path in seen.items(): if h - prev_h threshold: dupes.append((path, prev_path)) break seen[h] path return dupes如果资源里本身就存在这类重复帧更稳妥的做法是按视频片段重新划分数据集而不是仅删除重复图因为删除会让有效样本量缩水。5.5 类别样本数不均准确率被大头类带偏现象整体准确率 92%但查看每类召回率时发现“喝水”只有 60%而“安全驾驶”接近 98%。原因10 个类别的样本数量天然不均安全驾驶在真实驾驶场景中占比最高模型学到的分布偏向了多数类少数类贡献的 loss 被稀释。解决先统计分布再用 WeightedRandomSampler 按类别权重采样。from torch.utils.data import WeightedRandomSampler labels [sample[1] for sample in train_ds.samples] class_count torch.bincount(torch.tensor(labels)) weights 1.0 / class_count[labels].float() sampler WeightedRandomSampler(weights, num_sampleslen(labels), replacementTrue) train_loader DataLoader(train_ds, batch_size64, samplersampler, num_workers4)这种情况下评估时也别只看准确率把每类的 precision、recall 和 F1 都打出来我一般直接用 sklearn 的 classification_report 看细粒度结果。6. 从单帧分类到视频级分心判断一个能平滑误报的时序投票技巧模型在单帧图片上表现好不代表在视频里能用。实际车载摄像头输出的是连续帧单帧预测经常出现“上一帧安全、下一帧分心、再下一帧又安全”的抖动输出。直接拿这种结果去做驾驶行为报警后台会收到一堆互相矛盾的报警记录这是单帧分类部署时最现实的翻车现场。解决思路很简单用连续帧的投票结果替代单帧判决。6.1 时序投票用最近 K 帧的预测做加权决策我实现的投票逻辑不复杂维护一个长度为 K 的滑动窗口每来一帧推一个预测结果进队列队列满后取众数作为这一时刻的最终标签。from collections import deque, Counter K 9 window deque(maxlenK) for frame in video_frames: pred frame_predict(model, frame) # 返回类别索引 int window.append(pred) if len(window) K: continue final_label Counter(window).most_common(1)[0][0] push_to_display(final_label)K 取 9 是我在 30fps 视频流下的常用起点对应约 0.3 秒的判决窗口既能让单帧误报被多数正确帧吞掉又不至于因为窗口过长而漏掉眨眼间发生的分心动作。K 越大越平滑但报警越迟钝这块需要自己权衡。6.2 聚合类别做二分类工程上更好用的评估方式如果你做的是真实驾驶报警系统我强烈建议把 10 类结果聚合为“专注/分心”两个状态再上线。安全驾驶算专注打电话、发短信、喝水、与乘客交谈等全部合并为分心。这个合并动作在评测时意义很大因为报警系统最终关心的不是“他在喝水还是发短信”而是“他现在是否具备安全驾驶条件”。聚合后的二分类混淆矩阵才是产品层面真正要向合作方汇报的指标。对应地模型评估也从 10 类 Top-1 准确率改成二分类的误报率和漏报率。从那以后我每次跑视频流实验都强制在模型后接一个 K 帧投票模块再统计指标单帧图片上的准确率再高也只作参考。训练前跑一遍数据预览、训练时给每类都打印细粒度指标、部署时用时序投票兜底这三件事按顺序走完整个项目才算真正闭环。希望这些经验能帮到你。本文还有配套的精品资源点击获取
返回列表