CoolFace
Modelpublic

rita-cohere/tya-m1-multilingual

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
1likes9downloads
script.py528 linesDownload Raw Back to root
1"""IOL-AI 2026 — M1 iterate on best private~0.083 (temp0.6 + user-instr).2 3Keep: user-prompt instructions, /think, sample think @ TEMPERATURE.4CSV fixes from that run: strip _GCY/gloss; reject essay+alphabet+resample;5stronger user prompt; think 2048; greedy answer continuation.6"""7 8import os9import subprocess10import sys11 12 13def _install_bundled_deps() -> None:14    wheels_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "wheels")15    if not os.path.isdir(wheels_dir):16        return17    subprocess.run(18        [19            sys.executable,20            "-m",21            "pip",22            "install",23            "-q",24            "--no-index",25            f"--find-links={wheels_dir}",26            "transformers==4.56.2",27        ],28        check=True,29    )30 31 32_install_bundled_deps()33 34os.environ["HF_HUB_OFFLINE"] = "1"35os.environ["TRANSFORMERS_OFFLINE"] = "1"36MODEL_ID = "."37 38# "" for A1 (reasoning_options only); "/think" for M1 multilingual39USER_THINK_TOKEN = "/think"40 41import json42import re43 44import pandas as pd45import torch46from transformers import AutoModelForCausalLM, AutoTokenizer47 48END_THINKING = "<|END_THINKING|>"49START_THINKING = "<|START_THINKING|>"50 51THINKING_BUDGET = 204852ANSWER_CONTINUATION_TOKENS = 51253COT_MAX_NEW_TOKENS = 102454TEMPERATURE = 0.655TOP_P = 0.9556QUALITY_RESAMPLES = 457ANSWER_GREEDY = True  # format-sensitive answer phase58 59# Empty system — instructions go on the user turn (decode ablation vs v4)60SYSTEM = ""61 62USER_INSTRUCTIONS = """You solve International Linguistics Olympiad (IOL) problems from the data you are given.63You may see a task type you have never seen: follow the instruction and examples, and answer in the same form they use.64 65What to return by task type:66- translation: only the required form in the language the query asks for — do not add extra glosses or "form | meaning" unless asked67- fill_blanks: only the missing form for each blank — no extra glosses68- match_letters: ONLY the option letter (A, B, C, …), one letter per line — never copy option text, never arrows, never "A. word"69- text_to_num: the number in digits only70- num_to_text: the number written out in words, in the language asked71- kinship / sentence matching: the full required sentence or form — NOT roman numerals (i, ii, iii) and NOT an alphabet dump72- any other type: exactly what the instruction asks for, nothing else73 74Answer in the language and form the query asks for. Do not add glosses, translations, or explanations unless the instruction requires them.75 76Output rules:77- Put answers ONLY after a line that says exactly: FINAL ANSWERS:78- Never put answers before that marker.79- One answer per line; exactly as many lines as items asked in the query.80- Bare answers only: no numbering, no quotes, no commentary, no repeating the question.81 82Extra hard rules:83- Never refuse or apologize; always output FINAL ANSWERS: with your best guess.84- For match_letters: only bare letters (A, B, C, …) — never dump the alphabet (A B C D E F…), never option text.85- Never append tags or glosses: no "_GCY", "_NS", "form – meaning", "word - gloss", or markdown bold.86- Never write an essay or explanation of how the language works under FINAL ANSWERS: — only the answer strings.87- If the query asks for colour/color forms, output those forms only (one per line), not a linguistics write-up.88- Emit exactly as many answer lines as items asked — no more, no fewer."""89 90USER_INSTRUCTIONS_COT = (91    USER_INSTRUCTIONS92    + "\n\nThink step by step about the rules in the examples and how they apply to the query, "93    "then write FINAL ANSWERS: and the answer lines."94)95 96# --- parser (inlined from parse_iol.py) ---97_MD_PREFIX = r"(?:[#*_=\-\s`>]*)"98_MARKER = re.compile(99    rf"(?im)^{_MD_PREFIX}final\s+answers?{_MD_PREFIX}:?{_MD_PREFIX}\s*(.*)$"100)101_NUMBERING = re.compile(r"^\s*(?:\d+[.)]|[-*•])\s*")102_TURN_NOISE = re.compile(103    r"<\|/?END_OF_TURN_TOKEN\|>|<\|/?START_OF_TURN_TOKEN\|>|"104    r"<\|CHATBOT_TOKEN\|>|<EOS_TOKEN>|<BOS_TOKEN>"105)106_RESPONSE_BLOCK = re.compile(107    r"<\|START_RESPONSE\|>(.*?)<\|END_RESPONSE\|>",108    flags=re.S,109)110_MD_WRAP = re.compile(r"^[*_`#\s]+|[*_`#\s]+$")111_TRAILING_LETTER = re.compile(112    r"(?:[–—\-]|→|->)\s*([A-Za-z])(?:\s*[.)]|)\s*$"113)114_LEADING_LETTER_OPT = re.compile(r"^([A-Za-z])\s*[.):\-–—]\s+\S")115_WORD_THEN_LETTER = re.compile(r"^.+\s([A-Za-z])\s*$")116_REFUSAL = re.compile(117    r"(?i)\b("118    r"i'?m sorry|i am sorry|i don'?t have|i cannot|i can'?t|"119    r"unable to|not able to|no reliable|cannot supply|can'?t supply|"120    r"as an ai|i apologize"121    r")\b"122)123 124 125def _strip_gloss_keep_form(line: str) -> str:126    s = (line or "").strip()127    s = re.sub(r"\*\*", "", s)128    s = re.split(r"\s+_?(?:GCY|NS|N/A)_?\b", s, maxsplit=1, flags=re.I)[0].strip()129    s = re.sub(r"\s+_?(?:GCY|NS|N/A)_?\s*$", "", s, flags=re.I).strip()130    m = re.match(131        r"^(.+?)\s+[-–—]\s+((?:to|the|a|an|in|of|for|being|means?|black|white|red|green|yellow)\b.*)$",132        s,133        flags=re.I,134    )135    if m:136        s = m.group(1).strip()137    return s.strip()138 139 140def _clean_line(line: str) -> str:141    line = _NUMBERING.sub("", line).strip()142    line = _MD_WRAP.sub("", line).strip()143    line = line.replace("\u202f", " ").replace("\xa0", " ")144    return _strip_gloss_keep_form(line.strip())145 146 147def after_thinking(text: str) -> str:148    """Prefer content after the last <|END_THINKING|>; else drop an unclosed think block."""149    if END_THINKING in text:150        text = text.rsplit(END_THINKING, 1)[-1]151    elif START_THINKING in text:152        text = ""153    return _TURN_NOISE.sub("", text)154 155 156def _as_option_letter(line: str) -> str | None:157    line = _clean_line(line)158    if not line:159        return None160    if len(line) == 1 and line.isalpha():161        return line.upper()162    m = _LEADING_LETTER_OPT.match(line)163    if m:164        return m.group(1).upper()165    m = _TRAILING_LETTER.search(line)166    if m:167        return m.group(1).upper()168    if len(line) <= 40:169        m = _WORD_THEN_LETTER.match(line)170        if m:171            return m.group(1).upper()172    return None173 174 175def _expand_line(line: str) -> list[str]:176    line = _clean_line(line)177    if not line:178        return []179    if len(line) == 1 and line.isalpha():180        return [line]181    if _LEADING_LETTER_OPT.match(line) or _TRAILING_LETTER.search(line):182        letter = _as_option_letter(line)183        if letter:184            return [letter]185    if len(line) <= 40 and _WORD_THEN_LETTER.match(line):186        letter = _as_option_letter(line)187        if letter:188            return [letter]189    if "|" in line:190        parts = [p.strip() for p in line.split("|") if p.strip()]191        if len(parts) >= 2:192            if len(parts) >= 4 and len(parts) % 2 == 0:193                left, right = parts[0::2], parts[1::2]194                if sum(" " in r for r in right) >= max(1, len(right) // 2):195                    return [_clean_line(x) for x in left if _clean_line(x)]196            if len(parts) == 2:197                a, b = parts198                if (" " in b and " " not in a) or (199                    len(b) > 2 * max(len(a), 1) and " " in b200                ):201                    return [_clean_line(a)] if _clean_line(a) else []202            return [_clean_line(p) for p in parts if _clean_line(p)]203    return [line]204 205 206def _dedupe_runaway(parts: list[str]) -> list[str]:207    if len(parts) < 6:208        return parts209    out: list[str] = []210    run = 0211    prev = None212    for p in parts:213        if p == prev:214            run += 1215            if run >= 4:216                break217        else:218            run = 1219            prev = p220        out.append(p)221    return out222 223 224def _lines_from_region(region: str, *, allow_all_lines: bool) -> list[str]:225    markers = list(_MARKER.finditer(region))226    if markers:227        last = markers[-1]228        after_parts: list[str] = []229        same = _clean_line(last.group(1) or "")230        if same:231            after_parts.extend(_expand_line(same))232        for line in region[last.end() :].splitlines():233            after_parts.extend(_expand_line(line))234        if after_parts:235            return _dedupe_runaway(after_parts)236        before_parts: list[str] = []237        for line in region[: last.start()].splitlines():238            before_parts.extend(_expand_line(line))239        if before_parts:240            return _dedupe_runaway(before_parts)241 242    parts: list[str] = []243    for line in region.splitlines():244        parts.extend(_expand_line(line))245    if not parts:246        return []247    if allow_all_lines:248        return _dedupe_runaway(parts)249    return [parts[-1]]250 251 252def parse_answers(253    raw: str,254    *,255    n_expected: int | None = None,256    task_type: str = "",257) -> list[str]:258    text = after_thinking(raw)259    closed_blocks = _RESPONSE_BLOCK.findall(text)260    answers: list[str] = []261    if closed_blocks:262        for region in reversed(closed_blocks):263            answers = _lines_from_region(region.strip(), allow_all_lines=True)264            if answers:265                break266    if not answers:267        answers = _lines_from_region(text, allow_all_lines=False)268 269    if task_type == "match_letters":270        coerced: list[str] = []271        for a in answers:272            letter = _as_option_letter(a)273            coerced.append(letter if letter else a)274        answers = coerced275 276    if n_expected is not None and n_expected > 0 and len(answers) > n_expected:277        answers = answers[:n_expected]278    return answers279 280 281def _looks_like_alphabet_dump(answers: list[str]) -> bool:282    letters = [a.strip().upper() for a in answers if len(a.strip()) == 1 and a.strip().isalpha()]283    if len(letters) < 8:284        return False285    seq = 0286    for i, L in enumerate(letters):287        if ord(L) == ord("A") + i:288            seq += 1289        else:290            break291    return seq >= 8292 293 294def _looks_like_essay(answers: list[str]) -> bool:295    if any(len(str(a)) > 100 for a in answers):296        return True297    blob = " ".join(map(str, answers))298    return bool(299        re.search(300            r"(?i)\b(colors? are expressed|systematic set of lexical|"301            r"these stems are combined|step by step|as an ai|verification)\b",302            blob,303        )304    )305 306 307def _looks_like_refusal(answers: list[str]) -> bool:308    blob = " ".join(answers)309    return bool(_REFUSAL.search(blob)) or len(blob) > 400 and "dictionary" in blob.lower()310 311 312def has_usable_answer(313    answers: list[str],314    *,315    n_expected: int | None = None,316    task_type: str = "",317) -> bool:318    if not answers or not any(a.strip() for a in answers):319        return False320    if _looks_like_refusal(answers):321        return False322    if _looks_like_alphabet_dump(answers):323        return False324    if _looks_like_essay(answers):325        return False326    if any(re.search(r"(?i)_GCY\b|_NS\b", str(a)) for a in answers):327        return False328    if task_type != "match_letters" and len(answers) >= 4:329        if all(re.fullmatch(r"[ivxlcdm]+", str(a).strip(), flags=re.I) for a in answers):330            return False331    if n_expected is not None and n_expected > 0 and abs(len(answers) - n_expected) > max(332        2, n_expected // 2333    ):334        return False335    if task_type == "match_letters":336        letters = [a for a in answers if len(a) == 1 and a.isalpha()]337        if len(letters) < max(1, int(0.8 * len(answers))):338            return False339    return True340 341 342def _n_items_guess(query: str) -> int:343    nums = re.findall(r"(?m)^\s*(?:\(?\d+[.)]|\d+\))", query)344    return len(nums) if nums else 0345 346 347def _end_thinking_id(tok) -> int:348    end_id = tok.convert_tokens_to_ids(END_THINKING)349    if end_id is None or end_id == tok.unk_token_id:350        ids = tok.encode(END_THINKING, add_special_tokens=False)351        if len(ids) == 1:352            end_id = ids[0]353    if end_id is None or end_id == tok.unk_token_id:354        raise RuntimeError(f"Tokenizer missing end-think token {END_THINKING!r}")355    return int(end_id)356 357 358def _build_prompt_ids(tok, system: str, user: str, *, thinking: bool):359    messages = []360    if system.strip():361        messages.append({"role": "system", "content": system})362    messages.append({"role": "user", "content": user})363    try:364        return tok.apply_chat_template(365            messages,366            add_generation_prompt=True,367            return_tensors="pt",368            reasoning_options={"enabled": thinking},369        )370    except TypeError:371        return tok.apply_chat_template(372            messages, add_generation_prompt=True, return_tensors="pt"373        )374 375 376def _sample_kwargs():377    return dict(378        do_sample=True,379        temperature=TEMPERATURE,380        top_p=TOP_P,381    )382 383 384@torch.inference_mode()385def generate_with_think_budget(model, tok, prompt_ids, end_id: int):386    device = next(model.parameters()).device387    prompt_ids = prompt_ids.to(device)388    prompt_len = prompt_ids.shape[-1]389    gen_kw = _sample_kwargs()390 391    think_out = model.generate(392        prompt_ids,393        max_new_tokens=THINKING_BUDGET,394        pad_token_id=tok.pad_token_id or tok.eos_token_id,395        **gen_kw,396    )[0]397    gen_ids = think_out[prompt_len:].tolist()398    if end_id not in gen_ids:399        cont = torch.cat(400            [think_out, torch.tensor([end_id], device=device, dtype=think_out.dtype)]401        )402    else:403        cont = think_out404 405    ans_kw = dict(do_sample=False) if ANSWER_GREEDY else gen_kw406    full = model.generate(407        cont.unsqueeze(0),408        max_new_tokens=ANSWER_CONTINUATION_TOKENS,409        pad_token_id=tok.pad_token_id or tok.eos_token_id,410        **ans_kw,411    )[0]412    text = tok.decode(full[prompt_len:], skip_special_tokens=False)413    return _TURN_NOISE.sub("", text).strip()414 415 416@torch.inference_mode()417def generate_plain(model, tok, prompt_ids, max_new_tokens: int):418    device = next(model.parameters()).device419    prompt_ids = prompt_ids.to(device)420    prompt_len = prompt_ids.shape[-1]421    out = model.generate(422        prompt_ids,423        max_new_tokens=max_new_tokens,424        pad_token_id=tok.pad_token_id or tok.eos_token_id,425        **_sample_kwargs(),426    )[0]427    text = tok.decode(out[prompt_len:], skip_special_tokens=False)428    return _TURN_NOISE.sub("", text).strip()429 430 431def _build_user(432    instructions: str,433    context: str,434    query: str,435    *,436    n_guess: int,437    think_token: str = "",438) -> str:439    parts = [440        instructions.strip(),441        "",442        context.strip(),443        "",444        query.strip(),445    ]446    if n_guess:447        parts.append("")448        parts.append(f"(Emit exactly {n_guess} answer line(s) after FINAL ANSWERS:.)")449    if think_token:450        parts.append(think_token.strip())451    return "\n".join(parts)452 453 454tok = AutoTokenizer.from_pretrained(MODEL_ID)455end_id = _end_thinking_id(tok)456model = AutoModelForCausalLM.from_pretrained(457    MODEL_ID, torch_dtype=torch.float16, device_map="auto"458).eval()459 460df = pd.read_csv("/tmp/data/test.csv", dtype=str).fillna("")461 462rows = []463for i, r in df.iterrows():464    n_guess = _n_items_guess(r["query"])465    task = str(r.get("task_type", "") or "")466    n_exp = n_guess or None467 468    user_think = _build_user(469        USER_INSTRUCTIONS,470        r["context"],471        r["query"],472        n_guess=n_guess,473        think_token=USER_THINK_TOKEN,474    )475    user_plain = _build_user(476        USER_INSTRUCTIONS_COT,477        r["context"],478        r["query"],479        n_guess=n_guess,480        think_token="",481    )482 483    best_answers: list[str] = []484    used_cot = False485    for attempt in range(QUALITY_RESAMPLES):486        torch.manual_seed(1000 + int(i) * 97 + attempt * 31)487        if torch.cuda.is_available():488            torch.cuda.manual_seed_all(1000 + int(i) * 97 + attempt * 31)489        ids = _build_prompt_ids(tok, SYSTEM, user_think, thinking=True)490        text = generate_with_think_budget(model, tok, ids, end_id)491        answers = parse_answers(text, n_expected=n_exp, task_type=task)492        answers = [_strip_gloss_keep_form(a) for a in answers if _strip_gloss_keep_form(a)]493        if n_exp and n_exp > 0 and len(answers) > n_exp:494            answers = answers[:n_exp]495        if has_usable_answer(answers, n_expected=n_exp, task_type=task):496            best_answers = answers497            break498        if answers and not best_answers:499            best_answers = answers500        print(501            f"  quality resample {attempt + 1}/{QUALITY_RESAMPLES} id={r['id']} n={len(answers)}",502            flush=True,503        )504    answers = best_answers505 506    if not has_usable_answer(answers, n_expected=n_exp, task_type=task):507        used_cot = True508        # CoT: no /think, thinking channel off, capped budget509        cot_ids = _build_prompt_ids(tok, SYSTEM, user_plain, thinking=False)510        cot_text = generate_plain(model, tok, cot_ids, COT_MAX_NEW_TOKENS)511        cot_answers = parse_answers(cot_text, n_expected=n_exp, task_type=task)512        cot_answers = [513            _strip_gloss_keep_form(a) for a in cot_answers if _strip_gloss_keep_form(a)514        ]515        if has_usable_answer(cot_answers, n_expected=n_exp, task_type=task) or (516            cot_answers and not answers517        ):518            answers = cot_answers519 520    rows.append({"id": r["id"], "pred": json.dumps(answers, ensure_ascii=False)})521    print(522        f"[{i + 1}/{len(df)}] {len(answers)} answers cot={used_cot}",523        flush=True,524    )525 526pd.DataFrame(rows).to_csv("submission.csv", index=False)527print("wrote submission.csv", flush=True)528