Uniformly Mixing Coulomb Particle Bank / run_bench.py
Failed on benchmark
1import json
2import sys
3from pathlib import Path
4import numpy as np
5import torch
6
7sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
8from bench import get_dataset, make_model, train_model, sweep_baseline, make_report
9from bench.protocol import DEFAULT_SEEDS
10
11TRACK = 'tabular'
12MODEL = 'mlp_tiny'
13EPOCHS = 20
14BATCH = 128
15# Shared union: every lr and scale tested by the idea is also tested by baseline.
16GRID = [
17 {'lr': 1e-3, 'scale': 0.7},
18 {'lr': 3e-3, 'scale': 1.0},
19 {'lr': 1e-2, 'scale': 1.3},
20]
21
22
23def coulomb_bank(n, beta=2.0, eta=0.02, steps=180, eps=1e-5, seed=0):
24 rng = np.random.default_rng(seed)
25 a = np.linspace(0, 2*np.pi, n, endpoint=False) + rng.normal(0, .03, n)
26 z = np.c_[np.cos(a), np.sin(a)] * np.sqrt(n / 2.0)
27 for _ in range(steps):
28 d = z[:, None, :] - z[None, :, :]
29 r2 = np.sum(d*d, axis=-1) + eps
30 np.fill_diagonal(r2, np.inf)
31 rep = np.sum(d / r2[:, :, None], axis=1) / n
32 z += eta * (-z + rep) + np.sqrt(2 * eta / beta) * rng.normal(size=z.shape)
33 # Normalize to unit per-coordinate RMS, matching a conventional small init.
34 z /= max(np.sqrt(np.mean(z*z)), 1e-8)
35 return z
36
37
38def set_first_layer(net, mode, scale, seed):
39 # A 2-D Coulomb bank is lifted to the 10-D input rows by a fixed random
40 # orthonormal projection. The only system difference is this initialization.
41 layer = net[0]
42 with torch.no_grad():
43 if mode == 'coulomb':
44 z = coulomb_bank(layer.out_features, seed=seed)
45 rng = np.random.default_rng(seed + 991)
46 q, _ = np.linalg.qr(rng.normal(size=(layer.in_features, 2)))
47 w = z @ q.T
48 layer.weight.copy_(torch.tensor(w * (scale * 0.12), dtype=layer.weight.dtype))
49 layer.bias.zero_()
50 else:
51 torch.manual_seed(seed + 991)
52 torch.nn.init.normal_(layer.weight, mean=0.0, std=scale * 0.12)
53 torch.nn.init.zeros_(layer.bias)
54
55
56def train_one(seed, cfg, mode, return_net=False):
57 np.random.seed(seed)
58 torch.manual_seed(seed)
59 ds = get_dataset(TRACK, seed, n_train=400, n_test=400)
60 net = make_model(MODEL, ds['input_shape'], ds['out_dim'])
61 set_first_layer(net, mode, cfg['scale'], seed)
62 net, metric, hist = train_model(net, ds, epochs=EPOCHS, lr=cfg['lr'], batch=BATCH,
63 log=lambda *_: None)
64 if return_net:
65 return metric, net
66 return metric
67
68
69def baseline_factory(cfg):
70 return lambda seed: train_one(seed, cfg, 'gaussian')
71
72
73def stats_from_net(net):
74 w = net[0].weight.detach().cpu().numpy()
75 d = np.sqrt(((w[:, None] - w[None, :])**2).sum(-1))
76 np.fill_diagonal(d, np.inf)
77 return float(np.median(d.min(axis=1))), float(np.mean(d.min(axis=1))), float(np.std(w, axis=0).mean())
78
79
80def main():
81 # Baseline is tuned on the protocol's four-seed sweep, then re-evaluated on 8.
82 base = sweep_baseline(baseline_factory, GRID)
83 best = base['best_cfg']
84 idea_grid = [best, {'lr': 1e-3, 'scale': 0.7}, {'lr': 1e-2, 'scale': 1.3}]
85 # Deduplicate while preserving the required three-config idea budget.
86 uniq = []
87 for c in idea_grid:
88 if c not in uniq: uniq.append(c)
89 idea_cfg = min(uniq, key=lambda c: np.mean([train_one(s, c, 'coulomb') for s in (0,1,2,3)]))
90 idea_vals = [train_one(s, idea_cfg, 'coulomb') for s in DEFAULT_SEEDS]
91 # Paired trained-model signature: compare final first-layer nearest-neighbor spacing.
92 bnn, inn = [], []
93 for s in DEFAULT_SEEDS:
94 _, bn = train_one(s, best, 'gaussian', True)
95 _, cn = train_one(s, idea_cfg, 'coulomb', True)
96 bnn.append(stats_from_net(bn))
97 inn.append(stats_from_net(cn))
98 bmean = [float(np.mean(x)) for x in zip(*bnn)]
99 imean = [float(np.mean(x)) for x in zip(*inn)]
100 # The stage-1 prediction is higher non-collapse spacing after training;
101 # confirmation requires a positive, practically nontrivial observed ratio.
102 ratio = imean[0] / max(bmean[0], 1e-12)
103 sig = {'quantity': 'trained first-layer median nearest-neighbor distance',
104 'prediction': 'Coulomb repulsion preserves larger inter-row spacing as N=64 bank grows',
105 'baseline_observed': bmean[0], 'idea_observed': imean[0],
106 'observed_ratio_idea_over_baseline': ratio,
107 'confirmed': bool(ratio > 1.05),
108 'n_models': 8}
109 report = make_report(TRACK, MODEL, base, {'cfg': idea_cfg, 'mean': float(np.mean(idea_vals)),
110 'std': float(np.std(idea_vals)), 'per_seed': [float(x) for x in idea_vals],
111 'n': len(idea_vals)},
112 {'mechanism_signature': sig,
113 'track_choice': 'tabular: initialization is explicitly covered by the benchmark mapping; same mlp_tiny and training loop used.',
114 'idea_sweep': [{'cfg': c} for c in uniq],
115 'trained_geometry': {'baseline_median_nn': bmean[0], 'idea_median_nn': imean[0],
116 'baseline_mean_nn': bmean[1], 'idea_mean_nn': imean[1]}})
117 Path('bench_report.json').write_text(json.dumps(report, indent=2))
118 print(json.dumps(report, indent=2))
119
120if __name__ == '__main__':
121 main()