cce31055c1
Add scripts/inject_doc_plots.py that scans every .md and .rst page under docs/source/, executes each Python code-block in an isolated namespace with a non-interactive matplotlib backend, captures every figure produced, and inserts an inline image directive immediately after the code-block. Markers AUTO-PLOT-BEGIN/END make the injection idempotent on re-runs. Blocks that fail to execute or produce no figure are left untouched. Add a transparent __getattr__ fallback in python/optimizr/__init__.py that forwards any unresolved top-level attribute to the compiled _core extension. This lets all v1.x and v2.0 doc samples that use 'from optimizr import X' (estimate_ou_params_py, linear_bsde_constant_coeffs, mmd_gaussian, ...) execute as written. Augment the OU Parameter Estimation example (docs/source/algorithms/optimal_control.md) with a two-panel visualization (simulated path plus empirical/theoretical autocorrelation). Net effect: 14 doc pages now display matplotlib plots inline directly under the code that produced them -- including the OU page, point processes, Grid Search, HMM, MCMC, plus the 8 v2.0 RST pages.
500 lines
12 KiB
Markdown
500 lines
12 KiB
Markdown
# 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)
|
||
```
|
||
<!-- AUTO-PLOT-BEGIN -->
|
||

|
||
<!-- AUTO-PLOT-END -->
|
||
|
||
|
||
|
||
|
||
|
||
**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
|