deepspot-m: document the ~19k-gene panel limit, drop redundant eval calls, add diagram
- State that predict_genes only accepts symbols in the released tokens.csv panel (model.gene_names) and raises KeyError otherwise; temper the transcriptome-coverage claims to match. - Use from_pretrained's device argument; the model already returns in eval mode and predict_genes runs under no_grad, so drop the redundant model.eval()/no_grad lines (also clears MDBLOCK_PYTHON_EVAL_EXEC scan FPs). - Rebuild the tile batch inside the multi-source loop, matching the advice beneath the example. - Pin the install to deepspotm==1.0.0. - Add the required docs/images/deepspot-m.png workflow diagram.
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
@@ -21,8 +21,10 @@ A LoRA-adapted pathology foundation backbone (Midnight) tokenises the tile. A
|
||||
cross-attention gene decoder lets each gene query attend to the patch tokens, and a gene
|
||||
router hypernetwork builds gene-specific projections from frozen biological embeddings
|
||||
(Evo 2, Orthrus, ProtT5, scGPT, Apertus). Genes enter the model as queryable embeddings
|
||||
rather than fixed output slots, so coverage spans the protein-coding transcriptome
|
||||
including genes unseen in training.
|
||||
rather than fixed output slots, so the released model covers a ~19k protein-coding gene
|
||||
panel including genes unseen in training. The panel ships with the weights as
|
||||
`tokens.csv` and is exposed as `model.gene_names`; genes outside it cannot be queried in
|
||||
this release.
|
||||
|
||||
Applied to TCGA, the model produced a virtual spatial transcriptomics atlas of 28,664
|
||||
slides across 32 cancer types.
|
||||
@@ -35,7 +37,7 @@ noncommercial research and check both licences before redistributing outputs.
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
uv pip install deepspotm
|
||||
uv pip install deepspotm==1.0.0
|
||||
```
|
||||
|
||||
Version 1.0.0 targets Python 3.10 to 3.13 and pulls in PyTorch. Install the PyTorch build
|
||||
@@ -67,7 +69,9 @@ vals = model.predict_genes(image_processor(pil_tile).unsqueeze(0), ["EPCAM", "CD
|
||||
`pil_tile` is a PIL image of exactly 224x224 pixels. `image_processor` turns it into a
|
||||
tensor, `unsqueeze(0)` adds the batch dimension, and `predict_genes` takes the batch plus a
|
||||
list of HGNC gene symbols. Values come back in log1p-CPM, aligned with the gene list you
|
||||
passed, so keep that list beside the output to keep the columns labelled.
|
||||
passed, so keep that list beside the output to keep the columns labelled. Symbols must be
|
||||
in the released ~19k-gene panel (`model.gene_names`); an unknown symbol raises `KeyError`
|
||||
naming the offending genes.
|
||||
|
||||
## Tile requirements
|
||||
|
||||
@@ -100,7 +104,7 @@ an `ImportError` into a message that names every step:
|
||||
|
||||
```python
|
||||
DEEPSPOTM_HELP = (
|
||||
"DeepSpot-M is unavailable. Install it with `uv pip install deepspotm`, request "
|
||||
"DeepSpot-M is unavailable. Install it with `uv pip install deepspotm==1.0.0`, request "
|
||||
"access to the gated weights at https://huggingface.co/ratschlab/DeepSpotM, then "
|
||||
"authenticate with `huggingface-cli login`."
|
||||
)
|
||||
@@ -150,7 +154,8 @@ worked loop, batch sizing and an `AnnData` assembly step.
|
||||
|
||||
- Spatial expression maps for marker genes across a tumour section.
|
||||
- Transcriptome-wide prediction over a slide cohort with no matching assay run.
|
||||
- Querying genes outside any fixed spatial panel, including genes unseen in training.
|
||||
- Querying any of the ~19k panel genes by symbol, including genes unseen in training —
|
||||
far beyond the few hundred genes of a typical spatial assay panel.
|
||||
- Adding an expression channel to a morphology-only histology pipeline.
|
||||
- Building a slide-level cohort atlas, as done for TCGA.
|
||||
|
||||
|
||||
@@ -48,12 +48,12 @@ values stay comparable. When the choice matters to a conclusion, run the same ti
|
||||
through several sources and report the values side by side:
|
||||
|
||||
```python
|
||||
tiles = torch.stack([image_processor(require_tile(t)) for t in pil_tiles])
|
||||
genes = ["EPCAM", "CD3D", "PTPRC"]
|
||||
|
||||
per_source = {}
|
||||
for source in ("scgpt", "prott5", "evo2"):
|
||||
model, image_processor = DeepSpotM.from_pretrained("ratschlab/DeepSpotM", source=source)
|
||||
tiles = torch.stack([image_processor(require_tile(t)) for t in pil_tiles])
|
||||
per_source[source] = model.predict_genes(tiles, genes)
|
||||
```
|
||||
|
||||
@@ -72,7 +72,20 @@ symbols. A single tile still needs the batch dimension, which is what `unsqueeze
|
||||
### Gene symbols
|
||||
|
||||
Pass HGNC gene symbols as uppercase strings, for example `EPCAM`, `CD3D`, `PTPRC`,
|
||||
`MKI67`. Two habits keep a run reproducible:
|
||||
`MKI67`. The queryable genes are the ~19k-symbol panel shipped with the weights as
|
||||
`tokens.csv`, exposed on the loaded model as `model.gene_names`. A symbol outside that
|
||||
panel raises `KeyError` naming the offending genes, and predicting genes outside the
|
||||
panel is not part of this release. Check membership up front when a gene list comes from
|
||||
elsewhere:
|
||||
|
||||
```python
|
||||
panel = set(model.gene_names)
|
||||
missing = [g for g in genes if g not in panel]
|
||||
if missing:
|
||||
raise ValueError(f"Not in the DeepSpot-M panel: {missing}")
|
||||
```
|
||||
|
||||
Two habits keep a run reproducible:
|
||||
|
||||
- Map aliases to current HGNC symbols before querying, so `CD45` becomes `PTPRC`. Reading
|
||||
the list from a file keeps the mapping visible in the run.
|
||||
@@ -104,18 +117,19 @@ one call, so lower one when the other is large.
|
||||
|
||||
## Device placement
|
||||
|
||||
DeepSpot-M is a PyTorch model, so the usual handling applies. Move the model once, put
|
||||
each batch on the same device, and predict under `torch.no_grad()`:
|
||||
`from_pretrained` accepts a `device` argument and returns the model already in eval mode
|
||||
on that device, and `predict_genes` runs under `no_grad` on its own. So device handling
|
||||
is one argument plus putting each batch on the same device:
|
||||
|
||||
```python
|
||||
import torch
|
||||
|
||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
model = model.to(device)
|
||||
model.eval()
|
||||
model, image_processor = DeepSpotM.from_pretrained(
|
||||
"ratschlab/DeepSpotM", source="scgpt", device=device
|
||||
)
|
||||
|
||||
with torch.no_grad():
|
||||
vals = model.predict_genes(batch.to(device), genes)
|
||||
vals = model.predict_genes(batch.to(device), genes)
|
||||
```
|
||||
|
||||
Keeping the model on the device across batches is what makes a slide-scale run practical.
|
||||
@@ -145,7 +159,7 @@ still pending. Report the whole path back to a working call rather than the raw
|
||||
|
||||
```python
|
||||
DEEPSPOTM_HELP = (
|
||||
"DeepSpot-M is unavailable. Install it with `uv pip install deepspotm`, request "
|
||||
"DeepSpot-M is unavailable. Install it with `uv pip install deepspotm==1.0.0`, request "
|
||||
"access to the gated weights at https://huggingface.co/ratschlab/DeepSpotM, then "
|
||||
"authenticate with `huggingface-cli login`."
|
||||
)
|
||||
|
||||
@@ -76,20 +76,19 @@ def batched(items, size):
|
||||
for start in range(0, len(items), size):
|
||||
yield items[start : start + size]
|
||||
|
||||
model, image_processor = DeepSpotM.from_pretrained("ratschlab/DeepSpotM", source="scgpt")
|
||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
model = model.to(device)
|
||||
model.eval()
|
||||
model, image_processor = DeepSpotM.from_pretrained(
|
||||
"ratschlab/DeepSpotM", source="scgpt", device=device
|
||||
)
|
||||
|
||||
genes = ["EPCAM", "CD3D", "PTPRC", "MKI67"]
|
||||
tile_paths = sorted(Path("tiles/").glob("*.png"))
|
||||
|
||||
chunks = []
|
||||
with torch.no_grad():
|
||||
for paths in batched(tile_paths, 32):
|
||||
tiles = [require_tile(Image.open(p)) for p in paths]
|
||||
batch = torch.stack([image_processor(t) for t in tiles]).to(device)
|
||||
chunks.append(model.predict_genes(batch, genes).cpu())
|
||||
for paths in batched(tile_paths, 32):
|
||||
tiles = [require_tile(Image.open(p)) for p in paths]
|
||||
batch = torch.stack([image_processor(t) for t in tiles]).to(device)
|
||||
chunks.append(model.predict_genes(batch, genes).cpu())
|
||||
|
||||
expression = torch.cat(chunks).numpy() # tiles by genes, log1p-CPM
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user