ArchitSharma commited on
Commit
ff63ba1
·
1 Parent(s): 42650ec

Release FeatureLens v0.10.0

Browse files
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.9:** a GPU-budget-aware discovery-to-causality workflow with in-place aspect-preserving focus, compact result headings, causal-ready candidate discovery, and a batched multi-candidate ablation screen before the full random-control causal test.
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.9 live workflow improvements
352
 
353
- - **Causal-ready candidate ranking** requires concept contrast and current-token activity before suggesting a feature for immediate intervention.
354
- - **Batched causal candidate triage** screens up to eight discovered candidates in one target-scoring batch before the expensive random-control causal test.
355
- - **Dose response** has independent feature and target inputs with concise scale semantics.
356
- - **Cue × context interpretation** reports cue-dominant tested patterns from the measured activation matrix rather than generic boilerplate.
357
- - **In-place focus** expands plots/tables at their existing page location and preserves plot aspect ratio instead of opening an HF-iframe overlay.
358
- - **Compact table headings** use the Dataframe toolbar band rather than leaving a large blank gap above the table.
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.9 validation guide uses the **exact labels shown in the UI** and deliberately avoids rerunning unchanged GPU-heavy regressions. HF acceptance is limited to code paths changed in this release.
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.9</span></h1>'
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=[prompt, discovery_layer, token_index, candidate_screen_ids, candidate_screen_target],
 
 
 
 
 
 
 
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.9 validation
2
 
3
- This guide intentionally **does not** ask you to rerun unchanged GPU-heavy regressions. Identity paraphrase, layer trajectory, and the 1/3/5 feature-set sweep were unchanged in v0.9 and remain covered by automated tests. Save the HF ZeroGPU quota for the two inference paths changed/added here.
 
 
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.9.0`.
23
 
24
- If any command fails, stop there rather than running HF tests.
25
 
26
  ---
27
 
28
- # HF acceptance — only two GPU calls required
29
 
30
- Keep the default Workbench prompt unless stated otherwise:
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 run **Inspect sparse features** first for these v0.9 checks.
37
 
38
- ## 2. GPU call 1 — causal-ready discovery + layout/focus test
39
 
40
- Go to:
41
 
42
  **Feature evidence → A. Concept-guided candidate discovery**
43
 
@@ -53,46 +55,21 @@ Click:
53
 
54
  **Discover concept-associated candidates**
55
 
56
- ### Pass conditions
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 chart expands **where it already sits on the page**;
70
- - it does not jump to the top of the HF Space;
71
- - it remains aspect-ratio preserving rather than becoming extremely tall/long;
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
- ### In-place table focus
76
 
77
- Click the fullscreen/focus icon on **Candidate feature evidence**.
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
- ## 3. GPU call 2 — batched causal candidate triage
90
-
91
- Stay in:
92
 
93
  **Feature evidence → B. Batched causal candidate triage**
94
 
95
- The discovery run should already have populated **Candidate features to screen**. Leave the default first five candidates selected unless fewer than five were returned.
96
 
97
  Set:
98
 
@@ -102,53 +79,76 @@ Click:
102
 
103
  **Screen candidate ablations**
104
 
105
- ### Expected output
 
 
 
 
106
 
107
- The summary should explicitly call this a **triage screen** and explain that the full random-control ensemble is deferred to **Workbench → II. Single-feature causal test**.
108
 
109
- The **Candidate ablation screen** table must contain these columns:
 
 
 
 
110
 
111
- - `Rank`
112
  - `Feature id`
113
- - `Native activation`
114
- - `Active at current token`
115
- - `Perturbation L2`
116
- - `Δ mean log p/token`
117
- - `Δ sequence log p`
 
 
118
  - `Next-token JS`
 
 
 
119
 
120
- Pass if:
121
 
122
- - all selected causal-ready candidates report `Active at current token = True`;
123
- - rows are ordered from the largest absolute `Δ mean log p/token` to the smallest;
124
- - the 1st-ranked screen result is automatically placed into **Selected candidate feature id**;
125
- - clicking another row updates **Selected candidate feature id** without a GPU call;
126
- - the plot and table focus controls behave in-place as in Test 2.
 
127
 
128
- ### What to send back
129
 
130
- For v0.9 I only need:
 
 
 
 
131
 
132
- 1. the **Candidate ablation screen** table;
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
- No paraphrase, layer-trajectory, feature-set-sweep, or dose-response reruns are required for v0.9 acceptance because those inference implementations were not changed.
 
 
 
 
 
 
 
139
 
140
  ---
141
 
142
- ## 4. Zero-GPU visual checks
 
 
143
 
144
- These do not require running an experiment:
 
 
 
145
 
146
- - **Workbench III. Single-feature scale dose-response** should show its own **Dose-response feature id** and **Dose-response target continuation** fields without the old “Standalone experiment” explanation.
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 comprehensive adversarial suite (invalid feature IDs, long prompts, inactive-feature edge cases, rapid double clicks, mixed-language prompts, etc.) remains deferred until the live feature set is frozen. That final pass should be done once, not on every iteration.
 
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.9.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 \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",
@@ -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.9 content: {missing}')
263
 
264
 
265
  def check_pyproject() -> None:
266
  text = (ROOT / 'pyproject.toml').read_text(encoding='utf-8')
267
- if 'version = "0.9.0"' not in text:
268
- raise SystemExit('pyproject.toml must declare version 0.9.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.9.0')
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