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,56 @@
|
|
|
1
|
+
"""Attention sink (Gu et al., "When Attention Sink Emerges in LMs").
|
|
2
|
+
|
|
3
|
+
Measures how much attention mass collapses onto the first token (the "sink"),
|
|
4
|
+
per layer, averaged over heads and query positions. Operates on a ``Trace``.
|
|
5
|
+
|
|
6
|
+
References:
|
|
7
|
+
- Efficient Streaming Language Models with Attention Sinks (StreamingLLM)
|
|
8
|
+
Xiao et al., ICLR 2024 — arXiv:2309.17453
|
|
9
|
+
- When Attention Sink Emerges in Language Models: An Empirical View
|
|
10
|
+
Gu et al., ICLR 2025 — arXiv:2410.10781
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
from evalrx.core.analyzer import Analyzer
|
|
18
|
+
from evalrx.core.capability import Capability
|
|
19
|
+
from evalrx.core.registry import register_analyzer
|
|
20
|
+
from evalrx.core.result import Result
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from evalrx.core.case import CaseBatch
|
|
24
|
+
from evalrx.core.model import Model
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@register_analyzer("attention_sink")
|
|
28
|
+
class AttentionSinkAnalyzer(Analyzer):
|
|
29
|
+
"""Per-layer attention mass on the first token (sink strength)."""
|
|
30
|
+
|
|
31
|
+
name = "attention_sink"
|
|
32
|
+
requires = frozenset({Capability.ATTENTION})
|
|
33
|
+
applies_to_modalities = frozenset({"text", "image"})
|
|
34
|
+
|
|
35
|
+
def __init__(self, sink_pos: int = 0) -> None:
|
|
36
|
+
super().__init__(sink_pos=sink_pos)
|
|
37
|
+
|
|
38
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> Result:
|
|
39
|
+
case = cases[0]
|
|
40
|
+
trace = model.forward(case.inputs, capture={Capability.ATTENTION})
|
|
41
|
+
attns = trace.require(Capability.ATTENTION)
|
|
42
|
+
per_layer = []
|
|
43
|
+
for a in attns:
|
|
44
|
+
A = a.float().mean(0) # (seq, seq), head-averaged
|
|
45
|
+
per_layer.append(round(float(A[:, self.sink_pos].mean()), 4))
|
|
46
|
+
mean_sink = round(sum(per_layer) / max(len(per_layer), 1), 4)
|
|
47
|
+
return Result(
|
|
48
|
+
analyzer=self.name, model=repr(model), cases=cases,
|
|
49
|
+
artifacts={"per_layer_sink": per_layer},
|
|
50
|
+
findings={
|
|
51
|
+
"n_layers": len(attns),
|
|
52
|
+
"sink_token": trace.tokens[self.sink_pos] if trace.tokens else None,
|
|
53
|
+
"mean_sink_mass": mean_sink,
|
|
54
|
+
"per_layer_sink": per_layer,
|
|
55
|
+
},
|
|
56
|
+
)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Attention analysis for white-box models.
|
|
2
|
+
|
|
3
|
+
``AttentionAnalyzer`` is a :class:`~evalrx.core.analyzer.Analyzer`: configure
|
|
4
|
+
it with hyper-parameters, then ``run`` it on any model that provides the
|
|
5
|
+
``ATTENTION`` capability. It returns an :class:`AttentionResult`, whose
|
|
6
|
+
``findings`` are a light, agent-readable summary and whose ``artifacts`` hold the
|
|
7
|
+
raw per-layer/per-head tensors for plotting and numeric work.
|
|
8
|
+
|
|
9
|
+
References:
|
|
10
|
+
- "What's in the Image?" — vision attention deep-dive
|
|
11
|
+
- "VL-Cache" — modality-aware KV-cache
|
|
12
|
+
- "When Attention Sink Emerges in Language Models" — https://arxiv.org/abs/2309.17453
|
|
13
|
+
- "MLLMs Know Where to Look" (relative attention) — https://arxiv.org/abs/2502.17422
|
|
14
|
+
· code: https://github.com/saccharomycetes/mllms_know
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import TYPE_CHECKING, Any
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
import torch
|
|
24
|
+
|
|
25
|
+
from evalrx.core.analyzer import Analyzer
|
|
26
|
+
from evalrx.core.capability import Capability
|
|
27
|
+
from evalrx.core.registry import register_analyzer
|
|
28
|
+
from evalrx.core.result import Result
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from evalrx.core.case import CaseBatch
|
|
32
|
+
from evalrx.core.model import Model
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ======================================================================
|
|
36
|
+
# Result
|
|
37
|
+
# ======================================================================
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class AttentionResult(Result):
|
|
41
|
+
"""Attention analysis output for a single case.
|
|
42
|
+
|
|
43
|
+
The heavy per-layer attention tensors live in ``artifacts["attentions"]``
|
|
44
|
+
(each ``(heads, seq, seq)``). ``findings`` carries the agent-readable
|
|
45
|
+
summary. The convenience methods below operate on the artifacts.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
tokens: list[str] = field(default_factory=list)
|
|
49
|
+
token_ids: list[int] = field(default_factory=list)
|
|
50
|
+
|
|
51
|
+
# -- artifact accessors --------------------------------------------
|
|
52
|
+
@property
|
|
53
|
+
def attentions(self) -> list[torch.Tensor]:
|
|
54
|
+
return self.artifacts["attentions"]
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def num_layers(self) -> int:
|
|
58
|
+
return len(self.attentions)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def num_heads(self) -> int:
|
|
62
|
+
return self.attentions[0].shape[0]
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def seq_len(self) -> int:
|
|
66
|
+
return len(self.tokens)
|
|
67
|
+
|
|
68
|
+
# -- core aggregation ----------------------------------------------
|
|
69
|
+
def aggregate(self, layer: int = -1, head: int | str = "mean") -> torch.Tensor:
|
|
70
|
+
"""Return a ``(seq_len, seq_len)`` attention matrix.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
layer: Layer index (negative indexing supported; ``-1`` = last).
|
|
74
|
+
head: Head index, or ``"mean"`` to average over heads.
|
|
75
|
+
"""
|
|
76
|
+
attn = self.attentions[layer]
|
|
77
|
+
return attn.mean(dim=0) if head == "mean" else attn[int(head)]
|
|
78
|
+
|
|
79
|
+
def layer_head_matrix(self) -> torch.Tensor:
|
|
80
|
+
"""Stacked tensor of shape ``(layers, heads, seq_len, seq_len)``."""
|
|
81
|
+
return torch.stack(self.attentions, dim=0)
|
|
82
|
+
|
|
83
|
+
def to_numpy(self, layer: int = -1, head: int | str = "mean") -> np.ndarray:
|
|
84
|
+
return self.aggregate(layer=layer, head=head).cpu().float().numpy()
|
|
85
|
+
|
|
86
|
+
# -- inspection ----------------------------------------------------
|
|
87
|
+
def top_attended_tokens(
|
|
88
|
+
self,
|
|
89
|
+
query_pos: int = -1,
|
|
90
|
+
k: int = 10,
|
|
91
|
+
layer: int = -1,
|
|
92
|
+
head: int | str = "mean",
|
|
93
|
+
) -> list[tuple[str, float]]:
|
|
94
|
+
"""Top-k tokens attended to by the token at *query_pos*."""
|
|
95
|
+
row = self.aggregate(layer=layer, head=head)[query_pos]
|
|
96
|
+
k = min(k, self.seq_len)
|
|
97
|
+
topk = torch.topk(row, k)
|
|
98
|
+
return [(self.tokens[i], float(v)) for v, i in zip(topk.values, topk.indices)]
|
|
99
|
+
|
|
100
|
+
def attention_entropy(self, layer: int = -1, head: int | str = "mean") -> torch.Tensor:
|
|
101
|
+
"""Shannon entropy of the attention distribution per query position."""
|
|
102
|
+
mat = self.aggregate(layer=layer, head=head).float().clamp(min=1e-9)
|
|
103
|
+
return -(mat * mat.log()).sum(dim=-1)
|
|
104
|
+
|
|
105
|
+
# -- visualisation -------------------------------------------------
|
|
106
|
+
def plot(self, layer: int = -1, head: int | str = "mean", ax=None, figsize=(8, 7)):
|
|
107
|
+
"""Plot the attention matrix as a heatmap (requires ``evalrx[viz]``)."""
|
|
108
|
+
try:
|
|
109
|
+
import matplotlib.pyplot as plt
|
|
110
|
+
except ImportError:
|
|
111
|
+
raise ImportError(
|
|
112
|
+
"Visualisation requires matplotlib. Install: pip install evalrx[viz]"
|
|
113
|
+
) from None
|
|
114
|
+
|
|
115
|
+
created = ax is None
|
|
116
|
+
if created:
|
|
117
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
118
|
+
mat = self.to_numpy(layer=layer, head=head)
|
|
119
|
+
im = ax.imshow(mat, cmap="viridis", aspect="auto", vmin=0)
|
|
120
|
+
ax.set_xticks(range(self.seq_len))
|
|
121
|
+
ax.set_xticklabels(self.tokens, rotation=90, fontsize=7)
|
|
122
|
+
ax.set_yticks(range(self.seq_len))
|
|
123
|
+
ax.set_yticklabels(self.tokens, fontsize=7)
|
|
124
|
+
ax.set_title(f"Attention | layer={layer} head={head}")
|
|
125
|
+
plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
|
|
126
|
+
if created:
|
|
127
|
+
plt.tight_layout()
|
|
128
|
+
return fig
|
|
129
|
+
return ax
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ======================================================================
|
|
133
|
+
# Analyzer
|
|
134
|
+
# ======================================================================
|
|
135
|
+
|
|
136
|
+
@register_analyzer("attention")
|
|
137
|
+
class AttentionAnalyzer(Analyzer):
|
|
138
|
+
"""Extract and summarise attention weights from a white-box model.
|
|
139
|
+
|
|
140
|
+
Hyper-parameters (sklearn-style, passed to ``__init__``):
|
|
141
|
+
layer: Default layer for the ``findings`` summary (``-1`` = last).
|
|
142
|
+
head: Default head for the summary (``"mean"`` or an int).
|
|
143
|
+
top_k: Number of top attended tokens to include in ``findings``.
|
|
144
|
+
|
|
145
|
+
Example::
|
|
146
|
+
|
|
147
|
+
result = AttentionAnalyzer(layer=-1, top_k=5).run(qwen, "The capital of France is")
|
|
148
|
+
result.summary()
|
|
149
|
+
result.plot()
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
name = "attention"
|
|
153
|
+
requires = frozenset({Capability.ATTENTION})
|
|
154
|
+
|
|
155
|
+
def __init__(self, layer: int = -1, head: int | str = "mean", top_k: int = 10) -> None:
|
|
156
|
+
super().__init__(layer=layer, head=head, top_k=top_k)
|
|
157
|
+
|
|
158
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> AttentionResult:
|
|
159
|
+
# Stage 1: analyse the first case (single-case ergonomics). Multi-case
|
|
160
|
+
# aggregation is a Stage-2 extension that will return a batched result.
|
|
161
|
+
case = cases[0]
|
|
162
|
+
trace = model.forward(case.inputs, capture={Capability.ATTENTION})
|
|
163
|
+
attentions = trace.require(Capability.ATTENTION)
|
|
164
|
+
|
|
165
|
+
result = AttentionResult(
|
|
166
|
+
analyzer=self.name,
|
|
167
|
+
model=repr(model),
|
|
168
|
+
cases=cases,
|
|
169
|
+
tokens=trace.tokens,
|
|
170
|
+
token_ids=trace.token_ids,
|
|
171
|
+
artifacts={"attentions": attentions},
|
|
172
|
+
)
|
|
173
|
+
result.findings = self._summarise(result)
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
def _summarise(self, result: AttentionResult) -> dict[str, Any]:
|
|
177
|
+
"""Build the light, agent-readable summary stored in ``findings``."""
|
|
178
|
+
top = result.top_attended_tokens(
|
|
179
|
+
query_pos=-1, k=self.top_k, layer=self.layer, head=self.head
|
|
180
|
+
)
|
|
181
|
+
entropy = result.attention_entropy(layer=self.layer, head=self.head)
|
|
182
|
+
return {
|
|
183
|
+
"num_layers": result.num_layers,
|
|
184
|
+
"num_heads": result.num_heads,
|
|
185
|
+
"seq_len": result.seq_len,
|
|
186
|
+
"summary_layer": self.layer,
|
|
187
|
+
"summary_head": self.head,
|
|
188
|
+
"top_attended_tokens": [{"token": t, "weight": round(w, 4)} for t, w in top],
|
|
189
|
+
"mean_attention_entropy": round(float(entropy.mean()), 4),
|
|
190
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Gradient/attention attribution analyzers (white-box; require GRADIENTS)."""
|
|
2
|
+
|
|
3
|
+
from evalrx.analyzers.attribution.generic_attn import GenericAttentionExplainability
|
|
4
|
+
from evalrx.analyzers.attribution.gradcam import GradCAMAnalyzer
|
|
5
|
+
|
|
6
|
+
__all__ = ["GradCAMAnalyzer", "GenericAttentionExplainability"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Generic attention-model explainability (Chefer et al., 2021) (Stage 2).
|
|
2
|
+
|
|
3
|
+
Relevance propagation through attention layers using attention weights AND their
|
|
4
|
+
gradients — hence ``requires={ATTENTION, GRADIENTS}`` (white-box, not black-box,
|
|
5
|
+
correcting the original taxonomy). Built for enc-dec / bi-modal transformers; not
|
|
6
|
+
decoder-only-native.
|
|
7
|
+
|
|
8
|
+
References:
|
|
9
|
+
- Generic Attention-model Explainability for Interpreting Bi-Modal and Encoder-Decoder Transformers
|
|
10
|
+
Chefer, Gur & Wolf, ICCV 2021 — arXiv:2103.15679
|
|
11
|
+
- Predecessor — Transformer Interpretability Beyond Attention Visualization,
|
|
12
|
+
Chefer, Gur & Wolf, CVPR 2021 — arXiv:2012.09838
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from evalrx.core.analyzer import Analyzer
|
|
18
|
+
from evalrx.core.capability import Capability
|
|
19
|
+
from evalrx.core.registry import register_analyzer
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@register_analyzer("generic_attention")
|
|
23
|
+
class GenericAttentionExplainability(Analyzer):
|
|
24
|
+
name = "generic_attention"
|
|
25
|
+
requires = frozenset({Capability.ATTENTION, Capability.GRADIENTS})
|
|
26
|
+
applies_to_modalities = frozenset({"text", "image"})
|
|
27
|
+
|
|
28
|
+
def _run(self, model, cases):
|
|
29
|
+
raise NotImplementedError(
|
|
30
|
+
"Stage 2: Chefer-style relevance via attention × gradient-of-attention propagation."
|
|
31
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Grad-CAM on the vision tower (Stage 2).
|
|
2
|
+
|
|
3
|
+
Gradient-weighted activation maps over the ViT feature map → spatial saliency.
|
|
4
|
+
``requires=GRADIENTS`` (backward pass); VLM vision tower only. Needs a
|
|
5
|
+
``reshape_transform`` for ViT and the per-arch vision-tower target layer.
|
|
6
|
+
|
|
7
|
+
References:
|
|
8
|
+
- Grad-CAM: Visual Explanations from Deep Networks via Gradient-based Localization
|
|
9
|
+
Selvaraju et al., ICCV 2017 — arXiv:1610.02391
|
|
10
|
+
- Code: https://github.com/jacobgil/pytorch-grad-cam
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from evalrx.core.analyzer import Analyzer
|
|
16
|
+
from evalrx.core.capability import Capability
|
|
17
|
+
from evalrx.core.registry import register_analyzer
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@register_analyzer("gradcam")
|
|
21
|
+
class GradCAMAnalyzer(Analyzer):
|
|
22
|
+
name = "gradcam"
|
|
23
|
+
requires = frozenset({Capability.GRADIENTS})
|
|
24
|
+
applies_to_modalities = frozenset({"image"})
|
|
25
|
+
|
|
26
|
+
def _run(self, model, cases):
|
|
27
|
+
raise NotImplementedError(
|
|
28
|
+
"Stage 2: register a hook on the vision-tower target block, backward through a "
|
|
29
|
+
"target logit, weight activations by gradients (pytorch-grad-cam + reshape_transform)."
|
|
30
|
+
)
|
evalrx/analyzers/base.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Analyzer base re-export — every analyzer subclasses the core estimator contract.
|
|
2
|
+
|
|
3
|
+
Configure hyper-parameters in ``__init__``, declare ``requires`` (capabilities) and
|
|
4
|
+
``applies_to_modalities``, implement ``_run(model, cases) -> Result``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from evalrx.core.analyzer import Analyzer
|
|
10
|
+
from evalrx.core.result import Result
|
|
11
|
+
|
|
12
|
+
__all__ = ["Analyzer", "Result"]
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Embedding-geometry analyzers (HIDDEN_STATES). CLIP/SigLIP-tower-scoped — see caveats."""
|
|
2
|
+
|
|
3
|
+
from evalrx.analyzers.geometry.cka import CKAAnalyzer, linear_cka
|
|
4
|
+
from evalrx.analyzers.geometry.linear_probe import LinearProbeAnalyzer
|
|
5
|
+
|
|
6
|
+
__all__ = ["CKAAnalyzer", "linear_cka", "LinearProbeAnalyzer"]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Linear CKA — representational similarity across layers.
|
|
2
|
+
|
|
3
|
+
Computes pairwise linear Centered Kernel Alignment between layers' hidden states
|
|
4
|
+
(sequence positions as samples). ``requires=HIDDEN_STATES``.
|
|
5
|
+
|
|
6
|
+
Construct-validity caveat: CKA/cosine-geometry methods were validated on CLIP-style
|
|
7
|
+
two-tower encoders. A decoder-only MLLM has no joint two-tower space, so cross-modal
|
|
8
|
+
geometry numbers don't carry the meaning the papers claim — scope this to a frozen
|
|
9
|
+
CLIP/SigLIP tower's activations, or read it only as within-stream layer similarity.
|
|
10
|
+
|
|
11
|
+
References:
|
|
12
|
+
- Similarity of Neural Network Representations Revisited (linear CKA)
|
|
13
|
+
Kornblith, Norouzi, Lee & Hinton, ICML 2019 — arXiv:1905.00414
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from evalrx.core.analyzer import Analyzer
|
|
21
|
+
from evalrx.core.capability import Capability
|
|
22
|
+
from evalrx.core.registry import register_analyzer
|
|
23
|
+
from evalrx.core.result import Result
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from evalrx.core.case import CaseBatch
|
|
27
|
+
from evalrx.core.model import Model
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def linear_cka(X, Y) -> float:
|
|
31
|
+
"""Linear CKA between two activation matrices ``(n, d1)`` and ``(n, d2)``."""
|
|
32
|
+
|
|
33
|
+
X = X.float() - X.float().mean(0, keepdim=True)
|
|
34
|
+
Y = Y.float() - Y.float().mean(0, keepdim=True)
|
|
35
|
+
hsic = (Y.T @ X).pow(2).sum()
|
|
36
|
+
denom = (X.T @ X).norm() * (Y.T @ Y).norm()
|
|
37
|
+
return float(hsic / denom) if denom > 0 else 0.0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@register_analyzer("cka")
|
|
41
|
+
class CKAAnalyzer(Analyzer):
|
|
42
|
+
"""Pairwise linear-CKA matrix across the model's layers for one input."""
|
|
43
|
+
|
|
44
|
+
name = "cka"
|
|
45
|
+
requires = frozenset({Capability.HIDDEN_STATES})
|
|
46
|
+
applies_to_modalities = frozenset({"text", "image"})
|
|
47
|
+
|
|
48
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> Result:
|
|
49
|
+
import torch
|
|
50
|
+
|
|
51
|
+
case = cases[0]
|
|
52
|
+
trace = model.forward(case.inputs, capture={Capability.HIDDEN_STATES})
|
|
53
|
+
hidden = trace.require(Capability.HIDDEN_STATES) # list of (seq, dim)
|
|
54
|
+
L = len(hidden)
|
|
55
|
+
mat = torch.zeros(L, L)
|
|
56
|
+
for i in range(L):
|
|
57
|
+
for j in range(i, L):
|
|
58
|
+
c = linear_cka(hidden[i], hidden[j])
|
|
59
|
+
mat[i, j] = mat[j, i] = c
|
|
60
|
+
off = mat[~torch.eye(L, dtype=torch.bool)]
|
|
61
|
+
return Result(
|
|
62
|
+
analyzer=self.name, model=repr(model), cases=cases,
|
|
63
|
+
artifacts={"cka_matrix": mat},
|
|
64
|
+
findings={
|
|
65
|
+
"n_layers": L,
|
|
66
|
+
"mean_offdiagonal_cka": round(float(off.mean()), 4),
|
|
67
|
+
"adjacent_layer_cka": [round(float(mat[i, i + 1]), 4) for i in range(L - 1)],
|
|
68
|
+
"_caveat": "CKA validated on CLIP two-tower encoders; scope to a tower, not decoder MLLM cross-modal geometry.",
|
|
69
|
+
},
|
|
70
|
+
)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Linear probing — per-layer representation richness for a target property.
|
|
2
|
+
|
|
3
|
+
Train a linear classifier on each layer's hidden states (samples + labels) and
|
|
4
|
+
report per-layer HELD-OUT accuracy — where in the stack is the property linearly
|
|
5
|
+
decodable. Labels come from ``case.label`` (PASS/FAIL), so the probe answers:
|
|
6
|
+
"at which depth do the representations already separate failing from passing
|
|
7
|
+
inputs". Correlational, not causal.
|
|
8
|
+
|
|
9
|
+
Implementation notes (no sklearn dependency — torch only):
|
|
10
|
+
- one forward per case (``HIDDEN_STATES``), feature = hidden state at ``pos``;
|
|
11
|
+
- per-layer logistic regression, class-weighted BCE + L2, Adam;
|
|
12
|
+
- stratified 2-fold cross-validation: every reported number and every
|
|
13
|
+
``per_case`` probability is out-of-fold (never train-set);
|
|
14
|
+
- balanced accuracy (mean of per-class recalls) — robust to FAIL/PASS skew;
|
|
15
|
+
- too few labelled cases per class -> returns a SKIPPED result with the reason
|
|
16
|
+
instead of raising (an analyzer crash silently removes the evidence slot
|
|
17
|
+
from the loop's view; a degraded honest result does not).
|
|
18
|
+
|
|
19
|
+
References:
|
|
20
|
+
- Understanding intermediate layers using linear classifier probes
|
|
21
|
+
Alain & Bengio, 2016 — arXiv:1610.01644
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import TYPE_CHECKING
|
|
27
|
+
|
|
28
|
+
from evalrx.core.analyzer import Analyzer
|
|
29
|
+
from evalrx.core.capability import Capability
|
|
30
|
+
from evalrx.core.registry import register_analyzer
|
|
31
|
+
from evalrx.core.result import Result
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from evalrx.core.case import CaseBatch
|
|
35
|
+
from evalrx.core.model import Model
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@register_analyzer("linear_probe")
|
|
39
|
+
class LinearProbeAnalyzer(Analyzer):
|
|
40
|
+
"""Per-layer linear-probe (held-out, balanced) accuracy for PASS/FAIL."""
|
|
41
|
+
|
|
42
|
+
name = "linear_probe"
|
|
43
|
+
requires = frozenset({Capability.HIDDEN_STATES})
|
|
44
|
+
applies_to_modalities = frozenset({"text", "image"})
|
|
45
|
+
|
|
46
|
+
# max_cases sized for enriched batches (see LogitLensAnalyzer): the held-out
|
|
47
|
+
# probe needs enough of the scarce FAIL class for a trustworthy accuracy
|
|
48
|
+
# curve and a powered downstream contrast; one forward per case.
|
|
49
|
+
def __init__(self, pos: int = -1, max_cases: int = 0, min_per_class: int = 4,
|
|
50
|
+
l2: float = 1e-2, epochs: int = 200, seed: int = 0) -> None:
|
|
51
|
+
super().__init__(pos=pos, max_cases=max_cases, min_per_class=min_per_class,
|
|
52
|
+
l2=l2, epochs=epochs, seed=seed)
|
|
53
|
+
|
|
54
|
+
# ------------------------------------------------------------------
|
|
55
|
+
def _fit_predict(self, X_tr, y_tr, X_te):
|
|
56
|
+
"""Class-weighted logistic regression; returns P(fail) on X_te."""
|
|
57
|
+
import torch
|
|
58
|
+
|
|
59
|
+
torch.manual_seed(self.seed)
|
|
60
|
+
d = X_tr.shape[1]
|
|
61
|
+
w = torch.zeros(d, requires_grad=True)
|
|
62
|
+
b = torch.zeros(1, requires_grad=True)
|
|
63
|
+
n_pos = float(y_tr.sum())
|
|
64
|
+
pos_weight = torch.tensor((len(y_tr) - n_pos) / max(n_pos, 1.0))
|
|
65
|
+
opt = torch.optim.Adam([w, b], lr=0.05)
|
|
66
|
+
loss_fn = torch.nn.BCEWithLogitsLoss(pos_weight=pos_weight)
|
|
67
|
+
for _ in range(self.epochs):
|
|
68
|
+
opt.zero_grad()
|
|
69
|
+
loss = loss_fn(X_tr @ w + b, y_tr) + self.l2 * w.pow(2).sum()
|
|
70
|
+
loss.backward()
|
|
71
|
+
opt.step()
|
|
72
|
+
with torch.no_grad():
|
|
73
|
+
return torch.sigmoid(X_te @ w + b)
|
|
74
|
+
|
|
75
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> Result:
|
|
76
|
+
import torch
|
|
77
|
+
|
|
78
|
+
from evalrx.core.case import CaseBatch, Label
|
|
79
|
+
|
|
80
|
+
# Label-stratified subsample so the capped probe pool keeps enough FAIL
|
|
81
|
+
# cases (a plain head is mostly PASS on an enriched batch).
|
|
82
|
+
labelled_batch = CaseBatch([c for c in cases
|
|
83
|
+
if c.label in (Label.PASS, Label.FAIL)])
|
|
84
|
+
labelled = labelled_batch.stratified_head(self.max_cases)
|
|
85
|
+
n_fail = sum(1 for c in labelled if c.label == Label.FAIL)
|
|
86
|
+
n_pass = len(labelled) - n_fail
|
|
87
|
+
|
|
88
|
+
def skipped(reason: str) -> Result:
|
|
89
|
+
return Result(
|
|
90
|
+
analyzer=self.name, model=repr(model), cases=cases,
|
|
91
|
+
findings={"skipped": reason, "n_fail": n_fail, "n_pass": n_pass,
|
|
92
|
+
"n_layers": None, "per_layer_accuracy": [],
|
|
93
|
+
"best_layer": None, "best_accuracy": None, "per_case": []},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if min(n_fail, n_pass) < self.min_per_class:
|
|
97
|
+
return skipped(f"needs >= {self.min_per_class} cases per class "
|
|
98
|
+
f"(got fail={n_fail}, pass={n_pass})")
|
|
99
|
+
|
|
100
|
+
# -- collect features: one forward per case, all layers at `pos` ----
|
|
101
|
+
feats, ys, ids = [], [], []
|
|
102
|
+
for case in labelled:
|
|
103
|
+
trace = model.forward(case.inputs, capture={Capability.HIDDEN_STATES})
|
|
104
|
+
hidden = trace.require(Capability.HIDDEN_STATES)
|
|
105
|
+
feats.append(torch.stack([h[self.pos].detach().float().cpu() for h in hidden]))
|
|
106
|
+
ys.append(1.0 if case.label == Label.FAIL else 0.0)
|
|
107
|
+
ids.append(case.id)
|
|
108
|
+
X = torch.stack(feats) # (n, L, d)
|
|
109
|
+
y = torch.tensor(ys)
|
|
110
|
+
n, n_layers, _ = X.shape
|
|
111
|
+
|
|
112
|
+
# -- stratified 2-fold assignment (seeded, deterministic) -----------
|
|
113
|
+
g = torch.Generator().manual_seed(self.seed)
|
|
114
|
+
fold = torch.zeros(n, dtype=torch.long)
|
|
115
|
+
for cls in (0.0, 1.0):
|
|
116
|
+
idx = [i for i in range(n) if ys[i] == cls]
|
|
117
|
+
perm = torch.randperm(len(idx), generator=g)
|
|
118
|
+
for j, p in enumerate(perm):
|
|
119
|
+
fold[idx[p]] = j % 2
|
|
120
|
+
|
|
121
|
+
per_layer_acc, oof = [], torch.zeros(n_layers, n)
|
|
122
|
+
for layer in range(n_layers):
|
|
123
|
+
Xl = X[:, layer, :]
|
|
124
|
+
accs = []
|
|
125
|
+
for f in (0, 1):
|
|
126
|
+
tr, te = (fold != f), (fold == f)
|
|
127
|
+
mu, sd = Xl[tr].mean(0), Xl[tr].std(0).clamp_min(1e-6)
|
|
128
|
+
p = self._fit_predict((Xl[tr] - mu) / sd, y[tr], (Xl[te] - mu) / sd)
|
|
129
|
+
oof[layer, te] = p
|
|
130
|
+
pred = (p > 0.5).float()
|
|
131
|
+
yt = y[te]
|
|
132
|
+
rec_f = ((pred == 1) & (yt == 1)).sum() / (yt == 1).sum().clamp_min(1)
|
|
133
|
+
rec_p = ((pred == 0) & (yt == 0)).sum() / (yt == 0).sum().clamp_min(1)
|
|
134
|
+
accs.append(float((rec_f + rec_p) / 2))
|
|
135
|
+
per_layer_acc.append(round(sum(accs) / len(accs), 4))
|
|
136
|
+
|
|
137
|
+
best_layer = max(range(n_layers), key=lambda i: per_layer_acc[i])
|
|
138
|
+
per_case = [{
|
|
139
|
+
"sample_id": ids[i],
|
|
140
|
+
"fail_prob_best_layer": round(float(oof[best_layer, i]), 4),
|
|
141
|
+
"is_fail": bool(ys[i]),
|
|
142
|
+
} for i in range(n)]
|
|
143
|
+
|
|
144
|
+
return Result(
|
|
145
|
+
analyzer=self.name, model=repr(model), cases=cases,
|
|
146
|
+
artifacts={"per_layer_accuracy": per_layer_acc, "per_case": per_case},
|
|
147
|
+
findings={
|
|
148
|
+
"n_layers": n_layers, "pos": self.pos,
|
|
149
|
+
"n_cases": n, "n_fail": n_fail, "n_pass": n_pass,
|
|
150
|
+
"per_layer_accuracy": per_layer_acc,
|
|
151
|
+
"best_layer": best_layer,
|
|
152
|
+
"best_layer_frac": round(best_layer / max(1, n_layers - 1), 4),
|
|
153
|
+
"best_accuracy": per_layer_acc[best_layer],
|
|
154
|
+
"chance": 0.5, # balanced accuracy baseline
|
|
155
|
+
"per_case": per_case,
|
|
156
|
+
},
|
|
157
|
+
)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Hallucination diagnostics — the dominant MLLM failure mode (BB probes + WB attention)."""
|
|
2
|
+
|
|
3
|
+
from evalrx.analyzers.hallucination.chair import CHAIRAnalyzer, chair_score
|
|
4
|
+
from evalrx.analyzers.hallucination.opera import OPERAAnalyzer
|
|
5
|
+
from evalrx.analyzers.hallucination.pope import POPEAnalyzer
|
|
6
|
+
from evalrx.analyzers.hallucination.selfcheck import SelfCheckConsistencyAnalyzer
|
|
7
|
+
from evalrx.analyzers.hallucination.vcd import VCDAnalyzer
|
|
8
|
+
|
|
9
|
+
__all__ = ["POPEAnalyzer", "CHAIRAnalyzer", "chair_score", "OPERAAnalyzer", "VCDAnalyzer", "SelfCheckConsistencyAnalyzer"]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""CHAIR — Caption Hallucination Assessment with Image Relevance.
|
|
2
|
+
|
|
3
|
+
Black-box (``GENERATE``): generate a caption, extract mentioned objects from a
|
|
4
|
+
fixed object vocabulary, and compare to the image's gold objects
|
|
5
|
+
(``metadata["gt_objects"]``). Reports CHAIR_i (mean fraction of mentioned objects
|
|
6
|
+
that are hallucinated) and CHAIR_s (fraction of captions with ≥1 hallucination).
|
|
7
|
+
``chair_score`` is the reusable per-instance metric.
|
|
8
|
+
|
|
9
|
+
Paper: "Object Hallucination in Image Captioning"
|
|
10
|
+
Rohrbach et al., EMNLP 2018 — https://arxiv.org/abs/1809.02156
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import TYPE_CHECKING, Iterable
|
|
16
|
+
|
|
17
|
+
from evalrx.core.analyzer import Analyzer
|
|
18
|
+
from evalrx.core.capability import Capability
|
|
19
|
+
from evalrx.core.registry import register_analyzer
|
|
20
|
+
from evalrx.core.result import Result
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from evalrx.core.case import CaseBatch
|
|
24
|
+
from evalrx.core.model import Model
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def chair_score(generated_objects: Iterable[str], gt_objects: Iterable[str]) -> dict:
|
|
28
|
+
"""Per-instance CHAIR_i: fraction of mentioned objects that are hallucinated."""
|
|
29
|
+
gen = {o.lower() for o in generated_objects}
|
|
30
|
+
gt = {o.lower() for o in gt_objects}
|
|
31
|
+
hallucinated = sorted(gen - gt)
|
|
32
|
+
chair_i = len(hallucinated) / len(gen) if gen else 0.0
|
|
33
|
+
return {"chair_i": round(chair_i, 4), "hallucinated": hallucinated, "n_mentioned": len(gen)}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def extract_objects(caption: str, vocab: Iterable[str]) -> list[str]:
|
|
37
|
+
"""Naive object extractor: vocabulary words that appear in the caption."""
|
|
38
|
+
text = f" {caption.lower()} "
|
|
39
|
+
return [w for w in vocab if f" {w.lower()} " in text or f" {w.lower()}s " in text]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@register_analyzer("chair")
|
|
43
|
+
class CHAIRAnalyzer(Analyzer):
|
|
44
|
+
"""Object-hallucination rate of generated captions vs gold objects."""
|
|
45
|
+
|
|
46
|
+
name = "chair"
|
|
47
|
+
requires = frozenset({Capability.GENERATE})
|
|
48
|
+
applies_to_modalities = frozenset({"image"})
|
|
49
|
+
# An omni model DECLARES image even on an audio benchmark, so the registry
|
|
50
|
+
# match alone still offers this on a batch with no images in it.
|
|
51
|
+
requires_modalities = frozenset({"image"})
|
|
52
|
+
|
|
53
|
+
def __init__(self, object_vocab: Iterable[str], gt_key: str = "gt_objects") -> None:
|
|
54
|
+
super().__init__(object_vocab=list(object_vocab), gt_key=gt_key)
|
|
55
|
+
|
|
56
|
+
def _run(self, model: "Model", cases: "CaseBatch") -> Result:
|
|
57
|
+
per_case = []
|
|
58
|
+
chair_is = []
|
|
59
|
+
n_with_hallucination = 0
|
|
60
|
+
for case in cases:
|
|
61
|
+
caption = model.generate(case.inputs)
|
|
62
|
+
mentioned = extract_objects(caption, self.object_vocab)
|
|
63
|
+
gt = case.metadata.get(self.gt_key, [])
|
|
64
|
+
sc = chair_score(mentioned, gt)
|
|
65
|
+
per_case.append({"id": case.id, **sc, "mentioned": mentioned})
|
|
66
|
+
chair_is.append(sc["chair_i"])
|
|
67
|
+
if sc["hallucinated"]:
|
|
68
|
+
n_with_hallucination += 1
|
|
69
|
+
n = len(chair_is)
|
|
70
|
+
return Result(
|
|
71
|
+
analyzer=self.name, model=repr(model), cases=cases,
|
|
72
|
+
artifacts={"per_case": per_case},
|
|
73
|
+
findings={
|
|
74
|
+
"n": n,
|
|
75
|
+
"chair_i": round(sum(chair_is) / n, 4) if n else None, # mean per-instance hallucination rate
|
|
76
|
+
"chair_s": round(n_with_hallucination / n, 4) if n else None, # fraction of captions w/ a hallucination
|
|
77
|
+
},
|
|
78
|
+
)
|