Spaces:
Running on Zero
Running on Zero
Commit ·
463bcbe
1
Parent(s): 393bb89
Release FeatureLens v0.8.0
Browse files- CHANGELOG.md +24 -0
- README.md +12 -4
- app.py +244 -79
- docs/METHODOLOGY.md +26 -0
- docs/VALIDATION.md +144 -175
- featurelens/runtime.py +46 -6
- pyproject.toml +1 -1
- research_config.json +9 -1
- scripts/release_check.py +23 -4
- tests/test_live_runtime_helpers.py +39 -0
- tests/test_ui_helpers.py +36 -1
CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
| 1 |
# Changelog
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
## v0.7.0
|
| 4 |
|
| 5 |
### Research-instrument UI cleanup
|
|
|
|
| 1 |
# Changelog
|
| 2 |
|
| 3 |
+
## v0.8.0
|
| 4 |
+
|
| 5 |
+
### UI readability and focus
|
| 6 |
+
- Replaced plot-native fullscreen behavior with a **bounded FeatureLens focus overlay**. The plot is copied into a centered reading surface (max ~1120 px) instead of stretching across an ultrawide display; closing the overlay restores the original page position.
|
| 7 |
+
- Kept descriptive plot export filenames and the Gradio Dataframe fullscreen control.
|
| 8 |
+
- Replaced fragile native Dataframe labels with explicit **result-table headings** above every major table so table titles follow the same typography hierarchy as the rest of the application.
|
| 9 |
+
- Added a muted cue palette to the cue × context plot instead of relying on Gradio/Vega default saturated series colors.
|
| 10 |
+
|
| 11 |
+
### Independent experiment inputs
|
| 12 |
+
- The scale dose-response panel now owns its **Dose-response feature id** and **Dose-response target continuation**. It no longer depends on running the single-feature causal test or filling that section's optional target field first.
|
| 13 |
+
- Clarified in-panel provenance: dose response reads the current prompt/layer/token fields from Workbench Section I but is otherwise a standalone experiment.
|
| 14 |
+
|
| 15 |
+
### Candidate discovery
|
| 16 |
+
- Added **Causal-ready at current token** ranking. It requires positive concept contrast *and* activation at the selected Workbench token, then ranks those compatible candidates using balanced selectivity plus a log-scaled current-token activation term.
|
| 17 |
+
- Discovery summaries now report how many displayed candidates are actually active at the selected Workbench token. This makes the distinction between a prompt-wide concept candidate and an immediately ablatable feature explicit.
|
| 18 |
+
- Balanced selectivity and raw mean-difference modes remain available for methodological comparison.
|
| 19 |
+
|
| 20 |
+
### Cue specificity
|
| 21 |
+
- Cue × context summaries now derive the dominant cue, its context coverage, and off-dominant activity. A feature that fires for one cue in every tested context while all other cues stay inactive is reported as a **cue-dominant tested pattern**, not merely with generic interpretation text.
|
| 22 |
+
|
| 23 |
+
### Validation
|
| 24 |
+
- Expanded the automated suite to cover causal-ready candidate discovery, independent dose-response target state, cue-dominance diagnostics, plot-focus JavaScript markers, and explicit result-heading behavior.
|
| 25 |
+
- Retained compile, Ruff, actual Gradio `launch()`, release-check, and deferred final adversarial-suite gates.
|
| 26 |
+
|
| 27 |
## v0.7.0
|
| 28 |
|
| 29 |
### Research-instrument UI cleanup
|
README.md
CHANGED
|
@@ -13,7 +13,7 @@ license: mit
|
|
| 13 |
|
| 14 |
# FeatureLens — Causal Interpretability Workbench
|
| 15 |
|
| 16 |
-
> **v0.
|
| 17 |
|
| 18 |
**Research question:**
|
| 19 |
|
|
@@ -97,7 +97,8 @@ The main workflow supports:
|
|
| 97 |
- exact **full-continuation teacher-forced scoring**;
|
| 98 |
- per-target-token log-probability decomposition;
|
| 99 |
- 8-direction norm-matched random controls;
|
| 100 |
-
- batched scale dose-response
|
|
|
|
| 101 |
|
| 102 |
### Feature sets
|
| 103 |
|
|
@@ -345,6 +346,13 @@ FeatureLens/
|
|
| 345 |
└── research_config.json
|
| 346 |
```
|
| 347 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
## Validation
|
| 349 |
|
| 350 |
```bash
|
|
@@ -355,7 +363,7 @@ python scripts/ui_smoke.py
|
|
| 355 |
python scripts/release_check.py
|
| 356 |
```
|
| 357 |
|
| 358 |
-
See [`docs/VALIDATION.md`](docs/VALIDATION.md). The v0.
|
| 359 |
|
| 360 |
## Limitations
|
| 361 |
|
|
@@ -370,7 +378,7 @@ See [`docs/VALIDATION.md`](docs/VALIDATION.md). The v0.7 validation guide uses t
|
|
| 370 |
|
| 371 |
## Resume-ready description
|
| 372 |
|
| 373 |
-
> **FeatureLens — Causal Interpretability Workbench** | PyTorch, Qwen3, Sparse Autoencoders, Mechanistic Interpretability, Gradio
|
| 374 |
> Built an SAE-based interpretability system for Qwen3-1.7B with held-out concept discovery, concept-guided candidate discovery, token/prompt-wide, completion-cue, and cue × context 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.
|
| 375 |
|
| 376 |
## Acknowledgements
|
|
|
|
| 13 |
|
| 14 |
# FeatureLens — Causal Interpretability Workbench
|
| 15 |
|
| 16 |
+
> **v0.8:** a cleaner evidence workflow with bounded plot focus, explicit table headings, standalone dose-response inputs, a causal-ready current-token candidate ranking, and data-driven cue-dominant specificity summaries—while retaining balanced/raw discovery, causal controls, robustness, geometry, and prompt-wide evidence.
|
| 17 |
|
| 18 |
**Research question:**
|
| 19 |
|
|
|
|
| 97 |
- exact **full-continuation teacher-forced scoring**;
|
| 98 |
- per-target-token log-probability decomposition;
|
| 99 |
- 8-direction norm-matched random controls;
|
| 100 |
+
- **standalone** batched scale dose-response with its own feature id and target continuation;
|
| 101 |
+
- bounded plot focus views that do not stretch across ultrawide displays.
|
| 102 |
|
| 103 |
### Feature sets
|
| 104 |
|
|
|
|
| 346 |
└── research_config.json
|
| 347 |
```
|
| 348 |
|
| 349 |
+
## v0.8 live workflow improvements
|
| 350 |
+
|
| 351 |
+
- **Causal-ready candidate ranking** requires concept contrast and current-token activity before suggesting a feature for immediate intervention.
|
| 352 |
+
- **Standalone dose-response** has independent feature and target inputs; Section II does not need to run first.
|
| 353 |
+
- **Cue × context interpretation** reports cue dominance from the measured activation matrix rather than generic boilerplate.
|
| 354 |
+
- **Plot focus** uses a bounded overlay and preserves page position on close; table titles are explicit headings instead of relying on Gradio component labels.
|
| 355 |
+
|
| 356 |
## Validation
|
| 357 |
|
| 358 |
```bash
|
|
|
|
| 363 |
python scripts/release_check.py
|
| 364 |
```
|
| 365 |
|
| 366 |
+
See [`docs/VALIDATION.md`](docs/VALIDATION.md). The v0.8 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.
|
| 367 |
|
| 368 |
## Limitations
|
| 369 |
|
|
|
|
| 378 |
|
| 379 |
## Resume-ready description
|
| 380 |
|
| 381 |
+
> **FeatureLens — Causal Interpretability Workbench** | PyTorch, Qwen3, Sparse Autoencoders, Mechanistic Interpretability, Gradio
|
| 382 |
> Built an SAE-based interpretability system for Qwen3-1.7B with held-out concept discovery, concept-guided candidate discovery, token/prompt-wide, completion-cue, and cue × context 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.
|
| 383 |
|
| 384 |
## Acknowledgements
|
app.py
CHANGED
|
@@ -121,6 +121,13 @@ CSS = r"""
|
|
| 121 |
}
|
| 122 |
.start-card h3 { margin-top: 0; }
|
| 123 |
.graph-note { font-size: .95rem; opacity: .72; margin-top: 2px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
.result-table .label-wrap,
|
| 125 |
.result-table .label-wrap span,
|
| 126 |
.result-table label,
|
|
@@ -141,19 +148,60 @@ CSS = r"""
|
|
| 141 |
font-weight: 700 !important;
|
| 142 |
line-height: 1.25 !important;
|
| 143 |
}
|
| 144 |
-
/*
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
box-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
.result-table.fullscreen {
|
| 158 |
top: 5vh !important;
|
| 159 |
left: 50% !important;
|
|
@@ -165,7 +213,6 @@ CSS = r"""
|
|
| 165 |
overflow: auto !important;
|
| 166 |
box-shadow: 0 0 0 100vmax rgba(0, 0, 0, .66), 0 14px 40px rgba(0, 0, 0, .35) !important;
|
| 167 |
}
|
| 168 |
-
.fl-plot.fullscreen svg { max-width: 100% !important; }
|
| 169 |
.candidate-help { opacity: .78; font-size: .97rem; margin-top: -2px; }
|
| 170 |
@media (max-width: 900px) {
|
| 171 |
.gradio-container { width: 100% !important; padding-left: 12px !important; padding-right: 12px !important; }
|
|
@@ -222,9 +269,7 @@ INSTALL_REFLOW_JS = r"""
|
|
| 222 |
let timer = null;
|
| 223 |
const kick = () => {
|
| 224 |
window.clearTimeout(timer);
|
| 225 |
-
timer = window.setTimeout(() =>
|
| 226 |
-
window.dispatchEvent(new Event("resize"));
|
| 227 |
-
}, 80);
|
| 228 |
};
|
| 229 |
const root = document.querySelector(".gradio-container") || document.body;
|
| 230 |
if (window.ResizeObserver) {
|
|
@@ -236,33 +281,94 @@ INSTALL_REFLOW_JS = r"""
|
|
| 236 |
mutation.observe(root, {subtree: true, childList: true});
|
| 237 |
window.__featurelens_mutation_observer = mutation;
|
| 238 |
|
| 239 |
-
|
| 240 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
document.addEventListener("click", (event) => {
|
| 242 |
const button = event.target && event.target.closest ? event.target.closest("button") : null;
|
| 243 |
if (!button) return;
|
| 244 |
const label = `${button.getAttribute("aria-label") || ""} ${button.getAttribute("title") || ""} ${button.textContent || ""}`.toLowerCase();
|
| 245 |
if (!label.includes("fullscreen")) return;
|
| 246 |
-
const
|
| 247 |
-
if (!
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
window.__featurelens_focus_block = block;
|
| 252 |
-
window.setTimeout(() => {
|
| 253 |
-
block.scrollTop = 0;
|
| 254 |
-
if (window.__featurelens_focus_scroll_y != null) window.scrollTo(0, window.__featurelens_focus_scroll_y);
|
| 255 |
-
}, 80);
|
| 256 |
-
} else {
|
| 257 |
-
const y = window.__featurelens_focus_scroll_y;
|
| 258 |
-
window.setTimeout(() => {
|
| 259 |
-
if (y != null) window.scrollTo(0, y);
|
| 260 |
-
window.__featurelens_focus_scroll_y = null;
|
| 261 |
-
window.__featurelens_focus_block = null;
|
| 262 |
-
}, 80);
|
| 263 |
-
}
|
| 264 |
}, true);
|
| 265 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
// Gradio's native plot exporter currently emits chart.png. Remember which FeatureLens plot initiated
|
| 267 |
// the export, then rename the transient download anchor before the browser handles it.
|
| 268 |
document.addEventListener("click", (event) => {
|
|
@@ -305,6 +411,10 @@ def _copy_button(label: str = "Copy table with headers") -> gr.Button:
|
|
| 305 |
return gr.Button(label, size="sm", variant="primary", elem_classes=["copy-btn"])
|
| 306 |
|
| 307 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
def _copy_ack(_text: str) -> None:
|
| 309 |
gr.Info("Copied table with headers.", duration=1.2)
|
| 310 |
|
|
@@ -515,15 +625,21 @@ def _contrastive_metrics_markdown(result) -> str:
|
|
| 515 |
|
| 516 |
def _discovery_metrics_markdown(result) -> str:
|
| 517 |
if not result.candidate_ids:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 518 |
return (
|
| 519 |
f"No positively selective candidate features were found for **{result.concept}** at layer "
|
| 520 |
f"**{result.layer}** in this small live batch. Increase the sample or try another layer."
|
| 521 |
)
|
| 522 |
-
ranking =
|
| 523 |
-
"balanced selectivity × coverage × log-magnitude"
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
context = (
|
| 528 |
f"Current Workbench token **{result.current_token_index}** was checked in the same model batch. "
|
| 529 |
"The default candidate prefers the highest-ranked feature active at that token when one exists."
|
|
@@ -534,6 +650,8 @@ def _discovery_metrics_markdown(result) -> str:
|
|
| 534 |
f"Concept **{result.concept}** · layer **{result.layer}** · "
|
| 535 |
f"{result.prompts_per_concept} prompts/concept. \n"
|
| 536 |
f"Showing **{len(result.candidate_ids)}** candidates ranked by **{ranking}**. \n"
|
|
|
|
|
|
|
| 537 |
f"{context} \n\n"
|
| 538 |
"This is **candidate discovery**, not a semantic label. The offline held-out AUROC/F1 pipeline remains "
|
| 539 |
"the standard for a concept-selectivity claim."
|
|
@@ -545,13 +663,27 @@ def _cue_context_metrics_markdown(result) -> str:
|
|
| 545 |
f"{cue}: {count}/{len(result.stems)} contexts"
|
| 546 |
for cue, count in result.cue_active_context_counts.items()
|
| 547 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
return (
|
| 549 |
f"Feature **{result.feature_id}**, layer **{result.layer}** · active in "
|
| 550 |
f"**{result.active_condition_count}/{result.condition_count}** stem × cue conditions. \n"
|
| 551 |
f"Cue coverage — {active}. \n\n"
|
| 552 |
-
"
|
| 553 |
-
"
|
| 554 |
-
"a diagnostic, not a semantic label."
|
| 555 |
)
|
| 556 |
|
| 557 |
|
|
@@ -1033,11 +1165,11 @@ def run_concept_feature_discovery(
|
|
| 1033 |
workbench_token_index: int,
|
| 1034 |
):
|
| 1035 |
try:
|
| 1036 |
-
ranking_mode =
|
| 1037 |
-
"balanced_selectivity"
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
result = RUNTIME.concept_feature_discovery(
|
| 1042 |
concept=concept,
|
| 1043 |
layer=int(layer),
|
|
@@ -1154,7 +1286,7 @@ def set_mode_help(mode: str):
|
|
| 1154 |
with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_width=True) as demo:
|
| 1155 |
gr.HTML(
|
| 1156 |
'<header class="hero">'
|
| 1157 |
-
'<h1>FeatureLens <span style="font-size:.48em;opacity:.58;font-weight:400">v0.
|
| 1158 |
'<div class="subtitle">Causal Interpretability Workbench</div>'
|
| 1159 |
'<div class="question">Discover sparse features, test robustness, and separate correlation from causal influence.</div>'
|
| 1160 |
'</header>'
|
|
@@ -1241,11 +1373,12 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1241 |
analysis_metrics = gr.Markdown()
|
| 1242 |
with gr.Row(equal_height=False):
|
| 1243 |
with gr.Column(scale=3):
|
|
|
|
| 1244 |
feature_table = gr.Dataframe(
|
| 1245 |
headers=["Rank", "Feature id", "Activation", "Offline concept hint"],
|
| 1246 |
datatype=["number", "number", "number", "str"],
|
| 1247 |
interactive=False,
|
| 1248 |
-
label="Strongest active SAE features",
|
| 1249 |
wrap=False,
|
| 1250 |
max_height=380,
|
| 1251 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
|
@@ -1307,18 +1440,20 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1307 |
with gr.Row():
|
| 1308 |
baseline_out = gr.Textbox(label="Baseline greedy generation", lines=6, interactive=False)
|
| 1309 |
modified_out = gr.Textbox(label="SAE-edited greedy generation", lines=6, interactive=False)
|
|
|
|
| 1310 |
token_prob_table = gr.Dataframe(
|
| 1311 |
interactive=False,
|
| 1312 |
-
label="Next-token distribution shift",
|
| 1313 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1314 |
wrap=False,
|
| 1315 |
max_height=380,
|
| 1316 |
)
|
| 1317 |
token_prob_tsv = gr.Textbox(visible="hidden")
|
| 1318 |
token_prob_copy = _copy_button()
|
|
|
|
| 1319 |
target_token_table = gr.Dataframe(
|
| 1320 |
interactive=False,
|
| 1321 |
-
label="Target continuation token-by-token score",
|
| 1322 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1323 |
wrap=False,
|
| 1324 |
max_height=380,
|
|
@@ -1329,24 +1464,34 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1329 |
gr.HTML('<div class="section-rule">III. Single-feature scale dose-response</div>')
|
| 1330 |
with gr.Group():
|
| 1331 |
gr.Markdown("**Uses current Workbench prompt / layer / token.** Choose the feature for this experiment below.")
|
| 1332 |
-
|
| 1333 |
-
|
| 1334 |
-
|
| 1335 |
-
|
| 1336 |
-
|
| 1337 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1338 |
gr.Markdown(
|
|
|
|
|
|
|
| 1339 |
"Always a **scale** experiment: 0× = ablation, 1× = numerical no-edit reference, 2× = double "
|
| 1340 |
-
"the native coefficient. The six conditions are evaluated together
|
| 1341 |
-
"against the 1× row from that same batch."
|
| 1342 |
)
|
| 1343 |
dose_btn = gr.Button("Run scale dose-response", variant="primary", elem_classes=["action-btn"])
|
| 1344 |
dose_metrics = gr.Markdown()
|
| 1345 |
with gr.Row(equal_height=False):
|
| 1346 |
with gr.Column(scale=3):
|
|
|
|
| 1347 |
dose_table = gr.Dataframe(
|
| 1348 |
interactive=False,
|
| 1349 |
-
label="Scale dose-response measurements",
|
| 1350 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1351 |
wrap=False,
|
| 1352 |
max_height=380,
|
|
@@ -1402,9 +1547,10 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1402 |
contrastive_metrics = gr.Markdown()
|
| 1403 |
with gr.Row(equal_height=False):
|
| 1404 |
with gr.Column(scale=3):
|
|
|
|
| 1405 |
contrastive_table = gr.Dataframe(
|
| 1406 |
interactive=False,
|
| 1407 |
-
label="Contrastive continuation scores",
|
| 1408 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1409 |
wrap=False,
|
| 1410 |
max_height=320,
|
|
@@ -1469,9 +1615,10 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1469 |
set_metrics = gr.Markdown()
|
| 1470 |
with gr.Row(equal_height=False):
|
| 1471 |
with gr.Column(scale=2):
|
|
|
|
| 1472 |
set_feature_table = gr.Dataframe(
|
| 1473 |
interactive=False,
|
| 1474 |
-
label="Joint intervention features",
|
| 1475 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1476 |
wrap=False,
|
| 1477 |
max_height=380,
|
|
@@ -1479,9 +1626,10 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1479 |
set_feature_tsv = gr.Textbox(visible="hidden")
|
| 1480 |
set_feature_copy = _copy_button()
|
| 1481 |
with gr.Column(scale=3):
|
|
|
|
| 1482 |
set_target_table = gr.Dataframe(
|
| 1483 |
interactive=False,
|
| 1484 |
-
label="Target continuation token-by-token score",
|
| 1485 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1486 |
wrap=False,
|
| 1487 |
max_height=380,
|
|
@@ -1499,9 +1647,10 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1499 |
set_sweep_note = gr.Markdown()
|
| 1500 |
with gr.Row(equal_height=False):
|
| 1501 |
with gr.Column(scale=3):
|
|
|
|
| 1502 |
set_sweep_table = gr.Dataframe(
|
| 1503 |
interactive=False,
|
| 1504 |
-
label="Feature-set size measurements",
|
| 1505 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1506 |
wrap=False,
|
| 1507 |
max_height=380,
|
|
@@ -1535,9 +1684,10 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1535 |
interaction_metrics = gr.Markdown()
|
| 1536 |
with gr.Row(equal_height=False):
|
| 1537 |
with gr.Column(scale=3):
|
|
|
|
| 1538 |
interaction_table = gr.Dataframe(
|
| 1539 |
interactive=False,
|
| 1540 |
-
label="Individual and joint ablation measurements",
|
| 1541 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1542 |
wrap=False,
|
| 1543 |
max_height=380,
|
|
@@ -1572,9 +1722,10 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1572 |
geometry_metrics = gr.Markdown()
|
| 1573 |
with gr.Row(equal_height=False):
|
| 1574 |
with gr.Column(scale=3):
|
|
|
|
| 1575 |
geometry_table = gr.Dataframe(
|
| 1576 |
interactive=False,
|
| 1577 |
-
label="Pairwise decoder geometry",
|
| 1578 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1579 |
wrap=False,
|
| 1580 |
max_height=340,
|
|
@@ -1623,10 +1774,10 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1623 |
discovery_top_n = gr.Slider(5, 20, value=12, step=1, label="Candidate features")
|
| 1624 |
with gr.Row(equal_height=True):
|
| 1625 |
discovery_ranking = gr.Dropdown(
|
| 1626 |
-
choices=["Balanced selectivity", "Raw mean difference"],
|
| 1627 |
value="Balanced selectivity",
|
| 1628 |
label="Candidate ranking",
|
| 1629 |
-
info="Balanced selectivity
|
| 1630 |
scale=2,
|
| 1631 |
)
|
| 1632 |
gr.Markdown(
|
|
@@ -1637,8 +1788,9 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1637 |
discovery_metrics = gr.Markdown()
|
| 1638 |
with gr.Row(equal_height=False):
|
| 1639 |
with gr.Column(scale=3):
|
|
|
|
| 1640 |
discovery_table = gr.Dataframe(
|
| 1641 |
-
interactive=False, label="Candidate feature evidence", buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1642 |
wrap=False, max_height=420
|
| 1643 |
)
|
| 1644 |
discovery_tsv = gr.Textbox(visible="hidden")
|
|
@@ -1697,9 +1849,10 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1697 |
trace_metrics = gr.Markdown()
|
| 1698 |
with gr.Row(equal_height=False):
|
| 1699 |
with gr.Column(scale=3):
|
|
|
|
| 1700 |
trace_table = gr.Dataframe(
|
| 1701 |
interactive=False,
|
| 1702 |
-
label="Feature activation by prompt token",
|
| 1703 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1704 |
wrap=False,
|
| 1705 |
max_height=340,
|
|
@@ -1733,7 +1886,8 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1733 |
cue_metrics = gr.Markdown()
|
| 1734 |
with gr.Row(equal_height=False):
|
| 1735 |
with gr.Column(scale=3):
|
| 1736 |
-
|
|
|
|
| 1737 |
cue_tsv = gr.Textbox(visible="hidden")
|
| 1738 |
cue_copy = _copy_button()
|
| 1739 |
with gr.Column(scale=2):
|
|
@@ -1765,9 +1919,10 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1765 |
cue_context_metrics = gr.Markdown()
|
| 1766 |
with gr.Row(equal_height=False):
|
| 1767 |
with gr.Column(scale=3):
|
|
|
|
| 1768 |
cue_context_table = gr.Dataframe(
|
| 1769 |
interactive=False,
|
| 1770 |
-
label="Cue × context feature response",
|
| 1771 |
buttons=["fullscreen"],
|
| 1772 |
elem_classes=["result-table"],
|
| 1773 |
wrap=False,
|
|
@@ -1780,6 +1935,13 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1780 |
x="Prompt stem",
|
| 1781 |
y="Activation",
|
| 1782 |
color="Cue",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1783 |
title="Cue response across contexts",
|
| 1784 |
elem_id="plot-cue-context-matrix",
|
| 1785 |
x_title="Prompt stem",
|
|
@@ -1796,9 +1958,10 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1796 |
contrast_metrics = gr.Markdown()
|
| 1797 |
with gr.Row(equal_height=False):
|
| 1798 |
with gr.Column(scale=3):
|
|
|
|
| 1799 |
contrast_table = gr.Dataframe(
|
| 1800 |
interactive=False,
|
| 1801 |
-
label="Feature activation by controlled concept",
|
| 1802 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1803 |
wrap=False,
|
| 1804 |
max_height=380,
|
|
@@ -1857,9 +2020,10 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1857 |
para_metrics = gr.Markdown()
|
| 1858 |
with gr.Row(equal_height=False):
|
| 1859 |
with gr.Column(scale=3):
|
|
|
|
| 1860 |
para_table = gr.Dataframe(
|
| 1861 |
interactive=False,
|
| 1862 |
-
label="Top-feature overlap at selected tokens",
|
| 1863 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1864 |
wrap=False,
|
| 1865 |
max_height=380,
|
|
@@ -1904,9 +2068,10 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1904 |
trajectory_tokens = gr.HTML()
|
| 1905 |
with gr.Row(equal_height=False):
|
| 1906 |
with gr.Column(scale=3):
|
|
|
|
| 1907 |
trajectory_table = gr.Dataframe(
|
| 1908 |
interactive=False,
|
| 1909 |
-
label="Layer diagnostics",
|
| 1910 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1911 |
wrap=False,
|
| 1912 |
max_height=380,
|
|
@@ -2018,7 +2183,7 @@ Association, robustness, geometry, and intervention evidence remain separate cla
|
|
| 2018 |
)
|
| 2019 |
dose_btn.click(
|
| 2020 |
run_dose_response,
|
| 2021 |
-
inputs=[prompt, layer, token_index, dose_feature_id,
|
| 2022 |
outputs=[dose_table, dose_plot, dose_metrics, dose_tsv],
|
| 2023 |
)
|
| 2024 |
contrastive_mode.change(mode_help, inputs=[contrastive_mode], outputs=[contrastive_coefficient])
|
|
@@ -2127,4 +2292,4 @@ if __name__ == "__main__":
|
|
| 2127 |
theme=THEME,
|
| 2128 |
ssr_mode=False,
|
| 2129 |
show_error=True,
|
| 2130 |
-
)
|
|
|
|
| 121 |
}
|
| 122 |
.start-card h3 { margin-top: 0; }
|
| 123 |
.graph-note { font-size: .95rem; opacity: .72; margin-top: 2px; }
|
| 124 |
+
.table-heading { margin: 4px 0 7px !important; }
|
| 125 |
+
.table-heading h4, .table-heading p {
|
| 126 |
+
font-size: 1.24rem !important;
|
| 127 |
+
font-weight: 700 !important;
|
| 128 |
+
line-height: 1.28 !important;
|
| 129 |
+
margin: 0 !important;
|
| 130 |
+
}
|
| 131 |
.result-table .label-wrap,
|
| 132 |
.result-table .label-wrap span,
|
| 133 |
.result-table label,
|
|
|
|
| 148 |
font-weight: 700 !important;
|
| 149 |
line-height: 1.25 !important;
|
| 150 |
}
|
| 151 |
+
/* Plot focus is handled by a FeatureLens overlay instead of Gradio native fullscreen.
|
| 152 |
+
The original page never moves, and the focused chart is bounded for readability. */
|
| 153 |
+
#featurelens-plot-focus-overlay {
|
| 154 |
+
position: fixed;
|
| 155 |
+
inset: 0;
|
| 156 |
+
z-index: 100000;
|
| 157 |
+
background: rgba(8, 8, 8, .82);
|
| 158 |
+
display: flex;
|
| 159 |
+
align-items: flex-start;
|
| 160 |
+
justify-content: center;
|
| 161 |
+
padding: 5vh 3vw;
|
| 162 |
+
box-sizing: border-box;
|
| 163 |
+
}
|
| 164 |
+
.featurelens-focus-panel {
|
| 165 |
+
width: min(92vw, 1120px);
|
| 166 |
+
max-height: 90vh;
|
| 167 |
+
overflow: auto;
|
| 168 |
+
background: var(--background-fill-primary);
|
| 169 |
+
border: 1px solid var(--border-color-primary);
|
| 170 |
+
border-radius: 4px;
|
| 171 |
+
box-shadow: 0 14px 46px rgba(0, 0, 0, .45);
|
| 172 |
+
padding: 14px 18px 18px;
|
| 173 |
+
}
|
| 174 |
+
.featurelens-focus-toolbar {
|
| 175 |
+
display: flex;
|
| 176 |
+
align-items: center;
|
| 177 |
+
justify-content: space-between;
|
| 178 |
+
margin-bottom: 8px;
|
| 179 |
+
}
|
| 180 |
+
.featurelens-focus-title { font-size: 1.18rem; font-weight: 700; }
|
| 181 |
+
.featurelens-focus-close {
|
| 182 |
+
min-width: 36px !important;
|
| 183 |
+
min-height: 32px !important;
|
| 184 |
+
background: var(--background-fill-secondary) !important;
|
| 185 |
+
color: var(--body-text-color) !important;
|
| 186 |
+
border: 1px solid var(--border-color-primary) !important;
|
| 187 |
}
|
| 188 |
+
.featurelens-focus-chart {
|
| 189 |
+
width: 100%;
|
| 190 |
+
display: flex;
|
| 191 |
+
align-items: flex-start;
|
| 192 |
+
justify-content: center;
|
| 193 |
+
overflow: hidden;
|
| 194 |
+
}
|
| 195 |
+
.featurelens-focus-chart svg,
|
| 196 |
+
.featurelens-focus-chart canvas {
|
| 197 |
+
display: block;
|
| 198 |
+
width: 100% !important;
|
| 199 |
+
max-width: 1080px !important;
|
| 200 |
+
height: auto !important;
|
| 201 |
+
max-height: 78vh !important;
|
| 202 |
+
object-fit: contain;
|
| 203 |
+
}
|
| 204 |
+
/* Keep Gradio Dataframe fullscreen available; tables genuinely benefit from width. */
|
| 205 |
.result-table.fullscreen {
|
| 206 |
top: 5vh !important;
|
| 207 |
left: 50% !important;
|
|
|
|
| 213 |
overflow: auto !important;
|
| 214 |
box-shadow: 0 0 0 100vmax rgba(0, 0, 0, .66), 0 14px 40px rgba(0, 0, 0, .35) !important;
|
| 215 |
}
|
|
|
|
| 216 |
.candidate-help { opacity: .78; font-size: .97rem; margin-top: -2px; }
|
| 217 |
@media (max-width: 900px) {
|
| 218 |
.gradio-container { width: 100% !important; padding-left: 12px !important; padding-right: 12px !important; }
|
|
|
|
| 269 |
let timer = null;
|
| 270 |
const kick = () => {
|
| 271 |
window.clearTimeout(timer);
|
| 272 |
+
timer = window.setTimeout(() => window.dispatchEvent(new Event("resize")), 80);
|
|
|
|
|
|
|
| 273 |
};
|
| 274 |
const root = document.querySelector(".gradio-container") || document.body;
|
| 275 |
if (window.ResizeObserver) {
|
|
|
|
| 281 |
mutation.observe(root, {subtree: true, childList: true});
|
| 282 |
window.__featurelens_mutation_observer = mutation;
|
| 283 |
|
| 284 |
+
const closePlotFocus = () => {
|
| 285 |
+
const overlay = document.getElementById("featurelens-plot-focus-overlay");
|
| 286 |
+
if (!overlay) return;
|
| 287 |
+
overlay.remove();
|
| 288 |
+
document.body.style.overflow = window.__featurelens_body_overflow || "";
|
| 289 |
+
const y = window.__featurelens_plot_scroll_y;
|
| 290 |
+
window.__featurelens_plot_scroll_y = null;
|
| 291 |
+
window.__featurelens_body_overflow = null;
|
| 292 |
+
if (y != null) window.scrollTo(0, y);
|
| 293 |
+
};
|
| 294 |
+
|
| 295 |
+
const readableTitle = (block) => {
|
| 296 |
+
const id = (block && block.id) || "plot-featurelens-chart";
|
| 297 |
+
return id.replace(/^plot-/, "").replace(/[-_]+/g, " ")
|
| 298 |
+
.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
| 299 |
+
};
|
| 300 |
+
|
| 301 |
+
const openPlotFocus = (block) => {
|
| 302 |
+
closePlotFocus();
|
| 303 |
+
window.__featurelens_plot_scroll_y = window.scrollY;
|
| 304 |
+
window.__featurelens_body_overflow = document.body.style.overflow || "";
|
| 305 |
+
document.body.style.overflow = "hidden";
|
| 306 |
+
|
| 307 |
+
const overlay = document.createElement("div");
|
| 308 |
+
overlay.id = "featurelens-plot-focus-overlay";
|
| 309 |
+
const panel = document.createElement("div");
|
| 310 |
+
panel.className = "featurelens-focus-panel";
|
| 311 |
+
const toolbar = document.createElement("div");
|
| 312 |
+
toolbar.className = "featurelens-focus-toolbar";
|
| 313 |
+
const title = document.createElement("div");
|
| 314 |
+
title.className = "featurelens-focus-title";
|
| 315 |
+
title.textContent = readableTitle(block);
|
| 316 |
+
const close = document.createElement("button");
|
| 317 |
+
close.className = "featurelens-focus-close";
|
| 318 |
+
close.type = "button";
|
| 319 |
+
close.setAttribute("aria-label", "Close chart focus view");
|
| 320 |
+
close.textContent = "Close";
|
| 321 |
+
toolbar.append(title, close);
|
| 322 |
+
|
| 323 |
+
const chart = document.createElement("div");
|
| 324 |
+
chart.className = "featurelens-focus-chart";
|
| 325 |
+
const sourceSvg = block.querySelector("svg");
|
| 326 |
+
const sourceCanvas = block.querySelector("canvas");
|
| 327 |
+
if (sourceSvg) {
|
| 328 |
+
const clone = sourceSvg.cloneNode(true);
|
| 329 |
+
clone.removeAttribute("width");
|
| 330 |
+
clone.removeAttribute("height");
|
| 331 |
+
chart.appendChild(clone);
|
| 332 |
+
} else if (sourceCanvas) {
|
| 333 |
+
const canvas = document.createElement("canvas");
|
| 334 |
+
canvas.width = sourceCanvas.width;
|
| 335 |
+
canvas.height = sourceCanvas.height;
|
| 336 |
+
const context = canvas.getContext("2d");
|
| 337 |
+
if (context) context.drawImage(sourceCanvas, 0, 0);
|
| 338 |
+
chart.appendChild(canvas);
|
| 339 |
+
} else {
|
| 340 |
+
const fallback = block.cloneNode(true);
|
| 341 |
+
fallback.querySelectorAll("button").forEach((node) => node.remove());
|
| 342 |
+
fallback.removeAttribute("id");
|
| 343 |
+
chart.appendChild(fallback);
|
| 344 |
+
}
|
| 345 |
+
panel.append(toolbar, chart);
|
| 346 |
+
overlay.appendChild(panel);
|
| 347 |
+
document.body.appendChild(overlay);
|
| 348 |
+
close.addEventListener("click", closePlotFocus);
|
| 349 |
+
overlay.addEventListener("click", (event) => {
|
| 350 |
+
if (event.target === overlay) closePlotFocus();
|
| 351 |
+
});
|
| 352 |
+
};
|
| 353 |
+
|
| 354 |
+
// Intercept Gradio's plot fullscreen button before the native handler. Native fullscreen stretches
|
| 355 |
+
// Vega plots across the monitor; FeatureLens uses a bounded, static focus copy instead.
|
| 356 |
document.addEventListener("click", (event) => {
|
| 357 |
const button = event.target && event.target.closest ? event.target.closest("button") : null;
|
| 358 |
if (!button) return;
|
| 359 |
const label = `${button.getAttribute("aria-label") || ""} ${button.getAttribute("title") || ""} ${button.textContent || ""}`.toLowerCase();
|
| 360 |
if (!label.includes("fullscreen")) return;
|
| 361 |
+
const plot = button.closest(".fl-plot");
|
| 362 |
+
if (!plot) return;
|
| 363 |
+
event.preventDefault();
|
| 364 |
+
event.stopImmediatePropagation();
|
| 365 |
+
openPlotFocus(plot);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
}, true);
|
| 367 |
|
| 368 |
+
document.addEventListener("keydown", (event) => {
|
| 369 |
+
if (event.key === "Escape") closePlotFocus();
|
| 370 |
+
});
|
| 371 |
+
|
| 372 |
// Gradio's native plot exporter currently emits chart.png. Remember which FeatureLens plot initiated
|
| 373 |
// the export, then rename the transient download anchor before the browser handles it.
|
| 374 |
document.addEventListener("click", (event) => {
|
|
|
|
| 411 |
return gr.Button(label, size="sm", variant="primary", elem_classes=["copy-btn"])
|
| 412 |
|
| 413 |
|
| 414 |
+
def _table_heading(text: str) -> gr.Markdown:
|
| 415 |
+
return gr.Markdown(f"#### {text}", elem_classes=["table-heading"])
|
| 416 |
+
|
| 417 |
+
|
| 418 |
def _copy_ack(_text: str) -> None:
|
| 419 |
gr.Info("Copied table with headers.", duration=1.2)
|
| 420 |
|
|
|
|
| 625 |
|
| 626 |
def _discovery_metrics_markdown(result) -> str:
|
| 627 |
if not result.candidate_ids:
|
| 628 |
+
if result.ranking_mode == "causal_ready":
|
| 629 |
+
return (
|
| 630 |
+
f"No positively selective **current-token-active** candidates were found for **{result.concept}** "
|
| 631 |
+
f"at layer **{result.layer}** in this live batch. Try another token/layer or use Balanced selectivity "
|
| 632 |
+
"to discover concept-associated features elsewhere in the prompt."
|
| 633 |
+
)
|
| 634 |
return (
|
| 635 |
f"No positively selective candidate features were found for **{result.concept}** at layer "
|
| 636 |
f"**{result.layer}** in this small live batch. Increase the sample or try another layer."
|
| 637 |
)
|
| 638 |
+
ranking = {
|
| 639 |
+
"balanced_selectivity": "balanced selectivity × coverage × log-magnitude",
|
| 640 |
+
"raw_mean_difference": "raw target-minus-other mean activation",
|
| 641 |
+
"causal_ready": "causal-ready evidence at the current Workbench token",
|
| 642 |
+
}[result.ranking_mode]
|
| 643 |
context = (
|
| 644 |
f"Current Workbench token **{result.current_token_index}** was checked in the same model batch. "
|
| 645 |
"The default candidate prefers the highest-ranked feature active at that token when one exists."
|
|
|
|
| 650 |
f"Concept **{result.concept}** · layer **{result.layer}** · "
|
| 651 |
f"{result.prompts_per_concept} prompts/concept. \n"
|
| 652 |
f"Showing **{len(result.candidate_ids)}** candidates ranked by **{ranking}**. \n"
|
| 653 |
+
f"Displayed candidates active at the selected Workbench token: "
|
| 654 |
+
f"**{result.displayed_current_active_count}/{len(result.candidate_ids)}**. \n"
|
| 655 |
f"{context} \n\n"
|
| 656 |
"This is **candidate discovery**, not a semantic label. The offline held-out AUROC/F1 pipeline remains "
|
| 657 |
"the standard for a concept-selectivity claim."
|
|
|
|
| 663 |
f"{cue}: {count}/{len(result.stems)} contexts"
|
| 664 |
for cue, count in result.cue_active_context_counts.items()
|
| 665 |
)
|
| 666 |
+
interpretation = (
|
| 667 |
+
"No tested cue activated the feature."
|
| 668 |
+
if result.dominant_cue is None or result.active_condition_count == 0
|
| 669 |
+
else (
|
| 670 |
+
f"**Cue-dominant pattern:** `{result.dominant_cue}` activates in every tested context while all "
|
| 671 |
+
"other tested cues are inactive. Under this matrix, the evidence is much more consistent with a "
|
| 672 |
+
"lexical/cue-specific response than with a mathematics-specific response."
|
| 673 |
+
if result.dominant_cue_context_count == len(result.stems)
|
| 674 |
+
and result.off_dominant_active_count == 0
|
| 675 |
+
else (
|
| 676 |
+
f"The strongest cue is `{result.dominant_cue}` ({result.dominant_cue_context_count}/{len(result.stems)} "
|
| 677 |
+
"contexts), but other cues or context dependence remain. Treat the pattern as mixed/context-sensitive."
|
| 678 |
+
)
|
| 679 |
+
)
|
| 680 |
+
)
|
| 681 |
return (
|
| 682 |
f"Feature **{result.feature_id}**, layer **{result.layer}** · active in "
|
| 683 |
f"**{result.active_condition_count}/{result.condition_count}** stem × cue conditions. \n"
|
| 684 |
f"Cue coverage — {active}. \n\n"
|
| 685 |
+
f"{interpretation} \n\n"
|
| 686 |
+
"This is a controlled diagnostic over the tested stems/cues, not a universal semantic label."
|
|
|
|
| 687 |
)
|
| 688 |
|
| 689 |
|
|
|
|
| 1165 |
workbench_token_index: int,
|
| 1166 |
):
|
| 1167 |
try:
|
| 1168 |
+
ranking_mode = {
|
| 1169 |
+
"Balanced selectivity": "balanced_selectivity",
|
| 1170 |
+
"Raw mean difference": "raw_mean_difference",
|
| 1171 |
+
"Causal-ready at current token": "causal_ready",
|
| 1172 |
+
}[ranking_label]
|
| 1173 |
result = RUNTIME.concept_feature_discovery(
|
| 1174 |
concept=concept,
|
| 1175 |
layer=int(layer),
|
|
|
|
| 1286 |
with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_width=True) as demo:
|
| 1287 |
gr.HTML(
|
| 1288 |
'<header class="hero">'
|
| 1289 |
+
'<h1>FeatureLens <span style="font-size:.48em;opacity:.58;font-weight:400">v0.8</span></h1>'
|
| 1290 |
'<div class="subtitle">Causal Interpretability Workbench</div>'
|
| 1291 |
'<div class="question">Discover sparse features, test robustness, and separate correlation from causal influence.</div>'
|
| 1292 |
'</header>'
|
|
|
|
| 1373 |
analysis_metrics = gr.Markdown()
|
| 1374 |
with gr.Row(equal_height=False):
|
| 1375 |
with gr.Column(scale=3):
|
| 1376 |
+
_table_heading('Strongest active SAE features')
|
| 1377 |
feature_table = gr.Dataframe(
|
| 1378 |
headers=["Rank", "Feature id", "Activation", "Offline concept hint"],
|
| 1379 |
datatype=["number", "number", "number", "str"],
|
| 1380 |
interactive=False,
|
| 1381 |
+
label="Strongest active SAE features", show_label=False,
|
| 1382 |
wrap=False,
|
| 1383 |
max_height=380,
|
| 1384 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
|
|
|
| 1440 |
with gr.Row():
|
| 1441 |
baseline_out = gr.Textbox(label="Baseline greedy generation", lines=6, interactive=False)
|
| 1442 |
modified_out = gr.Textbox(label="SAE-edited greedy generation", lines=6, interactive=False)
|
| 1443 |
+
_table_heading('Next-token distribution shift')
|
| 1444 |
token_prob_table = gr.Dataframe(
|
| 1445 |
interactive=False,
|
| 1446 |
+
label="Next-token distribution shift", show_label=False,
|
| 1447 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1448 |
wrap=False,
|
| 1449 |
max_height=380,
|
| 1450 |
)
|
| 1451 |
token_prob_tsv = gr.Textbox(visible="hidden")
|
| 1452 |
token_prob_copy = _copy_button()
|
| 1453 |
+
_table_heading('Target continuation token-by-token score')
|
| 1454 |
target_token_table = gr.Dataframe(
|
| 1455 |
interactive=False,
|
| 1456 |
+
label="Target continuation token-by-token score", show_label=False,
|
| 1457 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1458 |
wrap=False,
|
| 1459 |
max_height=380,
|
|
|
|
| 1464 |
gr.HTML('<div class="section-rule">III. Single-feature scale dose-response</div>')
|
| 1465 |
with gr.Group():
|
| 1466 |
gr.Markdown("**Uses current Workbench prompt / layer / token.** Choose the feature for this experiment below.")
|
| 1467 |
+
with gr.Row(equal_height=True):
|
| 1468 |
+
dose_feature_id = gr.Dropdown(
|
| 1469 |
+
choices=[],
|
| 1470 |
+
allow_custom_value=True,
|
| 1471 |
+
label="Dose-response feature id",
|
| 1472 |
+
info="Populated from Workbench inspection, or enter any valid feature id directly.",
|
| 1473 |
+
scale=2,
|
| 1474 |
+
)
|
| 1475 |
+
dose_target_text = gr.Textbox(
|
| 1476 |
+
label="Dose-response target continuation",
|
| 1477 |
+
value="2x",
|
| 1478 |
+
info="Independent of the Single-feature causal test target field.",
|
| 1479 |
+
scale=2,
|
| 1480 |
+
)
|
| 1481 |
gr.Markdown(
|
| 1482 |
+
"**Standalone experiment:** you do not need to run Section II first. This panel reads the prompt, "
|
| 1483 |
+
"layer, and token fields from Section I and uses its own feature + target fields above. "
|
| 1484 |
"Always a **scale** experiment: 0× = ablation, 1× = numerical no-edit reference, 2× = double "
|
| 1485 |
+
"the native coefficient. The six conditions are evaluated together against the 1× row."
|
|
|
|
| 1486 |
)
|
| 1487 |
dose_btn = gr.Button("Run scale dose-response", variant="primary", elem_classes=["action-btn"])
|
| 1488 |
dose_metrics = gr.Markdown()
|
| 1489 |
with gr.Row(equal_height=False):
|
| 1490 |
with gr.Column(scale=3):
|
| 1491 |
+
_table_heading('Scale dose-response measurements')
|
| 1492 |
dose_table = gr.Dataframe(
|
| 1493 |
interactive=False,
|
| 1494 |
+
label="Scale dose-response measurements", show_label=False,
|
| 1495 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1496 |
wrap=False,
|
| 1497 |
max_height=380,
|
|
|
|
| 1547 |
contrastive_metrics = gr.Markdown()
|
| 1548 |
with gr.Row(equal_height=False):
|
| 1549 |
with gr.Column(scale=3):
|
| 1550 |
+
_table_heading('Contrastive continuation scores')
|
| 1551 |
contrastive_table = gr.Dataframe(
|
| 1552 |
interactive=False,
|
| 1553 |
+
label="Contrastive continuation scores", show_label=False,
|
| 1554 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1555 |
wrap=False,
|
| 1556 |
max_height=320,
|
|
|
|
| 1615 |
set_metrics = gr.Markdown()
|
| 1616 |
with gr.Row(equal_height=False):
|
| 1617 |
with gr.Column(scale=2):
|
| 1618 |
+
_table_heading('Joint intervention features')
|
| 1619 |
set_feature_table = gr.Dataframe(
|
| 1620 |
interactive=False,
|
| 1621 |
+
label="Joint intervention features", show_label=False,
|
| 1622 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1623 |
wrap=False,
|
| 1624 |
max_height=380,
|
|
|
|
| 1626 |
set_feature_tsv = gr.Textbox(visible="hidden")
|
| 1627 |
set_feature_copy = _copy_button()
|
| 1628 |
with gr.Column(scale=3):
|
| 1629 |
+
_table_heading('Target continuation token-by-token score')
|
| 1630 |
set_target_table = gr.Dataframe(
|
| 1631 |
interactive=False,
|
| 1632 |
+
label="Target continuation token-by-token score", show_label=False,
|
| 1633 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1634 |
wrap=False,
|
| 1635 |
max_height=380,
|
|
|
|
| 1647 |
set_sweep_note = gr.Markdown()
|
| 1648 |
with gr.Row(equal_height=False):
|
| 1649 |
with gr.Column(scale=3):
|
| 1650 |
+
_table_heading('Feature-set size measurements')
|
| 1651 |
set_sweep_table = gr.Dataframe(
|
| 1652 |
interactive=False,
|
| 1653 |
+
label="Feature-set size measurements", show_label=False,
|
| 1654 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1655 |
wrap=False,
|
| 1656 |
max_height=380,
|
|
|
|
| 1684 |
interaction_metrics = gr.Markdown()
|
| 1685 |
with gr.Row(equal_height=False):
|
| 1686 |
with gr.Column(scale=3):
|
| 1687 |
+
_table_heading('Individual and joint ablation measurements')
|
| 1688 |
interaction_table = gr.Dataframe(
|
| 1689 |
interactive=False,
|
| 1690 |
+
label="Individual and joint ablation measurements", show_label=False,
|
| 1691 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1692 |
wrap=False,
|
| 1693 |
max_height=380,
|
|
|
|
| 1722 |
geometry_metrics = gr.Markdown()
|
| 1723 |
with gr.Row(equal_height=False):
|
| 1724 |
with gr.Column(scale=3):
|
| 1725 |
+
_table_heading('Pairwise decoder geometry')
|
| 1726 |
geometry_table = gr.Dataframe(
|
| 1727 |
interactive=False,
|
| 1728 |
+
label="Pairwise decoder geometry", show_label=False,
|
| 1729 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1730 |
wrap=False,
|
| 1731 |
max_height=340,
|
|
|
|
| 1774 |
discovery_top_n = gr.Slider(5, 20, value=12, step=1, label="Candidate features")
|
| 1775 |
with gr.Row(equal_height=True):
|
| 1776 |
discovery_ranking = gr.Dropdown(
|
| 1777 |
+
choices=["Balanced selectivity", "Causal-ready at current token", "Raw mean difference"],
|
| 1778 |
value="Balanced selectivity",
|
| 1779 |
label="Candidate ranking",
|
| 1780 |
+
info="Balanced selectivity finds concept-associated candidates; Causal-ready restricts to features active at the selected Workbench token; raw mean difference exposes scale-dominated ranking.",
|
| 1781 |
scale=2,
|
| 1782 |
)
|
| 1783 |
gr.Markdown(
|
|
|
|
| 1788 |
discovery_metrics = gr.Markdown()
|
| 1789 |
with gr.Row(equal_height=False):
|
| 1790 |
with gr.Column(scale=3):
|
| 1791 |
+
_table_heading('Candidate feature evidence')
|
| 1792 |
discovery_table = gr.Dataframe(
|
| 1793 |
+
interactive=False, label="Candidate feature evidence", show_label=False, buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1794 |
wrap=False, max_height=420
|
| 1795 |
)
|
| 1796 |
discovery_tsv = gr.Textbox(visible="hidden")
|
|
|
|
| 1849 |
trace_metrics = gr.Markdown()
|
| 1850 |
with gr.Row(equal_height=False):
|
| 1851 |
with gr.Column(scale=3):
|
| 1852 |
+
_table_heading('Feature activation by prompt token')
|
| 1853 |
trace_table = gr.Dataframe(
|
| 1854 |
interactive=False,
|
| 1855 |
+
label="Feature activation by prompt token", show_label=False,
|
| 1856 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1857 |
wrap=False,
|
| 1858 |
max_height=340,
|
|
|
|
| 1886 |
cue_metrics = gr.Markdown()
|
| 1887 |
with gr.Row(equal_height=False):
|
| 1888 |
with gr.Column(scale=3):
|
| 1889 |
+
_table_heading('Feature response by completion cue')
|
| 1890 |
+
cue_table = gr.Dataframe(interactive=False, label="Feature response by completion cue", show_label=False, buttons=["fullscreen"], elem_classes=["result-table"], wrap=False, max_height=340)
|
| 1891 |
cue_tsv = gr.Textbox(visible="hidden")
|
| 1892 |
cue_copy = _copy_button()
|
| 1893 |
with gr.Column(scale=2):
|
|
|
|
| 1919 |
cue_context_metrics = gr.Markdown()
|
| 1920 |
with gr.Row(equal_height=False):
|
| 1921 |
with gr.Column(scale=3):
|
| 1922 |
+
_table_heading('Cue × context feature response')
|
| 1923 |
cue_context_table = gr.Dataframe(
|
| 1924 |
interactive=False,
|
| 1925 |
+
label="Cue × context feature response", show_label=False,
|
| 1926 |
buttons=["fullscreen"],
|
| 1927 |
elem_classes=["result-table"],
|
| 1928 |
wrap=False,
|
|
|
|
| 1935 |
x="Prompt stem",
|
| 1936 |
y="Activation",
|
| 1937 |
color="Cue",
|
| 1938 |
+
color_map={
|
| 1939 |
+
"is": INK_TEAL,
|
| 1940 |
+
"=": INK_UMBER,
|
| 1941 |
+
":": INK_RED,
|
| 1942 |
+
"equals": INK_PLUM,
|
| 1943 |
+
"therefore": INK_STONE,
|
| 1944 |
+
},
|
| 1945 |
title="Cue response across contexts",
|
| 1946 |
elem_id="plot-cue-context-matrix",
|
| 1947 |
x_title="Prompt stem",
|
|
|
|
| 1958 |
contrast_metrics = gr.Markdown()
|
| 1959 |
with gr.Row(equal_height=False):
|
| 1960 |
with gr.Column(scale=3):
|
| 1961 |
+
_table_heading('Feature activation by controlled concept')
|
| 1962 |
contrast_table = gr.Dataframe(
|
| 1963 |
interactive=False,
|
| 1964 |
+
label="Feature activation by controlled concept", show_label=False,
|
| 1965 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 1966 |
wrap=False,
|
| 1967 |
max_height=380,
|
|
|
|
| 2020 |
para_metrics = gr.Markdown()
|
| 2021 |
with gr.Row(equal_height=False):
|
| 2022 |
with gr.Column(scale=3):
|
| 2023 |
+
_table_heading('Top-feature overlap at selected tokens')
|
| 2024 |
para_table = gr.Dataframe(
|
| 2025 |
interactive=False,
|
| 2026 |
+
label="Top-feature overlap at selected tokens", show_label=False,
|
| 2027 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 2028 |
wrap=False,
|
| 2029 |
max_height=380,
|
|
|
|
| 2068 |
trajectory_tokens = gr.HTML()
|
| 2069 |
with gr.Row(equal_height=False):
|
| 2070 |
with gr.Column(scale=3):
|
| 2071 |
+
_table_heading('Layer diagnostics')
|
| 2072 |
trajectory_table = gr.Dataframe(
|
| 2073 |
interactive=False,
|
| 2074 |
+
label="Layer diagnostics", show_label=False,
|
| 2075 |
buttons=["fullscreen"], elem_classes=["result-table"],
|
| 2076 |
wrap=False,
|
| 2077 |
max_height=380,
|
|
|
|
| 2183 |
)
|
| 2184 |
dose_btn.click(
|
| 2185 |
run_dose_response,
|
| 2186 |
+
inputs=[prompt, layer, token_index, dose_feature_id, dose_target_text],
|
| 2187 |
outputs=[dose_table, dose_plot, dose_metrics, dose_tsv],
|
| 2188 |
)
|
| 2189 |
contrastive_mode.change(mode_help, inputs=[contrastive_mode], outputs=[contrastive_coefficient])
|
|
|
|
| 2292 |
theme=THEME,
|
| 2293 |
ssr_mode=False,
|
| 2294 |
show_error=True,
|
| 2295 |
+
)
|
docs/METHODOLOGY.md
CHANGED
|
@@ -391,3 +391,29 @@ S_f = \max(0,\mathrm{selectivity}_f) \cdot \mathrm{target\ rate}_f \cdot \log(1
|
|
| 391 |
$$
|
| 392 |
|
| 393 |
This ranking is still exploratory. It is designed to triage candidates, not replace held-out AUROC/F1. The same forward pass optionally includes the current Workbench prompt, allowing the candidate table to report current-prompt maximum activation and selected-token activation. A candidate can therefore be concept-associated in the live batch but visibly inactive at the current causal location.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
$$
|
| 392 |
|
| 393 |
This ranking is still exploratory. It is designed to triage candidates, not replace held-out AUROC/F1. The same forward pass optionally includes the current Workbench prompt, allowing the candidate table to report current-prompt maximum activation and selected-token activation. A candidate can therefore be concept-associated in the live batch but visibly inactive at the current causal location.
|
| 394 |
+
|
| 395 |
+
## v0.8 causal-ready candidate mode
|
| 396 |
+
|
| 397 |
+
Prompt-wide concept association and immediate causal usability are different constraints. A feature may rank highly for a concept across the controlled prompt set while having zero activation at the selected Workbench token, in which case ablation at that location is a no-op.
|
| 398 |
+
|
| 399 |
+
The **Causal-ready at current token** mode therefore requires:
|
| 400 |
+
|
| 401 |
+
```text
|
| 402 |
+
target_mean > 0
|
| 403 |
+
mean_difference > 0
|
| 404 |
+
current_token_activation > 0
|
| 405 |
+
```
|
| 406 |
+
|
| 407 |
+
and ranks eligible features using the balanced score multiplied by a log-scaled current-token activation term:
|
| 408 |
+
|
| 409 |
+
$$
|
| 410 |
+
S_f^{\mathrm{ready}} = S_f \cdot \log(1 + z_f^{\mathrm{current\ token}}).
|
| 411 |
+
$$
|
| 412 |
+
|
| 413 |
+
The log factor makes current-token presence matter without allowing a single very large coefficient to dominate as strongly as a raw activation product would. This remains an exploratory triage score, not held-out concept evidence.
|
| 414 |
+
|
| 415 |
+
## v0.8 cue-dominance summary
|
| 416 |
+
|
| 417 |
+
The cue × context matrix now summarizes the measured activation pattern rather than always returning generic interpretation text. For each cue, FeatureLens counts the number of tested contexts in which the feature is active and computes mean activation across those contexts.
|
| 418 |
+
|
| 419 |
+
A particularly strong tested cue-specific pattern occurs when one cue activates in every tested context and all other tested cues remain inactive. FeatureLens describes that pattern as **cue-dominant under the tested matrix**. The wording is intentionally local to the controlled stems and cues; it does not assert a universal semantic label for the SAE feature.
|
docs/VALIDATION.md
CHANGED
|
@@ -1,59 +1,34 @@
|
|
| 1 |
-
# FeatureLens v0.
|
| 2 |
|
| 3 |
-
This guide tests the
|
| 4 |
|
| 5 |
-
##
|
| 6 |
|
| 7 |
From the repository root:
|
| 8 |
|
| 9 |
```bash
|
| 10 |
-
python3 -m pytest -q
|
| 11 |
-
python3 -m compileall -q app.py featurelens experiments scripts
|
| 12 |
-
python3
|
|
|
|
| 13 |
python3 scripts/release_check.py
|
| 14 |
```
|
| 15 |
|
| 16 |
-
Expected:
|
| 17 |
|
| 18 |
```text
|
| 19 |
-
45 passed
|
| 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 |
-
## B. UI cleanup, table typography, focus mode, and export names
|
| 31 |
-
|
| 32 |
-
1. Open **Workbench**.
|
| 33 |
-
2. Confirm these are normal always-visible sections, **not collapsible dropdowns/accordions**:
|
| 34 |
-
- **III. Single-feature scale dose-response**
|
| 35 |
-
- **IV. Contrastive continuation preference**
|
| 36 |
-
3. Confirm there is only one experiment heading before each section. A plot title such as `Scale dose-response` is fine; there should not be a second accordion heading with the same text.
|
| 37 |
-
4. After any result table appears, confirm its table title (for example **Strongest active SAE features**) is visually larger/bolder than ordinary body text, and column headings are clearly readable.
|
| 38 |
-
5. Produce a plot and click its top-right **fullscreen/focus** icon.
|
| 39 |
-
- The focused plot should open near the top-center of the viewport.
|
| 40 |
-
- It should be bounded rather than stretching across the entire monitor.
|
| 41 |
-
- Axis labels should remain readable without needing to pan horizontally across a huge plot.
|
| 42 |
-
6. Exit focus mode. You should return to approximately the same page location you were at before opening it.
|
| 43 |
-
7. Repeat the focus test on one wide result table.
|
| 44 |
-
8. Click the plot **export** icon. Verify the downloaded filename is descriptive, for example:
|
| 45 |
-
|
| 46 |
-
```text
|
| 47 |
-
featurelens_activation-profile.png
|
| 48 |
-
featurelens_scale-dose-response.png
|
| 49 |
```
|
| 50 |
|
| 51 |
-
|
| 52 |
-
9. Click **Copy table with headers** and confirm the temporary `✓ Copied with headers` feedback still appears.
|
| 53 |
-
|
| 54 |
-
## C. Establish the shared Workbench context
|
| 55 |
|
| 56 |
-
|
| 57 |
|
| 58 |
```text
|
| 59 |
Prompt: The derivative of x squared is
|
|
@@ -64,40 +39,57 @@ Displayed active features: 12
|
|
| 64 |
|
| 65 |
Click **Inspect sparse features**.
|
| 66 |
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
reconstruction cosine: 0.9015
|
| 73 |
-
NMSE: 0.1874
|
| 74 |
-
Top-5 activation mass: 26.7%
|
| 75 |
-
```
|
| 76 |
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
-
|
| 80 |
|
| 81 |
-
|
|
|
|
| 82 |
|
| 83 |
-
|
| 84 |
|
| 85 |
-
|
| 86 |
-
- `french_language` is not available.
|
| 87 |
|
| 88 |
-
|
|
|
|
|
|
|
| 89 |
|
| 90 |
```text
|
| 91 |
-
|
|
|
|
| 92 |
```
|
| 93 |
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
-
|
| 97 |
|
| 98 |
-
|
| 99 |
|
| 100 |
-
|
| 101 |
|
| 102 |
```text
|
| 103 |
Target concept: mathematics
|
|
@@ -107,73 +99,76 @@ Candidate features: 12
|
|
| 107 |
Candidate ranking: Balanced selectivity
|
| 108 |
```
|
| 109 |
|
| 110 |
-
|
| 111 |
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
```text
|
| 115 |
-
|
| 116 |
-
Feature id
|
| 117 |
-
Candidate score
|
| 118 |
-
Target mean max
|
| 119 |
-
Other mean max
|
| 120 |
-
Mean difference
|
| 121 |
-
Selectivity
|
| 122 |
-
Target activation rate
|
| 123 |
-
Other activation rate
|
| 124 |
-
Current prompt max
|
| 125 |
-
Current token activation
|
| 126 |
-
Active at current token
|
| 127 |
```
|
| 128 |
|
| 129 |
-
|
| 130 |
|
| 131 |
-
|
| 132 |
-
-
|
| 133 |
-
-
|
| 134 |
-
-
|
| 135 |
|
| 136 |
-
|
| 137 |
|
| 138 |
-
##
|
| 139 |
|
| 140 |
-
|
| 141 |
|
| 142 |
```text
|
| 143 |
Candidate ranking: Raw mean difference
|
| 144 |
```
|
| 145 |
|
| 146 |
-
|
| 147 |
|
| 148 |
-
|
|
|
|
|
|
|
| 149 |
|
| 150 |
-
|
| 151 |
|
| 152 |
-
|
| 153 |
|
| 154 |
-
|
| 155 |
|
| 156 |
-
|
| 157 |
-
2. Click **Use selected candidate across feature tests**.
|
| 158 |
-
3. A visible confirmation should say that the feature was loaded.
|
| 159 |
-
4. Verify the same id appears in:
|
| 160 |
-
- **Workbench → Single feature id**;
|
| 161 |
-
- **Workbench → Dose-response feature id**;
|
| 162 |
-
- **Workbench → Contrastive feature id**;
|
| 163 |
-
- **Feature evidence → Feature id**.
|
| 164 |
|
| 165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
|
| 167 |
-
##
|
| 168 |
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
Set:
|
| 172 |
|
| 173 |
```text
|
| 174 |
Feature id: 22632
|
| 175 |
Residual layer: 14
|
| 176 |
-
Prompt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
Completion cues:
|
| 178 |
is
|
| 179 |
=
|
|
@@ -182,101 +177,75 @@ equals
|
|
| 182 |
therefore
|
| 183 |
```
|
| 184 |
|
| 185 |
-
Click **Run
|
| 186 |
-
|
| 187 |
-
The `is` condition reconstructs the original prompt and should remain approximately:
|
| 188 |
|
| 189 |
-
``
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
|
|
|
| 193 |
|
| 194 |
-
|
| 195 |
|
| 196 |
-
##
|
| 197 |
|
| 198 |
-
|
| 199 |
|
| 200 |
-
|
| 201 |
|
| 202 |
```text
|
| 203 |
-
|
| 204 |
-
|
| 205 |
|
| 206 |
-
|
| 207 |
-
The derivative of x squared
|
| 208 |
-
The capital of Germany
|
| 209 |
-
The weather today
|
| 210 |
-
My name
|
| 211 |
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
=
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
therefore
|
| 218 |
-
```
|
| 219 |
|
| 220 |
-
|
| 221 |
|
| 222 |
-
|
| 223 |
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
|
|
|
| 227 |
|
| 228 |
-
|
|
|
|
|
|
|
|
|
|
| 229 |
|
| 230 |
-
|
| 231 |
|
| 232 |
-
|
| 233 |
|
| 234 |
-
|
| 235 |
|
| 236 |
```text
|
| 237 |
-
|
| 238 |
-
Residual layer: 14
|
| 239 |
-
Prompts per concept: 4
|
| 240 |
```
|
| 241 |
|
| 242 |
-
Click **Run
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
2.
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
- this is the top-k causal sweep;
|
| 263 |
-
- it is **not** the Workbench `Strongest active SAE features` table;
|
| 264 |
-
- confirm k = 1, 3, 5 rows appear.
|
| 265 |
-
6. After dynamic results expand, scroll to the actual bottom without switching tabs. The v0.5 reflow fix should remain intact.
|
| 266 |
-
|
| 267 |
-
## K. Deferred final hardening
|
| 268 |
-
|
| 269 |
-
Do not spend ZeroGPU quota on the complete adversarial suite yet. Preserve these for the final hardening release:
|
| 270 |
-
|
| 271 |
-
- empty prompts;
|
| 272 |
-
- out-of-range token indices;
|
| 273 |
-
- feature ids `0`, `32767`, invalid `32768`;
|
| 274 |
-
- inactive feature ablation vs injection;
|
| 275 |
-
- whitespace-sensitive targets such as `2x` vs ` 2x`;
|
| 276 |
-
- missing required target continuations;
|
| 277 |
-
- unrelated paraphrases;
|
| 278 |
-
- negation and mixed-language prompts;
|
| 279 |
-
- long-prompt truncation;
|
| 280 |
-
- rapid repeated clicks / queue behavior;
|
| 281 |
-
- narrow/mobile-width layout;
|
| 282 |
-
- final copy/focus/export sweep.
|
|
|
|
| 1 |
+
# FeatureLens v0.8 validation
|
| 2 |
|
| 3 |
+
This guide tests the v0.8 changes plus a small set of high-value regressions. Use the **exact UI labels** below. The full adversarial/edge suite remains deferred until final hardening.
|
| 4 |
|
| 5 |
+
## 0. Local release gate
|
| 6 |
|
| 7 |
From the repository root:
|
| 8 |
|
| 9 |
```bash
|
| 10 |
+
python3 -m pytest -q && \
|
| 11 |
+
python3 -m compileall -q app.py featurelens experiments scripts && \
|
| 12 |
+
python3 -m ruff check app.py featurelens experiments tests scripts && \
|
| 13 |
+
python3 scripts/ui_smoke.py && \
|
| 14 |
python3 scripts/release_check.py
|
| 15 |
```
|
| 16 |
|
| 17 |
+
Expected release footer:
|
| 18 |
|
| 19 |
```text
|
|
|
|
|
|
|
| 20 |
FeatureLens release check: PASS
|
| 21 |
discovery prompts: 224
|
| 22 |
causal tasks: 28
|
| 23 |
layers: [4, 14, 26]
|
| 24 |
feature-set sizes: [1, 3, 5]
|
| 25 |
random controls: 8
|
| 26 |
+
release: v0.8.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
```
|
| 28 |
|
| 29 |
+
## 1. Establish the canonical Workbench context
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
+
Open **Workbench → I. Inspect a prompt location** and set:
|
| 32 |
|
| 33 |
```text
|
| 34 |
Prompt: The derivative of x squared is
|
|
|
|
| 39 |
|
| 40 |
Click **Inspect sparse features**.
|
| 41 |
|
| 42 |
+
Pass conditions:
|
| 43 |
+
- **Current Workbench context** reports layer 14 and token 5 (`' is'`).
|
| 44 |
+
- **Strongest active SAE features** begins with feature `22632` at about `32.90625`.
|
| 45 |
+
- Result-table titles such as **Strongest active SAE features** are visually larger than table body text and are not rendered as tiny native component labels.
|
| 46 |
|
| 47 |
+
## 2. Plot focus-view regression
|
| 48 |
+
|
| 49 |
+
On **Activation profile**, click the top-right fullscreen/focus icon.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
+
Pass conditions:
|
| 52 |
+
- FeatureLens opens a darkened overlay with a centered chart rather than Gradio's stretched native fullscreen view.
|
| 53 |
+
- The chart reading surface is bounded to roughly desktop-document width; it should not require multiple horizontal scrolls.
|
| 54 |
+
- The focused chart starts at its own top.
|
| 55 |
+
- Press **Close**, click the backdrop, or press **Esc**.
|
| 56 |
+
- The page returns to exactly the same Workbench scroll position.
|
| 57 |
|
| 58 |
+
Then click the export icon.
|
| 59 |
|
| 60 |
+
Pass condition:
|
| 61 |
+
- the filename is descriptive (for example `featurelens_activation-profile.png`), not `chart.png`.
|
| 62 |
|
| 63 |
+
Repeat the focus-view check on one long-label plot such as **Cue response across contexts**.
|
| 64 |
|
| 65 |
+
## 3. Standalone scale dose-response
|
|
|
|
| 66 |
|
| 67 |
+
Do **not** run **II. Single-feature causal test** first.
|
| 68 |
+
|
| 69 |
+
Under **III. Single-feature scale dose-response**, set:
|
| 70 |
|
| 71 |
```text
|
| 72 |
+
Dose-response feature id: 22632
|
| 73 |
+
Dose-response target continuation: 2x
|
| 74 |
```
|
| 75 |
|
| 76 |
+
Click **Run scale dose-response**.
|
| 77 |
+
|
| 78 |
+
Pass conditions:
|
| 79 |
+
- The experiment runs without using the optional target field from Section II.
|
| 80 |
+
- No `Enter a target continuation...` error occurs.
|
| 81 |
+
- The 1× row is the exact numerical null:
|
| 82 |
+
- Δ feature coefficient = 0
|
| 83 |
+
- Perturbation L2 = 0
|
| 84 |
+
- Δ mean log p/token = 0
|
| 85 |
+
- Δ sequence log p = 0
|
| 86 |
+
- Next-token JS = 0
|
| 87 |
|
| 88 |
+
The expected qualitative curve from prior Qwen runs is positive under ablation and negative at stronger amplification; exact floating-point values may vary slightly.
|
| 89 |
|
| 90 |
+
## 4. Candidate discovery — Balanced selectivity
|
| 91 |
|
| 92 |
+
Open **Feature evidence → A. Concept-guided candidate discovery** and set:
|
| 93 |
|
| 94 |
```text
|
| 95 |
Target concept: mathematics
|
|
|
|
| 99 |
Candidate ranking: Balanced selectivity
|
| 100 |
```
|
| 101 |
|
| 102 |
+
Click **Discover concept-associated candidates**.
|
| 103 |
|
| 104 |
+
Pass conditions:
|
| 105 |
+
- **Candidate feature evidence** has an explicit readable heading.
|
| 106 |
+
- The result summary states how many displayed candidates are active at the selected Workbench token.
|
| 107 |
+
- The ranking is selectivity-oriented rather than dominated by giant globally active features.
|
| 108 |
+
|
| 109 |
+
Record the top 12 rows.
|
| 110 |
+
|
| 111 |
+
## 5. Candidate discovery — Causal-ready at current token
|
| 112 |
+
|
| 113 |
+
Change only:
|
| 114 |
|
| 115 |
```text
|
| 116 |
+
Candidate ranking: Causal-ready at current token
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
```
|
| 118 |
|
| 119 |
+
Click **Discover concept-associated candidates**.
|
| 120 |
|
| 121 |
+
Pass conditions:
|
| 122 |
+
- Every returned row has **Active at current token = True**.
|
| 123 |
+
- The result summary reports `displayed/current-active = N/N`.
|
| 124 |
+
- If no positively selective current-token-active feature exists, the UI reports that clearly instead of silently falling back to a prompt-wide candidate.
|
| 125 |
|
| 126 |
+
This mode is intentionally stricter than Balanced selectivity: it asks for a candidate that is both concept-associated in the live batch **and immediately ablatable at the selected Workbench token**.
|
| 127 |
|
| 128 |
+
## 6. Candidate discovery — Raw mean difference
|
| 129 |
|
| 130 |
+
Change only:
|
| 131 |
|
| 132 |
```text
|
| 133 |
Candidate ranking: Raw mean difference
|
| 134 |
```
|
| 135 |
|
| 136 |
+
Click **Discover concept-associated candidates**.
|
| 137 |
|
| 138 |
+
Pass conditions:
|
| 139 |
+
- large-scale features such as the previously observed `31205`, `28345`, or other globally high-activation directions may return near the top;
|
| 140 |
+
- this visibly demonstrates why raw activation difference is a scale-sensitive baseline rather than the recommended discovery ranking.
|
| 141 |
|
| 142 |
+
## 7. Candidate handoff
|
| 143 |
|
| 144 |
+
Return to **Balanced selectivity** or **Causal-ready at current token**.
|
| 145 |
|
| 146 |
+
Click a row in **Candidate feature evidence**, then click:
|
| 147 |
|
| 148 |
+
**Use selected candidate across feature tests**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
+
Pass conditions:
|
| 151 |
+
- the status message names the selected feature;
|
| 152 |
+
- the feature propagates to:
|
| 153 |
+
- **Single feature id**
|
| 154 |
+
- **Dose-response feature id**
|
| 155 |
+
- **Contrastive feature id**
|
| 156 |
+
- **Feature evidence → Feature id**
|
| 157 |
+
- no GPU job is launched by the handoff action itself.
|
| 158 |
|
| 159 |
+
## 8. Cue × context specificity for feature 22632
|
| 160 |
|
| 161 |
+
Under **Feature evidence → D. Cue × context specificity**, use:
|
|
|
|
|
|
|
| 162 |
|
| 163 |
```text
|
| 164 |
Feature id: 22632
|
| 165 |
Residual layer: 14
|
| 166 |
+
Prompt stems:
|
| 167 |
+
The derivative of x squared
|
| 168 |
+
The capital of Germany
|
| 169 |
+
The weather today
|
| 170 |
+
My name
|
| 171 |
+
|
| 172 |
Completion cues:
|
| 173 |
is
|
| 174 |
=
|
|
|
|
| 177 |
therefore
|
| 178 |
```
|
| 179 |
|
| 180 |
+
Click **Run cue × context scan**.
|
|
|
|
|
|
|
| 181 |
|
| 182 |
+
Prior live evidence found `is` active in all 4 contexts and every other cue inactive. If that reproduces, pass conditions are:
|
| 183 |
+
- summary explicitly calls the tested matrix a **cue-dominant pattern**;
|
| 184 |
+
- `is: 4/4 contexts`;
|
| 185 |
+
- all off-cue conditions remain inactive;
|
| 186 |
+
- the plot uses the restrained FeatureLens cue palette rather than saturated default blue/orange/red/green colors.
|
| 187 |
|
| 188 |
+
Scientific interpretation: this is evidence that `22632` is much more consistent with an `is`-specific lexical/structural response under the tested matrix than with a mathematics-specific feature. It remains a controlled diagnostic, not a universal feature label.
|
| 189 |
|
| 190 |
+
## 9. High-value regressions
|
| 191 |
|
| 192 |
+
### 9a. Paraphrase identity control
|
| 193 |
|
| 194 |
+
Open **Paraphrase robustness** and set both text fields to:
|
| 195 |
|
| 196 |
```text
|
| 197 |
+
The derivative of x squared is
|
| 198 |
+
```
|
| 199 |
|
| 200 |
+
Use layer 14 and final token for both.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
|
| 202 |
+
Expected:
|
| 203 |
+
- selected-token Jaccard = 1.000
|
| 204 |
+
- selected-token cosine = 1.000
|
| 205 |
+
- prompt-wide Jaccard = 1.000
|
| 206 |
+
- prompt-wide cosine = 1.000
|
|
|
|
|
|
|
| 207 |
|
| 208 |
+
### 9b. Layer trajectory
|
| 209 |
|
| 210 |
+
Open **Layer trajectory** with:
|
| 211 |
|
| 212 |
+
```text
|
| 213 |
+
Prompt: The derivative of x squared is
|
| 214 |
+
Prompt token index: -1
|
| 215 |
+
```
|
| 216 |
|
| 217 |
+
Expected approximate regression values:
|
| 218 |
+
- layer 4 cosine ≈ 0.962
|
| 219 |
+
- layer 14 cosine ≈ 0.902
|
| 220 |
+
- layer 26 cosine ≈ 0.961
|
| 221 |
|
| 222 |
+
### 9c. Feature-set sensitivity
|
| 223 |
|
| 224 |
+
Open **Feature sets → Set-size sensitivity**.
|
| 225 |
|
| 226 |
+
Set:
|
| 227 |
|
| 228 |
```text
|
| 229 |
+
Target continuation for set-size sweep: 2x
|
|
|
|
|
|
|
| 230 |
```
|
| 231 |
|
| 232 |
+
Click **Run 1/3/5-feature ablation sweep**.
|
| 233 |
+
|
| 234 |
+
Pass conditions:
|
| 235 |
+
- rows for k = 1, 3, 5;
|
| 236 |
+
- 8 random controls per k;
|
| 237 |
+
- all conditions share the batched null reference;
|
| 238 |
+
- no regression to a single arbitrary random control.
|
| 239 |
+
|
| 240 |
+
## 10. What to send back
|
| 241 |
+
|
| 242 |
+
For v0.8 acceptance, send:
|
| 243 |
+
1. whether the bounded plot focus view finally solves the ultrawide/stretch problem;
|
| 244 |
+
2. whether closing focus returns to the same page position;
|
| 245 |
+
3. whether table headings are finally readable;
|
| 246 |
+
4. the **Causal-ready at current token** candidate table (or the explicit no-candidate message);
|
| 247 |
+
5. the cue × context summary for `22632`;
|
| 248 |
+
6. whether standalone dose response works without Section II;
|
| 249 |
+
7. any unexpected regression from 9a–9c.
|
| 250 |
+
|
| 251 |
+
The comprehensive malformed-input, invalid-feature-id, long-prompt, queue, narrow-screen, and repeated-click adversarial suite is still reserved for final release hardening.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
featurelens/runtime.py
CHANGED
|
@@ -240,6 +240,7 @@ class ConceptFeatureDiscoveryResult:
|
|
| 240 |
default_candidate_id: int | None
|
| 241 |
current_context_available: bool
|
| 242 |
current_token_index: int | None
|
|
|
|
| 243 |
|
| 244 |
|
| 245 |
@dataclass
|
|
@@ -264,6 +265,10 @@ class FeatureCueContextResult:
|
|
| 264 |
active_condition_count: int
|
| 265 |
condition_count: int
|
| 266 |
cue_active_context_counts: dict[str, int]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
|
| 268 |
|
| 269 |
@dataclass
|
|
@@ -1803,9 +1808,10 @@ class FeatureLensRuntime:
|
|
| 1803 |
|
| 1804 |
``balanced_selectivity`` downweights globally high-activation features by combining target selectivity,
|
| 1805 |
target coverage, and target activation magnitude. ``raw_mean_difference`` preserves the simpler raw
|
| 1806 |
-
mean-difference ranking for comparison.
|
| 1807 |
-
|
| 1808 |
-
current-prompt
|
|
|
|
| 1809 |
"""
|
| 1810 |
self.ensure_ready(preload_saes=False)
|
| 1811 |
assert self.model is not None and self.tokenizer is not None and self.sae_store is not None
|
|
@@ -1818,8 +1824,10 @@ class FeatureLensRuntime:
|
|
| 1818 |
top_n = int(top_n)
|
| 1819 |
if top_n < 1 or top_n > 25:
|
| 1820 |
raise ValueError('Number of candidate features must be between 1 and 25.')
|
| 1821 |
-
if ranking_mode not in {'balanced_selectivity', 'raw_mean_difference'}:
|
| 1822 |
-
raise ValueError(
|
|
|
|
|
|
|
| 1823 |
|
| 1824 |
rows = self._contrast_prompt_rows(n)
|
| 1825 |
available = sorted({str(row['concept']) for row in rows})
|
|
@@ -1905,6 +1913,19 @@ class FeatureLensRuntime:
|
|
| 1905 |
)
|
| 1906 |
|
| 1907 |
eligible = (target_mean > 0) & (mean_diff > 0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1908 |
candidate_idx = torch.nonzero(eligible, as_tuple=False).flatten()
|
| 1909 |
if candidate_idx.numel() == 0:
|
| 1910 |
return ConceptFeatureDiscoveryResult(
|
|
@@ -1919,9 +1940,9 @@ class FeatureLensRuntime:
|
|
| 1919 |
default_candidate_id=None,
|
| 1920 |
current_context_available=current_context_available,
|
| 1921 |
current_token_index=resolved_current_idx,
|
|
|
|
| 1922 |
)
|
| 1923 |
|
| 1924 |
-
ranking_values = balanced_score if ranking_mode == 'balanced_selectivity' else mean_diff
|
| 1925 |
order = torch.argsort(ranking_values[candidate_idx], descending=True)
|
| 1926 |
candidate_idx = candidate_idx[order[:top_n]]
|
| 1927 |
|
|
@@ -1967,6 +1988,7 @@ class FeatureLensRuntime:
|
|
| 1967 |
default_candidate_id=default_candidate_id,
|
| 1968 |
current_context_available=current_context_available,
|
| 1969 |
current_token_index=resolved_current_idx,
|
|
|
|
| 1970 |
)
|
| 1971 |
|
| 1972 |
@staticmethod
|
|
@@ -2118,6 +2140,7 @@ class FeatureLensRuntime:
|
|
| 2118 |
chart_rows: list[list[object]] = []
|
| 2119 |
active_count = 0
|
| 2120 |
cue_active_context_counts = {cue: 0 for cue in cue_list}
|
|
|
|
| 2121 |
for row_idx, (stem, cue, full_prompt) in enumerate(conditions):
|
| 2122 |
valid_positions = torch.nonzero(attention[row_idx], as_tuple=False).flatten()
|
| 2123 |
final_pos = int(valid_positions[-1].item())
|
|
@@ -2128,12 +2151,25 @@ class FeatureLensRuntime:
|
|
| 2128 |
active = activation > 0
|
| 2129 |
active_count += int(active)
|
| 2130 |
cue_active_context_counts[cue] += int(active)
|
|
|
|
| 2131 |
token_id = int(batch['input_ids'][row_idx, final_pos].item())
|
| 2132 |
final_token = self.tokenizer.decode([token_id])
|
| 2133 |
short_stem = stem if len(stem) <= 42 else stem[:39] + '…'
|
| 2134 |
rows.append([stem, cue, full_prompt, repr(final_token), activation, active])
|
| 2135 |
chart_rows.append([short_stem, cue, activation])
|
| 2136 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2137 |
return FeatureCueContextResult(
|
| 2138 |
feature_id=int(feature_id),
|
| 2139 |
layer=int(layer),
|
|
@@ -2144,6 +2180,10 @@ class FeatureLensRuntime:
|
|
| 2144 |
active_condition_count=active_count,
|
| 2145 |
condition_count=len(conditions),
|
| 2146 |
cue_active_context_counts=cue_active_context_counts,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2147 |
)
|
| 2148 |
|
| 2149 |
|
|
|
|
| 240 |
default_candidate_id: int | None
|
| 241 |
current_context_available: bool
|
| 242 |
current_token_index: int | None
|
| 243 |
+
displayed_current_active_count: int
|
| 244 |
|
| 245 |
|
| 246 |
@dataclass
|
|
|
|
| 265 |
active_condition_count: int
|
| 266 |
condition_count: int
|
| 267 |
cue_active_context_counts: dict[str, int]
|
| 268 |
+
cue_mean_activations: dict[str, float]
|
| 269 |
+
dominant_cue: str | None
|
| 270 |
+
dominant_cue_context_count: int
|
| 271 |
+
off_dominant_active_count: int
|
| 272 |
|
| 273 |
|
| 274 |
@dataclass
|
|
|
|
| 1808 |
|
| 1809 |
``balanced_selectivity`` downweights globally high-activation features by combining target selectivity,
|
| 1810 |
target coverage, and target activation magnitude. ``raw_mean_difference`` preserves the simpler raw
|
| 1811 |
+
mean-difference ranking for comparison. ``causal_ready`` further requires activation at the currently
|
| 1812 |
+
selected Workbench token and ranks those compatible candidates by balanced evidence plus a log-scaled
|
| 1813 |
+
current-token activation term. If a Workbench prompt is supplied it is appended to the same model batch,
|
| 1814 |
+
so current-prompt compatibility does not require another forward pass.
|
| 1815 |
"""
|
| 1816 |
self.ensure_ready(preload_saes=False)
|
| 1817 |
assert self.model is not None and self.tokenizer is not None and self.sae_store is not None
|
|
|
|
| 1824 |
top_n = int(top_n)
|
| 1825 |
if top_n < 1 or top_n > 25:
|
| 1826 |
raise ValueError('Number of candidate features must be between 1 and 25.')
|
| 1827 |
+
if ranking_mode not in {'balanced_selectivity', 'raw_mean_difference', 'causal_ready'}:
|
| 1828 |
+
raise ValueError(
|
| 1829 |
+
"ranking_mode must be 'balanced_selectivity', 'raw_mean_difference', or 'causal_ready'."
|
| 1830 |
+
)
|
| 1831 |
|
| 1832 |
rows = self._contrast_prompt_rows(n)
|
| 1833 |
available = sorted({str(row['concept']) for row in rows})
|
|
|
|
| 1913 |
)
|
| 1914 |
|
| 1915 |
eligible = (target_mean > 0) & (mean_diff > 0)
|
| 1916 |
+
if ranking_mode == 'causal_ready':
|
| 1917 |
+
if not current_context_available or resolved_current_idx is None:
|
| 1918 |
+
raise ValueError(
|
| 1919 |
+
"Causal-ready ranking requires a current Workbench prompt/token. "
|
| 1920 |
+
"Set the Workbench prompt/layer/token first."
|
| 1921 |
+
)
|
| 1922 |
+
eligible = eligible & (current_token_dense > 0)
|
| 1923 |
+
ranking_values = balanced_score * torch.log1p(current_token_dense.clamp_min(0))
|
| 1924 |
+
elif ranking_mode == 'balanced_selectivity':
|
| 1925 |
+
ranking_values = balanced_score
|
| 1926 |
+
else:
|
| 1927 |
+
ranking_values = mean_diff
|
| 1928 |
+
|
| 1929 |
candidate_idx = torch.nonzero(eligible, as_tuple=False).flatten()
|
| 1930 |
if candidate_idx.numel() == 0:
|
| 1931 |
return ConceptFeatureDiscoveryResult(
|
|
|
|
| 1940 |
default_candidate_id=None,
|
| 1941 |
current_context_available=current_context_available,
|
| 1942 |
current_token_index=resolved_current_idx,
|
| 1943 |
+
displayed_current_active_count=0,
|
| 1944 |
)
|
| 1945 |
|
|
|
|
| 1946 |
order = torch.argsort(ranking_values[candidate_idx], descending=True)
|
| 1947 |
candidate_idx = candidate_idx[order[:top_n]]
|
| 1948 |
|
|
|
|
| 1988 |
default_candidate_id=default_candidate_id,
|
| 1989 |
current_context_available=current_context_available,
|
| 1990 |
current_token_index=resolved_current_idx,
|
| 1991 |
+
displayed_current_active_count=sum(bool(row[-1]) for row in table_rows),
|
| 1992 |
)
|
| 1993 |
|
| 1994 |
@staticmethod
|
|
|
|
| 2140 |
chart_rows: list[list[object]] = []
|
| 2141 |
active_count = 0
|
| 2142 |
cue_active_context_counts = {cue: 0 for cue in cue_list}
|
| 2143 |
+
cue_activation_values = {cue: [] for cue in cue_list}
|
| 2144 |
for row_idx, (stem, cue, full_prompt) in enumerate(conditions):
|
| 2145 |
valid_positions = torch.nonzero(attention[row_idx], as_tuple=False).flatten()
|
| 2146 |
final_pos = int(valid_positions[-1].item())
|
|
|
|
| 2151 |
active = activation > 0
|
| 2152 |
active_count += int(active)
|
| 2153 |
cue_active_context_counts[cue] += int(active)
|
| 2154 |
+
cue_activation_values[cue].append(activation)
|
| 2155 |
token_id = int(batch['input_ids'][row_idx, final_pos].item())
|
| 2156 |
final_token = self.tokenizer.decode([token_id])
|
| 2157 |
short_stem = stem if len(stem) <= 42 else stem[:39] + '…'
|
| 2158 |
rows.append([stem, cue, full_prompt, repr(final_token), activation, active])
|
| 2159 |
chart_rows.append([short_stem, cue, activation])
|
| 2160 |
|
| 2161 |
+
cue_mean_activations = {
|
| 2162 |
+
cue: float(sum(vals) / len(vals)) if vals else 0.0
|
| 2163 |
+
for cue, vals in cue_activation_values.items()
|
| 2164 |
+
}
|
| 2165 |
+
dominant_cue = max(
|
| 2166 |
+
cue_list,
|
| 2167 |
+
key=lambda cue: (cue_active_context_counts[cue], cue_mean_activations[cue]),
|
| 2168 |
+
) if cue_list else None
|
| 2169 |
+
dominant_count = cue_active_context_counts.get(dominant_cue, 0) if dominant_cue else 0
|
| 2170 |
+
off_dominant_active_count = sum(
|
| 2171 |
+
count for cue, count in cue_active_context_counts.items() if cue != dominant_cue
|
| 2172 |
+
)
|
| 2173 |
return FeatureCueContextResult(
|
| 2174 |
feature_id=int(feature_id),
|
| 2175 |
layer=int(layer),
|
|
|
|
| 2180 |
active_condition_count=active_count,
|
| 2181 |
condition_count=len(conditions),
|
| 2182 |
cue_active_context_counts=cue_active_context_counts,
|
| 2183 |
+
cue_mean_activations=cue_mean_activations,
|
| 2184 |
+
dominant_cue=dominant_cue,
|
| 2185 |
+
dominant_cue_context_count=dominant_count,
|
| 2186 |
+
off_dominant_active_count=off_dominant_active_count,
|
| 2187 |
)
|
| 2188 |
|
| 2189 |
|
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.8.0"
|
| 4 |
description = "Causal sparse-feature interpretability workbench for Qwen3 and Qwen-Scope SAEs"
|
| 5 |
requires-python = ">=3.10"
|
| 6 |
|
research_config.json
CHANGED
|
@@ -90,7 +90,7 @@
|
|
| 90 |
"contrastive_continuation_preference_test",
|
| 91 |
"feature_decoder_geometry"
|
| 92 |
],
|
| 93 |
-
"concept_candidate_discovery_metric": "balanced exploratory score = selectivity
|
| 94 |
"completion_cue_scan": "final-token feature activation after controlled suffix/cue substitution",
|
| 95 |
"live_features_v0_6": [
|
| 96 |
"start_here_plain_language_onboarding",
|
|
@@ -109,5 +109,13 @@
|
|
| 109 |
"balanced_candidate_ranking_and_current_prompt_compatibility",
|
| 110 |
"click_to_select_candidate_rows",
|
| 111 |
"completion_cue_context_matrix"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
]
|
| 113 |
}
|
|
|
|
| 90 |
"contrastive_continuation_preference_test",
|
| 91 |
"feature_decoder_geometry"
|
| 92 |
],
|
| 93 |
+
"concept_candidate_discovery_metric": "balanced exploratory score = selectivity \u00d7 target activation rate \u00d7 log1p(target mean); causal-ready mode additionally requires current-token activity and log-scales that activation; raw mean-difference remains available as a scale-sensitive comparison",
|
| 94 |
"completion_cue_scan": "final-token feature activation after controlled suffix/cue substitution",
|
| 95 |
"live_features_v0_6": [
|
| 96 |
"start_here_plain_language_onboarding",
|
|
|
|
| 109 |
"balanced_candidate_ranking_and_current_prompt_compatibility",
|
| 110 |
"click_to_select_candidate_rows",
|
| 111 |
"completion_cue_context_matrix"
|
| 112 |
+
],
|
| 113 |
+
"live_features_v0_8": [
|
| 114 |
+
"bounded_plot_focus_overlay_with_scroll_restore",
|
| 115 |
+
"explicit_result_table_headings",
|
| 116 |
+
"standalone_dose_response_target_and_feature_inputs",
|
| 117 |
+
"causal_ready_current_token_candidate_ranking",
|
| 118 |
+
"cue_dominance_specificity_interpretation",
|
| 119 |
+
"muted_cue_context_plot_palette"
|
| 120 |
]
|
| 121 |
}
|
scripts/release_check.py
CHANGED
|
@@ -150,6 +150,21 @@ def check_config(config: dict) -> None:
|
|
| 150 |
'research_config.json live_features_v0_7 mismatch: '
|
| 151 |
f'{sorted(actual_live_v07)}'
|
| 152 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
if 'german_language' not in config.get('concepts', []) or 'french_language' in config.get('concepts', []):
|
| 154 |
raise SystemExit('research_config.json must use german_language and must not contain french_language.')
|
| 155 |
|
|
@@ -222,16 +237,20 @@ def check_readme() -> None:
|
|
| 222 |
'balanced selectivity',
|
| 223 |
'german',
|
| 224 |
'start here',
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
]
|
| 226 |
missing = [value for value in required_strings if value.lower() not in readme.lower()]
|
| 227 |
if missing:
|
| 228 |
-
raise SystemExit(f'README.md is missing required v0.
|
| 229 |
|
| 230 |
|
| 231 |
def check_pyproject() -> None:
|
| 232 |
text = (ROOT / 'pyproject.toml').read_text(encoding='utf-8')
|
| 233 |
-
if 'version = "0.
|
| 234 |
-
raise SystemExit('pyproject.toml must declare version 0.
|
| 235 |
|
| 236 |
|
| 237 |
def main() -> None:
|
|
@@ -249,7 +268,7 @@ def main() -> None:
|
|
| 249 |
print(f' layers: {config["layers"]}')
|
| 250 |
print(f' feature-set sizes: {config["feature_set_sizes"]}')
|
| 251 |
print(f' random controls: {config["live_random_controls"]}')
|
| 252 |
-
print(' release: v0.
|
| 253 |
|
| 254 |
|
| 255 |
if __name__ == '__main__':
|
|
|
|
| 150 |
'research_config.json live_features_v0_7 mismatch: '
|
| 151 |
f'{sorted(actual_live_v07)}'
|
| 152 |
)
|
| 153 |
+
|
| 154 |
+
required_live_v08 = {
|
| 155 |
+
'bounded_plot_focus_overlay_with_scroll_restore',
|
| 156 |
+
'explicit_result_table_headings',
|
| 157 |
+
'standalone_dose_response_target_and_feature_inputs',
|
| 158 |
+
'causal_ready_current_token_candidate_ranking',
|
| 159 |
+
'cue_dominance_specificity_interpretation',
|
| 160 |
+
'muted_cue_context_plot_palette',
|
| 161 |
+
}
|
| 162 |
+
actual_live_v08 = set(config.get('live_features_v0_8', []))
|
| 163 |
+
if actual_live_v08 != required_live_v08:
|
| 164 |
+
raise SystemExit(
|
| 165 |
+
'research_config.json live_features_v0_8 mismatch: ' f'{sorted(actual_live_v08)}'
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
if 'german_language' not in config.get('concepts', []) or 'french_language' in config.get('concepts', []):
|
| 169 |
raise SystemExit('research_config.json must use german_language and must not contain french_language.')
|
| 170 |
|
|
|
|
| 237 |
'balanced selectivity',
|
| 238 |
'german',
|
| 239 |
'start here',
|
| 240 |
+
'causal-ready',
|
| 241 |
+
'standalone',
|
| 242 |
+
'plot focus',
|
| 243 |
+
'cue-dominant',
|
| 244 |
]
|
| 245 |
missing = [value for value in required_strings if value.lower() not in readme.lower()]
|
| 246 |
if missing:
|
| 247 |
+
raise SystemExit(f'README.md is missing required v0.8 content: {missing}')
|
| 248 |
|
| 249 |
|
| 250 |
def check_pyproject() -> None:
|
| 251 |
text = (ROOT / 'pyproject.toml').read_text(encoding='utf-8')
|
| 252 |
+
if 'version = "0.8.0"' not in text:
|
| 253 |
+
raise SystemExit('pyproject.toml must declare version 0.8.0.')
|
| 254 |
|
| 255 |
|
| 256 |
def main() -> None:
|
|
|
|
| 268 |
print(f' layers: {config["layers"]}')
|
| 269 |
print(f' feature-set sizes: {config["feature_set_sizes"]}')
|
| 270 |
print(f' random controls: {config["live_random_controls"]}')
|
| 271 |
+
print(' release: v0.8.0')
|
| 272 |
|
| 273 |
|
| 274 |
if __name__ == '__main__':
|
tests/test_live_runtime_helpers.py
CHANGED
|
@@ -256,6 +256,41 @@ def test_concept_feature_discovery_supports_raw_mean_difference() -> None:
|
|
| 256 |
assert all(len(row) == 12 for row in result.rows)
|
| 257 |
|
| 258 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
def test_feature_cue_scan_runs_on_toy_runtime() -> None:
|
| 260 |
runtime = make_runtime()
|
| 261 |
result = runtime.feature_cue_scan(
|
|
@@ -283,4 +318,8 @@ def test_feature_cue_context_scan_runs_on_toy_runtime() -> None:
|
|
| 283 |
assert all(len(row) == 6 for row in result.rows)
|
| 284 |
assert 0 <= result.active_condition_count <= result.condition_count
|
| 285 |
assert set(result.cue_active_context_counts) == {'is', '=', ':'}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
assert len(result.chart_rows) == 6
|
|
|
|
| 256 |
assert all(len(row) == 12 for row in result.rows)
|
| 257 |
|
| 258 |
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def test_concept_feature_discovery_supports_causal_ready_mode() -> None:
|
| 262 |
+
runtime = make_runtime()
|
| 263 |
+
result = runtime.concept_feature_discovery(
|
| 264 |
+
concept='mathematics',
|
| 265 |
+
layer=0,
|
| 266 |
+
prompts_per_concept=1,
|
| 267 |
+
top_n=5,
|
| 268 |
+
ranking_mode='causal_ready',
|
| 269 |
+
current_text='abc',
|
| 270 |
+
current_token_index=-1,
|
| 271 |
+
)
|
| 272 |
+
assert result.ranking_mode == 'causal_ready'
|
| 273 |
+
assert result.current_context_available is True
|
| 274 |
+
assert result.displayed_current_active_count == len(result.rows)
|
| 275 |
+
assert all(bool(row[-1]) for row in result.rows)
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def test_concept_feature_discovery_causal_ready_requires_workbench_context() -> None:
|
| 279 |
+
runtime = make_runtime()
|
| 280 |
+
try:
|
| 281 |
+
runtime.concept_feature_discovery(
|
| 282 |
+
concept='mathematics',
|
| 283 |
+
layer=0,
|
| 284 |
+
prompts_per_concept=1,
|
| 285 |
+
top_n=3,
|
| 286 |
+
ranking_mode='causal_ready',
|
| 287 |
+
)
|
| 288 |
+
except ValueError as exc:
|
| 289 |
+
assert 'Workbench' in str(exc)
|
| 290 |
+
else:
|
| 291 |
+
raise AssertionError('causal_ready should require Workbench context')
|
| 292 |
+
|
| 293 |
+
|
| 294 |
def test_feature_cue_scan_runs_on_toy_runtime() -> None:
|
| 295 |
runtime = make_runtime()
|
| 296 |
result = runtime.feature_cue_scan(
|
|
|
|
| 318 |
assert all(len(row) == 6 for row in result.rows)
|
| 319 |
assert 0 <= result.active_condition_count <= result.condition_count
|
| 320 |
assert set(result.cue_active_context_counts) == {'is', '=', ':'}
|
| 321 |
+
assert set(result.cue_mean_activations) == {'is', '=', ':'}
|
| 322 |
+
assert result.dominant_cue in {'is', '=', ':'}
|
| 323 |
+
assert 0 <= result.dominant_cue_context_count <= 2
|
| 324 |
+
assert 0 <= result.off_dominant_active_count <= result.active_condition_count
|
| 325 |
assert len(result.chart_rows) == 6
|
tests/test_ui_helpers.py
CHANGED
|
@@ -68,5 +68,40 @@ def test_frontend_helpers_name_exports_and_preserve_focus_position() -> None:
|
|
| 68 |
app = _import_app()
|
| 69 |
assert 'featurelens_${stem' in app.INSTALL_REFLOW_JS
|
| 70 |
assert 'chart.png' in app.INSTALL_REFLOW_JS
|
| 71 |
-
assert '
|
|
|
|
|
|
|
| 72 |
assert 'window.scrollTo' in app.INSTALL_REFLOW_JS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
app = _import_app()
|
| 69 |
assert 'featurelens_${stem' in app.INSTALL_REFLOW_JS
|
| 70 |
assert 'chart.png' in app.INSTALL_REFLOW_JS
|
| 71 |
+
assert 'featurelens-plot-focus-overlay' in app.INSTALL_REFLOW_JS
|
| 72 |
+
assert '__featurelens_plot_scroll_y' in app.INSTALL_REFLOW_JS
|
| 73 |
+
assert 'stopImmediatePropagation' in app.INSTALL_REFLOW_JS
|
| 74 |
assert 'window.scrollTo' in app.INSTALL_REFLOW_JS
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def test_dose_response_has_independent_target_control() -> None:
|
| 78 |
+
app = _import_app()
|
| 79 |
+
assert app.dose_target_text.value == '2x'
|
| 80 |
+
# The single-feature target remains optional and independent.
|
| 81 |
+
assert app.target_text.value in {'', None}
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def test_cue_context_markdown_reports_strong_cue_dominance() -> None:
|
| 85 |
+
app = _import_app()
|
| 86 |
+
result = SimpleNamespace(
|
| 87 |
+
feature_id=22632,
|
| 88 |
+
layer=14,
|
| 89 |
+
stems=['math', 'capital', 'weather', 'name'],
|
| 90 |
+
active_condition_count=4,
|
| 91 |
+
condition_count=20,
|
| 92 |
+
cue_active_context_counts={'is': 4, '=': 0, ':': 0, 'equals': 0, 'therefore': 0},
|
| 93 |
+
dominant_cue='is',
|
| 94 |
+
dominant_cue_context_count=4,
|
| 95 |
+
off_dominant_active_count=0,
|
| 96 |
+
)
|
| 97 |
+
text = app._cue_context_metrics_markdown(result)
|
| 98 |
+
assert 'Cue-dominant pattern' in text
|
| 99 |
+
assert '`is` activates in every tested context' in text
|
| 100 |
+
assert 'lexical/cue-specific' in text
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def test_result_tables_hide_native_labels_in_favor_of_explicit_headings() -> None:
|
| 104 |
+
app = _import_app()
|
| 105 |
+
assert app.discovery_table.show_label is False
|
| 106 |
+
assert app.dose_table.show_label is False
|
| 107 |
+
assert '.table-heading' in app.CSS
|