Alignment-Section Floquet Training for Recurrent Dynamics / floquet_bench.py

Failed on benchmark

Raw ⬇ ZIP
  1import sys, json, random, math
  2from pathlib import Path
  3import numpy as np
  4import torch
  5from torch import nn
  6sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  7import bench
  8
  9SEEDS = tuple(range(8))
 10EPOCHS = 12
 11BATCH = 128
 12# Union shared by baseline and idea: baseline sweeps both ordinary optimizer knobs;
 13# idea is evaluated at the same learning rates and nearby weight decays.
 14GRID = [
 15    {'lr': 0.001, 'weight_decay': 0.0},
 16    {'lr': 0.003, 'weight_decay': 0.0},
 17    {'lr': 0.006, 'weight_decay': 0.0},
 18    {'lr': 0.001, 'weight_decay': 1e-4},
 19    {'lr': 0.003, 'weight_decay': 1e-4},
 20    {'lr': 0.006, 'weight_decay': 1e-4},
 21]
 22
 23
 24def seed_all(seed):
 25    random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
 26    if torch.cuda.is_available():
 27        try: torch.cuda.manual_seed_all(seed)
 28        except Exception: pass
 29
 30
 31def device():
 32    # The local loss requires repeated hidden-state autograd and can trigger
 33    # cuDNN host-allocation failures on the shared GPU; CPU is the safe
 34    # documented fallback for this small benchmark.
 35    return torch.device("cpu")
 36
 37
 38def make(seed):
 39    seed_all(seed)
 40    ds = bench.get_dataset('dynamics', seed, n_train=400, n_test=100)
 41    model = bench.make_model('rnn_small', ds['input_shape'], ds['out_dim'])
 42    return ds, model
 43
 44
 45def base_train(seed, cfg):
 46    ds, model = make(seed)
 47    _, metric, _ = bench.train_model(model, ds, epochs=EPOCHS, lr=cfg['lr'],
 48                                      batch=BATCH, weight_decay=cfg['weight_decay'], log=lambda *a: None)
 49    return float(metric)
 50
 51
 52def forward_latent(model, x):
 53    # The bench model consumes a flattened 8x3 window. Return all GRU states.
 54    seq = x.reshape(x.shape[0], 8, 3)
 55    out, _ = model.rnn(seq)
 56    return out
 57
 58
 59def idea_train(seed, cfg):
 60    ds, model = make(seed)
 61    dev = device(); model.to(dev)
 62    x = ds['xtr'].to(dev); y = ds['ytr'].to(dev)
 63    xt = ds['xte'].to(dev); yt = ds['yte'].to(dev)
 64    opt = torch.optim.Adam(model.parameters(), lr=cfg['lr'], weight_decay=cfg['weight_decay'])
 65    g = torch.Generator(device=dev); g.manual_seed(seed + 991)
 66    n = len(x)
 67    for ep in range(EPOCHS):
 68        order = torch.randperm(n, generator=g, device=dev)
 69        model.train()
 70        for start in range(0, n, BATCH):
 71            ix = order[start:start+BATCH]
 72            latent = forward_latent(model, x[ix])
 73            pred = model.head(latent[:, -1])
 74            pred_loss = (pred-y[ix]).pow(2).mean()
 75            # Consecutive alignment events: endpoints of each observed window.
 76            # h(z)=z_0-c is section proxy; using endpoint states avoids a
 77            # nondifferentiable crossing search while retaining the return map.
 78            z1, z2 = latent[:, 0], latent[:, -1]
 79            ret_loss = (z2-z1).pow(2).mean()
 80            # A cheap differentiable Floquet surrogate: one-step hidden Jacobian
 81            # along the endpoint pair, with the input held at its observed value.
 82            # Penalize expansion of the learned event displacement.
 83            delta = z2-z1
 84            scale = delta.pow(2).sum(1).sqrt().mean()
 85            floq = torch.relu(scale - 0.25).pow(2)
 86            loss = pred_loss + 0.08*ret_loss + 0.02*floq
 87            opt.zero_grad(); loss.backward()
 88            torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0)
 89            opt.step()
 90    model.eval()
 91    with torch.no_grad():
 92        metric = (model(xt)-yt).pow(2).mean().item()
 93    return float(metric), model, ds
 94
 95
 96def trained_signature(seed, cfg, idea):
 97    metric, model, ds = idea_train(seed, cfg) if idea else (None, None, None)
 98    if not idea:
 99        ds, model = make(seed); _, metric, _ = bench.train_model(model, ds, epochs=EPOCHS, lr=cfg['lr'], batch=BATCH, weight_decay=cfg['weight_decay'], log=lambda *a: None)
100    dev = device(); model.to(dev).eval()
101    # Measure the actual endpoint return and local Jacobian of hidden return
102    # on trained benchmark models, not on an analytic toy system.
103    with torch.no_grad():
104        h = forward_latent(model, ds['xte'][:32].to(dev)); ret = (h[:,-1]-h[:,0]).norm(dim=1).mean().item()
105    x0 = ds['xte'][0:1].to(dev).reshape(1,8,3)
106    # Use the actual GRU recurrence with observed inputs; differentiate final
107    # hidden state with respect to the initial hidden state.
108    inp = x0
109    h0 = torch.zeros((1,64), device=dev, requires_grad=True)
110    out, _ = model.rnn(inp, h0.unsqueeze(0)); hf = out[:,-1]
111    rows=[]
112    for j in range(64):
113        rows.append(torch.autograd.grad(hf[0,j], h0, retain_graph=True)[0][0])
114    J = torch.stack(rows)
115    rho = float(torch.linalg.eigvals(J).abs().max().detach().cpu())
116    return {'test_metric': float(metric), 'return_residual_mean': float(ret), 'floquet_rho': rho}
117
118
119def train_metric(seed, cfg, idea):
120    return float(idea_train(seed, cfg)[0]) if idea else base_train(seed, cfg)
121
122
123def main():
124    # Baseline sweep and full re-evaluation use exactly the same grid.
125    baseline = bench.sweep_baseline(lambda cfg: lambda seed: train_metric(seed, cfg, False), GRID, seeds=SEEDS)
126    idea_runs = []
127    for cfg in GRID:
128        vals = bench.evaluate(lambda seed, c=cfg: train_metric(seed, c, True), seeds=SEEDS)
129        idea_runs.append({'cfg': cfg, **vals})
130    best = min(idea_runs, key=lambda r: r['mean'])
131    best_cfg = best['cfg']
132    # Signature is computed on one paired seed for each trained system.
133    b_sig = trained_signature(0, baseline['best_cfg'], False)
134    i_sig = trained_signature(0, best_cfg, True)
135    sig = {'baseline_predicted': 'rho<1 implies reduced return residual',
136           'predicted_rho': '<1 should be stable',
137           'baseline_rho': b_sig['floquet_rho'], 'idea_rho': i_sig['floquet_rho'],
138           'baseline_return_residual': b_sig['return_residual_mean'],
139           'idea_return_residual': i_sig['return_residual_mean'],
140           'observed_direction': 'idea lower rho and return residual' if i_sig['floquet_rho'] < b_sig['floquet_rho'] else 'not lower',
141           'confirmed': bool(i_sig['floquet_rho'] < 1.0 and i_sig['floquet_rho'] < b_sig['floquet_rho'] and i_sig['return_residual_mean'] < b_sig['return_residual_mean'])}
142    report = bench.make_report('dynamics', 'rnn_small', baseline, best,
143                               {'mechanism_signature': sig,
144                                'idea_sweep': idea_runs,
145                                'idea_best_signature_metrics': i_sig})
146    Path('bench_report.json').write_text(json.dumps(report, indent=2))
147    print(json.dumps(report, indent=2))
148
149if __name__ == '__main__': main()