Equivariant Shared-Mechanism World Model / bench_experiment.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1import json
  2import random
  3from pathlib import Path
  4import numpy as np
  5import torch
  6from torch import nn
  7import sys
  8sys.path.insert(0, "/home/maxwelhelp/all/math2nn")
  9from bench import get_dataset, make_model, train_model, evaluate, sweep_baseline, make_report
 10
 11EPOCHS = 10
 12BATCH = 128
 13WEIGHT_DECAY = 0.0
 14
 15
 16def seed_all(seed):
 17    random.seed(seed)
 18    np.random.seed(seed)
 19    torch.manual_seed(seed)
 20    if torch.cuda.is_available():
 21        torch.cuda.manual_seed_all(seed)
 22
 23
 24def sign_transform(x):
 25    # Pendulum equations are equivariant under (theta, omega, u) -> -(theta, omega, u).
 26    return -x
 27
 28
 29def output_transform(y):
 30    return -y
 31
 32
 33def equivariance_penalty(net, x):
 34    pred = net(x)
 35    pred_transformed = net(sign_transform(x))
 36    return ((pred_transformed - output_transform(pred)) ** 2).mean()
 37
 38
 39def train_idea(net, ds, epochs, lr, lam):
 40    # Same Adam/batch/epochs as bench.train_model; only the proposed loss is added.
 41    devices = [("cuda", False), ("cuda", True), ("cpu", False)] if torch.cuda.is_available() else [("cpu", False)]
 42    errors = []
 43    for devname, no_cudnn in devices:
 44        try:
 45            device = torch.device(devname)
 46            old_cudnn = torch.backends.cudnn.enabled
 47            if no_cudnn:
 48                torch.backends.cudnn.enabled = False
 49            net = net.to(device)
 50            opt = torch.optim.Adam(net.parameters(), lr=lr, weight_decay=WEIGHT_DECAY)
 51            xtr, ytr = ds["xtr"].to(device), ds["ytr"].to(device)
 52            for _ in range(epochs):
 53                net.train()
 54                perm = torch.randperm(len(xtr), device=device)
 55                for i in range(0, len(xtr), BATCH):
 56                    idx = perm[i:i+BATCH]
 57                    pred = net(xtr[idx])
 58                    loss = ((pred - ytr[idx]) ** 2).mean() + lam * equivariance_penalty(net, xtr[idx])
 59                    opt.zero_grad(set_to_none=True)
 60                    loss.backward()
 61                    opt.step()
 62            net.eval()
 63            with torch.no_grad():
 64                metric = float(((net(ds["xte"].to(device)) - ds["yte"].to(device)) ** 2).mean().cpu())
 65            torch.backends.cudnn.enabled = old_cudnn
 66            return net, metric
 67        except RuntimeError as exc:
 68            errors.append(str(exc)[:160])
 69            if devname == "cuda":
 70                continue
 71            raise
 72    raise RuntimeError("training failed: " + " | ".join(errors))
 73
 74
 75def fit_baseline(cfg, seed):
 76    seed_all(seed)
 77    ds = get_dataset("dynamics", seed, n_train=400, n_test=400)
 78    net = make_model("rnn_small", ds["input_shape"], ds["out_dim"])
 79    _, metric, _ = train_model(net, ds, epochs=cfg["epochs"], lr=cfg["lr"], batch=BATCH, weight_decay=WEIGHT_DECAY, log=lambda *_: None)
 80    return metric
 81
 82
 83def fit_idea(cfg, seed, capture=False):
 84    seed_all(seed)
 85    ds = get_dataset("dynamics", seed, n_train=400, n_test=400)
 86    net = make_model("rnn_small", ds["input_shape"], ds["out_dim"])
 87    trained, metric = train_idea(net, ds, cfg["epochs"], cfg["lr"], cfg["lam"])
 88    if not capture:
 89        return metric
 90    device = next(trained.parameters()).device
 91    x = ds["xte"].to(device)
 92    with torch.no_grad():
 93        p = trained(x)
 94        pt = trained(-x)
 95        eq = float(((pt + p) ** 2).mean().cpu())
 96        # Behavioural signature: compare observed transformed prediction to the
 97        # transformed prediction expected from the trained model's original output.
 98        pred_scale = float(p.abs().mean().cpu())
 99    return metric, {"mean_prediction_abs": pred_scale, "trained_model_c2_penalty": eq}
100
101
102def math_check():
103    x = torch.randn(64, 24)
104    y = torch.randn(64, 1)
105    return {"c2_input_composition_max_abs": float((sign_transform(sign_transform(x)) - x).abs().max()),
106            "c2_output_composition_max_abs": float((output_transform(output_transform(y)) - y).abs().max()),
107            "exact_transform_identity": bool(torch.equal(sign_transform(sign_transform(x)), x))}
108
109
110def main():
111    # Union parity: every lr and lambda considered for the idea is also run by
112    # baseline; baseline lambda is the standard no-penalty value 0.
113    lrs = [1e-3, 3e-3, 1e-2]
114    base_grid = [{"lr": lr, "epochs": EPOCHS, "lam": 0.0} for lr in lrs]
115    idea_grid = [{"lr": lr, "epochs": EPOCHS, "lam": lam} for lr in lrs for lam in [0.05, 0.2, 1.0]]
116    def base_fn(cfg):
117        return lambda seed: fit_baseline(cfg, seed)
118    base = sweep_baseline(base_fn, base_grid)
119    # Evaluate all idea settings on sweep seeds for selection, then full 8 seeds.
120    idea_trials = []
121    for cfg in idea_grid:
122        r = evaluate(lambda seed, c=cfg: fit_idea(c, seed), seeds=(0, 1, 2, 3))
123        idea_trials.append({"cfg": cfg, "mean": r["mean"]})
124    best_cfg = min(idea_trials, key=lambda z: z["mean"])["cfg"]
125    idea = evaluate(lambda seed: fit_idea(best_cfg, seed), seeds=tuple(range(8)))
126    sig_metric, sig = fit_idea(best_cfg, 0, capture=True)
127    sig.update({"prediction_metric_seed0": sig_metric,
128                "expected_c2_penalty_direction": "lower is more equivariant",
129                "confirmed": sig["trained_model_c2_penalty"] < 1e-3})
130    report = make_report("dynamics", "rnn_small", {"best_cfg": base["best_cfg"], "sweep": base["sweep"], "full": base["full"]}, idea, {"mechanism_signature": sig, "math_check": math_check(), "idea_sweep": idea_trials, "structural_match": "controlled pendulum rollout has sign-equivariant local dynamics"})
131    Path("bench_report.json").write_text(json.dumps(report, indent=2))
132    print(json.dumps(report, indent=2))
133
134
135if __name__ == "__main__":
136    main()