简介:EEGLab V10.0.0.0b.rar 是面向脑电(EEG)分析学习者与神经科学、心理学、生物医学工程研究者的开源工具安装包,可解决从原始信号检查、预处理到事件相关电位、频谱分析与源定位等分析流程的软件环境搭建问题,也适合希望将脑电数据与机器学习、脑机接口及人工智能方法结合的中高级用户。压缩包体积约182.31MB,文件总数与类型明细上游暂未提供,具体目录和文件构成需以实际解压结果为准。资源已有197人浏览学习,热度可供参考。其价值在于提供一套较完整的EEG分析工具入口:读者可围绕去噪、滤波、眼动校正、平均参考等预处理环节建立规范流程,并进一步开展ERP分析、频域分析和源定位实践;对跨学科研究而言,还可作为连接神经影像、生理信号与分类算法实验的基础平台,便于后续开展脑状态识别与模型验证。
1. 从一份 2010 年的压缩包说起:EEGLab 10.0.0.0b 还值不值得拆
脑电分析这件事,本质上是信号处理、神经生理和机器学习三者的交叉学科活儿,而 EEGLab 恰好是这条链路上绕不开的一站。10.0.0.0b 这个版本号看着很旧,但它对应的是 MATLAB 图形界面还完全靠句柄控件撑起来的年代,包体小、依赖少、函数名干净,pop_系列和底层eeg_系列的分层非常清楚。我手上的场景很典型:一台装不了新版 MATLAB 的工控机,接的是老式放大器的 64 导采集,需要把原始连续数据一路做到能进分类器的分段数据。这个包能解决的就是这条完整链路——导入、滤波、重参考、分段、ICA 去伪迹、导出特征。适合谁?手上有旧系统、要做被试内小样本实验、又不想被新版工具箱的依赖链拖住的人。但它也有明确的版本边界,后面几章会一条条摊开。
2. 解包与首启自检:目录结构、MATLAB 依赖和四条验证命令
2.1 版本边界:这个包里有什么、没有什么
先把预期打平。10.0.0.0b 属于 EEGLab 的 10.x 分支,核心是一套 MATLAB 的.m函数加上一批 GUI 回调,数据处理逻辑全部在函数里,GUI 只是壳。这意味着即使 GUI 在新版 MATLAB 上显示错位,命令行依然能跑通全流程——这一点在后面的排错章节会反复用到。
这个版本里能用的关键函数族大致是这些:
| 功能层 | 函数 | 说明 |
|---|---|---|
| 容器与 IO | pop_loadset/pop_saveset/pop_loadcnt/pop_biosig | set 是主格式,cnt/bdf/edf 靠对应函数或插件 |
| 预处理 | pop_eegfilt/pop_reref/pop_resample/pop_rmbase | 滤波走的是老的eegfilt实现 |
| 分段 | pop_epoch/pop_select/pop_importevent | 事件结构分event与urevent两层 |
| 分解 | pop_runica/pop_subcomp/pop_selectcomps | 默认 Infomax,可切 fastica/jader |
| 检查 | pop_eegplot/pop_spectopo/pop_topoplot/pop_erpimage | 全部基于老式 figure |
| 自动剔除 | pop_jointprob/pop_rejkurt/pop_rejepoch | 基于概率与峰度的粗筛 |
没有的东西同样重要:这个版本里没有pop_eegfiltnew,也就是那套基于 Hamming 窗零相位 FIR 的新滤波实现;没有 ICLabel 这类自动成分分类;没有 clean_rawdata 那套 ASR 处理。很多人拿着新版的教程去套这个包,第一步就在函数名上翻车——pop_eegfiltnew在这个版本里根本不存在,报错信息还是含糊的 "Undefined function"。
所以定位很清楚:这是一个功能完整但工具链偏老的版本,够用,但不能照抄新教程。
2.2 三条命令把环境装干净:解压、路径、启动验证
第一步是解压。压缩包是 rar 格式,Linux 下用 unrar,Windows 下用 7-Zip 都可以。这里有个血泪经验:解压路径不要带中文、空格和括号。MATLAB 的addpath对这类路径的处理在不同版本上不一致,后面pop_loadset找不到文件时你根本想不到是路径的问题。
# 建一个纯英文、无空格的目录,避免后续 addpath 与文件读写出问题 mkdir -p /opt/eeg/eeglab10 unrar x EEGLab_V10.0.0.0b.rar /opt/eeg/eeglab10/ # 看一眼解压出来的顶层结构,确认 eeglab.m 在根目录下 find /opt/eeg/eeglab10 -maxdepth 2 -name "eeglab.m" -o -maxdepth 2 -type d | head -30命令的逻辑很直白:先建目录,再解压,最后定位eeglab.m的位置。最后那步不能省,因为有些压缩包会多套一层目录,解压出来是eeglab10/EEGLab_V10.0.0.0b/eeglab.m,如果你按第一层去addpath,启动就会失败。参数上-maxdepth 2是为了不把 plugins 下的几百个子目录全刷出来。
第二步是在 MATLAB 里挂路径。这里有个反直觉的点:不要用addpath(genpath(...))把整个目录树一次性加进去。genpath会把 plugins 下所有子目录、示例数据目录、旧版本备份目录全部塞进搜索路径,一旦出现同名函数(比如某个插件自带了一份改过的eegfilt.m),MATLAB 调用哪个取决于路径顺序,行为就变成了玄学。
% 只把根目录加进路径,其余子路径由 eeglab.m 自己按需添加 root = '/opt/eeg/eeglab10'; addpath(root); % 启动,GUI 会弹出并自动把 functions/ 与 plugins/ 写入路径 eeglab; % 如果是无界面环境,用下面这行代替,跑完批处理再退出 % eeglab('nogui');addpath(root)只加一层,这是刻意为之。eeglab函数内部会读取自身的安装位置,把必要子目录加进去,同时初始化一批全局变量。如果你在无显示器的服务器上跑,eeglab('nogui')能跳过界面初始化,但注意这个参数在老版本里的行为不完全等价——它仍然会尝试读一遍配置文件,所以第一次运行最好还是带界面跑一次,让配置落盘。
第三步是验证。别急着导数据,先跑四条断言,把环境问题挡在数据问题前面:
% 四条验证,任何一条为 0 都说明环境没装对 checks = { ... 'eeglab', exist('eeglab','file') == 2; ... 'pop_loadset', exist('pop_loadset','file') == 2; ... 'runica', exist('runica','file') == 2; ... 'eegfilt', exist('eegfilt','file') == 2; ... }; for k = 1:size(checks,1) fprintf('%-12s : %d\n', checks{k,1}, checks{k,2}); endexist(...,'file')返回 2 表示找到.m文件,返回 0 就是没挂上。为什么单独测runica和eegfilt?因为这两个是后面 ICA 和滤波的入口,前者藏在functions/的子目录里,后者是老滤波实现的底层函数,路径没加全的时候它们最容易缺失,而pop_系列反而因为都在浅层目录里,能过。
2.3 首次启动要看的四个信号
启动之后,命令窗口会打印一串路径信息和版本号。别急着关,看四样东西。
第一,看有没有 warning 提到图形句柄或uicontrol属性。这个版本的 GUI 是照着老式 MATLAB 图形系统写的,在较新的 MATLAB 上(图形系统换过一代之后),部分控件可能错位、回调失效或者菜单点不开。看到这类警告不代表不能用,但要有心理准备:接下来的操作尽量走命令行,GUI 只用来查看波形和地形图。
第二,看plugins目录有没有被识别。命令窗口一般会列出加载到的插件名。如果列表是空的,说明根目录指错了,你指的可能是上一层的父目录。
第三,跑一次pop_loadset的空调用,看它是否有反应。第四,随手help pop_epoch一下,能出帮助文本就说明路径基本没问题。
我一般的习惯是:第一次装完立刻把上面那四条断言存成check_env.m放在项目目录里,换机器、换 MATLAB 版本、升级工具箱之后先跑一遍这件事,能省掉大量"到底是数据坏了还是环境坏了"的纠结。
3. 原始脑电到可训练数据:导入、滤波、重参考、分段的参数怎么定
这一章是整条链路里最容易做废数据的一段,因为每一步都有默认值,而默认值几乎从来不适用于你的实验。下面按顺序过,每一步给出可抄的命令、参数含义和失败时看什么。
3.1 数据落盘格式与导入选择:set/fdt 双文件机制
先搞清楚 EEGLab 自己的存储格式,这是后面所有坑的根源。.set文件本质是一个 MATLAB 的.mat文件,里面存的是EEG结构体:通道位置、事件、采样率、数据维度等。数据本体有两种存法:
- 单文件:所有数据都塞在
.set里,文件大但只有一个。 - 双文件:
.set只存元信息和指针,实际数据单独存成.fdt(浮点二进制)。
双文件模式在老版本里是默认行为之一,尤其是数据量大的时候。这就是第一个致命坑:只拷.set不拷.fdt,文件在,数据没了。打开时报的是数据读取失败或者维度对不上,非常像文件损坏。
% 用 load 直接看 set 的底层结构,确认数据是存在文件里还是指向外部 fdt S = load('/data/sub01/sub01_pre.set', '-mat'); disp(fieldnames(S)); % 顶层字段 disp(size(S.EEG.data)); % 单文件模式下这里是真实数据 disp(S.EEG.datfile); % 双文件模式下这里会给出 fdt 文件名这段代码不依赖 EEGLab 的任何函数,纯 MATLAB 读 MAT。S.EEG.datfile如果非空,说明数据在外部.fdt里,搬运必须两个文件一起走,并且相对路径关系不能变——.fdt是按.set所在目录去找的,你把.set挪到别的文件夹而.fdt留在原地,照样打不开。
单文件还是双文件在保存时决定:
% 保存为单文件,便于拷贝分发;数据量特别大时会很慢 pop_saveset(EEG, 'filename', 'sub01_clean.set', ... 'filepath', '/data/sub01/', ... 'savemode', 'onefile'); % 保存为双文件,写入快,但必须成对管理 pop_saveset(EEG, 'filename', 'sub01_clean.set', ... 'filepath', '/data/sub01/', ... 'savemode', 'twofiles');savemode的两个取值对应的就是上面说的两种机制。我的做法是:中间过程用双文件(写盘快,反复覆盖不心疼),交付或归档时统一转成单文件,避免半年后自己都忘了哪个.fdt配哪个.set。
至于外部格式,.cnt(常见放大器原生格式)走pop_loadcnt,.bdf/.edf走pop_biosig(需要 BioSig 插件在位),.vhdr这类新格式在这个版本上支持很弱,常见做法是先在外面转成.set或.cnt再进来。导入之后第一件事是核对三样:通道数、采样率、总时长。三者任何一个对不上,后面全是白工。
EEG = pop_loadcnt('/data/sub01/sub01.cnt', 'dataformat', 'int16'); fprintf('通道 %d, 采样率 %d, 时长 %.1f s\n', ... EEG.nbchan, EEG.srate, EEG.pnts / EEG.srate);dataformat这个参数必须跟采集设备导出的实际类型一致,int16是最常见的,但如果放大器导出的是float32而你按int16读,波形会变成一片噪声或者幅度离谱的方波——这是导入环节最隐蔽的错误,因为不报错,只是数值不对。判断方法很简单:正常脑电在头皮上的量级是几十微伏,如果读进来动辄上千上万,先怀疑这个参数。
3.2 滤波:为什么在这个版本里 pop_eegfilt 要慎用
这个版本没有pop_eegfiltnew,只能用pop_eegfilt,底层是老的eegfilt.m。它内部的实现思路是设计一个 FIR 或 IIR 滤波器再做零相位滤波,问题是滤波器阶数和过渡带是自动估的,而且估得比较激进。
具体表现:做低截止频率的高通(比如 0.5 Hz 甚至 0.1 Hz)时,为了在这么低的频率上做出足够陡的过渡带,滤波器阶数会被拉得很高,滤波后在数据首尾产生明显的振铃和边缘畸变。如果你先分段再滤波,每个 epoch 的两端都会被污染;如果你先滤波再分段,整段数据的头尾几十秒基本不能要。
% 推荐顺序:在连续数据上滤波,然后再分段 EEG = pop_eegfilt(EEG, 0.5, 0); % 高通 0.5 Hz,压慢漂移 EEG = pop_eegfilt(EEG, 0, 30); % 低通 30 Hz,压肌电与高频噪声 EEG = pop_eegfilt(EEG, 48, 52); % 窄带,处理工频三个调用的参数含义:前两个位置参数是低截止和高截止,填 0 表示该侧不设限。第三个调用是带通形式,用来做工频抑制。这里必须提醒:pop_eegfilt的位置参数顺序在不同小版本之间有过调整,稳妥做法是在 GUI 里设一次参数,然后从主窗口的 History 里把生成的命令行原样复制出来,那条命令一定和当前版本匹配,比查文档可靠。
工频这块还有个取舍。50/60 Hz 的窄带陷波确实干净,但零相位滤波在陷波频率附近会引入相位扰动,对后续的 ERP 成分和 ICA 都有影响。我的习惯是:如果工频干扰不明显,只做 30 Hz 低通就够了,陷波留给确实被污染的数据;如果一定要做,陷波宽度别开太窄,48–52 这种 4 Hz 的带宽比 49.5–50.5 更稳。
失败时看什么:滤波后直接用pop_eegplot(EEG, 1, 1, 1)看连续波形,重点看头尾各 30 秒和大幅度跳变的位置。如果看到波形在两端有规律的衰减振荡,就是滤波器边缘效应,解决办法是裁掉头尾,或者把高通降到更低但接受更长的过渡。
3.3 重参考与降采样:顺序错了后面全错
重参考是改变参考电位,不改变信号本身,但它会改变通道之间的线性关系——而 ICA 恰恰吃这个关系。所以顺序上有一条硬规则:重参考必须在 ICA 之前完成,ICA 之后不要再改参考。ICA 之后改参考,分解出来的成分拓扑就和数据对不上了,删成分时判断会失准。
% 平均参考:传入空数组即对全部通道取平均 EEG = pop_reref(EEG, []); % 指定参考通道(例如双侧乳突,用通道索引) EEG = pop_reref(EEG, [30 47]);参数说明:第二个入参是参考通道索引数组,传[]就是平均参考。索引顺序要跟EEG.chanlocs里的顺序严格对应,建议先打印一遍标签确认:
for k = 1:EEG.nbchan fprintf('%2d %s\n', k, EEG.chanlocs(k).labels); end降采样放在哪一步也有讲究。降采样是不可逆的信息损失,必须在所有需要高频信息的处理之后、ICA 之前。ICA 在低采样率下跑得快很多,但如果你的目标频段包含 40 Hz 以上,降到 250 Hz 以下就会开始混叠影响。
% 从 1000 Hz 降到 250 Hz,先低通再降采样,避免混叠 EEG = pop_eegfilt(EEG, 0, 100); % 抗混叠预滤波,截止频率低于新奈奎斯特频率 EEG = pop_resample(EEG, 250);pop_resample的第二个参数就是目标采样率。注意这一步会重建时间轴,事件 latency 会按比例重算,EEGLAB 内部会处理,但如果你之前手工改过EEG.event的 latency 字段,重采样之后要重新核对一遍。这是第三类常见错误的来源:事件对不上了。
3.4 事件、分段与基线:boundary 与 epoch 对齐
事件是分段的前提。这个版本里事件有两层结构:EEG.event是当前事件表(分段、删数据之后会变),EEG.urevent是原始事件表(只读快照)。做回溯和对账的时候,永远以urevent为准。
外部事件表导入:
% events.txt 两列:latency 与 type,用制表符或逗号分隔 EEG = pop_importevent(EEG, 'event', 'events.txt', ... 'fields', {'latency','type'}, ... 'skipline', 1, 'timeunit', 1);timeunit是最容易搞错的一个参数:1 表示 latency 单位是采样点,0 表示是秒。填错的后果是全部分段位置偏移,而且是等比偏移,波形看着像"提前"或"延后"了。判断依据是你的采集系统导出的触发时间戳用的是哪种单位——大多数放大器导出的是采样点,少数行为学软件导出的是秒。
分段本身:
EEG = pop_epoch(EEG, {'S1','S2'}, [-0.2 0.8], ... 'newname', 'sub01_ep', 'epochinfo', 'yes'); % 基线校正,用刺激前 200 ms 作为零点 EEG = pop_rmbase(EEG, [-200 0]);三个关键参数:事件类型列表、时间窗(秒)、新数据集名。时间窗用了-0.2到0.8,对应 200 ms 基线加 800 ms 刺激后。pop_rmbase的参数是毫秒为单位的基线区间,必须落在 epoch 时间窗内,否则会静默失败或者报维度错误。
关于boundary事件:数据被切段或者删掉一段之后,EEGLAB 会插入类型为boundary的伪事件。分段时如果不避开它,跨断点的 epoch 会带着一段空白或者跳变进来。稳妥做法是分段前先查一遍:
nBoundary = sum(strcmp({EEG.event.type}, 'boundary')); fprintf('断点事件数: %d\n', nBoundary);有断点的话,用pop_select把断点附近的时间段裁掉,或者干脆把跨断点的 epoch 在分段后手动剔除。这一步省掉,后面 ICA 分解出来的成分会带明显的阶跃状拓扑,看起来像"成分不干净",其实是数据本身有断口。
4. ICA 去伪迹:runica 与 binica 的选择、成分判定和删除边界
4.1 runica 与 binica:为什么很多人卡在编译这一步
ICA 是这个包里最核心也最吃资源的一步。默认算法是 Infomax(runica),纯 MATLAB 实现,兼容性好但慢。另一个选项binica是同一算法的预编译加速版,速度快几倍,代价是要有对应平台的二进制文件。
在这个版本上,binica的坑非常集中:包里的预编译二进制大概率只覆盖了 32 位 Windows,在 64 位 Linux 或较新的 macOS 上直接调用会报"无法执行"或者干脆无声失败。想用得自己按源码重新编译,而编译链路又依赖特定版本的 C 编译器,这一步劝退过很多人。
我的建议很直接:除非数据量真的到跑不动的地步,否则就用runica,把时间成本换成确定性。
% 标准 Infomax,extended 打开扩展模式,适合超高斯与亚高斯混合的成分 EEG = pop_runica(EEG, 'icatype', 'runica', ... 'extended', 1, ... 'interrupt', 'on');三个参数的含义:icatype指定算法;extended为 1 时用扩展 Infomax,能同时处理超高斯(眨眼、肌电)和亚高斯(部分伪迹)源,做伪迹剔除时这是必须的;interrupt为on允许中途用 Ctrl+C 打断并保留当前结果,长任务必备。
还有一个隐藏参数是数据秩。如果你在 ICA 之前做了平均参考,数据的秩会掉 1(因为所有通道之和恒为零),此时直接跑 ICA,算法会尝试分解出 N 个独立成分,但实际上只有 N-1 个自由度,结果里会出现一对高度相似甚至完全重复的成分,看起来像"分解不收敛"。
% 先算秩,再决定是否降维 r = rank(EEG.data(:,:), 1e-6); fprintf('数据秩 %d,通道数 %d\n', r, EEG.nbchan); % 秩不足时用 pca 参数降维,让成分数等于秩 EEG = pop_runica(EEG, 'icatype', 'runica', 'extended', 1, ... 'pca', r, 'interrupt', 'on');'pca', r这个参数的意思是先做主成分压缩到 r 维再跑 ICA,成分数就等于 r。这步做了之后,成分拓扑会干净很多,也不会出现重复成分。这是我在平均参考数据上必做的一步,养成习惯之后省掉无数"为什么这两个成分一模一样"的疑问。
4.2 成分判定的三步法:拓扑、功率谱、ERP 图
ICA 跑完之后,EEG.icaact是成分时间序列,EEG.icawinv是混合矩阵(也就是每个成分的头皮投影)。判定哪个成分是伪迹,不能只看一个图。我固定走三步:
第一步看拓扑。眨眼伪迹的典型形态是前额正中、左右对称的双侧投影,权重集中在前几个通道;水平眼动的拓扑是左右反相,一侧正一侧负;肌电的拓扑通常弥散、杂乱、没有清晰的偶极子结构。
% 一次看前 12 个成分的地形图,4x3 排布 pop_topoplot(EEG, 0, [1:12], 'sub01 成分拓扑', [4 3], 0, 'electrodes', 'on');参数说明:第三个入参是要画的成分编号数组,第四个是标题,第五个是子图排布[行 列],最后一个'electrodes','on'会在图上标注电极位置,方便定位。
第二步看功率谱。眼动成分的能量集中在低频(1–5 Hz),曲线陡降;肌电成分是宽频抬升,10–40 Hz 甚至更高都有明显能量;心电伪迹会出现尖锐的谐波峰;而真正的神经源成分功率谱相对平缓,在 alpha 或 beta 频段有可见的隆起。
% 画成分功率谱,只关心 1-40 Hz pop_spectopo(EEG, 1, [], 'EEG', 'freq', [10 20], 'freqrange', [1 40]);第一个参数是 1 表示按数据子集画,第三个空数组表示用全部数据点,后面几个是频段与显示参数。
第三步看单试次图像。把成分时间序列按试次排开,眨眼伪迹会在每个试次里呈现固定位置的大幅尖峰,节律性伪迹会呈现水平条纹,而神经源成分应该是围绕基线波动的、不规则的。
pop_erpimage(EEG, 1, [], [], 'sub01 成分', 10, 1, {}, [], 'erp');三步走完,一个成分是眼、是肌、是心电、是神经源,基本能定性。这里要泼一盆冷水:ICA 只能分离"线性混合且统计独立"的源,不是万能的黑匣子。有些伪迹和神经信号在时间上高度相关,怎么分解都会混在一起,这时候硬删,删掉的就是真实信号。
4.3 一键删除还是半自动:概率法与峰度法的适用边界
这个版本提供pop_jointprob和pop_rejkurt两个自动筛查函数,思路是对每个通道(或成分)计算数据分布,把偏离整体分布若干个标准差的试次标出来。它们对付的是偶发的、孤立的大幅度跳变,比如电极瞬断、被试突然动一下。
% 对成分做概率筛查:阈值参数按数据情况调,先小步试 pop_jointprob(EEG, 1, [1:EEG.nbchan], 5, 3, 0, 1);参数顺序在版本之间有过微调,稳妥做法仍然是从 GUI 跑一次再看 History 输出的命令行。要理解的是这两个阈值的含义:一个是单变量阈值(以标准差计),一个是多变量阈值(联合概率)。把阈值调得很严,等于批量删除真实数据;调得很松,等于没做。
适用边界很清楚:这两个函数只能抓"幅度异常",抓不到"频谱异常"和"拓扑异常"。眨眼伪迹的幅度确实大,但它是每个试次都有的规律信号,概率法标不出来;反过来,一个幅度正常但频谱是肌电的成分,概率法也不会碰。所以合理的用法是:ICA 之后先做人工成分判定(上节的三步法),剩下的试次级异常再用概率法兜底。
% 删除指定成分,第三个参数为 0 表示不保留原成分数据的备份 EEG = pop_subcomp(EEG, [3 7 12], 0);第三个参数设 0 是为了省内存,代价是删错了没法撤销。我一般第一次跑的时候设成 1,确认效果之后再用 0 重跑一遍,算是一种廉价的后悔药。
4.4 ICA 在什么数据上跑:时机比参数更重要
有个问题被问得最多:ICA 应该在分段前还是分段后跑?
答案取决于目标。如果目标是剔除试次级的伪迹(眨眼、吞咽、动作),应该在连续数据上跑 ICA,因为连续数据样本多,分解更稳定,而且伪迹在连续段里分布更均匀。如果目标是研究单试次成分活动,分段后跑也可以,但要注意分段会切掉一部分数据,样本量变少,分解结果的稳定性下降。
我的固定流程是:连续数据 → 滤波 → 重参考 → 降采样 → 在连续数据上跑 ICA → 用 ICA 权重应用到分段后的数据 → 分段 → 基线校正 → 剔除坏试次。这个顺序的好处是 ICA 吃到了最长的数据,权重最稳,后面分段和剔除都不影响分解结果。
要提醒的是,如果在分段之后又做了任何改变通道线性组合的操作(比如再次重参考、插值坏电极),EEG.icawinv就失效了,必须重新计算投影。这点在跨步骤复用数据集时特别容易忘。
5. 避坑与排查:五条把数据做废的真实翻车记录
5.1 五条现象、原因与解决
第一条:只拷了.set没拷.fdt。现象:换机器打开数据,报数据维度不匹配或者读文件失败,.set文件本身能打开,能看到通道名和事件,但波形一片空白或者全是零。 原因:保存时用了双文件模式,数据本体在.fdt里,.set只是索引。搬运时只搬了索引。 解决:确认EEG.datfile字段非空之后,把同名.fdt一起拷走,且保持两者在同一目录。长期方案是归档时统一转成单文件,用pop_saveset(..., 'savemode','onefile')。
第二条:新版 MATLAB 上 GUI 控件错位、菜单点不开。现象:eeglab能启动,主窗口出来了,但按钮文字重叠、下拉框点开是空的,或者点某个菜单 MATLAB 直接报句柄属性错误。 原因:这个版本的界面是基于老式图形系统写的,较新的 MATLAB 换了图形对象体系,部分属性名和回调机制不兼容。 解决:放弃在 GUI 里做复杂操作,全部走命令行 + History 复制。pop_系列函数不依赖 GUI,功能完全一致。实在需要看图,用pop_eegplot、pop_topoplot这类独立出图的函数,它们受影响较小。
第三条:平均参考后跑 ICA,出现成对重复的成分。现象:分解出来的成分里,编号相邻的两个成分拓扑几乎一样,功率谱也重合,删掉一个另一个也跟着没意义。 原因:平均参考让所有通道之和恒为零,数据秩从 N 降到 N-1。ICA 仍按 N 个成分去解,多出来的自由度导致了退化。 解决:ICA 之前先rank()算秩,然后用'pca', r参数把维度压到秩的大小。这个操作在平均参考数据上应当是默认动作。
第四条:滤波后 epoch 两端出现巨大尖峰,被自动剔除误杀。现象:分段之后看单试次图,每个 epoch 的头尾各有一段规律振荡,幅度比中间的真实信号还大,pop_jointprob把大量好试次标成异常。 原因:低截止频率的高通滤波在数据边缘产生振铃,如果先分段再滤波,每个 epoch 的两端都会被污染。 解决:把滤波挪到分段之前,在连续数据上做;滤波后裁掉头尾各若干秒;或者把高通截止从 0.5 Hz 降到 0.1 Hz 并接受更长的过渡带。判据是看连续波形两端有没有衰减振荡。
第五条:路径含中文或空格,pop_loadset报文件不存在。现象:文件明明在,dir也能列出来,但pop_loadset就是找不到;或者找到了但保存时报写入失败。 原因:MATLAB 的文件 IO 在某些版本和某些系统语言设置下,对非 ASCII 路径和含空格路径处理不一致。 解决:项目根目录用纯 ASCII、无空格的短路径,比如/data/eeg_proj/。这条看起来像废话,但它排在所有诡异报错的原因榜首,值得写在实验室的墙上。
5.2 排查顺序:先分层,再定位
碰到问题时的排查顺序,比具体某一条坑更重要。我的顺序是:环境 → 文件 → 参数 → 数据。
先跑第 2 章那四条断言,确认函数都在路径上;再确认.set与.fdt成对、路径纯 ASCII;然后回看这一步用的参数(尤其是单位:秒还是采样点、毫秒还是秒);最后才怀疑数据本身。这个顺序能挡掉八成"看起来是数据坏了"的问题。
另一条实用习惯:每一步做完立刻另存一个带步骤后缀的.set,比如sub01_01raw、sub01_02filt、sub01_03ref、sub01_04ica。听起来很土,占用磁盘,但当你发现第五步的结果不对、需要回到第三步重跑时,这套命名能救你一整天。EEGLAB 的 History 会记录操作,但 History 只能告诉你怎么做的,不能替你保存中间状态。
6. 喂给机器学习之前:特征提取、交叉验证与一条能复现的基线
终于到了机器学习这一段。脑电做分类,难点从来不在模型,而在特征和验证。用这个版本导出的分段数据,我一般走最简单也最不容易出错的一条线:频带功率特征加线性判别。
6.1 从 .set 导出到特征矩阵
不依赖任何导出函数,直接操作结构体最稳:
% EEG.data 维度为 [通道 x 时间点 x 试次] data = EEG.data; fs = EEG.srate; labels = {EEG.chanlocs.labels}; % 试次标签需要从事件里解析,这里假设存在 EEG.epoch 的 eventtype 字段 y = zeros(1, size(data,3)); for k = 1:numel(y) et = EEG.epoch(k).eventtype; if iscell(et), et = et{1}; end y(k) = double(strcmp(et, 'S1')); % S1 记 1,其余记 0 end save('sub01_features.mat', 'data', 'fs', 'labels', 'y');这段的关键是维度顺序:EEGLAB 的EEG.data是通道在前、时间点在中、试次在后。很多人按 Python 的直觉写成试次在前,导出来之后特征全错位,模型准确率还是 50% 上下(二分类瞎猜水平),但你会以为是数据没信息。
Python 端的特征提取与分类:
import numpy as np, scipy.io as sio from scipy.signal import welch from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.discriminant_analysis import LinearDiscriminantAnalysis from sklearn.model_selection import StratifiedKFold, cross_val_score m = sio.loadmat('sub01_features.mat') data, fs, y = m['data'], float(m['fs'][0,0]), m['y'].ravel() # [ch, time, trial] def bandpower(x, fs, band): f, p = welch(x, fs=fs, nperseg=min(256, x.shape[-1]), axis=-1) idx = (f >= band[0]) & (f <= band[1]) return np.trapz(p[..., idx], f[idx], axis=-1) # [ch, trial] # 两个频带的绝对功率取对数,拼成特征矩阵 feat = np.concatenate([ np.log(bandpower(data, fs, (8, 13))), # mu / alpha np.log(bandpower(data, fs, (13, 30))) # beta ], axis=0).T # [trial, 2*ch] pipe = Pipeline([('sc', StandardScaler()), ('clf', LinearDiscriminantAnalysis())]) cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0) print(cross_val_score(pipe, feat, y, cv=cv).mean())nperseg取 256 与信号长度中的较小值,是为了在 epoch 只有几百毫秒时也能算出稳定的功率谱;np.trapz在频带上积分得到该频段的绝对功率;取对数是为了把长尾分布压平,让线性分类器的假设更接近成立。整条管线放进Pipeline里,标准化只在训练折上拟合,这是防止数据泄漏的最低要求。
6.2 交叉验证里最容易翻车的地方
第一,标准化、滤波、ICA 这些操作如果用了全量数据,就已经泄漏了。正确做法是这些步骤要么在预处理阶段对每个被试独立完成(且不利用标签),要么放进 Pipeline 里跟着折走。用全量数据算的归一化参数去标准化测试折,准确率会虚高几个百分点,而且虚高得不多,不容易被发现。
第二,重叠 epoch 会让交叉验证失效。如果你的 epoch 有重叠(步长小于窗长),相邻试次的信号高度相关,随机划分会把"几乎一样的样本"分到训练和测试两侧,准确率直接虚高到 80% 以上。解决办法是按时间块划分,或者干脆不做重叠。
第三,样本量小的时候别信单次结果。几十个试次的二分类,重复划分几次,准确率的波动可能有十几二十个百分点。差值不到显著水平的情况下,"我的模型比基线高 5%"这种结论基本站不住。
6.3 一条能复现的基线
我的习惯是先跑一条最笨的基线:只用两个频带、只用线性判别、只做 5 折、固定随机种子。这条基线跑出来的数字(一般在 60%–75% 之间,取决于任务和被试)才是后面所有复杂模型的参照。深度学习模型如果打不过这条基线,问题通常不在模型,而在特征对齐、标签解析或者数据泄漏上。
从那以后我每次拿到新数据,都强制先走一遍这条基线再谈其他——它的价值不在于准确率有多高,而在于它一旦跑通,就说明从.set到特征矩阵这条链路上的每一环都是对的。希望帮到你。
本文还有配套的精品资源,点击获取