Download model-4/Colab/src/stacker.py from Jyo-K/PoPTard5: direct link, hf CLI and curl.
- Browser
- Download file 17.7 kB
-
https://huggingface.co/Jyo-K/PoPTard5/resolve/main/model-4/Colab/src/stacker.py
- Command line
-
hf download hf://Jyo-K/PoPTard5/model-4/Colab/src/stacker.py
-
curl -L -o stacker.py https://huggingface.co/Jyo-K/PoPTard5/resolve/main/model-4/Colab/src/stacker.py
17.7 kB
| """Stage 1 (cheap LightGBM over all fused candidates), stage-2 candidate set, stage 2 (LightGBM + cross-encoder, | |
| calibration, decision selection), test inference + submission files, and the leave-one-country-out proxy.""" | |
| import pickle | |
| import subprocess | |
| import sys | |
| from multiprocessing import Pool | |
| import lightgbm as lgb | |
| import numpy as np | |
| import pandas as pd | |
| from common import get_logger, load_json, save_json, save_parquet, shard_path, n_workers | |
| from config import CFG, OUTPUT, PATHS, WORK | |
| from decide import crossfit_isotonic, decide, fit_isotonic, macro_f05 | |
| from features import feature_columns | |
| from prep import load_prep, load_splits | |
| from retrieve_dense import n_shards | |
| log = get_logger("stack") | |
| MODELS = WORK / "models" | |
| PARAMS = dict(objective="binary", learning_rate=0.05, num_leaves=127, min_data_in_leaf=100, feature_fraction=0.8, | |
| bagging_fraction=0.8, bagging_freq=1, lambda_l2=1.0, max_bin=255, verbose=-1) | |
| def fold_of(q, n): | |
| return (q.astype(np.int64) * 2654435761 % 2 ** 32) % n | |
| def train_lgb(X, y, q, path, seed): | |
| """Early stopping on an inner holdout of whole S1 groups; model saved atomically for resume.""" | |
| if path.exists(): | |
| return lgb.Booster(model_file=str(path)) | |
| inner = fold_of(q + 7, 10) == 0 | |
| p = dict(PARAMS, seed=seed, num_threads=CFG["lgb_threads"]) | |
| dtr = lgb.Dataset(X[~inner], y[~inner], free_raw_data=True) | |
| dva = lgb.Dataset(X[inner], y[inner], reference=dtr) | |
| m = lgb.train(p, dtr, num_boost_round=3000, valid_sets=[dva], | |
| callbacks=[lgb.early_stopping(100, verbose=False), lgb.log_evaluation(250)]) | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| tmp = path.with_suffix(".tmp") | |
| m.save_model(str(tmp), num_iteration=m.best_iteration) | |
| tmp.replace(path) | |
| log.info(f"saved {path.name} best_iter={m.best_iteration}") | |
| return lgb.Booster(model_file=str(path)) | |
| def load_feats(split, qset=None): | |
| parts = [] | |
| for i in range(n_shards(split)): | |
| d = pd.read_parquet(shard_path("feat", split, i)) | |
| if qset is not None: | |
| d = d[np.isin(d.q.values, qset)] | |
| parts.append(d) | |
| return pd.concat(parts, ignore_index=True) | |
| # ----------------------------------------------------------------------------------------------- stage 1 | |
| def run_stage1_train(): | |
| sp = load_splits() | |
| df = load_feats("train") | |
| feats = feature_columns(df) | |
| MODELS.mkdir(parents=True, exist_ok=True) | |
| save_json(feats, MODELS / "stage1_features.json") | |
| stack = df[np.isin(df.q.values, sp["g_stack"])].reset_index(drop=True) | |
| val = df[np.isin(df.q.values, sp["g_val"])].reset_index(drop=True) | |
| del df | |
| F = CFG["n_folds"] | |
| fold = fold_of(stack.q.values, F) | |
| oof = np.zeros(len(stack)) | |
| vp = np.zeros(len(val)) | |
| rng = np.random.default_rng(CFG["seed"]) | |
| for k in range(F): | |
| tr = stack[fold != k] | |
| keep = (tr.y.values == 1) | (tr.rrf_rank.values < CFG["s1_neg_keep_rank"]) | (rng.random(len(tr)) < CFG["s1_neg_frac"]) | |
| tr = tr[keep] | |
| m = train_lgb(tr[feats].values.astype(np.float32), tr.y.values, tr.q.values, MODELS / f"s1_fold{k}.txt", k) | |
| oof[fold == k] = m.predict(stack.loc[fold == k, feats].values.astype(np.float32)) | |
| vp += m.predict(val[feats].values.astype(np.float32)) / F | |
| out = pd.concat([stack[["q", "c", "y"]].assign(p1=oof), val[["q", "c", "y"]].assign(p1=vp)], ignore_index=True) | |
| save_parquet(out, WORK / "stage1" / "train.parquet") | |
| from sklearn.metrics import roc_auc_score | |
| log.info(f"stage1 AUC stack-oof={roc_auc_score(stack.y, oof):.5f} val={roc_auc_score(val.y, vp):.5f}") | |
| def fold_predictor(models, X_check): | |
| """Mean of the fold models' probabilities. Uses the exact GPU tree evaluator (gpu_lgb.py) when a GPU is | |
| present and it reproduces LightGBM on a check sample (|diff| < 1e-9); otherwise LightGBM on the CPU.""" | |
| cpu = lambda X: sum(m.predict(X, num_threads=CFG["lgb_threads"]) for m in models) / len(models) | |
| try: | |
| import torch | |
| if torch.cuda.is_available(): | |
| from gpu_lgb import GpuForest | |
| fs = [GpuForest(m, torch.device("cuda:0")) for m in models] | |
| gpu = lambda X: sum(f.predict(X) for f in fs) / len(fs) | |
| diff = float(np.abs(gpu(X_check) - cpu(X_check)).max()) | |
| if diff < 1e-9: | |
| log.info(f"LightGBM scoring on GPU (check max|diff|={diff:.1e})") | |
| return gpu | |
| log.warning(f"GPU tree evaluator differs from LightGBM ({diff:.1e}); using CPU") | |
| except Exception as e: # noqa: BLE001 | |
| log.warning(f"GPU tree evaluator unavailable ({e}); using CPU") | |
| return cpu | |
| def run_stage1_test(): | |
| feats = load_json(MODELS / "stage1_features.json") | |
| models = [lgb.Booster(model_file=str(MODELS / f"s1_fold{k}.txt")) for k in range(CFG["n_folds"])] | |
| predict = None | |
| for i in range(n_shards("test")): | |
| out = shard_path("stage1", "test", i) | |
| if out.exists(): | |
| continue | |
| d = pd.read_parquet(shard_path("feat", "test", i), columns=["q", "c"] + feats) | |
| X = d[feats].values.astype(np.float32) | |
| if predict is None: | |
| predict = fold_predictor(models, X[:20_000]) | |
| p = predict(X) | |
| save_parquet(d[["q", "c"]].assign(p1=p.astype(np.float32)), out) | |
| # ----------------------------------------------------------------------------------------------- stage-2 set | |
| def _s2(df): | |
| df = df.sort_values(["q", "p1"], ascending=[True, False], kind="stable") | |
| g = df.groupby("q") | |
| df["p1_rank"] = g.cumcount().astype(np.float32) | |
| df["p1_gap"] = (g.p1.transform("max") - df.p1).astype(np.float32) | |
| return df[(df.p1_rank < CFG["s2_topn"]) & (df.p1 >= CFG["s2_min_prob"])].reset_index(drop=True) | |
| def run_s2set(split): | |
| if split == "train": | |
| s2 = _s2(pd.read_parquet(WORK / "stage1" / "train.parquet")) | |
| sp = load_splits() | |
| feat_pos = int(pd.read_parquet(WORK / "stage1" / "train.parquet", columns=["y"]).y.sum()) | |
| allq = np.concatenate([sp["g_stack"], sp["g_val"]]) | |
| rep = {"rows": len(s2), "true_in_blocking": feat_pos, "true_in_s2": int(s2.y.sum()), | |
| "true_total": int(sp["n_true"][allq].sum())} | |
| rep["s2_recall_vs_blocking"] = rep["true_in_s2"] / max(feat_pos, 1) | |
| rep["s2_recall_vs_total"] = rep["true_in_s2"] / max(rep["true_total"], 1) | |
| save_json(rep, WORK / "reports" / "s2set_train.json") | |
| log.info(f"stage-2 set: {rep}") | |
| else: | |
| s2 = _s2(pd.concat([pd.read_parquet(shard_path("stage1", "test", i)) for i in range(n_shards("test"))], | |
| ignore_index=True)) | |
| save_parquet(s2, WORK / "s2" / f"{split}.parquet") | |
| # ----------------------------------------------------------------------------------------------- stage 2 | |
| def _attach(split, s2): | |
| """Join pair features and cross-encoder scores onto the stage-2 rows.""" | |
| from cross_encoder import load_ce_scores | |
| qs = np.unique(s2.q.values) | |
| parts = [] | |
| key = s2[["q", "c"]] | |
| for i in range(n_shards(split)): | |
| d = pd.read_parquet(shard_path("feat", split, i)) | |
| d = d[np.isin(d.q.values, qs)] | |
| parts.append(key[np.isin(key.q.values, np.unique(d.q.values))].merge(d, on=["q", "c"], how="left")) | |
| f = pd.concat(parts, ignore_index=True) | |
| s2 = s2.merge(f.drop(columns=[c for c in ("y",) if c in s2.columns]), on=["q", "c"], how="left") | |
| ce = load_ce_scores(split) | |
| s2 = s2.merge(ce, on=["q", "c"], how="left") | |
| g = s2.groupby("q") | |
| s2["ce_rank"] = g.ce.rank(ascending=False, method="first").astype(np.float32) | |
| s2["ce_gap"] = (g.ce.transform("max") - s2.ce).astype(np.float32) | |
| return s2 | |
| def stage2_features(s2, variant): | |
| base = [c for c in s2.columns if c not in ("q", "c", "y", "ce", "ce_rank", "ce_gap")] | |
| return base + (["ce", "ce_rank", "ce_gap"] if variant == "ce" else []) | |
| def run_stage2_train(): | |
| sp = load_splits() | |
| s2 = pd.read_parquet(WORK / "s2" / "train.parquet") | |
| s2 = _attach("train", s2) | |
| stack = s2[np.isin(s2.q.values, sp["g_stack"])].reset_index(drop=True) | |
| val = s2[np.isin(s2.q.values, sp["g_val"])].reset_index(drop=True) | |
| F = CFG["n_folds"] | |
| fold = fold_of(stack.q.values, F) | |
| country = load_prep("train", "s1", ["country"]).country.values | |
| results, preds = {}, {} | |
| with Pool(n_workers()) as pool: | |
| for variant in ("noce", "ce"): | |
| feats = stage2_features(s2, variant) | |
| oof, vp = np.zeros(len(stack)), np.zeros(len(val)) | |
| for k in range(F): | |
| tr = stack[fold != k] | |
| m = train_lgb(tr[feats].values.astype(np.float32), tr.y.values, tr.q.values, | |
| MODELS / f"s2_{variant}_fold{k}.txt", 100 + k) | |
| oof[fold == k] = m.predict(stack.loc[fold == k, feats].values.astype(np.float32)) | |
| vp += m.predict(val[feats].values.astype(np.float32)) / F | |
| cal = crossfit_isotonic(oof, stack.y.values, stack.q.values, F) | |
| preds[variant] = (oof, vp, feats) | |
| d = stack[["q", "c"]].assign(p=cal) | |
| fams = [("expf05", None)] + [("threshold", t) for t in CFG["thr_grid"]] | |
| for fam, thr in fams: | |
| sel = decide(d, fam, thr, CFG["arb_max_iter"], pool) | |
| f, _ = macro_f05(sel, sp["g_stack"], sp["n_true"], sp["gt_q"], sp["gt_c"]) | |
| results[(variant, fam, thr)] = f | |
| log.info(f"stack F0.5 variant={variant} family={fam} thr={thr}: {f:.5f}") | |
| best = max(results, key=results.get) | |
| variant, fam, thr = best | |
| oof, vp, feats = preds[variant] | |
| iso = fit_isotonic(oof, stack.y.values) | |
| vcal = iso.predict(vp) | |
| sel = decide(val[["q", "c"]].assign(p=vcal), fam, thr, CFG["arb_max_iter"], pool) | |
| f, per = macro_f05(sel, sp["g_val"], sp["n_true"], sp["gt_q"], sp["gt_c"]) | |
| per["country"] = country[per.q.values] | |
| by_c = per.groupby("country").f.mean().to_dict() | |
| npred = sel.groupby("q").size() | |
| val_stats = _pred_stats(sel, sp["g_val"], country) | |
| bundle = {"variant": variant, "family": fam, "thr": thr, "features": feats, | |
| "stack_scores": {f"{v}|{fa}|{t}": s for (v, fa, t), s in results.items()}, | |
| "val_f05": f, "val_f05_by_country": by_c, "val_pred_stats": val_stats} | |
| with open(MODELS / "stage2_iso.pkl", "wb") as fh: | |
| pickle.dump(iso, fh) | |
| save_json(bundle, MODELS / "stage2_bundle.json") | |
| # error analysis + importance | |
| imp = pd.DataFrame({"feature": feats}) | |
| for k in range(F): | |
| m = lgb.Booster(model_file=str(MODELS / f"s2_{variant}_fold{k}.txt")) | |
| imp[f"gain_{k}"] = m.feature_importance("gain") | |
| imp["gain"] = imp.filter(like="gain_").mean(1) | |
| imp.sort_values("gain", ascending=False)[["feature", "gain"]].to_csv(WORK / "reports" / "feature_importance.tsv", sep="\t", index=False) | |
| per.sort_values("f").head(2000).to_csv(WORK / "reports" / "val_errors.tsv", sep="\t", index=False) | |
| lines = [f"# Stage-2 validation report", "", | |
| f"- chosen: variant **{variant}**, decision **{fam}**" + (f" (thr {thr})" if thr else ""), | |
| f"- validation macro F0.5: **{f:.5f}**", | |
| f"- by country: {', '.join(f'{k}: {v:.5f}' for k, v in by_c.items())}", | |
| f"- stack F0.5 of all options: see models/stage2_bundle.json", | |
| f"- predicted matches per S1 (val): {val_stats}"] | |
| (WORK / "reports" / "report.md").write_text("\n".join(lines) + "\n") | |
| log.info("\n".join(lines)) | |
| def _pred_stats(sel, qs, country): | |
| n = pd.Series(0, index=qs).add(sel.groupby("q").size(), fill_value=0).loc[qs] | |
| c = pd.Series(country[qs], index=qs) | |
| out = {} | |
| for cc, g in n.groupby(c): | |
| out[cc] = {"n_s1": int(len(g)), "empty_rate": round(float((g == 0).mean()), 4), | |
| "mean_pred": round(float(g.mean()), 3), | |
| "dist": {str(k): round(float(v), 4) for k, v in g.clip(upper=6).value_counts(normalize=True).sort_index().items()}} | |
| return out | |
| # ----------------------------------------------------------------------------------------------- test + submission | |
| def run_stage2_test(): | |
| bundle = load_json(MODELS / "stage2_bundle.json") | |
| with open(MODELS / "stage2_iso.pkl", "rb") as fh: | |
| iso = pickle.load(fh) | |
| s2 = _attach("test", pd.read_parquet(WORK / "s2" / "test.parquet")) | |
| feats = bundle["features"] | |
| X = s2[feats].values.astype(np.float32) | |
| models = [lgb.Booster(model_file=str(MODELS / f"s2_{bundle['variant']}_fold{k}.txt")) for k in range(CFG["n_folds"])] | |
| raw = fold_predictor(models, X[:20_000])(X) | |
| s2["p"] = iso.predict(raw) | |
| save_parquet(s2[["q", "c", "p1", "p"] + (["ce"] if "ce" in s2 else [])], WORK / "stage2" / "test_scores.parquet") | |
| with Pool(n_workers()) as pool: | |
| sel = decide(s2[["q", "c", "p"]], bundle["family"], bundle["thr"], CFG["arb_max_iter"], pool) | |
| write_submission(s2, sel, bundle) | |
| def write_submission(s2, sel, bundle): | |
| sp = load_splits() | |
| s1 = load_prep("test", "s1", ["id", "country"]) | |
| pool_ids = load_prep("test", "pool", ["id"]).id.values | |
| q_all = sp["q_test"] | |
| OUTPUT.mkdir(parents=True, exist_ok=True) | |
| def lists(df): | |
| d = df.assign(cid=pool_ids[df.c.values]).groupby("q").cid.apply(lambda x: ",".join(dict.fromkeys(x))) | |
| return pd.Series(q_all).map(d).fillna("").values | |
| cand = pd.DataFrame({"source1_entity_id": s1.id.values[q_all], "candidate_entity_ids": lists(s2)}) | |
| match = pd.DataFrame({"source1_entity_id": s1.id.values[q_all], "matched_entity_ids": lists(sel)}) | |
| for df, name in ((match, "matching_results.tsv"), (cand, "candidate_pairs.tsv")): | |
| tmp = OUTPUT / f".{name}.tmp" | |
| df.to_csv(tmp, sep="\t", index=False) | |
| tmp.replace(OUTPUT / name) | |
| # in-script checks | |
| assert match.source1_entity_id.is_unique and len(match) == len(q_all) | |
| cset = dict(zip(cand.source1_entity_id, cand.candidate_entity_ids)) | |
| for sid, m in zip(match.source1_entity_id, match.matched_entity_ids): | |
| if m: | |
| ids = m.split(",") | |
| assert len(ids) == len(set(ids)) and all(x[:3] in ("S2-", "S3-") for x in ids) | |
| assert set(ids) <= set(cset[sid].split(",")), f"match outside candidates for {sid}" | |
| stats = _pred_stats(sel, q_all, s1.country.values) | |
| rep = {"test_pred_stats": stats, "val_pred_stats": bundle["val_pred_stats"], | |
| "train_singleton_rate": float((sp["n_true"] == 0).mean()), | |
| "train_mean_true": float(sp["n_true"].mean())} | |
| val_mean = np.mean([v["mean_pred"] for v in bundle["val_pred_stats"].values()]) | |
| val_empty = np.mean([v["empty_rate"] for v in bundle["val_pred_stats"].values()]) | |
| flags = [] | |
| for c, v in stats.items(): | |
| if abs(v["mean_pred"] - val_mean) > 0.35 * val_mean or abs(v["empty_rate"] - val_empty) > 0.05: | |
| flags.append(f"{c}: mean_pred={v['mean_pred']} empty_rate={v['empty_rate']} vs val {val_mean:.3f}/{val_empty:.3f}") | |
| rep["country_shift_flags"] = flags | |
| if CFG["test_limit"] is None: | |
| r = subprocess.run([sys.executable, str(PATHS["validator"]), "--matching", str(OUTPUT / "matching_results.tsv"), | |
| "--candidate", str(OUTPUT / "candidate_pairs.tsv"), "--test-dir", str(PATHS["raw_test_dir"]), "--check-ids"], | |
| capture_output=True, text=True) | |
| rep["validator"] = {"returncode": r.returncode, "stdout": r.stdout[-4000:], "stderr": r.stderr[-2000:]} | |
| if r.returncode != 0: | |
| save_json(rep, WORK / "reports" / "test_report.json") | |
| raise RuntimeError(f"validator failed:\n{r.stdout}\n{r.stderr}") | |
| save_json(rep, WORK / "reports" / "test_report.json") | |
| log.info(f"submission written to {OUTPUT}; per-country prediction stats: {stats}; flags: {flags}") | |
| # ----------------------------------------------------------------------------------------------- split B proxy | |
| def run_eval_b(): | |
| """Leave-one-country-out proxy for unseen France: train on one country's stack, evaluate on the other | |
| country's val (stage-1 features, no cross-encoder), compared with same-country val.""" | |
| sp = load_splits() | |
| country = load_prep("train", "s1", ["country"]).country.values | |
| df = load_feats("train") | |
| feats = feature_columns(df) | |
| rep = {} | |
| with Pool(n_workers()) as pool: | |
| for tr_c, te_c in (("us", "india"), ("india", "us")): | |
| st = df[np.isin(df.q.values, sp["g_stack"]) & (country[df.q.values] == tr_c)].reset_index(drop=True) | |
| fold = fold_of(st.q.values, 3) | |
| oof = np.zeros(len(st)) | |
| ms = [] | |
| for k in range(3): | |
| tr = st[fold != k] | |
| m = train_lgb(tr[feats].values.astype(np.float32), tr.y.values, tr.q.values, | |
| MODELS / f"B_{tr_c}_fold{k}.txt", 200 + k) | |
| oof[fold == k] = m.predict(st.loc[fold == k, feats].values.astype(np.float32)) | |
| ms.append(m) | |
| iso = fit_isotonic(oof, st.y.values) | |
| for ev_c in (tr_c, te_c): | |
| qv = sp["g_val"][country[sp["g_val"]] == ev_c] | |
| v = df[np.isin(df.q.values, qv)] | |
| p = iso.predict(sum(m.predict(v[feats].values.astype(np.float32)) for m in ms) / 3) | |
| sel = decide(v[["q", "c"]].assign(p=p), "expf05", None, CFG["arb_max_iter"], pool) | |
| f, _ = macro_f05(sel, qv, sp["n_true"], sp["gt_q"], sp["gt_c"]) | |
| rep[f"train_{tr_c}->val_{ev_c}"] = {"f05": f, **_pred_stats(sel, qv, country)[ev_c]} | |
| log.info(f"split B train {tr_c} -> val {ev_c}: F0.5={f:.5f}") | |
| save_json(rep, WORK / "reports" / "split_B.json") | |