
简介这是一份面向本科毕业设计场景的基于Transformer的运动想象脑电信号分类完整项目适合正在完成EEG课题或入门脑机接口BCI的本科生与相关方向研究者。项目采用CNNTransformer融合框架先由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里真正与想象相关的成分可能只有微伏级别而且还混着眼电、肌电和工频干扰。所以分类系统通常要先做带通滤波、伪迹去除和分段再进入模型。传统方法CSPCommon Spatial Patterns配合SVM已经能在这个任务上达到不错的准确率深度学习模型面对的真正问题是能不能在不依赖手工特征的情况下把ERD/ERS的空间和时序模式一起学出来。2.2 传统基线和浅层卷积为什么有效给Transformer做对手盘在动手搭Transformer之前必须先跑通一个传统基线否则你无法判断Transformer到底带来了什么。经典路线是8到30 Hz带通滤波、按trial分段、用CSP做空间滤波提取对数方差作为特征最后用SVM或LDA分类。CSP的本质是找到一组空间滤波器使得两类信号在滤波后方差差异最大化对二分类很有效四分类通常用一对多OvR或多类扩展版本。深度学习基线里最常用的是EEGNet它是一个紧凑的卷积网络第一层做时域卷积第二层做深度可分离卷积Depthwise Pointwise专门用来学习频带滤波和空间滤波的联合表示。EEGNet参数量很小只有几千个参数在公开数据集上四分类准确率能做到70%左右已经超过CSPSVM约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×501100。这种做法类似PatchTST的思路好处是序列长度短计算量小注意力可以关注不同时间段的依赖缺点是通道被直接压扁在特征里注意力计算的不是通道之间的空间关系。第二种做法是把每个电极当作一个tokentoken的特征是它在时间窗口上的采样点。比如用1秒的窗口250个采样点作为特征22个电极就是22个token序列长度等于22。注意力机制在22个token之间做交互学到的就是电极与电极之间的空间依赖这更接近EEG“空间拓扑关系”的本质。缺点是序列长度太短只有22个tokenTransformer的表达力发挥有限。我一般建议先做第二种因为它更符合脑电信号的空间结构而且序列短、训练快方便调试。后续如果想提升性能再改成第一种把时序切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 decay0.01学习率1e-4到1e-3配合warmup使用这个配置在公开的四分类运动想象数据上基本上能跑到与EEGNet持平的成绩。如果你的目标是论文或毕设更漂亮建议在两条token化路线上都做实验并对比这个对比结果本身的价值不亚于模型性能。3.3 小样本局面下的正则化策略为什么直接训练容易过拟合公开的EEG数据集通常只有几十到一百个受试者的几次session按trial算也就几百到几千个样本减去划分验证集之后单被试的训练样本常只有三五百个。这个量级对Transformer来说极度局促直接训练时Loss下降很快验证集准确率走到某个点就开始掉这是过拟合的典型信号。正则化策略可以从三个层面入手。数据层面使用滑窗重叠切段来扩充样本量——一个4秒的trial可以用2秒窗口、50%重叠切成3段样本量直接乘以3但要注意同一trial的段不能同时出现在训练和验证集里。模型层面TransformerEncoder里各子层默认有dropout建议再加一个DropPathStochastic 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, preloadTrue) # 选择运动想象任务的EEG通道排除EOG和ECG picks mne.pick_types(raw.info, eegTrue, eogFalse, ecgFalse) # 带通滤波到8-30Hz保留mu和beta节律 raw.filter(8.0, 30.0, pickspicks, methodiir) # 根据事件注解切分epochtmax4表示取提示后4秒 events, event_id mne.events_from_annotations(raw) epochs mne.Epochs( raw, events, event_id, tmin0.0, tmax4.0, baseline(None, 0.0), pickspicks, preloadTrue ) # 转换为numpy数组量纲是(trials, channels, times) X epochs.get_data() y epochs.events[:, -1] # 每个被试独立做z-score标准化 X (X - X.mean(axis(0, 2), keepdimsTrue)) / (X.std(axis(0, 2), keepdimsTrue) 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_model64, nhead4, num_layers3, dim_feedforward256, dropout0.1, n_classes4): 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_modeld_model, nheadnhead, dim_feedforwarddim_feedforward, dropoutdropout, batch_firstTrue ) self.transformer TransformerEncoder(encoder_layer, num_layersnum_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(dim1) # 序列平均池化 x self.dropout(x) return self.classifier(x)逻辑说明嵌入层用1x1卷积它不是学时间滤波而是对每个时间点做通道维的线性混合把22个通道压缩成64维向量等价于空间特征投影。位置编码加在时间维度上让模型知道每个token在时间轴上的先后顺序这对运动想象任务里ERD的时序演变有关键作用。x.mean(dim1)是在对全部时间步做平均池化和ViT里用class token的方式不同这样可以让模型在时间上“做决定”哪段ERD模式最显著对应的token贡献就更大。如果换成时间patch方案需要把嵌入层改成nn.Conv1d加nn.Unfold的形式在对时间维滑动切patch时保持通道维不变。这个改动可以放在对比实验里做。先跑电极token方案跑通后性能如果不理想再切换成时间patch方案每次改一个变量。4.3 训练配置与评价指标warmup、早停和Kappa值训练代码有以下几个关键配置优化器用AdamW学习率1e-4weight decay 0.01用线性warmup在前5个epoch把学习率慢慢升到目标值再配合余弦退火cosine annealing衰减到接近0。批大小和数据集的样本量挂钩单被试训练建议batch size32跨被试训练可以上64。from torch.optim import AdamW from torch.optim.lr_scheduler import OneCycleLR criterion nn.CrossEntropyLoss(label_smoothing0.1) optimizer AdamW(model.parameters(), lr1e-4, weight_decay0.01) scheduler OneCycleLR( optimizer, max_lr1e-4, steps_per_epochlen(train_loader), epochs50, pct_start0.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_norm1.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_norm1.0是为了应对少数trial里幅度异常的伪迹样本这类样本很容易让Loss突然变大一次更新就把Transformer的预训练嵌入冲坏。评价指标不能只看准确率。四分类任务里类别大致平衡准确率是合理的参考但有个更严格的指标是Cohens 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的部件每一步都用验证集说话这条路最稳。希望帮到你。本文还有配套的精品资源点击获取