尧图精选

SEED-VIG脑电数据集实战:Python实现驾驶员疲劳检测全流程

🕒 发布时间:2026/9/18 5:51:12 📁 来源:尧图网络
如果把公开的脑电数据集排个热度榜SEED-VIG在疲劳检测这个细分方向基本属于绕不开的基准选择。它的全称是SJTU Emotion EEG Dataset for Vigilance Estimation出自上海交大BCMI实验室受试者戴着脑电帽在驾驶模拟器里开车全程记录EEG和EOG信号。我去年用Python把这一整套流程完整跑通时前前后后踩了七八个坑很多问题不是模型结构造成的而是对数据本身不够了解。这篇文章不打算讲高深理论只把“用SEED-VIG训练一个驾驶员疲劳检测模型”这件事拆成5个可落地的步骤附上能直接运行的Python代码适合正准备入门脑电疲劳检测、或者想拿这个数据集做论文基线实验的同学。1. SEED-VIG到底是什么为什么疲劳检测绕不开这个脑电数据集1.1 数据集的来头与设计逻辑我一开始接触到SEED-VIG时第一反应是“情绪脑电数据集SEED不是很有名吗这个VIG又是怎么回事”。其实SEED-VIG就是SEED系列在警觉度方向上的延伸。VIG取的是Vigilance警觉度放到驾驶场景里就是你有多清醒、多容易犯困。这个数据集的核心实验场景是模拟驾驶。受试者坐在驾驶模拟器里需要持续开车一段时间采集过程中EEG和EOG信号被完整记录。疲劳检测本质上是个时序状态估计问题你不可能每秒钟都靠问卷问受试者“你现在困不困”所以必须找客观生理信号。EEG的优势在于时间分辨率高大脑状态变化会直接反映在脑电节律上EOG则记录了眨眼和眼动而眼皮打架本身又是最直接的犯困信号。这两个信号配合起来几乎是为了“疲劳”这件事量身定做的。很多刚接触这个数据集的人会问既然要做驾驶员疲劳检测为什么不用摄像头拍人脸用表情、眼睛闭合率来做不是更直观吗这个问题在工程上确实成立但SEED-VIG作为学术基准核心价值在于提供了一个“干净且可量化”的生理信号环境。摄像头方案受光照、遮挡、姿态影响很大而脑电虽然戴上麻烦却能捕捉到外部行为还没暴露出来的神经状态变化。很多论文做的是纯EEG或EEGEOG的疲劳检测SEED-VIG就是他们用来横向对比的公共基准。1.2 下载之后你会拿到哪些文件SEED-VIG下载下来后不是一张大表而是按受试者组织的多个mat文件。官方说大概有20名左右受试者我实际用到的是其中数据质量过关的部分。每一名受试者的记录包括EEG和EOG信号我记得EEG通道数在60个左右另外还有2个EOG通道采样率是1000Hz。这里要强调一下不同渠道重新打包过的文件命名可能不一样所以下载之后第一步不是跑模型而是先看清楚文件里到底有什么。我拿到的文件解压后大概是这样的结构seed_vig/ ├── 1_eeg.mat ├── 1_eog.mat ├── 1_label.mat ├── 2_eeg.mat ├── ...有的打包版本会在文件名里写subject编号有的会用session编号但基本逃不出eeg、eog、label这三类。加载之前我习惯先打印一下mat文件里的keys确认变量名到底是什么避免直接写死变量名然后发现key不存在。import scipy.io as sio mat sio.loadmat(1_eeg.mat) print([k for k in mat.keys() if not k.startswith(__)])这段代码会输出mat文件里实际存的变量名。如果看到的是eeg那直接用mat[eeg]就行如果看到的是eeg_1这种带编号的名字就对应改一下。这种检查习惯看起来很基础但能帮你省掉后面一大半报错排查时间。1.3 标签是怎么来的又该怎么理解SEED-VIG不是逐秒标注“此刻疲劳度打几分”而是通过客观指标生成状态标签。最核心的指标是PERCLOS也就是单位时间内眼睛闭合时间所占的比例。PERCLOS在疲劳研究领域是非常经典的指标简单理解就是眼睛睁开的程度和闭合的累积时间。除了PERCLOS之外数据集在标定过程中还结合了主观疲劳量表我记得有KSS这一类评分方法参与其中最终把数据分成清醒、疲劳、困倦三类状态。这里的“三类状态”不是一个精细到每秒钟的连续打分而更像是一个相对粗略的等级划分。清醒段、疲劳段、困倦段在整条记录里的占比并不均匀越到驾驶任务后期困倦段出现的比例越高。这个不均匀会直接导致类别不平衡问题后面训练模型时会专门处理。标签和信号的时间对应关系不同打包版本也可能不一样。有的是逐样本标签也就是每一个采样点都有一个状态值有的则是按时间段给出标签一段对应一个状态。加载后我建议先看label的长度和信号的总采样点数是不是相等再决定对齐方式。这个细节我会在第2节和第4节里用代码处理。2. 数据预处理先把EEG从“原始波形”变成“干净片段”2.1 加载mat文件时最容易翻车的两个地方SEED-VIG的mat文件版本是个很实际的问题。我用scipy.io.loadmat加载某些受试者的数据时报过这样一个错误NotImplementedError: Please use HDF reader for matlab v7.3 files。原因是matlab保存文件时用了v7.3格式这种格式底层是HDF5scipy默认不支持得换h5py来读。如果你遇到这个问题加载方式要改成这样import h5py import numpy as np with h5py.File(1_eeg.mat, r) as f: print(list(f.keys())) eeg f[eeg][:] # 读取出来可能是 samples x channels eeg np.array(eeg, dtypenp.float32)用h5py读出来的维度往往和loadmat读出来是转置关系。因为loadmat读出来的是channels x samples而h5py读出来经常是samples x channels。所以如果后面代码里发现维度对不上记得考虑做一次.T转置。这个坑我印象很深因为当时加载成功后的第一件事就是对着shape发懵。加载后确认shape也是一件值得认真做的事。假设通道数是60采样率1000Hz持续大概30分钟那总采样点数是60 * 1000 * 30 * 60 108000000个点如果存成float64光是这一个受试者的原始数据就有将近900MB。SEED-VIG完整数据不止一个受试者全部加载进内存会非常吃紧。所以我在预处理阶段会立刻做两件事转成float32节省一半内存然后马上降采样。2.2 降采样与带通滤波1000Hz的原始数据比你想的冗余先说结论再做解释脑电信号的有效频带基本集中在40Hz以下采样率1000Hz对疲劳检测这个任务来说属于严重过量。我直接把信号降采样到200Hz这样每个采样点的计算量降为原来的五分之一而信息几乎没有损失。降采样不能简单地每隔5个点抽一个。直接抽点会引入高频混叠一个标准的做法是先做抗混叠滤波再抽取。scipy.signal里提供了现成的decimate函数它在抽取之前会自动做低通滤波。下面是我在实验中验证过的一种处理方式from scipy.signal import decimate def downsample(data, fs_old, fs_new): data: channels x samples 返回降采样后的信号和新的采样率 q int(fs_old / fs_new) data_down decimate(data, q, axis1, ftypeiir) return data_down这里ftypeiir会让计算速度更快缺点是相比FIR滤波器会稍微有一点点相位变化但对后续按段提取特征的影响可以忽略。如果你特别担心相位问题可以把ftypefir代价是慢一些。降采样之后紧接着做带通滤波。脑电里真正有用的成分在0.5Hz到40Hz左右低于0.5Hz的部分主要是基线漂移高于40Hz的部分基本是肌电噪声和工频干扰。我用的是一个四阶巴特沃斯带通滤波器然后用filtfilt做零相位滤波from scipy.signal import butter, filtfilt def bandpass_filter(data, fs200, low0.5, high40.0, order4): b, a butter(order, [low, high], btypeband, fsfs) filtered filtfilt(b, a, data, axis1) return filtered注意axis1因为此时数据已经是channels x samples的结构。filtfilt会正向和反向各做一次滤波这样得到的信号没有相位偏移做波形对齐或后续分段时更安全。2.3 分段与伪迹剔除别让一次眨眼毁掉整个模型EEG是不能直接整段扔进模型的尤其在做分类任务时一般会把连续信号切成长度固定的窗口。窗口长度选多少是个权衡。窗口太短频率分辨率不够功率谱估计不稳定窗口太长一个窗口里可能混合了不同疲劳状态标签不纯。我实测下来1秒的窗口比较合适在200Hz采样率下也就是每个窗口200个采样点。如果追求更多的训练样本可以加一点重叠比如相邻窗口重叠0.5秒。这里先做无重叠版本逻辑最简单。分段的核心就一句话把整段信号切成若干个等长的片段同时把每个片段对应的标签对齐好。def segment_data(data, win_len, step): data: channels x samples win_len、step 都以采样点数为单位 返回: n_segments x channels x win_len n_samples data.shape[1] starts list(range(0, n_samples - win_len 1, step)) segments np.stack([data[:, s:s win_len] for s in starts], axis0) return segments, starts伪迹剔除这一步我一开始忽略了结果模型训练出来的准确率虚高但一换受试者就崩。原因在于模拟驾驶过程中受试者会不自觉地眨眼、吞咽、轻微转头这些动作在脑电上会产生幅度非常大的伪迹。模型看到这些异常幅度很容易记住它们而不是真正学到大脑疲劳状态。简单且有效的办法是计算每个窗口的幅度或标准差把明显异常的窗口去掉。我习惯用标准差法def remove_artifacts(segments, threshold_sigma3): segments: n_segments x channels x win_len 按每个窗口跨通道平均标准差剔除数值偏离整体均值过大的窗口 seg_std segments.std(axis-1).mean(axis1) mean_std seg_std.mean() std_std seg_std.std() keep seg_std (mean_std threshold_sigma * std_std) return segments[keep], keep这个方法不完美但能去掉绝大多数夸张的伪迹比完全不做要强得多。如果想要更精细可以做ICA人工筛眼电伪迹但对入门项目来说先把这条阈值路走通已经能拿到像样的基线效果了。3. 特征工程差分熵为什么是SEED-VIG的默认选项3.1 频带划分EEG特征的第一步是“分波段”EEG原始信号不是直接喂给普通分类器就能出好效果的因为单点电压值本身携带的信息量有限脑电信号的节律性才是关键。常见的做法是按频率把信号分解成几个生理意义的频带再在每个频带上统计能量、熵之类的指标。疲劳状态下脑电最典型的变化是慢波活动增加快波活动减弱。也就是说delta和theta频带的能量相对上升alpha和beta频带的相对比例变化这个规律在很多疲劳研究中被反复验证。SEED系列论文里常用差分熵作为特征它的本质是在每个频带上估计功率谱密度的对数既反映了能量大小又比单纯功率更稳定。频带划分我直接沿用实验中最常见的五波段频带名称频率范围(Hz)生理意义Delta0.5-4深度睡眠、疲劳后期Theta4-8困倦、注意力下降Alpha8-13放松、闭眼状态Beta13-30警觉、思维活跃Gamma30-40高级认知加工对这些频带分别提取特征相当于把原始信号从一个高维度但低信息密度的波形压缩成一组有明确生理含义的指标。这个压缩过程比直接丢原始波形给模型要高效得多尤其当你的训练数据量还没有大到能撑起端到端深度学习时。3.2 用Python把每个窗口转成特征向量差分熵的一种实用近似是在某个频带内信号的功率谱密度越集中熵越小越分散熵越大。工程实现上可以用Welch方法估计功率谱密度然后把目标频带内的功率取出来求平均再取对数。这个对数功率就是一个可用的DE特征近似值。下面的函数对一个窗口的所有通道逐一提取五个频带的特征from scipy.signal import welch BANDS { delta: (0.5, 4), theta: (4, 8), alpha: (8, 13), beta: (13, 30), gamma: (30, 40), } def extract_de_features(seg, fs200): seg: channels x win_len 返回一个一维特征向量长度 通道数 * 5 f, psd welch(seg, fsfs, npersegseg.shape[-1], axis1) features [] for band_name, (lo, hi) in BANDS.items(): idx (f lo) (f hi) band_power psd[:, idx].mean(axis1) # 防止取log时遇到0或极端小值 features.append(np.log(band_power 1e-10)) return np.concatenate(features)这段代码里有个细节值得注意nperseg我直接用了窗口长度。如果窗口是1秒采样率200Hz那么nperseg200频率分辨率是1Hz能比较干净地区分delta、theta、alpha这些相邻频带。如果窗口长度更长频率分辨率更高特征会更细腻但标签混合的风险也会增加。对一个受试者的所有窗口批量提取特征时可以直接循环def build_feature_matrix(segments, fs200): features [] for seg in segments: features.append(extract_de_features(seg, fs)) return np.array(features)最终的特征矩阵维度是样本数 x (通道数 * 5)。假设通道数62那每个窗口就是310维特征。这个维度对常见分类器来说非常友好训练速度快还不太容易过拟合。3.3 要不要把EOG也加进来SEED-VIG同时提供了EOG信号这是我在实验里得到的一个重要增量来源。EOG记录的是眼电也就是眼球运动和眨眼产生的电位变化。疲劳状态下眨眼频率会改变闭眼时间会增加这正好是PERCLOS指标的信号级体现。我建议不要只盯着EEG可以把EOG也走一遍同样的预处理和特征提取流程然后把EEG和EOG特征拼接起来。注意EEG通道和EOG通道在数据文件里可能是分开存放的需要分别加载分别提取特征最后做横向拼接。拼完之后特征维度会从310变成(622)*5320增加幅度不大但往往能带来1-3个百分点左右的准确率提升。原因也好理解EEG特征反映的是大脑内部状态EOG特征反映的是外部可见的疲劳行为两者提供的信息互补。尤其是对“困倦”这个类别EOG的眨眼特征往往比EEG更直接。4. 5步搭建疲劳检测模型从切窗到训练评估的完整代码4.1 第一步数据切窗与标签对齐前面已经把预处理和特征提取的函数准备好了现在把它们串成一个完整的pipeline。第一步是把第2节的代码整合起来完成原始信号的切窗和标签对齐。我之前说过SEED-VIG的标签可能是逐样本也可能是逐段。稳妥的做法是加载label之后先判断长度自适应对齐def load_subject_features(eeg_mat_path, label_mat_path, fs_old1000, fs_new200, win_sec1): # 读取EEG eeg_mat sio.loadmat(eeg_mat_path) eeg_key [k for k in eeg_mat.keys() if not k.startswith(__)][0] eeg np.asarray(eeg_mat[eeg_key], dtypenp.float32) if eeg.shape[0] eeg.shape[1]: eeg eeg.T # 保证是 channels x samples # 降采样 滤波 eeg_down downsample(eeg, fs_old, fs_new) eeg_filt bandpass_filter(eeg_down, fsfs_new) # 切窗 win_len int(fs_new * win_sec) segments, starts segment_data(eeg_filt, win_len, stepwin_len) # 读取标签并对齐 label_mat sio.loadmat(label_mat_path) label_key [k for k in label_mat.keys() if not k.startswith(__)][0] labels np.asarray(label_mat[label_key]).flatten().astype(int) if len(labels) eeg.shape[1]: # 逐样本标签取每个窗口最后一个采样点的标签 seg_labels labels[starts win_len - 1] elif len(labels) len(starts): # 逐段标签直接一一对应 seg_labels labels else: raise ValueError(f标签长度{len(labels)}和信号长度{eeg.shape[1]}、窗口数{len(starts)}都不匹配) # 伪迹剔除 clean_segments, keep remove_artifacts(segments) clean_labels seg_labels[keep] return clean_segments, clean_labels这个函数一次处理一个受试者。加载多个受试者时循环调用该函数把结果分别存起来即可。4.2 第二步特征提取与数据标准化对每一个受试者的干净窗口调用build_feature_matrix提取DE特征然后和标签一起保存。我习惯把所有受试者的特征汇总成两个numpy数组一个X一个y这样后续训练模型就统一了。但这里有一个非常关键的细节数据标准化。DE特征的数值范围在不同受试者之间是有差异的直接扔进模型会让模型更关注幅度大的特征。常见做法是z-score标准化也就是减去均值除以标准差。注意标准化参数只能从训练集计算不能用全量数据计算否则测试集信息会泄漏到训练过程里。这里为了先跑通流程我直接用全量标准化但在第5节我会专门解释为什么更严谨的做法是只用训练集统计量。from sklearn.preprocessing import StandardScaler def prepare_dataset(features_list, labels_list): X np.vstack(features_list) y np.concatenate(labels_list) scaler StandardScaler() X_norm scaler.fit_transform(X) return X_norm, y, scaler这里还需要检查一件事每个类别是否都存在。SEED-VIG里大概率是三类都有但如果某个受试者困倦段特别少剔除伪迹后可能只剩两三段这种情况比较极端但代码里要留意不能让它影响整个数组的形状。4.3 第三步搭建分类模型本文给的模型是一个三层MLP输入维度就是DE特征维度输出是3类。选MLP的原因有两个一是特征工程已经把时序信息压缩成了静态向量不需要再上LSTM二是在公开基准上DE特征加简单分类器已经能取得不错的成绩模型结构不是瓶颈。import torch import torch.nn as nn class FatigueMLP(nn.Module): def __init__(self, in_dim, num_classes3, hidden_dims(256, 128, 64), dropout0.3): super().__init__() layers [] prev in_dim for h in hidden_dims: layers.append(nn.Linear(prev, h)) layers.append(nn.ReLU()) layers.append(nn.Dropout(dropout)) prev h layers.append(nn.Linear(prev, num_classes)) self.net nn.Sequential(*layers) def forward(self, x): return self.net(x)隐藏层我用了256、128、64这样逐层递减的结构配合dropout防止过拟合。如果样本量很大或者你发现验证集一直在震荡可以把dropout调到0.4甚至0.5。如果训练集准确率不够高再适当减少dropout。4.4 第四步训练与验证训练之前先划分数据。最简单的方式是随机划分比如80%训练、20%测试。但是请记住这个划分方式对SEED-VIG来说是偏乐观的因为它会让同一个人的相邻窗口同时出现在训练集和测试集这一点我放到第5节说。先看主流程from sklearn.model_selection import train_test_split from torch.utils.data import DataLoader, TensorDataset def train_model(X, y, epochs50, batch_size256, lr1e-3): X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) # 这里用训练集的均值和标准差对训练/测试集做标准化 scaler StandardScaler() X_train scaler.fit_transform(X_train) X_test scaler.transform(X_test) model FatigueMLP(in_dimX.shape[1]) device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) train_dataset TensorDataset( torch.tensor(X_train, dtypetorch.float32), torch.tensor(y_train, dtypetorch.long) ) train_loader DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue) optimizer torch.optim.Adam(model.parameters(), lrlr) criterion nn.CrossEntropyLoss() for epoch in range(epochs): model.train() total_loss 0.0 for xb, yb in train_loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() out model(xb) loss criterion(out, yb) loss.backward() optimizer.step() total_loss loss.item() * xb.size(0) if (epoch 1) % 10 0: print(fEpoch {epoch1}/{epochs} | Avg Loss: {total_loss / len(train_dataset):.4f}) return model, scaler, (X_test, y_test)注意这里的标准化逻辑和前面prepare_dataset里略有不同。如果你已经在全局做过一次标准化这里就不应该再fit一次否则会重复标准化。我建议以train_model里的局部标准化为准因为它更严谨也更安全。4.5 第五步评估与混淆矩阵训练结束后用测试集做评估。除了准确率我建议一定要看F1-score和混淆矩阵。准确率在类别不平衡时会骗人比如困倦样本占比很少模型全部预测成清醒准确率可能也有70%以上但F1会非常难看。from sklearn.metrics import accuracy_score, f1_score, confusion_matrix import matplotlib.pyplot as plt def evaluate_model(model, X_test, y_test): device torch.device(cuda if torch.cuda.is_available() else cpu) model.eval() with torch.no_grad(): logits model(torch.tensor(X_test, dtypetorch.float32).to(device)) y_pred logits.argmax(dim1).cpu().numpy() acc accuracy_score(y_test, y_pred) f1 f1_score(y_test, y_pred, averageweighted) cm confusion_matrix(y_test, y_pred) print(fTest Accuracy: {acc:.4f}) print(fWeighted F1: {f1:.4f}) plt.figure(figsize(6, 5)) plt.imshow(cm, cmapBlues) plt.colorbar() plt.xlabel(Predicted) plt.ylabel(True) plt.xticks([0, 1, 2], [Alert, Fatigue, Drowsy]) plt.yticks([0, 1, 2], [Alert, Fatigue, Drowsy]) for i in range(3): for j in range(3): plt.text(j, i, cm[i, j], hacenter, vacenter, colorblack) plt.show() return acc, f1, cm这里我把标签名称直接映射成Alert、Fatigue、Drowsy方便看图。如果你下载的数据集标签范围是1到3而不是0到2记得先减1。到这一步你已经有了一个完整的可运行流程。用DE特征加MLP在随机划分的测试集上跑出80%以上的准确率是不难的甚至可能到88%到92%。但这个数字仅供参考因为随机划分本身就带着数据泄漏的嫌疑下一节专门说这个。5. 实测中的坑与调优方向从“能跑”到“能用”5.1 数据泄漏问题别再随机划分了随机划分训练集和测试集在大部分机器学习任务里是默认操作但在SEED-VIG这种连续时序数据上它会导致评估结果虚高。原因在于来自同一个受试者的相邻窗口之间高度相关。受试者的脑电特征在几十秒内不会突然变化前一秒的窗口和后一秒的窗口几乎可以看作是同一样本的微小扰动。随机划分会把非常相似的样本同时分配给训练集和测试集模型等于记住了训练集里的模式然后在几乎重复的测试集上做表演准确率自然好看但实际部署到新受试者身上就会打回原形。更严格的做法是按受试者划分。比如20名受试者取16名做训练4名做测试让测试集里的人完全不被模型见过。这个评估协议叫subject-independent也就是跨受试者评估它才是衡量疲劳检测模型真实泛化能力的有效方式。# 伪代码思路 all_subjects list(range(1, 21)) train_subjects all_subjects[:16] test_subjects all_subjects[16:] X_train_parts, y_train_parts [], [] for sid in train_subjects: feats, labels np.load(fsubject_{sid}_feats.npy), np.load(fsubject_{sid}_labels.npy) X_train_parts.append(feats) y_train_parts.append(labels) X_test_parts, y_test_parts [], [] for sid in test_subjects: feats, labels np.load(fsubject_{sid}_feats.npy), np.load(fsubject_{sid}_labels.npy) X_test_parts.append(feats) y_test_parts.append(labels) X_train np.vstack(X_train_parts) y_train np.concatenate(y_train_parts) X_test np.vstack(X_test_parts) y_test np.concatenate(y_test_parts)改成按受试者划分之后准确率通常会有明显下降。我在实验里从随机划分的90%左右掉到跨受试者的76%到82%这是非常正常的现象。请不要因为分数下降就怀疑代码写错了跨受试者评估本来就难得多。5.2 类别不平衡困倦样本不够怎么办SEED-VIG的三类样本数量不是均等的尤其是困倦状态往往只占整段记录的一小部分。我处理数据时清洗伪迹之后清醒段通常是最多的困倦段最少这个比例可能到2:1甚至3:1。处理不平衡有三条路可以走。一是在损失函数里加类别权重。PyTorch的CrossEntropyLoss支持weight参数权重设置成与样本量成反比让模型在训练时更关注少数类别。class_counts np.bincount(y_train) class_weights 1.0 / class_counts class_weights class_weights / class_weights.sum() * len(class_weights) class_weights torch.tensor(class_weights, dtypetorch.float32).to(device) criterion nn.CrossEntropyLoss(weightclass_weights)二是过采样少数类用imbalanced-learn库里的RandomOverSampler或SMOTE对训练集做重采样。这个方法简单直接但要注意必须在划分完训练集和测试集之后再进行不能对整个数据集做否则同样会造成信息泄漏。三是修改评估指标。准确率在这种场景下不够敏感F1-score、Kappa系数、各类别的召回率才是更值得关注的指标。尤其是困倦类别的召回率对疲劳检测来说意义重大漏检一个困倦状态可能比误报一个清醒状态严重得多。5.3 进阶方案从特征工程走向端到端当你在DE特征加MLP这条线上跑出稳定的基线之后可以往两个方向进阶。方向一是用更精细的特征。差分熵只是一个维度你还可以加入Hjorth参数、样本熵、近似熵、排列熵等。这些特征能捕捉信号的非线性动态特性有论文在一些数据集上验证过它们对疲劳检测有额外贡献。特征多了之后分类器也可以从MLP换成随机森林、XGBoost或者带交叉验证的SVM小数据集上有时传统方法会比深度学习更稳。方向二是直接跳过特征工程用原始波形做端到端学习。SEED-VIG虽然是脑电数据集但如果把每1秒窗口的原始信号直接当作输入用一维卷积或EEGNet这类专门设计给脑电的模型模型可以自动学习到频谱空间特征。端到端模型的优势是不需要人工设计特征局限是需要更多数据、更长训练时间而且结果不易解释。我个人的建议是先跑通特征工程版本的基线再用它作为对比参照再考虑是否上深度学习模型。另外补充一个常见实践EOG信号可以和EEG信号一起送去训练也可以单独训练一个模型然后把输出概率融合起来。两个模型做决策级融合通常比简单拼接特征更稳一点因为EEG模型和EOG模型是用完全不同模态的输入学出来的错误模式相关性低融合之后互补性更强。5.4 把SEED-VIG扩展到你自己的实验场景最后聊一点关于数据扩展和个人经验。SEED-VIG是公开数据集里少有的、在疲劳检测方向具有较高认可度的基准。但如果你要做的是一个真实的驾驶员疲劳预警系统只用这个数据集是不够的。真实驾驶环境里有环境噪声、头部运动、接触不良导致的电极漂移这些在实验室环境里要么不存在要么被刻意剔除了。所以我的建议是用SEED-VIG做算法开发和消融实验证明你的方法在公共基准上有效然后再用你自己的小规模实验数据做迁移验证。比如在一些轻量级场景里初始模型用SEED-VIG训练之后用自己的数据做微调这样能大幅减少冷启动阶段需要的标注数据量。这个思路在脑电疲劳检测的项目里非常实用因为个性化脑电差异是真实存在的跨受试者模型只能给你一个不差但未必最优的起点。回到代码本身如果你跑通了我上面给的那套流程你已经有能力继续向前探索了。把按受试者划分的评估结果作为你的基线指标之后所有改动都拿它对比。无论是加了一种新特征还是换了一个新模型只要这个数字在涨说明你的方向基本是对的。最后还有个小技巧数据预处理阶段每处理完一个受试者就立刻把特征矩阵和标签保存成npy文件不要每次都从原始mat重新加载。原始数据体积大加载一次要等很久而特征文件小得多训练时直接加载即用。我现在跑实验都是只维护一份特征文件目录后续任何模型迭代都从这里开始效率能提升一个量级。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →