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

add base implementation of optimum in OptimizerInterface #76

Merged
merged 26 commits into from
Sep 16, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e8d18bd
add base implementation of optimum in OptimizerInterface
amueller Sep 14, 2020
7ec3a94
Merge branch 'main' into optimum_in_interface
amueller Sep 15, 2020
4173d07
rename OptimizerInterface to OptimizerBase
amueller Sep 15, 2020
35ed138
simplify optimum, return points
amueller Sep 15, 2020
4b97779
rename file OptimizerInterface
amueller Sep 15, 2020
619438e
fix tests
amueller Sep 15, 2020
eccfca6
test optimum in BayesianOptimizerProxy
amueller Sep 15, 2020
b51842d
add pylint disable unused argumetn
amueller Sep 15, 2020
64bcd5f
compute optimum after registering
amueller Sep 15, 2020
6e9eb1d
Update source/Mlos.Python/mlos/Optimizers/OptimizerBase.py
amueller Sep 15, 2020
4767141
Update source/Mlos.Python/mlos/Optimizers/OptimizerBase.py
amueller Sep 15, 2020
02f52bd
fix extra whitespace
amueller Sep 15, 2020
551e3aa
Merge branch 'optimum_in_interface' of github.com:amueller/MLOS into …
amueller Sep 15, 2020
d6715d5
fix OptimizerBase docstring
amueller Sep 15, 2020
4316264
throw nicer error when calling optimum before register
amueller Sep 15, 2020
cce79ba
update type hint for optimum
amueller Sep 15, 2020
3599f84
remove unused dict from typing
amueller Sep 15, 2020
5a14657
add some more tests
amueller Sep 15, 2020
45c4bb0
more verbose logging
amueller Sep 15, 2020
7882b42
typos, more prints
amueller Sep 15, 2020
e8f5069
fix fstrings
amueller Sep 15, 2020
a97fac1
more typo
amueller Sep 15, 2020
65aba5b
Merge branch 'main' into optimum_in_interface
amueller Sep 15, 2020
76f8f12
typo
amueller Sep 15, 2020
d36cd2f
indentation merge issue
amueller Sep 15, 2020
f9fa1c4
undo merge mistake
amueller Sep 15, 2020
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -428,3 +428,6 @@ website/themes
website/sphinx/_build
website/sphinx/api
website/python_api

# python code coverage
coverage
bpkroth marked this conversation as resolved.
Show resolved Hide resolved
11 changes: 4 additions & 7 deletions source/Mlos.Python/mlos/Grpc/BayesianOptimizerProxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@
from mlos.global_values import deserialize_from_bytes_string
from mlos.Grpc import OptimizerService_pb2, OptimizerService_pb2_grpc
from mlos.Logger import create_logger
from mlos.Optimizers.OptimizerInterface import OptimizerInterface
from mlos.Optimizers.OptimizerBase import OptimizerBase
from mlos.Optimizers.RegressionModels.Prediction import Prediction
from mlos.Spaces import Point


class BayesianOptimizerProxy(OptimizerInterface):
class BayesianOptimizerProxy(OptimizerBase):
""" Client to remote BayesianOptimizer.

Wraps all implementation details around communicating with the remote BayesianOptimizer.
Expand All @@ -37,7 +37,7 @@ def __init__(
logger = create_logger("BayesianOptimizerClient")
self.logger = logger

OptimizerInterface.__init__(self, optimization_problem)
OptimizerBase.__init__(self, optimization_problem)
assert optimizer_config is not None

self._grpc_channel = grpc_channel
Expand Down Expand Up @@ -98,7 +98,7 @@ def predict(self, feature_values_pandas_frame, t=None): # pylint: disable=unuse
)
prediction_response = self._optimizer_stub.Predict(prediction_request)

# To be compliant with the OptimizerInterface, we need to recover a single Prediction object and return it.
# To be compliant with the OptimizerBase, we need to recover a single Prediction object and return it.
#
objective_predictions_pb2 = prediction_response.ObjectivePredictions
assert len(objective_predictions_pb2) == 1
Expand All @@ -109,9 +109,6 @@ def predict(self, feature_values_pandas_frame, t=None): # pylint: disable=unuse
prediction.add_invalid_rows_at_missing_indices(desired_index=feature_values_pandas_frame.index)
return prediction

def optimum(self, stay_focused=False): # pylint: disable=unused-argument,no-self-use
...

def focus(self, subspace): # pylint: disable=unused-argument,no-self-use
...

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import numpy as np
import pandas as pd

from mlos.Optimizers.OptimizerInterface import OptimizerInterface
from mlos.Optimizers.OptimizerBase import OptimizerBase
from mlos.Optimizers.RegressionModels.Prediction import Prediction
from mlos.Spaces import Point
from mlos.OptimizerMonitoring.Tomograph.Heatmap import Heatmap
Expand All @@ -23,7 +23,7 @@ class ModelTomograph:

def __init__(
self,
optimizer: OptimizerInterface,
optimizer: OptimizerBase,
resolution: int = DEFAULT_RESOLUTION,
dimension_names_to_skip=None, # TODO: remove - add an adapter that always removes useless dimension names.
figure_size=(10, 10)
Expand All @@ -50,7 +50,7 @@ def __init__(
is missing some important sectors of the search space.


:param optimizer: a reference to an object implementing the OptimizerInterface.
:param optimizer: a reference to an object implementing the OptimizerBase.
:param resolution: maximum number of pixels along a dimension of each heatmap.
:param dimension_names_to_skip: dimensions not to be plotted. Remove this. Consider a solution, where mutually exclusive subgrids are plotted on
separate figures.
Expand Down
27 changes: 3 additions & 24 deletions source/Mlos.Python/mlos/Optimizers/BayesianOptimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from mlos.Spaces import CategoricalDimension, DiscreteDimension, Point, SimpleHypergrid, DefaultConfigMeta

from mlos.Optimizers.BayesianOptimizerConvergenceState import BayesianOptimizerConvergenceState
from mlos.Optimizers.OptimizerInterface import OptimizerInterface
from mlos.Optimizers.OptimizerBase import OptimizerBase
from mlos.Optimizers.OptimizationProblem import OptimizationProblem
from mlos.Optimizers.ExperimentDesigner.ExperimentDesigner import ExperimentDesigner, ExperimentDesignerConfig
from mlos.Optimizers.RegressionModels.GoodnessOfFitMetrics import DataSetType
Expand Down Expand Up @@ -43,7 +43,7 @@ class BayesianOptimizerConfig(metaclass=DefaultConfigMeta):
)


class BayesianOptimizer(OptimizerInterface):
class BayesianOptimizer(OptimizerBase):
"""Generic Bayesian Optimizer based on regresson model

Uses extra trees as surrogate model and confidence bound acquisition function by default.
Expand All @@ -69,7 +69,7 @@ def __init__(
# Let's initialize the optimizer.
#
assert len(optimization_problem.objectives) == 1, "For now this is a single-objective optimizer."
OptimizerInterface.__init__(self, optimization_problem)
OptimizerBase.__init__(self, optimization_problem)

assert optimizer_config in BayesianOptimizerConfig.CONFIG_SPACE, "Invalid config."
self.optimizer_config = optimizer_config
Expand Down Expand Up @@ -142,27 +142,6 @@ def register(self, feature_values_pandas_frame, target_values_pandas_frame):
def predict(self, feature_values_pandas_frame, t=None):
return self.surrogate_model.predict(feature_values_pandas_frame)

@trace()
def optimum(self, stay_focused=False):
if self.optimization_problem.objectives[0].minimize:
index_of_best_target = self._target_values_df.idxmin()[0]
else:
index_of_best_target = self._target_values_df.idxmax()[0]
objective_name = self.optimization_problem.objectives[0].name
best_objective_value = self._target_values_df.loc[index_of_best_target][objective_name]

param_names = [dimension.name for dimension in self.optimization_problem.parameter_space.dimensions]
params_for_best_objective = self._feature_values_df.loc[index_of_best_target]

optimal_config_and_target = {
objective_name: best_objective_value,
}

for param_name in param_names:
optimal_config_and_target[param_name] = params_for_best_objective[param_name]

return optimal_config_and_target

def focus(self, subspace):
...

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from mlos.Optimizers.RegressionModels.Prediction import Prediction
from mlos.Spaces import Point

class OptimizerInterface(ABC):
class OptimizerBase(ABC):
""" Defines the interface to all our optimizers.
amueller marked this conversation as resolved.
Show resolved Hide resolved

"""
Expand Down Expand Up @@ -56,15 +56,29 @@ def predict(self, feature_values_pandas_frame, t=None) -> Prediction:
"""
raise NotImplementedError("All subclasses must implement this method.")

@abstractmethod
def optimum(self, stay_focused=False) -> Dict: # TODO: make it return an object
amueller marked this conversation as resolved.
Show resolved Hide resolved
def optimum(self, stay_focused=False) -> Dict: # pylint: disable=unused-argument # TODO take context
""" Return the optimal value found so far along with the related parameter values.

This could be either min or max, depending on the settings.

:return:
Returns
-------
best_config_point : Point
Configuration that corresponds to the optimum objective value.
best_objective : Point
amueller marked this conversation as resolved.
Show resolved Hide resolved
Best objective value observed so far.
amueller marked this conversation as resolved.
Show resolved Hide resolved
"""
raise NotImplementedError("All subclasses must implement this method.")
features_df, objectives_df = self.get_all_observations()
amueller marked this conversation as resolved.
Show resolved Hide resolved

if self.optimization_problem.objectives[0].minimize:
index_of_best_target = objectives_df.idxmin()[0]
else:
index_of_best_target = objectives_df.idxmax()[0]
best_objective = Point.from_dataframe(objectives_df.loc[[index_of_best_target]])
best_config_point = Point.from_dataframe(features_df.loc[[index_of_best_target]])


return best_config_point, best_objective

@abstractmethod
def focus(self, subspace):
Expand Down
20 changes: 4 additions & 16 deletions source/Mlos.Python/mlos/Optimizers/SimpleBayesianOptimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from mlos.Spaces import CategoricalDimension, ContinuousDimension, Dimension, DiscreteDimension, SimpleHypergrid, Point, DefaultConfigMeta
from .OptimizationProblem import OptimizationProblem
from .OptimizerInterface import OptimizerInterface
from .OptimizerBase import OptimizerBase


class SimpleBayesianOptimizerConfig(metaclass=DefaultConfigMeta):
Expand Down Expand Up @@ -76,14 +76,14 @@ def to_dict(self):
}


class SimpleBayesianOptimizer(OptimizerInterface):
class SimpleBayesianOptimizer(OptimizerBase):
""" A toy bayesian optimizer based on Gaussian processes.

"""

def __init__(self, optimization_problem: OptimizationProblem, optimizer_config: SimpleBayesianOptimizerConfig):
assert len(optimization_problem.objectives) == 1, "This is a single-objective optimizer."
OptimizerInterface.__init__(self, optimization_problem)
OptimizerBase.__init__(self, optimization_problem)
self.minimize = self.optimization_problem.objectives[0].minimize

self._ordered_parameter_names = [
Expand Down Expand Up @@ -191,7 +191,7 @@ def suggest(self, random=False, context=None): # pylint: disable=redefined-oute
return suggested_params

def register(self, params, target_value): # pylint: disable=arguments-differ
# TODO: make this conform to the OptimizerInterface
# TODO: make this conform to the OptimizerBase

if params in self._registered_param_combos:
return
Expand Down Expand Up @@ -300,18 +300,6 @@ def estimate_local_parameter_importance(self, params, t=None):

return local_parameter_importance

def optimum(self, stay_focused=False):
amueller marked this conversation as resolved.
Show resolved Hide resolved
# TODO: add arguments to set context
self._optimizer._space._bounds = self._full_feature_space_bounds

if stay_focused and self.focused:
self._optimizer._space._bounds = self._format_parameter_bounds(self._focused_parameter_space_bounds)

optimal_config_and_target = self._optimizer.max
if self.minimize:
optimal_config_and_target['target'] = -optimal_config_and_target['target']
return optimal_config_and_target

def focus(self, subspace):
assert subspace in self.parameter_space
parameter_bounds = self._format_search_space(subspace)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,7 @@ def test_bayesian_optimizer_on_simple_2d_quadratic_function_pre_heated(self):

# Register the observation with the optimizer
bayesian_optimizer.register(input_values_df, target_values_df)

self.logger.info(f"Optimum: {bayesian_optimizer.optimum()}")
self.logger.info(f"Optimum: {bayesian_optimizer.optimum()[1]}")
amueller marked this conversation as resolved.
Show resolved Hide resolved
trace_output_path = os.path.join(self.temp_dir, "PreHeatedTrace.json")
self.logger.info(f"Writing trace to {trace_output_path}")
global_values.tracer.dump_trace_to_file(output_file_path=trace_output_path)
Expand Down Expand Up @@ -167,9 +166,9 @@ def test_bayesian_optimizer_on_simple_2d_quadratic_function_cold_start(self):

bayesian_optimizer.register(input_values_df, target_values_df)
if i > 20 and i % 20 == 0:
self.logger.info(f"[{i}/{num_guided_samples}] Optimum: {bayesian_optimizer.optimum()}")
self.logger.info(f"[{i}/{num_guided_samples}] Optimum: {bayesian_optimizer.optimum()[1]}")
amueller marked this conversation as resolved.
Show resolved Hide resolved

self.logger.info(f"Optimum: {bayesian_optimizer.optimum()}")
self.logger.info(f"Optimum: {bayesian_optimizer.optimum()[1]}")
Copy link
Contributor

Choose a reason for hiding this comment

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

[1] seems like the more common value to be asking about, maybe it should be returned first?
Also, named values/properties seems maybe better.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

having configuration, optimum makes more sense to me, and I think actually the configuration is the more interesting one, but it's not used in the tests right now because it might not have been available before?

Copy link
Contributor

Choose a reason for hiding this comment

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

We can always log both :)

Copy link
Contributor

@byte-sculptor byte-sculptor Sep 15, 2020

Choose a reason for hiding this comment

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

Also, are there any invariants we can assert about the optima in this test and in the tests below? Specifically:

  1. That it is in fact the optimum
  2. That the config belongs to the parameter space
  3. That the optimum belongs to the objective space
  4. That the optimum is monotonically decreasing like you did in one of the tests below.
  5. Anything else you can think of

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm happy to add some of these but I feel that's mostly unrelated to this PR, right? More testing is always good but I'd love to push this to the students.


def test_hierarchical_quadratic_cold_start(self):

Expand Down Expand Up @@ -207,7 +206,7 @@ def test_hierarchical_quadratic_cold_start(self):
target_values_df = pd.DataFrame({'y': [y]})
bayesian_optimizer.register(input_values_df, target_values_df)

self.logger.info(f"[{restart_num}/{num_restarts}] Optimum: {bayesian_optimizer.optimum()}")
self.logger.info(f"[{restart_num}/{num_restarts}] Optimum: {bayesian_optimizer.optimum()[1]}")

def test_hierarchical_quadratic_cold_start_random_configs(self):

Expand Down Expand Up @@ -257,7 +256,7 @@ def test_hierarchical_quadratic_cold_start_random_configs(self):
target_values_df = pd.DataFrame({'y': [y]})
bayesian_optimizer.register(input_values_df, target_values_df)

self.logger.info(f"[Restart: {restart_num}/{num_restarts}] Optimum: {bayesian_optimizer.optimum()}")
self.logger.info(f"[Restart: {restart_num}/{num_restarts}] Optimum: {bayesian_optimizer.optimum()[1]}")
except Exception as e:
has_failed = True
error_file_path = os.path.join(os.getcwd(), "temp", "test_errors.txt")
Expand Down Expand Up @@ -322,5 +321,5 @@ def run_optimization(optimizer):

for _ in range(40):
run_optimization(optimizer)
print(optimizer.optimum()['function_value'])
self.assertLessEqual(sign * optimizer.optimum()['function_value'], -5.5)
print(optimizer.optimum()[1]['function_value'])
amueller marked this conversation as resolved.
Show resolved Hide resolved
self.assertLessEqual(sign * optimizer.optimum()[1]['function_value'], -5.5)
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def test_bayesian_optimizer_on_simple_2d_quadratic_function_pre_heated(self):
# Register the observation with the optimizer
bayesian_optimizer.register(input_values_df, target_values_df)

print(bayesian_optimizer.optimum())
print(bayesian_optimizer.optimum()[1])

def test_bayesian_optimizer_on_simple_2d_quadratic_function_cold_start(self):
""" Tests the bayesian optimizer on a simple quadratic function with no prior data.
Expand Down Expand Up @@ -175,7 +175,7 @@ def test_bayesian_optimizer_on_simple_2d_quadratic_function_cold_start(self):

bayesian_optimizer.register(input_values_df, target_values_df)
if i > optimizer_config.min_samples_required_for_guided_design_of_experiments and i % 10 == 1:
print(f"[{i}/{num_iterations}] Optimum: {bayesian_optimizer.optimum()}")
print(f"[{i}/{num_iterations}] Optimum: {bayesian_optimizer.optimum()[1]}")
convergence_state = bayesian_optimizer.get_optimizer_convergence_state()
random_forest_fit_state = convergence_state.surrogate_model_fit_state
random_forest_gof_metrics = random_forest_fit_state.current_train_gof_metrics
Expand Down Expand Up @@ -245,7 +245,7 @@ def test_hierarchical_quadratic_cold_start(self):
target_values_df = pd.DataFrame({'y': [y]})
bayesian_optimizer.register(input_values_df, target_values_df)

print(f"[{restart_num}/{num_restarts}] Optimum: {bayesian_optimizer.optimum()}")
print(f"[{restart_num}/{num_restarts}] Optimum: {bayesian_optimizer.optimum()[1]}")


def test_hierarchical_quadratic_cold_start_random_configs(self):
Expand Down Expand Up @@ -296,7 +296,7 @@ def test_hierarchical_quadratic_cold_start_random_configs(self):
target_values_df = pd.DataFrame({'y': [y]})
bayesian_optimizer.register(input_values_df, target_values_df)

print(f"[Restart: {restart_num}/{num_restarts}] Optimum: {bayesian_optimizer.optimum()}")
print(f"[Restart: {restart_num}/{num_restarts}] Optimum: {bayesian_optimizer.optimum()[1]}")
except Exception as e:
has_failed = True
error_file_path = os.path.join(os.getcwd(), "temp", "test_errors.txt")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import warnings

import grpc
import numpy as np
import pandas as pd

import mlos.global_values as global_values
Expand Down Expand Up @@ -169,6 +170,7 @@ def test_optimizer_with_named_config(self):
def optimize_quadratic(self, optimizer, num_iterations):
registered_features_df = None
registered_objectives_df = None
old_optimum = np.inf
for _ in range(num_iterations):
params = optimizer.suggest()
params_dict = params.to_dict()
Expand All @@ -177,6 +179,8 @@ def optimize_quadratic(self, optimizer, num_iterations):
prediction = optimizer.predict(features_df)
prediction_df = prediction.get_dataframe()



y = quadratic(**params_dict)
amueller marked this conversation as resolved.
Show resolved Hide resolved
print(f"Params: {params}, Actual: {y}, Prediction: {str(prediction_df)}")

Expand All @@ -192,4 +196,10 @@ def optimize_quadratic(self, optimizer, num_iterations):
registered_objectives_df = objectives_df
else:
registered_objectives_df = registered_objectives_df.append(objectives_df, ignore_index=True)

best_params, optimum = optimizer.optimum()
# ensure current optimum doesn't go up
assert optimum.y <= old_optimum
byte-sculptor marked this conversation as resolved.
Show resolved Hide resolved
old_optimum = optimum.y
print(f"Best Params: {best_params}, Best Value: {optimum.y}")
return registered_features_df, registered_objectives_df