Hyperparameter Tuning Guide 2026 — Optuna & Bayesian

Photo: Vitali Adutskevich / Pexels
Hyperparameters control how your model learns. Tuning them right can improve accuracy 5-30% without changing the algorithm. Tuning them poorly wastes hours and produces overfit models.
This guide covers the practical art of hyperparameter tuning — when it helps, when it doesn't, which methods to use, and how to do it efficiently.
Hyperparameters vs Parameters
Parameters: Learned from data during training. Weights of a neural network, coefficients in linear regression.
Hyperparameters: Set by you before training. Learning rate, number of trees, regularization strength, network depth.
You don't tune parameters; the algorithm learns them. You tune hyperparameters by trying different values and seeing what works.
When Hyperparameter Tuning Actually Matters
Worth Tuning
✅ Final production models where 1% accuracy improvement is worth hours of work ✅ Kaggle competitions (tiny improvements win) ✅ XGBoost / LightGBM (sensitive to hyperparameters) ✅ Neural networks (very sensitive) ✅ When you have a strong baseline already
NOT Worth Tuning
❌ Quick prototypes (use defaults, see if approach works) ❌ When your data is the problem (more data > better hyperparameters) ❌ When the model is the wrong choice entirely ❌ Small datasets where overfitting risk is high ❌ Random Forest with reasonable defaults (often good enough)
Rule: First get a baseline. Then improve features. Then improve algorithm choice. Tune hyperparameters last.
Method 1: Manual Tuning
Just try different values yourself. Most common for quick experimentation.
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
# Try a few configurations
for n_est in [100, 300, 500]:
for max_depth in [5, 10, None]:
model = RandomForestClassifier(n_estimators=n_est, max_depth=max_depth)
score = cross_val_score(model, X, y, cv=5).mean()
print(f"n={n_est}, d={max_depth}: {score:.3f}")
Pros: Fast iteration, builds intuition. Cons: Doesn't scale, biased by your beliefs.
Method 2: Grid Search
Try every combination of specified values.
from sklearn.model_selection import GridSearchCV
param_grid = {
'n_estimators': [100, 300, 500],
'max_depth': [5, 10, None],
'min_samples_leaf': [1, 5, 10]
}
# That's 3 × 3 × 3 = 27 combinations
grid = GridSearchCV(
RandomForestClassifier(),
param_grid,
cv=5,
n_jobs=-1,
scoring='roc_auc'
)
grid.fit(X, y)
print(grid.best_params_, grid.best_score_)
Pros: Exhaustive within the grid. Simple to understand. Cons: Exponential blowup — 6 params × 4 values each = 4,096 combinations. Wasteful.
Method 3: Random Search
Randomly sample combinations. Often beats grid search at same compute cost.
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import randint, uniform
param_dist = {
'n_estimators': randint(50, 1000),
'max_depth': randint(3, 30),
'min_samples_leaf': randint(1, 20),
'max_features': uniform(0.1, 0.9)
}
random_search = RandomizedSearchCV(
RandomForestClassifier(),
param_dist,
n_iter=100,
cv=5,
n_jobs=-1,
random_state=42
)
random_search.fit(X, y)
print(random_search.best_params_)
Why random beats grid: With grid search, important parameters get few samples (3-5). With random search, every parameter gets all 100 samples — better coverage.
Method 4: Bayesian Optimization (Optuna)
The best approach in 2026. Uses results from previous trials to pick better hyperparameters intelligently.
import optuna
import xgboost as xgb
from sklearn.model_selection import cross_val_score
def objective(trial):
params = {
'n_estimators': trial.suggest_int('n_estimators', 100, 1000),
'max_depth': trial.suggest_int('max_depth', 3, 12),
'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.3, log=True),
'subsample': trial.suggest_float('subsample', 0.5, 1.0),
'colsample_bytree': trial.suggest_float('colsample_bytree', 0.5, 1.0),
'reg_alpha': trial.suggest_float('reg_alpha', 1e-8, 10, log=True),
'reg_lambda': trial.suggest_float('reg_lambda', 1e-8, 10, log=True),
}
model = xgb.XGBClassifier(**params, random_state=42, n_jobs=-1)
score = cross_val_score(model, X, y, cv=5, scoring='roc_auc').mean()
return score
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=100)
print(f"Best score: {study.best_value:.4f}")
print(f"Best params: {study.best_params}")
Why Optuna wins:
- Smarter than random search
- Handles continuous, discrete, log-scale parameters
- Pruning: stops bad trials early
- Visualization built-in
- Distributed tuning across machines
Optuna Advanced Features
Pruning (Early Stopping for Trials)
Stop bad trials before they finish training.
import optuna
from optuna.integration import LightGBMPruningCallback
import lightgbm as lgb
def objective(trial):
params = {
'n_estimators': 1000, # High, but pruning will stop early if bad
'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.3, log=True),
'num_leaves': trial.suggest_int('num_leaves', 15, 255),
}
pruning_callback = LightGBMPruningCallback(trial, 'auc')
model = lgb.LGBMClassifier(**params)
model.fit(
X_train, y_train,
eval_set=[(X_val, y_val)],
callbacks=[pruning_callback]
)
return model.best_score_['valid_0']['auc']
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=200)
With pruning, you might evaluate 200 trial configurations in the time it took to evaluate 50 without it.
Multi-Objective Optimization
Optimize for multiple metrics at once.
def objective(trial):
params =...
model = train(params)
accuracy = evaluate_accuracy(model)
latency = measure_latency(model)
return accuracy, latency
study = optuna.create_study(directions=['maximize', 'minimize'])
study.optimize(objective, n_trials=100)
# Get Pareto front
print(study.best_trials)
Useful when you care about accuracy AND speed AND model size.
Parallel Tuning
study = optuna.create_study(
storage="sqlite:///optuna.db",
study_name="my_study",
direction='maximize'
)
# Run from multiple machines:
study.optimize(objective, n_trials=100)
All workers contribute to the same study, share learnings. Massive speedup for big projects.
What to Tune (Most Important Parameters)
XGBoost / LightGBM
Critical:
learning_rate(0.01-0.3, log scale)n_estimators(with early stopping)max_depth(3-12)
Important:
subsample(0.5-1.0)colsample_bytree(0.5-1.0)min_child_weight(1-10)
Nice to have:
reg_alpha,reg_lambda(regularization)gamma(split threshold)
Random Forest
Critical:
n_estimators(200-1000)max_depth(None or 5-20)
Important:
min_samples_leaf(1-10)max_features('sqrt' or 0.3-1.0)
Neural Networks
Critical:
learning_rate(1e-5 to 1e-2, log scale)batch_size(32-512)optimizer(Adam, AdamW, SGD)
Important:
- Network architecture (layers, width)
dropout(0.0-0.5)weight_decay(1e-5 to 1e-2, log scale)
Nice to have:
- Learning rate schedule
- Warmup steps
- Activation functions
Avoiding Hyperparameter Overfitting
You can overfit your hyperparameters to validation set.
The Right Setup
Train set → Used to train models
Validation set → Used to tune hyperparameters
Test set → Used ONCE at end to evaluate
Never touch test set during tuning. After all tuning is done, evaluate on test ONCE. That's your real performance estimate.
Use Cross-Validation
Better than single train/val split:
from sklearn.model_selection import KFold
kf = KFold(n_splits=5, shuffle=True, random_state=42)
scores = []
for train_idx, val_idx in kf.split(X):
# Train and evaluate
score = train_eval(X.iloc[train_idx], X.iloc[val_idx])
scores.append(score)
mean_score = sum(scores) / len(scores)
Cross-validation gives more reliable estimates and reduces single-split noise.
How Long to Tune?
Practical guidelines:
Quick (10-30 minutes): Random search with 50-100 trials. Often gets 80% of possible gains.
Medium (1-4 hours): Optuna with 200-500 trials. Squeeze more performance.
Long (1+ day): Optuna with 1000+ trials, multi-objective, careful evaluation. Diminishing returns past this.
Don't tune past diminishing returns. When the best score over the last 50 trials hasn't improved, stop.
# Optuna early stopping
study = optuna.create_study(direction='maximize')
study.optimize(
objective,
n_trials=500,
timeout=3600, # Max 1 hour
callbacks=[lambda study, trial: study.stop() if
study.best_value - study.trials[-50].value < 0.001 else None
]
)
Common Mistakes
1. Tuning on test set. Catastrophic mistake. Test set is sacred — touch once at the end.
2. Not using log scale for learning rate. Should explore 0.001, 0.01, 0.1 — not 0.1, 0.2, 0.3.
3. Too narrow ranges. If optimal is outside your search range, you'll never find it. Start wide.
4. Grid search with many parameters. Use random search or Bayesian instead.
5. Tuning when features are wrong. No hyperparameter saves bad features. Fix data first.
6. Tuning the wrong metric. Optimize for what matters in production, not just any metric.
7. Not setting random seeds. Tuning results need to be reproducible.
Practical Workflow
import optuna
import xgboost as xgb
from sklearn.model_selection import StratifiedKFold
# Step 1: Baseline (no tuning)
baseline = xgb.XGBClassifier()
baseline_scores = cross_val_score(baseline, X_train, y_train, cv=5)
print(f"Baseline: {baseline_scores.mean():.4f}")
# Step 2: Optuna tuning
def objective(trial):
params = {
'max_depth': trial.suggest_int('max_depth', 3, 10),
'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.3, log=True),
'n_estimators': trial.suggest_int('n_estimators', 100, 1000),
'subsample': trial.suggest_float('subsample', 0.5, 1.0),
'colsample_bytree': trial.suggest_float('colsample_bytree', 0.5, 1.0),
}
model = xgb.XGBClassifier(**params, random_state=42)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X_train, y_train, cv=cv, scoring='roc_auc')
return scores.mean()
study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=200)
print(f"Tuned: {study.best_value:.4f}")
print(f"Improvement: {study.best_value - baseline_scores.mean():.4f}")
# Step 3: Train final model
final_model = xgb.XGBClassifier(**study.best_params)
final_model.fit(X_train, y_train)
# Step 4: Evaluate on TEST set (only now)
test_score = roc_auc_score(y_test, final_model.predict_proba(X_test)[:, 1])
print(f"Test set: {test_score:.4f}")
Tools Beyond Optuna
- Hyperopt: Older Bayesian optimization, similar to Optuna
- Ray Tune: Distributed hyperparameter tuning at scale
- Weights & Biases Sweeps: Tuning with experiment tracking
- Keras Tuner: TensorFlow/Keras-specific
- GP-MinIO / Scikit-Optimize: Gaussian Process-based
Optuna is the modern default. Most others have specific niches.
FAQ
Q: How many trials should I run? A: 50-200 is the sweet spot for most problems. More if you have many parameters (10+) or compute is cheap.
Q: Grid vs Random vs Bayesian? A: Bayesian (Optuna) > Random > Grid. Use Optuna unless you have a specific reason.
Q: Can I use Optuna with Sklearn?
A: Yes, perfectly. Just call cross_val_score or similar inside the objective function.
Q: How long should one trial take? A: Aim for trials under 5 minutes. If individual trials take an hour, use pruning or smaller datasets for tuning.
Q: Does hyperparameter tuning fix overfitting? A: Sometimes. Regularization parameters (alpha, lambda, dropout) help. But proper validation matters more. See Overfitting and Underfitting Explained.
Related Articles
- XGBoost Explained
- Random Forest Explained
- Overfitting and Underfitting Explained
- Best Data Science Tools 2026
Bottom Line
Tune with Optuna. Use 100-500 trials. Tune learning rate on log scale. Never tune on test set.
The biggest wins in ML come from data quality, features, and algorithm choice. Hyperparameter tuning is the last 5-15% optimization — important for production, not the first thing to obsess about.
For quick prototypes: defaults are fine. For production: tune deliberately with proper cross-validation.
Last verified: May 2026
Follow new articles
Add SciHub101 to your feed reader to see new science and technology stories.
About the Author
SciHub101 is an independent science and technology publication founded and edited by Buğra Sözeri, a software engineer and app developer based in Türkiye. AI tools assist with drafting and research. Source documentation in the existing archive is uneven, and articles are being reviewed and corrected. The Editorial Team byline identifies editorial responsibility; it does not certify specialist or independent human review. Where we compare products, anything built by our own team is labelled as such — see the editorial guidelines and About page for how corrections and disclosures work.
Related Articles

XGBoost Explained — Why It Wins Kaggle Competitions
XGBoost explained: how gradient boosting works, why it dominates tabular ML, a full Python tutorial, hyperparameter tuning, and LightGBM/CatBoost comparison.


Reinforcement Learning Explained: How AI Learns
Reinforcement learning explained: how agents learn from rewards, how Q-learning, DQN, and PPO work, and how RL trained AlphaGo and shaped chatbots.

Random Forest Explained — The Reliable ML Algorithm 2026
Random Forest explained simply: how it works, when to use it, Python tutorial, hyperparameter tuning, and why it's still relevant in 2026 despite XGBoost.

ML Engineer Career Path 2026 — Skills, Salary, Roadmap
Machine learning engineer career path: skills, salary, hiring process, job market, vs data scientist. Timeline to first job.

Complete Machine Learning Guide 2026: Concepts to Practice
A practical machine learning hub: the ML workflow, supervised and unsupervised learning, how models learn, metrics, common mistakes, tools, and a learning path.