Thermodynamic Confidence Controller for SGD / thermo_controller.py
Mechanism failed
1"""MVP for a thermodynamic-confidence controller for minibatch SGD.
2Run: /home/maxwelhelp/main/bin/python3 thermo_controller.py
3"""
4import math, random, json
5import numpy as np
6import torch
7from sklearn.datasets import load_digits
8from sklearn.model_selection import train_test_split
9
10SEED = 3166
11
12def seed_all(seed=SEED):
13 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
14 if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
15
16# h from Eq. (9), evaluated stably for small u.
17def h(u):
18 u = np.asarray(u, dtype=float)
19 return 1.0 + u - np.sqrt(1.0 + 2.0*u)
20
21def rate(a, C, D):
22 """I(a)=(C/D^2)h(Da/C), with the D=0 equilibrium limit."""
23 if D <= 1e-12: return a*a/(2.0*C)
24 return float(C/(D*D) * h(D*a/C))
25
26def radius(T, delta, C, D, N=1.0):
27 """Smallest a for 2N exp(-T I(a)) <= delta, by bisection."""
28 target = math.log(2*N/delta)/T
29 if target <= 0: return 0.0
30 lo, hi = 0., max(math.sqrt(2*C*target), C*target + D*target, 1e-8)
31 while rate(hi,C,D) < target: hi *= 2
32 for _ in range(70):
33 mid=(lo+hi)/2
34 if rate(mid,C,D) >= target: hi=mid
35 else: lo=mid
36 return hi
37
38def autocorr_time(x):
39 x=np.asarray(x,float); x=x-x.mean()
40 if len(x)<4 or np.dot(x,x)<1e-14: return 1.0
41 ac=np.correlate(x,x,mode='full')[len(x)-1:]/np.dot(x,x)
42 # Initial-positive-sequence estimate, conservatively clipped.
43 s=1.0
44 for z in ac[1:]:
45 if z <= 0: break
46 s += 2*z
47 return float(np.clip(s,1.,len(x)/2))
48
49class CurrentController:
50 def __init__(self, window=32, delta=.05, batch=32, max_batch=128):
51 self.window=window; self.delta=delta; self.batch=batch; self.max_batch=max_batch
52 self.values=[]; self.radii=[]; self.means=[]; self.good=0
53 def observe(self, j):
54 self.values.append(float(j))
55 if len(self.values) >= self.window:
56 x=np.array(self.values[-self.window:]); mean=float(x.mean())
57 var=float(max(x.var(ddof=1),1e-10)); tau=autocorr_time(x)
58 # Empirical sub-gamma proxy: effective variance inflated by correlation.
59 C=2*var*tau; D=max(1e-8, 0.25*math.sqrt(var)*max(tau-1,0))
60 r=radius(self.window,self.delta,C,D)
61 self.radii.append(r); self.means.append(mean)
62 # The prescribed relative trigger, with a tiny noise-scale floor for m ~= 0.
63 if r > .25*max(abs(mean), .1*math.sqrt(var)) and self.batch < self.max_batch:
64 self.batch=min(self.max_batch,self.batch*2); self.good=0
65 elif r < .1*max(abs(mean), .1*math.sqrt(var)):
66 self.good += 1
67 if self.good >= 4: self.good=0 # permit LR increase in a full optimizer; not used here
68 else: self.good=0
69 return r, mean, tau
70 return None
71
72def math_check():
73 # Exact bound proxy: radius should fall ~ T^-1/2 in Gaussian regime and rise with tau/variance.
74 delta=.05; Ts=np.array([32,64,128,256,512])
75 rs=np.array([radius(int(T),delta,C=2.,D=0.) for T in Ts])
76 slope=np.polyfit(np.log(Ts),np.log(rs),1)[0]
77 r_var=radius(128,delta,2.,0.)
78 r_corr=radius(128,delta,8.,0.)
79 return {'T':Ts.tolist(),'radii':rs.tolist(),'loglog_slope':float(slope),
80 'radius_var1':float(r_var),'radius_var4':float(r_corr),
81 'scaling_ok':bool(-.65 < slope < -.35),'variance_ok':bool(r_corr>r_var)}
82
83class Net(torch.nn.Module):
84 def __init__(self):
85 super().__init__(); self.net=torch.nn.Sequential(torch.nn.Linear(64,64),torch.nn.ReLU(),torch.nn.Linear(64,10))
86 def forward(self,x): return self.net(x)
87
88def train(controlled, device, steps=300):
89 seed_all(SEED)
90 d=load_digits(); X=d.data.astype('float32')/16.; y=d.target.astype('int64')
91 Xtr,Xte,ytr,yte=train_test_split(X,y,test_size=.25,random_state=SEED,stratify=y)
92 xt=torch.tensor(Xtr,device=device); yt=torch.tensor(ytr,device=device)
93 xval=torch.tensor(Xte,device=device); yval=torch.tensor(yte,device=device)
94 model=Net().to(device); opt=torch.optim.SGD(model.parameters(),lr=.08)
95 ctrl=CurrentController() if controlled else None
96 v=torch.randn(sum(p.numel() for p in model.parameters()),device=device); v/=v.norm()
97 rng=np.random.default_rng(SEED); losses=[]; batch_history=[]; n=len(xt)
98 for step in range(steps):
99 bs=ctrl.batch if ctrl else 32
100 batch_history.append(bs)
101 ii=torch.tensor(rng.integers(0,n,size=bs),device=device)
102 opt.zero_grad(set_to_none=True); out=model(xt[ii]); loss=torch.nn.functional.cross_entropy(out,yt[ii]); loss.backward()
103 g=torch.cat([p.grad.detach().flatten() for p in model.parameters()]); j=float(torch.dot(v,g).detach().cpu())
104 if ctrl: ctrl.observe(j)
105 opt.step(); losses.append(float(loss.detach().cpu()))
106 with torch.no_grad(): acc=float((model(xval).argmax(1)==yval).float().mean().cpu())
107 return {'final_train_loss':float(np.mean(losses[-20:])),'test_accuracy':acc,
108 'mean_batch':float(np.mean(batch_history)),
109 'total_examples':int(sum(batch_history)),
110 'batch_values':sorted(set(batch_history)),
111 'batches_changed':int(len(set(batch_history))),
112 'radius_count':len(ctrl.radii) if ctrl else 0,
113 'radius_first_last':([float(ctrl.radii[0]),float(ctrl.radii[-1])] if ctrl and ctrl.radii else [])}
114
115def main():
116 try: device=torch.device('cuda' if torch.cuda.is_available() else 'cpu')
117 except Exception: device=torch.device('cpu')
118 try:
119 check=math_check(); base=train(False,device,steps=1102); idea=train(True,device)
120 except Exception as e:
121 if device.type=='cuda':
122 device=torch.device('cpu'); check=math_check(); base=train(False,device,steps=1102); idea=train(True,device)
123 else: raise
124 out={'device':str(device),'math_check':check,'baseline':base,'idea':idea}
125 print(json.dumps(out,indent=2))
126if __name__=='__main__': main()