import sys, json, time 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, reload_custom_tracks, evaluate, sweep_baseline, make_report TRACK = "relational_graph_classification" SEEDS = [0,1,2,3,4,5,6,7] SWEEP_SEEDS = [0,1,2,3] DEVICE = "cuda" if torch.cuda.is_available() else "cpu" def stable_softmax(x): return torch.softmax(x, dim=-1) def rainbow_scores(p): # p: [B,N,N,3], score for each candidate edge and color. n = p.shape[1] out = torch.zeros_like(p) for k in range(n): # For edge ij, use p[i,k] and p[j,k]; diagonal contributions are masked. pik, pjk = p[:, :, k, :].unsqueeze(2), p[:, k, :, :].unsqueeze(1) # r_a = products of the two other colors in both orders. out = out + torch.stack([ pik[...,1]*pjk[...,2] + pik[...,2]*pjk[...,1], pik[...,0]*pjk[...,2] + pik[...,2]*pjk[...,0], pik[...,0]*pjk[...,1] + pik[...,1]*pjk[...,0]], dim=-1) eye = torch.eye(n, device=p.device, dtype=p.dtype)[None,:,:,None] return out * (1.0-eye) class RelationalNet(nn.Module): def __init__(self, idea=False, beta4=0.8, tau=0.7, steps=2): super().__init__() self.idea, self.beta4, self.tau, self.steps = idea, beta4, tau, steps self.node = nn.Linear(1, 24) self.edge = nn.Sequential(nn.Linear(48, 24), nn.Tanh(), nn.Linear(24, 3)) self.rel = nn.ModuleList([nn.Linear(24, 24) for _ in range(3)]) self.head = nn.Sequential(nn.Linear(24, 24), nn.ReLU(), nn.Linear(24, 2)) self.beta = nn.Parameter(torch.tensor([0.08, -0.02, -0.06])) self.beta4_param = nn.Parameter(torch.tensor(float(np.log(np.exp(beta4)-1.0))) if beta4 > 0 else torch.tensor(-8.0)) def forward(self, x, return_aux=False): # x contains an 8x8 adjacency matrix and one scalar node attribute. a, nf = x[:, :, :8], x[:, :, 8:9] h = torch.tanh(self.node(nf)) pair = torch.cat([h[:, :, None, :].expand(-1,-1,8,-1), h[:, None, :, :].expand(-1,8,-1,-1)], dim=-1) logits = self.edge(pair) p0 = stable_softmax(logits) p = p0 if self.idea: b4 = F.softplus(self.beta4_param) for _ in range(self.steps): r = rainbow_scores(p) scores = 2*self.beta[None,None,None,:] + (b4/8.0)*r target = stable_softmax(scores / self.tau) p = 0.5*p + 0.5*target # Dense relation-weighted message passing; adjacency supplies edge existence. msg = 0.0 for c in range(3): msg = msg + p[...,c:c+1] * a[...,None] * self.rel[c](h[:,None,:,:]) z = h + msg.sum(dim=2) out = self.head(z.mean(dim=1)) if return_aux: return out, p, p0 return out def train_one(seed, idea, cfg, capture=False): torch.manual_seed(seed); np.random.seed(seed) ds = get_dataset(TRACK, seed, n_train=400, n_test=200) net = RelationalNet(idea=idea, beta4=cfg["beta4"], tau=cfg["tau"], steps=cfg["steps"]) # Canonical bench-like Adam minibatch loop; graph mechanism changes forward, not optimizer. try: dev = torch.device(DEVICE) except Exception: dev = torch.device("cpu") for attempt in ([dev, torch.device("cpu")] if dev.type == "cuda" else [dev]): try: net = net.to(attempt) xtr, ytr = ds["xtr"].to(attempt), ds["ytr"].to(attempt) opt = torch.optim.Adam(net.parameters(), lr=cfg["lr"], weight_decay=0.0) for ep in range(cfg["epochs"]): net.train(); perm = torch.randperm(len(xtr), device=attempt) for j in range(0,len(xtr),128): ix=perm[j:j+128]; loss=F.cross_entropy(net(xtr[ix]),ytr[ix]) opt.zero_grad(); loss.backward(); opt.step() net.eval() with torch.no_grad(): xt=ds["xte"].to(attempt); yt=ds["yte"].to(attempt) pred=net(xt); metric=float((pred.argmax(1)!=yt).float().mean()) if capture: _, p, p0 = net(xt, True) return metric, p.detach().cpu(), p0.detach().cpu() return metric except RuntimeError: if attempt.type == "cpu": raise net = RelationalNet(idea=idea, beta4=cfg["beta4"], tau=cfg["tau"], steps=cfg["steps"]) def math_checks(): torch.manual_seed(3); p=torch.softmax(torch.randn(2,8,8,3),-1) r=rainbow_scores(p); simplex=float((p.sum(-1)-1).abs().max()) # beta4=0 update is exactly unary softmax, independent of p. beta=torch.tensor([.1,-.03,-.07]); tau=.7 u=torch.softmax((2*beta/tau),-1) z=torch.softmax((2*beta[None,None,None,:]/tau).expand_as(p),-1) return {"simplex_max_error": simplex, "beta4_zero_max_error": float((z-u).abs().max()), "rainbow_score_nonnegative": bool(float(r.min()) >= -1e-7)} def run(): reload_custom_tracks() base_grid=[{"lr":lr,"epochs":18,"beta4":0.0,"tau":tau,"steps":0} for lr in [1e-3,3e-3,1e-2] for tau in [.5,1.0]] # Same union of lr and all central router knobs on both sides. idea_grid=[{"lr":lr,"epochs":18,"beta4":b,"tau":t,"steps":s} for lr in [1e-3,3e-3,1e-2] for b,t,s in [(0.4,.5,2),(0.8,.7,2),(1.2,1.0,3)]] t0=time.time() base=sweep_baseline(lambda cfg: lambda seed: train_one(seed,False,cfg), base_grid, seeds=SWEEP_SEEDS) # Evaluate all three idea settings on full paired seeds; choose by mean, while baseline has all lr/tau. idea_runs=[] for cfg in idea_grid: rr=evaluate(lambda seed,cfg=cfg: train_one(seed,True,cfg), seeds=SEEDS) idea_runs.append((rr["mean"],cfg,rr)) idea_best=sorted(idea_runs,key=lambda z:z[0])[0] # Mechanism signature uses trained model predictions, not synthetic arithmetic. m0=train_one(0,True,{"lr":idea_best[1]["lr"],"epochs":18,"beta4":0.0,"tau":idea_best[1]["tau"],"steps":2},True) mb=train_one(0,True,idea_best[1],True) def stats(q): q=q.numpy(); ent=float((-q*np.log(np.maximum(q,1e-9))).sum(-1).mean()) # expected rainbow probability over distinct triples, excluding diagonal edges vals=[] for i in range(8): for j in range(i+1,8): for k in range(j+1,8): vals.append(sum(q[:,i,j,a]*(q[:,i,k,b]*q[:,j,k,c]+q[:,i,k,c]*q[:,j,k,b]) for a in range(3) for b in range(3) for c in range(3) if len({a,b,c})==3).mean()) return ent,float(np.mean(vals)) e0,r0=stats(m0[1]); eb,rb=stats(mb[1]) sig={"trained_beta4_zero_change":float(np.abs(m0[1].numpy()-m0[2].numpy()).mean()), "trained_rainbow_density_beta4_0":r0,"trained_rainbow_density_beta4_best":rb, "trained_entropy_beta4_0":e0,"trained_entropy_beta4_best":eb, "prediction":"beta4=0 removes motif update; positive beta4 changes trained routing", "confirmed": bool(float(np.abs(m0[1].numpy()-m0[2].numpy()).mean()) < 1e-4 and abs(rb-r0)>1e-5)} rep=make_report(TRACK,"local_relational_gnn",base,idea_best[2],{"track_structure":"dense attributed relational graphs; matched graph backbone", "signature":sig}) rep["idea_sweep"]=[{"cfg":c,"mean":m} for m,c,_ in idea_runs] rep["runtime_sec"]=time.time()-t0; rep["math_checks"]=math_checks() Path("bench_report.json").write_text(json.dumps(rep,indent=2)) print(json.dumps(rep,indent=2)) if __name__=="__main__": run()