简介:DEAP情绪识别与分类项目包(sentimentclassify-master)是一套完整的情绪分类工程实现,面向情感计算、机器学习初学者以及心理学与信号处理交叉领域研究者。项目针对DEAP生理信号数据集,覆盖数据组织、特征提取、情绪四分类(喜、怒、哀、惧)的完整流程。资源共38个文件,以28个Java源码为核心,另有5个train训练语料、项目配置文件与说明文档,压缩包仅5.79MB,轻量易用,便于下载和二次开发。目前已有1127人学习下载。包内包含README、LICENSE、.project/.classpath等工程标配,以及book、kitchen、all、dvd、electronic等多个领域的.train文本数据,既能处理文本情感分类,也可结合DEAP多模态生理信号进行情绪识别实验。读者可直接复用源码做特征对比、算法调优或场景迁移,特别适合毕业设计、课程项目以及想深入理解情感分类技术细节的开发者。整体目录结构清晰,源码与训练数据分离,显著降低了使用门槛。
1. DEAP情绪识别的第一道门槛:先把数据集下载和格式弄清楚
拿到sentimentclassify-master这份代码后,先别急着训练,第一步是把 DEAP 数据集下载到位并弄懂它的结构。DEAP 情绪识别是目前公开生理信号情绪数据集里最常被用作基准的一个:32 名被试观看 40 段音乐视频,期间同步记录 32 通道脑电与 8 路外周生理信号,每段视频结束后被试按 1–9 分对 valence、arousal、dominance、liking 打分。标题里的 deap 数据集分类,落到代码上就是一条固定链路:下载解压、读 mat、去基线、分窗、提取特征、训练分类器、报告准确率。这篇文章把这条链路完整写一遍,并把我在 8064 与 7680 采样点、标签阈值、特征提取顺序上踩过的坑一并说清,适合刚拿到这份代码想复现 baseline 的读者。
2. DEAP数据集下载与 mat 解析:从申请到读出能训练的三维张量
2.1 下载 DEAP 数据集:申请审核与两种目录
DEAP 并不像 CIFAR 那样挂一个地址就能直接下。它在官网以学术数据集形式发布,常规下载流程是:先在数据发布页填写申请表单,写明单位与用途,一般用学校或机构邮箱提交,等审批邮件里给出下载链接。审核通常一两个工作日,个别时候会被反垃圾规则吞进回收站,收不到链接时记得先翻垃圾邮件。这一步没有太多技巧,耐心按表单填就行。
解压之后,你会看到一个包含两个核心目录的文件夹。data_preprocessed_matlab是我们训练时要用的版本,里面是 32 个被试各自独立的.mat文件,命名从s01.mat到s32.mat;data_original是原始信号目录,按视频分段存放,文件数量大、格式也更碎,一般基线实验用不到。如果你只是想跑通 sentimentclassify-master 分类流程,下载 preprocessed 目录就够,原始数据那部分占的空间和折腾成本都更高。
这里有个容易忽略的点:preprocessed 并不等于“可以直接进模型”。它做过的预处理是下采样到 128Hz、去除 EOG 伪迹、4–45Hz 带通滤波,但每个 trial 仍然包含基线片段,需要我们自己切掉。我第一次看到8064这个数字时误以为是单纯 60 秒乘以 128,后来才发现 8064 = 63 × 128,多出来的 3 秒就是基线。这个细节会直接影响后续窗口提取,我放到避坑章节专门讲。
2.2 mat 文件里到底存了什么:data 与 labels 两个字段
用scipy.io.loadmat读取s01.mat,输出里面有几个字段,常用的只有两个:
import scipy.io as sio raw = sio.loadmat('data_preprocessed_matlab/s01.mat') print(raw.keys()) data = raw['data'] # (40, 40, 8064) labels = raw['labels'] # (40, 4) print(data.shape, labels.shape, labels.dtype)先解释形状。data的三维分别是 trial、channel、sample:32 名被试每人 40 个 trial,对应 40 段音乐视频;每个 trial 内是 40 个通道、共 8064 个采样点。labels是 40 行 4 列,列顺序固定为 valence、arousal、dominance、liking,取值 1–9,分数越高代表越正向、越兴奋、越有支配感、越喜欢。
40 个通道里,前 32 个是 EEG 通道,顺序对应 10-20 系统电极布局;后面的 8 个是外围生理通道,常见顺序是水平/垂直眼电、颞肌电、额肌电、皮肤电、呼吸、脉搏与体温。做纯 EEG 研究的人会只取前 32 行,做多模态融合的人会保留全部。这个选择会影响特征维度,也会影响最终准确率。我一般会先打印raw.keys()确认字段名,因为个别版本里存在data与labels之外的附加字段,不要凭记忆取。
2.3 用 scipy 读取 mat 并拆出训练样本
我不建议在训练循环里反复loadmat,更常见的做法是写一个按被试加载的函数,一次只把一个被试读进内存。下面这个函数做了两件事:去掉前 3 秒基线,并把数据类型转换为 float32 以省内存。
import numpy as np def load_deap_subject(path, fs=128, baseline_sec=3): raw = sio.loadmat(path) data = raw['data'].astype(np.float32) # (40, 40, 8064) labels = raw['labels'].astype(np.float32) if baseline_sec > 0: data = data[:, :, fs * baseline_sec:] # 丢弃前 baseline_sec 秒 return data, labels注意这里为什么用fs * baseline_sec而不是硬编码384:采样率是数据集的约定,但代码里一旦写死,将来切换到 256Hz 版本或者其他数据集时就要到处改。用参数传进函数,调用侧只要写load_deap_subject('s01.mat')就默认拿到 40 × 40 × 7680 的张量。
输出 shape 这一步值得打印一次:(40, 40, 7680)才是真正意义上的 60 秒试次。后续所有分窗、特征提取都建立在这个形状之上。如果发现形状是(40, 40, 7680)却仍然得到 8064 相关的错误,多半是某个地方忘了切基线或忘了把 fs 换成 128。
3. 跑通 deap数据集分类的最小管线:加载、特征、训练一次出准确率
3.1 sentimentclassify-master 这类代码库的常见目录组织
sentimentclassify-master 这类仓库的目标就是让你用一串命令跑出 DEAP 上的分类结果,常见组织方式是把链路拆成独立脚本:数据加载放data_loader.py,特征提取放feature_extractor.py,模型定义与训练放train.py,评估与画图放evaluate.py。它的目录结构大致是:
sentimentclassify-master/ data/ data_preprocessed_matlab/ src/ data_loader.py feature_extractor.py train.py evaluate.py config/ deap_config.yaml我没有必要照搬它的文件名,重点是理解这个拆分逻辑:数据加载与特征提取必须是两个独立阶段,这样你换特征、换模型时才不用重写读取逻辑。实际运行前,先确认data_loader.py里读取的路径指向你解压 DEAP 的目录,这是一个最常见的起步报错,路径对不上时会直接抛FileNotFoundError。
3.2 最小可运行代码:用单被试效价二分类跑通流程
下面这段代码是把整套流程压缩到一个文件里,大概 40 行,跑通后再往工程化方向拆。核心思路是读一个被试、对每个 trial 提取频带特征、构造二分类标签、用 SVM 训练并输出测试准确率。
# train_baseline.py import numpy as np import scipy.io as sio from scipy.signal import welch from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split from sklearn.svm import SVC FREQ_BANDS = { 'delta': (1, 4), 'theta': (4, 8), 'alpha': (8, 13), 'beta': (14, 30), 'gamma': (30, 45), } def extract_feature(trial, fs=128): win = fs * 4 feats = [] for ch in trial: f, psd = welch(ch, fs=fs, nperseg=win) for lo, hi in FREQ_BANDS.values(): mask = (f >= lo) & (f <= hi) de = 0.5 * np.log2(psd[mask].mean() + 1e-12) feats.append(de) return np.array(feats) def load_subject(path): raw = sio.loadmat(path) data = raw['data'][:, :, 384:].astype(np.float32) labels = raw['labels'].astype(np.float32) return data, labels X, labels = load_subject('data_preprocessed_matlab/s01.mat') X_feat = np.array([extract_feature(t) for t in X]) y = (labels[:, 0] > 5).astype(int) X_tr, X_te, y_tr, y_te = train_test_split( X_feat, y, test_size=0.25, random_state=42, stratify=y) clf = make_pipeline( StandardScaler(), SVC(kernel='rbf', C=1.0, gamma='scale', class_weight='balanced') ) clf.fit(X_tr, y_tr) print('S01 valence ACC: %.3f' % clf.score(X_te, y_te))这段代码里有几个参数值得说明。win = fs * 4决定 Welch 方法里每个窗的长度,窗口越长频率分辨率越高,代价是窗口内的平稳性假设变弱;DEAP 的 60 秒试次用 4 秒窗是常见选择。nperseg必须小于等于信号长度,如果某个通道信号比窗口短,welch会直接报错,窗口设置超过 60 秒也会在这里翻车。
0.5 * np.log2(psd[mask].mean() + 1e-12)是对微分熵的简化近似。1e-12是数值保护项,避免频带能量为 0 时对 0 取对数。SVM 的class_weight='balanced'用于处理二分类标签不平衡,DEAP 按阈值切分后往往不是严格的 1:1,这个参数能防止模型整体往多数类偏。
3.3 每个试次只有一个样本?加滑动窗口才是正解
上面的代码一次运行只有 40 个 trial,训练集只有 30 个样本,score的方差非常大,跑十次准确率在 0.4 到 0.8 之间跳动都正常。想要稳定结果,必须把每个 trial 切成长度为数秒的窗口。
我一般在 4 秒窗口、2 秒步长下做滑动窗口,每个 60 秒的 trial 得到约 28 个窗口。特征提取逻辑与上面相同,区别只在外面多一层窗口循环。窗口之间虽然存在重叠,但在情绪识别实验里这是常规做法,因为相邻窗口的标签相同,重叠加倍扩充样本量,同时也引入了更强的时序冗余。
def extract_windows(data, fs=128, win_sec=4, step_sec=2): win = fs * win_sec step = fs * step_sec windows, label_idx = [], [] for t in range(data.shape[0]): trial = data[t] for start in range(0, trial.shape[1] - win + 1, step): seg = trial[:, start:start + win] windows.append(extract_feature(seg)) label_idx.append(t) return np.array(windows), np.array(label_idx)注意这里label_idx先存 trial 编号,再在训练前把它映射成 valence 二分类标签,避免在窗口循环里反复判断。窗口之后单个被试的特征矩阵一般有 1000 行以上,分类结果才谈得上稳定。这时候回来的准确率会下降一些,因为样本量大了、窗口间的噪声也被算进去了,但这个数字更接近真实水平。
3.4 评价指标与结果口径
单看准确率在 DEAP 上很容易被误导。试次少时 0.8 可以有很高偶然性,试次多时 0.65 可能是很扎实的结果。更稳妥的做法是同时打印 balanced accuracy、F1 和混淆矩阵,尤其是当二分类正负样本比例不是 1:1 时,靠准确率判断好坏基本是玄学。classification_report可以一次给出这三项,建议作为默认输出。
另一个口径问题是“单被试内分类”和“跨被试分类”。单被试内随机切分,模型见过同一个人的部分脑电模式,准确率天然偏高;跨被试用留一被试交叉验证,模型要面对没见过的被试,准确率会掉下来,但这才是有实用价值的情绪识别。两者不能混着比,第四章会专门聊模型与验证方式怎么匹配。
4. DEAP 情绪识别的特征与模型选型:微分熵、分窗和分类器怎么配
4.1 为什么微分熵是 DEAP 上最稳的频带特征
微分熵在 DEAP 上的流行不是偶然。生理信号分类里,时域方差、均值这类统计特征容易受到个体基线差异影响;频带能量特征与情绪效价的关联更稳定。微分熵的定义针对连续随机变量的熵,在 EEG 频带近似服从高斯分布的假设下,它刻画了频带能量在 log 域的不确定性,数值上等于对功率谱密度取对数再做常数调整。情绪状态变化时,不同频带的微分熵变化比原始幅度更能反映脑电节律的重分布。
DEAP 的 4–45Hz 带通滤波决定了可用频带:delta、theta、alpha、beta、gamma 这样切是标准做法,我在前面代码里就是按这个频带表做的。其中 30–45Hz 的 gamma 带在 DEAP 的情绪识别论文里出现频率很高,因为它与注意唤醒关系密切。建议不要省略 gamma,只保留 alpha、beta 会把模型的信息来源限制在较小范围。
频带边界本身也值得调。默认的(1, 4)在 128Hz 采样下分辨率足够,但如果你把窗口缩到 2 秒,频率分辨率降到 0.5Hz,delta 和 theta 的边界处会混入噪声。我一般先固定 4 秒窗口,再确定频带表,不要同时调两个变量。
4.2 标签映射:把 1–9 评分变成分类目标
DEAP 的四类标签都是连续评分,二分类是最容易入手的设定。常见阈值是 5:评分大于 5 算高分组,小于等于 5 算低分组。这个切法没有统一标准,与 DEAP 原文使用的自我评估模型口径也有关。另一个选择是按每个被试评分的中位数切,保证每类样本数量接近,但高/低分组含义会因被试而异。如果目标是对齐已发表论文,先读那篇论文的切分方式,再照做;如果要建立自己的评估协议,固定 5 分阈值更容易跨被试比较。
四分类一般将 valence 和 arousal 各自按阈值切成高/低,组合成 (HVHA, HVLA, LVHA, LVLA) 四个象限。这类任务对特征数量要求更高,单纯五频带微分熵做四分类会到 0.4 上下,和随机水平没有拉开差距,需要更多通道选择与更复杂的模型。如果你第一次跑就想出四分类的漂亮结果,大概率会失望;先把二分类做稳,再一步步扩类。
4.3 模型选型:SVM、随机森林与浅层 CNN 的边界
SVM:样本量几千到两万之间表现稳定,RBF 核配合标准化后的特征矩阵训练快、可解释。注意 RBF 的 C 和 gamma 默认值在情绪识别这类低信噪比任务上不一定最优,建议网格搜索 C ∈ {0.1, 1, 10}, gamma ∈ {scale, 0.01, 0.001}。我用这个范围在多数单被试二分类任务里能稳定提升 2–4 个点。
from sklearn.model_selection import GridSearchCV param_grid = { 'svc__C': [0.1, 1, 10], 'svc__gamma': ['scale', 0.01, 0.001], } grid = GridSearchCV(clf, param_grid, cv=5, scoring='balanced_accuracy') grid.fit(X_tr, y_tr) print(grid.best_params_, grid.best_score_)这段网格搜索直接复用 3.2 节的clf对象,svc__C里的前缀是因为 clf 是 pipeline,参数名要带步骤名。cv=5在窗口级样本量上千时足够,不需要更大的折数,否则每个折训练样本太少。
随机森林:对特征单调性和尺度更不敏感,但维度到了几百以后,每棵树都要在大量特征里找分裂点,训练时间和内存都会上升。它的好处是特征重要性可直接输出,用来回答“哪个频带最重要”这类问题。当特征数量不足时容易过拟合,需要注意限制树深度。
浅层 CNN:将原始窗口或频带谱图作为输入。常见的做法是把一个窗口的 32 通道数据排成 (32, 时间) 的矩阵,用一维卷积沿时间维度扫,后面接全局平均池化。CNN 的好处是端到端学习特征,不再手工指定频带,坏处是 32 个被试的个体差异大,网络容易记住被试身份而不是情绪状态,跨被试验证时性能会明显回落。小数据集上建议先用 SVM 跑通基线,再考虑深模型。
4.4 一条默认参数表:第一次跑就这么设
如果你不想从零调参,我习惯用下面这组参数作为起点。窗口 4 秒、步长 2 秒,频带按五段切,特征用微分熵近似,分类器用 RBF SVM,C=1,gamma=scale,样本权重平衡。标准化放在 pipeline 内,评估指标用 balanced accuracy 而非裸准确率。
这组参数在多数 DEAP 二分类任务上不会出大错。单被试 valence 分类大致落在 0.7–0.85,cross-subject 会跌到 0.55–0.7,如果差太多,先检查标签切分和是否有数据泄漏,而不是急着换模型。
5. DEAP 数据集避坑与常见问题排查:五条实战记录
5.1 数据形状里多出来的 384 点:8064 与 7680 的差异
现象:读取s01.mat,打印 data 形状是 (40, 40, 8064),而按 60 秒乘 128Hz 期望是 (40, 40, 7680)。
原因:每个 trial 记录的是 3 秒基线与 60 秒刺激,63 秒合计 8064 点,preprocessed 文件没有把基线单独切开。工具链里一旦有人按 60 秒写死形状,后面所有窗口索引都会错位。
解决:统一在加载后按data[:, :, fs*3:]丢弃前 3 秒再做后续处理。检查窗口提取、Welch 的 nperseg 是否超过 7680 也以这一步之后的长度为准。如果忘了去基线,前 3 秒的静息态数据会稀释刺激段特征,分类性能通常会有几个点下降。
5.2 阈值切分失效:valence 为什么不能直接拿 5 当全局阈值
现象:用labels[:, 0] > 5切高/低分组,打印标签分布发现有的被试高分组占 80%,有的被试低分组占 70%,模型训练后准确率很高但 F1 很差。
原因:DEAP 的评分 1–9 只是主观量表,被试的打分习惯不同。某些人普遍打 7、8 分,5 分阈值对他来说就是“负向”的,而另一个人可能只打 2、3 分。全局切分在个体层面会产生严重的不平衡。
解决:要么按被试各自的评分中位数切,要么干脆按领域先验只选评分 4 以下与 6 以上的 trial 做二分类,中间段剔除。后者能减少标签噪声,但样本量下降明显。选定哪种要在你的实验记录里写清楚,因为不同切法得到的准确率不可直接对比。
5.3 数据泄漏:标准化与特征提取位置错了一格
现象:跨被试实验表面准确率高达 0.9,换被试以后掉到 0.6,反复复现找不到原因。
原因:把全部被试数据合并后做StandardScaler.fit,再切训练测试集,测试集的信息通过均值和方差进入了模型。更隐蔽的版本是在训练前用全部数据做特征选择,同样属于泄漏。还有一种很容易踩的:在 3.3 节做窗口切分后直接随机切分训练测试,同一个 trial 的相邻窗口会同时出现在两边,造成乐观估计。
解决:把标准化放进make_pipeline,训练集 fit、测试集只 transform;特征选择放到交叉验证内部,或者干脆不用特征选择。窗口级切分时用GroupShuffleSplit按 trial 编号分组,保证同一个 trial 的窗口不跨集合。
5.4 内存爆炸:32 个被试一次读完后 OOM
现象:先写一个循环把所有s*.mat加载到 list,然后统一预处理,运行到第 25 个被试时MemoryError。
原因:32 个被试的原始数据全部以 float64 驻留内存,单个被试大约 100MB,堆上还有后续特征矩阵,32 份合起来轻松超过物理内存。这是一个典型的“图省事反而更慢”的操作,被 OOM 打断后还得从头加载。
解决:使用“按被试处理”的流程,一次只加载一个被试,特征提取后立即丢弃原始信号;释放用del data外加gc.collect()。windows 的特征矩阵保留,但维度远小于原始张量。如果内存仍然紧张,把特征矩阵分批写入.npy,训练时再按被试读入。
5.5 跨被试与被试内结果差异巨大:评估协议不一致
现象:同一批代码,论文里报 0.85,自己复现只有 0.65,老板质疑结果。
原因:论文用的可能是被试内“同一被试随机切分”协议,而复现时用了“跨被试留一被试”协议。两个协议回答的问题根本不是同一个——前者证明“脑电模式在该个体身上可区分”,后者证明“模型对新人可用”。
解决:先明确实验目标。产品化场景必须用留一被试,统计报告 mean±std 并保留每个被试的单项准确率,方便看个体差异。不要尝试通过调参把跨被试结果硬拉到被试内水平,那是徒劳,窗口重叠率再高也补不回个体差异带来的损失。
6. 验证结果与进阶方向:别让一次准确率骗了你
6.1 让结果可信的三个验证习惯
跑通基线后,我会固定做三件事。第一,用留一被试交叉验证重跑,得到 32 个准确率,看最低被试与最高被试相差多少,落差超过 0.3 时优先排查该被试是否存在脑电伪迹。第二,重复 5 次随机初始化,报告平均与标准差,避免拿单次结果当结论。第三,给每个被试打印混淆矩阵,确认高/低两类的错误不是集中在某一边,否则标签切分可能有问题。
6.2 进阶方向:从频带特征走向时频融合
如果单被试二分类稳定在 0.7 以上,再考虑进阶。一个投入产出比很高的方向是时频融合:对每个 4 秒窗口,把微分熵换成短时傅里叶变换或小波时频图,在时间轴与频率轴上同时保留信息,然后再送入 CNN。另一个方向是叠加 DEAP 的 8 路外周生理特征做多模态输入,皮肤电与呼吸对 arousal 的区分度比脑电更直接。做这两个方向时记得保持同一个评估协议,否则换特征带来的提升会被验证方式的变化淹没。
这几年做 DEAP 情绪识别,我最深的教训是:数据集下载与格式解析占了前期一半的坑,分类器反而是最不花时间的部分。先把固定协议定下来,再谈模型复杂度,结果才经得起推敲。希望帮到你。
本文还有配套的精品资源,点击获取