jvonrad commited on
Commit
4ed3d60
·
verified ·
1 Parent(s): f0d18ec

Upload src/xscript/eval/bts.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. 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 "-"