
简介这份资源面向脑电情绪识别方向的研究者与深度学习实践者提供一套将RNN与CNN结合的完整论文与源码实现可用于复现实验、改进模型或作为课程设计参考。方案从序列与图像两种视角表征脑电特征图一路以频带功率构建序列表示另一路以特征向量构建图像表示并通过基于显著性的信息组合方法实现两部分联合学习在SEED、SEED-IV、DEAP、MPED等公开数据集上完成评估。压缩包共21个文件约9.45MB包含7个Python脚本模型定义、特征提取、损失与工具函数、8个npy数据文件电极位置与示例输入、1篇PDF论文以及yml环境配置、依赖清单、README说明和LICENSE等便于快速搭建运行环境。目前已有2691人学习下载适合希望深入理解脑电通道空间关系建模与多模态特征融合的中高级读者参考。1. 拆开这份 RNNCNN 脑电情绪识别包它到底能跑出什么结果脑电情绪识别这个方向很多人卡在第一步——论文里的模型图看得懂但真要把 DEAP 或 SEED 数据集喂进去、把 CNN 和 RNN 拼起来、把 acc 跑到论文里那个数中间隔着一堆没写进论文的细节。这份资源给的就是一条从数据到结果的完整链路CNN 负责从电极通道里抽空间特征RNN 负责把时间序列上的依赖关系串起来两者叠在一起做情绪分类。数据集覆盖 SEED、DEAP、SEED-IV 三个常用基准源码和论文配套适合正在做毕设、写小论文或者想复现 baseline 的人。它不是那种跑完 demo 就扔的玩具而是能让你看清每个环节参数怎么设、坑在哪的工程包。2. CNNRNN 混合模型为什么这样拼、数据怎么进网络2.1 脑电信号的空间与时间双重特性脑电信号本质上是多通道时间序列。以 SEED 为例62 个电极通道按国际 10-20 系统排布每个通道采样率 200Hz一次实验持续几分钟。情绪相关的信息同时藏在两个维度里空间维度上不同脑区前额叶、颞叶、枕叶的激活模式不同这对应 CNN 的卷积核在通道维度上滑动时间维度上情绪不是瞬间产生的而是随时间演化的一段 1 秒的片段和它前后片段之间存在关联这对应 RNN 的时序建模能力。单独用 CNN 做脑电情绪识别常见做法是把多通道信号当成一张二维图通道×时间点来卷但这样时间维度上的长程依赖容易被局部卷积核截断。单独用 RNNLSTM/GRU能捕捉时间依赖但每个时间步输入的是所有通道的拼接向量空间结构信息被压平了。所以混合模型的思路很直接先用 CNN 在空间维度上做特征提取把 62 通道压缩成更紧凑的特征图再把这个特征图按时间顺序喂给 RNN让 RNN 在已经提纯的空间特征上做时序建模。2.2 数据预处理从原始信号到模型输入这份资源里的预处理流程我拆开看大致是这几步。以 SEED 为例原始数据是 MATLAB 的 .mat 文件里面存的是预处理后的 DE 特征或者原始时间序列取决于你用的是哪个版本。DEAP 则是 .dat 格式用 pickle 加载。import scipy.io as sio import numpy as np from sklearn.preprocessing import StandardScaler # 以 SEED 为例加载某个被试的 .mat 文件 # 常见结构data 字段为 (62, T) 的矩阵label 为 (1, T/片段长度) raw sio.loadmat(1_20131027.mat) eeg_data raw[data] # shape: (62, 总时间点) labels raw[label] # shape: (1, 片段数) # 切片SEED 通常按 1 秒一段切200Hz 下每段 200 个点 segment_len 200 n_segments eeg_data.shape[1] // segment_len segments [] for i in range(n_segments): seg eeg_data[:, i*segment_len:(i1)*segment_len] # (62, 200) segments.append(seg) segments np.array(segments) # (n_segments, 62, 200) # 标准化按通道做 z-score用训练集统计量避免数据泄漏 scaler StandardScaler() # 注意这里要 reshape 成 (样本数*时间点, 通道数) 再 fit n_seg, n_ch, n_time segments.shape flat segments.transpose(0, 2, 1).reshape(-1, n_ch) scaler.fit(flat) flat_norm scaler.transform(flat) segments_norm flat_norm.reshape(n_seg, n_time, n_ch).transpose(0, 2, 1)这段代码的逻辑说明先加载 .mat 文件拿到原始多通道数据和标签然后按固定窗口切片SEED 常用 1 秒窗接着做 z-score 标准化关键点是 fit 只能在训练集上做否则测试集信息会泄漏到训练过程这是很多人 acc 虚高的原因。参数方面segment_len 取决于采样率和论文设定DEAP 是 128Hz常用 1 秒或 2 秒窗标准化按通道独立做因为不同电极的幅值范围差异很大。2.3 模型结构CNN 提空间特征RNN 串时间依赖资源里的模型定义我按常见实现还原一下核心结构。CNN 部分通常用一维卷积在时间轴上滑或者二维卷积把通道×时间当图来卷。RNN 部分用 LSTM 或 GRU接在 CNN 输出的特征序列后面。import torch import torch.nn as nn class CNN_RNN(nn.Module): def __init__(self, n_channels62, n_time200, n_classes3, hidden_size64): super().__init__() # CNN在时间维度上做一维卷积输入 (batch, n_channels, n_time) self.conv1 nn.Conv1d(n_channels, 32, kernel_size5, padding2) self.bn1 nn.BatchNorm1d(32) self.conv2 nn.Conv1d(32, 64, kernel_size5, padding2) self.bn2 nn.BatchNorm1d(64) self.pool nn.MaxPool1d(2) self.relu nn.ReLU() # RNN输入维度 64时间步为 n_time//4经过两次池化 self.lstm nn.LSTM(input_size64, hidden_sizehidden_size, num_layers2, batch_firstTrue, bidirectionalTrue) self.fc nn.Linear(hidden_size*2, n_classes) def forward(self, x): # x: (batch, n_channels, n_time) x self.relu(self.bn1(self.conv1(x))) x self.pool(x) x self.relu(self.bn2(self.conv2(x))) x self.pool(x) # (batch, 64, n_time//4) x x.permute(0, 2, 1) # (batch, n_time//4, 64) out, _ self.lstm(x) # (batch, n_time//4, hidden*2) out out[:, -1, :] # 取最后时间步 return self.fc(out)逻辑说明卷积层在时间轴上滑动kernel_size5 对应约 25ms 的窗口200Hz 下两层卷积后通道数从 62 升到 64池化两次把时间维度压缩到原来的四分之一。RNN 接在压缩后的特征序列上双向 LSTM 同时看前后文最后取最后一个时间步的输出做分类。参数方面hidden_size 和 num_layers 是可调的SEED 三分类任务上 hidden_size64、两层 LSTM 通常够用DEAP 是二分类valence/arousaln_classes 改成 2。注意 permute 那一步Conv1d 的输出是 (batch, channel, time)LSTM 要的是 (batch, time, feature)顺序别搞反。3. 三个数据集怎么切、怎么训、怎么评3.1 SEED / SEED-IV / DEAP 的加载差异三个数据集的结构和标签体系不一样资源里应该分别处理。SEED 是 15 个被试、3 类情绪积极/中性/消极每个被试 3 个 sessionSEED-IV 是 4 类开心/悲伤/恐惧/中性72 个片段每 sessionDEAP 是 32 个被试、二分类valence 和 arousal 各高/低用 .dat 存pickle 加载。import pickle # DEAP 加载示例 with open(s01.dat, rb) as f: subject pickle.load(f, encodinglatin1) # subject[data] shape: (40, 40, 8064) - 40 试次, 40 通道, 8064 时间点 # subject[labels] shape: (40, 4) - valence, arousal, dominance, liking data subject[data][:, :32, :] # 常用前 32 个 EEG 通道 labels subject[labels][:, :2] # 取 valence 和 arousal # 二值化以 5 为阈值DEAP 常用做法 labels_bin (labels 5).astype(int)参数说明DEAP 的采样率是 128Hz8064 个点对应 63 秒前 3 秒是基线通常要切掉。SEED 的 .mat 文件里 label 是 1/0/-1 对应三类需要映射成 0/1/2。SEED-IV 的 label 是 0/1/2/3。这些映射关系在资源代码里应该有如果没有自己写个 dict 转一下。3.2 训练循环与关键超参训练部分资源里大概率用的是 PyTorch 标准流程。我按常见配置写一版重点说参数怎么定。from torch.utils.data import DataLoader, TensorDataset import torch.optim as optim # 假设 X_train: (N, 62, 200), y_train: (N,) train_ds TensorDataset(torch.FloatTensor(X_train), torch.LongTensor(y_train)) train_loader DataLoader(train_ds, batch_size32, shuffleTrue) model CNN_RNN(n_channels62, n_time200, n_classes3) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) for epoch in range(50): model.train() total_loss 0 for xb, yb in train_loader: optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() optimizer.step() total_loss loss.item() print(fEpoch {epoch}, Loss: {total_loss/len(train_loader):.4f})逻辑说明batch_size32 是脑电任务里比较稳的值太大容易过拟合太小训练不稳定。lr1e-3 配 Adam 是常规起点如果 loss 震荡就降到 5e-4。weight_decay1e-4 做 L2 正则脑电数据量不大正则很有必要。epoch 数看收敛情况SEED 上通常 30-50 轮就够DEAP 因为样本更少要配合早停。3.3 评估被试独立还是被试内这是脑电情绪识别里最容易翻车的地方。被试内within-subject评估是把同一个人的数据切分训练和测试acc 通常很高SEED 上 90%被试独立cross-subject是留出几个被试完全不参与训练acc 会掉到 60-70%。资源里如果没写明用的是哪种你复现出来的数可能和论文对不上。# 被试独立划分示例留一被试法 all_subjects list(range(1, 16)) # SEED 15 个被试 for test_subj in all_subjects: train_subjs [s for s in all_subjects if s ! test_subj] # 用 train_subjs 的数据训练test_subj 的数据测试 # 注意标准化统计量只能用训练被试的参数说明留一被试法LOSO是最严格的评估协议SEED 论文里常见的是被试内 10 折交叉验证两者差距很大。你如果要做跨被试标准化那步必须用训练集的均值和方差去 transform 测试集不能各自标准化。4. 避坑与排查那些让 acc 掉 20 个点的细节4.1 数据泄漏标准化和切片顺序搞反现象训练集 acc 95%测试集 acc 也有 90%但换一组被试直接掉到 50%。原因标准化时用了全量数据的统计量或者切片时训练集和测试集有重叠片段。解决标准化只在训练集上 fit测试集用同样的 scaler transform切片时按试次或按 session 划分别在时间上随机切。4.2 标签对齐SEED 的 label 长度和 data 对不上现象加载 SEED 的 .mat 后data 有 62×T 个点label 只有几十个值直接按索引取会越界。原因SEED 的 label 是按片段给的每个片段对应一段时间的信号不是逐点标注。解决先算清楚每个片段多少点200Hz 下 1 秒是 200 点然后按片段长度切片label 的索引对应片段索引不是时间点索引。4.3 RNN 输入维度搞反Conv1d 输出没 permute现象模型定义看着没问题一跑就报维度错误或者 LSTM 收到的输入 shape 是 (batch, feature, time) 而不是 (batch, time, feature)。原因PyTorch 的 Conv1d 输出是 (batch, channel, time)LSTM 默认要 (batch, time, feature)中间少了一步 permute。解决在 CNN 输出后加 x x.permute(0, 2, 1)把时间维换到中间。如果用了 batch_firstTrue这一步不能省。4.4 过拟合训练 loss 一直降验证 loss 先降后升现象训练 20 轮后训练 acc 接近 100%验证 acc 卡在 60% 不动甚至下降。原因脑电数据样本量小SEED 一个被试几百个片段模型参数量大很容易记住训练集。解决加 dropoutLSTM 里加 dropout0.3全连接前加 nn.Dropout(0.5)加 weight_decay减模型层数或 hidden_size用早停验证 loss 连续 5 轮不降就停。4.5 随机种子没固定每次跑结果都不一样现象同一份代码跑三次得到三个不同的 acc差距能有 5 个点。原因PyTorch 的权重初始化、DataLoader 的 shuffle、dropout 都有随机性。解决在代码开头固定种子。import torch import numpy as np import random def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False set_seed(42)注意cudnn.deterministicTrue 会稍微降低训练速度但结果可复现。如果资源里没固定种子你复现不出论文的数很正常。5. 进阶技巧把跨被试 acc 从 60% 往上抬的几个手段跨被试是脑电情绪识别的真正难点也是论文里最能拉开差距的地方。资源里的基础模型跑被试内没问题但你要做跨被试得额外加东西。我一般会从这几个方向入手。第一个是域适应domain adaptation。不同被试的脑电分布差异很大直接拿 A 的数据训、B 的数据测模型学到的特征偏向 A。常见做法是在训练时加一个域判别器让 CNN 提取的特征在不同被试之间尽量不可分类似 DANN 的思路。实现上就是在 CNN 输出后接一个梯度反转层再接一个被试分类器训练时让特征提取器骗过判别器。第二个是数据增强。脑电信号可以做的增强包括加高斯噪声模拟电极接触差异、时间偏移随机裁剪不同起点、通道 dropout随机遮掉几个电极模拟坏导。这些在训练时在线做能显著提升跨被试的泛化。def augment_eeg(x, noise_std0.1, max_shift20, channel_drop0.1): # x: (n_channels, n_time) # 加噪声 x x np.random.normal(0, noise_std, x.shape) # 时间偏移 shift np.random.randint(-max_shift, max_shift) x np.roll(x, shift, axis1) # 通道 dropout n_ch x.shape[0] drop_ch np.random.choice(n_ch, int(n_ch*channel_drop), replaceFalse) x[drop_ch, :] 0 return x参数说明noise_std 别超过 0.2否则信号被淹没max_shift 控制在片段长度的 10% 以内channel_drop 比例 0.1-0.2 比较安全太高会丢太多空间信息。第三个是特征融合。CNN 提的空间特征和手工特征比如 DE 微分熵、PSD 功率谱密度拼在一起再送进 RNN。SEED 数据集本身提供了 DE 特征你可以把原始信号过 CNN 得到的特征和 DE 特征做 concat往往比单用原始信号好。验证方法上我习惯跑三组对照只用 CNN、只用 RNN、CNNRNN看混合模型到底有没有增益。如果混合模型比单 CNN 还差大概率是 RNN 部分过拟合了把 LSTM 层数降到 1 层或者换成 GRU 试试。另外跨被试评估一定要用 LOSO别用随机划分随机划分出来的数没有参考价值。从那以后我每次拿到新的脑电数据集都强制先跑一遍数据泄漏检查确认标准化统计量只来自训练集、确认切片没有跨集重叠、确认标签和信号按片段对齐。这三步走完再谈模型不然 acc 再高都是假的。希望帮到你。本文还有配套的精品资源点击获取