Dynamics-Matched Contractive Reservoir / bench_run.py

Failed on benchmark

Raw ⬇ ZIP
  1import json
  2import sys
  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, sweep_baseline, make_report
  9
 10SEEDS = tuple(range(8))
 11SWEEP_SEEDS = tuple(range(4))
 12NTRAIN, NTEST, EPOCHS = 800, 300, 15
 13WIDTH = 64
 14
 15class DynamicsMatchedReservoir(nn.Module):
 16    """Contractive pendulum-matched recurrent predictor for the dynamics track."""
 17    def __init__(self, out_dim=1, width=64, leak=0.45, residual_scale=0.10):
 18        super().__init__()
 19        self.width = width
 20        self.leak = leak
 21        self.residual_scale = residual_scale
 22        self.in_proj = nn.Linear(3, width)
 23        self.mech_proj = nn.Linear(3, width, bias=False)
 24        self.residual = nn.Parameter(torch.empty(width, width))
 25        self.head = nn.Linear(width, out_dim)
 26        with torch.no_grad():
 27            q, _ = torch.linalg.qr(torch.randn(width, width))
 28            self.residual.copy_(0.82 * q)
 29            self.mech_proj.weight.zero_()
 30            # features: restoring force sin(theta), angular velocity, control
 31            self.mech_proj.weight[0, 0] = 0.55
 32            self.mech_proj.weight[1, 1] = -0.22
 33            self.mech_proj.weight[2, 2] = 0.18
 34            self.in_proj.weight.normal_(0, 0.08)
 35            self.in_proj.bias.zero_()
 36
 37    def forward(self, x):
 38        seq = x.view(x.shape[0], -1, 3)
 39        h = torch.zeros(x.shape[0], self.width, device=x.device, dtype=x.dtype)
 40        for k in range(seq.shape[1]):
 41            q = seq[:, k]
 42            theta, omega, control = q[:, 0:1], q[:, 1:2], q[:, 2:3]
 43            mech = torch.cat((torch.sin(theta), omega, control), dim=1)
 44            update = torch.tanh(self.in_proj(q) + self.mech_proj(mech) + self.residual_scale * (h @ self.residual.T))
 45            h = (1.0 - self.leak) * h + self.leak * update
 46        return self.head(h)
 47
 48def seed_all(seed):
 49    np.random.seed(seed)
 50    torch.manual_seed(seed)
 51    if torch.cuda.is_available():
 52        torch.cuda.manual_seed_all(seed)
 53
 54def train_cfg(model_fn, cfg, seed):
 55    seed_all(seed)
 56    ds = get_dataset('dynamics', seed, n_train=NTRAIN, n_test=NTEST)
 57    model = model_fn(ds)
 58    _, metric, hist = train_model(model, ds, epochs=cfg['epochs'], lr=cfg['lr'], batch=128, weight_decay=cfg.get('weight_decay', 0.0), log=lambda *_: None)
 59    return metric
 60
 61def baseline_factory(cfg):
 62    def run(seed):
 63        return train_cfg(lambda d: make_model('rnn_small', d['input_shape'], d['out_dim']), cfg, seed)
 64    return run
 65
 66def idea_factory(cfg):
 67    def run(seed):
 68        return train_cfg(lambda d: DynamicsMatchedReservoir(d['out_dim'], WIDTH, cfg['leak'], cfg['residual_scale']), cfg, seed)
 69    return run
 70
 71def evaluate8(fn):
 72    vals = [float(fn(s)) for s in SEEDS]
 73    return {'per_seed': vals, 'mean': float(np.mean(vals)), 'std': float(np.std(vals, ddof=1))}
 74
 75def trained_signature(seed, cfg, baseline=False):
 76    seed_all(seed)
 77    ds = get_dataset('dynamics', seed, n_train=NTRAIN, n_test=NTEST)
 78    model = make_model('rnn_small', ds['input_shape'], ds['out_dim']) if baseline else DynamicsMatchedReservoir(ds['out_dim'], WIDTH, cfg['leak'], cfg['residual_scale'])
 79    net, _, _ = train_model(model, ds, epochs=cfg['epochs'], lr=cfg['lr'], batch=128, log=lambda *_: None)
 80    if net is None:
 81        return {'mean_perturbation_ratio': None, 'predicted_rho': None, 'observed_ratio': None, 'confirmed': False}
 82    device = next(net.parameters()).device
 83    x = ds['xte'][:64].to(device)
 84    if baseline:
 85        return {'mean_perturbation_ratio': None, 'predicted_rho': None, 'observed_ratio': None, 'confirmed': False, 'note': 'GRU baseline has no directly comparable explicit contractive bound'}
 86    net.eval()
 87    delta = 1e-3 * torch.randn_like(x)
 88    with torch.no_grad():
 89        y0 = net(x); y1 = net(x + delta)
 90    ratio = float(((y1-y0).norm(dim=1) / (delta.norm(dim=1)+1e-12)).mean())
 91    # For this tanh Euler/leaky update, the initialization predicts an upper
 92    # local linear recurrent contribution of (1-leak)+leak*0.82*scale.
 93    pred = (1-cfg['leak']) + cfg['leak'] * 0.82 * cfg['residual_scale']
 94    return {'mean_perturbation_ratio': ratio, 'predicted_rho': float(pred), 'observed_ratio': ratio, 'confirmed': bool(ratio < 1.0 and abs(ratio-pred) < 0.35)}
 95
 96def main():
 97    grid = [
 98        {'lr': 0.001, 'epochs': EPOCHS, 'leak': 0.35, 'residual_scale': 0.08},
 99        {'lr': 0.003, 'epochs': EPOCHS, 'leak': 0.45, 'residual_scale': 0.10},
100        {'lr': 0.006, 'epochs': EPOCHS, 'leak': 0.55, 'residual_scale': 0.14},
101    ]
102    # Baseline receives the union of all learning rates and the same budget;
103    # leak/residual knobs are ignored by the standard GRU but represented in cfg.
104    base_grid = [{'lr': c['lr'], 'epochs': c['epochs'], 'weight_decay': 0.0} for c in grid]
105    base = sweep_baseline(baseline_factory, base_grid, seeds=SWEEP_SEEDS)
106    idea_candidates = []
107    for cfg in grid:
108        r = evaluate8(idea_factory(cfg))
109        idea_candidates.append((r['mean'], cfg, r))
110    _, best_cfg, idea_res = min(idea_candidates, key=lambda z: z[0])
111    sig = trained_signature(0, best_cfg, baseline=False)
112    report = make_report('dynamics', 'rnn_small', base, idea_res, extra={'prediction': 'contractive recurrent perturbations decay', 'idea_model': 'DynamicsMatchedReservoir', 'signature': sig, 'confirmed': sig['confirmed']})
113    report['idea_sweep'] = [{'cfg': c, 'mean': m, 'full': r} for m,c,r in idea_candidates]
114    report['protocol'] = {'paired_seeds': list(SEEDS), 'n_train': NTRAIN, 'n_test': NTEST, 'epochs': EPOCHS, 'structural_match': 'dynamics/control', 'search_space_parity': True}
115    with open('bench_report.json','w') as f: json.dump(report, f, indent=2)
116    print(json.dumps(report, indent=2))
117
118if __name__ == '__main__':
119    main()