Buckets:
| #!/usr/bin/env python3 | |
| """exp74 — 多模型特征对比:ViT-H-14 vs ViT-B-32(同一套 200 选 1 解码协议) | |
| ## 要回答的问题 | |
| > **更好的视觉特征 ⇒ 更好的脑解码吗?** | |
| 桶内已存在**两个** CLIP 在 THINGS-EEG 图像上的特征: | |
| | 特征集 | 文件 | 维度 | 模型 | | |
| |---|---|---:|---| | |
| | ViT-H-14 | `eeg/eeg-image-decode/ViT-H-14_features_{train,test}.pt` | 1024 | CLIP ViT-H/14 | | |
| | ViT-B-32 | `eeg/things-eeg/Preprocessed_data_250Hz_whiten/ViT-B-32_features_{train,test}.pt` | 512 | CLIP ViT-B/32 | | |
| 两者同族、同训练数据,计算量差 ~144×(632 vs 4.4 GFLOP/图),维度差 2×。 | |
| **两种结果都有意义**: | |
| - ViT-H-14 明显更好 ⇒ 特征质量确实决定脑解码质量(与"更大的模型更脑对齐"的文献一致); | |
| - 两者相当 ⇒ **脑读出已经饱和**,瓶颈在 EEG 侧而不在特征侧 | |
| (这本身就是一条重要结论:换更强的视觉模型救不了这件事)。 | |
| **不需要任何新的特征提取** —— 换个特征文件重跑同一套管线即可。 | |
| ## 前置:ViT-B-32 的行序**从未被验证过** | |
| 这正是当年"缺陷 C(概念顺序与特征顺序的对齐从未验证)"的同类风险。 | |
| 若行序错位,ViT-B-32 的解码准确率会掉到随机,届时**无法区分** | |
| "模型更弱"与"行序错位"⇒ 结论会变成一句废话。 | |
| 所以本脚本先做**只用特征、不碰 EEG**的对齐审计: | |
| 1. **对角线检验**:ViT-H-14 与 ViT-B-32 逐行配对的余弦相似度 | |
| vs 随机配对。ViT-H-14 与 EEG 的 1:1 对齐已被 exp64/66/68p 独立验证 | |
| (顺序对齐 p=0.005、522σ),因此**若两特征集逐行对齐,ViT-B-32 必然也对齐**。 | |
| 2. **概念组检验**:训练集的概念 = 图像清单里的**连续 10 张一组** | |
| (exp73b 已从桶内清单直接核实)。若某特征集的行序与图像清单一致, | |
| 则"连续 10 行的组内平均相似度"应显著高于随机配对。 | |
| 两个检验都只用特征 ⇒ **不依赖 EEG,也不依赖任何未验证的假设**。 | |
| ## 解码对比 | |
| 完全沿用 exp73 的管线(`import exp73_decode200` 复用同一份实现, | |
| 保证两个模型走的是**同一套代码**,不是"看起来一样"的两套代码): | |
| - 估计量:试次级 ridge,`A0 = G_full / n_rep`(exp73a 审计确定) | |
| - 设计维度:全部 630 维(不做窗口选择) | |
| - α:train split 图像级 5-fold CV(单试次预测的 R²),不用 test | |
| - 零分布:置换 train 的「图像 ↔ EEG」对应关系,8 次 | |
| - 指标:单试次 top-1/top-5、80 次平均后 top-1 | |
| **配对比较**:同一被试上比两个模型 ⇒ 用**配对符号检验**(n=10), | |
| 而不是比两个中位数(那是把 10 个被试当 20 个独立样本)。 | |
| ## 合成自测 | |
| 1. 对齐审计:构造"行对齐"与"行打乱"两组特征 ⇒ 对角线检验必须只对前者报警; | |
| 2. 概念组检验:构造"组内相似"与"随机"两组 ⇒ 必须区分; | |
| 3. 配对比较:构造"模型 A 恒优"与"无差异"两组 ⇒ 符号检验必须给对结论。 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import os | |
| import re | |
| import sys | |
| import time | |
| from pathlib import Path | |
| import numpy as np | |
| sys.path.insert(0, str(Path(__file__).resolve().parent)) | |
| import exp73_decode200 as E # noqa: E402 复用同一份解码实现 | |
| FS = 250.0 | |
| N_NULL = 8 | |
| ALPHAS = E.ALPHAS | |
| SEED = E.SEED | |
| TRAIN_4D = E.TRAIN_4D | |
| TEST_4D = E.TEST_4D | |
| OUT_DIR = E.OUT_DIR | |
| FEATURE_SETS = { | |
| "ViT-H-14": ("/training/eeg/eeg-image-decode/ViT-H-14_features_train.pt", | |
| "/training/eeg/eeg-image-decode/ViT-H-14_features_test.pt"), | |
| "ViT-B-32": ("/training/eeg/things-eeg/Preprocessed_data_250Hz_whiten/ViT-B-32_features_train.pt", | |
| "/training/eeg/things-eeg/Preprocessed_data_250Hz_whiten/ViT-B-32_features_test.pt"), | |
| } | |
| GROUP = 10 # 训练集:每概念 10 张连续(exp73b 已核实) | |
| def log(*a): | |
| print(*a, flush=True) | |
| def load_matrix(path, expect_rows): | |
| """读「矩阵式」特征文件(ViT-H-14:键 → (N,d) 张量)。 | |
| 兼容不同 key 名;形状必须与 expect_rows 一致。 | |
| """ | |
| import torch | |
| f = torch.load(path, map_location="cpu", weights_only=False) | |
| if isinstance(f, dict): | |
| key = None | |
| for k in ("img_features", "features", "image_features"): | |
| v = f.get(k) | |
| if hasattr(v, "shape") and len(v.shape) == 2 and v.shape[0] == expect_rows: | |
| key = k | |
| break | |
| if key is None: | |
| for k, v in f.items(): | |
| if hasattr(v, "shape") and len(v.shape) == 2 and v.shape[0] == expect_rows: | |
| key = k | |
| break | |
| if key is None: | |
| raise KeyError(f"{path} 里找不到 {expect_rows} 行的特征矩阵;键={list(f)[:8]}") | |
| arr = f[key].numpy() | |
| else: | |
| arr = f.numpy() | |
| arr = arr.astype(np.float64) | |
| if arr.shape[0] != expect_rows: | |
| raise ValueError(f"{path}: 行数 {arr.shape[0]} ≠ 期望 {expect_rows}") | |
| return arr, {"kind": "matrix", "shape": tuple(arr.shape)} | |
| PATH_RE = re.compile( | |
| r"^(?P<split>[A-Za-z_]*images)/(?P<cid>\d+)_(?P<cname>[^/]+)/(?P<file>[^/]+)$") | |
| def path_dict_to_matrix(d, expect_rows, expect_per_concept=None, label=""): | |
| """路径字典 → (N,d) 矩阵,行序由 (概念号, 文件名) 排序推导;并校验结构。 | |
| `expect_per_concept=None` 时**自行推断**每概念图数(只要求各概念一致), | |
| 避免把"train 10 张 / test 1 张"这种差异写死。 | |
| """ | |
| if len(d) != expect_rows: | |
| raise ValueError(f"{label}: 图像数 {len(d)} ≠ 期望 {expect_rows}") | |
| items = [] | |
| for k, v in d.items(): | |
| m = PATH_RE.match(str(k)) | |
| if not m: | |
| raise ValueError(f"{label}: 图像路径不符合预期格式:{k!r}") | |
| items.append((int(m.group("cid")), m.group("file"), str(k), v)) | |
| items.sort(key=lambda t: (t[0], t[1])) # == 桶内清单的字典序 | |
| cids = sorted({t[0] for t in items}) | |
| if cids != list(range(1, len(cids) + 1)): | |
| raise ValueError(f"{label}: 概念号不连续({cids[:3]}…{cids[-3:]})") | |
| per = {} | |
| for cid, _f, _k, _v in items: | |
| per[cid] = per.get(cid, 0) + 1 | |
| counts = set(per.values()) | |
| if len(counts) != 1: | |
| bad = [c for c, n in per.items() if n != counts.most_common(1)[0][0]][:5] | |
| raise ValueError(f"{label}: 各概念的图数不一致({sorted(counts)}),异常概念 {bad}") | |
| inferred = counts.pop() | |
| if expect_per_concept is not None and inferred != expect_per_concept: | |
| raise ValueError(f"{label}: 每概念 {inferred} 图 ≠ 期望 {expect_per_concept}") | |
| if len(cids) * inferred != expect_rows: | |
| raise ValueError(f"{label}: {len(cids)} 概念 × {inferred} 图 ≠ {expect_rows} 行") | |
| dim = int(items[0][3].shape[-1]) | |
| arr = np.stack([t[3].numpy() for t in items]).astype(np.float64) | |
| if arr.shape != (expect_rows, dim): | |
| raise ValueError(f"{label}: 拼出来是 {arr.shape},期望 {(expect_rows, dim)}") | |
| meta = {"kind": "path_dict", "shape": tuple(arr.shape), | |
| "n_concepts": len(cids), "per_concept": inferred, | |
| "dim": dim, "concept_id_range": [cids[0], cids[-1]], | |
| "first_key": items[0][2], "last_key": items[-1][2]} | |
| return arr, meta | |
| def load_path_dict(path, expect_rows, expect_per_concept): | |
| """读「路径字典式」特征文件(ViT-B-32:'image_features' = {图片路径 → 向量})。 | |
| 关键:**行序不是假定的,而是由图片路径推导出来的**。 | |
| 排序键 = (概念号, 文件名),与桶内 `Image_set` 清单的字典序完全一致 | |
| (概念号 5 位补零 ⇒ 字典序 = 数值序),而 EEG 的行序就是这份清单的顺序 | |
| (exp64/66/68p 已验证:ViT-H-14 与 EEG 1:1,图像路径为 `test_images/00002_antelope/...`)。 | |
| ⇒ 因此这里得到的是**按名字核对**的对齐,而不是"位置刚好对上了"。 | |
| 同时校验结构:概念数、每概念图数、概念号是否连续 —— 任一不符直接报错。 | |
| """ | |
| import torch | |
| f = torch.load(path, map_location="cpu", weights_only=False) | |
| d = None | |
| for k in ("image_features", "img_features", "features"): | |
| if isinstance(f.get(k), dict): | |
| d = f[k] | |
| break | |
| if d is None: | |
| raise KeyError(f"{path} 里找不到 image_features 路径字典;顶层键={list(f)[:8]}") | |
| return path_dict_to_matrix(d, expect_rows, expect_per_concept, label=str(path)) | |
| # --------------------------------------------------------------------------- | |
| # 对齐审计(只用特征,不碰 EEG) | |
| # --------------------------------------------------------------------------- | |
| def _unit(A): | |
| return A / np.maximum(np.linalg.norm(A, axis=1, keepdims=True), 1e-12) | |
| def cross_predict_test(A, B, rng, n_perm=5, fold=0.8): | |
| """A 与 B 是否**逐行**对应(**维度可以不同**)。 | |
| ViT-H-14 是 1024 维、ViT-B-32 是 512 维 ⇒ 没法逐元素做对角线检验。 | |
| 改用维度无关的**交叉预测**:用 B 线性预测 A(ridge,留出行评估 R²), | |
| 对照组 = 把 B 的行打乱后做同样的事。 | |
| ⇒ 若两套特征逐行对应,真配对的留出 R² 应远高于打乱对照。 | |
| 两个分支用**完全相同**的 λ 网格与折分,比较才公平。 | |
| """ | |
| n = len(A) | |
| Bn = _unit(B) | |
| idx = rng.permutation(n) | |
| n_tr = int(fold * n) | |
| tr, te = idx[:n_tr], idx[n_tr:] | |
| mu_b = Bn[tr].mean(axis=0) | |
| sd_b = Bn[tr].std(axis=0) + 1e-9 | |
| mu_a = A[tr].mean(axis=0) | |
| Ytr = A[tr] - mu_a | |
| Btr = (Bn[tr] - mu_b) / sd_b | |
| G = Btr.T @ Btr | |
| tr_scale = float(np.trace(G)) / G.shape[0] | |
| lams = [1e-4, 1e-3, 1e-2, 1e-1, 1.0, 10.0] | |
| def r2_of(Bx_rows): | |
| Bx = (Bx_rows - mu_b) / sd_b | |
| Btr_ = Bx[tr] | |
| Bte_ = Bx[te] | |
| G_ = Btr_.T @ Btr_ | |
| best = -np.inf | |
| for lam in lams: | |
| W = np.linalg.solve(G_ + lam * tr_scale * np.eye(G.shape[0]), | |
| Btr_.T @ Ytr) | |
| pred = Bte_ @ W + mu_a | |
| ss_res = float(((A[te] - pred) ** 2).sum()) | |
| ss_tot = float(((A[te] - mu_a) ** 2).sum()) | |
| r2 = 1.0 - ss_res / max(ss_tot, 1e-12) | |
| best = max(best, r2) | |
| return best | |
| r2_true = r2_of(Bn) | |
| nulls = [] | |
| for _ in range(n_perm): | |
| p = rng.permutation(n) | |
| nulls.append(r2_of(Bn[p])) | |
| z = (r2_true - float(np.mean(nulls))) / (float(np.std(nulls)) + 1e-12) | |
| return {"r2_true": float(r2_true), | |
| "null_mean": float(np.mean(nulls)), "null_std": float(np.std(nulls)), | |
| "z": float(z), "n_perm": n_perm, "fold": fold} | |
| def concept_group_test(F, rng, n_pairs=20000): | |
| """训练集的概念 = 连续 10 张一组。组内相似度应显著高于随机配对。 | |
| 返回 z 分数。**只用特征**,因此不需要 EEG。 | |
| """ | |
| Fn = _unit(F) | |
| n = len(Fn) | |
| n_grp = n // GROUP | |
| # 组内:每组取前 5 对(固定偏移,避免 O(n²)) | |
| within = [] | |
| for g in range(n_grp): | |
| base = g * GROUP | |
| for o in range(1, GROUP): | |
| within.append(float(Fn[base] @ Fn[base + o])) | |
| within = np.asarray(within) | |
| # 随机:跨组随机配对 | |
| gi = rng.integers(0, n_grp, n_pairs) | |
| gj = (gi + 1 + rng.integers(0, n_grp - 1, n_pairs)) % n_grp | |
| i = gi * GROUP | |
| j = gj * GROUP | |
| rand = np.einsum("ij,ij->i", Fn[i], Fn[j]) | |
| z = (within.mean() - rand.mean()) / (rand.std() + 1e-12) | |
| return {"within_mean": float(within.mean()), | |
| "random_mean": float(rand.mean()), | |
| "random_std": float(rand.std()), | |
| "z": float(z), | |
| "n_within": int(len(within))} | |
| def alignment_audit(FH, FB, rng): | |
| """两套特征的对齐审计(feature-only)。""" | |
| out = {} | |
| out["cross_predict_HB"] = cross_predict_test(FH, FB, rng) | |
| out["concept_group_H"] = concept_group_test(FH, rng) | |
| out["concept_group_B"] = concept_group_test(FB, rng) | |
| return out | |
| # --------------------------------------------------------------------------- | |
| # 解码对比 | |
| # --------------------------------------------------------------------------- | |
| def run_subject(sid, feats, rng): | |
| """feats = {model_name: (V_train, V_test)};跑完所有模型。""" | |
| t0 = time.time() | |
| tr_fp = TRAIN_4D / f"sub-{sid:02d}.npz" | |
| te_fp = TEST_4D / f"sub-{sid:02d}.npz" | |
| if not tr_fp.exists() or not te_fp.exists(): | |
| return None | |
| eeg = np.load(tr_fp)["eeg"] | |
| Xrep = E.per_rep_targets(eeg) | |
| del eeg | |
| mu, sd = E.zscore_fit(Xrep) | |
| Xrep = E.zscore_apply(Xrep, mu, sd) | |
| eeg = np.load(te_fp)["eeg"] | |
| Xte = E.per_rep_targets(eeg) | |
| del eeg | |
| Xte = E.zscore_apply(Xte, mu, sd) | |
| out = {"subject": f"sub-{sid:02d}", "models": {}} | |
| for name, (V, Vtest) in feats.items(): | |
| r = E.fit_and_decode(Xrep, Xte, V, Vtest, None, ALPHAS, rng, n_null=N_NULL) | |
| out["models"][name] = r | |
| log(f" sub-{sid:02d} [{name:10s}] α={r['alpha']:g} " | |
| f"单试次 acc1_cos={r['acc1_cos']:.4f} acc5_cos={r['acc5_cos']:.4f} " | |
| f"dot={r['acc1_dot']:.4f} | 80次平均={r['acc1_featavg_cos']:.4f} " | |
| f"| 零中位={np.median(r['null_acc1_cos']):.4f} [{time.time()-t0:.0f}s]") | |
| out["elapsed_s"] = round(time.time() - t0, 1) | |
| return out | |
| # --------------------------------------------------------------------------- | |
| # 配对比较 | |
| # --------------------------------------------------------------------------- | |
| def sign_test(diffs): | |
| """配对符号检验:H0 = 差值的中位为 0。 | |
| 用**标准精确二项检验**(与 R 的 `binom.test(alternative="two.sided")` 同义): | |
| 把所有"概率 ≤ 观测概率"的结局概率求和。 | |
| (不要用"单尾 × 2"的加倍法:非对称情形下它与标准定义不同。) | |
| """ | |
| from math import comb | |
| d = np.asarray(diffs) | |
| d = d[d != 0] | |
| n = len(d) | |
| if n == 0: | |
| return {"n": 0, "n_pos": 0, "n_neg": 0, "p_exact_two_sided": 1.0} | |
| k = int((d > 0).sum()) | |
| tot = 2 ** n | |
| p_obs = comb(n, k) / tot | |
| p = sum(comb(n, i) / tot for i in range(n + 1) | |
| if comb(n, i) / tot <= p_obs + 1e-15) | |
| return {"n": n, "n_pos": k, "n_neg": n - k, | |
| "p_exact_two_sided": float(min(1.0, p))} | |
| # --------------------------------------------------------------------------- | |
| # 自测 | |
| # --------------------------------------------------------------------------- | |
| def self_test(): | |
| log("=== 自测 ===") | |
| rng = np.random.default_rng(11) | |
| n, d = 400, 64 | |
| # (1) 交叉预测检验(维度可以不同):对齐 vs 打乱 | |
| base = rng.normal(size=(n, d)) | |
| other = base[:, :d // 2] + 0.35 * rng.normal(size=(n, d // 2)) # 维度不同! | |
| c_ok = cross_predict_test(base, other, rng)["z"] | |
| shuffled = other[rng.permutation(n)] | |
| c_bad = cross_predict_test(base, shuffled, rng)["z"] | |
| log(f" 交叉预测检验:对齐 z={c_ok:.1f} | 打乱 z={c_bad:.1f}") | |
| assert c_ok > 10 and abs(c_bad) < 5, "交叉预测检验没区分开" | |
| # (2) 概念组检验:组内相似 vs 随机 | |
| proto = rng.normal(size=(n // GROUP, d)) | |
| F_grp = (proto[:, None, :] + 0.15 * rng.normal(size=(n // GROUP, GROUP, d))).reshape(n, d) | |
| F_rnd = rng.normal(size=(n, d)) | |
| g_ok = concept_group_test(F_grp, rng)["z"] | |
| g_bad = concept_group_test(F_rnd, rng)["z"] | |
| log(f" 概念组检验:成组 z={g_ok:.1f} | 随机 z={g_bad:.1f}") | |
| assert g_ok > 5 and abs(g_bad) < 5, "概念组检验没区分开" | |
| # (3) 配对符号检验(n=10,与真实实验的被试数一致) | |
| s1 = sign_test([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) # 全正 ⇒ p=2/1024 | |
| s0 = sign_test([1, -1, 1, -1, 1, -1, 1, -1, 1, -1]) # 交替 ⇒ p=1.0 | |
| s8 = sign_test([1] * 8 + [-1, -1]) # 8 正 2 负 | |
| log(f" 符号检验:全正 p={s1['p_exact_two_sided']:.4f} | 交替 p={s0['p_exact_two_sided']:.4f} " | |
| f"| 8正2负 p={s8['p_exact_two_sided']:.4f}") | |
| assert s1["p_exact_two_sided"] < 0.01, "全正时 p 应为 0.00195" | |
| assert s0["p_exact_two_sided"] > 0.9, "交替时应为 1.0" | |
| assert 0.05 < s8["p_exact_two_sided"] < 0.2, "8正2负的标准精确 p 应约 0.109" | |
| # (4) 路径字典 loader:行序由路径推导 + 结构校验必须能抓错 | |
| # 注意 train 用 `train_images/`、test 用 `test_images/` 前缀,正则都要认 | |
| import torch | |
| d = {} | |
| for c in range(1, 4): | |
| for i in range(1, 11): | |
| d[f"train_images/{c:05d}_cc{c}/cc{c}_{i:02d}s.jpg"] = \ | |
| torch.full((4,), float(c), dtype=torch.float32) | |
| arr, meta = path_dict_to_matrix(d, 30, 10) | |
| assert meta["first_key"] == "train_images/00001_cc1/cc1_01s.jpg", meta | |
| assert meta["last_key"] == "train_images/00003_cc3/cc3_10s.jpg", meta | |
| assert meta["per_concept"] == 10, meta | |
| # 另一前缀也要认 | |
| d_test = {k.replace("train_images", "test_images"): v for k, v in d.items()} | |
| _a2, m2 = path_dict_to_matrix(d_test, 30, 10) | |
| assert m2["first_key"].startswith("test_images/"), m2 | |
| # 行序必须按 (概念号, 文件名) —— 第 1 组的首行是 01s,第 2 组首行紧随其后 | |
| assert abs(arr[0, 0] - 1.0) < 1e-9 and abs(arr[10, 0] - 2.0) < 1e-9 | |
| # 自行推断每概念图数 | |
| _a3, m3 = path_dict_to_matrix(d, 30, None) | |
| assert m3["per_concept"] == 10, m3 | |
| d2 = dict(d) | |
| d2.pop("train_images/00002_cc2/cc2_05s.jpg") | |
| try: | |
| path_dict_to_matrix(d2, 30, 10) | |
| raise AssertionError("概念号不连续却没报错") | |
| except ValueError: | |
| pass | |
| try: | |
| path_dict_to_matrix(d, 30, 9) | |
| raise AssertionError("每概念图数不对却没报错") | |
| except ValueError: | |
| pass | |
| log(f" 路径字典 loader:行序推导 {meta['first_key']} … {meta['last_key']},结构校验 OK") | |
| log("=== 自测通过 ===") | |
| # --------------------------------------------------------------------------- | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--only-subjects", type=str, default="") | |
| ap.add_argument("--self-test", action="store_true") | |
| args = ap.parse_args() | |
| if args.self_test: | |
| self_test() | |
| return | |
| t0 = time.time() | |
| OUT_DIR.mkdir(parents=True, exist_ok=True) | |
| rng = np.random.default_rng(SEED) | |
| # ---- 载入两套特征 ---- | |
| feats = {} | |
| metas = {} | |
| VH_tr, m1 = load_matrix(FEATURE_SETS["ViT-H-14"][0], 16540) | |
| VH_te, m2 = load_matrix(FEATURE_SETS["ViT-H-14"][1], 200) | |
| feats["ViT-H-14"] = (VH_tr, VH_te) | |
| metas["ViT-H-14"] = {"train": m1, "test": m2} | |
| log(f"特征 ViT-H-14: train {VH_tr.shape} / test {VH_te.shape}(矩阵式)") | |
| VB_tr, m3 = load_path_dict(FEATURE_SETS["ViT-B-32"][0], 16540, None) | |
| VB_te, m4 = load_path_dict(FEATURE_SETS["ViT-B-32"][1], 200, None) | |
| feats["ViT-B-32"] = (VB_tr, VB_te) | |
| metas["ViT-B-32"] = {"train": m3, "test": m4} | |
| log(f"特征 ViT-B-32: train {VB_tr.shape} / test {VB_te.shape}(路径字典式," | |
| f"行序由图片路径排序推导;每概念图数:train={m3['per_concept']} test={m4['per_concept']};" | |
| f"首={m3['first_key']} 末={m3['last_key']})") | |
| # ---- 对齐审计(只用特征)---- | |
| log("\n--- 对齐审计(feature-only,不碰 EEG)---") | |
| audit = alignment_audit(feats["ViT-H-14"][0], feats["ViT-B-32"][0], rng) | |
| audit["load_meta"] = metas | |
| log(f" 交叉预测检验 H←B: R²={audit['cross_predict_HB']['r2_true']:.4f} " | |
| f"打乱对照={audit['cross_predict_HB']['null_mean']:.4f} " | |
| f"z={audit['cross_predict_HB']['z']:.1f}") | |
| log(f" 概念组检验 ViT-H-14: 组内={audit['concept_group_H']['within_mean']:.4f} " | |
| f"随机={audit['concept_group_H']['random_mean']:.4f} z={audit['concept_group_H']['z']:.1f}") | |
| log(f" 概念组检验 ViT-B-32: 组内={audit['concept_group_B']['within_mean']:.4f} " | |
| f"随机={audit['concept_group_B']['random_mean']:.4f} z={audit['concept_group_B']['z']:.1f}") | |
| # ---- 解码对比 ---- | |
| sids = ([int(s) for s in args.only_subjects.split(",") if s.strip()] | |
| if args.only_subjects else list(range(1, 11))) | |
| rows = [] | |
| for sid in sids: | |
| rec = run_subject(sid, feats, rng) | |
| if rec is None: | |
| log(f" sub-{sid:02d}: 数据缺失,跳过") | |
| continue | |
| rows.append(rec) | |
| (OUT_DIR / "exp74_model_compare.json").write_text( | |
| json.dumps({"per_subject": rows}, indent=2, ensure_ascii=False), | |
| encoding="utf-8") | |
| # ---- 汇总 + 配对比较 ---- | |
| log("\n" + "=" * 84) | |
| log("汇总:ViT-H-14 vs ViT-B-32(200 选 1,随机 0.5%)") | |
| log("=" * 84) | |
| summary = {"alignment_audit": audit, "models": {}} | |
| for name in FEATURE_SETS: | |
| a = [r["models"][name]["acc1_cos"] for r in rows] | |
| n = [float(np.median(r["models"][name]["null_acc1_cos"])) for r in rows] | |
| a5 = [r["models"][name]["acc5_cos"] for r in rows] | |
| fa = [r["models"][name]["acc1_featavg_cos"] for r in rows] | |
| summary["models"][name] = { | |
| "n_subjects": len(a), | |
| "acc1_cos_median": float(np.median(a)), | |
| "acc1_cos_min": float(np.min(a)), "acc1_cos_max": float(np.max(a)), | |
| "ratio_vs_random": float(np.median(a) / 0.005), | |
| "null_median": float(np.median(n)), | |
| "acc5_cos_median": float(np.median(a5)), | |
| "featavg_median": float(np.median(fa)), | |
| "per_subject_acc1_cos": {r["subject"]: r["models"][name]["acc1_cos"] | |
| for r in rows}, | |
| } | |
| s = summary["models"][name] | |
| log(f" {name:10s}: 单试次 top1 中位 {s['acc1_cos_median']:.4f} " | |
| f"({s['ratio_vs_random']:.1f}× 随机) 范围 {s['acc1_cos_min']:.4f}–{s['acc1_cos_max']:.4f} " | |
| f"| 零中位 {s['null_median']:.4f} | top5 {s['acc5_cos_median']:.4f} " | |
| f"| 80次平均 {s['featavg_median']:.4f}") | |
| if len(rows) >= 2: | |
| A = "ViT-H-14" | |
| B = "ViT-B-32" | |
| diffs = [r["models"][A]["acc1_cos"] - r["models"][B]["acc1_cos"] for r in rows] | |
| st = sign_test(diffs) | |
| summary["paired"] = { | |
| "comparison": f"{A} − {B}(单试次 acc1_cos,被试内配对)", | |
| "median_diff": float(np.median(diffs)), | |
| "mean_diff": float(np.mean(diffs)), | |
| "per_subject_diff": {r["subject"]: d for r, d in zip(rows, diffs)}, | |
| "sign_test": st, | |
| } | |
| log(f" 配对差 ({A} − {B}): 中位 {np.median(diffs):+.4f} " | |
| f"均值 {np.mean(diffs):+.4f} | 正 {st['n_pos']} 负 {st['n_neg']} " | |
| f"精确双侧 p = {st['p_exact_two_sided']:.4f}") | |
| (OUT_DIR / "exp74_model_compare_full.json").write_text( | |
| json.dumps({"schema_version": "1.0", "script": "exp74_model_compare", | |
| "summary": summary, "per_subject": rows, | |
| "elapsed_s": round(time.time() - t0, 1)}, | |
| indent=2, ensure_ascii=False), encoding="utf-8") | |
| print("=== JSON BEGIN ===") | |
| print(json.dumps(summary, indent=2, ensure_ascii=False)) | |
| print("=== JSON END ===") | |
| log(f"\n耗时 {time.time()-t0:.0f}s") | |
| if __name__ == "__main__": | |
| main() | |
Xet Storage Details
- Size:
- 23.4 kB
- Xet hash:
- b1a69c38705fef86e34bf182c8c27269fad6d8af50edfa8b615b618d82c350df
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.