Day 43: Serving from FastAPI: the /classify container
The classifier becomes a service
You built this exact shape in Stage 0 Day 13 — a FastAPI service behind NestJS. Now the payload is a trained model. The /classify endpoint takes a garment photo, runs it through the model, and returns the predicted class with a confidence. Load the model once at startup, not per request — model loading is expensive and belongs outside the hot path.
from fastapi import FastAPI, UploadFile, HTTPException
from contextlib import asynccontextmanager
import torch, io
from PIL import Image
CLASSES = ["shirt", "trousers", "dress", "saree", "shoes", "bag", "jacket", "skirt"]
state = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
model = torch.jit.load("garment_classifier.torchscript") # load ONCE
model.eval()
state["model"], state["tf"] = model, build_val_transform()
yield
app = FastAPI(lifespan=lifespan)
@app.post("/classify")
async def classify(file: UploadFile):
img = Image.open(io.BytesIO(await file.read())).convert("RGB")
x = state["tf"](img).unsqueeze(0) # add batch dim: (1,3,224,224)
with torch.no_grad():
probs = state["model"](x).softmax(dim=1)[0]
idx = int(probs.argmax())
return {"class": CLASSES[idx], "confidence": round(float(probs[idx]), 4)}softmax turns logits into a confidence
The model outputs raw scores (logits); softmax normalizes them into probabilities that sum to 1, giving you the confidence to return. Logging both the predicted class and its confidence per request (a Stage 1 exit criterion — 'predictions logged') is the seed of the model telemetry that becomes central in Stage 5.
Key terms
- lifespan / startup load
- Loading the model once when the service starts, rather than per request, to keep inference latency low.
- softmax
- A function turning raw class scores (logits) into a probability distribution that sums to 1.
- Logits
- The raw, unnormalized output scores of a classifier before softmax.
Why should the model be loaded in the FastAPI startup/lifespan, not inside the /classify request handler?