Upload heldout_check.py with huggingface_hub
Browse files- 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}")
|