Buckets:

illusion-brain/brain-domains / scripts /exp42_tau_vs_decode.py
redmingwei's picture
download
raw
12.9 kB
"""Phase 42: τ 与解码能力是否相关?—— 构念效度检验(10 被试)。
## 要检验的假设
文献中的核心论断:τ 反映「神经整合窗口」——τ 越长,回路整合信息的时间
跨度越大,理论上应该支持更强的任务表现。
**可检验预测**:τ 长的被试,图像解码准确率应更高。
若成立 → τ 通过了**构念效度**检验(不只是"测得出",还"有意义")
若不成立 → τ 只是个统计量,没有功能意义
## 设计
10 个被试,每人算两个量:
τ —— ACF 全曲线指数拟合(已验证:5/5 通过分辨率检验)
acc@200 —— 200 类解码准确率(已验证:15–25× 随机)
然后:
1. **Spearman 相关** τ vs acc(非参数,n=10)
2. **必须控制混淆**:τ 和解码都可能受信号质量影响
- 控制「信噪比」:用 1/f 指数或 ACF 初值做代理
- 控制「有效维度」:PCA 前 32 维的解释方差
3. **留一交叉验证**:n=10 太小,必须看 LOO 稳定性
## 关键:必须报告"没有"的可能
n=10 时,Spearman |ρ| > 0.65 才 p<0.05。
若 ρ 在 0.3–0.6 之间,**既不能说有、也不能说没有** —— 必须如实报告功效不足。
这是本项目的纪律:**不做"接近显著"的过度解读。**
## 输出
runs/tau-vs-decode/
├── results.csv 逐被试:τ、acc、SNR代理、有效维度
└── report.json 相关分析 + 控制分析 + LOO
"""
from __future__ import annotations
import json
import os
import sys
import time
from pathlib import Path
import numpy as np
import pandas as pd
from scipy.optimize import curve_fit
from scipy.stats import spearmanr
FS = 250.0
EEG_DIR = Path("/domains/omnibrain4d/eeg")
OUT = Path("/domains/runs/tau-vs-decode")
SUBJECTS = list(range(1, 11))
N_PCS = 32
MAX_IMAGES = 40
K_DECODE = 200
N_TRIALS = 2
MAX_LAG_MS = 1000.0
BANDS = [(1, 4), (4, 8), (8, 13), (13, 30), (30, 45)]
WIN_MS = (50, 500)
N_TIME_BINS = 10
# ------------------------------------------------------------ ACF / τ
def acf_fft(x, max_lag):
T = x.shape[0]
xc = x - x.mean(axis=0, keepdims=True)
n = 1
while n < 2 * T:
n <<= 1
f = np.fft.rfft(xc, n=n, axis=0)
acf = np.fft.irfft(f * np.conj(f), n=n, axis=0)[:max_lag + 1]
norms = np.arange(T, T - max_lag - 1, -1)[:, None]
acf = acf / np.maximum(norms, 1)
return acf / np.maximum(acf[0:1], 1e-12)
def estimate_tau(P, dt_ms, max_lag):
a = np.median(acf_fft(P, max_lag), axis=1)
lags = np.arange(len(a))
ok = np.isfinite(a) & (a > 0.02) & (a < 0.99)
if ok.sum() < 5:
return None, None
def f(t, tt):
return np.exp(-t / np.maximum(tt, 1e-3))
try:
popt, _ = curve_fit(f, lags[ok] * dt_ms, a[ok], p0=[200.0],
bounds=([5.0], [5000.0]), maxfev=8000)
return float(popt[0]), float(a[1]) # τ, ACF(lag=1) 作为 SNR 代理
except Exception:
return None, None
# ------------------------------------------------------------ 特征 / 解码
def build_features(eeg):
a = int(WIN_MS[0] / 1000 * FS)
b = min(int(WIN_MS[1] / 1000 * FS), eeg.shape[-1])
X = eeg.astype(np.float32)
base = X[:, :, :, :max(a, 1)].mean(axis=3, keepdims=True)
Xw = X[:, :, :, a:b] - base
n_img, n_rep, n_ch, _ = Xw.shape
seg = (b - a) // N_TIME_BINS
Tb = Xw[:, :, :, :seg * N_TIME_BINS].reshape(
n_img, n_rep, n_ch, N_TIME_BINS, seg).mean(axis=4)
return Tb.reshape(n_img, n_rep, n_ch * N_TIME_BINS)
def _ridge_fit_predict(Xtr, ytr, Xte, n_class, lam=1.0):
n, d = Xtr.shape
Y = np.zeros((n, n_class), dtype=np.float32)
Y[np.arange(n), ytr] = 1.0
A = Xtr.T @ Xtr
A.flat[:: d + 1] += lam * np.trace(A) / d
try:
W = np.linalg.solve(A, Xtr.T @ Y)
except np.linalg.LinAlgError:
W = np.linalg.lstsq(A, Xtr.T @ Y, rcond=None)[0]
return (Xte @ W).argmax(axis=1)
def decode_once(Xf, k, seed):
n_img, n_rep, feat = Xf.shape
if k > n_img:
return None
rng = np.random.default_rng(seed)
n_tr = n_rep // 2
cls = rng.choice(n_img, k, replace=False)
Xs = Xf[cls].reshape(-1, feat)
y = np.repeat(np.arange(k), n_rep)
tr = np.concatenate([np.arange(i * n_rep, i * n_rep + n_tr) for i in range(k)])
te = np.concatenate([np.arange(i * n_rep + n_rep - n_tr, (i + 1) * n_rep)
for i in range(k)])
Xtr, ytr = Xs[tr], y[tr]
Xte, yte = Xs[te], y[te]
mu = Xtr.mean(0, keepdims=True)
sd = Xtr.std(0, keepdims=True) + 1e-6
pred = _ridge_fit_predict((Xtr - mu) / sd, ytr, (Xte - mu) / sd, k)
return float((pred == yte).mean())
def decode_multi(Xf, k, n_trials, seed0):
accs = []
for t in range(n_trials):
a = decode_once(Xf, k, seed0 + t * 1000 + k)
if a is not None:
accs.append(a)
if not accs:
return None, None
return float(np.mean(accs)), float(np.std(accs))
# ------------------------------------------------------------ 逐被试
def analyze(eeg, sid):
n_img, n_rep, n_ch, T = eeg.shape
d = min(N_PCS, n_ch)
n_use = min(n_img, MAX_IMAGES)
dt_ms = 1000.0 / FS
max_lag = int(min(MAX_LAG_MS / dt_ms, T - 2))
# ---- τ + SNR 代理 + 有效维度
taus, acf1s, evrs = [], [], []
for i in range(n_use):
seg = eeg[i].astype(np.float32)
X = seg.transpose(1, 0, 2).reshape(n_ch, n_rep * T)
mu = X.mean(axis=1, keepdims=True)
sd = X.std(axis=1, keepdims=True) + 1e-6
Z = ((X - mu) / sd).T
Zc = Z - Z.mean(axis=0, keepdims=True)
try:
U, S, _ = np.linalg.svd(Zc, full_matrices=False)
except np.linalg.LinAlgError:
continue
P = (U[:, :d] * S[:d]).astype(np.float64)
t, a1 = estimate_tau(P, dt_ms, max_lag)
if t is not None:
taus.append(t)
if a1 is not None:
acf1s.append(a1)
# 有效维度(参与率)
lam = S ** 2
evrs.append(float((lam.sum() ** 2) / max((lam ** 2).sum(), 1e-12)))
# ---- 解码
Xf = build_features(eeg)
acc, acc_std = decode_multi(Xf, K_DECODE, N_TRIALS, sid * 7919)
return {
"subject": f"sub-{sid:02d}",
"tau_ms": round(float(np.median(taus)), 1) if taus else None,
"tau_iqr": (round(float(np.percentile(taus, 75) - np.percentile(taus, 25)), 1)
if taus else None),
"acf1": round(float(np.median(acf1s)), 4) if acf1s else None,
"eff_dim": round(float(np.median(evrs)), 2) if evrs else None,
"acc_k200": round(acc, 5) if acc is not None else None,
"acc_std": round(acc_std, 5) if acc_std is not None else None,
"chance": 1.0 / K_DECODE,
"ratio": round(acc / (1.0 / K_DECODE), 2) if acc else None,
"n_images": len(taus),
}
def main():
OUT.mkdir(parents=True, exist_ok=True)
print("=" * 82, flush=True)
print("Phase 42 — τ 与解码能力是否相关(构念效度,10 被试)", flush=True)
print("=" * 82, flush=True)
print(f" τ: ACF 全曲线 | acc: {K_DECODE} 类 × {N_TRIALS} 次 | "
f"{N_PCS} 维 | {MAX_IMAGES} 图像", flush=True)
rows = []
t0 = time.time()
for sid in SUBJECTS:
f = EEG_DIR / f"sub-{sid:02d}.npz"
if not f.exists():
continue
eeg = np.load(f)["eeg"]
t1 = time.time()
r = analyze(eeg, sid)
rows.append(r)
del eeg
print(f" {r['subject']}: τ={r['tau_ms']:>6} ms | "
f"acc={r['acc_k200']:.4f} ({r['ratio']:.1f}x) | "
f"acf1={r['acf1']:.3f} | eff_dim={r['eff_dim']:.1f} "
f"[{time.time()-t1:.0f}s]", flush=True)
df = pd.DataFrame(rows)
df.to_csv(OUT / "results.csv", index=False)
print(f"\n 记录 {len(df)} | 耗时 {time.time()-t0:.0f}s", flush=True)
d = df.dropna(subset=["tau_ms", "acc_k200"])
n = len(d)
if n < 5:
print("样本不足", flush=True)
return
print("\n" + "=" * 82, flush=True)
print(f"相关分析(n={n})", flush=True)
print("=" * 82, flush=True)
def report(x, y, name):
rho, p = spearmanr(x, y)
print(f" {name:<26} ρ = {rho:+.3f} p = {p:.3f}", flush=True)
return float(rho), float(p)
rho_tau, p_tau = report(d.tau_ms, d.acc_k200, "τ vs acc")
rho_snr, p_snr = report(d.acf1, d.acc_k200, "ACF(lag1) vs acc [SNR代理]")
rho_eff, p_eff = report(d.eff_dim, d.acc_k200, "eff_dim vs acc")
rho_te, p_te = report(d.tau_ms, d.eff_dim, "τ vs eff_dim")
# ---- 控制 SNR / 有效维度后的偏相关
print("\n" + "-" * 82, flush=True)
print("偏相关(控制 SNR 代理 acf1)", flush=True)
print("-" * 82, flush=True)
def partial_spearman(x, y, z):
rx = pd.Series(x).rank()
ry = pd.Series(y).rank()
rz = pd.Series(z).rank()
def resid(a, b):
A = np.c_[np.ones(len(b)), b]
coef, *_ = np.linalg.lstsq(A, a, rcond=None)
return a - A @ coef
ex = resid(rx.to_numpy(), rz.to_numpy())
ey = resid(ry.to_numpy(), rz.to_numpy())
r, p = spearmanr(ex, ey)
return float(r), float(p)
pr_snr, pp_snr = partial_spearman(d.tau_ms, d.acc_k200, d.acf1)
print(f" τ vs acc | acf1 ρ = {pr_snr:+.3f} p = {pp_snr:.3f}", flush=True)
pr_eff, pp_eff = partial_spearman(d.tau_ms, d.acc_k200, d.eff_dim)
print(f" τ vs acc | eff_dim ρ = {pr_eff:+.3f} p = {pp_eff:.3f}", flush=True)
# ---- LOO 稳定性
print("\n" + "-" * 82, flush=True)
print("留一交叉验证(移除每个被试后重算 ρ)", flush=True)
print("-" * 82, flush=True)
loos = []
for i in range(n):
sub = d.drop(d.index[i])
r, _ = spearmanr(sub.tau_ms, sub.acc_k200)
loos.append(float(r))
print(f" ρ 范围: [{min(loos):+.3f}, {max(loos):+.3f}] 中位 {np.median(loos):+.3f}",
flush=True)
same_sign = all(np.sign(v) == np.sign(rho_tau) for v in loos)
print(f" 符号一致: {'是' if same_sign else '否'}", flush=True)
# ---- 判定(严格)
print("\n" + "=" * 82, flush=True)
print("判定", flush=True)
print("=" * 82, flush=True)
if p_tau < 0.05 and rho_tau > 0.5 and same_sign:
verdict = (f"✅ 构念效度成立:τ 与解码准确率显著正相关 "
f"(ρ={rho_tau:+.2f}, p={p_tau:.3f}),LOO 符号一致")
elif p_tau < 0.05 and rho_tau < -0.5:
verdict = (f"⚠️ 显著负相关 (ρ={rho_tau:+.2f}) —— 与'整合窗口'假设相反,"
"需查是否受混淆驱动")
elif abs(rho_tau) < 0.3:
verdict = (f"❌ 无相关 (ρ={rho_tau:+.2f}, p={p_tau:.3f}) —— "
"τ 不预测解码能力,没有功能意义")
else:
# 关键:诚实报告功效不足
power_needed = "|ρ| > 0.65"
verdict = (f"⚠️ **功效不足**:ρ={rho_tau:+.2f}, p={p_tau:.3f}。"
f"n={n} 时需 {power_needed} 才能 p<0.05。"
"既不能声称有、也不能声称没有。")
print(f" {verdict}", flush=True)
# 控制后的解读
print(f"\n 控制 SNR 后: ρ={pr_snr:+.2f} (p={pp_snr:.3f})", flush=True)
print(f" 控制 eff_dim 后: ρ={pr_eff:+.2f} (p={pp_eff:.3f})", flush=True)
if abs(pr_snr) < abs(rho_tau) * 0.6:
print(" → 大部分相关可由信号质量解释,不是 τ 的独立贡献", flush=True)
(OUT / "report.json").write_text(json.dumps({
"task": "tau_vs_decode",
"n_subjects": int(n),
"hypothesis": "τ 反映神经整合窗口,应预测解码能力",
"per_subject": rows,
"correlations": {
"tau_vs_acc": {"rho": round(rho_tau, 3), "p": round(p_tau, 4)},
"acf1_vs_acc": {"rho": round(rho_snr, 3), "p": round(p_snr, 4)},
"eff_dim_vs_acc": {"rho": round(rho_eff, 3), "p": round(p_eff, 4)},
"tau_vs_eff_dim": {"rho": round(rho_te, 3), "p": round(p_te, 4)},
},
"partial": {
"tau_acc_ctrl_acf1": {"rho": round(pr_snr, 3), "p": round(pp_snr, 4)},
"tau_acc_ctrl_effdim": {"rho": round(pr_eff, 3), "p": round(pp_eff, 4)},
},
"loo": {"min": round(min(loos), 3), "max": round(max(loos), 3),
"median": round(float(np.median(loos)), 3),
"sign_consistent": bool(same_sign)},
"power_note": f"n={n} 时 Spearman 需 |rho|>0.65 才 p<0.05",
"verdict": verdict,
"elapsed_s": round(time.time() - t0, 1),
}, indent=2, ensure_ascii=False), encoding="utf-8")
print(f"\n 耗时 {time.time()-t0:.0f}s", flush=True)
if __name__ == "__main__":
main()

Xet Storage Details

Size:
12.9 kB
·
Xet hash:
b1e8d6d724e2d9bad4b2563b2c86bd52498cd06f3ebd947548a3c510ca580b43

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.