Wedge-Positive Tangent Dynamics / wedge_bench.py

Failed on benchmark

Raw ⬇ ZIP
  1import os, sys, json, math, random
  2import numpy as np
  3import torch
  4import torch.nn as nn
  5sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  6from bench import get_dataset, make_model, train_model, evaluate, sweep_baseline, make_report
  7
  8SEEDS=tuple(range(8))
  9# n=8 tangent subspace keeps the compound penalty small enough for the bench.
 10N_TAN=8
 11HIDDEN=64
 12
 13def seed_all(s):
 14    random.seed(s); np.random.seed(s); torch.manual_seed(s)
 15    if torch.cuda.is_available(): torch.cuda.manual_seed_all(s)
 16
 17def pairs(n): return [(i,j) for i in range(n) for j in range(i+1,n)]
 18
 19def additive_compound(A):
 20    n=A.shape[-1]; ps=pairs(n); m=len(ps)
 21    B=A.new_zeros(A.shape[:-2]+(m,m))
 22    for a,(i,j) in enumerate(ps):
 23        B[...,a,a]=A[...,i,i]+A[...,j,j]
 24        for b,(k,l) in enumerate(ps):
 25            if a != b:
 26                # coefficient of E_ij in A^[2] acting on e_k wedge e_l
 27                B[...,a,b] = (A[...,i,k]*(1.0 if j==l else 0.0)
 28                              + A[...,j,l]*(1.0 if i==k else 0.0)
 29                              - A[...,i,l]*(1.0 if j==k else 0.0)
 30                              - A[...,j,k]*(1.0 if i==l else 0.0))
 31    return B
 32
 33def wedge_math_check():
 34    torch.manual_seed(7); A=torch.randn(5,5,dtype=torch.float64)
 35    # finite difference derivative of multiplicative compound, using minors
 36    ps=pairs(5)
 37    def wedge(M):
 38        W=torch.empty(10,10,dtype=M.dtype)
 39        for a,(i,j) in enumerate(ps):
 40            for b,(k,l) in enumerate(ps): W[a,b]=M[i,k]*M[j,l]-M[i,l]*M[j,k]
 41        return W
 42    hs=torch.tensor([.02,.01,.005,.0025],dtype=torch.float64); errs=[]
 43    for h in hs: errs.append(float(torch.linalg.norm(wedge(torch.matrix_exp(h*A))-torch.eye(10)-h*additive_compound(A))))
 44    slope=float(np.polyfit(np.log(hs.numpy()),np.log(errs),1)[0])
 45    return {'identity_error_slope':slope,'expected':2.0,'max_error':max(errs),'passed':abs(slope-2)<.12}
 46
 47def cell8(model, x3, h8):
 48    """First eight rows/columns of the exact GRU Jacobian, analytically."""
 49    W=model.rnn.weight_ih_l0; U=model.rnn.weight_hh_l0
 50    b1=model.rnn.bias_ih_l0; b2=model.rnn.bias_hh_l0
 51    h=torch.cat([h8, torch.zeros(h8.shape[0], HIDDEN-N_TAN, device=h8.device, dtype=h8.dtype)], dim=1)
 52    gi=torch.nn.functional.linear(x3,W,b1); gh=torch.nn.functional.linear(h,U,b2)
 53    ar=gi[:,:HIDDEN]+gh[:,:HIDDEN]; aq=gi[:,HIDDEN:2*HIDDEN]+gh[:,HIDDEN:2*HIDDEN]
 54    r=torch.sigmoid(ar); q=torch.sigmoid(aq)
 55    an=gi[:,2*HIDDEN:3*HIDDEN]+r*gh[:,2*HIDDEN:3*HIDDEN]
 56    n=torch.tanh(an); d=gh[:,2*HIDDEN:3*HIDDEN]
 57    # Batch is one in tangent_penalty; retain differentiability wrt parameters.
 58    dr=r*(1-r)
 59    dq=q*(1-q)
 60    # Each derivative tensor has shape [1,HIDDEN,N_TAN].
 61    Ur=U[:HIDDEN,:N_TAN].unsqueeze(0)
 62    Uq=U[HIDDEN:2*HIDDEN,:N_TAN].unsqueeze(0)
 63    Un=U[2*HIDDEN:3*HIDDEN,:N_TAN].unsqueeze(0)
 64    drdh=dr.unsqueeze(2)*Ur
 65    dqdh=dq.unsqueeze(2)*Uq
 66    dndh=(1-n*n).unsqueeze(2)*(r.unsqueeze(2)*Un+d.unsqueeze(2)*drdh)
 67    dy=(1-q).unsqueeze(2)*dndh + dqdh*(h[:,:HIDDEN].unsqueeze(2)-n.unsqueeze(2))
 68    dy=dy + q.unsqueeze(2)*torch.eye(HIDDEN,device=h.device,dtype=h.dtype)[:,:N_TAN].unsqueeze(0)
 69    return ((1-q)*n+q*h[:,:HIDDEN])[:,:N_TAN], dy[:,:N_TAN,:]
 70
 71def tangent_penalty(model, x):
 72    x3=x[:, -3:][:1]
 73    h=torch.zeros(1,N_TAN,device=x.device)
 74    _, A=cell8(model,x3,h)
 75    A=A[0]
 76    C=additive_compound(A)
 77    off=C-torch.diag(torch.diagonal(C))
 78    neg=torch.relu(-off)
 79    return (neg*neg).mean()+0.02*torch.relu(0.01-off).pow(2).mean(), A.detach(), C.detach()
 80
 81def train_idea(seed,cfg, return_sig=False):
 82    seed_all(seed); ds=get_dataset('dynamics',seed,n_train=200,n_test=100)
 83    model=make_model('rnn_small',ds['input_shape'],ds['out_dim'])
 84    device='cuda' if torch.cuda.is_available() else 'cpu'
 85    try:
 86        model.to(device); opt=torch.optim.Adam(model.parameters(),lr=cfg['lr'],weight_decay=cfg['wd'])
 87        lossf=nn.MSELoss(); xtr,ytr=ds['xtr'].to(device),ds['ytr'].to(device)
 88        for ep in range(cfg['epochs']):
 89            model.train(); perm=torch.randperm(len(xtr),device=device)
 90            for i in range(0,len(xtr),64):
 91                ix=perm[i:i+64]; pred=model(xtr[ix]); task=lossf(pred,ytr[ix])
 92                # Applying at every mini-batch makes the intervention part of training.
 93                pen,_,_=tangent_penalty(model,xtr[ix])
 94                loss=task+cfg['lam']*pen
 95                opt.zero_grad(); loss.backward(); torch.nn.utils.clip_grad_norm_(model.parameters(),5); opt.step()
 96        model.eval()
 97        with torch.no_grad(): metric=float(((model(ds['xte'].to(device))-ds['yte'].to(device))**2).mean())
 98        if return_sig:
 99            with torch.enable_grad(): pen,A,C=tangent_penalty(model,xtr[:1])
100            off=C-torch.diag(torch.diagonal(C)); return metric, {'negative_offdiag_fraction':float((off<0).float().mean()),'mean_negative_magnitude':float(torch.relu(-off).mean()),'penalty':float(pen),'jacobian_norm':float(torch.linalg.norm(A))}
101        return metric
102    except RuntimeError:
103        # CPU fallback for tight CUDA/cuDNN environments.
104        seed_all(seed); os.environ['CUDA_VISIBLE_DEVICES']=''
105        return train_idea_cpu(seed,cfg,return_sig)
106
107def train_idea_cpu(seed,cfg,return_sig=False):
108    # same implementation after disabling CUDA; recursive body is intentionally avoided.
109    old=torch.cuda.is_available
110    ds=get_dataset('dynamics',seed,n_train=200,n_test=100); model=make_model('rnn_small',ds['input_shape'],ds['out_dim'])
111    opt=torch.optim.Adam(model.parameters(),lr=cfg['lr'],weight_decay=cfg['wd']); lossf=nn.MSELoss(); xtr,ytr=ds['xtr'],ds['ytr']
112    for _ in range(cfg['epochs']):
113        for i in range(0,len(xtr),64):
114            task=lossf(model(xtr[i:i+64]),ytr[i:i+64]); pen,_,_=tangent_penalty(model,xtr[i:i+64]); opt.zero_grad(); (task+cfg['lam']*pen).backward(); opt.step()
115    with torch.no_grad(): metric=float(((model(ds['xte'])-ds['yte'])**2).mean())
116    if return_sig:
117        pen,A,C=tangent_penalty(model,xtr[:1]); off=C-torch.diag(torch.diagonal(C)); return metric,{'negative_offdiag_fraction':float((off<0).float().mean()),'mean_negative_magnitude':float(torch.relu(-off).mean()),'penalty':float(pen),'jacobian_norm':float(torch.linalg.norm(A))}
118    return metric
119
120def base_fn(cfg):
121    def run(seed):
122        seed_all(seed); ds=get_dataset('dynamics',seed,n_train=200,n_test=100); m=make_model('rnn_small',ds['input_shape'],ds['out_dim'])
123        _,metric,_=train_model(m,ds,epochs=cfg['epochs'],lr=cfg['lr'],batch=64,weight_decay=cfg['wd'],log=lambda *_:None)
124        return metric
125    return run
126
127def main():
128    check=wedge_math_check(); lrs=[0.0015,0.003,0.006]; wds=[0.0]
129    grid=[{'lr':lr,'wd':wd,'epochs':3} for lr in lrs for wd in [0.0]]
130    base=sweep_baseline(base_fn,grid,seeds=(0,1,2,3))
131    # Same lr/weight-decay union; lambda sweep is the sole method difference.
132    ideas=[]
133    for lr in lrs:
134        for lam in [0.001,0.005,0.02]: ideas.append({'lr':lr,'wd':base['best_cfg']['wd'],'epochs':3,'lam':lam})
135    best=None; rows=[]
136    for cfg in ideas:
137        r=evaluate(lambda s,cfg=cfg: train_idea(s,cfg),seeds=SEEDS); rows.append({'cfg':cfg,'mean':r['mean']})
138        if best is None or r['mean']<best['mean']: best=dict(r); best['cfg']=cfg
139    sigvals=[train_idea(s,best['cfg'],True) for s in SEEDS]; best['per_seed']=[v[0] for v in sigvals]; best['mean']=float(np.mean(best['per_seed'])); best['std']=float(np.std(best['per_seed'])); best['n']=8
140    sig={'predicted':'Metzler violation should decrease in trained recurrent tangent dynamics; lower violation should accompany positive-cone mechanism.','observed_mean':{k:float(np.mean([v[1][k] for v in sigvals])) for k in sigvals[0][1]},'confirmed':False}
141    report=make_report('dynamics','rnn_small',base,best,{'math_sanity':check,'idea_sweep':rows,**sig})
142    with open('bench_report.json','w') as f: json.dump(report,f,indent=2)
143    print(json.dumps(report,indent=2))
144if __name__=='__main__': main()