基于SEED-VIG脑电数据的驾驶疲劳检测:Python五步实战流程

📅 发布时间:2026/9/18 12:10:23
基于SEED-VIG脑电数据的驾驶疲劳检测:Python五步实战流程
从2019年开始做车载安全相关的算法验证我们团队一直在找驾驶疲劳方向的公开数据。当时找遍各大平台真正能用的疲劳脑电数据集非常少要么样本量太小要么只有单一受试者要么干脆没有明确的疲劳标注。SEED-VIG数据集算是少数能直接拿来当基准的一个——它是在模拟驾驶场景下采集的62通道脑电数据配合眼电和眼动信号把警觉度标成了三个等级。这篇文章我直接用Python把整个流程拆成5步从环境准备到模型评估全部走一遍代码都会贴在对应的部分适合正在做车辆安全、脑机接口应用、或者疲劳监测方向的同学直接参考复现。1. SEED-VIG数据集与5步方案的整体思路1.1 SEED-VIG到底是什么SEED-VIG是由上海交通大学BCMI实验室公开的一套基于脑电的驾驶警觉度检测数据集。它不同于普通静息态脑电实验采集场景是模拟驾驶舱被试需要持续完成模拟驾驶任务同时在视野中会有特定频率的视觉刺激用来诱发稳态视觉诱发电位。整个实验过程持续约一个小时采集了62通道脑电EEG、8通道眼电EOG还同步记录了瞳孔直径等眼动数据。这个数据集最关键的地方在于疲劳标签不是主观填问卷得到的而是研究人员根据瞳孔直径的动态变化做半监督标定。瞳孔直径会随疲劳程度发生规律性变化基于这个生理指标把每个时间段划分成三种状态警觉、中度疲劳、严重疲劳。这样得到的标签比单一的主观困倦评分可靠很多而且有明确的生理依据。从实际使用的角度SEED-VIG有几点非常值钱首先是公开且标注完整数据文件、实验协议、通道信息都给出去了不需要自己再去费劲找标注其次是采样率较高能够支撑后续做时频分析和功率谱特征最后是它贴近真实驾驶场景在这个数据上调出来的算法后续迁移到舱内感知系统时参考价值会比实验室静息态数据高很多。1.2 为什么拆成5步来做很多人拿到一个脑电数据集第一反应就是直接丢进深度学习模型里跑结果往往不理想。EEG数据有个特点信噪比低、跨个体差异大、时间维度长如果不做预处理和特征工程再厉害的模型也很难直接拟合。我在这个项目里把整个流程拆成5步数据加载与观察先把数据结构搞清楚确认张量维度、通道顺序、标签对应关系。预处理与通道选择做滤波、降采样去掉明显的伪迹和无关通道。特征提取把高维时域信号转成有物理意义的频域特征或熵特征。模型训练用传统机器学习模型建立分类器先拿到一个可靠基线。评估与可视化看总体准确率、混淆矩阵和逐类别表现判断方案可行性。这个顺序本质上是把一条标准的机器学习流水线落到EEG场景里每一步都有明确产出物方便中间调试。我的建议是先把这条基线跑通再考虑替换模块——比如用深度学习替换特征提取加分类器或者加入在线滑窗机制用于实时检测。1.3 整体技术栈清单这5步主要依赖的Python库并不多但每一样都是干EEG活儿的标配库名用途安装方式mneEEG数据处理滤波、切片、PSD计算pip install mnenumpy多维数组运算特征矩阵构建pip install numpypandas标签整理结构化操作pip install pandasscipymat数据读取信号处理辅助pip install scipyscikit-learn模型训练、切分、评估pip install scikit-learnmatplotlib / seaborn波形、频谱、混淆矩阵可视化pip install matplotlib seaborn建议统一使用Anaconda或Miniconda管理Python环境Python版本选3.9或3.10都行这两个版本对mne和scikit-learn的兼容性都很稳定。后面我会详细说环境怎么搭、依赖怎么装。2. 环境准备与数据集获取要点2.1 Python开发环境怎么搭考虑到关注这个数据集的同学有很大一部分是刚接触Python或脑电处理的新手我在这部分多说几句环境搭建的细节。用Anaconda比较省心它自带conda包管理器和大量常用科学计算库避免了很多编译安装的坑。安装完Anaconda之后建议先创建一个独立的虚拟环境不要直接把包装到base环境里方便后续做版本隔离。# 创建Python 3.10环境名为seedvig conda create -n seedvig python3.10 # 激活环境 conda activate seedvig # 安装核心依赖国内网络建议加清华源 pip install mne numpy pandas scipy scikit-learn matplotlib seaborn如果pip默认源下载速度很慢可以临时指定国内镜像源。以清华源为例在pip安装时加-i参数即可。实测下来mne安装包大约几十MB使用国内源能在1到2分钟内装完不会出现卡半天的情况。pip install -i https://pypi.tuna.tsinghua.edu.cn/simple mne numpy pandas安装完成后建议用一行代码验证环境是否正常import mne print(mne.__version__) import sklearn print(sklearn.__version__)能正常输出版本号就说明环境没问题。如果到这里卡住通常只有两种可能一是conda环境激活失败还在用base环境二是pip源访问超时。前者用conda env list确认当前环境后者换源重装就行。2.2 数据集下载与目录结构SEED-VIG数据集需要从上海交通大学BCMI实验室的官网申请下载。下载后一般是压缩包解压后主要包含以下目录不同版本的文件名可能有差异但结构类似EEG_Data存放预处理后的脑电数据EOG_Data存放眼电数据eye_data存放瞳孔直径等眼动记录实验协议说明或readme文件每个受试者在上午和下午各做一次实验对应session1和session2。每个session的数据又按试次切分成了多个mat文件。下载完成后建议保持原有目录结构不变然后在代码里用相对路径或配置文件指向数据根目录。需要注意两点第一数据集体积不小全是矩阵数据解压前先看一眼压缩包大小确保磁盘空间充足第二不同时间下载的数据包内部组织可能有差异我习惯先写一段探索代码把数据结构打出来而不是凭记忆硬编码路径。2.3 mat数据读取的底层逻辑SEED-VIG的核心数据是mat格式读取方式常用scipy.io.loadmat。但EEG数据的存储维度在不同发布版本里可能不一样有的维度顺序是trial × channel × sample有的是channel × trial × sample。如果直接按固定索引去取数据很容易出现shape不匹配报错。推荐的稳妥做法是加载mat文件后先打印变量的形状和类型弄清楚内部结构再动手写处理逻辑from scipy.io import loadmat import numpy as np data_path ./SEED-VIG/EEG_Data/eeg_1_1.mat raw_mat loadmat(data_path) print(键名, [k for k in raw_mat.keys() if not k.startswith(__)]) for key in raw_mat.keys(): if not key.startswith(__): val raw_mat[key] if hasattr(val, shape): print(f{key}: shape {val.shape}, dtype {val.dtype})如果打印出来发现某个变量名类似datashape是(n_trials, n_channels, n_samples)那后面的代码就按这个顺序索引。每个用户拿到的数据在内部命名和维度排列上可能不一样所以这段探索代码是后续所有步骤的前提。千万不要跳过去直接读这一步能帮你省下大量排查时间。3. 5步实战过程详解与代码实现3.1 第一步加载数据并组织实验协议假设数据结构已经通过上一步确认维度为trial × channel × sample。下面这段代码做的事是读取一个session的全部mat文件把所有试次的脑电数据合并成一个完整的时间序列同时记录每个样本对应的标签索引。import os import numpy as np from scipy.io import loadmat data_dir ./SEED-VIG/EEG_Data session 1 subject 1 all_data_list [] all_label_parts [] # 遍历session内的所有试次文件 for file_name in sorted(os.listdir(data_dir)): if not file_name.endswith(.mat): continue file_path os.path.join(data_dir, file_name) mat loadmat(file_path) # 注意变量名以实际打印结果为准这里假设是data data_key [k for k in mat.keys() if not k.startswith(__)][0] trial_data mat[data_key] # shape: (n_trials, n_channels, n_samples) print(file_name, trial_data.shape) all_data_list.append(trial_data) # 垂直拼接所有trial得到完整信号 full_data np.concatenate(all_data_list, axis2) # 沿时间维度拼接 print(完整EEG shape:, full_data.shape) # (n_channels, total_samples)这里把多个试次在时间维度上拼接是为了后续统一做滑窗。需要注意的是每个trial之间可能有短暂的间隔或伪迹如果在意这些边界效应可以在拼接前对每个trial首尾各去掉一小段数据比如0.5秒。标签的处理逻辑同样重要。SEED-VIG的每个trial内部也按时间段划分了疲劳等级通常labels会单独存在另一个mat变量或文件中。把标签数组读进来后按同样的方式沿时间维度拼接得到与完整EEG信号逐样本对应的标签序列。3.2 第二步EEG预处理与通道选择预处理是整个流程里最影响最终准确率的环节。我这里的策略分四步通道选择、降采样、滤波、建mne对象。如果数据量特别大或打算做实时检测降采样几乎是必须的能够大幅减少特征计算量。SEED-VIG有62通道但不是所有通道都对疲劳检测同等重要。从神经生理角度看前额区和中央区的theta频段活动与困倦高度相关枕区的alpha活动也很有参考价值。我做实验时习惯从前额区域选若干通道再结合中央区和枕区几个代表性通道组合成一个较小的通道子集既能降低特征维度又能保留主要的疲劳相关信号。下面是用mne完成核心预处理的代码import mne # 通道名列表需要与数据集实际顺序对应这里写示例 ch_names [ Fp1, Fp2, F3, Fz, F4, C3, Cz, C4, P3, Pz, P4, O1, Oz, O2 ] sfreq 1000 # 根据数据集实际采样率设置 # 构造mne Raw对象data的shape应为 (n_channels, n_samples) info mne.create_info(ch_namesch_names, sfreqsfreq, ch_typeseeg) raw mne.io.RawArray(full_data[: len(ch_names)], info, verboseFalse) # 带通滤波0.5~40Hz去除基线漂移和高频噪声 raw.filter(0.5, 40, fir_designfirwin, verboseFalse) # 降采样到128Hz减少数据量和计算压力 raw.resample(128) # 提取数据 processed_data raw.get_data() processed_sfreq 128滤波频率选择0.5到40Hz的考虑是0.5Hz以下主要是基线漂移和皮肤电位伪迹40Hz以上主要是肌电噪声而疲劳检测关心的delta、theta、alpha、beta频段全部落在0.5到30Hz这个范围里所以这个截止频率设定既不丢信息又能有效降噪。如果某个通道有明显的电极松动或持续噪声直接用mne的raw.plot()画一画波形就能看出来把问题通道剔除或插值即可。处理量大时还可以尝试ICA去伪迹但这会增加计算时间第一次跑通基线时可以先不做。3.3 第三步滑窗分割与特征提取预处理完成后数据仍然是高维时域信号直接丢给分类器效果不会太好。业界常规做法是做滑窗分段对每个窗口提取频域特征。我这里用的是固定长度10秒、重叠5秒的滑动窗口换算到128Hz采样率就是每窗1280个采样点滑动步长640个采样点。窗口长度的选择要有取舍窗口太短频率分辨率不足特征不稳定窗口太长又无法捕捉短时的疲劳状态变化。10秒窗口算是一个兼顾两者的折中在很多疲劳检测论文里都有使用。重叠5秒的作用是增加样本数量让训练集更充足。对于每个窗口我提取两类特征各频带功率谱密度PSD和差分熵DASM。PSD能反映脑电在不同频段的能量分布差分熵则衡量了大脑左右半球对应脑区的活动差异这两类特征在警觉度评估里都有较强的生理基础。from scipy.signal import welch import numpy as np def compute_psd_features(window_data, sfreq): window_data: (n_channels, n_window_samples) 返回每个通道的频带功率特征 bands { delta: [0.5, 4], theta: [4, 8], alpha: [8, 13], beta: [13, 30], } features [] for ch_data in window_data: freqs, psd welch(ch_data, fssfreq, nperseg256) ch_feat [] for band_name, (fmin, fmax) in bands.items(): mask (freqs fmin) (freqs fmax) band_power np.trapezoid(psd[mask], freqs[mask]) ch_feat.append(band_power) features.append(ch_feat) return np.array(features).flatten() def compute_dasm_features(window_data): 差分熵特征选取成对左右脑区通道计算熵差。 需要根据实际选用的通道对来扩展这里仅演示思路。 dasm_list [] for i in range(0, window_data.shape[0] - 1, 2): left window_data[i] right window_data[i 1] diff left - right # 简化熵特征用标准差近似表示信号复杂度差异 dasm_list.append(np.std(diff)) return np.array(dasm_list)把滑窗和特征计算串联起来最终构造出样本矩阵X和标签向量ywindow_len 10 * processed_sfreq step_len 5 * processed_sfreq X_list, y_list [], [] for start in range(0, processed_data.shape[1] - window_len, step_len): end start window_len win processed_data[:, start:end] # 若窗口内标签一致才保留避免窗口跨越疲劳等级边界 win_labels label_data[start:end] if np.all(win_labels win_labels[0]): psd_feat compute_psd_features(win, processed_sfreq) dasm_feat compute_dasm_features(win) # 组合两类特征 feature_vec np.concatenate([psd_feat, dasm_feat]) X_list.append(feature_vec) y_list.append(win_labels[0]) X np.array(X_list) y np.array(y_list) print(特征矩阵shape:, X.shape, 标签数量:, len(y))注意代码里对窗口标签的一致性做了检查这个细节非常重要。如果一个窗口正好跨越了疲劳等级切换的时刻那么窗口内既有警觉段又有疲劳段用这个窗口训练模型相当于给了一个相互矛盾的学习目标会明显干扰分类器收敛。3.4 第四步训练分类模型特征准备好之后训练模型这部分就非常标准了。考虑到SEED-VIG每个受试者的数据都存在个体差异第一版基线我建议先做个体内划分——即同一个受试者的数据切出训练集和测试集保证特征分布相对一致。关于模型选择我推荐从随机森林和线性支持向量机入手。相比复杂的深度学习模型这两个模型训练快、可解释性强、调参成本低而且在高维特征上都有不错的鲁棒性。等基线跑通之后再换梯度提升树或一维卷积网络也不迟。from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline # 保证切分时各类别比例一致 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) # 先用随机森林跑一遍 rf_model RandomForestClassifier( n_estimators200, max_depth12, min_samples_leaf3, random_state42, n_jobs-1, ) rf_model.fit(X_train, y_train) train_acc rf_model.score(X_train, y_train) test_acc rf_model.score(X_test, y_test) print(fRandomForest 训练集准确率: {train_acc:.3f}) print(fRandomForest 测试集准确率: {test_acc:.3f})这里有个很容易踩的坑不做标准化就使用SVM等对尺度敏感的模型特征间的量纲差异会导致训练过程严重偏向某个频带。所以如果用SVM建议套一层StandardScalersvm_model make_pipeline( StandardScaler(), SVC(kernelrbf, C10, gammascale, random_state42) ) svm_model.fit(X_train, y_train) svm_test_acc svm_model.score(X_test, y_test) print(fSVM 测试集准确率: {svm_test_acc:.3f})标准化是在训练集上fit后在测试集上transform而不是在整个数据集上统一fit否则会造成数据泄漏。sklearn的pipeline会自动处理好这一步这也是我习惯用make_pipeline的原因。3.5 第五步评估与可视化准确率只是评估的一部分对于疲劳检测这种多分类问题更重要的是看混淆矩阵和每类别的精确率、召回率。疲劳检测在实际应用中如果模型把严重疲劳误判成警觉后果远比把警觉误判成疲劳严重得多所以逐类别的分析必不可少。from sklearn.metrics import confusion_matrix, classification_report import matplotlib.pyplot as plt import seaborn as sns y_pred rf_model.predict(X_test) print(classification_report(y_test, y_pred, target_names[警觉, 中度疲劳, 严重疲劳])) cm confusion_matrix(y_test, y_pred) plt.figure(figsize(6, 5)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabels[警觉, 中度疲劳, 严重疲劳], yticklabels[警觉, 中度疲劳, 严重疲劳]) plt.xlabel(预测标签) plt.ylabel(真实标签) plt.title(Confusion Matrix - RandomForest) plt.tight_layout() plt.savefig(rf_confusion_matrix.png, dpi150) plt.show()如果发现某个类别召回率明显偏低比如严重疲劳总是被误分到中度疲劳可以考虑从几个方向调整增加该类别在训练集中的权重、收集更多严重疲劳样本、或者把严重疲劳和中度疲劳合并成二分类问题疲劳与不疲劳这在工程落地上往往更实用。此外还可以把特征重要性打印出来看看哪些通道和频带贡献最大。随机森林里直接调用feature_importances_就能拿到import numpy as np imp rf_model.feature_importances_ feat_names [] for ch in ch_names: for band in [delta, theta, alpha, beta]: feat_names.append(f{ch}_{band}) for i in np.argsort(imp)[-10:][::-1]: print(f{feat_names[i]:12s} {imp[i]:.4f})这个输出的参考价值在于如果靠前的特征集中在额叶theta频带和枕叶alpha频带说明实验结论与神经科学的基本认知一致模型的可靠性就比较高如果特征重要性分布完全随机甚至和电极位置毫无关系就要回头检查预处理是不是出问题了。4. 常见问题与排查技巧实录4.1 环境与依赖安装问题问题1mne安装后import报错No module named mne。通常原因就是环境混了。用Anaconda时如果在终端里激活了seedvig环境但Jupyter Notebook还是用的base环境的内核就会出现这种情况。解决办法是在seedvig环境下安装ipykernel然后为当前环境注册Jupyter内核conda activate seedvig pip install ipykernel python -m ipykernel install --user --name seedvig --display-name Python (seedvig)重新打开Notebook后在右上角切换内核到Python (seedvig)即可。问题2pip安装速度很慢或者超时。解决方法是换国内镜像源。除了临时加-i参数也可以直接设置全局pip源。在用户目录下创建或编辑pip.iniWindows或pip.confLinux/macOS写入清华源地址这样后续安装包都默认走镜像。[global] index-url https://pypi.tuna.tsinghua.edu.cn/simple [install] trusted-host pypi.tuna.tsinghua.edu.cn4.2 数据读取与维度不匹配问题问题3loadmat读取报错ValueError: Unknown mat file type。这个报错通常是mat文件版本太新scipy的loadmat无法解析。解决办法是先将mat文件用MATLAB重新保存为版本7.3之前的格式参考命令save(new_file.mat, data, -v7)。如果手头没有MATLAB可以改用h5py库读取7.3格式的mat文件但读出来的数据结构会变成字典套数组的形式需要额外处理。问题4数据shape理解错误拼接时维度对不上。这是新手最容易卡住的地方。建议在拼接之前花几分钟把每个mat文件的变量名和shape都打印出来画一张手动标注的草图搞清楚每个维度到底代表什么。比如看到(16, 62, 3000)大概率是16个trial、62个通道、每个trial 3000个采样点。把它记下来再写代码比反复试错高效得多。4.3 特征计算与模型训练问题问题5特征矩阵出现NaN或inf。这个问题的根源一般是原始信号里出现了异常值或滤波后数值爆炸。排查思路是先检查原始数据是否有问题通道再用np.isnan(X).sum()和np.isinf(X).sum()定位异常的样本。处理方法可以是在滑窗时跳过包含异常值的窗口或者对该窗口特征做均值填充。治本的办法还是在预处理阶段把明显坏掉的通道去掉。问题6训练集准确率很高测试集准确率却低得离谱。这是典型的过拟合信号。EEG数据的个体差异很大如果把某个受试者的大部分数据都用来训练模型很容易记住该受试者的独特脑电模式而不是通用的疲劳特征。缓解方法包括降低模型复杂度减少树深度或限制SVM的C值、增加正则化参数、使用更多样本做交叉验证。如果数据允许理想的方案是使用跨受试者评估即用一部分受试者的全部数据训练在另一部分受试者的数据上测试这样得到的指标才更接近真实场景的泛化能力。4.4 效果提升与工程化问题问题7分类准确率一直徘徊在60%左右怎么提升我实测下来影响准确率的因素按重要性排列是特征有效性、窗口长度、模型选择、通道选择。优先尝试下面三件事把窗口长度从10秒增加到20秒或30秒频域特征会更稳定。增加差分熵、样本熵、近似熵等非线性特征有些疲劳状态在功率谱上区分度不高但在熵特征上差异明显。使用跨试次交叉验证或把多个受试者的数据合并训练让模型见过更多个体差异。问题8模型离线跑通了但想实时检测怎么办实时检测的关键是滑窗不能重叠太严重且特征计算速度要够快。可以把计算好的Welch参数提前缓存或者用滑动平均的方式增量计算功率谱。另外实时场景下模型推理延迟要控制在几十毫秒内随机森林和SVM都能满足深度学习模型则建议先做量化或剪枝再部署。5. 实测心得与后续扩展方向我实际把SEED-VIG这套流程跑下来最大的感受是传统特征加机器学习仍然是EEG分类最稳妥的起点。在单个受试者数据上随机森林和SVM都能稳定拿到80%以上的分类准确率这个效果已经足够作为疲劳检测的可行性基线。真正费时间的往往不是模型训练而是数据预处理和特征调试。那些看起来不起眼的细节比如窗口标签一致性检查、标准化是否泄漏、通道选择是否合理每一个都可能让最终结果产生5到10个点的波动。后面想继续深入的话有几个方向值得探索一是把SEED-VIG和SEED情绪数据集做迁移学习利用情绪特征辅助疲劳识别二是引入图神经网络建模EEG通道之间的空间关系把62个电极的空间拓扑信息用起来三是做在线自适应学习让模型在驾驶过程中根据新数据持续更新应对个体疲劳模式的漂移。如果各位手里的设备支持把眼动信号和脑电做多模态融合效果通常会比单模态再高一个台阶。希望这篇实战记录能帮你在疲劳检测这条路上少走一些弯路。