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
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
"""LLM-as-judge via a real encoder classifier — classification, not generation.
|
|
2
|
+
|
|
3
|
+
Every judge in ``metrics/judge.py`` (``LLMJudge``, ``GEval``, ...) needs a
|
|
4
|
+
*generative* model: it asks a second LLM to produce free text, then parses a
|
|
5
|
+
verdict out of that text with a regex/marker. :class:`EncoderJudge` is a
|
|
6
|
+
genuinely different mechanism: any `AutoModelForSequenceClassification`
|
|
7
|
+
encoder — BERT, RoBERTa, DeBERTa, ALBERT, ELECTRA, XLNet, MPNet, and any
|
|
8
|
+
other architecture Hugging Face's ``Auto*`` classes cover — used directly as
|
|
9
|
+
the judge. It never generates anything; it classifies a (candidate,
|
|
10
|
+
reference) pair in one forward pass and reads the verdict straight off the
|
|
11
|
+
model's own output distribution, with no free-text parsing at all.
|
|
12
|
+
|
|
13
|
+
**Templating for an encoder** works differently than a chat/prompt template:
|
|
14
|
+
encoder classifiers built for pair-input tasks (NLI, similarity, cross-encoder
|
|
15
|
+
ranking, ...) take exactly *two* text spans and encode them together via the
|
|
16
|
+
tokenizer's native pair-sequence support (``tokenizer(text, text_pair)`` →
|
|
17
|
+
``[CLS] text [SEP] text_pair [SEP]`` for BERT-style models, with an
|
|
18
|
+
equivalent for every other encoder architecture) — there is no chat-template
|
|
19
|
+
string to write by hand. ``text_template=``/``text_pair_template=`` here
|
|
20
|
+
just decide *which rendered fields* fill those two slots; the actual
|
|
21
|
+
pair-encoding is handled by ``transformers``' own pipeline, not by manually
|
|
22
|
+
concatenating a literal ``"[SEP]"`` into one string (which does not
|
|
23
|
+
reliably match what any given tokenizer's real special-token handling would
|
|
24
|
+
produce — see ``FactualConsistency`` in ``hallucination.py`` for the
|
|
25
|
+
older, less reliable version of that mistake).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from typing import Any, Literal, Optional
|
|
31
|
+
|
|
32
|
+
from auditkit.metric import Metric
|
|
33
|
+
from auditkit.sample import Sample
|
|
34
|
+
from auditkit.score import Score
|
|
35
|
+
from auditkit.types import Direction, ScoreKind
|
|
36
|
+
from auditkit.registry import METRICS
|
|
37
|
+
from auditkit.errors import ExtraNotInstalled
|
|
38
|
+
|
|
39
|
+
# Label names commonly used across NLI/entailment/similarity-style encoder
|
|
40
|
+
# classifiers, for auto-detecting a sensible label_map when the checkpoint's
|
|
41
|
+
# own config.id2label uses real words. Deliberately conservative -- a
|
|
42
|
+
# checkpoint with meaningless labels (LABEL_0/LABEL_1/LABEL_2, the actual
|
|
43
|
+
# default for a lot of fine-tuned classifiers) can't be guessed at all, and
|
|
44
|
+
# EncoderJudge raises rather than fabricating a mapping in that case.
|
|
45
|
+
_POSITIVE_LABEL_NAMES = frozenset({
|
|
46
|
+
"entailment", "entail", "positive", "pos", "yes", "true", "consistent",
|
|
47
|
+
"supported", "match", "duplicate", "similar", "1", "label_1",
|
|
48
|
+
})
|
|
49
|
+
_NEGATIVE_LABEL_NAMES = frozenset({
|
|
50
|
+
"contradiction", "contradict", "negative", "neg", "no", "false",
|
|
51
|
+
"inconsistent", "unsupported", "mismatch", "not_duplicate",
|
|
52
|
+
"dissimilar", "0", "label_0",
|
|
53
|
+
})
|
|
54
|
+
_NEUTRAL_LABEL_NAMES = frozenset({"neutral", "unrelated"})
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _resolve_device(device: Optional[str]) -> int | str:
|
|
58
|
+
"""Best available device for transformers.pipeline(device=...), which
|
|
59
|
+
wants an int (-1=cpu, 0=cuda:0, ...) or a device string -- unlike
|
|
60
|
+
HFGenModel's backend, which takes a plain device string throughout.
|
|
61
|
+
"""
|
|
62
|
+
if device is not None:
|
|
63
|
+
return device
|
|
64
|
+
import torch
|
|
65
|
+
if torch.cuda.is_available():
|
|
66
|
+
return 0
|
|
67
|
+
if torch.backends.mps.is_available():
|
|
68
|
+
return "mps"
|
|
69
|
+
return -1
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@METRICS.register("encoder_judge")
|
|
73
|
+
class EncoderJudge(Metric):
|
|
74
|
+
"""LLM-as-judge backed by a real encoder classifier, not a prompted LLM.
|
|
75
|
+
|
|
76
|
+
Works with **any** `AutoModelForSequenceClassification`-compatible
|
|
77
|
+
checkpoint — BERT, RoBERTa, DeBERTa, ALBERT, ELECTRA, XLNet, MPNet, and
|
|
78
|
+
so on; genericity comes for free from ``transformers``' own ``Auto*``
|
|
79
|
+
classes, no architecture-specific code here.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
model_name
|
|
84
|
+
Any encoder sequence-classification checkpoint
|
|
85
|
+
(``"microsoft/deberta-base-mnli"``, ``"roberta-large-mnli"``,
|
|
86
|
+
``"textattack/bert-base-uncased-MNLI"``, a fine-tuned similarity/
|
|
87
|
+
toxicity/sentiment classifier, ...).
|
|
88
|
+
text_template, text_pair_template
|
|
89
|
+
Placeholder templates (``{input}``/``{output}``/``{target}``,
|
|
90
|
+
alias ``{expected}``/``{context}``, same field set as
|
|
91
|
+
:class:`~auditkit.metrics.judge.LLMJudge`) rendered into the two
|
|
92
|
+
text spans handed to the tokenizer's real pair-sequence encoding.
|
|
93
|
+
Default: candidate output as the first span, reference target as
|
|
94
|
+
the second (the standard NLI-as-judge setup — "does the output
|
|
95
|
+
entail the target"). Set ``text_pair_template=None`` for a
|
|
96
|
+
single-sequence classifier (e.g. a standalone toxicity/quality
|
|
97
|
+
classifier with no reference to compare against).
|
|
98
|
+
label_map
|
|
99
|
+
``{label_name_or_index: score}`` — maps the checkpoint's own
|
|
100
|
+
output labels to a 0–1 score. Keys match case-insensitively against
|
|
101
|
+
``model.config.id2label`` values, or against the numeric index
|
|
102
|
+
(as ``int`` or the literal string a generic checkpoint uses, e.g.
|
|
103
|
+
``"LABEL_1"``). If omitted, tries to auto-detect a sensible
|
|
104
|
+
mapping from common NLI/sentiment-style label names (entailment/
|
|
105
|
+
positive/yes → 1.0, contradiction/negative/no → 0.0, neutral →
|
|
106
|
+
0.5) — **raises** rather than guessing when the checkpoint's
|
|
107
|
+
labels aren't recognizable (e.g. bare ``LABEL_0``/``LABEL_1``),
|
|
108
|
+
since fabricating a mapping for unknown labels would silently
|
|
109
|
+
misjudge every single sample.
|
|
110
|
+
aggregation
|
|
111
|
+
``"probability"`` (default): the score is the softmax-weighted sum
|
|
112
|
+
of every label's probability times its mapped score — a
|
|
113
|
+
continuous value reflecting the classifier's actual confidence,
|
|
114
|
+
not just its single best guess (e.g. an entailment probability of
|
|
115
|
+
0.85 scores 0.85, not a rounded 1.0). ``"argmax"``: score is just
|
|
116
|
+
the mapped value of the single highest-probability label (a
|
|
117
|
+
cruder, binary-flavored pass/fail).
|
|
118
|
+
device
|
|
119
|
+
``None`` (default) auto-detects CUDA/MPS/CPU, same as every other
|
|
120
|
+
real local backend in this repo. Pass an explicit
|
|
121
|
+
``transformers.pipeline``-style device otherwise.
|
|
122
|
+
|
|
123
|
+
The returned :class:`Score` always carries ``reason`` (the predicted
|
|
124
|
+
label + its probability) and ``metadata`` (the full label→probability
|
|
125
|
+
distribution), so you can inspect exactly what the classifier saw and
|
|
126
|
+
decided, the same transparency :class:`~auditkit.metrics.judge.LLMJudge`
|
|
127
|
+
gives via its raw judge-model reply.
|
|
128
|
+
|
|
129
|
+
**Two real gotchas, confirmed live across multiple checkpoints:**
|
|
130
|
+
|
|
131
|
+
- **A generic checkpoint's label *order* is not standardized, even for
|
|
132
|
+
the nominally same task.** Two real MNLI-finetuned checkpoints tested
|
|
133
|
+
against each other disagreed: ``howey/electra-base-mnli`` uses
|
|
134
|
+
``LABEL_0=entailment, LABEL_1=neutral, LABEL_2=contradiction``, while
|
|
135
|
+
a naively-assumed "standard" order (0=contradiction, 1=entailment,
|
|
136
|
+
2=neutral, matching several other real checkpoints) is exactly
|
|
137
|
+
backwards for it. There is no way to know a generic checkpoint's real
|
|
138
|
+
label semantics without checking — this is exactly why
|
|
139
|
+
auto-detection refuses to guess for labels like ``LABEL_0``/
|
|
140
|
+
``LABEL_1`` rather than assuming any particular index convention.
|
|
141
|
+
Determine the real order empirically (a couple of known
|
|
142
|
+
obviously-true/obviously-false example pairs, checking which index
|
|
143
|
+
lights up) before trusting a ``label_map`` you haven't verified.
|
|
144
|
+
- **``aggregation="probability"`` assumes single-label, mutually-
|
|
145
|
+
exclusive classification** (NLI/entailment, sentiment — probabilities
|
|
146
|
+
across all labels sum to ~1, so the weighted sum is a genuine soft
|
|
147
|
+
interpolation between verdicts). For an independent **multi-label**
|
|
148
|
+
classifier (e.g. `unitary/toxic-bert`, sigmoid outputs, several
|
|
149
|
+
labels can be true at once), mapping a label to ``0.0`` means "this
|
|
150
|
+
label contributes nothing to the score" — **not** "the complement of
|
|
151
|
+
this label." There is no way to express "1 − P(toxic)" through the
|
|
152
|
+
weighted-sum formula alone; if you want that framing specifically,
|
|
153
|
+
use a dedicated metric built for it (e.g. this repo's own
|
|
154
|
+
`ToxicityScore`) rather than `EncoderJudge`.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
name = "encoder_judge"
|
|
158
|
+
kind = ScoreKind.JUDGE
|
|
159
|
+
direction = Direction.MAXIMIZE
|
|
160
|
+
is_deterministic = True
|
|
161
|
+
required_fields = frozenset({"target"})
|
|
162
|
+
|
|
163
|
+
def __init__(
|
|
164
|
+
self,
|
|
165
|
+
model_name: str = "microsoft/deberta-base-mnli",
|
|
166
|
+
*,
|
|
167
|
+
text_template: str = "{output}",
|
|
168
|
+
text_pair_template: Optional[str] = "{target}",
|
|
169
|
+
label_map: Optional[dict[Any, float]] = None,
|
|
170
|
+
aggregation: Literal["probability", "argmax"] = "probability",
|
|
171
|
+
device: Optional[str] = None,
|
|
172
|
+
name: str = "encoder_judge",
|
|
173
|
+
) -> None:
|
|
174
|
+
self._model_name = model_name
|
|
175
|
+
self.text_template = text_template
|
|
176
|
+
self.text_pair_template = text_pair_template
|
|
177
|
+
self._label_map_arg = dict(label_map) if label_map else None
|
|
178
|
+
if aggregation not in ("probability", "argmax"):
|
|
179
|
+
raise ValueError(f"EncoderJudge: aggregation must be 'probability' or 'argmax', got {aggregation!r}")
|
|
180
|
+
self.aggregation = aggregation
|
|
181
|
+
self._device = device
|
|
182
|
+
self.name = name
|
|
183
|
+
self._pipe = None
|
|
184
|
+
self._label_map: Optional[dict[str, float]] = None # resolved lazily, needs id2label
|
|
185
|
+
|
|
186
|
+
def identity(self) -> dict:
|
|
187
|
+
return {
|
|
188
|
+
"name": self.name,
|
|
189
|
+
"model_name": self._model_name,
|
|
190
|
+
"text_template": self.text_template,
|
|
191
|
+
"text_pair_template": self.text_pair_template,
|
|
192
|
+
"label_map": self._label_map_arg,
|
|
193
|
+
"aggregation": self.aggregation,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# -- model / label-map resolution --------------------------------------
|
|
197
|
+
|
|
198
|
+
def _ensure_pipeline(self) -> None:
|
|
199
|
+
if self._pipe is not None:
|
|
200
|
+
return
|
|
201
|
+
try:
|
|
202
|
+
from transformers import pipeline
|
|
203
|
+
except ImportError:
|
|
204
|
+
raise ExtraNotInstalled("transformers", "pip install auditkit[transformers]") from None
|
|
205
|
+
self._pipe = pipeline(
|
|
206
|
+
"text-classification",
|
|
207
|
+
model=self._model_name,
|
|
208
|
+
device=_resolve_device(self._device),
|
|
209
|
+
top_k=None, # return every label's probability, not just the top one
|
|
210
|
+
)
|
|
211
|
+
self._label_map = self._resolve_label_map()
|
|
212
|
+
|
|
213
|
+
def _resolve_label_map(self) -> dict[str, float]:
|
|
214
|
+
id2label: dict[int, str] = dict(self._pipe.model.config.id2label)
|
|
215
|
+
if self._label_map_arg is not None:
|
|
216
|
+
return self._normalize_explicit_label_map(self._label_map_arg, id2label)
|
|
217
|
+
return self._auto_detect_label_map(id2label)
|
|
218
|
+
|
|
219
|
+
@staticmethod
|
|
220
|
+
def _normalize_explicit_label_map(raw: dict[Any, float], id2label: dict[int, str]) -> dict[str, float]:
|
|
221
|
+
"""Resolve a user-supplied label_map (keyed by label name, case-
|
|
222
|
+
insensitive, or by numeric index) against the real label strings
|
|
223
|
+
the pipeline will actually emit."""
|
|
224
|
+
by_lower_name = {v.lower(): v for v in id2label.values()}
|
|
225
|
+
resolved: dict[str, float] = {}
|
|
226
|
+
for key, value in raw.items():
|
|
227
|
+
if isinstance(key, int) and key in id2label:
|
|
228
|
+
resolved[id2label[key]] = value
|
|
229
|
+
continue
|
|
230
|
+
key_str = str(key).lower()
|
|
231
|
+
if key_str in by_lower_name:
|
|
232
|
+
resolved[by_lower_name[key_str]] = value
|
|
233
|
+
continue
|
|
234
|
+
# Bare numeric string ("0", "1") against index-derived generic
|
|
235
|
+
# labels (LABEL_0, LABEL_1) that id2label itself may already use.
|
|
236
|
+
if key_str.isdigit() and int(key_str) in id2label:
|
|
237
|
+
resolved[id2label[int(key_str)]] = value
|
|
238
|
+
continue
|
|
239
|
+
raise ValueError(
|
|
240
|
+
f"EncoderJudge: label_map key {key!r} doesn't match any of this "
|
|
241
|
+
f"checkpoint's real labels {list(id2label.values())!r} (by name, "
|
|
242
|
+
f"case-insensitive, or by index)."
|
|
243
|
+
)
|
|
244
|
+
return resolved
|
|
245
|
+
|
|
246
|
+
@staticmethod
|
|
247
|
+
def _auto_detect_label_map(id2label: dict[int, str]) -> dict[str, float]:
|
|
248
|
+
detected: dict[str, float] = {}
|
|
249
|
+
for label in id2label.values():
|
|
250
|
+
norm = label.lower()
|
|
251
|
+
if norm in _POSITIVE_LABEL_NAMES:
|
|
252
|
+
detected[label] = 1.0
|
|
253
|
+
elif norm in _NEGATIVE_LABEL_NAMES:
|
|
254
|
+
detected[label] = 0.0
|
|
255
|
+
elif norm in _NEUTRAL_LABEL_NAMES:
|
|
256
|
+
detected[label] = 0.5
|
|
257
|
+
if len(detected) < len(id2label):
|
|
258
|
+
raise ValueError(
|
|
259
|
+
f"EncoderJudge: couldn't auto-detect a label_map for this "
|
|
260
|
+
f"checkpoint's labels {list(id2label.values())!r} -- pass "
|
|
261
|
+
f"label_map= explicitly, e.g. label_map={{{list(id2label.values())[0]!r}: 1.0, ...}}. "
|
|
262
|
+
f"Auto-detection only recognizes common NLI/sentiment-style "
|
|
263
|
+
f"names (entailment/positive/yes -> 1.0, contradiction/"
|
|
264
|
+
f"negative/no -> 0.0, neutral -> 0.5); a checkpoint with "
|
|
265
|
+
f"generic labels (LABEL_0/LABEL_1/...) can't be guessed "
|
|
266
|
+
f"without fabricating what they mean."
|
|
267
|
+
)
|
|
268
|
+
return detected
|
|
269
|
+
|
|
270
|
+
# -- templating ----------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
def _render(self, template: str, sample: Sample, output: str) -> str:
|
|
273
|
+
fields = {
|
|
274
|
+
"input": getattr(sample, "input_text", None) or str(sample.input),
|
|
275
|
+
"output": output,
|
|
276
|
+
"expected": sample.target or "",
|
|
277
|
+
"target": sample.target or "",
|
|
278
|
+
"context": "\n".join(sample.retrieval_context or []),
|
|
279
|
+
}
|
|
280
|
+
for k, v in (sample.metadata or {}).items():
|
|
281
|
+
fields.setdefault(k, v)
|
|
282
|
+
text = template
|
|
283
|
+
for key, val in fields.items():
|
|
284
|
+
text = text.replace("{" + key + "}", str(val))
|
|
285
|
+
return text
|
|
286
|
+
|
|
287
|
+
# -- Metric API ------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
def score(self, sample: Sample, output: str, context: Any = None) -> Score:
|
|
290
|
+
self._ensure_pipeline()
|
|
291
|
+
text = self._render(self.text_template, sample, output)
|
|
292
|
+
if self.text_pair_template is not None:
|
|
293
|
+
text_pair = self._render(self.text_pair_template, sample, output)
|
|
294
|
+
pipeline_input = {"text": text, "text_pair": text_pair}
|
|
295
|
+
else:
|
|
296
|
+
pipeline_input = text
|
|
297
|
+
# truncation=True is required, not optional: BERT/RoBERTa/ELECTRA-
|
|
298
|
+
# style checkpoints use absolute position embeddings with a hard
|
|
299
|
+
# 512-token limit and crash outright (RuntimeError: tensor size
|
|
300
|
+
# mismatch) on longer combined input -- confirmed live with two
|
|
301
|
+
# long real paragraphs on a BERT checkpoint. DeBERTa (relative
|
|
302
|
+
# position embeddings, no hard limit -- its tokenizer even reports
|
|
303
|
+
# model_max_length as the classic "unbounded" sentinel value)
|
|
304
|
+
# doesn't strictly need this, but truncation=True is confirmed
|
|
305
|
+
# harmless for it too.
|
|
306
|
+
results = self._pipe(pipeline_input, truncation=True)
|
|
307
|
+
# transformers' pipeline has an inconsistent return shape depending
|
|
308
|
+
# on input form: a bare string is treated as "a batch of one" and
|
|
309
|
+
# comes back nested ([[{label,score}, ...]]), while a single
|
|
310
|
+
# {"text":..., "text_pair":...} dict comes back flat
|
|
311
|
+
# ([{label,score}, ...]) -- confirmed live, both forms used here
|
|
312
|
+
# depending on whether text_pair_template is set. Normalize either
|
|
313
|
+
# way to a flat list of per-label dicts.
|
|
314
|
+
if results and isinstance(results[0], list):
|
|
315
|
+
results = results[0]
|
|
316
|
+
probs = {r["label"]: r["score"] for r in results}
|
|
317
|
+
predicted_label = max(results, key=lambda r: r["score"])["label"]
|
|
318
|
+
predicted_prob = probs[predicted_label]
|
|
319
|
+
|
|
320
|
+
if self.aggregation == "argmax":
|
|
321
|
+
value = self._label_map.get(predicted_label, 0.0)
|
|
322
|
+
else:
|
|
323
|
+
value = sum(probs.get(label, 0.0) * mapped for label, mapped in self._label_map.items())
|
|
324
|
+
|
|
325
|
+
return Score(
|
|
326
|
+
name=self.name,
|
|
327
|
+
value=value,
|
|
328
|
+
kind=self.kind,
|
|
329
|
+
reason=f"predicted={predicted_label!r} probability={predicted_prob:.3f} aggregation={self.aggregation}",
|
|
330
|
+
metadata={"probabilities": probs, "predicted_label": predicted_label},
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# ---------------------------------------------------------------------------
|
|
335
|
+
# Prebuilt judges — ready-to-use EncoderJudge subclasses for 6 real,
|
|
336
|
+
# live-verified checkpoints spanning 5 architecture families (see
|
|
337
|
+
# docs/encoder_judge.md's checkpoint table for the full verification
|
|
338
|
+
# detail). Every constructor argument still has the exact same meaning and
|
|
339
|
+
# is fully overridable -- these only supply the *correct, checkpoint-
|
|
340
|
+
# specific defaults* (model_name, label_map, template shape) so using one
|
|
341
|
+
# doesn't require knowing that checkpoint's real label semantics up front,
|
|
342
|
+
# mirroring how Factuality/ClosedQA/Relevance are LLMJudge with a
|
|
343
|
+
# pre-filled prompt/choices, not a different mechanism.
|
|
344
|
+
# ---------------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
@METRICS.register("factuality_encoder_judge")
|
|
347
|
+
class FactualityEncoderJudge(EncoderJudge):
|
|
348
|
+
"""Factual-consistency-as-judge via DeBERTa (v1) --
|
|
349
|
+
``microsoft/deberta-base-mnli``. Real label names
|
|
350
|
+
(ENTAILMENT/NEUTRAL/CONTRADICTION); label_map is auto-detected
|
|
351
|
+
correctly with no configuration needed.
|
|
352
|
+
|
|
353
|
+
The one kept from what was originally three near-identical NLI
|
|
354
|
+
prebuilts (DeBERTa v1/v3, RoBERTa) -- all three used checkpoints with
|
|
355
|
+
real, auto-detectable labels, so they added a class name but no real
|
|
356
|
+
behavior difference over passing ``model_name=`` directly. DeBERTa v1
|
|
357
|
+
is the strongest of the three in practice: relative position
|
|
358
|
+
embeddings (no hard input-length limit, unlike RoBERTa/BERT/ELECTRA's
|
|
359
|
+
512-token absolute limit) and empirically more confident/discriminative
|
|
360
|
+
than DeBERTa-v3 on ambiguous pairs (see the removed DebertaV3NLIJudge's
|
|
361
|
+
old docstring: 0.33 vs ~0.001 for the same "unrelated sentences" test).
|
|
362
|
+
Named for its actual use case (checking whether a claim/answer is
|
|
363
|
+
factually consistent with a reference), mirroring ``Factuality`` on
|
|
364
|
+
``LLMJudge`` -- same use case, two different judge mechanisms."""
|
|
365
|
+
|
|
366
|
+
def __init__(
|
|
367
|
+
self,
|
|
368
|
+
model_name: str = "microsoft/deberta-base-mnli",
|
|
369
|
+
*,
|
|
370
|
+
text_template: str = "{output}",
|
|
371
|
+
text_pair_template: Optional[str] = "{target}",
|
|
372
|
+
label_map: Optional[dict[Any, float]] = None,
|
|
373
|
+
aggregation: Literal["probability", "argmax"] = "probability",
|
|
374
|
+
device: Optional[str] = None,
|
|
375
|
+
name: str = "factuality_encoder_judge",
|
|
376
|
+
) -> None:
|
|
377
|
+
super().__init__(
|
|
378
|
+
model_name=model_name, text_template=text_template, text_pair_template=text_pair_template,
|
|
379
|
+
label_map=label_map, aggregation=aggregation, device=device, name=name,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@METRICS.register("sentiment_encoder_judge")
|
|
384
|
+
class SentimentEncoderJudge(EncoderJudge):
|
|
385
|
+
"""Sentiment-as-judge via DistilBERT --
|
|
386
|
+
``distilbert-base-uncased-finetuned-sst-2-english``. The only
|
|
387
|
+
remaining prebuilt with a genuinely different *task* from
|
|
388
|
+
:class:`FactualityEncoderJudge` (2-way sentiment, not NLI entailment)
|
|
389
|
+
and single-sequence (no reference needed) rather than pair mode --
|
|
390
|
+
demonstrates ``text_pair_template=None`` as a prebuilt, not just a
|
|
391
|
+
base-class option. Judges ``output`` alone; ``target`` is unused.
|
|
392
|
+
|
|
393
|
+
Originally shipped alongside ``BertNLIJudge``/``ElectraNLIJudge``,
|
|
394
|
+
both since removed: both did the exact same task as
|
|
395
|
+
``FactualityEncoderJudge`` (entailment/factual-consistency) on
|
|
396
|
+
checkpoints whose only distinguishing feature was an unrecoverable,
|
|
397
|
+
empirically-verified generic label order (``textattack/bert-base-
|
|
398
|
+
uncased-MNLI``: ``LABEL_0=contradiction, LABEL_1=entailment,
|
|
399
|
+
LABEL_2=neutral``; ``howey/electra-base-mnli``: the OPPOSITE order,
|
|
400
|
+
``LABEL_0=entailment, LABEL_1=neutral, LABEL_2=contradiction``) --
|
|
401
|
+
real, checkpoint-specific knowledge worth documenting (see
|
|
402
|
+
``docs/encoder_judge.md``), but not a distinct enough task to justify
|
|
403
|
+
two more classes with no behavior difference from
|
|
404
|
+
``FactualityEncoderJudge`` beyond which checkpoint they pin."""
|
|
405
|
+
|
|
406
|
+
required_fields = frozenset()
|
|
407
|
+
|
|
408
|
+
def __init__(
|
|
409
|
+
self,
|
|
410
|
+
model_name: str = "distilbert-base-uncased-finetuned-sst-2-english",
|
|
411
|
+
*,
|
|
412
|
+
text_template: str = "{output}",
|
|
413
|
+
text_pair_template: Optional[str] = None,
|
|
414
|
+
label_map: Optional[dict[Any, float]] = None,
|
|
415
|
+
aggregation: Literal["probability", "argmax"] = "probability",
|
|
416
|
+
device: Optional[str] = None,
|
|
417
|
+
name: str = "sentiment_encoder_judge",
|
|
418
|
+
) -> None:
|
|
419
|
+
super().__init__(
|
|
420
|
+
model_name=model_name, text_template=text_template, text_pair_template=text_pair_template,
|
|
421
|
+
label_map=label_map if label_map is not None else {"POSITIVE": 1.0, "NEGATIVE": 0.0},
|
|
422
|
+
aggregation=aggregation, device=device, name=name,
|
|
423
|
+
)
|