import argparse
import json
import time
import numpy as np
import scipy.sparse as sp
from scipy.sparse.linalg import LinearOperator, svds
DATA = "eval/data/bostrom_links.jsonl"
GAMMA = 0.5
D = 64
K_SM = 0.1
def load_links():
links = []
with open(DATA) as f:
for line in f:
d = json.loads(line)
links.append((d["h"], d["f"], d["t"]))
links.sort(key=lambda x: x[0])
return links
def pagerank(A, alpha=0.85, iters=300, tol=1e-12):
n = A.shape[0]
out = np.asarray(A.sum(axis=1)).ravel()
dang = out == 0
osafe = np.where(out == 0, 1.0, out)
P = (sp.diags(1.0 / osafe) @ A).T
phi = np.full(n, 1.0 / n)
for _ in range(iters):
prev = phi.copy()
dmass = phi[dang].sum()
phi = alpha * (P @ phi + dmass * phi) + (1 - alpha) / n
phi /= phi.sum()
if np.abs(phi - prev).sum() < tol:
break
return phi
def mixed_svd(A, phi, k):
n = A.shape[0]
ds = np.sqrt(phi)
At = A.T.tocsr()
def mv(x):
t = ds * x
ax = A @ t
a2x = A @ ax
return ds * (ax + GAMMA * a2x)
def mvt(x):
t = ds * x
atx = At @ t
at2x = At @ atx
return ds * (atx + GAMMA * at2x)
op = LinearOperator((n, n), matvec=mv, rmatvec=mvt, dtype=np.float64)
u, s, vt = svds(op, k=k, which="LM", maxiter=500, tol=1e-8)
order = np.argsort(-s)
return u[:, order], s[order], vt.T[:, order]
def rms(x, eps=1e-5):
return x / np.sqrt((x * x).mean() + eps)
def mrr_queries(E, queries):
r = 0.0
for seq, gold in queries:
f = seq[-1]
s = E @ rms(E[f])
r += 1.0 / (1 + int(np.sum(s > s[gold])))
return r / len(queries)
def bigram_mrr(links_train, queries, n):
from collections import defaultdict
cnt = defaultdict(int)
outc = defaultdict(int)
for f, t in links_train:
cnt[(f, t)] += 1
outc[f] += 1
r = 0.0
for seq, gold in queries:
f = seq[-1]
s = np.array([cnt.get((f, j), 0) + K_SM for j in range(n)])
s /= outc.get(f, 0) + K_SM * n
r += 1.0 / (1 + int(np.sum(s > s[gold])))
return r / len(queries)
def build_queries(test_pairs, preds, max_q=800, seed=0x9E3779B97F4A7C15, per_src=3):
rng = np.random.default_rng(seed)
pool, seen, src_n = [], set(), {}
for f, t in test_pairs:
if f not in preds or not preds[f]:
continue
if src_n.get(f, 0) >= per_src:
continue
b = preds[f][rng.integers(0, len(preds[f]))]
if b in preds and preds[b]:
a = preds[b][rng.integers(0, len(preds[b]))]
seq = (a, b, f)
else:
seq = (b, f)
if seq in seen:
continue
seen.add(seq)
src_n[f] = src_n.get(f, 0) + 1
pool.append((list(seq), t))
if len(pool) > max_q:
take = rng.choice(len(pool), max_q, replace=False)
pool = [pool[i] for i in take]
return pool
def bpr_train(E, edge_pool, edge_set, n, steps, B=256, lr=3e-4, seed=5,
log=(), eval_fn=None, eval_every=400):
rng = np.random.default_rng(seed)
E = E.copy()
m = np.zeros_like(E)
v = np.zeros_like(E)
b1, b2, eps = 0.9, 0.999, 1e-8
t0 = time.time()
history = [(0, eval_fn(E) if eval_fn else 0.0)]
for step in range(1, steps + 1):
idx = rng.integers(0, len(edge_pool), size=B)
grads = {}
loss = 0.0
for i in idx:
f, tp = edge_pool[i]
tn = int(rng.integers(0, n))
if (f, tn) in edge_set:
continue
sf = rms(E[f])
sp_ = float(sf @ E[tp])
sn = float(sf @ E[tn])
d = sp_ - sn
sig = 1.0 / (1.0 + np.exp(min(d, 50))) loss += np.log1p(np.exp(-min(d, 50)))
g = -sig
for j, coef in ((f, 0.0), (tp, 0.0), (tn, 0.0)):
pass
acc = grads.setdefault
acc(f, np.zeros(E.shape[1]))[:] += g * (E[tp] - E[tn])
acc(tp, np.zeros(E.shape[1]))[:] += g * sf
acc(tn, np.zeros(E.shape[1]))[:] += -g * sf
for j, g in grads.items():
m[j] = b1 * m[j] + (1 - b1) * g
v[j] = b2 * v[j] + (1 - b2) * g * g
mh = m[j] / (1 - b1 ** step)
vh = v[j] / (1 - b2 ** step)
E[j] -= lr * mh / (np.sqrt(vh) + eps)
if step % eval_every == 0:
val = eval_fn(E) if eval_fn else 0.0
history.append((step, val))
print(f" step {step:5} MRR {val:.4f} loss {loss/ B:.4f} ({time.time()-t0:.0f}s)", flush=True)
return E, history
def run_rung(links, target_particles, steps):
seen = set()
cut = len(links)
for i, (_, f, t) in enumerate(links):
seen.add(f)
seen.add(t)
if len(seen) >= target_particles:
cut = i + 1
break
prefix = links[:cut]
split = int(len(prefix) * 0.9)
train, test = prefix[:split], prefix[split:]
print(f"\n=== rung target {target_particles:,} -> prefix {len(prefix):,} links "
f"(train {len(train):,}, test {len(test):,}) ===", flush=True)
pid = {}
def intern(c):
if c not in pid:
pid[c] = len(pid)
return pid[c]
tr_pairs = []
preds = {}
for _, f, t in train:
fi, ti = intern(f), intern(t)
tr_pairs.append((fi, ti))
preds.setdefault(ti, []).append(fi)
n = len(pid)
print(f" particles {n:,}", flush=True)
test_pairs = [(intern(f), intern(t)) for _, f, t in test if f in pid and t in pid]
queries = build_queries(test_pairs, preds)
print(f" queries {len(queries)}", flush=True)
rows, cols = [], []
for f, t in tr_pairs:
rows.append(f)
cols.append(t)
A = sp.csr_matrix((np.ones(len(rows)), (rows, cols)), shape=(n, n))
phi = pagerank(A)
t0 = time.time()
U, s, V = mixed_svd(A, phi, D)
print(f" svds k={D} sigma1={s[0]:.4f} sigma_k={s[-1]:.6f} ({time.time()-t0:.0f}s)", flush=True)
E = U * np.sqrt(s)
floor = mrr_queries(E, queries)
bg = bigram_mrr(tr_pairs, queries, n)
print(f" floor MRR {floor:.4f} ยท bigram {bg:.4f}", flush=True)
edge_pool = tr_pairs
edge_set = set(tr_pairs)
eval_fn = lambda X: mrr_queries(X, queries)
rng = np.random.default_rng(3)
E_rand = rng.normal(0, 1, (n, D))
E_rand /= np.linalg.norm(E_rand, axis=1, keepdims=True)
print(" compiled E + BPR:", flush=True)
_, h_comp = bpr_train(E, edge_pool, edge_set, n, steps, eval_fn=eval_fn)
print(" random E + BPR:", flush=True)
_, h_rand = bpr_train(E_rand, edge_pool, edge_set, n, steps, eval_fn=eval_fn)
return {
"n_particles": n, "n_train": len(train), "n_test": len(test),
"n_queries": len(queries), "sigma": s.tolist(),
"floor": floor, "bigram": bg,
"compiled_bpr": h_comp, "random_bpr": h_rand,
}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--rungs", type=int, nargs="+",
default=[100_000, 500_000, 1_000_000, 3_100_000])
ap.add_argument("--steps", type=int, default=2000)
args = ap.parse_args()
links = load_links()
print(f"bostrom: {len(links):,} links loaded", flush=True)
results = {}
for r in args.rungs:
results[r] = run_rung(links, r, args.steps)
with open("eval/scale_ladder_result.json", "w") as f:
json.dump(results, f)
print("\n=== ladder summary ===")
print(f"{'rung':>10} {'particles':>10} {'floor':>8} {'bigram':>8} {'comp+BPR':>20} {'rand+BPR':>20}")
for r, res in results.items():
cf = " ".join(f"{v:.3f}" for _, v in res["compiled_bpr"])
rf = " ".join(f"{v:.3f}" for _, v in res["random_bpr"])
print(f"{r:>10,} {res['n_particles']:>10,} {res['floor']:8.4f} {res['bigram']:8.4f} {cf:18} {rf:18}")
if __name__ == "__main__":
main()