import sys, json, math, random from pathlib import Path import numpy as np import torch import torch.nn as nn sys.path.insert(0, '/home/maxwelhelp/all/math2nn') from bench import get_dataset, make_model, evaluate, sweep_baseline, make_report EPOCHS=10; NPOP=4; BATCH=128 LRS=[1e-3,3e-3,1e-2] BASE_GRID=[{'lr':lr,'weight_decay':wd} for lr in LRS for wd in (0.0,1e-4)] # Same lr union; interaction is the only method difference. IDEA_GRID=[{'lr':lr,'alpha':a,'normalized':norm} for lr in LRS for a,norm in ((0.0,True),(0.1,True),(0.1,False))] def seed_all(s): random.seed(s); np.random.seed(s); torch.manual_seed(s) if torch.cuda.is_available(): torch.cuda.manual_seed_all(s) def flat(m): return torch.cat([p.detach().reshape(-1) for p in m.parameters()]) def put(m,v): k=0 with torch.no_grad(): for p in m.parameters(): p.copy_(v[k:k+p.numel()].view_as(p)); k+=p.numel() def grad(m): return torch.cat([(p.grad if p.grad is not None else torch.zeros_like(p)).reshape(-1) for p in m.parameters()]) def population_train(seed, cfg, n_particles=NPOP, epochs=EPOCHS, weight_decay=0.0, want_sig=False, device=None): seed_all(seed); d=get_dataset('tabular',seed,n_train=800,n_test=400) device=device or ('cuda' if torch.cuda.is_available() else 'cpu') try: ms=[make_model('mlp_tiny',d['input_shape'],d['out_dim']).to(device) for _ in range(n_particles)] x,y=d['xtr'].to(device),d['ytr'].to(device); lossf=nn.MSELoss() # independent random starts are essential for an extreme-search population opt=[torch.optim.Adam(m.parameters(),lr=cfg['lr'],weight_decay=weight_decay) for m in ms] with torch.no_grad(): init=float(torch.stack([lossf(m(x),y) for m in ms]).mean()) target=init*0.70; first=None; ratios=[]; hist=[] for ep in range(epochs): perm=torch.randperm(len(x),device=device); losses=[] for q in range(0,len(x),BATCH): ix=perm[q:q+BATCH]; gs=[] for m,o in zip(ms,opt): m.train(); o.zero_grad(set_to_none=True); z=lossf(m(x[ix]),y[ix]); z.backward(); gs.append(grad(m)) th=torch.stack([flat(m) for m in ms]); disp=th[None]-th[:,None] psi=disp/(disp.norm(dim=2,keepdim=True).clamp_min(1e-8)); ii=torch.arange(n_particles,device=device); psi[ii,ii]=0 force=cfg.get('alpha',0.0)*psi.sum(1) if cfg.get('normalized',True) and n_particles>1: force/=n_particles-1 gg=torch.stack(gs); update=[-g+f for g,f in zip(gg,force)] if cfg.get('alpha',0.0): ratios.append(float(force.norm(dim=1).mean()/(gg.norm(dim=1).mean()+1e-12))) # Adam update with interaction added to the gradient direction. for m,o,u in zip(ms,opt,update): o.zero_grad(set_to_none=True) put(m,flat(m)+cfg['lr']*u/(u.square().sqrt()+1e-8)) losses.extend([float(lossf(m(x[ix]),y[ix]).detach()) for m in ms]) with torch.no_grad(): full=torch.stack([lossf(m(x),y) for m in ms]) hist.append(float(full.min())) if first is None and float(full.min())<=target: first=ep+1 with torch.no_grad(): xt,yt=d['xte'].to(device),d['yte'].to(device) test=[float(lossf(m(xt),yt)) for m in ms] return {'metric':min(test),'first_hit_epoch':first or epochs+1,'force_ratio':float(np.mean(ratios)) if ratios else 0.0,'history':hist} except Exception: if device=='cuda': torch.cuda.empty_cache(); return population_train(seed,cfg,n_particles,epochs,weight_decay,want_sig,'cpu') raise def baseline_fn(c): return lambda s: population_train(s,{'lr':c['lr'],'alpha':0.0,'normalized':True},weight_decay=c['weight_decay'])['metric'] def idea_fn(c): return lambda s: population_train(s,c)['metric'] def main(): base=sweep_baseline(baseline_fn,BASE_GRID) trials=[] for c in IDEA_GRID: trials.append((c,evaluate(idea_fn(c)))) best_cfg,best=min(trials,key=lambda z:z[1]['mean']) # Re-test the NN-scale prediction on trained populations; target is fixed relative to each run's initial loss. sig=[] for n in (2,4,8): oo=[population_train(s,best_cfg,n_particles=n,want_sig=True) for s in (0,1,2,3)] sig.append({'N':n,'mean_test_metric':float(np.mean([o['metric'] for o in oo])),'mean_first_hit_epoch':float(np.mean([o['first_hit_epoch'] for o in oo])),'mean_force_to_gradient':float(np.mean([o['force_ratio'] for o in oo]))}) slope=float(np.polyfit(np.log([z['N'] for z in sig]),np.log([z['mean_first_hit_epoch'] for z in sig]),1)[0]) mechanism={'prediction':'unnormalized coherent force should yield algebraic first-hit acceleration (ideal slope -1), unlike normalized bounded force','observed_NN_scale':sig,'observed_loglog_first_hit_slope':slope,'confirmed':bool(best_cfg['alpha']>0 and not best_cfg['normalized'] and slope< -0.3)} rep=make_report('tabular','mlp_tiny',base,best,{'prediction':'unnormalized coherent interaction','best_cfg':best_cfg,'mechanism_signature':mechanism}) rep['idea_sweep']=[{'cfg':c,'mean':r['mean'],'per_seed':r['per_seed']} for c,r in trials] rep['equal_budget']={'baseline':'4 independent Adam particles','idea':'4 interacting Adam particles','epochs_per_particle':EPOCHS,'batch':BATCH} Path('bench_report.json').write_text(json.dumps(rep,indent=2)); print(json.dumps(rep,indent=2)) if __name__=='__main__': main()