Dissipative drift parameterization / bench_experiment.py
Mechanism confirmed, baseline not beaten
1import sys, json, random
2from pathlib import Path
3import numpy as np
4import torch
5from torch import 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
10SEEDS = tuple(range(8))
11SWEEP_SEEDS = tuple(range(4))
12LR_GRID = [1e-3, 3e-3, 1e-2]
13WD_GRID = [0.0, 1e-4]
14MU = 0.30
15LF = 0.20
16EPOCHS = 30
17
18
19def seed_all(seed):
20 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
21 if torch.cuda.is_available():
22 try: torch.cuda.manual_seed_all(seed)
23 except Exception: pass
24
25
26class DissipativeRNN(nn.Module):
27 """Same rnn_small core; only output parameterization is changed.
28
29 The last action u is the action-coordinate analogue of a diffusion state.
30 The residual uses a state/history-dependent center and direct tanh(u), so
31 |f(u)-f(u')| <= LF |u-u'| exactly (for fixed observed history).
32 """
33 def __init__(self, input_shape, out_dim, mu=MU, lf=LF):
34 super().__init__()
35 self.core = make_model('rnn_small', input_shape, out_dim)
36 self.mu, self.lf = float(mu), float(lf)
37
38 def forward(self, x):
39 # core prediction supplies a learned history/state-dependent center;
40 # remove the current action from the core input before forming it.
41 seq = x.view(x.shape[0], -1, 3)
42 u = seq[:, -1:, 2:3]
43 masked = seq.clone(); masked[:, -1, 2] = 0.0
44 center = self.core(masked.reshape(x.shape[0], -1))
45 return -self.mu * u.reshape(x.shape[0], 1) + self.lf * torch.tanh(center + u.reshape(x.shape[0], 1))
46
47
48def train_one(kind, seed, lr, wd=0.0, return_model=False):
49 seed_all(seed)
50 ds = get_dataset('dynamics', seed, n_train=4000, n_test=1000)
51 if kind == 'baseline':
52 model = make_model('rnn_small', ds['input_shape'], ds['out_dim'])
53 else:
54 model = DissipativeRNN(ds['input_shape'], ds['out_dim'])
55 trained, metric, _ = train_model(model, ds, epochs=EPOCHS, lr=lr, batch=128,
56 weight_decay=wd, log=lambda *_: None)
57 if return_model:
58 return float(metric), trained, ds
59 return float(metric)
60
61
62def baseline_factory(cfg):
63 return lambda seed: train_one('baseline', seed, cfg['lr'], cfg['weight_decay'])
64
65
66def idea_eval(cfg):
67 return evaluate(lambda seed: train_one('idea', seed, cfg['lr'], cfg['weight_decay']), SEEDS)
68
69
70def math_check():
71 # Independent numerical sanity check of the exact scalar inequality.
72 rng = np.random.RandomState(123)
73 u = rng.randn(200000); up = rng.randn(200000); c = rng.randn(200000)
74 f = LF*np.tanh(c+u); fp = LF*np.tanh(c+up)
75 b = -MU*u+f; bp = -MU*up+fp
76 da = u-up; ratio = (b-bp)*da/(da*da+1e-12)
77 lip = np.abs(f-fp)/(np.abs(da)+1e-12)
78 return {'mu': MU, 'Lf': LF, 'theoretical_bound': -(MU-LF),
79 'max_residual_fd_lipschitz': float(lip.max()),
80 'max_one_sided_ratio': float(ratio.max()),
81 'violation_fraction': float(np.mean(ratio > -(MU-LF)+1e-10)),
82 'confirmed': bool(lip.max() <= LF + 1e-8 and ratio.max() <= -(MU-LF)+1e-8)}
83
84
85def mechanism_signature():
86 # Measure trained-model behavior, not an analytical identity.
87 mb, _, ds = train_one('baseline', 0, 3e-3, 0.0, True)
88 mi, model, ds = train_one('idea', 0, 3e-3, 0.0, True)
89 model.eval(); dev = next(model.parameters()).device
90 x = ds['xte'][:1000].clone().to(dev)
91 xp = x.clone(); xp[:, -1] += 0.5
92 with torch.no_grad():
93 y = model(x); yp = model(xp)
94 # Recover learned residual f=b+mu*u and compare finite differences.
95 u = x[:, -1:]; up = xp[:, -1:]
96 r = y + MU*u; rp = yp + MU*up
97 db = (y-yp).squeeze(1); da = (u-up).squeeze(1)
98 rr = ((db*da)/(da*da+1e-12)).cpu().numpy()
99 ll = (torch.abs(r-rp)/(torch.abs(u-up)+1e-12)).cpu().numpy().ravel()
100 # baseline model is separately trained so measure it explicitly
101 _, bm, _ = train_one('baseline', 0, 3e-3, 0.0, True)
102 bm.eval()
103 with torch.no_grad():
104 yb=bm(x); ybp=bm(xp)
105 rb=((yb-ybp).squeeze(1)*da/(da*da+1e-12)).cpu().numpy()
106 return {'mu': MU, 'Lf': LF, 'idea_observed_residual_fd_lipschitz_max': float(ll.max()),
107 'idea_observed_one_sided_ratio_max': float(rr.max()),
108 'baseline_observed_one_sided_ratio_max': float(rb.max()),
109 'predicted_bound': -(MU-LF),
110 'confirmed': bool(ll.max() <= LF+1e-5 and rr.max() <= -(MU-LF)+1e-5)}
111
112
113def main():
114 math = math_check()
115 grid = [{'lr': lr, 'weight_decay': wd} for lr in LR_GRID for wd in WD_GRID]
116 base = sweep_baseline(baseline_factory, grid, seeds=SWEEP_SEEDS)
117 # Union parity: all idea lr settings were included in baseline sweep;
118 # idea evaluates the best baseline lr plus two nearby grid settings.
119 best_lr = base['best_cfg']['lr']; ordered = sorted(LR_GRID, key=lambda z: abs(np.log(z/best_lr)))
120 idea_grid = [{'lr': z, 'weight_decay': base['best_cfg']['weight_decay']} for z in ordered[:3]]
121 idea_runs = [(cfg, idea_eval(cfg)) for cfg in idea_grid]
122 best_cfg, idea = min(idea_runs, key=lambda p: p[1]['mean'])
123 sig = mechanism_signature()
124 report = make_report('dynamics', 'rnn_small', base, idea,
125 {'mechanism_signature': sig,
126 'idea_grid': [{'cfg': c, 'mean': r['mean']} for c,r in idea_runs],
127 'math_sanity_check': math,
128 'custom_track': None})
129 report['idea_best_cfg'] = best_cfg
130 report['protocol_note'] = 'Dynamics is structurally matched: controlled pendulum windows contain (theta, omega, action), and the intervention acts on the action coordinate while retaining the shared GRU core.'
131 Path('bench_report.json').write_text(json.dumps(report, indent=2))
132 print(json.dumps(report, indent=2))
133
134if __name__ == '__main__': main()