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/crossref.py
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
"""Cross-reference neuron poles with published dictionaries (`open-interpretability crossref`).
|
|
2
|
+
|
|
3
|
+
This takes every neuron pole in the archive whose model has a dictionary preset for its
|
|
4
|
+
layer. It finds the latents whose decoder aligns with the pole's write, and whose encoder
|
|
5
|
+
aligns with its read (:mod:`openinterp.harness.crossref`). Rows go to
|
|
6
|
+
``docs/analyses/dictionary-alignment.jsonl``, one per pole and dictionary.
|
|
7
|
+
|
|
8
|
+
A row names only the dictionary and each latent's index. A latent's description is with
|
|
9
|
+
the dictionary's host, or in this registry if the latent was audited here. The host is
|
|
10
|
+
Neuronpedia, where :attr:`DictionarySpec.neuronpedia` says it has them. The description
|
|
11
|
+
is looked up when shown, and kept on disk so that each latent is fetched only once
|
|
12
|
+
(``$OPENINTERP_NEURONPEDIA_CACHE``, ``off`` to turn it off). A description from
|
|
13
|
+
Neuronpedia is credited to it and to the model that wrote it.
|
|
14
|
+
|
|
15
|
+
Agreement between a pole's explanation and an aligned latent's is supporting evidence.
|
|
16
|
+
Disagreement is something for the reviewer to probe (`open-interpretability review
|
|
17
|
+
--dictionary-latents`). Neither is gated.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import time
|
|
25
|
+
import urllib.error
|
|
26
|
+
import urllib.request
|
|
27
|
+
from collections import Counter, defaultdict
|
|
28
|
+
from collections.abc import Callable, Iterable
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
from openinterp import DISTRIBUTION
|
|
32
|
+
from openinterp.core.cache_dirs import cache_root
|
|
33
|
+
from openinterp.core.fence import fence
|
|
34
|
+
from openinterp.core.schemas import AuditRecord, NeuronIdentifier
|
|
35
|
+
from openinterp.core.strict import StrictModel
|
|
36
|
+
|
|
37
|
+
DEFAULT_PATH = Path("docs/analyses/dictionary-alignment.jsonl")
|
|
38
|
+
NEURONPEDIA = "https://www.neuronpedia.org"
|
|
39
|
+
MAX_WAIT = 60.0 # seconds: the longest wait before asking Neuronpedia again
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class LatentMatch(StrictModel):
|
|
43
|
+
"""One latent of a dictionary, and how well it aligns with the pole."""
|
|
44
|
+
|
|
45
|
+
feature_idx: int
|
|
46
|
+
cosine: float
|
|
47
|
+
channel: str | None = None # the neuron's read channel (gate, up, in), for a read
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AlignmentSide(StrictModel):
|
|
51
|
+
"""One side's best matches, by |cosine|.
|
|
52
|
+
|
|
53
|
+
``chance`` is what a random direction reaches. Also the share of the layer's neurons
|
|
54
|
+
whose best match is weaker.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
matches: list[LatentMatch]
|
|
58
|
+
chance: float
|
|
59
|
+
percentile: float
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class AlignmentRow(StrictModel):
|
|
63
|
+
"""One pole against one dictionary.
|
|
64
|
+
|
|
65
|
+
``writes`` compares each latent's decoder row with the pole's write. + means that the
|
|
66
|
+
latent writes what the pole writes. ``reads`` compares each latent's encoder with the
|
|
67
|
+
neuron's reads. + means that the latent reads what raises the neuron's
|
|
68
|
+
pre-activation. ``reads`` is ``None`` for a dictionary that doesn't read the MLP's
|
|
69
|
+
input.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
slug: str
|
|
73
|
+
dictionary: str
|
|
74
|
+
repo: str
|
|
75
|
+
revision: str
|
|
76
|
+
path: str
|
|
77
|
+
model_revision: str | None
|
|
78
|
+
writes: AlignmentSide
|
|
79
|
+
reads: AlignmentSide | None = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def load_rows(path: Path | None = DEFAULT_PATH) -> dict[str, list[AlignmentRow]]:
|
|
83
|
+
"""The rows in ``path`` by pole slug; empty if it doesn't exist."""
|
|
84
|
+
out: dict[str, list[AlignmentRow]] = defaultdict(list)
|
|
85
|
+
if path is None or not Path(path).exists():
|
|
86
|
+
return out
|
|
87
|
+
for line in Path(path).read_text().splitlines():
|
|
88
|
+
if line.strip():
|
|
89
|
+
row = AlignmentRow.model_validate_json(line)
|
|
90
|
+
out[row.slug].append(row)
|
|
91
|
+
return out
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def dictionaries_for(model_name: str, layer: int) -> list[str]:
|
|
95
|
+
"""The dictionary presets that cover ``layer`` of ``model_name``, sorted by name."""
|
|
96
|
+
from openinterp.harness.dictionaries import DICTIONARIES
|
|
97
|
+
|
|
98
|
+
return sorted(
|
|
99
|
+
name
|
|
100
|
+
for name, spec in DICTIONARIES.items()
|
|
101
|
+
if spec.model_name == model_name and layer in spec.layers
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _poles(records: Iterable[AuditRecord]) -> dict[str, tuple[NeuronIdentifier, Counter]]:
|
|
106
|
+
"""Each neuron pole in ``records``, with how often each model revision pinned it."""
|
|
107
|
+
poles: dict[str, tuple[NeuronIdentifier, Counter]] = {}
|
|
108
|
+
for record in records:
|
|
109
|
+
ident = record.submission.identifier
|
|
110
|
+
if not isinstance(ident, NeuronIdentifier):
|
|
111
|
+
continue
|
|
112
|
+
prov = record.submission.provenance
|
|
113
|
+
_, revisions = poles.setdefault(ident.slug, (ident, Counter()))
|
|
114
|
+
revisions[prov.model_revision if prov else None] += 1
|
|
115
|
+
return poles
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _todo(records: Iterable[AuditRecord], out: Path):
|
|
119
|
+
"""Return ``({(model, dictionary, layer): poles}, {model: revision})`` still to do.
|
|
120
|
+
|
|
121
|
+
These are the pairs that ``out`` has no row for. A model is read at the revision that
|
|
122
|
+
its records pin most often.
|
|
123
|
+
"""
|
|
124
|
+
done = {(r.slug, r.dictionary) for rows in load_rows(out).values() for r in rows}
|
|
125
|
+
todo: dict[tuple[str, str, int], list[NeuronIdentifier]] = defaultdict(list)
|
|
126
|
+
revisions: dict[str, Counter] = defaultdict(Counter)
|
|
127
|
+
for slug, (ident, seen) in _poles(records).items():
|
|
128
|
+
revisions[ident.model_name].update(seen)
|
|
129
|
+
for name in dictionaries_for(ident.model_name, ident.layer):
|
|
130
|
+
if (slug, name) not in done:
|
|
131
|
+
todo[(ident.model_name, name, ident.layer)].append(ident)
|
|
132
|
+
return todo, {m: _pinned(seen) for m, seen in revisions.items()}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def crossref(
|
|
136
|
+
records: Iterable[AuditRecord],
|
|
137
|
+
load_runner: Callable[[str, str | None], object],
|
|
138
|
+
out: Path,
|
|
139
|
+
*,
|
|
140
|
+
k: int = 5,
|
|
141
|
+
echo: Callable[[str], None] = print,
|
|
142
|
+
) -> int:
|
|
143
|
+
"""Append a row to ``out`` for each pole and covering dictionary without one yet.
|
|
144
|
+
|
|
145
|
+
Loads each model once. Returns how many rows it wrote.
|
|
146
|
+
"""
|
|
147
|
+
from openinterp.core.exceptions import ArchitectureNotSupportedError
|
|
148
|
+
from openinterp.harness.crossref import align_layer
|
|
149
|
+
from openinterp.harness.dictionaries import dictionary_spec
|
|
150
|
+
|
|
151
|
+
todo, revisions = _todo(records, out)
|
|
152
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
wrote, runner, loaded = 0, None, None
|
|
154
|
+
for model_name, name, layer in sorted(todo):
|
|
155
|
+
if loaded != model_name:
|
|
156
|
+
runner, loaded = load_runner(model_name, revisions[model_name]), model_name
|
|
157
|
+
spec, idents = dictionary_spec(name), todo[(model_name, name, layer)]
|
|
158
|
+
try:
|
|
159
|
+
found = align_layer(runner, spec, layer, [(i.neuron_idx, i.pole) for i in idents], k=k)
|
|
160
|
+
except ArchitectureNotSupportedError as exc:
|
|
161
|
+
echo(f"skip {name} L{layer}: {exc}")
|
|
162
|
+
continue
|
|
163
|
+
with out.open("a") as f:
|
|
164
|
+
for ident in idents:
|
|
165
|
+
write, read = found[(ident.neuron_idx, ident.pole)]
|
|
166
|
+
row = AlignmentRow(
|
|
167
|
+
slug=ident.slug,
|
|
168
|
+
dictionary=name,
|
|
169
|
+
repo=spec.repo,
|
|
170
|
+
revision=spec.revision,
|
|
171
|
+
path=spec.file(layer),
|
|
172
|
+
model_revision=revisions[model_name],
|
|
173
|
+
writes=_side(write),
|
|
174
|
+
reads=_side(read) if read is not None else None,
|
|
175
|
+
)
|
|
176
|
+
f.write(row.model_dump_json() + "\n")
|
|
177
|
+
wrote += len(idents)
|
|
178
|
+
echo(f"{name} L{layer}: {len(idents)} poles")
|
|
179
|
+
return wrote
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _pinned(revisions: Counter) -> str | None:
|
|
183
|
+
"""The revision most records pin; an unpinned record says nothing about which."""
|
|
184
|
+
pinned = Counter({r: n for r, n in revisions.items() if r is not None})
|
|
185
|
+
return pinned.most_common(1)[0][0] if pinned else None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _side(side) -> AlignmentSide:
|
|
189
|
+
return AlignmentSide(
|
|
190
|
+
matches=[
|
|
191
|
+
LatentMatch(feature_idx=m.feature_idx, cosine=round(m.cosine, 4), channel=m.channel)
|
|
192
|
+
for m in side.matches
|
|
193
|
+
],
|
|
194
|
+
chance=round(side.chance, 4),
|
|
195
|
+
percentile=round(side.percentile, 4),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def sort_rows(path: Path) -> None:
|
|
200
|
+
"""Rewrite ``path`` in a stable order, so reruns that append don't churn the diff."""
|
|
201
|
+
rows = sorted(
|
|
202
|
+
(r for rows in load_rows(path).values() for r in rows), key=lambda r: (r.slug, r.dictionary)
|
|
203
|
+
)
|
|
204
|
+
path.write_text("".join(r.model_dump_json() + "\n" for r in rows))
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def layer_of(row: AlignmentRow) -> int:
|
|
208
|
+
"""The layer of the pole that ``row`` is about, read from its slug."""
|
|
209
|
+
return int(row.slug.split("/L")[1].split("/")[0])
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def neuronpedia_url(row: AlignmentRow, feature_idx: int, *, api: bool = False) -> str | None:
|
|
213
|
+
"""The latent's Neuronpedia page (or API endpoint), where Neuronpedia hosts it."""
|
|
214
|
+
return latent_url(row.dictionary, layer_of(row), feature_idx, api=api)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def latent_url(dictionary: str, layer: int, feature_idx: int, *, api: bool = False) -> str | None:
|
|
218
|
+
"""The Neuronpedia page (or API endpoint) of a latent of a dictionary preset.
|
|
219
|
+
|
|
220
|
+
``None`` where Neuronpedia does not host that dictionary at ``layer``.
|
|
221
|
+
"""
|
|
222
|
+
from openinterp.harness.dictionaries import DICTIONARIES
|
|
223
|
+
|
|
224
|
+
spec = DICTIONARIES.get(dictionary)
|
|
225
|
+
source = spec and spec.neuronpedia_source(layer)
|
|
226
|
+
if not source:
|
|
227
|
+
return None
|
|
228
|
+
return f"{NEURONPEDIA}{'/api/feature' if api else ''}/{source}/{feature_idx}"
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class Explanation(StrictModel):
|
|
232
|
+
"""One Neuronpedia explanation of a latent, with who wrote it.
|
|
233
|
+
|
|
234
|
+
``explainer`` is the model that wrote it (e.g. ``gpt-4-turbo``), or ``"human"``.
|
|
235
|
+
The author's account is not kept.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
description: str
|
|
239
|
+
explainer: str
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def user_agent() -> str:
|
|
243
|
+
"""The User-Agent for requests to Neuronpedia: this package, its version and its repo."""
|
|
244
|
+
from importlib.metadata import PackageNotFoundError, metadata
|
|
245
|
+
|
|
246
|
+
from openinterp import __version__
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
urls = metadata(DISTRIBUTION).get_all("Project-URL") or []
|
|
250
|
+
except PackageNotFoundError:
|
|
251
|
+
urls = []
|
|
252
|
+
repo = next((u.split(", ", 1)[1] for u in urls if u.startswith("Repository, ")), None)
|
|
253
|
+
return f"openinterp/{__version__}" + (f" (+{repo})" if repo else "")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _cache_path(url: str) -> Path | None:
|
|
257
|
+
"""Where the explanations fetched from ``url`` are kept; None if the cache is off."""
|
|
258
|
+
root = cache_root("OPENINTERP_NEURONPEDIA_CACHE", "neuronpedia")
|
|
259
|
+
if root is None:
|
|
260
|
+
return None
|
|
261
|
+
return root / f"{url.split('/api/feature/', 1)[1]}.json"
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _get_json(url: str, *, timeout: float, retries: int, sleep: Callable, opener: Callable):
|
|
265
|
+
"""GET ``url`` as JSON. Wait and try again when the server says it is busy (429, 5xx).
|
|
266
|
+
|
|
267
|
+
Return ``None`` if the request fails or the server stays busy.
|
|
268
|
+
"""
|
|
269
|
+
# url is an https Neuronpedia endpoint from latent_url, never a file: URL.
|
|
270
|
+
request = urllib.request.Request(url, headers={"User-Agent": user_agent()}) # noqa: S310
|
|
271
|
+
for attempt in range(retries + 1):
|
|
272
|
+
try:
|
|
273
|
+
with opener(request, timeout=timeout) as response:
|
|
274
|
+
return json.load(response)
|
|
275
|
+
except urllib.error.HTTPError as err:
|
|
276
|
+
busy = err.code == 429 or err.code >= 500
|
|
277
|
+
if not busy or attempt == retries:
|
|
278
|
+
return None
|
|
279
|
+
try:
|
|
280
|
+
wait = float(err.headers.get("Retry-After", ""))
|
|
281
|
+
except (TypeError, ValueError):
|
|
282
|
+
wait = 2.0 * 2**attempt
|
|
283
|
+
sleep(min(wait, MAX_WAIT))
|
|
284
|
+
except (OSError, ValueError):
|
|
285
|
+
return None
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def neuronpedia_explanations(
|
|
290
|
+
row: AlignmentRow,
|
|
291
|
+
feature_idx: int,
|
|
292
|
+
timeout: float = 20,
|
|
293
|
+
**kwargs,
|
|
294
|
+
) -> list[Explanation] | None:
|
|
295
|
+
"""Return the Neuronpedia explanations of latent ``feature_idx`` aligned in ``row``."""
|
|
296
|
+
return latent_explanations(row.dictionary, layer_of(row), feature_idx, timeout, **kwargs)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def latent_explanations(
|
|
300
|
+
dictionary: str,
|
|
301
|
+
layer: int,
|
|
302
|
+
feature_idx: int,
|
|
303
|
+
timeout: float = 20,
|
|
304
|
+
*,
|
|
305
|
+
retries: int = 3,
|
|
306
|
+
sleep: Callable = time.sleep,
|
|
307
|
+
opener: Callable = urllib.request.urlopen,
|
|
308
|
+
) -> list[Explanation] | None:
|
|
309
|
+
"""Return the latent's Neuronpedia explanations, kept on disk after the first fetch.
|
|
310
|
+
|
|
311
|
+
A latent is fetched once: later calls read the cache, also when it has no
|
|
312
|
+
explanations. ``None`` if Neuronpedia does not host the latent or the request fails;
|
|
313
|
+
a failure is not cached.
|
|
314
|
+
"""
|
|
315
|
+
url = latent_url(dictionary, layer, feature_idx, api=True)
|
|
316
|
+
if url is None:
|
|
317
|
+
return None
|
|
318
|
+
path = _cache_path(url)
|
|
319
|
+
if path is not None and path.exists():
|
|
320
|
+
return [Explanation.model_validate(e) for e in json.loads(path.read_text())]
|
|
321
|
+
data = _get_json(url, timeout=timeout, retries=retries, sleep=sleep, opener=opener)
|
|
322
|
+
if not isinstance(data, dict):
|
|
323
|
+
return None
|
|
324
|
+
found = [
|
|
325
|
+
Explanation(
|
|
326
|
+
description=text,
|
|
327
|
+
explainer=e.get("explanationModelName")
|
|
328
|
+
or ("human" if e.get("typeName") == "human" else "unknown"),
|
|
329
|
+
)
|
|
330
|
+
for e in data.get("explanations") or []
|
|
331
|
+
if (text := (e.get("description") or "").strip())
|
|
332
|
+
]
|
|
333
|
+
if path is not None:
|
|
334
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
335
|
+
partial = path.with_suffix(f".{os.getpid()}.part") # reviews can run side by side
|
|
336
|
+
partial.write_text(json.dumps([e.model_dump() for e in found]))
|
|
337
|
+
partial.replace(path)
|
|
338
|
+
return found
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def neuronpedia_description(row: AlignmentRow, feature_idx: int, **kwargs) -> Explanation | None:
|
|
342
|
+
"""Return the latent's first machine-written Neuronpedia explanation, else its first.
|
|
343
|
+
|
|
344
|
+
``None`` if it has none or the request fails.
|
|
345
|
+
"""
|
|
346
|
+
found = neuronpedia_explanations(row, feature_idx, **kwargs) or []
|
|
347
|
+
return machine_explanation(found) or next(iter(found), None)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def machine_explanation(found: Iterable[Explanation] | None) -> Explanation | None:
|
|
351
|
+
"""The first machine-written explanation of ``found``; ``None`` if there is none."""
|
|
352
|
+
return next((e for e in found or [] if e.explainer != "human"), None)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def feature_slug(row: AlignmentRow, feature_idx: int) -> str:
|
|
356
|
+
"""The slug the latent would have in this registry, if audited here."""
|
|
357
|
+
model_slug = row.slug.split("/L")[0]
|
|
358
|
+
return f"{model_slug}/L{layer_of(row)}/{row.dictionary}/F{feature_idx}"
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def describe(
|
|
362
|
+
rows: Iterable[AlignmentRow],
|
|
363
|
+
registry: dict[str, str] | None = None,
|
|
364
|
+
*,
|
|
365
|
+
fetch: Callable = neuronpedia_description,
|
|
366
|
+
min_percentile: float = 0.0,
|
|
367
|
+
) -> list[dict]:
|
|
368
|
+
"""Return each aligned latent of ``rows`` with its description.
|
|
369
|
+
|
|
370
|
+
The description is the registry's explanation, where the latent was audited here
|
|
371
|
+
(``registry`` maps feature slugs to it). Otherwise it is what ``fetch`` returns.
|
|
372
|
+
``source`` says where it came from. That is "this registry", or Neuronpedia and the
|
|
373
|
+
model that wrote it.
|
|
374
|
+
"""
|
|
375
|
+
registry = registry or {}
|
|
376
|
+
out = []
|
|
377
|
+
for row in rows:
|
|
378
|
+
for side_name in ("writes", "reads"):
|
|
379
|
+
side = getattr(row, side_name)
|
|
380
|
+
if side is None or side.percentile < min_percentile:
|
|
381
|
+
continue
|
|
382
|
+
for m in side.matches:
|
|
383
|
+
if abs(m.cosine) <= side.chance:
|
|
384
|
+
continue
|
|
385
|
+
slug = feature_slug(row, m.feature_idx)
|
|
386
|
+
text, source = registry.get(slug), "this registry"
|
|
387
|
+
if text is None:
|
|
388
|
+
found = fetch(row, m.feature_idx)
|
|
389
|
+
text = found and found.description
|
|
390
|
+
source = found and f"Neuronpedia, {found.explainer}"
|
|
391
|
+
out.append(
|
|
392
|
+
{
|
|
393
|
+
"row": row,
|
|
394
|
+
"side": side_name,
|
|
395
|
+
"match": m,
|
|
396
|
+
"description": text,
|
|
397
|
+
"source": source if text else None,
|
|
398
|
+
}
|
|
399
|
+
)
|
|
400
|
+
return out
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def registry_explanations(records: Iterable[AuditRecord]) -> dict[str, str]:
|
|
404
|
+
"""The newest explanation of every dictionary latent audited here, by slug."""
|
|
405
|
+
newest: dict[str, AuditRecord] = {}
|
|
406
|
+
for record in records:
|
|
407
|
+
ident = record.submission.identifier
|
|
408
|
+
if ident.kind != "dictionary_feature" or record.verdict is None:
|
|
409
|
+
continue
|
|
410
|
+
old = newest.get(ident.slug)
|
|
411
|
+
if old is None or record.verdict.timestamp > old.verdict.timestamp:
|
|
412
|
+
newest[ident.slug] = record
|
|
413
|
+
return {s: r.submission.natural_language_explanation for s, r in newest.items()}
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def format_for_reviewer(described: list[dict]) -> str:
|
|
417
|
+
"""Return aligned latents for the reviewer's prompt.
|
|
418
|
+
|
|
419
|
+
They say what published dictionaries think a latent with this pole's weights does.
|
|
420
|
+
Only latents above chance are listed.
|
|
421
|
+
"""
|
|
422
|
+
lines = []
|
|
423
|
+
for d in described:
|
|
424
|
+
if not d["description"]:
|
|
425
|
+
continue
|
|
426
|
+
row, m = d["row"], d["match"]
|
|
427
|
+
what = "writes" if d["side"] == "writes" else f"reads (via {m.channel})"
|
|
428
|
+
lines.append(
|
|
429
|
+
f"- {row.dictionary} latent {m.feature_idx} {what} along this pole "
|
|
430
|
+
f"(cosine {m.cosine:+.2f}; chance {getattr(row, d['side']).chance:.2f}): "
|
|
431
|
+
f"{d['description']} [{d['source']}]"
|
|
432
|
+
)
|
|
433
|
+
if not lines:
|
|
434
|
+
return ""
|
|
435
|
+
return (
|
|
436
|
+
"Sparse-dictionary latents whose weights align with this pole's, and what their "
|
|
437
|
+
"authors' descriptions say they do. They are not measurements of this pole. Where "
|
|
438
|
+
"a description disagrees with the hypothesis, write probes that tell them apart; "
|
|
439
|
+
"where it agrees, the hypothesis may still be too broad or too narrow.\n"
|
|
440
|
+
+ fence("\n".join(lines))
|
|
441
|
+
+ "\n"
|
|
442
|
+
)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Data files shipped with the package: the built-in sample corpus."""
|
|
2
|
+
|
|
3
|
+
from importlib.resources import files
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def sample_corpus() -> list[str]:
|
|
7
|
+
"""A small, diverse built-in corpus for quick scans. Use --corpus for real audits."""
|
|
8
|
+
text = files(__name__).joinpath("sample_corpus.txt").read_text(encoding="utf-8")
|
|
9
|
+
return [line.strip() for line in text.splitlines() if line.strip()]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
asshole
|
|
2
|
+
assholes
|
|
3
|
+
blowjob
|
|
4
|
+
blowjobs
|
|
5
|
+
bullshit
|
|
6
|
+
cumshot
|
|
7
|
+
cumshots
|
|
8
|
+
cunt
|
|
9
|
+
cunts
|
|
10
|
+
dildo
|
|
11
|
+
dildos
|
|
12
|
+
faggot
|
|
13
|
+
faggots
|
|
14
|
+
fuck
|
|
15
|
+
fucked
|
|
16
|
+
fucker
|
|
17
|
+
fuckers
|
|
18
|
+
fuckin
|
|
19
|
+
fucking
|
|
20
|
+
fucks
|
|
21
|
+
gangbang
|
|
22
|
+
gook
|
|
23
|
+
gooks
|
|
24
|
+
handjob
|
|
25
|
+
handjobs
|
|
26
|
+
kike
|
|
27
|
+
kikes
|
|
28
|
+
motherfucker
|
|
29
|
+
motherfuckers
|
|
30
|
+
nigga
|
|
31
|
+
niggas
|
|
32
|
+
nigger
|
|
33
|
+
niggers
|
|
34
|
+
porn
|
|
35
|
+
porno
|
|
36
|
+
rape
|
|
37
|
+
raped
|
|
38
|
+
rapes
|
|
39
|
+
raping
|
|
40
|
+
rapist
|
|
41
|
+
rapists
|
|
42
|
+
raghead
|
|
43
|
+
ragheads
|
|
44
|
+
shit
|
|
45
|
+
shithead
|
|
46
|
+
shitheads
|
|
47
|
+
shits
|
|
48
|
+
shitting
|
|
49
|
+
shitty
|
|
50
|
+
slut
|
|
51
|
+
sluts
|
|
52
|
+
towelhead
|
|
53
|
+
towelheads
|
|
54
|
+
tranny
|
|
55
|
+
wetback
|
|
56
|
+
wetbacks
|
|
57
|
+
whore
|
|
58
|
+
whores
|