☰
Transformer运动想象脑电分类实战:数据处理、模型训练与避坑指南
2026/10/10 1:33:40 网站建设 项目流程

简介:这是一份面向本科毕业设计场景的基于Transformer的运动想象脑电信号分类完整项目,适合正在完成EEG课题或入门脑机接口(BCI)的本科生与相关方向研究者。项目采用CNN+Transformer融合框架,先由CNN提取局部时间与空间特征,再由Transformer捕捉全局依赖,从数据预处理、特征提取、模型构建、训练评估到结果可视化形成完整闭环,为传统机器学习方法在脑电序列建模中特征提取困难、长期依赖不足等问题提供了新方案。包体共31个文件,以23个Python脚本为核心,涵盖CNNTransformer模型、EEGNet、自注意力机制、训练与K折交叉验证、统计分析等模块;同时包含MATLAB预处理脚本、模型权重(.pth)、特征数据(.npy)、配置参数表格、README说明文档,压缩包总大小18.45MB,目录结构清晰便于按模块复用。读者可以对照源码理解自注意力机制在脑电信元中的应用,也可借助箱线图、t-SNE、脑热力图、AUC曲线等可视化与统计脚本深入分析分类结果,快速完成从数据到结论的毕业设计实验。目前已有276人学习,是一份兼具工程完整性与教学参考价值的资料。

1. 本科毕设做“Transformer运动想象脑电分类”,先别急着跑模型

很多同学拿到这个题目的第一反应,是找个现成的Transformer代码仓库,把脑电数据当成普通时间序列丢进去训练,跑了两周发现准确率比EEGNet还低十几个点,整个人都麻了。这个现象太常见了,因为运动想象脑电分类根本不是“换了更强的模型就能涨点”的任务,它要解决的是小样本、强噪声、跨被试差异大这三个基础问题,而Transformer恰好又是一个特别吃数据和训练技巧的架构。这个标题里的内容,其实就是围绕“怎么把Transformer用对”来展开的:数据怎么切、序列怎么组织、训练怎么配、坑在哪里。这篇笔记就带你走一遍从数据预处理到模型训练再到调优的完整路线,适合作为毕业设计入门参考,也适合刚接触脑电深度学习的同学照着复现。

2. 运动想象脑电分类先搞清楚数据长什么样:从ERD/ERS到样本组织

2.1 任务边界与数据形态:四分类运动想象不是通用的序列问题

运动想象(Motor Imagery, MI)任务的标准范式是让受试者在提示音后想象左手、右手、双脚或舌头的运动,持续约3到4秒。EEG记录到的信号里,运动皮层对应的mu节律(8-12 Hz)和beta节律(13-30 Hz)会出现事件相关去同步(ERD)和事件相关同步(ERS)现象,分类算法要学的就是这种频带能量在空间分布上的变化模式。

公开的四分类运动想象数据集通常用64通道或22通道的电极帽采集,采样率在250 Hz左右,每个trial包含从提示出现到想象结束的完整时间段。一个典型的训练样本形状是(通道数, 时间点数),比如22通道、1000个时间点(4秒×250 Hz)。这个维度特征意味着两点:第一,时间维度很长,单纯用CNN在小样本上很难学到长距离的时序依赖;第二,通道数相比图像任务的像素数少得可怜,直接把EEG当图像处理并不合适。

运动想象分类的难点在于信噪比极低,单个trial里真正与想象相关的成分可能只有微伏级别,而且还混着眼电、肌电和工频干扰。所以分类系统通常要先做带通滤波、伪迹去除和分段,再进入模型。传统方法CSP(Common Spatial Patterns)配合SVM已经能在这个任务上达到不错的准确率,深度学习模型面对的真正问题是:能不能在不依赖手工特征的情况下,把ERD/ERS的空间和时序模式一起学出来。

2.2 传统基线和浅层卷积为什么有效:给Transformer做对手盘

在动手搭Transformer之前,必须先跑通一个传统基线,否则你无法判断Transformer到底带来了什么。经典路线是:8到30 Hz带通滤波、按trial分段、用CSP做空间滤波,提取对数方差作为特征,最后用SVM或LDA分类。CSP的本质是找到一组空间滤波器,使得两类信号在滤波后方差差异最大化,对二分类很有效;四分类通常用一对多(OvR)或多类扩展版本。

深度学习基线里最常用的是EEGNet,它是一个紧凑的卷积网络,第一层做时域卷积,第二层做深度可分离卷积(Depthwise + Pointwise),专门用来学习频带滤波和空间滤波的联合表示。EEGNet参数量很小,只有几千个参数,在公开数据集上四分类准确率能做到70%左右,已经超过CSP+SVM约10个百分点。它的成功说明一个事实:运动想象分类的主要增益来自“滤波+空间特征提取”这两个操作的联合学习,而不是来自网络的深度。

这给Transformer的定位提出了明确要求:除非自注意力机制能在“长时序依赖”或“通道间关系建模”上带来可验证的增益,否则一个几千参数的浅层CNN就够了。所以Transformer模型要做的事情不是取代卷积,而是先在逻辑和实验上回答一个问题:运动想象EEG里是否存在卷积感受野覆盖不到的依赖关系?例如,想象任务后期(第2秒到第4秒)的ERD模式与任务开始阶段的提示特征是否存在跨时间的依赖,这些依赖对分类有没有帮助。

2.3 序列化切口:电极通道和时间窗口的两个选择

Transformer需要输入是“一组token的序列”,而EEG的原始形态是通道×时间的矩阵,所以第一步是把矩阵组织成token序列。我常见到两种做法。

第一种做法是把时间轴切成patch,每个patch当作一个token,通道作为token的特征维度。假设一个trial是22通道×1000点,把时间切成20段,每段50个点,就得到20个token,每个token的特征维度是22×50=1100。这种做法类似PatchTST的思路,好处是序列长度短,计算量小,注意力可以关注不同时间段的依赖;缺点是通道被直接压扁在特征里,注意力计算的不是通道之间的空间关系。

第二种做法是把每个电极当作一个token,token的特征是它在时间窗口上的采样点。比如用1秒的窗口,250个采样点作为特征,22个电极就是22个token,序列长度等于22。注意力机制在22个token之间做交互,学到的就是电极与电极之间的空间依赖,这更接近EEG“空间拓扑关系”的本质。缺点是序列长度太短,只有22个token,Transformer的表达力发挥有限。

我一般建议先做第二种,因为它更符合脑电信号的空间结构,而且序列短、训练快,方便调试。后续如果想提升性能,再改成第一种把时序切patch的方法,做两者的对比实验,这个对比本身也很有价值。

3. Transformer在脑电分类上的适用性:注意力机制到底在学什么

3.1 为什么常规CNN在这里够用但不够稳?

CNN在运动想象EEG上的工作方式可以理解为“先学一组短时滤波器,再学空间权重”,EEGNet就是这个逻辑的极致压缩版。EEGNet第一层Conv2d的卷积核覆盖全部通道,实际上等价于在学一组空间滤波器;深度可分离卷积的深度维在学每个通道自己的时序模式。这种设计很契合ERD/ERS的物理含义,但它有一个明显的边界:卷积核的时间长度是有限的,通常只有几十到一百个采样点,超过这个范围的时序依赖只能靠堆层数来近似,而浅层网络又堆不深。

运动想象任务里是否存在长时序依赖?有一个现象值得注意:受试者听到提示音之后,ERD往往不是瞬间出现的,而是有一个逐渐增强的过程;想象结束后,beta节律还会出现一个短暂的ERS反弹。这说明任务前后的信号模式之间存在时间结构,但这种结构是否对分类有决定性作用,文献里其实没有统一的结论。实际实验中你会发现,把时间窗口从4秒裁到2秒,EEGNet的准确率下降通常不超过2个百分点,这说明局部特征在运动中占了大头。

所以Transformer要证明自己的价值,不能只拿准确率说事,还要做“消融时间分辨率”的实验:如果把PatchTST风格的长序列token输入Transformer,它的短期和长期注意力权重分布是怎样的?如果注意力热图显示模型主要关注局部patch,那说明CNN已经覆盖了关键信息;如果它稳定关注远端patch,才说明长依赖这个因素确实存在。这个实验不需要额外数据,在现有测试集上把attention map可视化,就能解释。

3.2 模型结构拆解与超参初值

用Transformer做EEG分类,结构不必复杂,参考一个最小的baseline配置就能起步。

整个模型由四个部分组成:输入嵌入层(Embedding)、位置编码、TransformerEncoder、分类头。输入嵌入层负责把每个token的特征维度压缩到d_model,常见做法是用一个线性层或小卷积层。位置编码在脑电任务里建议使用可学习的位置编码(learned positional embedding),因为EEG的“位置”是电极编号或时间窗口序号,不存在图像那种绝对坐标的语义,可学习版本更容易适应数据分布。TransformerEncoder用PyTorch自带的即可,层数不用多,2到3层足够。分类头是一个线性层加Softmax或LogSoftmax,输出类别概率。

超参初始化的推荐值如下表所示。

参数推荐值说明
d_model64或128嵌入维度,64起步,显存不够不改
nhead4注意头数,头数太多参数暴涨
num_layers2到3层数与泛化性相关,超过4层小样本基本过拟合
dim_feedforward256FFN隐藏层,通常是d_model的2到4倍
dropout0.1到0.2嵌入层和注意力层都加
patch_size(时间切法)50个采样点200ms窗口(250Hz采样率)
batch_size32到64受限于样本量,越大越稳
优化器AdamW配合weight decay=0.01
学习率1e-4到1e-3配合warmup使用

这个配置在公开的四分类运动想象数据上,基本上能跑到与EEGNet持平的成绩。如果你的目标是论文或毕设更漂亮,建议在两条token化路线上都做实验并对比,这个对比结果本身的价值不亚于模型性能。

3.3 小样本局面下的正则化策略:为什么直接训练容易过拟合

公开的EEG数据集通常只有几十到一百个受试者的几次session,按trial算也就几百到几千个样本,减去划分验证集之后,单被试的训练样本常只有三五百个。这个量级对Transformer来说极度局促,直接训练时Loss下降很快,验证集准确率走到某个点就开始掉,这是过拟合的典型信号。

正则化策略可以从三个层面入手。数据层面,使用滑窗重叠切段来扩充样本量——一个4秒的trial可以用2秒窗口、50%重叠切成3段,样本量直接乘以3,但要注意同一trial的段不能同时出现在训练和验证集里。模型层面,TransformerEncoder里各子层默认有dropout,建议再加一个DropPath(Stochastic Depth),训练时随机跳过整个encoder block,这在视觉Transformer里被证明对小样本有效。损失函数层面,标签平滑(label smoothing)设为0.1,可以避免模型对训练集预测过于自信,间接缓解过拟合。

其实还有一个很实用的操作:先用EEGNet训练一个预训练模型,用它初始化Transformer的输入嵌入层。因为EEGNet的浅层卷积本质上是学频带滤波和空间滤波,这些特征在不同任务间是通用的。这样Transformer只需要从头学习序列建模部分,训练难度会下降很多。

4. 从原始EEG到训练Transformer:完整代码与关键参数

4.1 数据预处理与样本划分:MNE读取到标准化

这里以常见的公开运动想象数据集格式为例,数据通常以GDF或EEGLAB的set格式存储。用MNE库读取和预处理是最成熟的方案。

import mne import numpy as np # 读取原始数据,raw是mne.io.Raw对象 raw = mne.io.read_raw_gdf("subject01_train.gdf", preload=True) # 选择运动想象任务的EEG通道,排除EOG和ECG picks = mne.pick_types(raw.info, eeg=True, eog=False, ecg=False) # 带通滤波到8-30Hz,保留mu和beta节律 raw.filter(8.0, 30.0, picks=picks, method="iir") # 根据事件注解切分epoch,tmax=4表示取提示后4秒 events, event_id = mne.events_from_annotations(raw) epochs = mne.Epochs( raw, events, event_id, tmin=0.0, tmax=4.0, baseline=(None, 0.0), picks=picks, preload=True ) # 转换为numpy数组,量纲是(trials, channels, times) X = epochs.get_data() y = epochs.events[:, -1] # 每个被试独立做z-score标准化 X = (X - X.mean(axis=(0, 2), keepdims=True)) / (X.std(axis=(0, 2), keepdims=True) + 1e-6)

滤波到8到30Hz是为了去掉低频漂移和高频肌电,这是运动想象任务的惯例。tmax设为4秒是因为大多数范式在提示后4秒内包含完整ERD/ERS模式。基线校正用提示前0.2秒到0秒,能去掉直流偏置。标准化按通道和时间维计算均值和标准差,注意每一维求均值时用了keepdims,这样广播到原始形状不会有维度错位。有一个高频踩坑点:需要把X复制成float32,否则后续PyTorch训练会因为数据类型不匹配报错。

样本划分方面,我强烈建议按“被试内”划分来做主实验:同一个被试的trial按7:1.5:1.5拆成训练、验证、测试三部分。跨被试实验单独做,不能用全体数据混在一起划分,否则会出现数据泄露。

4.2 核心模型实现:一个可运行的Transformer分类器

下面这个实现以“电极token”方式工作:输入形状为(batch, channels, time),先转置成(batch, time, channels),再用卷积把通道数投影到d_model,相当于每个时间点是一个token。如果你的方案是时间patch切法,只需把嵌入层的卷积核改一下。

import torch import torch.nn as nn from torch.nn import TransformerEncoder, TransformerEncoderLayer class EEGTransformer(nn.Module): def __init__(self, n_channels, d_model=64, nhead=4, num_layers=3, dim_feedforward=256, dropout=0.1, n_classes=4): super().__init__() # 输入嵌入:用1x1卷积把通道数映射到d_model self.embed = nn.Conv2d(1, d_model, kernel_size=(1, 1)) # 可学习的位置编码,长度最长为2000个token self.pos_embed = nn.Parameter(torch.randn(1, 2000, d_model) * 0.02) # Transformer编码器 encoder_layer = TransformerEncoderLayer( d_model=d_model, nhead=nhead, dim_feedforward=dim_feedforward, dropout=dropout, batch_first=True ) self.transformer = TransformerEncoder(encoder_layer, num_layers=num_layers) # 分类头:对序列维度做平均池化后再分类 self.classifier = nn.Linear(d_model, n_classes) self.dropout = nn.Dropout(dropout) def forward(self, x): # x: (batch, channels, time) x = x.unsqueeze(1) # (batch, 1, channels, time) x = self.embed(x).squeeze(-2) # (batch, d_model, time) x = x.permute(0, 2, 1) # (batch, time, d_model) # 加入位置编码 seq_len = x.size(1) x = x + self.pos_embed[:, :seq_len, :] x = self.transformer(x) # (batch, time, d_model) x = x.mean(dim=1) # 序列平均池化 x = self.dropout(x) return self.classifier(x)

逻辑说明:嵌入层用1x1卷积,它不是学时间滤波,而是对每个时间点做通道维的线性混合,把22个通道压缩成64维向量,等价于空间特征投影。位置编码加在时间维度上,让模型知道每个token在时间轴上的先后顺序,这对运动想象任务里ERD的时序演变有关键作用。x.mean(dim=1)是在对全部时间步做平均池化,和ViT里用class token的方式不同,这样可以让模型在时间上“做决定”,哪段ERD模式最显著,对应的token贡献就更大。

如果换成时间patch方案,需要把嵌入层改成nn.Conv1d加nn.Unfold的形式,在对时间维滑动切patch时保持通道维不变。这个改动可以放在对比实验里做。先跑电极token方案,跑通后性能如果不理想,再切换成时间patch方案,每次改一个变量。

4.3 训练配置与评价指标:warmup、早停和Kappa值

训练代码有以下几个关键配置:优化器用AdamW,学习率1e-4,weight decay 0.01,用线性warmup在前5个epoch把学习率慢慢升到目标值,再配合余弦退火(cosine annealing)衰减到接近0。批大小和数据集的样本量挂钩,单被试训练建议batch size=32,跨被试训练可以上64。

from torch.optim import AdamW from torch.optim.lr_scheduler import OneCycleLR criterion = nn.CrossEntropyLoss(label_smoothing=0.1) optimizer = AdamW(model.parameters(), lr=1e-4, weight_decay=0.01) scheduler = OneCycleLR( optimizer, max_lr=1e-4, steps_per_epoch=len(train_loader), epochs=50, pct_start=0.1 ) best_acc = 0.0 for epoch in range(50): model.train() for X_batch, y_batch in train_loader: optimizer.zero_grad() out = model(X_batch) loss = criterion(out, y_batch) loss.backward() # 梯度裁剪,防止EEG中的异常样本造成梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() scheduler.step() # 验证集评测 model.eval() val_acc = evaluate(model, val_loader) if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), "best_model.pt")

以上只是训练循环的片段,值得注意的是OneCycleLR这个调度器:pct_start表示前10%的训练步数用来升学习率,后面90%做余弦退火。这个调度策略比固定学习率在EEG这种小样本任务上稳定很多,能有效避开局部最优。梯度裁剪的max_norm=1.0是为了应对少数trial里幅度异常的伪迹样本,这类样本很容易让Loss突然变大,一次更新就把Transformer的预训练嵌入冲坏。

评价指标不能只看准确率。四分类任务里类别大致平衡,准确率是合理的参考,但有个更严格的指标是Cohen's Kappa系数,它扣除了随机一致率,对类别不平衡更敏感。BCI竞赛的官方评分通常用Kappa值,满分是1.0,随机水平约为0。Kappa可以通过sklearn.metrics.cohen_kappa_score一行算出。多做5折交叉验证求均值和标准差,比单次划分的结果更有说服力。

5. 运动想象Transformer避坑图鉴:四个必看的翻车现场

5.1 现象:训练集准确率接近100%,验证集却只有40%,像随机猜测

这是运动想象深度学习里最常见的翻车现场。原因基本都指向数据划分或预处理出了问题,而不是模型的问题。最典型的有两个:一是标准化时用全体数据的均值和标准差计算,把验证集信息混进了训练过程;二是划分训练集和验证集时没有做随机种子隔离,同一个trial的滑窗截段同时出现在两边。

解决方法是严格分三步走:先划分训练/验证/测试,再对训练集单独计算标准化参数,然后把同样的参数应用到验证和测试集。滑窗截段时,按trial的原始编号进行分组,同一个trial的所有截段要么全在训练集,要么全在验证集,不能混放。

5.2 现象:位置编码一换,结果差了8个百分点,玄学

有同学发现,把可学习位置编码换成标准正弦位置编码后,准确率明显下降。原因在于EEG的序列长度本来就短(电极token方案只有22到40个token),而正弦位置编码的频率函数是基于长序列设计的,覆盖不到短序列的编码需求。可学习位置编码在短序列上的表达更自由,能适应电极位置或时间窗口的实际顺序结构。

解决方法是对于短序列任务一律用可学习位置编码,并在初始化时把std设小一些(如0.02),避免初始位置编码幅度过大干扰输入特征的梯度。另一种替代方案是去掉位置编码,改用“电极坐标先验嵌入”——把每个电极的x、y坐标(10-20系统标准位置)和trial中的分段序号拼成一个向量,用一个小MLP映射到d_model。这等于把位置编码从“序列位置”改成“空间位置+时间位置”,更贴近EEG的物理含义,有兴趣可以试,注意对比。

5.3 现象:注意力可视化热图看起来像随机噪声,找不到规律

训练完成后想可视化注意力,画出来的图是一团乱麻。多数情况下这不代表模型没学到东西,而是可视化方式不对。TransformerEncoder最后一层的注意力权重是多头的,如果直接对所有头取平均,那些关注低频全局信息的头和关注局部细节的头会相互抵消。

解决方法是改用Attention Rollout方法,把多层、多头的注意力权重复合传播到输入层,得到每个token对最终分类的贡献分数。对脑电任务,更直观的做法是不看原始注意力矩阵,而是把注意力得分加权回电极维度,画出头皮拓扑图(Topomap),再和ERD/ERS的经典空间分布对照。如果模型学得对,注意力高权重的区域应该集中在C3、C4、Cz这些运动皮层对应的电极附近,这个可视化结果可以直接放进论文里当证据。

5.4 现象:跨被试训练比单被试训练掉点,模型更“笨”了

有些同学盼着“数据越多越好”,把多个被试的数据合在一起训练,结果准确率不但没涨,反而比单被试模型低,尤其在目标被试的测试集上掉点明显。原因是跨被试的EEG信号存在巨大的个体差异,包括电极位置差异、皮层折叠差异、基线生理状态差异,同一个ERD模式在两个人头上的形态完全不同。

解决问题有两个思路:引入域适配操作,在训练时随机对通道做轻微缩放或加噪(通道级数据增强),强迫模型学习与电极绝对值无关的相对模式;与迁移学习结合,先在多个被试上预训练,再冻结前两层,只微调后面几层和分类头。微调时用目标被试的少量数据,训练10到20个epoch,学习率降到3e-5,通常比从零训练效果好。注意,做跨被试实验时,预训练被试数据里绝不能包含目标被试的任何trial。

5.5 现象:Loss在训练中期开始震荡,学习率调来调去也没改善

这个现象往往不出在模型本身,而是数据加载环节埋了雷。运动想象数据集有时不同trial末尾会有静息段或杂散标记,导致batch里不同样本的有效时间长度不一致。Transformer对序列长度不一致的容忍度比对CNN更低,因为不同长度的序列做完attention后,平均池化结果会偏向长序列。

解决方法是统一每个trial的长度,截断或补零到相同时间点数。训练时把每个batch的序列长度都检查一遍,出现不一致立刻修正。另一种叠加的原因是对比学习场景下使用了过大的学习率,导致注意力权重剧烈震荡,此时把warmup延长到前15%的步数,基本能压住。

6. 从跑通到有效:注意力可视化和跨被试微调的最后一公里

模型跑通、准确率稳定之后,毕业设计还差两步才算完整:证明Transformer确实学到了ERD/ERS相关特征,以及找到一条比从零训练更好的迁移路径。

先做第一件事,把注意力得分投影到头皮拓扑图。具体做法是:取模型最后一层Encoder输出的attention矩阵,对除class token外的序列维度做平均,得到每个电极(或时间patch)的注意力权重,再用MNE的plot_topomap画出2D头皮图。你会看到注意力热点会集中在运动皮层对应的通道附近。如果结果显示注意力权重分布在整个头皮上,没有聚焦,说明模型的嵌入层可能没有学到频带特征,需要在嵌入层前加一个短时傅里叶(STFT)变换层,把时序信号先转成时频表示,再进Transformer,这种做法需要重新训练,但可解释性会明显增强。

第二件事是跨被试微调。预训练一个多被试模型,然后把目标被试的少量数据拿出来做微调。注意一个细节:微调时不要加载位置编码的参数,因为不同被试的数据长度可能不同,位置编码的语义会变。应该做的是把位置编码从预训练权重中分离,重新初始化,再一并微调。我实测这种做法有稳定的增益,可以当作最后的性能提升手段。

回看整个方案,最值钱的其实不是Transformer本身,而是那一整套“任务数据怎么理解、样本怎么切、嵌入怎么设计、坑在哪里”的经验。我第一次做这个方向时,也经历了“换模型不如调数据”的教训,把注意力全放在搭模型上,最后被数据划分问题卡了一周。先跑通传统基线,再逐层加Transformer的部件,每一步都用验证集说话,这条路最稳。希望帮到你。

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

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

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

立即咨询