恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

基于Transformer的脑电信号分类实战:从预处理到PyTorch实现

  • 首页
  • 资讯中心
  • /
  • 基于Transformer的脑电信号分类实战:从预处理到PyTorch实现

相关资讯

Mac mini上部署GUI Agent Mano-P:本地视觉智能体实战全流程 2026/10/6 20:03:36
Godot编辑器移植鸿蒙PC:难度分析与可行性实践 2026/10/6 19:58:36
艺诚美业系统部署与二次开发实战指南 2026/10/6 19:58:36

最新资讯

Niushop开源商城小程序SAAS版:多商户入驻与二次开发实战指南
毕业设计模型训练三件套:数据集、源代码与文档说明搭建指南
labelImg图像标注工具实战:从VOC XML到YOLO格式转换与避坑指南
SpringBoot3 接入大模型:微信智能客服文本语音图片处理实战
Qt6桌面端硬件信息采集:QProcess调用系统命令实战
能碳管理系统选型:三大流派拆解与绿色合规作业派实战指南

今日推荐

2026 AI 开发全家桶落地指南:TaoToken 统一 Key 打通 IDE 插件、Agent 与自动化代码审查全链路配置实测
MR25H40CDF+STM32F031C6工业级高可靠数据存储方案
MRAM+STM32工业断电数据保全实战指南

本周热门

MR25H40CDF + PIC18F65K40:工业记录仪高可靠存储实战
基于STM32的数控恒压恒流电源设计:从硬件到PID调参全解析
LT9211 MIPI重定时器原理与双路扇出实战指南

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

基于Transformer的脑电信号分类实战:从预处理到PyTorch实现

发布时间:2026/10/6 20:03:36
基于Transformer的脑电信号分类实战:从预处理到PyTorch实现 简介这是一份面向本科毕业设计场景的Transformer脑电信号分类系统源码基于CNNTransformer混合框架实现主要服务于计算机、生物医学工程等相关专业正在准备毕业设计、课程设计或期末大作业的学生。项目由导师指导完成并以高分通过评审代码完整、开箱即可运行尤其适合希望快速上手深度学习实践的小白学习者。压缩包共包含31个文件整体大小约18.46MB其中有23个Python脚本、2个MATLAB脚本、2个Excel表格、1个XML工程配置、1个Markdown说明、1个npy数据文件和1个pth权重文件。Python脚本主要负责模型搭建、训练与可视化MATLAB脚本承载数据预处理与特征提取权重文件可直接用于推理或迁移学习目录结构清晰便于按模块对照学习。内容涵盖CNNTransformer、EEGNet、Spatial_Temporal_Attention等多种模型实现同时提供CAM脑区热力图、tSNE降维展示、箱线图与AUC曲线绘制等功能脚本可帮助读者完整复现脑电分类实验流程。目前已有64人学习下载无论用于毕设参考还是项目实战练习这套源码都能提供明确的技术路径与实现细节。1. 基于Transformer的脑电信号分类系统它不是换了骨架是把脑电当序列重新读了一遍如果说 CNN 是在问「这段脑电波形里有没有一个局部模板」那这个系统里的 Transformer 部分在问的是「模板出现之后后面几百毫秒发生了什么」。CNN 抓局部特征、Transformer 建全局依赖是脑电信号分类源码里最常见的组合也是本科毕业设计愿意选它的原因结构够新、效果有提升空间、代码闭环可控。这套框架适合手里有 DEAP / SEED 这类公开脑电数据集想用 PyTorch 从数据预处理一路跑到训练、评估和可视化的人。下文就按实际落地顺序展开先处理数据再搭模型再训练调参最后讲复现里最容易踩的坑以及答辩时能用上的可视化技巧。2. 数据与预处理把原始脑电喂给 Transformer 之前先过这几道关卡2.1 DEAP / SEED 数据集怎么读形状、通道和标签别想当然公开数据集的好处是任务定义清楚坏处是每个数据集的存储格式和基线位置都不一样想当然地读出数组就开训基本都会死在第 5 章那类坑里。DEAP 是 32 名受试者观看 40 段视频时的 32 通道脑电常见预处理版本把采样率降到 128 Hz每段实验含 3 秒基线它附带的 valence / arousal 打分常用于二分或回归任务。SEED 同样流行62 通道、采样率更高常用于情绪三分类。不同版本的文件在通道顺序、标签取值和基线长度上都不一致第一件事永远是把 shape 打出来看。import scipy.io as sio mat sio.loadmat(subject01.mat) print(mat.keys()) data mat[data] # 例如 (40, 40, 8064) labels mat[labels] # 例如 (40, 4) print(data.shape, labels.shape, labels.dtype) print(labels[:5])这段代码看似简单但能挡住一大半的翻车。DEAP 常见下发的 128 Hz 版本里8064 63 秒 × 128 Hz前 3 秒是基线所以每个 trial 的有效信号是后 60 秒通道维度是 40前 32 个才是脑电后面 8 个是眼电、肌电等外周信号。如果用的是 512 Hz 的原始版本基线对应的采样点就要从 384 改成 1536。模型只吃 32 个脑电通道外围信号别混进来。标签也一样要确认。DEAP 的维度顺序是 valence、arousal、dominance、liking取值 1 到 9不是现成的 0/1。做情绪效价二分类时常规做法是以 5 为阈值把 valence 变成两类也有人按 4.5 或中位数划分这个阈值本身就是论文里值得写一笔的决策。2.2 切窗、基线校正与归一化一份可以直接复制的 numpy 管线脑电信号在进入模型前通常要做三件事去掉基线、切窗、归一化。基线校正的做法是减去每个 trial 前 3 秒的平均信号因为脑电采集时存在直流漂移和初始状态偏移切窗是为了扩样本毕竟一个受试者只有 40 段视频直接整段分类数据量太小归一化则是让不同受试者、不同通道的幅值差异不主导梯度。import numpy as np # data: (trials, channels, samples)128Hz 版本去掉 384 个基线采样点 eeg data[:, :32, 384:].astype(np.float32) baseline eeg[:, :, :384].mean(axis2, keepdimsTrue) eeg eeg - baseline win 128 # 1 秒窗口128 Hz 下正好 128 个点 step 64 # 0.5 秒步长窗口重叠 50% X_list, y_list [], [] val_binary (labels[:, 0] 5).astype(np.int64) for trial in range(eeg.shape[0]): x eeg[trial] n_windows (x.shape[1] - win) // step 1 for start in range(0, x.shape[1] - win 1, step): X_list.append(x[:, start:start win]) y_list.append(val_binary[trial]) X np.stack(X_list) # (n_windows, 32, 128) y np.asarray(y_list) # (n_windows,) mean X.mean(axis(0, 2), keepdimsTrue) std X.std(axis(0, 2), keepdimsTrue) 1e-6 X (X - mean) / std代码逻辑先按 trial 循环每个 trial 切成若干 1 秒窗口步长 0.5 秒所以相邻窗口有 50% 重叠每个窗口的标签继承所属 trial 的 valence 二分类标签。归一化是按通道做的也就是对每个通道在所有时间点上算均值和标准差而不是把整段数据压到 0~1。脑电幅值受个体差异影响极大min-max 归一化会被一次眨眼伪迹拉到完全失真的尺度z-score 相对稳。波段的带通滤波也建议做常用 0.5~40 Hz 或 1~50 Hz。可以用 scipy.signal.butter 配合 filtfilt注意 filtfilt 是零相位滤波处理脑电比直接 lfilter 好得多。滤波放在切窗之前对整段 trial 做避免每个窗口边缘产生滤波伪影。2.3 怎么划分训练/验证/测试按试次或按人切别把窗口随机打散这是整个脑电分类源码里出现频率最高的坑也是判断代码作者有没有真的跑过脑电数据的试金石。切窗后一个 trial 会生成几十个窗口如果直接把所有窗口随机分成 train / val / test同一个 trial 的相邻窗口会同时出现在训练集和验证集里模型靠记忆脑电的连续性和噪声模式就能在验证集上拿高分但换到真实的单试次预测时会直接崩掉。正确的做法是分组划分要么按 subject 划分要么至少按 trial 划分。按 subject 划分的测试结果更能说明泛化能力但因为训练时没见过这个人准确率会略低按 trial 划分相当于同一个人的数据既训练又测试容易一些适合验证模型结构能不能学起来。工程上我一般先用 subject 划分做最终评估中间调参时按 trial 划分做 GroupKFold 交叉验证两份分数分开记录。sklearn 里直接有 GroupKFoldgroup 就是 trial 索引或 subject 索引调用方式跟 KFold 几乎一样。到这里X 的形状是 (n_windows, 32, 128)标签是 (n_windows,)接下来要处理的就是模型怎么读这些窗口了。3. 模型设计CNN 降维 Transformer 建模一套可以直接抄的 PyTorch 源码3.1 为什么总是 CNN 接 Transformer而不是反过来或只用一边看过 Transformer 通俗介绍的人很容易产生一个冲动把 128 个采样点直接送进自注意力让注意力自己发现模式。实际做一遍就会知道序列一旦到几百甚至几千注意力矩阵的规模和训练时间都会失控而且脑电里的 alpha 节律、ERD/ERS 这类特征本质是局部时间模式自注意力在噪声主导的脑电上学这件事效率很低。反过来只用 CNN它擅长提取局部模式但对「前额和枕区两个通道之间的远距离依赖」这种跨通道、跨时刻的关系表达得很弱。CNN 和 Transformer 组合的典型分工是CNN 先把原始波形压缩成一组 token每个 token 携带一小段时间窗的局部特征然后 Transformer 在这些 token 之间做全局交互。对脑电来说这就相当于先看每一小段波形里发生了什么再判断这些事件前后怎么串联。3.2 手写最小可用框架CNN-Transformer 分类器PyTorch下面这段代码可以直接放进工程输入是 (batch, channels, time_steps)输出是类别 logits。刻意不用第三方高级封装库方便对着源码改参数、讲答辩。import math import torch import torch.nn as nn class CNNTokenizer(nn.Module): 把 EEG 波形切成 token 并做局部特征提取 def __init__(self, in_channels32, d_model64, kernel16, stride8): super().__init__() self.conv nn.Sequential( nn.Conv1d(in_channels, d_model, kernel, stride, paddingkernel // 2), nn.BatchNorm1d(d_model), nn.GELU(), nn.Conv1d(d_model, d_model, 3, padding1), nn.BatchNorm1d(d_model), nn.GELU(), ) def forward(self, x): # x: (B, C, T) - (B, d_model, T) return self.conv(x).transpose(1, 2) # (B, T, d_model) class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len512, dropout0.1): super().__init__() pe torch.zeros(max_len, d_model) pos torch.arange(0, max_len, dtypetorch.float).unsqueeze(1) div torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] torch.sin(pos * div) pe[:, 1::2] torch.cos(pos * div) self.register_buffer(pe, pe.unsqueeze(0)) self.dropout nn.Dropout(dropout) def forward(self, x): return self.dropout(x self.pe[:, :x.size(1)]) class EEGTransformer(nn.Module): def __init__(self, in_channels32, d_model64, nhead4, num_layers2, n_classes2, kernel16, stride8, dropout0.1): super().__init__() self.tokenizer CNNTokenizer(in_channels, d_model, kernel, stride) self.pos_enc PositionalEncoding(d_model) encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforward256, dropoutdropout, batch_firstTrue, activationgelu) self.encoder nn.TransformerEncoder(encoder_layer, num_layers) self.head nn.Linear(d_model, n_classes) def forward(self, x): tokens self.tokenizer(x) # (B, L, d_model) tokens self.pos_enc(tokens) hidden self.encoder(tokens) # (B, L, d_model) pooled hidden.mean(dim1) # 全局平均池化 return self.head(pooled)逻辑说明CNNTokenizer 里的第一个 Conv1d 用 kernel16、stride8把 128 个采样点切成 17 个 token紧跟着的 3×1 卷积进一步扩大感受野让每个 token 看到更大范围的波形。位置编码用的是 sin/cos 固定编码脑电没有词汇表可学习位置编码在小数据集上容易过拟合。TransformerEncoder 开了 batch_firstTrue张量形状始终是 (B, L, d_model)调试时不用来回 transpose。最后用 mean pooling 聚合 token脑电任务里它比单独取第一个 token 作为 CLS 更稳。PyTorch 基础框架里最常见的坑就是忘记 batch_first导致 Attention 输出 (L, B, d_model) 的形状下游全乱。这里全部按 batch_first 写如果你把这段代码挪到自己工程里先检查其它模块的输入形状约定别在维度上省事。3.3 关键参数怎么定d_model、nhead、kernel 的参考表参数起步值调整方向kernel16想要更长感受野就加大但别超过一个完整节律周期stride8越小 token 越多上下文更细腻但更慢d_model64样本少就 32/64样本多可上 128nhead4必须能整除 d_modelnum_layers2过拟合先减到 1再考虑加深dropout0.1训练集冲到 99% 时加到 0.3~0.5dim_feedforward256通常是 d_model 的 4 倍每个参数背后都有脑电解码的含义。kernel16 在 128 Hz 下对应约 125 ms 感受野大约是一个 alpha 波的周期卷积在这个尺度上更容易学到节律特征stride 决定相邻 token 的重叠程度stride8 保住了连续性也把序列长度压到 17d_model 是 token 的特征宽度脑电数据集通常只有几千到几万窗口64 已经够用上来就 256 会让小模型直接过拟合。nhead 如果设成 8d_model 必须改成能被 8 整除的数值比如 64 或 128。调参顺序按表格从上往下走先固定 d_model 和层数再动 kernel 和 stride不要一上来就三个参数一起改那样翻车了根本不知道是谁的问题。4. 训练与调参交叉熵之外脑电项目最该先调的是这三个超参4.1 训练循环模板早停、梯度裁剪和验证集要一起上训练代码不需要花哨但有三样东西建议从一开始就放在里面早停、梯度裁剪、保存最优权重。脑电小数据集上验证集准确率经常在第 30 个 epoch 触顶然后缓慢下降没有早停你最终保存的可能是最差的那份权重梯度裁剪则防止个别异常窗口把 loss 顶成一个巨大值后模型一去不回。import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset device torch.device(cuda if torch.cuda.is_available() else cpu) model EEGTransformer().to(device) optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) loss_fn nn.CrossEntropyLoss() train_ds TensorDataset(torch.from_numpy(X_train), torch.from_numpy(y_train)) train_dl DataLoader(train_ds, batch_size64, shuffleTrue) best_acc, best_state, patience, bad_epochs 0.0, None, 12, 0 for epoch in range(80): model.train() train_loss 0.0 for xb, yb in train_dl: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() loss loss_fn(model(xb), yb) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() train_loss loss.item() val_acc evaluate(model, X_val, y_val, device) if val_acc best_acc: best_acc, best_state val_acc, {k: v.cpu().clone() for k, v in model.state_dict().items()} bad_epochs 0 else: bad_epochs 1 if bad_epochs patience: break model.load_state_dict(best_state)逻辑说明每轮训练先按 batch 前向、算交叉熵、反向、裁剪梯度再更新。clip_grad_norm_ 的 max_norm1.0 是个保守值既能防梯度爆炸又不会明显拖慢收敛。早停看的是验证集准确率连续 12 个 epoch 没刷新就停然后把验证集上表现最好的权重恢复到模型里。这里的关键是不要去管「训练集准确率已经多高」脑电训练集一旦接近 100%说明模型已经在背窗口噪声了。4.2 学习率、batch size 和类别不均衡先把这三样调明白脑电二分类常见 55 / 45 的标签分布看起来不算失衡但侧面反映 valence 二分类这个任务本身的边界模糊。如果标签九一开那就必须处理不均衡。最简单有效的方式是给交叉熵加类别权重让少数类的 loss 放大比人为过采样安全因为过采样在窗口重叠严重的脑电数据里会加倍放大泄漏。counts np.bincount(y_train) weights torch.tensor([1.0 / max(c, 1) for c in counts], dtypetorch.float32) weights weights / weights.mean() loss_fn nn.CrossEntropyLoss(weightweights.to(device))学习率方面AdamW 配 1e-4 起步最稳比 1e-3 慢但不会一开始就震荡如果你的数据特别干净、窗口数上万可以把学习率提到 3e-4但不要直接上 1e-3。batch size 在 32 到 64 之间选显存允许就 64batch 太小梯度噪声大、脑电这种低信噪比信号会更难收敛。weight_decay 设置 1e-4 到 1e-5 即可太大模型学不动太小起不到正则作用。4.3 多指标评估准确率会骗人F1 和混淆矩阵才说明问题脑电分类不像图像分类那样类别边界清晰准确率 75% 看起来还行但拉开混淆矩阵一看可能全是在预测多数类。评估代码直接用 sklearn一句话出分类报告和混淆矩阵from sklearn.metrics import classification_report, confusion_matrix import numpy as np preds predict(model, X_test, device) # 返回类别索引 print(classification_report(y_test, preds, target_names[neg, pos])) cm confusion_matrix(y_test, preds) print(cm)二分类报告里重点看 macro F1 而不是 accuracy尤其是正负类比例不均衡的时候。写论文时报告测试集上的 macro F1 和混淆矩阵比单写准确率更能扛住评委提问。还要记录随机种子做了什么torch.manual_seed(42)、np.random.seed(42)都要固定否则每次跑出来的准确率差三五个点论文图没法画。5. 源码复现的五个坑从 loss 不降、显存爆掉到验证集「虚高」5.1 loss 不降反升准确率卡在随机水平现象是这个坑最隐蔽模型训练了十几个 epochloss 纹丝不动甚至偶尔冲到 10 以上准确率在 50% 附近晃动像是随机猜。原因一般是三层学习率太大、数据没归一化、标签错位。脑电数据没做 z-score 时幅值动辄几百微伏梯度计算量级混乱标签错位则是切窗或索引的时候窗口和 label 对不上模型学到的是随机映射。还有一个容易忽略的点全连接层输出维度是 2标签却是 0 和 1 以外的值CrossEntropyLoss 直接崩。解决方法是按顺序排查。先把学习率降到 1e-5 试跑 5 个 epoch确认 loss 能降再打印一个 batch 的输入均值和标准差确认归一化生效最后随机抽 20 个窗口人工看一眼标签和原始波形是否匹配。这三步下来大部分「不收敛」都会被揪出来。5.2 训练集 99%验证集 55%脑电切窗泄漏在搞鬼现象非常典型训练集准确率一路冲到 99%验证集却只在 55% 上下而且怎么增大模型、加 dropout 都治不好。原因几乎可以断定是随机切分窗口造成的泄漏。一个 trial 切出几十个窗口把它们随机分到训练集和验证集验证集里就会出现「训练集那个窗口的邻居」模型记住的不是脑电特征而是相邻窗口的连续性。解决方法是把 group 信息显式传进划分函数group 是 trial 索引或 subject 索引。调参阶段用 GroupKFold 交叉验证最终评估用留出法按 subject 划分。改完之后验证集准确率会明显下降那才是真实水平。记住一句话验证集分数「虚高」的时候先怀疑划分再怀疑模型。5.3 显存 OOM 或一个 epoch 要跑十几分钟序列太长不是显存不够现象是 batch size 调到 32 还是报 CUDA out of memory或训练速度慢到让人怀疑人生。原因要看两个地方。第一输入序列长度没被压缩比如把 7680 个采样点直接送进 Transformer注意力矩阵是 7680×7680显存扛不住很正常第二batch size 和 token 长度互相放大17 个 token 时 batch 64 毫无压力但如果 stride 改成 4token 变成 33 个显存需求接近翻倍。解决方法是先确认 tokenizer 输出长度打印一下tokens.shape如果 L 超过一两百就加大 stride 或换更大的 kernel。再不行就降 batch 并用梯度累积补足PyTorch 里每累积 4 个小 batch 做一次 optimizer.step()效果接近大 batch。最后看一眼nvidia-smi脑电小模型一般占用不到 2 GB如果占满说明模型里某个模块维度写大了。5.4 同一个代码跑三次准确率差五个点小样本加随机性现象是固定了随机种子每次跑出来的验证准确率还是有波动让人怀疑环境有问题。原因是 torch 的 CPU 端卷积和某些算子存在非确定性实现即便设了随机种子跨运行的浮点累加顺序也可能不同加上脑电数据本身噪声大、样本量小几个样本的差异就会让准确率动三五个点。解决方法是多做几个随机种子常见做法是 5 个或 10 个 seed 各跑一遍报告均值和标准差。写论文时「准确率 78.4 ± 1.2%」比「最高一次 82.3%」可信得多。另外注意固定种子时要同时固定random库和 numpy否则 DataLoader 的随机打乱和数据集划分每次都不一样。5.5 复现别人的代码曲线却完全对不上数据集版本和预处理没对齐现象是你下载了一份源码代码逻辑看着什么都对但训练曲线和作者贴出来的完全不是一回事。原因多半出在数据版本。DEAP 的 128 Hz 版本和 512 Hz 版本基线长度不同、通道顺序不同SEED 的标签是 -1/0/1 三分类不是软间隔打分还有人把滑动窗口起点设在了基线里导致模型学到的其实是采集设备的状态差。解决方法是把预处理写成独立的脚本输入是原始数据文件输出是统一的.npy缓存文件并在缓存文件旁边记录一份参数清单采样率、基线剔除的采样点、滤波范围、窗口长度、步长、归一化方式。以后不管是换模型还是换代码直接从缓存读省得每次都在各种.mat文件里猜来猜去。这一步算是我自己吃了大亏之后总结出来的也是给未来半个月后的自己留的后悔药。6. 从「跑通」到「能答辩」注意力可视化、单试次推理和一点小技巧6.1 把注意力权重变成答辩图TransformerEncoderLayer 输出的attn_output_weights可以直接拿来做可视化。在 forward 里把权重保存到一个列表训练结束后对多层、多头的注意力权重取平均得到一张 token × token 的二维图横纵轴都对应时间片段颜色深的位置说明模型认为那两个时间段有关联。答辩时指出模型在情绪任务里更关注某个通道区域对应的 token比空口说「Transformer 很强大」有说服力得多。6.2 单试次预测窗口投票比直接分类更稳实际推理时一个 trial 会切成十几个窗口别只拿第一个窗口的结果当答案。常见做法是对所有窗口的概率做平均再按平均概率决定类别或者直接多数表决。概率平均在二分类里更稳因为窗口置信度能被保留。这个细节讲出来评委就知道你不是只跑通了训练脚本。6.3 小技巧把结果沉淀成一条可复现的命令我会把整套流程收进一个入口脚本一个参数控制数据集路径一个参数控制划分方式一个参数控制随机种子config 保存成 JSON连同每个 epoch 的指标一起落盘。这样过了两个月重跑实验不再需要回忆「我当时是怎么办的」。脑电分类的玄学主要来自预处理和划分的不透明把这些固定下来剩下的调参才有意义。回头看我第一次跑这套系统最亏的不是模型写错而是验证集泄漏的分数让我高兴了一周后来按 subject 重测直接掉到不如 CNN baseline。从那以后我做任何脑电实验都先验证划分再验证模型。希望这个顺序也能帮到你。本文还有配套的精品资源点击获取

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号