#!/usr/bin/env python3 """analyze_oak.py — OaK E9 vs E10 对照分析。 核心问题 (用户架构主张里最关键的一跳): Prediction -> Abstraction -> Option -> Planning -> Control 是否比 state -> RL policy -> action 更适合持续变化的磁通动力学。 E9 = --proposer oak --oak-options 0 (同一 base、同一门控, 不用 option) E10 = --proposer oak --oak-options 1 (+Options / 时间抽象) → **唯一变量是时间抽象**。 判据 (不止看均值, 也看用户要求的多项): any_time_acc 在线性能 worst_case_forget 最差遗忘界 (CL 的一等指标) final_mean_acc 末轮平均 option_starts/steps option 是否**真的被用上** (用不上 = 机制没生效, 不等于无效) n_options / actions 发现了什么技能 统计: Welch t + 效应量; 多 seed 可靠性标准 std < mean/2。 """ import glob import json import math import os import sys import numpy as np RES = sys.argv[1] if len(sys.argv) > 1 else "results" def welch(a, b): a, b = np.asarray(a, float), np.asarray(b, float) a = a[np.isfinite(a)]; b = b[np.isfinite(b)] if len(a) < 2 or len(b) < 2: return float("nan"), float("nan") va, vb = a.var(ddof=1), b.var(ddof=1) se = math.sqrt(va / len(a) + vb / len(b)) if se == 0: return (0.0 if a.mean() == b.mean() else float("inf")), float("nan") t = (a.mean() - b.mean()) / se # Welch–Satterthwaite 自由度 df = (va / len(a) + vb / len(b)) ** 2 / ( (va / len(a)) ** 2 / (len(a) - 1) + (vb / len(b)) ** 2 / (len(b) - 1)) try: from scipy import stats p = 2 * stats.t.sf(abs(t), df) except Exception: # 无 scipy: 正态近似 (n>=5 时够用, 标注清楚) p = math.erfc(abs(t) / math.sqrt(2)) return t, p def collect(pat, arm): """收集所有匹配目录的 `arm` 结果。arm in (naive, replay)。""" out = {"any_time": [], "forget": [], "final": [], "opts": [], "starts": [], "steps": [], "ntrans": [], "nopt": [], "dirs": []} for d in sorted(glob.glob(pat)): f = os.path.join(d, "lm4_wave_results.json") if not os.path.exists(f): continue try: j = json.load(open(f)) except Exception: continue if arm not in j: continue a = j[arm] out["dirs"].append(os.path.basename(d)) out["any_time"].append(a.get("any_time_acc", float("nan"))) out["forget"].append(a.get("worst_case_forget", float("nan"))) out["final"].append(a.get("final_mean_acc", float("nan"))) o = a.get("oak") or {} k = a.get("knowledge") or {} out["opts"].append(a.get("oak_transitions", o.get("n_trans", float("nan")))) out["starts"].append(o.get("option_starts", float("nan"))) out["steps"].append(o.get("option_steps", float("nan"))) out["ntrans"].append(o.get("n_trans", float("nan"))) out["nopt"].append(k.get("n_options", float("nan"))) return out def collect5(pat): """lm5 的指标在**顶层** (any_time_acc / worst_case_forget), 不像 lm4 嵌在 arm 下。 ★ 早先版本只写了一个 collect() 读 lm4 的 `lm4_wave_results.json`, lm5 的 20 个臂 (oak5L_*) 因此**完全没被读到**, 分析报告里那一节是空的。 """ out = {"any_time": [], "forget": [], "final": [], "opts": [], "starts": [], "steps": [], "ntrans": [], "nopt": [], "dirs": []} for d in sorted(glob.glob(pat)): f = os.path.join(d, "lm5_mm_results.json") if not os.path.exists(f): continue try: j = json.load(open(f)) except Exception: continue out["dirs"].append(os.path.basename(d)) out["any_time"].append(j.get("any_time_acc", float("nan"))) out["forget"].append(j.get("worst_case_forget", float("nan"))) # lm5 没有单一 final_mean_acc -> 用最后一行的域均值 m = j.get("matrix") or [] if m: last = [v for v in m[-1] if v is not None and v == v] out["final"].append(float(np.mean(last)) if last else float("nan")) else: out["final"].append(float("nan")) o = j.get("oak") or {} k = j.get("knowledge") or {} out["starts"].append(o.get("option_starts", float("nan"))) out["steps"].append(o.get("option_steps", float("nan"))) out["ntrans"].append(o.get("n_trans", float("nan"))) out["nopt"].append(k.get("n_options", float("nan"))) return out def fmt(v): v = np.asarray(v, float); v = v[np.isfinite(v)] if len(v) == 0: return "n/a" if len(v) == 1: return "%.4f (n=1)" % v[0] return "%.4f ± %.4f (n=%d, CV %.1f%%)" % ( v.mean(), v.std(ddof=1), len(v), 100 * v.std(ddof=1) / abs(v.mean()) if v.mean() else float("nan")) print("=" * 96) print("OaK E9 (无 Options) vs E10 (有 Options) —— 唯一变量是时间抽象") print("=" * 96) for arm in ("replay", "naive"): e9 = collect(os.path.join(RES, "oak_e9_s*"), arm) e10 = collect(os.path.join(RES, "oak_e10_s*"), arm) if not e9["dirs"] and not e10["dirs"]: continue print() print("── arm = %s (%s) " % (arm, "有意义的那臂" if arm == "replay" else "对照") + "─" * 50) print(" E9 最终平均 acc : %s" % fmt(e9["final"])) print(" E10 最终平均 acc : %s" % fmt(e10["final"])) print(" E9 any-time : %s" % fmt(e9["any_time"])) print(" E10 any-time : %s" % fmt(e10["any_time"])) print(" E9 最差遗忘界 : %s" % fmt(e9["forget"])) print(" E10 最差遗忘界 : %s" % fmt(e10["forget"])) print(" ── 机制是否真的生效 (E10) ──") print(" 发现的 options : %s" % fmt(e10["nopt"])) print(" option 启动次数 : %s" % fmt(e10["starts"])) print(" option 执行步数 : %s" % fmt(e10["steps"])) print(" 转移条数 : %s" % fmt(e10["ntrans"])) if np.nanmean(e10["steps"]) < 1 if len(e10["steps"]) else True: print(" ** 警告: option 几乎没被执行 -> 这不是「Options 无效」,") print(" 而是「Options 机制没被触发」, 结论不能当负面证据 **") print(" ── 显著性 ──") for name, key, better in [("any_time ", "any_time", "high"), ("最差遗忘界", "forget", "low"), ("最终 acc ", "final", "high")]: t, p = welch(e10[key], e9[key]) if not np.isfinite(t): print(" %s 样本不足" % name) continue d = np.nanmean(e10[key]) - np.nanmean(e9[key]) rel = 100 * d / abs(np.nanmean(e9[key])) if np.nanmean(e9[key]) else float("nan") verdict = ("E10 更好" if ((d > 0) == (better == "high")) else "E9 更好") print(" %s Δ=%+.4f (%+.1f%%) t=%+.2f p=%.4f -> %s %s" % (name, d, rel, t, p, verdict, "**显著**" if p < 0.05 else "不显著")) # ── E11 / E12: regime shift ──────────────────────────────────────────── print() print("=" * 96) print("regime shift (E11 单次 / E12 反复) —— T(s,a) 本身改变后的适应") print("=" * 96) for arm in ("replay",): for tag, lbl in [("e11", "E11 单次漂移 @40"), ("e12", "E12 反复漂移 @30 每25")]: opt = collect(os.path.join(RES, "oak_%sopt_s*" % tag), arm) no = collect(os.path.join(RES, "oak_%snoopt_s*" % tag), arm) if not opt["dirs"] and not no["dirs"]: continue print() print("── %s (arm=%s) " % (lbl, arm) + "─" * 46) print(" 有 Options : 最终 %s" % fmt(opt["final"])) print(" 遗忘 %s" % fmt(opt["forget"])) print(" 无 Options : 最终 %s" % fmt(no["final"])) print(" 遗忘 %s" % fmt(no["forget"])) t, pv = welch(opt["forget"], no["forget"]) if np.isfinite(t): d = np.nanmean(opt["forget"]) - np.nanmean(no["forget"]) print(" 遗忘 Δ=%+.4f t=%+.2f p=%.4f %s" % (d, t, pv, "**显著**" if pv < 0.05 else "不显著")) print(" 机制: options=%s option_starts=%s" % (fmt(opt["nopt"]), fmt(opt["starts"]))) # ── 步长 E 表 (E1~E5) ────────────────────────────────────────────────── print() print("=" * 96) print("步长 E 表 (E1 固定步长 / E2 IDBD / E3 idbd-raw / E4 Autostep / E5 Continual-IDBD)") print("=" * 96) _rows = [] fx = collect(os.path.join(RES, "oak_e1_fixedalpha_s*"), "replay") if fx["dirs"]: _rows.append(("E1 fixed-alpha (mu=0)", fx)) for algo, lbl in [("idbd", "E2 IDBD"), ("idbd-raw", "E3 idbd-raw"), ("autostep", "E4 Autostep"), ("cidbd", "E5 Continual-IDBD")]: d = collect(os.path.join(RES, "oak_algo_%s_s*" % algo), "replay") if d["dirs"]: _rows.append((lbl, d)) if _rows: print(" %-24s %-30s %-30s %s" % ("臂", "any_time", "最差遗忘界", "最终 acc")) for lbl, d in _rows: print(" %-24s %-30s %-30s %s" % (lbl, fmt(d["any_time"]), fmt(d["forget"]), fmt(d["final"]))) base = _rows[0][1] print(" ── 相对 E1(固定步长)的差 ──") for lbl, d in _rows[1:]: ta, pa = welch(d["any_time"], base["any_time"]) tf, pf = welch(d["forget"], base["forget"]) print(" %-22s any_time Δ=%+.4f (p=%.3f) 遗忘 Δ=%+.4f (p=%.3f)" % (lbl, np.nanmean(d["any_time"]) - np.nanmean(base["any_time"]), pa, np.nanmean(d["forget"]) - np.nanmean(base["forget"]), pf)) # ── lm5 两档配置 ─────────────────────────────────────────────────────── print() print("=" * 96) print("lm5 (多模态) E9 vs E10 —— 两档配置") print("=" * 96) for lbl, pa, pb in [("S4 短配置 (120 轮, 5 seed)", "oak5_e9_s*", "oak5_e10_s*"), ("S5 长配置 (200 轮, 10 seed)", "oak5L_e9_s*", "oak5L_e10_s*")]: _a, _b = collect5(os.path.join(RES, pa)), collect5(os.path.join(RES, pb)) if not _a["dirs"] and not _b["dirs"]: continue print() print("── %s " % lbl + "─" * 44) print(" E9 无 Options: any_time %s" % fmt(_a["any_time"])) print(" 遗忘 %s" % fmt(_a["forget"])) print(" E10 有 Options: any_time %s" % fmt(_b["any_time"])) print(" 遗忘 %s" % fmt(_b["forget"])) print(" 机制(E10): options=%s starts=%s steps=%s" % (fmt(_b["nopt"]), fmt(_b["starts"]), fmt(_b["steps"]))) t, pv = welch(_b["any_time"], _a["any_time"]) if np.isfinite(t): print(" any_time Δ=%+.4f t=%+.2f p=%.4f %s" % (np.nanmean(_b["any_time"]) - np.nanmean(_a["any_time"]), t, pv, "**显著**" if pv < 0.05 else "不显著")) print() print("=" * 96) print("lm5 加长配置细节 (text 400 / wave 400 / causal 40k / rounds 200)") print("=" * 96) e9_5 = collect5(os.path.join(RES, "oak5L_e9_s*")) e10_5 = collect5(os.path.join(RES, "oak5L_e10_s*")) for lbl, d in [("lm5L E9 (无 Options)", e9_5), ("lm5L E10 (有 Options)", e10_5)]: if not d["dirs"]: continue print(" %-24s any_time %s" % (lbl, fmt(d["any_time"]))) print(" %-24s 遗忘 %s" % ("", fmt(d["forget"]))) print(" %-24s 机制 options=%s starts=%s" % ("", fmt(d["nopt"]), fmt(d["starts"]))) if e9_5["dirs"] and e10_5["dirs"]: t, pv = welch(e10_5["any_time"], e9_5["any_time"]) if np.isfinite(t): print(" any_time Δ=%+.4f t=%+.2f p=%.4f %s" % (np.nanmean(e10_5["any_time"]) - np.nanmean(e9_5["any_time"]), t, pv, "**显著**" if pv < 0.05 else "不显著")) print() print("=" * 96) print("判读口径 (防止过度解读)") print("=" * 96) print(""" 1. 如果 option_steps ≈ 0 -> **机制没被触发**, 不能得出「Options 无效」的结论。 必须先修触发条件 (option 起点匹配半径 / τ_U 过滤强度) 再重跑。 2. 如果 E10 与 E9 的差异 p > 0.05 -> 如实写成负面结论, 不用「方向一致」搪塞。 3. 多 seed 可靠性: std > mean/2 的指标不可用于结论 (用户定的标准)。 4. 本实验是**被动观测 + 离线/模型内反事实**, 没有真实 actuator。 口径必须是 "predictive/control-policy learning on passive observations", 不能写成 "完成了磁通控制实验"。 """)