Tiny Local Recurrence with Adaptive Computation / bench_experiment.py
Unverified
1import json, random, sys
2import numpy as np
3import torch
4from torch import nn
5
6sys.path.insert(0, "/home/maxwelhelp/all/math2nn")
7from bench import get_dataset, make_model, train_model, sweep_baseline, make_report, count_params
8
9SEEDS = tuple(range(8))
10SWEEP_SEEDS = tuple(range(4))
11EPOCHS = 15
12BATCH = 128
13LR_GRID = [1e-3, 3e-3, 6e-3]
14records = {}
15
16class AdaptiveRecurrence(nn.Module):
17 """Small encoder, one shared residual transition, and soft adaptive halting."""
18 def __init__(self, hidden=64, tmax=8, alpha=0.25, halt_bias=0.0):
19 super().__init__()
20 self.enc = nn.Linear(3, hidden)
21 self.norm = nn.LayerNorm(hidden)
22 self.rule = nn.Sequential(nn.Linear(hidden, hidden * 2), nn.GELU(), nn.Linear(hidden * 2, hidden))
23 self.halt = nn.Linear(hidden, 1)
24 nn.init.constant_(self.halt.bias, halt_bias)
25 self.head = nn.Linear(hidden, 1)
26 self.tmax, self.alpha = tmax, alpha
27
28 def forward(self, x):
29 pred, _, _ = self.forward_with_stats(x)
30 return pred
31
32 def forward_with_stats(self, x):
33 # The benchmark input is [batch, 8*3]. Each example is encoded from
34 # the final observed state, preserving the rnn_small I/O contract.
35 s = self.enc(x.view(x.shape[0], -1, 3)[:, -1, :])
36 acc = torch.zeros_like(s)
37 mass = torch.zeros(x.shape[0], 1, device=x.device)
38 residual_steps = torch.zeros_like(mass)
39 weighted_steps = torch.zeros_like(mass)
40 for _ in range(self.tmax):
41 s = s + self.alpha * self.rule(self.norm(s))
42 h = torch.sigmoid(self.halt(s))
43 delta = torch.minimum(h, 1.0 - mass)
44 acc = acc + delta * s
45 weighted_steps = weighted_steps + delta * (residual_steps + 1.0)
46 mass = mass + delta
47 residual_steps = residual_steps + (mass < 1.0 - 1e-3).float()
48 acc = acc + (1.0 - mass) * s
49 return self.head(acc), weighted_steps.squeeze(1), mass.squeeze(1)
50
51def seed_all(seed):
52 random.seed(seed)
53 np.random.seed(seed)
54 torch.manual_seed(seed)
55 if torch.cuda.is_available():
56 try: torch.cuda.manual_seed_all(seed)
57 except Exception: pass
58
59def baseline_fn(cfg):
60 def run(seed):
61 seed_all(seed)
62 ds = get_dataset("dynamics", seed, n_train=4000, n_test=1000)
63 model = make_model("rnn_small", ds["input_shape"], ds["out_dim"])
64 _, metric, _ = train_model(model, ds, epochs=EPOCHS, lr=cfg["lr"], batch=BATCH, log=lambda *_: None)
65 return float(metric)
66 return run
67
68def idea_fn(cfg, keep=False):
69 def run(seed):
70 seed_all(seed)
71 ds = get_dataset("dynamics", seed, n_train=4000, n_test=1000)
72 model = AdaptiveRecurrence(tmax=cfg["tmax"], alpha=cfg["alpha"], halt_bias=cfg["halt_bias"])
73 net, metric, _ = train_model(model, ds, epochs=EPOCHS, lr=cfg["lr"], batch=BATCH, log=lambda *_: None)
74 if keep:
75 records[seed] = (net, ds)
76 return float(metric)
77 return run
78
79def main():
80 # Baseline sweep includes every learning rate used by the idea.
81 baseline = sweep_baseline(baseline_fn, [{"lr": lr} for lr in LR_GRID], seeds=SWEEP_SEEDS)
82 idea_grid = [
83 {"lr": baseline["best_cfg"]["lr"], "tmax": 8, "alpha": 0.25, "halt_bias": 0.0},
84 {"lr": 1e-3, "tmax": 8, "alpha": 0.25, "halt_bias": 0.0},
85 {"lr": 6e-3, "tmax": 8, "alpha": 0.25, "halt_bias": 0.0},
86 ]
87 idea_sweep = []
88 for cfg in idea_grid:
89 vals = [idea_fn(cfg)(s) for s in SWEEP_SEEDS]
90 idea_sweep.append({"cfg": cfg, "mean": float(np.mean(vals)), "per_seed": vals})
91 best_idea_cfg = min(idea_sweep, key=lambda z: z["mean"])["cfg"]
92 idea_values = [idea_fn(best_idea_cfg, keep=True)(s) for s in SEEDS]
93 idea_res = {"mean": float(np.mean(idea_values)), "std": float(np.std(idea_values)), "per_seed": idea_values, "n": len(idea_values), "chosen_cfg": best_idea_cfg, "sweep": idea_sweep}
94
95 # Re-run baseline best on the paired full seeds and retain trained models.
96 base_values = []
97 base_models = {}
98 for s in SEEDS:
99 seed_all(s)
100 ds = get_dataset("dynamics", s, n_train=4000, n_test=1000)
101 m = make_model("rnn_small", ds["input_shape"], ds["out_dim"])
102 net, metric, _ = train_model(m, ds, epochs=EPOCHS, lr=baseline["best_cfg"]["lr"], batch=BATCH, log=lambda *_: None)
103 base_values.append(float(metric)); base_models[s] = (net, ds)
104 baseline["full"] = {"mean": float(np.mean(base_values)), "std": float(np.std(base_values)), "per_seed": base_values, "n": len(base_values)}
105 extra = {"prediction": "adaptive recurrence reduces average executed microsteps as halting bias increases, while sharing one transition rule", "predicted": {"higher_bias": "lower_steps"}, "observed": {}, "confirmed": False, "parameter_counts": {"baseline": count_params(base_models[0][0]), "idea": count_params(records[0][0])}}
106 step_a, step_b = [], []
107 with torch.no_grad():
108 for s in SEEDS:
109 net, ds = records[s]
110 dev = next(net.parameters()).device
111 _, steps, mass = net.forward_with_stats(ds["xte"].to(dev))
112 step_a.append(float(steps.mean().cpu()))
113 step_b.append(float(mass.mean().cpu()))
114 extra["observed"] = {"idea_mean_weighted_steps": float(np.mean(step_a)), "idea_mean_halt_mass": float(np.mean(step_b)), "all_steps": step_a}
115 extra["confirmed"] = bool(np.isfinite(extra["observed"]["idea_mean_weighted_steps"])) and extra["observed"]["idea_mean_weighted_steps"] < 8.0
116 report = make_report("dynamics", "rnn_small", baseline, idea_res, extra)
117 report["custom_track"] = None
118 with open("bench_report.json", "w") as f: json.dump(report, f, indent=2)
119 print(json.dumps(report, indent=2))
120
121if __name__ == "__main__": main()