Upload src/xscript/eval/bts.py with huggingface_hub
Browse files- src/xscript/eval/bts.py +136 -0
src/xscript/eval/bts.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Bilingual Transfer Score and the thesis's interaction estimate.
|
| 2 |
+
|
| 3 |
+
BTS(L) = (BPB_mono(L) - BPB_bi(L)) / BPB_mono(L) (positive = bilingual helps)
|
| 4 |
+
|
| 5 |
+
Two matchings (the plan asks for both):
|
| 6 |
+
- matched-total : mono-final vs bilingual-final (both at the same TOTAL
|
| 7 |
+
tokens). This is ATLAS's framing; under a starved tokenizer
|
| 8 |
+
the cross-script partner saw fewer of its own tokens.
|
| 9 |
+
- matched-lang : bilingual-final vs the mono checkpoint at the same number
|
| 10 |
+
of *that-language* tokens the bilingual run actually saw
|
| 11 |
+
(= total x mixing-prob). Removes the token-count confound.
|
| 12 |
+
|
| 13 |
+
Headline interaction (the untested contribution):
|
| 14 |
+
penalty(C) = mean BTS(same-script) - mean BTS(cross-script) under tokenizer C
|
| 15 |
+
interaction = penalty(starved) - penalty(destarved)
|
| 16 |
+
A large positive interaction => the cross-script "penalty" is largely a
|
| 17 |
+
tokenizer starvation artifact, not intrinsic script transfer.
|
| 18 |
+
|
| 19 |
+
Run naming convention (see configs/matrix.py): "<mix>__<tok_name>", where mix is
|
| 20 |
+
one language ("en") or "en-<partner>", and tok_name is e.g. "bl_destarved".
|
| 21 |
+
"""
|
| 22 |
+
import json
|
| 23 |
+
from pathlib import Path
|
| 24 |
+
|
| 25 |
+
from ..langs import ANCHOR, PARTNERS, LANGS
|
| 26 |
+
from ..paths import RUNS, RESULTS, ensure
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _read_evals(name: str) -> list[dict]:
|
| 30 |
+
"""[(tokens, {source: bpb})...] sorted by tokens, from a run's train.jsonl."""
|
| 31 |
+
log = RUNS / name / "train.jsonl"
|
| 32 |
+
if not log.exists():
|
| 33 |
+
return []
|
| 34 |
+
series = {}
|
| 35 |
+
for line in log.read_text().splitlines():
|
| 36 |
+
try:
|
| 37 |
+
rec = json.loads(line)
|
| 38 |
+
except json.JSONDecodeError:
|
| 39 |
+
continue
|
| 40 |
+
ev = rec.get("eval") or rec.get("eval_final")
|
| 41 |
+
if ev:
|
| 42 |
+
series[rec["tokens"]] = {s: v["bpb"] for s, v in ev.items()}
|
| 43 |
+
return [{"tokens": t, "bpb": series[t]} for t in sorted(series)]
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _final_bpb(name: str, source: str) -> float | None:
|
| 47 |
+
ev = _read_evals(name)
|
| 48 |
+
for rec in reversed(ev):
|
| 49 |
+
if source in rec["bpb"]:
|
| 50 |
+
return rec["bpb"][source]
|
| 51 |
+
return None
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _bpb_at_lang_tokens(name: str, source: str, lang_tokens: float) -> float | None:
|
| 55 |
+
"""Mono BPB at the checkpoint closest to `lang_tokens` (matched-lang)."""
|
| 56 |
+
best, bd = None, None
|
| 57 |
+
for rec in _read_evals(name):
|
| 58 |
+
if source in rec["bpb"]:
|
| 59 |
+
d = abs(rec["tokens"] - lang_tokens)
|
| 60 |
+
if bd is None or d < bd:
|
| 61 |
+
bd, best = d, rec["bpb"][source]
|
| 62 |
+
return best
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def compute(tok_name: str, source_kind: str = "flores",
|
| 66 |
+
mix_prob: float = 0.5, total_tokens: float = 30e9) -> dict:
|
| 67 |
+
"""BTS for every partner under one tokenizer condition."""
|
| 68 |
+
rows = {}
|
| 69 |
+
for p in PARTNERS:
|
| 70 |
+
src = f"{source_kind}_{p}"
|
| 71 |
+
mono, bi = f"{p}__{tok_name}", f"{ANCHOR}-{p}__{tok_name}"
|
| 72 |
+
bpb_mono = _final_bpb(mono, src)
|
| 73 |
+
bpb_bi = _final_bpb(bi, src)
|
| 74 |
+
entry = {"same_script": LANGS[p].same_script_as_en,
|
| 75 |
+
"bpb_mono_final": bpb_mono, "bpb_bi_final": bpb_bi}
|
| 76 |
+
if bpb_mono and bpb_bi:
|
| 77 |
+
entry["bts_matched_total"] = (bpb_mono - bpb_bi) / bpb_mono
|
| 78 |
+
bpb_mono_lang = _bpb_at_lang_tokens(mono, src, total_tokens * mix_prob)
|
| 79 |
+
if bpb_mono_lang:
|
| 80 |
+
entry["bpb_mono_at_lang_tokens"] = bpb_mono_lang
|
| 81 |
+
entry["bts_matched_lang"] = (bpb_mono_lang - bpb_bi) / bpb_mono_lang
|
| 82 |
+
rows[p] = entry
|
| 83 |
+
return rows
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def _penalty(rows: dict, key: str) -> float | None:
|
| 87 |
+
same = [r[key] for r in rows.values() if r.get("same_script") and key in r]
|
| 88 |
+
cross = [r[key] for r in rows.values() if not r.get("same_script") and key in r]
|
| 89 |
+
if not same or not cross:
|
| 90 |
+
return None
|
| 91 |
+
return sum(same) / len(same) - sum(cross) / len(cross)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def run(flavor: str = "unigram", source_kind: str = "flores",
|
| 95 |
+
total_tokens: float = 30e9, mix_prob: float = 0.5,
|
| 96 |
+
out_dir: Path | None = None) -> dict:
|
| 97 |
+
out_dir = ensure(Path(out_dir) if out_dir else RESULTS / "bts")
|
| 98 |
+
conds = {c: compute(f"{flavor}_{c}", source_kind, mix_prob, total_tokens)
|
| 99 |
+
for c in ("starved", "destarved")}
|
| 100 |
+
inter = {}
|
| 101 |
+
for key in ("bts_matched_total", "bts_matched_lang"):
|
| 102 |
+
ps = {c: _penalty(conds[c], key) for c in conds}
|
| 103 |
+
if ps["starved"] is not None and ps["destarved"] is not None:
|
| 104 |
+
inter[key] = {"penalty_starved": ps["starved"],
|
| 105 |
+
"penalty_destarved": ps["destarved"],
|
| 106 |
+
"interaction": ps["starved"] - ps["destarved"]}
|
| 107 |
+
result = {"flavor": flavor, "source": source_kind, "by_condition": conds,
|
| 108 |
+
"interaction": inter}
|
| 109 |
+
(out_dir / f"bts_{flavor}_{source_kind}.json").write_text(json.dumps(result, indent=2))
|
| 110 |
+
|
| 111 |
+
md = [f"# BTS ({flavor}, eval on {source_kind})", ""]
|
| 112 |
+
for c, rows in conds.items():
|
| 113 |
+
md += [f"## {c}", "",
|
| 114 |
+
"| partner | script | BPB mono | BPB bi | BTS (total) | BTS (lang) |",
|
| 115 |
+
"|---|---|---|---|---|---|"]
|
| 116 |
+
for p, r in rows.items():
|
| 117 |
+
md.append("| {} | {} | {} | {} | {} | {} |".format(
|
| 118 |
+
p, "same" if r["same_script"] else "cross",
|
| 119 |
+
_f(r.get("bpb_mono_final")), _f(r.get("bpb_bi_final")),
|
| 120 |
+
_f(r.get("bts_matched_total")), _f(r.get("bts_matched_lang"))))
|
| 121 |
+
md.append("")
|
| 122 |
+
md += ["## Interaction (same-script penalty - cross-script penalty)", ""]
|
| 123 |
+
for key, v in inter.items():
|
| 124 |
+
md.append(f"- **{key}**: penalty(starved)={v['penalty_starved']:.4f}, "
|
| 125 |
+
f"penalty(destarved)={v['penalty_destarved']:.4f}, "
|
| 126 |
+
f"**interaction={v['interaction']:.4f}**")
|
| 127 |
+
md += ["", "> interaction >> 0 => cross-script penalty is a tokenizer-"
|
| 128 |
+
"starvation artifact.", "> interaction ~ 0 => penalty persists "
|
| 129 |
+
"under a fair tokenizer (genuine script effect)."]
|
| 130 |
+
(out_dir / f"bts_{flavor}_{source_kind}.md").write_text("\n".join(md) + "\n")
|
| 131 |
+
print(f"[bts] wrote {out_dir}/bts_{flavor}_{source_kind}.md")
|
| 132 |
+
return result
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def _f(x):
|
| 136 |
+
return f"{x:.4f}" if isinstance(x, float) else "-"
|