基于SEED-VIG脑电数据的驾驶疲劳检测:Python五步实战流程
2026/9/18 12:10:21 网站建设 项目流程

从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 mne
numpy多维数组运算,特征矩阵构建pip install numpy
pandas标签整理,结构化操作pip install pandas
scipymat数据读取,信号处理辅助pip install scipy
scikit-learn模型训练、切分、评估pip install scikit-learn
matplotlib / 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 python=3.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}")

如果打印出来发现某个变量名类似data,shape是(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, axis=2) # 沿时间维度拼接 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_names=ch_names, sfreq=sfreq, ch_types="eeg") raw = mne.io.RawArray(full_data[: len(ch_names)], info, verbose=False) # 带通滤波:0.5~40Hz,去除基线漂移和高频噪声 raw.filter(0.5, 40, fir_design="firwin", verbose=False) # 降采样到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, fs=sfreq, nperseg=256) 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和标签向量y:

window_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_size=0.2, stratify=y, random_state=42 ) # 先用随机森林跑一遍 rf_model = RandomForestClassifier( n_estimators=200, max_depth=12, min_samples_leaf=3, random_state=42, 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(f"RandomForest 训练集准确率: {train_acc:.3f}") print(f"RandomForest 测试集准确率: {test_acc:.3f}")

这里有个很容易踩的坑:不做标准化就使用SVM等对尺度敏感的模型,特征间的量纲差异会导致训练过程严重偏向某个频带。所以如果用SVM,建议套一层StandardScaler:

svm_model = make_pipeline( StandardScaler(), SVC(kernel="rbf", C=10, gamma="scale", random_state=42) ) svm_model.fit(X_train, y_train) svm_test_acc = svm_model.score(X_test, y_test) print(f"SVM 测试集准确率: {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, annot=True, fmt="d", cmap="Blues", xticklabels=["警觉", "中度疲劳", "严重疲劳"], yticklabels=["警觉", "中度疲劳", "严重疲劳"]) plt.xlabel("预测标签") plt.ylabel("真实标签") plt.title("Confusion Matrix - RandomForest") plt.tight_layout() plt.savefig("rf_confusion_matrix.png", dpi=150) 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 环境与依赖安装问题

问题1:mne安装后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)即可。

问题2:pip安装速度很慢或者超时。

解决方法是换国内镜像源。除了临时加-i参数,也可以直接设置全局pip源。在用户目录下创建或编辑pip.ini(Windows)或pip.conf(Linux/macOS),写入清华源地址,这样后续安装包都默认走镜像。

[global] index-url = https://pypi.tuna.tsinghua.edu.cn/simple [install] trusted-host = pypi.tuna.tsinghua.edu.cn

4.2 数据读取与维度不匹配问题

问题3:loadmat读取报错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个电极的空间拓扑信息用起来;三是做在线自适应学习,让模型在驾驶过程中根据新数据持续更新,应对个体疲劳模式的漂移。如果各位手里的设备支持,把眼动信号和脑电做多模态融合,效果通常会比单模态再高一个台阶。希望这篇实战记录能帮你在疲劳检测这条路上少走一些弯路。

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

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

立即咨询