Distinct-kink complexity regularizer and merger / kink_experiment.py

Failed on benchmark

Raw ⬇ ZIP
  1import json, math, os, time
  2import numpy as np
  3import torch
  4import torch.nn as nn
  5
  6SEED = 559
  7np.random.seed(SEED); torch.manual_seed(SEED)
  8try:
  9    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
 10except Exception:
 11    device = torch.device("cpu")
 12
 13
 14def forward(x, w, b, a, v, c):
 15    return torch.relu(x @ w.T + b) @ a + x @ v + c
 16
 17
 18def canonical_merge(w, b, a, tol=1e-7):
 19    """Exact/tolerance merge, preserving orientation (u,beta), never sign-flips."""
 20    w = np.asarray(w, dtype=np.float64); b=np.asarray(b, dtype=np.float64); a=np.asarray(a, dtype=np.float64)
 21    groups=[]; zero=[]
 22    for j in range(len(a)):
 23        r=np.linalg.norm(w[j])
 24        if r <= tol:
 25            zero.append(j); continue
 26        u=w[j]/r; beta=b[j]/r; alpha=a[j]*r
 27        found=None
 28        for g in groups:
 29            if np.max(np.abs(np.r_[u,beta]-np.r_[g['u'],g['beta']])) <= tol:
 30                found=g; break
 31        if found is None:
 32            groups.append({'u':u.copy(),'beta':float(beta),'alpha':float(alpha)})
 33        else: found['alpha'] += float(alpha)
 34    if not groups:
 35        return np.zeros((0,w.shape[1])), np.zeros(0), np.zeros(0), zero
 36    W=np.array([g['u'] for g in groups]); B=np.array([g['beta'] for g in groups]); A=np.array([g['alpha'] for g in groups])
 37    return W,B,A,zero
 38
 39
 40def jacobian_norm(model_tuple, x):
 41    x=x.detach().clone().requires_grad_(True)
 42    y=forward(x,*model_tuple)
 43    grads=torch.autograd.grad(y.sum(),x)[0]
 44    return float(torch.linalg.vector_norm(grads,dim=1).mean())
 45
 46
 47def main():
 48    d=2; n=600
 49    x=torch.randn(n,d,device=device)
 50    y=(torch.sin(1.7*x[:,0])+0.35*x[:,1]**2-0.2*x[:,0]*x[:,1]+0.08*torch.randn(n,device=device))
 51    xv=torch.randn(400,d,device=device)
 52    yv=(torch.sin(1.7*xv[:,0])+0.35*xv[:,1]**2-0.2*xv[:,0]*xv[:,1])
 53    # Train a small ordinary network; this is the compact representation to be duplicated.
 54    m=24
 55    w=nn.Parameter(0.8*torch.randn(m,d,device=device)); b=nn.Parameter(torch.zeros(m,device=device))
 56    a=nn.Parameter(0.3*torch.randn(m,device=device)); v=nn.Parameter(torch.zeros(d,device=device)); c=nn.Parameter(torch.zeros((),device=device))
 57    opt=torch.optim.Adam([w,b,a,v,c],lr=0.025)
 58    t0=time.time()
 59    for step in range(900):
 60        opt.zero_grad(); pred=forward(x,w,b,a,v,c); loss=((pred-y)**2).mean(); loss.backward(); opt.step()
 61    train_sec=time.time()-t0
 62    base=tuple(q.detach().clone() for q in (w,b,a,v,c))
 63    # Make a width-48 nominal model by splitting every coefficient into two identical neurons.
 64    wb,bb,ab,vb,cb=base
 65    wr=torch.repeat_interleave(wb,2,dim=0); br=torch.repeat_interleave(bb,2); ar=torch.repeat_interleave(ab/2,2)
 66    dense=(wr,br,ar,vb,cb)
 67    # Merge in CPU canonical coordinates, then evaluate the resulting model.
 68    W,B,A,zero=canonical_merge(wr.cpu().numpy(),br.cpu().numpy(),ar.cpu().numpy(),tol=1e-9)
 69    merged=tuple([torch.tensor(z,dtype=torch.float32,device=device) for z in (W,B,A)])+(vb,cb)
 70    with torch.no_grad():
 71        probe=torch.randn(5000,d,device=device)
 72        yd=forward(probe,*dense); ym=forward(probe,*merged); ycompact=forward(probe,*base)
 73        maxerr=float((yd-ym).abs().max()); rmserr=float(torch.sqrt(((yd-ym)**2).mean()))
 74        val_dense=float(((forward(xv,*dense)-yv)**2).mean()); val_merge=float(((forward(xv,*merged)-yv)**2).mean())
 75        val_comp=float(((forward(xv,*base)-yv)**2).mean())
 76    # Independent algebra check over random neurons, including non-unit scales.
 77    ww=np.random.randn(17,d); bb=np.random.randn(17); aa=np.random.randn(17)
 78    W2,B2,A2,_=canonical_merge(ww,bb,aa,tol=1e-12)
 79    z=torch.randn(1000,d); lhs=torch.relu(z@torch.tensor(ww,dtype=torch.float32).T+torch.tensor(bb,dtype=torch.float32))@torch.tensor(aa,dtype=torch.float32)
 80    rhs=torch.relu(z@torch.tensor(W2,dtype=torch.float32).T+torch.tensor(B2,dtype=torch.float32))@torch.tensor(A2,dtype=torch.float32)
 81    algebra_err=float((lhs-rhs).abs().max())
 82    jac_dense=jacobian_norm(dense,probe[:512]); jac_merge=jacobian_norm(merged,probe[:512])
 83    raw_params=2*m + m + d + 1 # width 24 compact
 84    redundant_params=2*(2*m)+(2*m)+d+1
 85    merged_params=2*m+m+d+1
 86    result={
 87      'device':str(device), 'seed':SEED, 'train_seconds':train_sec,
 88      'math_algebra_max_error':algebra_err,
 89      'constructed_width':2*m, 'merged_width':len(A), 'width_reduction_factor':(2*m)/len(A),
 90      'probe_max_abs_function_error':maxerr, 'probe_rmse_function_error':rmserr,
 91      'val_mse_redundant':val_dense, 'val_mse_merged':val_merge, 'val_mse_compact':val_comp,
 92      'jacobian_mean_norm_redundant':jac_dense, 'jacobian_mean_norm_merged':jac_merge,
 93      'params_redundant':redundant_params, 'params_merged':merged_params,
 94      'note':'The redundant model is an exact coefficient-split reparameterization of a trained compact model; this isolates the claimed merger phenomenon rather than claiming spontaneous duplicate discovery.'
 95    }
 96    os.makedirs('results',exist_ok=True)
 97    with open('results/metrics.json','w') as f: json.dump(result,f,indent=2)
 98    print(json.dumps(result,indent=2))
 99
100if __name__=='__main__':
101    try: main()
102    except Exception as e:
103        if torch.cuda.is_available() and str(device)=='cuda':
104            print('CUDA failed; rerun on CPU:',repr(e)); device=torch.device('cpu'); main()
105        else: raise