首页 / 资讯中心 / 文章详情

运动想象脑电分类:CNN+Transformer的PyTorch实现与避坑指南

运动想象脑电分类:CNN+Transformer的PyTorch实现与避坑指南 ★ FEATURED ARTICLE
简介面向本科毕业设计场景该压缩包提供一套基于CNNTransformer的运动想象脑电信号分类完整实现。项目以PyTorch为框架利用CNN提取局部时空特征、Transformer捕捉全局依赖并引入Grad-CAM对脑电地形图进行可视化同时包含EEGNet、CSP等对比方法与t-SNE降维分析。包内共31个文件涵盖23个Python脚本模型定义、训练、评估、绘图、2个MATLAB预处理脚本、2个Excel权重/通道记录、1个已训练模型权重与1个NPY数据文件等整体仅16.75MB结构清晰便于二次开发。目前已有442人学习下载适合需要完成相关课题的本科生或入门脑机接口的研究者参考。1. 运动想象脑电分类难在哪为什么 CNNTransformer 组合成了标配而不是选修运动想象Motor Imagery脑电分类的毕业设计最直接的挫败感来自“公开数据集上跑出来的准确率还不如传统 CSP 特征”这种尴尬。脑电信号信噪比极低运动想象引起的节律变化主要落在 mu8–13 Hz和 beta13–30 Hz频带响应窗口只有几百毫秒到几秒。CNN 擅长在时间和通道两个维度上提取局部时空模式Transformer 则能用一个注意力矩阵把整个 trial 里相隔很远的时间片段重新关联起来。两者组合是目前做运动想象脑电信号分类最顺手、也最适合在毕设里做成“基线对比 消融实验”的框架。这篇笔记覆盖数据预处理、架构原理、PyTorch 实现、训练参数和踩坑排查给复现和改写成自己的方案留足细节。2. 预处理与数据集从 BCI 公开 GDF 文件到可训练张量2.1 BCI Competition IV 2a选它做基准的四个理由与数据规格做运动想象分类绕不开 BCI Competition IV Dataset 2a。它是这个领域事实上的标准基准9 名受试者、5 天内的两个会话、22 个 EEG 通道和 3 个 EOG 通道四类运动想象任务左手、右手、脚、舌头单次试验的想象窗口约 4 秒采样率 250 Hz。相比自己采集数据它最大的价值是给你一个可以横向对比的官方基线——传统方法用 CSP LDA 在这个数据集上的四分类准确率大约 60%–75%你的深度学习模型如果打不过这个数字先别怀疑模型去检查数据流。选择 2a 还有四个实操层面的理由。其一数据是公开的GDF 格式可以用 MNE 直接读不需要自己写解析器其二每个受试者训练会话有 288 个 trial四类样本均衡交叉验证好做其三官方明确区分了训练会话和评估会话可以直接按受试者内方案做训练/验证切分也可以玩跨会话迁移其四社区里已经积攒了大量复现结果你跑出一个数字之后能很快判断出自己的模型在正常范围还是异常偏高/偏低。需要注意2a 的脑电通道布局以中央区为主C3、C4、Cz 一带正好覆盖手部运动想象的皮层区域这也是预处理时最值得单独做可视化的通道组。2.2 预处理流水线带通滤波、去 EOG、切段与通道标准化拿到手的是原始 GDF 文件第一步不是急着建模型而是把数据切成“模型能直接吃”的张量。我习惯的预处理顺序是读取原始文件 → 剔除 EOG → 带通滤波 → 按事件标记切段 → 基线校正 → 通道标准化。带通滤波选 4–38 Hz既保留 mu 和 beta 两个运动想象核心节律又滤掉基线漂移和大部分肌电伪迹这对于后级卷积和 Transformer 都有明显帮助。import mne import numpy as np # 读取官方 GDF 文件A01T 是第 1 名受试者的训练会话 raw mne.io.read_raw_gdf(A01T.gdf, preloadTrue, verboseFalse) # 只保留 22 个 EEG 通道EOG 通道在预处理阶段先排除 raw.pick_types(eegTrue, eogFalse, verboseFalse) # 运动想象有用的频谱集中在 4-38 Hz保留 mu 和 beta 节律 raw.filter(4, 38, fir_designfirwin, verboseFalse) # 先打印事件码确认类别映射不同 mne 版本间映射名称可能有差异 events, event_id mne.events_from_annotations(raw, verboseFalse) print(event_id)这里解释一下三个关键操作。pick_types去掉 EOG 通道后剩下来的 22 路 EEG 信号里仍然可能包含眨眼和眼动分量但 4–38 Hz 的带通滤波已经能压制大部分低频漂移如果后续发现眨眼伪迹仍然严重影响单 trial 波形再叠加 ICA 去伪迹不要一开始就做 ICA费时且容易把有效脑电分量一起滤掉。filter用的 FIR 设计能在 4 Hz 和 38 Hz 边缘保持较陡的衰减避免过度削弱节律边界频段。events_from_annotations这一步务必先打印结果因为 2a 在自己定义事件码的时候有些版本会把类别编码成 1/2/3/4有些版本会额外附加几个 event 类型。切段这一步直接决定了 Transformer 能看到多长的上下文。我通常取tmin-0.5、tmax4.5即以运动想象指令出现的前 0.5 秒开始一直截到想象结束之后 0.5 秒250 Hz 下相当于 1250 个采样点。基线校正用baseline(-0.5, 0)把指令出现之前的信号均值归零消除不同试次之间缓慢的电位漂移。# 保留四类运动想象事件假设打印结果里类别码是 1/2/3/4 target_codes [1, 2, 3, 4] epochs mne.Epochs( raw, events, event_idevent_id, tmin-0.5, tmax4.5, baseline(-0.5, 0), preloadTrue, verboseFalse, ) epochs epochs[target_codes] # 数据形状(n_trials, 22通道, 1250个采样点) X epochs.get_data(copyTrue) # 触发 copy避免误改原始数据 y epochs.events[:, -1] - 1 # 标签从 1-4 映射到 0-3方便交叉熵epochs[target_codes]这一步很多人会漏直接导致后续类别数量与模型输出维度对不上。映射完成后标签为 0/1/2/3分别对应左手、右手、脚、舌头。get_data(copyTrue)是为了防止后续标准化或者增强操作时无意间改到epochs内部缓存的原始数据这是我在多次因为 in-place 修改导致基线对比串味之后养成的习惯。最后做通道标准化。最稳妥的方式是按通道、跨时间计算均值和标准差因为不同通道的幅度差异很大C3 通道的 mu 节律可能远比额叶通道强不标准化会让 Transformer 的注意力一开始就被大数值通道主导。# 按通道标准化shape 为 (n_trials, 22, 1250) mean X.mean(axis(0, 2), keepdimsTrue) # 每个通道跨时间求均值 std X.std(axis(0, 2), keepdimsTrue) X (X - mean) / (std 1e-6)这里的axis(0, 2)表示在 trial 维度和时间维度上同时求均值保留通道维度。加入1e-6是为了防止某个通道在滤波后近乎零方差的极端情况下出现除零错误。注意这个标准化是按受试者独立做的如果后续做跨受试者实验应该每个受试者分别计算统计量不能把所有人混在一起拟合。2.3 数据划分策略受试者内划分与跨受试者差异2a 数据集单个受试者只有 288 个 trial直接划分容易让验证集波动很大。我建议使用分层五折交叉验证每一折里四类样本比例保持一致报告时输出五折均值和标准差。如果追求更高稳定性可以按 Sessions 划分用第一个会话训练、第二个会话验证这种做法模拟了“同一个人不同天使用”的真实 BCI 场景但也更考验模型的泛化能力。from sklearn.model_selection import StratifiedKFold skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) for train_idx, val_idx in skf.split(X, y): X_train, X_val X[train_idx], X[val_idx] y_train, y_val y[train_idx], y[val_idx] break # 示意先拿一折出来调试模型这里random_state42必须固定否则每次跑交叉验证的划分不同结果没法跟自己的工作对比。另有两点经验第一不要用随机划分的 test split 来反复调参否则所谓“验证准确率”其实已经被你无意中背下来了第二跨受试者迁移时模型性能通常会比受试者内掉 10–20%这不是 bug而是脑电个体差异的本质论文里如实报告即可不必硬追。3. 架构原理CNN 先做时空局部特征Transformer 再建模长程依赖3.1 时域卷积核 size 怎么定从采样率反推感受野设计 CNN 部分时第一个要定的参数是时间卷积核大小。采样率 250 Hz 意味着一个采样点代表 4 毫秒卷积核大小为 32 时感受野是 128 毫秒大约覆盖 8 Hz 信号的一个完整周期核大小 64 时感受野为 256 毫秒能覆盖 4 Hz 信号的一个周期同时包含两个 mu 节律周期。一般经验是时间卷积核至少要让模型“看见”目标频带的一个完整周期所以 4–38 Hz 频带对应核大小在 16–64 之间都有道理。我自己的选择逻辑很简单先问自己希望第一层卷积捕捉多长的节律片段。如果目标是提取 mu 节律的包络变化256 毫秒的感受野核大小 64更稳如果希望保留更精细的瞬态特征128 毫秒核大小 32能给出更高时间分辨率。注意卷积核越大参数量越大在 288 个 trial 的小样本上过拟合风险也越高。我的默认配置是核大小 32后续用池化层压缩时间维让 Transformer 在一百多个时间 token 上做交互。3.2 spatial depthwise 技巧通道间组合与参数量控制时间卷积之后接一个kernel_size1的一维卷积作用是对 22 个 EEG 通道做线性组合这也是传统空间滤波CSP在深度网络里的自然对应物。EEGNet 的做法把这一步做成 depthwise 卷积先对每个通道分别做时间卷积再在通道维度上用kernel_size1做线性混合。这样参数量远小于全连接的空间映射在样本量小的脑电数据上不容易过拟合。这里有个容易理解错误的点通道维度的“空间卷积”不是图像里的二维卷积而是一维卷积作用于通道方向。输入 shp 是(B, T)经过时间卷积变成(B, F, T)再把通道方向作为一维空间做kernel_size1卷积输出(B, F, T)。我常用的配置是时间卷积输出 32 个特征图空间卷积保持 32 个特征图并接 BatchNorm 与 ELU 激活。BatchNorm 对脑电尤其重要因为不同受试者的信号幅度差异大BatchNorm 能在训练中稳定地做二次归一化。3.3 Transformer 编码器补上了什么位置编码、注意力头数与网络深度CNN 输出的是一个特征序列序列长度由池化后的时间点数决定。在我的实现里1250 个采样点经过 4 倍池化变成约 312 个时间 token每个 token 的特征维度是 32。此刻 Transformer 编码器对这些 token 做自注意力相当于让模型自由决定“第 50 个时间片”和“第 200 个时间片”之间的关联权重。运动想象的 ERD 现象通常出现在提示后 0.5–2 秒而试次后期可能有 beta 回升这些跨时间的依赖正是 Transformer 比纯 CNN 强的地方。位置编码在这个场景里是必须加的。CNN 是卷积滑窗天然能感知局部顺序Transformer 的注意力对 token 位置不敏感如果不加位置编码模型无法区分“0.5 秒处出现的 ERD”和“3.5 秒处出现的 beta 回升”。我使用的是经典正弦位置编码不参与训练由register_buffer保存避免把位置信息当可学习参数来增加过拟合风险。注意力头数、层数、特征维度三者需要联动设置。d_model32时注意力头数取 4 意味着每个头只关注 8 维特征这在小数据集上够用但如果把d_model提到 128头数也可以继续加到 8。层数我一般控制在 2–3 层因为 288 个 trial 不足以训练更深的 Transformer。如果发现验证集准确性随着层数增加反而下降不要死磕层数优先回去看 dropot 和位置编码是否生效。4. 核心代码落地PyTorch 实现 CNNTransformer 分类器4.1 模型定义CNN 特征提取器 正弦位置编码 Transformer 编码器下面这段代码是一个完整可运行的模型定义输入是(batch, 22, 1250)的标准化脑电张量输出是四类运动想象 logits。核心思路CNN 把原始时间序列压缩成特征序列Transformer 在特征序列上建模长程依赖最后用全局平均池化接一个浅层 MLP 输出分类结果。import torch import torch.nn as nn class CNNFeatureExtractor(nn.Module): 把多通道 EEG 原始信号压缩成时间特征序列 def __init__(self, n_channels22, dropout0.3): super().__init__() self.temporal nn.Sequential( # 时域卷积核大小 32在 250Hz 下覆盖 128ms 感受野 nn.Conv1d(n_channels, 32, kernel_size32, stride1, padding16), nn.BatchNorm1d(32), nn.ELU(), nn.Dropout(dropout) ) self.spatial nn.Sequential( # 空间卷积kernel_size1在通道间做线性组合 nn.Conv1d(32, 32, kernel_size1), nn.BatchNorm1d(32), nn.ELU(), nn.Dropout(dropout) ) self.pool nn.AvgPool1d(kernel_size4, stride4) def forward(self, x): # x: (B, 22, T) x self.temporal(x) # (B, 32, T) x self.spatial(x) # (B, 32, T) x self.pool(x) # (B, 32, T//4) x x.transpose(1, 2) # (B, T//4, 32) return x class PositionalEncoding(nn.Module): 标准正弦位置编码Transformer 对顺序不敏感必须显式加 def __init__(self, d_model, max_len512): super().__init__() pe torch.zeros(max_len, d_model) pos torch.arange(0, max_len, dtypetorch.float).unsqueeze(1) div_term torch.exp( torch.arange(0, d_model, 2).float() * (-(torch.log(torch.tensor(10000.0)) / d_model)) ) pe[:, 0::2] torch.sin(pos * div_term) pe[:, 1::2] torch.cos(pos * div_term) self.register_buffer(pe, pe.unsqueeze(0)) def forward(self, x): # x: (B, L, d_model)把位置编码广播到 batch 维度 return x self.pe[:, :x.size(1)] class CNNDTransformer(nn.Module): def __init__(self, n_channels22, n_classes4, d_model32, nhead4, num_layers2, dropout0.3): super().__init__() self.cnn CNNFeatureExtractor(n_channels, dropout) self.pos_enc PositionalEncoding(d_model, max_len512) encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforward256, dropoutdropout, batch_firstTrue ) self.transformer nn.TransformerEncoder( encoder_layer, num_layersnum_layers ) self.classifier nn.Sequential( nn.Linear(d_model, 64), nn.ReLU(), nn.Dropout(dropout), nn.Linear(64, n_classes) ) def forward(self, x): x self.cnn(x) # (B, L, d_model) x self.pos_enc(x) # (B, L, d_model) x self.transformer(x) # (B, L, d_model) x x.mean(dim1) # 全局平均池化把 L 维压缩掉 x self.classifier(x) return x这段代码里有几个参数需要重点说明。第一padding16用来保持卷积后时间长度不变计算公式是(1250 - 32 2*16) / 1 1 1250这能让后续池化后的 token 数量完全确定。第二AvgPool1d(kernel_size4, stride4)把 1250 个点压成 312 个 token长度为 312 的序列在两层 Transformer 下计算量约为 31 万次注意力交互对显存非常友好。第三dim_feedforward256是 Transformer 前馈层的隐藏维度通常取d_model的 4–8 倍这里 32 到 256 是 8 倍在小模型里加强非线性表达能力。4.2 训练与验证循环损失、批处理、早停与学习率调度模型定义好后训练循环的细节决定能不能收敛。损失函数用CrossEntropyLoss优化器用 Adam初始学习率1e-3并配权重衰减1e-4这在 288 个 trial 的小数据集上是比较稳的组合。学习率调度我用余弦退火总 epoch 数 50让学习率从1e-3平滑降到接近 0降低后期在验证集上的震荡。import torch from torch.utils.data import DataLoader, TensorDataset device torch.device(cuda if torch.cuda.is_available() else cpu) model CNNDTransformer(n_channels22, n_classes4).to(device) criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max50) def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, correct, total 0.0, 0, 0 for xb, yb in loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() out model(xb) loss criterion(out, yb) loss.backward() optimizer.step() total_loss loss.item() * len(xb) correct (out.argmax(dim1) yb).sum().item() total len(yb) return total_loss / total, correct / total def evaluate(model, loader, criterion, device): model.eval() total_loss, correct, total 0.0, 0, 0 with torch.no_grad(): for xb, yb in loader: xb, yb xb.to(device), yb.to(device) out model(xb) loss criterion(out, yb) total_loss loss.item() * len(xb) correct (out.argmax(dim1) yb).sum().item() total len(yb) return total_loss / total, correct / total把数据封装成 DataLoader 时batch_size我取 16 或 32。batch 太小会导致 BatchNorm 统计量抖动厉害太大又会在小数据集上提前过拟合。16 是一个均衡值尤其是在 288 个 trial 一个 epoch 只有 18 个 batch 的情况下BatchNorm 的均值方差估计不会太噪声。推理时务必用model.eval()切换到评估模式否则 Dropout 和 BatchNorm 的行为会干扰测试结果这是新手最常犯的错误。早停的判断依据是验证集准确率连续 10 个 epoch 不上升就停止并保存最佳模型参数。我的习惯是训练过程中记录每个 epoch 的 val_acc如果val_acc在上升后连续 10 个 epoch 不再刷新最高值直接中断训练。torch.save(model.state_dict(), best_model.pt)4.3 评估代码混淆矩阵、Kappa 系数与逐受试者结果四分类任务只看准确率不够。运动想象分类里手部类别左手 vs 右手之间最容易混淆而脚和舌头通常比较容易分开。混淆矩阵能直观告诉你模型到底在哪两类之间犹豫Kappa 系数则能校正随机一致性带来的虚高。from sklearn.metrics import confusion_matrix, cohen_kappa_score preds, gts [], [] model.eval() with torch.no_grad(): for xb, yb in val_loader: out model(xb.to(device)) preds.extend(out.argmax(dim1).cpu().numpy()) gts.extend(yb.numpy()) cm confusion_matrix(gts, preds) kappa cohen_kappa_score(gts, preds, weightslinear) print(cm) print(fKappa: {kappa:.3f})混淆矩阵逐类看如果cm[0][1]和cm[1][0]明显偏高说明左手/右手分类不稳可以考虑在数据预处理阶段针对 C3/C4 通道做更针对性的特征增强。Kappa 系数在论文里通常和准确率一起报因为准确率在类别均衡的数据上可能虚高。整个训练评估流程跑完后把所有受试者的结果汇总按受试者算平均准确率和 Kappa 标准差这才是一份能写进毕业论文的结果表。5. 避坑与常见问题五个让模型翻车的细节5.1 训练 loss 在降验证准确率却卡在 25%四分类随机水平现象训练集 acc 能到 90% 以上但验证集一直在 25% 附近震荡好像模型“没学会”。原因最常见的是标签错位。BCI IV 2a 的事件码在不同 MNE 版本里映射名称不一样如果你直接拿网上某篇博客的event_id字典去切段很可能把类别标签全偏移了一位。另一个可能是 epoch 窗口没切对比如tmax取到了指令出现之前的休息段导致模型学到的全是基线噪声。解决在预处理阶段先打印events_from_annotations的结果逐个确认事件码含义。另外切段后把四类样本的平均波形图分别画出来目测 C3 通道在右手想象时是否出现事件相关去同步ERDC4 通道在左手想象时是否出现类似现象。如果波形图上两类差异不明确大概率是事件时间轴对齐出了问题而不是模型的问题。5.2 验证准确率在训练中期掉下来之后恢复不了现象前 20 个 epoch 训练平稳第 30 个 epoch 之后 val_acc 突然从 80% 掉到 60%之后再也回不去。原因学习率过高且没有调度。Adam 在小数据集上有时会在 loss 曲面震荡余弦退火能在后期把学习率压低减少验证集上的波动但如果你从头到尾用固定1e-3后期梯度更新步长仍然偏大一个不利 batch 就能把模型推离谷底。解决给训练加余弦退火调度甚至再加 5 个 epoch 的线性 warmup让模型先在小学习率下稳定探索再进入正常学习率区间。若问题依旧把weight_decay从 0 改成1e-4强制约束参数范数能缓解小样本下的参数震荡。5.3 同一份代码换台机器重跑结果差了两个点现象在自己的电脑上跑 80% 准确率放到实验室的 GPU 服务器上重跑同样代码变成了 77%怀疑数据或者模型被改过。原因PyTorch 默认的 cuDNN 卷积算法是“贪婪选择”不同 GPU 型号甚至同一型号不同驱动版本下卷积实现可能不同导致浮点计算顺序不是完全确定性的。如果没有固定随机种子数据加载顺序和 Dropout 掩码的变化会让结果波动更大。解决在训练脚本最开始加统一种子设置并把cudnn.deterministic和cudnn.benchmark配好如下方代码所示。def set_seed(seed42): import random random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False set_seed(42)注意cudnn.benchmark关闭后可能会让训练速度略降但换来的是跨机器的可复现性。如果论文需要报告多次重复实验的均值和标准差这个设置是底线。5.4 Batch size 调大后显存暴涨代码直接 OOM现象batch_size16训练正常改成 32 或 64 后 GPU 直接 Out of Memory。原因Transformer 注意力矩阵占用的显存是B × L × L × nhead的量级。如果预处理时没有做时间降采样直接把 1250 个采样点全部序列化序列长度 1250、batch 32注意力矩阵大小是32 × 1250 × 1250 × 4接近数亿个浮点数值不爆显存才怪。解决先确定序列长度再设计模型。常见做法是把 1250 点通过 CNN 池化降到 312 个 token或者直接在原始序列上做分段每 64 个采样点作为一个 patchpatch 之间 stride 32序列长度降到约 40。对脑电数据来说更激进的降采样不会丢太多信息因为 4–38 Hz 的频带信息在 125 Hz 采样率下仍然完全保留。5.5 复现论文时测试准确率比论文低 5–8%找不到原因现象按论文结构复现出的代码准确率始终低一截怎么调都差一口气。原因很可能是预处理细节不同。论文用的是完整 4 秒窗口你截了 2 秒论文做了 4–40 Hz 滤波你用了 8–30 Hz更常见的是论文用了重叠滑动窗口切段做数据增强把 288 个 trial 扩展成了上千个训练样本而你还在用原始 trial 数在训练。解决复现时先把对方论文的预处理参数列成一个表格逐项对齐不要贸然改动任何一项。如果对方用了重叠切段先把切段逻辑实现出来试一轮通常准确率能立刻提升几个点。我见过太多“模型创新”最后发现差异全在数据增强上的案例所以在对比实验里一定要把这部分差异写清楚否则你的模型提升很可能只是吃到了更长窗口的红利。6. 验证与收尾技巧消融实验、注意力可视化和毕设报告怎么呈现6.1 消融实验证明 CNN 和 Transformer 都缺一不可如果毕设题目是“基于 CNNTransformer 的运动想象分类”那么必须做带消融实验的对比表。三个最小变体建议是去掉 Transformer、把 CNN 换成线性投影、以及完整模型。去掉 Transformer 的做法是把Mean池化后的特征直接接到分类头上把 CNN 换成线性投影的做法是直接对原始 1250 点做展平降维代价大但能说明“CNN 结构确实更适配 EEG 局部特征”。在 BCI IV 2a 上常见结果是完整模型比纯 CNN 高 2–5 个百分点但不同受试者差异极大报告时按受试者拆开列不要只看均值。6.2 注意力可视化验证模型确实在关注运动想象窗口Transformer 虽然经常被当黑匣子但注意力权重可以可视化也能帮你判断模型有没有“学到点子上”。给TransformerEncoderLayer注册一个 forward hook取出self_attn权重矩阵计算所有头在 312 个 token 上的平均注意力分布然后把注意力热点叠加到时间轴上。如果看到峰值集中在运动想象指令出现后的 0.5–2 秒之间说明模型在利用真正的 ERD 特征如果注意力分布平坦如散沙则大概率模型是在靠通道间的静态差异分类。可视化代码不需要很复杂核心是 hook 里保存权重矩阵attn_cache {} def hook_fn(module, input, output): attn_cache[weights] module.self_attn.attn_weight # (B, heads, L, L) encoder_layer.register_forward_hook(hook_fn)实际命名取决于 PyTorch 版本内部 API但思路一致。这一步在毕业论文里非常加分评审老师一眼就能看出你不是把模型当黑箱在用。6.3 毕设报告里怎么组织基线对比和可复现脚本报告里建议用一张表格行是模型CSPLDA、EEGNet、DeepConvNet、你的模型列是受试者 A01–A09 的准确率、平均值、Kappa 系数。表格下方补一句说明所有模型共用同一套预处理流程和交叉验证划分确保对比公平。提交的代码仓库里除了训练脚本外建议包含一个固定随机种子的说明、一个requirements.txt以及一份列明关键参数的 README包括滤波频带、epoch 窗口、采样点数、batch size、初始学习率、epoch 数、注意力头数和 Transformer 层数。这些参数不是摆设其他人能否复现你的结果全看它。我最惨痛的一次经验是把 4 秒窗口截成 1 秒去跑实验第一版结果全黄改回完整窗口后同一套代码直接提升了 4 个百分点从此之后我养成了一个习惯任何预处理改动前先跑一遍官方基线把数字对齐再做模型创新否则出了问题根本不知道是哪一层在帮倒忙。希望这份流程能帮你把从头复现到写好毕业论文的时间压缩到两周内。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站