ccnasef-cyber
Add protein function prediction API
3d5b11c
Raw
History Blame Contribute Delete
5.58 kB
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