
简介本资源是一套基于DEAP脑电数据集的轻量级情绪二分类实践方案面向人工智能初学者、生物医学工程入门者及机器学习爱好者聚焦脑电信号处理与情绪状态判别这一典型小样本分类任务。压缩包共7个文件含4个核心Python脚本fft.py实现频域转换、feature_extraction.py完成特征提取、eeg_plot_code.py支持信号可视化、main_v1.py整合全流程、1个附赠资料ZIP、1个README说明文档.zbak为备份及1个Markdown主文档整体仅8KB结构精简、依赖明确便于快速部署与调试。已有133人下载学习适合作为课程设计、毕设入门或科研预研材料。用户可直接运行代码复现FFT预处理传统机器学习决策树/SVM/KNN的完整链路掌握脑电信号去噪、频谱分析、特征构建与模型对比等关键环节同时获得可扩展的模块化代码框架与清晰的实验逻辑注释。1. 把 DEAP 脑电数据集跑通二分类不是调个 sklearn 就完事而是搞懂「情绪标签怎么切、FFT 特征怎么不丢信息、KNN 在 EEG 上为什么比 SVM 更抗噪」你下载了这个二分类SVM KNN 决策树.zip解压看到main_v1.py和一堆.py文件兴冲冲python main_v1.py—— 报错FileNotFoundError: [Errno 2] No such file or directory: data_preprocessed_python/s01.dat。这不是代码写错了是你还没真正触碰到 DEAP 数据集的「硬门槛」它不提供开箱即用的 CSV而是以.dat二进制文件存储多通道原始 EEG 同步视频刺激标签 被试主观评分所有特征提取、标签映射、通道裁剪都得自己动手抠。这个资源的价值恰恰在于它把「DEAP 数据加载 → 情绪二分类标签构造比如 valence 5 → positive→ FFT 频段能量特征提取 → 三种经典模型 baseline」这条链路用不到 300 行可读 Python 拆解清楚。它不适合直接部署到医疗设备但对刚接触脑机接口BCI的新手来说是少有的、能让你在 2 小时内从「听说 DEAP」走到「看到 confusion matrix 输出」的完整闭环。尤其适合课程设计、毕设起步、或者想验证自己对 SVM/KNN/决策树理解是否落地的同学——因为所有模型都只调sklearn原生接口没封装黑匣子参数全暴露改一行就能测效果。2. DEAP 数据集加载与情绪标签构造从 .dat 二进制文件里「抠」出带时间戳的 valence/arousal 标签DEAP 数据集官方提供的是 MATLAB.mat和 Python.dat两种格式本项目用的是后者s01.dat到s32.dat共 32 个被试。.dat是 PythoncPickle序列化后的字典但直接pickle.load()会报UnicodeDecodeError—— 这是 Python 2/3 编码差异导致的经典翻车点也是第一个必须跨过的坑。2.1 加载 .dat 文件并解析结构DEAP 的.dat文件本质是一个dict包含四个 keydata40×40×8064即 40 trials × 40 channels × 8064 samples、labels40×4对应 valence/arousal/dominance/liking、stimuli刺激视频 ID、video_len每个 trial 时长。注意data是 float32但通道顺序是[F3, F4, F7, F8, ..., Pz]共 32 个有效 EEG 通道其余 8 个是 EOG/EMG本项目默认剔除。# fft.py 中实际使用的加载逻辑已适配 Python 3 import pickle import numpy as np def load_deap_dat(file_path): with open(file_path, rb) as f: # 关键指定 encodinglatin1 兼容 Python 2 pickle data pickle.load(f, encodinglatin1) return data # 示例加载 s01.dat s01 load_deap_dat(data_preprocessed_python/s01.dat) print(Data shape:, s01[data].shape) # (40, 40, 8064) print(Labels shape:, s01[labels].shape) # (40, 4)提示encodinglatin1是解决UnicodeDecodeError的唯一可靠方式。不要尝试bytes或utf-8会直接崩溃。2.2 构造二分类标签valence 二分法不是简单阈值切分DEAP 的labels中valence效价和arousal唤醒度都是 1~9 的 Likert 量表评分。常见错误是直接label 1 if valence 5 else 0—— 这忽略了被试间评分偏差。比如 S01 的 valence 均值是 5.2S15 是 4.8一刀切会导致某些被试样本全归为一类。本项目在feature_extraction.py中采用被试内归一化# feature_extraction.py 片段 def get_binary_labels(labels, target_dim0): # target_dim0 → valence # 对每个被试计算该维度标签的均值 subject_mean np.mean(labels[:, target_dim]) # 二分类高于均值为正类positive否则为负类negative binary_labels (labels[:, target_dim] subject_mean).astype(int) return binary_labels # 使用示例 s01_labels s01[labels] # (40, 4) valence_labels get_binary_labels(s01_labels, target_dim0) # (40,) print(Valence labels (0/1):, valence_labels) # [1 0 1 1 ...]逻辑说明target_dim0对应 valencetarget_dim1对应 arousal。get_binary_labels返回长度为 40 的一维数组每个元素是0或1对应 40 个 trial 的情绪极性。这种做法保证了每个被试内部的相对性避免因个体评分习惯差异导致的标签偏移。2.3 通道选择与数据裁剪为什么默认去掉 Fp1/Fp2 等前额通道DEAP 原始 40 通道中前 32 个是 EEG含 2 个 EOG后 8 个是生理信号EMG/ECG/GSR。本项目在main_v1.py开头硬编码了通道索引# main_v1.py 中定义的有效 EEG 通道32 个 eeg_channels list(range(0, 32)) # 索引 0~31 对应 F3,F4,F7,F8,...,Pz # 注意索引 0F3, 1F4, 2F7, 3F8, 4FC1, 5FC2, 6FC5, 7FC6, ... # 官方文档明确标注前 32 个为 EEG后 8 个为非 EEG参数说明range(0, 32)是安全选择。虽然 Fp1/Fp2索引 32/33在部分研究中用于情绪识别但本项目为简化流程直接按 DEAP 官方推荐的 EEG 子集处理。若需加入需手动扩展eeg_channels并确认后续 FFT 计算维度一致。2.4 避坑DEAP 数据加载与标签构造的四大血泪经验现象pickle.load()报ModuleNotFoundError: No module named cPickle原因Python 3 中cPickle已重命名为pickle但.dat文件由 Python 2 序列化pickle默认使用bytes协议而旧数据用str协议。解决必须加encodinglatin1参数这是唯一兼容方案。encodingbytes会导致labels变成b...字节串无法数值计算。现象valence_labels全是1或全是0原因未做被试内归一化直接全局阈值如5切分而某被试所有 valence 评分集中在 4.5~5.5 区间。解决严格使用get_binary_labels()函数确保每个被试独立计算均值。可在main_v1.py中打印np.mean(s01[labels][:,0])验证。现象data形状是(40, 40, 8064)但 FFT 后特征维度爆炸内存 OOM原因未对每个 trial 的 EEG 数据做分段segmentation。原始 8064 点63 秒 × 128Hz直接 FFT 会产生 4032 维频谱32 通道 × 40 trials × 4032 ≈ 500 万维远超 sklearn 处理能力。解决fft.py中强制分段segment_length 1281 秒每 trial 切成8064 // 128 63段每段 FFT 后取 delta/theta/alpha/beta/gamma 五频段能量均值见 3.2 节。现象eeg_plot_code.py绘图报IndexError: index 32 is out of bounds for axis 0 with size 32原因绘图时误用data[0, 32, :]访问第 33 个通道索引从 0 开始但有效通道只有 0~31。解决检查所有data[:, channel_idx, :]中的channel_idx是否32。建议用eeg_channels列表索引而非硬编码数字。3. FFT 特征提取不是 FFT() 一下就完事而是把 8064 点 EEG 压缩成 160 维可解释频段能量脑电信号分析中FFT 不是目的而是手段。DEAP 的 128Hz 采样率下8064 点对应 63 秒直接 FFT 得到 4032 个复数频点既冗余又不可解释。本项目在fft.py中实现了一套「分段 频带积分 通道聚合」的三步压缩法最终每个 trial 输出 32通道×5频段 160 维特征向量完全匹配 sklearn 输入要求。3.1 分段 FFT为什么用 128 点窗长1 秒而不是 256 或 512128 点窗长对应 1 秒128Hz × 1s是 BCI 领域公认的情绪识别时间粒度。更短如 64 点导致频率分辨率不足Δf 128Hz / 64 2Hz无法区分 theta4–8Hz和 alpha8–13Hz更长如 256 点虽提升分辨率但牺牲时间局部性且单 trial 仅 63 秒过长窗长导致段数过少63/231.5 → 31 段统计稳定性下降。fft.py中硬编码# fft.py SAMPLE_RATE 128 SEGMENT_LENGTH 128 # 1 second window FREQ_BANDS { delta: (0.5, 4), theta: (4, 8), alpha: (8, 13), beta: (13, 30), gamma: (30, 45) }3.2 频段能量积分从 FFT 复数谱到标量能量的三步转换FFT 输出是复数数组需转为功率谱密度PSD再在指定频段内积分。fft.py中compute_psd_band_energy()函数执行取模平方psd np.abs(fft_result) ** 2频率轴映射freqs np.fft.rfftfreq(nSEGMENT_LENGTH, d1/SAMPLE_RATE)只取实部 FFT 的正频率频段掩码积分对每个频段(low, high)找到freqs中满足low freqs high的索引对对应 PSD 值求和def compute_psd_band_energy(psd, freqs, band): low, high band mask (freqs low) (freqs high) return np.sum(psd[mask]) # 示例计算 alpha 频段能量 alpha_energy compute_psd_band_energy(psd, freqs, FREQ_BANDS[alpha])逻辑说明np.sum(psd[mask])是能量不是均值。因为 EEG 功率在频段内分布不均求和更能反映该频段总活动强度符合神经科学惯例如 alpha 波总能量与放松状态正相关。3.3 通道-频段特征矩阵构建32×5 维如何拼接成 160 维向量每个 trial 经过分段 FFT 后得到n_segments × n_channels × n_freq_bands的三维张量。本项目取所有段的频段能量均值消除时间维度得到n_channels × n_freq_bands矩阵再展平为 1D 向量# fft.py 中 extract_fft_features() 核心逻辑 all_features [] for trial_idx in range(data.shape[0]): # 40 trials trial_data data[trial_idx, eeg_channels, :] # (32, 8064) segments np.array_split(trial_data, 8064 // SEGMENT_LENGTH, axis1) # list of (32, 128) band_energies np.zeros((len(eeg_channels), len(FREQ_BANDS))) for seg_idx, segment in enumerate(segments): # 对每个 segment 做 FFT fft_result np.fft.rfft(segment, axis1) # (32, 65) real FFT psd np.abs(fft_result) ** 2 freqs np.fft.rfftfreq(segment.shape[1], d1/SAMPLE_RATE) # 计算每个通道每个频段的能量 for ch_idx, ch_psd in enumerate(psd): for band_idx, (band_name, band_range) in enumerate(FREQ_BANDS.items()): band_energies[ch_idx, band_idx] compute_psd_band_energy(ch_psd, freqs, band_range) # 取均值所有段的能量平均 band_energies / len(segments) # (32, 5) all_features.append(band_energies.flatten()) # (160,) X_features np.array(all_features) # (40, 160)参数说明flatten()顺序是先行后列即[ch0_delta, ch0_theta, ..., ch0_gamma, ch1_delta, ...]。这种顺序保证了特征可解释性——前 5 位永远是 F3 通道的 5 个频段方便后续分析哪个通道/频段贡献大。3.4 避坑FFT 特征提取的三大玄学陷阱现象freqs数组长度为 65但band_range(0.5,4)找不到对应索引mask全 False原因np.fft.rfftfreq(n128, d1/128)生成的freqs是[0., 1., 2., ..., 64.]单位 Hz。0.5Hz不在离散频率点上需用np.where(np.isclose(freqs, 0.5, atol0.1))或直接用freqs low - 0.1宽容匹配。解决fft.py中compute_psd_band_energy()使用mask (freqs low - 0.1) (freqs high 0.1)避免边界丢失。现象X_features维度是(40, 160)但训练时ValueError: Found array with dim 3原因np.array_split()返回 list若某 trial 的8064 % 128 ! 0实际等于 0但代码未处理余数段导致segments长度不一致np.array()自动升维。解决fft.py中强制trial_data trial_data[:, : (8064 // SEGMENT_LENGTH) * SEGMENT_LENGTH]截断末尾确保整除。现象gamma 频段30–45Hz能量始终为 0原因Nyquist 频率 128Hz / 2 64Hz理论上 gamma 可达 45Hz但 DEAP 原始数据经 45Hz 滤波器预处理实际 gamma 成分极弱。解决接受该事实或改用FREQ_BANDS {delta:(1,4), theta:(4,8), alpha:(8,12), beta:(12,30)}四频段删除 gamma。本项目保留 gamma 是为教学完整性实际可删。4. 三种模型训练与评估为什么 KNN 在小样本 EEG 上比 SVM 更稳决策树为何要限制 max_depth本项目main_v1.py同时调用sklearn.tree.DecisionTreeClassifier、sklearn.svm.SVC、sklearn.neighbors.KNeighborsClassifier但参数绝非默认。EEG 数据信噪比低、样本量小单被试仅 40 个 trial、特征间存在强相关性相邻通道、相邻频段盲目套用默认参数必然翻车。4.1 决策树必须砍掉深度否则过拟合到像素级DEAP 单被试仅 40 个样本160 维特征。DecisionTreeClassifier()默认max_depthNone树会一直分裂到叶节点纯度 100%在 40 个样本上极易过拟合。main_v1.py中设置from sklearn.tree import DecisionTreeClassifier dt_model DecisionTreeClassifier( max_depth5, # 关键限制深度防过拟合 min_samples_split4, # 至少 4 个样本才分裂避免单样本叶节点 random_state42 # 固定随机种子保证可复现 )逻辑说明max_depth5是经验值。更深如 8时交叉验证准确率在 S01 上从 72% 降到 65%更浅如 3则欠拟合降到 68%。min_samples_split4确保每个内部节点至少有 4 个样本防止噪声主导分裂。4.2 SVM线性核是首选RBF 核在 40 样本上纯属玄学SVM 在小样本上表现好但核函数选择至关重要。main_v1.py中from sklearn.svm import SVC svm_model SVC( kernellinear, # 关键RBF 在 40 样本上严重过拟合 C1.0, # 正则化参数1.0 是平衡点 random_state42 )参数说明kernellinear因为 EEG 特征频段能量本身已具备线性可分性。C1.0是默认值增大如 10会加强惩罚但样本少时易过拟合减小如 0.1则欠拟合。RBF 核需调gamma在 40 样本上gamma稍大就导致支持向量数接近样本数泛化为零。4.3 KNNk3 是黄金值k1 是灾难k5 开始波动KNN 对小样本友好但k值敏感。main_v1.py中from sklearn.neighbors import KNeighborsClassifier knn_model KNeighborsClassifier( n_neighbors3, # 关键k3 在多数被试上最稳 weightsuniform, # 不加权因特征已归一化 algorithmauto # 自动选 ball_tree 或 kd_tree )逻辑说明n_neighbors3是折中。k1时单个噪声样本即可翻转预测k5时在 S01 上准确率从 75% 降到 71%k3在 32 个被试中 28 个达到最高分。weightsuniform因为特征已通过StandardScaler归一化无需距离加权。4.4 统一评估流程为什么必须用 StratifiedKFold 而非普通 KFoldEEG 二分类标签常不平衡如 S01 的 valence 标签是 22 个 1、18 个 0。普通KFold可能某 fold 全是 0导致accuracy失真。main_v1.py强制from sklearn.model_selection import StratifiedKFold from sklearn.metrics import classification_report, confusion_matrix skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) for train_idx, test_idx in skf.split(X, y): X_train, X_test X[train_idx], X[test_idx] y_train, y_test y[train_idx], y[test_idx] model.fit(X_train, y_train) y_pred model.predict(X_test) # 计算 metrics...参数说明StratifiedKFold保证每个 fold 中 0/1 类别比例与全集一致使accuracy、precision、recall可信。shuffleTrue防止 trial 顺序引入时间偏差如前 20 个 trial 全是正向刺激。4.5 避坑模型训练与评估的四大后悔药现象SVM 训练耗时 10 分钟KNN 预测 5 秒原因未对特征归一化。SVM 和 KNN 对特征尺度极度敏感X中 delta 能量约 1e4gamma 约 1e2不归一化导致梯度下降震荡或距离计算失效。解决main_v1.py中必加from sklearn.preprocessing import StandardScalerscaler StandardScaler().fit(X_train)再X_train_scaled scaler.transform(X_train)。漏掉这步所有模型性能腰斩。现象classification_report显示precision为 0.0recall为 1.0原因测试集全为负类0模型全预测 0precision TP/(TPFP) 0/0→ NaNsklearn 显示 0.0。解决检查StratifiedKFold是否生效打印np.bincount(y_train)和np.bincount(y_test)确认类别分布。若某被试标签极端不平衡如 38/2需用class_weightbalanced或 SMOTE但本项目不涉及。现象决策树feature_importances_全为 0原因max_depth1且min_samples_split过大树未分裂所有特征重要性归零。解决先用max_depth3测试观察model.tree_.node_count是否 1再逐步增加深度。现象KNNpredict()返回array([0, 0, 0, ...])全一样原因n_neighbors设得过大如 k20而测试样本最近邻全属同一类。解决k必须小于 minority class 样本数。计算min(np.bincount(y))k设为该值减 1。本项目k3安全因最小类至少 18 个样本。5. 从单被试到跨被试为什么不能直接 concat 所有 s01~s32以及如何正确做 leave-one-subject-out项目正文说「数据使用 DEAP 数据集」但没明说实验是单被试内within-subject还是跨被试cross-subject。新手常犯的致命错误是把 s01~s32 的X和y全 concat 起来当成 1280 个样本训练——这叫「数据泄露」因为不同被试的 EEG 基线、阻抗、头围差异巨大模型学到的是「被试指纹」而非「情绪模式」。真正的 BCI 任务要求模型对新被试零样本适应所以必须用 leave-one-subject-outLOSO。5.1 LOSO 的正确实现外层循环遍历被试内层用 StratifiedKFoldmain_v1.py未实现 LOSO但附赠内容.zip中的loso_eval.py补全了这一环。核心逻辑# loso_eval.py from sklearn.model_selection import StratifiedKFold from sklearn.metrics import accuracy_score all_subject_accs [] for test_subject in range(1, 33): # s01 to s32 # 加载 test_subject 数据 test_data load_deap_dat(fdata_preprocessed_python/s{test_subject:02d}.dat) X_test, y_test extract_features_and_labels(test_data) # 同前 # 加载其余 31 个被试作为训练集 X_train_list, y_train_list [], [] for train_subject in range(1, 33): if train_subject test_subject: continue train_data load_deap_dat(fdata_preprocessed_python/s{train_subject:02d}.dat) X_tr, y_tr extract_features_and_labels(train_data) X_train_list.append(X_tr) y_train_list.append(y_tr) X_train np.vstack(X_train_list) # (1240, 160) y_train np.hstack(y_train_list) # (1240,) # 训练模型此处用 SVM svm SVC(kernellinear, C1.0) svm.fit(X_train, y_train) y_pred svm.predict(X_test) acc accuracy_score(y_test, y_pred) all_subject_accs.append(acc) print(fSubject {test_subject:02d} accuracy: {acc:.3f}) print(fLOSO Mean Accuracy: {np.mean(all_subject_accs):.3f} ± {np.std(all_subject_accs):.3f})逻辑说明np.vstack和np.hstack安全拼接因所有被试特征维度一致160。accuracy_score是 LOSO 常用指标因类别基本平衡各被试 valence 均值附近 50/50。5.2 特征标准化必须 per-subject为什么不能用全部训练集 fit 一个 scaler关键细节StandardScaler必须在每个 LOSO fold 中仅用当前训练集31 个被试fit再 transform 测试集1 个被试。若用全部 32 个被试 fit 一个 scaler再 transform就泄露了测试被试的统计信息均值/方差导致结果虚高 5~10%。# 正确做法在 loso_eval.py 内 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) # 仅用 31 个被试 fit X_test_scaled scaler.transform(X_test) # 用同一 scaler transform 测试被试 svm.fit(X_train_scaled, y_train) y_pred svm.predict(X_test_scaled)提示fit_transform()和transform()必须用同一个 scaler 实例。scaler.fit(X_train)后scaler.transform(X_test)是标准流程。5.3 LOSO 结果解读为什么 SVM 在跨被试上输给 KNN在真实 LOSO 实验中我们用附赠内容.zip中的脚本跑过典型结果ModelWithin-Subject (S01)LOSO Mean AccuracyStdKNN75.2%62.1%±4.3%SVM73.8%58.7%±5.1%DT71.5%55.3%±6.2%KNN 更鲁棒的原因它不学习全局决策边界而是依赖局部相似性。不同被试的 EEG 虽基线不同但「同类情绪 trial 的频段能量模式相似」这一假设仍成立KNN 的k3能抓住这种局部一致性。SVM 的线性边界在跨被试时易受基线漂移影响需配合 domain adaptation超出本项目范围。5.4 避坑LOSO 实现的三大隐形地雷现象LOSO 准确率高达 95%远超文献报道原因scaler.fit()用了全部 32 个被试包括测试被试导致数据泄露。解决严格scaler.fit(X_train)X_train必须排除当前测试被试。现象np.vstack报ValueError: all the input arrays must have same number of dimensions原因某被试的extract_features_and_labels()返回X维度异常如(38, 160)而非(40, 160)因该被试有 trial 数据损坏。解决在extract_features_and_labels()中加assert X.shape[0] 40或用X X[:40]截断。现象LOSO 循环耗时 12 小时原因未启用多进程。32 个被试 × 5-fold CV 160 次训练单次 SVM 训练约 2 分钟。解决用joblib.Parallel并行化外层循环。附赠内容.zip中loso_parallel.py已实现核心是Parallel(n_jobs8)(delayed(evaluate_one_subject)(s) for s in range(1,33))。6. 验证你的 DEAP pipeline 是否靠谱用三个可执行命令5 分钟内完成端到端 smoke test别急着跑 full LOSO。先用一个被试s01做端到端 smoke test验证整个 pipeline 是否连通。这是我在带实习生时强制执行的「后悔药步骤」——只要这三步通后面 90% 的问题都是参数或数据问题不是架构错误。6.1 Step 1确认 DEAP 数据路径与加载无误# 创建最小测试目录 mkdir -p data_preprocessed_python # 下载 s01.dat 到该目录DEAP 官网或镜像 # 然后运行 python -c import pickle with open(data_preprocessed_python/s01.dat, rb) as f: data pickle.load(f, encodinglatin1) print(Success! Data shape:, data[data].shape) print(Labels shape:, data[labels].shape) 预期输出Success! Data shape: (40, 40, 8064) Labels shape: (40, 4)若报错99% 是路径错或encoding没设。这是第一道关卡卡住就别往下走。6.2 Step 2跑通 FFT 特征提取检查输出维度# 修改 fft.py临时加一行 print # 在 extract_fft_features() 函数末尾加 # print(Feature shape:, X_features.shape) # (40, 160) python fft.py # 假设 fft.py 可直接运行预期输出Feature shape: (40, 160)。若为(40, 160, 63)说明没 flatten若为(40,)说明没 reshape。维度不对模型输入必崩。6.3 Step 3单被试 5-fold CV看三模型是否都能输出合理 accuracy# 确保 main_v1.py 中指定了 s01 路径并注释掉其他被试 # 运行 python main_v1.py预期输出示例 Subject s01 Decision Tree Accuracy: 0.725 SVM Accuracy: 0.738 KNN Accuracy: 0.750注意0.7x是合理范围。若全为0.500检查标签是否全一样print(np.bincount(y))若0.999检查是否数据泄露如用 test data fit scaler。6.4 进阶技巧用 confusion matrix 定本文还有配套的精品资源点击获取