open-interpretability 0.0.20__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.
- open_interpretability-0.0.20.dist-info/METADATA +157 -0
- open_interpretability-0.0.20.dist-info/RECORD +256 -0
- open_interpretability-0.0.20.dist-info/WHEEL +4 -0
- open_interpretability-0.0.20.dist-info/entry_points.txt +2 -0
- open_interpretability-0.0.20.dist-info/licenses/LICENSE +21 -0
- openinterp/__init__.py +6 -0
- openinterp/agents/__init__.py +25 -0
- openinterp/agents/base.py +229 -0
- openinterp/agents/judge.py +149 -0
- openinterp/agents/proposer.py +471 -0
- openinterp/agents/proposer_profiles.py +79 -0
- openinterp/agents/review_measurements.py +173 -0
- openinterp/agents/reviewer.py +498 -0
- openinterp/agreement.py +63 -0
- openinterp/audit_options.py +79 -0
- openinterp/baseline.py +254 -0
- openinterp/benchmark.py +450 -0
- openinterp/blends.py +239 -0
- openinterp/builder/__init__.py +6 -0
- openinterp/builder/agents.py +260 -0
- openinterp/builder/aligned.py +41 -0
- openinterp/builder/charts.py +411 -0
- openinterp/builder/circuit_pages.py +37 -0
- openinterp/builder/circuits.py +264 -0
- openinterp/builder/compiler.py +495 -0
- openinterp/builder/corpora.py +98 -0
- openinterp/builder/coverage.py +46 -0
- openinterp/builder/dashboard.py +255 -0
- openinterp/builder/disk_memo.py +65 -0
- openinterp/builder/experiment.py +105 -0
- openinterp/builder/heat.py +121 -0
- openinterp/builder/hrefs.py +71 -0
- openinterp/builder/learned_baseline.py +204 -0
- openinterp/builder/model_facts.py +138 -0
- openinterp/builder/neuronpedia_export.py +107 -0
- openinterp/builder/parallel.py +100 -0
- openinterp/builder/parquet_export.py +458 -0
- openinterp/builder/pieces.py +127 -0
- openinterp/builder/pii.py +211 -0
- openinterp/builder/registry_stats.py +144 -0
- openinterp/builder/research.py +109 -0
- openinterp/builder/sections.py +28 -0
- openinterp/builder/significance.py +103 -0
- openinterp/builder/sister_graph.py +180 -0
- openinterp/builder/standing.py +185 -0
- openinterp/builder/statuses.py +34 -0
- openinterp/builder/stopped.py +91 -0
- openinterp/builder/stops.py +30 -0
- openinterp/builder/superpositions.py +102 -0
- openinterp/builder/templates/_agent_role_cells.html +9 -0
- openinterp/builder/templates/_causal_chart.html +67 -0
- openinterp/builder/templates/_circuit.html +8 -0
- openinterp/builder/templates/_dashboard.html +133 -0
- openinterp/builder/templates/_experiment.html +500 -0
- openinterp/builder/templates/_macros.html +82 -0
- openinterp/builder/templates/_patchscope.html +33 -0
- openinterp/builder/templates/_prior_art.html +24 -0
- openinterp/builder/templates/_specificity.html +21 -0
- openinterp/builder/templates/_steering.html +34 -0
- openinterp/builder/templates/_superposition.html +62 -0
- openinterp/builder/templates/_vocab_chart.html +88 -0
- openinterp/builder/templates/_wiring.html +44 -0
- openinterp/builder/templates/about.html +95 -0
- openinterp/builder/templates/agent.html +69 -0
- openinterp/builder/templates/agents.html +80 -0
- openinterp/builder/templates/base.html +103 -0
- openinterp/builder/templates/circuit.html +64 -0
- openinterp/builder/templates/circuits.html +46 -0
- openinterp/builder/templates/experiment.html +21 -0
- openinterp/builder/templates/hub.html +16 -0
- openinterp/builder/templates/index.html +387 -0
- openinterp/builder/templates/layer.html +69 -0
- openinterp/builder/templates/learned-baseline.html +44 -0
- openinterp/builder/templates/model.html +151 -0
- openinterp/builder/templates/models.html +47 -0
- openinterp/builder/templates/neuron.html +234 -0
- openinterp/builder/templates/paper.html +39 -0
- openinterp/builder/templates/redirect.html +1 -0
- openinterp/builder/templates/research.html +31 -0
- openinterp/builder/templates/user.html +54 -0
- openinterp/builder/templates/users.html +39 -0
- openinterp/builder/templates/wiki/experiment.md +271 -0
- openinterp/builder/templates/wiki/index.md +32 -0
- openinterp/builder/templates/wiki/neuron.md +30 -0
- openinterp/builder/templates/wiki/stop.md +36 -0
- openinterp/builder/tree.py +470 -0
- openinterp/builder/users.py +142 -0
- openinterp/builder/wiki.py +427 -0
- openinterp/checks.py +429 -0
- openinterp/cli.py +494 -0
- openinterp/cli_baseline.py +332 -0
- openinterp/cli_benchmark.py +300 -0
- openinterp/cli_build.py +128 -0
- openinterp/cli_check.py +51 -0
- openinterp/cli_concepts.py +76 -0
- openinterp/cli_corpus.py +252 -0
- openinterp/cli_costs.py +257 -0
- openinterp/cli_coverage.py +76 -0
- openinterp/cli_crossref.py +86 -0
- openinterp/cli_errata.py +142 -0
- openinterp/cli_evidence.py +495 -0
- openinterp/cli_evidence_store.py +93 -0
- openinterp/cli_explain.py +101 -0
- openinterp/cli_loop.py +125 -0
- openinterp/cli_memory.py +28 -0
- openinterp/cli_neuronpedia.py +175 -0
- openinterp/cli_pause.py +45 -0
- openinterp/cli_pole_effects.py +133 -0
- openinterp/cli_probing.py +119 -0
- openinterp/cli_registry.py +348 -0
- openinterp/cli_reproduce.py +446 -0
- openinterp/cli_review.py +302 -0
- openinterp/cli_scan.py +223 -0
- openinterp/cli_score.py +446 -0
- openinterp/cli_sisters.py +86 -0
- openinterp/cli_submission.py +217 -0
- openinterp/cli_superpositions.py +118 -0
- openinterp/cli_wiring.py +54 -0
- openinterp/codeid.py +73 -0
- openinterp/concepts.py +400 -0
- openinterp/core/__init__.py +59 -0
- openinterp/core/agent_names.py +76 -0
- openinterp/core/attribution.py +101 -0
- openinterp/core/cache_dirs.py +15 -0
- openinterp/core/causal.py +82 -0
- openinterp/core/confounds.py +103 -0
- openinterp/core/contributors.py +26 -0
- openinterp/core/corpora.py +126 -0
- openinterp/core/costs.py +430 -0
- openinterp/core/draws.py +109 -0
- openinterp/core/errata.py +244 -0
- openinterp/core/evidence.py +160 -0
- openinterp/core/examples.py +93 -0
- openinterp/core/exceptions.py +29 -0
- openinterp/core/explanation.py +130 -0
- openinterp/core/fence.py +42 -0
- openinterp/core/heavy_lock.py +114 -0
- openinterp/core/identifiers.py +198 -0
- openinterp/core/isolation.py +149 -0
- openinterp/core/ledger.py +130 -0
- openinterp/core/licenses.py +28 -0
- openinterp/core/manifold.py +117 -0
- openinterp/core/partners.py +32 -0
- openinterp/core/patchscope.py +59 -0
- openinterp/core/paths.py +40 -0
- openinterp/core/pause.py +96 -0
- openinterp/core/pricing.py +178 -0
- openinterp/core/prior_art.py +44 -0
- openinterp/core/probing.py +146 -0
- openinterp/core/profiles.py +221 -0
- openinterp/core/project.py +28 -0
- openinterp/core/run_environment.py +38 -0
- openinterp/core/schemas.py +484 -0
- openinterp/core/specificity.py +49 -0
- openinterp/core/stats.py +172 -0
- openinterp/core/steering.py +49 -0
- openinterp/core/strict.py +19 -0
- openinterp/core/takedowns.py +136 -0
- openinterp/core/transcripts.py +130 -0
- openinterp/core/wiring.py +80 -0
- openinterp/corpus.py +476 -0
- openinterp/corpus_cache.py +145 -0
- openinterp/corpus_layouts.py +108 -0
- openinterp/corpus_presets.py +168 -0
- openinterp/corpus_rebuild.py +93 -0
- openinterp/coverage.py +155 -0
- openinterp/crossref.py +442 -0
- openinterp/data/__init__.py +9 -0
- openinterp/data/offensive_words.txt +58 -0
- openinterp/data/public_suffix_list.dat +15771 -0
- openinterp/data/sample_corpus.txt +40 -0
- openinterp/draw_steps.py +359 -0
- openinterp/errata_checks.py +129 -0
- openinterp/evidence.py +462 -0
- openinterp/evidence_branch.py +119 -0
- openinterp/evidence_lenses.py +75 -0
- openinterp/experiment_loop.py +357 -0
- openinterp/explain.py +252 -0
- openinterp/half_precision.py +79 -0
- openinterp/harness/__init__.py +37 -0
- openinterp/harness/ane/__init__.py +9 -0
- openinterp/harness/ane/backend.py +324 -0
- openinterp/harness/ane/cache.py +276 -0
- openinterp/harness/ane/chunks.py +163 -0
- openinterp/harness/ane/convert.py +116 -0
- openinterp/harness/ane/forward.py +258 -0
- openinterp/harness/ane/qualified.py +40 -0
- openinterp/harness/annotations.py +329 -0
- openinterp/harness/attribution.py +96 -0
- openinterp/harness/causal.py +264 -0
- openinterp/harness/confounds.py +184 -0
- openinterp/harness/covariance.py +301 -0
- openinterp/harness/crossref.py +195 -0
- openinterp/harness/cuda_devices.py +88 -0
- openinterp/harness/dead.py +124 -0
- openinterp/harness/devices.py +401 -0
- openinterp/harness/dictionaries.py +496 -0
- openinterp/harness/dreaming.py +116 -0
- openinterp/harness/environment.py +64 -0
- openinterp/harness/explanation_scores.py +371 -0
- openinterp/harness/features.py +276 -0
- openinterp/harness/first_position.py +137 -0
- openinterp/harness/heads.py +226 -0
- openinterp/harness/hooks.py +499 -0
- openinterp/harness/input_attribution.py +367 -0
- openinterp/harness/jlens.py +183 -0
- openinterp/harness/layer_cache.py +301 -0
- openinterp/harness/layer_major.py +274 -0
- openinterp/harness/lookahead.py +93 -0
- openinterp/harness/manifold.py +200 -0
- openinterp/harness/memory.py +471 -0
- openinterp/harness/moe.py +134 -0
- openinterp/harness/patcher.py +322 -0
- openinterp/harness/patchscope.py +177 -0
- openinterp/harness/probing.py +383 -0
- openinterp/harness/scan.py +462 -0
- openinterp/harness/scan_backend.py +201 -0
- openinterp/harness/sister_passes.py +463 -0
- openinterp/harness/sisters.py +363 -0
- openinterp/harness/sites.py +101 -0
- openinterp/harness/specificity.py +107 -0
- openinterp/harness/steering.py +159 -0
- openinterp/harness/superpositions.py +116 -0
- openinterp/harness/units.py +203 -0
- openinterp/harness/wiring.py +432 -0
- openinterp/harness/wiring_activations.py +285 -0
- openinterp/langid.py +73 -0
- openinterp/names.py +129 -0
- openinterp/neuronpedia_baseline.py +88 -0
- openinterp/neuronpedia_review.py +148 -0
- openinterp/planner.py +466 -0
- openinterp/planner_effects.py +89 -0
- openinterp/planner_heads.py +92 -0
- openinterp/planner_models.py +174 -0
- openinterp/planner_sisters.py +122 -0
- openinterp/planner_states.py +57 -0
- openinterp/planner_superpositions.py +114 -0
- openinterp/pole_effects.py +102 -0
- openinterp/prior_art.py +143 -0
- openinterp/reproduce.py +450 -0
- openinterp/revision.py +45 -0
- openinterp/runner_cache.py +35 -0
- openinterp/score_controls.py +169 -0
- openinterp/sensitive.py +243 -0
- openinterp/show.py +236 -0
- openinterp/signing.py +209 -0
- openinterp/sisters.py +476 -0
- openinterp/superposition_finders.py +225 -0
- openinterp/sync/__init__.py +5 -0
- openinterp/sync/r2.py +280 -0
- openinterp/syntax.py +399 -0
- openinterp/takedown_checks.py +76 -0
- openinterp/toxicity.py +158 -0
- openinterp/verdict_regression.py +79 -0
- openinterp/wiring_backfill.py +170 -0
- openinterp/wordnet.py +362 -0
openinterp/agreement.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Subject–verb agreement prompts: the templates, nouns and pairs (issues #3225 and #3686).
|
|
2
|
+
|
|
3
|
+
``scripts/sva_circuit.py`` traces a circuit on these prompts. The ``grammatical-number``
|
|
4
|
+
finder in :mod:`openinterp.superposition_finders` probes them. Each prompt ends where
|
|
5
|
+
the verb comes next.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import random
|
|
11
|
+
|
|
12
|
+
# Nouns whose singular and plural are one token each in Qwen2.5's tokenizer.
|
|
13
|
+
NOUNS = [
|
|
14
|
+
*("author", "pilot", "surgeon", "farmer", "manager", "customer", "officer", "teacher"),
|
|
15
|
+
*("senator", "guard", "doctor", "student", "dancer", "lawyer", "writer", "painter"),
|
|
16
|
+
*("driver", "player", "worker", "singer", "judge", "nurse", "minister", "boy", "girl"),
|
|
17
|
+
*("king", "friend", "parent", "dog", "cat"),
|
|
18
|
+
]
|
|
19
|
+
IRREGULAR = {"child": "children", "woman": "women", "man": "men"}
|
|
20
|
+
PREPOSITIONS = ("near", "behind", "beside", "with")
|
|
21
|
+
VERBS = ("liked", "saw", "knew", "met")
|
|
22
|
+
TEMPLATES = ("simple", "across_pp", "across_rc", "within_rc")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def plural(noun: str) -> str:
|
|
26
|
+
"""The plural of ``noun``."""
|
|
27
|
+
return IRREGULAR.get(noun, noun + "s")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def prompt(template: str, a: str, b: str, prep: str, verb: str) -> str:
|
|
31
|
+
"""The prompt. ``a`` is the head noun and ``b`` the second noun, both inflected."""
|
|
32
|
+
return {
|
|
33
|
+
"simple": f"The {a}",
|
|
34
|
+
"across_pp": f"The {a} {prep} the {b}",
|
|
35
|
+
"across_rc": f"The {a} that the {b} {verb}",
|
|
36
|
+
"within_rc": f"The {a} that the {b}",
|
|
37
|
+
}[template]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def pairs(template: str, n: int, rng: random.Random) -> list[dict]:
|
|
41
|
+
"""``n`` (clean, corrupted, sign) pairs. Half of them have a singular clean noun.
|
|
42
|
+
|
|
43
|
+
The verb agrees with the head noun, except within a relative clause, where it agrees
|
|
44
|
+
with the clause's subject. Only that noun's number differs between the two prompts.
|
|
45
|
+
The other noun's number is drawn at random, and is the same in both.
|
|
46
|
+
"""
|
|
47
|
+
nouns = [*NOUNS, *IRREGULAR]
|
|
48
|
+
out = []
|
|
49
|
+
for i in range(n):
|
|
50
|
+
a, b = rng.sample(nouns, 2)
|
|
51
|
+
prep, verb = rng.choice(PREPOSITIONS), rng.choice(VERBS)
|
|
52
|
+
singular = i % 2 == 0
|
|
53
|
+
agree, other = (b, a) if template == "within_rc" else (a, b)
|
|
54
|
+
other = other if rng.random() < 0.5 else plural(other)
|
|
55
|
+
clean, corrupted = (agree, plural(agree)) if singular else (plural(agree), agree)
|
|
56
|
+
|
|
57
|
+
def text(form, other=other, prep=prep, verb=verb):
|
|
58
|
+
head, second = (other, form) if template == "within_rc" else (form, other)
|
|
59
|
+
return prompt(template, head, second, prep, verb)
|
|
60
|
+
|
|
61
|
+
out.append({"clean": text(clean), "corrupted": text(corrupted),
|
|
62
|
+
"sign": 1.0 if singular else -1.0}) # fmt: skip
|
|
63
|
+
return out
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Options that ``open-interpretability audit`` and ``scan`` share, and how unit options resolve.
|
|
2
|
+
|
|
3
|
+
The shared ones are the scan cache and ``--selection``.
|
|
4
|
+
|
|
5
|
+
``--selection`` says how a chosen unit was selected, as its provenance records it.
|
|
6
|
+
|
|
7
|
+
A ``--random`` draw records ``random`` and counts towards the registry statistics. A
|
|
8
|
+
unit given by layer and index records ``manual``, unless ``--selection`` says otherwise.
|
|
9
|
+
The planner's effect-weighted explore draws (issue #3486) pass ``attribution``. They are
|
|
10
|
+
drawn in proportion to an attribution-patching estimate, so they must not count as random.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import click
|
|
19
|
+
|
|
20
|
+
scan_cache_option = click.option(
|
|
21
|
+
"--scan-cache/--no-scan-cache",
|
|
22
|
+
"use_scan_cache",
|
|
23
|
+
default=True,
|
|
24
|
+
show_default=True,
|
|
25
|
+
help="Replay a cached scan of the layer, or store this one "
|
|
26
|
+
"(see `open-interpretability scan-cache`).",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def open_scan_cache(enabled: bool):
|
|
31
|
+
"""The default layer-scan cache, or ``None`` when ``enabled`` is false."""
|
|
32
|
+
from openinterp.harness.layer_cache import LayerCache
|
|
33
|
+
|
|
34
|
+
return LayerCache.default() if enabled else None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
CHOSEN = ("manual", "attribution") # selections a unit given by layer and index may record
|
|
38
|
+
|
|
39
|
+
selection_option = click.option(
|
|
40
|
+
"--selection",
|
|
41
|
+
type=click.Choice(CHOSEN),
|
|
42
|
+
help="How a unit given by layer and index was chosen (default manual). The planner's "
|
|
43
|
+
"effect-weighted draws pass attribution. Ignored with --random.",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def selection(random_pick: bool, chosen: str | None) -> str:
|
|
48
|
+
"""The ``provenance.selection`` of a scan."""
|
|
49
|
+
return "random" if random_pick else chosen or "manual"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def read_superposition(path: Path, unit: str):
|
|
53
|
+
"""(unit, model, layer, profile) from a superposition's definition file."""
|
|
54
|
+
from pydantic import ValidationError
|
|
55
|
+
|
|
56
|
+
from openinterp.core.profiles import SuperpositionProfile
|
|
57
|
+
|
|
58
|
+
if unit != "neuron":
|
|
59
|
+
raise click.UsageError("--superposition is a unit of its own; don't combine it.")
|
|
60
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
61
|
+
try:
|
|
62
|
+
model, layer = data.pop("model"), data.pop("layer")
|
|
63
|
+
return "superposition", model, layer, SuperpositionProfile.model_validate(data)
|
|
64
|
+
except (KeyError, ValidationError) as exc:
|
|
65
|
+
raise click.BadParameter(str(exc), param_hint="--superposition") from exc
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def resolve_unit(unit, neuron, pole, head, feature, dictionary) -> str:
|
|
69
|
+
"""The unit ``--unit`` names, or the one the other options imply."""
|
|
70
|
+
if unit is None:
|
|
71
|
+
unit = "head" if head is not None else "neuron"
|
|
72
|
+
unit = "feature" if feature is not None or dictionary is not None else unit
|
|
73
|
+
if unit != "neuron" and (neuron is not None or pole is not None):
|
|
74
|
+
raise click.UsageError(f"--neuron and --pole are for neurons; a {unit} has neither.")
|
|
75
|
+
if head is not None and unit != "head":
|
|
76
|
+
raise click.UsageError("--head needs --unit head (or no --unit).")
|
|
77
|
+
if unit != "feature" and (feature is not None or dictionary is not None):
|
|
78
|
+
raise click.UsageError("--feature and --dictionary need --unit feature (or no --unit).")
|
|
79
|
+
return unit
|
openinterp/baseline.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""The Patchscope readout as a blind proposer baseline.
|
|
2
|
+
|
|
3
|
+
The command is `open-interpretability patchscope-baseline`.
|
|
4
|
+
|
|
5
|
+
Does the model's own, example-free description of a unit's output direction explain the
|
|
6
|
+
unit as well as the agent's hypothesis? One reading per unit is turned into an
|
|
7
|
+
explanation by a rule fixed before any baseline was scored:
|
|
8
|
+
|
|
9
|
+
* the ``category`` prompt, at the lowest layer read (layer 2, or the unit's own if
|
|
10
|
+
earlier), at ``BASELINE_SCALE``;
|
|
11
|
+
* wrapped as ``TEMPLATE``.
|
|
12
|
+
|
|
13
|
+
The baseline is then scored on the same held-out windows as the agent's explanation.
|
|
14
|
+
The record stores those windows with their labels, so the two scores are paired and
|
|
15
|
+
nothing is measured again. Detection and fuzzing need a judge. A pair counts only when
|
|
16
|
+
the same judge model answered both. The embedding score needs none. The units looked at while
|
|
17
|
+
choosing the prompts and the rule (``DEV_SLUGS``) are reported apart.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import math
|
|
23
|
+
from collections.abc import Sequence
|
|
24
|
+
from statistics import mean
|
|
25
|
+
|
|
26
|
+
from pydantic import BaseModel
|
|
27
|
+
|
|
28
|
+
from openinterp.core.explanation import ExplanationScores
|
|
29
|
+
from openinterp.core.patchscope import PatchscopeReading, PatchscopeReadout
|
|
30
|
+
|
|
31
|
+
BASELINE_PROMPT = "category"
|
|
32
|
+
BASELINE_SCALE = 2.0
|
|
33
|
+
TEMPLATE = 'Fires on tokens related to "{}".'
|
|
34
|
+
# Poles whose readouts were inspected while choosing the prompts, layers and scale.
|
|
35
|
+
DEV_SLUGS = frozenset(
|
|
36
|
+
f"Qwen--Qwen2.5-0.5B/{unit}"
|
|
37
|
+
for unit in (
|
|
38
|
+
"L0/N788-positive",
|
|
39
|
+
"L3/N3738-negative",
|
|
40
|
+
"L5/N2106-positive",
|
|
41
|
+
"L5/N2131-negative",
|
|
42
|
+
"L5/N4579-negative",
|
|
43
|
+
"L14/N2611-positive",
|
|
44
|
+
"L17/N4749-negative",
|
|
45
|
+
"L22/N1058-negative",
|
|
46
|
+
"L22/N2686-positive",
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
METRICS = ("detection", "fuzzing", "embedding")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def blind_reading(readout: PatchscopeReadout) -> PatchscopeReading | None:
|
|
53
|
+
"""The one reading the baseline uses, by the fixed rule; ``None`` if it wasn't read."""
|
|
54
|
+
readings = [r for r in readout.readings if r.prompt == BASELINE_PROMPT]
|
|
55
|
+
if not readings:
|
|
56
|
+
return None
|
|
57
|
+
lowest = min(r.layer for r in readings)
|
|
58
|
+
return next((r for r in readings if r.layer == lowest and r.scale == BASELINE_SCALE), None)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def blind_explanation(readout: PatchscopeReadout) -> str | None:
|
|
62
|
+
"""The baseline explanation for ``readout``: its blind reading in ``TEMPLATE``, or ``None``."""
|
|
63
|
+
reading = blind_reading(readout)
|
|
64
|
+
return TEMPLATE.format(reading.text) if reading and reading.text else None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def rescored(
|
|
68
|
+
scores: ExplanationScores,
|
|
69
|
+
explanation: str,
|
|
70
|
+
*,
|
|
71
|
+
judge_model: str | None = None,
|
|
72
|
+
detection: Sequence[bool] | None = None,
|
|
73
|
+
fuzzing: Sequence[bool] | None = None,
|
|
74
|
+
similarities: Sequence[float] | None = None,
|
|
75
|
+
) -> ExplanationScores:
|
|
76
|
+
"""Score another explanation on the windows and labels of ``scores``.
|
|
77
|
+
|
|
78
|
+
The result is what that explanation scores on the same held-out text. Answers not
|
|
79
|
+
given are left unjudged.
|
|
80
|
+
"""
|
|
81
|
+
from openinterp.harness.explanation_scores import finalize_scores
|
|
82
|
+
|
|
83
|
+
def swap(windows, answers, sims=None):
|
|
84
|
+
return [
|
|
85
|
+
w.model_copy(
|
|
86
|
+
update={
|
|
87
|
+
"judged": None if answers is None else answers[i],
|
|
88
|
+
"similarity": None if sims is None else sims[i],
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
for i, w in enumerate(windows)
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
if detection is not None and len(detection) != len(scores.detection_windows):
|
|
95
|
+
raise ValueError("one detection answer per window")
|
|
96
|
+
if fuzzing is not None and len(fuzzing) != len(scores.fuzzing_windows):
|
|
97
|
+
raise ValueError("one fuzzing answer per window")
|
|
98
|
+
return finalize_scores(
|
|
99
|
+
scores.model_copy(
|
|
100
|
+
update={
|
|
101
|
+
"explanation": explanation,
|
|
102
|
+
"judge_model": judge_model,
|
|
103
|
+
"detection_windows": swap(scores.detection_windows, detection, similarities),
|
|
104
|
+
"fuzzing_windows": swap(scores.fuzzing_windows, fuzzing),
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def metric(scores: ExplanationScores | None, name: str) -> float | None:
|
|
111
|
+
"""One metric of ``scores`` (a judge's balanced accuracy, or embedding AUROC), or ``None``."""
|
|
112
|
+
if scores is None:
|
|
113
|
+
return None
|
|
114
|
+
if name == "embedding":
|
|
115
|
+
return scores.embedding_auroc
|
|
116
|
+
judged = getattr(scores, name)
|
|
117
|
+
return None if judged is None else judged.balanced_accuracy
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class BaselineRow(BaseModel):
|
|
121
|
+
"""One experiment's agent explanation against the baseline, on the same windows."""
|
|
122
|
+
|
|
123
|
+
experiment_id: str
|
|
124
|
+
slug: str
|
|
125
|
+
status: str
|
|
126
|
+
selection: str | None
|
|
127
|
+
dev: bool
|
|
128
|
+
reading: str
|
|
129
|
+
agent_explanation: str
|
|
130
|
+
baseline_explanation: str | None
|
|
131
|
+
judge_model: str | None
|
|
132
|
+
baseline_judge_model: str | None = None
|
|
133
|
+
detection_answer: list[int] | None = None
|
|
134
|
+
fuzzing_answer: list[int] | None = None
|
|
135
|
+
agent: dict[str, float | None]
|
|
136
|
+
baseline: dict[str, float | None]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def sign_test(differences: Sequence[float]) -> float | None:
|
|
140
|
+
"""Two-sided exact sign test that baseline − agent is as often > 0 as < 0 (ties out)."""
|
|
141
|
+
up = sum(d > 0 for d in differences)
|
|
142
|
+
n = up + sum(d < 0 for d in differences)
|
|
143
|
+
if not n:
|
|
144
|
+
return None
|
|
145
|
+
tail = sum(math.comb(n, k) for k in range(min(up, n - up) + 1)) / 2**n
|
|
146
|
+
return min(1.0, 2 * tail)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def summarise(rows: Sequence[BaselineRow], name: str) -> dict | None:
|
|
150
|
+
"""Paired comparison on ``name`` over the rows where both explanations have a score."""
|
|
151
|
+
pairs = [(r.agent[name], r.baseline[name]) for r in rows]
|
|
152
|
+
pairs = [(a, b) for a, b in pairs if a is not None and b is not None]
|
|
153
|
+
if not pairs:
|
|
154
|
+
return None
|
|
155
|
+
diffs = [b - a for a, b in pairs]
|
|
156
|
+
return {
|
|
157
|
+
"n": len(pairs),
|
|
158
|
+
"agent": mean(a for a, _ in pairs),
|
|
159
|
+
"baseline": mean(b for _, b in pairs),
|
|
160
|
+
"difference": mean(diffs),
|
|
161
|
+
"baseline_at_least_agent": sum(d >= 0 for d in diffs) / len(diffs),
|
|
162
|
+
"sign_test_p": sign_test(diffs),
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def report(rows: Sequence[BaselineRow]) -> str:
|
|
167
|
+
"""Return the comparison as a Markdown page.
|
|
168
|
+
|
|
169
|
+
The page gives the rule and the readings. It also gives every metric over held-out
|
|
170
|
+
experiments, over randomly selected and VERIFIED units, and over the dev set.
|
|
171
|
+
"""
|
|
172
|
+
from collections import Counter
|
|
173
|
+
|
|
174
|
+
readings = Counter(r.reading for r in rows)
|
|
175
|
+
judged = sum(r.baseline_judge_model is not None for r in rows)
|
|
176
|
+
intro = (
|
|
177
|
+
f"Each unit's readout becomes one explanation by a rule fixed before scoring: the "
|
|
178
|
+
f"`{BASELINE_PROMPT}` prompt at the lowest layer read, at ×{BASELINE_SCALE:g} the "
|
|
179
|
+
f"replaced norm, as `{TEMPLATE.format('<reading>')}`. It is scored on the same "
|
|
180
|
+
f"held-out windows as the agent's explanation. Detection and fuzzing pair only "
|
|
181
|
+
f"experiments whose agent explanation was judged by the same model as the baseline's "
|
|
182
|
+
f"({judged} of {len(rows)}). Embedding uses the record's own encoder. The sign test "
|
|
183
|
+
f"counts experiments, and a unit with several experiments counts more than once."
|
|
184
|
+
)
|
|
185
|
+
common = ", ".join(f"{text!r} ×{n}" for text, n in readings.most_common(10))
|
|
186
|
+
after = [
|
|
187
|
+
f"{len(readings)} distinct readings over {len(rows)} experiments. The most common: "
|
|
188
|
+
f"{common}."
|
|
189
|
+
]
|
|
190
|
+
return paired_report(
|
|
191
|
+
rows,
|
|
192
|
+
title="Patchscope readout as a blind proposer baseline",
|
|
193
|
+
generated_by="`open-interpretability patchscope-baseline`",
|
|
194
|
+
method="`openinterp/baseline.py` and docs/PROTOCOL.md §4",
|
|
195
|
+
intro=intro,
|
|
196
|
+
after=after,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def paired_report(
|
|
201
|
+
rows: Sequence[BaselineRow],
|
|
202
|
+
*,
|
|
203
|
+
title: str,
|
|
204
|
+
generated_by: str,
|
|
205
|
+
method: str,
|
|
206
|
+
intro: str,
|
|
207
|
+
after: Sequence[str] = (),
|
|
208
|
+
) -> str:
|
|
209
|
+
"""Return a Markdown page: ``intro``, the paired table of every metric, then ``after``.
|
|
210
|
+
|
|
211
|
+
The table compares the agent and the baseline over held-out experiments, over
|
|
212
|
+
randomly selected and VERIFIED units, and over the dev set. Groups and metrics with
|
|
213
|
+
no pairs are left out.
|
|
214
|
+
"""
|
|
215
|
+
lines = [
|
|
216
|
+
f"# {title}",
|
|
217
|
+
"",
|
|
218
|
+
f"Generated by {generated_by}. Do not edit. Method: {method}.",
|
|
219
|
+
"",
|
|
220
|
+
intro,
|
|
221
|
+
"",
|
|
222
|
+
"| Group | Metric | n | Agent | Baseline | Baseline − agent | Baseline ≥ agent "
|
|
223
|
+
"| Sign test p |",
|
|
224
|
+
"| --- | --- | --- | --- | --- | --- | --- | --- |",
|
|
225
|
+
]
|
|
226
|
+
lines += [_metric_row(label, g, name) for label, g in _groups(rows) for name in METRICS]
|
|
227
|
+
lines = [line for line in lines if line is not None]
|
|
228
|
+
if after:
|
|
229
|
+
lines += ["", *after]
|
|
230
|
+
return "\n".join(lines) + "\n"
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _groups(rows: Sequence[BaselineRow]) -> list[tuple[str, list[BaselineRow]]]:
|
|
234
|
+
return [
|
|
235
|
+
("Held-out experiments", [r for r in rows if not r.dev]),
|
|
236
|
+
(
|
|
237
|
+
"Held-out, randomly selected units",
|
|
238
|
+
[r for r in rows if not r.dev and r.selection == "random"],
|
|
239
|
+
),
|
|
240
|
+
("Held-out, VERIFIED", [r for r in rows if not r.dev and r.status == "VERIFIED"]),
|
|
241
|
+
("Dev set (inspected while choosing the rule)", [r for r in rows if r.dev]),
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _metric_row(label: str, group: Sequence[BaselineRow], name: str) -> str | None:
|
|
246
|
+
"""One table row comparing the agent and the baseline on ``name``; ``None`` if unpaired."""
|
|
247
|
+
s = summarise(group, name)
|
|
248
|
+
if s is None:
|
|
249
|
+
return None
|
|
250
|
+
p = "—" if s["sign_test_p"] is None else f"{s['sign_test_p']:.2g}"
|
|
251
|
+
return (
|
|
252
|
+
f"| {label} | {name} | {s['n']} | {s['agent']:.2f} | {s['baseline']:.2f} | "
|
|
253
|
+
f"{s['difference']:+.2f} | {s['baseline_at_least_agent']:.0%} | {p} |"
|
|
254
|
+
)
|