Optuna 4 у Python: повний посібник з оптимізації гіперпараметрів на 2026
Практичний посібник з Optuna 4.9 для Python-розробників: TPE-семплер, прунінг з Hyperband, розподілені дослідження, багатоцільова оптимізація, інтеграція з XGBoost, LightGBM, scikit-learn та MLflow 3.
Optuna 4 це фреймворк для автоматичної оптимізації гіперпараметрів у Python, який використовує байєсівську оптимізацію на основі TPE-семплера й прунінг, щоб знайти найкращу конфігурацію моделі за десятки, а не тисячі спроб. На відміну від GridSearchCV, який перебирає всі комбінації, Optuna навчається на попередніх trials і зосереджується на перспективних областях простору пошуку. У версії 4.9 (червень 2026) додали розширену інтеграцію з MLflow 3, покращений MlflowSparkStudy та нові семплери в OptunaHub.
Чесно кажучи, я довго опирався переходу з GridSearchCV, поки не проґавив дедлайн на клієнтському проєкті, чекаючи 18 годин на перебір сітки для LightGBM. Optuna тоді зробила те саме за 40 хвилин. Ось як воно працює і чому це стало моїм дефолтом у 2026-му.
Optuna 4.9 (стабільна станом на 2026) використовує TPE-семплер з мультиваріативним режимом та підтримує NSGA-II/III для багатоцільової оптимізації.
Прунінг з HyperbandPruner або MedianPruner зупиняє безперспективні trials на ранніх епохах і часто економить 50–80% GPU-часу.
Define-by-run API дозволяє динамічно будувати простір пошуку з умовною логікою, чого не можна зробити у GridSearchCV.
Розподілені дослідження з n_jobs або спільним RDB storage масштабуються практично лінійно до 8+ воркерів.
Інтеграція з MLflow 3 через MlflowStorage та MlflowSparkStudy дає повний audit trail експериментів.
Для класичного ML з коротким тренуванням Optuna зазвичай перевершує Ray Tune за співвідношенням якість/складність.
Що таке Optuna і як він працює
Optuna, якщо коротко, це open-source фреймворк для автоматичної оптимізації гіперпараметрів (HPO), розроблений компанією Preferred Networks і випущений у 2019 році. У 2026 році він залишається стандартом де-факто для класичного ML і глибокого навчання серед Python-екосистеми. Основна ідея проста: трактувати підбір гіперпараметрів як послідовну задачу оптимізації, де кожен наступний trial обирається з урахуванням результатів попередніх.
В основі Optuna лежать три концепції. Study (одне дослідження, яке має напрям maximize або minimize), Trial (один запуск objective-функції з певним набором параметрів) і Sampler (алгоритм, який пропонує наступні значення). Замість того щоб описувати весь простір пошуку заздалегідь, ви пишете об'єктивну функцію, яка на льоту викликає trial.suggest_*. Це й називається define-by-run API.
Такий підхід дає дві переваги. По-перше, ви можете вбудовувати умовну логіку: наприклад, якщо обраний оптимізатор Adam, семплити beta1, інакше семплити momentum. По-друге, простір пошуку може динамічно розширюватися залежно від значень інших параметрів (як-от кількість шарів визначає, скільки треба підібрати ширин). Все це можна написати у звичайному Python-коді з циклами й if, що робить фреймворк дуже гнучким для складних архітектур.
Optuna vs GridSearchCV vs RandomizedSearchCV
Щоб зрозуміти, чому Optuna виграє в більшості практичних сценаріїв, розгляньмо основні альтернативи. GridSearchCV зі scikit-learn перебирає всі комбінації сітки: чотири гіперпараметри з п'ятьма кандидатами дають 625 навчань, а п'ять уже 3 125. RandomizedSearchCV зменшує кількість спроб через випадкове семплування, але не використовує інформацію з попередніх trials. 50-й trial такий самий сліпий, як і перший.
Optuna, як і Hyperopt та Ray Tune, використовує байєсівський підхід. Вона будує ймовірнісну модель P(score | params) і обирає наступні параметри там, де очікуваний виграш найбільший. Оскільки семплер навчається, він може за 50 trials досягти тієї якості, яку RandomizedSearchCV отримує за 300–500.
Характеристика
GridSearchCV
RandomizedSearchCV
Optuna 4
Ray Tune
Стратегія пошуку
Вичерпний перебір
Випадковий
TPE / GP / CmaEs / NSGA
Grid/Random/BO/PBT
Навчання на попередніх trials
Ні
Ні
Так
Так
Раннє зупинення (прунінг)
Ні
Ні
Так (Hyperband, ASHA, Median)
Так (ASHA, PBT)
Розподілені воркери
Тільки локально
Тільки локально
Через RDB storage або Spark
Кластер Ray
Умовні простори пошуку
Обмежено
Обмежено
Так (define-by-run)
Так
Багатоцільова оптимізація
Ні
Ні
Так (NSGA-II/III)
Частково
Крива навчання
Мінімальна
Мінімальна
Легка
Крута
Найкраще підходить для
Малі сітки, освіта
Швидке дослідження
Класичний ML, tabular
DL на кластері
На практиці для табличних даних і градієнтного бустингу Optuna майже завжди виграє. Для розподіленого DL з довгим навчанням і планувальниками рівня Population-Based Training Ray Tune залишається сильним варіантом, і Optuna часто працює всередині нього як алгоритм пошуку через OptunaSearch.
Встановлення Optuna 4.9 та залежностей
Optuna 4.9 підтримує Python 3.9–3.13. Мінімальна інсталяція займає кілька мегабайтів, а додаткові пакунки потрібні лише за необхідності візуалізації, дашборда чи інтеграцій з ML-фреймворками.
Розпочнімо з класичного прикладу: підбір гіперпараметрів RandomForestClassifier для датасету Iris. Ця вправа не має практичної цінності (RF на Iris ідеальний з дефолтами), але демонструє повний цикл Optuna за 30 рядків.
import optuna
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
X, y = load_iris(return_X_y=True)
def objective(trial: optuna.Trial) -> float:
# define-by-run: простір пошуку описуємо тут, у Python
n_estimators = trial.suggest_int("n_estimators", 50, 500, step=50)
max_depth = trial.suggest_int("max_depth", 2, 32, log=True)
min_samples_split = trial.suggest_int("min_samples_split", 2, 20)
criterion = trial.suggest_categorical("criterion", ["gini", "entropy", "log_loss"])
clf = RandomForestClassifier(
n_estimators=n_estimators,
max_depth=max_depth,
min_samples_split=min_samples_split,
criterion=criterion,
n_jobs=-1,
random_state=42,
)
score = cross_val_score(clf, X, y, cv=5, scoring="accuracy").mean()
return score # Optuna мінімізує або максимізує це значення
study = optuna.create_study(direction="maximize", study_name="iris-rf")
study.optimize(objective, n_trials=50, show_progress_bar=True)
print("Найкращий score:", study.best_value)
print("Найкращі параметри:", study.best_params)
Кілька важливих деталей. Метод suggest_int з log=True семплить у логарифмічному масштабі, що критично для параметрів на кшталт learning_rate чи C в SVM, які змінюють поведінку моделі на порядки. suggest_categorical приймає список, а suggest_float вказує діапазон дійсних чисел. Objective повертає єдине число, і direction визначає, чи максимізуємо ми його (accuracy, F1) чи мінімізуємо (RMSE, log loss).
Наступний рівень: зберегти дослідження на диск, щоб продовжити його пізніше або запустити паралельні воркери:
За замовчуванням Optuna використовує TPE (Tree-structured Parzen Estimator). Це байєсівський алгоритм, який моделює P(x | y), а не P(y | x). Він розділяє попередні trials на «хороші» та «погані» за квантилем і будує окремі щільності для обох груп, обираючи наступний параметр там, де співвідношення l(x)/g(x) максимальне.
У версіях 3.x і 4.x TPE-семплер отримав режим multivariate=True, який враховує залежності між гіперпараметрами (як learning rate і batch size), а також group=True для умовних просторів пошуку. Це суттєво покращує швидкість збіжності на складних задачах.
from optuna.samplers import TPESampler, NSGAIISampler, CmaEsSampler, GPSampler
# Мультиваріативний TPE, стандарт для tabular ML
sampler = TPESampler(
n_startup_trials=15, # спочатку random для розігріву
multivariate=True,
group=True,
seed=42,
)
study = optuna.create_study(direction="maximize", sampler=sampler)
# CMA-ES для неперервних задач з великим бюджетом trials
study_cma = optuna.create_study(sampler=CmaEsSampler(seed=42))
# GP (Gaussian Process): дорого, але точно для малої кількості trials
study_gp = optuna.create_study(sampler=GPSampler(seed=42))
OptunaHub, окремий репозиторій семплерів, що його підтримує спільнота. У 2026 році там додали OrthoBO для ортогонального байєсівського пошуку та PSO (Particle Swarm Optimization). Встановлюються вони окремо через optuna-hub.
Прунінг: як зупиняти невдалі trials
Прунінг (раннє зупинення) — друга суперсила Optuna після семплінгу. Ідея проста. Якщо після 5 епох модель показує accuracy 0.42, а середня по всіх попередніх trials на цьому кроці була 0.71, немає сенсу навчати її до кінця. Прунер перериває trial і звільняє ресурси для перспективніших кандидатів.
Я одного разу вимкнув прунінг «на всякий випадок» під час нічного запуску, і GPU відпрацював 11 годин на trials, які були приречені після третьої епохи. Не повторюйте моїх помилок.
Найпоширеніші прунери у 2026:
MedianPruner: простий, зупиняє trial, якщо його інтермедіат нижчий за медіану на цьому ж кроці серед попередніх завершених trials.
HyperbandPruner: bandit-алгоритм, який виділяє ресурси адаптивно між різними «brackets». Часто це найкращий вибір для довгих навчань.
SuccessiveHalvingPruner: базовий ASHA. Агресивно ріже третину trials на кожному rung.
PatientPruner: обгортка, яка чекає N кроків без покращення, перш ніж прунити.
Приклад для навчання нейромережі з ручним репортом intermediate values:
import optuna
from optuna.pruners import HyperbandPruner
import torch, torch.nn as nn
from torch.utils.data import DataLoader
def objective(trial: optuna.Trial) -> float:
lr = trial.suggest_float("lr", 1e-5, 1e-1, log=True)
hidden = trial.suggest_int("hidden", 32, 512, step=32)
dropout = trial.suggest_float("dropout", 0.0, 0.5)
model = build_model(hidden, dropout)
optim = torch.optim.AdamW(model.parameters(), lr=lr)
train_loader, val_loader = get_loaders()
for epoch in range(30):
train_one_epoch(model, optim, train_loader)
val_acc = evaluate(model, val_loader)
# Звітуємо прунеру після кожної епохи
trial.report(val_acc, step=epoch)
if trial.should_prune():
raise optuna.TrialPruned()
return val_acc
study = optuna.create_study(
direction="maximize",
sampler=optuna.samplers.TPESampler(multivariate=True, seed=42),
pruner=HyperbandPruner(min_resource=1, max_resource=30, reduction_factor=3),
)
study.optimize(objective, n_trials=100, timeout=3600)
Тюнінг XGBoost та LightGBM з Optuna
Градієнтний бустинг — головний use case Optuna в промисловості. Простір гіперпараметрів XGBoost, LightGBM і CatBoost великий (10+ параметрів), функція втрат нелінійна, а вартість trials помірна. Ідеальна ніша для байєсівської оптимізації. Якщо ви обираєте між фреймворками, читайте порівняння XGBoost, LightGBM та CatBoost на 2026.
Кілька практичних деталей із мого досвіду. По-перше, для gradient boosting корисно фіксувати learning_rate в широкому лог-діапазоні і давати Optuna самій знайти оптимум разом з n_estimators (через early_stopping). По-друге, reg_alpha і reg_lambda потребують логарифмічного масштабу від 1e-8, бо часто нуль виявляється кращим за будь-яке позитивне значення. По-третє, callback LightGBMPruningCallback вмикає прунінг на рівні boost-раундів. Величезна економія часу.
Для XGBoost і CatBoost принципи ті самі, змінюються тільки назви параметрів і callback (XGBoostPruningCallback, CatBoostPruningCallback). Ці налаштування добре стикуються з рекомендаціями зі статті про сучасні ML-пайплайни зі scikit-learn 1.8, де описано, як загорнути Optuna study в Pipeline з препроцесингом.
Розподілені та паралельні дослідження
Optuna пропонує два рівні паралелізму: локальний (кілька потоків у межах процесу) і розподілений (кілька процесів або машин зі спільним storage). Обидва прозорі для objective-функції, тобто ви не змінюєте її код.
Це запускає 8 потоків, які тягнуть trials з тієї самої study. Підходить, коли objective-функція не тримає GIL, тобто виконує IO, працює з NumPy/PyTorch або відпускає GIL всередині C-розширень.
Розподілене дослідження через RDB
Для реального масштабування використовується спільна база даних (PostgreSQL/MySQL/SQLite). На кожній машині запускаєте один і той же скрипт із однаковим study_name та storage:
# worker.py
import optuna
def objective(trial):
... # ваша логіка
if __name__ == "__main__":
study = optuna.load_study(
study_name="prod-xgb-tuning",
storage="postgresql://optuna:[email protected]:5432/optuna_db",
)
study.optimize(objective, n_trials=50) # цей воркер зробить 50 trials
Ви запускаєте python worker.py на 20 машинах, і вони спільно виконають 20 × 50 = 1000 trials, координуючись через БД. Продуктивність зростає майже лінійно за умови, що storage не стає bottleneck (для дуже великих досліджень використовуйте PostgreSQL з optuna.storages.JournalStorage для зменшення локів).
Spark-інтеграція для MLflow 3
MLflow 3, випущений у 2026, приніс клас MlflowSparkStudy, який розподіляє Optuna trials по Spark-executors:
from mlflow.optuna.storage import MlflowStorage
from mlflow.optuna.spark import MlflowSparkStudy
storage = MlflowStorage(tracking_uri="databricks")
study = MlflowSparkStudy(
study_name="prod-lgbm-2026-08",
storage=storage,
direction="minimize",
)
study.optimize(objective, n_trials=500, spark_parallelism=32)
Багатоцільова оптимізація з NSGA-II
У реальних задачах ви часто оптимізуєте не одну метрику. Приклад: хочете модель з максимальною accuracy і мінімальним inference latency. Ці цілі часто конфліктують, бо точніша модель більша й повільніша. Optuna розв'язує це через багатоцільову оптимізацію (multi-objective HPO) з поверненням Парето-фронту. Це множина рішень, кожне з яких не гірше за інші за всіма цілями.
import optuna, time
import lightgbm as lgb
from sklearn.model_selection import train_test_split
def objective(trial: optuna.Trial):
params = {
"objective": "binary",
"metric": "auc",
"verbosity": -1,
"num_leaves": trial.suggest_int("num_leaves", 15, 511),
"learning_rate": trial.suggest_float("lr", 1e-3, 0.3, log=True),
"min_child_samples": trial.suggest_int("min_child_samples", 5, 100),
}
booster = lgb.train(params, dtrain, num_boost_round=trial.suggest_int("n_est", 100, 2000))
# Ціль 1: AUC (максимізуємо)
auc = compute_auc(booster, X_val, y_val)
# Ціль 2: час inference на 10k рядків (мінімізуємо)
t0 = time.perf_counter()
_ = booster.predict(X_val[:10000])
latency_ms = (time.perf_counter() - t0) * 1000
return auc, latency_ms
study = optuna.create_study(
directions=["maximize", "minimize"],
sampler=optuna.samplers.NSGAIISampler(population_size=40, seed=42),
)
study.optimize(objective, n_trials=200)
# Парето-фронт: не одне рішення, а множина
for t in study.best_trials:
print(f"Trial {t.number}: AUC={t.values[0]:.4f}, latency={t.values[1]:.2f}ms, params={t.params}")
Обираючи фінальну модель, ви дивитеся на фронт і приймаєте бізнес-рішення: «beauty-точка», де крива різко згинається, зазвичай виявляється найкращим компромісом. Це особливо корисно для команд MLOps, які викочують моделі під SLA латентності. Читайте наш посібник з розгортання ML-моделей на FastAPI для контексту з боку продакшн-API.
Візуалізація та optuna-dashboard
Optuna має вбудовані функції візуалізації в optuna.visualization (Plotly) і optuna.visualization.matplotlib. Найкорисніші графіки, які я запускаю після кожного дослідження: optimization history (як покращувався кращий score), parameter importance (які параметри реально впливають) і parallel coordinate (як параметри корелюють з results).
Для інтерактивного моніторингу запустіть optuna-dashboard. Це веб-інтерфейс, що читає той самий RDB storage і оновлюється в реальному часі, поки воркери працюють:
Відкрийте http://localhost:8080, і побачите список studies, деталі кожного trial, графіки та трансляцію нових результатів. Це неоціненно, коли ви запустили дослідження на кластер і хочете переконатися, що воркери не завмерли.
Інтеграція з MLflow 3
Одна з найбільших новин 2026 року, як на мене, це глибша інтеграція Optuna з MLflow 3. Тепер можна логувати кожен trial як окремий MLflow run, а метадані study зберігати як MLflow experiment. Це дає повний audit trail для регульованих індустрій (фінтех, охорона здоров'я).
import mlflow
import optuna
mlflow.set_tracking_uri("http://mlflow.internal:5000")
mlflow.set_experiment("optuna-lgbm-tuning-2026-08")
def objective(trial: optuna.Trial) -> float:
with mlflow.start_run(nested=True):
params = {
"learning_rate": trial.suggest_float("lr", 1e-3, 0.3, log=True),
"num_leaves": trial.suggest_int("num_leaves", 15, 255),
}
mlflow.log_params(params)
score = train_and_eval(params)
mlflow.log_metric("val_auc", score)
return score
with mlflow.start_run(run_name="optuna-parent"):
study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=100)
mlflow.log_metric("best_auc", study.best_value)
mlflow.log_params({f"best_{k}": v for k, v in study.best_params.items()})
Кожен trial з'явиться як nested run у MLflow UI, і ви зможете сортувати, фільтрувати та порівнювати їх точно так само, як і будь-які інші експерименти. Для повноцінного pipeline також розгляньте ML-пайплайни зі scikit-learn 1.8, де Optuna працює всередині Pipeline-об'єкта.
Найкращі практики та типові помилки
Скільки trials достатньо?
Для класичного ML з 5–10 параметрами: 100–300 trials з TPE зазвичай досягають <1% розриву від абсолютного оптимуму. Для 20+ параметрів або складних DL-моделей потрібно 500–2000. Замість фіксованого n_trials використовуйте timeout у секундах, щоб не переплачувати за compute.
Правильний scope простору пошуку
Найпоширеніша помилка, яку я бачу в код-рев'ю, це надто вузький діапазон. Якщо ви обираєте learning_rate від 0.05 до 0.1, ви ніколи не дізнаєтеся, що 0.005 працює краще. Стартуйте з широкого лог-діапазону (наприклад, 1e-4 до 0.3), запустіть 50 trials, подивіться на plot_slice, і за потреби звузьте.
Двоетапна оптимізація
Ефективна стратегія: перший прохід це 100 trials з широкими діапазонами; аналіз parameter importance; другий прохід це 200 trials з фіксованими незначущими параметрами й звуженими діапазонами значущих. Це стабільно дає кращі результати за єдиний прохід з 300 trials.
Фіксуйте seed для відтворюваності
Завжди передавайте seed в семплер і, якщо можливо, у самій моделі. Це не гарантує 100% відтворюваності при n_jobs>1, але для single-process runs обов'язково.
Не забудьте про data leakage
Cross-validation має бути всередині objective-функції, а не поза нею. Якщо ви тюните на тому ж hold-out, який пізніше використовуєте для фінальної оцінки, ви отримаєте оптимістичний bias. Best practice це три split: train (для CV в objective), validation (для остаточного вибору trial), test (для звіту).
Warm-start з існуючих досліджень
Якщо ви робите повторний тюнінг з новими даними, не починайте з нуля. Використовуйте study.add_trials(previous_study.trials), і TPE-семплер побачить попередні результати й швидко адаптується. Також OptunaHub у 2026 додав meta-learning-розширення TPE, яке підтягує знання з попередніх studies автоматично.
Часті питання
Optuna краща за GridSearchCV?
Так, для більш ніж 3–4 гіперпараметрів Optuna майже завжди перевершує GridSearchCV за швидкістю збіжності та якістю знайденого оптимуму. GridSearch залишається корисним для дуже малих сіток (2–3 параметри з кількома значеннями) та навчальних цілей, коли важлива інтерпретованість повного перебору. Для більшості продакшн-задач Optuna економить години compute-часу.
Скільки trials потрібно для Optuna?
Практичний орієнтир: 30–50 trials на кожен неперервний параметр з логарифмічним діапазоном, або мінімум 100 trials у сумі. Для складних DL-моделей з 20+ параметрами планують 500–2000 trials з прунінгом. Замість фіксованого числа використовуйте timeout, бо це кращий контроль бюджету.
Чи можна запускати Optuna паралельно?
Так, двома способами: локально через n_jobs=N у методі study.optimize (потоки в одному процесі) або розподілено через спільний RDB storage (PostgreSQL/MySQL/SQLite), коли кілька процесів або машин виконують ту саму study. Розподілений режим масштабується практично лінійно до 8+ воркерів. У 2026 з'явився також MlflowSparkStudy для Spark-кластерів.
Що таке TPE-семплер в Optuna?
TPE (Tree-structured Parzen Estimator) це байєсівський алгоритм, який моделює розподіли параметрів окремо для «хороших» і «поганих» trials за квантилем і обирає наступні значення там, де співвідношення щільностей найвище. Це семплер за замовчуванням в Optuna, і для більшості задач мультиваріативний TPE (multivariate=True) дає найкращий компроміс якість/швидкість.
Чи підтримує Optuna раннє зупинення?
Так, через механізм прунерів (MedianPruner, HyperbandPruner, SuccessiveHalvingPruner). Ви викликаєте trial.report(value, step) на кожній ітерації навчання (епоха, boost-раунд) і trial.should_prune() для перевірки. Прунінг сумісний тільки з single-objective дослідженнями. Для multi-objective (NSGA-II) прунери не працюють, тож потрібно реалізовувати вручну.
Як інтегрувати Optuna з MLflow?
Простий шлях це логувати кожен trial як nested MLflow run через mlflow.start_run(nested=True) усередині objective. MLflow 3 у 2026 додав нативний MlflowStorage (Optuna використовує MLflow як backend) і MlflowSparkStudy для запуску на Spark-executors. Це особливо корисно для регульованих індустрій, де потрібен повний audit trail експериментів.
Marimo — це реактивний ноутбук для Python із детермінованим DAG виконання. Розбираю встановлення, SQL-комірки з DuckDB, WASM-експорт і міграцію з Jupyter.
Практичний посібник з розгортання ML-моделей на FastAPI у 2026: lifespan-події, Pydantic v2 з Rust-ядром, батчинг, Docker-збірка та Prometheus-моніторинг. Робочий код від scikit-learn до продакшн-API.
Практичний посібник з PyIceberg 0.11.1 у Python: REST і Glue каталоги, створення таблиць, UPSERT, schema evolution, time travel, інтеграції з PyArrow, Polars і DuckDB та стан підтримки Iceberg v3 у 2026.