资讯动态

脑电情绪识别:RNN+CNN模型在SEED/DEAP/SEED-IV上的工程实践

发布时间:2026/10/27 14:54:36 来源:尧图企业网站定制
简介面向脑电情绪识别研究者与深度学习入门者这份资源整合论文原文与可运行源码完整实现了结合层次RNN与CNN的双重情绪识别模型。模型同时处理基于序列的脑电频带功率表示与基于图像的特征向量表示并通过显著性分析促进双分支联合学习已在SEED、DEAP、SEED-IV、MPED四个公开数据集上验证。压缩包共21个文件、9.45MB其中7个Python脚本覆盖特征提取、模型定义与训练流程8个npy文件提供预处理的电极位置、输入图像及标签数据另含PDF论文、环境配置文件及说明文档便于快速复现实验环境。PDF论文详细阐述了方法细节npy数据已按统一流程处理省去自行转换脑电原始数据的繁琐步骤。目前已有2691人学习或下载适合需要参考可运行代码、直接对比多数据集情绪识别效果的研究者。1. RNN和CNN结合的脑电情绪识别这个方案能让你少走三个月弯路EEG情绪识别这个方向论文不少真正能复现的开源工程却不多。很多研究组把预处理、特征、模型各自封装在自己的脚本里换个数据集就要重写一遍。手头这个「论文加源码RNN和CNN结合的脑电情绪识别数据集为seed、deap、seed-iv.zip」把SEED、DEAP、SEED-IV三份公开数据集上的完整闭环都串起来了论文负责讲清楚RNN循环神经网络和CNN卷积神经网络为什么互补源码把从原始mat文件到训练曲线的每一步都铺开。先说结论在DEAP的32通道128Hz数据上单用CNN做二分类准确率往往卡在70%上下把双向LSTM接在卷积特征后面普遍能往上拉5到8个点。这个差距不是玄学——情绪诱发的脑电慢波分布在秒级时间尺度上CNN感受野不够长RNN循环神经网络恰好补上这一段。适合刚入脑机接口方向的研究生也适合想用公开数据集跑基线却不想从零写预处理的一线工程师。2. SEED、DEAP、SEED-IV三份数据集的预处理读mat、去基线、算差分熵做脑电情绪识别第一道坎不是模型是数据。三份数据集各有各的脾气采集设备不同62导与32导、采样率不同1000Hz采集与128Hz预处理、刺激材料不同中文影片片段与英文音乐视频、标签体系不同三分类、四分类与连续量表。如果你只在一个数据集上调参模型很可能学到的是这个数据集的采集噪声而不是情绪本身的表征。把这套源码里的数据读取和预处理逻辑拆开看能省掉大量翻论文查格式的时间。2.1 三个数据集先摆在一张表里不用来回翻论文数据集通道数采样率受试者试次规模标签形式常见难度SEED62导EEG1000Hz采集常用200Hz15人×3个session每个session 15段视频负性/中性/正性三分类中等DEAP32导EEG8路外围信号128Hz预处理版本40人每人40段音乐视频valence/arousal/dominance/liking连续量二分类相对好做SEED-IV62导EEG同SEED15人×3个session每个session 24段视频快乐/悲伤/恐惧/中性四分类较难DEAP的连续标签需要自己动手转二分类社区惯例是把valence和arousal各自按5分阈值切开大于5为正性/高唤醒小于等于5为负性/低唤醒。SEED和SEED-IV则是直接给出离散情绪标签但SEED是三分类、SEED-IV是四分类后者对模型分辨力的要求明显更高。三份数据集都按受试者分组存放跨session评估也顺理成章——同一个被试在不同日期的三次实验本身就是天然的域偏移测试。2.2 DEAP的读取先拿前32导脑电通道丢掉3秒基线DEAP官方预处理好后的数据是mat文件每个被试一个文件变量名固定为data和labels。data的形状是(trial数量, 通道数量, 采样点数量)40个通道里前32个是真正EEG信号后8个是EOG、EMG等外围生理信号做情绪分析时通常直接丢掉。import scipy.io as scio import numpy as np def load_deap_subject(mat_path, eeg_onlyTrue, drop_baselineTrue, fs128): mat scio.loadmat(mat_path) data mat[data] # 形状(40个trial, 40个通道, 8064个采样点) labels mat[labels] # 形状(40个trial, 4)对应valence/arousal/dominance/liking if eeg_only: data data[:, :32, :] # 只保留前32导EEG丢掉外围生理信号 if drop_baseline: data data[:, :, 3 * fs:] # 每段前3秒是静息基线只留刺激呈现的60秒 return data, labels这里有两个参数值得盯住。fs128对应DEAP预处理版的采样率8064个采样点等于128Hz乘以63秒去掉前3秒基线后剩60秒、7680个点。eeg_onlyTrue是社区最常见做法因为外围信号和情绪的关系不是本文关心的事。如果之后要做跨数据集迁移把数据统一成(trial, 通道, 时间)这种格式是第一步。2.3 SEED和SEED-IV的差分熵特征每个通道变成5个频带的能量SEED官方同时提供了原始波形和微分熵DE特征两种版本。社区里跑RNNCNN混合模型时用DE特征比用原始波形更常见因为DE特征把脑电的频带能量压缩成紧凑表示模型不需要自己从时域波形里学出频带信息。微分熵的定义简洁对某个频带内的信号DE等于0.5 * log(2πeσ²)本质就是该频带信号方差的对数能量。from scipy import signal def de_feature_per_window(win, fs200): win形状为(n_channel, n_samples)逐通道逐频带计算微分熵返回(n_channel*5,) bands [(1, 3), (4, 7), (8, 13), (14, 30), (31, 45)] # delta/theta/alpha/beta/gamma feats [] for ch in range(win.shape[0]): # 遍历每一个EEG通道 for low, high in bands: # 遍历5个频带 b, a signal.butter(4, [low, high], btypebandpass, fsfs) y signal.filtfilt(b, a, win[ch].astype(np.float64)) de 0.5 * np.log(2 * np.pi * np.e * np.var(y) 1e-8) feats.append(de) return np.array(feats, dtypenp.float32)注意signal.butter传入了fs参数这是SciPy 1.2.0之后的写法旧版本需要手动把截止频率归一化到[0, 1]。1e-8是防log(0)的保险实测中不加这个常数某些通道方差恰好为0时会直接算出-inf。SEED的原始采样率是1000Hz但社区常用200Hz重采样版本fs200时一个1秒窗口就是200个采样点切窗逻辑和DEAP完全一致。2.4 统一成同一种tensor格式(样本数, 通道数×频带数, 时间步)DEAP用原始波形、SEED用DE特征两者进入模型前必须统一形状。做混合模型时我习惯把DE特征也保留时间维对一张1秒窗口内的信号不把一个窗口的DE压成一根向量而是按时间继续细分让RNN循环神经网络有序列可读。常见做法是把1秒窗口切成10个100ms子窗每个子窗算62通道×5频带310维DE向量于是每个样本变成(310, 10)再unsqueeze成(1, 310, 10)给卷积层。def de_sequence(win, fs200, sub_win100): 把一段(n_channel, n_samples)信号切成子窗并提取DE返回(n_channel*5, n_sub_windows) step int(fs * sub_win / 1000) # 100ms子窗对应的采样点数 n_sub win.shape[1] // step cols [] for i in range(n_sub): seg win[:, i * step:(i 1) * step] cols.append(de_feature_per_window(seg, fsfs)) return np.stack(cols, axis1) # (n_channel*5, n_sub)这一步做完三个数据集在模型眼里就是同一种模态了。DEAP原始波形可以直接保持(32, 时间步)进入模型也可以照上面方式转成频带序列再进模型SEED和SEED-IV则几乎都是走DE序列这条路线。统一成(batch, 通道数×频带数, 时间步)之后第3章的模型代码就能完全复用不需要为每个数据集各写一套模型。3. 模型结构选型CNN卷积神经网络提空间与局部时域特征RNN循环神经网络管长程依赖模型结构是整个方案的核心也是论文里最容易读晕的部分。先把思路理清脑电情绪识别里CNN负责什么、RNN负责什么、两者拼接时怎么对齐维度这三个问题想透源码里的训练部分就是水到渠成的事。3.1 为什么纯CNN会在情绪识别上翻车图像上的深度学习cnn模型大杀四方但把同样的套路搬来脑电上效果往往不如预期。原因不复杂图像的空间关系是静态的而EEG是低信噪比、强时序依赖的信号。情绪诱发的慢波节律集中在theta到gamma频带单次事件相关电位只有几十毫秒但情绪状态的建立和维持是秒级的。普通1D卷积的感受野等于kernel_size × 层数想覆盖到一秒以上的上下文必须堆很深或者靠空洞卷积拉感受野。社区里纯CNN方案能跑出好成绩的几乎都用了dilation这就把工程复杂度抬高了。RNN循环神经网络论文里反复出现的一个结论是LSTM或GRU在时间维上维护隐状态能把整个窗口内数百个时间步的信息串在同一条路径上天然适合秒级情绪节律的建模。所以「卷积提局部、循环提长程」这种分工是经过大量实验验证的合理组合而不是为了拼凑网络结构硬凑热点。3.2 结合方式怎么选串行、并行、Attention三种搭法RNN和CNN结合的主流方案有三类按复现难度排序串行CNN→BiLSTM输入先进卷积层提取局部特征输出序列再交给双向LSTM建模长程依赖最后池化接分类头。这是最常用也最稳的结构训练时不容易翻车建议新手先跑通这条线。并行双分支CNN分支和LSTM分支各自吃同一份输入特征拼接后接全连接层。理论上能同时保留多粒度信息但两个分支收敛速度不一致时调参成本更高。CNN-BiLSTM-AttentionCNN输出的特征序列先进注意力层再进LSTM或替代LSTM。效果通常最好但训练也更敏感属于调好血赚、调不好血压拉满的方案。3.3 用Pytorch搭建一个可复现的CNN-BiLSTM混合模型我一般先用串行结构打底。输入形状是(batch, in_channels, time_steps)其中in_channels决定拼接方式DEAP原始波形是1SEED的DE特征是62×5310。下面这个模型定义可以直接抄import torch import torch.nn as nn class CNN_BiLSTM(nn.Module): def __init__(self, in_channels310, hidden128, n_layers2, n_class3, dropout0.3): super().__init__() self.conv nn.Sequential( nn.Conv1d(in_channels, 64, kernel_size3, padding1, biasFalse), nn.BatchNorm1d(64), nn.ReLU(inplaceTrue), nn.MaxPool1d(2), # 时间步减半 nn.Conv1d(64, 128, kernel_size3, padding1, biasFalse), nn.BatchNorm1d(128), nn.ReLU(inplaceTrue), ) self.lstm nn.LSTM(128, hidden, num_layersn_layers, batch_firstTrue, bidirectionalTrue, dropoutdropout) self.head nn.Sequential( nn.Linear(hidden * 2, 64), nn.ReLU(inplaceTrue), nn.Dropout(dropout), nn.Linear(64, n_class), ) def forward(self, x): # x: (batch, in_channels, time_steps) h self.conv(x) # (batch, 128, time_steps//2) h h.permute(0, 2, 1) # (batch, time_steps//2, 128) out, _ self.lstm(h) # (batch, time_steps//2, hidden*2) out out.mean(dim1) # 对所有时间步做平均池化 return self.head(out) # (batch, n_class)两个细节值得说明。第一nn.LSTM设置batch_firstTrue之后输入输出都是(batch, seq_len, feature)避免在时间维和batch维之间来回倒腾。第二池化方式选的是对LSTM输出所有时间步取平均而不是取最后时刻的隐状态——EEG信号没有自然句尾最后一个时间步并不代表整个窗口的总结平均池化在实验中更稳。hidden取128、n_layers取2是兼顾参数量和训练速度的常见配置如果数据量小可以把n_layers降到1防止过拟合。3.4 输入布局的细节通道、频带、时间三个维度怎么摆in_channels是个容易踩坑的位置。把通道数×频带数直接拼成卷积的输入通道是一种妥协但行之有效的做法卷积核在时间维上滑动同时隐式地混合通道与频带信息。如果你想把空间拓扑关系利用得更充分可以把输入改成(batch, 1, 通道数, 时间步)用2D卷积同时滑通道和时间两个方向或者更进一步用图卷积处理电极位置。但对RNNCNN这个标题来说最简单的拼接方式反而是最容易复现的先用in_channels62*5310跑通再逐步往复杂结构走。4. 把源码跑通的关键节点数据划分、训练参数与评估代码拿到源码后别急着python train.py。这套流程里最影响结果的东西不是网络结构本身而是数据划分方式和评估指标。同一个模型用subject-dependent划分可能刷到90%以上换成subject-independent立刻跌到60%多这种落差在脑电情绪识别里极其常见。4.1 两种评测设定subject-dependent是自嗨subject-independent才是真实水平按窗口随机划分训练集和测试集叫subject-dependent设定同一个人的大量数据同时出现在两边模型记住了这个人的脑电特征换个人立刻失效。这种设定下跑出来的分数没有参考价值。正经做法是留一被试交叉验证LOSO15个受试者里轮流拿1个做测试剩下14个做训练最终报告15次测试的平均准确率和标准差。社区报告数字时都会注明评测方式看到论文里不注明的要留个心眼。subjects list(range(15)) # 假设15个被试按受试者索引切分 for test_sub in subjects: train_subs [s for s in subjects if s ! test_sub] # 用 train_subs 对应的全部trial组装训练集 # 用 test_sub 对应的全部trial组装测试集 # 模型训练与评估完成后记录 test_sub 上的准确率这段骨架代码的关键是切分发生在受试者维度而不是数据行维度。先把每个被试的所有数据和标签按索引分组再在训练循环外面套这层循环。4.2 训练配置抄这一组先跑通再调参参数推荐值说明优化器Adam脑电任务上Adam比SGD收敛快调参压力小学习率1e-3带LSTM的模型不建议从1e-2起步容易震荡batch_size64DE特征输入时显存压力小可以适当加大滑动窗口1秒50%重叠1秒窗口能覆盖alpha节律的完整周期epoch上限50配合早停实际多数在20~30轮收敛early stoppingpatience10验证集指标连续10轮不提升就停梯度裁剪clip1.0防止BiLSTM反向传播时梯度爆炸权重衰减1e-4轻微正则DE特征维度较高时有用我习惯在train_one_epoch里就挂上梯度裁剪而不是等loss变NaN再回来加。脑电数据噪声大偶尔一个样本的梯度异常就能毁掉整个训练曲线裁剪是最便宜的后悔药。4.3 训练循环与评估准确率之外一定要算macro F1def train_one_epoch(model, loader, opt, criterion, clip1.0): model.train() losses [] for x, y in loader: opt.zero_grad() logits model(x) loss criterion(logits, y) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), clip) opt.step() losses.append(loss.item()) return float(np.mean(losses)) torch.no_grad() def evaluate(model, loader, criterion): model.eval() preds, trues, loss_sum [], [], 0.0 for x, y in loader: logits model(x) loss_sum criterion(logits, y).item() * x.size(0) preds.append(logits.argmax(dim1).cpu().numpy()) trues.append(y.cpu().numpy()) preds np.concatenate(preds) trues np.concatenate(trues) acc accuracy_score(trues, preds) f1 f1_score(trues, preds, averagemacro) return acc, f1, loss_sum / len(trues)criterion用nn.CrossEntropyLoss()如果类别不均衡可以在初始化时传入weight参数。evaluate里计算loss时乘了x.size(0)是为了在batch大小不一致时仍能按样本数归一化否则最后的平均loss会被大batch主导。准确率之外务必同时打印macro F1特别是在SEED-IV四分类任务上单独看acc可能被某个占比偏高的类别带偏。4.4 LOSO完整流程把三份数据集都跑一遍大概需要多久DEAP跑40折LOSO、SEED和SEED-IV跑15折LOSO每折都要独立训练一个模型。如果只用CPU跑DEAP一折大约几十分钟到几小时SEED因为62通道DE特征维度高时间更长。建议先用一到两个被试做冒烟测试确认整个pipeline能跑通、指标不会太低再挂机跑完整LOSO。源码里如果默认只跑subject-dependent记得改成LOSO之后数值会大幅缩水别慌那是真实的泛化水平。5. 避坑复现RNNCNN脑电情绪识别时最常见的4个问题这套流程的坑集中在数据准备和训练设定两片区域。下面四条是我在这类项目里反复踩过的按出现频率排序每一条都按「现象→原因→解决」拆开。5.1 训练集95%测试集55%标准化把测试集信息泄漏进去了现象模型在训练集上收敛得又快又漂亮验证集却一塌糊涂差距大到不像是正常泛化落差。原因预处理时先对全部数据调用了StandardScaler.fit()再切分训练集和测试集导致测试集的均值和标准差已经参与过训练。这是脑电情绪识别里最常见的泄漏方式。解决把scaler的fit操作严格限定在训练集上测试集只用同一个已拟合的scaler做transform。注意DE特征提取时用到的全局统计量同理也要在训练集上计算。提示凡是在全数据集上做的任何统计操作包括均值和方差、归一化、异常值截断全部都有可能造成跨受试者信息泄漏。5.2 按窗口随机切分后成绩虚高相邻窗口来自同一个trial现象用滑动窗口切出成千上万个样本直接train_test_split随机划分后测试集准确率比按trial切分高出10个百分点以上。原因窗口之间有重叠stride小于窗口长度时相邻样本高度相似同一个trial的数据被同时分到了训练集和测试集相当于测试集里混入了训练样本的近亲。解决先按trial维度和subject维度切分再在每个trial内部滑窗生成样本保证任何窗口都不会跨界。LOSO天然规避了这个问题。5.3 loss突然变NaN或者训练集都不收敛现象训练到某一步loss变成NaN或者loss一直维持在初始值附近不动弹。原因LSTM在长序列上反向传播路径很深梯度爆炸是常事另一种可能是学习率太高模型在loss曲面边缘来回弹跳。解决梯度裁剪clip_grad_norm_挂上clip1.0起步学习率从1e-3往下调如果还不收敛就试3e-4。还有一个冷门原因输入没有做float32转换某些数据读进来是float64Pytorch和numpy混用时会悄悄报类型错误。5.4 只报acc不报macro F1SEED-IV上四分类被主导类别支配现象SEED-IV跑完准确率看着有75%看一眼混淆矩阵才发现其中一个类别几乎没被预测出来。原因四分类任务里三个负性情绪类别特征相似模型倾向于把所有样本都判成占比最高的那一类acc照样不低。解决监控macro F1而不是单看acc给CrossEntropyLoss传入按类别频率倒数归一化的weight评估时把混淆矩阵打印出来逐类别看precision和recall。这也是社区后来普遍要求报告macro F1的原因——acc在这个任务上真的会骗人。6. 进阶用留一被试交叉验证和滑动窗口投票让结果经得起审稿人追问基础流程跑通之后有两件事能让结果可信度上一个台阶一是把LOSO的结果完整报告出来带均值和标准差二是用滑动窗口投票替代单窗口预测让每个trial的预测更稳。LOSO完整跑完你会拿到一组逐被试准确率比如15个数字。不要只报平均值把标准差也写上因为某些被试的脑电模式就是和大多数人格格不入导致那一折准确率特别低这是数据集本身的性质不是你的bug。报告格式可以写成74.2 ± 8.5%审稿人看到±号比看到单独一个百分比要放心得多。滑动窗口投票的实现很直接同一个trial被切成了多个窗口每个窗口单独过模型得到预测分布把所有窗口的概率平均取平均概率最大的类别作为这个trial的最终预测。def trial_vote(model, windows): windows: (n_win, in_channels, time_steps)返回trial级预测 preds [] for w in windows: logits model(w.unsqueeze(0)) # (1, n_class) preds.append(torch.softmax(logits, dim1)) prob torch.stack(preds).mean(dim0) # 对窗口维度取平均 return prob.argmax(dim1).item()窗口投票能在原有窗口级指标上再涨1到3个百分点因为它抹平了单个窗口里噪声尖峰造成的误判。还有一个值得做的对照实验把SEED的DE特征直接换成原始波形输入模型结构完全不动看看指标掉多少。如果掉得不多说明模型本身在从时域波形里学到有用的频带信息如果掉得厉害说明DE特征预处理才是这套流程的隐形功臣。这种对比实验写在论文里格外有说服力因为它回答了一个审稿人必问的问题你的增益到底来自模型还是来自特征工程。这套方向值不值得投入取决于你的目标。如果只是跑基线、验证ideaSEED和DEAP的LOSO流程性价比极高如果想冲SOTA注意力机制和跨数据集预训练是下一步该碰的东西。我做这个方向最深的一个教训是第一次在DEAP上跑subject-dependent拿到85%准确率以为自己调通了换成LOSO直接掉到62%才知道之前的分数没有任何参考价值。从那以后任何脑电情绪识别的结果我都先问一句这是哪种评测设定出来的希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑