init commit
This commit is contained in:
558
_research/SECTION_optuna.md
Normal file
558
_research/SECTION_optuna.md
Normal file
@@ -0,0 +1,558 @@
|
||||
---
|
||||
title: "Optuna — подбор гиперпараметров: полный разбор"
|
||||
section: HPO
|
||||
tool: Optuna
|
||||
supervisor: "мнс Павленко Б.В."
|
||||
status: draft
|
||||
tags: [hpo, optuna, hyperparameter-tuning, bayesian-optimization, pruning]
|
||||
---
|
||||
|
||||
# Optuna — библиотека для подбора гиперпараметров
|
||||
|
||||
> Источники: официальный сайт [optuna.org](https://optuna.org/), документация [optuna.readthedocs.io](https://optuna.readthedocs.io/en/stable/), а также блог команды Optuna на Medium (релизы [v4.5](https://medium.com/optuna/optuna-v4-5-81e78d8e077a), [v4.6](https://medium.com/optuna/announcing-optuna-4-6-a9e82183ab07) и [AutoSampler](https://medium.com/optuna/autosampler-full-support-for-multi-objective-constrained-optimization-c1c4fc957ba2)).
|
||||
|
||||
**Optuna** — это open-source фреймворк для автоматической оптимизации гиперпараметров, не привязанный к конкретной ML-библиотеке (framework-agnostic): одинаково применим к PyTorch, TensorFlow/Keras, scikit-learn, XGBoost, LightGBM и др. Ключевые архитектурные принципы по [optuna.org](https://optuna.org/):
|
||||
|
||||
- **Eager / define-by-run search spaces** — пространство поиска описывается обычным Python-кодом (условия, циклы) прямо внутри objective-функции, а не статической конфигурацией заранее.
|
||||
- **State-of-the-art алгоритмы** — эффективные сэмплеры (TPE, GP, CMA-ES) + прунинг бесперспективных trial-ов.
|
||||
- **Простая параллелизация** — распараллеливание по потокам/процессам/узлам без изменения кода objective.
|
||||
- **Визуализация и Dashboard** — встроенные графики истории, важности параметров, Pareto-фронта.
|
||||
|
||||
Установка (Python 3.9+):
|
||||
|
||||
```bash
|
||||
pip install optuna # ядро
|
||||
pip install optuna-dashboard # веб-дашборд (опционально)
|
||||
pip install optunahub # доступ к OptunaHub (AutoSampler и др.)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Базовая модель: Study, Trial, define-by-run API
|
||||
|
||||
Три фундаментальных объекта:
|
||||
|
||||
| Понятие | Что это | Роль |
|
||||
|---------|---------|------|
|
||||
| **Study** | Сессия оптимизации | Хранит все trial-ы, направление(я) оптимизации, sampler, pruner, storage |
|
||||
| **Trial** | Одна попытка (один набор гиперпараметров) | Через него `suggest_*` запрашивает значения и `report` отдаёт промежуточные метрики |
|
||||
| **objective(trial)** | Пользовательская функция | Возвращает метрику (или кортеж метрик), которую Study минимизирует/максимизирует |
|
||||
|
||||
### 1.1 define-by-run: методы `trial.suggest_*`
|
||||
|
||||
Пространство поиска формируется в момент исполнения — каждый вызов `suggest_*` регистрирует гиперпараметр:
|
||||
|
||||
```python
|
||||
import optuna
|
||||
|
||||
def objective(trial: optuna.Trial) -> float:
|
||||
# вещественный, равномерно
|
||||
lr = trial.suggest_float("lr", 1e-5, 1e-1, log=True) # log-uniform
|
||||
# вещественный с шагом
|
||||
dropout = trial.suggest_float("dropout", 0.0, 0.5, step=0.05)
|
||||
# целочисленный (можно log=True и step)
|
||||
n_layers = trial.suggest_int("n_layers", 1, 8)
|
||||
# категориальный
|
||||
optimizer = trial.suggest_categorical("optimizer", ["adam", "sgd", "adamw"])
|
||||
|
||||
# define-by-run: пространство зависит от уже выбранных значений
|
||||
units = [trial.suggest_int(f"units_l{i}", 16, 256, log=True)
|
||||
for i in range(n_layers)]
|
||||
|
||||
score = train_and_eval(lr, dropout, optimizer, units) # ваша логика
|
||||
return score
|
||||
```
|
||||
|
||||
Сводка методов suggest:
|
||||
|
||||
| Метод | Назначение | Полезные аргументы |
|
||||
|-------|-----------|--------------------|
|
||||
| `suggest_float(name, low, high, *, step, log)` | Вещественный параметр | `log=True` (лог-равномерно, для lr/wd); `step` (дискретизация) |
|
||||
| `suggest_int(name, low, high, *, step, log)` | Целочисленный | `log=True`, `step` |
|
||||
| `suggest_categorical(name, choices)` | Категориальный (строки/числа/bool) | список вариантов |
|
||||
|
||||
> `log=True` критичен для масштабно-чувствительных параметров (learning rate, weight decay): диапазон $[10^{-5}, 10^{-1}]$ при логарифмическом сэмплировании покрывается равномерно по порядкам величины. **Нельзя** одновременно задавать `step` и `log=True`.
|
||||
|
||||
### 1.2 Создание Study и запуск
|
||||
|
||||
```python
|
||||
study = optuna.create_study(
|
||||
direction="maximize", # или "minimize"
|
||||
sampler=optuna.samplers.TPESampler(seed=42),
|
||||
pruner=optuna.pruners.MedianPruner(),
|
||||
storage="sqlite:///hpo.db", # опц. — персистентность/параллелизм
|
||||
study_name="edgenext_hpo",
|
||||
load_if_exists=True,
|
||||
)
|
||||
|
||||
study.optimize(
|
||||
objective,
|
||||
n_trials=100, # сколько trial-ов выполнить
|
||||
timeout=3600, # ИЛИ ограничение по времени (сек)
|
||||
n_jobs=1, # параллелизм внутри процесса (потоки)
|
||||
gc_after_trial=True, # сборка мусора между trial (важно для GPU)
|
||||
)
|
||||
|
||||
print(study.best_value, study.best_params, study.best_trial.number)
|
||||
```
|
||||
|
||||
- `direction` / `directions` — `"minimize"` (loss, latency, params) или `"maximize"` (accuracy, R@1).
|
||||
- Остановка по `n_trials`, по `timeout` или по любому из них (что наступит раньше).
|
||||
- `study.best_params`, `study.best_value`, `study.best_trial`, `study.trials` (полный список), `study.trials_dataframe()` (экспорт в pandas).
|
||||
|
||||
---
|
||||
|
||||
## 2. Samplers (алгоритмы выбора следующего набора)
|
||||
|
||||
Sampler решает, **какие значения гиперпараметров пробовать дальше**. По [optuna.readthedocs.io — samplers](https://optuna.readthedocs.io/en/stable/reference/samplers/index.html):
|
||||
|
||||
| Sampler | Алгоритм / назначение | Сильные стороны | Слабые стороны | Когда применять |
|
||||
|---------|----------------------|-----------------|----------------|-----------------|
|
||||
| **TPESampler** (default) | Tree-structured Parzen Estimator (Bayesian) | Работает с категориями, условными пространствами; `multivariate=True` ловит зависимости между параметрами; `group=True` — групповая факторизация | Слабее GP на гладких непрерывных задачах; жадноват | Универсальный выбор, 100–1000 trial; смешанные/условные пространства |
|
||||
| **GPSampler** | Gaussian Process Bayesian optimization | Высокая sample-efficiency на непрерывных/целочисленных; поддержка constrained + multi-objective (с v4.4/v4.5); в v4.6 ускорен ~8× | $O(n^3)$ — дорог при больших $n$; хуже с категориями | Дорогие objective (обучение сети), малый бюджет (≲500 trial) |
|
||||
| **BoTorchSampler** (`optuna-integration`) | BO поверх BoTorch/GPyTorch | Гибкий BO, кастомные acquisition | $O(n^3)$, тяжёлые зависимости (torch) | Исследовательский BO, малый бюджет 10–100 trial |
|
||||
| **CmaEsSampler** | CMA-ES (эволюционная стратегия) | Очень эффективен на непрерывных задачах большой размерности | Не поддерживает multi-objective; плохо с категориями; не любит условные пространства | Непрерывные пространства, бюджет 1000–10000 trial |
|
||||
| **NSGAIISampler** | NSGA-II (генетический, multi-objective) | Pareto-фронт для 2–3 целей; масштабируется по числу trial; поддержка `constraints_func` | Не sample-efficient (нужно много trial); хуже many-objective | Multi-objective (2–3 цели), большой бюджет |
|
||||
| **NSGAIIISampler** | NSGA-III (reference-points) | Для **many-objective** (≥4 целей) | Нужно много trial; настройка reference points | Multi-objective при ≥4 целях |
|
||||
| **QMCSampler** | Quasi-Monte Carlo (low-discrepancy: Sobol/Halton) | Лучшее покрытие пространства, чем random; масштабируется неограниченно | Не использует историю (не адаптивный) | Стартовая разведка, baseline, генерация init-точек |
|
||||
| **RandomSampler** | Равномерный random search | Простой, без допущений, эталон сравнения | Неэффективен в больших пространствах | Baseline, sanity-check, очень малая размерность |
|
||||
| **GridSampler** | Полный перебор заданной сетки | Детерминированный, исчерпывающий | Комбинаторный взрыв | Малое дискретное пространство, нужна полнота |
|
||||
| **BruteForceSampler** | Define-by-run полный перебор | Перебирает всё дерево вариантов автоматически | Только для конечных малых пространств | Перебор небольшого условного дискретного пространства |
|
||||
| **PartialFixedSampler** | Часть параметров зафиксирована, остальное — другим sampler | Дообследование подпространства | Вспомогательный, не самостоятельный | Зафиксировать найденное и доискать остальное |
|
||||
| **AutoSampler** (OptunaHub) | Автоматический выбор из TPE/GP/NSGA-II/NSGA-III | Не нужно выбирать sampler вручную; ≥ default по качеству | Зависимость от `optunahub`; внутренний overhead анализа | Когда не хочется выбирать вручную (см. §2.2) |
|
||||
|
||||
### 2.1 Таблица поддержки возможностей (из официальной документации)
|
||||
|
||||
Легенда: ✓ — поддерживается, △ — работает, но неэффективно, ✗ — ошибка/нет интерфейса.
|
||||
|
||||
| Возможность | Auto | Random | TPE | GP | CMA-ES | NSGA-II | NSGA-III | Grid | QMC | BoTorch | BruteForce |
|
||||
|-------------|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
|
||||
| Float | ✓ | ✓ | ✓ | ✓ | ✓ | △ | △ | ✓ | ✓ | ✓ | ✓ |
|
||||
| Integer | ✓ | ✓ | ✓ | ✓ | ✓ | △ | △ | ✓ | ✓ | △ | ✓ |
|
||||
| Categorical | ✓ | ✓ | ✓ | ✓ | △ | ✓ | ✓ | ✓ | △ | △ | ✓ |
|
||||
| Pruning | △ | ✓ | ✓ | △ | △ | ✗/△ | ✗/△ | ✓ | ✓ | △ | ✓ |
|
||||
| Multivariate | ✓ | △ | ✓ | ✓ | ✓ | △ | △ | △ | △ | ✓ | △ |
|
||||
| Conditional space | ✓ | ✓ | ✓ | △ | △ | △ | △ | △ | △ | △ | ✓ |
|
||||
| Multi-objective | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| Batch optimization | ✓ | ✓ | ✓ | △ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| Distributed | ✓ | ✓ | ✓ | △ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||||
| Constrained | ✓ | ✗ | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ | ✓ | ✗ |
|
||||
|
||||
Ориентировочная вычислительная сложность и бюджет ($d$ — размерность, $n$ — число trial, $m$ — число целей, $p$ — размер популяции):
|
||||
|
||||
| Sampler | Сложность | Рекоменд. число trial |
|
||||
|---------|-----------|----------------------|
|
||||
| RandomSampler / QMCSampler | $O(d)$ / $O(dn)$ | не ограничено |
|
||||
| TPESampler | $O(dn\log n)$ | 100–1000 |
|
||||
| GPSampler / BoTorchSampler | $O(n^3)$ | ≲500 / 10–100 |
|
||||
| CmaEsSampler | $O(d^3)$ | 1000–10000 |
|
||||
| NSGA-II / NSGA-III | $O(mp^2)$ | 100–10000 |
|
||||
|
||||
### 2.2 AutoSampler (OptunaHub) — автоматический выбор алгоритма
|
||||
|
||||
Согласно статье [AutoSampler: full support for multi-objective & constrained optimization](https://medium.com/optuna/autosampler-full-support-for-multi-objective-constrained-optimization-c1c4fc957ba2), AutoSampler анализирует характеристики задачи (**бюджет вычислений, тип/структуру пространства, число целей, наличие ограничений**) и динамически переключается между четырьмя алгоритмами:
|
||||
|
||||
| Внутренний sampler | Когда AutoSampler его выбирает |
|
||||
|--------------------|--------------------------------|
|
||||
| **TPESampler** | сложные пространства с категориями и условными ветвлениями |
|
||||
| **GPSampler** | непрерывные/целочисленные пространства; теперь с полной поддержкой multi-objective и constraints |
|
||||
| **NSGAIISampler** | multi-objective при большом числе trial |
|
||||
| **NSGAIIISampler** | many-objective (много целей) |
|
||||
|
||||
Гарантия: AutoSampler выбирает алгоритм, который «эмпирически даёт результат не хуже default». Поддержка multi-objective + constrained стала возможна благодаря наращиванию возможностей GPSampler в версиях 4.2–4.5 и финализирована в **v4.6**. Бенчмарки: превосходство на WFG1 (multi-objective) и на rotated Rastrigin (constrained).
|
||||
|
||||
```python
|
||||
import optuna, optunahub
|
||||
|
||||
def objective(trial):
|
||||
x = trial.suggest_float("x", 0, 5)
|
||||
y = trial.suggest_float("y", 0, 3)
|
||||
return 4 * x**2 + 4 * y**2, (x - 5)**2 + (y - 5)**2 # 2 цели
|
||||
|
||||
module = optunahub.load_module("samplers/auto_sampler") # подгрузка с OptunaHub
|
||||
study = optuna.create_study(
|
||||
directions=["minimize", "minimize"],
|
||||
sampler=module.AutoSampler(),
|
||||
)
|
||||
study.optimize(objective, n_trials=300)
|
||||
```
|
||||
|
||||
Установка: `pip install -U optuna optunahub` + `pip install -r https://hub.optuna.org/samplers/auto_sampler/requirements.txt`. Принудительное обновление кэша: `optunahub.load_module("samplers/auto_sampler", force_reload=True)`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Pruners (ранняя остановка бесперспективных trial-ов)
|
||||
|
||||
Pruner отслеживает **промежуточные** метрики (например, val-accuracy по эпохам) и досрочно прерывает trial, который заведомо хуже остальных, экономя GPU-время. По [optuna.readthedocs.io — pruners](https://optuna.readthedocs.io/en/stable/reference/pruners.html).
|
||||
|
||||
> **Важное ограничение:** модуль pruners рассчитан **только на single-objective** оптимизацию.
|
||||
|
||||
| Pruner | Принцип | Ключевые параметры | Когда использовать |
|
||||
|--------|---------|--------------------|--------------------|
|
||||
| **MedianPruner** (default) | Median stopping rule: режет trial, если его промежуточное значение хуже медианы предыдущих на этом шаге | `n_startup_trials`, `n_warmup_steps`, `interval_steps` | Универсальный, простой и надёжный baseline |
|
||||
| **PercentilePruner** | Обобщение медианного: режет хуже заданного перцентиля | `percentile`, `n_startup_trials`, `n_warmup_steps` | Когда нужен более/менее агрессивный порог, чем медиана |
|
||||
| **SuccessiveHalvingPruner** | ASHA (Asynchronous Successive Halving): ресурсы удваиваются у «выживших» | `min_resource`, `reduction_factor`, `min_early_stopping_rate` | Большой бюджет, параллельный/распределённый поиск |
|
||||
| **HyperbandPruner** | Несколько «скобок» (brackets) SuccessiveHalving с разным балансом explore/exploit | `min_resource`, `max_resource`, `reduction_factor` | Когда неизвестен оптимальный бюджет на trial — часто лучший выбор |
|
||||
| **PatientPruner** | Обёртка над другим pruner: добавляет «терпение» (patience), не режет при временной просадке | `wrapped_pruner`, `patience`, `min_delta` | Шумные кривые обучения, чтобы не зарезать поздно-сходящиеся |
|
||||
| **ThresholdPruner** | Режет, если метрика вышла за абсолютный порог (детекция выбросов/расходимости) | `lower`, `upper`, `n_warmup_steps` | Отсев расходящихся trial (NaN, взрыв loss) |
|
||||
| **WilcoxonPruner** | Wilcoxon signed-rank test: статистически сравнивает trial с лучшим по набору инстансов | `p_threshold`, `n_startup_steps` | Когда objective усредняется по многим примерам/фолдам |
|
||||
| **NopPruner** | Никогда не режет (no-op) | — | Отключить прунинг, эталон сравнения |
|
||||
|
||||
### 3.1 Встраивание в цикл обучения: `report()` + `should_prune()`
|
||||
|
||||
Внутри objective на каждой эпохе сообщаем промежуточную метрику через `trial.report(value, step)` и проверяем `trial.should_prune()`:
|
||||
|
||||
```python
|
||||
import optuna
|
||||
|
||||
def objective(trial):
|
||||
model, loader_tr, loader_val = build(trial)
|
||||
for epoch in range(N_EPOCHS):
|
||||
train_one_epoch(model, loader_tr)
|
||||
acc = evaluate(model, loader_val)
|
||||
|
||||
trial.report(acc, step=epoch) # отдать промежуточную метрику
|
||||
if trial.should_prune(): # спросить pruner
|
||||
raise optuna.TrialPruned() # прервать trial
|
||||
return acc
|
||||
```
|
||||
|
||||
Логика: `report()` сохраняет $(step, value)$ в storage; `should_prune()` вызывает `pruner.prune(study, trial)`, который сравнивает кривую текущего trial с историей. Прерывание выполняется через исключение `optuna.TrialPruned` — Optuna помечает trial как `PRUNED`, а не `FAIL`.
|
||||
|
||||
> Для согласованности **шаги `step` должны совпадать между trial-ами** (например, номер эпохи), иначе медиана/перцентиль считаются по разным точкам.
|
||||
|
||||
---
|
||||
|
||||
## 4. Multi-objective и constrained optimization
|
||||
|
||||
### 4.1 Несколько целей
|
||||
|
||||
Задаётся через `directions` (список), objective возвращает **кортеж**:
|
||||
|
||||
```python
|
||||
def objective(trial):
|
||||
model = build(trial)
|
||||
acc = evaluate(model) # хотим максимизировать
|
||||
n_params = count_params(model) / 1e6 # хотим минимизировать (млн)
|
||||
return acc, n_params
|
||||
|
||||
study = optuna.create_study(
|
||||
directions=["maximize", "minimize"], # accuracy ↑, params ↓
|
||||
sampler=optuna.samplers.NSGAIISampler(),
|
||||
)
|
||||
study.optimize(objective, n_trials=200)
|
||||
|
||||
best_trials = study.best_trials # МНОЖЕСТВО Парето-оптимальных, не один best
|
||||
```
|
||||
|
||||
При нескольких целях единственного «лучшего» нет — есть **Pareto-фронт**: множество недоминируемых решений. Точка $a$ доминирует $b$, если не хуже по всем целям и строго лучше хотя бы по одной. `study.best_trials` возвращает весь фронт; выбор финальной точки — компромисс инженера (например, max accuracy при params ≤ заданного бюджета).
|
||||
|
||||
### 4.2 Ограничения (constraints)
|
||||
|
||||
Поддерживается в TPESampler, NSGAII/NSGAIIISampler, GPSampler, BoTorchSampler (см. таблицу §2.1). Паттерн по [Optuna FAQ](https://optuna.readthedocs.io/en/stable/faq.html): ограничения формулируются так, что **допустимое решение ⇔ значение ≤ 0**, сохраняются в `user_attrs`, а `constraints_func` их извлекает.
|
||||
|
||||
```python
|
||||
def objective(trial):
|
||||
x = trial.suggest_float("x", 0, 10)
|
||||
y = trial.suggest_float("y", 0, 10)
|
||||
# ограничения: c_i <= 0 означает "выполнено"
|
||||
c0 = (x - 5)**2 + y**2 - 25
|
||||
c1 = -(x - 8)**2 - (y + 3)**2 + 7.7
|
||||
trial.set_user_attr("constraint", (c0, c1)) # сохранить
|
||||
return 4*x**2 + y**2, (x - 5)**2 + (y - 5)**2 # 2 цели
|
||||
|
||||
def constraints(trial):
|
||||
return trial.user_attrs["constraint"] # извлечь для sampler
|
||||
|
||||
study = optuna.create_study(
|
||||
directions=["minimize", "minimize"],
|
||||
sampler=optuna.samplers.NSGAIISampler(constraints_func=constraints),
|
||||
)
|
||||
study.optimize(objective, n_trials=300)
|
||||
```
|
||||
|
||||
Sampler штрафует/отодвигает недопустимые области (где $c_i > 0$), концентрируя бюджет в feasible-зоне. В v4.5 GPSampler получил **constrained multi-objective** на базе log-EHVI (Expected Hypervolume Improvement), с моделированием и целей, и ограничений отдельными GP (см. §5).
|
||||
|
||||
---
|
||||
|
||||
## 5. Что нового в Optuna v4.5 и v4.6
|
||||
|
||||
### v4.5 (по [блогу Optuna](https://medium.com/optuna/optuna-v4-5-81e78d8e077a))
|
||||
|
||||
| Фича | Суть |
|
||||
|------|------|
|
||||
| **Constrained multi-objective в GPSampler** | GPSampler теперь умеет одновременно несколько целей + inequality-ограничения |
|
||||
| **Acquisition = log-EHVI** | Логарифм Expected Hypervolume Improvement как функция приобретения |
|
||||
| **GP для целей и для ограничений** | Отдельные гауссовские процессы; feasibility оценивается через Probability of Improvement (PI) |
|
||||
| **Box decomposition** | Ускорение вычислений до **6–800×**; практично до 4 целей |
|
||||
| **Эффективность** | Меньше «впустую» оценок в infeasible-областях; быстрее сходимости, чем TPE/NSGA-II на бенчмарках (целевой уровень за <50 trial) |
|
||||
|
||||
### v4.6 (по [блогу Optuna](https://medium.com/optuna/announcing-optuna-4-6-a9e82183ab07))
|
||||
|
||||
| Фича | Суть |
|
||||
|------|------|
|
||||
| **LLM-интеграция в Dashboard** | Фильтрация trial на естественном языке + автогенерация графиков (Plotly.js) |
|
||||
| **GPSampler ускорен ~8×** | Кэширование, параллелизация, оптимизация внутренних операций при сохранении качества |
|
||||
| **AutoSampler: full multi-objective + constrained** | Автоматический выбор среди GPSampler / NSGAIIISampler и др. по характеру задачи (см. §2.2) |
|
||||
| **Robust BO: CARBO и Value-at-Risk (VaR)** | Устойчивая байесовская оптимизация при шуме параметров (реальные приложения) |
|
||||
| **OpenTelemetry-мониторинг** | Туториал по экспорту метрик Dashboard в Prometheus/Grafana |
|
||||
| **LaTeX в OptunaHub** | Рендер математики (GitHub-совместимый синтаксис) |
|
||||
| **MCP Server: Structured Output** | Валидация и schema-based обработка результатов |
|
||||
|
||||
> Breaking-изменений API, формата storage и pruner-ов в v4.6 не заявлено.
|
||||
|
||||
---
|
||||
|
||||
## 6. OptunaHub, storage, распределённый поиск, warm-start
|
||||
|
||||
### 6.1 OptunaHub
|
||||
|
||||
Платформа обмена компонентами (samplers, pruners, visualization) — например, **AutoSampler** ставится не из ядра, а через `optunahub.load_module("samplers/auto_sampler")`. Позволяет публиковать и подключать сторонние алгоритмы без раздувания ядра Optuna.
|
||||
|
||||
### 6.2 Storage (персистентность и параллелизм)
|
||||
|
||||
| Backend | Описание | Когда |
|
||||
|---------|----------|-------|
|
||||
| **InMemoryStorage** | По умолчанию, в ОЗУ | Один процесс, не нужна персистентность |
|
||||
| **RDBStorage** | SQL-БД (SQLite / PostgreSQL / MySQL) | Персистентность, дашборд, многопроцессный/многоузловой поиск |
|
||||
| **JournalStorage** | Журнальный файл (file / Redis backend) | Распределённость без полноценной СУБД; устойчивость к гонкам записи |
|
||||
|
||||
```python
|
||||
# RDB (sqlite — для одной машины; postgres/mysql — для кластера)
|
||||
study = optuna.create_study(
|
||||
storage="sqlite:///hpo.db",
|
||||
study_name="edgenext_hpo",
|
||||
load_if_exists=True, # докинуть trial к существующему study
|
||||
)
|
||||
|
||||
# JournalStorage (рекомендован для распределённого/NFS)
|
||||
from optuna.storages import JournalStorage
|
||||
from optuna.storages.journal import JournalFileBackend
|
||||
storage = JournalStorage(JournalFileBackend("./hpo_journal.log"))
|
||||
```
|
||||
|
||||
### 6.3 Распределённый / параллельный поиск
|
||||
|
||||
- **Внутри процесса:** `study.optimize(objective, n_jobs=-1)` — потоки (полезно, если objective отпускает GIL / I/O-bound).
|
||||
- **Между процессами/узлами:** запустить тот же скрипт N раз с **общим storage** (RDB/Journal) и одинаковым `study_name` — воркеры сами координируются через БД. CLI-вариант: `optuna study optimize ... --n-jobs ...`.
|
||||
|
||||
### 6.4 Warm-start: `enqueue_trial` / `add_trial`
|
||||
|
||||
| Метод | Назначение |
|
||||
|-------|-----------|
|
||||
| `study.enqueue_trial(params)` | Поставить конкретный набор параметров в очередь на следующий запуск (например, известный хороший конфиг, baseline) |
|
||||
| `study.add_trial(trial)` | Добавить уже завершённый trial с известным результатом (перенос истории, тёплый старт sampler) |
|
||||
|
||||
```python
|
||||
# заранее проверить «ручной» хороший конфиг
|
||||
study.enqueue_trial({"lr": 3e-4, "dropout": 0.1, "optimizer": "adamw"})
|
||||
study.optimize(objective, n_trials=100)
|
||||
|
||||
# перенести результат внешнего эксперимента в историю
|
||||
from optuna.trial import create_trial
|
||||
from optuna.distributions import FloatDistribution
|
||||
study.add_trial(create_trial(
|
||||
params={"lr": 1e-3},
|
||||
distributions={"lr": FloatDistribution(1e-5, 1e-1, log=True)},
|
||||
value=0.81,
|
||||
))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Визуализация
|
||||
|
||||
По [optuna.readthedocs.io — visualization](https://optuna.readthedocs.io/en/stable/reference/visualization/index.html). Два backend: `optuna.visualization` (**Plotly**, интерактивно, default) и `optuna.visualization.matplotlib` (статично, для статей/отчётов).
|
||||
|
||||
| Функция | Что показывает | Применение |
|
||||
|---------|----------------|-----------|
|
||||
| `plot_optimization_history` | Лучшее значение по ходу trial | Скорость сходимости, плато |
|
||||
| `plot_param_importances` | Важность гиперпараметров | Какие параметры реально влияют (см. ниже) |
|
||||
| `plot_slice` | Срез: параметр vs objective | Влияние одного параметра, перспективные диапазоны |
|
||||
| `plot_contour` | 2D-контур пары параметров | Взаимодействие двух параметров |
|
||||
| `plot_parallel_coordinate` | Параллельные координаты | Многомерные паттерны хороших trial |
|
||||
| `plot_pareto_front` | Pareto-фронт | Компромиссы в multi-objective |
|
||||
| `plot_intermediate_values` | Промежуточные кривые (по эпохам) | Диагностика прунинга |
|
||||
| `plot_edf` | Эмпирическая функция распределения | Сравнение study между собой |
|
||||
| `plot_rank` | Ранги trial по параметрам | Локализация хороших зон |
|
||||
| `plot_timeline` | Временная диаграмма исполнения | Параллелизм, длительность trial |
|
||||
| `plot_hypervolume_history` | Рост hypervolume | Прогресс multi-objective |
|
||||
| `plot_terminator_improvement` | Оценка критерия остановки | Когда прекращать study |
|
||||
|
||||
**Важность параметров** (`plot_param_importances`, требует scikit-learn) считается оценщиками:
|
||||
|
||||
- **fANOVA** (`FanovaImportanceEvaluator`, default) — функциональный дисперсионный анализ через random forest: доля дисперсии objective, объясняемая параметром/взаимодействием.
|
||||
- **PED-ANOVA** (`PedAnovaImportanceEvaluator`) — быстрый оценщик, фокусируется на важности в **хорошей** области пространства (по перцентилю); полезен для больших историй.
|
||||
|
||||
```python
|
||||
from optuna.visualization import plot_optimization_history, plot_param_importances
|
||||
plot_optimization_history(study).show()
|
||||
plot_param_importances(study).show()
|
||||
# с PED-ANOVA:
|
||||
from optuna.importance import PedAnovaImportanceEvaluator
|
||||
plot_param_importances(study, evaluator=PedAnovaImportanceEvaluator()).show()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Интеграция с PyTorch
|
||||
|
||||
Базовый objective с прунингом по эпохам:
|
||||
|
||||
```python
|
||||
import optuna, torch, torch.nn as nn
|
||||
|
||||
def objective(trial):
|
||||
# define-by-run пространство
|
||||
lr = trial.suggest_float("lr", 1e-5, 1e-1, log=True)
|
||||
wd = trial.suggest_float("wd", 1e-6, 1e-2, log=True)
|
||||
dropout = trial.suggest_float("dropout", 0.0, 0.5, step=0.1)
|
||||
n_units = trial.suggest_int("n_units", 64, 512, log=True)
|
||||
|
||||
model = build_model(n_units, dropout).cuda()
|
||||
opt = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=wd)
|
||||
|
||||
for epoch in range(EPOCHS):
|
||||
train_one_epoch(model, train_loader, opt)
|
||||
val_acc = evaluate(model, val_loader)
|
||||
|
||||
trial.report(val_acc, step=epoch) # промежуточная метрика
|
||||
if trial.should_prune(): # запрос pruner
|
||||
raise optuna.TrialPruned()
|
||||
return val_acc
|
||||
|
||||
study = optuna.create_study(
|
||||
direction="maximize",
|
||||
sampler=optuna.samplers.TPESampler(multivariate=True),
|
||||
pruner=optuna.pruners.HyperbandPruner(min_resource=1, max_resource=EPOCHS),
|
||||
)
|
||||
study.optimize(objective, n_trials=100, gc_after_trial=True)
|
||||
```
|
||||
|
||||
### 8.1 Callbacks и интеграции
|
||||
|
||||
- **Callbacks:** `study.optimize(objective, callbacks=[cb])`, где `cb(study, trial)` вызывается после каждого trial (логирование, ранний стоп всего study, чекпойнты). Готовый `MaxTrialsCallback` ограничивает число завершённых trial.
|
||||
- **Готовые интеграции** (пакет `optuna-integration`): `PyTorchLightningPruningCallback`, `TorchDistributedTrial`, callbacks для Keras/XGBoost/LightGBM/MLflow/W&B.
|
||||
|
||||
### 8.2 TorchDistributedTrial (DDP)
|
||||
|
||||
При обучении в `torch.distributed` (DDP) гиперпараметры и решение о прунинге должны быть **согласованы между всеми ранками**. `TorchDistributedTrial` оборачивает trial: реальные `suggest_*`/`report`/`should_prune` исполняет rank 0 и **broadcast-ит** результат остальным процессам.
|
||||
|
||||
```python
|
||||
import optuna
|
||||
from optuna.integration import TorchDistributedTrial
|
||||
import torch.distributed as dist
|
||||
|
||||
def objective(single_trial):
|
||||
# rank0 владеет trial; остальные получают None
|
||||
trial = TorchDistributedTrial(single_trial if dist.get_rank() == 0 else None)
|
||||
lr = trial.suggest_float("lr", 1e-5, 1e-1, log=True) # одинаково на всех ранках
|
||||
for epoch in range(EPOCHS):
|
||||
train_ddp_epoch(lr, epoch)
|
||||
acc = evaluate_ddp()
|
||||
trial.report(acc, epoch)
|
||||
if trial.should_prune(): # согласованное решение
|
||||
raise optuna.TrialPruned()
|
||||
return acc
|
||||
```
|
||||
|
||||
Запускают одно study только на rank 0 (`if dist.get_rank() == 0: study.optimize(...)`), либо общий storage + барьеры — детали в `optuna-integration`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Практический полный пример: image-classification + pruning (skeleton)
|
||||
|
||||
```python
|
||||
import optuna
|
||||
from optuna.trial import TrialState
|
||||
import torch, torch.nn as nn, torch.nn.functional as F
|
||||
from torchvision import datasets, transforms
|
||||
|
||||
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
EPOCHS, N_TRIALS = 15, 100
|
||||
|
||||
def get_loaders(batch_size):
|
||||
tf = transforms.Compose([transforms.ToTensor(),
|
||||
transforms.Normalize((0.1307,), (0.3081,))])
|
||||
tr = datasets.MNIST("./data", train=True, download=True, transform=tf)
|
||||
va = datasets.MNIST("./data", train=False, download=True, transform=tf)
|
||||
return (torch.utils.data.DataLoader(tr, batch_size=batch_size, shuffle=True),
|
||||
torch.utils.data.DataLoader(va, batch_size=256))
|
||||
|
||||
def build_model(trial):
|
||||
n_layers = trial.suggest_int("n_layers", 1, 3)
|
||||
layers, in_f = [], 28 * 28
|
||||
for i in range(n_layers):
|
||||
out_f = trial.suggest_int(f"units_l{i}", 64, 512, log=True)
|
||||
p = trial.suggest_float(f"dropout_l{i}", 0.0, 0.5, step=0.1)
|
||||
layers += [nn.Linear(in_f, out_f), nn.ReLU(), nn.Dropout(p)]
|
||||
in_f = out_f
|
||||
layers.append(nn.Linear(in_f, 10))
|
||||
return nn.Sequential(nn.Flatten(), *layers)
|
||||
|
||||
def objective(trial):
|
||||
lr = trial.suggest_float("lr", 1e-5, 1e-1, log=True)
|
||||
bs = trial.suggest_categorical("batch_size", [32, 64, 128])
|
||||
opt_name = trial.suggest_categorical("optimizer", ["Adam", "SGD", "AdamW"])
|
||||
|
||||
model = build_model(trial).to(DEVICE)
|
||||
optimizer = getattr(torch.optim, opt_name)(model.parameters(), lr=lr)
|
||||
train_loader, val_loader = get_loaders(bs)
|
||||
|
||||
for epoch in range(EPOCHS):
|
||||
model.train()
|
||||
for x, y in train_loader:
|
||||
x, y = x.to(DEVICE), y.to(DEVICE)
|
||||
optimizer.zero_grad()
|
||||
F.cross_entropy(model(x), y).backward()
|
||||
optimizer.step()
|
||||
|
||||
model.eval(); correct = 0
|
||||
with torch.no_grad():
|
||||
for x, y in val_loader:
|
||||
x, y = x.to(DEVICE), y.to(DEVICE)
|
||||
correct += (model(x).argmax(1) == y).sum().item()
|
||||
acc = correct / len(val_loader.dataset)
|
||||
|
||||
trial.report(acc, epoch) # отдать метрику pruner-у
|
||||
if trial.should_prune(): # ранняя остановка
|
||||
raise optuna.TrialPruned()
|
||||
return acc
|
||||
|
||||
if __name__ == "__main__":
|
||||
study = optuna.create_study(
|
||||
direction="maximize",
|
||||
sampler=optuna.samplers.TPESampler(multivariate=True, seed=42),
|
||||
pruner=optuna.pruners.MedianPruner(n_startup_trials=5, n_warmup_steps=3),
|
||||
storage="sqlite:///mnist_hpo.db",
|
||||
study_name="mnist", load_if_exists=True,
|
||||
)
|
||||
study.optimize(objective, n_trials=N_TRIALS, timeout=3600, gc_after_trial=True)
|
||||
|
||||
pruned = study.get_trials(deepcopy=False, states=[TrialState.PRUNED])
|
||||
complete = study.get_trials(deepcopy=False, states=[TrialState.COMPLETE])
|
||||
print(f"pruned={len(pruned)} complete={len(complete)}")
|
||||
print("best acc:", study.best_value, "| params:", study.best_params)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Шпаргалка: «когда какой sampler + pruner»
|
||||
|
||||
| Сценарий | Sampler | Pruner | Обоснование |
|
||||
|----------|---------|--------|-------------|
|
||||
| Универсальный старт (DL, смешанное пространство) | **TPESampler** (`multivariate=True`) | **MedianPruner** | Надёжный default, ловит зависимости параметров |
|
||||
| Дорогой objective, малый бюджет (≲500), непрерывные параметры | **GPSampler** | PatientPruner / без прунинга | Максимальная sample-efficiency BO |
|
||||
| Большой бюджет (>1000), непрерывное пространство высокой размерности | **CmaEsSampler** | **SuccessiveHalvingPruner (ASHA)** | Эффективная эволюционная стратегия + дешёвый прунинг |
|
||||
| Не знаю бюджет на trial, длинное обучение | TPESampler | **HyperbandPruner** | Несколько brackets закрывают разный explore/exploit |
|
||||
| Multi-objective: accuracy ↑ vs params/latency ↓ (2–3 цели) | **NSGAIISampler** (+`constraints_func`) | — (multi-obj не прунится) | Строит Pareto-фронт; поддержка ограничений |
|
||||
| Many-objective (≥4 целей) | **NSGAIIISampler** | — | Reference-points масштабируют many-objective |
|
||||
| Шумные кривые обучения (поздняя сходимость) | TPESampler / GPSampler | **PatientPruner** (обёртка) | Не режет временные просадки |
|
||||
| Отсев расходящихся trial (NaN, взрыв loss) | любой | **ThresholdPruner** | Жёсткий абсолютный порог |
|
||||
| Objective усредняется по многим примерам/фолдам | TPESampler | **WilcoxonPruner** | Статистически корректное сравнение с лучшим |
|
||||
| Малое дискретное пространство, нужна полнота | **GridSampler / BruteForceSampler** | MedianPruner | Исчерпывающий перебор |
|
||||
| Стартовая разведка / baseline | **QMC / RandomSampler** | NopPruner | Равномерное покрытие, эталон |
|
||||
| Не хочу выбирать вручную, есть `optunahub` | **AutoSampler** | — (multi-obj) / MedianPruner (single) | Автовыбор TPE/GP/NSGA-II/III под задачу |
|
||||
| Распределённый кластер, много воркеров | TPESampler / RandomSampler + RDB/Journal storage | **SuccessiveHalvingPruner (ASHA)** | Асинхронность дружит с общим storage |
|
||||
|
||||
> Практический дефолт для одной цели в edge-CV задачах (как EdgeNeXt-style сети): **TPESampler(multivariate=True) + HyperbandPruner**, storage = SQLite, `gc_after_trial=True`. Для совместной оптимизации accuracy и числа параметров/latency — **NSGAIISampler + `constraints_func`** и анализ `plot_pareto_front`.
|
||||
Reference in New Issue
Block a user