
简介这份资源面向深度学习与生物信号处理方向的学习者和研究者围绕DEAP数据集展开脑电情绪识别实践解决从多模态生理信号中提取情感特征并完成分类的问题。包内共59个文件以npy数据文件、py脚本为主辅以xml配置与少量缓存文件压缩包约647.77MB其中npy保存预处理后的脑电与标签数据py脚本对应CNN、LSTM及组合模型的训练与对比流程。项目对比了四种网络架构借助PyEEG完成功率谱密度等特征提取最终实现约90%的识别准确率可帮助读者理解α、β、θ、δ等频段与情绪状态的关联。已有6397人学习下载适合希望复现完整实验、研究模型选型与特征工程的中高级读者参考。1. 拿到 DEAP 数据集之后脑电情绪识别到底该从哪里下手很多人第一次接触脑电情绪识别都是被 DEAP 数据集带进来的。下载完那个压缩包解压出一堆.dat文件用pickle一读发现里面是 40 个被试、每人 40 段视频诱发下的 32 通道脑电外加外周生理信号标签是 valence、arousal、dominance、liking 四个维度打的分。数据在手反而不知道下一步该干嘛——这是我最常被问到的问题。这篇笔记就围绕「基于 DEAP 数据集的脑电情绪识别」这件事把从数据读取、预处理、特征提取、分类建模到结果验证的完整链路讲清楚。适合两类人一类是刚拿到 DEAP 数据集、想跑通一个二分类 baseline 的新手另一类是做了一段时间、发现准确率卡在 60% 上下、想搞清楚问题出在哪的熟手。我不会只讲概念每一步都会落到能跑的代码和能调的参数上。2. 先把 DEAP 的数据结构吃透40 个被试、32 通道、4 个标签2.1 DEAP 里到底存了什么DEAP 数据集的核心文件是s01.dat到s32.dat每个文件对应一个被试。用 Python 读进来之后data是一个字典键包括data、labels、names等。其中data的 shape 是(40, 40, 8064)——40 段试验、40 个通道、8064 个采样点。前 32 个通道是 EEG后 8 个是外周信号眼电、肌电、皮电、呼吸、血容量脉搏、体温等。采样率是 128Hz每段试验 63 秒去掉 3 秒基线后正好 60 秒60×1287680但实际存的是 8064包含了基线段。标签在labels里shape 是(40, 4)依次是 valence、arousal、dominance、liking分值范围 1 到 9。做二分类时常见做法是以 5 为阈值大于 5 算高小于等于 5 算低。这里有个细节DEAP 官方论文里用的是 5 作为中位数分割但实际数据里恰好等于 5 的样本很少所以直接按5和5分就行。import pickle import numpy as np with open(s01.dat, rb) as f: subject pickle.load(f, encodinglatin1) eeg subject[data][:, :32, :] # 取前32通道EEG labels subject[labels] # (40, 4) # 以valence为例做二分类标签 valence labels[:, 0] binary_labels (valence 5).astype(int) print(eeg.shape, binary_labels.shape)这段代码做了三件事读取单个被试数据、切出 EEG 通道、把 valence 连续分数转成 0/1 标签。参数上要注意encodinglatin1这是 Python 3 读 Python 2 序列化文件的常见坑不加会报UnicodeDecodeError。另外subject[data]的通道顺序是固定的前 32 是 EEG不要搞混。2.2 为什么不能直接把 8064 个点丢进分类器原始 EEG 信号里混着大量噪声工频干扰、眼电伪迹、肌电伪迹还有被试个体差异带来的基线漂移。直接把 8064 维原始信号当特征维度灾难不说分类器学到的多半是噪声。所以必须做预处理和特征提取。常见流程是带通滤波比如 1-45Hz 或 4-45Hz去掉低频漂移和高频噪声然后做降采样比如降到 128Hz 或 256Hz再分段做特征。DEAP 本身已经是 128Hz 降采样后的数据但滤波这一步不能省。我一般用scipy.signal.butter设计一个 4 阶巴特沃斯带通滤波器filtfilt做零相位滤波避免相位偏移。from scipy.signal import butter, filtfilt def bandpass_filter(data, low1.0, high45.0, fs128, order4): nyq fs / 2.0 b, a butter(order, [low/nyq, high/nyq], btypeband) return filtfilt(b, a, data, axis-1) eeg_filtered bandpass_filter(eeg)low和high的选择有讲究下限设 1Hz 能去掉基线漂移但如果你关注 delta 波0.5-4Hz就得设到 0.5Hz上限设 45Hz 能保留 gamma 波的大部分同时避开 50Hz 工频。order4是经验值阶数太高容易不稳定太低则过渡带太宽。filtfilt比lfilter好在于它做了前后两次滤波相位不失真代价是计算量翻倍但 DEAP 这个数据量完全扛得住。3. 特征提取从功率谱到微分熵哪些特征真的有用3.1 频带功率和微分熵EEG 情绪识别里最常用的特征有两类频带功率和微分熵。频带功率就是把信号分到 delta1-4Hz、theta4-8Hz、alpha8-14Hz、beta14-30Hz、gamma30-45Hz五个频带算每个频带的平均功率。微分熵是功率谱熵的一种推广对高斯信号来说微分熵和功率谱的对数成正比计算更稳定。我一般用 Welch 方法算功率谱密度然后对每个频带积分。scipy.signal.welch的nperseg参数决定频率分辨率DEAP 是 128Hznperseg256能给出 0.5Hz 的分辨率够用。from scipy.signal import welch def band_power_features(data, fs128, bandsNone): if bands is None: bands {delta: (1, 4), theta: (4, 8), alpha: (8, 14), beta: (14, 30), gamma: (30, 45)} freqs, psd welch(data, fsfs, nperseg256, axis-1) features [] for name, (low, high) in bands.items(): idx np.logical_and(freqs low, freqs high) features.append(np.mean(psd[..., idx], axis-1)) return np.stack(features, axis-1) # (..., 5)这段代码对每个通道算 5 个频带的平均功率。nperseg256对应 2 秒窗频率分辨率 0.5Hz。np.mean沿频率轴取平均得到每个频带的标量。输出 shape 是(40, 32, 5)即 40 段试验、32 通道、5 个频带。如果你想把通道也压掉可以再对通道取平均但我不建议——不同脑区的情绪响应差异很大前额叶和顶叶的信息量完全不同压掉通道等于扔掉空间信息。3.2 特征拼接与降维把频带功率和微分熵拼在一起维度是 32 通道 × 5 频带 × 2 类特征 320 维。这个维度对 40 个样本来说还是太高直接上 SVM 容易过拟合。常见做法是先用 PCA 降到 20-50 维或者用 LDA 做有监督降维。我一般先用 PCA 保留 95% 方差再送进分类器。from sklearn.decomposition import PCA from sklearn.preprocessing import StandardScaler # X shape: (n_samples, 320) scaler StandardScaler() X_scaled scaler.fit_transform(X) pca PCA(n_components0.95) X_pca pca.fit_transform(X_scaled) print(X_pca.shape)StandardScaler不能省因为功率和熵的量纲不同不标准化的话 PCA 会被大方差特征主导。n_components0.95表示保留 95% 的方差实际跑下来 DEAP 单个被试通常降到 30-60 维。注意 PCA 要在训练集上 fit再 transform 测试集否则数据泄漏准确率会虚高。4. 分类器选型与跨被试验证为什么你的准确率虚高4.1 被试内分类和被试间分类是两回事DEAP 上最常见的错误就是把所有被试的数据混在一起随机划分训练测试集。这样做准确率能到 90% 以上但毫无意义——同一个被试的同一段数据既在训练集又在测试集分类器只是记住了这个人。正确的做法有两种被试内within-subject和被试间cross-subject。被试内是对每个被试单独建模用这个人的部分试验做训练、剩余做测试。被试间是留一个被试做测试其他被试做训练。被试间难度大得多准确率通常掉到 55%-65%但这才反映真实泛化能力。我建议新手先跑通被试内确认流程没问题再挑战被试间。from sklearn.model_selection import LeaveOneGroupOut from sklearn.svm import SVC from sklearn.pipeline import Pipeline # groups: 每个样本对应的被试编号 logo LeaveOneGroupOut() clf Pipeline([ (scaler, StandardScaler()), (pca, PCA(n_components0.95)), (svm, SVC(kernelrbf, C1.0, gammascale)) ]) scores [] for train_idx, test_idx in logo.split(X, y, groups): clf.fit(X[train_idx], y[train_idx]) scores.append(clf.score(X[test_idx], y[test_idx])) print(np.mean(scores))LeaveOneGroupOut按被试分组保证测试集的被试没在训练集里出现过。SVC的C和gamma是两个关键参数C越大越容易过拟合gamma越大决策边界越复杂。DEAP 上我一般从C1.0, gammascale起步再用网格搜索微调。注意gammascale是 sklearn 的默认值等于1/(n_features * X.var())比固定值更稳。4.2 标签阈值和类别平衡valence 和 arousal 的二分类里高/低样本比例通常接近 1:1但个别被试会偏。如果直接算准确率遇到偏斜数据会虚高。我一般同时看准确率和 F1 分数必要时用class_weightbalanced让 SVM 自动调整权重。另外阈值选 5 是惯例但不是铁律。有研究用 4.5 或 5.5 做分割结果会有几个百分点的波动。我的建议是固定用 5方便和文献对比如果你想刷高准确率可以试不同阈值但要在论文里说清楚别偷偷调。5. 避坑与排查DEAP 情绪识别里最容易翻车的 5 个地方5.1 现象准确率 95% 以上但换个人就崩原因数据泄漏。要么是随机划分没按被试分组要么是标准化/PCA 在全体数据上 fit 了。解决用LeaveOneGroupOut或GroupKFold把所有预处理步骤塞进Pipeline确保 fit 只发生在训练集上。5.2 现象训练 loss 正常测试准确率 50% 上下原因特征没区分度或者标签映射反了。DEAP 的 valence 标签是 1-9有人误把5写成5导致标签翻转。解决打印标签分布确认高/低样本数和预期一致再检查特征矩阵有没有全零列或 NaN。5.3 现象pickle.load报UnicodeDecodeError原因DEAP 的.dat文件是 Python 2 序列化的Python 3 默认用 utf-8 解码。解决加encodinglatin1这是官方文档里就写了的但很多人不看。5.4 现象滤波后信号幅度异常或者出现振铃原因滤波器阶数太高或者filtfilt的padlen不够。解决把order降到 2-4或者显式设置padlen3*max(len(a), len(b))。如果信号本身很短考虑用sosfiltfilt替代filtfilt数值更稳定。5.5 现象被试间准确率死活上不去 60%原因个体差异太大单靠功率特征不够。解决试试加入功能连接特征比如 PLV、相干性或者用域适应方法如 CORAL、DANN做跨被试对齐。另一个方向是换分类器比如用 EEGNet 这类端到端模型但数据量小的时候容易过拟合要配合强正则化。6. 把 baseline 跑稳之后一个提升跨被试准确率的具体技巧跑通 baseline 之后如果你想让被试间准确率再往上走几个点我推荐先试一个成本最低的技巧按被试做 z-score 标准化。具体做法是对每个被试的每个通道、每个频带特征减去该被试在所有试验上的均值再除以标准差。这一步能消掉个体间的基线差异让特征分布对齐。def subject_wise_zscore(X, groups): X_norm np.zeros_like(X) for g in np.unique(groups): mask groups g X_norm[mask] (X[mask] - X[mask].mean(axis0)) / (X[mask].std(axis0) 1e-8) return X_norm1e-8是防止除零别省。这个操作要在划分训练测试集之前做因为它只用到了每个被试自己的数据不涉及跨被试信息所以不算泄漏。我实测下来DEAP 上 valence 二分类的被试间准确率能从 58% 左右提到 63%-65%arousal 提升更明显有时能到 68%。另一个值得试的是特征选择。320 维里真正有用的可能就几十维用SelectKBest配合f_classif选前 50 维再送 SVM有时比 PCA 效果更好因为 PCA 是无监督的可能把判别信息也压掉了。from sklearn.feature_selection import SelectKBest, f_classif selector SelectKBest(f_classif, k50) X_selected selector.fit_transform(X_norm, y)k50是经验值你可以从 20 到 100 扫一遍看验证集准确率怎么变。注意f_classif是单变量筛选不考虑特征间关系所以别指望它一步到位但作为预处理够用了。最后说个我自己的习惯每次跑完实验我都会把被试编号、分类器参数、准确率和 F1 记在一个 CSV 里而不是只存代码。DEAP 这种 32 个被试的数据集跑一轮要不了多久但参数组合多了很容易记混。有次我调了一下午参数最后发现最佳结果对应的居然是忘了改的默认值血泪教训。希望帮到你。本文还有配套的精品资源点击获取