4. ERP提取与基线校正:从原始脑电到干净波形

好,咱们进入正题。这一章讲的是怎么从乱糟糟的原始EEG里,把ERP信号给“揪”出来。

说白了,ERP信号就藏在背景脑电里。单次刺激诱发的ERP,幅度可能只有几微伏,而背景EEG动辄几十微伏。怎么把它提取出来?靠的就是分段、基线校正和叠加平均这三板斧。

核心逻辑一句话:把连续脑电切成小段,每段对齐到刺激发生时刻,然后做基线校正消除漂移,最后把几十上百段叠加平均,噪声互相抵消,ERP波形就浮现出来了。

ERP提取核心流程 连续EEG信号 原始采集数据 事件标记对齐 找到刺激触发点 Epoch分段 [-200ms, 800ms] 基线校正 去漂移 叠加平均 信噪比提升 √ ERP波形可视化 分析成分 单试次分析:不叠加,直接看单次epoch

4.1 Epoch分段原理:把连续信号切成小段

原始EEG是连续记录的,就像一卷长长的电影胶片。我们要做的,就是找到每个刺激出现的时刻(事件标记),然后以这个时刻为中心,切出一段固定长度的信号。

分段参数怎么定?

  • 刺激前时间(pre-stimulus):通常是-200ms或-100ms。这段用来做基线校正。我个人习惯用-200ms,因为有些慢波成分需要更长的基线。
  • 刺激后时间(post-stimulus):取决于你关心的ERP成分。P300一般在300-600ms出现,所以取800ms够用。如果是研究N400,可能需要1000ms以上。
  • 总epoch长度:pre + post。比如[-200, 800]就是1000ms。

我的经验:分段时别太贪心。我曾经为了看一个晚期正成分,把epoch拉到2000ms,结果低频漂移严重,基线校正都救不回来。后来老老实实缩到1000ms,波形干净多了。

代码实现其实很简单,以MNE为例:

import mne

# 读取原始数据
raw = mne.io.read_raw_fif('sub-01_raw.fif', preload=True)

# 定义事件
events = mne.find_events(raw)

# 分段:刺激前200ms,刺激后800ms
epochs = mne.Epochs(raw, events, event_id={'target': 1, 'standard': 2},
                    tmin=-0.2, tmax=0.8, baseline=None,
                    preload=True)

print(epochs)
# 输出:

4.2 基线校正方法:把波形拉回零线

分段之后你会发现,每个epoch的起始点并不在零线上。有的偏正,有的偏负。这是因为EEG有低频漂移,或者皮肤电位在慢慢变化。

基线校正,就是把刺激前那段时间的平均值算出来,然后从整个epoch里减掉它。这样所有epoch就站在同一条起跑线上了。

两种常用方法:

方法 原理 适用场景 注意事项
均值基线 取基线窗口内所有点的平均值,从整个epoch减去 大多数标准ERP分析 基线窗口不能包含刺激后成分
线性基线 用基线窗口拟合一条直线,减去该直线趋势 有明显线性漂移的数据 可能过度校正,慎用

均值基线是最常用的。我做了这么多年ERP,90%的情况都用均值基线。线性基线只在数据有明显漂移时才用,而且我会先做高通滤波(0.1-0.5Hz)来去除漂移,而不是依赖线性基线校正。

代码示例:

# 均值基线校正(推荐)
epochs.apply_baseline(baseline=(-0.2, 0))

# 线性基线校正(MNE中需要手动实现)
import numpy as np
from scipy import stats

def linear_baseline_correction(epoch_data, baseline_idx):
    """对每个epoch做线性基线校正"""
    corrected = np.zeros_like(epoch_data)
    for i in range(epoch_data.shape[0]):
        for ch in range(epoch_data.shape[1]):
            # 提取基线段
            baseline_segment = epoch_data[i, ch, baseline_idx[0]:baseline_idx[1]]
            x = np.arange(len(baseline_segment))
            # 线性拟合
            slope, intercept, _, _, _ = stats.linregress(x, baseline_segment)
            # 构建校正趋势
            trend = slope * np.arange(epoch_data.shape[2]) + intercept
            corrected[i, ch, :] = epoch_data[i, ch, :] - trend
    return corrected

避坑指南:我曾经犯过一个低级错误——基线窗口选错了。有一次我把基线窗口设成了[-200, 100],结果100ms已经包含了早期成分(比如P50),基线校正直接把P50给减掉了。后来检查波形才发现不对劲。记住:基线窗口必须在刺激之前,不能包含任何刺激后成分。

4.3 Epoch叠加平均:让信号从噪声中浮现

这是ERP分析最核心的一步。原理很简单:

每个epoch = ERP信号 + 背景噪声

噪声是随机的,均值趋近于0。叠加N次后,信号增强N倍,噪声只增强√N倍。信噪比提升√N倍。

叠加多少次才够?

  • 视觉ERP(如P100、N170):30-50次就能看到清晰波形
  • 听觉ERP(如MMN、P300):50-100次比较稳妥
  • 认知实验(如ERN、N400):80-150次,取决于效应大小

代码实现:

# 叠加平均
evoked_target = epochs['target'].average()
evoked_standard = epochs['standard'].average()

# 查看结果
print(evoked_target)
# 输出:

我的习惯:叠加平均前,我会先看一眼每个epoch的方差。如果某个epoch的方差特别大(比如是其他epoch的3倍以上),我会把它标记为异常,考虑剔除。这叫“方差拒绝法”,比单纯看幅值更靠谱。

4.4 ERP波形可视化:一眼看出成分

波形画出来,才能看出有没有ERP成分。我一般会画两种图:

  1. 总平均波形图:所有条件叠加后的波形,对比不同条件
  2. 地形图:特定时间点的头皮分布,看成分的脑区定位

代码示例:

import matplotlib.pyplot as plt

# 画波形图
fig, ax = plt.subplots(figsize=(10, 6))
evoked_target.plot(axes=ax, picks=['Cz'], show=False, 
                   time_unit='ms', spatial_colors=False)
evoked_standard.plot(axes=ax, picks=['Cz'], show=False,
                     time_unit='ms', spatial_colors=False)
ax.legend(['Target', 'Standard'])
ax.set_title('ERP波形对比 - Cz电极')
ax.axvline(0, color='gray', linestyle='--', alpha=0.5)
ax.axhline(0, color='gray', linestyle='--', alpha=0.5)
plt.show()

# 画地形图
evoked_target.plot_topomap(times=[0.1, 0.2, 0.3, 0.4], 
                           ch_type='eeg', time_unit='ms')

看波形时,我习惯先看三个东西:

  • 基线是否平直:刺激前200ms应该是一条水平线,如果有趋势,说明基线校正没做好
  • 早期成分是否清晰:P100、N170这些早期成分应该能在单个电极上看到
  • 晚期成分的形态:P300应该是正向偏转,N400应该是负向偏转

4.5 单试次分析基础:不叠加,直接看单次

叠加平均虽然好用,但它抹掉了试次间的差异。有时候我们想知道:是不是每个试次都有ERP?还是只有部分试次有?

单试次分析,就是不叠加,直接看每个epoch的波形。当然,单次epoch的信噪比很低,波形可能很乱。但我们可以用一些方法:

  • 滤波:先做低通滤波(比如30Hz),去掉高频噪声
  • 单试次ERP估计:用回归或机器学习方法,从单次epoch中估计ERP成分
  • 时频分析:看单次epoch的时频能量变化

代码示例:

# 查看单试次
epochs['target'].plot_image(picks=['Cz'], 
                            vmin=-10, vmax=10,
                            colorbar=True)

# 单试次叠加图(每个试次一条线)
fig, ax = plt.subplots(figsize=(10, 6))
for i in range(10):  # 只看前10个试次
    epoch_data = epochs['target'][i].get_data(picks=['Cz'])[0]
    ax.plot(epochs.times * 1000, epoch_data, 
            alpha=0.5, linewidth=0.5)
ax.set_xlabel('Time (ms)')
ax.set_ylabel('Amplitude (µV)')
ax.set_title('单试次ERP波形(前10个试次)')
ax.axvline(0, color='gray', linestyle='--')
plt.show()

单试次分析的价值:我做过一个实验,叠加平均后P300很漂亮。但单试次一看,发现只有60%的试次有P300,剩下的40%是平的。这说明被试可能在某些试次中走神了。如果只看叠加平均,你永远不知道这个信息。

嗯,这一章的内容就这些。分段、基线校正、叠加平均,这三步是ERP分析的基石。单试次分析是进阶玩法,但理解它有助于你更深入地理解ERP的本质——它不是一个固定的波形,而是一个统计现象。


公众号:蓝海资料掘金营,微信deep3321