Finite-Excitation Orthogonal Gradient Memory / bench_experiment.py

Failed on benchmark

Raw ⬇ ZIP
  1import sys, json, random
  2from pathlib import Path
  3import numpy as np
  4import torch
  5import torch.nn as nn
  6sys.path.insert(0, "/home/maxwelhelp/all/math2nn")
  7from bench import get_dataset, make_model, evaluate, sweep_baseline, make_report
  8
  9TRACK, MODEL = "tabular", "mlp_tiny"
 10EPOCHS, BATCH = 8, 128
 11LRS = [0.001, 0.003, 0.006]  # identical union on baseline and idea sides
 12LAMBDAS = [0.02, 0.05, 0.10]
 13torch.set_num_threads(1)
 14
 15
 16def seed_all(s):
 17    random.seed(s); np.random.seed(s); torch.manual_seed(s)
 18    if torch.cuda.is_available():
 19        try: torch.cuda.manual_seed_all(s)
 20        except Exception: pass
 21
 22
 23def mgs_numpy(Z, Y, d, eps=1e-6):
 24    qs, ys, res = [], [], []
 25    for z, y in zip(Z, Y):
 26        nz = np.linalg.norm(z)
 27        if nz < eps: continue
 28        u = z / nz; v = u.copy()
 29        for q in qs: v -= q * np.dot(q, v)
 30        nv = np.linalg.norm(v); res.append(nv)
 31        if nv > eps:
 32            qs.append(v / nv); ys.append(y / nz)
 33            if len(qs) == d: break
 34    if not qs: return None, None, res
 35    return np.asarray(qs, dtype=np.float32), np.asarray(ys, dtype=np.float32), res
 36
 37
 38def run(seed, lr, lam=0.0, signature=False):
 39    seed_all(seed)
 40    ds = get_dataset(TRACK, seed=seed, n_train=400, n_test=200)
 41    net = make_model(MODEL, ds["input_shape"], ds["out_dim"])
 42    use_cuda = torch.cuda.is_available()
 43    device = "cuda" if use_cuda else "cpu"
 44    try:
 45        net.to(device)
 46        x, y = ds["xtr"].to(device), ds["ytr"].to(device)
 47        xt, yt = ds["xte"].to(device), ds["yte"].to(device)
 48        opt = torch.optim.Adam(net.parameters(), lr=lr)
 49        lossf = nn.MSELoss()
 50        head = [m for m in net.modules() if isinstance(m, nn.Linear)][-1]
 51        memz, memy, Q = [], [], None
 52        ranks, orth, eigs = [], [], []
 53        for ep in range(EPOCHS):
 54            net.train(); perm = torch.randperm(len(x), device=device)
 55            for st in range(0, len(x), BATCH):
 56                ix = perm[st:st+BATCH]; loss = lossf(net(x[ix]), y[ix])
 57                opt.zero_grad(); loss.backward(); opt.step()
 58                if lam:
 59                    with torch.no_grad():
 60                        h = x[ix]
 61                        for layer in list(net.children())[:-1]: h = layer(h)
 62                        memz.append(h.detach().cpu().numpy()); memy.append(y[ix].detach().cpu().numpy())
 63            if lam:
 64                # Refresh once per epoch, using at most 256 recent examples.
 65                Z = np.concatenate(memz)[-256:]; Y = np.concatenate(memy)[-256:]
 66                q, qy, _ = mgs_numpy(Z, Y, head.in_features)
 67                if q is not None:
 68                    Q = torch.from_numpy(q).to(device); Qy = torch.from_numpy(qy).to(device)
 69                    with torch.no_grad():
 70                        old = head.weight.detach().clone()
 71                        residual = Qy - Q @ head.weight.T
 72                        head.weight.sub_(lr * lam * (residual.T @ Q))
 73                        G = Q.T @ Q
 74                        ranks.append(int(Q.shape[1])); orth.append(float(torch.max(torch.abs(G-torch.eye(G.shape[0],device=device))).cpu()))
 75                        eigs.append(float(torch.linalg.eigvalsh(G).max().cpu()))
 76        with torch.no_grad(): metric = float(torch.mean((net(xt)-yt)**2).cpu())
 77        sig = None
 78        if signature and eigs:
 79            sig = {"feature_dim": head.in_features, "observed_rank_last": ranks[-1],
 80                   "predicted_identity_max_eigen_mean": float(np.mean(eigs)),
 81                   "observed_QtQ_error_max": max(orth),
 82                   "prediction": "MGS memory predicts identity Gramian after d independent directions",
 83                   "confirmed": bool(ranks[-1] == head.in_features and max(orth) < 1e-4 and abs(np.mean(eigs)-1)<1e-4)}
 84        return metric, sig
 85    except RuntimeError:
 86        if use_cuda:
 87            torch.cuda.empty_cache()
 88            # Explicit CPU retry, avoiding recursive CUDA detection.
 89            torch.cuda.is_available = lambda: False
 90            try: return run(seed, lr, lam, signature)
 91            finally: torch.cuda.is_available = lambda: True
 92        raise
 93
 94
 95def fn(cfg):
 96    return lambda s: run(s, cfg["lr"], cfg.get("lambda", 0.0))[0]
 97
 98
 99def main():
100    base = sweep_baseline(fn, [{"lr":lr, "weight_decay":0.0} for lr in LRS])
101    ideas = []
102    for lam in LAMBDAS:
103        cfg = {"lr": base["best_cfg"]["lr"], "lambda": lam}
104        ideas.append({"cfg":cfg, "result":evaluate(fn(cfg))})
105    best = min(ideas, key=lambda z:z["result"]["mean"])
106    _, sig = run(0, best["cfg"]["lr"], best["cfg"]["lambda"], True)
107    report = make_report(TRACK, MODEL, base, best["result"], {
108        "prediction": sig, "idea_configs": ideas,
109        "confirmed": bool(sig and sig["confirmed"])
110    })
111    report["track_justification"] = "Tabular is the prescribed optimizer track; Friedman regression with mlp_tiny has a trainable final linear head and fixed feature dimension, matching the memory correction."
112    Path("bench_report.json").write_text(json.dumps(report, indent=2))
113    print(json.dumps(report, indent=2))
114
115if __name__ == "__main__": main()