Symmetry-Quotiented Local Correlation Encoder / run_bench.py

✓✓ Beats tuned baseline

Raw ⬇ ZIP
  1import json
  2import sys
  3from pathlib import Path
  4import numpy as np
  5import torch
  6
  7sys.path.insert(0, "/home/maxwelhelp/all/math2nn")
  8from bench import make_model, train_model, sweep_baseline, evaluate, make_report, get_dataset
  9import orientation_track as ot
 10
 11SEEDS = tuple(range(8))
 12LR_GRID = [1e-3, 3e-3, 1e-2]
 13EPOCHS = 18
 14BATCH = 128
 15
 16
 17def seed_all(seed):
 18    np.random.seed(seed)
 19    torch.manual_seed(seed)
 20    if torch.cuda.is_available():
 21        torch.cuda.manual_seed_all(seed)
 22
 23
 24def ds_torch(d):
 25    out = dict(d)
 26    out["xtr"] = torch.tensor(d["xtr"], dtype=torch.float32)
 27    out["ytr"] = torch.tensor(d["ytr"], dtype=torch.long)
 28    out["xte"] = torch.tensor(d["xte"], dtype=torch.float32)
 29    out["yte"] = torch.tensor(d["yte"], dtype=torch.long)
 30    return out
 31
 32
 33def train_one(seed, lr, invariant):
 34    seed_all(seed)
 35    raw = get_dataset("orientation_phase_quotient", seed, 400, 160)
 36    raw_np = dict(raw)
 37    for k in ("xtr", "ytr", "xte", "yte"):
 38        if hasattr(raw_np[k], "detach"):
 39            raw_np[k] = raw_np[k].detach().cpu().numpy()
 40    d = ot.invariant_dataset(raw_np) if invariant else raw_np
 41    d = ds_torch(d)
 42    net = make_model("mlp_tiny", d["input_shape"], d["out_dim"])
 43    _, metric, _ = train_model(net, d, epochs=EPOCHS, lr=lr, batch=BATCH, log=lambda *_: None)
 44    return float(metric)
 45
 46
 47def baseline_factory(cfg):
 48    return lambda seed: train_one(seed, float(cfg["lr"]), False)
 49
 50
 51def idea_factory(cfg):
 52    return lambda seed: train_one(seed, float(cfg["lr"]), True)
 53
 54
 55def trained_signature(seed, lr):
 56    seed_all(seed)
 57    raw = get_dataset("orientation_phase_quotient", seed, 400, 160)
 58    raw_np = dict(raw)
 59    for k in ("xtr", "ytr", "xte", "yte"):
 60        if hasattr(raw_np[k], "detach"):
 61            raw_np[k] = raw_np[k].detach().cpu().numpy()
 62    inv = ot.invariant_dataset(raw_np)
 63    dr, di = ds_torch(raw_np), ds_torch(inv)
 64    br = make_model("mlp_tiny", dr["input_shape"], dr["out_dim"])
 65    bi = make_model("mlp_tiny", di["input_shape"], di["out_dim"])
 66    br, _, _ = train_model(br, dr, epochs=EPOCHS, lr=lr, batch=BATCH, log=lambda *_: None)
 67    bi, _, _ = train_model(bi, di, epochs=EPOCHS, lr=lr, batch=BATCH, log=lambda *_: None)
 68    rng = np.random.default_rng(seed + 991)
 69    u = raw_np["xte"].reshape(-1, 4 ** 3, 3)
 70    q, r = np.linalg.qr(rng.normal(size=(3, 3)))
 71    q = q @ np.diag(np.where(np.diag(r) >= 0, 1.0, -1.0))
 72    if np.linalg.det(q) < 0:
 73        q[:, 0] *= -1
 74    transformed = (u @ q.T) * rng.choice([-1.0, 1.0], size=(len(u), 4 ** 3, 1))
 75    raw2 = dict(raw_np)
 76    raw2["xte"] = transformed.reshape(len(u), -1).astype(np.float32)
 77    inv2 = ot.invariant_dataset(raw2)
 78    with torch.no_grad():
 79        devr = next(br.parameters()).device
 80        devi = next(bi.parameters()).device
 81        pbr = torch.softmax(br(torch.tensor(raw["xte"], dtype=torch.float32, device=devr)), 1)
 82        pbr2 = torch.softmax(br(torch.tensor(raw2["xte"], dtype=torch.float32, device=devr)), 1)
 83        pbi = torch.softmax(bi(torch.tensor(inv["xte"], dtype=torch.float32, device=devi)), 1)
 84        pbi2 = torch.softmax(bi(torch.tensor(inv2["xte"], dtype=torch.float32, device=devi)), 1)
 85    return {
 86        "baseline_mean_output_change": float(torch.abs(pbr - pbr2).mean().cpu()),
 87        "idea_mean_output_change": float(torch.abs(pbi - pbi2).mean().cpu()),
 88        "baseline_max_output_change": float(torch.abs(pbr - pbr2).max().cpu()),
 89        "idea_max_output_change": float(torch.abs(pbi - pbi2).max().cpu()),
 90    }
 91
 92
 93def main():
 94    # Core numerical check is performed before any neural training.
 95    math_err = ot.math_check()
 96    grid = [{"lr": x} for x in LR_GRID]
 97    base = sweep_baseline(baseline_factory, grid, seeds=(0, 1, 2, 3))
 98    idea_sweep = []
 99    for cfg in grid:
100        short = evaluate(idea_factory(cfg), seeds=(0, 1, 2, 3))
101        idea_sweep.append({"cfg": cfg, "mean": short["mean"]})
102    best_idea_cfg = min(grid, key=lambda c: next(x["mean"] for x in idea_sweep if x["cfg"] == c))
103    idea_full = evaluate(idea_factory(best_idea_cfg), seeds=SEEDS)
104    sig = trained_signature(0, float(best_idea_cfg["lr"]))
105    sig["predicted_invariant_output_change"] = 0.0
106    sig["observed_feature_change"] = math_err
107    sig["confirmed"] = bool(math_err < 1e-6 and sig["idea_mean_output_change"] < sig["baseline_mean_output_change"])
108    sig["prediction"] = "P2 local-correlation inputs and their trained predictions should be unchanged by global rotation and independent apolar flips."
109    report = make_report(
110        "orientation_phase_quotient", "mlp_tiny",
111        base,
112        dict(idea_full, best_cfg=best_idea_cfg, sweep=idea_sweep),
113        extra=sig,
114    )
115    result = {
116        "bench_report": report,
117        "custom_track": {"name": "orientation_phase_quotient", "file": "orientation_track.py", "domain": "molecular_orientation_symmetry"},
118        "math_check": {"max_transformed_feature_error": math_err, "predicted": 0.0},
119        "protocol": {"epochs": EPOCHS, "batch": BATCH, "lr_union": LR_GRID, "paired_seeds": list(SEEDS)},
120    }
121    Path("bench_results.json").write_text(json.dumps(result, indent=2, sort_keys=True))
122    print(json.dumps(result, indent=2, sort_keys=True))
123
124
125if __name__ == "__main__":
126    main()