evalsuite-python 0.4.0__py3-none-any.whl → 0.4.1__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/_bench_metrics.py +1391 -0
- evalsuite/benchmarks.py +17 -7
- evalsuite/cli/main.py +3 -2
- evalsuite/clinical/metrics.py +7 -4
- evalsuite/text/generation.py +9 -8
- evalsuite/version.py +1 -1
- {evalsuite_python-0.4.0.dist-info → evalsuite_python-0.4.1.dist-info}/METADATA +8 -2
- {evalsuite_python-0.4.0.dist-info → evalsuite_python-0.4.1.dist-info}/RECORD +11 -10
- {evalsuite_python-0.4.0.dist-info → evalsuite_python-0.4.1.dist-info}/WHEEL +0 -0
- {evalsuite_python-0.4.0.dist-info → evalsuite_python-0.4.1.dist-info}/entry_points.txt +0 -0
- {evalsuite_python-0.4.0.dist-info → evalsuite_python-0.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1391 @@
|
|
|
1
|
+
"""Per-metric benchmark cases: one row for every registered metric and every statistics function.
|
|
2
|
+
|
|
3
|
+
Used by :func:`evalsuite.benchmarks.run_benchmarks` with ``suite="metrics"``. Each case is named after the
|
|
4
|
+
metric's registry id (``classification.f1``, ``text.bleu``, ``statistics.t_test``) and times one call on
|
|
5
|
+
data of the requested size. Where an independent implementation exists it is timed on the same data and the
|
|
6
|
+
largest absolute difference is reported:
|
|
7
|
+
|
|
8
|
+
* a reference library (scikit-learn, SciPy, statsmodels, pycocotools, sacreBLEU, rouge-score, NLTK,
|
|
9
|
+
pycocoevalcap, ranx, krippendorff, choix, POT, jsonschema) when it is installed;
|
|
10
|
+
* otherwise, for metrics with a closed-form definition, a direct NumPy / standard-library implementation of
|
|
11
|
+
the textbook formula (labelled ``NumPy formula`` or ``Python stdlib``).
|
|
12
|
+
|
|
13
|
+
Metrics with neither (learned or judge-dependent scores, randomised procedures) are timed for EvalSuite only.
|
|
14
|
+
|
|
15
|
+
Sizes: ``n`` observations for classification, regression, clinical and statistics; ``n`` pixels for
|
|
16
|
+
segmentation; ``n / 1000`` images for detection; ``n / 100`` examples for text, retrieval, RAG, judge and
|
|
17
|
+
structured-output metrics; ``n / 1000`` examples for the token-embedding metrics (BERTScore, MoverScore)
|
|
18
|
+
and MAUVE.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import contextlib
|
|
24
|
+
import importlib
|
|
25
|
+
import json
|
|
26
|
+
import math
|
|
27
|
+
import re
|
|
28
|
+
import xml.etree.ElementTree as ET
|
|
29
|
+
from collections import Counter
|
|
30
|
+
from collections.abc import Callable
|
|
31
|
+
from typing import Any, Optional
|
|
32
|
+
|
|
33
|
+
import numpy as np
|
|
34
|
+
|
|
35
|
+
Case = tuple[str, str, Callable[[], Any], Optional[Callable[[], Any]]]
|
|
36
|
+
|
|
37
|
+
_VOCAB = ( # noqa: SIM905
|
|
38
|
+
"the a cat dog sat on mat quickly model models data run running ran results show improves baseline "
|
|
39
|
+
"evaluation paper method score test train large small new old good bad"
|
|
40
|
+
).split()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _opt(module: str) -> Any:
|
|
44
|
+
"""An optional reference module, or None when it is not installed (fixed names only)."""
|
|
45
|
+
try:
|
|
46
|
+
return importlib.import_module(module)
|
|
47
|
+
except ImportError:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _f(x: Any) -> float:
|
|
52
|
+
return float(x)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _numeric(x: Any) -> list[float]:
|
|
56
|
+
"""Flatten a result (MetricResult, array, scalar, test result) to a list of floats for comparison."""
|
|
57
|
+
if hasattr(x, "p_value"):
|
|
58
|
+
return [float(x.statistic), float(x.p_value)]
|
|
59
|
+
if hasattr(x, "low") and hasattr(x, "high"):
|
|
60
|
+
return [float(x.low), float(x.high)]
|
|
61
|
+
v = getattr(x, "value", x)
|
|
62
|
+
return [float(t) for t in np.ravel(np.asarray(v, dtype=float))]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class _Cases:
|
|
66
|
+
"""Collects (name, reference label, EvalSuite call, reference call)."""
|
|
67
|
+
|
|
68
|
+
def __init__(self) -> None:
|
|
69
|
+
self.rows: list[Case] = []
|
|
70
|
+
|
|
71
|
+
def add(
|
|
72
|
+
self,
|
|
73
|
+
name: str,
|
|
74
|
+
es_fn: Callable[[], Any],
|
|
75
|
+
ref: str = "",
|
|
76
|
+
ref_fn: Optional[Callable[[], Any]] = None,
|
|
77
|
+
) -> None:
|
|
78
|
+
self.rows.append((name, ref if ref_fn is not None else "", es_fn, ref_fn))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# --------------------------------------------------------------------------------------------- core
|
|
82
|
+
def _classification(c: _Cases, n: int, rng: np.random.Generator) -> None:
|
|
83
|
+
import evalsuite as es
|
|
84
|
+
|
|
85
|
+
y = rng.integers(0, 2, n)
|
|
86
|
+
p = np.where(rng.random(n) < 0.8, y, 1 - y)
|
|
87
|
+
prob = 1 / (1 + np.exp(-(2.0 * (y - 0.5) + rng.normal(0, 1, n))))
|
|
88
|
+
yk = rng.integers(0, 10, n)
|
|
89
|
+
logits = rng.normal(size=(n, 10))
|
|
90
|
+
logits[np.arange(n), yk] += 1.5
|
|
91
|
+
pk = np.exp(logits) / np.exp(logits).sum(1, keepdims=True)
|
|
92
|
+
Y = (rng.random((n, 5)) < 0.3).astype(int)
|
|
93
|
+
P = np.where(rng.random((n, 5)) < 0.85, Y, 1 - Y)
|
|
94
|
+
|
|
95
|
+
def ece_np() -> float:
|
|
96
|
+
bins = np.minimum((prob * 10).astype(int), 9)
|
|
97
|
+
total = 0.0
|
|
98
|
+
for b in range(10):
|
|
99
|
+
m = bins == b
|
|
100
|
+
if m.any():
|
|
101
|
+
total += m.mean() * abs(prob[m].mean() - y[m].mean())
|
|
102
|
+
return total
|
|
103
|
+
|
|
104
|
+
skm = _opt("sklearn.metrics")
|
|
105
|
+
|
|
106
|
+
def sk(fn: Callable[[], Any]) -> Optional[Callable[[], Any]]:
|
|
107
|
+
return fn if skm is not None else None
|
|
108
|
+
|
|
109
|
+
c.add(
|
|
110
|
+
"classification.accuracy",
|
|
111
|
+
lambda: _f(es.accuracy(y, p)),
|
|
112
|
+
"scikit-learn",
|
|
113
|
+
sk(lambda: skm.accuracy_score(y, p)),
|
|
114
|
+
)
|
|
115
|
+
c.add(
|
|
116
|
+
"classification.average_precision",
|
|
117
|
+
lambda: _f(es.average_precision(y, prob)),
|
|
118
|
+
"scikit-learn",
|
|
119
|
+
sk(lambda: skm.average_precision_score(y, prob)),
|
|
120
|
+
)
|
|
121
|
+
c.add(
|
|
122
|
+
"classification.balanced_accuracy",
|
|
123
|
+
lambda: _f(es.balanced_accuracy(yk, pk.argmax(1))),
|
|
124
|
+
"scikit-learn",
|
|
125
|
+
sk(lambda: skm.balanced_accuracy_score(yk, pk.argmax(1))),
|
|
126
|
+
)
|
|
127
|
+
c.add(
|
|
128
|
+
"classification.brier_score",
|
|
129
|
+
lambda: _f(es.brier_score(y, prob)),
|
|
130
|
+
"scikit-learn",
|
|
131
|
+
sk(lambda: skm.brier_score_loss(y, prob)),
|
|
132
|
+
)
|
|
133
|
+
c.add(
|
|
134
|
+
"classification.cohen_kappa",
|
|
135
|
+
lambda: _f(es.cohen_kappa(y, p)),
|
|
136
|
+
"scikit-learn",
|
|
137
|
+
sk(lambda: skm.cohen_kappa_score(y, p)),
|
|
138
|
+
)
|
|
139
|
+
c.add(
|
|
140
|
+
"classification.expected_calibration_error",
|
|
141
|
+
lambda: _f(es.expected_calibration_error(y, prob, n_bins=10)),
|
|
142
|
+
"NumPy formula",
|
|
143
|
+
ece_np,
|
|
144
|
+
)
|
|
145
|
+
c.add(
|
|
146
|
+
"classification.f1",
|
|
147
|
+
lambda: _f(es.f1(yk, pk.argmax(1), average="macro")),
|
|
148
|
+
"scikit-learn",
|
|
149
|
+
sk(lambda: skm.f1_score(yk, pk.argmax(1), average="macro")),
|
|
150
|
+
)
|
|
151
|
+
c.add(
|
|
152
|
+
"classification.fbeta",
|
|
153
|
+
lambda: _f(es.fbeta(y, p, beta=2)),
|
|
154
|
+
"scikit-learn",
|
|
155
|
+
sk(lambda: skm.fbeta_score(y, p, beta=2)),
|
|
156
|
+
)
|
|
157
|
+
c.add(
|
|
158
|
+
"classification.hamming_loss",
|
|
159
|
+
lambda: _f(es.hamming_loss(Y, P)),
|
|
160
|
+
"scikit-learn",
|
|
161
|
+
sk(lambda: skm.hamming_loss(Y, P)),
|
|
162
|
+
)
|
|
163
|
+
c.add(
|
|
164
|
+
"classification.jaccard",
|
|
165
|
+
lambda: _f(es.jaccard(y, p)),
|
|
166
|
+
"scikit-learn",
|
|
167
|
+
sk(lambda: skm.jaccard_score(y, p)),
|
|
168
|
+
)
|
|
169
|
+
c.add(
|
|
170
|
+
"classification.log_loss",
|
|
171
|
+
lambda: _f(es.log_loss(yk, pk)),
|
|
172
|
+
"scikit-learn",
|
|
173
|
+
sk(lambda: skm.log_loss(yk, pk)),
|
|
174
|
+
)
|
|
175
|
+
c.add(
|
|
176
|
+
"classification.mcc",
|
|
177
|
+
lambda: _f(es.mcc(yk, pk.argmax(1))),
|
|
178
|
+
"scikit-learn",
|
|
179
|
+
sk(lambda: skm.matthews_corrcoef(yk, pk.argmax(1))),
|
|
180
|
+
)
|
|
181
|
+
c.add(
|
|
182
|
+
"classification.npv",
|
|
183
|
+
lambda: _f(es.npv(y, p)),
|
|
184
|
+
"scikit-learn",
|
|
185
|
+
sk(lambda: skm.precision_score(y, p, pos_label=0)),
|
|
186
|
+
)
|
|
187
|
+
c.add(
|
|
188
|
+
"classification.precision",
|
|
189
|
+
lambda: _f(es.precision(yk, pk.argmax(1), average="weighted")),
|
|
190
|
+
"scikit-learn",
|
|
191
|
+
sk(lambda: skm.precision_score(yk, pk.argmax(1), average="weighted")),
|
|
192
|
+
)
|
|
193
|
+
c.add(
|
|
194
|
+
"classification.recall",
|
|
195
|
+
lambda: _f(es.recall(yk, pk.argmax(1), average="micro")),
|
|
196
|
+
"scikit-learn",
|
|
197
|
+
sk(lambda: skm.recall_score(yk, pk.argmax(1), average="micro")),
|
|
198
|
+
)
|
|
199
|
+
c.add(
|
|
200
|
+
"classification.roc_auc",
|
|
201
|
+
lambda: _f(es.roc_auc(yk, pk, multi_class="ovr")),
|
|
202
|
+
"scikit-learn",
|
|
203
|
+
sk(lambda: skm.roc_auc_score(yk, pk, multi_class="ovr")),
|
|
204
|
+
)
|
|
205
|
+
c.add(
|
|
206
|
+
"classification.specificity",
|
|
207
|
+
lambda: _f(es.specificity(y, p)),
|
|
208
|
+
"scikit-learn",
|
|
209
|
+
sk(lambda: skm.recall_score(y, p, pos_label=0)),
|
|
210
|
+
)
|
|
211
|
+
c.add(
|
|
212
|
+
"classification.top_k_accuracy",
|
|
213
|
+
lambda: _f(es.top_k_accuracy(yk, pk, k=3)),
|
|
214
|
+
"scikit-learn",
|
|
215
|
+
sk(lambda: skm.top_k_accuracy_score(yk, pk, k=3)),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _regression(c: _Cases, n: int, rng: np.random.Generator) -> None:
|
|
220
|
+
import evalsuite as es
|
|
221
|
+
|
|
222
|
+
yt = rng.gamma(3.0, 3.0, n) + 0.5
|
|
223
|
+
yp = np.abs(yt + rng.normal(0, 1, n)) + 0.1
|
|
224
|
+
e = yt - yp
|
|
225
|
+
skm = _opt("sklearn.metrics")
|
|
226
|
+
|
|
227
|
+
def sk(fn: Callable[[], Any]) -> Optional[Callable[[], Any]]:
|
|
228
|
+
return fn if skm is not None else None
|
|
229
|
+
|
|
230
|
+
def huber_np() -> float:
|
|
231
|
+
a = np.abs(e)
|
|
232
|
+
return float(np.mean(np.where(a <= 1.0, 0.5 * e**2, a - 0.5)))
|
|
233
|
+
|
|
234
|
+
def r2_np() -> float:
|
|
235
|
+
return float(1 - np.sum(e**2) / np.sum((yt - yt.mean()) ** 2))
|
|
236
|
+
|
|
237
|
+
c.add(
|
|
238
|
+
"regression.adjusted_r2",
|
|
239
|
+
lambda: _f(es.adjusted_r2(yt, yp, n_features=5)),
|
|
240
|
+
"NumPy formula",
|
|
241
|
+
lambda: 1 - (1 - r2_np()) * (n - 1) / (n - 5 - 1),
|
|
242
|
+
)
|
|
243
|
+
c.add(
|
|
244
|
+
"regression.explained_variance",
|
|
245
|
+
lambda: _f(es.explained_variance(yt, yp)),
|
|
246
|
+
"scikit-learn",
|
|
247
|
+
sk(lambda: skm.explained_variance_score(yt, yp)),
|
|
248
|
+
)
|
|
249
|
+
c.add("regression.huber_loss", lambda: _f(es.huber_loss(yt, yp)), "NumPy formula", huber_np)
|
|
250
|
+
c.add(
|
|
251
|
+
"regression.mae", lambda: _f(es.mae(yt, yp)), "scikit-learn", sk(lambda: skm.mean_absolute_error(yt, yp))
|
|
252
|
+
)
|
|
253
|
+
c.add(
|
|
254
|
+
"regression.mape",
|
|
255
|
+
lambda: _f(es.mape(yt, yp)),
|
|
256
|
+
"scikit-learn",
|
|
257
|
+
sk(lambda: skm.mean_absolute_percentage_error(yt, yp)),
|
|
258
|
+
)
|
|
259
|
+
c.add(
|
|
260
|
+
"regression.max_error", lambda: _f(es.max_error(yt, yp)), "scikit-learn", sk(lambda: skm.max_error(yt, yp))
|
|
261
|
+
)
|
|
262
|
+
c.add(
|
|
263
|
+
"regression.mean_bias_error",
|
|
264
|
+
lambda: _f(es.mean_bias_error(yt, yp)),
|
|
265
|
+
"NumPy formula",
|
|
266
|
+
lambda: float(np.mean(yp - yt)),
|
|
267
|
+
)
|
|
268
|
+
c.add(
|
|
269
|
+
"regression.median_absolute_error",
|
|
270
|
+
lambda: _f(es.median_absolute_error(yt, yp)),
|
|
271
|
+
"scikit-learn",
|
|
272
|
+
sk(lambda: skm.median_absolute_error(yt, yp)),
|
|
273
|
+
)
|
|
274
|
+
c.add("regression.mse", lambda: _f(es.mse(yt, yp)), "scikit-learn", sk(lambda: skm.mean_squared_error(yt, yp)))
|
|
275
|
+
c.add(
|
|
276
|
+
"regression.msle",
|
|
277
|
+
lambda: _f(es.msle(yt, yp)),
|
|
278
|
+
"scikit-learn",
|
|
279
|
+
sk(lambda: skm.mean_squared_log_error(yt, yp)),
|
|
280
|
+
)
|
|
281
|
+
c.add(
|
|
282
|
+
"regression.quantile_loss",
|
|
283
|
+
lambda: _f(es.quantile_loss(yt, yp, alpha=0.9)),
|
|
284
|
+
"scikit-learn",
|
|
285
|
+
sk(lambda: skm.mean_pinball_loss(yt, yp, alpha=0.9)),
|
|
286
|
+
)
|
|
287
|
+
c.add("regression.r2", lambda: _f(es.r2(yt, yp)), "scikit-learn", sk(lambda: skm.r2_score(yt, yp)))
|
|
288
|
+
c.add(
|
|
289
|
+
"regression.rae",
|
|
290
|
+
lambda: _f(es.rae(yt, yp)),
|
|
291
|
+
"NumPy formula",
|
|
292
|
+
lambda: float(np.sum(np.abs(e)) / np.sum(np.abs(yt - yt.mean()))),
|
|
293
|
+
)
|
|
294
|
+
c.add(
|
|
295
|
+
"regression.rmse",
|
|
296
|
+
lambda: _f(es.rmse(yt, yp)),
|
|
297
|
+
"NumPy formula",
|
|
298
|
+
lambda: float(np.sqrt(np.mean(e**2))),
|
|
299
|
+
)
|
|
300
|
+
c.add(
|
|
301
|
+
"regression.rmsle",
|
|
302
|
+
lambda: _f(es.rmsle(yt, yp)),
|
|
303
|
+
"NumPy formula",
|
|
304
|
+
lambda: float(np.sqrt(np.mean((np.log1p(yt) - np.log1p(yp)) ** 2))),
|
|
305
|
+
)
|
|
306
|
+
c.add(
|
|
307
|
+
"regression.rse",
|
|
308
|
+
lambda: _f(es.rse(yt, yp)),
|
|
309
|
+
"NumPy formula",
|
|
310
|
+
lambda: float(np.sum(e**2) / np.sum((yt - yt.mean()) ** 2)),
|
|
311
|
+
)
|
|
312
|
+
c.add(
|
|
313
|
+
"regression.smape",
|
|
314
|
+
lambda: _f(es.smape(yt, yp)),
|
|
315
|
+
"NumPy formula",
|
|
316
|
+
lambda: float(np.mean(2 * np.abs(e) / (np.abs(yt) + np.abs(yp)))),
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# --------------------------------------------------------------------------------------------- clinical
|
|
321
|
+
def _clinical(c: _Cases, n: int, rng: np.random.Generator) -> None:
|
|
322
|
+
import evalsuite as es
|
|
323
|
+
|
|
324
|
+
y = rng.integers(0, 2, n)
|
|
325
|
+
p = np.where(rng.random(n) < 0.8, y, 1 - y)
|
|
326
|
+
x = rng.normal(size=n)
|
|
327
|
+
yc = (rng.random(n) < 1 / (1 + np.exp(-(0.4 + 1.3 * x)))).astype(int)
|
|
328
|
+
risk = 1 / (1 + np.exp(-(0.1 + 2.0 * x)))
|
|
329
|
+
tp = int(np.sum((y == 1) & (p == 1)))
|
|
330
|
+
fn = int(np.sum((y == 1) & (p == 0)))
|
|
331
|
+
fp = int(np.sum((y == 0) & (p == 1)))
|
|
332
|
+
tn = int(np.sum((y == 0) & (p == 0)))
|
|
333
|
+
sens, spec = tp / (tp + fn), tn / (tn + fp)
|
|
334
|
+
|
|
335
|
+
def counts() -> tuple[int, int, int, int]:
|
|
336
|
+
return (
|
|
337
|
+
int(np.sum((y == 1) & (p == 1))),
|
|
338
|
+
int(np.sum((y == 1) & (p == 0))),
|
|
339
|
+
int(np.sum((y == 0) & (p == 1))),
|
|
340
|
+
int(np.sum((y == 0) & (p == 0))),
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
def mce_np() -> float:
|
|
344
|
+
bins = np.minimum((risk * 10).astype(int), 9)
|
|
345
|
+
return float(
|
|
346
|
+
max(abs(risk[bins == b].mean() - yc[bins == b].mean()) for b in range(10) if (bins == b).any())
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
def dor_np() -> float:
|
|
350
|
+
t, f_n, f_p, t_n = counts()
|
|
351
|
+
return (t * t_n) / (f_p * f_n)
|
|
352
|
+
|
|
353
|
+
def youden_np() -> float:
|
|
354
|
+
t, f_n, f_p, t_n = counts()
|
|
355
|
+
return t / (t + f_n) + t_n / (t_n + f_p) - 1
|
|
356
|
+
|
|
357
|
+
skm = _opt("sklearn.metrics")
|
|
358
|
+
|
|
359
|
+
def lrs() -> tuple[float, float]:
|
|
360
|
+
if skm is not None:
|
|
361
|
+
a, b = skm.class_likelihood_ratios(y, p)
|
|
362
|
+
return float(a), float(b)
|
|
363
|
+
t, f_n, f_p, t_n = counts()
|
|
364
|
+
se, sp = t / (t + f_n), t_n / (t_n + f_p)
|
|
365
|
+
return se / (1 - sp), (1 - se) / sp
|
|
366
|
+
|
|
367
|
+
sm = _opt("statsmodels.api")
|
|
368
|
+
|
|
369
|
+
def cal_slope() -> float:
|
|
370
|
+
lp = np.log(risk / (1 - risk))
|
|
371
|
+
return float(sm.GLM(yc, sm.add_constant(lp), family=sm.families.Binomial()).fit().params[1])
|
|
372
|
+
|
|
373
|
+
def cal_int() -> float:
|
|
374
|
+
lp = np.log(risk / (1 - risk))
|
|
375
|
+
return float(sm.GLM(yc, np.ones((n, 1)), family=sm.families.Binomial(), offset=lp).fit().params[0])
|
|
376
|
+
|
|
377
|
+
c.add(
|
|
378
|
+
"calibration.calibration_intercept",
|
|
379
|
+
lambda: _f(es.calibration_intercept(yc, risk)),
|
|
380
|
+
"statsmodels",
|
|
381
|
+
cal_int if sm is not None else None,
|
|
382
|
+
)
|
|
383
|
+
c.add(
|
|
384
|
+
"calibration.calibration_slope",
|
|
385
|
+
lambda: _f(es.calibration_slope(yc, risk)),
|
|
386
|
+
"statsmodels",
|
|
387
|
+
cal_slope if sm is not None else None,
|
|
388
|
+
)
|
|
389
|
+
c.add(
|
|
390
|
+
"calibration.maximum_calibration_error",
|
|
391
|
+
lambda: _f(es.maximum_calibration_error(yc, risk, n_bins=10)),
|
|
392
|
+
"NumPy formula",
|
|
393
|
+
mce_np,
|
|
394
|
+
)
|
|
395
|
+
c.add(
|
|
396
|
+
"clinical.diagnostic_odds_ratio",
|
|
397
|
+
lambda: _f(es.diagnostic_odds_ratio(y, p)),
|
|
398
|
+
"NumPy formula",
|
|
399
|
+
dor_np,
|
|
400
|
+
)
|
|
401
|
+
c.add(
|
|
402
|
+
"clinical.lr_negative",
|
|
403
|
+
lambda: _f(es.lr_negative(y, p)),
|
|
404
|
+
"scikit-learn" if skm else "NumPy formula",
|
|
405
|
+
lambda: lrs()[1],
|
|
406
|
+
)
|
|
407
|
+
c.add(
|
|
408
|
+
"clinical.lr_positive",
|
|
409
|
+
lambda: _f(es.lr_positive(y, p)),
|
|
410
|
+
"scikit-learn" if skm else "NumPy formula",
|
|
411
|
+
lambda: lrs()[0],
|
|
412
|
+
)
|
|
413
|
+
c.add(
|
|
414
|
+
"clinical.net_benefit",
|
|
415
|
+
lambda: _f(es.net_benefit(yc, risk, threshold=0.2)),
|
|
416
|
+
"NumPy formula",
|
|
417
|
+
lambda: float(np.sum((risk >= 0.2) & (yc == 1)) / n - np.sum((risk >= 0.2) & (yc == 0)) / n * 0.25),
|
|
418
|
+
)
|
|
419
|
+
c.add(
|
|
420
|
+
"clinical.ppv",
|
|
421
|
+
lambda: _f(es.ppv(y, p)),
|
|
422
|
+
"scikit-learn",
|
|
423
|
+
(lambda: skm.precision_score(y, p)) if skm is not None else None,
|
|
424
|
+
)
|
|
425
|
+
c.add(
|
|
426
|
+
"clinical.sensitivity",
|
|
427
|
+
lambda: _f(es.sensitivity(y, p)),
|
|
428
|
+
"scikit-learn",
|
|
429
|
+
(lambda: skm.recall_score(y, p)) if skm is not None else None,
|
|
430
|
+
)
|
|
431
|
+
c.add(
|
|
432
|
+
"clinical.youden_j",
|
|
433
|
+
lambda: _f(es.youden_j(y, p)),
|
|
434
|
+
"NumPy formula",
|
|
435
|
+
youden_np,
|
|
436
|
+
)
|
|
437
|
+
del sens, spec, tp, fn, fp, tn
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
# --------------------------------------------------------------------------------------------- statistics
|
|
441
|
+
def _statistics(c: _Cases, n: int, rng: np.random.Generator) -> None:
|
|
442
|
+
from scipy import stats
|
|
443
|
+
|
|
444
|
+
import evalsuite as es
|
|
445
|
+
|
|
446
|
+
a, b = rng.normal(0, 1, n), rng.normal(0.05, 1.2, n)
|
|
447
|
+
d = a + rng.normal(0.02, 0.5, n)
|
|
448
|
+
g3 = [rng.normal(mu, 1, n) for mu in (0.0, 0.05, 0.1)]
|
|
449
|
+
y = rng.integers(0, 2, n)
|
|
450
|
+
pa = np.where(rng.random(n) < 0.8, y, 1 - y)
|
|
451
|
+
pb = np.where(rng.random(n) < 0.78, y, 1 - y)
|
|
452
|
+
prob_a = 1 / (1 + np.exp(-(2.0 * (y - 0.5) + rng.normal(0, 1, n))))
|
|
453
|
+
prob_b = 1 / (1 + np.exp(-(1.6 * (y - 0.5) + rng.normal(0, 1, n))))
|
|
454
|
+
ga, gb = rng.integers(0, 5, n), rng.integers(0, 5, n)
|
|
455
|
+
table = np.zeros((5, 5), dtype=np.int64)
|
|
456
|
+
np.add.at(table, (ga, gb), 1)
|
|
457
|
+
t2 = np.array([[int(np.sum((y == 1) & (pa == 1))), int(np.sum((y == 1) & (pa == 0)))],
|
|
458
|
+
[int(np.sum((y == 0) & (pa == 1))), int(np.sum((y == 0) & (pa == 0)))]]) # fmt: skip
|
|
459
|
+
pvals = rng.random(n) ** 2
|
|
460
|
+
xs = rng.normal(size=min(n, 5000))
|
|
461
|
+
k = int(np.sum(y == pa))
|
|
462
|
+
n_boot = 200
|
|
463
|
+
|
|
464
|
+
sm_prop = _opt("statsmodels.stats.proportion")
|
|
465
|
+
sm_mt = _opt("statsmodels.stats.multitest")
|
|
466
|
+
sm_ct = _opt("statsmodels.stats.contingency_tables")
|
|
467
|
+
|
|
468
|
+
def cohens_d_np() -> float:
|
|
469
|
+
sp = math.sqrt(((n - 1) * a.var(ddof=1) + (n - 1) * b.var(ddof=1)) / (2 * n - 2))
|
|
470
|
+
return float((a.mean() - b.mean()) / sp)
|
|
471
|
+
|
|
472
|
+
c.add(
|
|
473
|
+
"statistics.accuracy_ci",
|
|
474
|
+
lambda: _numeric(es.accuracy_ci(y, pa)),
|
|
475
|
+
"statsmodels",
|
|
476
|
+
(lambda: list(sm_prop.proportion_confint(k, n, method="wilson"))) if sm_prop is not None else None,
|
|
477
|
+
)
|
|
478
|
+
c.add(
|
|
479
|
+
"statistics.adjust_pvalues",
|
|
480
|
+
lambda: es.adjust_pvalues(pvals, method="bh").tolist(),
|
|
481
|
+
"statsmodels",
|
|
482
|
+
(lambda: sm_mt.multipletests(pvals, method="fdr_bh")[1].tolist()) if sm_mt is not None else None,
|
|
483
|
+
)
|
|
484
|
+
c.add(
|
|
485
|
+
"statistics.bootstrap_ci",
|
|
486
|
+
lambda: _numeric(es.bootstrap_ci(es.accuracy, y, pa, n_resamples=n_boot, random_state=0)),
|
|
487
|
+
)
|
|
488
|
+
c.add(
|
|
489
|
+
"statistics.chi_square_test",
|
|
490
|
+
lambda: _numeric(es.chi_square_test(table)),
|
|
491
|
+
"SciPy",
|
|
492
|
+
lambda: list(stats.chi2_contingency(table)[:2]),
|
|
493
|
+
)
|
|
494
|
+
c.add(
|
|
495
|
+
"statistics.cliffs_delta",
|
|
496
|
+
lambda: _f(es.cliffs_delta(a, b)),
|
|
497
|
+
"SciPy",
|
|
498
|
+
lambda: 2 * stats.mannwhitneyu(a, b).statistic / (n * n) - 1,
|
|
499
|
+
)
|
|
500
|
+
c.add("statistics.cohens_d", lambda: _f(es.cohens_d(a, b)), "NumPy formula", cohens_d_np)
|
|
501
|
+
c.add(
|
|
502
|
+
"statistics.compare",
|
|
503
|
+
lambda: es.compare(y, {"a": pa, "b": pb}, metrics=["accuracy"], n_resamples=n_boot, random_state=0),
|
|
504
|
+
)
|
|
505
|
+
c.add(
|
|
506
|
+
"statistics.cramers_v",
|
|
507
|
+
lambda: _f(es.cramers_v(table)),
|
|
508
|
+
"SciPy",
|
|
509
|
+
lambda: stats.contingency.association(table, method="cramer"),
|
|
510
|
+
)
|
|
511
|
+
c.add(
|
|
512
|
+
"statistics.delong_test",
|
|
513
|
+
lambda: [float(es.delong_test(y, prob_a, prob_b).p_value)],
|
|
514
|
+
)
|
|
515
|
+
c.add(
|
|
516
|
+
"statistics.fisher_exact_test",
|
|
517
|
+
lambda: [float(es.fisher_exact_test(t2).p_value)],
|
|
518
|
+
"SciPy",
|
|
519
|
+
lambda: [stats.fisher_exact(t2).pvalue],
|
|
520
|
+
)
|
|
521
|
+
c.add(
|
|
522
|
+
"statistics.friedman_test",
|
|
523
|
+
lambda: _numeric(es.friedman_test(*g3)),
|
|
524
|
+
"SciPy",
|
|
525
|
+
lambda: list(stats.friedmanchisquare(*g3)),
|
|
526
|
+
)
|
|
527
|
+
c.add(
|
|
528
|
+
"statistics.hedges_g",
|
|
529
|
+
lambda: _f(es.hedges_g(a, b)),
|
|
530
|
+
"NumPy formula",
|
|
531
|
+
lambda: cohens_d_np() * (1 - 3 / (4 * (2 * n) - 9)),
|
|
532
|
+
)
|
|
533
|
+
c.add(
|
|
534
|
+
"statistics.kruskal_wallis_test",
|
|
535
|
+
lambda: _numeric(es.kruskal_wallis_test(*g3)),
|
|
536
|
+
"SciPy",
|
|
537
|
+
lambda: list(stats.kruskal(*g3)),
|
|
538
|
+
)
|
|
539
|
+
c.add(
|
|
540
|
+
"statistics.mann_whitney_test",
|
|
541
|
+
lambda: _numeric(es.mann_whitney_test(a, b)),
|
|
542
|
+
"SciPy",
|
|
543
|
+
lambda: list(stats.mannwhitneyu(a, b)),
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
def mcnemar_ref() -> list[float]:
|
|
547
|
+
b01 = int(np.sum((pa == y) & (pb != y)))
|
|
548
|
+
b10 = int(np.sum((pa != y) & (pb == y)))
|
|
549
|
+
r = sm_ct.mcnemar(np.array([[0, b01], [b10, 0]]), exact=False, correction=True)
|
|
550
|
+
return [float(r.statistic), float(r.pvalue)]
|
|
551
|
+
|
|
552
|
+
c.add(
|
|
553
|
+
"statistics.mcnemar_test",
|
|
554
|
+
lambda: _numeric(es.mcnemar_test(y, pa, pb, exact=False)),
|
|
555
|
+
"statsmodels",
|
|
556
|
+
mcnemar_ref if sm_ct is not None else None,
|
|
557
|
+
)
|
|
558
|
+
c.add(
|
|
559
|
+
"statistics.paired_bootstrap_test",
|
|
560
|
+
lambda: [
|
|
561
|
+
float(es.paired_bootstrap_test(es.accuracy, y, pa, pb, n_resamples=n_boot, random_state=0).p_value)
|
|
562
|
+
],
|
|
563
|
+
)
|
|
564
|
+
c.add(
|
|
565
|
+
"statistics.paired_t_test",
|
|
566
|
+
lambda: _numeric(es.paired_t_test(a, d)),
|
|
567
|
+
"SciPy",
|
|
568
|
+
lambda: list(stats.ttest_rel(a, d)),
|
|
569
|
+
)
|
|
570
|
+
c.add(
|
|
571
|
+
"statistics.proportion_ci",
|
|
572
|
+
lambda: _numeric(es.proportion_ci(k, n, method="clopper-pearson")),
|
|
573
|
+
"statsmodels",
|
|
574
|
+
(lambda: list(sm_prop.proportion_confint(k, n, method="beta"))) if sm_prop is not None else None,
|
|
575
|
+
)
|
|
576
|
+
c.add("statistics.roc_auc_ci", lambda: _numeric(es.roc_auc_ci(y, prob_a)))
|
|
577
|
+
c.add(
|
|
578
|
+
"statistics.shapiro_wilk_test",
|
|
579
|
+
lambda: _numeric(es.shapiro_wilk_test(xs)),
|
|
580
|
+
"SciPy",
|
|
581
|
+
lambda: list(stats.shapiro(xs)),
|
|
582
|
+
)
|
|
583
|
+
c.add(
|
|
584
|
+
"statistics.t_test",
|
|
585
|
+
lambda: _numeric(es.t_test(a, b)),
|
|
586
|
+
"SciPy",
|
|
587
|
+
lambda: list(stats.ttest_ind(a, b, equal_var=False)),
|
|
588
|
+
)
|
|
589
|
+
c.add(
|
|
590
|
+
"statistics.wilcoxon_test",
|
|
591
|
+
lambda: _numeric(es.wilcoxon_test(a, d)),
|
|
592
|
+
"SciPy",
|
|
593
|
+
lambda: list(stats.wilcoxon(a, d)),
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
# --------------------------------------------------------------------------------------------- vision
|
|
598
|
+
def _vision(c: _Cases, n: int, rng: np.random.Generator) -> None:
|
|
599
|
+
import evalsuite as es
|
|
600
|
+
|
|
601
|
+
side = 64
|
|
602
|
+
n_img = max(1, n // (side * side))
|
|
603
|
+
k = 5
|
|
604
|
+
yy, xx = np.ogrid[:side, :side]
|
|
605
|
+
true = np.zeros((n_img, side, side), dtype=np.int64)
|
|
606
|
+
for i in range(n_img):
|
|
607
|
+
for cl in range(1, k):
|
|
608
|
+
cy, cx, r = rng.integers(8, side - 8), rng.integers(8, side - 8), rng.integers(4, 14)
|
|
609
|
+
true[i][(yy - cy) ** 2 + (xx - cx) ** 2 <= r * r] = cl
|
|
610
|
+
pred = np.roll(true, 1, axis=2)
|
|
611
|
+
noise = rng.random(pred.shape) < 0.02
|
|
612
|
+
pred[noise] = rng.integers(0, k, int(noise.sum()))
|
|
613
|
+
labels = list(range(k))
|
|
614
|
+
n_sd = min(n_img, 50)
|
|
615
|
+
bt, bp = true[:n_sd] == k - 1, pred[:n_sd] == k - 1
|
|
616
|
+
ft, fp_ = true.ravel(), pred.ravel()
|
|
617
|
+
|
|
618
|
+
skm = _opt("sklearn.metrics")
|
|
619
|
+
from scipy import ndimage
|
|
620
|
+
from scipy.spatial.distance import directed_hausdorff
|
|
621
|
+
|
|
622
|
+
def surface(m: Any) -> Any:
|
|
623
|
+
er = ndimage.binary_erosion(m, structure=ndimage.generate_binary_structure(2, 1), border_value=0)
|
|
624
|
+
return np.argwhere(m & ~er).astype(float)
|
|
625
|
+
|
|
626
|
+
def hd_ref() -> list[float]:
|
|
627
|
+
out = []
|
|
628
|
+
for i in range(n_sd):
|
|
629
|
+
s1, s2 = surface(bt[i]), surface(bp[i])
|
|
630
|
+
out.append(max(directed_hausdorff(s1, s2)[0], directed_hausdorff(s2, s1)[0]))
|
|
631
|
+
return out
|
|
632
|
+
|
|
633
|
+
def asd_ref() -> list[float]:
|
|
634
|
+
from scipy.spatial import cKDTree
|
|
635
|
+
|
|
636
|
+
out = []
|
|
637
|
+
for i in range(n_sd):
|
|
638
|
+
s1, s2 = surface(bt[i]), surface(bp[i])
|
|
639
|
+
d1, _ = cKDTree(s2).query(s1)
|
|
640
|
+
d2, _ = cKDTree(s1).query(s2)
|
|
641
|
+
out.append(float((d1.sum() + d2.sum()) / (len(d1) + len(d2))))
|
|
642
|
+
return out
|
|
643
|
+
|
|
644
|
+
def miou_ref() -> float:
|
|
645
|
+
return float(np.mean(skm.jaccard_score(ft, fp_, labels=labels, average=None)))
|
|
646
|
+
|
|
647
|
+
def mpa_ref() -> float:
|
|
648
|
+
return float(np.mean(skm.recall_score(ft, fp_, labels=labels, average=None)))
|
|
649
|
+
|
|
650
|
+
c.add(
|
|
651
|
+
"segmentation.average_surface_distance",
|
|
652
|
+
lambda: [
|
|
653
|
+
float(es.average_surface_distance(bt[i].astype(int), bp[i].astype(int), labels=[1]))
|
|
654
|
+
for i in range(n_sd)
|
|
655
|
+
],
|
|
656
|
+
"SciPy",
|
|
657
|
+
asd_ref,
|
|
658
|
+
)
|
|
659
|
+
c.add("segmentation.boundary_iou", lambda: _f(es.boundary_iou(true, pred, labels=[k - 1])))
|
|
660
|
+
c.add(
|
|
661
|
+
"segmentation.dice",
|
|
662
|
+
lambda: _numeric(es.dice(true, pred, average=None)),
|
|
663
|
+
"scikit-learn",
|
|
664
|
+
(lambda: list(skm.f1_score(ft, fp_, labels=labels, average=None))) if skm is not None else None,
|
|
665
|
+
)
|
|
666
|
+
c.add(
|
|
667
|
+
"segmentation.hausdorff_distance",
|
|
668
|
+
lambda: [
|
|
669
|
+
float(es.hausdorff_distance(bt[i].astype(int), bp[i].astype(int), labels=[1])) for i in range(n_sd)
|
|
670
|
+
],
|
|
671
|
+
"SciPy",
|
|
672
|
+
hd_ref,
|
|
673
|
+
)
|
|
674
|
+
c.add(
|
|
675
|
+
"segmentation.iou",
|
|
676
|
+
lambda: _numeric(es.iou(true, pred, average=None)),
|
|
677
|
+
"scikit-learn",
|
|
678
|
+
(lambda: list(skm.jaccard_score(ft, fp_, labels=labels, average=None))) if skm is not None else None,
|
|
679
|
+
)
|
|
680
|
+
c.add(
|
|
681
|
+
"segmentation.mean_pixel_accuracy",
|
|
682
|
+
lambda: _f(es.mean_pixel_accuracy(true, pred, num_classes=k)),
|
|
683
|
+
"scikit-learn",
|
|
684
|
+
mpa_ref if skm is not None else None,
|
|
685
|
+
)
|
|
686
|
+
c.add(
|
|
687
|
+
"segmentation.miou",
|
|
688
|
+
lambda: _f(es.miou(true, pred, num_classes=k)),
|
|
689
|
+
"scikit-learn",
|
|
690
|
+
miou_ref if skm is not None else None,
|
|
691
|
+
)
|
|
692
|
+
c.add(
|
|
693
|
+
"segmentation.pixel_accuracy",
|
|
694
|
+
lambda: _f(es.pixel_accuracy(true, pred)),
|
|
695
|
+
"scikit-learn",
|
|
696
|
+
(lambda: skm.accuracy_score(ft, fp_)) if skm is not None else None,
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
# detection
|
|
700
|
+
n_det = max(10, n // 1000)
|
|
701
|
+
y_true, y_pred = [], []
|
|
702
|
+
for _ in range(n_det):
|
|
703
|
+
m = int(rng.integers(1, 8))
|
|
704
|
+
xy = rng.uniform(0, 500, (m, 2))
|
|
705
|
+
wh = rng.uniform(8, 160, (m, 2))
|
|
706
|
+
boxes = np.column_stack([xy, xy + wh])
|
|
707
|
+
lab = rng.integers(1, 6, m)
|
|
708
|
+
y_true.append({"boxes": boxes, "labels": lab})
|
|
709
|
+
jitter = boxes + rng.normal(0, 4, boxes.shape)
|
|
710
|
+
jitter[:, 2:] = np.maximum(jitter[:, 2:], jitter[:, :2] + 1)
|
|
711
|
+
extra = rng.uniform(0, 500, (3, 2))
|
|
712
|
+
fpb = np.column_stack([extra, extra + rng.uniform(10, 80, (3, 2))])
|
|
713
|
+
y_pred.append(
|
|
714
|
+
{
|
|
715
|
+
"boxes": np.vstack([jitter, fpb]),
|
|
716
|
+
"labels": np.r_[lab, rng.integers(1, 6, 3)],
|
|
717
|
+
"scores": rng.random(m + 3),
|
|
718
|
+
}
|
|
719
|
+
)
|
|
720
|
+
ba = np.vstack([t["boxes"] for t in y_true])
|
|
721
|
+
bb = np.vstack([p["boxes"] for p in y_pred])[: max(1, min(len(ba), 2000))]
|
|
722
|
+
ba = ba[:2000]
|
|
723
|
+
|
|
724
|
+
def iou_np() -> Any:
|
|
725
|
+
x1 = np.maximum(ba[:, None, 0], bb[None, :, 0])
|
|
726
|
+
y1 = np.maximum(ba[:, None, 1], bb[None, :, 1])
|
|
727
|
+
x2 = np.minimum(ba[:, None, 2], bb[None, :, 2])
|
|
728
|
+
y2 = np.minimum(ba[:, None, 3], bb[None, :, 3])
|
|
729
|
+
inter = np.clip(x2 - x1, 0, None) * np.clip(y2 - y1, 0, None)
|
|
730
|
+
aa = (ba[:, 2] - ba[:, 0]) * (ba[:, 3] - ba[:, 1])
|
|
731
|
+
ab = (bb[:, 2] - bb[:, 0]) * (bb[:, 3] - bb[:, 1])
|
|
732
|
+
return (inter / (aa[:, None] + ab[None, :] - inter)).ravel().tolist()
|
|
733
|
+
|
|
734
|
+
coco: Optional[Callable[[], Any]] = None
|
|
735
|
+
with contextlib.suppress(ImportError):
|
|
736
|
+
import io
|
|
737
|
+
|
|
738
|
+
from pycocotools.coco import COCO
|
|
739
|
+
from pycocotools.cocoeval import COCOeval
|
|
740
|
+
|
|
741
|
+
def coco_eval() -> Any:
|
|
742
|
+
images, anns, dets, aid = [], [], [], 1
|
|
743
|
+
for i, (t, p) in enumerate(zip(y_true, y_pred)):
|
|
744
|
+
images.append({"id": i + 1})
|
|
745
|
+
for bx, cl in zip(t["boxes"], t["labels"]):
|
|
746
|
+
w, h = bx[2] - bx[0], bx[3] - bx[1]
|
|
747
|
+
anns.append(
|
|
748
|
+
{
|
|
749
|
+
"id": aid,
|
|
750
|
+
"image_id": i + 1,
|
|
751
|
+
"category_id": int(cl),
|
|
752
|
+
"bbox": [bx[0], bx[1], w, h],
|
|
753
|
+
"area": w * h,
|
|
754
|
+
"iscrowd": 0,
|
|
755
|
+
}
|
|
756
|
+
)
|
|
757
|
+
aid += 1
|
|
758
|
+
for bx, cl, sc in zip(p["boxes"], p["labels"], p["scores"]):
|
|
759
|
+
dets.append(
|
|
760
|
+
{
|
|
761
|
+
"image_id": i + 1,
|
|
762
|
+
"category_id": int(cl),
|
|
763
|
+
"bbox": [bx[0], bx[1], bx[2] - bx[0], bx[3] - bx[1]],
|
|
764
|
+
"score": float(sc),
|
|
765
|
+
}
|
|
766
|
+
)
|
|
767
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
768
|
+
gt = COCO()
|
|
769
|
+
gt.dataset = {
|
|
770
|
+
"images": images,
|
|
771
|
+
"annotations": anns,
|
|
772
|
+
"categories": [{"id": cl} for cl in range(1, 6)],
|
|
773
|
+
}
|
|
774
|
+
gt.createIndex()
|
|
775
|
+
ev = COCOeval(gt, gt.loadRes(dets), "bbox")
|
|
776
|
+
ev.evaluate()
|
|
777
|
+
ev.accumulate()
|
|
778
|
+
ev.summarize()
|
|
779
|
+
return ev
|
|
780
|
+
|
|
781
|
+
coco = coco_eval
|
|
782
|
+
|
|
783
|
+
c.add(
|
|
784
|
+
"detection.average_precision_detection",
|
|
785
|
+
lambda: _f(es.average_precision_detection(y_true, y_pred, iou_threshold=0.5, average="macro")),
|
|
786
|
+
"pycocotools",
|
|
787
|
+
(lambda: float(coco().stats[1])) if coco is not None else None,
|
|
788
|
+
)
|
|
789
|
+
c.add("detection.box_iou", lambda: es.box_iou(ba, bb).ravel().tolist(), "NumPy formula", iou_np)
|
|
790
|
+
c.add(
|
|
791
|
+
"detection.mean_average_precision",
|
|
792
|
+
lambda: _f(es.mean_average_precision(y_true, y_pred)),
|
|
793
|
+
"pycocotools",
|
|
794
|
+
(lambda: float(coco().stats[0])) if coco is not None else None,
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
# --------------------------------------------------------------------------------------------- LLM
|
|
799
|
+
def _sentence(rng: np.random.Generator, lo: int = 5, hi: int = 25) -> list[str]:
|
|
800
|
+
return list(rng.choice(_VOCAB, int(rng.integers(lo, hi))))
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def _llm(c: _Cases, n: int, rng: np.random.Generator) -> None:
|
|
804
|
+
import evalsuite as es
|
|
805
|
+
|
|
806
|
+
m = max(20, n // 100)
|
|
807
|
+
me = max(10, n // 1000)
|
|
808
|
+
refs, preds, multi = [], [], []
|
|
809
|
+
for _ in range(m):
|
|
810
|
+
base = _sentence(rng)
|
|
811
|
+
keep = [w for w in base if rng.random() > 0.2] + list(rng.choice(_VOCAB, int(rng.integers(0, 4))))
|
|
812
|
+
preds.append(" ".join(keep))
|
|
813
|
+
refs.append(" ".join(base))
|
|
814
|
+
multi.append([" ".join(base), " ".join(_sentence(rng, 3, 12)), " ".join(_sentence(rng, 3, 12))])
|
|
815
|
+
lsum_refs = [r.replace(" mat ", " mat\n") for r in refs]
|
|
816
|
+
lsum_preds = [p.replace(" mat ", " mat\n") for p in preds]
|
|
817
|
+
|
|
818
|
+
# ---- text generation (sacreBLEU, rouge-score, NLTK, pycocoevalcap)
|
|
819
|
+
sacre = _opt("sacrebleu")
|
|
820
|
+
|
|
821
|
+
c.add(
|
|
822
|
+
"text.bleu",
|
|
823
|
+
lambda: _f(es.bleu(refs, preds)),
|
|
824
|
+
"sacreBLEU",
|
|
825
|
+
(lambda: sacre.corpus_bleu(preds, [refs], force=True).score) if sacre else None,
|
|
826
|
+
)
|
|
827
|
+
c.add(
|
|
828
|
+
"text.chrf",
|
|
829
|
+
lambda: _f(es.chrf(refs, preds)),
|
|
830
|
+
"sacreBLEU",
|
|
831
|
+
(lambda: sacre.corpus_chrf(preds, [refs]).score) if sacre else None,
|
|
832
|
+
)
|
|
833
|
+
c.add(
|
|
834
|
+
"text.sentence_bleu",
|
|
835
|
+
lambda: _numeric(es.sentence_bleu(refs, preds, average=None)),
|
|
836
|
+
"sacreBLEU",
|
|
837
|
+
(lambda: [sacre.sentence_bleu(p, [r]).score for r, p in zip(refs, preds)]) if sacre else None,
|
|
838
|
+
)
|
|
839
|
+
c.add(
|
|
840
|
+
"text.ter",
|
|
841
|
+
lambda: _f(es.ter(refs, preds)),
|
|
842
|
+
"sacreBLEU",
|
|
843
|
+
(lambda: sacre.corpus_ter(preds, [refs]).score) if sacre else None,
|
|
844
|
+
)
|
|
845
|
+
ms = refs[: max(20, m // 10)]
|
|
846
|
+
msp = preds[: len(ms)]
|
|
847
|
+
|
|
848
|
+
def self_bleu_ref() -> float:
|
|
849
|
+
out = []
|
|
850
|
+
for i, p in enumerate(msp):
|
|
851
|
+
others = [msp[j] for j in range(len(msp)) if j != i]
|
|
852
|
+
out.append(sacre.sentence_bleu(p, others).score)
|
|
853
|
+
return float(np.mean(out))
|
|
854
|
+
|
|
855
|
+
c.add("text.self_bleu", lambda: _f(es.self_bleu(msp)), "sacreBLEU", self_bleu_ref if sacre else None)
|
|
856
|
+
del ms
|
|
857
|
+
|
|
858
|
+
rs = _opt("rouge_score.rouge_scorer")
|
|
859
|
+
|
|
860
|
+
def rouge_ref(key: str, r_: list[str], p_: list[str]) -> Callable[[], float]:
|
|
861
|
+
def run() -> float:
|
|
862
|
+
sc = rs.RougeScorer([key])
|
|
863
|
+
return float(np.mean([sc.score(a, b)[key].fmeasure for a, b in zip(r_, p_)]))
|
|
864
|
+
|
|
865
|
+
return run
|
|
866
|
+
|
|
867
|
+
for name, key in (("rouge_1", "rouge1"), ("rouge_2", "rouge2"), ("rouge_l", "rougeL")):
|
|
868
|
+
fn = getattr(es, name)
|
|
869
|
+
c.add(
|
|
870
|
+
f"text.{name}",
|
|
871
|
+
lambda fn=fn: _f(fn(refs, preds)), # type: ignore[misc]
|
|
872
|
+
"rouge-score",
|
|
873
|
+
rouge_ref(key, refs, preds) if rs else None,
|
|
874
|
+
)
|
|
875
|
+
c.add(
|
|
876
|
+
"text.rouge_lsum",
|
|
877
|
+
lambda: _f(es.rouge_lsum(lsum_refs, lsum_preds)),
|
|
878
|
+
"rouge-score",
|
|
879
|
+
rouge_ref("rougeLsum", lsum_refs, lsum_preds) if rs else None,
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
meteor_ref: Optional[Callable[[], float]] = None
|
|
883
|
+
with contextlib.suppress(ImportError):
|
|
884
|
+
from nltk.stem.porter import PorterStemmer
|
|
885
|
+
from nltk.translate.meteor_score import meteor_score
|
|
886
|
+
|
|
887
|
+
class _NoWordNet:
|
|
888
|
+
def synsets(self, _w: str) -> list[Any]:
|
|
889
|
+
return []
|
|
890
|
+
|
|
891
|
+
def _meteor() -> float:
|
|
892
|
+
st, wn = PorterStemmer(), _NoWordNet()
|
|
893
|
+
return float(
|
|
894
|
+
np.mean(
|
|
895
|
+
[meteor_score([r.split()], p.split(), stemmer=st, wordnet=wn) for r, p in zip(refs, preds)]
|
|
896
|
+
)
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
meteor_ref = _meteor
|
|
900
|
+
c.add("text.meteor", lambda: _f(es.meteor(refs, preds)), "NLTK", meteor_ref)
|
|
901
|
+
|
|
902
|
+
cider_ref: Optional[Callable[[], float]] = None
|
|
903
|
+
with contextlib.suppress(ImportError):
|
|
904
|
+
from pycocoevalcap.cider.cider import Cider # type: ignore[import-untyped]
|
|
905
|
+
|
|
906
|
+
def _cider() -> float:
|
|
907
|
+
res = {i: [p] for i, p in enumerate(preds)}
|
|
908
|
+
return float(Cider().compute_score(dict(enumerate(multi)), res)[0])
|
|
909
|
+
|
|
910
|
+
cider_ref = _cider
|
|
911
|
+
c.add("text.cider", lambda: _f(es.cider(multi, preds)), "pycocoevalcap", cider_ref)
|
|
912
|
+
|
|
913
|
+
def distinct_ref() -> float:
|
|
914
|
+
grams = [tuple(t[i : i + 2]) for p in preds for t in [p.split()] for i in range(len(t) - 1)]
|
|
915
|
+
return len(set(grams)) / len(grams)
|
|
916
|
+
|
|
917
|
+
c.add("text.distinct_n", lambda: _f(es.distinct_n(preds, n=2)), "Python stdlib", distinct_ref)
|
|
918
|
+
|
|
919
|
+
lps = [np.log(rng.uniform(0.05, 1.0, int(rng.integers(5, 40)))) for _ in range(m)]
|
|
920
|
+
c.add(
|
|
921
|
+
"text.perplexity",
|
|
922
|
+
lambda: _f(es.perplexity(lps)),
|
|
923
|
+
"NumPy formula",
|
|
924
|
+
lambda: float(np.exp(-np.concatenate(lps).mean())),
|
|
925
|
+
)
|
|
926
|
+
c.add(
|
|
927
|
+
"text.cross_entropy",
|
|
928
|
+
lambda: _f(es.cross_entropy(lps, base=2)),
|
|
929
|
+
"NumPy formula",
|
|
930
|
+
lambda: float(-np.concatenate(lps).mean() / np.log(2)),
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
# ---- semantic
|
|
934
|
+
dim = 32
|
|
935
|
+
re_ = rng.normal(size=(m, dim))
|
|
936
|
+
pe = re_ + rng.normal(0, 0.5, (m, dim))
|
|
937
|
+
tok_r = [rng.normal(size=(int(rng.integers(5, 20)), dim)) for _ in range(me)]
|
|
938
|
+
tok_p = [rng.normal(size=(int(rng.integers(5, 20)), dim)) for _ in range(me)]
|
|
939
|
+
|
|
940
|
+
def unit(x: Any) -> Any:
|
|
941
|
+
return x / np.linalg.norm(x, axis=-1, keepdims=True)
|
|
942
|
+
|
|
943
|
+
def bert_np() -> float:
|
|
944
|
+
out = []
|
|
945
|
+
for r_, p_ in zip(tok_r, tok_p):
|
|
946
|
+
s = unit(r_) @ unit(p_).T
|
|
947
|
+
rec, prec = s.max(1).mean(), s.max(0).mean()
|
|
948
|
+
out.append(2 * prec * rec / (prec + rec))
|
|
949
|
+
return float(np.mean(out))
|
|
950
|
+
|
|
951
|
+
c.add("text.bertscore", lambda: _f(es.bertscore(tok_r, tok_p)), "NumPy formula", bert_np)
|
|
952
|
+
c.add(
|
|
953
|
+
"text.embedding_similarity",
|
|
954
|
+
lambda: _f(es.embedding_similarity(re_, pe)),
|
|
955
|
+
"NumPy formula",
|
|
956
|
+
lambda: float(np.mean(np.sum(unit(re_) * unit(pe), 1))),
|
|
957
|
+
)
|
|
958
|
+
ot = _opt("ot")
|
|
959
|
+
|
|
960
|
+
def mover_ref() -> float:
|
|
961
|
+
out = []
|
|
962
|
+
for x, y_ in zip(tok_r, tok_p):
|
|
963
|
+
a_, b_ = np.full(len(x), 1 / len(x)), np.full(len(y_), 1 / len(y_))
|
|
964
|
+
out.append(1 - ot.emd2(a_, b_, ot.dist(unit(x), unit(y_), metric="euclidean")))
|
|
965
|
+
return float(np.mean(out))
|
|
966
|
+
|
|
967
|
+
c.add("text.moverscore", lambda: _f(es.moverscore(tok_r, tok_p)), "POT", mover_ref if ot else None)
|
|
968
|
+
hf = rng.normal(size=(me * 5, 16))
|
|
969
|
+
gf = rng.normal(0.3, 1, size=(me * 5, 16))
|
|
970
|
+
c.add("text.mauve", lambda: _f(es.mauve(hf, gf, random_state=0)))
|
|
971
|
+
scores = rng.random(m)
|
|
972
|
+
c.add(
|
|
973
|
+
"text.model_score",
|
|
974
|
+
lambda: _f(es.model_score(refs, preds, scorer=lambda r, p: scores[: len(p)].tolist(), batch_size=10**9)),
|
|
975
|
+
)
|
|
976
|
+
|
|
977
|
+
# ---- QA and factuality
|
|
978
|
+
def squad_norm(s: str) -> list[str]:
|
|
979
|
+
s = s.lower()
|
|
980
|
+
s = "".join(ch for ch in s if ch not in set("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"))
|
|
981
|
+
s = re.sub(r"\b(a|an|the)\b", " ", s)
|
|
982
|
+
return s.split()
|
|
983
|
+
|
|
984
|
+
def em_ref() -> float:
|
|
985
|
+
return float(np.mean([squad_norm(r) == squad_norm(p) for r, p in zip(refs, preds)]))
|
|
986
|
+
|
|
987
|
+
def f1_ref() -> float:
|
|
988
|
+
out = []
|
|
989
|
+
for r, p in zip(refs, preds):
|
|
990
|
+
rt, pt = squad_norm(r), squad_norm(p)
|
|
991
|
+
common = sum((Counter(rt) & Counter(pt)).values())
|
|
992
|
+
if not rt or not pt:
|
|
993
|
+
out.append(float(rt == pt))
|
|
994
|
+
elif common == 0:
|
|
995
|
+
out.append(0.0)
|
|
996
|
+
else:
|
|
997
|
+
pr, rc = common / len(pt), common / len(rt)
|
|
998
|
+
out.append(2 * pr * rc / (pr + rc))
|
|
999
|
+
return float(np.mean(out))
|
|
1000
|
+
|
|
1001
|
+
c.add("qa.exact_match", lambda: _f(es.exact_match(refs, preds)), "SQuAD formula", em_ref)
|
|
1002
|
+
c.add("qa.token_f1", lambda: _f(es.token_f1(refs, preds)), "SQuAD formula", f1_ref)
|
|
1003
|
+
|
|
1004
|
+
verdict_vals = np.array(["supported", "contradicted", "unsupported"])
|
|
1005
|
+
claims = [list(rng.choice(verdict_vals, int(rng.integers(1, 8)), p=[0.7, 0.1, 0.2])) for _ in range(m)]
|
|
1006
|
+
c.add(
|
|
1007
|
+
"text.faithfulness",
|
|
1008
|
+
lambda: _f(es.faithfulness(claims)),
|
|
1009
|
+
"NumPy formula",
|
|
1010
|
+
lambda: float(np.mean([np.mean(np.asarray(v) == "supported") for v in claims])),
|
|
1011
|
+
)
|
|
1012
|
+
c.add(
|
|
1013
|
+
"text.hallucination_rate",
|
|
1014
|
+
lambda: _f(es.hallucination_rate(claims)),
|
|
1015
|
+
"NumPy formula",
|
|
1016
|
+
lambda: float(np.mean(np.concatenate([np.asarray(v) for v in claims]) != "supported")),
|
|
1017
|
+
)
|
|
1018
|
+
support = [rng.random(int(rng.integers(1, 6))) for _ in range(m)]
|
|
1019
|
+
c.add(
|
|
1020
|
+
"text.groundedness",
|
|
1021
|
+
lambda: _f(es.groundedness(support)),
|
|
1022
|
+
"NumPy formula",
|
|
1023
|
+
lambda: float(np.mean([s.mean() for s in support])),
|
|
1024
|
+
)
|
|
1025
|
+
cites = [
|
|
1026
|
+
[
|
|
1027
|
+
{
|
|
1028
|
+
"supported": bool(rng.random() < 0.7),
|
|
1029
|
+
"citations": [bool(t) for t in rng.random(int(rng.integers(0, 4))) < 0.6],
|
|
1030
|
+
}
|
|
1031
|
+
for _ in range(int(rng.integers(1, 5)))
|
|
1032
|
+
]
|
|
1033
|
+
for _ in range(m)
|
|
1034
|
+
]
|
|
1035
|
+
c.add("text.citation_precision", lambda: _f(es.citation_precision(cites)))
|
|
1036
|
+
c.add("text.citation_recall", lambda: _f(es.citation_recall(cites)))
|
|
1037
|
+
gold = rng.choice(verdict_vals, m)
|
|
1038
|
+
predv = np.where(rng.random(m) < 0.8, gold, rng.choice(verdict_vals, m))
|
|
1039
|
+
c.add(
|
|
1040
|
+
"text.claim_verification_accuracy",
|
|
1041
|
+
lambda: _f(es.claim_verification_accuracy(gold, predv)),
|
|
1042
|
+
"NumPy formula",
|
|
1043
|
+
lambda: float(np.mean(gold == predv)),
|
|
1044
|
+
)
|
|
1045
|
+
kb = {f"f{i}" for i in range(0, 500, 2)}
|
|
1046
|
+
facts = [{f"f{int(t)}" for t in rng.integers(0, 500, int(rng.integers(1, 6)))} for _ in range(m)]
|
|
1047
|
+
c.add(
|
|
1048
|
+
"text.knowledge_consistency",
|
|
1049
|
+
lambda: _f(es.knowledge_consistency(facts, kb)),
|
|
1050
|
+
"Python stdlib",
|
|
1051
|
+
lambda: sum(len(f & kb) for f in facts) / sum(len(f) for f in facts),
|
|
1052
|
+
)
|
|
1053
|
+
tp, fp, fn = rng.integers(0, 6, m), rng.integers(0, 3, m), rng.integers(0, 3, m)
|
|
1054
|
+
tp[tp + fp + fn == 0] = 1
|
|
1055
|
+
|
|
1056
|
+
def ac_ref() -> float:
|
|
1057
|
+
return float(np.mean(tp / (tp + 0.5 * (fp + fn))))
|
|
1058
|
+
|
|
1059
|
+
c.add("text.answer_correctness", lambda: _f(es.answer_correctness(tp, fp, fn)), "NumPy formula", ac_ref)
|
|
1060
|
+
qe = rng.normal(size=(m, dim))
|
|
1061
|
+
ge = [qe[i] + rng.normal(0, 0.7, (3, dim)) for i in range(m)]
|
|
1062
|
+
c.add(
|
|
1063
|
+
"text.answer_relevance",
|
|
1064
|
+
lambda: _f(es.answer_relevance(qe, ge)),
|
|
1065
|
+
"NumPy formula",
|
|
1066
|
+
lambda: float(np.mean([np.mean(unit(g) @ unit(q)) for q, g in zip(qe, ge)])),
|
|
1067
|
+
)
|
|
1068
|
+
sa = rng.random(m) < 0.2
|
|
1069
|
+
ab = np.where(rng.random(m) < 0.85, sa, ~sa)
|
|
1070
|
+
c.add(
|
|
1071
|
+
"text.abstention_accuracy",
|
|
1072
|
+
lambda: _f(es.abstention_accuracy(sa, ab)),
|
|
1073
|
+
"NumPy formula",
|
|
1074
|
+
lambda: float(np.mean(sa == ab)),
|
|
1075
|
+
)
|
|
1076
|
+
|
|
1077
|
+
# ---- judges and preferences
|
|
1078
|
+
out_vals = np.array(["win", "tie", "loss"])
|
|
1079
|
+
outcomes = rng.choice(out_vals, m, p=[0.45, 0.15, 0.4])
|
|
1080
|
+
c.add(
|
|
1081
|
+
"text.win_rate",
|
|
1082
|
+
lambda: _f(es.win_rate(outcomes)),
|
|
1083
|
+
"NumPy formula",
|
|
1084
|
+
lambda: float(np.mean((outcomes == "win") + 0.5 * (outcomes == "tie"))),
|
|
1085
|
+
)
|
|
1086
|
+
strength = rng.normal(size=8)
|
|
1087
|
+
comps, pairs = [], []
|
|
1088
|
+
for _ in range(max(m, 400)): # enough games that every model wins and loses (the MLE exists)
|
|
1089
|
+
i, j = rng.choice(8, 2, replace=False)
|
|
1090
|
+
win = rng.random() < 1 / (1 + np.exp(strength[j] - strength[i]))
|
|
1091
|
+
w, lo = (i, j) if win else (j, i)
|
|
1092
|
+
comps.append((f"m{w}", f"m{lo}", "win"))
|
|
1093
|
+
pairs.append((int(w), int(lo)))
|
|
1094
|
+
|
|
1095
|
+
choix = _opt("choix")
|
|
1096
|
+
|
|
1097
|
+
def bt_es() -> list[float]:
|
|
1098
|
+
r = es.bradley_terry(comps)
|
|
1099
|
+
order = np.argsort([int(s[1:]) for s in (r.labels or ())])
|
|
1100
|
+
return [float(v) for v in np.asarray(r.value, float)[order]]
|
|
1101
|
+
|
|
1102
|
+
def bt_ref() -> list[float]:
|
|
1103
|
+
v = choix.mm_pairwise(8, pairs, max_iter=100_000, tol=1e-12)
|
|
1104
|
+
return [float(t) for t in v - v.mean()]
|
|
1105
|
+
|
|
1106
|
+
c.add("text.bradley_terry", bt_es, "choix", bt_ref if choix else None)
|
|
1107
|
+
|
|
1108
|
+
def elo_ref() -> list[float]:
|
|
1109
|
+
rating = {f"m{i}": 1000.0 for i in range(8)}
|
|
1110
|
+
for a_, b_, _ in comps:
|
|
1111
|
+
ea = 1 / (1 + 10 ** ((rating[b_] - rating[a_]) / 400))
|
|
1112
|
+
rating[a_] += 4 * (1 - ea)
|
|
1113
|
+
rating[b_] -= 4 * (1 - ea)
|
|
1114
|
+
return [rating[f"m{i}"] for i in range(8)]
|
|
1115
|
+
|
|
1116
|
+
def elo_es() -> list[float]:
|
|
1117
|
+
r = es.elo_ratings(comps)
|
|
1118
|
+
d = dict(zip(r.labels or (), np.asarray(r.value, float)))
|
|
1119
|
+
return [d[f"m{i}"] for i in range(8)]
|
|
1120
|
+
|
|
1121
|
+
c.add("text.elo_ratings", elo_es, "Python stdlib", elo_ref)
|
|
1122
|
+
|
|
1123
|
+
ratings = rng.integers(1, 6, (4, m)).astype(float)
|
|
1124
|
+
ratings[rng.random(ratings.shape) < 0.1] = np.nan
|
|
1125
|
+
kd = _opt("krippendorff")
|
|
1126
|
+
c.add(
|
|
1127
|
+
"text.krippendorff_alpha",
|
|
1128
|
+
lambda: _f(es.krippendorff_alpha(ratings, level="ordinal")),
|
|
1129
|
+
"krippendorff",
|
|
1130
|
+
(lambda: float(kd.alpha(reliability_data=ratings, level_of_measurement="ordinal"))) if kd else None,
|
|
1131
|
+
)
|
|
1132
|
+
item_r = rng.integers(0, 3, (m, 4))
|
|
1133
|
+
ir = _opt("statsmodels.stats.inter_rater")
|
|
1134
|
+
c.add(
|
|
1135
|
+
"text.fleiss_kappa",
|
|
1136
|
+
lambda: _f(es.fleiss_kappa(item_r)),
|
|
1137
|
+
"statsmodels",
|
|
1138
|
+
(lambda: float(ir.fleiss_kappa(ir.aggregate_raters(item_r)[0]))) if ir else None,
|
|
1139
|
+
)
|
|
1140
|
+
js = rng.integers(1, 6, m)
|
|
1141
|
+
hs = np.clip(js + rng.integers(-1, 2, m), 1, 5)
|
|
1142
|
+
skm = _opt("sklearn.metrics")
|
|
1143
|
+
c.add(
|
|
1144
|
+
"text.judge_agreement",
|
|
1145
|
+
lambda: _f(es.judge_agreement(js, hs)),
|
|
1146
|
+
"scikit-learn",
|
|
1147
|
+
(lambda: skm.cohen_kappa_score(js, hs)) if skm is not None else None,
|
|
1148
|
+
)
|
|
1149
|
+
vo = rng.choice(np.array(["A", "B"]), m)
|
|
1150
|
+
vs = np.where(rng.random(m) < 0.8, vo, rng.choice(np.array(["A", "B"]), m))
|
|
1151
|
+
c.add(
|
|
1152
|
+
"text.position_consistency",
|
|
1153
|
+
lambda: _f(es.position_consistency(vo, vs)),
|
|
1154
|
+
"NumPy formula",
|
|
1155
|
+
lambda: float(np.mean(vo == vs)),
|
|
1156
|
+
)
|
|
1157
|
+
la, lb = rng.integers(20, 400, m), rng.integers(20, 400, m)
|
|
1158
|
+
c.add("text.verbosity_bias", lambda: _f(es.verbosity_bias(vo, la, lb)))
|
|
1159
|
+
jp, hp = rng.random(m) < 0.6, rng.random(m) < 0.5
|
|
1160
|
+
c.add("text.self_preference_bias", lambda: _f(es.self_preference_bias(jp, hp)))
|
|
1161
|
+
rub = rng.integers(1, 6, (m, 3))
|
|
1162
|
+
c.add(
|
|
1163
|
+
"text.rubric_score",
|
|
1164
|
+
lambda: _f(es.rubric_score(rub)),
|
|
1165
|
+
"NumPy formula",
|
|
1166
|
+
lambda: float(np.mean((rub - 1) / 4)),
|
|
1167
|
+
)
|
|
1168
|
+
|
|
1169
|
+
# ---- reasoning
|
|
1170
|
+
ns = np.full(m, 20)
|
|
1171
|
+
nc = rng.integers(0, 21, m)
|
|
1172
|
+
|
|
1173
|
+
def pass_ref() -> float:
|
|
1174
|
+
return float(
|
|
1175
|
+
np.mean([1 - math.comb(int(a_) - int(b_), 5) / math.comb(int(a_), 5) for a_, b_ in zip(ns, nc)])
|
|
1176
|
+
)
|
|
1177
|
+
|
|
1178
|
+
c.add("reasoning.pass_at_k", lambda: _f(es.pass_at_k(ns, nc, k=5)), "Python stdlib", pass_ref)
|
|
1179
|
+
answers = [str(int(t)) for t in rng.integers(0, 50, m)]
|
|
1180
|
+
samples = [[a_ if rng.random() < 0.6 else str(int(rng.integers(0, 50))) for _ in range(5)] for a_ in answers]
|
|
1181
|
+
|
|
1182
|
+
def mv_ref() -> float:
|
|
1183
|
+
return float(np.mean([Counter(s).most_common(1)[0][0] == a_ for a_, s in zip(answers, samples)]))
|
|
1184
|
+
|
|
1185
|
+
c.add(
|
|
1186
|
+
"reasoning.majority_vote_accuracy",
|
|
1187
|
+
lambda: _f(es.majority_vote_accuracy(answers, samples)),
|
|
1188
|
+
"Python stdlib",
|
|
1189
|
+
mv_ref,
|
|
1190
|
+
)
|
|
1191
|
+
sols = [f"step one ... #### {a_}" for a_ in answers]
|
|
1192
|
+
outs = [f"so the answer is {s[0]}" for s in samples]
|
|
1193
|
+
c.add("reasoning.benchmark_accuracy", lambda: _f(es.benchmark_accuracy(sols, outs, style="gsm8k")))
|
|
1194
|
+
|
|
1195
|
+
# ---- retrieval (ranx)
|
|
1196
|
+
rel = [set(rng.choice(200, int(rng.integers(1, 6)), replace=False).tolist()) for _ in range(m)]
|
|
1197
|
+
ret = [rng.permutation(200)[:20].tolist() for _ in range(m)]
|
|
1198
|
+
ranx = _opt("ranx")
|
|
1199
|
+
|
|
1200
|
+
def ranx_ref(metric: str) -> Callable[[], float]:
|
|
1201
|
+
def run() -> float:
|
|
1202
|
+
qrels = ranx.Qrels({f"q{i}": {f"d{d}": 1 for d in r} for i, r in enumerate(rel)})
|
|
1203
|
+
run_ = ranx.Run(
|
|
1204
|
+
{f"q{i}": {f"d{d}": float(len(r) - j) for j, d in enumerate(r)} for i, r in enumerate(ret)}
|
|
1205
|
+
)
|
|
1206
|
+
return float(ranx.evaluate(qrels, run_, metric))
|
|
1207
|
+
|
|
1208
|
+
return run
|
|
1209
|
+
|
|
1210
|
+
for name, call, metric in (
|
|
1211
|
+
("hit_rate_at_k", lambda: es.hit_rate_at_k(rel, ret, k=10), "hit_rate@10"),
|
|
1212
|
+
("mean_average_precision_at_k", lambda: es.mean_average_precision_at_k(rel, ret, k=20), "map@20"),
|
|
1213
|
+
("mrr", lambda: es.mrr(rel, ret), "mrr"),
|
|
1214
|
+
("ndcg_at_k", lambda: es.ndcg_at_k(rel, ret, k=10), "ndcg@10"),
|
|
1215
|
+
("precision_at_k", lambda: es.precision_at_k(rel, ret, k=10), "precision@10"),
|
|
1216
|
+
("recall_at_k", lambda: es.recall_at_k(rel, ret, k=10), "recall@10"),
|
|
1217
|
+
):
|
|
1218
|
+
c.add(
|
|
1219
|
+
f"retrieval.{name}",
|
|
1220
|
+
lambda call=call: _f(call()), # type: ignore[misc]
|
|
1221
|
+
"ranx",
|
|
1222
|
+
ranx_ref(metric) if ranx else None,
|
|
1223
|
+
)
|
|
1224
|
+
|
|
1225
|
+
# ---- RAG and operations
|
|
1226
|
+
chunk_rel = [list(rng.random(int(rng.integers(1, 10))) < 0.5) for _ in range(m)]
|
|
1227
|
+
|
|
1228
|
+
def cp_ref() -> float:
|
|
1229
|
+
out = []
|
|
1230
|
+
for ch in chunk_rel:
|
|
1231
|
+
arr = np.asarray(ch, float)
|
|
1232
|
+
if arr.sum() == 0:
|
|
1233
|
+
out.append(0.0)
|
|
1234
|
+
continue
|
|
1235
|
+
prec = np.cumsum(arr) / np.arange(1, len(arr) + 1)
|
|
1236
|
+
out.append(float(np.sum(prec * arr) / arr.sum()))
|
|
1237
|
+
return float(np.mean(out))
|
|
1238
|
+
|
|
1239
|
+
c.add("rag.context_precision", lambda: _f(es.context_precision(chunk_rel)), "NumPy formula", cp_ref)
|
|
1240
|
+
attributed = [list(rng.random(int(rng.integers(1, 8))) < 0.7) for _ in range(m)]
|
|
1241
|
+
c.add(
|
|
1242
|
+
"rag.context_recall",
|
|
1243
|
+
lambda: _f(es.context_recall(attributed)),
|
|
1244
|
+
"NumPy formula",
|
|
1245
|
+
lambda: float(np.mean([np.mean(a_) for a_ in attributed])),
|
|
1246
|
+
)
|
|
1247
|
+
c.add(
|
|
1248
|
+
"rag.context_relevance",
|
|
1249
|
+
lambda: _f(es.context_relevance(chunk_rel)),
|
|
1250
|
+
"NumPy formula",
|
|
1251
|
+
lambda: float(np.mean([np.mean(a_) for a_ in chunk_rel])),
|
|
1252
|
+
)
|
|
1253
|
+
stage_vals = np.array(["retrieval", "generation", "evidence", "orchestration", "other", ""])
|
|
1254
|
+
stages = [s or None for s in rng.choice(stage_vals, m)]
|
|
1255
|
+
c.add("rag.failure_attribution", lambda: _numeric(es.failure_attribution(stages)))
|
|
1256
|
+
lat = rng.lognormal(5, 0.5, m)
|
|
1257
|
+
c.add(
|
|
1258
|
+
"rag.latency_summary",
|
|
1259
|
+
lambda: _f(es.latency_summary(lat, statistic="p95")),
|
|
1260
|
+
"NumPy formula",
|
|
1261
|
+
lambda: float(np.percentile(lat, 95)),
|
|
1262
|
+
)
|
|
1263
|
+
succ = rng.random(m) < 0.8
|
|
1264
|
+
c.add(
|
|
1265
|
+
"rag.task_success_rate",
|
|
1266
|
+
lambda: _f(es.task_success_rate(succ)),
|
|
1267
|
+
"NumPy formula",
|
|
1268
|
+
lambda: float(np.mean(succ)),
|
|
1269
|
+
)
|
|
1270
|
+
|
|
1271
|
+
# ---- structured output
|
|
1272
|
+
schema = {
|
|
1273
|
+
"type": "object",
|
|
1274
|
+
"properties": {
|
|
1275
|
+
"name": {"type": "string", "minLength": 1},
|
|
1276
|
+
"age": {"type": "integer", "minimum": 0},
|
|
1277
|
+
"tags": {"type": "array", "items": {"type": "string"}, "uniqueItems": True},
|
|
1278
|
+
},
|
|
1279
|
+
"required": ["name", "age"],
|
|
1280
|
+
"additionalProperties": False,
|
|
1281
|
+
}
|
|
1282
|
+
docs = []
|
|
1283
|
+
for i in range(m):
|
|
1284
|
+
doc: dict[str, Any] = {"name": f"n{i}", "age": int(rng.integers(-2, 90)), "tags": ["a", "b"]}
|
|
1285
|
+
if rng.random() < 0.1:
|
|
1286
|
+
doc["extra"] = 1
|
|
1287
|
+
if rng.random() < 0.1:
|
|
1288
|
+
del doc["name"]
|
|
1289
|
+
docs.append(doc)
|
|
1290
|
+
docs_text = [json.dumps(d_) if rng.random() > 0.05 else json.dumps(d_)[:-1] for d_ in docs]
|
|
1291
|
+
|
|
1292
|
+
def valid_json(t: str) -> bool:
|
|
1293
|
+
try:
|
|
1294
|
+
json.loads(t)
|
|
1295
|
+
except ValueError:
|
|
1296
|
+
return False
|
|
1297
|
+
return True
|
|
1298
|
+
|
|
1299
|
+
c.add(
|
|
1300
|
+
"text.json_validity",
|
|
1301
|
+
lambda: _f(es.json_validity(docs_text)),
|
|
1302
|
+
"Python stdlib",
|
|
1303
|
+
lambda: float(np.mean([valid_json(t) for t in docs_text])),
|
|
1304
|
+
)
|
|
1305
|
+
jsch = _opt("jsonschema")
|
|
1306
|
+
|
|
1307
|
+
def jsch_ref() -> float:
|
|
1308
|
+
v = jsch.Draft202012Validator(schema)
|
|
1309
|
+
return float(np.mean([valid_json(t) and v.is_valid(json.loads(t)) for t in docs_text]))
|
|
1310
|
+
|
|
1311
|
+
c.add(
|
|
1312
|
+
"text.json_schema_compliance",
|
|
1313
|
+
lambda: _f(es.json_schema_compliance(docs_text, schema)),
|
|
1314
|
+
"jsonschema",
|
|
1315
|
+
jsch_ref if jsch else None,
|
|
1316
|
+
)
|
|
1317
|
+
xmls = [f"<a><b>{i}</b></a>" if rng.random() < 0.8 else f"<a><b>{i}</a>" for i in range(m)]
|
|
1318
|
+
|
|
1319
|
+
def valid_xml(t: str) -> bool:
|
|
1320
|
+
try:
|
|
1321
|
+
ET.fromstring(t) # noqa: S314 - benchmark data generated above, not user input
|
|
1322
|
+
except ET.ParseError:
|
|
1323
|
+
return False
|
|
1324
|
+
return True
|
|
1325
|
+
|
|
1326
|
+
c.add(
|
|
1327
|
+
"text.xml_validity",
|
|
1328
|
+
lambda: _f(es.xml_validity(xmls)),
|
|
1329
|
+
"Python stdlib",
|
|
1330
|
+
lambda: float(np.mean([valid_xml(t) for t in xmls])),
|
|
1331
|
+
)
|
|
1332
|
+
expected = [{"name": d_.get("name", "x"), "age": d_["age"]} for d_ in docs]
|
|
1333
|
+
c.add("text.required_field_accuracy", lambda: _f(es.required_field_accuracy(expected, docs_text)))
|
|
1334
|
+
tools = ["search", "lookup", "calc", "email"]
|
|
1335
|
+
exp_calls = [
|
|
1336
|
+
[{"name": str(rng.choice(tools)), "arguments": {"q": f"x{int(rng.integers(0, 5))}"}}] for _ in range(m)
|
|
1337
|
+
]
|
|
1338
|
+
pred_calls = [
|
|
1339
|
+
[c_ if rng.random() < 0.8 else {"name": str(rng.choice(tools)), "arguments": {"q": "x0"}} for c_ in e_]
|
|
1340
|
+
for e_ in exp_calls
|
|
1341
|
+
]
|
|
1342
|
+
c.add(
|
|
1343
|
+
"text.tool_selection_accuracy",
|
|
1344
|
+
lambda: _f(es.tool_selection_accuracy(exp_calls, pred_calls)),
|
|
1345
|
+
"Python stdlib",
|
|
1346
|
+
lambda: float(
|
|
1347
|
+
np.mean([[x["name"] for x in e_] == [x["name"] for x in p_] for e_, p_ in zip(exp_calls, pred_calls)])
|
|
1348
|
+
),
|
|
1349
|
+
)
|
|
1350
|
+
c.add("text.tool_argument_accuracy", lambda: _f(es.tool_argument_accuracy(exp_calls, pred_calls)))
|
|
1351
|
+
c.add("text.tool_call_f1", lambda: _f(es.tool_call_f1(exp_calls, pred_calls)))
|
|
1352
|
+
api = [bool(t) for t in rng.random(m) < 0.9]
|
|
1353
|
+
c.add(
|
|
1354
|
+
"text.api_call_success_rate",
|
|
1355
|
+
lambda: _f(es.api_call_success_rate(api)),
|
|
1356
|
+
"NumPy formula",
|
|
1357
|
+
lambda: float(np.mean(api)),
|
|
1358
|
+
)
|
|
1359
|
+
checks = [list(rng.random(int(rng.integers(1, 6))) < 0.8) for _ in range(m)]
|
|
1360
|
+
c.add(
|
|
1361
|
+
"text.instruction_compliance_rate",
|
|
1362
|
+
lambda: _f(es.instruction_compliance_rate(checks)),
|
|
1363
|
+
"NumPy formula",
|
|
1364
|
+
lambda: float(np.mean([all(ch) for ch in checks])),
|
|
1365
|
+
)
|
|
1366
|
+
c.add(
|
|
1367
|
+
"text.constraint_satisfaction_rate",
|
|
1368
|
+
lambda: _f(es.constraint_satisfaction_rate(checks)),
|
|
1369
|
+
"NumPy formula",
|
|
1370
|
+
lambda: float(np.mean(np.concatenate([np.asarray(ch, float) for ch in checks]))),
|
|
1371
|
+
)
|
|
1372
|
+
fmt = [f"Answer: {rng.choice(list('ABCDE'))}" for _ in range(m)]
|
|
1373
|
+
pat = re.compile(r"Answer: [A-D]", re.DOTALL)
|
|
1374
|
+
c.add(
|
|
1375
|
+
"text.format_compliance",
|
|
1376
|
+
lambda: _f(es.format_compliance(fmt, r"Answer: [A-D]")),
|
|
1377
|
+
"Python stdlib",
|
|
1378
|
+
lambda: float(np.mean([pat.fullmatch(t) is not None for t in fmt])),
|
|
1379
|
+
)
|
|
1380
|
+
turns = [[list(rng.random(3) < 0.8) for _ in range(int(rng.integers(1, 4)))] for _ in range(m)]
|
|
1381
|
+
c.add("text.instruction_retention", lambda: _f(es.instruction_retention(turns)))
|
|
1382
|
+
wrapped = [t if rng.random() < 0.8 else f"Sure! {t}" for t in docs_text]
|
|
1383
|
+
c.add("text.extra_content_rate", lambda: _f(es.extra_content_rate(wrapped)))
|
|
1384
|
+
|
|
1385
|
+
|
|
1386
|
+
def metric_cases(n: int, rng: np.random.Generator) -> list[Case]:
|
|
1387
|
+
"""One benchmark case for every registered metric and every statistics function."""
|
|
1388
|
+
c = _Cases()
|
|
1389
|
+
for build in (_classification, _regression, _clinical, _statistics, _vision, _llm):
|
|
1390
|
+
build(c, n, rng)
|
|
1391
|
+
return sorted(c.rows, key=lambda r: r[0])
|