Recurrence-to-Latent Cycling Regularizer / experiment.py

Mechanism failed

Raw ⬇ ZIP
  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()