Composite Density-Power Loss / stage2_bench.py
Mechanism confirmed, baseline not beaten
1import sys, json, random
2import numpy as np
3import torch
4import torch.nn.functional as F
5
6sys.path.insert(0, "/home/maxwelhelp/all/math2nn")
7from bench import get_dataset, make_model, evaluate, sweep_baseline, make_report
8
9SEEDS = tuple(range(8))
10SWEEP_SEEDS = tuple(range(4))
11LRS = [1e-3, 3e-3, 1e-2]
12ALPHAS = [0.3, 0.5, 0.7]
13EPOCHS = 4
14NTR, NTE = 300, 150
15NOISE = 0.20
16
17
18def seed_all(seed):
19 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
20 if torch.cuda.is_available():
21 try: torch.cuda.manual_seed_all(seed)
22 except Exception: pass
23
24
25def dpd_loss(logits, y, alpha):
26 p = F.softmax(logits.float(), dim=-1)
27 A = p.pow(1.0 + alpha).sum(dim=-1)
28 q = p.gather(1, y[:, None]).squeeze(1).clamp_min(1e-12)
29 return (A - (1.0 + 1.0 / alpha) * q.pow(alpha)).mean()
30
31
32def corrupted_ds(seed):
33 d = get_dataset("vision", seed=seed, n_train=NTR, n_test=NTE)
34 rng = np.random.RandomState(seed + 99173)
35 y = d["ytr"].clone()
36 mask = torch.as_tensor(rng.rand(len(y)) < NOISE)
37 new = torch.as_tensor(rng.randint(0, 10, len(y)), dtype=torch.long)
38 new = torch.where(new == y, (new + 1) % 10, new)
39 y[mask] = new[mask]
40 d["ytr"] = y
41 d["corrupt_mask"] = mask
42 return d
43
44
45def run(cfg, seed, return_model=False):
46 seed_all(seed)
47 d = corrupted_ds(seed)
48 net = make_model("cnn_small", d["input_shape"], d["out_dim"])
49 # This is a custom loop only because the intervention changes the loss.
50 try:
51 device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
52 net = net.to(device)
53 x, y = d["xtr"].to(device), d["ytr"].to(device)
54 opt = torch.optim.Adam(net.parameters(), lr=float(cfg["lr"]))
55 gen = torch.Generator(device="cpu").manual_seed(seed + 12345)
56 for _ in range(EPOCHS):
57 order = torch.randperm(len(x), generator=gen)
58 for start in range(0, len(x), 128):
59 ix = order[start:start+128].to(device)
60 logits = net(x[ix])
61 loss = (F.cross_entropy(logits, y[ix]) if cfg["kind"] == "ce"
62 else dpd_loss(logits, y[ix], float(cfg["alpha"])))
63 opt.zero_grad(set_to_none=True); loss.backward(); opt.step()
64 with torch.no_grad():
65 pred = net(d["xte"].to(device)).argmax(1).cpu()
66 metric = float((pred != d["yte"]).float().mean())
67 if return_model:
68 return metric, net, d, device
69 del net
70 if torch.cuda.is_available(): torch.cuda.empty_cache()
71 return metric
72 except RuntimeError:
73 # Explicit CPU fallback for a shared/fragmented GPU slot.
74 seed_all(seed)
75 d = corrupted_ds(seed)
76 net = make_model("cnn_small", d["input_shape"], d["out_dim"]).to("cpu")
77 opt = torch.optim.Adam(net.parameters(), lr=float(cfg["lr"]))
78 gen = torch.Generator().manual_seed(seed + 12345)
79 for _ in range(EPOCHS):
80 for start in range(0, NTR, 128):
81 ix = torch.randperm(NTR, generator=gen)[start:start+128]
82 z = net(d["xtr"][ix]); loss = (F.cross_entropy(z, d["ytr"][ix]) if cfg["kind"] == "ce" else dpd_loss(z, d["ytr"][ix], float(cfg["alpha"])))
83 opt.zero_grad(set_to_none=True); loss.backward(); opt.step()
84 with torch.no_grad(): metric = float((net(d["xte"]).argmax(1) != d["yte"]).float().mean())
85 if return_model: return metric, net, d, torch.device("cpu")
86 return metric
87
88
89def base_factory(cfg):
90 return lambda seed: run({"kind": "ce", "lr": cfg["lr"]}, seed)
91
92
93def idea_factory(cfg):
94 return lambda seed: run({"kind": "dpd", "lr": cfg["lr"], "alpha": cfg["alpha"]}, seed)
95
96
97def mechanism_signature():
98 # Measure the proposed score suppression on outputs of a trained NN.
99 cfg = {"kind": "dpd", "lr": 3e-3, "alpha": 0.5}
100 _, net, d, device = run(cfg, 0, return_model=True)
101 net.eval(); x = d["xtr"].to(device); y = d["ytr"].to(device)
102 mask = d["corrupt_mask"].to(device)
103 idx = torch.where(mask)[0][:min(48, int(mask.sum()))]
104 z = net(x[idx]).detach().requires_grad_(True)
105 yy = y[idx]
106 p = F.softmax(z, -1); q = p.gather(1, yy[:, None]).squeeze(1).clamp_min(1e-12)
107 g_ce = torch.autograd.grad((-q.log()).sum(), z, retain_graph=True)[0]
108 a = cfg["alpha"]
109 g_obs = torch.autograd.grad((-(1+1/a)*q.pow(a)).sum(), z)[0]
110 observed = (g_obs.norm(dim=1) / g_ce.norm(dim=1).clamp_min(1e-12)).detach().cpu().numpy()
111 predicted = ((1+a)*q.pow(a)).detach().cpu().numpy()
112 rel = np.abs(observed-predicted) / np.maximum(predicted, 1e-12)
113 return {"prediction": "observed-label score gradient ratio equals (1+alpha)*q^alpha", "alpha": a,
114 "n_corrupted": int(len(idx)), "mean_predicted_ratio": float(predicted.mean()),
115 "mean_observed_ratio": float(observed.mean()), "max_relative_error": float(rel.max()),
116 "corrupted_mean_q": float(q.detach().mean()),
117 "confirmed": bool(np.isfinite(rel).all() and float(rel.max()) < 1e-4)}
118
119
120def main():
121 base_grid = [{"lr": lr} for lr in LRS]
122 base = sweep_baseline(base_factory, base_grid, seeds=SWEEP_SEEDS)
123 trials = []
124 idea_lr = float(base["best_cfg"]["lr"])
125 for alpha in ALPHAS:
126 cfg = {"lr": idea_lr, "alpha": alpha}
127 trials.append({"cfg": cfg, "result": evaluate(idea_factory(cfg), SEEDS)})
128 best = min(trials, key=lambda r: r["result"]["mean"])
129 extra = {"track_choice": "vision: categorical class probability output matches exact DPD integral; label corruption tests robustness", "noise_rate": NOISE,
130 "idea_sweep": trials, "mechanism_signature": mechanism_signature()}
131 rep = make_report("vision", "cnn_small", base, best["result"], extra)
132 with open("bench_report.json", "w") as f: json.dump(rep, f, indent=2)
133 print(json.dumps(rep, indent=2))
134
135if __name__ == "__main__": main()