evalrx 0.1.2__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.
- evalrx/__init__.py +139 -0
- evalrx/agent_assets/__init__.py +2 -0
- evalrx/agent_assets/skills/README.md +28 -0
- evalrx/agent_assets/skills/eval-chart-style/SKILL.md +172 -0
- evalrx/agent_assets/skills/evalrx-report-ui/SKILL.md +116 -0
- evalrx/agent_assets/skills/nature-figure/LICENSE +201 -0
- evalrx/agent_assets/skills/nature-figure/README.md +412 -0
- evalrx/agent_assets/skills/nature-figure/SKILL.md +60 -0
- evalrx/agent_assets/skills/nature-figure/manifest.yaml +59 -0
- evalrx/agent_assets/skills/nature-figure/references/api.md +436 -0
- evalrx/agent_assets/skills/nature-figure/references/backend-selection.md +100 -0
- evalrx/agent_assets/skills/nature-figure/references/chart-types.md +281 -0
- evalrx/agent_assets/skills/nature-figure/references/common-patterns.md +350 -0
- evalrx/agent_assets/skills/nature-figure/references/demos.md +65 -0
- evalrx/agent_assets/skills/nature-figure/references/design-theory.md +439 -0
- evalrx/agent_assets/skills/nature-figure/references/figure-contract.md +93 -0
- evalrx/agent_assets/skills/nature-figure/references/figure-legend-conventions.md +71 -0
- evalrx/agent_assets/skills/nature-figure/references/nature-2026-observations.md +112 -0
- evalrx/agent_assets/skills/nature-figure/references/qa-contract.md +119 -0
- evalrx/agent_assets/skills/nature-figure/references/r-template-index.md +66 -0
- evalrx/agent_assets/skills/nature-figure/references/r-workflow.md +161 -0
- evalrx/agent_assets/skills/nature-figure/references/tutorials.md +251 -0
- evalrx/agent_assets/skills/nature-figure/static/core/contract.md +29 -0
- evalrx/agent_assets/skills/nature-figure/static/core/stance.md +37 -0
- evalrx/agent_assets/skills/nature-figure/static/fragments/backend/python.md +37 -0
- evalrx/agent_assets/skills/nature-figure/static/fragments/backend/r.md +44 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/SKILL.md +213 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/assets/analysis_report_template.md +53 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/references/model_selection.md +72 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.R +130 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.py +150 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.R +181 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.py +186 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.R +149 -0
- evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.py +177 -0
- evalrx/agent_assets/skills.py +27 -0
- evalrx/agent_runtime/__init__.py +78 -0
- evalrx/agent_runtime/_docker_runner.py +89 -0
- evalrx/agent_runtime/cli_runtime.py +103 -0
- evalrx/agent_runtime/cli_transcript.py +138 -0
- evalrx/agent_runtime/cli_types.py +68 -0
- evalrx/agent_runtime/codegen/__init__.py +5 -0
- evalrx/agent_runtime/codegen/runner.py +94 -0
- evalrx/agent_runtime/experiment_harness.py +117 -0
- evalrx/agent_runtime/factory.py +102 -0
- evalrx/agent_runtime/json_shape.py +44 -0
- evalrx/agent_runtime/judges/__init__.py +28 -0
- evalrx/agent_runtime/judges/agy.py +179 -0
- evalrx/agent_runtime/judges/autodetect.py +135 -0
- evalrx/agent_runtime/judges/claude.py +159 -0
- evalrx/agent_runtime/judges/codex.py +120 -0
- evalrx/agent_runtime/providers/__init__.py +21 -0
- evalrx/agent_runtime/providers/antigravity.py +31 -0
- evalrx/agent_runtime/providers/base.py +145 -0
- evalrx/agent_runtime/providers/claude_code.py +49 -0
- evalrx/agent_runtime/providers/codex.py +37 -0
- evalrx/agent_runtime/providers/gemini_cli.py +26 -0
- evalrx/agent_runtime/providers/kimi_cli.py +27 -0
- evalrx/agent_runtime/providers/opencode.py +27 -0
- evalrx/agent_runtime/providers/registry.py +58 -0
- evalrx/agent_runtime/sandbox.py +517 -0
- evalrx/agent_runtime/skill_audit.py +143 -0
- evalrx/agent_runtime/skills/__init__.py +19 -0
- evalrx/agent_runtime/skills/installer.py +68 -0
- evalrx/agent_runtime/skills/prompt_policy.py +86 -0
- evalrx/agent_runtime/skills/resolver.py +19 -0
- evalrx/analysis/__init__.py +132 -0
- evalrx/analysis/adjudicate.py +154 -0
- evalrx/analysis/analysis_module.py +361 -0
- evalrx/analysis/api.py +171 -0
- evalrx/analysis/case_studio.py +651 -0
- evalrx/analysis/cli.py +114 -0
- evalrx/analysis/dashboard.py +350 -0
- evalrx/analysis/eval_case_matrix.py +118 -0
- evalrx/analysis/eval_viz_theme.py +833 -0
- evalrx/analysis/explore_run.py +333 -0
- evalrx/analysis/explorer.py +1276 -0
- evalrx/analysis/failure_modes.py +607 -0
- evalrx/analysis/fused_pipeline.py +489 -0
- evalrx/analysis/holdout.py +300 -0
- evalrx/analysis/hypothesis_agent.py +230 -0
- evalrx/analysis/narration.py +177 -0
- evalrx/analysis/operationalize.py +442 -0
- evalrx/analysis/plain_language.py +42 -0
- evalrx/analysis/planner.py +283 -0
- evalrx/analysis/probe_search.py +203 -0
- evalrx/analysis/profile.py +268 -0
- evalrx/analysis/prompts/__init__.py +0 -0
- evalrx/analysis/prompts/explorer.py +417 -0
- evalrx/analysis/prompts/failure_modes.py +33 -0
- evalrx/analysis/prompts/holdout.py +27 -0
- evalrx/analysis/prompts/hypothesis_agent.py +78 -0
- evalrx/analysis/prompts/run_codebase.py +47 -0
- evalrx/analysis/prompts/stats_agent.py +72 -0
- evalrx/analysis/prompts/stats_tool_generator.py +43 -0
- evalrx/analysis/result_marker.py +47 -0
- evalrx/analysis/run_codebase.py +242 -0
- evalrx/analysis/run_view.py +205 -0
- evalrx/analysis/stage_views.py +93 -0
- evalrx/analysis/stats_agent.py +944 -0
- evalrx/analysis/stats_tool_agent.py +261 -0
- evalrx/analysis/stats_tool_generator.py +415 -0
- evalrx/analysis/stats_tools.py +1153 -0
- evalrx/analysis/trajectory_records.py +193 -0
- evalrx/analysis/workbench.py +431 -0
- evalrx/analyzers/__init__.py +42 -0
- evalrx/analyzers/agent/__init__.py +25 -0
- evalrx/analyzers/agent/counterfactual.py +84 -0
- evalrx/analyzers/agent/first_error_judge.py +96 -0
- evalrx/analyzers/agent/ignored_obs.py +81 -0
- evalrx/analyzers/agent/loop_detect.py +79 -0
- evalrx/analyzers/agent/reliability.py +165 -0
- evalrx/analyzers/agent/tool_shap.py +225 -0
- evalrx/analyzers/agent/trajectory_rubric.py +168 -0
- evalrx/analyzers/attention/__init__.py +19 -0
- evalrx/analyzers/attention/relative_attn.py +610 -0
- evalrx/analyzers/attention/rollout.py +73 -0
- evalrx/analyzers/attention/sink.py +56 -0
- evalrx/analyzers/attention/summary.py +190 -0
- evalrx/analyzers/attribution/__init__.py +6 -0
- evalrx/analyzers/attribution/generic_attn.py +31 -0
- evalrx/analyzers/attribution/gradcam.py +30 -0
- evalrx/analyzers/base.py +12 -0
- evalrx/analyzers/geometry/__init__.py +6 -0
- evalrx/analyzers/geometry/cka.py +70 -0
- evalrx/analyzers/geometry/linear_probe.py +157 -0
- evalrx/analyzers/hallucination/__init__.py +9 -0
- evalrx/analyzers/hallucination/chair.py +78 -0
- evalrx/analyzers/hallucination/opera.py +29 -0
- evalrx/analyzers/hallucination/pope.py +119 -0
- evalrx/analyzers/hallucination/selfcheck.py +155 -0
- evalrx/analyzers/hallucination/vcd.py +29 -0
- evalrx/analyzers/lens/__init__.py +7 -0
- evalrx/analyzers/lens/layer_contrast.py +133 -0
- evalrx/analyzers/lens/logit_lens.py +138 -0
- evalrx/analyzers/lens/tuned_lens.py +30 -0
- evalrx/analyzers/patching/__init__.py +5 -0
- evalrx/analyzers/patching/causal_trace.py +30 -0
- evalrx/analyzers/perturbation/__init__.py +23 -0
- evalrx/analyzers/perturbation/_shapley.py +54 -0
- evalrx/analyzers/perturbation/context_shap.py +174 -0
- evalrx/analyzers/perturbation/cot_faithfulness.py +239 -0
- evalrx/analyzers/perturbation/format_sensitivity.py +237 -0
- evalrx/analyzers/perturbation/mm_shap.py +146 -0
- evalrx/analyzers/perturbation/modality_ablation.py +196 -0
- evalrx/analyzers/perturbation/perturbation_battery.py +274 -0
- evalrx/analyzers/perturbation/prompt_contrast.py +265 -0
- evalrx/analyzers/perturbation/rise.py +94 -0
- evalrx/analyzers/perturbation/vl_shap.py +102 -0
- evalrx/analyzers/reasoning/__init__.py +33 -0
- evalrx/analyzers/reasoning/_text.py +328 -0
- evalrx/analyzers/reasoning/answer_extraction_audit.py +327 -0
- evalrx/analyzers/reasoning/arith_audit.py +226 -0
- evalrx/analyzers/reasoning/contamination.py +214 -0
- evalrx/analyzers/reasoning/knowledge_split.py +253 -0
- evalrx/analyzers/reasoning/self_repair.py +246 -0
- evalrx/analyzers/reasoning/step_rollout_value.py +216 -0
- evalrx/analyzers/reasoning/termination_audit.py +258 -0
- evalrx/analyzers/uncertainty/__init__.py +18 -0
- evalrx/analyzers/uncertainty/calibration.py +174 -0
- evalrx/analyzers/uncertainty/coverage_gap.py +199 -0
- evalrx/analyzers/uncertainty/entropy.py +90 -0
- evalrx/analyzers/uncertainty/logprob_entropy.py +69 -0
- evalrx/analyzers/uncertainty/self_consistency.py +204 -0
- evalrx/analyzers/uncertainty/verbalized_conf.py +64 -0
- evalrx/cli.py +411 -0
- evalrx/config.py +77 -0
- evalrx/contract/__init__.py +179 -0
- evalrx/contract/common.py +452 -0
- evalrx/contract/emit.py +948 -0
- evalrx/contract/export.py +237 -0
- evalrx/contract/m1.py +325 -0
- evalrx/contract/m2.py +317 -0
- evalrx/contract/m3.py +165 -0
- evalrx/contract/m4.py +130 -0
- evalrx/contract/m5.py +292 -0
- evalrx/contract/methodology.py +76 -0
- evalrx/contract/pre_m1.py +58 -0
- evalrx/contract/typescript.py +140 -0
- evalrx/core/__init__.py +85 -0
- evalrx/core/analyzer.py +174 -0
- evalrx/core/capability.py +54 -0
- evalrx/core/case.py +443 -0
- evalrx/core/experiment.py +106 -0
- evalrx/core/model.py +198 -0
- evalrx/core/pipeline.py +42 -0
- evalrx/core/registry.py +142 -0
- evalrx/core/result.py +64 -0
- evalrx/core/spec.py +173 -0
- evalrx/core/tokentype.py +165 -0
- evalrx/core/tool.py +92 -0
- evalrx/datasets/__init__.py +41 -0
- evalrx/datasets/base.py +68 -0
- evalrx/datasets/gui_os.py +52 -0
- evalrx/datasets/llm_qa.py +57 -0
- evalrx/datasets/pure_qa.py +12 -0
- evalrx/datasets/vlm_qa.py +695 -0
- evalrx/datasets/web_search_qa.py +52 -0
- evalrx/eval_agent/__init__.py +341 -0
- evalrx/eval_agent/_tools.py +81 -0
- evalrx/eval_agent/ab_runner.py +50 -0
- evalrx/eval_agent/agentic/__init__.py +43 -0
- evalrx/eval_agent/agentic/actions.py +216 -0
- evalrx/eval_agent/agentic/board.py +107 -0
- evalrx/eval_agent/agentic/loop.py +190 -0
- evalrx/eval_agent/agentic/tools.py +538 -0
- evalrx/eval_agent/checkpoint.py +57 -0
- evalrx/eval_agent/cli_agent.py +59 -0
- evalrx/eval_agent/cli_skills.py +5 -0
- evalrx/eval_agent/evolution.py +396 -0
- evalrx/eval_agent/git_manager.py +215 -0
- evalrx/eval_agent/hypothesis.py +172 -0
- evalrx/eval_agent/label_quarantine.py +209 -0
- evalrx/eval_agent/legacy.py +530 -0
- evalrx/eval_agent/log_schema.py +497 -0
- evalrx/eval_agent/loop.py +2159 -0
- evalrx/eval_agent/loop_reports.py +116 -0
- evalrx/eval_agent/model_instrumentation.py +282 -0
- evalrx/eval_agent/narration.py +193 -0
- evalrx/eval_agent/nl_runner.py +460 -0
- evalrx/eval_agent/orchestrator.py +61 -0
- evalrx/eval_agent/preregister.py +93 -0
- evalrx/eval_agent/prompts/__init__.py +1 -0
- evalrx/eval_agent/prompts/agentic.py +46 -0
- evalrx/eval_agent/prompts/case_discovery.py +25 -0
- evalrx/eval_agent/prompts/diagnosis.py +125 -0
- evalrx/eval_agent/prompts/experiment_writer.py +265 -0
- evalrx/eval_agent/prompts/explore_step.py +37 -0
- evalrx/eval_agent/prompts/fix_agent.py +257 -0
- evalrx/eval_agent/prompts/hypothesis_tester.py +15 -0
- evalrx/eval_agent/prompts/nl_runner.py +38 -0
- evalrx/eval_agent/prompts/probe_agent.py +25 -0
- evalrx/eval_agent/prompts/probe_candidate_generator.py +14 -0
- evalrx/eval_agent/prompts/probe_generator.py +35 -0
- evalrx/eval_agent/prompts/whitebox_probe_generator.py +38 -0
- evalrx/eval_agent/report.py +58 -0
- evalrx/eval_agent/run_context.py +354 -0
- evalrx/eval_agent/run_log.schema.json +1215 -0
- evalrx/eval_agent/run_logger_v2.py +1764 -0
- evalrx/eval_agent/run_metadata.py +208 -0
- evalrx/eval_agent/stages/__init__.py +56 -0
- evalrx/eval_agent/stages/case_discovery.py +293 -0
- evalrx/eval_agent/stages/diagnosis.py +1017 -0
- evalrx/eval_agent/stages/experiment_writer.py +1634 -0
- evalrx/eval_agent/stages/fix_agent.py +3916 -0
- evalrx/eval_agent/stages/fix_internals.py +499 -0
- evalrx/eval_agent/stages/fix_pipeline.py +725 -0
- evalrx/eval_agent/stages/fix_tiers.py +187 -0
- evalrx/eval_agent/stages/fix_tools.py +1034 -0
- evalrx/eval_agent/stages/hypothesis_tester.py +1014 -0
- evalrx/eval_agent/stages/probe.py +439 -0
- evalrx/eval_agent/stages/probe_agent.py +1079 -0
- evalrx/eval_agent/stages/probe_candidate_generator.py +128 -0
- evalrx/eval_agent/stages/probe_generator.py +326 -0
- evalrx/eval_agent/stages/probe_search_agent.py +106 -0
- evalrx/eval_agent/stages/protocol.py +112 -0
- evalrx/eval_agent/stages/repair_catalog.py +273 -0
- evalrx/eval_agent/stages/surgery.py +524 -0
- evalrx/eval_agent/stages/whitebox_probe_generator.py +351 -0
- evalrx/eval_agent/store.py +231 -0
- evalrx/logging_utils.py +112 -0
- evalrx/models/__init__.py +161 -0
- evalrx/models/_discover.py +101 -0
- evalrx/models/agent.py +380 -0
- evalrx/models/backends/__init__.py +58 -0
- evalrx/models/backends/api.py +169 -0
- evalrx/models/backends/base.py +57 -0
- evalrx/models/backends/gemini_compat.py +579 -0
- evalrx/models/backends/hf_local.py +2074 -0
- evalrx/models/backends/openai_compat.py +301 -0
- evalrx/models/backends/vllm_offline.py +116 -0
- evalrx/models/base.py +24 -0
- evalrx/models/blackbox/__init__.py +4 -0
- evalrx/models/blackbox/agent.py +31 -0
- evalrx/models/blackbox/base.py +29 -0
- evalrx/models/blackbox/gemini.py +279 -0
- evalrx/models/blackbox/llm_api.py +17 -0
- evalrx/models/blackbox/vlm_api.py +17 -0
- evalrx/models/compose.py +66 -0
- evalrx/models/inference.py +88 -0
- evalrx/models/paper_methods/__init__.py +8 -0
- evalrx/models/paper_methods/aad.py +53 -0
- evalrx/models/paper_methods/ifcd.py +204 -0
- evalrx/models/paper_methods/pai.py +164 -0
- evalrx/models/paper_methods/tcd.py +202 -0
- evalrx/models/paper_methods/vcd.py +45 -0
- evalrx/models/paper_methods/vicrop.py +137 -0
- evalrx/models/toolcodec.py +143 -0
- evalrx/models/tools/__init__.py +20 -0
- evalrx/models/tools/perception.py +300 -0
- evalrx/models/tools/visual.py +174 -0
- evalrx/models/whitebox/__init__.py +26 -0
- evalrx/models/whitebox/agent.py +31 -0
- evalrx/models/whitebox/base.py +24 -0
- evalrx/models/whitebox/qwen.py +61 -0
- evalrx/models/whitebox/qwen2_5_omni.py +29 -0
- evalrx/models/whitebox/qwen2_audio.py +25 -0
- evalrx/models/whitebox/qwen_omni.py +53 -0
- evalrx/models/whitebox/qwen_vl.py +62 -0
- evalrx/observability/__init__.py +21 -0
- evalrx/observability/envelope.py +122 -0
- evalrx/observability/outbox.py +111 -0
- evalrx/observability/tracer.py +882 -0
- evalrx/reporting/__init__.py +28 -0
- evalrx/reporting/case_study.py +947 -0
- evalrx/reporting/compiler.py +587 -0
- evalrx/reporting/dynamic.py +1882 -0
- evalrx/reporting/html_report.py +2225 -0
- evalrx/reporting/langfuse_exporter.py +38 -0
- evalrx/reporting/langfuse_source.py +155 -0
- evalrx/reporting/model.py +151 -0
- evalrx/reporting/run_events.py +184 -0
- evalrx/reporting/server.py +557 -0
- evalrx/reporting/stages.py +58 -0
- evalrx/reporting/static_export.py +142 -0
- evalrx/reporting/web_dist/index.html +146 -0
- evalrx/specs.py +727 -0
- evalrx/stats/__init__.py +47 -0
- evalrx/stats/api.py +192 -0
- evalrx/stats/bootstrap.py +86 -0
- evalrx/stats/ebh.py +27 -0
- evalrx/stats/evalue.py +98 -0
- evalrx/stats/friedman.py +138 -0
- evalrx/stats/mcnemar.py +40 -0
- evalrx/stats/multiplicity.py +159 -0
- evalrx/stats/subset_sampling.py +55 -0
- evalrx/term_links.py +43 -0
- evalrx/viz/__init__.py +7 -0
- evalrx/viz/labels.py +77 -0
- evalrx/viz/prompts.py +39 -0
- evalrx/viz/renderer.py +590 -0
- evalrx/viz/schema.py +36 -0
- evalrx/viz/style.py +134 -0
- evalrx-0.1.2.dist-info/METADATA +532 -0
- evalrx-0.1.2.dist-info/RECORD +339 -0
- evalrx-0.1.2.dist-info/WHEEL +5 -0
- evalrx-0.1.2.dist-info/entry_points.txt +3 -0
- evalrx-0.1.2.dist-info/licenses/LICENSE +121 -0
- evalrx-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Termination audit — *why* did the generation stop?
|
|
2
|
+
|
|
3
|
+
A missing answer has at least four unrelated causes: the decode hit the token
|
|
4
|
+
budget (truncated), the model explicitly declined (gave up), it fell into a
|
|
5
|
+
repetition loop (degenerate), or it finished cleanly and was simply wrong.
|
|
6
|
+
These need different fixes — a bigger budget, a prompt change, a decoding
|
|
7
|
+
change, a capability change — and only the last one is a reasoning failure.
|
|
8
|
+
|
|
9
|
+
This is a **confound control**, not a headline metric. Truncated and degenerate
|
|
10
|
+
cases carry short, tag-less outputs, which is exactly the signature that
|
|
11
|
+
correlates with *every* other probe's "bad" column, so leaving them in the pool
|
|
12
|
+
makes M2 attribute a decoding-budget problem to whatever mechanism is being
|
|
13
|
+
tested. Stratify on ``termination_class`` before reading any other probe.
|
|
14
|
+
|
|
15
|
+
One optional generation per non-clean case continues the truncated text: if the
|
|
16
|
+
continuation lands on an answer, the case was budget-limited, not stuck.
|
|
17
|
+
|
|
18
|
+
References:
|
|
19
|
+
- The Curious Case of Neural Text Degeneration — Holtzman et al., ICLR 2020 —
|
|
20
|
+
arXiv:1904.09751 (repetition as a decoding pathology, not a knowledge one)
|
|
21
|
+
- Measuring Faithfulness in Chain-of-Thought Reasoning — Lanham et al., 2023 —
|
|
22
|
+
arXiv:2307.13702 (truncated-reasoning protocol)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import dataclasses
|
|
28
|
+
import re
|
|
29
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional
|
|
30
|
+
|
|
31
|
+
from evalrx.analyzers.reasoning._text import (
|
|
32
|
+
answer_equal,
|
|
33
|
+
extract_answer,
|
|
34
|
+
has_answer_tag,
|
|
35
|
+
looks_like_give_up,
|
|
36
|
+
looks_truncated,
|
|
37
|
+
repetition_score,
|
|
38
|
+
)
|
|
39
|
+
from evalrx.core.analyzer import Analyzer
|
|
40
|
+
from evalrx.core.capability import Capability
|
|
41
|
+
from evalrx.core.registry import register_analyzer
|
|
42
|
+
from evalrx.core.result import Result
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
from evalrx.core.case import CaseBatch, FailureCase
|
|
46
|
+
from evalrx.core.model import Model
|
|
47
|
+
|
|
48
|
+
_CONTINUE_SUFFIX = (
|
|
49
|
+
"Continue the reasoning above from where it stops and finish it. "
|
|
50
|
+
"End with the final answer on its own last line as 'Answer: <answer>'."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
CLEAN = "clean"
|
|
54
|
+
TRUNCATED = "truncated"
|
|
55
|
+
DEGENERATE = "degenerate"
|
|
56
|
+
GAVE_UP = "gave_up"
|
|
57
|
+
NO_ANSWER = "no_answer"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@register_analyzer("termination_audit")
|
|
61
|
+
class TerminationAudit(Analyzer):
|
|
62
|
+
"""Classify how each generation ended and test whether continuing rescues it.
|
|
63
|
+
|
|
64
|
+
Hyper-parameters:
|
|
65
|
+
continue_non_clean: spend one generation continuing each non-clean case.
|
|
66
|
+
repetition_n: word n-gram width for the degeneration score.
|
|
67
|
+
repetition_thresh: repetition_score above this ⇒ ``degenerate``.
|
|
68
|
+
max_cases: label-stratified cap; 0 (the default) = every case.
|
|
69
|
+
answer_fn/match_fn: answer extraction / equality (see ``_text``).
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
name = "termination_audit"
|
|
73
|
+
requires = frozenset({Capability.GENERATE})
|
|
74
|
+
applies_to_modalities = frozenset({"text", "image"})
|
|
75
|
+
signal_docs = {
|
|
76
|
+
'clean_rate': ('Finished normally', 'Share of answers that finished normally.'),
|
|
77
|
+
'continuation_chars': 'How much more the model wrote when allowed to continue.',
|
|
78
|
+
'continuation_has_answer': 'Whether letting a cut-off answer continue produced an answer.',
|
|
79
|
+
'degenerate_rate': ('Fell into repeating', 'Share of answers that fell into repeating themselves.'),
|
|
80
|
+
'gave_up': ('Model gave up', 'Whether the model said it could not answer.'),
|
|
81
|
+
'giveup_rate': ('Gave up', 'Share of answers where the model declined to answer.'),
|
|
82
|
+
'has_answer_tag': 'Whether the model used the answer format it was asked for.',
|
|
83
|
+
'looks_truncated': ('Answer was cut off', 'Whether the answer appears to have been cut off mid-thought.'),
|
|
84
|
+
'matches_output_contract': 'Whether the answer is in the shape the task asked for.',
|
|
85
|
+
'n_cases': 'How many answers were inspected.',
|
|
86
|
+
'non_clean_rate': ('Did not finish cleanly', 'Share of answers that did NOT finish normally — cut off, repeating, or giving up.'),
|
|
87
|
+
'output_chars': ('Answer length', 'How long the answer was, in characters.'),
|
|
88
|
+
'output_words': ('Answer length (words)', 'How long the answer was, in words.'),
|
|
89
|
+
'recovered_rate': 'Share of cut-off answers that did produce an answer when allowed to continue.',
|
|
90
|
+
'repetition_score': ('Repetitiveness', 'How much the answer repeats itself. High means the model was looping.'),
|
|
91
|
+
'termination_class': 'How this answer ended: finished, cut off, repeating, or gave up.',
|
|
92
|
+
'truncation_rate': ('Cut off by length', 'Share of answers cut off by the length limit before finishing.'),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
continue_non_clean: bool = True,
|
|
98
|
+
repetition_n: int = 8,
|
|
99
|
+
repetition_thresh: float = 0.5,
|
|
100
|
+
max_cases: int = 0,
|
|
101
|
+
answer_fn: Optional[Callable[[Any], str]] = None,
|
|
102
|
+
match_fn: Optional[Callable[[Any, Any], bool]] = None,
|
|
103
|
+
) -> None:
|
|
104
|
+
super().__init__(
|
|
105
|
+
continue_non_clean=continue_non_clean,
|
|
106
|
+
repetition_n=repetition_n,
|
|
107
|
+
repetition_thresh=repetition_thresh,
|
|
108
|
+
max_cases=max_cases,
|
|
109
|
+
)
|
|
110
|
+
self.answer_fn = answer_fn or extract_answer
|
|
111
|
+
self.match_fn = match_fn or answer_equal
|
|
112
|
+
|
|
113
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> Result:
|
|
114
|
+
per_case: list[dict[str, Any]] = []
|
|
115
|
+
for case in cases.stratified_head(self.max_cases):
|
|
116
|
+
text = case.observed
|
|
117
|
+
if not str(text or "").strip():
|
|
118
|
+
# no stored output — generate one so the class is about THIS model
|
|
119
|
+
text = str(model.generate(case.inputs))
|
|
120
|
+
entry = self._classify(case, text)
|
|
121
|
+
if self.continue_non_clean and entry["termination_class"] != CLEAN:
|
|
122
|
+
entry.update(self._continue(model, case, text))
|
|
123
|
+
per_case.append(entry)
|
|
124
|
+
|
|
125
|
+
n = len(per_case)
|
|
126
|
+
counts = {
|
|
127
|
+
cls: sum(1 for c in per_case if c["termination_class"] == cls)
|
|
128
|
+
for cls in (CLEAN, TRUNCATED, DEGENERATE, GAVE_UP, NO_ANSWER)
|
|
129
|
+
}
|
|
130
|
+
rescued = [c for c in per_case if "recovered_by_continuation" in c]
|
|
131
|
+
findings: dict[str, Any] = {
|
|
132
|
+
"n_cases": n,
|
|
133
|
+
"class_counts": counts,
|
|
134
|
+
"clean_rate": round(counts[CLEAN] / n, 4) if n else None,
|
|
135
|
+
"non_clean_rate": round(1 - counts[CLEAN] / n, 4) if n else None,
|
|
136
|
+
"truncation_rate": round(counts[TRUNCATED] / n, 4) if n else None,
|
|
137
|
+
"degenerate_rate": round(counts[DEGENERATE] / n, 4) if n else None,
|
|
138
|
+
"giveup_rate": round(counts[GAVE_UP] / n, 4) if n else None,
|
|
139
|
+
"recovered_rate": (
|
|
140
|
+
round(sum(c["recovered_by_continuation"] for c in rescued) / len(rescued), 4)
|
|
141
|
+
if rescued
|
|
142
|
+
else None
|
|
143
|
+
),
|
|
144
|
+
"per_case": per_case,
|
|
145
|
+
"_caveat": (
|
|
146
|
+
"CONFOUND CONTROL — read this before any other probe. Cases with "
|
|
147
|
+
"termination_class != 'clean' have short, tag-less outputs, the "
|
|
148
|
+
"same signature every other probe scores as 'bad'; leaving them "
|
|
149
|
+
"pooled makes M2 attribute a decoding-budget or repetition "
|
|
150
|
+
"problem to whichever mechanism is under test. Stratify on "
|
|
151
|
+
"termination_class (or exclude non-clean cases) first. A high "
|
|
152
|
+
"recovered_by_continuation rate means the budget is the defect: "
|
|
153
|
+
"raise max_tokens and re-collect before diagnosing anything else. "
|
|
154
|
+
"The continuation columns are INTERVENTIONAL — held-out "
|
|
155
|
+
"verification must re-run them."
|
|
156
|
+
),
|
|
157
|
+
}
|
|
158
|
+
return Result(analyzer=self.name, model=repr(model), cases=cases, findings=findings)
|
|
159
|
+
|
|
160
|
+
# ------------------------------------------------------------------
|
|
161
|
+
def _classify(self, case: "FailureCase", text: Any) -> dict[str, Any]:
|
|
162
|
+
raw = str(text or "")
|
|
163
|
+
repetition = repetition_score(raw, self.repetition_n)
|
|
164
|
+
tagged = has_answer_tag(raw)
|
|
165
|
+
truncated = looks_truncated(raw)
|
|
166
|
+
gave_up = looks_like_give_up(raw)
|
|
167
|
+
|
|
168
|
+
# A bare short answer with no terminal punctuation and no <answer>/
|
|
169
|
+
# \boxed{} tag ("guitar", "two", "yes") is exactly what looks_truncated
|
|
170
|
+
# flags -- correctly, when it's a guess from text shape alone. But a
|
|
171
|
+
# short-answer dataset (VQA-style single word/number gold answers)
|
|
172
|
+
# produces that EXACT shape on every case that terminated perfectly
|
|
173
|
+
# normally. When the model itself has told us how generation actually
|
|
174
|
+
# ended (metadata["finish_reason"], populated by e.g. mine_cases.py's
|
|
175
|
+
# generate_with_meta -- see fix_agent.py's L0 telemetry gate for the
|
|
176
|
+
# other consumer of this same field), that real signal overrides the
|
|
177
|
+
# text-shape guess: "stop" means it did NOT hit the token budget,
|
|
178
|
+
# regardless of how terse the output looks; "length" confirms it did,
|
|
179
|
+
# even if the text happens to end in punctuation. No finish_reason on
|
|
180
|
+
# the case (API models, or examples that never wired mine_cases.py's
|
|
181
|
+
# meta path) falls back to the heuristic, unchanged from before.
|
|
182
|
+
finish_reason = (getattr(case, "metadata", {}) or {}).get("finish_reason")
|
|
183
|
+
if finish_reason == "stop":
|
|
184
|
+
truncated = False
|
|
185
|
+
elif finish_reason == "length":
|
|
186
|
+
truncated = True
|
|
187
|
+
|
|
188
|
+
contract = (getattr(case, "metadata", {}) or {}).get("output_contract") or {}
|
|
189
|
+
contract_match = _matches_output_contract(raw, contract)
|
|
190
|
+
if contract_match:
|
|
191
|
+
truncated = False
|
|
192
|
+
|
|
193
|
+
# Order matters: degeneration is checked first because a looping
|
|
194
|
+
# generation also LOOKS truncated (it is cut off mid-loop), and the fix
|
|
195
|
+
# for a loop is a decoding change, not a bigger budget.
|
|
196
|
+
if repetition >= self.repetition_thresh:
|
|
197
|
+
cls = DEGENERATE
|
|
198
|
+
elif gave_up:
|
|
199
|
+
cls = GAVE_UP
|
|
200
|
+
elif truncated:
|
|
201
|
+
cls = TRUNCATED
|
|
202
|
+
elif tagged or contract_match:
|
|
203
|
+
cls = CLEAN
|
|
204
|
+
else:
|
|
205
|
+
cls = NO_ANSWER
|
|
206
|
+
return {
|
|
207
|
+
"sample_id": case.id,
|
|
208
|
+
"termination_class": cls,
|
|
209
|
+
"repetition_score": repetition,
|
|
210
|
+
"has_answer_tag": int(tagged),
|
|
211
|
+
"looks_truncated": int(truncated),
|
|
212
|
+
"gave_up": int(gave_up),
|
|
213
|
+
"matches_output_contract": int(contract_match),
|
|
214
|
+
"output_chars": len(raw),
|
|
215
|
+
"output_words": len(raw.split()),
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
def _continue(self, model: "Model", case: "FailureCase", text: Any) -> dict[str, Any]:
|
|
219
|
+
prompt = (
|
|
220
|
+
f"{case.inputs.prompt or ''}\n\nPartial response:\n{text}\n\n{_CONTINUE_SUFFIX}"
|
|
221
|
+
)
|
|
222
|
+
continuation = str(model.generate(dataclasses.replace(case.inputs, prompt=prompt)))
|
|
223
|
+
out: dict[str, Any] = {
|
|
224
|
+
"continuation_has_answer": int(has_answer_tag(continuation)),
|
|
225
|
+
"continuation_chars": len(continuation),
|
|
226
|
+
}
|
|
227
|
+
graded = (
|
|
228
|
+
self.match_fn(self.answer_fn(continuation), case.expected)
|
|
229
|
+
if case.expected is not None
|
|
230
|
+
else None
|
|
231
|
+
)
|
|
232
|
+
# "Recovered" means the model could finish given more room: it produced
|
|
233
|
+
# an answer AND — when a gold exists — the right one.
|
|
234
|
+
out["recovered_by_continuation"] = int(
|
|
235
|
+
out["continuation_has_answer"] == 1 and graded is not False
|
|
236
|
+
)
|
|
237
|
+
if graded is not None:
|
|
238
|
+
out["continuation_correct"] = int(graded)
|
|
239
|
+
return out
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _matches_output_contract(raw: str, contract: Any) -> bool:
|
|
243
|
+
"""Return whether a terse response is complete under a declared contract."""
|
|
244
|
+
if not isinstance(contract, dict):
|
|
245
|
+
return False
|
|
246
|
+
text = str(raw or "").strip()
|
|
247
|
+
if not text:
|
|
248
|
+
return False
|
|
249
|
+
if contract.get("kind") == "multiple_choice_letter":
|
|
250
|
+
choices = {str(v).strip().upper() for v in contract.get("choices", [])}
|
|
251
|
+
return bool(choices) and text.upper() in choices
|
|
252
|
+
pattern = contract.get("pattern")
|
|
253
|
+
if isinstance(pattern, str) and pattern:
|
|
254
|
+
try:
|
|
255
|
+
return re.fullmatch(pattern, text) is not None
|
|
256
|
+
except re.error:
|
|
257
|
+
return False
|
|
258
|
+
return False
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Uncertainty analyzers — cheap, mostly black-box signals of model (un)certainty."""
|
|
2
|
+
|
|
3
|
+
from evalrx.analyzers.uncertainty.calibration import CalibrationAnalyzer
|
|
4
|
+
from evalrx.analyzers.uncertainty.coverage_gap import CoverageVerificationGap
|
|
5
|
+
from evalrx.analyzers.uncertainty.entropy import TokenEntropyAnalyzer, UncertaintyResult
|
|
6
|
+
from evalrx.analyzers.uncertainty.logprob_entropy import LogprobEntropyAnalyzer
|
|
7
|
+
from evalrx.analyzers.uncertainty.self_consistency import SelfConsistencyAnalyzer
|
|
8
|
+
from evalrx.analyzers.uncertainty.verbalized_conf import VerbalizedConfidenceAnalyzer
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"TokenEntropyAnalyzer",
|
|
12
|
+
"UncertaintyResult",
|
|
13
|
+
"LogprobEntropyAnalyzer",
|
|
14
|
+
"SelfConsistencyAnalyzer",
|
|
15
|
+
"VerbalizedConfidenceAnalyzer",
|
|
16
|
+
"CalibrationAnalyzer",
|
|
17
|
+
"CoverageVerificationGap",
|
|
18
|
+
]
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Confidence calibration — ECE / Brier / overconfidence gap against case labels.
|
|
2
|
+
|
|
3
|
+
Where :class:`LogprobEntropyAnalyzer` and :class:`VerbalizedConfidenceAnalyzer`
|
|
4
|
+
emit raw per-case uncertainty, this analyzer closes the loop against PASS/FAIL
|
|
5
|
+
labels: it bins confidence, compares each bin's stated confidence with its
|
|
6
|
+
actual accuracy, and reports expected calibration error plus the signed
|
|
7
|
+
overconfidence gap — for BOTH confidence channels (sequence logprob and
|
|
8
|
+
verbalized). A model that is wrong exactly where it is confident needs a
|
|
9
|
+
different fix (abstention / recalibration) than one that is merely inaccurate.
|
|
10
|
+
|
|
11
|
+
``requires=GENERATE``: the verbalized channel runs on every model; the logprob
|
|
12
|
+
channel needs :attr:`Capability.LOGPROBS` and is reported empty (``n=0``,
|
|
13
|
+
``logprob_channel_available=False``) on a model without it -- an API model
|
|
14
|
+
that returns no logprobs (Gemini) keeps its calibration signal instead of
|
|
15
|
+
losing the whole analyzer. Labels come from the batch, unlabeled cases are
|
|
16
|
+
skipped.
|
|
17
|
+
|
|
18
|
+
References:
|
|
19
|
+
- On Calibration of Modern Neural Networks — Guo et al., ICML 2017 —
|
|
20
|
+
arXiv:1706.04599 (ECE)
|
|
21
|
+
- Just Ask for Calibration — Tian et al., EMNLP 2023 — arXiv:2305.14975
|
|
22
|
+
- Language Models (Mostly) Know What They Know — Kadavath et al., 2022 —
|
|
23
|
+
arXiv:2207.05221
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import dataclasses
|
|
29
|
+
import math
|
|
30
|
+
import re
|
|
31
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
32
|
+
|
|
33
|
+
from evalrx.core.analyzer import Analyzer
|
|
34
|
+
from evalrx.core.capability import Capability
|
|
35
|
+
from evalrx.core.case import CaseBatch, Label
|
|
36
|
+
from evalrx.core.registry import register_analyzer
|
|
37
|
+
from evalrx.core.result import Result
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from evalrx.core.model import Model
|
|
41
|
+
|
|
42
|
+
_ELICIT = (
|
|
43
|
+
"\n\nAfter answering, state your confidence on a 0-100 scale "
|
|
44
|
+
"on its own line as: Confidence: <number>"
|
|
45
|
+
)
|
|
46
|
+
_CONF = re.compile(r"confidence[:=]?\s*(\d{1,3}(?:\.\d+)?)\s*%?", re.IGNORECASE)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _parse_conf(text: str) -> Optional[float]:
|
|
50
|
+
m = _CONF.search(str(text or ""))
|
|
51
|
+
if not m:
|
|
52
|
+
return None
|
|
53
|
+
raw = m.group(1)
|
|
54
|
+
val = float(raw)
|
|
55
|
+
if val > 1.0:
|
|
56
|
+
return max(0.0, min(1.0, val / 100.0))
|
|
57
|
+
# the elicit asks for a 0-100 scale: an integer 0/1 is a percent, while a
|
|
58
|
+
# decimal like 0.8 is taken as an (off-instruction but common) fraction
|
|
59
|
+
return max(0.0, min(1.0, val if "." in raw else val / 100.0))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def expected_calibration_error(pairs: list[tuple[float, bool]], n_bins: int) -> Optional[float]:
|
|
63
|
+
"""Standard binned ECE over ``(confidence, correct)`` pairs."""
|
|
64
|
+
if not pairs:
|
|
65
|
+
return None
|
|
66
|
+
bins: list[list[tuple[float, bool]]] = [[] for _ in range(n_bins)]
|
|
67
|
+
for conf, correct in pairs:
|
|
68
|
+
index = min(n_bins - 1, int(conf * n_bins))
|
|
69
|
+
bins[index].append((conf, correct))
|
|
70
|
+
total = len(pairs)
|
|
71
|
+
ece = 0.0
|
|
72
|
+
for bucket in bins:
|
|
73
|
+
if not bucket:
|
|
74
|
+
continue
|
|
75
|
+
mean_conf = sum(c for c, _ in bucket) / len(bucket)
|
|
76
|
+
accuracy = sum(1 for _, ok in bucket if ok) / len(bucket)
|
|
77
|
+
ece += (len(bucket) / total) * abs(mean_conf - accuracy)
|
|
78
|
+
return round(ece, 4)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@register_analyzer("calibration")
|
|
82
|
+
class CalibrationAnalyzer(Analyzer):
|
|
83
|
+
"""Label-anchored calibration: ECE, Brier and overconfidence gap for logprob and verbalized confidence.
|
|
84
|
+
|
|
85
|
+
Hyper-parameters:
|
|
86
|
+
n_bins: ECE bins.
|
|
87
|
+
max_cases: label-stratified cap (one logprobs + one generate per case); 0 (the default) = every case.
|
|
88
|
+
elicit: verbalized-confidence suffix appended to the prompt.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
name = "calibration"
|
|
92
|
+
requires = frozenset({Capability.GENERATE})
|
|
93
|
+
applies_to_modalities = frozenset({"text", "image"})
|
|
94
|
+
|
|
95
|
+
def __init__(self, n_bins: int = 10, max_cases: int = 0, elicit: str = _ELICIT) -> None:
|
|
96
|
+
super().__init__(n_bins=max(2, int(n_bins)), max_cases=max_cases, elicit=elicit)
|
|
97
|
+
|
|
98
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> Result:
|
|
99
|
+
per_case: list[dict[str, Any]] = []
|
|
100
|
+
lp_pairs: list[tuple[float, bool]] = []
|
|
101
|
+
vb_pairs: list[tuple[float, bool]] = []
|
|
102
|
+
labelled = CaseBatch([c for c in cases if c.label in (Label.PASS, Label.FAIL)])
|
|
103
|
+
n_unlabelled = len(list(cases)) - len(list(labelled))
|
|
104
|
+
has_logprobs = Capability.LOGPROBS in getattr(model, "capabilities", frozenset())
|
|
105
|
+
for case in labelled.stratified_head(self.max_cases):
|
|
106
|
+
correct = case.label == Label.PASS
|
|
107
|
+
entry: dict[str, Any] = {"sample_id": case.id, "correct": int(correct)}
|
|
108
|
+
|
|
109
|
+
# An API timeout is evidence missing for one case, not a reason to
|
|
110
|
+
# discard an otherwise useful calibration run. In particular,
|
|
111
|
+
# Gemini's transport can time out a single multimodal request while
|
|
112
|
+
# the next clip succeeds. Keep the failed case visible and carry
|
|
113
|
+
# on; summary denominators below then honestly reflect only the
|
|
114
|
+
# successful confidence measurements.
|
|
115
|
+
try:
|
|
116
|
+
toks = model.logprobs(case.inputs) if has_logprobs else []
|
|
117
|
+
except Exception as exc:
|
|
118
|
+
toks = []
|
|
119
|
+
entry["logprob_error"] = f"{type(exc).__name__}: {str(exc)[:160]}"
|
|
120
|
+
lps = [t.logprob for t in toks]
|
|
121
|
+
conf_lp = math.exp(sum(lps) / len(lps)) if lps else None
|
|
122
|
+
if conf_lp is not None:
|
|
123
|
+
conf_lp = max(0.0, min(1.0, conf_lp))
|
|
124
|
+
entry["conf_logprob"] = round(conf_lp, 4)
|
|
125
|
+
lp_pairs.append((conf_lp, correct))
|
|
126
|
+
|
|
127
|
+
elicited = dataclasses.replace(
|
|
128
|
+
case.inputs, prompt=(case.inputs.prompt or "") + self.elicit
|
|
129
|
+
)
|
|
130
|
+
try:
|
|
131
|
+
raw = str(model.generate(elicited))
|
|
132
|
+
except Exception as exc:
|
|
133
|
+
entry["verbalized_error"] = f"{type(exc).__name__}: {str(exc)[:160]}"
|
|
134
|
+
per_case.append(entry)
|
|
135
|
+
continue
|
|
136
|
+
conf_vb = _parse_conf(raw)
|
|
137
|
+
if conf_vb is not None:
|
|
138
|
+
entry["conf_verbal"] = round(conf_vb, 4)
|
|
139
|
+
vb_pairs.append((conf_vb, correct))
|
|
140
|
+
per_case.append(entry)
|
|
141
|
+
|
|
142
|
+
def _summary(pairs: list[tuple[float, bool]]) -> dict[str, Any]:
|
|
143
|
+
if not pairs:
|
|
144
|
+
return {"n": 0, "ece": None, "brier": None, "overconfidence_gap": None}
|
|
145
|
+
accuracy = sum(1 for _, ok in pairs if ok) / len(pairs)
|
|
146
|
+
mean_conf = sum(c for c, _ in pairs) / len(pairs)
|
|
147
|
+
brier = sum((c - (1.0 if ok else 0.0)) ** 2 for c, ok in pairs) / len(pairs)
|
|
148
|
+
return {
|
|
149
|
+
"n": len(pairs),
|
|
150
|
+
"ece": expected_calibration_error(pairs, self.n_bins),
|
|
151
|
+
"brier": round(brier, 4),
|
|
152
|
+
"overconfidence_gap": round(mean_conf - accuracy, 4),
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
findings: dict[str, Any] = {
|
|
156
|
+
"n_cases": len(per_case),
|
|
157
|
+
"n_unlabelled_skipped": n_unlabelled,
|
|
158
|
+
"n_bins": self.n_bins,
|
|
159
|
+
"logprob_channel_available": has_logprobs,
|
|
160
|
+
"logprob_channel": _summary(lp_pairs),
|
|
161
|
+
"verbalized_channel": _summary(vb_pairs),
|
|
162
|
+
"per_case": per_case,
|
|
163
|
+
"_caveat": (
|
|
164
|
+
"Needs PASS/FAIL labels — unlabeled cases are skipped, and ECE "
|
|
165
|
+
"on few labelled cases is unstable (aim for >= ~50). "
|
|
166
|
+
"conf_logprob is the geometric-mean token probability of the "
|
|
167
|
+
"model's own continuation — a proxy, not an answer probability; "
|
|
168
|
+
"on API backends it reflects only the returned top-k, and a model "
|
|
169
|
+
"without LOGPROBS reports only the verbalized channel. A positive "
|
|
170
|
+
"overconfidence_gap on FAIL-heavy slices argues for abstention/"
|
|
171
|
+
"recalibration fixes rather than capability fixes."
|
|
172
|
+
),
|
|
173
|
+
}
|
|
174
|
+
return Result(analyzer=self.name, model=repr(model), cases=cases, findings=findings)
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Coverage-vs-verification gap — did the model *have* the answer and fail to pick it?
|
|
2
|
+
|
|
3
|
+
pass@k and accuracy answer different questions, and the difference decides the
|
|
4
|
+
fix tier. If the gold answer appears in *some* of k samples but the majority
|
|
5
|
+
vote lands elsewhere, the capability is present and the SELECTION is broken —
|
|
6
|
+
a reranker, a verifier, or a better aggregation (L1/L2) recovers it outright.
|
|
7
|
+
If no sample is ever right, no amount of scaffolding helps and the fix has to
|
|
8
|
+
change the model (L3/L4). Reporting one accuracy number cannot tell these
|
|
9
|
+
apart, and the second is by far the more expensive mistake.
|
|
10
|
+
|
|
11
|
+
The optional self-verification arm goes one step further: it hands the model
|
|
12
|
+
its own k candidates and asks it to choose. The distance between ``pass_at_k``
|
|
13
|
+
and ``verify_correct`` is the headroom a self-verifier can actually reach, as
|
|
14
|
+
opposed to the headroom that exists in principle.
|
|
15
|
+
|
|
16
|
+
Cost: k generations per case (+1 with ``self_verify=True``).
|
|
17
|
+
|
|
18
|
+
References:
|
|
19
|
+
- Evaluating Large Language Models Trained on Code — Chen et al., 2021 —
|
|
20
|
+
arXiv:2107.03374 (the unbiased pass@k estimator)
|
|
21
|
+
- Self-Consistency Improves Chain of Thought Reasoning — Wang et al., ICLR 2023
|
|
22
|
+
— arXiv:2203.11171 (majority@k)
|
|
23
|
+
- Large Language Monkeys: Scaling Inference Compute with Repeated Sampling —
|
|
24
|
+
Brown et al., 2024 — arXiv:2407.21787 (coverage vs verification framing)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import dataclasses
|
|
30
|
+
import math
|
|
31
|
+
from collections import Counter
|
|
32
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional
|
|
33
|
+
|
|
34
|
+
from evalrx.analyzers.reasoning._text import (
|
|
35
|
+
answer_equal,
|
|
36
|
+
extract_answer,
|
|
37
|
+
normalize_answer,
|
|
38
|
+
)
|
|
39
|
+
from evalrx.core.analyzer import Analyzer
|
|
40
|
+
from evalrx.core.capability import Capability
|
|
41
|
+
from evalrx.core.registry import register_analyzer
|
|
42
|
+
from evalrx.core.result import Result
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
from evalrx.core.case import CaseBatch, FailureCase
|
|
46
|
+
from evalrx.core.model import Model
|
|
47
|
+
|
|
48
|
+
_VERIFY = (
|
|
49
|
+
"Here are candidate answers produced for the question above:\n\n{candidates}\n\n"
|
|
50
|
+
"Decide which one is correct. Give only that answer on its own last line as "
|
|
51
|
+
"'Answer: <answer>'."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def pass_at_k(n: int, c: int, k: int) -> float:
|
|
56
|
+
"""Unbiased pass@k for *c* correct out of *n* samples (Chen et al., 2021)."""
|
|
57
|
+
if n - c < k:
|
|
58
|
+
return 1.0
|
|
59
|
+
return round(1.0 - math.comb(n - c, k) / math.comb(n, k), 4)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@register_analyzer("coverage_verification_gap")
|
|
63
|
+
class CoverageVerificationGap(Analyzer):
|
|
64
|
+
"""Sample k answers and split "cannot solve" from "cannot select".
|
|
65
|
+
|
|
66
|
+
Hyper-parameters:
|
|
67
|
+
k: samples per case.
|
|
68
|
+
self_verify: +1 generation asking the model to choose among its own k.
|
|
69
|
+
max_cases: label-stratified cap (k generations each); 0 (the default) = every case.
|
|
70
|
+
gen_kwargs: passed to ``model.generate`` — REQUIRES temperature > 0;
|
|
71
|
+
at temperature 0 all k samples are identical and the gap is
|
|
72
|
+
structurally 0.
|
|
73
|
+
grader: ``callable(prediction, case) -> bool | None`` — injected the
|
|
74
|
+
same way as the agent-side ``reliability_probe``.
|
|
75
|
+
answer_fn: ``callable(text) -> str`` answer extractor.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
name = "coverage_verification_gap"
|
|
79
|
+
requires = frozenset({Capability.GENERATE})
|
|
80
|
+
applies_to_modalities = frozenset({"text", "image"})
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
k: int = 5,
|
|
85
|
+
self_verify: bool = False,
|
|
86
|
+
max_cases: int = 0,
|
|
87
|
+
gen_kwargs: Optional[dict] = None,
|
|
88
|
+
grader: Optional[Callable[[Any, "FailureCase"], Optional[bool]]] = None,
|
|
89
|
+
answer_fn: Optional[Callable[[Any], str]] = None,
|
|
90
|
+
) -> None:
|
|
91
|
+
super().__init__(
|
|
92
|
+
k=max(int(k), 1),
|
|
93
|
+
self_verify=self_verify,
|
|
94
|
+
max_cases=max_cases,
|
|
95
|
+
gen_kwargs=dict(gen_kwargs or {}),
|
|
96
|
+
)
|
|
97
|
+
# ctor names, so sklearn-style get_params() reflection works
|
|
98
|
+
self.grader = grader or _default_grader
|
|
99
|
+
self.answer_fn = answer_fn or extract_answer
|
|
100
|
+
|
|
101
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> Result:
|
|
102
|
+
per_case: list[dict[str, Any]] = []
|
|
103
|
+
for case in cases.stratified_head(self.max_cases):
|
|
104
|
+
if case.expected is None:
|
|
105
|
+
per_case.append({"sample_id": case.id, "skipped": "no gold answer"})
|
|
106
|
+
continue
|
|
107
|
+
per_case.append(self._probe_case(model, case))
|
|
108
|
+
|
|
109
|
+
scored = [c for c in per_case if "coverage_gap" in c]
|
|
110
|
+
degenerate = bool(scored) and all(c["n_unique"] == 1 for c in scored)
|
|
111
|
+
findings: dict[str, Any] = {
|
|
112
|
+
"n_cases": len(per_case),
|
|
113
|
+
"n_scored": len(scored),
|
|
114
|
+
"k": self.k,
|
|
115
|
+
"gen_kwargs": dict(self.gen_kwargs),
|
|
116
|
+
"mean_pass_at_k": _mean([c["pass_at_k"] for c in scored]),
|
|
117
|
+
"mean_majority_correct": _mean([c["majority_correct"] for c in scored]),
|
|
118
|
+
"coverage_gap_rate": _mean([c["coverage_gap"] for c in scored]),
|
|
119
|
+
"mean_verify_correct": _mean(
|
|
120
|
+
[c["verify_correct"] for c in scored if "verify_correct" in c]
|
|
121
|
+
),
|
|
122
|
+
"no_coverage_rate": _mean([1 - c["pass_at_k"] for c in scored]),
|
|
123
|
+
"degenerate_sampling": degenerate,
|
|
124
|
+
"per_case": per_case,
|
|
125
|
+
"_caveat": (
|
|
126
|
+
"coverage_gap = 'the answer was in the pool, the vote missed it' "
|
|
127
|
+
"⇒ a selection defect, fixable at L1/L2 with a verifier or "
|
|
128
|
+
"reranker. no_coverage_rate = 'never produced once in k' ⇒ a "
|
|
129
|
+
"capability defect no scaffold reaches; do not let a headline "
|
|
130
|
+
"pass@k hide which of the two dominates. REQUIRES sampling: "
|
|
131
|
+
"degenerate_sampling=true means every sample was identical "
|
|
132
|
+
"(temperature 0 or a deterministic backend) and the gap is "
|
|
133
|
+
"structurally zero, not measured. pass_at_k here uses the "
|
|
134
|
+
"unbiased estimator over exactly k draws, so with k small its "
|
|
135
|
+
"variance is large per case — read the batch rate. "
|
|
136
|
+
"INTERVENTIONAL: held-out confirmation must re-sample."
|
|
137
|
+
),
|
|
138
|
+
}
|
|
139
|
+
return Result(analyzer=self.name, model=repr(model), cases=cases, findings=findings)
|
|
140
|
+
|
|
141
|
+
# ------------------------------------------------------------------
|
|
142
|
+
def _probe_case(self, model: "Model", case: "FailureCase") -> dict[str, Any]:
|
|
143
|
+
samples = [
|
|
144
|
+
str(model.generate(case.inputs, **self.gen_kwargs)) for _ in range(self.k)
|
|
145
|
+
]
|
|
146
|
+
answers = [self.answer_fn(s) for s in samples]
|
|
147
|
+
grades = [self.grader(s, case) for s in samples]
|
|
148
|
+
graded = [g for g in grades if g is not None]
|
|
149
|
+
entry: dict[str, Any] = {"sample_id": case.id, "n_samples": len(samples)}
|
|
150
|
+
if not graded:
|
|
151
|
+
entry["skipped"] = "no sample could be graded"
|
|
152
|
+
return entry
|
|
153
|
+
|
|
154
|
+
n_correct = sum(graded)
|
|
155
|
+
counts = Counter(normalize_answer(a) for a in answers)
|
|
156
|
+
modal, modal_n = counts.most_common(1)[0]
|
|
157
|
+
modal_correct = bool(answer_equal(modal, case.expected))
|
|
158
|
+
|
|
159
|
+
entry.update(
|
|
160
|
+
{
|
|
161
|
+
"n_graded": len(graded),
|
|
162
|
+
"n_correct": n_correct,
|
|
163
|
+
"n_unique": len(counts),
|
|
164
|
+
"pass_at_k": pass_at_k(len(graded), n_correct, len(graded)),
|
|
165
|
+
"any_correct": int(n_correct > 0),
|
|
166
|
+
"majority_correct": int(modal_correct),
|
|
167
|
+
"majority_share": round(modal_n / len(answers), 4),
|
|
168
|
+
# the diagnostic column: present in the pool, missed by the vote
|
|
169
|
+
"coverage_gap": int(n_correct > 0 and not modal_correct),
|
|
170
|
+
"modal_answer": str(modal)[:80],
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
if self.self_verify:
|
|
174
|
+
listing = "\n".join(
|
|
175
|
+
f"{i + 1}. {a}" for i, a in enumerate(dict.fromkeys(answers))
|
|
176
|
+
)
|
|
177
|
+
chosen = model.generate(
|
|
178
|
+
dataclasses.replace(
|
|
179
|
+
case.inputs,
|
|
180
|
+
prompt=f"{case.inputs.prompt or ''}\n\n{_VERIFY.format(candidates=listing)}",
|
|
181
|
+
)
|
|
182
|
+
)
|
|
183
|
+
verdict = self.grader(chosen, case)
|
|
184
|
+
if verdict is not None:
|
|
185
|
+
entry["verify_correct"] = int(verdict)
|
|
186
|
+
# how much of the available headroom self-verification captured
|
|
187
|
+
entry["verifier_gap"] = int(n_correct > 0 and not verdict)
|
|
188
|
+
return entry
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _mean(values: list) -> Optional[float]:
|
|
192
|
+
clean = [v for v in values if v is not None]
|
|
193
|
+
return round(sum(clean) / len(clean), 4) if clean else None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _default_grader(prediction: Any, case: "FailureCase") -> Optional[bool]:
|
|
197
|
+
if case.expected is None:
|
|
198
|
+
return None
|
|
199
|
+
return answer_equal(extract_answer(prediction), case.expected)
|