第3章 MNE库入门:Raw对象创建与加载、数据可视化、通道选择与重命名、事件提取与Epoch生成

好,咱们进入MNE库的实战环节。说实话,MNE这个库我用了快十年了,从最早的0.9版本一路跟到现在。它就像一把瑞士军刀,处理脑电数据时几乎什么活儿都能干。这一章,咱们就把它最核心的几个功能摸透。

3.1 Raw对象:脑电数据的"容器"

Raw对象是什么?说白了,它就是MNE里用来装原始脑电数据的容器。你采集到的所有通道数据、采样频率、电极位置等信息,全塞在这个对象里。

3.1.1 从零创建Raw对象

有时候我们需要自己造数据来测试算法。比如你写了个滤波函数,想看看效果对不对,这时候手动创建Raw对象就很有用。

import mne
import numpy as np

# 模拟数据:5秒,250Hz采样,19个通道
sfreq = 250  # 采样频率
duration = 5  # 秒
n_channels = 19
data = np.random.randn(n_channels, sfreq * duration)

# 创建Info对象
info = mne.create_info(
    ch_names=['Fz', 'Cz', 'Pz', 'F3', 'F4', 'C3', 'C4', 
              'P3', 'P4', 'O1', 'O2', 'F7', 'F8', 'T7', 
              'T8', 'P7', 'P8', 'Fp1', 'Fp2'],
    sfreq=sfreq,
    ch_types='eeg'
)

# 创建Raw对象
raw = mne.io.RawArray(data, info)
print(raw)
print(f"数据形状: {raw.get_data().shape}")

嗯,这里要注意一点:ch_types参数我习惯写成'eeg',但如果你有眼电、心电等其他类型的信号,可以传一个列表,比如['eeg', 'eeg', 'eog', ...]。我在项目里就踩过这个坑——把所有通道都标成'eeg',结果做ICA去眼电时死活找不到EOG通道。

3.1.2 加载真实数据

实际项目中,我们更多是从文件加载数据。MNE支持几十种格式,我常用的就三种:

格式 加载函数 适用场景
EDF/EDF+ mne.io.read_raw_edf() 医院、公开数据集最常见
BrainVision mne.io.read_raw_brainvision() 德国Brain Products设备
FIF mne.io.read_raw_fif() MNE原生格式,速度最快
# 加载EDF文件(以公开数据集为例)
raw = mne.io.read_raw_edf('sample_data.edf', preload=True)
print(f"采样频率: {raw.info['sfreq']} Hz")
print(f"通道数: {len(raw.ch_names)}")
print(f"时间长度: {raw.times[-1]:.2f} 秒")

我个人建议,只要内存够用,就把preload=True加上。为什么?因为后续做滤波、重参考这些操作,如果数据没加载到内存,MNE会反复读硬盘,慢得让人抓狂。我曾经处理一个2小时的EEG数据,没加preload,一个简单的带通滤波跑了快10分钟……加了之后秒出。

3.2 数据可视化:先看看你的信号长什么样

拿到数据第一件事是什么?我反正不会直接跑分析。先画图看看,有没有坏段、有没有大噪声、基线漂不漂。这一步省不了。

3.2.1 原始信号绘图

# 绘制原始信号(前10秒)
raw.plot(duration=10, n_channels=10, scalings='auto')

# 绘制功率谱密度
raw.plot_psd(fmin=0.5, fmax=45, tmin=0, tmax=60)

scalings='auto'这个参数很贴心,它会根据每个通道的信号幅度自动调整显示比例。但有时候自动调整反而会掩盖问题——比如某个通道信号特别小,自动缩放后看起来跟正常通道一样,你就发现不了它坏了。所以我习惯先设成统一尺度看看:

# 统一尺度,更容易发现异常通道
raw.plot(duration=10, scalings=dict(eeg=100e-6))

3.2.2 通道布局图

电极位置对不对?这个一定要检查。我遇到过好几次,数据文件里的电极位置标错了,结果做源定位时出来的结果完全不对。

# 绘制电极位置
raw.plot_sensors(show_names=True)

# 如果电极位置信息缺失,可以手动设置
montage = mne.channels.make_standard_montage('standard_1020')
raw.set_montage(montage)
raw.plot_sensors(show_names=True)

这里有个坑:set_montage()之前,一定要确保通道名称跟模板里的名称能对上。比如你的数据里通道叫'Fz',模板里也叫'Fz',那就没问题。但如果你的数据里叫'FZ'(大写),那就对不上了。我一般会先打印montage.ch_names看看。

3.3 通道选择与重命名

实际数据很少能直接用。通道顺序乱七八糟、命名不规范、有些通道根本没用……这些都得处理。

3.3.1 选择通道子集

# 选择特定通道
raw_picked = raw.pick_channels(['Fz', 'Cz', 'Pz', 'O1', 'O2'])

# 按类型选择
raw_eeg = raw.pick_types(meg=False, eeg=True, eog=False)

# 排除坏通道
raw.drop_channels(['Fp1', 'Fp2'])  # 额极通道容易受眨眼污染

你想想看,为什么我经常把Fp1和Fp2丢掉?因为这两个通道离眼睛太近了,眨眼产生的伪迹幅度比脑电信号大几十倍。虽然可以用ICA去除,但有时候直接丢掉更省事——尤其是你关注的脑区不在额叶时。

3.3.2 重命名通道

不同设备厂商的命名规则五花八门。有的叫'Cz',有的叫'C_Z',还有的叫'Channel_12'。统一命名是第一步。

# 批量重命名
rename_dict = {
    'C_Z': 'Cz',
    'P_Z': 'Pz',
    'Channel_12': 'Fz',
    'Channel_14': 'Cz'
}
raw.rename_channels(rename_dict)

# 或者用函数批量处理
raw.rename_channels(lambda x: x.upper().replace(' ', ''))

嗯,这里有个小技巧:用lambda函数做批量处理特别灵活。比如你的通道名是'EEG Fz'、'EEG Cz'这种带前缀的,一行代码就能去掉前缀:raw.rename_channels(lambda x: x.replace('EEG ', ''))

3.4 事件提取与Epoch生成

终于到了最关键的部分。原始脑电信号是连续的,但我们做分析时,通常只关心刺激出现前后那一段。这就需要把连续信号切成一个个小片段——也就是Epoch。

3.4.1 从刺激通道提取事件

大多数脑电设备在采集数据时,会用一个专门的通道记录刺激标记。比如每次屏幕出现一个图片,这个通道的电压就会跳变一下。

# 从STIM通道提取事件
events = mne.find_events(raw, stim_channel='STI 014')
print(f"找到 {len(events)} 个事件")
print(events[:5])  # 前5个事件

events是一个n×3的数组,每一行代表一个事件。三列分别是:事件发生的采样点序号、前一个事件的持续时间(这个一般用不到)、事件类型(比如1代表目标刺激,2代表非目标刺激)。

我曾经踩过的坑: 有些设备的刺激通道不是'STI 014',而是'Trigger'、'Marker'或者干脆就叫'Channel_0'。如果找不到事件,先打印raw.ch_names看看有没有类似名字的通道。实在不行,就手动查看原始信号,找到那个有脉冲信号的通道。

3.4.2 事件ID映射

原始事件ID通常是数字,比如1、2、3、4。但每个数字代表什么含义,需要你自己定义。

# 定义事件ID映射
event_id = {
    '标准刺激': 1,
    '偏差刺激': 2,
    '目标刺激': 3
}

# 也可以反过来,从数字到名称
event_id_inv = {v: k for k, v in event_id.items()}
print(event_id_inv)  # {1: '标准刺激', 2: '偏差刺激', 3: '目标刺激'}

3.4.3 生成Epoch

有了事件,就可以切Epoch了。这里有两个关键参数:tmintmax,决定了每个Epoch的时间范围。

# 生成Epoch:刺激前0.2秒到刺激后0.8秒
tmin, tmax = -0.2, 0.8
epochs = mne.Epochs(
    raw, 
    events, 
    event_id=event_id,
    tmin=tmin, 
    tmax=tmax,
    baseline=(-0.2, 0),  # 基线校正:用刺激前200ms做基线
    preload=True,
    reject=dict(eeg=150e-6)  # 拒绝幅度超过150μV的试次
)

print(epochs)
print(f"Epoch数据形状: {epochs.get_data().shape}")

数据形状是(试次数, 通道数, 时间点)。比如你有100个试次、19个通道、每个Epoch有251个时间点(0.2+0.8秒 × 250Hz),那形状就是(100, 19, 251)。

我的经验: reject参数一定要设。不设的话,那些被眨眼、肌电污染的试次会混进来,把平均结果搞得一塌糊涂。150μV这个阈值是我做视觉ERP时常用的,但不同实验可以调整。我一般先画几个Epoch看看噪声水平,再定阈值。

3.4.4 Epoch的可视化与筛选

# 绘制所有Epoch(蝴蝶图)
epochs.plot(n_epochs=20, scalings='auto')

# 绘制平均波形
epochs['标准刺激'].average().plot()
epochs['偏差刺激'].average().plot()

# 按条件筛选Epoch
epochs_clean = epochs.copy().drop_bad()  # 去掉被拒绝的试次
print(f"保留试次: {len(epochs_clean)} / {len(epochs)}")

你可能会问:为什么用copy()?因为drop_bad()会直接修改原对象。我习惯先复制一份,万一删错了还能找回原始数据。

知识体系总览

这一章的内容比较多,我画了张图帮你理清脉络:

MNE库入门核心流程 Raw对象 创建 / 加载数据 数据可视化 原始信号 / 功率谱 通道处理 选择 / 重命名 事件提取 find_events() Epoch生成 mne.Epochs() Epoch筛选 drop_bad() / 平均 关键参数速查 • tmin/tmax:Epoch时间窗口(如-0.2~0.8秒) • baseline:基线校正区间(如(-0.2, 0)) • reject:伪迹拒绝阈值(如eeg=150e-6) • preload:是否预加载到内存(建议True) • event_id:事件类型映射字典

这张图把整个流程串起来了。从Raw对象开始,经过可视化检查、通道处理,再到事件提取和Epoch生成,每一步都有对应的MNE函数。你按照这个顺序走,基本不会出错。

核心要点回顾:

  • Raw对象是MNE的数据基石,支持多种格式加载
  • 可视化是数据质量检查的第一道防线
  • 通道重命名和选择是数据标准化的必要步骤
  • 事件提取要确认刺激通道名称和事件ID含义
  • Epoch生成时,基线校正和伪迹拒绝是标配

好了,这一章的内容就到这儿。代码不多,但每一步都是实战中绕不开的。你把这些基础打牢了,后面做特征提取、分类分析才会顺手。


专注资料整理