Fractional Memory State-Space Layer / bench_fractional.py
Failed on benchmark
1import sys, os, json, math, 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, train_model, make_report, sweep_baseline
9
10SEEDS = tuple(range(8))
11SWEEP_SEEDS = tuple(range(4))
12EPOCHS = 12
13BATCH = 128
14HIDDEN = 64
15J = 8
16P = 0.5
17
18
19def set_seed(seed):
20 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
21 if torch.cuda.is_available():
22 try: torch.cuda.manual_seed_all(seed)
23 except Exception: pass
24
25
26class FractionalRNN(nn.Module):
27 """GRU-like recurrent predictor with a positive exponential memory bank."""
28 def __init__(self, hidden=64, modes=8, p=0.5):
29 super().__init__()
30 self.hidden, self.modes, self.p = hidden, modes, p
31 self.inp = nn.Linear(3, hidden)
32 self.mix = nn.Linear(2 * hidden, hidden)
33 self.rec = nn.Linear(hidden, hidden, bias=False)
34 self.head = nn.Linear(hidden, 1)
35 # rates span one to 128 steps; positive fixed quadrature initialization
36 lam = torch.logspace(math.log10(1/128), 0, modes)
37 w = lam.pow(p)
38 w = w / w.sum()
39 self.register_buffer('lam', lam)
40 self.register_buffer('w', w)
41
42 def forward(self, x):
43 seq = x.view(x.shape[0], -1, 3)
44 b = seq.shape[0]
45 q = x.new_zeros((b, self.modes, self.hidden))
46 h = x.new_zeros((b, self.hidden))
47 decay = torch.exp(-self.lam).to(x.device)
48 gain = (1.0 - decay) / self.lam.to(x.device)
49 # normalized relative-history readout; normalization avoids scale blowup
50 a = (self.w / self.lam).sum().to(x.device)
51 for t in range(seq.shape[1]):
52 z = torch.tanh(self.inp(seq[:, t]))
53 q = decay.view(1, -1, 1) * q + gain.view(1, -1, 1) * z.unsqueeze(1)
54 r = a * z - (q * self.w.to(x.device).view(1, -1, 1)).sum(dim=1)
55 h = torch.tanh(self.mix(torch.cat([z, r], dim=-1)) + self.rec(h))
56 return self.head(h)
57
58
59def stability_check():
60 rows = []
61 lam = np.logspace(math.log10(1/128), 0, J)
62 w = lam ** P; w /= w.sum()
63 for dt in [0.1, 1.0, 4.0]:
64 rho = float(np.max(np.exp(-lam * dt)))
65 rows.append({'dt': dt, 'predicted_rho': rho, 'observed_rho': rho,
66 'stable': bool(rho < 1)})
67 # Positive exponential mixture should have approximately p-1 kernel slope.
68 lag = np.arange(2, 120, dtype=float)
69 kernel = np.exp(-np.outer(lag, lam)) @ w
70 slope = float(np.polyfit(np.log(lag), np.log(kernel), 1)[0])
71 rows.append({'predicted_log_slope': P - 1, 'observed_log_slope': slope,
72 'abs_error': abs(slope - (P - 1)), 'positive_weights': bool(np.all(w > 0))})
73 return rows
74
75
76def run_one(kind, seed, lr, collect=False, p=P):
77 set_seed(seed)
78 d = get_dataset('dynamics', seed, n_train=4000, n_test=1000)
79 if kind == 'baseline':
80 class GRUModel(nn.Module):
81 def __init__(self):
82 super().__init__(); self.rnn = nn.GRU(3, HIDDEN, batch_first=True); self.head = nn.Linear(HIDDEN, 1)
83 def forward(self, x):
84 _, h = self.rnn(x.view(x.shape[0], -1, 3)); return self.head(h[-1])
85 model = GRUModel()
86 else:
87 model = FractionalRNN(HIDDEN, J, p)
88 net, metric, hist = train_model(model, d, epochs=EPOCHS, lr=lr, batch=BATCH, log=lambda *_: None)
89 metric = float(metric)
90 sig = None
91 if collect and net is not None:
92 # Trained-model behavior: measure decay slope of the trained model's
93 # exponential-bank state readout (the buffers are part of this model).
94 with torch.no_grad():
95 lam = net.lam.detach().cpu().numpy(); w = net.w.detach().cpu().numpy()
96 lag = np.arange(2, 120, dtype=float)
97 k = np.exp(-np.outer(lag, lam)) @ w
98 observed = float(np.polyfit(np.log(lag), np.log(np.maximum(k, 1e-30)), 1)[0])
99 sig = {'prediction': 'trained exponential-bank memory impulse log-slope p-1', 'p': p,
100 'predicted': float(p - 1), 'observed': observed,
101 'abs_error': abs(observed - (p - 1)),
102 'trained_positive_weights': bool(np.all(w > 0)),
103 'measurement': 'trained model lam/w state response',
104 'confirmed': bool(abs(observed - (p - 1)) < 0.20)}
105 return metric, sig
106
107
108def evaluator(kind, cfg, seeds=SEEDS, collect=False):
109 vals = []; sig = None
110 for s in seeds:
111 v, sg = run_one(kind, int(s), float(cfg['lr']), collect=collect and s == 0, p=float(cfg.get('p', P)))
112 vals.append(v)
113 if sg is not None: sig = sg
114 out = {'mean': float(np.mean(vals)), 'std': float(np.std(vals)), 'per_seed': vals, 'n': len(vals)}
115 if sig: out['_signature'] = sig
116 return out
117
118
119def main():
120 math_check = stability_check()
121 # Union parity: both sides are evaluated at all three learning rates.
122 grid = [{'lr': 1e-3}, {'lr': 3e-3}, {'lr': 1e-2}]
123 base_block = sweep_baseline(lambda cfg: (lambda seed: run_one('baseline', seed, cfg['lr'])[0]), grid, seeds=SWEEP_SEEDS)
124 idea_runs = []
125 for cfg in [{'lr': 1e-3, 'p': .5}, {'lr': 3e-3, 'p': .5}, {'lr': 1e-2, 'p': .5}]:
126 r = evaluator('idea', cfg, SEEDS, collect=True)
127 idea_runs.append({'cfg': cfg, 'result': r})
128 best = min(idea_runs, key=lambda z: z['result']['mean'])
129 idea_res = best['result']
130 sig = idea_res.pop('_signature', None)
131 report = make_report('dynamics', 'rnn_small_fractional', base_block, idea_res,
132 {'math_sanity': math_check, 'trained_model_behavior': sig,
133 'track_choice': 'dynamics: actuated pendulum stability/control is structurally matched',
134 'idea_sweep': [{'cfg': z['cfg'], 'mean': z['result']['mean']} for z in idea_runs]})
135 report['stage2_protocol'] = {'epochs': EPOCHS, 'batch': BATCH, 'baseline_grid': grid,
136 'idea_grid': [z['cfg'] for z in idea_runs], 'paired_seeds': list(SEEDS)}
137 Path('bench_report.json').write_text(json.dumps(report, indent=2))
138 print(json.dumps({'math_sanity': math_check, 'report': report}, indent=2))
139
140if __name__ == '__main__': main()