Batch-corrected latent representations of single-cell RNA-seq data with a hierarchical VAE (HVAE), followed by confidence-based filtering of the resulting embedding.
The pipeline has three stages, all run by main.py:
-
Preprocessing (
preprocessing/create_data.py): loads the marker-gene list and optionally selects HVGs / splits the data (both disabled in the standard run). -
Training (
train_hvae.py,models/vae.py): a ZINB-decoder VAE with- learnable GMM priors over the latent
zand the per-batch library size, where each component's parameters are a shared base plus a per-component deviation (--no-shared_base_priorfor independent per-component parameters), - a library-size KL selectable with
--library_kl:original(default; sums every component's KL),marginalized(weights each component's KL byq_pi) orbatch(only each cell's batch-label component, matching how the library is sampled), - optionally (
--decoder_covariates) the batch embedding injected into the decoder, - an RBF-kernel MMD term between
q(z)and the prior (batch integration), - a marker-gene contrastive head: an NT-Xent loss on a projection of
zin which pairs of cells are weighted as positives by a Laplacian kernel over their log-normalized expression of each marker gene, averaged over marker genes, - a linearly annealed KL weight.
Minibatches are built by a domain-aware sampler (
dataset/sampler.py). - learnable GMM priors over the latent
-
Cleanup (
clean_data.py,plotting.py): Leiden clustering + t-SNE of the embedding, a radius-neighbors confidence model, and per-cluster filtering of low-confidence cells. Writes Plotly HTML plots.
conda env create -f environment.yml
conda activate scRefineenvironment.yml pins the minor versions scRefine was tested with (Python 3.11,
PyTorch 2.14, scanpy 1.11, PyTorch Lightning 2.6, Pyro 1.9) and installs the CUDA 12.6
build of PyTorch. A GPU is used automatically when available; to check that PyTorch sees
it:
python -c "import torch; print(torch.cuda.is_available())"If this prints False on a GPU machine, your NVIDIA driver is likely too old for CUDA
12.6. Change cu126 in environment.yml to a CUDA version your driver supports (see
https://pytorch.org/get-started).
An .h5ad file with raw counts in .X, and in .obs:
- a batch/domain column (default in
run.sh:dataset), - a cell-type label column (default in
run.sh:azimuth_final_level_label).
The marker genes in marker_genes.txt (one symbol per line) should be present in
var_names. Replace the file to use a different marker panel.
./run.sh /path/to/data.h5ad # seed 42
./run.sh /path/to/data.h5ad 1 # another seed
./run.sh /path/to/data.h5ad 42 --epochs 100 --no_plots # extra main.py argumentsEdit DATASET_SYMBOL / LABEL_SYMBOL at the top of run.sh to match your data. Outputs
go to the current working directory. See python main.py --help for all options.
--preset selects a named parameter set from PRESETS in main.py:
| Preset | Setting | Marker-gene head | Differs from the other preset in |
|---|---|---|---|
1 |
contrastive | NT-Xent loss on a projection of z (use_gene_contrastive=True, temperature 0.5) |
encode_covariates=True, shared_base_prior=True |
2 |
classifier | linear head predicting binned marker-gene expression, MSE loss (use_gene_contrastive=False) |
encode_covariates=False, shared_base_prior=False |
Both presets share the remaining values: 50 epochs, latent dimension 90, hidden dimension
128, 1 layer, learning rate 0.001, batch size 255, patience 15, kl_weight 0.001
(annealed to 0.05 over 20 epochs), mmd_weight 0.1, reconstruction_weight 1.0,
gene_factor 50, library_kl=original, no decoder covariates, and cleanup with
cluster_min 0.2, quantil 0.2, quantil_max 0.3.
A preset only replaces the parser defaults; any argument passed explicitly on the command line still takes precedence:
python main.py --preset 2 --epochs 100 ... # preset 2, but trained for 100 epochsNote that run.sh passes several of these arguments explicitly (including
--use_gene_contrastive), so they override a preset given as an extra argument. To run
preset 2 through run.sh, also pass --no-use_gene_contrastive, or call main.py
directly.
The scripts learn an embedding directly from existing
hard labels, such as cell-type annotations. Both read the same .h5ad input (raw counts
in .X).
An MLP Encoder (models/encoder.py) is trained with a supervised NT-Xent loss: within
a minibatch, cells that share a label in --label_symbol are positives and all other
cells are negatives. Minibatches are plain shuffled batches with no domain-aware
sampling and no batch-correction term. Only a random --sample_fraction of the cells
(default 0.1) is used for training. All cells are embedded afterwards.
python train_simclr.py --data_path data.h5ad --dataset_symbol dataset \
--label_symbol azimuth_final_level_label --epochs 100 \
--embedding_save embedding_simclr.npz --plot_name simclr_tsneWrites the embedding .npz (embedding, labels, batches, obs_names, same layout as
the HVAE output), the encoder weights (--contrastive_model), and optionally a t-SNE plot
(--plot_name). Other options: --latent_dim (default 1024), --batch_size (32),
--temperature (0.5).
classify.py first trains the SimCLR embedding above on --data_path (an .h5ad, with
the same SimCLR options). It saves the embedding to --embedding_save and then evaluates
it, depending on --run_type:
nn(default): trains a small MLP classifier (models/classifier.py) on the embedding to predict the labels (90/10 train/validation split) and prints train/validation accuracy per epoch. Saves the classifier weights to--model_pathand the validation labels, predictions and raw outputs to--evaluation(written withnp.savez, so give it an.npzname).kmeans: runs K-Means for several cluster counts and maps each cluster to the majority label of 50 sampled cells. It then fits a k-NN classifier on these pseudo-labels and prints accuracy, weighted F1 and silhouette scores against the true labels.predict: skips training. It loads an existing embedding (--embedding_save) and classifier (--model_path) and saves the predicted label indices to--evaluation(.npy).
python classify.py --data_path data.h5ad --dataset_symbol dataset \
--label_symbol azimuth_final_level_label --epochs 100 --run_type nn \
--embedding_save embedding_simclr.npz --model_path classifier.pth \
--evaluation classification.npzBecause both approaches are supervised by the labels, they only work on data that is already annotated.
With prefix screfine_seed<SEED>:
| File | Content |
|---|---|
<prefix>.npz |
HVAE embedding: embedding, batches, labels, obs_names |
<prefix>.ckpt |
trained model checkpoint |
<prefix>_conf.npz |
embedding with per-cell confidence and t-SNE coordinates |
<prefix>_filtered.npz |
cells kept after confidence filtering |
plots_<prefix>_*.html |
t-SNE plots (skipped with --no_plots) |