Files
iol-solver-qwen3/script.py
ModelHub XC 2027d13392 初始化项目,由ModelHub XC社区提供模型
Model: Santhoshini/iol-solver-qwen3
Source: Original Platform
2026-07-20 00:22:36 +08:00

449 lines
22 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os, sys, subprocess, importlib, importlib.metadata, unicodedata
from pathlib import Path
os.environ.setdefault("HF_HUB_OFFLINE","1"); os.environ.setdefault("TRANSFORMERS_OFFLINE","1")
SCRIPT_DIR = Path(__file__).resolve().parent if "__file__" in globals() else Path.cwd()
WHEELHOUSE = SCRIPT_DIR / "wheelhouse"
if not WHEELHOUSE.is_dir(): WHEELHOUSE = Path("wheelhouse")
RUNTIME_DIR = Path("/tmp/qwen3deps")
def emergency(reason):
try:
import pandas as pd, json as j
try: ids = pd.read_csv("/tmp/data/test.csv", dtype=str).fillna("")["id"].tolist()
except Exception: ids = []
pd.DataFrame([{"id":i,"pred":j.dumps([""]),"explanation":str(reason)[:100]} for i in ids],
columns=["id","pred","explanation"]).to_csv("submission.csv", index=False)
except Exception:
try: open("submission.csv","w").write("id,pred,explanation\n")
except Exception: pass
try:
wheels = [str(WHEELHOUSE / w) for w in os.listdir(WHEELHOUSE) if w.endswith(".whl")]
if not wheels: raise FileNotFoundError(f"no wheels in {WHEELHOUSE}")
RUNTIME_DIR.mkdir(parents=True, exist_ok=True)
subprocess.run([sys.executable,"-m","pip","install","--no-index","--no-deps","--upgrade",
"--target",str(RUNTIME_DIR)] + wheels, check=True, timeout=300)
sys.path.insert(0, str(RUNTIME_DIR)); importlib.invalidate_caches()
try: print("transformers:", importlib.metadata.version("transformers"), flush=True)
except Exception: pass
except Exception as e:
emergency(f"wheel install failed: {e}"); raise
import re, json, time
import pandas as pd, torch
from transformers import AutoTokenizer, AutoModelForCausalLM
import ast as _ast, hashlib as _hash
from fractions import Fraction as _Frac
from collections import OrderedDict as _OD
MODEL_ID="."; TIME_LIMIT=30*60; start=time.time()
def write_csv(rows):
import csv
with open("submission.csv.tmp","w",newline="",encoding="utf-8") as f:
w=csv.DictWriter(f,fieldnames=["id","pred","explanation"]); w.writeheader()
for r in rows: w.writerow(r)
os.replace("submission.csv.tmp","submission.csv")
try:
df = pd.read_csv("/tmp/data/test.csv", dtype=str).fillna("")
write_csv([{"id":i,"pred":json.dumps([""]),"explanation":"placeholder"} for i in df["id"]])
tok = AutoTokenizer.from_pretrained(MODEL_ID, local_files_only=True)
model = AutoModelForCausalLM.from_pretrained(MODEL_ID, torch_dtype=torch.float16,
device_map="auto", local_files_only=True).eval()
print("loaded, quantized:", getattr(model.config,"quantization_config",None) is not None, flush=True)
except Exception as e:
emergency(f"load failed: {e}"); raise
SYS=("You solve International Linguistics Olympiad problems about a language you have never seen. "
"Everything you need is in the examples. Answer every numbered item, in order. "
"Put each answer on its own line, with no numbering and no extra text.")
def n_expected(query):
items=re.findall(r"(?m)^\s*(\d+)\s*[.\)]", query)
if items: return len(items)
rng=re.search(r"\(\s*(\d+)\s*[-–—]\s*(\d+)\s*\)", query)
if rng:
lo,hi=int(rng.group(1)),int(rng.group(2))
if 0<hi-lo<100: return hi-lo+1
return None
def norm_generic(s):
s = unicodedata.normalize("NFC", s).strip()
s = re.sub(r"^\s*\(?\d+\)?\s*[.):\-]\s+", "", s)
s = re.sub(r"^\s*[-*•·]\s+", "", s)
s = re.sub(r"(?i)^\s*(answer|translation|output)\s*\d*\s*[:.\-]\s+", "", s)
s = s.strip("* ")
s = re.sub(r"\s{2,}", " ", s)
return s.strip()
def norm_number(s):
g = norm_generic(s)
m = re.search(r"-?\d[\d,\. ]*\d|\d", g)
if not m: return g
digits = re.sub(r"[^\d-]", "", m.group(0))
return digits if digits else g
def normalize(task_type, s):
if task_type == "text_to_num":
return norm_number(s)
return norm_generic(s)
def gen(context, query):
msgs=[{"role":"system","content":SYS},
{"role":"user","content":f"{context.strip()}\n\n{query.strip()}"}]
try:
enc=tok.apply_chat_template(msgs,add_generation_prompt=True,enable_thinking=False,
return_tensors="pt",return_dict=True).to(model.device)
ilen=enc["input_ids"].shape[-1]
with torch.no_grad(): out=model.generate(**enc,max_new_tokens=512,do_sample=False)
except Exception:
ids=tok.apply_chat_template(msgs,add_generation_prompt=True,enable_thinking=False,
return_tensors="pt").to(model.device)
ilen=ids.shape[-1]
with torch.no_grad(): out=model.generate(ids,max_new_tokens=512,do_sample=False)
return tok.decode(out[0][ilen:],skip_special_tokens=True).strip()
rows=[]; done=set()
try:
for _,r in df.iterrows():
try:
task_type = r.get("task_type","")
text=gen(r["context"],r["query"])
ans=[normalize(task_type, ln) for ln in text.splitlines() if ln.strip()]
n=n_expected(r["query"])
if n:
if len(ans)<n: ans=ans+[ans[-1] if ans else ""]*(n-len(ans))
elif len(ans)>n: ans=ans[:n]
if not ans: ans=[""]
expl=re.sub(r"\s+"," ",text[:300]).strip() or "derived from the examples"
rows.append({"id":r["id"],"pred":json.dumps(ans,ensure_ascii=False),"explanation":expl})
except Exception as e:
n=n_expected(r["query"]) or 1
rows.append({"id":r["id"],"pred":json.dumps([""]*n,ensure_ascii=False),"explanation":"fallback"})
print("row error",r["id"],e,flush=True)
done.add(r["id"]); write_csv(rows)
print(f"{len(rows)}/{len(df)} t={time.time()-start:.0f}s",flush=True)
if time.time()-start>TIME_LIMIT-60:
print("time up, stopping",flush=True); break
for _,r in df.iterrows():
if r["id"] in done: continue
n=n_expected(r["query"]) or 1
rows.append({"id":r["id"],"pred":json.dumps([""]*n,ensure_ascii=False),"explanation":"fallback"})
# ==========================================================================
# PASS 2: Grammar induction consensus.
# Appended after the proven 0.121 baseline completes. The submission.csv
# already has valid answers at this point. Pass 2 only IMPROVES rows where
# two independent grammar inductions agree exactly -- never empties them.
# ==========================================================================
_P2_INDUCTION_SYS = (
"You study an International Linguistics Olympiad problem. "
"From the examples only, write a RULE SHEET: with 3-8 bullet points "
"covering the grammar: word meanings, word order, morphology, numeral "
"composition, and exact-form constraints. "
"Do not answer the queries. List only rules verifiable from examples."
)
_P2_APPLICATION_SYS = (
"Apply the rule sheet to the IOL queries. "
"Use only the rule sheet and examples. "
"Follow the exact output format. No alternatives, no explanations."
)
_P2_SOFT_DEADLINE = 1620
_P2_RULE_CAP = 90
_P2_APPLY_CAP = 110
_P2_MIN_T = 25
_P2_SAMPLES = 2
_THINK_RE2 = re.compile(r"</?think\b", re.I)
_FINAL_RE2 = re.compile(r"^FINAL\s+ANSWERS\s*:\s*$", re.I)
_CHKRE2 = re.compile(r"^ARITHMETIC\s+CHECKS\s*:\s*$", re.I)
_BEGIN_RE2 = re.compile(r"^BEGIN\s+(ROW_[1-9]\d*)\s*$", re.I)
_END_RE2 = re.compile(r"^END\s+(ROW_[1-9]\d*)\s*$", re.I)
_JUNK2 = re.compile(r"^(?:note|explanation|reason(?:ing)?|answers?|here\s+(?:are|is))\s*:", re.I)
_FMT2 = re.compile(r"^(?:(?:\d{1,3}[.)]|\(\d{1,3}\))(?:\s+|(?=[^\d]))|[-*•]\s+)")
_SAFE_NUM2 = re.compile(r"^[0-9\s.,;+\-*/^=()\[\]{}×÷·\u2212]+$")
_OPT_RE2 = re.compile(r"(?m)^\s*([A-Za-z])[.)]\s")
def _p2_elapsed():
return time.time() - start
def _p2_field(r, k):
v = r.get(k, ""); return "" if v is None else str(v).strip()
def _p2_classify(r):
d = _p2_field(r,"task_type").lower().replace("-","_")
if d in {"translation","fill_blanks","match_letters","text_to_num","num_to_text"}: return d
q = _p2_field(r,"query").lower()
if "fill" in q and "blank" in q: return "fill_blanks"
if "correspondence" in q or ("match" in q and "letter" in q): return "match_letters"
if re.search(r"\bwrite\s+(?:in|as)\s+digits?\b", q): return "text_to_num"
if re.search(r"\bwrite\s+out\b", q): return "num_to_text"
return "translation"
def _p2_n(r):
return n_expected(_p2_field(r,"query"))
def _p2_opt_labels(r):
if _p2_classify(r) != "match_letters": return None
for src in (_p2_field(r,"query"), _p2_field(r,"context")):
labels = _OPT_RE2.findall(src)
numbered = len(re.findall(r"(?m)^\s*\d+[.)]\s+", src))
if len(labels)>=2 and len(set(labels))==len(labels) and numbered==len(labels):
return tuple(labels)
return None
def _p2_safe_eval(expr):
expr = (expr.replace("×","*").replace("÷","/").replace("·","*")
.replace("\u2212","-").replace("^","**").strip())
if not expr or len(expr)>120: return None
try: tree = _ast.parse(expr, mode="eval")
except (SyntaxError,ValueError): return None
def ev(n):
if isinstance(n,_ast.Expression): return ev(n.body)
if isinstance(n,_ast.Constant) and isinstance(n.value,(int,float)) and not isinstance(n.value,bool):
return _Frac(str(n.value))
if isinstance(n,_ast.UnaryOp) and isinstance(n.op,(_ast.UAdd,_ast.USub)):
v=ev(n.operand); return -v if isinstance(n.op,_ast.USub) else v
if isinstance(n,_ast.BinOp):
l,r=ev(n.left),ev(n.right); op=n.op
if isinstance(op,_ast.Add): return l+r
if isinstance(op,_ast.Sub): return l-r
if isinstance(op,_ast.Mult): return l*r
if isinstance(op,_ast.Div):
if r==0: raise ValueError
return l/r
if isinstance(op,_ast.Pow):
if r.denominator!=1 or not 0<=r.numerator<=10: raise ValueError
return l**r.numerator
raise ValueError
try:
v=ev(tree)
return None if abs(v.numerator)>10**15 else v
except Exception: return None
def _p2_verify_arith(answers, checks):
if len(answers)!=len(checks): return False
for ans,chk in zip(answers,checks):
if chk.count("=")!=1: return False
l,r=chk.split("=",1)
lv,rv=_p2_safe_eval(l),_p2_safe_eval(r)
if lv is None or rv is None or lv!=rv: return False
av=_p2_safe_eval(ans)
if av is None or av!=rv: return False
return True
def _p2_invalid(s):
return bool(s.startswith("```") or _THINK_RE2.search(s) or _FINAL_RE2.match(s)
or _CHKRE2.match(s) or _FMT2.match(s) or _JUNK2.match(s)
or (s.startswith("<") and s.endswith(">"))
or s.casefold() in {"n/a","unknown","?","-"})
def _p2_parse_block(text, n):
if not text or n<=0 or _THINK_RE2.search(text) or "```" in text: return None
lines=[l.strip() for l in text.splitlines()]
marks=[i for i,l in enumerate(lines) if _FINAL_RE2.match(l)]
if len(marks)!=1: return None
m=marks[0]
if any(l for l in lines[:m]): return None
answers=[l for l in lines[m+1:] if l]
if len(answers)!=n or any(_p2_invalid(a) for a in answers): return None
chk_marks=[i for i,l in enumerate(lines[:m]) if _CHKRE2.match(l)]
checks=None
if chk_marks:
if len(chk_marks)!=1 or any(lines[:chk_marks[0]]): return None
checks=[l for l in lines[chk_marks[0]+1:m] if l]
if len(checks)!=n: return None
return (answers, checks)
def _p2_parse_group(text, counts):
if not text: return {}
lines=[l.strip() for l in text.splitlines()]
results={}; seen=set(); dups=set()
for si,line in enumerate(lines):
bm=_BEGIN_RE2.match(line)
if not bm: continue
ordinal=int(bm.group(1).split("_",1)[1])-1
if not 0<=ordinal<len(counts): continue
if ordinal in seen: dups.add(ordinal)
seen.add(ordinal)
handle=f"ROW_{ordinal+1}".casefold()
end_idx=None
for j in range(si+1,len(lines)):
if _BEGIN_RE2.match(lines[j]): break
em=_END_RE2.match(lines[j])
if em and em.group(1).casefold()==handle: end_idx=j; break
if end_idx is None: continue
parsed=_p2_parse_block("\n".join(lines[si+1:end_idx]),counts[ordinal])
if ordinal not in dups and parsed is not None: results[ordinal]=parsed
for d in dups: results.pop(d,None)
return results
def _p2_validate(grow, parsed, counts):
valid={}
for ordinal,(answers,checks) in parsed.items():
if len(answers)!=counts[ordinal]: continue
r=grow[ordinal]; fam=_p2_classify(r)
if fam=="text_to_num":
if not all(any(c.isdigit() for c in a) for a in answers): continue
if not all(_SAFE_NUM2.fullmatch(a) for a in answers): continue
if checks is not None and not _p2_verify_arith(answers,checks): continue
if fam=="match_letters":
labels=_p2_opt_labels(r)
if labels is None: continue
if len(set(answers))!=len(answers): continue
if any(a not in labels for a in answers): continue
valid[ordinal]=answers
mords=[o for o,r in enumerate(grow) if _p2_classify(r)=="match_letters"]
if mords:
contracts=[_p2_opt_labels(grow[o]) for o in mords]
contracts=[c for c in contracts if c is not None]
if contracts and all(c==contracts[0] for c in contracts):
labels=contracts[0]
if sum(counts[o] for o in mords)==len(labels):
if any(o not in valid for o in mords):
for o in mords: valid.pop(o,None)
else:
flat=[a for o in mords for a in valid[o]]
if len(set(flat))!=len(flat) or set(flat)!=set(labels):
for o in mords: valid.pop(o,None)
return valid
def _p2_consensus(row, baseline, n, candidates):
valid=[c for c in candidates if c is not None and len(c)==n]
if len(valid)<2: return baseline
a,b=valid[0],valid[1]
if _p2_classify(row)=="match_letters" or len(baseline)!=n:
return a if a==b else baseline
return [a[i] if a[i]==b[i] else baseline[i] for i in range(n)]
def _p2_greedy(msgs, max_tok, time_limit=None):
try:
enc=tok.apply_chat_template(msgs,add_generation_prompt=True,enable_thinking=False,
return_tensors="pt",return_dict=True).to(model.device)
ilen=enc["input_ids"].shape[-1]
kw={"max_new_tokens":min(max_tok,40960-ilen-1),"do_sample":False}
if time_limit: kw["max_time"]=time_limit
with torch.no_grad(): out=model.generate(**enc,**kw)
except Exception:
ids=tok.apply_chat_template(msgs,add_generation_prompt=True,enable_thinking=False,
return_tensors="pt").to(model.device)
ilen=ids.shape[-1]
kw={"max_new_tokens":min(max_tok,40960-ilen-1),"do_sample":False}
if time_limit: kw["max_time"]=time_limit
with torch.no_grad(): out=model.generate(ids,**kw)
decoded=tok.decode(out[0][ilen:],skip_special_tokens=True).strip()
return decoded if decoded else None
def _p2_sampled(msgs, max_tok, time_limit, seed):
torch.manual_seed(seed)
if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)
try:
enc=tok.apply_chat_template(msgs,add_generation_prompt=True,enable_thinking=False,
return_tensors="pt",return_dict=True).to(model.device)
ilen=enc["input_ids"].shape[-1]
with torch.no_grad(): out=model.generate(**enc,
max_new_tokens=min(max_tok,40960-ilen-1),max_time=time_limit,
do_sample=True,temperature=0.7,top_p=0.8,top_k=20)
except Exception:
ids=tok.apply_chat_template(msgs,add_generation_prompt=True,enable_thinking=False,
return_tensors="pt").to(model.device)
ilen=ids.shape[-1]
with torch.no_grad(): out=model.generate(ids,
max_new_tokens=min(max_tok,40960-ilen-1),max_time=time_limit,
do_sample=True,temperature=0.7,top_p=0.8,top_k=20)
decoded=tok.decode(out[0][ilen:],skip_special_tokens=True).strip()
return decoded if decoded else None
def _p2_budget(remaining, cap):
if remaining<=0: return 0.0
avail=_P2_SOFT_DEADLINE-_p2_elapsed()
return max(0.0,min(cap,avail/remaining)) if avail>0 else 0.0
def _p2_seed(grow, idx):
payload=json.dumps([{"c":_p2_field(r,"context")[:200]} for r in grow[:1]],sort_keys=True,separators=(",",":"))
h=_hash.sha256(f"{idx}:{payload}".encode()).digest()
return int.from_bytes(h[:8],"big")%(2**31)
def _p2_guidance(grow):
fams={_p2_classify(r) for r in grow}
parts=[]
if "match_letters" in fams: parts.append("Matching: complete one-to-one correspondence.")
if "text_to_num" in fams: parts.append("Numbers: composition rules, verify arithmetic.")
if "translation" in fams: parts.append("Translation: vocabulary, word order, morphology.")
if "fill_blanks" in fams: parts.append("Blanks: exact morphological transformation.")
if "num_to_text" in fams: parts.append("Numeral construction: base, order, word forms.")
return " ".join(parts)
def _p2_induction_msgs(grow):
ctx=_p2_field(grow[0],"context")
hints=[f"[{i}; {_p2_classify(r)}]\n{_p2_field(r,'query')}" for i,r in enumerate(grow,1)]
return [{"role":"system","content":_P2_INDUCTION_SYS},
{"role":"user","content":(f"Focus: {_p2_guidance(grow)}\n\nEXAMPLES:\n{ctx}\n\n"
f"QUERIES (do not answer):\n"+"\n\n".join(hints))}]
def _p2_output_spec(r):
fam=_p2_classify(r)
if fam=="match_letters": return "One option label per line. Complete bijection."
if fam=="text_to_num": return "Digits only. Optional ARITHMETIC CHECKS: block before FINAL ANSWERS:."
if fam=="fill_blanks": return "One filled form per blank."
if fam=="num_to_text": return "One written numeral per item."
return "One translation per item. Exact surface form."
def _p2_app_msgs(grow, rules, counts):
ctx=_p2_field(grow[0],"context")
blocks=[f"ROW_{i} ({_p2_classify(r)}, {n} answers):\n{_p2_field(r,'query')}\nFormat: {_p2_output_spec(r)}"
for i,(r,n) in enumerate(zip(grow,counts),1)]
shapes=[f"BEGIN ROW_{i}\nFINAL ANSWERS:\n<{n} lines>\nEND ROW_{i}"
for i,n in enumerate(counts,1)]
return [{"role":"system","content":_P2_APPLICATION_SYS},
{"role":"user","content":(f"EXAMPLES:\n{ctx}\n\nRULE SHEET:\n{rules}\n\n"
f"QUERIES:\n"+"\n\n".join(blocks)+"\n\nExact structure:\n"+"\n\n".join(shapes))}]
def _p2_clean_rules(text):
text=(text or "").strip()
if not text or _THINK_RE2.search(text) or "```" in text: return None
if re.search(r"FINAL\s+ANSWERS\s*:",text,re.I): return None
lines=[l.rstrip() for l in text.splitlines()]
if lines and re.match(r"^RULE\s+SHEET\s*:?$",lines[0].strip(),re.I): lines=lines[1:]
text="\n".join(lines).strip()
return text if len(text)>=15 else None
# Group rows by shared context, sort by priority
_p2_grps=_OD()
for _p2_ri,(_,_p2_r) in enumerate(df.iterrows()):
_p2_grps.setdefault(_p2_field(_p2_r,"context"),[]).append(_p2_ri)
def _p2_prio(g):
fams={_p2_classify(df.iloc[i]) for i in g}
if fams&{"match_letters","text_to_num"}: fp=0
elif fams&{"fill_blanks","num_to_text"}: fp=1
else: fp=2
return fp,-len(g),g[0]
_p2_all=sorted(_p2_grps.values(),key=_p2_prio)
_p2_rem=len(_p2_all)*_P2_SAMPLES*2
_p2_avail=_P2_SOFT_DEADLINE-_p2_elapsed()
_p2_max=int(_p2_avail/(_P2_SAMPLES*2*_P2_MIN_T)) if _p2_avail>0 else 0
_p2_planned=_p2_all[:_p2_max]
print(f"pass2: {len(_p2_planned)}/{len(_p2_all)} groups t={_p2_elapsed():.0f}s",flush=True)
for _p2_gn,_p2_group in enumerate(_p2_planned,1):
if _p2_elapsed()>_P2_SOFT_DEADLINE: break
_p2_grow=[df.iloc[i] for i in _p2_group]
_p2_counts=[_p2_n(r) or len(json.loads(rows[i]["pred"])) for i,r in zip(_p2_group,_p2_grow)]
if any(c<=0 for c in _p2_counts): _p2_rem-=_P2_SAMPLES*2; continue
_p2_cands=[[] for _ in _p2_group]
for _p2_s in range(_P2_SAMPLES):
_p2_rt=_p2_budget(_p2_rem,_P2_RULE_CAP); _p2_rem-=1
if _p2_rt<_P2_MIN_T: _p2_rem=0; break
_p2_rules=None
try:
_p2_rules=_p2_clean_rules(_p2_sampled(_p2_induction_msgs(_p2_grow),640,_p2_rt,_p2_seed(_p2_grow,_p2_s)) or "")
except Exception as _e: print(f"rule g{_p2_gn} s{_p2_s}: {_e}",flush=True)
_p2_at=_p2_budget(_p2_rem,_P2_APPLY_CAP); _p2_rem-=1
_p2_val={}
if _p2_rules and _p2_at>=_P2_MIN_T:
try:
_p2_raw=_p2_greedy(_p2_app_msgs(_p2_grow,_p2_rules,_p2_counts),1536,_p2_at)
_p2_val=_p2_validate(_p2_grow,_p2_parse_group(_p2_raw or "",_p2_counts),_p2_counts)
except Exception as _e: print(f"apply g{_p2_gn} s{_p2_s}: {_e}",flush=True)
for _p2_j in range(len(_p2_group)): _p2_cands[_p2_j].append(_p2_val.get(_p2_j))
_p2_chg=0
for _p2_j,_p2_idx in enumerate(_p2_group):
_p2_bl=json.loads(rows[_p2_idx]["pred"])
_p2_fin=_p2_consensus(_p2_grow[_p2_j],_p2_bl,_p2_counts[_p2_j],_p2_cands[_p2_j])
if _p2_fin!=_p2_bl: _p2_chg+=1; rows[_p2_idx]["pred"]=json.dumps(_p2_fin,ensure_ascii=False)
write_csv(rows)
print(f"pass2 g{_p2_gn}/{len(_p2_planned)} chg={_p2_chg} t={_p2_elapsed():.0f}s",flush=True)
if _p2_rem<=0: break
write_csv(rows); print("DONE",flush=True)
except Exception as e:
emergency(f"main loop: {e}"); print("FATAL",e,flush=True)