第三节:脑电数据预处理——把脏数据洗干净
各位同学,欢迎来到预处理环节。
说实话,脑电数据预处理这步,我当年刚入行时觉得特别枯燥。不就是滤波、去噪、校正嘛,有什么好讲的?直到我第一次拿着没做ICA去伪迹的数据去跑分类模型,结果准确率只有55%——跟抛硬币差不多。嗯,从那以后我再也不敢小看预处理了。
今天我们要聊的内容,说白了就是四个步骤:带通滤波、陷波滤波、ICA去伪迹、基线校正。每一步都有它的道理,也有它的坑。我尽量把我在项目中踩过的雷都告诉你。
3.1 带通滤波:把不要的频率切掉
脑电信号里,真正有用的频率范围其实很窄。我个人习惯用0.5到45 Hz这个区间。为什么?
- 低于0.5 Hz:主要是基线漂移和慢电位,跟认知活动关系不大
- 高于45 Hz:肌电噪声(EMG)开始大量出现,还有各种高频干扰
你想想看,如果不做带通滤波,低频漂移会让你的数据看起来像过山车,高频噪声又会让波形变得毛刺刺的。我在做运动想象脑电时遇到过这种情况——没滤波之前,C3和C4导联的ERD/ERS模式根本看不出来,滤波之后才清晰可见。
核心参数:
- 低截止频率:0.5 Hz(有时用0.1 Hz,看具体任务)
- 高截止频率:45 Hz(或40 Hz,看你的采样率)
- 滤波器阶数:我一般用4阶Butterworth,效果够用
# Python代码示例 - MNE库实现带通滤波
import mne
# 加载原始数据
raw = mne.io.read_raw_fif('subject01_raw.fif', preload=True)
# 带通滤波 0.5-45 Hz
raw.filter(0.5, 45, fir_design='firwin')
print('带通滤波完成!')
⚠️ 注意:滤波顺序有讲究。我建议先做带通滤波,再做陷波滤波。如果反过来,陷波滤波可能会把带通滤波边缘附近的频率也削掉一部分,造成不必要的信号损失。
3.2 陷波滤波:干掉50 Hz工频干扰
工频干扰,说白了就是市电带来的噪声。在中国是50 Hz,在美国是60 Hz。这个频率的干扰在脑电数据里特别明显——你会看到所有导联上都有规律的50 Hz正弦波叠加在上面。
我记得有一次做静息态脑电分析,没做陷波滤波就直接跑功率谱密度,结果50 Hz那里冒出一个巨大的尖峰,把alpha波(8-12 Hz)的峰值都压下去了。嗯,从那以后我每次预处理都会检查一下功率谱,确认50 Hz被干掉没有。
💡 我的经验:陷波滤波的带宽不要设太宽。我一般用48-52 Hz,带宽4 Hz就够了。带宽太宽会把附近的脑电活动也滤掉,特别是如果你研究的是gamma频段(30-50 Hz),那就要格外小心。
# 陷波滤波 50 Hz
raw.notch_filter(50, picks='eeg', fir_design='firwin')
print('50 Hz 工频干扰已去除')
3.3 ICA去伪迹:把眼电和肌电分离出来
独立成分分析(ICA),这名字听起来挺高大上,其实原理不复杂。它假设脑电信号是由多个独立源混合而成的——脑电活动是一个源,眨眼是一个源,肌肉活动又是一个源。ICA就是把这些源分离开。
我刚开始用ICA时犯过一个错误:把所有成分都保留,只手动剔除那些明显是眼电的成分。后来发现,有些成分看起来像脑电,其实是肌电伪迹。怎么办?我现在的做法是:先用EOG通道做参考,自动识别眼电成分,再手动检查剩下的成分。
ICA去伪迹的关键步骤:
- 先做带通滤波和陷波滤波
- 运行ICA算法(我常用FastICA或Infomax)
- 查看各成分的拓扑图和时序图
- 剔除眼电成分(前额叶分布、低频高幅)
- 剔除肌电成分(后部分布、高频特征)
- 重建干净的脑电信号
# ICA去伪迹示例
from mne.preprocessing import ICA
# 初始化ICA,设置成分数量
ica = ICA(n_components=20, random_state=42)
# 拟合ICA
ica.fit(raw, picks='eeg')
# 自动识别眼电成分(如果有EOG通道)
eog_indices, eog_scores = ica.find_bads_eog(raw, ch_name='EOG', threshold=3.0)
# 标记并剔除眼电成分
ica.exclude = eog_indices
raw_clean = ica.apply(raw)
print(f'已剔除 {len(eog_indices)} 个眼电成分')
⚠️ 我曾经踩过的坑:有一次我一次性剔除了10个成分,结果发现脑电信号变得很奇怪——alpha波都没了。后来才意识到,ICA成分里有些是脑电活动本身,不能盲目剔除。我的建议是:每次剔除前先看拓扑图和频谱,确认它是伪迹再动手。
3.4 基线校正:让数据站在同一起跑线上
基线校正,说白了就是把每个trial的基线期(通常是刺激出现前的一段时间)的平均值减掉。为什么要做?因为不同trial的直流偏移不一样,如果不校正,你看到的ERP波形可能不是真正的脑电反应,而是基线差异造成的假象。
我一般用刺激前200毫秒作为基线期。比如刺激在0时刻出现,我就取-200 ms到0 ms这段数据,计算平均值,然后从整个trial里减掉。
💡 小技巧:基线校正后,记得检查一下所有trial的基线期是否都接近0微伏。如果某个trial的基线期偏离很大,说明这个trial可能还有残留噪声,可以考虑剔除。
# 基线校正示例
# 假设epochs已经创建,基线期为[-0.2, 0]秒
epochs = mne.Epochs(raw, events, event_id, tmin=-0.2, tmax=0.8, baseline=(-0.2, 0))
# 或者对已有的epochs做基线校正
epochs.apply_baseline(baseline=(-0.2, 0))
print('基线校正完成!')
3.5 预处理流程总结
好了,四个步骤都讲完了。我整理了一个表格,方便你对照使用:
| 步骤 | 目的 | 关键参数 | 注意事项 |
|---|---|---|---|
| 带通滤波 | 去除低频漂移和高频噪声 | 0.5-45 Hz,4阶Butterworth | 先做带通,再做陷波 |
| 陷波滤波 | 去除50 Hz工频干扰 | 48-52 Hz,带宽4 Hz | 带宽不要太宽,避免影响gamma频段 |
| ICA去伪迹 | 分离眼电、肌电等伪迹 | n_components=15-25 | 逐个检查成分,不要批量剔除 |
| 基线校正 | 消除trial间直流偏移差异 | 基线期[-0.2, 0]秒 | 校正后检查基线是否归零 |
一句话总结:预处理做得好,后续分析事半功倍。我见过太多人花大量时间调模型参数,却忽略了预处理——结果数据质量差,模型怎么调都调不好。记住,干净的输入数据,才是好模型的基石。
公众号:蓝海资料掘金营,微信deep3321