Optuna w Pythonie: strojenie hiperparametrów ML krok po kroku (2026)

Optuna 4.x to nowoczesna biblioteka do automatycznego strojenia hiperparametrów w Pythonie. Poznaj TPE, pruning, optymalizację wielokryterialną i integracje ze scikit-learn, XGBoost oraz LightGBM, z gotowymi przykładami od pierwszej funkcji celu po produkcję.

Optuna 4.x: Strojenie Hiperparametrów (2026)

Zaktualizowano: 31 maja 2026

Optuna to otwartoźródłowa biblioteka Pythona do automatycznego strojenia hiperparametrów modeli uczenia maszynowego, która wykorzystuje algorytm TPE (Tree-structured Parzen Estimator) oraz wczesne odrzucanie nieobiecujących prób (pruning), żeby znaleźć optymalną konfigurację modelu w ułamku czasu potrzebnego dla siatkowego przeszukiwania. W wersji 4.x (2026) Optuna obsługuje rozproszone studia, optymalizację wielokryterialną oraz natywne integracje z scikit-learn, XGBoost, LightGBM i PyTorch. W tym przewodniku przeprowadzę Cię od pierwszej funkcji celu do produkcyjnego pipeline'u strojenia, opierając się głównie na tym, czego nauczyłem się, strojąc modele kredytowe i NLP w ostatnich dwóch latach.

  • Optuna 4.x (wydana w 2026) zastępuje GridSearchCV i RandomizedSearchCV, oferując strategię TPE, która zazwyczaj znajduje lepsze hiperparametry w 5–10× mniejszej liczbie prób.
  • Pruning (np. MedianPruner, HyperbandPruner) przerywa nieobiecujące próby już po kilku epokach, oszczędzając nawet 60% czasu obliczeń.
  • Studium (Study) można utrwalić w SQLite lub PostgreSQL, dzięki czemu wielu pracowników może równolegle eksplorować przestrzeń hiperparametrów.
  • Optymalizacja wielokryterialna (algorytm NSGA-II) pozwala jednocześnie maksymalizować dokładność i minimalizować rozmiar modelu lub czas inferencji.
  • Wbudowane wizualizacje (parallel coordinate, contour, slice plot) pomagają zrozumieć, które hiperparametry naprawdę mają znaczenie statystyczne.
  • Integracje z scikit-learn, XGBoost, LightGBM, PyTorch i Hugging Face sprowadzają strojenie do kilku linii kodu.

Czym jest Optuna i dlaczego warto z niej korzystać?

Optuna to framework do automatycznej optymalizacji hiperparametrów, stworzony pierwotnie w Preferred Networks (Akiba i in., KDD 2019). W odróżnieniu od klasycznego GridSearchCV, który ślepo wypróbowuje każdą kombinację z siatki, Optuna stosuje strategię define-by-run: przestrzeń poszukiwań budowana jest dynamicznie wewnątrz funkcji celu, co pozwala na warunkowe gałęzie (np. inne hiperparametry dla SGD niż dla Adam).

Sercem biblioteki jest algorytm TPE, czyli bayesowska metoda oparta na modelu, która utrzymuje dwa rozkłady prawdopodobieństwa: jeden dla najlepszych prób, drugi dla pozostałych. Nowe próby są próbkowane tam, gdzie stosunek tych gęstości jest największy. W praktyce oznacza to, że po 30–50 próbach TPE zwykle dorównuje wynikom 500-elementowego przeszukiwania losowego.

Drugi filar to pruning, czyli wczesne przerywanie nieobiecujących prób. Jeśli model po 3 epokach osiąga gorszy wynik niż mediana wszystkich dotychczasowych prób w tym samym kroku, Optuna kończy go natychmiast. Według oficjalnej dokumentacji to typowo skraca całkowity czas o 40–70% bez utraty jakości najlepszego znaleziska. Jeśli wcześniej pracowałeś z potokami ML w scikit-learn, Optunę można dokleić do istniejącego Pipeline w kilka minut.

Instalacja i konfiguracja Optuna 4.x

Optuna 4.x wymaga Pythona ≥ 3.9 i działa zarówno z CPython, jak i PyPy. Najprostsza instalacja:

pip install "optuna>=4.0,<5.0"
# integracje opcjonalne, instalujemy tylko to, czego potrzebujemy
pip install optuna-integration[sklearn,xgboost,lightgbm,pytorch]
# wizualizacje wymagają plotly
pip install plotly

Domyślnym backendem przechowywania studiów jest pamięć operacyjna (InMemoryStorage), ale w produkcji warto użyć SQLite lub PostgreSQL. Wtedy studium przeżyje restart procesu, a wielu pracowników może równolegle dopisywać próby. Konfigurację robimy raz, na początku skryptu:

import optuna
from optuna.storages import RDBStorage

storage = RDBStorage(
    url="sqlite:///studies/optuna.db",
    engine_kwargs={"connect_args": {"timeout": 30}},  # zapobiega 'database is locked'
)
study = optuna.create_study(
    study_name="xgboost-credit-risk",
    storage=storage,
    direction="maximize",   # maksymalizujemy AUC
    load_if_exists=True,    # wznawia istniejące studium
)

Pierwsza funkcja celu krok po kroku

Funkcja celu (objective function) to centralny element Optuna. Przyjmuje obiekt trial, z którego losujemy hiperparametry, trenuje model i zwraca metrykę do zoptymalizowania. Zacznijmy od regresji logistycznej na klasycznym zbiorze breast cancer:

import optuna
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

X, y = load_breast_cancer(return_X_y=True)

def objective(trial: optuna.Trial) -> float:
    # suggest_* to fabryki próbujące wartości z zadanej przestrzeni
    C = trial.suggest_float("C", 1e-4, 1e2, log=True)
    solver = trial.suggest_categorical("solver", ["lbfgs", "liblinear", "saga"])
    # warunkowa gałąź: penalty zależy od solvera
    if solver == "liblinear":
        penalty = trial.suggest_categorical("penalty", ["l1", "l2"])
    elif solver == "saga":
        penalty = trial.suggest_categorical("penalty", ["l1", "l2", "elasticnet"])
    else:
        penalty = "l2"

    extra = {}
    if penalty == "elasticnet":
        extra["l1_ratio"] = trial.suggest_float("l1_ratio", 0.0, 1.0)

    pipe = Pipeline([
        ("scaler", StandardScaler()),
        ("clf", LogisticRegression(
            C=C, solver=solver, penalty=penalty, max_iter=5000, **extra
        )),
    ])
    # ROC AUC jako metryka, wyższa = lepsza
    scores = cross_val_score(pipe, X, y, cv=5, scoring="roc_auc", n_jobs=-1)
    return float(np.mean(scores))

study = optuna.create_study(direction="maximize", sampler=optuna.samplers.TPESampler(seed=42))
study.optimize(objective, n_trials=60, show_progress_bar=True)

print("Najlepsza AUC:", round(study.best_value, 4))
print("Najlepsze hiperparametry:", study.best_params)

Trzy rzeczy warto tu wyjaśnić. Po pierwsze, log=True w suggest_float losuje wartości w skali logarytmicznej, co jest kluczowe dla parametrów regularyzacji, które rozciągają się przez kilka rzędów wielkości. Po drugie, warunkowa gałąź dla penalty pokazuje siłę paradygmatu define-by-run: tego po prostu nie da się wyrazić w GridSearchCV. Po trzecie, ustawienie seed w samplerze daje powtarzalność, co jest wymogiem statystycznym każdej publikowanej analizy ablacyjnej.

Jak zintegrować Optuna ze scikit-learn?

Najprostsza droga to napisać własny objective, jak powyżej. Dla użytkowników, którzy chcą zachować API fit/predict, Optuna dostarcza OptunaSearchCV w pakiecie optuna-integration, czyli drop-in zamiennik dla GridSearchCV:

from optuna.integration import OptunaSearchCV
from optuna.distributions import FloatDistribution, CategoricalDistribution
from sklearn.ensemble import RandomForestClassifier

param_distributions = {
    "n_estimators": optuna.distributions.IntDistribution(50, 800),
    "max_depth": optuna.distributions.IntDistribution(2, 32),
    "min_samples_split": optuna.distributions.IntDistribution(2, 32),
    "max_features": FloatDistribution(0.1, 1.0),
    "criterion": CategoricalDistribution(["gini", "entropy", "log_loss"]),
}

search = OptunaSearchCV(
    estimator=RandomForestClassifier(random_state=0, n_jobs=-1),
    param_distributions=param_distributions,
    n_trials=80,
    cv=5,
    scoring="roc_auc",
    random_state=0,
    timeout=600,   # zatrzymaj po 10 minutach
)
search.fit(X, y)
print(search.best_params_, search.best_score_)

Po dopasowaniu search.best_estimator_ jest gotowym modelem scikit-learn, który możesz wrzucić do dowolnego Pipeline. Dla zbiorów, które trzeba wcześniej oczyścić, polecam mój wcześniejszy artykuł o profesjonalnym czyszczeniu danych w pandas 3.0. Szczerze mówiąc, dobrze dobrane cechy potrafią dać większy zysk niż jakiekolwiek strojenie.

Strojenie XGBoost i LightGBM z pruningiem

Gradient boosting to obszar, w którym pruning Optuna naprawdę błyszczy. Każda próba może raportować pośrednie wartości po każdej rundzie boostingu, a pruner natychmiast przerwie próbę odstającą od mediany. Przykład dla LightGBM:

import lightgbm as lgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score
from optuna.integration import LightGBMPruningCallback

X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=0, stratify=y)

def objective(trial: optuna.Trial) -> float:
    params = {
        "objective": "binary",
        "metric": "auc",
        "verbosity": -1,
        "boosting_type": "gbdt",
        "learning_rate": trial.suggest_float("learning_rate", 1e-3, 0.3, log=True),
        "num_leaves": trial.suggest_int("num_leaves", 8, 256, log=True),
        "max_depth": trial.suggest_int("max_depth", -1, 16),
        "min_child_samples": trial.suggest_int("min_child_samples", 5, 100),
        "feature_fraction": trial.suggest_float("feature_fraction", 0.4, 1.0),
        "bagging_fraction": trial.suggest_float("bagging_fraction", 0.4, 1.0),
        "bagging_freq": trial.suggest_int("bagging_freq", 0, 7),
        "lambda_l1": trial.suggest_float("lambda_l1", 1e-8, 10.0, log=True),
        "lambda_l2": trial.suggest_float("lambda_l2", 1e-8, 10.0, log=True),
    }
    dtrain = lgb.Dataset(X_train, label=y_train)
    dvalid = lgb.Dataset(X_val, label=y_val, reference=dtrain)

    pruning_callback = LightGBMPruningCallback(trial, "auc", valid_name="valid_0")
    model = lgb.train(
        params, dtrain, num_boost_round=1000,
        valid_sets=[dvalid], valid_names=["valid_0"],
        callbacks=[pruning_callback, lgb.early_stopping(50, verbose=False)],
    )
    preds = model.predict(X_val)
    return roc_auc_score(y_val, preds)

study = optuna.create_study(
    direction="maximize",
    sampler=optuna.samplers.TPESampler(multivariate=True, seed=0),
    pruner=optuna.pruners.MedianPruner(n_warmup_steps=20),
)
study.optimize(objective, n_trials=120, timeout=900)

Tryb multivariate=True w samplerze TPE modeluje współzależności między hiperparametrami (np. learning_rate i num_leaves nie są niezależne). Według artykułu Watanabego i in. (2023) daje to średnio 15–25% lepsze wyniki na benchmarkach NAS-Bench-201 i LCBench. Dla XGBoost zamień LightGBMPruningCallback na XGBoostPruningCallback z tego samego pakietu integracji.

Częstym pytaniem na forach Stack Overflow i Reddit jest: czy Optuna naprawdę jest lepsza od starego dobrego GridSearchCV? Krótka odpowiedź: tak, gdy przestrzeń hiperparametrów ma więcej niż 3 wymiary lub gdy każda próba kosztuje dużo czasu. Poniższa tabela podsumowuje porównanie.

CechaOptuna 4.x (TPE)GridSearchCVHyperopt (TPE)
StrategiaBayesowska (TPE/CMA-ES/NSGA-II)Wyczerpująca siatkaBayesowska (TPE)
PruningTak (Median, Hyperband, SHA)NieNie
Define-by-runTakNieCzęściowo
RozproszenieTak (SQLite/PostgreSQL/JournalStorage)Tak (joblib)Tak (MongoDB)
Multi-objectiveTak (NSGA-II, NSGA-III)NieOgraniczone
WizualizacjeWbudowane (plotly)BrakBrak
Aktywny rozwójTak, wersja 4.x (2026)TakMarginalny od 2022
Krzywa uczeniaŁagodnaNajłatwiejszaStroma

W praktyce GridSearchCV zostawiam dla mikroprzestrzeni (≤ 3 hiperparametry, ≤ 20 kombinacji) lub dla porównań typu baseline w publikacjach. We wszystkim innym Optuna wygrywa stosunkiem jakość/koszt obliczeniowy.

Optymalizacja wielokryterialna

W produkcji prawie nigdy nie zależy nam wyłącznie na dokładności. Chcemy też, żeby model był mały, szybki w inferencji albo żeby zużywał mało pamięci. Optuna pozwala optymalizować wiele celów jednocześnie za pomocą algorytmu NSGA-II, zwracając cały front Pareto:

import time

def objective(trial: optuna.Trial):
    n_estimators = trial.suggest_int("n_estimators", 50, 1000)
    max_depth = trial.suggest_int("max_depth", 2, 16)
    model = lgb.LGBMClassifier(
        n_estimators=n_estimators, max_depth=max_depth,
        learning_rate=0.05, verbosity=-1,
    )
    model.fit(X_train, y_train)
    auc = roc_auc_score(y_val, model.predict_proba(X_val)[:, 1])

    # czas inferencji 1000 próbek (s)
    t0 = time.perf_counter()
    for _ in range(10):
        model.predict(X_val)
    inference_time = (time.perf_counter() - t0) / 10

    return auc, inference_time   # maksymalizujemy AUC, minimalizujemy czas

study = optuna.create_study(
    directions=["maximize", "minimize"],
    sampler=optuna.samplers.NSGAIISampler(seed=0),
)
study.optimize(objective, n_trials=80)

for t in study.best_trials:   # rozwiązania Pareto-optymalne
    print(f"AUC={t.values[0]:.4f}  inference={t.values[1]*1000:.2f} ms  params={t.params}")

Wynik to zazwyczaj 5–15 rozwiązań na froncie Pareto. Wybór końcowy to decyzja biznesowa: ile dokładności jesteś gotów oddać za 2× szybszy model? W moim ostatnim projekcie scoringowym wybraliśmy konfigurację o 0,3 pp niższej AUC, ale trzykrotnie szybszą, bo SLA inferencji było twarde.

Wizualizacje i analiza ważności

Po zakończeniu studium pierwszą rzeczą, jaką robię, jest analiza ważności hiperparametrów: która zmienna realnie wpływała na wynik, a która była szumem. Optuna używa do tego algorytmu fANOVA z artykułu Huttera, Hoosa i Leyton-Browna (ICML 2014):

from optuna.importance import get_param_importances, FanovaImportanceEvaluator
from optuna.visualization import (
    plot_optimization_history,
    plot_param_importances,
    plot_parallel_coordinate,
    plot_contour,
    plot_slice,
)

importances = get_param_importances(study, evaluator=FanovaImportanceEvaluator(seed=0))
print(importances)
# Każdy wykres to obiekt Plotly. Można go pokazać w Jupyterze lub zapisać do HTML
plot_optimization_history(study).write_html("opt_history.html")
plot_param_importances(study).write_html("importances.html")
plot_parallel_coordinate(study, params=["learning_rate", "num_leaves", "max_depth"]).write_html("parallel.html")
plot_contour(study, params=["learning_rate", "num_leaves"]).write_html("contour.html")

Wykres współrzędnych równoległych jest szczególnie cenny dla wizualizacji wielowymiarowej, bo pozwala dostrzec interakcje, które umykają w pojedynczych wykresach. Jeśli pracujesz w Jupyterze, polecam zestaw narzędzi z mojego artykułu o wizualizacji danych w Pythonie. Łączenie wykresów Plotly z statycznymi raportami matplotlib daje najlepsze efekty w raportach dla biznesu.

Rozproszone studia na wielu maszynach

Studium Optuna przechowywane w bazie danych można współbieżnie eksplorować z wielu procesów lub maszyn. Wystarczy uruchomić ten sam skrypt na różnych workerach, wskazując ten sam storage i study_name:

# worker.py
import optuna

study = optuna.load_study(
    study_name="xgboost-credit-risk",
    storage="postgresql://user:[email protected]:5432/optuna",
)
study.optimize(objective, n_trials=50)   # każdy worker dorzuca 50 prób

W praktyce uruchamiam to przez kubectl run z replikami albo prosty parallel z GNU Parallel na klastrze obliczeniowym. Dla scenariuszy chmurowych Optuna 4.x dostarcza JournalStorage, czyli backend oparty na plikach append-only, który działa nawet na NFS i nie wymaga osobnej bazy danych.

Dobre praktyki produkcyjne

Z mojego doświadczenia z wdrożeniami modeli klasyfikacyjnych i regresyjnych warto trzymać się kilku zasad:

  • Stałe seedy. Ustaw seed w samplerze, prunerze i bibliotekach modelujących. Bez tego studium nie jest powtarzalne, a recenzent papieru lub kolega z zespołu nie zreplikuje wyniku.
  • Cross-validation, nie pojedynczy split. Pojedynczy zbiór walidacyjny prowadzi do przeuczenia względem tego konkretnego splitu. Używaj cross_val_score z co najmniej 5-fold CV, a dla niezbalansowanych danych StratifiedKFold.
  • Czas jako twarda granica. Zamiast n_trials=1000 ustawiaj timeout=3600. Strojenie powinno mieć budżet w godzinach, nie w próbach.
  • Nested CV dla uczciwej ewaluacji. Strojenie i ewaluacja na tym samym zbiorze walidacyjnym to klasyczny data leakage. Trzymaj zewnętrzny zbiór testowy, którego Optuna nigdy nie widzi (sam o tym zapomniałem raz, i wynik na produkcji spadł o 4 pp AUC).
  • Logowanie i wersjonowanie. Eksportuj study.trials_dataframe() do pliku Parquet po każdej kampanii strojenia. Łączenie z MLflow lub Weights & Biases ułatwia śledzenie eksperymentów.
  • Wczesne sanity-checki. Zanim odpalisz 500-próbne studium na klastrze, uruchom 5 prób lokalnie. Złamane objective najlepiej wykryć na laptopie, nie po 8 godzinach na chmurze (płaciłem za tę lekcję rachunkiem AWS).

Najczęściej zadawane pytania

Czym Optuna różni się od Hyperopt?

Obie biblioteki implementują algorytm TPE, ale Optuna ma aktywny rozwój (wersja 4.x w 2026), wsparcie dla pruningu, multi-objective i define-by-run, podczas gdy Hyperopt nie miał istotnego wydania od 2022 roku. Dla nowych projektów polecam wyłącznie Optunę.

Ile prób (trials) wystarczy w Optuna?

Dla 3–5 hiperparametrów wystarcza zazwyczaj 50–100 prób. Dla 10+ hiperparametrów planuj 300–500 prób z włączonym pruningiem. Lepiej ustawić timeout w sekundach niż sztywną liczbę prób, bo TPE poprawia wyniki coraz wolniej i warto przerwać, gdy plateau utrzymuje się przez 20–30 prób.

Czy Optuna działa z PyTorch i Hugging Face?

Tak. Pakiet optuna-integration[pytorch] dostarcza PyTorchLightningPruningCallback oraz integrację z transformers.Trainer przez parametr hp_space. Pruning po każdej epoce skraca strojenie dużych modeli językowych z dni do godzin.

Jak debugować błąd „database is locked” w Optuna?

Ten błąd pochodzi z SQLite, gdy zbyt wielu pracowników zapisuje równolegle. Rozwiązania: zmniejsz liczbę workerów do 4, zwiększ timeout w connect_args, albo przejdź na JournalStorage (nowość w Optuna 4.0) lub PostgreSQL dla większej skali.

Czy Optuna może optymalizować hiperparametry dla deep learningu?

Tak, to jeden z głównych przypadków użycia. Połączenie TPE z HyperbandPruner oraz raportowaniem walidacyjnej straty po każdej epoce pozwala odciąć słabe konfiguracje już po 1–2 epokach, oszczędzając godziny GPU. Optuna była używana m.in. w pracach laureata konkursu Kaggle Imagenette i w benchmarkach NAS-Bench-201.

Czy mogę użyć Optuna do strojenia modeli inżynierii cech?

Tak. Wystarczy umieścić kroki transformacji wewnątrz funkcji celu i potraktować ich parametry (np. liczbę binów dyskretyzatora, próg PCA, strategię imputacji) jako hiperparametry. Pamiętaj, by używać Pipeline ze scikit-learn, aby uniknąć wycieku danych podczas cross-validation.

Polecam też przejrzeć oficjalną dokumentację Optuna, changelog na GitHubie oraz oryginalny artykuł KDD 2019 opisujący architekturę biblioteki. Jeśli zależy Ci na zrozumieniu, dlaczego TPE działa, ta lektura jest niezastąpiona.

Dr. Elena Vasquez
O Autorze Dr. Elena Vasquez

Data scientist with a PhD in computational statistics. Translates papers into pandas one notebook at a time.