Commit
Β·
933ca80
1
Parent(s):
ec19834
update with 12L and 20L i4096 gc95M models, multitask and quantiz code
Browse filesThis view is limited to 50 files because it contains too many changes. Β
See raw diff
- .gitattributes +1 -1
- MANIFEST.in +3 -3
- config.json +9 -8
- fine_tuned_models/gf-12L-95M-i4096_MTLCellClassifier_CELLxGENE_240522/config.json +24 -0
- fine_tuned_models/gf-12L-95M-i4096_MTLCellClassifier_CELLxGENE_240522/pytorch_model.bin +3 -0
- fine_tuned_models/{geneformer-6L-30M_CellClassifier_cardiomyopathies_220224 β gf-6L-30M-i2048_CellClassifier_cardiomyopathies_220224}/config.json +0 -0
- fine_tuned_models/{geneformer-6L-30M_CellClassifier_cardiomyopathies_220224 β gf-6L-30M-i2048_CellClassifier_cardiomyopathies_220224}/optimizer.pt +0 -0
- fine_tuned_models/{geneformer-6L-30M_CellClassifier_cardiomyopathies_220224 β gf-6L-30M-i2048_CellClassifier_cardiomyopathies_220224}/pytorch_model.bin +0 -0
- fine_tuned_models/{geneformer-6L-30M_CellClassifier_cardiomyopathies_220224 β gf-6L-30M-i2048_CellClassifier_cardiomyopathies_220224}/rng_state.pth +0 -0
- fine_tuned_models/{geneformer-6L-30M_CellClassifier_cardiomyopathies_220224 β gf-6L-30M-i2048_CellClassifier_cardiomyopathies_220224}/scheduler.pt +0 -0
- fine_tuned_models/{geneformer-6L-30M_CellClassifier_cardiomyopathies_220224 β gf-6L-30M-i2048_CellClassifier_cardiomyopathies_220224}/trainer_state.json +0 -0
- fine_tuned_models/{geneformer-6L-30M_CellClassifier_cardiomyopathies_220224 β gf-6L-30M-i2048_CellClassifier_cardiomyopathies_220224}/training_args.bin +0 -0
- geneformer/__init__.py +10 -5
- geneformer/classifier.py +74 -16
- geneformer/classifier_utils.py +117 -5
- geneformer/collator_for_classification.py +15 -19
- geneformer/emb_extractor.py +20 -13
- geneformer/gene_dictionaries_30m/gene_median_dictionary_gc30M.pkl +3 -0
- geneformer/{gene_name_id_dict.pkl β gene_dictionaries_30m/gene_name_id_dict_gc30M.pkl} +0 -0
- geneformer/gene_dictionaries_30m/token_dictionary_gc30M.pkl +3 -0
- geneformer/gene_median_dictionary.pkl +0 -0
- geneformer/in_silico_perturber.py +733 -143
- geneformer/in_silico_perturber_stats.py +22 -6
- geneformer/mtl/__init__.py +0 -0
- geneformer/mtl/collators.py +66 -0
- geneformer/mtl/data.py +116 -0
- geneformer/mtl/eval_utils.py +81 -0
- geneformer/mtl/imports.py +46 -0
- geneformer/mtl/model.py +84 -0
- geneformer/mtl/optuna_utils.py +21 -0
- geneformer/mtl/train.py +242 -0
- geneformer/mtl/train_utils.py +126 -0
- geneformer/mtl/utils.py +106 -0
- geneformer/mtl_classifier.py +338 -0
- geneformer/perturber_utils.py +168 -16
- geneformer/pretrainer.py +0 -13
- geneformer/token_dictionary.pkl +0 -0
- geneformer/token_dictionary_gc95M.pkl +0 -0
- generation_config.json +5 -0
- {geneformer-12L-30M β gf-12L-30M-i2048}/config.json +0 -0
- {geneformer-12L-30M β gf-12L-30M-i2048}/pytorch_model.bin +0 -0
- {geneformer-12L-30M β gf-12L-30M-i2048}/training_args.bin +0 -0
- gf-12L-95M-i4096/config.json +24 -0
- gf-12L-95M-i4096/generation_config.json +5 -0
- gf-12L-95M-i4096/model.safetensors +3 -0
- gf-12L-95M-i4096/training_args.bin +3 -0
- gf-12L-95M-i4096_CLcancer/config.json +25 -0
- gf-12L-95M-i4096_CLcancer/generation_config.json +5 -0
- gf-12L-95M-i4096_CLcancer/model.safetensors +3 -0
- gf-12L-95M-i4096_CLcancer/training_args.bin +3 -0
.gitattributes
CHANGED
|
@@ -26,4 +26,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 26 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 27 |
*.zstandard filter=lfs diff=lfs merge=lfs -text
|
| 28 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
model.safetensors filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 26 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 27 |
*.zstandard filter=lfs diff=lfs merge=lfs -text
|
| 28 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
model.safetensors filter=lfs diff=lfs merge=lfs -text
|
MANIFEST.in
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
-
include geneformer/
|
| 2 |
-
include geneformer/
|
| 3 |
-
include geneformer/
|
|
|
|
| 1 |
+
include geneformer/gene_median_dictionary_95m.pkl
|
| 2 |
+
include geneformer/token_dictionary_95m.pkl
|
| 3 |
+
include geneformer/gene_name_id_dict_95m.pkl
|
config.json
CHANGED
|
@@ -3,21 +3,22 @@
|
|
| 3 |
"BertForMaskedLM"
|
| 4 |
],
|
| 5 |
"attention_probs_dropout_prob": 0.02,
|
| 6 |
-
"
|
| 7 |
"hidden_act": "relu",
|
| 8 |
"hidden_dropout_prob": 0.02,
|
| 9 |
-
"hidden_size":
|
| 10 |
"initializer_range": 0.02,
|
| 11 |
-
"intermediate_size":
|
| 12 |
"layer_norm_eps": 1e-12,
|
| 13 |
-
"max_position_embeddings":
|
| 14 |
"model_type": "bert",
|
| 15 |
-
"num_attention_heads":
|
| 16 |
-
"num_hidden_layers":
|
| 17 |
"pad_token_id": 0,
|
| 18 |
"position_embedding_type": "absolute",
|
| 19 |
-
"
|
|
|
|
| 20 |
"type_vocab_size": 2,
|
| 21 |
"use_cache": true,
|
| 22 |
-
"vocab_size":
|
| 23 |
}
|
|
|
|
| 3 |
"BertForMaskedLM"
|
| 4 |
],
|
| 5 |
"attention_probs_dropout_prob": 0.02,
|
| 6 |
+
"classifier_dropout": null,
|
| 7 |
"hidden_act": "relu",
|
| 8 |
"hidden_dropout_prob": 0.02,
|
| 9 |
+
"hidden_size": 512,
|
| 10 |
"initializer_range": 0.02,
|
| 11 |
+
"intermediate_size": 1024,
|
| 12 |
"layer_norm_eps": 1e-12,
|
| 13 |
+
"max_position_embeddings": 4096,
|
| 14 |
"model_type": "bert",
|
| 15 |
+
"num_attention_heads": 8,
|
| 16 |
+
"num_hidden_layers": 12,
|
| 17 |
"pad_token_id": 0,
|
| 18 |
"position_embedding_type": "absolute",
|
| 19 |
+
"torch_dtype": "float32",
|
| 20 |
+
"transformers_version": "4.37.1",
|
| 21 |
"type_vocab_size": 2,
|
| 22 |
"use_cache": true,
|
| 23 |
+
"vocab_size": 20275
|
| 24 |
}
|
fine_tuned_models/gf-12L-95M-i4096_MTLCellClassifier_CELLxGENE_240522/config.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"BertForMaskedLM"
|
| 4 |
+
],
|
| 5 |
+
"attention_probs_dropout_prob": 0.02,
|
| 6 |
+
"classifier_dropout": null,
|
| 7 |
+
"hidden_act": "relu",
|
| 8 |
+
"hidden_dropout_prob": 0.02,
|
| 9 |
+
"hidden_size": 512,
|
| 10 |
+
"initializer_range": 0.02,
|
| 11 |
+
"intermediate_size": 1024,
|
| 12 |
+
"layer_norm_eps": 1e-12,
|
| 13 |
+
"max_position_embeddings": 4096,
|
| 14 |
+
"model_type": "bert",
|
| 15 |
+
"num_attention_heads": 8,
|
| 16 |
+
"num_hidden_layers": 12,
|
| 17 |
+
"pad_token_id": 0,
|
| 18 |
+
"position_embedding_type": "absolute",
|
| 19 |
+
"torch_dtype": "float32",
|
| 20 |
+
"transformers_version": "4.37.2",
|
| 21 |
+
"type_vocab_size": 2,
|
| 22 |
+
"use_cache": true,
|
| 23 |
+
"vocab_size": 20275
|
| 24 |
+
}
|
fine_tuned_models/gf-12L-95M-i4096_MTLCellClassifier_CELLxGENE_240522/pytorch_model.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:07b28d8c7bb789d59755c42d32f6182cc04d2cf34aafaa6397aa50e4fdf1a9b4
|
| 3 |
+
size 152363342
|
fine_tuned_models/{geneformer-6L-30M_CellClassifier_cardiomyopathies_220224 β gf-6L-30M-i2048_CellClassifier_cardiomyopathies_220224}/config.json
RENAMED
|
File without changes
|
fine_tuned_models/{geneformer-6L-30M_CellClassifier_cardiomyopathies_220224 β gf-6L-30M-i2048_CellClassifier_cardiomyopathies_220224}/optimizer.pt
RENAMED
|
File without changes
|
fine_tuned_models/{geneformer-6L-30M_CellClassifier_cardiomyopathies_220224 β gf-6L-30M-i2048_CellClassifier_cardiomyopathies_220224}/pytorch_model.bin
RENAMED
|
File without changes
|
fine_tuned_models/{geneformer-6L-30M_CellClassifier_cardiomyopathies_220224 β gf-6L-30M-i2048_CellClassifier_cardiomyopathies_220224}/rng_state.pth
RENAMED
|
File without changes
|
fine_tuned_models/{geneformer-6L-30M_CellClassifier_cardiomyopathies_220224 β gf-6L-30M-i2048_CellClassifier_cardiomyopathies_220224}/scheduler.pt
RENAMED
|
File without changes
|
fine_tuned_models/{geneformer-6L-30M_CellClassifier_cardiomyopathies_220224 β gf-6L-30M-i2048_CellClassifier_cardiomyopathies_220224}/trainer_state.json
RENAMED
|
File without changes
|
fine_tuned_models/{geneformer-6L-30M_CellClassifier_cardiomyopathies_220224 β gf-6L-30M-i2048_CellClassifier_cardiomyopathies_220224}/training_args.bin
RENAMED
|
File without changes
|
geneformer/__init__.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
| 1 |
# ruff: noqa: F401
|
| 2 |
from pathlib import Path
|
|
|
|
|
|
|
| 3 |
|
| 4 |
-
GENE_MEDIAN_FILE = Path(__file__).parent / "
|
| 5 |
-
TOKEN_DICTIONARY_FILE = Path(__file__).parent / "
|
| 6 |
-
ENSEMBL_DICTIONARY_FILE = Path(__file__).parent / "
|
| 7 |
-
ENSEMBL_MAPPING_FILE = Path(__file__).parent / "
|
| 8 |
|
| 9 |
from . import (
|
| 10 |
collator_for_classification,
|
|
@@ -25,4 +27,7 @@ from .pretrainer import GeneformerPretrainer
|
|
| 25 |
from .tokenizer import TranscriptomeTokenizer
|
| 26 |
|
| 27 |
from . import classifier # noqa # isort:skip
|
| 28 |
-
from .classifier import Classifier # noqa # isort:skip
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# ruff: noqa: F401
|
| 2 |
from pathlib import Path
|
| 3 |
+
import warnings
|
| 4 |
+
warnings.filterwarnings("ignore", message=".*The 'nopython' keyword.*") # noqa # isort:skip
|
| 5 |
|
| 6 |
+
GENE_MEDIAN_FILE = Path(__file__).parent / "gene_median_dictionary_gc95M.pkl"
|
| 7 |
+
TOKEN_DICTIONARY_FILE = Path(__file__).parent / "token_dictionary_gc95M.pkl"
|
| 8 |
+
ENSEMBL_DICTIONARY_FILE = Path(__file__).parent / "gene_name_id_dict_gc95M.pkl"
|
| 9 |
+
ENSEMBL_MAPPING_FILE = Path(__file__).parent / "ensembl_mapping_dict_gc95M.pkl"
|
| 10 |
|
| 11 |
from . import (
|
| 12 |
collator_for_classification,
|
|
|
|
| 27 |
from .tokenizer import TranscriptomeTokenizer
|
| 28 |
|
| 29 |
from . import classifier # noqa # isort:skip
|
| 30 |
+
from .classifier import Classifier # noqa # isort:skip
|
| 31 |
+
|
| 32 |
+
from . import mtl_classifier # noqa # isort:skip
|
| 33 |
+
from .mtl_classifier import MTLClassifier # noqa # isort:skip
|
geneformer/classifier.py
CHANGED
|
@@ -72,6 +72,7 @@ logger = logging.getLogger(__name__)
|
|
| 72 |
class Classifier:
|
| 73 |
valid_option_dict = {
|
| 74 |
"classifier": {"cell", "gene"},
|
|
|
|
| 75 |
"cell_state_dict": {None, dict},
|
| 76 |
"gene_class_dict": {None, dict},
|
| 77 |
"filter_data": {None, dict},
|
|
@@ -93,6 +94,7 @@ class Classifier:
|
|
| 93 |
def __init__(
|
| 94 |
self,
|
| 95 |
classifier=None,
|
|
|
|
| 96 |
cell_state_dict=None,
|
| 97 |
gene_class_dict=None,
|
| 98 |
filter_data=None,
|
|
@@ -118,6 +120,13 @@ class Classifier:
|
|
| 118 |
|
| 119 |
classifier : {"cell", "gene"}
|
| 120 |
| Whether to fine-tune a cell state or gene classifier.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
cell_state_dict : None, dict
|
| 122 |
| Cell states to fine-tune model to distinguish.
|
| 123 |
| Two-item dictionary with keys: state_key and states
|
|
@@ -191,6 +200,7 @@ class Classifier:
|
|
| 191 |
self.model_type = "CellClassifier"
|
| 192 |
elif self.classifier == "gene":
|
| 193 |
self.model_type = "GeneClassifier"
|
|
|
|
| 194 |
self.cell_state_dict = cell_state_dict
|
| 195 |
self.gene_class_dict = gene_class_dict
|
| 196 |
self.filter_data = filter_data
|
|
@@ -256,7 +266,7 @@ class Classifier:
|
|
| 256 |
f"Genes to classify {missing_genes} are not in token dictionary."
|
| 257 |
)
|
| 258 |
self.gene_class_dict = {
|
| 259 |
-
k: set([self.gene_token_dict.get(gene) for gene in v])
|
| 260 |
for k, v in self.gene_class_dict.items()
|
| 261 |
}
|
| 262 |
empty_classes = []
|
|
@@ -403,6 +413,15 @@ class Classifier:
|
|
| 403 |
"Column name 'labels' must be reserved for class IDs. Please rename column."
|
| 404 |
)
|
| 405 |
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
|
| 407 |
if self.classifier == "cell":
|
| 408 |
# remove cell states representing < rare_threshold of cells
|
|
@@ -505,6 +524,7 @@ class Classifier:
|
|
| 505 |
output_directory,
|
| 506 |
output_prefix,
|
| 507 |
save_eval_output=True,
|
|
|
|
| 508 |
):
|
| 509 |
"""
|
| 510 |
Train cell state or gene classifier using all data.
|
|
@@ -525,13 +545,20 @@ class Classifier:
|
|
| 525 |
save_eval_output : bool
|
| 526 |
| Whether to save cross-fold eval output
|
| 527 |
| Saves as pickle file of dictionary of eval metrics
|
| 528 |
-
|
|
|
|
|
|
|
|
|
|
| 529 |
**Output**
|
| 530 |
|
| 531 |
Returns trainer after fine-tuning with all data.
|
| 532 |
|
| 533 |
"""
|
| 534 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 535 |
##### Load data and prepare output directory #####
|
| 536 |
# load numerical id to class dictionary (id:class)
|
| 537 |
with open(id_class_dict_file, "rb") as f:
|
|
@@ -563,7 +590,7 @@ class Classifier:
|
|
| 563 |
)
|
| 564 |
assert len(targets) == len(labels)
|
| 565 |
data = cu.prep_gene_classifier_all_data(
|
| 566 |
-
data, targets, labels, self.max_ncells, self.nproc
|
| 567 |
)
|
| 568 |
|
| 569 |
trainer = self.train_classifier(
|
|
@@ -582,12 +609,15 @@ class Classifier:
|
|
| 582 |
split_id_dict=None,
|
| 583 |
attr_to_split=None,
|
| 584 |
attr_to_balance=None,
|
|
|
|
| 585 |
max_trials=100,
|
| 586 |
pval_threshold=0.1,
|
| 587 |
save_eval_output=True,
|
| 588 |
predict_eval=True,
|
| 589 |
predict_trainer=False,
|
| 590 |
n_hyperopt_trials=0,
|
|
|
|
|
|
|
| 591 |
):
|
| 592 |
"""
|
| 593 |
(Cross-)validate cell state or gene classifier.
|
|
@@ -622,6 +652,9 @@ class Classifier:
|
|
| 622 |
attr_to_balance : None, list
|
| 623 |
| List of attribute keys on which to balance data while splitting on attr_to_split
|
| 624 |
| e.g. ["age", "sex"] for balancing these characteristics while splitting by patient
|
|
|
|
|
|
|
|
|
|
| 625 |
max_trials : None, int
|
| 626 |
| Maximum number of trials of random splitting to try to achieve balanced other attribute
|
| 627 |
| If no split is found without significant (p < pval_threshold) differences in other attributes, will select best
|
|
@@ -640,11 +673,17 @@ class Classifier:
|
|
| 640 |
n_hyperopt_trials : int
|
| 641 |
| Number of trials to run for hyperparameter optimization
|
| 642 |
| If 0, will not optimize hyperparameters
|
|
|
|
|
|
|
| 643 |
"""
|
| 644 |
if self.num_crossval_splits == 0:
|
| 645 |
logger.error("num_crossval_splits must be 1 or 5 to validate.")
|
| 646 |
raise
|
| 647 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 648 |
# ensure number of genes in each class is > 5 if validating model
|
| 649 |
if self.classifier == "gene":
|
| 650 |
insuff_classes = [k for k, v in self.gene_class_dict.items() if len(v) < 5]
|
|
@@ -725,7 +764,7 @@ class Classifier:
|
|
| 725 |
else:
|
| 726 |
# 5-fold cross-validate
|
| 727 |
num_cells = len(data)
|
| 728 |
-
fifth_cells = num_cells * 0.2
|
| 729 |
num_eval = min((self.eval_size * num_cells), fifth_cells)
|
| 730 |
start = i * fifth_cells
|
| 731 |
end = start + num_eval
|
|
@@ -804,8 +843,19 @@ class Classifier:
|
|
| 804 |
self.max_ncells,
|
| 805 |
iteration_num,
|
| 806 |
self.nproc,
|
|
|
|
| 807 |
)
|
| 808 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 809 |
if self.oos_test_size > 0:
|
| 810 |
test_data = cu.prep_gene_classifier_split(
|
| 811 |
data,
|
|
@@ -817,7 +867,14 @@ class Classifier:
|
|
| 817 |
iteration_num,
|
| 818 |
self.nproc,
|
| 819 |
)
|
| 820 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 821 |
if n_hyperopt_trials == 0:
|
| 822 |
trainer = self.train_classifier(
|
| 823 |
model_directory,
|
|
@@ -966,7 +1023,7 @@ class Classifier:
|
|
| 966 |
subprocess.call(f"mkdir {output_directory}", shell=True)
|
| 967 |
|
| 968 |
##### Load model and training args #####
|
| 969 |
-
model = pu.load_model(self.model_type, num_classes, model_directory, "train")
|
| 970 |
def_training_args, def_freeze_layers = cu.get_default_train_args(
|
| 971 |
model, self.classifier, train_data, output_directory
|
| 972 |
)
|
|
@@ -990,14 +1047,14 @@ class Classifier:
|
|
| 990 |
##### Fine-tune the model #####
|
| 991 |
# define the data collator
|
| 992 |
if self.classifier == "cell":
|
| 993 |
-
data_collator = DataCollatorForCellClassification()
|
| 994 |
elif self.classifier == "gene":
|
| 995 |
-
data_collator = DataCollatorForGeneClassification()
|
| 996 |
|
| 997 |
# define function to initiate model
|
| 998 |
def model_init():
|
| 999 |
model = pu.load_model(
|
| 1000 |
-
self.model_type, num_classes, model_directory, "train"
|
| 1001 |
)
|
| 1002 |
|
| 1003 |
if self.freeze_layers is not None:
|
|
@@ -1009,7 +1066,8 @@ class Classifier:
|
|
| 1009 |
for param in module.parameters():
|
| 1010 |
param.requires_grad = False
|
| 1011 |
|
| 1012 |
-
|
|
|
|
| 1013 |
return model
|
| 1014 |
|
| 1015 |
# create the trainer
|
|
@@ -1122,7 +1180,7 @@ class Classifier:
|
|
| 1122 |
subprocess.call(f"mkdir {output_directory}", shell=True)
|
| 1123 |
|
| 1124 |
##### Load model and training args #####
|
| 1125 |
-
model = pu.load_model(self.model_type, num_classes, model_directory, "train")
|
| 1126 |
|
| 1127 |
def_training_args, def_freeze_layers = cu.get_default_train_args(
|
| 1128 |
model, self.classifier, train_data, output_directory
|
|
@@ -1152,9 +1210,9 @@ class Classifier:
|
|
| 1152 |
##### Fine-tune the model #####
|
| 1153 |
# define the data collator
|
| 1154 |
if self.classifier == "cell":
|
| 1155 |
-
data_collator = DataCollatorForCellClassification()
|
| 1156 |
elif self.classifier == "gene":
|
| 1157 |
-
data_collator = DataCollatorForGeneClassification()
|
| 1158 |
|
| 1159 |
# create the trainer
|
| 1160 |
trainer = Trainer(
|
|
@@ -1276,7 +1334,7 @@ class Classifier:
|
|
| 1276 |
test_data = pu.load_and_filter(None, self.nproc, test_data_file)
|
| 1277 |
|
| 1278 |
# load previously fine-tuned model
|
| 1279 |
-
model = pu.load_model(self.model_type, num_classes, model_directory, "eval")
|
| 1280 |
|
| 1281 |
# evaluate the model
|
| 1282 |
result = self.evaluate_model(
|
|
|
|
| 72 |
class Classifier:
|
| 73 |
valid_option_dict = {
|
| 74 |
"classifier": {"cell", "gene"},
|
| 75 |
+
"quantize": {bool, dict},
|
| 76 |
"cell_state_dict": {None, dict},
|
| 77 |
"gene_class_dict": {None, dict},
|
| 78 |
"filter_data": {None, dict},
|
|
|
|
| 94 |
def __init__(
|
| 95 |
self,
|
| 96 |
classifier=None,
|
| 97 |
+
quantize=False,
|
| 98 |
cell_state_dict=None,
|
| 99 |
gene_class_dict=None,
|
| 100 |
filter_data=None,
|
|
|
|
| 120 |
|
| 121 |
classifier : {"cell", "gene"}
|
| 122 |
| Whether to fine-tune a cell state or gene classifier.
|
| 123 |
+
quantize : bool, dict
|
| 124 |
+
| Whether to fine-tune a quantized model.
|
| 125 |
+
| If True and no config provided, will use default.
|
| 126 |
+
| Will use custom config if provided.
|
| 127 |
+
| Configs should be provided as dictionary of BitsAndBytesConfig (transformers) and LoraConfig (peft).
|
| 128 |
+
| For example: {"bnb_config": BitsAndBytesConfig(...),
|
| 129 |
+
| "peft_config": LoraConfig(...)}
|
| 130 |
cell_state_dict : None, dict
|
| 131 |
| Cell states to fine-tune model to distinguish.
|
| 132 |
| Two-item dictionary with keys: state_key and states
|
|
|
|
| 200 |
self.model_type = "CellClassifier"
|
| 201 |
elif self.classifier == "gene":
|
| 202 |
self.model_type = "GeneClassifier"
|
| 203 |
+
self.quantize = quantize
|
| 204 |
self.cell_state_dict = cell_state_dict
|
| 205 |
self.gene_class_dict = gene_class_dict
|
| 206 |
self.filter_data = filter_data
|
|
|
|
| 266 |
f"Genes to classify {missing_genes} are not in token dictionary."
|
| 267 |
)
|
| 268 |
self.gene_class_dict = {
|
| 269 |
+
k: list(set([self.gene_token_dict.get(gene) for gene in v]))
|
| 270 |
for k, v in self.gene_class_dict.items()
|
| 271 |
}
|
| 272 |
empty_classes = []
|
|
|
|
| 413 |
"Column name 'labels' must be reserved for class IDs. Please rename column."
|
| 414 |
)
|
| 415 |
raise
|
| 416 |
+
|
| 417 |
+
if (attr_to_split is not None) and (attr_to_balance is None):
|
| 418 |
+
logger.error(
|
| 419 |
+
"Splitting by attribute while balancing confounders requires both attr_to_split and attr_to_balance to be defined."
|
| 420 |
+
)
|
| 421 |
+
raise
|
| 422 |
+
|
| 423 |
+
if not isinstance(attr_to_balance, list):
|
| 424 |
+
attr_to_balance = [attr_to_balance]
|
| 425 |
|
| 426 |
if self.classifier == "cell":
|
| 427 |
# remove cell states representing < rare_threshold of cells
|
|
|
|
| 524 |
output_directory,
|
| 525 |
output_prefix,
|
| 526 |
save_eval_output=True,
|
| 527 |
+
gene_balance=False,
|
| 528 |
):
|
| 529 |
"""
|
| 530 |
Train cell state or gene classifier using all data.
|
|
|
|
| 545 |
save_eval_output : bool
|
| 546 |
| Whether to save cross-fold eval output
|
| 547 |
| Saves as pickle file of dictionary of eval metrics
|
| 548 |
+
gene_balance : None, bool
|
| 549 |
+
| Whether to automatically balance genes in training set.
|
| 550 |
+
| Only available for binary gene classifications.
|
| 551 |
+
|
| 552 |
**Output**
|
| 553 |
|
| 554 |
Returns trainer after fine-tuning with all data.
|
| 555 |
|
| 556 |
"""
|
| 557 |
|
| 558 |
+
if (gene_balance is True) and (len(self.gene_class_dict.values())!=2):
|
| 559 |
+
logger.error("Automatically balancing gene sets for training is only available for binary gene classifications.")
|
| 560 |
+
raise
|
| 561 |
+
|
| 562 |
##### Load data and prepare output directory #####
|
| 563 |
# load numerical id to class dictionary (id:class)
|
| 564 |
with open(id_class_dict_file, "rb") as f:
|
|
|
|
| 590 |
)
|
| 591 |
assert len(targets) == len(labels)
|
| 592 |
data = cu.prep_gene_classifier_all_data(
|
| 593 |
+
data, targets, labels, self.max_ncells, self.nproc, gene_balance
|
| 594 |
)
|
| 595 |
|
| 596 |
trainer = self.train_classifier(
|
|
|
|
| 609 |
split_id_dict=None,
|
| 610 |
attr_to_split=None,
|
| 611 |
attr_to_balance=None,
|
| 612 |
+
gene_balance=False,
|
| 613 |
max_trials=100,
|
| 614 |
pval_threshold=0.1,
|
| 615 |
save_eval_output=True,
|
| 616 |
predict_eval=True,
|
| 617 |
predict_trainer=False,
|
| 618 |
n_hyperopt_trials=0,
|
| 619 |
+
save_gene_split_datasets=True,
|
| 620 |
+
debug_gene_split_datasets=False,
|
| 621 |
):
|
| 622 |
"""
|
| 623 |
(Cross-)validate cell state or gene classifier.
|
|
|
|
| 652 |
attr_to_balance : None, list
|
| 653 |
| List of attribute keys on which to balance data while splitting on attr_to_split
|
| 654 |
| e.g. ["age", "sex"] for balancing these characteristics while splitting by patient
|
| 655 |
+
gene_balance : None, bool
|
| 656 |
+
| Whether to automatically balance genes in training set.
|
| 657 |
+
| Only available for binary gene classifications.
|
| 658 |
max_trials : None, int
|
| 659 |
| Maximum number of trials of random splitting to try to achieve balanced other attribute
|
| 660 |
| If no split is found without significant (p < pval_threshold) differences in other attributes, will select best
|
|
|
|
| 673 |
n_hyperopt_trials : int
|
| 674 |
| Number of trials to run for hyperparameter optimization
|
| 675 |
| If 0, will not optimize hyperparameters
|
| 676 |
+
save_gene_split_datasets : bool
|
| 677 |
+
| Whether or not to save train, valid, and test gene-labeled datasets
|
| 678 |
"""
|
| 679 |
if self.num_crossval_splits == 0:
|
| 680 |
logger.error("num_crossval_splits must be 1 or 5 to validate.")
|
| 681 |
raise
|
| 682 |
+
|
| 683 |
+
if (gene_balance is True) and (len(self.gene_class_dict.values())!=2):
|
| 684 |
+
logger.error("Automatically balancing gene sets for training is only available for binary gene classifications.")
|
| 685 |
+
raise
|
| 686 |
+
|
| 687 |
# ensure number of genes in each class is > 5 if validating model
|
| 688 |
if self.classifier == "gene":
|
| 689 |
insuff_classes = [k for k, v in self.gene_class_dict.items() if len(v) < 5]
|
|
|
|
| 764 |
else:
|
| 765 |
# 5-fold cross-validate
|
| 766 |
num_cells = len(data)
|
| 767 |
+
fifth_cells = int(np.floor(num_cells * 0.2))
|
| 768 |
num_eval = min((self.eval_size * num_cells), fifth_cells)
|
| 769 |
start = i * fifth_cells
|
| 770 |
end = start + num_eval
|
|
|
|
| 843 |
self.max_ncells,
|
| 844 |
iteration_num,
|
| 845 |
self.nproc,
|
| 846 |
+
gene_balance,
|
| 847 |
)
|
| 848 |
+
|
| 849 |
+
if save_gene_split_datasets is True:
|
| 850 |
+
for split_name in ["train", "valid"]:
|
| 851 |
+
labeled_dataset_output_path = (
|
| 852 |
+
Path(output_dir) / f"{output_prefix}_{split_name}_gene_labeled_ksplit{iteration_num}"
|
| 853 |
+
).with_suffix(".dataset")
|
| 854 |
+
if split_name == "train":
|
| 855 |
+
train_data.save_to_disk(str(labeled_dataset_output_path))
|
| 856 |
+
elif split_name == "valid":
|
| 857 |
+
eval_data.save_to_disk(str(labeled_dataset_output_path))
|
| 858 |
+
|
| 859 |
if self.oos_test_size > 0:
|
| 860 |
test_data = cu.prep_gene_classifier_split(
|
| 861 |
data,
|
|
|
|
| 867 |
iteration_num,
|
| 868 |
self.nproc,
|
| 869 |
)
|
| 870 |
+
if save_gene_split_datasets is True:
|
| 871 |
+
test_labeled_dataset_output_path = (
|
| 872 |
+
Path(output_dir) / f"{output_prefix}_test_gene_labeled_ksplit{iteration_num}"
|
| 873 |
+
).with_suffix(".dataset")
|
| 874 |
+
test_data.save_to_disk(str(test_labeled_dataset_output_path))
|
| 875 |
+
if debug_gene_split_datasets is True:
|
| 876 |
+
logger.error("Exiting after saving gene split datasets given debug_gene_split_datasets = True.")
|
| 877 |
+
raise
|
| 878 |
if n_hyperopt_trials == 0:
|
| 879 |
trainer = self.train_classifier(
|
| 880 |
model_directory,
|
|
|
|
| 1023 |
subprocess.call(f"mkdir {output_directory}", shell=True)
|
| 1024 |
|
| 1025 |
##### Load model and training args #####
|
| 1026 |
+
model = pu.load_model(self.model_type, num_classes, model_directory, "train", quantize=self.quantize)
|
| 1027 |
def_training_args, def_freeze_layers = cu.get_default_train_args(
|
| 1028 |
model, self.classifier, train_data, output_directory
|
| 1029 |
)
|
|
|
|
| 1047 |
##### Fine-tune the model #####
|
| 1048 |
# define the data collator
|
| 1049 |
if self.classifier == "cell":
|
| 1050 |
+
data_collator = DataCollatorForCellClassification(token_dictionary=self.token_dictionary)
|
| 1051 |
elif self.classifier == "gene":
|
| 1052 |
+
data_collator = DataCollatorForGeneClassification(token_dictionary=self.token_dictionary)
|
| 1053 |
|
| 1054 |
# define function to initiate model
|
| 1055 |
def model_init():
|
| 1056 |
model = pu.load_model(
|
| 1057 |
+
self.model_type, num_classes, model_directory, "train", quantize=self.quantize
|
| 1058 |
)
|
| 1059 |
|
| 1060 |
if self.freeze_layers is not None:
|
|
|
|
| 1066 |
for param in module.parameters():
|
| 1067 |
param.requires_grad = False
|
| 1068 |
|
| 1069 |
+
if self.quantize is False:
|
| 1070 |
+
model = model.to("cuda:0")
|
| 1071 |
return model
|
| 1072 |
|
| 1073 |
# create the trainer
|
|
|
|
| 1180 |
subprocess.call(f"mkdir {output_directory}", shell=True)
|
| 1181 |
|
| 1182 |
##### Load model and training args #####
|
| 1183 |
+
model = pu.load_model(self.model_type, num_classes, model_directory, "train", quantize=self.quantize)
|
| 1184 |
|
| 1185 |
def_training_args, def_freeze_layers = cu.get_default_train_args(
|
| 1186 |
model, self.classifier, train_data, output_directory
|
|
|
|
| 1210 |
##### Fine-tune the model #####
|
| 1211 |
# define the data collator
|
| 1212 |
if self.classifier == "cell":
|
| 1213 |
+
data_collator = DataCollatorForCellClassification(token_dictionary=self.token_dictionary)
|
| 1214 |
elif self.classifier == "gene":
|
| 1215 |
+
data_collator = DataCollatorForGeneClassification(token_dictionary=self.token_dictionary)
|
| 1216 |
|
| 1217 |
# create the trainer
|
| 1218 |
trainer = Trainer(
|
|
|
|
| 1334 |
test_data = pu.load_and_filter(None, self.nproc, test_data_file)
|
| 1335 |
|
| 1336 |
# load previously fine-tuned model
|
| 1337 |
+
model = pu.load_model(self.model_type, num_classes, model_directory, "eval", quantize=self.quantize)
|
| 1338 |
|
| 1339 |
# evaluate the model
|
| 1340 |
result = self.evaluate_model(
|
geneformer/classifier_utils.py
CHANGED
|
@@ -137,21 +137,22 @@ def label_gene_classes(example, class_id_dict, gene_class_dict):
|
|
| 137 |
|
| 138 |
|
| 139 |
def prep_gene_classifier_train_eval_split(
|
| 140 |
-
data, targets, labels, train_index, eval_index, max_ncells, iteration_num, num_proc
|
| 141 |
):
|
| 142 |
# generate cross-validation splits
|
| 143 |
train_data = prep_gene_classifier_split(
|
| 144 |
-
data, targets, labels, train_index, "train", max_ncells, iteration_num, num_proc
|
| 145 |
)
|
| 146 |
eval_data = prep_gene_classifier_split(
|
| 147 |
-
data, targets, labels, eval_index, "eval", max_ncells, iteration_num, num_proc
|
| 148 |
)
|
| 149 |
return train_data, eval_data
|
| 150 |
|
| 151 |
|
| 152 |
def prep_gene_classifier_split(
|
| 153 |
-
data, targets, labels, index, subset_name, max_ncells, iteration_num, num_proc
|
| 154 |
):
|
|
|
|
| 155 |
# generate cross-validation splits
|
| 156 |
targets = np.array(targets)
|
| 157 |
labels = np.array(labels)
|
|
@@ -172,6 +173,10 @@ def prep_gene_classifier_split(
|
|
| 172 |
f"Filtered {round((1-len(subset_data)/len(data))*100)}%; {len(subset_data)} remain\n"
|
| 173 |
)
|
| 174 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
# subsample to max_ncells
|
| 176 |
subset_data = downsample_and_shuffle(subset_data, max_ncells, None, None)
|
| 177 |
|
|
@@ -187,7 +192,7 @@ def prep_gene_classifier_split(
|
|
| 187 |
return subset_data
|
| 188 |
|
| 189 |
|
| 190 |
-
def prep_gene_classifier_all_data(data, targets, labels, max_ncells, num_proc):
|
| 191 |
targets = np.array(targets)
|
| 192 |
labels = np.array(labels)
|
| 193 |
label_dict_train = dict(zip(targets, labels))
|
|
@@ -205,6 +210,9 @@ def prep_gene_classifier_all_data(data, targets, labels, max_ncells, num_proc):
|
|
| 205 |
f"Filtered {round((1-len(train_data)/len(data))*100)}%; {len(train_data)} remain\n"
|
| 206 |
)
|
| 207 |
|
|
|
|
|
|
|
|
|
|
| 208 |
# subsample to max_ncells
|
| 209 |
train_data = downsample_and_shuffle(train_data, max_ncells, None, None)
|
| 210 |
|
|
@@ -220,6 +228,110 @@ def prep_gene_classifier_all_data(data, targets, labels, max_ncells, num_proc):
|
|
| 220 |
return train_data
|
| 221 |
|
| 222 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
def balance_attr_splits(
|
| 224 |
data,
|
| 225 |
attr_to_split,
|
|
|
|
| 137 |
|
| 138 |
|
| 139 |
def prep_gene_classifier_train_eval_split(
|
| 140 |
+
data, targets, labels, train_index, eval_index, max_ncells, iteration_num, num_proc, balance=False
|
| 141 |
):
|
| 142 |
# generate cross-validation splits
|
| 143 |
train_data = prep_gene_classifier_split(
|
| 144 |
+
data, targets, labels, train_index, "train", max_ncells, iteration_num, num_proc, balance
|
| 145 |
)
|
| 146 |
eval_data = prep_gene_classifier_split(
|
| 147 |
+
data, targets, labels, eval_index, "eval", max_ncells, iteration_num, num_proc, balance
|
| 148 |
)
|
| 149 |
return train_data, eval_data
|
| 150 |
|
| 151 |
|
| 152 |
def prep_gene_classifier_split(
|
| 153 |
+
data, targets, labels, index, subset_name, max_ncells, iteration_num, num_proc, balance=False
|
| 154 |
):
|
| 155 |
+
|
| 156 |
# generate cross-validation splits
|
| 157 |
targets = np.array(targets)
|
| 158 |
labels = np.array(labels)
|
|
|
|
| 173 |
f"Filtered {round((1-len(subset_data)/len(data))*100)}%; {len(subset_data)} remain\n"
|
| 174 |
)
|
| 175 |
|
| 176 |
+
# balance gene subsets if train
|
| 177 |
+
if (subset_name == "train") and (balance is True):
|
| 178 |
+
subset_data, label_dict_subset = balance_gene_split(subset_data, label_dict_subset, num_proc)
|
| 179 |
+
|
| 180 |
# subsample to max_ncells
|
| 181 |
subset_data = downsample_and_shuffle(subset_data, max_ncells, None, None)
|
| 182 |
|
|
|
|
| 192 |
return subset_data
|
| 193 |
|
| 194 |
|
| 195 |
+
def prep_gene_classifier_all_data(data, targets, labels, max_ncells, num_proc, balance=False):
|
| 196 |
targets = np.array(targets)
|
| 197 |
labels = np.array(labels)
|
| 198 |
label_dict_train = dict(zip(targets, labels))
|
|
|
|
| 210 |
f"Filtered {round((1-len(train_data)/len(data))*100)}%; {len(train_data)} remain\n"
|
| 211 |
)
|
| 212 |
|
| 213 |
+
if balance is True:
|
| 214 |
+
train_data, label_dict_train = balance_gene_split(train_data, label_dict_train, num_proc)
|
| 215 |
+
|
| 216 |
# subsample to max_ncells
|
| 217 |
train_data = downsample_and_shuffle(train_data, max_ncells, None, None)
|
| 218 |
|
|
|
|
| 228 |
return train_data
|
| 229 |
|
| 230 |
|
| 231 |
+
def balance_gene_split(subset_data, label_dict_subset, num_proc):
|
| 232 |
+
# count occurrence of genes in each label category
|
| 233 |
+
label0_counts, label1_counts = count_genes_for_balancing(subset_data, label_dict_subset, num_proc)
|
| 234 |
+
label_ratio_0to1 = label0_counts/label1_counts
|
| 235 |
+
|
| 236 |
+
if 8/10 <= label_ratio_0to1 <= 10/8:
|
| 237 |
+
# gene sets already balanced
|
| 238 |
+
logger.info(
|
| 239 |
+
"Gene sets were already balanced within 0.8-1.25 fold and did not require balancing.\n"
|
| 240 |
+
)
|
| 241 |
+
return subset_data, label_dict_subset
|
| 242 |
+
else:
|
| 243 |
+
label_ratio_0to1_orig = label_ratio_0to1+0
|
| 244 |
+
label_dict_subset_orig = label_dict_subset.copy()
|
| 245 |
+
# balance gene sets
|
| 246 |
+
max_ntrials = 25
|
| 247 |
+
boost = 1
|
| 248 |
+
if label_ratio_0to1 > 10/8:
|
| 249 |
+
# downsample label 0
|
| 250 |
+
for i in range(max_ntrials):
|
| 251 |
+
label0 = 0
|
| 252 |
+
label0_genes = [k for k,v in label_dict_subset.items() if v == label0]
|
| 253 |
+
label0_ngenes = len(label0_genes)
|
| 254 |
+
label0_nremove = max(1,int(np.floor(label0_ngenes - label0_ngenes/(label_ratio_0to1*boost))))
|
| 255 |
+
random.seed(i)
|
| 256 |
+
label0_remove_genes = random.sample(label0_genes, label0_nremove)
|
| 257 |
+
label_dict_subset_new = {k:v for k,v in label_dict_subset.items() if k not in label0_remove_genes}
|
| 258 |
+
label0_counts, label1_counts = count_genes_for_balancing(subset_data, label_dict_subset_new, num_proc)
|
| 259 |
+
label_ratio_0to1 = label0_counts/label1_counts
|
| 260 |
+
if 8/10 <= label_ratio_0to1 <= 10/8:
|
| 261 |
+
# if gene sets now balanced, return new filtered data and new label_dict_subset
|
| 262 |
+
return filter_data_balanced_genes(subset_data, label_dict_subset_new, num_proc)
|
| 263 |
+
elif label_ratio_0to1 > 10/8:
|
| 264 |
+
boost = boost*1.1
|
| 265 |
+
elif label_ratio_0to1 < 8/10:
|
| 266 |
+
boost = boost*0.9
|
| 267 |
+
else:
|
| 268 |
+
# downsample label 1
|
| 269 |
+
for i in range(max_ntrials):
|
| 270 |
+
label1 = 1
|
| 271 |
+
label1_genes = [k for k,v in label_dict_subset.items() if v == label1]
|
| 272 |
+
label1_ngenes = len(label1_genes)
|
| 273 |
+
label1_nremove = max(1,int(np.floor(label1_ngenes - label1_ngenes/((1/label_ratio_0to1)*boost))))
|
| 274 |
+
random.seed(i)
|
| 275 |
+
label1_remove_genes = random.sample(label1_genes, label1_nremove)
|
| 276 |
+
label_dict_subset_new = {k:v for k,v in label_dict_subset.items() if k not in label1_remove_genes}
|
| 277 |
+
label0_counts, label1_counts = count_genes_for_balancing(subset_data, label_dict_subset_new, num_proc)
|
| 278 |
+
label_ratio_0to1 = label0_counts/label1_counts
|
| 279 |
+
if 8/10 <= label_ratio_0to1 <= 10/8:
|
| 280 |
+
# if gene sets now balanced, return new filtered data and new label_dict_subset
|
| 281 |
+
return filter_data_balanced_genes(subset_data, label_dict_subset_new, num_proc)
|
| 282 |
+
elif label_ratio_0to1 < 8/10:
|
| 283 |
+
boost = boost*1.1
|
| 284 |
+
elif label_ratio_0to1 > 10/8:
|
| 285 |
+
boost = boost*0.9
|
| 286 |
+
|
| 287 |
+
assert i+1 == max_ntrials
|
| 288 |
+
if (label_ratio_0to1 <= label_ratio_0to1_orig < 8/10) or (10/8 > label_ratio_0to1_orig >= label_ratio_0to1):
|
| 289 |
+
label_ratio_0to1 = label_ratio_0to1_orig
|
| 290 |
+
label_dict_subset_new = label_dict_subset_orig
|
| 291 |
+
logger.warning(
|
| 292 |
+
f"Gene sets were not able to be balanced within 0.8-1.25 fold after {max_ntrials} trials. Imbalance level: {label_ratio_0to1}\n"
|
| 293 |
+
)
|
| 294 |
+
return filter_data_balanced_genes(subset_data, label_dict_subset_new, num_proc)
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
def count_genes_for_balancing(subset_data, label_dict_subset, num_proc):
|
| 298 |
+
def count_targets(example):
|
| 299 |
+
labels = [
|
| 300 |
+
label_dict_subset.get(token_id, -100) for token_id in example["input_ids"]
|
| 301 |
+
]
|
| 302 |
+
counter_labels = Counter(labels)
|
| 303 |
+
# get count of labels 0 or 1, or if absent, return 0
|
| 304 |
+
example["labels_counts"] = [counter_labels.get(0,0),counter_labels.get(1,0)]
|
| 305 |
+
return example
|
| 306 |
+
|
| 307 |
+
subset_data = subset_data.map(count_targets, num_proc=num_proc)
|
| 308 |
+
|
| 309 |
+
label0_counts = sum([counts[0] for counts in subset_data["labels_counts"]])
|
| 310 |
+
label1_counts = sum([counts[1] for counts in subset_data["labels_counts"]])
|
| 311 |
+
|
| 312 |
+
subset_data = subset_data.remove_columns("labels_counts")
|
| 313 |
+
|
| 314 |
+
return label0_counts, label1_counts
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def filter_data_balanced_genes(subset_data, label_dict_subset, num_proc):
|
| 318 |
+
# function to filter by whether contains labels
|
| 319 |
+
def if_contains_subset_label(example):
|
| 320 |
+
a = list(label_dict_subset.keys())
|
| 321 |
+
b = example["input_ids"]
|
| 322 |
+
return not set(a).isdisjoint(b)
|
| 323 |
+
|
| 324 |
+
# filter dataset for examples containing classes for this split
|
| 325 |
+
logger.info("Filtering data for balanced genes")
|
| 326 |
+
subset_data_len_orig = len(subset_data)
|
| 327 |
+
subset_data = subset_data.filter(if_contains_subset_label, num_proc=num_proc)
|
| 328 |
+
logger.info(
|
| 329 |
+
f"Filtered {round((1-len(subset_data)/subset_data_len_orig)*100)}%; {len(subset_data)} remain\n"
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
return subset_data, label_dict_subset
|
| 333 |
+
|
| 334 |
+
|
| 335 |
def balance_attr_splits(
|
| 336 |
data,
|
| 337 |
attr_to_split,
|
geneformer/collator_for_classification.py
CHANGED
|
@@ -18,12 +18,6 @@ from transformers import (
|
|
| 18 |
from transformers.utils import is_tf_available, is_torch_available, logging, to_py_obj
|
| 19 |
from transformers.utils.generic import _is_tensorflow, _is_torch
|
| 20 |
|
| 21 |
-
from . import TOKEN_DICTIONARY_FILE
|
| 22 |
-
|
| 23 |
-
# load token dictionary (Ensembl IDs:token)
|
| 24 |
-
with open(TOKEN_DICTIONARY_FILE, "rb") as f:
|
| 25 |
-
token_dictionary = pickle.load(f)
|
| 26 |
-
|
| 27 |
EncodedInput = List[int]
|
| 28 |
logger = logging.get_logger(__name__)
|
| 29 |
VERY_LARGE_INTEGER = int(
|
|
@@ -85,16 +79,18 @@ class TensorType(ExplicitEnum):
|
|
| 85 |
|
| 86 |
|
| 87 |
class PrecollatorForGeneAndCellClassification(SpecialTokensMixin):
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
token_dictionary.get("<mask>")
|
| 95 |
-
token_dictionary.get("<pad>")
|
| 96 |
-
|
| 97 |
-
|
|
|
|
|
|
|
| 98 |
|
| 99 |
def _get_padding_truncation_strategies(
|
| 100 |
self, padding=True, truncation=False, max_length=None, pad_to_multiple_of=None, verbose=True, **kwargs
|
|
@@ -550,8 +546,7 @@ class DataCollatorForGeneClassification(DataCollatorForTokenClassification):
|
|
| 550 |
label_pad_token_id (:obj:`int`, `optional`, defaults to -100):
|
| 551 |
The id to use when padding the labels (-100 will be automatically ignore by PyTorch loss functions).
|
| 552 |
"""
|
| 553 |
-
|
| 554 |
-
tokenizer = PrecollatorForGeneAndCellClassification()
|
| 555 |
class_type = "gene"
|
| 556 |
padding: Union[bool, str, PaddingStrategy] = True
|
| 557 |
max_length: Optional[int] = None
|
|
@@ -559,8 +554,9 @@ class DataCollatorForGeneClassification(DataCollatorForTokenClassification):
|
|
| 559 |
label_pad_token_id: int = -100
|
| 560 |
|
| 561 |
def __init__(self, *args, **kwargs) -> None:
|
|
|
|
| 562 |
super().__init__(
|
| 563 |
-
tokenizer=self.
|
| 564 |
padding=self.padding,
|
| 565 |
max_length=self.max_length,
|
| 566 |
pad_to_multiple_of=self.pad_to_multiple_of,
|
|
|
|
| 18 |
from transformers.utils import is_tf_available, is_torch_available, logging, to_py_obj
|
| 19 |
from transformers.utils.generic import _is_tensorflow, _is_torch
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
EncodedInput = List[int]
|
| 22 |
logger = logging.get_logger(__name__)
|
| 23 |
VERY_LARGE_INTEGER = int(
|
|
|
|
| 79 |
|
| 80 |
|
| 81 |
class PrecollatorForGeneAndCellClassification(SpecialTokensMixin):
|
| 82 |
+
def __init__(self, *args, **kwargs) -> None:
|
| 83 |
+
super().__init__(mask_token="<mask>", pad_token="<pad>")
|
| 84 |
+
|
| 85 |
+
self.token_dictionary = kwargs.get("token_dictionary")
|
| 86 |
+
self.padding_side = "right"
|
| 87 |
+
self.model_input_names = ["input_ids"]
|
| 88 |
+
self.mask_token_id = self.token_dictionary.get("<mask>")
|
| 89 |
+
self.pad_token_id = self.token_dictionary.get("<pad>")
|
| 90 |
+
self.all_special_ids = [
|
| 91 |
+
self.token_dictionary.get("<mask>"),
|
| 92 |
+
self.token_dictionary.get("<pad>")
|
| 93 |
+
]
|
| 94 |
|
| 95 |
def _get_padding_truncation_strategies(
|
| 96 |
self, padding=True, truncation=False, max_length=None, pad_to_multiple_of=None, verbose=True, **kwargs
|
|
|
|
| 546 |
label_pad_token_id (:obj:`int`, `optional`, defaults to -100):
|
| 547 |
The id to use when padding the labels (-100 will be automatically ignore by PyTorch loss functions).
|
| 548 |
"""
|
| 549 |
+
|
|
|
|
| 550 |
class_type = "gene"
|
| 551 |
padding: Union[bool, str, PaddingStrategy] = True
|
| 552 |
max_length: Optional[int] = None
|
|
|
|
| 554 |
label_pad_token_id: int = -100
|
| 555 |
|
| 556 |
def __init__(self, *args, **kwargs) -> None:
|
| 557 |
+
self.token_dictionary = kwargs.pop("token_dictionary")
|
| 558 |
super().__init__(
|
| 559 |
+
tokenizer=PrecollatorForGeneAndCellClassification(token_dictionary=self.token_dictionary),
|
| 560 |
padding=self.padding,
|
| 561 |
max_length=self.max_length,
|
| 562 |
pad_to_multiple_of=self.pad_to_multiple_of,
|
geneformer/emb_extractor.py
CHANGED
|
@@ -286,12 +286,20 @@ def plot_umap(embs_df, emb_dims, label, output_file, kwargs_dict, seed=0):
|
|
| 286 |
sc.tl.umap(adata, random_state=seed)
|
| 287 |
sns.set(rc={"figure.figsize": (10, 10)}, font_scale=2.3)
|
| 288 |
sns.set_style("white")
|
| 289 |
-
default_kwargs_dict = {"
|
| 290 |
if kwargs_dict is not None:
|
| 291 |
default_kwargs_dict.update(kwargs_dict)
|
| 292 |
|
| 293 |
-
|
| 294 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
plt.savefig(output_file, bbox_inches="tight")
|
| 296 |
|
| 297 |
|
|
@@ -470,7 +478,6 @@ class EmbExtractor:
|
|
| 470 |
... emb_mode="cell",
|
| 471 |
... filter_data={"cell_type":["cardiomyocyte"]},
|
| 472 |
... max_ncells=1000,
|
| 473 |
-
... max_ncells_to_plot=1000,
|
| 474 |
... emb_layer=-1,
|
| 475 |
... emb_label=["disease", "cell_type"],
|
| 476 |
... labels_to_plot=["disease", "cell_type"])
|
|
@@ -783,15 +790,15 @@ class EmbExtractor:
|
|
| 783 |
logger.error("Plotting UMAP requires 'labels_to_plot'. ")
|
| 784 |
raise
|
| 785 |
|
| 786 |
-
if max_ncells_to_plot
|
| 787 |
-
max_ncells_to_plot
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
|
| 796 |
if self.emb_label is None:
|
| 797 |
label_len = 0
|
|
|
|
| 286 |
sc.tl.umap(adata, random_state=seed)
|
| 287 |
sns.set(rc={"figure.figsize": (10, 10)}, font_scale=2.3)
|
| 288 |
sns.set_style("white")
|
| 289 |
+
default_kwargs_dict = {"size": 200}
|
| 290 |
if kwargs_dict is not None:
|
| 291 |
default_kwargs_dict.update(kwargs_dict)
|
| 292 |
|
| 293 |
+
cats = set(embs_df[label])
|
| 294 |
+
|
| 295 |
+
with plt.rc_context():
|
| 296 |
+
ax = sc.pl.umap(adata, color=label, show=False, **default_kwargs_dict)
|
| 297 |
+
ax.legend(markerscale=2,
|
| 298 |
+
frameon=False,
|
| 299 |
+
loc="center left",
|
| 300 |
+
bbox_to_anchor=(1, 0.5),
|
| 301 |
+
ncol=(1 if len(cats) <= 14 else 2 if len(cats) <= 30 else 3))
|
| 302 |
+
plt.show()
|
| 303 |
plt.savefig(output_file, bbox_inches="tight")
|
| 304 |
|
| 305 |
|
|
|
|
| 478 |
... emb_mode="cell",
|
| 479 |
... filter_data={"cell_type":["cardiomyocyte"]},
|
| 480 |
... max_ncells=1000,
|
|
|
|
| 481 |
... emb_layer=-1,
|
| 482 |
... emb_label=["disease", "cell_type"],
|
| 483 |
... labels_to_plot=["disease", "cell_type"])
|
|
|
|
| 790 |
logger.error("Plotting UMAP requires 'labels_to_plot'. ")
|
| 791 |
raise
|
| 792 |
|
| 793 |
+
if max_ncells_to_plot is not None:
|
| 794 |
+
if max_ncells_to_plot > self.max_ncells:
|
| 795 |
+
max_ncells_to_plot = self.max_ncells
|
| 796 |
+
logger.warning(
|
| 797 |
+
"max_ncells_to_plot must be <= max_ncells. "
|
| 798 |
+
f"Changing max_ncells_to_plot to {self.max_ncells}."
|
| 799 |
+
)
|
| 800 |
+
elif max_ncells_to_plot < self.max_ncells:
|
| 801 |
+
embs = embs.sample(max_ncells_to_plot, axis=0)
|
| 802 |
|
| 803 |
if self.emb_label is None:
|
| 804 |
label_len = 0
|
geneformer/gene_dictionaries_30m/gene_median_dictionary_gc30M.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b3b589bb5ec75040d05fc44dd6bf0184cf87f3c362cf158d196a6ed3b7fe5f39
|
| 3 |
+
size 940965
|
geneformer/{gene_name_id_dict.pkl β gene_dictionaries_30m/gene_name_id_dict_gc30M.pkl}
RENAMED
|
File without changes
|
geneformer/gene_dictionaries_30m/token_dictionary_gc30M.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ab9dc40973fa5224d77b793e2fd114cacf3d08423ed9c4c49caf0ba9c7f218f1
|
| 3 |
+
size 788424
|
geneformer/gene_median_dictionary.pkl
DELETED
|
Binary file (941 kB)
|
|
|
geneformer/in_silico_perturber.py
CHANGED
|
@@ -63,7 +63,7 @@ class InSilicoPerturber:
|
|
| 63 |
"anchor_gene": {None, str},
|
| 64 |
"model_type": {"Pretrained", "GeneClassifier", "CellClassifier"},
|
| 65 |
"num_classes": {int},
|
| 66 |
-
"emb_mode": {"cell", "cell_and_gene"},
|
| 67 |
"cell_emb_style": {"mean_pool"},
|
| 68 |
"filter_data": {None, dict},
|
| 69 |
"cell_states_to_model": {None, dict},
|
|
@@ -71,6 +71,7 @@ class InSilicoPerturber:
|
|
| 71 |
"max_ncells": {None, int},
|
| 72 |
"cell_inds_to_perturb": {"all", dict},
|
| 73 |
"emb_layer": {-1, 0},
|
|
|
|
| 74 |
"forward_batch_size": {int},
|
| 75 |
"nproc": {int},
|
| 76 |
}
|
|
@@ -94,7 +95,8 @@ class InSilicoPerturber:
|
|
| 94 |
emb_layer=-1,
|
| 95 |
forward_batch_size=100,
|
| 96 |
nproc=4,
|
| 97 |
-
token_dictionary_file=
|
|
|
|
| 98 |
):
|
| 99 |
"""
|
| 100 |
Initialize in silico perturber.
|
|
@@ -129,16 +131,16 @@ class InSilicoPerturber:
|
|
| 129 |
| ENSEMBL ID of gene to use as anchor in combination perturbations.
|
| 130 |
| For example, if combos=1 and anchor_gene="ENSG00000148400":
|
| 131 |
| anchor gene will be perturbed in combination with each other gene.
|
| 132 |
-
model_type : {"Pretrained", "GeneClassifier", "CellClassifier"}
|
| 133 |
-
| Whether model is the pretrained Geneformer or a fine-tuned gene or cell classifier.
|
| 134 |
num_classes : int
|
| 135 |
| If model is a gene or cell classifier, specify number of classes it was trained to classify.
|
| 136 |
| For the pretrained Geneformer model, number of classes is 0 as it is not a classifier.
|
| 137 |
-
emb_mode : {"cell", "cell_and_gene"}
|
| 138 |
-
| Whether to output impact of perturbation on cell and/or gene embeddings.
|
| 139 |
| Gene embedding shifts only available as compared to original cell, not comparing to goal state.
|
| 140 |
cell_emb_style : "mean_pool"
|
| 141 |
-
| Method for summarizing cell embeddings.
|
| 142 |
| Currently only option is mean pooling of gene embeddings for given cell.
|
| 143 |
filter_data : None, dict
|
| 144 |
| Default is to use all input data for in silico perturbation study.
|
|
@@ -183,6 +185,8 @@ class InSilicoPerturber:
|
|
| 183 |
| Number of CPU processes to use.
|
| 184 |
token_dictionary_file : Path
|
| 185 |
| Path to pickle file containing token dictionary (Ensembl ID:token).
|
|
|
|
|
|
|
| 186 |
"""
|
| 187 |
try:
|
| 188 |
set_start_method("spawn")
|
|
@@ -219,15 +223,31 @@ class InSilicoPerturber:
|
|
| 219 |
self.emb_layer = emb_layer
|
| 220 |
self.forward_batch_size = forward_batch_size
|
| 221 |
self.nproc = nproc
|
|
|
|
|
|
|
| 222 |
|
| 223 |
self.validate_options()
|
| 224 |
|
| 225 |
# load token dictionary (Ensembl IDs:token)
|
|
|
|
|
|
|
| 226 |
with open(token_dictionary_file, "rb") as f:
|
| 227 |
self.gene_token_dict = pickle.load(f)
|
| 228 |
self.token_gene_dict = {v: k for k, v in self.gene_token_dict.items()}
|
| 229 |
|
| 230 |
self.pad_token_id = self.gene_token_dict.get("<pad>")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
|
| 232 |
if self.anchor_gene is None:
|
| 233 |
self.anchor_token = None
|
|
@@ -285,7 +305,7 @@ class InSilicoPerturber:
|
|
| 285 |
continue
|
| 286 |
valid_type = False
|
| 287 |
for option in valid_options:
|
| 288 |
-
if (option in [bool, int, list, dict]) and isinstance(
|
| 289 |
attr_value, option
|
| 290 |
):
|
| 291 |
valid_type = True
|
|
@@ -426,22 +446,46 @@ class InSilicoPerturber:
|
|
| 426 |
self.max_len = pu.get_model_input_size(model)
|
| 427 |
layer_to_quant = pu.quant_layers(model) + self.emb_layer
|
| 428 |
|
| 429 |
-
|
| 430 |
### filter input data ###
|
| 431 |
# general filtering of input data based on filter_data argument
|
| 432 |
filtered_input_data = pu.load_and_filter(
|
| 433 |
self.filter_data, self.nproc, input_data_file
|
| 434 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
filtered_input_data = self.apply_additional_filters(filtered_input_data)
|
| 436 |
|
| 437 |
if self.perturb_group is True:
|
| 438 |
-
self.
|
| 439 |
-
|
| 440 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
else:
|
| 442 |
-
self.
|
| 443 |
-
|
| 444 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 445 |
|
| 446 |
def apply_additional_filters(self, filtered_input_data):
|
| 447 |
# additional filtering of input data dependent on isp mode
|
|
@@ -486,6 +530,7 @@ class InSilicoPerturber:
|
|
| 486 |
layer_to_quant: int,
|
| 487 |
output_path_prefix: str,
|
| 488 |
):
|
|
|
|
| 489 |
def make_group_perturbation_batch(example):
|
| 490 |
example_input_ids = example["input_ids"]
|
| 491 |
example["tokens_to_perturb"] = self.tokens_to_perturb
|
|
@@ -504,7 +549,7 @@ class InSilicoPerturber:
|
|
| 504 |
if self.perturb_type == "delete":
|
| 505 |
example = pu.delete_indices(example)
|
| 506 |
elif self.perturb_type == "overexpress":
|
| 507 |
-
example = pu.overexpress_tokens(example, self.max_len)
|
| 508 |
example["n_overflow"] = pu.calc_n_overflow(
|
| 509 |
self.max_len,
|
| 510 |
example["length"],
|
|
@@ -678,8 +723,6 @@ class InSilicoPerturber:
|
|
| 678 |
cos_sims_dict = self.update_perturbation_dictionary(
|
| 679 |
cos_sims_dict,
|
| 680 |
cos_sims_data,
|
| 681 |
-
filtered_input_data,
|
| 682 |
-
indices_to_perturb,
|
| 683 |
gene_list,
|
| 684 |
)
|
| 685 |
else:
|
|
@@ -688,8 +731,6 @@ class InSilicoPerturber:
|
|
| 688 |
cos_sims_dict[state] = self.update_perturbation_dictionary(
|
| 689 |
cos_sims_dict[state],
|
| 690 |
cos_sims_data[state],
|
| 691 |
-
filtered_input_data,
|
| 692 |
-
indices_to_perturb,
|
| 693 |
gene_list,
|
| 694 |
)
|
| 695 |
del minibatch
|
|
@@ -711,6 +752,256 @@ class InSilicoPerturber:
|
|
| 711 |
f"{output_path_prefix}_gene_embs_dict_{self.tokens_to_perturb}",
|
| 712 |
)
|
| 713 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 714 |
def isp_perturb_all(
|
| 715 |
self,
|
| 716 |
model,
|
|
@@ -729,8 +1020,10 @@ class InSilicoPerturber:
|
|
| 729 |
|
| 730 |
if self.emb_mode == "cell_and_gene":
|
| 731 |
stored_gene_embs_dict = defaultdict(list)
|
| 732 |
-
|
| 733 |
-
|
|
|
|
|
|
|
| 734 |
full_original_emb = get_embs(
|
| 735 |
model,
|
| 736 |
example_cell,
|
|
@@ -738,18 +1031,33 @@ class InSilicoPerturber:
|
|
| 738 |
layer_to_quant,
|
| 739 |
self.pad_token_id,
|
| 740 |
self.forward_batch_size,
|
| 741 |
-
|
| 742 |
summary_stat=None,
|
| 743 |
silent=True,
|
| 744 |
)
|
| 745 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 746 |
# gene_list is used to assign cos sims back to genes
|
| 747 |
-
# need to remove the anchor gene
|
| 748 |
gene_list = example_cell["input_ids"][0][:]
|
|
|
|
| 749 |
if self.anchor_token is not None:
|
| 750 |
for token in self.anchor_token:
|
| 751 |
gene_list.remove(token)
|
| 752 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 753 |
perturbation_batch, indices_to_perturb = pu.make_perturbation_batch(
|
| 754 |
example_cell,
|
| 755 |
self.perturb_type,
|
|
@@ -759,148 +1067,430 @@ class InSilicoPerturber:
|
|
| 759 |
self.nproc,
|
| 760 |
)
|
| 761 |
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
self.forward_batch_size,
|
| 769 |
-
token_gene_dict=self.token_gene_dict,
|
| 770 |
-
summary_stat=None,
|
| 771 |
-
silent=True,
|
| 772 |
-
)
|
| 773 |
-
|
| 774 |
-
num_inds_perturbed = 1 + self.combos
|
| 775 |
-
# need to remove overexpressed gene to quantify cosine shifts
|
| 776 |
-
if self.perturb_type == "overexpress":
|
| 777 |
-
perturbation_emb = full_perturbation_emb[:, num_inds_perturbed:, :]
|
| 778 |
-
gene_list = gene_list[
|
| 779 |
-
num_inds_perturbed:
|
| 780 |
-
] # index 0 is not overexpressed
|
| 781 |
-
|
| 782 |
-
elif self.perturb_type == "delete":
|
| 783 |
-
perturbation_emb = full_perturbation_emb
|
| 784 |
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
emb_mode="gene",
|
| 796 |
-
)
|
| 797 |
-
if self.cell_states_to_model is not None:
|
| 798 |
-
original_cell_emb = pu.compute_nonpadded_cell_embedding(
|
| 799 |
-
full_original_emb, "mean_pool"
|
| 800 |
-
)
|
| 801 |
-
perturbation_cell_emb = pu.compute_nonpadded_cell_embedding(
|
| 802 |
-
full_perturbation_emb, "mean_pool"
|
| 803 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 804 |
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 811 |
)
|
| 812 |
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
|
|
|
|
|
|
| 818 |
}
|
| 819 |
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
perturbed_gene_dict[perturbed_gene]
|
| 823 |
-
):
|
| 824 |
-
try:
|
| 825 |
-
stored_gene_embs_dict[
|
| 826 |
-
(perturbed_gene, affected_gene)
|
| 827 |
-
].append(gene_cos_sims[perturbation_i, gene_j].item())
|
| 828 |
-
except KeyError:
|
| 829 |
-
stored_gene_embs_dict[
|
| 830 |
-
(perturbed_gene, affected_gene)
|
| 831 |
-
] = gene_cos_sims[perturbation_i, gene_j].item()
|
| 832 |
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
|
|
|
|
|
|
|
|
|
| 852 |
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
pu.write_perturbation_dictionary(
|
| 856 |
-
cos_sims_dict,
|
| 857 |
-
f"{output_path_prefix}_dict_cell_embs_1Kbatch{pickle_batch}",
|
| 858 |
-
)
|
| 859 |
-
if self.emb_mode == "cell_and_gene":
|
| 860 |
-
pu.write_perturbation_dictionary(
|
| 861 |
-
stored_gene_embs_dict,
|
| 862 |
-
f"{output_path_prefix}_dict_gene_embs_1Kbatch{pickle_batch}",
|
| 863 |
-
)
|
| 864 |
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
if self.cell_states_to_model is None:
|
| 869 |
-
cos_sims_dict = defaultdict(list)
|
| 870 |
-
else:
|
| 871 |
-
cos_sims_dict = {
|
| 872 |
-
state: defaultdict(list)
|
| 873 |
-
for state in pu.get_possible_states(self.cell_states_to_model)
|
| 874 |
-
}
|
| 875 |
|
| 876 |
-
|
| 877 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 878 |
|
| 879 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 880 |
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 884 |
|
| 885 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 886 |
pu.write_perturbation_dictionary(
|
| 887 |
-
|
| 888 |
-
f"{output_path_prefix}_dict_gene_embs_1Kbatch{pickle_batch}",
|
| 889 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 890 |
|
|
|
|
| 891 |
def update_perturbation_dictionary(
|
| 892 |
self,
|
| 893 |
cos_sims_dict: defaultdict,
|
| 894 |
cos_sims_data: torch.Tensor,
|
| 895 |
-
filtered_input_data: Dataset,
|
| 896 |
-
indices_to_perturb: List[List[int]],
|
| 897 |
gene_list=None,
|
| 898 |
):
|
| 899 |
if gene_list is not None and cos_sims_data.shape[0] != len(gene_list):
|
| 900 |
logger.error(
|
| 901 |
f"len(cos_sims_data.shape[0]) != len(gene_list). \n \
|
| 902 |
-
cos_sims_data.shape[0]
|
| 903 |
-
len(gene_list)
|
| 904 |
)
|
| 905 |
raise
|
| 906 |
|
|
@@ -924,4 +1514,4 @@ class InSilicoPerturber:
|
|
| 924 |
for i, cos in enumerate(cos_sims_data.tolist()):
|
| 925 |
cos_sims_dict[(gene_list[i], "cell_emb")].append(cos)
|
| 926 |
|
| 927 |
-
return cos_sims_dict
|
|
|
|
| 63 |
"anchor_gene": {None, str},
|
| 64 |
"model_type": {"Pretrained", "GeneClassifier", "CellClassifier"},
|
| 65 |
"num_classes": {int},
|
| 66 |
+
"emb_mode": {"cls", "cell", "cls_and_gene", "cell_and_gene"},
|
| 67 |
"cell_emb_style": {"mean_pool"},
|
| 68 |
"filter_data": {None, dict},
|
| 69 |
"cell_states_to_model": {None, dict},
|
|
|
|
| 71 |
"max_ncells": {None, int},
|
| 72 |
"cell_inds_to_perturb": {"all", dict},
|
| 73 |
"emb_layer": {-1, 0},
|
| 74 |
+
"token_dictionary_file" : {None, str},
|
| 75 |
"forward_batch_size": {int},
|
| 76 |
"nproc": {int},
|
| 77 |
}
|
|
|
|
| 95 |
emb_layer=-1,
|
| 96 |
forward_batch_size=100,
|
| 97 |
nproc=4,
|
| 98 |
+
token_dictionary_file=None,
|
| 99 |
+
clear_mem_ncells=1000,
|
| 100 |
):
|
| 101 |
"""
|
| 102 |
Initialize in silico perturber.
|
|
|
|
| 131 |
| ENSEMBL ID of gene to use as anchor in combination perturbations.
|
| 132 |
| For example, if combos=1 and anchor_gene="ENSG00000148400":
|
| 133 |
| anchor gene will be perturbed in combination with each other gene.
|
| 134 |
+
model_type : {"Pretrained", "GeneClassifier", "CellClassifier", "MTLCellClassifier", "MTLCellClassifier-Quantized"}
|
| 135 |
+
| Whether model is the pretrained Geneformer or a fine-tuned gene, cell, or multitask cell classifier (+/- 8bit quantization).
|
| 136 |
num_classes : int
|
| 137 |
| If model is a gene or cell classifier, specify number of classes it was trained to classify.
|
| 138 |
| For the pretrained Geneformer model, number of classes is 0 as it is not a classifier.
|
| 139 |
+
emb_mode : {"cls", "cell", "cls_and_gene","cell_and_gene"}
|
| 140 |
+
| Whether to output impact of perturbation on CLS token, cell, and/or gene embeddings.
|
| 141 |
| Gene embedding shifts only available as compared to original cell, not comparing to goal state.
|
| 142 |
cell_emb_style : "mean_pool"
|
| 143 |
+
| Method for summarizing cell embeddings if not using CLS token.
|
| 144 |
| Currently only option is mean pooling of gene embeddings for given cell.
|
| 145 |
filter_data : None, dict
|
| 146 |
| Default is to use all input data for in silico perturbation study.
|
|
|
|
| 185 |
| Number of CPU processes to use.
|
| 186 |
token_dictionary_file : Path
|
| 187 |
| Path to pickle file containing token dictionary (Ensembl ID:token).
|
| 188 |
+
clear_mem_ncells : int
|
| 189 |
+
| Clear memory every n cells.
|
| 190 |
"""
|
| 191 |
try:
|
| 192 |
set_start_method("spawn")
|
|
|
|
| 223 |
self.emb_layer = emb_layer
|
| 224 |
self.forward_batch_size = forward_batch_size
|
| 225 |
self.nproc = nproc
|
| 226 |
+
self.token_dictionary_file = token_dictionary_file
|
| 227 |
+
self.clear_mem_ncells = clear_mem_ncells
|
| 228 |
|
| 229 |
self.validate_options()
|
| 230 |
|
| 231 |
# load token dictionary (Ensembl IDs:token)
|
| 232 |
+
if self.token_dictionary_file is None:
|
| 233 |
+
token_dictionary_file = TOKEN_DICTIONARY_FILE
|
| 234 |
with open(token_dictionary_file, "rb") as f:
|
| 235 |
self.gene_token_dict = pickle.load(f)
|
| 236 |
self.token_gene_dict = {v: k for k, v in self.gene_token_dict.items()}
|
| 237 |
|
| 238 |
self.pad_token_id = self.gene_token_dict.get("<pad>")
|
| 239 |
+
self.cls_token_id = self.gene_token_dict.get("<cls>")
|
| 240 |
+
self.eos_token_id = self.gene_token_dict.get("<eos>")
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
# Identify if special token is present in the token dictionary
|
| 244 |
+
if (self.cls_token_id is not None) and (self.eos_token_id is not None):
|
| 245 |
+
self.special_token = True
|
| 246 |
+
else:
|
| 247 |
+
if "cls" in self.emb_mode:
|
| 248 |
+
logger.error(f"emb_mode set to {self.emb_mode} but <cls> or <eos> token not in token dictionary.")
|
| 249 |
+
raise
|
| 250 |
+
self.special_token = False
|
| 251 |
|
| 252 |
if self.anchor_gene is None:
|
| 253 |
self.anchor_token = None
|
|
|
|
| 305 |
continue
|
| 306 |
valid_type = False
|
| 307 |
for option in valid_options:
|
| 308 |
+
if (option in [bool, int, list, dict, str]) and isinstance(
|
| 309 |
attr_value, option
|
| 310 |
):
|
| 311 |
valid_type = True
|
|
|
|
| 446 |
self.max_len = pu.get_model_input_size(model)
|
| 447 |
layer_to_quant = pu.quant_layers(model) + self.emb_layer
|
| 448 |
|
|
|
|
| 449 |
### filter input data ###
|
| 450 |
# general filtering of input data based on filter_data argument
|
| 451 |
filtered_input_data = pu.load_and_filter(
|
| 452 |
self.filter_data, self.nproc, input_data_file
|
| 453 |
)
|
| 454 |
+
|
| 455 |
+
# Ensure emb_mode is cls if first token of the filtered input data is cls token
|
| 456 |
+
if self.special_token:
|
| 457 |
+
if (filtered_input_data["input_ids"][0][0] == self.cls_token_id) and ("cls" not in self.emb_mode):
|
| 458 |
+
logger.error(
|
| 459 |
+
"Emb mode 'cls' or 'cls_and_gene' required when first token is <cls>."
|
| 460 |
+
)
|
| 461 |
+
raise
|
| 462 |
+
if ("cls" in self.emb_mode):
|
| 463 |
+
if (filtered_input_data["input_ids"][0][0] != self.cls_token_id) or (filtered_input_data["input_ids"][0][-1] != self.eos_token_id):
|
| 464 |
+
logger.error(
|
| 465 |
+
"Emb mode 'cls' and 'cls_and_gene' require that first token is <cls> and last token is <eos>."
|
| 466 |
+
)
|
| 467 |
+
raise
|
| 468 |
+
|
| 469 |
filtered_input_data = self.apply_additional_filters(filtered_input_data)
|
| 470 |
|
| 471 |
if self.perturb_group is True:
|
| 472 |
+
if (self.special_token) and ("cls" in self.emb_mode):
|
| 473 |
+
self.isp_perturb_set_special(
|
| 474 |
+
model, filtered_input_data, layer_to_quant, output_path_prefix
|
| 475 |
+
)
|
| 476 |
+
else:
|
| 477 |
+
self.isp_perturb_set(
|
| 478 |
+
model, filtered_input_data, layer_to_quant, output_path_prefix
|
| 479 |
+
)
|
| 480 |
else:
|
| 481 |
+
if (self.special_token) and ("cls" in self.emb_mode):
|
| 482 |
+
self.isp_perturb_all_special(
|
| 483 |
+
model, filtered_input_data, layer_to_quant, output_path_prefix
|
| 484 |
+
)
|
| 485 |
+
else:
|
| 486 |
+
self.isp_perturb_all(
|
| 487 |
+
model, filtered_input_data, layer_to_quant, output_path_prefix
|
| 488 |
+
)
|
| 489 |
|
| 490 |
def apply_additional_filters(self, filtered_input_data):
|
| 491 |
# additional filtering of input data dependent on isp mode
|
|
|
|
| 530 |
layer_to_quant: int,
|
| 531 |
output_path_prefix: str,
|
| 532 |
):
|
| 533 |
+
|
| 534 |
def make_group_perturbation_batch(example):
|
| 535 |
example_input_ids = example["input_ids"]
|
| 536 |
example["tokens_to_perturb"] = self.tokens_to_perturb
|
|
|
|
| 549 |
if self.perturb_type == "delete":
|
| 550 |
example = pu.delete_indices(example)
|
| 551 |
elif self.perturb_type == "overexpress":
|
| 552 |
+
example = pu.overexpress_tokens(example, self.max_len, self.special_token)
|
| 553 |
example["n_overflow"] = pu.calc_n_overflow(
|
| 554 |
self.max_len,
|
| 555 |
example["length"],
|
|
|
|
| 723 |
cos_sims_dict = self.update_perturbation_dictionary(
|
| 724 |
cos_sims_dict,
|
| 725 |
cos_sims_data,
|
|
|
|
|
|
|
| 726 |
gene_list,
|
| 727 |
)
|
| 728 |
else:
|
|
|
|
| 731 |
cos_sims_dict[state] = self.update_perturbation_dictionary(
|
| 732 |
cos_sims_dict[state],
|
| 733 |
cos_sims_data[state],
|
|
|
|
|
|
|
| 734 |
gene_list,
|
| 735 |
)
|
| 736 |
del minibatch
|
|
|
|
| 752 |
f"{output_path_prefix}_gene_embs_dict_{self.tokens_to_perturb}",
|
| 753 |
)
|
| 754 |
|
| 755 |
+
|
| 756 |
+
def isp_perturb_set_special(
|
| 757 |
+
self,
|
| 758 |
+
model,
|
| 759 |
+
filtered_input_data: Dataset,
|
| 760 |
+
layer_to_quant: int,
|
| 761 |
+
output_path_prefix: str,
|
| 762 |
+
):
|
| 763 |
+
|
| 764 |
+
def make_group_perturbation_batch(example):
|
| 765 |
+
example_input_ids = example["input_ids"]
|
| 766 |
+
example["tokens_to_perturb"] = self.tokens_to_perturb
|
| 767 |
+
indices_to_perturb = [
|
| 768 |
+
example_input_ids.index(token) if token in example_input_ids else None
|
| 769 |
+
for token in self.tokens_to_perturb
|
| 770 |
+
]
|
| 771 |
+
indices_to_perturb = [
|
| 772 |
+
item for item in indices_to_perturb if item is not None
|
| 773 |
+
]
|
| 774 |
+
if len(indices_to_perturb) > 0:
|
| 775 |
+
example["perturb_index"] = indices_to_perturb
|
| 776 |
+
else:
|
| 777 |
+
# -100 indicates tokens to overexpress are not present in rank value encoding
|
| 778 |
+
example["perturb_index"] = [-100]
|
| 779 |
+
if self.perturb_type == "delete":
|
| 780 |
+
example = pu.delete_indices(example)
|
| 781 |
+
elif self.perturb_type == "overexpress":
|
| 782 |
+
example = pu.overexpress_tokens(example, self.max_len, self.special_token)
|
| 783 |
+
example["n_overflow"] = pu.calc_n_overflow(
|
| 784 |
+
self.max_len,
|
| 785 |
+
example["length"],
|
| 786 |
+
self.tokens_to_perturb,
|
| 787 |
+
indices_to_perturb,
|
| 788 |
+
)
|
| 789 |
+
return example
|
| 790 |
+
|
| 791 |
+
total_batch_length = len(filtered_input_data)
|
| 792 |
+
if self.cell_states_to_model is None:
|
| 793 |
+
cos_sims_dict = defaultdict(list)
|
| 794 |
+
else:
|
| 795 |
+
cos_sims_dict = {
|
| 796 |
+
state: defaultdict(list)
|
| 797 |
+
for state in pu.get_possible_states(self.cell_states_to_model)
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
perturbed_data = filtered_input_data.map(
|
| 801 |
+
make_group_perturbation_batch, num_proc=self.nproc
|
| 802 |
+
)
|
| 803 |
+
|
| 804 |
+
if self.perturb_type == "overexpress":
|
| 805 |
+
filtered_input_data = filtered_input_data.add_column(
|
| 806 |
+
"n_overflow", perturbed_data["n_overflow"]
|
| 807 |
+
)
|
| 808 |
+
filtered_input_data = filtered_input_data.map(
|
| 809 |
+
pu.truncate_by_n_overflow_special, num_proc=self.nproc
|
| 810 |
+
)
|
| 811 |
+
|
| 812 |
+
if self.emb_mode == "cls_and_gene":
|
| 813 |
+
stored_gene_embs_dict = defaultdict(list)
|
| 814 |
+
|
| 815 |
+
# iterate through batches
|
| 816 |
+
for i in trange(0, total_batch_length, self.forward_batch_size):
|
| 817 |
+
max_range = min(i + self.forward_batch_size, total_batch_length)
|
| 818 |
+
inds_select = [i for i in range(i, max_range)]
|
| 819 |
+
|
| 820 |
+
minibatch = filtered_input_data.select(inds_select)
|
| 821 |
+
perturbation_batch = perturbed_data.select(inds_select)
|
| 822 |
+
|
| 823 |
+
##### CLS Embedding Mode #####
|
| 824 |
+
if self.emb_mode == "cls":
|
| 825 |
+
indices_to_perturb = perturbation_batch["perturb_index"]
|
| 826 |
+
|
| 827 |
+
original_cls_emb = get_embs(
|
| 828 |
+
model,
|
| 829 |
+
minibatch,
|
| 830 |
+
"cls",
|
| 831 |
+
layer_to_quant,
|
| 832 |
+
self.pad_token_id,
|
| 833 |
+
self.forward_batch_size,
|
| 834 |
+
token_gene_dict=self.token_gene_dict,
|
| 835 |
+
summary_stat=None,
|
| 836 |
+
silent=True,
|
| 837 |
+
)
|
| 838 |
+
|
| 839 |
+
perturbation_cls_emb = get_embs(
|
| 840 |
+
model,
|
| 841 |
+
perturbation_batch,
|
| 842 |
+
"cls",
|
| 843 |
+
layer_to_quant,
|
| 844 |
+
self.pad_token_id,
|
| 845 |
+
self.forward_batch_size,
|
| 846 |
+
token_gene_dict=self.token_gene_dict,
|
| 847 |
+
summary_stat=None,
|
| 848 |
+
silent=True,
|
| 849 |
+
)
|
| 850 |
+
|
| 851 |
+
# Calculate the cosine similarities
|
| 852 |
+
cls_cos_sims = pu.quant_cos_sims(
|
| 853 |
+
perturbation_cls_emb,
|
| 854 |
+
original_cls_emb,
|
| 855 |
+
self.cell_states_to_model,
|
| 856 |
+
self.state_embs_dict,
|
| 857 |
+
emb_mode="cell")
|
| 858 |
+
|
| 859 |
+
# Update perturbation dictionary
|
| 860 |
+
if self.cell_states_to_model is None:
|
| 861 |
+
cos_sims_dict = self.update_perturbation_dictionary(
|
| 862 |
+
cos_sims_dict,
|
| 863 |
+
cls_cos_sims,
|
| 864 |
+
gene_list = None,
|
| 865 |
+
)
|
| 866 |
+
else:
|
| 867 |
+
for state in cos_sims_dict.keys():
|
| 868 |
+
cos_sims_dict[state] = self.update_perturbation_dictionary(
|
| 869 |
+
cos_sims_dict[state],
|
| 870 |
+
cls_cos_sims[state],
|
| 871 |
+
gene_list = None,
|
| 872 |
+
)
|
| 873 |
+
|
| 874 |
+
##### CLS and Gene Embedding Mode #####
|
| 875 |
+
elif self.emb_mode == "cls_and_gene":
|
| 876 |
+
full_original_emb = get_embs(
|
| 877 |
+
model,
|
| 878 |
+
minibatch,
|
| 879 |
+
"gene",
|
| 880 |
+
layer_to_quant,
|
| 881 |
+
self.pad_token_id,
|
| 882 |
+
self.forward_batch_size,
|
| 883 |
+
self.token_gene_dict,
|
| 884 |
+
summary_stat=None,
|
| 885 |
+
silent=True,
|
| 886 |
+
)
|
| 887 |
+
indices_to_perturb = perturbation_batch["perturb_index"]
|
| 888 |
+
# remove indices that were perturbed
|
| 889 |
+
original_emb = pu.remove_perturbed_indices_set(
|
| 890 |
+
full_original_emb,
|
| 891 |
+
self.perturb_type,
|
| 892 |
+
indices_to_perturb,
|
| 893 |
+
self.tokens_to_perturb,
|
| 894 |
+
minibatch["length"],
|
| 895 |
+
)
|
| 896 |
+
full_perturbation_emb = get_embs(
|
| 897 |
+
model,
|
| 898 |
+
perturbation_batch,
|
| 899 |
+
"gene",
|
| 900 |
+
layer_to_quant,
|
| 901 |
+
self.pad_token_id,
|
| 902 |
+
self.forward_batch_size,
|
| 903 |
+
self.token_gene_dict,
|
| 904 |
+
summary_stat=None,
|
| 905 |
+
silent=True,
|
| 906 |
+
)
|
| 907 |
+
|
| 908 |
+
# remove special tokens and padding
|
| 909 |
+
original_emb = original_emb[:, 1:-1, :]
|
| 910 |
+
if self.perturb_type == "overexpress":
|
| 911 |
+
perturbation_emb = full_perturbation_emb[:,1+len(self.tokens_to_perturb):-1,:]
|
| 912 |
+
elif self.perturb_type == "delete":
|
| 913 |
+
perturbation_emb = full_perturbation_emb[:,1:max(perturbation_batch["length"])-1,:]
|
| 914 |
+
|
| 915 |
+
n_perturbation_genes = perturbation_emb.size()[1]
|
| 916 |
+
|
| 917 |
+
gene_cos_sims = pu.quant_cos_sims(
|
| 918 |
+
perturbation_emb,
|
| 919 |
+
original_emb,
|
| 920 |
+
self.cell_states_to_model,
|
| 921 |
+
self.state_embs_dict,
|
| 922 |
+
emb_mode="gene",
|
| 923 |
+
)
|
| 924 |
+
|
| 925 |
+
# get cls emb
|
| 926 |
+
original_cls_emb = full_original_emb[:,0,:]
|
| 927 |
+
perturbation_cls_emb = full_perturbation_emb[:,0,:]
|
| 928 |
+
|
| 929 |
+
cls_cos_sims = pu.quant_cos_sims(
|
| 930 |
+
perturbation_cls_emb,
|
| 931 |
+
original_cls_emb,
|
| 932 |
+
self.cell_states_to_model,
|
| 933 |
+
self.state_embs_dict,
|
| 934 |
+
emb_mode="cell",
|
| 935 |
+
)
|
| 936 |
+
|
| 937 |
+
# get cosine similarities in gene embeddings
|
| 938 |
+
# since getting gene embeddings, need gene names
|
| 939 |
+
|
| 940 |
+
gene_list = minibatch["input_ids"]
|
| 941 |
+
# need to truncate gene_list
|
| 942 |
+
genes_to_exclude = self.tokens_to_perturb + [self.cls_token_id, self.eos_token_id]
|
| 943 |
+
gene_list = [
|
| 944 |
+
[g for g in genes if g not in genes_to_exclude][
|
| 945 |
+
:n_perturbation_genes
|
| 946 |
+
]
|
| 947 |
+
for genes in gene_list
|
| 948 |
+
]
|
| 949 |
+
|
| 950 |
+
for cell_i, genes in enumerate(gene_list):
|
| 951 |
+
for gene_j, affected_gene in enumerate(genes):
|
| 952 |
+
if len(self.genes_to_perturb) > 1:
|
| 953 |
+
tokens_to_perturb = tuple(self.tokens_to_perturb)
|
| 954 |
+
else:
|
| 955 |
+
tokens_to_perturb = self.tokens_to_perturb[0]
|
| 956 |
+
|
| 957 |
+
# fill in the gene cosine similarities
|
| 958 |
+
try:
|
| 959 |
+
stored_gene_embs_dict[
|
| 960 |
+
(tokens_to_perturb, affected_gene)
|
| 961 |
+
].append(gene_cos_sims[cell_i, gene_j].item())
|
| 962 |
+
except KeyError:
|
| 963 |
+
stored_gene_embs_dict[
|
| 964 |
+
(tokens_to_perturb, affected_gene)
|
| 965 |
+
] = gene_cos_sims[cell_i, gene_j].item()
|
| 966 |
+
|
| 967 |
+
if self.cell_states_to_model is None:
|
| 968 |
+
cos_sims_dict = self.update_perturbation_dictionary(
|
| 969 |
+
cos_sims_dict,
|
| 970 |
+
cls_cos_sims,
|
| 971 |
+
gene_list = None,
|
| 972 |
+
)
|
| 973 |
+
else:
|
| 974 |
+
for state in cos_sims_dict.keys():
|
| 975 |
+
cos_sims_dict[state] = self.update_perturbation_dictionary(
|
| 976 |
+
cos_sims_dict[state],
|
| 977 |
+
cls_cos_sims[state],
|
| 978 |
+
gene_list = None,
|
| 979 |
+
)
|
| 980 |
+
del full_original_emb
|
| 981 |
+
del original_emb
|
| 982 |
+
del full_perturbation_emb
|
| 983 |
+
del perturbation_emb
|
| 984 |
+
del gene_cos_sims
|
| 985 |
+
|
| 986 |
+
del original_cls_emb
|
| 987 |
+
del perturbation_cls_emb
|
| 988 |
+
del cls_cos_sims
|
| 989 |
+
del minibatch
|
| 990 |
+
del perturbation_batch
|
| 991 |
+
|
| 992 |
+
torch.cuda.empty_cache()
|
| 993 |
+
|
| 994 |
+
pu.write_perturbation_dictionary(
|
| 995 |
+
cos_sims_dict,
|
| 996 |
+
f"{output_path_prefix}_cell_embs_dict_{self.tokens_to_perturb}",
|
| 997 |
+
)
|
| 998 |
+
|
| 999 |
+
if self.emb_mode == "cls_and_gene":
|
| 1000 |
+
pu.write_perturbation_dictionary(
|
| 1001 |
+
stored_gene_embs_dict,
|
| 1002 |
+
f"{output_path_prefix}_gene_embs_dict_{self.tokens_to_perturb}",
|
| 1003 |
+
)
|
| 1004 |
+
|
| 1005 |
def isp_perturb_all(
|
| 1006 |
self,
|
| 1007 |
model,
|
|
|
|
| 1020 |
|
| 1021 |
if self.emb_mode == "cell_and_gene":
|
| 1022 |
stored_gene_embs_dict = defaultdict(list)
|
| 1023 |
+
|
| 1024 |
+
num_inds_perturbed = 1 + self.combos
|
| 1025 |
+
for h in trange(len(filtered_input_data)):
|
| 1026 |
+
example_cell = filtered_input_data.select([h])
|
| 1027 |
full_original_emb = get_embs(
|
| 1028 |
model,
|
| 1029 |
example_cell,
|
|
|
|
| 1031 |
layer_to_quant,
|
| 1032 |
self.pad_token_id,
|
| 1033 |
self.forward_batch_size,
|
| 1034 |
+
self.token_gene_dict,
|
| 1035 |
summary_stat=None,
|
| 1036 |
silent=True,
|
| 1037 |
)
|
| 1038 |
+
|
| 1039 |
+
if self.cell_states_to_model is not None:
|
| 1040 |
+
original_cell_emb = pu.compute_nonpadded_cell_embedding(
|
| 1041 |
+
full_original_emb, "mean_pool"
|
| 1042 |
+
)
|
| 1043 |
+
|
| 1044 |
# gene_list is used to assign cos sims back to genes
|
|
|
|
| 1045 |
gene_list = example_cell["input_ids"][0][:]
|
| 1046 |
+
# need to remove the anchor gene
|
| 1047 |
if self.anchor_token is not None:
|
| 1048 |
for token in self.anchor_token:
|
| 1049 |
gene_list.remove(token)
|
| 1050 |
+
# index 0 is not overexpressed so remove
|
| 1051 |
+
if self.perturb_type == "overexpress":
|
| 1052 |
+
gene_list = gene_list[
|
| 1053 |
+
num_inds_perturbed:
|
| 1054 |
+
]
|
| 1055 |
+
# remove perturbed index for gene list dict
|
| 1056 |
+
perturbed_gene_dict = {
|
| 1057 |
+
gene: gene_list[:i] + gene_list[i + 1 :]
|
| 1058 |
+
for i, gene in enumerate(gene_list)
|
| 1059 |
+
}
|
| 1060 |
+
|
| 1061 |
perturbation_batch, indices_to_perturb = pu.make_perturbation_batch(
|
| 1062 |
example_cell,
|
| 1063 |
self.perturb_type,
|
|
|
|
| 1067 |
self.nproc,
|
| 1068 |
)
|
| 1069 |
|
| 1070 |
+
ispall_total_batch_length = len(perturbation_batch)
|
| 1071 |
+
for i in trange(0, ispall_total_batch_length, self.forward_batch_size, leave=False):
|
| 1072 |
+
ispall_max_range = min(i + self.forward_batch_size, ispall_total_batch_length)
|
| 1073 |
+
perturbation_minibatch = perturbation_batch.select([i for i in range(i, ispall_max_range)])
|
| 1074 |
+
indices_to_perturb_mini = indices_to_perturb[i : ispall_max_range]
|
| 1075 |
+
gene_list_mini = gene_list[i : ispall_max_range] # only perturbed genes from this minibatch
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1076 |
|
| 1077 |
+
full_perturbation_emb = get_embs(
|
| 1078 |
+
model,
|
| 1079 |
+
perturbation_minibatch,
|
| 1080 |
+
"gene",
|
| 1081 |
+
layer_to_quant,
|
| 1082 |
+
self.pad_token_id,
|
| 1083 |
+
self.forward_batch_size,
|
| 1084 |
+
self.token_gene_dict,
|
| 1085 |
+
summary_stat=None,
|
| 1086 |
+
silent=True,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1087 |
)
|
| 1088 |
+
|
| 1089 |
+
del perturbation_minibatch
|
| 1090 |
+
|
| 1091 |
+
# need to remove overexpressed gene to quantify cosine shifts
|
| 1092 |
+
if self.perturb_type == "overexpress":
|
| 1093 |
+
perturbation_emb = full_perturbation_emb[:, num_inds_perturbed:, :]
|
| 1094 |
+
|
| 1095 |
+
elif self.perturb_type == "delete":
|
| 1096 |
+
perturbation_emb = full_perturbation_emb
|
| 1097 |
+
|
| 1098 |
+
|
| 1099 |
+
if self.cell_states_to_model is None or self.emb_mode == "cell_and_gene":
|
| 1100 |
+
original_emb_minibatch = pu.make_comparison_batch(
|
| 1101 |
+
full_original_emb, indices_to_perturb_mini, perturb_group=False
|
| 1102 |
+
)
|
| 1103 |
+
gene_cos_sims = pu.quant_cos_sims(
|
| 1104 |
+
perturbation_emb,
|
| 1105 |
+
original_emb_minibatch,
|
| 1106 |
+
self.cell_states_to_model,
|
| 1107 |
+
self.state_embs_dict,
|
| 1108 |
+
emb_mode="gene",
|
| 1109 |
+
)
|
| 1110 |
+
del original_emb_minibatch
|
| 1111 |
+
|
| 1112 |
+
if self.cell_states_to_model is not None:
|
| 1113 |
+
perturbation_cell_emb = pu.compute_nonpadded_cell_embedding(
|
| 1114 |
+
full_perturbation_emb, "mean_pool"
|
| 1115 |
+
)
|
| 1116 |
+
|
| 1117 |
+
cell_cos_sims = pu.quant_cos_sims(
|
| 1118 |
+
perturbation_cell_emb,
|
| 1119 |
+
original_cell_emb,
|
| 1120 |
+
self.cell_states_to_model,
|
| 1121 |
+
self.state_embs_dict,
|
| 1122 |
+
emb_mode="cell",
|
| 1123 |
+
)
|
| 1124 |
+
del perturbation_cell_emb
|
| 1125 |
+
|
| 1126 |
+
if self.emb_mode == "cell_and_gene":
|
| 1127 |
|
| 1128 |
+
for perturbation_i, perturbed_gene in enumerate(gene_list_mini):
|
| 1129 |
+
for gene_j, affected_gene in enumerate(
|
| 1130 |
+
perturbed_gene_dict[perturbed_gene]
|
| 1131 |
+
):
|
| 1132 |
+
try:
|
| 1133 |
+
stored_gene_embs_dict[
|
| 1134 |
+
(perturbed_gene, affected_gene)
|
| 1135 |
+
].append(gene_cos_sims[perturbation_i, gene_j].item())
|
| 1136 |
+
except KeyError:
|
| 1137 |
+
stored_gene_embs_dict[
|
| 1138 |
+
(perturbed_gene, affected_gene)
|
| 1139 |
+
] = gene_cos_sims[perturbation_i, gene_j].item()
|
| 1140 |
+
|
| 1141 |
+
del full_perturbation_emb
|
| 1142 |
+
|
| 1143 |
+
if self.cell_states_to_model is None:
|
| 1144 |
+
cos_sims_data = torch.mean(gene_cos_sims, dim=1)
|
| 1145 |
+
cos_sims_dict = self.update_perturbation_dictionary(
|
| 1146 |
+
cos_sims_dict,
|
| 1147 |
+
cos_sims_data,
|
| 1148 |
+
gene_list_mini,
|
| 1149 |
+
)
|
| 1150 |
+
else:
|
| 1151 |
+
cos_sims_data = cell_cos_sims
|
| 1152 |
+
for state in cos_sims_dict.keys():
|
| 1153 |
+
cos_sims_dict[state] = self.update_perturbation_dictionary(
|
| 1154 |
+
cos_sims_dict[state],
|
| 1155 |
+
cos_sims_data[state],
|
| 1156 |
+
gene_list_mini,
|
| 1157 |
+
)
|
| 1158 |
+
|
| 1159 |
+
# save dict to disk every self.clear_mem_ncells/10 (default 100) simulated cells
|
| 1160 |
+
if i % self.clear_mem_ncells/10 == 0:
|
| 1161 |
+
pu.write_perturbation_dictionary(
|
| 1162 |
+
cos_sims_dict,
|
| 1163 |
+
f"{output_path_prefix}_dict_cell_embs_{h}batch{pickle_batch}",
|
| 1164 |
+
)
|
| 1165 |
+
if self.emb_mode == "cell_and_gene":
|
| 1166 |
+
pu.write_perturbation_dictionary(
|
| 1167 |
+
stored_gene_embs_dict,
|
| 1168 |
+
f"{output_path_prefix}_dict_gene_embs_{h}batch{pickle_batch}",
|
| 1169 |
+
)
|
| 1170 |
+
|
| 1171 |
+
# reset and clear memory every self.clear_mem_ncells (default 1000) simulated cells or at the end of the example cell
|
| 1172 |
+
if i % self.clear_mem_ncells == 0:
|
| 1173 |
+
pickle_batch += 1
|
| 1174 |
+
if self.cell_states_to_model is None:
|
| 1175 |
+
cos_sims_dict = defaultdict(list)
|
| 1176 |
+
else:
|
| 1177 |
+
cos_sims_dict = {
|
| 1178 |
+
state: defaultdict(list)
|
| 1179 |
+
for state in pu.get_possible_states(self.cell_states_to_model)
|
| 1180 |
+
}
|
| 1181 |
+
|
| 1182 |
+
if self.emb_mode == "cell_and_gene":
|
| 1183 |
+
stored_gene_embs_dict = defaultdict(list)
|
| 1184 |
+
|
| 1185 |
+
torch.cuda.empty_cache()
|
| 1186 |
+
|
| 1187 |
+
pu.write_perturbation_dictionary(
|
| 1188 |
+
cos_sims_dict, f"{output_path_prefix}_dict_cell_embs_{h}batch{pickle_batch}"
|
| 1189 |
+
)
|
| 1190 |
+
|
| 1191 |
+
if self.emb_mode == "cell_and_gene":
|
| 1192 |
+
pu.write_perturbation_dictionary(
|
| 1193 |
+
stored_gene_embs_dict,
|
| 1194 |
+
f"{output_path_prefix}_dict_gene_embs_{h}batch{pickle_batch}",
|
| 1195 |
)
|
| 1196 |
|
| 1197 |
+
pickle_batch = -1
|
| 1198 |
+
if self.cell_states_to_model is None:
|
| 1199 |
+
cos_sims_dict = defaultdict(list)
|
| 1200 |
+
else:
|
| 1201 |
+
cos_sims_dict = {
|
| 1202 |
+
state: defaultdict(list)
|
| 1203 |
+
for state in pu.get_possible_states(self.cell_states_to_model)
|
| 1204 |
}
|
| 1205 |
|
| 1206 |
+
if self.emb_mode == "cell_and_gene":
|
| 1207 |
+
stored_gene_embs_dict = defaultdict(list)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1208 |
|
| 1209 |
+
# clear memory between cells
|
| 1210 |
+
del perturbation_batch
|
| 1211 |
+
del full_original_emb
|
| 1212 |
+
if self.cell_states_to_model is not None:
|
| 1213 |
+
del original_cell_emb
|
| 1214 |
+
torch.cuda.empty_cache()
|
| 1215 |
+
|
| 1216 |
+
def isp_perturb_all_special(
|
| 1217 |
+
self,
|
| 1218 |
+
model,
|
| 1219 |
+
filtered_input_data: Dataset,
|
| 1220 |
+
layer_to_quant: int,
|
| 1221 |
+
output_path_prefix: str,
|
| 1222 |
+
):
|
| 1223 |
+
pickle_batch = -1
|
| 1224 |
+
if self.cell_states_to_model is None:
|
| 1225 |
+
cos_sims_dict = defaultdict(list)
|
| 1226 |
+
else:
|
| 1227 |
+
cos_sims_dict = {
|
| 1228 |
+
state: defaultdict(list)
|
| 1229 |
+
for state in pu.get_possible_states(self.cell_states_to_model)
|
| 1230 |
+
}
|
| 1231 |
|
| 1232 |
+
if self.emb_mode == "cls_and_gene":
|
| 1233 |
+
stored_gene_embs_dict = defaultdict(list)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1234 |
|
| 1235 |
+
num_inds_perturbed = 1 + self.combos
|
| 1236 |
+
for h in trange(len(filtered_input_data)):
|
| 1237 |
+
example_cell = filtered_input_data.select([h])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1238 |
|
| 1239 |
+
# get original example cell cls and/or gene embs for comparison
|
| 1240 |
+
if self.emb_mode == "cls":
|
| 1241 |
+
original_cls_emb = get_embs(
|
| 1242 |
+
model,
|
| 1243 |
+
example_cell,
|
| 1244 |
+
"cls",
|
| 1245 |
+
layer_to_quant,
|
| 1246 |
+
self.pad_token_id,
|
| 1247 |
+
self.forward_batch_size,
|
| 1248 |
+
self.token_gene_dict,
|
| 1249 |
+
summary_stat=None,
|
| 1250 |
+
silent=True,
|
| 1251 |
+
)
|
| 1252 |
+
elif self.emb_mode == "cls_and_gene":
|
| 1253 |
+
full_original_emb = get_embs(
|
| 1254 |
+
model,
|
| 1255 |
+
example_cell,
|
| 1256 |
+
"gene",
|
| 1257 |
+
layer_to_quant,
|
| 1258 |
+
self.pad_token_id,
|
| 1259 |
+
self.forward_batch_size,
|
| 1260 |
+
self.token_gene_dict,
|
| 1261 |
+
summary_stat=None,
|
| 1262 |
+
silent=True,
|
| 1263 |
+
)
|
| 1264 |
+
original_cls_emb = full_original_emb[:,0,:].clone().detach()
|
| 1265 |
+
|
| 1266 |
+
# gene_list is used to assign cos sims back to genes
|
| 1267 |
+
gene_list = example_cell["input_ids"][0][:]
|
| 1268 |
|
| 1269 |
+
# need to remove special tokens
|
| 1270 |
+
for token in [self.cls_token_id, self.eos_token_id]:
|
| 1271 |
+
gene_list.remove(token)
|
| 1272 |
+
# need to remove the anchor gene
|
| 1273 |
+
if self.anchor_token is not None:
|
| 1274 |
+
for token in self.anchor_token:
|
| 1275 |
+
gene_list.remove(token)
|
| 1276 |
+
# index 0 is not overexpressed so remove
|
| 1277 |
+
if self.perturb_type == "overexpress":
|
| 1278 |
+
gene_list = gene_list[
|
| 1279 |
+
num_inds_perturbed:
|
| 1280 |
+
]
|
| 1281 |
+
# remove perturbed index for gene list dict
|
| 1282 |
+
perturbed_gene_dict = {
|
| 1283 |
+
gene: gene_list[:i] + gene_list[i + 1 :]
|
| 1284 |
+
for i, gene in enumerate(gene_list)
|
| 1285 |
+
}
|
| 1286 |
|
| 1287 |
+
perturbation_batch, indices_to_perturb = pu.make_perturbation_batch_special(
|
| 1288 |
+
example_cell,
|
| 1289 |
+
self.perturb_type,
|
| 1290 |
+
self.tokens_to_perturb,
|
| 1291 |
+
self.anchor_token,
|
| 1292 |
+
self.combos,
|
| 1293 |
+
self.nproc,
|
| 1294 |
+
)
|
| 1295 |
|
| 1296 |
+
ispall_total_batch_length = len(perturbation_batch)
|
| 1297 |
+
for i in trange(0, ispall_total_batch_length, self.forward_batch_size, leave=False):
|
| 1298 |
+
ispall_max_range = min(i + self.forward_batch_size, ispall_total_batch_length)
|
| 1299 |
+
perturbation_minibatch = perturbation_batch.select([i for i in range(i, ispall_max_range)])
|
| 1300 |
+
indices_to_perturb_mini = indices_to_perturb[i : ispall_max_range]
|
| 1301 |
+
gene_list_mini = gene_list[i : ispall_max_range] # only perturbed genes from this minibatch
|
| 1302 |
+
|
| 1303 |
+
##### CLS Embedding Mode #####
|
| 1304 |
+
if self.emb_mode == "cls":
|
| 1305 |
+
# Extract cls embeddings from perturbed cells
|
| 1306 |
+
perturbation_cls_emb = get_embs(
|
| 1307 |
+
model,
|
| 1308 |
+
perturbation_minibatch,
|
| 1309 |
+
"cls",
|
| 1310 |
+
layer_to_quant,
|
| 1311 |
+
self.pad_token_id,
|
| 1312 |
+
self.forward_batch_size,
|
| 1313 |
+
self.token_gene_dict,
|
| 1314 |
+
summary_stat=None,
|
| 1315 |
+
silent=True,
|
| 1316 |
+
)
|
| 1317 |
+
|
| 1318 |
+
# Calculate cosine similarities
|
| 1319 |
+
cls_cos_sims = pu.quant_cos_sims(
|
| 1320 |
+
perturbation_cls_emb,
|
| 1321 |
+
original_cls_emb,
|
| 1322 |
+
self.cell_states_to_model,
|
| 1323 |
+
self.state_embs_dict,
|
| 1324 |
+
emb_mode="cell",
|
| 1325 |
+
)
|
| 1326 |
+
|
| 1327 |
+
if self.cell_states_to_model is None:
|
| 1328 |
+
cos_sims_dict = self.update_perturbation_dictionary(
|
| 1329 |
+
cos_sims_dict,
|
| 1330 |
+
cls_cos_sims,
|
| 1331 |
+
gene_list_mini,
|
| 1332 |
+
)
|
| 1333 |
+
else:
|
| 1334 |
+
|
| 1335 |
+
for state in cos_sims_dict.keys():
|
| 1336 |
+
cos_sims_dict[state] = self.update_perturbation_dictionary(
|
| 1337 |
+
cos_sims_dict[state],
|
| 1338 |
+
cls_cos_sims[state],
|
| 1339 |
+
gene_list_mini,
|
| 1340 |
+
)
|
| 1341 |
+
|
| 1342 |
+
del perturbation_minibatch
|
| 1343 |
+
del perturbation_cls_emb
|
| 1344 |
+
del cls_cos_sims
|
| 1345 |
+
|
| 1346 |
+
##### CLS and Gene Embedding Mode #####
|
| 1347 |
+
elif self.emb_mode == "cls_and_gene":
|
| 1348 |
+
full_perturbation_emb = get_embs(
|
| 1349 |
+
model,
|
| 1350 |
+
perturbation_minibatch,
|
| 1351 |
+
"gene",
|
| 1352 |
+
layer_to_quant,
|
| 1353 |
+
self.pad_token_id,
|
| 1354 |
+
self.forward_batch_size,
|
| 1355 |
+
self.token_gene_dict,
|
| 1356 |
+
summary_stat=None,
|
| 1357 |
+
silent=True,
|
| 1358 |
+
)
|
| 1359 |
+
|
| 1360 |
+
# need to remove overexpressed gene and cls/eos to quantify cosine shifts
|
| 1361 |
+
if self.perturb_type == "overexpress":
|
| 1362 |
+
perturbation_emb = full_perturbation_emb[:, 1+num_inds_perturbed:-1, :].clone().detach()
|
| 1363 |
+
elif self.perturb_type == "delete":
|
| 1364 |
+
perturbation_emb = full_perturbation_emb[:, 1:-1, :].clone().detach()
|
| 1365 |
+
|
| 1366 |
+
original_emb_minibatch = pu.make_comparison_batch(
|
| 1367 |
+
full_original_emb, indices_to_perturb_mini, perturb_group=False
|
| 1368 |
+
)
|
| 1369 |
+
|
| 1370 |
+
original_emb_minibatch = original_emb_minibatch[:, 1:-1, :].clone().detach()
|
| 1371 |
+
gene_cos_sims = pu.quant_cos_sims(
|
| 1372 |
+
perturbation_emb,
|
| 1373 |
+
original_emb_minibatch,
|
| 1374 |
+
self.cell_states_to_model,
|
| 1375 |
+
self.state_embs_dict,
|
| 1376 |
+
emb_mode="gene",
|
| 1377 |
+
)
|
| 1378 |
+
|
| 1379 |
+
for perturbation_i, perturbed_gene in enumerate(gene_list_mini):
|
| 1380 |
+
for gene_j, affected_gene in enumerate(
|
| 1381 |
+
perturbed_gene_dict[perturbed_gene]
|
| 1382 |
+
):
|
| 1383 |
+
try:
|
| 1384 |
+
stored_gene_embs_dict[
|
| 1385 |
+
(perturbed_gene, affected_gene)
|
| 1386 |
+
].append(gene_cos_sims[perturbation_i, gene_j].item())
|
| 1387 |
+
except KeyError:
|
| 1388 |
+
stored_gene_embs_dict[
|
| 1389 |
+
(perturbed_gene, affected_gene)
|
| 1390 |
+
] = gene_cos_sims[perturbation_i, gene_j].item()
|
| 1391 |
+
|
| 1392 |
+
# get cls emb
|
| 1393 |
+
perturbation_cls_emb = full_perturbation_emb[:,0,:].clone().detach()
|
| 1394 |
+
|
| 1395 |
+
cls_cos_sims = pu.quant_cos_sims(
|
| 1396 |
+
perturbation_cls_emb,
|
| 1397 |
+
original_cls_emb,
|
| 1398 |
+
self.cell_states_to_model,
|
| 1399 |
+
self.state_embs_dict,
|
| 1400 |
+
emb_mode="cell",
|
| 1401 |
+
)
|
| 1402 |
+
|
| 1403 |
+
if self.cell_states_to_model is None:
|
| 1404 |
+
cos_sims_dict = self.update_perturbation_dictionary(
|
| 1405 |
+
cos_sims_dict,
|
| 1406 |
+
cls_cos_sims,
|
| 1407 |
+
gene_list_mini,
|
| 1408 |
+
)
|
| 1409 |
+
else:
|
| 1410 |
+
for state in cos_sims_dict.keys():
|
| 1411 |
+
cos_sims_dict[state] = self.update_perturbation_dictionary(
|
| 1412 |
+
cos_sims_dict[state],
|
| 1413 |
+
cls_cos_sims[state],
|
| 1414 |
+
gene_list_mini,
|
| 1415 |
+
)
|
| 1416 |
+
|
| 1417 |
+
del perturbation_minibatch
|
| 1418 |
+
del original_emb_minibatch
|
| 1419 |
+
del full_perturbation_emb
|
| 1420 |
+
del perturbation_emb
|
| 1421 |
+
del perturbation_cls_emb
|
| 1422 |
+
del cls_cos_sims
|
| 1423 |
+
del gene_cos_sims
|
| 1424 |
+
|
| 1425 |
+
# save dict to disk every self.clear_mem_ncells/10 (default 100) simulated cells
|
| 1426 |
+
if i % max(1,self.clear_mem_ncells/10) == 0:
|
| 1427 |
+
pu.write_perturbation_dictionary(
|
| 1428 |
+
cos_sims_dict,
|
| 1429 |
+
f"{output_path_prefix}_dict_cell_embs_{h}batch{pickle_batch}",
|
| 1430 |
+
)
|
| 1431 |
+
if self.emb_mode == "cls_and_gene":
|
| 1432 |
+
pu.write_perturbation_dictionary(
|
| 1433 |
+
stored_gene_embs_dict,
|
| 1434 |
+
f"{output_path_prefix}_dict_gene_embs_{h}batch{pickle_batch}",
|
| 1435 |
+
)
|
| 1436 |
+
|
| 1437 |
+
# reset and clear memory every self.clear_mem_ncells (default 1000) simulated cells or at the end of the example cell
|
| 1438 |
+
if i % self.clear_mem_ncells == 0:
|
| 1439 |
+
pickle_batch += 1
|
| 1440 |
+
if self.cell_states_to_model is None:
|
| 1441 |
+
cos_sims_dict = defaultdict(list)
|
| 1442 |
+
else:
|
| 1443 |
+
cos_sims_dict = {
|
| 1444 |
+
state: defaultdict(list)
|
| 1445 |
+
for state in pu.get_possible_states(self.cell_states_to_model)
|
| 1446 |
+
}
|
| 1447 |
+
|
| 1448 |
+
if self.emb_mode == "cls_and_gene":
|
| 1449 |
+
stored_gene_embs_dict = defaultdict(list)
|
| 1450 |
+
|
| 1451 |
+
torch.cuda.empty_cache()
|
| 1452 |
+
|
| 1453 |
pu.write_perturbation_dictionary(
|
| 1454 |
+
cos_sims_dict, f"{output_path_prefix}_dict_cell_embs_{h}batch{pickle_batch}"
|
|
|
|
| 1455 |
)
|
| 1456 |
+
|
| 1457 |
+
if self.emb_mode == "cls_and_gene":
|
| 1458 |
+
pu.write_perturbation_dictionary(
|
| 1459 |
+
stored_gene_embs_dict,
|
| 1460 |
+
f"{output_path_prefix}_dict_gene_embs_{h}batch{pickle_batch}",
|
| 1461 |
+
)
|
| 1462 |
+
|
| 1463 |
+
pickle_batch = -1
|
| 1464 |
+
if self.cell_states_to_model is None:
|
| 1465 |
+
cos_sims_dict = defaultdict(list)
|
| 1466 |
+
else:
|
| 1467 |
+
cos_sims_dict = {
|
| 1468 |
+
state: defaultdict(list)
|
| 1469 |
+
for state in pu.get_possible_states(self.cell_states_to_model)
|
| 1470 |
+
}
|
| 1471 |
+
|
| 1472 |
+
if self.emb_mode == "cls_and_gene":
|
| 1473 |
+
stored_gene_embs_dict = defaultdict(list)
|
| 1474 |
+
|
| 1475 |
+
# clear memory between cells
|
| 1476 |
+
del perturbation_batch
|
| 1477 |
+
del original_cls_emb
|
| 1478 |
+
if self.emb_mode == "cls_and_gene":
|
| 1479 |
+
del full_original_emb
|
| 1480 |
+
torch.cuda.empty_cache()
|
| 1481 |
|
| 1482 |
+
|
| 1483 |
def update_perturbation_dictionary(
|
| 1484 |
self,
|
| 1485 |
cos_sims_dict: defaultdict,
|
| 1486 |
cos_sims_data: torch.Tensor,
|
|
|
|
|
|
|
| 1487 |
gene_list=None,
|
| 1488 |
):
|
| 1489 |
if gene_list is not None and cos_sims_data.shape[0] != len(gene_list):
|
| 1490 |
logger.error(
|
| 1491 |
f"len(cos_sims_data.shape[0]) != len(gene_list). \n \
|
| 1492 |
+
{cos_sims_data.shape[0]=}.\n \
|
| 1493 |
+
{len(gene_list)=}."
|
| 1494 |
)
|
| 1495 |
raise
|
| 1496 |
|
|
|
|
| 1514 |
for i, cos in enumerate(cos_sims_data.tolist()):
|
| 1515 |
cos_sims_dict[(gene_list[i], "cell_emb")].append(cos)
|
| 1516 |
|
| 1517 |
+
return cos_sims_dict
|
geneformer/in_silico_perturber_stats.py
CHANGED
|
@@ -114,6 +114,7 @@ def read_dictionaries(
|
|
| 114 |
state_dict[state_value][key] += new_dict[key]
|
| 115 |
except KeyError:
|
| 116 |
state_dict[state_value][key] = new_dict[key]
|
|
|
|
| 117 |
if not file_found:
|
| 118 |
logger.error(
|
| 119 |
"No raw data for processing found within provided directory. "
|
|
@@ -237,13 +238,16 @@ def find(variable, x):
|
|
| 237 |
|
| 238 |
|
| 239 |
def isp_aggregate_gene_shifts(
|
| 240 |
-
cos_sims_df, dict_list, gene_token_id_dict, gene_id_name_dict
|
| 241 |
):
|
| 242 |
cos_shift_data = dict()
|
| 243 |
for i in trange(cos_sims_df.shape[0]):
|
| 244 |
token = cos_sims_df["Gene"][i]
|
| 245 |
for dict_i in dict_list:
|
| 246 |
-
|
|
|
|
|
|
|
|
|
|
| 247 |
for key in affected_pairs:
|
| 248 |
if key in cos_shift_data.keys():
|
| 249 |
cos_shift_data[key] += dict_i.get(key, [])
|
|
@@ -256,11 +260,11 @@ def isp_aggregate_gene_shifts(
|
|
| 256 |
cos_sims_full_df = pd.DataFrame()
|
| 257 |
cos_sims_full_df["Perturbed"] = [k[0] for k, v in cos_data_mean.items()]
|
| 258 |
cos_sims_full_df["Gene_name"] = [
|
| 259 |
-
cos_sims_df[cos_sims_df["Gene"] == k[0]]["Gene_name"]
|
| 260 |
for k, v in cos_data_mean.items()
|
| 261 |
]
|
| 262 |
cos_sims_full_df["Ensembl_ID"] = [
|
| 263 |
-
cos_sims_df[cos_sims_df["Gene"] == k[0]]["Ensembl_ID"]
|
| 264 |
for k, v in cos_data_mean.items()
|
| 265 |
]
|
| 266 |
|
|
@@ -690,7 +694,7 @@ class InSilicoPerturberStats:
|
|
| 690 |
| Default is assuming genes_to_perturb in isp experiment was "all" (each gene in each cell).
|
| 691 |
| Otherwise, may provide a list of ENSEMBL IDs of genes perturbed as a group all together.
|
| 692 |
combos : {0,1,2}
|
| 693 |
-
| Whether
|
| 694 |
anchor_gene : None, str
|
| 695 |
| ENSEMBL ID of gene to use as anchor in combination perturbations or in testing effect on downstream genes.
|
| 696 |
| For example, if combos=1 and anchor_gene="ENSG00000136574":
|
|
@@ -1014,7 +1018,7 @@ class InSilicoPerturberStats:
|
|
| 1014 |
},
|
| 1015 |
index=[i for i in range(len(gene_list))],
|
| 1016 |
)
|
| 1017 |
-
|
| 1018 |
if self.mode == "goal_state_shift":
|
| 1019 |
cos_sims_df = isp_stats_to_goal_state(
|
| 1020 |
cos_sims_df_initial,
|
|
@@ -1045,11 +1049,23 @@ class InSilicoPerturberStats:
|
|
| 1045 |
cos_sims_df = isp_aggregate_grouped_perturb(cos_sims_df_initial, dict_list, self.genes_perturbed)
|
| 1046 |
|
| 1047 |
elif self.mode == "aggregate_gene_shifts":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1048 |
cos_sims_df = isp_aggregate_gene_shifts(
|
| 1049 |
cos_sims_df_initial,
|
| 1050 |
dict_list,
|
| 1051 |
self.gene_token_id_dict,
|
| 1052 |
self.gene_id_name_dict,
|
|
|
|
| 1053 |
)
|
| 1054 |
|
| 1055 |
# save perturbation stats to output_path
|
|
|
|
| 114 |
state_dict[state_value][key] += new_dict[key]
|
| 115 |
except KeyError:
|
| 116 |
state_dict[state_value][key] = new_dict[key]
|
| 117 |
+
|
| 118 |
if not file_found:
|
| 119 |
logger.error(
|
| 120 |
"No raw data for processing found within provided directory. "
|
|
|
|
| 238 |
|
| 239 |
|
| 240 |
def isp_aggregate_gene_shifts(
|
| 241 |
+
cos_sims_df, dict_list, gene_token_id_dict, gene_id_name_dict, token_dtype
|
| 242 |
):
|
| 243 |
cos_shift_data = dict()
|
| 244 |
for i in trange(cos_sims_df.shape[0]):
|
| 245 |
token = cos_sims_df["Gene"][i]
|
| 246 |
for dict_i in dict_list:
|
| 247 |
+
if token_dtype == "nontuple":
|
| 248 |
+
affected_pairs = [k for k, v in dict_i.items() if k[0] == token]
|
| 249 |
+
else:
|
| 250 |
+
affected_pairs = [k for k, v in dict_i.items() if find(k[0], token)]
|
| 251 |
for key in affected_pairs:
|
| 252 |
if key in cos_shift_data.keys():
|
| 253 |
cos_shift_data[key] += dict_i.get(key, [])
|
|
|
|
| 260 |
cos_sims_full_df = pd.DataFrame()
|
| 261 |
cos_sims_full_df["Perturbed"] = [k[0] for k, v in cos_data_mean.items()]
|
| 262 |
cos_sims_full_df["Gene_name"] = [
|
| 263 |
+
cos_sims_df[cos_sims_df["Gene"] == k[0]]["Gene_name"].item()
|
| 264 |
for k, v in cos_data_mean.items()
|
| 265 |
]
|
| 266 |
cos_sims_full_df["Ensembl_ID"] = [
|
| 267 |
+
cos_sims_df[cos_sims_df["Gene"] == k[0]]["Ensembl_ID"].item()
|
| 268 |
for k, v in cos_data_mean.items()
|
| 269 |
]
|
| 270 |
|
|
|
|
| 694 |
| Default is assuming genes_to_perturb in isp experiment was "all" (each gene in each cell).
|
| 695 |
| Otherwise, may provide a list of ENSEMBL IDs of genes perturbed as a group all together.
|
| 696 |
combos : {0,1,2}
|
| 697 |
+
| Whether genex perturbed in isp experiment were perturbed individually (0), in pairs (1), or in triplets (2).
|
| 698 |
anchor_gene : None, str
|
| 699 |
| ENSEMBL ID of gene to use as anchor in combination perturbations or in testing effect on downstream genes.
|
| 700 |
| For example, if combos=1 and anchor_gene="ENSG00000136574":
|
|
|
|
| 1018 |
},
|
| 1019 |
index=[i for i in range(len(gene_list))],
|
| 1020 |
)
|
| 1021 |
+
|
| 1022 |
if self.mode == "goal_state_shift":
|
| 1023 |
cos_sims_df = isp_stats_to_goal_state(
|
| 1024 |
cos_sims_df_initial,
|
|
|
|
| 1049 |
cos_sims_df = isp_aggregate_grouped_perturb(cos_sims_df_initial, dict_list, self.genes_perturbed)
|
| 1050 |
|
| 1051 |
elif self.mode == "aggregate_gene_shifts":
|
| 1052 |
+
if (self.genes_perturbed == "all") and (self.combos == 0):
|
| 1053 |
+
tuple_types = [True if isinstance(genes, tuple) else False for genes in gene_list]
|
| 1054 |
+
if all(tuple_types):
|
| 1055 |
+
token_dtype = "tuple"
|
| 1056 |
+
elif not any(tuple_types):
|
| 1057 |
+
token_dtype = "nontuple"
|
| 1058 |
+
else:
|
| 1059 |
+
token_dtype = "mix"
|
| 1060 |
+
else:
|
| 1061 |
+
token_dtype = "mix"
|
| 1062 |
+
|
| 1063 |
cos_sims_df = isp_aggregate_gene_shifts(
|
| 1064 |
cos_sims_df_initial,
|
| 1065 |
dict_list,
|
| 1066 |
self.gene_token_id_dict,
|
| 1067 |
self.gene_id_name_dict,
|
| 1068 |
+
token_dtype
|
| 1069 |
)
|
| 1070 |
|
| 1071 |
# save perturbation stats to output_path
|
geneformer/mtl/__init__.py
ADDED
|
File without changes
|
geneformer/mtl/collators.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#imports
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
from ..collator_for_classification import DataCollatorForGeneClassification
|
| 5 |
+
|
| 6 |
+
"""
|
| 7 |
+
Geneformer collator for multi-task cell classification.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
class DataCollatorForMultitaskCellClassification(DataCollatorForGeneClassification):
|
| 11 |
+
class_type = "cell"
|
| 12 |
+
|
| 13 |
+
def __init__(self, *args, **kwargs) -> None:
|
| 14 |
+
super().__init__(*args, **kwargs)
|
| 15 |
+
|
| 16 |
+
def _prepare_batch(self, features):
|
| 17 |
+
# Process inputs as usual
|
| 18 |
+
batch = self.tokenizer.pad(
|
| 19 |
+
features,
|
| 20 |
+
class_type=self.class_type,
|
| 21 |
+
padding=self.padding,
|
| 22 |
+
max_length=self.max_length,
|
| 23 |
+
pad_to_multiple_of=self.pad_to_multiple_of,
|
| 24 |
+
return_tensors="pt",
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
# Check if labels are present
|
| 28 |
+
if "label" in features[0]:
|
| 29 |
+
# Initialize labels dictionary for all tasks
|
| 30 |
+
labels = {task: [] for task in features[0]["label"].keys()}
|
| 31 |
+
|
| 32 |
+
# Populate labels for each task
|
| 33 |
+
for feature in features:
|
| 34 |
+
for task, label in feature["label"].items():
|
| 35 |
+
labels[task].append(label)
|
| 36 |
+
|
| 37 |
+
# Convert label lists to tensors, handling dictionaries appropriately
|
| 38 |
+
for task in labels:
|
| 39 |
+
if isinstance(labels[task][0], (list, torch.Tensor)):
|
| 40 |
+
dtype = torch.long
|
| 41 |
+
labels[task] = torch.tensor(labels[task], dtype=dtype)
|
| 42 |
+
elif isinstance(labels[task][0], dict):
|
| 43 |
+
# Handle dict specifically if needed
|
| 44 |
+
pass # Resolve nested data structure
|
| 45 |
+
|
| 46 |
+
# Update the batch to include task-specific labels
|
| 47 |
+
batch["labels"] = labels
|
| 48 |
+
else:
|
| 49 |
+
# If no labels are present, create empty labels for all tasks
|
| 50 |
+
batch["labels"] = {task: torch.tensor([], dtype=torch.long) for task in features[0]["input_ids"].keys()}
|
| 51 |
+
|
| 52 |
+
return batch
|
| 53 |
+
|
| 54 |
+
def __call__(self, features):
|
| 55 |
+
batch = self._prepare_batch(features)
|
| 56 |
+
|
| 57 |
+
for k, v in batch.items():
|
| 58 |
+
if torch.is_tensor(v):
|
| 59 |
+
batch[k] = v.clone().detach()
|
| 60 |
+
elif isinstance(v, dict):
|
| 61 |
+
# Assuming nested structure needs conversion
|
| 62 |
+
batch[k] = {task: torch.tensor(labels, dtype=torch.int64) for task, labels in v.items()}
|
| 63 |
+
else:
|
| 64 |
+
batch[k] = torch.tensor(v, dtype=torch.int64)
|
| 65 |
+
|
| 66 |
+
return batch
|
geneformer/mtl/data.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .imports import *
|
| 2 |
+
import os
|
| 3 |
+
from .collators import DataCollatorForMultitaskCellClassification
|
| 4 |
+
|
| 5 |
+
def load_and_preprocess_data(dataset_path, config, is_test=False, dataset_type=""):
|
| 6 |
+
try:
|
| 7 |
+
dataset = load_from_disk(dataset_path)
|
| 8 |
+
|
| 9 |
+
task_names = [f"task{i+1}" for i in range(len(config["task_columns"]))]
|
| 10 |
+
task_to_column = dict(zip(task_names, config["task_columns"]))
|
| 11 |
+
config["task_names"] = task_names
|
| 12 |
+
|
| 13 |
+
if not is_test:
|
| 14 |
+
available_columns = set(dataset.column_names)
|
| 15 |
+
for column in task_to_column.values():
|
| 16 |
+
if column not in available_columns:
|
| 17 |
+
raise KeyError(f"Column {column} not found in the dataset. Available columns: {list(available_columns)}")
|
| 18 |
+
|
| 19 |
+
label_mappings = {}
|
| 20 |
+
task_label_mappings = {}
|
| 21 |
+
cell_id_mapping = {}
|
| 22 |
+
num_labels_list = []
|
| 23 |
+
|
| 24 |
+
# Load or create task label mappings
|
| 25 |
+
if not is_test:
|
| 26 |
+
for task, column in task_to_column.items():
|
| 27 |
+
unique_values = sorted(set(dataset[column])) # Ensure consistency
|
| 28 |
+
label_mappings[column] = {label: idx for idx, label in enumerate(unique_values)}
|
| 29 |
+
task_label_mappings[task] = label_mappings[column]
|
| 30 |
+
num_labels_list.append(len(unique_values))
|
| 31 |
+
|
| 32 |
+
# Print the mappings for each task with dataset type prefix
|
| 33 |
+
for task, mapping in task_label_mappings.items():
|
| 34 |
+
print(f"{dataset_type.capitalize()} mapping for {task}: {mapping}") # sanity check, for train/validation splits
|
| 35 |
+
|
| 36 |
+
# Save the task label mappings as a pickle file
|
| 37 |
+
with open(f"{config['results_dir']}/task_label_mappings.pkl", "wb") as f:
|
| 38 |
+
pickle.dump(task_label_mappings, f)
|
| 39 |
+
else:
|
| 40 |
+
# Load task label mappings from pickle file for test data
|
| 41 |
+
with open(f"{config['results_dir']}/task_label_mappings.pkl", "rb") as f:
|
| 42 |
+
task_label_mappings = pickle.load(f)
|
| 43 |
+
|
| 44 |
+
# Infer num_labels_list from task_label_mappings
|
| 45 |
+
for task, mapping in task_label_mappings.items():
|
| 46 |
+
num_labels_list.append(len(mapping))
|
| 47 |
+
|
| 48 |
+
# Store unique cell IDs in a separate dictionary
|
| 49 |
+
for idx, record in enumerate(dataset):
|
| 50 |
+
cell_id = record.get('unique_cell_id', idx)
|
| 51 |
+
cell_id_mapping[idx] = cell_id
|
| 52 |
+
|
| 53 |
+
# Transform records to the desired format
|
| 54 |
+
transformed_dataset = []
|
| 55 |
+
for idx, record in enumerate(dataset):
|
| 56 |
+
transformed_record = {}
|
| 57 |
+
transformed_record['input_ids'] = torch.tensor(record['input_ids'], dtype=torch.long)
|
| 58 |
+
|
| 59 |
+
# Use index-based cell ID for internal tracking
|
| 60 |
+
transformed_record['cell_id'] = idx
|
| 61 |
+
|
| 62 |
+
if not is_test:
|
| 63 |
+
# Prepare labels
|
| 64 |
+
label_dict = {}
|
| 65 |
+
for task, column in task_to_column.items():
|
| 66 |
+
label_value = record[column]
|
| 67 |
+
label_index = task_label_mappings[task][label_value]
|
| 68 |
+
label_dict[task] = label_index
|
| 69 |
+
transformed_record['label'] = label_dict
|
| 70 |
+
else:
|
| 71 |
+
# Create dummy labels for test data
|
| 72 |
+
label_dict = {task: -1 for task in config["task_names"]}
|
| 73 |
+
transformed_record['label'] = label_dict
|
| 74 |
+
|
| 75 |
+
transformed_dataset.append(transformed_record)
|
| 76 |
+
|
| 77 |
+
return transformed_dataset, cell_id_mapping, num_labels_list
|
| 78 |
+
except KeyError as e:
|
| 79 |
+
print(f"Missing configuration or dataset key: {e}")
|
| 80 |
+
except Exception as e:
|
| 81 |
+
print(f"An error occurred while loading or preprocessing data: {e}")
|
| 82 |
+
return None, None, None
|
| 83 |
+
|
| 84 |
+
def preload_and_process_data(config):
|
| 85 |
+
# Load and preprocess data once
|
| 86 |
+
train_dataset, train_cell_id_mapping, num_labels_list = load_and_preprocess_data(config["train_path"], config, dataset_type="train")
|
| 87 |
+
val_dataset, val_cell_id_mapping, _ = load_and_preprocess_data(config["val_path"], config, dataset_type="validation")
|
| 88 |
+
return train_dataset, train_cell_id_mapping, val_dataset, val_cell_id_mapping, num_labels_list
|
| 89 |
+
|
| 90 |
+
def get_data_loader(preprocessed_dataset, batch_size):
|
| 91 |
+
nproc = os.cpu_count() ### I/O operations
|
| 92 |
+
|
| 93 |
+
data_collator = DataCollatorForMultitaskCellClassification()
|
| 94 |
+
|
| 95 |
+
loader = DataLoader(preprocessed_dataset, batch_size=batch_size, shuffle=True,
|
| 96 |
+
collate_fn=data_collator, num_workers=nproc, pin_memory=True)
|
| 97 |
+
return loader
|
| 98 |
+
def preload_data(config):
|
| 99 |
+
# Preprocessing the data before the Optuna trials start
|
| 100 |
+
train_loader = get_data_loader("train", config)
|
| 101 |
+
val_loader = get_data_loader("val", config)
|
| 102 |
+
return train_loader, val_loader
|
| 103 |
+
|
| 104 |
+
def load_and_preprocess_test_data(config):
|
| 105 |
+
"""
|
| 106 |
+
Load and preprocess test data, treating it as unlabeled.
|
| 107 |
+
"""
|
| 108 |
+
return load_and_preprocess_data(config["test_path"], config, is_test=True)
|
| 109 |
+
|
| 110 |
+
def prepare_test_loader(config):
|
| 111 |
+
"""
|
| 112 |
+
Prepare DataLoader for the test dataset.
|
| 113 |
+
"""
|
| 114 |
+
test_dataset, cell_id_mapping, num_labels_list = load_and_preprocess_test_data(config)
|
| 115 |
+
test_loader = get_data_loader(test_dataset, config['batch_size'])
|
| 116 |
+
return test_loader, cell_id_mapping, num_labels_list
|
geneformer/mtl/eval_utils.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .imports import *
|
| 2 |
+
import pandas as pd
|
| 3 |
+
from .data import prepare_test_loader
|
| 4 |
+
from .model import GeneformerMultiTask
|
| 5 |
+
|
| 6 |
+
def evaluate_test_dataset(model, device, test_loader, cell_id_mapping, config):
|
| 7 |
+
task_pred_labels = {task_name: [] for task_name in config["task_names"]}
|
| 8 |
+
task_pred_probs = {task_name: [] for task_name in config["task_names"]}
|
| 9 |
+
cell_ids = []
|
| 10 |
+
|
| 11 |
+
# Load task label mappings from pickle file
|
| 12 |
+
with open(f"{config['results_dir']}/task_label_mappings.pkl", "rb") as f:
|
| 13 |
+
task_label_mappings = pickle.load(f)
|
| 14 |
+
|
| 15 |
+
model.eval()
|
| 16 |
+
with torch.no_grad():
|
| 17 |
+
for batch in test_loader:
|
| 18 |
+
input_ids = batch['input_ids'].to(device)
|
| 19 |
+
attention_mask = batch['attention_mask'].to(device)
|
| 20 |
+
_, logits, _ = model(input_ids, attention_mask)
|
| 21 |
+
for sample_idx in range(len(batch['input_ids'])):
|
| 22 |
+
cell_id = cell_id_mapping[batch['cell_id'][sample_idx].item()]
|
| 23 |
+
cell_ids.append(cell_id)
|
| 24 |
+
for i, task_name in enumerate(config["task_names"]):
|
| 25 |
+
pred_label = torch.argmax(logits[i][sample_idx], dim=-1).item()
|
| 26 |
+
pred_prob = torch.softmax(logits[i][sample_idx], dim=-1).cpu().numpy()
|
| 27 |
+
task_pred_labels[task_name].append(pred_label)
|
| 28 |
+
task_pred_probs[task_name].append(pred_prob)
|
| 29 |
+
|
| 30 |
+
# Save test predictions with cell IDs and probabilities to CSV
|
| 31 |
+
test_results_dir = config["results_dir"]
|
| 32 |
+
os.makedirs(test_results_dir, exist_ok=True)
|
| 33 |
+
test_preds_file = os.path.join(test_results_dir, "test_preds.csv")
|
| 34 |
+
|
| 35 |
+
rows = []
|
| 36 |
+
for sample_idx in range(len(cell_ids)):
|
| 37 |
+
row = {'Cell ID': cell_ids[sample_idx]}
|
| 38 |
+
for task_name in config["task_names"]:
|
| 39 |
+
row[f'{task_name} Prediction'] = task_pred_labels[task_name][sample_idx]
|
| 40 |
+
row[f'{task_name} Probabilities'] = ','.join(map(str, task_pred_probs[task_name][sample_idx]))
|
| 41 |
+
rows.append(row)
|
| 42 |
+
|
| 43 |
+
df = pd.DataFrame(rows)
|
| 44 |
+
df.to_csv(test_preds_file, index=False)
|
| 45 |
+
print(f"Test predictions saved to {test_preds_file}")
|
| 46 |
+
|
| 47 |
+
def load_and_evaluate_test_model(config):
|
| 48 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 49 |
+
test_loader, cell_id_mapping, num_labels_list = prepare_test_loader(config)
|
| 50 |
+
model_directory = os.path.join(config["model_save_path"], "GeneformerMultiTask")
|
| 51 |
+
hyperparams_path = os.path.join(model_directory, "hyperparameters.json")
|
| 52 |
+
|
| 53 |
+
# Load the saved best hyperparameters
|
| 54 |
+
with open(hyperparams_path, 'r') as f:
|
| 55 |
+
best_hyperparams = json.load(f)
|
| 56 |
+
|
| 57 |
+
# Extract the task weights if present, otherwise set to None
|
| 58 |
+
task_weights = best_hyperparams.get("task_weights", None)
|
| 59 |
+
normalized_task_weights = task_weights if task_weights else []
|
| 60 |
+
|
| 61 |
+
# Print the loaded hyperparameters
|
| 62 |
+
print("Loaded hyperparameters:")
|
| 63 |
+
for param, value in best_hyperparams.items():
|
| 64 |
+
if param == "task_weights":
|
| 65 |
+
print(f"normalized_task_weights: {value}")
|
| 66 |
+
else:
|
| 67 |
+
print(f"{param}: {value}")
|
| 68 |
+
|
| 69 |
+
best_model_path = os.path.join(model_directory, "pytorch_model.bin")
|
| 70 |
+
best_model = GeneformerMultiTask(
|
| 71 |
+
config["pretrained_path"],
|
| 72 |
+
num_labels_list,
|
| 73 |
+
dropout_rate=best_hyperparams["dropout_rate"],
|
| 74 |
+
use_task_weights=config["use_task_weights"],
|
| 75 |
+
task_weights=normalized_task_weights
|
| 76 |
+
)
|
| 77 |
+
best_model.load_state_dict(torch.load(best_model_path))
|
| 78 |
+
best_model.to(device)
|
| 79 |
+
|
| 80 |
+
evaluate_test_dataset(best_model, device, test_loader, cell_id_mapping, config)
|
| 81 |
+
print("Evaluation completed.")
|
geneformer/mtl/imports.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pickle
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn as nn
|
| 5 |
+
import torch.optim as optim
|
| 6 |
+
import torch.nn.functional as F
|
| 7 |
+
from torch.utils.data import DataLoader
|
| 8 |
+
|
| 9 |
+
from itertools import chain
|
| 10 |
+
import warnings
|
| 11 |
+
from enum import Enum
|
| 12 |
+
from typing import Dict, List, Optional, Union
|
| 13 |
+
import sys
|
| 14 |
+
import os
|
| 15 |
+
import json
|
| 16 |
+
import gc
|
| 17 |
+
import functools
|
| 18 |
+
import pandas as pd
|
| 19 |
+
|
| 20 |
+
from sklearn.metrics import f1_score, accuracy_score, roc_auc_score, roc_curve
|
| 21 |
+
from sklearn.preprocessing import LabelEncoder
|
| 22 |
+
from sklearn.model_selection import train_test_split
|
| 23 |
+
|
| 24 |
+
import optuna
|
| 25 |
+
|
| 26 |
+
from transformers import (
|
| 27 |
+
BertConfig,
|
| 28 |
+
BertModel,
|
| 29 |
+
AdamW,
|
| 30 |
+
get_linear_schedule_with_warmup,
|
| 31 |
+
get_cosine_schedule_with_warmup,
|
| 32 |
+
DataCollatorForTokenClassification,
|
| 33 |
+
SpecialTokensMixin,
|
| 34 |
+
BatchEncoding,
|
| 35 |
+
get_scheduler,
|
| 36 |
+
)
|
| 37 |
+
from transformers.utils import logging, to_py_obj
|
| 38 |
+
|
| 39 |
+
from datasets import load_from_disk
|
| 40 |
+
|
| 41 |
+
# local modules
|
| 42 |
+
from .data import preload_and_process_data, get_data_loader
|
| 43 |
+
from .model import GeneformerMultiTask
|
| 44 |
+
from .utils import save_model
|
| 45 |
+
from .optuna_utils import create_optuna_study
|
| 46 |
+
from .collators import DataCollatorForMultitaskCellClassification
|
geneformer/mtl/model.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from transformers import BertModel, BertConfig
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
|
| 5 |
+
class AttentionPool(nn.Module):
|
| 6 |
+
"""Attention-based pooling layer."""
|
| 7 |
+
def __init__(self, hidden_size):
|
| 8 |
+
super(AttentionPool, self).__init__()
|
| 9 |
+
self.attention_weights = nn.Parameter(torch.randn(hidden_size, 1))
|
| 10 |
+
nn.init.xavier_uniform_(self.attention_weights) # https://pytorch.org/docs/stable/nn.init.html
|
| 11 |
+
|
| 12 |
+
def forward(self, hidden_states):
|
| 13 |
+
attention_scores = torch.matmul(hidden_states, self.attention_weights)
|
| 14 |
+
attention_scores = torch.softmax(attention_scores, dim=1)
|
| 15 |
+
pooled_output = torch.sum(hidden_states * attention_scores, dim=1)
|
| 16 |
+
return pooled_output
|
| 17 |
+
|
| 18 |
+
class GeneformerMultiTask(nn.Module):
|
| 19 |
+
def __init__(self, pretrained_path, num_labels_list, dropout_rate=0.1, use_task_weights=False, task_weights=None, max_layers_to_freeze=0, use_attention_pooling=False):
|
| 20 |
+
super(GeneformerMultiTask, self).__init__()
|
| 21 |
+
self.config = BertConfig.from_pretrained(pretrained_path)
|
| 22 |
+
self.bert = BertModel(self.config)
|
| 23 |
+
self.num_labels_list = num_labels_list
|
| 24 |
+
self.use_task_weights = use_task_weights
|
| 25 |
+
self.dropout = nn.Dropout(dropout_rate)
|
| 26 |
+
self.use_attention_pooling = use_attention_pooling
|
| 27 |
+
|
| 28 |
+
if use_task_weights and (task_weights is None or len(task_weights) != len(num_labels_list)):
|
| 29 |
+
raise ValueError("Task weights must be defined and match the number of tasks when 'use_task_weights' is True.")
|
| 30 |
+
self.task_weights = task_weights if use_task_weights else [1.0] * len(num_labels_list)
|
| 31 |
+
|
| 32 |
+
# Freeze the specified initial layers
|
| 33 |
+
for layer in self.bert.encoder.layer[:max_layers_to_freeze]:
|
| 34 |
+
for param in layer.parameters():
|
| 35 |
+
param.requires_grad = False
|
| 36 |
+
|
| 37 |
+
self.attention_pool = AttentionPool(self.config.hidden_size) if use_attention_pooling else None
|
| 38 |
+
|
| 39 |
+
self.classification_heads = nn.ModuleList([
|
| 40 |
+
nn.Linear(self.config.hidden_size, num_labels) for num_labels in num_labels_list
|
| 41 |
+
])
|
| 42 |
+
# initialization of the classification heads: https://pytorch.org/docs/stable/nn.init.html
|
| 43 |
+
for head in self.classification_heads:
|
| 44 |
+
nn.init.xavier_uniform_(head.weight)
|
| 45 |
+
nn.init.zeros_(head.bias)
|
| 46 |
+
|
| 47 |
+
def forward(self, input_ids, attention_mask, labels=None):
|
| 48 |
+
try:
|
| 49 |
+
outputs = self.bert(input_ids=input_ids, attention_mask=attention_mask)
|
| 50 |
+
except Exception as e:
|
| 51 |
+
raise RuntimeError(f"Error during BERT forward pass: {e}")
|
| 52 |
+
|
| 53 |
+
sequence_output = outputs.last_hidden_state
|
| 54 |
+
|
| 55 |
+
try:
|
| 56 |
+
pooled_output = self.attention_pool(sequence_output) if self.use_attention_pooling else sequence_output[:, 0, :]
|
| 57 |
+
pooled_output = self.dropout(pooled_output)
|
| 58 |
+
except Exception as e:
|
| 59 |
+
raise RuntimeError(f"Error during pooling and dropout: {e}")
|
| 60 |
+
|
| 61 |
+
total_loss = 0
|
| 62 |
+
logits = []
|
| 63 |
+
losses = []
|
| 64 |
+
|
| 65 |
+
for task_id, (head, num_labels) in enumerate(zip(self.classification_heads, self.num_labels_list)):
|
| 66 |
+
try:
|
| 67 |
+
task_logits = head(pooled_output)
|
| 68 |
+
except Exception as e:
|
| 69 |
+
raise RuntimeError(f"Error during forward pass of classification head {task_id}: {e}")
|
| 70 |
+
|
| 71 |
+
logits.append(task_logits)
|
| 72 |
+
|
| 73 |
+
if labels is not None:
|
| 74 |
+
try:
|
| 75 |
+
loss_fct = nn.CrossEntropyLoss()
|
| 76 |
+
task_loss = loss_fct(task_logits.view(-1, num_labels), labels[task_id].view(-1))
|
| 77 |
+
if self.use_task_weights:
|
| 78 |
+
task_loss *= self.task_weights[task_id]
|
| 79 |
+
total_loss += task_loss
|
| 80 |
+
losses.append(task_loss.item())
|
| 81 |
+
except Exception as e:
|
| 82 |
+
raise RuntimeError(f"Error during loss computation for task {task_id}: {e}")
|
| 83 |
+
|
| 84 |
+
return total_loss, logits, losses if labels is not None else logits
|
geneformer/mtl/optuna_utils.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import optuna
|
| 2 |
+
from optuna.integration import TensorBoardCallback
|
| 3 |
+
|
| 4 |
+
def save_trial_callback(study, trial, trials_result_path):
|
| 5 |
+
with open(trials_result_path, "a") as f:
|
| 6 |
+
f.write(f"Trial {trial.number}: Value (F1 Macro): {trial.value}, Params: {trial.params}\n")
|
| 7 |
+
|
| 8 |
+
def create_optuna_study(objective, n_trials, trials_result_path, tensorboard_log_dir):
|
| 9 |
+
study = optuna.create_study(direction="maximize")
|
| 10 |
+
|
| 11 |
+
# init TensorBoard callback
|
| 12 |
+
tensorboard_callback = TensorBoardCallback(dirname=tensorboard_log_dir, metric_name="F1 Macro")
|
| 13 |
+
|
| 14 |
+
# callback and TensorBoard callback
|
| 15 |
+
callbacks = [
|
| 16 |
+
lambda study, trial: save_trial_callback(study, trial, trials_result_path),
|
| 17 |
+
tensorboard_callback
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
study.optimize(objective, n_trials=n_trials, callbacks=callbacks)
|
| 21 |
+
return study
|
geneformer/mtl/train.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .imports import *
|
| 2 |
+
from .data import preload_and_process_data, get_data_loader
|
| 3 |
+
from .model import GeneformerMultiTask
|
| 4 |
+
from .utils import calculate_task_specific_metrics
|
| 5 |
+
from torch.utils.tensorboard import SummaryWriter
|
| 6 |
+
import pandas as pd
|
| 7 |
+
import os
|
| 8 |
+
from tqdm import tqdm
|
| 9 |
+
import random
|
| 10 |
+
import numpy as np
|
| 11 |
+
import torch
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def set_seed(seed):
|
| 15 |
+
random.seed(seed)
|
| 16 |
+
np.random.seed(seed)
|
| 17 |
+
torch.manual_seed(seed)
|
| 18 |
+
torch.cuda.manual_seed_all(seed)
|
| 19 |
+
torch.backends.cudnn.deterministic = True
|
| 20 |
+
torch.backends.cudnn.benchmark = False
|
| 21 |
+
|
| 22 |
+
def initialize_wandb(config):
|
| 23 |
+
if config.get("use_wandb", False):
|
| 24 |
+
import wandb
|
| 25 |
+
wandb.init(project=config["wandb_project"], config=config)
|
| 26 |
+
print("Weights & Biases (wandb) initialized and will be used for logging.")
|
| 27 |
+
else:
|
| 28 |
+
print("Weights & Biases (wandb) is not enabled. Logging will use other methods.")
|
| 29 |
+
|
| 30 |
+
def create_model(config, num_labels_list, device):
|
| 31 |
+
model = GeneformerMultiTask(
|
| 32 |
+
config["pretrained_path"],
|
| 33 |
+
num_labels_list,
|
| 34 |
+
dropout_rate=config["dropout_rate"],
|
| 35 |
+
use_task_weights=config["use_task_weights"],
|
| 36 |
+
task_weights=config["task_weights"],
|
| 37 |
+
max_layers_to_freeze=config["max_layers_to_freeze"],
|
| 38 |
+
use_attention_pooling=config["use_attention_pooling"]
|
| 39 |
+
)
|
| 40 |
+
if config["use_data_parallel"]:
|
| 41 |
+
model = nn.DataParallel(model)
|
| 42 |
+
return model.to(device)
|
| 43 |
+
|
| 44 |
+
def setup_optimizer_and_scheduler(model, config, total_steps):
|
| 45 |
+
optimizer = AdamW(model.parameters(), lr=config["learning_rate"], weight_decay=config["weight_decay"])
|
| 46 |
+
warmup_steps = int(config["warmup_ratio"] * total_steps)
|
| 47 |
+
|
| 48 |
+
if config["lr_scheduler_type"] == "linear":
|
| 49 |
+
scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps=warmup_steps, num_training_steps=total_steps)
|
| 50 |
+
elif config["lr_scheduler_type"] == "cosine":
|
| 51 |
+
scheduler = get_cosine_schedule_with_warmup(optimizer, num_warmup_steps=warmup_steps, num_training_steps=total_steps, num_cycles=0.5)
|
| 52 |
+
|
| 53 |
+
return optimizer, scheduler
|
| 54 |
+
|
| 55 |
+
def train_epoch(model, train_loader, optimizer, scheduler, device, config, writer, epoch):
|
| 56 |
+
model.train()
|
| 57 |
+
progress_bar = tqdm(train_loader, desc=f"Epoch {epoch+1}/{config['epochs']}")
|
| 58 |
+
for batch_idx, batch in enumerate(progress_bar):
|
| 59 |
+
optimizer.zero_grad()
|
| 60 |
+
input_ids = batch['input_ids'].to(device)
|
| 61 |
+
attention_mask = batch['attention_mask'].to(device)
|
| 62 |
+
labels = [batch['labels'][task_name].to(device) for task_name in config["task_names"]]
|
| 63 |
+
|
| 64 |
+
loss, _, _ = model(input_ids, attention_mask, labels)
|
| 65 |
+
loss.backward()
|
| 66 |
+
|
| 67 |
+
if config["gradient_clipping"]:
|
| 68 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(), config["max_grad_norm"])
|
| 69 |
+
|
| 70 |
+
optimizer.step()
|
| 71 |
+
scheduler.step()
|
| 72 |
+
|
| 73 |
+
writer.add_scalar('Training Loss', loss.item(), epoch * len(train_loader) + batch_idx)
|
| 74 |
+
if config.get("use_wandb", False):
|
| 75 |
+
wandb.log({'Training Loss': loss.item()})
|
| 76 |
+
|
| 77 |
+
# Update progress bar
|
| 78 |
+
progress_bar.set_postfix({'loss': f"{loss.item():.4f}"})
|
| 79 |
+
|
| 80 |
+
return loss.item() # Return the last batch loss
|
| 81 |
+
|
| 82 |
+
def validate_model(model, val_loader, device, config):
|
| 83 |
+
model.eval()
|
| 84 |
+
val_loss = 0.0
|
| 85 |
+
task_true_labels = {task_name: [] for task_name in config["task_names"]}
|
| 86 |
+
task_pred_labels = {task_name: [] for task_name in config["task_names"]}
|
| 87 |
+
task_pred_probs = {task_name: [] for task_name in config["task_names"]}
|
| 88 |
+
|
| 89 |
+
with torch.no_grad():
|
| 90 |
+
for batch in val_loader:
|
| 91 |
+
input_ids = batch['input_ids'].to(device)
|
| 92 |
+
attention_mask = batch['attention_mask'].to(device)
|
| 93 |
+
labels = [batch['labels'][task_name].to(device) for task_name in config["task_names"]]
|
| 94 |
+
loss, logits, _ = model(input_ids, attention_mask, labels)
|
| 95 |
+
val_loss += loss.item()
|
| 96 |
+
|
| 97 |
+
for sample_idx in range(len(batch['input_ids'])):
|
| 98 |
+
for i, task_name in enumerate(config["task_names"]):
|
| 99 |
+
true_label = batch['labels'][task_name][sample_idx].item()
|
| 100 |
+
pred_label = torch.argmax(logits[i][sample_idx], dim=-1).item()
|
| 101 |
+
pred_prob = torch.softmax(logits[i][sample_idx], dim=-1).cpu().numpy()
|
| 102 |
+
task_true_labels[task_name].append(true_label)
|
| 103 |
+
task_pred_labels[task_name].append(pred_label)
|
| 104 |
+
task_pred_probs[task_name].append(pred_prob)
|
| 105 |
+
|
| 106 |
+
val_loss /= len(val_loader)
|
| 107 |
+
return val_loss, task_true_labels, task_pred_labels, task_pred_probs
|
| 108 |
+
|
| 109 |
+
def log_metrics(task_metrics, val_loss, config, writer, epochs):
|
| 110 |
+
for task_name, metrics in task_metrics.items():
|
| 111 |
+
print(f"{task_name} - Validation F1 Macro: {metrics['f1']:.4f}, Validation Accuracy: {metrics['accuracy']:.4f}")
|
| 112 |
+
if config.get("use_wandb", False):
|
| 113 |
+
import wandb
|
| 114 |
+
wandb.log({
|
| 115 |
+
f'{task_name} Validation F1 Macro': metrics['f1'],
|
| 116 |
+
f'{task_name} Validation Accuracy': metrics['accuracy']
|
| 117 |
+
})
|
| 118 |
+
|
| 119 |
+
writer.add_scalar('Validation Loss', val_loss, epochs)
|
| 120 |
+
for task_name, metrics in task_metrics.items():
|
| 121 |
+
writer.add_scalar(f'{task_name} - Validation F1 Macro', metrics['f1'], epochs)
|
| 122 |
+
writer.add_scalar(f'{task_name} - Validation Accuracy', metrics['accuracy'], epochs)
|
| 123 |
+
|
| 124 |
+
def save_validation_predictions(val_cell_id_mapping, task_true_labels, task_pred_labels, task_pred_probs, config, trial_number=None):
|
| 125 |
+
if trial_number is not None:
|
| 126 |
+
trial_results_dir = os.path.join(config["results_dir"], f"trial_{trial_number}")
|
| 127 |
+
os.makedirs(trial_results_dir, exist_ok=True)
|
| 128 |
+
val_preds_file = os.path.join(trial_results_dir, "val_preds.csv")
|
| 129 |
+
else:
|
| 130 |
+
val_preds_file = os.path.join(config["results_dir"], "manual_run_val_preds.csv")
|
| 131 |
+
|
| 132 |
+
rows = []
|
| 133 |
+
for sample_idx in range(len(val_cell_id_mapping)):
|
| 134 |
+
row = {'Cell ID': val_cell_id_mapping[sample_idx]}
|
| 135 |
+
for task_name in config["task_names"]:
|
| 136 |
+
row[f'{task_name} True'] = task_true_labels[task_name][sample_idx]
|
| 137 |
+
row[f'{task_name} Pred'] = task_pred_labels[task_name][sample_idx]
|
| 138 |
+
row[f'{task_name} Probabilities'] = ','.join(map(str, task_pred_probs[task_name][sample_idx]))
|
| 139 |
+
rows.append(row)
|
| 140 |
+
|
| 141 |
+
df = pd.DataFrame(rows)
|
| 142 |
+
df.to_csv(val_preds_file, index=False)
|
| 143 |
+
print(f"Validation predictions saved to {val_preds_file}")
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def train_model(config, device, train_loader, val_loader, train_cell_id_mapping, val_cell_id_mapping, num_labels_list):
|
| 147 |
+
set_seed(config["seed"])
|
| 148 |
+
initialize_wandb(config)
|
| 149 |
+
|
| 150 |
+
model = create_model(config, num_labels_list, device)
|
| 151 |
+
total_steps = len(train_loader) * config["epochs"]
|
| 152 |
+
optimizer, scheduler = setup_optimizer_and_scheduler(model, config, total_steps)
|
| 153 |
+
|
| 154 |
+
log_dir = os.path.join(config["tensorboard_log_dir"], "manual_run")
|
| 155 |
+
writer = SummaryWriter(log_dir=log_dir)
|
| 156 |
+
|
| 157 |
+
epoch_progress = tqdm(range(config["epochs"]), desc="Training Progress")
|
| 158 |
+
for epoch in epoch_progress:
|
| 159 |
+
last_loss = train_epoch(model, train_loader, optimizer, scheduler, device, config, writer, epoch)
|
| 160 |
+
epoch_progress.set_postfix({'last_loss': f"{last_loss:.4f}"})
|
| 161 |
+
|
| 162 |
+
val_loss, task_true_labels, task_pred_labels, task_pred_probs = validate_model(model, val_loader, device, config)
|
| 163 |
+
task_metrics = calculate_task_specific_metrics(task_true_labels, task_pred_labels)
|
| 164 |
+
|
| 165 |
+
log_metrics(task_metrics, val_loss, config, writer, config["epochs"])
|
| 166 |
+
writer.close()
|
| 167 |
+
|
| 168 |
+
save_validation_predictions(val_cell_id_mapping, task_true_labels, task_pred_labels, task_pred_probs, config)
|
| 169 |
+
|
| 170 |
+
if config.get("use_wandb", False):
|
| 171 |
+
import wandb
|
| 172 |
+
wandb.finish()
|
| 173 |
+
|
| 174 |
+
print(f"\nFinal Validation Loss: {val_loss:.4f}")
|
| 175 |
+
return val_loss, model # Return both the validation loss and the trained model
|
| 176 |
+
|
| 177 |
+
def objective(trial, train_loader, val_loader, train_cell_id_mapping, val_cell_id_mapping, num_labels_list, config, device):
|
| 178 |
+
set_seed(config["seed"]) # Set the seed before each trial
|
| 179 |
+
initialize_wandb(config)
|
| 180 |
+
|
| 181 |
+
# Hyperparameters
|
| 182 |
+
config["learning_rate"] = trial.suggest_float("learning_rate", config["hyperparameters"]["learning_rate"]["low"], config["hyperparameters"]["learning_rate"]["high"], log=config["hyperparameters"]["learning_rate"]["log"])
|
| 183 |
+
config["warmup_ratio"] = trial.suggest_float("warmup_ratio", config["hyperparameters"]["warmup_ratio"]["low"], config["hyperparameters"]["warmup_ratio"]["high"])
|
| 184 |
+
config["weight_decay"] = trial.suggest_float("weight_decay", config["hyperparameters"]["weight_decay"]["low"], config["hyperparameters"]["weight_decay"]["high"])
|
| 185 |
+
config["dropout_rate"] = trial.suggest_float("dropout_rate", config["hyperparameters"]["dropout_rate"]["low"], config["hyperparameters"]["dropout_rate"]["high"])
|
| 186 |
+
config["lr_scheduler_type"] = trial.suggest_categorical("lr_scheduler_type", config["hyperparameters"]["lr_scheduler_type"]["choices"])
|
| 187 |
+
config["use_attention_pooling"] = trial.suggest_categorical("use_attention_pooling", [True, False])
|
| 188 |
+
|
| 189 |
+
if config["use_task_weights"]:
|
| 190 |
+
config["task_weights"] = [trial.suggest_float(f"task_weight_{i}", config["hyperparameters"]["task_weights"]["low"], config["hyperparameters"]["task_weights"]["high"]) for i in range(len(num_labels_list))]
|
| 191 |
+
weight_sum = sum(config["task_weights"])
|
| 192 |
+
config["task_weights"] = [weight / weight_sum for weight in config["task_weights"]]
|
| 193 |
+
else:
|
| 194 |
+
config["task_weights"] = None
|
| 195 |
+
|
| 196 |
+
# Fix for max_layers_to_freeze
|
| 197 |
+
if isinstance(config["max_layers_to_freeze"], dict):
|
| 198 |
+
config["max_layers_to_freeze"] = trial.suggest_int("max_layers_to_freeze", config["max_layers_to_freeze"]["min"], config["max_layers_to_freeze"]["max"])
|
| 199 |
+
elif isinstance(config["max_layers_to_freeze"], int):
|
| 200 |
+
# If it's already an int, we don't need to suggest it
|
| 201 |
+
pass
|
| 202 |
+
else:
|
| 203 |
+
raise ValueError("Invalid type for max_layers_to_freeze. Expected dict or int.")
|
| 204 |
+
|
| 205 |
+
model = create_model(config, num_labels_list, device)
|
| 206 |
+
total_steps = len(train_loader) * config["epochs"]
|
| 207 |
+
optimizer, scheduler = setup_optimizer_and_scheduler(model, config, total_steps)
|
| 208 |
+
|
| 209 |
+
log_dir = os.path.join(config["tensorboard_log_dir"], f"trial_{trial.number}")
|
| 210 |
+
writer = SummaryWriter(log_dir=log_dir)
|
| 211 |
+
|
| 212 |
+
for epoch in range(config["epochs"]):
|
| 213 |
+
train_epoch(model, train_loader, optimizer, scheduler, device, config, writer, epoch)
|
| 214 |
+
|
| 215 |
+
val_loss, task_true_labels, task_pred_labels, task_pred_probs = validate_model(model, val_loader, device, config)
|
| 216 |
+
task_metrics = calculate_task_specific_metrics(task_true_labels, task_pred_labels)
|
| 217 |
+
|
| 218 |
+
log_metrics(task_metrics, val_loss, config, writer, config["epochs"])
|
| 219 |
+
writer.close()
|
| 220 |
+
|
| 221 |
+
save_validation_predictions(val_cell_id_mapping, task_true_labels, task_pred_labels, task_pred_probs, config, trial.number)
|
| 222 |
+
|
| 223 |
+
trial.set_user_attr("model_state_dict", model.state_dict())
|
| 224 |
+
trial.set_user_attr("task_weights", config["task_weights"])
|
| 225 |
+
|
| 226 |
+
trial.report(val_loss, config["epochs"])
|
| 227 |
+
|
| 228 |
+
if trial.should_prune():
|
| 229 |
+
raise optuna.TrialPruned()
|
| 230 |
+
|
| 231 |
+
if config.get("use_wandb", False):
|
| 232 |
+
import wandb
|
| 233 |
+
wandb.log({
|
| 234 |
+
"trial_number": trial.number,
|
| 235 |
+
"val_loss": val_loss,
|
| 236 |
+
**{f"{task_name}_f1": metrics['f1'] for task_name, metrics in task_metrics.items()},
|
| 237 |
+
**{f"{task_name}_accuracy": metrics['accuracy'] for task_name, metrics in task_metrics.items()},
|
| 238 |
+
**{k: v for k, v in config.items() if k in ["learning_rate", "warmup_ratio", "weight_decay", "dropout_rate", "lr_scheduler_type", "use_attention_pooling", "max_layers_to_freeze"]}
|
| 239 |
+
})
|
| 240 |
+
wandb.finish()
|
| 241 |
+
|
| 242 |
+
return val_loss
|
geneformer/mtl/train_utils.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .imports import *
|
| 2 |
+
from .data import preload_and_process_data, get_data_loader
|
| 3 |
+
from .train import objective, train_model
|
| 4 |
+
from .model import GeneformerMultiTask
|
| 5 |
+
from .utils import save_model
|
| 6 |
+
import random
|
| 7 |
+
|
| 8 |
+
def set_seed(seed):
|
| 9 |
+
random.seed(seed)
|
| 10 |
+
np.random.seed(seed)
|
| 11 |
+
torch.manual_seed(seed)
|
| 12 |
+
torch.cuda.manual_seed_all(seed)
|
| 13 |
+
torch.backends.cudnn.deterministic = True
|
| 14 |
+
torch.backends.cudnn.benchmark = False
|
| 15 |
+
|
| 16 |
+
def run_manual_tuning(config):
|
| 17 |
+
# Set seed for reproducibility
|
| 18 |
+
set_seed(config["seed"])
|
| 19 |
+
|
| 20 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 21 |
+
train_dataset, train_cell_id_mapping, val_dataset, val_cell_id_mapping, num_labels_list = preload_and_process_data(config)
|
| 22 |
+
train_loader = get_data_loader(train_dataset, config['batch_size'])
|
| 23 |
+
val_loader = get_data_loader(val_dataset, config['batch_size'])
|
| 24 |
+
|
| 25 |
+
# Print the manual hyperparameters being used
|
| 26 |
+
print("\nManual hyperparameters being used:")
|
| 27 |
+
for key, value in config["manual_hyperparameters"].items():
|
| 28 |
+
print(f"{key}: {value}")
|
| 29 |
+
print() # Add an empty line for better readability
|
| 30 |
+
|
| 31 |
+
# Use the manual hyperparameters
|
| 32 |
+
for key, value in config["manual_hyperparameters"].items():
|
| 33 |
+
config[key] = value
|
| 34 |
+
|
| 35 |
+
# Train the model
|
| 36 |
+
val_loss, trained_model = train_model(config, device, train_loader, val_loader, train_cell_id_mapping, val_cell_id_mapping, num_labels_list)
|
| 37 |
+
|
| 38 |
+
print(f"\nValidation loss with manual hyperparameters: {val_loss}")
|
| 39 |
+
|
| 40 |
+
# Save the trained model
|
| 41 |
+
model_save_directory = os.path.join(config["model_save_path"], "GeneformerMultiTask")
|
| 42 |
+
save_model(trained_model, model_save_directory)
|
| 43 |
+
|
| 44 |
+
# Save the hyperparameters
|
| 45 |
+
hyperparams_to_save = {
|
| 46 |
+
**config["manual_hyperparameters"],
|
| 47 |
+
"dropout_rate": config["dropout_rate"],
|
| 48 |
+
"use_task_weights": config["use_task_weights"],
|
| 49 |
+
"task_weights": config["task_weights"],
|
| 50 |
+
"max_layers_to_freeze": config["max_layers_to_freeze"],
|
| 51 |
+
"use_attention_pooling": config["use_attention_pooling"]
|
| 52 |
+
}
|
| 53 |
+
hyperparams_path = os.path.join(model_save_directory, "hyperparameters.json")
|
| 54 |
+
with open(hyperparams_path, 'w') as f:
|
| 55 |
+
json.dump(hyperparams_to_save, f)
|
| 56 |
+
print(f"Manual hyperparameters saved to {hyperparams_path}")
|
| 57 |
+
|
| 58 |
+
return val_loss
|
| 59 |
+
|
| 60 |
+
def run_optuna_study(config):
|
| 61 |
+
# Set seed for reproducibility
|
| 62 |
+
set_seed(config["seed"])
|
| 63 |
+
|
| 64 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 65 |
+
train_dataset, train_cell_id_mapping, val_dataset, val_cell_id_mapping, num_labels_list = preload_and_process_data(config)
|
| 66 |
+
train_loader = get_data_loader(train_dataset, config['batch_size'])
|
| 67 |
+
val_loader = get_data_loader(val_dataset, config['batch_size'])
|
| 68 |
+
|
| 69 |
+
if config["use_manual_hyperparameters"]:
|
| 70 |
+
train_model(config, device, train_loader, val_loader, train_cell_id_mapping, val_cell_id_mapping, num_labels_list)
|
| 71 |
+
else:
|
| 72 |
+
objective_with_config_and_data = functools.partial(
|
| 73 |
+
objective,
|
| 74 |
+
train_loader=train_loader,
|
| 75 |
+
val_loader=val_loader,
|
| 76 |
+
train_cell_id_mapping=train_cell_id_mapping,
|
| 77 |
+
val_cell_id_mapping=val_cell_id_mapping,
|
| 78 |
+
num_labels_list=num_labels_list,
|
| 79 |
+
config=config,
|
| 80 |
+
device=device
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
study = optuna.create_study(
|
| 84 |
+
direction='minimize', # Minimize validation loss
|
| 85 |
+
study_name=config["study_name"],
|
| 86 |
+
#storage=config["storage"],
|
| 87 |
+
load_if_exists=True
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
study.optimize(
|
| 91 |
+
objective_with_config_and_data,
|
| 92 |
+
n_trials=config["n_trials"]
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
# After finding the best trial
|
| 96 |
+
best_params = study.best_trial.params
|
| 97 |
+
best_task_weights = study.best_trial.user_attrs["task_weights"]
|
| 98 |
+
print("Saving the best model and its hyperparameters...")
|
| 99 |
+
|
| 100 |
+
# Saving model as before
|
| 101 |
+
best_model = GeneformerMultiTask(
|
| 102 |
+
config["pretrained_path"],
|
| 103 |
+
num_labels_list,
|
| 104 |
+
dropout_rate=best_params["dropout_rate"],
|
| 105 |
+
use_task_weights=config["use_task_weights"],
|
| 106 |
+
task_weights=best_task_weights
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
# Get the best model state dictionary
|
| 110 |
+
best_model_state_dict = study.best_trial.user_attrs["model_state_dict"]
|
| 111 |
+
|
| 112 |
+
# Remove the "module." prefix from the state dictionary keys if present
|
| 113 |
+
best_model_state_dict = {k.replace("module.", ""): v for k, v in best_model_state_dict.items()}
|
| 114 |
+
|
| 115 |
+
# Load the modified state dictionary into the model, skipping unexpected keys
|
| 116 |
+
best_model.load_state_dict(best_model_state_dict, strict=False)
|
| 117 |
+
|
| 118 |
+
model_save_directory = os.path.join(config["model_save_path"], "GeneformerMultiTask")
|
| 119 |
+
save_model(best_model, model_save_directory)
|
| 120 |
+
|
| 121 |
+
# Additionally, save the best hyperparameters and task weights
|
| 122 |
+
hyperparams_path = os.path.join(model_save_directory, "hyperparameters.json")
|
| 123 |
+
|
| 124 |
+
with open(hyperparams_path, 'w') as f:
|
| 125 |
+
json.dump({**best_params, "task_weights": best_task_weights}, f)
|
| 126 |
+
print(f"Best hyperparameters and task weights saved to {hyperparams_path}")
|
geneformer/mtl/utils.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .imports import *
|
| 2 |
+
from sklearn.metrics import f1_score, accuracy_score
|
| 3 |
+
from sklearn.preprocessing import LabelEncoder
|
| 4 |
+
from transformers import BertModel, BertConfig, AutoConfig
|
| 5 |
+
import os
|
| 6 |
+
import shutil
|
| 7 |
+
|
| 8 |
+
def save_model(model, model_save_directory):
|
| 9 |
+
if not os.path.exists(model_save_directory):
|
| 10 |
+
os.makedirs(model_save_directory)
|
| 11 |
+
|
| 12 |
+
# Get the state dict
|
| 13 |
+
if isinstance(model, nn.DataParallel):
|
| 14 |
+
model_state_dict = model.module.state_dict() # Use model.module to access the underlying model
|
| 15 |
+
else:
|
| 16 |
+
model_state_dict = model.state_dict()
|
| 17 |
+
|
| 18 |
+
# Remove the "module." prefix from the keys if present
|
| 19 |
+
model_state_dict = {k.replace("module.", ""): v for k, v in model_state_dict.items()}
|
| 20 |
+
|
| 21 |
+
model_save_path = os.path.join(model_save_directory, "pytorch_model.bin")
|
| 22 |
+
torch.save(model_state_dict, model_save_path)
|
| 23 |
+
|
| 24 |
+
# Save the model configuration
|
| 25 |
+
if isinstance(model, nn.DataParallel):
|
| 26 |
+
model.module.config.to_json_file(os.path.join(model_save_directory, "config.json"))
|
| 27 |
+
else:
|
| 28 |
+
model.config.to_json_file(os.path.join(model_save_directory, "config.json"))
|
| 29 |
+
|
| 30 |
+
print(f"Model and configuration saved to {model_save_directory}")
|
| 31 |
+
|
| 32 |
+
def calculate_task_specific_metrics(task_true_labels, task_pred_labels):
|
| 33 |
+
task_metrics = {}
|
| 34 |
+
for task_name in task_true_labels.keys():
|
| 35 |
+
true_labels = task_true_labels[task_name]
|
| 36 |
+
pred_labels = task_pred_labels[task_name]
|
| 37 |
+
f1 = f1_score(true_labels, pred_labels, average='macro')
|
| 38 |
+
accuracy = accuracy_score(true_labels, pred_labels)
|
| 39 |
+
task_metrics[task_name] = {'f1': f1, 'accuracy': accuracy}
|
| 40 |
+
return task_metrics
|
| 41 |
+
|
| 42 |
+
def calculate_combined_f1(combined_labels, combined_preds):
|
| 43 |
+
# Initialize the LabelEncoder
|
| 44 |
+
le = LabelEncoder()
|
| 45 |
+
|
| 46 |
+
# Fit and transform combined labels and predictions to numerical values
|
| 47 |
+
le.fit(combined_labels + combined_preds)
|
| 48 |
+
encoded_true_labels = le.transform(combined_labels)
|
| 49 |
+
encoded_pred_labels = le.transform(combined_preds)
|
| 50 |
+
|
| 51 |
+
# Print out the mapping for sanity check
|
| 52 |
+
print("\nLabel Encoder Mapping:")
|
| 53 |
+
for index, class_label in enumerate(le.classes_):
|
| 54 |
+
print(f"'{class_label}': {index}")
|
| 55 |
+
|
| 56 |
+
# Calculate accuracy
|
| 57 |
+
accuracy = accuracy_score(encoded_true_labels, encoded_pred_labels)
|
| 58 |
+
|
| 59 |
+
# Calculate F1 Macro score
|
| 60 |
+
f1 = f1_score(encoded_true_labels, encoded_pred_labels, average='macro')
|
| 61 |
+
|
| 62 |
+
return f1, accuracy
|
| 63 |
+
|
| 64 |
+
def save_model_without_heads(original_model_save_directory):
|
| 65 |
+
# Create a new directory for the model without heads
|
| 66 |
+
new_model_save_directory = original_model_save_directory + "_No_Heads"
|
| 67 |
+
if not os.path.exists(new_model_save_directory):
|
| 68 |
+
os.makedirs(new_model_save_directory)
|
| 69 |
+
|
| 70 |
+
# Load the model state dictionary
|
| 71 |
+
model_state_dict = torch.load(os.path.join(original_model_save_directory, "pytorch_model.bin"))
|
| 72 |
+
|
| 73 |
+
# Initialize a new BERT model without the classification heads
|
| 74 |
+
config = BertConfig.from_pretrained(os.path.join(original_model_save_directory, "config.json"))
|
| 75 |
+
model_without_heads = BertModel(config)
|
| 76 |
+
|
| 77 |
+
# Filter the state dict to exclude classification heads
|
| 78 |
+
model_without_heads_state_dict = {k: v for k, v in model_state_dict.items() if not k.startswith("classification_heads")}
|
| 79 |
+
|
| 80 |
+
# Load the filtered state dict into the model
|
| 81 |
+
model_without_heads.load_state_dict(model_without_heads_state_dict, strict=False)
|
| 82 |
+
|
| 83 |
+
# Save the model without heads
|
| 84 |
+
model_save_path = os.path.join(new_model_save_directory, "pytorch_model.bin")
|
| 85 |
+
torch.save(model_without_heads.state_dict(), model_save_path)
|
| 86 |
+
|
| 87 |
+
# Copy the configuration file
|
| 88 |
+
shutil.copy(os.path.join(original_model_save_directory, "config.json"), new_model_save_directory)
|
| 89 |
+
|
| 90 |
+
print(f"Model without classification heads saved to {new_model_save_directory}")
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def get_layer_freeze_range(pretrained_path):
|
| 94 |
+
"""
|
| 95 |
+
Dynamically determines the number of layers to freeze based on the model depth from its configuration.
|
| 96 |
+
Args:
|
| 97 |
+
pretrained_path (str): Path to the pretrained model directory or model identifier.
|
| 98 |
+
Returns:
|
| 99 |
+
dict: A dictionary with 'min' and 'max' keys indicating the range of layers to freeze.
|
| 100 |
+
"""
|
| 101 |
+
if pretrained_path:
|
| 102 |
+
config = AutoConfig.from_pretrained(pretrained_path)
|
| 103 |
+
total_layers = config.num_hidden_layers
|
| 104 |
+
return {"min": 0, "max": total_layers - 1}
|
| 105 |
+
else:
|
| 106 |
+
return {"min": 0, "max": 0}
|
geneformer/mtl_classifier.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Geneformer multi-task cell classifier.
|
| 3 |
+
|
| 4 |
+
**Input data:**
|
| 5 |
+
|
| 6 |
+
| Single-cell transcriptomes as Geneformer rank value encodings with cell state labels for each task in Geneformer .dataset format (generated from single-cell RNAseq data by tokenizer.py). Must contain "unique_cell_id" column for logging.
|
| 7 |
+
|
| 8 |
+
**Usage:**
|
| 9 |
+
|
| 10 |
+
.. code-block :: python
|
| 11 |
+
|
| 12 |
+
>>> from geneformer import MTLClassifier
|
| 13 |
+
>>> mc = MTLClassifier(task_columns = ["task1", "task2"],
|
| 14 |
+
... study_name = "mtl",
|
| 15 |
+
... pretrained_path = "/path/pretrained/model",
|
| 16 |
+
... train_path = "/path/train/set",
|
| 17 |
+
... val_path = "/path/eval/set",
|
| 18 |
+
... test_path = "/path/test/set",
|
| 19 |
+
... model_save_path = "/results/directory/save_path",
|
| 20 |
+
... trials_result_path = "/results/directory/results.txt",
|
| 21 |
+
... results_dir = "/results/directory",
|
| 22 |
+
... tensorboard_log_dir = "/results/tblogdir",
|
| 23 |
+
... hyperparameters = hyperparameters)
|
| 24 |
+
>>> mc.run_optuna_study()
|
| 25 |
+
>>> mc.load_and_evaluate_test_model()
|
| 26 |
+
>>> mc.save_model_without_heads()
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
import logging
|
| 30 |
+
import os
|
| 31 |
+
from .mtl import train_utils
|
| 32 |
+
from .mtl import utils
|
| 33 |
+
from .mtl import eval_utils
|
| 34 |
+
|
| 35 |
+
logger = logging.getLogger(__name__)
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class MTLClassifier:
|
| 39 |
+
valid_option_dict = {
|
| 40 |
+
"task_columns": {list},
|
| 41 |
+
"train_path": {None, str},
|
| 42 |
+
"val_path": {None, str},
|
| 43 |
+
"test_path": {None, str},
|
| 44 |
+
"pretrained_path": {None, str},
|
| 45 |
+
"model_save_path": {None, str},
|
| 46 |
+
"results_dir": {None, str},
|
| 47 |
+
"batch_size": {None, int},
|
| 48 |
+
"n_trials": {None, int},
|
| 49 |
+
"study_name": {None, str},
|
| 50 |
+
"max_layers_to_freeze": {None, dict},
|
| 51 |
+
"epochs": {None, int},
|
| 52 |
+
"tensorboard_log_dir": {None, str},
|
| 53 |
+
"use_data_parallel": {None, bool},
|
| 54 |
+
"use_attention_pooling": {None, bool},
|
| 55 |
+
"use_task_weights": {None, bool},
|
| 56 |
+
"hyperparameters": {None, dict},
|
| 57 |
+
"manual_hyperparameters": {None, dict},
|
| 58 |
+
"use_manual_hyperparameters": {None, bool},
|
| 59 |
+
"use_wandb": {None, bool},
|
| 60 |
+
"wandb_project": {None, str},
|
| 61 |
+
"gradient_clipping": {None, bool},
|
| 62 |
+
"max_grad_norm": {None, int, float},
|
| 63 |
+
"seed": {None, int},
|
| 64 |
+
"trials_result_path": {None, str},
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
def __init__(
|
| 68 |
+
self,
|
| 69 |
+
task_columns=None,
|
| 70 |
+
train_path=None,
|
| 71 |
+
val_path=None,
|
| 72 |
+
test_path=None,
|
| 73 |
+
pretrained_path=None,
|
| 74 |
+
model_save_path=None,
|
| 75 |
+
results_dir=None,
|
| 76 |
+
trials_result_path=None,
|
| 77 |
+
batch_size=4,
|
| 78 |
+
n_trials=15,
|
| 79 |
+
study_name="mtl",
|
| 80 |
+
max_layers_to_freeze=None,
|
| 81 |
+
epochs=1,
|
| 82 |
+
tensorboard_log_dir="/results/tblogdir",
|
| 83 |
+
use_data_parallel=False,
|
| 84 |
+
use_attention_pooling=True,
|
| 85 |
+
use_task_weights=True,
|
| 86 |
+
hyperparameters=None, # Default is None
|
| 87 |
+
manual_hyperparameters=None, # Default is None
|
| 88 |
+
use_manual_hyperparameters=False, # Default is False
|
| 89 |
+
use_wandb=False,
|
| 90 |
+
wandb_project=None,
|
| 91 |
+
gradient_clipping=False,
|
| 92 |
+
max_grad_norm=None,
|
| 93 |
+
seed=42 # Default seed value
|
| 94 |
+
):
|
| 95 |
+
|
| 96 |
+
"""
|
| 97 |
+
Initialize Geneformer multi-task classifier.
|
| 98 |
+
**Parameters:**
|
| 99 |
+
task_columns : list
|
| 100 |
+
| List of tasks for cell state classification
|
| 101 |
+
| Input data columns are labeled with corresponding task names
|
| 102 |
+
study_name : None, str
|
| 103 |
+
| Study name for labeling output files
|
| 104 |
+
pretrained_path : None, str
|
| 105 |
+
| Path to pretrained model
|
| 106 |
+
train_path : None, str
|
| 107 |
+
| Path to training dataset with task columns and "unique_cell_id" column
|
| 108 |
+
val_path : None, str
|
| 109 |
+
| Path to validation dataset with task columns and "unique_cell_id" column
|
| 110 |
+
test_path : None, str
|
| 111 |
+
| Path to test dataset with task columns and "unique_cell_id" column
|
| 112 |
+
model_save_path : None, str
|
| 113 |
+
| Path to directory to save output model (either full model or model without heads)
|
| 114 |
+
trials_result_path : None, str
|
| 115 |
+
| Path to directory to save hyperparameter tuning trial results
|
| 116 |
+
results_dir : None, str
|
| 117 |
+
| Path to directory to save results
|
| 118 |
+
tensorboard_log_dir : None, str
|
| 119 |
+
| Path to directory for Tensorboard logging results
|
| 120 |
+
use_data_parallel : None, bool
|
| 121 |
+
| Whether to use data parallelization
|
| 122 |
+
use_attention_pooling : None, bool
|
| 123 |
+
| Whether to use attention pooling
|
| 124 |
+
use_task_weights : None, bool
|
| 125 |
+
| Whether to use task weights
|
| 126 |
+
batch_size : None, int
|
| 127 |
+
| Batch size to use
|
| 128 |
+
n_trials : None, int
|
| 129 |
+
| Number of trials for hyperparameter tuning
|
| 130 |
+
epochs : None, int
|
| 131 |
+
| Number of epochs for training
|
| 132 |
+
max_layers_to_freeze : None, dict
|
| 133 |
+
| Dictionary with keys "min" and "max" indicating the min and max layers to freeze from fine-tuning (int)
|
| 134 |
+
| 0: no layers will be frozen; 2: first two layers will be frozen; etc.
|
| 135 |
+
hyperparameters : None, dict
|
| 136 |
+
| Dictionary of categorical max and min for each hyperparameter for tuning
|
| 137 |
+
| For example:
|
| 138 |
+
| {"learning_rate": {"type":"float", "low":"1e-5", "high":"1e-3", "log":True}, "task_weights": {...}, ...}
|
| 139 |
+
manual_hyperparameters : None, dict
|
| 140 |
+
| Dictionary of manually set value for each hyperparameter
|
| 141 |
+
| For example:
|
| 142 |
+
| {"learning_rate": 0.001, "task_weights": [1, 1], ...}
|
| 143 |
+
use_manual_hyperparameters : None, bool
|
| 144 |
+
| Whether to use manually set hyperparameters
|
| 145 |
+
use_wandb : None, bool
|
| 146 |
+
| Whether to use Weights & Biases for logging
|
| 147 |
+
wandb_project : None, str
|
| 148 |
+
| Weights & Biases project name
|
| 149 |
+
gradient_clipping : None, bool
|
| 150 |
+
| Whether to use gradient clipping
|
| 151 |
+
max_grad_norm : None, int, float
|
| 152 |
+
| Maximum norm for gradient clipping
|
| 153 |
+
seed : None, int
|
| 154 |
+
| Random seed
|
| 155 |
+
"""
|
| 156 |
+
|
| 157 |
+
self.task_columns = task_columns
|
| 158 |
+
self.train_path = train_path
|
| 159 |
+
self.val_path = val_path
|
| 160 |
+
self.test_path = test_path
|
| 161 |
+
self.pretrained_path = pretrained_path
|
| 162 |
+
self.model_save_path = model_save_path
|
| 163 |
+
self.results_dir = results_dir
|
| 164 |
+
self.trials_result_path = trials_result_path
|
| 165 |
+
self.batch_size = batch_size
|
| 166 |
+
self.n_trials = n_trials
|
| 167 |
+
self.study_name = study_name
|
| 168 |
+
|
| 169 |
+
if max_layers_to_freeze is None:
|
| 170 |
+
# Dynamically determine the range of layers to freeze
|
| 171 |
+
layer_freeze_range = utils.get_layer_freeze_range(pretrained_path)
|
| 172 |
+
self.max_layers_to_freeze = {"min": 1, "max": layer_freeze_range['max']}
|
| 173 |
+
else:
|
| 174 |
+
self.max_layers_to_freeze = max_layers_to_freeze
|
| 175 |
+
|
| 176 |
+
self.epochs = epochs
|
| 177 |
+
self.tensorboard_log_dir = tensorboard_log_dir
|
| 178 |
+
self.use_data_parallel = use_data_parallel
|
| 179 |
+
self.use_attention_pooling = use_attention_pooling
|
| 180 |
+
self.use_task_weights = use_task_weights
|
| 181 |
+
self.hyperparameters = hyperparameters if hyperparameters is not None else {
|
| 182 |
+
"learning_rate": {
|
| 183 |
+
"type": "float",
|
| 184 |
+
"low": 1e-5,
|
| 185 |
+
"high": 1e-3,
|
| 186 |
+
"log": True
|
| 187 |
+
},
|
| 188 |
+
"warmup_ratio": {
|
| 189 |
+
"type": "float",
|
| 190 |
+
"low": 0.005,
|
| 191 |
+
"high": 0.01
|
| 192 |
+
},
|
| 193 |
+
"weight_decay": {
|
| 194 |
+
"type": "float",
|
| 195 |
+
"low": 0.01,
|
| 196 |
+
"high": 0.1
|
| 197 |
+
},
|
| 198 |
+
"dropout_rate": {
|
| 199 |
+
"type": "float",
|
| 200 |
+
"low": 0.0,
|
| 201 |
+
"high": 0.7
|
| 202 |
+
},
|
| 203 |
+
"lr_scheduler_type": {
|
| 204 |
+
"type": "categorical",
|
| 205 |
+
"choices": ["cosine"]
|
| 206 |
+
},
|
| 207 |
+
"task_weights": {
|
| 208 |
+
"type": "float",
|
| 209 |
+
"low": 0.1,
|
| 210 |
+
"high": 2.0
|
| 211 |
+
}
|
| 212 |
+
}
|
| 213 |
+
self.manual_hyperparameters = manual_hyperparameters if manual_hyperparameters is not None else {
|
| 214 |
+
"learning_rate": 0.001,
|
| 215 |
+
"warmup_ratio": 0.01,
|
| 216 |
+
"weight_decay": 0.1,
|
| 217 |
+
"dropout_rate": 0.1,
|
| 218 |
+
"lr_scheduler_type": "cosine",
|
| 219 |
+
"use_attention_pooling": False,
|
| 220 |
+
"task_weights": [1, 1],
|
| 221 |
+
"max_layers_to_freeze": 2
|
| 222 |
+
}
|
| 223 |
+
self.use_manual_hyperparameters = use_manual_hyperparameters
|
| 224 |
+
self.use_wandb = use_wandb
|
| 225 |
+
self.wandb_project = wandb_project
|
| 226 |
+
self.gradient_clipping = gradient_clipping
|
| 227 |
+
self.max_grad_norm = max_grad_norm
|
| 228 |
+
self.seed = seed
|
| 229 |
+
|
| 230 |
+
if self.use_manual_hyperparameters:
|
| 231 |
+
logger.warning(
|
| 232 |
+
"Hyperparameter tuning is highly recommended for optimal results."
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
self.validate_options()
|
| 236 |
+
|
| 237 |
+
# set up output directories
|
| 238 |
+
if self.results_dir is not None:
|
| 239 |
+
self.trials_results_path = f"{self.results_dir}/results.txt".replace("//","/")
|
| 240 |
+
|
| 241 |
+
for output_dir in [self.model_save_path, self.results_dir]:
|
| 242 |
+
if not os.path.exists(output_dir):
|
| 243 |
+
os.makedirs(output_dir)
|
| 244 |
+
|
| 245 |
+
self.config = {key: value for key, value in self.__dict__.items() if key in self.valid_option_dict}
|
| 246 |
+
|
| 247 |
+
def validate_options(self):
|
| 248 |
+
# confirm arguments are within valid options and compatible with each other
|
| 249 |
+
for attr_name, valid_options in self.valid_option_dict.items():
|
| 250 |
+
attr_value = self.__dict__[attr_name]
|
| 251 |
+
if not isinstance(attr_value, (list, dict)):
|
| 252 |
+
if attr_value in valid_options:
|
| 253 |
+
continue
|
| 254 |
+
valid_type = False
|
| 255 |
+
for option in valid_options:
|
| 256 |
+
if (option in [int, float, list, dict, bool, str]) and isinstance(
|
| 257 |
+
attr_value, option
|
| 258 |
+
):
|
| 259 |
+
valid_type = True
|
| 260 |
+
break
|
| 261 |
+
if valid_type:
|
| 262 |
+
continue
|
| 263 |
+
logger.error(
|
| 264 |
+
f"Invalid option for {attr_name}. "
|
| 265 |
+
f"Valid options for {attr_name}: {valid_options}"
|
| 266 |
+
)
|
| 267 |
+
raise ValueError(f"Invalid option for {attr_name}. Valid options for {attr_name}: {valid_options}")
|
| 268 |
+
|
| 269 |
+
def run_manual_tuning(self):
|
| 270 |
+
"""
|
| 271 |
+
Manual hyperparameter tuning and multi-task fine-tuning of pretrained model.
|
| 272 |
+
"""
|
| 273 |
+
required_variable_names = ["train_path", "val_path", "pretrained_path", "model_save_path", "results_dir"]
|
| 274 |
+
required_variables = [self.train_path, self.val_path, self.pretrained_path, self.model_save_path, self.results_dir]
|
| 275 |
+
req_var_dict = dict(zip(required_variable_names, required_variables))
|
| 276 |
+
self.validate_additional_options(req_var_dict)
|
| 277 |
+
|
| 278 |
+
if not self.use_manual_hyperparameters:
|
| 279 |
+
raise ValueError("Manual hyperparameters are not enabled. Set use_manual_hyperparameters to True.")
|
| 280 |
+
|
| 281 |
+
# Ensure manual_hyperparameters are set in the config
|
| 282 |
+
self.config["manual_hyperparameters"] = self.manual_hyperparameters
|
| 283 |
+
self.config["use_manual_hyperparameters"] = True
|
| 284 |
+
|
| 285 |
+
train_utils.run_manual_tuning(self.config)
|
| 286 |
+
|
| 287 |
+
def validate_additional_options(self, req_var_dict):
|
| 288 |
+
missing_variable = False
|
| 289 |
+
for variable_name, variable in req_var_dict.items():
|
| 290 |
+
if variable is None:
|
| 291 |
+
logger.warning(
|
| 292 |
+
f"Please provide value to MTLClassifier for required variable {variable_name}"
|
| 293 |
+
)
|
| 294 |
+
missing_variable = True
|
| 295 |
+
if missing_variable is True:
|
| 296 |
+
raise ValueError("Missing required variables for MTLClassifier")
|
| 297 |
+
|
| 298 |
+
def run_optuna_study(
|
| 299 |
+
self,
|
| 300 |
+
):
|
| 301 |
+
"""
|
| 302 |
+
Hyperparameter optimization and/or multi-task fine-tuning of pretrained model.
|
| 303 |
+
"""
|
| 304 |
+
|
| 305 |
+
required_variable_names = ["train_path", "val_path", "pretrained_path", "model_save_path", "results_dir"]
|
| 306 |
+
required_variables = [self.train_path, self.val_path, self.pretrained_path, self.model_save_path, self.results_dir]
|
| 307 |
+
req_var_dict = dict(zip(required_variable_names, required_variables))
|
| 308 |
+
self.validate_additional_options(req_var_dict)
|
| 309 |
+
|
| 310 |
+
train_utils.run_optuna_study(self.config)
|
| 311 |
+
|
| 312 |
+
def load_and_evaluate_test_model(
|
| 313 |
+
self,
|
| 314 |
+
):
|
| 315 |
+
"""
|
| 316 |
+
Loads previously fine-tuned multi-task model and evaluates on test data.
|
| 317 |
+
"""
|
| 318 |
+
|
| 319 |
+
required_variable_names = ["test_path", "model_save_path", "results_dir"]
|
| 320 |
+
required_variables = [self.test_path, self.model_save_path, self.results_dir]
|
| 321 |
+
req_var_dict = dict(zip(required_variable_names, required_variables))
|
| 322 |
+
self.validate_additional_options(req_var_dict)
|
| 323 |
+
|
| 324 |
+
eval_utils.load_and_evaluate_test_model(self.config)
|
| 325 |
+
|
| 326 |
+
def save_model_without_heads(
|
| 327 |
+
self,
|
| 328 |
+
):
|
| 329 |
+
"""
|
| 330 |
+
Save previously fine-tuned multi-task model without classification heads.
|
| 331 |
+
"""
|
| 332 |
+
|
| 333 |
+
required_variable_names = ["model_save_path"]
|
| 334 |
+
required_variables = [self.model_save_path]
|
| 335 |
+
req_var_dict = dict(zip(required_variable_names, required_variables))
|
| 336 |
+
self.validate_additional_options(req_var_dict)
|
| 337 |
+
|
| 338 |
+
utils.save_model_without_heads(os.path.join(self.model_save_path, "GeneformerMultiTask"))
|
geneformer/perturber_utils.py
CHANGED
|
@@ -12,13 +12,17 @@ import pandas as pd
|
|
| 12 |
import seaborn as sns
|
| 13 |
import torch
|
| 14 |
from datasets import Dataset, load_from_disk
|
|
|
|
| 15 |
from transformers import (
|
| 16 |
BertForMaskedLM,
|
| 17 |
BertForSequenceClassification,
|
| 18 |
BertForTokenClassification,
|
|
|
|
| 19 |
)
|
| 20 |
|
| 21 |
-
|
|
|
|
|
|
|
| 22 |
|
| 23 |
|
| 24 |
logger = logging.getLogger(__name__)
|
|
@@ -111,17 +115,49 @@ def slice_by_inds_to_perturb(filtered_input_data, cell_inds_to_perturb):
|
|
| 111 |
|
| 112 |
|
| 113 |
# load model to GPU
|
| 114 |
-
def load_model(model_type, num_classes, model_directory, mode):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
if mode == "eval":
|
| 116 |
output_hidden_states = True
|
| 117 |
elif mode == "train":
|
| 118 |
output_hidden_states = False
|
| 119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
if model_type == "Pretrained":
|
| 121 |
model = BertForMaskedLM.from_pretrained(
|
| 122 |
model_directory,
|
| 123 |
output_hidden_states=output_hidden_states,
|
| 124 |
output_attentions=False,
|
|
|
|
| 125 |
)
|
| 126 |
elif model_type == "GeneClassifier":
|
| 127 |
model = BertForTokenClassification.from_pretrained(
|
|
@@ -129,6 +165,7 @@ def load_model(model_type, num_classes, model_directory, mode):
|
|
| 129 |
num_labels=num_classes,
|
| 130 |
output_hidden_states=output_hidden_states,
|
| 131 |
output_attentions=False,
|
|
|
|
| 132 |
)
|
| 133 |
elif model_type == "CellClassifier":
|
| 134 |
model = BertForSequenceClassification.from_pretrained(
|
|
@@ -136,11 +173,24 @@ def load_model(model_type, num_classes, model_directory, mode):
|
|
| 136 |
num_labels=num_classes,
|
| 137 |
output_hidden_states=output_hidden_states,
|
| 138 |
output_attentions=False,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
)
|
| 140 |
# if eval mode, put the model in eval mode for fwd pass
|
| 141 |
if mode == "eval":
|
| 142 |
model.eval()
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
return model
|
| 145 |
|
| 146 |
|
|
@@ -222,27 +272,47 @@ def overexpress_indices(example):
|
|
| 222 |
indices = example["perturb_index"]
|
| 223 |
if any(isinstance(el, list) for el in indices):
|
| 224 |
indices = flatten_list(indices)
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
|
|
|
| 228 |
example["length"] = len(example["input_ids"])
|
| 229 |
return example
|
| 230 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
|
| 232 |
# for genes_to_perturb = list of genes to overexpress that are not necessarily expressed in cell
|
| 233 |
-
def overexpress_tokens(example, max_len):
|
| 234 |
# -100 indicates tokens to overexpress are not present in rank value encoding
|
| 235 |
if example["perturb_index"] != [-100]:
|
| 236 |
example = delete_indices(example)
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
|
| 242 |
# truncate to max input size, must also truncate original emb to be comparable
|
| 243 |
if len(example["input_ids"]) > max_len:
|
| 244 |
-
|
| 245 |
-
|
|
|
|
|
|
|
| 246 |
example["length"] = len(example["input_ids"])
|
| 247 |
return example
|
| 248 |
|
|
@@ -259,6 +329,13 @@ def truncate_by_n_overflow(example):
|
|
| 259 |
example["length"] = len(example["input_ids"])
|
| 260 |
return example
|
| 261 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
|
| 263 |
def remove_indices_from_emb(emb, indices_to_remove, gene_dim):
|
| 264 |
# indices_to_remove is list of indices to remove
|
|
@@ -392,7 +469,81 @@ def make_perturbation_batch(
|
|
| 392 |
return perturbation_dataset, indices_to_perturb
|
| 393 |
|
| 394 |
|
| 395 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
# so that only non-perturbed gene embeddings are compared to each other
|
| 397 |
# in original or perturbed context
|
| 398 |
def make_comparison_batch(original_emb_batch, indices_to_perturb, perturb_group):
|
|
@@ -589,9 +740,10 @@ def quant_cos_sims(
|
|
| 589 |
cos = torch.nn.CosineSimilarity(dim=1)
|
| 590 |
|
| 591 |
# if emb_mode == "gene", can only calculate gene cos sims
|
| 592 |
-
# against original cell
|
| 593 |
if cell_states_to_model is None or emb_mode == "gene":
|
| 594 |
cos_sims = cos(perturbation_emb, original_emb).to("cuda")
|
|
|
|
| 595 |
elif cell_states_to_model is not None and emb_mode == "cell":
|
| 596 |
possible_states = get_possible_states(cell_states_to_model)
|
| 597 |
cos_sims = dict(zip(possible_states, [[] for _ in range(len(possible_states))]))
|
|
@@ -758,4 +910,4 @@ class GeneIdHandler:
|
|
| 758 |
return self.ens_to_symbol(self.token_to_ens(token))
|
| 759 |
|
| 760 |
def symbol_to_token(self, symbol):
|
| 761 |
-
return self.ens_to_token(self.symbol_to_ens(symbol))
|
|
|
|
| 12 |
import seaborn as sns
|
| 13 |
import torch
|
| 14 |
from datasets import Dataset, load_from_disk
|
| 15 |
+
from peft import LoraConfig, get_peft_model
|
| 16 |
from transformers import (
|
| 17 |
BertForMaskedLM,
|
| 18 |
BertForSequenceClassification,
|
| 19 |
BertForTokenClassification,
|
| 20 |
+
BitsAndBytesConfig,
|
| 21 |
)
|
| 22 |
|
| 23 |
+
GENE_MEDIAN_FILE = Path(__file__).parent / "gene_median_dictionary.pkl"
|
| 24 |
+
TOKEN_DICTIONARY_FILE = Path(__file__).parent / "token_dictionary.pkl"
|
| 25 |
+
ENSEMBL_DICTIONARY_FILE = Path(__file__).parent / "gene_name_id_dict.pkl"
|
| 26 |
|
| 27 |
|
| 28 |
logger = logging.getLogger(__name__)
|
|
|
|
| 115 |
|
| 116 |
|
| 117 |
# load model to GPU
|
| 118 |
+
def load_model(model_type, num_classes, model_directory, mode, quantize=False):
|
| 119 |
+
if model_type == "MTLCellClassifier-Quantized":
|
| 120 |
+
model_type = "MTLCellClassifier"
|
| 121 |
+
quantize = True
|
| 122 |
+
|
| 123 |
if mode == "eval":
|
| 124 |
output_hidden_states = True
|
| 125 |
elif mode == "train":
|
| 126 |
output_hidden_states = False
|
| 127 |
|
| 128 |
+
if quantize is True:
|
| 129 |
+
if model_type == "MTLCellClassifier":
|
| 130 |
+
quantize = {
|
| 131 |
+
"peft_config": None,
|
| 132 |
+
"bnb_config": BitsAndBytesConfig(
|
| 133 |
+
load_in_8bit=True,
|
| 134 |
+
)
|
| 135 |
+
}
|
| 136 |
+
else:
|
| 137 |
+
quantize = {
|
| 138 |
+
"peft_config": LoraConfig(
|
| 139 |
+
lora_alpha=128,
|
| 140 |
+
lora_dropout=0.1,
|
| 141 |
+
r=64,
|
| 142 |
+
bias="none",
|
| 143 |
+
task_type="TokenClassification",
|
| 144 |
+
),
|
| 145 |
+
"bnb_config": BitsAndBytesConfig(
|
| 146 |
+
load_in_4bit=True,
|
| 147 |
+
bnb_4bit_use_double_quant=True,
|
| 148 |
+
bnb_4bit_quant_type="nf4",
|
| 149 |
+
bnb_4bit_compute_dtype=torch.bfloat16
|
| 150 |
+
)
|
| 151 |
+
}
|
| 152 |
+
elif quantize is False:
|
| 153 |
+
quantize = {"bnb_config": None}
|
| 154 |
+
|
| 155 |
if model_type == "Pretrained":
|
| 156 |
model = BertForMaskedLM.from_pretrained(
|
| 157 |
model_directory,
|
| 158 |
output_hidden_states=output_hidden_states,
|
| 159 |
output_attentions=False,
|
| 160 |
+
quantization_config=quantize["bnb_config"],
|
| 161 |
)
|
| 162 |
elif model_type == "GeneClassifier":
|
| 163 |
model = BertForTokenClassification.from_pretrained(
|
|
|
|
| 165 |
num_labels=num_classes,
|
| 166 |
output_hidden_states=output_hidden_states,
|
| 167 |
output_attentions=False,
|
| 168 |
+
quantization_config=quantize["bnb_config"],
|
| 169 |
)
|
| 170 |
elif model_type == "CellClassifier":
|
| 171 |
model = BertForSequenceClassification.from_pretrained(
|
|
|
|
| 173 |
num_labels=num_classes,
|
| 174 |
output_hidden_states=output_hidden_states,
|
| 175 |
output_attentions=False,
|
| 176 |
+
quantization_config=quantize["bnb_config"],
|
| 177 |
+
)
|
| 178 |
+
elif model_type == "MTLCellClassifier":
|
| 179 |
+
model = BertForMaskedLM.from_pretrained(
|
| 180 |
+
model_directory,
|
| 181 |
+
num_labels=num_classes,
|
| 182 |
+
output_hidden_states=output_hidden_states,
|
| 183 |
+
output_attentions=False,
|
| 184 |
+
quantization_config=quantize["bnb_config"],
|
| 185 |
)
|
| 186 |
# if eval mode, put the model in eval mode for fwd pass
|
| 187 |
if mode == "eval":
|
| 188 |
model.eval()
|
| 189 |
+
if (quantize is False) or (quantize == {'bnb_config': None}) or (model_type == "MTLCellClassifier"):
|
| 190 |
+
model = model.to("cuda")
|
| 191 |
+
else:
|
| 192 |
+
model.enable_input_require_grads()
|
| 193 |
+
model = get_peft_model(model, quantize["peft_config"])
|
| 194 |
return model
|
| 195 |
|
| 196 |
|
|
|
|
| 272 |
indices = example["perturb_index"]
|
| 273 |
if any(isinstance(el, list) for el in indices):
|
| 274 |
indices = flatten_list(indices)
|
| 275 |
+
insert_pos = 0
|
| 276 |
+
for index in sorted(indices, reverse=False):
|
| 277 |
+
example["input_ids"].insert(insert_pos, example["input_ids"].pop(index))
|
| 278 |
+
insert_pos += 1
|
| 279 |
example["length"] = len(example["input_ids"])
|
| 280 |
return example
|
| 281 |
|
| 282 |
+
# if CLS token present, move to 1st rather than 0th position
|
| 283 |
+
def overexpress_indices_special(example):
|
| 284 |
+
indices = example["perturb_index"]
|
| 285 |
+
if any(isinstance(el, list) for el in indices):
|
| 286 |
+
indices = flatten_list(indices)
|
| 287 |
+
insert_pos = 1 # Insert starting after CLS token
|
| 288 |
+
for index in sorted(indices, reverse=False):
|
| 289 |
+
example["input_ids"].insert(insert_pos, example["input_ids"].pop(index))
|
| 290 |
+
insert_pos += 1
|
| 291 |
+
example["length"] = len(example["input_ids"])
|
| 292 |
+
return example
|
| 293 |
|
| 294 |
# for genes_to_perturb = list of genes to overexpress that are not necessarily expressed in cell
|
| 295 |
+
def overexpress_tokens(example, max_len, special_token):
|
| 296 |
# -100 indicates tokens to overexpress are not present in rank value encoding
|
| 297 |
if example["perturb_index"] != [-100]:
|
| 298 |
example = delete_indices(example)
|
| 299 |
+
if special_token:
|
| 300 |
+
[
|
| 301 |
+
example["input_ids"].insert(1, token)
|
| 302 |
+
for token in example["tokens_to_perturb"][::-1]
|
| 303 |
+
]
|
| 304 |
+
else:
|
| 305 |
+
[
|
| 306 |
+
example["input_ids"].insert(0, token)
|
| 307 |
+
for token in example["tokens_to_perturb"][::-1]
|
| 308 |
+
]
|
| 309 |
|
| 310 |
# truncate to max input size, must also truncate original emb to be comparable
|
| 311 |
if len(example["input_ids"]) > max_len:
|
| 312 |
+
if special_token:
|
| 313 |
+
example["input_ids"] = example["input_ids"][0:max_len-1]+[example["input_ids"][-1]]
|
| 314 |
+
else:
|
| 315 |
+
example["input_ids"] = example["input_ids"][0:max_len]
|
| 316 |
example["length"] = len(example["input_ids"])
|
| 317 |
return example
|
| 318 |
|
|
|
|
| 329 |
example["length"] = len(example["input_ids"])
|
| 330 |
return example
|
| 331 |
|
| 332 |
+
def truncate_by_n_overflow_special(example):
|
| 333 |
+
if example["n_overflow"] > 0:
|
| 334 |
+
new_max_len = example["length"] - example["n_overflow"]
|
| 335 |
+
example["input_ids"] = example["input_ids"][0:new_max_len-1]+[example["input_ids"][-1]]
|
| 336 |
+
example["length"] = len(example["input_ids"])
|
| 337 |
+
return example
|
| 338 |
+
|
| 339 |
|
| 340 |
def remove_indices_from_emb(emb, indices_to_remove, gene_dim):
|
| 341 |
# indices_to_remove is list of indices to remove
|
|
|
|
| 469 |
return perturbation_dataset, indices_to_perturb
|
| 470 |
|
| 471 |
|
| 472 |
+
def make_perturbation_batch_special(
|
| 473 |
+
example_cell, perturb_type, tokens_to_perturb, anchor_token, combo_lvl, num_proc
|
| 474 |
+
) -> tuple[Dataset, List[int]]:
|
| 475 |
+
if combo_lvl == 0 and tokens_to_perturb == "all":
|
| 476 |
+
if perturb_type in ["overexpress", "activate"]:
|
| 477 |
+
range_start = 1
|
| 478 |
+
elif perturb_type in ["delete", "inhibit"]:
|
| 479 |
+
range_start = 0
|
| 480 |
+
range_start += 1 # Starting after the CLS token
|
| 481 |
+
indices_to_perturb = [
|
| 482 |
+
[i] for i in range(range_start, example_cell["length"][0]-1) # And excluding the EOS token
|
| 483 |
+
]
|
| 484 |
+
|
| 485 |
+
# elif combo_lvl > 0 and anchor_token is None:
|
| 486 |
+
## to implement
|
| 487 |
+
elif combo_lvl > 0 and (anchor_token is not None):
|
| 488 |
+
example_input_ids = example_cell["input_ids"][0]
|
| 489 |
+
anchor_index = example_input_ids.index(anchor_token[0])
|
| 490 |
+
indices_to_perturb = [
|
| 491 |
+
sorted([anchor_index, i]) if i != anchor_index else None
|
| 492 |
+
for i in range(1, example_cell["length"][0]-1) # Exclude CLS and EOS tokens
|
| 493 |
+
]
|
| 494 |
+
indices_to_perturb = [item for item in indices_to_perturb if item is not None]
|
| 495 |
+
else:
|
| 496 |
+
example_input_ids = example_cell["input_ids"][0]
|
| 497 |
+
indices_to_perturb = [
|
| 498 |
+
[example_input_ids.index(token)] if token in example_input_ids else None
|
| 499 |
+
for token in tokens_to_perturb
|
| 500 |
+
]
|
| 501 |
+
indices_to_perturb = [item for item in indices_to_perturb if item is not None]
|
| 502 |
+
|
| 503 |
+
# create all permutations of combo_lvl of modifiers from tokens_to_perturb
|
| 504 |
+
if combo_lvl > 0 and (anchor_token is None):
|
| 505 |
+
if tokens_to_perturb != "all":
|
| 506 |
+
if len(tokens_to_perturb) == combo_lvl + 1:
|
| 507 |
+
indices_to_perturb = [
|
| 508 |
+
list(x) for x in it.combinations(indices_to_perturb, combo_lvl + 1)
|
| 509 |
+
]
|
| 510 |
+
else:
|
| 511 |
+
all_indices = [[i] for i in range(1, example_cell["length"][0]-1)] # Exclude CLS and EOS tokens
|
| 512 |
+
all_indices = [
|
| 513 |
+
index for index in all_indices if index not in indices_to_perturb
|
| 514 |
+
]
|
| 515 |
+
indices_to_perturb = [
|
| 516 |
+
[[j for i in indices_to_perturb for j in i], x] for x in all_indices
|
| 517 |
+
]
|
| 518 |
+
|
| 519 |
+
length = len(indices_to_perturb)
|
| 520 |
+
perturbation_dataset = Dataset.from_dict(
|
| 521 |
+
{
|
| 522 |
+
"input_ids": example_cell["input_ids"] * length,
|
| 523 |
+
"perturb_index": indices_to_perturb,
|
| 524 |
+
}
|
| 525 |
+
)
|
| 526 |
+
|
| 527 |
+
if length < 400:
|
| 528 |
+
num_proc_i = 1
|
| 529 |
+
else:
|
| 530 |
+
num_proc_i = num_proc
|
| 531 |
+
|
| 532 |
+
if perturb_type == "delete":
|
| 533 |
+
perturbation_dataset = perturbation_dataset.map(
|
| 534 |
+
delete_indices, num_proc=num_proc_i
|
| 535 |
+
)
|
| 536 |
+
elif perturb_type == "overexpress":
|
| 537 |
+
perturbation_dataset = perturbation_dataset.map(
|
| 538 |
+
overexpress_indices_special, num_proc=num_proc_i
|
| 539 |
+
)
|
| 540 |
+
|
| 541 |
+
perturbation_dataset = perturbation_dataset.map(measure_length, num_proc=num_proc_i)
|
| 542 |
+
|
| 543 |
+
return perturbation_dataset, indices_to_perturb
|
| 544 |
+
|
| 545 |
+
|
| 546 |
+
# original cell emb removing the activated/overexpressed/inhibited gene emb
|
| 547 |
# so that only non-perturbed gene embeddings are compared to each other
|
| 548 |
# in original or perturbed context
|
| 549 |
def make_comparison_batch(original_emb_batch, indices_to_perturb, perturb_group):
|
|
|
|
| 740 |
cos = torch.nn.CosineSimilarity(dim=1)
|
| 741 |
|
| 742 |
# if emb_mode == "gene", can only calculate gene cos sims
|
| 743 |
+
# against original cell
|
| 744 |
if cell_states_to_model is None or emb_mode == "gene":
|
| 745 |
cos_sims = cos(perturbation_emb, original_emb).to("cuda")
|
| 746 |
+
|
| 747 |
elif cell_states_to_model is not None and emb_mode == "cell":
|
| 748 |
possible_states = get_possible_states(cell_states_to_model)
|
| 749 |
cos_sims = dict(zip(possible_states, [[] for _ in range(len(possible_states))]))
|
|
|
|
| 910 |
return self.ens_to_symbol(self.token_to_ens(token))
|
| 911 |
|
| 912 |
def symbol_to_token(self, symbol):
|
| 913 |
+
return self.ens_to_token(self.symbol_to_ens(symbol))
|
geneformer/pretrainer.py
CHANGED
|
@@ -32,8 +32,6 @@ from transformers.training_args import ParallelMode
|
|
| 32 |
from transformers.utils import is_tf_available, is_torch_available, logging, to_py_obj
|
| 33 |
from transformers.utils.generic import _is_tensorflow, _is_torch
|
| 34 |
|
| 35 |
-
from . import TOKEN_DICTIONARY_FILE
|
| 36 |
-
|
| 37 |
logger = logging.get_logger(__name__)
|
| 38 |
EncodedInput = List[int]
|
| 39 |
VERY_LARGE_INTEGER = int(
|
|
@@ -52,9 +50,6 @@ _is_torch_generator_available = False
|
|
| 52 |
if version.parse(torch.__version__) >= version.parse("1.6"):
|
| 53 |
_is_torch_generator_available = True
|
| 54 |
|
| 55 |
-
with open(TOKEN_DICTIONARY_FILE, "rb") as f:
|
| 56 |
-
token_dictionary = pickle.load(f)
|
| 57 |
-
|
| 58 |
|
| 59 |
class ExplicitEnum(Enum):
|
| 60 |
"""
|
|
@@ -109,15 +104,7 @@ class GeneformerPreCollator(SpecialTokensMixin):
|
|
| 109 |
super().__init__(mask_token="<mask>", pad_token="<pad>")
|
| 110 |
|
| 111 |
self.token_dictionary = kwargs.get("token_dictionary")
|
| 112 |
-
# self.mask_token = "<mask>"
|
| 113 |
-
# self.mask_token_id = self.token_dictionary.get("<mask>")
|
| 114 |
-
# self.pad_token = "<pad>"
|
| 115 |
-
# self.pad_token_id = self.token_dictionary.get("<pad>")
|
| 116 |
self.padding_side = "right"
|
| 117 |
-
# self.all_special_ids = [
|
| 118 |
-
# self.token_dictionary.get("<mask>"),
|
| 119 |
-
# self.token_dictionary.get("<pad>"),
|
| 120 |
-
# ]
|
| 121 |
self.model_input_names = ["input_ids"]
|
| 122 |
|
| 123 |
def convert_ids_to_tokens(self, value):
|
|
|
|
| 32 |
from transformers.utils import is_tf_available, is_torch_available, logging, to_py_obj
|
| 33 |
from transformers.utils.generic import _is_tensorflow, _is_torch
|
| 34 |
|
|
|
|
|
|
|
| 35 |
logger = logging.get_logger(__name__)
|
| 36 |
EncodedInput = List[int]
|
| 37 |
VERY_LARGE_INTEGER = int(
|
|
|
|
| 50 |
if version.parse(torch.__version__) >= version.parse("1.6"):
|
| 51 |
_is_torch_generator_available = True
|
| 52 |
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
class ExplicitEnum(Enum):
|
| 55 |
"""
|
|
|
|
| 104 |
super().__init__(mask_token="<mask>", pad_token="<pad>")
|
| 105 |
|
| 106 |
self.token_dictionary = kwargs.get("token_dictionary")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
self.padding_side = "right"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
self.model_input_names = ["input_ids"]
|
| 109 |
|
| 110 |
def convert_ids_to_tokens(self, value):
|
geneformer/token_dictionary.pkl
DELETED
|
Binary file (788 kB)
|
|
|
geneformer/token_dictionary_gc95M.pkl
CHANGED
|
Binary files a/geneformer/token_dictionary_gc95M.pkl and b/geneformer/token_dictionary_gc95M.pkl differ
|
|
|
generation_config.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_from_model_config": true,
|
| 3 |
+
"pad_token_id": 0,
|
| 4 |
+
"transformers_version": "4.37.1"
|
| 5 |
+
}
|
{geneformer-12L-30M β gf-12L-30M-i2048}/config.json
RENAMED
|
File without changes
|
{geneformer-12L-30M β gf-12L-30M-i2048}/pytorch_model.bin
RENAMED
|
File without changes
|
{geneformer-12L-30M β gf-12L-30M-i2048}/training_args.bin
RENAMED
|
File without changes
|
gf-12L-95M-i4096/config.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"BertForMaskedLM"
|
| 4 |
+
],
|
| 5 |
+
"attention_probs_dropout_prob": 0.02,
|
| 6 |
+
"classifier_dropout": null,
|
| 7 |
+
"hidden_act": "relu",
|
| 8 |
+
"hidden_dropout_prob": 0.02,
|
| 9 |
+
"hidden_size": 512,
|
| 10 |
+
"initializer_range": 0.02,
|
| 11 |
+
"intermediate_size": 1024,
|
| 12 |
+
"layer_norm_eps": 1e-12,
|
| 13 |
+
"max_position_embeddings": 4096,
|
| 14 |
+
"model_type": "bert",
|
| 15 |
+
"num_attention_heads": 8,
|
| 16 |
+
"num_hidden_layers": 12,
|
| 17 |
+
"pad_token_id": 0,
|
| 18 |
+
"position_embedding_type": "absolute",
|
| 19 |
+
"torch_dtype": "float32",
|
| 20 |
+
"transformers_version": "4.37.1",
|
| 21 |
+
"type_vocab_size": 2,
|
| 22 |
+
"use_cache": true,
|
| 23 |
+
"vocab_size": 20275
|
| 24 |
+
}
|
gf-12L-95M-i4096/generation_config.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_from_model_config": true,
|
| 3 |
+
"pad_token_id": 0,
|
| 4 |
+
"transformers_version": "4.37.1"
|
| 5 |
+
}
|
gf-12L-95M-i4096/model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4365ba23e393fcfa0e65a94ac64a0983cd788bd23a8d4914f4ab66f85cfe043c
|
| 3 |
+
size 152012980
|
gf-12L-95M-i4096/training_args.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:21a45980734b138029422e95a5601def858821a9ec02cd473938b9f525ac108d
|
| 3 |
+
size 4920
|
gf-12L-95M-i4096_CLcancer/config.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_name_or_path": "/gladstone/theodoris/lab/pretrained_models/encoder/240402_194213_geneformer_94M_L12_emb512_SL4096_E3_B4_LR0.0005_LScosine_WU5000_Oadamw_DS8/models",
|
| 3 |
+
"architectures": [
|
| 4 |
+
"BertForMaskedLM"
|
| 5 |
+
],
|
| 6 |
+
"attention_probs_dropout_prob": 0.02,
|
| 7 |
+
"classifier_dropout": null,
|
| 8 |
+
"hidden_act": "relu",
|
| 9 |
+
"hidden_dropout_prob": 0.02,
|
| 10 |
+
"hidden_size": 512,
|
| 11 |
+
"initializer_range": 0.02,
|
| 12 |
+
"intermediate_size": 1024,
|
| 13 |
+
"layer_norm_eps": 1e-12,
|
| 14 |
+
"max_position_embeddings": 4096,
|
| 15 |
+
"model_type": "bert",
|
| 16 |
+
"num_attention_heads": 8,
|
| 17 |
+
"num_hidden_layers": 12,
|
| 18 |
+
"pad_token_id": 0,
|
| 19 |
+
"position_embedding_type": "absolute",
|
| 20 |
+
"torch_dtype": "float32",
|
| 21 |
+
"transformers_version": "4.37.1",
|
| 22 |
+
"type_vocab_size": 2,
|
| 23 |
+
"use_cache": true,
|
| 24 |
+
"vocab_size": 20275
|
| 25 |
+
}
|
gf-12L-95M-i4096_CLcancer/generation_config.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_from_model_config": true,
|
| 3 |
+
"pad_token_id": 0,
|
| 4 |
+
"transformers_version": "4.37.1"
|
| 5 |
+
}
|
gf-12L-95M-i4096_CLcancer/model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2451adeed240c165634fea60ccba17063da8a2843ea9fcdcc0ce185720bf0dc2
|
| 3 |
+
size 152012980
|
gf-12L-95M-i4096_CLcancer/training_args.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:37074f3ea62a6ba0a312c38526c20c2dccbb068a2c7ee8c7c73b435dd90ab7b1
|
| 3 |
+
size 5048
|