资讯动态

EEG脑电信号与RNN分类:MATLAB实现LSTM预处理与训练全流程解析

发布时间:2026/10/17 11:32:06 来源:尧图企业网站定制
简介这份代码包是基于论文《人类脑电图和递归神经网络在语音识别过程中表现出共同的时间动态》公开发布的MATLAB分析实现系统开源主要面向从事脑电信号处理、神经网络可解释性与语音认知研究的科研人员及高年级学生。包内共51个文件几乎全部为.m脚本另附1个README.md运行说明压缩包整体仅77KB。脚本按“频域表示→刺激包络与模型输出交叉相关→刺激包络与EEG交叉相关→表征相似性分析(RSA)”的主线组织覆盖窄带滤波、欧氏距离RDM计算、随机/训练网络对比、头皮拓扑可视化等关键步骤方便读者按顺序执行并复现论文图表依据README中的路径设置提示可快速在本地环境中跑通整个流程。目前已有162人学习/下载适合希望快速上手复现实验、深入理解语音识别过程中神经与模型时间动态对应关系的研究者。1. 一个跑通的脑电信号分类项目EEG 加 RNN 不只是拼接口如果你第一次拿到脑电数据把原始电压值直接扔进 LSTM十分钟后大概率会怀疑人生。我见过 A 同学这么干预处理只做了个去均值模型训练到一半内存先炸了调低 batch size 后准确率又掉到三成。EEG 信号分类真正难的从来不是网络结构而是要处理采样率高、噪声大、试次边界不齐、个体差异明显这一整串麻烦。这个名为 rsamatlab代码-EEGAndRNNAnalysis 的项目解决的就是这件事用 MATLAB 把脑电信号从原始文件一路处理成能喂给循环神经网络的序列数据并在同一个框架里完成训练、评估与结果可视化给“脑电图加神经网络分析”提供了一个可以直接改的系统开源方案。适合谁做脑机接口、认知实验或者生理信号分类的开发者以及那些手上有 EEG 数据、想换掉传统特征加分类器流水线、但又不想从零写预处理和网络训练的人。接下来我会从架构、核心代码、跑通步骤到改造思路把它完整拆开。2. 架构设计与技术选型为什么用 MATLAB 串起信号处理和深度学习2.1 技术栈三个工具箱的分工这个项目构建在 MATLAB 生态上核心依赖是三个工具箱Signal Processing Toolbox 负责滤波、重参考、去伪迹等经典信号处理Statistics Toolbox 处理特征统计、归一化和交叉验证分组Deep Learning Toolbox 提供 LSTM 层、训练选项和评估指标。三者通过脚本逐级串联形成一个从数据到分类结果的完整流水线。选择 MATLAB 做这个项目在我看是合理的。EEG 分析里大量操作是滤波、时频分解、独立成分分析这类信号处理MATLAB 在这块的函数库比通用编程语言成熟很多尤其在处理多通道连续信号时的内存管理和矩阵操作非常高效。另一方面Deep Learning Toolbox 让 LSTM 的搭建缩成几行配置数据格式也能直接对接通道×时间矩阵省掉了中间的数据序列化转换。项目本身是系统开源的结构上没有用 App 打包也不依赖某个具体的采集设备。脚本是典型的研究型代码风格按功能拆成多个 .m 文件用主脚本统一调用没有做成 GUI。也就是说你要用它不需要点界面而是打开脚本、配好路径、改参数、运行。2.2 目录结构与数据流拿到项目后先别急着跑花十分钟把目录看清楚。项目大概是这样的组织方式EEGAndRNNAnalysis/ ├── data/ # 原始EEG数据与试次信息 ├── preprocess/ # 滤波、去伪迹、分段的前处理函数 ├── features/ # RSA分析与特征构造脚本 ├── models/ # LSTM网络定义与训练入口 ├── evaluate/ # 分类报告与混淆矩阵输出 ├── utils/ # 加载数据、画图、指标计算等公共函数 └── run_pipeline.m # 全流程主脚本从数据流的角度看整条链路是连续脑电信号进入 preprocess 模块经过滤波、剔除坏段和重参考切分成固定长度的时间窗然后进入 features 模块在这里做 RSA 分析得到表征相似性矩阵把高维原始信号压缩成试次间相似性结构最后把处理后的序列张量交给 models 模块里的 LSTM 网络做分类。每个模块都有一到两个可独立运行的函数方便你单独调试某一步。有意思的是项目把 RSA表征相似性分析放在了特征提取阶段而不是像普通分类项目那样直接做功率谱或时域统计。这个设计的价值在于RSA 可以把不同试次间的表征距离结构化让 LSTM 看到的输入不只是原始电压而是一种更接近认知状态的表示。3. 核心模块拆解从连续脑电到分类结果的完整链路3.1 预处理模块滤波、分段与坏段剔除EEG 数据的第一步几乎都是滤波。项目里设计了一个统一的预处理函数输入是连续数据和采样率输出是干净的分段时间序列。核心逻辑是先做带通滤波保留有效频段再针对工频干扰做陷波最后按事件标记切分试次并剔除异常能量段。典型代码如下function [epochs, labels] preprocess_eeg(rawData, fs, events, win) % 带通滤波EEG一般保留0.5-40Hz filtData bandpass(rawData, [0.5 40], fs); % 50Hz陷波国内市电干扰 filtData notchfilter(filtData, 50, fs); % 按事件标记切分试次每个试次取[事件前0.2s, 事件后0.8s] trialIdx round(events * fs); for i 1:length(trialIdx) idx trialIdx(i) round([-0.2 0.8] * fs); epochs{i} filtData(:, idx(1):idx(2)); labels(i) get_label_from_event(events(i)); end % 剔除坏段计算每段方差去除方差异常大的试次 valid cellfun((x) var(x(:)) prctile(var(x(:)), 99), epochs); epochs epochs(valid); labels labels(valid); end这段代码有三个关键参数需要开发者自己确认。带通范围 [0.5 40] 是常规做法但如果你是处理高频成分较多的实验可以把上界提到 100Hz反之做慢皮层电位就降到 10Hz 附近。时间窗 [-0.2 0.8] 是另一个高频调整项它决定了每条输入序列的长度直接影响后续 LSTM 的时间步数。如果实验的事件间隔小于 1 秒这个窗口会重叠到相邻试次需要缩短到比如 [-0.1 0.5]。3.2 RSA 特征提取把多维时间序列转成相似性表征预处理之后项目没有直接把电压矩阵扔给网络而是先做了一次 RSA 分析。这条链路是很多 EEG 分类项目里比较少见的。RSA 的核心是计算所有试次对之间的表征距离矩阵然后把这个矩阵作为特征输入。因为 RSA 矩阵的行列对应试次LSTM 可以把它当作“每个试次与其他试次的相似性模式”来学习。代码如下function [rdm, trialFeatures] compute_rsa(epochs, labels) % 每个试次做时域特征提取这里使用平均功率 trialFeatures zeros(length(epochs), 64); for i 1:length(epochs) trialFeatures(i,:) mean(epochs{i}.^2, 2); end % 计算试次间相关距离得到RDM rdm 1 - corr(trialFeatures); % 用标签信息对RDM做归一化类别内/类别间分离 uniqueLabels unique(labels); for l1:length(uniqueLabels) m labels uniqueLabels(l); within mean(rdm(m,m)); rdm(m,m) rdm(m,m) - within; end end我一般会把 trialFeatures 再拼上原始通道均值因为平均功率会丢掉时间动态信息但 LSTM 的强项恰恰是学习时间依赖完全用静态特征会让网络退化成一个普通分类器。如果你改这个项目可以让 compute_rsa 同时输出特征矩阵和 RDM然后并行输入 LSTM 的两个输入分支或拼接成多通道序列。这里要非常注意 labels 的对齐事件时间偏移半个采样点都会导致标签错位训练时看不出来测试时准确率直接崩。3.3 LSTM 模型时间序列输入的构建与训练配置从数据流看LSTM 层的输入尺寸由两个因素决定每个时间步的特征数通道数、RSA 矩阵行数和窗口长度的乘积和时间步长度。在运行入口里数据会先被转换成 MATLAB 深度学习工具箱要求的格式用 num2cell 转化成 cell 数组:% 将数据转为LSTM需要的序列格式时间优先 XTrain cell(numTrials, 1); for i 1:numTrials XTrain{i} features{i}; % 转成 时间步数 × 特征数 end % 定义网络 layers [ sequenceInputLayer(size(XTrain{1}, 2)) bilstmLayer(64, OutputMode, last) dropoutLayer(0.2) fullyConnectedLayer(2) softmaxLayer classificationLayer]; options trainingOptions(adam, ... MaxEpochs, 50, ... MiniBatchSize, 32, ... ValidationData, {XValidation, YValidation}, ... ValidationFrequency, 10, ... Plots, training-progress); net trainNetwork(XTrain, YTrain, layers, options);这里两个配置直接影响最终效果。第一是 OutputMode 设置为 last意味着网络只输出最后一个时间步的隐藏状态用于序列级分类如果你要做的是时间逐点分类或找出关键时间窗要改成 sequence。第二是 dropout 放到了 BiLSTM 之后、全连接之前这是防止小样本 EEG 数据过拟合的主要手段。EEG 试次往往只有几百条50 个 epoch 的训练很容易让 loss 降到接近零但验证准确率停在原地——dropout 和早停是这个项目的防过度自信机制。3.4 评估模块混淆矩阵与单试次准确率训练完成之后项目里单独封装了一个评估脚本统一输出整体准确率、每类准确率、混淆矩阵和 loss 曲线。这部分代码不复杂但看结果时容易被整体准确率骗过去。EEG 实验最常见的就是其中一类试次数远大于另一类整体准确率 75% 但弱势类识别率只有 30%。因此项目里单独动了手把混淆矩阵标准化成按行百分比这样一眼就能看出类间混淆情况。% 混淆矩阵归一化显示 [cm, order] confusionmat(YTest, YPred); cmNorm cm ./ sum(cm, 2); fprintf(整体准确率: %.2f%%\n, 100 * sum(diag(cm)) / sum(cm(:))); disp(array2table(cmNorm, VariableNames, order, RowNames, order));如果你把这段代码跑在自己的数据上我建议加一个 perm-test随机打乱标签 1000 次重跑分类得到准确率的零分布然后看真实准确率落在哪个百分位。这个小步骤能让结果经得起审稿人追问。4. 从零跑通环境、数据准备和主脚本参数4.1 环境要求与数据准备开始之前确认三件事MATLAB 版本至少 R2019b因为 Deep Learning Toolbox 的序列输入层在这个版本之后才稳定支持可变长度序列内存建议 16GB 以上虽然单试次数据不大但训练时如果按默认的 miniBatchSize 跑GPU 预留显存至少要 6GB如果你不开 GPU训练时间会明显更久建议先把 MaxEpochs 调小到 20 试跑通。数据方面项目没有绑定特定采集设备只要你能把数据加载成 通道数×时间点数 的矩阵并提供一个对应的事件时间标记数组就能运行。典型的做法是自己写一个加载函数放到 utils 目录下把 .edf 或 .set 文件读成统一格式。例如function [rawData, fs, events, labels] load_my_data(subjectDir) % 读入EDF文件 [rawData, fs] readedf(fullfile(subjectDir, record.edf)); % 读入事件描述表格时间戳, 类型 evtTable readtable(fullfile(subjectDir, events.csv)); events evtTable.timestamp; labels evtTable.type; end4.2 主脚本参数说明run_pipeline.m 是唯一需要你动手改的主脚本。打开后注意力放在前三十行所有实验参数都集中定义在那里参数名典型值含义fs250采样率决定滤波范围和窗口大小freqRange[0.5 40]带通滤波范围可依据实验频带调整trialWin[-0.2 0.8]试次截取区间以事件为原点numUnits64LSTM隐藏单元数量dropoutRate0.2随机丢弃比例maxEpochs50最大训练轮数miniBatchSize32mini-batch大小dataDirdata/存放原始数据的目录首次运行建议按上表默认值先跑一个被试。整个过程会依次输出预处理日志、RSA 矩阵生成提示、网络训练进度条和一张训练过程图。训练结束后控制台会打印整体准确率和每类召回率同时保存一个 result.mat 文件里面是 net、YPred、YTest 和混淆矩阵方便你后续做统计分析。4.3 运行时常见输出含义训练进度图里有两条曲线一条是交叉熵损失另一条是验证准确率。如果验证准确率曲线随着训练收敛而在附近震荡不用太慌——这是小样本 EEG 数据的常态如果验证准确率持续走低而训练准确率逼近 100%那基本是过拟合这时下调 numUnits 或增大 dropoutRate。项目默认的验证频率是每 10 个 iteration 一次一个 epoch 只有几个 iteration 的话验证曲线会非常稀疏可以改成每 5 个 iteration 一次曲线会更平滑。运行遇到函数未定义报错绝大多数情况是 MATLAB 当前路径没有包含项目子目录。我习惯在脚本开头加两行cd(fileparts(mfilename(fullpath))); addpath(genpath(pwd));这段能把项目下所有子目录一次性加进路径防止最常见的 addpath 遗漏。5. 避坑指南EEG 与 RNN 结合时最容易翻车的五个地方5.1 数据泄漏归一化和交叉验证的顺序搞反了现象训练准确率极高测试准确率跟抛硬币差不多但你没有改任何超参数。原因有人习惯先把所有数据统一归一化再做交叉验证拆分。这会让验证集的信息进入训练过程属于典型的数据泄漏。EEG 数据里不同被试的幅值差异很大一旦全局归一化模型会依赖这种全局统计量来判断类别。解决把归一化写进训练流程内部只在训练集上计算均值和标准差再应用到验证集。你项目里如果用了 mapstd 或 normalize 函数检查它的调用位置是否在划分之后。5.2 序列方向弄反了LSTM 记住了乱七八糟的时序现象模型在验证集上不收敛loss 跳到 NaN。很多人在构造 XTrain 的时候直接用了 特征数×时间步 的矩阵LSTM 期望的是 时间步×特征数 的 cell 序列。原因MATLAB 的 sequenceInputLayer 要求序列维度是时间优先。项目里做了一次转置但如果你在改特征时手动拼接了新的特征数组很可能又把维度带偏了。解决每次构造完 XTrain 后用 size 验证一下维度顺序。排错时可以单独取一个样本打印前五行看是不是时间步从左往右增长。这是花两分钟能省两小时的习惯。5.3 采样率和时间窗口对不齐所有试次长度不一样现象运行到 trainNetwork 时报错说序列长度不一致导致 mini-batch 无法拼接。原因事件标记时间戳如果是从不同设备导出的精度可能不一样导致每个试次截出来的长度有几十个采样点的误差。解决在截取试次之后强制统一长度。常见做法是截取固定采样点数比如 fs 乘以窗口秒数而不是用窗口秒数直接换算。如果数据本身采样率浮动先重采样到统一采样率再做分段。5.4 模型随机性导致结果没法复现现象同一个脚本在两次运行之间准确率能差五个百分点。原因训练过程中权重初始化和 mini-batch 的随机洗牌都会引入随机性。MATLAB 默认不固定随机种子EEG 数据试次数少时这种波动会被放大。解决在脚本开头加一行 rng(42)固定全局随机数生成器。如果在做模型对比实验一定要保证所有模型在同一个随机种子下训练不然你对比的其实是随机噪声。5.5 类别不平衡全部预测成多数类现象整体准确率 80%但看每类召回率少数类只有 8%。原因EEG 实验中有人试次通常比无人试次多很多交叉熵损失函数天然偏向让网络倾向预测大类别。解决项目提供的 loss 默认是均匀加权的你可以改成加权交叉熵方法是在分类层前面乘一个类别权重矩阵。或者换一个更直接的思路在数据层面做少数类过采样把重复若干次的数据加回去然后再发给网络。6. 二次开发从更换特征到重构网络结构的三个改造方向第一个方向是升级特征模块把现有的平均功率换成时频特征。在 compute_rsa 函数里接入小波包变换对每个试次提取 4 到 8 个频段的能量序列然后拼接到原来的 trialFeatures 里。这样改动只需要在循环内加几行调用 cwt 函数的代码LSTM 的输入端不需要调整。注意对每个频段单独做 z-score 归一化否则幅值大的频段会压过其他信息。第二个方向是改造网络结构把 BiLSTM 换成 CNN-LSTM 并联结构。脑电信号里的空间模式通道间关系靠 LSTM 学得比较慢如果先在通道维度上做一层卷积可以让网络更快捕捉到空间特征。修改时把 sequenceInputLayer 后面插入一个 convolution1dLayer卷积核大小为通道数测试下来收敛速度有明显提升。第三个方向是增强评估模块的统计说服力。我通常会在评估脚本里加入一个排列检验随机打乱标签 1000 次并重新计算模型准确率得到零分布然后算出真实准确率的 p 值。这个功能放在 utils 目录里和整体流程解耦不会影响主线代码的运行。回看整个改造习惯我从这个项目里学到最关键的一条改任何模块之前先跑一次完整流程留好基线结果再动手改第一处代码。从那以后我每次改 EEG 处理流程都强制走一遍全流程跑通加基线记录判别改动是否有效全靠这个基准。这篇拆解从预处理、RSA、LSTM 到评估和改造都有了希望帮到你上手。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑