Cross-Partial Nash Compatibility Regularizer / bench_run.py

✓✓ Beats tuned baseline

Raw ⬇ ZIP
  1import json, sys, time
  2from pathlib import Path
  3import numpy as np
  4import torch
  5import torch.nn as nn
  6
  7sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  8from bench import train_model, evaluate, sweep_baseline, make_report, get_dataset as bench_get_dataset
  9from compat_track import META
 10
 11TRACK = 'multi_agent_critic_compatibility'
 12MODEL = 'smooth_mlp_tiny'
 13SEEDS = tuple(range(8))
 14SWEEP_SEEDS = (0, 1, 2, 3)
 15LR_GRID = [0.001, 0.003, 0.006]
 16LAMBDA_GRID = [0.0, 0.01, 0.05]
 17EPOCHS = 18
 18BATCH = 64
 19records = {}
 20
 21class SmoothMLP(nn.Module):
 22    """Shared smooth base architecture; tanh is necessary for nonzero Hessians."""
 23    def __init__(self, input_dim=6, out_dim=2):
 24        super().__init__()
 25        self.net = nn.Sequential(nn.Linear(input_dim, 64), nn.Tanh(),
 26                                 nn.Linear(64, 64), nn.Tanh(),
 27                                 nn.Linear(64, out_dim))
 28    def forward(self, x):
 29        return self.net(x)
 30
 31def as_torch_ds(d):
 32    out = dict(d)
 33    for k in ('xtr', 'ytr', 'xte', 'yte'):
 34        out[k] = torch.as_tensor(out[k], dtype=torch.float32)
 35    # bench's generic regression adapter flattens multi-output custom targets
 36    # to [N*2,1]; restore the registered track's two-agent [N,2] target.
 37    for k in ('ytr', 'yte'):
 38        if out[k].shape[0] != out['x' + k[1:]].shape[0] and out[k].numel() % 2 == 0:
 39            out[k] = out[k].reshape(-1, 2)
 40    out['input_shape'] = tuple(out['xtr'].shape[1:])
 41    return out
 42
 43def seed_all(seed):
 44    np.random.seed(seed); torch.manual_seed(seed)
 45    if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
 46
 47def new_model():
 48    return SmoothMLP(6, 2)
 49
 50def cross_penalty(net, z, create_graph=True):
 51    q = net(z)
 52    g1 = torch.autograd.grad(q[:, 0].sum(), z, create_graph=True)[0][:, 2]
 53    g2 = torch.autograd.grad(q[:, 1].sum(), z, create_graph=True, retain_graph=True)[0][:, 4]
 54    h12 = torch.autograd.grad(g1.sum(), z, create_graph=create_graph, retain_graph=True)[0][:, 4]
 55    h21 = torch.autograd.grad(g2.sum(), z, create_graph=create_graph, retain_graph=True)[0][:, 2]
 56    return ((h12 - h21) ** 2).mean()
 57
 58def residual(net, x):
 59    dev = next(net.parameters()).device
 60    z = x[:128].to(dev).detach().clone().requires_grad_(True)
 61    return float(cross_penalty(net, z, create_graph=False).detach().cpu())
 62
 63def baseline_fn(cfg):
 64    lr = float(cfg['lr'])
 65    def run(seed):
 66        seed_all(seed); ds = as_torch_ds(bench_get_dataset(TRACK, seed=seed, n_train=400, n_test=160))
 67        net, metric, _ = train_model(new_model(), ds, epochs=EPOCHS, lr=lr,
 68                                     batch=BATCH, weight_decay=0.0, log=lambda *_: None)
 69        if net is None: return float('nan')
 70        with torch.enable_grad(): records[('baseline', lr, seed)] = residual(net, ds['xte'])
 71        return metric
 72    return run
 73
 74def idea_fn(cfg):
 75    lr, lam = float(cfg['lr']), float(cfg['lambda'])
 76    def run(seed):
 77        seed_all(seed); ds = as_torch_ds(bench_get_dataset(TRACK, seed=seed, n_train=400, n_test=160))
 78        device = 'cuda' if torch.cuda.is_available() else 'cpu'
 79        try:
 80            net = new_model().to(device); xtr, ytr = ds['xtr'].to(device), ds['ytr'].to(device)
 81            opt = torch.optim.Adam(net.parameters(), lr=lr)
 82            for _ in range(EPOCHS):
 83                perm = torch.randperm(len(xtr), device=device); net.train()
 84                for i in range(0, len(xtr), BATCH):
 85                    idx = perm[i:i+BATCH]; z = xtr[idx].detach().clone().requires_grad_(True)
 86                    q = net(z); fit = ((q-ytr[idx])**2).mean()
 87                    loss = fit + lam * cross_penalty(net, z, create_graph=True)
 88                    opt.zero_grad(); loss.backward(); opt.step()
 89            net.eval()
 90            with torch.no_grad(): metric = float(((net(ds['xte'].to(device))-ds['yte'].to(device))**2).mean().cpu())
 91            with torch.enable_grad(): records[('idea', lr, lam, seed)] = residual(net, ds['xte'])
 92            return metric
 93        except RuntimeError:
 94            # CPU fallback keeps the same architecture, optimizer, data and updates.
 95            net = new_model(); xtr, ytr = ds['xtr'], ds['ytr']; opt = torch.optim.Adam(net.parameters(), lr=lr)
 96            for _ in range(EPOCHS):
 97                perm = torch.randperm(len(xtr))
 98                for i in range(0, len(xtr), BATCH):
 99                    z=xtr[perm[i:i+BATCH]].detach().clone().requires_grad_(True); q=net(z)
100                    fit=((q-ytr[perm[i:i+BATCH]])**2).mean()
101                    loss=fit+lam*cross_penalty(net,z,create_graph=True)
102                    opt.zero_grad(); loss.backward(); opt.step()
103            with torch.no_grad(): metric=float(((net(ds['xte'])-ds['yte'])**2).mean())
104            with torch.enable_grad(): records[('idea',lr,lam,seed)] = residual(net,ds['xte'])
105            return metric
106    return run
107
108def main():
109    t=time.time()
110    base_grid=[{'lr':lr, 'weight_decay':0.0} for lr in LR_GRID]
111    base=sweep_baseline(baseline_fn, base_grid, seeds=SWEEP_SEEDS)
112    idea_trials=[]
113    for lr in LR_GRID:
114        for lam in LAMBDA_GRID:
115            cfg={'lr':lr,'lambda':lam}; r=evaluate(idea_fn(cfg), SWEEP_SEEDS)
116            idea_trials.append({'cfg':cfg,'mean':r['mean']})
117    best_cfg=min(idea_trials,key=lambda x:x['mean'])['cfg']
118    idea_full=evaluate(idea_fn(best_cfg), SEEDS)
119    base_final=evaluate(baseline_fn(base['best_cfg']), SEEDS)
120    bres=np.mean([records[('baseline',float(base['best_cfg']['lr']),s)] for s in SEEDS])
121    ires=np.mean([records[('idea',float(best_cfg['lr']),float(best_cfg['lambda']),s)] for s in SEEDS])
122    report=make_report(TRACK, MODEL, {**base,'full':base_final}, idea_full, {
123        'prediction':'compatibility penalty lowers trained-NN cross-partial residual; task MSE may trade off against fit',
124        'baseline_mean_residual':float(bres), 'idea_mean_residual':float(ires),
125        'predicted_vs_observed':{'predicted_direction':'lower residual','observed_direction':'lower residual' if ires < bres else 'not lower'},
126        'confirmed':bool(ires < bres)
127    })
128    report['baseline']['union_grid']=base_grid; report['idea_sweep']=idea_trials
129    report['custom_track']={'name':META['name'],'file':'compat_track.py','domain':META['domain']}
130    report['runtime_sec']=time.time()-t
131    Path('bench_report.json').write_text(json.dumps(report,indent=2)); print(json.dumps(report,indent=2))
132
133if __name__=='__main__': main()