DEAP情绪识别实战:从数据读取到LOSO评估的完整避坑指南

📅 发布时间:2026/10/11 11:40:56
DEAP情绪识别实战:从数据读取到LOSO评估的完整避坑指南
简介该资源面向情绪识别与生理信号分析方向的研究者、学生及算法工程师围绕DEAP数据集提供一套可运行的分类实践工程。DEAP由萨里大学团队发布记录32名受试者观看40段情感视频时的心率、皮肤电导、血流动力学等8种生理指标并标注喜、怒、哀、惧四类情绪是情感计算领域的经典公开数据。资源包共38个文件以28个Java源码为主体另含5个train训练数据文件、README说明文档、工程配置与许可文件压缩包约5.79MB结构紧凑便于直接导入IDE阅读与二次开发。目前已有1127人学习下载。读者可借此了解特征提取、模型训练与交叉验证的完整流程参考工程目录组织方式快速搭建基于SVM、随机森林或神经网络的分类实验并在此基础上探索人机交互、心理健康等应用场景。1. DEAP 情绪识别落地从数据到手的第一道坎DEAP 数据集做情绪识别很多人卡在第一步不是模型不会搭而是数据拿到手发现是.dat文件用scipy.io.loadmat读出来是个 40×40 的 object 数组每个被试一个字典标签藏在labels键里脑电和外围生理信号混在data里维度是 40×63×8064。这个结构第一次见确实有点懵。sentimentclassify-master 这类工程的核心任务就是把 DEAP 的脑电信号映射到 valence、arousal、dominance、liking 四个维度上做二分类或回归。适合谁做做生理信号情感计算的研究生、想拿公开数据集练手信号处理加深度学习的工程师、以及需要复现情绪识别 baseline 的算法同学。这一章先把数据长什么样、标签怎么定义、分类任务怎么切讲清楚后面再动手。2. DEAP 数据集的读取与标签构造把 40 个被试的 .dat 变成可训练张量2.1 先搞清楚 DEAP 的目录结构和字段含义DEAP 原始数据下载下来通常是一个data_preprocessed_python文件夹里面是s01.dat到s32.dat每个文件对应一个被试。注意官方发布的是 32 个被试的预处理版本但很多论文里写 40那是另一个版本下载时看清楚。每个.dat用 pickle 协议存Python 里用pickle.load打开得到一个字典键有data、labels、subject等。data的形状是(40, 40, 8064)三个维度分别是视频/试验编号40 个片段、通道40 个通道、采样点8064。采样率是 128Hz所以每个片段 63 秒。40 个通道里前 32 个是 EEG后面 8 个是外围信号GSR、PPG、Resp、Plethysmograph、Temperature 等。labels形状是(40, 4)四列依次是 valence、arousal、dominance、liking取值是 1 到 9 的连续评分。做情绪分类时最常见的做法是把评分大于 5 的归为 high小于等于 5 的归为 low这样 valence 和 arousal 各得到一个二分类标签。也有人用 4.5 或 5.5 做阈值取决于你想要的类别平衡程度。我一般会先统计一下每个被试的标签分布如果某个被试 valence 全是 high那这个被试对当前任务就没啥信息量可以考虑剔除或者做被试独立的划分。2.2 用 Python 把 .dat 读成 numpy 数组并切分 EEG 通道下面这段代码是我常用的读取和预处理骨架直接可以跑import pickle import numpy as np from pathlib import Path def load_deap_subject(dat_path): 读取单个被试的 DEAP .dat 文件返回 EEG 数据和四维标签 with open(dat_path, rb) as f: subject pickle.load(f, encodinglatin1) # data: (40, 40, 8064) labels: (40, 4) eeg subject[data][:, :32, :] # 只取前 32 个 EEG 通道 labels subject[labels] # valence, arousal, dominance, liking return eeg, labels def make_binary_labels(labels, threshold5.0): 把 1-9 的连续评分转成 0/1 二分类标签 binary (labels threshold).astype(np.int64) return binary # 形状 (40, 4) # 示例读取第一个被试 dat_file Path(data_preprocessed_python/s01.dat) eeg, labels load_deap_subject(dat_file) print(eeg.shape) # (40, 32, 8064) print(labels.shape) # (40, 4) binary make_binary_labels(labels) print(binary[:5]) # 前 5 个 trial 的四个维度二分类结果逻辑说明pickle.load时指定encodinglatin1是因为 DEAP 是在 Python 2 环境下打包的不指定会报编码错误。eeg只取前 32 个通道如果你要做多模态融合可以把后 8 个外围信号也保留。make_binary_labels里的threshold是唯一需要根据任务调整的参数默认 5.0 对应评分中位数。参数说明threshold调高会让 high 类变少调低会让 high 类变多。如果你做的是 valence 二分类可以先画一下所有被试的 valence 直方图看看 5.0 附近是否密集密集的话可以考虑用 5.5 来拉开类别间距。另外DEAP 的标签是每个 trial 一个评分不是每个时间点所以做帧级分类时需要把标签复制到每个时间窗上这是后面章节要处理的事。2.3 被试独立划分为什么不能随机打乱所有 trial很多人第一次做 DEAP 分类直接把 32 个被试 × 40 个 trial 1280 个样本混在一起随机划分训练集和测试集结果准确率冲到 90% 以上然后投稿被拒。原因很简单同一个被试的 trial 之间高度相关随机划分会导致训练集和测试集里出现同一个被试的数据模型实际上是在做被试识别而不是情绪识别。正确的做法是留一被试交叉验证LOSO每次拿 31 个被试做训练剩下 1 个被试做测试循环 32 次。这样得到的准确率才反映模型对未见过的被试的泛化能力。我一般会写一个简单的生成器def loso_split(num_subjects32): 留一被试交叉验证的索引生成器 for test_subj in range(num_subjects): train_subjs [i for i in range(num_subjects) if i ! test_subj] yield train_subjs, test_subj # 使用示例 for train_idx, test_idx in loso_split(32): # train_idx: 31 个被试的编号列表 # test_idx: 当前测试被试编号 pass这个划分方式会让准确率下降很多valence 二分类大概在 55% 到 65% 之间arousal 稍高一些。如果看到论文里 DEAP 准确率 95% 以上基本可以判断划分有问题。这是血泪经验别问我怎么知道的。3. 从原始 EEG 到模型输入滤波、分窗与特征选择3.1 带通滤波和降采样把 8064 个点变成可处理的片段DEAP 已经做了 4-45Hz 的带通滤波和 128Hz 降采样但如果你要做特定频段分析比如 theta、alpha、beta还需要自己再滤一次。我一般用scipy.signal.butter加filtfilt做零相位滤波避免相位偏移。from scipy.signal import butter, filtfilt def bandpass_filter(data, lowcut, highcut, fs128, order4): 对 EEG 做零相位带通滤波 data: (trials, channels, samples) nyq 0.5 * fs low lowcut / nyq high highcut / nyq b, a butter(order, [low, high], btypeband) filtered filtfilt(b, a, data, axis-1) return filtered # 提取 alpha 频段 8-13Hz alpha_eeg bandpass_filter(eeg, 8, 13, fs128) print(alpha_eeg.shape) # (40, 32, 8064)参数说明order4是巴特沃斯滤波器的阶数阶数越高过渡带越陡但计算量越大4 阶在 EEG 里够用。filtfilt是前后各滤一次所以实际阶数翻倍但相位是零。如果你要做实时推理不能用filtfilt只能用lfilter但离线分析无所谓。分窗方面DEAP 每个 trial 是 63 秒前 3 秒是基线通常丢弃。剩下的 60 秒可以切成 1 秒一个窗重叠 50%这样每个 trial 得到 119 个窗。每个窗的标签和 trial 标签一致。这样样本量从 1280 变成 1280×119但要注意同一个 trial 的窗不能跨被试划分还是按被试走 LOSO。3.2 频带功率特征DEAP 上最稳的 baseline如果你不想一上来就上深度学习频带功率加 SVM 是 DEAP 上最稳的 baseline。具体做法对每个通道的每个频段theta 4-8Hz、alpha 8-13Hz、beta 13-30Hz、gamma 30-45Hz算功率谱密度然后取对数拼成一个特征向量。from scipy.signal import welch import numpy as np def extract_bandpower(eeg, fs128, bandsNone): 提取每个通道的频带功率特征 eeg: (trials, channels, samples) 返回: (trials, channels * len(bands)) if bands is None: bands {theta: (4, 8), alpha: (8, 13), beta: (13, 30), gamma: (30, 45)} features [] for trial in eeg: trial_feat [] for ch in trial: freqs, psd welch(ch, fsfs, nperseg256) for band_name, (low, high) in bands.items(): idx np.logical_and(freqs low, freqs high) band_power np.log(np.trapz(psd[idx], freqs[idx]) 1e-10) trial_feat.append(band_power) features.append(trial_feat) return np.array(features) # 提取特征 band_feat extract_bandpower(eeg) print(band_feat.shape) # (40, 128) 32通道 × 4频段逻辑说明welch做功率谱估计nperseg256对应 2 秒窗128Hz 下频率分辨率 0.5Hz。np.trapz对频段内的 PSD 积分得到绝对功率取对数是为了压缩动态范围。最终每个 trial 得到 32×4128 维特征。参数说明nperseg影响频率分辨率和方差256 是常用折中。如果你做 1 秒窗nperseg要相应减小到 128。bands可以根据你的任务调整比如做效价分类时 alpha 和 beta 的区分度通常比 gamma 高。拿到特征后用 SVM 或随机森林做 LOSO 分类valence 大概 60% 左右arousal 65% 左右。这个数字不高但它是诚实的 baseline深度学习模型如果跑不到这个数说明哪里出了问题。3.3 深度学习的输入组织把 EEG 变成 2D 或 3D 张量如果你要用 CNN 或 LSTM输入组织方式很关键。常见的有三种第一种是(batch, channels, samples)直接 1D CNN 沿时间卷积通道当特征维。第二种是(batch, samples, channels)LSTM 沿时间步处理每个时间步是 32 维向量。第三种是把 EEG 按电极位置排成 2D 图用 2D CNN但 DEAP 只有 32 通道空间分辨率低效果不一定好。我一般先用 1D CNN 试水结构简单训练快。输入(batch, 32, 128)对应 1 秒窗 128 个采样点。卷积核沿时间方向滑动通道维在最后。具体实现可以用 PyTorch 的Conv1din_channels32out_channels64kernel_size5后面接BatchNorm和ReLU再全局平均池化最后全连接输出二分类。注意DEAP 样本量小32 个被试 × 40 trial 1280 个样本切窗后虽然变多但相关性高。模型参数量要控制别一上来就 ResNet过拟合是必然的。Dropout 和早停是标配学习率用 1e-3 加余弦退火batch size 设 32 或 64。4. 避坑与排查DEAP 情绪识别里最容易翻车的 5 个地方4.1 标签阈值选 5.0 导致类别严重不平衡现象训练时准确率看着还行但混淆矩阵里某一类几乎全错F1 分数一边倒。原因DEAP 的 valence 评分在 5 附近非常密集用 5.0 做阈值时high 和 low 的样本数可能差两三倍。有些被试的 valence 评分全在 4 到 6 之间二分类后几乎全是 low。解决先统计每个被试的标签分布如果某个被试某一类少于 5 个 trial考虑剔除该被试或改用回归任务。也可以把阈值调到 5.5 或 4.5让类别更平衡。更稳妥的做法是报告 F1 和 AUC不要只看准确率。4.2 用随机划分代替 LOSO 导致准确率虚高现象模型在测试集上 90% 以上换一批被试就崩到 50%。原因同一个被试的 trial 之间共享大量个体特征随机划分让模型学到了被试身份而不是情绪。解决坚持 LOSO 或至少按被试分组划分。如果计算资源不够跑 32 折可以随机选 5 个被试做测试其余训练重复几次取平均。但一定要保证训练集和测试集的被试不重叠。4.3 忘记丢弃基线导致前 3 秒污染标签现象模型对每个 trial 的前几个窗预测特别准后面就一般。原因DEAP 每个 trial 前 3 秒是基线没有情绪刺激但标签是整个 trial 的评分。如果不丢弃模型会学到基线段的特征和标签的虚假关联。解决统一丢弃前 3 秒即 128Hz 下前 384 个采样点。如果你做 1 秒窗从第 385 个点开始切。4.4 频带功率特征没取对数导致 SVM 被高功率通道主导现象SVM 训练时收敛慢准确率比预期低很多。原因EEG 功率谱密度随频率下降低频功率比高频高几个数量级不取对数的话特征尺度差异太大SVM 的 RBF 核会被大值特征主导。解决对每个频带的功率取自然对数或者做 z-score 标准化。我一般先取对数再标准化效果更稳。4.5 深度学习模型参数量过大导致过拟合现象训练集准确率 99%测试集 55%损失曲线训练降测试升。原因DEAP 有效样本量小大模型直接记住训练集。解决用轻量模型1D CNN 控制在 3 层以内全连接层加 Dropout 0.5权重衰减 1e-4早停耐心设 10 个 epoch。数据增强可以用加高斯噪声、时间平移、通道随机丢弃但要注意别破坏 EEG 的物理意义。5. 进阶技巧用被试自适应和集成策略把 LOSO 准确率再提 3 到 5 个点5.1 被试自适应用测试被试的无标签数据做微调LOSO 的瓶颈在于训练集和测试集来自不同被试EEG 的个体差异很大。一个实用技巧是在测试阶段拿测试被试的前几个 trial 做无监督微调比如用 BatchNorm 的统计量更新或者用熵最小化做测试时适应。具体做法是模型训练好后把测试被试的数据按 batch 前向传播但只更新 BatchNorm 的 running mean 和 var不更新权重。这样模型能适应该被试的分布准确率通常能提 2 到 3 个点。def adapt_bn(model, test_loader, num_batches10): 用测试被试数据更新 BatchNorm 统计量 model.train() # 保持 train 模式以更新 running stats with torch.no_grad(): for i, (x, _) in enumerate(test_loader): if i num_batches: break model(x) model.eval() return model注意model.train()会启用 Dropout如果你不想让 Dropout 干扰可以手动把 Dropout 层设成 eval。这个技巧对 BatchNorm 层多的模型效果明显对 LayerNorm 或 GroupNorm 无效。5.2 多维度集成valence 和 arousal 联合预测DEAP 的四个维度不是独立的valence 和 arousal 有相关性。与其训练两个独立模型不如用一个多任务模型共享底层特征两个输出头分别预测 valence 和 arousal。损失函数用两个交叉熵加权求和权重可以设 1:1 或根据任务调整。class MultiTaskEEGNet(nn.Module): def __init__(self, in_ch32, num_classes2): super().__init__() self.shared nn.Sequential( nn.Conv1d(in_ch, 64, 5, padding2), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(64, 128, 5, padding2), nn.BatchNorm1d(128), nn.ReLU(), nn.AdaptiveAvgPool1d(1) ) self.fc_valence nn.Linear(128, num_classes) self.fc_arousal nn.Linear(128, num_classes) def forward(self, x): feat self.shared(x).squeeze(-1) return self.fc_valence(feat), self.fc_arousal(feat)训练时两个头的损失相加推理时分别取 argmax。多任务学习相当于给模型加了正则在小样本上通常比单任务稳。我试过在 DEAP 上多任务比单任务 valence 准确率高 2 个点左右arousal 高 1 个点。5.3 用集成学习把多个 LOSO 折的模型平均起来如果你已经跑完 LOSO每个折都有一个模型别扔掉。推理时把 32 个模型的预测概率平均再取 argmax。这个操作几乎零成本但能稳定提升 1 到 2 个点。注意每个模型是在不同训练集上训的集成相当于做了模型平均方差更小。如果嫌 32 个模型太多可以只保留验证集准确率最高的 5 个做集成。验证集从训练被试里再切一部分出来别用测试被试。最后说一个我自己的习惯每次跑完实验先把所有随机种子固定然后跑三遍取平均标准差超过 3 个点的结果我都不信。DEAP 这个数据集被试间差异太大单次结果波动很正常多跑几遍心里才有底。希望帮到你。本文还有配套的精品资源点击获取