Skip to content

Commit

Permalink
Adding initial version of eventified Bayesian optimization system (#32)
Browse files Browse the repository at this point in the history
* merging bo code with lava v5 updates

* removing standard pseudo-random generators to conform with flake8

* adding __init__.py files in some test directories

* updating tutorial to conform with latest updates

Co-authored-by: GaboFGuerra <[email protected]>
  • Loading branch information
shaymeister and GaboFGuerra authored Oct 19, 2022
1 parent 3e63bc7 commit 85e522c
Show file tree
Hide file tree
Showing 14 changed files with 2,150 additions and 0 deletions.
78 changes: 78 additions & 0 deletions src/lava/lib/optimization/problems/bayesian/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Copyright (C) 2022 Intel Corporation
# SPDX-License-Identifier: LGPL-2.1-or-later
# See: https://spdx.org/licenses/

import math
import numpy as np

from lava.magma.core.decorator import implements, requires, tag
from lava.magma.core.model.py.model import PyLoihiProcessModel
from lava.magma.core.model.py.ports import PyInPort, PyOutPort
from lava.magma.core.model.py.type import LavaPyType
from lava.magma.core.resources import CPU
from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol

from lava.lib.optimization.problems.bayesian.processes import (
DualInputFunction,
SingleInputFunction
)


@implements(proc=SingleInputFunction, protocol=LoihiProtocol)
@requires(CPU)
@tag('floating_pt')
class PySingleInputFunctionModel(PyLoihiProcessModel):
"""
A Python-based implementation of the SingleInput process that represents a
single input/output non-linear objective function.
"""

x_in: PyInPort = LavaPyType(PyInPort.VEC_DENSE, np.float64)
y_out: PyOutPort = LavaPyType(PyOutPort.VEC_DENSE, np.float64)

num_params = LavaPyType(int, int)
num_objectives = LavaPyType(int, int)

def run_spk(self) -> None:
"""tick the model forward by one time-step"""
x = self.x_in.recv()
y = math.cos(x) * math.sin(x) + (x * x / 25)

output_length: int = self.num_params + self.num_objectives
output = np.ndarray(
shape=(output_length, 1),
buffer=np.array([x, y])
)

self.y_out.send(output)


@implements(proc=DualInputFunction, protocol=LoihiProtocol)
@requires(CPU)
@tag('floating_pt')
class PyDualInputFunctionModel(PyLoihiProcessModel):
"""
A Python-based implementation of the DualInputFunction process that
represents a dual continuous input, single output, non-linear objective
function.
"""

x_in: PyInPort = LavaPyType(PyInPort.VEC_DENSE, np.float64)
y_out: PyOutPort = LavaPyType(PyOutPort.VEC_DENSE, np.float64)

num_params = LavaPyType(int, int)
num_objectives = LavaPyType(int, int)

def run_spk(self) -> None:
"""tick the model forward by one time-step"""

x = self.x_in.recv()
y = math.sin(x[1] * x[0]) + (0.2 * x[0]) ** 2 + math.cos(x[1])

output_length: int = self.num_objectives + self.num_params
output = np.ndarray(
shape=(output_length, 1),
buffer=np.array([x[0], x[1], y])
)

self.y_out.send(output)
58 changes: 58 additions & 0 deletions src/lava/lib/optimization/problems/bayesian/processes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Copyright (C) 2022 Intel Corporation
# SPDX-License-Identifier: LGPL-2.1-or-later
# See: https://spdx.org/licenses/

from lava.magma.core.process.ports.ports import InPort, OutPort
from lava.magma.core.process.process import AbstractProcess
from lava.magma.core.process.variable import Var


class BaseObjectiveFunction(AbstractProcess):
"""
A base objective function process that shall be used as the basis of
all black-box processes.
"""

def __init__(self, num_params: int, num_objectives: int,
**kwargs) -> None:
"""initialize the BaseObjectiveFunction
Parameters
----------
num_params : int
an integer specifying the number of parameters within the
search space
num_objectives : int
an integer specifying the number of qualitative attributes
used to measure the black-box function
"""
super().__init__(**kwargs)

# Internal State Variables
self.num_params = Var((1,), init=num_params)
self.num_objectives = Var((1,), init=num_objectives)

# Input/Output Ports
self.x_in = InPort((num_params, 1))
self.y_out = OutPort(((num_params + num_objectives), 1))


class SingleInputFunction(BaseObjectiveFunction):
"""
An abstract process representing a single input/output test function.
"""

def __init__(self, **kwargs) -> None:
"""Initialize the process with the associated parameters"""
super().__init__(num_params=1, num_objectives=1, **kwargs)


class DualInputFunction(BaseObjectiveFunction):
"""
An abstract process representing a dual input, single output
test function.
"""

def __init__(self, **kwargs) -> None:
"""Initialize the process with the associated parameters"""
super().__init__(num_params=2, num_objectives=1, **kwargs)
Empty file.
153 changes: 153 additions & 0 deletions src/lava/lib/optimization/solvers/bayesian/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Copyright (C) 2022 Intel Corporation
# SPDX-License-Identifier: LGPL-2.1-or-later
# See: https://spdx.org/licenses/

import numpy as np
from scipy.optimize import OptimizeResult
from skopt import Optimizer, Space
from skopt.space import Categorical, Integer, Real
from typing import Union

from lava.magma.core.decorator import implements, requires, tag
from lava.magma.core.model.py.model import PyLoihiProcessModel
from lava.magma.core.model.py.ports import PyInPort, PyOutPort
from lava.magma.core.model.py.type import LavaPyType
from lava.magma.core.resources import CPU
from lava.magma.core.sync.protocols.loihi_protocol import LoihiProtocol

from lava.lib.optimization.solvers.bayesian.processes import (
BayesianOptimizer
)


@implements(proc=BayesianOptimizer, protocol=LoihiProtocol)
@requires(CPU)
@tag('floating_pt')
class PyBayesianOptimizerModel(PyLoihiProcessModel):
"""
A Python-based implementation of the Bayesian Optimizer processes. For
more information, please refer to bayesian/processes.py.
"""
results_in: PyInPort = LavaPyType(PyInPort.VEC_DENSE, np.float64)
next_point_out: PyOutPort = LavaPyType(PyOutPort.VEC_DENSE, np.float64)

acq_func_config = LavaPyType(np.ndarray, np.ndarray)
acq_opt_config = LavaPyType(np.ndarray, np.ndarray)
search_space = LavaPyType(np.ndarray, np.ndarray)
est_config = LavaPyType(np.ndarray, np.ndarray)
ip_gen_config = LavaPyType(np.ndarray, np.ndarray)
num_ips = LavaPyType(int, int)
num_objectives = LavaPyType(int, int)
seed = LavaPyType(int, int)

initialized = LavaPyType(bool, bool)
num_iterations = LavaPyType(int, int)
results_log = LavaPyType(np.ndarray, np.ndarray)

def run_spk(self) -> None:
"""tick the model forward by one time-step"""

if self.initialized:
# receive a result vector from the black-box function
result_vec: np.ndarray = self.results_in.recv()

opt_result: OptimizeResult = self.process_result_vector(
result_vec
)
self.results_log[0].append(opt_result)
else:
# initialize the search space from the standard Bayesian
# optimization search space schema; for more information,
# please refer to the init_search_space method
self.search_space = self.init_search_space()
self.optimizer = Optimizer(
dimensions=self.search_space,
base_estimator=self.est_config[0],
n_initial_points=self.num_ips,
initial_point_generator=self.ip_gen_config[0],
acq_func=self.acq_func_config[0],
acq_optimizer=self.acq_opt_config[0],
random_state=self.seed
)
self.results_log[0]: list[OptimizeResult] = []
self.initialized: bool = True
self.num_iterations: int = -1

next_point: list = self.optimizer.ask()
next_point: np.ndarray = np.ndarray(
shape=(len(self.search_space), 1),
buffer=np.array(next_point)
)

self.next_point_out.send(next_point)
self.num_iterations += 1

def __del__(self) -> None:
"""finalize the optimization processing upon runtime conclusion"""

if hasattr(self, "results_log") and len(self.results_log) > 0:
print(self.results_log[-1])

def init_search_space(self) -> list:
"""initialize the search space from the standard schema
This method is designed to convert the numpy ndarray-based search
space description int scikit-optimize format compatible with all
lower-level processes. Your search space should consist of three
types of parameters:
1) ("continuous", <min_value>, <max_value>, np.nan, <name>)
2) ("integer", <min_value>, <max_value>, np.nan, <name>)
3) ("categorical", np.nan, np.nan, <choices>, <name>)
Returns
-------
search_space : list[Union[Real, Integer]]
a collection of continuous and discrete dimensions that represent
the entirety of the problem search space
"""
search_space: list[Union[Real, Integer]] = []

for i in range(self.search_space.shape[0]):
p_type: str = self.search_space[i, 0]
minimum: Union[int, float] = self.search_space[i, 1]
maximum: Union[int, float] = self.search_space[i, 2]
choices: list = self.search_space[i, 3]
name: str = self.search_space[i, 4]

factory_function: dict = {
"continuous": (lambda: Real(minimum, maximum, name=name)),
"integer": (lambda: Integer(minimum, maximum, name=name)),
"categorical": (lambda: Categorical(choices, name=name))
}

if p_type not in factory_function.keys():
raise ValueError(
f"parameter type [{p_type}] is not in valid "
+ f"parameter types: {factory_function.keys()}"
)

dimension_lambda = factory_function[p_type]
dimension = dimension_lambda()
search_space.append(dimension)

if not len(search_space) > 0:
raise ValueError("search space is empty")

return search_space

def process_result_vector(self, vec: np.ndarray) -> None:
"""parse vec into params/objectives before informing optimizer
Parameters
----------
vec : np.ndarray
a single array of data from the black-box process containing
all parameters and objectives for a total length of num_params
+ num_objectives
"""
vec: list = vec[:, 0].tolist()

evaluated_point: list = vec[:-self.num_objectives]
performance: list = vec[-self.num_objectives:]

return self.optimizer.tell(evaluated_point, performance[0])
Loading

0 comments on commit 85e522c

Please sign in to comment.