Extreme-Subset Adversarial Dropout / experiment.py
Mechanism failed
1import itertools
2import json
3import math
4import random
5from pathlib import Path
6
7import numpy as np
8import torch
9
10SEED = 466
11np.random.seed(SEED)
12random.seed(SEED)
13torch.manual_seed(SEED)
14
15
16def subset_scores(A, m):
17 """Return every m-row subset and its least singular value."""
18 subsets = list(itertools.combinations(range(A.shape[0]), m))
19 scores = np.array([
20 np.linalg.svd(A[list(T), :], compute_uv=False)[-1] for T in subsets
21 ], dtype=np.float64)
22 return subsets, scores
23
24
25def gaussian_scaling_check():
26 """Exhaustively check the theorem's log M / m behavior at gamma=2."""
27 gamma = 2.0
28 h = gamma * math.log(gamma) - (gamma - 1) * math.log(gamma - 1)
29 theory = -h
30 rows = []
31 for m in range(2, 9):
32 vals = []
33 for rep in range(10):
34 rng = np.random.default_rng(SEED + 1000 * m + rep)
35 A = rng.standard_normal((2 * m, m))
36 _, scores = subset_scores(A, m)
37 vals.append(math.log(max(float(scores.min()), 1e-300)) / m)
38 rows.append({"m": m, "mean": float(np.mean(vals)),
39 "std": float(np.std(vals)), "theory": theory})
40 rng = np.random.default_rng(SEED + 999)
41 A = rng.standard_normal((12, 6))
42 _, scores = subset_scores(A, 6)
43 return {"gamma": gamma, "theory_limit": theory, "scaling": rows,
44 "directional": {"min": float(scores.min()),
45 "median": float(np.median(scores)),
46 "min_over_median": float(scores.min() / np.median(scores))}}
47
48
49def train(mask_mode, W, X, y, masks, steps=500, lr=0.04):
50 """Train a small downstream network on fixed channel subsets."""
51 device = "cuda" if torch.cuda.is_available() else "cpu"
52 try:
53 torch.manual_seed(SEED)
54 model = torch.nn.Sequential(torch.nn.Linear(int(masks[0].numel()), 32),
55 torch.nn.Tanh(), torch.nn.Linear(32, 1)).to(device)
56 opt = torch.optim.Adam(model.parameters(), lr=lr)
57 Xt, yt = X.to(device), y.to(device)
58 Wt = W.to(device)
59 losses = []
60 for step in range(steps):
61 if mask_mode == "random":
62 # deterministic pseudo-random schedule, with the same pool size
63 j = (step * 7919 + 104729) % len(masks)
64 else:
65 # Scores are computed from the fixed readout matrix. The
66 # adversary chooses the lowest-score candidate each step.
67 j = 0
68 # masks are preordered by increasing score; cycle through the
69 # hardest few to avoid training on one pathological subset only.
70 j = (step // 25) % max(1, min(5, len(masks)))
71 idx = masks[j].to(device)
72 # Select surviving measurements and project to a common m-vector.
73 xb = Xt[:, idx]
74 pred = model(xb)
75 loss = torch.nn.functional.mse_loss(pred, yt)
76 opt.zero_grad(); loss.backward(); opt.step()
77 losses.append(float(loss.detach().cpu()))
78 return model, losses
79 except Exception:
80 # CPU fallback is required for shared/fragile CUDA environments.
81 model = torch.nn.Sequential(torch.nn.Linear(int(masks[0].numel()), 32),
82 torch.nn.Tanh(), torch.nn.Linear(32, 1))
83 opt = torch.optim.Adam(model.parameters(), lr=lr)
84 losses = []
85 for step in range(steps):
86 j = step % len(masks) if mask_mode == "random" else (step // 25) % min(5, len(masks))
87 pred = model(X[:, masks[j]])
88 loss = torch.nn.functional.mse_loss(pred, y)
89 opt.zero_grad(); loss.backward(); opt.step(); losses.append(float(loss))
90 return model, losses
91
92
93def regression_check():
94 """Compare random masks with low-condition-score masks at equal steps."""
95 rng = np.random.default_rng(SEED + 77)
96 n, N, m, latent = 1200, 12, 6, 6
97 Z = rng.standard_normal((n, latent)).astype(np.float32)
98 W_np = rng.standard_normal((N, latent)).astype(np.float32)
99 X_np = Z @ W_np.T + 0.05 * rng.standard_normal((n, N)).astype(np.float32)
100 y_np = (Z[:, :1] - 0.6 * Z[:, 1:2] + 0.2 * Z[:, 2:3]).astype(np.float32)
101 _, scores = subset_scores(W_np, m)
102 all_subsets = list(itertools.combinations(range(N), m))
103 order = np.argsort(scores)
104 ordered = [all_subsets[i] for i in order]
105 # Both methods have the same 20-mask training budget. Random uses a
106 # deterministic spread of the full pool; adversarial uses the 20 lowest scores.
107 random_subsets = [all_subsets[i] for i in np.linspace(0, len(all_subsets)-1, 20, dtype=int)]
108 masks = [torch.tensor(t, dtype=torch.long) for t in random_subsets]
109 hard_masks = [torch.tensor(t, dtype=torch.long) for t in ordered[:20]]
110 X, y, W = torch.tensor(X_np), torch.tensor(y_np), torch.tensor(W_np)
111 rmodel, rloss = train("random", W, X, y, masks)
112 hmodel, hloss = train("hard", W, X, y, hard_masks)
113 eval_device = next(rmodel.parameters()).device
114 Xe, ye = X.to(eval_device), y.to(eval_device)
115 # Identical exhaustive evaluation: this is the relevant robustness test.
116 with torch.no_grad():
117 random_eval = [float(torch.nn.functional.mse_loss(rmodel(Xe[:, torch.tensor(t, device=eval_device)]), ye).cpu()) for t in all_subsets]
118 hard_eval = [float(torch.nn.functional.mse_loss(hmodel(Xe[:, torch.tensor(t, device=eval_device)]), ye).cpu()) for t in all_subsets]
119 def summary(a):
120 a = np.asarray(a)
121 return {"mean_all": float(a.mean()), "worst_1pct": float(np.quantile(a, .99)),
122 "worst": float(a.max()), "best": float(a.min())}
123 return {"random_final_train_loss": rloss[-1], "hard_final_train_loss": hloss[-1],
124 "random_eval": summary(random_eval), "hard_eval": summary(hard_eval),
125 "random_low_score20_mean": float(np.mean([random_eval[i] for i in order[:20]])),
126 "hard_low_score20_mean": float(np.mean([hard_eval[i] for i in order[:20]])),
127 "hardest_score": float(scores[order[0]]),
128 "median_score": float(np.median(scores)),
129 "score_ratio": float(scores[order[0]] / np.median(scores)),
130 "random_curve": rloss, "hard_curve": hloss}
131
132
133def main():
134 out = {"seed": SEED, "gaussian": gaussian_scaling_check(), "regression": regression_check()}
135 Path("results.json").write_text(json.dumps(out, indent=2))
136 print(json.dumps({"gaussian": out["gaussian"], "regression_summary":
137 {k: v for k, v in out["regression"].items() if "curve" not in k}}, indent=2))
138
139
140if __name__ == "__main__":
141 main()