headless / tests /benchmark_b_matrix.py
logos42's picture
Upload folder using huggingface_hub
216004d verified
Raw History Blame Contribute Delete
5.73 kB
#!/usr/bin/env python3
"""benchmark_b_matrix.py — 用户框架里的 **B 矩阵** (在 KeyDoorMDP 上)。
## 测什么
B2/B3 A→B 适应速度 T_adapt
B4/B5 A→B→C 反复适应
B6/B7 A→B→A **知识复用** T_A^(2) < T_A^(1)?
B8/B9 多 regime sample efficiency R(N)
对照: `primitive` (纯 Q-learning) vs `rediscover` (Q + 跨 regime 保留的技能库)。
## 为什么这个环境能回答, lm4 不能
lm4 : A ≡ 0 (动作完全可交换) -> option 无空间, 测不出
KeyDoor : A ∈ [-1,+1], 33% 显著 -> **目标导向的多步行为真的有价值**
## 判据 (用户给的)
若 option 在某项上优于 primitive -> 那就是 option 体现价值的地方
若各项都不优于 (且 CI 不含 0) -> 才有资格讨论"option 不适用"
"""
import argparse
import json
import sys
from pathlib import Path
import numpy as np
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from hibs_lnn.skill_agent import DEFAULT_REGIMES, run_regime_chain # noqa: E402
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--episodes-per", type=int, default=300)
ap.add_argument("--seeds", type=int, default=5)
ap.add_argument("--modes", default="primitive,rediscover")
ap.add_argument("--cycles", type=int, default=2,
help="regime 链重复几轮 (2 = A B C A B C)")
ap.add_argument("--n-pos", type=int, default=8)
ap.add_argument("--out", default=str(ROOT / "results" / "b_matrix.json"))
a = ap.parse_args()
chain = DEFAULT_REGIMES * a.cycles
modes = [m.strip() for m in a.modes.split(",") if m.strip()]
print("regime 链: %s" % [f"{r['key_pos']}/{r['door_pos']}/{r['goal_pos']}" for r in chain])
print("模式: %s seed x %d episodes/regime=%d"
% (modes, a.seeds, a.episodes_per))
print()
all_rows = {}
for mode in modes:
per_seed = []
for si in range(a.seeds):
rows, agent = run_regime_chain(mode=mode, chain=chain, n_pos=a.n_pos,
episodes_per=a.episodes_per,
seed=42 + si)
# ★ option 台账: visits/success 是否真的被回填?
# 修复前恒 0 -> select() 里 rate=success/visits 永远取默认 0.5
# -> 技能库从不学习哪些 option 管用 (选择等于抛硬币)。
bk = agent.option_bookkeeping() if hasattr(agent, "option_bookkeeping") \
else {"total_visits": 0, "total_success": 0, "n_used": 0}
for r in rows:
r["bookkeeping"] = bk
per_seed.append(rows)
print(" [%s seed=%d] T_adapt=%s 最终成功率=%s n_options=%d 台账=%d/%d"
% (mode, 42 + si, [r["t_adapt"] for r in rows],
[round(r["final_succ"], 2) for r in rows],
rows[-1]["n_options"], bk["total_success"], bk["total_visits"]),
flush=True)
all_rows[mode] = per_seed
print()
print("=" * 96)
print("★ 知识复用: 同一 regime 在第 2 轮 (重复出现) 是否恢复更快?")
print("=" * 96)
n_seg = len(chain)
half = n_seg // a.cycles if a.cycles > 1 else n_seg
print("%-14s %-28s %-28s %s" % ("mode", "第1轮 T_adapt (均值)", "第2轮 T_adapt (均值)", "Δ / 加速比"))
summary = {}
for mode in modes:
arr = np.array([[r["t_adapt"] for r in rows] for rows in all_rows[mode]], float)
first = arr[:, :half].mean(axis=1)
second = arr[:, half:half * 2].mean(axis=1) if a.cycles > 1 else first
d = second - first
try:
from scipy import stats
t, p = stats.ttest_rel(first, second)
except Exception:
t, p = float("nan"), float("nan")
speed = float(first.mean() / max(second.mean(), 1e-9))
summary[mode] = {"first": float(first.mean()), "second": float(second.mean()),
"delta": float(d.mean()), "speedup": speed,
"p": float(p) if np.isfinite(p) else None}
print("%-14s %-28.2f %-28.2f %+.1f (%.2fx) p=%.3f"
% (mode, first.mean(), second.mean(), d.mean(), speed,
p if np.isfinite(p) else float("nan")))
print()
print(" 判据: Δ < 0 且 p < 0.05 -> **复用成立** (第 2 轮确实更快)")
print()
print("=" * 96)
print("逐段 T_adapt (跨 seed 均值 ± std)")
print("=" * 96)
for mode in modes:
arr = np.array([[r["t_adapt"] for r in rows] for rows in all_rows[mode]], float)
print(" %-14s %s" % (mode, " ".join("%.0f±%.0f" % (m, s)
for m, s in zip(arr.mean(0), arr.std(0)))))
print()
if len(modes) >= 2 and "primitive" in modes:
prim = np.array([[r["t_adapt"] for r in rows] for rows in all_rows["primitive"]], float)
for mode in modes:
if mode == "primitive":
continue
arr = np.array([[r["t_adapt"] for r in rows] for rows in all_rows[mode]], float)
d = prim.mean(0) - arr.mean(0) # 正 = option 更快
print(" %-14s 相对 primitive 的加速 (逐段, 正=更快): %s"
% (mode, " ".join("%+.0f" % x for x in d)))
Path(a.out).parent.mkdir(parents=True, exist_ok=True)
Path(a.out).write_text(json.dumps(
{"modes": modes, "seeds": a.seeds, "episodes_per": a.episodes_per,
"chain": chain, "summary": summary, "rows": all_rows},
indent=1, ensure_ascii=False))
print("\n已写 %s" % a.out)
if __name__ == "__main__":
main()