☰
MATLAB EEG与RNN实战:从数据预处理到LSTM分类的完整代码解析
2026/10/21 12:44:33 网站建设 项目流程

简介:本资源是论文《人类脑电图和递归神经网络在语音识别过程中表现出共同的时间动态》的配套MATLAB分析代码,面向认知神经科学、语音识别与类脑计算方向的研究生及科研人员,用于复现EEG信号与RNN模型输出之间的时间动态对比实验。压缩包共51个文件,以50个.m脚本和1个README.md说明文档为主,整体约77KB,涵盖频域表征、刺激包络与网络输出互相关、表征相似性分析(RSA)、RDM构建与统计检验等模块,脚本按描述中的顺序依次运行即可完成完整分析流程,使用时需将各脚本内路径改为本地路径。内容预览显示代码包含EEG与网络RDM比较、窄带EEG表征分析、随机网络与训练网络输出对比、PCA降维及绘图辅助函数等,结构完整、模块划分清晰。目前已有162人学习下载,适合希望深入理解脑电与神经网络时间动态对应关系、并需要可运行参考实现的研究者。

1. 从一份 EEG+RNN 的 MATLAB 代码包说起:它到底能跑出什么

如果你手头有一批脑电数据,想用循环神经网络做分类或预测,又不想从零搭预处理和训练框架,那这份名为 EEGAndRNNAnalysis 的 MATLAB 代码包值得先看一眼。它把脑电信号处理和 RNN 建模串成了一条可执行的链路:从原始 EEG 导入、滤波去噪、分段提取特征,到构建序列样本、训练循环网络、输出分类结果,基本覆盖了「数据进、结果出」的完整流程。适合两类人:一是做脑机接口、认知状态识别、睡眠分期等方向的研究生和工程师,二是想快速验证 RNN 在时序生理信号上效果、不想被工程细节拖住的人。系统开源的好处是你能看到每一步的参数和中间变量,而不是面对一个黑匣子。下面按「它怎么组织、怎么跑起来、哪里容易翻车、怎么改」的顺序拆开讲。

2. 代码结构与数据流:先看清模块边界再动手

2.1 目录组织与各脚本职责

拿到一个 MATLAB 代码包,第一件事不是急着 run,而是把目录结构和调用关系理清楚。这类 EEG+RNN 项目通常按「数据层—处理层—模型层—评估层」划分。常见做法是:一个主入口脚本负责串联流程,若干函数文件各管一段,数据单独放一个文件夹。你打开后大概率会看到类似这样的布局:

EEGAndRNNAnalysis/ ├── main.m % 主入口,按顺序调用各模块 ├── data/ % 存放 .mat 或 .set 格式的 EEG 数据 ├── preprocessing/ │ ├── loadEEG.m % 读取原始数据,统一成矩阵格式 │ ├── bandpassFilter.m % 带通滤波,去掉工频和漂移 │ └── epochSegmentation.m % 按事件标记切分时间段 ├── features/ │ └── extractFeatures.m % 提取功率谱、熵等特征 ├── model/ │ ├── buildRNN.m % 定义网络层数和结构 │ └── trainRNN.m % 训练循环与超参设置 └── evaluation/ └── evaluateModel.m % 混淆矩阵、准确率、召回率

这个结构的好处是每层可以单独调试。比如你怀疑滤波参数不对,只需要改 bandpassFilter.m 里的截止频率,不用动模型代码。我一般会先跑 main.m 看整体能不能通,再逐个函数检查输出维度是否对得上。

2.2 从原始 EEG 到 RNN 输入张量的关键转换

RNN 吃的是序列数据,形状通常是「时间步 × 特征维 × 样本数」。而原始 EEG 是「通道 × 采样点」的矩阵,中间要经过好几步转换。最容易出错的地方是维度顺序和分段边界。下面这段代码展示了从加载到构造序列样本的典型流程:

% 1. 加载原始 EEG 数据(假设为 channels x timepoints) load('data/subject01.mat'); % 变量名假设为 eegData fs = 250; % 采样率,必须和采集时一致 % 2. 带通滤波 0.5-45 Hz,去掉基线漂移和高频噪声 [b, a] = butter(4, [0.5 45]/(fs/2), 'bandpass'); filtered = filtfilt(b, a, double(eegData)')'; % 转置后滤波再转回 % 3. 按事件标记切分,假设事件发生在 samplePoints 指定的位置 window = 2 * fs; % 每段 2 秒 epochs = zeros(size(filtered,1), window, length(samplePoints)); for i = 1:length(samplePoints) idx = samplePoints(i) : samplePoints(i) + window - 1; epochs(:,:,i) = filtered(:, idx); end % 4. 提取特征或直接降采样作为 RNN 输入 % 这里用简单的降采样:每 10 个点取一个,减少时间步 downsampled = epochs(:, 1:10:end, :); % 最终输入 RNN 的形状:channels x timeSteps x trials

逻辑说明:filtfilt 做零相位滤波,避免相位偏移影响后续分段;window 控制每段时长,太短则频率分辨率不够,太长则样本数减少。参数方面,fs 必须和采集设备一致,否则滤波截止频率会算错;降采样倍数决定了 RNN 的时间步长度,一般控制在 50 到 200 步之间比较稳。如果数据里有明显眼电或肌电伪迹,还需要在滤波后加一步 ICA 或阈值剔除,这部分代码包里可能没带,得自己补。

2.3 网络构建与训练循环的入口在哪

模型部分通常是一个函数,返回一个 layerGraph 或 dlnetwork 对象。MATLAB 的深度学习工具箱支持直接定义 LSTM 或 GRU 层。常见写法是:

function layers = buildRNN(inputSize, numHidden, numClasses) layers = [ sequenceInputLayer(inputSize, 'Name', 'input') lstmLayer(numHidden, 'OutputMode', 'last', 'Name', 'lstm') dropoutLayer(0.3, 'Name', 'drop') fullyConnectedLayer(numClasses, 'Name', 'fc') softmaxLayer('Name', 'softmax') classificationLayer('Name', 'output') ]; end

inputSize 对应每个时间步的特征数(比如通道数或降采样后的通道数),numHidden 是 LSTM 隐藏单元数,一般从 64 或 128 起步。OutputMode 设为 last 表示只取最后一个时间步的输出做分类,如果是做序列标注则改成 sequence。训练入口一般在 main.m 里用 trainNetwork 或自定义训练循环调用。注意 MATLAB 版本差异:2021a 之后推荐用 dlnetwork + trainnet,老版本用 trainNetwork。代码包里如果用的是旧接口,升级 MATLAB 后可能要改几行。

3. 跑通第一遍:环境、数据与参数的最小闭环

3.1 MATLAB 版本与工具箱依赖检查

在命令行敲 ver 看已安装的工具箱。这份代码至少需要 Deep Learning Toolbox 和 Signal Processing Toolbox。如果预处理里用了 EEGLAB 的函数(比如 pop_loadset),还需要额外安装 EEGLAB 并加到路径。常见做法是先把 EEGLAB 的路径 addpath 进去,再跑主脚本。版本方面,LSTM 层在 2017b 之后就有,但 trainNetwork 的某些参数名在 2020a 前后有变动。如果你用的是 2019 之前的版本,sequenceInputLayer 的 'Name' 参数可能不支持,得删掉。我一般会在脚本开头加一段版本判断:

if verLessThan('matlab', '9.8') % 9.8 对应 2020a error('建议使用 2020a 及以上版本,否则部分层参数不兼容'); end

3.2 数据准备:格式、采样率与标签对齐

代码包通常自带一份示例数据或说明数据来源。如果没有,你需要自己准备 .mat 文件,里面至少包含两个变量:EEG 数据矩阵和标签向量。数据矩阵的维度要和代码里的假设一致——常见是「通道 × 采样点 × 试次」或「通道 × 采样点」。标签向量长度必须等于试次数,且顺序一一对应。采样率不一致是高频翻车点:比如数据是 512 Hz 采集的,代码里默认 250 Hz,滤波截止频率就会偏移,分段长度也会错。解决办法是在加载后统一重采样:

targetFs = 250; if fs ~= targetFs filtered = resample(filtered', targetFs, fs)'; fs = targetFs; end

标签对齐方面,如果事件标记存在单独的文件里,要确保切分时用的 samplePoints 和标签顺序一致。我习惯在切分后立刻画一张各类别的平均波形图,肉眼确认没有错位。

3.3 训练参数怎么设:隐藏单元、学习率与批大小

第一次跑不要追求高准确率,先让 loss 降下去。隐藏单元从 64 开始,学习率用 0.001(Adam 默认),批大小设为 16 或 32。如果显存不够就减小批大小,但太小会导致梯度噪声大。训练轮数先设 30,看验证集 loss 是否还在降。下面是一个典型的训练配置:

options = trainingOptions('adam', ... 'MaxEpochs', 30, ... 'MiniBatchSize', 16, ... 'InitialLearnRate', 0.001, ... 'LearnRateSchedule', 'piecewise', ... 'LearnRateDropPeriod', 10, ... 'LearnRateDropFactor', 0.5, ... 'ValidationData', {XVal, YVal}, ... 'ValidationFrequency', 10, ... 'Shuffle', 'every-epoch', ... 'Verbose', true, ... 'Plots', 'training-progress');

参数说明:LearnRateDropPeriod 设为 10 表示每 10 轮学习率减半,适合前期快速下降后期微调。ValidationFrequency 控制验证频率,太高会拖慢训练。Shuffle 设为 every-epoch 能打乱样本顺序,防止模型记住顺序。如果验证集 loss 开始上升而训练集还在降,就是过拟合了,加 dropout 或减隐藏单元。

4. 避坑与排查:那些让结果不可信的细节

4.1 滤波后信号失真:现象、原因与解决

现象:滤波后波形出现明显振铃或幅度异常。原因:filtfilt 的滤波器阶数太高,或者截止频率太接近奈奎斯特频率。解决:降低阶数到 2 或 4,确保截止频率不超过 fs/2 的 0.9 倍。如果数据有工频干扰,单独加一个 50 Hz 陷波,不要用宽带滤波硬压。

4.2 标签泄漏:现象、原因与解决

现象:训练集准确率 99%,测试集只有 50%。原因:分段时同一试次的数据同时出现在训练和测试集,或者归一化参数用了全量数据计算。解决:按试次划分训练测试集,归一化参数只在训练集上算,再应用到测试集。代码包里如果没做划分,自己加一段 split 逻辑。

4.3 序列长度不一致导致训练报错

现象:trainNetwork 报「序列长度不一致」或「维度不匹配」。原因:不同试次的分段长度不同,或者降采样后时间步数不统一。解决:统一分段长度,或者在 sequenceInputLayer 后加 padding 层。MATLAB 支持 'PaddingValue' 参数,但更简单的做法是切分时就固定窗口长度。

4.4 过拟合:验证集 loss 反弹

现象:训练 loss 持续降,验证 loss 先降后升。原因:模型参数太多、数据量太少、dropout 不够。解决:减隐藏单元到 32,加 dropout 到 0.5,或者加 L2 正则化。如果数据实在少,用数据增强(加噪声、时间平移)扩充样本。

4.5 随机种子未固定导致结果不可复现

现象:每次跑出来的准确率差好几个百分点。原因:MATLAB 默认随机初始化网络和打乱数据。解决:在脚本开头设 rng(42),并在 trainingOptions 里设 'Shuffle', 'once' 或固定种子。这样别人复现你的结果时才有可比性。

5. 进阶:把 RNN 换成 LSTM 堆叠与注意力机制的实操

5.1 从单层 LSTM 到双向堆叠的改动点

单层 LSTM 对长序列的记忆能力有限。常见升级是换成双向 LSTM 并堆叠两层。改动很小,在 buildRNN 里替换层定义即可:

layers = [ sequenceInputLayer(inputSize) bilstmLayer(128, 'OutputMode', 'sequence') dropoutLayer(0.3) bilstmLayer(64, 'OutputMode', 'last') dropoutLayer(0.3) fullyConnectedLayer(numClasses) softmaxLayer classificationLayer ];

注意第一层 bilstm 的 OutputMode 要设为 sequence,否则第二层拿不到序列输入。双向 LSTM 会正反各跑一遍,参数量翻倍,训练时间也翻倍。如果数据量不大,建议只堆叠一层双向,后面接全连接。

5.2 加入注意力层提升关键时间段权重

脑电信号里不是所有时间段都同等重要。加一个注意力层可以让模型自己学权重。MATLAB 没有内置的注意力层,需要自定义。常见做法是用一个全连接层输出权重,再和 LSTM 输出做加权求和。下面是一个简化实现:

function [output, weights] = attentionLayer(input) % input: timeSteps x features x batch score = fullyConnectedLayer(1)(input); % 每个时间步一个分数 weights = softmax(score, 'DataFormat', 'TCB'); output = sum(input .* weights, 1); % 加权求和 end

这段代码需要嵌入自定义训练循环,不能直接用 trainNetwork。如果你不想改太多,可以先用全局平均池化代替注意力,效果通常也比只取最后一步好。

5.3 验证方法:混淆矩阵与交叉验证

跑完训练别只看准确率。画混淆矩阵看各类别是否均衡,如果某一类召回率特别低,说明样本不均衡或特征区分度不够。交叉验证用 5 折,按试次划分,每折都重新初始化网络。下面这段代码输出混淆矩阵和每类指标:

[YPred, scores] = classify(net, XTest); cm = confusionmat(YTest, YPred); accuracy = sum(diag(cm)) / sum(cm(:)); precision = diag(cm) ./ sum(cm, 2); recall = diag(cm) ./ sum(cm, 1)'; f1 = 2 * precision .* recall ./ (precision + recall); disp(table(precision, recall, f1, 'RowNames', categories(YTest)));

如果某类 F1 低于 0.5,先检查这类样本数是不是太少,再检查预处理有没有引入伪迹。我一般会在交叉验证前把每个试次的信噪比算一遍,剔除信噪比过低的试次,往往比调模型更有效。

从那以后我每次拿到新的 EEG 数据,都强制先跑一遍可视化检查:原始波形、滤波后波形、各类别平均波形、标签分布,四张图看完再动模型。这一步花十分钟,能省掉后面几小时的玄学调参。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询