init
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user