--- 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`.