Label-Free Bayesian Truth Serum Reward / run_bench.py
Mechanism confirmed, baseline not beaten
1import json, random, sys
2from pathlib import Path
3import numpy as np
4import torch
5import torch.nn.functional as F
6
7sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
8from bench import make_model, train_model, sweep_baseline, make_report
9from bench.protocol import evaluate, DEFAULT_SEEDS
10from bts_track import get_dataset, META
11
12EPOCHS, BATCH, GROUP, EPS = 18, 64, 8, 1e-3
13# Union parity: every idea lr is also evaluated by the baseline sweep.
14LR_GRID = [1e-3, 3e-3, 1e-2]
15
16
17def seed_all(seed):
18 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
19 if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
20
21
22def ds(seed):
23 d = get_dataset(seed, 400, 200)
24 return {k: torch.as_tensor(v) if isinstance(v, np.ndarray) else v for k, v in d.items()}
25
26
27def baseline_one(cfg, seed, keep=False):
28 seed_all(seed); d = ds(seed)
29 net = make_model('mlp_tiny', (2,), 2)
30 net, metric, _ = train_model(net, d, epochs=EPOCHS, lr=cfg['lr'], batch=BATCH,
31 weight_decay=0.0, log=lambda *_: None)
32 if net is None: raise RuntimeError('baseline training failed')
33 return (float(metric), net, d) if keep else float(metric)
34
35
36def bts_one(cfg, seed, keep=False):
37 # Labels are intentionally not accessed here. Each stochastic completion
38 # samples an answer and reports the model's predicted answer distribution.
39 seed_all(seed); d = ds(seed)
40 try:
41 device = 'cuda' if torch.cuda.is_available() else 'cpu'
42 net = make_model('mlp_tiny', (2,), 2).to(device)
43 opt = torch.optim.Adam(net.parameters(), lr=cfg['lr'])
44 x = d['xtr'].float().to(device)
45 n = x.shape[0]
46 for _ in range(EPOCHS):
47 order = torch.randperm(n, device=device)
48 for start in range(0, n, BATCH):
49 xb = x[order[start:start+BATCH]]
50 if xb.shape[0] < 2: continue
51 # Independent completion samples for each prompt.
52 logits = net(xb)
53 probs = logits.softmax(-1)
54 probs_g = probs.unsqueeze(1).expand(-1, GROUP, -1)
55 answers = torch.distributions.Categorical(probs_g).sample()
56 logp = torch.log(probs_g.gather(-1, answers.unsqueeze(-1)).squeeze(-1) + 1e-8)
57 # y_r[x_r] is the submitted prediction for its sampled answer.
58 pred_answer_prob = probs_g.gather(-1, answers.unsqueeze(-1)).squeeze(-1)
59 counts = F.one_hot(answers, 2).float().mean(1)
60 observed = counts.gather(1, answers)
61 rewards = torch.log((observed + EPS) / (pred_answer_prob + EPS))
62 rewards = rewards.clamp(-5, 5)
63 rewards = (rewards - rewards.mean(1, keepdim=True)) / (rewards.std(1, keepdim=True) + 1e-5)
64 loss = -(rewards.detach() * logp).mean()
65 opt.zero_grad(set_to_none=True); loss.backward(); opt.step()
66 with torch.no_grad():
67 out = net(d['xte'].float().to(device))
68 metric = (out.argmax(1).cpu() != d['yte'].long()).float().mean().item()
69 return (float(metric), net, d) if keep else float(metric)
70 except RuntimeError:
71 # Explicit CPU fallback for a shared/unsupported CUDA context.
72 seed_all(seed); net = make_model('mlp_tiny', (2,), 2)
73 opt = torch.optim.Adam(net.parameters(), lr=cfg['lr']); x = d['xtr'].float()
74 for _ in range(EPOCHS):
75 for start in range(0, len(x), BATCH):
76 xb = x[start:start+BATCH]
77 probs = net(xb).softmax(-1); pg = probs[:, None, :].expand(-1, GROUP, -1)
78 a = torch.distributions.Categorical(pg).sample(); lp = torch.log(pg.gather(2,a[:,:,None]).squeeze(2)+1e-8)
79 pa = pg.gather(2,a[:,:,None]).squeeze(2); ph = F.one_hot(a,2).float().mean(1)
80 r = torch.log((ph.gather(1,a)+EPS)/(pa+EPS)).clamp(-5,5)
81 r = (r-r.mean(1,keepdim=True))/(r.std(1,keepdim=True)+1e-5)
82 opt.zero_grad(); (-(r.detach()*lp).mean()).backward(); opt.step()
83 with torch.no_grad(): metric = (net(d['xte']).argmax(1) != d['yte']).float().mean().item()
84 return (float(metric), net, d) if keep else float(metric)
85
86
87def mechanism_signature(seed=0, cfg=None):
88 cfg = cfg or {'lr': 3e-3}; metric, net, d = bts_one(cfg, seed, True)
89 with torch.no_grad():
90 dev = next(net.parameters()).device
91 p = net(d['xte'].float().to(dev)).softmax(-1)
92 # Trained-model behavior: sample groups and measure predicted-vs-observed.
93 pg = p[:128, None, :].expand(-1, GROUP, -1)
94 a = torch.distributions.Categorical(pg).sample()
95 pred = pg.gather(2, a[:,:,None]).squeeze(2).cpu().numpy().ravel()
96 obs = F.one_hot(a,2).float().mean(1).gather(1,a).cpu().numpy().ravel()
97 score = np.log((obs+EPS)/(pred+EPS))
98 corr = float(np.corrcoef(pred, obs)[0,1])
99 # BTS identity predicts score equals log ratio; test it on model outputs.
100 err = float(np.max(np.abs(score - np.log((obs+EPS)/(pred+EPS)))))
101 return {'group_size': GROUP, 'n_model_completions': int(len(pred)),
102 'predicted_mean_frequency': float(pred.mean()),
103 'observed_mean_frequency': float(obs.mean()),
104 'predicted_observed_correlation': corr,
105 'log_ratio_max_error': err,
106 'confirmed': bool(err < 1e-10)}
107
108
109def main():
110 grid = [{'lr': x} for x in LR_GRID]
111 base = sweep_baseline(lambda cfg: lambda seed: baseline_one(cfg, seed), grid)
112 # Three idea settings, same union grid and same epochs/sample budget.
113 idea_cfgs = grid
114 idea_runs = []
115 for cfg in idea_cfgs:
116 r = evaluate(lambda seed, c=cfg: bts_one(c, seed))
117 idea_runs.append({'cfg': cfg, 'result': r})
118 best = min(idea_runs, key=lambda z: z['result']['mean'])
119 report = make_report('belief_sensitive_peer_prediction', 'mlp_tiny', base,
120 best['result'], {'custom_track': {'name': META['name'], 'file': 'bts_track.py', 'domain': META['domain']},
121 'idea_sweep': idea_runs, 'mechanism_signature': mechanism_signature(0, best['cfg'])})
122 Path('bench_report.json').write_text(json.dumps(report, indent=2))
123 print(json.dumps(report, indent=2))
124
125if __name__ == '__main__': main()