auditkit 1.0.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.
- auditkit/README.md +99 -0
- auditkit/__init__.py +177 -0
- auditkit/__main__.py +3 -0
- auditkit/_bootstrap.py +77 -0
- auditkit/_identity_guard.py +99 -0
- auditkit/adapter.py +264 -0
- auditkit/annotator.py +339 -0
- auditkit/api.py +502 -0
- auditkit/assets/auditkit_logo.png +0 -0
- auditkit/cache.py +47 -0
- auditkit/cli.py +417 -0
- auditkit/comparison.py +563 -0
- auditkit/diff.py +265 -0
- auditkit/errors.py +54 -0
- auditkit/evaluator.py +20 -0
- auditkit/experiment.py +145 -0
- auditkit/hf_publish.py +262 -0
- auditkit/lmeval_engine.py +550 -0
- auditkit/loaders.py +121 -0
- auditkit/logs.py +18 -0
- auditkit/metric.py +199 -0
- auditkit/metrics/README.md +15 -0
- auditkit/metrics/__init__.py +0 -0
- auditkit/metrics/code.py +222 -0
- auditkit/metrics/embedding.py +131 -0
- auditkit/metrics/encoder_judge.py +423 -0
- auditkit/metrics/generation.py +331 -0
- auditkit/metrics/guard.py +412 -0
- auditkit/metrics/hallucination.py +45 -0
- auditkit/metrics/judge.py +547 -0
- auditkit/metrics/pairwise.py +153 -0
- auditkit/metrics/perf.py +53 -0
- auditkit/metrics/rag.py +149 -0
- auditkit/metrics/security.py +64 -0
- auditkit/metrics/toxicity.py +238 -0
- auditkit/model/README.md +16 -0
- auditkit/model/__init__.py +485 -0
- auditkit/model/anthropic.py +94 -0
- auditkit/model/api_gen.py +133 -0
- auditkit/model/groq_gen.py +121 -0
- auditkit/model/hf_gen.py +385 -0
- auditkit/model/lexsi.py +155 -0
- auditkit/model/litellm_gen.py +65 -0
- auditkit/model/openai.py +90 -0
- auditkit/model/openrouter_gen.py +152 -0
- auditkit/model/vllm_gen.py +316 -0
- auditkit/model_compare.py +655 -0
- auditkit/redteam/README.md +9 -0
- auditkit/redteam/__init__.py +26 -0
- auditkit/redteam/detector.py +37 -0
- auditkit/redteam/detectors/README.md +5 -0
- auditkit/redteam/detectors/builtin.py +126 -0
- auditkit/redteam/probe.py +39 -0
- auditkit/redteam/probes/README.md +5 -0
- auditkit/redteam/probes/builtin.py +85 -0
- auditkit/redteam/runner.py +206 -0
- auditkit/registry.py +65 -0
- auditkit/report.py +278 -0
- auditkit/report_format.py +52 -0
- auditkit/router.py +54 -0
- auditkit/runner.py +575 -0
- auditkit/runspec.py +159 -0
- auditkit/sample.py +40 -0
- auditkit/scenario.py +88 -0
- auditkit/scenarios/README.md +10 -0
- auditkit/scenarios/__init__.py +4 -0
- auditkit/scenarios/arc.py +33 -0
- auditkit/scenarios/gsm8k.py +32 -0
- auditkit/scenarios/hellaswag.py +33 -0
- auditkit/scenarios/humaneval.py +32 -0
- auditkit/scenarios/mmlu.py +34 -0
- auditkit/scenarios/truthfulqa.py +33 -0
- auditkit/score.py +165 -0
- auditkit/scorers.py +117 -0
- auditkit/scoring.py +79 -0
- auditkit/types.py +69 -0
- auditkit-1.0.0.dist-info/METADATA +396 -0
- auditkit-1.0.0.dist-info/RECORD +81 -0
- auditkit-1.0.0.dist-info/WHEEL +4 -0
- auditkit-1.0.0.dist-info/entry_points.txt +2 -0
- auditkit-1.0.0.dist-info/licenses/LICENSE.md +92 -0
auditkit/runspec.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""The reproducible description of a run: :class:`RunConfig` and :class:`RunSpec`.
|
|
2
|
+
|
|
3
|
+
A ``RunSpec`` binds together everything a run needs — the scenario, the model,
|
|
4
|
+
the adapter, the metrics, the config — and can distil itself to a stable
|
|
5
|
+
:meth:`RunSpec.fingerprint`. Two runs with the same fingerprint are the same
|
|
6
|
+
experiment; changing the model, the seed, the few-shot count, or the judge
|
|
7
|
+
changes the fingerprint, which is what makes results comparable and cacheable.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Any, TYPE_CHECKING, Union
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING: # avoid import cycles; these are only type hints here
|
|
18
|
+
from .adapter import Adapter
|
|
19
|
+
from .metric import Metric
|
|
20
|
+
from .model import Model
|
|
21
|
+
from .scenario import Scenario
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class RunConfig:
|
|
26
|
+
"""The knobs that shape a run (all optional, all with sane defaults)."""
|
|
27
|
+
|
|
28
|
+
num_fewshot: Union[int, None] = None
|
|
29
|
+
limit: Union[int, None] = None
|
|
30
|
+
# None (not 0): every other generation-affecting field here defaults to
|
|
31
|
+
# None so _gen_params() correctly skips it when unset. A concrete 0
|
|
32
|
+
# defeats that -- seed=0 was being force-included in every generation
|
|
33
|
+
# request whether the caller asked for it or not, and some providers
|
|
34
|
+
# (confirmed: at least one Groq model) reject the seed param outright,
|
|
35
|
+
# turning every request into a 400. Explicitly pass seed=0 if you want
|
|
36
|
+
# a real, reproducible seed sent to the backend.
|
|
37
|
+
seed: Union[int, None] = None
|
|
38
|
+
trials: int = 1
|
|
39
|
+
batch_size: Union[str, int] = "auto"
|
|
40
|
+
# Default 1 (not higher) because Runner._chunk() always splits requests
|
|
41
|
+
# into exactly `concurrency` chunks regardless of request count -- with
|
|
42
|
+
# fewer requests than concurrency, that produces empty chunks and wastes
|
|
43
|
+
# real model/API calls on them.
|
|
44
|
+
# concurrency<=1 takes Runner.execute()'s no-chunking path entirely, so
|
|
45
|
+
# this is the only value that's safe unconditionally.
|
|
46
|
+
concurrency: int = 1
|
|
47
|
+
split: SplitConfig | None = None
|
|
48
|
+
temperature: float = 0.0
|
|
49
|
+
top_p: float | None = None
|
|
50
|
+
top_k: int | None = None
|
|
51
|
+
max_tokens: int | None = None
|
|
52
|
+
stop_sequences: list[str] | None = None
|
|
53
|
+
presence_penalty: float | None = None
|
|
54
|
+
frequency_penalty: float | None = None
|
|
55
|
+
num_completions: int | None = None
|
|
56
|
+
best_of: int | None = None
|
|
57
|
+
timeout: float | None = None
|
|
58
|
+
max_retries: int = 3
|
|
59
|
+
retry_delay: float = 1.0
|
|
60
|
+
judge_model: Union[str, None] = None
|
|
61
|
+
judge_prompt_version: Union[str, None] = None
|
|
62
|
+
# On by default: RunResult.perf/model_size/token_usage are populated
|
|
63
|
+
# unless explicitly disabled. Set False to skip surfacing them (they are
|
|
64
|
+
# still measured for free during the run either way -- this only gates
|
|
65
|
+
# whether they're attached to the result; see Runner.run()) -- useful if
|
|
66
|
+
# local-backend model_size introspection's real, if best-effort, cost is
|
|
67
|
+
# unwanted. Part of the fingerprint (RunConfig.to_dict() is hashed
|
|
68
|
+
# whole), so toggling this forces a fresh run rather than replaying a
|
|
69
|
+
# cached perf-less result.
|
|
70
|
+
track_performance: bool = True
|
|
71
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
72
|
+
|
|
73
|
+
def to_dict(self) -> dict[str, Any]:
|
|
74
|
+
return {f.name: getattr(self, f.name) for f in self.__dataclass_fields__.values()}
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def from_dict(cls, data: dict[str, Any]) -> RunConfig:
|
|
78
|
+
field_names = {f.name for f in cls.__dataclass_fields__.values()}
|
|
79
|
+
valid = {k: v for k, v in data.items() if k in field_names}
|
|
80
|
+
return cls(**valid)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class SplitConfig:
|
|
85
|
+
strategy: str = "sequential"
|
|
86
|
+
train_ratio: float = 0.0
|
|
87
|
+
val_ratio: float = 0.0
|
|
88
|
+
test_ratio: float = 1.0
|
|
89
|
+
fold: int = 0
|
|
90
|
+
# Fixed, not None: Runner._split_samples()'s "random"/"stratified" paths
|
|
91
|
+
# seed `random.Random(seed)` fresh on every call. `None` draws OS entropy
|
|
92
|
+
# each time, so reusing the identical SplitConfig (or RunConfig) across a
|
|
93
|
+
# baseline and a candidate evaluate() call -- the normal "same eval
|
|
94
|
+
# config" comparison pattern -- would silently reshuffle differently each
|
|
95
|
+
# time, breaking RunComparison's sample_id-based alignment (newly_wrong,
|
|
96
|
+
# significance) even though nothing about the config looked different.
|
|
97
|
+
# A caller who genuinely wants a different shuffle per run still can, by
|
|
98
|
+
# passing a different seed= explicitly.
|
|
99
|
+
seed: int | None = 0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class RunSpec:
|
|
104
|
+
"""Everything a run needs, plus a stable fingerprint over what matters."""
|
|
105
|
+
|
|
106
|
+
scenario: "Scenario"
|
|
107
|
+
model: "Model"
|
|
108
|
+
adapter: "Adapter"
|
|
109
|
+
metrics: list["Metric"]
|
|
110
|
+
annotators: list[Any] = field(default_factory=list)
|
|
111
|
+
# Names an annotator (by its .name) whose context["extracted"] value
|
|
112
|
+
# should be scored instead of the raw output -- e.g. a RegexAnnotator
|
|
113
|
+
# pulling "42" out of "...FINAL ANSWER: 42". None (default): score the
|
|
114
|
+
# raw output, today's behavior, unchanged.
|
|
115
|
+
extracted_by: Union[str, None] = None
|
|
116
|
+
config: RunConfig = field(default_factory=RunConfig)
|
|
117
|
+
run_name: str = ""
|
|
118
|
+
|
|
119
|
+
def fingerprint(self) -> str:
|
|
120
|
+
"""A stable sha256[:16] over the identity-defining parts of the run.
|
|
121
|
+
|
|
122
|
+
Hashes the *entire* config (via ``RunConfig.to_dict()``) rather than a
|
|
123
|
+
hand-picked subset of fields, so a run-affecting setting (``limit``,
|
|
124
|
+
``batch_size``, a new field added later, ...) can never be silently
|
|
125
|
+
left out of the fingerprint the way ``limit`` previously was.
|
|
126
|
+
"""
|
|
127
|
+
def _identity(obj: Any, default_attr: str) -> Any:
|
|
128
|
+
fn = getattr(obj, "identity", None)
|
|
129
|
+
if callable(fn):
|
|
130
|
+
return fn()
|
|
131
|
+
return getattr(obj, default_attr, type(obj).__name__)
|
|
132
|
+
|
|
133
|
+
key = {
|
|
134
|
+
"engine": "native",
|
|
135
|
+
"model": _identity(self.model, "name"),
|
|
136
|
+
"scenario": getattr(self.scenario, "name", type(self.scenario).__name__),
|
|
137
|
+
# identity() captures the *prompt* (adapter template/system prompt,
|
|
138
|
+
# judge prompt/choices), not just the type — so changing a prompt
|
|
139
|
+
# changes the fingerprint and can't return a stale cached result.
|
|
140
|
+
"adapter": _identity(self.adapter, "method"),
|
|
141
|
+
"metrics": sorted(
|
|
142
|
+
json.dumps(_identity(m, "name"), sort_keys=True, default=str)
|
|
143
|
+
for m in self.metrics
|
|
144
|
+
),
|
|
145
|
+
# Annotators have no scoring effect unless extracted_by names
|
|
146
|
+
# one -- but hashing both unconditionally means changing a
|
|
147
|
+
# regex pattern (or which annotator feeds scoring) always
|
|
148
|
+
# invalidates the cache, never silently reuses a stale result
|
|
149
|
+
# scored under a different pattern.
|
|
150
|
+
"annotators": sorted(
|
|
151
|
+
json.dumps(_identity(a, "name"), sort_keys=True, default=str)
|
|
152
|
+
for a in self.annotators
|
|
153
|
+
),
|
|
154
|
+
"extracted_by": self.extracted_by,
|
|
155
|
+
"run_name": self.run_name,
|
|
156
|
+
"config": self.config.to_dict(),
|
|
157
|
+
}
|
|
158
|
+
blob = json.dumps(key, sort_keys=True, default=str).encode("utf-8")
|
|
159
|
+
return hashlib.sha256(blob).hexdigest()[:16]
|
auditkit/sample.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""The unit of evaluation: a :class:`Sample`.
|
|
2
|
+
|
|
3
|
+
A sample is one thing to ask a model and (optionally) what a correct answer
|
|
4
|
+
looks like. Most fields are optional and task-specific — an MCQ sample carries
|
|
5
|
+
``choices``, a RAG sample carries ``retrieval_context`` — so one dataclass
|
|
6
|
+
covers every task the spine handles.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any, Optional, Union
|
|
13
|
+
|
|
14
|
+
from .types import TaskKind
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Sample:
|
|
19
|
+
"""One evaluation item: what to ask, and (optionally) the reference answer."""
|
|
20
|
+
|
|
21
|
+
input: str
|
|
22
|
+
target: Optional[Union[str, int]] = None
|
|
23
|
+
id: Optional[str] = None
|
|
24
|
+
task: str = ""
|
|
25
|
+
kind: TaskKind = TaskKind.GENERATIVE
|
|
26
|
+
choices: Optional[list[str]] = None
|
|
27
|
+
retrieval_context: Optional[list[str]] = None
|
|
28
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
29
|
+
tags: list[str] = field(default_factory=list)
|
|
30
|
+
actual_output: Optional[str] = None
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def is_golden(self) -> bool:
|
|
34
|
+
"""True when the sample carries a reference answer to score against."""
|
|
35
|
+
return self.target is not None
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def input_text(self) -> str:
|
|
39
|
+
"""The input as plain text — what a text-only backend is prompted with."""
|
|
40
|
+
return self.input
|
auditkit/scenario.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Scenarios: a task and its data, yielding :class:`Sample`.
|
|
2
|
+
|
|
3
|
+
Users pass a task name, a list of samples, or a callable to ``evaluate()``; the
|
|
4
|
+
façade wraps whatever they pass into a :class:`Scenario` so the runner sees one
|
|
5
|
+
shape. :class:`ListScenario` is the trivial wrapper around an in-memory list;
|
|
6
|
+
:class:`CallableScenario` defers to a function that yields samples (a custom
|
|
7
|
+
loader, a generator, a streaming source).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
from abc import ABC, abstractmethod
|
|
15
|
+
from typing import Any, Callable, Iterable
|
|
16
|
+
|
|
17
|
+
from .sample import Sample
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Scenario(ABC):
|
|
21
|
+
"""A task plus its data, as an iterable of samples."""
|
|
22
|
+
|
|
23
|
+
name: str = "scenario"
|
|
24
|
+
|
|
25
|
+
def __init__(self, name: str = "scenario", metadata: dict[str, Any] | None = None) -> None:
|
|
26
|
+
self.name = name
|
|
27
|
+
self.metadata: dict[str, Any] = metadata or {}
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def samples(self) -> Iterable[Sample]:
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ListScenario(Scenario):
|
|
35
|
+
"""A scenario backed by an in-memory list of samples."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, samples: Iterable[Sample], name: str | None = None,
|
|
38
|
+
metadata: dict[str, Any] | None = None) -> None:
|
|
39
|
+
samples = list(samples)
|
|
40
|
+
if name is None:
|
|
41
|
+
# Deliberately excludes s.id: Runner mutates a sample's id in
|
|
42
|
+
# place the first time it's scored (`if sample.id is None:
|
|
43
|
+
# sample.id = str(index)`), so including id here meant re-running
|
|
44
|
+
# the exact same Sample objects a second time produced a
|
|
45
|
+
# different auto-generated name -- and therefore a different
|
|
46
|
+
# RunSpec.fingerprint() -- for identical content, defeating the
|
|
47
|
+
# cache purely from that side effect. Identity for caching
|
|
48
|
+
# purposes should track the data, not a bookkeeping field that
|
|
49
|
+
# changes as a side effect of a prior run.
|
|
50
|
+
# Covers every field a built-in Adapter actually reads (input,
|
|
51
|
+
# target for all; choices for MCQAdapter; retrieval_context for
|
|
52
|
+
# RAGAdapter) -- input/target alone let two samples that differ
|
|
53
|
+
# only in retrieval_context or choices collide onto the same
|
|
54
|
+
# name/fingerprint and silently share a DiskCache entry.
|
|
55
|
+
#
|
|
56
|
+
# actual_output is included for the same reason: it's what
|
|
57
|
+
# PrecomputedModel (reads_actual_output=True) actually scores in
|
|
58
|
+
# the documented "generate once, score many times" flow
|
|
59
|
+
# (ak.generate() -> model="precomputed"). Without it, two
|
|
60
|
+
# datasets sharing the same input/target but carrying DIFFERENT
|
|
61
|
+
# generated text (e.g. two different models' real outputs, or a
|
|
62
|
+
# re-generation after a prompt change) hash to the identical
|
|
63
|
+
# name/fingerprint -- confirmed live: scoring "positive" (correct,
|
|
64
|
+
# 1.0) then "negative" (wrong, should be 0.0) against the same
|
|
65
|
+
# input/target silently returned the FIRST call's cached 1.0 for
|
|
66
|
+
# the second, wrong-answer sample.
|
|
67
|
+
h = hashlib.sha256(json.dumps(
|
|
68
|
+
[(s.input, s.target, s.choices, s.retrieval_context, s.actual_output) for s in samples],
|
|
69
|
+
default=str, sort_keys=True,
|
|
70
|
+
).encode()).hexdigest()[:8]
|
|
71
|
+
name = f"inline_{len(samples)}_{h}"
|
|
72
|
+
super().__init__(name=name, metadata=metadata)
|
|
73
|
+
self._samples = samples
|
|
74
|
+
|
|
75
|
+
def samples(self) -> list[Sample]:
|
|
76
|
+
return list(self._samples)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class CallableScenario(Scenario):
|
|
80
|
+
"""A scenario that calls a function to produce samples each time it's read."""
|
|
81
|
+
|
|
82
|
+
def __init__(self, fn: Callable[[], Iterable[Sample]], name: str = "callable",
|
|
83
|
+
metadata: dict[str, Any] | None = None) -> None:
|
|
84
|
+
super().__init__(name=name, metadata=metadata)
|
|
85
|
+
self._fn = fn
|
|
86
|
+
|
|
87
|
+
def samples(self) -> list[Sample]:
|
|
88
|
+
return list(self._fn())
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Scenarios
|
|
2
|
+
|
|
3
|
+
Built-in benchmark scenarios for standardized evaluation:
|
|
4
|
+
|
|
5
|
+
- **MMLU** — Massive Multitask Language Understanding
|
|
6
|
+
- **GSM8K** — Grade School Math
|
|
7
|
+
- **ARC** — AI2 Reasoning Challenge
|
|
8
|
+
- **HellaSwag** — Commonsense NLI
|
|
9
|
+
- **HumanEval** — Code generation
|
|
10
|
+
- **TruthfulQA** — Factuality and truthfulness
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""ARC-Challenge benchmark scenario (lazy datasets import)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from auditkit.scenario import Scenario
|
|
5
|
+
from auditkit.sample import Sample
|
|
6
|
+
from auditkit.types import TaskKind
|
|
7
|
+
from auditkit.errors import ExtraNotInstalled
|
|
8
|
+
from auditkit.registry import SCENARIOS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@SCENARIOS.register("arc")
|
|
12
|
+
class ARCScenario(Scenario):
|
|
13
|
+
name = "arc"
|
|
14
|
+
|
|
15
|
+
def __init__(self, split: str = "test"):
|
|
16
|
+
self.split = split
|
|
17
|
+
super().__init__(name=f"arc_{split}")
|
|
18
|
+
|
|
19
|
+
def samples(self):
|
|
20
|
+
try:
|
|
21
|
+
import datasets
|
|
22
|
+
except ImportError:
|
|
23
|
+
raise ExtraNotInstalled("interop", "pip install auditkit[interop]")
|
|
24
|
+
data = datasets.load_dataset("arc", "ARC-Challenge", split=self.split)
|
|
25
|
+
return [
|
|
26
|
+
Sample(
|
|
27
|
+
input=row["question"],
|
|
28
|
+
choices=list(row["choices"]["text"]),
|
|
29
|
+
target=row["answerKey"],
|
|
30
|
+
kind=TaskKind.MCQ,
|
|
31
|
+
)
|
|
32
|
+
for row in data
|
|
33
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""GSM8K benchmark scenario (lazy datasets import)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from auditkit.scenario import Scenario
|
|
5
|
+
from auditkit.sample import Sample
|
|
6
|
+
from auditkit.types import TaskKind
|
|
7
|
+
from auditkit.errors import ExtraNotInstalled
|
|
8
|
+
from auditkit.registry import SCENARIOS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@SCENARIOS.register("gsm8k")
|
|
12
|
+
class GSM8KScenario(Scenario):
|
|
13
|
+
name = "gsm8k"
|
|
14
|
+
|
|
15
|
+
def __init__(self, split: str = "test"):
|
|
16
|
+
self.split = split
|
|
17
|
+
super().__init__(name=f"gsm8k_{split}")
|
|
18
|
+
|
|
19
|
+
def samples(self):
|
|
20
|
+
try:
|
|
21
|
+
import datasets
|
|
22
|
+
except ImportError:
|
|
23
|
+
raise ExtraNotInstalled("interop", "pip install auditkit[interop]")
|
|
24
|
+
data = datasets.load_dataset("gsm8k", "main", split=self.split)
|
|
25
|
+
return [
|
|
26
|
+
Sample(
|
|
27
|
+
input=row["question"],
|
|
28
|
+
target=row["answer"],
|
|
29
|
+
kind=TaskKind.GENERATIVE,
|
|
30
|
+
)
|
|
31
|
+
for row in data
|
|
32
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""HellaSwag benchmark scenario (lazy datasets import)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from auditkit.scenario import Scenario
|
|
5
|
+
from auditkit.sample import Sample
|
|
6
|
+
from auditkit.types import TaskKind
|
|
7
|
+
from auditkit.errors import ExtraNotInstalled
|
|
8
|
+
from auditkit.registry import SCENARIOS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@SCENARIOS.register("hellaswag")
|
|
12
|
+
class HellaSwagScenario(Scenario):
|
|
13
|
+
name = "hellaswag"
|
|
14
|
+
|
|
15
|
+
def __init__(self, split: str = "validation"):
|
|
16
|
+
self.split = split
|
|
17
|
+
super().__init__(name=f"hellaswag_{split}")
|
|
18
|
+
|
|
19
|
+
def samples(self):
|
|
20
|
+
try:
|
|
21
|
+
import datasets
|
|
22
|
+
except ImportError:
|
|
23
|
+
raise ExtraNotInstalled("interop", "pip install auditkit[interop]")
|
|
24
|
+
data = datasets.load_dataset("hellaswag", split=self.split)
|
|
25
|
+
return [
|
|
26
|
+
Sample(
|
|
27
|
+
input=row["ctx"],
|
|
28
|
+
choices=list(row["endings"]),
|
|
29
|
+
target=str(row["label"]),
|
|
30
|
+
kind=TaskKind.MCQ,
|
|
31
|
+
)
|
|
32
|
+
for row in data
|
|
33
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""HumanEval benchmark scenario (lazy datasets import)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from auditkit.scenario import Scenario
|
|
5
|
+
from auditkit.sample import Sample
|
|
6
|
+
from auditkit.types import TaskKind
|
|
7
|
+
from auditkit.errors import ExtraNotInstalled
|
|
8
|
+
from auditkit.registry import SCENARIOS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@SCENARIOS.register("humaneval")
|
|
12
|
+
class HumanEvalScenario(Scenario):
|
|
13
|
+
name = "humaneval"
|
|
14
|
+
|
|
15
|
+
def __init__(self, split: str = "test"):
|
|
16
|
+
self.split = split
|
|
17
|
+
super().__init__(name=f"humaneval_{split}")
|
|
18
|
+
|
|
19
|
+
def samples(self):
|
|
20
|
+
try:
|
|
21
|
+
import datasets
|
|
22
|
+
except ImportError:
|
|
23
|
+
raise ExtraNotInstalled("interop", "pip install auditkit[interop]")
|
|
24
|
+
data = datasets.load_dataset("openai_humaneval", split=self.split)
|
|
25
|
+
return [
|
|
26
|
+
Sample(
|
|
27
|
+
input=row["prompt"],
|
|
28
|
+
target=row["canonical_solution"],
|
|
29
|
+
kind=TaskKind.CODE,
|
|
30
|
+
)
|
|
31
|
+
for row in data
|
|
32
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""MMLU benchmark scenario (lazy datasets import)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from auditkit.scenario import Scenario
|
|
5
|
+
from auditkit.sample import Sample
|
|
6
|
+
from auditkit.types import TaskKind
|
|
7
|
+
from auditkit.errors import ExtraNotInstalled
|
|
8
|
+
from auditkit.registry import SCENARIOS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@SCENARIOS.register("mmlu")
|
|
12
|
+
class MMLUScenario(Scenario):
|
|
13
|
+
name = "mmlu"
|
|
14
|
+
|
|
15
|
+
def __init__(self, subject: str = "all", split: str = "test"):
|
|
16
|
+
self.subject = subject
|
|
17
|
+
self.split = split
|
|
18
|
+
super().__init__(name=f"mmlu_{subject}_{split}")
|
|
19
|
+
|
|
20
|
+
def samples(self):
|
|
21
|
+
try:
|
|
22
|
+
import datasets
|
|
23
|
+
except ImportError:
|
|
24
|
+
raise ExtraNotInstalled("interop", "pip install auditkit[interop]")
|
|
25
|
+
data = datasets.load_dataset("mmlu", self.subject, split=self.split)
|
|
26
|
+
return [
|
|
27
|
+
Sample(
|
|
28
|
+
input=row["question"],
|
|
29
|
+
choices=list(row["choices"]),
|
|
30
|
+
target=row["answer"],
|
|
31
|
+
kind=TaskKind.MCQ,
|
|
32
|
+
)
|
|
33
|
+
for row in data
|
|
34
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""TruthfulQA benchmark scenario (lazy datasets import)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from auditkit.scenario import Scenario
|
|
5
|
+
from auditkit.sample import Sample
|
|
6
|
+
from auditkit.types import TaskKind
|
|
7
|
+
from auditkit.errors import ExtraNotInstalled
|
|
8
|
+
from auditkit.registry import SCENARIOS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@SCENARIOS.register("truthfulqa")
|
|
12
|
+
class TruthfulQAScenario(Scenario):
|
|
13
|
+
name = "truthfulqa"
|
|
14
|
+
|
|
15
|
+
def __init__(self, split: str = "validation"):
|
|
16
|
+
self.split = split
|
|
17
|
+
super().__init__(name=f"truthfulqa_{split}")
|
|
18
|
+
|
|
19
|
+
def samples(self):
|
|
20
|
+
try:
|
|
21
|
+
import datasets
|
|
22
|
+
except ImportError:
|
|
23
|
+
raise ExtraNotInstalled("interop", "pip install auditkit[interop]")
|
|
24
|
+
data = datasets.load_dataset("truthfulqa", "multiple_choice", split=self.split)
|
|
25
|
+
return [
|
|
26
|
+
Sample(
|
|
27
|
+
input=row["question"],
|
|
28
|
+
choices=row["mc1_targets"]["choices"],
|
|
29
|
+
target=row["mc1_targets"]["labels"].index(1) if 1 in row["mc1_targets"]["labels"] else "0",
|
|
30
|
+
kind=TaskKind.MCQ,
|
|
31
|
+
)
|
|
32
|
+
for row in data
|
|
33
|
+
]
|
auditkit/score.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Scores, running statistics, and the pass@k / pass^k estimators.
|
|
2
|
+
|
|
3
|
+
A :class:`Score` is one measurement of one sample (a value plus what it means).
|
|
4
|
+
A :class:`Stat` accumulates many values into a mean/stderr/percentile summary
|
|
5
|
+
without holding every value in memory longer than it needs to. :func:`pass_at_k`
|
|
6
|
+
and :func:`pass_hat_k` turn "``c`` of ``n`` trials were correct" into the two
|
|
7
|
+
numbers people actually want: capability (can it ever?) and reliability (does it
|
|
8
|
+
every time?).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import math
|
|
14
|
+
import threading
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Any, Optional
|
|
17
|
+
|
|
18
|
+
from .types import DataType, Direction, ScoreKind, Source
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class Score:
|
|
23
|
+
"""One measurement of one sample.
|
|
24
|
+
|
|
25
|
+
``value`` is the number; the rest says how to read it. When ``threshold`` is
|
|
26
|
+
set, :attr:`passed` turns the number into a gate honoring ``direction`` —
|
|
27
|
+
higher-is-better for ``MAXIMIZE``, lower-is-better for ``MINIMIZE``.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
name: str
|
|
31
|
+
value: float
|
|
32
|
+
kind: ScoreKind = ScoreKind.BENCHMARK
|
|
33
|
+
data_type: DataType = DataType.NUMERIC
|
|
34
|
+
direction: Direction = Direction.MAXIMIZE
|
|
35
|
+
label: Optional[str] = None
|
|
36
|
+
reason: Optional[str] = None
|
|
37
|
+
threshold: Optional[float] = None
|
|
38
|
+
weight: float = 1.0
|
|
39
|
+
source: Source = Source.SDK
|
|
40
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def passed(self) -> Optional[bool]:
|
|
44
|
+
"""``True``/``False`` against ``threshold``, or ``None`` if ungated."""
|
|
45
|
+
if self.threshold is None:
|
|
46
|
+
return None
|
|
47
|
+
if self.direction == Direction.MINIMIZE:
|
|
48
|
+
return self.value <= self.threshold
|
|
49
|
+
return self.value >= self.threshold
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Stat:
|
|
53
|
+
"""A running summary of many values (mean, stderr, spread, percentiles).
|
|
54
|
+
|
|
55
|
+
Values are kept so percentiles can be computed exactly; everything else is
|
|
56
|
+
derived on read. Empty is safe: every reduction returns ``0.0``.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, name: str) -> None:
|
|
60
|
+
self.name = name
|
|
61
|
+
self._values: list[float] = []
|
|
62
|
+
self._lock = threading.Lock()
|
|
63
|
+
|
|
64
|
+
def add(self, value: float) -> "Stat":
|
|
65
|
+
"""Record one value; returns self so calls can be chained."""
|
|
66
|
+
with self._lock:
|
|
67
|
+
self._values.append(float(value))
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def count(self) -> int:
|
|
72
|
+
return len(self._values)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def mean(self) -> float:
|
|
76
|
+
return sum(self._values) / len(self._values) if self._values else 0.0
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def std(self) -> float:
|
|
80
|
+
"""Population standard deviation (0.0 for fewer than two values)."""
|
|
81
|
+
n = len(self._values)
|
|
82
|
+
if n < 2:
|
|
83
|
+
return 0.0
|
|
84
|
+
mu = self.mean
|
|
85
|
+
return math.sqrt(sum((v - mu) ** 2 for v in self._values) / n)
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def stderr(self) -> float:
|
|
89
|
+
n = len(self._values)
|
|
90
|
+
if n < 2:
|
|
91
|
+
return 0.0
|
|
92
|
+
return self.std / math.sqrt(n)
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def min(self) -> float:
|
|
96
|
+
return min(self._values) if self._values else 0.0
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def max(self) -> float:
|
|
100
|
+
return max(self._values) if self._values else 0.0
|
|
101
|
+
|
|
102
|
+
def percentile(self, p: float) -> float:
|
|
103
|
+
"""The ``p``-th percentile (0..100) by linear interpolation."""
|
|
104
|
+
if not self._values:
|
|
105
|
+
return 0.0
|
|
106
|
+
ordered = sorted(self._values)
|
|
107
|
+
if len(ordered) == 1:
|
|
108
|
+
return ordered[0]
|
|
109
|
+
rank = (p / 100.0) * (len(ordered) - 1)
|
|
110
|
+
lo = math.floor(rank)
|
|
111
|
+
hi = math.ceil(rank)
|
|
112
|
+
if lo == hi:
|
|
113
|
+
return ordered[lo]
|
|
114
|
+
frac = rank - lo
|
|
115
|
+
return ordered[lo] + (ordered[hi] - ordered[lo]) * frac
|
|
116
|
+
|
|
117
|
+
def to_dict(self) -> dict[str, Any]:
|
|
118
|
+
return {
|
|
119
|
+
"name": self.name,
|
|
120
|
+
"count": self.count,
|
|
121
|
+
"mean": self.mean,
|
|
122
|
+
"stderr": self.stderr,
|
|
123
|
+
"std": self.std,
|
|
124
|
+
"min": self.min,
|
|
125
|
+
"max": self.max,
|
|
126
|
+
"values": list(self._values),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def from_dict(cls, data: dict[str, Any]) -> Stat:
|
|
131
|
+
s = cls(data.get("name", ""))
|
|
132
|
+
for v in data.get("values", []):
|
|
133
|
+
s.add(v)
|
|
134
|
+
return s
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _check_draw(n: int, c: int, k: int) -> None:
|
|
138
|
+
if k > n:
|
|
139
|
+
raise ValueError(f"cannot draw k={k} from n={n} trials")
|
|
140
|
+
if c > n or c < 0:
|
|
141
|
+
raise ValueError(f"correct count c={c} out of range for n={n}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def pass_at_k(n: int, c: int, k: int) -> float:
|
|
145
|
+
"""Capability: probability that a draw of ``k`` of ``n`` trials has >=1 correct.
|
|
146
|
+
|
|
147
|
+
``1 - C(n-c, k) / C(n, k)`` — the unbiased HumanEval estimator.
|
|
148
|
+
"""
|
|
149
|
+
_check_draw(n, c, k)
|
|
150
|
+
if c == 0:
|
|
151
|
+
return 0.0
|
|
152
|
+
if n - c < k:
|
|
153
|
+
return 1.0
|
|
154
|
+
return 1.0 - math.comb(n - c, k) / math.comb(n, k)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def pass_hat_k(n: int, c: int, k: int) -> float:
|
|
158
|
+
"""Reliability: probability that all ``k`` of a draw of ``k`` are correct.
|
|
159
|
+
|
|
160
|
+
``C(c, k) / C(n, k)``.
|
|
161
|
+
"""
|
|
162
|
+
_check_draw(n, c, k)
|
|
163
|
+
if c < k:
|
|
164
|
+
return 0.0
|
|
165
|
+
return math.comb(c, k) / math.comb(n, k)
|