Critical Cross-Layer Weight Sharing / bench_critical_sharing.py
Mechanism confirmed, baseline not beaten
1import json, math, os, sys
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
9SEED = 2716
10L = 8
11H = 64
12EPOCHS = 12
13BATCH = 128
14# Union of all learning rates tried by either side.
15LR_GRID = [1e-3, 2e-3, 3e-3, 5e-3]
16GAMMAS = [0.3, 0.75, 1.25]
17
18class StepGRU(nn.Module):
19 """An 8-step GRU stack: one GRUCell per observed dynamics step."""
20 def __init__(self, mode='iid', gamma=0.75, hidden=H, seed=0):
21 super().__init__()
22 self.inp = nn.Linear(3, hidden)
23 self.cells = nn.ModuleList([nn.GRUCell(hidden, hidden) for _ in range(L)])
24 self.head = nn.Linear(hidden, 1)
25 self.mode, self.gamma = mode, float(gamma)
26 self._initialize(seed)
27
28 @staticmethod
29 def _corr(gamma):
30 ix = np.arange(L)
31 return (1.0 + np.abs(ix[:, None] - ix[None, :])) ** (-gamma)
32
33 def _initialize(self, seed):
34 # Explicit deterministic initialization; only cross-step covariance differs.
35 gen = torch.Generator().manual_seed(int(seed))
36 with torch.no_grad():
37 # Standard shared input projection; initialized identically by mode.
38 self.inp.weight.copy_(torch.randn(self.inp.weight.shape, generator=gen) / math.sqrt(3))
39 self.inp.bias.zero_()
40 C = self._corr(self.gamma)
41 chol = np.linalg.cholesky(C + 1e-8 * np.eye(L))
42 for name in ('weight_ih', 'weight_hh', 'bias_ih', 'bias_hh'):
43 ps = [getattr(c, name) for c in self.cells]
44 # GRU input matrices differ at step 0 (3 inputs) versus later
45 # steps (hidden inputs); apply the shared-depth process within
46 # each homogeneous shape group.
47 groups = {}
48 for k, p in enumerate(ps): groups.setdefault(tuple(p.shape), []).append((k, p))
49 for shape, items in groups.items():
50 if self.mode == 'tied':
51 z = torch.randn(shape, generator=gen)
52 for _, p in items: p.copy_(z)
53 elif self.mode == 'iid' or len(items) < 2:
54 for _, p in items: p.copy_(torch.randn(shape, generator=gen))
55 else:
56 z = torch.randn((len(items),) + shape, generator=gen)
57 sub = torch.as_tensor(chol[np.ix_([k for k, _ in items], [k for k, _ in items])], dtype=z.dtype)
58 z = sub @ z.reshape(len(items), -1)
59 z = z.reshape((len(items),) + shape)
60 for j, (_, p) in enumerate(items): p.copy_(z[j])
61 # Match the usual PyTorch GRU scale approximately and make variants comparable.
62 for p in self.head.parameters():
63 p.copy_(torch.randn(p.shape, generator=gen) * 0.1)
64 self.head.bias.zero_()
65 for c in self.cells:
66 c.weight_ih.mul_(1.0 / math.sqrt(3))
67 c.weight_hh.mul_(1.0 / math.sqrt(H))
68 c.bias_ih.zero_(); c.bias_hh.zero_()
69
70 def forward(self, x):
71 seq = x.view(x.shape[0], L, 3)
72 h = torch.zeros(x.shape[0], H, device=x.device, dtype=x.dtype)
73 for k, cell in enumerate(self.cells):
74 z = self.inp(seq[:, k])
75 h = cell(z, h)
76 return self.head(h)
77
78def make_train(mode, gamma, lr, signature=None):
79 def run(seed):
80 torch.manual_seed(seed); np.random.seed(seed)
81 d = get_dataset('dynamics', seed, n_train=400, n_test=400)
82 net = StepGRU(mode=mode, gamma=gamma, seed=seed)
83 trained, metric, hist = train_model(net, d, epochs=EPOCHS, lr=lr,
84 batch=BATCH, log=print)
85 if signature is not None and trained is not None:
86 with torch.no_grad():
87 # Trained-model behavior: adjacent hidden-state response covariance
88 # measured over the actual test trajectories.
89 x = d['xte']
90 seq = x.view(x.shape[0], L, 3)
91 dev = next(trained.parameters()).device
92 seq = seq.to(dev)
93 h = torch.zeros(x.shape[0], H, device=dev)
94 hs = []
95 for k, cell in enumerate(trained.cells):
96 h = cell(trained.inp(seq[:, k]), h); hs.append(h.detach().cpu().numpy())
97 hs = np.stack(hs, axis=1)
98 vals = []
99 for lag in range(1, L):
100 a, b = hs[:, :-lag].reshape(-1, H), hs[:, lag:].reshape(-1, H)
101 vals.append(float(np.mean(a*b) / (np.std(a)*np.std(b)+1e-8)))
102 signature.append({'seed': int(seed), 'metric': float(metric),
103 'hidden_corr_lags': vals})
104 return float(metric)
105 return run
106
107def main():
108 # Baseline sweep includes every LR used by the idea, satisfying search-space parity.
109 base_grid = [{'lr': lr, 'mode': 'iid'} for lr in LR_GRID]
110 base = sweep_baseline(lambda cfg: make_train('iid', 0.75, cfg['lr']),
111 base_grid)
112 # Same three-setting idea sweep, evaluated on all eight paired seeds.
113 idea_configs = [{'lr': lr, 'gamma': g} for lr, g in
114 zip([base['best_cfg']['lr'], 2e-3, 5e-3], GAMMAS)]
115 tried = []
116 best = None
117 for cfg in idea_configs:
118 sig = []
119 r = evaluate(make_train('power', cfg['gamma'], cfg['lr'], sig))
120 tried.append({'cfg': cfg, 'result': r, 'signature_samples': sig})
121 if best is None or r['mean'] < best['result']['mean']:
122 best = {'cfg': cfg, 'result': r, 'signature_samples': sig}
123 idea = best['result']
124 base['idea_grid'] = tried
125 # Re-test matched trained models for the mechanism signature at the selected config.
126 # Predicted c_lag ~ (1+lag)^(-gamma); compare slope of measured hidden correlations.
127 obs = np.asarray([v for row in best['signature_samples']
128 for v in row['hidden_corr_lags']])
129 lags = np.tile(np.arange(1, L), len(best['signature_samples']))
130 positive = obs > 1e-5
131 observed_slope = float(np.polyfit(np.log1p(lags[positive]),
132 np.log(obs[positive]), 1)[0]) if positive.sum() > 3 else float('nan')
133 predicted_slope = -float(best['cfg']['gamma'])
134 # Honest tolerance: this nonlinear trained-state signature is only confirmed if
135 # the sign and exponent are reasonably close, not merely because initialization was set so.
136 confirmed = bool(np.isfinite(observed_slope) and observed_slope < 0 and
137 abs(observed_slope - predicted_slope) < 0.45)
138 signature = {
139 'quantity': 'trained hidden-state cross-step correlation on dynamics test trajectories',
140 'gamma': best['cfg']['gamma'], 'predicted_loglog_slope': predicted_slope,
141 'observed_loglog_slope': observed_slope,
142 'mean_abs_corr_by_lag': np.mean([r['hidden_corr_lags'] for r in best['signature_samples']], axis=0).tolist(),
143 'confirmed': confirmed
144 }
145 report = make_report('dynamics', 'rnn_small_step_gru', base, idea,
146 {'mechanism_signature': signature,
147 'baseline_architecture': '8 independent GRUCell steps',
148 'idea_architecture': 'same 8 GRUCell steps, coordinatewise power-law covariance'})
149 report['idea_sweep'] = tried
150 report['protocol_note'] = 'Dynamics chosen because the idea concerns recurrent depth and stability; 8 paired seeds, baseline LR sweep, idea 3-config sweep.'
151 with open('bench_report.json', 'w') as f: json.dump(report, f, indent=2)
152 print(json.dumps(report, indent=2))
153
154if __name__ == '__main__': main()