File size: 5,349 Bytes
d04142e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | #!/usr/bin/env python3
"""Compare baseline vs encoder on the untouched test split. Writes outputs/metrics.json and outputs/eval_report.txt
and models/saudi-tender-tagger/config.json (which model ships per label)."""
import json, os, sys
from collections import Counter
import pandas as pd
from sklearn.metrics import accuracy_score, f1_score
sys.path.insert(0, os.path.dirname(__file__)); from common import *
O = f"{ROOT}/outputs"; te = pd.read_csv(f"{ROOT}/data/published/test.csv", dtype=str).fillna(""); tr = pd.read_csv(f"{ROOT}/data/published/train.csv", dtype=str).fillna("")
te = te.set_index("ref")
seen = set(zip(tr.title.map(norm_text), tr.agency.map(norm_text)))
novel = [(norm_text(t), norm_text(a)) not in seen for t, a in zip(te.title, te.agency)]
te["novel"] = novel
def m(y, p): return {"accuracy": round(accuracy_score(y, p), 4), "macro_f1": round(f1_score(y, p, average="macro"), 4)}
out, lines, ship = {}, [], {}
P = {}
for lab in ("activity", "ttype"):
for model in ("baseline", "encoder"):
P[(lab, model)] = pd.read_csv(f"{O}/pred_{model}_{lab}.csv", dtype=str).set_index("ref")
T3 = json.load(open(f"{O}/top3.json")); ENC = {l: json.load(open(f"{O}/encoder_{l}_metrics.json")) for l in ("activity", "ttype")}
for lab in ("activity", "ttype"):
rows = {}
for model in ("baseline", "encoder"):
d = P[(lab, model)].loc[te.index]
r = m(d.true, d.pred); r["accuracy_novel_titles"] = round(accuracy_score(d.true[te.novel], d.pred[te.novel]), 4)
rows[model] = r
c = Counter(zip(d.true[d.true != d.pred], d.pred[d.true != d.pred])).most_common(6)
lines.append(f"\n[{lab} / {model}] {r}\n top confusions (true -> predicted, count):")
lines += [f" {a} -> {b}: {n}" for (a, b), n in c]
f1s = pd.Series(f1_score(d.true, d.pred, average=None, labels=sorted(set(d.true)), zero_division=0), index=sorted(set(d.true)))
sup = d.true.value_counts()
w = f1s[sup[f1s.index] >= 5].sort_values().head(5)
lines.append(" weakest classes (support>=5): " + "; ".join(f"{k} F1={v:.2f} n={sup[k]}" for k, v in w.items()))
if lab == "activity": rows["baseline"]["top3_accuracy"] = round(T3["activity_baseline"], 4); rows["encoder"]["top3_accuracy"] = round(ENC["activity"]["top3_accuracy"], 4); rows["encoder"]["base_model"] = ENC["activity"]["base_model"]
win = max(rows, key=lambda k: (rows[k]["macro_f1"], rows[k]["accuracy"])); rows["ships"] = win; out[lab] = rows; ship[lab] = win
# derived labels from predicted activity vs truth
for model in ("baseline", "encoder"):
d = P[("activity", model)].loc[te.index]
ps = [sector_of(x) for x in d.pred]; pk = [kind_of(x, tt, ti) for x, tt, ti in zip(d.pred, P[("ttype", model)].loc[te.index].pred, te.title)]
out.setdefault("sector_derived", {})[model] = m(te.sector, ps); out.setdefault("kind_derived", {})[model] = m(te.kind, pk)
lines.append(f"\n[sector derived from predicted activity / {model}] {out['sector_derived'][model]}")
lines.append(f"[kind rules on predicted activity+type / {model}] {out['kind_derived'][model]}")
c = Counter((a, b) for a, b in zip(te.sector, ps) if a != b).most_common(5)
lines.append(" top sector confusions: " + "; ".join(f"{a}->{b}:{n}" for (a, b), n in c))
# MARBERTv2 6-epoch row (measured, not shipped: no stated license)
mb = json.load(open(f"{O}/marbert_6ep/encoder_activity_metrics.json"))
out["activity_marbertv2_6ep_not_shipped"] = {"accuracy": round(mb["accuracy"], 4), "macro_f1": round(mb["macro_f1"], 4), "top3_accuracy": round(T3.get("activity_marbert_6ep", float("nan")), 4), "reason": "base model has no stated license"}
# sector: direct TF-IDF head vs derived from the shipped activity model
ship_act = out["activity"]["ships"]
dsd = P[("activity", ship_act)].loc[te.index]
derived = m(te.sector, [sector_of(x) for x in dsd.pred])
bs = pd.read_csv(f"{O}/pred_baseline_sector.csv", dtype=str).set_index("ref").loc[te.index]
direct = m(bs.true, bs.pred)
out["sector_direct_tfidf"] = direct; out["sector_derived_from_shipped_activity"] = derived
out["sector_route"] = "direct" if direct["macro_f1"] > derived["macro_f1"] else "derived"
ship["sector"] = out["sector_route"]
lines.append(f"\n[sector] direct TF-IDF {direct} vs derived from {ship_act} activity {derived} -> ship {out['sector_route']}")
bm = json.load(open(f"{O}/baseline_metrics.json"))
out["baseline_direct_sector_and_kind"] = {k: bm[k] for k in ("sector", "kind")}
out["test_n"] = len(te); out["test_novel_title_n"] = int(te.novel.sum())
json.dump(out, open(f"{O}/metrics.json", "w"), indent=1, ensure_ascii=False)
open(f"{O}/eval_report.txt", "w").write(json.dumps(out, indent=1, ensure_ascii=False) + "\n" + "\n".join(lines))
cfg = {"name": "saudi-tender-tagger", "input": "title + ' | ' + agency", "max_length": 128,
"heads": {**{l: ({"type": "encoder", "path": f"{l}_encoder"} if ship[l] == "encoder" else {"type": "tfidf", "path": f"{l}_tfidf"}) for l in ("activity", "ttype")}, **({"sector": {"type": "tfidf", "path": "sector_tfidf"}} if ship["sector"] == "direct" else {})},
"derived": {"sector": "labels/activity_to_sector.json", "kind": "labels/activity_to_kind.json + rules in scripts/common.py"}}
json.dump(cfg, open(f"{ROOT}/models/saudi-tender-tagger/config.json", "w"), indent=1)
print(open(f"{O}/eval_report.txt").read())
|