Dissipative drift parameterization / bench_experiment.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  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()