Buckets:

illusion-brain/brain-domains / scripts /exp41_var_vs_acf.py
redmingwei's picture
download
raw
12.5 kB
"""Phase 41: 一锤定音 —— VAR(1) 极点 vs 单变量 ACF,谁该被信任。
## exp40 的关键发现
同一批数据、同样的降采样:
直接 ACF 指数拟合:186.0 → 185.2 (2x) → 185.9 (4x) 比值 1.000 稳定
VAR(1) 谱半径 :148.7 → 166.1 (2x) → 237.3 (4x) 比值 1.60 漂移
**问题不在采样率,在 VAR(1) 这个模型。**
### 为什么
多元 VAR(1) 的谱半径 ρ(A) 对应的是**最慢集体模态**,不是任一单变量的
边际自相关衰减率。多变量耦合产生集体模态谱,粗采样滤掉快模态后,
慢模态在归一化后占比变大 → 谱半径变大 → τ 变大。
而单变量 ACF 是**边际统计量**,不依赖变量间的耦合结构,
所以对降采样稳定(只要 τ >> 采样间隔)。
## 本实验:三个对比
### 对比 1: 修 τ_int(Sokal 窗口 bug)
exp40 的 τ_int = 4.0 ms(= 1 bin),明显错误。
根因:`taus[0] = 2*ρ(0) - 1 = 1`,此时条件 `M ≥ c·τ` 立即满足(0 < 5)。
修正:从 lag=1 开始搜索,要求 τ 累积值先上升再找稳定点。
### 对比 2: VAR(1) vs ACF 在多个 decimation 下的稳定性
同一批片段、同一 PCA 空间,两种方法各算 τ,比较比值。
### 对比 3: 用第一主成分(单变量)做 VAR(1)
如果单变量 VAR(1) 也稳定,说明问题在"多元"而非"VAR"。
如果单变量 VAR(1) 也漂移,说明 VAR 这个模型族本身不适合。
## 判据
单变量 ACF 稳定 + 多元 VAR 漂移 + 单变量 VAR 稳定
→ 结论:**用单变量 ACF;多元 VAR 的谱半径不是边际时间常数**
单变量 ACF 稳定 + 两种 VAR 都漂移
→ 结论:**VAR 族不适合,只用 ACF**
"""
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
FS = 250.0
EEG_DIR = Path("/domains/omnibrain4d/eeg")
OUT = Path("/domains/runs/var-vs-acf")
N_PCS = 32
MAX_IMAGES = 25
SUBJECTS = [1, 2, 3, 4, 5]
DECS = [1, 2, 4]
MAX_LAG_MS = 1000.0
# ------------------------------------------------------------ ACF
def acf_fft(x, max_lag):
"""x: (T, d) → (max_lag+1, d) 无偏归一化 ACF。"""
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 fit_exp_tau(lags, acf_vals, dt_ms):
ok = np.isfinite(acf_vals) & (acf_vals > 0.02) & (acf_vals < 0.99)
if ok.sum() < 4:
return None
t = lags[ok] * dt_ms
y = acf_vals[ok]
def f(tt, T):
return np.exp(-tt / np.maximum(T, 1e-3))
try:
popt, _ = curve_fit(f, t, y, p0=[100.0], bounds=([1.0], [10000.0]),
maxfev=8000)
return float(popt[0])
except Exception:
return None
def integrated_tau(acf_vals, dt_ms, c=5.0):
"""Sokal 积分自相关时间(修正版)。
修正:从 lag=1 开始搜索。lag=0 时 τ_int=1,条件立即满足,会误判。
正确做法:累积 τ(M) = 1 + 2Σ_{t=1}^{M} ρ(t),找最小 M 使 M ≥ c·τ(M)。
"""
n = len(acf_vals)
if n < 4:
return None
taus = np.cumsum(2.0 * acf_vals) - 1.0 # taus[M] = 1 + 2Σ_{1..M}ρ - 1? 见下
# 正确累积:τ(M) = 1 + 2 Σ_{t=1..M} ρ(t)
taus = 1.0 + 2.0 * np.cumsum(acf_vals[1:]) # 长度 n-1,索引对应于 M=1..n-1
Ms = np.arange(1, n)
m = Ms >= c * taus
if np.any(m):
idx = int(np.argmax(m))
return float(taus[idx] * dt_ms)
return None
# ------------------------------------------------------------ VAR(1)
def var_pole_tau(x, dt_ms, ridge=1e-2):
"""多元 VAR(1):拟合 x_t = A x_{t-1},返回最慢极点对应 τ。"""
x0, x1 = x[:-1], x[1:]
if len(x0) < 2 * x.shape[1]:
return None
x0c = x0 - x0.mean(axis=0, keepdims=True)
x1c = x1 - x1.mean(axis=0, keepdims=True)
G = x0c.T @ x0c
d = G.shape[0]
G.flat[:: d + 1] += ridge * np.trace(G) / d
try:
A = np.linalg.solve(G, x0c.T @ x1c)
except np.linalg.LinAlgError:
return None
ev = np.abs(np.linalg.eigvals(A))
ev = ev[(ev > 1e-4) & (ev < 1.0)]
if len(ev) == 0:
return None
lam = float(np.sort(ev)[::-1][0])
return float(-1.0 / np.log(lam) * dt_ms)
def scalar_var_tau(x1d, dt_ms, ridge=1e-2):
"""单变量 AR(1):x_t = a x_{t-1} + e,τ = -dt/ln|a|。"""
x0, x1 = x1d[:-1], x1d[1:]
x0c = x0 - x0.mean()
x1c = x1 - x1.mean()
num = float(x0c @ x1c)
den = float(x0c @ x0c) + ridge * float(x0c @ x0c)
if den <= 1e-12:
return None
a = num / den
a = abs(a)
if not (1e-4 < a < 1.0):
return None
return float(-1.0 / np.log(a) * dt_ms)
# ------------------------------------------------------------ 主分析
def decimate_naive(P, dec):
return P[::dec]
def decimate_filt(P, dec):
from scipy import signal
return signal.decimate(P, dec, axis=0, ftype="iir", zero_phase=True)
def analyze_subject(eeg, sid, max_images=MAX_IMAGES):
n_img, n_rep, n_ch, T = eeg.shape
d = min(N_PCS, n_ch)
n_use = min(n_img, max_images)
rows = []
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)
row = {"subject": sid, "image": i}
for dec in DECS:
dt = 1000.0 * dec / FS
if dec == 1:
Pd = P
else:
if P.shape[0] < 2 * dec + 10:
continue
Pd = decimate_naive(P, dec) # 用 naive(exp40 显示它稳定)
max_lag = int(min(MAX_LAG_MS / 1000 * FS, Pd.shape[0] - 2)) // dec
a = np.median(acf_fft(Pd, max_lag), axis=1)
lags = np.arange(len(a))
row[f"acf_{dec}x"] = fit_exp_tau(lags, a, dt)
row[f"int_{dec}x"] = integrated_tau(a, dt)
row[f"mvar_{dec}x"] = var_pole_tau(Pd, dt)
row[f"svar_{dec}x"] = scalar_var_tau(Pd[:, 0], dt)
rows.append(row)
return rows
def main():
OUT.mkdir(parents=True, exist_ok=True)
print("=" * 86, flush=True)
print("Phase 41 — VAR(1) 极点 vs 单变量 ACF:谁可信", flush=True)
print("=" * 86, flush=True)
print(f" 被试 {SUBJECTS} | {N_PCS} 维 | {MAX_IMAGES} 图像 | dec {DECS}",
flush=True)
all_rows = []
t0 = time.time()
for sid in SUBJECTS:
f = EEG_DIR / f"sub-{sid:02d}.npz"
if not f.exists():
continue
print(f"\n --- sub-{sid:02d} ---", flush=True)
eeg = np.load(f)["eeg"]
t1 = time.time()
rows = analyze_subject(eeg, f"sub-{sid:02d}")
all_rows += rows
del eeg
df = pd.DataFrame(rows)
if len(df):
print(f" ACF : 4ms {df.acf_1x.median():6.1f} | "
f"2x {df.acf_2x.median():6.1f} | 4x {df.acf_4x.median():6.1f}",
flush=True)
print(f" mVAR : 4ms {df.mvar_1x.median():6.1f} | "
f"2x {df.mvar_2x.median():6.1f} | 4x {df.mvar_4x.median():6.1f}",
flush=True)
print(f" sVAR : 4ms {df.svar_1x.median():6.1f} | "
f"2x {df.svar_2x.median():6.1f} | 4x {df.svar_4x.median():6.1f}",
flush=True)
print(f" τ_int : 4ms {df.int_1x.median():6.1f} | "
f"2x {df.int_2x.median():6.1f} | 4x {df.int_4x.median():6.1f} "
f"[{time.time()-t1:.1f}s]", flush=True)
df = pd.DataFrame(all_rows)
df.to_csv(OUT / "results.csv", index=False)
print(f"\n 记录 {len(df)} | 耗时 {time.time()-t0:.0f}s", flush=True)
if not len(df):
return
# ============ 汇总
methods = {
"ACF 指数拟合": ("acf_", "单变量边际"),
"单变量 AR(1)": ("svar_", "单变量参数"),
"多元 VAR(1) 极点": ("mvar_", "多元集体模态"),
"积分自相关 τ_int": ("int_", "非参数"),
}
print("\n" + "=" * 86, flush=True)
print("四种方法的降采样稳定性(τ ms,中位)", flush=True)
print("=" * 86, flush=True)
print(f"{'方法':<22} {'性质':<14} {'4ms':>8} {'2x':>8} {'4x':>8} "
f"{'比值4x':>8} {'判定':>8}", flush=True)
summary = {}
for name, (pre, kind) in methods.items():
v1 = df[f"{pre}1x"].median()
v2 = df[f"{pre}2x"].median()
v4 = df[f"{pre}4x"].median()
if not np.isfinite(v1) or v1 <= 0:
continue
r4 = v4 / v1
ok = 0.75 <= r4 <= 1.35
summary[name] = {"kind": kind, "tau_4ms": round(float(v1), 1),
"tau_2x": round(float(v2), 1),
"tau_4x": round(float(v4), 1),
"ratio_4x": round(float(r4), 3),
"consistent": bool(ok)}
print(f"{name:<22} {kind:<14} {v1:>8.1f} {v2:>8.1f} {v4:>8.1f} "
f"{r4:>8.3f} {'✅' if ok else '❌':>8}", flush=True)
# ============ 判定
print("\n" + "=" * 86, flush=True)
print("判定", flush=True)
print("=" * 86, flush=True)
acf_ok = summary.get("ACF 指数拟合", {}).get("consistent", False)
mvar_ok = summary.get("多元 VAR(1) 极点", {}).get("consistent", False)
svar_ok = summary.get("单变量 AR(1)", {}).get("consistent", False)
print(f" ACF 指数拟合 : {'✅ 稳定' if acf_ok else '❌ 漂移'}", flush=True)
print(f" 单变量 AR(1) : {'✅ 稳定' if svar_ok else '❌ 漂移'}", flush=True)
print(f" 多元 VAR(1) : {'✅ 稳定' if mvar_ok else '❌ 漂移'}", flush=True)
if acf_ok and (not mvar_ok):
if svar_ok:
verdict = ("✅ **结论明确**:单变量方法(ACF 指数拟合、AR(1))都稳定;"
"多元 VAR(1) 的谱半径不稳定。\n"
" 多元 VAR 的谱半径是'最慢集体模态',粗采样滤掉快模态后"
"慢模态占比变大 → 谱半径变大 → τ 变大。\n"
f" **可信的 τ ≈ {summary['ACF 指数拟合']['tau_4ms']:.0f} ms**"
"(ACF 指数拟合,4ms 基准)")
else:
verdict = ("⚠️ ACF 稳定但两种 VAR 都不稳 → VAR 模型族不适用,只用 ACF。"
f"τ ≈ {summary['ACF 指数拟合']['tau_4ms']:.0f} ms")
elif acf_ok and mvar_ok:
verdict = "✅ 四种方法都稳定 —— 之前的漂移可能是实现问题,现已修正"
else:
verdict = ("❌ ACF 也不稳定 —— τ 在当前数据上不可识别。"
"应改用 1/f 指数(exp40 显示跨度仅 0.064)")
print(f"\n {verdict}", flush=True)
(OUT / "report.json").write_text(json.dumps({
"task": "var_vs_acf",
"fs": FS, "n_pcs": N_PCS, "max_images": MAX_IMAGES,
"subjects": SUBJECTS, "decs": DECS,
"methods": summary,
"verdict": verdict,
"explanation": (
"多元 VAR(1) 的谱半径对应最慢集体模态,不是边际自相关衰减率。"
"多变量耦合产生集体模态谱;粗采样滤掉快模态后,慢模态在归一化后"
"占比变大 → 谱半径变大 → τ 变大。单变量 ACF 是边际统计量,"
"不依赖变量间耦合,故对降采样稳定。"
),
"tau_int_fix": ("exp40 的 τ_int = 4.0ms 是 Sokal 窗口 bug:lag=0 时 "
"τ=1,条件 M≥c·τ 立即满足。已改为从 lag=1 开始搜索。"),
"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.5 kB
·
Xet hash:
48cb62f39286100532c437fc6c6bcc0b2887e2b6042965e7434b3a8c0e99c3c3

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