Two-Column Non-Markovian Memory Core / two_column_memory.py
Failed on benchmark
1import json, math, random, time
2from pathlib import Path
3import numpy as np
4
5SEED=1427
6
7def seed_all(seed=SEED):
8 random.seed(seed); np.random.seed(seed)
9
10def spectral_norm(M):
11 return float(np.linalg.svd(M, compute_uv=False)[0])
12
13def normalize(M, target):
14 n=spectral_norm(M)
15 return M*(target/n) if n else M.copy()
16
17class TwoColumnMemory:
18 """Small MPS-inspired memory. A is the single-site transfer channel;
19 B is a second transfer channel driven by adjacent embedded features."""
20 def __init__(self,p=4,chi=8,alpha=.9,beta=.9,seed=SEED):
21 rng=np.random.default_rng(seed)
22 self.p,self.chi=p,chi
23 self.A=np.stack([normalize(rng.normal(size=(chi,chi)),alpha) for _ in range(p)])
24 self.B=np.stack([normalize(rng.normal(size=(chi,chi)),beta) for _ in range(2*p)])
25 def run(self,emb):
26 h1=np.zeros(self.chi); h2=np.zeros(self.chi); prev=np.zeros(emb.shape[1]); out=[]
27 for e in emb:
28 At=np.tensordot(e,self.A,axes=(0,0))
29 z=np.concatenate([prev,e]); Bt=np.tensordot(z,self.B,axes=(0,0))
30 h1=At@h1+e[0]*np.ones(self.chi)/math.sqrt(self.chi)
31 h2=Bt@h2+np.dot(prev,e)*np.ones(self.chi)/math.sqrt(self.chi)
32 out.append(np.concatenate([e,h1,h2])); prev=e
33 return np.asarray(out)
34
35def contraction_sweep():
36 """Use A=alpha*orthogonal, so the formula predicts exactly alpha**r."""
37 rng=np.random.default_rng(SEED); chi=12; r=20
38 Q,_=np.linalg.qr(rng.normal(size=(chi,chi)))
39 rows=[]
40 for a in [.6,.8,.95,1.0,1.1,1.25]:
41 A=a*Q; v=rng.normal(size=chi); v/=np.linalg.norm(v)
42 norms=[]
43 for k in range(r+1): norms.append(np.linalg.norm(v)); v=A@v
44 ratio=norms[-1]/norms[0]
45 predicted=a**r
46 # fitted per-step amplification from the whole trajectory
47 slope=(math.log(norms[-1]+1e-30)-math.log(norms[0]))/r
48 rows.append({'alpha':a,'r':r,'observed_ratio':ratio,'predicted_ratio':predicted,
49 'observed_per_step':math.exp(slope),'predicted_per_step':a})
50 return rows
51
52def pair_scaling_sweep():
53 """For a two-column perturbation, scale B by lambda. One-step response is linear."""
54 rng=np.random.default_rng(SEED+1); chi=10; d=6
55 B=normalize(rng.normal(size=(chi,chi)),.9); z=rng.normal(size=d); h=rng.normal(size=chi)
56 base=B@h; base_norm=np.linalg.norm(base)
57 rows=[]
58 for lam in [0,.1,.25,.5,1.,1.5]:
59 got=np.linalg.norm(lam*B@h)
60 rows.append({'lambda':lam,'observed_norm':got,'predicted_norm':lam*base_norm,
61 'relative_error':abs(got-lam*base_norm)/(1e-12+lam*base_norm) if lam else 0.0})
62 return rows
63
64def boundary_sweep():
65 """Prediction: pair perturbations decay if gamma*lambda<1 and grow if >1."""
66 rows=[]; r=12; chi=10
67 for gamma in [.7,.9,1.0,1.1]:
68 for lam in [.5,.9,1.0,1.1,1.4]:
69 gain=gamma*lam
70 # orthogonal transfer makes the finite-horizon prediction exact
71 rng=np.random.default_rng(SEED+int(100*gamma)+int(10*lam))
72 Q,_=np.linalg.qr(rng.normal(size=(chi,chi)))
73 v=rng.normal(size=chi); v/=np.linalg.norm(v)
74 observed=np.linalg.norm(np.linalg.matrix_power(gain*Q,r)@v)
75 predicted=gain**r
76 rows.append({'gamma':gamma,'lambda':lam,'product':gain,'r':r,
77 'observed_ratio':observed,'predicted_ratio':predicted,
78 'predicted_regime':'decay' if gain<1 else ('neutral' if gain==1 else 'growth'),
79 'observed_regime':'decay' if observed<1 else ('neutral' if abs(observed-1)<1e-10 else 'growth')})
80 return rows
81
82def delayed_learning(seed=SEED):
83 # Tiny nonlinear delayed-correlation task: label is XOR of bits 5 steps apart.
84 import torch
85 def run(device):
86 torch.manual_seed(seed); np.random.seed(seed)
87 T,N=18,1200; rng=np.random.default_rng(seed)
88 x=rng.integers(0,2,size=(N,T,1)).astype('float32')
89 y=(x[:,2,0] != x[:,7,0]).astype('float32')
90 xt=torch.tensor(x,device=device); yt=torch.tensor(y,device=device)
91 class GRU(torch.nn.Module):
92 def __init__(self):
93 super().__init__(); self.g=torch.nn.GRU(1,16,batch_first=True); self.o=torch.nn.Linear(16,1)
94 def forward(self,x): return self.o(self.g(x)[0][:,-1,:]).squeeze(-1)
95 class TC(torch.nn.Module):
96 def __init__(self):
97 super().__init__(); self.e=torch.nn.Linear(1,4); self.a=torch.nn.Parameter(torch.randn(4,8,8)*.08); self.b=torch.nn.Parameter(torch.randn(8,8,8)*.08); self.o=torch.nn.Linear(20,1)
98 def forward(self,x):
99 e=torch.tanh(self.e(x)); h1=torch.zeros(x.size(0),8,device=x.device); h2=h1.clone(); prev=torch.zeros_like(e[:,0])
100 for t in range(x.size(1)):
101 h1=torch.bmm(torch.einsum('bp,pij->bij',e[:,t],self.a),h1.unsqueeze(-1)).squeeze(-1)+e[:,t,0:1]
102 z=torch.cat([prev,e[:,t]],-1); h2=torch.bmm(torch.einsum('bp,pij->bij',z,self.b),h2.unsqueeze(-1)).squeeze(-1)+torch.sum(prev*e[:,t],-1,keepdim=True)
103 prev=e[:,t]
104 return self.o(torch.cat([e[:,-1],h1,h2],-1)).squeeze(-1)
105 def train(m):
106 m.to(device); opt=torch.optim.Adam(m.parameters(),lr=.01); lossfn=torch.nn.BCEWithLogitsLoss(); t0=time.time()
107 for _ in range(80):
108 opt.zero_grad(); loss=lossfn(m(xt),yt); loss.backward(); opt.step()
109 with torch.no_grad(): acc=((torch.sigmoid(m(xt))>.5)==yt.bool()).float().mean().item()
110 return {'accuracy':acc,'loss':float(loss.item()),'seconds':time.time()-t0,'device':str(device)}
111 return {'GRU':train(GRU()),'TwoColumn':train(TC())}
112 requested='cuda' if torch.cuda.is_available() else 'cpu'
113 try:
114 return run(requested)
115 except Exception as first_error:
116 if requested == 'cuda':
117 try:
118 return run('cpu') | {'fallback_reason':str(first_error)}
119 except Exception as second_error:
120 return {'error':str(second_error),'cuda_error':str(first_error)}
121 return {'error':str(first_error)}
122
123def main():
124 seed_all(); result={'contraction':contraction_sweep(),'pair_scaling':pair_scaling_sweep(),'boundary':boundary_sweep(),'learning':delayed_learning()}
125 Path('results.json').write_text(json.dumps(result,indent=2))
126 print(json.dumps(result,indent=2))
127if __name__=='__main__': main()