Weakly Normally Hyperbolic Cyclic Optimizer / bench_cyclic_dynamics.py
Failed on benchmark
1import os, sys, json, math, random
2from pathlib import Path
3import numpy as np
4
5sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
6import torch
7import torch.nn as nn
8from bench import get_dataset, make_model, train_model, sweep_baseline, evaluate, make_report
9
10ROOT = Path(__file__).resolve().parent
11SEEDS = tuple(range(8))
12# Common learning-rate union is used on both sides.
13LR_GRID = [0.0015, 0.0030, 0.0045]
14EPOCHS = 18
15BATCH = 64
16WEIGHT_DECAY = 0.0
17PERIOD = 8
18AMP_GRID = [0.15, 0.30, 0.45]
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 baseline_one(cfg, seed, retain=False):
31 seed_all(seed)
32 ds = get_dataset('dynamics', seed, n_train=400, n_test=100)
33 model = make_model('rnn_small', ds['input_shape'], ds['out_dim'])
34 if not retain:
35 _, metric, _ = train_model(
36 model, ds, epochs=EPOCHS, lr=cfg['lr'], batch=BATCH,
37 weight_decay=WEIGHT_DECAY, log=lambda *a, **k: None)
38 if metric is None:
39 raise RuntimeError('baseline training failed')
40 return float(metric)
41 return model, ds
42
43
44def cyclic_one(cfg, seed, return_model=False):
45 """Adam with phase-periodic step size and first-moment coefficient."""
46 seed_all(seed)
47 ds = get_dataset('dynamics', seed, n_train=400, n_test=100)
48 dev = device()
49 try:
50 model = make_model('rnn_small', ds['input_shape'], ds['out_dim']).to(dev)
51 x, y = ds['xtr'].to(dev), ds['ytr'].to(dev)
52 xe, ye = ds['xte'].to(dev), ds['yte'].to(dev)
53 lossf = nn.MSELoss()
54 opt = torch.optim.Adam(model.parameters(), lr=cfg['lr'], betas=(0.9, 0.999), weight_decay=WEIGHT_DECAY)
55 n = x.shape[0]; step = 0
56 for ep in range(EPOCHS):
57 g = torch.Generator(device='cpu'); g.manual_seed(seed + 1009 * ep)
58 order = torch.randperm(n, generator=g).to(dev)
59 for st in range(0, n, BATCH):
60 phase = 2 * math.pi * (step % PERIOD) / PERIOD
61 eta = cfg['lr'] * (1.0 + cfg['amp'] * math.sin(phase))
62 beta1 = min(0.98, max(0.50, 0.82 + 0.10 * math.cos(phase)))
63 for group in opt.param_groups:
64 group['lr'] = eta; group['betas'] = (beta1, 0.999)
65 ix = order[st:st + BATCH]
66 opt.zero_grad(set_to_none=True)
67 loss = lossf(model(x[ix]), y[ix]); loss.backward()
68 torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0)
69 opt.step(); step += 1
70 with torch.no_grad(): metric = float(lossf(model(xe), ye).detach().cpu())
71 return (model, ds, step) if return_model else metric
72 except Exception:
73 if dev == 'cuda':
74 torch.cuda.empty_cache()
75 return cyclic_one_cpu(cfg, seed, return_model)
76 raise
77
78
79def cyclic_one_cpu(cfg, seed, return_model=False):
80 seed_all(seed)
81 ds = get_dataset('dynamics', seed, n_train=400, n_test=100)
82 model = make_model('rnn_small', ds['input_shape'], ds['out_dim'])
83 x, y, xe, ye = ds['xtr'], ds['ytr'], ds['xte'], ds['yte']
84 lossf = nn.MSELoss(); opt = torch.optim.Adam(model.parameters(), lr=cfg['lr'], betas=(.9, .999), weight_decay=WEIGHT_DECAY)
85 step = 0
86 for ep in range(EPOCHS):
87 gen = torch.Generator(); gen.manual_seed(seed + 1009 * ep)
88 order = torch.randperm(len(x), generator=gen)
89 for st in range(0, len(x), BATCH):
90 phase = 2 * math.pi * (step % PERIOD) / PERIOD
91 for q in opt.param_groups:
92 q['lr'] = cfg['lr'] * (1 + cfg['amp'] * math.sin(phase))
93 q['betas'] = (min(.98, max(.5, .82 + .10 * math.cos(phase))), .999)
94 opt.zero_grad(set_to_none=True)
95 loss = lossf(model(x[order[st:st + BATCH]]), y[order[st:st + BATCH]])
96 loss.backward(); torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0); opt.step(); step += 1
97 with torch.no_grad(): metric = float(lossf(model(xe), ye))
98 return (model, ds, step) if return_model else metric
99
100
101def mechanism_signature(cfg, seed=0):
102 """Measure one-period local contraction using a trained rnn_small model."""
103 model, ds, steps = cyclic_one(cfg, seed, return_model=True)
104 model.eval(); params = [p for p in model.parameters() if p.requires_grad]
105 def clone_state(): return [p.detach().clone() for p in params]
106 base = clone_state(); eps = 1e-5
107 with torch.no_grad(): params[0].add_(eps)
108 pert = clone_state()
109 lossf = nn.MSELoss(); probe_dev = next(model.parameters()).device
110 x, y = ds['xtr'].to(probe_dev), ds['ytr'].to(probe_dev)
111 def run(vec):
112 with torch.no_grad():
113 for p, v in zip(params, vec): p.copy_(v)
114 local = torch.optim.SGD(params, lr=cfg['lr'])
115 for k in range(PERIOD):
116 local.zero_grad(); lossf(model(x), y).backward(); local.step()
117 return clone_state()
118 out0, out1 = run(base), run(pert)
119 d0 = math.sqrt(sum(float(((a - b) ** 2).sum()) for a, b in zip(pert, base)))
120 d1 = math.sqrt(sum(float(((a - b) ** 2).sum()) for a, b in zip(out1, out0)))
121 observed = d1 / max(d0, 1e-30)
122 mus = [.82 + .10 * math.cos(2 * math.pi * k / PERIOD) for k in range(PERIOD)]
123 predicted = float(np.prod(mus))
124 return {'period': PERIOD, 'predicted_transverse_multiplier': predicted,
125 'observed_trained_model_multiplier': observed,
126 'ratio_observed_to_predicted': observed / max(abs(predicted), 1e-30),
127 'confirmed': bool(abs(math.log(max(observed, 1e-30)) - math.log(max(abs(predicted), 1e-30))) < 1.0),
128 'seed': seed, 'probe': 'full_batch_gradient_on_trained_rnn'}
129
130
131def main():
132 grid = [{'lr': lr, 'amp': amp} for lr in LR_GRID for amp in AMP_GRID]
133 base = sweep_baseline(lambda c: (lambda s: baseline_one(c, s)), grid, seeds=(0, 1, 2, 3))
134 idea_runs = []
135 for c in grid:
136 idea_runs.append({'cfg': c, 'result': evaluate(lambda s, c=c: cyclic_one(c, s), seeds=SEEDS)})
137 best = min(idea_runs, key=lambda z: z['result']['mean'])
138 sig = mechanism_signature(best['cfg'], 0)
139 report = make_report('dynamics', 'rnn_small', base, best['result'], {
140 **sig, 'idea_grid': idea_runs,
141 'protocol_note': '8 paired seeds; baseline and idea share lr/amp union; 400/100 samples'
142 })
143 (ROOT / 'bench_report.json').write_text(json.dumps(report, indent=2))
144 print(json.dumps(report, indent=2))
145
146
147if __name__ == '__main__': main()