{"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 EXPERIMENT - Ratio Test [0.80, 0.20]\n\nTesting optimal blend ratio for ensemble + custom model combination.\n\nACKNOWLEDGMENTS:\n- KishoreKashyap: 34.9 custom model (V43) used as external submission\n- Nikita Kuznetsov: Original submission blending technique (34.6 baseline)\n\nEXPERIMENT:\n- Base ensemble: 34.5 (standard 3-model weight averaging)\n- External model: KishoreKashyap V43 (34.9)\n- Blend ratio: 80% ours / 20% Kishore's\n- Hypothesis: Conservative approach, less external reliance\n\nRESULTS:\n- Nikita 70/30 = 34.6\n- Nikita 20/80 = 34.4 (too much external)\n- Testing 80/20 = very conservative (might be safer for private LB)\n\nFeedback welcome in comments. Share your score if you use this!\n\"\"\"\n\nimport os\nimport pandas as pd\nimport torch\nfrom torch.utils.data import DataLoader, Dataset\nfrom transformers import AutoTokenizer, AutoModelForSeq2SeqLM\nimport re\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\": 8,\n    \"gen_params\": {\n        \"num_beams\": 8,\n        \"max_new_tokens\": 512,\n        \"length_penalty\": 1.10,\n        \"early_stopping\": True\n    },\n    \"blend_weights\": [0.80, 0.20]\n}\n\nprint(f\"EXPERIMENT: {CONFIG['blend_weights'][0]*100:.0f}% our model + {CONFIG['blend_weights'][1]*100:.0f}% external\")\nprint(f\"Baseline (Nikita 70/30): 34.6\")\n\ndef preprocess_transliteration(text):\n    if pd.isna(text): return \"\"\n    processed_text = str(text)\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\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 = text.replace('ḫ', 'h').replace('Ḫ', 'H')\n    sub_map = str.maketrans(\"₀₁₂₃₄₅₆₇₈₉\", \"0123456789\")\n    processed_text = processed_text.translate(sub_map)\n    processed_text = re.sub(r'(\\[x\\]|\\(x\\)|\\bx\\b)', '<gap>', processed_text, flags=re.I)\n    processed_text = re.sub(r'(\\.{3,}|…|\\[\\.+\\])', '<big_gap>', processed_text)\n    \n    processed_text = re.sub(r'<gap>\\s*<gap>', ' <big_gap> ', processed_text)\n    processed_text = re.sub(r'<big_gap>\\s*<big_gap>', ' <big_gap> ', processed_text)\n    processed_text = re.sub(r'\\((fem|plur|pl|sing|singular|plural|\\?|!)\\.?\\s*\\w*\\)', '', processed_text, flags=re.I)\n    processed_text = processed_text.replace('<gap>', '\\x00GAP\\x00').replace('<big_gap>', '\\x00BIG\\x00')\n    \n    bad_chars = '!?()\"—–<>⌈⌋⌊[]+ʾ/;'\n    processed_text = processed_text.translate(str.maketrans('', '', bad_chars))\n    processed_text = processed_text.replace('\\x00GAP\\x00', ' <gap> ').replace('\\x00BIG\\x00', ' <big_gap> ')\n    \n    frac_map = {\n        r'\\.5\\b': ' ½', r'\\.25\\b': ' ¼', r'\\.75\\b': ' ¾',\n        r'\\.33+\\d*\\b': ' ⅓', r'\\.66+\\d*\\b': ' ⅔'\n    }\n    for pat, rep in frac_map.items():\n        processed_text = re.sub(r'(\\d+)' + pat, r'\\1' + rep, processed_text)\n        processed_text = re.sub(r'\\b0' + pat, rep.strip(), processed_text)\n    \n    processed_text = re.sub(r'\\b(\\w+)(?:\\s+\\1\\b)+', r'\\1', processed_text)\n    for n in range(4, 1, -1):\n        pat = r'\\b((?:\\w+\\s+){' + str(n-1) + r'}\\w+)(?:\\s+\\1\\b)+'\n        processed_text = re.sub(pat, r'\\1', processed_text)\n    \n    if processed_text and processed_text[0].islower():\n        processed_text = processed_text[0].upper() + processed_text[1:]\n    \n    if processed_text and processed_text[-1] not in '.!?':\n        processed_text = processed_text + '.'\n    \n    return re.sub(r'\\s+', ' ', processed_text).strip().strip('-')\n\ndef blend_translations(text1, text2, weight1=0.80, weight2=0.20):\n    if not text1 or not text2:\n        return text1 if text1 else text2\n    \n    def score_text(text):\n        score = 0\n        words = text.split()\n        \n        if 10 <= len(words) <= 40:\n            score += 2\n        elif 5 <= len(words) <= 50:\n            score += 1\n        \n        if text and text[0].isupper():\n            score += 1\n        \n        if text and text[-1] in '.!?':\n            score += 1\n        \n        akkadian_keywords = ['tablet', 'king', 'city', 'god', 'said', 'wrote', 'year']\n        for keyword in akkadian_keywords:\n            if keyword in text.lower():\n                score += 0.5\n        \n        if '???' not in text and '...' not in text and 'xxx' not in text.lower():\n            score += 1\n        \n        return score\n    \n    score1 = score_text(text1)\n    score2 = score_text(text2)\n    \n    weighted_score1 = score1 * weight1\n    weighted_score2 = score2 * weight2\n    \n    if weighted_score1 >= weighted_score2:\n        return text1\n    else:\n        return text2\n\ndef create_model_soup():\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 weights: {[f'{w:.3f}' for w in WEIGHTS]}\")\n    \n    template_model = AutoModelForSeq2SeqLM.from_pretrained(CONFIG['models'][1])\n    soup_sd = template_model.state_dict()\n    \n    model_1_sd = AutoModelForSeq2SeqLM.from_pretrained(CONFIG['models'][0]).state_dict()\n    model_3_sd = AutoModelForSeq2SeqLM.from_pretrained(CONFIG['models'][2]).state_dict()\n    \n    for key in soup_sd:\n        weighted_value = WEIGHTS[1] * soup_sd[key]\n        norm_factor = WEIGHTS[1]\n        \n        if key in model_1_sd:\n            weighted_value += WEIGHTS[0] * model_1_sd[key]\n            norm_factor += WEIGHTS[0]\n        if key in model_3_sd:\n            weighted_value += WEIGHTS[2] * model_3_sd[key]\n            norm_factor += WEIGHTS[2]\n            \n        soup_sd[key] = weighted_value / norm_factor\n        \n    template_model.load_state_dict(soup_sd)\n    return template_model.to(CONFIG['device']).eval().float()\n\nclass AkkadianTranslationDataset(Dataset):\n    def __init__(self, dataframe):\n        self.ids = dataframe['id'].tolist()\n        self.texts = [\n            \"translate Akkadian to English: \" \n            + str(t) 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\nprint(\"\\nLoading external submission (KishoreKashyap V43)...\")\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\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(\"\\nCreating ensemble model...\")\nmodel = create_model_soup()\ntokenizer = AutoTokenizer.from_pretrained(CONFIG['models'][1])\n\ndata_loader = DataLoader(\n    AkkadianTranslationDataset(dataframe),\n    batch_size=CONFIG['batch_size'],\n    shuffle=False,\n    num_workers=2,\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(\"\\nGenerating predictions from ensemble...\")\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} batches\")\n\nprint(f\"Generated {len(our_predictions)} predictions\")\n\nprint(\"\\nBlending predictions...\")\nblended_results = []\nfor id_ in sorted(our_predictions.keys()):\n    our_translation = our_predictions[id_]\n    external_translation = external_dict.get(id_, \"\")\n    \n    blended = blend_translations(\n        our_translation, \n        external_translation,\n        weight1=CONFIG['blend_weights'][0],\n        weight2=CONFIG['blend_weights'][1]\n    )\n    \n    if not blended or blended.strip() == \"\":\n        blended = our_translation\n    \n    blended_results.append((id_, blended))\n\nsubmission_df = pd.DataFrame(blended_results, columns=['id', 'translation'])\n\nbroken_count = sum(1 for t in submission_df['translation'] \n                   if len(t.split()) < 3 or \"fragmentary\" in t.lower())\nprint(f\"Quality check: {broken_count} potentially problematic translations\")\n\nsubmission_df['translation'] = submission_df['translation'].apply(\n    lambda x: \"The tablet contains an incomplete inscription.\" \n    if len(x.split()) < 3 else x\n)\n\nsubmission_df.to_csv(\"submission.csv\", index=False)\nprint(f\"\\nSaved submission with {len(submission_df)} rows\")\n\nprint(\"\\nSample comparisons (first 3):\")\nfor i in range(min(3, len(submission_df))):\n    id_ = submission_df.iloc[i]['id']\n    print(f\"\\nID: {id_}\")\n    print(f\"Ours:     {our_predictions[id_][:70]}...\")\n    print(f\"External: {external_dict.get(id_, 'N/A')[:70]}...\")\n    print(f\"Blended:  {submission_df.iloc[i]['translation'][:70]}...\")\n\nprint(f\"\\nEXPERIMENT COMPLETE - Ratio: {CONFIG['blend_weights'][0]*100:.0f}/{CONFIG['blend_weights'][1]*100:.0f}\")\nprint(f\"Baseline: 34.6 | Share your score in comments!\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}