
做脑电情绪分析的人几乎绕不开DEAP这个数据集。DEAP的全称是Database for Emotion Analysis using Physiological Signals一个公开的情绪分析数据集记录受试者在观看音乐视频时的脑电EEG和其他生理信号以及对应的效价Valence、唤醒度Arousal、支配度Dominance、喜好度Liking评分。市面上大多数脑电情绪分类论文都是用这个数据集做基准验证。把DEAP数据集的情绪检测代码环境搭建起来核心流程其实就是四步准备环境、读取数据、预处理、训练分类模型。我接触DEAP项目的时间不算短最初在环境这一步就被折腾够呛——不是库装不上就是数据读不进来好不容易读进来又因为内存不足直接把电脑卡死。这篇博客就是把这些经历整理出来面向刚接触脑电情绪分类的学生、科研助理或者想转行做情感计算和脑机接口的开发者。如果你也想把DEAP跑通、跑出属于自己的情绪分类结果这篇文章应该能帮你省下不少折腾时间。1. 整体环境搭建思路与工具选型1.1 动手前先明确项目目标在敲第一行代码之前建议你先问自己一个问题要做的是什么任务很多人拿到DEAP数据集就急着开跑结果折腾到训练阶段才发现标签处理思路不对又回头改代码非常浪费时间。DEAP提供的标签是四个维度的连续评分范围在1到9之间。常见的做法有两种一种是做回归直接预测评分另一种是二分类把评分按阈值拆成高低两类。比如阈值设为5小于5算低大于等于5算高这就是一个标准的二分类问题。这两种做法的数据加载流程完全一样但最后的损失函数、评估指标会差很多。我建议初学者先从二分类入手先把整个训练闭环跑通再考虑做回归或者多分类。另外一个容易被忽略的问题是你到底想做“被试内”预测还是“被试间”预测。被试内是指用同一个人的部分数据训练同一人的另一部分数据测试被试间是指用一部分人的数据训练用另一部分人的数据测试。这个区别直接影响数据划分方式建议在下手前就想清楚不然训练集里出现“数据泄漏”精度虚高后面写论文时会很被动。现在vibe coding的风气很流行各种AI辅助编程工具确实能把代码生成得很漂亮但DEAP这类实验型项目我还是建议把每一步逻辑亲手理清楚。模型结构可以抄环境搭建和数据预处理流程如果也靠自动生成出问题时很难排查因为你根本不知道底层在做什么。1.2 开发环境选型Python版本与依赖包DEAP项目的主流技术栈是Python。推荐使用Python 3.10或3.11不建议直接用最新的Python 3.12或3.13原因很简单PyTorch和某些科学计算库对新版本Python的官方预编译包支持偶尔会滞后出了问题还要自己去编译太折腾。环境管理方面我建议用venv也就是Python自带的虚拟环境而不是把包直接装到系统Python里。系统Python一旦被装乱可能会影响其他项目用venv各管一套环境出问题直接删掉重建省心很多。创建环境的命令很简单python -m venv deap_env source deap_env/bin/activate # Windows下用 deap_env\Scripts\activate激活环境后安装依赖包。依赖清单我整理如下pip install numpy scipy h5py scikit-learn matplotlib pip install torch --index-url https://download.pytorch.org/whl/cpu如果电脑有NVIDIA显卡并想用GPU训练建议去PyTorch官网查支持你CUDA版本的安装命令不要凭记忆乱装。没有GPU也没关系脑电信号这类数据量不算特别夸张用CPU跑个小模型完全可行就是训练会慢一些。安装顺序上有讲究先装numpy、scipy这些基础库再装torch。因为torch的安装会顺带处理numpy的版本依赖你手动先装了一个太高版本的numpy反而可能被torch强制降级。实测下来这个顺序最稳。装完后可以用一行命令验证环境是否正常python -c import torch, scipy, h5py; print(torch.__version__)能正常输出版本号基本说明环境OK了。1.3 写代码的环境WSL2、编辑器与字体推荐开发平台方面Windows原生环境跑DEAP也能用但如果你希望后面用Linux服务器做训练代码能无缝迁移我建议直接上WSL2。WSL2的好处是文件系统、命令行体验和服务器一致而且对PyTorch这种CPU指令集的支持比Windows原生更好一些。装上WSL2之后把代码放在Linux文件系统内比如~/projects/deap读写性能比挂在/mnt/c/下的Windows目录要好不少。编辑器我推荐VS Code配上Remote-WSL插件可以在Windows界面里直接编辑WSL里的文件运行终端也自动进入WSL环境非常顺手。字体这块不少人忽略但实际影响阅读和排错效率。WSL2的默认字体在Windows里看起来总差点意思如果你想要接近macOS终端那种清爽感推荐两个等宽字体Cascadia Code和JetBrains Mono。这两个字体对中文注释的兼容性也还可以把等宽字体设置为编辑器默认字体后代码里的对齐和括号层级一目了然调试时心情会好很多。还有一个非常基础但被问爆的问题“文本文档怎么运行代码”很多新人在记事本里写完代码存成deap.py然后双击文件结果弹出记事本或者直接没反应。正确做法是打开终端切到文件所在目录运行python deap.py或者直接在VS Code里点右上角的运行按钮。双击.py文件通常不是执行代码而是用编辑器打开它这点需要先搞清楚。2. DEAP数据集的获取与数据规格2.1 数据集下载与文件完整性确认DEAP数据集需要去官网提交申请填写用途说明一般几个工作日内会收到下载链接。整个数据集包含32个被试受试者的文件文件名从s01.dat到s32.dat。每个被试的.mat或者.dat文件都包含该被试观看40段音乐视频时的生理信号和评分数据。下载的时候需要注意几个问题。第一是网络波动可能导致文件下载不完整但浏览器不会报错等后面用脚本读取时报错才意识到。建议下载后直接对比文件大小不同被试文件大小应该几乎相同如果某个文件明显比其他文件小那基本就是下载不完整了重新下载。第二是解压路径不要带中文和空格C:\Users\张三\桌面\DEAP 实验数据这种路径在Python里处理时很容易出幺蛾子最好统一放在D:\data\deap这样简洁的路径下。第三是文件格式官方提供.mat和.dat两种格式内容其实是同一个数据只是编码方式不同。用Python读取时两种格式的代码不太一样后面会细说。额外提醒一句下载链接的有效期可能有限同时文件总体积不小建议一次把32个文件全部下完统一放到data/raw/目录下后面预处理脚本直接从目录里批量读取。2.2 数据维度与标签含义这是DEAP项目里最容易搞混的一块我先把数据结构讲清楚。每个被试文件里的data字段是一个三维数组维度是(40, 40, 8064)。这三个数字的含义是第一维40该被试观看了40段一分钟的音乐视频第二维4040个通道前32个通道是脑电信号EEG后8个通道是外周生理信号眼电EOG、肌电EMG、皮电GSR、呼吸等第三维8064时间序列长度对应128Hz采样率下63秒的数据其中前3秒是基线后60秒是正式实验。这里有一个关键点DEAP下载完的mat文件已经是官方降采样到128Hz并做过基础预处理的结果不是原始512Hz采样率的数据。所以读取数据后每一秒对应128个采样点3秒基线就是384个采样点60秒实验段是7680个采样点。很多教程里写的“40×40×8064”如果你不理解8064是怎么来的后面做滑动窗口时很容易算错下标。labels字段则是一个(40, 4)的数组每一行对应一个视频的四个评分效价Valence、唤醒度Arousal、支配度Dominance、喜好度Liking取值范围1到9数值越大表示越正向或者越强烈。我用表格把核心参数整理出来方便你对照参数项数值说明被试人数32每人一个数据文件每被试视频数40一段视频对应一个评分标签通道数40前32通道EEG后8通道外周信号采样率128 Hz官方已降采样信号总时长63 秒前3秒基线 60秒实验总采样点数8064128 Hz × 63 秒评分维度4Valence / Arousal / Dominance / Liking理解了这个结构后面代码里的每一个维度操作就都能对上了。3. 数据预处理与特征提取实操3.1 读取mat文件与hdf5文件先看最常见的mat格式。DEAP的mat文件用scipy.io.loadmat就能读但有个细节读取成功后字段名通常是data和labels建议先打印出来看看结构不要想当然。import scipy.io as sio import numpy as np mat sio.loadmat(data/raw/s01.mat) print(字典键, [k for k in mat.keys() if not k.startswith(__)]) data mat[data] # 形状 (40, 40, 8064) labels mat[labels] # 形状 (40, 4) print(data shape:, data.shape) print(label shape:, labels.shape)如果下载的是.dat后缀的hdf5格式文件读取方式完全不同。.dat其实不是普通二进制文件而是HDF5格式要用h5py读取import h5py import numpy as np with h5py.File(data/raw/s01.dat, r) as f: print(文件内部键, list(f.keys())) data f[data][:] # 形状仍是 (40, 40, 8064) labels f[labels][:]这里有个容易踩的坑h5py读取出来的数据如果直接扔给后续处理类型很可能是uint8或者int16。脑电信号要保留到小数点后面的信息建议一律先转成float32不然后面的归一化和公式计算全部会丢精度。data data.astype(np.float32)处理完一个被试的数据后建议马上用np.save保存成npy格式这样下次就不用再读原始大文件了。比如np.save(data/processed/s01_data.npy, data) np.save(data/processed/s01_labels.npy, labels)从这一步开始后面所有操作都基于npy文件读取速度会快很多。3.2 基线校正与信号分段DEAP的官方说明里正式实验开始前有3秒静息基线。我们的目标是去除信号中由个体差异和仪器漂移带来的恒定偏移。具体做法取前3秒384个采样点每个通道的平均值作为该通道的基线然后用整段信号减去这个基线值。def remove_baseline(data, fs128, baseline_sec3): baseline_len baseline_sec * fs # 384 baseline data[:, :, :baseline_len].mean(axis2, keepdimsTrue) data_corrected data - baseline return data_corrected我把这个函数写成keepdimsTrue是为了保证减法和广播维度一致避免出现(40, 40, 1)去减(40, 40, 8064)时维度对不上的问题。实际写代码时最开始我用的是np.mean(axis2)减的时候直接报维度错误后来才意识到keepdims的用法。减完基线后一般直接丢弃前3秒只保留后60秒的实验数据data_exp data_corrected[:, :, baseline_len:] # 形状 (40, 40, 7680)基线校正不是可选项它对最后的分类准确率有明显影响尤其是效价这种受个体基线差异影响较大的标签。我用过不校正基线的数据直接训练同一个模型精度下降了好几个点所以这一步不要省。如果觉得每段视频只有1个样本40个视频太少不够训练可以用滑动窗口切段来增加样本量。比如按2秒长度256个采样点、步长0.5秒64个采样点滑动一段60秒信号能切成100多个窗口40段视频就有4000多个样本。注意滑动窗口的标签沿用原始视频的标签不能单独标注。def sliding_window(data, labels, window_len256, step64): n_videos, n_channels, n_samples data.shape windows [] window_labels [] for v in range(n_videos): for start in range(0, n_samples - window_len 1, step): windows.append(data[v, :, start:start window_len]) window_labels.append(labels[v]) return np.array(windows), np.array(window_labels)滑动窗口后数据量大了随之而来的就是内存问题。我建议在窗口化后再次转成float32然后把所有被试拼接起来保存到一个大npy文件里比如all_data.npy、all_labels.npy。这样内存占用可控后面DataLoader直接加载一个文件就行不用再逐个被试打开。3.3 特征工程直接用原始信号还是提取频带特征做脑电情绪分类模型输入有两种路线。一种是把预处理后的时域信号直接送进神经网络让模型自己学特征另一种是先提取频带特征比如把信号分解成delta、theta、alpha、beta、gamma几个频段的功率谱密度再把这些特征喂给传统机器学习或神经网络。对于DEAP数据集很多人喜欢提取功率谱密度PSD特征。原因很简单脑电情绪识别的研究文献里alpha频段和beta频段的功率变化与情绪状态密切相关。用傅里叶变换提取频带特征输入维度大幅缩小训练速度也快不少。还可以用库自带的方法帮助提取比如scipy.signal.welch可以直接算功率谱。from scipy.signal import welch def extract_psd(data, fs128): freqs, psd welch(data, fsfs, nperseg256) return freqs, psd不过我的建议是如果你是初学者先用原始时域信号配合神经网络跑通流程等后面想提升精度时再考虑频带特征。原因在于频带特征提取涉及很多参数比如窗函数、重叠率、频带划分各参数之间相互影响一旦结果不好很难确定该调哪里。先跑通流程再逐步优化特征是更稳的学习路径。4. 情绪分类模型的代码实现4.1 标签二值化与数据划分策略假设现在做效价Valence二分类。把评分大于等于5的样本视为正类高情绪小于5视为负类低情绪。这一步虽然简单但直接决定了模型学习的目标。y (labels[:, 0] 5).astype(int) # 取valence这一列这里一定要检查一下正负样本比例。DEAP的评分存在一定的不平衡风险如果正样本比负样本多很多模型可能倾向于把大部分样本都预测成正样本导致准确率虚高。初筛时直接打印np.bincount(y)看类别数量如果类别不均衡训练时可以考虑在损失函数里设置类别权重。数据划分策略是另一个关键点。如果先做滑动窗口再随机划分训练集和测试集同一个视频切出来的窗口会被同时分到两边造成泄漏导致测试结果虚高。正确的做法是先按视频或按被试划分再进行窗口化。from sklearn.model_selection import train_test_split video_ids np.arange(data.shape[0]) train_videos, test_videos train_test_split(video_ids, test_size0.2, random_state42)如果要更严格地评估跨被试泛化能力采用留一被试交叉验证LOSO也就是每次拿其中31个人的数据训练剩下的1个人来测试遍历32次。这种方法的结果更接近真实应用但训练成本高不建议在调参阶段频繁使用。4.2 一个能跑通的CNN模型结构DEAP的时域信号可以看成“通道数×时间采样点”的二维结构和图像的“高×宽”很类似所以卷积神经网络天然适用。EEGNet是脑电分类里一个经典又轻量的网络它的设计思路是先做空间卷积混合通道信息再做时间卷积提取时序特征。我给一个简化版输入形状是(batch, 1, 通道数, 时间点数)import torch import torch.nn as nn class SimpleEEGNet(nn.Module): def __init__(self, n_channels40, n_classes2): super().__init__() self.features nn.Sequential( # 空间卷积混合40个通道的信息相当于跨通道特征提取 nn.Conv2d(1, 8, kernel_size(n_channels, 1), padding0), nn.BatchNorm2d(8), nn.ReLU(), # 时间卷积在时间维度上卷积 nn.Conv2d(8, 16, kernel_size(1, 64), stride(1, 4), padding(0, 32)), nn.BatchNorm2d(16), nn.ReLU(), nn.AdaptiveAvgPool2d((1, 256)), ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(16 * 1 * 256, 64), nn.ReLU(), nn.Dropout(0.5), nn.Linear(64, n_classes), ) def forward(self, x): x self.features(x) x self.classifier(x) return x注意第一层卷积的kernel_size(n_channels, 1)意思是卷积核把40个通道一次性卷积完输出特征图的高度变成1。这种写法把“空间特征融合”和“时间卷积”拆开了是EEGNet的核心思想。用AdaptiveAvgPool是为了让全连接层的输入维度固定不管时间窗口取多长最后池化到固定尺寸避免因为窗口长度不一导致维度报错。在模型输入之前需要把numpy数组转成PyTorch张量并且调整维度顺序def to_torch(data_np): # 输入形状 (N, C, T) - (N, 1, C, T) return torch.tensor(data_np, dtypetorch.float32).unsqueeze(1)4.3 训练循环与验证评估训练部分的代码相对固定我直接给出一个完整的训练循环示例from torch.utils.data import TensorDataset, DataLoader train_dataset TensorDataset(train_x, train_y) test_dataset TensorDataset(test_x, test_y) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) test_loader DataLoader(test_dataset, batch_size64, shuffleFalse) model SimpleEEGNet(n_channels40, n_classes2) optimizer torch.optim.Adam(model.parameters(), lr0.001) criterion nn.CrossEntropyLoss() epochs 30 for epoch in range(epochs): model.train() total_loss 0.0 for batch_x, batch_y in train_loader: optimizer.zero_grad() outputs model(batch_x) loss criterion(outputs, batch_y) loss.backward() optimizer.step() total_loss loss.item() # 每个epoch结束评估一次 model.eval() correct 0 total 0 with torch.no_grad(): for batch_x, batch_y in test_loader: outputs model(batch_x) preds torch.argmax(outputs, dim1) correct (preds batch_y).sum().item() total batch_y.size(0) accuracy correct / total print(fEpoch {epoch1}/{epochs}, Loss: {total_loss/len(train_loader):.4f}, Acc: {accuracy:.4f})跑训练时会发现损失降得很快但验证集准确率可能一直在60%到70%徘徊这是DEAP这类小样本脑电数据的正常现象。不要被论文里的90%以上准确率吓到那些往往用了很复杂的预处理、数据增强、跨被试对齐等手段。你的第一版模型能稳定跑通、准确率高于随机水平二分类就是50%以上就已经是很好的起步了。调参时几个经验值供参考批量大小64到128都可以学习率先从0.001开始如果损失震荡厉害就降到0.0005。训练轮次设一个固定值比如30轮然后观察验证集准确率如果连续5轮没有提升就手动停掉节省时间。早停后保存最优模型权重torch.save(model.state_dict(), deap_valence_model.pt)5. 环境与实现中的常见问题速查5.1 问题汇总与排查方案这一段总结我在DEAP项目里实际遇到过的、以及帮助别人排查过的高频问题整理成表格方便你遇到问题时直接对照。问题现象可能原因解决办法loadmat报错文件无法读取文件下载不完整或用了.dat格式检查文件大小是否与其他被试一致.dat改用h5py读取h5py提示对象无法打开文件根本不是HDF5格式确认后缀是.dat且来自官方不要自己改名内存溢出程序被kill把所有被试数据一次性全部载入内存按被试逐个处理然后保存npy或把信号降采样到64Hz.npy加载后形状是(8064, 40, 40)numpy默认按C顺序读数组维度顺序和预期不同打印data.shape确认必要时用np.transpose(data, (2, 1, 0))调整模型训练时维度不匹配卷积层输出尺寸经pooling后和全连接层对不上打印每一层输出形状用AdaptiveAvgPool固定输出尺寸验证集准确率异常高接近100%数据泄漏同一个视频的窗口同时出现在训练集和测试集先划分视频再切窗口严格隔离装torch时下载太慢未指定国内或PyTorch官方合适的下载源用PyTorch官网的安装命令配合合适的index-urlscipy.io.loadmat读出负数或全0原始数据里有未处理的伪迹或读取字段错误打印数据最大值、最小值、均值检查字段名WSL2里运行代码特别卡文件放在/mnt/c/跨文件系统访问把项目放到WSL2内部的~/projects目录下表格里的第一项也就是文件读取问题是新手咨询里占比最高的。尤其是.dat文件很多人以为是文本文件用记事本打开后发现乱码就开始怀疑文件损坏。实际上DEAP的.dat是HDF5格式本身是二进制科学数据格式用记事本看本来就是乱码这不代表文件有问题直接用h5py读取就好。另外Windows用户如果用了torch.cuda.is_available()返回False先别急着装CUDA。先确认你的显卡是不是NVIDIA以及显卡驱动版本是否支持。实测下来很多新人的机器根本没有独立显卡或者显卡是AMD的那CPU训练完全够用不需要纠结GPU加速。5.2 我的几条避坑建议这几条建议来自我个人的多次踩坑经历不是教程里会写的内容。第一条不要一开始就想着把32个被试全部处理完。先选1个被试的文件把数据读取、基线校正、窗口切分、模型训练的完整链路跑通确认每个环节都没问题后再写循环批量处理其余被试。不然你花半小时处理完32个文件最后发现模型输入的维度写错了回过头来又得要重新处理很崩溃。第二条保存中间结果时多留一个心眼。比如把预处理后的数据保存成processed_s01.npy时顺手把当时的通道数、采样率、窗口参数写进一个config.json或者至少写在注释里。不要小看这一步DEAP这种项目很容易做了一两周之后回看代码完全想不起来当时设置的是什么采样率。第三条跑训练前先跑通一个“单batch冒烟测试”。也就是用10个样本的训练集跑1个step确认前向传播、反向传播、梯度更新都没报错然后再正式跑完整训练。这一步能避免很多“训练到一半爆显存”或者“维度报错要到第14轮才爆发”的低级问题。方法很简单就是直接把train_loader里的第一个batch取出来调用一次模型和loss函数。第四条也是最重要的一条——先别急着追求精度。DEAP这个任务想做高精度牵扯到的因素太多了数据增强方式、通道选择、频带特征、模型结构、正则化手段、跨被试对齐策略每一项都能写一整篇博客。但对初学者来说把一个能跑的框架搭起来比什么都重要。先有一个能稳定输出结果的baseline哪怕准确率只有55%你也有了一个可以持续改进的起点。最后再分享一个小细节。我一开始处理DEAP时没有做基线校正就直接切窗口结果发现同一个被试的不同窗口预测结果完全靠猜后面补上基线校正后准确率才稳定上升。现在每次拿到新的脑电数据我都会条件反射式地先看前几秒是不是静息基线这已经成了肌肉记忆。这个项目做到后面你会发现情绪检测技术栈里最难的往往不是模型而是对数据本身的理解。DEAP的好处在于它把原始生理信号和主观评分都开放出来了特别适合用来建立“从数据处理到模型训练”的完整认知。先把这一套流程跑通之后换任何脑电数据集核心思路都是通用的。