
1. 学习笔记开篇MNE脑电数据处理的第一步该怎么迈在脑电信号处理这个方向上工具选对能省下一大半的力气。MNE-Python是目前处理脑磁图MEG和脑电图EEG数据最主流的Python库之一它的优势在于把读取、预处理、可视化、统计分析、结果展示全部串在了一条流水线上不像过去那样要拿着MATLAB、EEGLAB、Fieldtrip好几个工具来回倒腾数据格式。这篇笔记是我整个MNE学习系列的第一篇聚焦最核心、日常使用频率最高的一整条链路数据的读取、可视化、剪裁、滤波和保存。这套流程解决什么问题很简单就是你拿到一份脑电数据之后在真正做ERP分析、时频分析、源定位之前必须先把数据“收拾干净”。原始EEG数据往往伴随着工频干扰、肌电伪迹、漂移和坏段直接拿去做统计分析结果基本没法看。通过本篇的方法你可以完成从原始数据到“待分析数据”的转换把数据切成你需要的时间窗滤掉噪声频段并保存成可复用的格式。整个思路适合刚接触MNE、正被各种数据结构绕晕的初学者也适合已经用其他工具处理数据、想迁移到MNE上的研究者。我自己最早接触MNE时最大的困惑不是函数不会调用而是搞不清楚数据结构到底长什么样。RAW是什么Epochs又是什么Evoked和它们什么关系这些概念不搞清楚后面每一步都会卡壳。所以这篇笔记不会只丢一堆代码给你我会把每个环节背后的数据结构变化和设计逻辑一起讲清楚这才是MNE用顺手的关键。先给一条主线MNE处理脑电数据本质上就是不断对数据做“标注-切分-变换-汇总”。你读取进来的原始数据是一个连续的时间序列信号经过数据切分裁成事件相关的片段、滤波去掉不要的频段、去伪迹、叠加平均最终得到有统计意义的结果。本篇只讲前半部分也就是从原始连续信号到干净数据段的存储与导出。2. 先从数据读取说起MNE能读懂哪些格式读进来之后数据变成什么样2.1 常见格式与读取函数对照MNE对数据格式的支持相当完善几乎所有主流脑电设备厂商的原始数据格式都能直接读取。我这里列一个常用对照表大家按自己的设备情况对号入座数据来源文件格式MNE读取函数国内医院/科研机构常见的EEG设备导出.edf / .bdfmne.io.read_raw_edf()Neuroscan设备.cnt / .eeg / .datmne.io.read_raw_cnt()BrainVision导出格式.vhdr / .eeg / .vmrkmne.io.read_raw_brainvision()EGI设备.mff / .rawmne.io.read_raw_egi()MNE原生格式保存专用.fifmne.io.read_raw_fif()通用欧洲数据格式.gdfmne.io.read_raw_gdf()自定义纯文本/CSV数据.txt / .csvmne.io.read_raw_custom()/RawArray()最常用的是EDF格式绝大多数医院和实验室导出的数据都是它兼容性也最好。如果是自己采集的原始数据设备厂商通常会提供Python或MATLAB导出接口导出成EDF之后再交给MNE处理是比较通用的做法。2.2 从文件到Raw对象读取时到底发生了什么读取数据时不只是“把文件导入内存”这么简单。MNE会创建一个Raw对象这个对象内部主要包含三个核心部分数据矩阵.get_data()形状是电极通道数采样点数的二维数组里面存的是实际信号幅度值单位一般是微伏µV或伏特V。测量信息.info这是一个字典结构记录了采样率、通道名称、通道类型、参考电极、滤波器状态等元数据。很多初学者容易忽略.info但其实预处理中的大量操作都是在修改它。注释.annotations记录数据时间轴上的标注比如“眨眼”、“静息”、“刺激呈现”等标记。注释在后续切分数据、去除伪迹时作用很大。读取EDF文件的代码只需要一行import mne raw mne.io.read_raw_edf(subj01.edf, preloadTrue)其中preloadTrue表示把数据一次性读入内存。如果数据文件很大可以设成False让MNE按需从磁盘读取这样内存占用小但后续处理会慢一些。我的习惯是只要内存够用一律preloadTrue因为后续滤波、剪裁操作在内存上跑会快很多而且很多函数比如ICA要求数据已经加载到内存。2.3 读取之后别急着干活先给数据做个体检读进来之后我会习惯性先看几样东西print(raw) print(raw.info) print(raw.ch_names)print(raw)会输出数据的基本信息包括通道数、采样率、时间长度raw.info会显示更细的参数raw.ch_names则告诉你每个通道叫什么名字。这里有个常见的坑有些设备导出的EDF文件参考通道和极标记通道会被当作普通EEG通道读进来如果直接做后续处理这些通道会干扰结果。建议用raw.set_channel_types()手动指定raw.set_channel_types({REF: eeg, STI 014: stim})其中REF是参考电极STI 014是刺激标记通道。MNE内置的许多函数依赖stim通道来识别事件标记如果标记通道没配好后续的Epochs切分根本无从谈起。3. 可视化先看见数据再决定怎么处理3.1 数据预览的基础方式整体波形图数据读取后的第一件事永远不应该是急着滤波而是先看数据长什么样。MNE的可视化做得非常出色一句raw.plot()就能打开交互式波形窗口raw.plot(n_channels30, duration10, scalingsauto)参数n_channels30控制每屏显示多少通道duration10表示每一屏显示10秒的数据scalingsauto会让MNE根据信号幅值自动调整显示尺度避免某些通道幅值过大导致其他通道波形被压扁。这个交互窗口实际用起来相当顺手可以横向缩放、切换通道分组、标记坏段甚至可以直接在界面上给数据添加注释。日常处理数据时我一般会开着这个窗口先快速扫一遍全程波形重点看有没有通道整体都是平线电极脱落或短路有没有幅度明显超过其他通道几十倍的“疯狂通道”有没有大段的、全通道同步的漂移可能是受试者大幅动作这里我多说一句经验之谈盲目跑Pipeline而不先肉眼看数据是脑电处理最常见的错误。算法再高级也不能替你发现“你采集的时候受试者手机在桌上震动”这种物理层面的问题。3.2 头皮拓扑图直观看到各通道空间分布做脑电研究的人很多都有“看着波形判断不出哪个脑区活跃”的困扰。MNE提供了raw.plot_sensors()和raw.plot_sensors(kind3d)前者画出通道的二维拓扑分布后者生成三维电极位置图。raw.plot_sensors(show_namesTrue) raw.plot_sensors(kind3d)这个功能主要用于检查电极位置是否合理、有没有电极位置信息缺失。MNE的源定位、拓扑图绘制前置条件就是要电极位置信息。如果读入的EDF文件自带位置信息多数设备导出的EDF会有这一步基本零成本。如果电极位置信息缺失可以用raw.set_montage()手动设置标准电极布局raw.set_montage(standard_1020)standard_1020是国际10-20系统布局大部分脑电帽都遵循这一标准。设置好montage之后后面画拓扑图、做源定位就都顺理成章了。3.3 频谱可视化先了解噪声分布再决定滤波参数我强烈建议在滤波之前先看一眼数据的频谱分布这有助于你确定滤波器的截止频率。MNE提供了简单实用的raw.compute_psd()方法raw.compute_psd(fmax50).plot()这条命令会画出各个通道的功率谱密度。如果数据是在室内采集的你大概率能看到50Hz处有一个尖峰那就是工频干扰如果是欧洲采集的数据会在50Hz处有干扰国内也是50Hz美国是60Hz。如果0-1Hz之间的功率异常高说明存在基线漂移。观察完这些特征之后滤波参数的选择就有了依据而不是拍脑袋定一个0.1-30Hz。可视化这一步看似简单但实际上是最能体现经验的地方。同样的数据会看的人几秒钟就能判断出数据质量、噪声类型、需要做什么预处理不会看的人把所有通道波形的平均图截出来也发现不了问题。4. 数据剪裁不只要会用crop还要理解时间轴的语义4.1 用crop截取感兴趣的时间段现实场景中采集到的数据往往比分析需要的数据长得多。比如一次实验采集了20分钟但有效刺激只集中在第5分钟到第15分钟之间。这时候就需要对数据进行剪裁。MNE中对应的函数是raw.crop()raw.crop(tmin300, tmax900)这行代码把数据剪裁为从第300秒到第900秒的区间。剪裁之后原来的Raw对象被修改了如果你需要保留原始完整数据建议先复制一份raw_original raw.copy() raw.crop(tmin300, tmax900)4.2 关于事件标记的一个重要提示裁剪会改变时间基准剪裁数据后一个容易被忽略的点是剪裁后的数据时间会从0重新开始计算也就是说新的tmin变成了0。与此同时如果你的数据有事件标记比如刺激出现的时间点这些标记的时间戳也会跟着平移。这里有一个实际工作中常踩的坑——如果你想在剪裁数据之后再去找某个刺激的精确时间点不能直接拿着原始标记时间去查因为时间基准已经变了。MNE的解决方案是使用Annotations对象它会自动跟踪注释在时间轴上的相对位置。如果你用read_raw_edf(..., preloadTrue)读取数据时事件标记被自动转换为Annotations那么在crop之后注释会自动调整到新的时间轴。4.3 坏段移除另一种形式的“剪裁”除了裁剪大段时间区间之外日常处理中还经常需要把某几个通道在某个时间段的数据标记为“坏段”。MNE提供了raw.annotations机制raw.annotations.append(onset120, duration2, descriptionbad)这行代码在第120秒到第122秒之间添加了一个描述为bad的注释。加了注释并不等于数据被删除它只是打了一个标签。后续在切分Epochs时你可以决定是丢弃包含bad注释的时间段还是在分析时将这些段标记为异常。如果你的数据中有明显的、大段的噪声比如受试者打哈欠、电极线被碰了一下手动添加bad注释比交给算法去检测要靠谱得多。算法再聪明也没有你对这个实验数据的理解深。4.4 用pick挑选通道只保留你需要的通道crop是对时间轴做剪裁pick则是对通道维度做筛选。很多EEG设备会记录几十乃至上百个通道但你的分析可能只关注其中部分通道比如枕区的O1、O2、Oz或者中央区的Cz、C3、C4。此时可以raw.pick_channels([O1, O2, Oz, Cz])MNE还支持按类型挑通道比如只想保留EEG通道、去掉EOG和ECG通道raw.pick_types(eegTrue, eogFalse, ecgFalse)这一步的价值不仅是减少计算量更重要的是防止非EEG通道如眼电、心电混入后续的ICA或伪迹去除流程导致把真实的神经信号当成伪迹去掉。我见过不少人处理数据时忘了排除EOG通道结果ICA之后把额叶的眨眼相关成分当成信号保留了后患无穷。5. 滤波脑电信号处理中最关键的一步也是坑最多的一步5.1 为什么要滤波不要什么信号都往脑子里装脑电信号EEG由不同频率的神经振荡叠加而成但我们关心的通常只有特定频段。与此同时采集过程中会混入各种噪声50Hz工频、高频肌电、低频漂移、电极极化等现象都会污染信号。滤波的目的就是把不关心频段的信号衰减掉、把关心频段保留下来让后续分析信噪比更高。MNE乃至所有信号处理工具中的滤波本质上都是数字滤波器的应用。设计滤波器的时候要确定几个关键参数滤波类型低通、高通、带通、带阻、截止频率、滤波器阶数、纹波大小。MNE把这一切封装成了非常简单的接口但你要是不理解背后逻辑很可能会用错。5.2 MNE中滤波的两种方式FIR和IIRMNE的filter()函数同时支持FIR有限脉冲响应和IIR无限脉冲响应两种滤波器设计方法。# FIR滤波默认方式 raw_filtered raw.copy().filter(l_freq0.1, h_freq30) # IIR滤波 raw_filtered raw.copy().filter(l_freq0.1, h_freq30, methodiir)FIR滤波器优点很多线性相位也就是说滤波后信号波形不会被扭曲、数值稳定性好、可以用firwin设计多带滤波器。MNE默认使用FIR设计背后调用了scipy.signal.firwin。IIR滤波器的优点是计算效率高、过渡带更窄但存在相位失真问题通常要用filtfilt做零相位双向滤波来补偿。MNE在methodiir时默认使用filtfilt来做零相位滤波。实际处理脑电数据我基本都用FIR因为它能保证不同频率成分的延迟一致这对后续分析时间锁定的ERP事件相关电位特别重要。如果用了非线性相位的滤波器可能会导致不同频率的ERP成分出现不同时间偏移这对毫秒级精度要求的ERP分析是致命的。5.3 滤波参数怎么给截止频率的确定方法关于截止频率没有一个放之四海而皆准的固定值需要根据你的研究问题和数据质量来定。我这里给出一些常见参考研究问题滤波频段建议说明静息态频段分析alpha/beta等0.1 - 40 Hz保留主要节律去掉肌电高频成分ERP成分分析P300、N400等0.1 - 30 Hz低频保留慢波高频限制在ERP成分的主要频带高频振荡分析gamma波段1 - 100 Hz需要保留较高频率对数据质量要求更高去除工频干扰带阻48-52Hz或58-62Hz仅在确认有工频干扰时使用避免过度滤波很多人有一个误区滤波范围越窄越干净。实际上过度滤波会让信号失真尤其是用高通截止频率过高比如高通1Hz以上时会显著衰减与事件相关的慢波成分比如P300的慢波。我的原则是用尽量宽的频带只去掉你确定不需要的频段。5.4 滤波的一条重要提醒先检查边界效应滤波最经典的坑是边界效应。数字滤波器在信号起始和结束位置的数据点会因为“没有足够的历史信息”而产生失真。MNE的解决办法是通过填充技术默认是padreflect来减小边界效应但在数据很短或滤波截止频率很低时边界段的数据仍然可能不可靠。实操中我通常在滤波之后会把每个Epoch首尾的几十毫秒视为“过渡区”在做ERP分析时不会太相信这个区间的数值。另外滤波器的顺序还会直接影响最终数据质量。标准的做法是先做坏段剔除或者至少人工检查数据再做滤波。如果数据中有极端的大幅值瞬态噪声比如电极线被碰了一下的尖峰滤波会把这种瞬态噪声扩散到周围的几百毫秒造成所谓的“振铃效应”。所以务必要记住先把明显坏段标记掉再滤波而不是先滤波再标记。5.5 滑动窗口滤波和滑动平均在脑电里怎么用在网上搜滤波相关的内容经常能看到“滑动窗口滤波”“滑动平均滤波”“高斯滤波”等词。这些在通用信号处理里很常见但它们在脑电处理中的使用场景要区别对待。滑动窗口滤波/滑动平均本质上是一种低通滤波用于平滑数据、去掉高频噪声。但在EEG分析中我不建议直接用滑动平均替代正规的FIR设计。原因很简单滑动平均的频率响应有旁瓣滤波效果不够锐利而且在时间上会模糊信号的快速变化。如果你只是想快速看一下整体趋势比如观察实验全程的基线漂移滑动平均确实直观好用import numpy as np data raw.get_data() window_size 100 # 100个采样点采样率1000Hz时是100ms kernel np.ones(window_size) / window_size smoothed np.convolve(data[0], kernel, modevalid)但如果你是要做严格的ERP分析或频段分析老老实实用MNE的filter()才是正道。高斯滤波在脑电中也有类似问题——它适合图像处理的空间平滑用在时间序列上会引入额外的时域畸变不是EEG处理的常规操作。5.6 滤波效果怎么看滤波前对比滤波后滤波完成后记得再画一次频谱图做前后对比确认噪声被压下去了目标频段被保留下来了raw_filtered.compute_psd(fmax50).plot()对比滤波前后的PSD图能直观地看到50Hz处工频尖峰是否降低了0.1Hz以下的超低频漂移是否被压制了目标频段的总体功率是否保留完整。如果滤波后连眼动伪迹通常集中在低频段、额叶通道波形都明显变形那大概率是滤波器参数设置过激了。6. 数据保存别再每次重跑一遍预处理好吗6.1 为什么保不保存这么重要脑电数据处理的流程通常很漫长从读取、可视化、剪裁、滤波到后续的伪迹去除每一步都可能要反复调参。如果你每次开始新分析都从头读原始数据再跑一遍预处理不仅费时费力还会因为中间某个步骤的细微差异导致结果不稳定。我在实际项目中早就形成了一个习惯一切预处理步骤完成之后先把“半成品”保存下来再继续做后续分析。这样即便后续分析代码写错了想重来也不需要回到最原始的文件重新开始。6.2 保存为MNE原生格式数据保真的最佳选择MNE的原生保存格式是.fif它完整保留了数据矩阵、通道信息、事件标记、注释、滤波器历史等所有元数据读取速度也快raw_filtered.save(subj01_cleaned.fif, overwriteTrue)读取时一行代码恢复raw_clean mne.io.read_raw_fif(subj01_cleaned.fif, preloadTrue)这里的overwriteTrue是MNE 1.0之后版本的要求——文件已存在时如果不加这个参数会直接报错防止误覆盖。这也是MNE的一个贴心设计不过初次使用的人经常被这个报错卡住。6.3 保存为通用格式跨软件协作时的选择如果你需要跟使用MATLAB/EEGLAB的同事协作.fif格式对方打不开这时可以导出为EDF等通用格式raw_filtered.export(subj01_cleaned.edf, overwriteTrue)但要注意导出为EDF会丢失MNE特有的部分元数据比如通道类型自定义信息、部分注释内容。同时EDF格式的数值精度和.fif相比有一定限制。所以如果你确定所有后续分析都在MNE内完成直接保存.fif是最省心的。6.4 保存时候的另一个选择只存数据还是连预处理参数一起存还有一类东西经常被忽略——预处理的参数记录。MNE在.info中会自动记录一些滤波参数比如raw.info[highpass]、raw.info[lowpass]这省了不少事。但像crop的时间范围、坏段标记这种信息MNE并不会自动记录。我的做法是在保存.fif之前把预处理的每一个关键参数写进数据文件名称或单独保存一个json文件import json preprocess_params { crop: [300, 900], filter: {l_freq: 0.1, h_freq: 30, method: fir}, bad_channels: raw.info[bads] } with open(subj01_preprocess_log.json, w) as f: json.dump(preprocess_params, f, indent2)这样做的好处是三个月后你回来重新看这批数据还能准确知道当时是如何处理的。科研数据的可复现性很大程度上就体现在这些细节里。7. 整合案例从原始EDF到干净可复用数据的完整代码理论学习再多不如直接跑通一遍完整代码。下面这段是我平时处理数据的一套标准流程压缩了上面提到的大部分操作可以直接复制下来按自己的文件路径和数据情况调整import mne import json # 1. 读取数据 raw mne.io.read_raw_edf(raw_data/subj01.edf, preloadTrue) print(raw) # 2. 数据体检查看通道与基本信息 print(通道列表, raw.ch_names) print(采样率, raw.info[sfreq]) # 3. 设置通道类型与电极位置 raw.set_channel_types({REF: eeg, STI 014: stim}) raw.set_montage(standard_1020) # 4. 可视化原始数据人工观察噪声 raw.plot(n_channels30, duration10, scalingsauto) # 5. 添加坏段注释示例第10秒到第15秒 raw.annotations.append(onset10, duration5, descriptionbad) # 6. 剪裁到有效时间范围 raw.crop(tmin30, tmax600) # 7. 只保留EEG通道 raw.pick_types(eegTrue, eogFalse, ecgFalse) # 8. 滤波 raw_filtered raw.copy().filter(l_freq0.1, h_freq30, methodfir) # 9. 滤完波再看一眼频谱图和波形 raw_filtered.compute_psd(fmax50).plot() raw_filtered.plot(n_channels30, duration10, scalingsauto) # 10. 保存预处理后的数据 raw_filtered.save(processed/subj01_cleaned.fif, overwriteTrue)按照这个流程走下来你手里会得到一份干净的、可以直接用于Epochs切分和ERP分析的MNE数据文件。之后不管你是要做事件相关电位叠加平均还是要做时频分析都从这个_cleaned.fif开始就行不用再碰原始数据了。我特别想强调第4步和第9步的两次可视化第一次是为了发现问题第二次是为了确认处理效果。整个预处理过程中可视化应该贯穿始终而不是只在最开始看一眼。很多人写代码从头到尾跑完不画图最后发现处理结果有问题还得回头查哪一步出了问题返工时间比画图多十倍。8. 常见问题排查我踩过的坑和对应解决办法8.1 读文件报错No data files found这是我见过最频繁的报错原因是路径写错了或者文件名拼写大小写不对。EDF文件的后缀名通常是小写.edf但也有人拿到.EDF在Linux/macOS上这两个不一样。解决办法是不要手动敲路径用os.listdir()列出目录内容来确定准确文件名。8.2 通道名打印出来全是数字有些设备导出EDF时通道名保存为EEG 1、EEG 2这种自动编号完全没有电极名称信息。这种情况下set_montage(standard_1020)会因为通道名不匹配而报错。解决方法是先手动重命名通道channel_rename_mapping {EEG 1: Fp1, EEG 2: Fp2} # 按实际顺序映射 raw.rename_channels(channel_rename_mapping)这一步需要你自己知道设备导出的通道顺序对应哪里的电极通常设备厂商的文档里会有说明。8.3 滤波之后波形振幅整体变小如果滤波后波形整体振幅明显降低多半是滤波频带设置得太窄把你关心的信号幅度也衰减掉了。解决办法是第一件事检查频谱图确认目标频段的功率是否在滤波后基本保留。8.4 保存后重新读取注释丢了save()之后重新用read_raw_fif()读取注释通常会保留在.annotations里。但如果你用export()导出成EDF再读回来MNE的注释和刺激事件标记可能会丢失或错位。原因在于EDF格式对注释的原生支持有限。解决办法是直接保存.fif不到万不得已不要用EDF做中间格式。8.5 数据太大内存报错对于数小时的连续EEG数据一次性preloadTrue可能会导致内存溢出。解决办法有三条路一是只读取需要分析的时段可以先用read_raw_edf不加preload调用crop后再load_data()二是分段读取分析三是降低采样率在确认目标频段之后用raw.resample(250)把数据降到250Hz能大幅减少内存和计算量。9. 这篇笔记之外后续还能做什么第一篇笔记的内容到这里就算完整了。数据读取、可视化、剪裁、滤波、保存这套流程是MNE使用的底座后面的所有分析都建立在这个基础之上。我自己在实际项目中的体会是预处理阶段的每一步都值得慢下来想清楚这个参数是依据什么定的这个操作会改变数据的哪些性质这个保存格式是否完整保留了可复现所需的信息。带着这些思考去处理数据远比复制粘贴别人的代码跑完一遍更有收获。最后再分享一个技巧无论处理谁的数据请在代码开头固定设置随机种子np.random.seed(42)在保存文件时保留原始文件名与处理参数的对照记录。脑电分析的可复现性问题很多时候不是仪器误差导致的而是处理流程不规范造成的。把这件事情做好你的分析结果才经得起检验。下一篇学习笔记可以围绕Epochs切分与ERP叠加平均展开就是把这个.fif文件变成真正的“事件相关电位”波形图的过程到时候我们继续。