Fenil Sonani

Hyperparameter Optimization in Machine Learning: Grid Search, Random Search, and Bayesian Methods

2 min read

AI-Generated Content Notice

Some code examples and technical explanations in this article were generated with AI assistance. The content has been reviewed for accuracy, but please test any code snippets in your development environment before using them.


Hyperparameter Optimization in Machine Learning: Grid Search, Random Search, and Bayesian Methods

Introduction

Hyperparameter optimization is the process of finding the best configuration of hyperparameters that maximizes model performance. Unlike model parameters that are learned during training, hyperparameters are set before training begins and significantly impact model behavior.

This guide covers practical hyperparameter optimization techniques from basic grid search to advanced Bayesian methods, with hands-on Python implementations to boost your model performance.

Why Hyperparameter Optimization Matters

Key Impact Areas:

  • Model Performance: Proper tuning can improve accuracy by 10-30%
  • Training Efficiency: Optimal learning rates reduce training time
  • Generalization: Right regularization prevents overfitting
  • Resource Usage: Efficient configurations save computational costs

Common hyperparameters include learning rate, regularization strength, tree depth, number of estimators, and batch size.

Grid Search

Grid Search exhaustively tests all parameter combinations in a predefined grid.

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import load_breast_cancer, make_classification
from sklearn.model_selection import (
    GridSearchCV, RandomizedSearchCV, cross_val_score,
    ParameterGrid, ParameterSampler
)
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report
from sklearn.preprocessing import StandardScaler
import time
from typing import Dict, List, Tuple, Any
import warnings
warnings.filterwarnings('ignore')

plt.style.use('seaborn-v0_8')

class HyperparameterOptimizer:
    """Comprehensive hyperparameter optimization analyzer"""
    
    def __init__(self, random_state: int = 42):
        self.random_state = random_state
        self.optimization_history = []
    
    def grid_search_analysis(self, X: np.ndarray, y: np.ndarray, 
                           model, param_grid: Dict, cv: int = 5) -> Dict:
        """Comprehensive Grid Search analysis"""
        
        print(f"Running Grid Search with {len(ParameterGrid(param_grid))} combinations...")
        
        start_time = time.time()
        
        # Perform Grid Search
        grid_search = GridSearchCV(
            model, param_grid, cv=cv, scoring='accuracy',
            n_jobs=-1, verbose=1, return_train_score=True
        )
        
        grid_search.fit(X, y)
        
        end_time = time.time()
        
        # Extract results
        results_df = pd.DataFrame(grid_search.cv_results_)
        
        optimization_result = {
            'method': 'Grid Search',
            'best_params': grid_search.best_params_,
            'best_score': grid_search.best_score_,
            'best_estimator': grid_search.best_estimator_,
            'total_time': end_time - start_time,
            'n_combinations': len(ParameterGrid(param_grid)),
            'cv_results': results_df,
            'grid_search_obj': grid_search
        }
        
        self.optimization_history.append(optimization_result)
        
        print(f"Best parameters: {grid_search.best_params_}")
        print(f"Best CV score: {grid_search.best_score_:.4f}")
        print(f"Time taken: {end_time - start_time:.2f} seconds")
        
        return optimization_result
    
    def plot_grid_search_heatmap(self, results: Dict, param1: str, param2: str):
        """Plot Grid Search results as heatmap"""
        
        if len(results['best_params']) < 2:
            print("Need at least 2 parameters for heatmap")
            return
        
        cv_results = results['cv_results']
        
        # Create pivot table for heatmap
        if f'param_{param1}' in cv_results.columns and f'param_{param2}' in cv_results.columns:
            pivot_table = cv_results.pivot_table(
                values='mean_test_score',
                index=f'param_{param1}',
                columns=f'param_{param2}',
                aggfunc='mean'
            )
            
            plt.figure(figsize=(10, 8))
            sns.heatmap(pivot_table, annot=True, fmt='.4f', cmap='viridis',
                       cbar_kws={'label': 'CV Accuracy'})
            plt.title(f'Grid Search Results: {param1} vs {param2}', fontweight='bold')
            plt.tight_layout()
            plt.show()
        else:
            print(f"Parameters {param1} or {param2} not found in results")

import pandas as pd

# Load dataset
data = load_breast_cancer()
X, y = data.data, data.target

# Standardize features
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# Initialize optimizer
optimizer = HyperparameterOptimizer()

# Grid Search for Random Forest
print("=== RANDOM FOREST GRID SEARCH ===")
rf_param_grid = {
    'n_estimators': [50, 100, 200],
    'max_depth': [3, 5, 10, None],
    'min_samples_split': [2, 5, 10],
    'min_samples_leaf': [1, 2, 4]
}

rf_model = RandomForestClassifier(random_state=42)
rf_grid_results = optimizer.grid_search_analysis(X, y, rf_model, rf_param_grid)

# Plot heatmap for Random Forest
optimizer.plot_grid_search_heatmap(rf_grid_results, 'n_estimators', 'max_depth')

Random Search

Random Search samples hyperparameter combinations randomly, often more efficient than grid search.

class RandomSearchAnalyzer:
    """Random Search optimization analyzer"""
    
    def __init__(self, random_state: int = 42):
        self.random_state = random_state
    
    def random_search_analysis(self, X: np.ndarray, y: np.ndarray,
                             model, param_distributions: Dict,
                             n_iter: int = 50, cv: int = 5) -> Dict:
        """Comprehensive Random Search analysis"""
        
        print(f"Running Random Search with {n_iter} iterations...")
        
        start_time = time.time()
        
        # Perform Random Search
        random_search = RandomizedSearchCV(
            model, param_distributions, n_iter=n_iter, cv=cv,
            scoring='accuracy', n_jobs=-1, verbose=1,
            random_state=self.random_state, return_train_score=True
        )
        
        random_search.fit(X, y)
        
        end_time = time.time()
        
        # Extract results
        results_df = pd.DataFrame(random_search.cv_results_)
        
        optimization_result = {
            'method': 'Random Search',
            'best_params': random_search.best_params_,
            'best_score': random_search.best_score_,
            'best_estimator': random_search.best_estimator_,
            'total_time': end_time - start_time,
            'n_iterations': n_iter,
            'cv_results': results_df,
            'random_search_obj': random_search
        }
        
        print(f"Best parameters: {random_search.best_params_}")
        print(f"Best CV score: {random_search.best_score_:.4f}")
        print(f"Time taken: {end_time - start_time:.2f} seconds")
        
        return optimization_result
    
    def compare_search_methods(self, X: np.ndarray, y: np.ndarray) -> Dict:
        """Compare Grid Search vs Random Search efficiency"""
        
        # Define parameter space
        param_grid = {
            'n_estimators': [50, 100, 200],
            'max_depth': [3, 5, 10, None],
            'min_samples_split': [2, 5, 10]
        }
        
        param_distributions = {
            'n_estimators': [50, 100, 150, 200, 250],
            'max_depth': [3, 5, 7, 10, None],
            'min_samples_split': [2, 5, 10, 15]
        }
        
        model = RandomForestClassifier(random_state=self.random_state)
        
        # Grid Search
        print("Running Grid Search comparison...")
        start_time = time.time()
        grid_search = GridSearchCV(model, param_grid, cv=3, scoring='accuracy', n_jobs=-1)
        grid_search.fit(X, y)
        grid_time = time.time() - start_time
        
        # Random Search (same number of iterations as grid combinations)
        n_combinations = len(ParameterGrid(param_grid))
        print(f"Running Random Search with {n_combinations} iterations...")
        start_time = time.time()
        random_search = RandomizedSearchCV(
            model, param_distributions, n_iter=n_combinations,
            cv=3, scoring='accuracy', n_jobs=-1, random_state=self.random_state
        )
        random_search.fit(X, y)
        random_time = time.time() - start_time
        
        # Random Search with fewer iterations
        n_iter_reduced = n_combinations // 2
        print(f"Running Random Search with {n_iter_reduced} iterations...")
        start_time = time.time()
        random_search_reduced = RandomizedSearchCV(
            model, param_distributions, n_iter=n_iter_reduced,
            cv=3, scoring='accuracy', n_jobs=-1, random_state=self.random_state
        )
        random_search_reduced.fit(X, y)
        random_time_reduced = time.time() - start_time
        
        comparison_results = {
            'Grid Search': {
                'best_score': grid_search.best_score_,
                'best_params': grid_search.best_params_,
                'time': grid_time,
                'n_fits': n_combinations
            },
            'Random Search (Full)': {
                'best_score': random_search.best_score_,
                'best_params': random_search.best_params_,
                'time': random_time,
                'n_fits': n_combinations
            },
            'Random Search (50%)': {
                'best_score': random_search_reduced.best_score_,
                'best_params': random_search_reduced.best_params_,
                'time': random_time_reduced,
                'n_fits': n_iter_reduced
            }
        }
        
        return comparison_results
    
    def plot_search_comparison(self, comparison_results: Dict):
        """Plot comparison between search methods"""
        
        methods = list(comparison_results.keys())
        scores = [comparison_results[method]['best_score'] for method in methods]
        times = [comparison_results[method]['time'] for method in methods]
        n_fits = [comparison_results[method]['n_fits'] for method in methods]
        
        fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(18, 6))
        
        # Plot 1: Best scores
        bars1 = ax1.bar(methods, scores, alpha=0.7, color=['blue', 'green', 'orange'])
        ax1.set_ylabel('Best CV Score', fontsize=12)
        ax1.set_title('Best Performance Comparison', fontweight='bold')
        ax1.tick_params(axis='x', rotation=45)
        ax1.grid(True, alpha=0.3)
        
        # Add value labels
        for bar, score in zip(bars1, scores):
            ax1.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.005,
                    f'{score:.4f}', ha='center', va='bottom', fontweight='bold')
        
        # Plot 2: Time comparison
        bars2 = ax2.bar(methods, times, alpha=0.7, color=['blue', 'green', 'orange'])
        ax2.set_ylabel('Time (seconds)', fontsize=12)
        ax2.set_title('Time Efficiency Comparison', fontweight='bold')
        ax2.tick_params(axis='x', rotation=45)
        ax2.grid(True, alpha=0.3)
        
        # Add value labels
        for bar, time_val in zip(bars2, times):
            ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height() + max(times)*0.02,
                    f'{time_val:.1f}s', ha='center', va='bottom', fontweight='bold')
        
        # Plot 3: Efficiency (score per second)
        efficiency = [score/time if time > 0 else 0 for score, time in zip(scores, times)]
        bars3 = ax3.bar(methods, efficiency, alpha=0.7, color=['blue', 'green', 'orange'])
        ax3.set_ylabel('Score per Second', fontsize=12)
        ax3.set_title('Search Efficiency', fontweight='bold')
        ax3.tick_params(axis='x', rotation=45)
        ax3.grid(True, alpha=0.3)
        
        # Add value labels
        for bar, eff in zip(bars3, efficiency):
            ax3.text(bar.get_x() + bar.get_width()/2, bar.get_height() + max(efficiency)*0.02,
                    f'{eff:.4f}', ha='center', va='bottom', fontweight='bold')
        
        plt.tight_layout()
        plt.show()
        
        # Print detailed comparison
        print("\n" + "="*80)
        print("SEARCH METHODS COMPARISON")
        print("="*80)
        print(f"{'Method':<20} {'Best Score':<12} {'Time (s)':<10} {'N Fits':<8} {'Efficiency':<12}")
        print("-"*80)
        
        for method in methods:
            data = comparison_results[method]
            efficiency = data['best_score'] / data['time'] if data['time'] > 0 else 0
            print(f"{method:<20} {data['best_score']:<12.4f} {data['time']:<10.1f} "
                  f"{data['n_fits']:<8} {efficiency:<12.4f}")

# Analyze Random Search
random_analyzer = RandomSearchAnalyzer()

print("\n=== RANDOM SEARCH ANALYSIS ===")
rf_param_distributions = {
    'n_estimators': [50, 100, 150, 200, 250, 300],
    'max_depth': [3, 5, 7, 10, 15, None],
    'min_samples_split': [2, 5, 10, 15],
    'min_samples_leaf': [1, 2, 4, 6]
}

rf_random_results = random_analyzer.random_search_analysis(
    X, y, rf_model, rf_param_distributions, n_iter=50
)

print("\n=== SEARCH METHODS COMPARISON ===")
comparison_results = random_analyzer.compare_search_methods(X, y)
random_analyzer.plot_search_comparison(comparison_results)

Bayesian Optimization

Bayesian Optimization uses probabilistic models to intelligently select hyperparameters.

# Note: Install with: pip install scikit-optimize
try:
    from skopt import gp_minimize, forest_minimize
    from skopt.space import Real, Integer
    from skopt.utils import use_named_args
    from skopt.plots import plot_convergence, plot_objective
    SKOPT_AVAILABLE = True
except ImportError:
    print("scikit-optimize not available. Install with: pip install scikit-optimize")
    SKOPT_AVAILABLE = False

class BayesianOptimizer:
    """Bayesian Optimization for hyperparameter tuning"""
    
    def __init__(self, random_state: int = 42):
        self.random_state = random_state
        self.optimization_results = []
    
    def bayesian_optimization_analysis(self, X: np.ndarray, y: np.ndarray) -> Dict:
        """Bayesian Optimization analysis"""
        
        if not SKOPT_AVAILABLE:
            return {"error": "scikit-optimize not available"}
        
        # Define search space
        dimensions = [
            Integer(10, 300, name='n_estimators'),
            Integer(1, 20, name='max_depth'),
            Integer(2, 20, name='min_samples_split'),
            Integer(1, 10, name='min_samples_leaf'),
            Real(0.1, 1.0, name='max_features')
        ]
        
        # Objective function
        @use_named_args(dimensions)
        def objective(**params):
            # Handle max_depth None case
            if params['max_depth'] == 20:
                params['max_depth'] = None
            
            model = RandomForestClassifier(
                random_state=self.random_state,
                **params
            )
            
            # Cross-validation score (negative because we minimize)
            scores = cross_val_score(model, X, y, cv=3, scoring='accuracy')
            return -scores.mean()  # Negative because we minimize
        
        print("Running Bayesian Optimization...")
        start_time = time.time()
        
        # Run Bayesian Optimization
        result = gp_minimize(
            func=objective,
            dimensions=dimensions,
            n_calls=50,
            random_state=self.random_state,
            acq_func='EI',  # Expected Improvement
            verbose=True
        )
        
        end_time = time.time()
        
        # Extract best parameters
        best_params = dict(zip([dim.name for dim in dimensions], result.x))
        if best_params['max_depth'] == 20:
            best_params['max_depth'] = None
        
        optimization_result = {
            'method': 'Bayesian Optimization',
            'best_params': best_params,
            'best_score': -result.fun,  # Convert back to positive
            'total_time': end_time - start_time,
            'n_calls': len(result.y_iters),
            'convergence_data': result.y_iters,
            'result_object': result
        }
        
        self.optimization_results.append(optimization_result)
        
        print(f"Best parameters: {best_params}")
        print(f"Best score: {-result.fun:.4f}")
        print(f"Time taken: {end_time - start_time:.2f} seconds")
        
        return optimization_result
    
    def plot_bayesian_convergence(self, results: Dict):
        """Plot Bayesian Optimization convergence"""
        
        if not SKOPT_AVAILABLE or 'result_object' not in results:
            print("Cannot plot convergence - scikit-optimize not available or no results")
            return
        
        result = results['result_object']
        
        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
        
        # Plot 1: Convergence
        plot_convergence(result, ax=ax1)
        ax1.set_title('Bayesian Optimization Convergence', fontweight='bold')
        
        # Plot 2: Manual convergence plot
        y_iters = results['convergence_data']
        best_so_far = []
        current_best = float('inf')
        
        for y in y_iters:
            if y < current_best:
                current_best = y
            best_so_far.append(current_best)
        
        ax2.plot(range(1, len(y_iters) + 1), [-y for y in y_iters], 
                'b.', alpha=0.6, label='Function evaluations')
        ax2.plot(range(1, len(best_so_far) + 1), [-y for y in best_so_far], 
                'r-', linewidth=2, label='Best so far')
        ax2.set_xlabel('Iteration', fontsize=12)
        ax2.set_ylabel('CV Accuracy', fontsize=12)
        ax2.set_title('Optimization Progress', fontweight='bold')
        ax2.legend()
        ax2.grid(True, alpha=0.3)
        
        plt.tight_layout()
        plt.show()

# Bayesian Optimization Analysis
if SKOPT_AVAILABLE:
    bayesian_optimizer = BayesianOptimizer()
    
    print("\n=== BAYESIAN OPTIMIZATION ===")
    bayesian_results = bayesian_optimizer.bayesian_optimization_analysis(X, y)
    bayesian_optimizer.plot_bayesian_convergence(bayesian_results)
else:
    print("Skipping Bayesian Optimization (scikit-optimize not available)")

Modern AutoML Approaches

# Simulate modern AutoML approach
class AutoMLSimulator:
    """Simplified AutoML hyperparameter optimization"""
    
    def __init__(self, random_state: int = 42):
        self.random_state = random_state
        self.models_configs = {
            'RandomForest': {
                'model': RandomForestClassifier,
                'params': {
                    'n_estimators': [50, 100, 200],
                    'max_depth': [5, 10, None],
                    'min_samples_split': [2, 5, 10]
                }
            },
            'SVM': {
                'model': SVC,
                'params': {
                    'C': [0.1, 1, 10],
                    'kernel': ['rbf', 'linear'],
                    'gamma': ['scale', 'auto']
                }
            },
            'LogisticRegression': {
                'model': LogisticRegression,
                'params': {
                    'C': [0.1, 1, 10],
                    'penalty': ['l1', 'l2'],
                    'solver': ['liblinear', 'saga']
                }
            }
        }
    
    def automl_optimization(self, X: np.ndarray, y: np.ndarray,
                          time_budget: int = 300) -> Dict:
        """Simplified AutoML optimization with time budget"""
        
        print(f"Running AutoML optimization with {time_budget}s budget...")
        
        start_time = time.time()
        results = {}
        best_overall_score = 0
        best_overall_config = None
        
        for model_name, config in self.models_configs.items():
            print(f"Optimizing {model_name}...")
            
            # Time-based early stopping
            if time.time() - start_time > time_budget:
                print(f"Time budget exceeded, stopping optimization")
                break
            
            model_class = config['model']
            param_grid = config['params']
            
            # Quick random search for each model
            model = model_class(random_state=self.random_state)
            
            try:
                # Adjust parameters for specific models
                if model_name == 'LogisticRegression':
                    model.set_params(max_iter=1000)
                
                random_search = RandomizedSearchCV(
                    model, param_grid, n_iter=10, cv=3,
                    scoring='accuracy', n_jobs=-1,
                    random_state=self.random_state
                )
                
                random_search.fit(X, y)
                
                results[model_name] = {
                    'best_score': random_search.best_score_,
                    'best_params': random_search.best_params_,
                    'best_estimator': random_search.best_estimator_
                }
                
                if random_search.best_score_ > best_overall_score:
                    best_overall_score = random_search.best_score_
                    best_overall_config = {
                        'model_name': model_name,
                        'best_params': random_search.best_params_,
                        'best_estimator': random_search.best_estimator_
                    }
                
                print(f"  {model_name}: {random_search.best_score_:.4f}")
                
            except Exception as e:
                print(f"  Error with {model_name}: {str(e)}")
                continue
        
        total_time = time.time() - start_time
        
        automl_result = {
            'method': 'AutoML Simulation',
            'total_time': total_time,
            'all_results': results,
            'best_overall_score': best_overall_score,
            'best_overall_config': best_overall_config,
            'models_tested': len(results)
        }
        
        print(f"\nBest overall model: {best_overall_config['model_name']}")
        print(f"Best overall score: {best_overall_score:.4f}")
        print(f"Total time: {total_time:.2f} seconds")
        
        return automl_result
    
    def plot_automl_results(self, results: Dict):
        """Plot AutoML results comparison"""
        
        all_results = results['all_results']
        if not all_results:
            print("No results to plot")
            return
        
        models = list(all_results.keys())
        scores = [all_results[model]['best_score'] for model in models]
        
        fig, ax = plt.subplots(figsize=(10, 6))
        
        # Color the best model differently
        colors = ['gold' if model == results['best_overall_config']['model_name'] 
                 else 'lightblue' for model in models]
        
        bars = ax.bar(models, scores, alpha=0.7, color=colors)
        ax.set_ylabel('Best CV Accuracy', fontsize=12)
        ax.set_title('AutoML Model Comparison', fontweight='bold')
        ax.grid(True, alpha=0.3)
        
        # Add value labels
        for bar, score in zip(bars, scores):
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.005,
                   f'{score:.4f}', ha='center', va='bottom', fontweight='bold')
        
        # Highlight best model
        best_model = results['best_overall_config']['model_name']
        ax.text(0.02, 0.98, f'Best Model: {best_model}', 
               transform=ax.transAxes, fontsize=12, fontweight='bold',
               bbox=dict(boxstyle='round', facecolor='gold', alpha=0.8),
               verticalalignment='top')
        
        plt.tight_layout()
        plt.show()

# AutoML Simulation
automl_simulator = AutoMLSimulator()

print("\n=== AUTOML SIMULATION ===")
automl_results = automl_simulator.automl_optimization(X_scaled, y, time_budget=120)
automl_simulator.plot_automl_results(automl_results)

Comprehensive Optimization Comparison

def comprehensive_optimization_comparison():
    """Compare all optimization methods"""
    
    # Collect all results
    all_methods = {}
    
    # Grid Search results
    if 'rf_grid_results' in globals():
        all_methods['Grid Search'] = {
            'best_score': rf_grid_results['best_score'],
            'time': rf_grid_results['total_time'],
            'method_type': 'Exhaustive'
        }
    
    # Random Search results
    if 'rf_random_results' in globals():
        all_methods['Random Search'] = {
            'best_score': rf_random_results['best_score'],
            'time': rf_random_results['total_time'],
            'method_type': 'Sampling'
        }
    
    # Bayesian Optimization results
    if SKOPT_AVAILABLE and 'bayesian_results' in globals():
        all_methods['Bayesian Optimization'] = {
            'best_score': bayesian_results['best_score'],
            'time': bayesian_results['total_time'],
            'method_type': 'Model-based'
        }
    
    # AutoML results
    if 'automl_results' in globals():
        all_methods['AutoML'] = {
            'best_score': automl_results['best_overall_score'],
            'time': automl_results['total_time'],
            'method_type': 'Automated'
        }
    
    if not all_methods:
        print("No optimization results to compare")
        return
    
    # Create comprehensive comparison plot
    fig, axes = plt.subplots(2, 2, figsize=(16, 12))
    
    methods = list(all_methods.keys())
    scores = [all_methods[method]['best_score'] for method in methods]
    times = [all_methods[method]['time'] for method in methods]
    method_types = [all_methods[method]['method_type'] for method in methods]
    
    # Color mapping for method types
    type_colors = {
        'Exhaustive': 'blue',
        'Sampling': 'green', 
        'Model-based': 'red',
        'Automated': 'orange'
    }
    colors = [type_colors[method_type] for method_type in method_types]
    
    # Plot 1: Performance comparison
    bars1 = axes[0, 0].bar(methods, scores, alpha=0.7, color=colors)
    axes[0, 0].set_ylabel('Best CV Accuracy', fontsize=12)
    axes[0, 0].set_title('Performance Comparison', fontweight='bold')
    axes[0, 0].tick_params(axis='x', rotation=45)
    axes[0, 0].grid(True, alpha=0.3)
    
    # Add value labels
    for bar, score in zip(bars1, scores):
        axes[0, 0].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.001,
                       f'{score:.4f}', ha='center', va='bottom', fontweight='bold')
    
    # Plot 2: Time comparison
    bars2 = axes[0, 1].bar(methods, times, alpha=0.7, color=colors)
    axes[0, 1].set_ylabel('Time (seconds)', fontsize=12)
    axes[0, 1].set_title('Time Efficiency', fontweight='bold')
    axes[0, 1].tick_params(axis='x', rotation=45)
    axes[0, 1].grid(True, alpha=0.3)
    axes[0, 1].set_yscale('log')
    
    # Plot 3: Efficiency scatter plot
    efficiency = [score/time if time > 0 else 0 for score, time in zip(scores, times)]
    scatter = axes[1, 0].scatter(times, scores, c=range(len(methods)), 
                                s=200, alpha=0.7, cmap='viridis')
    
    for i, method in enumerate(methods):
        axes[1, 0].annotate(method, (times[i], scores[i]), 
                           xytext=(5, 5), textcoords='offset points',
                           fontsize=10, ha='left')
    
    axes[1, 0].set_xlabel('Time (seconds)', fontsize=12)
    axes[1, 0].set_ylabel('Best CV Accuracy', fontsize=12)
    axes[1, 0].set_title('Efficiency Plot (Performance vs Time)', fontweight='bold')
    axes[1, 0].grid(True, alpha=0.3)
    axes[1, 0].set_xscale('log')
    
    # Plot 4: Method type summary
    type_summary = {}
    for method, data in all_methods.items():
        method_type = data['method_type']
        if method_type not in type_summary:
            type_summary[method_type] = {'count': 0, 'avg_score': 0, 'avg_time': 0}
        type_summary[method_type]['count'] += 1
        type_summary[method_type]['avg_score'] += data['best_score']
        type_summary[method_type]['avg_time'] += data['time']
    
    for method_type in type_summary:
        count = type_summary[method_type]['count']
        type_summary[method_type]['avg_score'] /= count
        type_summary[method_type]['avg_time'] /= count
    
    types = list(type_summary.keys())
    avg_scores = [type_summary[t]['avg_score'] for t in types]
    type_colors_list = [type_colors[t] for t in types]
    
    bars4 = axes[1, 1].bar(types, avg_scores, alpha=0.7, color=type_colors_list)
    axes[1, 1].set_ylabel('Average CV Accuracy', fontsize=12)
    axes[1, 1].set_title('Method Type Comparison', fontweight='bold')
    axes[1, 1].tick_params(axis='x', rotation=45)
    axes[1, 1].grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.show()
    
    # Print detailed comparison table
    print("\n" + "="*90)
    print("COMPREHENSIVE HYPERPARAMETER OPTIMIZATION COMPARISON")
    print("="*90)
    print(f"{'Method':<20} {'Type':<15} {'Best Score':<12} {'Time (s)':<10} {'Efficiency':<12}")
    print("-"*90)
    
    for method in methods:
        data = all_methods[method]
        efficiency = data['best_score'] / data['time'] if data['time'] > 0 else 0
        print(f"{method:<20} {data['method_type']:<15} {data['best_score']:<12.4f} "
              f"{data['time']:<10.1f} {efficiency:<12.4f}")
    
    # Recommendations
    print(f"\n{'RECOMMENDATIONS:':<20}")
    print("-" * 50)
    
    best_performance_method = max(all_methods.items(), key=lambda x: x[1]['best_score'])
    fastest_method = min(all_methods.items(), key=lambda x: x[1]['time'])
    most_efficient_method = max(all_methods.items(), 
                               key=lambda x: x[1]['best_score']/x[1]['time'] if x[1]['time'] > 0 else 0)
    
    print(f"Best Performance: {best_performance_method[0]} ({best_performance_method[1]['best_score']:.4f})")
    print(f"Fastest Method: {fastest_method[0]} ({fastest_method[1]['time']:.1f}s)")
    print(f"Most Efficient: {most_efficient_method[0]}")
    
    return all_methods

print("\n=== COMPREHENSIVE COMPARISON ===")
final_comparison = comprehensive_optimization_comparison()

Best Practices and Guidelines

Optimization Method Selection Guide

MethodBest ForProsConsWhen to Use
Grid SearchSmall parameter spacesExhaustive, reproducibleExpensive, curse of dimensionality< 4 parameters, sufficient compute
Random SearchMedium parameter spacesEfficient, good baselineNo learning from previous iterations4-10 parameters, limited time
Bayesian OptimizationExpensive evaluationsSample efficient, principledComplex setup, requires tuningExpensive models, continuous params
AutoMLQuick prototypingAutomated, tries multiple modelsLess control, black boxRapid experimentation, beginners

Key Recommendations

  1. Start with Random Search - Good balance of performance and efficiency
  2. Use Bayesian Optimization for expensive model training
  3. Grid Search only for final fine-tuning with small spaces
  4. Always use cross-validation to get reliable estimates
  5. Set time budgets to prevent endless optimization
  6. Monitor overfitting to validation set during optimization

Performance Improvement Guidelines

Typical improvements from proper hyperparameter tuning:

  • 10-30% accuracy improvement over default parameters
  • 2-5x training speed with optimal learning rates
  • Better generalization with proper regularization
  • Reduced resource usage with efficient configurations

Conclusion

Hyperparameter optimization is crucial for maximizing model performance. Key takeaways:

  • Random Search provides excellent baseline with minimal effort
  • Bayesian Optimization excels when training is expensive
  • Grid Search should be reserved for final fine-tuning
  • AutoML tools are great for rapid prototyping and comparison
  • Always validate with proper cross-validation
  • Time budgets prevent optimization from becoming endless

Choose your optimization strategy based on available compute resources, parameter space size, and model training cost.

References

  1. Bergstra, J., & Bengio, Y. (2012). Random search for hyper-parameter optimization. Journal of machine learning research.

  2. Snoek, J., Larochelle, H., & Adams, R. P. (2012). Practical bayesian optimization of machine learning algorithms. NIPS.

  3. Feurer, M., & Hutter, F. (2019). Hyperparameter optimization. Automated machine learning, 3-33.


Connect with me on LinkedIn or X to discuss hyperparameter optimization strategies!

Share this content

Reading time: 2 minutes
Progress: 0%
#Hyperparameter Optimization#Grid Search#Random Search#Bayesian Optimization#AutoML#Machine Learning#Python
Hyperparameter Optimization in Machine Learning: Grid Search, Random Search, and Bayesian Methods - Fenil Sonani