Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for grid search algorithm in Optuna Suggestion Service #2060

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Support for grid search algorithm in Optuna Suggestion Service
Signed-off-by: Yuki Iwai <yuki.iwai.tz@gmail.com>
  • Loading branch information
tenzen-y committed Dec 24, 2022
commit 339e67fe4fd753302c3383fb76be3c409b8c5a0b
2 changes: 1 addition & 1 deletion manifests/v1beta1/components/controller/katib-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ data:
"image": "docker.io/kubeflowkatib/suggestion-hyperopt:latest"
},
"grid": {
"image": "docker.io/kubeflowkatib/suggestion-chocolate:latest"
"image": "docker.io/kubeflowkatib/suggestion-optuna:latest"
},
"hyperband": {
"image": "docker.io/kubeflowkatib/suggestion-hyperband:latest"
Expand Down
30 changes: 27 additions & 3 deletions pkg/suggestion/v1beta1/internal/search_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@
# limitations under the License.

import logging
import numpy as np

from pkg.apis.manager.v1beta1.python import api_pb2 as api
from pkg.suggestion.v1beta1.internal.constant import INTEGER, DOUBLE, CATEGORICAL, DISCRETE
import pkg.suggestion.v1beta1.internal.constant as constant


logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

Expand All @@ -36,15 +37,38 @@ def convert(experiment):
search_space.goal = constant.MIN_GOAL
for p in experiment.spec.parameter_specs.parameters:
search_space.params.append(
HyperParameterSearchSpace.convertParameter(p))
HyperParameterSearchSpace.convert_parameter(p))
return search_space

@staticmethod
def convert_to_combinations(search_space):
combinations = {}

for parameter in search_space.params:
if parameter.type == INTEGER:
combinations[parameter.name] = range(int(parameter.min), int(parameter.max)+1, int(parameter.step))
elif parameter.type == DOUBLE:
if parameter.step == "" or parameter.step is None:
raise Exception(
"Param {} step is nil; For discrete search space, all parameters must include step".
format(parameter.name)
)
Comment on lines +52 to +55
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andreyvelich I updated this error message.

double_list = np.arange(float(parameter.min), float(parameter.max)+float(parameter.step),
float(parameter.step))
if double_list[-1] > float(parameter.max):
double_list = double_list[:-1]
combinations[parameter.name] = double_list
elif parameter.type == CATEGORICAL or parameter.type == DISCRETE:
combinations[parameter.name] = parameter.list

return combinations

def __str__(self):
return "HyperParameterSearchSpace(goal: {}, ".format(self.goal) + \
"params: {})".format(", ".join([element.__str__() for element in self.params]))

@staticmethod
def convertParameter(p):
def convert_parameter(p):
if p.parameter_type == api.INT:
# Default value for INT parameter step is 1
step = 1
Expand Down
5 changes: 5 additions & 0 deletions pkg/suggestion/v1beta1/optuna/base_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from pkg.suggestion.v1beta1.internal.constant import INTEGER, DOUBLE, CATEGORICAL, DISCRETE, MAX_GOAL
from pkg.suggestion.v1beta1.internal.trial import Assignment
from pkg.suggestion.v1beta1.internal.search_space import HyperParameterSearchSpace


class BaseOptunaService(object):
Expand Down Expand Up @@ -48,6 +49,10 @@ def _create_sampler(self):
elif self.algorithm_name == "random":
return optuna.samplers.RandomSampler(**self.algorithm_config)

elif self.algorithm_name == "grid":
combinations = HyperParameterSearchSpace.convert_to_combinations(self.search_space)
return optuna.samplers.GridSampler(combinations, **self.algorithm_config)

def get_suggestions(self, trials, current_request_number):
if len(trials) != 0:
self._tell(trials)
Expand Down
45 changes: 42 additions & 3 deletions pkg/suggestion/v1beta1/optuna/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
import threading
import grpc
import logging
import itertools

from pkg.apis.manager.v1beta1.python import api_pb2
from pkg.apis.manager.v1beta1.python import api_pb2_grpc
from pkg.suggestion.v1beta1.internal.constant import INTEGER, DOUBLE
from pkg.suggestion.v1beta1.internal.search_space import HyperParameterSearchSpace
from pkg.suggestion.v1beta1.internal.trial import Trial, Assignment
from pkg.suggestion.v1beta1.optuna.base_service import BaseOptunaService
Expand Down Expand Up @@ -55,7 +55,7 @@ def GetSuggestions(self, request, context):

def ValidateAlgorithmSettings(self, request, context):
is_valid, message = OptimizerConfiguration.validate_algorithm_spec(
request.experiment.spec.algorithm)
request.experiment)
if not is_valid:
context.set_code(grpc.StatusCode.INVALID_ARGUMENT)
context.set_details(message)
Expand Down Expand Up @@ -86,6 +86,9 @@ class OptimizerConfiguration(object):
"random": {
"seed": lambda x: int(x),
},
"grid": {
"seed": lambda x: int(x),
}
}

@classmethod
Expand All @@ -110,7 +113,8 @@ def convert_algorithm_spec(cls, algorithm_spec):
return algorithm_spec.algorithm_name, config

@classmethod
def validate_algorithm_spec(cls, algorithm_spec):
def validate_algorithm_spec(cls, experiment):
algorithm_spec = experiment.spec.algorithm
algorithm_name = algorithm_spec.algorithm_name
algorithm_settings = algorithm_spec.algorithm_settings

Expand All @@ -120,6 +124,10 @@ def validate_algorithm_spec(cls, algorithm_spec):
return cls._validate_cmaes_setting(algorithm_settings)
elif algorithm_name == "random":
return cls._validate_random_setting(algorithm_settings)
elif algorithm_name == "grid":
return cls._validate_grid_setting(experiment)
tenzen-y marked this conversation as resolved.
Show resolved Hide resolved
else:
return False, "unknown algorithm name {}".format(algorithm_name)

@classmethod
def _validate_tpe_setting(cls, algorithm_spec):
Expand Down Expand Up @@ -178,3 +186,34 @@ def _validate_random_setting(cls, algorithm_settings):
exception=e)

return True, ""

@classmethod
def _validate_grid_setting(cls, experiment):
algorithm_settings = experiment.spec.algorithm.algorithm_settings
search_space = HyperParameterSearchSpace.convert(experiment)

for s in algorithm_settings:
try:
if s.name == "random_state":
if not int(s.value) >= 0:
return False, ""
else:
return False, "unknown setting {} for algorithm grid".format(s.name)

except Exception as e:
return False, "failed to validate {name}({value}): {exception}".format(name=s.name, value=s.value,
exception=e)

try:
combinations = HyperParameterSearchSpace.convert_to_combinations(search_space)
num_combinations = len(list(itertools.product(*combinations.values())))
max_trial_count = experiment.spec.max_trial_count
if max_trial_count > num_combinations:
return False, "Max Trial Count: {max_trial} > all possible search combinations: {combinations}".\
format(max_trial=max_trial_count, combinations=num_combinations)
Comment on lines +211 to +213
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with optuna, Is it ok to set max_trial_count < num_combinations ? since with the documentation GridSampler looks like always trying to search all possible combinations.

Copy link
Member Author

@tenzen-y tenzen-y Dec 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's right about optuna behavior. Although optuna suggestion-service creates trials using already used parameters after all combinations used.

for _ in range(current_request_number):
optuna_trial = self.study.ask(fixed_distributions=self._get_optuna_search_space())
assignments = [Assignment(k, v) for k, v in optuna_trial.params.items()]
list_of_assignments.append(assignments)
assignments_key = self._get_assignments_key(assignments)
self.assignments_to_optuna_number[assignments_key].append(optuna_trial.number)

This does not mean the optuna suggestion service can remove duplicated suggestions in the above section since the suggestionclient faces the error in the following:

logger.Info("Getting suggestions", "endpoint", endpoint, "Number of current request parameters", currentRequestNum, "Number of response parameters", len(responseSuggestion.ParameterAssignments))
if len(responseSuggestion.ParameterAssignments) != currentRequestNum {
err := fmt.Errorf("The response contains unexpected trials")
logger.Error(err, "The response contains unexpected trials")
return err
}

Also, if users want to run experiments using all combinations, users can skip setting maxTrialCount in the Experiment.

For these reasons, it would be good to provide that validation.

@anencore94 Does that make sense?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeap I see now. Thanks for your kind reply :)


except Exception as e:
return False, "failed to validate parameters({parameters}): {exception}".\
format(parameters=search_space.params, exception=e)

return True, ""
Loading