Quotient-Fibre Mixing Network / bench_experiment.py
Failed on benchmark
1import json, random, sys
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, count_params
9
10SEEDS = tuple(range(8))
11# Shared union: every idea learning rate is also evaluated by baseline sweep.
12GRID = [{"lr": 1e-3}, {"lr": 3e-3}, {"lr": 1e-2}]
13EPOCHS = 12
14NTRAIN, NTEST = 400, 400
15
16
17def seed_all(seed):
18 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
19 if torch.cuda.is_available():
20 torch.cuda.manual_seed_all(seed)
21
22
23class QuotientFibreGRU(nn.Module):
24 """64-state triangular recurrent model: autonomous quotient and fibre."""
25 def __init__(self, out_dim=1, qdim=32, fdim=42):
26 super().__init__()
27 self.qdim, self.fdim = qdim, fdim
28 self.qcell = nn.GRUCell(3, qdim)
29 self.fcell = nn.GRUCell(3 + qdim, fdim)
30 self.head = nn.Linear(qdim + fdim, out_dim)
31
32 def forward(self, x, return_states=False):
33 seq = x.view(x.shape[0], -1, 3)
34 z = x.new_zeros((x.shape[0], self.qdim))
35 y = x.new_zeros((x.shape[0], self.fdim))
36 zs, ys = [], []
37 for t in range(seq.shape[1]):
38 u = seq[:, t]
39 z = self.qcell(u, z)
40 # Conditional fibre transition uses current quotient, never y to update z.
41 y = self.fcell(torch.cat([u, z], dim=-1), y)
42 zs.append(z); ys.append(y)
43 out = self.head(torch.cat([z, y], dim=-1))
44 if return_states:
45 return out, torch.stack(zs, 1), torch.stack(ys, 1)
46 return out
47
48
49def train_one(kind, seed, lr):
50 seed_all(seed)
51 ds = get_dataset("dynamics", seed, n_train=NTRAIN, n_test=NTEST)
52 if kind == "baseline":
53 model = make_model("rnn_small", ds["xtr"].shape[1:], ds["ytr"].shape[-1] if ds["ytr"].ndim > 1 else 1)
54 else:
55 model = QuotientFibreGRU(out_dim=1)
56 _, metric, _ = train_model(model, ds, epochs=EPOCHS, lr=lr, batch=128, log=lambda *_: None)
57 return float(metric)
58
59
60def model_for_signature(kind, seed, lr):
61 seed_all(seed)
62 ds = get_dataset("dynamics", seed, n_train=NTRAIN, n_test=NTEST)
63 model = (make_model("rnn_small", ds["xtr"].shape[1:], 1)
64 if kind == "baseline" else QuotientFibreGRU(1))
65 model, metric, _ = train_model(model, ds, epochs=EPOCHS, lr=lr, batch=128, log=lambda *_: None)
66 return model, ds, metric
67
68
69def fitted_rate(values, start=1):
70 a = np.maximum(np.asarray(values, dtype=float), 1e-8)
71 if len(a) <= start + 2: return float("nan")
72 slope = np.polyfit(np.arange(start, len(a)), np.log(a[start:]), 1)[0]
73 return float(np.exp(slope))
74
75
76def mechanism_signature(best_lr):
77 """Re-test max-rate and slow-branch invariance on trained benchmark models."""
78 model, ds, _ = model_for_signature("idea", 0, best_lr)
79 model.eval()
80 dev = next(model.parameters()).device
81 x = ds["xte"][:32].clone().to(dev)
82 # Empirical perturbation of initial hidden states, with identical observed inputs.
83 with torch.no_grad():
84 _, z1, y1 = model(x, True)
85 # Since the canonical model starts at zero, rerun with perturbed branch initial
86 # states through the same trained cells to measure each branch's decay.
87 seq = x.view(x.shape[0], -1, 3)
88 z = torch.zeros((len(x), model.qdim), device=dev); y = torch.zeros((len(x), model.fdim), device=dev)
89 z2 = torch.zeros_like(z); y2 = torch.zeros_like(y)
90 z2[:, 0] = 1.0; y2[:, 0] = 1.0
91 qnorm, fnorm, fullnorm = [], [], []
92 for t in range(seq.shape[1]):
93 u = seq[:, t]
94 z = model.qcell(u, z); y = model.fcell(torch.cat([u, z], -1), y)
95 z2 = model.qcell(u, z2); y2 = model.fcell(torch.cat([u, z2], -1), y2)
96 qnorm.append((z2-z).norm(dim=1).mean().item())
97 fnorm.append((y2-y).norm(dim=1).mean().item())
98 fullnorm.append(torch.sqrt((z2-z).pow(2).sum(1)+(y2-y).pow(2).sum(1)).mean().item())
99 aq, af, observed = fitted_rate(qnorm), fitted_rate(fnorm), fitted_rate(fullnorm)
100 pred = max(aq, af)
101 rel = abs(observed-pred) / max(abs(pred), 1e-8)
102 return {"predicted_max_rate": pred, "observed_full_rate": observed,
103 "observed_quotient_rate": aq, "observed_fibre_rate": af,
104 "relative_error": rel, "confirmed": bool(np.isfinite(rel) and rel <= .20),
105 "measurement": "trained quotient-fibre GRU hidden-state perturbation decay"}
106
107
108def main():
109 # Baseline sweep on four seeds, then full eight-seed reevaluation by harness.
110 base = sweep_baseline(lambda cfg: (lambda s: train_one("baseline", s, cfg["lr"])), GRID)
111 best_lr = float(base["best_cfg"]["lr"])
112 idea_cfgs = [best_lr] + [float(g["lr"]) for g in GRID if float(g["lr"]) != best_lr]
113 idea_runs = []
114 for lr in idea_cfgs:
115 r = evaluate(lambda s, lr=lr: train_one("idea", s, lr), seeds=SEEDS)
116 idea_runs.append({"lr": lr, "result": r})
117 idea_best = min(idea_runs, key=lambda q: q["result"]["mean"])
118 report = make_report("dynamics", "rnn_small", base, idea_best["result"], {
119 "predicted_vs_observed": mechanism_signature(idea_best["lr"]),
120 "idea_lr_sweep": idea_runs,
121 "parameter_counts": {"baseline": count_params(make_model("rnn_small", (24,), 1)), "idea": count_params(QuotientFibreGRU(1))},
122 "track_choice": "dynamics structurally matches stability/control and multi-step dynamical memory"
123 })
124 Path("bench_report.json").write_text(json.dumps(report, indent=2))
125 print(json.dumps(report, indent=2))
126
127if __name__ == "__main__":
128 main()