Conformal Lower-Clearance Certificate for Neural Selectors / bench_experiment.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1import sys, json, math, random
  2from pathlib import Path
  3import numpy as np
  4import torch
  5
  6sys.path.insert(0, "/home/maxwelhelp/all/math2nn")
  7from bench import get_dataset, make_model, train_model, sweep_baseline, make_report
  8from bench.protocol import evaluate
  9
 10ALPHA = 0.10
 11GAMMA = 0.25
 12K = 8
 13EPOCHS = 12
 14CAL_FRAC = 0.75
 15# This is the complete shared union: every idea lr is also a baseline lr.
 16GRID = [{"lr": 1e-3}, {"lr": 3e-3}, {"lr": 6e-3}]
 17SEEDS = tuple(range(8))
 18
 19
 20def cvar_lower(z, gamma):
 21    z = np.sort(np.asarray(z, dtype=float))
 22    h = gamma * len(z)
 23    m = int(math.floor(h))
 24    total = z[:m].sum()
 25    if h - m > 1e-12 and m < len(z):
 26        total += (h - m) * z[m]
 27    return float(total / h)
 28
 29
 30def cvar_eta(z, gamma):
 31    z = np.asarray(z, dtype=float)
 32    cand = np.r_[z, z.min() - 1., z.max() + 1.]
 33    vals = cand - np.maximum(cand[:, None] - z[None, :], 0).sum(1) / (gamma * len(z))
 34    return float(vals.max())
 35
 36
 37def conformal_q(r, alpha):
 38    r = np.sort(np.asarray(r, dtype=float))
 39    k = int(math.ceil((len(r) + 1) * (1 - alpha)))
 40    return float("inf") if k > len(r) else float(r[k - 1])
 41
 42
 43def seed_all(seed):
 44    random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
 45    if torch.cuda.is_available():
 46        try: torch.cuda.manual_seed_all(seed)
 47        except Exception: pass
 48
 49
 50def fit(seed, lr, idea):
 51    seed_all(seed)
 52    # The built-in dynamics task is the structural match: controlled pendulum
 53    # rollout and stability/safety margins. Use a held-out calibration slice.
 54    ds = get_dataset("dynamics", seed, n_train=400, n_test=400)
 55    ncal = int(len(ds["xtr"]) * (1 - CAL_FRAC))
 56    train_ds = dict(ds)
 57    train_ds["xtr"] = ds["xtr"][:len(ds["xtr"]) - ncal]
 58    train_ds["ytr"] = ds["ytr"][:len(ds["ytr"]) - ncal]
 59    net = make_model("rnn_small", ds["input_shape"], K)
 60    net, _, _ = train_model(net, train_ds, epochs=EPOCHS, lr=lr, batch=128, log=lambda *_: None)
 61    if net is None: return float("nan"), {}
 62    device = next(net.parameters()).device
 63    net.eval()
 64    with torch.no_grad():
 65        pc = net(ds["xtr"][-ncal:].to(device)).detach().cpu().numpy()
 66        pt = net(ds["xte"].to(device)).detach().cpu().numpy()
 67    yc = ds["ytr"][-ncal:].numpy().reshape(-1)
 68    yt = ds["yte"].numpy().reshape(-1)
 69    # Clearance is signed distance to the pendulum angle limit |theta|=1.5.
 70    # It is computed on the selected rollout output, not analytically.
 71    mc = 1.5 - np.abs(pc)
 72    mt = 1.5 - np.abs(pt)
 73    clearance_cvar = np.array([cvar_lower(row, GAMMA) for row in mc])
 74    clearance_test_cvar = np.array([cvar_lower(row, GAMMA) for row in mt])
 75    q = conformal_q(clearance_cvar - (1.5 - np.abs(yc)), ALPHA)
 76    cert = clearance_test_cvar - q
 77    # Preserve predicted direction while enforcing the lower clearance bound.
 78    sign = np.sign(pt.mean(1)); sign[sign == 0] = 1
 79    pred = sign * (1.5 - cert)
 80    pred = np.clip(pred, -3., 3.)
 81    if not idea:
 82        pred = pt.mean(1)
 83    mse = float(np.mean((pred - yt) ** 2))
 84    # Signature is measured from this trained model's predictions and observations.
 85    realized_clear = 1.5 - np.abs(yt)
 86    coverage = float(np.mean(realized_clear >= cert)) if idea else float(np.mean(realized_clear >= clearance_test_cvar))
 87    return mse, {"q": q, "coverage": coverage, "certificate_nonnegative": float(np.mean(cert >= 0)),
 88                 "predicted_clearance_mean": float(np.mean(clearance_test_cvar)),
 89                 "observed_clearance_mean": float(np.mean(realized_clear))}
 90
 91
 92def baseline_factory(cfg):
 93    return lambda seed: fit(seed, cfg["lr"], False)[0]
 94
 95
 96def idea_factory(cfg):
 97    return lambda seed: fit(seed, cfg["lr"], True)[0]
 98
 99
100def main():
101    # Core math sanity check comes before any neural training.
102    rng = np.random.default_rng(2747)
103    errs = [abs(cvar_eta((z := rng.normal(size=K)), GAMMA) - cvar_lower(z, GAMMA)) for _ in range(20)]
104    rr = np.sort(rng.normal(size=31)); q = conformal_q(rr, ALPHA)
105    math_check = {"cvar_max_abs_error": float(max(errs)), "q_is_order_statistic": bool(q in rr),
106                  "q_rank": int(np.where(rr == q)[0][0] + 1)}
107
108    base = sweep_baseline(baseline_factory, GRID, seeds=(0, 1, 2, 3))
109    idea_cfg = base["best_cfg"]
110    # Full idea sweep at exactly the same three learning rates; best is selected
111    # on the four sweep seeds, then rerun on all eight paired seeds.
112    idea_sweep = []
113    for cfg in GRID:
114        r = evaluate(idea_factory(cfg), seeds=(0, 1, 2, 3))
115        idea_sweep.append({"cfg": cfg, "mean": r["mean"]})
116    idea_cfg = min(idea_sweep, key=lambda x: x["mean"])["cfg"]
117    idea_full = evaluate(idea_factory(idea_cfg), seeds=SEEDS)
118    # Mechanism signature is recomputed using trained models on all paired seeds.
119    sig = [fit(s, idea_cfg["lr"], True)[1] for s in SEEDS]
120    signature = {"definition": "trained RNN predicted-vs-observed clearance coverage",
121                 "alpha": ALPHA, "gamma": GAMMA, "K": K,
122                 "coverage_mean": float(np.mean([x["coverage"] for x in sig])),
123                 "coverage_per_seed": [x["coverage"] for x in sig],
124                 "q_mean": float(np.mean([x["q"] for x in sig])),
125                 "certificate_nonnegative_mean": float(np.mean([x["certificate_nonnegative"] for x in sig])),
126                 "target_coverage": 1 - ALPHA,
127                 "confirmed": bool(abs(float(np.mean([x["coverage"] for x in sig])) - (1-ALPHA)) <= 0.06),
128                 "math_check": math_check,
129                 "idea_sweep": idea_sweep}
130    report = make_report("dynamics", "rnn_small", {**base, "sweep_union": GRID}, idea_full, signature)
131    report["idea"]["selected_cfg"] = idea_cfg
132    report["math_check"] = math_check
133    Path("bench_report.json").write_text(json.dumps(report, indent=2))
134    print(json.dumps(report, indent=2))
135
136if __name__ == "__main__": main()