{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":121150,"databundleVersionId":15833391,"isSourceIdPinned":false},{"sourceType":"datasetVersion","sourceId":14962950,"datasetId":9577356,"databundleVersionId":15834364},{"sourceType":"datasetVersion","sourceId":6932794,"datasetId":3980837,"databundleVersionId":7019165},{"sourceType":"datasetVersion","sourceId":14899351,"datasetId":9366631,"databundleVersionId":15763950},{"sourceType":"datasetVersion","sourceId":11948763,"datasetId":7130272,"databundleVersionId":12459644},{"sourceType":"datasetVersion","sourceId":14852218,"datasetId":9499879,"databundleVersionId":15712580},{"sourceType":"datasetVersion","sourceId":14917701,"datasetId":9545094,"databundleVersionId":15783986}],"dockerImageVersionId":31287,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import kagglehub\n\n# Download latest version\nkagglehub.dataset_download(\"fcbk95/evaluate-0-4-1\")\nkagglehub.dataset_download(\"junaid512/sentencepiece-wheel\")\nkagglehub.dataset_download(\"kdallash/sacrebleu\")\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-03-06T18:35:45.477937Z","iopub.execute_input":"2026-03-06T18:35:45.478447Z","iopub.status.idle":"2026-03-06T18:35:46.711221Z","shell.execute_reply.started":"2026-03-06T18:35:45.478421Z","shell.execute_reply":"2026-03-06T18:35:46.710621Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# !pip install -q transformers datasets optimum tqdm\n!pip install /kaggle/input/datasets/fcbk95/evaluate-0-4-1/evaluate/responses-0.18.0-py3-none-any.whl\n!pip install /kaggle/input/datasets/fcbk95/evaluate-0-4-1/evaluate/evaluate-0.4.1-py3-none-any.whl\n!pip install /kaggle/input/datasets/kdallash/sacrebleu/mmc/portalocker-3.2.0-py3-none-any.whl\n!pip install /kaggle/input/datasets/junaid512/sentencepiece-wheel/sentencepiece-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl\n!pip install /kaggle/input/datasets/kdallash/sacrebleu/mmc/sacrebleu-2.6.0-py3-none-any.whl\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-06T18:35:46.712348Z","iopub.execute_input":"2026-03-06T18:35:46.712807Z","iopub.status.idle":"2026-03-06T18:36:03.491372Z","shell.execute_reply.started":"2026-03-06T18:35:46.712782Z","shell.execute_reply":"2026-03-06T18:36:03.490478Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\"\"\"\nDeep Past Challenge - Optimized End-to-End Pipeline\nTranslating Old Assyrian cuneiform transliterations to English\nMetric: Geometric Mean of BLEU and chrF++ (micro-averaged)\n\nOPTIMIZATIONS FOR T4 x2 (32GB total VRAM):\n  - expandable_segments memory allocator\n  - gradient_checkpointing + adafactor optimizer\n  - Dynamic padding via DataCollatorForSeq2Seq (no wasteful fixed-length padding)\n  - Sequence length bucketing to minimize padding overhead\n  - Reduced beam search during training eval (num_beams=2), full beams at inference\n  - torch.backends.cuda.matmul.allow_tf32 for faster matmuls on Ampere+ GPUs\n  - Empty cache before training\n  - Multi-GPU verified via device_count check\n\"\"\"\n\n# ─────────────────────────────────────────────\n# SECTION 0: Memory config — MUST be before torch import\n# ─────────────────────────────────────────────\nimport os\nos.environ[\"PYTORCH_CUDA_ALLOC_CONF\"] = \"expandable_segments:True\"\n\nimport re, math, random\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom typing import Optional\n\nimport torch\nimport torch.backends.cuda\n\n# Allow TF32 for faster matmuls (safe for training, negligible precision loss)\ntorch.backends.cuda.matmul.allow_tf32 = True\ntorch.backends.cudnn.allow_tf32 = True\n\nfrom transformers import (\n    AutoTokenizer,\n    AutoModelForSeq2SeqLM,\n    Seq2SeqTrainer,\n    Seq2SeqTrainingArguments,\n    DataCollatorForSeq2Seq,\n    EarlyStoppingCallback,\n)\nfrom datasets import Dataset as HFDataset\nimport sacrebleu\n\n# ─────────────────────────────────────────────\n# SECTION 1: Configuration\n# ─────────────────────────────────────────────\n\nCFG = {\n    # Data paths\n    \"train_csv\":             \"/kaggle/input/competitions/deep-past-initiative-machine-translation/train.csv\",\n    \"test_csv\":              \"/kaggle/input/competitions/deep-past-initiative-machine-translation/test.csv\",\n    \"published_texts_csv\":   \"\",\n    \"publications_csv\":      \"\",\n    \"lexicon_csv\":           \"\",\n    \"sentences_csv\":         \"\",\n    \"output_dir\":            \"outputs\",\n    \"submission_path\":       \"submission.csv\",\n\n    \"model_name\":            \"/kaggle/input/datasets/zorojuro358/byt5-small/byt5-small\",\n\n    # Training — tuned for T4 x2\n    # ByT5 tokenizes byte-by-byte: a 100-char string = ~100 tokens.\n    # Keep lengths tight to avoid O(n²) attention blowup.\n    \"max_src_len\":           128,\n    \"max_tgt_len\":           128,\n    \"train_batch_size\":      8,      # per device → 16 effective across 2 GPUs\n    \"eval_batch_size\":       16,     # per device, eval is inference-only (no grads)\n    \"gradient_accumulation\": 4,      # effective batch = 8*2*4 = 64\n    \"num_epochs\":            20,\n    \"learning_rate\":         5e-4,   # adafactor works best with higher LR than adamw\n    \"warmup_ratio\":          0.1,\n    \"weight_decay\":          0.01,\n    \"fp16\":                  True,\n    \"seed\":                  42,\n    \"val_frac\":              0.1,\n\n    # Augmentation\n    \"use_supplemental\":      True,\n    \"max_supplemental\":      5000,\n\n    # Inference (full quality — no memory pressure at inference time)\n    \"num_beams\":             5,\n    \"length_penalty\":        1.0,\n    \"no_repeat_ngram_size\":  3,\n}\n\nrandom.seed(CFG[\"seed\"])\nnp.random.seed(CFG[\"seed\"])\ntorch.manual_seed(CFG[\"seed\"])\nos.makedirs(CFG[\"output_dir\"], exist_ok=True)\n\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nNUM_GPUS = torch.cuda.device_count()\nprint(f\"Using device: {DEVICE} | GPUs available: {NUM_GPUS}\")\nfor i in range(NUM_GPUS):\n    print(f\"  GPU {i}: {torch.cuda.get_device_name(i)} \"\n          f\"({torch.cuda.get_device_properties(i).total_memory / 1e9:.1f} GB)\")\n\n\n# ─────────────────────────────────────────────\n# SECTION 2: Data Loading & Preprocessing\n# ─────────────────────────────────────────────\n\ndef clean_transliteration(text: str) -> str:\n    if not isinstance(text, str):\n        return \"\"\n    text = re.sub(r\"^\\d+'*\\.\\s*\", \"\", text, flags=re.MULTILINE)\n    text = text.replace(\"\\n\", \" \")\n    text = re.sub(r\"\\b[xX]\\b\", \"\", text)\n    text = re.sub(r\"\\.{3}\", \"\", text)\n    text = re.sub(r\"\\[([^\\]]*)\\]\", r\"\\1\", text)\n    text = re.sub(r\"<([^>]*)>\", r\"\\1\", text)\n    text = re.sub(r\"\\(([^)]*)\\)\", r\"\\1\", text)\n    text = re.sub(r\"[{}|!?]\", \" \", text)\n    text = re.sub(r\"\\s+\", \" \", text).strip()\n    return text.lower()\n\n\ndef clean_translation(text: str) -> str:\n    if not isinstance(text, str):\n        return \"\"\n    text = text.replace(\"\\n\", \" \")\n    text = re.sub(r\"\\s+\", \" \", text).strip()\n    return text\n\n\ndef load_train_data() -> pd.DataFrame:\n    df = pd.read_csv(CFG[\"train_csv\"])\n    df[\"src\"] = df[\"transliteration\"].apply(clean_transliteration)\n    df[\"tgt\"] = df[\"translation\"].apply(clean_translation)\n    df = df[df[\"src\"].str.len() > 3]\n    df = df[df[\"tgt\"].str.len() > 3]\n    print(f\"Loaded {len(df)} training examples from train.csv\")\n    return df[[\"src\", \"tgt\"]]\n\n\ndef load_test_data() -> pd.DataFrame:\n    df = pd.read_csv(CFG[\"test_csv\"])\n    df[\"src\"] = df[\"transliteration\"].apply(clean_transliteration)\n    print(f\"Loaded {len(df)} test examples\")\n    return df\n\n\ndef check_sequence_lengths(df: pd.DataFrame, tokenizer, n: int = 500):\n    \"\"\"Inspect actual token lengths to validate max_src_len choice.\"\"\"\n    sample = df[\"src\"].head(n).tolist()\n    lengths = [len(tokenizer(s)[\"input_ids\"]) for s in sample]\n    print(f\"\\nSrc token lengths (n={n}):\")\n    print(f\"  mean={np.mean(lengths):.0f}, median={np.median(lengths):.0f}, \"\n          f\"p95={np.percentile(lengths, 95):.0f}, max={max(lengths)}\")\n    coverage = sum(1 for l in lengths if l <= CFG[\"max_src_len\"]) / len(lengths)\n    print(f\"  Coverage at max_src_len={CFG['max_src_len']}: {coverage:.1%}\")\n\n\n# ─────────────────────────────────────────────\n# SECTION 3: Supplemental Data Mining\n# ─────────────────────────────────────────────\n\ndef extract_supplemental_data(max_examples: int = 5000) -> pd.DataFrame:\n    print(\"Mining supplemental data from publications...\")\n    try:\n        pub_texts = pd.read_csv(CFG[\"published_texts_csv\"])\n        pubs = pd.read_csv(CFG[\"publications_csv\"])\n    except FileNotFoundError as e:\n        print(f\"  Supplemental file not found: {e}. Skipping.\")\n        return pd.DataFrame(columns=[\"src\", \"tgt\"])\n\n    pubs = pubs[pubs[\"has_akkadian\"] == True].copy()\n    pubs = pubs.dropna(subset=[\"page_text\"])\n\n    pairs = []\n    akkadian_char_pattern = re.compile(\n        r\"[šṭṣḫŠṬṢḪ₁₂₃₄₅₆₇₈₉₀]|\"\n        r\"\\b(um-ma|a-na|ki-ma|ša|šum-ma|i-na|li-ib-bi)\\b\"\n    )\n\n    for _, row in pubs.iterrows():\n        page = str(row[\"page_text\"])\n        lines = [l.strip() for l in page.split(\"\\n\") if l.strip()]\n        i = 0\n        while i < len(lines) - 1:\n            line, next_line = lines[i], lines[i + 1]\n            is_akk = bool(akkadian_char_pattern.search(line)) and len(line) < 200\n            is_eng = bool(re.match(r\"^[A-Z\\(\\\"\\']\", next_line)) and len(next_line) < 300\n            has_hyphens = line.count(\"-\") >= 2\n            if is_akk and has_hyphens and is_eng:\n                src = clean_transliteration(line)\n                tgt = clean_translation(next_line)\n                if len(src) > 5 and len(tgt) > 5:\n                    pairs.append({\"src\": src, \"tgt\": tgt})\n                i += 2\n            else:\n                i += 1\n        if len(pairs) >= max_examples:\n            break\n\n    df = pd.DataFrame(pairs).drop_duplicates(subset=[\"src\"])\n    print(f\"  Extracted {len(df)} supplemental pairs from publications\")\n    return df.head(max_examples)\n\n\n# ─────────────────────────────────────────────\n# SECTION 4: Lexicon Prefix\n# ─────────────────────────────────────────────\n\ndef build_lexicon_lookup() -> dict:\n    try:\n        lex = pd.read_csv(CFG[\"lexicon_csv\"])\n    except FileNotFoundError:\n        return {}\n    lookup = {}\n    for _, row in lex.iterrows():\n        form = str(row.get(\"form\", \"\")).strip().lower()\n        lexeme = str(row.get(\"lexeme\", \"\")).strip()\n        if form and lexeme and lexeme != \"nan\":\n            lookup[form] = lexeme\n    print(f\"Lexicon loaded: {len(lookup)} entries\")\n    return lookup\n\n\nLEXICON = {}\n\n\ndef add_lexicon_prefix(src: str, lexicon: dict, max_hints: int = 5) -> str:\n    if not lexicon:\n        return src\n    tokens = src.split()\n    hints, seen = [], set()\n    for tok in tokens:\n        tok_clean = tok.strip(\".,;:!?\")\n        if tok_clean in lexicon and tok_clean not in seen:\n            hints.append(f\"{tok_clean}={lexicon[tok_clean]}\")\n            seen.add(tok_clean)\n        if len(hints) >= max_hints:\n            break\n    return (\"GLOSSES: \" + \", \".join(hints) + \" | \" + src) if hints else src\n\n\n# ─────────────────────────────────────────────\n# SECTION 5: Dataset & Tokenization\n# ─────────────────────────────────────────────\n\ndef make_hf_dataset(df: pd.DataFrame, tokenizer, use_lexicon: bool = True):\n    sources = df[\"src\"].tolist()\n    targets = df[\"tgt\"].tolist()\n\n    if use_lexicon and LEXICON:\n        sources = [add_lexicon_prefix(s, LEXICON) for s in sources]\n\n    def tokenize(batch):\n        model_inputs = tokenizer(\n            batch[\"src\"],\n            max_length=CFG[\"max_src_len\"],\n            truncation=True,\n            padding=False,          # DataCollator handles padding dynamically\n        )\n        labels = tokenizer(\n            text_target=batch[\"tgt\"],\n            max_length=CFG[\"max_tgt_len\"],\n            truncation=True,\n            padding=False,\n        )\n        model_inputs[\"labels\"] = labels[\"input_ids\"]\n        return model_inputs\n\n    raw = HFDataset.from_dict({\"src\": sources, \"tgt\": targets})\n    tokenized = raw.map(\n        tokenize,\n        batched=True,\n        batch_size=256,\n        remove_columns=[\"src\", \"tgt\"],\n        desc=\"Tokenizing\",\n    )\n    # Sort by src length for bucketing — reduces padding waste per batch\n    tokenized = tokenized.map(\n        lambda x: {\"length\": len(x[\"input_ids\"])},\n        desc=\"Computing lengths\",\n    )\n    tokenized = tokenized.sort(\"length\")\n    tokenized = tokenized.remove_columns([\"length\"])\n    return tokenized\n\n\n# ─────────────────────────────────────────────\n# SECTION 6: Evaluation Metric\n# ─────────────────────────────────────────────\n\ndef compute_metrics_fn(tokenizer):\n    def compute_metrics(eval_preds):\n        preds, labels = eval_preds\n        if isinstance(preds, tuple):\n            preds = preds[0]\n\n        # Handle logits (3D) vs token IDs (2D)\n        if preds.ndim == 3:\n            preds = np.argmax(preds, axis=-1)\n\n        # Safety clip for ByT5's chr()-based decoding\n        preds = np.clip(preds, 0, tokenizer.vocab_size - 1)\n\n        labels = np.where(labels != -100, labels, tokenizer.pad_token_id)\n\n        decoded_preds = tokenizer.batch_decode(preds, skip_special_tokens=True)\n        decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True)\n\n        refs = [[l] for l in decoded_labels]\n        bleu = sacrebleu.corpus_bleu(decoded_preds, list(zip(*refs))).score\n        chrf = sacrebleu.corpus_chrf(\n            decoded_preds, list(zip(*refs)), word_order=2\n        ).score\n        geomean = math.sqrt(bleu * chrf) if bleu > 0 and chrf > 0 else 0.0\n\n        return {\"bleu\": bleu, \"chrf++\": chrf, \"geomean\": geomean}\n\n    return compute_metrics\n\n\n# ─────────────────────────────────────────────\n# SECTION 7: Training\n# ─────────────────────────────────────────────\n\ndef train(train_df: pd.DataFrame):\n    print(\"\\n=== Loading model & tokenizer ===\")\n    tokenizer = AutoTokenizer.from_pretrained(CFG[\"model_name\"])\n    model = AutoModelForSeq2SeqLM.from_pretrained(CFG[\"model_name\"])\n\n    # Silence tied-weights warning for ByT5\n    model.config.tie_word_embeddings = False\n    model = model.to(DEVICE)\n\n    # Inspect sequence lengths before training\n    check_sequence_lengths(train_df, tokenizer)\n\n    tokenizer.save_pretrained(os.path.join(CFG[\"output_dir\"], \"tokenizer\"))\n\n    val_size = max(50, int(len(train_df) * CFG[\"val_frac\"]))\n    val_df = train_df.sample(n=val_size, random_state=CFG[\"seed\"])\n    tr_df = train_df.drop(val_df.index)\n    print(f\"Train: {len(tr_df)} | Val: {len(val_df)}\")\n\n    train_ds = make_hf_dataset(tr_df, tokenizer)\n    val_ds = make_hf_dataset(val_df, tokenizer)\n\n    # Dynamic padding — only pads to the longest sequence IN each batch\n    data_collator = DataCollatorForSeq2Seq(\n        tokenizer,\n        model=model,\n        label_pad_token_id=-100,\n        pad_to_multiple_of=8,       # keeps tensor dims GPU-friendly\n    )\n\n    training_args = Seq2SeqTrainingArguments(\n        output_dir=CFG[\"output_dir\"],\n        eval_strategy=\"epoch\",\n        save_strategy=\"epoch\",\n        load_best_model_at_end=True,\n        metric_for_best_model=\"geomean\",\n        greater_is_better=True,\n        num_train_epochs=CFG[\"num_epochs\"],\n        per_device_train_batch_size=CFG[\"train_batch_size\"],\n        per_device_eval_batch_size=CFG[\"eval_batch_size\"],\n        gradient_accumulation_steps=CFG[\"gradient_accumulation\"],\n        learning_rate=CFG[\"learning_rate\"],\n        warmup_ratio=CFG[\"warmup_ratio\"],\n        weight_decay=CFG[\"weight_decay\"],\n        fp16=CFG[\"fp16\"] and DEVICE == \"cuda\",\n        predict_with_generate=True,\n        generation_max_length=CFG[\"max_tgt_len\"],\n        generation_num_beams=2,         # fast during training eval; full beams at inference\n        logging_steps=50,\n        save_total_limit=2,\n        seed=CFG[\"seed\"],\n        report_to=\"none\",\n        # ── Memory optimizations ──────────────────────────\n        gradient_checkpointing=True,    # recompute activations on backward pass\n        optim=\"adafactor\",              # ~4x less memory than AdamW (no moment tensors)\n        dataloader_pin_memory=False,    # reduces CPU↔GPU transfer overhead on shared RAM\n        dataloader_num_workers=2,       # parallel data loading\n        ddp_find_unused_parameters=False,  # faster multi-GPU DDP\n    )\n\n    # Clear VRAM before training starts\n    torch.cuda.empty_cache()\n\n    trainer = Seq2SeqTrainer(\n        model=model,\n        args=training_args,\n        train_dataset=train_ds,\n        eval_dataset=val_ds,\n        processing_class=tokenizer,\n        data_collator=data_collator,\n        compute_metrics=compute_metrics_fn(tokenizer),\n        callbacks=[EarlyStoppingCallback(early_stopping_patience=3)],\n    )\n\n    print(\"\\n=== Training ===\")\n    trainer.train()\n    trainer.save_model(os.path.join(CFG[\"output_dir\"], \"best_model\"))\n    print(f\"Best model saved to {CFG['output_dir']}/best_model\")\n    return trainer, tokenizer\n\n\n# ─────────────────────────────────────────────\n# SECTION 8: Inference\n# ─────────────────────────────────────────────\n\ndef generate_translations(\n    test_df: pd.DataFrame,\n    model_path: str,\n    tokenizer_path: Optional[str] = None,\n    batch_size: int = 32,\n) -> list[str]:\n    tok_path = tokenizer_path or model_path\n    tokenizer = AutoTokenizer.from_pretrained(tok_path)\n    model = AutoModelForSeq2SeqLM.from_pretrained(model_path).to(DEVICE)\n    model.eval()\n\n    sources = test_df[\"src\"].tolist()\n    if LEXICON:\n        sources = [add_lexicon_prefix(s, LEXICON) for s in sources]\n\n    translations = []\n    print(f\"\\n=== Generating translations for {len(sources)} examples ===\")\n\n    for i in range(0, len(sources), batch_size):\n        batch = sources[i: i + batch_size]\n        inputs = tokenizer(\n            batch,\n            return_tensors=\"pt\",\n            padding=True,\n            truncation=True,\n            max_length=CFG[\"max_src_len\"],\n        ).to(DEVICE)\n\n        with torch.no_grad():\n            out = model.generate(\n                **inputs,\n                num_beams=CFG[\"num_beams\"],         # full beams at inference\n                length_penalty=CFG[\"length_penalty\"],\n                no_repeat_ngram_size=CFG[\"no_repeat_ngram_size\"],\n                max_new_tokens=CFG[\"max_tgt_len\"],\n                early_stopping=True,\n            )\n\n        translations.extend(tokenizer.batch_decode(out, skip_special_tokens=True))\n        if (i // batch_size) % 10 == 0:\n            print(f\"  {i}/{len(sources)} done\")\n\n    return translations\n\n\n# ─────────────────────────────────────────────\n# SECTION 9: Submission\n# ─────────────────────────────────────────────\n\ndef make_submission(test_df: pd.DataFrame, translations: list[str]):\n    submission = pd.DataFrame({\n        \"id\": test_df[\"id\"].values,\n        \"translation\": translations,\n    })\n    submission.to_csv(CFG[\"submission_path\"], index=False)\n    print(f\"\\nSubmission saved to {CFG['submission_path']}\")\n    print(submission.head(3).to_string())\n    return submission\n\n\n# ─────────────────────────────────────────────\n# SECTION 10: Post-processing\n# ─────────────────────────────────────────────\n\ndef postprocess_translation(text: str) -> str:\n    text = text.strip()\n    if text:\n        text = text[0].upper() + text[1:]\n    # Remove beam-search repetition artifacts\n    words = text.split()\n    if len(words) > 10:\n        half = len(words) // 2\n        if words[:half] == words[half: half + half]:\n            text = \" \".join(words[:half])\n    return text\n\n\n# ─────────────────────────────────────────────\n# SECTION 11: Main Pipeline\n# ─────────────────────────────────────────────\n\ndef main():\n    global LEXICON\n\n    LEXICON = build_lexicon_lookup()\n    train_df = load_train_data()\n\n    if CFG[\"use_supplemental\"]:\n        supp_df = extract_supplemental_data(max_examples=CFG[\"max_supplemental\"])\n        if len(supp_df) > 0:\n            train_df = pd.concat([train_df, supp_df], ignore_index=True)\n            train_df = train_df.drop_duplicates(subset=[\"src\"])\n            print(f\"Combined training set: {len(train_df)} examples\")\n\n    test_df = load_test_data()\n    trainer, tokenizer = train(train_df)\n\n    model_path = os.path.join(CFG[\"output_dir\"], \"best_model\")\n    translations_raw = generate_translations(test_df, model_path)\n    translations = [postprocess_translation(t) for t in translations_raw]\n    make_submission(test_df, translations)\n    print(\"\\n=== Pipeline complete! ===\")\n\n\n# ─────────────────────────────────────────────\n# SECTION 12: Sanity check on train sample\n# ─────────────────────────────────────────────\n\ndef evaluate_on_train_sample(n: int = 100):\n    global LEXICON\n    LEXICON = build_lexicon_lookup()\n\n    train_df = pd.read_csv(CFG[\"train_csv\"])\n    sample = train_df.sample(n=min(n, len(train_df)), random_state=42)\n    sample[\"src\"] = sample[\"transliteration\"].apply(clean_transliteration)\n    sample[\"tgt\"] = sample[\"translation\"].apply(clean_translation)\n\n    model_path = os.path.join(CFG[\"output_dir\"], \"best_model\")\n    if not os.path.exists(model_path):\n        print(\"No trained model found. Run main() first.\")\n        return\n\n    translations = generate_translations(sample, model_path)\n    refs = [[t] for t in sample[\"tgt\"].tolist()]\n    bleu = sacrebleu.corpus_bleu(translations, list(zip(*refs))).score\n    chrf = sacrebleu.corpus_chrf(translations, list(zip(*refs)), word_order=2).score\n    geomean = math.sqrt(bleu * chrf)\n\n    print(f\"\\nSanity check on {n} train examples:\")\n    print(f\"  BLEU:    {bleu:.2f}\")\n    print(f\"  chrF++:  {chrf:.2f}\")\n    print(f\"  GeoMean: {geomean:.2f}\")\n\n    for i in range(min(3, len(sample))):\n        print(f\"\\n--- Example {i+1} ---\")\n        print(f\"  SRC: {sample['src'].iloc[i][:100]}\")\n        print(f\"  REF: {sample['tgt'].iloc[i][:100]}\")\n        print(f\"  HYP: {translations[i][:100]}\")\n\n\n# ─────────────────────────────────────────────\n# SECTION 13: Ensemble (optional)\n# ─────────────────────────────────────────────\n\ndef ensemble_predict(test_df: pd.DataFrame, model_paths: list[str]) -> list[str]:\n    all_translations = [generate_translations(test_df, mp) for mp in model_paths]\n    final = []\n    for i in range(len(all_translations[0])):\n        candidates = [all_translations[j][i] for j in range(len(model_paths))]\n        if len(candidates) == 1:\n            final.append(candidates[0])\n            continue\n        best, best_score = candidates[0], -1\n        for c in candidates:\n            others = [x for x in candidates if x != c]\n            score = np.mean([\n                sacrebleu.sentence_chrf(c, [o], word_order=2).score for o in others\n            ])\n            if score > best_score:\n                best, best_score = c, score\n        final.append(best)\n    return final\n\n\n# ─────────────────────────────────────────────\n# Entry point\n# ─────────────────────────────────────────────\n\nif __name__ == \"__main__\":\n    import sys\n    if len(sys.argv) > 1 and sys.argv[1] == \"eval\":\n        evaluate_on_train_sample(100)\n    else:\n        main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-06T18:37:53.828999Z","iopub.execute_input":"2026-03-06T18:37:53.829318Z","iopub.status.idle":"2026-03-06T20:52:57.361631Z","shell.execute_reply.started":"2026-03-06T18:37:53.829290Z","shell.execute_reply":"2026-03-06T20:52:57.360886Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}