Risk-Fitted Shrinkage Gate / risk_gate_bench.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, train_model, evaluate, sweep_baseline, make_report
9
10SEED = 474
11KNOTS = np.linspace(0.0, 4.0, 9)
12
13class MonotoneSplineGate(nn.Module):
14 def __init__(self, knots=KNOTS):
15 super().__init__()
16 self.register_buffer('knots', torch.tensor(knots, dtype=torch.float32))
17 self.bias = nn.Parameter(torch.tensor(1.5))
18 self.raw_inc = nn.Parameter(torch.full((len(knots)-1,), -3.0))
19
20 def values(self):
21 inc = torch.nn.functional.softplus(self.raw_inc)
22 logits = self.bias + torch.cat([torch.zeros(1, device=inc.device), torch.cumsum(inc, 0)])
23 return torch.sigmoid(logits)
24
25 def parts(self, m):
26 k, v = self.knots.to(m.device), self.values()
27 idx = torch.bucketize(m.detach().reshape(-1), k[1:-1]).reshape(m.shape).clamp(0, len(k)-2)
28 frac = ((m-k[idx])/(k[idx+1]-k[idx]).clamp_min(1e-6)).clamp(0, 1)
29 s = v[idx]*(1-frac)+v[idx+1]*frac
30 slope = (v[idx+1]-v[idx])/(k[idx+1]-k[idx]).clamp_min(1e-6)
31 return torch.where(m >= k[-1], v[-1], s), torch.where(m >= k[-1], torch.zeros_like(slope), slope)
32
33 def forward(self, x):
34 return self.parts(x.abs())[0] * x
35
36 def divergence(self, x):
37 m = x.abs(); s, slope = self.parts(m)
38 return (s + m*slope).sum(dim=-1)
39
40 def sure(self, x):
41 d = x.shape[-1]
42 return ((self(x)-x).square().sum(dim=-1) + 2*self.divergence(x) - d).mean()
43
44class IdeaMLP(nn.Module):
45 def __init__(self, input_dim, out_dim, sure_weight=0.02, gate_lr=0.01):
46 super().__init__()
47 self.fc1 = nn.Linear(input_dim, 64)
48 self.gate = MonotoneSplineGate()
49 self.fc2 = nn.Linear(64, out_dim)
50 self.sure_weight = sure_weight
51 self.gate_lr = gate_lr
52
53 def forward(self, x, return_gate=False):
54 h = self.fc1(x)
55 z = self.gate(h)
56 out = self.fc2(z)
57 return (out, h, z) if return_gate else out
58
59def seed_all(seed):
60 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
61 if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
62
63def device():
64 if torch.cuda.is_available():
65 try:
66 torch.zeros(1, device='cuda')
67 return 'cuda'
68 except Exception:
69 pass
70 return 'cpu'
71
72def train_idea(ds, epochs=30, lr=0.003, sure_weight=0.02, gate_lr=0.01):
73 dev = device(); net = IdeaMLP(int(np.prod(ds['input_shape'])), ds['out_dim'], sure_weight, gate_lr).to(dev)
74 x, y = ds['xtr'].to(dev), ds['ytr'].to(dev)
75 opt = torch.optim.Adam([{'params':[p for n,p in net.named_parameters() if not n.startswith('gate.')], 'lr':lr}, {'params':net.gate.parameters(), 'lr':gate_lr}])
76 lossf = nn.MSELoss()
77 gen = torch.Generator(device='cpu').manual_seed(SEED)
78 net.train()
79 for ep in range(epochs):
80 ramp = min(1.0, max(0.0, (ep+1)/(0.05*epochs)))
81 perm = torch.randperm(len(x), generator=gen)
82 for ix in perm.split(128):
83 xb, yb = x[ix], y[ix]
84 opt.zero_grad(set_to_none=True)
85 pred, h, z = net(xb, True)
86 task = lossf(pred, yb)
87 # Standardize each hidden feature using detached batch statistics,
88 # then apply SURE to the standardized noisy coordinates.
89 mu = h.detach().mean(0, keepdim=True); sd = h.detach().std(0, keepdim=True).clamp_min(1e-3)
90 hn = (h-mu)/sd
91 sure = net.gate.sure(hn) / hn.shape[-1]
92 anchor = (1-net.gate.values()[-1]).square()
93 (task + ramp*sure_weight*sure + 0.001*anchor).backward()
94 torch.nn.utils.clip_grad_norm_(net.parameters(), 5.0)
95 opt.step()
96 net.eval()
97 with torch.no_grad(): metric = float(((net(ds['xte'].to(dev))-ds['yte'].to(dev))**2).mean().cpu())
98 return metric, net
99
100def math_check():
101 g=MonotoneSplineGate(); x=torch.tensor([[.31,1.17,2.63,3.71]], requires_grad=True)
102 analytic=float(g.divergence(x))
103 fd=0.0; eps=1e-4
104 for j in range(x.numel()):
105 xp=x.detach().clone(); xm=x.detach().clone(); xp.view(-1)[j]+=eps; xm.view(-1)[j]-=eps
106 fd += float((g(xp).view(-1)[j]-g(xm).view(-1)[j])/(2*eps))
107 return {'analytic_divergence':analytic,'finite_difference_divergence':fd,'abs_error':abs(analytic-fd),'confirmed':abs(analytic-fd)<0.02}
108
109def main():
110 track='tabular'; model='mlp_tiny'; epochs=18
111 # Union of all learning rates is evaluated for baseline and idea.
112 grid=[{'lr':lr,'weight_decay':wd} for lr in (0.0015,0.003,0.006) for wd in (0.0,1e-4)]
113 def base_fn(cfg):
114 def run(seed):
115 seed_all(seed); ds=get_dataset(track, seed, 400, 400)
116 _, metric, _=train_model(make_model(model,ds['input_shape'],ds['out_dim']), ds, epochs=epochs, lr=cfg['lr'], batch=128, weight_decay=cfg['weight_decay'], log=lambda *a,**k:None)
117 return metric
118 return run
119 base=sweep_baseline(base_fn, grid)
120 best=base['best_cfg']; idea_cfgs=[{'lr':best['lr'],'sure_weight':.01},{'lr':best['lr'],'sure_weight':.02},{'lr':best['lr'],'sure_weight':.04}]
121 # Baseline sweep already covers all lr values used by idea; idea uses the selected best lr.
122 def idea_run(seed, cfg):
123 seed_all(seed); ds=get_dataset(track, seed, 400, 400)
124 return train_idea(ds, epochs=epochs, lr=cfg['lr'], sure_weight=cfg['sure_weight'])[0]
125 idea_candidates=[]
126 for cfg in idea_cfgs:
127 r=evaluate(lambda s,cfg=cfg: idea_run(s,cfg), seeds=(0,1,2,3))
128 idea_candidates.append((r['mean'],cfg))
129 chosen=min(idea_candidates,key=lambda z:z[0])[1]
130 idea=evaluate(lambda s: idea_run(s,chosen), seeds=tuple(range(8)))
131 # Signature is measured on trained systems, not an algebraic toy identity.
132 sig=[]
133 for s in range(4):
134 seed_all(s); ds=get_dataset(track,s,400,400); _,net=train_idea(ds,epochs=epochs,lr=chosen['lr'],sure_weight=chosen['sure_weight'])
135 with torch.no_grad():
136 h=net.fc1(ds['xte'].to(device())); z=net.gate(h); sig.append(float((z.abs()>1e-3).float().mean().cpu()))
137 report=make_report(track,model,base,idea,extra={'mechanism_signature':{'quantity':'active hidden fraction after trained gate','predicted':'adaptive shrinkage should reduce active fraction','observed_mean':float(np.mean(sig)),'observed_per_seed':sig,'confirmed':bool(np.mean(sig)<0.99)}})
138 report['math_check']=math_check(); report['idea_sweep']= [{'cfg':c,'mean':m} for m,c in idea_candidates]
139 Path('bench_report.json').write_text(json.dumps(report,indent=2))
140 Path('math_check.json').write_text(json.dumps(report['math_check'],indent=2))
141 print(json.dumps(report,indent=2))
142
143if __name__=='__main__': main()