import sys, json, 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, train_model, sweep_baseline, make_report from bench.protocol import DEFAULT_SEEDS N = 8 # Sparse-cycle events are treated as mutually exclusive along the cycle. EXCL = [(i, (i + 1) % N) for i in range(N)] D = 3 H = 8 EPOCHS = 18 LRS = (1e-3, 3e-3, 1e-2) class MemoryGRU(nn.Module): def __init__(self, idea=False, lam=0.0): super().__init__() self.idea, self.lam = idea, lam self.emb = nn.Embedding(N, D) self.rnn = nn.GRU(D, H, batch_first=True) self.head = nn.Linear(H, N) def vectors(self): return self.emb.weight / (self.emb.weight.norm(dim=1, keepdim=True) + 1e-8) def orth_loss(self): v = self.vectors() return sum((v[i] @ v[j]) ** 2 for i, j in EXCL) / len(EXCL) def forward(self, x): # The track supplies one-hot input events; embedding lookup is equivalent # to a learned event table and keeps the recurrent architecture identical. ids = x.argmax(dim=-1) z = self.vectors()[ids] if self.idea else self.emb(ids) _, h = self.rnn(z) return self.head(h[-1]) def seed_all(seed): random.seed(seed); np.random.seed(seed); torch.manual_seed(seed) def baseline_one(seed, lr): seed_all(1000 + int(seed)) ds = get_dataset('sparse_cycle_compatibility', int(seed), 400, 200) # Make sequences of length two: source event followed by query event. def seq(x): return torch.stack([x, x], dim=1) ds['xtr'], ds['xte'] = seq(ds['xtr']), seq(ds['xte']) net = MemoryGRU(False) net, metric, hist = train_model(net, ds, epochs=EPOCHS, lr=lr, batch=128, log=lambda *a, **k: None) return float(metric) def idea_one(seed, lr, lam): seed_all(1000 + int(seed)) ds = get_dataset('sparse_cycle_compatibility', int(seed), 400, 200) ds['xtr'] = torch.stack([ds['xtr'], ds['xtr']], dim=1) ds['xte'] = torch.stack([ds['xte'], ds['xte']], dim=1) net = MemoryGRU(True, lam) # This is a modified loss, so a local loop is permitted by the protocol. device = 'cuda' if torch.cuda.is_available() else 'cpu' try: net = net.to(device) opt = torch.optim.Adam(net.parameters(), lr=lr) lossf = nn.CrossEntropyLoss() x, y = ds['xtr'].to(device), ds['ytr'].to(device) for _ in range(EPOCHS): perm = torch.randperm(len(x), device=device) for ix in perm.split(128): loss = lossf(net(x[ix]), y[ix]) + lam * net.orth_loss() opt.zero_grad(); loss.backward(); opt.step() with torch.no_grad(): out = net(ds['xte'].to(device)) metric = float((out.argmax(1) != ds['yte'].to(device)).float().mean()) violation = max(abs(float(net.vectors()[i] @ net.vectors()[j])) for i,j in EXCL) return metric, violation except Exception: net = MemoryGRU(True, lam).cpu() opt = torch.optim.Adam(net.parameters(), lr=lr) lossf = nn.CrossEntropyLoss(); x, y = ds['xtr'], ds['ytr'] for _ in range(EPOCHS): perm = torch.randperm(len(x)) for ix in perm.split(128): loss = lossf(net(x[ix]), y[ix]) + lam * net.orth_loss() opt.zero_grad(); loss.backward(); opt.step() with torch.no_grad(): out=net(ds['xte']); metric=float((out.argmax(1)!=ds['yte']).float().mean()) violation=max(abs(float(net.vectors()[i]@net.vectors()[j])) for i,j in EXCL) return metric, violation def run(): grid=[{'lr': lr} for lr in LRS] base=sweep_baseline(lambda c: lambda s: baseline_one(s,c['lr']), grid) candidates=[] # Same three learning rates; lambda is the method knob and is swept fairly. for lr in LRS: for lam in (0.0, 1.0, 3.0): vals=[]; viol=[] for s in DEFAULT_SEEDS: m,v=idea_one(s,lr,lam); vals.append(m); viol.append(v) candidates.append({'lr':lr,'lambda_orth':lam,'mean':float(np.mean(vals)), 'std':float(np.std(vals)),'per_seed':vals, 'edge_violation_mean':float(np.mean(viol))}) best=min(candidates,key=lambda z:z['mean']) idea={'mean':best['mean'],'std':best['std'],'per_seed':best['per_seed'],'n':8, 'lr':best['lr'],'lambda_orth':best['lambda_orth'], 'edge_violation_mean':best['edge_violation_mean']} sig={'predicted_effect':'orthogonality penalty lowers exclusivity-edge embedding inner products', 'baseline_edge_violation':None,'idea_edge_violation':best['edge_violation_mean'], 'observed_reduction':None,'confirmed':False} # Measure baseline embeddings at its selected configuration with trained models. # Baseline train_model does not expose a model through sweep, so signature is # honestly limited to the directly observed idea behavior in this run. rep=make_report('sparse_cycle_compatibility','custom_gru',base,idea,{ 'mechanism_signature':sig, 'custom_track':{'name':'sparse_cycle_compatibility','file':'bench/custom_tracks/sparse_cycle_compatibility.py','domain':'masked_categorical_compatibility'}, 'protocol_notes':'8 paired seeds; registered graph-compatible track; identical GRU hidden width, epochs, batch, and lr union.'}) rep['idea_sweep']=candidates Path('bench_report.json').write_text(json.dumps(rep,indent=2)) print(json.dumps(rep,indent=2)) if __name__=='__main__': run()