Boundary-Radial Persistence Loss / bench_radial.py
Mechanism confirmed, baseline not beaten
1import os, sys, json, random
2import numpy as np
3import torch
4import torch.nn as nn
5from scipy.optimize import linear_sum_assignment
6
7sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
8from bench import make_report
9
10META = {"name": "radial_boundary_shapes", "domain": "segmentation_geometry", "description": "Regression from noisy radial boundary profiles to up to four component radii; radial interval matching is the intervention."}
11
12
13def get_dataset(seed, n_train=400, n_test=100):
14 rng = np.random.RandomState(seed)
15 def gen(n):
16 x = np.zeros((n, 16), np.float32)
17 y = np.zeros((n, 4), np.float32)
18 for i in range(n):
19 k = rng.randint(2, 5)
20 radii = np.sort(rng.uniform(.25, .9, k)).astype(np.float32)
21 y[i, :k] = radii
22 z = np.arange(16, dtype=np.float32) / 15.0
23 probe = np.zeros(16, np.float32)
24 for r in radii:
25 probe += np.exp(-((z-r) ** 2) / .006)
26 x[i] = probe + rng.normal(0, .055, 16)
27 return x, y
28 xtr, ytr = gen(n_train); xte, yte = gen(n_test)
29 return {"xtr": xtr, "ytr": ytr, "xte": xte, "yte": yte,
30 "task": "regression", "metric": "mse", "out_dim": 4}
31
32
33def make_net():
34 return nn.Sequential(nn.Linear(16, 64), nn.ReLU(), nn.Linear(64, 64),
35 nn.ReLU(), nn.Linear(64, 4))
36
37
38def radial_match_np(pred, target, unmatched=0.10):
39 p = np.sort(np.maximum(pred, 0.0)); t = np.sort(np.maximum(target, 0.0))
40 p = p[p > .04]; t = t[t > .04]
41 n, m = len(p), len(t)
42 if not n and not m: return 0.0
43 cost = np.full((n + m, n + m), unmatched, dtype=float)
44 if n and m:
45 cost[:n, :m] = np.abs(p[:, None] - t[None, :])
46 cost[n:, m:] = 0.0
47 rr, cc = linear_sum_assignment(cost)
48 return float(cost[rr, cc].sum())
49
50
51def radial_loss(pred, target, unmatched=.10):
52 # Smooth surrogate preserving the boundary-radial principle: sorted
53 # predicted radii are matched to sorted target intervals, while zero slots
54 # represent absent components and incur an unmatched penalty.
55 p = torch.sort(torch.relu(pred), dim=1).values
56 t = torch.sort(torch.relu(target), dim=1).values
57 active = (t > .04).float()
58 pair = (torch.abs(p - t) * active).sum(dim=1)
59 extra = (torch.relu(.04 - p) * 0.0 + (p > .04).float() * (1.0-active)).sum(dim=1) * unmatched
60 missing = ((1.0 - (p > .04).float()) * active).sum(dim=1) * unmatched
61 return (pair + extra + missing).mean()
62
63
64def train(seed, lr, alpha, epochs=18, batch=128):
65 torch.manual_seed(seed); np.random.seed(seed); random.seed(seed)
66 d = get_dataset(seed, 400, 120)
67 dev = 'cuda' if torch.cuda.is_available() else 'cpu'
68 try:
69 net = make_net().to(dev)
70 opt = torch.optim.Adam(net.parameters(), lr=lr)
71 x = torch.tensor(d['xtr'], dtype=torch.float32, device=dev)
72 y = torch.tensor(d['ytr'], dtype=torch.float32, device=dev)
73 net.train()
74 for _ in range(epochs):
75 order = torch.randperm(len(x), device=dev)
76 for ix in order.split(batch):
77 out = net(x[ix])
78 mse = ((out-y[ix])**2).mean()
79 loss = mse + alpha * radial_loss(out, y[ix])
80 opt.zero_grad(); loss.backward(); opt.step()
81 net.eval()
82 with torch.no_grad():
83 xt = torch.tensor(d['xte'], dtype=torch.float32, device=dev)
84 yt = torch.tensor(d['yte'], dtype=torch.float32, device=dev)
85 pred = net(xt)
86 metric = float(((pred-yt)**2).mean().cpu())
87 sig = float(torch.abs(pred-yt).mean().cpu())
88 rad = float(np.mean([radial_match_np(a, b, .5) for a, b in zip(pred.cpu().numpy(), yt.cpu().numpy())]))
89 return metric, sig, rad
90 except RuntimeError:
91 if dev == 'cuda':
92 torch.cuda.empty_cache()
93 torch.backends.cudnn.enabled = False
94 os.environ['CUDA_VISIBLE_DEVICES'] = ''
95 return train(seed, lr, alpha, epochs, batch)
96 raise
97
98
99def mechanism_check(lr, alpha):
100 # Measured on predictions of trained benchmark systems: the claim tested is
101 # that radial endpoint discrepancy tracks ordinary endpoint error.
102 rows = []
103 for seed in range(8):
104 mse, mae, rad = train(seed, lr, alpha)
105 rows.append((mae, rad))
106 mae = np.asarray([r[0] for r in rows]); rad = np.asarray([r[1] for r in rows])
107 corr = float(np.corrcoef(mae, rad)[0, 1]) if np.std(mae) and np.std(rad) else 0.0
108 return {"prediction": "trained outputs with larger endpoint error have larger radial matching loss", "predicted": "positive association", "observed_mae_mean": float(mae.mean()), "observed_radial_loss_mean": float(rad.mean()), "observed_pearson": corr, "confirmed": bool(corr > 0.5)}
109
110
111def main():
112 lrs = [1e-3, 3e-3, 1e-2]
113 seeds = list(range(8))
114 base_grid = []
115 for lr in lrs:
116 vals = [train(s, lr, 0.0)[0] for s in seeds]
117 base_grid.append({"config": {"lr": lr, "alpha": 0.0}, "per_seed": vals, "mean": float(np.mean(vals))})
118 best = min(base_grid, key=lambda z: z['mean'])
119 idea_configs = [best['config'], {"lr": best['config']['lr'], "alpha": .05}, {"lr": best['config']['lr'], "alpha": .20}]
120 idea_grid = []
121 for cfg in idea_configs:
122 vals = [train(s, cfg['lr'], cfg['alpha'])[0] for s in seeds]
123 idea_grid.append({"config": cfg, "per_seed": vals, "mean": float(np.mean(vals))})
124 idea = min(idea_grid, key=lambda z: z['mean'])
125 base = {"sweep": base_grid, "best_config": best['config'], "full": {"per_seed": best['per_seed'], "config": best['config']}}
126 report = make_report('radial_boundary_shapes', 'local_mlp_shared', base, {"per_seed": idea['per_seed'], "config": idea['config']}, {"mechanism_signature": mechanism_check(best['config']['lr'], idea['config']['alpha']), "idea_sweep": idea_grid, "custom_track": {"name": META['name'], "file": "bench_radial.py", "domain": META['domain']}})
127 with open('bench_report.json', 'w') as f: json.dump(report, f, indent=2)
128 print(json.dumps(report, indent=2))
129
130if __name__ == '__main__': main()