This commit is contained in:
cs
2026-07-20 14:10:12 +03:00
commit 2c07fd63c0
10 changed files with 764 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
.venv/
__pycache__/
*.pyc
mlflow.db
mlruns/
artifacts/*
data/MNIST/
*.log
+121
View File
@@ -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 | 256512 для 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`
+15
View File
@@ -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
+79
View File
@@ -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 ""
+54
View File
@@ -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()
+54
View File
@@ -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()
+83
View File
@@ -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()
+207
View File
@@ -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()
+115
View File
@@ -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
View File
@@ -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