Bidirectional Saturation-Aware Trust Region / bench_experiment.py

Failed on benchmark

Raw ⬇ ZIP
  1import sys, json, math, random
  2from pathlib import Path
  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, sweep_baseline, evaluate, make_report
  9
 10TRACK = "tabular"
 11MODEL = "mlp_tiny"
 12EPOCHS = 18
 13BATCH = 128
 14WEIGHT_DECAY = 0.0
 15# Shared union: baseline and idea both run every learning rate below.
 16LRS = [1e-3, 3e-3, 1e-2]
 17IDEA_SETTINGS = [
 18    {"lr": 1e-3, "r0": 0.02},
 19    {"lr": 3e-3, "r0": 0.02},
 20    {"lr": 1e-2, "r0": 0.02},
 21]
 22SEEDS = tuple(range(8))
 23SWEEP_SEEDS = tuple(range(4))
 24
 25
 26def seed_all(seed):
 27    random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
 28    if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
 29
 30
 31def global_norm(xs):
 32    return math.sqrt(sum(float(x.detach().pow(2).sum().cpu()) for x in xs))
 33
 34
 35def train_baseline(cfg, seed, return_model=False):
 36    seed_all(seed)
 37    ds = get_dataset(TRACK, seed=seed, n_train=400, n_test=400)
 38    net = make_model(MODEL, ds["input_shape"], ds["out_dim"])
 39    device = "cuda" if torch.cuda.is_available() else "cpu"
 40    try:
 41        net = net.to(device)
 42        opt = torch.optim.Adam(net.parameters(), lr=cfg["lr"], weight_decay=WEIGHT_DECAY)
 43        lossf = nn.MSELoss()
 44        x, y = ds["xtr"].to(device), ds["ytr"].to(device)
 45        for _ in range(EPOCHS):
 46            net.train(); perm = torch.randperm(len(x), device=device)
 47            for i in range(0, len(x), BATCH):
 48                ix = perm[i:i+BATCH]; loss = lossf(net(x[ix]), y[ix])
 49                opt.zero_grad(set_to_none=True); loss.backward(); opt.step()
 50        net.eval()
 51        with torch.no_grad(): metric = float(lossf(net(ds["xte"].to(device)), ds["yte"].to(device)).cpu())
 52        return (metric, net, ds, device) if return_model else metric
 53    except (RuntimeError, torch.cuda.CudaError):
 54        if device == "cuda":
 55            seed_all(seed); net = make_model(MODEL, ds["input_shape"], ds["out_dim"])
 56            return train_baseline(cfg, seed, return_model) if False else _train_cpu(net, ds, cfg, return_model)
 57        raise
 58
 59
 60def _train_cpu(net, ds, cfg, return_model=False):
 61    net = net.cpu(); opt = torch.optim.Adam(net.parameters(), lr=cfg["lr"], weight_decay=WEIGHT_DECAY)
 62    lossf = nn.MSELoss(); x, y = ds["xtr"], ds["ytr"]
 63    for _ in range(EPOCHS):
 64        perm = torch.randperm(len(x))
 65        for i in range(0, len(x), BATCH):
 66            ix=perm[i:i+BATCH]; loss=lossf(net(x[ix]), y[ix]); opt.zero_grad(set_to_none=True); loss.backward(); opt.step()
 67    with torch.no_grad(): metric=float(lossf(net(ds["xte"]), ds["yte"]))
 68    return (metric, net, ds, "cpu") if return_model else metric
 69
 70
 71def train_idea(cfg, seed, return_model=False):
 72    seed_all(seed)
 73    ds = get_dataset(TRACK, seed=seed, n_train=400, n_test=400)
 74    net = make_model(MODEL, ds["input_shape"], ds["out_dim"])
 75    device = "cuda" if torch.cuda.is_available() else "cpu"
 76    try:
 77        net=net.to(device); opt=torch.optim.Adam(net.parameters(), lr=cfg["lr"], weight_decay=WEIGHT_DECAY)
 78        lossf=nn.MSELoss(); x,y=ds["xtr"].to(device),ds["ytr"].to(device)
 79        # Fixed cap is deliberately identical to initial adaptive radius.
 80        r=float(cfg["r0"]); rmin=.1*cfg["r0"]; rmax=10*cfg["r0"]
 81        qs=[]; radii=[]; scales=[]; proposal_norms=[]
 82        for _ in range(EPOCHS):
 83            net.train(); perm=torch.randperm(len(x),device=device)
 84            for i in range(0,len(x),BATCH):
 85                ix=perm[i:i+BATCH]; loss=lossf(net(x[ix]),y[ix]); opt.zero_grad(set_to_none=True); loss.backward()
 86                # Adam's actual proposal is reconstructed from its state after a harmless step.
 87                # We instead use the standard Adam preconditioned proposal explicitly.
 88                props=[]
 89                beta1,beta2=opt.param_groups[0]["betas"]; eps=opt.param_groups[0]["eps"]; step=int(opt.state[next(net.parameters())].get("step",0))+1
 90                for p in net.parameters():
 91                    if p.grad is None: continue
 92                    st=opt.state[p]
 93                    if not st: st["step"]=torch.tensor(0.,device=p.device); st["exp_avg"]=torch.zeros_like(p); st["exp_avg_sq"]=torch.zeros_like(p)
 94                    m=st["exp_avg"]; v=st["exp_avg_sq"]; m.mul_(beta1).add_(p.grad,alpha=1-beta1); v.mul_(beta2).addcmul_(p.grad,p.grad,value=1-beta2)
 95                    bc1=1-beta1**step; bc2=1-beta2**step
 96                    props.append(-cfg["lr"]*(m/bc1)/(v.sqrt()/math.sqrt(bc2)+eps))
 97                pn=global_norm(props); q=min(1.,pn/(r+1e-12)); scale=min(1.,r/(pn+1e-12))
 98                with torch.no_grad():
 99                    for p,prop in zip([p for p in net.parameters() if p.grad is not None],props): p.add_(prop,alpha=scale)
100                r=float(np.clip(r*math.exp(.02*q-.01*(1-q)),rmin,rmax)); qs.append(q); radii.append(r); scales.append(scale); proposal_norms.append(pn)
101        net.eval();
102        with torch.no_grad(): metric=float(lossf(net(ds["xte"].to(device)),ds["yte"].to(device)).cpu())
103        if return_model: return metric,net,ds,device,{"mean_q":float(np.mean(qs)),"clip_fraction":float(np.mean(np.array(qs)>=1-1e-9)),"radius_final":r,"radius_initial":cfg["r0"],"mean_scale":float(np.mean(scales)),"mean_proposal_norm":float(np.mean(proposal_norms))}
104        return metric
105    except (RuntimeError, torch.cuda.CudaError):
106        raise RuntimeError("CUDA failure; rerun with CUDA_VISIBLE_DEVICES='' for CPU fallback")
107
108
109def baseline_factory(cfg): return lambda seed: train_baseline(cfg, seed)
110def idea_factory(cfg): return lambda seed: train_idea(cfg, seed)
111
112
113def main():
114    # Official baseline sweep, then full-seed baseline at selected cfg.
115    base=sweep_baseline(baseline_factory, [{"lr":lr} for lr in LRS], seeds=SWEEP_SEEDS)
116    idea_cfgs=IDEA_SETTINGS
117    idea_sweep=[]
118    for cfg in idea_cfgs: idea_sweep.append({"cfg":cfg,"mean":evaluate(idea_factory(cfg),SWEEP_SEEDS)["mean"]})
119    best_idea_cfg=min(idea_sweep,key=lambda z:z["mean"])["cfg"]
120    base_full=base["full"]
121    idea_full=evaluate(idea_factory(best_idea_cfg),SEEDS)
122    # Trained-model signature: inspect one paired seed's actual proposals/updates.
123    _,_,_,_,sig=train_idea(best_idea_cfg,0,True)
124    sig.update({"prediction":"sustained saturation expands radius and later unsaturation contracts it","observed_radius_increase":sig["radius_final"]>sig["radius_initial"],"observed_clipping_fraction":sig["clip_fraction"],"confirmed":bool(sig["radius_final"]>sig["radius_initial"] and sig["clip_fraction"]>0)})
125    report=make_report(TRACK,MODEL,{"best_cfg":base["best_cfg"],"sweep":base["sweep"],"full":base_full},idea_full,{"idea_sweep":idea_sweep,"mechanism_signature":sig})
126    Path("bench_report.json").write_text(json.dumps(report,indent=2))
127    print(json.dumps(report,indent=2))
128
129if __name__=="__main__": main()