import sys, os, json, random import numpy as np sys.path.insert(0, '/home/maxwelhelp/all/math2nn') from bench import get_dataset, make_model, train_model, sweep_baseline, make_report import torch import torch.nn as nn OUT='bench_report.json' 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 base_net(out_dim): # exact bench CNN, reconstructed so the intervention can replace only its readout return nn.Sequential(nn.Conv2d(3,32,3,padding=1),nn.ReLU(),nn.MaxPool2d(2), nn.Conv2d(32,64,3,padding=1),nn.ReLU(),nn.MaxPool2d(2), nn.Conv2d(64,96,3,padding=1),nn.ReLU(),nn.MaxPool2d(2),nn.Flatten(), nn.Linear(96*4*4,128),nn.ReLU(),nn.Linear(128,out_dim)) class DirectionalCNN(nn.Module): def __init__(self, out_dim, epsilon=.35, lam=.01, atoms=4): super().__init__() self.features=nn.Sequential(nn.Conv2d(3,32,3,padding=1),nn.ReLU(),nn.MaxPool2d(2), nn.Conv2d(32,64,3,padding=1),nn.ReLU(),nn.MaxPool2d(2), nn.Conv2d(64,96,3,padding=1),nn.ReLU(),nn.MaxPool2d(2)) # Fixed circular directions correspond to local 4-neighborhood sectors. # The 4x4 post-trunk map supplies 16 local directional samples. yy, xx = torch.meshgrid(torch.arange(4,dtype=torch.float32)-1.5, torch.arange(4,dtype=torch.float32)-1.5, indexing='ij') theta=torch.stack((xx.reshape(-1), yy.reshape(-1)),1) theta=theta/(theta.norm(dim=1,keepdim=True)+1e-8) self.register_buffer('theta',theta) self.atom_logits=nn.Parameter(torch.randn(atoms,2)*.05) self.epsilon=epsilon; self.lam=lam self.head=nn.Sequential(nn.Linear(96*atoms,128),nn.ReLU(),nn.Linear(128,out_dim)) def forward(self,x): h=self.features(x) # B,C,4,4; each pixel is a directional sample # image-plane directional atoms pooled over spatial locations theta=self.theta / (self.theta.norm(dim=1,keepdim=True)+1e-8) atoms=self.atom_logits/(self.atom_logits.norm(dim=1,keepdim=True)+1e-8) W=torch.softmax(theta@atoms.T/self.epsilon,dim=1) # K,n, partition unity c=torch.einsum('bck,kn->bcn',h.flatten(2),W).mean(1) # B,n (C is mixed below) # retain C channels per atom, unlike scalar toy descriptors cf=torch.einsum('bck,kn->bcn',h.flatten(2),W) / h.shape[-1]**2 G=(W.T@W)/W.shape[0] # detach Gram as specified initially; ridge makes solve stable A=G.detach()+self.lam*torch.eye(G.shape[0],device=x.device) z=torch.linalg.solve(A,cf.transpose(1,2)).transpose(1,2) return self.head(z.flatten(1)) def train_one(track, idea, seed, cfg): seed_all(seed) ds=get_dataset(track, seed=seed, n_train=400, n_test=200) if idea: net=DirectionalCNN(ds['out_dim'], epsilon=cfg.get('epsilon',.35), lam=cfg.get('lam',.01)) else: net=base_net(ds['out_dim']) # train_model is canonical; base and idea use identical budget _, metric, hist=train_model(net,ds,epochs=cfg['epochs'],lr=cfg['lr'],batch=128,weight_decay=cfg.get('wd',0.0),log=lambda *_:None) return metric def main(): track='vision'; model='cnn_small' # Union grid: baseline and idea both evaluated at all lr values. Baseline central # knob is pooling/readout itself, and is the fixed standard CNN readout. grid=[{'lr':x,'epochs':8,'epsilon':.25,'lam':.01} for x in (0.001,0.003,0.006)] base_grid=[dict(x) for x in grid] base=sweep_baseline(lambda cfg: lambda s: train_one(track,False,s,cfg),base_grid) # best baseline hyperparameters plus two nearby settings; same 3-point union idea_cfgs=[dict(x) for x in grid] idea_cfgs[1]['epsilon']=.35; idea_cfgs[1]['lam']=.01 vals=[] for cfg in idea_cfgs: vals.append({'cfg':cfg,'res':__import__('bench').evaluate(lambda s:train_one(track,True,s,cfg))}) ib=min(vals,key=lambda q:q['res']['mean']) # Quantitative NN-scale signature: predicted partition and energy inequality measured # on trained models' feature tensors, not an analytical identity. seed_all(0); ds=get_dataset(track,0,n_train=400,n_test=32) m=DirectionalCNN(ds['out_dim']) train_model(m, ds, epochs=8, lr=0.003, batch=128, log=lambda *_:None) m.cpu().eval() with torch.no_grad(): h=m.features(ds['xte'][:16].cpu()); th=m.theta; at=m.atom_logits/(m.atom_logits.norm(dim=1,keepdim=True)+1e-8) W=torch.softmax(th@at.T/m.epsilon,dim=1); G=W.T@W/W.shape[0] cf=torch.einsum('bck,kn->bcn',h.flatten(2),W)/h.shape[-1]**2 z=torch.linalg.solve(G+m.lam*torch.eye(4),cf.transpose(1,2)).transpose(1,2) # Compare projected energy with the original trained feature signal energy. raw=float((h.flatten(2)**2).mean()); proj=float((cf*z).sum()/(h.shape[0]*h.shape[1])) pou=float((W.sum(1)-1).abs().max()); sig={'predicted_pou_error~0':pou,'observed_pooled_energy':raw,'observed_ridge_projected_energy':proj, 'prediction_projected_le_raw':proj<=raw+1e-6,'confirmed':bool(pou<1e-6 and proj<=raw+1e-6)} rep=make_report(track,model,base,ib['res'],{**sig,'idea_sweep':vals, 'structural_match':'vision CNN spatial directional pooling; same task and trained systems'}) with open(OUT,'w') as f: json.dump(rep,f,indent=2) print(json.dumps(rep,indent=2)) if __name__=='__main__': main()