Capacity-Preserving Transient Message Passing / bench_graph.py
Failed on benchmark
1import json, os, sys
2import numpy as np
3import torch
4import torch.nn as nn
5
6sys.path.insert(0, "/home/maxwelhelp/all/math2nn")
7from bench import train_model, evaluate, sweep_baseline, make_report
8from bench.protocol import DEFAULT_SEEDS
9from graph_track import get_dataset, META
10
11DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
12
13
14class GeometricMessageNet(nn.Module):
15 """Matched residual GNN; only the edge weighting changes."""
16 def __init__(self, weighted=True, alpha=.5, hidden=32, depth=4):
17 super().__init__()
18 self.weighted, self.alpha = weighted, alpha
19 self.layers = nn.ModuleList([nn.Linear(5, hidden)] +
20 [nn.Linear(hidden, hidden) for _ in range(depth - 1)])
21 self.head = nn.Sequential(nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, 1))
22
23 def propagation(self, x):
24 xy = x[:, :, 2:4]
25 d2 = ((xy[:, :, None] - xy[:, None, :]) ** 2).sum(-1)
26 ids = d2.topk(6, dim=-1, largest=False).indices[:, :, 1:6]
27 knn = torch.zeros_like(d2)
28 knn.scatter_(2, ids, 1.0)
29 knn = torch.maximum(knn, knn.transpose(1, 2))
30 w = knn * (d2.clamp_min(1e-6) if self.weighted else 1.0)
31 return w / w.sum(-1, keepdim=True).clamp_min(1e-8)
32
33 def forward(self, x):
34 p = self.propagation(x)
35 h = x
36 for layer in self.layers:
37 h = torch.relu(layer((1.0 - self.alpha) * h + self.alpha * torch.bmm(p, h)))
38 root = x[:, :, 4:5]
39 return self.head((h * root).sum(1) / root.sum(1).clamp_min(1.0))
40
41
42def tensor_ds(seed):
43 raw = get_dataset(seed, n_train=400, n_test=160)
44 return {**raw, "xtr": torch.as_tensor(raw["xtr"], dtype=torch.float32),
45 "ytr": torch.as_tensor(raw["ytr"], dtype=torch.float32).reshape(-1, 1),
46 "xte": torch.as_tensor(raw["xte"], dtype=torch.float32),
47 "yte": torch.as_tensor(raw["yte"], dtype=torch.float32).reshape(-1, 1)}
48
49
50def train_one(seed, weighted, lr, alpha=.5, epochs=18, return_model=False):
51 np.random.seed(seed); torch.manual_seed(seed)
52 ds = tensor_ds(seed)
53 net = GeometricMessageNet(weighted=weighted, alpha=alpha)
54 trained, metric, hist = train_model(net, ds, epochs=epochs, lr=lr,
55 batch=128, weight_decay=0.0, log=lambda *_: None)
56 if trained is None:
57 return (float("nan"), None, ds) if return_model else float("nan")
58 return (float(metric), trained, ds) if return_model else float(metric)
59
60
61def make_train_fn(cfg, weighted):
62 return lambda seed: train_one(seed, weighted, cfg["lr"], cfg["alpha"])
63
64
65def influence_signature(seed=0):
66 result = {}
67 for name, weighted in (("baseline", False), ("idea", True)):
68 metric, net, ds = train_one(seed, weighted, .003, return_model=True)
69 if net is None:
70 result[name] = {"test_mse": float("nan"), "error": "training failed"}
71 continue
72 dev = next(net.parameters()).device
73 x = ds["xte"][:1].to(dev).clone().requires_grad_(True)
74 net.zero_grad(); net(x).sum().backward()
75 observed = x.grad[0, :, 0].abs().detach().cpu().numpy(); observed /= observed.sum() + 1e-12
76 with torch.no_grad():
77 p = net.propagation(x.detach())[0].detach().cpu().numpy()
78 root = int(np.argmax(ds["xte"][0, :, 4].numpy()))
79 predicted = np.linalg.matrix_power(p, 4)[root]
80 predicted /= predicted.sum() + 1e-12
81 result[name] = {"test_mse": metric,
82 "predicted_l2": float(np.linalg.norm(predicted)),
83 "observed_gradient_l2": float(np.linalg.norm(observed)),
84 "predicted_observed_corr": float(np.corrcoef(observed, predicted)[0, 1])}
85 b, i = result["baseline"], result["idea"]
86 result["prediction"] = "distance weighting preserves more long-range influence"
87 result["confirmed"] = bool(i["observed_gradient_l2"] > b["observed_gradient_l2"] + .01 and
88 i["predicted_observed_corr"] > .5)
89 return result
90
91
92def main():
93 grid = [{"lr": lr, "alpha": .5} for lr in (.001, .003, .01)]
94 base = sweep_baseline(lambda cfg: make_train_fn(cfg, False), grid)
95 trials = [{"cfg": cfg, "result": evaluate(make_train_fn(cfg, True), DEFAULT_SEEDS)} for cfg in grid]
96 best = min(trials, key=lambda z: z["result"]["mean"])
97 rep = make_report("geometric_graph_diffusion", "GeometricMessageNet", base, best["result"],
98 {"track_structure": "geometric graph node-field diffusion regression",
99 "trained_model_measurements": influence_signature(),
100 "idea_sweep": trials,
101 "confirmed": False})
102 rep["custom_track"] = {"name": META["name"], "file": "graph_track.py", "domain": META["domain"]}
103 rep["protocol_notes"] = {"paired_seeds": list(DEFAULT_SEEDS),
104 "baseline_and_idea_same_architecture": True,
105 "only_intervention": "edge weight d^2 versus unit edge weight",
106 "device": DEVICE}
107 os.makedirs("artifacts", exist_ok=True)
108 with open("artifacts/bench_report.json", "w") as f: json.dump(rep, f, indent=2)
109 print(json.dumps(rep, indent=2))
110
111
112if __name__ == "__main__": main()