Randomized-QMC gradient batches / qmc_experiment.py
Beats tuned baseline
1import json, math, random
2import numpy as np
3from scipy.special import ndtri
4from scipy.stats import qmc
5
6SEED = 2437
7np.random.seed(SEED); random.seed(SEED)
8
9
10def sobol_base(n, d):
11 return qmc.Sobol(d=d, scramble=False, seed=SEED).random_base2(int(round(math.log2(n))))
12
13
14def qmc_estimates(n, d, reps, fn):
15 x = sobol_base(n, d); out = []
16 rng = np.random.default_rng(SEED + 17*d + n)
17 for _ in range(reps):
18 out.append(float(np.mean(fn((x + rng.random(d)) % 1.0))))
19 return np.asarray(out)
20
21
22def iid_estimates(n, d, reps, fn):
23 rng = np.random.default_rng(SEED + 991*d + n)
24 return np.asarray([float(np.mean(fn(rng.random((n,d))))) for _ in range(reps)])
25
26
27def smooth(u):
28 return np.prod(1.0 + 0.7*np.cos(2*np.pi*u), axis=1)
29
30
31def nonsmooth(u):
32 return (u[:,0] < 0.37).astype(float)
33
34
35def toy_checks():
36 unbiased = qmc_estimates(16, 2, 20000, smooth)
37 ns = [4, 8, 16, 32, 64, 128, 256]
38 smooth_rows=[]; rough_rows=[]
39 for n in ns:
40 q=qmc_estimates(n,2,512,smooth); i=iid_estimates(n,2,512,smooth)
41 smooth_rows.append((n,float(np.sqrt(np.mean((q-1)**2))),float(np.sqrt(np.mean((i-1)**2)))))
42 q=qmc_estimates(n,2,512,nonsmooth); i=iid_estimates(n,2,512,nonsmooth)
43 rough_rows.append((n,float(np.sqrt(np.mean((q-.37)**2))),float(np.sqrt(np.mean((i-.37)**2)))))
44 dim_rows=[]
45 for d in [1,2,4,8,16]:
46 q=qmc_estimates(64,d,512,smooth); i=iid_estimates(64,d,512,smooth)
47 dim_rows.append((d,float(np.sqrt(np.mean((q-1)**2))),float(np.sqrt(np.mean((i-1)**2)))))
48 def slope(rows,col):
49 return float(np.polyfit(np.log([r[0] for r in rows]),np.log([max(r[col],1e-15) for r in rows]),1)[0])
50 return {'unbiasedness':{'target':1.0,'mean':float(unbiased.mean()),'absolute_error':float(abs(unbiased.mean()-1)),'shift_sd':float(unbiased.std())},'smooth_scaling':{'rows_n_qmc_rmse_iid_rmse':smooth_rows,'qmc_loglog_slope':slope(smooth_rows,1),'iid_loglog_slope':slope(smooth_rows,2)},'nonsmooth_control':{'rows_n_qmc_rmse_iid_rmse':rough_rows,'qmc_loglog_slope':slope(rough_rows,1),'iid_loglog_slope':slope(rough_rows,2)},'dimension':{'rows_d_qmc_rmse_iid_rmse':dim_rows}}
51
52
53def neural_gradient_check():
54 import torch
55 torch.manual_seed(SEED); dtype=torch.float64
56 x0=torch.linspace(-1.,1.,48,dtype=dtype).reshape(-1,1); y=torch.sin(2.4*x0)+.15*x0
57 net=torch.nn.Sequential(torch.nn.Linear(1,12),torch.nn.Tanh(),torch.nn.Linear(12,1)).double()
58 state={k:v.detach().clone() for k,v in net.state_dict().items()}
59 def grad_for(u):
60 net.load_state_dict(state); net.zero_grad()
61 z=torch.tensor(ndtri(np.clip(u,1e-6,1-1e-6)),dtype=dtype).reshape(-1,1)
62 pred=net(x0)+.25*z.mean(); loss=((pred-y)**2).mean(); loss.backward()
63 return np.concatenate([p.grad.detach().numpy().ravel() for p in net.parameters()]),float(loss.detach())
64 n=16; reps=192; base=sobol_base(n,1); rng=np.random.default_rng(SEED+777)
65 gq=[];gi=[];lq=[];li=[]
66 for _ in range(reps):
67 g,l=grad_for((base+rng.random(1))%1);gq.append(g);lq.append(l)
68 g,l=grad_for(rng.random((n,1)));gi.append(g);li.append(l)
69 gq=np.asarray(gq);gi=np.asarray(gi);qvar=float(np.mean(np.var(gq,axis=0,ddof=1)));ivar=float(np.mean(np.var(gi,axis=0,ddof=1)))
70 return {'n':n,'reps':reps,'gradient_component_variance_qmc':qvar,'gradient_component_variance_iid':ivar,'variance_reduction':1-qvar/ivar,'loss_mean_qmc':float(np.mean(lq)),'loss_mean_iid':float(np.mean(li)),'loss_mean_difference':float(np.mean(lq)-np.mean(li))}
71
72
73def training_check():
74 import torch
75 torch.manual_seed(SEED); dtype=torch.float64
76 x=torch.linspace(-1.,1.,64,dtype=dtype).reshape(-1,1)
77 target=lambda xx,zz: torch.sin(2.2*xx)+.25*zz
78 base_model=torch.nn.Sequential(torch.nn.Linear(2,16),torch.nn.Tanh(),torch.nn.Linear(16,1)).double()
79 init={k:v.detach().clone() for k,v in base_model.state_dict().items()}
80 n,steps=16,120; sob=sobol_base(n,2); rng=np.random.default_rng(SEED+314)
81 curves=[]
82 for mode in ['qmc','iid']:
83 model=torch.nn.Sequential(torch.nn.Linear(2,16),torch.nn.Tanh(),torch.nn.Linear(16,1)).double(); model.load_state_dict(init)
84 opt=torch.optim.Adam(model.parameters(),lr=.025); vals=[]
85 for step in range(steps):
86 if mode=='qmc': u=(sob+rng.random(2))%1
87 else: u=rng.random((n,2))
88 z=torch.tensor(ndtri(np.clip(u[:,1],1e-6,1-1e-6)),dtype=dtype).reshape(-1,1)
89 # Use the first coordinate to select fixed data points, while the second is latent noise.
90 idx=np.floor(u[:,0]*len(x)).astype(int); xx=x[idx]; yy=target(xx,z)
91 pred=model(torch.cat([xx,z],1)); loss=((pred-yy)**2).mean()
92 opt.zero_grad();loss.backward();opt.step();vals.append(float(loss.detach()))
93 curves.append(vals)
94 return {'steps':steps,'final_loss_qmc':curves[0][-1],'final_loss_iid':curves[1][-1],'best_loss_qmc':min(curves[0]),'best_loss_iid':min(curves[1]),'trajectory_first_last':{'qmc':[curves[0][0],curves[0][-1]],'iid':[curves[1][0],curves[1][-1]]}}
95
96
97def main():
98 result={'seed':SEED,'toy':toy_checks(),'neural_gradient':neural_gradient_check(),'training':training_check()}
99 with open('results.json','w') as f: json.dump(result,f,indent=2)
100 print(json.dumps(result,indent=2))
101
102if __name__=='__main__': main()