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.
Files changed (339) hide show
  1. evalrx/__init__.py +139 -0
  2. evalrx/agent_assets/__init__.py +2 -0
  3. evalrx/agent_assets/skills/README.md +28 -0
  4. evalrx/agent_assets/skills/eval-chart-style/SKILL.md +172 -0
  5. evalrx/agent_assets/skills/evalrx-report-ui/SKILL.md +116 -0
  6. evalrx/agent_assets/skills/nature-figure/LICENSE +201 -0
  7. evalrx/agent_assets/skills/nature-figure/README.md +412 -0
  8. evalrx/agent_assets/skills/nature-figure/SKILL.md +60 -0
  9. evalrx/agent_assets/skills/nature-figure/manifest.yaml +59 -0
  10. evalrx/agent_assets/skills/nature-figure/references/api.md +436 -0
  11. evalrx/agent_assets/skills/nature-figure/references/backend-selection.md +100 -0
  12. evalrx/agent_assets/skills/nature-figure/references/chart-types.md +281 -0
  13. evalrx/agent_assets/skills/nature-figure/references/common-patterns.md +350 -0
  14. evalrx/agent_assets/skills/nature-figure/references/demos.md +65 -0
  15. evalrx/agent_assets/skills/nature-figure/references/design-theory.md +439 -0
  16. evalrx/agent_assets/skills/nature-figure/references/figure-contract.md +93 -0
  17. evalrx/agent_assets/skills/nature-figure/references/figure-legend-conventions.md +71 -0
  18. evalrx/agent_assets/skills/nature-figure/references/nature-2026-observations.md +112 -0
  19. evalrx/agent_assets/skills/nature-figure/references/qa-contract.md +119 -0
  20. evalrx/agent_assets/skills/nature-figure/references/r-template-index.md +66 -0
  21. evalrx/agent_assets/skills/nature-figure/references/r-workflow.md +161 -0
  22. evalrx/agent_assets/skills/nature-figure/references/tutorials.md +251 -0
  23. evalrx/agent_assets/skills/nature-figure/static/core/contract.md +29 -0
  24. evalrx/agent_assets/skills/nature-figure/static/core/stance.md +37 -0
  25. evalrx/agent_assets/skills/nature-figure/static/fragments/backend/python.md +37 -0
  26. evalrx/agent_assets/skills/nature-figure/static/fragments/backend/r.md +44 -0
  27. evalrx/agent_assets/skills/outcome-driver-analysis/SKILL.md +213 -0
  28. evalrx/agent_assets/skills/outcome-driver-analysis/assets/analysis_report_template.md +53 -0
  29. evalrx/agent_assets/skills/outcome-driver-analysis/references/model_selection.md +72 -0
  30. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.R +130 -0
  31. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/explanatory_var_eda.py +150 -0
  32. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.R +181 -0
  33. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/fit_outcome_model.py +186 -0
  34. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.R +149 -0
  35. evalrx/agent_assets/skills/outcome-driver-analysis/scripts/univariate_eda.py +177 -0
  36. evalrx/agent_assets/skills.py +27 -0
  37. evalrx/agent_runtime/__init__.py +78 -0
  38. evalrx/agent_runtime/_docker_runner.py +89 -0
  39. evalrx/agent_runtime/cli_runtime.py +103 -0
  40. evalrx/agent_runtime/cli_transcript.py +138 -0
  41. evalrx/agent_runtime/cli_types.py +68 -0
  42. evalrx/agent_runtime/codegen/__init__.py +5 -0
  43. evalrx/agent_runtime/codegen/runner.py +94 -0
  44. evalrx/agent_runtime/experiment_harness.py +117 -0
  45. evalrx/agent_runtime/factory.py +102 -0
  46. evalrx/agent_runtime/json_shape.py +44 -0
  47. evalrx/agent_runtime/judges/__init__.py +28 -0
  48. evalrx/agent_runtime/judges/agy.py +179 -0
  49. evalrx/agent_runtime/judges/autodetect.py +135 -0
  50. evalrx/agent_runtime/judges/claude.py +159 -0
  51. evalrx/agent_runtime/judges/codex.py +120 -0
  52. evalrx/agent_runtime/providers/__init__.py +21 -0
  53. evalrx/agent_runtime/providers/antigravity.py +31 -0
  54. evalrx/agent_runtime/providers/base.py +145 -0
  55. evalrx/agent_runtime/providers/claude_code.py +49 -0
  56. evalrx/agent_runtime/providers/codex.py +37 -0
  57. evalrx/agent_runtime/providers/gemini_cli.py +26 -0
  58. evalrx/agent_runtime/providers/kimi_cli.py +27 -0
  59. evalrx/agent_runtime/providers/opencode.py +27 -0
  60. evalrx/agent_runtime/providers/registry.py +58 -0
  61. evalrx/agent_runtime/sandbox.py +517 -0
  62. evalrx/agent_runtime/skill_audit.py +143 -0
  63. evalrx/agent_runtime/skills/__init__.py +19 -0
  64. evalrx/agent_runtime/skills/installer.py +68 -0
  65. evalrx/agent_runtime/skills/prompt_policy.py +86 -0
  66. evalrx/agent_runtime/skills/resolver.py +19 -0
  67. evalrx/analysis/__init__.py +132 -0
  68. evalrx/analysis/adjudicate.py +154 -0
  69. evalrx/analysis/analysis_module.py +361 -0
  70. evalrx/analysis/api.py +171 -0
  71. evalrx/analysis/case_studio.py +651 -0
  72. evalrx/analysis/cli.py +114 -0
  73. evalrx/analysis/dashboard.py +350 -0
  74. evalrx/analysis/eval_case_matrix.py +118 -0
  75. evalrx/analysis/eval_viz_theme.py +833 -0
  76. evalrx/analysis/explore_run.py +333 -0
  77. evalrx/analysis/explorer.py +1276 -0
  78. evalrx/analysis/failure_modes.py +607 -0
  79. evalrx/analysis/fused_pipeline.py +489 -0
  80. evalrx/analysis/holdout.py +300 -0
  81. evalrx/analysis/hypothesis_agent.py +230 -0
  82. evalrx/analysis/narration.py +177 -0
  83. evalrx/analysis/operationalize.py +442 -0
  84. evalrx/analysis/plain_language.py +42 -0
  85. evalrx/analysis/planner.py +283 -0
  86. evalrx/analysis/probe_search.py +203 -0
  87. evalrx/analysis/profile.py +268 -0
  88. evalrx/analysis/prompts/__init__.py +0 -0
  89. evalrx/analysis/prompts/explorer.py +417 -0
  90. evalrx/analysis/prompts/failure_modes.py +33 -0
  91. evalrx/analysis/prompts/holdout.py +27 -0
  92. evalrx/analysis/prompts/hypothesis_agent.py +78 -0
  93. evalrx/analysis/prompts/run_codebase.py +47 -0
  94. evalrx/analysis/prompts/stats_agent.py +72 -0
  95. evalrx/analysis/prompts/stats_tool_generator.py +43 -0
  96. evalrx/analysis/result_marker.py +47 -0
  97. evalrx/analysis/run_codebase.py +242 -0
  98. evalrx/analysis/run_view.py +205 -0
  99. evalrx/analysis/stage_views.py +93 -0
  100. evalrx/analysis/stats_agent.py +944 -0
  101. evalrx/analysis/stats_tool_agent.py +261 -0
  102. evalrx/analysis/stats_tool_generator.py +415 -0
  103. evalrx/analysis/stats_tools.py +1153 -0
  104. evalrx/analysis/trajectory_records.py +193 -0
  105. evalrx/analysis/workbench.py +431 -0
  106. evalrx/analyzers/__init__.py +42 -0
  107. evalrx/analyzers/agent/__init__.py +25 -0
  108. evalrx/analyzers/agent/counterfactual.py +84 -0
  109. evalrx/analyzers/agent/first_error_judge.py +96 -0
  110. evalrx/analyzers/agent/ignored_obs.py +81 -0
  111. evalrx/analyzers/agent/loop_detect.py +79 -0
  112. evalrx/analyzers/agent/reliability.py +165 -0
  113. evalrx/analyzers/agent/tool_shap.py +225 -0
  114. evalrx/analyzers/agent/trajectory_rubric.py +168 -0
  115. evalrx/analyzers/attention/__init__.py +19 -0
  116. evalrx/analyzers/attention/relative_attn.py +610 -0
  117. evalrx/analyzers/attention/rollout.py +73 -0
  118. evalrx/analyzers/attention/sink.py +56 -0
  119. evalrx/analyzers/attention/summary.py +190 -0
  120. evalrx/analyzers/attribution/__init__.py +6 -0
  121. evalrx/analyzers/attribution/generic_attn.py +31 -0
  122. evalrx/analyzers/attribution/gradcam.py +30 -0
  123. evalrx/analyzers/base.py +12 -0
  124. evalrx/analyzers/geometry/__init__.py +6 -0
  125. evalrx/analyzers/geometry/cka.py +70 -0
  126. evalrx/analyzers/geometry/linear_probe.py +157 -0
  127. evalrx/analyzers/hallucination/__init__.py +9 -0
  128. evalrx/analyzers/hallucination/chair.py +78 -0
  129. evalrx/analyzers/hallucination/opera.py +29 -0
  130. evalrx/analyzers/hallucination/pope.py +119 -0
  131. evalrx/analyzers/hallucination/selfcheck.py +155 -0
  132. evalrx/analyzers/hallucination/vcd.py +29 -0
  133. evalrx/analyzers/lens/__init__.py +7 -0
  134. evalrx/analyzers/lens/layer_contrast.py +133 -0
  135. evalrx/analyzers/lens/logit_lens.py +138 -0
  136. evalrx/analyzers/lens/tuned_lens.py +30 -0
  137. evalrx/analyzers/patching/__init__.py +5 -0
  138. evalrx/analyzers/patching/causal_trace.py +30 -0
  139. evalrx/analyzers/perturbation/__init__.py +23 -0
  140. evalrx/analyzers/perturbation/_shapley.py +54 -0
  141. evalrx/analyzers/perturbation/context_shap.py +174 -0
  142. evalrx/analyzers/perturbation/cot_faithfulness.py +239 -0
  143. evalrx/analyzers/perturbation/format_sensitivity.py +237 -0
  144. evalrx/analyzers/perturbation/mm_shap.py +146 -0
  145. evalrx/analyzers/perturbation/modality_ablation.py +196 -0
  146. evalrx/analyzers/perturbation/perturbation_battery.py +274 -0
  147. evalrx/analyzers/perturbation/prompt_contrast.py +265 -0
  148. evalrx/analyzers/perturbation/rise.py +94 -0
  149. evalrx/analyzers/perturbation/vl_shap.py +102 -0
  150. evalrx/analyzers/reasoning/__init__.py +33 -0
  151. evalrx/analyzers/reasoning/_text.py +328 -0
  152. evalrx/analyzers/reasoning/answer_extraction_audit.py +327 -0
  153. evalrx/analyzers/reasoning/arith_audit.py +226 -0
  154. evalrx/analyzers/reasoning/contamination.py +214 -0
  155. evalrx/analyzers/reasoning/knowledge_split.py +253 -0
  156. evalrx/analyzers/reasoning/self_repair.py +246 -0
  157. evalrx/analyzers/reasoning/step_rollout_value.py +216 -0
  158. evalrx/analyzers/reasoning/termination_audit.py +258 -0
  159. evalrx/analyzers/uncertainty/__init__.py +18 -0
  160. evalrx/analyzers/uncertainty/calibration.py +174 -0
  161. evalrx/analyzers/uncertainty/coverage_gap.py +199 -0
  162. evalrx/analyzers/uncertainty/entropy.py +90 -0
  163. evalrx/analyzers/uncertainty/logprob_entropy.py +69 -0
  164. evalrx/analyzers/uncertainty/self_consistency.py +204 -0
  165. evalrx/analyzers/uncertainty/verbalized_conf.py +64 -0
  166. evalrx/cli.py +411 -0
  167. evalrx/config.py +77 -0
  168. evalrx/contract/__init__.py +179 -0
  169. evalrx/contract/common.py +452 -0
  170. evalrx/contract/emit.py +948 -0
  171. evalrx/contract/export.py +237 -0
  172. evalrx/contract/m1.py +325 -0
  173. evalrx/contract/m2.py +317 -0
  174. evalrx/contract/m3.py +165 -0
  175. evalrx/contract/m4.py +130 -0
  176. evalrx/contract/m5.py +292 -0
  177. evalrx/contract/methodology.py +76 -0
  178. evalrx/contract/pre_m1.py +58 -0
  179. evalrx/contract/typescript.py +140 -0
  180. evalrx/core/__init__.py +85 -0
  181. evalrx/core/analyzer.py +174 -0
  182. evalrx/core/capability.py +54 -0
  183. evalrx/core/case.py +443 -0
  184. evalrx/core/experiment.py +106 -0
  185. evalrx/core/model.py +198 -0
  186. evalrx/core/pipeline.py +42 -0
  187. evalrx/core/registry.py +142 -0
  188. evalrx/core/result.py +64 -0
  189. evalrx/core/spec.py +173 -0
  190. evalrx/core/tokentype.py +165 -0
  191. evalrx/core/tool.py +92 -0
  192. evalrx/datasets/__init__.py +41 -0
  193. evalrx/datasets/base.py +68 -0
  194. evalrx/datasets/gui_os.py +52 -0
  195. evalrx/datasets/llm_qa.py +57 -0
  196. evalrx/datasets/pure_qa.py +12 -0
  197. evalrx/datasets/vlm_qa.py +695 -0
  198. evalrx/datasets/web_search_qa.py +52 -0
  199. evalrx/eval_agent/__init__.py +341 -0
  200. evalrx/eval_agent/_tools.py +81 -0
  201. evalrx/eval_agent/ab_runner.py +50 -0
  202. evalrx/eval_agent/agentic/__init__.py +43 -0
  203. evalrx/eval_agent/agentic/actions.py +216 -0
  204. evalrx/eval_agent/agentic/board.py +107 -0
  205. evalrx/eval_agent/agentic/loop.py +190 -0
  206. evalrx/eval_agent/agentic/tools.py +538 -0
  207. evalrx/eval_agent/checkpoint.py +57 -0
  208. evalrx/eval_agent/cli_agent.py +59 -0
  209. evalrx/eval_agent/cli_skills.py +5 -0
  210. evalrx/eval_agent/evolution.py +396 -0
  211. evalrx/eval_agent/git_manager.py +215 -0
  212. evalrx/eval_agent/hypothesis.py +172 -0
  213. evalrx/eval_agent/label_quarantine.py +209 -0
  214. evalrx/eval_agent/legacy.py +530 -0
  215. evalrx/eval_agent/log_schema.py +497 -0
  216. evalrx/eval_agent/loop.py +2159 -0
  217. evalrx/eval_agent/loop_reports.py +116 -0
  218. evalrx/eval_agent/model_instrumentation.py +282 -0
  219. evalrx/eval_agent/narration.py +193 -0
  220. evalrx/eval_agent/nl_runner.py +460 -0
  221. evalrx/eval_agent/orchestrator.py +61 -0
  222. evalrx/eval_agent/preregister.py +93 -0
  223. evalrx/eval_agent/prompts/__init__.py +1 -0
  224. evalrx/eval_agent/prompts/agentic.py +46 -0
  225. evalrx/eval_agent/prompts/case_discovery.py +25 -0
  226. evalrx/eval_agent/prompts/diagnosis.py +125 -0
  227. evalrx/eval_agent/prompts/experiment_writer.py +265 -0
  228. evalrx/eval_agent/prompts/explore_step.py +37 -0
  229. evalrx/eval_agent/prompts/fix_agent.py +257 -0
  230. evalrx/eval_agent/prompts/hypothesis_tester.py +15 -0
  231. evalrx/eval_agent/prompts/nl_runner.py +38 -0
  232. evalrx/eval_agent/prompts/probe_agent.py +25 -0
  233. evalrx/eval_agent/prompts/probe_candidate_generator.py +14 -0
  234. evalrx/eval_agent/prompts/probe_generator.py +35 -0
  235. evalrx/eval_agent/prompts/whitebox_probe_generator.py +38 -0
  236. evalrx/eval_agent/report.py +58 -0
  237. evalrx/eval_agent/run_context.py +354 -0
  238. evalrx/eval_agent/run_log.schema.json +1215 -0
  239. evalrx/eval_agent/run_logger_v2.py +1764 -0
  240. evalrx/eval_agent/run_metadata.py +208 -0
  241. evalrx/eval_agent/stages/__init__.py +56 -0
  242. evalrx/eval_agent/stages/case_discovery.py +293 -0
  243. evalrx/eval_agent/stages/diagnosis.py +1017 -0
  244. evalrx/eval_agent/stages/experiment_writer.py +1634 -0
  245. evalrx/eval_agent/stages/fix_agent.py +3916 -0
  246. evalrx/eval_agent/stages/fix_internals.py +499 -0
  247. evalrx/eval_agent/stages/fix_pipeline.py +725 -0
  248. evalrx/eval_agent/stages/fix_tiers.py +187 -0
  249. evalrx/eval_agent/stages/fix_tools.py +1034 -0
  250. evalrx/eval_agent/stages/hypothesis_tester.py +1014 -0
  251. evalrx/eval_agent/stages/probe.py +439 -0
  252. evalrx/eval_agent/stages/probe_agent.py +1079 -0
  253. evalrx/eval_agent/stages/probe_candidate_generator.py +128 -0
  254. evalrx/eval_agent/stages/probe_generator.py +326 -0
  255. evalrx/eval_agent/stages/probe_search_agent.py +106 -0
  256. evalrx/eval_agent/stages/protocol.py +112 -0
  257. evalrx/eval_agent/stages/repair_catalog.py +273 -0
  258. evalrx/eval_agent/stages/surgery.py +524 -0
  259. evalrx/eval_agent/stages/whitebox_probe_generator.py +351 -0
  260. evalrx/eval_agent/store.py +231 -0
  261. evalrx/logging_utils.py +112 -0
  262. evalrx/models/__init__.py +161 -0
  263. evalrx/models/_discover.py +101 -0
  264. evalrx/models/agent.py +380 -0
  265. evalrx/models/backends/__init__.py +58 -0
  266. evalrx/models/backends/api.py +169 -0
  267. evalrx/models/backends/base.py +57 -0
  268. evalrx/models/backends/gemini_compat.py +579 -0
  269. evalrx/models/backends/hf_local.py +2074 -0
  270. evalrx/models/backends/openai_compat.py +301 -0
  271. evalrx/models/backends/vllm_offline.py +116 -0
  272. evalrx/models/base.py +24 -0
  273. evalrx/models/blackbox/__init__.py +4 -0
  274. evalrx/models/blackbox/agent.py +31 -0
  275. evalrx/models/blackbox/base.py +29 -0
  276. evalrx/models/blackbox/gemini.py +279 -0
  277. evalrx/models/blackbox/llm_api.py +17 -0
  278. evalrx/models/blackbox/vlm_api.py +17 -0
  279. evalrx/models/compose.py +66 -0
  280. evalrx/models/inference.py +88 -0
  281. evalrx/models/paper_methods/__init__.py +8 -0
  282. evalrx/models/paper_methods/aad.py +53 -0
  283. evalrx/models/paper_methods/ifcd.py +204 -0
  284. evalrx/models/paper_methods/pai.py +164 -0
  285. evalrx/models/paper_methods/tcd.py +202 -0
  286. evalrx/models/paper_methods/vcd.py +45 -0
  287. evalrx/models/paper_methods/vicrop.py +137 -0
  288. evalrx/models/toolcodec.py +143 -0
  289. evalrx/models/tools/__init__.py +20 -0
  290. evalrx/models/tools/perception.py +300 -0
  291. evalrx/models/tools/visual.py +174 -0
  292. evalrx/models/whitebox/__init__.py +26 -0
  293. evalrx/models/whitebox/agent.py +31 -0
  294. evalrx/models/whitebox/base.py +24 -0
  295. evalrx/models/whitebox/qwen.py +61 -0
  296. evalrx/models/whitebox/qwen2_5_omni.py +29 -0
  297. evalrx/models/whitebox/qwen2_audio.py +25 -0
  298. evalrx/models/whitebox/qwen_omni.py +53 -0
  299. evalrx/models/whitebox/qwen_vl.py +62 -0
  300. evalrx/observability/__init__.py +21 -0
  301. evalrx/observability/envelope.py +122 -0
  302. evalrx/observability/outbox.py +111 -0
  303. evalrx/observability/tracer.py +882 -0
  304. evalrx/reporting/__init__.py +28 -0
  305. evalrx/reporting/case_study.py +947 -0
  306. evalrx/reporting/compiler.py +587 -0
  307. evalrx/reporting/dynamic.py +1882 -0
  308. evalrx/reporting/html_report.py +2225 -0
  309. evalrx/reporting/langfuse_exporter.py +38 -0
  310. evalrx/reporting/langfuse_source.py +155 -0
  311. evalrx/reporting/model.py +151 -0
  312. evalrx/reporting/run_events.py +184 -0
  313. evalrx/reporting/server.py +557 -0
  314. evalrx/reporting/stages.py +58 -0
  315. evalrx/reporting/static_export.py +142 -0
  316. evalrx/reporting/web_dist/index.html +146 -0
  317. evalrx/specs.py +727 -0
  318. evalrx/stats/__init__.py +47 -0
  319. evalrx/stats/api.py +192 -0
  320. evalrx/stats/bootstrap.py +86 -0
  321. evalrx/stats/ebh.py +27 -0
  322. evalrx/stats/evalue.py +98 -0
  323. evalrx/stats/friedman.py +138 -0
  324. evalrx/stats/mcnemar.py +40 -0
  325. evalrx/stats/multiplicity.py +159 -0
  326. evalrx/stats/subset_sampling.py +55 -0
  327. evalrx/term_links.py +43 -0
  328. evalrx/viz/__init__.py +7 -0
  329. evalrx/viz/labels.py +77 -0
  330. evalrx/viz/prompts.py +39 -0
  331. evalrx/viz/renderer.py +590 -0
  332. evalrx/viz/schema.py +36 -0
  333. evalrx/viz/style.py +134 -0
  334. evalrx-0.1.2.dist-info/METADATA +532 -0
  335. evalrx-0.1.2.dist-info/RECORD +339 -0
  336. evalrx-0.1.2.dist-info/WHEEL +5 -0
  337. evalrx-0.1.2.dist-info/entry_points.txt +3 -0
  338. evalrx-0.1.2.dist-info/licenses/LICENSE +121 -0
  339. 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
+ )
@@ -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
+ )