Spaces:
Running on Zero
Running on Zero
Commit ·
1bb3265
1
Parent(s): 4a79e5b
Release FeatureLens v0.6.0
Browse files- CHANGELOG.md +20 -0
- README.md +36 -22
- app.py +301 -27
- docs/HF_DEPLOY.md +4 -1
- docs/METHODOLOGY.md +31 -2
- docs/VALIDATION.md +115 -199
- featurelens/runtime.py +215 -0
- pyproject.toml +1 -1
- research_config.json +11 -0
- scripts/release_check.py +24 -5
- tests/test_live_runtime_helpers.py +25 -0
CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
| 1 |
# Changelog
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
## v0.5.0
|
| 4 |
|
| 5 |
### Causal specificity
|
|
|
|
| 1 |
# Changelog
|
| 2 |
|
| 3 |
+
## v0.6.0
|
| 4 |
+
|
| 5 |
+
### UX / navigation
|
| 6 |
+
- Added a plain-language **Start here** tab with a three-step workflow and glossary for non-specialist reviewers.
|
| 7 |
+
- Added a persistent **Current Workbench context** banner so inherited prompt/layer/token state is visible from every tab.
|
| 8 |
+
- Added explicit editable feature selectors for **scale dose-response** and **contrastive continuation preference** instead of silently reusing the single-feature selector.
|
| 9 |
+
- Clarified state provenance in Feature Sets and Feature Evidence; experiment text now states whether it inherits Workbench context or uses an independent prompt set.
|
| 10 |
+
- Normalized heading hierarchy and increased table/header typography for readability.
|
| 11 |
+
- Added native plot **fullscreen** and **export PNG** controls to every BarPlot/LinePlot.
|
| 12 |
+
|
| 13 |
+
### Live research tools
|
| 14 |
+
- Added **concept-guided candidate feature discovery**: rank features for a selected controlled concept using prompt-wide target-minus-other mean maximum activation, with selectivity and activation-rate diagnostics.
|
| 15 |
+
- Added a **completion-cue sensitivity scan** that appends controlled suffixes to a prompt stem and measures the selected feature at the resulting final token.
|
| 16 |
+
- Added a one-click action to reuse a discovered candidate across single-feature, dose-response, contrastive, and evidence feature selectors.
|
| 17 |
+
- Candidate discovery and cue scans are explicitly exploratory; neither creates semantic labels or overwrites `Offline concept hint`.
|
| 18 |
+
|
| 19 |
+
### Validation
|
| 20 |
+
- Expanded toy-runtime coverage to **40 tests**, including candidate-feature discovery and completion-cue scans.
|
| 21 |
+
- Retained the actual Gradio `launch()` smoke gate, compile gate, release checker, batched-null regression tests, and final-release adversarial-test deferral.
|
| 22 |
+
|
| 23 |
## v0.5.0
|
| 24 |
|
| 25 |
### Causal specificity
|
README.md
CHANGED
|
@@ -13,7 +13,7 @@ license: mit
|
|
| 13 |
|
| 14 |
# FeatureLens — Causal Interpretability Workbench
|
| 15 |
|
| 16 |
-
> **v0.
|
| 17 |
|
| 18 |
**Research question:**
|
| 19 |
|
|
@@ -28,14 +28,16 @@ FeatureLens is independent of thesis code and thesis datasets.
|
|
| 28 |
1. **Reconstruction** — does the SAE represent the residual stream reasonably well?
|
| 29 |
2. **Prediction** — does a feature distinguish a controlled concept on held-out paraphrase groups?
|
| 30 |
3. **Robustness** — does the representation survive rewording locally and prompt-wide?
|
| 31 |
-
4. **
|
| 32 |
-
5. **
|
| 33 |
-
6. **
|
| 34 |
-
7. **
|
| 35 |
-
8. **
|
| 36 |
-
9. **
|
| 37 |
-
10. **
|
| 38 |
-
11. **
|
|
|
|
|
|
|
| 39 |
|
| 40 |
A high AUROC, high paraphrase overlap, or a large activation is still correlational evidence. Causal evidence comes from downstream change under controlled intervention.
|
| 41 |
|
|
@@ -60,7 +62,7 @@ The UI also reports single-forward → batched-null execution drift as a diagnos
|
|
| 60 |
|
| 61 |
## Random-control ensemble
|
| 62 |
|
| 63 |
-
A single random residual direction can be unusually weak or unusually disruptive by chance. FeatureLens uses
|
| 64 |
|
| 65 |
For a targeted residual delta `Δh`, each control satisfies:
|
| 66 |
|
|
@@ -125,10 +127,12 @@ The difference is reported as an **interaction excess**. A non-zero excess means
|
|
| 125 |
|
| 126 |
### Feature evidence
|
| 127 |
|
| 128 |
-
The **Feature evidence** tab now
|
| 129 |
|
| 130 |
-
1. **
|
| 131 |
-
2. **
|
|
|
|
|
|
|
| 132 |
|
| 133 |
- code;
|
| 134 |
- mathematics;
|
|
@@ -138,9 +142,9 @@ The **Feature evidence** tab now provides two complementary live views:
|
|
| 138 |
- factual entities;
|
| 139 |
- uncertainty.
|
| 140 |
|
| 141 |
-
|
| 142 |
|
| 143 |
-
|
| 144 |
|
| 145 |
### Contrastive continuation preference
|
| 146 |
|
|
@@ -169,11 +173,20 @@ The prompt-wide view reduces sensitivity to accidentally comparing semantically
|
|
| 169 |
|
| 170 |
The trajectory view compares SAE reconstruction and sparsity/concentration diagnostics at the same prompt token across layers 4, 14 and 26. It does **not** equate feature IDs across layer-specific SAE dictionaries.
|
| 171 |
|
| 172 |
-
## v0.
|
| 173 |
|
| 174 |
-
|
| 175 |
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
## Copy tables with headers
|
| 179 |
|
|
@@ -291,7 +304,7 @@ FEATURELENS_CONTRAST_PROMPTS_PER_CONCEPT=4
|
|
| 291 |
|
| 292 |
## UI design
|
| 293 |
|
| 294 |
-
v0.
|
| 295 |
|
| 296 |
- Times / Liberation Serif-style formal typography;
|
| 297 |
- restrained teal, umber, stone, plum, and muted red chart accents;
|
|
@@ -343,12 +356,13 @@ python scripts/ui_smoke.py
|
|
| 343 |
python scripts/release_check.py
|
| 344 |
```
|
| 345 |
|
| 346 |
-
See [`docs/VALIDATION.md`](docs/VALIDATION.md). The v0.
|
| 347 |
|
| 348 |
## Limitations
|
| 349 |
|
| 350 |
- SAE features are sparse directions, not guaranteed monosemantic concepts.
|
| 351 |
-
-
|
|
|
|
| 352 |
- Prompt-wide max pooling measures feature presence anywhere in the prompt and discards token order.
|
| 353 |
- Joint residual edits can be non-additive downstream without implying direct feature-feature interaction.
|
| 354 |
- Eight live random controls give only a coarse empirical tail probability.
|
|
@@ -358,7 +372,7 @@ See [`docs/VALIDATION.md`](docs/VALIDATION.md). The v0.5 validation guide uses t
|
|
| 358 |
## Resume-ready description
|
| 359 |
|
| 360 |
> **FeatureLens — Causal Interpretability Workbench** | PyTorch, Qwen3, Sparse Autoencoders, Mechanistic Interpretability, Gradio
|
| 361 |
-
> Built an SAE-based interpretability system for Qwen3-1.7B with held-out concept discovery, token/prompt-wide feature evidence, reconstruction-preserving single and multi-feature interventions, full-continuation and contrastive preference scoring, dose-response analysis, decoder-geometry/non-additivity diagnostics, and norm-matched random-control ensembles.
|
| 362 |
|
| 363 |
## Acknowledgements
|
| 364 |
|
|
|
|
| 13 |
|
| 14 |
# FeatureLens — Causal Interpretability Workbench
|
| 15 |
|
| 16 |
+
> **v0.6:** a plain-language **Start here** workflow, persistent experiment context, explicit per-experiment feature selectors, plot fullscreen/export controls, concept-guided candidate-feature discovery, and completion-cue sensitivity tests—while retaining the v0.5 causal, geometry, robustness, and prompt-wide evidence tools.
|
| 17 |
|
| 18 |
**Research question:**
|
| 19 |
|
|
|
|
| 28 |
1. **Reconstruction** — does the SAE represent the residual stream reasonably well?
|
| 29 |
2. **Prediction** — does a feature distinguish a controlled concept on held-out paraphrase groups?
|
| 30 |
3. **Robustness** — does the representation survive rewording locally and prompt-wide?
|
| 31 |
+
4. **Candidate discovery** — which features are more active for a chosen controlled concept than for the other groups?
|
| 32 |
+
5. **Controlled contrast** — does a selected feature prefer one concept group in a balanced live mini-batch?
|
| 33 |
+
6. **Cue sensitivity** — is a feature responding to a lexical/completion cue rather than the underlying concept?
|
| 34 |
+
7. **Single-feature causality** — does ablation/scaling change downstream probability?
|
| 35 |
+
8. **Dose-response** — does effect size vary coherently with feature coefficient?
|
| 36 |
+
9. **Feature-set causality** — do small sparse subspaces matter more than one feature?
|
| 37 |
+
10. **Non-additivity** — does a joint edit differ from the sum of individual effects?
|
| 38 |
+
11. **Specificity** — are targeted effects larger than norm-matched random residual perturbations?
|
| 39 |
+
12. **Contrastive specificity** — does an edit change preference between a desired and comparison continuation?
|
| 40 |
+
13. **Geometry** — are selected SAE decoder directions aligned, orthogonal, or cancelling before downstream model non-linearity?
|
| 41 |
|
| 42 |
A high AUROC, high paraphrase overlap, or a large activation is still correlational evidence. Causal evidence comes from downstream change under controlled intervention.
|
| 43 |
|
|
|
|
| 62 |
|
| 63 |
## Random-control ensemble
|
| 64 |
|
| 65 |
+
A single random residual direction can be unusually weak or unusually disruptive by chance. FeatureLens uses **8 deterministic norm-matched random directions**.
|
| 66 |
|
| 67 |
For a targeted residual delta `Δh`, each control satisfies:
|
| 68 |
|
|
|
|
| 127 |
|
| 128 |
### Feature evidence
|
| 129 |
|
| 130 |
+
The **Feature evidence** tab now supports four complementary live questions:
|
| 131 |
|
| 132 |
+
1. **Concept-guided candidate discovery** — choose one of the seven controlled concept groups and rank SAE features by the difference between that concept's prompt-wide mean maximum activation and the mean across the other six groups. The result is a candidate list, **not a semantic label**.
|
| 133 |
+
2. **Feature-token activation trace** — show exactly where a selected feature enters the SAE TopK support across every token of the current Workbench prompt.
|
| 134 |
+
3. **Completion-cue sensitivity** — append controlled suffixes/cues to one prompt stem and measure the selected feature at the final token. This is useful for separating concept evidence from lexical/structural completion cues.
|
| 135 |
+
4. **Prompt-wide controlled concept contrast** — evaluate one selected feature on a small balanced batch from:
|
| 136 |
|
| 137 |
- code;
|
| 138 |
- mathematics;
|
|
|
|
| 142 |
- factual entities;
|
| 143 |
- uncertainty.
|
| 144 |
|
| 145 |
+
Each controlled prompt is summarized by the feature's **maximum activation across non-padding tokens**. Candidate discovery ranks by a transparent target-minus-other mean difference and reports activation rates/selectivity alongside it. The controlled contrast reports mean/median prompt-wide maxima, prompt activation rate, mean activation when active, and maximum activation by concept.
|
| 146 |
|
| 147 |
+
Both tools are exploratory live evidence. They never overwrite `Offline concept hint`; the held-out offline AUROC/F1 benchmark remains the stronger feature/concept claim.
|
| 148 |
|
| 149 |
### Contrastive continuation preference
|
| 150 |
|
|
|
|
| 173 |
|
| 174 |
The trajectory view compares SAE reconstruction and sparsity/concentration diagnostics at the same prompt token across layers 4, 14 and 26. It does **not** equate feature IDs across layer-specific SAE dictionaries.
|
| 175 |
|
| 176 |
+
## v0.6 interface and navigation
|
| 177 |
|
| 178 |
+
v0.6 is designed so a reviewer does not need prior mechanistic-interpretability vocabulary to find the controls:
|
| 179 |
|
| 180 |
+
- a **Start here** tab explains the evidence workflow and glossary in plain language;
|
| 181 |
+
- a persistent **Current Workbench context** banner states the active prompt, layer and token after inspection;
|
| 182 |
+
- experiments that reuse that state explicitly say **Uses current Workbench context**;
|
| 183 |
+
- single-feature causal, dose-response, contrastive-preference, feature-set and evidence panels expose their own editable feature selectors instead of silently borrowing a hidden value;
|
| 184 |
+
- concept-guided discovery gives users a route to candidate features without knowing feature IDs in advance;
|
| 185 |
+
- headings now follow a conventional descending hierarchy and table headers use the same readable base scale as the surrounding interface;
|
| 186 |
+
- every native plot exposes Gradio's **fullscreen** control for zoomed reading and **export** control for PNG output;
|
| 187 |
+
- all action/copy buttons retain one muted-teal visual language and copy actions provide visible confirmation.
|
| 188 |
+
|
| 189 |
+
The centered 1600 px research canvas, bounded result-table heights, dynamic resize observer and explicit bottom padding from v0.5 remain in place for Hugging Face's embedded Space layout.
|
| 190 |
|
| 191 |
## Copy tables with headers
|
| 192 |
|
|
|
|
| 304 |
|
| 305 |
## UI design
|
| 306 |
|
| 307 |
+
v0.6 keeps the interface deliberately closer to a conventional research instrument than a rounded dashboard:
|
| 308 |
|
| 309 |
- Times / Liberation Serif-style formal typography;
|
| 310 |
- restrained teal, umber, stone, plum, and muted red chart accents;
|
|
|
|
| 356 |
python scripts/release_check.py
|
| 357 |
```
|
| 358 |
|
| 359 |
+
See [`docs/VALIDATION.md`](docs/VALIDATION.md). The v0.6 validation guide uses the **exact labels shown in the UI**, focuses on new-version acceptance/regression tests, and intentionally defers the comprehensive adversarial suite to the final release.
|
| 360 |
|
| 361 |
## Limitations
|
| 362 |
|
| 363 |
- SAE features are sparse directions, not guaranteed monosemantic concepts.
|
| 364 |
+
- Live candidate discovery and concept contrast are small controlled diagnostics, not a universal ontology or semantic labeling system.
|
| 365 |
+
- Completion-cue sensitivity diagnoses lexical/structural dependence but does not establish a feature's full semantics.
|
| 366 |
- Prompt-wide max pooling measures feature presence anywhere in the prompt and discards token order.
|
| 367 |
- Joint residual edits can be non-additive downstream without implying direct feature-feature interaction.
|
| 368 |
- Eight live random controls give only a coarse empirical tail probability.
|
|
|
|
| 372 |
## Resume-ready description
|
| 373 |
|
| 374 |
> **FeatureLens — Causal Interpretability Workbench** | PyTorch, Qwen3, Sparse Autoencoders, Mechanistic Interpretability, Gradio
|
| 375 |
+
> Built an SAE-based interpretability system for Qwen3-1.7B with held-out concept discovery, concept-guided candidate discovery, token/prompt-wide and completion-cue feature evidence, reconstruction-preserving single and multi-feature interventions, full-continuation and contrastive preference scoring, dose-response analysis, decoder-geometry/non-additivity diagnostics, and norm-matched random-control ensembles.
|
| 376 |
|
| 377 |
## Acknowledgements
|
| 378 |
|
app.py
CHANGED
|
@@ -38,12 +38,14 @@ CSS = r"""
|
|
| 38 |
.gradio-container input,
|
| 39 |
.gradio-container textarea,
|
| 40 |
.gradio-container select { font-size: 15.5px !important; }
|
| 41 |
-
.gradio-container table { font-size:
|
|
|
|
|
|
|
| 42 |
.gradio-container label, .gradio-container .label-wrap { font-size: 15px !important; }
|
| 43 |
.gradio-container p, .gradio-container li { font-size: 16px; }
|
| 44 |
-
.gradio-container h2 { font-size: 1.
|
| 45 |
-
.gradio-container h3 { font-size: 1.
|
| 46 |
-
.gradio-container h4 { font-size: 1.
|
| 47 |
.hero {
|
| 48 |
padding: 15px 2px 7px;
|
| 49 |
border-bottom: 1px solid var(--border-color-primary);
|
|
@@ -71,8 +73,9 @@ CSS = r"""
|
|
| 71 |
border-top: 1px solid var(--border-color-primary);
|
| 72 |
font-variant: small-caps;
|
| 73 |
letter-spacing: .045em;
|
| 74 |
-
font-size: .
|
| 75 |
-
|
|
|
|
| 76 |
}
|
| 77 |
.token-wrap { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px 2px 14px; line-height: 1.9; }
|
| 78 |
.token {
|
|
@@ -115,6 +118,25 @@ CSS = r"""
|
|
| 115 |
min-height: 110px;
|
| 116 |
}
|
| 117 |
.tabs, .tabitem { padding-bottom: 26px !important; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
@media (max-width: 900px) {
|
| 119 |
.gradio-container { width: 100% !important; padding-left: 12px !important; padding-right: 12px !important; }
|
| 120 |
}
|
|
@@ -409,6 +431,42 @@ def _contrastive_metrics_markdown(result) -> str:
|
|
| 409 |
)
|
| 410 |
|
| 411 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
@gpu(duration=30)
|
| 413 |
def analyze_prompt(prompt: str, layer: int, token_index: int, top_n: int):
|
| 414 |
try:
|
|
@@ -437,12 +495,15 @@ def analyze_prompt(prompt: str, layer: int, token_index: int, top_n: int):
|
|
| 437 |
table,
|
| 438 |
chart_df,
|
| 439 |
feature_update,
|
|
|
|
|
|
|
| 440 |
feature_set_update,
|
| 441 |
contrast_update,
|
| 442 |
gr.update(value=int(layer)),
|
| 443 |
_analysis_metrics_markdown(result),
|
| 444 |
location,
|
| 445 |
location,
|
|
|
|
| 446 |
_tsv(table),
|
| 447 |
)
|
| 448 |
except Exception as exc:
|
|
@@ -853,6 +914,65 @@ def run_contrastive_causal(
|
|
| 853 |
_raise_ui_error(exc)
|
| 854 |
|
| 855 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 856 |
def mode_help(mode: str):
|
| 857 |
if mode == "ablate":
|
| 858 |
return gr.update(value=0.0, interactive=False, label="Coefficient (unused for ablation)")
|
|
@@ -870,7 +990,7 @@ def set_mode_help(mode: str):
|
|
| 870 |
with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_width=True) as demo:
|
| 871 |
gr.HTML(
|
| 872 |
'<header class="hero">'
|
| 873 |
-
'<h1>FeatureLens <span style="font-size:.48em;opacity:.58;font-weight:400">v0.
|
| 874 |
'<div class="subtitle">Causal Interpretability Workbench</div>'
|
| 875 |
'<div class="metadata">Qwen3-1.7B-Base · Qwen-Scope residual SAEs · layers 4 / 14 / 26 · '
|
| 876 |
'32,768 features · TopK=50 · ZeroGPU</div>'
|
|
@@ -879,6 +999,45 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 879 |
'also causally influence the model’s behaviour?</div>'
|
| 880 |
)
|
| 881 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 882 |
with gr.Tab("Workbench"):
|
| 883 |
gr.HTML('<div class="section-rule">I. Inspect a prompt location</div>')
|
| 884 |
with gr.Row(equal_height=False):
|
|
@@ -942,6 +1101,7 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 942 |
x_title="Feature id",
|
| 943 |
y_title="Activation",
|
| 944 |
x_label_angle=-35,
|
|
|
|
| 945 |
height=330,
|
| 946 |
)
|
| 947 |
|
|
@@ -956,6 +1116,7 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 956 |
choices=[],
|
| 957 |
allow_custom_value=True,
|
| 958 |
label="Single feature id",
|
|
|
|
| 959 |
)
|
| 960 |
mode = gr.Dropdown(
|
| 961 |
choices=["ablate", "scale", "inject"],
|
|
@@ -1005,7 +1166,15 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1005 |
target_token_copy = _copy_button()
|
| 1006 |
|
| 1007 |
gr.HTML('<div class="section-rule">III. Single-feature scale dose-response</div>')
|
| 1008 |
-
with gr.Accordion("Scale dose-response", open=False):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1009 |
gr.Markdown(
|
| 1010 |
"Always a **scale** experiment: 0× = ablation, 1× = numerical no-edit reference, 2× = double "
|
| 1011 |
"the native coefficient. The six conditions are evaluated together, and all deltas are measured "
|
|
@@ -1033,11 +1202,20 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1033 |
title="Scale dose-response",
|
| 1034 |
x_title="Feature multiplier",
|
| 1035 |
y_title="Δ mean log p/token",
|
|
|
|
| 1036 |
height=330,
|
| 1037 |
)
|
| 1038 |
|
| 1039 |
gr.HTML('<div class="section-rule">IV. Contrastive continuation preference</div>')
|
| 1040 |
-
with gr.Accordion("Contrastive causal preference test", open=False):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1041 |
gr.Markdown(
|
| 1042 |
"Absolute target probability can move because an intervention broadly perturbs the distribution. "
|
| 1043 |
"This test asks the stricter question: **does the edit shift preference between two exact continuations?** "
|
|
@@ -1083,14 +1261,15 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1083 |
title="Preference between exact continuations",
|
| 1084 |
x_title="Execution condition",
|
| 1085 |
y_title="Sequence log-odds A−B",
|
|
|
|
| 1086 |
height=320,
|
| 1087 |
)
|
| 1088 |
|
| 1089 |
with gr.Tab("Feature sets"):
|
| 1090 |
gr.Markdown(
|
| 1091 |
"### Distributed sparse representations\n"
|
| 1092 |
-
"Run **Inspect sparse features** in the Workbench first.
|
| 1093 |
-
"
|
| 1094 |
)
|
| 1095 |
feature_set_location = gr.Markdown("No Workbench inspection has been run yet.")
|
| 1096 |
feature_set_ids = gr.Dropdown(
|
|
@@ -1182,6 +1361,7 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1182 |
title="Effect vs feature-set size",
|
| 1183 |
x_title="Number of jointly ablated features",
|
| 1184 |
y_title="Δ mean log p/token",
|
|
|
|
| 1185 |
height=330,
|
| 1186 |
)
|
| 1187 |
|
|
@@ -1215,12 +1395,14 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1215 |
x_title="Intervention condition",
|
| 1216 |
y_title="Δ mean log p/token",
|
| 1217 |
x_label_angle=-25,
|
|
|
|
| 1218 |
height=330,
|
| 1219 |
)
|
| 1220 |
|
| 1221 |
|
| 1222 |
gr.HTML('<div class="section-rule">Decoder geometry</div>')
|
| 1223 |
-
with gr.Accordion("Feature-set decoder geometry", open=False):
|
|
|
|
| 1224 |
gr.Markdown(
|
| 1225 |
"Joint causal effects can reflect both **decoder-direction geometry** and downstream non-linearity. "
|
| 1226 |
"This zero-extra-generation diagnostic measures pairwise decoder cosines and compares the actual "
|
|
@@ -1251,6 +1433,7 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1251 |
x_title="Feature pair",
|
| 1252 |
y_title="Cosine similarity",
|
| 1253 |
x_label_angle=-30,
|
|
|
|
| 1254 |
height=320,
|
| 1255 |
)
|
| 1256 |
|
|
@@ -1261,7 +1444,48 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1261 |
"across FeatureLens's seven controlled concept groups. The concept scan uses the feature's **maximum "
|
| 1262 |
"activation across non-padding tokens in each prompt**, avoiding arbitrary final-token comparisons."
|
| 1263 |
)
|
| 1264 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1265 |
with gr.Row(equal_height=True):
|
| 1266 |
contrast_feature_id = gr.Dropdown(
|
| 1267 |
choices=[],
|
|
@@ -1284,7 +1508,7 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1284 |
scale=2,
|
| 1285 |
)
|
| 1286 |
|
| 1287 |
-
gr.Markdown("###
|
| 1288 |
gr.Markdown(
|
| 1289 |
"Uses the prompt currently entered in **Workbench** and shows exactly which prompt tokens include the "
|
| 1290 |
"selected feature in the SAE TopK support."
|
|
@@ -1312,10 +1536,35 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1312 |
x_title="Prompt token",
|
| 1313 |
y_title="Activation",
|
| 1314 |
x_label_angle=-35,
|
|
|
|
| 1315 |
height=320,
|
| 1316 |
)
|
| 1317 |
|
| 1318 |
-
gr.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1319 |
contrast_btn = gr.Button("Run controlled concept contrast", variant="primary", elem_classes=["action-btn"])
|
| 1320 |
contrast_metrics = gr.Markdown()
|
| 1321 |
with gr.Row(equal_height=False):
|
|
@@ -1339,6 +1588,7 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1339 |
x_title="Concept",
|
| 1340 |
y_title="Mean max activation",
|
| 1341 |
x_label_angle=-25,
|
|
|
|
| 1342 |
height=330,
|
| 1343 |
)
|
| 1344 |
|
|
@@ -1346,7 +1596,7 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1346 |
gr.Markdown(
|
| 1347 |
"### Local and prompt-wide robustness\n"
|
| 1348 |
"The selected-token comparison is strict but can be misleading if the two chosen tokens play different "
|
| 1349 |
-
"semantic roles.
|
| 1350 |
"feature, take its maximum activation across all prompt tokens, then compare the two sparse profiles."
|
| 1351 |
)
|
| 1352 |
with gr.Row():
|
|
@@ -1399,6 +1649,7 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1399 |
x_title="Feature id",
|
| 1400 |
y_title="Activation",
|
| 1401 |
x_label_angle=-35,
|
|
|
|
| 1402 |
height=330,
|
| 1403 |
)
|
| 1404 |
|
|
@@ -1448,6 +1699,7 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1448 |
title="Representation trajectory",
|
| 1449 |
x_title="Layer",
|
| 1450 |
y_title="Normalized value",
|
|
|
|
| 1451 |
height=330,
|
| 1452 |
)
|
| 1453 |
|
|
@@ -1456,7 +1708,7 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1456 |
gr.Markdown(
|
| 1457 |
"The offline pipeline remains the source of held-out feature/concept AUROC + F1, reconstruction "
|
| 1458 |
"quality, paraphrase stability, dense residual linear probes, and causal intervention conclusions. "
|
| 1459 |
-
"v0.
|
| 1460 |
"the saved report should be generated only from real experiment artifacts."
|
| 1461 |
)
|
| 1462 |
|
|
@@ -1477,7 +1729,7 @@ $$h' = h + \sum_{i \in S}\Delta z_i d_i.$$
|
|
| 1477 |
|
| 1478 |
FeatureLens patches the delta into the **original residual**; it never replaces the residual with the complete SAE reconstruction.
|
| 1479 |
|
| 1480 |
-
### v0.
|
| 1481 |
|
| 1482 |
Batched experiments include an explicit **zero-edit row**. Causal effects are measured against that row rather than a separately executed baseline, which removes batch-vs-single floating-point drift from the measured effect. Random specificity uses an ensemble of norm-matched residual directions rather than one arbitrary seed.
|
| 1483 |
|
|
@@ -1485,13 +1737,14 @@ Batched experiments include an explicit **zero-edit row**. Causal effects are me
|
|
| 1485 |
|
| 1486 |
1. SAE reconstruction quality.
|
| 1487 |
2. Held-out feature/concept prediction.
|
| 1488 |
-
3.
|
| 1489 |
-
4.
|
| 1490 |
-
5.
|
| 1491 |
-
6.
|
| 1492 |
-
7.
|
| 1493 |
-
8.
|
| 1494 |
-
9.
|
|
|
|
| 1495 |
|
| 1496 |
Association, robustness, geometry, and intervention evidence remain separate claims.
|
| 1497 |
"""
|
|
@@ -1514,12 +1767,15 @@ Association, robustness, geometry, and intervention evidence remain separate cla
|
|
| 1514 |
feature_table,
|
| 1515 |
feature_plot,
|
| 1516 |
feature_id,
|
|
|
|
|
|
|
| 1517 |
feature_set_ids,
|
| 1518 |
contrast_feature_id,
|
| 1519 |
contrast_layer,
|
| 1520 |
analysis_metrics,
|
| 1521 |
feature_set_location,
|
| 1522 |
contrast_location,
|
|
|
|
| 1523 |
feature_tsv,
|
| 1524 |
],
|
| 1525 |
)
|
|
@@ -1539,13 +1795,13 @@ Association, robustness, geometry, and intervention evidence remain separate cla
|
|
| 1539 |
)
|
| 1540 |
dose_btn.click(
|
| 1541 |
run_dose_response,
|
| 1542 |
-
inputs=[prompt, layer, token_index,
|
| 1543 |
outputs=[dose_table, dose_plot, dose_metrics, dose_tsv],
|
| 1544 |
)
|
| 1545 |
contrastive_mode.change(mode_help, inputs=[contrastive_mode], outputs=[contrastive_coefficient])
|
| 1546 |
contrastive_btn.click(
|
| 1547 |
run_contrastive_causal,
|
| 1548 |
-
inputs=[prompt, layer, token_index,
|
| 1549 |
outputs=[contrastive_metrics, contrastive_table, contrastive_plot, contrastive_tsv],
|
| 1550 |
)
|
| 1551 |
set_mode.change(set_mode_help, inputs=[set_mode], outputs=[set_coefficient])
|
|
@@ -1579,6 +1835,22 @@ Association, robustness, geometry, and intervention evidence remain separate cla
|
|
| 1579 |
inputs=[contrast_feature_id, contrast_layer, contrast_n],
|
| 1580 |
outputs=[contrast_metrics, contrast_table, contrast_plot, contrast_tsv],
|
| 1581 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1582 |
para_btn.click(
|
| 1583 |
run_paraphrase_compare,
|
| 1584 |
inputs=[para_a, para_b, para_layer, para_idx_a, para_idx_b, para_top_n],
|
|
@@ -1603,6 +1875,8 @@ Association, robustness, geometry, and intervention evidence remain separate cla
|
|
| 1603 |
(geometry_copy, geometry_tsv),
|
| 1604 |
(trace_copy, trace_tsv),
|
| 1605 |
(contrast_copy, contrast_tsv),
|
|
|
|
|
|
|
| 1606 |
(para_copy, para_tsv),
|
| 1607 |
(trajectory_copy, trajectory_tsv),
|
| 1608 |
]:
|
|
|
|
| 38 |
.gradio-container input,
|
| 39 |
.gradio-container textarea,
|
| 40 |
.gradio-container select { font-size: 15.5px !important; }
|
| 41 |
+
.gradio-container table { font-size: 15.5px !important; }
|
| 42 |
+
.gradio-container table thead th, .gradio-container [role="columnheader"] { font-size: 16px !important; font-weight: 700 !important; }
|
| 43 |
+
.gradio-container table tbody td, .gradio-container [role="gridcell"] { font-size: 15.5px !important; }
|
| 44 |
.gradio-container label, .gradio-container .label-wrap { font-size: 15px !important; }
|
| 45 |
.gradio-container p, .gradio-container li { font-size: 16px; }
|
| 46 |
+
.gradio-container h2 { font-size: 1.72rem; line-height: 1.25; }
|
| 47 |
+
.gradio-container h3 { font-size: 1.46rem; line-height: 1.28; }
|
| 48 |
+
.gradio-container h4 { font-size: 1.24rem; line-height: 1.30; }
|
| 49 |
.hero {
|
| 50 |
padding: 15px 2px 7px;
|
| 51 |
border-bottom: 1px solid var(--border-color-primary);
|
|
|
|
| 73 |
border-top: 1px solid var(--border-color-primary);
|
| 74 |
font-variant: small-caps;
|
| 75 |
letter-spacing: .045em;
|
| 76 |
+
font-size: 1.12rem;
|
| 77 |
+
font-weight: 700;
|
| 78 |
+
opacity: .88;
|
| 79 |
}
|
| 80 |
.token-wrap { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px 2px 14px; line-height: 1.9; }
|
| 81 |
.token {
|
|
|
|
| 118 |
min-height: 110px;
|
| 119 |
}
|
| 120 |
.tabs, .tabitem { padding-bottom: 26px !important; }
|
| 121 |
+
.context-card {
|
| 122 |
+
border: 1px solid var(--border-color-primary);
|
| 123 |
+
border-left: 4px solid #708B86;
|
| 124 |
+
background: var(--background-fill-secondary);
|
| 125 |
+
padding: 10px 14px;
|
| 126 |
+
margin: 8px 0 16px;
|
| 127 |
+
border-radius: 2px;
|
| 128 |
+
}
|
| 129 |
+
.context-card p { margin: 0 !important; }
|
| 130 |
+
.start-card {
|
| 131 |
+
border: 1px solid var(--border-color-primary);
|
| 132 |
+
background: var(--background-fill-secondary);
|
| 133 |
+
padding: 14px 16px;
|
| 134 |
+
min-height: 150px;
|
| 135 |
+
border-radius: 2px;
|
| 136 |
+
}
|
| 137 |
+
.start-card h3 { margin-top: 0; }
|
| 138 |
+
.experiment-accordion > button { font-size: 1.18rem !important; font-weight: 700 !important; }
|
| 139 |
+
.graph-note { font-size: .95rem; opacity: .72; margin-top: 2px; }
|
| 140 |
@media (max-width: 900px) {
|
| 141 |
.gradio-container { width: 100% !important; padding-left: 12px !important; padding-right: 12px !important; }
|
| 142 |
}
|
|
|
|
| 431 |
)
|
| 432 |
|
| 433 |
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
def _discovery_metrics_markdown(result) -> str:
|
| 437 |
+
if not result.candidate_ids:
|
| 438 |
+
return (
|
| 439 |
+
f"No positively selective candidate features were found for **{result.concept}** at layer "
|
| 440 |
+
f"**{result.layer}** in this small live batch. Increase the sample or try another layer."
|
| 441 |
+
)
|
| 442 |
+
return (
|
| 443 |
+
f"Concept **{result.concept}** · layer **{result.layer}** · "
|
| 444 |
+
f"{result.prompts_per_concept} prompts/concept. \n"
|
| 445 |
+
f"Showing **{len(result.candidate_ids)}** features ranked by target-minus-other prompt-wide mean activation. \n\n"
|
| 446 |
+
"This is **candidate discovery**, not a semantic label. Use the offline held-out AUROC/F1 pipeline "
|
| 447 |
+
"before describing a feature as concept-selective."
|
| 448 |
+
)
|
| 449 |
+
|
| 450 |
+
|
| 451 |
+
def _cue_metrics_markdown(result) -> str:
|
| 452 |
+
return (
|
| 453 |
+
f"Feature **{result.feature_id}**, layer **{result.layer}** · active for "
|
| 454 |
+
f"**{result.active_cue_count}/{result.cue_count}** tested completion cues at the final token. \n"
|
| 455 |
+
"This helps distinguish a concept-associated feature from a lexical/structural completion-cue feature. "
|
| 456 |
+
"It is a controlled diagnostic, not a feature label."
|
| 457 |
+
)
|
| 458 |
+
|
| 459 |
+
|
| 460 |
+
def _global_context_markdown(prompt: str, layer: int, result) -> str:
|
| 461 |
+
token = result.tokens[result.token_index] if result.tokens else ""
|
| 462 |
+
short = prompt[:120] + ("…" if len(prompt) > 120 else "")
|
| 463 |
+
return (
|
| 464 |
+
"**Current Workbench context** — "
|
| 465 |
+
f"Prompt: `{short}` · layer **{int(layer)}** · token **{result.token_index}** ({token!r}). \n"
|
| 466 |
+
"Panels marked **Uses current Workbench context** inherit this prompt/layer/token. "
|
| 467 |
+
"Feature selectors remain editable inside each experiment."
|
| 468 |
+
)
|
| 469 |
+
|
| 470 |
@gpu(duration=30)
|
| 471 |
def analyze_prompt(prompt: str, layer: int, token_index: int, top_n: int):
|
| 472 |
try:
|
|
|
|
| 495 |
table,
|
| 496 |
chart_df,
|
| 497 |
feature_update,
|
| 498 |
+
gr.update(choices=choices, value=choices[0] if choices else None),
|
| 499 |
+
gr.update(choices=choices, value=choices[0] if choices else None),
|
| 500 |
feature_set_update,
|
| 501 |
contrast_update,
|
| 502 |
gr.update(value=int(layer)),
|
| 503 |
_analysis_metrics_markdown(result),
|
| 504 |
location,
|
| 505 |
location,
|
| 506 |
+
_global_context_markdown(prompt, int(layer), result),
|
| 507 |
_tsv(table),
|
| 508 |
)
|
| 509 |
except Exception as exc:
|
|
|
|
| 914 |
_raise_ui_error(exc)
|
| 915 |
|
| 916 |
|
| 917 |
+
|
| 918 |
+
@gpu(duration=35)
|
| 919 |
+
def run_concept_feature_discovery(concept: str, layer: int, prompts_per_concept: int, top_n: int):
|
| 920 |
+
try:
|
| 921 |
+
result = RUNTIME.concept_feature_discovery(
|
| 922 |
+
concept=concept,
|
| 923 |
+
layer=int(layer),
|
| 924 |
+
prompts_per_concept=int(prompts_per_concept),
|
| 925 |
+
top_n=int(top_n),
|
| 926 |
+
)
|
| 927 |
+
columns = [
|
| 928 |
+
"Rank",
|
| 929 |
+
"Feature id",
|
| 930 |
+
"Target mean max",
|
| 931 |
+
"Other mean max",
|
| 932 |
+
"Mean difference",
|
| 933 |
+
"Selectivity score",
|
| 934 |
+
"Target activation rate",
|
| 935 |
+
"Other activation rate",
|
| 936 |
+
"Target max activation",
|
| 937 |
+
]
|
| 938 |
+
table = pd.DataFrame(result.rows, columns=columns)
|
| 939 |
+
chart = pd.DataFrame(result.chart_rows, columns=["Feature", "Target − other mean"])
|
| 940 |
+
chart["Series"] = "Concept contrast"
|
| 941 |
+
choices = [str(fid) for fid in result.candidate_ids]
|
| 942 |
+
candidate_update = gr.update(choices=choices, value=choices[0] if choices else None)
|
| 943 |
+
return _discovery_metrics_markdown(result), table, chart, candidate_update, _tsv(table)
|
| 944 |
+
except Exception as exc:
|
| 945 |
+
_raise_ui_error(exc)
|
| 946 |
+
|
| 947 |
+
|
| 948 |
+
@gpu(duration=25)
|
| 949 |
+
def run_feature_cue_scan(feature_id: str, layer: int, prompt_stem: str, cue_text: str):
|
| 950 |
+
try:
|
| 951 |
+
if feature_id is None or str(feature_id).strip() == "":
|
| 952 |
+
raise ValueError("Choose a feature id first.")
|
| 953 |
+
cues = [line for line in str(cue_text).splitlines() if line.strip()]
|
| 954 |
+
result = RUNTIME.feature_cue_scan(
|
| 955 |
+
feature_id=int(float(feature_id)),
|
| 956 |
+
layer=int(layer),
|
| 957 |
+
prompt_stem=prompt_stem,
|
| 958 |
+
cues=cues,
|
| 959 |
+
)
|
| 960 |
+
columns = ["Cue", "Full prompt", "Final token", "Activation", "Active in TopK"]
|
| 961 |
+
table = pd.DataFrame(result.rows, columns=columns)
|
| 962 |
+
chart = pd.DataFrame(result.chart_rows, columns=["Cue", "Activation"])
|
| 963 |
+
chart["Series"] = "Cue response"
|
| 964 |
+
return _cue_metrics_markdown(result), table, chart, _tsv(table)
|
| 965 |
+
except Exception as exc:
|
| 966 |
+
_raise_ui_error(exc)
|
| 967 |
+
|
| 968 |
+
|
| 969 |
+
def use_candidate_feature(candidate_id: str):
|
| 970 |
+
if candidate_id is None or str(candidate_id).strip() == "":
|
| 971 |
+
raise gr.Error("Run concept-guided discovery and choose a candidate first.")
|
| 972 |
+
value = str(int(float(candidate_id)))
|
| 973 |
+
return value, value, value, value
|
| 974 |
+
|
| 975 |
+
|
| 976 |
def mode_help(mode: str):
|
| 977 |
if mode == "ablate":
|
| 978 |
return gr.update(value=0.0, interactive=False, label="Coefficient (unused for ablation)")
|
|
|
|
| 990 |
with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_width=True) as demo:
|
| 991 |
gr.HTML(
|
| 992 |
'<header class="hero">'
|
| 993 |
+
'<h1>FeatureLens <span style="font-size:.48em;opacity:.58;font-weight:400">v0.6</span></h1>'
|
| 994 |
'<div class="subtitle">Causal Interpretability Workbench</div>'
|
| 995 |
'<div class="metadata">Qwen3-1.7B-Base · Qwen-Scope residual SAEs · layers 4 / 14 / 26 · '
|
| 996 |
'32,768 features · TopK=50 · ZeroGPU</div>'
|
|
|
|
| 999 |
'also causally influence the model’s behaviour?</div>'
|
| 1000 |
)
|
| 1001 |
|
| 1002 |
+
global_context = gr.Markdown(
|
| 1003 |
+
"**Current Workbench context:** none yet. Go to **Workbench → I. Inspect a prompt location**, "
|
| 1004 |
+
"set the prompt/layer/token, then click **Inspect sparse features**.",
|
| 1005 |
+
elem_classes=["context-card"],
|
| 1006 |
+
)
|
| 1007 |
+
|
| 1008 |
+
with gr.Tab("Start here"):
|
| 1009 |
+
gr.Markdown(
|
| 1010 |
+
"## Start here\n"
|
| 1011 |
+
"FeatureLens is organized as a short evidence workflow. You do **not** need to know a feature id before "
|
| 1012 |
+
"starting: inspect a prompt first, or use **Feature evidence → Concept-guided candidate discovery**."
|
| 1013 |
+
)
|
| 1014 |
+
with gr.Row(equal_height=True):
|
| 1015 |
+
gr.HTML(
|
| 1016 |
+
'<div class="start-card"><h3>1. Choose a location</h3>'
|
| 1017 |
+
'<p>In <b>Workbench</b>, enter a prompt, choose a residual layer and token, then click '
|
| 1018 |
+
'<b>Inspect sparse features</b>. This establishes the shared Workbench context.</p></div>'
|
| 1019 |
+
)
|
| 1020 |
+
gr.HTML(
|
| 1021 |
+
'<div class="start-card"><h3>2. Choose evidence</h3>'
|
| 1022 |
+
'<p>Pick a feature in the experiment you want to run. Feature selectors are populated from the latest '
|
| 1023 |
+
'inspection but remain editable. Feature sets use 2–5 features.</p></div>'
|
| 1024 |
+
)
|
| 1025 |
+
gr.HTML(
|
| 1026 |
+
'<div class="start-card"><h3>3. Ask a causal question</h3>'
|
| 1027 |
+
'<p>Ablate/scale features, compare against norm-matched random controls, test dose response, '
|
| 1028 |
+
'preference shifts, robustness, geometry, or concept association.</p></div>'
|
| 1029 |
+
)
|
| 1030 |
+
gr.Markdown(
|
| 1031 |
+
"### Plain-language glossary\n"
|
| 1032 |
+
"- **Token:** a chunk of text the model processes.\n"
|
| 1033 |
+
"- **Residual layer:** a checkpoint in the model's internal computation.\n"
|
| 1034 |
+
"- **SAE feature:** one sparse direction used by the autoencoder to represent a residual activation.\n"
|
| 1035 |
+
"- **Ablate:** remove that feature's current contribution. **Scale:** multiply it.\n"
|
| 1036 |
+
"- **Random control:** an equally large residual edit in an unrelated direction.\n"
|
| 1037 |
+
"- **Teacher-forced score:** probability assigned to exact continuation text, even if greedy generation does not change.\n\n"
|
| 1038 |
+
"**Tables:** use fullscreen/copy controls. **Plots:** use the top-right fullscreen button to zoom and the export button for PNG."
|
| 1039 |
+
)
|
| 1040 |
+
|
| 1041 |
with gr.Tab("Workbench"):
|
| 1042 |
gr.HTML('<div class="section-rule">I. Inspect a prompt location</div>')
|
| 1043 |
with gr.Row(equal_height=False):
|
|
|
|
| 1101 |
x_title="Feature id",
|
| 1102 |
y_title="Activation",
|
| 1103 |
x_label_angle=-35,
|
| 1104 |
+
buttons=["fullscreen", "export"],
|
| 1105 |
height=330,
|
| 1106 |
)
|
| 1107 |
|
|
|
|
| 1116 |
choices=[],
|
| 1117 |
allow_custom_value=True,
|
| 1118 |
label="Single feature id",
|
| 1119 |
+
info="Populated from the latest inspection. You can choose another active feature or enter any valid id.",
|
| 1120 |
)
|
| 1121 |
mode = gr.Dropdown(
|
| 1122 |
choices=["ablate", "scale", "inject"],
|
|
|
|
| 1166 |
target_token_copy = _copy_button()
|
| 1167 |
|
| 1168 |
gr.HTML('<div class="section-rule">III. Single-feature scale dose-response</div>')
|
| 1169 |
+
with gr.Accordion("Scale dose-response", open=False, elem_classes=["experiment-accordion"]):
|
| 1170 |
+
gr.Markdown("### Scale dose-response")
|
| 1171 |
+
gr.Markdown("**Uses current Workbench prompt / layer / token.** Choose the feature for this experiment below.")
|
| 1172 |
+
dose_feature_id = gr.Dropdown(
|
| 1173 |
+
choices=[],
|
| 1174 |
+
allow_custom_value=True,
|
| 1175 |
+
label="Dose-response feature id",
|
| 1176 |
+
info="Populated from the latest Workbench inspection; editable here.",
|
| 1177 |
+
)
|
| 1178 |
gr.Markdown(
|
| 1179 |
"Always a **scale** experiment: 0× = ablation, 1× = numerical no-edit reference, 2× = double "
|
| 1180 |
"the native coefficient. The six conditions are evaluated together, and all deltas are measured "
|
|
|
|
| 1202 |
title="Scale dose-response",
|
| 1203 |
x_title="Feature multiplier",
|
| 1204 |
y_title="Δ mean log p/token",
|
| 1205 |
+
buttons=["fullscreen", "export"],
|
| 1206 |
height=330,
|
| 1207 |
)
|
| 1208 |
|
| 1209 |
gr.HTML('<div class="section-rule">IV. Contrastive continuation preference</div>')
|
| 1210 |
+
with gr.Accordion("Contrastive causal preference test", open=False, elem_classes=["experiment-accordion"]):
|
| 1211 |
+
gr.Markdown("### Contrastive causal preference test")
|
| 1212 |
+
gr.Markdown("**Uses current Workbench prompt / layer / token.** Select the feature explicitly below.")
|
| 1213 |
+
contrastive_feature_id = gr.Dropdown(
|
| 1214 |
+
choices=[],
|
| 1215 |
+
allow_custom_value=True,
|
| 1216 |
+
label="Contrastive feature id",
|
| 1217 |
+
info="Populated from the latest Workbench inspection; editable here.",
|
| 1218 |
+
)
|
| 1219 |
gr.Markdown(
|
| 1220 |
"Absolute target probability can move because an intervention broadly perturbs the distribution. "
|
| 1221 |
"This test asks the stricter question: **does the edit shift preference between two exact continuations?** "
|
|
|
|
| 1261 |
title="Preference between exact continuations",
|
| 1262 |
x_title="Execution condition",
|
| 1263 |
y_title="Sequence log-odds A−B",
|
| 1264 |
+
buttons=["fullscreen", "export"],
|
| 1265 |
height=320,
|
| 1266 |
)
|
| 1267 |
|
| 1268 |
with gr.Tab("Feature sets"):
|
| 1269 |
gr.Markdown(
|
| 1270 |
"### Distributed sparse representations\n"
|
| 1271 |
+
"Run **Inspect sparse features** in the Workbench first. **Uses current Workbench context:** prompt, layer, "
|
| 1272 |
+
"and token are inherited from that inspection. The feature set below is always editable."
|
| 1273 |
)
|
| 1274 |
feature_set_location = gr.Markdown("No Workbench inspection has been run yet.")
|
| 1275 |
feature_set_ids = gr.Dropdown(
|
|
|
|
| 1361 |
title="Effect vs feature-set size",
|
| 1362 |
x_title="Number of jointly ablated features",
|
| 1363 |
y_title="Δ mean log p/token",
|
| 1364 |
+
buttons=["fullscreen", "export"],
|
| 1365 |
height=330,
|
| 1366 |
)
|
| 1367 |
|
|
|
|
| 1395 |
x_title="Intervention condition",
|
| 1396 |
y_title="Δ mean log p/token",
|
| 1397 |
x_label_angle=-25,
|
| 1398 |
+
buttons=["fullscreen", "export"],
|
| 1399 |
height=330,
|
| 1400 |
)
|
| 1401 |
|
| 1402 |
|
| 1403 |
gr.HTML('<div class="section-rule">Decoder geometry</div>')
|
| 1404 |
+
with gr.Accordion("Feature-set decoder geometry", open=False, elem_classes=["experiment-accordion"]):
|
| 1405 |
+
gr.Markdown("### Feature-set decoder geometry")
|
| 1406 |
gr.Markdown(
|
| 1407 |
"Joint causal effects can reflect both **decoder-direction geometry** and downstream non-linearity. "
|
| 1408 |
"This zero-extra-generation diagnostic measures pairwise decoder cosines and compares the actual "
|
|
|
|
| 1433 |
x_title="Feature pair",
|
| 1434 |
y_title="Cosine similarity",
|
| 1435 |
x_label_angle=-30,
|
| 1436 |
+
buttons=["fullscreen", "export"],
|
| 1437 |
height=320,
|
| 1438 |
)
|
| 1439 |
|
|
|
|
| 1444 |
"across FeatureLens's seven controlled concept groups. The concept scan uses the feature's **maximum "
|
| 1445 |
"activation across non-padding tokens in each prompt**, avoiding arbitrary final-token comparisons."
|
| 1446 |
)
|
| 1447 |
+
gr.HTML('<div class="section-rule">A. Concept-guided candidate discovery</div>')
|
| 1448 |
+
gr.Markdown(
|
| 1449 |
+
"Don't know which feature id to inspect? Choose a controlled concept and let FeatureLens rank candidate "
|
| 1450 |
+
"features whose **prompt-wide maximum activation** is higher for that concept than for the other six groups. "
|
| 1451 |
+
"This is a live screening step, not a semantic label."
|
| 1452 |
+
)
|
| 1453 |
+
with gr.Row(equal_height=True):
|
| 1454 |
+
discovery_concept = gr.Dropdown(
|
| 1455 |
+
choices=[
|
| 1456 |
+
"code", "mathematics", "positive_sentiment", "negative_sentiment",
|
| 1457 |
+
"french_language", "factual_entities", "uncertainty"
|
| 1458 |
+
],
|
| 1459 |
+
value="mathematics",
|
| 1460 |
+
label="Target concept",
|
| 1461 |
+
)
|
| 1462 |
+
discovery_layer = gr.Dropdown(choices=list(SETTINGS.layers), value=SETTINGS.layers[1], label="Residual layer")
|
| 1463 |
+
discovery_n = gr.Slider(2, 6, value=SETTINGS.contrast_prompts_per_concept, step=1, label="Prompts per concept")
|
| 1464 |
+
discovery_top_n = gr.Slider(5, 20, value=12, step=1, label="Candidate features")
|
| 1465 |
+
discovery_btn = gr.Button("Discover concept-associated candidates", variant="primary", elem_classes=["action-btn"])
|
| 1466 |
+
discovery_metrics = gr.Markdown()
|
| 1467 |
+
with gr.Row(equal_height=False):
|
| 1468 |
+
with gr.Column(scale=3):
|
| 1469 |
+
discovery_table = gr.Dataframe(
|
| 1470 |
+
interactive=False, label="Candidate feature contrast", buttons=["fullscreen"], wrap=False, max_height=380
|
| 1471 |
+
)
|
| 1472 |
+
discovery_tsv = gr.Textbox(visible="hidden")
|
| 1473 |
+
discovery_copy = _copy_button()
|
| 1474 |
+
with gr.Column(scale=2):
|
| 1475 |
+
discovery_plot = gr.BarPlot(
|
| 1476 |
+
x="Feature", y="Target − other mean", color="Series",
|
| 1477 |
+
color_map={"Concept contrast": INK_TEAL}, title="Candidate concept contrast",
|
| 1478 |
+
x_title="Feature id", y_title="Target − other mean max activation", x_label_angle=-35,
|
| 1479 |
+
buttons=["fullscreen", "export"], height=330
|
| 1480 |
+
)
|
| 1481 |
+
with gr.Row(equal_height=True):
|
| 1482 |
+
discovery_candidate = gr.Dropdown(choices=[], label="Discovered candidate feature id", allow_custom_value=True, scale=3)
|
| 1483 |
+
use_candidate_btn = gr.Button("Use candidate in feature experiments", variant="primary", elem_classes=["action-btn"], scale=2)
|
| 1484 |
+
|
| 1485 |
+
gr.HTML('<div class="section-rule">B. Inspect one feature</div>')
|
| 1486 |
+
contrast_location = gr.Markdown(
|
| 1487 |
+
"**Activation trace uses the current Workbench prompt.** The controlled concept scan below uses its own balanced prompt set."
|
| 1488 |
+
)
|
| 1489 |
with gr.Row(equal_height=True):
|
| 1490 |
contrast_feature_id = gr.Dropdown(
|
| 1491 |
choices=[],
|
|
|
|
| 1508 |
scale=2,
|
| 1509 |
)
|
| 1510 |
|
| 1511 |
+
gr.Markdown("### Activation trace across the current Workbench prompt")
|
| 1512 |
gr.Markdown(
|
| 1513 |
"Uses the prompt currently entered in **Workbench** and shows exactly which prompt tokens include the "
|
| 1514 |
"selected feature in the SAE TopK support."
|
|
|
|
| 1536 |
x_title="Prompt token",
|
| 1537 |
y_title="Activation",
|
| 1538 |
x_label_angle=-35,
|
| 1539 |
+
buttons=["fullscreen", "export"],
|
| 1540 |
height=320,
|
| 1541 |
)
|
| 1542 |
|
| 1543 |
+
gr.HTML('<div class="section-rule">C. Completion-cue sensitivity</div>')
|
| 1544 |
+
gr.Markdown(
|
| 1545 |
+
"Feature 22632 in the current math example fires only on the final `is` token. This scan tests whether a "
|
| 1546 |
+
"selected feature responds to **completion cues themselves** rather than the underlying concept. Enter a stem "
|
| 1547 |
+
"and one cue per line; FeatureLens appends each cue and measures the feature at the final token."
|
| 1548 |
+
)
|
| 1549 |
+
with gr.Row(equal_height=True):
|
| 1550 |
+
cue_stem = gr.Textbox(label="Prompt stem", value="The derivative of x squared", lines=2, scale=3)
|
| 1551 |
+
cue_text = gr.Textbox(label="Completion cues (one per line)", value="is\n=\n:\nequals\ntherefore", lines=5, scale=2)
|
| 1552 |
+
cue_btn = gr.Button("Run completion-cue scan", variant="primary", elem_classes=["action-btn"])
|
| 1553 |
+
cue_metrics = gr.Markdown()
|
| 1554 |
+
with gr.Row(equal_height=False):
|
| 1555 |
+
with gr.Column(scale=3):
|
| 1556 |
+
cue_table = gr.Dataframe(interactive=False, label="Feature response by completion cue", buttons=["fullscreen"], wrap=False, max_height=340)
|
| 1557 |
+
cue_tsv = gr.Textbox(visible="hidden")
|
| 1558 |
+
cue_copy = _copy_button()
|
| 1559 |
+
with gr.Column(scale=2):
|
| 1560 |
+
cue_plot = gr.BarPlot(
|
| 1561 |
+
x="Cue", y="Activation", color="Series", color_map={"Cue response": INK_UMBER},
|
| 1562 |
+
title="Completion-cue feature response", x_title="Cue", y_title="Final-token activation",
|
| 1563 |
+
buttons=["fullscreen", "export"], height=320
|
| 1564 |
+
)
|
| 1565 |
+
|
| 1566 |
+
gr.HTML('<div class="section-rule">D. Controlled concept contrast for selected feature</div>')
|
| 1567 |
+
gr.Markdown("### Prompt-wide controlled concept contrast")
|
| 1568 |
contrast_btn = gr.Button("Run controlled concept contrast", variant="primary", elem_classes=["action-btn"])
|
| 1569 |
contrast_metrics = gr.Markdown()
|
| 1570 |
with gr.Row(equal_height=False):
|
|
|
|
| 1588 |
x_title="Concept",
|
| 1589 |
y_title="Mean max activation",
|
| 1590 |
x_label_angle=-25,
|
| 1591 |
+
buttons=["fullscreen", "export"],
|
| 1592 |
height=330,
|
| 1593 |
)
|
| 1594 |
|
|
|
|
| 1596 |
gr.Markdown(
|
| 1597 |
"### Local and prompt-wide robustness\n"
|
| 1598 |
"The selected-token comparison is strict but can be misleading if the two chosen tokens play different "
|
| 1599 |
+
"semantic roles. FeatureLens reports a **prompt-wide max-pooled feature profile**: for every SAE "
|
| 1600 |
"feature, take its maximum activation across all prompt tokens, then compare the two sparse profiles."
|
| 1601 |
)
|
| 1602 |
with gr.Row():
|
|
|
|
| 1649 |
x_title="Feature id",
|
| 1650 |
y_title="Activation",
|
| 1651 |
x_label_angle=-35,
|
| 1652 |
+
buttons=["fullscreen", "export"],
|
| 1653 |
height=330,
|
| 1654 |
)
|
| 1655 |
|
|
|
|
| 1699 |
title="Representation trajectory",
|
| 1700 |
x_title="Layer",
|
| 1701 |
y_title="Normalized value",
|
| 1702 |
+
buttons=["fullscreen", "export"],
|
| 1703 |
height=330,
|
| 1704 |
)
|
| 1705 |
|
|
|
|
| 1708 |
gr.Markdown(
|
| 1709 |
"The offline pipeline remains the source of held-out feature/concept AUROC + F1, reconstruction "
|
| 1710 |
"quality, paraphrase stability, dense residual linear probes, and causal intervention conclusions. "
|
| 1711 |
+
"v0.6 strengthens the live workbench with batch-context null references and random-control ensembles; "
|
| 1712 |
"the saved report should be generated only from real experiment artifacts."
|
| 1713 |
)
|
| 1714 |
|
|
|
|
| 1729 |
|
| 1730 |
FeatureLens patches the delta into the **original residual**; it never replaces the residual with the complete SAE reconstruction.
|
| 1731 |
|
| 1732 |
+
### v0.6 control discipline
|
| 1733 |
|
| 1734 |
Batched experiments include an explicit **zero-edit row**. Causal effects are measured against that row rather than a separately executed baseline, which removes batch-vs-single floating-point drift from the measured effect. Random specificity uses an ensemble of norm-matched residual directions rather than one arbitrary seed.
|
| 1735 |
|
|
|
|
| 1737 |
|
| 1738 |
1. SAE reconstruction quality.
|
| 1739 |
2. Held-out feature/concept prediction.
|
| 1740 |
+
3. Concept-guided candidate discovery and selected-feature concept contrast.
|
| 1741 |
+
4. Token-local activation traces and completion-cue sensitivity.
|
| 1742 |
+
5. Local and prompt-wide paraphrase robustness.
|
| 1743 |
+
6. Single-feature causal intervention and dose-response.
|
| 1744 |
+
7. Contrastive continuation preference under intervention.
|
| 1745 |
+
8. Joint feature-set intervention and set-size sensitivity.
|
| 1746 |
+
9. Decoder-direction geometry and individual-vs-joint non-additivity.
|
| 1747 |
+
10. Specificity relative to norm-matched random controls.
|
| 1748 |
|
| 1749 |
Association, robustness, geometry, and intervention evidence remain separate claims.
|
| 1750 |
"""
|
|
|
|
| 1767 |
feature_table,
|
| 1768 |
feature_plot,
|
| 1769 |
feature_id,
|
| 1770 |
+
dose_feature_id,
|
| 1771 |
+
contrastive_feature_id,
|
| 1772 |
feature_set_ids,
|
| 1773 |
contrast_feature_id,
|
| 1774 |
contrast_layer,
|
| 1775 |
analysis_metrics,
|
| 1776 |
feature_set_location,
|
| 1777 |
contrast_location,
|
| 1778 |
+
global_context,
|
| 1779 |
feature_tsv,
|
| 1780 |
],
|
| 1781 |
)
|
|
|
|
| 1795 |
)
|
| 1796 |
dose_btn.click(
|
| 1797 |
run_dose_response,
|
| 1798 |
+
inputs=[prompt, layer, token_index, dose_feature_id, target_text],
|
| 1799 |
outputs=[dose_table, dose_plot, dose_metrics, dose_tsv],
|
| 1800 |
)
|
| 1801 |
contrastive_mode.change(mode_help, inputs=[contrastive_mode], outputs=[contrastive_coefficient])
|
| 1802 |
contrastive_btn.click(
|
| 1803 |
run_contrastive_causal,
|
| 1804 |
+
inputs=[prompt, layer, token_index, contrastive_feature_id, contrastive_mode, contrastive_coefficient, contrastive_a, contrastive_b],
|
| 1805 |
outputs=[contrastive_metrics, contrastive_table, contrastive_plot, contrastive_tsv],
|
| 1806 |
)
|
| 1807 |
set_mode.change(set_mode_help, inputs=[set_mode], outputs=[set_coefficient])
|
|
|
|
| 1835 |
inputs=[contrast_feature_id, contrast_layer, contrast_n],
|
| 1836 |
outputs=[contrast_metrics, contrast_table, contrast_plot, contrast_tsv],
|
| 1837 |
)
|
| 1838 |
+
discovery_btn.click(
|
| 1839 |
+
run_concept_feature_discovery,
|
| 1840 |
+
inputs=[discovery_concept, discovery_layer, discovery_n, discovery_top_n],
|
| 1841 |
+
outputs=[discovery_metrics, discovery_table, discovery_plot, discovery_candidate, discovery_tsv],
|
| 1842 |
+
)
|
| 1843 |
+
use_candidate_btn.click(
|
| 1844 |
+
use_candidate_feature,
|
| 1845 |
+
inputs=[discovery_candidate],
|
| 1846 |
+
outputs=[feature_id, dose_feature_id, contrastive_feature_id, contrast_feature_id],
|
| 1847 |
+
queue=False,
|
| 1848 |
+
)
|
| 1849 |
+
cue_btn.click(
|
| 1850 |
+
run_feature_cue_scan,
|
| 1851 |
+
inputs=[contrast_feature_id, contrast_layer, cue_stem, cue_text],
|
| 1852 |
+
outputs=[cue_metrics, cue_table, cue_plot, cue_tsv],
|
| 1853 |
+
)
|
| 1854 |
para_btn.click(
|
| 1855 |
run_paraphrase_compare,
|
| 1856 |
inputs=[para_a, para_b, para_layer, para_idx_a, para_idx_b, para_top_n],
|
|
|
|
| 1875 |
(geometry_copy, geometry_tsv),
|
| 1876 |
(trace_copy, trace_tsv),
|
| 1877 |
(contrast_copy, contrast_tsv),
|
| 1878 |
+
(discovery_copy, discovery_tsv),
|
| 1879 |
+
(cue_copy, cue_tsv),
|
| 1880 |
(para_copy, para_tsv),
|
| 1881 |
(trajectory_copy, trajectory_tsv),
|
| 1882 |
]:
|
docs/HF_DEPLOY.md
CHANGED
|
@@ -15,8 +15,11 @@ The live app does not need every SAE layer from the full repository.
|
|
| 15 |
|
| 16 |
## GPU-decorated actions
|
| 17 |
|
| 18 |
-
Current v0.
|
| 19 |
|
|
|
|
|
|
|
|
|
|
| 20 |
- **Inspect sparse features**;
|
| 21 |
- **Run single-feature causal test**;
|
| 22 |
- **Run scale dose-response**;
|
|
|
|
| 15 |
|
| 16 |
## GPU-decorated actions
|
| 17 |
|
| 18 |
+
Current v0.6 live actions include:
|
| 19 |
|
| 20 |
+
- concept-guided candidate feature discovery and candidate reuse;
|
| 21 |
+
- completion-cue sensitivity scans;
|
| 22 |
+
- plot fullscreen/export controls and a persistent Workbench context banner;
|
| 23 |
- **Inspect sparse features**;
|
| 24 |
- **Run single-feature causal test**;
|
| 25 |
- **Run scale dose-response**;
|
docs/METHODOLOGY.md
CHANGED
|
@@ -264,7 +264,7 @@ The selected-token and prompt-wide metrics answer different questions and are in
|
|
| 264 |
|
| 265 |
## Feature-token activation trace
|
| 266 |
|
| 267 |
-
For a selected SAE feature and layer,
|
| 268 |
|
| 269 |
The trace reports activation at each token, active-token count, and the peak token. A zero entry means that feature is not present in that token's TopK SAE support.
|
| 270 |
|
|
@@ -274,7 +274,7 @@ The trace reports activation at each token, active-token count, and the peak tok
|
|
| 274 |
|
| 275 |
The live **Feature evidence** tab also provides an exploratory concept contrast for one selected feature.
|
| 276 |
|
| 277 |
-
It samples a small balanced batch from the same seven controlled concept groups, using only one wording from each paraphrase pair. Earlier versions sampled only each prompt's final token, which can miss a feature that is active elsewhere in the prompt.
|
| 278 |
|
| 279 |
```text
|
| 280 |
score(feature, prompt) = max over non-padding prompt tokens of z_feature
|
|
@@ -346,3 +346,32 @@ It then uses:
|
|
| 346 |
A large point-estimate ratio alone is not sufficient for a strong causal-specificity narrative if paired uncertainty remains weak.
|
| 347 |
|
| 348 |
FeatureLens keeps the raw rows even when the resulting conclusion is null, mixed, or contrary to the original hypothesis.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
|
| 265 |
## Feature-token activation trace
|
| 266 |
|
| 267 |
+
For a selected SAE feature and layer, FeatureLens can encode every prompt-token residual and report the feature's TopK activation token by token. This answers a basic localization question that a single selected-token view cannot: is the feature concentrated at one syntactic/semantic position, or does it recur across the prompt?
|
| 268 |
|
| 269 |
The trace reports activation at each token, active-token count, and the peak token. A zero entry means that feature is not present in that token's TopK SAE support.
|
| 270 |
|
|
|
|
| 274 |
|
| 275 |
The live **Feature evidence** tab also provides an exploratory concept contrast for one selected feature.
|
| 276 |
|
| 277 |
+
It samples a small balanced batch from the same seven controlled concept groups, using only one wording from each paraphrase pair. Earlier versions sampled only each prompt's final token, which can miss a feature that is active elsewhere in the prompt. the current live scan instead defines each prompt-level feature score as:
|
| 278 |
|
| 279 |
```text
|
| 280 |
score(feature, prompt) = max over non-padding prompt tokens of z_feature
|
|
|
|
| 346 |
A large point-estimate ratio alone is not sufficient for a strong causal-specificity narrative if paired uncertainty remains weak.
|
| 347 |
|
| 348 |
FeatureLens keeps the raw rows even when the resulting conclusion is null, mixed, or contrary to the original hypothesis.
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
## v0.6 live candidate discovery
|
| 352 |
+
|
| 353 |
+
The selected-feature concept contrast asks **given a feature, where does it activate?** v0.6 adds the reverse live question: **given a controlled concept, which SAE features are plausible candidates to investigate?**
|
| 354 |
+
|
| 355 |
+
For a balanced batch with `n` prompts from each of the seven controlled groups, FeatureLens encodes every non-padding token and forms a prompt-wide feature profile by taking the maximum TopK activation of each SAE feature over the prompt. For target concept `c` and feature `f`, it reports:
|
| 356 |
+
|
| 357 |
+
```text
|
| 358 |
+
target_mean(f) = mean prompt-wide max over prompts in c
|
| 359 |
+
other_mean(f) = mean prompt-wide max over prompts outside c
|
| 360 |
+
mean_difference(f) = target_mean(f) - other_mean(f)
|
| 361 |
+
selectivity(f) = mean_difference(f) / (target_mean(f) + other_mean(f) + eps)
|
| 362 |
+
```
|
| 363 |
+
|
| 364 |
+
Candidates are ranked by positive `mean_difference`. The table also reports target/other activation rates and target maximum activation. This is intentionally **not a held-out labeler**: the same small batch is used for live screening. Semantic claims still require the offline grouped train/test feature evaluation.
|
| 365 |
+
|
| 366 |
+
## v0.6 completion-cue sensitivity
|
| 367 |
+
|
| 368 |
+
The v0.5 math example showed feature `22632` active only at the final `is` token. That pattern motivates a lexical/structural control. For one prompt stem and a user-supplied list of completion cues, FeatureLens appends each cue, encodes the resulting prompt, and measures the selected feature at the final non-padding token.
|
| 369 |
+
|
| 370 |
+
The cue scan is intended to distinguish hypotheses such as:
|
| 371 |
+
|
| 372 |
+
- concept-linked activation;
|
| 373 |
+
- lexical activation tied to a particular token such as `is`;
|
| 374 |
+
- structural activation at a completion boundary;
|
| 375 |
+
- broad activation across several continuation cues.
|
| 376 |
+
|
| 377 |
+
It is a controlled diagnostic only. A cue response cannot establish the feature's complete semantics.
|
docs/VALIDATION.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
| 1 |
-
# FeatureLens v0.
|
| 2 |
|
| 3 |
-
This
|
| 4 |
|
| 5 |
-
|
|
|
|
|
|
|
| 6 |
|
| 7 |
From the repository root:
|
| 8 |
|
|
@@ -16,50 +18,33 @@ python3 scripts/release_check.py
|
|
| 16 |
Expected:
|
| 17 |
|
| 18 |
```text
|
| 19 |
-
|
| 20 |
-
FeatureLens UI launch smoke: PASS
|
| 21 |
FeatureLens release check: PASS
|
| 22 |
discovery prompts: 224
|
| 23 |
causal tasks: 28
|
| 24 |
layers: [4, 14, 26]
|
| 25 |
feature-set sizes: [1, 3, 5]
|
| 26 |
random controls: 8
|
| 27 |
-
release: v0.
|
| 28 |
```
|
| 29 |
|
| 30 |
-
|
| 31 |
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
-
|
| 35 |
|
| 36 |
-
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
```text
|
| 41 |
-
* Running on local URL: http://0.0.0.0:7860
|
| 42 |
-
```
|
| 43 |
-
|
| 44 |
-
There should be no SSR line, un-awaited coroutine warning, or theme/font traceback.
|
| 45 |
-
|
| 46 |
-
### Layout acceptance
|
| 47 |
-
|
| 48 |
-
On a desktop-width browser:
|
| 49 |
-
|
| 50 |
-
- The application should be **centered**, not pinned to the left edge.
|
| 51 |
-
- The usable canvas should expand substantially farther across a wide monitor than v0.4.
|
| 52 |
-
- Explicit action buttons and **Copy table with headers** buttons should use the same muted teal treatment.
|
| 53 |
-
- Body copy, labels, controls, and headings should use a consistent serif-oriented typography system.
|
| 54 |
-
- Tables should use the available width before horizontal scrolling is needed.
|
| 55 |
-
|
| 56 |
-
The widest research tables may still need horizontal scrolling; that is preferable to wrapping numerical headings into unreadable multi-line columns.
|
| 57 |
-
|
| 58 |
-
---
|
| 59 |
-
|
| 60 |
-
## C. Core Workbench inspection
|
| 61 |
-
|
| 62 |
-
In **Workbench** use:
|
| 63 |
|
| 64 |
```text
|
| 65 |
Prompt: The derivative of x squared is
|
|
@@ -70,239 +55,170 @@ Displayed active features: 12
|
|
| 70 |
|
| 71 |
Click **Inspect sparse features**.
|
| 72 |
|
| 73 |
-
Expected regression values
|
| 74 |
|
| 75 |
```text
|
| 76 |
-
|
| 77 |
-
|
|
|
|
| 78 |
NMSE: 0.1874
|
| 79 |
Top-5 activation mass: 26.7%
|
| 80 |
-
Top feature: 22632
|
| 81 |
-
Top activation: 32.90625
|
| 82 |
```
|
| 83 |
|
| 84 |
-
The **
|
| 85 |
-
|
| 86 |
-
### Copy feedback
|
| 87 |
|
| 88 |
-
|
| 89 |
|
| 90 |
-
|
| 91 |
|
| 92 |
-
|
| 93 |
-
```text
|
| 94 |
-
Rank<TAB>Feature id<TAB>Activation<TAB>Offline concept hint
|
| 95 |
-
```
|
| 96 |
-
2. The clicked button briefly changes to:
|
| 97 |
-
```text
|
| 98 |
-
✓ Copied with headers
|
| 99 |
-
```
|
| 100 |
-
and then returns to **Copy table with headers**.
|
| 101 |
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
## D. Dynamic-height / embedded scrolling regression
|
| 105 |
-
|
| 106 |
-
Still in **Workbench**, select feature `22632` and run:
|
| 107 |
|
| 108 |
```text
|
| 109 |
-
|
| 110 |
-
Target continuation (optional): 2x
|
| 111 |
-
Greedy generation length: 8
|
| 112 |
```
|
| 113 |
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
After the result expands, **do not switch tabs**.
|
| 117 |
-
|
| 118 |
-
Immediately try to scroll to:
|
| 119 |
|
| 120 |
-
|
| 121 |
-
- **IV. Contrastive continuation preference**
|
| 122 |
-
- and eventually the bottom of the Workbench tab.
|
| 123 |
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
| 129 |
|
| 130 |
-
##
|
| 131 |
|
| 132 |
-
Open **
|
| 133 |
|
| 134 |
-
|
| 135 |
|
| 136 |
```text
|
| 137 |
-
|
| 138 |
-
Δ feature coefficient: 0.0
|
| 139 |
-
Perturbation L2: 0.0
|
| 140 |
-
Δ mean log p/token: 0.0
|
| 141 |
-
Δ sequence log p: 0.0
|
| 142 |
-
Next-token JS: 0.0
|
| 143 |
```
|
| 144 |
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
---
|
| 148 |
-
|
| 149 |
-
## F. Contrastive continuation preference — new v0.5 test
|
| 150 |
-
|
| 151 |
-
In **Workbench → IV. Contrastive continuation preference**, use:
|
| 152 |
|
| 153 |
```text
|
|
|
|
| 154 |
Continuation A (preferred): 2x
|
| 155 |
Continuation B (comparison): x
|
| 156 |
Contrastive intervention: ablate
|
| 157 |
```
|
| 158 |
|
| 159 |
-
Use feature `22632`, layer `14`, prompt token `5` inherited from the Workbench.
|
| 160 |
-
|
| 161 |
Click **Run contrastive preference test**.
|
| 162 |
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
- **Contrastive continuation scores** with two rows: `A (preferred)` and `B (comparison)`.
|
| 166 |
-
- Exact sequence log-probability and mean log-probability/token for both continuations.
|
| 167 |
-
- Baseline A−B sequence log-odds.
|
| 168 |
-
- SAE-edited A−B sequence log-odds.
|
| 169 |
-
- Causal A−B log-odds shift.
|
| 170 |
-
- 8-control random magnitude mean/std, specificity ratio, and empirical tail p.
|
| 171 |
-
- **Preference between exact continuations** chart.
|
| 172 |
|
| 173 |
-
|
| 174 |
|
| 175 |
-
|
| 176 |
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
## G. Feature-token activation trace — new v0.5 test
|
| 180 |
-
|
| 181 |
-
Open **Feature evidence**. The Workbench inspection should have populated:
|
| 182 |
|
| 183 |
```text
|
| 184 |
-
|
| 185 |
Residual layer: 14
|
|
|
|
|
|
|
| 186 |
```
|
| 187 |
|
| 188 |
-
Click **
|
| 189 |
-
|
| 190 |
-
For the canonical prompt, the output should contain one row per prompt token. The final selected token (`is`, token index `5`) should reproduce the known feature activation approximately:
|
| 191 |
-
|
| 192 |
-
```text
|
| 193 |
-
Feature 22632 at token 5 ≈ 32.90625
|
| 194 |
-
```
|
| 195 |
-
|
| 196 |
-
Expected outputs:
|
| 197 |
|
| 198 |
-
|
| 199 |
-
- peak token position and activation;
|
| 200 |
-
- **Feature activation by prompt token** table;
|
| 201 |
-
- **Feature activation across prompt tokens** chart.
|
| 202 |
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
|
| 205 |
-
|
| 206 |
|
| 207 |
-
##
|
| 208 |
|
| 209 |
-
|
| 210 |
|
| 211 |
```text
|
| 212 |
-
|
| 213 |
-
Residual layer: 14
|
| 214 |
-
Prompts per concept: 4
|
| 215 |
```
|
| 216 |
|
| 217 |
-
|
| 218 |
|
| 219 |
-
|
|
|
|
|
|
|
|
|
|
| 220 |
|
| 221 |
-
|
| 222 |
|
| 223 |
-
|
| 224 |
-
Concept
|
| 225 |
-
Prompts
|
| 226 |
-
Mean prompt-wide max
|
| 227 |
-
Median prompt-wide max
|
| 228 |
-
Prompt activation rate
|
| 229 |
-
Mean when active
|
| 230 |
-
Max activation
|
| 231 |
-
```
|
| 232 |
-
|
| 233 |
-
Two outcomes are valid:
|
| 234 |
-
|
| 235 |
-
### If at least one sampled prompt activates the feature
|
| 236 |
-
The UI may report a highest-mean concept and a runner-up ratio.
|
| 237 |
|
| 238 |
-
|
| 239 |
-
The UI must say that the feature was **inactive in every sampled prompt**. It must **not** report `code` (or any other concept) as a winner and must not print an undefined winner ratio.
|
| 240 |
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
## I. Feature-set decoder geometry — new v0.5 test
|
| 244 |
-
|
| 245 |
-
Open **Feature sets** after the canonical Workbench inspection. Select the top three features:
|
| 246 |
|
| 247 |
```text
|
| 248 |
-
22632
|
| 249 |
-
|
| 250 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
```
|
| 252 |
|
| 253 |
-
|
| 254 |
|
| 255 |
-
|
| 256 |
|
| 257 |
-
|
| 258 |
-
- decoder cosine for each pair;
|
| 259 |
-
- mean absolute decoder cosine;
|
| 260 |
-
- maximum absolute decoder cosine;
|
| 261 |
-
- activation-weighted joint-ablation L2;
|
| 262 |
-
- independent-direction root-sum-square L2 reference;
|
| 263 |
-
- geometry ratio.
|
| 264 |
|
| 265 |
-
|
| 266 |
|
| 267 |
-
```
|
| 268 |
-
ratio < 1 -> net geometric cancellation
|
| 269 |
-
ratio ≈ 1 -> approximately independent/orthogonal net norm
|
| 270 |
-
ratio > 1 -> net geometric alignment
|
| 271 |
-
```
|
| 272 |
|
| 273 |
-
|
| 274 |
|
| 275 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
|
| 277 |
-
|
| 278 |
|
| 279 |
-
|
| 280 |
|
| 281 |
-
|
| 282 |
-
2. **Run individual-vs-joint decomposition** still produces three individual rows plus one joint row for the selected top three features.
|
| 283 |
-
3. In **Paraphrase robustness**, identical prompt vs identical prompt should still return:
|
| 284 |
-
```text
|
| 285 |
-
selected-token Jaccard = 1.000
|
| 286 |
-
selected-token cosine = 1.000
|
| 287 |
-
prompt-wide Jaccard = 1.000
|
| 288 |
-
prompt-wide cosine = 1.000
|
| 289 |
-
```
|
| 290 |
-
4. **Layer trajectory → Compare layers** still returns layers `4`, `14`, and `26`.
|
| 291 |
|
| 292 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
|
| 294 |
-
## Deferred final
|
| 295 |
|
| 296 |
-
|
| 297 |
|
| 298 |
-
- empty prompts
|
| 299 |
-
-
|
| 300 |
-
- feature
|
| 301 |
-
- inactive feature ablation
|
| 302 |
-
- whitespace-sensitive
|
| 303 |
-
-
|
| 304 |
-
-
|
|
|
|
| 305 |
- long-prompt truncation;
|
| 306 |
-
- rapid repeated clicks /
|
| 307 |
-
- narrow/mobile layout;
|
| 308 |
-
-
|
|
|
|
| 1 |
+
# FeatureLens v0.6 validation
|
| 2 |
|
| 3 |
+
This guide tests the **new v0.6 behavior** plus a few high-value regressions. The comprehensive edge/adversarial suite remains deferred until the final hardening release, as agreed.
|
| 4 |
|
| 5 |
+
Use the exact UI labels below.
|
| 6 |
+
|
| 7 |
+
## A. Local release gates
|
| 8 |
|
| 9 |
From the repository root:
|
| 10 |
|
|
|
|
| 18 |
Expected:
|
| 19 |
|
| 20 |
```text
|
| 21 |
+
40 passed
|
| 22 |
+
FeatureLens UI launch smoke: PASS
|
| 23 |
FeatureLens release check: PASS
|
| 24 |
discovery prompts: 224
|
| 25 |
causal tasks: 28
|
| 26 |
layers: [4, 14, 26]
|
| 27 |
feature-set sizes: [1, 3, 5]
|
| 28 |
random controls: 8
|
| 29 |
+
release: v0.6.0
|
| 30 |
```
|
| 31 |
|
| 32 |
+
## B. Navigation / typography / plot controls
|
| 33 |
|
| 34 |
+
1. Open **Start here**.
|
| 35 |
+
2. Confirm the three workflow cards and the plain-language glossary are readable without tiny heading text.
|
| 36 |
+
3. Open **Workbench**.
|
| 37 |
+
4. Confirm the hierarchy is visually descending: section title > subsection heading > explanatory body text.
|
| 38 |
+
5. Confirm result-table column headers are not noticeably smaller than normal body text.
|
| 39 |
+
6. After any plot is produced, use its top-right **fullscreen** button. The plot should open large enough for labels to be read comfortably.
|
| 40 |
+
7. Confirm the adjacent **export** control produces a PNG.
|
| 41 |
+
8. Confirm copy buttons still show `✓ Copied with headers` briefly.
|
| 42 |
|
| 43 |
+
Pass condition: no heading level appears smaller than the body text it introduces, tables remain readable, and every native plot has fullscreen/export controls.
|
| 44 |
|
| 45 |
+
## C. Establish the shared Workbench context
|
| 46 |
|
| 47 |
+
In **Workbench → I. Inspect a prompt location** set:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
```text
|
| 50 |
Prompt: The derivative of x squared is
|
|
|
|
| 55 |
|
| 56 |
Click **Inspect sparse features**.
|
| 57 |
|
| 58 |
+
Expected regression values are approximately:
|
| 59 |
|
| 60 |
```text
|
| 61 |
+
selected token: 5 (' is')
|
| 62 |
+
feature 22632 activation: 32.90625
|
| 63 |
+
reconstruction cosine: 0.9015
|
| 64 |
NMSE: 0.1874
|
| 65 |
Top-5 activation mass: 26.7%
|
|
|
|
|
|
|
| 66 |
```
|
| 67 |
|
| 68 |
+
The persistent **Current Workbench context** banner above the tabs must now state the inspected prompt, layer 14, and token 5. Switch to **Feature sets** and **Feature evidence**; the banner should remain visible.
|
|
|
|
|
|
|
| 69 |
|
| 70 |
+
## D. Explicit experiment feature selectors
|
| 71 |
|
| 72 |
+
### D1. Scale dose-response
|
| 73 |
|
| 74 |
+
Open **Workbench → III. Single-feature scale dose-response → Scale dose-response**.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
+
Confirm there is a visible field named:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
```text
|
| 79 |
+
Dose-response feature id
|
|
|
|
|
|
|
| 80 |
```
|
| 81 |
|
| 82 |
+
It should be populated from the latest inspection but editable independently. Set it to `22632` and run with target continuation `2x` from the single-feature causal panel.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
+
The `1.0` multiplier row must remain the exact numerical null:
|
|
|
|
|
|
|
| 85 |
|
| 86 |
+
```text
|
| 87 |
+
Δ feature coefficient = 0
|
| 88 |
+
Perturbation L2 = 0
|
| 89 |
+
Δ mean log p/token = 0
|
| 90 |
+
Δ sequence log p = 0
|
| 91 |
+
Next-token JS = 0
|
| 92 |
+
```
|
| 93 |
|
| 94 |
+
### D2. Contrastive continuation preference
|
| 95 |
|
| 96 |
+
Open **Workbench → IV. Contrastive continuation preference → Contrastive causal preference test**.
|
| 97 |
|
| 98 |
+
Confirm there is now a visible field named:
|
| 99 |
|
| 100 |
```text
|
| 101 |
+
Contrastive feature id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
```
|
| 103 |
|
| 104 |
+
Set:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
```text
|
| 107 |
+
Contrastive feature id: 22632
|
| 108 |
Continuation A (preferred): 2x
|
| 109 |
Continuation B (comparison): x
|
| 110 |
Contrastive intervention: ablate
|
| 111 |
```
|
| 112 |
|
|
|
|
|
|
|
| 113 |
Click **Run contrastive preference test**.
|
| 114 |
|
| 115 |
+
The v0.5 reference result was a causal sequence-log-odds shift of about `-0.0882`; small numerical variation is acceptable. The important regression is that the selected feature is explicit and the panel no longer silently inherits a hidden feature id.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
+
## E. Concept-guided candidate feature discovery
|
| 118 |
|
| 119 |
+
Open **Feature evidence → A. Concept-guided candidate discovery**.
|
| 120 |
|
| 121 |
+
Set:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
```text
|
| 124 |
+
Target concept: mathematics
|
| 125 |
Residual layer: 14
|
| 126 |
+
Prompts per concept: 4
|
| 127 |
+
Candidate features: 12
|
| 128 |
```
|
| 129 |
|
| 130 |
+
Click **Discover concept-associated candidates**.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
+
Verify:
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
+
- the result is a table named **Candidate feature contrast**;
|
| 135 |
+
- rows contain `Feature id`, target/other prompt-wide means, mean difference, selectivity score, and activation rates;
|
| 136 |
+
- candidates are ordered by positive `Target − other mean` evidence;
|
| 137 |
+
- the UI calls them **candidates**, not semantic labels;
|
| 138 |
+
- **Discovered candidate feature id** is populated when candidates exist;
|
| 139 |
+
- the plot has fullscreen/export controls.
|
| 140 |
|
| 141 |
+
An empty candidate list is not itself a software failure if the UI explicitly reports that no positively selective candidate was found. It must not invent a label.
|
| 142 |
|
| 143 |
+
## F. Reuse a discovered candidate
|
| 144 |
|
| 145 |
+
After Test E, choose any value in **Discovered candidate feature id** and click:
|
| 146 |
|
| 147 |
```text
|
| 148 |
+
Use candidate in feature experiments
|
|
|
|
|
|
|
| 149 |
```
|
| 150 |
|
| 151 |
+
Verify that the same id appears in:
|
| 152 |
|
| 153 |
+
- **Workbench → Single feature id**;
|
| 154 |
+
- **Workbench → Dose-response feature id**;
|
| 155 |
+
- **Workbench → Contrastive feature id**;
|
| 156 |
+
- **Feature evidence → Feature id**.
|
| 157 |
|
| 158 |
+
This is a UI/state-routing test only; it should not invoke the GPU.
|
| 159 |
|
| 160 |
+
## G. Completion-cue sensitivity
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
|
| 162 |
+
Open **Feature evidence → C. Completion-cue sensitivity**.
|
|
|
|
| 163 |
|
| 164 |
+
Use:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
|
| 166 |
```text
|
| 167 |
+
Feature id: 22632
|
| 168 |
+
Residual layer: 14
|
| 169 |
+
Prompt stem: The derivative of x squared
|
| 170 |
+
Completion cues:
|
| 171 |
+
is
|
| 172 |
+
=
|
| 173 |
+
:
|
| 174 |
+
equals
|
| 175 |
+
therefore
|
| 176 |
```
|
| 177 |
|
| 178 |
+
Click **Run completion-cue scan**.
|
| 179 |
|
| 180 |
+
The `is` row reconstructs the original prompt `The derivative of x squared is`, so its final-token activation should be approximately `32.90625` and `Active in TopK = True`.
|
| 181 |
|
| 182 |
+
Record the remaining cue activations. Their scientific purpose is to test whether feature 22632 is strongly tied to a completion cue such as `is`, rather than to mathematics generally. Do **not** require the other cues to be zero; that is an empirical result.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
|
| 184 |
+
## H. Prompt-wide selected-feature contrast regression
|
| 185 |
|
| 186 |
+
Still in **Feature evidence**, use feature `22632`, layer `14`, 4 prompts/concept and click **Run controlled concept contrast**.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
|
| 188 |
+
Reference v0.5 behavior:
|
| 189 |
|
| 190 |
+
```text
|
| 191 |
+
active in 7/28 prompts
|
| 192 |
+
uncertainty mean ≈ 7.55
|
| 193 |
+
mathematics mean ≈ 5.42
|
| 194 |
+
french_language mean = 0
|
| 195 |
+
```
|
| 196 |
|
| 197 |
+
Small numerical variation is acceptable. This verifies the existing prompt-wide selected-feature scan remains unchanged while the new candidate-discovery scan answers the reverse question: *given a concept, which features should I investigate?*
|
| 198 |
|
| 199 |
+
## I. Quick existing regressions
|
| 200 |
|
| 201 |
+
You do not need to resend full tables unless something changes unexpectedly.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
|
| 203 |
+
1. **Paraphrase robustness**: default pair should remain roughly selected-token cosine `0.301`, prompt-wide cosine `0.981`.
|
| 204 |
+
2. **Identical-prompt control**: all four robustness metrics should remain `1.000`.
|
| 205 |
+
3. **Layer trajectory**: layer-14 reconstruction should remain the weakest of layers 4/14/26 for the default prompt.
|
| 206 |
+
4. **Feature sets → Set-size sensitivity → Run 1/3/5-feature ablation sweep**: this is the “top-k sweep.” It is **not** the basic Workbench inspection. Only confirm the experiment still runs and produces k = 1, 3, 5 rows.
|
| 207 |
+
5. Scroll all the way to **End of workbench** after dynamic outputs expand; no tab switch should be necessary.
|
| 208 |
|
| 209 |
+
## J. Deferred final hardening
|
| 210 |
|
| 211 |
+
Do not spend ZeroGPU quota on the complete adversarial suite yet. Keep these for the final release:
|
| 212 |
|
| 213 |
+
- empty prompts;
|
| 214 |
+
- out-of-range token indices;
|
| 215 |
+
- feature ids `0`, `32767`, invalid `32768`;
|
| 216 |
+
- inactive feature ablation vs injection;
|
| 217 |
+
- whitespace-sensitive targets such as `2x` vs ` 2x`;
|
| 218 |
+
- missing required target continuations;
|
| 219 |
+
- unrelated paraphrases;
|
| 220 |
+
- negation and mixed-language prompts;
|
| 221 |
- long-prompt truncation;
|
| 222 |
+
- rapid repeated clicks / queue behavior;
|
| 223 |
+
- narrow/mobile-width layout;
|
| 224 |
+
- final table-copy and plot fullscreen/export sweep.
|
featurelens/runtime.py
CHANGED
|
@@ -227,6 +227,28 @@ class ContrastiveCausalResult:
|
|
| 227 |
random_control_count: int
|
| 228 |
|
| 229 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
@dataclass
|
| 231 |
class ParaphraseResult:
|
| 232 |
tokens_a: list[str]
|
|
@@ -1749,6 +1771,199 @@ class FeatureLensRuntime:
|
|
| 1749 |
total_prompt_count=len(values),
|
| 1750 |
)
|
| 1751 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1752 |
|
| 1753 |
RUNTIME = FeatureLensRuntime()
|
| 1754 |
|
|
|
|
| 227 |
random_control_count: int
|
| 228 |
|
| 229 |
|
| 230 |
+
@dataclass
|
| 231 |
+
class ConceptFeatureDiscoveryResult:
|
| 232 |
+
concept: str
|
| 233 |
+
layer: int
|
| 234 |
+
prompts_per_concept: int
|
| 235 |
+
top_n: int
|
| 236 |
+
rows: list[list[object]]
|
| 237 |
+
chart_rows: list[list[object]]
|
| 238 |
+
candidate_ids: list[int]
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
@dataclass
|
| 242 |
+
class FeatureCueScanResult:
|
| 243 |
+
feature_id: int
|
| 244 |
+
layer: int
|
| 245 |
+
prompt_stem: str
|
| 246 |
+
rows: list[list[object]]
|
| 247 |
+
chart_rows: list[list[object]]
|
| 248 |
+
active_cue_count: int
|
| 249 |
+
cue_count: int
|
| 250 |
+
|
| 251 |
+
|
| 252 |
@dataclass
|
| 253 |
class ParaphraseResult:
|
| 254 |
tokens_a: list[str]
|
|
|
|
| 1771 |
total_prompt_count=len(values),
|
| 1772 |
)
|
| 1773 |
|
| 1774 |
+
@torch.inference_mode()
|
| 1775 |
+
def concept_feature_discovery(
|
| 1776 |
+
self,
|
| 1777 |
+
concept: str,
|
| 1778 |
+
layer: int,
|
| 1779 |
+
prompts_per_concept: int | None = None,
|
| 1780 |
+
top_n: int = 12,
|
| 1781 |
+
) -> ConceptFeatureDiscoveryResult:
|
| 1782 |
+
"""Find SAE features whose prompt-wide activation is higher for one controlled concept."""
|
| 1783 |
+
self.ensure_ready(preload_saes=False)
|
| 1784 |
+
assert self.model is not None and self.tokenizer is not None and self.sae_store is not None
|
| 1785 |
+
concept = str(concept).strip()
|
| 1786 |
+
if not concept:
|
| 1787 |
+
raise ValueError('Choose a target concept.')
|
| 1788 |
+
n = int(prompts_per_concept or self.settings.contrast_prompts_per_concept)
|
| 1789 |
+
if n < 1 or n > 8:
|
| 1790 |
+
raise ValueError('Discovery prompts per concept must be between 1 and 8.')
|
| 1791 |
+
top_n = int(top_n)
|
| 1792 |
+
if top_n < 1 or top_n > 25:
|
| 1793 |
+
raise ValueError('Number of candidate features must be between 1 and 25.')
|
| 1794 |
+
rows = self._contrast_prompt_rows(n)
|
| 1795 |
+
available = sorted({str(row['concept']) for row in rows})
|
| 1796 |
+
if concept not in available:
|
| 1797 |
+
raise ValueError(f'Concept must be one of {available}.')
|
| 1798 |
+
|
| 1799 |
+
texts = [str(row['text']) for row in rows]
|
| 1800 |
+
batch = self.tokenizer(
|
| 1801 |
+
texts,
|
| 1802 |
+
return_tensors='pt',
|
| 1803 |
+
padding=True,
|
| 1804 |
+
truncation=True,
|
| 1805 |
+
max_length=self.settings.max_prompt_tokens,
|
| 1806 |
+
)
|
| 1807 |
+
batch = {key: value.to(self.device) for key, value in batch.items()}
|
| 1808 |
+
capture: dict = {}
|
| 1809 |
+
with self._capture_hook(int(layer), capture):
|
| 1810 |
+
self.model(**batch, use_cache=False)
|
| 1811 |
+
sae = self.sae_store.get(int(layer))
|
| 1812 |
+
encoding = sae.encode(capture['hidden'])
|
| 1813 |
+
attention = batch.get('attention_mask', torch.ones_like(batch['input_ids'])).bool()
|
| 1814 |
+
valid = attention.unsqueeze(-1).expand_as(encoding.values)
|
| 1815 |
+
values = torch.where(valid, encoding.values, torch.zeros_like(encoding.values)).float()
|
| 1816 |
+
|
| 1817 |
+
dense = torch.zeros(
|
| 1818 |
+
(values.shape[0], self.settings.sae_width),
|
| 1819 |
+
device=values.device,
|
| 1820 |
+
dtype=torch.float32,
|
| 1821 |
+
)
|
| 1822 |
+
dense.scatter_reduce_(
|
| 1823 |
+
1,
|
| 1824 |
+
encoding.indices.reshape(values.shape[0], -1),
|
| 1825 |
+
values.reshape(values.shape[0], -1),
|
| 1826 |
+
reduce='amax',
|
| 1827 |
+
include_self=True,
|
| 1828 |
+
)
|
| 1829 |
+
target_mask = torch.tensor(
|
| 1830 |
+
[str(row['concept']) == concept for row in rows],
|
| 1831 |
+
device=dense.device,
|
| 1832 |
+
dtype=torch.bool,
|
| 1833 |
+
)
|
| 1834 |
+
other_mask = ~target_mask
|
| 1835 |
+
target = dense[target_mask]
|
| 1836 |
+
other = dense[other_mask]
|
| 1837 |
+
target_mean = target.mean(dim=0)
|
| 1838 |
+
other_mean = other.mean(dim=0)
|
| 1839 |
+
target_rate = (target > 0).float().mean(dim=0)
|
| 1840 |
+
other_rate = (other > 0).float().mean(dim=0)
|
| 1841 |
+
mean_diff = target_mean - other_mean
|
| 1842 |
+
selectivity = mean_diff / (target_mean + other_mean + 1e-8)
|
| 1843 |
+
|
| 1844 |
+
eligible = (target_mean > 0) & (mean_diff > 0)
|
| 1845 |
+
candidate_idx = torch.nonzero(eligible, as_tuple=False).flatten()
|
| 1846 |
+
if candidate_idx.numel() == 0:
|
| 1847 |
+
return ConceptFeatureDiscoveryResult(
|
| 1848 |
+
concept=concept,
|
| 1849 |
+
layer=int(layer),
|
| 1850 |
+
prompts_per_concept=n,
|
| 1851 |
+
top_n=top_n,
|
| 1852 |
+
rows=[],
|
| 1853 |
+
chart_rows=[],
|
| 1854 |
+
candidate_ids=[],
|
| 1855 |
+
)
|
| 1856 |
+
order = torch.argsort(mean_diff[candidate_idx], descending=True)
|
| 1857 |
+
candidate_idx = candidate_idx[order[:top_n]]
|
| 1858 |
+
|
| 1859 |
+
table_rows: list[list[object]] = []
|
| 1860 |
+
chart_rows: list[list[object]] = []
|
| 1861 |
+
for rank, feature_tensor in enumerate(candidate_idx, start=1):
|
| 1862 |
+
fid = int(feature_tensor.item())
|
| 1863 |
+
row = [
|
| 1864 |
+
rank,
|
| 1865 |
+
fid,
|
| 1866 |
+
float(target_mean[fid].item()),
|
| 1867 |
+
float(other_mean[fid].item()),
|
| 1868 |
+
float(mean_diff[fid].item()),
|
| 1869 |
+
float(selectivity[fid].item()),
|
| 1870 |
+
float(target_rate[fid].item()),
|
| 1871 |
+
float(other_rate[fid].item()),
|
| 1872 |
+
float(target[:, fid].max().item()),
|
| 1873 |
+
]
|
| 1874 |
+
table_rows.append(row)
|
| 1875 |
+
chart_rows.append([str(fid), float(mean_diff[fid].item())])
|
| 1876 |
+
return ConceptFeatureDiscoveryResult(
|
| 1877 |
+
concept=concept,
|
| 1878 |
+
layer=int(layer),
|
| 1879 |
+
prompts_per_concept=n,
|
| 1880 |
+
top_n=top_n,
|
| 1881 |
+
rows=table_rows,
|
| 1882 |
+
chart_rows=chart_rows,
|
| 1883 |
+
candidate_ids=[int(x.item()) for x in candidate_idx],
|
| 1884 |
+
)
|
| 1885 |
+
|
| 1886 |
+
@staticmethod
|
| 1887 |
+
def _cue_prompt(stem: str, cue: str) -> str:
|
| 1888 |
+
stem = stem.rstrip()
|
| 1889 |
+
cue = cue.strip()
|
| 1890 |
+
if not cue:
|
| 1891 |
+
return stem
|
| 1892 |
+
if cue[0] in ':;,.!?=)]}':
|
| 1893 |
+
return stem + cue
|
| 1894 |
+
return stem + ' ' + cue
|
| 1895 |
+
|
| 1896 |
+
@torch.inference_mode()
|
| 1897 |
+
def feature_cue_scan(
|
| 1898 |
+
self,
|
| 1899 |
+
feature_id: int,
|
| 1900 |
+
layer: int,
|
| 1901 |
+
prompt_stem: str,
|
| 1902 |
+
cues: Sequence[str],
|
| 1903 |
+
) -> FeatureCueScanResult:
|
| 1904 |
+
"""Measure one feature at the final token after appending controlled completion cues."""
|
| 1905 |
+
if not prompt_stem.strip():
|
| 1906 |
+
raise ValueError('Enter a prompt stem.')
|
| 1907 |
+
cue_list: list[str] = []
|
| 1908 |
+
seen: set[str] = set()
|
| 1909 |
+
for raw in cues:
|
| 1910 |
+
cue = str(raw).strip()
|
| 1911 |
+
if not cue or cue in seen:
|
| 1912 |
+
continue
|
| 1913 |
+
seen.add(cue)
|
| 1914 |
+
cue_list.append(cue)
|
| 1915 |
+
if not cue_list:
|
| 1916 |
+
raise ValueError('Enter at least one cue.')
|
| 1917 |
+
if len(cue_list) > 12:
|
| 1918 |
+
raise ValueError('Cue scan supports at most 12 cues per run.')
|
| 1919 |
+
if int(feature_id) < 0 or int(feature_id) >= self.settings.sae_width:
|
| 1920 |
+
raise ValueError(f'Feature id must be in [0, {self.settings.sae_width - 1}].')
|
| 1921 |
+
|
| 1922 |
+
self.ensure_ready(preload_saes=False)
|
| 1923 |
+
assert self.model is not None and self.tokenizer is not None and self.sae_store is not None
|
| 1924 |
+
prompts = [self._cue_prompt(prompt_stem, cue) for cue in cue_list]
|
| 1925 |
+
batch = self.tokenizer(
|
| 1926 |
+
prompts,
|
| 1927 |
+
return_tensors='pt',
|
| 1928 |
+
padding=True,
|
| 1929 |
+
truncation=True,
|
| 1930 |
+
max_length=self.settings.max_prompt_tokens,
|
| 1931 |
+
)
|
| 1932 |
+
batch = {key: value.to(self.device) for key, value in batch.items()}
|
| 1933 |
+
capture: dict = {}
|
| 1934 |
+
with self._capture_hook(int(layer), capture):
|
| 1935 |
+
self.model(**batch, use_cache=False)
|
| 1936 |
+
sae = self.sae_store.get(int(layer))
|
| 1937 |
+
encoding = sae.encode(capture['hidden'])
|
| 1938 |
+
attention = batch.get('attention_mask', torch.ones_like(batch['input_ids'])).bool()
|
| 1939 |
+
|
| 1940 |
+
rows: list[list[object]] = []
|
| 1941 |
+
chart_rows: list[list[object]] = []
|
| 1942 |
+
active_count = 0
|
| 1943 |
+
for row_idx, (cue, full_prompt) in enumerate(zip(cue_list, prompts, strict=True)):
|
| 1944 |
+
valid_positions = torch.nonzero(attention[row_idx], as_tuple=False).flatten()
|
| 1945 |
+
final_pos = int(valid_positions[-1].item())
|
| 1946 |
+
indices = encoding.indices[row_idx, final_pos]
|
| 1947 |
+
values = encoding.values[row_idx, final_pos]
|
| 1948 |
+
mask = indices == int(feature_id)
|
| 1949 |
+
activation = float(values[mask][0].item()) if bool(mask.any()) else 0.0
|
| 1950 |
+
active = activation > 0
|
| 1951 |
+
active_count += int(active)
|
| 1952 |
+
token_id = int(batch['input_ids'][row_idx, final_pos].item())
|
| 1953 |
+
final_token = self.tokenizer.decode([token_id])
|
| 1954 |
+
rows.append([cue, full_prompt, repr(final_token), activation, active])
|
| 1955 |
+
chart_rows.append([cue, activation])
|
| 1956 |
+
|
| 1957 |
+
return FeatureCueScanResult(
|
| 1958 |
+
feature_id=int(feature_id),
|
| 1959 |
+
layer=int(layer),
|
| 1960 |
+
prompt_stem=prompt_stem,
|
| 1961 |
+
rows=rows,
|
| 1962 |
+
chart_rows=chart_rows,
|
| 1963 |
+
active_cue_count=active_count,
|
| 1964 |
+
cue_count=len(cue_list),
|
| 1965 |
+
)
|
| 1966 |
+
|
| 1967 |
|
| 1968 |
RUNTIME = FeatureLensRuntime()
|
| 1969 |
|
pyproject.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
[project]
|
| 2 |
name = "featurelens"
|
| 3 |
-
version = "0.
|
| 4 |
description = "Causal sparse-feature interpretability workbench for Qwen3 and Qwen-Scope SAEs"
|
| 5 |
requires-python = ">=3.10"
|
| 6 |
|
|
|
|
| 1 |
[project]
|
| 2 |
name = "featurelens"
|
| 3 |
+
version = "0.6.0"
|
| 4 |
description = "Causal sparse-feature interpretability workbench for Qwen3 and Qwen-Scope SAEs"
|
| 5 |
requires-python = ">=3.10"
|
| 6 |
|
research_config.json
CHANGED
|
@@ -89,5 +89,16 @@
|
|
| 89 |
"feature_token_activation_trace",
|
| 90 |
"contrastive_continuation_preference_test",
|
| 91 |
"feature_decoder_geometry"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
]
|
| 93 |
}
|
|
|
|
| 89 |
"feature_token_activation_trace",
|
| 90 |
"contrastive_continuation_preference_test",
|
| 91 |
"feature_decoder_geometry"
|
| 92 |
+
],
|
| 93 |
+
"concept_candidate_discovery_metric": "prompt-wide target-minus-other mean max activation; exploratory only",
|
| 94 |
+
"completion_cue_scan": "final-token feature activation after controlled suffix/cue substitution",
|
| 95 |
+
"live_features_v0_6": [
|
| 96 |
+
"start_here_plain_language_onboarding",
|
| 97 |
+
"persistent_workbench_context_banner",
|
| 98 |
+
"explicit_per_experiment_feature_selectors",
|
| 99 |
+
"plot_fullscreen_and_export_controls",
|
| 100 |
+
"consistent_heading_and_table_typography",
|
| 101 |
+
"concept_guided_candidate_feature_discovery",
|
| 102 |
+
"completion_cue_sensitivity_scan"
|
| 103 |
]
|
| 104 |
}
|
scripts/release_check.py
CHANGED
|
@@ -120,6 +120,22 @@ def check_config(config: dict) -> None:
|
|
| 120 |
f'{sorted(actual_live_v05)}'
|
| 121 |
)
|
| 122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
def check_datasets(config: dict) -> tuple[list[dict], list[dict]]:
|
| 125 |
prompts = load_jsonl(ROOT / 'data' / 'prompts.jsonl')
|
|
@@ -176,23 +192,26 @@ def check_readme() -> None:
|
|
| 176 |
'full-continuation',
|
| 177 |
'feature-set',
|
| 178 |
'paraphrase',
|
| 179 |
-
'random ensemble',
|
| 180 |
'batched zero-edit',
|
| 181 |
'concept contrast',
|
| 182 |
'non-additivity',
|
| 183 |
'contrastive',
|
| 184 |
'decoder geometry',
|
| 185 |
'token activation',
|
|
|
|
|
|
|
|
|
|
| 186 |
]
|
| 187 |
missing = [value for value in required_strings if value.lower() not in readme.lower()]
|
| 188 |
if missing:
|
| 189 |
-
raise SystemExit(f'README.md is missing required v0.
|
| 190 |
|
| 191 |
|
| 192 |
def check_pyproject() -> None:
|
| 193 |
text = (ROOT / 'pyproject.toml').read_text(encoding='utf-8')
|
| 194 |
-
if 'version = "0.
|
| 195 |
-
raise SystemExit('pyproject.toml must declare version 0.
|
| 196 |
|
| 197 |
|
| 198 |
def main() -> None:
|
|
@@ -210,7 +229,7 @@ def main() -> None:
|
|
| 210 |
print(f' layers: {config["layers"]}')
|
| 211 |
print(f' feature-set sizes: {config["feature_set_sizes"]}')
|
| 212 |
print(f' random controls: {config["live_random_controls"]}')
|
| 213 |
-
print(' release: v0.
|
| 214 |
|
| 215 |
|
| 216 |
if __name__ == '__main__':
|
|
|
|
| 120 |
f'{sorted(actual_live_v05)}'
|
| 121 |
)
|
| 122 |
|
| 123 |
+
required_live_v06 = {
|
| 124 |
+
'start_here_plain_language_onboarding',
|
| 125 |
+
'persistent_workbench_context_banner',
|
| 126 |
+
'explicit_per_experiment_feature_selectors',
|
| 127 |
+
'plot_fullscreen_and_export_controls',
|
| 128 |
+
'consistent_heading_and_table_typography',
|
| 129 |
+
'concept_guided_candidate_feature_discovery',
|
| 130 |
+
'completion_cue_sensitivity_scan',
|
| 131 |
+
}
|
| 132 |
+
actual_live_v06 = set(config.get('live_features_v0_6', []))
|
| 133 |
+
if actual_live_v06 != required_live_v06:
|
| 134 |
+
raise SystemExit(
|
| 135 |
+
'research_config.json live_features_v0_6 mismatch: '
|
| 136 |
+
f'{sorted(actual_live_v06)}'
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
|
| 140 |
def check_datasets(config: dict) -> tuple[list[dict], list[dict]]:
|
| 141 |
prompts = load_jsonl(ROOT / 'data' / 'prompts.jsonl')
|
|
|
|
| 192 |
'full-continuation',
|
| 193 |
'feature-set',
|
| 194 |
'paraphrase',
|
| 195 |
+
'random-control ensemble',
|
| 196 |
'batched zero-edit',
|
| 197 |
'concept contrast',
|
| 198 |
'non-additivity',
|
| 199 |
'contrastive',
|
| 200 |
'decoder geometry',
|
| 201 |
'token activation',
|
| 202 |
+
'concept-guided candidate',
|
| 203 |
+
'completion-cue',
|
| 204 |
+
'start here',
|
| 205 |
]
|
| 206 |
missing = [value for value in required_strings if value.lower() not in readme.lower()]
|
| 207 |
if missing:
|
| 208 |
+
raise SystemExit(f'README.md is missing required v0.6 content: {missing}')
|
| 209 |
|
| 210 |
|
| 211 |
def check_pyproject() -> None:
|
| 212 |
text = (ROOT / 'pyproject.toml').read_text(encoding='utf-8')
|
| 213 |
+
if 'version = "0.6.0"' not in text:
|
| 214 |
+
raise SystemExit('pyproject.toml must declare version 0.6.0.')
|
| 215 |
|
| 216 |
|
| 217 |
def main() -> None:
|
|
|
|
| 229 |
print(f' layers: {config["layers"]}')
|
| 230 |
print(f' feature-set sizes: {config["feature_set_sizes"]}')
|
| 231 |
print(f' random controls: {config["live_random_controls"]}')
|
| 232 |
+
print(' release: v0.6.0')
|
| 233 |
|
| 234 |
|
| 235 |
if __name__ == '__main__':
|
tests/test_live_runtime_helpers.py
CHANGED
|
@@ -214,3 +214,28 @@ def test_concept_contrast_promptwide_scan_runs_on_toy_runtime() -> None:
|
|
| 214 |
assert len(result.rows) == 7
|
| 215 |
assert all(len(row) == 7 for row in result.rows)
|
| 216 |
assert 0 <= result.active_prompt_count <= result.total_prompt_count
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
assert len(result.rows) == 7
|
| 215 |
assert all(len(row) == 7 for row in result.rows)
|
| 216 |
assert 0 <= result.active_prompt_count <= result.total_prompt_count
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def test_concept_feature_discovery_runs_on_toy_runtime() -> None:
|
| 220 |
+
runtime = make_runtime()
|
| 221 |
+
result = runtime.concept_feature_discovery(
|
| 222 |
+
concept='mathematics', layer=0, prompts_per_concept=1, top_n=3
|
| 223 |
+
)
|
| 224 |
+
assert result.concept == 'mathematics'
|
| 225 |
+
assert len(result.rows) <= 3
|
| 226 |
+
assert result.candidate_ids == [int(row[1]) for row in result.rows]
|
| 227 |
+
assert all(len(row) == 9 for row in result.rows)
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def test_feature_cue_scan_runs_on_toy_runtime() -> None:
|
| 231 |
+
runtime = make_runtime()
|
| 232 |
+
result = runtime.feature_cue_scan(
|
| 233 |
+
feature_id=0,
|
| 234 |
+
layer=0,
|
| 235 |
+
prompt_stem='abc',
|
| 236 |
+
cues=['is', '=', ':'],
|
| 237 |
+
)
|
| 238 |
+
assert result.cue_count == 3
|
| 239 |
+
assert len(result.rows) == 3
|
| 240 |
+
assert all(len(row) == 5 for row in result.rows)
|
| 241 |
+
assert 0 <= result.active_cue_count <= result.cue_count
|