Recursive Bellman Variance Targets / verify.py

Failed on benchmark

Raw ⬇ ZIP
  1import json, math, random
  2from pathlib import Path
  3import numpy as np
  4
  5SEED = 924
  6rng = np.random.default_rng(SEED)
  7
  8# A binary-state finite-horizon process. At each step, next state is sampled,
  9# reward has conditional mean r[state,next] and Gaussian conditional noise.
 10def recursion(h, p, gamma, r, reward_var, terminal_mean=None, terminal_var=None):
 11    # m[t,x], v[t,x] describe return from state x with t steps remaining.
 12    m = np.zeros((h + 1, 2)); v = np.zeros((h + 1, 2))
 13    if terminal_mean is not None: m[0] = terminal_mean
 14    if terminal_var is not None: v[0] = terminal_var
 15    P = np.array([[1-p, p], [p, 1-p]], float)
 16    for t in range(1, h + 1):
 17        for x in range(2):
 18            z = r[x] + gamma * m[t-1] # reward mean depends only on current state
 19            m[t,x] = z @ P[x]
 20            # conditional reward noise plus propagated child uncertainty and selection variance
 21            v[t,x] = np.sum(P[x] * (reward_var[x] + gamma**2 * v[t-1] + (z - m[t,x])**2))
 22    return m, v
 23
 24def sample_returns(h, n, p, gamma, r, sigma, start=0):
 25    x = np.full(n, start, dtype=np.int64); out = np.zeros(n)
 26    for t in range(h):
 27        nx = np.where(rng.random(n) < p, 1-x, x)
 28        out += (gamma**t) * (r[x] + sigma[x] * rng.normal(size=n))
 29        x = nx
 30    return out
 31
 32def main():
 33    gamma=.9; r=np.array([-1., 1.]); sig=np.array([.35,.35]); rv=sig**2
 34    rows=[]
 35    # Prediction 1: recursive variance equals held-out Monte Carlo variance.
 36    for h in [1,2,4,8,12]:
 37        m,v=recursion(h,.35,gamma,r,rv)
 38        g=sample_returns(h, 300000, .35,gamma,r,sig)
 39        rows.append({'h':h,'recursive_var':float(v[h,0]),'mc_var':float(g.var()),
 40                     'relative_error':float(abs(g.var()-v[h,0])/v[h,0])})
 41    # Prediction 2: one-step transition-selection term is p(1-p)*(delta child mean)^2,
 42    # hence zero at deterministic transitions and maximal at p=.5.
 43    sel=[]
 44    child=np.array([-2., 3.]); delta=child[1]-child[0]
 45    for p in [0.,.1,.25,.5,.75,.9,1.]:
 46        P=np.array([1-p,p]); mean=P@child
 47        measured=np.sum(P*(child-mean)**2)
 48        predicted=p*(1-p)*delta**2
 49        sel.append({'p':p,'predicted':float(predicted),'measured':float(measured)})
 50    # Prediction 3: with independently redrawn state each step, variance is the
 51    # discounted sum of per-step variances; removing transition randomness removes
 52    # the state-selection contribution and leaves reward-noise variance only.
 53    horizon=[]
 54    for transition_var in [0.,1.]:
 55        # transition_var=1 means iid +/-1 state each step, 0 means fixed state +1.
 56        vals=[]
 57        for h in [1,2,4,8,16]:
 58            state_var=transition_var # Var +/-1 with equal probability
 59            predicted=sum((gamma**(2*t))*(state_var + .35**2) for t in range(h))
 60            g=np.zeros(250000)
 61            for t in range(h):
 62                states=(rng.choice([-1.,1.],size=g.size) if transition_var else np.ones(g.size))
 63                g += gamma**t*(states + .35*rng.normal(size=g.size))
 64            vals.append({'h':h,'predicted':float(predicted),'mc_var':float(g.var())})
 65        horizon.append({'transition_randomness':transition_var,'points':vals})
 66    # Small baseline-vs-idea regression: contexts have heteroscedastic known recursive
 67    # targets. MSE is standard; weighted NLL uses the detached recursive variance.
 68    try:
 69        import torch
 70        torch.manual_seed(SEED)
 71        device='cuda' if torch.cuda.is_available() else 'cpu'
 72        try:
 73            x=torch.rand(12000,1,device=device)
 74            # x controls branch probability; target is child +/-1 plus reward noise.
 75            pp=.05+.9*x[:,0]
 76            means=2*pp-1
 77            q=.25 + 4*pp*(1-pp) # reward variance + transition selection variance
 78            y=means + torch.sqrt(torch.tensor(.25,device=device))*torch.randn_like(means)
 79            # split is fixed and models are intentionally tiny
 80            def train(kind):
 81                net=torch.nn.Sequential(torch.nn.Linear(1,24),torch.nn.Tanh(),torch.nn.Linear(24,2)).to(device)
 82                opt=torch.optim.Adam(net.parameters(),lr=.01)
 83                for _ in range(180):
 84                    out=net(x[:9000]); mu=out[:,0]; logv=out[:,1]
 85                    if kind=='mse': loss=((mu-y[:9000])**2).mean()
 86                    else:
 87                        # recursive q is detached, as prescribed; mean is trained with
 88                        # heteroscedastic Gaussian NLL and the variance head is fitted too.
 89                        loss=.5*(((y[:9000]-mu)**2)/q[:9000].detach()+torch.log(q[:9000].detach())).mean()
 90                        loss += .05*.5*(logv-q[:9000].detach().log()).pow(2).mean()
 91                    opt.zero_grad(); loss.backward(); opt.step()
 92                with torch.no_grad():
 93                    o=net(x[9000:]); err=(o[:,0]-y[9000:])**2
 94                    cal=(err/q[9000:]).mean().item()
 95                    mse=err.mean().item()
 96                return mse,cal
 97            mse,cal_mse=train('mse'); mse_i,cal_i=train('idea')
 98            neural={'device':device,'baseline_mse':mse,'baseline_calibration_ratio':cal_mse,
 99                    'idea_mse':mse_i,'idea_calibration_ratio':cal_i}
100        except Exception as e:
101            neural={'fallback_error':str(e)}
102    except Exception as e: neural={'torch_error':str(e)}
103    result={'seed':SEED,'parameters':{'gamma':gamma,'reward_sigma':.35},
104            'prediction_1_recursive_vs_mc':rows,'prediction_2_selection_term':sel,
105            'prediction_3_horizon':horizon,'mini_experiment':neural}
106    Path('results.json').write_text(json.dumps(result,indent=2))
107    print(json.dumps(result,indent=2))
108
109if __name__=='__main__': main()