OT Primitive Universal Flow / bench_ot_dynamics.py
Mechanism confirmed, baseline not beaten
1import os, sys, json, random
2import numpy as np
3import torch
4import torch.nn.functional as F
5
6sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
7import bench
8
9SEEDS = tuple(range(8))
10# Union is shared by baseline and idea, satisfying step-size parity.
11GRID = [
12 {'lr': 0.0015, 'weight_decay': 0.0},
13 {'lr': 0.0030, 'weight_decay': 0.0},
14 {'lr': 0.0060, 'weight_decay': 0.0},
15]
16EPOCHS = 24
17BATCH = 128
18
19
20def seed_all(seed):
21 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
22 if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
23
24
25def run(seed, cfg, idea=False, collect_signature=False):
26 seed_all(seed)
27 ds = bench.get_dataset('dynamics', seed, n_train=400, n_test=100)
28 model = bench.make_model('rnn_small', ds['input_shape'], ds['out_dim'])
29 # Keep the same architecture and optimizer on both sides.
30 opt = torch.optim.Adam(model.parameters(), lr=cfg['lr'], weight_decay=cfg['weight_decay'])
31 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
32 try:
33 model.to(device)
34 x = torch.as_tensor(ds['xtr'], dtype=torch.float32, device=device)
35 y = torch.as_tensor(ds['ytr'], dtype=torch.float32, device=device)
36 xt = torch.as_tensor(ds['xte'], dtype=torch.float32, device=device)
37 yt = torch.as_tensor(ds['yte'], dtype=torch.float32, device=device)
38 for ep in range(EPOCHS):
39 perm = torch.randperm(len(x), device=device)
40 for ix in perm.split(BATCH):
41 xb, yb = x[ix], y[ix]
42 pred = model(xb)
43 loss = F.mse_loss(pred, yb)
44 if idea:
45 # Euclidean 1-D Exp_z(grad psi)=z+psi'(z) is monotone
46 # when its derivative is positive. Penalize violation of
47 # the diffeomorphic OT primitive condition w.r.t. the
48 # terminal angle, while retaining the task loss.
49 xbq = xb.detach().clone().requires_grad_(True)
50 pq = model(xbq)
51 d = torch.autograd.grad(pq.sum(), xbq, create_graph=True)[0]
52 # final theta is feature index 21 (8 triples).
53 jac = d[:, 21]
54 mono = F.relu(0.05 - jac).square().mean()
55 # Small squared displacement cost, as in L_OT, scaled so
56 # it is comparable to MSE rather than dominating it.
57 disp = (pq - xbq[:, 21:22]).square().mean()
58 loss = loss + 0.03 * mono + 0.002 * disp
59 opt.zero_grad(set_to_none=True); loss.backward()
60 torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0)
61 opt.step()
62 model.eval()
63 with torch.no_grad(): metric = float(F.mse_loss(model(xt), yt).item())
64 sig = None
65 if collect_signature:
66 # Re-test the structural prediction on trained models, not an
67 # analytic toy: observed Jacobian positivity vs predicted OT sign.
68 q = xt[:min(64, len(xt))].detach().clone().requires_grad_(True)
69 p = model(q)
70 g = torch.autograd.grad(p.sum(), q)[0][:, 21]
71 sig = {'predicted_monotone_fraction': 1.0,
72 'observed_monotone_fraction': float((g > 0).float().mean().item()),
73 'observed_mean_terminal_angle_jacobian': float(g.mean().item()),
74 'confirmed': bool(float((g > 0).float().mean().item()) >= 0.90)}
75 return metric, sig
76 except Exception as e:
77 # Explicit GPU->CPU fallback, mirroring bench.train_model's robust path.
78 if device.type == 'cuda':
79 torch.cuda.empty_cache()
80 os.environ['CUDA_VISIBLE_DEVICES'] = ''
81 return run(seed, cfg, idea, collect_signature)
82 raise e
83
84
85def train_fn(idea, cfg):
86 return lambda s: run(s, cfg, idea=idea)[0]
87
88
89def main():
90 # Baseline sweep over the complete shared hyperparameter union.
91 base = bench.sweep_baseline(lambda cfg: train_fn(False, cfg), GRID, seeds=(0,1,2,3))
92 base_full = base['full']
93 # Explicitly evaluate every idea setting on all paired seeds. This also
94 # evaluates the baseline at every setting through the sweep, with the
95 # selected setting re-evaluated on all eight seeds.
96 idea_runs = []
97 for cfg in GRID:
98 r = bench.evaluate(train_fn(True, cfg), seeds=SEEDS)
99 idea_runs.append({'cfg': cfg, **r})
100 best_idea = min(idea_runs, key=lambda z: z['mean'])
101 idea_res = {'best_cfg': best_idea['cfg'], 'sweep': idea_runs,
102 'per_seed': best_idea['per_seed'], 'mean': best_idea['mean'],
103 'std': best_idea['std'], 'n': best_idea['n']}
104 # Matched paired comparison uses same seed and same best setting.
105 base_paired = bench.evaluate(train_fn(False, best_idea['cfg']), seeds=SEEDS)
106 paired = {'baseline_at_idea_cfg': base_paired,
107 'deltas_idea_minus_baseline': [a-b for a,b in zip(idea_res['per_seed'], base_paired['per_seed'])],
108 'pvalue': bench.permutation_pvalue([a-b for a,b in zip(idea_res['per_seed'], base_paired['per_seed'])])}
109 # Signature from independently trained baseline and idea models at seed 0.
110 _, sig_i = run(0, best_idea['cfg'], idea=True, collect_signature=True)
111 _, sig_b = run(0, best_idea['cfg'], idea=False, collect_signature=True)
112 sig = {'prediction': 'OT monotonicity predicts positive terminal-angle Jacobian',
113 'idea': sig_i, 'baseline': sig_b,
114 'confirmed': bool(sig_i['confirmed'])}
115 report = bench.make_report('dynamics', 'rnn_small',
116 {'best_cfg': base['best_cfg'], 'sweep': base['sweep'], 'full': base_full},
117 idea_res,
118 {'mechanism_signature': sig, 'paired': paired,
119 'protocol': {'epochs': EPOCHS, 'batch': BATCH, 'n_seeds': 8,
120 'loss': 'MSE plus OT monotonicity and displacement penalties'}})
121 report['comparison']['paired'] = paired
122 with open('bench_report.json', 'w') as f: json.dump(report, f, indent=2)
123 print(json.dumps(report, indent=2))
124
125if __name__ == '__main__': main()