Tiny Local Recurrence with Adaptive Computation / bench_experiment.py

Unverified

Raw ⬇ ZIP
  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()