尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于DEAP脑电数据的LSTM与RNN情绪识别改进实践

基于DEAP脑电数据的LSTM与RNN情绪识别改进实践 简介面向脑电情绪识别研究者与深度学习开发者这份资料提供了基于DEAP数据集的情绪识别改进实现。作者在Acharya D.等人原LSTM RNN模型基础上调整网络结构与训练策略使觉醒度准确率提升至约92.17%、效价度约94.46%显著优于原论文的88.6%和88.72%。包内共65个文件包含20个Python源码、14篇PDF论文或文献、12个h5训练好的模型权重、8个csv数据记录等压缩包约94.43MB。目录按数据预处理、特征提取、模型构建与训练、结果保存等模块清晰划分涵盖FFT频谱预处理、MRMR与PCA特征筛选以及PSO、GWO、CS等优化算法用于通道/特征选择便于对照论文逐步复现实验。训练好的模型与历史记录也已一并保存可直接加载验证效果或调整参数在LSTM、RNN等模型间切换对比。已有1509人学习该资源适合需要参考改进思路、复现对比实验或在此基础上做进一步优化的中高级学习者。1. 在 DEAP 上把 LSTM 和 RNN 的论文代码改到能写进论文在 DEAP 上把论文源码跑通准确率往往停在 65% 上下。对情绪识别方向来说这个数字够交课程报告却撑不起一篇能写进简历的论文“改进”成了一件刚需的事把 LSTM 和 RNN 的基础代码调成能稳定收敛、指标能写入论文的版本。基于 DEAP 的脑电情绪识别论文源码改进版本题材很常见做法却不只是把 epoch 调大。数据预处理是不是彻底、LSTM 和 RNN 结构该怎么选、改进的尝试是否有统计意义这三件事决定了复现代码最后能到多高的准确率。下面按复现论文时的顺序走一遍数据解析、滑窗切分、模型结构、训练技巧、验证方式全部给出可复现代码适合正在改论文代码的人直接对照着调。2. DEAP 脑电数据预处理与加载滑窗切分、去基线与 Dataset 封装2.1 先看数据长什么样data_preprocessed_matlab 目录与 .mat 文件解析搜索 DEAP 数据集下载时找到的资源大多是data_preprocessed_matlab这个目录里面按受试者编号存放s01.mat到s32.mat共 32 名受试者每名受试者看过 40 段一分钟音乐视频。用scipy.io的loadmat解析单个文件里面的data和labels两个变量就能把全部实验数据取出来。import scipy.io as sio import numpy as np mat sio.loadmat(data_preprocessed_matlab/s01.mat) data mat[data] # (40, 40, 8064) float64 labels mat[labels] # (40, 4) float64 print(data.shape, labels.shape) print(labels[0]) # 第一个 trial 的四个评分data的 8064 这个维度要拆开看DEAP 的采样率是 128Hz60 秒视频对应 7680 个采样点另外每段视频前还有 3 秒静息基线即 384 个点加起来正好 8064。40 个通道里前 32 是脑电对应 10-20 国际导联系统后 8 是眼电、肌电、皮电、呼吸等外周生理信号。labels的四个评分依次是 valence效价、arousal唤醒度、dominance支配度、liking喜好都是受试者看完视频后自己打的 1-9 分。所以一个样本的本质是一段视频、40 条通道、8064 个采样点、4 个主观评分。2.2 去基线与通道选择用代码把原始信号切干净8064 个点里包含前 384 点静息基线论文实验的标准做法是把每个通道前 384 点的均值作为该通道的基线从整段信号里减掉。不做这一步模型会把基线漂移当成情绪特征做跨受试者测试时通常掉 3 到 5 个百分点的准确率。def remove_baseline(trial_data, baseline_points384): # trial_data: (40, 8064)先对每个通道取前384点的均值 baseline_mean trial_data[:, :baseline_points].mean(axis1, keepdimsTrue) return trial_data - baseline_mean data_clean np.stack([remove_baseline(t) for t in data], axis0)baseline_points384对应 3 秒基线。想验证去基线到底有没有用可以把它设成 0 再跑一组对比实验通常在 DEAP 上差 2 到 4 个百分点。通道选择上前 32 个通道索引 0 到 31是脑电后 8 个通道是外周生理信号。先用 32 通道跑 LSTM把后 8 个通道留作消融实验的变量这样能单独评估外周信号对情绪识别的贡献。2.3 滑动窗口切片LSTM 训练样本太少时最常用的增强方法一名受试者只有 40 段视频每段视频只有一条 7680 点的长序列。直接拿整段序列进 LSTM一个受试者只有 40 个样本32 个人全部加起来也只有 1280 个样本对两层 LSTM 来说严重不足。常见做法是做滑窗把去基线后的信号按固定窗口长度和步长切成重叠的小段。窗口长度决定 LSTM 在一个时间步内看到的上下文步长决定样本量和重叠度。def make_windows(trial_data, window_len256, step_len128): # trial_data: (channels, time_points)输出 (n_windows, time_step, channels) n_steps (trial_data.shape[1] - window_len) // step_len 1 windows [] for i in range(n_steps): w trial_data[:, i * step_len:i * step_len window_len] windows.append(w.T) # 转置成 (time_step, channels) return np.array(windows) win_data make_windows(data_clean[0]) # (59, 256, 32)这里的window_len256在 128Hz 采样率下正好是 2 秒step_len128是 1 秒步长重叠率 50%。每个 trial 产出 59 个窗口单受试者 40 个 trial 合计 2360 个样本32 人拼起来就是 75520 个样本训练数据量完全够两层 LSTM 用了。窗口长度步长重叠率每段视频窗口数适用场景1286450%119数据量紧张快速验证管道25612850%59平衡时间上下文与训练速度51225650%29更长的情绪变化模式需更多 epoch标签处理用阈值二分类DEAP 的评分是 1-9按 5 分划界大于 5 记为正类小于等于 5 记为负类。这样每个窗口都继承它所在视频的 valence/arousal 类别分类目标变成二分类。def binarize_label(score, threshold5.0): return 1 if score threshold else 0滑窗产生的类别分布基本平衡因为 DEAP 在实验设计时就尽量覆盖了高低效价和唤醒度的组合不需要额外做重采样。2.4 PyTorch Dataset 与 DataLoader喂给模型前的最后一步所有受试者的窗口拼到一起后要包成 PyTorch 的 Dataset 才能交给 DataLoader 做批量化训练。这里有个容易被新手忽略的点窗口必须打乱。同一段视频里相邻窗口高度相似如果不打乱每个 batch 内的样本几乎一样模型会在小范围内过拟合验证集指标来回跳动。import torch from torch.utils.data import Dataset, DataLoader class DEAPWindowDataset(Dataset): def __init__(self, windows, labels): self.windows torch.FloatTensor(windows) # (N, time_step, channels) self.labels torch.LongTensor(labels) def __len__(self): return len(self.labels) def __getitem__(self, idx): return self.windows[idx], self.labels[idx] dataset DEAPWindowDataset(all_windows, all_labels) loader DataLoader(dataset, batch_size64, shuffleTrue, drop_lastTrue)drop_lastTrue是为了避免最后一个 batch 样本数不满时BatchNorm 和后续统计量的抖动。DEAP 这种固定大小的数据集每个 epoch 的样本数不是总样本数整除 batch_size不丢尾部会造成某些 epoch 多一个 batch训练指标曲线出现周期性的毛刺。3. DEAP 脑电信号里的 LSTM 与 RNN 结构选择门控机制与梯度问题3.1 为什么序列模型比全连接更合适脑电信号脑电信号本质上是时间序列。DEAP 里一个 256 点的窗口前额叶的 theta 波、颞叶的 alpha 波都有明确的时间先后关系。全连接模型把 256×32 的矩阵展平成向量丢掉的恰好是时间步之间的依赖关系。RNN 的结构和原理不复杂隐藏状态 h_t tanh(W_ih · x_t W_hh · h_{t-1} b)循环连接让信息沿时间轴传递。每一时间步读取当前通道向量输出传递给下一步参数跨时间步共享。这种结构天然对顺序敏感而顺序信息在脑电情绪识别里就是各个频段成分随情绪状态演化的过程。但纯 RNN 在 DEAP 这种长度上并不好用。反向传播时梯度要沿时间轴连乘多个 W_hh如果 W_hh 的最大奇异值小于 1梯度会指数级衰减256 步的长序列上前面时间步几乎收不到有效梯度。这就是为什么大多数改进版论文直接把 RNN 换成 LSTM而不是只调学习率。LSTM 引入细胞状态 C_t让信息在时间步之间线性传递从结构上缓解了长序列梯度消失的问题。3.2 标准 LSTM 结构遗忘门的输入是什么数据LSTM 里最常被单独讨论的是遗忘门它决定上一时刻的细胞状态哪些保留、哪些丢弃。网上经常搜到“LSTM 遗忘门的输入是什么数据”这类问题答案不是单独某个向量而是上一时刻隐藏状态 h_{t-1} 和当前输入 x_t 的拼接f_t sigmoid(W_f · [h_{t-1}, x_t] b_f)拼接后的向量同时经过线性变换和 sigmoid 激活输出 0 到 1 之间的权重再逐元素乘到上一时刻细胞状态 C_{t-1} 上。权重接近 1 表示信息大部分保留接近 0 表示丢弃。PyTorch 里不需要自己写这套门控逻辑直接调用封装好的nn.LSTMmodel_lstm nn.LSTM( input_size32, hidden_size64, num_layers2, batch_firstTrue, dropout0.4, )input_size32是窗口里每个时间步的脑电通道数只用 EEG 通道时就是 32如果把后 8 个外周信号也加进来就是 40。hidden_size64控制隐向量维度DEAP 改进版代码从 64 开始调最稳妥128 在总样本量不大时反而容易过拟合。num_layers2让高层捕捉更抽象的时间模式但第一层的输出是第二层的输入层间dropout0.4只有在num_layers1时才生效。窗口数据喂进去后前向输出形状要心里有数out, (h_n, c_n) model_lstm(x) # x: (batch, seq_len256, input_size32) # out: (batch, seq_len256, hidden_size64) last_out out[:, -1, :] # (batch, 64)out[:, -1, :]取最后一个时间步的隐藏状态。它和h_n[-1]的值几乎一致但out[:, -1, :]的写法在接入注意力机制时更直观因为注意力需要对整个时间步的输出做加权而不是只取最后一步。3.3 RNN、LSTM、双向 LSTM 在 DEAP 上的对比结构门控数量单层参数量级256 步梯度状态DEAP 上的表现普通 RNN无hidden × (input hidden 1)容易梯度消失收敛慢acc 偏低LSTM3 个门4 × hidden × (input hidden 1)相对稳定常用 baseline双向 LSTM3 个门 × 2约为单向的 2 倍稳定能看上下文训练更慢普通 RNN 在 DEAP 二分类上通常比 LSTM 低 3 到 6 个百分点主要差距来自长序列梯度衰减。双向 LSTM 每个时间步同时看到前后文对“整段视频一个标签”的任务有意义因为情绪状态贯穿整个窗口后面的脑电模式可能解释前面的异常波动。但双向结构在跨受试者评估里要小心如果窗口之间信息重叠过多双向比单向更容易过拟合到受试者特异性特征。改进版的常见套路是保留三类模型做消融对比用普通 RNN 做基线用 LSTM 做主模型用双向 LSTM 做上限参考。这样论文里能写清楚门控机制带来的提升也能回答为什么最终选了 LSTM。4. 改进版模型的可复现优化标签平滑、注意力池化与早停4.1 标签平滑主观评分噪声的兜底策略DEAP 的标签是受试者自己打的 1-9 分同一个人的同一段视频在不同时间打分可能跳到不同的类别。二分类阈值设在 5 时5 和 6 之间的样本本来就模糊。改进版代码里对这类主观噪声最有效的处理是标签平滑不让模型的预测目标变成 0 和 1 两个硬点而是允许一定比例的软标签模型就不会为了强行拟合边界样本而把权重拉得很大。class LabelSmoothLoss(nn.Module): def __init__(self, smoothing0.1): super().__init__() self.smoothing smoothing def forward(self, logits, targets): log_probs torch.log_softmax(logits, dim-1) n_class logits.size(-1) with torch.no_grad(): true_dist torch.empty_like(log_probs) true_dist.fill_(self.smoothing / (n_class - 1)) true_dist.scatter_(1, targets.unsqueeze(1), 1.0 - self.smoothing) return torch.mean(torch.sum(-true_dist * log_probs, dim-1))smoothing0.1时正类目标从 1 变成 0.9负类从 0 变成 0.1/(2-1)0.1。这个技巧对 DEAP 这类主观评分数据效果明显通常比换模型结构更稳定。注意梯度被锁在torch.no_grad()里软标签不参与反向传播。4.2 学习率调度与早停稳住验证集指标的震荡脑电数据噪声大训练曲线波动明显固定学习率要么后期震荡要么停在局部极小值。改进版代码里一般同时上学习率调度和早停让训练过程自己决定什么时候降低步长、什么时候停止。from torch.optim.lr_scheduler import ReduceLROnPlateau scheduler ReduceLROnPlateau( optimizer, modemin, factor0.5, patience5, min_lr1e-6, )modemin表示跟踪验证集损失损失不再下降就触发。patience5是连续 5 个 epoch 没有更低验证损失就把学习率乘以factor0.5。min_lr1e-6防止学习率无限下降。如果改跟踪验证集准确率要把mode改成max逻辑正好相反。class EarlyStopping: def __init__(self, patience8, min_delta1e-4): self.patience patience self.min_delta min_delta self.best_loss float(inf) self.counter 0 def __call__(self, val_loss): if val_loss self.best_loss - self.min_delta: self.best_loss val_loss self.counter 0 return False self.counter 1 return self.counter self.patiencemin_delta1e-4是“进步”的最低门槛验证损失降幅小于这个值不算改善。早停的patience一般比学习率调度的patience大 3 到 5 个 epoch让学习率先降到合适的区间再考虑整体停掉。4.3 用注意力池化替代最后一步输出LSTM 的out[:, -1, :]只用了最后一个时间步的隐藏状态。对 DEAP 的 256 步窗口来说情绪相关的脑电特征可能出现在窗口中部最后一步未必最有代表性。简单注意力机制对 LSTM 每一步的输出算权重再加权平均让模型自己决定侧重哪些时间段。class AttentionPooling(nn.Module): def __init__(self, hidden_size): super().__init__() self.attn nn.Linear(hidden_size, 1) def forward(self, lstm_output): # lstm_output: (batch, seq_len, hidden_size) weights torch.softmax(self.attn(lstm_output), dim-2) return torch.sum(lstm_output * weights, dim1) # (batch, hidden_size)self.attn把每个时间步的隐藏状态映射成一个标量权重dim-2是对时间步维度做 softmax所有时间步的权重和为 1。torch.sum(lstm_output * weights, dim1)把加权后的向量在时间步维度求和输出一个(batch, hidden_size)的特征向量。这个向量再进分类层替代原来的out[:, -1, :]。4.4 完整训练脚本与超参数配置把上面三个技巧整合进一个模型类里用model_type参数同时支持 LSTM 和 RNN方便做对比实验class RNNEmotion(nn.Module): def __init__(self, input_size32, hidden_size64, num_layers2, num_classes2, dropout0.4, model_typelstm): super().__init__() if model_type lstm: self.rnn nn.LSTM(input_size, hidden_size, num_layers, batch_firstTrue, dropoutdropout) else: self.rnn nn.RNN(input_size, hidden_size, num_layers, batch_firstTrue, dropoutdropout) self.attention AttentionPooling(hidden_size) self.classifier nn.Sequential( nn.Dropout(dropout), nn.Linear(hidden_size, 32), nn.ReLU(), nn.Linear(32, num_classes), ) def forward(self, x): out, _ self.rnn(x) feat self.attention(out) return self.classifier(feat)训练循环用标准写法注意梯度裁剪def train_one_epoch(model, loader, optimizer, criterion): model.train() total_loss, correct, total 0.0, 0, 0 for x, y in loader: optimizer.zero_grad() pred model(x) loss criterion(pred, y) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() total_loss loss.item() * len(y) correct (pred.argmax(dim1) y).sum().item() total len(y) return total_loss / total, correct / totalclip_grad_norm_(1.0)限制梯度范数不超过 1LSTM 在长序列上偶尔会出现梯度爆炸裁剪之后训练稳定很多。推荐超参起步值参数取值说明lr1e-3用 AdamWloss 不降再往 1e-4 调batch_size64显存小降 32同步调低 lrhidden_size64样本量小128 以上容易过拟合num_layers2先 1 层验证管道再加第二层dropout0.4过拟合明显时提到 0.5weight_decay1e-4AdamW 权重衰减稳定后期收敛在单张普通显卡上55 个 epoch 大约 15 到 30 分钟取决于窗口数和 batch size 的搭配。5. 验证改进效果混淆矩阵、Kappa 系数与跨受试者评估模型改进的好坏不能只看损失曲线还要看它到底分对了哪些样本。DEAP 的标签是 1 到 9 的连续评分按阈值 5 切成两类后受试者之间的评分习惯差异很大混淆矩阵是最直接的诊断工具。5.1 混淆矩阵看模型错在哪里from sklearn.metrics import confusion_matrix, cohen_kappa_score import seaborn as sns import matplotlib.pyplot as plt cm confusion_matrix(y_true, y_pred) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabels[LV, HV], yticklabels[LV, HV]) plt.savefig(cm.png, dpi150)y_true是测试集真实标签y_pred是模型预测标签。二分类的混淆矩阵能直接看出模型是偏向预测正类还是负类。DEAP 上常见的问题是 valence 的低唤醒样本被模型分到高唤醒类因为低唤醒状态下的脑电模式在能量上更接近静息态。如果对角线外的数量差距很大说明阈值 5 的分类边界和模型学到的特征边界不一致这时可以检查训练集里的类别数量是否均衡。5.2 跨受试者验证subject-dependent 和 subject-independentDEAP 里最容易被高估的验证方式是随机切分窗口相邻窗口来自同一段视频模型会记住受试者的个体特征而不是情绪特征。严格的做法是 subject-independent 评估训练集和测试集来自不同受试者测试时模型完全没见过这个人。用LeaveOneGroupOut实现32 名受试者每人都当一次测试集from sklearn.model_selection import LeaveOneGroupOut logo LeaveOneGroupOut() for train_idx, test_idx in logo.split(all_windows, all_labels, groupssubject_ids): train_loader make_loader(all_windows[train_idx], all_labels[train_idx]) test_loader make_loader(all_windows[test_idx], all_labels[test_idx]) # 训练并在 test_loader 上计算指标 kappa cohen_kappa_score(y_true, y_pred)cohen_kappa_score衡量预测与真实标签的一致性数值大于 0.4 就是中等一致大于 0.6 算强一致。subject-independent 场景下准确率和 Kappa 会明显低于 subject-dependent 场景这是正常的改进版代码里要同时报告两种结果审稿人和答辩老师最关心的就是这个差距。注意力池化在 LSTM 输出维度上做加权平均input_size 和 hidden_size 任何一端改掉都要同步调整 AttentionPooling 的构造参数。本文还有配套的精品资源点击获取
返回列表