PoPTard5 / model-3 /scripts /ce2_ensemble.py
Jyo-K's picture
Upload folder using huggingface_hub
e79d917 verified
Raw History Blame Contribute Delete
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)")