Spaces:
Running on Zero
Running on Zero
Commit ·
ff63ba1
1
Parent(s): 42650ec
Release FeatureLens v0.10.0
Browse files- CHANGELOG.md +9 -0
- README.md +11 -11
- app.py +181 -2
- docs/METHODOLOGY.md +23 -0
- docs/VALIDATION.md +71 -71
- pyproject.toml +1 -1
- research_config.json +14 -1
- scripts/release_check.py +8 -4
- tests/test_ui_helpers.py +51 -0
CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
| 1 |
# Changelog
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
## v0.9.0
|
| 4 |
|
| 5 |
### In-place focus and layout polish
|
|
|
|
| 1 |
# Changelog
|
| 2 |
|
| 3 |
+
## v0.10.0
|
| 4 |
+
|
| 5 |
+
- Added a zero-extra-GPU **Discovery–causality alignment** panel after candidate triage.
|
| 6 |
+
- Joined discovery rank/score with target-effect rank and next-token-distribution-shift rank for the same screened candidates.
|
| 7 |
+
- Added descriptive Spearman ρ summaries for candidate evidence vs absolute target effect and vs next-token JS.
|
| 8 |
+
- Added an association-evidence-vs-target-effect scatter plot and rank-shift table.
|
| 9 |
+
- Kept the v0.9 in-place focus implementation unchanged after HF validation.
|
| 10 |
+
- HF validation remains GPU-budget-aware: only the discovery and candidate-triage paths need to be exercised.
|
| 11 |
+
|
| 12 |
## v0.9.0
|
| 13 |
|
| 14 |
### In-place focus and layout polish
|
README.md
CHANGED
|
@@ -13,7 +13,7 @@ license: mit
|
|
| 13 |
|
| 14 |
# FeatureLens — Causal Interpretability Workbench
|
| 15 |
|
| 16 |
-
> **v0.
|
| 17 |
|
| 18 |
**Research question:**
|
| 19 |
|
|
@@ -37,6 +37,7 @@ FeatureLens uses **Qwen3-1.7B-Base** with **Qwen-Scope residual-stream sparse au
|
|
| 37 |
11. **Specificity** — are targeted effects larger than norm-matched random residual perturbations?
|
| 38 |
12. **Contrastive specificity** — does an edit change preference between a desired and comparison continuation?
|
| 39 |
13. **Geometry** — are selected SAE decoder directions aligned, orthogonal, or cancelling before downstream model non-linearity?
|
|
|
|
| 40 |
|
| 41 |
A high AUROC, high paraphrase overlap, or a large activation is still correlational evidence. Causal evidence comes from downstream change under controlled intervention.
|
| 42 |
|
|
@@ -348,15 +349,14 @@ FeatureLens/
|
|
| 348 |
└── research_config.json
|
| 349 |
```
|
| 350 |
|
| 351 |
-
## v0.
|
| 352 |
|
| 353 |
-
- **
|
| 354 |
-
- **
|
| 355 |
-
- **
|
| 356 |
-
- **
|
| 357 |
-
- **
|
| 358 |
-
-
|
| 359 |
-
- HF validation is **GPU-budget-aware**: unchanged inference paths are not rerun on every release.
|
| 360 |
|
| 361 |
## Validation
|
| 362 |
|
|
@@ -368,7 +368,7 @@ python scripts/ui_smoke.py
|
|
| 368 |
python scripts/release_check.py
|
| 369 |
```
|
| 370 |
|
| 371 |
-
See [`docs/VALIDATION.md`](docs/VALIDATION.md). The v0.
|
| 372 |
|
| 373 |
## Limitations
|
| 374 |
|
|
@@ -384,7 +384,7 @@ See [`docs/VALIDATION.md`](docs/VALIDATION.md). The v0.9 validation guide uses t
|
|
| 384 |
## Resume-ready description
|
| 385 |
|
| 386 |
> **FeatureLens — Causal Interpretability Workbench** | PyTorch, Qwen3, Sparse Autoencoders, Mechanistic Interpretability, Gradio
|
| 387 |
-
> 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.
|
| 388 |
|
| 389 |
## Acknowledgements
|
| 390 |
|
|
|
|
| 13 |
|
| 14 |
# FeatureLens — Causal Interpretability Workbench
|
| 15 |
|
| 16 |
+
> **v0.10:** an association-to-causality synthesis workflow that joins concept discovery and batched causal triage, exposes rank disagreement, and quantifies descriptive concordance without spending another GPU call.
|
| 17 |
|
| 18 |
**Research question:**
|
| 19 |
|
|
|
|
| 37 |
11. **Specificity** — are targeted effects larger than norm-matched random residual perturbations?
|
| 38 |
12. **Contrastive specificity** — does an edit change preference between a desired and comparison continuation?
|
| 39 |
13. **Geometry** — are selected SAE decoder directions aligned, orthogonal, or cancelling before downstream model non-linearity?
|
| 40 |
+
14. **Association–causality concordance** — do features ranked highly by concept evidence also rank highly by downstream target effect or distribution shift?
|
| 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 |
|
|
|
|
| 349 |
└── research_config.json
|
| 350 |
```
|
| 351 |
|
| 352 |
+
## v0.10 association-to-causality synthesis
|
| 353 |
|
| 354 |
+
- **Discovery–causality alignment** joins the concept-discovery table with the batched ablation screen for the same shortlist.
|
| 355 |
+
- **Three ranks are kept separate:** discovery evidence, target-continuation effect, and whole next-token distribution shift.
|
| 356 |
+
- **Descriptive Spearman concordance** reports candidate-score correlation with absolute target effect and with next-token JS; small live screens are never presented as significance tests.
|
| 357 |
+
- **Rank-shift diagnostics** make cases such as a discovery-rank #1 feature becoming target-effect rank #5 immediately visible.
|
| 358 |
+
- **Association-vs-target-effect scatter** is generated from the same two existing GPU calls; the synthesis itself costs no additional GPU time.
|
| 359 |
+
- Existing v0.9 causal-ready discovery, candidate triage, in-place focus, and GPU-budget-aware validation remain unchanged.
|
|
|
|
| 360 |
|
| 361 |
## Validation
|
| 362 |
|
|
|
|
| 368 |
python scripts/release_check.py
|
| 369 |
```
|
| 370 |
|
| 371 |
+
See [`docs/VALIDATION.md`](docs/VALIDATION.md). The v0.10 validation guide uses the **exact labels shown in the UI**. It reuses the existing discovery + triage workflow and does not ask for paraphrase, trajectory, feature-set, dose-response, or focus regressions because those implementations were not changed.
|
| 372 |
|
| 373 |
## Limitations
|
| 374 |
|
|
|
|
| 384 |
## Resume-ready description
|
| 385 |
|
| 386 |
> **FeatureLens — Causal Interpretability Workbench** | PyTorch, Qwen3, Sparse Autoencoders, Mechanistic Interpretability, Gradio
|
| 387 |
+
> 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, discovery-to-causality rank analysis, and norm-matched random-control ensembles.
|
| 388 |
|
| 389 |
## Acknowledgements
|
| 390 |
|
app.py
CHANGED
|
@@ -632,6 +632,123 @@ def _candidate_screen_metrics_markdown(result) -> str:
|
|
| 632 |
)
|
| 633 |
|
| 634 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 635 |
def _cue_context_metrics_markdown(result) -> str:
|
| 636 |
active = ", ".join(
|
| 637 |
f"{cue}: {count}/{len(result.stems)} contexts"
|
|
@@ -1197,6 +1314,7 @@ def run_candidate_causal_screen(
|
|
| 1197 |
token_index: int,
|
| 1198 |
feature_ids: list[str] | None,
|
| 1199 |
target_text: str,
|
|
|
|
| 1200 |
):
|
| 1201 |
try:
|
| 1202 |
selected = [int(float(value)) for value in (feature_ids or [])]
|
|
@@ -1228,12 +1346,19 @@ def run_candidate_causal_screen(
|
|
| 1228 |
choices=choices,
|
| 1229 |
value=choices[0] if choices else None,
|
| 1230 |
)
|
|
|
|
|
|
|
|
|
|
| 1231 |
return (
|
| 1232 |
_candidate_screen_metrics_markdown(result),
|
| 1233 |
table,
|
| 1234 |
chart,
|
| 1235 |
candidate_update,
|
| 1236 |
_tsv(table),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1237 |
)
|
| 1238 |
except Exception as exc:
|
| 1239 |
_raise_ui_error(exc)
|
|
@@ -1320,7 +1445,7 @@ def set_mode_help(mode: str):
|
|
| 1320 |
with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_width=True) as demo:
|
| 1321 |
gr.HTML(
|
| 1322 |
'<header class="hero">'
|
| 1323 |
-
'<h1>FeatureLens <span style="font-size:.48em;opacity:.58;font-weight:400">v0.
|
| 1324 |
'<div class="subtitle">Causal Interpretability Workbench</div>'
|
| 1325 |
'<div class="question">Discover sparse features, test robustness, and separate correlation from causal influence.</div>'
|
| 1326 |
'</header>'
|
|
@@ -1908,6 +2033,48 @@ with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_w
|
|
| 1908 |
elem_classes=["candidate-help"],
|
| 1909 |
)
|
| 1910 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1911 |
gr.HTML('<div class="section-rule">C. Inspect one feature</div>')
|
| 1912 |
contrast_location = gr.Markdown(
|
| 1913 |
"**Activation trace uses the current Workbench prompt.** The controlled concept scan below uses its own balanced prompt set."
|
|
@@ -2334,13 +2501,24 @@ Association, robustness, geometry, and intervention evidence remain separate cla
|
|
| 2334 |
)
|
| 2335 |
candidate_screen_btn.click(
|
| 2336 |
run_candidate_causal_screen,
|
| 2337 |
-
inputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2338 |
outputs=[
|
| 2339 |
candidate_screen_metrics,
|
| 2340 |
candidate_screen_table,
|
| 2341 |
candidate_screen_plot,
|
| 2342 |
discovery_candidate,
|
| 2343 |
candidate_screen_tsv,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2344 |
],
|
| 2345 |
)
|
| 2346 |
candidate_screen_table.select(
|
|
@@ -2397,6 +2575,7 @@ Association, robustness, geometry, and intervention evidence remain separate cla
|
|
| 2397 |
(contrast_copy, contrast_tsv),
|
| 2398 |
(discovery_copy, discovery_tsv),
|
| 2399 |
(candidate_screen_copy, candidate_screen_tsv),
|
|
|
|
| 2400 |
(cue_copy, cue_tsv),
|
| 2401 |
(cue_context_copy, cue_context_tsv),
|
| 2402 |
(para_copy, para_tsv),
|
|
|
|
| 632 |
)
|
| 633 |
|
| 634 |
|
| 635 |
+
|
| 636 |
+
def _spearman_rank_corr(left: list[float], right: list[float]) -> float | None:
|
| 637 |
+
"""Descriptive Spearman correlation with tie-aware average ranks."""
|
| 638 |
+
if len(left) != len(right) or len(left) < 2:
|
| 639 |
+
return None
|
| 640 |
+
left_s = pd.Series(left, dtype="float64")
|
| 641 |
+
right_s = pd.Series(right, dtype="float64")
|
| 642 |
+
if left_s.nunique(dropna=True) < 2 or right_s.nunique(dropna=True) < 2:
|
| 643 |
+
return None
|
| 644 |
+
value = left_s.rank(method="average").corr(right_s.rank(method="average"))
|
| 645 |
+
return None if pd.isna(value) else float(value)
|
| 646 |
+
|
| 647 |
+
|
| 648 |
+
def _candidate_alignment_outputs(
|
| 649 |
+
discovery_table: pd.DataFrame | None,
|
| 650 |
+
screen_table: pd.DataFrame | None,
|
| 651 |
+
) -> tuple[str, pd.DataFrame, pd.DataFrame]:
|
| 652 |
+
"""Join discovery evidence to causal triage results without another model call."""
|
| 653 |
+
empty_columns = [
|
| 654 |
+
"Feature id",
|
| 655 |
+
"Discovery rank",
|
| 656 |
+
"Target-effect rank",
|
| 657 |
+
"Distribution-shift rank",
|
| 658 |
+
"Candidate score",
|
| 659 |
+
"Selectivity",
|
| 660 |
+
"Current token activation",
|
| 661 |
+
"|Δ mean log p/token|",
|
| 662 |
+
"Next-token JS",
|
| 663 |
+
"Discovery→target rank shift",
|
| 664 |
+
]
|
| 665 |
+
if discovery_table is None or screen_table is None:
|
| 666 |
+
return "", pd.DataFrame(columns=empty_columns), pd.DataFrame()
|
| 667 |
+
discovery = pd.DataFrame(discovery_table).copy()
|
| 668 |
+
screen = pd.DataFrame(screen_table).copy()
|
| 669 |
+
if discovery.empty or screen.empty or "Feature id" not in discovery or "Feature id" not in screen:
|
| 670 |
+
return "", pd.DataFrame(columns=empty_columns), pd.DataFrame()
|
| 671 |
+
|
| 672 |
+
discovery["Feature id"] = pd.to_numeric(discovery["Feature id"], errors="coerce")
|
| 673 |
+
screen["Feature id"] = pd.to_numeric(screen["Feature id"], errors="coerce")
|
| 674 |
+
discovery = discovery.dropna(subset=["Feature id"]).copy()
|
| 675 |
+
screen = screen.dropna(subset=["Feature id"]).copy()
|
| 676 |
+
discovery["Feature id"] = discovery["Feature id"].astype(int)
|
| 677 |
+
screen["Feature id"] = screen["Feature id"].astype(int)
|
| 678 |
+
|
| 679 |
+
needed_discovery = {"Rank", "Candidate score", "Selectivity", "Current token activation"}
|
| 680 |
+
needed_screen = {"Rank", "Δ mean log p/token", "Next-token JS"}
|
| 681 |
+
if not needed_discovery.issubset(discovery.columns) or not needed_screen.issubset(screen.columns):
|
| 682 |
+
return "", pd.DataFrame(columns=empty_columns), pd.DataFrame()
|
| 683 |
+
|
| 684 |
+
discovery_lookup = discovery.set_index("Feature id", drop=False)
|
| 685 |
+
js_ranked = screen.sort_values(["Next-token JS", "Feature id"], ascending=[False, True]).reset_index(drop=True)
|
| 686 |
+
js_ranks = {int(row["Feature id"]): rank for rank, (_, row) in enumerate(js_ranked.iterrows(), start=1)}
|
| 687 |
+
|
| 688 |
+
rows: list[list[object]] = []
|
| 689 |
+
for _, causal_row in screen.iterrows():
|
| 690 |
+
feature_id = int(causal_row["Feature id"])
|
| 691 |
+
if feature_id not in discovery_lookup.index:
|
| 692 |
+
continue
|
| 693 |
+
discovery_row = discovery_lookup.loc[feature_id]
|
| 694 |
+
# set_index can technically return a DataFrame for duplicate ids; use the first row deterministically.
|
| 695 |
+
if isinstance(discovery_row, pd.DataFrame):
|
| 696 |
+
discovery_row = discovery_row.iloc[0]
|
| 697 |
+
discovery_rank = int(float(discovery_row["Rank"]))
|
| 698 |
+
target_rank = int(float(causal_row["Rank"]))
|
| 699 |
+
mean_delta = float(causal_row["Δ mean log p/token"])
|
| 700 |
+
rows.append(
|
| 701 |
+
[
|
| 702 |
+
feature_id,
|
| 703 |
+
discovery_rank,
|
| 704 |
+
target_rank,
|
| 705 |
+
int(js_ranks[feature_id]),
|
| 706 |
+
float(discovery_row["Candidate score"]),
|
| 707 |
+
float(discovery_row["Selectivity"]),
|
| 708 |
+
float(discovery_row["Current token activation"]),
|
| 709 |
+
abs(mean_delta),
|
| 710 |
+
float(causal_row["Next-token JS"]),
|
| 711 |
+
discovery_rank - target_rank,
|
| 712 |
+
]
|
| 713 |
+
)
|
| 714 |
+
|
| 715 |
+
table = pd.DataFrame(rows, columns=empty_columns)
|
| 716 |
+
if table.empty:
|
| 717 |
+
return "", table, pd.DataFrame()
|
| 718 |
+
|
| 719 |
+
rho_target = _spearman_rank_corr(
|
| 720 |
+
table["Candidate score"].astype(float).tolist(),
|
| 721 |
+
table["|Δ mean log p/token|"].astype(float).tolist(),
|
| 722 |
+
)
|
| 723 |
+
rho_js = _spearman_rank_corr(
|
| 724 |
+
table["Candidate score"].astype(float).tolist(),
|
| 725 |
+
table["Next-token JS"].astype(float).tolist(),
|
| 726 |
+
)
|
| 727 |
+
|
| 728 |
+
top_discovery = table.sort_values(["Discovery rank", "Feature id"]).iloc[0]
|
| 729 |
+
top_target = table.sort_values(["Target-effect rank", "Feature id"]).iloc[0]
|
| 730 |
+
top_js = table.sort_values(["Distribution-shift rank", "Feature id"]).iloc[0]
|
| 731 |
+
|
| 732 |
+
def fmt_rho(value: float | None) -> str:
|
| 733 |
+
return "undefined" if value is None else f"{value:+.3f}"
|
| 734 |
+
|
| 735 |
+
summary = (
|
| 736 |
+
f"Compared **{len(table)}** screened candidates using the discovery evidence and causal triage from the same workflow. \n"
|
| 737 |
+
f"Top discovery candidate: **{int(top_discovery['Feature id'])}** · strongest target effect: "
|
| 738 |
+
f"**{int(top_target['Feature id'])}** · strongest next-token distribution shift: **{int(top_js['Feature id'])}**. \n"
|
| 739 |
+
f"Spearman ρ(candidate score, |target effect|): **{fmt_rho(rho_target)}** · "
|
| 740 |
+
f"ρ(candidate score, next-token JS): **{fmt_rho(rho_js)}**. \n\n"
|
| 741 |
+
"This is the project's central association-vs-causality diagnostic: a feature can rank highly by concept evidence yet "
|
| 742 |
+
"rank weakly by downstream target effect, or vice versa. With only a handful of screened features and no random-control "
|
| 743 |
+
"ensemble in triage, these correlations are **descriptive**, not significance claims."
|
| 744 |
+
)
|
| 745 |
+
|
| 746 |
+
chart = table[["Feature id", "Candidate score", "|Δ mean log p/token|", "Discovery rank", "Target-effect rank", "Next-token JS"]].copy()
|
| 747 |
+
chart["Feature id"] = chart["Feature id"].astype(str)
|
| 748 |
+
chart["Series"] = "Screened candidate"
|
| 749 |
+
return summary, table, chart
|
| 750 |
+
|
| 751 |
+
|
| 752 |
def _cue_context_metrics_markdown(result) -> str:
|
| 753 |
active = ", ".join(
|
| 754 |
f"{cue}: {count}/{len(result.stems)} contexts"
|
|
|
|
| 1314 |
token_index: int,
|
| 1315 |
feature_ids: list[str] | None,
|
| 1316 |
target_text: str,
|
| 1317 |
+
discovery_table: pd.DataFrame | None,
|
| 1318 |
):
|
| 1319 |
try:
|
| 1320 |
selected = [int(float(value)) for value in (feature_ids or [])]
|
|
|
|
| 1346 |
choices=choices,
|
| 1347 |
value=choices[0] if choices else None,
|
| 1348 |
)
|
| 1349 |
+
alignment_metrics, alignment_table, alignment_chart = _candidate_alignment_outputs(
|
| 1350 |
+
discovery_table, table
|
| 1351 |
+
)
|
| 1352 |
return (
|
| 1353 |
_candidate_screen_metrics_markdown(result),
|
| 1354 |
table,
|
| 1355 |
chart,
|
| 1356 |
candidate_update,
|
| 1357 |
_tsv(table),
|
| 1358 |
+
alignment_metrics,
|
| 1359 |
+
alignment_table,
|
| 1360 |
+
alignment_chart,
|
| 1361 |
+
_tsv(alignment_table),
|
| 1362 |
)
|
| 1363 |
except Exception as exc:
|
| 1364 |
_raise_ui_error(exc)
|
|
|
|
| 1445 |
with gr.Blocks(title="FeatureLens — Causal Interpretability Workbench", fill_width=True) as demo:
|
| 1446 |
gr.HTML(
|
| 1447 |
'<header class="hero">'
|
| 1448 |
+
'<h1>FeatureLens <span style="font-size:.48em;opacity:.58;font-weight:400">v0.10</span></h1>'
|
| 1449 |
'<div class="subtitle">Causal Interpretability Workbench</div>'
|
| 1450 |
'<div class="question">Discover sparse features, test robustness, and separate correlation from causal influence.</div>'
|
| 1451 |
'</header>'
|
|
|
|
| 2033 |
elem_classes=["candidate-help"],
|
| 2034 |
)
|
| 2035 |
|
| 2036 |
+
gr.Markdown("#### Association vs causal influence")
|
| 2037 |
+
gr.Markdown(
|
| 2038 |
+
"Computed from the discovery table and the ablation screen above — **no additional GPU call**. "
|
| 2039 |
+
"It compares concept-evidence rank with target-effect and distribution-shift ranks for the same shortlist.",
|
| 2040 |
+
elem_classes=["candidate-help"],
|
| 2041 |
+
)
|
| 2042 |
+
candidate_alignment_metrics = gr.Markdown()
|
| 2043 |
+
with gr.Row(equal_height=False):
|
| 2044 |
+
with gr.Column(scale=3):
|
| 2045 |
+
_table_heading('Discovery–causality alignment')
|
| 2046 |
+
candidate_alignment_table = gr.Dataframe(
|
| 2047 |
+
interactive=False,
|
| 2048 |
+
label="Discovery–causality alignment",
|
| 2049 |
+
show_label=False,
|
| 2050 |
+
buttons=["fullscreen"],
|
| 2051 |
+
elem_classes=["result-table"],
|
| 2052 |
+
wrap=False,
|
| 2053 |
+
max_height=360,
|
| 2054 |
+
)
|
| 2055 |
+
candidate_alignment_tsv = gr.Textbox(visible="hidden")
|
| 2056 |
+
candidate_alignment_copy = _copy_button()
|
| 2057 |
+
with gr.Column(scale=2):
|
| 2058 |
+
candidate_alignment_plot = gr.ScatterPlot(
|
| 2059 |
+
x="Candidate score",
|
| 2060 |
+
y="|Δ mean log p/token|",
|
| 2061 |
+
color="Series",
|
| 2062 |
+
color_map={"Screened candidate": INK_TEAL},
|
| 2063 |
+
title="Association evidence vs target effect",
|
| 2064 |
+
elem_id="plot-association-causality",
|
| 2065 |
+
x_title="Discovery candidate score",
|
| 2066 |
+
y_title="|Δ mean log p/token|",
|
| 2067 |
+
tooltip=[
|
| 2068 |
+
"Feature id",
|
| 2069 |
+
"Discovery rank",
|
| 2070 |
+
"Target-effect rank",
|
| 2071 |
+
"Next-token JS",
|
| 2072 |
+
],
|
| 2073 |
+
buttons=["fullscreen", "export"],
|
| 2074 |
+
elem_classes=["fl-plot"],
|
| 2075 |
+
height=330,
|
| 2076 |
+
)
|
| 2077 |
+
|
| 2078 |
gr.HTML('<div class="section-rule">C. Inspect one feature</div>')
|
| 2079 |
contrast_location = gr.Markdown(
|
| 2080 |
"**Activation trace uses the current Workbench prompt.** The controlled concept scan below uses its own balanced prompt set."
|
|
|
|
| 2501 |
)
|
| 2502 |
candidate_screen_btn.click(
|
| 2503 |
run_candidate_causal_screen,
|
| 2504 |
+
inputs=[
|
| 2505 |
+
prompt,
|
| 2506 |
+
discovery_layer,
|
| 2507 |
+
token_index,
|
| 2508 |
+
candidate_screen_ids,
|
| 2509 |
+
candidate_screen_target,
|
| 2510 |
+
discovery_table,
|
| 2511 |
+
],
|
| 2512 |
outputs=[
|
| 2513 |
candidate_screen_metrics,
|
| 2514 |
candidate_screen_table,
|
| 2515 |
candidate_screen_plot,
|
| 2516 |
discovery_candidate,
|
| 2517 |
candidate_screen_tsv,
|
| 2518 |
+
candidate_alignment_metrics,
|
| 2519 |
+
candidate_alignment_table,
|
| 2520 |
+
candidate_alignment_plot,
|
| 2521 |
+
candidate_alignment_tsv,
|
| 2522 |
],
|
| 2523 |
)
|
| 2524 |
candidate_screen_table.select(
|
|
|
|
| 2575 |
(contrast_copy, contrast_tsv),
|
| 2576 |
(discovery_copy, discovery_tsv),
|
| 2577 |
(candidate_screen_copy, candidate_screen_tsv),
|
| 2578 |
+
(candidate_alignment_copy, candidate_alignment_tsv),
|
| 2579 |
(cue_copy, cue_tsv),
|
| 2580 |
(cue_context_copy, cue_context_tsv),
|
| 2581 |
(para_copy, para_tsv),
|
docs/METHODOLOGY.md
CHANGED
|
@@ -439,3 +439,26 @@ for each candidate feature $i$, stacks a zero-edit reference plus all candidate
|
|
| 439 |
Rows are ordered by absolute target mean-log-probability effect. This ordering is deliberately a **native-ablation effect screen**, not a significance or specificity statistic. No random-control ensemble is used at this stage. A promising candidate should be promoted to the existing single-feature causal test, which compares the SAE edit with the eight-direction norm-matched random ensemble.
|
| 440 |
|
| 441 |
This two-stage design reduces live GPU use while preserving the stronger causal standard for any result that is ultimately interpreted.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 439 |
Rows are ordered by absolute target mean-log-probability effect. This ordering is deliberately a **native-ablation effect screen**, not a significance or specificity statistic. No random-control ensemble is used at this stage. A promising candidate should be promoted to the existing single-feature causal test, which compares the SAE edit with the eight-direction norm-matched random ensemble.
|
| 440 |
|
| 441 |
This two-stage design reduces live GPU use while preserving the stronger causal standard for any result that is ultimately interpreted.
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
## v0.10 discovery-to-causality concordance
|
| 445 |
+
|
| 446 |
+
The central FeatureLens question is not only whether a concept-associated SAE feature can be intervened on, but whether **association strength predicts causal influence**. v0.10 therefore joins the live concept-discovery evidence with the batched candidate-ablation screen for the same shortlist.
|
| 447 |
+
|
| 448 |
+
For each screened feature, the synthesis keeps three orderings separate:
|
| 449 |
+
|
| 450 |
+
1. **Discovery rank** from the chosen exploratory concept-evidence score.
|
| 451 |
+
2. **Target-effect rank** from $|\Delta \bar{\ell}_{\mathrm{target}}|$, the absolute change in teacher-forced mean target log-probability per token under native ablation.
|
| 452 |
+
3. **Distribution-shift rank** from next-token Jensen–Shannon divergence.
|
| 453 |
+
|
| 454 |
+
The UI also reports a rank shift
|
| 455 |
+
|
| 456 |
+
$$
|
| 457 |
+
\Delta r_i = r_i^{\mathrm{discovery}} - r_i^{\mathrm{target}},
|
| 458 |
+
$$
|
| 459 |
+
|
| 460 |
+
so positive values indicate a feature that rises in the target-effect ordering relative to discovery, while negative values indicate a feature that looked stronger associatively than causally for the specified continuation.
|
| 461 |
+
|
| 462 |
+
Across the small screened set, FeatureLens computes tie-aware Spearman correlations between candidate score and (a) absolute target effect and (b) next-token JS. These are deliberately labeled **descriptive** because the live shortlist is small and the triage stage does not spend random-control ensembles. A promoted feature still requires the full single-feature causal test before specificity is interpreted.
|
| 463 |
+
|
| 464 |
+
This synthesis costs no additional model forward: it is computed from the discovery and triage tables already produced by the two-stage workflow.
|
docs/VALIDATION.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
-
# FeatureLens v0.
|
| 2 |
|
| 3 |
-
|
|
|
|
|
|
|
| 4 |
|
| 5 |
## 1. Local release gate — no HF GPU
|
| 6 |
|
|
@@ -19,25 +21,25 @@ Expected high-level result:
|
|
| 19 |
- all tests pass;
|
| 20 |
- Ruff reports `All checks passed!`;
|
| 21 |
- `FeatureLens UI launch smoke: PASS`;
|
| 22 |
-
- release check ends with `release: v0.
|
| 23 |
|
| 24 |
-
|
| 25 |
|
| 26 |
---
|
| 27 |
|
| 28 |
-
# HF acceptance —
|
| 29 |
|
| 30 |
-
|
| 31 |
|
| 32 |
- **Workbench → Prompt:** `The derivative of x squared is`
|
| 33 |
- **Workbench → Residual layer:** `14`
|
| 34 |
- **Workbench → Prompt token index:** `-1`
|
| 35 |
|
| 36 |
-
You do **not** need to
|
| 37 |
|
| 38 |
-
## 2. GPU call 1 — causal-ready
|
| 39 |
|
| 40 |
-
|
| 41 |
|
| 42 |
**Feature evidence → A. Concept-guided candidate discovery**
|
| 43 |
|
|
@@ -53,46 +55,21 @@ Click:
|
|
| 53 |
|
| 54 |
**Discover concept-associated candidates**
|
| 55 |
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
1. **Candidate feature evidence** is visually close to the table itself. There should not be a large empty band between the heading and the first table row/header.
|
| 59 |
-
2. **Candidate features to screen** in section B is automatically populated with up to the first five returned candidate IDs.
|
| 60 |
-
3. The discovery summary says all displayed candidates are active at the selected Workbench token for this ranking.
|
| 61 |
-
4. No page jump occurs merely because results render.
|
| 62 |
-
|
| 63 |
-
### In-place plot focus
|
| 64 |
-
|
| 65 |
-
On the **Candidate evidence score** graph, click the existing fullscreen/focus toolbar icon.
|
| 66 |
-
|
| 67 |
-
Pass if:
|
| 68 |
|
| 69 |
-
- the
|
| 70 |
-
-
|
| 71 |
-
-
|
| 72 |
-
- it does not require horizontal page traversal;
|
| 73 |
-
- clicking the same icon again (or pressing `Esc`) returns it to the normal size without changing your surrounding page location.
|
| 74 |
|
| 75 |
-
|
| 76 |
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
Pass if:
|
| 80 |
-
|
| 81 |
-
- the table widens at its current location rather than opening at the top of the Space;
|
| 82 |
-
- the surrounding page position is unchanged after toggling back;
|
| 83 |
-
- horizontal scrolling stays inside the table when needed.
|
| 84 |
-
|
| 85 |
-
Do not rerun discovery merely to test focus. Use this already-populated output for both focus checks.
|
| 86 |
-
|
| 87 |
-
---
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
Stay in:
|
| 92 |
|
| 93 |
**Feature evidence → B. Batched causal candidate triage**
|
| 94 |
|
| 95 |
-
|
| 96 |
|
| 97 |
Set:
|
| 98 |
|
|
@@ -102,53 +79,76 @@ Click:
|
|
| 102 |
|
| 103 |
**Screen candidate ablations**
|
| 104 |
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
|
| 107 |
-
|
| 108 |
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
-
- `Rank`
|
| 112 |
- `Feature id`
|
| 113 |
-
- `
|
| 114 |
-
- `
|
| 115 |
-
- `
|
| 116 |
-
- `
|
| 117 |
-
- `
|
|
|
|
|
|
|
| 118 |
- `Next-token JS`
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
-
|
| 121 |
|
| 122 |
-
-
|
| 123 |
-
-
|
| 124 |
-
-
|
| 125 |
-
-
|
| 126 |
-
-
|
|
|
|
| 127 |
|
| 128 |
-
|
| 129 |
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
-
|
| 133 |
-
2. the triage summary;
|
| 134 |
-
3. whether plot focus passed;
|
| 135 |
-
4. whether table focus passed;
|
| 136 |
-
5. whether heading/table spacing now looks natural.
|
| 137 |
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
---
|
| 141 |
|
| 142 |
-
## 4.
|
|
|
|
|
|
|
| 143 |
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
-
|
| 147 |
-
- The short explanatory line should only define the 0× / 1× / 2× scale semantics.
|
| 148 |
-
- Existing result-table headings should use the same compact spacing pattern throughout the app.
|
| 149 |
|
| 150 |
---
|
| 151 |
|
| 152 |
# Final hardening later
|
| 153 |
|
| 154 |
-
The
|
|
|
|
| 1 |
+
# FeatureLens v0.10 validation
|
| 2 |
|
| 3 |
+
v0.10 does **not** change paraphrase robustness, layer trajectory, feature-set interventions, dose-response inference, cue × context, or the in-place focus implementation. Do not spend ZeroGPU quota rerunning those paths.
|
| 4 |
+
|
| 5 |
+
The only new live behavior is the **association-to-causality synthesis** computed after the existing discovery + candidate-triage workflow.
|
| 6 |
|
| 7 |
## 1. Local release gate — no HF GPU
|
| 8 |
|
|
|
|
| 21 |
- all tests pass;
|
| 22 |
- Ruff reports `All checks passed!`;
|
| 23 |
- `FeatureLens UI launch smoke: PASS`;
|
| 24 |
+
- release check ends with `release: v0.10.0`.
|
| 25 |
|
| 26 |
+
Stop if any local gate fails.
|
| 27 |
|
| 28 |
---
|
| 29 |
|
| 30 |
+
# HF acceptance — two GPU calls total
|
| 31 |
|
| 32 |
+
Use the existing Workbench context:
|
| 33 |
|
| 34 |
- **Workbench → Prompt:** `The derivative of x squared is`
|
| 35 |
- **Workbench → Residual layer:** `14`
|
| 36 |
- **Workbench → Prompt token index:** `-1`
|
| 37 |
|
| 38 |
+
You do **not** need to click **Inspect sparse features** first.
|
| 39 |
|
| 40 |
+
## 2. GPU call 1 — causal-ready mathematics discovery
|
| 41 |
|
| 42 |
+
Exact path:
|
| 43 |
|
| 44 |
**Feature evidence → A. Concept-guided candidate discovery**
|
| 45 |
|
|
|
|
| 55 |
|
| 56 |
**Discover concept-associated candidates**
|
| 57 |
|
| 58 |
+
Expected regression behavior from v0.9:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
+
- the summary should say `12/12` displayed candidates are active at the selected Workbench token;
|
| 61 |
+
- **Candidate features to screen** should auto-populate with the first five returned feature IDs;
|
| 62 |
+
- no additional UI/focus regression testing is required because that code was not changed in v0.10.
|
|
|
|
|
|
|
| 63 |
|
| 64 |
+
For the current canonical prompt, the v0.9 result began with candidates `16369`, `5712`, `26112`, `25992`, `21670`. Exact floating-point values can vary slightly, but a major ordering change should be reported.
|
| 65 |
|
| 66 |
+
## 3. GPU call 2 — triage + association-to-causality synthesis
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
+
Exact path:
|
|
|
|
|
|
|
| 69 |
|
| 70 |
**Feature evidence → B. Batched causal candidate triage**
|
| 71 |
|
| 72 |
+
Leave the five auto-populated candidates selected.
|
| 73 |
|
| 74 |
Set:
|
| 75 |
|
|
|
|
| 79 |
|
| 80 |
**Screen candidate ablations**
|
| 81 |
|
| 82 |
+
The normal **Candidate ablation screen** should appear first.
|
| 83 |
+
|
| 84 |
+
Immediately below it, without another GPU action, v0.10 should populate:
|
| 85 |
+
|
| 86 |
+
### `Association vs causal influence`
|
| 87 |
|
| 88 |
+
with:
|
| 89 |
|
| 90 |
+
1. a descriptive summary;
|
| 91 |
+
2. **Discovery–causality alignment** table;
|
| 92 |
+
3. **Association evidence vs target effect** scatter plot.
|
| 93 |
+
|
| 94 |
+
### Required alignment-table columns
|
| 95 |
|
|
|
|
| 96 |
- `Feature id`
|
| 97 |
+
- `Discovery rank`
|
| 98 |
+
- `Target-effect rank`
|
| 99 |
+
- `Distribution-shift rank`
|
| 100 |
+
- `Candidate score`
|
| 101 |
+
- `Selectivity`
|
| 102 |
+
- `Current token activation`
|
| 103 |
+
- `|Δ mean log p/token|`
|
| 104 |
- `Next-token JS`
|
| 105 |
+
- `Discovery→target rank shift`
|
| 106 |
+
|
| 107 |
+
### Required summary behavior
|
| 108 |
|
| 109 |
+
The summary must identify separately:
|
| 110 |
|
| 111 |
+
- top discovery candidate;
|
| 112 |
+
- strongest target-effect candidate;
|
| 113 |
+
- strongest next-token distribution-shift candidate;
|
| 114 |
+
- Spearman `ρ(candidate score, |target effect|)`;
|
| 115 |
+
- Spearman `ρ(candidate score, next-token JS)`;
|
| 116 |
+
- an explicit warning that the live correlations are descriptive because the screened set is small and triage has no random-control ensemble.
|
| 117 |
|
| 118 |
+
For the exact v0.9 values you reported, the expected qualitative pattern is:
|
| 119 |
|
| 120 |
+
- discovery rank #1: feature `16369`;
|
| 121 |
+
- strongest target effect: feature `25992`;
|
| 122 |
+
- strongest next-token JS shift: feature `16369`;
|
| 123 |
+
- discovery score versus target-effect magnitude: strongly negative descriptive rank correlation;
|
| 124 |
+
- discovery score versus JS: positive but weaker descriptive rank correlation.
|
| 125 |
|
| 126 |
+
Do not require exact decimals as a pass condition.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
|
| 128 |
+
### Rank-shift sanity check
|
| 129 |
+
|
| 130 |
+
For the v0.9 ordering:
|
| 131 |
+
|
| 132 |
+
- feature `25992`: discovery rank `4`, target-effect rank `1` → `Discovery→target rank shift = +3`;
|
| 133 |
+
- feature `16369`: discovery rank `1`, target-effect rank `5` → rank shift `-4`.
|
| 134 |
+
|
| 135 |
+
This is the most important v0.10 regression check because it demonstrates that concept-evidence rank and target-causal rank are not interchangeable.
|
| 136 |
|
| 137 |
---
|
| 138 |
|
| 139 |
+
## 4. What to send back
|
| 140 |
+
|
| 141 |
+
Only send:
|
| 142 |
|
| 143 |
+
1. the v0.10 **Candidate ablation screen** if it changed materially from v0.9;
|
| 144 |
+
2. the new **Discovery–causality alignment** table;
|
| 145 |
+
3. the new association/causality summary with the two Spearman values;
|
| 146 |
+
4. optionally a screenshot of **Association evidence vs target effect** if the plot looks wrong.
|
| 147 |
|
| 148 |
+
Do **not** rerun identity paraphrase, layer trajectory, 1/3/5 sweep, dose response, cue × context, or focus behavior for v0.10. Those implementations were not changed.
|
|
|
|
|
|
|
| 149 |
|
| 150 |
---
|
| 151 |
|
| 152 |
# Final hardening later
|
| 153 |
|
| 154 |
+
The broad adversarial/release suite remains deferred until the live feature set is frozen. After v0.10 acceptance, the next high-value step should be the real offline held-out benchmark rather than another round of unrelated live widgets.
|
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.10.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",
|
|
@@ -126,5 +126,18 @@
|
|
| 126 |
"concise_independent_dose_response_copy",
|
| 127 |
"batched_candidate_causal_triage",
|
| 128 |
"gpu_budget_aware_hf_validation_scope"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
]
|
| 130 |
}
|
|
|
|
| 90 |
"contrastive_continuation_preference_test",
|
| 91 |
"feature_decoder_geometry"
|
| 92 |
],
|
| 93 |
+
"concept_candidate_discovery_metric": "balanced exploratory score = selectivity × target activation rate × 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",
|
|
|
|
| 126 |
"concise_independent_dose_response_copy",
|
| 127 |
"batched_candidate_causal_triage",
|
| 128 |
"gpu_budget_aware_hf_validation_scope"
|
| 129 |
+
],
|
| 130 |
+
"candidate_alignment_metrics": [
|
| 131 |
+
"discovery rank versus target-effect rank",
|
| 132 |
+
"discovery rank versus next-token JS rank",
|
| 133 |
+
"Spearman candidate score versus absolute target effect",
|
| 134 |
+
"Spearman candidate score versus next-token JS"
|
| 135 |
+
],
|
| 136 |
+
"live_features_v0_10": [
|
| 137 |
+
"discovery_to_causality_alignment_table",
|
| 138 |
+
"association_evidence_vs_target_effect_scatter",
|
| 139 |
+
"descriptive_spearman_concordance_summary",
|
| 140 |
+
"target_effect_vs_distribution_shift_rank_separation",
|
| 141 |
+
"no_extra_gpu_candidate_synthesis"
|
| 142 |
]
|
| 143 |
}
|
scripts/release_check.py
CHANGED
|
@@ -256,16 +256,20 @@ def check_readme() -> None:
|
|
| 256 |
'batched causal candidate triage',
|
| 257 |
'in-place',
|
| 258 |
'gpu',
|
|
|
|
|
|
|
|
|
|
|
|
|
| 259 |
]
|
| 260 |
missing = [value for value in required_strings if value.lower() not in readme.lower()]
|
| 261 |
if missing:
|
| 262 |
-
raise SystemExit(f'README.md is missing required v0.
|
| 263 |
|
| 264 |
|
| 265 |
def check_pyproject() -> None:
|
| 266 |
text = (ROOT / 'pyproject.toml').read_text(encoding='utf-8')
|
| 267 |
-
if 'version = "0.
|
| 268 |
-
raise SystemExit('pyproject.toml must declare version 0.
|
| 269 |
|
| 270 |
|
| 271 |
def main() -> None:
|
|
@@ -283,7 +287,7 @@ def main() -> None:
|
|
| 283 |
print(f' layers: {config["layers"]}')
|
| 284 |
print(f' feature-set sizes: {config["feature_set_sizes"]}')
|
| 285 |
print(f' random controls: {config["live_random_controls"]}')
|
| 286 |
-
print(' release: v0.
|
| 287 |
|
| 288 |
|
| 289 |
if __name__ == '__main__':
|
|
|
|
| 256 |
'batched causal candidate triage',
|
| 257 |
'in-place',
|
| 258 |
'gpu',
|
| 259 |
+
'discovery–causality alignment',
|
| 260 |
+
'spearman',
|
| 261 |
+
'rank-shift',
|
| 262 |
+
'no additional gpu',
|
| 263 |
]
|
| 264 |
missing = [value for value in required_strings if value.lower() not in readme.lower()]
|
| 265 |
if missing:
|
| 266 |
+
raise SystemExit(f'README.md is missing required v0.10 content: {missing}')
|
| 267 |
|
| 268 |
|
| 269 |
def check_pyproject() -> None:
|
| 270 |
text = (ROOT / 'pyproject.toml').read_text(encoding='utf-8')
|
| 271 |
+
if 'version = "0.10.0"' not in text:
|
| 272 |
+
raise SystemExit('pyproject.toml must declare version 0.10.0.')
|
| 273 |
|
| 274 |
|
| 275 |
def main() -> None:
|
|
|
|
| 287 |
print(f' layers: {config["layers"]}')
|
| 288 |
print(f' feature-set sizes: {config["feature_set_sizes"]}')
|
| 289 |
print(f' random controls: {config["live_random_controls"]}')
|
| 290 |
+
print(' release: v0.10.0')
|
| 291 |
|
| 292 |
|
| 293 |
if __name__ == '__main__':
|
tests/test_ui_helpers.py
CHANGED
|
@@ -130,3 +130,54 @@ def test_candidate_screen_has_independent_target_and_multiselect() -> None:
|
|
| 130 |
assert app.candidate_screen_target.value == '2x'
|
| 131 |
assert app.candidate_screen_ids.multiselect is True
|
| 132 |
assert app.candidate_screen_ids.max_choices == 8
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
assert app.candidate_screen_target.value == '2x'
|
| 131 |
assert app.candidate_screen_ids.multiselect is True
|
| 132 |
assert app.candidate_screen_ids.max_choices == 8
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def test_candidate_alignment_exposes_discovery_causality_divergence() -> None:
|
| 136 |
+
app = _import_app()
|
| 137 |
+
discovery = pd.DataFrame(
|
| 138 |
+
[
|
| 139 |
+
[1, 16369, 4.7348, 15.0, 0.0, 15.0, 1.0, 0.5, 0.0, 50.0, 29.25, True],
|
| 140 |
+
[2, 5712, 4.1969, 9.1, 1.5, 7.6, 0.72, 1.0, 0.17, 11.4, 11.38, True],
|
| 141 |
+
[3, 26112, 3.9419, 10.8, 0.0, 10.8, 1.0, 0.5, 0.0, 23.4, 23.42, True],
|
| 142 |
+
[4, 25992, 3.7342, 10.1, 0.0, 10.1, 1.0, 0.5, 0.0, 21.4, 21.36, True],
|
| 143 |
+
[5, 21670, 3.6200, 18.9, 2.0, 16.9, 0.81, 0.75, 0.125, 25.3, 6.36, True],
|
| 144 |
+
],
|
| 145 |
+
columns=[
|
| 146 |
+
'Rank', 'Feature id', 'Candidate score', 'Target mean max', 'Other mean max',
|
| 147 |
+
'Mean difference', 'Selectivity', 'Target activation rate', 'Other activation rate',
|
| 148 |
+
'Current prompt max', 'Current token activation', 'Active at current token',
|
| 149 |
+
],
|
| 150 |
+
)
|
| 151 |
+
screen = pd.DataFrame(
|
| 152 |
+
[
|
| 153 |
+
[1, 25992, 21.3, True, 21.3, -0.1119, -0.2238, 0.001066],
|
| 154 |
+
[2, 21670, 6.36, True, 6.36, 0.0313, 0.0626, 0.000106],
|
| 155 |
+
[3, 26112, 23.4, True, 23.4, -0.0211, -0.0423, 0.000417],
|
| 156 |
+
[4, 5712, 11.38, True, 11.38, 0.0110, 0.0221, 0.000155],
|
| 157 |
+
[5, 16369, 29.23, True, 29.23, -0.0101, -0.0201, 0.001714],
|
| 158 |
+
],
|
| 159 |
+
columns=[
|
| 160 |
+
'Rank', 'Feature id', 'Native activation', 'Active at current token', 'Perturbation L2',
|
| 161 |
+
'Δ mean log p/token', 'Δ sequence log p', 'Next-token JS',
|
| 162 |
+
],
|
| 163 |
+
)
|
| 164 |
+
summary, table, chart = app._candidate_alignment_outputs(discovery, screen)
|
| 165 |
+
assert len(table) == 5
|
| 166 |
+
assert int(table.loc[table['Target-effect rank'].idxmin(), 'Feature id']) == 25992
|
| 167 |
+
assert int(table.loc[table['Distribution-shift rank'].idxmin(), 'Feature id']) == 16369
|
| 168 |
+
row_25992 = table.loc[table['Feature id'] == 25992].iloc[0]
|
| 169 |
+
row_16369 = table.loc[table['Feature id'] == 16369].iloc[0]
|
| 170 |
+
assert int(row_25992['Discovery→target rank shift']) == 3
|
| 171 |
+
assert int(row_16369['Discovery→target rank shift']) == -4
|
| 172 |
+
assert 'Spearman' in summary
|
| 173 |
+
assert '-0.900' in summary
|
| 174 |
+
assert '+0.600' in summary
|
| 175 |
+
assert 'descriptive' in summary
|
| 176 |
+
assert set(chart['Feature id']) == {'16369', '5712', '26112', '25992', '21670'}
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def test_candidate_alignment_ui_uses_same_screen_call_and_no_new_gpu_button() -> None:
|
| 180 |
+
app = _import_app()
|
| 181 |
+
assert app.candidate_alignment_table.show_label is False
|
| 182 |
+
assert app.candidate_alignment_plot.visible is True
|
| 183 |
+
assert 'Association evidence vs target effect' == app.candidate_alignment_plot.title
|