
简介这份资源面向希望上手脑电情绪识别的研究生、算法初学者与相关课程学习者围绕公开的SEED数据集提供一套可运行的EEG情绪识别实践代码。包内共18个文件以Python脚本、XML配置、Markdown说明与TXT结果记录为主另有docx实验记录和iml工程文件压缩包约10.65MB体量轻便便于快速部署与二次修改。代码部分包含基于DE_LDS特征的SVM分类流程与原始EEG信号的CNN训练脚本并配有utils工具模块覆盖从数据读取、特征提取到模型训练与结果记录的完整链路results目录下的日志与结果文档可用于对照实验表现README与说明文件则帮助理解目录组织与运行方式。目前已有640人学习下载适合作为情绪识别入门练手、课程设计或小论文复现的参考方案读者可据此掌握SEED数据预处理、传统机器学习与深度学习两条基线思路并在此基础上调整特征与网络结构开展对比实验。1. 拿到 SEED 数据集做 EEG 情绪识别先搞清楚这条链路到底难在哪SEED 数据集在 EEG 情绪识别圈子里算是绕不开的基准15 名被试、62 导联、三类情绪积极、中性、消极用 62 通道电极帽按国际 10-20 系统采集采样率 1000Hz每名被试看 15 个电影片段、每个片段约 4 分钟看完立刻做情绪自评。很多人第一次拿到这份数据第一反应是「数据挺规整跑个分类不就完了」然后被现实按在地上摩擦——跨被试准确率死活上不去同一被试内轻松 90%换个人直接掉到 50% 出头接近瞎猜。这条链路真正的难点不在模型结构而在三件事一是 EEG 信噪比极低情绪相关的频段能量变化很容易被眼电、肌电、工频干扰淹没二是样本量看着大、实际有效独立样本少同一段视频切出来的窗口高度相关交叉验证稍不注意就数据泄漏三是 SEED 官方给的预处理版本和原始版本差异不小用哪个、怎么对齐直接决定你后面所有实验能不能复现。这篇笔记就按「数据怎么读进来 → 特征怎么提 → 模型怎么搭 → 跨被试怎么救 → 坑在哪」的顺序把一条能跑通、能复现的 SEED 情绪识别链路讲清楚适合刚接触 EEG 情绪识别、想拿 SEED 做第一个完整实验的人也适合做过但跨被试一直翻车、想找排查方向的人。2. SEED 数据集的读取与预处理从 .mat 到可训练张量2.1 先认清 SEED 的文件结构和两种版本SEED 常见的有两个版本原始版Preprocessed_EEG和官方预处理版Preprocessed_EEG 里已经做了降采样到 200Hz、带通滤波、去眼电等。原始版采样率 1000Hz官方预处理版是 200Hz很多论文里写的「SEED 上 200Hz」指的就是后者。文件按被试组织每个被试一个 .mat里面按 session 和视频片段存成多个变量比如djc_eeg1、djc_eeg2这种命名变量里是[62, T]的矩阵T 是时间点。标签单独放在label.mat里三类情绪对应 1/0/-1积极/中性/消极顺序和视频片段编号对应。我一般直接用官方预处理版理由是省掉一大半滤波和去噪的调试时间而且大部分公开结果都是在这个版本上报的方便横向对比。如果你要做的是「原始信号去噪」这类研究才需要回到 1000Hz 原始版自己走滤波链路。2.2 用 Python 把 .mat 读成 numpy 并切窗import scipy.io as sio import numpy as np # 读取单个被试的预处理 EEG 和标签 eeg sio.loadmat(Preprocessed_EEG/1_20131027.mat) label sio.loadmat(Preprocessed_EEG/label.mat)[label].flatten() # 该被试的 15 段视频数据变量名形如 djc_eeg1 ... djc_eeg15 segments [] for i in range(1, 16): key fdjc_eeg{i} seg eeg[key] # shape: [62, T] segments.append(seg) # 按 1 秒窗、无重叠切分200Hz 下每窗 200 点 win_len 200 X, y [], [] for seg_idx, seg in enumerate(segments): n_win seg.shape[1] // win_len for w in range(n_win): window seg[:, w*win_len:(w1)*win_len] # [62, 200] X.append(window) y.append(label[seg_idx]) X np.array(X) # [N, 62, 200] y np.array(y) print(X.shape, y.shape)这段代码做了三件事按变量名把 15 段数据取出来、按固定窗长切分、把片段标签广播到每个窗口。win_len200对应 200Hz 下的 1 秒窗这是 SEED 上最常用的窗长之一再短0.5 秒频域分辨率不够再长4 秒单段视频切出来的窗口数太少、样本不够。切窗时不要重叠重叠窗会让训练集和验证集高度相关后面交叉验证的分数会虚高这是新手最容易踩的坑之一。2.3 标签对齐和通道选择的两个细节标签对齐上label.mat里的顺序是固定的 15 个片段但不同被试的文件里变量编号不一定严格按 1 到 15 排列读之前最好先print(eeg.keys())确认一遍别默认djc_eeg1就是第一段。通道选择上62 导联全用是最稳的但如果你要做轻量化或者可穿戴方向常见做法是挑出情绪相关较强的额区和中颞区通道比如 Fp1、Fp2、F3、F4、F7、F8、T7、T8、P3、P4 等通道数能砍到 20 以内代价是准确率通常掉 3 到 8 个百分点具体掉多少要自己在验证集上测。提示SEED 官方预处理版已经做过降采样和滤波不要再叠加一次 50Hz 陷波否则会把情绪相关的低频成分一起削掉我见过有人这么干完准确率直接掉 10 个点。3. 特征提取频段能量、微分熵和它们为什么在 SEED 上管用3.1 为什么 SEED 上微分熵是默认首选特征SEED 上最经典的特征是微分熵Differential Entropy, DE论文里报得最多的也是它。原因不玄学DE 在固定频段内和高斯分布下的对数能量近似等价而 EEG 各频段delta 1-4Hz、theta 4-8Hz、alpha 8-14Hz、beta 14-30Hz、gamma 30-50Hz的能量变化和情绪唤醒度、效价都有相关性尤其是 beta 和 gamma 在积极情绪下能量上升比较明显。DE 相比直接算功率谱密度数值更稳定、对窗长不敏感做跨被试时鲁棒性更好。3.2 用 scipy 算 DE 特征的最小实现from scipy.signal import welch import numpy as np bands {delta: (1, 4), theta: (4, 8), alpha: (8, 14), beta: (14, 30), gamma: (30, 50)} def compute_de(window, fs200): # window: [62, 200] feats [] for ch in range(window.shape[0]): sig window[ch, :] freqs, psd welch(sig, fsfs, npersegmin(200, len(sig))) for name, (lo, hi) in bands.items(): idx np.logical_and(freqs lo, freqs hi) band_power np.trapz(psd[idx], freqs[idx]) # 高斯假设下 DE 0.5 * log(2*pi*e*sigma^2)用 band_power 近似 de 0.5 * np.log(2 * np.pi * np.e * band_power 1e-8) feats.append(de) return np.array(feats) # [62*5] X_de np.array([compute_de(w) for w in X]) print(X_de.shape) # [N, 310]welch用默认的汉宁窗做功率谱估计nperseg取窗长本身保证频率分辨率够分到 1Hz 级别。np.trapz对频段内 PSD 积分得到该频段能量再套 DE 公式。加1e-8是防止 log(0)这个细节不做的话遇到全零通道会直接出 nan后面模型训练报错还不好定位。每个通道 5 个频段62 通道就是 310 维这个维度对 SVM 和浅层网络都还算友好。3.3 特征归一化和几种常见组合的取舍DE 特征做完之后一定要做被试内归一化常见做法是对每个被试的每个特征维度做 z-score或者按 session 做归一化。不做归一化的话不同被试之间的绝对能量差异会盖过情绪带来的相对变化跨被试直接崩。除了 DE常见的还有 PSD、Hjorth 参数、微分不对称DASM、有理不对称RASM等DASM/RASM 是左右脑对称通道的 DE 差值/比值对效价区分有一定帮助可以拼在 DE 后面一起送进模型维度翻倍但通常能涨 1 到 3 个点。特征不是越多越好我试过把 DE、PSD、Hjorth 全拼一起维度到 1000结果因为冗余和过拟合验证集反而掉了最后只留 DE DASM。注意归一化的统计量只能用训练集算再应用到验证集和测试集。用全量数据算均值和方差是典型的数据泄漏分数会虚高但换被试就现原形。4. 模型搭建从 SVM 基线到 EEGNet 的落地选择4.1 先用 SVM 把基线跑出来别一上来就上深度网络。SEED 上 SVM DE 是一个很强的基线被试内经常能到 85% 以上跨被试也有 55% 到 65%。先把这个基线跑通你才知道后面的深度模型是真涨了还是只是调参调出来的。from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline from sklearn.model_selection import cross_val_score clf make_pipeline(StandardScaler(), SVC(kernelrbf, C1.0, gammascale)) # 被试内 5 折注意按时间块分折不要随机打乱窗口 scores cross_val_score(clf, X_de, y, cv5, scoringaccuracy) print(scores.mean(), scores.std())StandardScaler放在 pipeline 里是为了让归一化只在每折的训练部分 fit避免泄漏。C1.0、gammascale是稳妥起点SEED 上 C 在 0.1 到 10 之间调gamma 用 scale 通常够用。交叉验证这里有个关键点按时间块分折比如前 12 个窗口训练、后 3 个验证而不是随机打乱。随机打乱会让相邻高度相关的窗口同时出现在训练和验证集分数虚高 5 到 10 个点这个坑我在早期实验里踩过不止一次。4.2 EEGNet 在 SEED 上的输入形状和关键参数EEGNet 是 EEG 分类里最常被拿来当深度基线的紧凑网络参数量小、不容易过拟合适合 SEED 这种样本量中等的场景。输入形状是[batch, 1, channels, time]SEED 上就是[N, 1, 62, 200]。import torch import torch.nn as nn class EEGNet(nn.Module): def __init__(self, n_ch62, n_time200, n_cls3, F18, D2, F216, drop0.5): super().__init__() self.block1 nn.Sequential( nn.Conv2d(1, F1, (1, 64), padding(0, 32), biasFalse), nn.BatchNorm2d(F1), nn.Conv2d(F1, F1*D, (n_ch, 1), groupsF1, biasFalse), # 深度卷积 nn.BatchNorm2d(F1*D), nn.ELU(), nn.AvgPool2d((1, 4)), nn.Dropout(drop) ) self.block2 nn.Sequential( nn.Conv2d(F1*D, F1*D, (1, 16), padding(0, 8), groupsF1*D, biasFalse), nn.Conv2d(F1*D, F2, (1, 1), biasFalse), nn.BatchNorm2d(F2), nn.ELU(), nn.AvgPool2d((1, 8)), nn.Dropout(drop) ) self.fc nn.Linear(F2 * (n_time // 32), n_cls) def forward(self, x): x self.block1(x) x self.block2(x) x x.flatten(1) return self.fc(x) model EEGNet() print(sum(p.numel() for p in model.parameters())) # 参数量约几千F18、D2、F216是 EEGNet 原论文的默认配置SEED 上直接能用。第一个卷积核(1, 64)在时间维上做滤波相当于学一个可训练带通深度卷积(n_ch, 1)在通道维上做空间滤波这是 EEGNet 能跨通道泛化的关键。drop0.5在 SEED 上偏大样本少的时候可以调到 0.25 到 0.3我一般先用 0.5 跑一版过拟合明显再降。训练时学习率 1e-3、Adam、batch size 64 是稳妥起点epoch 不用太多20 到 30 就够再多就开始过拟合。4.3 被试内和被试间实验该怎么设计被试内within-subject每个被试单独训练一个模型用该被试自己的数据分训练/测试。这个设定下准确率普遍高SVM 和 EEGNet 都能到 85% 到 95%但它的实际意义有限因为真实场景里你不可能给每个新用户重新采一遍标定数据。被试间cross-subject留一被试法LOSO用 14 个被试训练、1 个被试测试轮流一遍。这个设定才是真正有落地价值的也是 SEED 上分数最难看的浅层方法普遍 55% 到 70%深度方法加上域适应能到 70% 到 85%。做跨被试时训练集内部的验证划分也要按被试分不能按窗口随机分否则你调出来的超参在真正跨被试时会失效。5. 跨被试掉点排查那些让准确率一夜回到解放前的坑5.1 现象被试内 90%跨被试 50% 出头原因不同被试的 EEG 绝对幅值、阻抗、电极位置微小差异导致特征分布整体偏移模型学到的是「这个被试长什么样」而不是「情绪长什么样」。解决先做被试内 z-score 归一化再上域适应方法比如在特征层加 CORAL 或 MMD 损失对齐训练被试和测试被试的二阶统计量或者用 AdaBN 在测试时更新 BatchNorm 的 running stats。我一般先试 AdaBN改动最小、不用重新训练往往能拉回 5 到 10 个点。5.2 现象验证集分数很高一换被试就崩原因数据泄漏。要么是切窗时重叠了要么是归一化用了全量统计量要么是交叉验证随机打乱了窗口。解决切窗不重叠、归一化统计量只在训练折上算、交叉验证按时间块或被试分折。这三条挨个检查一遍能排掉大部分「虚高」问题。5.3 现象训练 loss 一直降验证 loss 早早开始涨原因过拟合。SEED 单被试有效样本其实不多62 通道 310 维特征对 SVM 还好对深度网络就偏少。解决加 dropout、加 L2 正则、减小模型宽度F1 从 8 降到 4、或者做数据增强加高斯噪声、时间裁剪、通道随机丢弃。通道随机丢弃在 EEG 上效果不错相当于让模型不依赖某几个特定通道对跨被试也有帮助。5.4 现象某个被试怎么调都只有 40% 多原因个体差异极端或者该被试数据本身质量差阻抗高、动作多。解决先看该被试的原始信号如果噪声明显考虑剔除或单独做更强的去噪如果信号正常但就是分不开可能是该被试的情绪诱发效果本身弱SEED 里确实有个别被试的自评和标签一致性偏低。这种情况不要死磕把它当异常值处理报告结果时说明剔除理由。5.5 现象换了台机器跑结果对不上原因随机种子没固定、PyTorch 和 numpy 的随机性、GPU 非确定性算子。解决固定torch.manual_seed、np.random.seed、random.seed设置torch.backends.cudnn.deterministic True并且把数据划分的索引存下来复用。EEG 实验里结果复现不了八成是这几个地方没锁死。提示排查跨被试问题时先画一张 t-SNE 或 PCA 图把训练被试和测试被试的 DE 特征投到二维看一眼。如果两团点完全不重叠说明分布偏移严重先做对齐再谈模型如果本来就重叠得不错但模型还是分不开那问题在模型或标签不在域偏移。6. 把跨被试准确率往上推的几个具体技巧前面把链路和坑都过了一遍最后落到一个具体问题跨被试准确率卡在 65% 左右怎么再往上推。我自己的习惯是分三步走每一步都有明确的验证方式不靠玄学调参。第一步特征层面做被试内标准化 通道级重参考。标准化前面说过重参考是把每个通道减去所有通道的均值CAR能压掉一部分共模噪声对跨被试的分布对齐有轻微帮助。这一步做完LOSO 通常能涨 2 到 4 个点验证方式是看 t-SNE 图上训练和测试被试的重叠度有没有变好。第二步模型层面加一个轻量的域对齐损失。以 EEGNet 为例在 flatten 之后、fc 之前接一个 CORAL 损失对齐源域和目标域特征的协方差矩阵def coral_loss(src, tgt): d src.size(1) src_c src - src.mean(0, keepdimTrue) tgt_c tgt - tgt.mean(0, keepdimTrue) cov_s src_c.t() src_c / (src.size(0) - 1) cov_t tgt_c.t() tgt_c / (tgt.size(0) - 1) return ((cov_s - cov_t) ** 2).sum() / (4 * d * d) # 训练时loss ce_loss lam * coral_loss(feat_src, feat_tgt)lam从 0.1 开始试0.5 到 1.0 之间通常效果最好太大反而会让特征塌缩、分类性能下降。CORAL 的好处是不需要目标域标签测试被试的数据可以直接拿来算协方差属于无监督域适应落地时不需要新用户标定。这一步在 SEED 的 LOSO 上一般能再涨 5 到 10 个点具体多少取决于被试对。第三步测试时自适应AdaBN。把训练好的模型拿到测试被试上用测试被试的无标签数据前向几遍只更新 BatchNorm 层的 running mean 和 running var其他参数冻结。这一步几乎零成本但经常能再拉 3 到 5 个点。注意只更新 BN 统计量不要动卷积核和全连接权重否则容易过拟合到测试被试的少量数据上。这三步做完SEED 上 LOSO 到 75% 到 85% 是比较现实的区间再往上就要考虑更复杂的域适应方法或者多源域融合了。最后说个我自己的习惯每次跑完实验把被试编号、随机种子、特征版本、模型配置和最终准确率记在一个表里别嫌麻烦。EEG 实验的变量太多不记录的话两周后你自己都说不清哪个结果对应哪套配置想复现只能重跑那才是真的血泪经验。希望帮到你。本文还有配套的精品资源点击获取