Action-calibrated cycle-hopping RNN / bench_stage2.py
Failed on benchmark
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()