Skip to content

Commit 719df3a

Browse files
feat: spatial-attribution experiment infra + depth-confound correction
Adds the machinery for the weak-vs-dense supervision experiment (docs/EXPERIMENT_SPATIAL_ATTRIBUTION.md) and corrects two data defects that made the pathway targets largely a measurement of sequencing depth. Depth confound: the target score correlates with library size at |r|~0.93, and depth is itself spatially autocorrelated -- so ~65% of target variance was technical. Pathway files now store per-spot total_counts, and --residualize-depth regresses the measured covariate out at load time so training, checkpoint selection and evaluation optimise the same quantity. Regression beats PC1 removal: PC1 destroys 63% of variance and still leaks (|r|=0.077), regression destroys 51% and leaks exactly zero, lifting split-half reliability from 0.49 to 0.67. Output head: residualised targets are ~50% negative, which the softplus head cannot represent -- the model collapsed to a constant (pred_variance 1.5e-05, val_loss frozen). Adds output_activation={softplus,linear}, defaulting to linear whenever --residualize-depth is set. Gene identifiers: HEST uses three incompatible conventions and two of them silently scored 0/50 pathways while the batch summary still said "0 failed". Ensembl-indexed slides now resolve via their symbol column (41 slides -- the entire cardiac study, i.e. 41 of 42 Heart slides), and genome-prefixed symbols tolerate a variable underscore count (GRCh38__X and GRCh38______X; 16 slides, 15 of 17 Uterus slides). QC alignment: load_pathway_activities returns a valid_mask that the dataset discarded, so spots QC-filtered out of the pathway file were zero-filled and trained on as real measurements -- 8.5% of spots corpus-wide, up to 16.4% per slide. Those spots are now excluded. Also: study-grouped splitting (split_hest_by_dataset, iter_loso_folds, split_colonmap_vs_rest) wired to stf-train via --held-out-study / --colonmap-vs-rest / --technology, since `patient` is unusable as a grouping key for this corpus; per-slide feature standardisation (--normalize-features); attribution.py (attention, saliency, confound removal, fidelity metrics); and nine analysis scripts including a PCA+Ridge reproduction of the HEST-Benchmark protocol used to validate the data path. BREAKING: PATHWAY_FILE_VERSION 2 -> 3. Existing pathway files are rejected; regenerate with `stf-compute-pathways --overwrite`. Tests: 244 passed, 3 skipped (from 228). Signed-off-by: BenjaminIsaac0111 <12176376+BenjaminIsaac0111@users.noreply.github.com>
1 parent cfebfc3 commit 719df3a

16 files changed

Lines changed: 1981 additions & 26 deletions

File tree

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
# Experiment: Does weakly-supervised attention recover spatial biology?
2+
3+
**Status:** v2 redesign, revised 2026-07-30.
4+
5+
Corpus is complete: **421/421 human Visium slides** have CTransPath features and
6+
depth-aware pathway targets (`format_version 3`); H-Optimus-0 features exist for
7+
the 73 Bowel slides only. Pipeline validated against the published
8+
HEST-Benchmark. §2 records the v1 assumptions that measurement falsified; §6
9+
records why the backbone choice is load-bearing.
10+
11+
**Not yet run:** the model matrix itself. Outstanding build items are in §10.
12+
13+
## 1. The question
14+
15+
> Can a *weakly-supervised* (slide/bag-level) model resolve the **actual
16+
> spatial distribution of pathway activity** through its attention — and does
17+
> **direct spatial supervision** improve on this?
18+
19+
Weakly-supervised pathology models (MIL; SURVPATH for survival) routinely
20+
visualise attention spatially and argue it highlights the biologically relevant
21+
regions. None of them could *verify* that claim: they had no spatially-resolved
22+
ground truth. We do. This connects to the *Attention is/is-not Explanation*
23+
debate (Jain & Wallace 2019; Wiegreffe & Pinter 2019) by grounding attention
24+
validity in a biological signal rather than a proxy task.
25+
26+
## 2. What v1 got wrong (measured, not assumed)
27+
28+
Five findings from the review. Each is reproducible via the scripts named.
29+
30+
**(a) The shared component is sequencing depth, not "tissue density."**
31+
`|r(PC1, log total_counts)| = 0.93` median across slides (0.91 vs genes
32+
detected). Depth is itself spatially autocorrelated (Moran's I 0.76), which is
33+
*why* every pathway inherits a high raw Moran's I (~0.61). The old scorer's
34+
docstring claimed it was "depth-normalised via the prior CP10k step" — CP10k
35+
fixes the per-spot *total*, but the mean over a gene subset is still driven by
36+
dropout, so that claim was false.
37+
38+
**(b) PC1 removal is the wrong instrument.** Compared against regressing out
39+
the *measured* depth covariate:
40+
41+
| | variance destroyed | residual depth correlation |
42+
| --- | --- | --- |
43+
| PC1 removal | 63.0% | 0.077 |
44+
| depth regression | 51.3% | **0.000** |
45+
46+
Depth regression removes the confound completely while preserving ~12
47+
percentage points more variance. PC1 is an unsupervised direction that merely
48+
*correlates* with depth, so it also destroys biology aligned with it — and
49+
since tumour/stroma tracks cell density tracks depth, that plausibly includes
50+
the dominant biological axis.
51+
52+
**(c) The v1 headline contrast was close to tautological.** v1 compared
53+
`stf_dense[prediction]` (a directly supervised dense output) against
54+
`stf_weak[attention]` (an unsupervised internal mechanism). The supervised
55+
output was always going to win; that conflates *supervision* with *readout*.
56+
See §5 for the corrected contrasts.
57+
58+
**(d) Absolute fidelity numbers are uninterpretable on their own.** Three
59+
distinct quantities must not be conflated:
60+
61+
| quantity | value | meaning |
62+
| --- | --- | --- |
63+
| measurement reliability | 0.67 | how well the target reproduces itself (split-half, `measure_target_reliability.py`) |
64+
| **achievable** | **0.28 within-study / 0.19 cross-study** | what a strong linear baseline reaches (`baseline_pca_ridge.py`, H-Optimus-0) |
65+
| observed || what a given model scores |
66+
67+
Split-half reliability (binomial UMI split, both halves scored, Spearman-Brown
68+
corrected) is **0.85 for raw maps and 0.67 for depth-residualised maps** — so
69+
roughly a third of the pathway-specific variance is sampling noise. But
70+
reliability is an upper bound on *measurement*, not on *predictability from
71+
morphology*: the achievable figure is far lower. **Normalise reported fidelity
72+
by achievable, not by reliability**, and quote the baseline alongside every
73+
model number.
74+
75+
**(e) The selected pathways are ~6 axes, not 14.** Participation ratio 6.1;
76+
EMT↔MYOGENESIS r=0.78, IFN-γ↔IFN-α r=0.77, E2F↔G2M r=0.64. Averaging over 14
77+
as if independent overstates precision.
78+
79+
**Also corrected:** `pathway_morans_i` stored in each `.h5` is the **raw,
80+
zero-clipped** Moran's I, not the residual — it cannot shortcut pathway
81+
selection (`scripts/select_residual_pathways.py` recomputes properly).
82+
83+
## 3. Scope: all human Visium, not Bowel-only
84+
85+
v1 used 73 Bowel/Visium slides across 10 studies — 5 of them singletons, with
86+
COLON MAP at 56% of the corpus. That is structurally underpowered: a two-sided
87+
Wilcoxon on *n* paired folds cannot return p below 2^-(n-1), so with n<6 folds
88+
significance is unreachable *regardless of effect size*.
89+
90+
**v2 uses all human Visium: 421 samples, 61 studies, 16 organs.** LOSO becomes
91+
n=61 (floor ~0), COLON MAP drops to 10% of the corpus, and cross-organ
92+
generalisation becomes claimable rather than assumed. Acquisition needs 348
93+
downloads (~178 GB; storage is not a constraint) via
94+
`scripts/download_hest_visium.py`, which fetches only `st/` and `patches/`
95+
feature extraction reads pre-extracted patches, so the 25 GB of whole-slide
96+
images per ~95 samples is skipped.
97+
98+
Excluded: Xenium (73), Visium HD (21), Spatial Transcriptomics (163) — different
99+
chemistries and panels, so pathway-target semantics are not comparable.
100+
101+
## 4. Targets: depth-robust scoring + explicit QC
102+
103+
The v1 target (`mean log1p CP10k over member genes`) is ~65% sequencing depth
104+
and is not, strictly, "pathway activity" — it is gene-set mean expression.
105+
p53 activity is post-translational; averaging P53_PATHWAY transcripts does not
106+
measure it. This is a framing exposure as much as a technical one, and it is
107+
why PROGENy uses fitted downstream-responsive genes instead of nominal
108+
membership.
109+
110+
**Decision (revised after tuning, `scripts/tune_aucell.py`): keep the mean
111+
log1p CP10k score and regress out measured depth. Do not adopt AUCell.**
112+
113+
AUCell was the initial choice because within-spot ranking is depth-robust
114+
*and* preserves slide-stationarity. Tuning falsified the premise:
115+
116+
| config | depth `\|r\|` | reliability (ceiling) |
117+
| --- | --- | --- |
118+
| mean log1p (raw) | 0.751 ||
119+
| **mean log1p + depth-regression** | **0.000** | **0.668** |
120+
| AUCell R=100 (raw) | 0.274 ||
121+
| AUCell R=100 + depth-regression | 0.000 | 0.531 |
122+
123+
Depth regression removes the confound *completely*, so AUCell's rank-robustness
124+
buys nothing while its discarding of magnitude costs 0.14 of ceiling. Switching
125+
residualisation from PC1 to depth-regression also lifts the ceiling from 0.49
126+
to **0.668** on its own — the single largest improvement available here.
127+
128+
**The slide-stationarity objection dissolves under inspection.** The property
129+
was never actually achieved: median depth varies **83×** across studies and the
130+
score correlates with depth at 0.93, so identical biology already yields wildly
131+
different target values on different slides. Per-slide depth regression
132+
*restores* cross-slide comparability rather than breaking it — and the primary
133+
metric is within-slide anyway (§7), where per-slide transforms are harmless.
134+
135+
**Tuning also exposed a trap worth recording.** Optimising
136+
`reliability − depth_leakage` naively selects `max_rank=25`, which looks
137+
excellent (depth `|r|` 0.09) because **98% of scores are exactly zero and only
138+
5 of 50 pathways retain any variance** — a near-constant correlates with
139+
nothing. `tune_aucell.py` now rejects configurations failing a
140+
non-degeneracy guard (`--min-live-frac`, `--max-frac-zero`). Best *viable*
141+
AUCell setting was common-universe `max_rank=100` (all 50 pathways live), which
142+
is what the table above uses.
143+
144+
A second slide-stationarity hazard surfaced en route: HEST gene universes range
145+
**17,943–36,601** genes, so any *fractional* rank cutoff means a different
146+
absolute cutoff per slide. Irrelevant now that AUCell is dropped, but it would
147+
bite any future rank-based scorer.
148+
149+
Remaining requirements:
150+
151+
- **QC-filter on reliability, not just depth.** Slides whose targets barely
152+
reproduce against themselves contribute noise: MISC69 (607 genes/spot) has
153+
residual reliability 0.255 vs 0.550 for MISC73. Drop below a preregistered
154+
threshold.
155+
- **Apply the residualisation to training targets too**, so training,
156+
checkpoint selection and evaluation all optimise the same quantity. v1
157+
trained and selected on raw targets while grading on the residual, which
158+
would have made a weak `stf_dense` result ambiguous between "dense
159+
supervision fails" and "we never optimised for it."
160+
161+
## 5. Design: hold the readout fixed, vary the supervision
162+
163+
The corrected primary contrasts — same readout on both sides, so only
164+
supervision differs:
165+
166+
| Contrast | Isolates |
167+
| --- | --- |
168+
| `stf_dense[attention]` vs `stf_weak[attention]` | **Primary.** Same mechanism, different supervision. |
169+
| `stf_dense[prediction]` vs `stf_weak[dense-head]` | **Primary.** Same head, different supervision. |
170+
| `stf_dense[prediction]` vs `stf_weak[attention]` | Reported for continuity with prior work, flagged as readout-confounded. |
171+
172+
Both ingredients were already being computed in v1 — this is a choice-of-contrast
173+
fix, not new data collection.
174+
175+
## 6. Model matrix
176+
177+
Identical features (**H-Optimus-0**, `--backbone h_optimus_0`), study-grouped
178+
splits, pathway set, loss and epochs.
179+
180+
**Backbone choice is load-bearing, not incidental.** CTransPath is second-worst
181+
of the eleven encoders in the HEST-Benchmark, and on this task it is the
182+
difference between an experiment and a null result:
183+
184+
| backbone | within-study | cross-study (LOSO) |
185+
| --- | --- | --- |
186+
| CTransPath | 0.122 | **-0.026** |
187+
| H-Optimus-0 | **0.280** | **+0.192** |
188+
189+
With CTransPath, cross-study transfer collapses to nothing and every arm of the
190+
matrix would score ~0 — the study-grouped LOSO protocol of §3 would be
191+
unrunnable. With H-Optimus-0 cross-study *exceeds* CTransPath's within-study
192+
figure, so LOSO is viable. UNI / GigaPath / Virchow need institutional access
193+
and are unavailable; Phikon is reachable but only ~6% better than CTransPath on
194+
average. **Spatial PE enabled on both STF arms** — the experiment is about
195+
spatial biology, so a weak-attention result must not be attributable to a
196+
missing positional encoding; PE-on remains architecture-matched across the pair.
197+
198+
| ID | `--model` | Supervision | Per-pathway signal(s) |
199+
| --- | --- | --- | --- |
200+
| `stf_dense` | `interaction` | dense per-spot | dense prediction; pathway→patch attention; gradient saliency |
201+
| `stf_weak` | `interaction --weak-supervision` | bag | **dense-head output** (never spot-supervised); attention; saliency |
202+
| `attn_mil` | `attention_mil --weak-supervision` | bag | shared attention (single map); saliency |
203+
| `transmil` | `transmil --weak-supervision` | bag | shared CLS→patch attention; saliency |
204+
| `random` | `interaction` (untrained) || attribution floor |
205+
206+
MIL shared attention is one map broadcast across pathways, so its flat
207+
per-pathway profile is a *structural property*, not an empirical finding —
208+
state it as such. Gradient saliency is what gives MIL a genuinely per-pathway
209+
map.
210+
211+
## 7. Metric: fidelity as a fraction of the achievable ceiling
212+
213+
Per held-out slide, per pathway, produce an `(N, P)` signal map and compare to
214+
ground truth:
215+
216+
- **Primary — residual fidelity, reported against the linear baseline:**
217+
Pearson between depth-residualised signal and depth-residualised truth, quoted
218+
*alongside* `baseline_pca_ridge.py` run on the identical split (§2d). A model
219+
scoring 0.19 cross-study has merely matched Ridge; beating it is the claim
220+
worth making. Split-half reliability is reported as context, not as the
221+
denominator.
222+
- **Secondary — raw fidelity**, for comparability with prior work, explicitly
223+
flagged as depth-inflated.
224+
- **Report by cluster** (§2e): aggregate within the ~6 empirical pathway
225+
clusters before averaging, so no axis is counted five times.
226+
- Per-slide z-scoring before correlating; Spearman alongside Pearson.
227+
- **Statistics at study level**, paired across held-out studies (n=61).
228+
229+
## 8. Controls
230+
231+
- **Negative:** untrained `random` → fidelity ≈ 0.
232+
- **Ceiling:** split-half reliability per slide and per pathway.
233+
- **Sanity:** dense-prediction fidelity must match engine `validate()`
234+
per-pathway PCC (implemented, agrees to 1.4e-08).
235+
- **Batch shortcut:** `dataset_title` is 98.5% predictable from CTransPath
236+
features (vs 60.3% majority baseline), and median depth varies **83×** across
237+
studies — so "predict depth from H&E" partly reduces to study recognition.
238+
Study-grouped LOSO handles test leakage but not within-training shortcutting;
239+
report this alongside results. Note this was measured on CTransPath, and much
240+
of it appears to be a CTransPath weakness: per-slide feature standardisation
241+
is essential there (-0.026 → +0.044) but nearly irrelevant for H-Optimus-0
242+
(+0.192 → +0.198), whose embeddings are already far more study-invariant.
243+
Worth re-measuring the shortcut on H-Optimus-0 before quoting it.
244+
- **Two attributions** (attention + gradient) so conclusions aren't artefacts of
245+
one explainer.
246+
247+
## 9. Expected outcomes
248+
249+
| Observation | Interpretation |
250+
| --- | --- |
251+
| `stf_dense[attention]``stf_weak[attention]` | Dense supervision improves the *same* mechanism — supports the thesis. |
252+
| `stf_dense[attention]``stf_weak[attention]` | Attention localises comparably regardless of supervision. |
253+
| `stf_weak[dense-head]` > 0 | Bag supervision localises implicitly, without spot labels. |
254+
| All arms ≈ 0 **and** ceiling ≈ 0 | Targets are noise-dominated; report as a measurement-limit result, not a modelling one. |
255+
| Raw fidelity high, residual ≈ 0 | Model is riding depth, not resolving pathway-specific biology. |
256+
| Shared-attention MIL flat across pathways | Structural, not empirical (§6). |
257+
258+
## 10. Build status
259+
260+
Reusable from v1: `attribution.py`, dataset-grouped splitting + CLI,
261+
`evaluate_spatial_attribution.py` (incl. §8 sanity check),
262+
`run_attribution_experiment.py`, `diagnose_batch_shortcut.py`, tests.
263+
New in v2: `select_residual_pathways.py`, `measure_target_reliability.py`,
264+
`download_hest_visium.py`.
265+
266+
Still to build: AUCell scorer in `compute_pathway_activities.py` (with
267+
`max_rank` tuned per §4 and a `format_version` bump), depth-residualised
268+
training targets, reliability-normalised metric and cluster aggregation in the
269+
eval script, and the corrected primary contrasts in the orchestrator.
270+
271+
## 11. Sequencing
272+
273+
1. **Acquire** (running) — 348 human-Visium samples.
274+
2. **Fix targets on the existing 73** — implement AUCell, tune `max_rank`
275+
against depth correlation, re-measure reliability. Validate small before
276+
scaling: recomputing 421 samples with a bad scorer is the expensive mistake.
277+
3. **Extract features + score pathways** at full scale.
278+
4. **Pilot** COLON-MAP-vs-rest to validate the pipeline end-to-end.
279+
5. **Full LOSO** (n=61) for the headline result.

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ filterwarnings = [
5858
"ignore::DeprecationWarning",
5959
"ignore:FigureCanvasAgg is non-interactive:UserWarning",
6060
"ignore:os.fork:RuntimeWarning",
61+
# torch.compile perf hint on Ampere+. Deliberately not acted on: TF32 gives
62+
# ~+23% on fp32 matmuls, but the hot paths (training, feature extraction)
63+
# run under --use-amp, which is ~2.3x faster still. Enabling TF32 globally
64+
# would trade precision for a speedup we do not benefit from.
65+
"ignore:TensorFloat32 tensor cores:UserWarning",
6166
"ignore:torch.utils._pytree._register_pytree_node is deprecated:UserWarning",
6267
]
6368

0 commit comments

Comments
 (0)