File size: 9,791 Bytes
216004d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
#!/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 值会被虚假放大, 不作为结论。")