{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","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":"gpu","dataSources":[{"sourceId":121150,"databundleVersionId":15061024,"sourceType":"competition"},{"sourceId":14374989,"sourceType":"datasetVersion","datasetId":9147887},{"sourceId":14376272,"sourceType":"datasetVersion","datasetId":9181082},{"sourceId":14398637,"sourceType":"datasetVersion","datasetId":9195725},{"sourceId":14410665,"sourceType":"datasetVersion","datasetId":9203761},{"sourceId":14530709,"sourceType":"datasetVersion","datasetId":9280771}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# 🏆 Akkadian-English Translation: Inference Notebook\n\nThis notebook performs inference using a trained ByT5 model.\n\n**Key Post-Processing:**\n- Gap normalization (x → `<gap>`, ... → `<big_gap>`)\n- Decimal to fraction conversion (0.5 → ½)\n- Task prefix for model input\n\n**Usage:**\n1. Add your trained model as a Kaggle dataset\n2. Update `MODEL_PATH` below\n3. Run all cells","metadata":{}},{"cell_type":"code","source":"import re\nimport pandas as pd\nimport torch\nfrom torch.utils.data import Dataset, DataLoader\nfrom transformers import AutoTokenizer, AutoModelForSeq2SeqLM\n\n# =============================================================================\n# CONFIGURATION - OPTIMIZED FOR 35+ LB\n# =============================================================================\nTEST_DATA_PATH = \"/kaggle/input/deep-past-initiative-machine-translation/test.csv\"\n\nMODEL1_PATH = \"/kaggle/input/byt5-base-big-data2\"\nMODEL2_PATH = \"/kaggle/input/byt5-akkadian-model\"\nMODEL3_PATH = \"/kaggle/input/train-gap-all-2/byt5-base-akkadian_gap_setence2\"\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nPREFIX = \"translate Akkadian to English: \"\nMAX_SOURCE_LEN = 512\nBATCH_SIZE = 8\n\n# Generation Parameters - TUNED FOR HIGHER SCORE\nNUM_BEAMS = 8               # Increased from 6 → 8 (more search = better output)\nMAX_NEW_TOKENS = 512        # Increased from 384 → 512 (longer outputs = less brevity penalty)\nLENGTH_PENALTY = 1.10       # Increased from 1.05 → 1.10 (encourages longer output)\nEARLY_STOPPING = True\nUSE_AMP = False\n\n# =============================================================================\n# INPUT PREPROCESSING - Gap normalization for transliteration\n# =============================================================================\ndef preprocess_input(text):\n    \"\"\"Normalize gaps in INPUT (transliteration) text.\"\"\"\n    if pd.isna(text):\n        return \"\"\n    text = str(text)\n    \n    # Multi-dot patterns → <big_gap>\n    text = re.sub(r'\\.{3}(?:\\s+\\.{3})+', '<big_gap>', text)\n    text = re.sub(r'\\.\\.\\.\\.\\.\\.', '<big_gap>', text)\n    text = re.sub(r'\\.\\.\\.', '<big_gap>', text)\n    text = re.sub(r'……', '<big_gap>', text)\n    text = re.sub(r'…', '<big_gap>', text)\n    \n    # x patterns → <gap>\n    text = re.sub(r'xx+', '<gap>', text)\n    text = re.sub(r' x ', ' <gap> ', text)\n    \n    return text\n\n# =============================================================================\n# OUTPUT POST-PROCESSING - ENHANCED VERSION\n# =============================================================================\ndef postprocess_output(text):\n    \"\"\"\n    Enhanced post-processing with NEW optimizations:\n    1. ḫ→h conversion (test set uses h, not ḫ)\n    2. Subscript number normalization\n    3. All previous improvements\n    \"\"\"\n    if not isinstance(text, str):\n        return str(text) if text else \"broken text\"\n    \n    if not text.strip():\n        return \"broken text\"\n    \n    # =========================================================================\n    # NEW: ḫ/Ḫ → h/H conversion (Test set uses h, not ḫ!)\n    # This was mentioned by MPWARE (10th place) as critical\n    # =========================================================================\n    text = text.replace('ḫ', 'h')\n    text = text.replace('Ḫ', 'H')\n    \n    # =========================================================================\n    # NEW: Subscript number normalization\n    # Test set uses DU10, not DU₁₀\n    # =========================================================================\n    subscript_map = {\n        '₀': '0', '₁': '1', '₂': '2', '₃': '3', '₄': '4',\n        '₅': '5', '₆': '6', '₇': '7', '₈': '8', '₉': '9',\n    }\n    for sub, normal in subscript_map.items():\n        text = text.replace(sub, normal)\n    \n    # =========================================================================\n    # STEP 1: Convert gap patterns in output to proper tags\n    # =========================================================================\n    text = re.sub(r'\\[x\\]', '<gap>', text, flags=re.IGNORECASE)\n    text = re.sub(r'\\(x\\)', '<gap>', text, flags=re.IGNORECASE)\n    text = re.sub(r'\\bx\\b', '<gap>', text)\n    text = re.sub(r'\\.\\.\\.+', '<big_gap>', text)\n    text = re.sub(r'…', '<big_gap>', text)\n    text = re.sub(r'\\[\\.+\\]', '<big_gap>', text)\n    \n    # =========================================================================\n    # STEP 2: Gap consolidation (same-type only!)\n    # =========================================================================\n    text = re.sub(r'<gap>\\s*<gap>', '<big_gap>', text)\n    text = re.sub(r'<big_gap>\\s*<big_gap>', '<big_gap>', text)\n    \n    # =========================================================================\n    # STEP 3: Remove scribal annotations\n    # =========================================================================\n    scribal_patterns = [\n        r'\\(fem\\.\\s*plur\\.\\)', r'\\(fem\\.\\)', r'\\(plur\\.\\)', \n        r'\\(pl\\.\\)', r'\\(sing\\.\\)', r'\\(singular\\)', r'\\(plural\\)',\n        r'\\(\\?\\)', r'\\(!\\)',\n    ]\n    for pattern in scribal_patterns:\n        text = re.sub(pattern, '', text, flags=re.IGNORECASE)\n    \n    # =========================================================================\n    # STEP 4: Character denoising\n    # =========================================================================\n    text = text.replace('<gap>', '\\x00GAP\\x00')\n    text = text.replace('<big_gap>', '\\x00BIGGAP\\x00')\n    \n    forbidden_chars = [\n        '!', '?', '(', ')', '\"', '\"', '\"', ';', '—', '–',\n        '<', '>', '⌈', '⌊', '⌋', '[', ']', '+', 'ʾ', '/',\n    ]\n    for char in forbidden_chars:\n        text = text.replace(char, '')\n    \n    text = text.replace('\\x00GAP\\x00', '<gap>')\n    text = text.replace('\\x00BIGGAP\\x00', '<big_gap>')\n    \n    # =========================================================================\n    # STEP 5: Fraction normalization\n    # =========================================================================\n    text = re.sub(r'(\\d+)\\.5\\b', r'\\1 ½', text)\n    text = re.sub(r'(\\d+)\\.33+\\d*\\b', r'\\1 ⅓', text)\n    text = re.sub(r'(\\d+)\\.66+\\d*\\b', r'\\1 ⅔', text)\n    text = re.sub(r'(\\d+)\\.25\\b', r'\\1 ¼', text)\n    text = re.sub(r'(\\d+)\\.75\\b', r'\\1 ¾', text)\n    \n    text = re.sub(r'\\b0\\.5\\b', '½', text)\n    text = re.sub(r'\\b0\\.33+\\d*\\b', '⅓', text)\n    text = re.sub(r'\\b0\\.66+\\d*\\b', '⅔', text)\n    text = re.sub(r'\\b0\\.25\\b', '¼', text)\n    text = re.sub(r'\\b0\\.75\\b', '¾', text)\n    \n    # After / removal, \"1/2\" becomes \"1 2\"\n    text = text.replace(' 1 2', ' ½')\n    text = text.replace(' 1 3', ' ⅓')\n    text = text.replace(' 2 3', ' ⅔')\n    text = text.replace(' 1 4', ' ¼')\n    text = text.replace(' 3 4', ' ¾')\n    \n    # =========================================================================\n    # STEP 6: Final cleanup\n    # =========================================================================\n    text = re.sub(r'\\s+', ' ', text)\n    text = text.strip()\n    text = re.sub(r'^-\\s*', '', text)\n    text = re.sub(r'\\s*-$', '', text)\n    \n    return text if text else \"broken text\"\n\n# =============================================================================\n# MODEL LOADING - Weighted Ensemble (Model Soup)\n# =============================================================================\nperf1, perf2, perf3 = 0.98, 1.00, 0.40\ntotal = perf1 + perf2 + perf3\nw1, w2, w3 = perf1/total, perf2/total, perf3/total\nprint(f\"Ensemble Weights: M1={w1:.4f}, M2={w2:.4f}, M3={w3:.4f}\")\n\nprint(\"Loading models for ensemble...\")\nm1 = AutoModelForSeq2SeqLM.from_pretrained(MODEL1_PATH)\nm2 = AutoModelForSeq2SeqLM.from_pretrained(MODEL2_PATH)\nm3 = AutoModelForSeq2SeqLM.from_pretrained(MODEL3_PATH)\n\nsd1, sd2, sd3 = m1.state_dict(), m2.state_dict(), m3.state_dict()\n\nprint(\"Averaging model weights...\")\nfinal_sd = sd2.copy()\nfor k in final_sd:\n    if k in sd1 and k in sd3:\n        final_sd[k] = w1 * sd1[k] + w2 * sd2[k] + w3 * sd3[k]\n    elif k in sd1:\n        final_sd[k] = w1 * sd1[k] + (w2 + w3) * sd2[k]\n    elif k in sd3:\n        final_sd[k] = w3 * sd3[k] + (w1 + w2) * sd2[k]\n\nmodel = AutoModelForSeq2SeqLM.from_pretrained(MODEL2_PATH)\nmodel.load_state_dict(final_sd)\nmodel.to(DEVICE).eval()\nmodel.float()\n\ntokenizer = AutoTokenizer.from_pretrained(MODEL2_PATH)\n\n# =============================================================================\n# DATASET AND DATALOADER\n# =============================================================================\ntest_df = pd.read_csv(TEST_DATA_PATH)\ntest_df[\"transliteration\"] = test_df[\"transliteration\"].apply(preprocess_input)\n\nclass InferenceDataset(Dataset):\n    def __init__(self, df):\n        self.ids = df[\"id\"].tolist()\n        self.texts = [PREFIX + t for t in df[\"transliteration\"].astype(str).tolist()]\n\n    def __len__(self):\n        return len(self.texts)\n\n    def __getitem__(self, idx):\n        return self.ids[idx], self.texts[idx]\n\ndef collate_fn(batch):\n    ids, texts = zip(*batch)\n    enc = tokenizer(\n        list(texts),\n        max_length=MAX_SOURCE_LEN,\n        truncation=True,\n        padding=True,\n        return_tensors=\"pt\"\n    )\n    return list(ids), enc[\"input_ids\"], enc[\"attention_mask\"]\n\nloader = DataLoader(\n    InferenceDataset(test_df),\n    batch_size=BATCH_SIZE,\n    shuffle=False,\n    num_workers=2,\n    pin_memory=(DEVICE.type == \"cuda\"),\n    collate_fn=collate_fn\n)\n\n# =============================================================================\n# INFERENCE\n# =============================================================================\nall_ids, all_pred = [], []\ntorch.set_grad_enabled(False)\n\nprint(\"Starting inference with ENHANCED settings...\")\nprint(f\"  NUM_BEAMS: {NUM_BEAMS}\")\nprint(f\"  MAX_NEW_TOKENS: {MAX_NEW_TOKENS}\")\nprint(f\"  LENGTH_PENALTY: {LENGTH_PENALTY}\")\n\nwith torch.inference_mode():\n    for batch_idx, (ids, input_ids, attention_mask) in enumerate(loader):\n        input_ids = input_ids.to(DEVICE)\n        attention_mask = attention_mask.to(DEVICE)\n\n        outputs = model.generate(\n            input_ids=input_ids,\n            attention_mask=attention_mask,\n            num_beams=NUM_BEAMS,\n            max_new_tokens=MAX_NEW_TOKENS,\n            length_penalty=LENGTH_PENALTY,\n            early_stopping=EARLY_STOPPING,\n        )\n\n        decoded = tokenizer.batch_decode(outputs, skip_special_tokens=True)\n        decoded = [postprocess_output(d) for d in decoded]\n\n        all_ids.extend(ids)\n        all_pred.extend(decoded)\n        \n        if (batch_idx + 1) % 10 == 0:\n            print(f\"Processed batch {batch_idx + 1}\")\n\n# =============================================================================\n# OUTPUT\n# =============================================================================\nsubmission = pd.DataFrame({\"id\": all_ids, \"translation\": all_pred})\nsubmission.to_csv(\"submission.csv\", index=False)\nprint(f\"Saved submission.csv with {len(submission)} rows\")\nprint(\"\\nSample predictions:\")\nprint(submission.head(10).to_string())\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-18T03:19:43.454284Z","iopub.execute_input":"2026-01-18T03:19:43.454572Z","iopub.status.idle":"2026-01-18T03:21:03.08131Z","shell.execute_reply.started":"2026-01-18T03:19:43.454547Z","shell.execute_reply":"2026-01-18T03:21:03.080489Z"}},"outputs":[],"execution_count":null}]}