Files
iolai26-solve/solver/metrics.py
ModelHub XC 5b016c1af1 初始化项目,由ModelHub XC社区提供模型
Model: rpant/iolai26-solve
Source: Original Platform
2026-07-28 09:36:12 +08:00

133 lines
4.6 KiB
Python

"""Metrics matching the official eval notebook (iolai-2026-workshop).
Lives in solver/ (not eval/) because the RUNTIME needs it: the verifier and
the fallback ladder score candidate answers with chrF/EM at inference time.
The dev-only eval/ package re-exports from here.
Semantics:
- EM: case-insensitive exact string equality after .strip() ONLY — punctuation
and internal whitespace are significant. Gold items may carry alternatives
(list of accepted strings); a hit on any alternative counts.
- chrF: sacrebleu CHRF() defaults (char n-grams 1..6, beta=2, whitespace not
in n-grams, epsilon-smoothed per order), 0..1 here (notebook prints 0..100).
Max over alternatives.
- Aggregate: per-item average of each metric; geometric mean reported as the
headline (the competition combines EM and chrF; the notebook prints both).
chrF is implemented in pure python replicating sacrebleu's algorithm so the
eval sandbox needs no dependency; tests/test_scorer.py checks parity against
sacrebleu when it is installed.
"""
from __future__ import annotations
import math
import re
import unicodedata
from collections import Counter
from typing import Dict, List, Optional, Sequence, Union
CHRF_NGRAM_ORDER = 6
CHRF_BETA = 2.0
_EPS = 1e-16
Gold = Union[str, Sequence[str]] # a gold item: one string or alternatives
def _alts(gold: Gold) -> List[str]:
if isinstance(gold, str):
return [gold]
return [str(a) for a in gold] or [""]
def normalize_answer(s: str) -> str:
"""Official EM normalization: strip + lowercase. Nothing else — final
punctuation and internal spacing are significant."""
return str(s).strip().lower()
def exact_match(pred: str, gold: Gold) -> float:
p = normalize_answer(pred)
return 1.0 if any(p == normalize_answer(a) for a in _alts(gold)) else 0.0
def _char_ngrams(s: str, n: int) -> Counter:
return Counter(s[i : i + n] for i in range(len(s) - n + 1))
def _chrf_single(pred: str, gold: str, n_order: int = CHRF_NGRAM_ORDER,
beta: float = CHRF_BETA) -> float:
"""Exact replication of sacrebleu CHRF defaults (whitespace stripped,
effective-order smoothing): precision/recall averaged over orders where
BOTH sides have n-grams; hypothesis counts are zeroed for orders the
reference lacks. Returns [0, 1] (sacrebleu reports x100)."""
pred_s = "".join(str(pred).split())
gold_s = "".join(str(gold).split())
avg_prec = avg_rec = 0.0
effective = 0
for n in range(1, n_order + 1):
gn = _char_ngrams(gold_s, n)
pn = _char_ngrams(pred_s, n)
n_ref = sum(gn.values())
n_hyp = sum(pn.values()) if gn else 0 # sacrebleu: no ref => no hyp hits
if n_hyp > 0 and n_ref > 0:
overlap = sum((pn & gn).values())
avg_prec += overlap / n_hyp
avg_rec += overlap / n_ref
effective += 1
if effective == 0:
return 0.0
avg_prec /= effective
avg_rec /= effective
if avg_prec + avg_rec == 0:
return 0.0
b2 = beta * beta
return (1 + b2) * avg_prec * avg_rec / (b2 * avg_prec + avg_rec)
def chrf(pred: str, gold: Gold) -> float:
return max(_chrf_single(pred, a) for a in _alts(gold))
def item_scores(pred: str, gold: Gold) -> Dict[str, float]:
return {"em": exact_match(pred, gold), "chrf": chrf(pred, gold)}
def score_submission(
preds: Sequence[Sequence[str]],
golds: Sequence[Sequence[Gold]],
weights: Optional[Sequence[Sequence[float]]] = None,
) -> Dict[str, float]:
"""Score a full submission.
preds/golds: per row, a list of items; each gold item is a string or a
list of accepted alternatives. weights: optional per-item point values;
uniform if None. Length mismatches within a row are penalized: missing
preds score 0, extra preds are ignored (notebook lines preds up by
position exactly the same way).
"""
total_w = 0.0
em_w = 0.0
chrf_w = 0.0
n_items = 0
for ri, (prow, grow) in enumerate(zip(preds, golds)):
wrow = list(weights[ri]) if weights is not None else [1.0] * len(grow)
for ii, gold in enumerate(grow):
w = wrow[ii] if ii < len(wrow) else 1.0
pred = prow[ii] if ii < len(prow) else ""
total_w += w
em_w += w * exact_match(pred, gold)
chrf_w += w * chrf(pred, gold)
n_items += 1
if total_w == 0:
return {"em": 0.0, "chrf": 0.0, "score": 0.0, "n_items": 0}
em_avg = em_w / total_w
chrf_avg = chrf_w / total_w
return {
"em": em_avg,
"chrf": chrf_avg,
"score": math.sqrt(em_avg * chrf_avg),
"n_items": n_items,
}