Розгортання ML моделей - Від Jupyter до продакшну
Кафедра ШІзики
Автор
40 хвилин
Час читання
12.01.2025
Дата публікації
Розгортання ML моделей: Від Jupyter до продакшну
Питання на мільйон
Марія дивилась на екран ноутбука племінника. Модель була ідеальною:
- Test RMSE: 6.4 піци
- Точність на вихідних: 96%
- XGBoost, налаштований з Optuna
“Це неймовірно,” — сказала вона. “Але як мені насправді ВИКОРИСТОВУВАТИ це? Я не можу запускати Jupyter notebook щоранку, щоб передбачити сьогоднішні замовлення.”
Племінник посміхнувся. “Це питання на мільйон, тітко. Нам потрібно РОЗГОРНУТИ цю модель.”
“Розгорнути?”
“Перетворити її з експерименту в notebook на реальну систему. Щось, чим може користуватись твій персонал. Щось, що працює автоматично. Щось, що не зламається, коли мене не буде поруч.”
Марія повільно кивнула. Це була остання частина пазла.
Ласкаво просимо до розгортання моделей — мосту між data science та реальним впливом. Модель, яка залишається в notebook — це лише академічна вправа. Сьогодні ви навчитесь випускати свої моделі на волю.
🎯 Виклики розгортання
Чому розгортання складне? Тому що продакшн — це ЗОВСІМ НЕ як ваш notebook:
| Jupyter Notebook | Продакшн |
|---|---|
| Ви запускаєте вручну | Працює 24/7 автоматично |
| Впало? Просто перезапустіть ядро | Впало? Втрачені гроші, злі клієнти |
| Один користувач (ви) | Сотні/тисячі запитів |
| Python оточення, яке ви контролюєте | Різні сервери, залежності |
| ”На моїй машині працює” | Має працювати на КОЖНІЙ машині |
Шлях розгортання
Шлях розгортання ML
Натисніть на крок, щоб дізнатись більше
Пройдімось по кожному кроку.
💾 Крок 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?
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:
Pizza Demand Predictor
Swagger UIМарія була вражена. “Тобто будь-хто може це використовувати? Без знання 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/healthDocker 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 Framework | FastAPI | Обслуговування передбачень |
| Контейнеризація | 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}'Ключові висновки
- Зберігайте моделі з метаданими — подякуєте собі потім
- FastAPI робить API простими — автоматична документація, валідація, async підтримка
- Docker забезпечує консистентність — “працює всюди” стає реальністю
- Розгортайте поступово — shadow mode, потім canary, потім повний випуск
- Завжди майте план відкату — щось піде не так
🚀 Що далі?
Вашу модель розгорнуто! Але подорож не закінчується тут.
У Статті 12: Моніторинг ML моделей ви дізнаєтесь:
- Чому моделі деградують з часом
- Виявлення data drift та concept drift
- Налаштування дашбордів моніторингу
- Коли і як перенавчати моделі
- Реагування на інциденти для ML систем
Бо розгорнута модель — як рослина — потребує постійного догляду для процвітання.
Маєте питання про розгортання? Почніть з найпростішого підходу, який працює, потім додавайте складність за потреби. Працюючий API сьогодні краще за ідеальну архітектуру завтра!