Symplectic Hamiltonian Optimizer / stage2_bench.py
Mechanism confirmed, baseline not beaten
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))
12EPOCHS = 8
13BATCH = 128
14# Union parity: every idea step size is also evaluated by Adam.
15LRS = [1e-3, 3e-3, 6e-3]
16WDS = [0.0, 1e-4]
17
18
19def seed_all(seed):
20 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
21 if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
22
23
24def dev():
25 return torch.device('cuda' if torch.cuda.is_available() else 'cpu')
26
27
28def batches(n, seed, device):
29 g = torch.Generator(device=device).manual_seed(seed)
30 ix = torch.randperm(n, generator=g, device=device)
31 for i in range(0, n, BATCH):
32 yield ix[i:i+BATCH]
33
34
35def train_adam(seed, lr, wd, capture=False):
36 seed_all(seed)
37 ds = get_dataset('tabular', seed, 400, 400)
38 device = dev()
39 try:
40 net = make_model('mlp_tiny', ds['input_shape'], ds['out_dim']).to(device)
41 x, y = ds['xtr'].to(device), ds['ytr'].to(device)
42 opt = torch.optim.Adam(net.parameters(), lr=lr, weight_decay=wd)
43 history = []
44 for ep in range(EPOCHS):
45 net.train()
46 for ix in batches(len(x), seed + 1009 * ep, device):
47 loss = nn.functional.mse_loss(net(x[ix]), y[ix])
48 opt.zero_grad(set_to_none=True)
49 loss.backward(); opt.step()
50 history.append(float(loss.detach().cpu()))
51 net.eval()
52 with torch.no_grad():
53 metric = float(nn.functional.mse_loss(net(ds['xte'].to(device)), ds['yte'].to(device)).cpu())
54 return metric, {'history': history, 'energy': []}
55 except RuntimeError:
56 if device.type == 'cuda':
57 torch.cuda.empty_cache()
58 return train_adam_cpu(seed, lr, wd, capture)
59 raise
60
61
62def train_adam_cpu(seed, lr, wd, capture=False):
63 seed_all(seed)
64 ds = get_dataset('tabular', seed, 400, 400)
65 net = make_model('mlp_tiny', ds['input_shape'], ds['out_dim'])
66 x, y = ds['xtr'], ds['ytr']
67 opt = torch.optim.Adam(net.parameters(), lr=lr, weight_decay=wd)
68 history = []
69 for ep in range(EPOCHS):
70 for ix in batches(len(x), seed + 1009 * ep, torch.device('cpu')):
71 loss = nn.functional.mse_loss(net(x[ix]), y[ix])
72 opt.zero_grad(set_to_none=True); loss.backward(); opt.step()
73 history.append(float(loss.detach()))
74 with torch.no_grad(): metric = float(nn.functional.mse_loss(net(ds['xte']), ds['yte']))
75 return metric, {'history': history, 'energy': []}
76
77
78def flat_params(params):
79 return torch.cat([p.detach().reshape(-1) for p in params])
80
81
82def train_leapfrog(seed, h, mass=1.0, capture=False):
83 seed_all(seed)
84 ds = get_dataset('tabular', seed, 400, 400)
85 device = dev()
86 try:
87 net = make_model('mlp_tiny', ds['input_shape'], ds['out_dim']).to(device)
88 x, y = ds['xtr'].to(device), ds['ytr'].to(device)
89 params = list(net.parameters())
90 mom = [torch.zeros_like(p) for p in params]
91 energies = []; history = []
92 # Two gradient evaluations per leapfrog step, matching Adam's budget by epochs.
93 for ep in range(EPOCHS):
94 net.train()
95 for bi, ix in enumerate(batches(len(x), seed + 1009 * ep, device)):
96 net.zero_grad(set_to_none=True)
97 loss = nn.functional.mse_loss(net(x[ix]), y[ix]); loss.backward()
98 g1 = [p.grad.detach().clone() for p in params]
99 with torch.no_grad():
100 for p, q, g in zip(params, mom, g1):
101 q.sub_(0.5 * h * g); p.add_(h * q / mass)
102 net.zero_grad(set_to_none=True)
103 loss2 = nn.functional.mse_loss(net(x[ix]), y[ix]); loss2.backward()
104 g2 = [p.grad.detach().clone() for p in params]
105 with torch.no_grad():
106 for q, g in zip(mom, g2): q.sub_(0.5 * h * g)
107 if capture and (bi == 0 or bi == len(list(batches(len(x), seed + 1009 * ep, device))) - 1):
108 with torch.no_grad():
109 kinetic = 0.5 * sum(float((q*q).sum().cpu()) / mass for q in mom)
110 potential = float(nn.functional.mse_loss(net(x), y).detach().cpu())
111 energies.append(potential + kinetic)
112 history.append(float(loss2.detach().cpu()))
113 net.eval()
114 with torch.no_grad(): metric = float(nn.functional.mse_loss(net(ds['xte'].to(device)), ds['yte'].to(device)).cpu())
115 return metric, {'history': history, 'energy': energies}
116 except RuntimeError:
117 if device.type == 'cuda':
118 torch.cuda.empty_cache()
119 old = globals()['dev']; globals()['dev'] = lambda: torch.device('cpu')
120 try: return train_leapfrog(seed, h, mass, capture)
121 finally: globals()['dev'] = old
122 raise
123
124
125def main():
126 # Baseline sweep includes all step sizes and both central Adam weight-decay values.
127 grid = [{'lr': lr, 'weight_decay': wd} for lr in LRS for wd in WDS]
128 base = sweep_baseline(lambda c: lambda s: train_adam(s, c['lr'], c['weight_decay'])[0], grid)
129 # Idea sweep: best baseline h plus two nearby settings; all are in baseline union.
130 idea_grid = [{'h': h, 'mass': 1.0} for h in LRS]
131 idea_cfg_results = []
132 for cfg in idea_grid:
133 r = evaluate(lambda s, c=cfg: train_leapfrog(s, c['h'], c['mass'])[0])
134 idea_cfg_results.append({'cfg': cfg, 'result': r})
135 best = min(idea_cfg_results, key=lambda z: z['result']['mean'])
136 idea = best['result']; cfg = best['cfg']
137
138 # Re-test the mechanism on trained systems: energy oscillation versus h.
139 hs = [0.001, 0.003, 0.006]
140 ranges = []
141 for h in hs:
142 vals = []
143 for s in range(4):
144 z = train_leapfrog(s, h, 1.0, capture=True)[1]['energy']
145 vals.append(max(z) - min(z) if z else float('nan'))
146 ranges.append(float(np.nanmean(vals)))
147 slope = float(np.polyfit(np.log(hs), np.log(np.maximum(ranges, 1e-30)), 1)[0])
148 sig = {'prediction': 'leapfrog trained-model energy oscillation scales approximately as h^2',
149 'step_sizes': hs, 'observed_energy_ranges': ranges,
150 'loglog_slope': slope, 'confirmed': bool(1.5 < slope < 2.5)}
151 report = make_report('tabular', 'mlp_tiny', base, idea, {
152 'track_choice': 'optimizer intervention structurally matches the tabular optimizer track',
153 'baseline_grid': grid, 'idea_grid': idea_grid, 'idea_best_cfg': cfg,
154 'mechanism_signature': sig})
155 report['idea_sweep'] = idea_cfg_results
156 Path('bench_report.json').write_text(json.dumps(report, indent=2))
157 print(json.dumps(report, indent=2))
158
159
160if __name__ == '__main__': main()