import pickle
from typing import Any, Dict, List, Optional
from ..Configs.ParamSpec import get_param_choices
from .SearchAlgorithm import SearchAlgorithm
[docs]
class GridSearch(SearchAlgorithm):
"""
Grid search algorithm that systematically tries all combinations of parameters up to a maximum number of iterations.
This implementation performs a search through a pre-generated grid of parameter
combinations. It iterates through these combinations and keeps track of the best
parameters found and their corresponding score.
Attributes:
max_iterations: The maximum number of parameter combinations to try.
param_space: Dictionary mapping parameter names to lists of their possible values.
indexed_param_space: A list of dictionaries, where each dictionary represents a unique
combination of parameter values.
curr_iteration: The current iteration number, indicating which parameter combination
is being evaluated or will be evaluated next.
best_score: The highest score achieved so far during the search.
best_params: The dictionary of parameters that achieved the best_score.
"""
[docs]
def __init__(self, max_iterations: int = 10):
"""
Initialize the GridSearch algorithm.
Args:
max_iterations: The maximum number of iterations to run.
"""
self.requested_max_iterations = max_iterations
self.max_iterations = max_iterations
self.param_space: Dict[str, List] = {}
self.curr_iteration: int = 0
self.indexed_param_space: List[Dict[str, Any]] = []
self.history: List[Dict[str, Any]] = []
self.best_params: Optional[Dict[str, Any]] = None
self.best_score: float = -float("inf")
self.reset()
@property
def current_idx(self):
"""Alias for curr_iteration for backward compatibility."""
return self.curr_iteration
@current_idx.setter
def current_idx(self, value):
self.curr_iteration = value
@property
def param_combinations(self):
"""Alias for indexed_param_space for backward compatibility."""
return self.indexed_param_space
@param_combinations.setter
def param_combinations(self, value):
self.indexed_param_space = value
[docs]
def initialize(self, param_space: Dict[str, Any]):
"""
Initialize the search algorithm with the parameter space.
Args:
param_space: Dictionary of parameter names and their possible values
"""
self.reset()
self.param_space = self._normalize_param_space(param_space)
self.indexed_param_space = self._generate_indexed_param_space()
self.max_iterations = min(
self.requested_max_iterations, len(self.indexed_param_space)
)
def _normalize_param_space(self, param_space: Dict[str, Any]) -> Dict[str, List]:
normalized = {}
for name, spec in param_space.items():
choices = get_param_choices(spec)
if choices is None:
raise ValueError(
f"GridSearch requires finite choices for parameter '{name}'"
)
normalized[name] = choices
return normalized
def _my_product(self, value_lists: List[List[Any]]) -> List[tuple[Any, ...]]:
"""
Computes the Cartesian product of a list of lists.
Example: _my_product([[1, 2], ['a', 'b']]) -> [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')]
"""
if not value_lists:
return [()]
product_tuples: List[tuple[Any, ...]] = [()]
for current_list_values in value_lists:
if not current_list_values:
return []
product_tuples = [
existing_tuple + (item,)
for existing_tuple in product_tuples
for item in current_list_values
]
return product_tuples
def _generate_indexed_param_space(self):
"""
Generates an indexed parameter space from the provided param_space.
The indexed parameter space is a list of dictionaries, where each dictionary
represents a unique combination of parameter values.
Raises:
ValueError: If param_space is None.
Returns:
List[Dict[str, Any]]: The indexed parameter space.
"""
if self.param_space is None:
raise ValueError("param_space is None")
param_names = list(self.param_space.keys())
value_lists = list(self.param_space.values())
values_combined = self._my_product(value_lists)
indexed_param_space = [{} for _ in range(len(values_combined))]
i = 0
for value_combination in values_combined:
param = {}
j = 0
for value in value_combination:
param[param_names[j]] = value
j = j + 1
indexed_param_space[i] = param
i = i + 1
return indexed_param_space
[docs]
def get_next_params(self) -> Optional[Dict[str, Any]]:
"""
Get the next set of parameters to try.
Returns:
Dictionary of parameter names and values to try, or None if finished
"""
if self.is_finished():
return None
params = self.indexed_param_space[self.curr_iteration]
self.curr_iteration = self.curr_iteration + 1
return params
[docs]
def update(self, params: Dict[str, Any], score: float):
"""
Update the search algorithm with the results of the latest trial.
Args:
params: Dictionary of parameter names and values that were tried
score: The evaluation score for the parameters
"""
self.history.append({"params": params, "score": score})
if self.best_score < score:
self.best_score = score
self.best_params = params
[docs]
def save_state(self, filepath: str):
"""
Save the current state of the search algorithm to a file.
Args:
filepath: The path to the file where the state should be saved.
"""
state = {
"param_space": self.param_space,
"requested_max_iterations": self.requested_max_iterations,
"max_iterations": self.max_iterations,
"curr_iteration": self.curr_iteration,
"indexed_param_space": self.indexed_param_space,
"history": self.history,
"best_params": self.best_params,
"best_score": self.best_score,
}
with open(filepath, "wb") as f:
pickle.dump(state, f)
[docs]
def load_state(self, filepath: str):
"""
Load the state of the search algorithm from a file.
Args:
filepath: The path to the file from which the state should be loaded.
"""
with open(filepath, "rb") as f:
state = pickle.load(f)
self.param_space = state["param_space"]
self.curr_iteration = state["curr_iteration"]
self.indexed_param_space = state["indexed_param_space"]
self.requested_max_iterations = state["requested_max_iterations"]
self.max_iterations = state["max_iterations"]
self.history = state.get("history", [])
self.best_params = state["best_params"]
self.best_score = state["best_score"]
[docs]
def get_best_params(self) -> Optional[Dict[str, Any]]:
"""
Get the best set of parameters found so far.
Returns:
Dictionary of the best parameter names and values, or None if no params yet.
"""
return self.best_params
[docs]
def get_best_score(self) -> Optional[float]:
"""
Get the best score achieved so far.
Returns:
The best score, or None if no score yet.
"""
return self.best_score
[docs]
def reset(self):
"""
Reset the search algorithm to its initial state while preserving param_space.
If param_space is set, it regenerates the combinations.
"""
# Preserve param_space if it exists
preserved_param_space = getattr(self, "param_space", {})
# Reset all state
self.param_space = {}
self.curr_iteration = 0
self.indexed_param_space = []
self.history = []
self.best_params = None
self.best_score = -float("inf")
# Restore and regenerate if param_space was set
if preserved_param_space:
self.param_space = preserved_param_space
self.indexed_param_space = self._generate_indexed_param_space()
[docs]
def is_finished(self) -> bool:
"""
Check if the search algorithm has finished its search (e.g., budget exhausted).
Returns:
True if the search is finished, False otherwise.
"""
return self.curr_iteration >= self.max_iterations