Inverse-Square Adaptive Parameter Reset / experiment.py

Mechanism failed

Raw ⬇ ZIP
  1import json, math, os
  2import numpy as np
  3
  4# Inverse-square reset diffusion toy experiment.
  5# Units: dt is the diffusion time step and D=1 unless specified.
  6def beta_pred(ratio):
  7    return (math.sqrt(1.0 + 4.0 * ratio) - 1.0) / 2.0
  8
  9def simulate(r0, lam=-2.0, D=1.0, eps=0.1, dt=0.002, nwalk=1800,
 10             steps=9000, burn=1500, seed=0, box=80.0):
 11    rng = np.random.default_rng(seed)
 12    z = np.zeros(nwalk, dtype=np.float64)
 13    samples = []
 14    reset_count = 0
 15    for t in range(steps):
 16        z += math.sqrt(2*D*dt) * rng.standard_normal(nwalk)
 17        # generalized hazard; for lambda=-2 this is exactly the proposal
 18        if lam == -2:
 19            rate = r0 / (z*z + eps*eps)
 20        else:
 21            rate = r0 / (np.abs(z) + eps) ** (-lam)
 22        q = 1.0 - np.exp(-dt * rate)
 23        hit = rng.random(nwalk) < q
 24        reset_count += int(hit.sum())
 25        z[hit] = 0.0
 26        # A large reflecting box avoids numerical overflow; box dependence is measured separately.
 27        z[z > box] = 2*box - z[z > box]
 28        z[z < -box] = -2*box - z[z < -box]
 29        if t >= burn and t % 10 == 0:
 30            samples.append(z.copy())
 31    x = np.abs(np.concatenate(samples))
 32    return x, reset_count / (nwalk * steps)
 33
 34def tail_fit(x, lo=2.0, hi=15.0):
 35    # CCDF ~ x^-(beta-1), so density exponent is one minus CCDF slope.
 36    xx = np.linspace(lo, hi, 40)
 37    cc = np.array([(x >= a).mean() for a in xx])
 38    good = (cc > 3e-4) & (cc < .3)
 39    if good.sum() < 5: return float('nan'), float('nan')
 40    slope, intercept = np.polyfit(np.log(xx[good]), np.log(cc[good]), 1)
 41    return 1.0 - slope, float(np.corrcoef(np.log(xx[good]), np.log(cc[good]))[0,1])
 42
 43def run_toy():
 44    ratios = [0.5, 1.0, 1.5, 2.0, 2.5, 4.0, 8.0]
 45    rows=[]
 46    for i,r in enumerate(ratios):
 47        x, rr = simulate(r, seed=100+i)
 48        b, fitq = tail_fit(x)
 49        rows.append({'r0_over_D':r, 'beta_pred':beta_pred(r), 'beta_fit':b,
 50                     'tail_fit_r':fitq, 'mean_abs':float(x.mean()),
 51                     'p_abs_gt_5':float((x>5).mean()), 'reset_rate_step':rr})
 52    # Boundary prediction: above ratio 2 has finite first moment only for beta>2,
 53    # while normalizability itself is beta>1. Compare probability mass in a growing box.
 54    box_rows=[]
 55    for r in [1.0, 4.0]:
 56        vals=[]
 57        for B in [10.,20.,40.]:
 58            x,_=simulate(r, box=B, steps=8000, burn=1200, seed=900+int(B)+int(r*10))
 59            vals.append({'box':B,'mean_abs':float(x.mean()),'p_gt_5':float((x>5).mean())})
 60        box_rows.append({'ratio':r,'beta_pred':beta_pred(r),'box_scaling':vals})
 61    # Controls for the claimed lambda classification, at r0=4.
 62    controls=[]
 63    for lam in [-3.,-2.,-1.]:
 64        x,_=simulate(4.,lam=lam,steps=9000,burn=1500,seed=700+int((lam+3)*10))
 65        b,q=tail_fit(x)
 66        controls.append({'lambda':lam,'mean_abs':float(x.mean()),'tail_density_exponent_fit':b,'fit_r':q})
 67    return {'toy_rows':rows,'box_rows':box_rows,'lambda_controls':controls}
 68
 69def run_ml(seed=42):
 70    # Small, reproducible regression; compare SGD with a checkpoint reset wrapper.
 71    import torch
 72    torch.manual_seed(seed); np.random.seed(seed)
 73    device='cuda' if torch.cuda.is_available() else 'cpu'
 74    try:
 75        dev=torch.device(device)
 76        x=torch.linspace(-3,3,512,device=dev).unsqueeze(1)
 77        y=torch.sin(3*x)+0.15*torch.randn_like(x)
 78        tr=x[:384]; ty=y[:384]; va=x[384:]; vy=y[384:]
 79        def train(reset=False):
 80            torch.manual_seed(seed)
 81            m=torch.nn.Sequential(torch.nn.Linear(1,32),torch.nn.Tanh(),torch.nn.Linear(32,1)).to(dev)
 82            opt=torch.optim.SGD(m.parameters(),lr=.035,momentum=.8)
 83            ref=[p.detach().clone() for p in m.parameters()]
 84            best=1e9; resets=0
 85            for step in range(700):
 86                idx=torch.randint(0,len(tr),(64,),device=dev)
 87                opt.zero_grad(); loss=((m(tr[idx])-ty[idx])**2).mean(); loss.backward(); opt.step()
 88                with torch.no_grad():
 89                    vl=((m(va)-vy)**2).mean().item()
 90                    if vl<best: best=vl; ref=[p.detach().clone() for p in m.parameters()]
 91                    if reset:
 92                        z=math.sqrt(sum(((p-r)**2).sum().item() for p,r in zip(m.parameters(),ref)))
 93                        # D estimate is deliberately fixed to the toy scale; this is a stress test, not tuning.
 94                        q=1-math.exp(-.004/(z*z+.05*.05))
 95                        if np.random.random()<q:
 96                            for p,r in zip(m.parameters(),ref): p.copy_(r)
 97                            opt.state.clear(); resets+=1
 98            return float(((m(va)-vy)**2).mean().item()),resets
 99        base=train(False); idea=train(True)
100        return {'device':str(dev),'baseline_val_mse':base[0],'reset_val_mse':idea[0],'reset_count':idea[1]}
101    except Exception as e:
102        return {'device':'cpu-fallback','error':repr(e)}
103
104if __name__=='__main__':
105    out={'toy':run_toy(),'ml':run_ml()}
106    with open('results.json','w') as f: json.dump(out,f,indent=2)
107    print(json.dumps(out,indent=2))