Online Taylor Residual World Model / stage2_bench.py

✓✓ Beats tuned baseline

Raw ⬇ ZIP
  1import sys, json, random, time
  2from pathlib import Path
  3import numpy as np
  4import torch
  5
  6sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  7from bench import get_dataset, make_model, train_model, sweep_baseline, make_report
  8
  9SEEDS = tuple(range(8)); NTR, NTE = 400, 200; EPOCHS, BATCH = 4, 128
 10LRS = [1e-3, 3e-3, 6e-3]
 11
 12
 13def seed_all(seed):
 14    random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
 15    if torch.cuda.is_available():
 16        try: torch.cuda.manual_seed_all(seed)
 17        except Exception: pass
 18
 19
 20def aggregate(vals):
 21    return {'mean': float(np.mean(vals)), 'std': float(np.std(vals)),
 22            'per_seed': [float(x) for x in vals], 'n': len(vals)}
 23
 24
 25def monomials(z, degree=1):
 26    z = np.asarray(z, dtype=np.float64).reshape(-1)
 27    return np.r_[1., z]
 28
 29
 30class OnlineTaylorRLS:
 31    def __init__(self, in_dim, out_dim, lam=.98, p0=10.):
 32        self.lam = lam; self.d = in_dim + 1
 33        self.W = np.zeros((out_dim, self.d), dtype=np.float64)
 34        self.P = np.eye(self.d, dtype=np.float64) * p0
 35
 36    def update(self, z, target):
 37        phi = monomials(z); Pphi = self.P @ phi
 38        K = Pphi / (self.lam + phi @ Pphi)
 39        err = np.asarray(target) - self.W @ phi
 40        self.W += np.outer(err, K)
 41        self.P = (self.P - np.outer(K, phi @ self.P)) / self.lam
 42        self.P = (self.P + self.P.T) * .5
 43        return err
 44
 45    def predict(self, z): return self.W @ monomials(z)
 46
 47
 48def baseline_one(cfg, seed, return_model=False):
 49    seed_all(seed + 10000)
 50    ds = get_dataset('dynamics', seed, n_train=NTR, n_test=NTE)
 51    model = make_model('rnn_small', ds['input_shape'], 1)
 52    model, metric, hist = train_model(model, ds, epochs=EPOCHS, lr=cfg['lr'],
 53                                      batch=BATCH, weight_decay=cfg.get('wd', 0.),
 54                                      log=lambda *_: None)
 55    return (metric, model, ds) if return_model else metric
 56
 57
 58def idea_one(cfg, seed, return_details=False):
 59    # Train exactly the same rnn_small neural prior as baseline, then adapt only
 60    # the residual coefficients online on the observed training transitions.
 61    seed_all(seed + 10000)
 62    ds = get_dataset('dynamics', seed, n_train=NTR, n_test=NTE)
 63    model = make_model('rnn_small', ds['input_shape'], 1)
 64    model, _, _ = train_model(model, ds, epochs=EPOCHS, lr=cfg['lr'],
 65                              batch=BATCH, weight_decay=cfg.get('wd', 0.),
 66                              log=lambda *_: None)
 67    device = next(model.parameters()).device
 68    xtr, ytr = ds['xtr'].to(device), ds['ytr'].to(device)
 69    xte, yte = ds['xte'].to(device), ds['yte'].to(device)
 70    # dynamics windows are [batch,time,features]; predict each observed next step
 71    rls = OnlineTaylorRLS(3, ytr.shape[-1], cfg['lam'], cfg['p0'])
 72    with torch.no_grad():
 73        pred_tr = model(xtr).detach().cpu().numpy()
 74        pred_te = model(xte).detach().cpu().numpy()
 75    # Use the current window's last state and last action as local coordinates.
 76    xt = xtr.detach().cpu().numpy(); yt = ytr.detach().cpu().numpy()
 77    for i in range(len(xt)):
 78        state = xt[i].reshape(-1, 3)[-1]
 79        z = state
 80        rls.update(z, yt[i] - pred_tr[i])
 81    xe = xte.detach().cpu().numpy(); corr = []
 82    for i in range(len(xe)):
 83        state = xe[i].reshape(-1, 3)[-1]
 84        corr.append(rls.predict(state))
 85    corrected = pred_te + np.asarray(corr)
 86    metric = float(np.mean((corrected - yte.detach().cpu().numpy()) ** 2))
 87    if return_details: return metric, model, ds, rls, pred_te, corrected
 88    return metric
 89
 90
 91def permutation_pvalue(diffs, n=20000, seed=991):
 92    rng = np.random.default_rng(seed); diffs = np.asarray(diffs); obs = diffs.mean()
 93    signs = rng.choice([-1., 1.], size=(n, len(diffs)))
 94    return float((np.mean(signs * diffs, axis=1) <= obs + 1e-15).mean())
 95
 96
 97def main():
 98    t = time.time()
 99    # Union parity: all idea learning rates are included in baseline sweep.
100    grid = [{'lr': lr, 'wd': 0.0} for lr in LRS]
101    sw = sweep_baseline(lambda cfg: (lambda seed: baseline_one(cfg, seed)), grid, seeds=SEEDS)
102    base = {'best_cfg': sw['best_cfg'], 'sweep': sw['sweep'], 'full': sw['full']}
103    best_lr = sw['best_cfg']['lr']
104    idea_grid = [{'lr': best_lr, 'wd': 0., 'lam': lam, 'p0': 10.}
105                 for lam in (.90, .98, .995)]
106    idea_runs = []
107    for cfg in idea_grid:
108        vals = [idea_one(cfg, s) for s in SEEDS]
109        idea_runs.append({'cfg': cfg, 'result': aggregate(vals)})
110    best = min(idea_runs, key=lambda r: r['result']['mean'])
111    # Paired comparison uses the same seed-wise baseline configuration and idea.
112    bvals = [baseline_one(best['cfg'], s) for s in SEEDS]
113    ivals = best['result']['per_seed']; diffs = np.asarray(ivals) - np.asarray(bvals)
114    comparison = {'delta_mean': float(diffs.mean()),
115                  'idea_wins': int((diffs < 0).sum()), 'n_pairs': 8,
116                  'per_seed_diffs': diffs.tolist(),
117                  'p_value': permutation_pvalue(diffs),
118                  'mde': float(1.96 * diffs.std(ddof=1) / np.sqrt(8)),
119                  'mde_rel_pct': float(100 * (1.96 * diffs.std(ddof=1) / np.sqrt(8)) / np.mean(bvals)),
120                  'verdict': 'idea better (significant)' if diffs.mean() < 0 and permutation_pvalue(diffs) < .05 else 'no significant win',
121                  'system_worked': bool(diffs.mean() < 0 and permutation_pvalue(diffs) < .05)}
122    # Signature is measured on trained models, testing the actual local correction.
123    metric, model, ds, rls, pte, corrected = idea_one(best['cfg'], 0, True)
124    yte = ds['yte'].detach().cpu().numpy(); residual_before = np.mean((pte-yte)**2)
125    residual_after = np.mean((corrected-yte)**2)
126    sig = {'trained_model_test_mse_before_rls': float(residual_before),
127           'trained_model_test_mse_after_rls': float(residual_after),
128           'relative_correction': float((residual_before-residual_after)/max(residual_before,1e-12)),
129           'effective_memory_approx': float(1/(1-best['cfg']['lam'])),
130           'confirmed': bool(residual_after < residual_before)}
131    rep = make_report('dynamics', 'rnn_small', base, best['result'], extra=sig)
132    rep['idea_sweep'] = idea_runs; rep['comparison'] = comparison; rep['runtime_sec'] = time.time()-t
133    Path('bench_report.json').write_text(json.dumps(rep, indent=2)); print(json.dumps(rep, indent=2))
134
135if __name__ == '__main__': main()