资讯动态

DEAP脑电情绪识别:PSD与DE脑图喂二维卷积的完整走法

发布时间:2026/10/2 20:05:15 来源:尧图企业网站定制
简介面向脑电信号处理与情感计算方向的研究者这份压缩包集合论文与完整源码可复现基于DEAP数据集的脑电情绪识别方案。核心思路是从脑电信号中提取微分熵和功率谱密度特征按α、β、γ、θ四个频段和0.5秒观察窗叠加生成脑图再利用多任务二维卷积神经网络同时输出效价与唤醒度预测在DEAP上准确率分别达到96.28%和96.62%。包内共19个文件、约7.7MB包含9个Python脚本、2个Jupyter Notebook、2个PDF论文以及脑图、模型结构和消融实验图片代码覆盖数据加载、建模、训练评估等模块目录清晰方便对照论文复现或修改扩展。当前已有3098人学习下载对刚接触脑电特征提取或二维卷积分类的读者来说这套资料能显著缩短前期调研和代码整理时间。1. 用脑图喂给二维卷积做情绪识别DEAP 项目的完整闭环长什么样我第一次用 DEAP 数据集做脑电情绪识别时把功率谱密度 PSD 和微分熵 DE 都提取出来了却只拼成一长条向量喂给一维卷积模型在验证集上怎么都过不了 60%。后来发现问题不在特征提取而在数据形状EEG 电极分布在头皮上32 个通道之间的邻近关系本身就是一张空间图把这张图弄丢了二维卷积的空间归纳能力就完全没有发挥出来。这篇笔记讲的就是把 PSD 和 DE 重排成脑图再交给二维卷积时的完整走法——从 DEAP 原始文件怎么切到两种特征的提取参数再到 9×9 脑图的构建和模型训练时的常见翻车点。适合正在做基于 DEAP 的脑电情绪识别毕业设计、论文复现或者想自己搭 baseline 的从业者。2. 先把 DEAP 数据切成能用的样本加载、切片与标准化复现任何脑电情绪识别项目第一步都不是写网络而是把 DEAP 原始数据变成“窗口 × 通道 × 时间长度”的三维张量。这一步做错后面所有特征和模型都在错误的地基上跑。2.1 deap数据集下载 后的第一个选择128Hz 预处理版还是 512Hz 原始版搜索 deap数据集下载你会看到官方给出两种数据形态一种是 512Hz 的原始 BDF 记录需要自己做眼电去伪迹、降采样和重参考另一种是已经处理到 128Hz、切成 63 秒片段的 Python/MATLAB 预处理版本。常见做法是直接选后者做实验因为省去大量的早期信号处理且大多数公开复现代码也依赖这个版本。预处理版本里每个受试者对应一个 .mat 文件文件名通常形如s01.mat。我用 scipy 一次性读取全部受试者import numpy as np import scipy.io as sio subject_files [fs{idx:02d}.mat for idx in range(1, 33)] data_list, label_list [], [] for f in subject_files: mat sio.loadmat(f) data_list.append(mat[data]) # (40, 40, 8064) label_list.append(mat[labels]) # (40, 4) all_data np.concatenate(data_list, axis0) # (1280, 40, 8064) all_label np.concatenate(label_list, axis0)代码逻辑不复杂loadmat返回字典data键存的是单个受试者的脑电数据形状是(试验数, 通道数, 采样点数)labels是(试验数, 4)的评分矩阵。把 32 个受试者沿试验维度拼接后得到 1280 个试验。需要说明的是40 个通道里前 32 个才是真正的脑电通道后 8 个是眼电、肌电、呼吸等外围生理信号在用脑图做二维卷积时通常只取前 32 个。2.2 基线校准与滑动窗口从 8064 个采样点切出 1 秒样本DEAP 的 8064 个采样点对应 63 秒其中前 3 秒是静息基线后 60 秒才是受试者看情绪视频的脑电。常见做法是减掉基线均值再把刺激段切成 1 秒窗口窗口重叠设 50%既保证样本量又不至于让相邻窗口完全独立。窗口长度这一步值得好好选1 秒窗在情绪识别里是折中值太短频率分辨率不够太长样本数大幅缩水。fs 128 baseline_len 3 * fs win_len 128 # 1 秒窗口 step_len 64 # 0.5 秒步长50% 重叠 eeg_all all_data[:, :32, baseline_len:] # 去掉前 3 秒基线 n_trials eeg_all.shape[0] n_windows int((60 * fs - win_len) / step_len) 1 X_windows np.zeros((n_trials * n_windows, 32, win_len)) y_windows np.zeros((n_trials * n_windows, 4)) win_idx 0 for t in range(n_trials): trial_data eeg_all[t] for w in range(n_windows): start w * step_len X_windows[win_idx] trial_data[:, start:start win_len] y_windows[win_idx] all_label[t] win_idx 1这里的参数值得展开。n_windows算出来是(7680-128)/64 1 119每个试验产生 119 个窗口总共 152320 个样本。对二分类任务来说样本量充足但要警惕同一试验的相邻窗口高度相关如果划分训练集和验证集时把同一个试验的窗口拆到两边就会出现严重的数据泄漏验证集准确率虚高。后面讲到模型训练时我会重点说这个坑。2.3 Z-score 标准化放在切窗之后做拟合对象必须是被试内数据脑电原始幅值在不同受试者、不同电极之间差异很大通常要做 Z-score 标准化。标准化有两个细节容易被忽略一是放在切窗后做因为后续特征提取如 PSD 和 DE 都依赖幅值分布二是均值和标准差只能从训练集拟合再应用到验证集和测试集。如果直接对全部数据统一标准化验证集的信息在训练阶段就已经被模型看到了。from sklearn.preprocessing import StandardScaler train_windows, val_windows X_windows[:100000], X_windows[100000:] scaler StandardScaler() # 只对训练窗口拟合 train_shape train_windows.shape val_shape val_windows.shape train_flat train_windows.reshape(train_shape[0], -1) val_flat val_windows.reshape(val_shape[0], -1) scaler.fit(train_flat) train_norm scaler.transform(train_flat).reshape(train_shape) val_norm scaler.transform(val_flat).reshape(val_shape)上述代码把 32 通道的 1 秒窗口展平成一条 4096 维向量每个时间点的幅值都会被独立标准化。这样做的缺点是破坏了通道间原有的幅值对比但优点是对抗不同受试者的个体差异更有效。实际项目里我更倾向按通道标准化即每个通道的均值方差独立计算这样能保留通道间的相对强弱后面做脑图时空间差异更容易被卷积层捕捉。3. 提取 PSD 和微分熵 DE频带怎么选、窗参数怎么调特征提取是整个项目里“理论味”最重的一步。PSD 描述信号在各频率上的能量分布微分熵 DE 描述频带信号的不确定性程度。两者都依赖合理的频带划分你不能直接把 1 到 64Hz 的全频段丢给模型。3.1 情绪相关的频带为什么 θ、α、β、γ 是默认配置脑电情绪识别里最常见的频带划分是 θ(4-8Hz)、α(8-12Hz)、β(12-30Hz)、γ(30-45Hz)。低频 0.5-4Hz 的 δ 波通常和深度睡眠相关在情绪诱发实验里贡献不大还会被眼动伪迹污染45Hz 以上容易混入肌电噪声。因此大多数基于 DEAP 的论文都止步于 45Hz。频带宽度不均匀是有原因的α 波在情绪刺激下会伴随额叶不对称性变化γ 波段则被认为与情绪唤醒度的加工相关。如果你做的是跨被试泛化实验可以把 β 和 γ 合并到 12-45Hz因为 γ 段跨个体差异太大单独作为特征经常导致过拟合。3.2 功率谱密度 PSDWelch 方法的窗口和重叠率计算 PSD 的首选是 Welch 方法它把一段信号分成多个重叠窗分别做周期图再取平均方差比直接 FFT 小得多。在 128Hz 采样率下我常用的参数是nperseg128、noverlap64频率分辨率正好是 1Hz能完整覆盖 4-45Hz 的频带范围。from scipy.signal import welch def extract_psd(eeg_window, fs128): freqs, psd welch(eeg_window, fsfs, nperseg128, noverlap64) # psd 形状: (32, 65)freqs 长度为 65对应 0-64Hz band_idx { theta: (freqs 4) (freqs 8), alpha: (freqs 8) (freqs 12), beta: (freqs 12) (freqs 30), gamma: (freqs 30) (freqs 45) } psd_features [] for band in [theta, alpha, beta, gamma]: idx band_idx[band] psd_features.append(np.mean(psd[:, idx], axis1)) # 每个通道一个均值 return np.stack(psd_features, axis1) # (32, 4)参数说明nperseg128意味着每个子窗口有 128 个点在 128Hz 下恰好 1 秒noverlap64让相邻子窗口重叠 50%这是 Welch 方法的经典配置。如果nperseg缩小到 64频率分辨率变为 2Hzθ 频带4-8Hz里只有两个频点可取方差会变大调大nperseg会牺牲时间局部性但对 1 秒窗口内的静态特征反而更平滑。对情绪分类任务我通常不会用 log 变换 PSD 以外的操作保持幅度相对差异即可。3.3 微分熵 DE用带通滤波加方差估算更稳微分熵的定义是连续随机变量的信息熵。对 EEG 信号而言工程上常用高斯近似DE 1/2 × log(2πeσ²)其中 σ² 是频带内信号的方差。这给了一个很便捷的实现路径先对每个频带做带通滤波再在滑动窗内算方差最后取对数。from scipy.signal import butter, sosfiltfilt def extract_de(eeg_window, fs128): bands [(4, 8), (8, 12), (12, 30), (30, 45)] de_features [] for fl, fh in bands: sos butter(4, [fl, fh], btypebandpass, fsfs, outputsos) filtered sosfiltfilt(sos, eeg_window, axis1) variance np.var(filtered, axis1, ddof1) de 0.5 * np.log(2 * np.pi * np.e * variance 1e-6) de_features.append(de) return np.stack(de_features, axis1) # (32, 4)这里有几个容易踩的细节。sosfiltfilt是零相位滤波正向反向各做一次保证滤波后信号没有相位偏移这对后续方差计算很重要。加 1e-6 是为了防止variance为 0 时log出现负无穷。ddof1是样本方差比总体方差更贴近真实估计尤其窗口只有 128 个点时差异不可忽略。从效果上看PSD 和 DE 有相关性但不完全等价。PSD 是能量的绝对度量DE 更关注频带内信号的分布宽度当频带信号被噪声干扰变成非高斯分布时两者会出现明显分歧。所以实践中的通常做法是把两者都提取出来作为两个独立的特征通道喂给网络而不是拼接成一个长向量。3.4 特征形状怎么和脑图对接假设你按上面的代码逐窗口提取 PSD 和 DE每类特征形状是(窗口数, 32, 4)也就是每个窗口对应 32 通道 × 4 频带。搭建脑图二维卷积时常见做法是把 4 个频带当作通道数把 32 个电极铺成 9×9 的脑图最终输入张量形状为(batch, 4, 9, 9)。如果想把 PSD 和 DE 都利用上可以叠成两个“模式通道”输入形状变成(batch, 8, 9, 9)让卷积核同时看到两种特征在空间上的分布。我试过把 PSD 和 DE 直接拼接成 8 维特征向量再接全连接层作为对照组同样数据下比二维卷积低 3-5 个百分点。这说明了空间脑图对模型性能的贡献不是玄学而是卷积核确实在利用邻近电极之间的相关性。4. 从一维特征到二维脑图电极坐标映射与插值边界这是整个项目里最容易被跳过的环节。很多人把 32 个通道的特征直接排成 4×8 或 8×4 的矩阵当图片用这样做虽然形状上是二维但通道之间的空间关系是假的。真正的脑图要模拟电极在头皮上的俯视位置。4.1 二维卷积要的空间邻域从哪来脑电信号在头皮上传播时相邻电极捕获的电位变化是连续相关的这正是二维卷积能发挥作用的前提。如果把通道顺序胡乱排列卷积核学到的所谓空间特征毫无物理意义。构建脑图的本质是把 32 个电极从 10-20 系统映射到一张二维网格上让卷积核的感受野恰好覆盖头皮上相邻的电极组。4.2 两种映射方案固定人工布局 vs 真实坐标插值第一种方案是手工指定每个电极落在 9×9 网格的哪个位置。优点是极快、可复现缺点是网格稀疏只有 32 个非零点其余位置都要填 0卷积核在空白区域浪费感受野。第二种方案是用标准 10-20 系统坐标做插值得到稠密脑图。神经成像里常用scipy.interpolate.griddata做双三次插值让空白位置由邻近电极的场强推出来。两种方案在不同任务里表现有明显差异。做二分类情绪识别时人工布局配合零填充往往更稳因为插值会引入虚假信息做多分类或回归时插值后的稠密脑图让卷积特征更平滑模型更容易收敛。4.3 一个能用的 9×9 人工布局表下面这张表是我在实验里验证过的行号对应头顶俯视图的前后轴行 0 是前额行 8 是枕部列号对应左右轴列 4 是中线。CHANNEL_NAMES [ FP1,AF3,F3,F7,FC5,FC1,C3,T7,CP5,CP1, P3,P7,PO3,O1,Oz,Pz,FP2,AF4,Fz,F4, F8,FC6,FC2,Cz,C4,T8,CP6,CP2,P4,P8, PO4,O2 ] LAYOUT_9x9 { FP1: (0, 3), FP2: (0, 5), AF3: (1, 2), AF4: (1, 6), F7: (2, 1), F3: (2, 3), Fz: (2, 4), F4: (2, 5), F8: (2, 7), FC5: (3, 2), FC1: (3, 3), FC2: (3, 5), FC6: (3, 6), T7: (4, 1), C3: (4, 3), Cz: (4, 4), C4: (4, 5), T8: (4, 7), CP5: (5, 2), CP1: (5, 3), CP2: (5, 5), CP6: (5, 6), P7: (6, 1), P3: (6, 3), Pz: (6, 4), P4: (6, 5), P8: (6, 7), PO3: (7, 3), PO4: (7, 5), O1: (8, 3), Oz: (8, 4), O2: (8, 5) }这个布局参考了 10-20 系统的拓扑关系T7/T8 在左右颞叶外侧C3/C4/Cz 在中央沟附近Oz 在枕区中央。用代码把每个电极的频带特征填到网格对应位置def features_to_image(feature_matrix, layout, grid_size9): # feature_matrix: (32,), 代表某个频带下各通道的特征值 image np.zeros((grid_size, grid_size)) for i, ch_name in enumerate(CHANNEL_NAMES): r, c layout[ch_name] image[r, c] feature_matrix[i] return image这个方法有个明显问题网格边界处电极密度低例如 T7 在 (4,1) 和 O1 在 (8,3) 之间大片空白。如果你不想让卷积核在空白处做无意义计算可以引入可学习的掩码层将空白位置对应的卷积输出直接置零。我在实际项目里会先跑一轮固定布局把结果当作 baseline再看要不要上插值。4.4 用真实坐标做插值scipy 的具体操作如果你决定走插值路线可以用标准 10-20 系统的归一化坐标通过griddata把 32 个离散点的特征插值成 9×9 的稠密图。这里的关键是不要用methodcubic时对超出电极凸包的空白区域外推否则会产生离谱的伪值。from scipy.interpolate import griddata import numpy as np def interpolate_to_grid(feature_vec, coords, grid_size9): # coords: (32, 2) 的二维归一化坐标 grid_x np.linspace(0, 8, grid_size) grid_y np.linspace(0, 8, grid_size) xi np.stack(np.meshgrid(grid_x, grid_y), axis-1) # (9, 9, 2) image griddata(coords, feature_vec, xi, methodcubic) return np.nan_to_num(image, nan0.0)参数说明griddata的第三个参数是要插值的特征向量必须与坐标第一维长度一致xi是目标网格点形状为 (81, 2) 才符合接口上面代码用meshgrid生成后会自动广播。nan_to_num把无效值填成 0用于处理插值点落在凸包外的场景。一个能直接套用的坐标来源是 EEGLAB 的标准电极坐标文件但不同版本坐标朝向可能不同建议先可视化几个关键电极的位置确认前额在网格上方、枕部在下方。5. 二维卷积模型搭建与训练排查输入张量、超参和四个翻车点有了脑图特征模型搭建反而是最直接的一步。这里用一个轻量级二维卷积网络就能跑出不错的效果真正让项目翻车的大多不是网络结构而是数据划分和特征对齐。5.1 把 PSD 和 DE 组织成卷积输入张量假设已经对每个窗口分别提取了 PSD 和 DE各得到 (32, 4) 的特征矩阵。先按上文布局把每个频带展开成 9×9 脑图再把四个频带叠在一起最终每个窗口得到两个 4×9×9 的张量。psd_img np.zeros((n_samples, 4, 9, 9)) de_img np.zeros((n_samples, 4, 9, 9)) for i in range(n_samples): for b in range(4): psd_img[i, b] features_to_image(psd_features[i, :, b], LAYOUT_9x9) de_img[i, b] features_to_image(de_features[i, :, b], LAYOUT_9x9) X_combined np.concatenate([psd_img, de_img], axis1) # (n, 8, 9, 9)这里的n_samples是你的窗口总数。axis1合并后网络输入通道为 8其中前 4 个通道是 PSD 的四个频带后 4 个通道是 DE 的四个频带。这种组织方式让卷积层能在同一空间位置上同时看见能量和熵两类信息。5.2 一个先保证能跑通的紧凑网络用一个两层卷积加一个分类头的结构起步。不要一上来就堆 ResNet 或注意力DEAP 单模态脑电数据量有限模型越大越容易过拟合。import torch import torch.nn as nn class EEGEmotionCNN(nn.Module): def __init__(self, in_channels8, n_classes2): super().__init__() self.features nn.Sequential( nn.Conv2d(in_channels, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.AdaptiveAvgPool2d((2, 2)) ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(64 * 2 * 2, 64), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(64, n_classes) ) def forward(self, x): x self.features(x) return self.classifier(x)参数说明第一层卷积用 3×3 核加 padding1保证 9×9 输入经过卷积后尺寸不变AdaptiveAvgPool2d((2,2))把特征图统一压到 2×2这样不管中间哪些层改变分辨率分类头输入维度都是固定的。Dropout 放 0.5 是这类小数据集的常用节奏太低关不住过拟合太高会让训练集收敛明显变慢。5.3 训练超参数的底线学习率、batch size 与早停训练脑电模型时我习惯把 batch size 设为 64学习率初始 0.001优化器 Adam。如果看到训练集损失下降但验证集纹丝不动先把学习率降到 0.0003 重新跑一轮不要急着换模型结构。optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.5) loss_fn nn.CrossEntropyLoss()StepLR每 10 个 epoch 把学习率减半这是低成本有效的策略。对 DEAP 这类样本量约 15 万的窗口我通常训练 30-50 个 epoch并设置早停连续 8 个 epoch 验证集损失不降就停。认真注意一点早停必须在验证集上判断而不是靠训练集准确率。5.4 排查DEAP 二维卷积训练中常见的四个翻车点翻车点一是 loss 突然变成 NaN。现象训练到第几百步时 loss 变为 nan之后永远回不来。原因通常是 DE 特征里log(0)或标准化后输入包含极端值让梯度爆炸。解决提取 DE 时加 1e-6 平滑项并在网络开头加一层nn.BatchNorm同时用torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)限制梯度范数。翻车点二是“验证集准确率高于训练集”这一反常现象。现象训练集 70%验证集 92%。原因切窗时同一个试验的相邻窗口被分到训练和验证集由于窗口重叠 50%验证集里有大量与训练窗口几乎重复的样本模型等于开卷考试。解决按试验或按受试者划分数据集不能按窗口随机划分。翻车点三是准确率始终在 55%-60% 徘徊。现象换了好几个网络结构都没有明显提升。原因DEAP 的标签是 1-9 的连续评分很多教程直接把评分大于等于 5 记为积极情绪、小于 5 记为消极情绪但正负样本严重不平衡。解决检查标签分布如果积极样本占比超过 65%改用类别加权损失或者尝试按受试者各自中位数做二分类消除个体评分偏置。翻车点四是脑图布局换了网络就没法做消融实验。现象论文里要对比“有脑图 vs 无脑图”但网络输入维度不兼容。解决在代码里抽出一个形状变换函数把 (32,4) 的特征既输出为 (128,) 的向量也输出为 (4,9,9) 的脑图用同一份特征跑两组实验。这样得到的对比结论才是干净的。6. 用跨被试划分和混淆矩阵验证模型别被虚高的准确率骗了跑通模型只是第一步验证这套二维卷积方案是否真的可用需要一套不注水的验收流程。我自己的习惯是拿到模型后先不做任何调参用跨被试划分跑一次完整实验然后看混淆矩阵和每个受试者单独的正确率这两件事能挡住大多数虚假结论。跨被试划分指的是把受试者按照名称分组比如 32 个人中 24 人做训练集4 人做验证集4 人做测试集过程中每个受试者的所有窗口只出现在一组里。这样测出来的模型面对全新受试者时的泛化能力才有参考价值。很多论文用窗口级随机划分测试集里包含同一受试者的相邻窗口准确率虚高 5-10 个百分点属于自欺欺人。from sklearn.metrics import confusion_matrix, classification_report from sklearn.model_selection import GroupShuffleSplit subject_ids np.repeat(np.arange(32), n_windows * 40) # 每个受试者对应样本数 splitter GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, test_idx next(splitter.split(X_combined, y_binary, groupssubject_ids))这里的关键是GroupShuffleSplit的groups参数它保证同一受试者的全部窗口只会被分到训练或测试中的一侧。如果你不想引入 scikit-learn也可以手动实现把 32 个受试者编号打乱选前 80% 的受试者作为训练组剩余 20% 作为测试组。拿到测试集预测结果后不仅看准确率还要输出每个类别的精确率和召回率。脑电情绪识别里最常见的现象是“总体准确率 85%但消极情绪召回率只有 60%”这说明模型在学会偷懒——把所有样本都判成多数类。如果看到这种情况就要回到标签处理用类别权重或者阈值偏移。还有个更细的验证技巧分别计算每个受试者的测试集准确率做成柱状分布图。个别受试者准确率低于 50% 是正常现象因为 DEAP 本身就是被试主观评分有人对视频的情绪响应与标签完全相反。如果大多数受试者接近随机水平而少数几个受试者接近 100%基本可以断定模型只是记住了被试特征而不是真正识别情绪状态。最后做一次细粒度检查把 9×9 脑图按频带单独可视化叠加模型在各通道上的重要性。具体做法是把测试集脑图张量求平均减去随机打乱通道顺序后的平均脑图差异大的位置就是卷积核真正依赖的电极区域。额叶和颞叶的差异通常比较明显如果模型反而依赖枕区很可能是在利用视觉诱发电位而非情绪特征需要重新检查预处理。我自己的习惯是每次改实验参数先跑一个 batch size 为 1 的最小样例确认 loss 能在几个 step 内下降再跑全量数据。这一步能挡住下标写错、维度不对、数据没有对齐等最低级的问题省下来的调试时间远超过写样例本身。希望这篇笔记能帮到正在折腾 DEAP 脑电情绪识别的你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑