| |
| """ |
| Two things: |
| 1. Tokenizer fertility analysis across all 4 languages + English baseline |
| 2. Add new lang tokens for Tulu (tcy_Knda) and Gondi (gon_Deva) |
| initialized from their proxy token embeddings (kan_Knda, hin_Deva) |
| |
| Run this BEFORE nllb_pretrain.py. |
| Saves extended tokenizer to ./nllb-extended-tokenizer/ |
| """ |
|
|
| import os |
| import random |
| import torch |
| import numpy as np |
| import pandas as pd |
| from datasets import load_dataset |
| from transformers import AutoTokenizer |
| from transformers.models.m2m_100 import M2M100ForConditionalGeneration |
|
|
| MODEL_ID = "facebook/nllb-200-1.3B" |
| MONO_DIR = "../../Dataset/Monolingual" |
| OUTPUT_DIR = "../nllb-extended-tokenizer" |
| SAMPLE_SIZE = 1_000 |
| RANDOM_SEED = 42 |
|
|
| random.seed(RANDOM_SEED) |
| os.makedirs(OUTPUT_DIR, exist_ok=True) |
|
|
| |
| |
| |
| LANGUAGES = [ |
| { |
| "name": "Mizo", |
| "file": "mizo_mono_cpt_train.tsv", |
| "existing_token": "lus_Latn", |
| "new_token": None, |
| }, |
| { |
| "name": "Khasi", |
| "file": "khasi_mono_cpt_train.tsv", |
| "existing_token": "kha_Latn", |
| "new_token": None, |
| }, |
| { |
| "name": "Kannada (proxy for Tulu)", |
| "file": "kannada_mono_cpt_train.tsv", |
| "existing_token": "kan_Knda", |
| "new_token": None, |
| }, |
| { |
| "name": "Tulu", |
| "file": None, |
| "existing_token": "kan_Knda", |
| "new_token": "tcy_Knda", |
| }, |
| { |
| "name": "Hindi (proxy for Gondi)", |
| "file": "hindi_mono_cpt_train.tsv", |
| "existing_token": "hin_Deva", |
| "new_token": None, |
| }, |
| { |
| "name": "Gondi", |
| "file": None, |
| "existing_token": "hin_Deva", |
| "new_token": "gon_Deva", |
| }, |
| ] |
|
|
| |
| ENGLISH_SAMPLE = [ |
| "The government announced new policies for education reform.", |
| "She walked to the market every morning to buy fresh vegetables.", |
| "The river flooded several villages in the northern district.", |
| "Children played in the courtyard while their parents worked.", |
| "The doctor advised him to rest for at least a week.", |
| ] |
|
|
| |
| print(f"Loading tokenizer: {MODEL_ID}") |
| tokenizer = AutoTokenizer.from_pretrained(MODEL_ID) |
| print(f"Vocab size before extension: {tokenizer.vocab_size:,}") |
|
|
| |
| def compute_fertility(texts, lang_token, tokenizer, sample_size=SAMPLE_SIZE): |
| """ |
| Fertility = avg tokens per word. |
| Lower is better β 1.0 = perfect (one token per word). |
| Also reports % unknown tokens (shown as <unk>). |
| """ |
| if not texts: |
| return {"fertility": None, "unk_rate": None, "n": 0} |
|
|
| sample = random.sample(texts, min(sample_size, len(texts))) |
| tokenizer.src_lang = lang_token |
|
|
| fertilities = [] |
| unk_counts = [] |
| total_toks = [] |
|
|
| for text in sample: |
| words = text.split() |
| if not words: |
| continue |
| token_ids = tokenizer.encode(text, add_special_tokens=False) |
| n_toks = len(token_ids) |
| n_words = len(words) |
| n_unk = token_ids.count(tokenizer.unk_token_id) |
|
|
| fertilities.append(n_toks / n_words) |
| unk_counts.append(n_unk) |
| total_toks.append(n_toks) |
|
|
| return { |
| "fertility": round(np.mean(fertilities), 3), |
| "fertility_std": round(np.std(fertilities), 3), |
| "unk_rate": round(sum(unk_counts) / max(sum(total_toks), 1) * 100, 2), |
| "n": len(fertilities), |
| } |
|
|
|
|
| def load_sample(filepath): |
| if filepath is None or not os.path.exists(filepath): |
| return [] |
| df = pd.read_csv(filepath, sep='\t', header=None, names=['text'], |
| on_bad_lines='skip', quoting=3) |
| return df['text'].dropna().str.strip().tolist() |
|
|
|
|
| print("\n" + "="*65) |
| print(" TOKENIZER FERTILITY ANALYSIS") |
| print("="*65) |
| print(f" Model : {MODEL_ID}") |
| print(f" Sample : {SAMPLE_SIZE} sentences per language") |
| print("="*65) |
|
|
| fertility_rows = [] |
|
|
| |
| tokenizer.src_lang = "eng_Latn" |
| eng_fertilities = [] |
| for text in ENGLISH_SAMPLE * 20: |
| ids = tokenizer.encode(text, add_special_tokens=False) |
| eng_fertilities.append(len(ids) / len(text.split())) |
|
|
| eng_fertility = round(np.mean(eng_fertilities), 3) |
| print(f"\n {'Language':<35} {'Token':>10} {'Fertility':>10} {'Std':>6} {'UNK%':>7} {'N':>6}") |
| print(f" {'-'*35} {'-'*10} {'-'*10} {'-'*6} {'-'*7} {'-'*6}") |
| print(f" {'English (baseline)':<35} {'eng_Latn':>10} {eng_fertility:>10.3f} {'β':>6} {'β':>7} {'~100':>6}") |
|
|
| fertility_rows.append({ |
| "Language": "English (baseline)", |
| "NLLB Token": "eng_Latn", |
| "Fertility": eng_fertility, |
| "Fertility Std": "β", |
| "UNK%": "β", |
| }) |
|
|
| for lang in LANGUAGES: |
| if lang["file"] is None: |
| |
| print(f" {lang['name']:<35} {lang['existing_token']:>10} {'N/A (no data)':>10}") |
| fertility_rows.append({ |
| "Language": lang["name"], |
| "NLLB Token": lang["existing_token"], |
| "Fertility": "N/A", |
| "Fertility Std": "N/A", |
| "UNK%": "N/A", |
| }) |
| continue |
|
|
| filepath = os.path.join(MONO_DIR, lang["file"]) |
| texts = load_sample(filepath) |
|
|
| if not texts: |
| print(f" {lang['name']:<35} FILE NOT FOUND: {filepath}") |
| continue |
|
|
| stats = compute_fertility(texts, lang["existing_token"], tokenizer) |
| flag = " β HIGH" if stats["fertility"] and stats["fertility"] > 3.5 else "" |
|
|
| print(f" {lang['name']:<35} {lang['existing_token']:>10} " |
| f"{stats['fertility']:>10.3f} {stats['fertility_std']:>6.3f} " |
| f"{stats['unk_rate']:>7.2f}% {stats['n']:>6}{flag}") |
|
|
| fertility_rows.append({ |
| "Language": lang["name"], |
| "NLLB Token": lang["existing_token"], |
| "Fertility": stats["fertility"], |
| "Fertility Std": stats["fertility_std"], |
| "UNK%": stats["unk_rate"], |
| }) |
|
|
| print("\n Fertility interpretation:") |
| print(" < 1.5 β Excellent | 1.5-2.5 β Good | 2.5-3.5 β Poor | > 3.5 β Very poor") |
| print(" UNK% > 5% β tokenizer has no subwords for this language\n") |
|
|
| |
| fertility_df = pd.DataFrame(fertility_rows) |
| fertility_path = os.path.join(OUTPUT_DIR, "fertility_report.tsv") |
| fertility_df.to_csv(fertility_path, sep='\t', index=False) |
| print(f" Fertility report saved β {fertility_path}") |
|
|
| |
| print("\n" + "="*65) |
| print(" ADDING NEW LANG TOKENS: tcy_Knda (Tulu), gon_Deva (Gondi)") |
| print("="*65) |
|
|
| NEW_TOKENS = [ |
| {"new": "tcy_Knda", "proxy": "kan_Knda"}, |
| {"new": "gon_Deva", "proxy": "hin_Deva"}, |
| ] |
|
|
| |
| tokens_to_add = [] |
| for t in NEW_TOKENS: |
| if t["new"] in tokenizer.all_special_tokens: |
| print(f" {t['new']} already exists β skipping") |
| else: |
| tokens_to_add.append(t["new"]) |
| print(f" Will add: {t['new']} (init from {t['proxy']})") |
|
|
| if tokens_to_add: |
| |
| tokenizer.add_special_tokens({"additional_special_tokens": tokens_to_add}) |
| print(f"\n Vocab size after extension: {len(tokenizer):,} " |
| f"(added {len(tokens_to_add)} tokens)") |
|
|
| |
| print("\n Loading model to extend embedding layer...") |
| model = M2M100ForConditionalGeneration.from_pretrained(MODEL_ID, dtype=torch.float32) |
| model.resize_token_embeddings(len(tokenizer)) |
|
|
| |
| with torch.no_grad(): |
| embed = model.model.shared.weight |
|
|
| for t in NEW_TOKENS: |
| if t["new"] not in tokens_to_add: |
| continue |
| new_id = tokenizer.convert_tokens_to_ids(t["new"]) |
| proxy_id = tokenizer.convert_tokens_to_ids(t["proxy"]) |
|
|
| |
| proxy_emb = embed[proxy_id].clone() |
| noise = torch.randn_like(proxy_emb) * 0.01 |
| embed[new_id] = proxy_emb + noise |
|
|
| print(f" Initialized {t['new']} (id={new_id}) " |
| f"from {t['proxy']} (id={proxy_id})") |
|
|
| |
| |
| tokenizer.save_pretrained(OUTPUT_DIR) |
| print(f"\n Extended tokenizer saved β {OUTPUT_DIR}/") |
| print(f" NOTE: Update nllb_pretrain.py to load from '{OUTPUT_DIR}'") |
| print(f" and call model.resize_token_embeddings(len(tokenizer))") |
|
|
| else: |
| tokenizer.save_pretrained(OUTPUT_DIR) |
| print(" No new tokens needed. Tokenizer saved as-is.") |
|
|
| |
| print("\n" + "="*65) |
| print(" VERIFICATION") |
| print("="*65) |
| for t in NEW_TOKENS: |
| tid = tokenizer.convert_tokens_to_ids(t["new"]) |
| pid = tokenizer.convert_tokens_to_ids(t["proxy"]) |
| print(f" {t['new']:15s} β id {tid:6d} | " |
| f"proxy {t['proxy']:15s} β id {pid:6d}") |
|
|
| print("\nDone. Run nllb_pretrain.py next.") |