恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
EEGNET脑电分类实战:从信号预处理到模型训练与调参
首页
资讯中心
/
EEGNET脑电分类实战:从信号预处理到模型训练与调参
EEGNET脑电分类实战:从信号预处理到模型训练与调参
发布时间:2026/10/2 18:00:45
简介这份资源面向脑电信号处理与深度学习入门者提供EEGNET网络的完整Python实现用于脑电分类任务。EEGNET借鉴CNN与CRNN思想由空间卷积层和时序卷积层组成能同时捕捉EEG数据的空间与时间依赖特征是生物信号分析中处理复杂非线性数据的实用模型。压缩包内共1个py文件大小约2KB核心脚本涵盖数据预处理、模型构建、训练优化、验证评估、超参数调优及推理应用等环节并涉及小波变换、短时傅里叶变换、功率谱分析等时频特征提取方法以及PCA、ICA等降维思路。已有1876人学习下载适合希望快速理解EEGNET结构并动手实践脑电分类的读者参考可据此搭建实验流程、复现训练与评估步骤并在此基础上迁移到其他生物信号分析场景。1. EEGNET网络实现脑电分类从原始信号到可复现的最小闭环脑电分类这件事很多人第一次做都会卡在同一个地方数据读进来了模型也搭起来了但训练出来的准确率在 50% 上下晃跟掷硬币差不多。EEGNET 这个网络结构之所以在脑电分类圈子里被反复提起恰恰是因为它用极少的参数量在运动想象、P300、SSVEP 这几类典型任务上都能跑出一个能看的结果。它不是什么新架构本质是一个专门为脑电信号设计的紧凑型卷积神经网络把通道维度和时间维度的卷积拆开做再配合深度可分离卷积压缩参数量。如果你手头有 OpenBCI、BrainProducts 或者公开数据集比如 BCI Competition IV 2a的脑电数据想用一个不折腾、能复现的深度学习方案做分类EEGNET 是一个很合适的起点。它不需要 GPU 集群单卡甚至 CPU 都能跑通代码量也不大。接下来我会按「信号怎么进网络 → 网络怎么搭 → 训练怎么调 → 坑在哪」的顺序把这条链路拆开讲清楚。信号处理和深度学习在这里不是两个割裂的环节而是一条流水线前面滤波没做对后面网络再深也白搭。2. 脑电信号进网络之前预处理管线的搭建与参数选择2.1 为什么原始脑电不能直接喂给 EEGNET脑电信号是微伏级别的电压波动采集时混着工频干扰、眼电、肌电、电极接触噪声。直接把原始时序丢进卷积网络模型大概率会去学那些跟任务无关的伪迹。常见做法是先带通滤波到 0.5–40 Hz 或 4–38 Hz运动想象任务常用 8–30 Hz再做通道级标准化。EEGNET 原文里用的是 2–32 Hz 带通加指数移动标准化这个配置在多数运动想象数据集上表现稳定。滤波器的选择上FIR 和 IIR 各有拥趸。IIR 阶数低、计算快但相位非线性可能让波形发生畸变FIR 可以做到线性相位但阶数高、延迟大。我一般用 MNE 的filter函数做零相位 FIR 滤波虽然慢一点但省心。下面这段代码是一个可复用的预处理函数import numpy as np from scipy.signal import butter, filtfilt def bandpass_eeg(data, fs, low0.5, high40.0, order4): data: shape (n_channels, n_times) 或 (n_epochs, n_channels, n_times) fs: 采样率 返回同形状的滤波后数据 nyq fs / 2.0 b, a butter(order, [low / nyq, high / nyq], btypeband) # filtfilt 做零相位滤波避免波形时移 return filtfilt(b, a, data, axis-1) def normalize_per_channel(epochs): epochs: shape (n_epochs, n_channels, n_times) 对每个 epoch 的每个通道做零均值单位方差 mean epochs.mean(axis-1, keepdimsTrue) std epochs.std(axis-1, keepdimsTrue) 1e-8 return (epochs - mean) / stdbutter的order4是一个折中值阶数太高容易在通带边缘产生振铃阶数太低则阻带衰减不够。filtfilt做的是前向-反向两次滤波等效阶数翻倍但相位为零适合离线处理。标准化按通道独立做是因为不同电极的阻抗和增益差异可能很大不归一化的话网络会偏向数值大的通道。2.2 数据分段与标签对齐的实操细节脑电分类任务里epoch 的切分方式直接影响样本量和类别平衡。运动想象通常取 cue 后 0.5–2.5 秒这一段P300 取刺激后 0.3–0.8 秒。切早了包含视觉诱发电位切晚了信号已经衰减。下面是一个按事件切分的示例def make_epochs(raw_data, events, fs, tmin, tmax, baselineNone): raw_data: (n_channels, n_times) events: list of (sample_index, label) tmin/tmax: 相对于事件起点的秒数 start_offset int(tmin * fs) end_offset int(tmax * fs) epochs, labels [], [] for onset, label in events: s onset start_offset e onset end_offset if s 0 or e raw_data.shape[1]: continue # 边界样本丢弃 seg raw_data[:, s:e].copy() if baseline is not None: b_start int(baseline[0] * fs) b_end int(baseline[1] * fs) seg seg - seg[:, b_start:b_end].mean(axis1, keepdimsTrue) epochs.append(seg) labels.append(label) return np.stack(epochs), np.array(labels)这里有两个容易翻车的点。一是边界样本的处理如果事件靠近记录末尾切出来的段长度不够直接continue丢掉比补零更安全补零会引入虚假的静息段。二是 baseline 校正用 cue 前 0.5 秒的均值做基线能去掉慢漂移但如果 baseline 窗口里本身有噪声反而会引入偏差所以 baseline 区间要选在刺激之前、相对干净的一段。2.3 通道选择与重参考对分类的影响不是所有电极都对任务有贡献。运动想象主要看 C3、Cz、C4 及其周围P300 看 Pz、Fz、Cz。全通道输入不是不行但会增加噪声通道的干扰。常见做法有两种一是按先验知识手动选 8–16 个通道二是用 CSP共空间模式做通道加权。EEGNET 本身有通道维度的卷积理论上能学到通道权重但前提是数据量够。小样本情况下手动选通道更稳。重参考方面常见的有 CAR共同平均参考和 Laplacian。CAR 简单把所有通道减去平均能抑制全局噪声但也会削弱局部信号。Laplacian 用邻近电极做局部平均空间分辨率更高但对电极布局有要求。我一般先用 CAR如果分类效果不理想再试 Laplacian。这一步没有绝对最优跟数据集和任务相关。3. EEGNET 网络结构拆解从论文到可运行代码3.1 两个关键模块时间卷积与深度可分离卷积EEGNET 的结构可以概括为三个块。第一块做时间维度的卷积用一个(1, kernel_length)的卷积核提取频率特征等价于对每个通道做带通滤波。第二块做深度卷积用(n_channels, 1)的卷积核在每个时间点上跨通道融合这一步学的是空间滤波。第三块用可分离卷积先对每个特征图做时间卷积再做 1x1 的跨特征图混合压缩参数量的同时保留表达能力。这个设计的核心思想是脑电信号的空间和时间特征可以解耦。传统 CNN 用(n_channels, kernel_length)的二维卷积核参数量是n_channels × kernel_length × n_filters而 EEGNET 把这一步拆成深度卷积加可分离卷积参数量降到n_channels × depth_multiplier n_filters × kernel_length的量级。对于通道数 64、时间点 128 的输入参数量差距能到十倍以上。3.2 用 PyTorch 搭一个最小可用的 EEGNET下面是一个可以直接跑的 EEGNET 实现输入形状是(batch, 1, n_channels, n_times)import torch import torch.nn as nn class EEGNet(nn.Module): def __init__(self, n_channels64, n_times128, n_classes4, F18, D2, F216, kernel_length64, dropout0.5): super().__init__() # Block 1: 时间卷积 深度卷积 self.block1 nn.Sequential( nn.Conv2d(1, F1, (1, kernel_length), padding(0, kernel_length // 2), biasFalse), nn.BatchNorm2d(F1), # 深度卷积每个通道独立做空间滤波 nn.Conv2d(F1, F1 * D, (n_channels, 1), groupsF1, biasFalse), nn.BatchNorm2d(F1 * D), nn.ELU(), nn.AvgPool2d((1, 4)), nn.Dropout(dropout) ) # Block 2: 可分离卷积 self.block2 nn.Sequential( nn.Conv2d(F1 * D, F1 * D, (1, 16), padding(0, 8), groupsF1 * D, biasFalse), nn.Conv2d(F1 * D, F2, (1, 1), biasFalse), nn.BatchNorm2d(F2), nn.ELU(), nn.AvgPool2d((1, 8)), nn.Dropout(dropout) ) # 分类头 self.classifier nn.Linear(F2 * (n_times // 32), n_classes) def forward(self, x): x self.block1(x) x self.block2(x) x x.flatten(start_dim1) return self.classifier(x)F18是时间卷积的输出特征图数D2是深度乘数F216是可分离卷积的输出通道数。这三个参数是 EEGNET 原文的默认值在多数数据集上不需要大改。kernel_length64对应采样率 128 Hz 下的 0.5 秒时间窗如果采样率是 250 Hz这个值要按比例调整到 125 左右。padding保持时间维度不变AvgPool2d逐步降采样最后flatten后的维度是F2 × (n_times // 32)这个数要跟Linear的输入对上否则会报维度错误。3.3 参数量与感受野的权衡EEGNET 的参数量通常在 2k–10k 之间具体取决于F1、D、F2和kernel_length。参数量小是优点也是限制小样本下不容易过拟合但如果任务复杂、类别多表达能力可能不够。我试过在 4 类运动想象上把F1从 8 加到 16F2从 16 加到 32参数量翻了三倍准确率只涨了不到 2 个百分点但训练时间明显变长。所以除非数据量很大否则不建议盲目加宽。感受野方面第一层时间卷积的kernel_length64覆盖 0.5 秒第二层可分离卷积的kernel_length16覆盖 0.125 秒。这个组合对运动想象和 P300 都够用但如果你做的是高频 SSVEP可能需要缩短第一层卷积核让网络更关注快速振荡。感受野不是越大越好脑电信号的有效信息往往在特定时间尺度上卷积核太长反而会平滑掉细节。4. 训练配置与调参学习率、批大小和正则化的实际取值4.1 优化器与学习率调度EEGNET 原文用的是 Adam学习率 0.001没有用学习率衰减。我在实际训练中发现加一个余弦退火或者ReduceLROnPlateau能让后期收敛更稳。下面是一个训练循环的骨架from torch.optim import Adam from torch.optim.lr_scheduler import ReduceLROnPlateau model EEGNet(n_channels64, n_times128, n_classes4) optimizer Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler ReduceLROnPlateau(optimizer, modemin, factor0.5, patience10) criterion nn.CrossEntropyLoss() for epoch in range(200): model.train() for xb, yb in train_loader: optimizer.zero_grad() loss criterion(model(xb), yb) loss.backward() # 梯度裁剪防止深度卷积层梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() # 验证 model.eval() val_loss 0.0 with torch.no_grad(): for xb, yb in val_loader: val_loss criterion(model(xb), yb).item() scheduler.step(val_loss)weight_decay1e-4是 L2 正则配合 dropout 一起用。梯度裁剪的max_norm1.0在深度卷积层上很有必要因为groupsF1的卷积反向传播时梯度容易累积。ReduceLROnPlateau的patience10意味着验证损失 10 个 epoch 不降就砍半学习率这个值可以根据数据集大小调小数据集可以设小一点比如 5。4.2 批大小与样本量的关系批大小直接影响梯度估计的噪声。EEGNET 参数量小批大小设 16–64 都行。如果样本量只有几百批大小设 16 或 32让每个 epoch 有足够多的更新次数。如果样本量上万可以设 64 或 128训练更快。注意批大小和学习率要联动批大小翻倍学习率通常也要适当增大否则收敛变慢。类别不平衡是脑电分类的常见问题。运动想象里左手和右手样本数可能差一倍。处理方式有两种一是用WeightedRandomSampler过采样少数类二是在损失函数里加类别权重。我一般先用采样器因为它不改变损失函数的形状调起来更直观。4.3 早停与模型选择的判断标准早停的触发条件不能只看验证集准确率因为准确率在类别不平衡时会失真。更稳的做法是看验证集的损失或者平衡准确率balanced accuracy。如果验证损失连续 20 个 epoch 不降就停。保存模型时保存验证损失最低的那个 checkpoint而不是最后一个 epoch 的。下面是一个简单的早停实现class EarlyStopping: def __init__(self, patience20, min_delta1e-4): self.patience patience self.min_delta min_delta self.best_loss float(inf) self.counter 0 self.best_state None def step(self, val_loss, model): if val_loss self.best_loss - self.min_delta: self.best_loss val_loss self.counter 0 self.best_state {k: v.cpu().clone() for k, v in model.state_dict().items()} else: self.counter 1 return self.counter self.patiencemin_delta1e-4是为了避免验证损失在极小范围内波动时误判为「不降」。best_state保存的是 CPU 上的副本避免 GPU 显存被占满。这个早停逻辑配合ReduceLROnPlateau一起用基本能覆盖多数训练场景。5. 避坑与排查脑电分类训练中最容易翻车的五个地方5.1 准确率卡在随机水平损失不降现象训练集和验证集的准确率都在 25% 左右4 分类损失从第一个 epoch 开始就不怎么动。原因通常是标签和样本没对齐比如 events 里的 sample_index 是相对于原始记录的但 raw_data 已经做过裁剪或重采样索引偏移了。解决方法是打印几个样本的标签和对应的波形人工确认一下。另一个可能是输入数据的形状不对比如把(n_channels, n_times)直接喂给了期望(1, n_channels, n_times)的网络PyTorch 会广播成错误的结果但不报错。5.2 验证集准确率远高于训练集现象训练集准确率 60%验证集 85%。这听起来是好事但通常是数据泄漏。检查一下切分数据集时有没有把同一个 trial 的相邻 epoch 分到训练集和验证集两边。脑电信号的相邻时间段高度相关如果随机按 epoch 切分验证集里的样本可能跟训练集里的几乎一样。正确做法是按 trial 或按 session 切分确保验证集的 trial 在训练集中没出现过。5.3 训练后期损失突然变成 NaN现象前几十个 epoch 正常然后损失突然变成 NaN。原因可能是学习率太大导致梯度爆炸或者filtfilt滤波后的数据里有极端值。先检查输入数据的最大值和最小值如果超过 ±1000 微伏说明有伪迹没处理干净。然后在训练循环里加梯度裁剪把max_norm设成 1.0 或 0.5。如果还不行把学习率降到 1e-4 再试。5.4 不同随机种子下结果波动超过 10%现象换个随机种子准确率从 75% 掉到 62%。这是小样本脑电分类的常态。解决方式不是去调网络而是做交叉验证。用 5 折或 10 折交叉验证报告平均准确率和标准差。如果标准差超过 5 个百分点说明模型不稳定可能需要增加数据量或者简化网络。另外固定 PyTorch 的随机种子、NumPy 的随机种子和 CUDA 的随机种子能减少一部分波动但不能完全消除。5.5 推理时单样本预测结果与批量预测不一致现象把单个样本喂给模型输出跟批量预测的结果对不上。这通常是 BatchNorm 在作怪。训练模式下 BatchNorm 用当前批的均值和方差推理模式下用滑动平均。如果模型没调eval()单样本推理时 BatchNorm 会用样本自己的统计量导致输出偏移。解决方法是推理前务必调model.eval()并且用torch.no_grad()包住前向传播。6. 把 EEGNET 推到更高精度迁移学习与集成策略的实操EEGNET 在单数据集上做到 70%–80% 不算难但想再往上走单靠调参空间有限。我试过两个方向效果比较实在。第一个是跨被试迁移先在多个被试的数据上预训练再在目标被试的小样本上微调。具体做法是把预训练模型的分类头换成目标被试的类别数冻结前两个 block只训练分类头和最后一个 block。微调时学习率设成预训练的十分之一epoch 控制在 50 以内避免过拟合。第二个是集成。EEGNET 的随机性主要来自权重初始化和 dropout训练 5–10 个不同种子的模型推理时对 softmax 输出取平均通常能涨 2–4 个百分点。如果嫌训练多个模型太慢可以用 snapshot ensemble在一个训练循环里用余弦退火让学习率周期性回升在每个周期的最低点保存模型最后集成这些 snapshot。这样只训练一次就能拿到多个模型。验证集成效果时不要只看准确率还要看混淆矩阵。脑电分类里某些类别的样本容易被混在一起比如左手和双脚的运动想象。如果集成后混淆矩阵的对角线更集中说明集成确实在起作用。最后分享一个我踩过的坑微调时如果解冻太多层目标被试的少量数据会把预训练学到的通用特征覆盖掉验证损失会在几个 epoch 内快速上升。所以微调的第一原则是「少动」先只调分类头不够再逐层解冻。希望帮到你。本文还有配套的精品资源点击获取