静息态 EEG 微状态分析(上)
换一个视角看脑电:不问"哪个频段多响",而问"头皮上的电位构型何时切换、 停留多久、怎么切换"。这一课从 GFP 一路走到转移概率矩阵,把微状态流水线完整走一遍。
🎯 本节学习目标
- 能用"电视频道切换"的类比说清微状态的准稳态含义,并说明它与频谱分析的互补关系
- 独立完成 GFP 计算、峰值提取、极性无关 k-means 聚类与反向拟合全流程
- 会计算并解读五大类指标:解释方差、duration、occurrence、coverage、转移概率
- 理解从被试级走向组级统计的关键前提:所有被试拟合到同一套原型地图
- 知道 K 值、平滑窗、最短持续时间、参考方式等参数如何选择并在论文中报告
一、微状态是什么:大脑的"电视频道"
到目前为止,我们观察脑电的方式无非两种:顺着电极看波形(ERP,模块 3), 或者顺着频段看能量(时频分析,模块 4)。微状态分析提供第三种视角: 看整个头皮的电位分布图(拓扑图)如何随时间演变。
头皮的电位拓扑并不是连续缓慢地变化,而是在几十毫秒内保持一种构型("准稳态"), 然后在几毫秒内整体切换成另一种构型,如此往复—— 像电视机换台:每个频道画面稳定,换台在一瞬间完成。
经典研究(Lehmann 学派)发现:静息态脑电的拓扑图基本只在少数几张"原型地图"之间切换, 每张地图持续约 60–120ms。更有意思的是,最常出现的四张地图(惯称 A、B、C、D) 在不同人身上高度相似,且分别与 fMRI 的视觉网络、听觉网络、凸显网络和额顶注意网络存在对应。 于是这个领域流传着一句很有画面感的话:"思想的原子"—— 微状态被视为大规模脑网络短暂同步化的电生理表现。
频谱分析问"哪个频段多响",微状态问"大脑此刻整体处于哪张地图、每张停留多久、怎么切换"。前者是幅值视角,后者是拓扑(空间构型)视角,两者互补而非替代。
二、互补的视角:拓扑分析 vs 频谱分析
| 维度 | 频谱 / 时频分析 | 微状态分析 |
|---|---|---|
| 观察对象 | 各电极信号随频率的幅值分布 | 全头皮电位的空间分布(拓扑图) |
| 时间尺度 | 数百毫秒到数秒的分析窗 | 毫秒级(一段约 60–120ms) |
| 回答的问题 | "哪些频段能量多大、何时变大" | "大脑整体处于哪种构型、如何切换" |
| 对参考的依赖 | 幅值指标相对不敏感 | 强依赖参考(必须平均参考) |
| 典型产出 | 功率谱、时频图、ERD/ERS | 原型地图、duration、coverage、转移矩阵 |
三、分析流水线总览
① 预处理(平均参考是硬要求)+ 必要时降采样(如 250Hz) ② 计算全局场功率 GFP 曲线 ③ 只在 GFP 峰值时刻提取拓扑图(信噪比最高的时刻) ④ 极性无关 k-means 聚类 → K 张原型地图 ⑤ 反向拟合:给每个时间点贴上微状态标签(含平滑与最短持续时间) ⑥ 指标计算(duration / occurrence / coverage / 转移概率) ⑦ 被试级指标 → 组级统计
这条流水线看起来很长,其实每一步的代码都不超过三十行。下面逐步拆解。
四、第一步:GFP 曲线与峰值提取
每个时间点上,所有电极电位相对于其平均值的空间离散度(空间标准差)。GFP 高 = 拓扑图"对比度"强、神经信号占优;GFP 低 = 拓扑主要由噪声决定。
为什么只在峰值处取样?因为微状态的切换几乎总是发生在 GFP 峰值附近—— 峰值时刻的拓扑最"清晰"。只在峰值处采样,既能大幅减少数据量, 又能去掉低信噪比的时刻,让聚类更稳。
% ---- 第 1 步:GFP 曲线与峰值提取 ----
% 前提:数据已完成模块 2 的预处理,并使用平均参考(微状态对参考极其敏感)
fs = EEG.srate;
data = EEG.data - mean(EEG.data, 1); % 再做一次空间中心化,保证拓扑"相对平均"
gfp = sqrt(mean(data.^2, 1)); % GFP(t):每个时刻的空间标准差
figure; plot(EEG.times/1000, gfp); % 目检:GFP 应是快速起伏的曲线
xlabel('时间 (s)'); ylabel('GFP (\muV)'); title('全局场功率');
% 只保留 GFP 局部极大值处的拓扑:间隔至少 10ms、高度超过均值
[pk, idx] = findpeaks(gfp, 'MinPeakDistance', round(0.01*fs), ...
'MinPeakHeight', mean(gfp));
maps = data(:, idx); % 通道 x 峰值时刻:候选拓扑集合
fprintf('采样拓扑数 %d(占全部时间点 %.1f%%)\n', ...
size(maps,2), 100*size(maps,2)/size(data,2));
% findpeaks 来自 Signal Processing Toolbox(后面的 filtfilt 也是)
五、第二步:聚类成原型地图
现在手里有几千张候选拓扑图,但它们其实只是少数几种构型的反复出现。
聚类的任务就是把它们归并成 K 张"原型地图"。为什么不能直接用
Matlab 自带的 kmeans?因为微状态的聚类有两处特殊改造:
- 极性无关:一张拓扑图和它整体变号后的图(红蓝互换)在生理上是同一个状态,相似度必须取空间相关的绝对值;
- 中心更新用空间主成分:同一类里可能混着正负两种朝向的图,直接取均值会互相抵消,用类的空间第一主成分(SVD 的第一奇异向量)才站得住。
% ---- 第 2 步:极性无关 k-means 聚类(教学简化版)----
K = 4; nInit = 50; % 经典微状态研究常取 K=4
nS = size(maps, 2);
mapsN = maps ./ vecnorm(maps); % 样本归一化:只比较"形状",不比较强弱
gfp2 = sum(maps.^2, 1); % 每个样本的 GFP^2(后面当权重)
bestGEV = -inf;
for init = 1:nInit % 随机重启:对抗 k-means 的初值敏感性
C = maps(:, randperm(nS, K)); % 初始中心:随机挑 K 张真实拓扑
for it = 1:50 % 教学版固定迭代;工程版加收敛判断
sim = abs((C ./ vecnorm(C))' * mapsN); % 分配步:K x nS 的|相关|矩阵
[~, seg] = max(sim, [], 1);
for k = 1:K % 更新步:类内空间第一主成分当新中心
Xk = mapsN(:, seg == k);
if size(Xk, 2) < 2, continue; end
[u, ~, ~] = svd(Xk * Xk', 'econ');
C(:, k) = u(:, 1);
end
end
% 用全局解释方差 GEV 给本次初始化打分(这 K 张地图解释了多少电位方差)
gev = 0;
for k = 1:K
sel = (seg == k);
cn = C(:, k) / norm(C(:, k));
gev = gev + sum((cn' * mapsN(:, sel)).^2 .* gfp2(sel));
end
gev = gev / sum(gfp2);
if gev > bestGEV
bestGEV = gev;
Cbest = C ./ vecnorm(C); % 归一化的 K 张原型地图
end
end
C = Cbest;
fprintf('K = %d, 全局解释方差 GEV = %.1f%%\n', K, 100*bestGEV);
% 直觉:GEV 是"这 K 张地图演了全剧的多大比例";K=4 时静息态典型值约 70-80%
让 K 从 2 取到 8,画一条 GEV 随 K 变化的曲线,看"肘部";更严谨的做法是交叉验证(留出一段数据看原型地图的泛化)。不要只因为"经典研究用 4"就盲从 4——你的数据、你的范式、你的研究问题都可能需要别的 K。工程上也不必每次手写:EEGLAB 的微状态插件或下一课的 CARTOOL 都有成熟实现;手写的价值在于你知道每个按钮背后在算什么。
六、第三步:反向拟合:给全数据贴标签
原型地图回答了"有哪几种状态",反向拟合(backfitting)回答"每一时刻处于哪种状态": 对每个时间点,计算它与 K 张地图的空间相关,贴上最像的那张的标签。 随后必须做两件清理工作:时间平滑(去掉几毫秒的闪烁标签) 和最短持续时间约束(真实微状态至少持续几十毫秒)。
% ---- 第 3 步:反向拟合——给每个时间点贴标签 ----
dataN = data ./ max(vecnorm(data), eps); % 逐点归一化:不让高 GFP 时刻主导
sim = abs(C' * dataN); % K x 时间点 的|空间相关|
[~, labels] = max(sim, [], 1); % 每个时刻最像哪张地图
labels(gfp < 0.5*mean(gfp)) = 0; % GFP 太低的时刻不归类(0 = 噪声主导)
% 时间平滑 + 最短持续时间:真实微状态持续约 60ms 以上
labels = medfilt1(double(labels), 3); % 3 点中值滤波去掉孤立标签(奇数窗不出小数)
% labels = enforce_min_duration(labels, round(0.03*fs)); % <30ms 并入邻段:课后练习 3
七、指标体系:给微状态"记账"
贴好标签后,整段数据变成了一串"频道号"序列。下面这些指标就是这串序列的账本:
| 指标 | 定义 | 直觉解释 |
|---|---|---|
| 解释方差 GEV | 原型地图解释的总电位方差比例 | 这 K 张图"演了全剧的多大比例" |
| 平均持续时间 duration | 每段微状态的平均时长 | 一个"频道"平均播多久 |
| 出现频率 occurrence | 每秒进入该状态的次数 | 换到这个频道多频繁 |
| 覆盖率 coverage | 该状态占总时间的比例 | 这个频道全天播放占比 |
| 转移概率 | P(下一状态 j | 当前离开状态 i) | 观众从频道 i 换台时去哪 |
% ---- 第 4 步:指标计算(游程编码思路:先切段,再按类汇总)----
d = diff(labels);
edges = [1, find(d ~= 0) + 1, numel(labels) + 1]; % 每一段的起止边界
durPts = diff(edges); % 每段持续的采样点数
segLab = labels(edges(1:end-1)); % 每段的微状态编号
valid = segLab > 0; % 排除未归类(0)的段
recSec = numel(labels) / fs;
occurrence = accumarray(segLab(valid)', 1, [K 1]) / recSec; % 次/秒
nSeg = accumarray(segLab(valid)', 1, [K 1]);
coverage = accumarray(segLab(valid)', durPts(valid)') / sum(durPts); % 时间占比
duration = accumarray(segLab(valid)', durPts(valid)', [K 1]) ./ max(nSeg, 1);
duration_ms = duration / fs * 1000; % 每段平均持续毫秒数
% 转移概率矩阵:行 = 当前状态,列 = 下一状态(行归一化的条件概率)
T = zeros(K);
seq = segLab(valid);
for s = 1:numel(seq) - 1
if seq(s) ~= seq(s+1)
T(seq(s), seq(s+1)) = T(seq(s), seq(s+1)) + 1;
end
end
T = T ./ max(sum(T, 2), 1);
disp('转移概率矩阵:'); disp(round(T, 3));
八、从被试级到组级统计
单被试的指标算出来只是原材料。走向组级之前有一条铁律: 所有被试、所有条件必须拟合到同一套原型地图上。 这套地图通常来自组级总平均数据的峰值拓扑聚类(或用全体被试合并的数据), 然后逐被试反向拟合、逐被试算指标。指标在组间用置换检验或 bootstrap 置信区间比较, 多个状态 × 多个指标的比较要做 FDR 校正(模块 5 讲过)。
① 每个被试各聚各的再比较:你的 A 类和别人的 A 类根本不是同一张图,"组间差异"可能只是地图不同造成的假象;② 忘记平均参考:微状态的拓扑完全由"相对参考的分布"定义,参考不统一,一切指标不可比。论文报告时写清:参考方式、K 及其确定方法、聚类算法、峰值采样间隔、平滑与最短持续时间、GEV。
📌 本节小结
- 微状态 = 头皮拓扑的毫秒级准稳态;四张经典地图与大尺度脑网络存在对应
- 流水线:GFP → 峰值拓扑 → 极性无关 k-means → 反向拟合 → 指标 → 组级统计
- 两处"必须不同":相似度取绝对值(极性无关),中心更新用空间主成分(不是均值)
- 五大指标 = 演多久(duration)、多频繁(occurrence)、占多少(coverage)、怎么切换(转移矩阵)、演了多少(GEV)
- 组级比较的前提:所有被试拟合到同一套原型地图;平均参考不可省
✏️ 课后练习
- 对一段至少 60 秒的静息态数据计算 GFP 并画图,用
findpeaks提取峰值拓扑,统计峰值数量与平均间隔(毫秒)。 - 令 K = 2…8 分别聚类,画出 GEV 随 K 变化的曲线,找"肘部",并写两句话论证你的研究问题需要几个状态。
- 实现
enforce_min_duration(labels, minPts):把短于阈值的段并入前后较长的邻段,比较处理前后 duration 的分布变化。