Source code for zellij.core.search_space

# @Author: Thomas Firmin <ThomasFirmin>
# @Date:   2022-05-03T15:41:48+02:00
# @Email:  thomas.firmin@univ-lille.fr
# @Project: Zellij
# @Last modified by:   tfirmin
# @Last modified time: 2022-10-07T16:58:11+02:00
# @License: CeCILL-C (http://www.cecill.info/index.fr.html)


from zellij.core.variables import Variable, ArrayVar, FloatVar, IntVar
from zellij.core.loss_func import LossFunc
from zellij.utils.distances import Distance, Mixed, Euclidean
from zellij.core.addons import SearchspaceAddon
import numpy as np
import copy
import os
from abc import ABC, abstractmethod


import logging

logger = logging.getLogger("zellij.space")


[docs]class Searchspace(ABC): """Searchspace Searchspace is an essential class for Zellij. Define your search space with this object. Attributes ---------- values : Variable Determines the decision space. See `MixedSearchspace`, `ContinuousSearchspace`, `DiscreteSearchspace` for more info. loss : LossFunc Callable of type `LossFunc`. See :ref:`lf` for more information. `loss` will be used by the :ref:`sp` object and by optimization See Also -------- LossFunc : Parent class for a loss function. """ def __init__(self, values, loss, **kwargs): """__init__(values, loss, **kwargs) Parameters ---------- values : ArrayVar Determines the decision space. See :code:`MixedSearchspace`, :code:`ContinuousSearchspace`, :code:`DiscreteSearchspace` for more info. loss : LossFunc Callable of type :code:`LossFunc`. See :ref:`lf` for more information. :code:`loss` will be used by the :ref:`sp` object and by optimization algorithms. **kwargs : dict Kwargs are the different addons one want to add to a `Variable`. Common addons are: * to_discrete : Converter * Will be called when converting to discrete is needed. * to_continuous : Converter * Will be called when converting to continuous is needed. * neighbor : Neighborhood * Will be called when a neighborhood is needed. * distance: Distance, default, Mixed * Will be called when computing a distance is needed * And other operators linked to the optimization algorithms (crossover, mutation,...) """ assert isinstance(loss, LossFunc), logger.error( f"`loss` must be a `LossFunc`, got {loss}" ) ############## # PARAMETERS # ############## self.values = values self.loss = loss self.kwargs = kwargs ############# # VARIABLES # ############# self.size = len(self.values) self._add_addons(**kwargs) def _add_addons(self, **kwargs): for k in kwargs: assert isinstance(kwargs[k], SearchspaceAddon), logger.error( f"Kwargs must be of type `SearchspaceAddon`, got {k}:{kwargs[k]}" ) if kwargs[k]: setattr(self, k, copy.copy(kwargs[k])) addon = getattr(self, k) addon.target = self else: setattr(self, k, kwargs[k]) addon = getattr(self, k) addon.target = self # Return 1 or size=n random attribute from the search space, can exclude one attribute
[docs] def random_attribute(self, size=1, replace=True, exclude=None): """random_attribute(size=1,replace=True, exclude=None) Draw random features from the search space. Parameters ---------- size : int, default=1 Select randomly <size> features. replace : boolean, default=True Select randomly <size> features with replacement if True. See numpy.random.choice exclude : Variable or list[Variable] or type or list[type] or int or list[int], default=None Exclude one or several `Variable` to be drawn. Can also exclude types.For example one can exclude all Constant type. Can also exclude variables according to their index or value. Returns ------- out : numpy.array(dtype=int) Array of index, corresponding to the selected Variable in `values`. Examples -------- >>> rand_att = sp.random_attribute(3) >>> print(rand_att) array([FloatVar(float_1, [2;12]), CatVar(cat_1, ['Hello', 87, 2.56]), FloatVar(float_1, [2;12])], dtype=object) """ if exclude: if isinstance(exclude, int): index = [exclude] elif isinstance(exclude, Variable): index = [exclude._idx] elif isinstance(exclude, type): index = [] for elem in self.values.values: if isinstance(elem, exclude): index.append(elem._idx) elif isinstance(exclude, list) or isinstance(exclude, tuple): if all(isinstance(elem, int) for elem in exclude): index = exclude elif all(isinstance(elem, Variable) for elem in exclude): index = [] for elem in exclude: index.append(elem._idx) elif all(isinstance(elem, type) for elem in exclude): index = [] for elem in self.values.values: if isinstance(elem, tuple(exclude)): index.append(elem._idx) p = np.full(self.size, 1 / (self.size - len(index))) p[index] = 0 else: p = np.full(self.size, 1 / self.size) return np.random.choice( self.values.values, size=size, replace=replace, p=p )
# Return a random point of the search space
[docs] def random_point(self, size=1): """random_point(size=1) Return a random point from the search space Parameters ---------- size : int, default=1 Draw <size> points. Returns ------- points : list[list[{int, float, str}]] List of <point>. Examples -------- >>> rand_pts = sp.random_point(3) >>> print(f"Random points: {rand_pts}") Random points: [[-3.830114043118622, 9, 'sigmoid'], ... [3.065902630698311, 3, 'sigmoid'], ... [-0.6839762230289024, 10, 'relu']] """ return self.values.random(size)
[docs] def subspace(self, low_bounds, up_bounds, **kwargs): """subspace(self, lo_bounds, up_bounds) Build a sub space according to the actual Searchspace using two vectors containing lower and upper bounds of the subspace. Can change type to Constant if necessary Parameters ---------- lo_bounds : list Lower bounds of the subspace. See `Variable` for more info. up_bounds : boolean, default=False Upper bounds of the subspace. See `Variable` for more info. Returns ------- out : Searchspace Return a subspace of the actual Searchspace. Examples -------- """ sub = self.values.subset(low_bounds, up_bounds) sp = type(self)(sub, self.loss, **kwargs, **self.kwargs) return sp
def __len__(self): return len(self.values)
[docs]class MixedSearchspace(Searchspace): """MixedSearchspace :code:`MixedSearchspace` is a search space made for HyperParameter Optimization (HPO). The decision space can be made of various `Variable` types. Attributes ---------- values : ArrayVar Determines the bounds of the search space. For `ContinuousSearchspace` the `values` must be an `ArrayVar` of `FloatVar`, `IntVar`, `CatVar`. The :ref:`sp` will then manipulate this array. loss : LossFunc Callable of type `LossFunc`. See :ref:`lf` for more information. `loss` will be used by the :ref:`sp` object and by optimization Methods ------- random_attribute(self,size=1,replace=True, exclude=None) Draw random features from the search space. Return the selected `Variable` random_point(self,size=1) Return random points from the search space subspace(self,lo_bounds,up_bounds) Build a sub space according to the actual Searchspace using two vectors containing lower and upper bounds of the subspace. See Also -------- LossFunc : Parent class for a loss function. Examples -------- >>> from zellij.core.variables import ArrayVar, IntVar, FloatVar, CatVar >>> from zellij.utils.distances import Mixed >>> from zellij.core.loss_func import Loss >>> from zellij.utils.benchmark import himmelblau >>> from zellij.core.search_space import MixedSearchspace >>> a = ArrayVar(IntVar("int_1", 0,8), >>> IntVar("int_2", 4,45), >>> FloatVar("float_1", 2,12), >>> CatVar("cat_1", ["Hello", 87, 2.56])) >>> lf = Loss()(himmelblau) >>> sp = MixedSearchspace(a,lf, distance=Mixed()) >>> p1,p2 = sp.random_point(), sp.random_point() >>> print(p1) [5, 34, 4.8808143412719485, 87] >>> print(p2) [3, 42, 2.8196595134477738, 'Hello'] """ # Initialize the search space def __init__(self, values, loss, **kwargs): """__init__(values, loss, **kwargs) Parameters ---------- values : ArrayVar Determines the bounds of the search space. For :code:`ContinuousSearchspace` the :code:`values` must be an :code:`ArrayVar` of :code:`FloatVar`, :code:`IntVar`, :code:`CatVar`. The :ref:`sp` will then manipulate this array. loss : LossFunc Callable of type :code:`LossFunc`. See :ref:`lf` for more information. :code:`loss` will be used by the :ref:`sp` object and by optimization algorithms. **kwargs : dict Kwargs are the different addons one want to add to a `Variable`. Common addons are: * to_discrete : Converter * Will be called when converting to discrete is needed. * to_continuous : Converter * Will be called when converting to continuous is needed. * neighbor : Neighborhood * Will be called when a neighborhood is needed. * distance: Distance, default, Mixed * Will be called when computing a distance is needed * And other operators linked to the optimization algorithms (crossover, mutation,...) """ ############## # ASSERTIONS # ############## assert isinstance(values, ArrayVar), logger.error( f"`values` must be be an `ArrayVar`, got {values}" ) self.distance = kwargs.pop("distance", Mixed()) super(MixedSearchspace, self).__init__(values, loss, **kwargs) assert isinstance(self.distance, Distance), logger.error( f"Kwargs `distance` must be of type `Distance`, got {self.distance}" ) self.distance.target = self self.loss.labels = [v.label for v in self.values]
[docs]class ContinuousSearchspace(Searchspace): """ContinuousSearchspace :code:`ContinuousSearchspace` is a search space made for continuous optimization. The decision space is made of `FloatVar`. Attributes ---------- values : ArrayVar Determines the bounds of the search space. For :code:`ContinuousSearchspace` the :code:`values` must be an :code:`ArrayVar` of :code:`FloatVar`. The :ref:`sp` will then manipulate this array. loss : LossFunc Callable of type :code:`LossFunc`. See :ref:`lf` for more information. :code:`loss` will be used by the :ref:`sp` object and by optimization Methods ------- random_attribute(self,size=1,replace=True, exclude=None) Draw random features from the search space. Return the selected `Variable` random_point(self,size=1) Return random points from the search space subspace(self,lo_bounds,up_bounds) Build a sub space according to the actual Searchspace using two vectors containing lower and upper bounds of the subspace. See Also -------- LossFunc : Parent class for a loss function. Examples -------- >>> from zellij.core.variables import ArrayVar, FloatVar >>> from zellij.core.loss_func import Loss >>> from zellij.utils.benchmark import himmelblau >>> from zellij.core.search_space import ContinuousSearchspace >>> lf = Loss()(himmelblau) >>> a = ArrayVar(FloatVar("float_1", 0,1), ... FloatVar("float_2", 0,1)) >>> sp = ContinuousSearchspace(a,lf) >>> p1,p2 = sp.random_point(), sp.random_point() >>> print(p1) [0.8922761649920034, 0.12709277668616326] >>> print(p2) [0.7730279148456985, 0.14715728189857524] """ # Initialize the search space def __init__(self, values, loss, **kwargs): """__init__(values, loss, **kwargs) Parameters ---------- values : ArrayVar Determines the bounds of the search space. For `ContinuousSearchspace` the `values` must be an `ArrayVar` of `FloatVar`. The :ref:`sp` will then manipulate this array. loss : LossFunc Callable of type `LossFunc`. See :ref:`lf` for more information. `loss` will be used by the :ref:`sp` object and by optimization algorithms. **kwargs : dict Kwargs are the different addons one want to add to a `Variable`. Common addons are: * to_discrete : Converter * Will be called when converting to discrete is needed. * to_continuous : Converter * Will be called when converting to continuous is needed. * neighbor : Neighborhood * Will be called when a neighborhood is needed. * distance: Distance, default, Mixed * Will be called when computing a distance is needed * And other operators linked to the optimization algorithms (crossover, mutation,...) """ super(ContinuousSearchspace, self).__init__(values, loss, **kwargs) ############## # ASSERTIONS # ############## assert isinstance(values, ArrayVar) and all( isinstance(v, FloatVar) for v in values ), logger.error( f"`values` must be be an `ArrayVar` of `FloatVar`, got {values}" ) self.distance = kwargs.pop("distance", Euclidean(self)) assert isinstance(self.distance, Distance), logger.error( f"Kwargs `distance` must be of type `Distance`, got {self.distance}" ) self.distance.target = self
[docs]class DiscreteSearchspace(Searchspace): """DiscreteSearchspace :code:`DiscreteSearchspace` is a search space made for continuous optimization. The decision space is made of :code:`IntVar`. Attributes ---------- values : ArrayVar Determines the bounds of the search space. For :code:`DiscreteSearchspace` the :code:`values` must be an :code:`ArrayVar` of :code:`IntVar`. The :ref:`sp` will then manipulate this array. loss : LossFunc Callable of type :code:`LossFunc`. See :ref:`lf` for more information. :code:`loss` will be used by the :ref:`sp` object and by optimization Methods ------- random_attribute(self,size=1,replace=True, exclude=None) Draw random features from the search space. Return the selected `Variable` random_point(self,size=1) Return random points from the search space subspace(self,lo_bounds,up_bounds) Build a sub space according to the actual Searchspace using two vectors containing lower and upper bounds of the subspace. See Also -------- LossFunc : Parent class for a loss function. Examples -------- >>> from zellij.core.variables import ArrayVar, IntVar >>> from zellij.core.loss_func import Loss >>> from zellij.utils.benchmark import himmelblau >>> from zellij.core.search_space import DiscreteSearchspace >>> a = ArrayVar(IntVar("int_1", 0,8), >>> IntVar("int_2", 4,45)) >>> lf = Loss()(himmelblau) >>> sp = DiscreteSearchspace(a,lf) >>> p1,p2 = sp.random_point(), sp.random_point() >>> print(p1) [5, 34] >>> print(p2) [3, 42] """ # Initialize the search space def __init__(self, values, loss, **kwargs): """__init__(values, loss, **kwargs) Parameters ---------- values : ArrayVar Determines the bounds of the search space. For :code:`DiscreteSearchspace` the :code:`values` must be an :code:`ArrayVar` of :code:`IntVar`. The :ref:`sp` will then manipulate this array. loss : LossFunc Callable of type :code:`LossFunc`. See :ref:`lf` for more information. :code:`loss` will be used by the :ref:`sp` object and by optimization algorithms. **kwargs : dict Kwargs are the different addons one want to add to a `Variable`. Common addons are: * to_discrete : Converter * Will be called when converting to discrete is needed. * to_continuous : Converter * Will be called when converting to continuous is needed. * neighbor : Neighborhood * Will be called when a neighborhood is needed. * distance: Distance, default, Mixed * Will be called when computing a distance is needed * And other operators linked to the optimization algorithms (crossover, mutation,...) """ super(DiscreteSearchspace, self).__init__(values, loss, **kwargs) ############## # ASSERTIONS # ############## assert isinstance(values, ArrayVar) and all( isinstance(v, IntVar) for v in values ), logger.error( f"`values` must be be an `ArrayVar` of `FloatVar`, got {values}" ) self.distance = kwargs.pop("distance", Euclidean(self)) assert isinstance(self.distance, Distance), logger.error( f"Kwargs `distance` must be of type `Distance`, got {self.distance}" ) self.distance.target = self