johanjanil's picture
Added handler for the model
0ef5f4e verified
Raw
History Blame Contribute Delete
3.48 kB
import torch
import base64
import io
from typing import Dict, List, Any
from PIL import Image
from torchvision import transforms, models
import torch.nn as nn
import torch.nn.functional as F
class ResNet50Classifier(nn.Module):
def __init__(self, num_classes: int = 3, dropout: float = 0.3):
super().__init__()
backbone = models.resnet50(weights=None)
self.features = nn.Sequential(*list(backbone.children())[:-1])
self.classifier = nn.Sequential(
nn.Linear(2048, 512), nn.GELU(), nn.Dropout(dropout),
nn.Linear(512, 128), nn.GELU(), nn.Dropout(dropout),
nn.Linear(128, num_classes),
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.classifier(self.features(x).flatten(1))
class EndpointHandler:
CLASSES = ["benign", "malignant", "normal"]
# Identical to the val/test transform in the notebook
TRANSFORM = transforms.Compose([
transforms.Grayscale(num_output_channels=3),
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(
mean=[0.485, 0.456, 0.406],
std= [0.229, 0.224, 0.225],
),
])
def __init__(self, path: str = ""):
"""
Loads the ResNet-50 mammography classifier from a local checkpoint.
Args:
path: Directory containing best_model.pth (HF Inference Endpoints
sets this to the local snapshot of your model repo).
"""
self.device = "cuda" if torch.cuda.is_available() else "cpu"
self.model = ResNet50Classifier(num_classes=len(self.CLASSES)).to(self.device)
import os
ckpt_path = os.path.join(path, "best_model.pth")
self.model.load_state_dict(
torch.load(ckpt_path, map_location=self.device)
)
self.model.eval()
def __call__(self, data: Dict[str, Any]) -> List[Dict[str, Any]]:
"""
Args:
data: Dictionary with key "inputs" containing:
- "image": Base64-encoded PNG/JPG string
Returns:
List of {"label": str, "score": float} sorted by score descending.
Example:
[
{"label": "malignant", "score": 0.821},
{"label": "benign", "score": 0.134},
{"label": "normal", "score": 0.045},
]
"""
inputs = data.pop("inputs", data)
image_base64 = inputs.get("image")
if not image_base64:
return [{"error": "Missing 'image' in payload"}]
# 1. Decode base64 image
try:
image = Image.open(
io.BytesIO(base64.b64decode(image_base64))
).convert("RGB")
except Exception as e:
return [{"error": f"Failed to decode image: {str(e)}"}]
# 2. Preprocess — same pipeline as notebook val/test transform
tensor = self.TRANSFORM(image).unsqueeze(0).to(self.device) # (1, 3, 224, 224)
# 3. Inference
with torch.no_grad():
logits = self.model(tensor) # (1, 3)
probs = F.softmax(logits, dim=1).cpu().numpy()[0] # (3,)
# 4. Format and sort
results = [
{"label": label, "score": float(prob)}
for label, prob in zip(self.CLASSES, probs)
]
return sorted(results, key=lambda x: x["score"], reverse=True)