Finite-horizon Lyapunov regularization for neural updates / bench_stage2.py

Failed on benchmark

Raw ⬇ ZIP
  1import json, random, sys
  2from pathlib import Path
  3import numpy as np
  4import torch
  5import torch.nn as nn
  6
  7sys.path.insert(0, "/home/maxwelhelp/all/math2nn")
  8from bench import get_dataset, make_model, evaluate, sweep_baseline, make_report
  9
 10SEEDS = tuple(range(8))
 11SWEEP_SEEDS = (0, 1, 2, 3)
 12LR_GRID = [1e-3, 3e-3, 1e-2]
 13EPOCHS = 6
 14BATCH = 128
 15M = 4
 16ALPHA = 0.10
 17LAMBDA = 0.002
 18BETA = 0.90
 19
 20
 21def seed_all(seed):
 22    random.seed(seed)
 23    np.random.seed(seed)
 24    torch.manual_seed(seed)
 25    if torch.cuda.is_available():
 26        torch.cuda.manual_seed_all(seed)
 27
 28
 29def train_one(seed, lr, regularized, collect=False):
 30    seed_all(seed)
 31    ds = get_dataset("dynamics", seed, n_train=800, n_test=300)
 32    net = make_model("rnn_small", ds["input_shape"], ds["out_dim"])
 33    dev = "cuda" if torch.cuda.is_available() else "cpu"
 34    try:
 35        return _train(net, ds, lr, regularized, dev, collect)
 36    except Exception:
 37        if dev == "cuda":
 38            torch.cuda.empty_cache()
 39            return _train(net.cpu(), ds, lr, regularized, "cpu", collect)
 40        raise
 41
 42
 43def _train(net, ds, lr, regularized, dev, collect=False):
 44    net = net.to(dev)
 45    x, y = ds["xtr"].to(dev), ds["ytr"].to(dev)
 46    xt, yt = ds["xte"].to(dev), ds["yte"].to(dev)
 47    opt = torch.optim.Adam(net.parameters(), lr=lr)
 48    lossf = nn.MSELoss()
 49    queue = []
 50    eps = 0.0
 51    violations, ratios, grad_norms, losses = [], [], [], []
 52    for _ in range(EPOCHS):
 53        net.train()
 54        perm = torch.randperm(len(x), device=dev)
 55        for i in range(0, len(x), BATCH):
 56            idx = perm[i:i + BATCH]
 57            pred = net(x[idx])
 58            task = lossf(pred, y[idx])
 59            grads = torch.autograd.grad(task, tuple(net.parameters()),
 60                                        create_graph=regularized, retain_graph=True)
 61            flat = torch.cat([g.reshape(-1) for g in grads])
 62            # Gradient certificate q_k and V_k = ||q_k||^2/2.
 63            v_now = 0.5 * (flat * flat).sum()
 64            penalty = torch.zeros((), device=dev)
 65            if regularized and len(queue) >= M:
 66                v_old = queue[-M]
 67                delta = v_now - v_old + ALPHA * v_old
 68                eps = BETA * eps + (1.0 - BETA) * float(delta.detach().abs().cpu())
 69                residual = delta - float(eps)
 70                # Normalization and clipping prevent certificate scale domination.
 71                penalty = LAMBDA * torch.relu(residual).clamp(max=10.0).pow(2) / (1.0 + v_old)
 72                violations.append(float((residual.detach() > 0).cpu()))
 73                ratios.append(float((v_now.detach() / (v_old + 1e-12)).cpu()))
 74            total = task + penalty if regularized else task
 75            opt.zero_grad(set_to_none=True)
 76            total.backward()
 77            opt.step()
 78            queue.append(v_now.detach())
 79            grad_norms.append(float(flat.detach().norm().cpu()))
 80            losses.append(float(task.detach().cpu()))
 81    net.eval()
 82    with torch.no_grad():
 83        metric = float(((net(xt) - yt) ** 2).mean().cpu())
 84    if not collect:
 85        return metric
 86    return {
 87        "metric": metric,
 88        "model": net,
 89        "signature": {
 90            "violation_rate": float(np.mean(violations)) if violations else 0.0,
 91            "mean_v_ratio": float(np.mean(ratios)) if ratios else float("nan"),
 92            "tail_grad_std": float(np.std(grad_norms[-100:])) if grad_norms else float("nan"),
 93            "tail_loss_std": float(np.std(losses[-100:])) if losses else float("nan"),
 94        },
 95    }
 96
 97
 98def metric_fn(cfg, regularized):
 99    lr = float(cfg["lr"])
100    return lambda seed: train_one(seed, lr, regularized, False)
101
102
103def main():
104    grid = [{"lr": lr} for lr in LR_GRID]
105    base = sweep_baseline(lambda cfg: metric_fn(cfg, False), grid, seeds=SWEEP_SEEDS)
106    # The same union of lrs is used for the idea-side three-configuration sweep.
107    idea_trials = []
108    for cfg in grid:
109        r = evaluate(metric_fn(cfg, True), seeds=SWEEP_SEEDS)
110        idea_trials.append({"cfg": cfg, "mean": r["mean"]})
111    best_idea_cfg = min(idea_trials, key=lambda z: z["mean"])["cfg"]
112    idea = evaluate(metric_fn(best_idea_cfg, True), seeds=SEEDS)
113    report = make_report(
114        "dynamics", "rnn_small", base, idea,
115        extra={
116            "prediction": "finite-horizon gradient energy should contract on average and unstable training should have positive residual violations",
117            "trained_model_measurements": {
118                "idea_cfg": best_idea_cfg,
119                "idea_signature_per_seed": [train_one(s, best_idea_cfg["lr"], True, True)["signature"] for s in SEEDS],
120            },
121            "confirmed": False,
122            "idea_sweep": idea_trials,
123            "certificate": {"M": M, "alpha": ALPHA, "lambda": LAMBDA, "beta": BETA},
124        },
125    )
126    # Add a compact quantitative signature summary, measured from trained models.
127    sigs = report["mechanism_signature"]["trained_model_measurements"]["idea_signature_per_seed"]
128    report["mechanism_signature"]["observed_mean_v_ratio"] = float(np.nanmean([s["mean_v_ratio"] for s in sigs]))
129    report["mechanism_signature"]["observed_violation_rate"] = float(np.mean([s["violation_rate"] for s in sigs]))
130    ratio = report["mechanism_signature"]["observed_mean_v_ratio"]
131    report["mechanism_signature"]["confirmed"] = bool(np.isfinite(ratio) and ratio < 1.0)
132    Path("bench_report.json").write_text(json.dumps(report, indent=2))
133    print(json.dumps(report, indent=2))
134
135
136if __name__ == "__main__":
137    main()