Мы часто видим: модель обучена, baseline accuracy вроде приемлем, но гиперпараметры взяты «из примеров». Learning rate — потому что «так в туториале», batch size — «стандартный», dropout — «на глаз». После грамотной HPO на тех же данных и той же архитектуре получаем +4–8% accuracy. Это не магия, а систематический поиск с помощью Optuna, Ray Tune и Hyperopt. Разберём, как мы внедряем HPO в production и экономим до 5× вычислительных ресурсов.
Почему Bayesian Optimization выигрывает у Random Search?
Random Search эффективен при высокой размерности и малом бюджете. Но когда важных гиперпараметров 3–5 (типичный случай), Bayesian Optimization с TPE начинает выигрывать с ~30-го триала. TPE строит раздельные плотности для «хороших» (top-25%) и «плохих» конфигураций, затем предлагает конфигурации с высоким Expected Improvement. Grid Search сегодня применим только к двум гиперпараметрам — дальше комбинаторный взрыв.
Как Optuna сокращает время поиска?
Optuna — de-facto стандарт HPO в Python. Ключевые преимущества: Pythonic API без YAML-конфигов, встроенный pruning, интеграция с MLflow и Weights & Biases. Ключевая фишка — Hyperband Pruner, который обрезает плохие триалы на ранних rounds. На практике из 200 триалов LightGBM 40–60% обрезаются после 50–100 rounds вместо полных 2000. Итоговое ускорение: 3–5×.
Полный пример: оптимизация 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 = 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))
sampler = optuna.samplers.TPESampler(
n_startup_trials=20,
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',
load_if_exists=True
)
study.optimize(
lambda trial: objective(trial, X, y),
n_trials=200,
n_jobs=4,
timeout=3600,
show_progress_bar=True
)
print(f'Best AUC: {study.best_value:.4f}')
print(f'Best params: {study.best_params}')
Визуализация и анализ важности параметров:
import optuna.visualization as vis
fig = vis.plot_param_importances(study)
fig.show()
fig = vis.plot_optimization_history(study)
fig.show()
fig = vis.plot_contour(study, params=['num_leaves', 'learning_rate'])
fig.show()
Анализ fANOVA часто даёт неожиданные результаты: num_leaves и min_child_samples оказываются важнее learning_rate для LightGBM на несбалансированных данных.
Когда стоит выбрать Ray Tune?
Ray Tune решает другую задачу — параллельный поиск на кластере GPU. Если Optuna с n_jobs=4 параллелит на одной машине, Ray Tune масштабируется до сотен узлов. Ray Tune лучше подходит для deep learning с распределённым обучением, а Optuna — для классического ML на одной машине.
from ray import tune
from ray.tune.schedulers import ASHAScheduler
from ray.tune.search.optuna import OptunaSearch
import torch
def train_transformer(config: dict):
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)
tune.report(val_loss=val_loss, val_acc=val_acc, epoch=epoch)
scheduler = ASHAScheduler(
time_attr='epoch',
max_t=100,
grace_period=10,
reduction_factor=3,
metric='val_loss',
mode='min'
)
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',
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). Самые важные параметры: 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 |
Экономия от внедрения: снижение ложных срабатываний на 23% сэкономило клиенту существенную сумму на ручной верификации.
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
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.
Что входит в работу под ключ
- Аудит текущего пайплайна и выбор инструмента (Optuna / Ray Tune / Hyperopt)
- Настройка search space и метрик на основе бизнес-целей
- Реализация HPO с pruning и параллельными триалами
- Интеграция с MLflow для трекинга экспериментов
- Документация по воспроизведению результатов
- Обучение команды работе с инструментом
Процесс работы
- Аналитика — сбор требований, изучение данных, baseline модели.
- Проектирование — выбор HPO-фреймворка, определение search space, метрик.
- Реализация — написание objective-функции, настройка параллелизма и pruning.
- Тестирование — запуск на CV, проверка на holdout, сравнение с baseline.
- Деплой — внедрение лучшей конфигурации в CI/CD, мониторинг в production.
Сроки и стоимость
Сроки: базовая HPO с Optuna на одной задаче — 2–5 дней. Distributed HPO с Ray Tune и интеграцией в CI/CD — 2–4 недели. Стоимость рассчитывается индивидуально в зависимости от сложности задачи, объёма данных и требуемой инфраструктуры. Оценим ваш проект бесплатно — свяжитесь с нами для консультации.
Наша команда имеет многолетний опыт в ML-продакшене и реализовала десятки проектов по HPO для клиентов из fintech, e-commerce и рекламных технологий.







