"""Deterministic scorers. LongBench-style normalization for QA F1/EM; numeric with tolerance; yes/no; abstention.""" from __future__ import annotations import re, string from collections import Counter ABSTAIN_PATTERNS = [r"\bunknown\b", r"\bnot (?:stated|mentioned|provided|specified|given|available)\b", r"\bdoes not (?:say|state|mention|specify)\b", r"\bcannot be determined\b", r"\bno information\b", r"\bnot in the document\b", r"\bi don't know\b", r"\bnot found\b", r"\binsufficient information\b"] def normalize_answer(s: str) -> str: s = s.lower() s = "".join(ch for ch in s if ch not in set(string.punctuation)) s = re.sub(r"\b(a|an|the)\b", " ", s) return " ".join(s.split()) def f1_score(pred: str, gold: str) -> float: p, g = normalize_answer(pred).split(), normalize_answer(gold).split() common = Counter(p) & Counter(g) ns = sum(common.values()) if ns == 0: return 0.0 prec, rec = ns / len(p), ns / len(g) return 2 * prec * rec / (prec + rec) def qa_f1(pred: str, golds: list[str]) -> float: return max(f1_score(pred, g) for g in golds) if golds else 0.0 def exact_match(pred: str, golds: list[str]) -> float: n = normalize_answer(pred) return float(any(n == normalize_answer(g) for g in golds)) def contains_match(pred: str, golds: list[str]) -> float: n = normalize_answer(pred) return float(any(normalize_answer(g) in n for g in golds if normalize_answer(g))) _num_re = re.compile(r"[-+]?(?:\d{1,3}(?:[,  ]\d{3})+|\d+)(?:\.\d+)?") def extract_numbers(s: str) -> list[float]: out = [] for m in _num_re.finditer(s): t = m.group(0).replace(",", "").replace(" ", "").replace(" ", "") try: out.append(float(t)) except ValueError: pass return out def numeric_match(pred: str, gold: str, tol: float = 0.0) -> float: g = extract_numbers(gold) if not g: return exact_match(pred, [gold]) gv = g[0] nums = extract_numbers(pred) if not nums: return 0.0 # accept if the first number (or the only number) matches; penalize answers listing many numbers by requiring first or last cands = [nums[0], nums[-1]] if len(nums) > 1 else nums for v in cands: if gv == 0: if abs(v) <= tol: return 1.0 elif abs(v - gv) <= max(tol * abs(gv), 1e-9): return 1.0 return 0.0 def yesno_match(pred: str, gold: str) -> float: n = normalize_answer(pred) toks = n.split() has_yes = "yes" in toks[:3]; has_no = "no" in toks[:3] or n.startswith("not ") if has_yes and not has_no: return float(gold == "yes") if has_no and not has_yes: return float(gold == "no") return 0.0 def is_abstention(pred: str) -> bool: n = pred.lower() return any(re.search(p, n) for p in ABSTAIN_PATTERNS) def score_synthetic(pred: str, q: dict) -> dict: at = q["answer_type"] if at == "numeric": s = numeric_match(pred, q["answer"], q.get("tolerance", 0.0)) elif at == "yesno": s = yesno_match(pred, q["answer"]) elif at == "abstain": s = float(is_abstention(pred)) else: golds = [q["answer"]] + list(q.get("aliases", [])) s = exact_match(pred, golds) if s == 0.0: s = contains_match(pred, golds) # lenient containment counts as correct for exact-type answers return {"correct": s, "abstained": float(is_abstention(pred)), "f1": qa_f1(pred, [q["answer"]])} def score_public(pred: str, golds: list[str]) -> dict: return {"f1": qa_f1(pred, golds), "em": exact_match(pred, golds), "abstained": float(is_abstention(pred))}