Spaces:
Sleeping
Sleeping
Add /downstream endpoint serving 8 fine-tuned task heads per backbone
Browse files
app.py
CHANGED
|
@@ -7,6 +7,7 @@ Endpoints:
|
|
| 7 |
POST /predict β per-position 20-AA softmax over the sequence
|
| 8 |
POST /embed β mean-pooled last-layer embedding
|
| 9 |
POST /mutation β wildtype-marginal Ξlog-likelihood matrix [L, 20]
|
|
|
|
| 10 |
"""
|
| 11 |
|
| 12 |
import os
|
|
@@ -17,7 +18,7 @@ import torch.nn.functional as F
|
|
| 17 |
from fastapi import FastAPI, HTTPException
|
| 18 |
from fastapi.middleware.cors import CORSMiddleware
|
| 19 |
from pydantic import BaseModel, Field
|
| 20 |
-
from transformers import EsmForMaskedLM, EsmTokenizer
|
| 21 |
|
| 22 |
|
| 23 |
MODEL_REGISTRY: Dict[str, str] = {
|
|
@@ -26,11 +27,46 @@ MODEL_REGISTRY: Dict[str, str] = {
|
|
| 26 |
"miplm-msa": "HUBioDataLab/esm2-8m-msa",
|
| 27 |
}
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
STANDARD_AAS = "ACDEFGHIKLMNPQRSTVWY"
|
| 30 |
MAX_LEN = 1024
|
| 31 |
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| 32 |
|
| 33 |
_models: Dict[str, Tuple[EsmForMaskedLM, EsmTokenizer, List[int]]] = {}
|
|
|
|
| 34 |
|
| 35 |
|
| 36 |
def get_model(name: str):
|
|
@@ -100,6 +136,10 @@ def _warmup():
|
|
| 100 |
def root():
|
| 101 |
return {
|
| 102 |
"models": list(MODEL_REGISTRY.keys()),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
"device": DEVICE,
|
| 104 |
"max_length": MAX_LEN,
|
| 105 |
"aa_order": STANDARD_AAS,
|
|
@@ -157,3 +197,95 @@ def mutation(req: _SeqRequest):
|
|
| 157 |
wt_logp = log_probs.gather(1, wt_idx[:, None]) # [L, 1]
|
| 158 |
delta = (log_probs - wt_logp).tolist()
|
| 159 |
return MutationResponse(model=req.model, sequence=seq, aa_order=STANDARD_AAS, scores=delta)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
POST /predict β per-position 20-AA softmax over the sequence
|
| 8 |
POST /embed β mean-pooled last-layer embedding
|
| 9 |
POST /mutation β wildtype-marginal Ξlog-likelihood matrix [L, 20]
|
| 10 |
+
POST /downstream β all fine-tuned task heads (classification + regression)
|
| 11 |
"""
|
| 12 |
|
| 13 |
import os
|
|
|
|
| 18 |
from fastapi import FastAPI, HTTPException
|
| 19 |
from fastapi.middleware.cors import CORSMiddleware
|
| 20 |
from pydantic import BaseModel, Field
|
| 21 |
+
from transformers import EsmForMaskedLM, EsmForSequenceClassification, EsmTokenizer
|
| 22 |
|
| 23 |
|
| 24 |
MODEL_REGISTRY: Dict[str, str] = {
|
|
|
|
| 27 |
"miplm-msa": "HUBioDataLab/esm2-8m-msa",
|
| 28 |
}
|
| 29 |
|
| 30 |
+
|
| 31 |
+
# Downstream heads β one HF repo per backbone, with 8 task subfolders each.
|
| 32 |
+
# Map frontend backbone name β HF repo.
|
| 33 |
+
DOWNSTREAM_REGISTRY: Dict[str, str] = {
|
| 34 |
+
"miplm-ce": "HUBioDataLab/miplm-ce-tasks",
|
| 35 |
+
"miplm-blosum": "HUBioDataLab/miplm-blosum-tasks",
|
| 36 |
+
"miplm-msa": "HUBioDataLab/miplm-msa-tasks",
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
# Task metadata. `kind` drives post-processing:
|
| 40 |
+
# single_label β softmax β top-K classes with probabilities
|
| 41 |
+
# multi_label β sigmoid β top-K classes with independent probabilities
|
| 42 |
+
# regression β raw scalar
|
| 43 |
+
# `labels` is the human-readable class vocabulary (when known). When unset the
|
| 44 |
+
# response falls back to numeric class indices.
|
| 45 |
+
DEEPLOC10_LABELS = [
|
| 46 |
+
"Cytoplasm", "Nucleus", "Extracellular", "Cell membrane",
|
| 47 |
+
"Endoplasmic reticulum", "Plastid", "Golgi apparatus",
|
| 48 |
+
"Lysosome/Vacuole", "Mitochondrion", "Peroxisome",
|
| 49 |
+
]
|
| 50 |
+
DEEPLOC2_LABELS = ["Soluble", "Membrane"]
|
| 51 |
+
METAL_LABELS = ["Non-binder", "Metal-ion binder"]
|
| 52 |
+
|
| 53 |
+
DOWNSTREAM_TASKS: Dict[str, Dict] = {
|
| 54 |
+
"deeploc-cls10": {"kind": "single_label", "labels": DEEPLOC10_LABELS, "title": "Subcellular localization (10-way)"},
|
| 55 |
+
"deeploc-cls2": {"kind": "single_label", "labels": DEEPLOC2_LABELS, "title": "Soluble vs membrane"},
|
| 56 |
+
"metalionbinding": {"kind": "single_label", "labels": METAL_LABELS, "title": "Metal-ion binding"},
|
| 57 |
+
"thermostability": {"kind": "regression", "labels": None, "title": "Thermostability", "unit": "normalised score"},
|
| 58 |
+
"ec": {"kind": "multi_label", "labels": None, "title": "EC enzyme class", "num_classes": 585},
|
| 59 |
+
"go-bp": {"kind": "multi_label", "labels": None, "title": "GO biological process", "num_classes": 1943},
|
| 60 |
+
"go-cc": {"kind": "multi_label", "labels": None, "title": "GO cellular component", "num_classes": 320},
|
| 61 |
+
"go-mf": {"kind": "multi_label", "labels": None, "title": "GO molecular function", "num_classes": 489},
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
STANDARD_AAS = "ACDEFGHIKLMNPQRSTVWY"
|
| 65 |
MAX_LEN = 1024
|
| 66 |
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| 67 |
|
| 68 |
_models: Dict[str, Tuple[EsmForMaskedLM, EsmTokenizer, List[int]]] = {}
|
| 69 |
+
_downstream: Dict[Tuple[str, str], EsmForSequenceClassification] = {}
|
| 70 |
|
| 71 |
|
| 72 |
def get_model(name: str):
|
|
|
|
| 136 |
def root():
|
| 137 |
return {
|
| 138 |
"models": list(MODEL_REGISTRY.keys()),
|
| 139 |
+
"downstream_tasks": [
|
| 140 |
+
{"id": t, "title": cfg["title"], "kind": cfg["kind"]}
|
| 141 |
+
for t, cfg in DOWNSTREAM_TASKS.items()
|
| 142 |
+
],
|
| 143 |
"device": DEVICE,
|
| 144 |
"max_length": MAX_LEN,
|
| 145 |
"aa_order": STANDARD_AAS,
|
|
|
|
| 197 |
wt_logp = log_probs.gather(1, wt_idx[:, None]) # [L, 1]
|
| 198 |
delta = (log_probs - wt_logp).tolist()
|
| 199 |
return MutationResponse(model=req.model, sequence=seq, aa_order=STANDARD_AAS, scores=delta)
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
# βββ Downstream task heads ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 203 |
+
|
| 204 |
+
def get_downstream(backbone: str, task: str) -> EsmForSequenceClassification:
|
| 205 |
+
"""Lazy-load and cache fine-tuned classification/regression heads."""
|
| 206 |
+
if backbone not in DOWNSTREAM_REGISTRY:
|
| 207 |
+
raise HTTPException(400, f"unknown backbone {backbone!r}; available: {list(DOWNSTREAM_REGISTRY)}")
|
| 208 |
+
if task not in DOWNSTREAM_TASKS:
|
| 209 |
+
raise HTTPException(400, f"unknown task {task!r}; available: {list(DOWNSTREAM_TASKS)}")
|
| 210 |
+
key = (backbone, task)
|
| 211 |
+
if key not in _downstream:
|
| 212 |
+
repo = DOWNSTREAM_REGISTRY[backbone]
|
| 213 |
+
print(f"[downstream] loading {repo} :: {task}")
|
| 214 |
+
m = EsmForSequenceClassification.from_pretrained(repo, subfolder=task).to(DEVICE).eval()
|
| 215 |
+
_downstream[key] = m
|
| 216 |
+
return _downstream[key]
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
class DownstreamRequest(BaseModel):
|
| 220 |
+
sequence: str = Field(..., min_length=1, max_length=MAX_LEN, pattern=r"^[A-Za-z]+$")
|
| 221 |
+
backbone: str = "miplm-blosum"
|
| 222 |
+
top_k: int = 3
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
class TaskPrediction(BaseModel):
|
| 226 |
+
task: str
|
| 227 |
+
title: str
|
| 228 |
+
kind: str # single_label | multi_label | regression
|
| 229 |
+
value: float | None = None # set for regression
|
| 230 |
+
unit: str | None = None
|
| 231 |
+
top: List[Dict] | None = None # set for classification β [{label/index, prob}]
|
| 232 |
+
num_classes: int | None = None
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
class DownstreamResponse(BaseModel):
|
| 236 |
+
backbone: str
|
| 237 |
+
sequence: str
|
| 238 |
+
predictions: List[TaskPrediction]
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
@app.post("/downstream", response_model=DownstreamResponse)
|
| 242 |
+
@torch.inference_mode()
|
| 243 |
+
def downstream(req: DownstreamRequest):
|
| 244 |
+
seq = req.sequence.upper()
|
| 245 |
+
if req.backbone not in DOWNSTREAM_REGISTRY:
|
| 246 |
+
raise HTTPException(400, f"unknown backbone {req.backbone!r}")
|
| 247 |
+
|
| 248 |
+
# All downstream heads share the ESM-2 tokenizer with the base backbone.
|
| 249 |
+
_, tokenizer, _ = get_model(req.backbone)
|
| 250 |
+
batch = tokenizer(seq, return_tensors="pt").to(DEVICE)
|
| 251 |
+
|
| 252 |
+
predictions: List[TaskPrediction] = []
|
| 253 |
+
for task, cfg in DOWNSTREAM_TASKS.items():
|
| 254 |
+
model = get_downstream(req.backbone, task)
|
| 255 |
+
logits = model(**batch).logits[0] # [num_labels] for sequence-level head
|
| 256 |
+
|
| 257 |
+
kind = cfg["kind"]
|
| 258 |
+
if kind == "regression":
|
| 259 |
+
predictions.append(TaskPrediction(
|
| 260 |
+
task=task, title=cfg["title"], kind=kind,
|
| 261 |
+
value=float(logits.item()),
|
| 262 |
+
unit=cfg.get("unit"),
|
| 263 |
+
))
|
| 264 |
+
elif kind == "single_label":
|
| 265 |
+
probs = F.softmax(logits, dim=-1)
|
| 266 |
+
top_idx = torch.topk(probs, k=min(req.top_k, probs.numel())).indices.tolist()
|
| 267 |
+
top = [
|
| 268 |
+
{"label": cfg["labels"][i] if cfg["labels"] else f"class_{i}",
|
| 269 |
+
"index": int(i),
|
| 270 |
+
"prob": float(probs[i].item())}
|
| 271 |
+
for i in top_idx
|
| 272 |
+
]
|
| 273 |
+
predictions.append(TaskPrediction(
|
| 274 |
+
task=task, title=cfg["title"], kind=kind,
|
| 275 |
+
top=top, num_classes=int(probs.numel()),
|
| 276 |
+
))
|
| 277 |
+
else: # multi_label
|
| 278 |
+
probs = torch.sigmoid(logits)
|
| 279 |
+
top_idx = torch.topk(probs, k=min(req.top_k, probs.numel())).indices.tolist()
|
| 280 |
+
top = [
|
| 281 |
+
{"label": (cfg["labels"][i] if cfg["labels"] else f"class_{i}"),
|
| 282 |
+
"index": int(i),
|
| 283 |
+
"prob": float(probs[i].item())}
|
| 284 |
+
for i in top_idx
|
| 285 |
+
]
|
| 286 |
+
predictions.append(TaskPrediction(
|
| 287 |
+
task=task, title=cfg["title"], kind=kind,
|
| 288 |
+
top=top, num_classes=int(probs.numel()),
|
| 289 |
+
))
|
| 290 |
+
|
| 291 |
+
return DownstreamResponse(backbone=req.backbone, sequence=seq, predictions=predictions)
|