init
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
mlflow.db
|
||||||
|
mlruns/
|
||||||
|
artifacts/*
|
||||||
|
data/MNIST/
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# 🧪 MLflow Practice — практический проект
|
||||||
|
|
||||||
|
Проект для изучения MLflow на практике: от логирования экспериментов
|
||||||
|
до Model Registry и загрузки моделей. Поддерживает **GPU (RTX 3090)** и CPU.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Структура
|
||||||
|
|
||||||
|
```
|
||||||
|
mlflow-practice/
|
||||||
|
├── requirements.txt # зависимости
|
||||||
|
├── start_ui.sh # запуск MLflow Tracking Server
|
||||||
|
├── README.md # этот файл
|
||||||
|
├── data/ # датасеты (MNIST скачается автоматически)
|
||||||
|
├── artifacts/ # графики, отчёты
|
||||||
|
└── src/
|
||||||
|
├── train_simple.py # Урок 1: scikit-learn + Tracking
|
||||||
|
├── train_gpu.py # Урок 2: PyTorch CNN на GPU (3090)
|
||||||
|
├── register_model.py # Урок 3: Model Registry
|
||||||
|
├── load_and_predict.py # Урок 4: загрузка и предсказание
|
||||||
|
└── compare_runs.py # Урок 5: сравнение запусков
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Быстрый старт
|
||||||
|
|
||||||
|
### 1. Установка зависимостей
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mlflow-practice
|
||||||
|
|
||||||
|
# Базовые зависимости (CPU — работает везде)
|
||||||
|
pip install mlflow scikit-learn pandas numpy matplotlib
|
||||||
|
|
||||||
|
# Для GPU-урока (RTX 3090) — установите PyTorch под вашу CUDA:
|
||||||
|
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Запуск MLflow UI (в отдельном терминале)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./start_ui.sh
|
||||||
|
# → откройте http://localhost:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Урок 1 — scikit-learn (быстрый, CPU)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python src/train_simple.py --n-estimators 50 --max-depth 5
|
||||||
|
python src/train_simple.py --n-estimators 100 --max-depth 8
|
||||||
|
python src/train_simple.py --n-estimators 200 --max-depth 12
|
||||||
|
```
|
||||||
|
|
||||||
|
Сравните результаты в UI → вкладка **Experiments → digits_classification**.
|
||||||
|
|
||||||
|
### 4. Урок 2 — PyTorch CNN на 3090
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# На GPU — быстро (3090 справится за пару минут)
|
||||||
|
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
|
||||||
|
python src/register_model.py --experiment digits_classification
|
||||||
|
```
|
||||||
|
|
||||||
|
В UI → вкладка **Models** появится модель `digits_rf_model` со стадией **Production**.
|
||||||
|
|
||||||
|
### 6. Урок 4 — Загрузка и предсказание
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python src/load_and_predict.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Урок 5 — Сравнение запусков
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python src/compare_runs.py --experiment digits_classification --top 10
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🖥️ Специфика RTX 3090
|
||||||
|
|
||||||
|
| Параметр | Значение |
|
||||||
|
|---|---|
|
||||||
|
| VRAM | 24 GB GDDR6X |
|
||||||
|
| CUDA cores | 10496 |
|
||||||
|
| Рекомендуемый batch-size | 256–512 для MNIST |
|
||||||
|
| CUDA версия | 12.1 (рекомендуется) |
|
||||||
|
|
||||||
|
Код в `train_gpu.py` **автоматически** определяет CUDA:
|
||||||
|
|
||||||
|
```python
|
||||||
|
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||||
|
```
|
||||||
|
|
||||||
|
На 3090 обучение MNIST (10 epochs) займёт ~1-2 минуты.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Эксперименты для самостоятельной работы
|
||||||
|
|
||||||
|
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`
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# === Базовые ===
|
||||||
|
mlflow>=2.11.0
|
||||||
|
scikit-learn>=1.3.0
|
||||||
|
pandas>=2.0.0
|
||||||
|
numpy>=1.24.0
|
||||||
|
matplotlib>=3.7.0
|
||||||
|
|
||||||
|
# === Для GPU-версии (PyTorch + 3090) ===
|
||||||
|
# Устанавливайте torch отдельно под вашу CUDA:
|
||||||
|
# pip install torch --index-url https://download.pytorch.org/whl/cu121
|
||||||
|
torch>=2.1.0
|
||||||
|
torchvision>=0.16.0
|
||||||
|
|
||||||
|
# === Для MLflow UI / Model Registry ===
|
||||||
|
# SQLite backend уже входит в стандартную поставку Python
|
||||||
Executable
+79
@@ -0,0 +1,79 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# Установка окружения MLflow на GPU-сервере (RTX 3090)
|
||||||
|
# Запускать НА СЕРВЕРЕ: bash setup_server.sh
|
||||||
|
# ============================================================
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "============================================"
|
||||||
|
echo " MLflow Practice — установка на сервере"
|
||||||
|
echo "============================================"
|
||||||
|
|
||||||
|
# --- Проверка GPU ---
|
||||||
|
echo ""
|
||||||
|
echo "📡 Проверка GPU..."
|
||||||
|
if command -v nvidia-smi &> /dev/null; then
|
||||||
|
nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader
|
||||||
|
else
|
||||||
|
echo "⚠️ nvidia-smi не найден — CUDA может быть не установлена"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Проверка Python ---
|
||||||
|
echo ""
|
||||||
|
echo "🐍 Проверка Python..."
|
||||||
|
python3 --version
|
||||||
|
|
||||||
|
# --- Создание виртуального окружения ---
|
||||||
|
echo ""
|
||||||
|
echo "📦 Создание виртуального окружения venv..."
|
||||||
|
if [ ! -d ".venv" ]; then
|
||||||
|
python3 -m venv .venv
|
||||||
|
fi
|
||||||
|
source .venv/bin/activate
|
||||||
|
echo " Активировано: $(which python)"
|
||||||
|
|
||||||
|
# --- Обновление pip ---
|
||||||
|
echo ""
|
||||||
|
echo "⬆️ Обновление pip..."
|
||||||
|
pip install --upgrade pip wheel setuptools
|
||||||
|
|
||||||
|
# --- Установка MLflow и базовых библиотек ---
|
||||||
|
echo ""
|
||||||
|
echo "📥 Установка MLflow + scikit-learn + matplotlib..."
|
||||||
|
pip install mlflow scikit-learn pandas numpy matplotlib
|
||||||
|
|
||||||
|
# --- Установка PyTorch под CUDA ---
|
||||||
|
echo ""
|
||||||
|
echo "🔥 Установка PyTorch (CUDA 12.1)..."
|
||||||
|
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121
|
||||||
|
|
||||||
|
# --- Проверка CUDA ---
|
||||||
|
echo ""
|
||||||
|
echo "✅ Проверка PyTorch + CUDA..."
|
||||||
|
python3 -c "
|
||||||
|
import torch
|
||||||
|
print(f'PyTorch: {torch.__version__}')
|
||||||
|
print(f'CUDA available: {torch.cuda.is_available()}')
|
||||||
|
if torch.cuda.is_available():
|
||||||
|
print(f'GPU: {torch.cuda.get_device_name(0)}')
|
||||||
|
print(f'VRAM: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB')
|
||||||
|
"
|
||||||
|
|
||||||
|
# --- Проверка MLflow ---
|
||||||
|
echo ""
|
||||||
|
echo "✅ Проверка MLflow..."
|
||||||
|
python3 -c "import mlflow; print(f'MLflow: {mlflow.__version__}')"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "============================================"
|
||||||
|
echo " ✅ Установка завершена!"
|
||||||
|
echo "============================================"
|
||||||
|
echo ""
|
||||||
|
echo "Следующие шаги:"
|
||||||
|
echo " 1. Активируйте окружение: source .venv/bin/activate"
|
||||||
|
echo " 2. Запустите MLflow UI: ./start_ui.sh"
|
||||||
|
echo " 3. В другом терминале:"
|
||||||
|
echo " source .venv/bin/activate"
|
||||||
|
echo " python src/train_simple.py --n-estimators 100 --max-depth 8"
|
||||||
|
echo " python src/train_gpu.py --epochs 10 --batch-size 256 --lr 0.001"
|
||||||
|
echo ""
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""
|
||||||
|
Урок 5: Сравнение запусков программно
|
||||||
|
=======================================
|
||||||
|
Показывает, как через API получить все run'ы эксперимента
|
||||||
|
и сравнить их по метрикам — без веб-интерфейса.
|
||||||
|
|
||||||
|
Запуск:
|
||||||
|
python src/compare_runs.py --experiment digits_classification
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import mlflow
|
||||||
|
from mlflow.tracking import MlflowClient
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--experiment", type=str, default="digits_classification")
|
||||||
|
parser.add_argument("--metric", type=str, default="accuracy")
|
||||||
|
parser.add_argument("--top", type=int, default=10)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
client = MlflowClient()
|
||||||
|
exp = client.get_experiment_by_name(args.experiment)
|
||||||
|
if exp is None:
|
||||||
|
print(f"❌ Эксперимент '{args.experiment}' не найден!")
|
||||||
|
return
|
||||||
|
|
||||||
|
runs = client.search_runs(
|
||||||
|
experiment_ids=[exp.experiment_id],
|
||||||
|
order_by=[f"metrics.{args.metric} DESC"],
|
||||||
|
max_results=args.top,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not runs:
|
||||||
|
print("❌ Нет запусков!")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"📊 Топ-{len(runs)} запусков в '{args.experiment}' по {args.metric}:")
|
||||||
|
print(f"{'#':>3} | {'Run ID':>36} | {'accuracy':>9} | {'n_est':>6} | {'depth':>5}")
|
||||||
|
print("-" * 75)
|
||||||
|
|
||||||
|
for i, run in enumerate(runs):
|
||||||
|
run_id = run.info.run_id
|
||||||
|
acc = run.data.metrics.get(args.metric, 0)
|
||||||
|
n_est = run.data.params.get("n_estimators", "—")
|
||||||
|
depth = run.data.params.get("max_depth", "—")
|
||||||
|
print(f"{i+1:>3} | {run_id:>36} | {acc:>9.4f} | {n_est:>6} | {depth:>5}")
|
||||||
|
|
||||||
|
best = runs[0]
|
||||||
|
print(f"\n🏆 Лучший: accuracy={best.data.metrics.get(args.metric, 0):.4f}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""
|
||||||
|
Урок 4: Загрузка модели из Model Registry и предсказание
|
||||||
|
==========================================================
|
||||||
|
Демонстрирует, как загрузить модель из реестра (по имени + стадия)
|
||||||
|
и сделать предсказание.
|
||||||
|
|
||||||
|
Запуск:
|
||||||
|
python src/load_and_predict.py
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
from sklearn.datasets import load_digits
|
||||||
|
|
||||||
|
import mlflow
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Загружаем модель из реестра по имени и стадии
|
||||||
|
model_name = "digits_rf_model"
|
||||||
|
stage = "Production"
|
||||||
|
|
||||||
|
print(f"📥 Загружаем модель '{model_name}' ({stage})...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
model = mlflow.sklearn.load_model(
|
||||||
|
model_uri=f"models:/{model_name}/{stage}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Не удалось загрузить: {e}")
|
||||||
|
print(" Сначала запустите train_simple.py и register_model.py")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Делаем предсказание на нескольких образцах
|
||||||
|
digits = load_digits()
|
||||||
|
X, y = digits.data, digits.target
|
||||||
|
|
||||||
|
n_samples = 5
|
||||||
|
indices = np.random.choice(len(X), n_samples, replace=False)
|
||||||
|
samples = X[indices]
|
||||||
|
true_labels = y[indices]
|
||||||
|
predictions = model.predict(samples)
|
||||||
|
|
||||||
|
print(f"\n🔮 Предсказания для {n_samples} случайных образов:")
|
||||||
|
print(f"{'Образ':>6} | {'Истинный':>8} | {'Предсказанный':>14} | {'OK':>4}")
|
||||||
|
print("-" * 45)
|
||||||
|
for i in range(n_samples):
|
||||||
|
ok = "✅" if predictions[i] == true_labels[i] else "❌"
|
||||||
|
print(f"{i+1:>6} | {true_labels[i]:>8} | {predictions[i]:>14} | {ok}")
|
||||||
|
|
||||||
|
acc = np.mean(predictions == true_labels)
|
||||||
|
print(f"\nТочность на {n_samples} образцах: {acc:.2%}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
"""
|
||||||
|
Урок 3: Model Registry — управление версиями моделей
|
||||||
|
=====================================================
|
||||||
|
Ищет лучший run по метрике accuracy, регистрирует модель
|
||||||
|
в Model Registry и переводит её в стадию Production.
|
||||||
|
|
||||||
|
Запуск:
|
||||||
|
python src/register_model.py --experiment digits_classification
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import mlflow
|
||||||
|
from mlflow.tracking import MlflowClient
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Register best model in MLflow")
|
||||||
|
parser.add_argument("--experiment", type=str, default="digits_classification")
|
||||||
|
parser.add_argument("--metric", type=str, default="accuracy")
|
||||||
|
parser.add_argument("--model-name", type=str, default="digits_rf_model")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
client = MlflowClient()
|
||||||
|
exp = client.get_experiment_by_name(args.experiment)
|
||||||
|
if exp is None:
|
||||||
|
print(f"❌ Эксперимент '{args.experiment}' не найден!")
|
||||||
|
print(" Сначала запустите train_simple.py")
|
||||||
|
return
|
||||||
|
|
||||||
|
# --- Ищем лучший run по метрике ---
|
||||||
|
runs = client.search_runs(
|
||||||
|
experiment_ids=[exp.experiment_id],
|
||||||
|
order_by=[f"metrics.{args.metric} DESC"],
|
||||||
|
max_results=1,
|
||||||
|
)
|
||||||
|
if not runs:
|
||||||
|
print("❌ Нет запусков в эксперименте!")
|
||||||
|
return
|
||||||
|
|
||||||
|
best_run = runs[0]
|
||||||
|
best_acc = best_run.data.metrics.get(args.metric, 0)
|
||||||
|
run_id = best_run.info.run_id
|
||||||
|
print(f"🏆 Лучший run: {run_id}")
|
||||||
|
print(f" {args.metric} = {best_acc:.4f}")
|
||||||
|
print(f" Параметры: {dict(best_run.data.params)}")
|
||||||
|
|
||||||
|
# --- Регистрируем модель ---
|
||||||
|
model_uri = f"runs:/{run_id}/model"
|
||||||
|
|
||||||
|
result = mlflow.register_model(model_uri=model_uri, name=args.model_name)
|
||||||
|
print(f"\n📦 Модель зарегистрирована: {args.model_name} v{result.version}")
|
||||||
|
|
||||||
|
# --- Переводим в Production ---
|
||||||
|
client = MlflowClient()
|
||||||
|
client.set_registered_model_tag(args.model_name, "task", "classification")
|
||||||
|
|
||||||
|
# Снимаем Production со всех предыдущих версий
|
||||||
|
versions = client.search_model_versions(f"name='{args.model_name}'")
|
||||||
|
for v in versions:
|
||||||
|
if v.current_stage == "Production" and v.version != result.version:
|
||||||
|
client.transition_model_version_stage(
|
||||||
|
name=args.model_name,
|
||||||
|
version=v.version,
|
||||||
|
stage="Archived",
|
||||||
|
)
|
||||||
|
print(f" ↓ Версия {v.version} → Archived")
|
||||||
|
|
||||||
|
client.transition_model_version_stage(
|
||||||
|
name=args.model_name,
|
||||||
|
version=result.version,
|
||||||
|
stage="Production",
|
||||||
|
archive_existing_versions=True,
|
||||||
|
)
|
||||||
|
print(f" ↑ Версия {result.version} → Production ✅")
|
||||||
|
|
||||||
|
# --- Список всех версий ---
|
||||||
|
print(f"\n📋 Все версии модели '{args.model_name}':")
|
||||||
|
versions = client.search_model_versions(f"name='{args.model_name}'")
|
||||||
|
for v in versions:
|
||||||
|
print(f" v{v.version} | stage={v.current_stage} | run_id={v.run_id}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
"""
|
||||||
|
Урок 2: MLflow + PyTorch на GPU (RTX 3090)
|
||||||
|
=============================================
|
||||||
|
Обучаем CNN на MNIST. Автоматически использует CUDA если доступна,
|
||||||
|
иначе — CPU. Все параметры, метрики и модель логируются в MLflow.
|
||||||
|
|
||||||
|
Запуск на 3090:
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
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
|
||||||
|
from torchvision import datasets, transforms
|
||||||
|
|
||||||
|
import mlflow
|
||||||
|
import mlflow.pytorch
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Модель: простая CNN ───
|
||||||
|
class SimpleCNN(nn.Module):
|
||||||
|
def __init__(self, num_classes=10):
|
||||||
|
super().__init__()
|
||||||
|
self.features = nn.Sequential(
|
||||||
|
nn.Conv2d(1, 32, 3, padding=1),
|
||||||
|
nn.ReLU(),
|
||||||
|
nn.MaxPool2d(2),
|
||||||
|
nn.Conv2d(32, 64, 3, padding=1),
|
||||||
|
nn.ReLU(),
|
||||||
|
nn.MaxPool2d(2),
|
||||||
|
)
|
||||||
|
self.classifier = nn.Sequential(
|
||||||
|
nn.Linear(64 * 7 * 7, 128),
|
||||||
|
nn.ReLU(),
|
||||||
|
nn.Dropout(0.3),
|
||||||
|
nn.Linear(128, num_classes),
|
||||||
|
)
|
||||||
|
|
||||||
|
def forward(self, x):
|
||||||
|
x = self.features(x)
|
||||||
|
x = x.view(x.size(0), -1)
|
||||||
|
return self.classifier(x)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="MLflow + PyTorch GPU demo")
|
||||||
|
parser.add_argument("--epochs", type=int, default=5)
|
||||||
|
parser.add_argument("--batch-size", type=int, default=128)
|
||||||
|
parser.add_argument("--lr", type=float, default=0.001)
|
||||||
|
parser.add_argument("--experiment-name", type=str, default="mnist_cnn_gpu")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# ─── Устройство ───
|
||||||
|
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||||
|
print(f"🖥️ Устройство: {device}")
|
||||||
|
if device.type == "cuda":
|
||||||
|
gpu_name = torch.cuda.get_device_name(0)
|
||||||
|
gpu_mem = torch.cuda.get_device_properties(0).total_memory / 1e9
|
||||||
|
print(f" GPU: {gpu_name} ({gpu_mem:.1f} GB)")
|
||||||
|
|
||||||
|
# ─── MLflow эксперимент ───
|
||||||
|
mlflow.set_experiment(args.experiment_name)
|
||||||
|
|
||||||
|
# ─── Данные ───
|
||||||
|
transform = transforms.Compose([
|
||||||
|
transforms.ToTensor(),
|
||||||
|
transforms.Normalize((0.1307,), (0.3081,)),
|
||||||
|
])
|
||||||
|
data_dir = "data"
|
||||||
|
os.makedirs(data_dir, exist_ok=True)
|
||||||
|
train_ds = datasets.MNIST(data_dir, train=True, download=True, transform=transform)
|
||||||
|
test_ds = datasets.MNIST(data_dir, train=False, download=True, transform=transform)
|
||||||
|
train_loader = DataLoader(train_ds, batch_size=args.batch_size, shuffle=True, num_workers=4)
|
||||||
|
test_loader = DataLoader(test_ds, batch_size=args.batch_size, shuffle=False, num_workers=4)
|
||||||
|
print(f"📊 Train: {len(train_ds)}, Test: {len(test_ds)}")
|
||||||
|
|
||||||
|
# ─── MLflow run ───
|
||||||
|
with mlflow.start_run(run_name=f"cnn_e{args.epochs}_bs{args.batch_size}") as run:
|
||||||
|
print(f"MLflow run ID: {run.info.run_id}")
|
||||||
|
|
||||||
|
# Логируем параметры
|
||||||
|
mlflow.log_param("epochs", args.epochs)
|
||||||
|
mlflow.log_param("batch_size", args.batch_size)
|
||||||
|
mlflow.log_param("learning_rate", args.lr)
|
||||||
|
mlflow.log_param("device", str(device))
|
||||||
|
mlflow.log_param("optimizer", "Adam")
|
||||||
|
if device.type == "cuda":
|
||||||
|
mlflow.log_param("gpu_name", gpu_name)
|
||||||
|
|
||||||
|
# ─── Модель, оптимизатор, функция потерь ───
|
||||||
|
model = SimpleCNN().to(device)
|
||||||
|
optimizer = optim.Adam(model.parameters(), lr=args.lr)
|
||||||
|
criterion = nn.CrossEntropyLoss()
|
||||||
|
|
||||||
|
# ─── Цикл обучения ───
|
||||||
|
train_losses, test_accs = [], []
|
||||||
|
|
||||||
|
for epoch in range(args.epochs):
|
||||||
|
model.train()
|
||||||
|
epoch_loss = 0.0
|
||||||
|
t0 = time.time()
|
||||||
|
|
||||||
|
for batch_idx, (data, target) in enumerate(train_loader):
|
||||||
|
data, target = data.to(device), target.to(device)
|
||||||
|
optimizer.zero_grad()
|
||||||
|
output = model(data)
|
||||||
|
loss = criterion(output, target)
|
||||||
|
loss.backward()
|
||||||
|
optimizer.step()
|
||||||
|
epoch_loss += loss.item()
|
||||||
|
|
||||||
|
if batch_idx % 100 == 0:
|
||||||
|
print(f" Epoch {epoch+1}/{args.epochs} "
|
||||||
|
f"[{batch_idx * len(data)}/{len(train_loader.dataset)}] "
|
||||||
|
f"loss={loss.item():.4f}")
|
||||||
|
|
||||||
|
avg_loss = epoch_loss / len(train_loader)
|
||||||
|
train_losses.append(avg_loss)
|
||||||
|
|
||||||
|
# ─── Валидация ───
|
||||||
|
model.eval()
|
||||||
|
correct, total = 0, 0
|
||||||
|
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)
|
||||||
|
correct += (pred == target).sum().item()
|
||||||
|
total += target.size(0)
|
||||||
|
|
||||||
|
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}, "
|
||||||
|
f"acc={acc:.4f}, time={elapsed:.1f}s")
|
||||||
|
|
||||||
|
# ─── График обучения ───
|
||||||
|
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
|
||||||
|
ax1.plot(range(1, args.epochs + 1), train_losses, "b-o")
|
||||||
|
ax1.set_title("Train Loss")
|
||||||
|
ax1.set_xlabel("Epoch")
|
||||||
|
ax2.plot(range(1, args.epochs + 1), test_accs, "g-o")
|
||||||
|
ax2.set_title("Test Accuracy")
|
||||||
|
ax2.set_xlabel("Epoch")
|
||||||
|
plt.tight_layout()
|
||||||
|
os.makedirs("artifacts", exist_ok=True)
|
||||||
|
plot_path = "artifacts/training_curve.png"
|
||||||
|
fig.savefig(plot_path)
|
||||||
|
mlflow.log_artifact(plot_path)
|
||||||
|
plt.close(fig)
|
||||||
|
|
||||||
|
# ─── Логируем модель ───
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ─── Теги ───
|
||||||
|
mlflow.set_tag("model_type", "SimpleCNN")
|
||||||
|
mlflow.set_tag("framework", "PyTorch")
|
||||||
|
mlflow.set_tag("dataset", "MNIST")
|
||||||
|
|
||||||
|
final_acc = test_accs[-1]
|
||||||
|
print(f"\n✅ Обучение завершено!")
|
||||||
|
print(f" Финальная accuracy: {final_acc:.4f}")
|
||||||
|
print(f" Run ID: {run.info.run_id}")
|
||||||
|
print(f" Артефакты: training_curve.png, model/")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
"""
|
||||||
|
Урок 1: Основы MLflow Tracking
|
||||||
|
================================
|
||||||
|
Обучаем RandomForest на встроенном датасете digits (scikit-learn).
|
||||||
|
Логируем параметры, метрики, модель и графики в MLflow.
|
||||||
|
|
||||||
|
Запуск:
|
||||||
|
python src/train_simple.py --n-estimators 100 --max-depth 8
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use("Agg") # без GUI
|
||||||
|
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.model_selection import train_test_split
|
||||||
|
|
||||||
|
import mlflow
|
||||||
|
import mlflow.sklearn
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="MLflow + scikit-learn demo")
|
||||||
|
parser.add_argument("--n-estimators", type=int, default=100, help="количество деревьев")
|
||||||
|
parser.add_argument("--max-depth", type=int, default=8, help="макс. глубина дерева")
|
||||||
|
parser.add_argument("--experiment-name", type=str, default="digits_classification")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# --- MLflow: задаём эксперимент ---
|
||||||
|
mlflow.set_experiment(args.experiment_name)
|
||||||
|
|
||||||
|
# --- Данные ---
|
||||||
|
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
|
||||||
|
)
|
||||||
|
print(f"Датасет digits: {X.shape[0]} образцов, {X.shape[1]} признаков")
|
||||||
|
|
||||||
|
# --- MLflow: начинаем run ---
|
||||||
|
with mlflow.start_run(run_name=f"rf_{args.n_estimators}_{args.max_depth}") as run:
|
||||||
|
print(f"MLflow run ID: {run.info.run_id}")
|
||||||
|
|
||||||
|
# Логируем параметры (до обучения!)
|
||||||
|
mlflow.log_param("n_estimators", args.n_estimators)
|
||||||
|
mlflow.log_param("max_depth", args.max_depth)
|
||||||
|
mlflow.log_param("dataset", "digits")
|
||||||
|
mlflow.log_param("test_size", 0.2)
|
||||||
|
|
||||||
|
# --- Обучение ---
|
||||||
|
model = RandomForestClassifier(
|
||||||
|
n_estimators=args.n_estimators,
|
||||||
|
max_depth=args.max_depth,
|
||||||
|
random_state=42,
|
||||||
|
n_jobs=-1,
|
||||||
|
)
|
||||||
|
model.fit(X_train, y_train)
|
||||||
|
|
||||||
|
# --- Предсказание и метрики ---
|
||||||
|
y_pred = model.predict(X_test)
|
||||||
|
acc = accuracy_score(y_test, y_pred)
|
||||||
|
print(f"Accuracy: {acc:.4f}")
|
||||||
|
|
||||||
|
# Логируем метрики
|
||||||
|
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))
|
||||||
|
mlflow.log_metric("per_tree_accuracy", tree_acc, step=i)
|
||||||
|
|
||||||
|
# --- Confusion matrix как артефакт ---
|
||||||
|
cm = confusion_matrix(y_test, y_pred)
|
||||||
|
fig, ax = plt.subplots(figsize=(8, 6))
|
||||||
|
ax.imshow(cm, cmap="Blues")
|
||||||
|
ax.set_title("Confusion Matrix")
|
||||||
|
ax.set_xlabel("Predicted")
|
||||||
|
ax.set_ylabel("Actual")
|
||||||
|
plt.tight_layout()
|
||||||
|
os.makedirs("artifacts", exist_ok=True)
|
||||||
|
cm_path = "artifacts/confusion_matrix.png"
|
||||||
|
fig.savefig(cm_path)
|
||||||
|
mlflow.log_artifact(cm_path)
|
||||||
|
plt.close(fig)
|
||||||
|
|
||||||
|
# --- Classification report как текстовый артефакт ---
|
||||||
|
report = classification_report(y_test, y_pred)
|
||||||
|
report_path = "artifacts/classification_report.txt"
|
||||||
|
with open(report_path, "w") as f:
|
||||||
|
f.write(report)
|
||||||
|
mlflow.log_artifact(report_path)
|
||||||
|
|
||||||
|
# --- Логируем саму модель ---
|
||||||
|
mlflow.sklearn.log_model(
|
||||||
|
model,
|
||||||
|
artifact_path="model",
|
||||||
|
registered_model_name=None, # регистрация — в отдельном скрипте
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Теги ---
|
||||||
|
mlflow.set_tag("model_type", "RandomForest")
|
||||||
|
mlflow.set_tag("author", "practice")
|
||||||
|
|
||||||
|
print(f"\n✅ Готово! Откройте MLflow UI и найдите эксперимент '{args.experiment_name}'")
|
||||||
|
print(f" Run ID: {run.info.run_id}")
|
||||||
|
print(f" Accuracy: {acc:.4f}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================
|
||||||
|
# Запуск MLflow Tracking Server с SQLite backend
|
||||||
|
# Откройте в браузере: http://localhost:5000
|
||||||
|
# ============================================================
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
echo "🚀 Запускаем MLflow Tracking Server..."
|
||||||
|
echo " UI: http://10.0.0.7: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
|
||||||
Reference in New Issue
Block a user