Dynamic-programming Doob sampler for exact rare-event conditioning / doob_bench.py
Unverified
1import sys, json, math
2from pathlib import Path
3import numpy as np
4import torch
5import torch.nn as nn
6
7sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
8from bench import make_model, train_model, evaluate, sweep_baseline, make_report
9
10META = {'name': 'rare_event_markov', 'domain': 'dynamics',
11 'description': 'Discrete Markov state-space event prediction with exact terminal reachability conditioning.'}
12NSTATE, TARGET, MAX_H, Q = 10, 9, 12, 0.30
13SEEDS = tuple(range(8))
14LR_GRID = [1e-3, 3e-3, 1e-2]
15EPOCHS = 20
16
17
18def exact_message(state, horizon):
19 need = TARGET - int(state)
20 if need < 0:
21 return 1.0
22 if need > int(horizon):
23 return 0.0
24 return float(math.comb(int(horizon), need) * Q ** need * (1-Q) ** (int(horizon)-need))
25
26
27def get_dataset(seed, n_train=400, n_test=400):
28 rng = np.random.RandomState(seed)
29 def make(n):
30 state = rng.randint(0, 5, size=n)
31 horizon = rng.randint(5, MAX_H + 1, size=n)
32 y = np.zeros(n, dtype=np.int64)
33 for i in range(n):
34 z = int(state[i])
35 for _ in range(int(horizon[i])):
36 if z < TARGET and rng.rand() < Q:
37 z += 1
38 y[i] = int(z == TARGET)
39 x = np.stack([state / TARGET, horizon / MAX_H], axis=1).astype(np.float32)
40 msg = np.array([exact_message(s, h) for s, h in zip(state, horizon)], dtype=np.float32)
41 return x, y, msg
42 xtr, ytr, mtr = make(n_train)
43 xte, yte, mte = make(n_test)
44 return {'xtr': torch.from_numpy(xtr), 'ytr': torch.from_numpy(ytr),
45 'xte': torch.from_numpy(xte), 'yte': torch.from_numpy(yte),
46 'message_tr': torch.from_numpy(mtr), 'message_te': torch.from_numpy(mte),
47 'task': 'classification', 'metric': 'err', 'input_shape': (2,), 'out_dim': 2}
48
49
50def baseline_train(cfg, seed):
51 torch.manual_seed(seed); np.random.seed(seed)
52 d = get_dataset(seed)
53 net = make_model('mlp_tiny', d['input_shape'], d['out_dim'])
54 _, metric, _ = train_model(net, d, epochs=EPOCHS, lr=cfg['lr'],
55 weight_decay=cfg.get('weight_decay', 0.0), log=lambda *_: None)
56 return float(metric)
57
58
59def idea_train(cfg, seed, return_signature=False):
60 torch.manual_seed(seed); np.random.seed(seed)
61 d = get_dataset(seed)
62 net = make_model('mlp_tiny', d['input_shape'], d['out_dim'])
63 # This is the intervention: train the transition-event predictor toward
64 # the exact backward feasibility message in addition to the event label.
65 device = 'cuda' if torch.cuda.is_available() else 'cpu'
66 try:
67 net = net.to(device)
68 x, y, msg = d['xtr'].to(device), d['ytr'].to(device), d['message_tr'].to(device)
69 opt = torch.optim.Adam(net.parameters(), lr=cfg['lr'])
70 ce = nn.CrossEntropyLoss()
71 for _ in range(EPOCHS):
72 perm = torch.randperm(len(x), device=device)
73 for i in range(0, len(x), 128):
74 ix = perm[i:i+128]
75 out = net(x[ix])
76 prob = torch.softmax(out, 1)[:, 1]
77 loss = ce(out, y[ix]) + cfg['alpha'] * ((prob - msg[ix]) ** 2).mean()
78 opt.zero_grad(); loss.backward(); opt.step()
79 net.eval()
80 with torch.no_grad():
81 out = net(d['xte'].to(device)); prob = torch.softmax(out, 1)[:, 1]
82 metric = float((out.argmax(1) != d['yte'].to(device)).float().mean())
83 rmse = float(torch.sqrt(((prob - d['message_te'].to(device)) ** 2).mean()))
84 corr = float(torch.corrcoef(torch.stack([prob, d['message_te'].to(device)]))[0, 1])
85 except RuntimeError:
86 # CPU fallback mirrors the benchmark's robust device policy.
87 net = make_model('mlp_tiny', d['input_shape'], d['out_dim'])
88 net = net.to('cpu'); x, y, msg = d['xtr'], d['ytr'], d['message_tr']
89 opt = torch.optim.Adam(net.parameters(), lr=cfg['lr']); ce = nn.CrossEntropyLoss()
90 for _ in range(EPOCHS):
91 for i in range(0, len(x), 128):
92 out = net(x[i:i+128]); prob = torch.softmax(out, 1)[:, 1]
93 loss = ce(out, y[i:i+128]) + cfg['alpha'] * ((prob-msg[i:i+128])**2).mean()
94 opt.zero_grad(); loss.backward(); opt.step()
95 with torch.no_grad():
96 out = net(d['xte']); prob = torch.softmax(out, 1)
97 metric = float((out.argmax(1) != d['yte']).float().mean())
98 rmse = float(torch.sqrt(((prob-d['message_te'])**2).mean()))
99 corr = float(torch.corrcoef(torch.stack([prob, d['message_te']]))[0,1])
100 if return_signature:
101 return float(metric), {'message_rmse': rmse, 'message_corr': corr}
102 return float(metric)
103
104
105def main():
106 # Baseline sweep includes every lr evaluated by the idea; alpha is the
107 # method knob and is swept symmetrically on the idea side (baseline alpha=0).
108 grid = [{'lr': lr, 'weight_decay': wd} for lr in LR_GRID for wd in [0.0, 1e-4]]
109 base = sweep_baseline(lambda cfg: (lambda seed: baseline_train(cfg, seed)), grid)
110 idea_grid = [{'lr': lr, 'alpha': a} for lr in LR_GRID for a in [0.1, 0.3, 1.0]]
111 candidates = []
112 for cfg in idea_grid:
113 r = evaluate(lambda seed, c=cfg: idea_train(c, seed), SEEDS[:4])
114 candidates.append({'cfg': cfg, 'mean': r['mean']})
115 best = min(candidates, key=lambda z: z['mean'])['cfg']
116 idea = evaluate(lambda seed: idea_train(best, seed), SEEDS)
117 sigs = [idea_train(best, s, True)[1] for s in SEEDS]
118 signature = {'predicted': 'exact backward message should improve NN feasibility-probability fidelity',
119 'observed_message_rmse_mean': float(np.mean([x['message_rmse'] for x in sigs])),
120 'observed_message_corr_mean': float(np.mean([x['message_corr'] for x in sigs])),
121 'confirmed': bool(np.mean([x['message_corr'] for x in sigs]) > 0.9),
122 'idea_sweep': candidates}
123 report = make_report('rare_event_markov', 'mlp_tiny', base, idea, signature)
124 Path('bench_report.json').write_text(json.dumps(report, indent=2))
125 print(json.dumps(report, indent=2))
126
127if __name__ == '__main__':
128 main()