MNE脑电数据预处理完整流程:从读取到保存的实战指南 写这份学习笔记之前我翻了翻之前跑过的分析流程发现真正卡住新手的往往不是某个算法不好懂而是“读数据之后不知道下一步该干嘛”。MNE这个库本身功能很全但全也意味着容易迷路是先滤波还是先剪裁用crop还是Epochs为什么画出来的图全是毛刺这篇笔记就把我平时处理脑电数据最常用的一条主流程——读取、可视化、剪裁、滤波、保存——完整过一遍每一步都说明原因也把踩过的坑一并写出来。适合手里刚拿到一批脑电数据、想用MNE做预处理但还没有完整思路的同学参考。1. 先把环境收拾利索MNE安装与原始数据的内存结构1.1 我安装MNE时踩过的几个依赖坑MNE本体装起来不难pip install mne一行就能搞定。但实际处理脑电数据时几乎一定会用到NumPy、SciPy、Matplotlib做ICA去伪迹还需要scikit-learn。我最初图省事只装了mne等到运行raw.plot()时才发现Matplotlib版本不匹配弹窗直接报错排查了半天才意识到是依赖版本问题。建议一次性装齐pip install mne numpy scipy matplotlib scikit-learn国内网络环境下直接用pip有时候会卡在下载大文件上我习惯先把pip源切到清华镜像再安装就顺畅多了。版本方面需要注意的一点MNE从1.0开始有些旧API已经调整比如raw.plot_psd()逐渐被raw.compute_psd().plot()取代网上下载的旧代码直接跑往往会有DeprecationWarning不是不能用但最好跟着新版API走后面维护省心。如果你用的是Anaconda也可以conda install -c conda-forge mneconda-forge源里的MNE更新速度跟PyPI基本同步还自动帮你处理底层依赖对新手更友好。1.2 Raw对象到底是个什么东西MNE里所有原始数据都装在一个叫Raw的对象里。这个对象不是简单的二维数组而是个三层结构数据矩阵形状是(通道数, 采样点数)比如64导脑电、采样率1000Hz、记录了10分钟那矩阵就是(64, 600000)。info字典保存了采样率、通道名称、通道类型、电极位置、滤波历史等所有元数据。annotations保存实验过程中的注释比如事件标记、伪影区间说明。很多新手不理解为什么不能直接对原始数组操作。举个例子你想看某个通道的数据raw.get_data()拿到的是完整矩阵但MNE里更推荐用raw.copy().pick_channels()或者raw.get_data(picks[Fz, Cz])因为这样拿到的数据仍然带着通道名、单位这些“上下文”。数据一旦脱离了MNE对象单位、通道顺序这些信息就全丢了。单位问题尤其容易被忽略。MNE内部统一用伏特V存储数据但很多脑电设备导出时用的是微伏μV。如果没意识到这一点画图时可能发现幅值小得离谱或者大得离谱适应放大倍数之后才能看清波形。后面保存导出时单位转换也是一定要处理的点。1.3 FIF、EDF、BrainVision三种常见格式读进来之后要核对什么不同设备导出的文件格式不一样MNE针对常见格式都提供了对应的读取函数。我实际用下来最常碰到的三种格式扩展名读取函数典型场景FIF.fifmne.io.read_raw_fif()MNE原生格式信息最完整EDF/EDF.edfmne.io.read_raw_edf()医院临床数据、公开数据集BrainVision.vhdrmne.io.read_raw_brainvision()科研设备Brain Products等常见导出格式读取函数按文件格式选就行。比如BrainVision格式实际是三个文件一起的.vhdr是头文件.eeg是数据文件.vmrk是标记文件读取时只需要传入.vhdr的路径MNE会自动去找另外两个。读进来之后我强烈建议先做三件事再谈处理import mne raw mne.io.read_raw_fif(sub-01_raw.fif, preloadTrue) # 1. 看整体信息 print(raw.info) # 2. 看采样率和通道名 print(采样率:, raw.info[sfreq]) print(通道:, raw.info[ch_names]) # 3. 看坏道标注默认是空的 print(已标注坏道:, raw.info[bads])preloadTrue这一步很多人会忽略。如果不加MNE是懒加载模式数据不会一次性读入内存后续每次访问都要去磁盘读做滤波、切分段时速度慢得让人崩溃。如果你的数据量不大几百MB以内直接preloadTrue最省事。如果设备导出的文件里包含多种类型的通道比如脑电和眼电混在一起可以先筛选出需要的部分再继续raw.pick_types(megFalse, eegTrue, eogTrue)这句话的意思是只保留脑电和眼电通道去掉可能存在的肌电、心电等通道。为什么要这么做因为后续ICA去伪迹时眼电通道是很重要的参考信号而肌电通道如果不单独处理会被混进ICA成分里干扰判断。2. 可视化不是给你看的是给你找问题的很多教程把可视化放在最后当展示环节但我的习惯是数据读进来第一件事就是画图。不是走形式看个大概而是通过图快速判断这段数据“脏不脏”、要不要做额外处理。这一步做扎实了后面滤波参数的设定才有依据。2.1 raw.plot()交互界面的五个高频操作打开交互式绘图界面raw.plot(n_channels20, duration10, scalingsauto, blockTrue)n_channels控制一屏显示多少个通道duration控制每屏显示多少秒blockTrue会让脚本停在绘图窗口关掉窗口才继续往下跑——这个参数在Jupyter里跑很有用不然图一弹出脚本就继续执行了你根本来不及交互。窗口打开之后这几个操作是我每批数据都要用到的滚轮缩放鼠标悬停在时间轴上滚动可以拉近看单个波形的细节也可以拉远看整段的大趋势。按住左键拖动平移时间窗口快速扫视整段数据。我一般会花几十秒从头到尾过一遍找有没有大幅飘移或突然的尖峰。点击通道名可以暂时隐藏某个通道让波形显示更清爽。鼠标悬停在数据点上左下角会实时显示当前通道名、时间点、幅值。怀疑某段数据有异常时鼠标指过去就能看到具体数值比肉眼估靠谱得多。选中一段区域右键可以直接添加注释annotation比如标记一段明显的眼动伪影。这个操作对后面的Epochs处理特别有用。scalingsauto的意思是让MNE根据每个通道的实际幅值自动选择缩放系数。这个参数一定要加否则脑电信号幅度几十微伏和眼电信号幅度一两百微伏相差很大默认尺缩下脑电会被压成一条看似平整的线什么细节都看不清。2.2 用频谱图快速定位噪声源只看时域波形还不够我建议紧接着画频谱图raw.compute_psd().plot(fmin1, fmax80)如果是旧版本MNE可以用raw.plot_psd(fmin1, fmax80)效果一样。画这一步的目的是回答三个问题50Hz附近有没有一个异常突出的尖峰我国市电频率是50Hz如果屏蔽没做好工频干扰会以50Hz为中心形成很明显的窄峰。一旦看到这个尖峰后面就要考虑加陷波滤波。低频段0-1Hz是不是能量异常高如果是说明存在明显的基线漂移高通滤波的截止频率就不能设得太低。高频段是不是一直喇着下不去如果30Hz以上还有很大能量很可能是肌电干扰比较重这时候低通滤波的截止频率要考虑压低一些。读图的时候对比一下电力线噪声的尖峰和脑电本身在alpha频段8-12Hz的能量峰闭眼静息态数据在alpha频段会有一个小鼓包如果没有、还全是乱七八糟的毛刺那数据质量就要打问号了。我每次处理新一批数据前都会先看一眼频谱再决定滤波参数而不是拿到数据直接套一个“高通1Hz低通40Hz”的模板。参数是服务于数据质量的不是拿来装样子的。3. 剪裁数据按时间裁剪和按事件切分是两码事“剪裁”这个词在MNE里其实对应两件不同的事一种是直接按时间轴切一段数据出来用另一种是按实验事件把连续数据切成一个个小片段。两种操作的目的完全不同混着用容易出乱子。3.1 copy()和crop()的恩怨直接按时间裁剪用raw.crop()raw_cropped raw.copy().crop(tmin10, tmax100)tmin和tmax单位是秒。这里最关键的坑在于MNE里很多方法是原地修改in-place的crop()会在原对象上直接截断数据。如果你不想破坏原始数据就必须先raw.copy()再crop或者先把原始文件备份一份。我第一次用crop时没注意这个问题直接raw.crop(tmin10)然后发现raw对象只剩下一分钟的数据了后面想重新处理只能重新读文件。听起来是小事但在处理经过复杂预处理的数据时这个失误会浪费很多时间。另外提醒一下copy()是深拷贝数据量大的时候会占不少内存。比如一个1GB的FIF文件copy后再crop内存峰值可能到2GB以上。如果内存紧张可以先crop再存盘再从盘上读回避免两个大对象同时在内存里。3.2 从刺激通道里把事件标记抠出来按事件切分数据第一步是提取事件列表events mne.find_events(raw, stim_channelSTI 014)events是一个n_events × 3的数组每一行代表一个事件三列的含义分别是事件发生时的采样点序号、前一事件的数值这个一般用不上、事件编码。不同设备的刺激通道名称不一样有的叫STI 014有的叫Trigger有的叫DI16。如果你不确定自己的设备用的是哪个通道直接查看通道列表print(raw.info[ch_names])找到那个名字看起来像触发通道的再去看它的数据类型。如果实在找不到也可以print(events)看提取出来的事件编码是不是和实验设计一致。find_events()默认会把一段连续的同一触发脉冲合并成一个事件这个默认行为通常是对的。如果有些实验里需要保留每一次脉冲比如连续TMS刺激可以设置consecutiveFalse调整。3.3 用Epochs把连续数据切成可用片段事件列表提取好之后就可以切分数据了epochs mne.Epochs( raw, events, event_id{Go: 1, NoGo: 2}, tmin-0.2, tmax0.8, baseline(-0.2, 0), preloadTrue, rejectdict(eeg150e-6) )几个关键参数说一下event_id把实验条件映射到事件编码上。比如编码1对应“Go”刺激编码2对应“NoGo”刺激。做ERP分析时后面按条件分别叠加平均靠的就是这个映射。tmin-0.2, tmax0.8表示取刺激前200ms到刺激后800ms的数据。前200ms通常用作基线所以baseline(-0.2, 0)的意思就是用这段时间的平均值做基线校正。preloadTrue会把所有分段一次性加载进内存后面的运算明显更快。rejectdict(eeg150e-6)表示如果某段数据中任何一个脑电通道的峰值超过150微伏这段就自动标记为bad。注意这里单位是伏特150微伏要写成150e-6写错单位会让拒绝阈值失效。切完之后epochs对象就成了一个三维数据结构(试验次数, 通道数, 采样点数)。到这个阶段你才真正拥有了可以做叠加平均、时频分析、源定位的“干净数据块”。4. 滤波参数好定顺序更值得花心思滤波是脑电预处理里最绕不开的环节也是问题最多的环节。参数选得不合理波形可能被扭曲顺序搞反了后面的ICA可能白做。这一节把我自己的处理逻辑完整写一遍。4.1 高通、低通、陷波各管什么噪声高通滤波保留高频滤掉低频主要对付基线漂移。脑电记录过程中电极与皮肤接触状态会缓慢变化导致整段数据出现大幅度上下飘移这种飘移的频率通常低于1Hz对后续分析干扰很大。一般设0.1Hz到1Hz之间具体看漂移有多严重。漂移明显的设高一点不明显就设低一点尽量保留更多低频脑电成分。低通滤波保留低频滤掉高频主要对付高频噪声和肌电干扰。肌肉活动咬牙、皱眉、颈部紧张会产生30-200Hz范围内的高频信号混进脑电里。一般的ERP分析设40Hz低通就够了如果关心的是高频gamma频段30Hz以上低通可以设到100Hz。陷波滤波专门对付固定频率的工频干扰。国内市电50Hz国外有些地方是60Hz。先把频谱图调出来看一眼50Hz如果有尖峰就加陷波没有就不加不必习惯性加50Hz陷波。具体代码# 只做高通 raw.filter(l_freq0.5, h_freqNone, pickseeg) # 高通低通 raw.filter(l_freq0.5, h_freq40, pickseeg) # 单独做50Hz陷波 raw.notch_filter(freqs50, pickseeg)4.2 滤波顺序比参数更影响结果这是我觉得MNE预处理流程里最容易被忽略的部分。教科书上经常把滤波、分段、伪迹剔除分开讲但实际处理时顺序错了结果会差很多。我自己的处理顺序是剔除坏道、设置参考电极高通滤波0.5Hz左右ICA去伪迹去除眼动、心跳等低通滤波40Hz或100Hz分段成Epochs为什么低通要放到ICA后面因为ICA做成分分离时靠的是不同信号在时间波形上的独立性。眼电伪迹和脑电在低频段有较多重叠但波形形态差别明显ICA恰恰能利用这些形态差异把眼电分离出来。如果你先做了40Hz低通眼电伪迹里比较“锐利”的瞬态成分会被削掉一部分ICA能找到的独立性线索就变少了分离效果会明显变差。这也是我自己实际试出来的教训。有一次处理一批行为学数据我为了省事先把滤波全做完再跑ICA结果ICA成分图里怎么也看不出一眼干净的眼电成分反复调参数都没用。后来把低通挪到ICA后面同样的数据眨眼伪迹成分干净利落地就分出来了。高通放在最前面也有讲究基线漂移如果不先去掉ICA会分出一些“飘移成分”这些成分看起来像是低频振荡容易干扰对真正脑电成分的判断。4.3 滤波参数怎么选以实际数据为准滤波参数里除了截止频率还有几个容易被忽略的选项raw.filter( l_freq0.5, h_freq40, fir_designfirwin, phasezero, padreflect )fir_designfirwin用FIR滤波器而不是IIR。FIR是线性相位对所有频率成分的延迟一致不会造成波形扭曲虽然计算量大一点但脑电分析里波形保真度比实时性重要得多。phasezero零相位滤波输出不会产生时间偏移。如果不设置这个滤波后的ERP波形会出现几毫秒到几十毫秒的延迟做潜伏期分析时会带来系统性误差。padreflect对信号边缘做反射填充减少滤波起始和结束时的边界效应。边界效应是滤波的固有问题——滤波器窗口在数据开头和结尾会“露出半截”导致这两段的数据更不可靠。处理完数据后我会把每段开头和结尾的几十毫秒数据当作潜在问题区分析重点放在时间段中间。滤波效果需要量化验证。我会在滤波前后各做一次频谱对比确认目标频段确实被压下去了、非目标频段没有明显改变而不是只看时域波形觉得“顺眼”就行。再就是滤波后的波形如果出现明显的振铃在某个陡峭波形前后出现等幅振荡说明滤波参数过于激进要降低阶数或放宽截止频率。5. 保存存成什么格式决定了你下一步能不能省事预处理做完了很多人随便存个CSV就交差结果下次要回看某个参数、想换一种滤波方案重跑时原数据已经找不到了processed数据又缺信息只能重新处理。保存这一环值得认真对待。5.1 处理完的数据优先存FIFMNE的原生FIF格式保存的信息最完整通道名、采样率、事件标记、坏道标注、滤波历史全部都会保留raw_clean raw.copy().filter(l_freq0.5, h_freq40) raw_clean.save(sub-01_task-read_clean_raw.fif, overwriteTrue)如果你已经切成了Epochs也可以直接保存epochsepochs.save(sub-01_task-read_epo.fif, overwriteTrue)下次使用只需要一行代码就能把预处理结果完整加载回来不用重新读原始文件、重新滤波、重新分段。我个人的习惯是原始数据不动所有预处理结果都存成带_clean_raw或_epo后缀的新文件文件名里写清楚被试ID和任务这样后续做统计分析、画图直接load对应文件就行。5.2 什么时候导出CSV和MAT虽然FIF格式最全但有时候不可避免要跟别的工具链对接。比如要把脑电数据导入SPSS做统计分析或者把这个数据交给不用MNE的同事CSV和MAT就会派上用场。导出CSVimport pandas as pd data raw_clean.get_data(pickseeg).T * 1e6 # 转置成(时间点, 通道)并转成微伏 df pd.DataFrame(data, columnsraw_clean.info[ch_names]) df.to_csv(sub-01_export.csv, indexFalse)导出MAT给Matlab用from scipy.io import savemat savemat( sub-01_export.mat, { data: raw_clean.get_data(pickseeg), sfreq: raw_clean.info[sfreq], ch_names: raw_clean.info[ch_names] } )导出时有三件事必须处理否则别人拿到数据会一头雾水一是单位MNE内部用伏特统计分析常用微伏我导出前会乘以1e6转成微伏二是采样率一定要写清楚否则做时间轴分析就是猜盲盒三是通道顺序直接raw.get_data()返回的顺序和raw.info[ch_names]是对应的导出的列名也按这个顺序写不要自己重新排序。5.3 保存前过一遍检查清单预处理到保存之间我每次都会过一遍检查清单避免“存完了才发现漏了一步”坏道是否已经标注进raw.info[bads]如果只是肉眼看到某个通道波形异常但没标注后续分析它还会被当作正常通道参与计算。参考电极设置了吗如果用的是平均参考set_eeg_reference(average)这一步执行了吗滤波参数是否记录在案我会把高通、低通、陷波的截止频率、滤波器类型、ICA成分数量等关键参数写进一个JSON文件和数据一起存好。import json params { subject: sub-01, task: read, sfreq: raw.info[sfreq], filter: {l_freq: 0.5, h_freq: 40, fir_design: firwin}, notch: None, ica_components: 20, reference: average, bads: raw.info[bads], n_epochs: len(epochs) } with open(sub-01_processing_params.json, w) as f: json.dump(params, f, indent2, ensure_asciiFalse)这个JSON文件看起来不起眼但过两个月再回来写论文时它就是你还原整个预处理流程的救命稻草。脑电分析最忌讳的就是做完不记录过程最后审稿人一问参数你只能翻聊天记录。我个人的习惯是不管数据多急着分析读取之后一定先花几分钟把图和频谱过一遍再做任何处理。虽然看起来是“浪费时间”但它能帮你避开一整天的无用功。还有一个实操上的小建议如果做的是ERP研究滤波之前的raw也留一份备份。不少审稿人和复现者会要求看到未滤波的原始数据和完整的处理流程手上有这份备份回审意见时会从容很多。