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

Upload compare_with_external.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. compare_with_external.py +269 -0
compare_with_external.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ compare_with_external.py
4
+ Compare our best 3 tokenizers (one per vocab size) against existing
5
+ Arabic/Darija tokenizers from HuggingFace.
6
+
7
+ Our tokenizers (from benchmark):
8
+ - concat_bpe_8000 (V=8K)
9
+ - concat_wordpiece_16000 (V=16K)
10
+ - concat_wordpiece_32000 (V=32K)
11
+
12
+ External tokenizers:
13
+ - CAMeL-Lab/bert-base-arabic-camelbert-msa (WordPiece 30K, MSA)
14
+ - asafaya/bert-base-arabic (WordPiece 32K, MSA)
15
+ - riotu-lab/Aranizer-SP-86k (SentencePiece 86K, MSA)
16
+ - SI2M-Lab/DarijaBERT (WordPiece 80K, Darija Arabic)
17
+ - SI2M-Lab/DarijaBERT-arabizi (WordPiece 110K, Darija Arabizi)
18
+ """
19
+
20
+ import json, os, sys, time, re, warnings
21
+ from collections import Counter
22
+ from dataclasses import dataclass, field, asdict
23
+ from typing import List, Dict, Tuple
24
+
25
+ import numpy as np
26
+
27
+ warnings.filterwarnings("ignore")
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Paths
31
+ # ---------------------------------------------------------------------------
32
+ BASE = "/root/oiq_cc_tokenizer"
33
+ RESULTS = os.path.join(BASE, "results")
34
+ CORPORA = os.path.join(RESULTS, "corpora")
35
+ TOKENIZER_DIR = os.path.join(RESULTS, "tokenizers")
36
+ TRANS_DIR = os.path.join(RESULTS, "transformers_tokenizers")
37
+
38
+ import regex
39
+ _WORD_PAT = regex.compile(r"[\p{L}\p{M}\p{N}]+", regex.UNICODE)
40
+ _AR_PAT = regex.compile(r"[\u0600-\u06FF\u0750-\u077F]")
41
+
42
+
43
+ def segment_words(text):
44
+ return _WORD_PAT.findall(text)
45
+
46
+
47
+ def count_graphemes(text):
48
+ return len(regex.findall(r"\X", text))
49
+
50
+
51
+ def detect_script(text):
52
+ return "ar" if len(_AR_PAT.findall(text)) > len(text) * 0.3 else "az"
53
+
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # Load test corpora
57
+ # ---------------------------------------------------------------------------
58
+ def load_test_texts():
59
+ texts = {"ar": [], "az": [], "mi": []}
60
+ for split in ("test", "val"):
61
+ for script in ("ar", "az", "mi"):
62
+ path = os.path.join(CORPORA, f"{split}_{script}.txt")
63
+ if os.path.exists(path):
64
+ with open(path, encoding="utf-8") as f:
65
+ texts[script].extend(l.strip() for l in f if l.strip())
66
+ return texts
67
+
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # Tokenizer wrappers
71
+ # ---------------------------------------------------------------------------
72
+ class OurConcatTokenizer:
73
+ """Wrapper for our concatenated tokenizers (HuggingFace tokenizers lib)."""
74
+ def __init__(self, ar_dir, az_dir):
75
+ from tokenizers import Tokenizer
76
+ self.tok_ar = Tokenizer.from_file(os.path.join(ar_dir, "tokenizer.json"))
77
+ self.tok_az = Tokenizer.from_file(os.path.join(az_dir, "tokenizer.json"))
78
+
79
+ def encode(self, text):
80
+ script = detect_script(text)
81
+ if script == "ar":
82
+ enc = self.tok_ar.encode(text)
83
+ else:
84
+ enc = self.tok_az.encode(text)
85
+ return enc.tokens, enc.ids
86
+
87
+ def decode(self, ids, script=None):
88
+ if script == "ar":
89
+ return self.tok_ar.decode(ids, skip_special_tokens=True)
90
+ else:
91
+ return self.tok_az.decode(ids, skip_special_tokens=True)
92
+
93
+
94
+ class HFTokenizer:
95
+ """Wrapper for HuggingFace transformers tokenizers."""
96
+ def __init__(self, repo_id):
97
+ from transformers import AutoTokenizer
98
+ self.tok = AutoTokenizer.from_pretrained(repo_id, trust_remote_code=True)
99
+
100
+ def encode(self, text):
101
+ ids = self.tok.encode(text, add_special_tokens=False)
102
+ tokens = self.tok.convert_ids_to_tokens(ids)
103
+ return tokens, ids
104
+
105
+ def decode(self, ids, script=None):
106
+ return self.tok.decode(ids, skip_special_tokens=True)
107
+
108
+
109
+ # ---------------------------------------------------------------------------
110
+ # Evaluation
111
+ # ---------------------------------------------------------------------------
112
+ @dataclass
113
+ class CompMetrics:
114
+ name: str = ""
115
+ source: str = ""
116
+ vocab_size: int = 0
117
+ fertility_ar: float = 0.0
118
+ fertility_az: float = 0.0
119
+ fertility_overall: float = 0.0
120
+ disparity: float = 0.0
121
+ cpt_ar: float = 0.0
122
+ cpt_az: float = 0.0
123
+ exact_match_ar: float = 0.0
124
+ exact_match_az: float = 0.0
125
+
126
+
127
+ def evaluate_tokenizer(tok, name, source, vocab_size, test_texts):
128
+ metrics = CompMetrics(name=name, source=source, vocab_size=vocab_size)
129
+
130
+ # Track per-script stats
131
+ ar_fert_list, az_fert_list = [], []
132
+ ar_cpt_list, az_cpt_list = [], []
133
+ ar_match, az_match = 0, 0
134
+ ar_total, az_total = 0, 0
135
+
136
+ all_texts = test_texts["ar"] + test_texts["az"] + test_texts["mi"]
137
+ all_fert = []
138
+ n_total = len(all_texts)
139
+
140
+ for i, text in enumerate(all_texts):
141
+ if (i + 1) % 5000 == 0:
142
+ print(f" [{i+1}/{n_total}] {name}", flush=True)
143
+ script = detect_script(text)
144
+ try:
145
+ tokens, ids = tok.encode(text)
146
+ filtered = [t for t in tokens if not t.startswith("[") and not t.startswith("<") and t not in ("[CLS]", "[SEP]", "[PAD]", "[UNK]", "<s>", "</s>", "<unk>", "<pad>")]
147
+
148
+ words = segment_words(text)
149
+ if len(words) == 0:
150
+ continue
151
+
152
+ fertility = len(filtered) / len(words)
153
+ all_fert.append(fertility)
154
+
155
+ try:
156
+ decoded = tok.decode(ids, script=script)
157
+ exact = decoded.strip() == text.strip()
158
+ except Exception:
159
+ exact = False
160
+
161
+ if script == "ar":
162
+ ar_fert_list.append(fertility)
163
+ ar_cpt_list.append(count_graphemes(text) / max(len(filtered), 1))
164
+ ar_total += 1
165
+ if exact:
166
+ ar_match += 1
167
+ else:
168
+ az_fert_list.append(fertility)
169
+ az_cpt_list.append(count_graphemes(text) / max(len(filtered), 1))
170
+ az_total += 1
171
+ if exact:
172
+ az_match += 1
173
+ except Exception as e:
174
+ pass
175
+
176
+ metrics.fertility_ar = float(np.mean(ar_fert_list)) if ar_fert_list else 0
177
+ metrics.fertility_az = float(np.mean(az_fert_list)) if az_fert_list else 0
178
+ metrics.fertility_overall = float(np.mean(all_fert)) if all_fert else 0
179
+ metrics.disparity = abs(metrics.fertility_ar - metrics.fertility_az) / max(metrics.fertility_ar, metrics.fertility_az, 1e-9)
180
+ metrics.cpt_ar = float(np.mean(ar_cpt_list)) if ar_cpt_list else 0
181
+ metrics.cpt_az = float(np.mean(az_cpt_list)) if az_cpt_list else 0
182
+ metrics.exact_match_ar = ar_match / max(ar_total, 1)
183
+ metrics.exact_match_az = az_match / max(az_total, 1)
184
+
185
+ return metrics
186
+
187
+
188
+ # ---------------------------------------------------------------------------
189
+ # Main
190
+ # ---------------------------------------------------------------------------
191
+ def main():
192
+ print("Loading test texts...")
193
+ test_texts = load_test_texts()
194
+ total = sum(len(v) for v in test_texts.values())
195
+ print(f" Total: {total} texts (ar={len(test_texts['ar'])}, az={len(test_texts['az'])}, mi={len(test_texts['mi'])})")
196
+
197
+ all_results = []
198
+
199
+ # --- Our tokenizers ---
200
+ ours = [
201
+ ("Ours: concat_bpe_8K", "ours", 8000,
202
+ os.path.join(TRANS_DIR, "concat_bpe_8000_tokenizer_ar"),
203
+ os.path.join(TRANS_DIR, "concat_bpe_8000_tokenizer_az")),
204
+ ("Ours: concat_wp_16K", "ours", 16000,
205
+ os.path.join(TRANS_DIR, "concat_wordpiece_16000_tokenizer_ar"),
206
+ os.path.join(TRANS_DIR, "concat_wordpiece_16000_tokenizer_az")),
207
+ ("Ours: concat_wp_32K", "ours", 32000,
208
+ os.path.join(TRANS_DIR, "concat_wordpiece_32000_tokenizer_ar"),
209
+ os.path.join(TRANS_DIR, "concat_wordpiece_32000_tokenizer_az")),
210
+ ]
211
+
212
+ for name, source, vsz, ar_dir, az_dir in ours:
213
+ if os.path.exists(ar_dir) and os.path.exists(az_dir):
214
+ print(f"\nEvaluating {name}...")
215
+ t0 = time.perf_counter()
216
+ tok = OurConcatTokenizer(ar_dir, az_dir)
217
+ m = evaluate_tokenizer(tok, name, source, vsz, test_texts)
218
+ print(f" [{time.perf_counter()-t0:.1f}s] Fert={m.fertility_overall:.3f} Disp={m.disparity:.3f} EM_ar={m.exact_match_ar:.2%} EM_az={m.exact_match_az:.2%}")
219
+ all_results.append(m)
220
+ else:
221
+ print(f"\nSKIP {name} (missing: {ar_dir} or {az_dir})")
222
+
223
+ # --- External tokenizers ---
224
+ externals = [
225
+ ("CaMeLBERT-MSA (30K WP)", "external_msa", 30000, "CAMeL-Lab/bert-base-arabic-camelbert-msa"),
226
+ ("Asafaya-BERT (32K WP)", "external_msa", 32000, "asafaya/bert-base-arabic"),
227
+ ("Aranizer (86K SP)", "external_msa", 86000, "riotu-lab/Aranizer-SP-86k"),
228
+ ("DarijaBERT-ar (80K WP)", "external_darija", 80000, "SI2M-Lab/DarijaBERT"),
229
+ ("DarijaBERT-az (110K WP)", "external_darija", 110000, "SI2M-Lab/DarijaBERT-arabizi"),
230
+ ]
231
+
232
+ for name, source, vsz, repo in externals:
233
+ print(f"\nEvaluating {name} ({repo})...")
234
+ try:
235
+ t0 = time.perf_counter()
236
+ tok = HFTokenizer(repo)
237
+ m = evaluate_tokenizer(tok, name, source, vsz, test_texts)
238
+ print(f" [{time.perf_counter()-t0:.1f}s] Fert={m.fertility_overall:.3f} Disp={m.disparity:.3f} EM_ar={m.exact_match_ar:.2%} EM_az={m.exact_match_az:.2%}")
239
+ all_results.append(m)
240
+ except Exception as e:
241
+ print(f" FAILED: {e}")
242
+
243
+ # --- Save results ---
244
+ out_csv = os.path.join(RESULTS, "external_comparison.csv")
245
+ out_json = os.path.join(RESULTS, "external_comparison.json")
246
+
247
+ import csv
248
+ with open(out_csv, "w", newline="", encoding="utf-8") as f:
249
+ w = csv.DictWriter(f, fieldnames=[k for k in asdict(all_results[0]).keys()])
250
+ w.writeheader()
251
+ for m in all_results:
252
+ w.writerow(asdict(m))
253
+
254
+ with open(out_json, "w", encoding="utf-8") as f:
255
+ json.dump([asdict(m) for m in all_results], f, indent=2)
256
+
257
+ print(f"\nResults saved: {out_csv}, {out_json}")
258
+
259
+ # --- Print summary table ---
260
+ print("\n" + "=" * 120)
261
+ print(f"{'Name':<30} {'Source':<16} {'V':>6} {'Fert':>7} {'F_ar':>7} {'F_az':>7} {'Disp':>7} {'CPT_ar':>7} {'CPT_az':>7} {'EM_ar':>7} {'EM_az':>7}")
262
+ print("-" * 120)
263
+ for m in sorted(all_results, key=lambda x: (x.source, x.vocab_size)):
264
+ print(f"{m.name:<30} {m.source:<16} {m.vocab_size:>6,} {m.fertility_overall:>7.3f} {m.fertility_ar:>7.3f} {m.fertility_az:>7.3f} {m.disparity:>7.3f} {m.cpt_ar:>7.3f} {m.cpt_az:>7.3f} {m.exact_match_ar:>7.2%} {m.exact_match_az:>7.2%}")
265
+ print("=" * 120)
266
+
267
+
268
+ if __name__ == "__main__":
269
+ main()