Уроки 6-9: autolog, hyperparam sweep, grid search, serving + MLproject, walkthrough
This commit is contained in:
@@ -6,3 +6,6 @@ mlruns/
|
||||
artifacts/*
|
||||
data/MNIST/
|
||||
*.log
|
||||
|
||||
# Flask serve
|
||||
instance/
|
||||
|
||||
@@ -0,0 +1,516 @@
|
||||
# 🔍 Экскурс по коду — разбор каждого файла
|
||||
|
||||
Полный разбор всех файлов проекта с пояснениями, что и почему.
|
||||
|
||||
---
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
mlflow-practice/
|
||||
├── MLproject # Формат MLflow Projects (воспроизводимость)
|
||||
├── python_env.yaml # Окружение для MLproject
|
||||
├── requirements.txt # Зависимости
|
||||
├── setup_server.sh # Установка на GPU-сервере
|
||||
├── start_ui.sh # Запуск MLflow Tracking Server
|
||||
├── scripts/
|
||||
│ └── setup_git.sh # Настройка Git (HTTPS + токен)
|
||||
└── src/
|
||||
├── train_simple.py # Урок 1: sklearn + Tracking (ручное логирование)
|
||||
├── train_gpu.py # Урок 2: PyTorch CNN на GPU + логирование по эпохам
|
||||
├── register_model.py # Урок 3: Model Registry (версии, стадии)
|
||||
├── load_and_predict.py # Урок 4: загрузка модели + предсказание
|
||||
├── compare_runs.py # Урок 5: сравнение запусков через API
|
||||
├── autolog_demo.py # Урок 6: autolog (автоматическое логирование)
|
||||
├── hyperparam_sweep.py # Урок 7: перебор гиперпараметров (вручную)
|
||||
├── grid_search_cv.py # Урок 8: GridSearchCV + autolog
|
||||
└── serve_model.py # Урок 9: REST API сервер модели
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 train_simple.py — основы MLflow Tracking
|
||||
|
||||
Это **главный файл для понимания**. Здесь показаны все базовые операции MLflow.
|
||||
|
||||
### Импорты
|
||||
|
||||
```python
|
||||
import mlflow
|
||||
import mlflow.sklearn
|
||||
```
|
||||
|
||||
`mlflow` — ядро (start_run, log_param, log_metric, log_artifact).
|
||||
`mlflow.sklearn` — flavor для scikit-learn моделей (save_model, load_model).
|
||||
|
||||
### Задание эксперимента
|
||||
|
||||
```python
|
||||
mlflow.set_experiment("digits_classification")
|
||||
```
|
||||
|
||||
Эксперимент — это **контейнер для связанных запусков (runs)**.
|
||||
Если эксперимент не существует — MLflow создаст его автоматически.
|
||||
Все runs с одинаковым именем эксперимента группируются вместе в UI.
|
||||
|
||||
### Контекстный менеджер run
|
||||
|
||||
```python
|
||||
with mlflow.start_run(run_name="rf_100_8") as run:
|
||||
```
|
||||
|
||||
`start_run()` начинает новый запуск. Контекстный менеджер (`with`)
|
||||
гарантирует, что run закроется даже при ошибке — всё сохранится.
|
||||
`run.info.run_id` — уникальный ID запуска, через него можно к нему вернуться.
|
||||
|
||||
### Логирование параметров
|
||||
|
||||
```python
|
||||
mlflow.log_param("n_estimators", args.n_estimators)
|
||||
mlflow.log_param("max_depth", args.max_depth)
|
||||
```
|
||||
|
||||
Параметры логируются **ДО или ВО ВРЕМЯ обучения**. Это то, что вы
|
||||
выбрали — гиперпараметры модели. Каждый параметр — строка/число.
|
||||
В UI параметры показываются в таблице запусков как колонки.
|
||||
|
||||
### Логирование метрик
|
||||
|
||||
```python
|
||||
mlflow.log_metric("accuracy", acc)
|
||||
```
|
||||
|
||||
Метрики — числа, которые оценивают качество. Главное отличие от
|
||||
параметров: метрики могут логироваться **несколько раз** с разными
|
||||
шагами (`step=N`), образуя график:
|
||||
|
||||
```python
|
||||
for i, tree in enumerate(model.estimators_):
|
||||
tree_acc = accuracy_score(y_test, tree.predict(X_test))
|
||||
mlflow.log_metric("per_tree_accuracy", tree_acc, step=i)
|
||||
```
|
||||
|
||||
В UI это покажется как линейный график — как accuracy растёт
|
||||
по мере добавления деревьев в RandomForest.
|
||||
|
||||
### Логирование артефактов
|
||||
|
||||
```python
|
||||
mlflow.log_artifact("artifacts/confusion_matrix.png")
|
||||
```
|
||||
|
||||
Артефакт — **любой файл**: картинка, текст, JSON, модель. MLflow
|
||||
копирует файл в хранилище артефактов (папка `./artifacts/` или S3).
|
||||
В UI артефакты можно просматривать прямо в браузере.
|
||||
|
||||
### Логирование модели
|
||||
|
||||
```python
|
||||
mlflow.sklearn.log_model(model, artifact_path="model")
|
||||
```
|
||||
|
||||
Модель — это особый артефакт. MLflow сохраняет её в стандартном
|
||||
формате с файлом `MLmodel` (метаданные: flavor, версия, сигнатура).
|
||||
Позже её можно загрузить по `run_id` или зарегистрировать в Registry.
|
||||
|
||||
### Теги
|
||||
|
||||
```python
|
||||
mlflow.set_tag("model_type", "RandomForest")
|
||||
mlflow.set_tag("author", "practice")
|
||||
```
|
||||
|
||||
Теги — произвольные метки для фильтрации и поиска. В отличие от
|
||||
параметров, теги можно менять после завершения run.
|
||||
|
||||
---
|
||||
|
||||
## 📄 train_gpu.py — PyTorch + GPU + логирование по эпохам
|
||||
|
||||
### Определение устройства
|
||||
|
||||
```python
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
```
|
||||
|
||||
Код **GPU-aware**: если есть CUDA — обучение на 3090, иначе на CPU.
|
||||
Всё переносится на устройство: `model.to(device)`, `data.to(device)`.
|
||||
|
||||
### Логирование параметров окружения
|
||||
|
||||
```python
|
||||
mlflow.log_param("device", str(device))
|
||||
mlflow.log_param("gpu_name", gpu_name)
|
||||
```
|
||||
|
||||
Полезно: в UI видно, на каком GPU обучалась модель. Если запускаете
|
||||
на разных машинах — сразу видно различия.
|
||||
|
||||
### Логирование метрик по эпохам
|
||||
|
||||
```python
|
||||
mlflow.log_metric("train_loss", avg_loss, step=epoch)
|
||||
mlflow.log_metric("test_accuracy", acc, step=epoch)
|
||||
```
|
||||
|
||||
`step=epoch` — ключевой момент. Каждая метрика логируется с номером
|
||||
шага. В UI это строит **график learning curve** — как loss падает, а
|
||||
accuracy растёт от эпохи к эпохе.
|
||||
|
||||
### Сохранение графика как артефакта
|
||||
|
||||
```python
|
||||
fig.savefig("artifacts/training_curve.png")
|
||||
mlflow.log_artifact(plot_path)
|
||||
```
|
||||
|
||||
Matplotlib-график сохраняется в файл и логируется. В UI его можно
|
||||
открыть прямо в браузере — артефакты отображаются превью.
|
||||
|
||||
### Логирование PyTorch-модели
|
||||
|
||||
```python
|
||||
mlflow.pytorch.log_model(
|
||||
model,
|
||||
artifact_path="model",
|
||||
serialization_format="pickle",
|
||||
input_example=torch.randn(1, 1, 28, 28).to(device),
|
||||
)
|
||||
```
|
||||
|
||||
`serialization_format="pickle"` — формат сохранения (см. урок 2).
|
||||
`input_example` — пример входа, нужен для построения сигнатуры модели.
|
||||
MLflow сохраняет модель в формате, совместимом с `mlflow.pytorch.load_model()`.
|
||||
|
||||
---
|
||||
|
||||
## 📄 register_model.py — Model Registry
|
||||
|
||||
### Поиск лучшего запуска
|
||||
|
||||
```python
|
||||
runs = client.search_runs(
|
||||
experiment_ids=[exp.experiment_id],
|
||||
order_by=["metrics.accuracy DESC"],
|
||||
max_results=1,
|
||||
)
|
||||
```
|
||||
|
||||
MlflowClient — программный доступ к API Tracking Server.
|
||||
`search_runs` с `order_by` — сортировка по метрике. Так можно найти
|
||||
лучший run без UI — полностью программно.
|
||||
|
||||
### Регистрация модели
|
||||
|
||||
```python
|
||||
model_uri = f"runs:/{run_id}/model"
|
||||
result = mlflow.register_model(model_uri=model_uri, name="digits_rf_model")
|
||||
```
|
||||
|
||||
`model_uri` — указывает на модель внутри конкретного run.
|
||||
`register_model` создаёт новую версию в Model Registry.
|
||||
`result.version` — номер версии (1, 2, 3...).
|
||||
|
||||
### Управление стадиями
|
||||
|
||||
```python
|
||||
client.transition_model_version_stage(
|
||||
name="digits_rf_model",
|
||||
version=result.version,
|
||||
stage="Production",
|
||||
archive_existing_versions=True,
|
||||
)
|
||||
```
|
||||
|
||||
`archive_existing_versions=True` — автоматически переводит все
|
||||
предыдущие Production-версии в Archived. Так в Production всегда
|
||||
только одна версия.
|
||||
|
||||
### Жизненный цикл модели
|
||||
|
||||
```
|
||||
None → Staging → Production → Archived
|
||||
↑ ↓
|
||||
└──────────────────┘
|
||||
(можно вернуть обратно)
|
||||
```
|
||||
|
||||
- **Staging** — тестирование, ещё не готова
|
||||
- **Production** — используется в приложении
|
||||
- **Archived** — устарела, но сохранена для истории
|
||||
|
||||
---
|
||||
|
||||
## 📄 load_and_predict.py — загрузка модели
|
||||
|
||||
### Загрузка по имени и стадии
|
||||
|
||||
```python
|
||||
model = mlflow.sklearn.load_model("models:/digits_rf_model/Production")
|
||||
```
|
||||
|
||||
`models:/` — URI для Model Registry. Формат: `models:/{name}/{stage}`
|
||||
или `models:/{name}/{version}`. Загружает последнюю версию в стадии
|
||||
Production. Если версию перевели в Archived — эта загрузка сломается,
|
||||
что правильно — так вы узнаёте о проблеме сразу.
|
||||
|
||||
### Отличие от загрузки по run_id
|
||||
|
||||
```python
|
||||
# По run_id (привязка к конкретному запуску):
|
||||
model = mlflow.sklearn.load_model("runs:/abc123/model")
|
||||
|
||||
# По registry (привязка к имени + стадии):
|
||||
model = mlflow.sklearn.load_model("models:/digits_rf_model/Production")
|
||||
```
|
||||
|
||||
Registry — уровень абстракции выше. Код не меняется при смене версии
|
||||
модели, только стадия в UI.
|
||||
|
||||
---
|
||||
|
||||
## 📄 compare_runs.py — сравнение через API
|
||||
|
||||
```python
|
||||
runs = client.search_runs(
|
||||
experiment_ids=[exp.experiment_id],
|
||||
order_by=["metrics.accuracy DESC"],
|
||||
max_results=10,
|
||||
)
|
||||
```
|
||||
|
||||
Программный аналог таблицы в UI. Каждый `run` содержит:
|
||||
- `run.info.run_id` — ID
|
||||
- `run.data.params` — словарь параметров
|
||||
- `run.data.metrics` — словарь метрик
|
||||
- `run.data.tags` — теги
|
||||
|
||||
Это позволяет автоматизировать: найти лучший, сравнить, отсеять.
|
||||
|
||||
---
|
||||
|
||||
## 📄 autolog_demo.py — автоматическое логирование
|
||||
|
||||
### Ключевая строка
|
||||
|
||||
```python
|
||||
mlflow.sklearn.autolog(
|
||||
log_models=True,
|
||||
log_datasets=True,
|
||||
max_tuning_runs=10,
|
||||
)
|
||||
```
|
||||
|
||||
После вызова `autolog()` — **больше не нужно** писать `log_param`,
|
||||
`log_metric`, `log_model` вручную. MLflow перехватывает вызовы sklearn
|
||||
и логирует всё автоматически:
|
||||
|
||||
| Что autolog перехватывает | Что логируется |
|
||||
|---|---|
|
||||
| `model.fit()` | все параметры конструктора модели |
|
||||
| Обучение | `training_accuracy`, `training_log_loss` |
|
||||
| Модель | сохраняется в `artifacts/model/` |
|
||||
| Датасет | профиль (размер, типы признаков) |
|
||||
| GridSearchCV | каждый под-вариант как отдельный run |
|
||||
|
||||
### Сравнение: ручное vs autolog
|
||||
|
||||
```python
|
||||
# Ручное (train_simple.py):
|
||||
mlflow.log_param("n_estimators", 100)
|
||||
mlflow.log_param("max_depth", 8)
|
||||
mlflow.log_metric("accuracy", acc)
|
||||
mlflow.sklearn.log_model(model, "model")
|
||||
|
||||
# Autolog (autolog_demo.py):
|
||||
mlflow.sklearn.autolog() # одна строка!
|
||||
model.fit(X_train, y_train) # всё залогируется само
|
||||
```
|
||||
|
||||
**Когда использовать autolog**: быстрые эксперименты, прототипы.
|
||||
**Когда ручное логирование**: нужен контроль над тем, что попадает
|
||||
в MLflow, кастомные метрики, специфичные артефакты.
|
||||
|
||||
---
|
||||
|
||||
## 📄 hyperparam_sweep.py — перебор гиперпараметров
|
||||
|
||||
### Генерация комбинаций
|
||||
|
||||
```python
|
||||
all_combos = list(itertools.product(
|
||||
param_grid["n_estimators"],
|
||||
param_grid["max_depth"],
|
||||
...
|
||||
))
|
||||
```
|
||||
|
||||
`itertools.product` — декартово произведение всех параметров.
|
||||
5 × 6 × 3 × 3 × 3 = 810 возможных комбинаций. Берём случайные
|
||||
`max_combos` из них — для скорости.
|
||||
|
||||
### Почему случайные, а не все 810?
|
||||
|
||||
Полный перебор (Grid Search) = 810 × 5 (CV) = 4050 обучений.
|
||||
На 3090 это реально, но:
|
||||
- Random Search часто находит хорошие параметры быстрее
|
||||
- MLflow хранит каждый run — 810 runs засоряют UI
|
||||
- 20-50 случайных обычно достаточно для практики
|
||||
|
||||
### Кросс-валидация
|
||||
|
||||
```python
|
||||
cv_scores = cross_val_score(model, X_train, y_train, cv=5, scoring="accuracy")
|
||||
mlflow.log_metric("cv_mean_accuracy", cv_scores.mean())
|
||||
mlflow.log_metric("cv_std_accuracy", cv_scores.std())
|
||||
```
|
||||
|
||||
CV делит train на 5 фолдов, обучает 5 раз, усредняет.
|
||||
`cv_std` — разброс между фолдами. Если `std` высокий — модель
|
||||
нестабильна (зависит от разбиения данных).
|
||||
|
||||
### Что искать в UI
|
||||
|
||||
1. Отсортировать по `test_accuracy` DESC
|
||||
2. Сравнить `cv_mean_accuracy` и `test_accuracy` — если CV сильно
|
||||
выше теста → переобучение (overfitting)
|
||||
3. Высокий `cv_std_accuracy` → нестабильная модель
|
||||
4. Scatter plot: `n_estimators` vs `accuracy` — видна ли зависимость
|
||||
|
||||
---
|
||||
|
||||
## 📄 grid_search_cv.py — GridSearchCV + autolog
|
||||
|
||||
```python
|
||||
mlflow.sklearn.autolog(max_tuning_runs=20)
|
||||
...
|
||||
grid = GridSearchCV(estimator=..., param_grid=..., cv=5)
|
||||
grid.fit(X_train, y_train)
|
||||
```
|
||||
|
||||
MLflow autolog + GridSearchCV = **дерево запусков**:
|
||||
- **Parent run** (`gridsearch_rf`) — главный, содержит лучшие параметры
|
||||
- **Child runs** — каждая комбинация гиперпараметров отдельный run
|
||||
|
||||
В UI это видно как иерархия. Parent показывает итог, children — детали.
|
||||
|
||||
---
|
||||
|
||||
## 📄 serve_model.py — REST API
|
||||
|
||||
### Загрузка модели при старте
|
||||
|
||||
```python
|
||||
model = mlflow.sklearn.load_model(f"models:/{model_name}/{stage}")
|
||||
```
|
||||
|
||||
Загружается **один раз** при создании Flask-приложения, не на каждый
|
||||
запрос. Это важно для производительности.
|
||||
|
||||
### Эндпоинты
|
||||
|
||||
| Эндпоинт | Метод | Что делает |
|
||||
|---|---|---|
|
||||
| `/health` | GET | Проверка состояния ("жив" ли сервер) |
|
||||
| `/info` | GET | Информация о модели (признаки, классы) |
|
||||
| `/predict` | POST | Предсказание для переданных данных |
|
||||
| `/predict_random` | GET | Случайный образец из digits + предсказание |
|
||||
|
||||
### Формат запроса /predict
|
||||
|
||||
```json
|
||||
{
|
||||
"instances": [
|
||||
[0, 0, 0, ..., 16, 8, 0, ...], // 64 числа — 8×8 пикселей
|
||||
[0, 0, 12, ..., 0, 0, 0, ...] // можно несколько образцов
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### predict_proba
|
||||
|
||||
```python
|
||||
probabilities = model.predict_proba(instances)
|
||||
```
|
||||
|
||||
RandomForest поддерживает `predict_proba` — вероятности для каждого
|
||||
класса. В ответе: предсказание + вероятности + confidence.
|
||||
|
||||
### Альтернатива — встроенный MLflow server
|
||||
|
||||
```bash
|
||||
mlflow models serve -m models:/digits_rf_model/Production -p 5001
|
||||
```
|
||||
|
||||
MLflow сам поднимает сервер с стандартным API. Кастомный скрипт
|
||||
(serve_model.py) даёт больше контроля — свои эндпоинты, логика,
|
||||
демо-эндпоинт с случайным образом.
|
||||
|
||||
---
|
||||
|
||||
## 📄 MLproject — воспроизводимость
|
||||
|
||||
```yaml
|
||||
name: mlflow-practice
|
||||
entry_points:
|
||||
main:
|
||||
parameters:
|
||||
n_estimators: {type: int, default: 100}
|
||||
command: "python src/train_simple.py --n-estimators {n_estimators}"
|
||||
```
|
||||
|
||||
`MLproject` — декларативное описание проекта. MLflow может запустить
|
||||
его одной командой:
|
||||
|
||||
```bash
|
||||
mlflow run . -P n_estimators=200 -P max_depth=12
|
||||
```
|
||||
|
||||
При этом MLflow:
|
||||
1. Читает `python_env.yaml` → создаёт виртуальное окружение
|
||||
2. Устанавливает зависимости
|
||||
3. Запускает команду из entry_point с параметрами
|
||||
4. Логирует всё в tracking server
|
||||
|
||||
Это гарантирует воспроизводимость: любой человек на любой машине
|
||||
может запустить ваш эксперимент с тем же результатом.
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ Как файлы связаны между собой
|
||||
|
||||
```
|
||||
train_simple.py ──→ логирует run ──→ register_model.py ──→ load_and_predict.py
|
||||
│ │
|
||||
│ ↓
|
||||
│ Model Registry
|
||||
│ │
|
||||
↓ ↓
|
||||
compare_runs.py serve_model.py
|
||||
(ищет лучший run) (загружает из Registry)
|
||||
|
||||
autolog_demo.py ──→ autolog (альтернатива ручному логированию)
|
||||
|
||||
hyperparam_sweep.py ──→ много runs (перебор)
|
||||
grid_search_cv.py ──→ parent + child runs (autolog + GridSearch)
|
||||
|
||||
MLproject ──→ оборачивает всё в воспроизводимый формат
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 Ключевые концепции MLflow — шпаргалка
|
||||
|
||||
| Концепция | Что это | Где используется |
|
||||
|---|---|---|
|
||||
| **Experiment** | Группа связанных runs | `set_experiment()` |
|
||||
| **Run** | Один запуск обучения | `start_run()` |
|
||||
| **Param** | Гиперпараметр (вход) | `log_param()` |
|
||||
| **Metric** | Метрика (результат), может быть графиком | `log_metric(step=N)` |
|
||||
| **Artifact** | Любой файл (картинка, модель) | `log_artifact()` |
|
||||
| **Tag** | Метка для поиска/фильтрации | `set_tag()` |
|
||||
| **Flavor** | Формат модели (sklearn, pytorch) | `mlflow.sklearn`, `mlflow.pytorch` |
|
||||
| **Model Registry** | Реестр версий моделей | `register_model()`, `transition_stage()` |
|
||||
| **Autolog** | Автоматическое логирование | `mlflow.sklearn.autolog()` |
|
||||
| **MLproject** | Воспроизводимый запуск | `mlflow run .` |
|
||||
| **Model URI** | Ссылка на модель | `models:/name/Production`, `runs:/id/model` |
|
||||
@@ -0,0 +1,33 @@
|
||||
# MLflow Project — формат воспроизводимых экспериментов
|
||||
# ========================================================
|
||||
# Запуск:
|
||||
# mlflow run . -P n_estimators=100 -P max_depth=8
|
||||
# mlflow run . -P n_estimators=200 -P max_depth=12 --experiment-name digits_project
|
||||
#
|
||||
# MLflow автоматически:
|
||||
# 1. Создаст изолированное окружение из conda.yaml (или requirements.txt)
|
||||
# 2. Запустит entry point с указанными параметрами
|
||||
# 3. Логирует всё в MLflow tracking
|
||||
|
||||
name: mlflow-practice
|
||||
|
||||
python_env: python_env.yaml
|
||||
|
||||
entry_points:
|
||||
main:
|
||||
parameters:
|
||||
n_estimators: {type: int, default: 100}
|
||||
max_depth: {type: int, default: 8}
|
||||
command: "python src/train_simple.py --n-estimators {n_estimators} --max-depth {max_depth}"
|
||||
|
||||
gpu:
|
||||
parameters:
|
||||
epochs: {type: int, default: 10}
|
||||
batch_size: {type: int, default: 256}
|
||||
lr: {type: float, default: 0.001}
|
||||
command: "python src/train_gpu.py --epochs {epochs} --batch-size {batch_size} --lr {lr}"
|
||||
|
||||
sweep:
|
||||
parameters:
|
||||
max_combos: {type: int, default: 20}
|
||||
command: "python src/hyperparam_sweep.py --max-combos {max_combos}"
|
||||
@@ -1,7 +1,10 @@
|
||||
# 🧪 MLflow Practice — практический проект
|
||||
|
||||
Проект для изучения MLflow на практике: от логирования экспериментов
|
||||
до Model Registry и загрузки моделей. Поддерживает **GPU (RTX 3090)** и CPU.
|
||||
до Model Registry, Model Serving и воспроизводимых запусков.
|
||||
Поддерживает **GPU (RTX 3090)** и CPU.
|
||||
|
||||
📖 **[CODE_WALKTHROUGH.md](CODE_WALKTHROUGH.md)** — подробный разбор каждого файла.
|
||||
|
||||
---
|
||||
|
||||
@@ -9,113 +12,128 @@
|
||||
|
||||
```
|
||||
mlflow-practice/
|
||||
├── MLproject # Формат MLflow Projects (воспроизводимость)
|
||||
├── python_env.yaml # Окружение для MLproject
|
||||
├── requirements.txt # зависимости
|
||||
├── setup_server.sh # установка на GPU-сервере
|
||||
├── start_ui.sh # запуск MLflow Tracking Server
|
||||
├── README.md # этот файл
|
||||
├── data/ # датасеты (MNIST скачается автоматически)
|
||||
├── artifacts/ # графики, отчёты
|
||||
├── CODE_WALKTHROUGH.md # экскурс по коду
|
||||
├── scripts/
|
||||
│ └── setup_git.sh # настройка Git (HTTPS + токен)
|
||||
└── src/
|
||||
├── train_simple.py # Урок 1: scikit-learn + Tracking
|
||||
├── train_gpu.py # Урок 2: PyTorch CNN на GPU (3090)
|
||||
├── train_simple.py # Урок 1: sklearn + Tracking (ручное)
|
||||
├── train_gpu.py # Урок 2: PyTorch CNN на GPU
|
||||
├── register_model.py # Урок 3: Model Registry
|
||||
├── load_and_predict.py # Урок 4: загрузка и предсказание
|
||||
└── compare_runs.py # Урок 5: сравнение запусков
|
||||
├── load_and_predict.py # Урок 4: загрузка + предсказание
|
||||
├── compare_runs.py # Урок 5: сравнение через API
|
||||
├── autolog_demo.py # Урок 6: autolog (автоматическое)
|
||||
├── hyperparam_sweep.py # Урок 7: перебор гиперпараметров
|
||||
├── grid_search_cv.py # Урок 8: GridSearchCV + autolog
|
||||
└── serve_model.py # Урок 9: REST API сервер
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Быстрый старт
|
||||
|
||||
### 1. Установка зависимостей
|
||||
### 1. Установка
|
||||
|
||||
```bash
|
||||
cd mlflow-practice
|
||||
# На GPU-сервере:
|
||||
bash setup_server.sh
|
||||
source .venv/bin/activate
|
||||
|
||||
# Базовые зависимости (CPU — работает везде)
|
||||
pip install mlflow scikit-learn pandas numpy matplotlib
|
||||
|
||||
# Для GPU-урока (RTX 3090) — установите PyTorch под вашу CUDA:
|
||||
# PyTorch под CUDA:
|
||||
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121
|
||||
pip install flask # для serve_model.py
|
||||
```
|
||||
|
||||
### 2. Запуск MLflow UI (в отдельном терминале)
|
||||
### 2. Запуск MLflow UI
|
||||
|
||||
```bash
|
||||
./start_ui.sh
|
||||
# → откройте http://localhost:5000
|
||||
# → http://10.0.0.7:5000
|
||||
```
|
||||
|
||||
### 3. Урок 1 — scikit-learn (быстрый, CPU)
|
||||
### 3. Уроки по порядку
|
||||
|
||||
```bash
|
||||
python src/train_simple.py --n-estimators 50 --max-depth 5
|
||||
# Урок 1 — scikit-learn (CPU, ~5 сек)
|
||||
python src/train_simple.py --n-estimators 100 --max-depth 8
|
||||
python src/train_simple.py --n-estimators 200 --max-depth 12
|
||||
```
|
||||
python src/train_simple.py --n-estimators 50 --max-depth 5
|
||||
|
||||
Сравните результаты в UI → вкладка **Experiments → digits_classification**.
|
||||
|
||||
### 4. Урок 2 — PyTorch CNN на 3090
|
||||
|
||||
```bash
|
||||
# На GPU — быстро (3090 справится за пару минут)
|
||||
# Урок 2 — PyTorch CNN на 3090 (~1-2 мин)
|
||||
python src/train_gpu.py --epochs 10 --batch-size 256 --lr 0.001
|
||||
|
||||
# На CPU — медленно, уменьшите эпохи
|
||||
python src/train_gpu.py --epochs 2 --batch-size 64 --lr 0.01
|
||||
```
|
||||
|
||||
В UI посмотрите графики `train_loss` и `test_accuracy` по эпохам.
|
||||
|
||||
### 5. Урок 3 — Model Registry
|
||||
|
||||
```bash
|
||||
# Урок 3 — Регистрация лучшей модели
|
||||
python src/register_model.py --experiment digits_classification
|
||||
```
|
||||
|
||||
В UI → вкладка **Models** появится модель `digits_rf_model` со стадией **Production**.
|
||||
|
||||
### 6. Урок 4 — Загрузка и предсказание
|
||||
|
||||
```bash
|
||||
# Урок 4 — Загрузка и предсказание
|
||||
python src/load_and_predict.py
|
||||
|
||||
# Урок 5 — Сравнение запусков
|
||||
python src/compare_runs.py --experiment digits_classification --top 10
|
||||
|
||||
# Урок 6 — Autolog (автоматическое логирование)
|
||||
python src/autolog_demo.py
|
||||
|
||||
# Урок 7 — Hyperparameter sweep (20 комбинаций)
|
||||
python src/hyperparam_sweep.py --max-combos 20
|
||||
|
||||
# Урок 8 — GridSearchCV + autolog (parent/child runs)
|
||||
python src/grid_search_cv.py
|
||||
|
||||
# Урок 9 — Model serving (REST API)
|
||||
python src/serve_model.py --port 5001
|
||||
# → curl http://localhost:5001/predict_random
|
||||
```
|
||||
|
||||
### 7. Урок 5 — Сравнение запусков
|
||||
### 4. MLflow Projects (воспроизводимый запуск)
|
||||
|
||||
```bash
|
||||
python src/compare_runs.py --experiment digits_classification --top 10
|
||||
# Запуск через MLproject — MLflow сам создаст окружение
|
||||
mlflow run . -P n_estimators=200 -P max_depth=12
|
||||
mlflow run . -e gpu -P epochs=10 -P batch_size=256 -P lr=0.001
|
||||
mlflow run . -e sweep -P max_combos=30
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🖥️ Специфика RTX 3090
|
||||
## 📚 Что изучает каждый урок
|
||||
|
||||
| Урок | Концепция MLflow | На чём |
|
||||
|---|---|---|
|
||||
| 1 | **Tracking**: params, metrics, artifacts, tags | RandomForest (digits) |
|
||||
| 2 | **Tracking + GPU**: логирование по эпохам, learning curves | PyTorch CNN (MNIST) на 3090 |
|
||||
| 3 | **Model Registry**: версии, стадии Staging→Production | лучшая модель из Урока 1 |
|
||||
| 4 | **Model Loading**: models:/name/stage | предсказание |
|
||||
| 5 | **API**: search_runs, order_by | сортировка по accuracy |
|
||||
| 6 | **Autolog**: одна строка вместо ручного логирования | GradientBoosting (wine) |
|
||||
| 7 | **Hyperparameter sweep**: itertools + random search | 20+ комбинаций RandomForest |
|
||||
| 8 | **GridSearchCV + autolog**: parent/child runs | 18 комбинаций × 5 фолдов |
|
||||
| 9 | **Model serving**: Flask REST API | REST эндпоинты для модели |
|
||||
|
||||
---
|
||||
|
||||
## 🖥️ RTX 3090
|
||||
|
||||
| Параметр | Значение |
|
||||
|---|---|
|
||||
| VRAM | 24 GB GDDR6X |
|
||||
| CUDA cores | 10496 |
|
||||
| Рекомендуемый batch-size | 256–512 для MNIST |
|
||||
| CUDA версия | 12.1 (рекомендуется) |
|
||||
| CUDA | 12.1 |
|
||||
| MNIST 10 epochs | ~1-2 минуты |
|
||||
|
||||
Код в `train_gpu.py` **автоматически** определяет CUDA:
|
||||
|
||||
```python
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
```
|
||||
|
||||
На 3090 обучение MNIST (10 epochs) займёт ~1-2 минуты.
|
||||
Код GPU-aware — автоматически выбирает CUDA если доступна.
|
||||
|
||||
---
|
||||
|
||||
## 💡 Эксперименты для самостоятельной работы
|
||||
|
||||
1. **Подбор гиперпараметров**: запустите `train_simple.py` с разными
|
||||
`--n-estimators` и `--max-depth`, сравните в UI
|
||||
2. **Визуализация**: добавьте логирование ROC-кривой как артефакта
|
||||
3. **Автолог**: попробуйте `mlflow.autolog()` вместо ручного логирования
|
||||
4. **GPU эксперименты**: в `train_gpu.py` попробуйте разные `--lr` и
|
||||
`--batch-size`, сравните графики learning curves
|
||||
5. **Model Registry**: обучите несколько версий, переведите лучшую в
|
||||
Production, остальные в Archived
|
||||
6. **Serving**: попробуйте `mlflow models serve -m models:/digits_rf_model/Production -p 5001`
|
||||
1. **Подбор гиперпараметров** — запустите sweep с `--max-combos 50`
|
||||
2. **Сравнение моделей** — обучите RandomForest vs GradientBoosting vs SVM
|
||||
3. **Autolog vs ручное** — запустите один датасет с autolog и без, сравните
|
||||
4. **Model Registry** — обучите несколько версий, переведите лучшую в Production
|
||||
5. **Serving** — поднимите REST API, отправьте запрос через curl
|
||||
6. **MLproject** — запустите через `mlflow run .` на другой машине
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Спецификация окружения Python для MLflow Projects
|
||||
# MLflow создаст виртуальное окружение по этому файлу
|
||||
python: "3.10"
|
||||
dependencies:
|
||||
- mlflow>=2.11.0
|
||||
- scikit-learn>=1.3.0
|
||||
- pandas>=2.0.0
|
||||
- numpy>=1.24.0
|
||||
- matplotlib>=3.7.0
|
||||
# Для GPU entry point установите torch отдельно:
|
||||
# pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121
|
||||
@@ -11,5 +11,8 @@ matplotlib>=3.7.0
|
||||
torch>=2.1.0
|
||||
torchvision>=0.16.0
|
||||
|
||||
# === Для Model Serving (Урок 9) ===
|
||||
flask>=3.0.0
|
||||
|
||||
# === Для MLflow UI / Model Registry ===
|
||||
# SQLite backend уже входит в стандартную поставку Python
|
||||
|
||||
Executable
+148
@@ -0,0 +1,148 @@
|
||||
#!/bin/bash
|
||||
# ============================================================
|
||||
# Глобальная настройка Git (HTTPS + токен, без SSH)
|
||||
# Запуск: bash scripts/setup_git.sh
|
||||
# ============================================================
|
||||
set -e
|
||||
|
||||
echo "🔧 Глобальная настройка Git"
|
||||
echo "============================"
|
||||
echo ""
|
||||
|
||||
if ! command -v git &> /dev/null; then
|
||||
echo "❌ Git не установлен!"
|
||||
echo " macOS: brew install git"
|
||||
echo " Ubuntu: sudo apt install git"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Git: $(git --version)"
|
||||
echo ""
|
||||
|
||||
# ─── 1. Имя и email ──────────────────────────────────────────
|
||||
DEFAULT_NAME="admin"
|
||||
DEFAULT_EMAIL="admin@e7.by"
|
||||
GIT_NAME="${GIT_NAME:-$DEFAULT_NAME}"
|
||||
GIT_EMAIL="${GIT_EMAIL:-$DEFAULT_EMAIL}"
|
||||
|
||||
read -p "Имя [$GIT_NAME]: " input_name
|
||||
read -p "Email [$GIT_EMAIL]: " input_email
|
||||
|
||||
git config --global user.name "${input_name:-$GIT_NAME}"
|
||||
git config --global user.email "${input_email:-$GIT_EMAIL}"
|
||||
echo "✅ user.name = $(git config --global user.name)"
|
||||
echo "✅ user.email = $(git config --global user.email)"
|
||||
echo ""
|
||||
|
||||
# ─── 2. Базовые настройки ────────────────────────────────────
|
||||
git config --global init.defaultBranch main
|
||||
git config --global core.autocrlf input
|
||||
git config --global core.safecrlf true
|
||||
git config --global core.longpaths true
|
||||
git config --global core.editor "vi"
|
||||
git config --global core.pager "less -FRX"
|
||||
git config --global color.ui auto
|
||||
git config --global pull.rebase true
|
||||
git config --global rebase.autoStash true
|
||||
git config --global push.default current
|
||||
echo "✅ Базовые настройки (main, rebase, colors, pager)"
|
||||
|
||||
# ─── 3. Алиасы ───────────────────────────────────────────────
|
||||
git config --global alias.st "status -sb"
|
||||
git config --global alias.co "checkout"
|
||||
git config --global alias.br "branch"
|
||||
git config --global alias.ci "commit"
|
||||
git config --global alias.cm "commit -m"
|
||||
git config --global alias.ca "commit --amend"
|
||||
git config --global alias.lg "log --oneline --graph --decorate --all"
|
||||
git config --global alias.last "log -1 HEAD --stat"
|
||||
git config --global alias.unstage "restore --staged"
|
||||
git config --global alias.discard "checkout --"
|
||||
git config --global alias.aliases "config --get-regexp ^alias\\."
|
||||
echo "✅ Алиасы: git st, git lg, git ci, git cm, git co, git br, git ca, git last"
|
||||
|
||||
# ─── 4. Credential helper (хранение токенов) ─────────────────
|
||||
# macOS: токен в Keychain (безопасно, шифруется)
|
||||
# Linux: токен в ~/.git-credentials (файл, без шифрования)
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
git config --global credential.helper osxkeychain
|
||||
echo "✅ credential.helper = osxkeychain (macOS Keychain)"
|
||||
else
|
||||
git config --global credential.helper store
|
||||
chmod 600 ~/.git-credentials 2>/dev/null || true
|
||||
echo "✅ credential.helper = store (~/.git-credentials, chmod 600)"
|
||||
fi
|
||||
|
||||
# ─── 5. Настройка токена для git.e7.by ────────────────────────
|
||||
echo ""
|
||||
echo "🔐 Настройка токена для git.e7.by"
|
||||
echo "--------------------------------------------"
|
||||
echo " git.e7.by работает только через HTTPS + токен."
|
||||
echo " Токен можно создать в: Profile → Settings → Access Tokens"
|
||||
echo ""
|
||||
|
||||
read -p "Введите токен (или Enter чтобы пропустить): " TOKEN
|
||||
|
||||
if [ -n "$TOKEN" ]; then
|
||||
# Сохраняем токен через credential helper
|
||||
# Формат: https://username:token@host
|
||||
echo "https://core:${TOKEN}@git.e7.by" | git credential-store store 2>/dev/null || \
|
||||
echo "https://core:${TOKEN}@git.e7.by" >> ~/.git-credentials 2>/dev/null || true
|
||||
|
||||
# Для macOS Keychain — через security
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# Записываем в Keychain напрямую
|
||||
security add-internet-password -s git.e7.by -a core -w "$TOKEN" -r https 2>/dev/null || true
|
||||
echo "✅ Токен сохранён в macOS Keychain (git.e7.by / core)"
|
||||
else
|
||||
chmod 600 ~/.git-credentials 2>/dev/null || true
|
||||
echo "✅ Токен сохранён в ~/.git-credentials"
|
||||
fi
|
||||
|
||||
# ВАЖНО: remote URL должен быть БЕЗ токена!
|
||||
# Токен подхватится автоматически из credential helper
|
||||
echo ""
|
||||
echo "📌 Теперь добавляйте remote БЕЗ токена:"
|
||||
echo " git remote add origin https://git.e7.by/sandbox/repo.git"
|
||||
echo " git push -u origin main"
|
||||
echo " (токен подставится автоматически из credential helper)"
|
||||
else
|
||||
echo "⏭️ Токен не введён — пропускаю"
|
||||
echo " Добавьте позже:"
|
||||
echo " echo 'https://core:ТОКЕН@git.e7.by' >> ~/.git-credentials # Linux"
|
||||
echo " security add-internet-password -s git.e7.by -a core -w 'ТОКЕН' -r https # macOS"
|
||||
fi
|
||||
|
||||
# ─── 6. Если репо уже есть — чистим токен из remote URL ──────
|
||||
echo ""
|
||||
echo "🧹 Проверка текущих remote на наличие токена в URL..."
|
||||
CLEANED=0
|
||||
for repo_remote in $(git remote 2>/dev/null); do
|
||||
url=$(git remote get-url "$repo_remote" 2>/dev/null || true)
|
||||
if echo "$url" | grep -q "://.*:.*@"; then
|
||||
clean_url=$(echo "$url" | sed 's|://[^@]*@|://|')
|
||||
git remote set-url "$repo_remote" "$clean_url"
|
||||
echo " ✅ $repo_remote: токен удалён из URL"
|
||||
echo " было: $url"
|
||||
echo " стало: $clean_url"
|
||||
CLEANED=1
|
||||
fi
|
||||
done
|
||||
if [ "$CLEANED" -eq 0 ]; then
|
||||
echo " ✅ Токенов в remote URL не найдено"
|
||||
fi
|
||||
|
||||
# ─── 7. Финал ────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " ✅ Настройка завершена!"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "📋 Глобальные настройки:"
|
||||
echo "--------------------------------------------"
|
||||
git config --global --list
|
||||
echo "--------------------------------------------"
|
||||
echo ""
|
||||
echo "🧪 Проверка токена:"
|
||||
echo " git ls-remote https://git.e7.by/sandbox/mlflow-practice.git"
|
||||
echo " (если спросит пароль — токен не подхватился)"
|
||||
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
Урок 6: MLflow Autolog — автоматическое логирование
|
||||
====================================================
|
||||
Вместо ручного log_param/log_metric можно вызвать mlflow.autolog()
|
||||
ОДИН раз — и MLflow сам залогирует всё: параметры, метрики, модель,
|
||||
признаки, даже feature importance.
|
||||
|
||||
Сравните с train_simple.py — там всё логировалось вручную.
|
||||
|
||||
Запуск:
|
||||
python src/autolog_demo.py
|
||||
"""
|
||||
from sklearn.datasets import load_wine
|
||||
from sklearn.ensemble import GradientBoostingClassifier
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.metrics import accuracy_score
|
||||
|
||||
import mlflow
|
||||
import mlflow.sklearn
|
||||
|
||||
|
||||
def main():
|
||||
# ─── КЛЮЧЕВАЯ СТРОКА ──────────────────────────────────────
|
||||
# autolog() включает автоматическое логирование для sklearn.
|
||||
# Доступно для: sklearn, pytorch, tensorflow, xgboost, lightgbm,
|
||||
# keras, fastai, spark, autogluon, statsmodels
|
||||
mlflow.sklearn.autolog(
|
||||
log_input_examples=False, # не логировать примеры входа
|
||||
log_model_signatures=False, # не строить сигнатуру модели
|
||||
log_models=True, # сохранить модель автоматически
|
||||
log_datasets=True, # залогировать датасет
|
||||
max_tuning_runs=10, # лимит для GridSearch
|
||||
log_post_training_metrics=True,
|
||||
)
|
||||
# ─────────────────────────────────────────────────────────
|
||||
|
||||
mlflow.set_experiment("autolog_wine")
|
||||
|
||||
# Датасет wine — классификация вин по 3 сортам
|
||||
wine = load_wine()
|
||||
X, y = wine.data, wine.target
|
||||
X_train, X_test, y_train, y_test = train_test_split(
|
||||
X, y, test_size=0.3, random_state=42
|
||||
)
|
||||
print(f"📊 Датасет wine: {X.shape[0]} образцов, {X.shape[1]} признаков, 3 класса")
|
||||
|
||||
# autolog начинает работать автоматически внутри start_run
|
||||
with mlflow.start_run(run_name="gb_autolog") as run:
|
||||
print(f"MLflow run ID: {run.info.run_id}")
|
||||
|
||||
# Просто обучаем модель — НИКАКИХ log_param/log_metric!
|
||||
model = GradientBoostingClassifier(
|
||||
n_estimators=100,
|
||||
learning_rate=0.1,
|
||||
max_depth=3,
|
||||
random_state=42,
|
||||
)
|
||||
model.fit(X_train, y_train)
|
||||
|
||||
# Можно добавить и ручные метрики — они дополнят автолог
|
||||
y_pred = model.predict(X_test)
|
||||
acc = accuracy_score(y_test, y_pred)
|
||||
mlflow.log_metric("manual_accuracy", acc)
|
||||
|
||||
print(f"\n✅ Готово! Откройте MLflow UI:")
|
||||
print(f" Эксперимент: autolog_wine")
|
||||
print(f" Run: {run.info.run_id}")
|
||||
print(f" Accuracy: {acc:.4f}")
|
||||
print(f"\n🔍 Что autolog залогировал автоматически:")
|
||||
print(f" • Параметры: n_estimators, learning_rate, max_depth, ...")
|
||||
print(f" • Метрики: training_accuracy, training_log_loss, ...")
|
||||
print(f" • Модель: сохранена в artifacts/model/")
|
||||
print(f" • Датасет: профиль в artifacts/")
|
||||
print(f" • Ручная метрика: manual_accuracy")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
Урок 9: MLflow + GridSearchCV — автолог перебора
|
||||
==================================================
|
||||
sklearn GridSearchCV сам перебирает гиперпараметры.
|
||||
MLflow autolog автоматически залогирует КАЖУЮ попытку как отдельный run.
|
||||
|
||||
Запуск:
|
||||
python src/grid_search_cv.py
|
||||
"""
|
||||
from sklearn.datasets import load_digits
|
||||
from sklearn.ensemble import RandomForestClassifier
|
||||
from sklearn.model_selection import GridSearchCV, train_test_split
|
||||
from sklearn.metrics import accuracy_score
|
||||
|
||||
import mlflow
|
||||
import mlflow.sklearn
|
||||
|
||||
|
||||
def main():
|
||||
# autolog для sklearn — залогирует все промежуточные попытки GridSearch
|
||||
mlflow.sklearn.autolog(
|
||||
log_models=False, # не сохранять каждую модель (экономим место)
|
||||
max_tuning_runs=20, # максимум залогированных под-запусков
|
||||
log_datasets=False,
|
||||
)
|
||||
|
||||
mlflow.set_experiment("gridsearch_digits")
|
||||
|
||||
digits = load_digits()
|
||||
X, y = digits.data, digits.target
|
||||
X_train, X_test, y_train, y_test = train_test_split(
|
||||
X, y, test_size=0.2, random_state=42
|
||||
)
|
||||
|
||||
# Сетка для перебора
|
||||
param_grid = {
|
||||
"n_estimators": [50, 100, 200],
|
||||
"max_depth": [4, 8, 12],
|
||||
"min_samples_split": [2, 5],
|
||||
}
|
||||
# 3 × 3 × 2 = 18 комбинаций × 5 фолдов = 90 обучений
|
||||
|
||||
print(f"🔬 GridSearchCV")
|
||||
print(f" Комбинаций: {len(param_grid['n_estimators'])} × "
|
||||
f"{len(param_grid['max_depth'])} × "
|
||||
f"{len(param_grid['min_samples_split'])} = "
|
||||
f"{np.prod([len(v) for v in param_grid.values()])}")
|
||||
print(f" CV фолдов: 5")
|
||||
print(f" Всего обучений: {np.prod([len(v) for v in param_grid.values()]) * 5}")
|
||||
print()
|
||||
|
||||
with mlflow.start_run(run_name="gridsearch_rf") as run:
|
||||
print(f"MLflow parent run: {run.info.run_id}")
|
||||
|
||||
grid = GridSearchCV(
|
||||
estimator=RandomForestClassifier(random_state=42, n_jobs=-1),
|
||||
param_grid=param_grid,
|
||||
cv=5,
|
||||
scoring="accuracy",
|
||||
n_jobs=-1,
|
||||
verbose=1,
|
||||
)
|
||||
grid.fit(X_train, y_train)
|
||||
|
||||
# Логируем лучший результат
|
||||
best = grid.best_estimator_
|
||||
y_pred = best.predict(X_test)
|
||||
test_acc = accuracy_score(y_test, y_pred)
|
||||
|
||||
mlflow.log_param("best_params", str(grid.best_params_))
|
||||
mlflow.log_metric("best_cv_score", grid.best_score_)
|
||||
mlflow.log_metric("test_accuracy", test_acc)
|
||||
|
||||
print(f"\n🏆 Лучшие параметры: {grid.best_params_}")
|
||||
print(f" CV score: {grid.best_score_:.4f}")
|
||||
print(f" Test accuracy: {test_acc:.4f}")
|
||||
print(f"\n📊 В MLflow UI:")
|
||||
print(f" Эксперимент: gridsearch_digits")
|
||||
print(f" Parent run: {run.info.run_id}")
|
||||
print(f" Child runs: {len(grid.cv_results_['params'])} под-запусков")
|
||||
print(f" Каждый child — отдельная комбинация гиперпараметров")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import numpy as np
|
||||
main()
|
||||
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
Урок 7: Hyperparameter Sweep — перебор гиперпараметров
|
||||
========================================================
|
||||
Запускает множество комбинаций гиперпараметров и логирует каждый
|
||||
вариант в MLflow. В UI можно сравнить все варианты и найти лучший.
|
||||
|
||||
Запуск:
|
||||
python src/hyperparam_sweep.py
|
||||
python src/hyperparam_sweep.py --max-combos 50 # больше комбинаций
|
||||
"""
|
||||
import argparse
|
||||
import itertools
|
||||
import random
|
||||
import time
|
||||
|
||||
from sklearn.datasets import load_digits
|
||||
from sklearn.ensemble import RandomForestClassifier
|
||||
from sklearn.model_selection import cross_val_score
|
||||
from sklearn.metrics import recall_score, precision_score, f1_score, accuracy_score
|
||||
from sklearn.model_selection import train_test_split
|
||||
|
||||
import mlflow
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="MLflow hyperparameter sweep")
|
||||
parser.add_argument("--max-combos", type=int, default=20,
|
||||
help="максимум комбинаций для перебора")
|
||||
parser.add_argument("--cv-folds", type=int, default=5,
|
||||
help="фолдов кросс-валидации")
|
||||
parser.add_argument("--experiment-name", type=str, default="sweep_digits")
|
||||
args = parser.parse_args()
|
||||
|
||||
# ─── Сетка гиперпараметров ───────────────────────────────
|
||||
param_grid = {
|
||||
"n_estimators": [50, 100, 150, 200, 300],
|
||||
"max_depth": [4, 6, 8, 12, 16, None],
|
||||
"min_samples_split": [2, 5, 10],
|
||||
"min_samples_leaf": [1, 2, 4],
|
||||
"max_features": ["sqrt", "log2", None],
|
||||
}
|
||||
|
||||
# Генерируем все комбинации, берём случайные max_combos
|
||||
all_combos = list(itertools.product(
|
||||
param_grid["n_estimators"],
|
||||
param_grid["max_depth"],
|
||||
param_grid["min_samples_split"],
|
||||
param_grid["min_samples_leaf"],
|
||||
param_grid["max_features"],
|
||||
))
|
||||
random.seed(42)
|
||||
random.shuffle(all_combos)
|
||||
combos = all_combos[:args.max_combos]
|
||||
|
||||
print(f"🔬 Hyperparameter Sweep")
|
||||
print(f" Всего возможных комбинаций: {len(all_combos)}")
|
||||
print(f" Будет запущено: {len(combos)}")
|
||||
print(f" CV фолдов: {args.cv_folds}")
|
||||
print(f" Эксперимент: {args.experiment_name}")
|
||||
print()
|
||||
|
||||
# ─── Данные ──────────────────────────────────────────────
|
||||
digits = load_digits()
|
||||
X, y = digits.data, digits.target
|
||||
X_train, X_test, y_train, y_test = train_test_split(
|
||||
X, y, test_size=0.2, random_state=42
|
||||
)
|
||||
|
||||
mlflow.set_experiment(args.experiment_name)
|
||||
|
||||
best_acc = 0.0
|
||||
best_run_id = None
|
||||
best_params = None
|
||||
|
||||
for i, (n_est, depth, min_split, min_leaf, max_feat) in enumerate(combos):
|
||||
params = {
|
||||
"n_estimators": n_est,
|
||||
"max_depth": depth,
|
||||
"min_samples_split": min_split,
|
||||
"min_samples_leaf": min_leaf,
|
||||
"max_features": max_feat,
|
||||
}
|
||||
|
||||
# Описание для имени run
|
||||
depth_str = str(depth) if depth else "None"
|
||||
feat_str = str(max_feat) if max_feat else "None"
|
||||
run_name = f"rf_ne{n_est}_d{depth_str}_ms{min_split}_ml{min_leaf}_mf{feat_str}"
|
||||
|
||||
with mlflow.start_run(run_name=run_name) as run:
|
||||
# Логируем ВСЕ параметры
|
||||
mlflow.log_params(params)
|
||||
|
||||
# Обучаем
|
||||
model = RandomForestClassifier(
|
||||
random_state=42, n_jobs=-1, **params
|
||||
)
|
||||
model.fit(X_train, y_train)
|
||||
|
||||
# Кросс-валидация (более надёжная оценка)
|
||||
cv_scores = cross_val_score(model, X_train, y_train,
|
||||
cv=args.cv_folds, scoring="accuracy")
|
||||
|
||||
# Метрики на тесте
|
||||
y_pred = model.predict(X_test)
|
||||
test_acc = accuracy_score(y_test, y_pred)
|
||||
test_recall = recall_score(y_test, y_pred, average="macro")
|
||||
test_precision = precision_score(y_test, y_pred, average="macro")
|
||||
test_f1 = f1_score(y_test, y_pred, average="macro")
|
||||
|
||||
# Логируем метрики
|
||||
mlflow.log_metric("cv_mean_accuracy", cv_scores.mean())
|
||||
mlflow.log_metric("cv_std_accuracy", cv_scores.std())
|
||||
mlflow.log_metric("test_accuracy", test_acc)
|
||||
mlflow.log_metric("test_recall_macro", test_recall)
|
||||
mlflow.log_metric("test_precision_macro", test_precision)
|
||||
mlflow.log_metric("test_f1_macro", test_f1)
|
||||
|
||||
# Тег — номер комбинации
|
||||
mlflow.set_tag("combo_index", str(i))
|
||||
|
||||
# Отслеживаем лучший
|
||||
if test_acc > best_acc:
|
||||
best_acc = test_acc
|
||||
best_run_id = run.info.run_id
|
||||
best_params = params
|
||||
|
||||
# Прогресс
|
||||
bar_len = 30
|
||||
filled = int(bar_len * (i + 1) / len(combos))
|
||||
bar = "█" * filled + "░" * (bar_len - filled)
|
||||
print(f"\r [{bar}] {i+1}/{len(combos)} | "
|
||||
f"acc={test_acc:.4f} | {run_name[:40]:<40}", end="")
|
||||
|
||||
print(f"\n\n{'='*60}")
|
||||
print(f"🏆 Лучший результат:")
|
||||
print(f" Run ID: {best_run_id}")
|
||||
print(f" Accuracy: {best_acc:.4f}")
|
||||
print(f" Параметры: {best_params}")
|
||||
print(f"\n📊 Откройте MLflow UI:")
|
||||
print(f" Эксперимент: {args.experiment_name}")
|
||||
print(f" Отсортируйте по test_accuracy (клик на заголовок колонки)")
|
||||
print(f" Сравните cv_mean_accuracy vs test_accuracy")
|
||||
print(f" если cv_std_accuracy высокий — модель нестабильна")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
Урок 8: Model Serving — разворачивание модели как REST API
|
||||
============================================================
|
||||
Загружает модель из Model Registry и запускает HTTP-сервер.
|
||||
Модель принимает JSON-запросы и возвращает предсказания.
|
||||
|
||||
Два способа:
|
||||
1. Встроенный MLflow server (простой):
|
||||
mlflow models serve -m models:/digits_rf_model/Production -p 5001
|
||||
|
||||
2. Этот скрипт — кастомный сервер с дополнительной логикой:
|
||||
|
||||
Запуск:
|
||||
python src/serve_model.py --port 5001
|
||||
|
||||
Запрос:
|
||||
curl -X POST http://localhost:5001/predict \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"instances": [[0,0,0,...,0]]}'
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import numpy as np
|
||||
from sklearn.datasets import load_digits
|
||||
|
||||
import mlflow
|
||||
from flask import Flask, request, jsonify
|
||||
|
||||
|
||||
def create_app(model_name, stage):
|
||||
app = Flask(__name__)
|
||||
|
||||
# Загружаем модель один раз при старте
|
||||
print(f"📥 Загружаем модель '{model_name}' ({stage})...")
|
||||
model = mlflow.sklearn.load_model(f"models:/{model_name}/{stage}")
|
||||
print(f"✅ Модель загружена!")
|
||||
|
||||
# Загружаем датасет для демо-генерации примеров
|
||||
digits = load_digits()
|
||||
|
||||
@app.route("/health", methods=["GET"])
|
||||
def health():
|
||||
return jsonify({"status": "ok", "model": model_name, "stage": stage})
|
||||
|
||||
@app.route("/info", methods=["GET"])
|
||||
def info():
|
||||
return jsonify({
|
||||
"model": model_name,
|
||||
"stage": stage,
|
||||
"n_features": 64,
|
||||
"classes": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
"endpoints": ["/health", "/info", "/predict", "/predict_random"],
|
||||
})
|
||||
|
||||
@app.route("/predict", methods=["POST"])
|
||||
def predict():
|
||||
"""Предсказание для переданных образцов.
|
||||
Формат запроса:
|
||||
{"instances": [[...64 числа...], [...64 числа...]]}
|
||||
"""
|
||||
data = request.get_json(force=True)
|
||||
|
||||
if "instances" not in data:
|
||||
return jsonify({"error": "поле 'instances' обязательно"}), 400
|
||||
|
||||
instances = np.array(data["instances"])
|
||||
|
||||
if instances.shape[-1] != 64:
|
||||
return jsonify({
|
||||
"error": f"ожидалось 64 признака, получено {instances.shape[-1]}"
|
||||
}), 400
|
||||
|
||||
predictions = model.predict(instances)
|
||||
|
||||
# Если модель поддерживает predict_proba — добавим вероятности
|
||||
try:
|
||||
probabilities = model.predict_proba(instances)
|
||||
return jsonify({
|
||||
"predictions": predictions.tolist(),
|
||||
"probabilities": probabilities.tolist(),
|
||||
"n_samples": len(predictions),
|
||||
})
|
||||
except AttributeError:
|
||||
return jsonify({
|
||||
"predictions": predictions.tolist(),
|
||||
"n_samples": len(predictions),
|
||||
})
|
||||
|
||||
@app.route("/predict_random", methods=["GET"])
|
||||
def predict_random():
|
||||
"""Демо-эндпоинт: берёт случайный образец из digits и предсказывает."""
|
||||
idx = np.random.randint(0, len(digits.data))
|
||||
sample = digits.data[idx:idx+1]
|
||||
true_label = int(digits.target[idx])
|
||||
prediction = int(model.predict(sample)[0])
|
||||
|
||||
try:
|
||||
probs = model.predict_proba(sample)[0]
|
||||
confidence = float(max(probs))
|
||||
except AttributeError:
|
||||
confidence = None
|
||||
|
||||
return jsonify({
|
||||
"true_label": true_label,
|
||||
"prediction": prediction,
|
||||
"correct": true_label == prediction,
|
||||
"confidence": confidence,
|
||||
"sample_image": sample[0].reshape(8, 8).tolist(),
|
||||
})
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="MLflow model serving")
|
||||
parser.add_argument("--model-name", default="digits_rf_model")
|
||||
parser.add_argument("--stage", default="Production")
|
||||
parser.add_argument("--port", type=int, default=5001)
|
||||
parser.add_argument("--host", default="0.0.0.0")
|
||||
args = parser.parse_args()
|
||||
|
||||
app = create_app(args.model_name, args.stage)
|
||||
|
||||
print(f"\n🚀 Сервер запускается на http://{args.host}:{args.port}")
|
||||
print(f" /health — проверка состояния")
|
||||
print(f" /info — информация о модели")
|
||||
print(f" /predict — предсказание (POST JSON)")
|
||||
print(f" /predict_random — случайный образец (GET)")
|
||||
print(f"\n📋 Примеры запросов:")
|
||||
print(f" curl http://localhost:{args.port}/health")
|
||||
print(f" curl http://localhost:{args.port}/predict_random")
|
||||
print()
|
||||
|
||||
app.run(host=args.host, port=args.port, debug=False)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -18,7 +18,6 @@ import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import torch
|
||||
from sklearn.metrics import recall_score, precision_score, f1_score
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
from torch.utils.data import DataLoader
|
||||
@@ -141,27 +140,11 @@ def main():
|
||||
|
||||
acc = correct / total
|
||||
test_accs.append(acc)
|
||||
|
||||
# Recall / Precision / F1 (macro-усреднение по 10 классам)
|
||||
all_preds, all_labels = [], []
|
||||
with torch.no_grad():
|
||||
for data, target in test_loader:
|
||||
data, target = data.to(device), target.to(device)
|
||||
output = model(data)
|
||||
pred = output.argmax(dim=1)
|
||||
all_preds.extend(pred.cpu().numpy())
|
||||
all_labels.extend(target.cpu().numpy())
|
||||
recall = recall_score(all_labels, all_preds, average="macro")
|
||||
precision = precision_score(all_labels, all_preds, average="macro")
|
||||
f1 = f1_score(all_labels, all_preds, average="macro")
|
||||
elapsed = time.time() - t0
|
||||
|
||||
# Логируем метрики по эпохам
|
||||
mlflow.log_metric("train_loss", avg_loss, step=epoch)
|
||||
mlflow.log_metric("test_accuracy", acc, step=epoch)
|
||||
mlflow.log_metric("recall_macro", recall, step=epoch)
|
||||
mlflow.log_metric("precision_macro", precision, step=epoch)
|
||||
mlflow.log_metric("f1_macro", f1, step=epoch)
|
||||
mlflow.log_metric("epoch_time_sec", elapsed, step=epoch)
|
||||
|
||||
print(f" → Epoch {epoch+1}: loss={avg_loss:.4f}, "
|
||||
@@ -186,7 +169,6 @@ def main():
|
||||
mlflow.pytorch.log_model(
|
||||
model,
|
||||
artifact_path="model",
|
||||
serialization_format="pickle",
|
||||
registered_model_name=None,
|
||||
input_example=torch.randn(1, 1, 28, 28).to(device),
|
||||
)
|
||||
|
||||
+1
-4
@@ -15,7 +15,7 @@ import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from sklearn.datasets import load_digits
|
||||
from sklearn.ensemble import RandomForestClassifier
|
||||
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report, recall_score, precision_score, f1_score
|
||||
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
|
||||
from sklearn.model_selection import train_test_split
|
||||
|
||||
import mlflow
|
||||
@@ -66,9 +66,6 @@ def main():
|
||||
|
||||
# Логируем метрики
|
||||
mlflow.log_metric("accuracy", acc)
|
||||
mlflow.log_metric("recall_macro", recall_score(y_test, y_pred, average="macro"))
|
||||
mlflow.log_metric("precision_macro", precision_score(y_test, y_pred, average="macro"))
|
||||
mlflow.log_metric("f1_macro", f1_score(y_test, y_pred, average="macro"))
|
||||
# можно логировать несколько шагов (для графиков в UI)
|
||||
for i, tree in enumerate(model.estimators_):
|
||||
tree_acc = accuracy_score(y_test, tree.predict(X_test))
|
||||
|
||||
+2
-9
@@ -7,22 +7,15 @@ set -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "🚀 Запускаем MLflow Tracking Server..."
|
||||
echo " UI: http://10.0.0.7:5000"
|
||||
echo " UI: http://localhost:5000"
|
||||
echo " DB: sqlite:///mlflow.db"
|
||||
echo " Artifacts: ./artifacts"
|
||||
echo ""
|
||||
echo " Нажмите Ctrl+C для остановки"
|
||||
echo ""
|
||||
|
||||
export MLFLOW_ALLOW_ORIGIN="*"
|
||||
|
||||
mlflow server \
|
||||
--backend-store-uri sqlite:///mlflow.db \
|
||||
--default-artifact-root ./artifacts \
|
||||
--serve-artifacts \
|
||||
--allowed-hosts "*" \
|
||||
--cors-allowed-origins "*" \
|
||||
--artifacts-destination ./artifacts \
|
||||
--host 0.0.0.0 \
|
||||
--port 5000 \
|
||||
--dev
|
||||
--port 5000
|
||||
|
||||
Reference in New Issue
Block a user