基于Transformer的运动想象脑电信号分类实战:从数据预处理到注意力可视化
发布时间:2026/9/26 18:30:30来源:尧图网络
简介这份本科毕业设计资源聚焦于基于Transformer的运动想象脑电信号分类面向人工智能与生物医学工程交叉方向的本科生及脑机接口入门研究者。项目采用CNNTransformer混合框架由CNN提取局部时空特征、Transformer捕捉全局依赖覆盖EEG数据预处理、特征提取、模型训练与k折交叉验证评估的完整流程并配有可视化与统计分析模块。压缩包共31个文件约18.45MB以23个Python脚本为核心辅以2个MATLAB预处理脚本、2个xlsx实验数据表、1个pth模型权重、1个npy数据文件及xml、md等配置说明结构清晰便于按模块查阅。目前已有275人学习下载。读者可借此获得一套可复现的毕业设计实现方案理解自注意力机制在脑电序列建模中的应用思路并参考其中的训练脚本、混淆矩阵与t-SNE可视化代码快速搭建自己的运动想象分类实验。1. 从一份本科毕设压缩包说起Transformer 怎么做运动想象脑电分类运动想象脑电信号分类说白了就是让人坐在那里“想”左手或右手动作算法从头皮电极里把那点微弱意图读出来。这个方向做了几十年CSPSVM 那套传统方案在 2 分类上能到 70% 上下但换个人、换一天数据就掉得厉害。近几年 Transformer 从 NLP 杀过来大家发现它那套自注意力机制天然适合建模多通道脑电的长程依赖——毕竟 64 个电极不是孤立的C3 和 C4 的节律耦合才是判别关键。这份本科毕设标题里的“基于 Transformer 的运动想象脑电信号分类”核心就是拿 BCI Competition IV 2a 或 2b 这类公开数据集把原始 EEG 时间序列送进 Transformer 编码器输出左右手或手脚的分类概率。适合谁看正在做脑机接口方向毕设的本科生、刚转进 EEG 深度学习的研究生以及想拿一个完整 pipeline 练手 Transformer 时序建模的工程师。下面我按自己复现过的路径把数据、模型、训练、排坑全讲一遍。2. 数据准备与预处理从 GDF 到模型能吃的张量2.1 为什么选 BCI IV 2a 而不是自己采数据本科毕设最怕数据出问题。自己采脑电光阻抗调平衡就能耗掉一周还未必能采到合格 trial。常见做法是直接用 BCI Competition IV 2a 数据集9 名被试每人 2 个 session每个 session 288 个 trial4 类运动想象左手、右手、双脚、舌头。做二分类就取左手和右手共 576 个 trial。采样率 250 Hz25 个通道含 3 个 EOG。这个数据集标注干净、有官方评估标准答辩时没人会质疑数据来源。下载后是.gdf格式需要mne读取。我一般先做 0.5–100 Hz 带通再降采样到 128 Hz然后用 4–38 Hz 带通滤出运动想象相关的 mu 节律8–13 Hz和 beta 节律13–30 Hz。注意别用 50 Hz 陷波代替带通陷波只去工频运动想象的关键频段还在。import mne import numpy as np # 读取单个被试的 gdf 文件 raw mne.io.read_raw_gdf(A01T.gdf, preloadTrue) # 1. 先做宽带滤波去掉基线漂移和肌电 raw.filter(0.5, 100., fir_designfirwin) # 2. 降采样到 128 Hz减少后续 Transformer 序列长度 raw.resample(128, npadauto) # 3. 提取事件BCI IV 2a 的事件编码769左手770右手 events, event_id mne.events_from_annotations(raw) # 只保留左手和右手 selected {k: v for k, v in event_id.items() if k in [769, 770]} # 4. 切 epochtmin-0.5s 到 tmax4.0s运动想象通常取 cue 后 0.5-2.5s epochs mne.Epochs(raw, events, selected, tmin-0.5, tmax4.0, baseline(-0.5, 0), preloadTrue) # 5. 带通到运动想象频段 epochs.filter(4., 38., fir_designfirwin) # 6. 取 cue 后 0.5s 到 2.5s 的数据段 data epochs.get_data()[:, :, 64:320] # 128Hz 下 0.5s64点2.5s320点 labels epochs.events[:, -1] - 769 # 左手0右手1 print(data.shape) # (n_trials, 25, 256)这段代码的关键参数tmin-0.5是为了保留基线做去均值tmax4.0覆盖完整 trial但真正送模型的是64:320这 256 个点。为什么取 0.5–2.5s运动想象相关去同步ERD在 cue 后 0.5s 开始显现2.5s 后开始恢复这个窗口信噪比最高。baseline(-0.5, 0)用 cue 前数据做基线校正消除被试间的绝对幅值差异。2.2 通道选择与归一化别把 EOG 喂给 Transformer25 个通道里有 3 个是眼电EOG运动想象分类用不上反而引入眨眼伪迹。我一般只保留 22 个 EEG 通道或者进一步按 C3、C4、Cz 及其周围通道做子集。但本科毕设建议先用全部 22 通道让 Transformer 自己学注意力权重答辩时还能可视化注意力图说明模型确实关注了感觉运动区。归一化用 trial 级 z-score不是全局归一化。因为不同 trial 的绝对幅值受电极阻抗、被试状态影响很大但同一 trial 内的相对变化才是节律特征。# data shape: (n_trials, 22, 256) # 对每个 trial 的每个通道单独做 z-score mean data.mean(axis2, keepdimsTrue) std data.std(axis2, keepdimsTrue) 1e-8 data_norm (data - mean) / std # 标签转 one-hot 或保持整数 from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( data_norm, labels, test_size0.2, random_state42, stratifylabels) print(X_train.shape, X_test.shape) # (460, 22, 256) (116, 22, 256)这里有个血泪经验千万别用全局均值和方差归一化。我第一版就是全局归一化结果测试集准确率比训练集低 20 个点因为不同 session 的幅值分布偏移被放大了。trial 级 z-score 相当于把每个样本拉到同一尺度模型只学波形模式。注意如果做跨被试实验归一化统计量只能从训练被试算不能碰测试被试数据否则就是数据泄露。3. Transformer 编码器搭在 EEG 上位置编码和注意力怎么改3.1 为什么原始 Transformer 不能直接套原始 Transformer 是为离散 token 设计的输入是(batch, seq_len, d_model)。EEG 是连续时间序列22 通道 × 256 时间点。直接展平成一个 5632 维向量那自注意力的计算量爆炸而且丢失了通道间的空间结构。常见做法是先把每个时间点的 22 通道值投影到d_model维得到(batch, 256, d_model)再送进 Transformer 编码器。这样序列长度是 256注意力矩阵是 256×256单卡就能跑。位置编码用可学习的位置嵌入不用正弦编码。因为 EEG 的“位置”是时间点但运动想象的关键是节律的相位关系固定正弦编码反而限制表达。我一般加一个nn.Parameter(torch.randn(1, 256, d_model))让模型自己学每个时间点的重要性。import torch import torch.nn as nn class EEGTransformer(nn.Module): def __init__(self, n_channels22, seq_len256, d_model64, nhead4, num_layers3, num_classes2, dropout0.3): super().__init__() # 通道投影把 22 通道映射到 d_model self.input_proj nn.Linear(n_channels, d_model) # 可学习位置编码 self.pos_embed nn.Parameter(torch.randn(1, seq_len, d_model) * 0.02) # Transformer 编码器层 encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforwardd_model * 4, dropoutdropout, batch_firstTrue) self.transformer nn.TransformerEncoder(encoder_layer, num_layersnum_layers) # 分类头用平均池化代替 [CLS] token self.norm nn.LayerNorm(d_model) self.classifier nn.Linear(d_model, num_classes) def forward(self, x): # x: (batch, n_channels, seq_len) - (batch, seq_len, n_channels) x x.permute(0, 2, 1) x self.input_proj(x) # (batch, seq_len, d_model) x x self.pos_embed # 加位置编码 x self.transformer(x) # (batch, seq_len, d_model) x self.norm(x) x x.mean(dim1) # 时间维平均池化 return self.classifier(x)参数说明d_model64是权衡表达能力和过拟合的结果。EEG 数据量小一个被试几百个 triald_model太大直接过拟合。nhead4让每个头关注不同频段或通道组合。num_layers3是我试过 2/3/4 层后的选择4 层在验证集上就开始掉点。dropout0.3比原始 Transformer 的 0.1 大因为 EEG 信噪比低需要更强正则。3.2 注意力池化 vs 平均池化分类头的小改动平均池化把 256 个时间点同等看待但运动想象的关键信息集中在 cue 后 0.5–2.5s 的 ERD 窗口。我后来改成注意力池化加一个可学习的 query 向量对编码器输出做一次注意力加权。class AttentionPool(nn.Module): def __init__(self, d_model): super().__init__() self.query nn.Parameter(torch.randn(1, 1, d_model) * 0.02) self.scale d_model ** 0.5 def forward(self, x): # x: (batch, seq_len, d_model) attn torch.matmul(self.query, x.transpose(1, 2)) / self.scale attn torch.softmax(attn, dim-1) # (batch, 1, seq_len) return torch.matmul(attn, x).squeeze(1) # (batch, d_model)把x.mean(dim1)换成AttentionPool(d_model)(x)验证集准确率在我这边涨了 1.5–2 个点。代价是多了几万个参数但 EEG 数据量下还能承受。答辩时还能把attn权重画出来展示模型确实关注了 0.5–2.5s 区间比平均池化更有说服力。提示如果显存不够把seq_len从 256 降到 128降采样到 64 Hz准确率掉不到 1 个点但训练速度翻倍。4. 训练策略与调参小数据集上 Transformer 怎么不翻车4.1 学习率预热和余弦退火Transformer 对学习率敏感EEG 小数据集更甚。直接用 1e-3 会震荡用 1e-5 收敛太慢。我一般用 AdamW初始学习率 1e-4前 10 个 epoch 线性预热到 1e-3然后余弦退火到 1e-6。权重衰减 0.01配合 dropout 一起压过拟合。from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts model EEGTransformer().cuda() optimizer AdamW(model.parameters(), lr1e-4, weight_decay0.01) scheduler CosineAnnealingWarmRestarts(optimizer, T_020, T_mult2) # 手动预热前 10 个 epoch 线性增加学习率 def warmup_lr(epoch, warmup_epochs10, base_lr1e-4, max_lr1e-3): if epoch warmup_epochs: return base_lr (max_lr - base_lr) * epoch / warmup_epochs return None # 交给 scheduler criterion nn.CrossEntropyLoss(label_smoothing0.1)label_smoothing0.1是我必加的。EEG 标签有噪声被试有时候自己都没想清楚硬标签会让模型过度自信。平滑后训练集准确率降一点但测试集涨 2–3 个点。T_020表示第一次重启周期 20 个 epochT_mult2让后续周期翻倍避免后期学习率降太快。4.2 数据增强滑动窗口和通道 dropout一个被试只有 576 个 trial直接训练 Transformer 肯定过拟合。我常用两种增强滑动窗口切分和高斯噪声注入。滑动窗口把每个 trial 的 256 点按 200 点窗口、50 点步长切一个 trial 变 2 个样本翻倍。高斯噪声加在输入上标准差 0.05–0.1模拟电极噪声。def augment_sliding_window(x, window200, stride50): # x: (batch, channels, seq_len) segments [] for start in range(0, x.shape[2] - window 1, stride): segments.append(x[:, :, start:startwindow]) return torch.stack(segments, dim1) # (batch, n_seg, channels, window) def augment_gaussian(x, std0.08): return x torch.randn_like(x) * std # 训练循环里 for x, y in train_loader: x augment_gaussian(x) x x.cuda() # 如果用了滑动窗口需要把 n_seg 维合并到 batch # 这里简化只做高斯噪声 logits model(x) loss criterion(logits, y.cuda()) ...滑动窗口要注意切分后每个窗口的标签和原 trial 一致但窗口之间高度重叠验证集不能用同样方式增强否则数据泄露。我一般只在训练集做滑动窗口验证集和测试集用完整 trial。注意通道 dropout 是另一种增强随机把 1–2 个通道置零强迫模型不依赖单一电极。但别 drop C3/C4那是运动想象的核心通道。5. 避坑与排查那些让准确率卡在 50% 的坑5.1 现象训练 loss 降验证 loss 升准确率卡在随机水平原因过拟合。EEG 数据量小Transformer 参数量大模型直接记住了训练集噪声。解决先加 dropout 到 0.5加 weight_decay 到 0.05再不行就减层数到 2 层、d_model 到 32。我试过最狠的一次把 d_model 降到 16准确率反而从 62% 涨到 68%。5.2 现象不同被试准确率差异巨大有的 80% 有的 50%原因被试间差异。EEG 的个体差异比图像数据大得多电极位置、头骨厚度、皮层折叠都影响信号。解决做被试独立实验时用被试归一化每个被试单独 z-score或者用 AdaBN 在测试时更新 BatchNorm 统计量。本科毕设如果只做单被试这个问题不明显但答辩老师一定会问跨被试表现。5.3 现象注意力图全是一样的看不出关注了哪个时间段原因位置编码没学好或者注意力坍缩。解决检查位置编码的初始化标准差0.02 是经验值太大太小都学不好。另外可以在损失里加一个正则项鼓励注意力熵不要太低。我一般先可视化几个样本的注意力权重如果全是均匀分布说明模型没学到东西。5.4 现象用预训练 Transformer 微调效果还不如从头训练原因预训练权重来自 NLP 或图像和 EEG 的统计特性完全不匹配。解决要么从头训练要么用 EEG 专用预训练模型如 BIOT、LaBraM但本科毕设阶段不建议碰数据量和算力都不够。老老实实从头训把正则做足。5.5 现象测试集准确率比验证集高很多原因数据泄露。常见的是归一化时用了全局统计量或者滑动窗口增强时训练集和验证集有重叠 trial。解决严格按 trial 划分归一化统计量只从训练集算验证集用训练集的均值和方差。6. 进阶技巧用注意力可视化验证模型真的在“想”最后一章说一个我答辩时用来镇场子的技巧把 Transformer 最后一层的注意力权重拿出来按通道和时间两个维度画热力图。具体做法是取model.transformer.layers[-1].self_attn的输出对多头取平均得到(seq_len, seq_len)矩阵。然后看两件事第一对角线附近是否高亮说明模型关注局部时间模式第二C3 和 C4 对应的时间点之间是否有强注意力说明模型学到了通道间的节律耦合。# 注册钩子抓注意力权重 attn_weights [] def hook(module, input, output): # nn.MultiheadAttention 的 output 是 (attn_output, attn_weights) attn_weights.append(output[1].detach().cpu()) # 假设 model.transformer.layers[-1].self_attn 是 MultiheadAttention handle model.transformer.layers[-1].self_attn.register_forward_hook(hook) model.eval() with torch.no_grad(): _ model(X_test[:1].cuda()) handle.remove() # attn_weights[0] shape: (batch, nhead, seq_len, seq_len) attn attn_weights[0].mean(dim1)[0] # 对多头平均 import matplotlib.pyplot as plt plt.imshow(attn.numpy(), cmaphot, aspectauto) plt.xlabel(Key time step); plt.ylabel(Query time step) plt.colorbar(); plt.show()如果注意力图显示 0.5–2.5s 区间有明显的块状高亮说明模型确实聚焦在 ERD 窗口。如果是一片均匀回去检查位置编码和学习率。我一般还会把 C3、C4 通道的注意力单独拎出来对比如果 C3 对 C4 的注意力权重显著高于 C3 对 Fz 的说明模型学到了运动想象的空间模式。这个图放在毕设论文里比单纯报准确率有说服力得多。另一个实用技巧是测试时增强TTA对测试 trial 加不同随机噪声跑 5 次取平均概率。我这边稳定涨 1–2 个点代价只是推理时间翻 5 倍。本科毕设的测试集不大完全扛得住。提示注意力可视化别用model.transformer.layers[-1]的输入输出直接算PyTorch 的MultiheadAttention默认不返回权重需要设need_weightsTrue或者用钩子抓。不同版本 API 有差异跑之前先打印一下output的类型。我自己做这个方向最大的教训是别一上来就堆 Transformer 层数。最开始我用了 6 层、d_model128训练 loss 降到 0.1测试准确率 52%。后来砍到 3 层、d_model64加了 trial 级归一化和标签平滑直接到 74%。EEG 这行数据质量比模型复杂度重要十倍。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网