Symplectic Hamiltonian Optimizer / experiment.py
Mechanism confirmed, baseline not beaten
1import json, math, os, random, time
2from pathlib import Path
3import numpy as np
4import torch
5from torch import nn
6from sklearn.datasets import load_digits
7from sklearn.model_selection import train_test_split
8from sklearn.preprocessing import StandardScaler
9
10SEED = 645
11OUT = Path('results.json')
12
13def seed_all(seed=SEED):
14 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
15 if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
16
17
18def harmonic_check():
19 # H(q,p)=1/2(q^2+p^2), exact leapfrog versus explicit Euler.
20 h = 0.5; n = 2000
21 q0, p0 = 1.0, 0.0
22 ql, pl, qe, pe = q0, p0, q0, p0
23 hl, he = [], []
24 for _ in range(n):
25 hl.append(0.5*(ql*ql+pl*pl)); he.append(0.5*(qe*qe+pe*pe))
26 pl -= 0.5*h*ql
27 ql += h*pl
28 pl -= 0.5*h*ql
29 # explicit Euler for dq/dt=p, dp/dt=-q
30 qe, pe = qe + h*pe, pe - h*qe
31 hl, he = np.asarray(hl), np.asarray(he)
32 return {
33 'step': h, 'steps': n,
34 'leapfrog_energy_start': float(hl[0]),
35 'leapfrog_energy_end': float(hl[-1]),
36 'leapfrog_energy_abs_drift': float(abs(hl[-1]-hl[0])),
37 'leapfrog_energy_range': float(hl.max()-hl.min()),
38 'euler_energy_start': float(he[0]),
39 'euler_energy_end': float(he[-1]),
40 'euler_energy_growth_factor': float(he[-1]/he[0]),
41 'euler_energy_range': float(he.max()-he.min()),
42 'claim_observed': bool((hl.max()-hl.min()) < 0.01 and he[-1] > 100*he[0])
43 }
44
45class MLP(nn.Module):
46 def __init__(self):
47 super().__init__()
48 self.net = nn.Sequential(nn.Linear(64, 64), nn.Tanh(), nn.Linear(64, 10))
49 def forward(self, x): return self.net(x)
50
51def make_data():
52 x, y = load_digits(return_X_y=True)
53 x = StandardScaler().fit_transform(x).astype('float32')
54 xtr, xva, ytr, yva = train_test_split(x, y, test_size=.25, random_state=SEED, stratify=y)
55 return (torch.tensor(xtr), torch.tensor(ytr, dtype=torch.long),
56 torch.tensor(xva), torch.tensor(yva, dtype=torch.long))
57
58def evaluate(model, xv, yv):
59 model.eval()
60 with torch.no_grad():
61 z = model(xv); loss = nn.functional.cross_entropy(z, yv).item()
62 acc = (z.argmax(1) == yv).float().mean().item()
63 model.train(); return loss, acc
64
65def batches(x, y, bs=128, seed=0):
66 g = torch.Generator().manual_seed(seed)
67 ix = torch.randperm(len(x), generator=g)
68 for s in range(0, len(x), bs):
69 j = ix[s:s+bs]; yield x[j], y[j]
70
71def flat_grads(model):
72 return torch.cat([p.grad.detach().reshape(-1) for p in model.parameters()])
73
74def flat_params(model):
75 return torch.cat([p.detach().reshape(-1) for p in model.parameters()])
76
77def train(kind, xtr, ytr, xva, yva, grad_budget=240):
78 # CPU is deliberately used for robust shared-machine reproducibility.
79 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
80 model = MLP().to(device); xtr=xtr.to(device); ytr=ytr.to(device); xva=xva.to(device); yva=yva.to(device)
81 if kind == 'sgd': opt = torch.optim.SGD(model.parameters(), lr=0.08, momentum=0.9)
82 elif kind == 'adamw': opt = torch.optim.AdamW(model.parameters(), lr=0.003, weight_decay=0.0)
83 params = list(model.parameters())
84 velocity = [torch.zeros_like(p) for p in params]
85 mass = 1.0; h = 0.08
86 losses=[]; grad_norms=[]; evals=0; step=0
87 t0=time.time()
88 while evals < grad_budget:
89 # Fixed deterministic cycling through reshuffled minibatches.
90 for xb, yb in batches(xtr, ytr, 128, SEED+step//100):
91 if evals >= grad_budget: break
92 if kind == 'leapfrog':
93 model.zero_grad(set_to_none=True); loss=nn.functional.cross_entropy(model(xb), yb); loss.backward()
94 g1=flat_grads(model); grad_norms.append(float(g1.norm().cpu()))
95 with torch.no_grad():
96 for p, v in zip(params, velocity): p.add_(h*v/mass); v.sub_(0.5*h*p.grad)
97 # Correct kick-drift ordering requires first kick before drift; undo/restate compactly:
98 # position was advanced using old v, so apply kick ordering from saved old state below is avoided
99 # by using the equivalent implementation in the next branch's explicit state correction.
100 # This branch is replaced immediately by a clean two-kick update using old parameters.
101 raise RuntimeError('internal leapfrog branch should not execute')
102 else:
103 opt.zero_grad(set_to_none=True); loss=nn.functional.cross_entropy(model(xb), yb); loss.backward()
104 grad_norms.append(float(flat_grads(model).norm().detach().cpu())); opt.step(); evals += 1; step += 1
105 if step % 40 == 0: losses.append((step, evals, float(loss.detach().cpu())))
106 return {}
107
108def train_leapfrog(xtr,ytr,xva,yva,grad_budget=240):
109 device=torch.device('cuda' if torch.cuda.is_available() else 'cpu')
110 model=MLP().to(device); xtr=xtr.to(device); ytr=ytr.to(device); xva=xva.to(device); yva=yva.to(device)
111 params=list(model.parameters()); vel=[torch.zeros_like(p) for p in params]; h=.08; evals=0; step=0; gn=[]; trace=[]; t0=time.time()
112 while evals+1 <= grad_budget:
113 for xb,yb in batches(xtr,ytr,128,SEED+step//100):
114 if evals+1>grad_budget: break
115 model.zero_grad(set_to_none=True); loss=nn.functional.cross_entropy(model(xb),yb); loss.backward(); gn.append(float(flat_grads(model).norm().cpu())); evals+=1
116 with torch.no_grad():
117 for p,v in zip(params,vel): v.sub_(.5*h*p.grad); p.add_(h*v)
118 model.zero_grad(set_to_none=True); loss2=nn.functional.cross_entropy(model(xb),yb); loss2.backward(); gn.append(float(flat_grads(model).norm().cpu())); evals+=1
119 with torch.no_grad():
120 for p,v in zip(params,vel): v.sub_(.5*h*p.grad)
121 step+=1
122 if step%20==0: trace.append((step,evals,float(loss2.detach().cpu())))
123 if evals>=grad_budget: break
124 vl,va=evaluate(model,xva,yva)
125 return {'steps':step,'gradient_evals':evals,'final_train_loss':float(loss2.detach().cpu()),'val_loss':vl,'val_accuracy':va,'mean_grad_norm':float(np.mean(gn)),'time_sec':time.time()-t0,'trace':trace}
126
127def train_standard(kind,xtr,ytr,xva,yva,grad_budget=240):
128 device=torch.device('cuda' if torch.cuda.is_available() else 'cpu'); model=MLP().to(device)
129 xtr=xtr.to(device);ytr=ytr.to(device);xva=xva.to(device);yva=yva.to(device); params=list(model.parameters())
130 opt=torch.optim.SGD(params,lr=.08,momentum=.9) if kind=='sgd' else torch.optim.AdamW(params,lr=.003,weight_decay=0.)
131 evals=step=0;gn=[];trace=[];t0=time.time()
132 while evals<grad_budget:
133 for xb,yb in batches(xtr,ytr,128,SEED+step//100):
134 if evals>=grad_budget: break
135 opt.zero_grad(set_to_none=True); loss=nn.functional.cross_entropy(model(xb),yb); loss.backward(); gn.append(float(flat_grads(model).norm().cpu())); opt.step(); evals+=1;step+=1
136 if step%40==0:trace.append((step,evals,float(loss.detach().cpu())))
137 vl,va=evaluate(model,xva,yva)
138 return {'steps':step,'gradient_evals':evals,'final_train_loss':float(loss.detach().cpu()),'val_loss':vl,'val_accuracy':va,'mean_grad_norm':float(np.mean(gn)),'time_sec':time.time()-t0,'trace':trace}
139
140def main():
141 seed_all(); toy=harmonic_check(); xtr,ytr,xva,yva=make_data()
142 results={'harmonic_check':toy,'device':str(torch.device('cuda' if torch.cuda.is_available() else 'cpu'))}
143 for k in ['sgd','adamw','leapfrog']:
144 seed_all(); results[k]=train_leapfrog(xtr,ytr,xva,yva) if k=='leapfrog' else train_standard(k,xtr,ytr,xva,yva)
145 OUT.write_text(json.dumps(results,indent=2)); print(json.dumps(results,indent=2))
146if __name__=='__main__': main()