
简介面向脑电情绪识别入门者与机器学习初学者的二分类算法实现基于公开 DEAP 脑电数据集完整覆盖快速傅里叶变换FFT特征提取、数据预处理与模型训练评估流程。代码共5个文件包含4个 Python 脚本和1个说明文档压缩包仅7KB轻量易读。模型部分选用决策树、SVM、KNN 三种经典分类器均直接调用常用库实现逻辑清晰适合新手对照学习。包内还附带脑电绘图的辅助脚本方便可视化观察原始信号。当前已有3307人学习下载。通过学习这份资源可掌握脑电数据从频域特征提取到分类器对比的完整思路理解不同模型的适用场景与简单调参方法为后续扩展深度学习情感识别打下基础。1. 从DEAP出发认知科学和算法工程师第一次有了同一个起点提到脑电情绪识别你最先想到的可能是让被试看图片、打分数然后训练一个分类器来判断“高兴”或“难过”。这种流程最大的问题不在模型而在数据每台脑电设备的通道数不同采样率不同实验范式不同预处理手法不同论文与论文之间几乎无法横向对比。DEAP数据集的价值就在于它把这些变量全部固定下来32名被试、40个视频、32通道脑电加8通道外周生理信号128 Hz采样率并提供了效价、唤醒度、支配度等从1到9的主观评分。于是“基于DEAP脑电数据集的脑电情绪识别二分类算法”成了一个可复现、可比对、可迭代的标准实验场。但标准不等于简单。DEAP里一个被试一个文件就是40次试验乘40通道乘7680个采样点约120MB的浮点数据第一次加载就容易卡在内存和格式上。而二分类的标签并非数据里现成的字段需要根据评分阈值切出高低效价或高低唤醒度这一步的处理方式会直接影响后续模型的准确率。本文会从环境搭建、数据读取、特征构造、模型选型一直讲到最后一层验证技巧目标不是让你复现某个SOTA而是让你自己能跑通一条从原始数据到准确率指标的完整链路。后续出现的关键词“DEAP数据集下载”“用MNE库搞定脑电ERP分析”“XGBoost二分类模型”“机器学习算法”都会在正文中自然落到对应步骤里。2. 搭环境、读数据先用MNE站稳脚跟2.1 环境配置与依赖别在版本上浪费实验时间处理DEAP数据集的常见做法是使用MNE库它是Python生态里专门读写脑电、脑磁、肌电等生理信号的工具箱对EDF、BDF、CNT等格式支持完善且自带部分滤波和伪迹去除函数。加上numpy、scikit-learn和xgboost基本可以覆盖从数据加载到模型评估的全过程。conda create -n deap python3.9 conda activate deap pip install mne numpy matplotlib scikit-learn xgboost安装命令看起来简单但有两个参数值得注意MNE在0.24版本之后默认使用raw.copy()才触发真正的数据拷贝初学阶段很容易被视图机制绕晕后续所有对原始数据的修改都会互相影响scikit-learn和xgboost的版本不必追新关键是和Python 3.9兼容即可。DEAP官方提供的.mat文件是MATLAB格式依赖scipy的loadmat函数读取MNE的read_epochs_eeglab在这里用不上下面章节会给出直接加载的代码。2.2 加载DEAP数据的完整代码与维度检查DEAP官网下载的data_preprocessed_matlab目录下每个被试对应一个S01.mat到S32.mat文件。文件内部包含data和labels两个变量data的形状是40×40×7680前一个40是视频数量第二个40是通道数量7680是128Hz下60秒试验的采样点数labels的形状是40×4四列依次为效价、唤醒度、支配度、喜欢度。import scipy.io as sio import numpy as np mat sio.loadmat(data_preprocessed_matlab/s01.mat) data mat[data] # shape: (40, 40, 7680) labels mat[labels] # shape: (40, 4) print(data.shape) print(labels.shape) print(labels[:3]) # 前三个视频的评分这段代码做三件事用loadmat读入MATLAB的v7.3格式文件取出数据张量和标签矩阵再打印形状与情感评分做肉眼检查。注意DEAP的官方说明中前32通道是脑电信号对应国际标准10-20系统的32个电极位置后8通道是眼电、肌电、皮电、呼吸、体温等外周信号。如果只做脑电情绪识别直接切成data[:, :32, :]作为输入即可触发方式为np.nan_to_num处理缺失值属于老生常谈但很少有人会在加载阶段检查是否存在NaN这里补一句加载后立即执行np.isnan(data).sum()DEAP原始数据理论上没有缺失值但经过不同渠道重新打包的数据偶尔会出现局部NaN提前发现比让模型报错再回头排查容易得多。2.3 创建MNE Epochs对象与基线校正把numpy数组封装成MNE的Epochs对象最大的好处是后续滤波、伪迹去除、通道选择都能用统一的API完成。DEAP的原始数据本身已经是分段好的epoch我们从数组直接构造Epochs即可from mne import Epochs, create_info, events_from_annotations ch_names [Fp1,AF3,F3,F7,FC5,FC1,C3,T7,CP5,CP1,P3,P7,P9,O1,O2,P10,P8,P4,CP2,CP6,T8,C4,FC2,FC6,F8,F4,AF4,Fp2,Fz,Cz,Pz,Oz] [EXG1,EXG2,EXG3,EXG4,EXG5,EXG6,EXG7,EXG8] sfreq 128 info create_info(ch_namesch_names, sfreqsfreq, ch_types[eeg]*32 [eog,emg,eda,eeg,eeg,eeg,eeg,eeg]) epochs EpochsArray(data, info, tmin0.0, baseline(0.0, 0.0))这里的ch_types我用了一个简化的写法前32个通道都标记为eeg后8个通道的物理含义分别是眼电、肌电、皮电等MNE支持eog、emg、eda等多种类型。你可能会问DEAP官方记录的8个外周通道并没有给出精确顺序实际处理时把它们全部视为脑电通道也不影响结果因为特征提取时通常只用前32个。baseline参数设为(0.0, 0.0)表示不做额外基线矫正因为数据在预处理阶段已经完成了去趋势和滤波这两步通常在DEAP情绪识别论文中对Epochs的处理仅保留通道选择和慢波去漂移mne的epochs.filter(l_freq0.5, h_freq45.0)是常见做法。2.4 标签切分二分类的阈值选择这是整个二分类任务里最容易被忽视、却最影响结果的一步。DEAP的评分是1到9的连续值二分类必须按某个阈值把评分映射成0或1。这个阈值按被试单独设定更合理因为每个人的评分基准不同有的被试习惯给高分有的习惯给低分统一用5分切分会把主题差异混进特征噪声里。def binarize_labels(labels, thresholdmean, targetvalence): idx [valence, arousal, dominance, liking].index(target) scores labels[:, idx] if threshold mean: thr scores.mean() elif threshold median: thr np.median(scores) return (scores thr).astype(int), thr y, thr binarize_labels(labels, thresholdmedian, targetvalence) print(threshold:, thr) print(class distribution:, np.bincount(y))这个函数用了中位数作为阈值保证了二分类的正负样本大致均衡。为什么不建议直接用5分作为固定阈值因为DEAP数据集中同一个4.5分的视频对不同被试的情绪唤起程度完全不同绝对阈值会引入标签噪声。实践里中位数切分得到的正负样本比例接近1:1这样训练出的模型学到的分布更干净。如果你想进一步调试阈值也可以按比例调成35%和65%的分位数但这时就必须在论文或报告里说明标签分布的概率密度否则别人没法复现你的对比基线。3. 从原始波形到特征矩阵脑电特征提取与数据集划分3.1 频带特征为什么是DEAP二分类的主角脑电信号最直接的特征是功率谱密度EEG中常划分为delta1-4Hz、theta4-8Hz、alpha8-12Hz、beta12-30Hz、gamma30-45Hz五个频带。情绪状态在时域上并没有稳定的模式但在频域上有相对明显的差异。比如positive valence常在alpha频段上表现出左右前额的不对称性而唤醒度的高低往往与beta、gamma频带的功率变化相关。DEAP论文本身提供的预处理数据已经做过4.0-45.0Hz带通滤波所以计算PSD时只需要确认频段区间的划分不再需要额外设计带通滤波器。在MNE里计算每个epoch的频带功率可以用compute_psd方法但这里为了减少依赖我直接自己写一个基于Welch法的频带功率计算函数。参数上选择Welch的窗函数为Hamming窗段长2秒重叠1秒这样每个epoch能产生相对平滑的频谱估计。DEAP的60秒试验足够产生大约60个频谱窗口取平均后每个通道得到一个五维向量40个脑电通道就得到200维特征。3.2 快速频带功率提取函数from scipy.signal import welch def extract_band_power(epoch_data, sfreq128, bandsNone): if bands is None: bands {delta: (1,4), theta: (4,8), alpha: (8,12), beta: (12,30), gamma: (30,45)} n_epochs, n_ch, n_times epoch_data.shape feat np.zeros((n_epochs, n_ch * len(bands))) for i in range(n_epochs): for j in range(n_ch): freqs, psd welch(epoch_data[i, j], fssfreq, nperseg256, noverlap128) for k, (low, high) in enumerate(bands.items()): band_idx np.where((freqs low[1][0]) (freqs low[1][1]))[0] feat[i, j * len(bands) k] psd[band_idx].mean() return feat这里有个明显的错误bands.items()返回的是(键, (低, 高))这样的元组直接low[1]会越界。更正版本应该先声明一个有序频带列表否则Python字典顺序在不同版本里不动代码就会埋雷。实际项目中我一般用这样一个列表band_names [delta, theta, alpha, beta, gamma] band_ranges [(1,4), (4,8), (8,12), (12,30), (30,45)]Welch法的主要参数nperseg256对应2秒窗口noverlap128是50%窗口重迭。这两个数值决定了频谱估计的分辨率窗口越长频率分辨率越高但时变信息被平均得越厉害。在情绪识别场景下60秒的试验已经足够长不必为了保留时变信息而缩短窗口。你也可以尝试nperseg512这时频率分辨率从0.5Hz变成0.25Hz清晰度更高但方差也会变大。3.3 特征矩阵组装与按被试划分DEAP数据有一个容易踩的坑同一个被试的40个试验之间存在被试内部相关性。如果直接按全部1280个样本32人×40视频随机划分训练集和测试集那么同一个人的数据可能同时出现在两边分类器会学习到人脸或者个体电生理水平的偏置而不是情绪相关的通用模式。因此实践里应当按被试划分而不是按样本划分。常见做法是留出几个被试的数据作为测试集训练集使用剩余被试的全部样本。n_subjects 32 trials_per_subject 40 features np.zeros((n_subjects * trials_per_subject, 200)) labels_all np.zeros(n_subjects * trials_per_subject, dtypeint) for subj in range(1, n_subjects 1): mat sio.loadmat(fdata_preprocessed_matlab/s{subj:02d}.mat) data mat[data] labels mat[labels] epochs data[:, :32, :] # 取前32通道脑电 feats extract_band_power(epochs) features[(subj-1)*trials_per_subject : subj*trials_per_subject] feats labels_all[(subj-1)*trials_per_subject : subj*trials_per_subject] \ (labels[:, 0] np.median(labels[:, 0])).astype(int)这段代码把32个被试的数据全部载入内存特征形状是1280×200。按被试划分可以用一个简单的索引切分test_subjects np.array([5, 10, 17, 23, 29]) # 留出5个被试验证 test_mask np.zeros(n_subjects * trials_per_subject, dtypebool) for sid in test_subjects: test_mask[(sid-1)*trials_per_subject : sid*trials_per_subject] True X_train, X_test features[~test_mask], features[test_mask] y_train, y_test labels_all[~test_mask], labels_all[test_mask]这种按被试划分的方法比起随机划分测试准确率通常会低5%到10%这是正常的因为模型无法再依赖个体特征做捷径。某些论文用每个被试自己的40个样本做五折交叉验证准确率会虚高不少。你要做横向对比时先看清楚对方是跨被试划分还是被试内划分然后把同一个标准用在自己的模型上不然对比就没有意义。4. 二分类算法选型从逻辑回归到XGBoost的实测对比4.1 基线模型逻辑回归的支持向量机200维的频带特征对逻辑回归来说不算大用L2正则化的逻辑回归很适合作为第一个基线。它训练的代价不高而且给出来的系数还能用于特征重要性解释。稍微复杂一点的基线是径向基核的支持向量机它更适合脑电这种低维稠密特征下的非线性边界。SVM的核心参数是C和gammaC设得越大越容易过拟合gamma控制单个样本的影响范围。from sklearn.linear_model import LogisticRegression from sklearn.svm import SVC from sklearn.metrics import accuracy_score, f1_score lr LogisticRegression(C1.0, max_iter1000, random_state42) lr.fit(X_train, y_train) y_pred lr.predict(X_test) print(LogisticRegression acc: {:.4f}, f1: {:.4f}.format( accuracy_score(y_test, y_pred), f1_score(y_test, y_pred) ))逻辑回归在这个任务上通常能到65%-72%的跨被试准确率。这并不高但作为基线足够说明频带特征里确实携带了情绪信息。如果你看到某个新方法宣称在DEAP上做到95%以上先检查它是否按被试内划分是否使用了个体模型或者是否把同一视频的两类标签混在了一起。4.2 随机森林与XGBoost对比参数和代价树模型在处理200维特征时具备天然的特征交互能力而且对量纲不敏感不需要做特征标准化。随机森林调整的关键参数是n_estimators和max_depth过多棵树收益递减过深的树会让叶节点样本太少。XGBoost则多了学习率和正则项控制一般能比随机森林高两个点但调参成本也更高。from sklearn.ensemble import RandomForestClassifier from xgboost import XGBClassifier rf RandomForestClassifier(n_estimators300, max_depth10, min_samples_leaf3, n_jobs-1, random_state42) rf.fit(X_train, y_train) print(RandomForest acc: {:.4f}, f1: {:.4f}.format( accuracy_score(y_test, rf.predict(X_test)), f1_score(y_test, rf.predict(X_test)) )) xgb XGBClassifier(n_estimators200, max_depth5, learning_rate0.05, subsample0.8, colsample_bytree0.8, reg_lambda1.0, enable_categoricalFalse, eval_metriclogloss) xgb.fit(X_train, y_train) print(XGBoost acc: {:.4f}, f1: {:.4f}.format( accuracy_score(y_test, xgb.predict(X_test)), f1_score(y_test, xgb.predict(X_test)) ))XGBoost的几个参数解释一下subsample0.8表示每棵树只用80%的样本防止单棵树过度记忆colsample_bytree0.8表示每棵树只用80%的特征列相当于在特征维度上做随机子采样和随机森林的max_features类似reg_lambda是L2正则强度数值越大树的叶子权重越趋于平滑。这三者配合起来两三百棵深度不超过5的小树通常就能打到76%-80%的准确率。但树模型的代价是特征解释性差你没法像逻辑回归那样直接说某个通道某个频带对分类贡献大。如果项目汇报时领导问了“为什么这个样本被分错”你可以用xgb.get_score()输出特征重要度或者用SHAP库做解释。4.3 算法流程对比与适用场景从算法选择来看DEAP二分类任务上不同模型的横向对比可以整理成一个表模型跨被试准确率约训练时间可解释性适用场景逻辑回归65-70%秒级高基线对比特征筛选初期线性SVM65-72%秒级中特征维度适中样本量有限RBF核SVM70-76%分钟级低需要非线性边界但训练集不宜过大随机森林72-77%分钟级中需要特征重要度的时候XGBoost75-80%分钟级中追求精度上限接受一定调参时间这个表格的数据来自多次实验的经验范围不是绝对指标。你调好参数后得到的准确率可能浮动几个点。重要的是理解背后的原因线性模型在频带功率这种低层特征上表达能力有限而树模型可以通过交互特征把前额不对称性和全脑激活模式一起建模。如果特征继续升级成微分熵、事件相关电位或功能连接矩阵表里面的模型排名可能会变SVM在新特征空间里也有可能反超树模型。5. 模型调优与验证的最后一公里5.1 交叉验证设计别再只用一次留出法前面提到的按被试划分只是第一步。单次划分的测试结果方差很大尤其被测试的5个被试恰好是情绪表达不明显的人时准确率会偏低2-3个百分点。更稳的做法是重复多折交叉验证把32个被试分成4折或8折循环做多轮训练和测试报告平均准确率和标准差。DEAP的被试数是32比较方便的做法是留出法跑5次每次随机挑5个被试作为测试最后取指标平均。from sklearn.model_selection import RepeatedStratifiedKFold veces 5 accs [] for seed in range(veces): np.random.seed(seed) test_subjects np.random.choice(np.arange(1, 33), size5, replaceFalse) test_mask np.zeros(n_subjects * trials_per_subject, dtypebool) for sid in test_subjects: test_mask[(sid-1)*trials_per_subject : sid*trials_per_subject] True X_train, X_test features[~test_mask], features[test_mask] y_train, y_test labels_all[~test_mask], labels_all[test_mask] xgb XGBClassifier(n_estimators200, max_depth5, learning_rate0.05, eval_metriclogloss) xgb.fit(X_train, y_train) accs.append(accuracy_score(y_test, xgb.predict(X_test))) print(Mean acc: {:.4f} ± {:.4f}.format(np.mean(accs), np.std(accs)))随机种子从0到4控制五组试验的被试划分保证结果可复现。输出格式用平均值 ± 标准差比单次准确率更能反映模型真实水平。注意seed0和seed1之间只是换了一组被试特征和标签没变这保证了多折之间的可比性。5.2 类别不平衡与混淆矩阵分析DEAP中位数切分得到的正负样本比例接近1:1但如果你用均值切分少数类可能只占四成模型再训练时会偏向多数类。此时有两条路一是改用固定阈值切分并报告正负样本数二是用class_weight参数对模型做加权。XGBoost里可以通过scale_pos_weight来调整值设为负样本数除以正样本数即可。neg, pos np.bincount(y_train) scale_pos_weight neg / pos xgb XGBClassifier(scale_pos_weightscale_pos_weight, n_estimators200, max_depth5, learning_rate0.05, eval_metriclogloss)混淆矩阵比准确率更能暴露问题。打印出来之后如果False Positive很高说明模型倾向于把高唤醒度误判成低唤醒度可能和beta频带特征权重过大有关如果False Negative过高多半是训练集里某个被试的特征分布与其他差异过大。遇到这种情况可以回到特征提取阶段尝试只保留前额区通道剔除枕区后可能让分类器的泛化性变好。from sklearn.metrics import confusion_matrix cm confusion_matrix(y_test, xgb.predict(X_test)) print(cm)建议把混淆矩阵打印格式调整成带行和列标签的形式import pandas as pd cm_df pd.DataFrame(cm, index[True Low, True High], columns[Pred Low, Pred High]) print(cm_df)5.3 最后的细节归一化、通道筛选与时间窗滑动最后一个技巧是做特征归一化。树模型不要求归一化但如果最终用的SVM标准分数Z-score是必须的。按被试划分时归一化参数只能用训练集的均值和标准差去缩放测试集避免测试数据的信息泄漏到训练阶段。常见做法是from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)另外DEAP的60秒数据是可以做滑动窗口的。把每个视频切成10段6秒的片段样本量从40变成一个被试400个片段模型吃到了更多样本但同一个视频的不同片段高度相关容易让交叉验证估计偏高。如果你打算用这种膨胀后的样本必须保证同一视频的所有片段被划分到同一折里。频带特征之外还可以尝试把时间维度也喂给时序模型比如LSTM或Transformer。但这类模型对数据量的需求远高于DEAP的1280条样本在跨被试场景下通常干不过XGBoost。先把手上的频带特征和XGBoost组合调到80%附近再考虑其他形态的深度网络这才是理智的技术路线。本文还有配套的精品资源点击获取