Orthogonal-Rank Contextual Memory / experiment.py
Failed on benchmark
1import json, random, time
2from itertools import combinations
3import numpy as np
4
5# Explicit orthogonality graph: vertices are representatives of nonzero {-1,0,1}^3 vectors.
6V = np.array([(0,0,1),(0,1,-1),(0,1,0),(0,1,1),(1,-1,-1),(1,-1,0),
7 (1,-1,1),(1,0,-1),(1,0,0),(1,0,1),(1,1,-1),(1,1,0),(1,1,1)], dtype=np.float32)
8N = len(V)
9E = [(i,j) for i in range(N) for j in range(i) if np.dot(V[i], V[j]) == 0]
10
11# Small exact backtracking coloring check.
12def colorable(k):
13 adj = [set() for _ in range(N)]
14 for i,j in E: adj[i].add(j); adj[j].add(i)
15 c = [-1]*N
16 def go(done):
17 if done == N: return True
18 u = max((i for i in range(N) if c[i] < 0), key=lambda i: len(adj[i]))
19 for col in range(k):
20 if all(c[w] != col for w in adj[u]):
21 c[u] = col
22 if go(done+1): return True
23 c[u] = -1
24 return False
25 return go(0)
26
27def graph_check():
28 gram = V @ V.T
29 edge_vals = np.array([gram[i,j] for i,j in E])
30 # Unit-normalized vectors are the orthogonal-rank witness.
31 U = V / np.linalg.norm(V, axis=1, keepdims=True)
32 ug = U @ U.T
33 violation = max(abs(ug[i,j]) for i,j in E)
34 clique = max((len(C) for r in range(1,N+1) for C in combinations(range(N),r)
35 if all(((a,b) in E or (b,a) in E) for a,b in combinations(C,2))), default=0)
36 return {'vertices':N, 'edges':len(E), 'explicit_dimension':3,
37 'edge_dot_max_raw':float(max(abs(edge_vals))),
38 'edge_violation_normalized':float(violation),
39 'clique_lower_bound_xi':clique,
40 'colorable_3':colorable(3), 'colorable_4':colorable(4)}
41
42# Dataset: retain the first event through distractors and decide whether the final
43# event is exclusive with it. This is a graph-defined relational language.
44def make_data(num, T, seed):
45 rng = np.random.default_rng(seed)
46 # A learnable graph-walk-style promise language: the first event is the
47 # context, fixed neutral distractors follow, and the final event is queried.
48 # The target remains exactly edge compatibility with the initial event.
49 x = np.zeros((num,T), dtype=np.int64)
50 x[:,0] = rng.integers(0,N,size=num)
51 x[:,-1] = rng.integers(0,N,size=num)
52 edge = np.zeros((N,N), dtype=np.float32)
53 for i,j in E: edge[i,j]=edge[j,i]=1
54 y = 1.0 - edge[x[:,0], x[:,-1]]
55 return x, y.astype(np.float32)
56
57
58import torch
59import torch.nn as nn
60
61def train_one(name, width, orth_lambda, seed, device):
62 torch.manual_seed(seed); np.random.seed(seed); random.seed(seed)
63 class Recognizer(nn.Module):
64 def __init__(self):
65 super().__init__()
66 self.emb = nn.Embedding(N,width)
67 self.gru = nn.GRU(width,width,batch_first=True)
68 self.out = nn.Linear(width,1)
69 def forward(self,x):
70 h,_ = self.gru(self.emb(x))
71 return self.out(h[:,-1]).squeeze(-1)
72 def orth_loss(self):
73 u = self.emb.weight/(self.emb.weight.norm(dim=1,keepdim=True)+1e-8)
74 dots = (u @ u.T)
75 return sum(dots[i,j]**2 for i,j in E) / len(E)
76 try:
77 model=Recognizer().to(device)
78 opt=torch.optim.Adam(model.parameters(),lr=3e-3)
79 xb,yb=make_data(4000,16,seed+100)
80 xt,yt=make_data(2000,64,seed+200)
81 xb=torch.tensor(xb,device=device); yb=torch.tensor(yb,device=device)
82 xt=torch.tensor(xt,device=device); yt=torch.tensor(yt,device=device)
83 loss_fn=nn.BCEWithLogitsLoss()
84 t0=time.time()
85 for epoch in range(90):
86 perm=torch.randperm(len(xb),device=device)
87 for ix in perm.split(128):
88 loss=loss_fn(model(xb[ix]),yb[ix])
89 if orth_lambda: loss=loss+orth_lambda*model.orth_loss()
90 opt.zero_grad(); loss.backward(); opt.step()
91 with torch.no_grad():
92 model.eval()
93 def score(x,y):
94 p=(torch.sigmoid(model(x))>.5).float()
95 return float((p==y).float().mean()), float(loss_fn(model(x),y))
96 a,l=score(xt,yt); tr,_=score(xb,yb)
97 u=model.emb.weight/(model.emb.weight.norm(dim=1,keepdim=True)+1e-8)
98 gv=max(abs(float((u[i]@u[j]).cpu())) for i,j in E)
99 return {'name':name,'width':width,'train_acc':tr,'long_acc':a,'long_loss':l,
100 'edge_violation':gv,'seconds':time.time()-t0,'params':sum(p.numel() for p in model.parameters())}
101 except Exception as e:
102 raise RuntimeError(f'{name}: {e}') from e
103
104def main():
105 check=graph_check()
106 # CUDA is attempted but any runtime/device problem falls back to CPU.
107 device='cuda' if torch.cuda.is_available() else 'cpu'
108 try:
109 if device=='cuda': torch.zeros(1,device='cuda')
110 except Exception: device='cpu'
111 results=[]
112 # Standard width-4 GRU (classical color lower bound), width-3 controls,
113 # and width-3 with the proposed edge orthogonality penalty. If CUDA has
114 # any allocation/cuDNN failure, rerun the complete matched comparison CPU.
115 configs=[('gru_width_chi',4,0.0),('gru_width_xi',3,0.0),('orthogonal_memory',3,3.0)]
116 try:
117 for args in configs: results.append(train_one(*args,seed=7,device=device))
118 except Exception as exc:
119 if device != 'cpu':
120 device='cpu'; results=[]
121 for args in configs: results.append(train_one(*args,seed=7,device=device))
122 else: raise
123 out={'device':device,'graph_check':check,'results':results,
124 'note':'xi is only upper-bounded by the explicit 3D representation; clique size supplies a matching lower bound here.'}
125 with open('results.json','w') as f: json.dump(out,f,indent=2)
126 print(json.dumps(out,indent=2))
127if __name__=='__main__': main()