☰
基于SVM的脑电情绪检测:从特征工程到通道融合实战
2026/10/27 19:06:56 网站建设 项目流程

简介:这份资源是一篇面向情感计算与脑机接口方向学习者的研究论文,聚焦基于EEG信号的情绪检测,适合做毕业设计、课程研究或算法复现的高校学生与初级研究者参考。论文以DEAP和SEED-IV两个公开数据库为数据来源,采用离散小波变换将预处理信号分解为5个频段,提取功率、能量、微分熵与时域等特征,并构建通道级SVM分类器与通道组合器完成二维情绪模型下的状态识别,四类分类准确率在DEAP上为74%、86%、72%、84%,在SEED-IV上为79%、76%、77%、74%。资源包共1个PDF文件,约701KB,完整呈现论文的摘要、方法、实验设计与结果讨论,便于读者梳理EEG情绪识别的特征工程与分类流程,对照复现实验并理解支持向量机在多通道生理信号中的应用思路。目前已有346人学习下载。

1. 从一段 32 通道脑电说起:这个 SVM 情绪检测项目到底能跑出什么

想象你手里有一份 32 通道、500Hz 采样的脑电记录,被试刚看完一段 1 分钟的视频,你要回答的问题不是“他看到了什么”,而是“他此刻处在高唤醒高愉悦还是低唤醒低愉悦”。这个项目干的就是这件事:把 DEAP 和 SEED-IV 两个公开脑电数据库的原始信号,经过陷波、中值、带通三级滤波,用 8 层 db8 离散小波变换拆出 delta 到 gamma 五个频段,再抽取时域统计量、频段能量、微分熵等 34 维特征,最后按通道各训一个 SVM,用通道融合投票把情绪判成 HAHV、HALV、LALV、LAHV 四类。论文给出的分类率在 DEAP 上是 74%、86%、72%、84%,SEED-IV 上是 79%、76%、77%、74%。它适合谁?做脑机接口课程设计、情感计算毕业设计、想拿公开数据把“特征工程 + 传统分类器”这条链路完整走一遍的开发者。整套流程不依赖深度学习的算力,一台普通笔记本就能复现主干,这也是它作为毕业设计选题的实用价值所在。

2. 数据与信号链路:从原始脑电到 34 维特征向量

2.1 两个数据库的差异先摸清楚

动手之前必须把两个库的“形状”对齐,否则后面特征矩阵的维度会直接对不上。DEAP 是 32 名被试、32 通道、500Hz 采样,每人 40 个 1 分钟视频 trial;SEED-IV 是 15 名被试、每人 3 个不同日期的 session(合计 45 份数据)、64 通道、1000Hz 采样,每段视频 2 分钟。论文里为了统一,只取两者共有的通道,并把 SEED-IV 降采样。这个取舍很关键:通道数不一致时,通道级分类器的数量就没法复用,所以先做通道交集是省事的做法。

维度DEAPSEED-IV
被试数3215(3 session,共 45 份)
通道数3264(取交集后对齐)
采样率500Hz1000Hz(需降采样)
单 trial 时长1 分钟2 分钟
标签体系valence/arousal 二维四类离散情绪

事件切分这一步,DEAP 原始数据是连续记录,需要按采集协议把每个 trial 切出来存成 mat;SEED-IV 官方给的已经是切好的 mat,直接读即可。我一般会先写一个脚本把两个库都转成统一的(n_trials, n_channels, n_samples)三维数组,后面所有处理都基于这个中间格式,避免每换一个库就改一遍下游代码。

2.2 三级滤波:陷波、中值、带通一个都不能少

预处理是这条链路里最容易翻车的地方。原始脑电里混着工频干扰、电极跳变引起的脉冲噪声,还有远超脑电有效带宽的高频成分。论文用了三级串联滤波,顺序有讲究:先陷波去工频,再中值去脉冲,最后带通限带。用 MATLAB 的medfilt和filtfilt实现,Python 侧对应scipy.signal.medfilt和scipy.signal.filtfilt。

import numpy as np from scipy.signal import iirnotp, filtfilt, medfilt, butter def preprocess_eeg(raw, fs=500): # 第一级:50Hz 陷波,去掉工频干扰(当地电网 50Hz 时用 50,60Hz 电网改 60) b_notch, a_notch = iirnotp(50, 30, fs) x = filtfilt(b_notch, a_notch, raw, axis=-1) # 第二级:1 维 10 阶中值滤波,压制电极跳变等脉冲噪声 # 注意 medfilt 的 kernel 必须是奇数,10 阶对应 kernel_size=11 x = medfilt(x, kernel_size=11) # 第三级:20 阶带通 0.1~60Hz,把信号限制在脑电有效频段 b_bp, a_bp = butter(20, [0.1, 60], btype='band', fs=fs) x = filtfilt(b_bp, a_bp, x, axis=-1) return x

逐段说清楚:iirnotp(50, 30, fs)里 50 是陷波中心频率,30 是品质因数 Q,Q 越大陷波越窄,30 是个折中值,太窄压不住工频漂移,太宽会把 50Hz 附近的 gamma 成分一起削掉。medfilt的kernel_size=11对应论文的 10 阶,阶数必须是奇数,写 10 会直接报错,这是新手第一个坑。带通的butter(20, [0.1, 60], ...)里 20 是滤波器阶数,阶数越高过渡带越陡但相位失真越大,配合filtfilt做零相位正反两遍滤波可以把相位失真抵消掉,代价是等效阶数翻倍。filtfilt要求输入长度大于 3 倍滤波器阶数,短 trial 上会抛异常,所以切事件时别切得太碎。

提示:陷波频率要跟采集环境的电网频率一致,用错了不但去不掉干扰,还会在频谱上留一个假凹陷,后面特征全被带偏。

2.3 8 层 db8 小波分解与五频段特征

滤波完的信号送进离散小波变换。论文用 db8 小波做 8 层分解,得到近似系数和细节系数,再重组出 delta(1-3Hz)、theta(4-7Hz)、alpha(8-13Hz)、beta(14-30Hz)、gamma(31-60Hz) 五个频段。为什么是 8 层?因为 500Hz 采样下每层分解把频带上限砍半,8 层之后最低频段正好落到 delta 的量级,能把这五个经典节律干净地分开。

import pywt def band_decompose(signal, wavelet='db8', level=8, fs=500): # 8 层分解,返回近似系数 a 和各级细节系数 d coeffs = pywt.wavedec(signal, wavelet, level=level) # 用零填充重构出各频段,便于单独算能量 bands = {} names = ['gamma', 'beta', 'alpha', 'theta', 'delta'] for i, name in enumerate(names): c = [np.zeros_like(cf) for cf in coeffs] c[-(i + 1)] = coeffs[-(i + 1)] bands[name] = pywt.waverec(c, wavelet)[:len(signal)] return bands

pywt.wavedec返回的系数列表是[cA8, cD8, cD7, ..., cD1],索引从后往前对应从高频到低频,所以重构第 i 个频段时把对应那一层细节系数保留、其余置零再waverec。waverec重构出来的长度可能比原信号多一两个点,用切片截断对齐。每个频段上再算四类量:频段能量(系数平方和)、功率谱密度、微分熵、平均频带功率。微分熵在论文里是重点特征,对高斯分布近似下等于0.5 * log(2πeσ²),实现时直接对频段信号求方差再套公式即可。

时域那 12 个统计特征(均值、RMS、标准差、一阶差分、归一化一阶差分、二阶差分、归一化二阶差分、峰度、偏度、方差、Hjorth mobility、Hjorth complexity)加上 Hurst 指数和排列熵,凑成 34 维。这里有个容易忽略的点:Hjorth 的 mobility 和 complexity 是递推定义的,mobility 是一阶导数标准差比原信号标准差,complexity 是导数 mobility 比原 mobility,算的时候别把顺序搞反。

3. 通道级 SVM 与融合投票:32 个分类器怎么协同

3.1 特征矩阵的构造与训练测试划分

论文的做法是“每个通道一个分类器”,所以特征矩阵是按通道组织的。DEAP 用 22 名被试训练、10 名测试,每个通道的特征向量是 880×34(22 人 × 40 trial = 880 行,34 个特征列);SEED-IV 用 32 名被试训练、13 名测试,每通道 742×34(32 人 × 24 事件 ≈ 742 行)。注意这里是按被试划分而不是按 trial 随机划分,这是防数据泄漏的关键——同一被试的 trial 之间高度相关,随机划分会让测试集“见过”训练集的人,准确率虚高。

import numpy as np from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline def train_channel_classifiers(X_train, y_train, n_channels=32): # X_train: (n_samples, n_channels, n_features) models = [] for ch in range(n_channels): clf = make_pipeline( StandardScaler(), # 34 维量纲差异大,必须标准化 SVC(kernel='rbf', C=1.0, gamma='scale') # RBF 核处理非线性边界 ) clf.fit(X_train[:, ch, :], y_train) models.append(clf) return models

StandardScaler不能省:能量特征动辄上千,偏度峰度在个位数,不标准化的话 RBF 核的距离计算会被大数值特征完全主导。SVC的三个关键参数——kernel选 rbf 是因为四类情绪在特征空间里不是线性可分的;C是正则化强度,越大越容易过拟合,论文没给具体值,我一般从 1.0 起步在验证集上网格搜;gamma='scale'让 sklearn 按1/(n_features * X.var())自动定,比手写'auto'稳。四分类用SVC默认的 one-vs-one 策略,32 个通道各训一个,总共 32 个模型。

3.2 通道融合:多数投票还是概率加权

32 个分类器各给一个预测,怎么合成最终情绪状态?论文叫“channel combiner”,最直接的是多数投票。但多数投票有个问题:某些通道(比如额区)对情绪判别力强,枕区弱,一票一票平权会稀释强通道的信号。更稳的做法是用decision_function的输出做加权投票,权重可以按各通道在验证集上的单通道准确率来定。

def fuse_channels(models, X_test, weights=None): # X_test: (n_samples, n_channels, n_features) n_samples = X_test.shape[0] n_classes = len(models[0].classes_) votes = np.zeros((n_samples, n_classes)) if weights is None: weights = np.ones(len(models)) / len(models) for ch, clf in enumerate(models): # decision_function 在 ovo 下形状是 (n_samples, n_classes*(n_classes-1)/2) scores = clf.decision_function(X_test[:, ch, :]) pred = clf.predict(X_test[:, ch, :]) for i, p in enumerate(pred): votes[i, p] += weights[ch] return np.argmax(votes, axis=1)

这段里votes[i, p] += weights[ch]就是加权投票的核心:每个通道投给预测类别的票数等于它的权重。权重怎么来?在训练集上做一次留一被试交叉验证,统计每个通道单独分类的准确率,归一化后当权重。实测下来额区和颞区的通道权重普遍偏高,跟情绪加工脑区的先验知识对得上,这也算是个 sanity check——如果权重分布完全随机,多半是特征或标签出了问题。

注意:decision_function在 one-vs-one 多分类下的输出维度是n_classes*(n_classes-1)/2,不是n_classes,直接拿去做 softmax 会维度错位。要么用predict_proba(需要probability=True,训练会慢),要么就老老实实用predict做投票。

3.3 复现论文指标:74%/86%/72%/84% 怎么对上

论文报的四类准确率是每类单独算的,不是整体准确率。复现时要用classification_report看 per-class recall,别只看accuracy_score。DEAP 上 class2(HALV)能到 86%,class3(LALV)只有 72%,这个不均衡很典型——低唤醒低愉悦的样本在 DEAP 里相对少,且边界模糊。如果复现出来整体 accuracy 在 75% 上下但 per-class 差异很大,先别怀疑代码,去看类别分布,必要时对少数类做重采样或调class_weight='balanced'。SEED-IV 四类比较均衡(79/76/77/74),复现难度反而低一些,建议先拿 SEED-IV 跑通再上 DEAP。

4. 避坑与排查:这条链路上最容易翻车的五个地方

4.1 现象:小波重构后信号长度对不上,特征矩阵报错

原因:pywt.waverec重构长度受分解层数和信号长度影响,可能比原信号多 1~2 个采样点,直接拿去算特征会导致不同 trial 的特征维度不一致。解决:重构后统一[:len(signal)]截断,或者在切事件时保证每个 trial 长度是 2^level 的整数倍,从源头消除边界效应。

4.2 现象:SVM 训练准确率 99%,测试集只有 50%

原因:按 trial 随机划分训练测试集,同一被试的相邻 trial 高度相关,模型实际上在“背人”。解决:严格按被试划分,DEAP 用 22 人训练 10 人测试,SEED-IV 用 32 人训练 13 人测试,跟论文保持一致。划分完再检查一遍训练集和测试集的被试 ID 有没有交集。

4.3 现象:SEED-IV 降采样后频段能量整体偏移

原因:1000Hz 降到 500Hz 前没做抗混叠低通,高于 250Hz 的成分折叠进低频,污染了 gamma 频段。解决:降采样前先过一遍截止 250Hz 的低通(scipy.signal.decimate内部自带),或者直接用resample_poly做多相滤波重采样,别用简单的隔点抽取。

4.4 现象:中值滤波后波形出现平台,细节丢失

原因:kernel_size设太大,10 阶中值在 500Hz 下对应 20ms 窗口,对短时脑电事件来说窗口偏宽,会把快速变化抹平。解决:中值滤波只用来去脉冲,窗口控制在 5~11 个采样点,去完脉冲后靠带通滤波补回频带限制,两级分工别混。

4.5 现象:通道融合后准确率反而低于单通道最好的那个

原因:弱通道的噪声投票拉低了强通道的判断,等权投票在通道质量差异大时会拖后腿。解决:按验证集单通道准确率加权,或者先做通道筛选,把单通道准确率低于随机水平(四分类是 25%)的通道直接剔除再融合。

5. 二次开发:把论文代码改成能落地的实验框架

这套流程跑通之后,真正有价值的是把它改成一个可复用的实验框架,而不是每次换数据集就重写一遍。我的做法是把链路拆成四个可替换的组件:loader(读 DEAP/SEED-IV 并统一成三维数组)、preprocessor(三级滤波 + 小波分解)、extractor(34 维特征,支持按名字增删)、classifier(通道级 SVM + 融合策略)。每个组件定义一个基类,换数据集只换 loader,换特征只改 extractor 的配置列表。

FEATURE_CONFIG = { 'time': ['mean', 'rms', 'std', 'kurtosis', 'skewness', 'hjorst_mobility', 'hjorst_complexity'], 'freq': ['band_energy', 'psd', 'diff_entropy', 'avg_band_power'], 'other': ['hurst', 'perm_entropy'], } def build_features(bands, cfg=FEATURE_CONFIG): feats = [] for group, names in cfg.items(): for name in names: feats.append(FEATURE_FUNCS[name](bands)) return np.concatenate(feats, axis=-1)

这样改的好处是消融实验变得极便宜:想验证微分熵到底贡献多少,把diff_entropy从配置里删掉重跑一遍就行,不用动任何处理逻辑。我一般会先跑一个全特征基线,再逐组剔除,看 per-class recall 掉多少,掉得最多的那组就是这套数据上的关键特征。论文里没做消融,但这恰恰是二次开发最该补的实验——审稿人或答辩老师最爱问“你这 34 维里哪些真有用”。

另一个值得改的点是融合策略的参数化。把投票方式(等权/加权/概率平均)、通道筛选阈值、SVM 的 C 和 gamma 都抽成配置,用sklearn.model_selection.GridSearchCV在训练集内部做嵌套交叉验证选参,而不是在测试集上试。我见过太多毕设直接在测试集上调参调到 90% 然后写进论文,答辩时被问一句“测试集参与调参了吗”就露馅。

验证方法上,除了 per-class recall,建议再加一个混淆矩阵和通道权重热力图。混淆矩阵能看出哪两类最容易混(DEAP 上 HALV 和 HAHV 经常互串,因为唤醒度边界在特征空间里本来就模糊);通道权重热力图按 10-20 系统画到头皮上,能直观看到哪些脑区在起作用,这个图放进毕设报告里比一张准确率表格有说服力得多。

最后说个血泪教训:我最早复现时图省事,把两个库的预处理参数写死成同一套,结果 SEED-IV 因为采样率是 1000Hz,同样的kernel_size=11中值窗口实际时长只有 DEAP 的一半,去脉冲效果差一截,gamma 频段能量一直偏高,查了两天才定位到是窗口时长没随采样率换算。从那以后我每次写滤波参数都强制用“秒”做单位、进函数再乘fs换算成采样点,再也不敢直接写死点数。希望这个习惯能帮你少走两天弯路,也希望这套拆解能让你把这份资源真正跑起来、改下去。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询