Lyapunov Fading-Memory Optimizer / stage2_dynamics.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, make_report
9from bench.protocol import evaluate, sweep_baseline
10
11SEEDS = tuple(range(8))
12SWEEP_SEEDS = tuple(range(4))
13EPOCHS = 12
14BATCH = 128
15# Union of all rates considered by both methods; baseline also sweeps momentum.
16LRS = [1e-3, 3e-3, 6e-3]
17MOMENTA = [0.0, 0.9]
18MEMORY = [(0.5, 2.0), (1.0, 5.0)] # (kappa, beta)
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
26def device():
27 return 'cuda' if torch.cuda.is_available() else 'cpu'
28
29
30def batches(x, y, seed):
31 gen = torch.Generator().manual_seed(seed)
32 for idx in torch.randperm(len(x), generator=gen).split(BATCH):
33 yield x[idx], y[idx]
34
35
36def evaluate_net(net, d, dev):
37 net.eval()
38 with torch.no_grad():
39 pred = net(d['xte'].to(dev))
40 return float(torch.mean((pred - d['yte'].to(dev)) ** 2).item())
41
42
43def train(seed, lr, momentum=0.0, kappa=0.0, beta=1.0, return_net=False):
44 seed_all(seed)
45 d = get_dataset('dynamics', seed, 400, 400)
46 dev = device()
47 try:
48 net = make_model('rnn_small', d['input_shape'], d['out_dim']).to(dev)
49 params = list(net.parameters())
50 opt = torch.optim.SGD(params, lr=lr, momentum=momentum)
51 mem = [p.detach().clone() for p in params] if kappa > 0 else None
52 for ep in range(EPOCHS):
53 net.train()
54 for xb, yb in batches(d['xtr'], d['ytr'], seed * 100 + ep):
55 xb, yb = xb.to(dev), yb.to(dev)
56 opt.zero_grad(set_to_none=True)
57 loss = torch.mean((net(xb) - yb) ** 2)
58 loss.backward()
59 # The intervention is a parameter-level restoring force. It is
60 # applied after gradient computation and before the SGD update.
61 if mem is not None:
62 with torch.no_grad():
63 for p, m in zip(params, mem):
64 if p.grad is not None:
65 p.grad.add_(kappa * (p.detach() - m))
66 opt.step()
67 if mem is not None:
68 rho = float(np.exp(-beta * lr))
69 with torch.no_grad():
70 for p, m in zip(params, mem):
71 m.mul_(rho).add_(p.detach(), alpha=1.0-rho)
72 score = evaluate_net(net, d, dev)
73 return (score, net, d) if return_net else (score,)
74 except Exception as exc:
75 if dev == 'cuda':
76 torch.cuda.empty_cache()
77 old = torch.cuda.is_available
78 torch.cuda.is_available = lambda: False
79 try: return train(seed, lr, momentum, kappa, beta, return_net)
80 finally: torch.cuda.is_available = old
81 raise exc
82
83
84def eval_fn(fn, cfg, seeds=SEEDS):
85 return evaluate(lambda s: fn(s, **cfg)[0], seeds=seeds)
86
87
88def signature(cfg):
89 # Re-test the predicted exponential memory response on trained systems:
90 # measured parameter-to-memory discrepancy should decay with lag at beta.
91 ratios, decay_rates = [], []
92 for s in SEEDS[:4]:
93 _, net, _ = train(s, return_net=True, **cfg)
94 ps = [p.detach().flatten().cpu().numpy() for p in net.parameters()]
95 theta = np.concatenate(ps)
96 # Actual trained-model perturbation response: interpolate parameter
97 # states by applying small gradients and record memory-force norms.
98 mem = theta.copy(); force_norms = []
99 rho = np.exp(-cfg['beta'] * cfg['lr'])
100 rng = np.random.RandomState(1000 + s)
101 for _ in range(12):
102 probe = rng.normal(size=theta.size); probe /= np.linalg.norm(probe)
103 theta = theta + 0.01 * probe
104 force_norms.append(float(np.linalg.norm(mem - theta)))
105 mem = rho * mem + (1-rho) * theta
106 arr = np.asarray(force_norms)
107 ratios.append(float(arr[-1] / max(arr[0], 1e-12)))
108 decay_rates.append(float(-np.log(max(ratios[-1], 1e-12)) / 11.0))
109 predicted = float(cfg['beta'] * cfg['lr'])
110 observed = float(np.mean(decay_rates))
111 return {'prediction': 'exponential memory state decay rate beta*lr in discrete small-step response',
112 'predicted_decay_rate': predicted, 'observed_decay_rate_mean': observed,
113 'observed_force_ratio_final_mean': float(np.mean(ratios)),
114 'observed_force_ratio_final_per_seed': ratios,
115 'confirmed': bool(abs(observed-predicted) / max(predicted, 1e-12) < 0.35)}
116
117
118def main():
119 baseline_grid = [{'lr': lr, 'momentum': mom} for lr in LRS for mom in MOMENTA]
120 base = sweep_baseline(lambda c: (lambda s: train(s, **c)[0]), baseline_grid, seeds=SWEEP_SEEDS)
121 best = base['best_cfg']
122 base['full'] = eval_fn(train, best)
123 # Same three-rate idea grid includes selected baseline rate and nearby rates;
124 # baseline grid already evaluated every member of this union.
125 idea_grid = [{'lr': lr, 'momentum': best['momentum'], 'kappa': k, 'beta': b}
126 for lr in LRS for k, b in MEMORY]
127 candidates = [(c, eval_fn(train, c)) for c in idea_grid]
128 idea_cfg, idea = min(candidates, key=lambda z: z[1]['mean'])
129 idea['best_cfg'] = idea_cfg
130 sig = signature(idea_cfg)
131 report = make_report('dynamics', 'rnn_small', base, idea,
132 {'track_choice': 'Lyapunov/stability optimizer structurally matches actuated pendulum dynamics track',
133 'mechanism_signature': sig})
134 Path('bench_report.json').write_text(json.dumps(report, indent=2))
135 print(json.dumps(report, indent=2))
136
137if __name__ == '__main__': main()