Positive-Measure Span Regularizer / run_bench.py
Mechanism confirmed, baseline not beaten
1import os, sys, json, random, time
2import numpy as np
3import torch
4from torch import nn
5
6sys.path.insert(0, "/home/maxwelhelp/all/math2nn")
7from bench import (get_dataset, make_model, sweep_baseline, make_report,
8 permutation_pvalue)
9
10SEEDS = tuple(range(8))
11EPOCHS = 20
12BATCH = 128
13WEIGHT_DECAY = 0.0
14# Shared union: both baseline and idea are evaluated at every lr.
15GRID = [{"lr": 1e-3}, {"lr": 3e-3}, {"lr": 1e-2}]
16TAU = 0.15
17BETA = 5.0
18LAMBDA = 0.01
19
20
21def seed_all(seed):
22 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
23 if torch.cuda.is_available():
24 try: torch.cuda.manual_seed_all(seed)
25 except Exception: pass
26
27
28def rank_proxy(z, tau=TAU):
29 """Frobenius-normalized smooth effective rank of rows of z."""
30 z = z / (torch.linalg.norm(z, ord="fro") + 1e-8)
31 s = torch.linalg.svdvals(z)
32 return (s.square() / (s.square() + tau * tau)).sum()
33
34
35def span_loss(z, tau=TAU, beta=BETA):
36 # Tabular local regions are approximated by contiguous shuffled minibatches.
37 # The minibatch is the positive-measure local sample set available here.
38 r = rank_proxy(z, tau)
39 # One group is the exact soft-min over this batch; splitting into several
40 # groups would make groups too small for the 64-wide encoder.
41 return -r
42
43
44class EncodedMLP(nn.Module):
45 """Same mlp_tiny function as bench, with a public representation tap."""
46 def __init__(self, input_dim, out_dim):
47 super().__init__()
48 self.enc = nn.Sequential(nn.Linear(input_dim, 64), nn.ReLU(),
49 nn.Linear(64, 64), nn.ReLU())
50 self.head = nn.Linear(64, out_dim)
51 def forward(self, x, return_z=False):
52 z = self.enc(x)
53 y = self.head(z)
54 return (y, z) if return_z else y
55
56
57def make_net(ds):
58 # Explicitly reproduce bench mlp_tiny architecture, enabling the tap.
59 return EncodedMLP(int(np.prod(ds["input_shape"])), ds["out_dim"])
60
61
62def train(seed, lr, use_span):
63 seed_all(seed)
64 ds = get_dataset("tabular", seed, n_train=400, n_test=200)
65 net = make_net(ds)
66 device = "cuda" if torch.cuda.is_available() else "cpu"
67 try:
68 net = net.to(device)
69 x, y = ds["xtr"].to(device), ds["ytr"].to(device)
70 opt = torch.optim.Adam(net.parameters(), lr=lr, weight_decay=WEIGHT_DECAY)
71 lossf = nn.MSELoss()
72 for _ in range(EPOCHS):
73 net.train(); perm = torch.randperm(len(x), device=device)
74 for i in range(0, len(x), BATCH):
75 ind = perm[i:i+BATCH]
76 pred, z = net(x[ind], True)
77 loss = lossf(pred, y[ind])
78 if use_span:
79 loss = loss + LAMBDA * span_loss(z)
80 opt.zero_grad(); loss.backward(); opt.step()
81 net.eval()
82 with torch.no_grad():
83 pred, z = net(ds["xte"].to(device), True)
84 mse = float(((pred - ds["yte"].to(device)) ** 2).mean().cpu())
85 rank = float(rank_proxy(z).cpu())
86 # A directly measured collapse signature: normalized smallest
87 # singular value and effective rank on the held-out representations.
88 sv = torch.linalg.svdvals(z / (torch.linalg.norm(z, ord="fro") + 1e-8))
89 sv_ratio = float((sv[-1] / (sv[0] + 1e-8)).cpu())
90 return {"metric": mse, "test_rank": rank, "minmax_sv_ratio": sv_ratio}
91 except Exception as e:
92 if str(device) == "cuda":
93 # Explicit CPU fallback required by the benchmark instructions.
94 seed_all(seed); net = make_net(ds).cpu()
95 x, y = ds["xtr"], ds["ytr"]
96 opt = torch.optim.Adam(net.parameters(), lr=lr, weight_decay=WEIGHT_DECAY)
97 for _ in range(EPOCHS):
98 perm = torch.randperm(len(x))
99 for i in range(0, len(x), BATCH):
100 ind=perm[i:i+BATCH]; pred,z=net(x[ind],True)
101 loss=nn.functional.mse_loss(pred,y[ind]) + (LAMBDA*span_loss(z) if use_span else 0)
102 opt.zero_grad(); loss.backward(); opt.step()
103 with torch.no_grad():
104 pred,z=net(ds["xte"],True); mse=float(nn.functional.mse_loss(pred,ds["yte"]))
105 rank=float(rank_proxy(z)); sv=torch.linalg.svdvals(z/(torch.linalg.norm(z,ord='fro')+1e-8))
106 ratio=float(sv[-1]/(sv[0]+1e-8))
107 return {"metric":mse,"test_rank":rank,"minmax_sv_ratio":ratio,"fallback":str(e)[:120]}
108 raise
109
110
111def baseline_fn(cfg):
112 return lambda seed: train(seed, cfg["lr"], False)["metric"]
113
114
115def run_side(lr, use_span):
116 vals=[]
117 for seed in SEEDS:
118 r=train(seed,lr,use_span); r["seed"]=seed; vals.append(r)
119 return {"per_seed": vals, "mean": float(np.mean([r["metric"] for r in vals]))}
120
121
122def main():
123 t=time.time()
124 # Required baseline sweep uses the benchmark helper and its standard
125 # evaluation protocol; each grid point is also explicitly paired below.
126 base = sweep_baseline(baseline_fn, GRID, seeds=SEEDS)
127 base_full = {"per_seed": [], "mean": 0.0}
128 for seed in SEEDS:
129 r=train(seed, base["best_cfg"]["lr"], False); r["seed"]=seed
130 base_full["per_seed"].append(r)
131 base_full["mean"] = float(np.mean([r["metric"] for r in base_full["per_seed"]]))
132 base_diag = base_full["per_seed"]
133 base["full"] = {"per_seed": [r["metric"] for r in base_diag],
134 "mean": base_full["mean"]}
135 base["diagnostics"] = base_diag
136 idea_grid = [run_side(c["lr"], True) for c in GRID]
137 best_i = min(idea_grid, key=lambda x:x["mean"])
138 best_i_report = {"per_seed": [r["metric"] for r in best_i["per_seed"]],
139 "mean": best_i["mean"], "diagnostics": best_i["per_seed"]}
140 # The signature compares trained systems at the selected common lr.
141 bmap={r["seed"]:r for r in base_diag}; imap={r["seed"]:r for r in best_i["per_seed"]}
142 deltas=[imap[s]["metric"]-bmap[s]["metric"] for s in SEEDS]
143 sig={"prediction":"span regularization increases held-out effective rank",
144 "predicted_direction":"idea rank > baseline rank",
145 "baseline_mean_rank":float(np.mean([bmap[s]["test_rank"] for s in SEEDS])),
146 "idea_mean_rank":float(np.mean([imap[s]["test_rank"] for s in SEEDS])),
147 "baseline_mean_sv_ratio":float(np.mean([bmap[s]["minmax_sv_ratio"] for s in SEEDS])),
148 "idea_mean_sv_ratio":float(np.mean([imap[s]["minmax_sv_ratio"] for s in SEEDS])),
149 "confirmed":float(np.mean([imap[s]["test_rank"] for s in SEEDS])) > float(np.mean([bmap[s]["test_rank"] for s in SEEDS]))}
150 report=make_report("tabular","mlp_tiny",base,best_i_report,{"mechanism_signature":sig,
151 "idea_grid":[{"lr":c["lr"],"mean":r["mean"]} for c,r in zip(GRID,idea_grid)],
152 "paired_deltas":deltas,"paired_p_value":permutation_pvalue(deltas)})
153 report["elapsed_sec"]=time.time()-t
154 with open("bench_report.json","w") as f: json.dump(report,f,indent=2)
155 print(json.dumps(report,indent=2))
156
157if __name__ == "__main__": main()