Neighborhood-separator attention / bench_experiment.py
Failed on benchmark
1import sys, json, math, random
2from pathlib import Path
3import numpy as np
4import torch
5import torch.nn as nn
6
7sys.path.insert(0, '/home/maxwelhelp/all/math2nn')
8from bench import get_dataset, make_model, train_model, sweep_baseline, make_report
9
10SEEDS = tuple(range(8))
11GRID = [{'lr': 1e-3}, {'lr': 3e-3}, {'lr': 6e-3}]
12EPOCHS, BATCH, NTR, NTE = 10, 128, 1200, 400
13
14
15def separator_mask_from_graph(adj, max_sep=2):
16 n = adj.shape[0]
17 active = np.ones(n, dtype=bool)
18 sep = []
19 for _ in range(max_sep):
20 tri = np.zeros(n, dtype=float)
21 deg = (adj & active[:, None] & active[None, :]).sum(1)
22 for i in np.flatnonzero(active):
23 ns = np.flatnonzero(adj[i] & active)
24 tri[i] = float(adj[np.ix_(ns, ns)].sum()) / 2.0
25 score = np.where(active, tri / (deg + 1.0), -1.0)
26 x = int(np.argmax(score))
27 if tri[x] < 1:
28 break
29 sep.append(x)
30 active[x] = False
31 active[adj[x]] = False
32 seen = np.zeros(n, dtype=bool)
33 comps = []
34 for st in np.flatnonzero(active):
35 if seen[st]:
36 continue
37 stack, c = [int(st)], []
38 seen[st] = True
39 while stack:
40 u = stack.pop(); c.append(u)
41 for v in np.flatnonzero(adj[u] & active):
42 if not seen[v]:
43 seen[v] = True; stack.append(int(v))
44 comps.append(np.asarray(c, dtype=int))
45 covered = np.zeros(n, dtype=bool)
46 if sep:
47 covered[sep] = True
48 covered |= adj[:, sep].any(axis=1)
49 mask = covered[:, None] | covered[None, :]
50 for c in comps:
51 mask[np.ix_(c, c)] = True
52 np.fill_diagonal(mask, True)
53 return mask, sep, comps, covered
54
55
56def affinity_graph(x, r=6):
57 # Cheap symmetric proxy from token embeddings, as specified by the idea.
58 with torch.no_grad():
59 z = x.detach().cpu().numpy().astype(np.float64)
60 # Rows are tokens; retain the sequence length rather than flattening d features.
61 s = (z @ z.T) / math.sqrt(max(1, z.shape[1]))
62 np.fill_diagonal(s, -np.inf)
63 n = len(z); r = min(r, max(1, n - 1))
64 a = np.zeros((n, n), dtype=bool)
65 for i in range(n):
66 a[i, np.argpartition(s[i], -r)[-r:]] = True
67 a |= a.T
68 np.fill_diagonal(a, False)
69 return a
70
71
72def structural_check():
73 g = np.zeros((10, 10), dtype=bool)
74 for block in ([0,1,2], [5,6,7]):
75 g[np.ix_(block, block)] = True
76 np.fill_diagonal(g, False)
77 m, sep, comps, covered = separator_mask_from_graph(g, 2)
78 anti = all(not g[np.ix_(comps[i], comps[j])].any()
79 for i in range(len(comps)) for j in range(i))
80 formula = covered[:, None] | covered[None, :]
81 for c in comps: formula[np.ix_(c, c)] = True
82 return {'separator_size': len(sep), 'components': [len(c) for c in comps],
83 'anti_adjacent_components': bool(anti),
84 'mask_formula_exact': bool(np.array_equal(m, formula))}
85
86
87class SeparatorTransformer(nn.Module):
88 """Benchmark-compatible transformer_tiny with graph-masked encoder attention."""
89 def __init__(self, win, out_dim=1, d=64, depth=2, r=6, max_sep=2):
90 super().__init__()
91 self.inp = nn.Linear(1, d)
92 self.pos = nn.Parameter(torch.zeros(1, win, d))
93 nn.init.normal_(self.pos, std=.02)
94 layer = nn.TransformerEncoderLayer(d, nhead=2, dim_feedforward=128,
95 batch_first=True, dropout=0.0)
96 self.enc = nn.TransformerEncoder(layer, depth)
97 self.head = nn.Linear(win * d, out_dim)
98 self.r, self.max_sep = r, max_sep
99 self.last_stats = {}
100
101 def forward(self, x):
102 h = self.inp(x.unsqueeze(-1)) + self.pos[:, :x.shape[1]]
103 # A single graph is built for the current sequence and reused per batch.
104 q = h[0].detach()
105 g = affinity_graph(q, self.r)
106 mask, sep, comps, covered = separator_mask_from_graph(g, self.max_sep)
107 # True means forbidden for TransformerEncoder.
108 attn_mask = torch.from_numpy(~mask).to(h.device)
109 h = self.enc(h, mask=attn_mask)
110 self.last_stats = {'allowed_fraction': float(mask.mean()),
111 'separator_size': len(sep),
112 'components': len(comps),
113 'covered_fraction': float(covered.mean())}
114 return self.head(h.reshape(h.shape[0], -1))
115
116
117def seed_all(seed):
118 random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
119 if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
120
121
122def train_one(seed, cfg, idea=False, capture=False):
123 seed_all(seed)
124 d = get_dataset('sequence', seed, n_train=NTR, n_test=NTE)
125 if idea:
126 net = SeparatorTransformer(d['input_shape'][0], d['ytr'].shape[1], r=6, max_sep=2)
127 else:
128 net = make_model('transformer_tiny', d['input_shape'], d['ytr'].shape[1])
129 net, metric, hist = train_model(net, d, epochs=EPOCHS, lr=cfg['lr'], batch=BATCH)
130 if metric is None: return float('nan'), {}
131 return float(metric), getattr(net, 'last_stats', {}) if capture else {}
132
133
134def main():
135 check = structural_check()
136 def base_factory(cfg):
137 return lambda seed: train_one(seed, cfg, False)[0]
138 base = sweep_baseline(base_factory, GRID, seeds=(0,1,2,3))
139 # Same union of lrs on both sides; idea is evaluated at best and two neighbors.
140 idea_cfgs = GRID
141 idea_vals = []
142 idea_stats = []
143 best_cfg = base['best_cfg']
144 for cfg in idea_cfgs:
145 vals = [train_one(s, cfg, True, False)[0] for s in SEEDS]
146 idea_vals.append({'cfg': cfg, 'mean': float(np.nanmean(vals)), 'per_seed': vals})
147 if cfg == best_cfg: best_vals = vals
148 idea = {'mean': float(np.nanmean(best_vals)), 'std': float(np.nanstd(best_vals)),
149 'per_seed': [float(v) for v in best_vals], 'n': len(best_vals),
150 'selected_cfg': best_cfg, 'sweep': idea_vals}
151 # Re-test trained behavior on each paired seed, not an analytic toy identity.
152 for s in SEEDS:
153 _, st = train_one(s, best_cfg, True, True)
154 idea_stats.append(st)
155 sig = {'predicted': {'sparse_allowed_fraction': '< 1.0', 'anti_adjacent_components': True},
156 'observed_mean_allowed_fraction': float(np.mean([x.get('allowed_fraction', 1.0) for x in idea_stats])),
157 'observed_mean_components': float(np.mean([x.get('components', 1) for x in idea_stats])),
158 'observed_mean_separator_size': float(np.mean([x.get('separator_size', 0) for x in idea_stats])),
159 'confirmed': bool(np.mean([x.get('allowed_fraction', 1.0) for x in idea_stats]) < 0.95 and
160 np.mean([x.get('components', 1) for x in idea_stats]) > 1)}
161 report = make_report('sequence', 'transformer_tiny', base, idea,
162 {'mechanism_signature': sig,
163 'structural_check': check,
164 'custom_track': None,
165 'protocol_notes': '8 paired seeds; baseline sweep and idea sweep share lr union.'})
166 Path('bench_report.json').write_text(json.dumps(report, indent=2))
167 print(json.dumps(report, indent=2))
168
169if __name__ == '__main__':
170 main()