Unify BioMod names, flatten weight paths, and remove duplicate source directories
Browse filesRename project-owned entrypoints, resource directories, classes and helpers to BioMod/biomod, and update imports, notebooks, documentation and command examples. Preserve old CLI flags as aliases and keep serialized state/configuration keys, raw gene-set IDs, result schemas and third-party interfaces compatible. Move the existing checkpoint/configuration pair to weight/BioMod.pth and weight/BioMod.pkl using their unchanged objects. Remove only the eight approved duplicate files from the source module-data directory and nested attention package; retain the shared copies. Raw datasets, legacy reference data copies and all weight contents remain unchanged. Validation: 90 Python 3 files, 65 notebook cells, 12 utility tests, 7 synthetic backbone comparisons and 10 classifier comparisons passed. No real model weights or datasets were deserialized, and no real-data training was run.
- .gitignore +3 -7
- BioMod/README.md +57 -73
- BioMod/{performer_module.py → biomod.py} +25 -25
- BioMod/{module_dataset.py → biomod_dataset.py} +4 -4
- BioMod/cell_annotation/{module_data/module1_hallmarks.json → biomod_data/biomod1_hallmarks.json} +0 -0
- BioMod/cell_annotation/{module_data/module2_reactome_2022_filtered.json → biomod_data/biomod2_reactome_2022_filtered.json} +0 -0
- BioMod/cell_annotation/{module_data/load_module.py → biomod_data/load_biomod.py} +2 -2
- BioMod/cell_annotation/{performer_module_finetune.py → biomod_finetune.py} +75 -74
- BioMod/cell_annotation/cmd_finetune.md +4 -4
- BioMod/command.txt +4 -4
- BioMod/drug_combination/README.md +23 -23
- BioMod/drug_combination/{train_drug_combination_performer_module.py → train_drug_combination_biomod.py} +60 -60
- BioMod/drug_response/cmd.txt +4 -4
- BioMod/drug_response/{train_drug_performer_module.py → train_drug_biomod.py} +78 -78
- BioMod/environment.yml +2 -2
- BioMod/gene_perturbation/README.md +30 -30
- BioMod/gene_perturbation/gears_reduce_local.py +2 -2
- BioMod/gene_perturbation/{model_module.py → model_biomod.py} +31 -31
- BioMod/gene_perturbation/{train_module.py → train_biomod.py} +20 -20
- BioMod/interpretability/README.md +55 -55
- BioMod/interpretability/__init__.py +4 -4
- BioMod/interpretability/attention/README.md +12 -12
- BioMod/interpretability/attention/README_gene_attention.md +11 -11
- BioMod/interpretability/attention/{module_crosstalk.py → biomod_crosstalk.py} +21 -21
- BioMod/interpretability/attention/{module_gene_attention.py → biomod_gene_attention.py} +19 -19
- BioMod/interpretability/{module_geometry.py → biomod_geometry.py} +27 -27
- BioMod/interpretability/{module_importance → biomod_importance}/README.md +32 -32
- BioMod/interpretability/{module_importance → biomod_importance}/RESULTS.md +5 -5
- BioMod/interpretability/{module_importance → biomod_importance}/TODO.md +1 -1
- BioMod/interpretability/{module_importance → biomod_importance}/__init__.py +0 -0
- BioMod/interpretability/{module_importance/module_importance.py → biomod_importance/biomod_importance.py} +32 -32
- BioMod/interpretability/{module_importance/module_importance_publication.py → biomod_importance/biomod_importance_publication.py} +5 -5
- BioMod/interpretability/{module_importance → biomod_importance}/cross_class_similarity.py +0 -0
- BioMod/interpretability/{module_importance → biomod_importance}/family_ablation.py +8 -8
- BioMod/interpretability/{module_importance → biomod_importance}/family_importance_analysis.py +0 -0
- BioMod/interpretability/{module_importance → biomod_importance}/marker_gene_verification.py +8 -8
- BioMod/interpretability/{module_importance → biomod_importance}/masking_ablation.py +8 -8
- BioMod/interpretability/{module_importance → biomod_importance}/pub_utils.py +7 -7
- BioMod/interpretability/{module_importance → biomod_importance}/random_baseline.py +9 -9
- BioMod/interpretability/{module_importance → biomod_importance}/tcell_reactome_analysis.py +0 -0
- BioMod/interpretability/{module_text_alignment → biomod_text_alignment}/DESIGN.md +33 -33
- BioMod/interpretability/{module_text_alignment → biomod_text_alignment}/MANUSCRIPT_SECTION.md +0 -0
- BioMod/interpretability/{module_text_alignment → biomod_text_alignment}/__init__.py +0 -0
- BioMod/interpretability/{module_text_alignment/module_text_alignment.py → biomod_text_alignment/biomod_text_alignment.py} +14 -14
- BioMod/interpretability/{module_text_alignment/module_text_alignment_extended.py → biomod_text_alignment/biomod_text_alignment_extended.py} +15 -15
- BioMod/interpretability/{module_text_alignment/module_text_alignment_publication.py → biomod_text_alignment/biomod_text_alignment_publication.py} +33 -33
- BioMod/interpretability/{module_text_alignment → biomod_text_alignment}/fetch_reactome_descriptions.py +9 -9
- BioMod/interpretability/{module_text_alignment → biomod_text_alignment}/reactome_descriptions.json +0 -0
- BioMod/interpretability/utils.py +5 -5
- BioMod/{models_performer_module.py → models_biomod.py} +38 -38
|
@@ -31,12 +31,8 @@ __pycache__/
|
|
| 31 |
.env
|
| 32 |
*.local
|
| 33 |
|
| 34 |
-
#
|
| 35 |
!weight/
|
| 36 |
weight/*
|
| 37 |
-
!weight/
|
| 38 |
-
!weight/
|
| 39 |
-
!weight/0629model/*.pth
|
| 40 |
-
!weight/0810model/
|
| 41 |
-
!weight/0810model/*.pkl
|
| 42 |
-
!weight/0810model/*.pth
|
|
|
|
| 31 |
.env
|
| 32 |
*.local
|
| 33 |
|
| 34 |
+
# BioMod model weights and their matching configuration are versioned on Hugging Face.
|
| 35 |
!weight/
|
| 36 |
weight/*
|
| 37 |
+
!weight/BioMod.pkl
|
| 38 |
+
!weight/BioMod.pth
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,7 +1,7 @@
|
|
| 1 |
-
# BioMod
|
| 2 |
|
| 3 |
Source code and tutorial notebooks for BioMod single-cell pretraining and
|
| 4 |
-
downstream experiments using
|
| 5 |
|
| 6 |
This source directory contains the pretraining entry point and shared training
|
| 7 |
utilities, alongside downstream task runners, tutorial notebooks, environment
|
|
@@ -29,67 +29,63 @@ from these repository trees.
|
|
| 29 |
```text
|
| 30 |
BioMod/ # Task code: training, evaluation, interpretability and tutorials
|
| 31 |
├── cell_annotation/ # Cell-type annotation and model fine-tuning
|
| 32 |
-
│ ├──
|
| 33 |
-
│ │ ├──
|
| 34 |
-
│ │ ├──
|
| 35 |
-
│ │ └──
|
| 36 |
│ ├── cmd_finetune.md
|
| 37 |
-
│ └──
|
| 38 |
├── drug_combination/ # Drug-combination synergy prediction
|
| 39 |
│ ├── README.md
|
| 40 |
-
│ └──
|
| 41 |
├── drug_response/ # Single-drug response prediction and training
|
| 42 |
│ ├── cmd.txt
|
| 43 |
-
│ └──
|
| 44 |
├── gene_perturbation/ # Perturbation response prediction: processing, training and inference
|
| 45 |
│ ├── data_utils_local.py
|
| 46 |
│ ├── gears_reduce_local.py
|
| 47 |
│ ├── inference_local.py
|
| 48 |
-
│ ├──
|
| 49 |
│ ├── pertdata_local.py
|
| 50 |
│ ├── README.md
|
| 51 |
│ ├── scfm_utils_local.py
|
| 52 |
-
│ ├──
|
| 53 |
│ └── utils.py
|
| 54 |
├── interpretability/ # Module representations and biological interpretability
|
| 55 |
│ ├── attention/ # Module crosstalk and within-module gene attention
|
| 56 |
│ │ ├── __init__.py
|
| 57 |
-
│ │ ├──
|
| 58 |
-
│ │ ├──
|
| 59 |
│ │ ├── README.md
|
| 60 |
│ │ └── README_gene_attention.md
|
| 61 |
-
│ ├──
|
| 62 |
│ │ ├── __init__.py
|
| 63 |
│ │ ├── cross_class_similarity.py
|
| 64 |
│ │ ├── family_ablation.py
|
| 65 |
│ │ ├── family_importance_analysis.py
|
| 66 |
│ │ ├── marker_gene_verification.py
|
| 67 |
│ │ ├── masking_ablation.py
|
| 68 |
-
│ │ ├──
|
| 69 |
-
│ │ ├──
|
| 70 |
│ │ ├── pub_utils.py
|
| 71 |
│ │ ├── random_baseline.py
|
| 72 |
│ │ ├── README.md
|
| 73 |
│ │ ├── RESULTS.md
|
| 74 |
│ │ ├── tcell_reactome_analysis.py
|
| 75 |
│ │ └── TODO.md
|
| 76 |
-
│ ├──
|
| 77 |
│ │ ├── __init__.py
|
| 78 |
│ │ ├── DESIGN.md
|
| 79 |
│ │ ├── fetch_reactome_descriptions.py
|
| 80 |
│ │ ├── MANUSCRIPT_SECTION.md
|
| 81 |
-
│ │ ├──
|
| 82 |
-
│ │ ├──
|
| 83 |
-
│ │ ├──
|
| 84 |
│ │ └── reactome_descriptions.json
|
| 85 |
│ ├── __init__.py
|
| 86 |
-
│ ├──
|
| 87 |
│ ├── README.md
|
| 88 |
│ └── utils.py
|
| 89 |
-
├── module_data_all/ # Module definitions, merging and uncovered-gene tools
|
| 90 |
-
│ ├── create_uncovered_modules.py
|
| 91 |
-
│ ├── merge_modules.py
|
| 92 |
-
│ └── merged_modules.json
|
| 93 |
├── tests/ # CPU tests for shared training utilities
|
| 94 |
│ └── test_training_utils.py
|
| 95 |
├── toturials/ # Jupyter tutorials for five downstream tasks
|
|
@@ -103,7 +99,7 @@ BioMod/ # Task code: training,
|
|
| 103 |
│ │ ├── cmd.txt
|
| 104 |
│ │ ├── make_low_depth_h5ad.py
|
| 105 |
│ │ └── run_zero_shot_low_depth.py
|
| 106 |
-
│ ├──
|
| 107 |
│ ├── README.md
|
| 108 |
│ └── visualize_results.py
|
| 109 |
├── .env.example
|
|
@@ -111,9 +107,9 @@ BioMod/ # Task code: training,
|
|
| 111 |
├── .gitignore
|
| 112 |
├── command.txt
|
| 113 |
├── environment.yml
|
| 114 |
-
├──
|
| 115 |
-
├──
|
| 116 |
-
├──
|
| 117 |
├── README.md
|
| 118 |
├── requirements.txt
|
| 119 |
└── training_utils.py
|
|
@@ -130,7 +126,7 @@ environment:
|
|
| 130 |
```bash
|
| 131 |
cd BioMod
|
| 132 |
conda env create -f environment.yml
|
| 133 |
-
conda activate
|
| 134 |
```
|
| 135 |
|
| 136 |
Or install Python packages into an existing CUDA/PyTorch environment:
|
|
@@ -143,7 +139,7 @@ From this source directory, activate the supplied environment and start Jupyter
|
|
| 143 |
with the tutorial directory as its working directory:
|
| 144 |
|
| 145 |
```bash
|
| 146 |
-
conda activate
|
| 147 |
cd toturials
|
| 148 |
jupyter lab
|
| 149 |
```
|
|
@@ -161,26 +157,26 @@ imports after installing the dependencies:
|
|
| 161 |
|
| 162 |
```bash
|
| 163 |
python -B -m unittest discover -s tests -p test_training_utils.py -v
|
| 164 |
-
python -B
|
| 165 |
```
|
| 166 |
|
| 167 |
The utility tests use synthetic tensors and temporary checkpoints; they do not
|
| 168 |
load the supplied pretrained weights. Passing these checks does not validate a
|
| 169 |
full training run. The pretraining entry point requires CUDA and an NCCL
|
| 170 |
process group. Supply `--data_path` explicitly: the pretraining corpus is not
|
| 171 |
-
bundled. Configure module inputs with `--
|
| 172 |
Generated checkpoints default to `tutorial_runs/pretraining/` under the outer
|
| 173 |
repository root, with periodic latest checkpoints under its `latest/` subdirectory.
|
| 174 |
|
| 175 |
## Shared Assets
|
| 176 |
|
| 177 |
The BioMod model weights and matching configuration are stored in
|
| 178 |
-
the outer repository's `weight/
|
| 179 |
together.
|
| 180 |
|
| 181 |
-
Tutorials expect a pretrained BioMod
|
| 182 |
gene assets. For the shell examples below, set `BIOMOD_ROOT` to the directory containing
|
| 183 |
-
`weight/
|
| 184 |
|
| 185 |
```bash
|
| 186 |
# Run this from the inner BioMod/ source directory.
|
|
@@ -190,45 +186,33 @@ export BIOMOD_ROOT="$(cd .. && pwd)"
|
|
| 190 |
Current local asset files:
|
| 191 |
|
| 192 |
```text
|
| 193 |
-
$BIOMOD_ROOT/weight/
|
| 194 |
-
├──
|
| 195 |
-
└──
|
| 196 |
|
| 197 |
$BIOMOD_ROOT/model_dependencies/ # Shared gene mappings, gene lists, embeddings and model components
|
| 198 |
-
├──
|
| 199 |
-
│ ├──
|
| 200 |
-
│ ├──
|
| 201 |
-
│ └──
|
| 202 |
-
├──
|
| 203 |
-
│ ├── performer_pytorch_cont/ # Nested package containing copies of the model component source files
|
| 204 |
-
│ │ ├── __init__.py
|
| 205 |
-
│ │ ├── ding_models.py
|
| 206 |
-
│ │ ├── performer_pytorch.py
|
| 207 |
-
│ │ ├── performer_pytorch_old.py
|
| 208 |
-
│ │ └── reversible.py
|
| 209 |
│ ├── __init__.py
|
| 210 |
│ ├── ding_models.py
|
| 211 |
-
│ ├──
|
| 212 |
-
│ ├──
|
| 213 |
│ └── reversible.py
|
| 214 |
├── gene2ens.csv
|
| 215 |
├── selected_gene2vec_27k.npy
|
| 216 |
└── selected_genes_27k.txt
|
| 217 |
```
|
| 218 |
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
```text
|
| 222 |
-
BioMod/module_data_all/ # Module definitions, merging and uncovered-gene tools
|
| 223 |
-
├── create_uncovered_modules.py
|
| 224 |
-
├── merge_modules.py
|
| 225 |
-
└── merged_modules.json
|
| 226 |
-
```
|
| 227 |
|
| 228 |
## BioMod model weights
|
| 229 |
|
| 230 |
-
Use `
|
| 231 |
-
`
|
| 232 |
configuration specifies a 2-layer encoder, a 27,874-gene axis, and 660 module
|
| 233 |
definitions. Keep this configuration paired with the weights so that task
|
| 234 |
runners can restore the corresponding architecture settings.
|
|
@@ -240,8 +224,8 @@ runners can restore the corresponding architecture settings.
|
|
| 240 |
| Cell-type annotation | `toturials/cell_annotation.ipynb` | task script under `cell_annotation/` | metrics, classification report, confusion matrix |
|
| 241 |
| Zero-shot batch integration | `toturials/zero_shot_batch_integration.ipynb` | `zero_shot_batch/` runner | UMAP PDF/PNG, avgBatch, avgBio |
|
| 242 |
| Gene perturbation | `toturials/gene_perturbation.ipynb` | `gene_perturbation/` runner | selected model, configuration, parameters, and training log |
|
| 243 |
-
| Drug response prediction | `toturials/drug_response_prediction.ipynb` | `drug_response/
|
| 244 |
-
| Drug-combination synergy | `toturials/drug_combination.ipynb` | `drug_combination/
|
| 245 |
|
| 246 |
Each notebook derives paths from `PROJECT_ROOT = Path('../..').resolve()` when
|
| 247 |
Jupyter runs from `BioMod/toturials/`. From the repository root, change this to
|
|
@@ -296,12 +280,12 @@ Run these commands from the inner `BioMod/` source directory after setting
|
|
| 296 |
`BIOMOD_ROOT` as shown above. Drug response prediction with the BioMod model weights:
|
| 297 |
|
| 298 |
```bash
|
| 299 |
-
python drug_response/
|
| 300 |
--gpu_id 0 \
|
| 301 |
--data_path "$BIOMOD_ROOT/data/downstream/final_data/single_drug_response/data" \
|
| 302 |
-
--checkpoint_path "$BIOMOD_ROOT/weight/
|
| 303 |
-
--hyper_params_path "$BIOMOD_ROOT/weight/
|
| 304 |
-
--
|
| 305 |
--gene_list_path "$BIOMOD_ROOT/model_dependencies/selected_genes_27k.txt" \
|
| 306 |
--gene_mapping_path "$BIOMOD_ROOT/model_dependencies/gene2ens.csv" \
|
| 307 |
--gene2vec_path "$BIOMOD_ROOT/model_dependencies/selected_gene2vec_27k.npy" \
|
|
@@ -315,10 +299,10 @@ python drug_response/train_drug_performer_module.py \
|
|
| 315 |
Drug-combination synergy prediction:
|
| 316 |
|
| 317 |
```bash
|
| 318 |
-
python drug_combination/
|
| 319 |
-
--checkpoint_path "$BIOMOD_ROOT/weight/
|
| 320 |
-
--hyper_params_path "$BIOMOD_ROOT/weight/
|
| 321 |
-
--
|
| 322 |
--gene_list_path "$BIOMOD_ROOT/model_dependencies/selected_genes_27k.txt" \
|
| 323 |
--gene_mapping_path "$BIOMOD_ROOT/model_dependencies/gene2ens.csv" \
|
| 324 |
--gene2vec_path "$BIOMOD_ROOT/model_dependencies/selected_gene2vec_27k.npy" \
|
|
@@ -376,7 +360,7 @@ files and shows which entries are missing.
|
|
| 376 |
|
| 377 |
Check `BIOMOD_ROOT` for shell commands or `PROJECT_ROOT` in a notebook, and verify that the BioMod model weights, matching
|
| 378 |
`.pkl` configuration, gene2vec file, gene list, gene mapping, and
|
| 379 |
-
`
|
| 380 |
|
| 381 |
### No space left on device
|
| 382 |
|
|
|
|
| 1 |
+
# BioMod
|
| 2 |
|
| 3 |
Source code and tutorial notebooks for BioMod single-cell pretraining and
|
| 4 |
+
downstream experiments using BioMod representations.
|
| 5 |
|
| 6 |
This source directory contains the pretraining entry point and shared training
|
| 7 |
utilities, alongside downstream task runners, tutorial notebooks, environment
|
|
|
|
| 29 |
```text
|
| 30 |
BioMod/ # Task code: training, evaluation, interpretability and tutorials
|
| 31 |
├── cell_annotation/ # Cell-type annotation and model fine-tuning
|
| 32 |
+
│ ├── biomod_data/ # Hallmark and Reactome modules with loading utilities
|
| 33 |
+
│ │ ├── load_biomod.py
|
| 34 |
+
│ │ ├── biomod1_hallmarks.json
|
| 35 |
+
│ │ └── biomod2_reactome_2022_filtered.json
|
| 36 |
│ ├── cmd_finetune.md
|
| 37 |
+
│ └── biomod_finetune.py
|
| 38 |
├── drug_combination/ # Drug-combination synergy prediction
|
| 39 |
│ ├── README.md
|
| 40 |
+
│ └── train_drug_combination_biomod.py
|
| 41 |
├── drug_response/ # Single-drug response prediction and training
|
| 42 |
│ ├── cmd.txt
|
| 43 |
+
│ └── train_drug_biomod.py
|
| 44 |
├── gene_perturbation/ # Perturbation response prediction: processing, training and inference
|
| 45 |
│ ├── data_utils_local.py
|
| 46 |
│ ├── gears_reduce_local.py
|
| 47 |
│ ├── inference_local.py
|
| 48 |
+
│ ├── model_biomod.py
|
| 49 |
│ ├── pertdata_local.py
|
| 50 |
│ ├── README.md
|
| 51 |
│ ├── scfm_utils_local.py
|
| 52 |
+
│ ├── train_biomod.py
|
| 53 |
│ └── utils.py
|
| 54 |
├── interpretability/ # Module representations and biological interpretability
|
| 55 |
│ ├── attention/ # Module crosstalk and within-module gene attention
|
| 56 |
│ │ ├── __init__.py
|
| 57 |
+
│ │ ├── biomod_crosstalk.py
|
| 58 |
+
│ │ ├── biomod_gene_attention.py
|
| 59 |
│ │ ├── README.md
|
| 60 |
│ │ └── README_gene_attention.md
|
| 61 |
+
│ ├── biomod_importance/ # Module importance, ablation studies and marker-gene validation
|
| 62 |
│ │ ├── __init__.py
|
| 63 |
│ │ ├── cross_class_similarity.py
|
| 64 |
│ │ ├── family_ablation.py
|
| 65 |
│ │ ├── family_importance_analysis.py
|
| 66 |
│ │ ├── marker_gene_verification.py
|
| 67 |
│ │ ├── masking_ablation.py
|
| 68 |
+
│ │ ├── biomod_importance.py
|
| 69 |
+
│ │ ├── biomod_importance_publication.py
|
| 70 |
│ │ ├── pub_utils.py
|
| 71 |
│ │ ├── random_baseline.py
|
| 72 |
│ │ ├── README.md
|
| 73 |
│ │ ├── RESULTS.md
|
| 74 |
│ │ ├── tcell_reactome_analysis.py
|
| 75 |
│ │ └── TODO.md
|
| 76 |
+
│ ├── biomod_text_alignment/ # Semantic alignment of module representations and pathway text
|
| 77 |
│ │ ├── __init__.py
|
| 78 |
│ │ ├── DESIGN.md
|
| 79 |
│ │ ├── fetch_reactome_descriptions.py
|
| 80 |
│ │ ├── MANUSCRIPT_SECTION.md
|
| 81 |
+
│ │ ├── biomod_text_alignment.py
|
| 82 |
+
│ │ ├── biomod_text_alignment_extended.py
|
| 83 |
+
│ │ ├── biomod_text_alignment_publication.py
|
| 84 |
│ │ └── reactome_descriptions.json
|
| 85 |
│ ├── __init__.py
|
| 86 |
+
│ ├── biomod_geometry.py
|
| 87 |
│ ├── README.md
|
| 88 |
│ └── utils.py
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
├── tests/ # CPU tests for shared training utilities
|
| 90 |
│ └── test_training_utils.py
|
| 91 |
├── toturials/ # Jupyter tutorials for five downstream tasks
|
|
|
|
| 99 |
│ │ ├── cmd.txt
|
| 100 |
│ │ ├── make_low_depth_h5ad.py
|
| 101 |
│ │ └── run_zero_shot_low_depth.py
|
| 102 |
+
│ ├── biomod_zero_shot.py
|
| 103 |
│ ├── README.md
|
| 104 |
│ └── visualize_results.py
|
| 105 |
├── .env.example
|
|
|
|
| 107 |
├── .gitignore
|
| 108 |
├── command.txt
|
| 109 |
├── environment.yml
|
| 110 |
+
├── models_biomod.py
|
| 111 |
+
├── biomod_dataset.py
|
| 112 |
+
├── biomod.py
|
| 113 |
├── README.md
|
| 114 |
├── requirements.txt
|
| 115 |
└── training_utils.py
|
|
|
|
| 126 |
```bash
|
| 127 |
cd BioMod
|
| 128 |
conda env create -f environment.yml
|
| 129 |
+
conda activate biomod
|
| 130 |
```
|
| 131 |
|
| 132 |
Or install Python packages into an existing CUDA/PyTorch environment:
|
|
|
|
| 139 |
with the tutorial directory as its working directory:
|
| 140 |
|
| 141 |
```bash
|
| 142 |
+
conda activate biomod
|
| 143 |
cd toturials
|
| 144 |
jupyter lab
|
| 145 |
```
|
|
|
|
| 157 |
|
| 158 |
```bash
|
| 159 |
python -B -m unittest discover -s tests -p test_training_utils.py -v
|
| 160 |
+
python -B biomod.py --help
|
| 161 |
```
|
| 162 |
|
| 163 |
The utility tests use synthetic tensors and temporary checkpoints; they do not
|
| 164 |
load the supplied pretrained weights. Passing these checks does not validate a
|
| 165 |
full training run. The pretraining entry point requires CUDA and an NCCL
|
| 166 |
process group. Supply `--data_path` explicitly: the pretraining corpus is not
|
| 167 |
+
bundled. Configure module inputs with `--biomod_data_dir` or `--biomod_data_path`.
|
| 168 |
Generated checkpoints default to `tutorial_runs/pretraining/` under the outer
|
| 169 |
repository root, with periodic latest checkpoints under its `latest/` subdirectory.
|
| 170 |
|
| 171 |
## Shared Assets
|
| 172 |
|
| 173 |
The BioMod model weights and matching configuration are stored in
|
| 174 |
+
the outer repository's `weight/`. Use the supplied `.pth` and `.pkl`
|
| 175 |
together.
|
| 176 |
|
| 177 |
+
Tutorials expect a pretrained BioMod checkpoint and shared
|
| 178 |
gene assets. For the shell examples below, set `BIOMOD_ROOT` to the directory containing
|
| 179 |
+
`weight/` and `model_dependencies/` (the outer `BioMod/` project directory).
|
| 180 |
|
| 181 |
```bash
|
| 182 |
# Run this from the inner BioMod/ source directory.
|
|
|
|
| 186 |
Current local asset files:
|
| 187 |
|
| 188 |
```text
|
| 189 |
+
$BIOMOD_ROOT/weight/ # BioMod model weights and matching configuration
|
| 190 |
+
├── BioMod.pkl
|
| 191 |
+
└── BioMod.pth
|
| 192 |
|
| 193 |
$BIOMOD_ROOT/model_dependencies/ # Shared gene mappings, gene lists, embeddings and model components
|
| 194 |
+
├── biomod_data_all/ # Shared module definitions and construction tools
|
| 195 |
+
│ ├── create_uncovered_biomods.py
|
| 196 |
+
│ ├── merge_biomods.py
|
| 197 |
+
│ └── merged_biomods.json
|
| 198 |
+
├── pytorch_cont/ # Attention and model components
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
│ ├── __init__.py
|
| 200 |
│ ├── ding_models.py
|
| 201 |
+
│ ├── pytorch.py
|
| 202 |
+
│ ├── pytorch_old.py
|
| 203 |
│ └── reversible.py
|
| 204 |
├── gene2ens.csv
|
| 205 |
├── selected_gene2vec_27k.npy
|
| 206 |
└── selected_genes_27k.txt
|
| 207 |
```
|
| 208 |
|
| 209 |
+
Module definitions and construction tools are shared through
|
| 210 |
+
`$BIOMOD_ROOT/model_dependencies/biomod_data_all/`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
|
| 212 |
## BioMod model weights
|
| 213 |
|
| 214 |
+
Use `BioMod.pth` with
|
| 215 |
+
`BioMod.pkl` from the shared-assets directory above. The supplied
|
| 216 |
configuration specifies a 2-layer encoder, a 27,874-gene axis, and 660 module
|
| 217 |
definitions. Keep this configuration paired with the weights so that task
|
| 218 |
runners can restore the corresponding architecture settings.
|
|
|
|
| 224 |
| Cell-type annotation | `toturials/cell_annotation.ipynb` | task script under `cell_annotation/` | metrics, classification report, confusion matrix |
|
| 225 |
| Zero-shot batch integration | `toturials/zero_shot_batch_integration.ipynb` | `zero_shot_batch/` runner | UMAP PDF/PNG, avgBatch, avgBio |
|
| 226 |
| Gene perturbation | `toturials/gene_perturbation.ipynb` | `gene_perturbation/` runner | selected model, configuration, parameters, and training log |
|
| 227 |
+
| Drug response prediction | `toturials/drug_response_prediction.ipynb` | `drug_response/train_drug_biomod.py` | scatter, residuals, ROC, per-drug and per-cancer tables |
|
| 228 |
+
| Drug-combination synergy | `toturials/drug_combination.ipynb` | `drug_combination/train_drug_combination_biomod.py` | label balance, loss, validation metrics, and ROC curves |
|
| 229 |
|
| 230 |
Each notebook derives paths from `PROJECT_ROOT = Path('../..').resolve()` when
|
| 231 |
Jupyter runs from `BioMod/toturials/`. From the repository root, change this to
|
|
|
|
| 280 |
`BIOMOD_ROOT` as shown above. Drug response prediction with the BioMod model weights:
|
| 281 |
|
| 282 |
```bash
|
| 283 |
+
python drug_response/train_drug_biomod.py \
|
| 284 |
--gpu_id 0 \
|
| 285 |
--data_path "$BIOMOD_ROOT/data/downstream/final_data/single_drug_response/data" \
|
| 286 |
+
--checkpoint_path "$BIOMOD_ROOT/weight/BioMod.pth" \
|
| 287 |
+
--hyper_params_path "$BIOMOD_ROOT/weight/BioMod.pkl" \
|
| 288 |
+
--biomod_data_dir "$BIOMOD_ROOT/model_dependencies/biomod_data_all" \
|
| 289 |
--gene_list_path "$BIOMOD_ROOT/model_dependencies/selected_genes_27k.txt" \
|
| 290 |
--gene_mapping_path "$BIOMOD_ROOT/model_dependencies/gene2ens.csv" \
|
| 291 |
--gene2vec_path "$BIOMOD_ROOT/model_dependencies/selected_gene2vec_27k.npy" \
|
|
|
|
| 299 |
Drug-combination synergy prediction:
|
| 300 |
|
| 301 |
```bash
|
| 302 |
+
python drug_combination/train_drug_combination_biomod.py \
|
| 303 |
+
--checkpoint_path "$BIOMOD_ROOT/weight/BioMod.pth" \
|
| 304 |
+
--hyper_params_path "$BIOMOD_ROOT/weight/BioMod.pkl" \
|
| 305 |
+
--biomod_data_dir "$BIOMOD_ROOT/model_dependencies/biomod_data_all" \
|
| 306 |
--gene_list_path "$BIOMOD_ROOT/model_dependencies/selected_genes_27k.txt" \
|
| 307 |
--gene_mapping_path "$BIOMOD_ROOT/model_dependencies/gene2ens.csv" \
|
| 308 |
--gene2vec_path "$BIOMOD_ROOT/model_dependencies/selected_gene2vec_27k.npy" \
|
|
|
|
| 360 |
|
| 361 |
Check `BIOMOD_ROOT` for shell commands or `PROJECT_ROOT` in a notebook, and verify that the BioMod model weights, matching
|
| 362 |
`.pkl` configuration, gene2vec file, gene list, gene mapping, and
|
| 363 |
+
`pytorch_cont/` directory exist at the paths listed under Shared Assets.
|
| 364 |
|
| 365 |
### No space left on device
|
| 366 |
|
|
@@ -2,14 +2,14 @@
|
|
| 2 |
|
| 3 |
"""
|
| 4 |
================================================================================
|
| 5 |
-
Pre-training script for
|
| 6 |
================================================================================
|
| 7 |
-
Refer to MAE's data input and masking methods, but use
|
| 8 |
Main features:
|
| 9 |
1. Use module data input (refer to MAE)
|
| 10 |
2. Mask the module (refer to MAE’s random_masking)
|
| 11 |
3. Use a configurable reconstruction decoder or direct prediction heads
|
| 12 |
-
4. Use
|
| 13 |
|
| 14 |
================================================================================
|
| 15 |
[Data flow example - taking default parameters as an example]
|
|
@@ -25,11 +25,11 @@
|
|
| 25 |
- 3 = batch_size
|
| 26 |
- 27874 = num_genes (gene expression value)
|
| 27 |
|
| 28 |
-
Step 3: Model forward propagation (see
|
| 29 |
├─> 3.1 Flat to Module: (3, 27874) → (3, num_modules, module_avg_size)
|
| 30 |
├─> 3.2 Module Embedding: → (3, num_modules, 1024)
|
| 31 |
├─> 3.3 Random Masking: mask out 75% of modules and keep 25%
|
| 32 |
-
├─> 3.4
|
| 33 |
├─> 3.5 Reconstruction: Predict the expression value of the masked module
|
| 34 |
└─> 3.6 Calculate Loss: Calculate MSE only on the masked module
|
| 35 |
|
|
@@ -63,11 +63,11 @@
|
|
| 63 |
ASSET_ROOT = PROJECT_ROOT / 'model_dependencies'
|
| 64 |
if str(SOURCE_DIR) not in sys.path:
|
| 65 |
sys.path.insert(0, str(SOURCE_DIR))
|
| 66 |
-
from
|
| 67 |
-
|
| 68 |
-
|
| 69 |
)
|
| 70 |
-
from
|
| 71 |
from training_utils import (
|
| 72 |
CosineAnnealingWarmupRestarts,
|
| 73 |
SequentialDistributedSampler,
|
|
@@ -125,7 +125,7 @@
|
|
| 125 |
parser.add_argument("--dec_heads", type=int, default=8, help='Number of decoder attention heads (valid when performer/cross_attn)')
|
| 126 |
parser.add_argument("--dec_dim_head", type=int, default=64, help='Performer decoder dim per head (ignored when cross_attn)')
|
| 127 |
parser.add_argument("--ckpt_dir", type=str, default=str(PROJECT_ROOT / 'tutorial_runs' / 'pretraining'), help='Directory for generated checkpoints; user relative paths use the current working directory.')
|
| 128 |
-
parser.add_argument("--model_name", type=str, default='
|
| 129 |
parser.add_argument("--data_path", type=str, required=True,
|
| 130 |
help='Pretraining input: a directory for npz/txt or a file for h5/h5ad. Supply your own pretraining data; it is not bundled.')
|
| 131 |
parser.add_argument("--data_format", type=str, default="npz", choices=["npz", "h5", "h5ad", "txt"],
|
|
@@ -133,8 +133,8 @@
|
|
| 133 |
parser.add_argument("--h5_dataset_key", type=str, default="expression",
|
| 134 |
help='The data set key name of the expression matrix in h5 format. The default is to read h5[\"expression\"]')
|
| 135 |
# Module related parameters
|
| 136 |
-
parser.add_argument("--module_data_path", type=str, default=None, help='Path to module JSON file (single file).')
|
| 137 |
-
parser.add_argument("--module_data_dir", type=str, default=None, help='Path to directory containing module JSON files.')
|
| 138 |
parser.add_argument("--gene_mapping_path", type=str, default=str(ASSET_ROOT / 'gene2ens.csv'), help='Path to gene symbol to Ensembl ID mapping CSV.')
|
| 139 |
parser.add_argument("--gene2vec_data_path", type=str, default=str(ASSET_ROOT / 'selected_gene2vec_27k.npy'))
|
| 140 |
parser.add_argument("--fusion_method", type=str, default="multiply",
|
|
@@ -167,8 +167,8 @@
|
|
| 167 |
parser.add_argument("--load_opt_and_sched", action='store_true')
|
| 168 |
parser.add_argument("--lr_step_within_epoch", action='store_true',
|
| 169 |
help='Update the learning rate according to the optimizer update step; the cycle length is updates_per_epoch (gradually decreasing in each epoch)')
|
| 170 |
-
parser.add_argument("--shuffle_module_genes", action='store_true',
|
| 171 |
-
help='Whether to disrupt the gene allocation within the module (used for comparative experiments to verify the effectiveness of module division)')
|
| 172 |
parser.add_argument("--shuffle_seed", type=int, default=2021,
|
| 173 |
help='Random seed used when scrambling module genes (downstream tasks need to use the same value)')
|
| 174 |
# Gene2vec ablation experiment parameters
|
|
@@ -278,7 +278,7 @@ def restore_model_args_from_config(args, config):
|
|
| 278 |
args.model_config_path,
|
| 279 |
)
|
| 280 |
with open(resolved_model_config_path, "rb") as handle:
|
| 281 |
-
resume_model_config =
|
| 282 |
pickle.load(handle),
|
| 283 |
source=resolved_model_config_path,
|
| 284 |
log_fn=print,
|
|
@@ -325,11 +325,11 @@ def restore_model_args_from_config(args, config):
|
|
| 325 |
|
| 326 |
# WandB resume logic
|
| 327 |
wandb_init_kwargs = {
|
| 328 |
-
"project": "
|
| 329 |
"name": args.model_name,
|
| 330 |
"entity": "icml_2023_mlb",
|
| 331 |
"config": {
|
| 332 |
-
"architecture": "
|
| 333 |
"gene_num": args.gene_num,
|
| 334 |
"cell_num": args.cell_num,
|
| 335 |
"load model": args.model_path,
|
|
@@ -490,7 +490,7 @@ def resolve_total_cells_for_split():
|
|
| 490 |
if is_master:
|
| 491 |
print(f"Using {len(module_sets)} modules restored from model config PKL")
|
| 492 |
else:
|
| 493 |
-
module_sets =
|
| 494 |
module_data_path=args.module_data_path,
|
| 495 |
module_data_dir=args.module_data_dir,
|
| 496 |
gene_list_path=args.gene_list_path,
|
|
@@ -616,14 +616,14 @@ def resolve_total_cells_for_split():
|
|
| 616 |
)
|
| 617 |
|
| 618 |
# Step 3.3: Instantiate the model
|
| 619 |
-
#
|
| 620 |
# Input: (batch_size, num_genes) = (3, 27874)
|
| 621 |
# ├─> Module division: (batch_size, num_modules, module_avg_size) ≈ (3, 500, 55)
|
| 622 |
# ├─> Module Embedding: (batch_size, num_modules, base_dim) = (3, 500, 1024)
|
| 623 |
# ├─> Random Masking (75%): Keep about 125 visible modules
|
| 624 |
-
# ├─>
|
| 625 |
# └─> Reconstruction prediction: Output the reconstruction value of the masked module
|
| 626 |
-
model =
|
| 627 |
|
| 628 |
def configure_trainable_modules(model_obj):
|
| 629 |
for param in model_obj.parameters():
|
|
@@ -721,8 +721,8 @@ def save_initialized_checkpoint_and_exit(model_obj):
|
|
| 721 |
# Step 2.1/2.2 has been completed previously
|
| 722 |
|
| 723 |
# Step 2.3: Create training and validation datasets
|
| 724 |
-
#
|
| 725 |
-
train_dataset =
|
| 726 |
data_path=args.data_path,
|
| 727 |
module_sets=module_sets, # Pass module_sets to maintain compatibility, but no longer used in __getitem__
|
| 728 |
data=None,
|
|
@@ -735,7 +735,7 @@ def save_initialized_checkpoint_and_exit(model_obj):
|
|
| 735 |
cell_num=None,
|
| 736 |
verbose=is_master
|
| 737 |
)
|
| 738 |
-
val_dataset =
|
| 739 |
data_path=args.data_path,
|
| 740 |
module_sets=module_sets,
|
| 741 |
data=None,
|
|
@@ -880,7 +880,7 @@ def any_rank_true(local_flag: bool) -> bool:
|
|
| 880 |
# 1. Flat to Module: (3, 27874) -> (3, num_modules, module_avg_size)
|
| 881 |
# 2. Module Embedding: -> (3, num_modules, 1024)
|
| 882 |
# 3. Random Masking: mask out 75% of modules
|
| 883 |
-
# 4.
|
| 884 |
# 5. Reconstruction: Predict the masked module
|
| 885 |
# Output: loss=MSE on masked module, preds=reconstructed value, mask=mask mark
|
| 886 |
loss, preds, mask = model(data)
|
|
|
|
| 2 |
|
| 3 |
"""
|
| 4 |
================================================================================
|
| 5 |
+
Pre-training script for encoder-only BioMod model
|
| 6 |
================================================================================
|
| 7 |
+
Refer to MAE's data input and masking methods, but use the BioMod attention encoder
|
| 8 |
Main features:
|
| 9 |
1. Use module data input (refer to MAE)
|
| 10 |
2. Mask the module (refer to MAE’s random_masking)
|
| 11 |
3. Use a configurable reconstruction decoder or direct prediction heads
|
| 12 |
+
4. Use the BioMod attention encoder
|
| 13 |
|
| 14 |
================================================================================
|
| 15 |
[Data flow example - taking default parameters as an example]
|
|
|
|
| 25 |
- 3 = batch_size
|
| 26 |
- 27874 = num_genes (gene expression value)
|
| 27 |
|
| 28 |
+
Step 3: Model forward propagation (see models_biomod.py for details)
|
| 29 |
├─> 3.1 Flat to Module: (3, 27874) → (3, num_modules, module_avg_size)
|
| 30 |
├─> 3.2 Module Embedding: → (3, num_modules, 1024)
|
| 31 |
├─> 3.3 Random Masking: mask out 75% of modules and keep 25%
|
| 32 |
+
├─> 3.4 BioMod encoder: Process visible modules
|
| 33 |
├─> 3.5 Reconstruction: Predict the expression value of the masked module
|
| 34 |
└─> 3.6 Calculate Loss: Calculate MSE only on the masked module
|
| 35 |
|
|
|
|
| 63 |
ASSET_ROOT = PROJECT_ROOT / 'model_dependencies'
|
| 64 |
if str(SOURCE_DIR) not in sys.path:
|
| 65 |
sys.path.insert(0, str(SOURCE_DIR))
|
| 66 |
+
from models_biomod import (
|
| 67 |
+
BioMod,
|
| 68 |
+
sanitize_biomod_config,
|
| 69 |
)
|
| 70 |
+
from biomod_dataset import BioModSCDataset, load_biomod_data
|
| 71 |
from training_utils import (
|
| 72 |
CosineAnnealingWarmupRestarts,
|
| 73 |
SequentialDistributedSampler,
|
|
|
|
| 125 |
parser.add_argument("--dec_heads", type=int, default=8, help='Number of decoder attention heads (valid when performer/cross_attn)')
|
| 126 |
parser.add_argument("--dec_dim_head", type=int, default=64, help='Performer decoder dim per head (ignored when cross_attn)')
|
| 127 |
parser.add_argument("--ckpt_dir", type=str, default=str(PROJECT_ROOT / 'tutorial_runs' / 'pretraining'), help='Directory for generated checkpoints; user relative paths use the current working directory.')
|
| 128 |
+
parser.add_argument("--model_name", type=str, default='biomod_27k', help='Pretrained model name.')
|
| 129 |
parser.add_argument("--data_path", type=str, required=True,
|
| 130 |
help='Pretraining input: a directory for npz/txt or a file for h5/h5ad. Supply your own pretraining data; it is not bundled.')
|
| 131 |
parser.add_argument("--data_format", type=str, default="npz", choices=["npz", "h5", "h5ad", "txt"],
|
|
|
|
| 133 |
parser.add_argument("--h5_dataset_key", type=str, default="expression",
|
| 134 |
help='The data set key name of the expression matrix in h5 format. The default is to read h5[\"expression\"]')
|
| 135 |
# Module related parameters
|
| 136 |
+
parser.add_argument("--biomod_data_path", "--module_data_path", type=str, default=None, help='Path to module JSON file (single file).', dest='module_data_path')
|
| 137 |
+
parser.add_argument("--biomod_data_dir", "--module_data_dir", type=str, default=None, help='Path to directory containing module JSON files.', dest='module_data_dir')
|
| 138 |
parser.add_argument("--gene_mapping_path", type=str, default=str(ASSET_ROOT / 'gene2ens.csv'), help='Path to gene symbol to Ensembl ID mapping CSV.')
|
| 139 |
parser.add_argument("--gene2vec_data_path", type=str, default=str(ASSET_ROOT / 'selected_gene2vec_27k.npy'))
|
| 140 |
parser.add_argument("--fusion_method", type=str, default="multiply",
|
|
|
|
| 167 |
parser.add_argument("--load_opt_and_sched", action='store_true')
|
| 168 |
parser.add_argument("--lr_step_within_epoch", action='store_true',
|
| 169 |
help='Update the learning rate according to the optimizer update step; the cycle length is updates_per_epoch (gradually decreasing in each epoch)')
|
| 170 |
+
parser.add_argument("--shuffle_biomod_genes", "--shuffle_module_genes", action='store_true',
|
| 171 |
+
help='Whether to disrupt the gene allocation within the module (used for comparative experiments to verify the effectiveness of module division)', dest='shuffle_module_genes')
|
| 172 |
parser.add_argument("--shuffle_seed", type=int, default=2021,
|
| 173 |
help='Random seed used when scrambling module genes (downstream tasks need to use the same value)')
|
| 174 |
# Gene2vec ablation experiment parameters
|
|
|
|
| 278 |
args.model_config_path,
|
| 279 |
)
|
| 280 |
with open(resolved_model_config_path, "rb") as handle:
|
| 281 |
+
resume_model_config = sanitize_biomod_config(
|
| 282 |
pickle.load(handle),
|
| 283 |
source=resolved_model_config_path,
|
| 284 |
log_fn=print,
|
|
|
|
| 325 |
|
| 326 |
# WandB resume logic
|
| 327 |
wandb_init_kwargs = {
|
| 328 |
+
"project": "pretrain_biomod",
|
| 329 |
"name": args.model_name,
|
| 330 |
"entity": "icml_2023_mlb",
|
| 331 |
"config": {
|
| 332 |
+
"architecture": "biomod",
|
| 333 |
"gene_num": args.gene_num,
|
| 334 |
"cell_num": args.cell_num,
|
| 335 |
"load model": args.model_path,
|
|
|
|
| 490 |
if is_master:
|
| 491 |
print(f"Using {len(module_sets)} modules restored from model config PKL")
|
| 492 |
else:
|
| 493 |
+
module_sets = load_biomod_data(
|
| 494 |
module_data_path=args.module_data_path,
|
| 495 |
module_data_dir=args.module_data_dir,
|
| 496 |
gene_list_path=args.gene_list_path,
|
|
|
|
| 616 |
)
|
| 617 |
|
| 618 |
# Step 3.3: Instantiate the model
|
| 619 |
+
# BioMod model process:
|
| 620 |
# Input: (batch_size, num_genes) = (3, 27874)
|
| 621 |
# ├─> Module division: (batch_size, num_modules, module_avg_size) ≈ (3, 500, 55)
|
| 622 |
# ├─> Module Embedding: (batch_size, num_modules, base_dim) = (3, 500, 1024)
|
| 623 |
# ├─> Random Masking (75%): Keep about 125 visible modules
|
| 624 |
+
# ├─> BioMod encoder: Process visible modules
|
| 625 |
# └─> Reconstruction prediction: Output the reconstruction value of the masked module
|
| 626 |
+
model = BioMod(**model_params)
|
| 627 |
|
| 628 |
def configure_trainable_modules(model_obj):
|
| 629 |
for param in model_obj.parameters():
|
|
|
|
| 721 |
# Step 2.1/2.2 has been completed previously
|
| 722 |
|
| 723 |
# Step 2.3: Create training and validation datasets
|
| 724 |
+
# BioModSCDataset directly returns the gene expression value, and the module conversion is completed inside the model.
|
| 725 |
+
train_dataset = BioModSCDataset(
|
| 726 |
data_path=args.data_path,
|
| 727 |
module_sets=module_sets, # Pass module_sets to maintain compatibility, but no longer used in __getitem__
|
| 728 |
data=None,
|
|
|
|
| 735 |
cell_num=None,
|
| 736 |
verbose=is_master
|
| 737 |
)
|
| 738 |
+
val_dataset = BioModSCDataset(
|
| 739 |
data_path=args.data_path,
|
| 740 |
module_sets=module_sets,
|
| 741 |
data=None,
|
|
|
|
| 880 |
# 1. Flat to Module: (3, 27874) -> (3, num_modules, module_avg_size)
|
| 881 |
# 2. Module Embedding: -> (3, num_modules, 1024)
|
| 882 |
# 3. Random Masking: mask out 75% of modules
|
| 883 |
+
# 4. BioMod encoder: Process visible modules
|
| 884 |
# 5. Reconstruction: Predict the masked module
|
| 885 |
# Output: loss=MSE on masked module, preds=reconstructed value, mask=mask mark
|
| 886 |
loss, preds, mask = model(data)
|
|
@@ -25,7 +25,7 @@
|
|
| 25 |
h5py = None
|
| 26 |
|
| 27 |
|
| 28 |
-
def
|
| 29 |
module_data_path: Optional[str] = None,
|
| 30 |
module_data_dir: Optional[str] = None,
|
| 31 |
gene_list_path: Optional[str] = None,
|
|
@@ -219,7 +219,7 @@ def _convert_genes_to_indices(genes: list) -> list[int]:
|
|
| 219 |
elif isinstance(data, list):
|
| 220 |
modules = data
|
| 221 |
else:
|
| 222 |
-
raise ValueError("
|
| 223 |
|
| 224 |
module_sets = []
|
| 225 |
for p in modules:
|
|
@@ -246,13 +246,13 @@ def _convert_genes_to_indices(genes: list) -> list[int]:
|
|
| 246 |
raise ValueError("module_data_path or module_data_dir must be provided")
|
| 247 |
|
| 248 |
|
| 249 |
-
class
|
| 250 |
"""
|
| 251 |
single cell data set
|
| 252 |
Supports .npz (sparse CSR), .h5ad and .txt formats
|
| 253 |
|
| 254 |
Note: This class no longer performs module conversion (redundant _flat_to_modules removed)
|
| 255 |
-
Module conversion is completed by the
|
| 256 |
"""
|
| 257 |
|
| 258 |
def __init__(
|
|
|
|
| 25 |
h5py = None
|
| 26 |
|
| 27 |
|
| 28 |
+
def load_biomod_data(
|
| 29 |
module_data_path: Optional[str] = None,
|
| 30 |
module_data_dir: Optional[str] = None,
|
| 31 |
gene_list_path: Optional[str] = None,
|
|
|
|
| 219 |
elif isinstance(data, list):
|
| 220 |
modules = data
|
| 221 |
else:
|
| 222 |
+
raise ValueError("biomod_data format is incorrect")
|
| 223 |
|
| 224 |
module_sets = []
|
| 225 |
for p in modules:
|
|
|
|
| 246 |
raise ValueError("module_data_path or module_data_dir must be provided")
|
| 247 |
|
| 248 |
|
| 249 |
+
class BioModSCDataset(Dataset):
|
| 250 |
"""
|
| 251 |
single cell data set
|
| 252 |
Supports .npz (sparse CSR), .h5ad and .txt formats
|
| 253 |
|
| 254 |
Note: This class no longer performs module conversion (redundant _flat_to_modules removed)
|
| 255 |
+
Module conversion is completed by the BioModEmbedding layer inside the model to avoid repeated calculations
|
| 256 |
"""
|
| 257 |
|
| 258 |
def __init__(
|
|
File without changes
|
|
File without changes
|
|
@@ -29,8 +29,8 @@ def load_json_files(file1_path, file2_path):
|
|
| 29 |
|
| 30 |
|
| 31 |
# hallmark_data, reactome_data = load_json_files(
|
| 32 |
-
# '
|
| 33 |
-
# '
|
| 34 |
# )
|
| 35 |
# len(hallmark_data.keys())
|
| 36 |
# len(reactome_data.keys())
|
|
|
|
| 29 |
|
| 30 |
|
| 31 |
# hallmark_data, reactome_data = load_json_files(
|
| 32 |
+
# 'biomod1_hallmarks.json',
|
| 33 |
+
# 'biomod2_reactome_2022_filtered.json'
|
| 34 |
# )
|
| 35 |
# len(hallmark_data.keys())
|
| 36 |
# len(reactome_data.keys())
|
|
@@ -2,12 +2,12 @@
|
|
| 2 |
# -*- coding: utf-8 -*-
|
| 3 |
|
| 4 |
"""
|
| 5 |
-
End-to-end cell annotation fine-tuning for
|
| 6 |
|
| 7 |
This script is intentionally independent from the legacy two-stage pipeline:
|
| 8 |
1. No feature extraction to .npy files.
|
| 9 |
-
2. No imports from other scripts in
|
| 10 |
-
3. Reuses the pretrained
|
| 11 |
|
| 12 |
Supported cell embedding heads:
|
| 13 |
- module_mean: mean over restored module tokens
|
|
@@ -48,26 +48,26 @@
|
|
| 48 |
from torch.utils.data.distributed import DistributedSampler
|
| 49 |
|
| 50 |
SCRIPT_DIR = Path(__file__).resolve().parent
|
| 51 |
-
|
| 52 |
-
SCLONG_ROOT =
|
| 53 |
|
| 54 |
REPO_ROOT = Path(__file__).resolve().parents[2]
|
| 55 |
ASSET_DIR = REPO_ROOT / "model_dependencies"
|
| 56 |
DOWNSTREAM_DATA_DIR = REPO_ROOT / "data" / "downstream" / "final_data"
|
| 57 |
-
WEIGHTS_DIR = REPO_ROOT / "weight"
|
| 58 |
|
| 59 |
-
if str(
|
| 60 |
-
sys.path.insert(0, str(
|
| 61 |
if str(SCLONG_ROOT) not in sys.path:
|
| 62 |
sys.path.insert(0, str(SCLONG_ROOT))
|
| 63 |
|
| 64 |
-
from
|
| 65 |
-
|
| 66 |
-
|
| 67 |
)
|
| 68 |
|
| 69 |
if TYPE_CHECKING:
|
| 70 |
-
from
|
| 71 |
|
| 72 |
|
| 73 |
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
|
@@ -373,7 +373,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
| 373 |
return self.out_layer(x)
|
| 374 |
|
| 375 |
|
| 376 |
-
class
|
| 377 |
"""Per-module scorer: soft weighting during training, hard threshold during inference."""
|
| 378 |
|
| 379 |
def __init__(self, embed_dim: int):
|
|
@@ -395,10 +395,10 @@ def forward(
|
|
| 395 |
return x, scores
|
| 396 |
|
| 397 |
|
| 398 |
-
class
|
| 399 |
def __init__(
|
| 400 |
self,
|
| 401 |
-
backbone: "
|
| 402 |
module_sets: List[List[int]],
|
| 403 |
num_classes: int,
|
| 404 |
head_type: str,
|
|
@@ -430,13 +430,13 @@ def __init__(
|
|
| 430 |
self.use_module_selector = use_module_selector
|
| 431 |
self.selector_tau = 0.5
|
| 432 |
if use_module_selector:
|
| 433 |
-
self.module_selector =
|
| 434 |
else:
|
| 435 |
self.module_selector = None
|
| 436 |
self._selector_scores = None
|
| 437 |
|
| 438 |
if self.head_type == "gene_mean":
|
| 439 |
-
module_idx, gene_idx, gene_counts = self.
|
| 440 |
self.register_buffer("agg_module_idx", module_idx)
|
| 441 |
self.register_buffer("agg_gene_idx", gene_idx)
|
| 442 |
self.register_buffer("agg_gene_counts", gene_counts)
|
|
@@ -454,12 +454,12 @@ def __init__(
|
|
| 454 |
def load_state_dict(self, state_dict, strict: bool = True, assign: bool = False):
|
| 455 |
"""Accept current module names and older serialized backbone names."""
|
| 456 |
return super().load_state_dict(
|
| 457 |
-
|
| 458 |
strict=strict,
|
| 459 |
assign=assign,
|
| 460 |
)
|
| 461 |
|
| 462 |
-
def
|
| 463 |
module_indices = []
|
| 464 |
gene_indices = []
|
| 465 |
gene_counts = torch.zeros(self.num_genes, dtype=torch.long)
|
|
@@ -475,7 +475,7 @@ def _build_module_gene_index(self) -> Tuple[torch.Tensor, torch.Tensor, torch.Te
|
|
| 475 |
gene_counts,
|
| 476 |
)
|
| 477 |
|
| 478 |
-
def
|
| 479 |
if self.use_cls:
|
| 480 |
module_tokens = latent[:, 1:, :]
|
| 481 |
else:
|
|
@@ -483,7 +483,7 @@ def _restore_module_order(self, latent: torch.Tensor, ids_restore: torch.Tensor)
|
|
| 483 |
gather_index = ids_restore.unsqueeze(-1).expand(-1, -1, module_tokens.shape[-1])
|
| 484 |
return torch.gather(module_tokens, dim=1, index=gather_index)
|
| 485 |
|
| 486 |
-
def
|
| 487 |
batch_size, _, dim = module_tokens.shape
|
| 488 |
gene_feats = torch.zeros(
|
| 489 |
batch_size,
|
|
@@ -519,7 +519,7 @@ def forward(self, x_model: torch.Tensor) -> torch.Tensor:
|
|
| 519 |
if self.head_type == "cls":
|
| 520 |
cell_emb = latent[:, 0, :]
|
| 521 |
else:
|
| 522 |
-
module_tokens = self.
|
| 523 |
if self.module_selector is not None:
|
| 524 |
module_tokens, self._selector_scores = self.module_selector(
|
| 525 |
module_tokens, tau=self.selector_tau, hard=not self.training,
|
|
@@ -531,7 +531,7 @@ def forward(self, x_model: torch.Tensor) -> torch.Tensor:
|
|
| 531 |
elif self.head_type == "attn_pool":
|
| 532 |
cell_emb, _ = self.pooler(module_tokens)
|
| 533 |
elif self.head_type == "gene_mean":
|
| 534 |
-
gene_tokens = self.
|
| 535 |
cell_emb = gene_tokens.mean(dim=1)
|
| 536 |
else:
|
| 537 |
raise ValueError(f"Unsupported head_type: {self.head_type}")
|
|
@@ -540,13 +540,13 @@ def forward(self, x_model: torch.Tensor) -> torch.Tensor:
|
|
| 540 |
return self.classifier(cell_emb)
|
| 541 |
|
| 542 |
|
| 543 |
-
def get_encoder_blocks(backbone: "
|
| 544 |
net = backbone.encoder.net
|
| 545 |
if hasattr(net, "layers"):
|
| 546 |
return list(net.layers)
|
| 547 |
if hasattr(net, "blocks"):
|
| 548 |
return list(net.blocks)
|
| 549 |
-
raise AttributeError("Cannot find encoder blocks on
|
| 550 |
|
| 551 |
|
| 552 |
def set_requires_grad(module: nn.Module, flag: bool) -> None:
|
|
@@ -555,7 +555,7 @@ def set_requires_grad(module: nn.Module, flag: bool) -> None:
|
|
| 555 |
|
| 556 |
|
| 557 |
def configure_backbone_trainability(
|
| 558 |
-
backbone: "
|
| 559 |
finetune_mode: str,
|
| 560 |
unfreeze_last_n: int,
|
| 561 |
train_pos_emb: bool,
|
|
@@ -604,7 +604,7 @@ def configure_backbone_trainability(
|
|
| 604 |
|
| 605 |
|
| 606 |
def build_optimizer(
|
| 607 |
-
model:
|
| 608 |
lr_head: float,
|
| 609 |
lr_backbone: float,
|
| 610 |
weight_decay: float,
|
|
@@ -669,20 +669,20 @@ def resolve_batch_sizes(args, world_size: int) -> Tuple[int, int, int]:
|
|
| 669 |
return train_batch_size, eval_batch_size, effective_global_batch
|
| 670 |
|
| 671 |
|
| 672 |
-
def infer_model_config(args, checkpoint: dict, pkl_config: Optional[dict],
|
| 673 |
-
from
|
| 674 |
|
| 675 |
if pkl_config is not None and isinstance(pkl_config, dict):
|
| 676 |
-
config =
|
| 677 |
pkl_config,
|
| 678 |
source=str(args.config_pkl),
|
| 679 |
log_fn=logger.info,
|
| 680 |
)
|
| 681 |
logger.info("Using config from PKL: %s", args.config_pkl)
|
| 682 |
if "module_sets" in config:
|
| 683 |
-
|
| 684 |
elif isinstance(checkpoint, dict) and "config" in checkpoint:
|
| 685 |
-
config =
|
| 686 |
checkpoint["config"],
|
| 687 |
source=str(resolve_path(args.performer_checkpoint)),
|
| 688 |
log_fn=logger.info,
|
|
@@ -695,7 +695,7 @@ def infer_model_config(args, checkpoint: dict, pkl_config: Optional[dict], modul
|
|
| 695 |
)
|
| 696 |
|
| 697 |
return {
|
| 698 |
-
"module_sets":
|
| 699 |
"num_genes": config["num_genes"],
|
| 700 |
"base_dim": config["base_dim"],
|
| 701 |
"enc_depth": config["enc_depth"],
|
|
@@ -722,9 +722,9 @@ def infer_model_config(args, checkpoint: dict, pkl_config: Optional[dict], modul
|
|
| 722 |
}
|
| 723 |
|
| 724 |
|
| 725 |
-
def
|
| 726 |
-
"""Read
|
| 727 |
-
merged_path = Path(module_data_dir) / "
|
| 728 |
with open(merged_path) as f:
|
| 729 |
modules = json.load(f)
|
| 730 |
PREFIXES = [
|
|
@@ -744,32 +744,32 @@ def compute_module_family_ids(module_data_dir: str) -> List[int]:
|
|
| 744 |
return family_ids
|
| 745 |
|
| 746 |
|
| 747 |
-
def load_backbone(args, device: torch.device) -> Tuple["
|
| 748 |
-
from
|
| 749 |
-
from
|
| 750 |
|
| 751 |
config_pkl = None
|
| 752 |
config_path = resolve_path(args.config_pkl)
|
| 753 |
if config_path.exists():
|
| 754 |
with open(config_path, "rb") as handle:
|
| 755 |
-
config_pkl =
|
| 756 |
else:
|
| 757 |
logger.warning("Config PKL not found at %s; fallback to checkpoint config", config_path)
|
| 758 |
|
| 759 |
ckpt_path = resolve_path(args.performer_checkpoint)
|
| 760 |
checkpoint = torch.load(ckpt_path, map_location=device)
|
| 761 |
|
| 762 |
-
|
| 763 |
if isinstance(config_pkl, dict) and "module_sets" in config_pkl:
|
| 764 |
-
|
| 765 |
-
logger.info("Using module_sets from PKL (%d modules)", len(
|
| 766 |
elif isinstance(checkpoint, dict) and isinstance(checkpoint.get("config"), dict) and "module_sets" in checkpoint["config"]:
|
| 767 |
-
|
| 768 |
-
logger.info("Using module_sets from checkpoint config (%d modules)", len(
|
| 769 |
else:
|
| 770 |
if not args.module_data_dir:
|
| 771 |
raise ValueError("module_data_dir is required when module_sets are not stored in the PKL/checkpoint config")
|
| 772 |
-
|
| 773 |
module_data_dir=str(resolve_path(args.module_data_dir)),
|
| 774 |
gene_list_path=str(resolve_path(args.gene_list_path)),
|
| 775 |
gene_mapping_path=str(resolve_path(args.gene_mapping_path)),
|
|
@@ -779,18 +779,18 @@ def load_backbone(args, device: torch.device) -> Tuple["PerformerEncoderOnly_Mod
|
|
| 779 |
if args.shuffle_module_genes:
|
| 780 |
logger.info("Shuffling genes across modules with seed=%d", args.shuffle_seed)
|
| 781 |
rng = random.Random(args.shuffle_seed)
|
| 782 |
-
module_sizes = [len(module) for module in
|
| 783 |
-
all_genes = [gene for module in
|
| 784 |
rng.shuffle(all_genes)
|
| 785 |
shuffled = []
|
| 786 |
start = 0
|
| 787 |
for size in module_sizes:
|
| 788 |
shuffled.append(all_genes[start:start + size])
|
| 789 |
start += size
|
| 790 |
-
|
| 791 |
|
| 792 |
-
model_config = infer_model_config(args, checkpoint, config_pkl,
|
| 793 |
-
|
| 794 |
|
| 795 |
if "model_state_dict" in checkpoint:
|
| 796 |
state_dict = checkpoint["model_state_dict"]
|
|
@@ -807,7 +807,7 @@ def load_backbone(args, device: torch.device) -> Tuple["PerformerEncoderOnly_Mod
|
|
| 807 |
model_config["base_dim"],
|
| 808 |
model_config["enc_depth"],
|
| 809 |
model_config["enc_heads"],
|
| 810 |
-
len(
|
| 811 |
use_cls_token,
|
| 812 |
)
|
| 813 |
|
|
@@ -815,13 +815,13 @@ def load_backbone(args, device: torch.device) -> Tuple["PerformerEncoderOnly_Mod
|
|
| 815 |
if getattr(args, "use_family_emb", False):
|
| 816 |
pdd = getattr(args, "module_data_dir", None)
|
| 817 |
if pdd:
|
| 818 |
-
module_family_ids =
|
| 819 |
logger.info("Family embedding enabled: %d module family IDs computed", len(module_family_ids))
|
| 820 |
|
| 821 |
-
backbone =
|
| 822 |
-
module_sets=
|
| 823 |
num_genes=model_config["num_genes"],
|
| 824 |
-
max_seq_len=len(
|
| 825 |
base_dim=model_config["base_dim"],
|
| 826 |
enc_depth=model_config["enc_depth"],
|
| 827 |
enc_heads=model_config["enc_heads"],
|
|
@@ -866,7 +866,7 @@ def load_backbone(args, device: torch.device) -> Tuple["PerformerEncoderOnly_Mod
|
|
| 866 |
logger.warning("Unexpected keys: %s", unexpected_keys[:10])
|
| 867 |
|
| 868 |
backbone.to(device)
|
| 869 |
-
return backbone,
|
| 870 |
|
| 871 |
|
| 872 |
def stratified_split(
|
|
@@ -908,7 +908,7 @@ def stratified_train_val_split(
|
|
| 908 |
|
| 909 |
@torch.no_grad()
|
| 910 |
def evaluate(
|
| 911 |
-
model:
|
| 912 |
data_loader: DataLoader,
|
| 913 |
preprocessor: DataPreprocessor,
|
| 914 |
reindex_state: dict,
|
|
@@ -948,7 +948,7 @@ def evaluate(
|
|
| 948 |
|
| 949 |
|
| 950 |
def train_one_epoch(
|
| 951 |
-
model:
|
| 952 |
data_loader: DataLoader,
|
| 953 |
preprocessor: DataPreprocessor,
|
| 954 |
reindex_state: dict,
|
|
@@ -1075,7 +1075,7 @@ def apply_dataset_preset(args) -> None:
|
|
| 1075 |
|
| 1076 |
|
| 1077 |
def parse_args():
|
| 1078 |
-
parser = argparse.ArgumentParser(description="End-to-end fine-tuning for
|
| 1079 |
|
| 1080 |
parser.add_argument("--seed", type=int, default=2021)
|
| 1081 |
parser.add_argument("--device", type=str, default="cuda")
|
|
@@ -1117,13 +1117,13 @@ def parse_args():
|
|
| 1117 |
)
|
| 1118 |
parser.add_argument("--unfreeze_last_n", type=int, default=1)
|
| 1119 |
parser.add_argument("--train_pos_emb", action="store_true")
|
| 1120 |
-
parser.add_argument("--train_module_embed", action="store_true")
|
| 1121 |
parser.add_argument("--use_family_emb", action="store_true",
|
| 1122 |
help="Add family-type embedding (Hallmarks/Reactome/Hotspot/WGCNA)")
|
| 1123 |
-
parser.add_argument("--use_module_gate", action="store_true",
|
| 1124 |
-
help="Add learnable sparse gate per module")
|
| 1125 |
-
parser.add_argument("--use_module_selector", action="store_true",
|
| 1126 |
-
help="Dynamic module selector with sparsity loss (soft train, hard inference)")
|
| 1127 |
parser.add_argument("--selector_sparsity_weight", type=float, default=0.1,
|
| 1128 |
help="Lambda for sparsity loss on module selector scores")
|
| 1129 |
parser.add_argument("--selector_tau", type=float, default=0.5,
|
|
@@ -1152,32 +1152,33 @@ def parse_args():
|
|
| 1152 |
parser.add_argument("--min_cells", type=int, default=10)
|
| 1153 |
parser.add_argument("--max_cells", type=int, default=None)
|
| 1154 |
|
| 1155 |
-
parser.add_argument("--config_pkl", type=str, default=str(WEIGHTS_DIR / "
|
| 1156 |
parser.add_argument(
|
| 1157 |
-
"--performer_checkpoint",
|
| 1158 |
type=str,
|
| 1159 |
-
default=str(WEIGHTS_DIR / "
|
|
|
|
| 1160 |
)
|
| 1161 |
-
parser.add_argument("--module_data_dir", type=str, default=str(ASSET_DIR / "
|
| 1162 |
parser.add_argument("--gene_list_path", type=str, default=str(ASSET_DIR / "selected_genes_27k.txt"))
|
| 1163 |
parser.add_argument("--gene_mapping_path", type=str, default=str(ASSET_DIR / "gene2ens.csv"))
|
| 1164 |
parser.add_argument("--gene2vec_data_path", type=str, default=str(ASSET_DIR / "selected_gene2vec_27k.npy"))
|
| 1165 |
parser.add_argument("--gene2ens_file", type=str, default=str(ASSET_DIR / "gene2ens.csv"))
|
| 1166 |
parser.add_argument("--gene2ens_column", type=str, default="Ensembl gene ID")
|
| 1167 |
parser.add_argument(
|
| 1168 |
-
"--shuffle_module_genes",
|
| 1169 |
action="store_true",
|
| 1170 |
help="Apply an extra downstream reshuffle on top of module_sets restored from PKL. "
|
| 1171 |
"Do not pass this when reproducing the pretrained backbone as-is.",
|
| 1172 |
-
|
| 1173 |
parser.add_argument(
|
| 1174 |
"--shuffle_seed",
|
| 1175 |
type=int,
|
| 1176 |
default=2021,
|
| 1177 |
-
help="Random seed for the downstream extra reshuffle when --
|
| 1178 |
)
|
| 1179 |
|
| 1180 |
-
parser.add_argument("--output_dir", type=str, default="./
|
| 1181 |
parser.add_argument("--run_name", type=str, default="")
|
| 1182 |
parser.add_argument(
|
| 1183 |
"--save_test_artifacts",
|
|
@@ -1384,7 +1385,7 @@ def main():
|
|
| 1384 |
train_module_embed=args.train_module_embed,
|
| 1385 |
)
|
| 1386 |
|
| 1387 |
-
model =
|
| 1388 |
backbone=backbone,
|
| 1389 |
module_sets=module_sets,
|
| 1390 |
num_classes=num_classes,
|
|
|
|
| 2 |
# -*- coding: utf-8 -*-
|
| 3 |
|
| 4 |
"""
|
| 5 |
+
End-to-end cell annotation fine-tuning for BioMod.
|
| 6 |
|
| 7 |
This script is intentionally independent from the legacy two-stage pipeline:
|
| 8 |
1. No feature extraction to .npy files.
|
| 9 |
+
2. No imports from other scripts in biomod/cell_annotation.
|
| 10 |
+
3. Reuses the pretrained BioMod backbone directly.
|
| 11 |
|
| 12 |
Supported cell embedding heads:
|
| 13 |
- module_mean: mean over restored module tokens
|
|
|
|
| 48 |
from torch.utils.data.distributed import DistributedSampler
|
| 49 |
|
| 50 |
SCRIPT_DIR = Path(__file__).resolve().parent
|
| 51 |
+
BIOMOD_ROOT = SCRIPT_DIR.parent
|
| 52 |
+
SCLONG_ROOT = BIOMOD_ROOT.parent
|
| 53 |
|
| 54 |
REPO_ROOT = Path(__file__).resolve().parents[2]
|
| 55 |
ASSET_DIR = REPO_ROOT / "model_dependencies"
|
| 56 |
DOWNSTREAM_DATA_DIR = REPO_ROOT / "data" / "downstream" / "final_data"
|
| 57 |
+
WEIGHTS_DIR = REPO_ROOT / "weight"
|
| 58 |
|
| 59 |
+
if str(BIOMOD_ROOT) not in sys.path:
|
| 60 |
+
sys.path.insert(0, str(BIOMOD_ROOT))
|
| 61 |
if str(SCLONG_ROOT) not in sys.path:
|
| 62 |
sys.path.insert(0, str(SCLONG_ROOT))
|
| 63 |
|
| 64 |
+
from models_biomod import (
|
| 65 |
+
migrate_legacy_biomod_config,
|
| 66 |
+
migrate_legacy_biomod_state_dict,
|
| 67 |
)
|
| 68 |
|
| 69 |
if TYPE_CHECKING:
|
| 70 |
+
from models_biomod import BioMod
|
| 71 |
|
| 72 |
|
| 73 |
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
|
|
|
| 373 |
return self.out_layer(x)
|
| 374 |
|
| 375 |
|
| 376 |
+
class BioModSelector(nn.Module):
|
| 377 |
"""Per-module scorer: soft weighting during training, hard threshold during inference."""
|
| 378 |
|
| 379 |
def __init__(self, embed_dim: int):
|
|
|
|
| 395 |
return x, scores
|
| 396 |
|
| 397 |
|
| 398 |
+
class BioModCellClassifier(nn.Module):
|
| 399 |
def __init__(
|
| 400 |
self,
|
| 401 |
+
backbone: "BioMod",
|
| 402 |
module_sets: List[List[int]],
|
| 403 |
num_classes: int,
|
| 404 |
head_type: str,
|
|
|
|
| 430 |
self.use_module_selector = use_module_selector
|
| 431 |
self.selector_tau = 0.5
|
| 432 |
if use_module_selector:
|
| 433 |
+
self.module_selector = BioModSelector(self.embed_dim)
|
| 434 |
else:
|
| 435 |
self.module_selector = None
|
| 436 |
self._selector_scores = None
|
| 437 |
|
| 438 |
if self.head_type == "gene_mean":
|
| 439 |
+
module_idx, gene_idx, gene_counts = self._build_biomod_gene_index()
|
| 440 |
self.register_buffer("agg_module_idx", module_idx)
|
| 441 |
self.register_buffer("agg_gene_idx", gene_idx)
|
| 442 |
self.register_buffer("agg_gene_counts", gene_counts)
|
|
|
|
| 454 |
def load_state_dict(self, state_dict, strict: bool = True, assign: bool = False):
|
| 455 |
"""Accept current module names and older serialized backbone names."""
|
| 456 |
return super().load_state_dict(
|
| 457 |
+
migrate_legacy_biomod_state_dict(state_dict),
|
| 458 |
strict=strict,
|
| 459 |
assign=assign,
|
| 460 |
)
|
| 461 |
|
| 462 |
+
def _build_biomod_gene_index(self) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 463 |
module_indices = []
|
| 464 |
gene_indices = []
|
| 465 |
gene_counts = torch.zeros(self.num_genes, dtype=torch.long)
|
|
|
|
| 475 |
gene_counts,
|
| 476 |
)
|
| 477 |
|
| 478 |
+
def _restore_biomod_order(self, latent: torch.Tensor, ids_restore: torch.Tensor) -> torch.Tensor:
|
| 479 |
if self.use_cls:
|
| 480 |
module_tokens = latent[:, 1:, :]
|
| 481 |
else:
|
|
|
|
| 483 |
gather_index = ids_restore.unsqueeze(-1).expand(-1, -1, module_tokens.shape[-1])
|
| 484 |
return torch.gather(module_tokens, dim=1, index=gather_index)
|
| 485 |
|
| 486 |
+
def _biomod_to_gene(self, module_tokens: torch.Tensor) -> torch.Tensor:
|
| 487 |
batch_size, _, dim = module_tokens.shape
|
| 488 |
gene_feats = torch.zeros(
|
| 489 |
batch_size,
|
|
|
|
| 519 |
if self.head_type == "cls":
|
| 520 |
cell_emb = latent[:, 0, :]
|
| 521 |
else:
|
| 522 |
+
module_tokens = self._restore_biomod_order(latent, ids_restore)
|
| 523 |
if self.module_selector is not None:
|
| 524 |
module_tokens, self._selector_scores = self.module_selector(
|
| 525 |
module_tokens, tau=self.selector_tau, hard=not self.training,
|
|
|
|
| 531 |
elif self.head_type == "attn_pool":
|
| 532 |
cell_emb, _ = self.pooler(module_tokens)
|
| 533 |
elif self.head_type == "gene_mean":
|
| 534 |
+
gene_tokens = self._biomod_to_gene(module_tokens)
|
| 535 |
cell_emb = gene_tokens.mean(dim=1)
|
| 536 |
else:
|
| 537 |
raise ValueError(f"Unsupported head_type: {self.head_type}")
|
|
|
|
| 540 |
return self.classifier(cell_emb)
|
| 541 |
|
| 542 |
|
| 543 |
+
def get_encoder_blocks(backbone: "BioMod") -> List[nn.Module]:
|
| 544 |
net = backbone.encoder.net
|
| 545 |
if hasattr(net, "layers"):
|
| 546 |
return list(net.layers)
|
| 547 |
if hasattr(net, "blocks"):
|
| 548 |
return list(net.blocks)
|
| 549 |
+
raise AttributeError("Cannot find encoder blocks on BioMod backbone")
|
| 550 |
|
| 551 |
|
| 552 |
def set_requires_grad(module: nn.Module, flag: bool) -> None:
|
|
|
|
| 555 |
|
| 556 |
|
| 557 |
def configure_backbone_trainability(
|
| 558 |
+
backbone: "BioMod",
|
| 559 |
finetune_mode: str,
|
| 560 |
unfreeze_last_n: int,
|
| 561 |
train_pos_emb: bool,
|
|
|
|
| 604 |
|
| 605 |
|
| 606 |
def build_optimizer(
|
| 607 |
+
model: BioModCellClassifier,
|
| 608 |
lr_head: float,
|
| 609 |
lr_backbone: float,
|
| 610 |
weight_decay: float,
|
|
|
|
| 669 |
return train_batch_size, eval_batch_size, effective_global_batch
|
| 670 |
|
| 671 |
|
| 672 |
+
def infer_model_config(args, checkpoint: dict, pkl_config: Optional[dict], biomod_data: List[List[int]]) -> dict:
|
| 673 |
+
from models_biomod import sanitize_biomod_config
|
| 674 |
|
| 675 |
if pkl_config is not None and isinstance(pkl_config, dict):
|
| 676 |
+
config = sanitize_biomod_config(
|
| 677 |
pkl_config,
|
| 678 |
source=str(args.config_pkl),
|
| 679 |
log_fn=logger.info,
|
| 680 |
)
|
| 681 |
logger.info("Using config from PKL: %s", args.config_pkl)
|
| 682 |
if "module_sets" in config:
|
| 683 |
+
biomod_data = config["module_sets"]
|
| 684 |
elif isinstance(checkpoint, dict) and "config" in checkpoint:
|
| 685 |
+
config = sanitize_biomod_config(
|
| 686 |
checkpoint["config"],
|
| 687 |
source=str(resolve_path(args.performer_checkpoint)),
|
| 688 |
log_fn=logger.info,
|
|
|
|
| 695 |
)
|
| 696 |
|
| 697 |
return {
|
| 698 |
+
"module_sets": biomod_data,
|
| 699 |
"num_genes": config["num_genes"],
|
| 700 |
"base_dim": config["base_dim"],
|
| 701 |
"enc_depth": config["enc_depth"],
|
|
|
|
| 722 |
}
|
| 723 |
|
| 724 |
|
| 725 |
+
def compute_biomod_family_ids(module_data_dir: str) -> List[int]:
|
| 726 |
+
"""Read merged_biomods.json, return family ID (0-3) for each module."""
|
| 727 |
+
merged_path = Path(module_data_dir) / "merged_biomods.json"
|
| 728 |
with open(merged_path) as f:
|
| 729 |
modules = json.load(f)
|
| 730 |
PREFIXES = [
|
|
|
|
| 744 |
return family_ids
|
| 745 |
|
| 746 |
|
| 747 |
+
def load_backbone(args, device: torch.device) -> Tuple["BioMod", List[List[int]], dict]:
|
| 748 |
+
from models_biomod import BioMod
|
| 749 |
+
from biomod_dataset import load_biomod_data
|
| 750 |
|
| 751 |
config_pkl = None
|
| 752 |
config_path = resolve_path(args.config_pkl)
|
| 753 |
if config_path.exists():
|
| 754 |
with open(config_path, "rb") as handle:
|
| 755 |
+
config_pkl = migrate_legacy_biomod_config(pickle.load(handle))
|
| 756 |
else:
|
| 757 |
logger.warning("Config PKL not found at %s; fallback to checkpoint config", config_path)
|
| 758 |
|
| 759 |
ckpt_path = resolve_path(args.performer_checkpoint)
|
| 760 |
checkpoint = torch.load(ckpt_path, map_location=device)
|
| 761 |
|
| 762 |
+
biomod_data = None
|
| 763 |
if isinstance(config_pkl, dict) and "module_sets" in config_pkl:
|
| 764 |
+
biomod_data = config_pkl["module_sets"]
|
| 765 |
+
logger.info("Using module_sets from PKL (%d modules)", len(biomod_data))
|
| 766 |
elif isinstance(checkpoint, dict) and isinstance(checkpoint.get("config"), dict) and "module_sets" in checkpoint["config"]:
|
| 767 |
+
biomod_data = checkpoint["config"]["module_sets"]
|
| 768 |
+
logger.info("Using module_sets from checkpoint config (%d modules)", len(biomod_data))
|
| 769 |
else:
|
| 770 |
if not args.module_data_dir:
|
| 771 |
raise ValueError("module_data_dir is required when module_sets are not stored in the PKL/checkpoint config")
|
| 772 |
+
biomod_data = load_biomod_data(
|
| 773 |
module_data_dir=str(resolve_path(args.module_data_dir)),
|
| 774 |
gene_list_path=str(resolve_path(args.gene_list_path)),
|
| 775 |
gene_mapping_path=str(resolve_path(args.gene_mapping_path)),
|
|
|
|
| 779 |
if args.shuffle_module_genes:
|
| 780 |
logger.info("Shuffling genes across modules with seed=%d", args.shuffle_seed)
|
| 781 |
rng = random.Random(args.shuffle_seed)
|
| 782 |
+
module_sizes = [len(module) for module in biomod_data]
|
| 783 |
+
all_genes = [gene for module in biomod_data for gene in module]
|
| 784 |
rng.shuffle(all_genes)
|
| 785 |
shuffled = []
|
| 786 |
start = 0
|
| 787 |
for size in module_sizes:
|
| 788 |
shuffled.append(all_genes[start:start + size])
|
| 789 |
start += size
|
| 790 |
+
biomod_data = shuffled
|
| 791 |
|
| 792 |
+
model_config = infer_model_config(args, checkpoint, config_pkl, biomod_data)
|
| 793 |
+
biomod_data = model_config["module_sets"]
|
| 794 |
|
| 795 |
if "model_state_dict" in checkpoint:
|
| 796 |
state_dict = checkpoint["model_state_dict"]
|
|
|
|
| 807 |
model_config["base_dim"],
|
| 808 |
model_config["enc_depth"],
|
| 809 |
model_config["enc_heads"],
|
| 810 |
+
len(biomod_data),
|
| 811 |
use_cls_token,
|
| 812 |
)
|
| 813 |
|
|
|
|
| 815 |
if getattr(args, "use_family_emb", False):
|
| 816 |
pdd = getattr(args, "module_data_dir", None)
|
| 817 |
if pdd:
|
| 818 |
+
module_family_ids = compute_biomod_family_ids(str(resolve_path(pdd)))
|
| 819 |
logger.info("Family embedding enabled: %d module family IDs computed", len(module_family_ids))
|
| 820 |
|
| 821 |
+
backbone = BioMod(
|
| 822 |
+
module_sets=biomod_data,
|
| 823 |
num_genes=model_config["num_genes"],
|
| 824 |
+
max_seq_len=len(biomod_data),
|
| 825 |
base_dim=model_config["base_dim"],
|
| 826 |
enc_depth=model_config["enc_depth"],
|
| 827 |
enc_heads=model_config["enc_heads"],
|
|
|
|
| 866 |
logger.warning("Unexpected keys: %s", unexpected_keys[:10])
|
| 867 |
|
| 868 |
backbone.to(device)
|
| 869 |
+
return backbone, biomod_data, model_config
|
| 870 |
|
| 871 |
|
| 872 |
def stratified_split(
|
|
|
|
| 908 |
|
| 909 |
@torch.no_grad()
|
| 910 |
def evaluate(
|
| 911 |
+
model: BioModCellClassifier,
|
| 912 |
data_loader: DataLoader,
|
| 913 |
preprocessor: DataPreprocessor,
|
| 914 |
reindex_state: dict,
|
|
|
|
| 948 |
|
| 949 |
|
| 950 |
def train_one_epoch(
|
| 951 |
+
model: BioModCellClassifier,
|
| 952 |
data_loader: DataLoader,
|
| 953 |
preprocessor: DataPreprocessor,
|
| 954 |
reindex_state: dict,
|
|
|
|
| 1075 |
|
| 1076 |
|
| 1077 |
def parse_args():
|
| 1078 |
+
parser = argparse.ArgumentParser(description="End-to-end fine-tuning for BioMod cell annotation")
|
| 1079 |
|
| 1080 |
parser.add_argument("--seed", type=int, default=2021)
|
| 1081 |
parser.add_argument("--device", type=str, default="cuda")
|
|
|
|
| 1117 |
)
|
| 1118 |
parser.add_argument("--unfreeze_last_n", type=int, default=1)
|
| 1119 |
parser.add_argument("--train_pos_emb", action="store_true")
|
| 1120 |
+
parser.add_argument("--train_biomod_embed", "--train_module_embed", action="store_true", dest='train_module_embed')
|
| 1121 |
parser.add_argument("--use_family_emb", action="store_true",
|
| 1122 |
help="Add family-type embedding (Hallmarks/Reactome/Hotspot/WGCNA)")
|
| 1123 |
+
parser.add_argument("--use_biomod_gate", "--use_module_gate", action="store_true",
|
| 1124 |
+
help="Add learnable sparse gate per module", dest='use_module_gate')
|
| 1125 |
+
parser.add_argument("--use_biomod_selector", "--use_module_selector", action="store_true",
|
| 1126 |
+
help="Dynamic module selector with sparsity loss (soft train, hard inference)", dest='use_module_selector')
|
| 1127 |
parser.add_argument("--selector_sparsity_weight", type=float, default=0.1,
|
| 1128 |
help="Lambda for sparsity loss on module selector scores")
|
| 1129 |
parser.add_argument("--selector_tau", type=float, default=0.5,
|
|
|
|
| 1152 |
parser.add_argument("--min_cells", type=int, default=10)
|
| 1153 |
parser.add_argument("--max_cells", type=int, default=None)
|
| 1154 |
|
| 1155 |
+
parser.add_argument("--config_pkl", type=str, default=str(WEIGHTS_DIR / "BioMod.pkl"))
|
| 1156 |
parser.add_argument(
|
| 1157 |
+
"--biomod_checkpoint", "--performer_checkpoint",
|
| 1158 |
type=str,
|
| 1159 |
+
default=str(WEIGHTS_DIR / "BioMod.pth"),
|
| 1160 |
+
dest='performer_checkpoint',
|
| 1161 |
)
|
| 1162 |
+
parser.add_argument("--biomod_data_dir", "--module_data_dir", type=str, default=str(ASSET_DIR / "biomod_data_all"), dest='module_data_dir')
|
| 1163 |
parser.add_argument("--gene_list_path", type=str, default=str(ASSET_DIR / "selected_genes_27k.txt"))
|
| 1164 |
parser.add_argument("--gene_mapping_path", type=str, default=str(ASSET_DIR / "gene2ens.csv"))
|
| 1165 |
parser.add_argument("--gene2vec_data_path", type=str, default=str(ASSET_DIR / "selected_gene2vec_27k.npy"))
|
| 1166 |
parser.add_argument("--gene2ens_file", type=str, default=str(ASSET_DIR / "gene2ens.csv"))
|
| 1167 |
parser.add_argument("--gene2ens_column", type=str, default="Ensembl gene ID")
|
| 1168 |
parser.add_argument(
|
| 1169 |
+
"--shuffle_biomod_genes", "--shuffle_module_genes",
|
| 1170 |
action="store_true",
|
| 1171 |
help="Apply an extra downstream reshuffle on top of module_sets restored from PKL. "
|
| 1172 |
"Do not pass this when reproducing the pretrained backbone as-is.",
|
| 1173 |
+
dest='shuffle_module_genes')
|
| 1174 |
parser.add_argument(
|
| 1175 |
"--shuffle_seed",
|
| 1176 |
type=int,
|
| 1177 |
default=2021,
|
| 1178 |
+
help="Random seed for the downstream extra reshuffle when --shuffle_biomod_genes is enabled.",
|
| 1179 |
)
|
| 1180 |
|
| 1181 |
+
parser.add_argument("--output_dir", type=str, default="./ckpts_biomod_finetune")
|
| 1182 |
parser.add_argument("--run_name", type=str, default="")
|
| 1183 |
parser.add_argument(
|
| 1184 |
"--save_test_artifacts",
|
|
|
|
| 1385 |
train_module_embed=args.train_module_embed,
|
| 1386 |
)
|
| 1387 |
|
| 1388 |
+
model = BioModCellClassifier(
|
| 1389 |
backbone=backbone,
|
| 1390 |
module_sets=module_sets,
|
| 1391 |
num_classes=num_classes,
|
|
@@ -3,9 +3,9 @@
|
|
| 3 |
Run this command from the repository root in a CUDA/NCCL environment. Resource defaults resolve from the script location; explicit relative paths resolve from the working directory.
|
| 4 |
|
| 5 |
```bash
|
| 6 |
-
torchrun --nproc_per_node=4 BioMod/cell_annotation/
|
| 7 |
-
--config_pkl weight/
|
| 8 |
-
--
|
| 9 |
--data_path data/downstream/final_data/Zheng68K.h5ad \
|
| 10 |
--label_column celltype \
|
| 11 |
--head_type attn_pool \
|
|
@@ -16,6 +16,6 @@ torchrun --nproc_per_node=4 BioMod/cell_annotation/performer_module_finetune.py
|
|
| 16 |
--lr_backbone 1e-5 \
|
| 17 |
--batch_size 256 \
|
| 18 |
--epochs 300 \
|
| 19 |
-
--
|
| 20 |
--output_dir tutorial_runs/cell_annotation_zheng68k
|
| 21 |
```
|
|
|
|
| 3 |
Run this command from the repository root in a CUDA/NCCL environment. Resource defaults resolve from the script location; explicit relative paths resolve from the working directory.
|
| 4 |
|
| 5 |
```bash
|
| 6 |
+
torchrun --nproc_per_node=4 BioMod/cell_annotation/biomod_finetune.py \
|
| 7 |
+
--config_pkl weight/BioMod.pkl \
|
| 8 |
+
--biomod_checkpoint weight/BioMod.pth \
|
| 9 |
--data_path data/downstream/final_data/Zheng68K.h5ad \
|
| 10 |
--label_column celltype \
|
| 11 |
--head_type attn_pool \
|
|
|
|
| 16 |
--lr_backbone 1e-5 \
|
| 17 |
--batch_size 256 \
|
| 18 |
--epochs 300 \
|
| 19 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 20 |
--output_dir tutorial_runs/cell_annotation_zheng68k
|
| 21 |
```
|
|
@@ -1,10 +1,10 @@
|
|
| 1 |
# Run from the outer repository root in a Linux CUDA/NCCL environment.
|
| 2 |
# Set BIOMOD_PRETRAIN_DATA to your preprocessed per-cell NPZ directory.
|
| 3 |
# The pretraining corpus is not included in this repository.
|
| 4 |
-
conda activate
|
| 5 |
: "${BIOMOD_PRETRAIN_DATA:?Set BIOMOD_PRETRAIN_DATA to the pretraining NPZ directory}"
|
| 6 |
torchrun --standalone --nproc_per_node="${NPROC_PER_NODE:-1}" \
|
| 7 |
-
BioMod/
|
| 8 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 9 |
--cell_num 4000000 \
|
| 10 |
--epoch 15 \
|
|
@@ -15,11 +15,11 @@ torchrun --standalone --nproc_per_node="${NPROC_PER_NODE:-1}" \
|
|
| 15 |
--log_every 100 \
|
| 16 |
--mask_ratio 0.75 \
|
| 17 |
--data_path "$BIOMOD_PRETRAIN_DATA" \
|
| 18 |
-
--
|
| 19 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 20 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
| 21 |
--ckpt_dir tutorial_runs/pretraining \
|
| 22 |
-
--model_name
|
| 23 |
--fusion_method multiply \
|
| 24 |
--loss_mode gene_mean \
|
| 25 |
--decoder_type simple \
|
|
|
|
| 1 |
# Run from the outer repository root in a Linux CUDA/NCCL environment.
|
| 2 |
# Set BIOMOD_PRETRAIN_DATA to your preprocessed per-cell NPZ directory.
|
| 3 |
# The pretraining corpus is not included in this repository.
|
| 4 |
+
conda activate biomod
|
| 5 |
: "${BIOMOD_PRETRAIN_DATA:?Set BIOMOD_PRETRAIN_DATA to the pretraining NPZ directory}"
|
| 6 |
torchrun --standalone --nproc_per_node="${NPROC_PER_NODE:-1}" \
|
| 7 |
+
BioMod/biomod.py \
|
| 8 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 9 |
--cell_num 4000000 \
|
| 10 |
--epoch 15 \
|
|
|
|
| 15 |
--log_every 100 \
|
| 16 |
--mask_ratio 0.75 \
|
| 17 |
--data_path "$BIOMOD_PRETRAIN_DATA" \
|
| 18 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 19 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 20 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
| 21 |
--ckpt_dir tutorial_runs/pretraining \
|
| 22 |
+
--model_name biomod_pretraining \
|
| 23 |
--fusion_method multiply \
|
| 24 |
--loss_mode gene_mean \
|
| 25 |
--decoder_type simple \
|
|
@@ -1,19 +1,19 @@
|
|
| 1 |
# Drug Combination Synergy Prediction with BioMod
|
| 2 |
|
| 3 |
This task predicts drug-combination synergy using BioMod cell embeddings from
|
| 4 |
-
the
|
| 5 |
[BioMod model weights and matching configuration](../../README.md#biomod-model-weights).
|
| 6 |
|
| 7 |
It is adapted from scLong's drug-combination implementation
|
| 8 |
(DeepDDs), replacing the scfm/geneformer cell-line representation with
|
| 9 |
-
|
| 10 |
|
| 11 |
## Model Architecture
|
| 12 |
|
| 13 |
| Component | Description |
|
| 14 |
| --- | --- |
|
| 15 |
| Drug branch | Three PyG GCNConv layers (78 -> 78 -> 156 -> 312), `global_max_pool`, and two fully connected layers to 128 dimensions |
|
| 16 |
-
| Cell branch | Pretrained
|
| 17 |
| Fusion layer | `concat(drug1, drug2, cell) = 384 -> MLP -> 2-class prediction` |
|
| 18 |
|
| 19 |
The drug1 and drug2 branches share weights, matching scLong.
|
|
@@ -55,20 +55,20 @@ pip install RDKit -i https://pypi.tuna.tsinghua.edu.cn/simple
|
|
| 55 |
### Basic Run (Domain Generalization Mode, Default)
|
| 56 |
|
| 57 |
```bash
|
| 58 |
-
python BioMod/drug_combination/
|
| 59 |
-
--checkpoint_path weight/
|
| 60 |
-
--hyper_params_path weight/
|
| 61 |
-
--
|
| 62 |
--data_dir data/downstream/final_data/drug_combination_synergy/data_summary
|
| 63 |
```
|
| 64 |
|
| 65 |
### Custom Paths
|
| 66 |
|
| 67 |
```bash
|
| 68 |
-
python BioMod/drug_combination/
|
| 69 |
-
--checkpoint_path weight/
|
| 70 |
-
--hyper_params_path weight/
|
| 71 |
-
--
|
| 72 |
--gene_list_path model_dependencies/selected_genes_27k.txt\
|
| 73 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 74 |
--gene2vec_path model_dependencies/selected_gene2vec_27k.npy \
|
|
@@ -87,20 +87,20 @@ Reference note: `1e-4 500 0.6824301861702127`.
|
|
| 87 |
### Five-Fold Cross-Validation (Disable Domain Generalization)
|
| 88 |
|
| 89 |
```bash
|
| 90 |
-
python BioMod/drug_combination/
|
| 91 |
-
--checkpoint_path weight/
|
| 92 |
-
--hyper_params_path weight/
|
| 93 |
-
--
|
| 94 |
--no_domain_gen
|
| 95 |
```
|
| 96 |
|
| 97 |
### Force Reprocessing After Changing the Model or Data
|
| 98 |
|
| 99 |
```bash
|
| 100 |
-
python BioMod/drug_combination/
|
| 101 |
-
--checkpoint_path weight/
|
| 102 |
-
--hyper_params_path weight/
|
| 103 |
-
--
|
| 104 |
--force_preprocess
|
| 105 |
```
|
| 106 |
|
|
@@ -109,8 +109,8 @@ python BioMod/drug_combination/train_drug_combination_performer_module.py \
|
|
| 109 |
| Argument | Default | Description |
|
| 110 |
| --- | --- | --- |
|
| 111 |
| `--checkpoint_path` | Required | Path to the BioMod model weights |
|
| 112 |
-
| `--hyper_params_path` | Required | Path to the
|
| 113 |
-
| `--
|
| 114 |
| `--data_dir` | `data/downstream/final_data/drug_combination_synergy/data_summary` | Data directory |
|
| 115 |
| `--validation_label_file` | `test_data/independent_input.csv` | Validation labels for checkpoint selection; legacy alias: `--test_label_file` |
|
| 116 |
| `--validation_cell_file` | `test_data/independent_cell_features_954.csv` | Validation cell features; legacy alias: `--test_cell_file` |
|
|
@@ -120,7 +120,7 @@ python BioMod/drug_combination/train_drug_combination_performer_module.py \
|
|
| 120 |
| `--n_folds` | 5 | Number of folds |
|
| 121 |
| `--domain_gen` | True | Domain generalization mode: train on the full training set and select checkpoints using a separate validation set |
|
| 122 |
| `--eval_interval` | 5 | Evaluate every N epochs |
|
| 123 |
-
| `--pool_method` | mean |
|
| 124 |
| `--cache_emb` | True | Cache cell embeddings to a `.npy` file |
|
| 125 |
| `--force_preprocess` | False | Force regeneration of the PyG dataset |
|
| 126 |
|
|
@@ -130,7 +130,7 @@ Results are saved under `--output_dir` (default: `BioMod/drug_combination/result
|
|
| 130 |
|
| 131 |
- `best_model_fold{i}.pt` - model weights selected by the highest validation AUC for each fold
|
| 132 |
- `fpr_tpr_fold{i}.pkl` - validation FPR/TPR curve data for drawing ROC curves
|
| 133 |
-
- `cell_emb_cache_{pool_method}.npy` - cached
|
| 134 |
|
| 135 |
## Evaluation Metrics
|
| 136 |
|
|
|
|
| 1 |
# Drug Combination Synergy Prediction with BioMod
|
| 2 |
|
| 3 |
This task predicts drug-combination synergy using BioMod cell embeddings from
|
| 4 |
+
the BioMod encoder and GCN-based drug molecular graph encoders. Use the
|
| 5 |
[BioMod model weights and matching configuration](../../README.md#biomod-model-weights).
|
| 6 |
|
| 7 |
It is adapted from scLong's drug-combination implementation
|
| 8 |
(DeepDDs), replacing the scfm/geneformer cell-line representation with
|
| 9 |
+
BioMod.
|
| 10 |
|
| 11 |
## Model Architecture
|
| 12 |
|
| 13 |
| Component | Description |
|
| 14 |
| --- | --- |
|
| 15 |
| Drug branch | Three PyG GCNConv layers (78 -> 78 -> 156 -> 312), `global_max_pool`, and two fully connected layers to 128 dimensions |
|
| 16 |
+
| Cell branch | Pretrained BioMod encoder, module mean pooling, and an MLP to 128 dimensions |
|
| 17 |
| Fusion layer | `concat(drug1, drug2, cell) = 384 -> MLP -> 2-class prediction` |
|
| 18 |
|
| 19 |
The drug1 and drug2 branches share weights, matching scLong.
|
|
|
|
| 55 |
### Basic Run (Domain Generalization Mode, Default)
|
| 56 |
|
| 57 |
```bash
|
| 58 |
+
python BioMod/drug_combination/train_drug_combination_biomod.py \
|
| 59 |
+
--checkpoint_path weight/BioMod.pth \
|
| 60 |
+
--hyper_params_path weight/BioMod.pkl \
|
| 61 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 62 |
--data_dir data/downstream/final_data/drug_combination_synergy/data_summary
|
| 63 |
```
|
| 64 |
|
| 65 |
### Custom Paths
|
| 66 |
|
| 67 |
```bash
|
| 68 |
+
python BioMod/drug_combination/train_drug_combination_biomod.py \
|
| 69 |
+
--checkpoint_path weight/BioMod.pth \
|
| 70 |
+
--hyper_params_path weight/BioMod.pkl \
|
| 71 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 72 |
--gene_list_path model_dependencies/selected_genes_27k.txt\
|
| 73 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 74 |
--gene2vec_path model_dependencies/selected_gene2vec_27k.npy \
|
|
|
|
| 87 |
### Five-Fold Cross-Validation (Disable Domain Generalization)
|
| 88 |
|
| 89 |
```bash
|
| 90 |
+
python BioMod/drug_combination/train_drug_combination_biomod.py \
|
| 91 |
+
--checkpoint_path weight/BioMod.pth \
|
| 92 |
+
--hyper_params_path weight/BioMod.pkl \
|
| 93 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 94 |
--no_domain_gen
|
| 95 |
```
|
| 96 |
|
| 97 |
### Force Reprocessing After Changing the Model or Data
|
| 98 |
|
| 99 |
```bash
|
| 100 |
+
python BioMod/drug_combination/train_drug_combination_biomod.py \
|
| 101 |
+
--checkpoint_path weight/BioMod.pth \
|
| 102 |
+
--hyper_params_path weight/BioMod.pkl \
|
| 103 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 104 |
--force_preprocess
|
| 105 |
```
|
| 106 |
|
|
|
|
| 109 |
| Argument | Default | Description |
|
| 110 |
| --- | --- | --- |
|
| 111 |
| `--checkpoint_path` | Required | Path to the BioMod model weights |
|
| 112 |
+
| `--hyper_params_path` | Required | Path to the BioMod hyper-parameter pkl file |
|
| 113 |
+
| `--biomod_data_dir` | None | Directory containing module data files |
|
| 114 |
| `--data_dir` | `data/downstream/final_data/drug_combination_synergy/data_summary` | Data directory |
|
| 115 |
| `--validation_label_file` | `test_data/independent_input.csv` | Validation labels for checkpoint selection; legacy alias: `--test_label_file` |
|
| 116 |
| `--validation_cell_file` | `test_data/independent_cell_features_954.csv` | Validation cell features; legacy alias: `--test_cell_file` |
|
|
|
|
| 120 |
| `--n_folds` | 5 | Number of folds |
|
| 121 |
| `--domain_gen` | True | Domain generalization mode: train on the full training set and select checkpoints using a separate validation set |
|
| 122 |
| `--eval_interval` | 5 | Evaluate every N epochs |
|
| 123 |
+
| `--pool_method` | mean | BioMod pooling method (`mean`, `max`, or `concat`) |
|
| 124 |
| `--cache_emb` | True | Cache cell embeddings to a `.npy` file |
|
| 125 |
| `--force_preprocess` | False | Force regeneration of the PyG dataset |
|
| 126 |
|
|
|
|
| 130 |
|
| 131 |
- `best_model_fold{i}.pt` - model weights selected by the highest validation AUC for each fold
|
| 132 |
- `fpr_tpr_fold{i}.pkl` - validation FPR/TPR curve data for drawing ROC curves
|
| 133 |
+
- `cell_emb_cache_{pool_method}.npy` - cached BioMod cell embeddings
|
| 134 |
|
| 135 |
## Evaluation Metrics
|
| 136 |
|
|
@@ -1,18 +1,18 @@
|
|
| 1 |
#!/usr/bin/env python
|
| 2 |
# -*- coding: utf-8 -*-
|
| 3 |
"""
|
| 4 |
-
Drug Combination Synergy Prediction with
|
| 5 |
|
| 6 |
Aligned with scLong/drug_combination (DeepDDs) architecture:
|
| 7 |
- Drug branch: PyG GCNConv + global_max_pool (shared weights for drug1 & drug2)
|
| 8 |
-
- Cell branch:
|
| 9 |
- Fusion: concat(drug1, drug2, cell) -> MLP -> 2-class classification
|
| 10 |
|
| 11 |
Usage:
|
| 12 |
# Run from the repository root.
|
| 13 |
-
python BioMod/drug_combination/
|
| 14 |
--checkpoint_path <ckpt> --hyper_params_path <pkl> \
|
| 15 |
-
--
|
| 16 |
--data_dir data/downstream/final_data/drug_combination_synergy/data_summary
|
| 17 |
"""
|
| 18 |
|
|
@@ -54,35 +54,35 @@
|
|
| 54 |
|
| 55 |
# ===================== Path setup =====================
|
| 56 |
SCRIPT_DIR = Path(__file__).resolve().parent
|
| 57 |
-
|
| 58 |
-
SCLONG_ROOT =
|
| 59 |
|
| 60 |
REPO_ROOT = Path(__file__).resolve().parents[2]
|
| 61 |
ASSET_DIR = REPO_ROOT / "model_dependencies"
|
| 62 |
DOWNSTREAM_DATA_DIR = REPO_ROOT / "data" / "downstream" / "final_data"
|
| 63 |
-
WEIGHTS_DIR = REPO_ROOT / "weight"
|
| 64 |
|
| 65 |
-
if str(
|
| 66 |
-
sys.path.insert(0, str(
|
| 67 |
if str(SCLONG_ROOT) not in sys.path:
|
| 68 |
sys.path.insert(0, str(SCLONG_ROOT))
|
| 69 |
|
| 70 |
import importlib.util
|
| 71 |
|
| 72 |
-
# Import
|
| 73 |
-
_models_path = os.path.join(
|
| 74 |
-
_spec = importlib.util.spec_from_file_location("
|
| 75 |
_models_mod = importlib.util.module_from_spec(_spec)
|
| 76 |
_spec.loader.exec_module(_models_mod)
|
| 77 |
-
|
| 78 |
-
|
| 79 |
|
| 80 |
-
# Import
|
| 81 |
-
_dataset_path = os.path.join(
|
| 82 |
-
_spec2 = importlib.util.spec_from_file_location("
|
| 83 |
_module_mod = importlib.util.module_from_spec(_spec2)
|
| 84 |
_spec2.loader.exec_module(_module_mod)
|
| 85 |
-
|
| 86 |
|
| 87 |
|
| 88 |
# ===================== Utilities =====================
|
|
@@ -111,19 +111,19 @@ def reindex_tensor_universal(tensor, index_positions, dim, filler="0", device="c
|
|
| 111 |
return reindexed
|
| 112 |
|
| 113 |
|
| 114 |
-
# =====================
|
| 115 |
|
| 116 |
-
def
|
| 117 |
checkpoint_path, hyper_params_path, module_data_dir, module_data_path,
|
| 118 |
gene_list_path, gene_mapping_path, gene2vec_path, device,
|
| 119 |
shuffle_module_genes=False, shuffle_seed=2021,
|
| 120 |
):
|
| 121 |
-
print_sys(f"Loading
|
| 122 |
|
| 123 |
with open(hyper_params_path, 'rb') as f:
|
| 124 |
model_params = pickle.load(f)
|
| 125 |
model_params.pop('module_embed_chunk_padded_gene_budget', None)
|
| 126 |
-
model_params =
|
| 127 |
model_params, source=hyper_params_path, log_fn=print_sys,
|
| 128 |
)
|
| 129 |
|
|
@@ -133,7 +133,7 @@ def load_performer_module_model(
|
|
| 133 |
|
| 134 |
if 'module_sets' not in model_params or model_params.get('module_sets') is None:
|
| 135 |
if module_data_dir or module_data_path:
|
| 136 |
-
module_sets =
|
| 137 |
module_data_path=module_data_path,
|
| 138 |
module_data_dir=module_data_dir,
|
| 139 |
gene_list_path=gene_list_path,
|
|
@@ -143,8 +143,8 @@ def load_performer_module_model(
|
|
| 143 |
model_params['module_sets'] = module_sets
|
| 144 |
else:
|
| 145 |
raise ValueError(
|
| 146 |
-
"Model needs module_sets but neither --
|
| 147 |
-
"nor --
|
| 148 |
)
|
| 149 |
|
| 150 |
if shuffle_module_genes:
|
|
@@ -164,7 +164,7 @@ def load_performer_module_model(
|
|
| 164 |
model_params['module_sets'] = shuffled_module_sets
|
| 165 |
print_sys(f"Shuffled {len(all_gene_indices)} genes across {len(shuffled_module_sets)} modules")
|
| 166 |
|
| 167 |
-
model =
|
| 168 |
|
| 169 |
checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=False)
|
| 170 |
state_dict = checkpoint.get('model_state_dict', checkpoint)
|
|
@@ -173,7 +173,7 @@ def load_performer_module_model(
|
|
| 173 |
if 'go_conv' not in k and 'G_go' not in k.lower()
|
| 174 |
}
|
| 175 |
model.load_state_dict(filtered, strict=True)
|
| 176 |
-
print_sys("Loaded pretrained
|
| 177 |
|
| 178 |
model.eval()
|
| 179 |
for p in model.parameters():
|
|
@@ -182,7 +182,7 @@ def load_performer_module_model(
|
|
| 182 |
return model, len(model_params['module_sets']), model_params.get('base_dim', 1024)
|
| 183 |
|
| 184 |
|
| 185 |
-
def
|
| 186 |
gene2ens = pd.read_csv(symbol2ens_path, index_col=0)
|
| 187 |
symbol2ens = gene2ens["Ensembl gene ID"].to_dict()
|
| 188 |
|
|
@@ -191,15 +191,15 @@ def build_performer_module_gene_mapping(symbol2ens_path, gene_list_path, expr_ge
|
|
| 191 |
input_mapping = {gid: i for i, gid in enumerate(input_genes)}
|
| 192 |
|
| 193 |
with open(gene_list_path, "r") as f:
|
| 194 |
-
|
| 195 |
-
|
| 196 |
|
| 197 |
-
|
| 198 |
-
input_mapping.get(gid, input_gene_num) for gid in
|
| 199 |
]
|
| 200 |
-
intersect = np.intersect1d(input_genes,
|
| 201 |
-
print_sys(f"Target and
|
| 202 |
-
return input_mapping,
|
| 203 |
|
| 204 |
|
| 205 |
def build_cell_embeddings(
|
|
@@ -208,14 +208,14 @@ def build_cell_embeddings(
|
|
| 208 |
module_data_path, device, pool_method="mean",
|
| 209 |
shuffle_module_genes=False, shuffle_seed=2021,
|
| 210 |
):
|
| 211 |
-
model, num_modules, encoder_embed_dim =
|
| 212 |
checkpoint_path, hyper_params_path, module_data_dir, module_data_path,
|
| 213 |
gene_list_path, gene_mapping_path, gene2vec_path, device,
|
| 214 |
shuffle_module_genes=shuffle_module_genes, shuffle_seed=shuffle_seed,
|
| 215 |
)
|
| 216 |
|
| 217 |
gene_columns = list(cell_features_df.columns)
|
| 218 |
-
_,
|
| 219 |
symbol2ens_path, gene_list_path, gene_columns,
|
| 220 |
)
|
| 221 |
|
|
@@ -225,10 +225,10 @@ def build_cell_embeddings(
|
|
| 225 |
with torch.no_grad():
|
| 226 |
for cell_line_id, row in cell_features_df.iterrows():
|
| 227 |
x = torch.tensor(row.values[None, :], dtype=torch.float32, device=device)
|
| 228 |
-
|
| 229 |
-
x,
|
| 230 |
)
|
| 231 |
-
out = model(
|
| 232 |
|
| 233 |
if 'module_encodings' in out:
|
| 234 |
module_enc = out['module_encodings']
|
|
@@ -251,7 +251,7 @@ def build_cell_embeddings(
|
|
| 251 |
first_emb_dim = cell_emb_np.shape[0]
|
| 252 |
|
| 253 |
print_sys(
|
| 254 |
-
f"Built
|
| 255 |
f"dim={first_emb_dim}, pool_method={pool_method}"
|
| 256 |
)
|
| 257 |
return cell_emb_dict, first_emb_dim
|
|
@@ -319,7 +319,7 @@ class TestbedDataset(InMemoryDataset):
|
|
| 319 |
"""
|
| 320 |
PyG InMemoryDataset for drug combination prediction.
|
| 321 |
Same pattern as scLong/drug_combination/utils_test.py::TestbedDataset,
|
| 322 |
-
but uses
|
| 323 |
"""
|
| 324 |
|
| 325 |
def __init__(
|
|
@@ -412,7 +412,7 @@ class GCNNet(torch.nn.Module):
|
|
| 412 |
|
| 413 |
Drug branch: 3-layer GCNConv (78->78->156->312) + global_max_pool + 2 FC layers
|
| 414 |
Drug1 and drug2 share weights (drug2 uses drug1_conv* layers, same as scLong).
|
| 415 |
-
Cell branch:
|
| 416 |
Fusion: concat(drug1, drug2, cell) -> fc1(256) -> fc2(128) -> out(2)
|
| 417 |
"""
|
| 418 |
|
|
@@ -432,7 +432,7 @@ def __init__(
|
|
| 432 |
self.drug1_fc_g1 = nn.Linear(num_features_xd * 4, num_features_xd * 2)
|
| 433 |
self.drug1_fc_g2 = nn.Linear(num_features_xd * 2, output_dim)
|
| 434 |
|
| 435 |
-
# Cell branch (
|
| 436 |
self.reduction = nn.Sequential(
|
| 437 |
nn.Linear(cell_emb_dim, 512),
|
| 438 |
nn.ReLU(),
|
|
@@ -584,18 +584,18 @@ def load_cell_features(cell_file):
|
|
| 584 |
|
| 585 |
def main():
|
| 586 |
parser = argparse.ArgumentParser(
|
| 587 |
-
description='Drug Combination Synergy Prediction with
|
| 588 |
)
|
| 589 |
|
| 590 |
-
#
|
| 591 |
parser.add_argument('--checkpoint_path', type=str, required=True,
|
| 592 |
-
help='Path to
|
| 593 |
parser.add_argument('--hyper_params_path', type=str, required=True,
|
| 594 |
-
help='Path to
|
| 595 |
-
parser.add_argument('--module_data_dir', type=str, default=None,
|
| 596 |
-
help='Directory containing module data files')
|
| 597 |
-
parser.add_argument('--module_data_path', type=str, default=None,
|
| 598 |
-
help='Path to single module data file')
|
| 599 |
parser.add_argument('--gene_list_path', type=str,
|
| 600 |
default=str(ASSET_DIR / 'selected_genes_27k.txt'))
|
| 601 |
parser.add_argument('--gene_mapping_path', type=str,
|
|
@@ -606,8 +606,8 @@ def main():
|
|
| 606 |
default=str(ASSET_DIR / 'gene2ens.csv'))
|
| 607 |
parser.add_argument('--pool_method', type=str, default='mean',
|
| 608 |
choices=['mean', 'max', 'concat'])
|
| 609 |
-
parser.add_argument('--shuffle_module_genes', action='store_true',
|
| 610 |
-
help='Disrupt the gene allocation within the module (used for comparative experiments to verify the effectiveness of module division)')
|
| 611 |
parser.add_argument('--shuffle_seed', type=int, default=2021,
|
| 612 |
help='The random seed used when scrambling the module gene (needs to be consistent with the value used during pre-training)')
|
| 613 |
|
|
@@ -638,7 +638,7 @@ def main():
|
|
| 638 |
default=os.path.join(SCRIPT_DIR, 'results'))
|
| 639 |
parser.add_argument('--seed', type=int, default=42)
|
| 640 |
parser.add_argument('--cache_emb', action='store_true', default=True,
|
| 641 |
-
help='Cache
|
| 642 |
parser.add_argument('--no_cache_emb', dest='cache_emb', action='store_false')
|
| 643 |
parser.add_argument('--force_preprocess', action='store_true', default=False,
|
| 644 |
help='Force re-creation of PyG .pt datasets')
|
|
@@ -656,7 +656,7 @@ def main():
|
|
| 656 |
|
| 657 |
os.makedirs(args.output_dir, exist_ok=True)
|
| 658 |
|
| 659 |
-
# -------- Step 1: Build
|
| 660 |
train_cell_file = os.path.join(args.data_dir, args.cell_file)
|
| 661 |
train_cell_df = load_cell_features(train_cell_file)
|
| 662 |
print_sys(f'Loaded train cell features: {train_cell_df.shape}')
|
|
@@ -737,12 +737,12 @@ def main():
|
|
| 737 |
labels_train = train_labels['label'].tolist()
|
| 738 |
|
| 739 |
train_d1_dataset = TestbedDataset(
|
| 740 |
-
root=args.data_dir, dataset='
|
| 741 |
xd=drug1_train, xt=cells_train, y=labels_train,
|
| 742 |
cell_emb_dict=cell_emb_dict, smile_graph=smile_graph,
|
| 743 |
)
|
| 744 |
train_d2_dataset = TestbedDataset(
|
| 745 |
-
root=args.data_dir, dataset='
|
| 746 |
xd=drug2_train, xt=cells_train, y=labels_train,
|
| 747 |
cell_emb_dict=cell_emb_dict, smile_graph=smile_graph,
|
| 748 |
)
|
|
@@ -755,12 +755,12 @@ def main():
|
|
| 755 |
labels_validation = validation_labels['label'].tolist()
|
| 756 |
|
| 757 |
validation_d1_dataset = TestbedDataset(
|
| 758 |
-
root=args.data_dir, dataset='
|
| 759 |
xd=drug1_validation, xt=cells_validation, y=labels_validation,
|
| 760 |
cell_emb_dict=cell_emb_dict, smile_graph=smile_graph,
|
| 761 |
)
|
| 762 |
validation_d2_dataset = TestbedDataset(
|
| 763 |
-
root=args.data_dir, dataset='
|
| 764 |
xd=drug2_validation, xt=cells_validation, y=labels_validation,
|
| 765 |
cell_emb_dict=cell_emb_dict, smile_graph=smile_graph,
|
| 766 |
)
|
|
|
|
| 1 |
#!/usr/bin/env python
|
| 2 |
# -*- coding: utf-8 -*-
|
| 3 |
"""
|
| 4 |
+
Drug Combination Synergy Prediction with BioMod cell embeddings.
|
| 5 |
|
| 6 |
Aligned with scLong/drug_combination (DeepDDs) architecture:
|
| 7 |
- Drug branch: PyG GCNConv + global_max_pool (shared weights for drug1 & drug2)
|
| 8 |
+
- Cell branch: BioMod pretrained encoder -> MLP
|
| 9 |
- Fusion: concat(drug1, drug2, cell) -> MLP -> 2-class classification
|
| 10 |
|
| 11 |
Usage:
|
| 12 |
# Run from the repository root.
|
| 13 |
+
python BioMod/drug_combination/train_drug_combination_biomod.py \
|
| 14 |
--checkpoint_path <ckpt> --hyper_params_path <pkl> \
|
| 15 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 16 |
--data_dir data/downstream/final_data/drug_combination_synergy/data_summary
|
| 17 |
"""
|
| 18 |
|
|
|
|
| 54 |
|
| 55 |
# ===================== Path setup =====================
|
| 56 |
SCRIPT_DIR = Path(__file__).resolve().parent
|
| 57 |
+
BIOMOD_ROOT = SCRIPT_DIR.parent
|
| 58 |
+
SCLONG_ROOT = BIOMOD_ROOT.parent
|
| 59 |
|
| 60 |
REPO_ROOT = Path(__file__).resolve().parents[2]
|
| 61 |
ASSET_DIR = REPO_ROOT / "model_dependencies"
|
| 62 |
DOWNSTREAM_DATA_DIR = REPO_ROOT / "data" / "downstream" / "final_data"
|
| 63 |
+
WEIGHTS_DIR = REPO_ROOT / "weight"
|
| 64 |
|
| 65 |
+
if str(BIOMOD_ROOT) not in sys.path:
|
| 66 |
+
sys.path.insert(0, str(BIOMOD_ROOT))
|
| 67 |
if str(SCLONG_ROOT) not in sys.path:
|
| 68 |
sys.path.insert(0, str(SCLONG_ROOT))
|
| 69 |
|
| 70 |
import importlib.util
|
| 71 |
|
| 72 |
+
# Import BioMod model
|
| 73 |
+
_models_path = os.path.join(BIOMOD_ROOT, 'models_biomod.py')
|
| 74 |
+
_spec = importlib.util.spec_from_file_location("models_biomod", _models_path)
|
| 75 |
_models_mod = importlib.util.module_from_spec(_spec)
|
| 76 |
_spec.loader.exec_module(_models_mod)
|
| 77 |
+
BioMod = _models_mod.BioMod
|
| 78 |
+
sanitize_biomod_config = _models_mod.sanitize_biomod_config
|
| 79 |
|
| 80 |
+
# Import biomod_dataset
|
| 81 |
+
_dataset_path = os.path.join(BIOMOD_ROOT, 'biomod_dataset.py')
|
| 82 |
+
_spec2 = importlib.util.spec_from_file_location("biomod_dataset", _dataset_path)
|
| 83 |
_module_mod = importlib.util.module_from_spec(_spec2)
|
| 84 |
_spec2.loader.exec_module(_module_mod)
|
| 85 |
+
load_biomod_data = _module_mod.load_biomod_data
|
| 86 |
|
| 87 |
|
| 88 |
# ===================== Utilities =====================
|
|
|
|
| 111 |
return reindexed
|
| 112 |
|
| 113 |
|
| 114 |
+
# ===================== BioMod Loading =====================
|
| 115 |
|
| 116 |
+
def load_biomod_model(
|
| 117 |
checkpoint_path, hyper_params_path, module_data_dir, module_data_path,
|
| 118 |
gene_list_path, gene_mapping_path, gene2vec_path, device,
|
| 119 |
shuffle_module_genes=False, shuffle_seed=2021,
|
| 120 |
):
|
| 121 |
+
print_sys(f"Loading BioMod model from: {checkpoint_path}")
|
| 122 |
|
| 123 |
with open(hyper_params_path, 'rb') as f:
|
| 124 |
model_params = pickle.load(f)
|
| 125 |
model_params.pop('module_embed_chunk_padded_gene_budget', None)
|
| 126 |
+
model_params = sanitize_biomod_config(
|
| 127 |
model_params, source=hyper_params_path, log_fn=print_sys,
|
| 128 |
)
|
| 129 |
|
|
|
|
| 133 |
|
| 134 |
if 'module_sets' not in model_params or model_params.get('module_sets') is None:
|
| 135 |
if module_data_dir or module_data_path:
|
| 136 |
+
module_sets = load_biomod_data(
|
| 137 |
module_data_path=module_data_path,
|
| 138 |
module_data_dir=module_data_dir,
|
| 139 |
gene_list_path=gene_list_path,
|
|
|
|
| 143 |
model_params['module_sets'] = module_sets
|
| 144 |
else:
|
| 145 |
raise ValueError(
|
| 146 |
+
"Model needs module_sets but neither --biomod_data_dir "
|
| 147 |
+
"nor --biomod_data_path provided"
|
| 148 |
)
|
| 149 |
|
| 150 |
if shuffle_module_genes:
|
|
|
|
| 164 |
model_params['module_sets'] = shuffled_module_sets
|
| 165 |
print_sys(f"Shuffled {len(all_gene_indices)} genes across {len(shuffled_module_sets)} modules")
|
| 166 |
|
| 167 |
+
model = BioMod(**model_params).to(device)
|
| 168 |
|
| 169 |
checkpoint = torch.load(checkpoint_path, map_location=device, weights_only=False)
|
| 170 |
state_dict = checkpoint.get('model_state_dict', checkpoint)
|
|
|
|
| 173 |
if 'go_conv' not in k and 'G_go' not in k.lower()
|
| 174 |
}
|
| 175 |
model.load_state_dict(filtered, strict=True)
|
| 176 |
+
print_sys("Loaded pretrained BioMod weights.")
|
| 177 |
|
| 178 |
model.eval()
|
| 179 |
for p in model.parameters():
|
|
|
|
| 182 |
return model, len(model_params['module_sets']), model_params.get('base_dim', 1024)
|
| 183 |
|
| 184 |
|
| 185 |
+
def build_biomod_gene_mapping(symbol2ens_path, gene_list_path, expr_genes):
|
| 186 |
gene2ens = pd.read_csv(symbol2ens_path, index_col=0)
|
| 187 |
symbol2ens = gene2ens["Ensembl gene ID"].to_dict()
|
| 188 |
|
|
|
|
| 191 |
input_mapping = {gid: i for i, gid in enumerate(input_genes)}
|
| 192 |
|
| 193 |
with open(gene_list_path, "r") as f:
|
| 194 |
+
biomod_genes = [line.rstrip("\n") for line in f.readlines()]
|
| 195 |
+
biomod_genes_pad = biomod_genes + ["PAD"]
|
| 196 |
|
| 197 |
+
biomod_index_positions = [
|
| 198 |
+
input_mapping.get(gid, input_gene_num) for gid in biomod_genes_pad
|
| 199 |
]
|
| 200 |
+
intersect = np.intersect1d(input_genes, biomod_genes)
|
| 201 |
+
print_sys(f"Target and BioMod genes intersect: {len(intersect)}")
|
| 202 |
+
return input_mapping, biomod_index_positions
|
| 203 |
|
| 204 |
|
| 205 |
def build_cell_embeddings(
|
|
|
|
| 208 |
module_data_path, device, pool_method="mean",
|
| 209 |
shuffle_module_genes=False, shuffle_seed=2021,
|
| 210 |
):
|
| 211 |
+
model, num_modules, encoder_embed_dim = load_biomod_model(
|
| 212 |
checkpoint_path, hyper_params_path, module_data_dir, module_data_path,
|
| 213 |
gene_list_path, gene_mapping_path, gene2vec_path, device,
|
| 214 |
shuffle_module_genes=shuffle_module_genes, shuffle_seed=shuffle_seed,
|
| 215 |
)
|
| 216 |
|
| 217 |
gene_columns = list(cell_features_df.columns)
|
| 218 |
+
_, biomod_index_positions = build_biomod_gene_mapping(
|
| 219 |
symbol2ens_path, gene_list_path, gene_columns,
|
| 220 |
)
|
| 221 |
|
|
|
|
| 225 |
with torch.no_grad():
|
| 226 |
for cell_line_id, row in cell_features_df.iterrows():
|
| 227 |
x = torch.tensor(row.values[None, :], dtype=torch.float32, device=device)
|
| 228 |
+
x_biomod = reindex_tensor_universal(
|
| 229 |
+
x, biomod_index_positions, dim=1, filler="0", device=device,
|
| 230 |
)
|
| 231 |
+
out = model(x_biomod, mask_ratio=0.0, return_encodings=True)
|
| 232 |
|
| 233 |
if 'module_encodings' in out:
|
| 234 |
module_enc = out['module_encodings']
|
|
|
|
| 251 |
first_emb_dim = cell_emb_np.shape[0]
|
| 252 |
|
| 253 |
print_sys(
|
| 254 |
+
f"Built BioMod embeddings for {len(cell_emb_dict)} cell lines, "
|
| 255 |
f"dim={first_emb_dim}, pool_method={pool_method}"
|
| 256 |
)
|
| 257 |
return cell_emb_dict, first_emb_dim
|
|
|
|
| 319 |
"""
|
| 320 |
PyG InMemoryDataset for drug combination prediction.
|
| 321 |
Same pattern as scLong/drug_combination/utils_test.py::TestbedDataset,
|
| 322 |
+
but uses BioMod cell embeddings instead of scfm/geneformer.
|
| 323 |
"""
|
| 324 |
|
| 325 |
def __init__(
|
|
|
|
| 412 |
|
| 413 |
Drug branch: 3-layer GCNConv (78->78->156->312) + global_max_pool + 2 FC layers
|
| 414 |
Drug1 and drug2 share weights (drug2 uses drug1_conv* layers, same as scLong).
|
| 415 |
+
Cell branch: BioMod embedding -> MLP -> output_dim
|
| 416 |
Fusion: concat(drug1, drug2, cell) -> fc1(256) -> fc2(128) -> out(2)
|
| 417 |
"""
|
| 418 |
|
|
|
|
| 432 |
self.drug1_fc_g1 = nn.Linear(num_features_xd * 4, num_features_xd * 2)
|
| 433 |
self.drug1_fc_g2 = nn.Linear(num_features_xd * 2, output_dim)
|
| 434 |
|
| 435 |
+
# Cell branch (BioMod embedding -> MLP)
|
| 436 |
self.reduction = nn.Sequential(
|
| 437 |
nn.Linear(cell_emb_dim, 512),
|
| 438 |
nn.ReLU(),
|
|
|
|
| 584 |
|
| 585 |
def main():
|
| 586 |
parser = argparse.ArgumentParser(
|
| 587 |
+
description='Drug Combination Synergy Prediction with BioMod'
|
| 588 |
)
|
| 589 |
|
| 590 |
+
# BioMod model args
|
| 591 |
parser.add_argument('--checkpoint_path', type=str, required=True,
|
| 592 |
+
help='Path to BioMod pretrained checkpoint')
|
| 593 |
parser.add_argument('--hyper_params_path', type=str, required=True,
|
| 594 |
+
help='Path to BioMod hyper parameters pkl')
|
| 595 |
+
parser.add_argument('--biomod_data_dir', '--module_data_dir', type=str, default=None,
|
| 596 |
+
help='Directory containing module data files', dest='module_data_dir')
|
| 597 |
+
parser.add_argument('--biomod_data_path', '--module_data_path', type=str, default=None,
|
| 598 |
+
help='Path to single module data file', dest='module_data_path')
|
| 599 |
parser.add_argument('--gene_list_path', type=str,
|
| 600 |
default=str(ASSET_DIR / 'selected_genes_27k.txt'))
|
| 601 |
parser.add_argument('--gene_mapping_path', type=str,
|
|
|
|
| 606 |
default=str(ASSET_DIR / 'gene2ens.csv'))
|
| 607 |
parser.add_argument('--pool_method', type=str, default='mean',
|
| 608 |
choices=['mean', 'max', 'concat'])
|
| 609 |
+
parser.add_argument('--shuffle_biomod_genes', '--shuffle_module_genes', action='store_true',
|
| 610 |
+
help='Disrupt the gene allocation within the module (used for comparative experiments to verify the effectiveness of module division)', dest='shuffle_module_genes')
|
| 611 |
parser.add_argument('--shuffle_seed', type=int, default=2021,
|
| 612 |
help='The random seed used when scrambling the module gene (needs to be consistent with the value used during pre-training)')
|
| 613 |
|
|
|
|
| 638 |
default=os.path.join(SCRIPT_DIR, 'results'))
|
| 639 |
parser.add_argument('--seed', type=int, default=42)
|
| 640 |
parser.add_argument('--cache_emb', action='store_true', default=True,
|
| 641 |
+
help='Cache BioMod cell embeddings to .npy')
|
| 642 |
parser.add_argument('--no_cache_emb', dest='cache_emb', action='store_false')
|
| 643 |
parser.add_argument('--force_preprocess', action='store_true', default=False,
|
| 644 |
help='Force re-creation of PyG .pt datasets')
|
|
|
|
| 656 |
|
| 657 |
os.makedirs(args.output_dir, exist_ok=True)
|
| 658 |
|
| 659 |
+
# -------- Step 1: Build BioMod cell embeddings --------
|
| 660 |
train_cell_file = os.path.join(args.data_dir, args.cell_file)
|
| 661 |
train_cell_df = load_cell_features(train_cell_file)
|
| 662 |
print_sys(f'Loaded train cell features: {train_cell_df.shape}')
|
|
|
|
| 737 |
labels_train = train_labels['label'].tolist()
|
| 738 |
|
| 739 |
train_d1_dataset = TestbedDataset(
|
| 740 |
+
root=args.data_dir, dataset='train_drug1_biomod',
|
| 741 |
xd=drug1_train, xt=cells_train, y=labels_train,
|
| 742 |
cell_emb_dict=cell_emb_dict, smile_graph=smile_graph,
|
| 743 |
)
|
| 744 |
train_d2_dataset = TestbedDataset(
|
| 745 |
+
root=args.data_dir, dataset='train_drug2_biomod',
|
| 746 |
xd=drug2_train, xt=cells_train, y=labels_train,
|
| 747 |
cell_emb_dict=cell_emb_dict, smile_graph=smile_graph,
|
| 748 |
)
|
|
|
|
| 755 |
labels_validation = validation_labels['label'].tolist()
|
| 756 |
|
| 757 |
validation_d1_dataset = TestbedDataset(
|
| 758 |
+
root=args.data_dir, dataset='test_drug1_biomod',
|
| 759 |
xd=drug1_validation, xt=cells_validation, y=labels_validation,
|
| 760 |
cell_emb_dict=cell_emb_dict, smile_graph=smile_graph,
|
| 761 |
)
|
| 762 |
validation_d2_dataset = TestbedDataset(
|
| 763 |
+
root=args.data_dir, dataset='test_drug2_biomod',
|
| 764 |
xd=drug2_validation, xt=cells_validation, y=labels_validation,
|
| 765 |
cell_emb_dict=cell_emb_dict, smile_graph=smile_graph,
|
| 766 |
)
|
|
@@ -1,10 +1,10 @@
|
|
| 1 |
# Run from the repository root.
|
| 2 |
-
python BioMod/drug_response/
|
| 3 |
--gpu_id 0 \
|
| 4 |
--data_path data/downstream/final_data/single_drug_response/data \
|
| 5 |
-
--checkpoint_path weight/
|
| 6 |
-
--hyper_params_path weight/
|
| 7 |
--epochs 300 \
|
| 8 |
-
--
|
| 9 |
--save_dir tutorial_runs/drug_response \
|
| 10 |
--pool_method max
|
|
|
|
| 1 |
# Run from the repository root.
|
| 2 |
+
python BioMod/drug_response/train_drug_biomod.py \
|
| 3 |
--gpu_id 0 \
|
| 4 |
--data_path data/downstream/final_data/single_drug_response/data \
|
| 5 |
+
--checkpoint_path weight/BioMod.pth \
|
| 6 |
+
--hyper_params_path weight/BioMod.pkl \
|
| 7 |
--epochs 300 \
|
| 8 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 9 |
--save_dir tutorial_runs/drug_response \
|
| 10 |
--pool_method max
|
|
@@ -1,20 +1,20 @@
|
|
| 1 |
#!/usr/bin/env python
|
| 2 |
# -*- coding: utf-8 -*-
|
| 3 |
"""
|
| 4 |
-
Drug response prediction: using drug molecular structure +
|
| 5 |
|
| 6 |
Architecture:
|
| 7 |
- Drug branch: 3-layer GCN + residual connection + double pooling
|
| 8 |
-
- Cell branch: extract module-level features from pre-trained
|
| 9 |
- Fusion layer: attention mechanism + deep MLP
|
| 10 |
- Optimization strategy: AdamW + cosine annealing + early stopping
|
| 11 |
|
| 12 |
How to use:
|
| 13 |
# Run from the repository root; see drug_response/cmd.txt for a full example.
|
| 14 |
-
python BioMod/drug_response/
|
| 15 |
--gpu_id 0 --epochs 300 \
|
| 16 |
-
--checkpoint_path weight/
|
| 17 |
-
--hyper_params_path weight/
|
| 18 |
"""
|
| 19 |
|
| 20 |
import argparse
|
|
@@ -27,7 +27,7 @@
|
|
| 27 |
REPO_ROOT = Path(__file__).resolve().parents[2]
|
| 28 |
ASSET_DIR = REPO_ROOT / "model_dependencies"
|
| 29 |
DOWNSTREAM_DATA_DIR = REPO_ROOT / "data" / "downstream" / "final_data"
|
| 30 |
-
WEIGHTS_DIR = REPO_ROOT / "weight"
|
| 31 |
from typing import Dict, List, Tuple, Union, Optional
|
| 32 |
|
| 33 |
import hickle as hkl
|
|
@@ -39,7 +39,7 @@
|
|
| 39 |
from scipy.stats import pearsonr
|
| 40 |
from torch.utils.data import DataLoader, Dataset
|
| 41 |
|
| 42 |
-
# Import
|
| 43 |
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 44 |
if project_root not in sys.path:
|
| 45 |
sys.path.insert(0, project_root)
|
|
@@ -47,26 +47,26 @@
|
|
| 47 |
|
| 48 |
# Use direct path import to avoid conflicts with files with the same name
|
| 49 |
import importlib.util
|
| 50 |
-
|
| 51 |
|
| 52 |
-
# import
|
| 53 |
-
models_path = os.path.join(
|
| 54 |
if not os.path.exists(models_path):
|
| 55 |
raise FileNotFoundError(f"Model file not found: {models_path}")
|
| 56 |
-
spec = importlib.util.spec_from_file_location("
|
| 57 |
-
|
| 58 |
-
spec.loader.exec_module(
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
# Import
|
| 63 |
-
dataset_path = os.path.join(
|
| 64 |
if not os.path.exists(dataset_path):
|
| 65 |
raise FileNotFoundError(f"Dataset file not found: {dataset_path}")
|
| 66 |
-
spec = importlib.util.spec_from_file_location("
|
| 67 |
-
|
| 68 |
-
spec.loader.exec_module(
|
| 69 |
-
|
| 70 |
|
| 71 |
|
| 72 |
# ===================== Utility functions =====================
|
|
@@ -105,9 +105,9 @@ def reindex_tensor_universal(
|
|
| 105 |
return reindexed
|
| 106 |
|
| 107 |
|
| 108 |
-
# =====================
|
| 109 |
|
| 110 |
-
def
|
| 111 |
checkpoint_path: str,
|
| 112 |
hyper_params_path: str,
|
| 113 |
module_data_dir: Optional[str],
|
|
@@ -118,16 +118,16 @@ def load_performer_module_model(
|
|
| 118 |
device: torch.device,
|
| 119 |
shuffle_module_genes: bool = False,
|
| 120 |
seed: int = 0,
|
| 121 |
-
) ->
|
| 122 |
-
"""Load a pretrained
|
| 123 |
-
print_sys(f"Loading
|
| 124 |
|
| 125 |
# Load model hyperparameters
|
| 126 |
print_sys(f"Loading hyper parameters from: {hyper_params_path}")
|
| 127 |
with open(hyper_params_path, 'rb') as f:
|
| 128 |
model_params = pickle.load(f)
|
| 129 |
model_params.pop('module_embed_chunk_padded_gene_budget', None)
|
| 130 |
-
model_params =
|
| 131 |
model_params,
|
| 132 |
source=hyper_params_path,
|
| 133 |
log_fn=print_sys,
|
|
@@ -150,7 +150,7 @@ def load_performer_module_model(
|
|
| 150 |
if 'module_sets' not in model_params or model_params.get('module_sets') is None:
|
| 151 |
if module_data_dir or module_data_path:
|
| 152 |
print_sys("Loading module data...")
|
| 153 |
-
module_sets =
|
| 154 |
module_data_path=module_data_path,
|
| 155 |
module_data_dir=module_data_dir,
|
| 156 |
gene_list_path=gene_list_path,
|
|
@@ -159,7 +159,7 @@ def load_performer_module_model(
|
|
| 159 |
)
|
| 160 |
model_params['module_sets'] = module_sets
|
| 161 |
else:
|
| 162 |
-
raise ValueError("Model requires module_sets, but --
|
| 163 |
else:
|
| 164 |
module_sets = model_params['module_sets']
|
| 165 |
print_sys(f"Using module_sets from hyper parameters ({len(module_sets)} modules)")
|
|
@@ -191,11 +191,11 @@ def load_performer_module_model(
|
|
| 191 |
model_params['module_sets'] = module_sets
|
| 192 |
print_sys(f"Shuffled {len(all_gene_indices)} genes across {len(module_sets)} modules")
|
| 193 |
|
| 194 |
-
print_sys(f"
|
| 195 |
f"num_modules={len(module_sets)}")
|
| 196 |
|
| 197 |
# Build model
|
| 198 |
-
model =
|
| 199 |
|
| 200 |
# Load weights
|
| 201 |
print_sys(f"Loading checkpoint from: {checkpoint_path}")
|
|
@@ -216,7 +216,7 @@ def load_performer_module_model(
|
|
| 216 |
# Use strict=True to ensure the weights match exactly (except for filtered GO parameters)
|
| 217 |
model.load_state_dict(filtered_state_dict, strict=True)
|
| 218 |
|
| 219 |
-
print_sys("Loaded pretrained
|
| 220 |
|
| 221 |
# freeze model
|
| 222 |
model.eval()
|
|
@@ -226,7 +226,7 @@ def load_performer_module_model(
|
|
| 226 |
return model, len(module_sets), model_params.get('base_dim', 1024)
|
| 227 |
|
| 228 |
|
| 229 |
-
def
|
| 230 |
symbol2ens_path: str,
|
| 231 |
gene_list_path: str,
|
| 232 |
expr_genes: List[str],
|
|
@@ -240,18 +240,18 @@ def build_performer_module_gene_mapping(
|
|
| 240 |
input_mapping = {gid: i for i, gid in enumerate(input_genes)}
|
| 241 |
|
| 242 |
with open(gene_list_path, "r") as f:
|
| 243 |
-
|
| 244 |
-
|
| 245 |
|
| 246 |
-
|
| 247 |
-
input_mapping.get(gid, input_gene_num) for gid in
|
| 248 |
]
|
| 249 |
-
intersect = np.intersect1d(input_genes,
|
| 250 |
-
print_sys(f"Target and
|
| 251 |
-
return input_mapping,
|
| 252 |
|
| 253 |
|
| 254 |
-
def
|
| 255 |
gexpr_feature: pd.DataFrame,
|
| 256 |
symbol2ens_path: str,
|
| 257 |
gene_list_path: str,
|
|
@@ -267,7 +267,7 @@ def build_cell_embeddings_with_performer_module(
|
|
| 267 |
seed: int = 0,
|
| 268 |
) -> Tuple[Dict[str, np.ndarray], int]:
|
| 269 |
"""
|
| 270 |
-
Use
|
| 271 |
|
| 272 |
Args:
|
| 273 |
pool_method: pooling method, 'mean', 'max', 'cls', or 'concat'
|
|
@@ -277,7 +277,7 @@ def build_cell_embeddings_with_performer_module(
|
|
| 277 |
- 'concat': concatenate all modules (the dimension will be very large)
|
| 278 |
use_gene2vec: automatically restored by pre-trained PKL, no longer covered downstream
|
| 279 |
"""
|
| 280 |
-
model, num_modules, encoder_embed_dim =
|
| 281 |
checkpoint_path=checkpoint_path,
|
| 282 |
hyper_params_path=hyper_params_path,
|
| 283 |
module_data_dir=module_data_dir,
|
|
@@ -290,7 +290,7 @@ def build_cell_embeddings_with_performer_module(
|
|
| 290 |
seed=seed,
|
| 291 |
)
|
| 292 |
|
| 293 |
-
_,
|
| 294 |
symbol2ens_path=symbol2ens_path,
|
| 295 |
gene_list_path=gene_list_path,
|
| 296 |
expr_genes=list(gexpr_feature.columns),
|
|
@@ -302,12 +302,12 @@ def build_cell_embeddings_with_performer_module(
|
|
| 302 |
with torch.no_grad():
|
| 303 |
for cell_line_id, row in gexpr_feature.iterrows():
|
| 304 |
x = torch.tensor(row.values[None, :], dtype=torch.float32, device=device)
|
| 305 |
-
|
| 306 |
-
x,
|
| 307 |
)
|
| 308 |
|
| 309 |
# Extract features through encoder (without masking, mask_ratio=0.0)
|
| 310 |
-
out = model(
|
| 311 |
|
| 312 |
# Extract module encodings
|
| 313 |
if 'module_encodings' in out:
|
|
@@ -318,7 +318,7 @@ def build_cell_embeddings_with_performer_module(
|
|
| 318 |
if model.use_cls:
|
| 319 |
# Use CLS token (in latent, but module_encodings does not contain CLS)
|
| 320 |
# Need to get the complete latent from forward_encoder
|
| 321 |
-
enc_out = model.forward_encoder(
|
| 322 |
latent = enc_out[0]
|
| 323 |
if model.use_cls:
|
| 324 |
cell_emb = latent[:, 0, :] # (1, encoder_embed_dim)
|
|
@@ -345,7 +345,7 @@ def build_cell_embeddings_with_performer_module(
|
|
| 345 |
first_emb_dim = cell_emb_np.shape[0]
|
| 346 |
|
| 347 |
print_sys(
|
| 348 |
-
f"Built
|
| 349 |
f"dim={first_emb_dim}, pool_method={pool_method}"
|
| 350 |
)
|
| 351 |
return cell_emb_dict, first_emb_dim
|
|
@@ -525,8 +525,8 @@ def feature_extract(
|
|
| 525 |
|
| 526 |
# ===================== Dataset =====================
|
| 527 |
|
| 528 |
-
class
|
| 529 |
-
"""Drug +
|
| 530 |
|
| 531 |
def __init__(
|
| 532 |
self,
|
|
@@ -550,7 +550,7 @@ def __getitem__(self, idx: int):
|
|
| 550 |
drug_feat = torch.from_numpy(feat).float()
|
| 551 |
drug_adj = torch.from_numpy(adj).float()
|
| 552 |
|
| 553 |
-
#
|
| 554 |
cell_line_id = self.index_list[idx][0]
|
| 555 |
cell_emb_np = self.cell_emb_dict[cell_line_id]
|
| 556 |
cell_emb = torch.from_numpy(cell_emb_np).float()
|
|
@@ -595,13 +595,13 @@ def forward(self, x: torch.Tensor, adj: torch.Tensor) -> torch.Tensor:
|
|
| 595 |
return out
|
| 596 |
|
| 597 |
|
| 598 |
-
class
|
| 599 |
"""
|
| 600 |
-
Drugs +
|
| 601 |
|
| 602 |
Architecture:
|
| 603 |
- Drug branch: 3-layer GraphConv + residual connection + double pooling
|
| 604 |
-
- Cell branch: 2-layer MLP (processing
|
| 605 |
- Fusion layer: 4-layer MLP + attention mechanism
|
| 606 |
"""
|
| 607 |
|
|
@@ -624,7 +624,7 @@ def __init__(
|
|
| 624 |
self.drug_bn2 = nn.BatchNorm1d(256)
|
| 625 |
self.drug_bn3 = nn.BatchNorm1d(128)
|
| 626 |
|
| 627 |
-
# Cell branch: 2-layer MLP (processing
|
| 628 |
self.cell_fc1 = nn.Linear(cell_emb_dim, 256)
|
| 629 |
self.cell_bn1 = nn.BatchNorm1d(256)
|
| 630 |
self.cell_fc2 = nn.Linear(256, 128)
|
|
@@ -689,7 +689,7 @@ def forward(
|
|
| 689 |
x_drug_mean = x_drug.mean(dim=1) # (B, 128)
|
| 690 |
x_drug = x_drug_max + x_drug_mean # Fusion
|
| 691 |
|
| 692 |
-
# Cell branching (handling
|
| 693 |
x_cell = self.cell_fc1(cell_emb) # (B, 256)
|
| 694 |
x_cell = self.cell_bn1(x_cell)
|
| 695 |
x_cell = self.relu(x_cell)
|
|
@@ -783,7 +783,7 @@ def eval_epoch(model, loader, device):
|
|
| 783 |
|
| 784 |
def main():
|
| 785 |
parser = argparse.ArgumentParser(
|
| 786 |
-
description="Drug response prediction using Drug +
|
| 787 |
)
|
| 788 |
parser.add_argument("--gpu_id", type=str, default="0")
|
| 789 |
parser.add_argument("--data_path", type=str, default=str(DOWNSTREAM_DATA_DIR / "single_drug_response" / "data"))
|
|
@@ -795,31 +795,31 @@ def main():
|
|
| 795 |
parser.add_argument("--dropout", type=float, default=0.1)
|
| 796 |
parser.add_argument("--patience", type=int, default=30)
|
| 797 |
|
| 798 |
-
#
|
| 799 |
parser.add_argument(
|
| 800 |
"--checkpoint_path",
|
| 801 |
type=str,
|
| 802 |
required=True,
|
| 803 |
-
help="Path to pre-trained
|
| 804 |
)
|
| 805 |
parser.add_argument(
|
| 806 |
"--hyper_params_path",
|
| 807 |
type=str,
|
| 808 |
required=True,
|
| 809 |
-
help="Path to
|
| 810 |
)
|
| 811 |
parser.add_argument(
|
| 812 |
-
"--module_data_dir",
|
| 813 |
type=str,
|
| 814 |
default=None,
|
| 815 |
help="Directory containing module JSON files"
|
| 816 |
-
)
|
| 817 |
parser.add_argument(
|
| 818 |
-
"--module_data_path",
|
| 819 |
type=str,
|
| 820 |
default=None,
|
| 821 |
help="Path to module JSON file (single file)"
|
| 822 |
-
)
|
| 823 |
parser.add_argument(
|
| 824 |
"--gene_list_path",
|
| 825 |
type=str,
|
|
@@ -849,25 +849,25 @@ def main():
|
|
| 849 |
type=str,
|
| 850 |
default="mean",
|
| 851 |
choices=["mean", "max", "cls", "concat"],
|
| 852 |
-
help="Pooling method for
|
| 853 |
)
|
| 854 |
parser.add_argument(
|
| 855 |
-
"--shuffle_module_genes",
|
| 856 |
action="store_true",
|
| 857 |
help="Optional downstream-only extra reshuffle on top of module_sets restored from PKL. "
|
| 858 |
"Do not pass this when reproducing the pretrained backbone as-is."
|
| 859 |
-
)
|
| 860 |
parser.add_argument(
|
| 861 |
"--shuffle_seed",
|
| 862 |
type=int,
|
| 863 |
default=2021,
|
| 864 |
-
help="Random seed for the downstream extra reshuffle when --
|
| 865 |
)
|
| 866 |
# Output path
|
| 867 |
parser.add_argument(
|
| 868 |
"--save_dir",
|
| 869 |
type=str,
|
| 870 |
-
default="./
|
| 871 |
help="Directory to save the model"
|
| 872 |
)
|
| 873 |
|
|
@@ -877,7 +877,7 @@ def main():
|
|
| 877 |
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 878 |
|
| 879 |
print_sys("=" * 70)
|
| 880 |
-
print_sys("Drug Response Prediction: Drug +
|
| 881 |
print_sys("=" * 70)
|
| 882 |
print_sys(f"Device: {device}")
|
| 883 |
print_sys(f"Epochs: {args.epochs}")
|
|
@@ -916,9 +916,9 @@ def main():
|
|
| 916 |
gene_expression_file,
|
| 917 |
)
|
| 918 |
|
| 919 |
-
# 2) Build
|
| 920 |
-
print_sys("\n[Step 2/6] Building
|
| 921 |
-
cell_emb_dict, cell_emb_dim =
|
| 922 |
gexpr_feature=gexpr_feature,
|
| 923 |
symbol2ens_path=args.symbol2ens_path,
|
| 924 |
gene_list_path=args.gene_list_path,
|
|
@@ -950,10 +950,10 @@ def main():
|
|
| 950 |
|
| 951 |
# 5) Build a data set
|
| 952 |
print_sys("\n[Step 5/6] Creating datasets...")
|
| 953 |
-
train_ds =
|
| 954 |
X_drug_train, Y_train, train_index_list, cell_emb_dict
|
| 955 |
)
|
| 956 |
-
validation_ds =
|
| 957 |
X_drug_validation, Y_validation, validation_index_list, cell_emb_dict
|
| 958 |
)
|
| 959 |
|
|
@@ -977,7 +977,7 @@ def main():
|
|
| 977 |
|
| 978 |
# 6) Build the model
|
| 979 |
print_sys("\n[Step 6/6] Building model...")
|
| 980 |
-
model =
|
| 981 |
drug_feat_dim=drug_dim,
|
| 982 |
cell_emb_dim=cell_emb_dim,
|
| 983 |
hidden_dim=args.hidden_dim,
|
|
|
|
| 1 |
#!/usr/bin/env python
|
| 2 |
# -*- coding: utf-8 -*-
|
| 3 |
"""
|
| 4 |
+
Drug response prediction: using drug molecular structure + BioMod cell embedding
|
| 5 |
|
| 6 |
Architecture:
|
| 7 |
- Drug branch: 3-layer GCN + residual connection + double pooling
|
| 8 |
+
- Cell branch: extract module-level features from pre-trained BioMod
|
| 9 |
- Fusion layer: attention mechanism + deep MLP
|
| 10 |
- Optimization strategy: AdamW + cosine annealing + early stopping
|
| 11 |
|
| 12 |
How to use:
|
| 13 |
# Run from the repository root; see drug_response/cmd.txt for a full example.
|
| 14 |
+
python BioMod/drug_response/train_drug_biomod.py \
|
| 15 |
--gpu_id 0 --epochs 300 \
|
| 16 |
+
--checkpoint_path weight/BioMod.pth \
|
| 17 |
+
--hyper_params_path weight/BioMod.pkl
|
| 18 |
"""
|
| 19 |
|
| 20 |
import argparse
|
|
|
|
| 27 |
REPO_ROOT = Path(__file__).resolve().parents[2]
|
| 28 |
ASSET_DIR = REPO_ROOT / "model_dependencies"
|
| 29 |
DOWNSTREAM_DATA_DIR = REPO_ROOT / "data" / "downstream" / "final_data"
|
| 30 |
+
WEIGHTS_DIR = REPO_ROOT / "weight"
|
| 31 |
from typing import Dict, List, Tuple, Union, Optional
|
| 32 |
|
| 33 |
import hickle as hkl
|
|
|
|
| 39 |
from scipy.stats import pearsonr
|
| 40 |
from torch.utils.data import DataLoader, Dataset
|
| 41 |
|
| 42 |
+
# Import BioMod model
|
| 43 |
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 44 |
if project_root not in sys.path:
|
| 45 |
sys.path.insert(0, project_root)
|
|
|
|
| 47 |
|
| 48 |
# Use direct path import to avoid conflicts with files with the same name
|
| 49 |
import importlib.util
|
| 50 |
+
biomod_dir = project_root
|
| 51 |
|
| 52 |
+
# import models_biomod
|
| 53 |
+
models_path = os.path.join(biomod_dir, 'models_biomod.py')
|
| 54 |
if not os.path.exists(models_path):
|
| 55 |
raise FileNotFoundError(f"Model file not found: {models_path}")
|
| 56 |
+
spec = importlib.util.spec_from_file_location("models_biomod", models_path)
|
| 57 |
+
models_biomod = importlib.util.module_from_spec(spec)
|
| 58 |
+
spec.loader.exec_module(models_biomod)
|
| 59 |
+
BioMod = models_biomod.BioMod
|
| 60 |
+
sanitize_biomod_config = models_biomod.sanitize_biomod_config
|
| 61 |
+
|
| 62 |
+
# Import biomod_dataset
|
| 63 |
+
dataset_path = os.path.join(biomod_dir, 'biomod_dataset.py')
|
| 64 |
if not os.path.exists(dataset_path):
|
| 65 |
raise FileNotFoundError(f"Dataset file not found: {dataset_path}")
|
| 66 |
+
spec = importlib.util.spec_from_file_location("biomod_dataset", dataset_path)
|
| 67 |
+
biomod_dataset_module = importlib.util.module_from_spec(spec)
|
| 68 |
+
spec.loader.exec_module(biomod_dataset_module)
|
| 69 |
+
load_biomod_data = biomod_dataset_module.load_biomod_data
|
| 70 |
|
| 71 |
|
| 72 |
# ===================== Utility functions =====================
|
|
|
|
| 105 |
return reindexed
|
| 106 |
|
| 107 |
|
| 108 |
+
# ===================== BioMod related =====================
|
| 109 |
|
| 110 |
+
def load_biomod_model(
|
| 111 |
checkpoint_path: str,
|
| 112 |
hyper_params_path: str,
|
| 113 |
module_data_dir: Optional[str],
|
|
|
|
| 118 |
device: torch.device,
|
| 119 |
shuffle_module_genes: bool = False,
|
| 120 |
seed: int = 0,
|
| 121 |
+
) -> BioMod:
|
| 122 |
+
"""Load a pretrained BioMod model"""
|
| 123 |
+
print_sys(f"Loading BioMod model from: {checkpoint_path}")
|
| 124 |
|
| 125 |
# Load model hyperparameters
|
| 126 |
print_sys(f"Loading hyper parameters from: {hyper_params_path}")
|
| 127 |
with open(hyper_params_path, 'rb') as f:
|
| 128 |
model_params = pickle.load(f)
|
| 129 |
model_params.pop('module_embed_chunk_padded_gene_budget', None)
|
| 130 |
+
model_params = sanitize_biomod_config(
|
| 131 |
model_params,
|
| 132 |
source=hyper_params_path,
|
| 133 |
log_fn=print_sys,
|
|
|
|
| 150 |
if 'module_sets' not in model_params or model_params.get('module_sets') is None:
|
| 151 |
if module_data_dir or module_data_path:
|
| 152 |
print_sys("Loading module data...")
|
| 153 |
+
module_sets = load_biomod_data(
|
| 154 |
module_data_path=module_data_path,
|
| 155 |
module_data_dir=module_data_dir,
|
| 156 |
gene_list_path=gene_list_path,
|
|
|
|
| 159 |
)
|
| 160 |
model_params['module_sets'] = module_sets
|
| 161 |
else:
|
| 162 |
+
raise ValueError("Model requires module_sets, but --biomod_data_dir or --biomod_data_path was not provided")
|
| 163 |
else:
|
| 164 |
module_sets = model_params['module_sets']
|
| 165 |
print_sys(f"Using module_sets from hyper parameters ({len(module_sets)} modules)")
|
|
|
|
| 191 |
model_params['module_sets'] = module_sets
|
| 192 |
print_sys(f"Shuffled {len(all_gene_indices)} genes across {len(module_sets)} modules")
|
| 193 |
|
| 194 |
+
print_sys(f"BioMod config: base_dim={model_params.get('base_dim', 'N/A')}, "
|
| 195 |
f"num_modules={len(module_sets)}")
|
| 196 |
|
| 197 |
# Build model
|
| 198 |
+
model = BioMod(**model_params).to(device)
|
| 199 |
|
| 200 |
# Load weights
|
| 201 |
print_sys(f"Loading checkpoint from: {checkpoint_path}")
|
|
|
|
| 216 |
# Use strict=True to ensure the weights match exactly (except for filtered GO parameters)
|
| 217 |
model.load_state_dict(filtered_state_dict, strict=True)
|
| 218 |
|
| 219 |
+
print_sys("Loaded pretrained BioMod weights.")
|
| 220 |
|
| 221 |
# freeze model
|
| 222 |
model.eval()
|
|
|
|
| 226 |
return model, len(module_sets), model_params.get('base_dim', 1024)
|
| 227 |
|
| 228 |
|
| 229 |
+
def build_biomod_gene_mapping(
|
| 230 |
symbol2ens_path: str,
|
| 231 |
gene_list_path: str,
|
| 232 |
expr_genes: List[str],
|
|
|
|
| 240 |
input_mapping = {gid: i for i, gid in enumerate(input_genes)}
|
| 241 |
|
| 242 |
with open(gene_list_path, "r") as f:
|
| 243 |
+
biomod_genes = [line.rstrip("\n") for line in f.readlines()]
|
| 244 |
+
biomod_genes_pad = biomod_genes + ["PAD"]
|
| 245 |
|
| 246 |
+
biomod_index_positions = [
|
| 247 |
+
input_mapping.get(gid, input_gene_num) for gid in biomod_genes_pad
|
| 248 |
]
|
| 249 |
+
intersect = np.intersect1d(input_genes, biomod_genes)
|
| 250 |
+
print_sys(f"Target and BioMod genes intersect: {len(intersect)}")
|
| 251 |
+
return input_mapping, biomod_index_positions
|
| 252 |
|
| 253 |
|
| 254 |
+
def build_cell_embeddings_with_biomod(
|
| 255 |
gexpr_feature: pd.DataFrame,
|
| 256 |
symbol2ens_path: str,
|
| 257 |
gene_list_path: str,
|
|
|
|
| 267 |
seed: int = 0,
|
| 268 |
) -> Tuple[Dict[str, np.ndarray], int]:
|
| 269 |
"""
|
| 270 |
+
Use BioMod to construct embedding for each cell line
|
| 271 |
|
| 272 |
Args:
|
| 273 |
pool_method: pooling method, 'mean', 'max', 'cls', or 'concat'
|
|
|
|
| 277 |
- 'concat': concatenate all modules (the dimension will be very large)
|
| 278 |
use_gene2vec: automatically restored by pre-trained PKL, no longer covered downstream
|
| 279 |
"""
|
| 280 |
+
model, num_modules, encoder_embed_dim = load_biomod_model(
|
| 281 |
checkpoint_path=checkpoint_path,
|
| 282 |
hyper_params_path=hyper_params_path,
|
| 283 |
module_data_dir=module_data_dir,
|
|
|
|
| 290 |
seed=seed,
|
| 291 |
)
|
| 292 |
|
| 293 |
+
_, biomod_index_positions = build_biomod_gene_mapping(
|
| 294 |
symbol2ens_path=symbol2ens_path,
|
| 295 |
gene_list_path=gene_list_path,
|
| 296 |
expr_genes=list(gexpr_feature.columns),
|
|
|
|
| 302 |
with torch.no_grad():
|
| 303 |
for cell_line_id, row in gexpr_feature.iterrows():
|
| 304 |
x = torch.tensor(row.values[None, :], dtype=torch.float32, device=device)
|
| 305 |
+
x_biomod = reindex_tensor_universal(
|
| 306 |
+
x, biomod_index_positions, dim=1, filler="0", device=device
|
| 307 |
)
|
| 308 |
|
| 309 |
# Extract features through encoder (without masking, mask_ratio=0.0)
|
| 310 |
+
out = model(x_biomod, mask_ratio=0.0, return_encodings=True)
|
| 311 |
|
| 312 |
# Extract module encodings
|
| 313 |
if 'module_encodings' in out:
|
|
|
|
| 318 |
if model.use_cls:
|
| 319 |
# Use CLS token (in latent, but module_encodings does not contain CLS)
|
| 320 |
# Need to get the complete latent from forward_encoder
|
| 321 |
+
enc_out = model.forward_encoder(x_biomod, mask_ratio=0.0)
|
| 322 |
latent = enc_out[0]
|
| 323 |
if model.use_cls:
|
| 324 |
cell_emb = latent[:, 0, :] # (1, encoder_embed_dim)
|
|
|
|
| 345 |
first_emb_dim = cell_emb_np.shape[0]
|
| 346 |
|
| 347 |
print_sys(
|
| 348 |
+
f"Built BioMod embeddings for {len(cell_emb_dict)} cell lines, "
|
| 349 |
f"dim={first_emb_dim}, pool_method={pool_method}"
|
| 350 |
)
|
| 351 |
return cell_emb_dict, first_emb_dim
|
|
|
|
| 525 |
|
| 526 |
# ===================== Dataset =====================
|
| 527 |
|
| 528 |
+
class DrugBioModDataset(Dataset):
|
| 529 |
+
"""Drug + BioMod Dataset"""
|
| 530 |
|
| 531 |
def __init__(
|
| 532 |
self,
|
|
|
|
| 550 |
drug_feat = torch.from_numpy(feat).float()
|
| 551 |
drug_adj = torch.from_numpy(adj).float()
|
| 552 |
|
| 553 |
+
# BioMod cell embedding
|
| 554 |
cell_line_id = self.index_list[idx][0]
|
| 555 |
cell_emb_np = self.cell_emb_dict[cell_line_id]
|
| 556 |
cell_emb = torch.from_numpy(cell_emb_np).float()
|
|
|
|
| 595 |
return out
|
| 596 |
|
| 597 |
|
| 598 |
+
class DrugBioModModel(nn.Module):
|
| 599 |
"""
|
| 600 |
+
Drugs + BioMod Predictive Model
|
| 601 |
|
| 602 |
Architecture:
|
| 603 |
- Drug branch: 3-layer GraphConv + residual connection + double pooling
|
| 604 |
+
- Cell branch: 2-layer MLP (processing BioMod module embeddings)
|
| 605 |
- Fusion layer: 4-layer MLP + attention mechanism
|
| 606 |
"""
|
| 607 |
|
|
|
|
| 624 |
self.drug_bn2 = nn.BatchNorm1d(256)
|
| 625 |
self.drug_bn3 = nn.BatchNorm1d(128)
|
| 626 |
|
| 627 |
+
# Cell branch: 2-layer MLP (processing BioMod embeddings)
|
| 628 |
self.cell_fc1 = nn.Linear(cell_emb_dim, 256)
|
| 629 |
self.cell_bn1 = nn.BatchNorm1d(256)
|
| 630 |
self.cell_fc2 = nn.Linear(256, 128)
|
|
|
|
| 689 |
x_drug_mean = x_drug.mean(dim=1) # (B, 128)
|
| 690 |
x_drug = x_drug_max + x_drug_mean # Fusion
|
| 691 |
|
| 692 |
+
# Cell branching (handling BioMod embeddings)
|
| 693 |
x_cell = self.cell_fc1(cell_emb) # (B, 256)
|
| 694 |
x_cell = self.cell_bn1(x_cell)
|
| 695 |
x_cell = self.relu(x_cell)
|
|
|
|
| 783 |
|
| 784 |
def main():
|
| 785 |
parser = argparse.ArgumentParser(
|
| 786 |
+
description="Drug response prediction using Drug + BioMod"
|
| 787 |
)
|
| 788 |
parser.add_argument("--gpu_id", type=str, default="0")
|
| 789 |
parser.add_argument("--data_path", type=str, default=str(DOWNSTREAM_DATA_DIR / "single_drug_response" / "data"))
|
|
|
|
| 795 |
parser.add_argument("--dropout", type=float, default=0.1)
|
| 796 |
parser.add_argument("--patience", type=int, default=30)
|
| 797 |
|
| 798 |
+
# BioMod related paths
|
| 799 |
parser.add_argument(
|
| 800 |
"--checkpoint_path",
|
| 801 |
type=str,
|
| 802 |
required=True,
|
| 803 |
+
help="Path to pre-trained BioMod checkpoint"
|
| 804 |
)
|
| 805 |
parser.add_argument(
|
| 806 |
"--hyper_params_path",
|
| 807 |
type=str,
|
| 808 |
required=True,
|
| 809 |
+
help="Path to BioMod model hyper parameters pkl file"
|
| 810 |
)
|
| 811 |
parser.add_argument(
|
| 812 |
+
"--biomod_data_dir", "--module_data_dir",
|
| 813 |
type=str,
|
| 814 |
default=None,
|
| 815 |
help="Directory containing module JSON files"
|
| 816 |
+
, dest='module_data_dir')
|
| 817 |
parser.add_argument(
|
| 818 |
+
"--biomod_data_path", "--module_data_path",
|
| 819 |
type=str,
|
| 820 |
default=None,
|
| 821 |
help="Path to module JSON file (single file)"
|
| 822 |
+
, dest='module_data_path')
|
| 823 |
parser.add_argument(
|
| 824 |
"--gene_list_path",
|
| 825 |
type=str,
|
|
|
|
| 849 |
type=str,
|
| 850 |
default="mean",
|
| 851 |
choices=["mean", "max", "cls", "concat"],
|
| 852 |
+
help="Pooling method for BioMod module embeddings"
|
| 853 |
)
|
| 854 |
parser.add_argument(
|
| 855 |
+
"--shuffle_biomod_genes", "--shuffle_module_genes",
|
| 856 |
action="store_true",
|
| 857 |
help="Optional downstream-only extra reshuffle on top of module_sets restored from PKL. "
|
| 858 |
"Do not pass this when reproducing the pretrained backbone as-is."
|
| 859 |
+
, dest='shuffle_module_genes')
|
| 860 |
parser.add_argument(
|
| 861 |
"--shuffle_seed",
|
| 862 |
type=int,
|
| 863 |
default=2021,
|
| 864 |
+
help="Random seed for the downstream extra reshuffle when --shuffle_biomod_genes is enabled."
|
| 865 |
)
|
| 866 |
# Output path
|
| 867 |
parser.add_argument(
|
| 868 |
"--save_dir",
|
| 869 |
type=str,
|
| 870 |
+
default="./models_drug_biomod",
|
| 871 |
help="Directory to save the model"
|
| 872 |
)
|
| 873 |
|
|
|
|
| 877 |
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 878 |
|
| 879 |
print_sys("=" * 70)
|
| 880 |
+
print_sys("Drug Response Prediction: Drug + BioMod")
|
| 881 |
print_sys("=" * 70)
|
| 882 |
print_sys(f"Device: {device}")
|
| 883 |
print_sys(f"Epochs: {args.epochs}")
|
|
|
|
| 916 |
gene_expression_file,
|
| 917 |
)
|
| 918 |
|
| 919 |
+
# 2) Build BioMod embeddings
|
| 920 |
+
print_sys("\n[Step 2/6] Building BioMod embeddings...")
|
| 921 |
+
cell_emb_dict, cell_emb_dim = build_cell_embeddings_with_biomod( # cell_emb_dict │ Dict[str, np.ndarray] │ {cell_line_id: embedding} mapping dictionary
|
| 922 |
gexpr_feature=gexpr_feature,
|
| 923 |
symbol2ens_path=args.symbol2ens_path,
|
| 924 |
gene_list_path=args.gene_list_path,
|
|
|
|
| 950 |
|
| 951 |
# 5) Build a data set
|
| 952 |
print_sys("\n[Step 5/6] Creating datasets...")
|
| 953 |
+
train_ds = DrugBioModDataset(
|
| 954 |
X_drug_train, Y_train, train_index_list, cell_emb_dict
|
| 955 |
)
|
| 956 |
+
validation_ds = DrugBioModDataset(
|
| 957 |
X_drug_validation, Y_validation, validation_index_list, cell_emb_dict
|
| 958 |
)
|
| 959 |
|
|
|
|
| 977 |
|
| 978 |
# 6) Build the model
|
| 979 |
print_sys("\n[Step 6/6] Building model...")
|
| 980 |
+
model = DrugBioModModel(
|
| 981 |
drug_feat_dim=drug_dim,
|
| 982 |
cell_emb_dim=cell_emb_dim,
|
| 983 |
hidden_dim=args.hidden_dim,
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
name:
|
| 2 |
channels:
|
| 3 |
- pytorch
|
| 4 |
- nvidia
|
|
@@ -45,7 +45,7 @@ dependencies:
|
|
| 45 |
- rdkit
|
| 46 |
|
| 47 |
- pip:
|
| 48 |
-
#
|
| 49 |
- einops==0.8.1
|
| 50 |
- local-attention==1.11.2
|
| 51 |
- hickle>=5.0.0
|
|
|
|
| 1 |
+
name: biomod
|
| 2 |
channels:
|
| 3 |
- pytorch
|
| 4 |
- nvidia
|
|
|
|
| 45 |
- rdkit
|
| 46 |
|
| 47 |
- pip:
|
| 48 |
+
# BioMod model utilities
|
| 49 |
- einops==0.8.1
|
| 50 |
- local-attention==1.11.2
|
| 51 |
- hickle>=5.0.0
|
|
@@ -1,14 +1,14 @@
|
|
| 1 |
# Gene Perturbation Prediction with BioMod
|
| 2 |
|
| 3 |
This document describes the BioMod gene perturbation workflow (GEARS), using
|
| 4 |
-
the `
|
| 5 |
[BioMod model weights and matching configuration](../../README.md#biomod-model-weights)
|
| 6 |
are shared with the other downstream tasks.
|
| 7 |
|
| 8 |
## Overview
|
| 9 |
|
| 10 |
This adaptation replaces the original GEARS gene perturbation model based on
|
| 11 |
-
`DualEncoderSCFM` with `
|
| 12 |
are:
|
| 13 |
|
| 14 |
1. **Model architecture adaptation**: replace the original dual-encoder model with a module-based encoder-only model.
|
|
@@ -17,12 +17,12 @@ are:
|
|
| 17 |
|
| 18 |
## Main Adaptations
|
| 19 |
|
| 20 |
-
### 1. `
|
| 21 |
|
| 22 |
-
`
|
| 23 |
|
| 24 |
- **Input handling**: reindex input gene expression values to the BioMod gene order.
|
| 25 |
-
- **Module encoding**: obtain module-level encodings from `
|
| 26 |
- **Gene embedding recovery**: recover gene-level embeddings from module encodings.
|
| 27 |
- For each gene, find all modules that contain it.
|
| 28 |
- Average the encodings of those modules to obtain the gene embedding.
|
|
@@ -42,22 +42,22 @@ for gene_idx in range(len(self.scfm_index_positions) - 1):
|
|
| 42 |
gene_emb = module_encodings.mean(dim=1) # B, D (global average)
|
| 43 |
```
|
| 44 |
|
| 45 |
-
### 2. `
|
| 46 |
|
| 47 |
-
`
|
| 48 |
|
| 49 |
-
- **Use `
|
| 50 |
- **Module data loading** supports loading module data from hyper-parameters or file paths.
|
| 51 |
- **Other components remain unchanged**, including the GNN layers and decoder.
|
| 52 |
|
| 53 |
-
### 3. Training Script (`
|
| 54 |
|
| 55 |
The training script is based on `GEARS/train_dual_ddp_reduce_dec.py`. Main changes:
|
| 56 |
|
| 57 |
-
- **Model class**: use `
|
| 58 |
- **New arguments**:
|
| 59 |
-
- `--
|
| 60 |
-
- `--
|
| 61 |
- `--gene_mapping_path`: CSV file mapping gene symbols to Ensembl IDs.
|
| 62 |
|
| 63 |
## Usage
|
|
@@ -78,12 +78,12 @@ Run from the repository root in a Linux CUDA/NCCL environment. Built-in asset de
|
|
| 78 |
```bash
|
| 79 |
# DDP training on multiple GPUs.
|
| 80 |
python -m torch.distributed.run --standalone --nproc_per_node=4 \
|
| 81 |
-
BioMod/gene_perturbation/
|
| 82 |
--data_dir data/downstream/final_data \
|
| 83 |
--data_name norman \
|
| 84 |
-
--scfm_hyper_params_path weight/
|
| 85 |
-
--scfm_ckpt_path weight/
|
| 86 |
-
--
|
| 87 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 88 |
--scfm_genes_list_path model_dependencies/selected_genes_27k.txt \
|
| 89 |
--scfm_gene2vec_file model_dependencies/selected_gene2vec_27k.npy \
|
|
@@ -100,7 +100,7 @@ python -m torch.distributed.run --standalone --nproc_per_node=4 \
|
|
| 100 |
|
| 101 |
- `--scfm_hyper_params_path`: path to the BioMod model configuration file.
|
| 102 |
- `--scfm_ckpt_path`: path to the BioMod model weights.
|
| 103 |
-
- `--
|
| 104 |
|
| 105 |
**Recommended arguments**:
|
| 106 |
|
|
@@ -110,7 +110,7 @@ python -m torch.distributed.run --standalone --nproc_per_node=4 \
|
|
| 110 |
|
| 111 |
**Optional arguments**:
|
| 112 |
|
| 113 |
-
- `--
|
| 114 |
- Default: False, meaning gene-level features are used and recommended.
|
| 115 |
|
| 116 |
## Comparison with Original sclong
|
|
@@ -119,7 +119,7 @@ python -m torch.distributed.run --standalone --nproc_per_node=4 \
|
|
| 119 |
|
| 120 |
1. **Model architecture**:
|
| 121 |
- **Original sclong**: uses `DualEncoderSCFM` and directly processes gene sequences.
|
| 122 |
-
- **Module version**: uses `
|
| 123 |
|
| 124 |
2. **Data representation**:
|
| 125 |
- **Original sclong**: gene-level embeddings come directly from encoder outputs.
|
|
@@ -144,7 +144,7 @@ To compare model performance:
|
|
| 144 |
|
| 145 |
```bash
|
| 146 |
# Run from the BioMod repository root.
|
| 147 |
-
python -m torch.distributed.run --standalone --nproc_per_node=1 BioMod/gene_perturbation/
|
| 148 |
```
|
| 149 |
|
| 150 |
3. **Compare metrics**:
|
|
@@ -161,7 +161,7 @@ module version.
|
|
| 161 |
|
| 162 |
### Module-to-Gene Mapping
|
| 163 |
|
| 164 |
-
In `
|
| 165 |
|
| 166 |
```python
|
| 167 |
self.gene_to_modules = {} # {gene_idx: [module_idx1, module_idx2, ...]}
|
|
@@ -193,11 +193,11 @@ BioMod/gene_perturbation/ # Perturbation response
|
|
| 193 |
├── data_utils_local.py
|
| 194 |
├── gears_reduce_local.py
|
| 195 |
├── inference_local.py
|
| 196 |
-
├──
|
| 197 |
├── pertdata_local.py
|
| 198 |
├── README.md
|
| 199 |
├── scfm_utils_local.py
|
| 200 |
-
├──
|
| 201 |
└── utils.py
|
| 202 |
```
|
| 203 |
|
|
@@ -212,7 +212,7 @@ BioMod/gene_perturbation/ # Perturbation response
|
|
| 212 |
|
| 213 |
### Issue 1: Module Data Not Found
|
| 214 |
|
| 215 |
-
- Check whether `--
|
| 216 |
- Make sure the module data file exists and has the expected format.
|
| 217 |
|
| 218 |
### Issue 2: Gene Mapping Failed
|
|
@@ -228,13 +228,13 @@ BioMod/gene_perturbation/ # Perturbation response
|
|
| 228 |
## Gene-Based vs Module-Based Methods
|
| 229 |
|
| 230 |
This implementation supports two feature extraction modes controlled by
|
| 231 |
-
`--
|
| 232 |
|
| 233 |
### Method 1: Gene-Based (Default, Recommended)
|
| 234 |
|
| 235 |
```bash
|
| 236 |
-
# Default mode; do not pass --
|
| 237 |
-
python -m torch.distributed.run --standalone --nproc_per_node=1 BioMod/gene_perturbation/
|
| 238 |
```
|
| 239 |
|
| 240 |
**Characteristics**:
|
|
@@ -248,7 +248,7 @@ python -m torch.distributed.run --standalone --nproc_per_node=1 BioMod/gene_pert
|
|
| 248 |
|
| 249 |
```bash
|
| 250 |
# Enable module-level mode.
|
| 251 |
-
python -m torch.distributed.run --standalone --nproc_per_node=1 BioMod/gene_perturbation/
|
| 252 |
```
|
| 253 |
|
| 254 |
**Characteristics**:
|
|
@@ -262,8 +262,8 @@ python -m torch.distributed.run --standalone --nproc_per_node=1 BioMod/gene_pert
|
|
| 262 |
## References
|
| 263 |
|
| 264 |
- Original GEARS code is an external reference and is not included in this source directory.
|
| 265 |
-
-
|
| 266 |
-
- Module data processing: `BioMod/
|
| 267 |
|
| 268 |
## Optional external annotation resources
|
| 269 |
|
|
|
|
| 1 |
# Gene Perturbation Prediction with BioMod
|
| 2 |
|
| 3 |
This document describes the BioMod gene perturbation workflow (GEARS), using
|
| 4 |
+
the `BioMod` encoder. The
|
| 5 |
[BioMod model weights and matching configuration](../../README.md#biomod-model-weights)
|
| 6 |
are shared with the other downstream tasks.
|
| 7 |
|
| 8 |
## Overview
|
| 9 |
|
| 10 |
This adaptation replaces the original GEARS gene perturbation model based on
|
| 11 |
+
`DualEncoderSCFM` with `BioMod`. The main adaptation points
|
| 12 |
are:
|
| 13 |
|
| 14 |
1. **Model architecture adaptation**: replace the original dual-encoder model with a module-based encoder-only model.
|
|
|
|
| 17 |
|
| 18 |
## Main Adaptations
|
| 19 |
|
| 20 |
+
### 1. `SCFM_BioMod` (`model_biomod.py`)
|
| 21 |
|
| 22 |
+
`SCFM_BioMod` wraps `BioMod` and provides these functions:
|
| 23 |
|
| 24 |
- **Input handling**: reindex input gene expression values to the BioMod gene order.
|
| 25 |
+
- **Module encoding**: obtain module-level encodings from `BioMod`.
|
| 26 |
- **Gene embedding recovery**: recover gene-level embeddings from module encodings.
|
| 27 |
- For each gene, find all modules that contain it.
|
| 28 |
- Average the encodings of those modules to obtain the gene embedding.
|
|
|
|
| 42 |
gene_emb = module_encodings.mean(dim=1) # B, D (global average)
|
| 43 |
```
|
| 44 |
|
| 45 |
+
### 2. `GEARS_Model_BioMod` (`model_biomod.py`)
|
| 46 |
|
| 47 |
+
`GEARS_Model_BioMod` inherits the original GEARS architecture. Main changes:
|
| 48 |
|
| 49 |
+
- **Use `SCFM_BioMod`** instead of the original `SCFM_different_genes`.
|
| 50 |
- **Module data loading** supports loading module data from hyper-parameters or file paths.
|
| 51 |
- **Other components remain unchanged**, including the GNN layers and decoder.
|
| 52 |
|
| 53 |
+
### 3. Training Script (`train_biomod.py`)
|
| 54 |
|
| 55 |
The training script is based on `GEARS/train_dual_ddp_reduce_dec.py`. Main changes:
|
| 56 |
|
| 57 |
+
- **Model class**: use `GEARS_Model_BioMod` instead of `GEARS_Model_Acc`.
|
| 58 |
- **New arguments**:
|
| 59 |
+
- `--biomod_data_path`: path to one module JSON file.
|
| 60 |
+
- `--biomod_data_dir`: directory containing module JSON files.
|
| 61 |
- `--gene_mapping_path`: CSV file mapping gene symbols to Ensembl IDs.
|
| 62 |
|
| 63 |
## Usage
|
|
|
|
| 78 |
```bash
|
| 79 |
# DDP training on multiple GPUs.
|
| 80 |
python -m torch.distributed.run --standalone --nproc_per_node=4 \
|
| 81 |
+
BioMod/gene_perturbation/train_biomod.py \
|
| 82 |
--data_dir data/downstream/final_data \
|
| 83 |
--data_name norman \
|
| 84 |
+
--scfm_hyper_params_path weight/BioMod.pkl \
|
| 85 |
+
--scfm_ckpt_path weight/BioMod.pth \
|
| 86 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 87 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 88 |
--scfm_genes_list_path model_dependencies/selected_genes_27k.txt \
|
| 89 |
--scfm_gene2vec_file model_dependencies/selected_gene2vec_27k.npy \
|
|
|
|
| 100 |
|
| 101 |
- `--scfm_hyper_params_path`: path to the BioMod model configuration file.
|
| 102 |
- `--scfm_ckpt_path`: path to the BioMod model weights.
|
| 103 |
+
- `--biomod_data_path` or `--biomod_data_dir`: required only when the model configuration does not already contain module definitions.
|
| 104 |
|
| 105 |
**Recommended arguments**:
|
| 106 |
|
|
|
|
| 110 |
|
| 111 |
**Optional arguments**:
|
| 112 |
|
| 113 |
+
- `--use_biomod_level`: use module-level features. This is experimental and is not fully supported at the moment.
|
| 114 |
- Default: False, meaning gene-level features are used and recommended.
|
| 115 |
|
| 116 |
## Comparison with Original sclong
|
|
|
|
| 119 |
|
| 120 |
1. **Model architecture**:
|
| 121 |
- **Original sclong**: uses `DualEncoderSCFM` and directly processes gene sequences.
|
| 122 |
+
- **Module version**: uses `BioMod` and processes module-based data.
|
| 123 |
|
| 124 |
2. **Data representation**:
|
| 125 |
- **Original sclong**: gene-level embeddings come directly from encoder outputs.
|
|
|
|
| 144 |
|
| 145 |
```bash
|
| 146 |
# Run from the BioMod repository root.
|
| 147 |
+
python -m torch.distributed.run --standalone --nproc_per_node=1 BioMod/gene_perturbation/train_biomod.py [arguments...]
|
| 148 |
```
|
| 149 |
|
| 150 |
3. **Compare metrics**:
|
|
|
|
| 161 |
|
| 162 |
### Module-to-Gene Mapping
|
| 163 |
|
| 164 |
+
In `SCFM_BioMod`, `gene_to_modules` records which modules contain each gene:
|
| 165 |
|
| 166 |
```python
|
| 167 |
self.gene_to_modules = {} # {gene_idx: [module_idx1, module_idx2, ...]}
|
|
|
|
| 193 |
├── data_utils_local.py
|
| 194 |
├── gears_reduce_local.py
|
| 195 |
├── inference_local.py
|
| 196 |
+
├── model_biomod.py
|
| 197 |
├── pertdata_local.py
|
| 198 |
├── README.md
|
| 199 |
├── scfm_utils_local.py
|
| 200 |
+
├── train_biomod.py
|
| 201 |
└── utils.py
|
| 202 |
```
|
| 203 |
|
|
|
|
| 212 |
|
| 213 |
### Issue 1: Module Data Not Found
|
| 214 |
|
| 215 |
+
- Check whether `--biomod_data_path` or `--biomod_data_dir` is correct.
|
| 216 |
- Make sure the module data file exists and has the expected format.
|
| 217 |
|
| 218 |
### Issue 2: Gene Mapping Failed
|
|
|
|
| 228 |
## Gene-Based vs Module-Based Methods
|
| 229 |
|
| 230 |
This implementation supports two feature extraction modes controlled by
|
| 231 |
+
`--use_biomod_level`.
|
| 232 |
|
| 233 |
### Method 1: Gene-Based (Default, Recommended)
|
| 234 |
|
| 235 |
```bash
|
| 236 |
+
# Default mode; do not pass --use_biomod_level or set it to False.
|
| 237 |
+
python -m torch.distributed.run --standalone --nproc_per_node=1 BioMod/gene_perturbation/train_biomod.py [other arguments...]
|
| 238 |
```
|
| 239 |
|
| 240 |
**Characteristics**:
|
|
|
|
| 248 |
|
| 249 |
```bash
|
| 250 |
# Enable module-level mode.
|
| 251 |
+
python -m torch.distributed.run --standalone --nproc_per_node=1 BioMod/gene_perturbation/train_biomod.py --use_biomod_level [other arguments...]
|
| 252 |
```
|
| 253 |
|
| 254 |
**Characteristics**:
|
|
|
|
| 262 |
## References
|
| 263 |
|
| 264 |
- Original GEARS code is an external reference and is not included in this source directory.
|
| 265 |
+
- BioMod model: `BioMod/models_biomod.py`
|
| 266 |
+
- Module data processing: `BioMod/biomod_dataset.py`
|
| 267 |
|
| 268 |
## Optional external annotation resources
|
| 269 |
|
|
@@ -46,7 +46,7 @@
|
|
| 46 |
# ============================================================
|
| 47 |
# Use native modules (optimized versions)
|
| 48 |
# ============================================================
|
| 49 |
-
from
|
| 50 |
from inference_local import evaluate, compute_metrics, deeper_analysis, \
|
| 51 |
non_dropout_analysis, compute_synergy_loss
|
| 52 |
from utils import loss_fct, uncertainty_loss_fct, parse_any_pert, \
|
|
@@ -74,7 +74,7 @@ class GEARS:
|
|
| 74 |
[Usage process]
|
| 75 |
```python
|
| 76 |
gears = GEARS(pert_data, local_rank, is_master, world_size, ...)
|
| 77 |
-
gears.model_initialize(hidden_size=64, model_class=
|
| 78 |
gears.train(epochs=20, result_dir='./results', lr=1e-3)
|
| 79 |
```
|
| 80 |
"""
|
|
|
|
| 46 |
# ============================================================
|
| 47 |
# Use native modules (optimized versions)
|
| 48 |
# ============================================================
|
| 49 |
+
from model_biomod import GEARS_Model_BioMod
|
| 50 |
from inference_local import evaluate, compute_metrics, deeper_analysis, \
|
| 51 |
non_dropout_analysis, compute_synergy_loss
|
| 52 |
from utils import loss_fct, uncertainty_loss_fct, parse_any_pert, \
|
|
|
|
| 74 |
[Usage process]
|
| 75 |
```python
|
| 76 |
gears = GEARS(pert_data, local_rank, is_master, world_size, ...)
|
| 77 |
+
gears.model_initialize(hidden_size=64, model_class=GEARS_Model_BioMod, ...)
|
| 78 |
gears.train(epochs=20, result_dir='./results', lr=1e-3)
|
| 79 |
```
|
| 80 |
"""
|
|
@@ -1,24 +1,24 @@
|
|
| 1 |
"""
|
| 2 |
================================================================================
|
| 3 |
-
Genetic perturbation task model definition (
|
| 4 |
================================================================================
|
| 5 |
|
| 6 |
[Model Architecture Overview]
|
| 7 |
This document implements a deep learning model for genetic perturbation prediction. The core components include:
|
| 8 |
|
| 9 |
-
1.
|
| 10 |
- Supports multiple integration methods (multiply/add/gated_residual)
|
| 11 |
- Use attention pooling to aggregate genetic information within the module
|
| 12 |
|
| 13 |
-
2.
|
| 14 |
- Use Performer (Linear Complexity Attention) to process large numbers of module tokens
|
| 15 |
- Support Masked Autoencoder (MAE) pre-training paradigm
|
| 16 |
|
| 17 |
-
3.
|
| 18 |
- Process the mapping of input genes to pre-trained model genes
|
| 19 |
- Recover gene-level embeddings from module encoding
|
| 20 |
|
| 21 |
-
4.
|
| 22 |
- Integrate pre-trained encoder + GNN + decoder
|
| 23 |
- Predict expression changes after gene perturbation
|
| 24 |
|
|
@@ -49,19 +49,19 @@
|
|
| 49 |
# Performer is a linear complexity Transformer variant suitable for processing long sequences
|
| 50 |
# ============================================================
|
| 51 |
try:
|
| 52 |
-
from BioMod.
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
)
|
| 57 |
except ModuleNotFoundError:
|
| 58 |
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 59 |
if project_root not in sys.path:
|
| 60 |
sys.path.insert(0, project_root)
|
| 61 |
-
from BioMod.
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
)
|
| 66 |
|
| 67 |
# ============================================================
|
|
@@ -114,13 +114,13 @@ def reindex_tensor_universal(tensor, index_positions, dim, filler='0', device='c
|
|
| 114 |
return reindexed
|
| 115 |
|
| 116 |
|
| 117 |
-
class
|
| 118 |
"""
|
| 119 |
================================================================================
|
| 120 |
-
|
| 121 |
|
| 122 |
[Core functions]
|
| 123 |
-
Adapt the
|
| 124 |
|
| 125 |
1. Gene mapping: The gene order of the input data may be different from that of the pre-trained model, and re-indexing is required.
|
| 126 |
2. Module-to-gene recovery: Recovering gene-level embeddings from module encodings
|
|
@@ -150,7 +150,7 @@ def __init__(self, input_genes, scfm_genes,
|
|
| 150 |
device='cpu',
|
| 151 |
output_dim=200,
|
| 152 |
use_module_level=False):
|
| 153 |
-
super(
|
| 154 |
self.device = device
|
| 155 |
self.use_module_level = use_module_level
|
| 156 |
|
|
@@ -242,12 +242,12 @@ def __init__(self, input_genes, scfm_genes,
|
|
| 242 |
self.register_buffer('scatter_module_idx', torch.tensor(module_indices, dtype=torch.long))
|
| 243 |
self.register_buffer('scatter_gene_idx', torch.tensor(gene_indices, dtype=torch.long))
|
| 244 |
|
| 245 |
-
# Create
|
| 246 |
scfm_hyper_params['module_sets'] = module_sets
|
| 247 |
scfm_hyper_params['num_genes'] = len(scfm_genes)
|
| 248 |
scfm_hyper_params['max_seq_len'] = len(module_sets)
|
| 249 |
|
| 250 |
-
self.scfm =
|
| 251 |
|
| 252 |
# ============================================================
|
| 253 |
# [Key] Load pre-trained weights
|
|
@@ -298,7 +298,7 @@ def forward(self, x):
|
|
| 298 |
|
| 299 |
[Processing Process]
|
| 300 |
1. Re-index the input gene expression into the gene order of the pre-trained model
|
| 301 |
-
2. Get the module encoding through
|
| 302 |
3. Restore gene-level embedding from module encoding (or return module encoding directly)
|
| 303 |
4. Reindex gene embeddings back to input gene order
|
| 304 |
|
|
@@ -395,17 +395,17 @@ def forward(self, x):
|
|
| 395 |
return output_reindex
|
| 396 |
|
| 397 |
|
| 398 |
-
class
|
| 399 |
"""
|
| 400 |
================================================================================
|
| 401 |
-
|
| 402 |
|
| 403 |
[Core functions]
|
| 404 |
Predicting transcriptomic changes in cells following genetic perturbation. Enter a perturbation combination (such as knockout gene A),
|
| 405 |
Output the expression value prediction of each gene after perturbation.
|
| 406 |
|
| 407 |
[Model Architecture]
|
| 408 |
-
1. Pretrained encoder (
|
| 409 |
2. Perturbation embedding layer: generate perturbation features for perturbed genes
|
| 410 |
3. GO graph neural network (GNN): using gene ontology knowledge to enhance perturbation representation
|
| 411 |
4. Decoder: predict gene expression values after perturbation
|
|
@@ -424,7 +424,7 @@ class GEARS_Model_Module(torch.nn.Module):
|
|
| 424 |
================================================================================
|
| 425 |
"""
|
| 426 |
def __init__(self, args):
|
| 427 |
-
super(
|
| 428 |
self.args = args
|
| 429 |
|
| 430 |
# Forecast record related settings
|
|
@@ -462,7 +462,7 @@ def __init__(self, args):
|
|
| 462 |
with open(args["scfm_hyper_params_path"], 'rb') as handle:
|
| 463 |
self.scfm_hyper_params = pickle5.load(handle)
|
| 464 |
self.scfm_hyper_params.pop('module_embed_chunk_padded_gene_budget', None)
|
| 465 |
-
self.scfm_hyper_params =
|
| 466 |
self.scfm_hyper_params,
|
| 467 |
source=args["scfm_hyper_params_path"],
|
| 468 |
log_fn=print,
|
|
@@ -480,8 +480,8 @@ def __init__(self, args):
|
|
| 480 |
if 'module_sets' in self.scfm_hyper_params and self.scfm_hyper_params['module_sets'] is not None:
|
| 481 |
module_sets = self.scfm_hyper_params['module_sets']
|
| 482 |
elif args.get('module_data_path') or args.get('module_data_dir'):
|
| 483 |
-
from BioMod.
|
| 484 |
-
module_sets =
|
| 485 |
module_data_path=args.get('module_data_path'),
|
| 486 |
module_data_dir=args.get('module_data_dir'),
|
| 487 |
gene_list_path=args.get('scfm_genes_list_path'),
|
|
@@ -499,7 +499,7 @@ def __init__(self, args):
|
|
| 499 |
if args.get('shuffle_module_genes', False):
|
| 500 |
shuffle_seed = args.get('shuffle_seed', 2021)
|
| 501 |
print(
|
| 502 |
-
f"[
|
| 503 |
f"(seed={shuffle_seed}). Pretraining-time module_sets from PKL will be reshuffled again."
|
| 504 |
)
|
| 505 |
all_gene_indices = []
|
|
@@ -518,7 +518,7 @@ def __init__(self, args):
|
|
| 518 |
shuffled_module_sets.append(all_gene_indices[start_idx:start_idx + size])
|
| 519 |
start_idx += size
|
| 520 |
module_sets = shuffled_module_sets
|
| 521 |
-
print(f"[
|
| 522 |
|
| 523 |
# ============================================================
|
| 524 |
# [Perturbation Embedding Layer]
|
|
@@ -585,13 +585,13 @@ def __init__(self, args):
|
|
| 585 |
self.uncertainty_w = MLP([hidden_size, hidden_size*2, hidden_size, 1], last_layer_act='linear')
|
| 586 |
|
| 587 |
# ============================================================
|
| 588 |
-
# [Core] Initialize pre-trained encoder (
|
| 589 |
# requires_grad: whether to fine-tune pre-training parameters
|
| 590 |
# - finetune_method='frozen': freeze pre-training parameters
|
| 591 |
# - Others: Full parameter fine-tuning
|
| 592 |
# ============================================================
|
| 593 |
self.use_module_level = args.get('use_module_level', False)
|
| 594 |
-
self.singlecell_model =
|
| 595 |
self.input_genes_list,
|
| 596 |
self.scfm_genes_list,
|
| 597 |
self.scfm_hyper_params,
|
|
|
|
| 1 |
"""
|
| 2 |
================================================================================
|
| 3 |
+
Genetic perturbation task model definition (model_biomod.py)
|
| 4 |
================================================================================
|
| 5 |
|
| 6 |
[Model Architecture Overview]
|
| 7 |
This document implements a deep learning model for genetic perturbation prediction. The core components include:
|
| 8 |
|
| 9 |
+
1. BioModEmbedding: Convert gene-level expression values into module-level tokens
|
| 10 |
- Supports multiple integration methods (multiply/add/gated_residual)
|
| 11 |
- Use attention pooling to aggregate genetic information within the module
|
| 12 |
|
| 13 |
+
2. BioMod: Encoder-only Transformer model
|
| 14 |
- Use Performer (Linear Complexity Attention) to process large numbers of module tokens
|
| 15 |
- Support Masked Autoencoder (MAE) pre-training paradigm
|
| 16 |
|
| 17 |
+
3. SCFM_BioMod: wrapper for pre-trained models
|
| 18 |
- Process the mapping of input genes to pre-trained model genes
|
| 19 |
- Recover gene-level embeddings from module encoding
|
| 20 |
|
| 21 |
+
4. GEARS_Model_BioMod: Genetic perturbation prediction model
|
| 22 |
- Integrate pre-trained encoder + GNN + decoder
|
| 23 |
- Predict expression changes after gene perturbation
|
| 24 |
|
|
|
|
| 49 |
# Performer is a linear complexity Transformer variant suitable for processing long sequences
|
| 50 |
# ============================================================
|
| 51 |
try:
|
| 52 |
+
from BioMod.models_biomod import (
|
| 53 |
+
BioModEmbedding,
|
| 54 |
+
BioMod,
|
| 55 |
+
sanitize_biomod_config,
|
| 56 |
)
|
| 57 |
except ModuleNotFoundError:
|
| 58 |
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 59 |
if project_root not in sys.path:
|
| 60 |
sys.path.insert(0, project_root)
|
| 61 |
+
from BioMod.models_biomod import (
|
| 62 |
+
BioModEmbedding,
|
| 63 |
+
BioMod,
|
| 64 |
+
sanitize_biomod_config,
|
| 65 |
)
|
| 66 |
|
| 67 |
# ============================================================
|
|
|
|
| 114 |
return reindexed
|
| 115 |
|
| 116 |
|
| 117 |
+
class SCFM_BioMod(torch.nn.Module):
|
| 118 |
"""
|
| 119 |
================================================================================
|
| 120 |
+
SCFM_BioMod: wrapper for pre-trained single cell base models
|
| 121 |
|
| 122 |
[Core functions]
|
| 123 |
+
Adapt the BioMod model to the genetic perturbation task to address the following key issues:
|
| 124 |
|
| 125 |
1. Gene mapping: The gene order of the input data may be different from that of the pre-trained model, and re-indexing is required.
|
| 126 |
2. Module-to-gene recovery: Recovering gene-level embeddings from module encodings
|
|
|
|
| 150 |
device='cpu',
|
| 151 |
output_dim=200,
|
| 152 |
use_module_level=False):
|
| 153 |
+
super(SCFM_BioMod, self).__init__()
|
| 154 |
self.device = device
|
| 155 |
self.use_module_level = use_module_level
|
| 156 |
|
|
|
|
| 242 |
self.register_buffer('scatter_module_idx', torch.tensor(module_indices, dtype=torch.long))
|
| 243 |
self.register_buffer('scatter_gene_idx', torch.tensor(gene_indices, dtype=torch.long))
|
| 244 |
|
| 245 |
+
# Create BioMod model
|
| 246 |
scfm_hyper_params['module_sets'] = module_sets
|
| 247 |
scfm_hyper_params['num_genes'] = len(scfm_genes)
|
| 248 |
scfm_hyper_params['max_seq_len'] = len(module_sets)
|
| 249 |
|
| 250 |
+
self.scfm = BioMod(**scfm_hyper_params)
|
| 251 |
|
| 252 |
# ============================================================
|
| 253 |
# [Key] Load pre-trained weights
|
|
|
|
| 298 |
|
| 299 |
[Processing Process]
|
| 300 |
1. Re-index the input gene expression into the gene order of the pre-trained model
|
| 301 |
+
2. Get the module encoding through BioMod
|
| 302 |
3. Restore gene-level embedding from module encoding (or return module encoding directly)
|
| 303 |
4. Reindex gene embeddings back to input gene order
|
| 304 |
|
|
|
|
| 395 |
return output_reindex
|
| 396 |
|
| 397 |
|
| 398 |
+
class GEARS_Model_BioMod(torch.nn.Module):
|
| 399 |
"""
|
| 400 |
================================================================================
|
| 401 |
+
GEARS_Model_BioMod: Genetic perturbation prediction model
|
| 402 |
|
| 403 |
[Core functions]
|
| 404 |
Predicting transcriptomic changes in cells following genetic perturbation. Enter a perturbation combination (such as knockout gene A),
|
| 405 |
Output the expression value prediction of each gene after perturbation.
|
| 406 |
|
| 407 |
[Model Architecture]
|
| 408 |
+
1. Pretrained encoder (SCFM_BioMod): Encode gene expression into embedding vectors
|
| 409 |
2. Perturbation embedding layer: generate perturbation features for perturbed genes
|
| 410 |
3. GO graph neural network (GNN): using gene ontology knowledge to enhance perturbation representation
|
| 411 |
4. Decoder: predict gene expression values after perturbation
|
|
|
|
| 424 |
================================================================================
|
| 425 |
"""
|
| 426 |
def __init__(self, args):
|
| 427 |
+
super(GEARS_Model_BioMod, self).__init__()
|
| 428 |
self.args = args
|
| 429 |
|
| 430 |
# Forecast record related settings
|
|
|
|
| 462 |
with open(args["scfm_hyper_params_path"], 'rb') as handle:
|
| 463 |
self.scfm_hyper_params = pickle5.load(handle)
|
| 464 |
self.scfm_hyper_params.pop('module_embed_chunk_padded_gene_budget', None)
|
| 465 |
+
self.scfm_hyper_params = sanitize_biomod_config(
|
| 466 |
self.scfm_hyper_params,
|
| 467 |
source=args["scfm_hyper_params_path"],
|
| 468 |
log_fn=print,
|
|
|
|
| 480 |
if 'module_sets' in self.scfm_hyper_params and self.scfm_hyper_params['module_sets'] is not None:
|
| 481 |
module_sets = self.scfm_hyper_params['module_sets']
|
| 482 |
elif args.get('module_data_path') or args.get('module_data_dir'):
|
| 483 |
+
from BioMod.biomod_dataset import load_biomod_data
|
| 484 |
+
module_sets = load_biomod_data(
|
| 485 |
module_data_path=args.get('module_data_path'),
|
| 486 |
module_data_dir=args.get('module_data_dir'),
|
| 487 |
gene_list_path=args.get('scfm_genes_list_path'),
|
|
|
|
| 499 |
if args.get('shuffle_module_genes', False):
|
| 500 |
shuffle_seed = args.get('shuffle_seed', 2021)
|
| 501 |
print(
|
| 502 |
+
f"[GEARS_Model_BioMod] Applying downstream-only extra module reshuffle "
|
| 503 |
f"(seed={shuffle_seed}). Pretraining-time module_sets from PKL will be reshuffled again."
|
| 504 |
)
|
| 505 |
all_gene_indices = []
|
|
|
|
| 518 |
shuffled_module_sets.append(all_gene_indices[start_idx:start_idx + size])
|
| 519 |
start_idx += size
|
| 520 |
module_sets = shuffled_module_sets
|
| 521 |
+
print(f"[GEARS_Model_BioMod] Shuffled {len(all_gene_indices)} genes across {len(module_sets)} modules")
|
| 522 |
|
| 523 |
# ============================================================
|
| 524 |
# [Perturbation Embedding Layer]
|
|
|
|
| 585 |
self.uncertainty_w = MLP([hidden_size, hidden_size*2, hidden_size, 1], last_layer_act='linear')
|
| 586 |
|
| 587 |
# ============================================================
|
| 588 |
+
# [Core] Initialize pre-trained encoder (SCFM_BioMod)
|
| 589 |
# requires_grad: whether to fine-tune pre-training parameters
|
| 590 |
# - finetune_method='frozen': freeze pre-training parameters
|
| 591 |
# - Others: Full parameter fine-tuning
|
| 592 |
# ============================================================
|
| 593 |
self.use_module_level = args.get('use_module_level', False)
|
| 594 |
+
self.singlecell_model = SCFM_BioMod(
|
| 595 |
self.input_genes_list,
|
| 596 |
self.scfm_genes_list,
|
| 597 |
self.scfm_hyper_params,
|
|
@@ -1,6 +1,6 @@
|
|
| 1 |
"""
|
| 2 |
================================================================================
|
| 3 |
-
Genetic perturbation task training script (
|
| 4 |
================================================================================
|
| 5 |
|
| 6 |
[Task Overview]
|
|
@@ -10,7 +10,7 @@
|
|
| 10 |
|
| 11 |
[Core process]
|
| 12 |
1. Load perturbation data sets (such as Norman, Adamson and other public data sets)
|
| 13 |
-
2. Initialize the pre-trained single-cell basic model (
|
| 14 |
3. Fine-tune the model on perturbed data and learn to predict the effect of perturbation
|
| 15 |
4. Evaluate model performance (MSE, Pearson correlation coefficient and other indicators)
|
| 16 |
|
|
@@ -47,7 +47,7 @@
|
|
| 47 |
REPO_ROOT = Path(__file__).resolve().parents[2]
|
| 48 |
ASSET_DIR = REPO_ROOT / "model_dependencies"
|
| 49 |
DOWNSTREAM_DATA_DIR = REPO_ROOT / "data" / "downstream" / "final_data"
|
| 50 |
-
WEIGHTS_DIR = REPO_ROOT / "weight"
|
| 51 |
|
| 52 |
# ============================================================
|
| 53 |
# [Key] Correctly set the order of sys.path
|
|
@@ -58,7 +58,7 @@
|
|
| 58 |
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 59 |
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 60 |
|
| 61 |
-
# First add the project root directory (for
|
| 62 |
if project_root not in sys.path:
|
| 63 |
sys.path.append(project_root)
|
| 64 |
|
|
@@ -87,7 +87,7 @@
|
|
| 87 |
from pertdata_local import PertData # Local version, does not rely on external gears_reduce
|
| 88 |
|
| 89 |
# Import module model
|
| 90 |
-
from
|
| 91 |
|
| 92 |
def main(parser):
|
| 93 |
"""
|
|
@@ -105,7 +105,7 @@ def main(parser):
|
|
| 105 |
|
| 106 |
args = parser.parse_args()
|
| 107 |
if args.module_data_path is None and args.module_data_dir is None:
|
| 108 |
-
args.module_data_dir = str(ASSET_DIR / '
|
| 109 |
|
| 110 |
# ============================================================
|
| 111 |
# [Distributed training initialization]
|
|
@@ -190,7 +190,7 @@ def main(parser):
|
|
| 190 |
|
| 191 |
# ============================================================
|
| 192 |
# [Model parameter configuration]
|
| 193 |
-
# Use
|
| 194 |
# Key parameter description:
|
| 195 |
# - hidden_size: hidden layer dimension, affecting model capacity
|
| 196 |
# - finetune_method: fine-tuning method ('frozen' freezes pre-training parameters, None means fine-tuning all parameters)
|
|
@@ -212,7 +212,7 @@ def main(parser):
|
|
| 212 |
key_enc=None,
|
| 213 |
scfm_gene2vec_file=args.scfm_gene2vec_file,
|
| 214 |
record_pred=args.record_pred,
|
| 215 |
-
model_class=
|
| 216 |
# ============================================================
|
| 217 |
# [Module related parameters] The following parameters affect how the model processes module-based data
|
| 218 |
# module_data_path/module_data_dir: module divides the data path
|
|
@@ -243,7 +243,7 @@ def main(parser):
|
|
| 243 |
param_pd.to_csv(f'{args.result_dir}/params.csv')
|
| 244 |
|
| 245 |
if __name__ == '__main__':
|
| 246 |
-
parser = argparse.ArgumentParser(description='GEARS with
|
| 247 |
|
| 248 |
# ============================================================
|
| 249 |
# [Distributed training parameters]
|
|
@@ -297,9 +297,9 @@ def main(parser):
|
|
| 297 |
parser.add_argument('--scfm_genes_list_path', type=str,
|
| 298 |
default=str(ASSET_DIR / "selected_genes_27k.txt"))
|
| 299 |
parser.add_argument('--scfm_hyper_params_path', type=str,
|
| 300 |
-
default=str(WEIGHTS_DIR / "
|
| 301 |
parser.add_argument('--scfm_ckpt_path', type=str,
|
| 302 |
-
default=str(WEIGHTS_DIR / "
|
| 303 |
parser.add_argument('--run_name', type=str,
|
| 304 |
default="default_run_module")
|
| 305 |
parser.add_argument('--scfm_gene2vec_file', type=str,
|
|
@@ -314,20 +314,20 @@ def main(parser):
|
|
| 314 |
# fusion_method: Read from pkl to ensure consistency with pre-training
|
| 315 |
# shuffle_module_genes: [Ablation experiment] Disrupt the genes in the module to verify the importance of the module structure
|
| 316 |
# ============================================================
|
| 317 |
-
parser.add_argument('--module_data_path', type=str, default=None,
|
| 318 |
-
help='Path to module JSON file (single file)')
|
| 319 |
-
parser.add_argument('--module_data_dir', type=str, default=None,
|
| 320 |
-
help='Module JSON directory; defaults to bundled
|
| 321 |
parser.add_argument('--gene_mapping_path', type=str, default=str(ASSET_DIR / 'gene2ens.csv'),
|
| 322 |
help='Path to gene symbol to Ensembl ID mapping CSV')
|
| 323 |
-
parser.add_argument('--use_module_level', action='store_true',
|
| 324 |
help='Use module-level features directly instead of recovering to gene level. '
|
| 325 |
-
'Note: This requires compatible downstream architecture (not fully implemented).')
|
| 326 |
-
parser.add_argument('--shuffle_module_genes', action='store_true',
|
| 327 |
help='[Ablation experiment] Additionally shuffle module_sets after restoring them from the pretrained PKL. '
|
| 328 |
-
'Do not pass this parameter when reproducing the experiment normally.')
|
| 329 |
parser.add_argument('--shuffle_seed', type=int, default=2021,
|
| 330 |
-
help='Only takes effect when --
|
| 331 |
parser.add_argument('--lr', type=float, default=1e-3)
|
| 332 |
|
| 333 |
main(parser)
|
|
|
|
| 1 |
"""
|
| 2 |
================================================================================
|
| 3 |
+
Genetic perturbation task training script (train_biomod.py)
|
| 4 |
================================================================================
|
| 5 |
|
| 6 |
[Task Overview]
|
|
|
|
| 10 |
|
| 11 |
[Core process]
|
| 12 |
1. Load perturbation data sets (such as Norman, Adamson and other public data sets)
|
| 13 |
+
2. Initialize the pre-trained single-cell basic model (BioMod)
|
| 14 |
3. Fine-tune the model on perturbed data and learn to predict the effect of perturbation
|
| 15 |
4. Evaluate model performance (MSE, Pearson correlation coefficient and other indicators)
|
| 16 |
|
|
|
|
| 47 |
REPO_ROOT = Path(__file__).resolve().parents[2]
|
| 48 |
ASSET_DIR = REPO_ROOT / "model_dependencies"
|
| 49 |
DOWNSTREAM_DATA_DIR = REPO_ROOT / "data" / "downstream" / "final_data"
|
| 50 |
+
WEIGHTS_DIR = REPO_ROOT / "weight"
|
| 51 |
|
| 52 |
# ============================================================
|
| 53 |
# [Key] Correctly set the order of sys.path
|
|
|
|
| 58 |
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 59 |
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 60 |
|
| 61 |
+
# First add the project root directory (for pytorch_cont)
|
| 62 |
if project_root not in sys.path:
|
| 63 |
sys.path.append(project_root)
|
| 64 |
|
|
|
|
| 87 |
from pertdata_local import PertData # Local version, does not rely on external gears_reduce
|
| 88 |
|
| 89 |
# Import module model
|
| 90 |
+
from model_biomod import GEARS_Model_BioMod
|
| 91 |
|
| 92 |
def main(parser):
|
| 93 |
"""
|
|
|
|
| 105 |
|
| 106 |
args = parser.parse_args()
|
| 107 |
if args.module_data_path is None and args.module_data_dir is None:
|
| 108 |
+
args.module_data_dir = str(ASSET_DIR / 'biomod_data_all')
|
| 109 |
|
| 110 |
# ============================================================
|
| 111 |
# [Distributed training initialization]
|
|
|
|
| 190 |
|
| 191 |
# ============================================================
|
| 192 |
# [Model parameter configuration]
|
| 193 |
+
# Use GEARS_Model_BioMod instead of GEARS_Model_Acc
|
| 194 |
# Key parameter description:
|
| 195 |
# - hidden_size: hidden layer dimension, affecting model capacity
|
| 196 |
# - finetune_method: fine-tuning method ('frozen' freezes pre-training parameters, None means fine-tuning all parameters)
|
|
|
|
| 212 |
key_enc=None,
|
| 213 |
scfm_gene2vec_file=args.scfm_gene2vec_file,
|
| 214 |
record_pred=args.record_pred,
|
| 215 |
+
model_class=GEARS_Model_BioMod, # Use module model class
|
| 216 |
# ============================================================
|
| 217 |
# [Module related parameters] The following parameters affect how the model processes module-based data
|
| 218 |
# module_data_path/module_data_dir: module divides the data path
|
|
|
|
| 243 |
param_pd.to_csv(f'{args.result_dir}/params.csv')
|
| 244 |
|
| 245 |
if __name__ == '__main__':
|
| 246 |
+
parser = argparse.ArgumentParser(description='GEARS with BioMod')
|
| 247 |
|
| 248 |
# ============================================================
|
| 249 |
# [Distributed training parameters]
|
|
|
|
| 297 |
parser.add_argument('--scfm_genes_list_path', type=str,
|
| 298 |
default=str(ASSET_DIR / "selected_genes_27k.txt"))
|
| 299 |
parser.add_argument('--scfm_hyper_params_path', type=str,
|
| 300 |
+
default=str(WEIGHTS_DIR / "BioMod.pkl"))
|
| 301 |
parser.add_argument('--scfm_ckpt_path', type=str,
|
| 302 |
+
default=str(WEIGHTS_DIR / "BioMod.pth"))
|
| 303 |
parser.add_argument('--run_name', type=str,
|
| 304 |
default="default_run_module")
|
| 305 |
parser.add_argument('--scfm_gene2vec_file', type=str,
|
|
|
|
| 314 |
# fusion_method: Read from pkl to ensure consistency with pre-training
|
| 315 |
# shuffle_module_genes: [Ablation experiment] Disrupt the genes in the module to verify the importance of the module structure
|
| 316 |
# ============================================================
|
| 317 |
+
parser.add_argument('--biomod_data_path', '--module_data_path', type=str, default=None,
|
| 318 |
+
help='Path to module JSON file (single file)', dest='module_data_path')
|
| 319 |
+
parser.add_argument('--biomod_data_dir', '--module_data_dir', type=str, default=None,
|
| 320 |
+
help='Module JSON directory; defaults to bundled biomod_data_all only when no module file or directory is supplied', dest='module_data_dir')
|
| 321 |
parser.add_argument('--gene_mapping_path', type=str, default=str(ASSET_DIR / 'gene2ens.csv'),
|
| 322 |
help='Path to gene symbol to Ensembl ID mapping CSV')
|
| 323 |
+
parser.add_argument('--use_biomod_level', '--use_module_level', action='store_true',
|
| 324 |
help='Use module-level features directly instead of recovering to gene level. '
|
| 325 |
+
'Note: This requires compatible downstream architecture (not fully implemented).', dest='use_module_level')
|
| 326 |
+
parser.add_argument('--shuffle_biomod_genes', '--shuffle_module_genes', action='store_true',
|
| 327 |
help='[Ablation experiment] Additionally shuffle module_sets after restoring them from the pretrained PKL. '
|
| 328 |
+
'Do not pass this parameter when reproducing the experiment normally.', dest='shuffle_module_genes')
|
| 329 |
parser.add_argument('--shuffle_seed', type=int, default=2021,
|
| 330 |
+
help='Only takes effect when --shuffle_biomod_genes is turned on and is used for additional downstream shuffling.')
|
| 331 |
parser.add_argument('--lr', type=float, default=1e-3)
|
| 332 |
|
| 333 |
main(parser)
|
|
@@ -11,11 +11,11 @@ when loading the pretrained backbone.
|
|
| 11 |
|
| 12 |
| # | Script | Core question | Status |
|
| 13 |
| --- | --- | --- | --- |
|
| 14 |
-
| 1 | `
|
| 15 |
-
| 2 | `
|
| 16 |
-
| 3 | `
|
| 17 |
| 4 | `module_vs_gene.py` | Is BioMod better than GenePT/scGPT for pathway-level interpretability, | TODO |
|
| 18 |
-
| 6 | `attention/
|
| 19 |
|
| 20 |
Experiments 1 and 2 are the core differentiators because gene-level baselines
|
| 21 |
cannot structurally reproduce them. Experiment 3 provides task-level functional
|
|
@@ -24,7 +24,7 @@ experiments, such as module crosstalk vs Reactome, module dynamics along
|
|
| 24 |
developmental trajectories, and retraining-based module-granularity ablation, can
|
| 25 |
be pursued after the core experiments are complete.
|
| 26 |
|
| 27 |
-
## Experiment 1: Intrinsic Geometry of Module Embeddings (`
|
| 28 |
|
| 29 |
**Hypothesis:** the BioMod encoder is trained with a masked-module
|
| 30 |
reconstruction task, but it should still encode semantically related modules,
|
|
@@ -36,7 +36,7 @@ never seen during training.
|
|
| 36 |
|
| 37 |
1. Load the pretrained backbone and run inference with `mask_ratio=0.0` and `return_encodings=True` to obtain module encodings shaped `[B, num_modules, base_dim]`.
|
| 38 |
2. Average over the cell population to obtain `[num_modules, base_dim]`, using streaming accumulation rather than storing the full table in memory.
|
| 39 |
-
3. Parse family assignments from the keys in `
|
| 40 |
4. Quantify family separation:
|
| 41 |
- silhouette coefficient, using both cosine and Euclidean metrics.
|
| 42 |
- kNN purity at `k=10`, using both cosine and Euclidean metrics.
|
|
@@ -56,36 +56,36 @@ aggregation end to end during pretraining.
|
|
| 56 |
BioMod/interpretability/ # Module representations and biological interpretability
|
| 57 |
├── attention/ # Module crosstalk and within-module gene attention
|
| 58 |
│ ├── __init__.py
|
| 59 |
-
│ ├──
|
| 60 |
-
│ ├──
|
| 61 |
│ ├── README.md
|
| 62 |
│ └── README_gene_attention.md
|
| 63 |
-
├──
|
| 64 |
│ ├── __init__.py
|
| 65 |
│ ├── cross_class_similarity.py
|
| 66 |
│ ├── family_ablation.py
|
| 67 |
│ ├── family_importance_analysis.py
|
| 68 |
│ ├── marker_gene_verification.py
|
| 69 |
│ ├── masking_ablation.py
|
| 70 |
-
│ ├──
|
| 71 |
-
│ ├──
|
| 72 |
│ ├── pub_utils.py
|
| 73 |
│ ├── random_baseline.py
|
| 74 |
│ ├── README.md
|
| 75 |
│ ├── RESULTS.md
|
| 76 |
│ ├── tcell_reactome_analysis.py
|
| 77 |
│ └── TODO.md
|
| 78 |
-
├──
|
| 79 |
│ ├── __init__.py
|
| 80 |
│ ├── DESIGN.md
|
| 81 |
│ ├── fetch_reactome_descriptions.py
|
| 82 |
│ ├── MANUSCRIPT_SECTION.md
|
| 83 |
-
│ ├
|
| 84 |
-
│ ├──
|
| 85 |
-
│ ├──
|
| 86 |
│ └── reactome_descriptions.json
|
| 87 |
├── __init__.py
|
| 88 |
-
├──
|
| 89 |
├── README.md
|
| 90 |
└── utils.py
|
| 91 |
```
|
|
@@ -104,10 +104,10 @@ bundled; run the corresponding upstream analysis before using those paths.
|
|
| 104 |
|
| 105 |
```bash
|
| 106 |
# General h5ad input: cell annotation, zero-shot, or any annotated dataset.
|
| 107 |
-
python BioMod/interpretability/
|
| 108 |
-
--
|
| 109 |
-
--config_pkl weight/
|
| 110 |
-
--
|
| 111 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 112 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 113 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
@@ -123,10 +123,10 @@ For pretraining data, where each cell is an `.npz` file already in the 27,874
|
|
| 123 |
model-gene space:
|
| 124 |
|
| 125 |
```bash
|
| 126 |
-
python BioMod/interpretability/
|
| 127 |
-
--
|
| 128 |
-
--config_pkl weight/
|
| 129 |
-
--
|
| 130 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 131 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 132 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
@@ -144,10 +144,10 @@ processes a subset of cells, embeddings are aggregated with `all_reduce`, and
|
|
| 144 |
rank 0 computes metrics and figures. Single-GPU and CPU behavior is unchanged.
|
| 145 |
|
| 146 |
```bash
|
| 147 |
-
torchrun --nproc_per_node=2 BioMod/interpretability/
|
| 148 |
-
--
|
| 149 |
-
--config_pkl weight/
|
| 150 |
-
--
|
| 151 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 152 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 153 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
@@ -167,7 +167,7 @@ torchrun --nproc_per_node=2 BioMod/interpretability/module_geometry.py \
|
|
| 167 |
| `--knn_k` | 10 | `k` for kNN purity. |
|
| 168 |
| `--null_permutations` | 200 | Number of label permutations for the empirical null distribution. |
|
| 169 |
| `--include_other_in_metrics` | Off | By default, modules from the `Other` family are excluded. |
|
| 170 |
-
| `--
|
| 171 |
| `--umap_n_neighbors` | 15 | UMAP graph-construction parameter. |
|
| 172 |
| `--umap_min_dist` | 0.3 | UMAP layout parameter. |
|
| 173 |
|
|
@@ -190,9 +190,9 @@ Files are written to `<output_dir>/`:
|
|
| 190 |
|
| 191 |
Two control experiments make Experiment 1 more rigorous:
|
| 192 |
|
| 193 |
-
1. **Random module assignment**: rerun with `--
|
| 194 |
|
| 195 |
-
2. **Untrained backbone**: set `--
|
| 196 |
|
| 197 |
Together, these controls support the claim that the family structure of the module
|
| 198 |
latent space is produced by pathway-based module design plus pretraining, rather
|
|
@@ -200,16 +200,16 @@ than by module design alone or by a generic encoder architecture.
|
|
| 200 |
|
| 201 |
### Control 1: Random Module Assignment (`shuffle_module_genes`)
|
| 202 |
|
| 203 |
-
Add `--
|
| 204 |
directory.
|
| 205 |
|
| 206 |
For h5ad data, using `pancreas_scib` as an example:
|
| 207 |
|
| 208 |
```bash
|
| 209 |
-
python BioMod/interpretability/
|
| 210 |
-
--
|
| 211 |
-
--config_pkl weight/
|
| 212 |
-
--
|
| 213 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 214 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 215 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
@@ -218,17 +218,17 @@ python BioMod/interpretability/module_geometry.py \
|
|
| 218 |
--data_mode h5ad \
|
| 219 |
--gene_name_column auto \
|
| 220 |
--batch_size 640 \
|
| 221 |
-
--
|
| 222 |
--output_dir outputs/interpretability/geometry/pancreas_scib_shuffle
|
| 223 |
```
|
| 224 |
|
| 225 |
For pretraining npz data:
|
| 226 |
|
| 227 |
```bash
|
| 228 |
-
python BioMod/interpretability/
|
| 229 |
-
--
|
| 230 |
-
--config_pkl weight/
|
| 231 |
-
--
|
| 232 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 233 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 234 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
@@ -237,11 +237,11 @@ python BioMod/interpretability/module_geometry.py \
|
|
| 237 |
--data_mode pretrain_npz \
|
| 238 |
--max_cells 50000 \
|
| 239 |
--batch_size 640 \
|
| 240 |
-
--
|
| 241 |
--output_dir outputs/interpretability/geometry/pretrain_subset_shuffle
|
| 242 |
```
|
| 243 |
|
| 244 |
-
How `--
|
| 245 |
kept unchanged, all genes are collected into one list, the list is randomly
|
| 246 |
shuffled, and genes are reassigned to modules. The seed is controlled by
|
| 247 |
`--shuffle_seed`, defaulting to 2021. Model weights are unchanged; only the
|
|
@@ -253,7 +253,7 @@ records `"shuffle_module_genes": true`.
|
|
| 253 |
|
| 254 |
### Control 2: Untrained Backbone with Random Initialization
|
| 255 |
|
| 256 |
-
First generate a randomly initialized checkpoint, then run `
|
| 257 |
with it.
|
| 258 |
|
| 259 |
A matching randomly initialized checkpoint and configuration are not bundled.
|
|
@@ -263,8 +263,8 @@ Keep the generated control files in a separate output directory. Then pass their
|
|
| 263 |
paths explicitly; do not use the pretrained BioMod weights for this control.
|
| 264 |
|
| 265 |
```bash
|
| 266 |
-
python BioMod/interpretability/
|
| 267 |
-
--
|
| 268 |
--config_pkl /absolute/path/to/random_init.pkl \
|
| 269 |
--data_path data/downstream/final_data/human_pancreas_norm_complexBatch_with_gene_name.h5ad \
|
| 270 |
--data_mode h5ad \
|
|
@@ -292,7 +292,7 @@ The three experiments jointly show that the family structure of the module laten
|
|
| 292 |
space is generated by both pathway-based module design and pretraining; both are
|
| 293 |
necessary.
|
| 294 |
|
| 295 |
-
## Experiment 2: Module Embedding to Pathway Text Alignment (`
|
| 296 |
|
| 297 |
**Hypothesis:** the structure of the BioMod module embedding space significantly
|
| 298 |
aligns with natural-language pathway semantics. Semantically similar pathways
|
|
@@ -301,7 +301,7 @@ experiment because individual gene names do not carry phrase-level semantics.
|
|
| 301 |
|
| 302 |
**Method:**
|
| 303 |
|
| 304 |
-
1. Parse 541 semantically named modules from `
|
| 305 |
2. Encode pathway names with S-PubMedBert-MS-MARCO, a sentence-level fine-tuned PubMedBERT model.
|
| 306 |
3. Load `module_embeddings.npy` from Experiment 1, containing cell-averaged module embeddings.
|
| 307 |
4. Measure alignment:
|
|
@@ -322,19 +322,19 @@ raw expression data are not required.
|
|
| 322 |
# Network access or an existing local model cache is required.
|
| 323 |
|
| 324 |
# Main experiment: RSA + CKA + LOO retrieval.
|
| 325 |
-
python BioMod/interpretability/
|
| 326 |
-
--
|
| 327 |
-
--
|
| 328 |
--sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
|
| 329 |
--n_permutations 1000 \
|
| 330 |
--output_dir outputs/interpretability/text_alignment/pretrain_subset_v2
|
| 331 |
|
| 332 |
# Extended analysis: broad Reactome categories and Hallmark deep dive.
|
| 333 |
-
python BioMod/interpretability/
|
| 334 |
-
--
|
| 335 |
-
--
|
| 336 |
--sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
|
| 337 |
-
--descriptions_file BioMod/interpretability/
|
| 338 |
--n_permutations 1000 \
|
| 339 |
--output_dir outputs/interpretability/text_alignment/pretrain_subset_v2_extended
|
| 340 |
```
|
|
|
|
| 11 |
|
| 12 |
| # | Script | Core question | Status |
|
| 13 |
| --- | --- | --- | --- |
|
| 14 |
+
| 1 | `biomod_geometry.py` | Does the module latent space self-organize by pathway family, | Completed |
|
| 15 |
+
| 2 | `biomod_text_alignment/biomod_text_alignment.py` | Do module embeddings align with natural-language pathway descriptions, | Implemented |
|
| 16 |
+
| 3 | `biomod_importance/biomod_importance.py` | Does task-driven module importance match known pathway-cell-type or pathway-disease associations, | Implemented |
|
| 17 |
| 4 | `module_vs_gene.py` | Is BioMod better than GenePT/scGPT for pathway-level interpretability, | TODO |
|
| 18 |
+
| 6 | `attention/biomod_crosstalk.py` | Do module interactions match Reactome/KEGG pathway relationships, | Implemented |
|
| 19 |
|
| 20 |
Experiments 1 and 2 are the core differentiators because gene-level baselines
|
| 21 |
cannot structurally reproduce them. Experiment 3 provides task-level functional
|
|
|
|
| 24 |
developmental trajectories, and retraining-based module-granularity ablation, can
|
| 25 |
be pursued after the core experiments are complete.
|
| 26 |
|
| 27 |
+
## Experiment 1: Intrinsic Geometry of Module Embeddings (`biomod_geometry.py`)
|
| 28 |
|
| 29 |
**Hypothesis:** the BioMod encoder is trained with a masked-module
|
| 30 |
reconstruction task, but it should still encode semantically related modules,
|
|
|
|
| 36 |
|
| 37 |
1. Load the pretrained backbone and run inference with `mask_ratio=0.0` and `return_encodings=True` to obtain module encodings shaped `[B, num_modules, base_dim]`.
|
| 38 |
2. Average over the cell population to obtain `[num_modules, base_dim]`, using streaming accumulation rather than storing the full table in memory.
|
| 39 |
+
3. Parse family assignments from the keys in `merged_biomods.json`: `Hallmarks` (50), `Reactome` (491), `Hotspot` (73), and `Processed` (46). Hallmark modules are also annotated with one of eight MSigDB biological-process categories: immune, metabolic, signaling, proliferation, development, dna_damage, pathway, and cellular_component.
|
| 40 |
4. Quantify family separation:
|
| 41 |
- silhouette coefficient, using both cosine and Euclidean metrics.
|
| 42 |
- kNN purity at `k=10`, using both cosine and Euclidean metrics.
|
|
|
|
| 56 |
BioMod/interpretability/ # Module representations and biological interpretability
|
| 57 |
├── attention/ # Module crosstalk and within-module gene attention
|
| 58 |
│ ├── __init__.py
|
| 59 |
+
│ ├── biomod_crosstalk.py
|
| 60 |
+
│ ├── biomod_gene_attention.py
|
| 61 |
│ ├── README.md
|
| 62 |
│ └── README_gene_attention.md
|
| 63 |
+
├── biomod_importance/ # Module importance, ablation studies and marker-gene validation
|
| 64 |
│ ├── __init__.py
|
| 65 |
│ ├── cross_class_similarity.py
|
| 66 |
│ ├── family_ablation.py
|
| 67 |
│ ├── family_importance_analysis.py
|
| 68 |
│ ├── marker_gene_verification.py
|
| 69 |
│ ├── masking_ablation.py
|
| 70 |
+
│ ├── biomod_importance.py
|
| 71 |
+
│ ├── biomod_importance_publication.py
|
| 72 |
│ ├── pub_utils.py
|
| 73 |
│ ├── random_baseline.py
|
| 74 |
│ ├── README.md
|
| 75 |
│ ├── RESULTS.md
|
| 76 |
│ ├── tcell_reactome_analysis.py
|
| 77 |
│ └── TODO.md
|
| 78 |
+
├── biomod_text_alignment/ # Semantic alignment of module representations and pathway text
|
| 79 |
│ ├── __init__.py
|
| 80 |
│ ├── DESIGN.md
|
| 81 |
│ ├── fetch_reactome_descriptions.py
|
| 82 |
│ ├── MANUSCRIPT_SECTION.md
|
| 83 |
+
│ ├── biomod_text_alignment.py
|
| 84 |
+
│ ├── biomod_text_alignment_extended.py
|
| 85 |
+
│ ├── biomod_text_alignment_publication.py
|
| 86 |
│ └── reactome_descriptions.json
|
| 87 |
├── __init__.py
|
| 88 |
+
├── biomod_geometry.py
|
| 89 |
├── README.md
|
| 90 |
└── utils.py
|
| 91 |
```
|
|
|
|
| 104 |
|
| 105 |
```bash
|
| 106 |
# General h5ad input: cell annotation, zero-shot, or any annotated dataset.
|
| 107 |
+
python BioMod/interpretability/biomod_geometry.py \
|
| 108 |
+
--biomod_checkpoint weight/BioMod.pth \
|
| 109 |
+
--config_pkl weight/BioMod.pkl \
|
| 110 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 111 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 112 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 113 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
|
|
| 123 |
model-gene space:
|
| 124 |
|
| 125 |
```bash
|
| 126 |
+
python BioMod/interpretability/biomod_geometry.py \
|
| 127 |
+
--biomod_checkpoint weight/BioMod.pth \
|
| 128 |
+
--config_pkl weight/BioMod.pkl \
|
| 129 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 130 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 131 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 132 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
|
|
| 144 |
rank 0 computes metrics and figures. Single-GPU and CPU behavior is unchanged.
|
| 145 |
|
| 146 |
```bash
|
| 147 |
+
torchrun --nproc_per_node=2 BioMod/interpretability/biomod_geometry.py \
|
| 148 |
+
--biomod_checkpoint weight/BioMod.pth \
|
| 149 |
+
--config_pkl weight/BioMod.pkl \
|
| 150 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 151 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 152 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 153 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
|
|
| 167 |
| `--knn_k` | 10 | `k` for kNN purity. |
|
| 168 |
| `--null_permutations` | 200 | Number of label permutations for the empirical null distribution. |
|
| 169 |
| `--include_other_in_metrics` | Off | By default, modules from the `Other` family are excluded. |
|
| 170 |
+
| `--shuffle_biomod_genes` | Off | Randomly shuffle gene-to-module assignments as a negative control or ablation. Uses the same flag as pretraining and downstream finetuning. |
|
| 171 |
| `--umap_n_neighbors` | 15 | UMAP graph-construction parameter. |
|
| 172 |
| `--umap_min_dist` | 0.3 | UMAP layout parameter. |
|
| 173 |
|
|
|
|
| 190 |
|
| 191 |
Two control experiments make Experiment 1 more rigorous:
|
| 192 |
|
| 193 |
+
1. **Random module assignment**: rerun with `--shuffle_biomod_genes`. This flag preserves module sizes but randomly assigns genes to modules, matching the same ablation flag used for pretraining and finetuning. If geometry is pathway-driven rather than a generic property of the encoder, family separation should drop close to the null distribution.
|
| 194 |
|
| 195 |
+
2. **Untrained backbone**: set `--biomod_checkpoint` to the `*_init.pth` checkpoint generated during pretraining with `--save_init_ckpt_and_exit`. Family separation should be close to the null distribution. The gap between trained and untrained models quantifies the contribution of pretraining.
|
| 196 |
|
| 197 |
Together, these controls support the claim that the family structure of the module
|
| 198 |
latent space is produced by pathway-based module design plus pretraining, rather
|
|
|
|
| 200 |
|
| 201 |
### Control 1: Random Module Assignment (`shuffle_module_genes`)
|
| 202 |
|
| 203 |
+
Add `--shuffle_biomod_genes` to the original command and write to a control output
|
| 204 |
directory.
|
| 205 |
|
| 206 |
For h5ad data, using `pancreas_scib` as an example:
|
| 207 |
|
| 208 |
```bash
|
| 209 |
+
python BioMod/interpretability/biomod_geometry.py \
|
| 210 |
+
--biomod_checkpoint weight/BioMod.pth \
|
| 211 |
+
--config_pkl weight/BioMod.pkl \
|
| 212 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 213 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 214 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 215 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
|
|
| 218 |
--data_mode h5ad \
|
| 219 |
--gene_name_column auto \
|
| 220 |
--batch_size 640 \
|
| 221 |
+
--shuffle_biomod_genes \
|
| 222 |
--output_dir outputs/interpretability/geometry/pancreas_scib_shuffle
|
| 223 |
```
|
| 224 |
|
| 225 |
For pretraining npz data:
|
| 226 |
|
| 227 |
```bash
|
| 228 |
+
python BioMod/interpretability/biomod_geometry.py \
|
| 229 |
+
--biomod_checkpoint weight/BioMod.pth \
|
| 230 |
+
--config_pkl weight/BioMod.pkl \
|
| 231 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 232 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 233 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 234 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
|
|
| 237 |
--data_mode pretrain_npz \
|
| 238 |
--max_cells 50000 \
|
| 239 |
--batch_size 640 \
|
| 240 |
+
--shuffle_biomod_genes \
|
| 241 |
--output_dir outputs/interpretability/geometry/pretrain_subset_shuffle
|
| 242 |
```
|
| 243 |
|
| 244 |
+
How `--shuffle_biomod_genes` works: inside `load_backbone()`, module sizes are
|
| 245 |
kept unchanged, all genes are collected into one list, the list is randomly
|
| 246 |
shuffled, and genes are reassigned to modules. The seed is controlled by
|
| 247 |
`--shuffle_seed`, defaulting to 2021. Model weights are unchanged; only the
|
|
|
|
| 253 |
|
| 254 |
### Control 2: Untrained Backbone with Random Initialization
|
| 255 |
|
| 256 |
+
First generate a randomly initialized checkpoint, then run `biomod_geometry.py`
|
| 257 |
with it.
|
| 258 |
|
| 259 |
A matching randomly initialized checkpoint and configuration are not bundled.
|
|
|
|
| 263 |
paths explicitly; do not use the pretrained BioMod weights for this control.
|
| 264 |
|
| 265 |
```bash
|
| 266 |
+
python BioMod/interpretability/biomod_geometry.py \
|
| 267 |
+
--biomod_checkpoint /absolute/path/to/random_init.pth \
|
| 268 |
--config_pkl /absolute/path/to/random_init.pkl \
|
| 269 |
--data_path data/downstream/final_data/human_pancreas_norm_complexBatch_with_gene_name.h5ad \
|
| 270 |
--data_mode h5ad \
|
|
|
|
| 292 |
space is generated by both pathway-based module design and pretraining; both are
|
| 293 |
necessary.
|
| 294 |
|
| 295 |
+
## Experiment 2: Module Embedding to Pathway Text Alignment (`biomod_text_alignment/`)
|
| 296 |
|
| 297 |
**Hypothesis:** the structure of the BioMod module embedding space significantly
|
| 298 |
aligns with natural-language pathway semantics. Semantically similar pathways
|
|
|
|
| 301 |
|
| 302 |
**Method:**
|
| 303 |
|
| 304 |
+
1. Parse 541 semantically named modules from `merged_biomods.json`: 50 Hallmark and 491 Reactome.
|
| 305 |
2. Encode pathway names with S-PubMedBert-MS-MARCO, a sentence-level fine-tuned PubMedBERT model.
|
| 306 |
3. Load `module_embeddings.npy` from Experiment 1, containing cell-averaged module embeddings.
|
| 307 |
4. Measure alignment:
|
|
|
|
| 322 |
# Network access or an existing local model cache is required.
|
| 323 |
|
| 324 |
# Main experiment: RSA + CKA + LOO retrieval.
|
| 325 |
+
python BioMod/interpretability/biomod_text_alignment/biomod_text_alignment.py \
|
| 326 |
+
--biomod_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
|
| 327 |
+
--biomod_json model_dependencies/biomod_data_all/merged_biomods.json \
|
| 328 |
--sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
|
| 329 |
--n_permutations 1000 \
|
| 330 |
--output_dir outputs/interpretability/text_alignment/pretrain_subset_v2
|
| 331 |
|
| 332 |
# Extended analysis: broad Reactome categories and Hallmark deep dive.
|
| 333 |
+
python BioMod/interpretability/biomod_text_alignment/biomod_text_alignment_extended.py \
|
| 334 |
+
--biomod_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
|
| 335 |
+
--biomod_json model_dependencies/biomod_data_all/merged_biomods.json \
|
| 336 |
--sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
|
| 337 |
+
--descriptions_file BioMod/interpretability/biomod_text_alignment/reactome_descriptions.json \
|
| 338 |
--n_permutations 1000 \
|
| 339 |
--output_dir outputs/interpretability/text_alignment/pretrain_subset_v2_extended
|
| 340 |
```
|
|
@@ -1,8 +1,8 @@
|
|
| 1 |
-
"""Module interpretability experiments for scLong (
|
| 2 |
|
| 3 |
Roadmap (priority order):
|
| 4 |
-
1.
|
| 5 |
-
2.
|
| 6 |
-
3.
|
| 7 |
4. module_vs_gene.py — comparison with gene-level baselines (GenePT, scGPT, ...).
|
| 8 |
"""
|
|
|
|
| 1 |
+
"""Module interpretability experiments for scLong (biomod).
|
| 2 |
|
| 3 |
Roadmap (priority order):
|
| 4 |
+
1. biomod_geometry.py — intrinsic module-embedding geometry (current).
|
| 5 |
+
2. biomod_text_alignment.py — module ↔ pathway natural-language alignment.
|
| 6 |
+
3. biomod_importance.py — task-driven ablation + literature enrichment.
|
| 7 |
4. module_vs_gene.py — comparison with gene-level baselines (GenePT, scGPT, ...).
|
| 8 |
"""
|
|
@@ -30,10 +30,10 @@ included; replace `/absolute/path/to/pretraining_cell_npz` with your own directo
|
|
| 30 |
# Run from the BioMod repository root.
|
| 31 |
|
| 32 |
# Full run, methods A + C.
|
| 33 |
-
python BioMod/interpretability/attention/
|
| 34 |
-
--
|
| 35 |
-
--config_pkl weight/
|
| 36 |
-
--
|
| 37 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 38 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 39 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
@@ -46,10 +46,10 @@ python BioMod/interpretability/attention/module_crosstalk.py \
|
|
| 46 |
--pool_mode max
|
| 47 |
|
| 48 |
# Method A only, for quick validation.
|
| 49 |
-
python BioMod/interpretability/attention/
|
| 50 |
-
--
|
| 51 |
-
--config_pkl weight/
|
| 52 |
-
--
|
| 53 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 54 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 55 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
@@ -61,10 +61,10 @@ python BioMod/interpretability/attention/module_crosstalk.py \
|
|
| 61 |
--output_dir outputs/interpretability/crosstalk/pancreas_cosine_only
|
| 62 |
|
| 63 |
# Pretraining npz data.
|
| 64 |
-
python BioMod/interpretability/attention/
|
| 65 |
-
--
|
| 66 |
-
--config_pkl weight/
|
| 67 |
-
--
|
| 68 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 69 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 70 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
|
|
| 30 |
# Run from the BioMod repository root.
|
| 31 |
|
| 32 |
# Full run, methods A + C.
|
| 33 |
+
python BioMod/interpretability/attention/biomod_crosstalk.py \
|
| 34 |
+
--biomod_checkpoint weight/BioMod.pth \
|
| 35 |
+
--config_pkl weight/BioMod.pkl \
|
| 36 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 37 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 38 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 39 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
|
|
| 46 |
--pool_mode max
|
| 47 |
|
| 48 |
# Method A only, for quick validation.
|
| 49 |
+
python BioMod/interpretability/attention/biomod_crosstalk.py \
|
| 50 |
+
--biomod_checkpoint weight/BioMod.pth \
|
| 51 |
+
--config_pkl weight/BioMod.pkl \
|
| 52 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 53 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 54 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 55 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
|
|
| 61 |
--output_dir outputs/interpretability/crosstalk/pancreas_cosine_only
|
| 62 |
|
| 63 |
# Pretraining npz data.
|
| 64 |
+
python BioMod/interpretability/attention/biomod_crosstalk.py \
|
| 65 |
+
--biomod_checkpoint weight/BioMod.pth \
|
| 66 |
+
--config_pkl weight/BioMod.pkl \
|
| 67 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 68 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 69 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 70 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
@@ -1,7 +1,7 @@
|
|
| 1 |
-
# Gene Attention Interpretability:
|
| 2 |
|
| 3 |
Analyze gene contributions within modules using the content-based attention
|
| 4 |
-
pooling in
|
| 5 |
|
| 6 |
## Analyses
|
| 7 |
|
|
@@ -32,10 +32,10 @@ the column name has not been assumed for the bundled pancreas file.
|
|
| 32 |
```bash
|
| 33 |
# Run from the BioMod repository root.
|
| 34 |
|
| 35 |
-
python BioMod/interpretability/attention/
|
| 36 |
-
--
|
| 37 |
-
--config_pkl weight/
|
| 38 |
-
--
|
| 39 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 40 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 41 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
@@ -50,10 +50,10 @@ python BioMod/interpretability/attention/module_gene_attention.py \
|
|
| 50 |
### With DEG Comparison
|
| 51 |
|
| 52 |
```bash
|
| 53 |
-
python BioMod/interpretability/attention/
|
| 54 |
-
--
|
| 55 |
-
--config_pkl weight/
|
| 56 |
-
--
|
| 57 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 58 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 59 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
@@ -89,7 +89,7 @@ Add `--max_cells 5000` to the complete basic command above and change
|
|
| 89 |
| `--marker_db` | None | Custom marker CSV; built-in pancreas markers are used when omitted |
|
| 90 |
| `--deg_csv` | None | DEG result CSV; DEG correlation analysis is skipped when omitted |
|
| 91 |
| `--celltype_column` | celltype | Cell-type column name in h5ad |
|
| 92 |
-
| `--
|
| 93 |
|
| 94 |
## Output Files
|
| 95 |
|
|
|
|
| 1 |
+
# Gene Attention Interpretability: BioModEmbedding
|
| 2 |
|
| 3 |
Analyze gene contributions within modules using the content-based attention
|
| 4 |
+
pooling in BioModEmbedding.
|
| 5 |
|
| 6 |
## Analyses
|
| 7 |
|
|
|
|
| 32 |
```bash
|
| 33 |
# Run from the BioMod repository root.
|
| 34 |
|
| 35 |
+
python BioMod/interpretability/attention/biomod_gene_attention.py \
|
| 36 |
+
--biomod_checkpoint weight/BioMod.pth \
|
| 37 |
+
--config_pkl weight/BioMod.pkl \
|
| 38 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 39 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 40 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 41 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
|
|
| 50 |
### With DEG Comparison
|
| 51 |
|
| 52 |
```bash
|
| 53 |
+
python BioMod/interpretability/attention/biomod_gene_attention.py \
|
| 54 |
+
--biomod_checkpoint weight/BioMod.pth \
|
| 55 |
+
--config_pkl weight/BioMod.pkl \
|
| 56 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 57 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 58 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 59 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
|
|
| 89 |
| `--marker_db` | None | Custom marker CSV; built-in pancreas markers are used when omitted |
|
| 90 |
| `--deg_csv` | None | DEG result CSV; DEG correlation analysis is skipped when omitted |
|
| 91 |
| `--celltype_column` | celltype | Cell-type column name in h5ad |
|
| 92 |
+
| `--shuffle_biomod_genes` | off | Randomly shuffle gene-to-module assignment as a negative control |
|
| 93 |
|
| 94 |
## Output Files
|
| 95 |
|
|
@@ -19,10 +19,10 @@
|
|
| 19 |
- Permutation null p-values for all metrics
|
| 20 |
|
| 21 |
Usage (run from the BioMod repository root):
|
| 22 |
-
python BioMod/interpretability/attention/
|
| 23 |
-
--
|
| 24 |
-
--config_pkl weight/
|
| 25 |
-
--
|
| 26 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 27 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 28 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
@@ -53,14 +53,14 @@
|
|
| 53 |
from sklearn.metrics import roc_auc_score
|
| 54 |
|
| 55 |
HERE = Path(__file__).resolve()
|
| 56 |
-
|
| 57 |
REPO_ROOT = HERE.parents[3]
|
| 58 |
ASSET_ROOT = REPO_ROOT / "model_dependencies"
|
| 59 |
-
for p in (str(
|
| 60 |
if p not in sys.path:
|
| 61 |
sys.path.insert(0, p)
|
| 62 |
|
| 63 |
-
from cell_annotation.
|
| 64 |
DataPreprocessor,
|
| 65 |
load_backbone,
|
| 66 |
seed_all,
|
|
@@ -69,9 +69,9 @@
|
|
| 69 |
from interpretability.utils import (
|
| 70 |
MatrixRowDataset,
|
| 71 |
NpzCellDataset,
|
| 72 |
-
|
| 73 |
ensure_dir,
|
| 74 |
-
|
| 75 |
)
|
| 76 |
|
| 77 |
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
|
@@ -84,16 +84,16 @@
|
|
| 84 |
def parse_args() -> argparse.Namespace:
|
| 85 |
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 86 |
|
| 87 |
-
p.add_argument("--performer_checkpoint", type=str, required=True)
|
| 88 |
p.add_argument("--config_pkl", type=str, required=True)
|
| 89 |
-
p.add_argument("--module_data_dir", type=str, default=str(ASSET_ROOT / "
|
| 90 |
-
p.add_argument("--module_json", type=str, default=None)
|
| 91 |
p.add_argument("--gene_list_path", type=str, default=str(ASSET_ROOT / "selected_genes_27k.txt"))
|
| 92 |
p.add_argument("--gene_mapping_path", type=str, default=str(ASSET_ROOT / "gene2ens.csv"))
|
| 93 |
p.add_argument("--gene2vec_data_path", type=str, default=str(ASSET_ROOT / "selected_gene2vec_27k.npy"))
|
| 94 |
p.add_argument("--gene2ens_file", type=str, default=str(ASSET_ROOT / "gene2ens.csv"))
|
| 95 |
p.add_argument("--gene2ens_column", type=str, default="Ensembl gene ID")
|
| 96 |
-
p.add_argument("--shuffle_module_genes", action="store_true")
|
| 97 |
p.add_argument("--shuffle_seed", type=int, default=2021)
|
| 98 |
|
| 99 |
p.add_argument("--data_path", type=str, required=True)
|
|
@@ -129,7 +129,7 @@ def parse_args() -> argparse.Namespace:
|
|
| 129 |
|
| 130 |
|
| 131 |
# ---------------------------------------------------------------------------
|
| 132 |
-
# Data loading (reuses patterns from
|
| 133 |
# ---------------------------------------------------------------------------
|
| 134 |
|
| 135 |
def build_dataset(args, preprocessor):
|
|
@@ -273,9 +273,9 @@ def _compute_single_cell_jacobian(backbone, cell, num_modules, base_dim, device)
|
|
| 273 |
|
| 274 |
with torch.enable_grad():
|
| 275 |
if backbone.mask_style == "replace":
|
| 276 |
-
x = backbone.
|
| 277 |
else:
|
| 278 |
-
x = backbone.
|
| 279 |
|
| 280 |
if backbone.use_cls and backbone.cls_token is not None:
|
| 281 |
cls_token = backbone.cls_token + backbone.pos_emb[:, :1, :]
|
|
@@ -477,16 +477,16 @@ def main():
|
|
| 477 |
setattr(args, attr, str(Path(val).resolve()))
|
| 478 |
|
| 479 |
logger.info("Loading backbone...")
|
| 480 |
-
backbone,
|
| 481 |
backbone.eval()
|
| 482 |
|
| 483 |
-
num_modules = len(
|
| 484 |
base_dim = int(model_config["base_dim"])
|
| 485 |
expected_genes = int(model_config.get("num_genes", 27874))
|
| 486 |
logger.info("Model: %d modules, dim=%d, genes=%d", num_modules, base_dim, expected_genes)
|
| 487 |
|
| 488 |
-
module_json_path = args.module_json or str(Path(args.module_data_dir) / "
|
| 489 |
-
module_names =
|
| 490 |
with open(module_json_path, "r", encoding="utf-8") as f:
|
| 491 |
module_gene_dict = json.load(f)
|
| 492 |
|
|
@@ -571,7 +571,7 @@ def main():
|
|
| 571 |
json.dump(results, f, indent=2, ensure_ascii=False)
|
| 572 |
|
| 573 |
# --- Plots ---
|
| 574 |
-
meta =
|
| 575 |
families = [row["family"] for row in meta]
|
| 576 |
|
| 577 |
plot_interaction_matrix(cos_matrix, "Method A: Cosine Similarity",
|
|
|
|
| 19 |
- Permutation null p-values for all metrics
|
| 20 |
|
| 21 |
Usage (run from the BioMod repository root):
|
| 22 |
+
python BioMod/interpretability/attention/biomod_crosstalk.py \
|
| 23 |
+
--biomod_checkpoint weight/BioMod.pth \
|
| 24 |
+
--config_pkl weight/BioMod.pkl \
|
| 25 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 26 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 27 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 28 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
|
|
| 53 |
from sklearn.metrics import roc_auc_score
|
| 54 |
|
| 55 |
HERE = Path(__file__).resolve()
|
| 56 |
+
BIOMOD_SOURCE = HERE.parents[2]
|
| 57 |
REPO_ROOT = HERE.parents[3]
|
| 58 |
ASSET_ROOT = REPO_ROOT / "model_dependencies"
|
| 59 |
+
for p in (str(BIOMOD_SOURCE), str(REPO_ROOT)):
|
| 60 |
if p not in sys.path:
|
| 61 |
sys.path.insert(0, p)
|
| 62 |
|
| 63 |
+
from cell_annotation.biomod_finetune import (
|
| 64 |
DataPreprocessor,
|
| 65 |
load_backbone,
|
| 66 |
seed_all,
|
|
|
|
| 69 |
from interpretability.utils import (
|
| 70 |
MatrixRowDataset,
|
| 71 |
NpzCellDataset,
|
| 72 |
+
build_biomod_metadata,
|
| 73 |
ensure_dir,
|
| 74 |
+
load_biomod_names,
|
| 75 |
)
|
| 76 |
|
| 77 |
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
|
|
|
| 84 |
def parse_args() -> argparse.Namespace:
|
| 85 |
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 86 |
|
| 87 |
+
p.add_argument("--biomod_checkpoint", "--performer_checkpoint", type=str, required=True, dest='performer_checkpoint')
|
| 88 |
p.add_argument("--config_pkl", type=str, required=True)
|
| 89 |
+
p.add_argument("--biomod_data_dir", "--module_data_dir", type=str, default=str(ASSET_ROOT / "biomod_data_all"), dest='module_data_dir')
|
| 90 |
+
p.add_argument("--biomod_json", "--module_json", type=str, default=None, dest='module_json')
|
| 91 |
p.add_argument("--gene_list_path", type=str, default=str(ASSET_ROOT / "selected_genes_27k.txt"))
|
| 92 |
p.add_argument("--gene_mapping_path", type=str, default=str(ASSET_ROOT / "gene2ens.csv"))
|
| 93 |
p.add_argument("--gene2vec_data_path", type=str, default=str(ASSET_ROOT / "selected_gene2vec_27k.npy"))
|
| 94 |
p.add_argument("--gene2ens_file", type=str, default=str(ASSET_ROOT / "gene2ens.csv"))
|
| 95 |
p.add_argument("--gene2ens_column", type=str, default="Ensembl gene ID")
|
| 96 |
+
p.add_argument("--shuffle_biomod_genes", "--shuffle_module_genes", action="store_true", dest='shuffle_module_genes')
|
| 97 |
p.add_argument("--shuffle_seed", type=int, default=2021)
|
| 98 |
|
| 99 |
p.add_argument("--data_path", type=str, required=True)
|
|
|
|
| 129 |
|
| 130 |
|
| 131 |
# ---------------------------------------------------------------------------
|
| 132 |
+
# Data loading (reuses patterns from biomod_geometry.py)
|
| 133 |
# ---------------------------------------------------------------------------
|
| 134 |
|
| 135 |
def build_dataset(args, preprocessor):
|
|
|
|
| 273 |
|
| 274 |
with torch.enable_grad():
|
| 275 |
if backbone.mask_style == "replace":
|
| 276 |
+
x = backbone._add_biomod_position_embedding(x_modules)
|
| 277 |
else:
|
| 278 |
+
x = backbone._add_biomod_position_embedding(x_modules)
|
| 279 |
|
| 280 |
if backbone.use_cls and backbone.cls_token is not None:
|
| 281 |
cls_token = backbone.cls_token + backbone.pos_emb[:, :1, :]
|
|
|
|
| 477 |
setattr(args, attr, str(Path(val).resolve()))
|
| 478 |
|
| 479 |
logger.info("Loading backbone...")
|
| 480 |
+
backbone, biomod_data, model_config = load_backbone(args, device)
|
| 481 |
backbone.eval()
|
| 482 |
|
| 483 |
+
num_modules = len(biomod_data)
|
| 484 |
base_dim = int(model_config["base_dim"])
|
| 485 |
expected_genes = int(model_config.get("num_genes", 27874))
|
| 486 |
logger.info("Model: %d modules, dim=%d, genes=%d", num_modules, base_dim, expected_genes)
|
| 487 |
|
| 488 |
+
module_json_path = args.module_json or str(Path(args.module_data_dir) / "merged_biomods.json")
|
| 489 |
+
module_names = load_biomod_names(module_json_path)
|
| 490 |
with open(module_json_path, "r", encoding="utf-8") as f:
|
| 491 |
module_gene_dict = json.load(f)
|
| 492 |
|
|
|
|
| 571 |
json.dump(results, f, indent=2, ensure_ascii=False)
|
| 572 |
|
| 573 |
# --- Plots ---
|
| 574 |
+
meta = build_biomod_metadata(module_names)
|
| 575 |
families = [row["family"] for row in meta]
|
| 576 |
|
| 577 |
plot_interaction_matrix(cos_matrix, "Method A: Cosine Similarity",
|
|
@@ -1,6 +1,6 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
-
Interpretability analysis using single-head content-based attention pooling in
|
| 4 |
Streaming computing mode: memory usage O(n_celltypes × n_genes).
|
| 5 |
|
| 6 |
Analysis content:
|
|
@@ -14,10 +14,10 @@
|
|
| 14 |
8. DEG comparison
|
| 15 |
|
| 16 |
Usage (run from the BioMod repository root):
|
| 17 |
-
python BioMod/interpretability/attention/
|
| 18 |
-
--
|
| 19 |
-
--config_pkl weight/
|
| 20 |
-
--
|
| 21 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 22 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 23 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
@@ -51,30 +51,30 @@
|
|
| 51 |
from scipy.stats import spearmanr, t as t_dist
|
| 52 |
|
| 53 |
HERE = Path(__file__).resolve()
|
| 54 |
-
|
| 55 |
REPO_ROOT = HERE.parents[3]
|
| 56 |
ASSET_ROOT = REPO_ROOT / "model_dependencies"
|
| 57 |
-
for p in (str(
|
| 58 |
if p not in sys.path:
|
| 59 |
sys.path.insert(0, p)
|
| 60 |
|
| 61 |
# Publication utilities
|
| 62 |
-
|
| 63 |
-
if str(
|
| 64 |
-
sys.path.insert(0, str(
|
| 65 |
|
| 66 |
from pub_utils import setup_publication_style, savefig as pub_savefig
|
| 67 |
|
| 68 |
-
from cell_annotation.
|
| 69 |
DataPreprocessor,
|
| 70 |
load_backbone,
|
| 71 |
seed_all,
|
| 72 |
)
|
| 73 |
from interpretability.utils import (
|
| 74 |
MatrixRowDataset,
|
| 75 |
-
|
| 76 |
ensure_dir,
|
| 77 |
-
|
| 78 |
)
|
| 79 |
|
| 80 |
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
|
@@ -88,16 +88,16 @@
|
|
| 88 |
def parse_args() -> argparse.Namespace:
|
| 89 |
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 90 |
|
| 91 |
-
p.add_argument("--performer_checkpoint", type=str, required=True)
|
| 92 |
p.add_argument("--config_pkl", type=str, required=True)
|
| 93 |
-
p.add_argument("--module_data_dir", type=str, default=str(ASSET_ROOT / "
|
| 94 |
-
p.add_argument("--module_json", type=str, default=None)
|
| 95 |
p.add_argument("--gene_list_path", type=str, default=str(ASSET_ROOT / "selected_genes_27k.txt"))
|
| 96 |
p.add_argument("--gene_mapping_path", type=str, default=str(ASSET_ROOT / "gene2ens.csv"))
|
| 97 |
p.add_argument("--gene2vec_data_path", type=str, default=str(ASSET_ROOT / "selected_gene2vec_27k.npy"))
|
| 98 |
p.add_argument("--gene2ens_file", type=str, default=str(ASSET_ROOT / "gene2ens.csv"))
|
| 99 |
p.add_argument("--gene2ens_column", type=str, default="Ensembl gene ID")
|
| 100 |
-
p.add_argument("--shuffle_module_genes", action="store_true")
|
| 101 |
p.add_argument("--shuffle_seed", type=int, default=2021)
|
| 102 |
|
| 103 |
p.add_argument("--data_path", type=str, required=True)
|
|
@@ -915,7 +915,7 @@ def main():
|
|
| 915 |
|
| 916 |
# --- Load model ---
|
| 917 |
logger.info("Loading backbone from %s", args.performer_checkpoint)
|
| 918 |
-
backbone,
|
| 919 |
backbone.eval()
|
| 920 |
|
| 921 |
# --- Build module gene name mapping ---
|
|
|
|
| 1 |
+
"""BioModEmbedding Attention-based Gene Interpretability Analysis (Streaming).
|
| 2 |
|
| 3 |
+
Interpretability analysis using single-head content-based attention pooling in BioModEmbedding.
|
| 4 |
Streaming computing mode: memory usage O(n_celltypes × n_genes).
|
| 5 |
|
| 6 |
Analysis content:
|
|
|
|
| 14 |
8. DEG comparison
|
| 15 |
|
| 16 |
Usage (run from the BioMod repository root):
|
| 17 |
+
python BioMod/interpretability/attention/biomod_gene_attention.py \
|
| 18 |
+
--biomod_checkpoint weight/BioMod.pth \
|
| 19 |
+
--config_pkl weight/BioMod.pkl \
|
| 20 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 21 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 22 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 23 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
|
|
| 51 |
from scipy.stats import spearmanr, t as t_dist
|
| 52 |
|
| 53 |
HERE = Path(__file__).resolve()
|
| 54 |
+
BIOMOD_SOURCE = HERE.parents[2]
|
| 55 |
REPO_ROOT = HERE.parents[3]
|
| 56 |
ASSET_ROOT = REPO_ROOT / "model_dependencies"
|
| 57 |
+
for p in (str(BIOMOD_SOURCE), str(REPO_ROOT)):
|
| 58 |
if p not in sys.path:
|
| 59 |
sys.path.insert(0, p)
|
| 60 |
|
| 61 |
# Publication utilities
|
| 62 |
+
BIOMOD_IMPORTANCE_DIR = BIOMOD_SOURCE / "interpretability" / "biomod_importance"
|
| 63 |
+
if str(BIOMOD_IMPORTANCE_DIR) not in sys.path:
|
| 64 |
+
sys.path.insert(0, str(BIOMOD_IMPORTANCE_DIR))
|
| 65 |
|
| 66 |
from pub_utils import setup_publication_style, savefig as pub_savefig
|
| 67 |
|
| 68 |
+
from cell_annotation.biomod_finetune import (
|
| 69 |
DataPreprocessor,
|
| 70 |
load_backbone,
|
| 71 |
seed_all,
|
| 72 |
)
|
| 73 |
from interpretability.utils import (
|
| 74 |
MatrixRowDataset,
|
| 75 |
+
build_biomod_metadata,
|
| 76 |
ensure_dir,
|
| 77 |
+
load_biomod_names,
|
| 78 |
)
|
| 79 |
|
| 80 |
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
|
|
|
| 88 |
def parse_args() -> argparse.Namespace:
|
| 89 |
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 90 |
|
| 91 |
+
p.add_argument("--biomod_checkpoint", "--performer_checkpoint", type=str, required=True, dest='performer_checkpoint')
|
| 92 |
p.add_argument("--config_pkl", type=str, required=True)
|
| 93 |
+
p.add_argument("--biomod_data_dir", "--module_data_dir", type=str, default=str(ASSET_ROOT / "biomod_data_all"), dest='module_data_dir')
|
| 94 |
+
p.add_argument("--biomod_json", "--module_json", type=str, default=None, dest='module_json')
|
| 95 |
p.add_argument("--gene_list_path", type=str, default=str(ASSET_ROOT / "selected_genes_27k.txt"))
|
| 96 |
p.add_argument("--gene_mapping_path", type=str, default=str(ASSET_ROOT / "gene2ens.csv"))
|
| 97 |
p.add_argument("--gene2vec_data_path", type=str, default=str(ASSET_ROOT / "selected_gene2vec_27k.npy"))
|
| 98 |
p.add_argument("--gene2ens_file", type=str, default=str(ASSET_ROOT / "gene2ens.csv"))
|
| 99 |
p.add_argument("--gene2ens_column", type=str, default="Ensembl gene ID")
|
| 100 |
+
p.add_argument("--shuffle_biomod_genes", "--shuffle_module_genes", action="store_true", dest='shuffle_module_genes')
|
| 101 |
p.add_argument("--shuffle_seed", type=int, default=2021)
|
| 102 |
|
| 103 |
p.add_argument("--data_path", type=str, required=True)
|
|
|
|
| 915 |
|
| 916 |
# --- Load model ---
|
| 917 |
logger.info("Loading backbone from %s", args.performer_checkpoint)
|
| 918 |
+
backbone, biomod_data, model_config = load_backbone(args, device)
|
| 919 |
backbone.eval()
|
| 920 |
|
| 921 |
# --- Build module gene name mapping ---
|
|
@@ -12,7 +12,7 @@
|
|
| 12 |
1. Load pretrained backbone (forward with mask_ratio=0).
|
| 13 |
2. Iterate over a population of cells, accumulate the per-module encoder output
|
| 14 |
in a streaming average -> [num_modules, base_dim].
|
| 15 |
-
3. Parse each module's family from
|
| 16 |
(Hallmarks / Reactome / Hotspot / Processed) and, where applicable, the
|
| 17 |
MSigDB hallmark process subcategory (immune / metabolic / signaling / ...).
|
| 18 |
4. Quantify family separation:
|
|
@@ -44,8 +44,8 @@
|
|
| 44 |
|
| 45 |
CLI
|
| 46 |
---
|
| 47 |
-
python
|
| 48 |
-
--
|
| 49 |
--config_pkl <config.pkl> \\
|
| 50 |
--data_path <path.h5ad> \\
|
| 51 |
--data_mode h5ad \\
|
|
@@ -79,14 +79,14 @@
|
|
| 79 |
|
| 80 |
# Make sibling modules importable when launched as a script
|
| 81 |
HERE = Path(__file__).resolve()
|
| 82 |
-
|
| 83 |
REPO_ROOT = HERE.parents[2]
|
| 84 |
ASSET_ROOT = REPO_ROOT / "model_dependencies"
|
| 85 |
-
for p in (str(
|
| 86 |
if p not in sys.path:
|
| 87 |
sys.path.insert(0, p)
|
| 88 |
|
| 89 |
-
from cell_annotation.
|
| 90 |
DataPreprocessor,
|
| 91 |
get_rank,
|
| 92 |
get_world_size,
|
|
@@ -102,9 +102,9 @@
|
|
| 102 |
from interpretability.utils import ( # noqa: E402
|
| 103 |
MatrixRowDataset,
|
| 104 |
NpzCellDataset,
|
| 105 |
-
|
| 106 |
ensure_dir,
|
| 107 |
-
|
| 108 |
)
|
| 109 |
|
| 110 |
logger = logging.getLogger(__name__)
|
|
@@ -115,19 +115,19 @@
|
|
| 115 |
def parse_args() -> argparse.Namespace:
|
| 116 |
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 117 |
|
| 118 |
-
# Pretrained backbone (mirrors cell_annotation.
|
| 119 |
-
parser.add_argument("--performer_checkpoint", type=str, required=True)
|
| 120 |
parser.add_argument("--config_pkl", type=str, required=True)
|
| 121 |
-
parser.add_argument("--module_data_dir", type=str, default=str(ASSET_ROOT / "
|
| 122 |
-
parser.add_argument("--module_json", type=str, default=None,
|
| 123 |
-
help="Path to
|
| 124 |
parser.add_argument("--gene_list_path", type=str, default=str(ASSET_ROOT / "selected_genes_27k.txt"))
|
| 125 |
parser.add_argument("--gene_mapping_path", type=str, default=str(ASSET_ROOT / "gene2ens.csv"))
|
| 126 |
parser.add_argument("--gene2vec_data_path", type=str, default=str(ASSET_ROOT / "selected_gene2vec_27k.npy"))
|
| 127 |
parser.add_argument("--gene2ens_file", type=str, default=str(ASSET_ROOT / "gene2ens.csv"))
|
| 128 |
parser.add_argument("--gene2ens_column", type=str, default="Ensembl gene ID")
|
| 129 |
-
parser.add_argument("--shuffle_module_genes", action="store_true",
|
| 130 |
-
help="Randomly reshuffle gene-to-module assignment (control / ablation).")
|
| 131 |
parser.add_argument("--shuffle_seed", type=int, default=2021)
|
| 132 |
|
| 133 |
# Data
|
|
@@ -245,7 +245,7 @@ def build_inference_loader(
|
|
| 245 |
# ---------- inference: cell-averaged module embeddings ----------
|
| 246 |
|
| 247 |
@torch.no_grad()
|
| 248 |
-
def
|
| 249 |
backbone,
|
| 250 |
loader: DataLoader,
|
| 251 |
preprocessor: Optional[DataPreprocessor],
|
|
@@ -285,7 +285,7 @@ def extract_cell_averaged_module_embeddings(
|
|
| 285 |
raise RuntimeError(
|
| 286 |
f"Encoder output shape mismatch: got {tuple(enc.shape)}, "
|
| 287 |
f"expected (*, {num_modules}, {base_dim}). "
|
| 288 |
-
"Module order between
|
| 289 |
)
|
| 290 |
sum_emb += enc.to(torch.float64).sum(dim=0)
|
| 291 |
n_cells += enc.shape[0]
|
|
@@ -497,13 +497,13 @@ def setup_logging(output_dir: Path) -> None:
|
|
| 497 |
)
|
| 498 |
|
| 499 |
|
| 500 |
-
def
|
| 501 |
-
"""Resolve relative paths against
|
| 502 |
consistent with how `load_backbone` reads `module_data_dir`.
|
| 503 |
"""
|
| 504 |
if args.module_json is not None:
|
| 505 |
return str(ca_resolve_path(args.module_json))
|
| 506 |
-
return str(ca_resolve_path(args.module_data_dir) / "
|
| 507 |
|
| 508 |
|
| 509 |
def main() -> None:
|
|
@@ -526,15 +526,15 @@ def main() -> None:
|
|
| 526 |
logger.info("DDP: use_ddp=%s, rank=%d/%d, device=%s", use_ddp, rank, world_size, device)
|
| 527 |
|
| 528 |
# 1. Load backbone
|
| 529 |
-
backbone,
|
| 530 |
backbone.eval()
|
| 531 |
base_dim = int(model_config["base_dim"])
|
| 532 |
-
num_modules = len(
|
| 533 |
logger.info("Loaded backbone: num_modules=%d, base_dim=%d", num_modules, base_dim)
|
| 534 |
|
| 535 |
-
# 2. Load module names (must align with
|
| 536 |
-
module_json_path =
|
| 537 |
-
module_names =
|
| 538 |
if len(module_names) != num_modules:
|
| 539 |
raise RuntimeError(
|
| 540 |
f"Module name count ({len(module_names)} from {module_json_path}) does not match "
|
|
@@ -542,7 +542,7 @@ def main() -> None:
|
|
| 542 |
"model and the one given here must be identical, with no modules dropped during "
|
| 543 |
"gene-symbol resolution."
|
| 544 |
)
|
| 545 |
-
metadata =
|
| 546 |
metadata_df = pd.DataFrame(metadata)
|
| 547 |
if is_main_process():
|
| 548 |
logger.info("Family distribution:\n%s", metadata_df["family"].value_counts().to_string())
|
|
@@ -569,7 +569,7 @@ def main() -> None:
|
|
| 569 |
|
| 570 |
# 4. Inference
|
| 571 |
logger.info("Extracting cell-averaged module embeddings over %d cells...", n_cells)
|
| 572 |
-
mean_emb, processed =
|
| 573 |
backbone, loader, preprocessor, reindex_state, device,
|
| 574 |
num_modules=num_modules, base_dim=base_dim,
|
| 575 |
expected_input_genes=expected_input_genes,
|
|
|
|
| 12 |
1. Load pretrained backbone (forward with mask_ratio=0).
|
| 13 |
2. Iterate over a population of cells, accumulate the per-module encoder output
|
| 14 |
in a streaming average -> [num_modules, base_dim].
|
| 15 |
+
3. Parse each module's family from merged_biomods.json keys
|
| 16 |
(Hallmarks / Reactome / Hotspot / Processed) and, where applicable, the
|
| 17 |
MSigDB hallmark process subcategory (immune / metabolic / signaling / ...).
|
| 18 |
4. Quantify family separation:
|
|
|
|
| 44 |
|
| 45 |
CLI
|
| 46 |
---
|
| 47 |
+
python BioMod/interpretability/biomod_geometry.py \\
|
| 48 |
+
--biomod_checkpoint <ckpt.pth> \\
|
| 49 |
--config_pkl <config.pkl> \\
|
| 50 |
--data_path <path.h5ad> \\
|
| 51 |
--data_mode h5ad \\
|
|
|
|
| 79 |
|
| 80 |
# Make sibling modules importable when launched as a script
|
| 81 |
HERE = Path(__file__).resolve()
|
| 82 |
+
BIOMOD_SOURCE = HERE.parents[1]
|
| 83 |
REPO_ROOT = HERE.parents[2]
|
| 84 |
ASSET_ROOT = REPO_ROOT / "model_dependencies"
|
| 85 |
+
for p in (str(BIOMOD_SOURCE), str(REPO_ROOT)):
|
| 86 |
if p not in sys.path:
|
| 87 |
sys.path.insert(0, p)
|
| 88 |
|
| 89 |
+
from cell_annotation.biomod_finetune import ( # noqa: E402
|
| 90 |
DataPreprocessor,
|
| 91 |
get_rank,
|
| 92 |
get_world_size,
|
|
|
|
| 102 |
from interpretability.utils import ( # noqa: E402
|
| 103 |
MatrixRowDataset,
|
| 104 |
NpzCellDataset,
|
| 105 |
+
build_biomod_metadata,
|
| 106 |
ensure_dir,
|
| 107 |
+
load_biomod_names,
|
| 108 |
)
|
| 109 |
|
| 110 |
logger = logging.getLogger(__name__)
|
|
|
|
| 115 |
def parse_args() -> argparse.Namespace:
|
| 116 |
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 117 |
|
| 118 |
+
# Pretrained backbone (mirrors cell_annotation.biomod_finetune)
|
| 119 |
+
parser.add_argument("--biomod_checkpoint", "--performer_checkpoint", type=str, required=True, dest='performer_checkpoint')
|
| 120 |
parser.add_argument("--config_pkl", type=str, required=True)
|
| 121 |
+
parser.add_argument("--biomod_data_dir", "--module_data_dir", type=str, default=str(ASSET_ROOT / "biomod_data_all"), dest='module_data_dir')
|
| 122 |
+
parser.add_argument("--biomod_json", "--module_json", type=str, default=None,
|
| 123 |
+
help="Path to merged_biomods.json. Defaults to <module_data_dir>/merged_biomods.json.", dest='module_json')
|
| 124 |
parser.add_argument("--gene_list_path", type=str, default=str(ASSET_ROOT / "selected_genes_27k.txt"))
|
| 125 |
parser.add_argument("--gene_mapping_path", type=str, default=str(ASSET_ROOT / "gene2ens.csv"))
|
| 126 |
parser.add_argument("--gene2vec_data_path", type=str, default=str(ASSET_ROOT / "selected_gene2vec_27k.npy"))
|
| 127 |
parser.add_argument("--gene2ens_file", type=str, default=str(ASSET_ROOT / "gene2ens.csv"))
|
| 128 |
parser.add_argument("--gene2ens_column", type=str, default="Ensembl gene ID")
|
| 129 |
+
parser.add_argument("--shuffle_biomod_genes", "--shuffle_module_genes", action="store_true",
|
| 130 |
+
help="Randomly reshuffle gene-to-module assignment (control / ablation).", dest='shuffle_module_genes')
|
| 131 |
parser.add_argument("--shuffle_seed", type=int, default=2021)
|
| 132 |
|
| 133 |
# Data
|
|
|
|
| 245 |
# ---------- inference: cell-averaged module embeddings ----------
|
| 246 |
|
| 247 |
@torch.no_grad()
|
| 248 |
+
def extract_cell_averaged_biomod_embeddings(
|
| 249 |
backbone,
|
| 250 |
loader: DataLoader,
|
| 251 |
preprocessor: Optional[DataPreprocessor],
|
|
|
|
| 285 |
raise RuntimeError(
|
| 286 |
f"Encoder output shape mismatch: got {tuple(enc.shape)}, "
|
| 287 |
f"expected (*, {num_modules}, {base_dim}). "
|
| 288 |
+
"Module order between merged_biomods.json and the loaded backbone may have drifted."
|
| 289 |
)
|
| 290 |
sum_emb += enc.to(torch.float64).sum(dim=0)
|
| 291 |
n_cells += enc.shape[0]
|
|
|
|
| 497 |
)
|
| 498 |
|
| 499 |
|
| 500 |
+
def resolve_biomod_json(args: argparse.Namespace) -> str:
|
| 501 |
+
"""Resolve relative paths against biomod/cell_annotation/ to stay
|
| 502 |
consistent with how `load_backbone` reads `module_data_dir`.
|
| 503 |
"""
|
| 504 |
if args.module_json is not None:
|
| 505 |
return str(ca_resolve_path(args.module_json))
|
| 506 |
+
return str(ca_resolve_path(args.module_data_dir) / "merged_biomods.json")
|
| 507 |
|
| 508 |
|
| 509 |
def main() -> None:
|
|
|
|
| 526 |
logger.info("DDP: use_ddp=%s, rank=%d/%d, device=%s", use_ddp, rank, world_size, device)
|
| 527 |
|
| 528 |
# 1. Load backbone
|
| 529 |
+
backbone, biomod_data, model_config = load_backbone(args, device)
|
| 530 |
backbone.eval()
|
| 531 |
base_dim = int(model_config["base_dim"])
|
| 532 |
+
num_modules = len(biomod_data)
|
| 533 |
logger.info("Loaded backbone: num_modules=%d, base_dim=%d", num_modules, base_dim)
|
| 534 |
|
| 535 |
+
# 2. Load module names (must align with biomod_data order)
|
| 536 |
+
module_json_path = resolve_biomod_json(args)
|
| 537 |
+
module_names = load_biomod_names(module_json_path)
|
| 538 |
if len(module_names) != num_modules:
|
| 539 |
raise RuntimeError(
|
| 540 |
f"Module name count ({len(module_names)} from {module_json_path}) does not match "
|
|
|
|
| 542 |
"model and the one given here must be identical, with no modules dropped during "
|
| 543 |
"gene-symbol resolution."
|
| 544 |
)
|
| 545 |
+
metadata = build_biomod_metadata(module_names)
|
| 546 |
metadata_df = pd.DataFrame(metadata)
|
| 547 |
if is_main_process():
|
| 548 |
logger.info("Family distribution:\n%s", metadata_df["family"].value_counts().to_string())
|
|
|
|
| 569 |
|
| 570 |
# 4. Inference
|
| 571 |
logger.info("Extracting cell-averaged module embeddings over %d cells...", n_cells)
|
| 572 |
+
mean_emb, processed = extract_cell_averaged_biomod_embeddings(
|
| 573 |
backbone, loader, preprocessor, reindex_state, device,
|
| 574 |
num_modules=num_modules, base_dim=base_dim,
|
| 575 |
expected_input_genes=expected_input_genes,
|
|
@@ -33,10 +33,10 @@ preceding steps; they are not bundled inputs.
|
|
| 33 |
|
| 34 |
```bash
|
| 35 |
# Run from the BioMod repository root.
|
| 36 |
-
python BioMod/interpretability/
|
| 37 |
--finetune_checkpoint tutorial_runs/cell_annotation_zheng68k_biomod/best_model.pt \
|
| 38 |
-
--config_pkl weight/
|
| 39 |
-
--
|
| 40 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 41 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 42 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
@@ -52,10 +52,10 @@ python BioMod/interpretability/module_importance/module_importance.py \
|
|
| 52 |
Pre-encoder mode, more accurate but slower:
|
| 53 |
|
| 54 |
```bash
|
| 55 |
-
python BioMod/interpretability/
|
| 56 |
--finetune_checkpoint tutorial_runs/cell_annotation_zheng68k_biomod/best_model.pt \
|
| 57 |
-
--config_pkl weight/
|
| 58 |
-
--
|
| 59 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 60 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 61 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
@@ -72,10 +72,10 @@ python BioMod/interpretability/module_importance/module_importance.py \
|
|
| 72 |
Pre-encoder mode on all cells with DDP multi-GPU acceleration:
|
| 73 |
|
| 74 |
```bash
|
| 75 |
-
torchrun --nproc_per_node=4 BioMod/interpretability/
|
| 76 |
--finetune_checkpoint tutorial_runs/cell_annotation_zheng68k_biomod/best_model.pt \
|
| 77 |
-
--config_pkl weight/
|
| 78 |
-
--
|
| 79 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 80 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 81 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
@@ -132,10 +132,10 @@ If the model learns meaningful module importance:
|
|
| 132 |
Progressively mask top, bottom, or random modules and measure the accuracy drop.
|
| 133 |
|
| 134 |
```bash
|
| 135 |
-
python BioMod/interpretability/
|
| 136 |
--finetune_checkpoint tutorial_runs/cell_annotation_zheng68k_biomod/best_model.pt \
|
| 137 |
-
--config_pkl weight/
|
| 138 |
-
--
|
| 139 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 140 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 141 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
@@ -154,11 +154,11 @@ python BioMod/interpretability/module_importance/masking_ablation.py \
|
|
| 154 |
|
| 155 |
```bash
|
| 156 |
# Mode A: shuffle modules, GPU and slow.
|
| 157 |
-
python BioMod/interpretability/
|
| 158 |
--mode shuffle_modules \
|
| 159 |
--finetune_checkpoint tutorial_runs/cell_annotation_zheng68k_biomod/best_model.pt \
|
| 160 |
-
--config_pkl weight/
|
| 161 |
-
--
|
| 162 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 163 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 164 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
@@ -170,7 +170,7 @@ python BioMod/interpretability/module_importance/random_baseline.py \
|
|
| 170 |
--output_dir outputs/interpretability/importance/permutation/
|
| 171 |
|
| 172 |
# Mode B: shuffle labels, CPU and fast, requires per_cell_deltas from ablation.
|
| 173 |
-
python BioMod/interpretability/
|
| 174 |
--mode shuffle_labels \
|
| 175 |
--importance_matrix outputs/interpretability/importance/zheng68k_pre_13690/importance_matrix.npy \
|
| 176 |
--per_cell_deltas outputs/interpretability/importance/ablation/per_cell_deltas.npy \
|
|
@@ -182,21 +182,21 @@ python BioMod/interpretability/module_importance/random_baseline.py \
|
|
| 182 |
### 3-C: Marker Gene Verification, CPU Only
|
| 183 |
|
| 184 |
```bash
|
| 185 |
-
python BioMod/interpretability/
|
| 186 |
--importance_matrix outputs/interpretability/importance/zheng68k_pre_13690/importance_matrix.npy \
|
| 187 |
--importance_csv <importance_matrix.csv> \
|
| 188 |
-
--
|
| 189 |
--output_dir outputs/interpretability/importance/marker_verification/
|
| 190 |
```
|
| 191 |
|
| 192 |
### Publication Figures
|
| 193 |
|
| 194 |
```bash
|
| 195 |
-
python BioMod/interpretability/
|
| 196 |
--results_dir outputs/interpretability/importance/zheng68k_pre_13690/ \
|
| 197 |
--ablation_dir outputs/interpretability/importance/ablation \
|
| 198 |
--permutation_dir outputs/interpretability/importance/permutation/ \
|
| 199 |
-
--
|
| 200 |
--output_dir outputs/interpretability/importance/publication_figures/
|
| 201 |
```
|
| 202 |
|
|
@@ -206,7 +206,7 @@ Analyze importance distributions, contribution fractions, and enrichment tests b
|
|
| 206 |
module source family: Hallmarks, Reactome, Hotspot, and WGCNA.
|
| 207 |
|
| 208 |
```bash
|
| 209 |
-
python BioMod/interpretability/
|
| 210 |
--results_dir outputs/interpretability/importance/zheng68k_pre \
|
| 211 |
--output_dir outputs/interpretability/importance/family_analysis \
|
| 212 |
--top_k 10 20 50 \
|
|
@@ -231,10 +231,10 @@ Ablate whole families by removing or keeping one module family and measuring
|
|
| 231 |
classification-accuracy changes.
|
| 232 |
|
| 233 |
```bash
|
| 234 |
-
python BioMod/interpretability/
|
| 235 |
--finetune_checkpoint tutorial_runs/cell_annotation_zheng68k_biomod/best_model.pt \
|
| 236 |
-
--config_pkl weight/
|
| 237 |
-
--
|
| 238 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 239 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 240 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
@@ -257,7 +257,7 @@ Analyze correlations and similarity between important modules across the 11 cell
|
|
| 257 |
classes.
|
| 258 |
|
| 259 |
```bash
|
| 260 |
-
python BioMod/interpretability/
|
| 261 |
--results_dir outputs/interpretability/importance/zheng68k_pre \
|
| 262 |
--output_dir outputs/interpretability/importance/publication_figures \
|
| 263 |
--top_k 50
|
|
@@ -275,10 +275,10 @@ parameters are trained from zero initialization.
|
|
| 275 |
|
| 276 |
```bash
|
| 277 |
# Baseline, no new modules.
|
| 278 |
-
python BioMod/cell_annotation/
|
| 279 |
-
--config_pkl weight/
|
| 280 |
-
--
|
| 281 |
-
--
|
| 282 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 283 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 284 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
@@ -296,7 +296,7 @@ python BioMod/cell_annotation/performer_module_finetune.py \
|
|
| 296 |
The same command can be extended with:
|
| 297 |
|
| 298 |
- `--use_family_emb` to enable family-type embedding, adding `4 x 200 = 800` parameters.
|
| 299 |
-
- `--
|
| 300 |
- Both flags can be used together.
|
| 301 |
|
| 302 |
The new module parameters use `lr_head`, a higher learning rate, and are
|
|
@@ -306,11 +306,11 @@ independent of frozen backbone parameters.
|
|
| 306 |
|
| 307 |
| File | Description |
|
| 308 |
| --- | --- |
|
| 309 |
-
| `
|
| 310 |
| `masking_ablation.py` | Ablation-curve experiment, GPU |
|
| 311 |
| `random_baseline.py` | Permutation tests, GPU/CPU |
|
| 312 |
| `marker_gene_verification.py` | Marker verification, CPU |
|
| 313 |
-
| `
|
| 314 |
| `family_importance_analysis.py` | Family-level importance analysis, CPU |
|
| 315 |
| `family_ablation.py` | Family-level ablation, GPU |
|
| 316 |
| `cross_class_similarity.py` | Cross-class similarity analysis, CPU |
|
|
|
|
| 33 |
|
| 34 |
```bash
|
| 35 |
# Run from the BioMod repository root.
|
| 36 |
+
python BioMod/interpretability/biomod_importance/biomod_importance.py \
|
| 37 |
--finetune_checkpoint tutorial_runs/cell_annotation_zheng68k_biomod/best_model.pt \
|
| 38 |
+
--config_pkl weight/BioMod.pkl \
|
| 39 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 40 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 41 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 42 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
|
|
| 52 |
Pre-encoder mode, more accurate but slower:
|
| 53 |
|
| 54 |
```bash
|
| 55 |
+
python BioMod/interpretability/biomod_importance/biomod_importance.py \
|
| 56 |
--finetune_checkpoint tutorial_runs/cell_annotation_zheng68k_biomod/best_model.pt \
|
| 57 |
+
--config_pkl weight/BioMod.pkl \
|
| 58 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 59 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 60 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 61 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
|
|
| 72 |
Pre-encoder mode on all cells with DDP multi-GPU acceleration:
|
| 73 |
|
| 74 |
```bash
|
| 75 |
+
torchrun --nproc_per_node=4 BioMod/interpretability/biomod_importance/biomod_importance.py \
|
| 76 |
--finetune_checkpoint tutorial_runs/cell_annotation_zheng68k_biomod/best_model.pt \
|
| 77 |
+
--config_pkl weight/BioMod.pkl \
|
| 78 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 79 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 80 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 81 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
|
|
| 132 |
Progressively mask top, bottom, or random modules and measure the accuracy drop.
|
| 133 |
|
| 134 |
```bash
|
| 135 |
+
python BioMod/interpretability/biomod_importance/masking_ablation.py \
|
| 136 |
--finetune_checkpoint tutorial_runs/cell_annotation_zheng68k_biomod/best_model.pt \
|
| 137 |
+
--config_pkl weight/BioMod.pkl \
|
| 138 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 139 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 140 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 141 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
|
|
| 154 |
|
| 155 |
```bash
|
| 156 |
# Mode A: shuffle modules, GPU and slow.
|
| 157 |
+
python BioMod/interpretability/biomod_importance/random_baseline.py \
|
| 158 |
--mode shuffle_modules \
|
| 159 |
--finetune_checkpoint tutorial_runs/cell_annotation_zheng68k_biomod/best_model.pt \
|
| 160 |
+
--config_pkl weight/BioMod.pkl \
|
| 161 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 162 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 163 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 164 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
|
|
| 170 |
--output_dir outputs/interpretability/importance/permutation/
|
| 171 |
|
| 172 |
# Mode B: shuffle labels, CPU and fast, requires per_cell_deltas from ablation.
|
| 173 |
+
python BioMod/interpretability/biomod_importance/random_baseline.py \
|
| 174 |
--mode shuffle_labels \
|
| 175 |
--importance_matrix outputs/interpretability/importance/zheng68k_pre_13690/importance_matrix.npy \
|
| 176 |
--per_cell_deltas outputs/interpretability/importance/ablation/per_cell_deltas.npy \
|
|
|
|
| 182 |
### 3-C: Marker Gene Verification, CPU Only
|
| 183 |
|
| 184 |
```bash
|
| 185 |
+
python BioMod/interpretability/biomod_importance/marker_gene_verification.py \
|
| 186 |
--importance_matrix outputs/interpretability/importance/zheng68k_pre_13690/importance_matrix.npy \
|
| 187 |
--importance_csv <importance_matrix.csv> \
|
| 188 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 189 |
--output_dir outputs/interpretability/importance/marker_verification/
|
| 190 |
```
|
| 191 |
|
| 192 |
### Publication Figures
|
| 193 |
|
| 194 |
```bash
|
| 195 |
+
python BioMod/interpretability/biomod_importance/biomod_importance_publication.py \
|
| 196 |
--results_dir outputs/interpretability/importance/zheng68k_pre_13690/ \
|
| 197 |
--ablation_dir outputs/interpretability/importance/ablation \
|
| 198 |
--permutation_dir outputs/interpretability/importance/permutation/ \
|
| 199 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 200 |
--output_dir outputs/interpretability/importance/publication_figures/
|
| 201 |
```
|
| 202 |
|
|
|
|
| 206 |
module source family: Hallmarks, Reactome, Hotspot, and WGCNA.
|
| 207 |
|
| 208 |
```bash
|
| 209 |
+
python BioMod/interpretability/biomod_importance/family_importance_analysis.py \
|
| 210 |
--results_dir outputs/interpretability/importance/zheng68k_pre \
|
| 211 |
--output_dir outputs/interpretability/importance/family_analysis \
|
| 212 |
--top_k 10 20 50 \
|
|
|
|
| 231 |
classification-accuracy changes.
|
| 232 |
|
| 233 |
```bash
|
| 234 |
+
python BioMod/interpretability/biomod_importance/family_ablation.py \
|
| 235 |
--finetune_checkpoint tutorial_runs/cell_annotation_zheng68k_biomod/best_model.pt \
|
| 236 |
+
--config_pkl weight/BioMod.pkl \
|
| 237 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 238 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 239 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 240 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
|
|
| 257 |
classes.
|
| 258 |
|
| 259 |
```bash
|
| 260 |
+
python BioMod/interpretability/biomod_importance/cross_class_similarity.py \
|
| 261 |
--results_dir outputs/interpretability/importance/zheng68k_pre \
|
| 262 |
--output_dir outputs/interpretability/importance/publication_figures \
|
| 263 |
--top_k 50
|
|
|
|
| 275 |
|
| 276 |
```bash
|
| 277 |
# Baseline, no new modules.
|
| 278 |
+
python BioMod/cell_annotation/biomod_finetune.py \
|
| 279 |
+
--config_pkl weight/BioMod.pkl \
|
| 280 |
+
--biomod_checkpoint weight/BioMod.pth \
|
| 281 |
+
--biomod_data_dir model_dependencies/biomod_data_all \
|
| 282 |
--gene_list_path model_dependencies/selected_genes_27k.txt \
|
| 283 |
--gene_mapping_path model_dependencies/gene2ens.csv \
|
| 284 |
--gene2vec_data_path model_dependencies/selected_gene2vec_27k.npy \
|
|
|
|
| 296 |
The same command can be extended with:
|
| 297 |
|
| 298 |
- `--use_family_emb` to enable family-type embedding, adding `4 x 200 = 800` parameters.
|
| 299 |
+
- `--use_biomod_gate` to enable sparse module gating, adding LayerNorm + Linear, 601 parameters.
|
| 300 |
- Both flags can be used together.
|
| 301 |
|
| 302 |
The new module parameters use `lr_head`, a higher learning rate, and are
|
|
|
|
| 306 |
|
| 307 |
| File | Description |
|
| 308 |
| --- | --- |
|
| 309 |
+
| `biomod_importance.py` | Core importance computation with DDP support |
|
| 310 |
| `masking_ablation.py` | Ablation-curve experiment, GPU |
|
| 311 |
| `random_baseline.py` | Permutation tests, GPU/CPU |
|
| 312 |
| `marker_gene_verification.py` | Marker verification, CPU |
|
| 313 |
+
| `biomod_importance_publication.py` | Publication figures |
|
| 314 |
| `family_importance_analysis.py` | Family-level importance analysis, CPU |
|
| 315 |
| `family_ablation.py` | Family-level ablation, GPU |
|
| 316 |
| `cross_class_similarity.py` | Cross-class similarity analysis, CPU |
|
|
@@ -5,7 +5,7 @@
|
|
| 5 |
## Dataset
|
| 6 |
|
| 7 |
- **Zheng68K**: 11 cell types, 68,450 cells, with 13,690 test cells.
|
| 8 |
-
- Model:
|
| 9 |
- Baseline test accuracy: **83.1%**.
|
| 10 |
|
| 11 |
## Data Sources
|
|
@@ -13,9 +13,9 @@
|
|
| 13 |
These are historical experiment results. The analysis output files below are not
|
| 14 |
included in the repository; recreate them using the commands in `README.md` or
|
| 15 |
point the analysis scripts to the original run directory. The shared module JSON
|
| 16 |
-
is bundled under `model_dependencies/
|
| 17 |
|
| 18 |
-
All analyses are based on the following core files, generated by `
|
| 19 |
with `pre_encoder` over all 13,690 test cells:
|
| 20 |
|
| 21 |
| File | Path | Description |
|
|
@@ -24,10 +24,10 @@ with `pre_encoder` over all 13,690 test cells:
|
|
| 24 |
| `importance_matrix.csv` | `outputs/interpretability/importance/zheng68k_pre_13690/importance_matrix.csv` | Same matrix with row and column labels |
|
| 25 |
| `enrichment_results.csv` | `outputs/interpretability/importance/zheng68k_pre_13690/enrichment_results.csv` | Enrichment-test results |
|
| 26 |
| `top_modules_per_celltype.json` | `outputs/interpretability/importance/zheng68k_pre_13690/top_modules_per_celltype.json` | Top-module details |
|
| 27 |
-
| `
|
| 28 |
|
| 29 |
Marker verification uses `importance_matrix.npy`, `importance_matrix.csv`, and
|
| 30 |
-
`
|
| 31 |
|
| 32 |
Publication figures use all files above plus ablation and permutation results.
|
| 33 |
|
|
|
|
| 5 |
## Dataset
|
| 6 |
|
| 7 |
- **Zheng68K**: 11 cell types, 68,450 cells, with 13,690 test cells.
|
| 8 |
+
- Model: BioModCellClassifier with attention-pooling head and 48M backbone.
|
| 9 |
- Baseline test accuracy: **83.1%**.
|
| 10 |
|
| 11 |
## Data Sources
|
|
|
|
| 13 |
These are historical experiment results. The analysis output files below are not
|
| 14 |
included in the repository; recreate them using the commands in `README.md` or
|
| 15 |
point the analysis scripts to the original run directory. The shared module JSON
|
| 16 |
+
is bundled under `model_dependencies/biomod_data_all/` at the repository root.
|
| 17 |
|
| 18 |
+
All analyses are based on the following core files, generated by `biomod_importance.py`
|
| 19 |
with `pre_encoder` over all 13,690 test cells:
|
| 20 |
|
| 21 |
| File | Path | Description |
|
|
|
|
| 24 |
| `importance_matrix.csv` | `outputs/interpretability/importance/zheng68k_pre_13690/importance_matrix.csv` | Same matrix with row and column labels |
|
| 25 |
| `enrichment_results.csv` | `outputs/interpretability/importance/zheng68k_pre_13690/enrichment_results.csv` | Enrichment-test results |
|
| 26 |
| `top_modules_per_celltype.json` | `outputs/interpretability/importance/zheng68k_pre_13690/top_modules_per_celltype.json` | Top-module details |
|
| 27 |
+
| `merged_biomods.json` | `model_dependencies/biomod_data_all/merged_biomods.json` | Gene composition of 660 modules |
|
| 28 |
|
| 29 |
Marker verification uses `importance_matrix.npy`, `importance_matrix.csv`, and
|
| 30 |
+
`merged_biomods.json`.
|
| 31 |
|
| 32 |
Publication figures use all files above plus ablation and permutation results.
|
| 33 |
|
|
@@ -14,7 +14,7 @@
|
|
| 14 |
- [x] `marker_gene_verification.py`: canonical marker verification.
|
| 15 |
- Result: 80% of markers are in the top 5%, median percentile = 0.9%.
|
| 16 |
- Figures: `fig_marker_ranks.pdf`, `fig_marker_summary_bar.pdf`.
|
| 17 |
-
- [x] `
|
| 18 |
- [x] `RESULTS.md`: result documentation.
|
| 19 |
- [x] `TODO.md`: this file.
|
| 20 |
|
|
|
|
| 14 |
- [x] `marker_gene_verification.py`: canonical marker verification.
|
| 15 |
- Result: 80% of markers are in the top 5%, median percentile = 0.9%.
|
| 16 |
- Figures: `fig_marker_ranks.pdf`, `fig_marker_summary_bar.pdf`.
|
| 17 |
+
- [x] `biomod_importance_publication.py`: publication-figure generator.
|
| 18 |
- [x] `RESULTS.md`: result documentation.
|
| 19 |
- [x] `TODO.md`: this file.
|
| 20 |
|
|
File without changes
|
|
@@ -33,10 +33,10 @@
|
|
| 33 |
|
| 34 |
SCRIPT_DIR = Path(__file__).resolve().parent
|
| 35 |
INTERP_DIR = SCRIPT_DIR.parent
|
| 36 |
-
|
| 37 |
-
SCLONG_ROOT =
|
| 38 |
|
| 39 |
-
for p in (str(
|
| 40 |
if p not in sys.path:
|
| 41 |
sys.path.insert(0, p)
|
| 42 |
|
|
@@ -89,8 +89,8 @@ def __getitem__(self, idx):
|
|
| 89 |
|
| 90 |
def load_finetuned_model(args, device: torch.device):
|
| 91 |
"""Load the full finetuned classifier (backbone + pooler + head)."""
|
| 92 |
-
from
|
| 93 |
-
from
|
| 94 |
|
| 95 |
ckpt_path = Path(args.finetune_checkpoint)
|
| 96 |
checkpoint = torch.load(ckpt_path, map_location=device)
|
|
@@ -106,13 +106,13 @@ def load_finetuned_model(args, device: torch.device):
|
|
| 106 |
with open(config_pkl_path, "rb") as f:
|
| 107 |
config_pkl = pickle.load(f)
|
| 108 |
|
| 109 |
-
|
| 110 |
if isinstance(config_pkl, dict) and "module_sets" in config_pkl:
|
| 111 |
-
|
| 112 |
elif "module_sets" in backbone_config:
|
| 113 |
-
|
| 114 |
else:
|
| 115 |
-
|
| 116 |
module_data_dir=args.module_data_dir,
|
| 117 |
gene_list_path=args.gene_list_path,
|
| 118 |
gene_mapping_path=args.gene_mapping_path,
|
|
@@ -141,10 +141,10 @@ def load_finetuned_model(args, device: torch.device):
|
|
| 141 |
dec_heads = backbone_config.get("dec_heads", 8)
|
| 142 |
dec_dim_head = backbone_config.get("dec_dim_head", 64)
|
| 143 |
|
| 144 |
-
backbone =
|
| 145 |
-
module_sets=
|
| 146 |
num_genes=num_genes,
|
| 147 |
-
max_seq_len=len(
|
| 148 |
base_dim=base_dim,
|
| 149 |
enc_depth=enc_depth,
|
| 150 |
enc_heads=enc_heads,
|
|
@@ -171,10 +171,10 @@ def load_finetuned_model(args, device: torch.device):
|
|
| 171 |
dec_dim_head=dec_dim_head,
|
| 172 |
)
|
| 173 |
|
| 174 |
-
sys.path.insert(0, str(
|
| 175 |
-
from
|
| 176 |
GatedAttentionPool,
|
| 177 |
-
|
| 178 |
ScGPTClsDecoder,
|
| 179 |
)
|
| 180 |
|
|
@@ -183,9 +183,9 @@ def load_finetuned_model(args, device: torch.device):
|
|
| 183 |
pool_hidden_dim = saved_args.get("pool_hidden_dim", 0)
|
| 184 |
cls_decoder_layers = saved_args.get("cls_decoder_layers", 3)
|
| 185 |
|
| 186 |
-
model =
|
| 187 |
backbone=backbone,
|
| 188 |
-
module_sets=
|
| 189 |
num_classes=num_classes,
|
| 190 |
head_type=head_type,
|
| 191 |
pool_hidden_dim=pool_hidden_dim,
|
|
@@ -199,9 +199,9 @@ def load_finetuned_model(args, device: torch.device):
|
|
| 199 |
|
| 200 |
logger.info(
|
| 201 |
"Loaded finetuned model: head=%s, classes=%d, modules=%d",
|
| 202 |
-
head_type, num_classes, len(
|
| 203 |
)
|
| 204 |
-
return model,
|
| 205 |
|
| 206 |
|
| 207 |
# ─── Data Loading ───────────────────────────────────────────────────────────
|
|
@@ -210,8 +210,8 @@ def load_test_data(args, label_classes: List[str], device: torch.device):
|
|
| 210 |
"""Load Zheng68K and reproduce the test split."""
|
| 211 |
import anndata as ad
|
| 212 |
|
| 213 |
-
sys.path.insert(0, str(
|
| 214 |
-
from
|
| 215 |
|
| 216 |
preprocessor = DataPreprocessor(
|
| 217 |
gene2ens_file=args.gene2ens_file,
|
|
@@ -276,7 +276,7 @@ def compute_importance_post_encoder(
|
|
| 276 |
if model.head_type == "cls":
|
| 277 |
module_tokens = latent[:, 1:, :]
|
| 278 |
else:
|
| 279 |
-
module_tokens = model.
|
| 280 |
|
| 281 |
baseline_logits = _classify_from_tokens(model, module_tokens)
|
| 282 |
y_tensor = torch.from_numpy(y_batch).to(device)
|
|
@@ -340,7 +340,7 @@ def _classify_from_tokens(model: nn.Module, module_tokens: torch.Tensor) -> torc
|
|
| 340 |
elif model.head_type == "attn_pool":
|
| 341 |
cell_emb, _ = model.pooler(module_tokens)
|
| 342 |
elif model.head_type == "gene_mean":
|
| 343 |
-
gene_tokens = model.
|
| 344 |
cell_emb = gene_tokens.mean(dim=1)
|
| 345 |
else:
|
| 346 |
raise ValueError(f"Unsupported head_type: {model.head_type}")
|
|
@@ -533,12 +533,12 @@ def run_enrichment(
|
|
| 533 |
|
| 534 |
# ─── Module Gene Mapping ─────────────────────────────────────────────────────
|
| 535 |
|
| 536 |
-
def
|
| 537 |
module_data_dir: str,
|
| 538 |
gene_list_path: str,
|
| 539 |
) -> Tuple[List[str], Dict[str, List[str]]]:
|
| 540 |
"""Return (module_names, {module_name: [gene_symbols]})."""
|
| 541 |
-
merged_path = Path(module_data_dir) / "
|
| 542 |
with open(merged_path, "r") as f:
|
| 543 |
merged = json.load(f)
|
| 544 |
|
|
@@ -582,7 +582,7 @@ def plot_importance_heatmap(
|
|
| 582 |
logger.info("Saved heatmap: %s", output_path)
|
| 583 |
|
| 584 |
|
| 585 |
-
def
|
| 586 |
importance: np.ndarray,
|
| 587 |
label_classes: List[str],
|
| 588 |
module_names: List[str],
|
|
@@ -628,8 +628,8 @@ def parse_args():
|
|
| 628 |
help="Path to best_model.pt from cell annotation finetuning")
|
| 629 |
p.add_argument("--config_pkl", required=True,
|
| 630 |
help="Path to pretrained model .pkl config")
|
| 631 |
-
p.add_argument("--module_data_dir", required=True,
|
| 632 |
-
help="Directory containing
|
| 633 |
p.add_argument("--gene_list_path", required=True,
|
| 634 |
help="Path to selected_genes_27k.txt")
|
| 635 |
p.add_argument("--gene_mapping_path", required=True)
|
|
@@ -677,8 +677,8 @@ def main():
|
|
| 677 |
setattr(args, attr, str(Path(val).resolve()))
|
| 678 |
|
| 679 |
logger.info("Loading finetuned model...")
|
| 680 |
-
model,
|
| 681 |
-
num_modules = len(
|
| 682 |
num_classes = len(label_classes)
|
| 683 |
|
| 684 |
logger.info("Loading test data...")
|
|
@@ -752,7 +752,7 @@ def main():
|
|
| 752 |
# Only rank 0 does enrichment, plotting, saving
|
| 753 |
if rank == 0:
|
| 754 |
logger.info("Building module-gene mapping...")
|
| 755 |
-
module_names, module_gene_symbols =
|
| 756 |
args.module_data_dir, args.gene_list_path
|
| 757 |
)
|
| 758 |
|
|
@@ -810,7 +810,7 @@ def main():
|
|
| 810 |
plot_importance_heatmap(
|
| 811 |
importance, label_classes, output_dir / "heatmap_importance.png"
|
| 812 |
)
|
| 813 |
-
|
| 814 |
importance, label_classes, module_names, output_dir
|
| 815 |
)
|
| 816 |
|
|
|
|
| 33 |
|
| 34 |
SCRIPT_DIR = Path(__file__).resolve().parent
|
| 35 |
INTERP_DIR = SCRIPT_DIR.parent
|
| 36 |
+
BIOMOD_ROOT = INTERP_DIR.parent
|
| 37 |
+
SCLONG_ROOT = BIOMOD_ROOT.parent
|
| 38 |
|
| 39 |
+
for p in (str(BIOMOD_ROOT), str(SCLONG_ROOT)):
|
| 40 |
if p not in sys.path:
|
| 41 |
sys.path.insert(0, p)
|
| 42 |
|
|
|
|
| 89 |
|
| 90 |
def load_finetuned_model(args, device: torch.device):
|
| 91 |
"""Load the full finetuned classifier (backbone + pooler + head)."""
|
| 92 |
+
from models_biomod import BioMod
|
| 93 |
+
from biomod_dataset import load_biomod_data
|
| 94 |
|
| 95 |
ckpt_path = Path(args.finetune_checkpoint)
|
| 96 |
checkpoint = torch.load(ckpt_path, map_location=device)
|
|
|
|
| 106 |
with open(config_pkl_path, "rb") as f:
|
| 107 |
config_pkl = pickle.load(f)
|
| 108 |
|
| 109 |
+
biomod_data = None
|
| 110 |
if isinstance(config_pkl, dict) and "module_sets" in config_pkl:
|
| 111 |
+
biomod_data = config_pkl["module_sets"]
|
| 112 |
elif "module_sets" in backbone_config:
|
| 113 |
+
biomod_data = backbone_config["module_sets"]
|
| 114 |
else:
|
| 115 |
+
biomod_data = load_biomod_data(
|
| 116 |
module_data_dir=args.module_data_dir,
|
| 117 |
gene_list_path=args.gene_list_path,
|
| 118 |
gene_mapping_path=args.gene_mapping_path,
|
|
|
|
| 141 |
dec_heads = backbone_config.get("dec_heads", 8)
|
| 142 |
dec_dim_head = backbone_config.get("dec_dim_head", 64)
|
| 143 |
|
| 144 |
+
backbone = BioMod(
|
| 145 |
+
module_sets=biomod_data,
|
| 146 |
num_genes=num_genes,
|
| 147 |
+
max_seq_len=len(biomod_data),
|
| 148 |
base_dim=base_dim,
|
| 149 |
enc_depth=enc_depth,
|
| 150 |
enc_heads=enc_heads,
|
|
|
|
| 171 |
dec_dim_head=dec_dim_head,
|
| 172 |
)
|
| 173 |
|
| 174 |
+
sys.path.insert(0, str(BIOMOD_ROOT / "cell_annotation"))
|
| 175 |
+
from biomod_finetune import (
|
| 176 |
GatedAttentionPool,
|
| 177 |
+
BioModCellClassifier,
|
| 178 |
ScGPTClsDecoder,
|
| 179 |
)
|
| 180 |
|
|
|
|
| 183 |
pool_hidden_dim = saved_args.get("pool_hidden_dim", 0)
|
| 184 |
cls_decoder_layers = saved_args.get("cls_decoder_layers", 3)
|
| 185 |
|
| 186 |
+
model = BioModCellClassifier(
|
| 187 |
backbone=backbone,
|
| 188 |
+
module_sets=biomod_data,
|
| 189 |
num_classes=num_classes,
|
| 190 |
head_type=head_type,
|
| 191 |
pool_hidden_dim=pool_hidden_dim,
|
|
|
|
| 199 |
|
| 200 |
logger.info(
|
| 201 |
"Loaded finetuned model: head=%s, classes=%d, modules=%d",
|
| 202 |
+
head_type, num_classes, len(biomod_data),
|
| 203 |
)
|
| 204 |
+
return model, biomod_data, label_classes, head_type
|
| 205 |
|
| 206 |
|
| 207 |
# ─── Data Loading ───────────────────────────────────────────────────────────
|
|
|
|
| 210 |
"""Load Zheng68K and reproduce the test split."""
|
| 211 |
import anndata as ad
|
| 212 |
|
| 213 |
+
sys.path.insert(0, str(BIOMOD_ROOT / "cell_annotation"))
|
| 214 |
+
from biomod_finetune import DataPreprocessor
|
| 215 |
|
| 216 |
preprocessor = DataPreprocessor(
|
| 217 |
gene2ens_file=args.gene2ens_file,
|
|
|
|
| 276 |
if model.head_type == "cls":
|
| 277 |
module_tokens = latent[:, 1:, :]
|
| 278 |
else:
|
| 279 |
+
module_tokens = model._restore_biomod_order(latent, ids_restore)
|
| 280 |
|
| 281 |
baseline_logits = _classify_from_tokens(model, module_tokens)
|
| 282 |
y_tensor = torch.from_numpy(y_batch).to(device)
|
|
|
|
| 340 |
elif model.head_type == "attn_pool":
|
| 341 |
cell_emb, _ = model.pooler(module_tokens)
|
| 342 |
elif model.head_type == "gene_mean":
|
| 343 |
+
gene_tokens = model._biomod_to_gene(module_tokens)
|
| 344 |
cell_emb = gene_tokens.mean(dim=1)
|
| 345 |
else:
|
| 346 |
raise ValueError(f"Unsupported head_type: {model.head_type}")
|
|
|
|
| 533 |
|
| 534 |
# ─── Module Gene Mapping ─────────────────────────────────────────────────────
|
| 535 |
|
| 536 |
+
def build_biomod_gene_mapping(
|
| 537 |
module_data_dir: str,
|
| 538 |
gene_list_path: str,
|
| 539 |
) -> Tuple[List[str], Dict[str, List[str]]]:
|
| 540 |
"""Return (module_names, {module_name: [gene_symbols]})."""
|
| 541 |
+
merged_path = Path(module_data_dir) / "merged_biomods.json"
|
| 542 |
with open(merged_path, "r") as f:
|
| 543 |
merged = json.load(f)
|
| 544 |
|
|
|
|
| 582 |
logger.info("Saved heatmap: %s", output_path)
|
| 583 |
|
| 584 |
|
| 585 |
+
def plot_top_biomods_bar(
|
| 586 |
importance: np.ndarray,
|
| 587 |
label_classes: List[str],
|
| 588 |
module_names: List[str],
|
|
|
|
| 628 |
help="Path to best_model.pt from cell annotation finetuning")
|
| 629 |
p.add_argument("--config_pkl", required=True,
|
| 630 |
help="Path to pretrained model .pkl config")
|
| 631 |
+
p.add_argument("--biomod_data_dir", "--module_data_dir", required=True,
|
| 632 |
+
help="Directory containing merged_biomods.json", dest='module_data_dir')
|
| 633 |
p.add_argument("--gene_list_path", required=True,
|
| 634 |
help="Path to selected_genes_27k.txt")
|
| 635 |
p.add_argument("--gene_mapping_path", required=True)
|
|
|
|
| 677 |
setattr(args, attr, str(Path(val).resolve()))
|
| 678 |
|
| 679 |
logger.info("Loading finetuned model...")
|
| 680 |
+
model, biomod_data, label_classes, head_type = load_finetuned_model(args, device)
|
| 681 |
+
num_modules = len(biomod_data)
|
| 682 |
num_classes = len(label_classes)
|
| 683 |
|
| 684 |
logger.info("Loading test data...")
|
|
|
|
| 752 |
# Only rank 0 does enrichment, plotting, saving
|
| 753 |
if rank == 0:
|
| 754 |
logger.info("Building module-gene mapping...")
|
| 755 |
+
module_names, module_gene_symbols = build_biomod_gene_mapping(
|
| 756 |
args.module_data_dir, args.gene_list_path
|
| 757 |
)
|
| 758 |
|
|
|
|
| 810 |
plot_importance_heatmap(
|
| 811 |
importance, label_classes, output_dir / "heatmap_importance.png"
|
| 812 |
)
|
| 813 |
+
plot_top_biomods_bar(
|
| 814 |
importance, label_classes, module_names, output_dir
|
| 815 |
)
|
| 816 |
|
|
@@ -31,9 +31,9 @@
|
|
| 31 |
from pub_utils import (
|
| 32 |
CELL_TYPE_COLORS,
|
| 33 |
MODULE_FAMILY_COLORS,
|
| 34 |
-
|
| 35 |
load_importance_results,
|
| 36 |
-
|
| 37 |
savefig,
|
| 38 |
setup_publication_style,
|
| 39 |
)
|
|
@@ -204,7 +204,7 @@ def plot_importance_heatmap_clustered(results_dir, module_data_dir, output_dir):
|
|
| 204 |
label_classes = data["label_classes"]
|
| 205 |
module_names = data["module_names"]
|
| 206 |
|
| 207 |
-
families =
|
| 208 |
family_colors = [MODULE_FAMILY_COLORS.get(f, "#CCCCCC") for f in families]
|
| 209 |
|
| 210 |
# Cluster modules
|
|
@@ -329,8 +329,8 @@ def parse_args():
|
|
| 329 |
help="Ablation results directory")
|
| 330 |
p.add_argument("--permutation_dir",
|
| 331 |
help="Permutation results directory")
|
| 332 |
-
p.add_argument("--module_data_dir", required=True,
|
| 333 |
-
help="Module data directory (
|
| 334 |
p.add_argument("--output_dir", required=True)
|
| 335 |
return p.parse_args()
|
| 336 |
|
|
|
|
| 31 |
from pub_utils import (
|
| 32 |
CELL_TYPE_COLORS,
|
| 33 |
MODULE_FAMILY_COLORS,
|
| 34 |
+
get_biomod_families,
|
| 35 |
load_importance_results,
|
| 36 |
+
load_biomod_gene_mapping,
|
| 37 |
savefig,
|
| 38 |
setup_publication_style,
|
| 39 |
)
|
|
|
|
| 204 |
label_classes = data["label_classes"]
|
| 205 |
module_names = data["module_names"]
|
| 206 |
|
| 207 |
+
families = get_biomod_families(module_names)
|
| 208 |
family_colors = [MODULE_FAMILY_COLORS.get(f, "#CCCCCC") for f in families]
|
| 209 |
|
| 210 |
# Cluster modules
|
|
|
|
| 329 |
help="Ablation results directory")
|
| 330 |
p.add_argument("--permutation_dir",
|
| 331 |
help="Permutation results directory")
|
| 332 |
+
p.add_argument("--biomod_data_dir", "--module_data_dir", required=True,
|
| 333 |
+
help="Module data directory (merged_biomods.json)", dest='module_data_dir')
|
| 334 |
p.add_argument("--output_dir", required=True)
|
| 335 |
return p.parse_args()
|
| 336 |
|
|
File without changes
|
|
@@ -29,10 +29,10 @@
|
|
| 29 |
|
| 30 |
SCRIPT_DIR = Path(__file__).resolve().parent
|
| 31 |
INTERP_DIR = SCRIPT_DIR.parent
|
| 32 |
-
|
| 33 |
-
SCLONG_ROOT =
|
| 34 |
|
| 35 |
-
for p in (str(SCRIPT_DIR), str(
|
| 36 |
if p not in sys.path:
|
| 37 |
sys.path.insert(0, p)
|
| 38 |
|
|
@@ -40,10 +40,10 @@
|
|
| 40 |
compute_masked_accuracy_post_encoder,
|
| 41 |
encode_all_cells,
|
| 42 |
)
|
| 43 |
-
from
|
| 44 |
CellDataset,
|
| 45 |
_classify_from_tokens,
|
| 46 |
-
|
| 47 |
load_finetuned_model,
|
| 48 |
load_test_data,
|
| 49 |
)
|
|
@@ -296,7 +296,7 @@ def parse_args():
|
|
| 296 |
p = argparse.ArgumentParser(description="Family-level ablation experiment")
|
| 297 |
p.add_argument("--finetune_checkpoint", required=True)
|
| 298 |
p.add_argument("--config_pkl", required=True)
|
| 299 |
-
p.add_argument("--module_data_dir", required=True)
|
| 300 |
p.add_argument("--gene_list_path", required=True)
|
| 301 |
p.add_argument("--gene_mapping_path", required=True)
|
| 302 |
p.add_argument("--gene2vec_data_path", required=True)
|
|
@@ -329,8 +329,8 @@ def main():
|
|
| 329 |
setattr(args, attr, str(Path(val).resolve()))
|
| 330 |
|
| 331 |
logger.info("Loading model...")
|
| 332 |
-
model,
|
| 333 |
-
module_names, _ =
|
| 334 |
family_indices = get_family_indices(module_names)
|
| 335 |
for fam in FAMILY_ORDER:
|
| 336 |
logger.info(" %s: %d modules", fam, len(family_indices[fam]))
|
|
|
|
| 29 |
|
| 30 |
SCRIPT_DIR = Path(__file__).resolve().parent
|
| 31 |
INTERP_DIR = SCRIPT_DIR.parent
|
| 32 |
+
BIOMOD_ROOT = INTERP_DIR.parent
|
| 33 |
+
SCLONG_ROOT = BIOMOD_ROOT.parent
|
| 34 |
|
| 35 |
+
for p in (str(SCRIPT_DIR), str(BIOMOD_ROOT), str(SCLONG_ROOT)):
|
| 36 |
if p not in sys.path:
|
| 37 |
sys.path.insert(0, p)
|
| 38 |
|
|
|
|
| 40 |
compute_masked_accuracy_post_encoder,
|
| 41 |
encode_all_cells,
|
| 42 |
)
|
| 43 |
+
from biomod_importance import (
|
| 44 |
CellDataset,
|
| 45 |
_classify_from_tokens,
|
| 46 |
+
build_biomod_gene_mapping,
|
| 47 |
load_finetuned_model,
|
| 48 |
load_test_data,
|
| 49 |
)
|
|
|
|
| 296 |
p = argparse.ArgumentParser(description="Family-level ablation experiment")
|
| 297 |
p.add_argument("--finetune_checkpoint", required=True)
|
| 298 |
p.add_argument("--config_pkl", required=True)
|
| 299 |
+
p.add_argument("--biomod_data_dir", "--module_data_dir", required=True, dest='module_data_dir')
|
| 300 |
p.add_argument("--gene_list_path", required=True)
|
| 301 |
p.add_argument("--gene_mapping_path", required=True)
|
| 302 |
p.add_argument("--gene2vec_data_path", required=True)
|
|
|
|
| 329 |
setattr(args, attr, str(Path(val).resolve()))
|
| 330 |
|
| 331 |
logger.info("Loading model...")
|
| 332 |
+
model, biomod_data, label_classes, head_type = load_finetuned_model(args, device)
|
| 333 |
+
module_names, _ = build_biomod_gene_mapping(args.module_data_dir, args.gene_list_path)
|
| 334 |
family_indices = get_family_indices(module_names)
|
| 335 |
for fam in FAMILY_ORDER:
|
| 336 |
logger.info(" %s: %d modules", fam, len(family_indices[fam]))
|
|
File without changes
|
|
@@ -28,11 +28,11 @@
|
|
| 28 |
from pub_utils import (
|
| 29 |
CANONICAL_MARKERS,
|
| 30 |
CELL_TYPE_COLORS,
|
| 31 |
-
|
| 32 |
-
|
| 33 |
importance_rank_percentile,
|
| 34 |
load_importance_results,
|
| 35 |
-
|
| 36 |
savefig,
|
| 37 |
setup_publication_style,
|
| 38 |
)
|
|
@@ -55,7 +55,7 @@ def compute_marker_ranks(
|
|
| 55 |
ranked_indices = np.argsort(-imp_row)
|
| 56 |
|
| 57 |
for marker in markers:
|
| 58 |
-
containing_modules =
|
| 59 |
if not containing_modules:
|
| 60 |
rows.append({
|
| 61 |
"cell_type": cls_name,
|
|
@@ -89,7 +89,7 @@ def compute_marker_ranks(
|
|
| 89 |
"found_in_modules": True,
|
| 90 |
"num_modules_containing": len(containing_modules),
|
| 91 |
"best_module": module_names[best_pi],
|
| 92 |
-
"best_module_family":
|
| 93 |
"best_rank": best_rank + 1,
|
| 94 |
"rank_percentile": 100.0 * best_rank / num_modules,
|
| 95 |
"best_importance": float(imp_row[best_pi]),
|
|
@@ -252,8 +252,8 @@ def parse_args():
|
|
| 252 |
help="Path to importance_matrix.npy")
|
| 253 |
p.add_argument("--importance_csv", required=True,
|
| 254 |
help="Path to importance_matrix.csv (for labels)")
|
| 255 |
-
p.add_argument("--module_data_dir", required=True,
|
| 256 |
-
help="Directory containing
|
| 257 |
p.add_argument("--output_dir", required=True)
|
| 258 |
return p.parse_args()
|
| 259 |
|
|
@@ -268,7 +268,7 @@ def main():
|
|
| 268 |
label_classes = list(imp_csv.index)
|
| 269 |
module_names = list(imp_csv.columns)
|
| 270 |
|
| 271 |
-
_, module_gene_symbols =
|
| 272 |
|
| 273 |
print(f"Importance matrix: {importance.shape}")
|
| 274 |
print(f"Cell types: {len(label_classes)}")
|
|
|
|
| 28 |
from pub_utils import (
|
| 29 |
CANONICAL_MARKERS,
|
| 30 |
CELL_TYPE_COLORS,
|
| 31 |
+
find_gene_biomods,
|
| 32 |
+
get_biomod_family,
|
| 33 |
importance_rank_percentile,
|
| 34 |
load_importance_results,
|
| 35 |
+
load_biomod_gene_mapping,
|
| 36 |
savefig,
|
| 37 |
setup_publication_style,
|
| 38 |
)
|
|
|
|
| 55 |
ranked_indices = np.argsort(-imp_row)
|
| 56 |
|
| 57 |
for marker in markers:
|
| 58 |
+
containing_modules = find_gene_biomods(marker, module_gene_symbols)
|
| 59 |
if not containing_modules:
|
| 60 |
rows.append({
|
| 61 |
"cell_type": cls_name,
|
|
|
|
| 89 |
"found_in_modules": True,
|
| 90 |
"num_modules_containing": len(containing_modules),
|
| 91 |
"best_module": module_names[best_pi],
|
| 92 |
+
"best_module_family": get_biomod_family(module_names[best_pi]),
|
| 93 |
"best_rank": best_rank + 1,
|
| 94 |
"rank_percentile": 100.0 * best_rank / num_modules,
|
| 95 |
"best_importance": float(imp_row[best_pi]),
|
|
|
|
| 252 |
help="Path to importance_matrix.npy")
|
| 253 |
p.add_argument("--importance_csv", required=True,
|
| 254 |
help="Path to importance_matrix.csv (for labels)")
|
| 255 |
+
p.add_argument("--biomod_data_dir", "--module_data_dir", required=True,
|
| 256 |
+
help="Directory containing merged_biomods.json", dest='module_data_dir')
|
| 257 |
p.add_argument("--output_dir", required=True)
|
| 258 |
return p.parse_args()
|
| 259 |
|
|
|
|
| 268 |
label_classes = list(imp_csv.index)
|
| 269 |
module_names = list(imp_csv.columns)
|
| 270 |
|
| 271 |
+
_, module_gene_symbols = load_biomod_gene_mapping(args.module_data_dir)
|
| 272 |
|
| 273 |
print(f"Importance matrix: {importance.shape}")
|
| 274 |
print(f"Cell types: {len(label_classes)}")
|
|
@@ -29,14 +29,14 @@
|
|
| 29 |
|
| 30 |
SCRIPT_DIR = Path(__file__).resolve().parent
|
| 31 |
INTERP_DIR = SCRIPT_DIR.parent
|
| 32 |
-
|
| 33 |
-
SCLONG_ROOT =
|
| 34 |
|
| 35 |
-
for p in (str(SCRIPT_DIR), str(
|
| 36 |
if p not in sys.path:
|
| 37 |
sys.path.insert(0, p)
|
| 38 |
|
| 39 |
-
from
|
| 40 |
CellDataset,
|
| 41 |
_classify_from_tokens,
|
| 42 |
load_finetuned_model,
|
|
@@ -63,7 +63,7 @@ def encode_all_cells(model, dataloader, preprocessor, reindex_state, device):
|
|
| 63 |
if model.head_type == "cls":
|
| 64 |
module_tokens = latent[:, 1:, :]
|
| 65 |
else:
|
| 66 |
-
module_tokens = model.
|
| 67 |
all_tokens.append(module_tokens.cpu())
|
| 68 |
all_labels.append(y_batch.numpy())
|
| 69 |
|
|
@@ -248,7 +248,7 @@ def parse_args():
|
|
| 248 |
p = argparse.ArgumentParser(description="Masking ablation curves")
|
| 249 |
p.add_argument("--finetune_checkpoint", required=True)
|
| 250 |
p.add_argument("--config_pkl", required=True)
|
| 251 |
-
p.add_argument("--module_data_dir", required=True)
|
| 252 |
p.add_argument("--gene_list_path", required=True)
|
| 253 |
p.add_argument("--gene_mapping_path", required=True)
|
| 254 |
p.add_argument("--gene2vec_data_path", required=True)
|
|
@@ -290,8 +290,8 @@ def main():
|
|
| 290 |
setattr(args, attr, str(Path(val).resolve()))
|
| 291 |
|
| 292 |
logger.info("Loading model...")
|
| 293 |
-
model,
|
| 294 |
-
num_modules = len(
|
| 295 |
|
| 296 |
logger.info("Loading test data...")
|
| 297 |
X_test, y_test, le, preprocessor, reindex_state = load_test_data(
|
|
|
|
| 29 |
|
| 30 |
SCRIPT_DIR = Path(__file__).resolve().parent
|
| 31 |
INTERP_DIR = SCRIPT_DIR.parent
|
| 32 |
+
BIOMOD_ROOT = INTERP_DIR.parent
|
| 33 |
+
SCLONG_ROOT = BIOMOD_ROOT.parent
|
| 34 |
|
| 35 |
+
for p in (str(SCRIPT_DIR), str(BIOMOD_ROOT), str(SCLONG_ROOT)):
|
| 36 |
if p not in sys.path:
|
| 37 |
sys.path.insert(0, p)
|
| 38 |
|
| 39 |
+
from biomod_importance import (
|
| 40 |
CellDataset,
|
| 41 |
_classify_from_tokens,
|
| 42 |
load_finetuned_model,
|
|
|
|
| 63 |
if model.head_type == "cls":
|
| 64 |
module_tokens = latent[:, 1:, :]
|
| 65 |
else:
|
| 66 |
+
module_tokens = model._restore_biomod_order(latent, ids_restore)
|
| 67 |
all_tokens.append(module_tokens.cpu())
|
| 68 |
all_labels.append(y_batch.numpy())
|
| 69 |
|
|
|
|
| 248 |
p = argparse.ArgumentParser(description="Masking ablation curves")
|
| 249 |
p.add_argument("--finetune_checkpoint", required=True)
|
| 250 |
p.add_argument("--config_pkl", required=True)
|
| 251 |
+
p.add_argument("--biomod_data_dir", "--module_data_dir", required=True, dest='module_data_dir')
|
| 252 |
p.add_argument("--gene_list_path", required=True)
|
| 253 |
p.add_argument("--gene_mapping_path", required=True)
|
| 254 |
p.add_argument("--gene2vec_data_path", required=True)
|
|
|
|
| 290 |
setattr(args, attr, str(Path(val).resolve()))
|
| 291 |
|
| 292 |
logger.info("Loading model...")
|
| 293 |
+
model, biomod_data, label_classes, head_type = load_finetuned_model(args, device)
|
| 294 |
+
num_modules = len(biomod_data)
|
| 295 |
|
| 296 |
logger.info("Loading test data...")
|
| 297 |
X_test, y_test, le, preprocessor, reindex_state = load_test_data(
|
|
@@ -140,15 +140,15 @@ def load_importance_results(results_dir) -> dict:
|
|
| 140 |
return data
|
| 141 |
|
| 142 |
|
| 143 |
-
def
|
| 144 |
-
"""Load
|
| 145 |
|
| 146 |
Returns:
|
| 147 |
module_names: ordered list of module names
|
| 148 |
module_gene_symbols: dict mapping module_name -> list of gene symbols
|
| 149 |
"""
|
| 150 |
module_data_dir = Path(module_data_dir)
|
| 151 |
-
with open(module_data_dir / "
|
| 152 |
modules = json.load(f)
|
| 153 |
module_names = list(modules.keys())
|
| 154 |
module_gene_symbols = {}
|
|
@@ -160,7 +160,7 @@ def load_module_gene_mapping(module_data_dir) -> Tuple[List[str], Dict[str, List
|
|
| 160 |
return module_names, module_gene_symbols
|
| 161 |
|
| 162 |
|
| 163 |
-
def
|
| 164 |
"""Determine which family a module belongs to based on its name prefix."""
|
| 165 |
if module_name.startswith("module1_hallmarks"):
|
| 166 |
return "Hallmarks"
|
|
@@ -173,14 +173,14 @@ def get_module_family(module_name: str) -> str:
|
|
| 173 |
return "Unknown"
|
| 174 |
|
| 175 |
|
| 176 |
-
def
|
| 177 |
"""Get family labels for a list of module names."""
|
| 178 |
-
return [
|
| 179 |
|
| 180 |
|
| 181 |
# ─── Analysis Helpers ────────────────────────────────────────────────────────
|
| 182 |
|
| 183 |
-
def
|
| 184 |
gene_symbol: str, module_gene_symbols: Dict[str, List[str]]
|
| 185 |
) -> List[str]:
|
| 186 |
"""Find all modules containing a given gene symbol."""
|
|
|
|
| 140 |
return data
|
| 141 |
|
| 142 |
|
| 143 |
+
def load_biomod_gene_mapping(module_data_dir) -> Tuple[List[str], Dict[str, List[str]]]:
|
| 144 |
+
"""Load merged_biomods.json and return (module_names, module_gene_symbols).
|
| 145 |
|
| 146 |
Returns:
|
| 147 |
module_names: ordered list of module names
|
| 148 |
module_gene_symbols: dict mapping module_name -> list of gene symbols
|
| 149 |
"""
|
| 150 |
module_data_dir = Path(module_data_dir)
|
| 151 |
+
with open(module_data_dir / "merged_biomods.json") as f:
|
| 152 |
modules = json.load(f)
|
| 153 |
module_names = list(modules.keys())
|
| 154 |
module_gene_symbols = {}
|
|
|
|
| 160 |
return module_names, module_gene_symbols
|
| 161 |
|
| 162 |
|
| 163 |
+
def get_biomod_family(module_name: str) -> str:
|
| 164 |
"""Determine which family a module belongs to based on its name prefix."""
|
| 165 |
if module_name.startswith("module1_hallmarks"):
|
| 166 |
return "Hallmarks"
|
|
|
|
| 173 |
return "Unknown"
|
| 174 |
|
| 175 |
|
| 176 |
+
def get_biomod_families(module_names: List[str]) -> List[str]:
|
| 177 |
"""Get family labels for a list of module names."""
|
| 178 |
+
return [get_biomod_family(p) for p in module_names]
|
| 179 |
|
| 180 |
|
| 181 |
# ─── Analysis Helpers ────────────────────────────────────────────────────────
|
| 182 |
|
| 183 |
+
def find_gene_biomods(
|
| 184 |
gene_symbol: str, module_gene_symbols: Dict[str, List[str]]
|
| 185 |
) -> List[str]:
|
| 186 |
"""Find all modules containing a given gene symbol."""
|
|
@@ -27,14 +27,14 @@
|
|
| 27 |
|
| 28 |
SCRIPT_DIR = Path(__file__).resolve().parent
|
| 29 |
INTERP_DIR = SCRIPT_DIR.parent
|
| 30 |
-
|
| 31 |
-
SCLONG_ROOT =
|
| 32 |
|
| 33 |
-
for p in (str(SCRIPT_DIR), str(
|
| 34 |
if p not in sys.path:
|
| 35 |
sys.path.insert(0, p)
|
| 36 |
|
| 37 |
-
from
|
| 38 |
CellDataset,
|
| 39 |
_classify_from_tokens,
|
| 40 |
load_finetuned_model,
|
|
@@ -48,7 +48,7 @@
|
|
| 48 |
# ─── Mode A: Shuffle Modules (GPU) ──────────────────────────────────────────
|
| 49 |
|
| 50 |
@torch.no_grad()
|
| 51 |
-
def
|
| 52 |
model, dataloader, preprocessor, reindex_state,
|
| 53 |
num_modules, num_classes, device, seed,
|
| 54 |
):
|
|
@@ -77,7 +77,7 @@ def compute_importance_shuffled_modules(
|
|
| 77 |
if model.head_type == "cls":
|
| 78 |
module_tokens = latent[:, 1:, :]
|
| 79 |
else:
|
| 80 |
-
module_tokens = model.
|
| 81 |
|
| 82 |
# Permute module token order (breaks module identity)
|
| 83 |
module_tokens_shuffled = module_tokens[:, perm, :]
|
|
@@ -206,7 +206,7 @@ def parse_args():
|
|
| 206 |
# Model args (for shuffle_modules mode)
|
| 207 |
p.add_argument("--finetune_checkpoint")
|
| 208 |
p.add_argument("--config_pkl")
|
| 209 |
-
p.add_argument("--module_data_dir")
|
| 210 |
p.add_argument("--gene_list_path")
|
| 211 |
p.add_argument("--gene_mapping_path")
|
| 212 |
p.add_argument("--gene2vec_data_path")
|
|
@@ -256,7 +256,7 @@ def main():
|
|
| 256 |
setattr(args, attr, str(Path(val).resolve()))
|
| 257 |
|
| 258 |
logger.info("Loading model for shuffle_modules mode...")
|
| 259 |
-
model,
|
| 260 |
|
| 261 |
logger.info("Loading test data...")
|
| 262 |
X_test, y_test, le, preprocessor, reindex_state = load_test_data(
|
|
@@ -273,7 +273,7 @@ def main():
|
|
| 273 |
)
|
| 274 |
for pi in range(n_perms_a):
|
| 275 |
logger.info(" Permutation %d/%d...", pi + 1, n_perms_a)
|
| 276 |
-
null_module_shuffle[pi] =
|
| 277 |
model, dataloader, preprocessor, reindex_state,
|
| 278 |
num_modules, num_classes, device,
|
| 279 |
seed=args.seed + pi + 1000,
|
|
|
|
| 27 |
|
| 28 |
SCRIPT_DIR = Path(__file__).resolve().parent
|
| 29 |
INTERP_DIR = SCRIPT_DIR.parent
|
| 30 |
+
BIOMOD_ROOT = INTERP_DIR.parent
|
| 31 |
+
SCLONG_ROOT = BIOMOD_ROOT.parent
|
| 32 |
|
| 33 |
+
for p in (str(SCRIPT_DIR), str(BIOMOD_ROOT), str(SCLONG_ROOT)):
|
| 34 |
if p not in sys.path:
|
| 35 |
sys.path.insert(0, p)
|
| 36 |
|
| 37 |
+
from biomod_importance import (
|
| 38 |
CellDataset,
|
| 39 |
_classify_from_tokens,
|
| 40 |
load_finetuned_model,
|
|
|
|
| 48 |
# ─── Mode A: Shuffle Modules (GPU) ──────────────────────────────────────────
|
| 49 |
|
| 50 |
@torch.no_grad()
|
| 51 |
+
def compute_importance_shuffled_biomods(
|
| 52 |
model, dataloader, preprocessor, reindex_state,
|
| 53 |
num_modules, num_classes, device, seed,
|
| 54 |
):
|
|
|
|
| 77 |
if model.head_type == "cls":
|
| 78 |
module_tokens = latent[:, 1:, :]
|
| 79 |
else:
|
| 80 |
+
module_tokens = model._restore_biomod_order(latent, ids_restore)
|
| 81 |
|
| 82 |
# Permute module token order (breaks module identity)
|
| 83 |
module_tokens_shuffled = module_tokens[:, perm, :]
|
|
|
|
| 206 |
# Model args (for shuffle_modules mode)
|
| 207 |
p.add_argument("--finetune_checkpoint")
|
| 208 |
p.add_argument("--config_pkl")
|
| 209 |
+
p.add_argument("--biomod_data_dir", "--module_data_dir", dest='module_data_dir')
|
| 210 |
p.add_argument("--gene_list_path")
|
| 211 |
p.add_argument("--gene_mapping_path")
|
| 212 |
p.add_argument("--gene2vec_data_path")
|
|
|
|
| 256 |
setattr(args, attr, str(Path(val).resolve()))
|
| 257 |
|
| 258 |
logger.info("Loading model for shuffle_modules mode...")
|
| 259 |
+
model, biomod_data, label_classes, _ = load_finetuned_model(args, device)
|
| 260 |
|
| 261 |
logger.info("Loading test data...")
|
| 262 |
X_test, y_test, le, preprocessor, reindex_state = load_test_data(
|
|
|
|
| 273 |
)
|
| 274 |
for pi in range(n_perms_a):
|
| 275 |
logger.info(" Permutation %d/%d...", pi + 1, n_perms_a)
|
| 276 |
+
null_module_shuffle[pi] = compute_importance_shuffled_biomods(
|
| 277 |
model, dataloader, preprocessor, reindex_state,
|
| 278 |
num_modules, num_classes, device,
|
| 279 |
seed=args.seed + pi + 1000,
|
|
File without changes
|
|
@@ -11,15 +11,15 @@ gene names do not carry phrase-level semantics.
|
|
| 11 |
|
| 12 |
| Script | Function |
|
| 13 |
| --- | --- |
|
| 14 |
-
| `
|
| 15 |
-
| `
|
| 16 |
-
| `
|
| 17 |
| `fetch_reactome_descriptions.py` | Fetch `summaryText` for 491 pathways from the Reactome API |
|
| 18 |
|
| 19 |
## Commands
|
| 20 |
|
| 21 |
Run from the BioMod repository root. Generate each `module_embeddings.npy` with
|
| 22 |
-
`BioMod/interpretability/
|
| 23 |
`pretrain_subset` output requires a separately supplied pretraining dataset;
|
| 24 |
the `pancreas_scib` example uses the bundled pancreas h5ad file. Existing result
|
| 25 |
tables below describe historical runs; their raw output directories are not
|
|
@@ -41,24 +41,24 @@ pip install sentence-transformers -i https://pypi.tuna.tsinghua.edu.cn/simple
|
|
| 41 |
|
| 42 |
```bash
|
| 43 |
# pretrain_subset is recommended because averaging over 50k cells is more stable.
|
| 44 |
-
python BioMod/interpretability/
|
| 45 |
-
--
|
| 46 |
-
--
|
| 47 |
--sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
|
| 48 |
--n_permutations 1000 \
|
| 49 |
--output_dir outputs/interpretability/text_alignment/pretrain_subset_v2
|
| 50 |
|
| 51 |
# pancreas_scib validates cross-dataset consistency.
|
| 52 |
-
python BioMod/interpretability/
|
| 53 |
-
--
|
| 54 |
-
--
|
| 55 |
--sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
|
| 56 |
--n_permutations 1000 \
|
| 57 |
--output_dir outputs/interpretability/text_alignment/pancreas_scib_v2
|
| 58 |
|
| 59 |
# Quick mode: skip LOO retrieval and run only RSA + CKA.
|
| 60 |
-
python BioMod/interpretability/
|
| 61 |
-
--
|
| 62 |
--sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
|
| 63 |
--skip_loo \
|
| 64 |
--output_dir outputs/interpretability/text_alignment/pretrain_subset_quick
|
|
@@ -68,24 +68,24 @@ python BioMod/interpretability/module_text_alignment/module_text_alignment.py \
|
|
| 68 |
|
| 69 |
```bash
|
| 70 |
# Re-fetch if needed. Requires network access to reactome.org and takes about 2 minutes.
|
| 71 |
-
python BioMod/interpretability/
|
| 72 |
-
--
|
| 73 |
--output reactome_descriptions.json --delay 0.05
|
| 74 |
```
|
| 75 |
|
| 76 |
The command writes refreshed descriptions to `reactome_descriptions.json` in the
|
| 77 |
repository root. The analysis examples below use the bundled
|
| 78 |
-
`BioMod/interpretability/
|
| 79 |
set `--descriptions_file reactome_descriptions.json` to use the refreshed output.
|
| 80 |
|
| 81 |
### 4. Extended Analysis: Reactome Categories, Description-Based RSA, Hallmark Deep Dive
|
| 82 |
|
| 83 |
```bash
|
| 84 |
-
python BioMod/interpretability/
|
| 85 |
-
--
|
| 86 |
-
--
|
| 87 |
--sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
|
| 88 |
-
--descriptions_file BioMod/interpretability/
|
| 89 |
--n_permutations 1000 \
|
| 90 |
--output_dir outputs/interpretability/text_alignment/pretrain_subset_v2_extended
|
| 91 |
```
|
|
@@ -94,23 +94,23 @@ python BioMod/interpretability/module_text_alignment/module_text_alignment_exten
|
|
| 94 |
|
| 95 |
```bash
|
| 96 |
# Basic run: one dataset and one model.
|
| 97 |
-
python BioMod/interpretability/
|
| 98 |
-
--
|
| 99 |
-
--
|
| 100 |
--sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
|
| 101 |
-
--descriptions_file BioMod/interpretability/
|
| 102 |
--n_permutations 1000 \
|
| 103 |
--n_bootstrap 2000 \
|
| 104 |
--output_dir outputs/interpretability/text_alignment/publication
|
| 105 |
|
| 106 |
# Full run: cross-dataset validation and multi-model comparison.
|
| 107 |
-
python BioMod/interpretability/
|
| 108 |
-
--
|
| 109 |
-
--
|
| 110 |
-
--
|
| 111 |
--sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
|
| 112 |
--sentence_models_compare "sentence-transformers/all-mpnet-base-v2,sentence-transformers/all-MiniLM-L6-v2" \
|
| 113 |
-
--descriptions_file BioMod/interpretability/
|
| 114 |
--n_permutations 1000 \
|
| 115 |
--n_bootstrap 2000 \
|
| 116 |
--output_dir outputs/interpretability/text_alignment/publication_full
|
|
@@ -120,8 +120,8 @@ python BioMod/interpretability/module_text_alignment/module_text_alignment_publi
|
|
| 120 |
|
| 121 |
| Argument | Description |
|
| 122 |
| --- | --- |
|
| 123 |
-
| `--
|
| 124 |
-
| `--
|
| 125 |
| `--sentence_models_compare` | Comma-separated model names for comparison, downloaded automatically from HuggingFace |
|
| 126 |
| `--n_permutations` | Mantel-test permutations; 1000 is standard and 10000 gives more precise p-values |
|
| 127 |
| `--n_bootstrap` | Bootstrap resampling iterations; 2000 is standard for 95% CIs |
|
|
@@ -135,7 +135,7 @@ pip install sentence-transformers -i https://pypi.tuna.tsinghua.edu.cn/simple
|
|
| 135 |
|
| 136 |
## Output Files
|
| 137 |
|
| 138 |
-
### Main Experiment Outputs (`
|
| 139 |
|
| 140 |
| File | Content |
|
| 141 |
| --- | --- |
|
|
@@ -149,7 +149,7 @@ pip install sentence-transformers -i https://pypi.tuna.tsinghua.edu.cn/simple
|
|
| 149 |
| `retrieval_examples.csv` | Top-5 retrieval results for representative pathways |
|
| 150 |
| `run.log` | Full log |
|
| 151 |
|
| 152 |
-
### Extended Analysis Outputs (`
|
| 153 |
|
| 154 |
| File | Content |
|
| 155 |
| --- | --- |
|
|
@@ -159,7 +159,7 @@ pip install sentence-transformers -i https://pypi.tuna.tsinghua.edu.cn/simple
|
|
| 159 |
| `hallmarks_per_module_alignment.csv` | Per-module alignment ranking for 50 Hallmarks |
|
| 160 |
| `run_extended.log` | Extended-analysis log |
|
| 161 |
|
| 162 |
-
### Publication-Level Outputs (`
|
| 163 |
|
| 164 |
| File | Content |
|
| 165 |
| --- | --- |
|
|
|
|
| 11 |
|
| 12 |
| Script | Function |
|
| 13 |
| --- | --- |
|
| 14 |
+
| `biomod_text_alignment.py` | Main script: RSA/Mantel test, CKA, and leave-one-out Ridge retrieval |
|
| 15 |
+
| `biomod_text_alignment_extended.py` | Extended analysis: Reactome broad-category RSA, Reactome description-based RSA, and Hallmark deep dive |
|
| 16 |
+
| `biomod_text_alignment_publication.py` | Publication-level analysis: six main figures, bootstrap CIs, multi-model comparison, cross-dataset validation, and deep analysis |
|
| 17 |
| `fetch_reactome_descriptions.py` | Fetch `summaryText` for 491 pathways from the Reactome API |
|
| 18 |
|
| 19 |
## Commands
|
| 20 |
|
| 21 |
Run from the BioMod repository root. Generate each `module_embeddings.npy` with
|
| 22 |
+
`BioMod/interpretability/biomod_geometry.py` before using it here. The historical
|
| 23 |
`pretrain_subset` output requires a separately supplied pretraining dataset;
|
| 24 |
the `pancreas_scib` example uses the bundled pancreas h5ad file. Existing result
|
| 25 |
tables below describe historical runs; their raw output directories are not
|
|
|
|
| 41 |
|
| 42 |
```bash
|
| 43 |
# pretrain_subset is recommended because averaging over 50k cells is more stable.
|
| 44 |
+
python BioMod/interpretability/biomod_text_alignment/biomod_text_alignment.py \
|
| 45 |
+
--biomod_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
|
| 46 |
+
--biomod_json model_dependencies/biomod_data_all/merged_biomods.json \
|
| 47 |
--sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
|
| 48 |
--n_permutations 1000 \
|
| 49 |
--output_dir outputs/interpretability/text_alignment/pretrain_subset_v2
|
| 50 |
|
| 51 |
# pancreas_scib validates cross-dataset consistency.
|
| 52 |
+
python BioMod/interpretability/biomod_text_alignment/biomod_text_alignment.py \
|
| 53 |
+
--biomod_embedding_path outputs/interpretability/geometry/pancreas_scib/module_embeddings.npy \
|
| 54 |
+
--biomod_json model_dependencies/biomod_data_all/merged_biomods.json \
|
| 55 |
--sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
|
| 56 |
--n_permutations 1000 \
|
| 57 |
--output_dir outputs/interpretability/text_alignment/pancreas_scib_v2
|
| 58 |
|
| 59 |
# Quick mode: skip LOO retrieval and run only RSA + CKA.
|
| 60 |
+
python BioMod/interpretability/biomod_text_alignment/biomod_text_alignment.py \
|
| 61 |
+
--biomod_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
|
| 62 |
--sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
|
| 63 |
--skip_loo \
|
| 64 |
--output_dir outputs/interpretability/text_alignment/pretrain_subset_quick
|
|
|
|
| 68 |
|
| 69 |
```bash
|
| 70 |
# Re-fetch if needed. Requires network access to reactome.org and takes about 2 minutes.
|
| 71 |
+
python BioMod/interpretability/biomod_text_alignment/fetch_reactome_descriptions.py \
|
| 72 |
+
--biomod_json model_dependencies/biomod_data_all/merged_biomods.json \
|
| 73 |
--output reactome_descriptions.json --delay 0.05
|
| 74 |
```
|
| 75 |
|
| 76 |
The command writes refreshed descriptions to `reactome_descriptions.json` in the
|
| 77 |
repository root. The analysis examples below use the bundled
|
| 78 |
+
`BioMod/interpretability/biomod_text_alignment/reactome_descriptions.json` instead;
|
| 79 |
set `--descriptions_file reactome_descriptions.json` to use the refreshed output.
|
| 80 |
|
| 81 |
### 4. Extended Analysis: Reactome Categories, Description-Based RSA, Hallmark Deep Dive
|
| 82 |
|
| 83 |
```bash
|
| 84 |
+
python BioMod/interpretability/biomod_text_alignment/biomod_text_alignment_extended.py \
|
| 85 |
+
--biomod_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
|
| 86 |
+
--biomod_json model_dependencies/biomod_data_all/merged_biomods.json \
|
| 87 |
--sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
|
| 88 |
+
--descriptions_file BioMod/interpretability/biomod_text_alignment/reactome_descriptions.json \
|
| 89 |
--n_permutations 1000 \
|
| 90 |
--output_dir outputs/interpretability/text_alignment/pretrain_subset_v2_extended
|
| 91 |
```
|
|
|
|
| 94 |
|
| 95 |
```bash
|
| 96 |
# Basic run: one dataset and one model.
|
| 97 |
+
python BioMod/interpretability/biomod_text_alignment/biomod_text_alignment_publication.py \
|
| 98 |
+
--biomod_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
|
| 99 |
+
--biomod_json model_dependencies/biomod_data_all/merged_biomods.json \
|
| 100 |
--sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
|
| 101 |
+
--descriptions_file BioMod/interpretability/biomod_text_alignment/reactome_descriptions.json \
|
| 102 |
--n_permutations 1000 \
|
| 103 |
--n_bootstrap 2000 \
|
| 104 |
--output_dir outputs/interpretability/text_alignment/publication
|
| 105 |
|
| 106 |
# Full run: cross-dataset validation and multi-model comparison.
|
| 107 |
+
python BioMod/interpretability/biomod_text_alignment/biomod_text_alignment_publication.py \
|
| 108 |
+
--biomod_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
|
| 109 |
+
--biomod_embedding_path2 outputs/interpretability/geometry/pancreas_scib/module_embeddings.npy \
|
| 110 |
+
--biomod_json model_dependencies/biomod_data_all/merged_biomods.json \
|
| 111 |
--sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
|
| 112 |
--sentence_models_compare "sentence-transformers/all-mpnet-base-v2,sentence-transformers/all-MiniLM-L6-v2" \
|
| 113 |
+
--descriptions_file BioMod/interpretability/biomod_text_alignment/reactome_descriptions.json \
|
| 114 |
--n_permutations 1000 \
|
| 115 |
--n_bootstrap 2000 \
|
| 116 |
--output_dir outputs/interpretability/text_alignment/publication_full
|
|
|
|
| 120 |
|
| 121 |
| Argument | Description |
|
| 122 |
| --- | --- |
|
| 123 |
+
| `--biomod_embedding_path` | `module_embeddings.npy` for the main dataset, from Experiment 1 |
|
| 124 |
+
| `--biomod_embedding_path2` | Optional second dataset for cross-dataset validation |
|
| 125 |
| `--sentence_models_compare` | Comma-separated model names for comparison, downloaded automatically from HuggingFace |
|
| 126 |
| `--n_permutations` | Mantel-test permutations; 1000 is standard and 10000 gives more precise p-values |
|
| 127 |
| `--n_bootstrap` | Bootstrap resampling iterations; 2000 is standard for 95% CIs |
|
|
|
|
| 135 |
|
| 136 |
## Output Files
|
| 137 |
|
| 138 |
+
### Main Experiment Outputs (`biomod_text_alignment.py`)
|
| 139 |
|
| 140 |
| File | Content |
|
| 141 |
| --- | --- |
|
|
|
|
| 149 |
| `retrieval_examples.csv` | Top-5 retrieval results for representative pathways |
|
| 150 |
| `run.log` | Full log |
|
| 151 |
|
| 152 |
+
### Extended Analysis Outputs (`biomod_text_alignment_extended.py`)
|
| 153 |
|
| 154 |
| File | Content |
|
| 155 |
| --- | --- |
|
|
|
|
| 159 |
| `hallmarks_per_module_alignment.csv` | Per-module alignment ranking for 50 Hallmarks |
|
| 160 |
| `run_extended.log` | Extended-analysis log |
|
| 161 |
|
| 162 |
+
### Publication-Level Outputs (`biomod_text_alignment_publication.py`)
|
| 163 |
|
| 164 |
| File | Content |
|
| 165 |
| --- | --- |
|
|
File without changes
|
|
File without changes
|
|
@@ -10,7 +10,7 @@
|
|
| 10 |
|
| 11 |
Pipeline
|
| 12 |
--------
|
| 13 |
-
1. Parse pathway names from
|
| 14 |
Reactome (491) modules. Hotspot/Processed are excluded (no semantic names).
|
| 15 |
2. Encode pathway names with a biomedical sentence encoder.
|
| 16 |
3. Load cell-averaged module embeddings from experiment 1 output.
|
|
@@ -58,16 +58,16 @@
|
|
| 58 |
|
| 59 |
HERE = Path(__file__).resolve().parent
|
| 60 |
INTERP_DIR = HERE.parent
|
| 61 |
-
|
| 62 |
-
REPO_ROOT =
|
| 63 |
-
for p in (str(INTERP_DIR), str(
|
| 64 |
if p not in sys.path:
|
| 65 |
sys.path.insert(0, p)
|
| 66 |
|
| 67 |
from interpretability.utils import (
|
| 68 |
MODULE_FAMILY_MAP,
|
| 69 |
-
|
| 70 |
-
|
| 71 |
)
|
| 72 |
|
| 73 |
logger = logging.getLogger(__name__)
|
|
@@ -79,7 +79,7 @@
|
|
| 79 |
|
| 80 |
def parse_pathway_text(module_name: str) -> Optional[str]:
|
| 81 |
"""Extract a human-readable pathway name from a module key."""
|
| 82 |
-
family =
|
| 83 |
if family == "Hallmarks":
|
| 84 |
raw = module_name.split("__", 1)[1]
|
| 85 |
raw = raw.replace("HALLMARK_", "", 1)
|
|
@@ -218,7 +218,7 @@ def compute_rsa_by_family(
|
|
| 218 |
) -> Dict[str, Dict[str, float]]:
|
| 219 |
"""RSA computed separately for each module family (Hallmarks vs Reactome)."""
|
| 220 |
results = {}
|
| 221 |
-
families = [
|
| 222 |
for fam in ["Hallmarks", "Reactome"]:
|
| 223 |
mask = np.array([f == fam for f in families])
|
| 224 |
if mask.sum() < 10:
|
|
@@ -447,11 +447,11 @@ def parse_args() -> argparse.Namespace:
|
|
| 447 |
description=__doc__,
|
| 448 |
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 449 |
)
|
| 450 |
-
parser.add_argument("--module_embedding_path", type=str, required=True,
|
| 451 |
-
help="Path to module_embeddings.npy from experiment 1.")
|
| 452 |
-
parser.add_argument("--module_json", type=str,
|
| 453 |
-
default=str(REPO_ROOT / "model_dependencies" / "
|
| 454 |
-
help="Path to
|
| 455 |
parser.add_argument("--descriptions_file", type=str, default=None,
|
| 456 |
help="Optional JSON with extended pathway descriptions.")
|
| 457 |
parser.add_argument("--sentence_model", type=str,
|
|
@@ -487,7 +487,7 @@ def main():
|
|
| 487 |
|
| 488 |
# --- Load module names and embeddings ---
|
| 489 |
logger.info("Loading module names from %s", args.module_json)
|
| 490 |
-
module_names =
|
| 491 |
logger.info("Total modules: %d", len(module_names))
|
| 492 |
|
| 493 |
logger.info("Loading module embeddings from %s", args.module_embedding_path)
|
|
|
|
| 10 |
|
| 11 |
Pipeline
|
| 12 |
--------
|
| 13 |
+
1. Parse pathway names from merged_biomods.json keys for Hallmarks (50) and
|
| 14 |
Reactome (491) modules. Hotspot/Processed are excluded (no semantic names).
|
| 15 |
2. Encode pathway names with a biomedical sentence encoder.
|
| 16 |
3. Load cell-averaged module embeddings from experiment 1 output.
|
|
|
|
| 58 |
|
| 59 |
HERE = Path(__file__).resolve().parent
|
| 60 |
INTERP_DIR = HERE.parent
|
| 61 |
+
BIOMOD_SOURCE = INTERP_DIR.parent
|
| 62 |
+
REPO_ROOT = BIOMOD_SOURCE.parent
|
| 63 |
+
for p in (str(INTERP_DIR), str(BIOMOD_SOURCE), str(REPO_ROOT)):
|
| 64 |
if p not in sys.path:
|
| 65 |
sys.path.insert(0, p)
|
| 66 |
|
| 67 |
from interpretability.utils import (
|
| 68 |
MODULE_FAMILY_MAP,
|
| 69 |
+
load_biomod_names,
|
| 70 |
+
parse_biomod_family,
|
| 71 |
)
|
| 72 |
|
| 73 |
logger = logging.getLogger(__name__)
|
|
|
|
| 79 |
|
| 80 |
def parse_pathway_text(module_name: str) -> Optional[str]:
|
| 81 |
"""Extract a human-readable pathway name from a module key."""
|
| 82 |
+
family = parse_biomod_family(module_name)
|
| 83 |
if family == "Hallmarks":
|
| 84 |
raw = module_name.split("__", 1)[1]
|
| 85 |
raw = raw.replace("HALLMARK_", "", 1)
|
|
|
|
| 218 |
) -> Dict[str, Dict[str, float]]:
|
| 219 |
"""RSA computed separately for each module family (Hallmarks vs Reactome)."""
|
| 220 |
results = {}
|
| 221 |
+
families = [parse_biomod_family(n) for n in names]
|
| 222 |
for fam in ["Hallmarks", "Reactome"]:
|
| 223 |
mask = np.array([f == fam for f in families])
|
| 224 |
if mask.sum() < 10:
|
|
|
|
| 447 |
description=__doc__,
|
| 448 |
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 449 |
)
|
| 450 |
+
parser.add_argument("--biomod_embedding_path", "--module_embedding_path", type=str, required=True,
|
| 451 |
+
help="Path to module_embeddings.npy from experiment 1.", dest='module_embedding_path')
|
| 452 |
+
parser.add_argument("--biomod_json", "--module_json", type=str,
|
| 453 |
+
default=str(REPO_ROOT / "model_dependencies" / "biomod_data_all" / "merged_biomods.json"),
|
| 454 |
+
help="Path to merged_biomods.json.", dest='module_json')
|
| 455 |
parser.add_argument("--descriptions_file", type=str, default=None,
|
| 456 |
help="Optional JSON with extended pathway descriptions.")
|
| 457 |
parser.add_argument("--sentence_model", type=str,
|
|
|
|
| 487 |
|
| 488 |
# --- Load module names and embeddings ---
|
| 489 |
logger.info("Loading module names from %s", args.module_json)
|
| 490 |
+
module_names = load_biomod_names(args.module_json)
|
| 491 |
logger.info("Total modules: %d", len(module_names))
|
| 492 |
|
| 493 |
logger.info("Loading module embeddings from %s", args.module_embedding_path)
|
|
@@ -6,9 +6,9 @@
|
|
| 6 |
3. Hallmarks deep dive (per-module alignment scores, heatmap)
|
| 7 |
|
| 8 |
Usage (run from the BioMod repository root):
|
| 9 |
-
python BioMod/interpretability/
|
| 10 |
-
--
|
| 11 |
-
--
|
| 12 |
--sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
|
| 13 |
--output_dir outputs/interpretability/text_alignment/pretrain_subset_v2_extended
|
| 14 |
"""
|
|
@@ -36,14 +36,14 @@
|
|
| 36 |
|
| 37 |
HERE = Path(__file__).resolve().parent
|
| 38 |
INTERP_DIR = HERE.parent
|
| 39 |
-
|
| 40 |
-
REPO_ROOT =
|
| 41 |
-
for p in (str(INTERP_DIR), str(
|
| 42 |
if p not in sys.path:
|
| 43 |
sys.path.insert(0, p)
|
| 44 |
|
| 45 |
-
from interpretability.utils import
|
| 46 |
-
from
|
| 47 |
build_text_descriptions,
|
| 48 |
compute_cka,
|
| 49 |
compute_rsa,
|
|
@@ -150,7 +150,7 @@ def reactome_category_rsa(
|
|
| 150 |
logger.info("Analysis 1: Reactome category-level RSA")
|
| 151 |
logger.info("=" * 50)
|
| 152 |
|
| 153 |
-
families = [
|
| 154 |
reactome_mask = np.array([f == "Reactome" for f in families])
|
| 155 |
reactome_idx = np.where(reactome_mask)[0]
|
| 156 |
|
|
@@ -259,7 +259,7 @@ def reactome_with_descriptions(
|
|
| 259 |
with open(descriptions_file, "r", encoding="utf-8") as f:
|
| 260 |
descriptions = json.load(f)
|
| 261 |
|
| 262 |
-
families = [
|
| 263 |
reactome_mask = np.array([f == "Reactome" for f in families])
|
| 264 |
reactome_idx = np.where(reactome_mask)[0]
|
| 265 |
|
|
@@ -317,7 +317,7 @@ def hallmarks_deep_dive(
|
|
| 317 |
logger.info("Analysis 3: Hallmarks deep dive")
|
| 318 |
logger.info("=" * 50)
|
| 319 |
|
| 320 |
-
families = [
|
| 321 |
hallmark_mask = np.array([f == "Hallmarks" for f in families])
|
| 322 |
hallmark_idx = np.where(hallmark_mask)[0]
|
| 323 |
|
|
@@ -445,9 +445,9 @@ def hallmarks_deep_dive(
|
|
| 445 |
def parse_args():
|
| 446 |
parser = argparse.ArgumentParser(description=__doc__,
|
| 447 |
formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 448 |
-
parser.add_argument("--module_embedding_path", type=str, required=True)
|
| 449 |
-
parser.add_argument("--module_json", type=str,
|
| 450 |
-
default=str(REPO_ROOT / "model_dependencies" / "
|
| 451 |
parser.add_argument("--descriptions_file", type=str, default=None,
|
| 452 |
help="JSON with Reactome summaryText descriptions.")
|
| 453 |
parser.add_argument("--sentence_model", type=str,
|
|
@@ -474,7 +474,7 @@ def main():
|
|
| 474 |
)
|
| 475 |
|
| 476 |
# Load data
|
| 477 |
-
module_names =
|
| 478 |
all_embeddings = np.load(args.module_embedding_path)
|
| 479 |
indices, names, texts = build_text_descriptions(module_names)
|
| 480 |
module_emb = all_embeddings[indices]
|
|
|
|
| 6 |
3. Hallmarks deep dive (per-module alignment scores, heatmap)
|
| 7 |
|
| 8 |
Usage (run from the BioMod repository root):
|
| 9 |
+
python BioMod/interpretability/biomod_text_alignment/biomod_text_alignment_extended.py \
|
| 10 |
+
--biomod_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
|
| 11 |
+
--biomod_json model_dependencies/biomod_data_all/merged_biomods.json \
|
| 12 |
--sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
|
| 13 |
--output_dir outputs/interpretability/text_alignment/pretrain_subset_v2_extended
|
| 14 |
"""
|
|
|
|
| 36 |
|
| 37 |
HERE = Path(__file__).resolve().parent
|
| 38 |
INTERP_DIR = HERE.parent
|
| 39 |
+
BIOMOD_SOURCE = INTERP_DIR.parent
|
| 40 |
+
REPO_ROOT = BIOMOD_SOURCE.parent
|
| 41 |
+
for p in (str(INTERP_DIR), str(BIOMOD_SOURCE), str(REPO_ROOT)):
|
| 42 |
if p not in sys.path:
|
| 43 |
sys.path.insert(0, p)
|
| 44 |
|
| 45 |
+
from interpretability.utils import load_biomod_names, parse_biomod_family
|
| 46 |
+
from biomod_text_alignment.biomod_text_alignment import (
|
| 47 |
build_text_descriptions,
|
| 48 |
compute_cka,
|
| 49 |
compute_rsa,
|
|
|
|
| 150 |
logger.info("Analysis 1: Reactome category-level RSA")
|
| 151 |
logger.info("=" * 50)
|
| 152 |
|
| 153 |
+
families = [parse_biomod_family(n) for n in names]
|
| 154 |
reactome_mask = np.array([f == "Reactome" for f in families])
|
| 155 |
reactome_idx = np.where(reactome_mask)[0]
|
| 156 |
|
|
|
|
| 259 |
with open(descriptions_file, "r", encoding="utf-8") as f:
|
| 260 |
descriptions = json.load(f)
|
| 261 |
|
| 262 |
+
families = [parse_biomod_family(n) for n in names]
|
| 263 |
reactome_mask = np.array([f == "Reactome" for f in families])
|
| 264 |
reactome_idx = np.where(reactome_mask)[0]
|
| 265 |
|
|
|
|
| 317 |
logger.info("Analysis 3: Hallmarks deep dive")
|
| 318 |
logger.info("=" * 50)
|
| 319 |
|
| 320 |
+
families = [parse_biomod_family(n) for n in names]
|
| 321 |
hallmark_mask = np.array([f == "Hallmarks" for f in families])
|
| 322 |
hallmark_idx = np.where(hallmark_mask)[0]
|
| 323 |
|
|
|
|
| 445 |
def parse_args():
|
| 446 |
parser = argparse.ArgumentParser(description=__doc__,
|
| 447 |
formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 448 |
+
parser.add_argument("--biomod_embedding_path", "--module_embedding_path", type=str, required=True, dest='module_embedding_path')
|
| 449 |
+
parser.add_argument("--biomod_json", "--module_json", type=str,
|
| 450 |
+
default=str(REPO_ROOT / "model_dependencies" / "biomod_data_all" / "merged_biomods.json"), dest='module_json')
|
| 451 |
parser.add_argument("--descriptions_file", type=str, default=None,
|
| 452 |
help="JSON with Reactome summaryText descriptions.")
|
| 453 |
parser.add_argument("--sentence_model", type=str,
|
|
|
|
| 474 |
)
|
| 475 |
|
| 476 |
# Load data
|
| 477 |
+
module_names = load_biomod_names(args.module_json)
|
| 478 |
all_embeddings = np.load(args.module_embedding_path)
|
| 479 |
indices, names, texts = build_text_descriptions(module_names)
|
| 480 |
module_emb = all_embeddings[indices]
|
|
@@ -4,13 +4,13 @@
|
|
| 4 |
Supports multi-model comparison and cross-dataset validation.
|
| 5 |
|
| 6 |
Usage (run from the BioMod repository root):
|
| 7 |
-
python BioMod/interpretability/
|
| 8 |
-
--
|
| 9 |
-
--
|
| 10 |
-
--
|
| 11 |
--sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
|
| 12 |
--sentence_models_compare "sentence-transformers/all-mpnet-base-v2" \
|
| 13 |
-
--descriptions_file BioMod/interpretability/
|
| 14 |
--n_permutations 1000 \
|
| 15 |
--n_bootstrap 2000 \
|
| 16 |
--output_dir outputs/interpretability/text_alignment/publication
|
|
@@ -40,19 +40,19 @@
|
|
| 40 |
|
| 41 |
HERE = Path(__file__).resolve().parent
|
| 42 |
INTERP_DIR = HERE.parent
|
| 43 |
-
|
| 44 |
-
REPO_ROOT =
|
| 45 |
-
for p in (str(HERE), str(INTERP_DIR), str(
|
| 46 |
if p not in sys.path:
|
| 47 |
sys.path.insert(0, p)
|
| 48 |
|
| 49 |
from interpretability.utils import (
|
| 50 |
HALLMARK_PROCESS,
|
| 51 |
-
|
| 52 |
parse_hallmark_process,
|
| 53 |
-
|
| 54 |
)
|
| 55 |
-
from
|
| 56 |
build_text_descriptions,
|
| 57 |
compute_cka,
|
| 58 |
compute_rsa,
|
|
@@ -60,7 +60,7 @@
|
|
| 60 |
mantel_test,
|
| 61 |
permutation_test_cka,
|
| 62 |
)
|
| 63 |
-
from
|
| 64 |
REACTOME_CATEGORIES,
|
| 65 |
categorize_reactome,
|
| 66 |
)
|
|
@@ -166,8 +166,8 @@ def bootstrap_rsa(
|
|
| 166 |
float(np.percentile(boot_rs, 97.5))
|
| 167 |
|
| 168 |
|
| 169 |
-
def
|
| 170 |
-
"""Get number of genes per module from
|
| 171 |
with open(module_json, "r", encoding="utf-8") as f:
|
| 172 |
modules = json.load(f)
|
| 173 |
return {name: len(genes) for name, genes in modules.items()}
|
|
@@ -182,12 +182,12 @@ def parse_args() -> argparse.Namespace:
|
|
| 182 |
description=__doc__,
|
| 183 |
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 184 |
)
|
| 185 |
-
parser.add_argument("--module_embedding_path", type=str, required=True,
|
| 186 |
-
help="Primary module_embeddings.npy (e.g. pretrain_subset)")
|
| 187 |
-
parser.add_argument("--module_embedding_path2", type=str, default=None,
|
| 188 |
-
help="Second dataset module_embeddings.npy for cross-dataset comparison")
|
| 189 |
-
parser.add_argument("--module_json", type=str,
|
| 190 |
-
default=str(REPO_ROOT / "model_dependencies" / "
|
| 191 |
parser.add_argument("--descriptions_file", type=str, default=None)
|
| 192 |
parser.add_argument("--sentence_model", type=str,
|
| 193 |
default="pritamdeka/S-PubMedBert-MS-MARCO")
|
|
@@ -314,7 +314,7 @@ def fig2_hallmarks_heatmaps(
|
|
| 314 |
output_dir: Path,
|
| 315 |
) -> Dict:
|
| 316 |
"""Hallmarks 50x50 similarity matrices with rank-transform and clustering."""
|
| 317 |
-
families = [
|
| 318 |
hallmark_mask = np.array([f == "Hallmarks" for f in families])
|
| 319 |
hallmark_idx = np.where(hallmark_mask)[0]
|
| 320 |
|
|
@@ -433,7 +433,7 @@ def fig3_per_hallmark_alignment(
|
|
| 433 |
seed: int = 42,
|
| 434 |
) -> Dict:
|
| 435 |
"""Lollipop chart of per-hallmark alignment scores, colored by process."""
|
| 436 |
-
families = [
|
| 437 |
hallmark_mask = np.array([f == "Hallmarks" for f in families])
|
| 438 |
hallmark_idx = np.where(hallmark_mask)[0]
|
| 439 |
|
|
@@ -531,7 +531,7 @@ def fig4_category_alignment(
|
|
| 531 |
output_dir: Path,
|
| 532 |
) -> Dict:
|
| 533 |
"""Violin/box plot of alignment scores grouped by MSigDB process category."""
|
| 534 |
-
families = [
|
| 535 |
hallmark_mask = np.array([f == "Hallmarks" for f in families])
|
| 536 |
hallmark_idx = np.where(hallmark_mask)[0]
|
| 537 |
|
|
@@ -634,7 +634,7 @@ def fig5_granularity_effect(
|
|
| 634 |
output_dir: Path = None,
|
| 635 |
) -> Dict:
|
| 636 |
"""RSA at multiple granularity levels: fine → category → super-category."""
|
| 637 |
-
families = [
|
| 638 |
reactome_mask = np.array([f == "Reactome" for f in families])
|
| 639 |
hallmark_mask = np.array([f == "Hallmarks" for f in families])
|
| 640 |
reactome_idx = np.where(reactome_mask)[0]
|
|
@@ -761,7 +761,7 @@ def fig6_model_comparison(
|
|
| 761 |
output_dir: Path = None,
|
| 762 |
) -> Dict:
|
| 763 |
"""Grouped bar chart comparing RSA across models and datasets."""
|
| 764 |
-
families = [
|
| 765 |
hallmark_mask = np.array([f == "Hallmarks" for f in families])
|
| 766 |
hallmark_idx = np.where(hallmark_mask)[0]
|
| 767 |
|
|
@@ -861,7 +861,7 @@ def fig6_model_comparison(
|
|
| 861 |
# Deep Analysis: Alignment vs Module Size
|
| 862 |
# ============================================================
|
| 863 |
|
| 864 |
-
def
|
| 865 |
module_emb: np.ndarray,
|
| 866 |
text_emb: np.ndarray,
|
| 867 |
names: List[str],
|
|
@@ -870,7 +870,7 @@ def analysis_alignment_vs_module_size(
|
|
| 870 |
output_dir: Path,
|
| 871 |
) -> Dict:
|
| 872 |
"""Scatter: n_genes vs alignment score. Tests if larger modules align better."""
|
| 873 |
-
families = [
|
| 874 |
hallmark_mask = np.array([f == "Hallmarks" for f in families])
|
| 875 |
hallmark_idx = np.where(hallmark_mask)[0]
|
| 876 |
|
|
@@ -891,7 +891,7 @@ def analysis_alignment_vs_module_size(
|
|
| 891 |
per_module_corr[i] = r
|
| 892 |
|
| 893 |
# Module sizes
|
| 894 |
-
module_sizes =
|
| 895 |
h_sizes = np.array([module_sizes.get(n, 0) for n in h_names])
|
| 896 |
|
| 897 |
# Correlation
|
|
@@ -945,7 +945,7 @@ def analysis_negative_results(
|
|
| 945 |
n_show: int = 5,
|
| 946 |
) -> Dict:
|
| 947 |
"""For poorly-aligned modules, show neighbor divergence."""
|
| 948 |
-
families = [
|
| 949 |
hallmark_mask = np.array([f == "Hallmarks" for f in families])
|
| 950 |
hallmark_idx = np.where(hallmark_mask)[0]
|
| 951 |
|
|
@@ -1022,7 +1022,7 @@ def analysis_knn_overlap(
|
|
| 1022 |
output_dir: Path = None,
|
| 1023 |
) -> Dict:
|
| 1024 |
"""kNN overlap with bootstrap CI and per-category breakdown."""
|
| 1025 |
-
families = [
|
| 1026 |
hallmark_mask = np.array([f == "Hallmarks" for f in families])
|
| 1027 |
hallmark_idx = np.where(hallmark_mask)[0]
|
| 1028 |
|
|
@@ -1103,13 +1103,13 @@ def main():
|
|
| 1103 |
logger.info("Args: %s", vars(args))
|
| 1104 |
|
| 1105 |
# ---- Load data ----
|
| 1106 |
-
module_names =
|
| 1107 |
all_module_emb = np.load(args.module_embedding_path)
|
| 1108 |
logger.info("Loaded module embeddings: %s from %s",
|
| 1109 |
all_module_emb.shape, args.module_embedding_path)
|
| 1110 |
|
| 1111 |
# Filter to semantic modules (Hallmarks + Reactome)
|
| 1112 |
-
families = [
|
| 1113 |
semantic_mask = np.array([f in ("Hallmarks", "Reactome") for f in families])
|
| 1114 |
semantic_idx = np.where(semantic_mask)[0]
|
| 1115 |
module_emb = all_module_emb[semantic_idx]
|
|
@@ -1205,7 +1205,7 @@ def main():
|
|
| 1205 |
|
| 1206 |
# ---- Deep Analysis ----
|
| 1207 |
logger.info("Running deep analysis: alignment vs module size...")
|
| 1208 |
-
metrics_size =
|
| 1209 |
module_emb, text_emb, names, texts, args.module_json, output_dir,
|
| 1210 |
)
|
| 1211 |
|
|
|
|
| 4 |
Supports multi-model comparison and cross-dataset validation.
|
| 5 |
|
| 6 |
Usage (run from the BioMod repository root):
|
| 7 |
+
python BioMod/interpretability/biomod_text_alignment/biomod_text_alignment_publication.py \
|
| 8 |
+
--biomod_embedding_path outputs/interpretability/geometry/pretrain_subset/module_embeddings.npy \
|
| 9 |
+
--biomod_embedding_path2 outputs/interpretability/geometry/pancreas_scib/module_embeddings.npy \
|
| 10 |
+
--biomod_json model_dependencies/biomod_data_all/merged_biomods.json \
|
| 11 |
--sentence_model pritamdeka/S-PubMedBert-MS-MARCO \
|
| 12 |
--sentence_models_compare "sentence-transformers/all-mpnet-base-v2" \
|
| 13 |
+
--descriptions_file BioMod/interpretability/biomod_text_alignment/reactome_descriptions.json \
|
| 14 |
--n_permutations 1000 \
|
| 15 |
--n_bootstrap 2000 \
|
| 16 |
--output_dir outputs/interpretability/text_alignment/publication
|
|
|
|
| 40 |
|
| 41 |
HERE = Path(__file__).resolve().parent
|
| 42 |
INTERP_DIR = HERE.parent
|
| 43 |
+
BIOMOD_SOURCE = INTERP_DIR.parent
|
| 44 |
+
REPO_ROOT = BIOMOD_SOURCE.parent
|
| 45 |
+
for p in (str(HERE), str(INTERP_DIR), str(BIOMOD_SOURCE), str(REPO_ROOT)):
|
| 46 |
if p not in sys.path:
|
| 47 |
sys.path.insert(0, p)
|
| 48 |
|
| 49 |
from interpretability.utils import (
|
| 50 |
HALLMARK_PROCESS,
|
| 51 |
+
load_biomod_names,
|
| 52 |
parse_hallmark_process,
|
| 53 |
+
parse_biomod_family,
|
| 54 |
)
|
| 55 |
+
from biomod_text_alignment.biomod_text_alignment import (
|
| 56 |
build_text_descriptions,
|
| 57 |
compute_cka,
|
| 58 |
compute_rsa,
|
|
|
|
| 60 |
mantel_test,
|
| 61 |
permutation_test_cka,
|
| 62 |
)
|
| 63 |
+
from biomod_text_alignment.biomod_text_alignment_extended import (
|
| 64 |
REACTOME_CATEGORIES,
|
| 65 |
categorize_reactome,
|
| 66 |
)
|
|
|
|
| 166 |
float(np.percentile(boot_rs, 97.5))
|
| 167 |
|
| 168 |
|
| 169 |
+
def get_biomod_sizes(module_json: str) -> Dict[str, int]:
|
| 170 |
+
"""Get number of genes per module from merged_biomods.json."""
|
| 171 |
with open(module_json, "r", encoding="utf-8") as f:
|
| 172 |
modules = json.load(f)
|
| 173 |
return {name: len(genes) for name, genes in modules.items()}
|
|
|
|
| 182 |
description=__doc__,
|
| 183 |
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 184 |
)
|
| 185 |
+
parser.add_argument("--biomod_embedding_path", "--module_embedding_path", type=str, required=True,
|
| 186 |
+
help="Primary module_embeddings.npy (e.g. pretrain_subset)", dest='module_embedding_path')
|
| 187 |
+
parser.add_argument("--biomod_embedding_path2", "--module_embedding_path2", type=str, default=None,
|
| 188 |
+
help="Second dataset module_embeddings.npy for cross-dataset comparison", dest='module_embedding_path2')
|
| 189 |
+
parser.add_argument("--biomod_json", "--module_json", type=str,
|
| 190 |
+
default=str(REPO_ROOT / "model_dependencies" / "biomod_data_all" / "merged_biomods.json"), dest='module_json')
|
| 191 |
parser.add_argument("--descriptions_file", type=str, default=None)
|
| 192 |
parser.add_argument("--sentence_model", type=str,
|
| 193 |
default="pritamdeka/S-PubMedBert-MS-MARCO")
|
|
|
|
| 314 |
output_dir: Path,
|
| 315 |
) -> Dict:
|
| 316 |
"""Hallmarks 50x50 similarity matrices with rank-transform and clustering."""
|
| 317 |
+
families = [parse_biomod_family(n) for n in names]
|
| 318 |
hallmark_mask = np.array([f == "Hallmarks" for f in families])
|
| 319 |
hallmark_idx = np.where(hallmark_mask)[0]
|
| 320 |
|
|
|
|
| 433 |
seed: int = 42,
|
| 434 |
) -> Dict:
|
| 435 |
"""Lollipop chart of per-hallmark alignment scores, colored by process."""
|
| 436 |
+
families = [parse_biomod_family(n) for n in names]
|
| 437 |
hallmark_mask = np.array([f == "Hallmarks" for f in families])
|
| 438 |
hallmark_idx = np.where(hallmark_mask)[0]
|
| 439 |
|
|
|
|
| 531 |
output_dir: Path,
|
| 532 |
) -> Dict:
|
| 533 |
"""Violin/box plot of alignment scores grouped by MSigDB process category."""
|
| 534 |
+
families = [parse_biomod_family(n) for n in names]
|
| 535 |
hallmark_mask = np.array([f == "Hallmarks" for f in families])
|
| 536 |
hallmark_idx = np.where(hallmark_mask)[0]
|
| 537 |
|
|
|
|
| 634 |
output_dir: Path = None,
|
| 635 |
) -> Dict:
|
| 636 |
"""RSA at multiple granularity levels: fine → category → super-category."""
|
| 637 |
+
families = [parse_biomod_family(n) for n in names]
|
| 638 |
reactome_mask = np.array([f == "Reactome" for f in families])
|
| 639 |
hallmark_mask = np.array([f == "Hallmarks" for f in families])
|
| 640 |
reactome_idx = np.where(reactome_mask)[0]
|
|
|
|
| 761 |
output_dir: Path = None,
|
| 762 |
) -> Dict:
|
| 763 |
"""Grouped bar chart comparing RSA across models and datasets."""
|
| 764 |
+
families = [parse_biomod_family(n) for n in names]
|
| 765 |
hallmark_mask = np.array([f == "Hallmarks" for f in families])
|
| 766 |
hallmark_idx = np.where(hallmark_mask)[0]
|
| 767 |
|
|
|
|
| 861 |
# Deep Analysis: Alignment vs Module Size
|
| 862 |
# ============================================================
|
| 863 |
|
| 864 |
+
def analysis_alignment_vs_biomod_size(
|
| 865 |
module_emb: np.ndarray,
|
| 866 |
text_emb: np.ndarray,
|
| 867 |
names: List[str],
|
|
|
|
| 870 |
output_dir: Path,
|
| 871 |
) -> Dict:
|
| 872 |
"""Scatter: n_genes vs alignment score. Tests if larger modules align better."""
|
| 873 |
+
families = [parse_biomod_family(n) for n in names]
|
| 874 |
hallmark_mask = np.array([f == "Hallmarks" for f in families])
|
| 875 |
hallmark_idx = np.where(hallmark_mask)[0]
|
| 876 |
|
|
|
|
| 891 |
per_module_corr[i] = r
|
| 892 |
|
| 893 |
# Module sizes
|
| 894 |
+
module_sizes = get_biomod_sizes(module_json)
|
| 895 |
h_sizes = np.array([module_sizes.get(n, 0) for n in h_names])
|
| 896 |
|
| 897 |
# Correlation
|
|
|
|
| 945 |
n_show: int = 5,
|
| 946 |
) -> Dict:
|
| 947 |
"""For poorly-aligned modules, show neighbor divergence."""
|
| 948 |
+
families = [parse_biomod_family(n) for n in names]
|
| 949 |
hallmark_mask = np.array([f == "Hallmarks" for f in families])
|
| 950 |
hallmark_idx = np.where(hallmark_mask)[0]
|
| 951 |
|
|
|
|
| 1022 |
output_dir: Path = None,
|
| 1023 |
) -> Dict:
|
| 1024 |
"""kNN overlap with bootstrap CI and per-category breakdown."""
|
| 1025 |
+
families = [parse_biomod_family(n) for n in names]
|
| 1026 |
hallmark_mask = np.array([f == "Hallmarks" for f in families])
|
| 1027 |
hallmark_idx = np.where(hallmark_mask)[0]
|
| 1028 |
|
|
|
|
| 1103 |
logger.info("Args: %s", vars(args))
|
| 1104 |
|
| 1105 |
# ---- Load data ----
|
| 1106 |
+
module_names = load_biomod_names(args.module_json)
|
| 1107 |
all_module_emb = np.load(args.module_embedding_path)
|
| 1108 |
logger.info("Loaded module embeddings: %s from %s",
|
| 1109 |
all_module_emb.shape, args.module_embedding_path)
|
| 1110 |
|
| 1111 |
# Filter to semantic modules (Hallmarks + Reactome)
|
| 1112 |
+
families = [parse_biomod_family(n) for n in module_names]
|
| 1113 |
semantic_mask = np.array([f in ("Hallmarks", "Reactome") for f in families])
|
| 1114 |
semantic_idx = np.where(semantic_mask)[0]
|
| 1115 |
module_emb = all_module_emb[semantic_idx]
|
|
|
|
| 1205 |
|
| 1206 |
# ---- Deep Analysis ----
|
| 1207 |
logger.info("Running deep analysis: alignment vs module size...")
|
| 1208 |
+
metrics_size = analysis_alignment_vs_biomod_size(
|
| 1209 |
module_emb, text_emb, names, texts, args.module_json, output_dir,
|
| 1210 |
)
|
| 1211 |
|
|
@@ -1,11 +1,11 @@
|
|
| 1 |
"""Fetch Reactome pathway descriptions via the Content Service REST API.
|
| 2 |
|
| 3 |
Produces a JSON file mapping module_name -> summaryText that can be passed
|
| 4 |
-
to
|
| 5 |
|
| 6 |
Usage (run from the BioMod repository root):
|
| 7 |
-
python BioMod/interpretability/
|
| 8 |
-
--
|
| 9 |
--output reactome_descriptions.json
|
| 10 |
|
| 11 |
Requires internet access to https://reactome.org/ContentService/
|
|
@@ -24,12 +24,12 @@
|
|
| 24 |
|
| 25 |
HERE = Path(__file__).resolve().parent
|
| 26 |
INTERP_DIR = HERE.parent
|
| 27 |
-
|
| 28 |
-
for p in (str(INTERP_DIR), str(
|
| 29 |
if p not in sys.path:
|
| 30 |
sys.path.insert(0, p)
|
| 31 |
|
| 32 |
-
from interpretability.utils import
|
| 33 |
|
| 34 |
REACTOME_API = "https://reactome.org/ContentService/data/query"
|
| 35 |
|
|
@@ -56,14 +56,14 @@ def fetch_summary(rhs_id: str, retries: int = 3) -> str:
|
|
| 56 |
|
| 57 |
def main():
|
| 58 |
parser = argparse.ArgumentParser(description=__doc__)
|
| 59 |
-
parser.add_argument("--module_json", type=str, required=True)
|
| 60 |
parser.add_argument("--output", type=str, default="reactome_descriptions.json")
|
| 61 |
parser.add_argument("--delay", type=float, default=0.1,
|
| 62 |
help="Delay between API calls (seconds)")
|
| 63 |
args = parser.parse_args()
|
| 64 |
|
| 65 |
-
module_names =
|
| 66 |
-
reactome_modules = [n for n in module_names if
|
| 67 |
print(f"Reactome modules to fetch: {len(reactome_modules)}")
|
| 68 |
|
| 69 |
descriptions = {}
|
|
|
|
| 1 |
"""Fetch Reactome pathway descriptions via the Content Service REST API.
|
| 2 |
|
| 3 |
Produces a JSON file mapping module_name -> summaryText that can be passed
|
| 4 |
+
to biomod_text_alignment_extended.py via --descriptions_file.
|
| 5 |
|
| 6 |
Usage (run from the BioMod repository root):
|
| 7 |
+
python BioMod/interpretability/biomod_text_alignment/fetch_reactome_descriptions.py \
|
| 8 |
+
--biomod_json model_dependencies/biomod_data_all/merged_biomods.json \
|
| 9 |
--output reactome_descriptions.json
|
| 10 |
|
| 11 |
Requires internet access to https://reactome.org/ContentService/
|
|
|
|
| 24 |
|
| 25 |
HERE = Path(__file__).resolve().parent
|
| 26 |
INTERP_DIR = HERE.parent
|
| 27 |
+
BIOMOD_SOURCE = INTERP_DIR.parent
|
| 28 |
+
for p in (str(INTERP_DIR), str(BIOMOD_SOURCE)):
|
| 29 |
if p not in sys.path:
|
| 30 |
sys.path.insert(0, p)
|
| 31 |
|
| 32 |
+
from interpretability.utils import load_biomod_names, parse_biomod_family
|
| 33 |
|
| 34 |
REACTOME_API = "https://reactome.org/ContentService/data/query"
|
| 35 |
|
|
|
|
| 56 |
|
| 57 |
def main():
|
| 58 |
parser = argparse.ArgumentParser(description=__doc__)
|
| 59 |
+
parser.add_argument("--biomod_json", "--module_json", type=str, required=True, dest='module_json')
|
| 60 |
parser.add_argument("--output", type=str, default="reactome_descriptions.json")
|
| 61 |
parser.add_argument("--delay", type=float, default=0.1,
|
| 62 |
help="Delay between API calls (seconds)")
|
| 63 |
args = parser.parse_args()
|
| 64 |
|
| 65 |
+
module_names = load_biomod_names(args.module_json)
|
| 66 |
+
reactome_modules = [n for n in module_names if parse_biomod_family(n) == "Reactome"]
|
| 67 |
print(f"Reactome modules to fetch: {len(reactome_modules)}")
|
| 68 |
|
| 69 |
descriptions = {}
|
|
File without changes
|
|
@@ -1,7 +1,7 @@
|
|
| 1 |
"""Shared helpers for module interpretability experiments.
|
| 2 |
|
| 3 |
Provides:
|
| 4 |
-
* module family / hallmark-process parsing from
|
| 5 |
* a generic in-memory dataset that yields raw [G_input] expression rows,
|
| 6 |
compatible with DataPreprocessor.reindex_tensor_to_model_space
|
| 7 |
"""
|
|
@@ -86,7 +86,7 @@
|
|
| 86 |
}
|
| 87 |
|
| 88 |
|
| 89 |
-
def
|
| 90 |
head = module_name.split("__", 1)[0]
|
| 91 |
return MODULE_FAMILY_MAP.get(head, "Other")
|
| 92 |
|
|
@@ -98,18 +98,18 @@ def parse_hallmark_process(module_name: str) -> Optional[str]:
|
|
| 98 |
return HALLMARK_PROCESS.get(short)
|
| 99 |
|
| 100 |
|
| 101 |
-
def
|
| 102 |
rows = []
|
| 103 |
for name in module_names:
|
| 104 |
rows.append({
|
| 105 |
"module_name": name,
|
| 106 |
-
"family":
|
| 107 |
"hallmark_process": parse_hallmark_process(name),
|
| 108 |
})
|
| 109 |
return rows
|
| 110 |
|
| 111 |
|
| 112 |
-
def
|
| 113 |
with open(module_json_path, "r", encoding="utf-8") as handle:
|
| 114 |
return list(json.load(handle).keys())
|
| 115 |
|
|
|
|
| 1 |
"""Shared helpers for module interpretability experiments.
|
| 2 |
|
| 3 |
Provides:
|
| 4 |
+
* module family / hallmark-process parsing from merged_biomods.json keys
|
| 5 |
* a generic in-memory dataset that yields raw [G_input] expression rows,
|
| 6 |
compatible with DataPreprocessor.reindex_tensor_to_model_space
|
| 7 |
"""
|
|
|
|
| 86 |
}
|
| 87 |
|
| 88 |
|
| 89 |
+
def parse_biomod_family(module_name: str) -> str:
|
| 90 |
head = module_name.split("__", 1)[0]
|
| 91 |
return MODULE_FAMILY_MAP.get(head, "Other")
|
| 92 |
|
|
|
|
| 98 |
return HALLMARK_PROCESS.get(short)
|
| 99 |
|
| 100 |
|
| 101 |
+
def build_biomod_metadata(module_names: Sequence[str]):
|
| 102 |
rows = []
|
| 103 |
for name in module_names:
|
| 104 |
rows.append({
|
| 105 |
"module_name": name,
|
| 106 |
+
"family": parse_biomod_family(name),
|
| 107 |
"hallmark_process": parse_hallmark_process(name),
|
| 108 |
})
|
| 109 |
return rows
|
| 110 |
|
| 111 |
|
| 112 |
+
def load_biomod_names(module_json_path: str) -> List[str]:
|
| 113 |
with open(module_json_path, "r", encoding="utf-8") as handle:
|
| 114 |
return list(json.load(handle).keys())
|
| 115 |
|
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""
|
| 2 |
reconstruction decoders or direct prediction heads."""
|
| 3 |
|
| 4 |
import sys
|
|
@@ -16,21 +16,21 @@
|
|
| 16 |
sys.path.insert(0, model_dependencies_dir)
|
| 17 |
|
| 18 |
try:
|
| 19 |
-
from
|
| 20 |
-
Performer,
|
| 21 |
Always,
|
| 22 |
cast_tuple,
|
| 23 |
)
|
| 24 |
except ImportError:
|
| 25 |
try:
|
| 26 |
-
from
|
| 27 |
-
Performer,
|
| 28 |
Always,
|
| 29 |
cast_tuple,
|
| 30 |
)
|
| 31 |
except ImportError:
|
| 32 |
from performer_pytorch.performer_pytorch import (
|
| 33 |
-
Performer,
|
| 34 |
Always,
|
| 35 |
cast_tuple,
|
| 36 |
)
|
|
@@ -41,7 +41,7 @@
|
|
| 41 |
import numpy as np
|
| 42 |
|
| 43 |
|
| 44 |
-
def
|
| 45 |
"""Translate serialized pre-rename names without changing their semantics."""
|
| 46 |
replacements = (
|
| 47 |
("PATHCH", "MODULE"),
|
|
@@ -59,13 +59,13 @@ def _migrate_legacy_module_name(value: str) -> str:
|
|
| 59 |
return value
|
| 60 |
|
| 61 |
|
| 62 |
-
def
|
| 63 |
"""Return a module-named copy of an older serialized configuration."""
|
| 64 |
def translate(value):
|
| 65 |
if isinstance(value, dict):
|
| 66 |
migrated = {}
|
| 67 |
for key, item in value.items():
|
| 68 |
-
new_key =
|
| 69 |
if new_key in migrated:
|
| 70 |
raise ValueError(
|
| 71 |
f"Legacy configuration migration would duplicate key {new_key!r}"
|
|
@@ -77,13 +77,13 @@ def translate(value):
|
|
| 77 |
if isinstance(value, tuple):
|
| 78 |
return tuple(translate(item) for item in value)
|
| 79 |
if isinstance(value, str):
|
| 80 |
-
return
|
| 81 |
return value
|
| 82 |
|
| 83 |
return translate(dict(config))
|
| 84 |
|
| 85 |
|
| 86 |
-
def
|
| 87 |
"""Translate old state-dict key names before PyTorch performs strict loading."""
|
| 88 |
try:
|
| 89 |
migrated = state_dict.__class__()
|
|
@@ -91,7 +91,7 @@ def migrate_legacy_module_state_dict(state_dict):
|
|
| 91 |
migrated = {}
|
| 92 |
|
| 93 |
for key, value in state_dict.items():
|
| 94 |
-
new_key =
|
| 95 |
if new_key in migrated:
|
| 96 |
# If both spellings are supplied, prefer the explicit new spelling.
|
| 97 |
if key == new_key:
|
|
@@ -101,7 +101,7 @@ def migrate_legacy_module_state_dict(state_dict):
|
|
| 101 |
|
| 102 |
if hasattr(state_dict, "_metadata"):
|
| 103 |
migrated._metadata = {
|
| 104 |
-
|
| 105 |
for key, value in state_dict._metadata.items()
|
| 106 |
}
|
| 107 |
return migrated
|
|
@@ -135,13 +135,13 @@ def normalize_fusion_method(fusion_method: str) -> str:
|
|
| 135 |
)
|
| 136 |
|
| 137 |
|
| 138 |
-
def
|
| 139 |
config: Dict[str, object],
|
| 140 |
*,
|
| 141 |
source: str = "config",
|
| 142 |
log_fn: Optional[Callable[[str], None]] = None,
|
| 143 |
) -> Dict[str, object]:
|
| 144 |
-
cleaned =
|
| 145 |
legacy_mode = str(cleaned.get("extra_gene_mode", "none")).lower()
|
| 146 |
legacy_num = int(cleaned.get("extra_gene_num", 0) or 0)
|
| 147 |
if legacy_mode != "none" or legacy_num > 0:
|
|
@@ -160,7 +160,7 @@ def sanitize_performer_module_config(
|
|
| 160 |
|
| 161 |
return cleaned
|
| 162 |
|
| 163 |
-
#
|
| 164 |
# from sclong_module.models_module import ModuleToGeneDecoder
|
| 165 |
|
| 166 |
|
|
@@ -220,7 +220,7 @@ def forward(self, query: torch.Tensor, memory: torch.Tensor) -> torch.Tensor:
|
|
| 220 |
return query
|
| 221 |
|
| 222 |
|
| 223 |
-
class
|
| 224 |
"""Gene embedding, MLP, and attention pooling produce a module token.
|
| 225 |
|
| 226 |
Expression fusion supports three methods:
|
|
@@ -823,8 +823,8 @@ def forward(
|
|
| 823 |
return self._forward_bucketed(expr, return_module_targets)
|
| 824 |
|
| 825 |
|
| 826 |
-
class
|
| 827 |
-
"""
|
| 828 |
|
| 829 |
use_gene2vec=True initializes from supplied pretrained gene embeddings;
|
| 830 |
False uses random gene embeddings for ablation experiments."""
|
|
@@ -996,7 +996,7 @@ def __init__(
|
|
| 996 |
print(f"[INFO] use_gene2vec=False: Using randomly initialized gene embeddings (dim={gene_embed_dim})")
|
| 997 |
|
| 998 |
# Module embedding layer.
|
| 999 |
-
self.module_embed =
|
| 1000 |
module_sets=module_sets,
|
| 1001 |
num_genes=num_genes,
|
| 1002 |
gene_embed_dim=gene_embed_dim,
|
|
@@ -1054,7 +1054,7 @@ def __init__(
|
|
| 1054 |
self.dropout = nn.Dropout(emb_dropout)
|
| 1055 |
|
| 1056 |
# Performer encoder.
|
| 1057 |
-
self.encoder =
|
| 1058 |
base_dim, enc_depth, enc_heads, enc_dim_head,
|
| 1059 |
local_attn_heads, local_window_size, causal, ff_mult, nb_features,
|
| 1060 |
feature_redraw_interval, reversible, ff_chunks, generalized_attention,
|
|
@@ -1090,7 +1090,7 @@ def __init__(
|
|
| 1090 |
if self.decoder_type == "performer":
|
| 1091 |
self.decoder_pos_emb = nn.Parameter(torch.zeros(1, pos_tokens, self.dec_embed_dim))
|
| 1092 |
self.decoder_dropout = nn.Dropout(emb_dropout)
|
| 1093 |
-
self.decoder =
|
| 1094 |
self.dec_embed_dim, dec_depth, dec_heads, dec_dim_head,
|
| 1095 |
local_attn_heads=cast_tuple(0), local_window_size=256, causal=False, ff_mult=dec_ff_mult,
|
| 1096 |
nb_features=None, feature_redraw_interval=feature_redraw_interval,
|
|
@@ -1203,7 +1203,7 @@ def _init_weights(self) -> None:
|
|
| 1203 |
nn.init.constant_(m.bias, 0)
|
| 1204 |
nn.init.constant_(m.weight, 1.0)
|
| 1205 |
|
| 1206 |
-
def
|
| 1207 |
self,
|
| 1208 |
batch_size: int,
|
| 1209 |
seq_len: int,
|
|
@@ -1237,7 +1237,7 @@ def random_masking(self, x: torch.Tensor, mask_ratio: float) -> Tuple[torch.Tens
|
|
| 1237 |
ids_restore = [3, 9, 1, 7, 2, 8, 4, 6, 5, 0] is the inverse permutation.
|
| 1238 |
For example, original module 0 occupies position 3 after shuffling."""
|
| 1239 |
b, l, d = x.shape
|
| 1240 |
-
mask, ids_shuffle, ids_restore = self.
|
| 1241 |
batch_size=b,
|
| 1242 |
seq_len=l,
|
| 1243 |
device=x.device,
|
|
@@ -1260,7 +1260,7 @@ def replace_masking(
|
|
| 1260 |
raise RuntimeError("replace_masking requires encoder_mask_token to be initialized.")
|
| 1261 |
|
| 1262 |
b, l, _ = x.shape
|
| 1263 |
-
mask, _, _ = self.
|
| 1264 |
batch_size=b,
|
| 1265 |
seq_len=l,
|
| 1266 |
device=x.device,
|
|
@@ -1271,7 +1271,7 @@ def replace_masking(
|
|
| 1271 |
x_masked = torch.where(mask.bool().unsqueeze(-1), mask_tokens, x)
|
| 1272 |
return x_masked, mask, ids_restore
|
| 1273 |
|
| 1274 |
-
def
|
| 1275 |
pos = self.pos_emb[:, 1:, :] if self.use_cls else self.pos_emb
|
| 1276 |
if pos.shape[1] != x.shape[1]:
|
| 1277 |
raise RuntimeError(
|
|
@@ -1306,10 +1306,10 @@ def forward_encoder(
|
|
| 1306 |
if self.mask_style == "replace":
|
| 1307 |
# Replace token contents, then add module-ID embeddings while retaining all positions.
|
| 1308 |
x, mask, ids_restore = self.replace_masking(x, mask_ratio)
|
| 1309 |
-
x = self.
|
| 1310 |
else:
|
| 1311 |
# MAE path: add module-ID embeddings, then drop masked modules.
|
| 1312 |
-
x = self.
|
| 1313 |
x, mask, ids_restore = self.random_masking(x, mask_ratio)
|
| 1314 |
|
| 1315 |
# Sparse module gating (1B): suppress unimportant modules
|
|
@@ -1361,7 +1361,7 @@ def _build_full_tokens(self, x: torch.Tensor, ids_restore: torch.Tensor) -> torc
|
|
| 1361 |
return torch.cat([cls_token, x_], dim=1)
|
| 1362 |
return x_
|
| 1363 |
|
| 1364 |
-
def
|
| 1365 |
self,
|
| 1366 |
module_feats: torch.Tensor,
|
| 1367 |
) -> torch.Tensor:
|
|
@@ -1383,13 +1383,13 @@ def _aggregate_module_to_gene(
|
|
| 1383 |
gene_counts = gene_counts.clamp(min=1.0).unsqueeze(-1) # (B, G, 1)
|
| 1384 |
return gene_feats / gene_counts
|
| 1385 |
|
| 1386 |
-
def
|
| 1387 |
self,
|
| 1388 |
module_feats: torch.Tensor,
|
| 1389 |
) -> Union[List[torch.Tensor], torch.Tensor]:
|
| 1390 |
"""module_feats: (B, L, D)"""
|
| 1391 |
if self.recon_target_level == "gene" and self.decoder_gene_agg:
|
| 1392 |
-
gene_feats = self.
|
| 1393 |
assert self.gene_w is not None and self.gene_b is not None
|
| 1394 |
# Apply a separate linear projection to each gene.
|
| 1395 |
preds = torch.einsum("bgd,gdk->bgk", gene_feats, self.gene_w).squeeze(-1)
|
|
@@ -1445,9 +1445,9 @@ def forward_decoder_simple(
|
|
| 1445 |
Returns per-module predictions, or a (B, G) tensor for decoder_gene_agg."""
|
| 1446 |
x_full = self._build_full_tokens(x, ids_restore)
|
| 1447 |
module_feats = x_full[:, 1:, :] if self.use_cls else x_full
|
| 1448 |
-
return self.
|
| 1449 |
|
| 1450 |
-
def
|
| 1451 |
self,
|
| 1452 |
x: torch.Tensor,
|
| 1453 |
ids_restore: torch.Tensor,
|
|
@@ -1459,7 +1459,7 @@ def forward_decoder_performer(
|
|
| 1459 |
x_full = self.decoder(x_full, pos_emb=self.layer_pos_emb(x_full))
|
| 1460 |
x_full = self.decoder_norm(x_full)
|
| 1461 |
module_feats = x_full[:, 1:, :] if self.use_cls else x_full
|
| 1462 |
-
return self.
|
| 1463 |
|
| 1464 |
def forward_decoder_cross_attn(
|
| 1465 |
self,
|
|
@@ -1508,14 +1508,14 @@ def forward_decoder_cross_attn(
|
|
| 1508 |
masked_queries = self.decoder_norm(masked_queries).to(dtype=module_feats.dtype)
|
| 1509 |
module_feats[mask_bool] = masked_queries.reshape(-1, dim)
|
| 1510 |
|
| 1511 |
-
return self.
|
| 1512 |
|
| 1513 |
def forward_predictor_from_encoder(
|
| 1514 |
self,
|
| 1515 |
x: torch.Tensor,
|
| 1516 |
) -> Union[List[torch.Tensor], torch.Tensor]:
|
| 1517 |
module_feats = x[:, 1:, :] if self.use_cls else x
|
| 1518 |
-
return self.
|
| 1519 |
|
| 1520 |
def compute_loss(
|
| 1521 |
self,
|
|
@@ -1618,7 +1618,7 @@ def forward(
|
|
| 1618 |
return out
|
| 1619 |
|
| 1620 |
if self.decoder_type == "performer":
|
| 1621 |
-
preds = self.
|
| 1622 |
elif self.decoder_type == "cross_attn":
|
| 1623 |
preds = self.forward_decoder_cross_attn(latent, mask, ids_restore)
|
| 1624 |
elif self.decoder_type == "simple":
|
|
@@ -1643,7 +1643,7 @@ def get_last_loss_breakdown(self) -> dict:
|
|
| 1643 |
def load_state_dict(self, state_dict, strict: bool = True, assign: bool = False):
|
| 1644 |
"""Load current or pre-rename weights under the module-named interface."""
|
| 1645 |
return super().load_state_dict(
|
| 1646 |
-
|
| 1647 |
strict=strict,
|
| 1648 |
assign=assign,
|
| 1649 |
)
|
|
|
|
| 1 |
+
"""BioMod encoder for gene groups with MAE-style masking and configurable
|
| 2 |
reconstruction decoders or direct prediction heads."""
|
| 3 |
|
| 4 |
import sys
|
|
|
|
| 16 |
sys.path.insert(0, model_dependencies_dir)
|
| 17 |
|
| 18 |
try:
|
| 19 |
+
from pytorch_cont.ding_models import (
|
| 20 |
+
Performer as BioModEncoder,
|
| 21 |
Always,
|
| 22 |
cast_tuple,
|
| 23 |
)
|
| 24 |
except ImportError:
|
| 25 |
try:
|
| 26 |
+
from pytorch_cont.pytorch import (
|
| 27 |
+
Performer as BioModEncoder,
|
| 28 |
Always,
|
| 29 |
cast_tuple,
|
| 30 |
)
|
| 31 |
except ImportError:
|
| 32 |
from performer_pytorch.performer_pytorch import (
|
| 33 |
+
Performer as BioModEncoder,
|
| 34 |
Always,
|
| 35 |
cast_tuple,
|
| 36 |
)
|
|
|
|
| 41 |
import numpy as np
|
| 42 |
|
| 43 |
|
| 44 |
+
def _migrate_legacy_biomod_name(value: str) -> str:
|
| 45 |
"""Translate serialized pre-rename names without changing their semantics."""
|
| 46 |
replacements = (
|
| 47 |
("PATHCH", "MODULE"),
|
|
|
|
| 59 |
return value
|
| 60 |
|
| 61 |
|
| 62 |
+
def migrate_legacy_biomod_config(config: Dict[str, object]) -> Dict[str, object]:
|
| 63 |
"""Return a module-named copy of an older serialized configuration."""
|
| 64 |
def translate(value):
|
| 65 |
if isinstance(value, dict):
|
| 66 |
migrated = {}
|
| 67 |
for key, item in value.items():
|
| 68 |
+
new_key = _migrate_legacy_biomod_name(key) if isinstance(key, str) else key
|
| 69 |
if new_key in migrated:
|
| 70 |
raise ValueError(
|
| 71 |
f"Legacy configuration migration would duplicate key {new_key!r}"
|
|
|
|
| 77 |
if isinstance(value, tuple):
|
| 78 |
return tuple(translate(item) for item in value)
|
| 79 |
if isinstance(value, str):
|
| 80 |
+
return _migrate_legacy_biomod_name(value)
|
| 81 |
return value
|
| 82 |
|
| 83 |
return translate(dict(config))
|
| 84 |
|
| 85 |
|
| 86 |
+
def migrate_legacy_biomod_state_dict(state_dict):
|
| 87 |
"""Translate old state-dict key names before PyTorch performs strict loading."""
|
| 88 |
try:
|
| 89 |
migrated = state_dict.__class__()
|
|
|
|
| 91 |
migrated = {}
|
| 92 |
|
| 93 |
for key, value in state_dict.items():
|
| 94 |
+
new_key = _migrate_legacy_biomod_name(key) if isinstance(key, str) else key
|
| 95 |
if new_key in migrated:
|
| 96 |
# If both spellings are supplied, prefer the explicit new spelling.
|
| 97 |
if key == new_key:
|
|
|
|
| 101 |
|
| 102 |
if hasattr(state_dict, "_metadata"):
|
| 103 |
migrated._metadata = {
|
| 104 |
+
_migrate_legacy_biomod_name(key): value
|
| 105 |
for key, value in state_dict._metadata.items()
|
| 106 |
}
|
| 107 |
return migrated
|
|
|
|
| 135 |
)
|
| 136 |
|
| 137 |
|
| 138 |
+
def sanitize_biomod_config(
|
| 139 |
config: Dict[str, object],
|
| 140 |
*,
|
| 141 |
source: str = "config",
|
| 142 |
log_fn: Optional[Callable[[str], None]] = None,
|
| 143 |
) -> Dict[str, object]:
|
| 144 |
+
cleaned = migrate_legacy_biomod_config(config)
|
| 145 |
legacy_mode = str(cleaned.get("extra_gene_mode", "none")).lower()
|
| 146 |
legacy_num = int(cleaned.get("extra_gene_num", 0) or 0)
|
| 147 |
if legacy_mode != "none" or legacy_num > 0:
|
|
|
|
| 160 |
|
| 161 |
return cleaned
|
| 162 |
|
| 163 |
+
# BioModEmbedding is implemented here to support three expression-fusion methods.
|
| 164 |
# from sclong_module.models_module import ModuleToGeneDecoder
|
| 165 |
|
| 166 |
|
|
|
|
| 220 |
return query
|
| 221 |
|
| 222 |
|
| 223 |
+
class BioModEmbedding(nn.Module):
|
| 224 |
"""Gene embedding, MLP, and attention pooling produce a module token.
|
| 225 |
|
| 226 |
Expression fusion supports three methods:
|
|
|
|
| 823 |
return self._forward_bucketed(expr, return_module_targets)
|
| 824 |
|
| 825 |
|
| 826 |
+
class BioMod(nn.Module):
|
| 827 |
+
"""BioMod encoder for gene-group inputs with configurable masking and decoding.
|
| 828 |
|
| 829 |
use_gene2vec=True initializes from supplied pretrained gene embeddings;
|
| 830 |
False uses random gene embeddings for ablation experiments."""
|
|
|
|
| 996 |
print(f"[INFO] use_gene2vec=False: Using randomly initialized gene embeddings (dim={gene_embed_dim})")
|
| 997 |
|
| 998 |
# Module embedding layer.
|
| 999 |
+
self.module_embed = BioModEmbedding(
|
| 1000 |
module_sets=module_sets,
|
| 1001 |
num_genes=num_genes,
|
| 1002 |
gene_embed_dim=gene_embed_dim,
|
|
|
|
| 1054 |
self.dropout = nn.Dropout(emb_dropout)
|
| 1055 |
|
| 1056 |
# Performer encoder.
|
| 1057 |
+
self.encoder = BioModEncoder(
|
| 1058 |
base_dim, enc_depth, enc_heads, enc_dim_head,
|
| 1059 |
local_attn_heads, local_window_size, causal, ff_mult, nb_features,
|
| 1060 |
feature_redraw_interval, reversible, ff_chunks, generalized_attention,
|
|
|
|
| 1090 |
if self.decoder_type == "performer":
|
| 1091 |
self.decoder_pos_emb = nn.Parameter(torch.zeros(1, pos_tokens, self.dec_embed_dim))
|
| 1092 |
self.decoder_dropout = nn.Dropout(emb_dropout)
|
| 1093 |
+
self.decoder = BioModEncoder(
|
| 1094 |
self.dec_embed_dim, dec_depth, dec_heads, dec_dim_head,
|
| 1095 |
local_attn_heads=cast_tuple(0), local_window_size=256, causal=False, ff_mult=dec_ff_mult,
|
| 1096 |
nb_features=None, feature_redraw_interval=feature_redraw_interval,
|
|
|
|
| 1203 |
nn.init.constant_(m.bias, 0)
|
| 1204 |
nn.init.constant_(m.weight, 1.0)
|
| 1205 |
|
| 1206 |
+
def _sample_biomod_mask(
|
| 1207 |
self,
|
| 1208 |
batch_size: int,
|
| 1209 |
seq_len: int,
|
|
|
|
| 1237 |
ids_restore = [3, 9, 1, 7, 2, 8, 4, 6, 5, 0] is the inverse permutation.
|
| 1238 |
For example, original module 0 occupies position 3 after shuffling."""
|
| 1239 |
b, l, d = x.shape
|
| 1240 |
+
mask, ids_shuffle, ids_restore = self._sample_biomod_mask(
|
| 1241 |
batch_size=b,
|
| 1242 |
seq_len=l,
|
| 1243 |
device=x.device,
|
|
|
|
| 1260 |
raise RuntimeError("replace_masking requires encoder_mask_token to be initialized.")
|
| 1261 |
|
| 1262 |
b, l, _ = x.shape
|
| 1263 |
+
mask, _, _ = self._sample_biomod_mask(
|
| 1264 |
batch_size=b,
|
| 1265 |
seq_len=l,
|
| 1266 |
device=x.device,
|
|
|
|
| 1271 |
x_masked = torch.where(mask.bool().unsqueeze(-1), mask_tokens, x)
|
| 1272 |
return x_masked, mask, ids_restore
|
| 1273 |
|
| 1274 |
+
def _add_biomod_position_embedding(self, x: torch.Tensor) -> torch.Tensor:
|
| 1275 |
pos = self.pos_emb[:, 1:, :] if self.use_cls else self.pos_emb
|
| 1276 |
if pos.shape[1] != x.shape[1]:
|
| 1277 |
raise RuntimeError(
|
|
|
|
| 1306 |
if self.mask_style == "replace":
|
| 1307 |
# Replace token contents, then add module-ID embeddings while retaining all positions.
|
| 1308 |
x, mask, ids_restore = self.replace_masking(x, mask_ratio)
|
| 1309 |
+
x = self._add_biomod_position_embedding(x)
|
| 1310 |
else:
|
| 1311 |
# MAE path: add module-ID embeddings, then drop masked modules.
|
| 1312 |
+
x = self._add_biomod_position_embedding(x)
|
| 1313 |
x, mask, ids_restore = self.random_masking(x, mask_ratio)
|
| 1314 |
|
| 1315 |
# Sparse module gating (1B): suppress unimportant modules
|
|
|
|
| 1361 |
return torch.cat([cls_token, x_], dim=1)
|
| 1362 |
return x_
|
| 1363 |
|
| 1364 |
+
def _aggregate_biomod_to_gene(
|
| 1365 |
self,
|
| 1366 |
module_feats: torch.Tensor,
|
| 1367 |
) -> torch.Tensor:
|
|
|
|
| 1383 |
gene_counts = gene_counts.clamp(min=1.0).unsqueeze(-1) # (B, G, 1)
|
| 1384 |
return gene_feats / gene_counts
|
| 1385 |
|
| 1386 |
+
def _predict_from_biomod_features(
|
| 1387 |
self,
|
| 1388 |
module_feats: torch.Tensor,
|
| 1389 |
) -> Union[List[torch.Tensor], torch.Tensor]:
|
| 1390 |
"""module_feats: (B, L, D)"""
|
| 1391 |
if self.recon_target_level == "gene" and self.decoder_gene_agg:
|
| 1392 |
+
gene_feats = self._aggregate_biomod_to_gene(module_feats) # (B, G, D)
|
| 1393 |
assert self.gene_w is not None and self.gene_b is not None
|
| 1394 |
# Apply a separate linear projection to each gene.
|
| 1395 |
preds = torch.einsum("bgd,gdk->bgk", gene_feats, self.gene_w).squeeze(-1)
|
|
|
|
| 1445 |
Returns per-module predictions, or a (B, G) tensor for decoder_gene_agg."""
|
| 1446 |
x_full = self._build_full_tokens(x, ids_restore)
|
| 1447 |
module_feats = x_full[:, 1:, :] if self.use_cls else x_full
|
| 1448 |
+
return self._predict_from_biomod_features(module_feats)
|
| 1449 |
|
| 1450 |
+
def forward_decoder_biomod(
|
| 1451 |
self,
|
| 1452 |
x: torch.Tensor,
|
| 1453 |
ids_restore: torch.Tensor,
|
|
|
|
| 1459 |
x_full = self.decoder(x_full, pos_emb=self.layer_pos_emb(x_full))
|
| 1460 |
x_full = self.decoder_norm(x_full)
|
| 1461 |
module_feats = x_full[:, 1:, :] if self.use_cls else x_full
|
| 1462 |
+
return self._predict_from_biomod_features(module_feats)
|
| 1463 |
|
| 1464 |
def forward_decoder_cross_attn(
|
| 1465 |
self,
|
|
|
|
| 1508 |
masked_queries = self.decoder_norm(masked_queries).to(dtype=module_feats.dtype)
|
| 1509 |
module_feats[mask_bool] = masked_queries.reshape(-1, dim)
|
| 1510 |
|
| 1511 |
+
return self._predict_from_biomod_features(module_feats)
|
| 1512 |
|
| 1513 |
def forward_predictor_from_encoder(
|
| 1514 |
self,
|
| 1515 |
x: torch.Tensor,
|
| 1516 |
) -> Union[List[torch.Tensor], torch.Tensor]:
|
| 1517 |
module_feats = x[:, 1:, :] if self.use_cls else x
|
| 1518 |
+
return self._predict_from_biomod_features(module_feats)
|
| 1519 |
|
| 1520 |
def compute_loss(
|
| 1521 |
self,
|
|
|
|
| 1618 |
return out
|
| 1619 |
|
| 1620 |
if self.decoder_type == "performer":
|
| 1621 |
+
preds = self.forward_decoder_biomod(latent, ids_restore)
|
| 1622 |
elif self.decoder_type == "cross_attn":
|
| 1623 |
preds = self.forward_decoder_cross_attn(latent, mask, ids_restore)
|
| 1624 |
elif self.decoder_type == "simple":
|
|
|
|
| 1643 |
def load_state_dict(self, state_dict, strict: bool = True, assign: bool = False):
|
| 1644 |
"""Load current or pre-rename weights under the module-named interface."""
|
| 1645 |
return super().load_state_dict(
|
| 1646 |
+
migrate_legacy_biomod_state_dict(state_dict),
|
| 1647 |
strict=strict,
|
| 1648 |
assign=assign,
|
| 1649 |
)
|