Skip to content

Feature/mil benchmark and representation methods - #154

Open
Dinesh-Adhithya-H wants to merge 9 commits into
lueckenlab:mainfrom
Dinesh-Adhithya-H:feature/mil-benchmark-and-representation-methods
Open

Feature/mil benchmark and representation methods#154
Dinesh-Adhithya-H wants to merge 9 commits into
lueckenlab:mainfrom
Dinesh-Adhithya-H:feature/mil-benchmark-and-representation-methods

Conversation

@Dinesh-Adhithya-H

Copy link
Copy Markdown

No description provided.

…ions

New modules
-----------
src/patpy/tl/mil_models.py
  TorchMILWrapper adapter for torchmil (ABMIL, TransMIL, DSMIL) with
  per-label training, cell-importance extraction, and multi-class support.

src/patpy/tl/benchmark.py
  MILBenchmark: cross-validated benchmark for inductive MIL models.
  RepresentationBenchmark: benchmark for any method that returns sample
  representations (unsupervised Pseudobulk / CellComposition / GroupedPseudobulk
  and supervised MixMIL / ABMIL used as feature extractors), fitting a
  LogisticRegression / Ridge probe on train and evaluating on val/test.

src/patpy/pp/splits.py
  make_sample_splits: stratified train/val/test splitting at sample level.

src/patpy/pl/mil.py
  plot_attention_umap, plot_attention_by_cell_type,
  plot_attention_celltype_heatmap: cell-level attention visualisations.

Changes to existing files
--------------------------
src/patpy/tl/supervised.py
  MixMIL: get_sample_representations, fine_tune, predict_on_adata.
  PULSAR / PaSCient: get_sample_representations.

src/patpy/pl/__init__.py, src/patpy/pp/__init__.py, src/patpy/tl/__init__.py
  Export new public symbols.

pyproject.toml
  Add torchmil and related optional dependencies.

docs/notebooks/mil_methods_example.ipynb
  Tutorial covering MIL training, cross-validated benchmarking,
  patient UMAPs, representation quality, ROC curves, and attention plots
  for multiple labels.
n_classes in _build_tensors now reads from _label_mappings (built on full
adata) instead of np.unique on the training split, preventing index-out-of-
bounds when a class is absent from the current split.

Also rename `col` → `lk_vals` in MILBenchmark._run_one_split to avoid
shadowing the outer split-column variable.
…tcome)

_build_label_mappings and MILBenchmark pre-population both skipped numeric
columns. Numeric classification labels (e.g. Outcome with values 2.0-5.0)
were cast directly to int, producing out-of-range class indices when target
values don't start at 0.

Fix: build a sorted 0-indexed class mapping for all classification labels
regardless of dtype.
ABMIL does not expose intermediate bag embeddings so it cannot be used
as a representation extractor. Remove it from the rep_models dict.
Use a forward hook on model.classifier to capture the bag embedding
(attention-weighted aggregation of cell features) as the patient
representation. Re-adds ABMIL to the RepresentationBenchmark.
@review-notebook-app

Copy link
Copy Markdown

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

@VladimirShitov

Copy link
Copy Markdown
Member

We moved the tutorials to https://github.com/lueckenlab/patpy_tutorials , can you push the notebook there instead?

@VladimirShitov

Copy link
Copy Markdown
Member

Also please render the notebook. It will appear in the documentation in the same way as it looks as a notebook. So all the images and cell outputs must be there

@VladimirShitov VladimirShitov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a very valuable contribution, thanks for implementing it! I left some comments that need to be addressed before the PR can be merged. Briefly:

  1. The tutorial notebook should be moved to a separate repo and rendered. Also, I'd advocate for extending supervised methods example notebook instead of creating a new one.
  2. Benchmark functionality should be deleted from this PR and moved to another one. It's definitely very interesting, but too much for this PR, and it will slow down the merging.
  3. Some code pieces need improvement, see the comments below.

Comment thread src/patpy/pl/mil.py Outdated
Comment thread src/patpy/pl/mil.py Outdated
"raw pre-softmax scores — returning softmax-normalised weights instead.",
stacklevel=3,
)
return model.get_cell_importance(label=label)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should simply be an extension of get_cell_importance method in the corresponding class. I don't see necessity in a separate function, and certainly not in the plotting module.

Comment thread src/patpy/pl/mil.py
Comment thread src/patpy/pl/mil.py
Comment thread src/patpy/pl/mil.py Outdated
att_col = f"{label}_importance"
weight_label = "Mean attention (softmax)" if normalized else "Mean attention score (pre-softmax)"

frame = pd.DataFrame(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is repeated in every function, worth a separate util function

# Capture pre-softmax inputs via a hook on nn.Softmax modules
pre_softmax: list[torch.Tensor] = []

def _make_hook(store: list):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it needed on what does it do? Please leave a comment in the code

model.eval()
weights_all: list[np.ndarray] = []

if normalized:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function looks very complicated and some blocks of code repeat in different branches. Can it be simplified?

Comment thread src/patpy/tl/mil_models.py Outdated
captured: list[torch.Tensor] = []

def _hook(module, inp, out):
captured.append(inp[0].detach().cpu())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a simpler way of doing it?

lr: float = 1e-3,
weight_decay: float = 1e-4,
device: str = "auto",
seed: int = 42,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to double check, are the defaults the same as in original repo or the ones authors recommend?

weight_decay=weight_decay,
device=device,
seed=seed,
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love how simple these classes look, this is great!

Dinesh-Adhithya-H and others added 4 commits May 6, 2026 14:15
torchmil models expose return_rep in their forward signature; use it
directly instead of a classifier-input hook. Falls back to the hook for
models that don't support return_rep.
- Add TransMIL and DSMIL to make_models with max_cells_per_bag=2000 to
  prevent OOM on large COMBAT bags (up to ~9k cells/patient)
- Use return_rep=True (local torchmil extension) for bag embeddings with
  fallback to classifier-input hook for stock torchmil
- Add paper-ready summary table: mean ± std on test set, pivoted by
  label × model × metric
- Add patient representation UMAPs for all methods (unsupervised +
  MixMIL/ABMIL) using model.plot_embedding()
- Remove module-level docstring header from pl/mil.py (reviewer request)
- Replace _get_cell_importance wrapper with _store_importance that writes
  weights into adata.obs for sc.pl compatibility
- plot_attention_umap: use sc.pl.umap per-class subset instead of raw
  matplotlib scatter
- plot_attention_by_cell_type: use sc.pl.violin grouped by cell type
- Add normalized= parameter to MixMIL.get_cell_importance for API
  consistency with TorchMILWrapper (MixMIL always returns softmax weights)
- Fix val_size docstring in splits.py: clarifies it is a fraction of all
  samples, not of remaining (non-test) samples

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@VladimirShitov

Copy link
Copy Markdown
Member

Could you also add tests to make sure the code is running well? Coding agents typically do it well

Comment thread src/patpy/tl/benchmark.py
label_keys: list[str],
tasks: list[_PREDICTION_TASKS],
n_splits: int = 3,
split_col: str = "split",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is in common cases no split for unsupervised representation methods, no?

Comment thread src/patpy/tl/benchmark.py
else:
return pd.Series(probe.predict(X), index=index, name=label)

def _compute_metrics(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running time could be interesting to report as well. @VladimirShitov wdyt?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, definitely!

@VladimirShitov

Copy link
Copy Markdown
Member

@Dinesh-Adhithya-H , could you address comments in this PR and move the benchmarking code to another one?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants