Faithful Latent Fixed-Point Solver / bench_experiment.py
Mechanism confirmed, baseline not beaten
1import os, sys, json, random
2import numpy as np
3import torch
4import torch.nn as nn
5
6sys.path.insert(0, "/home/maxwelhelp/all/math2nn")
7from bench import get_dataset, make_model, train_model, evaluate, sweep_baseline, make_report
8
9# Faithful latent fixed-point solver on the structurally matched controlled-dynamics track.
10# The benchmark target is an 8-step pendulum forecast; the model is trained end-to-end.
11SEEDS = tuple(range(8))
12SWEEP = (0, 1, 2, 3)
13# Union is shared by both systems; baseline gets every setting tried by idea.
14GRID = [
15 {"lr": 0.001, "epochs": 18, "weight_decay": 0.0},
16 {"lr": 0.003, "epochs": 18, "weight_decay": 0.0},
17 {"lr": 0.006, "epochs": 18, "weight_decay": 0.0},
18]
19
20
21def seed_all(seed):
22 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
23 if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
24
25
26class LatentFixedPointRNN(nn.Module):
27 """RNN with m-dimensional latent state z and decoder D(z)->hidden state.
28
29 T is the recurrent latent map. The prediction head is shared in form with the
30 standard GRU baseline. Faithfulness losses use the model's own hidden update,
31 not an analytic teacher formula.
32 """
33 def __init__(self, input_dim, out_dim=1, hidden=64, latent=16):
34 super().__init__()
35 self.latent = latent
36 self.in_proj = nn.Linear(input_dim, latent)
37 self.T = nn.GRUCell(latent, latent)
38 self.D = nn.Linear(latent, hidden)
39 self.E = nn.Linear(hidden, latent)
40 self.head = nn.Sequential(nn.Tanh(), nn.Linear(hidden, out_dim))
41 self.h0 = nn.Parameter(torch.zeros(hidden))
42 self.z0 = nn.Parameter(torch.zeros(latent))
43
44 def forward(self, x, return_aux=False):
45 # x is flattened (batch, 24), represented as 8 (theta,omega,u) tokens.
46 b = x.shape[0]
47 tok = x.reshape(b, 8, 3)
48 z = self.in_proj(tok[:, 0]) + self.z0
49 z_states = []
50 for k in range(8):
51 if k > 0:
52 z = self.T(self.in_proj(tok[:, k]) + z)
53 z_states.append(z)
54 zf = z_states[-1]
55 h = self.D(zf)
56 pred = self.head(h)
57 if not return_aux:
58 return pred
59 # Intertwining residual in the learned state: E(D(T(z)))-T(z),
60 # plus reconstruction D(E(h))-h. Both are model-behavior quantities.
61 zs = torch.stack(z_states, dim=1)
62 hs = self.D(zs)
63 comm = self.E(hs[:, 1:]) - zs[:, 1:]
64 rec = self.D(self.E(hs)) - hs
65 return pred, comm, rec
66
67
68def train_baseline(cfg, seed):
69 seed_all(seed)
70 d = get_dataset("dynamics", seed, n_train=400, n_test=400)
71 model = make_model("rnn_small", d["input_shape"], d["out_dim"])
72 _, metric, _ = train_model(model, d, epochs=cfg["epochs"], lr=cfg["lr"],
73 batch=128, weight_decay=cfg["weight_decay"], log=lambda *a: None)
74 return float(metric) if metric is not None else float("inf")
75
76
77def train_idea(cfg, seed, collect=False):
78 seed_all(seed)
79 d = get_dataset("dynamics", seed, n_train=400, n_test=400)
80 model = LatentFixedPointRNN(3, 1, hidden=64, latent=16)
81 # train_model cannot inject a mechanism loss, so this is intentionally a
82 # local loop: the intervention is the faithful intertwining objective.
83 device = "cuda" if torch.cuda.is_available() else "cpu"
84 try:
85 model.to(device); torch.zeros(1, device=device)
86 except Exception:
87 device = "cpu"; model.to(device)
88 opt = torch.optim.Adam(model.parameters(), lr=cfg["lr"], weight_decay=cfg["weight_decay"])
89 xtr, ytr = d["xtr"].to(device), d["ytr"].to(device)
90 xte, yte = d["xte"].to(device), d["yte"].to(device)
91 n = len(xtr)
92 for _ in range(cfg["epochs"]):
93 perm = torch.randperm(n, device=device)
94 for j in range(0, n, 128):
95 ix = perm[j:j+128]
96 pred, comm, rec = model(xtr[ix], True)
97 task = ((pred-ytr[ix])**2).mean()
98 # Explicit E∘D ~= identity and D∘T ~= decoded next state.
99 loss = task + 0.08 * (comm**2).mean() + 0.08 * (rec**2).mean()
100 opt.zero_grad(); loss.backward(); opt.step()
101 with torch.no_grad():
102 pred = model(xte)
103 mse = ((pred-yte)**2).mean().item()
104 _, comm, rec = model(xte, True)
105 sig = {"comm_rmse": float(torch.sqrt((comm**2).mean()).item()),
106 "reconstruction_rmse": float(torch.sqrt((rec**2).mean()).item())}
107 if collect: return mse, sig, model
108 return mse
109
110
111def main():
112 # Baseline is swept over the complete union grid, including all idea settings.
113 base = sweep_baseline(lambda cfg: lambda seed: train_baseline(cfg, seed), GRID, seeds=SWEEP)
114 # Required idea 3-point sweep at baseline-best and two nearby settings; GRID is
115 # exactly those three settings and is also fully evaluated for baseline.
116 idea_trials = []
117 for cfg in GRID:
118 r = evaluate(lambda seed, c=cfg: train_idea(c, seed), seeds=SEEDS)
119 idea_trials.append({"cfg": cfg, "result": r})
120 best_trial = min(idea_trials, key=lambda q: q["result"]["mean"])
121 idea = best_trial["result"]
122 # Re-test trained models on benchmark data for a behavior-based signature.
123 observed = []
124 for s in SEEDS:
125 mse, sig, _ = train_idea(best_trial["cfg"], s, collect=True)
126 observed.append(sig)
127 mean_comm = float(np.mean([q["comm_rmse"] for q in observed]))
128 mean_rec = float(np.mean([q["reconstruction_rmse"] for q in observed]))
129 # Stage-1 prediction at NN scale: lower intertwining residual should accompany
130 # latent rollout; confirmation is quantitative only when both are small.
131 signature = {
132 "prediction": "trained latent update should approximately commute with encoding/decoding",
133 "observed_predicted_numbers": {"predicted_comm_rmse": "near_zero", "observed_comm_rmse": mean_comm,
134 "predicted_reconstruction_rmse": "near_zero", "observed_reconstruction_rmse": mean_rec},
135 "confirmed": bool(mean_comm < 0.10 and mean_rec < 0.10),
136 "n_behavior_models": 8,
137 }
138 report = make_report("dynamics", "rnn_small", base, idea, {"idea_sweep": idea_trials, **signature})
139 report["protocol"] = {"paired_seeds": list(SEEDS), "sweep_seeds": list(SWEEP),
140 "grid_union": GRID, "baseline_tuned_and_reevaluated": True,
141 "task_match": "controlled pendulum stability/control"}
142 with open("bench_report.json", "w") as f: json.dump(report, f, indent=2)
143 print(json.dumps(report, indent=2))
144
145if __name__ == "__main__": main()