Spectral pinning of neural modules / stage2_bench.py

✓✓ Beats tuned baseline

Raw ⬇ ZIP
  1import sys, json, random
  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, sweep_baseline, evaluate, make_report
  8
  9NMOD = 4
 10EPOCHS = 10
 11BATCH = 128
 12SEEDS = tuple(range(8))
 13SWEEP_SEEDS = tuple(range(4))
 14
 15
 16def graph_laplacian():
 17    A = np.array([[0,.8,.2,.1],[.8,0,.5,.15],[.2,.5,0,.7],[.1,.15,.7,0]], dtype=float)
 18    return A, np.diag(A.sum(1)) - A
 19
 20
 21def grounded_gap(L, pins, strength):
 22    p = np.zeros(len(L)); p[list(pins)] = strength
 23    return float(np.linalg.eigvalsh(L + np.diag(p))[0])
 24
 25
 26def greedy_pins(L, m=2, strength=.5):
 27    chosen = []
 28    for _ in range(m):
 29        candidates = [(grounded_gap(L, chosen + [i], strength), i)
 30                      for i in range(len(L)) if i not in chosen]
 31        chosen.append(max(candidates, key=lambda z: (z[0], -z[1]))[1])
 32    return chosen, grounded_gap(L, chosen, strength)
 33
 34
 35class Module(nn.Module):
 36    def __init__(self):
 37        super().__init__()
 38        self.rnn = nn.GRU(3, 32, batch_first=True)
 39        self.head = nn.Linear(32, 1)
 40
 41    def forward(self, x):
 42        _, h = self.rnn(x.view(x.shape[0], 8, 3))
 43        return self.head(h[-1]), h[-1]
 44
 45
 46def train_system(seed, lr, consensus, pin_strength=0.0, return_signature=False):
 47    random.seed(seed); np.random.seed(seed); torch.manual_seed(seed)
 48    try:
 49        device = "cuda" if torch.cuda.is_available() else "cpu"
 50        if device == "cuda":
 51            try:
 52                torch.cuda.set_device(0)
 53                torch.backends.cudnn.enabled = False
 54            except Exception:
 55                device = "cpu"
 56    except Exception:
 57        device = "cpu"
 58    ds = get_dataset("dynamics", seed, n_train=400, n_test=200)
 59    xtr, ytr = ds["xtr"].to(device), ds["ytr"].to(device).reshape(-1, 1)
 60    xte, yte = ds["xte"].to(device), ds["yte"].to(device).reshape(-1, 1)
 61    nets = nn.ModuleList([Module() for _ in range(NMOD)]).to(device)
 62    opt = torch.optim.Adam(nets.parameters(), lr=lr)
 63    _, L = graph_laplacian()
 64    pins, gap = greedy_pins(L, 2, pin_strength if pin_strength else .5)
 65    pinmask = torch.zeros(NMOD, device=device)
 66    if pin_strength > 0: pinmask[pins] = 1.
 67    history = []
 68    for ep in range(EPOCHS):
 69        perm = torch.randperm(len(xtr), device=device)
 70        for start in range(0, len(xtr), BATCH):
 71            idx = perm[start:start+BATCH]
 72            preds, hs = [], []
 73            for net in nets:
 74                q, h = net(xtr[idx]); preds.append(q); hs.append(h)
 75            pred = torch.stack(preds)
 76            h = torch.stack(hs)
 77            loss = ((pred - ytr[idx].unsqueeze(0)) ** 2).mean()
 78            # L2 graph-consensus regularization, plus stronger corrective
 79            # teacher/anchor loss on spectral-greedy pinned modules.
 80            diff = h[:, None] - h[None, :]
 81            loss = loss + consensus * sum(L[i,j] * (h[i]-h[j]).pow(2).mean()
 82                                          for i in range(NMOD) for j in range(NMOD)) / 2
 83            if pin_strength > 0:
 84                mean = h.mean(0, keepdim=True).detach()
 85                loss = loss + pin_strength * ((h - mean).pow(2).mean(1) * pinmask[:, None]).mean()
 86            opt.zero_grad(); loss.backward(); opt.step()
 87        with torch.no_grad():
 88            ev = torch.stack([net(xte)[0] for net in nets])
 89            history.append(float(((ev-yte.unsqueeze(0))**2).mean()))
 90    with torch.no_grad():
 91        out = torch.stack([net(xte)[0] for net in nets])
 92        mse = float(((out-yte.unsqueeze(0))**2).mean())
 93        module_mean = out.mean(0, keepdim=True)
 94        disagreement = float(.5 * ((out-module_mean)**2).mean())
 95        # Measured trained-model behavior, not an analytical identity.
 96        train_h = torch.stack([net(xte)[1] for net in nets])
 97        measured_dis = float(.5 * ((train_h-train_h.mean(0,keepdim=True))**2).mean())
 98    if return_signature:
 99        return mse, {"pins": pins, "lambda_min_grounded": gap,
100                      "test_representation_disagreement": measured_dis,
101                      "task_prediction_disagreement": disagreement}
102    return mse
103
104
105def make_fn(cfg):
106    return lambda seed: train_system(seed, cfg["lr"], cfg["consensus"], 0.0)
107
108
109def main():
110    # Search-space parity: every idea lr/consensus pair is also evaluated by
111    # the baseline sweep. The baseline method's central consensus knob is swept.
112    grid = [{"lr": lr, "consensus": c} for lr in [1e-3, 3e-3, 1e-2]
113            for c in [0.0, 0.01, 0.05]]
114    base = sweep_baseline(make_fn, grid, seeds=SWEEP_SEEDS)
115    best = base["best_cfg"]
116    idea_cfgs = [best,
117                 {"lr": best["lr"], "consensus": best["consensus"] + .01},
118                 {"lr": best["lr"], "consensus": max(0.0, best["consensus"] - .01)}]
119    # Keep the idea's three settings on the same union grid where possible.
120    all_grid = list(grid)
121    for cfg in idea_cfgs:
122        if cfg not in all_grid: all_grid.append(cfg)
123    base = sweep_baseline(make_fn, all_grid, seeds=SWEEP_SEEDS)
124    idea_trials = []
125    for cfg in idea_cfgs:
126        r = evaluate(lambda s, cfg=cfg: train_system(s, cfg["lr"], cfg["consensus"], .5), seeds=SEEDS)
127        idea_trials.append({"cfg": cfg, "result": r})
128    chosen = min(idea_trials, key=lambda z: z["result"]["mean"])
129    idea = chosen["result"]
130    best_idea_cfg = chosen["cfg"]
131    sig_mse, sig = train_system(0, best_idea_cfg["lr"], best_idea_cfg["consensus"], .5, True)
132    _, L = graph_laplacian()
133    random_gap = float(np.mean([grounded_gap(L, p, .5) for p in ([0,1],[1,2],[2,3],[0,3])]))
134    sig.update({"baseline_random_gap": random_gap,
135                "observed_vs_predicted": "task-independent disagreement is measured on trained representations; no exact decay slope is identifiable from final snapshots",
136                "confirmed": False})
137    report = make_report("dynamics", "rnn_small", base, idea, sig)
138    report["idea_sweep"] = idea_trials
139    report["baseline_sweep_union"] = all_grid
140    with open("bench_report.json", "w") as f: json.dump(report, f, indent=2)
141    print(json.dumps(report, indent=2))
142
143if __name__ == "__main__": main()