
Развернуть модель машинного обучения — это не просто запустить скрипт. На ноутбуке модель работает отлично, а на сервере начинаются проблемы: версии библиотек не совпадают, ресурсов меньше, входные данные отличаются от обучающих.
Разберу путь от Jupyter до работающего API на учебном примере: классификатор на scikit-learn, Docker, Flask и Kubernetes.
От Jupyter до Python-модуля
В Jupyter удобно исследовать: загрузить данные, обучить модель, посмотреть метрики. Но для продакшена код нужно вынести из ячеек в обычный Python-файл.
# model.py
import joblib
from sklearn.ensemble import RandomForestClassifier
def train_model(X_train, y_train):
clf = RandomForestClassifier(n_estimators=100, random_state=42)
clf.fit(X_train, y_train)
return clf
def save_model(model, path):
joblib.dump(model, path)
def load_model(path):
return joblib.load(path)
Обязательно зафиксируйте версии всех зависимостей:
pip freeze > requirements.txt
Это важно. Если в requirements.txt указан scikit-learn без версии, через месяц поставится новая, и сохранённая модель может просто не загрузиться или начать вести себя иначе.
Обучение отдельно, сервис отдельно
Модель лучше обучать отдельным шагом (скриптом или в CI) и сохранять в файл, а в образ класть уже готовый model.pkl. Если обучать модель прямо при сборке образа, каждая правка кода будет запускать обучение заново, а результат станет трудно воспроизвести.
# train.py
import pandas as pd
from model import train_model, save_model
X = pd.read_csv('train_data/X.csv')
y = pd.read_csv('train_data/y.csv').values.ravel()
save_model(train_model(X, y), 'model.pkl')
REST API вокруг модели
Чтобы модель отвечала на HTTP-запросы, оборачиваем её в небольшой сервис:
# api.py
import logging
import pandas as pd
from flask import Flask, request, jsonify
from model import load_model
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = Flask(__name__)
model = load_model('model.pkl')
@app.route('/predict', methods=['POST'])
def predict():
try:
df = pd.DataFrame([request.get_json()])
result = model.predict(df)
logger.info('prediction=%s', result[0])
return jsonify({'prediction': int(result[0])})
except Exception:
logger.exception('predict failed')
return jsonify({'error': 'bad input'}), 400
@app.route('/health')
def health():
return jsonify({'status': 'ok'})
Адрес /health нужен, чтобы Kubernetes мог проверять, жив ли сервис. В логах — результат и ошибки. Сами входные данные целиком в лог лучше не писать: в них могут оказаться персональные данные.
Упаковка в Docker
Docker упаковывает код, модель и все зависимости в один образ, и сервис везде работает одинаково. Встроенный сервер Flask подходит только для разработки, поэтому запускаем через gunicorn (добавьте его в requirements.txt).
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY model.py api.py model.pkl ./
USER nobody
EXPOSE 5000
CMD ["gunicorn", "-w", "2", "-b", "0.0.0.0:5000", "api:app"]
Образ собирается слоями. Пока requirements.txt не меняется, Docker берёт установленные библиотеки из кэша, и пересборка после правки кода или новой модели занимает секунды.
Kubernetes: проверки и плавное обновление
Локально хватает docker run. В Kubernetes модель работает в поде среди других сервисов, и платформе нужно объяснить:
- когда под готов принимать запросы (readinessProbe);
- когда сервис завис и его пора перезапустить (livenessProbe);
- как дать ему спокойно доработать при обновлении (preStop и terminationGracePeriodSeconds).
apiVersion: apps/v1
kind: Deployment
metadata:
name: ml-model
spec:
replicas: 2
selector:
matchLabels:
app: ml-model
template:
metadata:
labels:
app: ml-model
spec:
terminationGracePeriodSeconds: 30
containers:
- name: model
image: ml-model:1.0
ports:
- containerPort: 5000
readinessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 15
periodSeconds: 20
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"]
Что здесь происходит:
readinessProbe— Kubernetes проверяет/healthкаждые 10 секунд. Если под не ответит 3 раза подряд (значение по умолчанию), на него перестанут отправлять запросы.livenessProbe— если сервис завис и долго не отвечает, Kubernetes перезапустит контейнер.preStopсоsleep 5— при остановке пода Kubernetes сначала убирает его из балансировки, но на это нужно пару секунд. Пауза даёт время, чтобы новые запросы перестали приходить, а текущие успели завершиться. Потом контейнер получает сигнал на завершение. Если он не остановился заterminationGracePeriodSeconds(отсчёт идёт с начала остановки, вместе с паузой), его завершают принудительно.
Типичная ошибка — забыть про preStop. Тогда при каждом обновлении модели часть запросов обрывается на середине. На своём учебном кластере я проверял это под нагрузкой: без паузы при выкатке были ошибки, с паузой — ноль.
Учёт версий в MLflow
Когда моделей и экспериментов становится много, вести версии вручную тяжело. Здесь помогает MLflow: он хранит историю запусков, параметры, метрики и сами модели.
import mlflow
from model import train_model
with mlflow.start_run():
mlflow.log_param('n_estimators', 100)
clf = train_model(X_train, y_train)
mlflow.log_metric('train_accuracy', clf.score(X_train, y_train))
mlflow.log_metric('val_accuracy', clf.score(X_val, y_val))
mlflow.sklearn.log_model(clf, name='model')
Потом нужную модель можно загрузить прямо из MLflow по идентификатору запуска:
model = mlflow.sklearn.load_model('runs:/<run_id>/model')
У себя я развернул MLflow с PostgreSQL на учебной платформе. Так видно, какая комбинация параметров работает лучше, где метрика просела и какую модель выкатывать.
Проверка перед выкаткой
Прежде чем отправлять модель в Kubernetes, проверьте:
- Данные. Входные данные в продакшене отличаются от обучающих: другие форматы, пропуски, новые значения. Об этом часто забывают, и модель выдаёт чепуху.
- Ошибки ввода. Если API получит данные в неправильном формате, он должен вернуть понятную ошибку, а не упасть.
- Скорость. На мощном ноутбуке предсказание занимает 0,1 секунды, а на скромном сервере может занять секунду и больше. Хватит ли ресурсов на нужное число запросов?
- Логи. Какие запросы приходили и какие были ошибки. Без логов остаётся только гадать.
Когда модель ломается
Даже после проверок бывает, что модель выдаёт странные ответы, качество падает или сервис не справляется с нагрузкой.
Самое быстрое решение — откат:
kubectl rollout undo deployment/ml-model
Kubernetes вернёт предыдущую версию образа, а вместе с ней и предыдущую модель, ведь она лежит внутри образа.
Чтобы заметить проблему вовремя, стоит собирать метрики в Prometheus: число запросов, долю ошибок и время ответа. Если ошибок стало больше, это сразу видно на графике и приходит алерт.
Итоги
Путь от Jupyter к продакшену: Python-модуль → отдельное обучение → API → Docker → Kubernetes. На каждом шаге свои подводные камни:
- не зафиксировали версии в requirements.txt — модель не загрузится или поведёт себя иначе;
- нет проверок
/health— Kubernetes не узнает, что сервис завис; - нет
preStop— запросы обрываются при обновлении; - нет логов и метрик — непонятно, почему модель ошибается.
Когда задача сложнее одного классификатора, добавляются MLflow для версий, Prometheus для метрик и отдельное хранилище для больших моделей.
Нужна помощь с выкаткой модели или инфраструктурой для MLOps — напишите, обсудим.
Нужен Telegram-бот для вашего дела?
Расскажите задачу — отвечу в течение дня, предложу решение и точную цену.
Написать в Telegram