Recall везде + фиксы окружения

- 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
This commit is contained in:
2026-07-20 22:28:23 +03:00
parent d6a4b46db0
commit d5c5bf3767
15 changed files with 547 additions and 257 deletions
+3
View File
@@ -9,3 +9,6 @@ data/MNIST/
# Flask serve # Flask serve
instance/ instance/
# macOS
.DS_Store
+206
View File
@@ -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 '<class 'torch.Tensor'>'`. В `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:/<run_id>/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.
+19 -19
View File
@@ -23,8 +23,8 @@ mlflow-practice/
├── compare_runs.py # Урок 5: сравнение запусков через API ├── compare_runs.py # Урок 5: сравнение запусков через API
├── autolog_demo.py # Урок 6: autolog (автоматическое логирование) ├── autolog_demo.py # Урок 6: autolog (автоматическое логирование)
├── hyperparam_sweep.py # Урок 7: перебор гиперпараметров (вручную) ├── hyperparam_sweep.py # Урок 7: перебор гиперпараметров (вручную)
├── grid_search_cv.py # Урок 8: GridSearchCV + autolog ├── serve_model.py # Урок 8: REST API сервер модели
└── serve_model.py # Урок 9: 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 ## 📄 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 — воспроизводимость ## 📄 MLproject — воспроизводимость
```yaml ```yaml
+7 -7
View File
@@ -28,8 +28,8 @@ mlflow-practice/
├── compare_runs.py # Урок 5: сравнение через API ├── compare_runs.py # Урок 5: сравнение через API
├── autolog_demo.py # Урок 6: autolog (автоматическое) ├── autolog_demo.py # Урок 6: autolog (автоматическое)
├── hyperparam_sweep.py # Урок 7: перебор гиперпараметров ├── hyperparam_sweep.py # Урок 7: перебор гиперпараметров
├── grid_search_cv.py # Урок 8: GridSearchCV + autolog ├── serve_model.py # Урок 8: REST API сервер
└── serve_model.py # Урок 9: REST API сервер └── grid_search_cv.py # Урок 9: GridSearchCV + autolog
``` ```
--- ---
@@ -52,7 +52,7 @@ pip install flask # для serve_model.py
```bash ```bash
./start_ui.sh ./start_ui.sh
# → http://10.0.0.7:5000 # → http://10.0.0.7:5555
``` ```
### 3. Уроки по порядку ### 3. Уроки по порядку
@@ -81,12 +81,12 @@ python src/autolog_demo.py
# Урок 7 — Hyperparameter sweep (20 комбинаций) # Урок 7 — Hyperparameter sweep (20 комбинаций)
python src/hyperparam_sweep.py --max-combos 20 python src/hyperparam_sweep.py --max-combos 20
# Урок 8 — GridSearchCV + autolog (parent/child runs) # Урок 8 — Model serving (REST API)
python src/grid_search_cv.py
# Урок 9 — Model serving (REST API)
python src/serve_model.py --port 5001 python src/serve_model.py --port 5001
# → curl http://localhost:5001/predict_random # → curl http://localhost:5001/predict_random
# Урок 9 — GridSearchCV + autolog (parent/child runs)
python src/grid_search_cv.py
``` ```
### 4. MLflow Projects (воспроизводимый запуск) ### 4. MLflow Projects (воспроизводимый запуск)
-148
View File
@@ -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 " (если спросит пароль — токен не подхватился)"
+128 -42
View File
@@ -1,56 +1,134 @@
#!/bin/bash #!/bin/bash
# ============================================================ # ============================================================
# Установка окружения MLflow на GPU-сервере (RTX 3090) # Установка окружения MLflow Practice
# Запускать НА СЕРВЕРЕ: bash setup_server.sh # Поддержка: Linux (GPU-сервер, RTX 3090) и macOS (Intel/Apple Silicon)
# Запускать: bash setup_server.sh
# ============================================================ # ============================================================
set -e set -e
echo "============================================" echo "============================================"
echo " MLflow Practice — установка на сервере" echo " MLflow Practice — установка окружения"
echo "============================================" echo "============================================"
# --- Проверка GPU --- # --- Определение ОС ---
echo "" OS="$(uname -s)"
echo "📡 Проверка GPU..." case "$OS" in
if command -v nvidia-smi &> /dev/null; then Darwin*) PLATFORM="macos" ;;
nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader 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 else
echo "⚠️ nvidia-smi не найден — CUDA может быть не установлена"
fi
# --- Проверка Python --- # --- Проверка GPU ---
echo "" echo ""
echo "🐍 Проверка Python..." echo "📡 Проверка GPU..."
python3 --version 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 ""
echo "📦 Создание виртуального окружения venv..." echo "🐍 Проверка Python..."
if [ ! -d ".venv" ]; then python3 --version
python3 -m venv .venv
fi
source .venv/bin/activate
echo " Активировано: $(which python)"
# --- Обновление pip --- # --- Создание venv ---
echo "" echo ""
echo "⬆️ Обновление pip..." echo "📦 Создание виртуального окружения venv..."
pip install --upgrade pip wheel setuptools if [ ! -d ".venv" ]; then
python3 -m venv .venv
fi
source .venv/bin/activate
echo " Активировано: $(which python)"
# --- Установка MLflow и базовых библиотек --- # --- pip ---
echo "" echo ""
echo "📥 Установка MLflow + scikit-learn + matplotlib..." echo "⬆️ Обновление pip..."
pip install mlflow scikit-learn pandas numpy matplotlib pip install --upgrade pip wheel setuptools
# --- Установка PyTorch под CUDA --- # --- MLflow + базовые библиотеки ---
echo "" echo ""
echo "🔥 Установка PyTorch (CUDA 12.1)..." echo "📥 Установка MLflow + scikit-learn + matplotlib..."
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 pip install mlflow scikit-learn pandas numpy matplotlib
# --- Проверка CUDA --- # --- PyTorch под CUDA 12.1 ---
echo "" echo ""
echo "✅ Проверка PyTorch + CUDA..." echo "🔥 Установка PyTorch (CUDA 12.1)..."
python3 -c " pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121
# --- Проверка PyTorch + CUDA ---
echo ""
echo "✅ Проверка PyTorch + CUDA..."
python3 -c "
import torch import torch
print(f'PyTorch: {torch.__version__}') print(f'PyTorch: {torch.__version__}')
print(f'CUDA available: {torch.cuda.is_available()}') 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'GPU: {torch.cuda.get_device_name(0)}')
print(f'VRAM: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB') print(f'VRAM: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB')
" "
fi
# --- Проверка MLflow --- # ============================================
# Общая проверка MLflow (обе платформы)
# ============================================
echo "" echo ""
echo "✅ Проверка MLflow..." echo "✅ Проверка MLflow..."
python3 -c "import mlflow; print(f'MLflow: {mlflow.__version__}')" python3 -c "import mlflow; print(f'MLflow: {mlflow.__version__}')"
echo "" echo ""
echo "============================================" echo "============================================"
echo " ✅ Установка завершена!" echo " ✅ Установка завершена! ($PLATFORM)"
echo "============================================" echo "============================================"
echo "" echo ""
echo "Следующие шаги:" echo "Следующие шаги:"
@@ -74,6 +155,11 @@ echo " 1. Активируйте окружение: source .venv/bin/activate
echo " 2. Запустите MLflow UI: ./start_ui.sh" echo " 2. Запустите MLflow UI: ./start_ui.sh"
echo " 3. В другом терминале:" echo " 3. В другом терминале:"
echo " source .venv/bin/activate" echo " source .venv/bin/activate"
echo " python src/train_simple.py --n-estimators 100 --max-depth 8" if [ "$PLATFORM" = "macos" ]; then
echo " python src/train_gpu.py --epochs 10 --batch-size 256 --lr 0.001" 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 "" echo ""
+12 -2
View File
@@ -13,11 +13,15 @@
from sklearn.datasets import load_wine from sklearn.datasets import load_wine
from sklearn.ensemble import GradientBoostingClassifier from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import train_test_split 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
import mlflow.sklearn import mlflow.sklearn
# Подавляем INFO MLflow о переменных окружения (напр. OPENAI_API_KEY) при логировании модели
import os
os.environ.setdefault("MLFLOW_RECORD_ENV_VARS_IN_MODEL_LOGGING", "false")
def main(): def main():
# ─── КЛЮЧЕВАЯ СТРОКА ────────────────────────────────────── # ─── КЛЮЧЕВАЯ СТРОКА ──────────────────────────────────────
@@ -60,12 +64,18 @@ def main():
# Можно добавить и ручные метрики — они дополнят автолог # Можно добавить и ручные метрики — они дополнят автолог
y_pred = model.predict(X_test) y_pred = model.predict(X_test)
acc = accuracy_score(y_test, y_pred) 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_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"\n✅ Готово! Откройте MLflow UI:")
print(f" Эксперимент: autolog_wine") print(f" Эксперимент: autolog_wine")
print(f" Run: {run.info.run_id}") 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🔍 Что autolog залогировал автоматически:")
print(f" • Параметры: n_estimators, learning_rate, max_depth, ...") print(f" • Параметры: n_estimators, learning_rate, max_depth, ...")
print(f" • Метрики: training_accuracy, training_log_loss, ...") print(f" • Метрики: training_accuracy, training_log_loss, ...")
+21 -5
View File
@@ -35,19 +35,35 @@ def main():
print("❌ Нет запусков!") print("❌ Нет запусков!")
return 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"📊 Топ-{len(runs)} запусков в '{args.experiment}' по {args.metric}:")
print(f"{'#':>3} | {'Run ID':>36} | {'accuracy':>9} | {'n_est':>6} | {'depth':>5}") header = f"{'#':>3} | {'Run ID':>20} | {'n_est':>6} | {'depth':>5}"
print("-" * 75) for m in metric_cols:
header += f" | {m[:14]:>14}"
print(header)
print("-" * len(header))
for i, run in enumerate(runs): for i, run in enumerate(runs):
run_id = run.info.run_id run_id = run.info.run_id
acc = run.data.metrics.get(args.metric, 0)
n_est = run.data.params.get("n_estimators", "") n_est = run.data.params.get("n_estimators", "")
depth = run.data.params.get("max_depth", "") 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] 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__": if __name__ == "__main__":
+19 -5
View File
@@ -10,11 +10,15 @@ MLflow autolog автоматически залогирует КАЖУЮ поп
from sklearn.datasets import load_digits from sklearn.datasets import load_digits
from sklearn.ensemble import RandomForestClassifier from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GridSearchCV, train_test_split 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
import mlflow.sklearn import mlflow.sklearn
# Подавляем INFO MLflow о переменных окружения (напр. OPENAI_API_KEY) при логировании модели
import os
os.environ.setdefault("MLFLOW_RECORD_ENV_VARS_IN_MODEL_LOGGING", "false")
def main(): def main():
# autolog для sklearn — залогирует все промежуточные попытки GridSearch # autolog для sklearn — залогирует все промежуточные попытки GridSearch
@@ -56,7 +60,8 @@ def main():
estimator=RandomForestClassifier(random_state=42, n_jobs=-1), estimator=RandomForestClassifier(random_state=42, n_jobs=-1),
param_grid=param_grid, param_grid=param_grid,
cv=5, cv=5,
scoring="accuracy", scoring=["accuracy", "recall_macro"], # мульти-метрика: accuracy + recall
refit="accuracy", # лучший выбираем по accuracy
n_jobs=-1, n_jobs=-1,
verbose=1, verbose=1,
) )
@@ -66,14 +71,23 @@ def main():
best = grid.best_estimator_ best = grid.best_estimator_
y_pred = best.predict(X_test) y_pred = best.predict(X_test)
test_acc = accuracy_score(y_test, y_pred) 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_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_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"\n🏆 Лучшие параметры: {grid.best_params_}")
print(f" CV score: {grid.best_score_:.4f}") print(f" CV accuracy: {grid.best_score_:.4f}")
print(f" Test accuracy: {test_acc:.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"\n📊 В MLflow UI:")
print(f" Эксперимент: gridsearch_digits") print(f" Эксперимент: gridsearch_digits")
print(f" Parent run: {run.info.run_id}") print(f" Parent run: {run.info.run_id}")
+3 -1
View File
@@ -104,6 +104,7 @@ def main():
y_pred = model.predict(X_test) y_pred = model.predict(X_test)
test_acc = accuracy_score(y_test, y_pred) test_acc = accuracy_score(y_test, y_pred)
test_recall = recall_score(y_test, y_pred, average="macro") 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_precision = precision_score(y_test, y_pred, average="macro")
test_f1 = f1_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("cv_std_accuracy", cv_scores.std())
mlflow.log_metric("test_accuracy", test_acc) mlflow.log_metric("test_accuracy", test_acc)
mlflow.log_metric("test_recall_macro", test_recall) 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_precision_macro", test_precision)
mlflow.log_metric("test_f1_macro", test_f1) mlflow.log_metric("test_f1_macro", test_f1)
@@ -129,7 +131,7 @@ def main():
filled = int(bar_len * (i + 1) / len(combos)) filled = int(bar_len * (i + 1) / len(combos))
bar = "" * filled + "" * (bar_len - filled) bar = "" * filled + "" * (bar_len - filled)
print(f"\r [{bar}] {i+1}/{len(combos)} | " 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"\n\n{'='*60}")
print(f"🏆 Лучший результат:") print(f"🏆 Лучший результат:")
+18 -3
View File
@@ -29,10 +29,25 @@ def main():
print(" Сначала запустите train_simple.py и register_model.py") print(" Сначала запустите train_simple.py и register_model.py")
return return
# Делаем предсказание на нескольких образцах # --- Качество на отложенной тестовой выборке (как в train_simple.py) ---
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, recall_score
digits = load_digits() digits = load_digits()
X, y = digits.data, digits.target 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 n_samples = 5
indices = np.random.choice(len(X), n_samples, replace=False) indices = np.random.choice(len(X), n_samples, replace=False)
samples = X[indices] samples = X[indices]
@@ -46,8 +61,8 @@ def main():
ok = "" if predictions[i] == true_labels[i] else "" ok = "" if predictions[i] == true_labels[i] else ""
print(f"{i+1:>6} | {true_labels[i]:>8} | {predictions[i]:>14} | {ok}") print(f"{i+1:>6} | {true_labels[i]:>8} | {predictions[i]:>14} | {ok}")
acc = np.mean(predictions == true_labels) demo_acc = np.mean(predictions == true_labels)
print(f"\nТочность на {n_samples} образцах: {acc:.2%}") print(f"\nТочность на {n_samples} образцах: {demo_acc:.2%}")
if __name__ == "__main__": if __name__ == "__main__":
+6 -2
View File
@@ -1,8 +1,12 @@
""" """
Урок 3: Model Registry — управление версиями моделей Урок 3: Model Registry — управление версиями моделей
===================================================== =====================================================
Ищет лучший run по метрике accuracy, регистрирует модель Ищет лучший run по заданной метрике (по умолчанию accuracy),
в Model Registry и переводит её в стадию Production. регистрирует модель в 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 python src/register_model.py --experiment digits_classification
+59 -10
View File
@@ -2,11 +2,15 @@
Урок 2: MLflow + PyTorch на GPU (RTX 3090) Урок 2: MLflow + PyTorch на GPU (RTX 3090)
============================================= =============================================
Обучаем CNN на MNIST. Автоматически использует CUDA если доступна, Обучаем 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 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 (для теста): Запуск на CPU (для теста):
python src/train_gpu.py --epochs 2 --batch-size 64 --lr 0.01 python src/train_gpu.py --epochs 2 --batch-size 64 --lr 0.01
""" """
@@ -17,15 +21,21 @@ import time
import matplotlib import matplotlib
matplotlib.use("Agg") matplotlib.use("Agg")
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import numpy as np
import torch import torch
import torch.nn as nn import torch.nn as nn
import torch.optim as optim import torch.optim as optim
from torch.utils.data import DataLoader from torch.utils.data import DataLoader
from torchvision import datasets, transforms from torchvision import datasets, transforms
from sklearn.metrics import recall_score
import mlflow import mlflow
import mlflow.pytorch import mlflow.pytorch
# Подавляем INFO MLflow о переменных окружения (напр. OPENAI_API_KEY) при логировании модели
import os
os.environ.setdefault("MLFLOW_RECORD_ENV_VARS_IN_MODEL_LOGGING", "false")
# ─── Модель: простая CNN ─── # ─── Модель: простая CNN ───
class SimpleCNN(nn.Module): class SimpleCNN(nn.Module):
@@ -52,6 +62,15 @@ class SimpleCNN(nn.Module):
return self.classifier(x) 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(): def main():
parser = argparse.ArgumentParser(description="MLflow + PyTorch GPU demo") parser = argparse.ArgumentParser(description="MLflow + PyTorch GPU demo")
parser.add_argument("--epochs", type=int, default=5) 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") parser.add_argument("--experiment-name", type=str, default="mnist_cnn_gpu")
args = parser.parse_args() args = parser.parse_args()
# ─── Устройство ─── # ─── Устройство: CUDA -> MPS -> CPU ───
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") device = get_device()
print(f"🖥️ Устройство: {device}") print(f"🖥️ Устройство: {device}")
gpu_name = None
if device.type == "cuda": if device.type == "cuda":
gpu_name = torch.cuda.get_device_name(0) gpu_name = torch.cuda.get_device_name(0)
gpu_mem = torch.cuda.get_device_properties(0).total_memory / 1e9 gpu_mem = torch.cuda.get_device_properties(0).total_memory / 1e9
print(f" GPU: {gpu_name} ({gpu_mem:.1f} GB)") print(f" GPU: {gpu_name} ({gpu_mem:.1f} GB)")
elif device.type == "mps":
print(" Apple Silicon GPU (MPS)")
# ─── MLflow эксперимент ─── # ─── MLflow эксперимент ───
mlflow.set_experiment(args.experiment_name) mlflow.set_experiment(args.experiment_name)
@@ -80,9 +102,22 @@ def main():
os.makedirs(data_dir, exist_ok=True) os.makedirs(data_dir, exist_ok=True)
train_ds = datasets.MNIST(data_dir, train=True, download=True, transform=transform) train_ds = datasets.MNIST(data_dir, train=True, download=True, transform=transform)
test_ds = datasets.MNIST(data_dir, train=False, 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) # num_workers: на macOS с MPS многопроцессная загрузка нестабильна
print(f"📊 Train: {len(train_ds)}, Test: {len(test_ds)}") # (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 ─── # ─── MLflow run ───
with mlflow.start_run(run_name=f"cnn_e{args.epochs}_bs{args.batch_size}") as 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() model.eval()
correct, total = 0, 0 correct, total = 0, 0
all_preds, all_targets = [], []
with torch.no_grad(): with torch.no_grad():
for data, target in test_loader: for data, target in test_loader:
data, target = data.to(device), target.to(device) data, target = data.to(device), target.to(device)
@@ -137,18 +173,28 @@ def main():
pred = output.argmax(dim=1) pred = output.argmax(dim=1)
correct += (pred == target).sum().item() correct += (pred == target).sum().item()
total += target.size(0) total += target.size(0)
all_preds.append(pred.cpu())
all_targets.append(target.cpu())
acc = correct / total 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) test_accs.append(acc)
elapsed = time.time() - t0 elapsed = time.time() - t0
# Логируем метрики по эпохам # Логируем метрики по эпохам
mlflow.log_metric("train_loss", avg_loss, step=epoch) mlflow.log_metric("train_loss", avg_loss, step=epoch)
mlflow.log_metric("test_accuracy", acc, 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) mlflow.log_metric("epoch_time_sec", elapsed, step=epoch)
print(f" → Epoch {epoch+1}: loss={avg_loss:.4f}, " 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)) fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
@@ -166,17 +212,20 @@ def main():
plt.close(fig) plt.close(fig)
# ─── Логируем модель ─── # ─── Логируем модель ───
# input_example — numpy ndarray (MLflow 3.x не принимает torch.Tensor).
# Форма как у входа модели: (batch, channels, H, W) = (1, 1, 28, 28).
mlflow.pytorch.log_model( mlflow.pytorch.log_model(
model, model,
artifact_path="model", name="model", # name вместо устаревшего artifact_path
registered_model_name=None, 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("model_type", "SimpleCNN")
mlflow.set_tag("framework", "PyTorch") mlflow.set_tag("framework", "PyTorch")
mlflow.set_tag("dataset", "MNIST") mlflow.set_tag("dataset", "MNIST")
mlflow.set_tag("device_type", device.type)
final_acc = test_accs[-1] final_acc = test_accs[-1]
print(f"\n✅ Обучение завершено!") print(f"\n✅ Обучение завершено!")
+19 -5
View File
@@ -15,12 +15,17 @@ import matplotlib.pyplot as plt
import numpy as np import numpy as np
from sklearn.datasets import load_digits from sklearn.datasets import load_digits
from sklearn.ensemble import RandomForestClassifier 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 from sklearn.model_selection import train_test_split
import mlflow import mlflow
import mlflow.sklearn 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(): def main():
parser = argparse.ArgumentParser(description="MLflow + scikit-learn demo") parser = argparse.ArgumentParser(description="MLflow + scikit-learn demo")
@@ -62,10 +67,16 @@ def main():
# --- Предсказание и метрики --- # --- Предсказание и метрики ---
y_pred = model.predict(X_test) y_pred = model.predict(X_test)
acc = accuracy_score(y_test, y_pred) 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("accuracy", acc)
mlflow.log_metric("recall_macro", recall_macro)
mlflow.log_metric("recall_weighted", recall_weighted)
# можно логировать несколько шагов (для графиков в UI) # можно логировать несколько шагов (для графиков в UI)
for i, tree in enumerate(model.estimators_): for i, tree in enumerate(model.estimators_):
tree_acc = accuracy_score(y_test, tree.predict(X_test)) tree_acc = accuracy_score(y_test, tree.predict(X_test))
@@ -92,11 +103,14 @@ def main():
f.write(report) f.write(report)
mlflow.log_artifact(report_path) mlflow.log_artifact(report_path)
# --- Логируем саму модель --- # --- Логируем саму модель (с сигнатурой и примером входа) ---
signature = infer_signature(X_test[:5], model.predict(X_test[:5]))
mlflow.sklearn.log_model( mlflow.sklearn.log_model(
model, model,
artifact_path="model", name="model", # name вместо устаревшего artifact_path
registered_model_name=None, # регистрация — в отдельном скрипте signature=signature, # явная сигнатура модели
input_example=X_test[:5], # пример входа → убирает warning
registered_model_name=None, # регистрация — в отдельном скрипте
) )
# --- Теги --- # --- Теги ---
+27 -8
View File
@@ -1,21 +1,40 @@
#!/bin/bash #!/bin/bash
# ============================================================ # ============================================================
# Запуск MLflow Tracking Server с SQLite backend # Запуск MLflow Tracking Server
# Откройте в браузере: http://localhost:5000 # Откройте в браузере: 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 set -e
cd "$(dirname "$0")" cd "$(dirname "$0")"
PORT=5555
echo "🚀 Запускаем MLflow Tracking Server..." echo "🚀 Запускаем MLflow Tracking Server..."
echo " UI: http://localhost:5000" echo " UI: http://localhost:${PORT}"
echo " DB: sqlite:///mlflow.db" echo " Store: file://${PWD}/mlruns"
echo " Artifacts: ./artifacts" echo " Artifacts: file://${PWD}/artifacts"
echo "" echo ""
echo " Нажмите Ctrl+C для остановки" echo " Нажмите Ctrl+C для остановки"
echo "" echo ""
# ─── FILE backend (по умолчанию — единое хранилище со скриптами) ───
mlflow server \ mlflow server \
--backend-store-uri sqlite:///mlflow.db \ --backend-store-uri "file://${PWD}/mlruns" \
--default-artifact-root ./artifacts \ --default-artifact-root "file://${PWD}/artifacts" \
--host 0.0.0.0 \ --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}"