☰
深度学习分心驾驶识别实战:从模型训练到边缘部署
2026/10/11 13:11:05 网站建设 项目流程

简介:面向毕业设计、课程设计与期末大作业场景,这套驾驶员分心驾驶行为识别项目基于深度学习技术实现,整合了完整源码、训练数据集、预训练模型及配套论文文档。压缩包共三十一个文件,整体约六十五点三六兆字节,主要包含实验脚本、可视化报告、工具脚本、论文文档、演示动画与说明文件,目录结构清晰,类型覆盖代码、结果与文档,便于按需查阅。实现中采用多种主流卷积神经网络进行迁移学习微调,包括VGG、ResNet、Inception、Xception等经典结构,并提供了特征提取、数据划分、可视化等辅助脚本,代码注释详细,可完整展示从数据预处理到模型评估的深度学习流程。目前已有七百三十九人学习或下载,部署简单,适合作为毕业设计、课程设计或期末大作业的高分参考,兼具实用性与展示价值。

1. 分心驾驶识别:一个好毕设,更是一个能看到工业落地面的项目

第一次看到「基于深度学习实现驾驶员分心驾驶行为识别」这个题目,很多人第一反应是「又一个目标分类任务」——拿公开数据集训练一个 CNN,跑通就算毕业。但我真正把这个方向做完之后才意识到,它处在目标检测、时序分类和边缘部署的交汇点上,比单纯做一个图像分类器要复杂得多。你既要处理「驾驶员有没有看手机、打电话、喝水、伸手拿东西」这类状态识别,又要回答「模型在实车环境里到底靠不靠谱」,这恰好是工业界 ADAS 系统里一直在解决的问题。

这个项目适合两类人:一类是毕业设计选了相关方向、需要源码和数据集快速搭起实验的学生;另一类是已经在做车载视觉方案、想评估分心识别算法可行性但没时间从头攒数据集的工程师。它的核心产出可以拆成四块:一个能跑的深度学习模型、一份整理好标注的数据集、一整套训练与推理代码,以及支撑论文的消融实验和可视化结果。数据层面,公开方案基本都绕不开 State Farm 分心驾驶员数据集或自动驾驶场景下的驾驶员监控数据;模型层面,从 ResNet、MobileNet 这类图像分类网络到 YOLO 这类目标检测器都有各自的位置。接下来我按「数据选型 → 建模选型 → 训练配置 → 踩坑排查 → 论文落地」的顺序,把这条技术链路完整拆开。

2. 分心驾驶数据集怎么选:公开数据集与自建数据的搭配策略

2.1 State Farm 数据集的核心结构与局限

做分心驾驶识别,绕不开 Kaggle 上的 State Farm Distracted Driver Detection 数据集。它把驾驶行为分成十类:正常驾驶、右手发短信、右手打电话、左手发短信、左手打电话、操作收音机、喝水和拿东西。每个类别大约有数千张图片,都是驾驶舱视角拍出来的,背景是车辆座椅、仪表台和车窗,干扰相对可控。

但这里有个很关键的问题:这个数据集毕竟是 Kaggle 竞赛数据,图像拍摄环境相对理想,光线、角度都固定。拿它在测试集上刷到 95% 以上准确率并不难,难的是放在真实行车环境里,光线变化、夜间场景、戴墨镜、头部偏转角度大、方向盘遮挡,这些工况会让准确率直接掉到七成多。我一般建议把它当作「算法验证集」而不是「最终性能基准」。如果毕设论文里只报这个数据集上的指标,答辩时被问「实车场景怎么保证」会非常被动。

2.2 自建数据集的三个原则

如果你有实车条件,哪怕只是在固定车位拍摄,也比纯用公开数据集更有说服力。我见过很多做得好的毕设,都会自己补充 1000 到 2000 张真实驾驶舱数据。但自建数据有几个硬性要求:

第一,相机位置必须固定。安装在仪表台中央或后视镜附近,视角要能看到驾驶员头部和双手。第二,覆盖多种光照条件。白天逆光、地库暗光、夜间仪表盘光照,每样都要取一批。第三,标注要按动作切分,而不是只按时间段切分。拿「喝水」来说,手握住杯子和杯子送到嘴边这两个阶段差异很大,如果模型只学到了「手靠近中控」这个特征,换一个喝水姿势就识别错了。

import os import shutil from sklearn.model_selection import train_test_split # 自建数据目录结构: # data/raw/{class_name}/*.jpg # 划分成 train / val,比例按 8:2 raw_root = "data/raw" train_root = "data/train" val_root = "data/val" for class_name in os.listdir(raw_root): class_dir = os.path.join(raw_root, class_name) if not os.path.isdir(class_dir): continue images = [f for f in os.listdir(class_dir) if f.endswith(".jpg")] train_list, val_list = train_test_split(images, test_size=0.2, random_state=42) for split_root, split_list in zip([train_root, val_root], [train_list, val_list]): dest_dir = os.path.join(split_root, class_name) os.makedirs(dest_dir, exist_ok=True) for img in split_list: shutil.copy(os.path.join(class_dir, img), os.path.join(dest_dir, img))

这段代码做的是标准的训练集划分。注意random_state=42保证可复现,否则每次运行划分结果不一样,论文里报的准确率每次都会有微小浮动。用stratify按类别比例划分对于类别不平衡数据更好,但分心驾驶数据集本身类别还算均衡,用普通划分问题不大。

2.3 类别不均衡这个老问题

实际行车数据里「正常驾驶」的占比可能超过 60%,打电话、喝水等行为占比极小。如果不做处理,模型会学成「一切不确定的行为都归为正常」,整体准确率看起来有 90%,但每个异常类别的召回率可能只有 30%。

处理方式一般有两类:一是重采样,对少数类做过采样、对多数类做下采样;二是用加权损失函数,在交叉熵损失里给样本较少的类别更高的权重。PyTorch 里实现很简单:

class_counts = torch.tensor([3000, 350, 280, 320, 260, 240, 200, 210, 180, 190]) # 按类别样本数统计得到,顺序与数据集类别一致 weights = 1.0 / class_counts weights = weights / weights.sum() * len(class_counts) loss_fn = nn.CrossEntropyLoss(weight=weights.to(device))

weight参数就是在交叉熵计算时对每个类别乘以一个系数。注意这里的weights做了归一化,把均值拉回到 1 附近,避免损失值因加权而变得过大、带动学习率失控。我在第一次做实验时没做这一步,训练损失从 1.2 直接跳到了 12,模型完全学不进去,排查了半天才发现是权重把 loss 撑爆了。

3. 模型选型与训练策略:从分类网络到检测器的演进路径

3.1 图像分类路线:ResNet 与 MobileNet 的选择逻辑

分心驾驶识别最直接的建模思路是把它看成一个图像分类问题:输入一帧驾驶舱图像,输出 10 个行为类别的概率。基础模型选 ResNet 还是 MobileNet,取决于你要不要部署到 Jetson 这类嵌入式设备上。

如果你在服务器上用 GPU 训练,论文里可以考虑以 ResNet-50 为主模型,理由很实际:它在 ImageNet 上预训练权重的泛化能力最好,迁移学习起步效果稳定。毕设答辩时,ResNet-50 这个选择不容易被质疑。但 ResNet-50 单帧推理在 Jetson Nano 上大约能跑 15 到 25 FPS,如果后续还想叠加目标检测或其他预处理模块,帧率就会被拖垮。

MobileNetV3 是轻量化部署的另一个选择。它用深度可分离卷积替换标准卷积,参数量只有 ResNet-18 的约三分之一左右,同样帧率下占用更少的算力。我一般给出的建议是:论文主实验用 ResNet-50 报性能上限,同时加一个 MobileNetV3 的对比实验体现轻量化考虑,这也是评审老师喜欢看到的「部署意识」。

import torchvision.models as models import torch.nn as nn def build_model(num_classes=10, model_name="resnet50"): if model_name == "resnet50": model = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V2) in_features = model.fc.in_features model.fc = nn.Sequential(nn.Dropout(0.5), nn.Linear(in_features, num_classes)) elif model_name == "mobilenetv3": model = models.mobilenet_v3_large(weights=models.MobileNet_V3_Large_Weights.IMAGENET1K_V2) in_features = model.classifier[-1].in_features model.classifier[-1] = nn.Linear(in_features, num_classes) else: raise ValueError(f"not supported: {model_name}") return model

这里把最后一层全连接做了替换,同时额外加了一个 Dropout 层,比例设 0.5。它的作用是防止全连接层过拟合——尤其是在总量不到一万张的中小型数据集上,全连接层是过拟合最集中的区域。注意 ResNet-50 的fc是单个全连接层,而 MobileNetV3 的classifier是一个序列,需要替换最后一个Linear而不是整个classifier,两者的代码写法并不相同。

3.2 目标检测路线:为什么还要考虑 YOLO

分类模型回答的问题是「这帧图像里驾驶员在做什么」,但没法回答「驾驶员的手在哪里、手机在哪里」。有些异常行为的空间特征很关键——比如双手都离开方向盘这一条,分类模型可能漏掉,但检测模型可以定位手部和方向盘的位置关系,用逻辑规则做二次判断,识别就稳健得多。

YOLOv5 是当前做这个方向最顺手的检测器,不是因为它精度最高,而是因为工程链最完整:官方仓库自带训练脚本、数据增强策略和导出工具,用一份标注好的 VOC/COCO 格式数据从零训练到 ONNX 导出,几乎不需要改代码。如果你要检测「手部+手机+水杯+方向盘区域」,数据集标注需要转换到 YOLO 格式:

def voc_to_yolo(xml_path, img_w, img_h): # 从VOC XML读取并转换为YOLO txt格式 import xml.etree.ElementTree as ET tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall("object"): cls_name = obj.find("name").text # 标签映射:hand -> 0, phone -> 1, cup -> 2, wheel -> 3 cls_id = label_map[cls_name] bndbox = obj.find("bndbox") xmin = float(bndbox.find("xmin").text) ymin = float(bndbox.find("ymin").text) xmax = float(bndbox.find("xmax").text) ymax = float(bndbox.find("ymax").text) # 归一化到0~1 x_center = ((xmin + xmax) / 2) / img_w y_center = ((ymin + ymax) / 2) / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") return lines

这段代码看着简单,但四个边界坑很容易踩:一是坐标必须归一化到 0 到 1,直接用像素值训练会让边界框损失从大数值直接崩溃;二是边界框宽高最小限制要处理,有些标注框只有 5 个像素宽,归一化后不到 0.01,需要过滤掉;三是类别编号从 0 开始,而不是从 1 开始,写错一个数字整个训练就崩了;四是图像高度宽度要对应数据集原始尺寸,如果你训练时做了 resize,标注也要跟着做同等比例的缩放,否则框的位置会整体偏移。

3.3 分心行为的时间连续性怎么处理

单帧图像分类和单帧目标检测都忽略了一个关键信息:分心行为在时间上是连续的。正常驾驶持续了 5 秒,期间可能某一帧因为遮挡或光线被误判为异常;反过来,喝水这个动作从拿起杯子到放下差不多 2 秒,如果只识别单帧,很容易出现抖动。

给模型加时序信息有三种常见路线:第一种是把连续多帧图像堆叠成一个输入,经过 CNN 之后再用 LSTM 建模时序,这是最常用的双流结构;第二种是直接在帧级别的分类概率上做滑窗滤波和投票;第三种是用 3D CNN(如 SlowFast)直接处理视频片段,但计算量太大,毕设阶段不建议优先尝试。第三条路线对硬件要求高,一般不建议在毕设设备上做。我最常推荐的是第二种升级版——时序平滑:

import numpy as np from collections import deque class TemporalSmoother: def __init__(self, history_len=5): self.history = deque(maxlen=history_len) def update(self, class_id, prob): # 保存最近N帧的分类结果 self.history.append((class_id, prob)) def predict(self): # 多数投票 + 概率加权 votes = {} for cls_id, prob in self.history: votes[cls_id] = votes.get(cls_id, 0) + prob return max(votes, key=votes.get)

用概率加权的多数投票比单纯数票数更稳健。原因在于:如果某一帧模型给出的各类概率都很接近 0.1,说明模型自己也很犹豫,这时它的投票权重就应该低一些;而概率集中在某个类别 0.8 以上的帧,才是模型真正有把握的观测,应该给更高权重。这个滑窗大小调 5 到 7 帧比较合适,太大了会带来明显延迟,在真车测试时人能直观感觉到结果「慢半拍」。

4. 训练工程配置:预训练权重、优化器与训练超参数的落地清单

4.1 模型输入尺寸与数据增强策略

分心驾驶识别模型推荐把输入分辨率定在 224×224,这是个很现实的折中。原有图像是 640×480 左右,直接压到 224 会损失不少细节,但考虑到手机、水杯这类目标本身在画面里占比不小,224 分辨率足够分类任务用;如果走 YOLO 检测方向,输入再用 640 分辨率。分类和检测在分辨率选择上不一样,需要按实际任务决定。

数据增强有一组「性价比极高」的固定组合:随机水平翻转、随机旋转±15度、随机亮度对比度扰动。水平翻转要注意类别不对称的问题——「左手打电话」翻转为「右手打电话」,如果模型不知道翻转会交换左右含义,标签就错了。所以做水平翻转时,类别映射表也必须同步翻转换。在 PyTorch 里可以这样实现:

from torchvision import transforms train_transform = transforms.Compose([ transforms.Resize(256), transforms.RandomCrop(224), transforms.RandomHorizontalFlip(p=0.5), transforms.ColorJitter(brightness=0.3, contrast=0.2), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ])

注意Normalize用的均值和标准差是 ImageNet 统计值,不能改。预训练模型对输入分布有硬性要求,若改了数值特征分布就会出现奇怪的结果——训练 loss 一直降不下去。这是我入行以来见过最多的低级错误,不只是学生,很多开源项目里也有。如果你的代码是自己写的模型不加载预训练权重,那一组 normalize 值影响就很小;但一旦用了 ImageNet 预训练模型,这组值是强制的。

4.2 优化器与学习率:AdamW 是一切的起点

分心识别这种中小型数据集任务,优化器我建议直接定 AdamW,不要花时间试 SGD。AdamW 相对 Adam 主要改进了权重衰减的实现方式,把权重衰减从梯度更新中剥离出来,显式地在参数更新时独立执行。效果是训练过程更稳定,不容易出现 loss 在后期剧烈震荡的情况。

import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR optimizer = optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=50)

核心参数解释如下:

  • lr=1e-4是迁移学习时遇到的两个坑之一。用过高的学习率(比如 1e-3)去微调预训练模型,前面几层提取到的通用特征会被大幅破坏。但如果你冻结了主干只训练分类头,学习率可以放宽到 1e-3。
  • weight_decay=1e-4是 L2 正则化的具体实现,防止过拟合。如果训练集只有几千张,可以提高为 5e-4;如果数据量上万,1e-4 足够。权重衰减设置在 optimizer 里即可。
  • CosineAnnealingLR配合 50 到 70 个 epoch 的总训练周期,学习率会沿着余弦曲线从初始值降到 0。这比每隔固定步数手动降学习率的做法好——它不再需要人工盯验证集 loss 来确定降学习率的时机,训练收敛也更平滑。

4.3 训练过程中的三个关键监测指标

分心驾驶识别项目在训练过程中需要时刻关注三个指标:验证集准确率、训练集准确率与验证集准确率的差值、以及每个类别的召回率而非整体准确率。

如果训练集准确率到了 99% 而验证集只有 82%,这是过拟合的典型信号。这时候优先增加 Dropout 概率,然后加大数据增强强度,最后才是引入更复杂的正则化手段。如果训练集准确率本身就只有 85%,那大概率是模型没收敛——检查学习率是不是过低、权重初始化是否正确、数据加载顺序是否有问题。

整体准确率最容易骗人。类别不均衡时整体准确率 90% 但异常行为召回率不到一半的情况很常见。所以训练脚本里要单独输出每个类别的 precision 和 recall,其中「正常驾驶被误判为异常」这类错误的代价很高,宁可少报也别虚报。

from sklearn.metrics import classification_report y_true_list = [] y_pred_list = [] # 每轮验证结束后收集真实标签与预测标签 report = classification_report( y_true_list, y_pred_list, target_names=["safe", "text_right", "call_right", "text_left", "call_left", "radio", "drink", "reach"], digits=3 ) print(report)

这份分类报告能直接展示每个类别的 precision、recall 和 F1,写论文做表格说明时可以直接引用。注意digits=3控制小数位数,毕设论文里保留三位小数比较严谨,写报告时再统一改成百分比,千万不要出现「92.3%」和「0.92」混用的情况。

5. 分心行为识别避坑指南:五条血泪经验

5.1 预训练模型加载失败:权重的黑匣子问题

现象描述:模型训练 loss 一开始就居高不下,或者干脆报错Missing key(s) in state_dict,提示fc.weight维度不匹配。

直接原因分析:最后一个全连接层的类别数不一致。你下载的预训练模型在 ImageNet 上输出是 1000 类,而你的任务只有 10 类,自然对不上,这是最容易被排除的原因。但真正隐蔽的问题在于权重下载路径——很多代码里写了pretrained=True,但离线环境下权重缓存找不到,代码会自动跳过加载。你以为是迁移学习,实际是随机初始化训练,性能和收敛速度都会差很多。

解决办法:你要做三件事来防止这种黑匣子情况。第一步,打印模型文件大小,确认权重真实存在且大小在 80MB 以上(ResNet-50 预训练权重约 90MB)。第二步,设置环境变量TORCH_HOME指定权重缓存目录,避免下载到系统临时目录被清理。第三步,加载权重后用一行代码验证特征提取器是否正常工作:跑一次随机输入看输出维度,再对比加载前后同一个输入的特征向量是否一致。

5.2 验证集准确率很高但实际效果差:目标泄露

现象描述:训练曲线很漂亮,验证集准确率 94%,但你拿手机对着电脑摄像头模拟分心动作测试时,识别结果几乎全是错的。

深层原因分析:数据集划分操作失误。比如用split_folders按图片文件随机划分时没有按驾驶者 ID 隔离,同一个人的训练图和测试图高度相似,模型学到的是「这是某个人」而不是「这是某个动作」。在 State Farm 数据集上,这个坑极其常见——训练集和测试集可能来自不同驾驶员,如果你直接按文件名随机划分,就会造成测试时看到同一个人在不同图片里的相似姿态,这属于典型的目标泄露。

解决办法:按驾驶员 ID 划分数据集,而不是按图片文件划分。State Farm 数据集的文件名里带有 driver ID 字段,可以通过groupby方式做分组划分,确保同一个驾驶员的全部图片只在训练集或只在验证集。

5.3 模型在夜间场景集体翻车:光照分布的代沟

现象描述:白天验证集准确率 93%,换到夜间或者地库光线环境,准确率掉到 74%。模型把正常驾驶误判为打电话和喝水,整个推理结果没法用。

本质原因分析:训练集中夜间图像占比可能不足 5%,模型学到的是「亮堂堂」环境下的特征组合。夜间图像对比度低,座椅颜色和皮肤颜色在暗光下的分布跟白天完全不同。模型大概学到了用亮度做判断的捷径,而不是用驾驶员姿态做判断。

解决办法有三层:第一层是数据补齐,夜间样本至少要补到总量 15% 到 20%,MixUp 等策略帮助有限;第二层是图像增强,在训练时加入随机 gamma 变换和对比度缩放,模拟不同光照条件下的图像分布;第三层是推理时做自适应预处理——先计算输入图像的平均亮度,低亮度时自动拉升对比度再进模型。第三层在部署时非常有用,夜间的浅色衣物和座椅、深色皮肤的暗部细节都能被拉开。

5.4 训练时 loss 出现 NaN:学习率与归一化的双重陷阱

现象描述:训练到某个 epoch,loss 直接变成nan,验证集准确率瞬间归零,而且不管怎么调低学习率都恢复不过去。

常见原因分析:一是损失函数里出现了 log 0 的情况;二是学习率过大导致梯度过冲;三是输入数据里有异常像素值。分心驾驶识别最容易触发第三个原因——某几张图像是损坏的或者全黑图片,归一化后变成所有通道都是负值输入到网络里。

解决办法:要养成训练前做数据体检的习惯。核心是一条命令扫描所有图像文件,检查是否能被PIL正常打开、尺寸是否一致、像素值是否全为 0。全黑图和缺失文件直接删除或者标记。另外强调检查损失计算时是否用了torch.nn.CrossEntropyLoss而不是手动实现-log(softmax(x))。手动实现很容易在数值上出问题。

5.5 测试集上的 FPS 达标但实车延迟高:处理瓶颈不在模型推理

现象描述:本地测试时模型推理速度 30 FPS,帧率看起来还行。但部署到实车后,从摄像头取帧到画面显示结果,体感延迟超过 1 秒,完全做不到实时反馈。

本质原因分析:延迟的主要来源不是模型推理,而是视频采集链路和前后处理。OpenCV 的VideoCapture默认缓冲队列会堆积帧,导致读取到的是几百毫秒前的画面;目标检测或关键点检测等预处理模块如果和分类串行执行,帧率会被最慢的模块拖住。

实际解决办法:摄像头用cv2.CAP_PROP_BUFFERSIZE拉到 1 到 2 个缓冲;推理用独立线程,把模型输入和结果输出解耦;前后处理能并行的就并行。这一套组合拳做下来,体感延迟能从 1.2 秒降到 200 到 300 毫秒左右。顺序处理 vs 并行处理在同一台 Jetson 设备上带来的延迟差异非常明显,值得认真对待。

6. 论文落地与毕设部署:消融实验、可视化与模型导出验证

6.1 消融实验:务必证明每一个模块都有存在价值

答辩时评委最常问的一个问题是:「你这个准确率比基线高,是因为你的方法好,还是因为你花了更多时间调参?」消融实验就是用来回答这类质疑的。它的标准做法是围绕模型最小可用版本逐步加模块,分别记录准确率变化。分心驾驶识别方向,建议至少做四组对照实验:

第一组:只用 ResNet-50 分类单帧,不做时序平滑。基线组。第二组:用 ResNet-50 加时序滑窗平滑。验证时序贡献。第三组:用 MobileNetV3 加时序平滑。验证轻量化方案性能差异。第四组:用 YOLO 检测手部与水杯,再加规则判断异常行为。验证目标检测路线的性能。

experiments = { "baseline": {"model": "resnet50", "smooth": False, "yolo": False}, "resnet50_smooth": {"model": "resnet50", "smooth": True, "yolo": False}, "mobilenet_smooth": {"model": "mobilenetv3", "smooth": True, "yolo": False}, "yolo_rule": {"model": "yolov5s", "smooth": True, "yolo": True}, } # 每组实验固定训练种子:random_state=42

写代码要统一固定随机种子,还要保证训练数据顺序一致。不然实验结果差异到底是来自方法还是来自随机性,完全说不清楚。每组实验的 epoch 数和 batch size 保持一致,控制变量之后,论文里的数据才有说服力。

6.2 注意力热力图与混淆矩阵:答辩最有说服力的两张图

分心驾驶识别方向的毕设答辩,只贴训练曲线会显得单薄,因为训练曲线只能证明模型在训练过程收敛了,不能证明它学会了关注驾驶员手部和手机区域。建议你用 Grad-CAM 生成注意力热力图,可视化模型在判断「打电话」时到底在聚焦哪些区域。如果热力图集中在方向盘和驾驶员面部,那么这个模型的决策依据很可疑;如果集中在手部和手机位置,说明模型学到了有意义的视觉特征。

混淆矩阵同样重要。分心驾驶的十类行为里有几对极易混淆:「左手发短信 vs 右手发短信」「喝水 vs 拿东西」,这两组动作在姿态上很接近,混淆矩阵能直观看出来模型在哪里犹豫。论文里不需要贴满 10×10 的密集矩阵,可以挑 4 到 5 对最关键的行为类别做小规模的混淆矩阵分析,或者把最大混淆值标红吸引视力,这会很直观。

6.3 从 PyTorch 到 ONNX 导出与推理验证

毕设如果只是写「准确率达到 90%」,评审印象不会太深。但如果附加完成一个 ONNX 导出与推理验证环节,整体项目成熟度会上一个台阶。这也是实践环节最容易展示的部分。

import torch import onnxruntime as ort import numpy as np model.eval() # 导出 ONNX dummy_input = torch.randn(1, 3, 224, 224) onnx_path = "driver_distraction.onnx" torch.onnx.export( model, dummy_input, onnx_path, opset_version=12, input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}} ) # 用 ONNX Runtime 验证推理结果与 PyTorch 一致性 ort_session = ort.InferenceSession(onnx_path) ort_inputs = {"input": dummy_input.numpy()} ort_outputs = ort_session.run(None, ort_inputs) # 对比输出差异 pt_output = model(dummy_input).detach().numpy() diff = np.abs(pt_output - ort_outputs[0]).max() print(f"max diff: {diff:.6f}")

导出和验证的要点有三个。第一,opset_version选 12 是为了兼容性,过高版本的算子在某些推理框架上不被支持。第二,dynamic_axes设置动态 batch 维度可以让推理时单帧和多帧批处理都支持,一次导出多样使用。第三,PyTorch 输出和 ONNX Runtime 输出最大差值如果超过 1e-3,说明模型里存在某些算子转换精度损失,考虑用脚本化方式重导出或者检查量化设置。

6.4 真机验证的一个习惯

完成代码和模型之后,很多学生直接就交论文了,忽略了一个关键环节:真机验证。如果实验室能借到工业相机或者 USB 摄像头,你可以做一个非常简单的验证台:摄像头正对驾驶模拟器的座椅位置,做一个简单的 PyQt 界面,实时显示当前分类名称和动作概率。值得强调的是:验证完之后一定记得标注一下延迟数据是多少,这组真实测试数据比任何训练曲线都更有说服力。

我个人的一个习惯是,在真机验证时记录模型输出的原始概率而不仅是最终分类结果。因为分类结果只能告诉你是「打电话」,概率的变化过程才能告诉你模型的置信度是稳定的还是忽高忽低的。如果模型在喝水动作面前输出概率在 0.3 到 0.8 之间来回震荡,那说明模型对类别边界没有学踏实,后面还需要补充对应的训练数据。

另一个不太被注意但很实际的教训是:训练的时候多存几份不同阶段的 checkpoint,不要只保留最后一份。因为训练后期可能已经开始过拟合,验证集上表现最好的那个 epoch 往往不是最后一个 epoch。我习惯每 5 个 epoch 存一份权重,每个 epoch 结束时记录验证集准确率,训练结束后取验证集准确率最高的 checkpoint 作为最终版本。这个习惯帮我避免过多次「训练了一晚上最后一轮模型表现不行」导致的返工重跑纠结,算是给未来留的一份后悔药。

做这个项目整体收益还是值得的——它既覆盖了深度学习的标准流程,又有真实工业背景,还给了你一个从离线训练走到在线部署的完整视角,希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询