import sys, json, random from pathlib import Path import numpy as np import torch import torch.nn as nn import torch.nn.functional as F sys.path.insert(0, '/home/maxwelhelp/all/math2nn') from bench import get_dataset, make_model, train_model, evaluate, sweep_baseline, make_report from bench.protocol import DEFAULT_SEEDS SEEDS = DEFAULT_SEEDS GRID = [{'lr': 1e-3}, {'lr': 3e-3}, {'lr': 5e-3}] def cut_ratio(W, y): yy = y[:, None] != y[None, :] return float((W * yy).sum() / (W.sum() + 1e-12)) def graph(z, k=5): z = z.reshape(len(z), -1).astype('float32') z = z / (np.linalg.norm(z, axis=1, keepdims=True) + 1e-8) sim = z @ z.T np.fill_diagonal(sim, -np.inf) idx = np.argpartition(-sim, min(k, len(z)-1)-1, axis=1)[:, :k] W = np.zeros_like(sim, dtype='float32') rows = np.arange(len(z))[:, None] W[rows, idx] = np.maximum(sim[rows, idx], 0) return np.maximum(W, W.T) def augment(x, policy): # CIFAR tensors are [N,C,H,W], values in [0,1]. Policies intentionally include # a label-destroying permutation, making the proposed filter testable. if policy == 'identity': return x if policy == 'flip': return torch.flip(x, dims=[3]) if policy == 'harmful': return x[torch.randperm(len(x), device=x.device)] raise ValueError(policy) def policy_weights(d, beta): x, y = d['xtr'], d['ytr'] cuts=[] for p in ('identity','flip','harmful'): z=augment(x,p).detach().cpu().numpy() cuts.append(cut_ratio(graph(z), y.cpu().numpy())) q=np.exp(-beta*(np.asarray(cuts)-min(cuts))); q/=q.sum() return np.asarray(cuts), q def train_one(seed, lr, mode, beta=12., epochs=8, return_model=False): random.seed(seed); np.random.seed(seed); torch.manual_seed(seed) d=get_dataset('vision', seed, n_train=400, n_test=200) # Graph regularization uses detached image-space policy graphs and prediction # vectors; this is the intervention, so a local loop is required. cuts,q=policy_weights(d,beta) Ws=[] for p in ('identity','flip','harmful'): Ws.append(torch.tensor(graph(augment(d['xtr'],p).cpu().numpy()), dtype=torch.float32)) W=sum((float(qi) if mode=='cut' else 1/3)*w for qi,w in zip(q,Ws)) W=W/(W.sum()+1e-8) net=make_model('cnn_small', d['input_shape'], d['out_dim']) device='cuda' if torch.cuda.is_available() else 'cpu' try: net=net.to(device); x=d['xtr'].to(device); y=d['ytr'].to(device); W=W.to(device) opt=torch.optim.Adam(net.parameters(),lr=lr) for _ in range(epochs): net.train(); perm=torch.randperm(len(x),device=device) for i in range(0,len(x),64): ix=perm[i:i+64]; logits=net(x[ix]); sup=F.cross_entropy(logits,y[ix]) # Full graph penalty is computed on a small 400-sample track. p=F.softmax(net(x),1); diff=(p[:,None,:]-p[None,:,:]).pow(2).sum(-1) loss=sup + 0.15*(W*diff).sum()/len(x) opt.zero_grad(); loss.backward(); opt.step() net.eval() with torch.no_grad(): pred=net(d['xte'].to(device)).argmax(1).cpu(); metric=float((pred!=d['yte']).float().mean()) out={'metric':metric,'cuts':cuts.tolist(),'q':q.tolist(),'graph_reg':float((W*diff).sum().detach().cpu())} except RuntimeError: # deterministic CPU fallback random.seed(seed); np.random.seed(seed); torch.manual_seed(seed) net=make_model('cnn_small', d['input_shape'], d['out_dim']) W=W.cpu(); x=d['xtr']; y=d['ytr']; opt=torch.optim.Adam(net.parameters(),lr=lr) for _ in range(epochs): p=F.softmax(net(x),1); sup=F.cross_entropy(net(x),y); diff=(p[:,None,:]-p[None,:,:]).pow(2).sum(-1) loss=sup+0.15*(W*diff).sum()/len(x); opt.zero_grad(); loss.backward(); opt.step() with torch.no_grad(): metric=float((net(d['xte']).argmax(1)!=d['yte']).float().mean()) out={'metric':metric,'cuts':cuts.tolist(),'q':q.tolist(),'graph_reg':float((W*diff).sum())} return (out['metric'],out) if return_model else out['metric'] def main(): # Cheap numerical verification of the claimed scale invariance and exponential ordering. 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) math_check={'scale_error':abs(cut_ratio(W,y)-cut_ratio(7.3*W,y)),'q_order':bool(np.exp(-10*0)