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:
@@ -9,3 +9,6 @@ data/MNIST/
|
||||
|
||||
# Flask serve
|
||||
instance/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 (воспроизводимый запуск)
|
||||
|
||||
@@ -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 " (если спросит пароль — токен не подхватился)"
|
||||
+126
-40
@@ -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
|
||||
|
||||
# --- Проверка 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
|
||||
fi
|
||||
|
||||
# --- Проверка Python ---
|
||||
echo ""
|
||||
echo "🐍 Проверка Python..."
|
||||
python3 --version
|
||||
# --- Проверка Python ---
|
||||
echo ""
|
||||
echo "🐍 Проверка Python..."
|
||||
python3 --version
|
||||
|
||||
# --- Создание виртуального окружения ---
|
||||
echo ""
|
||||
echo "📦 Создание виртуального окружения venv..."
|
||||
if [ ! -d ".venv" ]; then
|
||||
# --- Создание venv ---
|
||||
echo ""
|
||||
echo "📦 Создание виртуального окружения venv..."
|
||||
if [ ! -d ".venv" ]; then
|
||||
python3 -m venv .venv
|
||||
fi
|
||||
source .venv/bin/activate
|
||||
echo " Активировано: $(which python)"
|
||||
fi
|
||||
source .venv/bin/activate
|
||||
echo " Активировано: $(which python)"
|
||||
|
||||
# --- Обновление pip ---
|
||||
echo ""
|
||||
echo "⬆️ Обновление pip..."
|
||||
pip install --upgrade pip wheel setuptools
|
||||
# --- 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
|
||||
# --- MLflow + базовые библиотеки ---
|
||||
echo ""
|
||||
echo "📥 Установка MLflow + scikit-learn + matplotlib..."
|
||||
pip install mlflow scikit-learn pandas numpy matplotlib
|
||||
|
||||
# --- Установка PyTorch под CUDA ---
|
||||
echo ""
|
||||
echo "🔥 Установка PyTorch (CUDA 12.1)..."
|
||||
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121
|
||||
# --- PyTorch под CUDA 12.1 ---
|
||||
echo ""
|
||||
echo "🔥 Установка PyTorch (CUDA 12.1)..."
|
||||
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121
|
||||
|
||||
# --- Проверка CUDA ---
|
||||
echo ""
|
||||
echo "✅ Проверка PyTorch + CUDA..."
|
||||
python3 -c "
|
||||
# --- Проверка 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 ""
|
||||
|
||||
+11
-1
@@ -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" 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, ...")
|
||||
|
||||
+21
-5
@@ -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__":
|
||||
|
||||
+18
-4
@@ -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" 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}")
|
||||
|
||||
@@ -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"🏆 Лучший результат:")
|
||||
|
||||
+18
-3
@@ -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__":
|
||||
|
||||
@@ -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
|
||||
|
||||
+59
-10
@@ -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✅ Обучение завершено!")
|
||||
|
||||
+17
-3
@@ -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)
|
||||
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,10 +103,13 @@ 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",
|
||||
name="model", # name вместо устаревшего artifact_path
|
||||
signature=signature, # явная сигнатура модели
|
||||
input_example=X_test[:5], # пример входа → убирает warning
|
||||
registered_model_name=None, # регистрация — в отдельном скрипте
|
||||
)
|
||||
|
||||
|
||||
+27
-8
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user