From d5c5bf376766b5d81d6861933b24e30b8825d72a Mon Sep 17 00:00:00 2001 From: second_constantine Date: Mon, 20 Jul 2026 22:28:23 +0300 Subject: [PATCH] =?UTF-8?q?Recall=20=D0=B2=D0=B5=D0=B7=D0=B4=D0=B5=20+=20?= =?UTF-8?q?=D1=84=D0=B8=D0=BA=D1=81=D1=8B=20=D0=BE=D0=BA=D1=80=D1=83=D0=B6?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - recall_macro/recall_weighted во всех скриптах с метриками (train_simple, train_gpu, autolog_demo, hyperparam_sweep, grid_search_cv, load_and_predict, compare_runs) - grid_search_cv: multi-metric scoring (accuracy + recall_macro) - register_model: выбор лучшей модели по recall (--metric) - MLflow warnings: name вместо artifact_path, сигнатура модели, numpy input_example для pytorch, подавление env-var INFO - start_ui.sh: порт 5555 + file-store backend (фикс: UI не показывал эксперименты, т.к. скрипты писали в mlruns/, а сервер читал sqlite) - setup_server.sh: развилка macOS/Linux + проверка MPS - train_gpu: get_device CUDA→MPS→CPU, num_workers=0 на macOS - AGENTS.md: гид для агента + саморегламент обновления - README/CODE_WALKTHROUGH: выровнена нумерация уроков 8/9 - .gitignore: +.DS_Store - удалён scripts/setup_git.sh --- .gitignore | 3 + AGENTS.md | 206 ++++++++++++++++++++++++++++++++++++++++ CODE_WALKTHROUGH.md | 38 ++++---- README.md | 14 +-- scripts/setup_git.sh | 148 ----------------------------- setup_server.sh | 170 +++++++++++++++++++++++++-------- src/autolog_demo.py | 14 ++- src/compare_runs.py | 26 ++++- src/grid_search_cv.py | 24 ++++- src/hyperparam_sweep.py | 4 +- src/load_and_predict.py | 21 +++- src/register_model.py | 8 +- src/train_gpu.py | 69 ++++++++++++-- src/train_simple.py | 24 ++++- start_ui.sh | 35 +++++-- 15 files changed, 547 insertions(+), 257 deletions(-) create mode 100644 AGENTS.md delete mode 100755 scripts/setup_git.sh diff --git a/.gitignore b/.gitignore index b11c688..fc9bdc4 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ data/MNIST/ # Flask serve instance/ + +# macOS +.DS_Store diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4fb9a2b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,206 @@ +# AGENTS.md — гид для агента по проекту mlflow-practice + +> Этот файл — ориентир для любого AI-агента (и человека), который будет +> работать с проектом. Читай его **первым** при входе в репозиторий. + +--- + +## 🎯 Назначение проекта + +Учебный проект для изучения **MLflow** на практике: от логирования +экспериментов до Model Registry и serving моделей. Девять последовательных +уроков, каждый — отдельный скрипт в `src/`. Поддерживает **GPU (RTX 3090, CUDA)** +и **macOS (Intel CPU / Apple Silicon MPS)**. + +Не production-система. Цель — показать полный цикл MLOps на маленьких +встроенных датасетах (digits, wine, MNIST). + +--- + +## 📁 Карта проекта + +``` +mlflow-practice/ +├── AGENTS.md # ← этот файл (поддерживай актуальным!) +├── README.md # пользовательская документация (как запускать) +├── CODE_WALKTHROUGH.md # подробный разбор каждого файла с пояснениями +├── MLproject # MLflow Projects: entry points main/gpu/sweep +├── python_env.yaml # окружение для MLproject (python 3.10) +├── requirements.txt # pip-зависимости +├── setup_server.sh # установка окружения (РАЗВИЛКА macOS/Linux) +├── start_ui.sh # запуск MLflow Tracking Server (UI на :5555, file-store) +├── .gitignore +├── scripts/ +│ └── setup_git.sh # настройка Git (HTTPS + токен) +├── src/ # ← весь код уроков здесь +│ ├── train_simple.py # Урок 1: sklearn + Tracking (ручное логирование) +│ ├── train_gpu.py # Урок 2: PyTorch CNN, CUDA→MPS→CPU +│ ├── register_model.py # Урок 3: Model Registry (версии, стадии) +│ ├── load_and_predict.py # Урок 4: загрузка модели + предсказание +│ ├── compare_runs.py # Урок 5: сравнение запусков через API +│ ├── autolog_demo.py # Урок 6: mlflow.autolog() +│ ├── hyperparam_sweep.py # Урок 7: перебор гиперпараметров вручную +│ ├── serve_model.py # Урок 8: REST API сервер модели (Flask) +│ └── grid_search_cv.py # Урок 9: GridSearchCV + autolog +├── data/ # датасеты (MNIST скачается автоматически) +├── artifacts/ # графики, отчёты (логируются в MLflow) +└── mlruns/ # локальный file store MLflow (в .gitignore) +``` + +--- + +## 🧩 Роль каждого файла (кратко) + +| Файл | Что делает | Датасет | Фреймворк | +|---|---|---|---| +| `train_simple.py` | RandomForest, ручное логирование params/metrics/model/artifacts | digits | sklearn | +| `train_gpu.py` | CNN, логирование по эпохам, выбор устройства `get_device()` | MNIST | PyTorch | +| `register_model.py` | ищет лучший run по метрике, регистрирует модель, стадия Production | — | MLflow API | +| `load_and_predict.py` | грузит `models:/digits_rf_model/Production`, предсказывает | digits | MLflow | +| `compare_runs.py` | `MlflowClient.search_runs`, топ-N по метрике | — | MLflow API | +| `autolog_demo.py` | `mlflow.autolog()` для GradientBoosting | wine | sklearn | +| `hyperparam_sweep.py` | ручной перебор комбинаций, `--max-combos` | digits | sklearn | +| `serve_model.py` | Flask-сервер `/predict`, грузит модель из реестра | digits | Flask + MLflow | +| `grid_search_cv.py` | `GridSearchCV`, autolog залогит каждую попытку | digits | sklearn | + +--- + +## 🖥️ Платформенная специфика + +### Выбор устройства в `train_gpu.py` +Функция `get_device()` определяет устройство в порядке: +**CUDA → MPS (Apple Silicon) → CPU**. Тег MLflow `device_type` хранит выбор +(`cuda` / `mps` / `cpu`) — по нему удобно фильтровать запуски в UI. + +### `num_workers` в DataLoader +На **macOS с MPS** многопроцессная загрузка нестабильна (fork + MPS), +поэтому `num_workers=0` на Darwin и `4` на Linux. `pin_memory=True` +только для CUDA. Логика — в `train_gpu.py` рядом с `DataLoader`. + +### `setup_server.sh` — развивка по ОС +- `Darwin` → macOS: проверка Homebrew, `python@3.12` через brew, PyTorch + из PyPI (без CUDA), проверка `torch.backends.mps.is_available()`. +- `Linux` → GPU-сервер: `nvidia-smi`, PyTorch из + `--index-url https://download.pytorch.org/whl/cu121`, проверка VRAM. + +При добавлении новой платформы — расширяй `case "$OS"` в начале скрипта. + +--- + +## 🚀 Стандартные команды + +```bash +# Окружение +bash setup_server.sh # установка (macOS или Linux) +source .venv/bin/activate +./start_ui.sh # MLflow UI → http://localhost:5555 + +# Уроки (в отдельном терминале, с активированным venv) +python src/train_simple.py --n-estimators 100 --max-depth 8 +python src/train_gpu.py --epochs 5 --batch-size 128 --lr 0.001 # macOS/MPS +python src/register_model.py --experiment digits_classification +python src/load_and_predict.py +python src/compare_runs.py --experiment digits_classification --top 10 +python src/autolog_demo.py +python src/hyperparam_sweep.py --max-combos 20 +python src/serve_model.py --port 5001 +python src/grid_search_cv.py + +# Через MLflow Projects (изолированное окружение по MLproject) +mlflow run . -P n_estimators=100 -P max_depth=8 +mlflow run . -P epochs=10 -P batch_size=256 -P lr=0.001 --entry-point gpu +``` + +--- + +## 🔧 Конвенции кода + +- **Датасеты** — только встроенные (`load_digits`, `load_wine`, MNIST через + `torchvision`). Никаких внешних файлов в `data/`, кроме скачанного MNIST. +- **Логирование** — параметры/метрики/модель/артефакты. Графики сохраняются + в `artifacts/` и логируются через `mlflow.log_artifact`. +- **Имена экспериментов**: `digits_classification`, `mnist_cnn_gpu`, + `wine_autolog` и т.п. Модель в реестре — `digits_rf_model`. +- **`matplotlib.use("Agg")`** обязателен перед импортом pyplot — скрипты + работают без GUI (на сервере). +- **Python ≥ 3.10** (см. `python_env.yaml`). type hints опциональны. +- **CLI через `argparse`** — все скрипты с параметрами запускаются из CLI. + +--- + +## ⚠️ Известные шероховатости / TODO + +- ~~Нумерация уроков 8/9 расходилась между докстрингами и README/CODE_WALKTHROUGH~~ — + **исправлено**: канон `serve_model.py`=Урок 8 / `grid_search_cv.py`=Урок 9, + README.md и CODE_WALKTHROUGH.md выровнены под докстринги (порядок секций тоже). +- `requirements.txt` включает `torch`/`torchvision` из PyPI (CPU/MPS). + Для CUDA их нужно ставить отдельно через `--index-url .../whl/cu121` + (см. `setup_server.sh`, ветка Linux). +- **Порт MLflow UI = 5555** (не 5000): на macOS порт 5000 занят системным сервисом AirPlay Receiver (процесс `ControlCenter`), поэтому `start_ui.sh` использует 5555. На Linux можно вернуть 5000, но 5555 работает везде. +- **MLflow 3.x: `artifact_path` устарел** — во всех `log_model` используется `name=...` (был `artifact_path=...`). При обновлении/добавлении log_model не используй `artifact_path`. +- **Сигнатура модели**: `train_simple.py` логирует модель с явной `signature=` и `input_example=` (через `mlflow.models.signature.infer_signature`). Без этого MLflow пишет warning «Model logged without a signature and input example». +- `train_gpu.py` передаёт `input_example` → сигнатура авто-infer'ится. +- `autolog_demo.py` намеренно отключает сигнатуры (`log_model_signatures=False`) как демо опций autolog — там warning ожидаем. +- **INFO про переменные окружения** (`OPENAI_API_KEY` и т.п.) при логировании подавлено через `MLFLOW_RECORD_ENV_VARS_IN_MODEL_LOGGING=false` в скриптах, логирующих модели. +- **`input_example` для `mlflow.pytorch.log_model` — только numpy**, не `torch.Tensor`: MLflow 3.x валидирует пример входа и падает с `MlflowException ... but got ''`. В `train_gpu.py` передаётся `np.random.rand(1,1,28,28).astype(np.float32)`. Аналогично для других flavor'ов — пример должен быть DataFrame/ndarray/dict/list/скаляр. +- **urllib3 `NotOpenSSLWarning`** на macOS (системный `ssl` собран с LibreSSL 2.8.3, urllib3 v2 просит OpenSSL 1.1.1+) — стороннее шумовое предупреждение, к проекту отношения не имеет; не подавляем (чтобы не прятать реальные предупреждения). +- **«Эксперимент не виден в UI» (главная причина)**: скрипты по умолчанию пишут в локальный file store `./mlruns`, а `start_ui.sh` раньше поднимал сервер на **SQLite** (`mlflow.db`) — два разных хранилища, UI ничего не показывал. Теперь `start_ui.sh` использует `--backend-store-uri file://${PWD}/mlruns` — **то же хранилище, что и скрипты**, поэтому всё видно без настройки tracking URI. Альтернатива (SQLite-бэкенд) оставлена закомментированной в `start_ui.sh`; при её включении в скриптах нужно задать `mlflow.set_tracking_uri('http://localhost:5555')` (или `MLFLOW_TRACKING_URI`). +- `mlruns/`, `artifacts/*`, `data/MNIST/`, `mlflow.db` — в `.gitignore`, + в репозиторий не попадают. + +--- + +## 🔄 Когда обновлять этот файл + +**AGENTS.md — живой документ. Обновляй его при:** + +1. **Добавлении/удалении файла в `src/`** — обнови карту проекта и таблицу ролей. +2. **Изменении платформенной логики** (новое устройство, новая ОС в + `setup_server.sh`, изменение `get_device()` или `num_workers`) — обнови + раздел «Платформенная специфика». +3. **Изменении имён экспериментов/моделей** в реестре — обнови «Конвенции». +4. **Изменении зависимостей или версии Python** (`requirements.txt`, + `python_env.yaml`) — проверь раздел команд и конвенции. +5. **Появлении новых entry points в `MLproject`** — добавь в карту и команды. +6. **Обнаружении gotcha/шероховатости** — занеси в «Известные шероховатости», + чтобы следующий агент не наступал повторно. + +Правило простое: **если ты что-то изменил в структуре/поведении проекта, +отрази это в AGENTS.md в том же коммите.** Если файл устарел относительно +кода — это баг, который нужно исправить. + +--- + +## 📌 Шпаргалка по MLflow API (часто используемое в проекте) + +```python +mlflow.set_experiment("name") +with mlflow.start_run(run_name="...") as run: + mlflow.log_param("k", v) + mlflow.log_metric("acc", 0.95, step=epoch) + mlflow.log_artifact("artifacts/curve.png") + mlflow.sklearn.log_model(model, "model") # sklearn flavor + mlflow.pytorch.log_model(model, "model") # pytorch flavor + mlflow.set_tag("device_type", "mps") + mlflow.autolog() # авто-логирование + +# Model Registry +client = MlflowClient() +client.search_runs([experiment_id], order_by=["metrics.accuracy DESC"]) +mlflow.register_model("runs://model", "digits_rf_model") +client.transition_model_version_stage("digits_rf_model", version=1, stage="Production") +mlflow.pyfunc.load_model("models:/digits_rf_model/Production") +``` + +--- + +## 🧭 С чего начать новому агенту + +1. Прочитай этот файл целиком. +2. Прочитай `README.md` (пользовательский взгляд) и `CODE_WALKTHROUGH.md` + (детальный разбор кода). +3. Прогони `bash setup_server.sh` на целевой платформе, чтобы понять + состояние окружения. +4. Перед правками проверь `git status` — в этом окружении `.git` может быть + read-only для агента (тогда коммит делает пользователь вручную). +5. Любое структурное изменение → коммит + правка AGENTS.md. diff --git a/CODE_WALKTHROUGH.md b/CODE_WALKTHROUGH.md index 5b45db1..271188a 100644 --- a/CODE_WALKTHROUGH.md +++ b/CODE_WALKTHROUGH.md @@ -23,8 +23,8 @@ mlflow-practice/ ├── 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 сервер модели + ├── serve_model.py # Урок 8: REST API сервер модели + └── grid_search_cv.py # Урок 9: GridSearchCV + autolog ``` --- @@ -379,23 +379,6 @@ CV делит train на 5 фолдов, обучает 5 раз, усредня --- -## 📄 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 ### Загрузка модели при старте @@ -448,6 +431,23 @@ MLflow сам поднимает сервер с стандартным API. К --- +## 📄 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 — детали. + +--- + ## 📄 MLproject — воспроизводимость ```yaml diff --git a/README.md b/README.md index 20a4148..961cc48 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,8 @@ mlflow-practice/ ├── 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 сервер + ├── serve_model.py # Урок 8: REST API сервер + └── grid_search_cv.py # Урок 9: GridSearchCV + autolog ``` --- @@ -52,7 +52,7 @@ pip install flask # для serve_model.py ```bash ./start_ui.sh -# → http://10.0.0.7:5000 +# → http://10.0.0.7:5555 ``` ### 3. Уроки по порядку @@ -81,12 +81,12 @@ 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) +# Урок 8 — Model serving (REST API) python src/serve_model.py --port 5001 # → curl http://localhost:5001/predict_random + +# Урок 9 — GridSearchCV + autolog (parent/child runs) +python src/grid_search_cv.py ``` ### 4. MLflow Projects (воспроизводимый запуск) diff --git a/scripts/setup_git.sh b/scripts/setup_git.sh deleted file mode 100755 index ca201a5..0000000 --- a/scripts/setup_git.sh +++ /dev/null @@ -1,148 +0,0 @@ -#!/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 " (если спросит пароль — токен не подхватился)" diff --git a/setup_server.sh b/setup_server.sh index bc703ef..0952c0d 100755 --- a/setup_server.sh +++ b/setup_server.sh @@ -1,56 +1,134 @@ #!/bin/bash # ============================================================ -# Установка окружения MLflow на GPU-сервере (RTX 3090) -# Запускать НА СЕРВЕРЕ: bash setup_server.sh +# Установка окружения MLflow Practice +# Поддержка: Linux (GPU-сервер, RTX 3090) и macOS (Intel/Apple Silicon) +# Запускать: bash setup_server.sh # ============================================================ set -e echo "============================================" -echo " MLflow Practice — установка на сервере" +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 +# --- Определение ОС --- +OS="$(uname -s)" +case "$OS" in + Darwin*) PLATFORM="macos" ;; + Linux*) PLATFORM="linux" ;; + *) echo "❌ Неподдерживаемая ОС: $OS"; exit 1 ;; +esac +ARCH="$(uname -m)" +echo "📍 Платформа: $PLATFORM ($ARCH)" + +# ============================================ +# macOS — установка без CUDA (есть MPS на Apple Silicon) +# ============================================ +if [ "$PLATFORM" = "macos" ]; then + + echo "" + echo "🍎 macOS: подготовка системы..." + + # --- Homebrew --- + if ! command -v brew &> /dev/null; then + echo "⚠️ Homebrew не найден. Установите его вручную:" + echo " /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"" + echo " и запустите скрипт снова." + exit 1 + fi + echo " Homebrew: $(brew --version | head -1)" + + # --- Python через brew (если нет) --- + if ! command -v python3 &> /dev/null; then + echo "📥 Устанавливаю python@3.13 через brew..." + brew install python@3.13 + fi + echo "🐍 Python: $(python3.13 --version)" + + # --- Создание venv --- + echo "" + echo "📦 Создание виртуального окружения venv..." + if [ ! -d ".venv" ]; then + python3.13 -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 (CPU на Intel, MPS на Apple Silicon) --- + echo "" + echo "🔥 Установка PyTorch (PyPI — без CUDA)..." + pip install torch torchvision + + # --- Проверка PyTorch + устройства --- + echo "" + echo "✅ Проверка PyTorch..." + python3 -c " +import torch, platform +print(f'PyTorch: {torch.__version__}') +print(f'MPS available: {torch.backends.mps.is_available()}') +if torch.backends.mps.is_available(): + print('Устройство: Apple Silicon GPU (MPS)') +else: + print('Устройство: CPU (Intel Mac)') +print('ℹ️ На macOS CUDA недоступна — train_gpu.py будет использовать CPU/MPS') +" + +# ============================================ +# Linux — GPU-сервер с CUDA (RTX 3090) +# ============================================ else - echo "⚠️ nvidia-smi не найден — CUDA может быть не установлена" -fi -# --- Проверка Python --- -echo "" -echo "🐍 Проверка Python..." -python3 --version + # --- Проверка 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 -# --- Создание виртуального окружения --- -echo "" -echo "📦 Создание виртуального окружения venv..." -if [ ! -d ".venv" ]; then - python3 -m venv .venv -fi -source .venv/bin/activate -echo " Активировано: $(which python)" + # --- Проверка Python --- + echo "" + echo "🐍 Проверка Python..." + python3 --version -# --- Обновление pip --- -echo "" -echo "⬆️ Обновление pip..." -pip install --upgrade pip wheel setuptools + # --- Создание venv --- + echo "" + echo "📦 Создание виртуального окружения venv..." + if [ ! -d ".venv" ]; then + python3 -m venv .venv + fi + source .venv/bin/activate + echo " Активировано: $(which python)" -# --- Установка MLflow и базовых библиотек --- -echo "" -echo "📥 Установка MLflow + scikit-learn + matplotlib..." -pip install mlflow scikit-learn pandas numpy matplotlib + # --- pip --- + echo "" + echo "⬆️ Обновление pip..." + pip install --upgrade pip wheel setuptools -# --- Установка PyTorch под CUDA --- -echo "" -echo "🔥 Установка PyTorch (CUDA 12.1)..." -pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 + # --- MLflow + базовые библиотеки --- + echo "" + echo "📥 Установка MLflow + scikit-learn + matplotlib..." + pip install mlflow scikit-learn pandas numpy matplotlib -# --- Проверка CUDA --- -echo "" -echo "✅ Проверка PyTorch + CUDA..." -python3 -c " + # --- PyTorch под CUDA 12.1 --- + echo "" + echo "🔥 Установка PyTorch (CUDA 12.1)..." + pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 + + # --- Проверка PyTorch + CUDA --- + echo "" + echo "✅ Проверка PyTorch + CUDA..." + python3 -c " import torch print(f'PyTorch: {torch.__version__}') print(f'CUDA available: {torch.cuda.is_available()}') @@ -58,15 +136,18 @@ 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') " +fi -# --- Проверка MLflow --- +# ============================================ +# Общая проверка MLflow (обе платформы) +# ============================================ echo "" echo "✅ Проверка MLflow..." python3 -c "import mlflow; print(f'MLflow: {mlflow.__version__}')" echo "" echo "============================================" -echo " ✅ Установка завершена!" +echo " ✅ Установка завершена! ($PLATFORM)" echo "============================================" echo "" echo "Следующие шаги:" @@ -74,6 +155,11 @@ 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" +if [ "$PLATFORM" = "macos" ]; then + echo " python src/train_simple.py --n-estimators 100 --max-depth 8" + echo " python src/train_gpu.py --epochs 3 --batch-size 64 --lr 0.01 # CPU/MPS, меньше эпох" +else + 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" +fi echo "" diff --git a/src/autolog_demo.py b/src/autolog_demo.py index 4c9a3cf..f22093a 100644 --- a/src/autolog_demo.py +++ b/src/autolog_demo.py @@ -13,11 +13,15 @@ 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 +from sklearn.metrics import accuracy_score, recall_score import mlflow import mlflow.sklearn +# Подавляем INFO MLflow о переменных окружения (напр. OPENAI_API_KEY) при логировании модели +import os +os.environ.setdefault("MLFLOW_RECORD_ENV_VARS_IN_MODEL_LOGGING", "false") + def main(): # ─── КЛЮЧЕВАЯ СТРОКА ────────────────────────────────────── @@ -60,12 +64,18 @@ def main(): # Можно добавить и ручные метрики — они дополнят автолог y_pred = model.predict(X_test) acc = accuracy_score(y_test, y_pred) + recall_macro = recall_score(y_test, y_pred, average="macro") + recall_weighted = recall_score(y_test, y_pred, average="weighted") mlflow.log_metric("manual_accuracy", acc) + mlflow.log_metric("manual_recall_macro", recall_macro) + mlflow.log_metric("manual_recall_weighted", recall_weighted) print(f"\n✅ Готово! Откройте MLflow UI:") print(f" Эксперимент: autolog_wine") print(f" Run: {run.info.run_id}") - print(f" Accuracy: {acc:.4f}") + print(f" Accuracy: {acc:.4f}") + print(f" Recall (macro): {recall_macro:.4f}") + print(f" Recall (weighted):{recall_weighted:.4f}") print(f"\n🔍 Что autolog залогировал автоматически:") print(f" • Параметры: n_estimators, learning_rate, max_depth, ...") print(f" • Метрики: training_accuracy, training_log_loss, ...") diff --git a/src/compare_runs.py b/src/compare_runs.py index a3ed9f0..c1f398b 100644 --- a/src/compare_runs.py +++ b/src/compare_runs.py @@ -35,19 +35,35 @@ def main(): print("❌ Нет запусков!") return + # Какие метрики показывать (показываем recall везде, где он есть) + metric_cols = [args.metric, "recall_macro", "recall_weighted", + "test_recall_macro", "test_recall_weighted", + "manual_recall_macro"] + print(f"📊 Топ-{len(runs)} запусков в '{args.experiment}' по {args.metric}:") - print(f"{'#':>3} | {'Run ID':>36} | {'accuracy':>9} | {'n_est':>6} | {'depth':>5}") - print("-" * 75) + header = f"{'#':>3} | {'Run ID':>20} | {'n_est':>6} | {'depth':>5}" + for m in metric_cols: + header += f" | {m[:14]:>14}" + print(header) + print("-" * len(header)) 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}") + row = f"{i+1:>3} | {run_id[:20]:>20} | {str(n_est):>6} | {str(depth):>5}" + for m in metric_cols: + v = run.data.metrics.get(m) + row += f" | {v:>14.4f}" if v is not None else f" | {'—':>14}" + print(row) best = runs[0] - print(f"\n🏆 Лучший: accuracy={best.data.metrics.get(args.metric, 0):.4f}") + print(f"\n🏆 Лучший по {args.metric}: {best.data.metrics.get(args.metric, 0):.4f}") + # Покажем recall лучшего, если он залогирован + for rm in ("recall_macro", "test_recall_macro", "manual_recall_macro"): + rv = best.data.metrics.get(rm) + if rv is not None: + print(f" {rm} = {rv:.4f}") if __name__ == "__main__": diff --git a/src/grid_search_cv.py b/src/grid_search_cv.py index 80d2c4a..546b77b 100644 --- a/src/grid_search_cv.py +++ b/src/grid_search_cv.py @@ -10,11 +10,15 @@ MLflow autolog автоматически залогирует КАЖУЮ поп 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 +from sklearn.metrics import accuracy_score, recall_score import mlflow import mlflow.sklearn +# Подавляем INFO MLflow о переменных окружения (напр. OPENAI_API_KEY) при логировании модели +import os +os.environ.setdefault("MLFLOW_RECORD_ENV_VARS_IN_MODEL_LOGGING", "false") + def main(): # autolog для sklearn — залогирует все промежуточные попытки GridSearch @@ -56,7 +60,8 @@ def main(): estimator=RandomForestClassifier(random_state=42, n_jobs=-1), param_grid=param_grid, cv=5, - scoring="accuracy", + scoring=["accuracy", "recall_macro"], # мульти-метрика: accuracy + recall + refit="accuracy", # лучший выбираем по accuracy n_jobs=-1, verbose=1, ) @@ -66,14 +71,23 @@ def main(): best = grid.best_estimator_ y_pred = best.predict(X_test) test_acc = accuracy_score(y_test, y_pred) + test_recall_macro = recall_score(y_test, y_pred, average="macro") + test_recall_weighted = recall_score(y_test, y_pred, average="weighted") + best_cv_recall = grid.cv_results_["mean_test_recall_macro"][grid.best_index_] mlflow.log_param("best_params", str(grid.best_params_)) - mlflow.log_metric("best_cv_score", grid.best_score_) + mlflow.log_metric("best_cv_score", grid.best_score_) # accuracy (refit) + mlflow.log_metric("best_cv_recall_macro", best_cv_recall) mlflow.log_metric("test_accuracy", test_acc) + mlflow.log_metric("test_recall_macro", test_recall_macro) + mlflow.log_metric("test_recall_weighted", test_recall_weighted) print(f"\n🏆 Лучшие параметры: {grid.best_params_}") - print(f" CV score: {grid.best_score_:.4f}") - print(f" Test accuracy: {test_acc:.4f}") + print(f" CV accuracy: {grid.best_score_:.4f}") + print(f" CV recall_macro: {best_cv_recall:.4f}") + print(f" Test accuracy: {test_acc:.4f}") + print(f" Test recall_macro: {test_recall_macro:.4f}") + print(f" Test recall_weighted: {test_recall_weighted:.4f}") print(f"\n📊 В MLflow UI:") print(f" Эксперимент: gridsearch_digits") print(f" Parent run: {run.info.run_id}") diff --git a/src/hyperparam_sweep.py b/src/hyperparam_sweep.py index 4ba99d1..a50a188 100644 --- a/src/hyperparam_sweep.py +++ b/src/hyperparam_sweep.py @@ -104,6 +104,7 @@ def main(): 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_recall_weighted = recall_score(y_test, y_pred, average="weighted") test_precision = precision_score(y_test, y_pred, average="macro") test_f1 = f1_score(y_test, y_pred, average="macro") @@ -112,6 +113,7 @@ def main(): 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_recall_weighted", test_recall_weighted) mlflow.log_metric("test_precision_macro", test_precision) mlflow.log_metric("test_f1_macro", test_f1) @@ -129,7 +131,7 @@ def main(): 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="") + f"acc={test_acc:.4f} rec={test_recall:.4f} | {run_name[:38]:<38}", end="") print(f"\n\n{'='*60}") print(f"🏆 Лучший результат:") diff --git a/src/load_and_predict.py b/src/load_and_predict.py index 647c11e..b4b2dcb 100644 --- a/src/load_and_predict.py +++ b/src/load_and_predict.py @@ -29,10 +29,25 @@ def main(): print(" Сначала запустите train_simple.py и register_model.py") return - # Делаем предсказание на нескольких образцах + # --- Качество на отложенной тестовой выборке (как в train_simple.py) --- + from sklearn.model_selection import train_test_split + from sklearn.metrics import accuracy_score, recall_score + 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 + ) + y_pred = model.predict(X_test) + acc = accuracy_score(y_test, y_pred) + recall_macro = recall_score(y_test, y_pred, average="macro") + recall_weighted = recall_score(y_test, y_pred, average="weighted") + print(f"\n📈 Качество загруженной модели на тесте ({len(y_test)} образов):") + print(f" Accuracy: {acc:.4f}") + print(f" Recall (macro): {recall_macro:.4f}") + print(f" Recall (weighted): {recall_weighted:.4f}") + # --- Демо: предсказание на нескольких случайных образцах --- n_samples = 5 indices = np.random.choice(len(X), n_samples, replace=False) samples = X[indices] @@ -46,8 +61,8 @@ def main(): 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%}") + demo_acc = np.mean(predictions == true_labels) + print(f"\nТочность на {n_samples} образцах: {demo_acc:.2%}") if __name__ == "__main__": diff --git a/src/register_model.py b/src/register_model.py index 89e272a..5550758 100644 --- a/src/register_model.py +++ b/src/register_model.py @@ -1,8 +1,12 @@ """ Урок 3: Model Registry — управление версиями моделей ===================================================== -Ищет лучший run по метрике accuracy, регистрирует модель -в Model Registry и переводит её в стадию Production. +Ищет лучший run по заданной метрике (по умолчанию accuracy), +регистрирует модель в Model Registry и переводит её в стадию Production. + +Можно выбирать лучшую модель по recall: + python src/register_model.py --experiment digits_classification --metric recall_macro + python src/register_model.py --experiment sweep_digits --metric test_recall_macro Запуск: python src/register_model.py --experiment digits_classification diff --git a/src/train_gpu.py b/src/train_gpu.py index 7c0bbe0..b288c42 100644 --- a/src/train_gpu.py +++ b/src/train_gpu.py @@ -2,11 +2,15 @@ Урок 2: MLflow + PyTorch на GPU (RTX 3090) ============================================= Обучаем CNN на MNIST. Автоматически использует CUDA если доступна, -иначе — CPU. Все параметры, метрики и модель логируются в MLflow. +иначе — CPU. На Apple Silicon используется MPS. +Все параметры, метрики и модель логируются в MLflow. -Запуск на 3090: +Запуск на 3090 (CUDA): python src/train_gpu.py --epochs 10 --batch-size 256 --lr 0.001 +Запуск на Apple Silicon (MPS): + python src/train_gpu.py --epochs 5 --batch-size 128 --lr 0.001 + Запуск на CPU (для теста): python src/train_gpu.py --epochs 2 --batch-size 64 --lr 0.01 """ @@ -17,15 +21,21 @@ import time import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt +import numpy as np import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, transforms +from sklearn.metrics import recall_score import mlflow import mlflow.pytorch +# Подавляем INFO MLflow о переменных окружения (напр. OPENAI_API_KEY) при логировании модели +import os +os.environ.setdefault("MLFLOW_RECORD_ENV_VARS_IN_MODEL_LOGGING", "false") + # ─── Модель: простая CNN ─── class SimpleCNN(nn.Module): @@ -52,6 +62,15 @@ class SimpleCNN(nn.Module): return self.classifier(x) +def get_device() -> torch.device: + """Выбор устройства: CUDA -> MPS (Apple Silicon) -> CPU.""" + if torch.cuda.is_available(): + return torch.device("cuda") + if getattr(torch.backends, "mps", None) is not None and torch.backends.mps.is_available(): + return torch.device("mps") + return torch.device("cpu") + + def main(): parser = argparse.ArgumentParser(description="MLflow + PyTorch GPU demo") parser.add_argument("--epochs", type=int, default=5) @@ -60,13 +79,16 @@ def main(): 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") + # ─── Устройство: CUDA -> MPS -> CPU ─── + device = get_device() print(f"🖥️ Устройство: {device}") + gpu_name = None 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)") + elif device.type == "mps": + print(" Apple Silicon GPU (MPS)") # ─── MLflow эксперимент ─── mlflow.set_experiment(args.experiment_name) @@ -80,9 +102,22 @@ def main(): 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)}") + + # num_workers: на macOS с MPS многопроцессная загрузка нестабильна + # (fork + MPS) → используем 0 воркеров на mac, 4 на Linux/CUDA. + import platform + num_workers = 0 if platform.system() == "Darwin" else 4 + pin_memory = device.type == "cuda" + train_loader = DataLoader( + train_ds, batch_size=args.batch_size, shuffle=True, + num_workers=num_workers, pin_memory=pin_memory, + ) + test_loader = DataLoader( + test_ds, batch_size=args.batch_size, shuffle=False, + num_workers=num_workers, pin_memory=pin_memory, + ) + print(f"📊 Train: {len(train_ds)}, Test: {len(test_ds)} " + f"(num_workers={num_workers}, pin_memory={pin_memory})") # ─── MLflow run ─── with mlflow.start_run(run_name=f"cnn_e{args.epochs}_bs{args.batch_size}") as run: @@ -130,6 +165,7 @@ def main(): # ─── Валидация ─── model.eval() correct, total = 0, 0 + all_preds, all_targets = [], [] with torch.no_grad(): for data, target in test_loader: data, target = data.to(device), target.to(device) @@ -137,18 +173,28 @@ def main(): pred = output.argmax(dim=1) correct += (pred == target).sum().item() total += target.size(0) + all_preds.append(pred.cpu()) + all_targets.append(target.cpu()) acc = correct / total + # Recall (macro) — полнота по всем классам MNIST + recall_macro = recall_score( + torch.cat(all_targets).numpy(), + torch.cat(all_preds).numpy(), + average="macro", + zero_division=0, + ) test_accs.append(acc) elapsed = time.time() - t0 # Логируем метрики по эпохам mlflow.log_metric("train_loss", avg_loss, step=epoch) mlflow.log_metric("test_accuracy", acc, step=epoch) + mlflow.log_metric("test_recall_macro", recall_macro, 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") + f"acc={acc:.4f}, recall={recall_macro:.4f}, time={elapsed:.1f}s") # ─── График обучения ─── fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4)) @@ -166,17 +212,20 @@ def main(): plt.close(fig) # ─── Логируем модель ─── + # input_example — numpy ndarray (MLflow 3.x не принимает torch.Tensor). + # Форма как у входа модели: (batch, channels, H, W) = (1, 1, 28, 28). mlflow.pytorch.log_model( model, - artifact_path="model", + name="model", # name вместо устаревшего artifact_path registered_model_name=None, - input_example=torch.randn(1, 1, 28, 28).to(device), + input_example=np.random.rand(1, 1, 28, 28).astype(np.float32), ) # ─── Теги ─── mlflow.set_tag("model_type", "SimpleCNN") mlflow.set_tag("framework", "PyTorch") mlflow.set_tag("dataset", "MNIST") + mlflow.set_tag("device_type", device.type) final_acc = test_accs[-1] print(f"\n✅ Обучение завершено!") diff --git a/src/train_simple.py b/src/train_simple.py index 2f33238..cd516bc 100644 --- a/src/train_simple.py +++ b/src/train_simple.py @@ -15,12 +15,17 @@ 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 +from sklearn.metrics import accuracy_score, confusion_matrix, classification_report, recall_score from sklearn.model_selection import train_test_split import mlflow import mlflow.sklearn +# Подавляем INFO MLflow о переменных окружения (напр. OPENAI_API_KEY) при логировании модели +import os +os.environ.setdefault("MLFLOW_RECORD_ENV_VARS_IN_MODEL_LOGGING", "false") +from mlflow.models.signature import infer_signature + def main(): parser = argparse.ArgumentParser(description="MLflow + scikit-learn demo") @@ -62,10 +67,16 @@ def main(): # --- Предсказание и метрики --- y_pred = model.predict(X_test) acc = accuracy_score(y_test, y_pred) - print(f"Accuracy: {acc:.4f}") + recall_macro = recall_score(y_test, y_pred, average="macro") + recall_weighted = recall_score(y_test, y_pred, average="weighted") + print(f"Accuracy: {acc:.4f}") + print(f"Recall (macro): {recall_macro:.4f}") + print(f"Recall (weighted): {recall_weighted:.4f}") # Логируем метрики mlflow.log_metric("accuracy", acc) + mlflow.log_metric("recall_macro", recall_macro) + mlflow.log_metric("recall_weighted", recall_weighted) # можно логировать несколько шагов (для графиков в UI) for i, tree in enumerate(model.estimators_): tree_acc = accuracy_score(y_test, tree.predict(X_test)) @@ -92,11 +103,14 @@ def main(): f.write(report) mlflow.log_artifact(report_path) - # --- Логируем саму модель --- + # --- Логируем саму модель (с сигнатурой и примером входа) --- + signature = infer_signature(X_test[:5], model.predict(X_test[:5])) mlflow.sklearn.log_model( model, - artifact_path="model", - registered_model_name=None, # регистрация — в отдельном скрипте + name="model", # name вместо устаревшего artifact_path + signature=signature, # явная сигнатура модели + input_example=X_test[:5], # пример входа → убирает warning + registered_model_name=None, # регистрация — в отдельном скрипте ) # --- Теги --- diff --git a/start_ui.sh b/start_ui.sh index 5537389..0033470 100755 --- a/start_ui.sh +++ b/start_ui.sh @@ -1,21 +1,40 @@ #!/bin/bash # ============================================================ -# Запуск MLflow Tracking Server с SQLite backend -# Откройте в браузере: http://localhost:5000 +# Запуск MLflow Tracking Server +# Откройте в браузере: http://localhost:5555 +# +# Backend store = file://$(pwd)/mlruns (тот же каталог, куда скрипты +# пишут по умолчанию). Поэтому UI видит все эксперименты/запуски +# без доп. настройки tracking URI в скриптах. +# +# Альтернатива — SQLite-бэкенд (нужен, только если хотите SQL-запросы +# к запускам). Тогда раскомментируйте блок SQLITE ниже и закомментируйте +# FILE, а в скриптах задайте mlflow.set_tracking_uri("http://localhost:5555") +# (или export MLFLOW_TRACKING_URI=http://localhost:5555). # ============================================================ set -e cd "$(dirname "$0")" +PORT=5555 + echo "🚀 Запускаем MLflow Tracking Server..." -echo " UI: http://localhost:5000" -echo " DB: sqlite:///mlflow.db" -echo " Artifacts: ./artifacts" +echo " UI: http://localhost:${PORT}" +echo " Store: file://${PWD}/mlruns" +echo " Artifacts: file://${PWD}/artifacts" echo "" echo " Нажмите Ctrl+C для остановки" echo "" +# ─── FILE backend (по умолчанию — единое хранилище со скриптами) ─── mlflow server \ - --backend-store-uri sqlite:///mlflow.db \ - --default-artifact-root ./artifacts \ + --backend-store-uri "file://${PWD}/mlruns" \ + --default-artifact-root "file://${PWD}/artifacts" \ --host 0.0.0.0 \ - --port 5000 + --port "${PORT}" + +# ─── SQLITE backend (альтернатива — см. комментарий в шапке) ────── +# mlflow server \ +# --backend-store-uri sqlite:///mlflow.db \ +# --default-artifact-root ./artifacts \ +# --host 0.0.0.0 \ +# --port "${PORT}"