Уроки 6-9: autolog, hyperparam sweep, grid search, serving + MLproject, walkthrough

This commit is contained in:
2026-07-20 15:01:31 +03:00
parent 2c07fd63c0
commit d6a4b46db0
14 changed files with 1248 additions and 95 deletions
+78
View File
@@ -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()
+86
View File
@@ -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()
+147
View File
@@ -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()
+138
View File
@@ -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
View File
@@ -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
View File
@@ -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))