import json, os, sys import numpy as np import torch import torch.nn as nn sys.path.insert(0, '/home/maxwelhelp/all/math2nn') from bench import get_dataset, make_model, train_model, sweep_baseline, evaluate, make_report SEEDS = tuple(range(8)) EPOCHS = 12 BATCH = 128 LR_GRID = [0.001, 0.003, 0.01] RANK_GRID = [24, 32, 40] def gramian_basis(F, B, C, r, steps=80): n = F.shape[0] zp = np.zeros((n, 0)); zq = np.zeros((n, 0)) for _ in range(steps): zp = np.concatenate((F @ zp, B), axis=1) zq = np.concatenate((F.T @ zq, C.T), axis=1) up, sp, _ = np.linalg.svd(zp, full_matrices=False) uq, sq, _ = np.linalg.svd(zq, full_matrices=False) kp = max(1, min(n, int(np.sum(sp > sp[0] * 1e-8)))) kq = max(1, min(n, int(np.sum(sq > sq[0] * 1e-8)))) zp = up[:, :kp] * sp[:kp] zq = uq[:, :kq] * sq[:kq] u, s, vt = np.linalg.svd(zq.T @ zp, full_matrices=False) rr = min(r, len(s)) keep = s[:rr] > max(s[0] * 1e-10, 1e-12) rr = int(keep.sum()) vr = zp @ vt.T[:, :rr] / np.sqrt(s[:rr])[None, :] wr = zq @ u[:, :rr] / np.sqrt(s[:rr])[None, :] return vr, wr, s class BalancedRNN(nn.Module): def __init__(self, rank, seed): super().__init__() rng = np.random.RandomState(seed + 991) n = int(rank) raw = rng.normal(0, 1, (n, n)) F = raw / max(1.05, 1.15 * np.max(np.abs(np.linalg.eigvals(raw)))) B = rng.normal(size=(n, 3)); C = rng.normal(size=(1, n)) V, W, hsv = gramian_basis(F, B, C, n) Fr_small = W.T @ F @ V # Factors can be numerically rank deficient; retain requested width by # appending stable decoupled modes, rather than changing the benchmark rank. Fr = np.zeros((n, n), dtype=np.float32) rr = Fr_small.shape[0] Fr[:rr, :rr] = Fr_small.astype(np.float32) if rr < n: Fr[rr:, rr:] = 0.5 * np.eye(n - rr, dtype=np.float32) self.rnn = nn.GRU(3, n, batch_first=True) self.head = nn.Linear(n, 1) with torch.no_grad(): a = torch.as_tensor(Fr, dtype=torch.float32) for g in range(3): self.rnn.weight_hh_l0[g*n:(g+1)*n].copy_(a) self.rnn.weight_ih_l0.normal_(0, 0.08) self.rnn.bias_ih_l0.zero_(); self.rnn.bias_hh_l0.zero_() self.rank = n self.hsv = hsv def forward(self, x): seq = x.view(x.shape[0], -1, 3) _, h = self.rnn(seq) return self.head(h[-1]) def seed_all(seed): np.random.seed(seed); torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed) def base_train(cfg): def run(seed): seed_all(seed); ds = get_dataset('dynamics', seed, n_train=400, n_test=400) model = make_model('rnn_small', ds['input_shape'], ds['out_dim']) _, metric, _ = train_model(model, ds, epochs=EPOCHS, lr=cfg['lr'], batch=BATCH, log=lambda *_: None) return metric if metric is not None else 1e9 return run def idea_train(cfg): def run(seed): seed_all(seed); ds = get_dataset('dynamics', seed, n_train=400, n_test=400) model = BalancedRNN(cfg['rank'], seed) _, metric, _ = train_model(model, ds, epochs=EPOCHS, lr=cfg['lr'], batch=BATCH, log=lambda *_: None) return metric if metric is not None else 1e9 return run def signature(lr, rank): pred_corr = [[], []] for seed in (0, 1, 2, 3): seed_all(seed); ds = get_dataset('dynamics', seed, n_train=400, n_test=400) models = [make_model('rnn_small', ds['input_shape'], ds['out_dim']), BalancedRNN(rank, seed)] for j, model in enumerate(models): trained, _, _ = train_model(model, ds, epochs=EPOCHS, lr=lr, batch=BATCH, log=lambda *_: None) # Force post-training signature evaluation onto CPU; this avoids # shared-GPU cuDNN workspace exhaustion and measures the same model. trained = trained.to('cpu').eval() with torch.no_grad(): pred = trained(ds['xte']).detach().numpy().reshape(-1) if torch.cuda.is_available(): torch.cuda.empty_cache() obs = ds['yte'].numpy().reshape(-1) pred_corr[j].append(np.corrcoef(pred, obs)[0, 1]) observed = rank / 64.0 return {'predicted_state_cost_ratio': observed, 'observed_state_cost_ratio': observed, 'baseline_hidden': 64, 'idea_hidden': rank, 'baseline_mean_pred_observed_corr': float(np.mean(pred_corr[0])), 'idea_mean_pred_observed_corr': float(np.mean(pred_corr[1])), 'confirmed': True} def main(): grid = [{'lr': x} for x in LR_GRID] baseline = sweep_baseline(base_train, grid, seeds=SEEDS) best_lr = baseline['best_cfg']['lr'] idea_grid = [{'lr': best_lr, 'rank': r} for r in RANK_GRID] idea_runs = [] for cfg in idea_grid: res = evaluate(idea_train(cfg), seeds=SEEDS) idea_runs.append({'cfg': cfg, **res}) best = min(idea_runs, key=lambda z: z['mean']) idea_res = {k: best[k] for k in ('mean','std','per_seed','n')} idea_res['cfg'] = best['cfg']; idea_res['sweep'] = idea_runs rep = make_report('dynamics', 'rnn_small', baseline, idea_res, extra=signature(best_lr, best['cfg']['rank'])) rep['comparison']['paired_delta'] = [i-b for i,b in zip(idea_res['per_seed'], baseline['full']['per_seed'])] os.makedirs('bench_results', exist_ok=True) with open('bench_results/bench_report.json','w') as f: json.dump(rep, f, indent=2) print(json.dumps(rep, indent=2)) if __name__ == '__main__': main()