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,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
+ )
@@ -0,0 +1,5 @@
1
+ """Causal intervention analyzers (activation patching / causal tracing)."""
2
+
3
+ from evalrx.analyzers.patching.causal_trace import CausalTraceAnalyzer
4
+
5
+ __all__ = ["CausalTraceAnalyzer"]