evalsuite-python 0.3.1__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- evalsuite/__init__.py +5 -1
- evalsuite/benchmarks.py +157 -6
- evalsuite/cli/main.py +31 -1
- evalsuite/plot.py +146 -0
- evalsuite/stats/_resolve.py +23 -3
- evalsuite/stats/compare.py +7 -3
- evalsuite/text/__init__.py +152 -0
- evalsuite/text/_common.py +94 -0
- evalsuite/text/factuality.py +469 -0
- evalsuite/text/generation.py +1126 -0
- evalsuite/text/judge.py +532 -0
- evalsuite/text/qa.py +108 -0
- evalsuite/text/rag.py +235 -0
- evalsuite/text/reasoning.py +215 -0
- evalsuite/text/report.py +158 -0
- evalsuite/text/retrieval.py +238 -0
- evalsuite/text/semantic.py +465 -0
- evalsuite/text/structured.py +789 -0
- evalsuite/version.py +1 -1
- {evalsuite_python-0.3.1.dist-info → evalsuite_python-0.4.0.dist-info}/METADATA +59 -22
- {evalsuite_python-0.3.1.dist-info → evalsuite_python-0.4.0.dist-info}/RECORD +24 -12
- {evalsuite_python-0.3.1.dist-info → evalsuite_python-0.4.0.dist-info}/WHEEL +0 -0
- {evalsuite_python-0.3.1.dist-info → evalsuite_python-0.4.0.dist-info}/entry_points.txt +0 -0
- {evalsuite_python-0.3.1.dist-info → evalsuite_python-0.4.0.dist-info}/licenses/LICENSE +0 -0
evalsuite/__init__.py
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
>>> print(result.summary()) # doctest: +SKIP
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from . import calibration, classification, clinical, plot, regression, stats, vision
|
|
8
|
+
from . import calibration, classification, clinical, plot, regression, stats, text, vision
|
|
9
9
|
from .api import evaluate
|
|
10
10
|
from .calibration import (
|
|
11
11
|
CalibrationReport,
|
|
@@ -110,6 +110,8 @@ from .stats import (
|
|
|
110
110
|
t_test,
|
|
111
111
|
wilcoxon_test,
|
|
112
112
|
)
|
|
113
|
+
from .text import * # noqa: F403 (every LLM-evaluation function, listed in text.__all__)
|
|
114
|
+
from .text import __all__ as _text_all
|
|
113
115
|
from .version import __version__
|
|
114
116
|
from .vision import (
|
|
115
117
|
DetectionReport,
|
|
@@ -134,6 +136,7 @@ from .vision import (
|
|
|
134
136
|
)
|
|
135
137
|
|
|
136
138
|
__all__ = [
|
|
139
|
+
"text",
|
|
137
140
|
"SegmentationReport",
|
|
138
141
|
"segmentation_report",
|
|
139
142
|
"detection_pr_curve",
|
|
@@ -258,3 +261,4 @@ __all__ = [
|
|
|
258
261
|
"specificity",
|
|
259
262
|
"top_k_accuracy",
|
|
260
263
|
]
|
|
264
|
+
__all__ += list(_text_all)
|
evalsuite/benchmarks.py
CHANGED
|
@@ -213,8 +213,8 @@ def _vision_cases(n: int, rng: np.random.Generator) -> list[Case]:
|
|
|
213
213
|
import contextlib as _ctx
|
|
214
214
|
import io
|
|
215
215
|
|
|
216
|
-
from pycocotools.coco import COCO
|
|
217
|
-
from pycocotools.cocoeval import COCOeval
|
|
216
|
+
from pycocotools.coco import COCO
|
|
217
|
+
from pycocotools.cocoeval import COCOeval
|
|
218
218
|
|
|
219
219
|
def coco_map() -> list[float]:
|
|
220
220
|
images, anns, dets, aid = [], [], [], 1
|
|
@@ -262,6 +262,155 @@ def _vision_cases(n: int, rng: np.random.Generator) -> list[Case]:
|
|
|
262
262
|
return [(name, ref, es_fn, refs.get(name)) for name, ref, es_fn, _ in cases]
|
|
263
263
|
|
|
264
264
|
|
|
265
|
+
_LLM_VOCAB = ( # noqa: SIM905
|
|
266
|
+
"the a cat dog sat on mat quickly model models data run running ran 2.5 3,000 well-known state-of-the-art "
|
|
267
|
+
"( ) , . ! ? don't U.S. e-mail Hello WORLD naive cafe results show improves baseline evaluation"
|
|
268
|
+
).split()
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _llm_cases(n: int, rng: np.random.Generator) -> list[Case]:
|
|
272
|
+
"""v0.4.0: text generation, retrieval, rater agreement and structured output (n / 100 examples)."""
|
|
273
|
+
import evalsuite as es
|
|
274
|
+
|
|
275
|
+
m = max(10, n // 100)
|
|
276
|
+
preds, refs_ = [], []
|
|
277
|
+
for _ in range(m):
|
|
278
|
+
base = list(rng.choice(_LLM_VOCAB, int(rng.integers(5, 25))))
|
|
279
|
+
keep = [w for w in base if rng.random() > 0.2] + list(rng.choice(_LLM_VOCAB, int(rng.integers(0, 4))))
|
|
280
|
+
preds.append(" ".join(keep))
|
|
281
|
+
refs_.append(" ".join(base))
|
|
282
|
+
queries = [
|
|
283
|
+
(set(rng.choice(200, int(rng.integers(1, 6)), replace=False).tolist()), rng.permutation(200)[:20].tolist())
|
|
284
|
+
for _ in range(m)
|
|
285
|
+
]
|
|
286
|
+
rel, ret = [q[0] for q in queries], [q[1] for q in queries]
|
|
287
|
+
ratings = rng.integers(1, 6, (4, m)).astype(float)
|
|
288
|
+
ratings[rng.random(ratings.shape) < 0.1] = np.nan
|
|
289
|
+
schema = {
|
|
290
|
+
"type": "object",
|
|
291
|
+
"properties": {
|
|
292
|
+
"name": {"type": "string", "minLength": 1},
|
|
293
|
+
"age": {"type": "integer", "minimum": 0},
|
|
294
|
+
"tags": {"type": "array", "items": {"type": "string"}, "uniqueItems": True},
|
|
295
|
+
},
|
|
296
|
+
"required": ["name", "age"],
|
|
297
|
+
"additionalProperties": False,
|
|
298
|
+
}
|
|
299
|
+
docs = []
|
|
300
|
+
for i in range(m):
|
|
301
|
+
d: dict[str, Any] = {"name": f"n{i}", "age": int(rng.integers(-2, 90)), "tags": ["a", "b"]}
|
|
302
|
+
if rng.random() < 0.1:
|
|
303
|
+
d["extra"] = 1
|
|
304
|
+
if rng.random() < 0.1:
|
|
305
|
+
del d["name"]
|
|
306
|
+
docs.append(d)
|
|
307
|
+
import json as _json
|
|
308
|
+
|
|
309
|
+
docs_text = [_json.dumps(d) for d in docs]
|
|
310
|
+
|
|
311
|
+
cases: list[Case] = [
|
|
312
|
+
(
|
|
313
|
+
f"text: corpus BLEU and chrF ({m} sentences)",
|
|
314
|
+
"sacreBLEU",
|
|
315
|
+
lambda: [float(es.bleu(refs_, preds)), float(es.chrf(refs_, preds))],
|
|
316
|
+
None,
|
|
317
|
+
),
|
|
318
|
+
(
|
|
319
|
+
f"text: ROUGE-1, ROUGE-2, ROUGE-L ({m} sentences)",
|
|
320
|
+
"rouge-score",
|
|
321
|
+
lambda: [
|
|
322
|
+
float(es.rouge_1(refs_, preds)),
|
|
323
|
+
float(es.rouge_2(refs_, preds)),
|
|
324
|
+
float(es.rouge_l(refs_, preds)),
|
|
325
|
+
],
|
|
326
|
+
None,
|
|
327
|
+
),
|
|
328
|
+
(
|
|
329
|
+
f"text: METEOR, exact and stem matches ({m} sentences)",
|
|
330
|
+
"NLTK",
|
|
331
|
+
lambda: [float(es.meteor(refs_, preds))],
|
|
332
|
+
None,
|
|
333
|
+
),
|
|
334
|
+
(
|
|
335
|
+
f"retrieval: MRR, MAP@20, NDCG@10 ({m} queries)",
|
|
336
|
+
"ranx",
|
|
337
|
+
lambda: [
|
|
338
|
+
float(es.mrr(rel, ret)),
|
|
339
|
+
float(es.mean_average_precision_at_k(rel, ret, k=20)),
|
|
340
|
+
float(es.ndcg_at_k(rel, ret, k=10)),
|
|
341
|
+
],
|
|
342
|
+
None,
|
|
343
|
+
),
|
|
344
|
+
(
|
|
345
|
+
f"agreement: Krippendorff's alpha, interval (4 raters × {m} items)",
|
|
346
|
+
"krippendorff",
|
|
347
|
+
lambda: [float(es.krippendorff_alpha(ratings, level="interval"))],
|
|
348
|
+
None,
|
|
349
|
+
),
|
|
350
|
+
(
|
|
351
|
+
f"structured: JSON Schema compliance ({m} documents)",
|
|
352
|
+
"jsonschema",
|
|
353
|
+
lambda: [float(es.json_schema_compliance(docs_text, schema))],
|
|
354
|
+
None,
|
|
355
|
+
),
|
|
356
|
+
]
|
|
357
|
+
refs: dict[str, Callable[[], Any]] = {}
|
|
358
|
+
with contextlib.suppress(ImportError):
|
|
359
|
+
import sacrebleu
|
|
360
|
+
|
|
361
|
+
refs[cases[0][0]] = lambda: [
|
|
362
|
+
sacrebleu.corpus_bleu(preds, [refs_], force=True).score,
|
|
363
|
+
sacrebleu.corpus_chrf(preds, [refs_]).score,
|
|
364
|
+
]
|
|
365
|
+
with contextlib.suppress(ImportError):
|
|
366
|
+
from rouge_score import rouge_scorer
|
|
367
|
+
|
|
368
|
+
def rouge_ref() -> list[float]:
|
|
369
|
+
sc = rouge_scorer.RougeScorer(["rouge1", "rouge2", "rougeL"])
|
|
370
|
+
out = [sc.score(r, p) for r, p in zip(refs_, preds)]
|
|
371
|
+
return [float(np.mean([o[k].fmeasure for o in out])) for k in ("rouge1", "rouge2", "rougeL")]
|
|
372
|
+
|
|
373
|
+
refs[cases[1][0]] = rouge_ref
|
|
374
|
+
with contextlib.suppress(ImportError):
|
|
375
|
+
from nltk.translate.meteor_score import meteor_score
|
|
376
|
+
|
|
377
|
+
class _NoWordNet:
|
|
378
|
+
def synsets(self, _w: str) -> list[Any]:
|
|
379
|
+
return []
|
|
380
|
+
|
|
381
|
+
refs[cases[2][0]] = lambda: [
|
|
382
|
+
float(
|
|
383
|
+
np.mean([meteor_score([r.split()], p.split(), wordnet=_NoWordNet()) for r, p in zip(refs_, preds)])
|
|
384
|
+
)
|
|
385
|
+
]
|
|
386
|
+
with contextlib.suppress(ImportError):
|
|
387
|
+
from ranx import Qrels, Run
|
|
388
|
+
from ranx import evaluate as ranx_evaluate
|
|
389
|
+
|
|
390
|
+
def ranx_ref() -> list[float]:
|
|
391
|
+
qrels = Qrels({f"q{i}": {f"d{d}": 1 for d in r} for i, r in enumerate(rel)})
|
|
392
|
+
run = Run({f"q{i}": {f"d{d}": float(len(r) - j) for j, d in enumerate(r)} for i, r in enumerate(ret)})
|
|
393
|
+
res = ranx_evaluate(qrels, run, ["mrr", "map@20", "ndcg@10"])
|
|
394
|
+
return [float(res["mrr"]), float(res["map@20"]), float(res["ndcg@10"])]
|
|
395
|
+
|
|
396
|
+
refs[cases[3][0]] = ranx_ref
|
|
397
|
+
with contextlib.suppress(ImportError):
|
|
398
|
+
import krippendorff
|
|
399
|
+
|
|
400
|
+
refs[cases[4][0]] = lambda: [
|
|
401
|
+
float(krippendorff.alpha(reliability_data=ratings, level_of_measurement="interval"))
|
|
402
|
+
]
|
|
403
|
+
with contextlib.suppress(ImportError):
|
|
404
|
+
import jsonschema
|
|
405
|
+
|
|
406
|
+
def jsonschema_ref() -> list[float]:
|
|
407
|
+
v = jsonschema.Draft202012Validator(schema)
|
|
408
|
+
return [float(np.mean([v.is_valid(_json.loads(t)) for t in docs_text]))]
|
|
409
|
+
|
|
410
|
+
refs[cases[5][0]] = jsonschema_ref
|
|
411
|
+
return [(name, ref, es_fn, refs.get(name)) for name, ref, es_fn, _ in cases]
|
|
412
|
+
|
|
413
|
+
|
|
265
414
|
def _clinical_cases(n: int, rng: np.random.Generator) -> list[Case]:
|
|
266
415
|
"""v0.2.0: diagnostic accuracy, calibration, decision curves and statistical tests."""
|
|
267
416
|
import evalsuite as es
|
|
@@ -377,6 +526,7 @@ _GROUPS = (
|
|
|
377
526
|
("Classification and regression", ("binary", "10 classes", "regression")),
|
|
378
527
|
("Clinical, calibration and statistics", ("clinical", "calibration", "decision", "statistics", "multiple")),
|
|
379
528
|
("Segmentation and object detection", ("segmentation", "detection")),
|
|
529
|
+
("LLM evaluation", ("text", "retrieval", "agreement", "structured")),
|
|
380
530
|
)
|
|
381
531
|
MATCH_TOLERANCE = 1e-9
|
|
382
532
|
|
|
@@ -419,7 +569,7 @@ class BenchmarkResult:
|
|
|
419
569
|
speedups = [r["speedup"] for r in compared]
|
|
420
570
|
return {
|
|
421
571
|
"group": name,
|
|
422
|
-
"cases": len({re.sub(r"\s*\(
|
|
572
|
+
"cases": len({re.sub(r"\s*\((?=[^)]*\d)[^)]*\)$", "", r["case"]) for r in rows}),
|
|
423
573
|
"rows": len(rows),
|
|
424
574
|
"compared": len(compared),
|
|
425
575
|
"matching": sum(
|
|
@@ -597,8 +747,8 @@ def run_benchmarks(
|
|
|
597
747
|
|
|
598
748
|
if repeat < 1:
|
|
599
749
|
raise ValueError("repeat must be at least 1.")
|
|
600
|
-
if suite not in ("all", "core", "clinical", "vision"):
|
|
601
|
-
raise ValueError("suite must be 'all', 'core', 'clinical' or '
|
|
750
|
+
if suite not in ("all", "core", "clinical", "vision", "llm"):
|
|
751
|
+
raise ValueError("suite must be 'all', 'core', 'clinical', 'vision' or 'llm'.")
|
|
602
752
|
rng = np.random.default_rng(random_state)
|
|
603
753
|
rows: list[dict[str, Any]] = []
|
|
604
754
|
versions: dict[str, Optional[str]] = {"sklearn": None, "statsmodels": None, "pycocotools": None}
|
|
@@ -613,7 +763,8 @@ def run_benchmarks(
|
|
|
613
763
|
"core": [_core_cases],
|
|
614
764
|
"clinical": [_clinical_cases],
|
|
615
765
|
"vision": [_vision_cases],
|
|
616
|
-
"
|
|
766
|
+
"llm": [_llm_cases],
|
|
767
|
+
"all": [_core_cases, _clinical_cases, _vision_cases, _llm_cases],
|
|
617
768
|
}[suite]
|
|
618
769
|
for n in sizes:
|
|
619
770
|
for build in builders:
|
evalsuite/cli/main.py
CHANGED
|
@@ -297,6 +297,24 @@ def cmd_detection(args: argparse.Namespace) -> int:
|
|
|
297
297
|
return 0
|
|
298
298
|
|
|
299
299
|
|
|
300
|
+
def cmd_text(args: argparse.Namespace) -> int:
|
|
301
|
+
import evalsuite as es
|
|
302
|
+
|
|
303
|
+
df = read_table(args.file)
|
|
304
|
+
preds = [str(v) for v in column(df, args.prediction, "--prediction")]
|
|
305
|
+
ref_cols = [column(df, c, "--reference") for c in args.reference]
|
|
306
|
+
refs = [[str(col[i]) for col in ref_cols if not _missing(col[i])] for i in range(len(preds))]
|
|
307
|
+
if any(not r for r in refs):
|
|
308
|
+
raise CLIError("Every row needs at least one non-empty reference.")
|
|
309
|
+
report = es.text_report(refs, preds, metrics=metric_list(args.metrics))
|
|
310
|
+
emit(report, args)
|
|
311
|
+
return 0
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _missing(v: Any) -> bool:
|
|
315
|
+
return v is None or (isinstance(v, float) and v != v) or (isinstance(v, str) and not v.strip())
|
|
316
|
+
|
|
317
|
+
|
|
300
318
|
def cmd_compare(args: argparse.Namespace) -> int:
|
|
301
319
|
import evalsuite as es
|
|
302
320
|
|
|
@@ -506,6 +524,18 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
506
524
|
add_output(p, digits=3)
|
|
507
525
|
p.set_defaults(func=cmd_detection)
|
|
508
526
|
|
|
527
|
+
p = sub.add_parser(
|
|
528
|
+
"text", help="text generation: BLEU, chrF, chrF++, TER, ROUGE, METEOR, exact match, token F1"
|
|
529
|
+
)
|
|
530
|
+
p.add_argument("file", help="CSV, TSV, Parquet or JSON-lines file with one example per row ('-' for stdin)")
|
|
531
|
+
p.add_argument("--prediction", required=True, help="column with the model output")
|
|
532
|
+
p.add_argument(
|
|
533
|
+
"--reference", required=True, action="append", help="column with a reference (repeat for several)"
|
|
534
|
+
)
|
|
535
|
+
p.add_argument("--metrics", help="comma-separated subset, e.g. bleu,chrf,rouge_l")
|
|
536
|
+
add_output(p)
|
|
537
|
+
p.set_defaults(func=cmd_text)
|
|
538
|
+
|
|
509
539
|
p = sub.add_parser(
|
|
510
540
|
"plot", help="save a ROC, PR, calibration, decision-curve, confusion-matrix or residual plot"
|
|
511
541
|
)
|
|
@@ -529,7 +559,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
529
559
|
p.add_argument("--repeat", type=int, default=5, help="timed repetitions per case; the fastest is reported")
|
|
530
560
|
p.add_argument(
|
|
531
561
|
"--suite",
|
|
532
|
-
choices=("all", "core", "clinical", "vision"),
|
|
562
|
+
choices=("all", "core", "clinical", "vision", "llm"),
|
|
533
563
|
default="all",
|
|
534
564
|
help="core: classification/regression; clinical: clinical, calibration, tests; vision: segmentation, "
|
|
535
565
|
"detection (default all)",
|
evalsuite/plot.py
CHANGED
|
@@ -503,3 +503,149 @@ def detection_pr(
|
|
|
503
503
|
)
|
|
504
504
|
ax.legend(loc="lower left", frameon=False, fontsize="small")
|
|
505
505
|
return ax
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
# ---------------------------------------------------------------- v0.4.0: preferences and text
|
|
509
|
+
def _rating_fit(method: str, rows: list[Any]) -> Any:
|
|
510
|
+
from .text.judge import bradley_terry, elo_ratings
|
|
511
|
+
|
|
512
|
+
if method == "bradley_terry":
|
|
513
|
+
return bradley_terry(rows, scale="elo")
|
|
514
|
+
if method == "elo":
|
|
515
|
+
return elo_ratings(rows)
|
|
516
|
+
raise InputValidationError("method must be 'bradley_terry' or 'elo'.")
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def ratings(
|
|
520
|
+
comparisons: Any,
|
|
521
|
+
*,
|
|
522
|
+
method: str = "bradley_terry",
|
|
523
|
+
n_resamples: int = 200,
|
|
524
|
+
level: float = 0.95,
|
|
525
|
+
random_state: Optional[int] = 0,
|
|
526
|
+
ax: Optional[Axes] = None,
|
|
527
|
+
) -> Axes:
|
|
528
|
+
"""Leaderboard of model ratings from pairwise ``(model_a, model_b, outcome)`` comparisons, with percentile
|
|
529
|
+
bootstrap intervals over comparisons (Chatbot Arena style). ``method``: ``"bradley_terry"`` (Elo scale) or
|
|
530
|
+
``"elo"`` (online Elo). Resamples on which the Bradley–Terry fit does not exist are skipped and counted in
|
|
531
|
+
the x-axis label."""
|
|
532
|
+
rows = list(comparisons)
|
|
533
|
+
point = _rating_fit(method, rows)
|
|
534
|
+
names = list(point.labels or ())
|
|
535
|
+
est = np.asarray(point.value, dtype=float)
|
|
536
|
+
rng = np.random.default_rng(random_state)
|
|
537
|
+
boot, failed = [], 0
|
|
538
|
+
for _ in range(int(n_resamples)):
|
|
539
|
+
sample = [rows[i] for i in rng.integers(0, len(rows), len(rows))]
|
|
540
|
+
try:
|
|
541
|
+
r = _rating_fit(method, sample)
|
|
542
|
+
except InputValidationError:
|
|
543
|
+
failed += 1
|
|
544
|
+
continue
|
|
545
|
+
lookup = dict(zip(r.labels or (), np.asarray(r.value, dtype=float)))
|
|
546
|
+
boot.append([lookup.get(n, np.nan) for n in names])
|
|
547
|
+
order = np.argsort(est)
|
|
548
|
+
ax = _axes(ax, (5.5, max(2.6, 0.45 * len(names) + 1.2)))
|
|
549
|
+
y = np.arange(len(names))
|
|
550
|
+
if boot:
|
|
551
|
+
b = np.asarray(boot)
|
|
552
|
+
a = (1 - level) / 2
|
|
553
|
+
lo, hi = np.nanquantile(b, a, axis=0), np.nanquantile(b, 1 - a, axis=0)
|
|
554
|
+
ax.errorbar(
|
|
555
|
+
est[order],
|
|
556
|
+
y,
|
|
557
|
+
xerr=[est[order] - lo[order], hi[order] - est[order]],
|
|
558
|
+
fmt="o",
|
|
559
|
+
color="#2a6fb0",
|
|
560
|
+
ecolor="0.45",
|
|
561
|
+
capsize=3,
|
|
562
|
+
)
|
|
563
|
+
else:
|
|
564
|
+
ax.plot(est[order], y, "o", color="#2a6fb0")
|
|
565
|
+
ax.set_yticks(y, [str(names[i]) for i in order])
|
|
566
|
+
title = "Bradley–Terry rating" if method == "bradley_terry" else "Elo rating"
|
|
567
|
+
note = f"{int(level * 100)}% bootstrap CI, {len(boot)} resamples" + (f", {failed} skipped" if failed else "")
|
|
568
|
+
ax.set(xlabel=f"{title} ({note})", title=f"{title}s from {len(rows)} comparisons")
|
|
569
|
+
ax.grid(axis="x", alpha=0.3)
|
|
570
|
+
return ax
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def win_matrix(comparisons: Any, *, ax: Optional[Axes] = None, annotate: bool = True) -> Axes:
|
|
574
|
+
"""Heatmap of pairwise win rates: cell (row, column) is how often the row model beat the column model (ties
|
|
575
|
+
count half); blank where the two were never compared."""
|
|
576
|
+
from .text.judge import _comparisons
|
|
577
|
+
|
|
578
|
+
names, rows = _comparisons(comparisons)
|
|
579
|
+
m = len(names)
|
|
580
|
+
wins, games = np.zeros((m, m)), np.zeros((m, m))
|
|
581
|
+
for a, b, o in rows:
|
|
582
|
+
wins[a, b] += o
|
|
583
|
+
wins[b, a] += 1 - o
|
|
584
|
+
games[a, b] += 1
|
|
585
|
+
games[b, a] += 1
|
|
586
|
+
with np.errstate(invalid="ignore", divide="ignore"):
|
|
587
|
+
rate = np.where(games > 0, wins / np.where(games > 0, games, 1), np.nan)
|
|
588
|
+
order = np.argsort(-np.nanmean(np.where(np.eye(m, dtype=bool), np.nan, rate), axis=1)) # strongest first
|
|
589
|
+
rate = rate[np.ix_(order, order)]
|
|
590
|
+
names = [names[i] for i in order]
|
|
591
|
+
ax = _axes(ax, (1.2 + 0.7 * m, 0.9 + 0.6 * m))
|
|
592
|
+
im = ax.imshow(rate, cmap="RdBu", vmin=0, vmax=1)
|
|
593
|
+
ax.set_xticks(range(m), [str(n) for n in names], rotation=45, ha="right")
|
|
594
|
+
ax.set_yticks(range(m), [str(n) for n in names])
|
|
595
|
+
if annotate:
|
|
596
|
+
for i in range(m):
|
|
597
|
+
for j in range(m):
|
|
598
|
+
if not np.isnan(rate[i, j]):
|
|
599
|
+
ax.text(
|
|
600
|
+
j,
|
|
601
|
+
i,
|
|
602
|
+
f"{rate[i, j]:.2f}",
|
|
603
|
+
ha="center",
|
|
604
|
+
va="center",
|
|
605
|
+
fontsize=8,
|
|
606
|
+
color="white" if abs(rate[i, j] - 0.5) > 0.3 else "black",
|
|
607
|
+
)
|
|
608
|
+
ax.figure.colorbar(im, ax=ax, label="win rate (row vs column)")
|
|
609
|
+
ax.set_title("Pairwise win rates")
|
|
610
|
+
return ax
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def text_scores(
|
|
614
|
+
references: Any,
|
|
615
|
+
predictions: Mapping[str, Any],
|
|
616
|
+
*,
|
|
617
|
+
metric: Any = "token_f1",
|
|
618
|
+
ax: Optional[Axes] = None,
|
|
619
|
+
**metric_kwargs: Any,
|
|
620
|
+
) -> Axes:
|
|
621
|
+
"""Distribution of a per-example text metric for several systems (box plots with the mean marked), e.g.
|
|
622
|
+
``metric="rouge_l"``, ``"token_f1"``, ``"meteor"``, ``"sentence_bleu"`` or ``"bertscore"`` (with
|
|
623
|
+
embeddings). Pair it with ``es.compare`` for intervals and tests."""
|
|
624
|
+
from .stats._resolve import resolve_metric
|
|
625
|
+
|
|
626
|
+
fn, name = resolve_metric(metric)
|
|
627
|
+
import inspect
|
|
628
|
+
|
|
629
|
+
if "average" not in inspect.signature(fn).parameters:
|
|
630
|
+
raise InputValidationError(
|
|
631
|
+
f"{name} is a corpus-level metric without per-example scores; use e.g. sentence_bleu, rouge_l, "
|
|
632
|
+
"token_f1 or meteor, or es.compare for corpus metrics."
|
|
633
|
+
)
|
|
634
|
+
if not isinstance(predictions, Mapping) or not predictions:
|
|
635
|
+
raise InputValidationError("predictions must be a dict of system name -> predictions.")
|
|
636
|
+
data, labels = [], []
|
|
637
|
+
for sys_name, preds in predictions.items():
|
|
638
|
+
res = fn(references, preds, average=None, **metric_kwargs)
|
|
639
|
+
vals = np.asarray(res.value, dtype=float)
|
|
640
|
+
if vals.ndim != 1:
|
|
641
|
+
raise InputValidationError(f"{name} does not give one score per example.")
|
|
642
|
+
data.append(vals[~np.isnan(vals)])
|
|
643
|
+
labels.append(str(sys_name))
|
|
644
|
+
ax = _axes(ax, (1.6 + 1.1 * len(labels), 4.2))
|
|
645
|
+
ax.boxplot(
|
|
646
|
+
data, showmeans=True, meanprops={"marker": "D", "markerfacecolor": "#d9822b", "markeredgecolor": "#d9822b"}
|
|
647
|
+
)
|
|
648
|
+
ax.set_xticks(range(1, len(labels) + 1), labels)
|
|
649
|
+
ax.set(ylabel=f"{res.name} per example", title=f"{res.name} by system (diamond = mean)")
|
|
650
|
+
ax.grid(axis="y", alpha=0.3)
|
|
651
|
+
return ax
|
evalsuite/stats/_resolve.py
CHANGED
|
@@ -54,6 +54,19 @@ def as_observations(x: Any, name: str) -> NDArray[Any]:
|
|
|
54
54
|
return to_numpy(x, name, allow_2d=True)
|
|
55
55
|
|
|
56
56
|
|
|
57
|
+
def as_items(x: Any, name: str) -> NDArray[Any]:
|
|
58
|
+
"""A 1-D object array holding one item per observation, whatever each item is."""
|
|
59
|
+
if x is None or isinstance(x, (str, bytes)):
|
|
60
|
+
raise InputValidationError(f"{name} must be a list with one item per example.")
|
|
61
|
+
seq = list(x.tolist() if isinstance(x, np.ndarray) and x.dtype != object and x.ndim == 1 else x)
|
|
62
|
+
if not seq:
|
|
63
|
+
raise InputValidationError(f"{name} is empty. Provide at least one observation.")
|
|
64
|
+
out = np.empty(len(seq), dtype=object)
|
|
65
|
+
for i, v in enumerate(seq):
|
|
66
|
+
out[i] = v
|
|
67
|
+
return out
|
|
68
|
+
|
|
69
|
+
|
|
57
70
|
def is_categorical(y: NDArray[Any]) -> bool:
|
|
58
71
|
if y.ndim > 2 or (y.dtype == object and y.size and not isinstance(y.flat[0], (str, bytes))):
|
|
59
72
|
return False
|
|
@@ -80,9 +93,16 @@ class MetricCall:
|
|
|
80
93
|
"Pass exactly one of y_pred (label/value metrics) or y_prob (probability metrics)."
|
|
81
94
|
)
|
|
82
95
|
self.fn = fn
|
|
83
|
-
self.y_true = as_observations(y_true, "y_true")
|
|
84
96
|
second = y_pred if y_pred is not None else y_prob
|
|
85
|
-
self.
|
|
97
|
+
self.items = bool(getattr(fn, "__evalsuite_items__", False))
|
|
98
|
+
if self.items:
|
|
99
|
+
# Text, retrieval and other item-level metrics: each observation is a whole item (a string, a list
|
|
100
|
+
# of references, a ranked list, an embedding matrix); resample items, never stratify by value.
|
|
101
|
+
self.y_true = as_items(y_true, "y_true")
|
|
102
|
+
self.second = as_items(second, "y_pred" if y_pred is not None else "y_prob")
|
|
103
|
+
else:
|
|
104
|
+
self.y_true = as_observations(y_true, "y_true")
|
|
105
|
+
self.second = as_observations(second, "y_pred" if y_pred is not None else "y_prob")
|
|
86
106
|
if self.second.shape[0] != self.y_true.shape[0]:
|
|
87
107
|
raise InputValidationError(
|
|
88
108
|
f"y_true and {'y_pred' if y_pred is not None else 'y_prob'} must contain the same number of "
|
|
@@ -97,7 +117,7 @@ class MetricCall:
|
|
|
97
117
|
self.weight = None if sample_weight is None else to_numpy(sample_weight, "sample_weight")
|
|
98
118
|
self.kwargs = dict(kwargs or {})
|
|
99
119
|
params = inspect.signature(fn).parameters
|
|
100
|
-
self.categorical = is_categorical(self.y_true)
|
|
120
|
+
self.categorical = not self.items and is_categorical(self.y_true)
|
|
101
121
|
# Keep every class in every resample so per-class and averaged metrics stay comparable.
|
|
102
122
|
if self.categorical and "labels" in params and "labels" not in self.kwargs and self.y_true.ndim == 1:
|
|
103
123
|
present = self.y_true if y_pred is None else np.concatenate([self.y_true, self.second])
|
evalsuite/stats/compare.py
CHANGED
|
@@ -15,7 +15,7 @@ from ..core.exceptions import InputValidationError
|
|
|
15
15
|
from ..core.registry import metric_info
|
|
16
16
|
from ..core.result import _json_safe, _latex_escape, _latex_table
|
|
17
17
|
from ..core.types import ArrayLike
|
|
18
|
-
from ._resolve import MetricCall, as_observations, is_categorical, resolve_metric
|
|
18
|
+
from ._resolve import MetricCall, as_items, as_observations, is_categorical, resolve_metric
|
|
19
19
|
from .effect import adjust_pvalues
|
|
20
20
|
from .intervals import _check_level, _check_resamples, bootstrap_distribution, resample_indices
|
|
21
21
|
from .paired import delong_test, mcnemar_test
|
|
@@ -295,8 +295,12 @@ def compare(
|
|
|
295
295
|
raise InputValidationError("compare() needs at least two models (a dict of name -> predictions).")
|
|
296
296
|
if baseline is not None and baseline not in names:
|
|
297
297
|
raise InputValidationError(f"baseline={baseline!r} is not one of the models: {', '.join(names)}.")
|
|
298
|
-
|
|
299
|
-
|
|
298
|
+
item_level = metrics is not None and any(
|
|
299
|
+
getattr(resolve_metric(m)[0], "__evalsuite_items__", False) for m in metrics
|
|
300
|
+
)
|
|
301
|
+
# Text, retrieval and other item-level metrics resample whole items and never stratify by value.
|
|
302
|
+
yt = as_items(y_true, "y_true") if item_level else as_observations(y_true, "y_true")
|
|
303
|
+
categorical = not item_level and is_categorical(yt)
|
|
300
304
|
if metrics is None:
|
|
301
305
|
if yt.ndim > 2 or (yt.dtype == object and yt.size and not isinstance(yt[0], (str, bytes))):
|
|
302
306
|
is_detection = yt.dtype == object and isinstance(yt[0], Mapping)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Language-model evaluation (v0.4.0): text generation, semantic similarity, factuality and hallucination,
|
|
2
|
+
LLM-as-a-judge and preferences, reasoning benchmarks, retrieval-augmented generation, and instruction following
|
|
3
|
+
and structured output."""
|
|
4
|
+
|
|
5
|
+
from .factuality import (
|
|
6
|
+
abstention_accuracy,
|
|
7
|
+
answer_correctness,
|
|
8
|
+
answer_relevance,
|
|
9
|
+
citation_precision,
|
|
10
|
+
citation_recall,
|
|
11
|
+
claim_verification_accuracy,
|
|
12
|
+
faithfulness,
|
|
13
|
+
groundedness,
|
|
14
|
+
hallucination_rate,
|
|
15
|
+
knowledge_consistency,
|
|
16
|
+
)
|
|
17
|
+
from .generation import (
|
|
18
|
+
bleu,
|
|
19
|
+
chrf,
|
|
20
|
+
cider,
|
|
21
|
+
cross_entropy,
|
|
22
|
+
distinct_n,
|
|
23
|
+
meteor,
|
|
24
|
+
perplexity,
|
|
25
|
+
rouge_1,
|
|
26
|
+
rouge_2,
|
|
27
|
+
rouge_l,
|
|
28
|
+
rouge_lsum,
|
|
29
|
+
self_bleu,
|
|
30
|
+
sentence_bleu,
|
|
31
|
+
ter,
|
|
32
|
+
)
|
|
33
|
+
from .judge import (
|
|
34
|
+
bradley_terry,
|
|
35
|
+
elo_ratings,
|
|
36
|
+
fleiss_kappa,
|
|
37
|
+
judge_agreement,
|
|
38
|
+
krippendorff_alpha,
|
|
39
|
+
position_consistency,
|
|
40
|
+
rubric_score,
|
|
41
|
+
self_preference_bias,
|
|
42
|
+
verbosity_bias,
|
|
43
|
+
win_rate,
|
|
44
|
+
)
|
|
45
|
+
from .qa import exact_match, normalize_answer, token_f1
|
|
46
|
+
from .rag import (
|
|
47
|
+
context_precision,
|
|
48
|
+
context_recall,
|
|
49
|
+
context_relevance,
|
|
50
|
+
failure_attribution,
|
|
51
|
+
latency_summary,
|
|
52
|
+
task_success_rate,
|
|
53
|
+
)
|
|
54
|
+
from .reasoning import benchmark_accuracy, extract_answer, majority_vote_accuracy, pass_at_k
|
|
55
|
+
from .report import TextReport, text_report
|
|
56
|
+
from .retrieval import hit_rate_at_k, mean_average_precision_at_k, mrr, ndcg_at_k, precision_at_k, recall_at_k
|
|
57
|
+
from .semantic import bertscore, embedding_similarity, mauve, model_score, moverscore
|
|
58
|
+
from .structured import (
|
|
59
|
+
api_call_success_rate,
|
|
60
|
+
constraint_satisfaction_rate,
|
|
61
|
+
extra_content_rate,
|
|
62
|
+
extract_json,
|
|
63
|
+
format_compliance,
|
|
64
|
+
instruction_compliance_rate,
|
|
65
|
+
instruction_retention,
|
|
66
|
+
json_schema_compliance,
|
|
67
|
+
json_validity,
|
|
68
|
+
required_field_accuracy,
|
|
69
|
+
tool_argument_accuracy,
|
|
70
|
+
tool_call_f1,
|
|
71
|
+
tool_selection_accuracy,
|
|
72
|
+
validate_json_schema,
|
|
73
|
+
xml_validity,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
__all__ = [
|
|
77
|
+
"TextReport",
|
|
78
|
+
"text_report",
|
|
79
|
+
"abstention_accuracy",
|
|
80
|
+
"answer_correctness",
|
|
81
|
+
"answer_relevance",
|
|
82
|
+
"api_call_success_rate",
|
|
83
|
+
"benchmark_accuracy",
|
|
84
|
+
"bertscore",
|
|
85
|
+
"bleu",
|
|
86
|
+
"bradley_terry",
|
|
87
|
+
"chrf",
|
|
88
|
+
"cider",
|
|
89
|
+
"citation_precision",
|
|
90
|
+
"citation_recall",
|
|
91
|
+
"claim_verification_accuracy",
|
|
92
|
+
"constraint_satisfaction_rate",
|
|
93
|
+
"context_precision",
|
|
94
|
+
"context_recall",
|
|
95
|
+
"context_relevance",
|
|
96
|
+
"cross_entropy",
|
|
97
|
+
"distinct_n",
|
|
98
|
+
"elo_ratings",
|
|
99
|
+
"embedding_similarity",
|
|
100
|
+
"exact_match",
|
|
101
|
+
"extra_content_rate",
|
|
102
|
+
"extract_answer",
|
|
103
|
+
"extract_json",
|
|
104
|
+
"failure_attribution",
|
|
105
|
+
"faithfulness",
|
|
106
|
+
"fleiss_kappa",
|
|
107
|
+
"format_compliance",
|
|
108
|
+
"groundedness",
|
|
109
|
+
"hallucination_rate",
|
|
110
|
+
"hit_rate_at_k",
|
|
111
|
+
"instruction_compliance_rate",
|
|
112
|
+
"instruction_retention",
|
|
113
|
+
"json_schema_compliance",
|
|
114
|
+
"json_validity",
|
|
115
|
+
"judge_agreement",
|
|
116
|
+
"knowledge_consistency",
|
|
117
|
+
"krippendorff_alpha",
|
|
118
|
+
"latency_summary",
|
|
119
|
+
"majority_vote_accuracy",
|
|
120
|
+
"mauve",
|
|
121
|
+
"mean_average_precision_at_k",
|
|
122
|
+
"meteor",
|
|
123
|
+
"model_score",
|
|
124
|
+
"moverscore",
|
|
125
|
+
"mrr",
|
|
126
|
+
"ndcg_at_k",
|
|
127
|
+
"normalize_answer",
|
|
128
|
+
"pass_at_k",
|
|
129
|
+
"perplexity",
|
|
130
|
+
"position_consistency",
|
|
131
|
+
"precision_at_k",
|
|
132
|
+
"recall_at_k",
|
|
133
|
+
"required_field_accuracy",
|
|
134
|
+
"rouge_1",
|
|
135
|
+
"rouge_2",
|
|
136
|
+
"rouge_l",
|
|
137
|
+
"rouge_lsum",
|
|
138
|
+
"rubric_score",
|
|
139
|
+
"self_bleu",
|
|
140
|
+
"self_preference_bias",
|
|
141
|
+
"sentence_bleu",
|
|
142
|
+
"task_success_rate",
|
|
143
|
+
"ter",
|
|
144
|
+
"token_f1",
|
|
145
|
+
"tool_argument_accuracy",
|
|
146
|
+
"tool_call_f1",
|
|
147
|
+
"tool_selection_accuracy",
|
|
148
|
+
"validate_json_schema",
|
|
149
|
+
"verbosity_bias",
|
|
150
|
+
"win_rate",
|
|
151
|
+
"xml_validity",
|
|
152
|
+
]
|