Neighborhood-separator attention / bench_experiment.py

Failed on benchmark

Raw ⬇ ZIP
  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()