Download model-3/scripts/ce2_ensemble.py from Jyo-K/PoPTard5: direct link, hf CLI and curl.
- Browser
- Download file 5.73 kB
-
https://huggingface.co/Jyo-K/PoPTard5/resolve/main/model-3/scripts/ce2_ensemble.py
- Command line
-
hf download hf://Jyo-K/PoPTard5/model-3/scripts/ce2_ensemble.py
-
curl -L -o ce2_ensemble.py https://huggingface.co/Jyo-K/PoPTard5/resolve/main/model-3/scripts/ce2_ensemble.py
5.73 kB
| """Does a SECOND cross-encoder close part of the 0.0118 separation gap? | |
| Measured facts this targets: | |
| - oracle on our candidates 0.9942 vs shipped 0.98214 -> 0.0121 scorer loss | |
| - of that, only 0.00031 is within-entity RANKING (oracle rerank at fixed k) | |
| - so ~0.0118 is SEPARATION: true pairs sitting below 0.70 with negatives interleaved | |
| Better separation is what a second, architecturally different cross-encoder can supply | |
| (bge-reranker-v2-m3 is XLM-R based; Chinmay's is mDeBERTa-v3). Same pair_text | |
| serialization, verified identical, so his checkpoint applies directly to our pairs. | |
| METHOD, leak-free: split the 20,000 val entities in half by a fixed hash. Fit his score's | |
| isotonic calibration, the blend weight and the threshold on half A; report on half B only. | |
| Half A numbers are shown too, marked in-sample. | |
| """ | |
| import json, sys, os, time | |
| import numpy as np, pandas as pd, joblib | |
| sys.path.insert(0, "src") | |
| import config as C, decide as DC | |
| from evaluate import macro_f05 | |
| from cross_encoder import pair_text, predict_cross_encoder | |
| from sklearn.isotonic import IsotonicRegression | |
| CE2 = "/scratch/user4/dai/bge-out/cache/ce_model_c8a595de7fa1" | |
| CACHE = "work/val_scored_20000.parquet" | |
| OUT = "work/val_ce2_scores.npy" | |
| b = joblib.load("work/results_A/model_bundle.joblib") | |
| MM, THR = b["miss_mass"], b["param"] | |
| cand = pd.read_parquet(CACHE) | |
| truths = {int(k): set(v) for k, v in json.load(open(CACHE + ".truth.json")).items()} | |
| ids = sorted(truths) | |
| print(f"val entities {len(ids):,} scored pairs {len(cand):,}") | |
| p = C.PATHS["train"] | |
| cols = ["entity_id", "business_name", "business_address", "country_norm"] | |
| s1 = pd.read_parquet(p["source1"], columns=cols) | |
| pool = pd.concat([pd.read_parquet(p[k], columns=cols) for k in ("source2", "source3")], | |
| ignore_index=True) | |
| print(f"s1 {len(s1):,} pool {len(pool):,}") | |
| qi, ci = cand["q"].values, cand["c"].values | |
| s1n, s1a, s1c = (s1["business_name"].values, s1["business_address"].values, | |
| s1["country_norm"].values) | |
| pn, pa, pc = (pool["business_name"].values, pool["business_address"].values, | |
| pool["country_norm"].values) | |
| if os.path.exists(OUT): | |
| p2 = np.load(OUT) | |
| assert len(p2) == len(cand), "cached ce2 scores do not match the candidate table" | |
| print(f"reusing cached ce2 scores ({len(p2):,})") | |
| else: | |
| ta = [pair_text(s1n[r], s1a[r], s1c[r]) for r in qi] | |
| tb = [pair_text(pn[c], pa[c], pc[c]) for c in ci] | |
| print("sample A:", ta[0][:110]) | |
| print("sample B:", tb[0][:110]) | |
| cfg = dict(C.CFG) | |
| cfg["ce_max_len"] = 160 | |
| cfg["ce_eval_batch"] = 512 | |
| cfg["ce_num_workers"] = 0 | |
| t0 = time.time() | |
| p2 = predict_cross_encoder(ta, tb, CE2, cfg, cache="work/ce2_pred") | |
| print(f"scored {len(p2):,} pairs with the mDeBERTa CE in {time.time()-t0:.0f}s") | |
| np.save(OUT, p2) | |
| cand = cand.copy() | |
| cand["p2"] = p2.astype(np.float32) | |
| cand["y"] = [1 if c in truths.get(q, ()) else 0 for q, c in zip(cand["q"], cand["c"])] | |
| # leak-free half split on entities | |
| h = np.array([hash(("split", int(q))) % 2 for q in ids]) | |
| A = set(np.array(ids)[h == 0].tolist()) | |
| B = set(np.array(ids)[h == 1].tolist()) | |
| print(f"\nfit half A: {len(A):,} entities report half B: {len(B):,} entities") | |
| inA = cand["q"].isin(A).values | |
| ca, cb = cand[inA], cand[~inA] | |
| idsA, idsB = sorted(A), sorted(B) | |
| def ev(df, idl, col): | |
| pr = DC.predict_sets(df, "q", "c", col, "threshold", THR, MM, True) | |
| m, _ = macro_f05(pr, truths, idl) | |
| return m | |
| print("\n--- baseline (our bge reranker stack, shipped threshold 0.70) ---") | |
| mA, mB = ev(ca, idsA, "p"), ev(cb, idsB, "p") | |
| print(f" half A {mA['f05']:.5f} half B {mB['f05']:.5f}") | |
| # his CE alone, isotonic-calibrated on A | |
| iso = IsotonicRegression(out_of_bounds="clip", y_min=0.0, y_max=1.0) | |
| iso.fit(ca["p2"].values, ca["y"].values) | |
| cand["p2c"] = iso.predict(cand["p2"].values).astype(np.float32) | |
| ca, cb = cand[inA], cand[~inA] | |
| print("\n--- mDeBERTa CE alone (isotonic fit on A) ---") | |
| print(f" half A {ev(ca,idsA,'p2c')['f05']:.5f} half B {ev(cb,idsB,'p2c')['f05']:.5f}") | |
| print("\n--- BLEND: (1-w)*ours + w*theirs, both calibrated ---") | |
| print(f"{'w':>6}{'thr':>7}{'half A (in-sample)':>20}{'half B (reported)':>20}") | |
| best = (-1, None, None) | |
| for w in [0.0, 0.1, 0.15, 0.2, 0.25, 0.3, 0.4, 0.5]: | |
| cand["pb"] = (1 - w) * cand["p"] + w * cand["p2c"] | |
| ca, cb = cand[inA], cand[~inA] | |
| # tune the threshold on A only | |
| bt, bv = THR, -1 | |
| for t in np.round(np.arange(0.50, 0.93, 0.025), 3): | |
| pr = DC.predict_sets(ca, "q", "c", "pb", "threshold", t, MM, True) | |
| v = macro_f05(pr, truths, idsA)[0]["f05"] | |
| if v > bv: | |
| bt, bv = t, v | |
| prB = DC.predict_sets(cb, "q", "c", "pb", "threshold", bt, MM, True) | |
| vB = macro_f05(prB, truths, idsB)[0]["f05"] | |
| print(f"{w:>6.2f}{bt:>7.3f}{bv:>20.5f}{vB:>20.5f}") | |
| if vB > best[0]: | |
| best = (vB, w, bt) | |
| print(f"\n BEST on held-out half B: w={best[1]} thr={best[2]} -> {best[0]:.5f}") | |
| print(f" baseline on half B : {mB['f05']:.5f}") | |
| print(f" DELTA (held-out, honest) : {best[0]-mB['f05']:+.5f}") | |
| # does the second CE rescue the pairs we reject? | |
| rej = cand[(cand.y == 1) & (cand.p < THR)] | |
| print(f"\n--- the {len(rej):,} true pairs our stack puts below 0.70 ---") | |
| print(f" their mDeBERTa calibrated p: median {rej['p2c'].median():.4f}, " | |
| f"frac >= 0.70: {(rej['p2c']>=0.70).mean():.2%}") | |
| neg = cand[(cand.y == 0) & (cand.p < THR)] | |
| print(f" for comparison, {len(neg):,} TRUE NEGATIVES below 0.70: " | |
| f"frac >= 0.70: {(neg['p2c']>=0.70).mean():.4%}") | |
| tp = int((rej['p2c'] >= 0.70).sum()); fp = int((neg['p2c'] >= 0.70).sum()) | |
| print(f" => rescue trade: {tp:,} TP vs {fp:,} FP = {tp/max(fp,1):.2f}:1 " | |
| f"(needs > 4:1 to gain)") | |