twinlens 1.1.0__tar.gz
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.
- twinlens-1.1.0/LICENSE +21 -0
- twinlens-1.1.0/PKG-INFO +201 -0
- twinlens-1.1.0/README.md +159 -0
- twinlens-1.1.0/pyproject.toml +77 -0
- twinlens-1.1.0/server/__init__.py +0 -0
- twinlens-1.1.0/server/app.py +414 -0
- twinlens-1.1.0/server/schemas.py +41 -0
- twinlens-1.1.0/server/state.py +43 -0
- twinlens-1.1.0/setup.cfg +4 -0
- twinlens-1.1.0/tests/test_api.py +143 -0
- twinlens-1.1.0/tests/test_api_validation.py +130 -0
- twinlens-1.1.0/tests/test_causal.py +95 -0
- twinlens-1.1.0/tests/test_cli.py +107 -0
- twinlens-1.1.0/tests/test_gpt2.py +59 -0
- twinlens-1.1.0/tests/test_metrics.py +103 -0
- twinlens-1.1.0/tests/test_qwen.py +107 -0
- twinlens-1.1.0/tests/test_reproduction.py +73 -0
- twinlens-1.1.0/tests/test_twin.py +116 -0
- twinlens-1.1.0/twinlens/__init__.py +40 -0
- twinlens-1.1.0/twinlens/causal/__init__.py +0 -0
- twinlens-1.1.0/twinlens/causal/attribution.py +97 -0
- twinlens-1.1.0/twinlens/causal/interventions.py +142 -0
- twinlens-1.1.0/twinlens/causal/patching.py +167 -0
- twinlens-1.1.0/twinlens/circuits/__init__.py +0 -0
- twinlens-1.1.0/twinlens/circuits/datasets.py +158 -0
- twinlens-1.1.0/twinlens/circuits/known.py +53 -0
- twinlens-1.1.0/twinlens/cli.py +178 -0
- twinlens-1.1.0/twinlens/core/__init__.py +0 -0
- twinlens-1.1.0/twinlens/core/gpt2.py +295 -0
- twinlens-1.1.0/twinlens/core/qwen3.py +180 -0
- twinlens-1.1.0/twinlens/core/weights.py +119 -0
- twinlens-1.1.0/twinlens/fidelity/__init__.py +0 -0
- twinlens-1.1.0/twinlens/fidelity/engine.py +131 -0
- twinlens-1.1.0/twinlens/fidelity/metrics.py +99 -0
- twinlens-1.1.0/twinlens/fidelity/signals.py +53 -0
- twinlens-1.1.0/twinlens/fidelity/trust.py +91 -0
- twinlens-1.1.0/twinlens/fidelity/trust_model.json +52 -0
- twinlens-1.1.0/twinlens/twin/__init__.py +0 -0
- twinlens-1.1.0/twinlens/twin/replacement.py +452 -0
- twinlens-1.1.0/twinlens/twin/transcoders.py +221 -0
- twinlens-1.1.0/twinlens.egg-info/PKG-INFO +201 -0
- twinlens-1.1.0/twinlens.egg-info/SOURCES.txt +44 -0
- twinlens-1.1.0/twinlens.egg-info/dependency_links.txt +1 -0
- twinlens-1.1.0/twinlens.egg-info/entry_points.txt +2 -0
- twinlens-1.1.0/twinlens.egg-info/requires.txt +22 -0
- twinlens-1.1.0/twinlens.egg-info/top_level.txt +2 -0
twinlens-1.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SpiliosDimakopoulos
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
twinlens-1.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: twinlens
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: A digital twin of an LLM's internals that measures how far its own explanations can be trusted.
|
|
5
|
+
Author: Spilios Dimakopoulos
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://spiliosdimakopoulos.github.io/twinlens/
|
|
8
|
+
Project-URL: Source, https://github.com/SpiliosDimakopoulos/twinlens
|
|
9
|
+
Project-URL: Issues, https://github.com/SpiliosDimakopoulos/twinlens/issues
|
|
10
|
+
Project-URL: Citation, https://doi.org/10.5281/zenodo.23437556
|
|
11
|
+
Keywords: interpretability,mechanistic-interpretability,transcoders,gpt-2,faithfulness,explanations,llm
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: torch>=2.2
|
|
22
|
+
Requires-Dist: numpy>=1.26
|
|
23
|
+
Requires-Dist: tokenizers>=0.19
|
|
24
|
+
Requires-Dist: huggingface_hub>=0.23
|
|
25
|
+
Requires-Dist: pandas>=2.1
|
|
26
|
+
Requires-Dist: pydantic>=2.5
|
|
27
|
+
Provides-Extra: server
|
|
28
|
+
Requires-Dist: fastapi>=0.110; extra == "server"
|
|
29
|
+
Requires-Dist: uvicorn[standard]>=0.29; extra == "server"
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: twinlens[server]; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-timeout; extra == "dev"
|
|
34
|
+
Requires-Dist: httpx; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff; extra == "dev"
|
|
36
|
+
Requires-Dist: matplotlib; extra == "dev"
|
|
37
|
+
Requires-Dist: pyarrow; extra == "dev"
|
|
38
|
+
Requires-Dist: scipy; extra == "dev"
|
|
39
|
+
Requires-Dist: psutil; extra == "dev"
|
|
40
|
+
Requires-Dist: transformers; extra == "dev"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# TwinLens
|
|
44
|
+
|
|
45
|
+
[](https://github.com/SpiliosDimakopoulos/twinlens/actions/workflows/ci.yml) [](https://spiliosdimakopoulos.github.io/twinlens/) [](https://doi.org/10.5281/zenodo.23437556) [](https://colab.research.google.com/github/SpiliosDimakopoulos/twinlens/blob/main/notebooks/audit_your_prompt.ipynb)
|
|
46
|
+
|
|
47
|
+
**Do attribution graphs predict what a language model actually does?** An attribution graph
|
|
48
|
+
explains a prediction with arrows: "this internal feature pushes the answer up by this much".
|
|
49
|
+
TwinLens tests those arrows. For each one it switches the feature off in GPT-2 and compares the
|
|
50
|
+
graph's predicted effect with the real one.
|
|
51
|
+
|
|
52
|
+
On 8,064 such edits the graph is often wrong, sometimes in the opposite direction. Most of the
|
|
53
|
+
error comes from two parts of the model that the graph holds fixed, LayerNorm and attention.
|
|
54
|
+
Adding their first-order response back keeps the graph linear and raises its rank agreement
|
|
55
|
+
with GPT-2 from ρ = 0.20 to 0.53. The result replicates on Qwen3-0.6B.
|
|
56
|
+
|
|
57
|
+

|
|
58
|
+
|
|
59
|
+
*In the IOI prompt above, today's attribution graph predicts that switching off one feature
|
|
60
|
+
lowers the probability of "Mary" by 8.1 nats. GPT-2 actually raises it by 0.23. The same linear
|
|
61
|
+
graph with the two derivatives restored predicts +0.32.*
|
|
62
|
+
|
|
63
|
+
## Try it
|
|
64
|
+
|
|
65
|
+
**In the browser:** the [live demo](https://spiliosdimakopoulos.github.io/twinlens/) runs on
|
|
66
|
+
precomputed GPT-2 runs. Click a dot in the graph and compare each prediction with what GPT-2 did.
|
|
67
|
+
|
|
68
|
+
**On your own prompt, in Colab:** [`notebooks/audit_your_prompt.ipynb`](https://colab.research.google.com/github/SpiliosDimakopoulos/twinlens/blob/main/notebooks/audit_your_prompt.ipynb)
|
|
69
|
+
(free CPU runtime, about five minutes including downloads).
|
|
70
|
+
|
|
71
|
+
**From a terminal:**
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install torch --index-url https://download.pytorch.org/whl/cpu # CPU build, ~200 MB
|
|
75
|
+
pip install "twinlens @ git+https://github.com/SpiliosDimakopoulos/twinlens"
|
|
76
|
+
twinlens audit "When Mary and John went to the store, John gave a drink to"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The first run downloads GPT-2 small (0.5 GB) and its 12 transcoders (1.8 GB) from the Hugging
|
|
80
|
+
Face Hub into `~/.cache/twinlens` (set `TWINLENS_CACHE` to move it). `twinlens audit` checks the
|
|
81
|
+
strongest arrows of the graph and prints one row per arrow:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
layer word feature graph fixed GPT-2 graph verdict fixed verdict trust
|
|
85
|
+
0 Mary 5545 -8.117 +0.318 +0.228 WRONG SIGN ok 7%
|
|
86
|
+
10 to 13585 +0.014 +0.031 +0.032 understated ok 98%
|
|
87
|
+
7 Mary 12771 -0.186 +0.032 +0.034 WRONG SIGN ok 81%
|
|
88
|
+
...
|
|
89
|
+
Today's graph is off on 3 of 8 edges; with the fix, 0 of 8.
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- **graph**: today's attribution graph (LayerNorm and attention frozen);
|
|
93
|
+
- **fixed**: the same linear graph with the LayerNorm and attention derivatives kept;
|
|
94
|
+
- **GPT-2**: the real model with the edit applied;
|
|
95
|
+
- numbers are the change in log-probability of GPT-2's answer (−1 ≈ 2.7× less likely);
|
|
96
|
+
- **trust** estimates, from the unedited run only, how likely the graph's number is to be close.
|
|
97
|
+
It was fitted on ablations and ×2 edits of this study's prompts; treat it as a warning light,
|
|
98
|
+
not a probability you can bank on.
|
|
99
|
+
|
|
100
|
+
Add `--json` for machine-readable output and `--top 20` for more arrows.
|
|
101
|
+
|
|
102
|
+
## Results
|
|
103
|
+
|
|
104
|
+
From 8,064 edits on GPT-2 small (56 prompts, 8 task families). Full write-up:
|
|
105
|
+
[report](https://spiliosdimakopoulos.github.io/twinlens/report/) ([PDF](docs/TwinLens_report.pdf)).
|
|
106
|
+
|
|
107
|
+
| | today's graph | gradient through real GPT-2 | with the fix |
|
|
108
|
+
|---|---|---|---|
|
|
109
|
+
| rank agreement with the real effect (Spearman ρ) | 0.20 | 0.38 | 0.53 |
|
|
110
|
+
| right direction, on edits that move the answer | 67% | 77% | 86% |
|
|
111
|
+
| badly wrong (off by more than 1 nat) | 2.2% | 0.1% | 0.05% |
|
|
112
|
+
|
|
113
|
+
- **Why the graph fails.** Frozen LayerNorm causes most of the error and nearly all large
|
|
114
|
+
misses, almost all of them for edits in the first three layers. Frozen attention causes most
|
|
115
|
+
of the ranking error.
|
|
116
|
+
- **The fix.** Keeping the LayerNorm and attention-softmax Jacobians gives a graph that is still
|
|
117
|
+
linear and matches the nonlinear twin. It also ranks and signs effects better than attribution
|
|
118
|
+
patching, a gradient through the real model (Δρ +0.15 [0.11, 0.19]). On Qwen3-0.6B the gain
|
|
119
|
+
over today's graph is smaller but holds on every metric (Δρ +0.12 [0.05, 0.20]). IOI prompts
|
|
120
|
+
go from ρ 0.01 to 0.59.
|
|
121
|
+
- **Spotting bad predictions.** A logistic model over nine signals from the unedited run flags
|
|
122
|
+
failures with AUROC 0.84 on edits that move the answer. Disagreement between today's graph
|
|
123
|
+
and the fixed graph is an even better warning (0.94).
|
|
124
|
+
- **Upstream.** The fix is proposed to the open-source
|
|
125
|
+
[circuit-tracer](https://github.com/decoderesearch/circuit-tracer) library as opt-in flags
|
|
126
|
+
([PR #123](https://github.com/decoderesearch/circuit-tracer/pull/123), under review).
|
|
127
|
+
|
|
128
|
+
<p>
|
|
129
|
+
<img src="experiments/figures/F5_fix.png" width="100%" alt="Keeping two derivatives makes the linear graph as faithful as the nonlinear twin">
|
|
130
|
+
</p>
|
|
131
|
+
|
|
132
|
+
## Limitations
|
|
133
|
+
|
|
134
|
+
- Two small models (GPT-2 small; Qwen3-0.6B, upper 18 layers only) and per-layer transcoders.
|
|
135
|
+
Cross-layer transcoders, used by the current circuit-tracing work, are untested.
|
|
136
|
+
- 56 short prompts written by the author; one prediction target per prompt.
|
|
137
|
+
- Only feature-to-output effects are measured, not feature-to-feature edges.
|
|
138
|
+
- Most edits change the answer very little; the rank and sign results are the robust ones.
|
|
139
|
+
|
|
140
|
+
Details and the remaining caveats are in §5 of the [report](https://spiliosdimakopoulos.github.io/twinlens/report/).
|
|
141
|
+
|
|
142
|
+
## How it works
|
|
143
|
+
|
|
144
|
+
GPT-2 runs on a ~250-line PyTorch implementation with hook points
|
|
145
|
+
([`twinlens/core/gpt2.py`](twinlens/core/gpt2.py)); its logits match Hugging Face to 7×10⁻⁵.
|
|
146
|
+
Before measuring anything new it reproduces known GPT-2 circuits (induction heads, the IOI name
|
|
147
|
+
movers, the sites of factual recall), each pinned as a test. MLPs are replaced by the
|
|
148
|
+
transcoders of Dunefsky, Chlenski & Nanda (2024) plus a fixed error term, and five twin levels
|
|
149
|
+
switch one part of the real model back on at a time, so every gap is attributed to its source.
|
|
150
|
+
|
|
151
|
+
Weights and transcoders are memory-mapped, so the whole system runs in 0.82 GB of RAM on a
|
|
152
|
+
laptop CPU ([`DECISIONS.md`](DECISIONS.md)):
|
|
153
|
+
|
|
154
|
+
| CPU latency (Ryzen 7 5700U, 15 tokens) | |
|
|
155
|
+
|---|---|
|
|
156
|
+
| forward pass | 194 ms |
|
|
157
|
+
| twin trace (12 transcoders) | 1.0 s |
|
|
158
|
+
| what-if, graph twin / nonlinear twin | 0.17 s / 0.45 s |
|
|
159
|
+
|
|
160
|
+
## Run the dashboard locally
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
164
|
+
pip install torch --index-url https://download.pytorch.org/whl/cpu
|
|
165
|
+
pip install -e ".[dev]"
|
|
166
|
+
cd web && npm ci && cd ..
|
|
167
|
+
make serve # http://localhost:8000
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Locally the dashboard accepts any prompt. `docker compose up` does the same in a container.
|
|
171
|
+
|
|
172
|
+
## Reproduce the paper
|
|
173
|
+
|
|
174
|
+
The sweep's raw results are committed, so `make analyze` re-derives every number in the report
|
|
175
|
+
in a few minutes without a model. Re-running the sweeps themselves is described in §7 of the
|
|
176
|
+
report. `make test`, `make web-test` and `make e2e` run the test suites.
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
twinlens/ model, twin levels, interventions, fidelity metrics, trust model, CLI
|
|
180
|
+
server/ FastAPI (REST + WebSocket)
|
|
181
|
+
web/ React + TypeScript dashboard
|
|
182
|
+
experiments/ sweeps, analysis, figures, committed results
|
|
183
|
+
paper/ the technical report
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Cite
|
|
187
|
+
|
|
188
|
+
```bibtex
|
|
189
|
+
@techreport{dimakopoulos2026twinlens,
|
|
190
|
+
title = {Attribution graphs fail because of the derivatives they drop, not because they are linear},
|
|
191
|
+
author = {Dimakopoulos, Spilios},
|
|
192
|
+
year = {2026},
|
|
193
|
+
institution = {TwinLens project},
|
|
194
|
+
doi = {10.5281/zenodo.23437556},
|
|
195
|
+
url = {https://github.com/SpiliosDimakopoulos/twinlens}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Contributing
|
|
200
|
+
|
|
201
|
+
Issues and pull requests are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md). MIT licence.
|
twinlens-1.1.0/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# TwinLens
|
|
2
|
+
|
|
3
|
+
[](https://github.com/SpiliosDimakopoulos/twinlens/actions/workflows/ci.yml) [](https://spiliosdimakopoulos.github.io/twinlens/) [](https://doi.org/10.5281/zenodo.23437556) [](https://colab.research.google.com/github/SpiliosDimakopoulos/twinlens/blob/main/notebooks/audit_your_prompt.ipynb)
|
|
4
|
+
|
|
5
|
+
**Do attribution graphs predict what a language model actually does?** An attribution graph
|
|
6
|
+
explains a prediction with arrows: "this internal feature pushes the answer up by this much".
|
|
7
|
+
TwinLens tests those arrows. For each one it switches the feature off in GPT-2 and compares the
|
|
8
|
+
graph's predicted effect with the real one.
|
|
9
|
+
|
|
10
|
+
On 8,064 such edits the graph is often wrong, sometimes in the opposite direction. Most of the
|
|
11
|
+
error comes from two parts of the model that the graph holds fixed, LayerNorm and attention.
|
|
12
|
+
Adding their first-order response back keeps the graph linear and raises its rank agreement
|
|
13
|
+
with GPT-2 from ρ = 0.20 to 0.53. The result replicates on Qwen3-0.6B.
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
*In the IOI prompt above, today's attribution graph predicts that switching off one feature
|
|
18
|
+
lowers the probability of "Mary" by 8.1 nats. GPT-2 actually raises it by 0.23. The same linear
|
|
19
|
+
graph with the two derivatives restored predicts +0.32.*
|
|
20
|
+
|
|
21
|
+
## Try it
|
|
22
|
+
|
|
23
|
+
**In the browser:** the [live demo](https://spiliosdimakopoulos.github.io/twinlens/) runs on
|
|
24
|
+
precomputed GPT-2 runs. Click a dot in the graph and compare each prediction with what GPT-2 did.
|
|
25
|
+
|
|
26
|
+
**On your own prompt, in Colab:** [`notebooks/audit_your_prompt.ipynb`](https://colab.research.google.com/github/SpiliosDimakopoulos/twinlens/blob/main/notebooks/audit_your_prompt.ipynb)
|
|
27
|
+
(free CPU runtime, about five minutes including downloads).
|
|
28
|
+
|
|
29
|
+
**From a terminal:**
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install torch --index-url https://download.pytorch.org/whl/cpu # CPU build, ~200 MB
|
|
33
|
+
pip install "twinlens @ git+https://github.com/SpiliosDimakopoulos/twinlens"
|
|
34
|
+
twinlens audit "When Mary and John went to the store, John gave a drink to"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The first run downloads GPT-2 small (0.5 GB) and its 12 transcoders (1.8 GB) from the Hugging
|
|
38
|
+
Face Hub into `~/.cache/twinlens` (set `TWINLENS_CACHE` to move it). `twinlens audit` checks the
|
|
39
|
+
strongest arrows of the graph and prints one row per arrow:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
layer word feature graph fixed GPT-2 graph verdict fixed verdict trust
|
|
43
|
+
0 Mary 5545 -8.117 +0.318 +0.228 WRONG SIGN ok 7%
|
|
44
|
+
10 to 13585 +0.014 +0.031 +0.032 understated ok 98%
|
|
45
|
+
7 Mary 12771 -0.186 +0.032 +0.034 WRONG SIGN ok 81%
|
|
46
|
+
...
|
|
47
|
+
Today's graph is off on 3 of 8 edges; with the fix, 0 of 8.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- **graph**: today's attribution graph (LayerNorm and attention frozen);
|
|
51
|
+
- **fixed**: the same linear graph with the LayerNorm and attention derivatives kept;
|
|
52
|
+
- **GPT-2**: the real model with the edit applied;
|
|
53
|
+
- numbers are the change in log-probability of GPT-2's answer (−1 ≈ 2.7× less likely);
|
|
54
|
+
- **trust** estimates, from the unedited run only, how likely the graph's number is to be close.
|
|
55
|
+
It was fitted on ablations and ×2 edits of this study's prompts; treat it as a warning light,
|
|
56
|
+
not a probability you can bank on.
|
|
57
|
+
|
|
58
|
+
Add `--json` for machine-readable output and `--top 20` for more arrows.
|
|
59
|
+
|
|
60
|
+
## Results
|
|
61
|
+
|
|
62
|
+
From 8,064 edits on GPT-2 small (56 prompts, 8 task families). Full write-up:
|
|
63
|
+
[report](https://spiliosdimakopoulos.github.io/twinlens/report/) ([PDF](docs/TwinLens_report.pdf)).
|
|
64
|
+
|
|
65
|
+
| | today's graph | gradient through real GPT-2 | with the fix |
|
|
66
|
+
|---|---|---|---|
|
|
67
|
+
| rank agreement with the real effect (Spearman ρ) | 0.20 | 0.38 | 0.53 |
|
|
68
|
+
| right direction, on edits that move the answer | 67% | 77% | 86% |
|
|
69
|
+
| badly wrong (off by more than 1 nat) | 2.2% | 0.1% | 0.05% |
|
|
70
|
+
|
|
71
|
+
- **Why the graph fails.** Frozen LayerNorm causes most of the error and nearly all large
|
|
72
|
+
misses, almost all of them for edits in the first three layers. Frozen attention causes most
|
|
73
|
+
of the ranking error.
|
|
74
|
+
- **The fix.** Keeping the LayerNorm and attention-softmax Jacobians gives a graph that is still
|
|
75
|
+
linear and matches the nonlinear twin. It also ranks and signs effects better than attribution
|
|
76
|
+
patching, a gradient through the real model (Δρ +0.15 [0.11, 0.19]). On Qwen3-0.6B the gain
|
|
77
|
+
over today's graph is smaller but holds on every metric (Δρ +0.12 [0.05, 0.20]). IOI prompts
|
|
78
|
+
go from ρ 0.01 to 0.59.
|
|
79
|
+
- **Spotting bad predictions.** A logistic model over nine signals from the unedited run flags
|
|
80
|
+
failures with AUROC 0.84 on edits that move the answer. Disagreement between today's graph
|
|
81
|
+
and the fixed graph is an even better warning (0.94).
|
|
82
|
+
- **Upstream.** The fix is proposed to the open-source
|
|
83
|
+
[circuit-tracer](https://github.com/decoderesearch/circuit-tracer) library as opt-in flags
|
|
84
|
+
([PR #123](https://github.com/decoderesearch/circuit-tracer/pull/123), under review).
|
|
85
|
+
|
|
86
|
+
<p>
|
|
87
|
+
<img src="experiments/figures/F5_fix.png" width="100%" alt="Keeping two derivatives makes the linear graph as faithful as the nonlinear twin">
|
|
88
|
+
</p>
|
|
89
|
+
|
|
90
|
+
## Limitations
|
|
91
|
+
|
|
92
|
+
- Two small models (GPT-2 small; Qwen3-0.6B, upper 18 layers only) and per-layer transcoders.
|
|
93
|
+
Cross-layer transcoders, used by the current circuit-tracing work, are untested.
|
|
94
|
+
- 56 short prompts written by the author; one prediction target per prompt.
|
|
95
|
+
- Only feature-to-output effects are measured, not feature-to-feature edges.
|
|
96
|
+
- Most edits change the answer very little; the rank and sign results are the robust ones.
|
|
97
|
+
|
|
98
|
+
Details and the remaining caveats are in §5 of the [report](https://spiliosdimakopoulos.github.io/twinlens/report/).
|
|
99
|
+
|
|
100
|
+
## How it works
|
|
101
|
+
|
|
102
|
+
GPT-2 runs on a ~250-line PyTorch implementation with hook points
|
|
103
|
+
([`twinlens/core/gpt2.py`](twinlens/core/gpt2.py)); its logits match Hugging Face to 7×10⁻⁵.
|
|
104
|
+
Before measuring anything new it reproduces known GPT-2 circuits (induction heads, the IOI name
|
|
105
|
+
movers, the sites of factual recall), each pinned as a test. MLPs are replaced by the
|
|
106
|
+
transcoders of Dunefsky, Chlenski & Nanda (2024) plus a fixed error term, and five twin levels
|
|
107
|
+
switch one part of the real model back on at a time, so every gap is attributed to its source.
|
|
108
|
+
|
|
109
|
+
Weights and transcoders are memory-mapped, so the whole system runs in 0.82 GB of RAM on a
|
|
110
|
+
laptop CPU ([`DECISIONS.md`](DECISIONS.md)):
|
|
111
|
+
|
|
112
|
+
| CPU latency (Ryzen 7 5700U, 15 tokens) | |
|
|
113
|
+
|---|---|
|
|
114
|
+
| forward pass | 194 ms |
|
|
115
|
+
| twin trace (12 transcoders) | 1.0 s |
|
|
116
|
+
| what-if, graph twin / nonlinear twin | 0.17 s / 0.45 s |
|
|
117
|
+
|
|
118
|
+
## Run the dashboard locally
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
122
|
+
pip install torch --index-url https://download.pytorch.org/whl/cpu
|
|
123
|
+
pip install -e ".[dev]"
|
|
124
|
+
cd web && npm ci && cd ..
|
|
125
|
+
make serve # http://localhost:8000
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Locally the dashboard accepts any prompt. `docker compose up` does the same in a container.
|
|
129
|
+
|
|
130
|
+
## Reproduce the paper
|
|
131
|
+
|
|
132
|
+
The sweep's raw results are committed, so `make analyze` re-derives every number in the report
|
|
133
|
+
in a few minutes without a model. Re-running the sweeps themselves is described in §7 of the
|
|
134
|
+
report. `make test`, `make web-test` and `make e2e` run the test suites.
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
twinlens/ model, twin levels, interventions, fidelity metrics, trust model, CLI
|
|
138
|
+
server/ FastAPI (REST + WebSocket)
|
|
139
|
+
web/ React + TypeScript dashboard
|
|
140
|
+
experiments/ sweeps, analysis, figures, committed results
|
|
141
|
+
paper/ the technical report
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Cite
|
|
145
|
+
|
|
146
|
+
```bibtex
|
|
147
|
+
@techreport{dimakopoulos2026twinlens,
|
|
148
|
+
title = {Attribution graphs fail because of the derivatives they drop, not because they are linear},
|
|
149
|
+
author = {Dimakopoulos, Spilios},
|
|
150
|
+
year = {2026},
|
|
151
|
+
institution = {TwinLens project},
|
|
152
|
+
doi = {10.5281/zenodo.23437556},
|
|
153
|
+
url = {https://github.com/SpiliosDimakopoulos/twinlens}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Contributing
|
|
158
|
+
|
|
159
|
+
Issues and pull requests are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md). MIT licence.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "twinlens"
|
|
7
|
+
version = "1.1.0"
|
|
8
|
+
description = "A digital twin of an LLM's internals that measures how far its own explanations can be trusted."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Spilios Dimakopoulos" }]
|
|
14
|
+
keywords = ["interpretability", "mechanistic-interpretability", "transcoders", "gpt-2", "faithfulness", "explanations", "llm"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"torch>=2.2",
|
|
25
|
+
"numpy>=1.26",
|
|
26
|
+
"tokenizers>=0.19",
|
|
27
|
+
"huggingface_hub>=0.23",
|
|
28
|
+
"pandas>=2.1",
|
|
29
|
+
"pydantic>=2.5",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://spiliosdimakopoulos.github.io/twinlens/"
|
|
34
|
+
Source = "https://github.com/SpiliosDimakopoulos/twinlens"
|
|
35
|
+
Issues = "https://github.com/SpiliosDimakopoulos/twinlens/issues"
|
|
36
|
+
Citation = "https://doi.org/10.5281/zenodo.23437556"
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
twinlens = "twinlens.cli:main"
|
|
40
|
+
|
|
41
|
+
[project.optional-dependencies]
|
|
42
|
+
server = ["fastapi>=0.110", "uvicorn[standard]>=0.29"]
|
|
43
|
+
dev = ["twinlens[server]", "pytest", "pytest-timeout", "httpx", "ruff", "matplotlib", "pyarrow", "scipy", "psutil", "transformers"]
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.packages.find]
|
|
46
|
+
include = ["twinlens*", "server*"]
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.package-data]
|
|
49
|
+
twinlens = ["fidelity/trust_model.json"] # so `pip install` users get the trust score
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
line-length = 100
|
|
53
|
+
target-version = "py311"
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint]
|
|
56
|
+
select = ["E", "F", "I", "B", "UP", "SIM", "C4", "RUF", "PT"]
|
|
57
|
+
ignore = [
|
|
58
|
+
"E741", # `l` for layer is the field convention
|
|
59
|
+
"PT018", # composite asserts in tests (7 hits); split later
|
|
60
|
+
"RUF001", # intentional unicode (e.g. minus, arrows) in strings (4 hits)
|
|
61
|
+
"RUF002", # same, in docstrings (3 hits)
|
|
62
|
+
"RUF005", # [a] + b style (2 hits); fix later
|
|
63
|
+
"RUF007", # zip(x, x[1:]) (1 hit); fix later
|
|
64
|
+
"C416", # identity comprehensions (3 hits); fix later
|
|
65
|
+
"RUF100", # one stale noqa (1 hit); fix later
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
[tool.ruff.lint.per-file-ignores]
|
|
69
|
+
"experiments/build_report.py" = ["E501"] # embedded CSS
|
|
70
|
+
|
|
71
|
+
[tool.pytest.ini_options]
|
|
72
|
+
testpaths = ["tests"]
|
|
73
|
+
addopts = "-m 'not slow and not qwen'"
|
|
74
|
+
markers = ["slow: needs extra RAM (HF parity)", "qwen: needs the prepared Qwen3 transcoders (~7 GB)"]
|
|
75
|
+
|
|
76
|
+
[tool.ruff.format]
|
|
77
|
+
quote-style = "double"
|
|
File without changes
|