Ouaill commited on
Commit
4927edf
·
verified ·
1 Parent(s): 343bbb9

Upload eval_and_compare.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. eval_and_compare.py +277 -0
eval_and_compare.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3 -u
2
+ """eval_and_compare.py — Evaluate all ours (28) + externals, generate plot, save results."""
3
+
4
+ import json, os, sys, time, csv, gc, warnings
5
+ from collections import Counter
6
+ from dataclasses import dataclass, field, asdict
7
+ from typing import List, Dict
8
+
9
+ import numpy as np
10
+ warnings.filterwarnings("ignore")
11
+
12
+ import matplotlib
13
+ matplotlib.use("Agg")
14
+ import matplotlib.pyplot as plt
15
+
16
+ BASE = "/root/oiq_cc_tokenizer/results"
17
+ CORPORA = os.path.join(BASE, "corpora")
18
+ TOK_DIR = os.path.join(BASE, "tokenizers")
19
+ PLOTS_DIR = os.path.join(BASE, "plots")
20
+
21
+ import regex
22
+ _WORD_PAT = regex.compile(r"[\p{L}\p{M}\p{N}]+", regex.UNICODE)
23
+ _AR_PAT = regex.compile(r"[\u0600-\u06FF\u0750-\u077F]")
24
+ _SPECIAL = {"<unk>", "<s>", "</s>", "[CLS]", "[SEP]", "[PAD]", "[UNK]", "<pad>"}
25
+
26
+ def segment_words(t): return _WORD_PAT.findall(t)
27
+ def count_graphemes(t): return len(regex.findall(r"\X", t))
28
+ def detect_script(t): return "ar" if len(_AR_PAT.findall(t)) > len(t) * 0.3 else "az"
29
+ def filter_sp(tokens): return [t for t in tokens if t not in _SPECIAL]
30
+
31
+ @dataclass
32
+ class M:
33
+ name: str = ""
34
+ source: str = ""
35
+ algorithm: str = ""
36
+ architecture: str = ""
37
+ vocab_size: int = 0
38
+ fertility_ar: float = 0.0
39
+ fertility_az: float = 0.0
40
+ fertility_overall: float = 0.0
41
+ disparity: float = 0.0
42
+ cpt_ar: float = 0.0
43
+ cpt_az: float = 0.0
44
+ exact_match_ar: float = 0.0
45
+ exact_match_az: float = 0.0
46
+
47
+
48
+ class RawConcat:
49
+ def __init__(self, ar_j, az_j):
50
+ from tokenizers import Tokenizer
51
+ self.ar = Tokenizer.from_file(ar_j)
52
+ self.az = Tokenizer.from_file(az_j)
53
+
54
+ def encode(self, text):
55
+ s = detect_script(text)
56
+ t = self.ar if s == "ar" else self.az
57
+ enc = t.encode(text)
58
+ return enc.tokens, enc.ids, s
59
+
60
+ def decode(self, ids, script):
61
+ t = self.ar if script == "ar" else self.az
62
+ return t.decode(ids, skip_special_tokens=True)
63
+
64
+
65
+ class RawShared:
66
+ def __init__(self, j):
67
+ from tokenizers import Tokenizer
68
+ self.tok = Tokenizer.from_file(j)
69
+
70
+ def encode(self, text):
71
+ enc = self.tok.encode(text)
72
+ return enc.tokens, enc.ids, detect_script(text)
73
+
74
+ def decode(self, ids, script):
75
+ return self.tok.decode(ids, skip_special_tokens=True)
76
+
77
+
78
+ class HFTok:
79
+ def __init__(self, repo):
80
+ from transformers import AutoTokenizer
81
+ self.tok = AutoTokenizer.from_pretrained(repo, trust_remote_code=True)
82
+
83
+ def encode(self, text):
84
+ ids = self.tok.encode(text, add_special_tokens=False)
85
+ return self.tok.convert_ids_to_tokens(ids), ids, detect_script(text)
86
+
87
+ def decode(self, ids, script):
88
+ return self.tok.decode(ids, skip_special_tokens=True)
89
+
90
+
91
+ def evaluate(tok, name, source, algo, arch, vsz, texts):
92
+ m = M(name=name, source=source, algorithm=algo, architecture=arch, vocab_size=vsz)
93
+ ar_f, az_f, all_f = [], [], []
94
+ ar_c, az_c = [], []
95
+ ar_ok, az_ok, ar_n, az_n = 0, 0, 0, 0
96
+
97
+ for i, text in enumerate(texts):
98
+ if (i + 1) % 5000 == 0:
99
+ print(f" [{i+1}/{len(texts)}] {name}", flush=True)
100
+ try:
101
+ tokens, ids, script = tok.encode(text)
102
+ content = filter_sp(tokens)
103
+ words = segment_words(text)
104
+ if not words:
105
+ continue
106
+ fert = len(content) / len(words)
107
+ all_f.append(fert)
108
+ cpt = count_graphemes(text) / max(len(content), 1)
109
+ try:
110
+ dec = tok.decode(ids, script)
111
+ exact = dec.strip() == text.strip()
112
+ except:
113
+ exact = False
114
+ if script == "ar":
115
+ ar_f.append(fert); ar_c.append(cpt); ar_n += 1
116
+ if exact: ar_ok += 1
117
+ else:
118
+ az_f.append(fert); az_c.append(cpt); az_n += 1
119
+ if exact: az_ok += 1
120
+ except:
121
+ pass
122
+
123
+ m.fertility_ar = float(np.mean(ar_f)) if ar_f else 0
124
+ m.fertility_az = float(np.mean(az_f)) if az_f else 0
125
+ m.fertility_overall = float(np.mean(all_f)) if all_f else 0
126
+ mx = max(m.fertility_ar, m.fertility_az, 1e-9)
127
+ m.disparity = abs(m.fertility_ar - m.fertility_az) / mx
128
+ m.cpt_ar = float(np.mean(ar_c)) if ar_c else 0
129
+ m.cpt_az = float(np.mean(az_c)) if az_c else 0
130
+ m.exact_match_ar = ar_ok / max(ar_n, 1)
131
+ m.exact_match_az = az_ok / max(az_n, 1)
132
+ return m
133
+
134
+
135
+ def main():
136
+ # Load test texts
137
+ texts = []
138
+ for s in ("test_ar", "test_az", "test_mi"):
139
+ p = os.path.join(CORPORA, f"{s}.txt")
140
+ if os.path.exists(p):
141
+ with open(p) as f:
142
+ texts.extend(l.strip() for l in f if l.strip())
143
+ print(f"{len(texts)} texts", flush=True)
144
+
145
+ results = []
146
+
147
+ # --- Our tokenizers ---
148
+ for vsz in (8000, 16000, 32000):
149
+ for algo in ("bpe", "unigram", "wordpiece", "bbpe"):
150
+ # Shared
151
+ jp = os.path.join(TOK_DIR, f"shared_{algo}_{vsz}.json")
152
+ if os.path.exists(jp):
153
+ name = f"shared_{algo}_{vsz}"
154
+ print(f"\n{name}", flush=True)
155
+ tok = RawShared(jp)
156
+ r = evaluate(tok, name, "ours", algo, "shared", vsz, texts)
157
+ print(f" F={r.fertility_overall:.3f} D={r.disparity:.3f} EM_ar={r.exact_match_ar:.2%}", flush=True)
158
+ results.append(r)
159
+ del tok; gc.collect()
160
+
161
+ # Concat
162
+ ar_j = os.path.join(TOK_DIR, f"concat_ar_{algo}_{vsz//2}.json")
163
+ az_j = os.path.join(TOK_DIR, f"concat_az_{algo}_{vsz//2}.json")
164
+ if os.path.exists(ar_j) and os.path.exists(az_j):
165
+ name = f"concat_{algo}_{vsz}"
166
+ print(f"\n{name}", flush=True)
167
+ tok = RawConcat(ar_j, az_j)
168
+ r = evaluate(tok, name, "ours", algo, "concatenated", vsz, texts)
169
+ print(f" F={r.fertility_overall:.3f} D={r.disparity:.3f} EM_ar={r.exact_match_ar:.2%}", flush=True)
170
+ results.append(r)
171
+ del tok; gc.collect()
172
+
173
+ # --- External ---
174
+ externals = [
175
+ ("CaMeLBERT-MSA", "external_msa", "WordPiece", "shared", 30000, "CAMeL-Lab/bert-base-arabic-camelbert-msa"),
176
+ ("Asafaya-BERT", "external_msa", "WordPiece", "shared", 32000, "asafaya/bert-base-arabic"),
177
+ ("Aranizer-SP-86k", "external_msa", "SentencePiece", "shared", 86000, "riotu-lab/Aranizer-SP-86k"),
178
+ ("DarijaBERT-ar", "external_darija", "WordPiece", "shared", 80000, "SI2M-Lab/DarijaBERT"),
179
+ ("DarijaBERT-az", "external_darija", "WordPiece", "shared", 110000, "SI2M-Lab/DarijaBERT-arabizi"),
180
+ ]
181
+ for name, src, algo, arch, vsz, repo in externals:
182
+ print(f"\n{name} ({repo})", flush=True)
183
+ try:
184
+ tok = HFTok(repo)
185
+ r = evaluate(tok, name, src, algo, arch, vsz, texts)
186
+ print(f" F={r.fertility_overall:.3f} D={r.disparity:.3f} EM_ar={r.exact_match_ar:.2%}", flush=True)
187
+ results.append(r)
188
+ del tok; gc.collect()
189
+ except Exception as e:
190
+ print(f" FAILED: {e}", flush=True)
191
+
192
+ # Save
193
+ out_csv = os.path.join(BASE, "external_comparison.csv")
194
+ out_json = os.path.join(BASE, "external_comparison.json")
195
+ with open(out_csv, "w", newline="") as f:
196
+ w = csv.DictWriter(f, fieldnames=list(asdict(results[0]).keys()))
197
+ w.writeheader()
198
+ for r in results: w.writerow(asdict(r))
199
+ with open(out_json, "w") as f:
200
+ json.dump([asdict(r) for r in results], f, indent=2)
201
+
202
+ # Print table
203
+ print("\n" + "=" * 130, flush=True)
204
+ hdr = f"{'Name':<30} {'Source':<16} {'V':>7} {'Fert':>7} {'F_ar':>7} {'F_az':>7} {'Disp':>7} {'CPT_ar':>7} {'CPT_az':>7} {'EM_ar':>7} {'EM_az':>7}"
205
+ print(hdr, flush=True)
206
+ print("-" * 130, flush=True)
207
+ for r in sorted(results, key=lambda x: (0 if x.source == "ours" else 1, x.vocab_size)):
208
+ print(f"{r.name:<30} {r.source:<16} {r.vocab_size:>7,} {r.fertility_overall:>7.3f} {r.fertility_ar:>7.3f} {r.fertility_az:>7.3f} {r.disparity:>7.3f} {r.cpt_ar:>7.3f} {r.cpt_az:>7.3f} {r.exact_match_ar:>7.2%} {r.exact_match_az:>7.2%}", flush=True)
209
+ print("=" * 130, flush=True)
210
+
211
+ # Generate comparison plot
212
+ ours_best = []
213
+ for vsz in (8000, 16000, 32000):
214
+ cands = [r for r in results if r.vocab_size == vsz and r.architecture == "concatenated" and r.source == "ours"]
215
+ if cands:
216
+ best = min(cands, key=lambda x: x.fertility_overall)
217
+ ours_best.append(best)
218
+ ext = [r for r in results if r.source != "ours"]
219
+ plot_data = ours_best + ext
220
+
221
+ fig, axes = plt.subplots(2, 2, figsize=(16, 11))
222
+ colors = {"8000": "#E69F00", "16000": "#009E73", "32000": "#0072B2",
223
+ "external_msa": "#CC79A7", "external_darija": "#D55E00"}
224
+ labels = [f"Ours\n{r.name}\n({r.vocab_size:,})" for r in ours_best] + \
225
+ [f"{r.name}\n({r.vocab_size:,})" for r in ext]
226
+ bar_c = [colors[str(r.vocab_size)] for r in ours_best] + \
227
+ [colors.get(r.source, "#999") for r in ext]
228
+ n = len(plot_data)
229
+
230
+ for idx, (key, vals_fn, title, ylabel) in enumerate([
231
+ ("fert", lambda r: r.fertility_overall, "Overall Fertility (Lower = Better)", "Fertility"),
232
+ ("disp", lambda r: r.disparity, "Cross-Script Disparity (Lower = Better)", "Disparity"),
233
+ ]):
234
+ ax = axes[0, idx]
235
+ vals = [vals_fn(r) for r in plot_data]
236
+ bars = ax.bar(range(n), vals, color=bar_c, edgecolor="gray", linewidth=0.5)
237
+ ax.set_xticks(range(n))
238
+ ax.set_xticklabels(labels, fontsize=6, ha="center")
239
+ ax.set_ylabel(ylabel, fontsize=9)
240
+ ax.set_title(title, fontsize=10, fontweight="bold")
241
+ for b, v in zip(bars, vals):
242
+ ax.text(b.get_x()+b.get_width()/2, b.get_height()+0.005, f"{v:.3f}", ha="center", va="bottom", fontsize=6)
243
+
244
+ # Exact match grouped
245
+ ax = axes[1, 0]
246
+ x = np.arange(n)
247
+ w = 0.35
248
+ ax.bar(x-w/2, [r.exact_match_ar*100 for r in plot_data], w, label="Arabic", color="#56B4E9")
249
+ ax.bar(x+w/2, [r.exact_match_az*100 for r in plot_data], w, label="Arabizi", color="#333")
250
+ ax.set_xticks(x); ax.set_xticklabels(labels, fontsize=6, ha="center")
251
+ ax.set_ylabel("Exact Match (%)"); ax.set_title("Exact Reconstruction", fontsize=10, fontweight="bold")
252
+ ax.legend(fontsize=7); ax.set_ylim(0, 108)
253
+
254
+ # CPT grouped
255
+ ax = axes[1, 1]
256
+ ax.bar(x-w/2, [r.cpt_ar for r in plot_data], w, label="Arabic", color="#56B4E9")
257
+ ax.bar(x+w/2, [r.cpt_az for r in plot_data], w, label="Arabizi", color="#333")
258
+ ax.set_xticks(x); ax.set_xticklabels(labels, fontsize=6, ha="center")
259
+ ax.set_ylabel("CPT"); ax.set_title("Characters Per Token (Higher = Better)", fontsize=10, fontweight="bold")
260
+ ax.legend(fontsize=7)
261
+
262
+ from matplotlib.patches import Patch
263
+ fig.legend(handles=[
264
+ Patch(fc=colors["8000"], label="Ours (8K)"),
265
+ Patch(fc=colors["16000"], label="Ours (16K)"),
266
+ Patch(fc=colors["32000"], label="Ours (32K)"),
267
+ Patch(fc=colors["external_msa"], label="External (MSA)"),
268
+ Patch(fc=colors["external_darija"], label="External (Darija)"),
269
+ ], loc="upper center", ncol=5, fontsize=8, bbox_to_anchor=(0.5, 0.98), frameon=True)
270
+ plt.tight_layout(rect=[0, 0, 1, 0.95])
271
+ fig.savefig(os.path.join(PLOTS_DIR, "external_comparison.png"), dpi=150, bbox_inches="tight")
272
+ plt.close(fig)
273
+ print(f"\nPlot: {os.path.join(PLOTS_DIR, 'external_comparison.png')}", flush=True)
274
+ print("DONE!", flush=True)
275
+
276
+ if __name__ == "__main__":
277
+ main()