阶段四 · 连接与溯源 · 模块 6 · 第 17 课 / 共 28 课

基于 FieldTrip 的功能连接实践

把 6.1 的原理和 6.2 的统计装进 FieldTrip 的工程流水线:数据进来、矩阵出去、 图自动画好,最后搭出能跑一整批被试的组级骨架。75 分钟大课,代码管够。

🕐 75 分钟🎯 难度:高阶✅ 前置:6.2(源空间部分可参考模块 7,允许前后互链)

🎯 本节学习目标

  • 说清 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要求的输入输出字段特点
cohfreq(fourier 谱)cohspctrm幅值+相位混合,不抗零滞后
plvfreq(fourier 谱)plvspctrm相位同步集中度,不抗零滞后
wpli / wpli_debiasedfreq(fourier 谱)wplispctrm抗零滞后,头皮水平首选
ppcfreqppcspctrm相位一致性,对试次数不敏感的改良版
corrraw / timelockcorr时域相关,最简单
granger / dtf / pdcmvar 或 freq各 method 对应字段方向性(有效连接),本课不展开

和 6.1 的关系:6.1 手算 PLV 走的是"时域希尔伯特相位差"路线;FieldTrip 的 plv 走频域互谱路线——思想相同(相位差的集中度),实现路径不同。殊途同归, 所以 6.1 的直觉全部适用。

📖 概念:labelcmb(通道组合)

FieldTrip 连接输出的核心字段:labelcmb 是 N×2 的通道对列表(如 F3 与 O1), 与之配套的 *spctrm 数组按同样的顺序存放每条边的值。看懂这两个字段, 你就能把任何连接结果翻译成 6.2 画的 N×N 矩阵。

常用 cfg 字段速查(均为真实字段名):

字段作用
cfg.method连接指标,全小写:'coh''plv''wpli' 等(写成大写会报错)
cfg.channelcmbN×2 cell,指定只算哪些通道对——控制内存的第一道闸
cfg.trials指定用哪些试次参与计算
cfg.complex'abs'(默认)/ 'imag' / 'angle':coh/csd/plv 输出的复数形式;取 'imag' 得"虚部相干",也是抗零滞后的老思路
cfg.partchannelpartial coherence 时要剔除的通道
cfg.bandwidth只有 'psi'(相位斜率指数)和 'plm' 这类方向性指标需要:跨频率积分的半带宽(Hz)。算 wPLI/相干用不到它

三、头皮电极水平完整流程(主代码)

📌 为什么必须是 fourier 而不是 power

功率谱 = 复数频谱自己乘自己的共轭,相位信息在这一步被彻底扔掉(模块 4 讲过)。 而所有相位类连接指标都要相位。所以在 ft_freqanalysis 里必须 cfg.output = 'fourier',并保留试次维——互谱由 ft_connectivityanalysis 内部跨试次完成。

Matlab · 头皮水平连接分析(单被试完整流程)
%% ===== 头皮电极水平连接分析(单被试完整流程) =====
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 行的小工具是本课最重要的"胶水":

Matlab · conn2matrix.m
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 的圆形网络图

五、源空间连接:"先源后连"

📖 概念:虚拟电极(virtual electrode)

用逆问题方法(如 beamformer)在某个源点/ROI 上"重建"出来的时间序列。它不再是一个 电极对所有源的混合,而是试图还原该位置神经活动本身——用它算连接, 才是在"两个产生信号的地方"之间算连接。

为什么要"先源后连":头皮上任何两电极的信号都互含对方的成分(容积传导), 连接指标天生被污染;先定位源、再取 ROI 时间序列,混合在源水平被大幅削弱。 思路一句话:节点先定准,再谈边。头模型、leadfield、beamformer 细节 属于模块 7(本模块与它允许前后互链),这里给骨架:

Matlab · 源空间连接骨架(头模型细节见模块 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 的统计与校正——这就是一篇连接论文的全部工程骨架:

Matlab · 组级骨架(循环 + 统计 + 存档)
%% ===== 组级骨架:循环被试 -> 被试 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 无效或 unsupportedcfg.method 大小写/拼写错(必须全小写,如 'wpli'对照第二节速查表逐字母核对
提示输入数据类型不对 / 找不到频谱给了功率谱或时域数据ft_freqanalysis 里设 cfg.output = 'fourier'
提示数据应为 rpt 格式试次维被平均掉了检查 cfg.keeptrials = 'yes' 是否生效
内存爆掉 / 卡死全通道对 × 全频段 × 多 tapercfg.channelcmb 限定通道对 + cfg.foilim 限定频段
ft_connectivityplot 空白或报错cfg.parameter 与输出字段名不一致fieldnames(conn) 抄准确的名字(如 'wplispctrm'
'psi' 时报需要 bandwidth该字段只服务方向性指标按 help 给 cfg.bandwidth 赋值(Hz),或改用无向指标
试次长度不一致报错分段不齐ft_redefinetrialcfg.length 统一切段
读 .set 报错路径含中文/空格,或没跑 ft_defaults数据移到英文路径;脚本开头执行 ft_defaults

九、本模块收官检查单

从 6.1 到 6.3,一条完整的连接分析链路。离开本模块前自查:

  1. 指标:能说清为什么选 wPLI/相干,公式和容积传导立场都能口头复述;
  2. 数据:Fourier 谱 + 试次维齐备,频段、平滑、试次数都有记录;
  3. 统计:2016 条边的多重比较有明确校正方案,阈值做过敏感性检查;
  4. :矩阵图 + 网络图各一张,图注完整;
  5. 存档:每被试 .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、试次维还在不在

✏️ 课后练习

  1. 用自己(或公开数据集)的一个被试跑通第三节全流程:把 cfg.foilim 分别设为 [8 13][13 30] 各跑一遍,对比两次 ft_connectivityplot 的结果差异。
  2. cfg.channelcmb 从 3 对扩到 10 对,记录运行时间与 conn 字段变化;再用 conn2matrix + 6.2 的 draw_circle_net 画出这 10 对边的网络图。
  3. 挑战:给第七节的组级骨架补上日志文件与质控图(模仿模块 2.4 的 try-catch 模式),跑 2 个"被试"验证单个失败不中断整批,并用 fdr_bh 报告显著边数。