본문 바로가기

scFoundation

[코드 해석] Geneformer

HuggingFace 참고

https://huggingface.co/ctheodoris/Geneformer/tree/main

 

ctheodoris/Geneformer at main

 

huggingface.co

 

요구사항은 requirements.txt 참고(패키지 설치)

 

모델 관련 폴더

Geneformer/

├── Geneformer-V1-10M
├── Geneformer-V2-104M
├── Geneformer-V2-104M_CLcancer  // 암 세포 데이터에 대해 특화된 pretrained 버전으로 보임 
├── Geneformer-V2-316M

pretrained 된 모델 가중치가 저장됨.

V1, V2는 버전, 10M, 104M..등은 파라미터 개수

 

<각 폴더 내부 구조> 

config.json: 모델 구조 설정(hidden_size, hidden_dropout_prob, model_type, num_attention_heads 등)

model.safetensors: pytorch_model.bin의 안전한 버전 

pytorch_model.bin: pytorch 형식의 실제 모델 weight 파일

training_args.bin: 모델 사전학습시에 사용된 하이퍼파라미터 

 

 

모델 아키텍쳐, 코드

cf. .pkl 파일: python pickle 포맷, 직렬화된 데이터를 담는 바이너리 파일

 

geneformer/ (일부만 표시)

├── gene_dictionaries_30m   // v1 모델 -> Geneformer V1 10M에 매핑?되는 파일들 

          ├── ensembl_mapping_dict_gc30M.pkl        // 유전자ID <-> 기타 ID 매핑 정보

          ├── gene_median_dictionary_gc30M.pkl      // 각 유전자의 median expression 정보(정규화에 사용)

          ├── gene_name_id_dict_gc30M.pkl              // 유전자 이름 <-> 내부 ID 매핑

          ├── token_dictionary_gc30M.pkl                   // 모델에 입력되는 토큰과 ID 간의 매핑
├── classifier.py              // 모델 정의 

├── classifier_utils.py        // classifier 보조 함수

├── mtl_classifier.py        // 멀티태스크 학습을 위한 classifier 모델 구조를 정의
├── tokenizer.py               // rank encoding 
├── emb_extractor.py           // 임베딩 추출 

├── collator_for_classification.py          // 전처리 도우미(?)
├── perturber_utils.py         // perturbation 실험용 
├── in_silico_perturber.py     // 유전자 삭제 실험

├── in_silico_perturber_stats.py     // in_silico_perturber.py 유전자를 삭제하거나 활성화한 뒤, 모델 임베딩의 변화량을 저장하면 이 파일이 그 차이를 정량화하고 통계적으로 유의한지 분석
├── pretrainer.py              // 사전 학습 
├── evaluation_utils.py        // 성능 평가 함수

├── ensembl_mapping_dict_gc104M.pkl

├── gene_median_dictionary_gc104M.pkl  // rank normalization 참조용

├── gene_name_id_dict_gc30M.pkl              // 유전자 이름 <-> 내부 ID 매핑

├── token_dictionary_gc30M.pkl                   // 모델에 입력되는 토큰과 ID 간의 매핑

 

 

데이터 전처리 & rank encoding - tokenizer.py

gene vector 배열을 만들고, 발현량이 0이 아닌 유전자만 토큰화 

rank_genes()에서 발현량이 높은 유전자가 시퀀스의 앞쪽에 위치하도록 토큰 정렬

def rank_genes(gene_vector, gene_tokens):
    # sort by median-scaled gene values
    sorted_indices = np.argsort(-gene_vector)
    return gene_tokens[sorted_indices]
    
    
def tokenize_cell(gene_vector, gene_tokens):
    # create array of gene vector with token indices
    # mask undetected genes
    nonzero_mask = np.nonzero(gene_vector)[0]
    # rank by median-scaled gene values
    return rank_genes(gene_vector[nonzero_mask], gene_tokens[nonzero_mask])

 

TranscriptomeTokenizer 클래스에서는, 파일형식이 .h5ad, .loom이냐에 따라 나누어서 토큰화 진행

실제 유전자 데이터 파일을 열고, 발현값 정규화 → 유전자별 rank 변환 → token list 생성 

✨ 논문에서 ranking매길 시에 유전자들의 발현량을 절대적으로 비교하는게 아니라, 특정 유전자의 발현량을 전체 세포에서의 그 유전자의 발현량의 정규화값(평균?중앙값?)과 비교해서 상대적으로 ranking한다고 언급 ... → 코드에서 확인 완료 

class TranscriptomeTokenizer:
# 생략
	def tokenize_anndata(self, adata_file_path, target_sum=10_000):
        adata = sum_ensembl_ids(
            adata_file_path,
            self.collapse_gene_ids,
            self.gene_mapping_dict,
            self.gene_token_dict,
            self.custom_attr_name_dict,
            self.use_h5ad_index,
            file_format="h5ad",
            chunk_size=self.chunk_size,
        )
        # 생략
        for i in range(0, len(filter_pass_loc), self.chunk_size):
            idx = filter_pass_loc[i : i + self.chunk_size]

            n_counts = adata[idx].obs["n_counts"].values[:, None]
            X_view0 = adata[idx, :].X
            X_view = X_view0[:, coding_miRNA_loc]
            X_norm_unscaled = X_view / n_counts * target_sum
            
            # X_norm_unscaled: 각 세포의 유전자 발현값을 정규화 
            # norm_factor_vector: gene_median_dict에서 가져온 각 유전자의 전체 cell에서의 발현량 median
            X_norm = X_norm_unscaled / norm_factor_vector
            X_norm = sp.csr_matrix(X_norm)
            X_norm_unscaled = sp.csr_matrix(X_norm_unscaled)

            tokenized_cells += [
                rank_genes(X_norm[i].data, coding_miRNA_tokens[X_norm[i].indices])
                for i in range(X_norm.shape[0])
            ]

            if self.keep_counts:
                X_norm_unscaled = sp.csr_matrix(X_norm_unscaled)
                tokenized_counts += [
                    rank_genes(X_norm[i].data, X_norm_unscaled[i].data)
                    for i in range(X_norm.shape[0])
                ]

            # add custom attributes for subview to dict
            if self.custom_attr_name_dict is not None:
                for k in file_cell_metadata.keys():
                    file_cell_metadata[k] += adata[idx].obs[k].tolist()
            else:
                file_cell_metadata = None

 

 

모델 아키텍쳐 - classifier.py

classifier는 cell, gene으로 나뉨.

사용 예시는 아래 Examples 참고

 

아래는 model을 불러오고 training 파라미터 불러오는 예시. + fine-tuning

from . import perturber_utils as pu

##### Load model and training args #####
        model = pu.load_model(
            self.model_type,
            num_classes,
            model_directory,
            "train",
            quantize=self.quantize,
        )
        def_training_args, def_freeze_layers = cu.get_default_train_args(
            model, self.classifier, train_data, output_directory
        )
        del model

        if self.training_args is not None:
            def_training_args.update(self.training_args)
        logging_steps = round(
            len(train_data) / def_training_args["per_device_train_batch_size"] / 10
        )
        def_training_args["logging_steps"] = logging_steps
        def_training_args["output_dir"] = output_directory
        if eval_data is None:
            def_training_args["evaluation_strategy"] = "no"
            def_training_args["load_best_model_at_end"] = False
        if transformers_version >= parse("4.46"):
            def_training_args["eval_strategy"] = def_training_args.pop("evaluation_strategy")
        def_training_args.update(
            {"save_strategy": "epoch", "save_total_limit": 1}
        )  # only save last model for each run
        training_args_init = TrainingArguments(**def_training_args)

        ##### Fine-tune the model #####
        # define the data collator
        if self.classifier == "cell":
            data_collator = DataCollatorForCellClassification(
                token_dictionary=self.gene_token_dict
            )
        elif self.classifier == "gene":
            data_collator = DataCollatorForGeneClassification(
                token_dictionary=self.gene_token_dict
            )

 

 

pertuber_utils.py

from transformers import (
    BertForMaskedLM,
    BertForSequenceClassification,
    BertForTokenClassification,
    BitsAndBytesConfig,
)


# load model to GPU
def load_model(model_type, num_classes, model_directory, mode, quantize=False):
    if model_type == "Pretrained-Quantized":
        inference_only = True
        model_type = "Pretrained"
        quantize = True
    elif model_type == "MTLCellClassifier-Quantized":
        inference_only = True
        model_type = "MTLCellClassifier"
        quantize = True
    else:
        inference_only = False

    output_hidden_states = (mode == "eval")

    # Quantization logic
    if isinstance(quantize, dict):
        quantize_config = quantize.get("bnb_config", None)
        peft_config = quantize.get("peft_config", None)
    elif quantize:
        if inference_only:
            quantize_config = BitsAndBytesConfig(load_in_8bit=True)
            peft_config = None
        else:
            quantize_config = BitsAndBytesConfig(
                load_in_4bit=True,
                bnb_4bit_use_double_quant=True,
                bnb_4bit_quant_type="nf4",
                bnb_4bit_compute_dtype=torch.bfloat16,
            )
            try:
                peft_config = LoraConfig(
                    lora_alpha=128,
                    lora_dropout=0.1,
                    r=64,
                    bias="none",
                    task_type="TokenClassification", 
                )
            except ValueError as e:
                peft_config = LoraConfig(
                    lora_alpha=128,
                    lora_dropout=0.1,
                    r=64,
                    bias="none",
                    task_type="TOKEN_CLS",
                )
    else:
        quantize_config = None
        peft_config = None

    # Model class selection
    model_classes = {
        "Pretrained": BertForMaskedLM,
        "GeneClassifier": BertForTokenClassification,
        "CellClassifier": BertForSequenceClassification,
        "MTLCellClassifier": BertForMaskedLM
    }

    model_class = model_classes.get(model_type)
    if not model_class:
        raise ValueError(f"Unknown model type: {model_type}")

    # Model loading
    model_args = {
        "pretrained_model_name_or_path": model_directory,
        "output_hidden_states": output_hidden_states,
        "output_attentions": False,
    }

    if model_type != "Pretrained":
        model_args["num_labels"] = num_classes

    if quantize_config:
        model_args["quantization_config"] = quantize_config

    # Load the model
    model = model_class.from_pretrained(**model_args)

    if mode == "eval":
        model.eval()

Transformer 구조 기반인 BERT를 로딩하고 있는 것으로 확인됨

 

BertModel         ← BERT base (encoder-only transformer)
├── BertForMaskedLM            ← [BERT + MLM head] → Pretraining용
├── BertForSequenceClassification  ← [BERT + classification head]
├── BertForTokenClassification     ← [BERT + token classification head]

 

⁉️BERT에 대해서도 공부해봐야할듯...

 

Examples(downstream task 적용)

examples/

├── cell_classification.ipynb               // 세포단위로 질병 상태 분류
├── distributed_multitask_cell_classification.ipynb             // multi-task learning ex) 세포 타입, 질병 상태 동시에 분류 
├── extract_and_plot_cell_embeddings.ipynb              // 임베딩 추출, 시각화 예제 코드 
├── gene_classification.ipynb                 // dosage sensitive vs insensitive TFs 분류(논문 참고)
├── in_silico_pertubation_ipynb          // 어떤 유전자의 삭제가 dcm -> 정상 상태로 임베딩을 shifting 시키는 지 알아내는 예제
├── multitask_cell_classification.ipynb           // how to use the Geneformer multi-task cell classifier 

├── tokenizing_scRNAseq_data.ipynb           // .loom, .h5ad scRNA-seq 데이터를 rank value encoding .dataset으로 토큰화하는 코드 

 

 

<cell_classification.ipynb>

cardiomyopathy 질병 상태(nf/hcm/dcm) 를 세포 단위에서 예측하도록 Geneformer를 fine-tune하는 예시 

from geneformer import Classifier

cc = Classifier(classifier="cell",
                cell_state_dict = {"state_key": "disease", "states": "all"},
                filter_data=filter_data_dict,
                training_args=training_args,
                max_ncells=None,
                freeze_layers = 2,
                num_crossval_splits = 1,
                forward_batch_size=200,
                model_version="V1",  # OF NOTE: SET TO V1 MODEL, PROVIDE V1 MODEL PATH IN SUBSEQUENT CODE
                nproc=16)

먼저, geneformer에서 Classifier 불러오고,

Classifier 불러올 때 classifier="cell"로 정의

이때, 학습 파라미터는 pretrained된 값 사용

 

cc.prepare_data(input_data_file="/path/to/human_dcm_hcm_nf_2048_w_length.dataset",
                output_directory=output_dir,
                output_prefix=output_prefix,
                split_id_dict=train_test_id_split_dict)

데이터 준비: dataset을 train/valid/test로 나누고 전처리 진행 

 

all_metrics = cc.validate(model_directory="/path/to/Geneformer",  # OF NOTE: SET TO V1 MODEL ABOVE, PROVIDE V1 MODEL PATH HERE
                          prepared_input_data_file=f"{output_dir}/{output_prefix}_labeled_train.dataset",
                          id_class_dict_file=f"{output_dir}/{output_prefix}_id_class_dict.pkl",
                          output_directory=output_dir,
                          output_prefix=output_prefix,
                          split_id_dict=train_valid_id_split_dict)

사전학습모델(v1 혹은 v2) 사용하여 학습 + 검증 진행

즉, fine-tuning 진행

 

all_metrics_test = cc.evaluate_saved_model(
        model_directory=f"{output_dir}/{datestamp_min}_geneformer_cellClassifier_{output_prefix}/ksplit1/",
        id_class_dict_file=f"{output_dir}/{output_prefix}_id_class_dict.pkl",
        test_data_file=f"{output_dir}/{output_prefix}_labeled_test.dataset",
        output_directory=output_dir,
        output_prefix=output_prefix,
    )

테스트 성능 평가 (evaluate_saved_model())로 테스트셋 평가) 

 

 

 

<gene_classification.ipynb>

Dosage-sensitive vs. -Insensitive Transcription Factors(TFs)

from geneformer import Classifier

cc = Classifier(classifier="gene",
                gene_class_dict = gene_class_dict,
                max_ncells = 10_000,
                freeze_layers = 4,
                num_crossval_splits = 5,
                forward_batch_size=200,
                model_version="V1",  # OF NOTE: SET TO V1 MODEL, PROVIDE V1 MODEL PATH IN SUBSEQUENT CODE
                nproc=16)

먼저, geneformer에서 Classifier 불러오고,

Classifier 불러올 때 classifier="gene"로 정의

이때, 학습 파라미터는 pretrained된 값 사용

 

cc.prepare_data(input_data_file="/path/to/gc-30M_sample50k.dataset",
                output_directory=output_dir,
                output_prefix=output_prefix)

데이터 준비: dataset을 train/valid/test로 나누고 전처리 진행 

 

# V1 model: https://huggingface.co/ctheodoris/Geneformer/blob/main/Geneformer-V1-10M/model.safetensors
all_metrics = cc.validate(model_directory="/path/to/Geneformer",  # OF NOTE: SET TO V1 MODEL ABOVE, PROVIDE V1 MODEL PATH HERE
                          prepared_input_data_file=f"{output_dir}/{output_prefix}_labeled.dataset",
                          id_class_dict_file=f"{output_dir}/{output_prefix}_id_class_dict.pkl",
                          output_directory=output_dir,
                          output_prefix=output_prefix)

사전학습모델(v1 혹은 v2) 사용하여 학습 + 검증 진행

즉, fine-tuning 진행; 이 예제에서는 5-fold cross-validation 진행