基于 FieldTrip 的功能连接实践
把 6.1 的原理和 6.2 的统计装进 FieldTrip 的工程流水线:数据进来、矩阵出去、 图自动画好,最后搭出能跑一整批被试的组级骨架。75 分钟大课,代码管够。
🎯 本节学习目标
- 说清 ft_connectivityanalysis 对输入数据的要求:为什么必须是频域 Fourier 谱
- 独立跑通头皮电极水平流程:ft_preprocessing → ft_freqanalysis → ft_connectivityanalysis → ft_connectivityplot
- 会用 cfg.channelcmb 限定通道对、cfg.foilim 限定频段,控制内存与计算量
- 理解"先源后连":ROI 虚拟电极怎么做、为什么能绕开头皮混合(细节衔接模块 7)
- 能写出单被试到组级统计的循环骨架,并用 6.2 的 FDR 完成边级校正
- 能自查并修复本课列出的高频报错与参数坑
一、FieldTrip 做连接分析:路线图
模块 2 的 EEGLAB 擅长预处理和 ERP,连接分析这一站换 模块 5 用过的 FieldTrip:cfg 驱动、函数之间数据结构自动衔接,且与统计模块无缝对接。 本课主线就一条流水线:
ft_preprocessing 读入数据 + 保留试次
|
ft_freqanalysis cfg.output = 'fourier':拿到带相位的复数频谱
|
ft_connectivityanalysis cfg.method = 'wpli' / 'coh' / 'plv':算连接
|
ft_connectivityplot 自带折线图;或 conn2matrix 转矩阵后用 6.2 的画图代码
先提醒一个和直觉相反的事实:整条流水线里最容易错的是第二步——大多数人第一次 跑连接,都是折在"给了功率谱"上。为什么,看完第二节你就懂了。
二、ft_connectivityanalysis:先读懂数据类型要求
这个函数接受的输入可以是 ft_preprocessing / ft_timelockanalysis /
ft_freqanalysis / ft_sourceanalysis 的输出,但每个 method
只支持特定类型(下面以官方文档为准):
| cfg.method | 要求的输入 | 输出字段 | 特点 |
|---|---|---|---|
coh | freq(fourier 谱) | cohspctrm | 幅值+相位混合,不抗零滞后 |
plv | freq(fourier 谱) | plvspctrm | 相位同步集中度,不抗零滞后 |
wpli / wpli_debiased | freq(fourier 谱) | wplispctrm 等 | 抗零滞后,头皮水平首选 |
ppc | freq | ppcspctrm | 相位一致性,对试次数不敏感的改良版 |
corr | raw / timelock | corr | 时域相关,最简单 |
granger / dtf / pdc | mvar 或 freq | 各 method 对应字段 | 方向性(有效连接),本课不展开 |
和 6.1 的关系:6.1 手算 PLV 走的是"时域希尔伯特相位差"路线;FieldTrip 的
plv 走频域互谱路线——思想相同(相位差的集中度),实现路径不同。殊途同归,
所以 6.1 的直觉全部适用。
FieldTrip 连接输出的核心字段:labelcmb 是 N×2 的通道对列表(如 F3 与 O1),
与之配套的 *spctrm 数组按同样的顺序存放每条边的值。看懂这两个字段,
你就能把任何连接结果翻译成 6.2 画的 N×N 矩阵。
常用 cfg 字段速查(均为真实字段名):
| 字段 | 作用 |
|---|---|
cfg.method | 连接指标,全小写:'coh'、'plv'、'wpli' 等(写成大写会报错) |
cfg.channelcmb | N×2 cell,指定只算哪些通道对——控制内存的第一道闸 |
cfg.trials | 指定用哪些试次参与计算 |
cfg.complex | 'abs'(默认)/ 'imag' / 'angle':coh/csd/plv 输出的复数形式;取 'imag' 得"虚部相干",也是抗零滞后的老思路 |
cfg.partchannel | partial coherence 时要剔除的通道 |
cfg.bandwidth | 只有 'psi'(相位斜率指数)和 'plm' 这类方向性指标需要:跨频率积分的半带宽(Hz)。算 wPLI/相干用不到它 |
三、头皮电极水平完整流程(主代码)
功率谱 = 复数频谱自己乘自己的共轭,相位信息在这一步被彻底扔掉(模块 4 讲过)。
而所有相位类连接指标都要相位。所以在 ft_freqanalysis 里必须
cfg.output = 'fourier',并保留试次维——互谱由 ft_connectivityanalysis
内部跨试次完成。
%% ===== 头皮电极水平连接分析(单被试完整流程) =====
clear; clc; close all;
ft_defaults % 启动 FieldTrip(每个脚本开头跑一次)
subj = 'sub01';
% --- 1) 读入模块 2 预处理好的 EEGLAB .set(FieldTrip 能直接读 .set) ---
cfg = [];
cfg.dataset = ['D:\project\processed\' subj '_pp.set'];
cfg.channel = 'eeg'; % 只保留脑电通道(自动剔除 EOG/ECG 等)
data = ft_preprocessing(cfg);
% (若数据是未分段的连续记录:先切等长小段充当"试次",连接统计需要重复观测)
% cfg = []; cfg.length = 2; % 每段 2 秒
% data = ft_redefinetrial(cfg, data);
% --- 2) 频域分解:要 Fourier 谱,不要功率谱 ---
% 连接靠相位;功率谱是"相位盲"的:output 必须设为 'fourier'
cfg = [];
cfg.method = 'mtmfft'; % 多窗 FFT:逐试次的频谱估计
cfg.output = 'fourier'; % 关键:输出复数频谱(保留相位)
cfg.taper = 'dpss';
cfg.foilim = [8 13]; % 只算 alpha 频段:内存与计算量直线下降
cfg.tapsmofrq = 2; % 2Hz 频率平滑:稳定相位估计
cfg.keeptrials = 'yes'; % 保留试次维!互谱要跨试次求
freq = ft_freqanalysis(cfg, data);
% --- 3) 连接计算 ---
cfg = [];
cfg.method = 'wpli'; % 头皮水平抗零滞后首选(6.1 对比表)
cfg.channelcmb = {'F3' 'O1'; 'F4' 'O2'; 'Fz' 'Pz'}; % 只算这 3 对:内存闸门
conn = ft_connectivityanalysis(cfg, freq);
disp(conn) % 重点看两个字段:labelcmb 和 wplispctrm
% --- 4) 自带绘图:通道对 x 频率的折线图 ---
cfg = [];
cfg.parameter = 'wplispctrm'; % 必须与输出字段名完全一致
cfg.zlim = [0 1];
ft_connectivityplot(cfg, conn);
% 注意:wpli 的输出可能带符号(含方向信息),看"强度"时对结果取 abs
第一次跑:用一个被试 + 3 对通道 + 一个频段把流程跑通,确认输出结构无误, 再放大到全通道、多被试。直接上 64 通道 × 全频段 × 全被试,跑半小时报一个内存错误, 是最浪费时间的路径(模块 2.4 的教训同样适用)。
四、从输出到图:矩阵化 + 复用 6.2 的画图代码
ft_connectivityplot 画的是"通道对 × 频率"的折线,适合看少数几对边的频谱。
要画 6.2 那种 N×N 热图和圆形网络图,先把 labelcmb + *spctrm
翻译成矩阵——这个 30 行的小工具是本课最重要的"胶水":
function C = conn2matrix(conn, param, freqOfInterest)
% 把 ft_connectivityanalysis 的输出(labelcmb + *_spctrm)转成 N×N 对称矩阵
% 转完直接喂给 6.2 的矩阵图 / draw_circle_net 代码
% conn: 连接输出; param: 'cohspctrm' / 'wplispctrm' / 'plvspctrm'
% freqOfInterest: 感兴趣频率(Hz),取最近的频率 bin;不填则对整个频段取均值
labels = unique(conn.labelcmb(:)); % 由通道对反推通道名(按字母排序)
N = numel(labels);
vals = squeeze(conn.(param)); % squeeze 后:通道对 x 频率
if nargin < 3 || isempty(freqOfInterest)
v = mean(vals, 2); % 不指定频率:整个频段取均值
else
[~, kfr] = min(abs(conn.freq - freqOfInterest));
v = vals(:, kfr);
end
C = nan(N, N);
for k = 1:size(conn.labelcmb, 1)
i = find(strcmp(labels, conn.labelcmb{k,1}));
j = find(strcmp(labels, conn.labelcmb{k,2}));
C(i, j) = v(k);
C(j, i) = v(k); % 对称指标:两格都填
end
end
% 用法:和 6.2 的画图代码无缝衔接
% C = conn2matrix(conn, 'wplispctrm', 10); % 取最接近 10Hz 的结果
% draw_circle_net(C, labels, 0.3); % 6.2 的圆形网络图
五、源空间连接:"先源后连"
用逆问题方法(如 beamformer)在某个源点/ROI 上"重建"出来的时间序列。它不再是一个 电极对所有源的混合,而是试图还原该位置神经活动本身——用它算连接, 才是在"两个产生信号的地方"之间算连接。
为什么要"先源后连":头皮上任何两电极的信号都互含对方的成分(容积传导), 连接指标天生被污染;先定位源、再取 ROI 时间序列,混合在源水平被大幅削弱。 思路一句话:节点先定准,再谈边。头模型、leadfield、beamformer 细节 属于模块 7(本模块与它允许前后互链),这里给骨架:
%% ===== 源空间连接:先源后连(骨架) =====
% 前置(模块 7):headmodel = ft_prepare_headmodel(...);
% sourcemodel = ft_prepare_leadfield(...)(含每个源点的导联场)
% 1) 在数据上估计 LCMV 空域滤波器,并保留滤波器
cfg = [];
cfg.method = 'lcmv';
cfg.headmodel = headmodel;
cfg.sourcemodel = sourcemodel;
cfg.lcmv.keepfilter = 'yes'; % 保留每个源点的滤波器(下一步要用)
source = ft_sourceanalysis(cfg, dataAll);
% 注意:raw 输入时滤波器在 source.filter,timelock 输入时在 source.avg.filter
% 2) 用同一组滤波器提取各 ROI 的虚拟电极时间序列
cfg = [];
cfg.method = 'lcmv';
cfg.sourcemodel = sourcemodel;
cfg.sourcemodel.filter = source.avg.filter; % 复用第 1 步估计的滤波器
roi = ft_sourceanalysis(cfg, dataTrial); % roi.time{trial} = 各源点的时间序列
% 3) 之后与头皮水平完全一样:
% 对 ROI 时间序列做 ft_freqanalysis(output='fourier') -> ft_connectivityanalysis
% 区别只是:labelcmb 里现在是"ROI 对"而不是"电极对"
beamformer 的空间分辨率有限,相邻源点的时间序列会互相"漏"进去——泄漏同样制造
假连接(尤其零滞后型)。对策:继续用 wPLI 类指标兜底(扔零滞后);或用 FieldTrip 的
'powcorr_ortho'(基于正交化的功率相关)等泄漏抑制方法。源空间连接报告里
不提泄漏问题,是审稿意见的常客。
六、频段限定与结果解释
频段在 ft_freqanalysis 里用 cfg.foilim 限定(如 [8 13]
只算 alpha)。三个实操要点:
- 频率分辨率下限是 Rayleigh 频率 = 1/试次长度:2 秒的试次分辨率 0.5Hz—— 想分辨 8.5 与 9Hz 的差异,先把试次切长;
- tapsmofrq 的权衡:平滑越大估计越稳、但频率细节越糊;窄频段 (如 8–13Hz)配 2Hz 平滑是常见起点;
- 解释绑定频段:alpha 的长程连接与 beta 的局部连接含义不同,报告时 "频段宽度 + 中心频率"必须写清;频段内取均值与取峰值是两种结果,别混着说。
解释陷阱提前打预防针:wPLI 低不等于"没通信"——零滞后型的交互被指标主动丢弃; 指标之间的数值不可直接比较(wPLI 的 0.3 和相干的 0.3 不是一个概念);临床方向常见 "alpha 频段过度连接"的报道,但先确认方法学(参考、校正、试次数)再谈生理意义。
七、组级循环骨架:从单被试到统计
把第三节的单被试流程打包成函数,外面套被试循环,收集被试 × 边的值矩阵, 再交给 6.2 的统计与校正——这就是一篇连接论文的全部工程骨架:
%% ===== 组级骨架:循环被试 -> 被试 x 边矩阵 -> 6.2 的统计 =====
subjects = {'sub01','sub02','sub03','sub04','sub05'};
edges = {'F3','O1'; 'F4','O2'; 'Fz','Pz'; 'C3','C4'; 'T7','T8'}; % 预选的边
nSub = numel(subjects);
nEdge = size(edges, 1);
W = nan(nSub, nEdge); % 被试 x 边 的 wPLI 矩阵(核心结果)
for s = 1:nSub
try
freq = run_subject_connectivity(subjects{s}); % 第三节第 1-2 步打包的函数
cfg = [];
cfg.method = 'wpli';
cfg.channelcmb = edges;
conn = ft_connectivityanalysis(cfg, freq);
v = squeeze(conn.wplispctrm);
if size(v, 2) > 1, v = mean(v, 2); end % 频段内取均值
W(s, :) = abs(v)'; % abs 取强度(wpli 输出可带符号)
fprintf('%s done\n', subjects{s});
catch err
fprintf(2, '%s FAILED: %s\n', subjects{s}, err.message); % 单个被试失败不中断
W(s, :) = NaN; % 标记缺失,统计时剔除
end
end
% --- 两条件配对比较(示意):逐边配对 t 检验 + 6.2 的 BH-FDR ---
% WA、WB:两条件各自的"被试 x 边"矩阵(同上循环各跑一遍)
p = nan(nEdge, 1);
for e = 1:nEdge
[~, p(e)] = ttest(WA(:, e), WB(:, e)); % ttest 需 Statistics Toolbox;也可换 signflip 置换
end
sig = fdr_bh(p, 0.05); % 6.2 写的 BH-FDR,直接复用
fprintf('通过 FDR 的边:%d / %d\n', nnz(sig), nEdge);
save('group_wpli.mat', 'subjects', 'edges', 'W', 'p', 'sig'); % 存档:可复现
逐边 t 检验 + FDR 是最透明的起点;想升级就把每条边换进
ft_statistics_montecarlo 的置换框架(cfg.correctm = 'maxstat',
与模块 5 同一套思路),或用 ft_networkanalysis 走 NBS。
无论哪种,每被试的连接结果先 save 成 .mat,统计永远在存档之上做。
八、常见报错与参数坑速查
| 症状 | 原因 | 解法 |
|---|---|---|
| 提示 method 无效或 unsupported | cfg.method 大小写/拼写错(必须全小写,如 'wpli') | 对照第二节速查表逐字母核对 |
| 提示输入数据类型不对 / 找不到频谱 | 给了功率谱或时域数据 | ft_freqanalysis 里设 cfg.output = 'fourier' |
| 提示数据应为 rpt 格式 | 试次维被平均掉了 | 检查 cfg.keeptrials = 'yes' 是否生效 |
| 内存爆掉 / 卡死 | 全通道对 × 全频段 × 多 taper | cfg.channelcmb 限定通道对 + cfg.foilim 限定频段 |
ft_connectivityplot 空白或报错 | cfg.parameter 与输出字段名不一致 | fieldnames(conn) 抄准确的名字(如 'wplispctrm') |
用 'psi' 时报需要 bandwidth | 该字段只服务方向性指标 | 按 help 给 cfg.bandwidth 赋值(Hz),或改用无向指标 |
| 试次长度不一致报错 | 分段不齐 | ft_redefinetrial 用 cfg.length 统一切段 |
| 读 .set 报错 | 路径含中文/空格,或没跑 ft_defaults | 数据移到英文路径;脚本开头执行 ft_defaults |
九、本模块收官检查单
从 6.1 到 6.3,一条完整的连接分析链路。离开本模块前自查:
- 指标:能说清为什么选 wPLI/相干,公式和容积传导立场都能口头复述;
- 数据:Fourier 谱 + 试次维齐备,频段、平滑、试次数都有记录;
- 统计:2016 条边的多重比较有明确校正方案,阈值做过敏感性检查;
- 图:矩阵图 + 网络图各一张,图注完整;
- 存档:每被试 .mat + 组级脚本 + 随机种子,三个月后还能一键复现。
下一站模块 7:把"节点"做实——头模型、 正逆问题与源定位,回来再跑本课第五节的源空间连接,你会对"先源后连"有全新体会。
📌 本节小结
- 连接指标几乎都吃频域输入:ft_freqanalysis 必须 cfg.output 用 fourier 且 keeptrials 为 yes
- 流水线四步:ft_preprocessing → ft_freqanalysis → ft_connectivityanalysis → ft_connectivityplot
- cfg.channelcmb 限定通道对 + cfg.foilim 限定频段 = 控制内存与计算量的两道闸;cfg.bandwidth 只用于 psi/plm 方向性指标
- 源空间"先源后连":beamformer 虚拟电极时间序列再走同一套频域流程(细节见模块 7),并留意空间泄漏
- 组级分析 = 被试×边值矩阵 + 6.2 的多重比较校正;wPLI 结果在 wplispctrm,强度记得取绝对值
- 报错先查三件事:method 大小写、有没有 fourier、试次维还在不在
✏️ 课后练习
- 用自己(或公开数据集)的一个被试跑通第三节全流程:把
cfg.foilim分别设为[8 13]和[13 30]各跑一遍,对比两次ft_connectivityplot的结果差异。 - 把
cfg.channelcmb从 3 对扩到 10 对,记录运行时间与conn字段变化;再用conn2matrix+ 6.2 的draw_circle_net画出这 10 对边的网络图。 - 挑战:给第七节的组级骨架补上日志文件与质控图(模仿模块 2.4 的 try-catch 模式),跑 2 个"被试"验证单个失败不中断整批,并用
fdr_bh报告显著边数。