Hidden-Diffusion Irreversibility Monitor / stage2_bench.py

Failed on benchmark

Raw ⬇ ZIP
  1import sys, json, random
  2import numpy as np
  3import torch
  4import torch.nn as nn
  5
  6sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
  7from bench import get_dataset, make_model, sweep_baseline, make_report
  8
  9SEEDS = tuple(range(8))
 10# Includes every idea learning rate, and sweeps the baseline's weight decay knob too.
 11GRID = [
 12    {'lr': 1e-3, 'weight_decay': 0.0},
 13    {'lr': 1e-3, 'weight_decay': 1e-4},
 14    {'lr': 3e-3, 'weight_decay': 0.0},
 15    {'lr': 3e-3, 'weight_decay': 1e-4},
 16    {'lr': 6e-3, 'weight_decay': 0.0},
 17    {'lr': 6e-3, 'weight_decay': 1e-4},
 18]
 19# Three nearby settings, including the baseline's eventual best lr.
 20IDEA_GRID = [
 21    {'lr': 1e-3, 'weight_decay': 0.0, 'lam': 1e-3},
 22    {'lr': 3e-3, 'weight_decay': 0.0, 'lam': 1e-3},
 23    {'lr': 6e-3, 'weight_decay': 0.0, 'lam': 1e-3},
 24]
 25EPOCHS, BATCH = 6, 128
 26
 27
 28def seed_all(seed):
 29    random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
 30    if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
 31
 32
 33def forward_with_hidden(net, x):
 34    seq = x.view(x.shape[0], -1, 3)
 35    h, _ = net.rnn(seq)
 36    return h, net.head(h[:, -1])
 37
 38
 39def cross_diffusion_monitor(h):
 40    """Empirical OU monitor on a two-dimensional learned latent subspace.
 41
 42    A local drift is fitted from hidden increments and residual covariance gives
 43    D=residual_cov/(2 dt). The regularizer discourages cross diffusion relative
 44    to diagonal diffusion; the task loss remains the primary benchmark metric.
 45    """
 46    if h.shape[0] < 4 or h.shape[1] < 2:
 47        return h.new_zeros(()), h.new_zeros(())
 48    z = h[:, :-1, :2].reshape(-1, 2)
 49    dz = (h[:, 1:, :2] - h[:, :-1, :2]).reshape(-1, 2)
 50    # Regression is detached: the monitor estimates latent dynamics, while the
 51    # covariance penalty remains differentiable through residual increments.
 52    with torch.no_grad():
 53        xtx = z.T @ z + 1e-4 * torch.eye(2, device=z.device)
 54        coef = torch.linalg.solve(xtx, z.T @ dz)
 55    resid = dz - z @ coef.detach()
 56    cov = (resid.T @ resid) / max(1, resid.shape[0] - 1)
 57    d = cov / 2.0
 58    dxy = d[0, 1]
 59    scale = d.diag().mean().detach() + 1e-5
 60    penalty = (dxy / scale).pow(2)
 61    return penalty, dxy.detach()
 62
 63
 64def run_one(seed, cfg, idea, n_train=400, n_test=100, diagnostics=False):
 65    seed_all(seed)
 66    ds = get_dataset('dynamics', seed, n_train=n_train, n_test=n_test)
 67    net = make_model('rnn_small', ds['input_shape'], ds['out_dim'])
 68    device = 'cuda' if torch.cuda.is_available() else 'cpu'
 69    try:
 70        net.to(device)
 71        opt = torch.optim.Adam(net.parameters(), lr=cfg['lr'],
 72                               weight_decay=cfg['weight_decay'])
 73        x, y = ds['xtr'].to(device), ds['ytr'].to(device)
 74        for _ in range(EPOCHS):
 75            net.train()
 76            perm = torch.randperm(len(x), device=device)
 77            for i in range(0, len(x), BATCH):
 78                ind = perm[i:i+BATCH]
 79                h, pred = forward_with_hidden(net, x[ind])
 80                loss = ((pred-y[ind])**2).mean()
 81                if idea:
 82                    reg, _ = cross_diffusion_monitor(h)
 83                    loss = loss + cfg['lam'] * reg
 84                opt.zero_grad(); loss.backward(); opt.step()
 85        net.eval()
 86        with torch.no_grad():
 87            hte, out = forward_with_hidden(net, ds['xte'].to(device))
 88            metric = float(((out-ds['yte'].to(device))**2).mean().cpu())
 89        if not diagnostics:
 90            return metric
 91        with torch.no_grad():
 92            reg, dxy = cross_diffusion_monitor(hte)
 93            out = out[:, 0].detach().cpu().numpy()
 94            hh = hte[:, :, :2].reshape(-1, 2).detach().cpu().numpy()
 95            # Behavior-derived output-spectrum proxy and reverse/forward gap.
 96            centered = out - out.mean()
 97            spectrum0 = float(np.mean(centered**2))
 98            a = hh[:-1]; b = hh[1:]
 99            dz = b-a
100            coef = np.linalg.solve(a.T@a + 1e-4*np.eye(2), a.T@dz)
101            r = dz-a@coef
102            cov = np.cov(r, rowvar=False) + 1e-4*np.eye(2)
103            inv = np.linalg.inv(cov)
104            # Gaussian transition log-likelihood forward vs reversed ordering.
105            rf = dz
106            rr = a-b-b@coef
107            gap = float(np.mean(-0.5*np.einsum('ij,jk,ik->i',rf,inv,rf) + 0.5*np.einsum('ij,jk,ik->i',rr,inv,rr)))
108            return metric, {'dxy': float(dxy.cpu()), 'output_power': spectrum0,
109                            'reverse_forward_gap': gap, 'monitor': float(reg.cpu())}
110    except RuntimeError:
111        # Required robust CUDA fallback: recreate and execute on CPU.
112        if device != 'cuda': raise
113        torch.cuda.empty_cache()
114        return run_one_cpu(seed, cfg, idea, n_train, n_test, diagnostics)
115
116
117def run_one_cpu(seed, cfg, idea, n_train, n_test, diagnostics):
118    old = torch.cuda.is_available
119    ds = get_dataset('dynamics', seed, n_train=n_train, n_test=n_test)
120    net = make_model('rnn_small', ds['input_shape'], ds['out_dim'])
121    opt = torch.optim.Adam(net.parameters(), lr=cfg['lr'], weight_decay=cfg['weight_decay'])
122    x, y = ds['xtr'], ds['ytr']
123    for _ in range(EPOCHS):
124        perm = torch.randperm(len(x))
125        for i in range(0, len(x), BATCH):
126            ind=perm[i:i+BATCH]; h,p=forward_with_hidden(net,x[ind]); loss=((p-y[ind])**2).mean()
127            if idea: loss=loss+cfg['lam']*cross_diffusion_monitor(h)[0]
128            opt.zero_grad(); loss.backward(); opt.step()
129    with torch.no_grad():
130        h,o=forward_with_hidden(net,ds['xte']); metric=float(((o-ds['yte'])**2).mean())
131        if not diagnostics: return metric
132        reg,dxy=cross_diffusion_monitor(h); q=o[:,0].numpy(); z=h[:,:,:2].reshape(-1,2).numpy()
133        
134        z=h[:,:,:2].reshape(-1,2).numpy(); aa=z[:-1]; bb=z[1:]; dd=bb-aa
135        cc=np.cov(dd-aa@np.linalg.solve(aa.T@aa+1e-4*np.eye(2),aa.T@dd),rowvar=False)+1e-4*np.eye(2); ii=np.linalg.inv(cc)
136        gap=float(np.mean(-.5*np.einsum('ij,jk,ik->i',dd,ii,dd)+.5*np.einsum('ij,jk,ik->i',aa-bb-bb@np.linalg.solve(aa.T@aa+1e-4*np.eye(2),aa.T@dd),ii,aa-bb-bb@np.linalg.solve(aa.T@aa+1e-4*np.eye(2),aa.T@dd))))
137        return metric, {'dxy':float(dxy),'output_power':float(np.mean((q-q.mean())**2)), 'reverse_forward_gap':gap, 'monitor':float(reg)}
138
139
140def vals(fn): return [float(fn(s)) for s in SEEDS]
141
142
143def main():
144    def base_make(cfg): return lambda s: run_one(s, cfg, False)
145    base = sweep_baseline(base_make, GRID)
146    idea_runs=[]
147    for cfg in IDEA_GRID:
148        v=vals(lambda s, c=cfg: run_one(s,c,True))
149        idea_runs.append({'cfg':cfg,'mean':float(np.mean(v)),'per_seed':v})
150    best=min(idea_runs,key=lambda r:r['mean'])
151    idea_res={'mean':best['mean'],'std':float(np.std(best['per_seed'])),'per_seed':best['per_seed'],'n':8,'cfg':best['cfg'], 'sweep':idea_runs}
152    sig_base=[]; sig_idea=[]
153    for s in SEEDS:
154        _,b=run_one(s,base['best_cfg'],False,diagnostics=True)
155        _,a=run_one(s,best['cfg'],True,diagnostics=True)
156        sig_base.append(b); sig_idea.append(a)
157    def meanfield(rows,k): return float(np.mean([r[k] for r in rows]))
158    signature={'predicted': 'hidden cross diffusion can vary while observed output power is comparatively stable',
159      'baseline_observed_output_power_std':float(np.std([r['output_power'] for r in sig_base])),
160      'idea_observed_output_power_std':float(np.std([r['output_power'] for r in sig_idea])),
161      'baseline_hidden_dxy_std':float(np.std([r['dxy'] for r in sig_base])),
162      'idea_hidden_dxy_std':float(np.std([r['dxy'] for r in sig_idea])),
163      'idea_mean_reverse_forward_gap':meanfield(sig_idea,'reverse_forward_gap'),
164      'confirmed': bool(np.std([r['dxy'] for r in sig_idea]) > np.std([r['output_power'] for r in sig_idea]))}
165    report=make_report('dynamics','rnn_small',base,idea_res,{'mechanism_signature':signature,
166      'protocol_note':'Baseline and idea use identical rnn_small systems; only latent diffusion penalty differs.',
167      'idea_grid':idea_runs})
168    with open('bench_report.json','w') as f: json.dump(report,f,indent=2)
169    print(json.dumps(report,indent=2))
170
171if __name__=='__main__': main()