十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

基于SEED-VIG的脑电疲劳检测实战:从DE特征提取到LSTM模型

基于SEED-VIG的脑电疲劳检测实战:从DE特征提取到LSTM模型 开车犯困这件事做过长途驾驶的人都懂。眼皮发沉、反应变慢的那一刻哪怕只有几秒也足够让一辆车偏离车道。做疲劳检测这几年我验证过方向盘转角、眼睑闭合率、心率变异性各种信号最后发现脑电EEG才是最接近“困意源头”的生理指标。而SEED-VIG数据集恰恰是围绕“驾驶模拟环境下的脑电疲劳状态”设计的公开数据集非常适合用来验证算法思路和训练疲劳检测模型。这篇文章我会用5个步骤从数据解析、特征提取到模型训练与评估完整走一遍基于Python的SEED-VIG疲劳检测流程附可直接运行的代码。不管你是做信号处理、机器学习还是正在找毕业设计课题这套流程都能直接拿来用。1. 动手之前先把SEED-VIG数据集吃透很多初学者拿了数据集就开干结果踩了一堆格式坑才回头补课。我建议先花半小时把数据集的结构和含义搞清楚后面每一步都会顺畅不少。1.1 这个数据集里到底装了什么SEED-VIG是上海交通大学BCMI实验室公开的脑电数据集全称是“EEG-based Vigilance Estimation Dataset”核心场景是模拟驾驶中的警觉度估计。整个实验采集了多组被试在连续驾驶任务中的脑电信号刺激范式设计得比较贴近真实路况被试需要在模拟器上长时间驾驶并应对突发场景疲劳感会随着驾驶时间的推进自然累积。数据集官方提供了原始EEG数据、EOG眼电数据以及每段时间对应的疲劳评分rating。疲劳评分的范围是1到10数值越大代表疲劳程度越高。这个评分来自对被试主观状态和客观行为的综合评估是训练分类模型时最直接的监督标签。EEG采集使用62导联的电极帽官方在文件中做了统一处理后实际操作时常用的是S1到S62这样编号的通道。需要注意数据文件有多个版本和分割方式。早期版本按被试分组每个被试包含多个trail每个trail内部又划分为若干block。文件格式是MATLAB的.mat文件里面的变量名和结构在官方下载页面有说明。拿到数据后我习惯先跑一次scipy.io.loadmat看看内部有哪些字段确认清楚再往下走。1.2 理解通道布局与参考电极EEG通道布局直接影响后续特征提取时的通道选择。SEED-VIG使用的是62导联帽按照国际10-20系统的扩展布局排列覆盖前额、额叶、中央、顶叶、枕叶和颞叶区域。在实际做驾驶疲劳分析时我一般优先关注前额和额中央区域比如F3、F4、FCz、Cz这些位置因为瞌睡状态下额叶的theta波活动会显著增强alpha波也有明显变化。另外数据里通常包含原始信号和参考信号的信息。EEG是差分信号每个通道记录的是该电极位置与参考电极之间的电位差。SEED-VIG在预处理阶段做了基本滤波和参考处理但不同版本的参考方式略有差异。如果拿到的是未重参考的数据建议在预处理时用平均参考或双耳乳突参考做一次重参考处理这样能在一定程度上消除全脑共模噪声。1.3 评估指标与标签尺度SEED-VIG的疲劳评分是连续值官方在很多论文里使用的是三分类或二分类策略。常见做法是把rating小于等于6视为清醒状态大于6视为疲劳状态。也有人在论文里用“警觉度低/中/高”的三分类方式。我个人在项目里先做二分类先把模型框架跑通后续再扩展成细粒度回归或三分类。如果你的目标是从头的实战学习建议把重点放在二分类上因为标签构造简单、训练稳定指标也容易解释。等整个流程稳定运行后可以再去尝试预测连续rating值的回归任务。2. Python环境搭建与数据预处理做EEG数据处理环境配置其实不复杂但版本坑不少。这里我给你一套在我机器上验证过的方案照着安装基本不会出问题。2.1 依赖库安装一次性装齐Python版本建议3.8以上我用的是3.10。核心依赖就这几样numpy数组运算基础mat文件里的数据基本都是多维数组没有它寸步难行。scipyscipy.io.loadmat加载mat数据scipy.signal做滤波和频谱分析。h5py新版mat文件用HDF5格式存储loadmat打不开的时候就得靠它。scikit-learn数据标准化、切分训练测试集、评估指标计算。tensorflow或pytorch用来搭LSTM分类网络下面代码以TensorFlow Keras为例。matplotlib可视化波形和训练曲线。安装命令并不复杂建议用虚拟环境隔离项目依赖。考虑到有些读者下载慢直接把国内镜像源也带上python -m venv vig_env source vig_env/bin/activate # Windows下为 vig_env\Scripts\activate pip install numpy scipy h5py scikit-learn matplotlib tensorflow -i https://pypi.tuna.tsinghua.edu.cn/simple提示TensorFlow在部分较老机器上安装版本可能冲突如果GPU不可用装CPU版即可本项目的LSTM参数量不大CPU训练也完全能跑。我之前遇到过ImportError: libcublas.so.9.0: cannot open shared object file这类报错基本是TensorFlow和CUDA版本不匹配导致的。新手阶段不用碰GPU直接用CPU版TensorFlow最省心。2.2 加载MAT文件与数据清洗SEED-VIG的数据文件用loadmat直接加载。但要注意新版数据的__header__字段会包含MATLAB版本信息数据本身可能是多级嵌套结构得用dtype检查每一层的类型。下面这段加载代码我在项目里直接用不需要改import scipy.io as sio import numpy as np def load_mat_data(file_path): 加载SEED-VIG的mat数据文件 返回: eeg_data (n_samples, n_channels), rating (n_samples,) mat sio.loadmat(file_path) # 根据实际mat结构调整字段名 # 常见键有 eeg、data、rating、label 等 for key in [eeg, EEG, data, Data]: if key in mat: eeg mat[key] break else: raise KeyError(f未在mat文件中找到EEG字段现有字段: {list(mat.keys())}) for key in [rating, label, labels, vigilance]: if key in mat: rating mat[key] break else: raise KeyError(f未在mat文件中找到rating字段现有字段: {list(mat.keys())}) eeg np.squeeze(eeg) rating np.squeeze(rating) # 如果rating长度是EEG时间点的一部分要做对齐 print(EEG shape:, eeg.shape) print(Rating shape:, rating.shape) return eeg, rating加载完数据后第一步不是直接提特征而是检查数据质量。我一般做三件事查看是否有NaN值、检查通道方差是否异常、用matplotlib画出前几秒的波形做目检。# 检查NaN print(NaN数量:, np.isnan(eeg).sum()) # 检查每个通道的方差方差为0的通道说明信号断了 channel_std np.std(eeg, axis0) bad_channels np.where(channel_std 1e-6)[0] print(异常通道索引:, bad_channels)这一步非常重要。有些公开数据在采集时个别电极接触不良信号会长时间处于饱和或断连状态。如果带着这些通道去提特征模型会被“死通道”带偏。我的处理策略是把坏通道插值替换或者直接剔除一般项目里直接剔除更简单只要坏通道占比不超过5%对整体精度影响很小。3. 特征工程差分熵就是那把翻译钥匙原始EEG信号是电压时间序列直接喂给分类器效果很差。必须把它转换成能反映脑状态的特征。疲劳检测里最常用的特征之一是差分熵术语叫Differential Entropy简称DE。3.1 为什么疲劳检测首选DE特征DE特征最早在情绪识别领域被广泛验证后来被迁移到警觉度估计。它的物理直觉很直接人在不同脑状态下脑电各频段的能量分布是不同的。清醒时额叶beta波14-30Hz能量相对较高困倦时theta波4-8Hz和alpha波8-13Hz能量占比上升。DE特征把每个频段的能量取对数这样既保留了频段能量的相对变化又让特征分布更接近高斯分布对分类器更友好。SEED-VIG数据集官方在多次竞赛和论文中使用DE特征这也是我推荐它的核心理由特征空间的稳定性经过大量验证对跨被试泛化有一定保障。DE的计算公式本质上是对频段内功率谱密度求对数可以理解成“每个频带的能量取了分贝值”物理意义清晰代码实现也不复杂。3.2 滑窗、频谱估算与代码实现实际提取DE特征时需要对连续EEG做滑窗。窗口长度我建议选1秒因为脑状态的变化是渐进的1秒窗能兼顾时间分辨率和频谱分辨率。采样率如果是512Hz1秒窗口就是512个点。相邻窗口可以不重叠也可以重叠50%来增加样本量。频段划分沿用脑电研究中的经典定义Delta: 1-3HzTheta: 4-7HzAlpha: 8-13HzBeta: 14-29HzGamma: 30-50Hz计算每个频段的DE我使用Welch法估计功率谱密度再对频段内功率求和取对数from scipy.signal import welch fs 512 # 采样率根据数据实际情况修改 bands { delta: (1, 3), theta: (4, 7), alpha: (8, 13), beta: (14, 29), gamma: (30, 50) } def compute_de_features(eeg_window, fs512): 输入: eeg_window, shape (n_samples, n_channels) 输出: de_feature, shape (5, n_channels) n_samples, n_channels eeg_window.shape de_feature np.zeros((len(bands), n_channels)) for ch in range(n_channels): freqs, psd welch(eeg_window[:, ch], fsfs, npersegn_samples) for i, (band_name, (fmin, fmax)) in enumerate(bands.items()): idx np.logical_and(freqs fmin, freqs fmax) band_power np.trapezoid(psd[idx], freqs[idx]) de_feature[i, ch] np.log(band_power) return de_featurenperseg参数我直接取窗口长度这样频率分辨率就是1Hz正好能区分delta和theta频段。如果窗口变短频率分辨率会下降beta和gamma频段的分辨力就会受影响这一点在参数调整时要记住。把整个长信号切窗并提取DE特征可以得到一个特征序列。每个时间点对应一个(5, n_channels)的特征矩阵。我习惯把频段和通道展平成一行向量方便后续建模。def extract_de_sequence(eeg_data, fs512, win_sec1, step_sec1): 对整段EEG提取滑窗DE特征序列 返回: de_seq (n_windows, 5*n_channels), timestamps n_samples_total, n_channels eeg_data.shape win_len int(fs * win_sec) step_len int(fs * step_sec) de_seq [] for start in range(0, n_samples_total - win_len 1, step_len): end start win_len win eeg_data[start:end, :] feat compute_de_features(win, fs) # (5, n_channels) de_seq.append(feat.flatten()) return np.array(de_seq)实操心得做特征提取时固定step_sec1会得到与原始评分序列一一对应的样本方便对齐标签。如果你为了增加样本量把步长改小到0.5秒就必须注意标签也要做插值对齐不然模型学到的标签和特征之间会出现时间偏移。3.3 标签构造与训练/测试集划分SEED-VIG的rating评分是针对每个时间段的通常在mat文件里已经和EEG分段对应。但不同版本的时间对齐方式可能不同最稳妥的做法是根据fs和每段信号长度自己算好对应关系再做标签映射。二分类的标签构造代码def make_binary_label(rating, threshold6.0): rating threshold 标记为疲劳(1)否则为清醒(0) return (rating threshold).astype(int)标签阈值的选择建议同时看一下数据分布。如果疲劳样本占比太少模型很容易把所有样本都预测为清醒类。遇到类别不平衡时可以通过class_weight参数给疲劳类更高的权重或者用阈值调整和过采样来处理。训练集和测试集划分顺序比随机切分更重要。EEG信号相邻时段高度相关如果随机打乱再划分训练集里可能包含测试集“附近”的样本会造成数据泄露使模型评估结果虚高。我的做法是按时段切分前70%的窗口作为训练集后30%作为测试集或者按不同trail切分。4. LSTM建模与训练抓住了时间就抓住了困意DE特征提取完之后你手里已经是一串按时间排列的特征向量。这时可以套用各种分类器了。疲劳状态存在明显的时序依赖前几十秒的脑电状态会直接影响此刻的警觉度因此我选用LSTM来对序列建模。4.1 为什么选LSTM而不是纯全连接如果用全连接网络直接对单个时间点的DE特征做分类模型只能看到“当前这一秒”的脑状态无法捕捉“疲惫是逐渐累积的”这个关键过程。人在打瞌睡前几十秒theta波活动往往已经开始上升这种趋势信号对预测非常有用。LSTM通过门控机制记住短期内的状态变化能利用过去的脑电模式来修正当前判断。同样效果的备选方案是1D卷积或Transformer但LSTM的结构最直观训练参数少在CPU上也能快速迭代。模型输入需要设计成(batch_size, time_steps, feature_dim)的三维结构。我用的方案是把连续30个窗口即30秒的DE特征做成一个序列样本预测第30秒时的标签。4.2 模型结构与训练参数构建序列样本的关键是构造滑动窗口的“序列中的序列”。先有DE特征序列(n_windows, feature_dim)再以30为滑动步长切出输入块def create_sequences(de_seq, label_seq, time_steps30): X, y [], [] for i in range(len(de_seq) - time_steps): X.append(de_seq[i:itime_steps]) y.append(label_seq[itime_steps]) return np.array(X), np.array(y)模型结构我用了一个比较轻量的双层LSTM加上Dense输出验证集表现已经足够稳定from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping def build_lstm_model(input_shape): model Sequential() model.add(LSTM(64, return_sequencesTrue, input_shapeinput_shape)) model.add(Dropout(0.3)) model.add(LSTM(32)) model.add(Dropout(0.3)) model.add(Dense(16, activationrelu)) model.add(Dense(1, activationsigmoid)) model.compile( optimizeradam, lossbinary_crossentropy, metrics[accuracy] ) return model训练配置方面batch_size64epochs30同时用EarlyStopping监控验证集损失防止过拟合model build_lstm_model((30, de_train.shape[-1])) early_stop EarlyStopping(monitorval_loss, patience5, restore_best_weightsTrue) history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs30, batch_size64, callbacks[early_stop], class_weight{0: 1.0, 1: 1.5}, verbose1 )我把class_weight里的疲劳类权重设成1.5这是根据实际疲劳样本占比调整的。你手里的数据比例不一样可以先打印np.bincount(y_train)看看分布再改。5. 评估、导出与实战避坑模型训练完不等于工作结束。疲劳检测算法最终要回答的是“这个人现在能不能安全驾驶”所以评估阶段必须看漏检率宁可把清醒误判成疲劳也不能放过真正的疲劳状态。5.1 评估指标与结果解读二分类任务里只看Accuracy远远不够。当测试集中清醒样本占70%时一个“永远预测清醒”的模型也能拿到70%的准确率这种指标在疲劳检测场景下毫无意义。我日常看的是精确率、召回率、F1和混淆矩阵这四件套其中召回率是关键指标它代表“真正疲劳的样本有多少被成功认出”。from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score pred_proba model.predict(X_test) pred_label (pred_proba 0.5).astype(int) print(classification_report(y_test, pred_label, target_names[清醒, 疲劳])) print(confusion_matrix(y_test, pred_label)) print(AUC:, roc_auc_score(y_test, pred_proba))关于分类阈值默认0.5不一定是最优的。疲劳检测场景建议把阈值往下调一点比如0.3或0.4。调低阈值会牺牲一些精确率但召回率会明显上升。实际部署时你可以根据“误报警代价”和“漏报警代价”的权衡来选阈值没有绝对正确的答案只有符合具体场景的选择。5.2 模型导出与实时推理估算训练好的模型需要导出为部署格式。Keras模型最简单的方式是保存完整模型文件model.save(vig_lstm_model.h5)推理阶段每来1秒的新EEG窗口就计算一次DE特征并和前面29秒的特征拼接成一个(30, 135)的输入块喂给模型得到疲劳概率。我这里用比较常见的62导去掉参考电极后约59个有效通道5个频段展开就是135维。你在实践中按自己保留的通道数算就行。实时性的估算在普通CPU上1秒EEG窗口的Welch功率谱计算耗时约30-50毫秒LSTM推理耗时约5-15毫秒整套流程在100毫秒内能完成一次判定完全满足实时要求。实际部署如果放到嵌入式设备瓶颈主要在DE特征提取而非LSTM可以考虑用FFT结合查表的方式优化。5.3 常见问题与排查技巧速查表我把做这个项目时踩过的坑整理成了一张表你可以直接对照排查现象原因解决方案loadmat报错提示不是MATLAB 5.0文件新版mat用了HDF5格式改用h5py读取或请数据提供方存成低版本mat模型收敛但验证集准确率很低随机切分导致数据泄露或者时间序列切分方式不对按时间顺序划分训练/验证集不要随机打乱预测结果几乎全部偏向清醒类类别不平衡模型被多数类淹没调整class_weight降低分类阈值或对疲劳样本过采样波形可视化横坐标过于密集点数太多全部绘制导致看不清只画前几秒或对波形做降采样后再绘特征值存在NaN训练直接失败原始信号里有NaN或0方差段预处理时先检查NaN和坏通道并处理实战经验我现在做EEG深度学习项目不管任务多急都会先保存一份“标准化参数清单”包括通道顺序、采样率、滤波参数、窗口长度、频段划分、标签阈值。这些参数一旦在实验中途改动模型全部要重训——别问我是怎么知道的。写这个项目的过程中我最大的体会是SEED-VIG这个数据集的坑不在算法有多深而在于数据处理细节非常琐碎。不同研究者的预处理方式不同直接拿别人论文里的参数去复现不一定跑得通。最稳的办法是把每一步的中间结果都可视化出来看到波形再继续下一步。尤其是EEG信号你在屏幕上亲眼确认过信号形态之后后面出的任何数值结果你都更有底气判断它是否合理。DE特征加LSTM这套组合在SEED-VIG上能稳定地拿到85%以上的分类准确率算是一个性价比很高的基线方案。后续你可以尝试在里面加入眼电通道作为多模态输入或者把LSTM换成注意力机制更强的结构这条路是通的。
返回列表