简介:基于 DEAP 与 SEED-IV 数据库的 EEG 情绪检测研究论文,聚焦二维情绪模型下的内心情绪识别,适合脑机接口、情感计算方向的在校生及科研人员参考。文中采用离散小波变换提取五个频段,从功率、能量、微分熵和时域等维度构造特征,并设计通道级 SVM 分类器配合通道组合器完成情绪状态判定;在 DEAP 上四分类准确率达到 74%、86%、72%、84%,在 SEED-IV 上为 79%、76%、77%、74%,实验设置与结果数据完整,便于对比复现。资源包共 1 个 PDF 文件,大小约 701KB,为期刊全文排版,可离线阅读、标注,也适合作为毕业设计算法选型和论文写作的参考底稿。该资源已有 346 人学习下载,对需要快速梳理基于公开脑电数据集做情绪分类任务、掌握小波特征与 SVM 建模流程的读者尤为实用。
1. 基于 EEG 的情绪分类:从 DEAP、SEED-IV 到 SVM 的最短可行路线
情绪检测不是新鲜事,但当你真正把 DEAP 或 SEED-IV 里的原始脑电读进内存,遇到的第一组词会是:小样本、高噪声、标签主观。很多团队第一反应是直接上深度学习,然后被几十个受试者的样本量和动辄几小时的训练卡住;相比之下,用 SVM 在几十维到几百维特征上先跑通一个可解释的判别器,是复现论文基准、快速验证特征设计最稳妥的路。本文按“数据 → 预处理 → 特征 → 调参与避坑”的顺序,给出一条能从头跑完的情绪检测最小路径。
DEAP 和 SEED-IV 是情绪识别里被引用最多的两个公开脑电数据库。DEAP 提供 32 名受试者、32 通道 EEG,标签是效价、唤醒、支配、喜好四类评分;SEED-IV 则是 62 通道的四分类情绪数据,每个受试者分成多个 session。两者的通道数、基线设计、试次个数完全不同,把代码从一套搬到另一套时最容易出问题。
文章适合三类人:准备复现情绪分类论文的、用公开数据做课程设计或毕业设计的、以及想快速判断 SVM 在脑电任务上值不值得投入的工程师。看完后你能直接搭起一套不泄漏、可复现、结果可信的 SVM 分类流程,也知道哪些参数值得调、哪些地方是坑。
2. 先读懂两个公开数据集:DEAP 和 SEED-IV 的结构差异与预处理
两个数据集虽然都叫“情绪脑电”,但内部组织方式差异很大。DEAP 以试次为单位、每个受试者一个文件,标签是连续评分;SEED-IV 以 session 为单位、情绪是离散类别。先用代码把结构摸清楚,再谈特征和模型。
2.1 DEAP 的数据组织:32 通道、40 试次与效价唤醒标签
DEAP 的原始包里,每个受试者对应一个.mat文件,内部通常是data和labels两个数组。data的形状一般是(40, 40, 8064):40 个试次、40 个通道、8064 个采样点。8064 这个数字来自 3 秒静息基线加上 60 秒情绪诱发视频,采样率 128Hz,计算方式是63 × 128 = 8064。40 个通道里前 32 个是 EEG 通道,后面几个是眼电、肌电等外围信号,特征提取前要把它们分开。
import scipy.io as sio # 按常见文件命名方式读取受试者1,不同来源命名可能不同 raw = sio.loadmat("deap/s01.mat") data = raw["data"] labels = raw["labels"] # data: (40, 40, 8064),labels: (40, 4) n_trials, n_channels, n_points = data.shape print("trials:", n_trials, "channels:", n_channels, "points:", n_points) print("label columns: valence, arousal, dominance, liking") eeg = data[:, :32, :] # 只保留前32个EEG通道 peripheral = data[:, 32:, :]很多版本的 DEAP 文件中,data和labels的键名略有差别,可能是data、labels,也可能是嵌套结构。load 之后先打印.keys()和 shape,比直接瞎索引省时间。标签列顺序一般是效价、唤醒、支配、喜好,做二分类时最常用的是效价和唤醒,按阈值 5 把 1 到 9 的评分切成高低两类。
这种“每受试者一个数组”的组织方式,决定了后面交叉验证时要特别小心:同一个人的 40 个试次不能同时散落在训练集和测试集里,否则模型学到的很可能不是情绪,而是这个人特有的脑电模式。这一点在后面的避坑章节会专门展开。
2.2 SEED-IV 的组织:62 通道、四分类情绪与多 session 结构
SEED-IV 的设计思路和 DEAP 不一样。它采集 62 个通道的 EEG,采样率更高,情绪标签是快乐、悲伤、恐惧、中性四类,每个受试者分多个 session 采集,session 之间隔一段时间,用来测试跨时间稳定性。数据文件常见的是按受试者和 session 拆分,每个文件里是已经切好试次的矩阵。
import scipy.io as sio # SEED-IV 单个 session 数据文件常见结构 mat = sio.loadmat("seed_iv/eeg_1_1.mat") # 不同版本键名不一样,先看结构再取数 print([k for k in mat.keys() if not k.startswith("__")]) # 常见结构: (n_trials, n_channels, n_points) # 每个 trial 里前几秒可能是静息或提示画面SEED-IV 六十二个通道本身就说明一个问题:通道数翻倍,特征维度也翻倍,但每个试次带来的样本量并没有成倍增加。直接用全部通道的原始信号做 SVM,特征维度很容易冲到几千,需要靠频段特征把维度压下来,这一点放到第三章详细说。
另外 SEED-IV 的多 session 设计经常被忽略。同一个受试者三次 session 的数据如果混在一起当成普通样本,等于默认三天里脑电分布完全一样,但实际并非如此。做跨 session 验证时,正确的做法是按受试者、按 session 分别划分,而不是把所有数据扔进同一个矩阵里 shuffle。
2.3 时间轴与基线处理:3 秒静息到底该不该保留
DEAP 每个试次前 3 秒是静息基线,后 60 秒是情绪诱发过程。主流做法是把前 3 秒去掉,只用后 60 秒做分析。原因很直白:情绪刺激呈现之前那段时间里,信号反映的是受试者的即时状态,而不是目标情绪。如果保留基线,模型会学到“观众还没开始看视频时的状态”和效价标签之间的关系,这在实验设计上说不通。
fs = 128 baseline_sec = 3 start_point = baseline_sec * fs # 384 analysis = eeg[:, :, start_point:] # 只保留 60s 诱发段 # 如果需要做差分/相对基线特征 baseline_mean = np.mean(eeg[:, :, :start_point], axis=2, keepdims=True) eeg_relative = analysis - baseline_mean处理基线的另一个常见做法是计算“相对功率”:把诱发段各频带功率除以基线段对应频带功率再取对数。这个做法试图消除受试者当天的整体脑电水平差异,但对情绪分类的提升并不是稳定的,有时反而会因为基线段的估计方差过大带来噪声。我一般先把绝对特征跑一版,再试相对特征,哪个在验证集上稳定就用哪个,不要一上来就叠加太多变换。
基线处理没有统一答案,不同论文的取舍也不一样。关键是你在方法部分写清楚了自己怎么处理,审稿人和复现者才能照做。把“去 3 秒基线”作为默认,把“保留基线做差”作为对比实验,是最省事也最不容易被挑毛病的组合。
3. 特征工程:把原始脑电变成 SVM 能吃的向量
SVM 本身不关心信号是什么,它只吃固定维度的数值向量。脑电的原始采样点维度太高、噪声太重,直接拼成一维向量往往效果很差。这一步需要把每个试次转换成一组低维、有物理意义的特征。
3.1 频带划分与微分熵:为什么 DE 特征在情绪任务里这么常用
情绪脑电研究里最常用的特征不是时域均值,而是各个频带的能量。频带沿用睡眠分期的习惯划分为 delta、theta、alpha、beta、gamma 五段,但情绪任务里 alpha、beta、gamma 的区分度通常更强。简单的做法是计算每个频带的功率谱密度均值,也就是 band power;进一步的做法是计算微分熵,也就是 DE,SEED 系列论文里大量使用这个特征。
DE 在数学上是“把信号近似看成高斯分布后,对其概率密度函数做微分熵运算”,工程实现时可以简化为对频带内功率取对数。这个变换的好处有两个:一是压缩了功率的动态范围,避免个别幅值大的通道主导距离计算;二是让特征分布更接近对称,SVM 的 RBF 核在度量距离时会更稳。
import numpy as np from scipy.signal import welch def band_de(signal, fs=128.0): """计算五个频带的微分熵近似值:log(带内平均功率)""" bands = { "delta": (1, 4), "theta": (4, 8), "alpha": (8, 13), "beta": (13, 30), "gamma": (30, 45), } result = {} for name, (lo, hi) in bands.items(): freqs, pxx = welch(signal, fs=fs, nperseg=fs) mask = (freqs >= lo) & (freqs <= hi) band_power = np.mean(pxx[mask]) result[name] = np.log(band_power + 1e-12) # 加小常数防零 return resultnperseg=fs表示用 1 秒窗口做 Welch 谱估计,这样低频段也能有不少谱线参与平均,不会只有三五根频点撑起一个带。1e-12是防止全零信号取对数时出现负无穷。函数返回的是五个频段的 DE 近似值,对于一个试次,最终特征是把“通道 × 频段”全部串起来:32 通道 × 5 频段就是 160 维。
需要说明的是,严格 DE 公式里还有0.5 * log(2πeσ²)这样的常数项和系数。对 SVM 来说,特征向量所有维度同时乘一个常数、加一个常数,不会改变相对距离,所以工程上直接取log(band_power)是等价近似。如果你要写进论文,建议在方法部分写清楚“采用 log 功率近似微分熵”,避免被同行挑数学细节。
3.2 特征矩阵拼装:按试次切片,别把窗口维度搞混
一个试次有 60 秒信号,可以整段算一个特征向量,也可以切成多个短窗口各算一个特征向量。整段计算的优点是特征稳定,缺点是一个试次只能贡献一个样本,40 个试次太少,SVM 很容易欠拟合。我一般先用 1 秒不重叠窗口切分,每个试次得到 60 个样本,模型有更多数据可学。
def trial_to_features(eeg_trial, fs=128.0, win_sec=1.0): """把一个试次(通道×时间)切成多个窗口,每个窗口生成 通道×频段 特征""" channels, n_points = eeg_trial.shape win_len = int(win_sec * fs) n_windows = n_points // win_len features = [] for i in range(n_windows): seg = eeg_trial[:, i*win_len:(i+1)*win_len] vec = [] for ch in range(channels): de = band_de(seg[ch], fs) vec.extend([de["delta"], de["theta"], de["alpha"], de["beta"], de["gamma"]]) features.append(vec) return np.array(features) # (n_windows, channels*5)窗口长度是一个超参数。1 秒窗口能提供大量样本,但某些情绪成分在时间上可能不是稳定持续的,太短的窗口会让特征噪声变大。0.5 秒到 2 秒都有人用,我用 1 秒作为默认值,然后在验证集上试一下 2 秒。别小看这个参数,它在 DEAP 上往往能带来 3 到 5 个百分点的准确率差异。
3.3 归一化与特征选择:先切训练集,再拟合变换器
SVM 对特征尺度非常敏感。RBF 核的距离公式里,量级大的维度会直接压过量级小的维度,所以必须先做标准化。最容易被忽视的是标准化只能看着训练集算均值和方差,再把这个均值和方差用到验证集上。如果先对全量数据做标准化,再做交叉验证,就是一种温和的数据泄漏,会高估模型表现。
from sklearn.preprocessing import StandardScaler # X_all 形状 (n_samples, n_features) # 正确做法: 先切训练/测试,再 fit 标准化 X_train, X_test, y_train, y_test = train_test_split( X_all, y_all, test_size=0.2, random_state=42, stratify=y_all ) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) # 只 transform,不 fit特征选择方面,全通道全频段拼出来的特征维度并不算高,SVM 本身也不是特别怕冗余特征,我在实际任务里很少做复杂的特征选择。如果某个通道明显有坏数据,比如全零、方差极小,直接删掉通道比任何算法都有效。一个简单的做法是计算每个特征维度的方差,把方差为 0 的维度去掉,这能避免某些数据文件里缺失通道导致的报错。
4. SVM 情绪分类落地:核函数、网格搜索与交叉验证
特征准备好之后,SVM 的使用反而相对直接。情绪分类是小样本、非线性边界的问题,RBF 核是默认选择,但在某些设定下线性核更稳。关键在于参数怎么设、验证怎么切。
4.1 选 RBF 还是线性核:先看样本量和特征维度
RBF 核有两个重要参数:C和gamma。C控制误分类惩罚,越大越容易过拟合;gamma控制单个样本的影响半径,越大决策边界越复杂。在几百个样本、一两百维特征的设定下,RBF 核的拟合能力足够,但也很容易过拟合,尤其是gamma偏大的时候。
线性核没有gamma,只有C,更容易调。对 DEAP 二分类任务,线性核往往能拿到和 RBF 差不多的准确率,因为特征本身已经被频段聚合过,非线性边界带来的提升有限。对 SEED-IV 这种四分类任务,RBF 通常略好一些,但优势也不是压倒性的。
from sklearn.svm import SVC # RBF 核示例,放缩和模型组合成 pipeline pipe = Pipeline([ ("scale", StandardScaler()), ("svm", SVC(kernel="rbf", class_weight="balanced", decision_function_shape="ovo", random_state=42)) ])decision_function_shape="ovo"的意思是多分类时使用 one-vs-one 策略,这是 SVM 原生策略,结果会比 one-vs-rest 在类别不均衡时更稳定。class_weight="balanced"会自动按类别频率加权,四分类情绪里如果某个类别样本偏少,这个参数能防止模型全体预测到多数类。
4.2 网格搜索与交叉验证:按受试者分组是关键
网格搜索参数时,交叉验证的划分方式比网格本身更重要。先看两个错误做法:一是把所有受试者的窗口混在一起直接 KFold,这是最常见的虚高来源;二是把每个受试者单独训练测试,得到指标后再平均,这是“受试者相关”的设定,结果通常偏高。
真正有意义的是“跨受试者”设定:一个受试者的所有窗口只能出现在训练集或者测试集,不能两边都出现。实现上可以用GroupKFold,把受试者编号当作 group。
from sklearn.model_selection import GroupKFold, GridSearchCV # groups 是每个样本所属的受试者编号 groups = subject_ids_for_each_window param_grid = { "svm__C": [0.1, 1, 10, 100], "svm__gamma": [0.001, 0.01, 0.1, 1, "scale"], } gkf = GroupKFold(n_splits=10) # 10折,同一受试者不会同时进训练和测试 grid = GridSearchCV( pipe, param_grid, cv=gkf, scoring="f1_macro", n_jobs=-1 ) grid.fit(X_all, y_all, groups=groups) print(grid.best_params_, grid.best_score_)GroupKFold的 10 折会把所有受试者分成 10 组,每折拿走一组做测试。这样评估的是“新受试者”的表现,更接近真实使用场景。需要提醒的是,如果窗口来自同一个用户且重叠过多,跨受试者分组还不够,还要保证同一个试次的两个重叠窗口不跨训练测试集,这个在避坑章里细说。
4.3 评价指标:只看准确率会骗自己
DEAP 二分类任务里,多数论文报告准确率,但正负样本其实可能不是严格 50:50,只看准确率容易被占比高的一类拉高。SEED-IV 四分类任务更是这样,如果四类样本数量不完全一致,宏平均 F1 比准确率更能反映真实水平。
from sklearn.metrics import classification_report, confusion_matrix y_pred = grid.predict(X_test) print(classification_report(y_test, y_pred, digits=3)) print(confusion_matrix(y_test, y_pred))classification_report会同时给出每个类别的精确率、召回率、F1,以及宏平均和加权平均。我一般看两行:宏平均 F1 和各类别的召回率。如果某一个情绪类别的召回率明显低于其他类别,比如 SEED-IV 里悲伤和中性经常互相混,模型整体准确率再高也不适合上线使用。
公开数据集上,DEAP 二分类的 RBF 或线性 SVM 基准在 80% 到 87% 左右,SEED-IV 四分类在 70% 到 80% 之间,具体数值受特征窗口、通道选择、是否跨受试者影响很大。如果跑出来的结果明显高于这个区间,先怀疑评估流程是否有泄漏,而不是沾沾自喜。
5. 基于 EEG 的情绪 SVM 避坑指南:五个必看陷阱和排查方法
这一章专门写我在这类任务里最常见的五个坑。每条按现象、原因、解决方式展开,希望能帮你省下反复试错的时间。
5.1 全量标准化引发的数据泄漏
现象:交叉验证准确率 92%,看起来非常漂亮,但换到独立受试者上只有 74%,前后差距大到不合常理。原因是把StandardScaler放在切分之前,对全量数据 fit 了均值和方差。测试集的统计信息混进了训练过程,模型相当于间接看到了测试集的尺度分布。
解决方式:把标准化放进Pipeline,让GridSearchCV或者手动切分在每一折内部重新 fit。只要标准化器不在交叉验证循环外部对整个X调用fit,这个泄漏就能避免。排查时直接看代码里有没有对X_all调scaler.fit。
5.2 同一受试者的窗口同时出现在训练集和测试集
现象:个人相关验证准确率 88%,跨受试者验证只有 64%。原因很典型:没有使用分组交叉验证,同一个人的 40 个试次、60 个窗口被随机打散,训练集里学到的个体脑电特征直接帮助了测试集。这不算模型学会了情绪,而是学会了认人。
解决方式:用GroupKFold并把受试者编号传入groups。如果目标是新受试者泛化,这是最低要求。论文里要在方法部分明确写“受试者独立划分”,否则审稿人大概率会质疑结果。
5.3 滑动窗口重叠造成的样本重复
现象:用 1 秒窗、0.5 秒步长切窗后,准确率上升了 2 到 3 个百分点,但细看发现相邻窗口共享一半信号。原因是重叠窗口生成高度相似样本,训练集和测试集之间可能存在近似重复,模型等于看到了模板匹配。
解决方式:步长大于或等于窗口长度,即采用不重叠窗口。如果一定要重叠窗口来增广,必须保证任意一个原始试次的全部窗口都落在同一侧,也就是分组划分时以试次为粒度,而不是以窗口为粒度。这一点在写代码时很容易漏掉。
5.4 类别不均衡时盲目追求准确率
现象:四分类任务里,中性类别样本占比 40%,其他三类各 20%,模型全部预测中性就能拿到 40% 准确率。调参后准确率提了,但混淆矩阵里有两类几乎不预测。原因是没看每类别召回率,也没用类别均衡机制。
解决方式:给SVC设置class_weight="balanced",评价指标改用f1_macro。网格搜索的 scoring 也应该设为f1_macro,否则参数选择过程本身会偏向占比高的类别。
5.5 随机数种子对结果影响过大
现象:同一套代码跑三次,准确率分别是 81.2%、76.5%、79.8%,差异大到没法比较两个特征方案的好坏。原因是数据切分、模型初始化里的随机数没有固定,少数受试者的划分变化对几百个样本的模型影响很大。
解决方式:所有可能引入随机的地方都固定random_state,包括train_test_split、SVC、GridSearchCV。在论文里报告多次种子下的均值加减标准差,而不是只报最好一次。我现在做这类实验的习惯,是直接固定random_state=42跑完整套流程,然后换一个种子整体重跑一遍,两次结论一致才敢写进文档。
6. 从最小实验起步,把 SVM 情绪分类跑出可信结果
如果你只想先跑通一版,不需要从头写特征工程。假设你已经按第三章的方式得到了特征矩阵X_all、标签y_all和受试者编号groups,下面这个脚本可以直接作为实验起点。
import numpy as np from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC from sklearn.model_selection import GroupKFold, cross_val_score X_all = np.load("features.npy") y_all = np.load("labels.npy") groups = np.load("subject_ids.npy") pipe = Pipeline([ ("scale", StandardScaler()), ("svm", SVC( kernel="rbf", C=10, gamma="scale", class_weight="balanced", random_state=42 )) ]) cv = GroupKFold(n_splits=10) scores = cross_val_score( pipe, X_all, y_all, groups=groups, scoring="f1_macro", cv=cv, n_jobs=-1 ) print("F1 macro: %.3f ± %.3f" % (scores.mean(), scores.std()))跑完这版之后,我有三个习惯性的验证动作。第一,打印混淆矩阵,看哪些类别互相混淆;如果悲伤和中性大面积互相串,说明特征里区分这两个类别的信息不足,优先加时域特征或者换频段权重。第二,对比线性核和 RBF 的宏平均 F1,如果差距小于 1 个百分点,我会选线性核,因为可解释性更好、参数更少。第三,统计每个受试者的预测结果,看看是不是集中在某几个人特别差,这通常是通道质量或个体基线差异导致的。
作为第一人称的教训:我在早期做 DEAP 二分类时,某次实验结果跑到 89%,当时第一反应是调参有效,后来发现窗口重叠加全量标准化两个问题叠加,折半之后真实水平只有 82% 左右。从那以后,我对每个实验步骤都先问一句“这一步有没有可能让测试集信息提前泄漏”,再往下走。情绪检测这个方向本身并不缺模型复杂度,缺的是用同一套标准流程对比不同特征时的确定性。希望这篇笔记能帮你在 DEAP 或 SEED-IV 上少走几个弯路,把精力留在真正值得研究的问题上。
本文还有配套的精品资源,点击获取