{ "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, ?it/s]" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ "β Model loaded!\n" ] } ], "source": [ "from transformers import AutoTokenizer, AutoModelForCausalLM\n", "\n", "print('Loading tokeniserβ¦')\n", "tok = AutoTokenizer.from_pretrained(LOCAL_MODEL_NAME, trust_remote_code=True)\n", "\n", "print('Loading AWQ modelβ¦')\n", "local_model = AutoModelForCausalLM.from_pretrained(\n", " LOCAL_MODEL_NAME,\n", " torch_dtype=torch.float16,\n", " device_map='auto',\n", " trust_remote_code=True,\n", ").eval()\n", "\n", "print('β Model loaded!')" ] }, { "cell_type": "code", "execution_count": 41, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "In the sentences you provided, \"ka\" is a verb auxiliary that indicates the first person singular subject. Let's break down the sentences:\n", "\n", "1. **Εa ka kΙ€ ne** = \"Do I go?\"\n", "2. **nΙ€ ka ku ne** = \"Does/Will you go?\"\n", "\n", "### Analysis:\n", "- **Εa ka kΙ€ ne**: \n", " - **Εa** is likely a subject marker or a form of the verb \"to be\" in this context.\n", " \n" ] } ], "source": [ "# ββ Quick sanity test βββββββββββββββββββββββββββββββββββββββββββββββββββββ\n", "device = 'cuda' if torch.cuda.is_available() else 'cpu'\n", "test_messages = [\n", " {'role': 'system', 'content': 'You are an expert linguist.'},\n", " {'role': 'user', 'content': 'What does \"ka\" mean if Εa ka kΙ€ ne = \"Do I go?\" and nΙ€ ka ku ne = \"Does/Will you go?\"'}\n", "]\n", "\n", "test_inputs = tok.apply_chat_template(\n", " test_messages, add_generation_prompt=True, return_tensors='pt', return_dict=True\n", ")\n", "test_inputs = {k: v.to(device) for k, v in test_inputs.items()}\n", "\n", "with torch.no_grad():\n", " out = local_model.generate(**test_inputs, max_new_tokens=100, do_sample=False)\n", "\n", "print(tok.decode(out[0][test_inputs['input_ids'].shape[-1]:], skip_special_tokens=True))\n" ] }, { "cell_type": "code", "execution_count": 42, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "5d1131487f8047afb1fcb61cf881d62e", "version_major": 2, "version_minor": 0 }, "text/plain": [ "Local model inference: 0%| | 0/3 [00:00, ?it/s]" ] }, "metadata": {}, "output_type": "display_data" }, { "name": "stdout", "output_type": "stream", "text": [ " [1/3] id=012023020100 items=3 time=37.3s\n", " [2/3] id=012023030200 items=3 time=12.4s\n", " [3/3] id=012024010300 items=3 time=9.7s\n", "\n", "β Wrote 3 rows β submission.csv\n" ] }, { "data": { "text/html": [ "\n", "
| \n", " | id | \n", "pred | \n", "explanation | \n", "
|---|---|---|---|
| 0 | \n", "012023020100 | \n", "[\"Will you sleep? / Do you sleep?\", \"Did I eat... | \n", "The sentences provided follow a pattern where ... | \n", "
| 1 | \n", "012023030200 | \n", "[\"6\", \"16\", \"228\"] | \n", "From the given translations and the base 6 num... | \n", "
| 2 | \n", "012024010300 | \n", "[\"1. kite = fish β fishes = kitenΓ‘\", \"2. nΓ£ka ... | \n", "From the examples, we can see that the plural ... | \n", "