Recurrence-to-Latent Cycling Regularizer / experiment.py
Mechanism failed
1import json, math, time
2import numpy as np
3import torch
4from torch import nn
5
6SEED=2196
7np.random.seed(SEED); torch.manual_seed(SEED)
8try:
9 device=torch.device('cuda' if torch.cuda.is_available() else 'cpu')
10 if device.type=='cuda':
11 torch.cuda.empty_cache()
12 # force a tiny allocation so CUDA errors are caught here
13 torch.zeros(1, device=device)
14except Exception:
15 device=torch.device('cpu')
16
17
18def adjacent_radius(z):
19 return torch.linalg.vector_norm(z[1:]-z[:-1], dim=1).max().item()
20
21def recurrence_features(z, min_gap=2, top_k=8, margin=1e-4):
22 """Cheap upper-triangular recurrence proxy. Lifetime is distance margin
23 between a close-return threshold and the pair's distance; detached."""
24 with torch.no_grad():
25 D=torch.cdist(z,z)
26 n=z.shape[0]; R=adjacent_radius(z)+margin
27 vals=[]
28 # local-in-time close returns, excluding the diagonal
29 for i in range(n):
30 for j in range(i+min_gap,n):
31 d=float(D[i,j])
32 if d < R:
33 # proxy filtration lifetime: threshold minus birth
34 vals.append((R-d,i,j))
35 vals.sort(reverse=True)
36 return [(float(w),i,j) for w,i,j in vals[:top_k]]
37
38def cycle_loss(z, features):
39 out=z.new_zeros(())
40 for w,i,j in features:
41 path=((z[i+1:j+1]-z[i:j])**2).sum()
42 shortcut=((z[j]-z[i])**2).sum()
43 out=out+float(w)*(path+shortcut)
44 return out
45
46def boundary(edges):
47 b={}
48 for a,c,coef in edges:
49 b[a]=b.get(a,0)+coef; b[c]=b.get(c,0)-coef
50 return {k:v for k,v in b.items() if v}
51
52def loop_edges(i,j):
53 # c_Gamma(i,j)-[i,j], oriented i -> ... -> j then j -> i
54 e=[(k,k+1,1) for k in range(i,j)]
55 e.append((j,i,1))
56 return e
57
58class Encoder(nn.Module):
59 def __init__(self):
60 super().__init__(); self.gru=nn.GRU(1,12,batch_first=True); self.proj=nn.Linear(12,3); self.head=nn.Linear(3,2)
61 def forward(self,x):
62 h,_=self.gru(x); z=self.proj(h); return z,self.head(z[:,-1])
63
64def make_data(N=192,n=24):
65 rng=np.random.default_rng(SEED); X=[]; y=[]
66 for k in range(N):
67 cls=k%2; t=np.linspace(0,2*np.pi,n)
68 f=1.0 if cls==0 else 2.0
69 sig=np.sin(f*t+rng.normal(0,.3))+rng.normal(0,.10,n)
70 X.append(sig[:,None]); y.append(cls)
71 p=rng.permutation(N); split=int(.75*N)
72 return (torch.tensor(np.array(X)[p[:split]],dtype=torch.float32),torch.tensor(np.array(y)[p[:split]],dtype=torch.long),torch.tensor(np.array(X)[p[split:]],dtype=torch.float32),torch.tensor(np.array(y)[p[split:]],dtype=torch.long))
73
74def _train_on(reg, dev, epochs=35):
75 torch.manual_seed(SEED + (1 if reg else 0)); np.random.seed(SEED)
76 xa,ya,xb,yb=make_data(); model=Encoder().to(dev); opt=torch.optim.Adam(model.parameters(),lr=.012)
77 xa,ya,xb,yb=[q.to(dev) for q in (xa,ya,xb,yb)]
78 t0=time.perf_counter()
79 for _ in range(epochs):
80 model.train(); opt.zero_grad(); z,logits=model(xa); loss=nn.functional.cross_entropy(logits,ya)
81 if reg:
82 for q in range(min(32,xa.shape[0])):
83 f=recurrence_features(z[q]); loss=loss+0.004*cycle_loss(z[q],f)
84 loss.backward(); opt.step()
85 model.eval()
86 with torch.no_grad(): pred=model(xb)[1].argmax(1); acc=(pred==yb).float().mean().item()
87 return acc, time.perf_counter()-t0
88
89def train(reg, epochs=35):
90 try:
91 return _train_on(reg, device, epochs)
92 except Exception as exc:
93 print(json.dumps({'cuda_fallback':type(exc).__name__}))
94 return _train_on(reg, torch.device('cpu'), epochs)
95
96def main():
97 # deterministic algebraic toy trajectory
98 z=torch.tensor([[0.,0.],[1.,0.],[1.,1.],[0.,1.],[0.,0.]],dtype=torch.float32)
99 maxadj=adjacent_radius(z)
100 b=boundary(loop_edges(0,4))
101 boundary_ok=(b=={})
102 margins=np.array([-0.01,0.0,0.0001,0.25,1.0])
103 D=torch.cdist(z,z).numpy()
104 # Prediction 1: every adjacent path edge is admitted exactly when r>R_gamma.
105 path_ok=[bool(all(D[k,k+1] < maxadj+float(m) for k in range(len(z)-1))) for m in margins]
106 path_predicted=[bool(m>0) for m in margins]
107 # Prediction 2: the first nonadjacent recurrence appears at
108 # m*=min_{j>=i+2} D_ij - R_gamma (strict inequality).
109 nonadj=[D[i,j] for i in range(len(z)) for j in range(i+2,len(z))]
110 predicted_nonadj=float(min(nonadj)-maxadj)
111 recurrence_counts=[int(sum(D[i,j] < maxadj+float(m) for i in range(len(z)) for j in range(i+2,len(z)))) for m in margins]
112 observed_nonadj=float(margins[next(k for k,c in enumerate(recurrence_counts) if c>0)]) if any(recurrence_counts) else None
113 # Persistence/scale prediction: alpha multiplication gives alpha loss.
114 zz=torch.tensor([[0.,0.],[.7,.1],[1.,.8],[.2,1.1],[0.,0.]],dtype=torch.float32)
115 fs=[(0.8,0,4),(0.4,1,3)]
116 base=float(cycle_loss(zz,fs)); scales=[0.,.25,.5,1.,2.]
117 losses=[float(cycle_loss(zz,[(a*w,i,j) for w,i,j in fs])) for a in scales]
118 ratios=[(v/base if base else 0.) for v in losses]
119 # A second prediction: zero selected features => zero regularizer.
120 zero=float(cycle_loss(zz,[]))
121 baseline=train(False); idea=train(True)
122 result={'device':str(device),'math':{
123 'boundary_identity_predicted_zero':0,'boundary_residual':b,'boundary_confirmed':boundary_ok,
124 'R_gamma':maxadj,'path_admission_prediction':'all adjacent edges iff margin > 0',
125 'path_admission_observed':path_ok,'path_admission_predicted':path_predicted,
126 'nonadjacent_transition_prediction':predicted_nonadj,
127 'margins':margins.tolist(),'recurrence_vertex_counts':recurrence_counts,
128 'observed_nonadjacent_transition_margin':observed_nonadj,
129 'scale_prediction':'L(alpha*w)/L(w)=alpha; L(empty)=0','scales':scales,'losses':losses,'ratios':ratios,
130 'zero_feature_loss':zero},
131 'mini_experiment':{'baseline_accuracy':baseline[0],'idea_accuracy':idea[0],'baseline_seconds':baseline[1],'idea_seconds':idea[1]}}
132 print(json.dumps(result,indent=2))
133
134if __name__=='__main__': main()