kozo2 commited on
Commit
9e4b2c5
·
verified ·
1 Parent(s): 6f24bf5

Upload heldout_check.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. heldout_check.py +101 -0
heldout_check.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Retrain on 90% of edges and score the held-out 10%: does it generalise?
2
+
3
+ Same procedure as the released pyg/heldout_check.py, with the sample size scaled
4
+ down for this much smaller graph (848 nodes, ~359k possible node pairs)."""
5
+
6
+ import argparse
7
+ import time
8
+
9
+ import torch
10
+ from torch_geometric.utils import to_undirected
11
+
12
+ from config import Paths, add_source_arg
13
+ from node2vec_model import build_model, load_graph
14
+
15
+ _ap = argparse.ArgumentParser()
16
+ add_source_arg(_ap)
17
+ _ap.add_argument("--epochs", type=int, default=200,
18
+ help="match the budget used for the saved embeddings")
19
+ cli = _ap.parse_args()
20
+ paths = Paths(cli.source_table)
21
+
22
+ dev = torch.device("cuda" if torch.cuda.is_available() else "cpu")
23
+ g = torch.Generator(device="cpu").manual_seed(1)
24
+
25
+ data = load_graph(paths.graph)
26
+ N = data.num_nodes
27
+ ei = data.edge_index.to(dev)
28
+ u, v = ei.min(0).values.to(torch.int64), ei.max(0).values.to(torch.int64)
29
+ keys = torch.unique(u * N + v)
30
+
31
+ perm = torch.randperm(keys.numel(), generator=g).to(dev)
32
+ n_test = keys.numel() // 10
33
+ test_keys, train_keys = keys[perm[:n_test]], keys[perm[n_test:]]
34
+ train_ei = to_undirected(
35
+ torch.stack([train_keys // N, train_keys % N]), num_nodes=N)
36
+ deg_tr = torch.bincount(train_ei[0].to(dev), minlength=N)
37
+ print(f"train edges {train_keys.numel():,} held-out {test_keys.numel():,} "
38
+ f"nodes isolated by the split: {(deg_tr == 0).sum().item()}")
39
+
40
+ args = argparse.Namespace(
41
+ embedding_dim=128, walk_length=20, context_size=10, walks_per_node=10,
42
+ num_negative_samples=1, p=1.0, q=1.0)
43
+ model = build_model(type("D", (), {"edge_index": train_ei.cpu(), "num_nodes": N}),
44
+ args, dev)
45
+ loader = model.loader(batch_size=128, shuffle=True, num_workers=4)
46
+ opt = torch.optim.SparseAdam(list(model.parameters()), lr=0.01)
47
+
48
+ for epoch in range(1, cli.epochs + 1):
49
+ model.train()
50
+ t0, tot, n = time.perf_counter(), 0.0, 0
51
+ for pos_rw, neg_rw in loader:
52
+ opt.zero_grad()
53
+ loss = model.loss(pos_rw.to(dev), neg_rw.to(dev))
54
+ loss.backward()
55
+ opt.step()
56
+ tot, n = tot + loss.item(), n + 1
57
+ if epoch % max(cli.epochs // 5, 1) == 0 or epoch == 1:
58
+ print(f" epoch {epoch:>3} loss {tot / n:.4f} {time.perf_counter() - t0:.1f}s")
59
+
60
+ model.eval()
61
+ with torch.no_grad():
62
+ z = model()
63
+ zc = torch.nn.functional.normalize(z, dim=1)
64
+
65
+
66
+ def sample_non_edges(n):
67
+ out, got = [], 0
68
+ while got < n:
69
+ c = torch.randint(N, (2, n), generator=g).to(dev)
70
+ a, b = c.min(0).values, c.max(0).values
71
+ ok = (a != b) & ~torch.isin(a.to(torch.int64) * N + b.to(torch.int64), keys)
72
+ out.append(torch.stack([a[ok], b[ok]]))
73
+ got += int(ok.sum())
74
+ return torch.cat(out, dim=1)[:, :n]
75
+
76
+
77
+ def auc(pos, neg):
78
+ s = torch.cat([pos, neg]).double()
79
+ order = torch.argsort(s)
80
+ ranks = torch.empty_like(s)
81
+ ranks[order] = torch.arange(1, s.numel() + 1, dtype=torch.float64, device=s.device)
82
+ np_, nn_ = pos.numel(), neg.numel()
83
+ return ((ranks[:np_].sum() - np_ * (np_ + 1) / 2) / (np_ * nn_)).item()
84
+
85
+
86
+ M = 20_000
87
+ te = test_keys[torch.randperm(n_test, generator=g)[:M].to(dev)]
88
+ pe = torch.stack([te // N, te % N])
89
+ tr = train_keys[torch.randperm(train_keys.numel(), generator=g)[:M].to(dev)]
90
+ tre = torch.stack([tr // N, tr % N])
91
+ ne = sample_non_edges(M)
92
+
93
+ print("\nlink prediction AUC (cosine):")
94
+ for name, e in (("held-out edges (never seen)", pe), ("train edges (in-sample)", tre)):
95
+ print(f" {name:<28} {auc((zc[e[0]] * zc[e[1]]).sum(1), (zc[ne[0]] * zc[ne[1]]).sum(1)):.4f}")
96
+
97
+ # degree-only baseline: would preferential attachment alone explain it?
98
+ d = deg_tr.double()
99
+ print("\ndegree baseline (preferential attachment, d_u * d_v):")
100
+ print(f" held-out edges "
101
+ f"{auc(d[pe[0]] * d[pe[1]], d[ne[0]] * d[ne[1]]):.4f}")