Boundary-Radial Persistence Loss / bench_radial.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  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()