Files
PracticeClassif/_research/SECTION_optuna.md
2026-06-30 15:17:40 +03:00

559 lines
40 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 на гладких непрерывных задачах; жадноват | Универсальный выбор, 1001000 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, малый бюджет 10100 trial |
| **CmaEsSampler** | CMA-ES (эволюционная стратегия) | Очень эффективен на непрерывных задачах большой размерности | Не поддерживает multi-objective; плохо с категориями; не любит условные пространства | Непрерывные пространства, бюджет 100010000 trial |
| **NSGAIISampler** | NSGA-II (генетический, multi-objective) | Pareto-фронт для 23 целей; масштабируется по числу trial; поддержка `constraints_func` | Не sample-efficient (нужно много trial); хуже many-objective | Multi-objective (23 цели), большой бюджет |
| **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)$ | 1001000 |
| GPSampler / BoTorchSampler | $O(n^3)$ | ≲500 / 10100 |
| CmaEsSampler | $O(d^3)$ | 100010000 |
| NSGA-II / NSGA-III | $O(mp^2)$ | 10010000 |
### 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.24.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** | Ускорение вычислений до **6800×**; практично до 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 ↓ (23 цели) | **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`.