1. 为什么EEG分类值得单独写一篇体系化梳理
脑电信号分类这个方向,我前前后后跟了快六年,从最早拿SVM加手工特征跑二分类,到后来用CNN做端到端,再到现在折腾图神经网络建模电极间拓扑关系,踩过的坑比跑通的实验多得多。身边不少刚入门的同学经常问我同一个问题:EEG分类到底有多少条技术路线,为什么论文里的方法五花八门,自己上手却总是复现不出来。这个问题的根源在于,EEG分类不是一个单一任务,它下面挂着好几种截然不同的子问题,每种问题的数据形态、标签结构、评价方式都不一样,用同一套思路去套必然翻车。
所谓EEG数据分类,说白了就是给一段脑电信号打上标签。这个标签可以是"这段信号来自想象左手运动还是右手运动",可以是"这个受试者此刻处于清醒还是困倦状态",也可以是"这段波形属于癫痫发作期还是间歇期"。任务不同,但底层逻辑是相通的:先把原始的高维时序信号压缩成有判别力的表示,再用分类器把这个表示映射到标签空间。区别只在于,这个"压缩"和"映射"是用手工规则做的,还是用神经网络学出来的。
这篇文章适合三类人看。第一类是刚进实验室的研究生,需要快速建立EEG分类的全景认知,知道有哪些流派、各自适合什么场景。第二类是做工程落地的开发者,手上有脑电设备,想把分类模型部署到实际产品里,需要了解不同方案的算力开销和鲁棒性差异。第三类是从其他领域转过来的深度学习从业者,熟悉CNN、RNN、GNN这些工具,但不清楚它们在EEG上的特殊约束。我会把分类体系拆开讲透,每个分支都配上原理说明、实操要点和我自己踩过的坑,尽量做到看完就能动手。
2. EEG分类体系的全景拆解与流派划分
2.1 按任务类型划分:从二分类到多任务学习
EEG分类最粗的一层划分是按任务类型走。最基础的是二分类,比如运动想象里的左右手区分、癫痫检测里的发作与不发作区分。这类任务数据相对好标注,评价指标也简单,准确率和AUC基本能说明问题。往上一层是多分类,典型的是运动想象的四分类(左手、右手、双脚、舌头),或者情绪识别里的效价-唤醒度多级分类。多分类的难点在于类间边界模糊,尤其是相邻类别在频域上的差异可能只有几个赫兹,模型很容易混淆。
再往上是多标签和多任务。多标签指的是一个样本同时属于多个类别,比如一段脑电既包含疲劳特征又包含注意力下降特征。多任务则是同时预测多个相关但不同的目标,比如一边做运动想象分类一边做伪迹检测。这类任务在近两年的论文里越来越多,因为实际应用场景很少是单一标签的。我做过一个睡眠分期加异常检测的联合模型,共享底层特征提取器,两个任务头分别输出,效果比单独训两个模型好不少,因为底层特征被迫学到了更通用的表示。
2.2 按信号处理层级划分:从手工特征到端到端
第二层划分是按处理层级走,这也是传统方法和深度学习方法的分水岭。传统路线是"预处理-特征提取-特征选择-分类器"四段式。预处理负责去噪和滤波,特征提取负责从时域、频域、时频域挖出统计量,特征选择负责降维去冗余,最后丢给SVM、随机森林或者LDA做分类。这条路线的优势是可解释性强,每个特征都有明确的生理意义,比如alpha波功率、Hjorth参数、样本熵。劣势是特征工程极度依赖领域知识,换一个任务可能整套特征都要重做。
深度学习路线则是端到端,原始信号或者经过最简预处理的信号直接喂进网络,特征提取和分类一起学。CNN负责抓局部时频模式,RNN负责抓时序依赖,GNN负责抓电极间的空间关系。这条路线的优势是省去了手工特征设计的麻烦,理论上能学到人想不到的表示。劣势是数据需求量大,而EEG恰恰是典型的小样本问题,一个受试者做几百次trial就算多了,跨受试者数据又存在巨大的分布差异。
我个人的经验是,数据量小于五百个样本时,传统方法加好的特征往往比深度学习更稳。数据量上千且做了充分的跨受试者对齐后,深度学习才开始显现优势。这个阈值不是绝对的,但方向是对的。
2.3 按模型架构划分:CNN、RNN、GNN三条主线
第三层划分是按模型架构走,这也是当前论文里最活跃的部分。CNN系的核心思想是把EEG当成图像来处理,常见做法是把多通道信号转成二维矩阵,一个轴是时间,一个轴是通道,然后套用图像分类里的卷积核。更讲究一点的做法是先做时频变换得到频谱图,再对频谱图做卷积。CNN的优势是局部模式提取能力强,对高频振荡和瞬态波形的捕捉很到位。
RNN系的核心思想是把EEG当成序列来处理,利用循环结构建模时间依赖。LSTM和GRU是主力,因为它们能缓解长序列的梯度消失问题。RNN适合处理那些状态随时间演变的信号,比如情绪渐变、疲劳累积。但RNN的训练速度慢,并行性差,在长序列上容易遗忘早期信息。
GNN系是近几年的新贵,核心思想是把电极当成图的节点,电极间的相关性当成边,用图卷积来聚合空间信息。这个思路很符合EEG的物理本质,因为脑电本身就是空间上相互影响的。GNN的难点在于图结构怎么定义,是用固定的距离图,还是用数据驱动学出来的自适应图。我试过用皮尔逊相关系数建图,也试过用可学习的邻接矩阵,后者在小数据集上容易过拟合,前者更稳但不够灵活。
2.4 三条路线的对比与选型建议
| 维度 | CNN系 | RNN系 | GNN系 |
|---|---|---|---|
| 擅长建模 | 局部时频模式 | 长时序依赖 | 空间拓扑关系 |
| 数据需求 | 中等 | 较大 | 中等偏大 |
| 训练速度 | 快 | 慢 | 中等 |
| 可解释性 | 中等 | 较低 | 较高 |
| 典型场景 | 运动想象、癫痫检测 | 情绪识别、疲劳监测 | 跨通道分析、源定位辅助 |
| 主要风险 | 过拟合、通道顺序敏感 | 梯度问题、并行性差 | 图结构设计依赖经验 |
选型的时候不要迷信某一条路线。我见过太多论文把三种架构堆在一起,号称混合模型,结果消融实验一做发现提升全来自某一个分支。实际项目里,先跑通最简单的CNN基线,再根据任务特点决定要不要加RNN或GNN,这个顺序最稳妥。
3. 核心细节解析:从原始信号到分类标签的关键环节
3.1 预处理:去噪、滤波与重参考
EEG信号的信噪比低得令人发指,这是所有从业者的共识。原始信号里混着眼电、肌电、心电、工频干扰,还有电极接触不良带来的漂移。预处理做不好,后面模型再花哨也是白搭。去噪的常规流程是先做带通滤波,通常保留0.5到45赫兹,因为大部分有意义的脑电活动都在这个范围。工频干扰用陷波滤波干掉,50赫兹或60赫兹看地区。
眼电伪迹的处理是个分水岭。简单做法是用阈值法直接剔除幅度超标的段,但这样会丢数据。讲究一点的做法是用独立成分分析把眼电成分分离出来再减掉,或者用回归方法把眼电参考通道的影响扣掉。我试过用ICA,效果不错但计算量大,而且成分的挑选需要人工判断,自动化程度低。后来转向用自适应滤波,速度快但残留较多。现在有些论文用深度学习做伪迹去除,思路是训一个自编码器把干净信号重建出来,这个方向值得关注但还没到完全成熟的阶段。
重参考这一步经常被新手忽略。默认的参考电极选择会直接影响信号的空间分布,常用的有耳垂参考、乳突参考、平均参考。平均参考在通道数多的时候比较稳,但会引入全局的共模噪声。我的习惯是先用平均参考跑一遍,如果分类效果不理想再换回乳突参考对比。
注意:预处理阶段千万不要做过度滤波。我见过有人把带通卡到8到30赫兹,结果把慢波和gamma波全滤没了,分类器只能靠噪声做判断,交叉验证准确率虚高但实际部署一塌糊涂。
3.2 特征工程:时域、频域、时频域与空间域
传统路线的核心在特征。时域特征包括均值、方差、偏度、峰度、过零率、Hjorth参数。这些特征计算快,但对非平稳信号描述能力有限。频域特征主要是各频段的功率谱密度,delta、theta、alpha、beta、gamma五个经典频段,可以算绝对功率也可以算相对功率。相对功率对个体差异更鲁棒,我一般优先用相对功率。
时频域特征用短时傅里叶变换或者小波变换得到,能同时保留时间和频率信息。小波变换在EEG上用得更多,因为它的多分辨率特性适合分析瞬态成分。常用的母小波有Daubechies和Morlet,前者适合检测突变,后者适合分析振荡。选择哪个母小波没有标准答案,我通常拿几个候选跑一遍交叉验证看哪个效果好。
空间域特征包括共空间模式、拉普拉斯算子、通道间相关性。共空间模式在运动想象任务里是经典方法,核心思想是找一组空间滤波器让两类信号的方差比最大化。这个方法的数学推导有点绕,但实现起来不复杂,很多工具箱都有现成的。通道间相关性可以构建功能连接矩阵,这个矩阵既可以作为特征喂给分类器,也可以作为GNN的输入图。
3.3 深度学习模型的关键设计选择
用CNN处理EEG时,第一个要决定的是输入形态。是把原始时序直接卷,还是先转成时频图再卷。直接卷时序的做法叫一维卷积,计算量小但感受野有限。转成时频图用二维卷积,感受野大但计算量翻倍。我一般先用一维卷积做快速验证,效果不够再上二维。
第二个要决定的是通道维度的处理方式。一种做法是把通道当成卷积核的深度,让网络自己学通道间的组合。另一种做法是先对每个通道独立卷积,再用一个空间卷积层聚合。前者参数少但可能忽略空间结构,后者参数多但表达能力强。运动想象任务里后者通常更好,因为左右手想象的空间分布差异是核心判别信息。
RNN这边,LSTM和GRU的选择其实没那么关键,GRU参数少一点训练快一点,LSTM在超长序列上略稳。真正关键的是序列切分方式。是把整个trial当成一个序列,还是切成多个时间窗分别预测再投票。前者上下文完整但序列长,后者训练样本多但丢失了跨窗依赖。我倾向于用滑动窗加注意力机制,让模型自己决定哪些窗重要。
GNN这边,图结构的定义是命门。固定图用电极间的物理距离或者功能连接阈值来建边,简单但不够灵活。自适应图让网络学一个邻接矩阵,灵活但容易过拟合。折中方案是用物理距离图作为先验,再叠加一个可学习的残差。这个思路我在一个跨受试者情绪识别项目里用过,比纯固定图提升了三个百分点左右。
3.4 标签处理与数据增强
EEG数据的小样本特性决定了数据增强不是可选项而是必选项。常用的增强手段包括加高斯噪声、时间平移、幅度缩放、通道丢弃、频段扰动。加噪声的幅度要控制好,太大就淹没有效信号了,我一般按信号标准差的百分之五到十来加。时间平移要注意别把标签对应的关键段移出窗口。
还有一个容易被忽略的点是标签平滑。EEG的标签往往带有噪声,尤其是靠受试者自我报告得到的情绪标签,边界样本的标签可信度低。用硬标签训练会让模型对这些样本过度自信。标签平滑把硬标签换成软标签,能缓解这个问题。我通常把平滑系数设在0.1左右,太高会让模型学不到判别边界。
4. 实操过程:从零搭建一个EEG分类流水线
4.1 环境准备与数据加载
我习惯用Python做原型开发,核心依赖是MNE负责信号处理,PyTorch负责建模,scikit-learn负责传统方法和评价指标。环境配置这块,miniconda建虚拟环境最省心,避免包版本冲突。MNE的安装直接pip就行,但要注意它依赖的scipy和numpy版本,太新太旧都可能出问题。
数据加载阶段,公开数据集是入门的好选择。运动想象有BCI Competition IV 2a,癫痫检测有CHB-MIT,情绪识别有DEAP和SEED。这些数据集的格式各不相同,MNE提供了不少读取接口,但有些需要自己写解析脚本。加载后第一件事是检查采样率、通道名、事件标记是否对齐。我踩过一次坑,事件标记的时间戳和信号时间轴差了半个采样周期,导致切出来的trial全部偏移,分类准确率一直在随机水平徘徊,查了两天才发现是这个问题。
import mne import numpy as np raw = mne.io.read_raw_gdf('dataset.gdf', preload=True) raw.filter(0.5, 45., fir_design='firwin') events, event_id = mne.events_from_annotations(raw) epochs = mne.Epochs(raw, events, event_id, tmin=-0.2, tmax=0.8, baseline=(-0.2, 0), preload=True) X = epochs.get_data() # shape: (n_epochs, n_channels, n_times) y = epochs.events[:, -1]这段代码是标准起手式。tmin和tmax的选择取决于任务,运动想象一般取提示后0.5到2.5秒,因为运动想象的ERD现象有延迟。baseline用提示前的一段做基线校正,能去掉个体间的直流偏移差异。
4.2 传统基线:CSP加LDA的快速验证
在碰深度学习之前,我一定会先跑一个CSP加LDA的基线。原因很简单,如果这个基线能到百分之七十以上,说明数据本身有可分性,深度学习有发挥空间。如果基线只有百分之五十几,那要么是预处理有问题,要么是任务本身太难,这时候上深度学习大概率也是白费。
CSP的实现用MNE的CSP类,取前几个空间滤波器。LDA用scikit-learn的默认配置就行。交叉验证用分层K折,折数取五或者十。这里有个细节,CSP的滤波器必须在训练折上拟合,不能在全量数据上拟合后再切分,否则就是数据泄露。我见过不少论文犯这个错误,报出来的准确率高得离谱,实际复现差一大截。
from mne.decoding import CSP from sklearn.discriminant_analysis import LinearDiscriminantAnalysis from sklearn.pipeline import Pipeline from sklearn.model_selection import StratifiedKFold, cross_val_score clf = Pipeline([('CSP', CSP(n_components=4, reg=None, log=True)), ('LDA', LinearDiscriminantAnalysis())]) cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) scores = cross_val_score(clf, X, y, cv=cv, n_jobs=1) print(scores.mean())4.3 深度学习模型搭建:以CNN为例
CNN的搭建我倾向于从简到繁。第一版就用两三个卷积块加全局平均池化加全连接,别一上来就堆残差连接和注意力。卷积核大小在时间维度上取采样率的四分之一左右,比如250赫兹采样就取64,这样能覆盖一个完整的alpha周期。通道维度上第一层卷积核深度等于通道数,让网络自己学空间滤波。
import torch import torch.nn as nn class EEGNet(nn.Module): def __init__(self, n_channels, n_times, n_classes): super().__init__() self.conv1 = nn.Conv2d(1, 16, (1, 64), padding=(0, 32)) self.bn1 = nn.BatchNorm2d(16) self.conv2 = nn.Conv2d(16, 32, (n_channels, 1)) self.bn2 = nn.BatchNorm2d(32) self.pool = nn.AvgPool2d((1, 4)) self.drop = nn.Dropout(0.5) self.fc = nn.Linear(32 * (n_times // 4), n_classes) def forward(self, x): x = x.unsqueeze(1) x = torch.relu(self.bn1(self.conv1(x))) x = torch.relu(self.bn2(self.conv2(x))) x = self.pool(x) x = self.drop(x) x = x.flatten(1) return self.fc(x)这个结构是EEGNet的简化版,原论文里还有深度可分离卷积,我这里为了讲清楚原理先省略。训练的时候用Adam,学习率从千分之一开始,batch size取32或者64。早停的耐心值设十到十五个epoch,EEG数据很容易过拟合,训练损失还在降但验证损失已经抬头的时候就得停。
4.4 跨受试者迁移的处理
跨受试者是EEG分类最头疼的问题。同一个模型在受试者A上准确率百分之九十,换到受试者B可能掉到百分之六十。原因是每个人的脑电幅值、频率分布、头型尺寸都不一样。处理这个问题有几条路。一是做数据对齐,用欧氏对齐把每个受试者的协方差矩阵对齐到单位矩阵,这个操作简单但效果显著,我基本是标配。二是做域适应,用对抗训练让特征提取器学到的表示在不同受试者间不可区分。三是做微调,用目标受试者的少量标注数据调整模型最后几层。
欧氏对齐的实现不复杂,核心是对每个trial的协方差矩阵做白化变换。MNE里没有现成的函数,得自己写,但代码量不大。我实测下来,光这一步就能把跨受试者准确率提升五到十个点,性价比极高。
提示:跨受试者实验里,测试受试者的数据绝对不能参与任何形式的训练,包括标准化参数的拟合。我见过有人用全量数据算均值和方差再做标准化,这属于典型的数据泄露,报出来的结果没有参考价值。
5. 常见问题与排查技巧实录
5.1 准确率卡在随机水平怎么办
这是新手最常遇到的问题。排查顺序我一般是这样的。先看标签对不对齐,事件标记和信号时间轴是否一致,这个用肉眼画图就能查。再看预处理有没有把有效信号滤掉,把滤波前后的信号画出来对比。然后看数据有没有做基线校正,没做基线校正的话直流漂移会主导分类。最后看交叉验证有没有数据泄露,CSP和标准化这类依赖数据的操作必须在训练折内拟合。
如果以上都没问题,那可能是任务本身的可分性就低。这时候可以降级任务,比如四分类改二分类,看看准确率有没有提升。如果二分类也上不去,那大概率是数据质量问题,检查电极阻抗记录,看看有没有通道全程饱和或者全是噪声。
5.2 模型过拟合的识别与缓解
过拟合的典型表现是训练准确率一路涨到九十九,验证准确率在六七十徘徊。缓解手段按优先级排,第一是加数据增强,这是最根本的。第二是加Dropout和权重衰减,Dropout率从0.5开始试,权重衰减从万分之一开始。第三是减小模型容量,把卷积核数量减半或者去掉一层。第四是早停,这个最简单但最有效。
我个人的经验是,EEG任务里模型参数量控制在十万以内比较稳,超过五十万基本必过拟合,除非数据量上万。所以别盲目堆大模型,小模型加好特征往往更实用。
5.3 训练不收敛的排查清单
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 损失一直是NaN | 学习率太大或输入有NaN | 降学习率,检查数据 |
| 损失不降 | 学习率太小或初始化差 | 调学习率,换初始化 |
| 损失震荡 | batch size太小 | 增大batch size |
| 验证损失先降后升 | 过拟合 | 早停、加正则 |
| 准确率不涨但损失降 | 标签噪声或类别不平衡 | 检查标签分布 |
类别不平衡在EEG里很常见,比如癫痫检测里发作段远少于非发作段。处理方法是加类别权重或者用focal loss。我一般先用加权交叉熵,简单有效。如果效果不够再上focal loss,但focal loss的超参数比较敏感,调起来费劲。
5.4 部署阶段的性能优化
模型训好了要部署到实际设备上,这时候算力和内存就是硬约束。优化手段包括模型量化、剪枝、知识蒸馏。量化把浮点权重转成定点,速度能快两三倍,精度损失通常在一个点以内。剪枝去掉不重要的连接,能减小模型体积。知识蒸馏用大模型教小模型,适合对延迟敏感的场景。
我做过一个嵌入式部署的项目,原始模型在服务器上跑一次推理要五十毫秒,量化加剪枝后降到八毫秒,精度只掉了零点五个点。这个优化幅度在实时脑机接口场景里是决定性的,因为超过二十毫秒的延迟用户就能感觉到卡顿。
6. 我在这条路上踩过的几个坑
第一个坑是迷信复杂模型。刚入门的时候觉得模型越深越好,把ResNet搬过来改改就用,结果在小数据集上过拟合得一塌糊涂。后来回归简单结构,反而效果更好。EEG分类的核心矛盾是样本少而维度高,模型复杂度必须和数据量匹配。
第二个坑是忽略个体差异。早期做跨受试者实验,直接把所有人的数据混在一起训,结果模型学到的是受试者身份而不是任务相关特征。后来加了欧氏对齐和域适应,才把这个问题缓解。现在我做任何跨受试者实验,第一件事就是检查模型有没有在偷学受试者特征,方法是看混淆矩阵里有没有按受试者聚类的模式。
第三个坑是评价指标选错。EEG数据经常类别不平衡,这时候准确率是有欺骗性的。一个把所有样本都判为多数类的模型也能有百分之八九十的准确率。所以我现在一律看AUC和F1,必要时看混淆矩阵。如果是多分类,看宏平均F1而不是微平均。
第四个坑是复现论文时不看细节。很多论文的方法部分写得含糊,超参数、预处理步骤、数据划分方式都一笔带过。直接照搬往往复现不出来。我的做法是先找有没有开源代码,没有的话就发邮件问作者要预处理后的数据或者详细配置。实在不行就自己按合理猜测补全,然后在论文里注明这是复现尝试而非严格复现。
EEG分类这个方向,工具在变,模型在变,但底层逻辑没变:理解信号、尊重数据、控制复杂度。把这三点做好了,不管用CNN还是GNN,都能做出靠谱的结果。