PoPTard5 / model-3 /src /run_blocking.py
Jyo-K's picture
Upload folder using huggingface_hub
e79d917 verified
Raw History Blame Contribute Delete
9.52 kB
"""Blocking only: generate LABELLED candidate pairs for an 80/20 train/test split.
python src/run_blocking.py --gpu 1 # all 2.21M train S1
python src/run_blocking.py --gpu 1 --n-queries 500000 # subset, faster
Does NOT train anything. Produces the candidate-pair dataset the matcher will
later consume, plus a blocking-quality report.
Split
80/20 over Source-1 ENTITIES (not pairs), seeded, so no entity appears in both
sides and there is no leakage through the shared S2/S3 pool.
Output (work/blocking/<model>/)
pairs_train/part_XXXXX.parquet q_id, c_id, emb_score, emb_rank,
pairs_test/part_XXXXX.parquet lex_score, lex_rank, n_keys, y
report.json recall, candidates/query, reduction ratio,
per-slice recall, F0.5 oracle ceiling
Resumable: finished chunks are skipped on re-run.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
import numpy as np
import pandas as pd
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import blocking as B # noqa: E402
import data as D # noqa: E402
import text_utils as T # noqa: E402
from config import CFG, EMB, PATHS, emb_dir # noqa: E402
from encoder import load_matrix, read_ids # noqa: E402
T0 = time.time()
def log(m):
print(f"[{time.time() - T0:7.0f}s] {m}", flush=True)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--gpu", default=os.environ.get("M3_GPU"))
ap.add_argument("--n-queries", type=int, default=0, help="0 = all train S1")
ap.add_argument("--chunk", type=int, default=100_000, help="queries per resumable chunk")
ap.add_argument("--test-frac", type=float, default=0.2)
ap.add_argument("--seed", type=int, default=42)
a = ap.parse_args()
if a.gpu is None:
sys.exit("pass --gpu 0 / --gpu 1 / --gpu cpu")
dev = "cpu" if str(a.gpu) == "cpu" else f"cuda:{int(a.gpu)}"
out = os.path.join(PATHS["work_dir"], "blocking", EMB["model"])
for sub in ("pairs_train", "pairs_test"):
os.makedirs(os.path.join(out, sub), exist_ok=True)
# ---------------------------------------------------------------- data
log("loading train sources + ground truth")
s1, s23, gt = D.load_data(PATHS["train"])
s1 = s1.merge(gt.rename(columns={"source1_entity_id": "entity_id"}), on="entity_id",
how="left", validate="one_to_one")
s1["matched_entity_ids"] = s1["matched_entity_ids"].fillna("").astype(str)
del gt
log(f"S1 {len(s1):,} S2+S3 {len(s23):,}")
# abbreviation map mined from TRAIN-side positives only (no leakage into test)
rng = np.random.default_rng(a.seed)
perm = rng.permutation(len(s1))
n_test = int(len(perm) * a.test_frac)
test_rows = np.sort(perm[:n_test])
train_rows = np.sort(perm[n_test:])
is_test = np.zeros(len(s1), bool)
is_test[test_rows] = True
log(f"split: train {len(train_rows):,} entities | test {len(test_rows):,} entities "
f"({a.test_frac:.0%})")
s23_index = pd.Index(s23["entity_id"])
lists = s1["matched_entity_ids"].map(D.split_ids)
owner = np.repeat(np.arange(len(s1)), lists.map(len).values)
pos = s23_index.get_indexer([x for l in lists for x in l])
ok = pos >= 0
truth = {}
for q, c in zip(owner[ok].tolist(), pos[ok].tolist()):
truth.setdefault(q, set()).add(c)
n_true_total = int(ok.sum())
log(f"true pairs: {n_true_total:,} ({int((~ok).sum()):,} unresolved ids)")
log("mining abbreviations from TRAIN-side positives only")
pa, pb = [], []
for q in train_rows[:200_000]:
for c in truth.get(q, ()):
pa.append(s1["name_core"].values[q]); pb.append(s23["name_core"].values[c])
if len(pa) > CFG["abbrev_max_pos_pairs"]:
sel = rng.choice(len(pa), CFG["abbrev_max_pos_pairs"], replace=False)
pa, pb = [pa[i] for i in sel], [pb[i] for i in sel]
nb = [pb[i] for i in rng.permutation(len(pb))]
amap = T.mine_abbreviations(zip(pa, pb), zip(pa, nb),
CFG["abbrev_min_support"], CFG["abbrev_min_precision"])
json.dump(amap, open(os.path.join(out, "abbrev_map.json"), "w"), indent=1, ensure_ascii=False)
log(f"{len(amap)} abbreviation rules")
del pa, pb, nb
s1["name_core_abbr"] = [T.apply_abbrev(x, amap) for x in s1["name_core"].values]
s23["name_core_abbr"] = [T.apply_abbrev(x, amap) for x in s23["name_core"].values]
s1_nca = s1["name_core_abbr"].values.astype(object)
s1_addr = s1["address_abbrev_norm"].values.astype(object)
s23_nca = s23["name_core_abbr"].values.astype(object)
s23_addr = s23["address_abbrev_norm"].values.astype(object)
q_country = s1["country_norm"].values.astype(object)
c_country = s23["country_norm"].values.astype(object)
s1_ids = s1["entity_id"].values.astype(object)
s23_ids = s23["entity_id"].values.astype(object)
# ---------------------------------------------------------------- embeddings
import emb_source
c_emb, q_emb, cmeta = emb_source.load("train", list(s1["entity_id"]),
list(s23["entity_id"]))
log(f"embeddings: pool {c_emb.shape} queries {q_emb.shape} "
f"({cmeta['model']}, source={cmeta.get('source', 'internal')})")
# ---------------------------------------------------------------- lexical index
log("building lexical index over the 10.3M pool (one-off, slowest single step)")
t = time.time()
lidx = B.LexicalIndex([B.lexical_tokens(n, ad) for n, ad in zip(s23_nca, s23_addr)], CFG)
log(f"lexical index built in {time.time() - t:.0f}s")
# ---------------------------------------------------------------- query order
all_q = np.arange(len(s1)) if a.n_queries <= 0 else np.sort(
rng.choice(len(s1), min(a.n_queries, len(s1)), replace=False))
log(f"blocking {len(all_q):,} queries in chunks of {a.chunk:,}")
stats = {"train": [], "test": []}
for ci, lo in enumerate(range(0, len(all_q), a.chunk)):
qs = all_q[lo:lo + a.chunk]
done = [os.path.join(out, f"pairs_{s}", f"part_{ci:05d}.parquet") for s in ("train", "test")]
if all(os.path.exists(p) for p in done):
continue
t = time.time()
emb_s, emb_i = B.dense_topk(np.asarray(q_emb[qs]), c_emb, CFG, dev)
lex = B.lexical_topk(lidx, [B.lexical_tokens(s1_nca[i], s1_addr[i]) for i in qs],
CFG["lex_n_jobs"])
cand = B.build_candidates(qs, emb_s, emb_i, lex, q_country, c_country,
country_filter=True)
cand = B.fill_missing_emb(cand, lambda rows, _m={r: j for j, r in enumerate(qs.tolist())}:
q_emb[[qs[_m[r]] for r in rows]], c_emb)
cand["y"] = np.fromiter((c in truth.get(q, ()) for q, c in
zip(cand["q"].values, cand["c"].values)), np.int8, len(cand))
cand["q_id"] = s1_ids[cand["q"].values]
cand["c_id"] = s23_ids[cand["c"].values]
side = is_test[cand["q"].values]
for name, mask in (("train", ~side), ("test", side)):
sub = cand[mask]
cols = ["q_id", "c_id", "emb_score", "emb_rank", "lex_score", "lex_rank", "n_keys", "y"]
p = os.path.join(out, f"pairs_{name}", f"part_{ci:05d}.parquet")
sub[cols].to_parquet(p + ".tmp", index=False)
os.replace(p + ".tmp", p)
rows = qs[is_test[qs]] if name == "test" else qs[~is_test[qs]]
stats[name].append({"n_q": int(len(rows)), "n_cand": int(len(sub)),
"n_hit": int(sub["y"].sum()),
"n_true": int(sum(len(truth.get(q, ())) for q in rows))})
log(f"chunk {ci + 1}/{(len(all_q) + a.chunk - 1) // a.chunk}: {len(qs):,} queries, "
f"{len(cand):,} pairs, {time.time() - t:.0f}s")
json.dump(stats, open(os.path.join(out, "stats_partial.json"), "w"))
# ---------------------------------------------------------------- report
rep = {"model": cmeta["model"], "dim": cmeta["dim"], "emb_k": CFG["emb_k"],
"lex_k": CFG["lex_k"], "country_filter": True, "seed": a.seed,
"test_frac": a.test_frac, "n_queries_blocked": int(len(all_q)),
"abbrev_rules": len(amap), "pool_size": int(len(s23))}
for name in ("train", "test"):
S = stats[name]
nq = sum(x["n_q"] for x in S); nc = sum(x["n_cand"] for x in S)
nh = sum(x["n_hit"] for x in S); nt = sum(x["n_true"] for x in S)
rep[name] = {
"n_entities": nq, "n_candidates": nc, "n_true_pairs": nt, "n_true_retrieved": nh,
"pair_recall": nh / max(nt, 1),
"candidates_per_entity": nc / max(nq, 1),
"reduction_ratio": 1 - nc / max(nq * len(s23), 1),
}
rep["seconds"] = round(time.time() - T0)
json.dump(rep, open(os.path.join(out, "report.json"), "w"), indent=2)
log("---- blocking report ----")
for name in ("train", "test"):
r = rep[name]
log(f"{name}: {r['n_entities']:,} entities | {r['n_candidates']:,} candidates "
f"({r['candidates_per_entity']:.1f}/entity) | pair recall {r['pair_recall']:.4f} "
f"| reduction {r['reduction_ratio']:.6f}")
log(f"-> {out}")
if __name__ == "__main__":
main()