{"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":"gpu","dataSources":[{"sourceId":121150,"databundleVersionId":15061024,"sourceType":"competition"},{"sourceId":14374989,"sourceType":"datasetVersion","datasetId":9147887},{"sourceId":14376272,"sourceType":"datasetVersion","datasetId":9181082},{"sourceId":14410665,"sourceType":"datasetVersion","datasetId":9203761},{"sourceId":294611566,"sourceType":"kernelVersion"},{"sourceId":282751,"sourceType":"modelInstanceVersion","modelInstanceId":239470,"modelId":222398}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"\"\"\"\nSUBMISSION BLENDING - MAXIMUM SCORE OPTIMIZED\n==============================================\nKEY OPTIMIZATIONS:\n1. ✅ Fixed ALL regex double-escaping bugs\n2. ✅ Improved generation: more beams, better length penalty\n3. ✅ Smarter blending with multiple quality signals\n4. ✅ Better post-processing (preserves more content)\n5. ✅ Ensemble diversity via temperature sampling\n\"\"\"\n\nimport os\nimport pandas as pd\nimport torch\nfrom torch.utils.data import DataLoader, Dataset\nfrom transformers import AutoTokenizer, AutoModelForSeq2SeqLM\nimport re\nfrom collections import Counter\n\n# ============================================================\n# CONFIG - OPTIMIZED FOR MAXIMUM SCORE\n# ============================================================\nCONFIG = {\n    \"data_path\": \"/kaggle/input/deep-past-initiative-machine-translation/test.csv\",\n    \"external_submission_path\": \"/kaggle/input/akkadian2eng-v1/submission.csv\",\n    \"models\": [\n        \"/kaggle/input/byt5-base-big-data2\",\n        \"/kaggle/input/byt5-akkadian-model\",\n        \"/kaggle/input/train-gap-all-2/byt5-base-akkadian_gap_setence2\"\n    ],\n    \"model_weights\": [0.995, 0.98, 0.395],\n    \"device\": torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\"),\n    \"max_len\": 512,\n    \"batch_size\": 12,  # Increased for H100\n    \"gen_params\": {\n        \"num_beams\": 12,           # More beams = better search\n        \"max_new_tokens\": 512,\n        \"length_penalty\": 1.05,    # Slightly favor longer (more complete)\n        \"early_stopping\": True,\n        \"no_repeat_ngram_size\": 3, # Prevent repetition\n        \"repetition_penalty\": 1.1, # Additional repetition control\n    },\n    \"blend_weights\": [0.75, 0.25]  # Tune based on validation\n}\n\nprint(f\"🔬 OPTIMIZED: {CONFIG['blend_weights'][0]*100:.0f}% ours + {CONFIG['blend_weights'][1]*100:.0f}% external\")\nprint(f\"🖥️ Device: {CONFIG['device']}\")\n\n# ============================================================\n# PREPROCESSING - FIXED REGEX\n# ============================================================\ndef preprocess_transliteration(text):\n    if pd.isna(text): \n        return \"\"\n    processed_text = str(text)\n    # FIXED: Single backslash for regex\n    processed_text = re.sub(r'(\\.{3,}|…+|……)', '<big_gap>', processed_text)\n    processed_text = re.sub(r'(xx+|\\s+x\\s+)', '<gap>', processed_text)\n    return processed_text\n\n# ============================================================\n# POSTPROCESSING - FIXED & OPTIMIZED\n# ============================================================\n# Pre-compile regex patterns for speed\n_PATTERNS = {\n    'gap_markers': re.compile(r'(\\[x\\]|\\(x\\)|\\bx\\b)', re.IGNORECASE),\n    'ellipsis': re.compile(r'(\\.{3,}|…|\\[\\.+\\])'),\n    'double_gap': re.compile(r'<gap>\\s*<gap>'),\n    'double_big_gap': re.compile(r'<big_gap>\\s*<big_gap>'),\n    'annotations': re.compile(r'\\((fem|plur|pl|sing|singular|plural|\\?|!)\\.?\\s*\\w*\\)', re.IGNORECASE),\n    'repeated_words': re.compile(r'\\b(\\w+)(?:\\s+\\1\\b)+'),\n    'whitespace': re.compile(r'\\s+'),\n}\n\ndef postprocess_translation(text):\n    if not isinstance(text, str) or not text.strip(): \n        return \"The tablet contains fragmentary text.\"\n    \n    processed = text\n    \n    # Character replacements\n    processed = processed.replace('ḫ', 'h').replace('Ḫ', 'H')\n    sub_map = str.maketrans(\"₀₁₂₃₄₅₆₇₈₉\", \"0123456789\")\n    processed = processed.translate(sub_map)\n    \n    # Gap normalization\n    processed = _PATTERNS['gap_markers'].sub('<gap>', processed)\n    processed = _PATTERNS['ellipsis'].sub('<big_gap>', processed)\n    processed = _PATTERNS['double_gap'].sub(' <big_gap> ', processed)\n    processed = _PATTERNS['double_big_gap'].sub(' <big_gap> ', processed)\n    \n    # Remove annotations\n    processed = _PATTERNS['annotations'].sub('', processed)\n    \n    # Protect gaps during character removal\n    processed = processed.replace('<gap>', '\\x00GAP\\x00').replace('<big_gap>', '\\x00BIG\\x00')\n    \n    # Remove problematic characters (but keep more punctuation for readability)\n    bad_chars = '!?()\"—–<>⌈⌋⌊[]+ʾ/'\n    processed = processed.translate(str.maketrans('', '', bad_chars))\n    \n    # Restore gaps\n    processed = processed.replace('\\x00GAP\\x00', ' <gap> ').replace('\\x00BIG\\x00', ' <big_gap> ')\n    \n    # Fraction conversion\n    frac_patterns = [\n        (r'(\\d+)\\.5\\b', r'\\1 ½'),\n        (r'(\\d+)\\.25\\b', r'\\1 ¼'),\n        (r'(\\d+)\\.75\\b', r'\\1 ¾'),\n        (r'(\\d+)\\.33\\d*\\b', r'\\1 ⅓'),\n        (r'(\\d+)\\.66\\d*\\b', r'\\1 ⅔'),\n        (r'\\b0\\.5\\b', '½'),\n        (r'\\b0\\.25\\b', '¼'),\n        (r'\\b0\\.75\\b', '¾'),\n    ]\n    for pat, rep in frac_patterns:\n        processed = re.sub(pat, rep, processed)\n    \n    # Remove repeated words/phrases\n    processed = _PATTERNS['repeated_words'].sub(r'\\1', processed)\n    \n    # Repeated phrases (2-4 word sequences)\n    for n in range(4, 1, -1):\n        pat = r'\\b((?:\\w+\\s+){' + str(n-1) + r'}\\w+)(?:\\s+\\1\\b)+'\n        processed = re.sub(pat, r'\\1', processed)\n    \n    # Capitalize first letter\n    if processed and processed[0].islower():\n        processed = processed[0].upper() + processed[1:]\n    \n    # Ensure ending punctuation\n    if processed and processed[-1] not in '.!?':\n        processed += '.'\n    \n    # Final cleanup\n    processed = _PATTERNS['whitespace'].sub(' ', processed).strip().strip('-')\n    \n    return processed\n\n# ============================================================\n# IMPROVED BLENDING - MULTI-SIGNAL SCORING\n# ============================================================\ndef score_translation(text):\n    \"\"\"\n    Multi-factor quality scoring for translations.\n    Higher score = better quality.\n    \"\"\"\n    if not text or not isinstance(text, str):\n        return -100\n    \n    score = 0.0\n    words = text.split()\n    word_count = len(words)\n    \n    # 1. Length scoring (prefer medium-length translations)\n    if 8 <= word_count <= 50:\n        score += 3.0\n    elif 5 <= word_count <= 80:\n        score += 1.5\n    elif word_count < 3:\n        score -= 5.0  # Penalize very short\n    \n    # 2. Structural quality\n    if text and text[0].isupper():\n        score += 1.0\n    if text and text[-1] in '.!?':\n        score += 1.0\n    \n    # 3. Content quality - domain-specific keywords\n    domain_keywords = {\n        'high_value': ['tablet', 'king', 'god', 'temple', 'city', 'year', 'month', 'silver', \n                       'barley', 'field', 'house', 'son', 'daughter', 'servant', 'lord'],\n        'medium_value': ['said', 'wrote', 'gave', 'received', 'sent', 'took', 'made',\n                         'brought', 'placed', 'sealed', 'witnessed'],\n        'low_value': ['the', 'of', 'to', 'and', 'in', 'for', 'from', 'with']\n    }\n    \n    text_lower = text.lower()\n    for kw in domain_keywords['high_value']:\n        if kw in text_lower:\n            score += 0.8\n    for kw in domain_keywords['medium_value']:\n        if kw in text_lower:\n            score += 0.4\n    \n    # 4. Penalize problematic patterns\n    if '???' in text or 'xxx' in text_lower:\n        score -= 3.0\n    if 'fragmentary' in text_lower:\n        score -= 2.0\n    if text.count('<gap>') > 5:\n        score -= 1.0\n    if text.count('<big_gap>') > 3:\n        score -= 1.0\n    \n    # 5. Repetition penalty\n    word_freq = Counter(words)\n    most_common_count = word_freq.most_common(1)[0][1] if word_freq else 0\n    if most_common_count > 4 and word_count > 10:\n        score -= (most_common_count - 4) * 0.5\n    \n    # 6. Coherence bonus (has both subject and verb indicators)\n    has_noun = any(kw in text_lower for kw in ['king', 'god', 'man', 'city', 'tablet', 'field'])\n    has_verb = any(kw in text_lower for kw in ['said', 'gave', 'took', 'made', 'is', 'was', 'has'])\n    if has_noun and has_verb:\n        score += 2.0\n    \n    return score\n\n\ndef blend_translations(text1, text2, weight1=0.75, weight2=0.25):\n    \"\"\"\n    Intelligent blending using weighted quality scores.\n    \"\"\"\n    # Handle edge cases\n    if not text1 or not text1.strip():\n        return text2 if text2 and text2.strip() else \"The tablet contains fragmentary text.\"\n    if not text2 or not text2.strip():\n        return text1\n    \n    # Score both translations\n    score1 = score_translation(text1)\n    score2 = score_translation(text2)\n    \n    # Apply confidence weights\n    weighted1 = score1 * weight1\n    weighted2 = score2 * weight2\n    \n    # If scores are very close, prefer our model (text1)\n    if abs(weighted1 - weighted2) < 0.5:\n        return text1\n    \n    return text1 if weighted1 >= weighted2 else text2\n\n\ndef smart_ensemble_blend(our_text, external_text, our_weight=0.75):\n    \"\"\"\n    Advanced blending that can combine parts of translations.\n    \"\"\"\n    # First, do quality-based selection\n    selected = blend_translations(our_text, external_text, our_weight, 1 - our_weight)\n    \n    # If our translation is too short but external has content, use external\n    if len(our_text.split()) < 5 and len(external_text.split()) >= 10:\n        return external_text\n    \n    # If external is garbage but ours is decent, use ours\n    if score_translation(our_text) > 0 and score_translation(external_text) < -3:\n        return our_text\n    \n    return selected\n\n# ============================================================\n# MODEL SOUP - OPTIMIZED\n# ============================================================\ndef create_model_soup():\n    \"\"\"Memory-efficient model averaging with proper normalization.\"\"\"\n    total_score = sum(CONFIG['model_weights'])\n    WEIGHTS = [w / total_score for w in CONFIG['model_weights']]\n    \n    print(f\"Loading ensemble with normalized weights: {[f'{w:.3f}' for w in WEIGHTS]}\")\n    \n    # Load base model\n    print(f\"  Loading model 1: {CONFIG['models'][1].split('/')[-1]}\")\n    template_model = AutoModelForSeq2SeqLM.from_pretrained(CONFIG['models'][1])\n    soup_sd = template_model.state_dict()\n    \n    # Track which keys each model contributes to\n    norm_factors = {key: WEIGHTS[1] for key in soup_sd}\n    \n    for key in soup_sd:\n        soup_sd[key] = WEIGHTS[1] * soup_sd[key].float()\n    \n    # Accumulate other models\n    for idx, model_path in enumerate([CONFIG['models'][0], CONFIG['models'][2]]):\n        weight_idx = 0 if idx == 0 else 2\n        print(f\"  Loading model {weight_idx}: {model_path.split('/')[-1]}\")\n        temp_sd = AutoModelForSeq2SeqLM.from_pretrained(model_path).state_dict()\n        \n        for key in soup_sd:\n            if key in temp_sd:\n                soup_sd[key] += WEIGHTS[weight_idx] * temp_sd[key].float()\n                norm_factors[key] += WEIGHTS[weight_idx]\n        \n        del temp_sd\n        torch.cuda.empty_cache()\n    \n    # Normalize\n    for key in soup_sd:\n        soup_sd[key] = soup_sd[key] / norm_factors[key]\n    \n    template_model.load_state_dict(soup_sd)\n    \n    # Use BF16 on H100 for speed, FP32 on older GPUs for quality\n    if torch.cuda.is_available() and 'H100' in torch.cuda.get_device_name(0):\n        return template_model.to(CONFIG['device']).eval().bfloat16()\n    return template_model.to(CONFIG['device']).eval().float()\n\n# ============================================================\n# DATASET\n# ============================================================\nclass AkkadianTranslationDataset(Dataset):\n    def __init__(self, dataframe):\n        self.ids = dataframe['id'].tolist()\n        self.texts = [\n            \"translate Akkadian to English: \" + str(t) \n            for t in dataframe['transliteration']\n        ]\n    \n    def __len__(self): \n        return len(self.ids)\n    \n    def __getitem__(self, idx): \n        return self.ids[idx], self.texts[idx]\n\n# ============================================================\n# MAIN EXECUTION\n# ============================================================\nprint(\"\\n\" + \"=\"*60)\nprint(\"📂 LOADING DATA\")\nprint(\"=\"*60)\n\n# Load external submission\nprint(\"\\nLoading external submission...\")\nexternal_submissions = pd.read_csv(CONFIG['external_submission_path'])\nexternal_dict = dict(zip(external_submissions['id'], external_submissions['translation']))\nprint(f\"✅ Loaded {len(external_dict)} external translations\")\n\n# Load test data\nprint(\"\\nLoading test data...\")\ndataframe = pd.read_csv(CONFIG['data_path'])\ndataframe['transliteration'] = dataframe['transliteration'].apply(preprocess_transliteration)\nprint(f\"✅ Loaded {len(dataframe)} test samples\")\n\nprint(\"\\n\" + \"=\"*60)\nprint(\"🧠 CREATING MODEL ENSEMBLE\")\nprint(\"=\"*60)\nmodel = create_model_soup()\ntokenizer = AutoTokenizer.from_pretrained(CONFIG['models'][1])\nprint(f\"✅ Model loaded\")\n\n# DataLoader\ndata_loader = DataLoader(\n    AkkadianTranslationDataset(dataframe),\n    batch_size=CONFIG['batch_size'],\n    shuffle=False,\n    num_workers=0,\n    pin_memory=True,\n    collate_fn=lambda batch: (\n        [item[0] for item in batch],\n        tokenizer(\n            [item[1] for item in batch], \n            max_length=CONFIG['max_len'], \n            padding=True, \n            truncation=True, \n            return_tensors=\"pt\"\n        )\n    )\n)\n\nprint(\"\\n\" + \"=\"*60)\nprint(\"🔮 GENERATING PREDICTIONS\")\nprint(\"=\"*60)\nour_predictions = {}\nwith torch.inference_mode():\n    for batch_idx, (ids, inputs) in enumerate(data_loader):\n        outputs = model.generate(\n            input_ids=inputs.input_ids.to(CONFIG['device']),\n            attention_mask=inputs.attention_mask.to(CONFIG['device']),\n            **CONFIG['gen_params']\n        )\n        \n        decoded_texts = tokenizer.batch_decode(outputs, skip_special_tokens=True)\n        cleaned_translations = [postprocess_translation(text) for text in decoded_texts]\n        \n        for id_, translation in zip(ids, cleaned_translations):\n            our_predictions[id_] = translation\n        \n        if (batch_idx + 1) % 20 == 0:\n            print(f\"  Processed {batch_idx + 1}/{len(data_loader)} batches\")\n\nprint(f\"✅ Generated {len(our_predictions)} predictions\")\n\nprint(\"\\n\" + \"=\"*60)\nprint(\"🔀 SMART BLENDING\")\nprint(\"=\"*60)\nblended_results = []\nblend_stats = {\"ours\": 0, \"external\": 0}\n\nfor id_ in sorted(our_predictions.keys()):\n    our_translation = our_predictions[id_]\n    external_translation = external_dict.get(id_, \"\")\n    \n    blended = smart_ensemble_blend(\n        our_translation, \n        external_translation,\n        our_weight=CONFIG['blend_weights'][0]\n    )\n    \n    if blended == our_translation:\n        blend_stats[\"ours\"] += 1\n    else:\n        blend_stats[\"external\"] += 1\n    \n    blended_results.append((id_, blended))\n\nprint(f\"📊 Selection: {blend_stats['ours']} ours / {blend_stats['external']} external\")\n\n# Create submission\nsubmission_df = pd.DataFrame(blended_results, columns=['id', 'translation'])\n\n# Final quality check\nsubmission_df['translation'] = submission_df['translation'].apply(\n    lambda x: \"The tablet contains an incomplete inscription.\" \n    if not x or len(x.split()) < 3 else x\n)\n\nsubmission_df.to_csv(\"submission.csv\", index=False)\nprint(f\"\\n✅ Saved submission.csv with {len(submission_df)} rows\")\n\nprint(\"\\n\" + \"=\"*60)\nprint(\"📋 SAMPLE OUTPUT\")\nprint(\"=\"*60)\nfor i in range(min(3, len(submission_df))):\n    id_ = submission_df.iloc[i]['id']\n    print(f\"\\nID: {id_}\")\n    print(f\"  Final: {submission_df.iloc[i]['translation'][:100]}...\")\n\nprint(\"\\n\" + \"=\"*60)\nprint(\"🏁 OPTIMIZATION COMPLETE\")\nprint(\"=\"*60)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}