Action-calibrated cycle-hopping RNN / bench_stage2.py

Failed on benchmark

Raw ⬇ ZIP
  1import sys, json, random
  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, train_model, sweep_baseline, make_report
  9
 10SEEDS = tuple(range(8))
 11SWEEP_SEEDS = tuple(range(4))
 12EPOCHS = 12
 13BATCH = 128
 14# Union of learning rates is used on both sides. Adam weight decay is also
 15# swept for the baseline's central optimization knob.
 16LRS = (1e-3, 3e-3, 1e-2)
 17WDS = (0.0, 1e-4)
 18IDEA_LAMBDA = 0.02
 19TARGET = 0.90
 20
 21
 22def seed_all(seed):
 23    random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
 24    if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
 25
 26
 27def ds_for(seed):
 28    return get_dataset("dynamics", seed=seed, n_train=400, n_test=400)
 29
 30
 31def baseline_fn(cfg):
 32    def run(seed):
 33        seed_all(seed)
 34        ds = ds_for(seed)
 35        model = make_model("rnn_small", tuple(ds["xtr"].shape[1:]), 1)
 36        _, metric, _ = train_model(model, ds, epochs=EPOCHS, lr=cfg["lr"],
 37                                   batch=BATCH, weight_decay=cfg["weight_decay"], log=lambda *_: None)
 38        return float(metric) if metric is not None else float("inf")
 39    return run
 40
 41
 42def recurrent_contraction_penalty(model, x, target=TARGET):
 43    """Penalty on the local hidden transition expansion of the trained GRU.
 44
 45    The hidden state before the final observed control input is computed, then
 46    two deterministic one-step GRU transitions are compared under a small
 47    hidden perturbation. Only expansion above target is penalized, preserving
 48    useful contracting dynamics instead of forcing every transition to zero.
 49    """
 50    seq = x.view(x.shape[0], -1, 3)
 51    with torch.no_grad():
 52        _, h0 = model.rnn(seq[:, :-1])
 53    h0 = h0.detach().requires_grad_(True)
 54    last = seq[:, -1:, :]
 55    out, _ = model.rnn(last, h0)
 56    eps = torch.randn_like(h0) * 1e-3
 57    outp, _ = model.rnn(last, h0 + eps)
 58    # Batch-averaged local gain estimate; denominator avoids scale artifacts.
 59    gain = (outp - out).pow(2).mean(dim=(0, 2)).sqrt() / (eps.pow(2).mean(dim=(0, 2)).sqrt() + 1e-8)
 60    return torch.relu(gain - target).pow(2).mean()
 61
 62
 63def idea_train(model, ds, epochs, lr, weight_decay, lam):
 64    errs = []
 65    ladder = [("cuda", False)] if torch.cuda.is_available() else []
 66    ladder += [("cpu", False)]
 67    for device, _ in ladder:
 68        try:
 69            net = model.to(device)
 70            xtr, ytr = ds["xtr"].to(device), ds["ytr"].to(device)
 71            opt = torch.optim.Adam(net.parameters(), lr=lr, weight_decay=weight_decay)
 72            lossf = nn.MSELoss()
 73            for _ in range(epochs):
 74                net.train(); perm = torch.randperm(len(xtr), device=device)
 75                for i in range(0, len(xtr), BATCH):
 76                    idx = perm[i:i+BATCH]; xb, yb = xtr[idx], ytr[idx]
 77                    pred = net(xb)
 78                    loss = lossf(pred, yb) + lam * recurrent_contraction_penalty(net, xb)
 79                    opt.zero_grad(); loss.backward(); torch.nn.utils.clip_grad_norm_(net.parameters(), 5.0); opt.step()
 80            net.eval()
 81            with torch.no_grad(): metric = float(((net(ds["xte"].to(device)) - ds["yte"].to(device))**2).mean())
 82            return net, metric
 83        except RuntimeError as e:
 84            errs.append(str(e));
 85            if device == "cuda":
 86                torch.cuda.empty_cache()
 87    return None, float("inf")
 88
 89
 90def idea_fn(cfg):
 91    def run(seed):
 92        seed_all(seed); ds = ds_for(seed)
 93        model = make_model("rnn_small", tuple(ds["xtr"].shape[1:]), 1)
 94        _, metric = idea_train(model, ds, EPOCHS, cfg["lr"], cfg["weight_decay"], cfg["lambda"])
 95        return metric
 96    return run
 97
 98
 99def trained_signature(seed, cfg, idea):
100    seed_all(seed); ds = ds_for(seed)
101    model = make_model("rnn_small", tuple(ds["xtr"].shape[1:]), 1)
102    if idea:
103        model, _ = idea_train(model, ds, EPOCHS, cfg["lr"], cfg["weight_decay"], cfg["lambda"])
104    else:
105        model, _, _ = train_model(model, ds, epochs=EPOCHS, lr=cfg["lr"], batch=BATCH, weight_decay=cfg["weight_decay"], log=lambda *_: None)
106    # Signature probing is deliberately CPU-only: it is a small diagnostic and
107    # avoids consuming the shared GPU allocator after the paired runs.
108    model = model.cpu()
109    device = torch.device("cpu")
110    x = ds["xte"][:128].to(device)
111    seq = x.view(x.shape[0], -1, 3)
112    with torch.no_grad(): _, h = model.rnn(seq[:, :-1])
113    eps = torch.randn_like(h) * 1e-3
114    with torch.no_grad():
115        a, _ = model.rnn(seq[:, -1:], h); b, _ = model.rnn(seq[:, -1:], h + eps)
116    gain = float(((b-a).pow(2).mean().sqrt() / (eps.pow(2).mean().sqrt()+1e-8)).cpu())
117    return gain
118
119
120def main():
121    baseline_grid = [{"lr": lr, "weight_decay": wd} for lr in LRS for wd in WDS]
122    base = sweep_baseline(baseline_fn, baseline_grid, seeds=SWEEP_SEEDS)
123    best = base["best_cfg"]
124    # Three idea settings: best baseline lr and both adjacent rates, all also
125    # present in baseline_grid (same shared architecture and optimizer family).
126    idea_grid = [
127        {"lr": 1e-3, "weight_decay": best["weight_decay"], "lambda": IDEA_LAMBDA},
128        {"lr": 3e-3, "weight_decay": best["weight_decay"], "lambda": IDEA_LAMBDA},
129        {"lr": 1e-2, "weight_decay": best["weight_decay"], "lambda": IDEA_LAMBDA},
130    ]
131    idea_runs = []
132    for cfg in idea_grid:
133        r = __import__('bench').evaluate(idea_fn(cfg), seeds=SEEDS)
134        idea_runs.append({"cfg": cfg, "result": r})
135    chosen = min(idea_runs, key=lambda z: z["result"]["mean"])
136    rep = make_report("dynamics", "rnn_small", base, chosen["result"], extra={})
137    sig_b = trained_signature(0, best, False); sig_i = trained_signature(0, chosen["cfg"], True)
138    # The quantitative stage-1 prediction is contraction/stability; this is
139    # measured on trained systems, not analytically asserted.
140    rep["mechanism_signature"] = {
141        "prediction": "cycle-stabilizing intervention lowers local recurrent hidden-state gain",
142        "baseline_hidden_gain": sig_b, "idea_hidden_gain": sig_i,
143        "relative_gain_change": (sig_i-sig_b)/(abs(sig_b)+1e-8),
144        "confirmed": bool(sig_i < sig_b),
145        "measurement": "finite 1e-3 hidden perturbation through trained GRU final-step transition"
146    }
147    rep["idea_sweep"] = idea_runs
148    rep["protocol_notes"] = {"epochs": EPOCHS, "n_train": 400, "n_test": 400, "baseline_grid": baseline_grid, "idea_grid": idea_grid}
149    Path("bench_report.json").write_text(json.dumps(rep, indent=2))
150    print(json.dumps(rep, indent=2))
151
152if __name__ == "__main__": main()