headless / tests /analyze_benchmark.py
logos42's picture
Upload folder using huggingface_hub
216004d verified
Raw History Blame Contribute Delete
9.79 kB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""LM4 修正版 benchmark 汇总: 任务流 + 调度臂, 报边缘设备关心的指标。
路径前缀同时覆盖两批 (2026-09-14):
- `bm_bins_*` 任务流臂 (不受价值函数 bug 影响)
- `bm2_*` 修正版价值函数臂 (con 实现 / sim 自适应 / 池 60)
指标:
replay 最终平均准确率
any_time 在线全程平均 (边学边可用)
worst_forget 最差遗忘界 (任一旧任务的最大跌幅; 安全功能不能崩)
mean_forget 平均遗忘
统计口径:
mean±std 用样本标准差 (ddof=1); 两两对比用 Welch t + Welch–Satterthwaite 自由度,
双侧 p 由 t 分布精确算 (无 scipy 依赖)。判定阈值 |t| >= 2 —— n=3 时只算边缘证据。
已知数据缺口 (见 docs/bm_benchmark_results.md §0):
lm4 `bm2_random-matched_s*` 的 json 被 int64 序列化 bug 截断 → 回退读 `lm4_wave_report.md`
(只有 replay / 平均遗忘), any-time 与最差遗忘界缺失。
"""
import glob
import json
import math
import os
import numpy as np
R = "/work/liuyuanjie/headless/results"
SEEDS = ["42", "1", "7"]
# ---------------- 统计 ----------------
def _betacf(a, b, x):
MAXIT, EPS, FPMIN = 200, 3e-16, 1e-300
qab, qap, qam = a + b, a + 1.0, a - 1.0
c, d = 1.0, 1.0 - qab * x / qap
if abs(d) < FPMIN:
d = FPMIN
d = 1.0 / d
h = d
for m in range(1, MAXIT + 1):
m2 = 2 * m
aa = m * (b - m) * x / ((qam + m2) * (a + m2))
d = 1.0 + aa * d
if abs(d) < FPMIN:
d = FPMIN
c = 1.0 + aa / c
if abs(c) < FPMIN:
c = FPMIN
d = 1.0 / d
h *= d * c
aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2))
d = 1.0 + aa * d
if abs(d) < FPMIN:
d = FPMIN
c = 1.0 + aa / c
if abs(c) < FPMIN:
c = FPMIN
d = 1.0 / d
de = d * c
h *= de
if abs(de - 1.0) < EPS:
break
return h
def _betai(a, b, x):
if x <= 0:
return 0.0
if x >= 1:
return 1.0
lb = math.lgamma(a + b) - math.lgamma(a) - math.lgamma(b)
bt = math.exp(lb + a * math.log(x) + b * math.log(1 - x))
if x < (a + 1) / (a + b + 2):
return bt * _betacf(a, b, x) / a
return 1.0 - bt * _betacf(b, a, 1 - x) / b
def welch(a, b):
"""Welch t 检验; 返回 Δ / SE / t / df / p(双侧), 样本 < 2 则返回 None。"""
a = [x for x in a if x is not None and x == x]
b = [x for x in b if x is not None and x == x]
if len(a) < 2 or len(b) < 2:
return None
ma, mb = float(np.mean(a)), float(np.mean(b))
va, vb = float(np.var(a, ddof=1)), float(np.var(b, ddof=1))
na, nb = len(a), len(b)
se = math.sqrt(va / na + vb / nb)
if se == 0:
return dict(delta=ma - mb, se=0.0, t=float("nan"), df=float("nan"), p=float("nan"))
t = (ma - mb) / se
df = (va / na + vb / nb) ** 2 / ((va / na) ** 2 / (na - 1) + (vb / nb) ** 2 / (nb - 1))
return dict(delta=ma - mb, se=se, t=t, df=df, p=_betai(df / 2.0, 0.5, df / (df + t * t)))
# ---------------- 装载 ----------------
def _clean(x):
if x is None:
return None
try:
x = float(x)
except Exception:
return None
return None if x != x else x
def load(pat):
"""pat 形如 'bm2_value_s{s}'; 返回 {seed: {...}}。json 坏了则回退 report.md。"""
out = {}
for s in SEEDS:
d = os.path.join(R, pat.format(s=s))
if not os.path.isdir(d):
continue
rec = None
p = os.path.join(d, "lm4_wave_results.json")
if os.path.exists(p):
try:
r = (json.load(open(p)) or {}).get("replay") or {}
rec = {
"replay": _clean(r.get("final_mean_acc")),
"any_time": _clean(r.get("any_time_acc")),
"worst": _clean(r.get("worst_case_forget")),
"mean_f": _clean(r.get("mean_forget_all")),
"src": "json",
}
except Exception:
rec = None
if rec is None:
rp = os.path.join(d, "lm4_wave_report.md")
if os.path.exists(rp):
for line in open(rp):
c = [x.strip() for x in line.strip().strip("|").split("|")]
if len(c) == 3 and c[0] == "replay":
try:
rec = {"replay": float(c[1]), "mean_f": float(c[2]),
"any_time": None, "worst": None, "src": "report.md"}
except Exception:
pass
if rec:
out[s] = rec
return out
ARMS = [
("启发式 fixed 域序", "bm_bins_fixed_s{s}"),
("流: perm (随机排列)", "bm_bins_perm_s{s}"),
("流: revisit (反复)", "bm_bins_revisit_s{s}"),
("流: nonstationary", "bm_bins_nonstationary_s{s}"),
("★ 价值函数 value", "bm2_value_s{s}"),
(" 消融 value-nofb", "bm2_value-nofb_s{s}"),
(" 均匀随机 random", "bm2_random_s{s}"),
("★ 频率对齐 random-matched", "bm2_random-matched_s{s}"),
]
# lm4 `random-matched` 原 driver 的 json 全部被 int64 bug 截断, 且 seed 1/7 的结果文件
# 事后被另一路会话用**改过的脚本**重跑覆盖过。主口径取"与 value 臂同修订版"的原日志值
# (seed 42 已用 不带 default= 的反向修复独立复现, 逐位相同), 其余作敏感性对照。
RM_MATCHED_ORIG = {"42": (0.7635, 0.1178), "1": (0.7542, 0.1982), "7": (0.7594, 0.1116)}
USE_ORIG_RM = os.environ.get("BM_RM_SENSITIVITY", "0") != "1"
if USE_ORIG_RM:
_old = load("bm2_random-matched_s{s}")
_patched = {}
for s, (acc, mf) in RM_MATCHED_ORIG.items():
# any_time / worst 原 json 被截断, 且目录值已被新修订版覆盖 -> 主口径留空,
# 不拿"混合修订版"的数字充数 (想看被覆盖后的值: BM_RM_SENSITIVITY=1)
rec = dict(_old.get(s) or {})
rec.update({"replay": acc, "mean_f": mf, "any_time": None, "worst": None,
"src": "bm2.log (同修订版)"})
_patched[s] = rec
_load = load
def load(pat, _p=_patched, _l=_load): # noqa: F811
if pat == "bm2_random-matched_s{s}":
return _p
return _l(pat)
def fmt(vals):
v = [x for x in vals if x is not None]
if not v:
return "-"
if len(v) == 1:
return "%.4f" % v[0]
return "%.4f±%.4f" % (np.mean(v), np.std(v, ddof=1))
print("=" * 104)
print("LM4 修正版 benchmark · 任务流 + 调度臂 (12 轮 × 3 seed, 6 域, replay)")
print(" 主口径: bm_bins_* (任务流) + bm2_* (修正版价值函数); random-matched = 同修订版日志值")
print(" 设 BM_RM_SENSITIVITY=1 可改用被覆盖后的目录值 (敏感性对照)")
print("=" * 104)
print()
print(" %-26s %-20s %-20s %-14s %-12s %s" %
("臂", "replay mean±std", "any-time mean±std", "最差遗忘界", "平均遗忘", "n"))
print(" " + "-" * 98)
rows = {}
recs = {}
for name, pat in ARMS:
d = load(pat)
recs[name] = d
if not d:
print(" %-26s (无结果)" % name)
continue
rep = [v["replay"] for v in d.values()]
ant = [v["any_time"] for v in d.values()]
wst = [v["worst"] for v in d.values()]
mf = [v["mean_f"] for v in d.values()]
rep_v = [x for x in rep if x is not None]
cv = 100 * np.std(rep_v, ddof=1) / max(1e-9, abs(np.mean(rep_v))) if len(rep_v) > 1 else float("nan")
rows[name] = (np.mean(rep_v) if rep_v else np.nan,
np.std(rep_v, ddof=1) if len(rep_v) > 1 else 0.0, len(rep_v))
print(" %-26s %-20s %-20s %-14s %-12s %d (CV %4.1f%%)" %
(name, fmt(rep), fmt(ant),
("%.4f" % np.mean([x for x in wst if x is not None])) if any(x is not None for x in wst) else "-",
("%.4f" % np.mean([x for x in mf if x is not None])) if any(x is not None for x in mf) else "-",
len(rep_v), cv))
src = sorted(set(v["src"] for v in d.values()))
if src != ["json"]:
print(" %-26s ↳ 数据来源: %s | 逐 seed: %s" %
("", ",".join(src), {s: (round(v["replay"], 4) if v["replay"] is not None else None)
for s, v in d.items()}))
print()
print(" ── 关键对比 (Welch t, ddof=1; |t|>=2 判显著) ──")
def cmp(a, b, key="replay"):
da, db = recs.get(a), recs.get(b)
if not da or not db:
return " %-30s vs %-30s (数据不全)" % (a, b)
A = [da[s][key] for s in SEEDS if s in da]
B = [db[s][key] for s in SEEDS if s in db]
w = welch(A, B)
if not w:
return " %-30s vs %-30s (样本不足: %d vs %d)" % (a, b, len(A), len(B))
return (" %-30s vs %-30s [%-7s] Δ=%+.4f SE=%.4f t=%+.2f df=%.1f p=%.4f -> %s" %
(a, b, key, w["delta"], w["se"], w["t"], w["df"], w["p"],
"★显著" if abs(w["t"]) >= 2 else "不显著"))
for a, b in [("★ 价值函数 value", "★ 频率对齐 random-matched"),
("★ 价值函数 value", " 均匀随机 random"),
("★ 价值函数 value", " 消融 value-nofb"),
("★ 价值函数 value", "流: perm (随机排列)"),
("启发式 fixed 域序", "流: perm (随机排列)")]:
for key in ("replay", "any_time"):
print(cmp(a, b, key))
print()
print(" 判读要点: `value` vs `random-matched` 才是「价值函数是否更聪明」的干净对照;")
print(" `value` vs `random` 混淆了「更聪明」与「复习更均匀」。")
print(" 注意 `random` 臂三 seed 几乎相同 (CV ~0), 其 t 值会被虚假放大, 不作为结论。")