简介:面向脑电情绪识别入门学习者,这份资源基于公开的DEAP脑电数据集,实现情绪二分类任务。流程覆盖快速傅里叶变换、信号预处理与模型训练,以简单实用的方式展示完整的分析路径。压缩包共包含7个文件,大小约8KB,内有4个Python脚本,分别对应特征提取、傅里叶变换、脑电绘图与主流程;另有Markdown说明文档、备份文件和一个附赠压缩包,模块划分清楚,便于分段查阅与二次开发。目前已有134人学习。通过本资源可掌握脑电信号频域转换、滤波去噪、去伪迹与归一化等预处理方法,并直接调用决策树、支持向量机与最近邻模型完成二分类实验。README与附赠内容可辅助快速上手,简洁的代码结构很适合新手在此基础上继续尝试深度学习模型。
1. DEAP脑电情绪二分类:这份代码把整条链路拆成了三段
如果你刚接触脑电情绪识别,第一次拿到DEAP数据集,最难受的不是不懂算法,而是不知道从哪下手:几十个.mat文件躺在目录里、每段信号上千个采样点、论文里那个准确率怎么也算不出来。这个基于DEAP脑电数据集的情绪二分类项目,恰好把整条链路拆成很直白的三段——FFT频域转换、特征提取、模型训练。模型只用了决策树、SVM、KNN三个经典分类器,全部直接调库,没有任何自定义网络结构,代码能一行行跟读。它解决的核心问题,是让你用最少的前置知识完整走通“原始脑电→频域特征→情绪二分类”的流程,在拿到准确率的同时理解每个环节在干什么。适合刚进组的研究生,也适合想快速建立EEG加机器学习实战经验的自学者。
2. 数据读取与预处理:先把 .mat 里的脑电信号变成能进模型的矩阵
2.1 数据结构:DEAP 的 data 和 labels 到底怎么读
DEAP数据集是典型的.mat打包格式,每个被试一个文件,比如s01.mat。打开之后主要就是两个变量:data和labels。data的张量形状是(40, 40, 8064),分别对应40个试验、40个通道、8064个采样点。这个8064不是随便来的:128Hz采样率乘63秒,前3秒是基线记录,后面60秒是正式刺激阶段。labels的形状是(40, 4),对应每段试验在valence、arousal、dominance、liking四个维度上的评分。如果还没拿到DEAP,直接搜deap数据集下载就能找到官方申请入口,走一遍学术申请流程即可。
读取用scipy.io.loadmat就行,但有两个细节容易忽略。一个是mat文件里变量名大小写,老版本DEAP有的包成字典结构,用squeeze_me=True可以把单维度压掉,避免后面取索引时多出一层空维度。另一个是通道顺序,DEAP官方文档里40通道由32个EEG通道加8个外围生理信号组成,具体顺序白皮书里列过。只关心脑电的话,后面切数据时要留意边界。我一般在读完之后先打印一次data.shape和labels.shape,确认没有读成(1, 40, 40, 8064)这种多包一层的形状,再接下一步。
import scipy.io as sio import numpy as np mat = sio.loadmat('s01.mat', squeeze_me=True) data = mat['data'] # (40, 40, 8064) labels = mat['labels'] # (40, 4) [valence, arousal, dominance, liking] print(data.shape, labels.shape) # 如果发现 data 是 (1, 40, 40, 8064),需要执行 data = data[0]这段代码的作用是加载单个被试的数据并确认维度。squeeze_me=True把尺寸为1的维度压掉,省得手动reshape。打印形状这一步看着啰嗦,但能在预处理之前就暴露读档错误。后面所有通道操作都建立在data第二个维度是40通道这个前提下。
多被试要把数据拼起来。常见做法是循环读取s01到s40共40个文件,沿第0维堆叠成一个大的数据张量,标签同理。样本总数就是40被试乘40试验,一共1600条,这个数字在后面所有模型训练里都会用到。
2.2 标签怎么切成二分类:阈值卡在 5 还是 0.5
DEAP的labels评分范围有两种常见版本:原始评分1到9,另一种经过归一化是0到1。二分类最常用的切法是拿valence(愉悦度)或arousal(唤醒度)其中一个维度做阈值分类,比如valence大于中间值记为正类(高愉悦),小于等于中间值记为负类(低愉悦)。阈值选多少取决于你下载到的版本量纲——1到9用5做分界,0到1用0.5做分界,这个不查清楚,后面所有实验的数字都会对不上。
复现这个项目时我踩过这个坑,第一次跑出的准确率稳定在60%上下,后来发现是把0到1的评分用5去切,所有样本全归成了负类,分类器学到的只是“全部猜负类”的决策规则。写代码时我一般先打印labels的min、max和均值,确认量纲再决定阈值。下面这段是把valence和arousal分别二值化,同时保留了复合标签的可能性,方便之后做四象限分析。
def binarize_labels(labels, dim=0, threshold=5.0): """dim: 0=valence, 1=arousal; threshold 按量纲传 5.0 或 0.5""" y = labels[:, dim].copy() y = (y > threshold).astype(int) return y y_val = binarize_labels(labels, dim=0, threshold=5.0) y_aro = binarize_labels(labels, dim=1, threshold=5.0) print('valence 正类占比: {:.2f}'.format(y_val.mean()))这里阈值按量纲传参,比写死在代码里好维护。打印正类占比是个好习惯,如果正负类比例严重失衡,比如0.9比0.1,说明阈值切得有问题,或者样本本身不均衡,模型后面需要加class_weight处理。两个维度都切还有一个好处:把valence和arousal组合起来可以做四象限,高愉悦高唤醒算一类,其他象限算另一类,直接复用这份二值化代码就能扩成更细的情绪分类任务。
2.3 滤波、基线校正与归一化:三步去掉干扰再谈分类
原始脑电信号直接喂给分类器基本没法用,里面混着工频干扰、眼电伪迹,还有个体间的幅值差异。常用预处理顺序是:先带通滤波保留有效频段,再做基线校正,最后逐通道归一化。滤波我一般用巴特沃斯带通,范围0.5到45Hz,脑电情绪研究关注的主要频段都落在这个区间,50Hz市电工频刚好被滤掉。用filtfilt不是lfilter,前者是零相位滤波,不会让波形在时间轴上产生偏移,这对后续特征提取很关键。
基线校正的逻辑是拿每段试验前3秒的基线数据求均值,然后从整段里减掉,这样能把直流漂移和个体基础电位差异去掉一块。归一化用z-score,每个通道单独算均值和标准差,否则幅值大的通道会在距离计算里主导KNN这类对尺度敏感的模型。
from scipy.signal import butter, filtfilt fs = 128.0 low, high = 0.5, 45.0 b, a = butter(4, [low / (fs/2), high / (fs/2)], btype='band') def preprocess_eeg(eeg): # eeg: (trials, channels, samples) filtered = np.zeros_like(eeg) for t in range(eeg.shape[0]): for c in range(eeg.shape[1]): filtered[t, c] = filtfilt(b, a, eeg[t, c]) # 前 3 秒基线校正 baseline_len = int(3 * fs) baseline_mean = filtered[:, :, :baseline_len].mean(axis=2, keepdims=True) filtered -= baseline_mean # 逐通道 z-score mean = filtered.mean(axis=2, keepdims=True) std = filtered.std(axis=2, keepdims=True) + 1e-6 normalized = (filtered - mean) / std return normalized这段预处理用双层循环遍历每个试验和每个通道,数据量不大时性能可以接受,逻辑上比向量化写法好懂。butter的4阶是经验值,阶数太高相位畸变加大、计算量上升,太低带外衰减不够。filtfilt注意传入的是归一化频率,也就是截止频率除以奈奎斯特频率(fs/2),写错这个参数滤波器就形同虚设。基线校正在滤波之后做,是因为滤波本身会引入瞬态响应,先滤后减更稳。z-score加1e-6防止某个通道方差为零时除零报错。
到这里有人会问,眼电伪迹怎么处理。这个项目偏向入门,所以没上ICA去伪迹,基线校正加滤波对基础任务已经够用。如果后面想追求更高准确率,ICA是值得加的一步,但那是进阶内容,先把当前流程跑通更重要。预处理做完,数据就准备好进频域特征提取了。
3. 特征提取:FFT 频段功率到底怎么算才对
3.1 为什么要从时域转到频域
预处理之后的EEG信号仍然是一个时间序列,但不同情绪状态在时间波形上的差异并不直观。反倒是频域里有个很经典的观察:人在紧张、放松、愉悦等状态下,各频段的能量分布变化明显。脑电研究习惯上把频谱切成几个频段,delta(0.5到4Hz)、theta(4到8Hz)、alpha(8到13Hz)、beta(13到30Hz)、gamma(30到45Hz),情绪识别用得最多的就是这些频段上的平均功率。FFT在这里的作用,是把128Hz采样的时间序列拆解成不同频率分量的叠加,再统计每个频段的能量。
如果直接拿时域波形当特征,一个63秒的试验就是8064维,而且相邻点高度相关,对决策树、SVM都不是好事。FFT之后只取若干频段统计量,一组试验的特征维度能压缩到几百以内,分类器学起来轻松得多,也更符合情绪脑电研究的领域习惯。这也是为什么这份代码把fft.py单独拎出来,它是整个特征提取的起点。特征提取算法这件事,在这个项目里就具体体现为“频段划分加能量聚合”,不复杂,但每一处选择都会影响最终准确率。
3.2 fft.py 的核心逻辑:功率谱与频段平均
fft.py做的事,按这份代码的常见实现推下来应该是这样的:对每个通道的整段信号做快速傅里叶变换,取模的平方得到功率谱,然后按频率轴把各频段对应的功率取平均,得到一组频段特征。这里有两个技术细节直接决定特征质量。第一是FFT之前要加窗,常用汉宁窗来抑制频谱泄漏;第二是只用正频率部分,np.fft.rfft比np.fft.fft少一半无用数据,也更符合功率谱的物理含义。
频率分辨率取决于窗长和采样率的比值。128Hz采样、63秒整段做FFT,分辨率大概是0.016Hz,非常细,但单个频段的功率估计方差偏大。所以我一般会在FFT前后做个权衡:要么用整段加窗,要么缩短到4到8秒的滑窗再平均,也就是Welch法。这个项目用的应该是整段FFT,初学者理解容易,但如果你在复现时发现频段功率波动大,换成Welch是第一条优化路径。
import numpy as np def compute_band_power(signal, fs=128.0, bands=None): if bands is None: bands = {'delta': (0.5, 4), 'theta': (4, 8), 'alpha': (8, 13), 'beta': (13, 30), 'gamma': (30, 45)} n = len(signal) window = np.hanning(n) signal = signal * window # 加窗抑制频谱泄漏 spectrum = np.fft.rfft(signal) power = np.abs(spectrum) ** 2 freqs = np.fft.rfftfreq(n, d=1.0/fs) features = {} for name, (f_lo, f_hi) in bands.items(): mask = (freqs >= f_lo) & (freqs < f_hi) features[name] = power[mask].mean() return features这段代码输出一个字典,每个频段一个平均功率值。加窗用的是整段汉宁窗,rfft给出的频谱只包含0到奈奎斯特频率的部分,rfftfreq生成的频率轴和rfft一一对应。频段边界用了左闭右开,避免相邻频段在边界频率上重复计入。功率取平均而不是求和,是为了消除不同频段宽度不同带来的量纲差异。如果你把整段FFT改成Welch,核心就是加一步信号分帧再加窗平均,其余逻辑不变。
3.3 feature_extraction.py 怎么把特征拼成矩阵
fft.py算出的只是单通道特征,特征提取要做的是把所有通道的特征拼成一个一维向量,作为一条样本。对于40通道的DEAP数据,每个通道5个频段功率,拼起来就是200维。有些实现还会加额叶左右不对称性特征,比如F3和F4电极的alpha功率差,但那属于进阶,这个项目里应该没做,直接拼通道特征就够。
feature_extraction.py的职责就是遍历所有试验、所有通道,把频段特征排成(n_samples, n_features)的矩阵,同时返回对应的标签向量。这里要注意对齐问题:data的维度顺序是(试验, 通道, 采样点),提取的时候别把通道维和试验维搞混,否则拼出来的特征矩阵含义全乱了。main_v1.py应该是把预处理、fft、特征提取、模型训练全部串起来的入口脚本,它的价值在于把整个流程顺序固定下来。我复现时也习惯保留一个这样的总入口,方便拿到代码的人一条命令跑通。
def build_feature_matrix(data, fs=128.0): n_trials, n_channels, _ = data.shape feature_list = [] for t in range(n_trials): trial_features = [] for c in range(n_channels): bp = compute_band_power(data[t, c], fs=fs) trial_features.extend(bp.values()) feature_list.append(trial_features) return np.array(feature_list) # (n_trials, n_channels * n_bands)这个循环的写法刻意保持了和单通道函数的一致性,方便逐行调试。n_channels乘n_bands就是最终特征维度,200维对三个分类器来说都是合理规模。如果你把通道数从40改成32,这个函数不需要改,特征维度自动变成160,这也是把通道遍历放在内层的原因之一。多被试的数据到这里也要拼一次,沿样本维堆叠所有被试的特征矩阵,同时把标签拼好,保证顺序一一对应,这一步错了后面模型训练全是虚的。
4. 二分类模型实战:决策树、SVM、KNN 的参数与选型
4.1 决策树:先当 baseline,顺便看哪个频段最有效
决策树是三个模型里最好解释的,它能把“alpha频段功率大于某个值就归为正类”这种规则直接可视化出来。但也因为它太灵活,在200维特征上不加限制会严重过拟合。训练集上95%、测试集上55%的情况,基本就是树太深了。我一般会把max_depth限制在5到8,min_samples_leaf限制在10以上,让每个叶子节点至少有足够样本支撑它的决策,树结构也更稳定。
训练决策树还有一个额外好处:feature_importances_属性直接告诉你哪些频段对分类贡献最大。比如跑完发现alpha和theta频段的重要度明显高于delta,这和情绪研究中“放松状态alpha波增强”的结论能对上,就说明特征提取的思路没问题。如果重要度最高的反而是gamma高频段,你就要回头检查滤波是否把工频干扰漏了进来。
from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score X, y = feature_matrix, y_val # feature_matrix 来自 build_feature_matrix X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y) clf = DecisionTreeClassifier(max_depth=5, min_samples_leaf=10, random_state=42) clf.fit(X_train, y_train) print('test acc: {:.3f}'.format(accuracy_score(y_test, clf.predict(X_test)))) importances = sorted(zip(clf.feature_importances_, range(X.shape[1])), reverse=True)[:5] print('top5 特征索引:', [idx for _, idx in importances])train_test_split的stratify=y很重要,它保证训练测试两个集合里正负类比例一致,脑电情绪数据样本不均衡时,分层划分能减少偶然性。max_depth=5是起点,后续可以拿3到10做网格搜索,但先固定一个值跑通流程比一开始就调参更重要。标签列y是从第2章的binarize_labels算出来的,和特征矩阵的行一一对应,这里直接传进去就行。
决策树在这个项目里的定位就是baseline。它很少是准确率最高的那个,但它的价值在于可解释性和特征诊断功能。我个人的习惯是每换一种特征提取方案,先跑决策树看重要度排序,确认特征的物理意义还在,再上复杂模型。
4.2 SVM:脑电二分类里最稳的选择
SVM在脑电这类小样本高维数据上表现一直不错。DEAP这种40个被试、每个被试40个试验的数据规模,总共1600条样本,训练SVM几乎秒级完成。核函数的选择上,线性核和RBF核都可以试。线性核参数少、不容易过拟合,适合特征已经比较有区分度的情况;RBF核能捕捉非线性关系,但需要调C和gamma,新手容易在这里翻车。
我复现这个项目时的经验是:先用线性核跑一版拿到baseline,再换RBF核看有没有提升。gamma记得用'scale',它会根据特征方差自动设置初值,比手动猜一个数靠谱。C控制误分类惩罚,C太大容易过拟合训练集中的噪声,C太小则欠拟合,常用区间是0.1到10。DEAP二分类这个场景,C=1配合RBF核通常够用,往下调优再走网格搜索。多被试拼出来的1600条样本里,如果正负类不太均衡,顺手给SVC传一个class_weight='balanced',按类别的样本量自动加权。
from sklearn.svm import SVC svm = SVC(kernel='rbf', C=1.0, gamma='scale', class_weight='balanced', random_state=42) svm.fit(X_train, y_train) print('SVM test acc: {:.3f}'.format(accuracy_score(y_test, svm.predict(X_test)))) print('支持向量数:', svm.n_support_)打印支持向量数是个排查技巧,如果支持向量占了训练样本的大半,说明C太小或gamma不合适,模型几乎退化成最近邻记忆,泛化能力会变差。SVM的决策函数是距离超平面的,输出直接把类别标号取出来喂给accuracy_score就行。训练集和测试集沿用决策树那一份,保证三个模型在完全相同的划分下对比,才谈得上公平。
4.3 KNN:先标准化,再谈K值怎么选
KNN在三个模型里最依赖数据预处理。它的分类决策基于样本间的距离,而200维特征里各维度的量纲如果不一致,幅值大的频段会直接淹没幅值小的频段。所以KNN之前必须做标准化,常见做法是把StandardScaler和KNN包进一个Pipeline,避免模型训练和特征处理脱节。
K值的选择也有讲究。K太小时模型对噪声敏感,K太大时会把远距离的异类样本也拉进来。我一般从奇数开始试,3、5、7、9,用交叉验证挑一个使验证集准确率最高的值。DEAP二分类这种规模,K=5到K=11之间通常会有一个稳定区间,如果K=1效果最好,说明数据划分有问题或者特征本身噪声大,不能把这个结果当真。
from sklearn.neighbors import KNeighborsClassifier from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline from sklearn.model_selection import GridSearchCV pipe = Pipeline([ ('scaler', StandardScaler()), ('knn', KNeighborsClassifier()) ]) param_grid = {'knn__n_neighbors': [3, 5, 7, 9, 11]} grid = GridSearchCV(pipe, param_grid, cv=5, scoring='accuracy') grid.fit(X_train, y_train) print('best k:', grid.best_params_) print('KNN test acc: {:.3f}'.format(accuracy_score(y_test, grid.predict(X_test))))把标准化放进Pipeline里的好处是,交叉验证的每一折只会用训练部分拟合scaler,不会把测试集的信息漏进去。如果你先在全量数据上fit然后切分,等于提前偷看了测试分布,准确率会虚高。GridSearchCV的cv=5用的是分层K折,底层自动处理类别比例。KNN训练几乎没有时间成本,慢的是预测,所以k的选择完全可以用网格搜索覆盖,不用手试。
4.4 三个模型对比:准确率不是唯一标准
| 模型 | 是否需要标准化 | 可解释性 | 训练速度 | DEAP二分类常见表现 |
|---|---|---|---|---|
| 决策树 | 不需要 | 高 | 极快 | 中等,视深度而定 |
| SVM(RBF) | 需要 | 低 | 快 | 通常最好 |
| KNN | 必须 | 低 | 训练快预测慢 | 略低于SVM |
这个项目选这三个模型的好处是覆盖了不同的分类思路:决策树走规则划分,SVM走最大间隔,KNN走距离度量。三者结果放在一起,如果SVM明显优于另外两个,说明特征线性可分性尚可;如果三个模型都不超过60%,问题大概率出在预处理或标签定义上,而不是模型。我建议固定训练集和测试集划分后,一口气跑三个模型,把准确率、混淆矩阵一起打出来看,而不是只看一个数字。
混淆矩阵特别值得看。准确率60%可能是正负类都猜一半的结果,也可能是把某一类完全猜错但另一类猜得特别准。脑电情绪分类里,正类召回率低是常态,这时要考虑迁移阈值或者调整class_weight。这份代码没做这些,但模型训练完之后,这些是我会顺手检查的。main_v1.py串起的就是从数据处理到这整段模型训练,你按顺序跑完,拿到三个准确率,项目的核心闭环就算结束了。
5. 避坑排查:DEAP 二分类最容易翻车的 5 个地方
5.1 数据泄漏:先归一化再切分,测试集信息提前泄露
现象:训练集准确率正常,测试集准确率却异常高,高到不符合常理,比如决策树在200维噪声特征上还能到80%。
原因:预处理时在全量数据上做了z-score归一化或标准化,然后再切分训练测试集,scaler的均值和方差混入了测试样本。scaler看到测试集分布后,相当于让模型提前知道了测试数据的尺度信息,这是典型的数据泄漏。
解决:先切分,再fit scaler。写进Pipeline里最省心,或者手动调整顺序,确保scaler只用训练集拟合。这个坑最坑的地方在于它不会报错,准确率还特别好看,你会以为模型真的学到了规律,直到换新数据才发现崩了。
5.2 标签量纲没查,阈值切错
现象:跑完发现valence标签正类占比是0.0或者1.0,分类器完全退化成猜一个类别,准确率稳定在某个奇数百分比上。
原因:下载的DEAP数据集labels可能是0到1归一化版本,也可能是1到9原始评分版本,用同一个阈值切,结果天差地别。1到9的量纲用5切,0到1的量纲用0.5切,写反了就是全正类或全负类。
解决:先打印labels.min()、labels.max()、labels.mean(),确认量纲后再决定threshold传5.0还是0.5。这个检查只要一行print,却省得后面所有结果作废。顺便多嘴一句,网上有些版本还把labels做了别的预处理,总之拿到数据先看统计量,不要相信记忆。
5.3 40通道里混了8路外围生理信号
现象:特征提取用了全部40通道,模型准确率上不去,跑重要度分析时发现某些通道的数值一直异常甚至恒为零。
原因:DEAP的40通道由32个EEG通道加8个外围信号构成,EOG和EMG这些不属于脑电。把它们拼进特征,理论上引入了和情绪无关的生理噪声,模型要额外花容量去忽略这些通道,效果自然差。
解决:确认你下载的数据版本和文档里的通道顺序,只取EEG通道段做特征提取。如果代码里全是40通道的硬编码,改成按索引切片即可,特征维度会从200变成160。注意别把这里和“40被试”搞混,通道数和被试数是两个维度,很多人第一次看shape会愣一下。
5.4 FFT不加窗,频段边界出现频谱泄漏
现象:alpha频段功率波动特别大,换一段试验结果就跳变,分类器稳定性差,同一套参数两次训练结果对不上。
原因:整段信号直接做FFT相当于默认加矩形窗,窗边缘的突变会向邻近频率泄漏能量,频段边界的值尤其受影响。alpha频段只有5Hz带宽,泄漏进来的能量一多,特征就没法看了。
解决:FFT前加汉宁窗,或者改用Welch平均周期图。加窗是改动最小的方案,一行np.hanning就能明显改善功率谱的平滑度。如果换了加窗还不够稳,把整段FFT换成4到8秒滑窗Welch,频段功率的方差会降一个量级。
5.5 随机种子没固定,结果没法复现
现象:同一份代码两次运行,准确率差2%到3%,不知道信哪次结果。跟别人对答案时,两边数字永远对不上,有种搞玄学的错觉。
原因:train_test_split、SVM、KNN甚至决策树的分裂过程里都有随机性,不固定随机种子,结果天然抖动。脑电数据本身信噪比低,几个百分点的抖动足以掩盖真实的改进。
解决:在train_test_split传random_state=42,在模型构造函数里也传random_state=42。想更严格一点,固定数据后跑5个不同的种子,取均值和标准差,比单次运行更能反映真实水平。从那以后我跑任何实验,第一步就是check代码里有没有种子的硬编码,没有就先补上。
6. 验证与进阶:从随机划分到留一被试交叉验证
6.1 按被试分组的交叉验证:LOSO才是正解
随机划分训练测试集在DEAP上有个隐蔽问题:同一个被试的试验会被同时分到训练集和测试集,被试个体差异造成的特征残留会让模型“认识”测试样本,准确率虚高。要验证模型真的学到了情绪相关规律,应该按被试分组做留一被试交叉验证,或者至少用GroupKFold按被试ID划分。
from sklearn.model_selection import GroupKFold groups = np.repeat(np.arange(40), 40) # 40个被试,每人40段试验 gkf = GroupKFold(n_splits=5) for train_idx, test_idx in gkf.split(X, y, groups=groups): clf = SVC(kernel='rbf', C=1.0, gamma='scale', random_state=42) clf.fit(X[train_idx], y[train_idx]) print(accuracy_score(y[test_idx], clf.predict(X[test_idx])))GroupKFold保证同一个被试不会同时出现在训练和测试集合中,这样得到的准确率才代表模型对新人的泛化能力。如果随机划分下准确率70%左右,LOSO通常会掉到55%到65%区间,这是正常现象,不代表模型坏了。
6.2 特征升级:微分熵比功率谱更常用
FFT功率谱是最直观的频域特征,但在脑电情绪识别的主流工作里,微分熵出场率更高,计算方式是0.5乘以log(2πe乘方差),本质上是频段信号方差的对数度量。DE对频段方差变化更敏感,实际分类效果往往更好,不少情绪识别论文用的都是它。
想验证改特征有没有用,就把上一章三个模型的实验原样跑一遍,对比准确率变化。边改特征边改模型是最容易犯的错,改了多个变量就说不清是谁起了作用。一次只动一个环节,这是调实验的基本纪律。
6.3 模型升级:加一个XGBoost做对照
三个基础模型跑通之后,最自然的下一步是加一个梯度提升树。XGBoost在中小表格数据上往往能超过SVM,而且能直接输出特征重要度,和决策树对比能更精细地观察特征分布。
from xgboost import XGBClassifier xgb = XGBClassifier(n_estimators=100, max_depth=4, learning_rate=0.1, random_state=42, eval_metric='logloss') xgb.fit(X_train, y_train) print('XGB test acc: {:.3f}'.format(accuracy_score(y_test, xgb.predict(X_test))))n_estimators=100和max_depth=4是保守起点,XGBoost需要重点关注的是learning_rate和max_depth,树的数量不是越多越好,配合早停可以避免后期过拟合。
到这里整个流程就闭环了:从DEAP原始数据读取,到预处理、FFT特征提取、三个模型训练,再到分组交叉验证和特征升级。我自己的习惯是拿到一份新EEG数据集后,先固定预处理和特征提取,把这三个模型和XGBoost全跑一遍,用同一份划分对比数字,确认baseline之后再往深度学习方向走。从那以后我每次跑脑电实验,都强制先打印数据维度、标签量纲、通道数这三个数,确认无误才继续往下写,这个步骤帮我挡掉了至少五回无效实验。希望帮到你。
本文还有配套的精品资源,点击获取