{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 🧠 IOL-AI 2026 β€” Solution Notebook\n", "\n", "**International Linguistics Olympiad AI Challenge** \n", "Can a system reason out a language it has never seen?\n", "\n", "---\n", "\n", "## Strategy\n", "This notebook has **two modes**:\n", "\n", "| Mode | When to use | Model |\n", "|------|-------------|-------|\n", "| **API mode** (Section 2) | Local / Colab prototyping | Gemini 2.0 Flash (free tier) |\n", "| **Local model mode** (Section 3) | HuggingFace sandbox / final submission | Qwen2.5-7B-Instruct (4-bit) |\n", "\n", "### Scoring formula\n", "$$\\text{score} = \\sqrt{\\text{EM}_w \\times \\text{chrF}_w}$$\n", "\n", "We maximise both exact matches AND near-miss character overlap." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Section 0 β€” Install Dependencies" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "βœ“ Dependencies installed\n" ] } ], "source": [ "# Install required packages\n", "!pip install -q google-generativeai pandas sacrebleu tqdm\n", "\n", "print('βœ“ Dependencies installed')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Section 1 β€” Data Setup & Mock Data\n", "\n", "The real test CSV is mounted at `/tmp/data/test.csv` on the HF eval sandbox. \n", "For local development we create a **mock dataset** with real IOL-style problems." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Mode: MOCK (local)\n", "Data: mock_test.csv\n" ] } ], "source": [ "import os\n", "import json\n", "import re\n", "import time\n", "import pandas as pd\n", "from pathlib import Path\n", "\n", "# ── Path selection ─────────────────────────────────────────────────────────\n", "HF_DATA_PATH = '/tmp/data/test.csv'\n", "LOCAL_DATA_PATH = 'mock_test.csv'\n", "\n", "USE_MOCK = not os.path.exists(HF_DATA_PATH)\n", "DATA_PATH = HF_DATA_PATH if not USE_MOCK else LOCAL_DATA_PATH\n", "\n", "print(f'Mode: {\"MOCK (local)\" if USE_MOCK else \"REAL (HF sandbox)\"}')\n", "print(f'Data: {DATA_PATH}')" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "βœ“ Created mock dataset with 3 problems\n" ] } ], "source": [ "# ── Create realistic mock data (IOL-style problems) ──────────────────────────\n", "MOCK_PROBLEMS = [\n", " {\n", " 'id': '012023020100',\n", " 'context': '''Here are some sentences in Hakhun Tangsa (a Tibeto-Burman language of India) with English translations:\n", "Ε‹a ka kΙ€ ne | Do I go?\n", "nΙ€ Κ’ip tuΚ” ne | Did you sleep?\n", "Ε‹a Κ’ip kΙ€ ne | Do I sleep?\n", "nΙ€ ka tuΚ” ne | Did you go?\n", "Ε‹a man kΙ€ ne | Do I eat?\n", "nΙ€ man tuΚ” ne | Did you eat?\n", "Ε‹a Κ’ip ku ne | Does/Will I sleep?\n", "nΙ€ ka ku ne | Does/Will you go?''',\n", " 'query': 'Translate into English:\\n1. nΙ€ Κ’ip ku ne\\n2. Ε‹a man tuΚ” ne\\n3. nΙ€ ka kΙ€ ne',\n", " 'work_lang': 'eng_Latn',\n", " 'task_lang': 'tcz_Latn',\n", " 'task_type': 'translation',\n", " 'eval_type': 'single',\n", " },\n", " {\n", " 'id': '012023030200',\n", " 'context': '''Here are the squares of 1 through 10 in Ndom (a language of Papua New Guinea), given in arbitrary order:\n", "mer an thef abo thonith [= 25]\n", "nif thef abo tondor abo mer abo thonith [= 64]\n", "tondor abo mer abo thonith [= 49]\n", "thonith [= 1]\n", "mer abo thonith [= 9]\n", "ithin [= 100]\n", "nif abo thonith [= 16]\n", "\n", "Ndom number system (base 6):\n", "thonith = 1\n", "mer = 2\n", "tondor = 3 (also used as 3-groups)\n", "thef = 6\n", "nif = 36\n", "ithin = 216\n", "\n", "Simpler data β€” Ndom numbers:\n", "thonith = 1\n", "mer = 2 \n", "tondor = 3\n", "an = 4\n", "thef = 5\n", "nif = 6\n", "nif abo thonith = 7\n", "nif abo mer = 8\n", "nif abo tondor = 9\n", "nif abo an = 10\n", "nif abo thef = 11\n", "mer nif = 12\n", "ithin = 36\n", "ithin abo thonith = 37''',\n", " 'query': 'Write in numerals:\\n1. nif abo thonith\\n2. mer nif abo an\\n3. ithin abo nif abo mer',\n", " 'work_lang': 'eng_Latn',\n", " 'task_lang': 'ndo_Latn',\n", " 'task_type': 'text_to_num',\n", " 'eval_type': 'single',\n", " },\n", " {\n", " 'id': '012024010300',\n", " 'context': '''Here are some words in ApurinΓ£ (an Arawakan language of Brazil) with English translations:\n", "kama = macaw (a bird)\n", "Γ£kiti = monkey\n", "tsura = jaguar\n", "kamana = macaws\n", "Γ£kitia = monkeys\n", "tsuraa = jaguars\n", "apa = father\n", "apana = fathers\n", "ama = mother\n", "amaa = mothers\n", "putsu = stone\n", "putsuna = stones''',\n", " 'query': 'Fill in the blanks:\\n1. kite = fish β†’ fishes = ____\\n2. nΓ£ka = tree β†’ trees = ____\\n3. Γ£kitia = monkeys β†’ monkey = ____',\n", " 'work_lang': 'eng_Latn',\n", " 'task_lang': 'apu_Latn',\n", " 'task_type': 'fill_blanks',\n", " 'eval_type': 'single',\n", " },\n", "]\n", "\n", "if USE_MOCK:\n", " mock_df = pd.DataFrame(MOCK_PROBLEMS)\n", " mock_df.to_csv(LOCAL_DATA_PATH, index=False)\n", " print(f'βœ“ Created mock dataset with {len(mock_df)} problems')\n", " mock_df[['id','task_type','task_lang']].head(10)" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Loaded 3 problems\n", "\n", "Columns: ['id', 'context', 'query', 'work_lang', 'task_lang', 'task_type', 'eval_type']\n", "\n", "Task type distribution:\n", "task_type\n", "translation 1\n", "text_to_num 1\n", "fill_blanks 1\n", "\n", "--- First problem preview ---\n", "Here are some sentences in Hakhun Tangsa (a Tibeto-Burman language of India) with English translations:\n", "Ε‹a ka kΙ€ ne | Do I go?\n", "nΙ€ Κ’ip tuΚ” ne | Did you sleep?\n", "Ε‹a Κ’ip kΙ€ ne | Do I sleep?\n", "nΙ€ ka tuΚ” ne | Did you go?\n", "Ε‹a man kΙ€ ne | Do I eat?\n", "nΙ€ man tuΚ” ne | Did you eat?\n", "Ε‹a Κ’ip ku ne | Does/Will I sleep?\n", "nΙ€ ka ku ne | Does/Will you go?\n", "...\n", "Translate into English:\n", "1. nΙ€ Κ’ip ku ne\n", "2. Ε‹a man tuΚ” ne\n", "3. nΙ€ ka kΙ€ ne\n" ] } ], "source": [ "# ── Load and inspect the data ──────────────────────────────────────────────\n", "df = pd.read_csv(DATA_PATH, dtype=str).fillna('')\n", "print(f'Loaded {len(df)} problems')\n", "print()\n", "print('Columns:', df.columns.tolist())\n", "print()\n", "print('Task type distribution:')\n", "print(df['task_type'].value_counts().to_string())\n", "print()\n", "print('--- First problem preview ---')\n", "print(df.iloc[0]['context'][:500])\n", "print('...')\n", "print(df.iloc[0]['query'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Section 2 β€” API Mode: Gemini 2.0 Flash (Prototyping)\n", "\n", "Use this in Colab to quickly prototype and iterate. **Does NOT work in the HF sandbox** (no internet). \n", "Set your Gemini API key from [aistudio.google.com](https://aistudio.google.com)." ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Gemini API: DISABLED (no key set)\n" ] } ], "source": [ "# ═══════════════════════════════════════════════════════════════════\n", "# πŸ”‘ SET YOUR GEMINI API KEY HERE (or use Colab Secrets sidebar)\n", "# ═══════════════════════════════════════════════════════════════════\n", "GEMINI_API_KEY = '' # <-- paste your key, or leave blank to skip\n", "\n", "# Alternatively, load from Colab secrets:\n", "try:\n", " from google.colab import userdata\n", " GEMINI_API_KEY = userdata.get('GEMINI_API_KEY') or GEMINI_API_KEY\n", "except Exception:\n", " pass\n", "\n", "USE_API = bool(GEMINI_API_KEY)\n", "print(f'Gemini API: {\"ENABLED\" if USE_API else \"DISABLED (no key set)\"}')" ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "βœ“ Prompt utilities ready\n" ] } ], "source": [ "# ── Prompt engineering ─────────────────────────────────────────────────────\n", "\n", "SYSTEM_PROMPT = \"\"\"You are an expert solver of International Linguistics Olympiad (IOL) problems.\n", "These puzzles are COMPLETELY self-contained: all needed information is in the problem text.\n", "NEVER use external language knowledge β€” deduce everything from the examples given.\n", "\n", "## Method (follow every time):\n", "1. READ the full problem carefully.\n", "2. IDENTIFY patterns: morphemes, grammatical markers, number systems, word order, affixes.\n", "3. BUILD a concise rule table / lexicon from the examples.\n", "4. VERIFY your rules on a few given examples before answering.\n", "5. APPLY rules systematically to each numbered item.\n", "\n", "## Output format (CRITICAL):\n", "First, show your reasoning (2-10 lines). Then output:\n", "\n", "===ANSWERS===\n", "[answer to item 1]\n", "[answer to item 2]\n", "...\n", "\n", "One line per numbered item, in order, NO numbering, NO extra text.\n", "If multiple answers are valid, write them all on one line separated by \" / \".\"\"\"\n", "\n", "\n", "def build_user_prompt(context: str, query: str, task_type: str = '') -> str:\n", " hints = {\n", " 'text_to_num': '\\n[TASK TYPE: Convert written number words β†’ Arabic numerals. Map each number word methodically.]\\n',\n", " 'num_to_text': '\\n[TASK TYPE: Convert Arabic numerals β†’ number words in the target language.]\\n',\n", " 'fill_blanks': '\\n[TASK TYPE: Fill in missing words using grammatical patterns from the examples.]\\n',\n", " 'match_letters': '\\n[TASK TYPE: Map each symbol/letter to its romanised equivalent using the given correspondences.]\\n',\n", " 'translation': '\\n[TASK TYPE: Translate sentences using the vocabulary and grammar patterns shown.]\\n',\n", " }\n", " hint = hints.get(task_type, '')\n", " return f'{context.strip()}\\n{hint}\\n{query.strip()}'\n", "\n", "\n", "def count_items(query: str) -> int:\n", " \"\"\"Count numbered items (1., 2., 17., …) in the query.\"\"\"\n", " return max(len(re.findall(r'^\\s*\\d+\\.', query, re.MULTILINE)), 1)\n", "\n", "\n", "def parse_answers(raw: str, n_items: int) -> list:\n", " \"\"\"Extract ===ANSWERS=== block, or fall back to last N lines.\"\"\"\n", " if '===ANSWERS===' in raw:\n", " block = raw.split('===ANSWERS===', 1)[1].strip()\n", " lines = [l.strip() for l in block.splitlines() if l.strip()]\n", " if lines:\n", " return lines[:n_items] if len(lines) >= n_items else lines\n", " # Fallback\n", " lines = [l.strip() for l in raw.splitlines() if l.strip()]\n", " return lines[-n_items:] if len(lines) >= n_items else lines\n", "\n", "\n", "def extract_explanation(raw: str) -> str:\n", " \"\"\"Extract the reasoning section (before ===ANSWERS===) as explanation.\"\"\"\n", " if '===ANSWERS===' in raw:\n", " reasoning = raw.split('===ANSWERS===')[0].strip()\n", " else:\n", " reasoning = raw\n", " # Trim to 500 chars for the explanation column\n", " return reasoning[:500]\n", "\n", "\n", "print('βœ“ Prompt utilities ready')" ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [], "source": [ "# ── Gemini API inference ────────────────────────────────────────────────────\n", "if USE_API:\n", " import google.generativeai as genai\n", "\n", " genai.configure(api_key=GEMINI_API_KEY)\n", "\n", " generation_config = genai.GenerationConfig(\n", " temperature=0.0, # Deterministic\n", " max_output_tokens=1024,\n", " )\n", "\n", " gemini_model = genai.GenerativeModel(\n", " model_name='gemini-2.0-flash',\n", " system_instruction=SYSTEM_PROMPT,\n", " generation_config=generation_config,\n", " )\n", "\n", " print('βœ“ Gemini model initialised: gemini-2.0-flash')\n", "\n", " # Quick test\n", " test_resp = gemini_model.generate_content('Say: ready')\n", " print('Test:', test_resp.text.strip())" ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [], "source": [ "# ── Run API inference over the dataset ─────────────────────────────────────\n", "if USE_API:\n", " from tqdm.notebook import tqdm\n", " import time\n", "\n", " api_rows = []\n", " RATE_LIMIT_DELAY = 1.5 # seconds between calls (free tier: 15 RPM)\n", "\n", " for idx, row in tqdm(df.iterrows(), total=len(df), desc='Solving problems'):\n", " t0 = time.time()\n", " n_items = count_items(row['query'])\n", " user_text = build_user_prompt(row['context'], row['query'], row.get('task_type', ''))\n", "\n", " try:\n", " response = gemini_model.generate_content(user_text)\n", " raw = response.text.strip()\n", " except Exception as e:\n", " print(f' ⚠ Error on row {idx}: {e}')\n", " raw = ''\n", "\n", " answers = parse_answers(raw, n_items)\n", " explanation = extract_explanation(raw)\n", "\n", " api_rows.append({\n", " 'id': row['id'],\n", " 'pred': json.dumps(answers, ensure_ascii=False),\n", " 'explanation': explanation,\n", " })\n", "\n", " elapsed = time.time() - t0\n", " sleep_time = max(0, RATE_LIMIT_DELAY - elapsed)\n", " if sleep_time > 0:\n", " time.sleep(sleep_time)\n", "\n", " api_submission = pd.DataFrame(api_rows)\n", " api_submission.to_csv('submission_api.csv', index=False)\n", " print(f'\\nβœ“ Wrote {len(api_rows)} rows β†’ submission_api.csv')\n", " api_submission.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Section 3 β€” Local Model Mode (Qwen2.5-7B-Instruct, 4-bit)\n", "\n", "This is what runs in the **HuggingFace eval sandbox** (no internet, T4 GPU). \n", "Enable a GPU runtime in Colab: Runtime β†’ Change runtime type β†’ T4 GPU." ] }, { "cell_type": "code", "execution_count": 37, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CUDA available: True\n", "GPU: Tesla T4\n", "VRAM: 15.6 GB\n" ] } ], "source": [ "# ── Check GPU availability ─────────────────────────────────────────────────\n", "import torch\n", "print(f'CUDA available: {torch.cuda.is_available()}')\n", "if torch.cuda.is_available():\n", " print(f'GPU: {torch.cuda.get_device_name(0)}')\n", " print(f'VRAM: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB')" ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "βœ“ Transformers stack ready\n" ] } ], "source": [ "# ── Install model dependencies (skip if already installed) ─────────────────\n", "!pip install -q transformers accelerate autoawq\n", "print('βœ“ Transformers stack ready')" ] }, { "cell_type": "code", "execution_count": 39, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Will use model: Qwen/Qwen2.5-7B-Instruct\n", "Note: First download takes ~15 GB of disk space.\n" ] } ], "source": [ "# ── Download Qwen2.5-14B-AWQ-Instruct (for local Colab testing) ────────────────\n", "# In the HF submission, the model is already in the repo root ('.')\n", "# Here we download it for local testing.\n", "\n", "LOCAL_MODEL_NAME = 'Qwen/Qwen2.5-14B-Instruct-AWQ'\n", "# Smaller alternative for RAM-limited environments:\n", "# LOCAL_MODEL_NAME = 'Qwen/Qwen2.5-3B-Instruct'\n", "\n", "print(f'Will use model: {LOCAL_MODEL_NAME}')\n", "print('Note: First download takes ~9 GB of disk space.')" ] }, { "cell_type": "code", "execution_count": 40, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Loading tokeniser…\n", "Loading model in 4-bit (this may take 3–5 minutes)…\n" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "dda4b131a8ef4555829dfd13ee92ad87", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Loading weights: 0%| | 0/339 [00:00\n", "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
idpredexplanation
0012023020100[\"Will you sleep? / Do you sleep?\", \"Did I eat...The sentences provided follow a pattern where ...
1012023030200[\"6\", \"16\", \"228\"]From the given translations and the base 6 num...
2012024010300[\"1. kite = fish β†’ fishes = kitenΓ‘\", \"2. nΓ£ka ...From the examples, we can see that the plural ...
\n", "
\n", "
\n", " \n", "
\n", " \n", " \n", " \n", "\n", " \n", "
\n", " \n", "
\n", " \n", " " ], "text/plain": [ " id pred \\\n", "0 012023020100 [\"Will you sleep? / Do you sleep?\", \"Did I eat... \n", "1 012023030200 [\"6\", \"16\", \"228\"] \n", "2 012024010300 [\"1. kite = fish β†’ fishes = kitenΓ‘\", \"2. nΓ£ka ... \n", "\n", " explanation \n", "0 The sentences provided follow a pattern where ... \n", "1 From the given translations and the base 6 num... \n", "2 From the examples, we can see that the plural ... " ] }, "execution_count": 42, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# ── Run local model inference ──────────────────────────────────────────────\n", "from tqdm.notebook import tqdm\n", "import time\n", "import json\n", "import torch\n", "\n", "device = 'cuda' if torch.cuda.is_available() else 'cpu'\n", "MAX_NEW_TOKENS = 1024\n", "local_rows = []\n", "\n", "for idx, row in tqdm(df.iterrows(), total=len(df), desc='Local model inference'):\n", " t0 = time.time()\n", " n_items = count_items(row['query'])\n", " user_text = build_user_prompt(row['context'], row['query'], row.get('task_type', ''))\n", "\n", " messages = [\n", " {'role': 'system', 'content': SYSTEM_PROMPT},\n", " {'role': 'user', 'content': user_text},\n", " ]\n", "\n", " model_inputs = tok.apply_chat_template(\n", " messages, add_generation_prompt=True, return_tensors='pt', return_dict=True\n", " )\n", " model_inputs = {k: v.to(device) for k, v in model_inputs.items()}\n", " input_ids = model_inputs['input_ids']\n", "\n", " with torch.no_grad():\n", " output_ids = local_model.generate(\n", " **model_inputs,\n", " max_new_tokens=MAX_NEW_TOKENS,\n", " do_sample=False,\n", " repetition_penalty=1.1,\n", " )\n", "\n", " raw = tok.decode(\n", " output_ids[0][input_ids.shape[-1]:],\n", " skip_special_tokens=True\n", " ).strip()\n", "\n", " answers = parse_answers(raw, n_items)\n", " explanation = extract_explanation(raw)\n", "\n", " local_rows.append({\n", " 'id': row['id'],\n", " 'pred': json.dumps(answers, ensure_ascii=False),\n", " 'explanation': explanation,\n", " })\n", "\n", " elapsed = time.time() - t0\n", " print(f' [{len(local_rows)}/{len(df)}] id={row[\"id\"]} items={n_items} time={elapsed:.1f}s')\n", "\n", "local_submission = pd.DataFrame(local_rows)\n", "local_submission.to_csv('submission.csv', index=False)\n", "print(f'\\nβœ“ Wrote {len(local_rows)} rows β†’ submission.csv')\n", "local_submission.head()\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Section 4 β€” Score Evaluation (Local)\n", "\n", "Estimate score on the mock data to iterate on your prompt." ] }, { "cell_type": "code", "execution_count": 43, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Evaluating submission.csv…\n", "\n", " [012023020100] item 1: pred=\"Will you sleep? / Do you sleep?\" ref=\"Does/Will you sleep?\" EM=0 chrF=0.670\n", " [012023020100] item 2: pred=\"Did I eat?\" ref=\"Did I eat?\" EM=1 chrF=1.000\n", " [012023020100] item 3: pred=\"Did you go?\" ref=\"Do you go?\" EM=0 chrF=0.593\n", " [012023030200] item 1: pred=\"6\" ref=\"7\" EM=0 chrF=0.000\n", " [012023030200] item 2: pred=\"16\" ref=\"20\" EM=0 chrF=0.000\n", " [012023030200] item 3: pred=\"228\" ref=\"44\" EM=0 chrF=0.000\n", " [012024010300] item 1: pred=\"1. kite = fish β†’ fishes = kitenΓ‘\" ref=\"kitea\" EM=0 chrF=0.259\n", " [012024010300] item 2: pred=\"2. nΓ£ka = tree β†’ trees = nΓ£kanΓ‘\" ref=\"nΓ£kana\" EM=0 chrF=0.334\n", " [012024010300] item 3: pred=\"3. Γ£kitia = monkeys β†’ monkey = Γ£kiti\" ref=\"Γ£kiti\" EM=0 chrF=0.379\n", "\n", "══════════════════════════════\n", " Exact Match : 0.1111\n", " chrF : 0.3594\n", " Final Score : 0.1998\n", "══════════════════════════════\n" ] } ], "source": [ "import os\n", "import json\n", "import math\n", "import pandas as pd\n", "import sacrebleu\n", "\n", "# Fallback definition if Section 1 was not run\n", "if 'USE_MOCK' not in globals():\n", " USE_MOCK = not os.path.exists('/tmp/data/test.csv')\n", "\n", "# Mock ground-truth answers (matching our mock problems)\n", "MOCK_ANSWERS = {\n", " '012023020100': ['Does/Will you sleep?', 'Did I eat?', 'Do you go?'],\n", " '012023030200': ['7', '20', '44'],\n", " '012024010300': ['kitea', 'nΓ£kana', 'Γ£kiti'],\n", "}\n", "\n", "\n", "def chrf_score(hypothesis: str, reference: str) -> float:\n", " \"\"\"Compute chrF for a single hypothesis-reference pair.\"\"\"\n", " result = sacrebleu.corpus_chrf([hypothesis], [[reference]])\n", " return result.score / 100.0\n", "\n", "\n", "def evaluate_submission(submission_path: str, answers: dict) -> dict:\n", " sub = pd.read_csv(submission_path, dtype=str).fillna('')\n", " total_em = 0\n", " total_chrf = 0\n", " total_items = 0\n", "\n", " for _, row in sub.iterrows():\n", " pid = row['id']\n", " if pid not in answers:\n", " continue\n", " refs = answers[pid]\n", " \n", " try:\n", " preds = json.loads(row['pred'])\n", " except Exception:\n", " preds = []\n", "\n", " # Pad preds to match refs length to ensure we score all items\n", " while len(preds) < len(refs):\n", " preds.append('')\n", "\n", " for i, (pred, ref) in enumerate(zip(preds, refs)):\n", " em = int(pred.strip().lower() == ref.strip().lower())\n", " cf = chrf_score(pred, ref)\n", " total_em += em\n", " total_chrf += cf\n", " total_items += 1\n", " print(f' [{pid}] item {i+1}: pred=\"{pred}\" ref=\"{ref}\" EM={em} chrF={cf:.3f}')\n", "\n", " if total_items == 0:\n", " return {'em': 0, 'chrf': 0, 'score': 0}\n", "\n", " em_avg = total_em / total_items\n", " chrf_avg = total_chrf / total_items\n", " score = math.sqrt(em_avg * chrf_avg)\n", " return {'em': em_avg, 'chrf': chrf_avg, 'score': score, 'n_items': total_items}\n", "\n", "\n", "if USE_MOCK:\n", " # Evaluate whichever submission exists\n", " sub_path = 'submission.csv' if os.path.exists('submission.csv') else 'submission_api.csv'\n", " if os.path.exists(sub_path):\n", " print(f'Evaluating {sub_path}…\\n')\n", " results = evaluate_submission(sub_path, MOCK_ANSWERS)\n", " print(f'\\n══════════════════════════════')\n", " print(f' Exact Match : {results[\"em\"]:.4f}')\n", " print(f' chrF : {results[\"chrf\"]:.4f}')\n", " print(f' Final Score : {results[\"score\"]:.4f}')\n", " print(f'══════════════════════════════')\n", " else:\n", " print('No submission file found. Run Section 2 or 3 first.')\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Section 5 β€” Inspect Outputs" ] }, { "cell_type": "code", "execution_count": 44, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "ID: 12023020100\n", " 1. Will you sleep? / Do you sleep?\n", " 2. Did I eat?\n", " 3. Did you go?\n", " [Explanation]: The sentences provided follow a pattern where the subject is indicated by the first word, followed by the verb, then a particle indicating past tense or future/will, and finally \"ne\" which seems to be\n", "\n", "ID: 12023030200\n", " 1. 6\n", " 2. 16\n", " 3. 228\n", " [Explanation]: From the given translations and the base 6 number system, we can map the words directly to their numerical values.\n", "\n", "1. nif abo thonith = 6 * 1 = 6\n", "2. mer nif abo an = 2 * 6 + 4 = 12 + 4 = 16\n", "3. ithin \n", "\n", "ID: 12024010300\n", " 1. 1. kite = fish β†’ fishes = kitenΓ‘\n", " 2. 2. nΓ£ka = tree β†’ trees = nΓ£kanΓ‘\n", " 3. 3. Γ£kitia = monkeys β†’ monkey = Γ£kiti\n", " [Explanation]: From the examples, we can see that the plural form is created by adding \"-na\" to the stem of the word. For singular forms, it seems there might be a different pattern or no additional suffix for singu\n", "\n" ] } ], "source": [ "sub_path = 'submission.csv' if os.path.exists('submission.csv') else 'submission_api.csv'\n", "if os.path.exists(sub_path):\n", " sub = pd.read_csv(sub_path)\n", " for _, row in sub.iterrows():\n", " print(f'ID: {row[\"id\"]}')\n", " preds = json.loads(row['pred'])\n", " for i, p in enumerate(preds, 1):\n", " print(f' {i}. {p}')\n", " print(f' [Explanation]: {str(row.get(\"explanation\", \"\"))[:200]}')\n", " print()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ── Sync 14B AWQ model weights directly from Colab to Hugging Face ──────────\n", "# Run this cell inside Google Colab to download and sync the weights.\n", "# Colab's high network bandwidth completes this download + upload in seconds.\n", "\n", "from huggingface_hub import snapshot_download, HfApi\n", "\n", "import getpass\n", "TOKEN = getpass.getpass('Enter your Hugging Face WRITE token: ')\n", "SOURCE_REPO = 'Qwen/Qwen2.5-14B-Instruct-AWQ'\n", "TARGET_REPO = 'ahsanatiq98/iol-ai-submission'\n", "\n", "print('Downloading 14B AWQ model weights to Colab server cache...')\n", "local_path = snapshot_download(\n", " repo_id=SOURCE_REPO,\n", " token=TOKEN,\n", " ignore_patterns=['*.git*', '*.gitattributes']\n", ")\n", "print('Download completed.')\n", "\n", "print(f'Uploading 14B AWQ model weights to target repo: {TARGET_REPO}...')\n", "api = HfApi(token=TOKEN)\n", "api.upload_folder(\n", " folder_path=local_path,\n", " repo_id=TARGET_REPO,\n", " repo_type='model',\n", " commit_message='Upload Qwen2.5-14B-Instruct-AWQ weights from Colab'\n", ")\n", "print('βœ“ Upload completed! Model weights are successfully synced.')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Section 6 β€” HuggingFace Submission Prep\n", "\n", "Steps to submit to the competition:\n", "\n", "1. Create a **public** HuggingFace model repo\n", "2. Upload `script.py` (the standalone script, not this notebook)\n", "3. Download Qwen2.5-7B-Instruct weights into the repo:\n", " ```bash\n", " # In your HF repo, clone and run:\n", " huggingface-cli download Qwen/Qwen2.5-7B-Instruct --local-dir . --local-dir-use-symlinks False\n", " git add . && git commit -m \"Add model weights\" && git push\n", " ```\n", "4. Submit your repo ID at the competition Space\n", "\n", "### Download the submission.csv" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Download the submission file (in Google Colab)\n", "try:\n", " from google.colab import files\n", " if os.path.exists('submission.csv'):\n", " files.download('submission.csv')\n", " print('βœ“ Downloading submission.csv')\n", " elif os.path.exists('submission_api.csv'):\n", " files.download('submission_api.csv')\n", " print('βœ“ Downloading submission_api.csv')\n", " else:\n", " print('No submission file found.')\n", "except ImportError:\n", " print('Not in Colab. Find submission.csv in the current directory.')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "## πŸ“ Notes & Tips\n", "\n", "### Improving Score\n", "- **Few-shot examples**: Add solved examples from previous IOL competitions to the system prompt\n", "- **Self-consistency**: Run the model N times and take majority vote\n", "- **Task-specific prompts**: Different prompt templates per `task_type`\n", "- **Post-processing**: Strip numbering from answers, normalise unicode\n", "\n", "### Rate Limits (Gemini Free Tier)\n", "- 15 requests/minute (RPM)\n", "- 1,500 requests/day (RPD)\n", "- Use `time.sleep(4)` between calls to stay safe\n", "\n", "### HuggingFace Sandbox Constraints\n", "- No internet at eval time\n", "- T4 GPU, 16 GB VRAM\n", "- 30-minute time limit\n", "- Pre-installed: `bitsandbytes`, `autoawq`, `transformers`, `pandas`, `torch`\n", "\n", "### Scoring Reminder\n", "$$\\text{score} = \\sqrt{\\text{EM}_w \\times \\text{chrF}_w}$$\n", "Both need to be high β€” exact matches matter, but partial credit counts too." ] } ], "metadata": { "accelerator": "GPU", "colab": { "gpuType": "T4", "name": "IOL-AI-2026-Solution.ipynb", "provenance": [] }, "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" } }, "nbformat": 4, "nbformat_minor": 0 }