Инференс-сервис на FastAPI
Минимальный, но правильный инференс-сервис: модель грузится один раз, вход валидируется, есть health-check.
Инференс-сервис — это веб-приложение, которое держит модель в памяти и по HTTP-запросу с признаками возвращает предсказание.
Анатомия правильного сервиса
Хороший инференс-эндпоинт делает четыре вещи: грузит модель один раз при старте (а не на каждый запрос), валидирует вход по схеме, применяет тот же препроцессинг, что и при обучении, и отдаёт предсказание. Плюс health-check, чтобы оркестратор знал, что сервис жив.
Скелет на FastAPI
Этот код запускается на сервере (FastAPI + ML), поэтому он показан для чтения, без кнопки запуска.
from fastapi import FastAPI
from pydantic import BaseModel
import joblib
app = FastAPI()
# модель грузится ОДИН раз при старте, не на каждый запрос
model = joblib.load("model.pkl") # это весь pipeline: scaler + estimator
class Request(BaseModel):
age: int
income: float
orders_30d: int
class Response(BaseModel):
probability: float
label: int
@app.get("/health")
def health():
return {"status": "ok"}
@app.post("/predict", response_model=Response)
def predict(req: Request):
features = [[req.age, req.income, req.orders_30d]]
proba = model.predict_proba(features)[0][1]
return Response(probability=proba, label=int(proba >= 0.5))
Почему модель грузится при старте
Загрузка модели с диска — дорогая операция (десятки-сотни мс). Если делать её внутри обработчика, каждый запрос будет медленным. Поэтому модель загружают один раз в глобальную область при старте процесса и переиспользуют — все запросы работают с уже готовым объектом в памяти.
Зачем строгая схема запроса
Pydantic-модель Request валидирует вход: если придёт строка вместо числа или не хватит поля — сервис вернёт понятную ошибку 422, а не упадёт внутри predict с невнятным стектрейсом. Это первая линия защиты от битых данных в проде.
Логика порога: смоделируем на чистом Python
Внутри predict вероятность превращается в метку по порогу. Эту часть можно прочувствовать без фреймворка.
def to_label(proba, threshold=0.5):
return 1 if proba >= threshold else 0
probas = [0.12, 0.49, 0.50, 0.73, 0.91]
print("Порог 0.5:")
for p in probas:
print(f" proba={p:.2f} -> label={to_label(p)}")
print("Порог 0.7 (строже к классу 1):")
for p in probas:
print(f" proba={p:.2f} -> label={to_label(p, 0.7)}")
Вывод:
Порог 0.5: proba=0.12 -> label=0 proba=0.49 -> label=0 proba=0.50 -> label=1 proba=0.73 -> label=1 proba=0.91 -> label=1 Порог 0.7 (строже к классу 1): proba=0.12 -> label=0 proba=0.49 -> label=0 proba=0.50 -> label=0 proba=0.73 -> label=1 proba=0.91 -> label=1
Порог — это бизнес-решение: повышая его, мы реже срабатываем (меньше ложных тревог, но больше пропусков). Сервис должен отдавать и вероятность, и метку, чтобы потребитель мог выбрать порог под свою задачу.
Как работает под капотом
Под FastAPI работает ASGI-сервер (uvicorn) с воркерами-процессами. Каждый воркер при старте грузит свою копию модели в память. Запросы распределяются по воркерам; внутри воркера обработка обычно синхронна для CPU-инференса. Health-check на /health опрашивает оркестратор (k8s): если эндпоинт перестал отвечать — под перезапускают. Версию модели сервис берёт из реестра или из тега образа, что связывает рантайм с lineage.
Частые ошибки
- Грузить модель внутри обработчика. Каждый запрос станет медленным; грузите при старте.
- Не валидировать вход. Битый запрос уронит predict невнятной ошибкой вместо 422.
- Отдавать только метку. Без вероятности потребитель не сможет настроить порог под себя.
- Забыть health-check. Оркестратор не сможет понять, что сервис завис.
Итог
- Инференс-сервис грузит модель один раз при старте и переиспользует её для всех запросов.
- Строгая схема запроса (Pydantic) отсекает битый вход понятной ошибкой до вызова модели.
- Сервис отдаёт и вероятность, и метку; порог — настраиваемое бизнес-решение; health-check нужен оркестратору.