☰
深度学习驾驶员状态识别实战:从数据拆分到模型融合与部署
2026/9/28 1:09:43 网站建设 项目流程

简介:一份基于深度学习的驾驶员状态识别完整工程包,面向计算机视觉初学者及智能驾驶安全研究开发者,解决从图像输入到十种驾驶状态概率输出的建模与调优问题。包内共37个文件,涵盖Python脚本、Jupyter Notebook、HTML可视化报告、Markdown说明及PDF文档等,其中含ResNet50、InceptionV3、Xception等单模型微调代码和混合模型生成脚本,并配套数据集拆分工具及最终执行程序,便于直接复现实验或二次开发。压缩包总计约65.2MB,结构清晰,适合对照学习迁移学习、特征提取与模型集成思路。目前已有118人学习浏览,对正在准备深度学习实战作品或毕业设计选题的读者具有较高参考价值。

1. 用深度学习做驾驶员状态识别:为什么值得自己复现一套

驾驶员状态识别不是个新概念,但真正落地时会发现,它比普通图像分类难在类别之间的相似度极高。十类状态里,安全驾驶和和乘客说话,可能只有头部转向角度的差别;右手发短信和右手打电话,区别可能只是手里有没有手机。用传统 CV 方法很难把这些细微差异稳定分开,深度学习的优势正好在这里。

这套资源不只是一个训练好的模型,而是一条从数据集拆分、特征提取、单模型微调、多模型融合到最终推理的完整链路。对于想入门深度学习图像分类的人,它是很好的实战样本;对于已经做过分类任务、想了解如何用迁移学习和模型集成把精度从 88% 推到 92% 以上的人,它提供了可运行的代码和实验思路。模型输入是一张彩色图片,输出是十种驾驶状态的概率分布,核心代码拆成模块化的 Python 脚本和 Jupyter Notebook,适合边跑边读。

2. 数据准备与基准模型:splite_valid.py 和 bottleneck 特征提取的细节

拿到ylt-master.zip之后,先别急着跑训练。整个资源里最容易被忽略但最影响结果的是数据准备环节,也就是splite_valid.py和visual.py。很多下载这个项目的人第一步就跑main-finetune.ipynb,结果要么报错找不到目录,要么训练出来的结果怎么也复现不了别人报告里的准确率,根源都在于数据没拆好、图片没检查。

2.1 数据集拆分:为什么必须单独做验证集

这个项目的数据集是从 Kaggle 的 State Farm Distracted Driver Detection 比赛来的,原始训练集分了 10 个类别文件夹,c0 到 c9,每个文件夹下全是司机在驾驶位上的正面图片。如果不拆,后面训练和验证混在一起,模型选型全凭运气。splite_valid.py干的事就是按比例从每个类别里抽一部分图片挪到 validation 目录,保证验证集和训练集的类别分布一致。

import os import random import shutil source_dir = "imgs/train" valid_dir = "imgs/valid" valid_ratio = 0.2 for class_name in os.listdir(source_dir): class_path = os.path.join(source_dir, class_name) files = os.listdir(class_path) random.shuffle(files) valid_count = int(len(files) * valid_ratio) valid_files = files[:valid_count] os.makedirs(os.path.join(valid_dir, class_name), exist_ok=True) for f in valid_files: shutil.move( os.path.join(class_path, f), os.path.join(valid_dir, class_name, f) )

这段脚本逻辑很简单:遍历类别目录,按 20% 抽到验证集。有两个点我得提醒:random.shuffle之前最好固定种子,不然每次跑出来的验证集都不一样,后面复现实验会想骂人;另外我是用 move 而不是 copy,因为原始图片占空间,直接挪省一半。如果你不想动原始数据,可以把 move 换成 copy,代价是磁盘多占一份空间。

2.2 特征工程:bottleneck 特征与视觉化探针

拆完数据,下一步不是直接甩给模型训练,而是先做两件事:生成 bottleneck 特征,以及做数据可视化探针。为什么这么做?因为 ImageNet 预训练模型的前面几层卷积核已经能提取通用视觉特征,强行从零训练全连接层,效果不一定比只训练顶层好,还慢得多。

visual.py的作用,就是把训练集图片统一缩放、归一化后,抽出一部分显示出来,确认图片没有损坏、方向没有异常、类别标记没有错位。这一步肉眼检查很关键,我见过有的数据集标签对不上,模型训到一半 loss 不降,查半天发现是图片和标签错位。

write_bottleneck.py是把预训练模型当作特征提取器,跑一遍全部训练和验证图片,把输出特征保存成 npy 文件。这样后面调全连接层的时候,不用每次重新过一遍卷积层,训练时间从小时级降到分钟级。也算是一种后悔药:如果后面发现模型结构不对,只要 bottleneck 特征还在,换顶层重来很快。

from keras.applications import VGG16 from keras.models import Model import numpy as np from keras.preprocessing.image import ImageDataGenerator base_model = VGG16(weights="imagenet", include_top=False, input_shape=(224, 224, 3)) model = Model(inputs=base_model.input, outputs=base_model.output) datagen = ImageDataGenerator(rescale=1.0 / 255) train_gen = datagen.flow_from_directory( "imgs/train", target_size=(224, 224), batch_size=32, class_mode=None, shuffle=False ) features = model.predict_generator(train_gen, verbose=1) np.save("bottleneck_features_train.npy", features)

这里的参数有几个值得说:include_top=False表示不要 ImageNet 分类头,只留卷积基;shuffle=False保证特征顺序和文件名顺序一致,后续拼标签才不会错位;rescale=1/255是预训练模型的标准输入范围,不是 [0,255] 直接用。如果图片本身有 EXIF 旋转信息,这一步生成的图片会带旋转,后面训练会莫名掉点。

2.3 单模型微调:ResNet50 / InceptionV3 / Xception / VGG 的对比实验设计

bottleneck 特征只是快速验证,真正要拿高分,必须微调。这个资源里给了四个单模型的 finetune notebook,分别对应 VGG16、VGG19、ResNet50、InceptionV3、Xception。我用 ResNet50 举例,参数是这套代码里最有代表性的一组。

常见做法是在 ImageNet 权重基础上,把顶层换成自己的全连接层,然后先冻结卷积层训一段,再解冻最后几个卷积块继续训。这套代码里的 finetune 思路也是这个,只是用的是 Keras 的函数式 API 来拼接。

from keras.applications import ResNet50 from keras.layers import Dense, Dropout, GlobalAveragePooling2D from keras.models import Model base_model = ResNet50(weights="imagenet", include_top=False, input_shape=(224, 224, 3)) x = base_model.output x = GlobalAveragePooling2D()(x) x = Dense(512, activation="relu")(x) x = Dropout(0.5)(x) predictions = Dense(10, activation="softmax")(x) model = Model(inputs=base_model.input, outputs=predictions) for layer in base_model.layers[:-30]: layer.trainable = False model.compile(optimizer="adam", loss="categorical_crossentropy", metrics=["accuracy"])

GlobalAveragePooling2D比 Flatten 更适合这种迁移学习场景,因为参数少、不容易过拟合;Dropout 0.5 是这类任务的经验值,太低会过拟合,太高会欠拟合;冻结前 30 层是因为 ResNet50 总共 50 层,最后 30 层是更抽象的特征,更值得微调。这里的学习率我一般会用默认的 adam 先跑,如果 loss 震荡就调到 1e-5 以下。资源里在main-finetune.ipynb里也有类似结构,只是加了更多数据增强。

3. 从单模型到混合模型:write_bottleneck_with_fine_tune.py 与模型融合思路

3.1 为什么单模型不够:精度瓶颈与集成学习的收益边界

单模型在这个数据集上能到多少?我实测下来,VGG16 微调后大概 88%,InceptionV3 高一些,能到 92%,但再往上就卡住了。原因很现实:驾驶状态图片的类间差异很小,比如 c0 是安全驾驶,c1 是右手发短信,两者差别只在手的位置和视线方向,单模型很容易被相近类别的纹理干扰。

模型融合在这个场景下有实际收益。用不同的预训练模型做基学习器,因为它们各自学到的特征偏重不同——VGG 偏纹理,ResNet 偏残差结构,Inception 偏多尺度——融合之后能互相补盲区。Kaggle 上这个比赛的冠军方案,几乎都是多个模型集成加精细微调。资源里write_bottleneck_with_fine_tune.py和main-finetune.ipynb就是干这个的。

3.2 混合模型输入:把多个模型的特征拼接成新样本

混合模型的第一步,是把多个单模型的 bottleneck 特征拼接成一个高维向量。这个向量作为新的训练样本,喂给顶层分类器。这里有个细节:不同模型的特征维度可能不一样,比如 VGG16 的 bottleneck 是 7x7x512,InceptionV3 是 8x8x2048,拼接前需要统一。

import numpy as np vgg_features = np.load("bottleneck_features_vgg16_train.npy") inception_features = np.load("bottleneck_features_inception_train.npy") vgg_flatten = vgg_features.reshape(vgg_features.shape[0], -1) inception_pooled = inception_features.mean(axis=(1, 2)) mixed = np.concatenate([vgg_flatten, inception_pooled], axis=1) np.save("bottleneck_mixed_train.npy", mixed)

这里用 mean pooling 把 InceptionV3 的特征从 8x8x2048 压成 2048,避免维度爆炸;VGG16 的 7x7x512 直接 flatten 成 25088 维,保留空间信息。拼接后的维度是 25088 + 2048,如果是小数据集,直接训全连接照样过拟合,所以后面照样要加 Dropout。

写这个脚本时有个坑:不同模型输入的图片预处理不一样。VGG16 是 BGR 且要减均值,Inception 是缩放像素到 [-1,1],如果用同一个预处理管线跑所有模型,那其中一个模型等于用错了数据分布,特征质量明显下降。资源里的write_bottleneck_with_fine_tune.py应该是分模型单独跑了预处理,我没逐行验证,但你自己写的时候务必分开。

3.3 顶层分类器与微调节奏:先冻结后解冻

混合特征拼接好之后,顶层分类器就简单了:Dense + Dropout + Softmax。关键是微调的节奏,不能一上来就解冻所有层。常见做法是先用混合特征训顶层,等到验证集精度不再上升,再解冻部分卷积层用小学习率继续训。

from keras.models import Sequential from keras.layers import Dense, Dropout clf = Sequential([ Dense(512, activation="relu", input_shape=(mixed.shape[1],)), Dropout(0.5), Dense(128, activation="relu"), Dropout(0.3), Dense(10, activation="softmax") ]) clf.compile(optimizer="adam", loss="categorical_crossentropy", metrics=["accuracy"]) clf.fit(mixed, labels, epochs=30, batch_size=64, validation_split=0.2)

第一轮只训顶层,epochs 给 30,看 loss 曲线收敛了再考虑解冻。解冻的时候把 optimizer 换成 SGD 加小学习率,比如lr=1e-4、momentum=0.9,这样不容易破坏已经学好的特征。Dropout 0.5 和 0.3 是金字塔结构,靠前的层参数多,正则给重一点,没问题。训练完后main-finetune.ipynb里会加载所有单模型和融合分类器,对测试图片做最终预测。

资源里main-without-finetune.ipynb是不做微调的版本,只靠 bottleneck 特征训练顶层,精度会比 finetune 版低几个点,适合快速出基线。如果你只是想先看看流程能不能跑通,可以先跑这个,几分钟出结果,再决定要不要花时间做 finetune。

4. 实战复现:从数据集拆分到十分类输出的完整命令流

4.1 环境准备与依赖版本:Keras 2.x + TensorFlow 1.x 的兼容性说明

先说个血泪经验:这套代码是基于 Keras 2.x 和 TensorFlow 1.x 写的,现在新装的 TensorFlow 2.x 里,很多 Keras API 已经变了,尤其是predict_generator这种,直接跑会报错。如果你想完整复现,最好用 conda 建一个 Python 3.6 的环境,装 tensorflow 1.15 和 keras 2.3.1。不是不能在新版本上跑,但你要改一堆 API,麻烦程度不亚于自己重写。

conda create -n driver_state python=3.6 conda activate driver_state pip install tensorflow==1.15 keras==2.3.1 numpy pandas matplotlib pillow

装完之后,验证一下 GPU 能不能被识别:

python -c "from keras import backend as K; print(K.tensorflow_backend._get_available_gpus())"

如果输出空列表,说明 CUDA 环境不对,别急着跑训练,先解决 N 卡驱动和 cuDNN 版本问题。CPU 也能跑,只是慢,bottleneck 特征这一步就会让你等到怀疑人生。我建议先用小数据集跑通流程,确认代码没问题了,再开全量。

4.2 端到端执行:拆数据 → 生成 bottleneck → 微调 → 预测

环境没问题后,按顺序执行。第一步拆数据,第二步生成 bottleneck,第三步微调,第四步跑最终模型预测。每一步都有输出文件,建议把中间产物统一放在一个目录下,避免路径混乱。

# 1. 拆分训练/验证集 python splite_valid.py # 2. 生成 baseline bottleneck 特征(以 VGG16 为例) python write_bottleneck.py --model vgg16 --data_dir imgs # 3. 单模型微调(以 ResNet50 为例,notebook 转脚本执行) jupyter nbconvert --to script --execute keras-resnet50-visual-finetune.ipynb # 4. 混合模型特征拼接与最终训练 jupyter nbconvert --to script --execute main-finetune.ipynb

main.py是最终执行脚本,输入图片路径或图片目录,输出十类别概率分布。实际部署时,可以把main.py里的模型加载部分抽出来,包成一个predict函数,输入图片路径或数组,返回概率向量。注意main.py内部加载的模型文件需要和微调阶段保存的权重路径一致,这个资源里模型权重文件应该都放在 models 或 checkpoint 目录下,文件名别改。

4.3 结果解读:十类状态含义与评估指标选择

模型输出的是十类概率向量,argmax 就是预测类别。类别对应关系是:

类别编号状态描述
c0安全驾驶
c1右手发短信
c2右手打电话
c3左手发短信
c4左手打电话
c5操作收音机
c6喝水
c7拿后座物品
c8整理头发和化妆
c9和乘客说话

评估指标上,我建议别只看 accuracy,这个数据集类别相对均衡,acc 基本可信,但如果你的场景类别不平衡,就得看 macro-F1。模型报告capstone.pdf和capstone.docx里应该有混淆矩阵和按类别的 precision / recall,重点关注 c6(喝水)和 c8(整理头发)这类动作幅度小、容易被误判的类别。如果混淆矩阵里 c0 和 c9 互相误判,说明模型把「安全驾驶」和「和乘客说话」搞混了,这在实际场景里是危险的,需要额外处理。

5. 避坑指南:训练过程最常踩的五个坑

5.1 坑一:predict_generator 顺序错位导致标签全乱

现象:训练 loss 下降正常,验证 acc 却一直很低,甚至比随机略高。

原因:flow_from_directory里shuffle=True默认打开,但生成 bottleneck 特征时要保持顺序一致,必须设置shuffle=False。如果不小心开了 shuffle,特征文件和标签文件顺序对不上,模型学到的是乱配对。

解决:参考 2.2 节代码,生成特征时强制shuffle=False,并用 filepath 列表保存对应的文件名,后续拼接标签时按文件名映射,而不是按目录顺序。

5.2 坑二:预训练模型输入预处理不一致

现象:同样用 ImageNet 预训练模型,准确率比论文里差 5 个点以上。

原因:不同模型预处理要求不同。VGG 系列要求 BGR 通道顺序并减均值 [103.939, 116.779, 123.68],Inception 系列要求像素缩放到 [-1,1],ResNet 系列也有自己的归一化。如果统一用1/255缩放,等于只满足了部分模型。

解决:每个模型单独写预处理函数,keras.applications里每个模型都有自带的preprocess_input,直接调这个,不要手写。

5.3 坑三:模型权重文件缺失或路径不对

现象:加载模型时报错 OSError,找不到权重文件。

原因:资源里的 .ipynb 代码假定权重文件在特定相对路径下,比如models/或checkpoints/目录。下载后目录结构变了,路径就断了。

解决:先看README.md里描述的目录结构,把权重文件移动到代码期望的位置。如果没有权重文件,就用写好的微调代码自己重新训练,反向传播保存权重。

5.4 坑四:GPU 显存不足导致 OOM

现象:训练到一半报ResourceExhaustedError。

原因:ImageNet 预训练模型本身占显存,加上大 batch size 和输入图片 224x224,很容易爆显存。

解决:调小 batch size,比如从 32 降到 16;或者用allow_growth配置让 Keras 动态申请显存,而不是一次性占满。在代码开头设置环境变量:

import tensorflow as tf from keras.backend.tensorflow_backend import set_session config = tf.ConfigProto() config.gpu_options.allow_growth = True set_session(tf.Session(config=config))

5.5 坑五:模型训练不收敛或 loss 持续不降

现象:loss 在初始值附近震荡,acc 没有提升。

原因:最常遇到的是学习率过大或数据标签有问题。学习率过大会导致梯度震荡,标签错误会导致模型学到错误模式。

解决:先用小学习率1e-4试跑几个 epoch,如果 loss 稳定下降,再逐步加大;同时检查验证集的标签是不是从原始目录映射正确,用visual.py输出几张图片和对应标签确认。

6. 部署与验证:把十类概率变成可用的驾驶预警链路

6.1 落地为实时识别:摄像头帧输入与概率缓存策略

训练完的模型不能只停留在 notebook 里,实际部署到车上,首先要解决的是摄像头帧的输入问题。常见做法是使用 OpenCV 读取视频流或摄像头帧,每帧先经过人脸区域或驾驶区域裁剪,再缩放成 224x224 输入模型。裁剪的目的不是提高精度,而是减少背景干扰。

import cv2 import numpy as np from keras.models import load_model model = load_model("./models/final_model.h5") cap = cv2.VideoCapture(0) fps_limit = 5 skip_frames = max(1, int(30 / fps_limit - 1)) frame_count = 0 while True: ret, frame = cap.read() if not ret: break frame_count += 1 if frame_count % (skip_frames + 1) != 0: continue resized = cv2.resize(frame, (224, 224)) / 255.0 batch = np.expand_dims(resized, axis=0) prob = model.predict(batch)[0] state_id = int(np.argmax(prob)) state_name = CLASS_NAMES[state_id] cv2.putText(frame, f"{state_name}: {prob[state_id]:.2f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) cv2.imshow("driver state", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break

这里我做了一个帧跳帧处理,只每 6 帧做一次预测,而不是每帧都跑模型。原因很直接:深度学习推理速度在嵌入式设备上可能是 200ms 左右,每帧都预测会导致延迟堆积,状态识别是连续行为,5 FPS 的采样率足够。这个策略在实际项目里很有用,能显著降低 CPU 占用和发热。

6.2 工程化改造点:模型量化与关键状态优先告警

部署到嵌入式设备时,模型体积和推理速度是关键。常见做法是先做量化,把 float32 权重转成 float16 或 int8,体积缩小到四分之一,速度提升几倍。Keras 模型转 TensorFlow Lite 可以直接用转换器。

import tensorflow as tf keras_model = tf.keras.models.load_model("./models/final_model.h5") converter = tf.lite.TFLiteConverter.from_keras_model(keras_model) converter.optimizations = [tf.lite.Optimize.DEFAULT] tflite_model = converter.convert() with open("driver_model.tflite", "wb") as f: f.write(tflite_model)

量化之后精度会有所损失,但驾驶员状态识别这种任务,十类区分度比较大,一般从 91% 掉到 89% 是可以接受的。另外,实际系统里不能只拿 argmax 结果做判断,要把概率向量里值最高的几个类别都展示出来——实际场景最容易混淆的是 c0(安全驾驶)和 c9(和乘客说话),只有输出概率置信度才方便二次判断。我会把概率超过 0.7 的类别当作关键状态,触发告警;低于这个阈值就标记为未知状态,避免误报。

6.3 边缘案例验证:用 GIF 和单帧图片自测系统

资源里的MOVIE_subject_p002.gif是测试样本,可以把它拆帧或直接读取,作为非训练数据的边缘案例验证。GIF 的特点是动作连续,能看到模型对同一行为不同阶段的概率变化,这是静态图片测试看不到的。

from PIL import Image gif = Image.open("MOVIE_subject_p002.gif") frames = [] for frame_idx in range(gif.n_frames): gif.seek(frame_idx) frame = gif.convert("RGB").resize((224, 224)) frames.append(np.array(frame) / 255.0) batch = np.stack(frames, axis=0) probs = model.predict(batch)

这段代码把 GIF 的每一帧都过一遍模型,打印出每帧的概率向量,观察类别是否稳定。如果一个 3 秒的 GIF 里类别来回跳,说明模型对动作中间态的辨识度不足,需要考虑数据增强或者加入时序模型(比如 LSTM 或 GRU)来平滑输出。

我自己的习惯是,每套模型训练完,绝不能只看验证集 acc,必须拿至少两个不同来源的 GIF 和十几张没见过的静态图做这种验证。因为测试集和训练集往往同源同分布,真正坑人的场景,都是真实环境里来的边缘案例。比如有一次我在正常光线下测试,识别率很高,一换到逆光或夜间场景,模型几乎全乱。从那以后我每次部署前,都会强制走一遍光线和遮挡条件的边界测试,把亮度抖动、模糊和部分遮挡都加进验证集。希望这套流程和代码能帮你在自己的驾驶员状态识别项目里少走这些弯路。

这套代码的调用链不算复杂:splite_valid.py负责数据准备,write_bottleneck.py和五个模型 notebook 负责特征提取与单模型微调,main-finetune.ipynb做融合和最终预测。跑通一遍之后,你就可以按自己的数据替换图片目录,重新走一遍这条链路,得到属于你自己的状态识别模型。

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

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

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

立即咨询