ISS-Gated Positive Neural State Module / bench_experiment.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, evaluate, sweep_baseline, make_report
9
10SEEDS = tuple(range(8))
11EPOCHS = 12
12BATCH = 128
13# Shared union: baseline is evaluated at every lr/wd used by idea.
14GRID = [
15 {"lr": 1e-3, "weight_decay": 0.0},
16 {"lr": 2e-3, "weight_decay": 0.0},
17 {"lr": 3e-3, "weight_decay": 0.0},
18 {"lr": 1e-3, "weight_decay": 1e-4},
19 {"lr": 2e-3, "weight_decay": 1e-4},
20 {"lr": 3e-3, "weight_decay": 1e-4},
21]
22IDEA_GRID = [
23 {"lr": 1e-3, "weight_decay": 0.0, "lambda_iss": 0.0},
24 {"lr": 2e-3, "weight_decay": 0.0, "lambda_iss": 0.01},
25 {"lr": 3e-3, "weight_decay": 1e-4, "lambda_iss": 0.03},
26]
27
28
29def seed_all(seed):
30 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
31 if torch.cuda.is_available():
32 try: torch.cuda.manual_seed_all(seed)
33 except Exception: pass
34
35
36class PositiveISSRNN(nn.Module):
37 """Positive birth/death mass-action recurrent state with ISS residual."""
38 def __init__(self, out_dim=1, hidden=64, dt=0.25):
39 super().__init__()
40 self.hidden, self.dt = hidden, dt
41 self.rate = nn.Linear(3, 2 * hidden)
42 self.readout = nn.Linear(hidden, out_dim)
43 self.register_buffer("h_star", torch.ones(hidden))
44 self.last_signature = {}
45 self.last_v_tensor = None
46 self.last_vdot_tensor = None
47
48 def forward(self, x, return_stats=False):
49 seq = x.view(x.shape[0], -1, 3)
50 h = torch.ones(x.shape[0], self.hidden, device=x.device)
51 v_before, vdot_vals, h_vals = [], [], []
52 for t in range(seq.shape[1]):
53 u = torch.nn.functional.softplus(self.rate(seq[:, t])) + 1e-4
54 prod, decay = u[:, :self.hidden], u[:, self.hidden:]
55 # x -> 2x and x -> empty: f = prod - decay*h
56 v = h * torch.log(torch.clamp(h / self.h_star, min=1e-8)) - h + self.h_star
57 h_next = torch.clamp(h + self.dt * (prod - decay * h), min=1e-5)
58 vn = h_next * torch.log(torch.clamp(h_next / self.h_star, min=1e-8)) - h_next + self.h_star
59 v_before.append(v.mean()); vdot_vals.append(((vn.sum(1)-v.sum(1))/self.dt).mean())
60 h = h_next; h_vals.append(h.detach())
61 out = self.readout(h)
62 self.last_v_tensor = torch.stack(v_before).mean()
63 self.last_vdot_tensor = torch.stack(vdot_vals).mean()
64 if return_stats:
65 self.last_signature = {"V": float(torch.stack(v_before).mean().detach().cpu()),
66 "Vdot": float(torch.stack(vdot_vals).mean().detach().cpu()),
67 "h_max": float(torch.cat(h_vals).max().cpu()),
68 "h_min": float(torch.cat(h_vals).min().cpu())}
69 return out
70
71
72def train_idea(ds, cfg, seed):
73 seed_all(seed)
74 net = PositiveISSRNN(out_dim=1)
75 net.train()
76 # The ISS penalty is the intervention, hence a local loop is appropriate.
77 opt = torch.optim.Adam(net.parameters(), lr=cfg["lr"], weight_decay=cfg["weight_decay"])
78 x, y = ds["xtr"], ds["ytr"]
79 for _ in range(EPOCHS):
80 perm = torch.randperm(len(x))
81 for i in range(0, len(x), BATCH):
82 ix = perm[i:i+BATCH]; pred = net(x[ix])
83 task = ((pred-y[ix])**2).mean()
84 # Approximate nominal c=0.02, bounded-rate gain k=0.02.
85 # Penalize excessive positive free-energy derivative on observed states.
86 residual = torch.relu(net.last_vdot_tensor + 0.02 * net.last_v_tensor)
87 loss = task + cfg["lambda_iss"] * residual * residual
88 opt.zero_grad(); loss.backward(); torch.nn.utils.clip_grad_norm_(net.parameters(), 5.0); opt.step()
89 net.eval()
90 with torch.no_grad():
91 metric = float(((net(ds["xte"])-ds["yte"])**2).mean())
92 net(ds["xte"], return_stats=True)
93 return metric, net.last_signature
94
95
96def base_fn(cfg):
97 def run(seed):
98 seed_all(seed); ds = get_dataset("dynamics", seed, n_train=400, n_test=200)
99 net = make_model("rnn_small", ds["input_shape"], ds["out_dim"])
100 _, metric, _ = train_model(net, ds, epochs=EPOCHS, lr=cfg["lr"], batch=BATCH, weight_decay=cfg["weight_decay"], log=lambda *_: None)
101 return metric
102 return run
103
104
105def idea_fn(cfg, collect=False):
106 records = []
107 def run(seed):
108 ds = get_dataset("dynamics", seed, n_train=400, n_test=200)
109 metric, sig = train_idea(ds, cfg, seed)
110 if collect: records.append(sig)
111 return metric
112 return run, records
113
114
115def main():
116 # Baseline sweep uses exactly the same six settings' lr/wd union.
117 base = sweep_baseline(base_fn, GRID, seeds=SEEDS)
118 # Idea sweep over three settings; all corresponding lr/wd values are in base grid.
119 idea_trials = []
120 for cfg in IDEA_GRID:
121 fn, _ = idea_fn(cfg)
122 r = evaluate(fn, SEEDS)
123 idea_trials.append({"cfg": cfg, "mean": r["mean"], "result": r})
124 best = min(idea_trials, key=lambda z: z["mean"])
125 idea_res = best["result"]
126 # Re-run chosen idea settings for signature on all paired trained models.
127 fn, sigs = idea_fn(best["cfg"], collect=True)
128 idea_res = evaluate(fn, SEEDS)
129 baseline_full = base["full"]
130 # Trained baseline behavior at best configuration, for identical test inputs.
131 base_sigs = []
132 for seed in SEEDS:
133 seed_all(seed); ds = get_dataset("dynamics", seed, n_train=400, n_test=200)
134 net = make_model("rnn_small", ds["input_shape"], ds["out_dim"])
135 net, _, _ = train_model(net, ds, epochs=EPOCHS, lr=base["best_cfg"]["lr"], batch=BATCH, weight_decay=base["best_cfg"]["weight_decay"], log=lambda *_: None)
136 with torch.no_grad():
137 dev = next(net.parameters()).device
138 out = net(ds["xte"].to(dev))
139 base_sigs.append({"pred_std": float(out.std()), "pred_abs_mean": float(out.abs().mean())})
140 sig = {"prediction": {"claim": "bounded positive state should remain finite and ISS energy should not grow", "idea_mean_V": float(np.mean([s["V"] for s in sigs])), "idea_mean_Vdot": float(np.mean([s["Vdot"] for s in sigs])), "idea_h_min": float(np.min([s["h_min"] for s in sigs])), "idea_h_max": float(np.max([s["h_max"] for s in sigs])), "baseline_pred_abs_mean": float(np.mean([s["pred_abs_mean"] for s in base_sigs]))}, "confirmed": bool(all(s["h_min"] > 0 and np.isfinite(s["V"]) and np.isfinite(s["Vdot"]) for s in sigs))}
141 report = make_report("dynamics", "rnn_small", base, idea_res, sig)
142 report["idea_sweep"] = [{"cfg": z["cfg"], "mean": z["mean"]} for z in idea_trials]
143 Path("bench_report.json").write_text(json.dumps(report, indent=2))
144 print(json.dumps(report, indent=2))
145
146if __name__ == "__main__": main()