Resonance-Aware Stochastic RNN Control / bench_resonance.py
Beats tuned baseline
1import sys, json, math
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, evaluate, sweep_baseline, make_report
9
10EPOCHS = 12
11NTR, NTE = 400, 200
12DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
13
14
15def fit_mode(c):
16 c = np.asarray(c, dtype=float)
17 best = (1e99, .2, .05)
18 k = np.arange(len(c), dtype=float)
19 for r in np.linspace(.20, .995, 80):
20 for w in np.linspace(.05, 1.45, 90):
21 A = (r ** k)[:, None] * np.c_[np.cos(w*k), np.sin(w*k)]
22 ab = np.linalg.lstsq(A, c, rcond=None)[0]
23 err = float(np.mean((A @ ab - c) ** 2))
24 if err < best[0]: best = (err, r, w)
25 return float(best[1]), float(best[2])
26
27
28def math_sanity(seed=0):
29 rng = np.random.default_rng(seed)
30 r, w, T = .93, .42, 5000
31 R = np.array([[np.cos(w), -np.sin(w)], [np.sin(w), np.cos(w)]])
32 z = np.zeros((T, 2))
33 for t in range(1, T): z[t] = r * R @ z[t-1] + .15 * rng.normal(size=2)
34 mu = z[:, 0].mean()
35 c = np.array([np.mean((z[:-k,0]-mu)*(z[k:,0]-mu)) if k else np.var(z[:,0])
36 for k in range(31)])
37 c /= c[0]
38 fr, fw = fit_mode(c)
39 return {'true_radius': r, 'fitted_radius': fr, 'true_omega': w,
40 'fitted_omega': fw, 'radius_abs_error': abs(fr-r),
41 'frequency_abs_error': abs(fw-w), 'pass': abs(fr-r)<.03 and abs(fw-w)<.04}
42
43
44def seed_all(seed):
45 np.random.seed(seed); torch.manual_seed(seed)
46 if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
47
48
49def recurrent_radius(model):
50 W = model.rnn.weight_hh_l0
51 return torch.linalg.matrix_norm(W, ord=2)
52
53
54def hidden_observations(model, x, noise_std=.03):
55 seq = x.view(x.shape[0], -1, 3)
56 h = torch.zeros(1, x.shape[0], model.rnn.hidden_size, device=x.device)
57 hs = []
58 # Use the GRU's actual trained recurrent dynamics, with injected noise.
59 old_cudnn = torch.backends.cudnn.enabled
60 try:
61 torch.backends.cudnn.enabled = False
62 for t in range(seq.shape[1]):
63 inp = seq[:, t:t+1] + noise_std * torch.randn_like(seq[:, t:t+1])
64 out, h = model.rnn(inp, h)
65 hs.append(h[-1])
66 return torch.stack(hs, dim=1)
67 finally:
68 torch.backends.cudnn.enabled = old_cudnn
69
70
71def resonance_penalty(model, x, r_max=.90, beta=.02):
72 # Radius term is differentiable; mode estimates are measured periodically
73 # for the signature and stop-gradient by design as prescribed by the idea.
74 radius = recurrent_radius(model)
75 return beta * torch.relu(radius - r_max) ** 2, float(radius.detach())
76
77
78def train_baseline(seed, cfg):
79 seed_all(seed)
80 d = get_dataset('dynamics', seed, NTR, NTE)
81 model = make_model('rnn_small', d['input_shape'], d['out_dim'])
82 opt = torch.optim.Adam(model.parameters(), lr=cfg['lr'], weight_decay=cfg['weight_decay'])
83 lossf = nn.MSELoss()
84 dev = torch.device(DEVICE)
85 try:
86 model.to(dev); xtr,ytr=d['xtr'].to(dev),d['ytr'].to(dev)
87 for _ in range(EPOCHS):
88 p=torch.randperm(len(xtr),device=dev)
89 for i in range(0,len(xtr),128):
90 ix=p[i:i+128]; loss=lossf(model(xtr[ix]),ytr[ix])
91 opt.zero_grad(); loss.backward(); opt.step()
92 with torch.no_grad():
93 return float(lossf(model(d['xte'].to(dev)), d['yte'].to(dev)))
94 except RuntimeError:
95 model.cpu(); opt=torch.optim.Adam(model.parameters(),lr=cfg['lr'],weight_decay=cfg['weight_decay'])
96 xtr,ytr=d['xtr'],d['ytr']
97 for _ in range(EPOCHS):
98 p=torch.randperm(len(xtr))
99 for i in range(0,len(xtr),128):
100 ix=p[i:i+128]; loss=lossf(model(xtr[ix]),ytr[ix]); opt.zero_grad(); loss.backward(); opt.step()
101 with torch.no_grad(): return float(lossf(model(d['xte']),d['yte']))
102
103
104def train_idea(seed, cfg, collect=False):
105 seed_all(seed)
106 d=get_dataset('dynamics',seed,NTR,NTE)
107 model=make_model('rnn_small',d['input_shape'],d['out_dim'])
108 lossf=nn.MSELoss(); dev=torch.device(DEVICE)
109 try: model.to(dev); xtr,ytr=d['xtr'].to(dev),d['ytr'].to(dev)
110 except RuntimeError: dev=torch.device('cpu'); model.cpu(); xtr,ytr=d['xtr'],d['ytr']
111 opt=torch.optim.Adam(model.parameters(),lr=cfg['lr'],weight_decay=cfg['weight_decay'])
112 radii=[]; mode_rows=[]
113 for ep in range(EPOCHS):
114 p=torch.randperm(len(xtr),device=dev)
115 for i in range(0,len(xtr),128):
116 ix=p[i:i+128]; pred=model(xtr[ix]); task=lossf(pred,ytr[ix])
117 pen,rad=resonance_penalty(model,xtr[ix],cfg['r_max'],cfg['beta'])
118 loss=task+pen; opt.zero_grad(); loss.backward(); opt.step(); radii.append(rad)
119 if ep in (0,EPOCHS-1):
120 with torch.no_grad():
121 hs=hidden_observations(model,xtr[:128])
122 obs=hs[:,:,0].detach().cpu().numpy(); obs-=obs.mean(axis=1,keepdims=True)
123 c=np.array([(obs[:,:-k]*obs[:,k:]).mean() if k else (obs*obs).mean() for k in range(6)])
124 c/=max(c[0],1e-8); fr,fw=fit_mode(c); mode_rows.append({'epoch':ep,'fitted_radius':fr,'fitted_omega':fw})
125 with torch.no_grad(): metric=float(lossf(model(d['xte'].to(dev)),d['yte'].to(dev)))
126 if collect: return metric, {'radius_mean':float(np.mean(radii)),'mode_rows':mode_rows,'model':model}
127 return metric
128
129
130def main():
131 lr_grid=[1e-3,3e-3,6e-3]
132 # Union parity: both methods are evaluated at every lr and shared WD values.
133 grid=[{'lr':lr,'weight_decay':wd} for lr in lr_grid for wd in (0.0,1e-4)]
134 base=sweep_baseline(lambda cfg: lambda seed: train_baseline(seed,cfg),grid)
135 idea_cfgs=[dict(base['best_cfg'], r_max=.90, beta=.02), dict(lr=3e-3, weight_decay=base['best_cfg']['weight_decay'], r_max=.90, beta=.02), dict(lr=6e-3, weight_decay=base['best_cfg']['weight_decay'], r_max=.90, beta=.02)]
136 # All idea settings are already in the baseline union grid.
137 idea_trials=[]
138 for cfg in idea_cfgs:
139 rr=evaluate(lambda seed: train_idea(seed,cfg))
140 idea_trials.append({'cfg':cfg,'result':rr})
141 idea=min(idea_trials,key=lambda z:z['result']['mean'])
142 sig_metric,sig=train_idea(0,idea['cfg'],collect=True)
143 signature={'predicted_vs_observed':{'target_radius':idea['cfg']['r_max'],'observed_radius_mean':sig['radius_mean'],
144 'fitted_mode_rows':sig['mode_rows']},'confirmed':bool(sig['radius_mean'] <= idea['cfg']['r_max']+.05)}
145 rep=make_report('dynamics','rnn_small',base,idea['result'],signature)
146 rep['idea_trials']=idea_trials; rep['math_sanity']=math_sanity()
147 Path('bench_report.json').write_text(json.dumps(rep,indent=2))
148 print(json.dumps(rep,indent=2))
149
150if __name__=='__main__': main()