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,29 @@
|
|
|
1
|
+
"""OPERA — over-trust penalty + retrospection-allocation on attention (Stage 2).
|
|
2
|
+
|
|
3
|
+
Diagnoses object hallucination from the attention pattern (over-trust on a few
|
|
4
|
+
summary tokens). ``requires=ATTENTION`` (+ decode-loop control for the mitigation).
|
|
5
|
+
White-box, VLM.
|
|
6
|
+
|
|
7
|
+
References:
|
|
8
|
+
- OPERA: Alleviating Hallucination in MLLMs via Over-trust Penalty and Retrospection-Allocation
|
|
9
|
+
Huang et al., CVPR 2024 — arXiv:2311.17911
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from evalrx.core.analyzer import Analyzer
|
|
15
|
+
from evalrx.core.capability import Capability
|
|
16
|
+
from evalrx.core.registry import register_analyzer
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@register_analyzer("opera")
|
|
20
|
+
class OPERAAnalyzer(Analyzer):
|
|
21
|
+
name = "opera"
|
|
22
|
+
requires = frozenset({Capability.ATTENTION})
|
|
23
|
+
applies_to_modalities = frozenset({"image"})
|
|
24
|
+
# An omni model DECLARES image even on an audio benchmark, so the registry
|
|
25
|
+
# match alone still offers this on a batch with no images in it.
|
|
26
|
+
requires_modalities = frozenset({"image"})
|
|
27
|
+
|
|
28
|
+
def _run(self, model, cases):
|
|
29
|
+
raise NotImplementedError("Stage 2: attention over-trust diagnosis (needs decode-loop control).")
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""POPE — Polling-based Object Probing Evaluation.
|
|
2
|
+
|
|
3
|
+
Black-box (``GENERATE``): pose yes/no object-presence questions about an image and
|
|
4
|
+
score the model's answers against gold labels. Each case carries the gold label
|
|
5
|
+
in ``metadata["pope_label"]`` ("yes"/"no") — build a probe set with the datasets
|
|
6
|
+
layer. Reports accuracy / precision / recall / F1 (positive class = "yes").
|
|
7
|
+
|
|
8
|
+
Paper: "Evaluating Object Hallucination in Large Vision-Language Models"
|
|
9
|
+
Li et al., EMNLP 2023 — https://arxiv.org/abs/2305.10355
|
|
10
|
+
Code: https://github.com/AoiDragon/POPE
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from typing import TYPE_CHECKING, Optional
|
|
17
|
+
|
|
18
|
+
from evalrx.core.analyzer import Analyzer
|
|
19
|
+
from evalrx.core.capability import Capability
|
|
20
|
+
from evalrx.core.registry import register_analyzer
|
|
21
|
+
from evalrx.core.result import Result
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from evalrx.core.case import CaseBatch
|
|
25
|
+
from evalrx.core.model import Model
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def parse_yes_no(text: str) -> Optional[str]:
|
|
29
|
+
"""Map a free-form answer to 'yes' / 'no' / None (word-boundary, not substring).
|
|
30
|
+
|
|
31
|
+
Matching whole words avoids the classic trap where 'not'/'none'/'nothing'
|
|
32
|
+
contain the substring 'no'.
|
|
33
|
+
"""
|
|
34
|
+
words = re.findall(r"[a-z]+", str(text).lower())
|
|
35
|
+
for w in words[:6]: # decision is almost always in the opening words
|
|
36
|
+
if w == "yes":
|
|
37
|
+
return "yes"
|
|
38
|
+
if w == "no":
|
|
39
|
+
return "no"
|
|
40
|
+
if "yes" in words:
|
|
41
|
+
return "yes"
|
|
42
|
+
if "no" in words:
|
|
43
|
+
return "no"
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@register_analyzer("pope")
|
|
48
|
+
class POPEAnalyzer(Analyzer):
|
|
49
|
+
"""Score yes/no object-presence probes against gold labels."""
|
|
50
|
+
|
|
51
|
+
name = "pope"
|
|
52
|
+
requires = frozenset({Capability.GENERATE})
|
|
53
|
+
applies_to_modalities = frozenset({"image"})
|
|
54
|
+
# An omni model DECLARES image even on an audio benchmark, so the registry
|
|
55
|
+
# match alone still offers this on a batch with no images in it.
|
|
56
|
+
requires_modalities = frozenset({"image"})
|
|
57
|
+
|
|
58
|
+
def __init__(self, label_key: str = "pope_label") -> None:
|
|
59
|
+
super().__init__(label_key=label_key)
|
|
60
|
+
|
|
61
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> Result:
|
|
62
|
+
tp = fp = fn = tn = unparsed = 0
|
|
63
|
+
per_case = []
|
|
64
|
+
for case in cases:
|
|
65
|
+
gold = str(case.metadata.get(self.label_key, "")).strip().lower()
|
|
66
|
+
pred = parse_yes_no(model.generate(case.inputs))
|
|
67
|
+
is_correct = pred == gold if gold in {"yes", "no"} and pred is not None else None
|
|
68
|
+
per_case.append({
|
|
69
|
+
"id": case.id,
|
|
70
|
+
"gold": gold,
|
|
71
|
+
"pred": pred,
|
|
72
|
+
# Mechanism signals first: M2's stats layer caps how many per-case
|
|
73
|
+
# signal keys it tests, and these two carry the diagnosis
|
|
74
|
+
# (hallucinated presence vs missed finding) — is_correct alone
|
|
75
|
+
# cannot distinguish them.
|
|
76
|
+
"false_positive": gold == "no" and pred == "yes",
|
|
77
|
+
"false_negative": gold == "yes" and pred == "no",
|
|
78
|
+
# The two MARGINALS those flags are built from. false_positive /
|
|
79
|
+
# false_negative are gold x pred conjunctions -- each a subset of
|
|
80
|
+
# FAIL by construction, so the planner keeps them out of the
|
|
81
|
+
# tested family (descriptive only). answered_yes / gold_yes are
|
|
82
|
+
# not label functions and are what M2/M4 test a directional
|
|
83
|
+
# ("over-affirmation") hypothesis on. Same columns as
|
|
84
|
+
# answer_extraction_audit emits on any yes/no task.
|
|
85
|
+
**({"answered_yes": pred == "yes"} if pred is not None else {}),
|
|
86
|
+
**({"gold_yes": gold == "yes"} if gold in {"yes", "no"} else {}),
|
|
87
|
+
"has_gold": gold in {"yes", "no"},
|
|
88
|
+
"unparsed": pred is None,
|
|
89
|
+
"is_correct": is_correct,
|
|
90
|
+
})
|
|
91
|
+
if pred is None:
|
|
92
|
+
unparsed += 1
|
|
93
|
+
continue
|
|
94
|
+
if gold == "yes" and pred == "yes":
|
|
95
|
+
tp += 1
|
|
96
|
+
elif gold == "no" and pred == "yes":
|
|
97
|
+
fp += 1
|
|
98
|
+
elif gold == "yes" and pred == "no":
|
|
99
|
+
fn += 1
|
|
100
|
+
elif gold == "no" and pred == "no":
|
|
101
|
+
tn += 1
|
|
102
|
+
n = tp + fp + fn + tn
|
|
103
|
+
precision = tp / (tp + fp) if (tp + fp) else 0.0
|
|
104
|
+
recall = tp / (tp + fn) if (tp + fn) else 0.0
|
|
105
|
+
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) else 0.0
|
|
106
|
+
return Result(
|
|
107
|
+
analyzer=self.name, model=repr(model), cases=cases,
|
|
108
|
+
artifacts={"per_case": per_case},
|
|
109
|
+
findings={
|
|
110
|
+
"n": n, "unparsed": unparsed,
|
|
111
|
+
"accuracy": round((tp + tn) / n, 4) if n else None,
|
|
112
|
+
"precision": round(precision, 4),
|
|
113
|
+
"recall": round(recall, 4),
|
|
114
|
+
"f1": round(f1, 4),
|
|
115
|
+
"yes_rate": round((tp + fp) / n, 4) if n else None, # >0.5 ⇒ over-affirmation (hallucination)
|
|
116
|
+
"false_positive_rate": round(fp / (fp + tn), 4) if (fp + tn) else None,
|
|
117
|
+
"per_case": per_case,
|
|
118
|
+
},
|
|
119
|
+
)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Sampled-passage factual consistency — the text counterpart of POPE/CHAIR (black-box).
|
|
2
|
+
|
|
3
|
+
SelfCheckGPT's premise: if the model KNOWS a fact, independently sampled
|
|
4
|
+
generations agree on it; hallucinated content diverges across samples. This
|
|
5
|
+
port implements the dependency-free unigram-containment variant: each sentence
|
|
6
|
+
of the baseline answer is scored by how well the resampled passages cover its
|
|
7
|
+
content words. ``requires=GENERATE`` only, so it runs on API models.
|
|
8
|
+
|
|
9
|
+
The score is a per-case M2 column, not a truth oracle: correlate
|
|
10
|
+
``selfcheck_inconsistency`` with the outcome label downstream instead of
|
|
11
|
+
treating a high score as a verdict.
|
|
12
|
+
|
|
13
|
+
References:
|
|
14
|
+
- SelfCheckGPT: Zero-Resource Black-Box Hallucination Detection for Generative
|
|
15
|
+
Large Language Models — Manakul et al., EMNLP 2023 — arXiv:2303.08896
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
22
|
+
|
|
23
|
+
from evalrx.core.analyzer import Analyzer
|
|
24
|
+
from evalrx.core.capability import Capability
|
|
25
|
+
from evalrx.core.registry import register_analyzer
|
|
26
|
+
from evalrx.core.result import Result
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from evalrx.core.case import CaseBatch
|
|
30
|
+
from evalrx.core.model import Model
|
|
31
|
+
|
|
32
|
+
_WORD = re.compile(r"[a-z0-9]+")
|
|
33
|
+
_SENT_SPLIT = re.compile(r"(?<=[.!?])\s+|(?<=[\u3002\uff01\uff1f])\s*|\n+")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def split_sentences(text: str, min_chars: int = 10) -> list[str]:
|
|
37
|
+
"""Split into scoreable sentences, dropping fragments below ``min_chars``."""
|
|
38
|
+
parts = [p.strip() for p in _SENT_SPLIT.split(str(text or ""))]
|
|
39
|
+
return [p for p in parts if len(p) >= min_chars]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _char_bigrams(text: str) -> set[str]:
|
|
43
|
+
chars = [c for c in text if not c.isspace()]
|
|
44
|
+
return {a + b for a, b in zip(chars, chars[1:])} or set(chars)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def containment(sentence: str, passage: str) -> float:
|
|
48
|
+
"""Fraction of the sentence's content units that appear in the passage.
|
|
49
|
+
|
|
50
|
+
Latin/digit words when the sentence has any; character bigrams otherwise,
|
|
51
|
+
so CJK (or symbol-only) sentences are scored instead of silently passing.
|
|
52
|
+
"""
|
|
53
|
+
sent = set(_WORD.findall(sentence.lower()))
|
|
54
|
+
if sent:
|
|
55
|
+
passage_words = set(_WORD.findall(passage.lower()))
|
|
56
|
+
return len(sent & passage_words) / len(sent)
|
|
57
|
+
bigrams = _char_bigrams(sentence.lower())
|
|
58
|
+
if not bigrams:
|
|
59
|
+
return 1.0
|
|
60
|
+
return len(bigrams & _char_bigrams(passage.lower())) / len(bigrams)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@register_analyzer("selfcheck_consistency")
|
|
64
|
+
class SelfCheckConsistencyAnalyzer(Analyzer):
|
|
65
|
+
"""SelfCheckGPT-style sampled-passage consistency: sentence-level hallucination signal from resampling.
|
|
66
|
+
|
|
67
|
+
Hyper-parameters:
|
|
68
|
+
n_samples: resampled passages per case (baseline answer excluded).
|
|
69
|
+
gen_kwargs: sampling config for the resamples — MUST be stochastic
|
|
70
|
+
(default ``{"temperature": 1.0}``); at temperature 0 the
|
|
71
|
+
score is degenerate and reported as such.
|
|
72
|
+
max_cases: label-stratified cap on probed cases; 0 (the default) = every case.
|
|
73
|
+
min_sentence_chars: shorter fragments are not scored.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
name = "selfcheck_consistency"
|
|
77
|
+
requires = frozenset({Capability.GENERATE})
|
|
78
|
+
applies_to_modalities = frozenset({"text", "image"})
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
n_samples: int = 4,
|
|
83
|
+
gen_kwargs: Optional[dict] = None,
|
|
84
|
+
max_cases: int = 0,
|
|
85
|
+
min_sentence_chars: int = 10,
|
|
86
|
+
) -> None:
|
|
87
|
+
super().__init__(
|
|
88
|
+
n_samples=max(1, int(n_samples)),
|
|
89
|
+
gen_kwargs=dict(gen_kwargs) if gen_kwargs is not None else {"temperature": 1.0},
|
|
90
|
+
max_cases=max_cases,
|
|
91
|
+
min_sentence_chars=min_sentence_chars,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> Result:
|
|
95
|
+
per_case: list[dict[str, Any]] = []
|
|
96
|
+
for case in cases.stratified_head(self.max_cases):
|
|
97
|
+
baseline = str(case.observed or "").strip()
|
|
98
|
+
baseline_from = "observed"
|
|
99
|
+
if not baseline:
|
|
100
|
+
baseline = str(model.generate(case.inputs))
|
|
101
|
+
baseline_from = "generated"
|
|
102
|
+
sentences = split_sentences(baseline, self.min_sentence_chars)
|
|
103
|
+
entry: dict[str, Any] = {
|
|
104
|
+
"sample_id": case.id,
|
|
105
|
+
"baseline_from": baseline_from,
|
|
106
|
+
"n_sentences": len(sentences),
|
|
107
|
+
}
|
|
108
|
+
if not sentences:
|
|
109
|
+
entry["skipped"] = "no scoreable sentence in the baseline answer"
|
|
110
|
+
per_case.append(entry)
|
|
111
|
+
continue
|
|
112
|
+
samples = [
|
|
113
|
+
str(model.generate(case.inputs, **self.gen_kwargs))
|
|
114
|
+
for _ in range(self.n_samples)
|
|
115
|
+
]
|
|
116
|
+
# SelfCheckGPT scores a sentence by its support in OTHER samples;
|
|
117
|
+
# a sentence no resample reproduces is the hallucination suspect.
|
|
118
|
+
sentence_scores = [
|
|
119
|
+
1.0 - max(containment(sent, sample) for sample in samples)
|
|
120
|
+
for sent in sentences
|
|
121
|
+
]
|
|
122
|
+
entry["selfcheck_inconsistency"] = round(
|
|
123
|
+
sum(sentence_scores) / len(sentence_scores), 4
|
|
124
|
+
)
|
|
125
|
+
entry["selfcheck_worst_sentence"] = round(max(sentence_scores), 4)
|
|
126
|
+
worst = sentences[sentence_scores.index(max(sentence_scores))]
|
|
127
|
+
entry["worst_sentence_text"] = worst[:200]
|
|
128
|
+
per_case.append(entry)
|
|
129
|
+
|
|
130
|
+
scored = [c["selfcheck_inconsistency"] for c in per_case if "selfcheck_inconsistency" in c]
|
|
131
|
+
temperature = self.gen_kwargs.get("temperature")
|
|
132
|
+
degenerate = (
|
|
133
|
+
(temperature is not None and float(temperature) == 0.0)
|
|
134
|
+
or self.gen_kwargs.get("do_sample") is False
|
|
135
|
+
)
|
|
136
|
+
findings: dict[str, Any] = {
|
|
137
|
+
"n_cases": len(per_case),
|
|
138
|
+
"n_samples": self.n_samples,
|
|
139
|
+
"degenerate_sampling": degenerate,
|
|
140
|
+
"gen_kwargs": dict(self.gen_kwargs),
|
|
141
|
+
"mean_inconsistency": round(sum(scored) / len(scored), 4) if scored else None,
|
|
142
|
+
"per_case": per_case,
|
|
143
|
+
"_caveat": (
|
|
144
|
+
"Unigram-containment proxy for SelfCheckGPT (no NLI model): "
|
|
145
|
+
"paraphrases inflate the score, verbatim repetition of a wrong "
|
|
146
|
+
"fact deflates it. Requires stochastic sampling — with "
|
|
147
|
+
"temperature 0 the resamples repeat one greedy generation and the "
|
|
148
|
+
"column degenerates to baseline-vs-greedy disagreement "
|
|
149
|
+
"(degenerate_sampling flags this). "
|
|
150
|
+
"selfcheck_inconsistency / selfcheck_worst_sentence are numeric "
|
|
151
|
+
"M2 columns; treat them as signals to correlate with the outcome "
|
|
152
|
+
"label, never as a hallucination verdict."
|
|
153
|
+
),
|
|
154
|
+
}
|
|
155
|
+
return Result(analyzer=self.name, model=repr(model), cases=cases, findings=findings)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""VCD — Visual Contrastive Decoding (Leng et al.) (Stage 2).
|
|
2
|
+
|
|
3
|
+
Contrast logits on the original vs a distorted image to suppress
|
|
4
|
+
language-prior-driven hallucination. Needs decode-time logit control (in-process
|
|
5
|
+
HF), VLM. Modeled here as an analyzer that reports the contrastive shift.
|
|
6
|
+
|
|
7
|
+
References:
|
|
8
|
+
- Mitigating Object Hallucinations in LVLMs through Visual Contrastive Decoding (VCD)
|
|
9
|
+
Leng et al., CVPR 2024 — arXiv:2311.16922
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from evalrx.core.analyzer import Analyzer
|
|
15
|
+
from evalrx.core.capability import Capability
|
|
16
|
+
from evalrx.core.registry import register_analyzer
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@register_analyzer("vcd")
|
|
20
|
+
class VCDAnalyzer(Analyzer):
|
|
21
|
+
name = "vcd"
|
|
22
|
+
requires = frozenset({Capability.LOGITS})
|
|
23
|
+
applies_to_modalities = frozenset({"image"})
|
|
24
|
+
# An omni model DECLARES image even on an audio benchmark, so the registry
|
|
25
|
+
# match alone still offers this on a batch with no images in it.
|
|
26
|
+
requires_modalities = frozenset({"image"})
|
|
27
|
+
|
|
28
|
+
def _run(self, model, cases):
|
|
29
|
+
raise NotImplementedError("Stage 2: contrastive decoding over original vs distorted image (logit control).")
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Lens analyzers — project hidden states toward the vocabulary."""
|
|
2
|
+
|
|
3
|
+
from evalrx.analyzers.lens.layer_contrast import LayerContrastAnalyzer
|
|
4
|
+
from evalrx.analyzers.lens.logit_lens import LogitLensAnalyzer
|
|
5
|
+
from evalrx.analyzers.lens.tuned_lens import TunedLensAnalyzer
|
|
6
|
+
|
|
7
|
+
__all__ = ["LogitLensAnalyzer", "TunedLensAnalyzer", "LayerContrastAnalyzer"]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Premature-layer vs final-layer divergence — the DoLa/DeCo family's diagnostic half.
|
|
2
|
+
|
|
3
|
+
DoLa's observation: factual knowledge surfaces in specific layers, and tokens
|
|
4
|
+
whose final distribution diverges sharply from premature layers are where
|
|
5
|
+
"what the layers know" and "what gets decoded" disagree — a hallucination
|
|
6
|
+
indicator. DeCo's follow-up: hallucinated targets are often *correct* in
|
|
7
|
+
preceding layers and suppressed at the top. This analyzer measures those
|
|
8
|
+
dynamics per case WITHOUT changing decoding: it emits divergence/agreement
|
|
9
|
+
columns that, when they separate FAIL from PASS in M2, point the fix search at
|
|
10
|
+
layer-contrast decoding repairs (DoLa/DeCo/VISTA family).
|
|
11
|
+
|
|
12
|
+
White-box: ``requires=HIDDEN_STATES`` plus the model's unembedding
|
|
13
|
+
(``unembed_weight()``), same access contract as :class:`LogitLensAnalyzer`.
|
|
14
|
+
|
|
15
|
+
References:
|
|
16
|
+
- DoLa: Decoding by Contrasting Layers Improves Factuality in Large Language
|
|
17
|
+
Models — Chuang et al., ICLR 2024 — arXiv:2309.03883
|
|
18
|
+
- MLLM Can See? Dynamic Correction Decoding for Hallucination Mitigation
|
|
19
|
+
(DeCo) — Wang et al., ICLR 2025 — arXiv:2410.11779
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import TYPE_CHECKING, Any
|
|
25
|
+
|
|
26
|
+
from evalrx.core.analyzer import Analyzer
|
|
27
|
+
from evalrx.core.capability import Capability
|
|
28
|
+
from evalrx.core.registry import register_analyzer
|
|
29
|
+
from evalrx.core.result import Result
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from evalrx.core.case import CaseBatch
|
|
33
|
+
from evalrx.core.model import Model
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@register_analyzer("layer_contrast")
|
|
37
|
+
class LayerContrastAnalyzer(Analyzer):
|
|
38
|
+
"""DoLa-style premature/final layer divergence columns (JSD, agreement, contrast margin).
|
|
39
|
+
|
|
40
|
+
Hyper-parameters:
|
|
41
|
+
pos: query position to read (default ``-1``, the last token).
|
|
42
|
+
skip_first_frac: fraction of early layers excluded from the premature
|
|
43
|
+
candidate set (embedding-adjacent layers are noise).
|
|
44
|
+
max_cases: label-stratified cap (one forward per case); 0 (the default) = every case.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
name = "layer_contrast"
|
|
48
|
+
requires = frozenset({Capability.HIDDEN_STATES})
|
|
49
|
+
applies_to_modalities = frozenset({"text", "image"})
|
|
50
|
+
|
|
51
|
+
def __init__(self, pos: int = -1, skip_first_frac: float = 0.25, max_cases: int = 0) -> None:
|
|
52
|
+
super().__init__(pos=pos, skip_first_frac=float(skip_first_frac), max_cases=max_cases)
|
|
53
|
+
|
|
54
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> Result:
|
|
55
|
+
import torch
|
|
56
|
+
|
|
57
|
+
W = model.unembed_weight()
|
|
58
|
+
if W is None:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"{type(model).__name__} exposes no unembed_weight(); layer-contrast "
|
|
61
|
+
"needs it (white-box local backend)."
|
|
62
|
+
)
|
|
63
|
+
W = W.detach().float()
|
|
64
|
+
device = W.device
|
|
65
|
+
norm = model.final_norm() if hasattr(model, "final_norm") else None
|
|
66
|
+
norm_dtype = next(norm.parameters()).dtype if norm is not None and any(
|
|
67
|
+
True for _ in norm.parameters()) else None
|
|
68
|
+
|
|
69
|
+
def _jsd(p: "torch.Tensor", q: "torch.Tensor") -> float:
|
|
70
|
+
m = 0.5 * (p + q)
|
|
71
|
+
kl_pm = torch.sum(p * (torch.log(p + 1e-12) - torch.log(m + 1e-12)))
|
|
72
|
+
kl_qm = torch.sum(q * (torch.log(q + 1e-12) - torch.log(m + 1e-12)))
|
|
73
|
+
return float(0.5 * (kl_pm + kl_qm))
|
|
74
|
+
|
|
75
|
+
per_case: list[dict[str, Any]] = []
|
|
76
|
+
n_layers = 0
|
|
77
|
+
for case in cases.stratified_head(self.max_cases):
|
|
78
|
+
trace = model.forward(case.inputs, capture={Capability.HIDDEN_STATES})
|
|
79
|
+
hidden = trace.require(Capability.HIDDEN_STATES) # list per layer: (seq, dim)
|
|
80
|
+
n_layers = len(hidden)
|
|
81
|
+
with torch.no_grad():
|
|
82
|
+
probs = []
|
|
83
|
+
for index, h in enumerate(hidden):
|
|
84
|
+
vec = h[self.pos].detach().to(device)
|
|
85
|
+
# HF backends return the FINAL hidden-states entry already
|
|
86
|
+
# post final-norm; re-normalising it would square the RMS
|
|
87
|
+
# gain and distort every divergence below.
|
|
88
|
+
if norm is not None and index < len(hidden) - 1:
|
|
89
|
+
vec = norm(vec.to(norm_dtype)) if norm_dtype is not None else norm(vec)
|
|
90
|
+
probs.append(torch.softmax(vec.float() @ W.T, dim=-1))
|
|
91
|
+
final = probs[-1]
|
|
92
|
+
top1 = int(final.argmax())
|
|
93
|
+
start = min(int(self.skip_first_frac * n_layers), max(0, n_layers - 2))
|
|
94
|
+
candidates = list(range(start, n_layers - 1))
|
|
95
|
+
if not candidates:
|
|
96
|
+
candidates = [max(0, n_layers - 2)]
|
|
97
|
+
jsds = [_jsd(final, probs[layer]) for layer in candidates]
|
|
98
|
+
jsd_max = max(jsds)
|
|
99
|
+
max_layer = candidates[jsds.index(jsd_max)]
|
|
100
|
+
agree = sum(1 for layer in candidates if int(probs[layer].argmax()) == top1)
|
|
101
|
+
# DoLa contrast margin at the max-divergence premature layer:
|
|
102
|
+
# positive = the final layer amplifies its top-1 over the
|
|
103
|
+
# premature layer; negative = the top-1 was STRONGER early and
|
|
104
|
+
# got suppressed at the top (DeCo's hallucination signature).
|
|
105
|
+
contrast_margin = float(
|
|
106
|
+
torch.log(final[top1] + 1e-12) - torch.log(probs[max_layer][top1] + 1e-12)
|
|
107
|
+
)
|
|
108
|
+
per_case.append({
|
|
109
|
+
"sample_id": case.id,
|
|
110
|
+
"jsd_max": round(jsd_max, 4),
|
|
111
|
+
"jsd_mean": round(sum(jsds) / len(jsds), 4),
|
|
112
|
+
"jsd_max_layer_frac": round(max_layer / max(1, n_layers - 1), 4),
|
|
113
|
+
"layer_agreement_frac": round(agree / len(candidates), 4),
|
|
114
|
+
"contrast_margin": round(contrast_margin, 4),
|
|
115
|
+
"final_top1_prob": round(float(final[top1]), 4),
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
findings: dict[str, Any] = {
|
|
119
|
+
"n_cases": len(per_case),
|
|
120
|
+
"n_layers": n_layers,
|
|
121
|
+
"pos": self.pos,
|
|
122
|
+
"premature_layer_window": f"[{self.skip_first_frac:.2f}L, L-1)",
|
|
123
|
+
"per_case": per_case,
|
|
124
|
+
"_caveat": (
|
|
125
|
+
"Divergence DIAGNOSTICS only — no decoding intervention is "
|
|
126
|
+
"performed. If jsd_max / negative contrast_margin separate FAIL "
|
|
127
|
+
"from PASS in M2, the indicated repair family is layer-contrast "
|
|
128
|
+
"decoding (DoLa/DeCo/VISTA), an L0/L3 fix. Columns read one "
|
|
129
|
+
"position (pos) of a teacher-forced forward; generation-time "
|
|
130
|
+
"dynamics can differ."
|
|
131
|
+
),
|
|
132
|
+
}
|
|
133
|
+
return Result(analyzer=self.name, model=repr(model), cases=cases, findings=findings)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Logit lens (nostalgebraist) — project intermediate hidden states through the unembed.
|
|
2
|
+
|
|
3
|
+
Reveals *when* (which layer) the model's prediction forms. Needs ``HIDDEN_STATES``
|
|
4
|
+
plus the model's unembedding matrix (``model.unembed_weight()``). The cheapest,
|
|
5
|
+
highest-value white-box signal — build it first.
|
|
6
|
+
|
|
7
|
+
Faithfulness: when the model exposes ``final_norm()`` (RMSNorm-family models —
|
|
8
|
+
Llama, Qwen, ...), each PREMATURE layer's hidden state is normalized before
|
|
9
|
+
unembedding, matching ``lm_head(norm(h_i))``; raw projection distorts
|
|
10
|
+
trajectories on those models. The final hidden-states entry is projected
|
|
11
|
+
as-is: HF backends return it already post final-norm, so re-normalising it
|
|
12
|
+
would square the RMS gain and distort the reference distribution. Findings
|
|
13
|
+
note whether the norm was applied.
|
|
14
|
+
|
|
15
|
+
References:
|
|
16
|
+
- interpreting GPT: the logit lens — nostalgebraist (2020), LessWrong
|
|
17
|
+
https://www.lesswrong.com/posts/AcKRB8wDpdaN6v6ru/interpreting-gpt-the-logit-lens
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
from evalrx.core.analyzer import Analyzer
|
|
25
|
+
from evalrx.core.capability import Capability
|
|
26
|
+
from evalrx.core.registry import register_analyzer
|
|
27
|
+
from evalrx.core.result import Result
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from evalrx.core.case import CaseBatch
|
|
31
|
+
from evalrx.core.model import Model
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@register_analyzer("logit_lens")
|
|
35
|
+
class LogitLensAnalyzer(Analyzer):
|
|
36
|
+
"""Per-layer readout at one position, plus per-case decision-depth signals.
|
|
37
|
+
|
|
38
|
+
Per case (``findings["per_case"]``, consumable by M2's stats layer):
|
|
39
|
+
decision_layer / decision_frac: first layer whose top-1 equals the
|
|
40
|
+
final top-1 — late decisions mean the answer formed in the last
|
|
41
|
+
layers (prior-override candidates).
|
|
42
|
+
final_top1_prob: confidence of the emitted token at the final layer.
|
|
43
|
+
interim_peak_prob / late_drop: peak probability of the final top-1
|
|
44
|
+
across layers and its drop to the final layer (non-monotonic
|
|
45
|
+
trajectories flag suppression dynamics).
|
|
46
|
+
|
|
47
|
+
Hyper-parameters:
|
|
48
|
+
pos: query position to read (default ``-1``, the last token).
|
|
49
|
+
top_k: number of candidates to report per layer (first case only).
|
|
50
|
+
max_cases: cap on cases analyzed (one forward each); 0 (the default) = every case.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
name = "logit_lens"
|
|
54
|
+
requires = frozenset({Capability.HIDDEN_STATES})
|
|
55
|
+
applies_to_modalities = frozenset({"text", "image"})
|
|
56
|
+
|
|
57
|
+
# Default cap sized for ENRICHED diagnosis batches: the per-layer fail/pass
|
|
58
|
+
# contrast is powered by the scarce FAIL group, and a small cap (e.g. 32)
|
|
59
|
+
# starves it — on a curated batch the stratified subsample then carries only
|
|
60
|
+
# ~16 FAILs, too few for M4 to clear the e-value bar. 128 covers typical
|
|
61
|
+
# mined fail counts (tens) plus controls at one forward each.
|
|
62
|
+
def __init__(self, pos: int = -1, top_k: int = 3, max_cases: int = 0) -> None:
|
|
63
|
+
super().__init__(pos=pos, top_k=top_k, max_cases=max_cases)
|
|
64
|
+
|
|
65
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> Result:
|
|
66
|
+
import torch
|
|
67
|
+
|
|
68
|
+
W = model.unembed_weight()
|
|
69
|
+
if W is None:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"{type(model).__name__} exposes no unembed_weight(); logit-lens needs it "
|
|
72
|
+
"(white-box local backend)."
|
|
73
|
+
)
|
|
74
|
+
W = W.detach().float() # (vocab, dim), once — not per layer
|
|
75
|
+
device = W.device
|
|
76
|
+
norm = model.final_norm() if hasattr(model, "final_norm") else None
|
|
77
|
+
norm_dtype = next(norm.parameters()).dtype if norm is not None and any(
|
|
78
|
+
True for _ in norm.parameters()) else None
|
|
79
|
+
|
|
80
|
+
# Label-stratified subsample: a plain head is mostly PASS on an enriched
|
|
81
|
+
# batch, starving the FAIL group that the downstream contrast needs.
|
|
82
|
+
per_case, per_layer_top, n_layers = [], None, 0
|
|
83
|
+
for case in cases.stratified_head(self.max_cases):
|
|
84
|
+
trace = model.forward(case.inputs, capture={Capability.HIDDEN_STATES})
|
|
85
|
+
hidden = trace.require(Capability.HIDDEN_STATES) # list per layer: (seq, dim)
|
|
86
|
+
n_layers = len(hidden)
|
|
87
|
+
|
|
88
|
+
layer_logits = []
|
|
89
|
+
with torch.no_grad():
|
|
90
|
+
for layer_index, h in enumerate(hidden):
|
|
91
|
+
vec = h[self.pos].detach().to(device) # device-align: states may be on CPU
|
|
92
|
+
# HF backends return the FINAL hidden-states entry already
|
|
93
|
+
# post final-norm (this analyzer requests the full stack, so
|
|
94
|
+
# the last element IS that entry); re-normalising it would
|
|
95
|
+
# square the RMS gain and shift the final distribution the
|
|
96
|
+
# decision-depth columns are anchored to.
|
|
97
|
+
if norm is not None and layer_index < len(hidden) - 1:
|
|
98
|
+
vec = norm(vec.to(norm_dtype)) if norm_dtype is not None else norm(vec)
|
|
99
|
+
layer_logits.append(vec.float() @ W.T) # (vocab,)
|
|
100
|
+
|
|
101
|
+
final = layer_logits[-1]
|
|
102
|
+
top1 = int(final.argmax())
|
|
103
|
+
p_top1 = [float(torch.softmax(lg, dim=-1)[top1]) for lg in layer_logits]
|
|
104
|
+
argmaxes = [int(lg.argmax()) for lg in layer_logits]
|
|
105
|
+
decision_layer = next(
|
|
106
|
+
(i for i in range(n_layers) if all(a == top1 for a in argmaxes[i:])), n_layers - 1)
|
|
107
|
+
peak = max(p_top1)
|
|
108
|
+
per_case.append({
|
|
109
|
+
"sample_id": case.id,
|
|
110
|
+
"decision_layer": decision_layer,
|
|
111
|
+
"decision_frac": round(decision_layer / max(1, n_layers - 1), 4),
|
|
112
|
+
"final_top1_prob": round(p_top1[-1], 4),
|
|
113
|
+
"interim_peak_prob": round(peak, 4),
|
|
114
|
+
"late_drop": round(peak - p_top1[-1], 4),
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
if per_layer_top is None: # detailed per-layer table for the first case only
|
|
118
|
+
per_layer_top = []
|
|
119
|
+
for layer_idx, lg in enumerate(layer_logits):
|
|
120
|
+
k = min(self.top_k, lg.shape[-1])
|
|
121
|
+
topk = torch.topk(lg, k)
|
|
122
|
+
per_layer_top.append({
|
|
123
|
+
"layer": layer_idx,
|
|
124
|
+
"top": [{"token_id": int(i), "logit": round(float(v), 3)}
|
|
125
|
+
for v, i in zip(topk.values, topk.indices)],
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
return Result(
|
|
129
|
+
analyzer=self.name, model=repr(model), cases=cases,
|
|
130
|
+
artifacts={"per_case": per_case},
|
|
131
|
+
findings={
|
|
132
|
+
"n_layers": n_layers, "pos": self.pos,
|
|
133
|
+
"n_cases_analyzed": len(per_case),
|
|
134
|
+
"final_norm_applied": norm is not None,
|
|
135
|
+
"per_layer_top": per_layer_top or [],
|
|
136
|
+
"per_case": per_case,
|
|
137
|
+
},
|
|
138
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Tuned lens (Belrose et al.) — per-layer learned affine translators before unembed.
|
|
2
|
+
|
|
3
|
+
More faithful than the raw logit lens, but requires PER-MODEL trained translators.
|
|
4
|
+
Stage 2: train/load translators, then apply like the logit lens.
|
|
5
|
+
|
|
6
|
+
References:
|
|
7
|
+
- Eliciting Latent Predictions from Transformers with the Tuned Lens
|
|
8
|
+
Belrose et al., 2023 — arXiv:2303.08112
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from evalrx.core.analyzer import Analyzer
|
|
14
|
+
from evalrx.core.capability import Capability
|
|
15
|
+
from evalrx.core.registry import register_analyzer
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@register_analyzer("tuned_lens")
|
|
19
|
+
class TunedLensAnalyzer(Analyzer):
|
|
20
|
+
"""Tuned-lens projection of hidden states (needs trained translators)."""
|
|
21
|
+
|
|
22
|
+
name = "tuned_lens"
|
|
23
|
+
requires = frozenset({Capability.HIDDEN_STATES})
|
|
24
|
+
applies_to_modalities = frozenset({"text", "image"})
|
|
25
|
+
|
|
26
|
+
def _run(self, model, cases):
|
|
27
|
+
raise NotImplementedError(
|
|
28
|
+
"Stage 2: load/train per-layer affine translators (e.g. via the `tuned-lens` package), "
|
|
29
|
+
"then project hidden states through them before the unembed."
|
|
30
|
+
)
|