init commit

This commit is contained in:
2026-06-30 15:17:40 +03:00
commit e622129c30
8 changed files with 2588 additions and 0 deletions

558
_research/SECTION_optuna.md Normal file
View 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 на гладких непрерывных задачах; жадноват | Универсальный выбор, 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`.