# Grid Search **Grid Search** (also called parameter sweep) is a deterministic hyperparameter optimization method that exhaustively evaluates all combinations of parameter values from a predefined grid. While simple, it provides guaranteed coverage of the search space and is ideal for low-dimensional problems. This module provides a fast implementation with optional Rust acceleration for Cartesian product generation and parallel evaluation. --- ## Mathematical Foundations ### Problem Formulation Given an objective function $f(\theta)$ and a discrete parameter grid: $$ \Theta = \Theta_1 \times \Theta_2 \times \cdots \times \Theta_D $$ where $\Theta_i = \{\theta_{i,1}, \theta_{i,2}, \ldots, \theta_{i,n_i}\}$ is the set of candidate values for parameter $i$. **Objective:** Find the optimal parameters: $$ \theta^* = \arg\min_{\theta \in \Theta} f(\theta) $$ ### Total Evaluations The number of function evaluations grows as the **Cartesian product**: $$ |\Theta| = \prod_{i=1}^{D} n_i $$ where $n_i = |\Theta_i|$ is the number of values for parameter $i$. | Parameters | Values Each | Total Evaluations | |------------|-------------|-------------------| | 2 | 5 | 25 | | 3 | 5 | 125 | | 4 | 5 | 625 | | 5 | 5 | 3,125 | | 3 | 10 | 1,000 | | 5 | 10 | 100,000 | **Warning:** Grid search suffers from the **curse of dimensionality**. Use sparingly for $D > 4$ or when function evaluations are expensive. --- ### Algorithm ``` Algorithm: Grid Search ────────────────────── Input: objective f, parameter grid Θ = Θ₁ × Θ₂ × ... × Θ_D 1. Generate all combinations: C = Θ₁ × Θ₂ × ... × Θ_D 2. Initialize: best_score = ∞, best_params = None 3. For each θ in C: a. Evaluate: score = f(θ) b. If score < best_score: best_score = score best_params = θ 4. Return best_params, best_score ``` --- ## When to Use Grid Search ### Good Use Cases ✅ **Few parameters** (D ≤ 3–4) ✅ **Coarse exploration** before fine-tuning ✅ **Discrete parameters** (e.g., layer counts, categoricals) ✅ **Reproducibility required** (deterministic) ✅ **Parameter interactions** need full coverage ✅ **Fast objectives** (< 1 second per evaluation) ### Poor Use Cases ❌ **Many parameters** (D > 5) — exponential explosion ❌ **Continuous parameters** — wastes evaluations between grid points ❌ **Expensive objectives** — better to use adaptive methods ❌ **High-resolution search** — consider random search or DE --- ## Python API ### Basic Usage ```python from optimizr import grid_search # Objective returns a scalar score (lower is better) def objective(params): lr = params["lr"] dropout = params["dropout"] # Simulate validation loss return (lr - 0.02)**2 + (dropout - 0.1)**2 best_params, best_score = grid_search( objective_fn=objective, param_grid={ "lr": [0.005, 0.01, 0.02, 0.05, 0.1], "dropout": [0.0, 0.05, 0.1, 0.2, 0.3], }, ) print(f"Best parameters: {best_params}") print(f"Best score: {best_score:.6f}") ``` **Expected output:** ``` Best parameters: {'lr': 0.02, 'dropout': 0.1} Best score: 0.000000 ``` ### With Verbose Logging ```python best_params, best_score = grid_search( objective_fn=objective, param_grid=param_grid, verbose=True, # print progress ) ``` **Output:** ``` [1/25] lr=0.005, dropout=0.0 → score=0.0127 [2/25] lr=0.005, dropout=0.05 → score=0.0102 ... [15/25] lr=0.02, dropout=0.1 → score=0.0000 [BEST] ... ``` ### Parallel Evaluation For independent, thread-safe objectives: ```python best_params, best_score = grid_search( objective_fn=objective, param_grid=param_grid, n_jobs=4, # parallel workers ) ``` **Note:** Objective function must be thread-safe (no shared mutable state). --- ## Grid Construction Strategies ### Linear Grid Evenly spaced values: ```python param_grid = { "lr": [0.001, 0.005, 0.01, 0.05, 0.1], # linear } ``` ### Logarithmic Grid For parameters spanning orders of magnitude: ```python import numpy as np param_grid = { "lr": list(np.logspace(-4, -1, 10)), # 1e-4 to 1e-1 "weight_decay": list(np.logspace(-5, -2, 8)), } ``` ### Mixed Types ```python param_grid = { "lr": [0.001, 0.01, 0.1], # continuous "batch_size": [16, 32, 64, 128], # integer "optimizer": ["adam", "sgd", "rmsprop"], # categorical "use_bn": [True, False], # boolean } ``` --- ## Practical Example: Neural Network Tuning ```python import numpy as np from optimizr import grid_search def train_and_evaluate(params): """ Train a neural network with given hyperparameters and return validation loss. """ lr = params["lr"] dropout = params["dropout"] hidden_size = params["hidden_size"] # Simulated training (replace with real training loop) # In practice: build model, train, return val_loss # Synthetic loss surface for demonstration loss = ( (lr - 0.01)**2 / 0.001 + (dropout - 0.15)**2 / 0.01 + (hidden_size - 128)**2 / 10000 ) return loss + np.random.normal(0, 0.001) # add noise param_grid = { "lr": [0.001, 0.005, 0.01, 0.02, 0.05], "dropout": [0.0, 0.1, 0.2, 0.3], "hidden_size": [64, 128, 256], } print(f"Total combinations: {5 * 4 * 3} = 60") best_params, best_score = grid_search( objective_fn=train_and_evaluate, param_grid=param_grid, verbose=True, ) print(f"\nBest configuration:") print(f" Learning rate: {best_params['lr']}") print(f" Dropout: {best_params['dropout']}") print(f" Hidden size: {best_params['hidden_size']}") print(f" Validation loss: {best_score:.4f}") ``` **Expected output:** ``` Total combinations: 60 [1/60] lr=0.001, dropout=0.0, hidden_size=64 → score=0.1892 ... [32/60] lr=0.01, dropout=0.2, hidden_size=128 → score=0.0027 [BEST] ... Best configuration: Learning rate: 0.01 Dropout: 0.2 Hidden size: 128 Validation loss: 0.0027 ``` --- ## Visualization ### Results Heatmap ```python import matplotlib.pyplot as plt import numpy as np # Collect all results results = {} for lr in param_grid["lr"]: for dropout in param_grid["dropout"]: params = {"lr": lr, "dropout": dropout, "hidden_size": 128} results[(lr, dropout)] = train_and_evaluate(params) # Create heatmap lrs = param_grid["lr"] dropouts = param_grid["dropout"] Z = np.array([[results[(lr, d)] for d in dropouts] for lr in lrs]) plt.figure(figsize=(8, 6)) plt.imshow(Z, origin='lower', aspect='auto', cmap='viridis_r') plt.colorbar(label='Loss') plt.xticks(range(len(dropouts)), dropouts) plt.yticks(range(len(lrs)), lrs) plt.xlabel('Dropout') plt.ylabel('Learning Rate') plt.title('Grid Search: Loss Surface') plt.savefig('grid_search_heatmap.png', dpi=150) ``` ![Generated plot](../_static/auto/algorithms__grid_search/block_08_fig_01.png) **Output:** Produces a 2D heatmap showing the loss landscape across different hyperparameter combinations: - **X-axis**: Dropout rate values - **Y-axis**: Learning rate values - **Color intensity**: Loss values (darker = lower loss = better performance) This visualization helps identify optimal parameter regions and understand parameter interactions. 📓 **Real-World Examples**: See [04_real_world_applications.ipynb](https://github.com/ThotDjehuty/optimiz-r/blob/main/examples/notebooks/04_real_world_applications.ipynb) for grid search applied to: - Neural network hyperparameter tuning - Portfolio optimization - Trading strategy parameter selection --- ## Combining with Other Methods ### Coarse-to-Fine Search Use grid search to identify promising regions, then refine: ```python from optimizr import grid_search, differential_evolution # Step 1: Coarse grid search coarse_grid = { "lr": [0.001, 0.01, 0.1], "dropout": [0.0, 0.2, 0.4], } coarse_best, _ = grid_search(objective, coarse_grid) print(f"Coarse best: {coarse_best}") # Step 2: Fine grid around best fine_grid = { "lr": np.linspace( coarse_best["lr"] * 0.5, coarse_best["lr"] * 2, 10 ).tolist(), "dropout": np.linspace( max(0, coarse_best["dropout"] - 0.1), min(0.5, coarse_best["dropout"] + 0.1), 10 ).tolist(), } final_best, final_score = grid_search(objective, fine_grid) print(f"Final best: {final_best}, score: {final_score:.6f}") ``` ### Warm-Start Differential Evolution Use grid search results to seed DE: ```python from optimizr import grid_search, differential_evolution # Quick grid search for initial region best_grid, _ = grid_search(objective, coarse_grid) # Initialize DE population around grid search result bounds = [ (best_grid["lr"] * 0.1, best_grid["lr"] * 10), (max(0, best_grid["dropout"] - 0.2), min(0.5, best_grid["dropout"] + 0.2)), ] final_x, final_fx = differential_evolution( objective_fn=lambda x: objective({"lr": x[0], "dropout": x[1]}), bounds=bounds, maxiter=100, ) ``` --- ## Performance Comparison | Method | Evaluations | Coverage | Best For | |--------|-------------|----------|----------| | **Grid Search** | $\prod n_i$ (exponential) | Complete | Low-D, discrete | | Random Search | Fixed budget | Probabilistic | High-D, continuous | | Bayesian Optimization | Adaptive | Adaptive | Expensive objectives | | Differential Evolution | $N_P \times \text{gens}$ | Adaptive | Complex landscapes | ### When to Switch Methods | Scenario | Recommendation | |----------|----------------| | D ≤ 3, fast objective | Grid search | | D = 4–10, moderate objective | Random search + grid refinement | | D > 10 or expensive objective | Bayesian optimization or DE | | Noisy objective | DE or ensemble methods | --- ## Advantages & Limitations ### Advantages ✅ **Simple and deterministic** — reproducible results ✅ **Complete coverage** — won't miss global optimum in grid ✅ **No tuning required** — no algorithm-specific hyperparameters ✅ **Parallel-friendly** — each evaluation is independent ✅ **Good for discrete parameters** — natural fit ### Limitations ❌ **Exponential scaling** — infeasible for many parameters ❌ **Wasteful for continuous spaces** — evaluates between optimal values ❌ **Uniform allocation** — doesn't focus on promising regions ❌ **Resolution trade-off** — coarse grids miss optima, fine grids explode --- ## Tips ### 1. Start Coarse Begin with 3–5 values per parameter. Refine after identifying good regions. ### 2. Use Log Scales For parameters spanning orders of magnitude (learning rates, regularization): ```python lrs = np.logspace(-4, -1, 10) # 1e-4 to 0.1 ``` ### 3. Limit Dimensions Keep $D \leq 4$ for full grid search. Beyond that, use: - Random search (same budget, better coverage) - Successive halving - Bayesian optimization ### 4. Cache Expensive Computations If objective shares preprocessing: ```python # Pre-compute shared data X_train, y_train = load_and_preprocess() def objective(params): # Reuse X_train, y_train return train_model(X_train, y_train, **params) ``` ### 5. Save All Results Store every evaluation for analysis: ```python all_results = [] def logging_objective(params): score = actual_objective(params) all_results.append({"params": params, "score": score}) return score grid_search(logging_objective, param_grid) # Analyze all results afterward import pandas as pd df = pd.DataFrame(all_results) print(df.sort_values("score").head(10)) ``` --- ## Related Topics - [Differential Evolution](differential_evolution.md) – Adaptive global optimization - [MCMC](mcmc.md) – Sampling-based exploration with uncertainty - [HMM](hmm.md) – Model selection with grid search over states