Schur-Riesz Greedy Adapter Expansion / stage2_bench.py
Failed on benchmark
1import os, sys, json, random
2import numpy as np
3import torch
4import torch.nn as nn
5
6sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
7from bench import get_dataset, train_model, evaluate, sweep_baseline, make_report
8
9SEED0 = 2145
10EPOCHS = 18
11NTR, NTE = 400, 200
12N_CAND, ACTIVE = 8, 3
13
14class CandidateMLP(nn.Module):
15 """Shared architecture: incumbent MLP plus a bank of gated feature blocks."""
16 def __init__(self, input_dim, out_dim=1, active=(0,1,2)):
17 super().__init__()
18 self.base = nn.Sequential(nn.Linear(input_dim, 32), nn.Tanh(), nn.Linear(32, 16), nn.Tanh())
19 self.base_head = nn.Linear(16, out_dim)
20 self.cands = nn.ModuleList([nn.Sequential(nn.Linear(input_dim, 8), nn.Tanh(), nn.Linear(8, out_dim)) for _ in range(N_CAND)])
21 self.active = tuple(int(x) for x in active)
22 def forward(self, x):
23 y = self.base_head(self.base(x))
24 for j in self.active:
25 y = y + self.cands[j](x)
26 return y
27 def incumbent(self, x):
28 return self.base_head(self.base(x))
29 def candidate_response(self, x, j):
30 return self.cands[j](x)
31
32def seed_all(seed):
33 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
34 if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
35
36def projected_scores(net, x):
37 """Q_j=(I-P_B) C_j for scalar responses, Y=I; return gains and singular values."""
38 with torch.no_grad():
39 b = net.incumbent(x).detach().cpu().numpy().reshape(-1, 1)
40 cs = [net.candidate_response(x, j).detach().cpu().numpy().reshape(-1, 1) for j in range(N_CAND)]
41 # Stable Y-weighted projector via least squares, no explicit inverse assumption.
42 scores = []
43 qs = []
44 for c in cs:
45 coef, *_ = np.linalg.lstsq(b, c, rcond=1e-8)
46 q = c - b @ coef
47 sv = np.linalg.svd(q, compute_uv=False)
48 scores.append(float(np.sum(q*q))); qs.append(q)
49 return scores, qs
50
51def train_one(track, seed, lr, mode, return_net=False):
52 seed_all(seed)
53 d = get_dataset(track, seed, n_train=NTR, n_test=NTE)
54 dim = int(np.prod(d['input_shape']))
55 # Keep exact same initial bank for paired baseline/idea; only activation differs.
56 probe = CandidateMLP(dim, d['out_dim'], active=())
57 scores, qs = projected_scores(probe, d['xtr'][:min(128, NTR)])
58 if mode == 'idea':
59 ranked = sorted(range(N_CAND), key=lambda j: scores[j], reverse=True)
60 # Riesz filter: reject nearly null and badly scaled responses; fallback preserves budget.
61 accepted = [j for j in ranked if np.linalg.norm(qs[j]) >= 1e-4 and np.linalg.norm(qs[j]) <= 30][:ACTIVE]
62 if len(accepted) < ACTIVE: accepted = ranked[:ACTIVE]
63 else:
64 accepted = list(range(ACTIVE))
65 net = CandidateMLP(dim, d['out_dim'], active=accepted)
66 # train_model is the canonical path for the trained system
67 net, metric, hist = train_model(net, d, epochs=EPOCHS, lr=lr, batch=128, log=lambda *_: None)
68 if net is None: return float('nan') if not return_net else (float('nan'), None, accepted, scores)
69 if return_net: return float(metric), net, accepted, scores
70 return float(metric)
71
72def make_baseline(track, cfg):
73 return lambda seed: train_one(track, int(seed), float(cfg['lr']), 'baseline')
74
75def make_idea(track, cfg):
76 return lambda seed: train_one(track, int(seed), float(cfg['lr']), 'idea')
77
78def signature(track, cfg, seeds=(0,1,2,3,4,5,6,7)):
79 pred, obs = [], []
80 for s in seeds:
81 metric, net, selected, scores = train_one(track, s, cfg['lr'], 'idea', True)
82 d = get_dataset(track, s, n_train=NTR, n_test=NTE)
83 x = d['xte'].to(next(net.parameters()).device)
84 with torch.no_grad():
85 full = net(x).cpu().numpy().reshape(-1)
86 inc = net.incumbent(x).cpu().numpy().reshape(-1)
87 # predicted residual energy on calibration; observed response energy on held-out trained model
88 pred.append(float(sum(scores[j] for j in selected)))
89 obs.append(float(np.mean((full-inc)**2)))
90 corr = float(np.corrcoef(pred, obs)[0,1]) if np.std(pred)>0 and np.std(obs)>0 else 0.0
91 return {'predicted_projected_gain_mean': float(np.mean(pred)), 'observed_heldout_selected_response_mse_mean': float(np.mean(obs)), 'predicted_per_seed': pred, 'observed_per_seed': obs, 'correlation': corr, 'confirmed': bool(corr > 0.3)}
92
93def main():
94 track, model = 'tabular', 'candidate_mlp_shared'
95 # Union of all step sizes is shared by both sides; three settings satisfy idea nearby sweep.
96 grid = [{'lr': 1e-3}, {'lr': 3e-3}, {'lr': 1e-2}]
97 base = sweep_baseline(lambda cfg: make_baseline(track, cfg), grid)
98 idea_trials = []
99 for cfg in grid:
100 r = evaluate(make_idea(track, cfg))
101 idea_trials.append({'cfg': cfg, 'result': r})
102 best = min(idea_trials, key=lambda z: z['result']['mean'])
103 report = make_report(track, model, base, best['result'], extra=signature(track, best['cfg']))
104 report['idea_sweep'] = idea_trials
105 report['selection'] = {'candidates': N_CAND, 'active_blocks': ACTIVE, 'epochs': EPOCHS, 'n_train': NTR, 'n_test': NTE, 'track_rationale': 'tabular regression is the built-in optimizer/regularizer/architecture-compatible small MLP track; no PDE/control structure is required by this adapter-bank idea'}
106 with open('bench_report.json','w') as f: json.dump(report, f, indent=2)
107 print(json.dumps(report, indent=2))
108
109if __name__ == '__main__': main()