Реалізація Hyperparameter Optimization (Optuna, Ray Tune, Hyperopt)

Проектуємо та впроваджуємо системи штучного інтелекту: від прототипу до production-ready рішення. Наша команда поєднує експертизу в машинному навчанні, дата-інжинірингу та MLOps, щоб AI працював не в лабораторії, а в реальному бізнесі.
Показано 1 з 1Усі 1566 послуг
Реалізація Hyperparameter Optimization (Optuna, Ray Tune, Hyperopt)
Середній
~2-3 дні
Часті запитання

Напрямки AI-розробки

Етапи розробки AI-рішення

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1284
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1196
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    901
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1119
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    586
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    853

Оптимізація гіперпараметрів: Optuna та Ray Tune

Типова історія: модель навчена, базовий accuracy начебто прийнятний. Але learning_rate=0.001 взято «з прикладів у документації», batch_size=32 «бо стандарт», dropout=0.3 «на око». Після грамотної HPO на тому ж датасеті і тій же архітектурі отримуємо +4-8% accuracy - просто правильних гіперпараметрів. Не магія, це систематичний пошук.

Чому Random Search програє Bayesian Optimization

Random Search ефективний при високій розмірності простору та малому бюджеті тріалів. Але як тільки важливих гіперпараметрів 3-5 (а це типовий випадок), Bayesian Optimization з TPE (Tree-structured Parzen Estimator) починає вигравати з 30-го тріалу. Суть TPE: будує роздільні щільності ймовірності для «хороших» (top-25%) та «поганих» конфігурацій, потім пропонує конфігурації з високим EI (Expected Improvement).

Grid Search у 2025 році застосуємо лише до двох гіперпараметрів максимум — далі комбінаторний вибух робить його недоцільним.

Глибокий розбір: Optuna у production

Optuna - de-facto стандарт для HPO в Python-екосистемі. Ключові переваги перед конкурентами: Pythonic API без YAML-конфігів, вбудована підтримка pruning (обрізання поганих тріалів на ранній стадії), інтеграція з MLflow та Weights & Biases.

Повний приклад: оптимізація LightGBM з pruning

import optuna
from optuna.integration import LightGBMPruningCallback
import lightgbm as lgb
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import roc_auc_score
import numpy as np

def objective(trial: optuna.Trial, X, y) -> float:
    params = {
        'objective': 'binary',
        'metric': 'auc',
        'verbosity': -1,
        'boosting_type': trial.suggest_categorical('boosting', ['gbdt', 'dart']),
        'n_estimators': trial.suggest_int('n_estimators', 100, 2000),
        'learning_rate': trial.suggest_float('learning_rate', 1e-4, 0.3, log=True),
        'num_leaves': trial.suggest_int('num_leaves', 20, 300),
        'max_depth': trial.suggest_int('max_depth', 3, 12),
        'min_child_samples': trial.suggest_int('min_child_samples', 5, 300),
        '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', 1, 7),
        'reg_alpha': trial.suggest_float('reg_alpha', 1e-9, 10.0, log=True),
        'reg_lambda': trial.suggest_float('reg_lambda', 1e-9, 10.0, log=True),
    }

    cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
    cv_scores = []

    for fold, (train_idx, val_idx) in enumerate(cv.split(X, y)):
        X_train, X_val = X[train_idx], X[val_idx]
        y_train, y_val = y[train_idx], y[val_idx]

        dtrain = lgb.Dataset(X_train, label=y_train)
        dval = lgb.Dataset(X_val, label=y_val, reference=dtrain)

        # Pruning callback: обрезает плохие триалы после каждого round
        pruning_callback = LightGBMPruningCallback(trial, 'auc', valid_name='valid_1')

        model = lgb.train(
            params,
            dtrain,
            valid_sets=[dtrain, dval],
            num_boost_round=params['n_estimators'],
            callbacks=[
                lgb.early_stopping(stopping_rounds=50, verbose=False),
                lgb.log_evaluation(period=-1),
                pruning_callback,
            ],
        )

        y_pred = model.predict(X_val)
        cv_scores.append(roc_auc_score(y_val, y_pred))

    return float(np.mean(cv_scores))


# Создаём study с TPE sampler и Hyperband pruner
sampler = optuna.samplers.TPESampler(
    n_startup_trials=20,     # случайный поиск до построения surrogate
    multivariate=True,       # учитывает корреляции между параметрами
    seed=42
)
pruner = optuna.pruners.HyperbandPruner(
    min_resource=50,
    max_resource=2000,
    reduction_factor=3
)

study = optuna.create_study(
    direction='maximize',
    sampler=sampler,
    pruner=pruner,
    study_name='lgbm_credit_scoring',
    storage='sqlite:///optuna_studies.db',  # persistence между сессиями
    load_if_exists=True
)

study.optimize(
    lambda trial: objective(trial, X, y),
    n_trials=200,
    n_jobs=4,          # параллельные триалы
    timeout=3600,      # максимум 1 час
    show_progress_bar=True
)

print(f'Best AUC: {study.best_value:.4f}')
print(f'Best params: {study.best_params}')

Pruning - ключова економія обчислень. Hyperband Pruner вбиває погані тріали на ранніх rounds навчання. На практиці: із 200 тріалів LightGBM — 40–60% обрізаються після 50–100 rounds замість повних 2000. Підсумкове прискорення: 3–5× у порівнянні з тим самим числом повних тріалів.

Візуалізація та аналіз важливості параметрів:

import optuna.visualization as vis

# Какие гиперпараметры реально влияют на результат
fig = vis.plot_param_importances(study)
fig.show()

# История оптимизации — смотрим, сошлась ли она
fig = vis.plot_optimization_history(study)
fig.show()

# Correlation matrix: num_leaves vs learning_rate
fig = vis.plot_contour(study, params=['num_leaves', 'learning_rate'])
fig.show()

Аналіз важливості параметрів через fANOVA часто дає несподівані результати: num_leaves та min_child_samples виявляються важливішими за learning_rate для LightGBM на незбалансованих даних. Це змінює стратегію – наступний пошук фокусується на вузькому діапазоні важливих параметрів.

Ray Tune: distributed HPO на кластері

Ray Tune вирішує інше завдання – паралельний пошук на кластері GPU. Якщо Optuna з n_jobs = 4 паралеліт на одній машині, Ray Tune масштабується до сотень вузлів.

from ray import tune
from ray.tune.schedulers import ASHAScheduler
from ray.tune.search.optuna import OptunaSearch
import torch

def train_transformer(config: dict):
    """
    Ray Tune ожидает функцию, которая репортит метрики через tune.report().
    """
    model = build_model(
        hidden_dim=config['hidden_dim'],
        num_heads=config['num_heads'],
        num_layers=config['num_layers'],
        dropout=config['dropout']
    )
    optimizer = torch.optim.AdamW(
        model.parameters(),
        lr=config['lr'],
        weight_decay=config['weight_decay']
    )

    for epoch in range(config['max_epochs']):
        train_loss = train_one_epoch(model, optimizer)
        val_loss, val_acc = evaluate(model)

        # Ray Tune получает метрику для Scheduler/Search
        tune.report(
            val_loss=val_loss,
            val_acc=val_acc,
            epoch=epoch
        )

# ASHA (Asynchronous Successive Halving) — aggressive early stopping
scheduler = ASHAScheduler(
    time_attr='epoch',
    max_t=100,              # максимум epochs
    grace_period=10,        # минимум epochs до обрезки
    reduction_factor=3,     # каждые 3× — половина триалов выбывает
    metric='val_loss',
    mode='min'
)

# OptunaSearch внутри Ray Tune — лучший из обоих миров
search_alg = OptunaSearch(
    metric='val_loss',
    mode='min',
    sampler=optuna.samplers.TPESampler(seed=42)
)

search_space = {
    'hidden_dim': tune.choice([128, 256, 512]),
    'num_heads': tune.choice([4, 8, 16]),
    'num_layers': tune.randint(2, 8),
    'dropout': tune.uniform(0.0, 0.5),
    'lr': tune.loguniform(1e-5, 1e-2),
    'weight_decay': tune.loguniform(1e-8, 1e-3),
    'max_epochs': 100
}

analysis = tune.run(
    train_transformer,
    config=search_space,
    num_samples=100,        # общее число триалов
    scheduler=scheduler,
    search_alg=search_alg,
    resources_per_trial={'gpu': 1, 'cpu': 4},
    storage_path='s3://my-bucket/ray-results',   # S3 для distributed setup
    name='transformer_hpo_v2'
)

best_config = analysis.get_best_config(metric='val_loss', mode='min')

Кейс: HPO для fraud detection моделі

Завдання: бінарна класифікація транзакцій, дисбаланс 1: 340 (fraud: normal), 2.1M записів. Baseline XGBoost із дефолтними параметрами: PR-AUC = 0.412.

Optuna, 150 тріалів, 4 паралельні воркери, ~2.5 години:

  • search space: 11 параметрів XGBoost + scale_pos_weight (1-350)
  • метрика: PR-AUC на stratified 5-fold CV
  • pruner: MedianPruner (обрізає тріали нижче медіани на ранніх етапах)

Результат: PR-AUC = 0.581 (+41% щодо baseline). Найважливіші параметри по fANOVA: scale_pos_weight (22% важливості), min_child_weight (18%), subsample (15%). max_depth та n_estimators - сумарно 14%.

Етап PR-AUC Recall у Precision=0.8
XGBoost default 0.412 0.34
Random Search (50 trials) 0.521 0.47
Optuna TPE (150 trials) 0.581 0.56
+ Feature engineering 0.634 0.62

Optuna vs Ray Tune: коли що вибрати

Критерій Optuna Ray Tune
Одна машина, 1-8 GPU + надмірний
Кластер 10+ GPU/вузлів складніше +
Deep learning (PyTorch/JAX) + +
Класичний ML (sklearn, lgbm) + працює
Інтеграція з distributed training через callbacks native
Відновлення після збою SQLite/PostgreSQL backend +
Крива навчання для нової команди полога крутіше

Інтеграція з MLflow та Weights & Biases

import mlflow
import optuna

def objective_with_tracking(trial):
    with mlflow.start_run(nested=True):
        params = {
            'lr': trial.suggest_float('lr', 1e-5, 1e-1, log=True),
            'dropout': trial.suggest_float('dropout', 0.1, 0.5),
        }
        mlflow.log_params(params)
        # ... обучение
        val_acc = train_and_evaluate(params)
        mlflow.log_metric('val_acc', val_acc)
        return val_acc

# Все триалы — отдельные MLflow runs, удобно для сравнения
with mlflow.start_run(run_name='hpo_study'):
    study.optimize(objective_with_tracking, n_trials=100)
    mlflow.log_metric('best_val_acc', study.best_value)
    mlflow.log_params(study.best_params)

Типові помилки. Data leakage в objective: якщо preprocessing (StandardScaler, target encoding) фітується на всьому train-set перед CV результати HPO оптимістично завищені, production-деградація гарантована. Scaler повинен гнітитися тільки на train-fold всередині CV. Ще одна: оптимізація accuracy замість бізнес-метрики при дисбалансі класів – знаходимо конфігурацію з accuracy 98.3% при recall на minority-клас 0.04.

Терміни: базова HPO з Optuna на одному завданні — 2–5 днів, включаючи налаштування оточення та аналіз результатів. Distributed HPO з Ray Tune на кластері, інтеграція з CI/CD пайплайном - 2-4 тижні.