Cut-Aware Augmentation Filtering / stage2_bench.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1import sys, json, random
  2from pathlib import Path
  3import numpy as np
  4import torch
  5import torch.nn as nn
  6import torch.nn.functional as F
  7sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  8from bench import get_dataset, make_model, train_model, evaluate, sweep_baseline, make_report
  9from bench.protocol import DEFAULT_SEEDS
 10
 11SEEDS = DEFAULT_SEEDS
 12GRID = [{'lr': 1e-3}, {'lr': 3e-3}, {'lr': 5e-3}]
 13
 14def cut_ratio(W, y):
 15    yy = y[:, None] != y[None, :]
 16    return float((W * yy).sum() / (W.sum() + 1e-12))
 17
 18def graph(z, k=5):
 19    z = z.reshape(len(z), -1).astype('float32')
 20    z = z / (np.linalg.norm(z, axis=1, keepdims=True) + 1e-8)
 21    sim = z @ z.T
 22    np.fill_diagonal(sim, -np.inf)
 23    idx = np.argpartition(-sim, min(k, len(z)-1)-1, axis=1)[:, :k]
 24    W = np.zeros_like(sim, dtype='float32')
 25    rows = np.arange(len(z))[:, None]
 26    W[rows, idx] = np.maximum(sim[rows, idx], 0)
 27    return np.maximum(W, W.T)
 28
 29def augment(x, policy):
 30    # CIFAR tensors are [N,C,H,W], values in [0,1]. Policies intentionally include
 31    # a label-destroying permutation, making the proposed filter testable.
 32    if policy == 'identity': return x
 33    if policy == 'flip': return torch.flip(x, dims=[3])
 34    if policy == 'harmful': return x[torch.randperm(len(x), device=x.device)]
 35    raise ValueError(policy)
 36
 37def policy_weights(d, beta):
 38    x, y = d['xtr'], d['ytr']
 39    cuts=[]
 40    for p in ('identity','flip','harmful'):
 41        z=augment(x,p).detach().cpu().numpy()
 42        cuts.append(cut_ratio(graph(z), y.cpu().numpy()))
 43    q=np.exp(-beta*(np.asarray(cuts)-min(cuts))); q/=q.sum()
 44    return np.asarray(cuts), q
 45
 46def train_one(seed, lr, mode, beta=12., epochs=8, return_model=False):
 47    random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
 48    d=get_dataset('vision', seed, n_train=400, n_test=200)
 49    # Graph regularization uses detached image-space policy graphs and prediction
 50    # vectors; this is the intervention, so a local loop is required.
 51    cuts,q=policy_weights(d,beta)
 52    Ws=[]
 53    for p in ('identity','flip','harmful'):
 54        Ws.append(torch.tensor(graph(augment(d['xtr'],p).cpu().numpy()), dtype=torch.float32))
 55    W=sum((float(qi) if mode=='cut' else 1/3)*w for qi,w in zip(q,Ws))
 56    W=W/(W.sum()+1e-8)
 57    net=make_model('cnn_small', d['input_shape'], d['out_dim'])
 58    device='cuda' if torch.cuda.is_available() else 'cpu'
 59    try:
 60        net=net.to(device); x=d['xtr'].to(device); y=d['ytr'].to(device); W=W.to(device)
 61        opt=torch.optim.Adam(net.parameters(),lr=lr)
 62        for _ in range(epochs):
 63            net.train(); perm=torch.randperm(len(x),device=device)
 64            for i in range(0,len(x),64):
 65                ix=perm[i:i+64]; logits=net(x[ix]); sup=F.cross_entropy(logits,y[ix])
 66                # Full graph penalty is computed on a small 400-sample track.
 67                p=F.softmax(net(x),1); diff=(p[:,None,:]-p[None,:,:]).pow(2).sum(-1)
 68                loss=sup + 0.15*(W*diff).sum()/len(x)
 69                opt.zero_grad(); loss.backward(); opt.step()
 70        net.eval()
 71        with torch.no_grad():
 72            pred=net(d['xte'].to(device)).argmax(1).cpu(); metric=float((pred!=d['yte']).float().mean())
 73        out={'metric':metric,'cuts':cuts.tolist(),'q':q.tolist(),'graph_reg':float((W*diff).sum().detach().cpu())}
 74    except RuntimeError:
 75        # deterministic CPU fallback
 76        random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
 77        net=make_model('cnn_small', d['input_shape'], d['out_dim'])
 78        W=W.cpu(); x=d['xtr']; y=d['ytr']; opt=torch.optim.Adam(net.parameters(),lr=lr)
 79        for _ in range(epochs):
 80            p=F.softmax(net(x),1); sup=F.cross_entropy(net(x),y); diff=(p[:,None,:]-p[None,:,:]).pow(2).sum(-1)
 81            loss=sup+0.15*(W*diff).sum()/len(x); opt.zero_grad(); loss.backward(); opt.step()
 82        with torch.no_grad(): metric=float((net(d['xte']).argmax(1)!=d['yte']).float().mean())
 83        out={'metric':metric,'cuts':cuts.tolist(),'q':q.tolist(),'graph_reg':float((W*diff).sum())}
 84    return (out['metric'],out) if return_model else out['metric']
 85
 86def main():
 87    # Cheap numerical verification of the claimed scale invariance and exponential ordering.
 88    y=np.array([0,0,1,1]); W=np.array([[0,1,0,0],[1,0,0,0],[0,0,0,1],[0,0,1,0]],float)
 89    math_check={'scale_error':abs(cut_ratio(W,y)-cut_ratio(7.3*W,y)),'q_order':bool(np.exp(-10*0)<np.exp(-10*0.5))}
 90    def base_factory(c): return lambda s: train_one(s,c['lr'],'uniform')
 91    base=sweep_baseline(base_factory, GRID)
 92    trials=[]
 93    for c in GRID:
 94        vals=[train_one(s,c['lr'],'cut') for s in SEEDS]
 95        trials.append({'cfg':c,'result':{'mean':float(np.mean(vals)),'std':float(np.std(vals)),'per_seed':vals,'n':8}})
 96    best=min(trials,key=lambda z:z['result']['mean']); idea=best['result']
 97    sig=[]
 98    for s in SEEDS[:2]: sig.append({'seed':s,'baseline':train_one(s,base['best_cfg']['lr'],'uniform',return_model=True)[1],'idea':train_one(s,base['best_cfg']['lr'],'cut',return_model=True)[1]})
 99    signature={'prediction':'higher estimated cut policy receives lower q','observed':sig,'confirmed':all(min(x['idea']['q']) <= min(x['baseline']['q']) for x in sig)}
100    rep=make_report('vision','cnn_small',base,idea,{'math_check':math_check,'idea_sweep':trials,'mechanism_signature':signature,'structural_match':'CIFAR image augmentations and CNN prediction graph'})
101    rep['custom_track']=None
102    Path('bench_report.json').write_text(json.dumps(rep,indent=2)); print(json.dumps(rep,indent=2))
103if __name__=='__main__': main()