Кафедра ШІзики

Розгортання ML моделей - Від Jupyter до продакшну

Кафедра ШІзики

Автор

40 хвилин

Час читання

12.01.2025

Дата публікації

Рівень:
Середній
Теги: #deployment #mlops #fastapi #docker #production #api

Розгортання ML моделей: Від Jupyter до продакшну

Питання на мільйон

Марія дивилась на екран ноутбука племінника. Модель була ідеальною:

  • Test RMSE: 6.4 піци
  • Точність на вихідних: 96%
  • XGBoost, налаштований з Optuna

“Це неймовірно,” — сказала вона. “Але як мені насправді ВИКОРИСТОВУВАТИ це? Я не можу запускати Jupyter notebook щоранку, щоб передбачити сьогоднішні замовлення.”

Племінник посміхнувся. “Це питання на мільйон, тітко. Нам потрібно РОЗГОРНУТИ цю модель.”

“Розгорнути?”

“Перетворити її з експерименту в notebook на реальну систему. Щось, чим може користуватись твій персонал. Щось, що працює автоматично. Щось, що не зламається, коли мене не буде поруч.”

Марія повільно кивнула. Це була остання частина пазла.

Ласкаво просимо до розгортання моделей — мосту між data science та реальним впливом. Модель, яка залишається в notebook — це лише академічна вправа. Сьогодні ви навчитесь випускати свої моделі на волю.


🎯 Виклики розгортання

Чому розгортання складне? Тому що продакшн — це ЗОВСІМ НЕ як ваш notebook:

Jupyter NotebookПродакшн
Ви запускаєте вручнуПрацює 24/7 автоматично
Впало? Просто перезапустіть ядроВпало? Втрачені гроші, злі клієнти
Один користувач (ви)Сотні/тисячі запитів
Python оточення, яке ви контролюєтеРізні сервери, залежності
”На моїй машині працює”Має працювати на КОЖНІЙ машині

Шлях розгортання

Шлях розгортання ML

Натисніть на крок, щоб дізнатись більше

📓
Розробка та тестування в Jupyter
📓 Jupyter Notebook
vs
🚀 Продакшн
Запуск вручну
Працює 24/7 автоматично
Впало? Перезапуск ядра
Впало? Втрачені гроші
Один користувач (ви)
1000+ запитів
"На моїй машині працює"
Працює всюди

Пройдімось по кожному кроку.


💾 Крок 1: Збереження моделі

Не можна розгорнути те, що не можна зберегти. Розглянемо варіанти.

Варіант A: Pickle (Класика)

import pickle

# Зберегти модель
with open('pizza_model.pkl', 'wb') as f:
    pickle.dump(model, f)

# Завантажити модель
with open('pizza_model.pkl', 'rb') as f:
    loaded_model = pickle.load(f)

# Використати
prediction = loaded_model.predict([[25, 1, 35, 1]])
print(f"Передбачення: {prediction[0]:.0f} піц")

Плюси: Просто, вбудовано в Python Мінуси: Ризики безпеки (не завантажуйте ненадійні pickle!), чутливий до версії Python

Варіант B: Joblib (Краще для ML)

import joblib

# Зберегти — оптимізовано для великих numpy масивів
joblib.dump(model, 'pizza_model.joblib')

# Завантажити
loaded_model = joblib.load('pizza_model.joblib')

# Перевірити, що працює
test_input = [[25, 1, 35, 1]]  # temp, friday, rolling_avg, peak_hour
print(f"Передбачення: {loaded_model.predict(test_input)[0]:.0f} піц")

Плюси: Швидше для великих масивів, краща компресія Мінуси: Все ще специфічний для Python

Варіант C: ONNX (Крос-платформений)

Для моделей, які мають працювати поза Python (мобільні, веб, інші мови):

from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType

# Визначити форму входу
initial_type = [('float_input', FloatTensorType([None, 4]))]

# Конвертувати в ONNX
onnx_model = convert_sklearn(model, initial_types=initial_type)

# Зберегти
with open('pizza_model.onnx', 'wb') as f:
    f.write(onnx_model.SerializeToString())

Плюси: Працює всюди (C++, JavaScript, мобільні) Мінуси: Підтримуються не всі моделі, складніше

Що обрав племінник Марії

import joblib
import json

# Зберегти модель
joblib.dump(best_model, 'models/pizza_predictor_v1.joblib')

# Зберегти метадані (критично для продакшну!)
metadata = {
    'model_type': 'XGBRegressor',
    'version': '1.0.0',
    'trained_date': '2025-01-12',
    'features': ['temperature', 'is_friday_evening', 'rolling_avg', 'is_peak_hour'],
    'target': 'pizza_demand',
    'metrics': {
        'test_rmse': 6.4,
        'test_r2': 0.89
    },
    'training_data_rows': 5000
}

with open('models/pizza_predictor_v1_metadata.json', 'w') as f:
    json.dump(metadata, f, indent=2)

print("Модель та метадані збережено!")

“Завжди зберігайте метадані,” — пояснив племінник. “Через 6 місяців ви забудете, які ознаки очікує модель.”


🌐 Крок 2: Створення API для передбачень

Збережений файл моделі сам по собі не корисний. Нам потрібен API — спосіб для інших систем запитувати передбачення.

Навіщо API?

😩 Без API
1
🙋
Персонал: "Яке передбачення?"
2
💻
Ви: Відкриваєте ноутбук
3
📓
Ви: Запускаєте Jupyter
4
Ви: Запускаєте комірки...
5
🗣️
Ви: "82 піци"
Час: 5 хвилин
VS
🚀 З API
1
📱
Персонал: Відкриває додаток
2
🔗
Додаток: Викликає API
3
🤖
API: Запускає модель
4
Персонал: Бачить "82 піци"
5
🍕
Персонал: Готує 82 піци
Час: 0.1 секунди
📱 App
🔗 API
🤖 Model
✨ Result

FastAPI: Сучасний вибір

FastAPI швидкий, простий та автоматично генерує документацію.

# app.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import joblib
import numpy as np

# Ініціалізуємо FastAPI додаток
app = FastAPI(
    title="Pizza Demand Predictor",
    description="ML-прогнозування попиту для Mama ML",
    version="1.0.0"
)

# Завантажуємо модель при старті
model = joblib.load('models/pizza_predictor_v1.joblib')

# Визначаємо схему запиту
class PredictionRequest(BaseModel):
    temperature: float
    is_friday_evening: int  # 0 або 1
    rolling_avg: float
    is_peak_hour: int  # 0 або 1

# Визначаємо схему відповіді
class PredictionResponse(BaseModel):
    predicted_pizzas: int
    confidence: str
    model_version: str

# Ендпоінт перевірки здоров'я
@app.get("/health")
def health_check():
    return {"status": "healthy", "model_loaded": model is not None}

# Ендпоінт передбачення
@app.post("/predict", response_model=PredictionResponse)
def predict(request: PredictionRequest):
    try:
        # Готуємо ознаки
        features = np.array([[
            request.temperature,
            request.is_friday_evening,
            request.rolling_avg,
            request.is_peak_hour
        ]])

        # Робимо передбачення
        prediction = model.predict(features)[0]

        # Округлюємо до цілих піц
        predicted_pizzas = int(round(prediction))

        # Проста оцінка впевненості
        confidence = "high" if 0 <= request.temperature <= 40 else "medium"

        return PredictionResponse(
            predicted_pizzas=predicted_pizzas,
            confidence=confidence,
            model_version="1.0.0"
        )

    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

# Запуск: uvicorn app:app --reload

Запуск API

# Встановити залежності
pip install fastapi uvicorn joblib numpy

# Запустити сервер
uvicorn app:app --reload --host 0.0.0.0 --port 8000

Тестування API

import requests

# Зробити запит передбачення
response = requests.post(
    "http://localhost:8000/predict",
    json={
        "temperature": 25,
        "is_friday_evening": 1,
        "rolling_avg": 35,
        "is_peak_hour": 1
    }
)

print(response.json())
# {'predicted_pizzas': 82, 'confidence': 'high', 'model_version': '1.0.0'}

Автоматично згенерована документація

FastAPI автоматично створює інтерактивну документацію на http://localhost:8000/docs:

🔒 http://localhost:8000/docs
GET /health Перевірка здоров'я
POST /predict Отримати передбачення попиту
Тестуйте API прямо в браузері!
📋 Schemas
PizzaFeatures
PredictionResponse

Марія була вражена. “Тобто будь-хто може це використовувати? Без знання Python?”

“Саме так. Твоя касова система, твій додаток, навіть проста веб-форма — будь-що може викликати цей API.”


🐳 Крок 3: Контейнеризація з Docker

API працює на вашому ноутбуці. А що щодо сервера? Docker гарантує, що він працюватиме всюди.

Навіщо Docker?

Без Docker:                     З Docker:

"На моїй машині працює!"        "Працює на КОЖНІЙ машині!"

- Різні версії Python           - Однакове оточення всюди
- Відсутні залежності           - Всі залежності включені
- Різниці в ОС                  - Ізольовано від хост-системи
- "Ви встановили numpy?"        - Просто запустіть контейнер

Створення Dockerfile

# Dockerfile
FROM python:3.10-slim

# Встановити робочу директорію
WORKDIR /app

# Скопіювати requirements спочатку (для кешування)
COPY requirements.txt .

# Встановити залежності
RUN pip install --no-cache-dir -r requirements.txt

# Скопіювати код додатку
COPY app.py .
COPY models/ ./models/

# Відкрити порт
EXPOSE 8000

# Запустити додаток
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

Файл залежностей

# requirements.txt
fastapi==0.104.1
uvicorn==0.24.0
joblib==1.3.2
numpy==1.26.2
scikit-learn==1.3.2
xgboost==2.0.2
pydantic==2.5.2

Збірка та запуск

# Збудувати образ
docker build -t pizza-predictor:v1 .

# Запустити контейнер
docker run -d -p 8000:8000 --name pizza-api pizza-predictor:v1

# Перевірити, що працює
docker ps

# Переглянути логи
docker logs pizza-api

# Протестувати API
curl http://localhost:8000/health

Docker Compose для розробки

# docker-compose.yml
version: '3.8'

services:
  pizza-api:
    build: .
    ports:
      - "8000:8000"
    volumes:
      - ./models:/app/models  # Монтуємо директорію моделей
    environment:
      - MODEL_PATH=/app/models/pizza_predictor_v1.joblib
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
# Запустити все
docker-compose up -d

# Зупинити все
docker-compose down

🚀 Крок 4: Стратегії розгортання

Ваш контейнеризований API готовий. Але як безпечно випустити його в продакшн?

Стратегія 1: Shadow Mode (Спочатку спостерігаємо)

Запустіть нову модель паралельно зі старою системою, але НЕ використовуйте її передбачення поки що.

@app.post("/predict")
def predict(request: PredictionRequest):
    # Отримати передбачення від нової моделі
    new_prediction = new_model.predict(features)

    # Отримати передбачення від старої системи
    old_prediction = old_system.predict(features)

    # Логувати обидва для порівняння
    logger.info(f"New: {new_prediction}, Old: {old_prediction}, Diff: {abs(new_prediction - old_prediction)}")

    # Повернути СТАРЕ передбачення (безпечно!)
    return {"predicted_pizzas": old_prediction}

Використовувати для: Перше розгортання, високоризикові сценарії Тривалість: 1-2 тижні

Стратегія 2: Canary Deployment (Тестуємо на небагатьох)

Направляйте невеликий відсоток трафіку на нову модель.

import random

@app.post("/predict")
def predict(request: PredictionRequest):
    # 5% запитів йдуть до нової моделі
    use_new_model = random.random() < 0.05

    if use_new_model:
        prediction = new_model.predict(features)
        model_version = "v2-canary"
    else:
        prediction = stable_model.predict(features)
        model_version = "v1-stable"

    # Логувати яку модель використано
    logger.info(f"Model: {model_version}, Prediction: {prediction}")

    return {
        "predicted_pizzas": prediction,
        "model_version": model_version
    }

Використовувати для: Тестування нових версій моделі Прогресія: 5% → 10% → 25% → 50% → 100%

Стратегія 3: A/B Тестування (Порівнюємо продуктивність)

Випадково призначайте користувачів до моделі A або B, вимірюйте бізнес-результати.

@app.post("/predict")
def predict(request: PredictionRequest, user_id: str):
    # Консистентне призначення для кожного користувача
    use_model_b = hash(user_id) % 100 < 50

    if use_model_b:
        prediction = model_b.predict(features)
        variant = "B"
    else:
        prediction = model_a.predict(features)
        variant = "A"

    # Логувати для аналізу
    log_experiment(
        user_id=user_id,
        variant=variant,
        prediction=prediction,
        timestamp=datetime.now()
    )

    return {"predicted_pizzas": prediction, "variant": variant}

Використовувати для: Вимірювання реального бізнес-впливу Тривалість: До статистичної значущості

План розгортання Марії

# Тижні 1-2: Shadow mode
# - Нова модель працює, але передбачення не використовуються
# - Порівнюємо точність з поточними ручними оцінками

# Тиждень 3: Canary на 10%
# - 10% передбачень від ML моделі
# - Моніторимо помилки та точність

# Тиждень 4: Розширюємо до 50%
# - Якщо метрики добрі, збільшуємо трафік

# Тиждень 5: Повний випуск (100%)
# - Нова модель обслуговує всі передбачення
# - Стара система як резерв

🧪 Крок 5: Тестування в продакшні

Валідація входу

from pydantic import BaseModel, validator

class PredictionRequest(BaseModel):
    temperature: float
    is_friday_evening: int
    rolling_avg: float
    is_peak_hour: int

    @validator('temperature')
    def temperature_must_be_reasonable(cls, v):
        if not -20 <= v <= 50:
            raise ValueError('Температура має бути між -20 та 50°C')
        return v

    @validator('is_friday_evening', 'is_peak_hour')
    def binary_values(cls, v):
        if v not in [0, 1]:
            raise ValueError('Має бути 0 або 1')
        return v

    @validator('rolling_avg')
    def rolling_avg_positive(cls, v):
        if v < 0:
            raise ValueError('Ковзне середнє не може бути від\'ємним')
        return v

Логування для дебагінгу

import logging
from datetime import datetime

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@app.post("/predict")
def predict(request: PredictionRequest):
    request_id = str(uuid.uuid4())[:8]

    logger.info(f"[{request_id}] Request: {request.dict()}")

    start_time = datetime.now()
    prediction = model.predict(features)[0]
    latency = (datetime.now() - start_time).total_seconds() * 1000

    logger.info(f"[{request_id}] Prediction: {prediction:.0f}, Latency: {latency:.1f}ms")

    return {"predicted_pizzas": int(prediction), "request_id": request_id}

Обробка помилок

from fastapi import HTTPException

@app.post("/predict")
def predict(request: PredictionRequest):
    try:
        features = prepare_features(request)
        prediction = model.predict(features)[0]

        # Перевірка адекватності передбачення
        if prediction < 0:
            logger.warning(f"Від'ємне передбачення: {prediction}, обмежуємо до 0")
            prediction = 0
        if prediction > 500:
            logger.warning(f"Незвично високе передбачення: {prediction}")

        return {"predicted_pizzas": int(prediction)}

    except ValueError as e:
        logger.error(f"Помилка валідації: {e}")
        raise HTTPException(status_code=400, detail=str(e))

    except Exception as e:
        logger.error(f"Передбачення не вдалось: {e}")
        raise HTTPException(status_code=500, detail="Помилка сервісу передбачень")

Перевірки здоров’я

@app.get("/health")
def health():
    """Базова перевірка здоров'я"""
    return {"status": "healthy"}

@app.get("/health/detailed")
def detailed_health():
    """Детальна перевірка здоров'я"""
    checks = {
        "model_loaded": model is not None,
        "model_version": metadata.get("version", "unknown"),
        "last_prediction_time": last_prediction_time,
    }

    # Тестове передбачення
    try:
        test_pred = model.predict([[20, 0, 30, 0]])[0]
        checks["model_working"] = True
        checks["test_prediction"] = float(test_pred)
    except Exception as e:
        checks["model_working"] = False
        checks["error"] = str(e)

    status = "healthy" if all([
        checks["model_loaded"],
        checks.get("model_working", False)
    ]) else "unhealthy"

    return {"status": status, "checks": checks}

📋 Чек-лист розгортання

Перед запуском перевірте все:

Перед розгортанням

  • Модель збережена з метаданими (версія, ознаки, метрики)
  • API протестований локально з різними входами
  • Реалізована валідація входу
  • Обробка помилок для граничних випадків
  • Налаштоване логування
  • Ендпоінти перевірки здоров’я працюють
  • Docker контейнер успішно збудований
  • Контейнер протестований локально

Під час розгортання

  • Shadow mode запущений (якщо застосовно)
  • Налаштовані дашборди моніторингу
  • Налаштовані сповіщення
  • Задокументована процедура відкату
  • Команда сповіщена про розгортання

Після розгортання

  • API відповідає на запити
  • Затримка в прийнятних межах
  • Рівень помилок нижче порогу
  • Передбачення виглядають адекватно
  • Логи надходять до системи моніторингу

🎬 Піцерія Марії виходить у продакшн

Після двох тижнів shadow mode та одного тижня canary тестування настав момент.

# deployment_day.py
print("=" * 60)
print("🍕 ПРОГНОЗАТОР ПІЦИ MAMA ML - ПРОДАКШН РОЗГОРТАННЯ 🍕")
print("=" * 60)

# Фінальні перевірки
health = requests.get("http://api.mamaml.com/health/detailed").json()
print(f"\nСтатус здоров'я: {health['status']}")
print(f"Версія моделі: {health['checks']['model_version']}")
print(f"Тестове передбачення: {health['checks']['test_prediction']:.0f} піц")

# Перше реальне передбачення
current_conditions = {
    "temperature": 22,
    "is_friday_evening": 1,
    "rolling_avg": 45,
    "is_peak_hour": 1
}

response = requests.post(
    "http://api.mamaml.com/predict",
    json=current_conditions
)

result = response.json()
print(f"\n🎯 Перше продакшн передбачення: {result['predicted_pizzas']} піц")
print(f"   Впевненість: {result['confidence']}")
print(f"   Модель: {result['model_version']}")

print("\n" + "=" * 60)
print("✅ РОЗГОРТАННЯ УСПІШНЕ!")
print("=" * 60)

Марія дивилась на дашборд на комп’ютері в офісі. Число оновлювалось автоматично: 87 піц.

Вона пішла на кухню. “Готуйте на 87 сьогодні,” — сказала вона персоналу.

Племінник посміхнувся. “Більше ніякого вгадування, тітко.”


🏁 Підсумок: Ваш інструментарій розгортання

Стек розгортання

КомпонентІнструментПризначення
Збереження моделіJoblibСеріалізація навченої моделі
API FrameworkFastAPIОбслуговування передбачень
КонтейнеризаціяDockerУпаковка всього
ОркестраціяDocker ComposeКерування сервісами
ВипускCanary/ShadowБезпечне розгортання

Ключові команди

# Зберегти модель
joblib.dump(model, 'model.joblib')

# Запустити API локально
uvicorn app:app --reload

# Збудувати контейнер
docker build -t my-model:v1 .

# Запустити контейнер
docker run -p 8000:8000 my-model:v1

# Протестувати ендпоінт
curl -X POST http://localhost:8000/predict \
  -H "Content-Type: application/json" \
  -d '{"temperature": 25, "is_friday_evening": 1}'

Ключові висновки

  1. Зберігайте моделі з метаданими — подякуєте собі потім
  2. FastAPI робить API простими — автоматична документація, валідація, async підтримка
  3. Docker забезпечує консистентність — “працює всюди” стає реальністю
  4. Розгортайте поступово — shadow mode, потім canary, потім повний випуск
  5. Завжди майте план відкату — щось піде не так

🚀 Що далі?

Вашу модель розгорнуто! Але подорож не закінчується тут.

У Статті 12: Моніторинг ML моделей ви дізнаєтесь:

  • Чому моделі деградують з часом
  • Виявлення data drift та concept drift
  • Налаштування дашбордів моніторингу
  • Коли і як перенавчати моделі
  • Реагування на інциденти для ML систем

Бо розгорнута модель — як рослина — потребує постійного догляду для процвітання.


Маєте питання про розгортання? Почніть з найпростішого підходу, який працює, потім додавайте складність за потреби. Працюючий API сьогодні краще за ідеальну архітектуру завтра!