
做脑电数据处理这几年MNE-Python一直是我最顺手的工具包没有之一。这篇笔记是我MNE脑电信号处理学习笔记的第一篇主题是脑电数据最基础也最绕不开的一整套流程数据的读取、可视化、剪裁、滤波和保存。你可能会说这不就是几个函数的事吗确实MNE把很多操作都封装成了几行代码但真正上手时格式选不对、参数没配好、滤波顺序错了、保存格式不合适都会让你抓狂半天。这篇笔记就是专门给刚接触MNE、准备拿自己脑电数据练手的同学看的把我踩过的坑、常用的参数配置、筛选逻辑都写清楚让你少走弯路。1. MNE是什么脑电数据处理的第一站1.1 MNE解决的三个痛点MNE-Python下文统一叫MNE最初是专门为处理脑磁图MEG和脑电图EEG数据设计的开源Python库后来成了整个神经科学领域处理电生理信号的事实标准之一。它能把从原始脑电文件到特征分析这条长链路吃干净对我这种既要处理几十个通道连续记录、又要切分事件相关电位、还要做时频分析的研究人员来说相当于把“读文件、预处理、切段、统计”这些活打包成了一整套标准零件。它解决了我三个长期痛点。第一脑电数据格式五花八门EDF、BrainVision、BDF、NeuroScan、EEGLAB的set文件每个厂商的脑电文件长得都不一样MNE把这些读取接口统一成了一个模式基本是“换一个read_raw_xxx函数后面的处理逻辑完全一致”。第二脑电信号里噪声太多眨眼、肌电、电极松动、市电工频MNE提供了滤波、ICA去伪迹、坏通道插值、重参考等一系列标准流程不需要自己用numpy从头写滤波函数。第三脑电数据量大动辄上百MB甚至几个GBMNE的Raw对象用内存映射的方式处理数据不强制把所有数据一次读进内存在实际操作中非常灵活。1.2 环境搭建与数据准备MNE的安装很简单直接pip装就行pip install mne如果你要在Jupyter Notebook里做交互式可视化我建议再加两个包pip install jupyter ipympl然后在Notebook里输入%matplotlib widget这样出来的脑电波形就能放大、拖动、测量比静态图片好太多。我做预处理的时候基本全程开着这个交互模式眼睛盯住波形随时判断滤波参数合不合适。除了MNE本身日常处理脑电几乎避不开numpy和pandas建议一并装上。数据方面你可以用自己手里的脑电数据也可以去公开数据集找比如PhysioNet上有大量带注释的睡眠脑电、癫痫脑电数据集格式多为EDF正好适合用来练手。我自己初期就是拿一个包含眼电伪迹的睁眼闭眼静息态数据来试流程因为这类数据特征清晰滤波和ICA效果很容易看出来。2. 数据读取把脑电文件变成Raw对象MNE里所有的连续脑电数据都被封装成Raw对象后面所有操作——可视化、滤波、剪裁、保存——几乎都是围绕Raw对象展开的。所以第一步的关键就是把不同格式的原始文件读成Raw。2.1 读取EDF格式最通用的起步姿势EDFEuropean Data Format是最通用的脑电数据格式之一很多医院设备和公开数据集都用它。MNE读取EDF的代码非常直接import mne raw mne.io.read_raw_edf(eeg_data.edf, preloadTrue) print(raw.info) print(raw.ch_names)这里有个参数重点说一下preload。它的作用是“是否把全部数据读进内存”。我个人的习惯是如果文件不大几十MB直接preloadTrue后续每次访问数据都快很多如果文件特别大或者你只想先看看通道信息、做几秒钟试读就用默认的preloadFalse此时MNE是按需从磁盘读取数据内存占用低但后续每次做实际运算时它会自动帮你load速度慢一些。读取之后raw.info会输出一大串信息包括采样率sfreq、通道数nchan、通道类型、测量日期、文件内部的事件标记数量等。我每次读完文件先不急着看波形一定先扫一眼raw.info确认采样率跟我预期一致、通道名没有乱码、有没有畸形通道名因为后面所有操作都建立在info正确的前提下。2.2 读取BrainVision格式BrainVision格式.vhdr .eeg .vmrk在科研圈也很常见MNE同样提供了专门函数raw mne.io.read_raw_brainvision(demo.vhdr, preloadTrue)这个函数会根据.vhdr文件里记录的参数自动找到对应的.eeg数据文件和.vmrk事件标记文件。读取后建议立刻看一眼raw.info[sfreq]因为BrainVision格式允许不同通道用不同采样率不推荐但存在一旦各通道采样率不一致MNE会以最大采样率重采样并产生警告你最好提前知道这一点。其他常见读法再列两个备用BDF格式用mne.io.read_raw_bdfEEGLAB导出的.set文件用mne.io.read_raw_eeglab。用法大同小异注意安装相应的依赖包如pyedflib、pymatreader即可。2.3 读取数据时的三个高频坑读取阶段我实际踩过、也帮别人排查过的坑主要有三个。第一个坑是通道名不标准。有的设备会导出类似EEG Fz、Fz-REF这种夹杂额外字符的通道名或者干脆把两个通道合并成一个字符串。这会导致后面做电极定位set_montage时完全匹配不上。解决办法是读取后先统一清洗比如用raw.rename_channels批量改名或者用字典把通道名映射成标准名称。第二个坑是参考电极与接地电极的类型问题。有的EDF文件把参考通道标记为EEG类型导致后续pick_types(eegTrue)时把参考也算进去了。我通常会在读取后用raw.set_channel_types手动把参考通道改成REF类型或者直接用raw.drop_channels把多余通道删掉。第三个坑是单位不统一。MNE读取不同厂商数据时有些原始数据用的是µV有些是V有些甚至带着奇怪的缩放因子。读取后先统计一下各通道的信号幅度范围比如计算raw.get_data().std(axis1)可以快速判断有没有哪个通道幅度离谱。如果读出来信号小到1e-10量级多半是单位换算没对上要检查设备说明书里的单位定义。3. 可视化处理之前先把数据“看明白”脑电处理有一条铁律先看图再动手。没有可视化你根本不知道数据里是啥情况——是基线漂移严重还是工频干扰很大还是某个通道已经掉了。MNE提供了好几层可视化工具我从最常用的讲起。3.1 原始波形怎么看raw.plot()查看原始脑电波形的函数是raw.plot()我用得最多的配置是这样的raw.plot( n_channels10, # 一屏显示10个通道 duration5, # 每屏显示5秒数据 scalingsauto, # 自动调整幅值显示范围 blockTrue # 窗口阻塞关掉窗口才继续运行 )blockTrue这个参数对我来说非常重要。如果脚本里后面还有自动处理步骤不加block的话波形窗口一闪而过连看的机会都没有加上block代码会停在这里直到你手动关掉窗口再继续往下跑。这在调试流程时特别顺手相当于一个可视化断点。打开波形窗口后不要只盯着波形来回翻。我一般先看三个东西有没有某个通道的波形幅度比其他通道大一个数量级多半是坏通道或电极松动有没有明显的、周期性的50Hz噪声叠加在信号上波形看起来像毛茸茸的锯齿有没有大幅度的低频漂移整段波形像波浪一样上下起伏。这三种情况对应后续不同的处理策略坏通道要么标记要么插值工频噪声靠陷波漂移靠高通滤波。另外raw.plot还有标记坏通道的交互功能。在波形窗口里直接点击某个通道名它就会被标成红色并且同步写入raw.info[bads]。这个操作看起来不起眼实际上非常提升效率我每次先花几分钟把明显坏的通道点掉后续处理就省很多事。3.2 通道信息与电极位置可视化如果数据里带电极坐标你可以把电极位置画出来检查有没有放错位置的情况montage mne.channels.make_standard_montage(standard_1020) raw.set_montage(montage) raw.plot_sensors(show_namesTrue)这里分两步先根据通道名创建一个标准的10-20系统电极位置模板再把这个模板赋给Raw对象。如果通道名和模板里的电极名匹配不上MNE会直接报错或者自动忽略不匹配的通道。这个步骤常被新手忽略但它对后面做topography地图、源定位、ICA去除眼电伪迹都很关键。没有电极位置很多可视化功能直接废掉。有一个小技巧如果标准模板里有部分电极匹配不上多半是通道名大小写、多余字符问题先清洗通道名再set_montage能解决大部分报错。3.3 频谱可视化功率谱密度处理脑电只盯着时域波形是远远不够的频率域信息同样关键。MNE里看功率谱的写法在不同版本里有变化新版本推荐用compute_psdraw.compute_psd(fmax50).plot()这里fmax50表示只画到50Hz。不同频段对应不同生理意义delta波1-4Hz在深睡眠多theta波4-8Hz在困倦和记忆任务中常见alpha波8-13Hz在睁眼闭眼放松状态非常明显beta波13-30Hz与主动思考、运动相关。我拿到一段未知数据后先看PSD能快速判断数据大致属于哪类实验。更关键的是PSD能清楚暴露噪声的类型。50Hz附近出现又尖又高的峰就是典型工频干扰30Hz以上整体抬高多半是肌电噪声全频段均匀抬高可能是放大器底噪低频段异常抬高往往是基线漂移或电极接触不良。每次做完滤波我都会再画一次PSD对比确认目标频段压下去了、该保留的成分没被误伤。4. 数据剪裁去坏段、选通道、切事件原始脑电记录往往很长中间可能有休息、有无关事件还有被受试者动作污染的片段。剪裁分为三个维度时间维度上切取感兴趣区段通道维度上去掉不需要的通道事件维度上把连续数据切成一段一段的事件相关数据。4.1 时间维度裁剪cropcrop是最直接的时间剪裁它按时间早晚切一段raw_cropped raw.copy().crop(tmin10, tmax120)这段代码把数据切到第10秒到第120秒。这里有两件事要提醒。第一建议先raw.copy()再crop因为crop默认是在原对象上直接修改。虽然它只是修改元数据、不实际释放原来的数据块但如果你后面还要用原始数据做对比先copy一份更稳妥。第二crop之后数据看起来变短了但raw.info里面的“测量日期”等元数据不会变所以如果你需要记录处理链条最好自己另外保存一个处理日志别指望自动留痕。crop很常用但有一个细节处理脑电时如果滤波或ICA之后前几秒经常因为边界效应变得不可用很多人会选择先滤波、后crop把受污染的边界直接切掉。这是一个低成本且有效的处理顺序。4.2 通道维度选择pick系列数据里不一定每个通道都有用比如有的文件把心电、呼吸、血氧都记录进去了分析脑电时就得把它们挑出来单独处理。MNE提供了多种pick用法# 按通道名精确选择 raw_eeg_names raw.pick(picks[Fz, Cz, Pz]) # 按通道类型选择 raw_eeg raw.pick_types(eegTrue, eogFalse, stimFalse) # 排除坏通道 raw_good raw.pick_types(eegTrue, excludebads)我习惯用pick_types(eegTrue)来框定所有脑电通道因为它对通道类型的识别比名称更鲁棒。excludebads这个参数建议每次都写上它会把之前标记为坏通道的那些通道自动排除避免它们混进后续分析里。需要注意的是pick类操作默认也直接修改原对象需要用copy()保护原始数据的话就要先复制。另外pick之后如果通道数太少比如只剩两三个通道后续做ICA或源定位可能会因为信息不足效果极差这时候要想清楚为什么要drop别手滑把关键通道删了。4.3 事件切分从连续数据到epochs做事件相关电位分析时连续数据只是原料真正的分析单元是epochs——围绕每个事件标记截取的一段固定长度数据。MNE切epochs的核心是事件events数组。事件数组是一个n x 3的整数数组每一行包含三个数事件发生的时间点单位为采样点不是秒、前一个事件的数值MNE用的占位值、事件类型编码。要从连续脑电里提取事件先要找到事件标记所在的刺激通道events mne.find_events(raw, stim_channelSTI 014) print(events[:5])这里的STI 014是很多EDF文件里默认的刺激通道名。不同设备、不同采集软件刺激通道名字可能不同有的叫Trigger有的叫Status甚至有的把刺激信息编码在某个模拟通道里。我第一次处理一个EDF文件时死活找不到事件后来发现该文件的刺激信息在STI 014通道里而不是我以为的那个通道。遇到这种情况建议先把所有通道名列出来找名字里带STI、TRIG、Trigger、Event、Digital之类的通道。拿到events之后再定义你要切哪些类型的事件创建epochsevent_id {visual_stim: 1, auditory_stim: 2} epochs mne.Epochs( raw, events, event_id, tmin-0.2, # 事件前200ms tmax0.8, # 事件后800ms baseline(None, 0), # 基线校正到事件前 preloadTrue ) print(epochs)这里tmin-0.2表示从事件前200毫秒开始截取tmax0.8表示截到事件后800毫秒。基线校正baseline(None, 0)的意思是取事件前所有数据作为基线把这段的平均值从整个epoch中减去能有效消除直流漂移带来的基线差异。创建epochs后我建议第一时间检查两个东西一是epochs.drop_log看有多少个epoch因为超出数据边界或包含坏段被自动丢弃了二是epochs.plot()随机看几十个epoch确认切出来的数据和事件对齐是否正确。这一步千万别省如果事件延迟有偏差后面所有的ERP结果都会出问题。5. 滤波选对参数事半功倍滤波是脑电预处理里最需要“知其所以然”的一步。MNE的滤波代码很简单难的是理解为什么这样设参数、这样设会带来什么后果。5.1 滤波参数怎么定从生理信号说起脑电滤波一般分三类高通滤波、低通滤波、陷波滤波。高通滤波用来去掉低频漂移常见参数是0.1Hz到1Hz低通滤波用来去掉高频噪声常见参数是30Hz到100Hz陷波滤波用来去掉特定频率的干扰最常见的是50Hz或者60Hz对应市电工频。怎么选具体数值取决于你要分析什么信号。静息态功能连接分析通常用0.1-40Hz事件相关电位分析往往用0.1-30Hz或0.5-30Hz高频的gamma振荡研究低通会放到100Hz甚至更高。如果只是做个通用预处理0.1-40Hz是很多人的默认区间既能保留大部分生理信号又能滤掉绝大多数高频肌电和漂移。市电频率要根据你所在国家来定国内是50Hz北美有些地区是60Hz。MNE里有专门的陷波函数raw_notched raw.copy().notch_filter(freqs50)我看到有人会一次性列出多个陷波频率来滤基波加谐波比如50、100、150Hz但我的经验是先做低通滤波把40Hz以上的信号整体压掉一部分那么50Hz陷波其实压力小很多如果你的实验根本不关心40Hz以上的频段低通滤波本身就能干掉大部分工频陷波只是精修。5.2 滤波实操代码带通和陷波组合在一起的实际代码是这样的# 先做带通滤波 raw_band raw.copy().filter(l_freq1, h_freq40) # 再做50Hz陷波 raw_clean raw_band.notch_filter(freqs50)执行完后我强烈建议做两件事看一眼raw_clean.plot()确认波形没有明显变形再看一眼raw_clean.compute_psd(fmax60).plot()确认频段特征符合预期。MNE滤波默认是零相位FIR滤波也就是说滤波不会引入相位偏移这对ERP分析极其重要——如果你用的工具会引入相位延迟事件相关电位的时间点就会偏离真实位置。但零相位FIR不是没有代价它需要更长的滤波器阶数计算量大而且边界会有振铃效应。解决办法一是滤波后裁掉首尾一小段二是尽量在数据量足够多时先滤波后切割。5.3 滤波的边界效应与顺序问题我刚开始用MNE滤波时总喜欢在crop之后直接filter结果发现每次滤波后数据开头总有一段不自然的大幅波动这就是边界振铃效应。滤波器在数据开头拿不到足够的历史数据就会出现这种人为伪差。现在我的习惯是先滤波再crop掉边界那段受影响的区域或者直接先filter再切epochs因为epochs是切在数据中间的不受边界影响。滤波顺序方面常规做法是先做高通去漂移再做低通去高频噪声最后陷波去工频。实际上只要不是特别离谱顺序对结果影响不算大。但有一类特殊情况要小心如果后续要跑ICA去眼电伪迹我一般只先做1Hz高通滤波不做40Hz低通因为高频成分丢掉太多ICA分离眼电和脑电的效果会变差。ICA做完之后再补低通滤波。这就是一个典型的“按步骤调整滤波”的场景。还有一个小细节滤波会对幅度有影响这是正常的因为带通滤波本身就是对频段加权。做ERP分析时不同被试用了不同滤波参数会让结果完全不可比所以同一实验里一定要统一滤波参数并且在文章里明确报告。MNE里可以用raw.info[highpass]和raw.info[lowpass]来记录高通和低通的截止频率保存数据后再次读取依然能看到是个不错的元数据保留方式。6. 数据保存处理结果如何落地处理完之后数据总要存下来。MNE提供了很完善的保存机制但不同的保存方式对应不同的下游需求不能随手存一个格式就完事。6.1 保存fif格式MNE的原生格式MNE的“原生格式”是.fif保存只需要一行raw_clean.save(sub01_clean_raw.fif, overwriteTrue)用fif格式保存的好处是你处理过程中的所有元信息都会跟着数据一起保存通道类型、采样率、滤波参数、坏通道列表、电极位置、事件标记甚至你手动添加的注释一并保留。下次mne.io.read_raw_fif读回来基本就是接着上次处理完的状态继续往下做断点续跑非常方便。对于epochs级别的数据同样有对应的保存方式epochs.save(sub01_epochs-epo.fif, overwriteTrue) epochs_data mne.read_epochs(sub01_epochs-epo.fif)如果是多人数据集每个人一个fif文件再统一管理是目前我见过最整洁的方案。不建议把所有人的数据都揉进一个超大文件MNE对文件大小和加载速度都会有压力。6.2 导出EDF、CSV等其他格式fif虽然好用但有时候合作方、别的工具或平台不认识fif格式这时候需要导出成通用格式。MNE从较新版本开始支持直接导出EDFraw_clean.export(sub01_clean.edf, fmtedf)导出EDF适合把预处理后的数据交给医院系统或老牌分析软件处理。注意不是所有数据结构都能完美导回EDF比如某些特殊通道类型或刺激编码规则可能丢失导出后最好重新读取一遍检查通道名和事件标记是否完好。如果你想把脑电数据当成普通数值矩阵用pandas、Excel或者其他机器学习库来分析那最常见的做法是导出CSVimport pandas as pd data raw_clean.get_data() # 返回 numpy 数组形状(通道数, 采样点数) df pd.DataFrame(data.T, columnsraw_clean.ch_names) df.to_csv(sub01_eeg.csv, indexFalse)拿到CSV之后你就能用任何熟悉的数据分析工具来处理脑电数据了。但记住CSV是纯数值文本采样率、通道类型、电极位置这些元信息全部丢了所以导出CSV只是“最后一步送数据出去”的做法不建议作为中间处理环节的存档。6.3 保存实操要点保存这块我踩过最值得说的坑有两个。第一个坑是中途处理完直接覆盖了原始文件。早期我在Notebook里调试一个不留意raw.save(原始数据文件名.fif)把原始数据覆盖了。后来我养成一个硬性习惯原始数据永远只读不写任何中间产物都保存成带处理步骤后缀的新名字比如sub01_raw_orig、sub01_clean_1_40Hz、sub01_epochs_ica。这样处理流程可以回退、可以复现一旦发现后面某步出了问题还能回到上一步重新来。第二个坑是保存前不记得drop坏通道。如果你在可视化阶段点击标记了一些坏通道但没执行raw.drop_channels(raw.info[bads])或后续的ICA插值那么保存出来的数据里依然带着坏通道只是通道名不太一样或数据还是脏的。所以保存前我会习惯性检查一下raw.info[bads]和raw.annotations确认我自己知道的数据状态和实际数据文件是一致的。7. 常见问题与排查技巧实录最后把这套流程里经常遇到的典型问题整理成一张直观的速查表都是我自己实际踩过或帮人排查过的案例供你遇到类似情况时对症下药。问题现象可能原因排查与解决办法读取EDF报错或通道乱码非标准EDF头信息、编码问题用mne.io.read_raw_edf(..., verboseerror)缩小范围检查通道名是否含空格/特殊字符raw.plot()窗口一闪而过脚本没有阻塞等待窗口关闭加上blockTrueNotebook环境改用%matplotlib widget找不到事件标记刺激通道名称不对或刺激编码不在这条通道先print(raw.ch_names)找含STI/Trigger/Status/Event的通道用mne.find_events时调整min_duration参数滤波后数据开头出现大波动滤波器边界效应先滤波后crop或者先切epochs再滤波避开数据边界50Hz工频滤不干净低通截止没设好或陷波参数不对先低通滤波压掉高频再陷波检查是否用了notch_filter(freqs50)PSD里低频异常高基线漂移或电极接触不良使用高通滤波检查原始记录时段的电极阻抗记录保存后的fif文件打开没通道位置信息没执行set_montage保存前确认raw.get_montage()不为空导出的CSV特别大Excel打不开脑电数据本身采样点多降低采样率后再导出或只导出分析需要的通道和时段除了这张表我再分享一个排查习惯。遇到数据异常时不要急着改参数再跑一遍先逐层定位先看原始数据是否正常再看处理后的数据是否正常然后用二分法找到是哪个函数、哪个参数引入的问题。具体操作上我会用raw.copy().crop(tmin0, tmax10).get_data()把一小段数据拿出来打印最大值、最小值、均值对比处理前后的数值变化。脑电数据的尺度一般在几十到几百微伏之间如果你看到处理后的数据数值大到几千十有八九是单位或者参考问题而不是滤波参数问题。最后再分享一个小技巧每个处理步骤的关键参数建议直接以注释的形式写进脚本并且用raw.info里能保存的字段记录下来比如设置前记得raw.info[highpass]和raw.info[lowpass]已经对应上你用的滤波频率。脑电预处理的“可复现性”比你想象的重要几个月后回来整理数据写论文时你会发现当初随手写的参数记录、坏通道记录有多值钱。