40 KiB
title, section, tool, supervisor, status, tags
| title | section | tool | supervisor | status | tags | |||||
|---|---|---|---|---|---|---|---|---|---|---|
| Optuna — подбор гиперпараметров: полный разбор | HPO | Optuna | мнс Павленко Б.В. | draft |
|
Optuna — библиотека для подбора гиперпараметров
Источники: официальный сайт optuna.org, документация optuna.readthedocs.io, а также блог команды Optuna на Medium (релизы v4.5, v4.6 и AutoSampler).
Optuna — это open-source фреймворк для автоматической оптимизации гиперпараметров, не привязанный к конкретной ML-библиотеке (framework-agnostic): одинаково применим к PyTorch, TensorFlow/Keras, scikit-learn, XGBoost, LightGBM и др. Ключевые архитектурные принципы по 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+):
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_* регистрирует гиперпараметр:
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 и запуск
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:
| 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, 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).
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.
Важное ограничение: модуль 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():
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 возвращает кортеж:
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: ограничения формулируются так, что допустимое решение ⇔ значение ≤ 0, сохраняются в user_attrs, а constraints_func их извлекает.
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)
| Фича | Суть |
|---|---|
| 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)
| Фича | Суть |
|---|---|
| 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) | Распределённость без полноценной СУБД; устойчивость к гонкам записи |
# 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) |
# заранее проверить «ручной» хороший конфиг
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. Два 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) — быстрый оценщик, фокусируется на важности в хорошей области пространства (по перцентилю); полезен для больших историй.
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 с прунингом по эпохам:
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-ит результат остальным процессам.
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)
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.