Spaces:
Sleeping
Sleeping
| import torch | |
| import numpy as np | |
| from transformers import AutoTokenizer, AutoModel | |
| from typing import List | |
| from app.config import ( | |
| MODEL_PATH, GO_TERMS_PATH, | |
| ESM2_MODEL_NAME, ESM2_DIM, HIDDEN_DIM, NUM_LABELS, | |
| THRESHOLD, WINDOW_SIZE, WINDOW_STRIDE, | |
| ) | |
| from app.model import ProteinGNN | |
| from app.schemas import PredictionResponse, ProteinResult, GOTermPrediction | |
| from utils.fasta_parser import parse_fasta, sliding_window | |
| class ProteinPredictor: | |
| def __init__(self): | |
| self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| self.gnn: ProteinGNN = None | |
| self.tokenizer = None | |
| self.esm_model = None | |
| self.go_terms: List[str] = [] | |
| self.is_ready = False | |
| # ββ Load ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def load(self): | |
| # 1. Load GO terms list (format: GO:ID \t name \t ia_weight) | |
| if not GO_TERMS_PATH.exists(): | |
| raise FileNotFoundError(f"GO terms file not found at {GO_TERMS_PATH}") | |
| with open(GO_TERMS_PATH) as f: | |
| self.go_terms = [line.strip().split("\t")[0] for line in f if line.strip()] | |
| print(f" β GO terms loaded: {len(self.go_terms)}") | |
| # 2. Load GNN weights | |
| if not MODEL_PATH.exists(): | |
| raise FileNotFoundError(f"Model file not found at {MODEL_PATH}") | |
| self.gnn = ProteinGNN(ESM2_DIM, HIDDEN_DIM, NUM_LABELS).to(self.device) | |
| checkpoint = torch.load(MODEL_PATH, map_location=self.device, weights_only=False) | |
| checkpoint = torch.load(MODEL_PATH, map_location=self.device, weights_only=False) | |
| state = (checkpoint.get("model_state_dict") | |
| or checkpoint.get("model_state") | |
| or checkpoint | |
| ) | |
| if any(k.startswith("gnn.") for k in state.keys()): | |
| state = {k[4:]: v for k, v in state.items() if k.startswith("gnn.")} | |
| self.gnn.load_state_dict(state) | |
| self.gnn.eval() | |
| print(f" β GNN model loaded from {MODEL_PATH}") | |
| # 3. Load ESM2 tokenizer + model | |
| print(f" π Loading ESM2 ({ESM2_MODEL_NAME}) β this may take a moment...") | |
| self.tokenizer = AutoTokenizer.from_pretrained(ESM2_MODEL_NAME) | |
| self.esm_model = AutoModel.from_pretrained(ESM2_MODEL_NAME).to(self.device) | |
| self.esm_model.eval() | |
| print(" β ESM2 ready") | |
| self.is_ready = True | |
| # ββ Public API ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def predict(self, fasta_text: str) -> PredictionResponse: | |
| proteins = parse_fasta(fasta_text) | |
| results = [self._predict_one(pid, seq) for pid, seq in proteins] | |
| return PredictionResponse(results=results, total_proteins=len(results)) | |
| # ββ Internal ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _predict_one(self, protein_id: str, sequence: str) -> ProteinResult: | |
| # 1. ESM2 embedding (with sliding window for long sequences) | |
| embedding = self._get_esm2_embedding(sequence) # (1280,) | |
| # 2. Inductive GNN inference (classifier only, no graph) | |
| with torch.no_grad(): | |
| x = embedding.unsqueeze(0).to(self.device) # (1, 1280) | |
| logits = self.gnn.forward_inductive(x) # (1, 4201) | |
| probs = torch.sigmoid(logits).squeeze(0).cpu().numpy() # (4201,) | |
| # 3. Apply threshold | |
| predicted_indices = np.where(probs >= THRESHOLD)[0] | |
| go_predictions = [ | |
| GOTermPrediction( | |
| go_term=self.go_terms[i], | |
| score=round(float(probs[i]), 4), | |
| ) | |
| for i in predicted_indices | |
| ] | |
| # Sort by confidence descending | |
| go_predictions.sort(key=lambda x: x.score, reverse=True) | |
| return ProteinResult( | |
| protein_id=protein_id, | |
| sequence_length=len(sequence), | |
| predicted_go_terms=go_predictions, | |
| num_predictions=len(go_predictions), | |
| ) | |
| def _get_esm2_embedding(self, sequence: str) -> torch.Tensor: | |
| """ | |
| Get ESM2 mean-pooled embedding. | |
| Uses sliding window + averaging for sequences longer than WINDOW_SIZE. | |
| """ | |
| windows = sliding_window(sequence, WINDOW_SIZE, WINDOW_STRIDE) | |
| window_embeddings = [] | |
| for window_seq in windows: | |
| inputs = self.tokenizer( | |
| window_seq, | |
| return_tensors="pt", | |
| truncation=True, | |
| max_length=WINDOW_SIZE + 2, # +2 for [CLS] and [EOS] | |
| padding=False, | |
| ) | |
| inputs = {k: v.to(self.device) for k, v in inputs.items()} | |
| with torch.no_grad(): | |
| outputs = self.esm_model(**inputs) | |
| # Mean-pool over sequence tokens (exclude [CLS] and [EOS]) | |
| hidden = outputs.last_hidden_state[0, 1:-1, :] # (seq_len, 1280) | |
| emb = hidden.mean(dim=0) # (1280,) | |
| window_embeddings.append(emb) | |
| # Average all window embeddings | |
| final_embedding = torch.stack(window_embeddings).mean(dim=0) | |
| return final_embedding | |