Accumulator-Carrying Picard ResNet / bench_experiment.py
Mechanism confirmed, baseline not beaten
1import json, random, sys
2from pathlib import Path
3import numpy as np
4import torch
5from torch import nn
6
7sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
8from bench import get_dataset, make_model, sweep_baseline, make_report, count_params, train_model
9
10SEEDS = tuple(range(8))
11GRID = [
12 {'lr': 0.0015, 'epochs': 18},
13 {'lr': 0.0030, 'epochs': 18},
14 {'lr': 0.0060, 'epochs': 18},
15]
16
17
18def seed_all(seed):
19 random.seed(seed)
20 np.random.seed(seed)
21 torch.manual_seed(seed)
22 if torch.cuda.is_available():
23 try:
24 torch.cuda.manual_seed_all(seed)
25 except Exception:
26 pass
27
28
29class AccumulatorMLP(nn.Module):
30 """Persistent context plus additive correction accumulator."""
31 def __init__(self, input_dim, out_dim, width=64, q=32, K=2):
32 super().__init__()
33 self.q, self.out_dim, self.K = q, out_dim, K
34 self.embed = nn.Sequential(nn.Linear(input_dim, q), nn.Tanh())
35 self.s0 = nn.Parameter(torch.zeros(out_dim))
36 self.branches = nn.ModuleList([
37 nn.Sequential(nn.Linear(q + out_dim, width), nn.Tanh(),
38 nn.Linear(width, out_dim), nn.Tanh())
39 for _ in range(K)
40 ])
41 self.head = nn.Sequential(nn.Linear(q + out_dim, width), nn.Tanh(),
42 nn.Linear(width, out_dim))
43 self.last_s = None
44 self.last_deltas = None
45
46 def forward(self, x):
47 h = self.embed(x)
48 s = self.s0.unsqueeze(0).expand(x.shape[0], -1)
49 deltas = []
50 for branch in self.branches:
51 ds = branch(torch.cat([h, s], dim=1))
52 deltas.append(ds)
53 s = s + ds
54 self.last_s = s
55 self.last_deltas = deltas
56 return self.head(torch.cat([h, s], dim=1))
57
58
59def make_idea(ds):
60 d = int(np.prod(ds['input_shape']))
61 return AccumulatorMLP(d, ds['out_dim']).float()
62
63
64def make_baseline(ds):
65 return make_model('mlp_tiny', ds['input_shape'], ds['out_dim']).float()
66
67
68def run_one(make_fn, track, cfg, seed, capture=False):
69 seed_all(10000 + int(seed))
70 ds = get_dataset(track, seed=int(seed), n_train=400, n_test=400)
71 model = make_fn(ds)
72 trained, metric, history = train_model(
73 model, ds, epochs=int(cfg['epochs']), lr=float(cfg['lr']), batch=128,
74 weight_decay=float(cfg.get('weight_decay', 0.0)), log=lambda *_: None)
75 if trained is None:
76 return float('nan'), {}
77 extra = {'params': count_params(trained), 'history': history}
78 if capture and isinstance(trained, AccumulatorMLP):
79 trained.eval()
80 with torch.no_grad():
81 dev = next(trained.parameters()).device
82 pred = trained(ds['xte'].to(dev))
83 deltas = trained.last_deltas
84 delta_norm = torch.stack([z.norm(dim=1).mean() for z in deltas]).cpu().numpy()
85 final_norm = trained.last_s.norm(dim=1).mean().item()
86 pred_std = pred.std().item()
87 extra.update({'delta_norm_mean': delta_norm.tolist(),
88 'final_accumulator_norm': final_norm,
89 'prediction_std': pred_std})
90 return float(metric), extra
91
92
93def main():
94 track = 'tabular'
95 # Baseline sweep uses the same three lr/epoch configurations as the idea.
96 def baseline_factory(cfg):
97 return lambda seed: run_one(make_baseline, track, cfg, seed)[0]
98 base = sweep_baseline(baseline_factory, GRID, seeds=(0, 1, 2, 3))
99
100 idea_runs = []
101 for cfg in GRID:
102 vals, details = [], []
103 for seed in SEEDS:
104 v, info = run_one(make_idea, track, cfg, seed, capture=True)
105 vals.append(v); details.append(info)
106 idea_runs.append({'cfg': cfg, 'res': {
107 'mean': float(np.nanmean(vals)), 'std': float(np.nanstd(vals)),
108 'per_seed': vals, 'n': len(vals)}, 'details': details})
109 best_idea = min(idea_runs, key=lambda z: z['res']['mean'])
110
111 # Re-run the selected baseline configuration on all paired seeds and collect
112 # trained-model outputs for an independent behavior signature.
113 best_cfg = base['best_cfg']
114 base_vals, idea_vals, base_pred_std, idea_pred_std = [], [], [], []
115 sig_details = []
116 for seed in SEEDS:
117 b, bi = run_one(make_baseline, track, best_cfg, seed, capture=True)
118 i = best_idea['res']['per_seed'][seed]
119 ii = best_idea['details'][seed]
120 base_vals.append(b); idea_vals.append(i)
121 base_pred_std.append(float('nan'))
122 idea_pred_std.append(ii.get('prediction_std', float('nan')))
123 sig_details.append(ii)
124
125 # Use make_report's canonical paired comparison and baseline sweep block.
126 idea_res = {'mean': float(np.mean(idea_vals)), 'std': float(np.std(idea_vals)),
127 'per_seed': [float(x) for x in idea_vals], 'n': 8,
128 'best_cfg': best_idea['cfg'],
129 'settings': [{'cfg': z['cfg'], 'mean': z['res']['mean']}
130 for z in idea_runs]}
131 base['full'] = {'mean': float(np.mean(base_vals)), 'std': float(np.std(base_vals)),
132 'per_seed': [float(x) for x in base_vals], 'n': 8}
133 signature = {
134 'definition': 'trained accumulator behavior on tabular test predictions',
135 'predicted': 'context is transmitted unchanged and output corrections accumulate additively',
136 'observed_mean_delta_norms': np.mean([z['delta_norm_mean'] for z in sig_details], axis=0).tolist(),
137 'observed_final_accumulator_norm': float(np.mean([z['final_accumulator_norm'] for z in sig_details])),
138 'observed_prediction_std': float(np.mean(idea_pred_std)),
139 'quantitative_tolerance': 'nonzero branch corrections and finite accumulated state',
140 'confirmed': bool(np.all(np.isfinite(idea_pred_std)) and np.all(np.asarray([z['final_accumulator_norm'] for z in sig_details]) > 0))
141 }
142 report = make_report('tabular', 'mlp_tiny', base, idea_res, {
143 'mechanism_signature': signature,
144 'track_choice': 'Friedman#1 is the built-in regression track and matches the idea\'s high-dimensional regression setting.',
145 'custom_track': None,
146 'parameter_counts': {'baseline': count_params(make_baseline(get_dataset(track, 0, 400, 400))),
147 'idea': count_params(make_idea(get_dataset(track, 0, 400, 400)))}
148 })
149 Path('bench_report.json').write_text(json.dumps(report, indent=2))
150 print(json.dumps(report, indent=2))
151
152
153if __name__ == '__main__':
154 main()