ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

DEAP情绪识别实战:PSD与微分熵构建二维脑图卷积模型

DEAP情绪识别实战:PSD与微分熵构建二维脑图卷积模型 简介资源为论文加源码基于DEAP的脑电情绪识别完整方案面向脑机接口、情感计算方向的开发者与研究人员。模型以多任务卷积神经网络MT-CNN为核心将微分熵DE与功率谱密度PSD特征构建为脑图经二维卷积完成效价和唤醒度分类在DEAP数据集上分别取得96.28%和96.62%的准确率优于多种对比方法。压缩包共19个文件容量约7.7MB包含模型定义、数据加载、训练与测试等Python脚本9个py、2个可交互的Notebook演示以及论文PDF、脑图可视化PNG和项目依赖清单目录与文件命名清晰便于按模块复用。当前已有3098人学习下载。读者可获得完整可运行代码、数据表示与模型结构图、论文原文及实验配置说明适合作为情绪识别方向入门复现与算法改进的参考。1. 脑电情绪识别与DEAP为什么选二维卷积PSD/DE这条路跑过DEAP数据集做情绪识别的人都知道真正的门槛不在模型结构而是从原始EEG信号到模型输入之间那条长长的特征工程链路。很多人上来就搭CNN结果输入层都不知道该喂什么形状的张量——因为DEAP存的是二维通道×时间序列而卷积神经网络需要的是二维图像结构。本文要拆的这份“论文加源码”资源核心思路是把PSD功率谱密度和微分熵两种特征提取出来再按电极位置映射成二维脑图喂给二维卷积网络做情绪分类。这个方案在DEAP上被反复验证过baseline能跑到不错的准确率而且代码路径清晰适合作为情绪识别方向的入门复现项目。这套流程不仅能解决特征怎么提、脑图怎么构建的问题还能直接当模板改到其他EEG任务上。2. 特征提取PSD与DE的算法原理与计算细节2.1 从时域到频域功率谱密度PSD的提法脑电信号最基础的处理思路是把时域波形变换到频域。人脑在静息和情绪刺激下不同频段的能量分布是有差异的——比如alpha波在放松时增强beta波在焦虑或兴奋时增强。PSD做的事情就是衡量信号在不同频率上的功率分布常用的计算方法是Welch法。Welch法的基本思路是把长信号分段对每段加窗常见汉宁窗然后做FFT再把所有段的功率谱取平均降低方差。代码上直接调用SciPy就能搞定from scipy import signal import numpy as np def compute_psd(data, fs128): 计算单通道脑电信号的功率谱密度 data: (n_samples,) 一维时序信号 fs: 采样率DEAP是128Hz 返回: freqs: 频率轴, psd: 对应功率值 # nperseg取256对应2秒窗口overlap取128重叠50% freqs, psd signal.welch(data, fsfs, nperseg256, noverlap128, windowhann) return freqs, psd关于参数选择nperseg不建议取太小频率分辨率不够会丢掉细粒度频带信息也不建议取太大短时脑电信号本身是非平稳的窗口太长反而把时变特征平均掉了。256在128Hz采样率下对应2秒窗口、0.5Hz分辨率对DEAP这种60秒长的trail来说够用。Welch法比直接对整段信号做FFT的方差更小这是特征提取环节第一个值得留意的参数。2.2 微分熵DE为什么情绪识别任务里它更稳PSD描述的是频域能量分布但它没有考虑信号在时域上的不确定性。微分熵是对连续信号香农熵的推广数学形式为h(X) -∫ f(x) log(f(x)) dx对于脑电信号通常假设每个频段的幅值服从高斯分布此时微分熵有闭式解h(X) (1/2) * log(2πe σ²)其中σ²是信号在该频段的方差。这也是为什么DE只需要对频段信号求方差再取对数比直接估计概率密度函数快得多。2.3 PSD与DE在DEAP上的选型对比在DEAP这种128Hz采样率的数据上PSD和DE的区别值得明确说出来。PSD是功率谱上的逐点值信息粒度细但维数高、噪声大DE是对每个频段做整体统计相当于做了降维和压缩抗噪能力更强。实际训练时DE做输入特征往往比直接用PSD的准确率高几个点尤其在受试者间泛化评估时更明显。特征计算维度抗噪性分类表现PSD逐频点较弱中等DE频段级较强较好DEAP数据集来自32名受试者观看40段音乐视频的脑电记录通道数为32采样率128Hz每段视频时长约60秒。因为每位受试者的EEG幅值差异很大特征计算完成后通常要做最大最小归一化。我一般把频率划分为delta(0.5-4Hz)、theta(4-8Hz)、alpha(8-14Hz)、beta(14-31Hz)、gamma(31-45Hz)五个频段对每个通道每个频段分别计算统计特征。完整代码如下def extract_de(data, fs128, bandsNone): 提取微分熵特征 data: (n_channels, n_samples) 多通道EEG bands: 频段边界默认[1,4,8,14,31,45] 返回: (n_channels, n_bands) 特征矩阵 if bands is None: bands [1, 4, 8, 14, 31, 45] freqs, psd compute_psd(data, fsfs) n_channels data.shape[0] features np.zeros((n_channels, len(bands)-1)) for ch in range(n_channels): for i in range(len(bands)-1): # 取对应频段内的psd直接算方差再取对数 band_psd psd[ch, (freqs bands[i]) (freqs bands[i1])] # 避免log0加一个小epsilon features[ch, i] 0.5 * np.log(2 * np.pi * np.e * np.var(band_psd) 1e-8) return features需要注意的是DE直接取np.var(psd)是一种简化的工程实现——严格来说DE计算的是信号幅值方差的熵代码里对PSD的方差再取对数相当于做了一次非线性压缩。这个版本在DEAP上的表现足够稳定适合做baseline。3. 二维脑图构建把一维通道特征映射到卷积输入3.1 电极位置与二维映射布局拿到特征向量之后还不能直接进CNN因为二维卷积要吃的是“图片”结构。Deap数据集使用了国际10-20系统的32个电极通道虽然DEAP官方提供的是通道名称列表但原始坐标信息需要自己映射。常见的做法是参考10-20系统的电极拓扑图手工定义一个布局矩阵。比如32个通道按照脑区前额、额叶、颞叶、中央、顶叶、枕叶排成6×7或8×8的网格非电极位置补零。这里的映射顺序必须和原始数据通道顺序保持一致否则特征图里电极位置错位模型学到的空间关系全是乱的。我自己用的布局版本是8×9网格加掩码具体代码如下def map_channels_to_2d(features, layout): 将通道特征映射到二维矩阵 features: (n_channels, n_bands) layout: (H, W) 的通道索引矩阵, -1表示无电极 返回: (H, W, n_bands) 的2D特征图 H, W layout.shape n_bands features.shape[1] brain_map np.zeros((H, W, n_bands), dtypenp.float32) for i in range(H): for j in range(W): ch_idx layout[i, j] if ch_idx ! -1: brain_map[i, j, :] features[ch_idx, :] return brain_map布局数组layout需要手工填写。我这里给出最常用的32通道近似布局结构对应DEAP的标准通道排序方式layout np.array([ [-1, -1, 1, 2, 3, -1, -1], [-1, 4, 5, 6, 7, 8, -1], [ 9, 10, 11, 12, 13, 14, 15], [16, 17, 18, 19, 20, 21, 22], [-1, 23, 24, 25, 26, 27, -1], [-1, -1, 28, 29, 30, -1, -1], [-1, -1, -1, 31, 32, -1, -1], ])这种排布方式是近似模拟电极在头皮上的空间分布前额、枕叶在两侧中央区在中部。严格来说DEAP没有提供标准坐标文件所以这块属于“工程近似”但实验证明这种手动布局比随机网格的效果好得多。3.2 数据窗口切分从整段trail到训练样本DEAP每个trial有60秒数据直接对整个trail做特征提取只能拿1个样本训练数据太少了。所以一般先对脑电信号做切窗把每个trail切成若干个非重叠或重叠的短窗口再对每个窗口做特征提取。比如每4秒切一个窗60秒数据可以得到15个窗口32个受试者×40个视频×15个窗口19200个样本足够训练一个小型CNN。切窗要注意窗口长度与特征频率分辨率之间的折中窗口太短、FFT频率分辨率低窗口太长、样本数少。我用的是4秒窗口nperseg256重叠50%。def sliding_window(eeg_data, window_len512, step_len256): eeg_data: (n_channels, n_samples) 返回: (n_windows, n_channels, window_len) n_channels, n_samples eeg_data.shape windows [] for start in range(0, n_samples - window_len 1, step_len): end start window_len windows.append(eeg_data[:, start:end]) return np.array(windows)参数背后的逻辑是情绪变化一般是缓慢过程4秒窗口既保留了足够的频域信息又能通过重叠切窗增加样本量。如果样本量仍然不够就把重叠率提高到75%代价是相邻窗口相关性变高验证结果偏乐观这个坑在第4章细说。3.3 CNN模型一个结构简单的二维卷积baseline二维脑图构建完成后输入就是(batch, H, W, n_bands)的张量。n_bands5作为特征通道H×W7×7的布局矩阵。模型不需要很复杂DEAP数据量级撑不起大网络简单结构反而更稳。我的基准模型是三层卷积全连接import torch.nn as nn class EEGEmotionNet(nn.Module): def __init__(self, in_channels5, num_classes2): super().__init__() self.conv_block1 nn.Sequential( nn.Conv2d(in_channels, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(2) ) self.conv_block2 nn.Sequential( nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(2) ) self.conv_block3 nn.Sequential( nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(), nn.AdaptiveAvgPool2d(1) ) self.fc nn.Linear(128, num_classes) def forward(self, x): x self.conv_block1(x) x self.conv_block2(x) x self.conv_block3(x) x x.view(x.size(0), -1) return self.fc(x)in_channels5对应五个频段的DE特征num_classes根据分类任务设定。二分类是valence高/低或arousal高/低四分类是valence×arousal的组合。卷积核3×3因为脑图本身才7×7大卷积核一次就把感受野铺满整个脑区学不到局部特征。BatchNorm在这里几乎是必须的——不同受试者的特征分布差异很大不做归一化训练很难收敛。4. 避坑指南DEAP特征提取与分类的五条血泪经验4.1 通道顺序错位导致特征图全乱现象模型在训练集上收敛正常验证集准确率明显偏低而且特征图可视化后空间分布看不出脑区集中的信号响应。原因DEAP数据文件中的通道顺序是按官方文档排列的但很多教程在读取时用错了索引序列。如果你的layout矩阵是按自己预期的脑区位置写的而通道索引对应的实际电极名对不上特征图里前额特征跑到了枕叶位置。解决先从DEAP官方文档拿到32个通道的标准顺序逐个核对原始数据中channel_names与索引的对应关系。我在项目中保留了原始通道名的对照表layout矩阵的每个格子里填的是通道名而非数字索引避免数字错位难以察觉。4.2 数据泄漏鬼影成绩的幕后推手现象训练准确率95%以上测试准确率也高得离谱结果发现代码里把同一个受试者的不同窗口同时放进了训练集和测试集。原因滑动窗口产生的相邻窗口本来就有大量重叠信息。如果按窗口随机划分而不是按受试者划分同一受试者的数据会同时出现在训练和测试两侧模型相当于见了答案考试。解决模型评估必须按受试者隔离——比如用32个人中的前24人做训练后8人做测试。代码里先按受试者划分再对每个受试者内部的窗口做shuffle。这是DEAP上最容易被人忽略且后果最严重的坑。4.3 基线处理方式不对现象特征数值波动大同一受试者不同trail的同一类情绪特征分布差异明显。原因DEAP每个trail的前3秒是基线静息状态有人直接丢弃有人直接把基线特征也拼进整体特征图。这两种做法都有问题——前者丢失了个体差异校准信息后者把静息特征混入情绪特征导致分类欺骗。解决我采用的做法是分别计算每个trail前3秒基线的DE特征然后对该trail内每个切片的DE特征做逐通道逐频段的均值除以。实际效果比直接拼接稳定得多准确率也有提升。4.4 频带划分边界与采样率不匹配现象换了不同版本的代码结果同一数据跑出来的特征值差异巨大。原因有的版本把gamma频段上限设成45Hz有的设成50Hz有的甚至把delta下限从0.5Hz改成了1Hz。DEAP数据本身采样率128Hz奈奎斯特频率64Hz但信号经过官方预处理时已经做了滤波实际有效成分一般在0.5~45Hz之间。边界设置不同DE特征完全不同。解决固定频带划分边界并在代码里写清楚每个频段对应的实际频率范围用常量表管理而不是散落在函数参数里。论文里报告的频段也要和代码一致否则复现时对不上。4.5 类不平衡陷阱现象二分类准确率60%但仔细看混淆矩阵发现模型几乎把所有样本都预测为负类。原因DEAP标签的划分方式有很多种按均值划分高低效价时两类样本量未必均衡。有些trail的评分恰好等于阈值分类时两边都算样本比例就歪了。解决要么用中位数划分保证训练集两个类别的样本数尽量接近要么在损失函数里给少数类加权重。我在代码里额外打印了每类的样本数集中检查这部分才把问题定位出来。5. 进阶跨受试者评估与注意力可视化5.1 受试者外评估的实验设置DEAP上真正有说服力的结果是跨受试者评估——用n个受试者的数据训练让模型去猜测没见过的受试者的情绪状态。这比受试者内评估难得多但更贴近“情绪识别模型能用在新人身上”的落地场景。典型的设置是留一受试者交叉验证LOSO也就是每一轮用31个人训练、1个人测试重复32次取平均准确率。这个指标在文献里给出的baseline一般在60%~70%之间能跑到75%以上的结构都已经算不错了。LOSO的实验代码框架大概是from sklearn.model_selection import LeaveOneGroupOut from sklearn.svm import SVC # X: (n_samples, n_features) 所有受试者的特征 # y: (n_samples,) 标签 # groups: (n_samples,) 每个样本所属的受试者编号 logo LeaveOneGroupOut() accuracies [] for train_idx, test_idx in logo.split(X, y, groups): clf SVC(C1.0, kernelrbf) clf.fit(X[train_idx], y[train_idx]) acc clf.score(X[test_idx], y[test_idx]) accuracies.append(acc) print(fLOSO mean acc: {np.mean(accuracies):.4f}±{np.std(accuracies):.4f})这里用SVM做对比实验很有价值——它能验证CNN提取到的空间特征确实比手工拉平特征更有效。如果你在LOSO设置下发现CNN和SVM差不多那说明你这个2D脑图没有学到有用的空间信息问题多半出在layout设计或者卷积结构上。5.2 注意力热图查看模型到底在关注哪个脑区模型训练到位后不能只盯着准确率。把梯度类激活映射Grad-CAM套在脑图上能直接看到模型做决策时关注的是哪个区域。这对写论文特别有用也能反过来检验layout是否合理。def grad_cam_visualize(model, brain_map, target_class, layer_nameconv_block3): 用Grad-CAM生成注意力热图 brain_map: (1, n_bands, H, W) 的输入脑图 返回: (H, W) 热图 model.eval() brain_map brain_map.requires_grad_(True) out model(brain_map) # 目标类别的得分 score out[0, target_class] score.backward() # 取目标层输出的梯度均值作为权重 grads brain_map.grad.squeeze(0).mean(dim0) return grads.detach().numpy()如果热图总是集中在某个固定位置而不管样本标签是什么大概率是layout写错了偏置比如把某个通道重复映射到了多个位置或者特征图里出现了一个极端值抢占了softmax的注意力。这类可视化排查和定量评价配合使用能让实验结论更加扎实。5.3 数据增强跨受试者泛化的实用技巧对EEG这类小样本数据数据增强是提升跨受试者泛化最立竿见影的手段。做法上我不会用图像那种随机裁剪或翻转——EEG头图本身没有平移不变性翻转后脑区对应关系就破坏了。可操作的是两种方式一是对原始时域信号做小幅度的高斯噪声注入二是对同一窗口的特征用不同的频带边界做随机微调。前者是对信号的物理增强后者是在特征层面制造扰动抗过拟合效果都还凑合。我习惯的做法是前者在滑动窗口阶段做若干轮随机加噪然后独立做特征提取相当于把同样的脑电数据复制出一批带噪声的变体。噪声幅度取信号标准差在0.02倍以内太大就会把原有频带结构打散处理不好直接让性能往下掉。从那以后我在所有DEAP实验里都保留了这套加噪增强逻辑每次模型抽风时我都会先从数据切窗和受试者划分重新核对一遍确认这两个坑都没踩之后再去动模型结构。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进