Cross-Ratio Reversible Lattice Layer / stage2_cross_ratio_bench.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1import sys, json, random
  2import numpy as np
  3import torch
  4import torch.nn as nn
  5
  6sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  7from bench import get_dataset, train_model, sweep_baseline, make_report
  8from bench.protocol import evaluate
  9
 10# Dynamics is the structurally matched built-in: it is an actuated pendulum
 11# rollout task and the proposal claims improved recurrent stability/long horizon behavior.
 12SEEDS = tuple(range(8))
 13SWEEP_SEEDS = (0, 1, 2, 3)
 14
 15
 16def seed_all(seed):
 17    random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
 18    if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
 19
 20
 21class StandardRNN(nn.Module):
 22    def __init__(self, input_dim, hidden=64, out_dim=1):
 23        super().__init__(); self.inp=nn.Linear(input_dim, hidden)
 24        self.rec=nn.Linear(hidden, hidden); self.head=nn.Linear(hidden, out_dim)
 25    def forward(self, x):
 26        # x is (batch, features) for the bench dynamics task; make it one-step.
 27        h=torch.tanh(self.inp(x)); h=torch.tanh(self.rec(h)+h)
 28        return self.head(h)
 29
 30
 31class CrossRatioRNN(nn.Module):
 32    """Real-channel implementation of the cross-ratio completion rule.
 33
 34    Four complex points are represented by 8 real hidden channels. Three
 35    points are retained from the ordinary recurrent proposal; the fourth is
 36    replaced by d=((b-c)a-(a-b)c)/((b-c)-(a-b)). A bounded fallback is used
 37    only for near-singular denominators, avoiding NaNs during optimization.
 38    """
 39    def __init__(self, input_dim, hidden=64, out_dim=1, eps=1e-4):
 40        super().__init__(); assert hidden % 8 == 0
 41        self.inp=nn.Linear(input_dim, hidden); self.rec=nn.Linear(hidden, hidden)
 42        self.head=nn.Linear(hidden, out_dim); self.eps=eps
 43    def forward(self, x, return_signature=False):
 44        q=torch.tanh(self.inp(x)+self.rec(torch.zeros(x.shape[0], self.rec.in_features, device=x.device)))
 45        # q has 8-channel blocks: each block is four complex lattice points.
 46        v=q.reshape(q.shape[0], -1, 8)
 47        a=v[...,0]+1j*v[...,1]; b=v[...,2]+1j*v[...,3]
 48        c=v[...,4]+1j*v[...,5]
 49        den=(b-c)-(a-b); num=(b-c)*a-(a-b)*c
 50        safe=torch.where(den.abs() >= self.eps, den, torch.ones_like(den))
 51        d=num/safe
 52        d=torch.where(den.abs() >= self.eps, d, c)
 53        complete=torch.stack((d.real,d.imag), dim=-1)
 54        # replace the fourth point while retaining the learned three corners
 55        v2=torch.cat((v[...,:6], complete), dim=-1)
 56        h=v2.reshape(q.shape[0], -1)
 57        out=self.head(h)
 58        if return_signature:
 59            cr=(a-b)*(c-d)/((b-c)*(d-a)+1e-12)
 60            residual=(cr+1).abs().mean()
 61            return out, float(residual.detach().cpu()), float((den.abs()<self.eps).float().mean().detach().cpu())
 62        return out
 63
 64
 65def run_one(seed, idea, lr, epochs, wd):
 66    seed_all(seed)
 67    ds=get_dataset('dynamics', seed=seed, n_train=400, n_test=160)
 68    # The bench rnn_small has 64 hidden units. Keep every shared dimension and
 69    # optimizer setting equal; only the recurrent mechanism differs.
 70    model=CrossRatioRNN(ds['xtr'].shape[1],64,ds['out_dim']) if idea else StandardRNN(ds['xtr'].shape[1],64,ds['out_dim'])
 71    _, metric, _=train_model(model, ds, epochs=epochs, lr=lr, batch=128, weight_decay=wd, log=lambda *_: None)
 72    return metric
 73
 74
 75def baseline_factory(cfg):
 76    return lambda seed: run_one(seed, False, cfg['lr'], cfg['epochs'], cfg['weight_decay'])
 77
 78def idea_factory(cfg):
 79    return lambda seed: run_one(seed, True, cfg['lr'], cfg['epochs'], cfg['weight_decay'])
 80
 81
 82def main():
 83    # Union parity: all idea lrs and method-central weight decays are also baseline-tested.
 84    grid=[]
 85    for lr in (1e-3,3e-3,1e-2):
 86        for wd in (0.0,1e-4): grid.append({'lr':lr,'epochs':25,'weight_decay':wd})
 87    base=sweep_baseline(baseline_factory, grid, seeds=SWEEP_SEEDS)
 88    idea_cfgs=[base['best_cfg'], {'lr':1e-3,'epochs':25,'weight_decay':0.0}, {'lr':1e-2,'epochs':25,'weight_decay':1e-4}]
 89    idea_trials=[]
 90    for cfg in idea_cfgs:
 91        r=evaluate(idea_factory(cfg), seeds=SEEDS)
 92        idea_trials.append({'cfg':cfg,'result':r})
 93    best=min(idea_trials, key=lambda z:z['result']['mean'])
 94    # Signature is re-measured on trained NN outputs, not the synthetic graph.
 95    seed_all(0); ds=get_dataset('dynamics',seed=0,n_train=400,n_test=160)
 96    m=CrossRatioRNN(ds['xtr'].shape[1],64,ds['out_dim']); _,_,_=train_model(m,ds,epochs=best['cfg']['epochs'],lr=best['cfg']['lr'],batch=128,weight_decay=best['cfg']['weight_decay'],log=lambda *_:None)
 97    m.eval()
 98    dev=next(m.parameters()).device
 99    with torch.no_grad():
100        _, res, singular=m(ds['xte'].to(dev),return_signature=True)
101    signature={'prediction':'trained model plaquette residual near zero','observed_mean_residual':res,'observed_singular_fraction':singular,'predicted_float32_scale':'~1e-6','confirmed': bool(np.isfinite(res) and res < 1e-3)}
102    rep=make_report('dynamics','rnn_small',base,best['result'],extra={'mechanism_signature':signature,'idea_sweep':idea_trials,'structural_match':'stability/control dynamics'})
103    with open('bench_report.json','w') as f: json.dump(rep,f,indent=2)
104    print(json.dumps(rep,indent=2))
105
106if __name__=='__main__': main()