PAC transition-cover training monitor / pac_transition_cover.py

Failed on benchmark

Raw ⬇ ZIP
  1import json, math, random
  2from pathlib import Path
  3import numpy as np
  4
  5# Reproducible MVP for PAC transition-cover monitoring.
  6SEED = 2077
  7
  8class PACCellMonitor:
  9    def __init__(self, n_cells, epsilon=0.1, delta=0.1):
 10        self.n_cells = n_cells
 11        self.epsilon = epsilon
 12        self.delta = delta
 13        self.required = int(math.ceil(math.log(1.0 / delta) / epsilon))
 14        self.counts = np.zeros(n_cells, dtype=int)
 15        self.successes = np.zeros(n_cells, dtype=int)
 16
 17    def observe(self, cell, hit):
 18        self.counts[cell] += 1
 19        self.successes[cell] += int(hit)
 20
 21    def deficient_cells(self):
 22        # A cell is not PAC-ready until it has the theorem's sample count.
 23        return np.flatnonzero(self.counts < self.required).tolist()
 24
 25    def report(self):
 26        return {"required_n": self.required,
 27                "counts": self.counts.tolist(),
 28                "deficient": self.deficient_cells()}
 29
 30
 31def math_sanity(rng):
 32    # Prediction 1: missed-region probability follows (1-eps)^n and its
 33    # exponential upper bound. Prediction 2: n >= log(1/delta)/eps crosses
 34    # below delta. Each row uses independent Bernoulli region hits.
 35    rows = []
 36    reps = 40000
 37    for eps in (0.02, 0.05, 0.10, 0.20):
 38        for n in (10, 25, 50, 100, 200):
 39            hits = rng.random((reps, n)) < eps
 40            observed = float(np.mean(~np.any(hits, axis=1)))
 41            exact = (1.0 - eps) ** n
 42            bound = math.exp(-n * eps)
 43            rows.append({"epsilon": eps, "n": n, "observed_miss": observed,
 44                         "exact": exact, "exp_bound": bound,
 45                         "abs_error_exact": abs(observed-exact)})
 46    threshold_rows = []
 47    for eps in (0.02, 0.05, 0.10, 0.20):
 48        delta = 0.1
 49        n_req = math.ceil(math.log(1/delta) / eps)
 50        hits = rng.random((reps, n_req)) < eps
 51        observed = float(np.mean(~np.any(hits, axis=1)))
 52        threshold_rows.append({"epsilon": eps, "delta": delta, "n_required": n_req,
 53                               "observed_miss": observed, "target_delta": delta,
 54                               "exact_miss": (1-eps)**n_req})
 55    max_err = max(x["abs_error_exact"] for x in rows)
 56    threshold_pass = all(x["observed_miss"] <= x["target_delta"] + 0.01
 57                         for x in threshold_rows)
 58    return {"scaling_rows": rows, "threshold_rows": threshold_rows,
 59            "max_abs_error_vs_exact": max_err, "threshold_pass": threshold_pass}
 60
 61
 62def dynamics(x, rng):
 63    # A nonlinear noisy transition with a rare, dynamically important branch.
 64    # The branch is concentrated in cell 3 and is observable from x.
 65    cell = np.clip(((x + 1.0) / 2.0 * 8).astype(int), 0, 7)
 66    rare = (cell == 3) & (rng.random(len(x)) < 0.12)
 67    y = 0.72*x + 0.22*np.sin(3*x) + rng.normal(0, 0.025, len(x))
 68    y = y + rare * (0.85 + 0.08*np.sin(5*x))
 69    return np.clip(y, -1.5, 1.5), cell, rare
 70
 71
 72def collect_uniform(rng, budget):
 73    x = rng.uniform(-1, 1, budget)
 74    y, cells, rare = dynamics(x, rng)
 75    return x, y, cells, rare
 76
 77
 78def collect_pac(rng, budget, n_cells=8, epsilon=0.1, delta=0.1):
 79    mon = PACCellMonitor(n_cells, epsilon, delta)
 80    xs, ys, cs, rs = [], [], [], []
 81    # Deficit-first allocation: this is the proposed extra-rollout policy.
 82    for t in range(budget):
 83        deficient = mon.deficient_cells()
 84        if deficient:
 85            c = deficient[t % len(deficient)]
 86        else:
 87            c = int(rng.integers(n_cells))
 88        lo, hi = -1 + 2*c/n_cells, -1 + 2*(c+1)/n_cells
 89        x = np.array([rng.uniform(lo, hi)])
 90        y, actual_cell, rare = dynamics(x, rng)
 91        # For this toy, a hit means observing the rare successor component;
 92        # ordinary transitions are also retained in the training buffer.
 93        mon.observe(c, bool(rare[0]))
 94        xs.append(x[0]); ys.append(y[0]); cs.append(c); rs.append(bool(rare[0]))
 95    return np.array(xs), np.array(ys), np.array(cs), np.array(rs), mon.report()
 96
 97
 98def fit_and_score(x, y, rng, steps=350):
 99    # Small torch MLP; CPU fallback is automatic and CUDA is optional.
100    try:
101        import torch
102        import torch.nn as nn
103        device = "cuda" if torch.cuda.is_available() else "cpu"
104        try:
105            torch.manual_seed(SEED)
106            model = nn.Sequential(nn.Linear(1, 24), nn.Tanh(), nn.Linear(24, 24),
107                                  nn.Tanh(), nn.Linear(24, 1)).to(device)
108            opt = torch.optim.Adam(model.parameters(), lr=0.008)
109            X = torch.tensor(x[:,None], dtype=torch.float32, device=device)
110            Y = torch.tensor(y[:,None], dtype=torch.float32, device=device)
111            for _ in range(steps):
112                idx = torch.randint(0, len(x), (min(128, len(x)),), device=device)
113                loss = ((model(X[idx])-Y[idx])**2).mean()
114                opt.zero_grad(); loss.backward(); opt.step()
115            # Balanced test plus explicit rare-branch probe.
116            tx = np.linspace(-1, 1, 1600)
117            ty, tc, tr = dynamics(tx, np.random.default_rng(SEED+99))
118            with torch.no_grad(): pred = model(torch.tensor(tx[:,None], dtype=torch.float32, device=device)).cpu().numpy()[:,0]
119            mse = float(np.mean((pred-ty)**2))
120            rare_mask = (tc == 3) & tr
121            common_mask = (tc == 3) & ~tr
122            rare_mse = float(np.mean((pred[rare_mask]-ty[rare_mask])**2)) if rare_mask.any() else float('nan')
123            cell_mse = float(np.mean((pred[tc==3]-ty[tc==3])**2))
124            return {"test_mse": mse, "cell3_mse": cell_mse, "rare_branch_mse": rare_mse,
125                    "device": device}
126        except Exception:
127            pass
128    except Exception:
129        pass
130    # Deterministic least-squares fallback if torch/CUDA fails.
131    z = np.stack([x**k for k in range(6)], axis=1)
132    coef = np.linalg.lstsq(z, y, rcond=None)[0]
133    tx = np.linspace(-1,1,1600); ty,tc,tr = dynamics(tx, np.random.default_rng(SEED+99))
134    pred = np.stack([tx**k for k in range(6)], axis=1) @ coef
135    return {"test_mse": float(np.mean((pred-ty)**2)), "cell3_mse": float(np.mean((pred[tc==3]-ty[tc==3])**2)),
136            "rare_branch_mse": float(np.mean((pred[(tc==3)&tr]-ty[(tc==3)&tr])**2)), "device":"cpu-fallback"}
137
138
139def mini_experiment(rng):
140    budget = 640
141    ux, uy, uc, ur = collect_uniform(rng, budget)
142    px, py, pc, pr, report = collect_pac(rng, budget)
143    baseline = fit_and_score(ux, uy, rng)
144    idea = fit_and_score(px, py, rng)
145    return {"budget": budget, "baseline": baseline, "idea": idea,
146            "training_cell_counts": {"uniform": np.bincount(uc, minlength=8).tolist(),
147                                     "pac": np.bincount(pc, minlength=8).tolist()},
148            "pac_monitor": report,
149            "rare_samples": {"uniform": int(ur.sum()), "pac": int(pr.sum())}}
150
151
152def main():
153    random.seed(SEED); np.random.seed(SEED)
154    rng = np.random.default_rng(SEED)
155    out = {"seed": SEED, "math": math_sanity(rng), "mini_experiment": mini_experiment(rng)}
156    Path("results.json").write_text(json.dumps(out, indent=2))
157    print(json.dumps(out, indent=2))
158
159if __name__ == "__main__":
160    main()