Download model-3/scripts/cascade_check.py from Jyo-K/PoPTard5: direct link, hf CLI and curl.
- Browser
- Download file 8.23 kB
-
https://huggingface.co/Jyo-K/PoPTard5/resolve/main/model-3/scripts/cascade_check.py
- Command line
-
hf download hf://Jyo-K/PoPTard5/model-3/scripts/cascade_check.py
-
curl -L -o cascade_check.py https://huggingface.co/Jyo-K/PoPTard5/resolve/main/model-3/scripts/cascade_check.py
8.23 kB
| """Does reranking only the top-M candidates per entity cost any F0.5? | |
| The test stage reranks all ~45 candidates/entity (80M pairs, ~12 h). But the answer | |
| averages 3.46 matches and expected-F0.5 selection stops early on a precision-weighted | |
| metric, so deep-rank candidates may never be selected. If so, reranking only the | |
| top-M by a RERANKER-FREE ranker is free speed. | |
| Method (paired - same entities, same models, only the reranking scope differs): | |
| full : p = iso(lgbm_ce(features + ce_prob)) for every candidate | |
| cascade-M : rank by out-of-fold lgbm_noce; keep top-M/entity; | |
| those keep p; the rest are dropped (never rerankable, so never chosen) | |
| then identical calibration / conflict resolution / expected-F0.5 selection. | |
| Paired on the same entities, so the comparison is far more sensitive than two | |
| independent runs. Ship only at delta == 0. | |
| """ | |
| import os, sys, time, json | |
| import numpy as np | |
| import pandas as pd | |
| import joblib | |
| import lightgbm as lgb | |
| from sklearn.isotonic import IsotonicRegression | |
| sys.path.insert(0, "src") | |
| import config as cfgmod | |
| from config import CFG, EMB, PATHS, emb_dir | |
| import blocking as B | |
| import data as D | |
| import decide as DC | |
| import text_utils as T | |
| import emb_source | |
| from encoder import load_matrix, read_ids, Encoder | |
| from features import compute_features | |
| from evaluate import macro_f05 | |
| N_VAL = int(sys.argv[1]) if len(sys.argv) > 1 else 10000 | |
| GPU = sys.argv[2] if len(sys.argv) > 2 else "cuda:0" | |
| if not str(GPU).startswith("cuda") and GPU != "cpu": | |
| GPU = f"cuda:{int(GPU)}" | |
| MS = [4, 6, 8, 10, 12, 16, 20, 25] | |
| ART = "work/results_A" | |
| t0 = time.time() | |
| log = lambda m: print(f"[{time.time()-t0:6.0f}s] {m}", flush=True) | |
| bundle = joblib.load(os.path.join(ART, "model_bundle.joblib")) | |
| method, param, miss_mass = bundle["method"], bundle["param"], bundle["miss_mass"] | |
| feat_cols, lgbm_ce = bundle["feat_cols"], bundle["lgbm"] | |
| amap = json.load(open(os.path.join(ART, "abbrev_map.json"))) | |
| ce_dir = bundle["ce_dir"] | |
| log(f"bundle: {method}({param}) miss_mass={miss_mass:.4f} feats={len(feat_cols)}") | |
| log("loading train data") | |
| s1, s23, gt = D.load_data(PATHS["train"], with_gt=True) | |
| 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] | |
| def vals(df): | |
| v = {c: df[c].values.astype(object) for c in | |
| ["entity_id", "name_core_abbr", "name_norm", "address_abbrev_norm", | |
| "legal_suffix", "country_norm"]} | |
| nums = df["address_numbers"].values.astype(object) | |
| v["nums"] = np.where(nums == "", df["business_address"].values.astype(object), nums) | |
| return v | |
| s1v, s23v = vals(s1), vals(s23) | |
| pool_pos = {e: i for i, e in enumerate(s23["entity_id"].values)} | |
| s1 = D.augment_s1(s1, gt) # gt is a DataFrame; this is how the pipeline does it | |
| lists = s1["matched_entity_ids"].map(D.split_ids) | |
| truth_rows = {} | |
| for r, ms in enumerate(lists): | |
| if ms: | |
| truth_rows[r] = set(pool_pos[m] for m in ms if m in pool_pos) | |
| _npairs = sum(len(v) for v in truth_rows.values()) | |
| log(f"ground truth: {len(truth_rows):,} entities with matches, {_npairs:,} true pairs") | |
| assert _npairs > 5_000_000, f"ground truth looks wrong ({_npairs} pairs) - expected ~7.6M" | |
| rng = np.random.RandomState(CFG["seed"]) | |
| ce_rows, st_rows, va_rows = D.make_split(s1, CFG, rng) | |
| va_rows = np.sort(rng.choice(va_rows, min(N_VAL, len(va_rows)), replace=False)) | |
| log(f"using {len(va_rows):,} val entities (same split/seed as the real run)") | |
| # ------------------------------------------------------------------ candidates | |
| c_emb, cmeta = load_matrix(emb_dir("train_pool")) | |
| log(f"pool emb {c_emb.shape}") | |
| enc = Encoder(CFG["emb_model"], GPU, EMB["max_len"], EMB["batch"]) | |
| q_emb = enc.encode([B.record_text(s1["business_name"].values[i], | |
| s1["business_address"].values[i], | |
| s1["country"].values[i]) for i in va_rows], | |
| CFG["emb_query_prefix"]) | |
| del enc | |
| emb_s, emb_i = B.dense_topk(np.asarray(q_emb), c_emb, CFG, GPU) | |
| emb2_s = emb2_i = None | |
| if CFG.get("emb2_enabled"): | |
| c2, q2 = emb_source.load_emb2("train", len(s1), len(s23)) | |
| cfg2 = dict(CFG); cfg2["emb_k"] = CFG["emb2_k"] | |
| emb2_s, emb2_i = B.dense_topk(np.asarray(q2[va_rows]), c2, cfg2, GPU) | |
| del c2, q2 | |
| log("dense channels done") | |
| lidx = B.LexicalIndex([B.lexical_tokens(n, a) for n, a in | |
| zip(s23v["name_core_abbr"], s23v["address_abbrev_norm"])], CFG) | |
| lex = B.lexical_topk(lidx, [B.lexical_tokens(s1v["name_core_abbr"][i], | |
| s1v["address_abbrev_norm"][i]) | |
| for i in va_rows], CFG["lex_n_jobs"]) | |
| del lidx | |
| cand = B.build_candidates(va_rows, emb_s, emb_i, lex, s1v["country_norm"], | |
| s23v["country_norm"], CFG["country_filter"], | |
| emb2_s=emb2_s, emb2_i=emb2_i) | |
| qpos = {r: i for i, r in enumerate(va_rows.tolist())} | |
| cand = B.fill_missing_emb(cand, lambda rows: q_emb[[qpos[r] for r in rows]], c_emb) | |
| cand["y"] = np.fromiter((c in truth_rows.get(q, ()) for q, c in | |
| zip(cand["q"].values, cand["c"].values)), np.int8, len(cand)) | |
| log(f"{len(cand):,} candidates ({len(cand)/len(va_rows):.1f}/entity)") | |
| feats = compute_features(cand, s1v, s23v, CFG["feat_n_jobs"]) | |
| cand = pd.concat([cand, feats], axis=1); del feats | |
| log("features done") | |
| # ------------------------------------------------------------------ reranker | |
| from cross_encoder import pair_text, predict_cross_encoder | |
| ta = [pair_text(s1["business_name"].values[r], s1["business_address"].values[r], | |
| s1["country"].values[r]) for r in cand["q"].values] | |
| tb = [pair_text(s23["business_name"].values[c], s23["business_address"].values[c], | |
| s23["country"].values[c]) for c in cand["c"].values] | |
| cand["ce_prob"] = predict_cross_encoder(ta, tb, ce_dir, CFG) | |
| del ta, tb | |
| log("reranker scoring done") | |
| # ------------------------------------------------------ CE-free pre-ranker (OOF) | |
| noce_cols = [c for c in feat_cols if c != "ce_prob"] | |
| oof = np.zeros(len(cand)) | |
| groups = cand["q"].values | |
| uq = np.unique(groups) | |
| fold_of = {q: i % 5 for i, q in enumerate(rng.permutation(uq))} | |
| fold = np.array([fold_of[q] for q in groups]) | |
| for f in range(5): | |
| tr, te = fold != f, fold == f | |
| m = lgb.train({**CFG["lgbm_params"], "verbose": -1}, | |
| lgb.Dataset(cand.loc[tr, noce_cols], cand.loc[tr, "y"]), | |
| num_boost_round=300) | |
| oof[te] = m.predict(cand.loc[te, noce_cols]) | |
| cand["noce"] = oof | |
| log("out-of-fold CE-free pre-ranker built") | |
| # ------------------------------------------------------------------ compare | |
| cand["p"] = lgbm_ce.predict(cand[feat_cols]) | |
| cand["p"] = bundle["calibrator"].predict(cand["p"].values) # the real calibrator | |
| truths = {q: truth_rows.get(q, set()) for q in va_rows} | |
| ids = list(va_rows) | |
| full_preds = DC.predict_sets(cand, "q", "c", "p", method, param, miss_mass, True) | |
| m_full, per_full = macro_f05(full_preds, truths, ids) | |
| log(f"FULL F0.5 {m_full['f05']:.6f} P {m_full['precision']:.4f} R {m_full['recall']:.4f}") | |
| if not (0.93 <= m_full["f05"] <= 0.995): | |
| sys.exit(f"SANITY FAIL: full-pipeline F0.5 {m_full['f05']:.4f} is nowhere near the " | |
| f"0.9812 the real run reported - the harness is wrong, not the cascade") | |
| cand["rk"] = cand.groupby("q")["noce"].rank(ascending=False, method="first") | |
| print() | |
| print(f"{'M':>4} {'F0.5':>10} {'delta':>11} {'P':>8} {'R':>8} {'rerank%':>9} {'selected outside top-M':>24}") | |
| print(f"{'full':>4} {m_full['f05']:>10.6f} {0.0:>11.6f} {m_full['precision']:>8.4f} " | |
| f"{m_full['recall']:>8.4f} {100.0:>8.1f}% {0:>24}") | |
| for M in MS: | |
| sub = cand[cand["rk"] <= M] | |
| preds = DC.predict_sets(sub, "q", "c", "p", method, param, miss_mass, True) | |
| mm, per = macro_f05(preds, truths, ids) | |
| lost = sum(1 for q, cs in full_preds.items() for c in cs | |
| if c not in set(preds.get(q, []))) | |
| print(f"{M:>4} {mm['f05']:>10.6f} {mm['f05']-m_full['f05']:>+11.6f} " | |
| f"{mm['precision']:>8.4f} {mm['recall']:>8.4f} " | |
| f"{100*len(sub)/len(cand):>8.1f}% {lost:>24}") | |
| print() | |
| print(f"paired on {len(ids):,} identical entities; ship only at delta == 0") | |