From 93ccefc310862be2c61c0379e0227274fca23a90 Mon Sep 17 00:00:00 2001 From: Chinmaya Sahu Date: Fri, 1 Nov 2024 19:34:45 +0530 Subject: [PATCH 1/8] Made the hill climb algorithm and tested locally --- package/samplers/hill_climb_search/LICENSE | 21 +++ package/samplers/hill_climb_search/README.md | 110 +++++++++++ .../samplers/hill_climb_search/__init__.py | 4 + package/samplers/hill_climb_search/example.py | 16 ++ .../hill_climb_search/hill_climb_search.py | 177 ++++++++++++++++++ 5 files changed, 328 insertions(+) create mode 100644 package/samplers/hill_climb_search/LICENSE create mode 100644 package/samplers/hill_climb_search/README.md create mode 100644 package/samplers/hill_climb_search/__init__.py create mode 100644 package/samplers/hill_climb_search/example.py create mode 100644 package/samplers/hill_climb_search/hill_climb_search.py diff --git a/package/samplers/hill_climb_search/LICENSE b/package/samplers/hill_climb_search/LICENSE new file mode 100644 index 00000000..652ef72f --- /dev/null +++ b/package/samplers/hill_climb_search/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Chinmaya Sahu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/package/samplers/hill_climb_search/README.md b/package/samplers/hill_climb_search/README.md new file mode 100644 index 00000000..d4e40702 --- /dev/null +++ b/package/samplers/hill_climb_search/README.md @@ -0,0 +1,110 @@ +--- +author: Please fill in the author name here. (e.g., John Smith) +title: Please fill in the title of the feature here. (e.g., Gaussian-Process Expected Improvement Sampler) +description: Please fill in the description of the feature here. (e.g., This sampler searches for each trial based on expected improvement using Gaussian process.) +tags: [Please fill in the list of tags here. (e.g., sampler, visualization, pruner)] +optuna_versions: ['Please fill in the list of versions of Optuna in which you have confirmed the feature works, e.g., 3.6.1.'] +license: MIT License +--- + + + +Please read the [tutorial guide](https://optuna.github.io/optunahub-registry/recipes/001_first.html) to register your feature in OptunaHub. +You can find more detailed explanation of the following contents in the tutorial. +Looking at [other packages' implementations](https://github.com/optuna/optunahub-registry/tree/main/package) will also help you. + +## Abstract + +You can provide an abstract for your package here. +This section will help attract potential users to your package. + +**Example** + +This package provides a sampler based on Gaussian process-based Bayesian optimization. The sampler is highly sample-efficient, so it is suitable for computationally expensive optimization problems with a limited evaluation budget, such as hyperparameter optimization of machine learning algorithms. + +## Class or Function Names + +Please fill in the class/function names which you implement here. + +**Example** + +- GPSampler + +## Installation + +If you have additional dependencies, please fill in the installation guide here. +If no additional dependencies is required, **this section can be removed**. + +**Example** + +```shell +$ pip install scipy torch +``` + +If your package has `requirements.txt`, it will be automatically uploaded to the OptunaHub, and the package dependencies will be available to install as follows. + +```shell + pip install -r https://hub.optuna.org/{category}/{your_package_name}/requirements.txt +``` + +## Example + +Please fill in the code snippet to use the implemented feature here. + +**Example** + +```python +import optuna +import optunahub + + +def objective(trial): + x = trial.suggest_float("x", -5, 5) + return x**2 + + +sampler = optunahub.load_module(package="samplers/gp").GPSampler() +study = optuna.create_study(sampler=sampler) +study.optimize(objective, n_trials=100) +``` + +## Others + +Please fill in any other information if you have here by adding child sections (###). +If there is no additional information, **this section can be removed**. + + diff --git a/package/samplers/hill_climb_search/__init__.py b/package/samplers/hill_climb_search/__init__.py new file mode 100644 index 00000000..6b6c35bb --- /dev/null +++ b/package/samplers/hill_climb_search/__init__.py @@ -0,0 +1,4 @@ +from .hill_climb_search import HillClimbSearch + + +__all__ = ["HillClimbSearch"] diff --git a/package/samplers/hill_climb_search/example.py b/package/samplers/hill_climb_search/example.py new file mode 100644 index 00000000..420e2efa --- /dev/null +++ b/package/samplers/hill_climb_search/example.py @@ -0,0 +1,16 @@ +import optuna +import optunahub + +if __name__ == "__main__": + mod = optunahub.load_module("samplers/hill_climb_search") + + def objective(trial): + x = trial.suggest_discrete_uniform("x", -10, 10) + y = trial.suggest_discrete_uniform("y", -10, 10) + return -(x**2 + y**2) + + sampler = mod.HillClimbSearch() + study = optuna.create_study(sampler=sampler) + study.optimize(objective, n_trials=20) + + print(study.best_trial.value, study.best_trial.params) diff --git a/package/samplers/hill_climb_search/hill_climb_search.py b/package/samplers/hill_climb_search/hill_climb_search.py new file mode 100644 index 00000000..2132f4f7 --- /dev/null +++ b/package/samplers/hill_climb_search/hill_climb_search.py @@ -0,0 +1,177 @@ +from __future__ import annotations + +from typing import Any + +import numpy as np +import optuna +import optunahub + +class HillClimbSearch(optunahub.samplers.SimpleBaseSampler): + """A sampler based on the Hill Climb Local Search Algorithm dealing with discrete values. + + Args: + + + """ + + def __init__(self,search_space: dict[str, optuna.distributions.BaseDistribution] | None = None) -> None: + super().__init__(search_space) + self._remaining_points = [] + self._rng = np.random.RandomState() + self._current_point = None + self._current_point_value = None + self._current_state = "Not Initialized" + self._best_neighbor = None + self._best_neighbor_value = None + + def _generate_random_point(self, search_space): + params = {} + for param_name, param_distribution in search_space.items(): + if isinstance(param_distribution, optuna.distributions.FloatDistribution): + total_points = int((param_distribution.high - param_distribution.low) / param_distribution.step) + params[param_name] = param_distribution.low + self._rng.randint(0, total_points)*param_distribution.step + else: + raise NotImplementedError + return params + + def _remove_tried_points(self, neighbors, search_space, current_point): + final_neighbors = [] + + tried_points = [trial.params for trial in study.get_trials(deepcopy=False)] + points_to_try = self._remaining_points + + invalid_points = tried_points + points_to_try + [current_point] + + for neighbor in neighbors: + if neighbor not in invalid_points: + final_neighbors.append(neighbor) + + return final_neighbors + + def _generate_neighbors(self, current_point, search_space): + neighbors = [] + for param_name, param_distribution in search_space.items(): + if isinstance(param_distribution, optuna.distributions.FloatDistribution): + current_value = current_point[param_name] + step = param_distribution.step + + neighbor_low = max(param_distribution.low, current_value - step) + neighbor_high = min(param_distribution.high, current_value + step) + + neighbor_low_point = current_point.copy() + neighbor_low_point[param_name] = neighbor_low + neighbor_high_point = current_point.copy() + neighbor_high_point[param_name] = neighbor_high + + neighbors.append(neighbor_low_point) + neighbors.append(neighbor_high_point) + else: + raise NotImplementedError + + valid_neighbors = self._remove_tried_points(neighbors, search_space, current_point) + + return valid_neighbors + + def _get_previous_trial_value(self, study:optuna.study.Study) -> float: + if len(study.trials) > 1: + return study.trials[-2].value + else: + return None + + def sample_relative(self, study:optuna.study.Study, trial:optuna.trial.FrozenTrial, search_space: dict[str, optuna.distributions.BaseDistribution]) -> dict[str, Any]: + if search_space == {}: + return {} + + if self._current_state == "Not Initialized": + #Create the current point + starting_point = self._generate_random_point(search_space) + self._current_point = starting_point + + #Add the neighbours + neighbors = self._generate_neighbors(starting_point, search_space) + self._remaining_points.extend(neighbors) + + #Change the state to initialized + self._current_state = "Initialized" + + #Return the current point + return starting_point + + elif self._current_state == "Initialized": + #This section is only for storing the value of the current point and best neighbor point + previous_trial = study.get_trials(deepcopy=False)[-2] + if previous_trial.params == self._current_point: + #Just now the current point was evaluated + #Store the value of the current point + self._current_point_value = previous_trial.value + else: + #The neighbour was evaluated + #Store the value of the neighbour, if it improves upon the current point + neighbor_value = previous_trial.value + + if neighbor_value < self._current_point_value: + self._best_neighbor = previous_trial.params + self._best_neighbor_value = neighbor_value + + #This section is for the next point to be evaluated + if len(self._remaining_points) == 0: + #This means that all the neighbours have been processed + #Now you have to select the best neighbour + #Change the state to Neighbours Processed + self._current_state = "Neighbours Processed" + + if self._best_neighbor is not None: + #Select the best neighbour, make that the current point and add its neighbours + self._current_point = self._best_neighbor + self._current_point_value = self._best_neighbor_value + + self._best_neighbor = None + self._best_neighbor_value = None + self._remaining_points = [] #Just for clarity + + #Add the neighbours + neighbors = self._generate_neighbors(self._current_point, search_space) + self._remaining_points.extend(neighbors) + + self._current_state = "Initialized" + + return self._current_point + + else: + #If none of the neighbours are better then do a random restart + self._current_state = "Not Initialized" + restarting_point = self._generate_random_point(search_space) + self._current_point = restarting_point + + self._best_neighbor = None + self._best_neighbor_value = None + + #Add the neighbours + neighbors = self._generate_neighbors(restarting_point, search_space) + self._remaining_points.extend(neighbors) + + #Change the state to initialized + self._current_state = "Initialized" + + #Return the current point + return self._current_point + + else: + #Process as normal + current_point = self._remaining_points.pop() + return current_point + + +if __name__ == "__main__": + def objective(trial): + x = trial.suggest_float("x", -10, 10, step=1) + y = trial.suggest_float("y", -10, 10, step=1) + z = trial.suggest_float("z", -10, 10, step=1) + + return x**2+y**2+z + + sampler = HillClimbSearch() + study = optuna.create_study(sampler=sampler) + study.optimize(objective, n_trials=100) + + print(study.best_trial.value, study.best_trial.params) \ No newline at end of file From 655d8661b11d2bef4b2ff531575ead17a613308e Mon Sep 17 00:00:00 2001 From: Chinmaya Sahu Date: Fri, 1 Nov 2024 19:53:21 +0530 Subject: [PATCH 2/8] Made a few changes --- .../hill_climb_search/hill_climb_search.py | 57 +++++++++---------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/package/samplers/hill_climb_search/hill_climb_search.py b/package/samplers/hill_climb_search/hill_climb_search.py index 2132f4f7..13bf31e2 100644 --- a/package/samplers/hill_climb_search/hill_climb_search.py +++ b/package/samplers/hill_climb_search/hill_climb_search.py @@ -8,23 +8,25 @@ class HillClimbSearch(optunahub.samplers.SimpleBaseSampler): """A sampler based on the Hill Climb Local Search Algorithm dealing with discrete values. - - Args: - - """ def __init__(self,search_space: dict[str, optuna.distributions.BaseDistribution] | None = None) -> None: super().__init__(search_space) self._remaining_points = [] self._rng = np.random.RandomState() + + #This is for storing the current point whose neighbors are under analysis self._current_point = None self._current_point_value = None self._current_state = "Not Initialized" + + #This is for keeping track of the best neighbor self._best_neighbor = None self._best_neighbor_value = None def _generate_random_point(self, search_space): + """This function generates a random discrete point in the search space + """ params = {} for param_name, param_distribution in search_space.items(): if isinstance(param_distribution, optuna.distributions.FloatDistribution): @@ -34,7 +36,9 @@ def _generate_random_point(self, search_space): raise NotImplementedError return params - def _remove_tried_points(self, neighbors, search_space, current_point): + def _remove_tried_points(self, neighbors, study, current_point): + """This function removes the points that have already been tried from the list of neighbors + """ final_neighbors = [] tried_points = [trial.params for trial in study.get_trials(deepcopy=False)] @@ -48,7 +52,9 @@ def _remove_tried_points(self, neighbors, search_space, current_point): return final_neighbors - def _generate_neighbors(self, current_point, search_space): + def _generate_neighbors(self, current_point, search_space, study): + """This function generates the neighbors of the current point + """ neighbors = [] for param_name, param_distribution in search_space.items(): if isinstance(param_distribution, optuna.distributions.FloatDistribution): @@ -68,16 +74,10 @@ def _generate_neighbors(self, current_point, search_space): else: raise NotImplementedError - valid_neighbors = self._remove_tried_points(neighbors, search_space, current_point) + valid_neighbors = self._remove_tried_points(neighbors, study, current_point) return valid_neighbors - def _get_previous_trial_value(self, study:optuna.study.Study) -> float: - if len(study.trials) > 1: - return study.trials[-2].value - else: - return None - def sample_relative(self, study:optuna.study.Study, trial:optuna.trial.FrozenTrial, search_space: dict[str, optuna.distributions.BaseDistribution]) -> dict[str, Any]: if search_space == {}: return {} @@ -87,8 +87,8 @@ def sample_relative(self, study:optuna.study.Study, trial:optuna.trial.FrozenTri starting_point = self._generate_random_point(search_space) self._current_point = starting_point - #Add the neighbours - neighbors = self._generate_neighbors(starting_point, search_space) + #Add the neighbors + neighbors = self._generate_neighbors(starting_point, search_space, study) self._remaining_points.extend(neighbors) #Change the state to initialized @@ -105,8 +105,8 @@ def sample_relative(self, study:optuna.study.Study, trial:optuna.trial.FrozenTri #Store the value of the current point self._current_point_value = previous_trial.value else: - #The neighbour was evaluated - #Store the value of the neighbour, if it improves upon the current point + #The neighbor was evaluated + #Store the value of the neighbor, if it improves upon the current point neighbor_value = previous_trial.value if neighbor_value < self._current_point_value: @@ -115,22 +115,21 @@ def sample_relative(self, study:optuna.study.Study, trial:optuna.trial.FrozenTri #This section is for the next point to be evaluated if len(self._remaining_points) == 0: - #This means that all the neighbours have been processed - #Now you have to select the best neighbour - #Change the state to Neighbours Processed - self._current_state = "Neighbours Processed" + #This means that all the neighbors have been processed + #Now you have to select the best neighbor if self._best_neighbor is not None: - #Select the best neighbour, make that the current point and add its neighbours + #There was an improvement + #Select the best neighbor, make that the current point and add its neighbors self._current_point = self._best_neighbor self._current_point_value = self._best_neighbor_value self._best_neighbor = None self._best_neighbor_value = None - self._remaining_points = [] #Just for clarity + self._remaining_points = [] #Happens by virtue of the condition, but just for clarity - #Add the neighbours - neighbors = self._generate_neighbors(self._current_point, search_space) + #Add the neighbors + neighbors = self._generate_neighbors(self._current_point, search_space, study) self._remaining_points.extend(neighbors) self._current_state = "Initialized" @@ -138,7 +137,7 @@ def sample_relative(self, study:optuna.study.Study, trial:optuna.trial.FrozenTri return self._current_point else: - #If none of the neighbours are better then do a random restart + #If none of the neighbors are better then do a random restart self._current_state = "Not Initialized" restarting_point = self._generate_random_point(search_space) self._current_point = restarting_point @@ -146,8 +145,8 @@ def sample_relative(self, study:optuna.study.Study, trial:optuna.trial.FrozenTri self._best_neighbor = None self._best_neighbor_value = None - #Add the neighbours - neighbors = self._generate_neighbors(restarting_point, search_space) + #Add the neighbors + neighbors = self._generate_neighbors(restarting_point, search_space, study) self._remaining_points.extend(neighbors) #Change the state to initialized @@ -168,7 +167,7 @@ def objective(trial): y = trial.suggest_float("y", -10, 10, step=1) z = trial.suggest_float("z", -10, 10, step=1) - return x**2+y**2+z + return x**2+y**2-z sampler = HillClimbSearch() study = optuna.create_study(sampler=sampler) From 575e0cd7e69f052e4bf650795526c561d1318ee1 Mon Sep 17 00:00:00 2001 From: Chinmaya Sahu Date: Fri, 1 Nov 2024 19:53:52 +0530 Subject: [PATCH 3/8] Removed testing part --- .../hill_climb_search/hill_climb_search.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/package/samplers/hill_climb_search/hill_climb_search.py b/package/samplers/hill_climb_search/hill_climb_search.py index 13bf31e2..63ae3f18 100644 --- a/package/samplers/hill_climb_search/hill_climb_search.py +++ b/package/samplers/hill_climb_search/hill_climb_search.py @@ -158,19 +158,4 @@ def sample_relative(self, study:optuna.study.Study, trial:optuna.trial.FrozenTri else: #Process as normal current_point = self._remaining_points.pop() - return current_point - - -if __name__ == "__main__": - def objective(trial): - x = trial.suggest_float("x", -10, 10, step=1) - y = trial.suggest_float("y", -10, 10, step=1) - z = trial.suggest_float("z", -10, 10, step=1) - - return x**2+y**2-z - - sampler = HillClimbSearch() - study = optuna.create_study(sampler=sampler) - study.optimize(objective, n_trials=100) - - print(study.best_trial.value, study.best_trial.params) \ No newline at end of file + return current_point \ No newline at end of file From c406cb17872bdeb776c0a6fc2f89435b071bb2af Mon Sep 17 00:00:00 2001 From: Chinmaya Sahu Date: Fri, 1 Nov 2024 20:02:15 +0530 Subject: [PATCH 4/8] Made some changes in example --- package/samplers/hill_climb_search/README.md | 104 +++--------------- package/samplers/hill_climb_search/example.py | 2 +- 2 files changed, 15 insertions(+), 91 deletions(-) diff --git a/package/samplers/hill_climb_search/README.md b/package/samplers/hill_climb_search/README.md index d4e40702..831c6ae9 100644 --- a/package/samplers/hill_climb_search/README.md +++ b/package/samplers/hill_climb_search/README.md @@ -1,110 +1,34 @@ --- -author: Please fill in the author name here. (e.g., John Smith) -title: Please fill in the title of the feature here. (e.g., Gaussian-Process Expected Improvement Sampler) -description: Please fill in the description of the feature here. (e.g., This sampler searches for each trial based on expected improvement using Gaussian process.) -tags: [Please fill in the list of tags here. (e.g., sampler, visualization, pruner)] -optuna_versions: ['Please fill in the list of versions of Optuna in which you have confirmed the feature works, e.g., 3.6.1.'] +author: Chinmaya Sahu +title: Hill Climb Local Search Sampler +description: This sampler used the Hill Climb Algorithm to improve the searching, by selecting the best neighbors and moving in that direction. +tags: [sampler,hill climb] +optuna_versions: [4.0.0] license: MIT License --- - - -Please read the [tutorial guide](https://optuna.github.io/optunahub-registry/recipes/001_first.html) to register your feature in OptunaHub. -You can find more detailed explanation of the following contents in the tutorial. -Looking at [other packages' implementations](https://github.com/optuna/optunahub-registry/tree/main/package) will also help you. ## Abstract -You can provide an abstract for your package here. -This section will help attract potential users to your package. - -**Example** - -This package provides a sampler based on Gaussian process-based Bayesian optimization. The sampler is highly sample-efficient, so it is suitable for computationally expensive optimization problems with a limited evaluation budget, such as hyperparameter optimization of machine learning algorithms. +The **hill climbing algorithm** is an optimization technique that iteratively improves a solution by evaluating neighboring solutions in search of a local maximum or minimum. Starting with an initial guess, the algorithm examines nearby "neighbor" solutions, moving to a better neighbor if one is found. This process continues until no improvement is possible, resulting in a locally optimal solution. Hill climbing is efficient and easy to implement but can get stuck in local optima, making it suitable for simple optimization landscapes or applications with limited time constraints. Variants like random restarts and stochastic selection help overcome some limitations. ## Class or Function Names -Please fill in the class/function names which you implement here. - -**Example** - -- GPSampler - -## Installation - -If you have additional dependencies, please fill in the installation guide here. -If no additional dependencies is required, **this section can be removed**. - -**Example** - -```shell -$ pip install scipy torch -``` - -If your package has `requirements.txt`, it will be automatically uploaded to the OptunaHub, and the package dependencies will be available to install as follows. - -```shell - pip install -r https://hub.optuna.org/{category}/{your_package_name}/requirements.txt -``` +- HillClimbSearch ## Example -Please fill in the code snippet to use the implemented feature here. - -**Example** - ```python import optuna import optunahub - - + def objective(trial): - x = trial.suggest_float("x", -5, 5) - return x**2 - + x = trial.suggest_discrete_uniform("x", -10, 10) + y = trial.suggest_discrete_uniform("y", -10, 10) + return -(x**2 + y**2) -sampler = optunahub.load_module(package="samplers/gp").GPSampler() +mod = optunahub.load_module("samplers/hill_climb_search") +sampler = mod.HillClimbSearch() study = optuna.create_study(sampler=sampler) -study.optimize(objective, n_trials=100) -``` - -## Others - -Please fill in any other information if you have here by adding child sections (###). -If there is no additional information, **this section can be removed**. - - diff --git a/package/samplers/hill_climb_search/example.py b/package/samplers/hill_climb_search/example.py index 420e2efa..9d2f6a48 100644 --- a/package/samplers/hill_climb_search/example.py +++ b/package/samplers/hill_climb_search/example.py @@ -2,7 +2,7 @@ import optunahub if __name__ == "__main__": - mod = optunahub.load_module("samplers/hill_climb_search") + mod = optunahub.load_module(package="samplers/hill_climb_search") def objective(trial): x = trial.suggest_discrete_uniform("x", -10, 10) From db86207595634a0764c72218e52ef34454edb98d Mon Sep 17 00:00:00 2001 From: Chinmaya Sahu Date: Fri, 1 Nov 2024 20:05:55 +0530 Subject: [PATCH 5/8] Changed examples --- package/samplers/hill_climb_search/example.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/package/samplers/hill_climb_search/example.py b/package/samplers/hill_climb_search/example.py index 9d2f6a48..8952d301 100644 --- a/package/samplers/hill_climb_search/example.py +++ b/package/samplers/hill_climb_search/example.py @@ -1,15 +1,17 @@ import optuna import optunahub -if __name__ == "__main__": - mod = optunahub.load_module(package="samplers/hill_climb_search") - +if __name__ == "__main__": def objective(trial): x = trial.suggest_discrete_uniform("x", -10, 10) y = trial.suggest_discrete_uniform("y", -10, 10) return -(x**2 + y**2) - sampler = mod.HillClimbSearch() + module = optunahub.load_module( + package="samplers/hill-climb-search", + repo_owner="csking101", + ref="hill-climb-algorithm") + sampler = module.HillClimbSearch() study = optuna.create_study(sampler=sampler) study.optimize(objective, n_trials=20) From 3ec949fd7279aec73ef02a66abeb1d7d8f225755 Mon Sep 17 00:00:00 2001 From: Chinmaya Sahu Date: Fri, 1 Nov 2024 20:14:25 +0530 Subject: [PATCH 6/8] Pre-commit hooks --- package/samplers/hill_climb_search/README.md | 3 +- package/samplers/hill_climb_search/example.py | 11 +- .../hill_climb_search/hill_climb_search.py | 174 ++++++++++-------- 3 files changed, 104 insertions(+), 84 deletions(-) diff --git a/package/samplers/hill_climb_search/README.md b/package/samplers/hill_climb_search/README.md index 831c6ae9..dee21610 100644 --- a/package/samplers/hill_climb_search/README.md +++ b/package/samplers/hill_climb_search/README.md @@ -2,12 +2,11 @@ author: Chinmaya Sahu title: Hill Climb Local Search Sampler description: This sampler used the Hill Climb Algorithm to improve the searching, by selecting the best neighbors and moving in that direction. -tags: [sampler,hill climb] +tags: [sampler, hill climb] optuna_versions: [4.0.0] license: MIT License --- - ## Abstract The **hill climbing algorithm** is an optimization technique that iteratively improves a solution by evaluating neighboring solutions in search of a local maximum or minimum. Starting with an initial guess, the algorithm examines nearby "neighbor" solutions, moving to a better neighbor if one is found. This process continues until no improvement is possible, resulting in a locally optimal solution. Hill climbing is efficient and easy to implement but can get stuck in local optima, making it suitable for simple optimization landscapes or applications with limited time constraints. Variants like random restarts and stochastic selection help overcome some limitations. diff --git a/package/samplers/hill_climb_search/example.py b/package/samplers/hill_climb_search/example.py index 8952d301..a2c27703 100644 --- a/package/samplers/hill_climb_search/example.py +++ b/package/samplers/hill_climb_search/example.py @@ -1,16 +1,17 @@ import optuna import optunahub -if __name__ == "__main__": - def objective(trial): + +if __name__ == "__main__": + + def objective(trial: optuna.trial.Trial) -> None: x = trial.suggest_discrete_uniform("x", -10, 10) y = trial.suggest_discrete_uniform("y", -10, 10) return -(x**2 + y**2) module = optunahub.load_module( - package="samplers/hill-climb-search", - repo_owner="csking101", - ref="hill-climb-algorithm") + package="samplers/hill-climb-search", repo_owner="csking101", ref="hill-climb-algorithm" + ) sampler = module.HillClimbSearch() study = optuna.create_study(sampler=sampler) study.optimize(objective, n_trials=20) diff --git a/package/samplers/hill_climb_search/hill_climb_search.py b/package/samplers/hill_climb_search/hill_climb_search.py index 63ae3f18..0f79d301 100644 --- a/package/samplers/hill_climb_search/hill_climb_search.py +++ b/package/samplers/hill_climb_search/hill_climb_search.py @@ -6,156 +6,176 @@ import optuna import optunahub + class HillClimbSearch(optunahub.samplers.SimpleBaseSampler): - """A sampler based on the Hill Climb Local Search Algorithm dealing with discrete values. - """ + """A sampler based on the Hill Climb Local Search Algorithm dealing with discrete values.""" - def __init__(self,search_space: dict[str, optuna.distributions.BaseDistribution] | None = None) -> None: + def __init__( + self, search_space: dict[str, optuna.distributions.BaseDistribution] | None = None + ) -> None: super().__init__(search_space) - self._remaining_points = [] + self._remaining_points: list[dict] = [] self._rng = np.random.RandomState() - - #This is for storing the current point whose neighbors are under analysis - self._current_point = None + + # This is for storing the current point whose neighbors are under analysis + self._current_point: dict | None = None self._current_point_value = None self._current_state = "Not Initialized" - - #This is for keeping track of the best neighbor + + # This is for keeping track of the best neighbor self._best_neighbor = None self._best_neighbor_value = None - - def _generate_random_point(self, search_space): - """This function generates a random discrete point in the search space - """ + + def _generate_random_point( + self, search_space: dict[str, optuna.distributions.BaseDistribution] + ) -> dict: + """This function generates a random discrete point in the search space""" params = {} for param_name, param_distribution in search_space.items(): if isinstance(param_distribution, optuna.distributions.FloatDistribution): - total_points = int((param_distribution.high - param_distribution.low) / param_distribution.step) - params[param_name] = param_distribution.low + self._rng.randint(0, total_points)*param_distribution.step + total_points = int( + (param_distribution.high - param_distribution.low) / param_distribution.step + ) + params[param_name] = ( + param_distribution.low + + self._rng.randint(0, total_points) * param_distribution.step + ) else: raise NotImplementedError return params - - def _remove_tried_points(self, neighbors, study, current_point): - """This function removes the points that have already been tried from the list of neighbors - """ + + def _remove_tried_points( + self, neighbors: list[dict], study: optuna.study.Study, current_point: dict + ) -> list[dict]: + """This function removes the points that have already been tried from the list of neighbors""" final_neighbors = [] - + tried_points = [trial.params for trial in study.get_trials(deepcopy=False)] points_to_try = self._remaining_points - + invalid_points = tried_points + points_to_try + [current_point] - + for neighbor in neighbors: if neighbor not in invalid_points: final_neighbors.append(neighbor) - - return final_neighbors - - def _generate_neighbors(self, current_point, search_space, study): - """This function generates the neighbors of the current point - """ + + return final_neighbors + + def _generate_neighbors( + self, + current_point: dict, + search_space: dict[str, optuna.distributions.BaseDistribution], + study: optuna.study.Study, + ) -> list[dict]: + """This function generates the neighbors of the current point""" neighbors = [] for param_name, param_distribution in search_space.items(): if isinstance(param_distribution, optuna.distributions.FloatDistribution): current_value = current_point[param_name] step = param_distribution.step - + neighbor_low = max(param_distribution.low, current_value - step) neighbor_high = min(param_distribution.high, current_value + step) - + neighbor_low_point = current_point.copy() neighbor_low_point[param_name] = neighbor_low neighbor_high_point = current_point.copy() neighbor_high_point[param_name] = neighbor_high - + neighbors.append(neighbor_low_point) neighbors.append(neighbor_high_point) else: raise NotImplementedError - - valid_neighbors = self._remove_tried_points(neighbors, study, current_point) - + + valid_neighbors = self._remove_tried_points(neighbors, study, current_point) + return valid_neighbors - - def sample_relative(self, study:optuna.study.Study, trial:optuna.trial.FrozenTrial, search_space: dict[str, optuna.distributions.BaseDistribution]) -> dict[str, Any]: + + def sample_relative( + self, + study: optuna.study.Study, + trial: optuna.trial.FrozenTrial, + search_space: dict[str, optuna.distributions.BaseDistribution], + ) -> dict[str, Any] | None: if search_space == {}: return {} - + if self._current_state == "Not Initialized": - #Create the current point - starting_point = self._generate_random_point(search_space) + # Create the current point + starting_point = self._generate_random_point(search_space) self._current_point = starting_point - - #Add the neighbors + + # Add the neighbors neighbors = self._generate_neighbors(starting_point, search_space, study) self._remaining_points.extend(neighbors) - - #Change the state to initialized + + # Change the state to initialized self._current_state = "Initialized" - - #Return the current point + + # Return the current point return starting_point - + elif self._current_state == "Initialized": - #This section is only for storing the value of the current point and best neighbor point + # This section is only for storing the value of the current point and best neighbor point previous_trial = study.get_trials(deepcopy=False)[-2] if previous_trial.params == self._current_point: - #Just now the current point was evaluated - #Store the value of the current point + # Just now the current point was evaluated + # Store the value of the current point self._current_point_value = previous_trial.value else: - #The neighbor was evaluated - #Store the value of the neighbor, if it improves upon the current point + # The neighbor was evaluated + # Store the value of the neighbor, if it improves upon the current point neighbor_value = previous_trial.value - + if neighbor_value < self._current_point_value: self._best_neighbor = previous_trial.params self._best_neighbor_value = neighbor_value - - #This section is for the next point to be evaluated + + # This section is for the next point to be evaluated if len(self._remaining_points) == 0: - #This means that all the neighbors have been processed - #Now you have to select the best neighbor - + # This means that all the neighbors have been processed + # Now you have to select the best neighbor + if self._best_neighbor is not None: - #There was an improvement - #Select the best neighbor, make that the current point and add its neighbors + # There was an improvement + # Select the best neighbor, make that the current point and add its neighbors self._current_point = self._best_neighbor self._current_point_value = self._best_neighbor_value - + self._best_neighbor = None self._best_neighbor_value = None - self._remaining_points = [] #Happens by virtue of the condition, but just for clarity - - #Add the neighbors + self._remaining_points = [] # Happens by virtue of the condition, but just for clarity + + # Add the neighbors neighbors = self._generate_neighbors(self._current_point, search_space, study) self._remaining_points.extend(neighbors) - + self._current_state = "Initialized" - + return self._current_point - + else: - #If none of the neighbors are better then do a random restart + # If none of the neighbors are better then do a random restart self._current_state = "Not Initialized" restarting_point = self._generate_random_point(search_space) self._current_point = restarting_point - + self._best_neighbor = None self._best_neighbor_value = None - - #Add the neighbors + + # Add the neighbors neighbors = self._generate_neighbors(restarting_point, search_space, study) self._remaining_points.extend(neighbors) - - #Change the state to initialized + + # Change the state to initialized self._current_state = "Initialized" - - #Return the current point + + # Return the current point return self._current_point - + else: - #Process as normal + # Process as normal current_point = self._remaining_points.pop() - return current_point \ No newline at end of file + return current_point + + return {} From 2012e7ea53ed9670f9283f8d6e9328f1c79cb2f6 Mon Sep 17 00:00:00 2001 From: Chinmaya Sahu Date: Thu, 7 Nov 2024 16:16:45 +0530 Subject: [PATCH 7/8] Made some improvements --- package/samplers/hill_climb_search/README.md | 8 ++++---- package/samplers/hill_climb_search/__init__.py | 4 ++-- package/samplers/hill_climb_search/example.py | 10 ++++------ .../hill_climb_search/hill_climb_search.py | 14 ++++++++++---- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/package/samplers/hill_climb_search/README.md b/package/samplers/hill_climb_search/README.md index dee21610..4a9ab190 100644 --- a/package/samplers/hill_climb_search/README.md +++ b/package/samplers/hill_climb_search/README.md @@ -13,7 +13,7 @@ The **hill climbing algorithm** is an optimization technique that iteratively im ## Class or Function Names -- HillClimbSearch +- HillClimbingSampler ## Example @@ -22,12 +22,12 @@ import optuna import optunahub def objective(trial): - x = trial.suggest_discrete_uniform("x", -10, 10) - y = trial.suggest_discrete_uniform("y", -10, 10) + x = trial.suggest_int("x", -10, 10) + y = trial.suggest_int("y", -10, 10) return -(x**2 + y**2) mod = optunahub.load_module("samplers/hill_climb_search") -sampler = mod.HillClimbSearch() +sampler = mod.HillClimbingSampler() study = optuna.create_study(sampler=sampler) study.optimize(objective, n_trials=20) ``` diff --git a/package/samplers/hill_climb_search/__init__.py b/package/samplers/hill_climb_search/__init__.py index 6b6c35bb..cfd376ea 100644 --- a/package/samplers/hill_climb_search/__init__.py +++ b/package/samplers/hill_climb_search/__init__.py @@ -1,4 +1,4 @@ -from .hill_climb_search import HillClimbSearch +from .hill_climb_search import HillClimbingSampler -__all__ = ["HillClimbSearch"] +__all__ = ["HillClimbingSampler"] diff --git a/package/samplers/hill_climb_search/example.py b/package/samplers/hill_climb_search/example.py index a2c27703..0d26c2d0 100644 --- a/package/samplers/hill_climb_search/example.py +++ b/package/samplers/hill_climb_search/example.py @@ -5,14 +5,12 @@ if __name__ == "__main__": def objective(trial: optuna.trial.Trial) -> None: - x = trial.suggest_discrete_uniform("x", -10, 10) - y = trial.suggest_discrete_uniform("y", -10, 10) + x = trial.suggest_int("x", -10, 10) + y = trial.suggest_int("y", -10, 10) return -(x**2 + y**2) - module = optunahub.load_module( - package="samplers/hill-climb-search", repo_owner="csking101", ref="hill-climb-algorithm" - ) - sampler = module.HillClimbSearch() + module = optunahub.load_module(package="samplers/hill-climb-search") + sampler = module.HillClimbingSampler() study = optuna.create_study(sampler=sampler) study.optimize(objective, n_trials=20) diff --git a/package/samplers/hill_climb_search/hill_climb_search.py b/package/samplers/hill_climb_search/hill_climb_search.py index 0f79d301..24e946ed 100644 --- a/package/samplers/hill_climb_search/hill_climb_search.py +++ b/package/samplers/hill_climb_search/hill_climb_search.py @@ -7,7 +7,7 @@ import optunahub -class HillClimbSearch(optunahub.samplers.SimpleBaseSampler): +class HillClimbingSampler(optunahub.samplers.SimpleBaseSampler): """A sampler based on the Hill Climb Local Search Algorithm dealing with discrete values.""" def __init__( @@ -32,7 +32,7 @@ def _generate_random_point( """This function generates a random discrete point in the search space""" params = {} for param_name, param_distribution in search_space.items(): - if isinstance(param_distribution, optuna.distributions.FloatDistribution): + if isinstance(param_distribution, optuna.distributions.IntDistribution): total_points = int( (param_distribution.high - param_distribution.low) / param_distribution.step ) @@ -70,7 +70,7 @@ def _generate_neighbors( """This function generates the neighbors of the current point""" neighbors = [] for param_name, param_distribution in search_space.items(): - if isinstance(param_distribution, optuna.distributions.FloatDistribution): + if isinstance(param_distribution, optuna.distributions.IntDistribution): current_value = current_point[param_name] step = param_distribution.step @@ -127,7 +127,13 @@ def sample_relative( # Store the value of the neighbor, if it improves upon the current point neighbor_value = previous_trial.value - if neighbor_value < self._current_point_value: + criteria = ( + neighbor_value < self._current_point_value + if study.direction == "minimize" + else neighbor_value > self._current_point_value + ) + + if criteria: self._best_neighbor = previous_trial.params self._best_neighbor_value = neighbor_value From 37a2d490fc549d3e780a2aa4816c8728dcb9833e Mon Sep 17 00:00:00 2001 From: Yoshihiko Ozaki <30489874+y0z@users.noreply.github.com> Date: Fri, 8 Nov 2024 18:21:27 +0900 Subject: [PATCH 8/8] Update package/samplers/hill_climb_search/hill_climb_search.py --- package/samplers/hill_climb_search/hill_climb_search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/samplers/hill_climb_search/hill_climb_search.py b/package/samplers/hill_climb_search/hill_climb_search.py index 24e946ed..d2c671af 100644 --- a/package/samplers/hill_climb_search/hill_climb_search.py +++ b/package/samplers/hill_climb_search/hill_climb_search.py @@ -129,7 +129,7 @@ def sample_relative( criteria = ( neighbor_value < self._current_point_value - if study.direction == "minimize" + if study.direction == optuna.study.StudyDirection.MINIMIZE else neighbor_value > self._current_point_value )