From 17e7b454e136f4941c7c17f3466c47fd189e95a3 Mon Sep 17 00:00:00 2001 From: Brian Delhaisse Date: Fri, 19 Apr 2019 17:43:52 +0200 Subject: [PATCH] refactor/update optimizers (ongoing) --- pyrobolearn/optimizers/bound.py | 54 ++++++++ pyrobolearn/optimizers/cma_optimizer.py | 130 +++++++++++++++++- pyrobolearn/optimizers/constraint.py | 58 ++++++++ pyrobolearn/optimizers/gpyopt_optimizer.py | 117 +++++++++++++++- pyrobolearn/optimizers/ipopt_optimizer.py | 42 ++++-- pyrobolearn/optimizers/nlopt_optimizer.py | 92 +++++++++---- pyrobolearn/optimizers/objective.py | 54 ++++++++ .../optimizers/optimization_problem.py | 115 ++++++++++++++++ pyrobolearn/optimizers/optimizer.py | 73 ++++++++-- pyrobolearn/optimizers/qpsolvers_optimizer.py | 9 +- pyrobolearn/optimizers/scipy_optimizer.py | 55 +++++--- 11 files changed, 725 insertions(+), 74 deletions(-) create mode 100644 pyrobolearn/optimizers/bound.py create mode 100644 pyrobolearn/optimizers/constraint.py create mode 100644 pyrobolearn/optimizers/objective.py create mode 100644 pyrobolearn/optimizers/optimization_problem.py diff --git a/pyrobolearn/optimizers/bound.py b/pyrobolearn/optimizers/bound.py new file mode 100644 index 0000000..f6fac26 --- /dev/null +++ b/pyrobolearn/optimizers/bound.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +r"""Define the bounds in an optimization problem. + +A constrained optimization problem is generally given by: + +.. math:: \min_{x \in R^n} f(x) + +subject to + +.. math:: + + g_L \leq g(x) \leq g_U + x_L \leq x \leq x_U + +where :math:`f` is the objective function, :math:`x` are the variables that are being optimized, :math:`(x_L, x_U)` +are the lower and upper bound on these variables, :math:`g` is a constraint function that maps the variables :math:`x` +to another space, and :math:`(g_L, g_U)` are the lower and upper bound in that space. + +References: + [1] https://nlopt.readthedocs.io/en/latest/ +""" + +import torch +import numpy as np + + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2018, PyRoboLearn" +__credits__ = ["Brian Delhaisse"] +__license__ = "MIT" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class Bound(object): + r"""Lower and upper bounds + + """ + + def __init__(self, lower, upper): + """ + Initialize the bounds. + + Args: + lower (np.array, torch.Tensor, float, int): lower bound + upper (np.array, torch.Tensor, float, int): upper bound + """ + self._lower = lower + self._upper = upper + + def __call__(self, variables): + return self._lower <= variables <= self._upper diff --git a/pyrobolearn/optimizers/cma_optimizer.py b/pyrobolearn/optimizers/cma_optimizer.py index d64e0cc..a5fbd61 100644 --- a/pyrobolearn/optimizers/cma_optimizer.py +++ b/pyrobolearn/optimizers/cma_optimizer.py @@ -7,6 +7,7 @@ References: """ import numpy as np +import torch # CMA-ES try: @@ -14,7 +15,8 @@ try: except ImportError as e: raise ImportError(e.__str__() + "\n HINT: you can install CMA-ES or `pycma` directly via 'pip install cma'.") -from optimizer import Optimizer +from pyrobolearn.optimizers import Optimizer + __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" @@ -26,6 +28,126 @@ __email__ = "briandelhaisse@gmail.com" __status__ = "Development" -# TODO: check the `pyrobolearn/algos/cmaes` algo -class CMAES(object): - pass +class CMAES(Optimizer): + r"""Covariance Matrix Adaptation Evolution Strategy (CMA-ES) + + Type: population-based (genetic), stochastic and derivative-free, exploration in parameter space, optimization + for non-linear and non-convex functions, episode-based. + + 'The Covariance Matrix Adaptation Evolution Strategy (CMA-ES) is a stochastic derivative-free numerical + optimization algorithm for difficult (non-convex, ill-conditioned, multi-modal, rugged, noisy) optimization + problems in continuous search spaces.' [3] + + References: + [1] "Completely Derandomized Self-Adaptation in Evolution Strategies", Hansen et al., 2001 + [2] "The CMA Evolution Strategy: A Tutorial", Hansen, 2016 + [3] "Python implementation of CMA-ES", Hansen et al., 2019: https://github.com/CMA-ES/pycma + [4] pycma API documentation: cma.gforge.inria.fr/apidocs-pycma + + Python Implementations: + - pycma: https://github.com/CMA-ES/pycma + """ + + def __init__(self, population_size=20, sigma=0.5, *args, **kwargs): + """ + Initialize the CMA-ES optimizer. + """ + super(CMAES, self).__init__(*args, **kwargs) + self.population_size = population_size + self.sigma = sigma + self.shape = None + self.dtype = None + + ############## + # Properties # + ############## + + @property + def population_size(self): + """Return the population size.""" + return self._population_size + + @population_size.setter + def population_size(self, size): + """Set the population size.""" + # check argument + if not isinstance(size, int): + raise TypeError("Expecting the population size to be an integer.") + if size < 1: + raise ValueError("Expecting the population size to be an integer bigger than 0.") + + # set population size + self._population_size = size + + ########### + # Methods # + ########### + + def convert_from(self, parameters): + if isinstance(parameters, np.ndarray): + self.shape = parameters.shape + self.dtype = np.ndarray + return parameters.reshape(-1) + if isinstance(parameters, torch.Tensor): + self.shape = tuple(parameters.shape) + self.dtype = torch.Tensor + if parameters.requires_grad: + return parameters.detach().numpy() + return parameters.numpy() + + def convert_to(self, parameters): + if isinstance(parameters, np.ndarray): + if self.dtype == np.ndarray: + return parameters.reshape(self.shape) + if self.dtype == torch.Tensor: + return torch.from_numpy(parameters.reshape(self.shape)) + + def optimize(self, parameters, loss, bounds=None, max_iters=1, seed=None, options={}, verbose=False, + *args, **kwargs): + """ + Optimize the given loss function with respect to the given parameters. + + Args: + parameters (np.array): parameters to optimize. + loss (callable): callable objective / loss function to minimize. + bounds (tuple, list, np.array): parameter bounds. E.g. bounds=[0, np.inf] + max_iters (int): number of maximum iterations. + verbose (bool): if True, it will display information during the optimization process. + *args: list of arguments to give to the loss function if callable. + **kwargs: dictionary of arguments to give to the loss function if callable. + + Returns: + float, torch.Tensor, np.array: loss scalar value. + object: best parameters + """ + # check loss function + if not callable(loss): + raise TypeError("Expecting the given loss function to be callable.") + + # check optimizer options + opts = {'popsize': self.population_size} + if bounds is not None: # set the parameter bounds + opts['bounds'] = bounds + if seed is not None: # set the seed + opts['seed'] = seed + if max_iters is not None: # set the maximum number of iterations + opts['maxiter'] = max_iters + + # update the rest of options + opts.update(options) + + # create CMA-ES + self.optimizer = cma.CMAEvolutionStrategy(parameters, sigma0=self.sigma, inopts=opts) + + # optimize + parameters = self.optimizer.ask() + values = [loss(params, *args, **kwargs) for params in parameters] + self.optimizer.tell(parameters, values) + + # save the best result and parameters + self.best_parameters = self.optimizer.result[0] + self.best_result = self.optimizer.result[1] + + return self.best_result, self.best_parameters + + diff --git a/pyrobolearn/optimizers/constraint.py b/pyrobolearn/optimizers/constraint.py new file mode 100644 index 0000000..9544224 --- /dev/null +++ b/pyrobolearn/optimizers/constraint.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +r"""Define the constraints in an optimization problem. + +A constrained optimization problem is generally given by: + +.. math:: \min_{x \in R^n} f(x) + +subject to + +.. math:: + + g_L \leq g(x) \leq g_U + x_L \leq x \leq x_U + +where :math:`f` is the objective function, :math:`x` are the variables that are being optimized, :math:`(x_L, x_U)` +are the lower and upper bound on these variables, :math:`g` is a constraint function that maps the variables :math:`x` +to another space, and :math:`(g_L, g_U)` are the lower and upper bound in that space. + +References: + [1] https://nlopt.readthedocs.io/en/latest/ +""" + +import torch +import numpy as np + + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2018, PyRoboLearn" +__credits__ = ["Brian Delhaisse"] +__license__ = "MIT" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class Constraint(object): + r"""(Inequality) Constraint + + """ + + def __init__(self, constraint, lower, upper): + """ + Initialize the bounds. + + Args: + constraint (callable): constraint function. + lower (np.array, torch.Tensor, float, int): lower bound + upper (np.array, torch.Tensor, float, int): upper bound + """ + if not callable(constraint): + raise TypeError("Expecting the given constraint function {} to be callable.".format(constraint)) + self._constraint = constraint + self._lower = lower + self._upper = upper + + def __call__(self, variables): + return self._lower <= self._constraint(variables) <= self._upper diff --git a/pyrobolearn/optimizers/gpyopt_optimizer.py b/pyrobolearn/optimizers/gpyopt_optimizer.py index 8926b07..0a20806 100644 --- a/pyrobolearn/optimizers/gpyopt_optimizer.py +++ b/pyrobolearn/optimizers/gpyopt_optimizer.py @@ -5,6 +5,7 @@ References: [1] https://sheffieldml.github.io/GPyOpt/ """ +import time import numpy as np # Bayesian optimization @@ -15,7 +16,8 @@ except ImportError as e: raise ImportError(e.__str__() + "\n HINT: you can install GPy/GPyOpt directly via 'pip install GPy' and " "'pip install GPyOpt'.") -from optimizer import Optimizer +from pyrobolearn.optimizers import Optimizer + __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" @@ -27,6 +29,113 @@ __email__ = "briandelhaisse@gmail.com" __status__ = "Development" -# TODO: check the `pyrobolearn/algos/bo` algo -class BayesianOptimizer(object): - pass +class BayesianOptimizer(Optimizer): + r"""Bayesian Optimization + + Bayesian Optimization is a global (gradient-free), probabilistic, non-parametric, model-based, optimization of + black-box functions. + + Bayesian optimization can be formulated as an optimization problem: + + .. math:: \theta^* = arg\,max_{\theta} f(\theta) + + where :math:`\theta` are the parameters of the model we are trying to optimize, and :math:`f` is the unknown + objective function which is modeled using a probabilistic model such as a Gaussian Process (GP). By samp + + Popular acquisition functions which specify which parameters to test next by making a trade-off between + exploitation and exploration, include: + * Probability of Improvement (PI) [7]: + * Expected Improvement (EI) [8]: + * Upper Confidence Bound (UCB) [9]: + + Pseudo-Algo (from [3]): + D <-- if available: {\theta, f(\theta)} + Prior <-- if available: prior of the response surface + while optimize: + train a response surface from D + + References: + [1] "Bayesian Approach to Global Optimization: Theory and Applications", Mockus, 1989 + [2] "A Tutorial on Bayesian Optimization of Expensive Cost Functions, with Application to Active User Modeling + and Hierarchical Reinforcement Learning", Brochu et al., 2010 + [3] "Taking the Human Out of the Loop: a Review of Bayesian Optimization", Shahriari et al., 2016 + [4] "Bayesian Optimization for Learning Gaits under Uncertainty: An Experimental Comparison on a Dynamic + Bipedal Walker", Calandra et al., 2015 + [5] "Gaussian Processes for Machine Learning", Rasmussen and Williams, 2006 + [6] "GPyOpt: A Bayesian Optimization framework in python" (2016), https://github.com/SheffieldML/GPyOpt + [7] "A New Method of Locating the Maximum Point of an Arbitrary Multipeak Curve in the Presence of Noise", + Kushner, 1964 + [8] "The Application of Bayesian Methods for Seeking the Extremum", Mockus et al., 1978 + [9] "A Statistical Method for Global Optimization", Cox et al., 1997 + """ + + def __init__(self, num_workers=1, *args, **kwargs): + """ + Initialize the Bayesian optimizer. + """ + super(BayesianOptimizer, self).__init__(*args, **kwargs) + self.num_workers = num_workers + + ########### + # Methods # + ########### + + def optimize(self, parameters, loss, max_iters=1, verbose=False, *args, **kwargs): + """ + Optimize the given loss function with respect to the given parameters. + + Args: + parameters: parameters. + loss: callable objective / loss function to minimize. + max_iters (int): number of maximum iterations. + verbose (bool): if True, it will display information during the optimization process. + *args: list of arguments to give to the loss function if callable. + **kwargs: dictionary of arguments to give to the loss function if callable. + + Returns: + float, torch.Tensor, np.array: loss scalar value. + object: best parameters + """ + # define domain + # domain = [{'name': 'params', 'type': 'continuous', 'domain': self.domain, 'dimensionality': len(parameters)}] + + # Solve the optimization + self.optimizer = GPyOpt.methods.BayesianOptimization(f=loss, + # domain=domain, + # constraints=constraints, + model_type='GP', # 'sparseGP' + acquisition_type='EI', # 'UCB'/'LCB', 'EI', 'MPI' + acquisition_optimizer_type='lbfgs', # 'DIRECT', 'CMA' + num_cores=self.num_workers, + verbosity=verbose, + maximize=self.is_maximizing, + verbosity_model=False, # True + kernel=GPy.kern.RBF(input_dim=1)) + + # print(opt.model.kernel.name) + + # Run the optimization + max_iter = max_iters if max_iters < 5 else max_iters - 5 # evaluation budget (min=5) + # max_time = max_time # time budget + eps = 1.e-6 # Minimum allows distance between the last two observations + + if verbose: + print('Optimizing...') + + # optimize + start = time.time() + self.optimizer.run_optimization(max_iter, max_time, eps) + end = time.time() + + if verbose: + print('Done with total time: {}'.format(end - start)) + + # save best parameters and reward + self.best_parameters = self.optimizer.x_opt + self.best_result = self.optimizer.fx_opt + + # print best reward + if verbose: + print("\nBest loss value found: {}".format(self.best_result)) + + return self.best_result, self.best_parameters diff --git a/pyrobolearn/optimizers/ipopt_optimizer.py b/pyrobolearn/optimizers/ipopt_optimizer.py index 1958b07..d98bd3b 100644 --- a/pyrobolearn/optimizers/ipopt_optimizer.py +++ b/pyrobolearn/optimizers/ipopt_optimizer.py @@ -15,7 +15,8 @@ except ImportError as e: "If ipopt is already installed, you can install the python wrapper via " "`pip install ipopt`.") -from optimizer import Optimizer +from pyrobolearn.optimizers import Optimizer + __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" @@ -119,12 +120,33 @@ class IPopt(Optimizer): [4] Repos: https://github.com/coin-or/Ipopt and https://pypi.org/project/ipopt/ """ - def __init__(self, model, losses, hyperparameters): - super(IPopt, self).__init__(model, losses, hyperparameters) + def __init__(self, *args, **kwargs): + """ + Initialize the interior point optimizer. + """ + super(IPopt, self).__init__(*args, **kwargs) + + def optimize(self, parameters, loss, max_iters=1, verbose=False, *args, **kwargs): + """ + Optimize the given loss function with respect to the given parameters. + + Args: + parameters (np.array): parameters to optimize. + loss (callable): callable objective / loss function to minimize. + bounds (tuple, list, np.array): parameter bounds. E.g. bounds=[0, np.inf] + max_iters (int): number of maximum iterations. + verbose (bool): if True, it will display information during the optimization process. + *args: list of arguments to give to the loss function if callable. + **kwargs: dictionary of arguments to give to the loss function if callable. + + Returns: + float, torch.Tensor, np.array: loss scalar value. + object: best parameters + """ + N = len(parameters) - def optimize(self): # define initial value - x0 = np.array([0.1] * N) # important that the initial value != 0 for the computation of the grad! + x0 = parameters # important that the initial value != 0 for the computation of the grad! # define (lower and upper) bound constraints lb = [-1] * N @@ -140,9 +162,13 @@ class IPopt(Optimizer): opt.add_constraint(OrthogonalConstraint(x)) # define the nonlinear optimization problem - nlp = ipopt.problem(n=N, m=len(cl[:i]), problem_obj=opt, lb=lb, ub=ub, cl=cl[:i], cu=cu[:i]) + self.optimizer = ipopt.problem(n=N, m=len(cl[:i]), problem_obj=opt, lb=lb, ub=ub, cl=cl[:i], cu=cu[:i]) # solve problem - x, info = nlp.solve(x0) + x, info = self.optimizer.solve(x0) - return x + # save the results + self.best_parameters = x + self.best_result = info['obj_val'] + + return self.best_result, self.best_parameters diff --git a/pyrobolearn/optimizers/nlopt_optimizer.py b/pyrobolearn/optimizers/nlopt_optimizer.py index 2352fa2..9a5f5a2 100644 --- a/pyrobolearn/optimizers/nlopt_optimizer.py +++ b/pyrobolearn/optimizers/nlopt_optimizer.py @@ -5,7 +5,9 @@ References: [1] https://nlopt.readthedocs.io/en/latest/ """ -import numpy as np +from autograd import numpy as np +from autograd import grad +import torch # NLopt optimizers try: @@ -13,7 +15,7 @@ try: except ImportError as e: raise ImportError(e.__str__() + "\n HINT: you can install nlopt via `pip install nlopt`.") -from optimizer import Optimizer +from pyrobolearn.optimizers import Optimizer __author__ = "Brian Delhaisse" @@ -26,7 +28,7 @@ __email__ = "briandelhaisse@gmail.com" __status__ = "Development" -class NLopt(object): +class NLopt(Optimizer): r"""Non-Linear Optimizer Non-linear optimizers based on the `nlopt` libraries. @@ -50,10 +52,18 @@ class NLopt(object): [3] Github repo: https://github.com/stevengj/nlopt """ - # def __init__(self, model, losses, hyperparameters, seed): - # super(NLopt, self).__init__(model, losses, hyperparameters) + def __init__(self, method, submethod=None, seed=None, *args, **kwargs): + """ + Initialize the non-linear optimizer. - def __init__(self, method, submethod=None, seed=None): + Args: + method: + submethod: + seed: + *args: + **kwargs: + """ + super(NLopt, self).__init__(*args, **kwargs) # define useful variables self.results = {1: 'success', 2: 'stop_val reached', 3: 'ftol reached', 4: 'xtol reached', @@ -64,7 +74,16 @@ class NLopt(object): nlopt.srand(seed) # define which solver to use - def get_opt(method): + def get_optimizer(method): + """ + Get the optimizer associated with the given method. + + Args: + method (str): optimizer string + + Returns: + + """ if method == 'ISRES': return nlopt.opt(nlopt.GN_ISRES, M) elif method == 'COBYLA': @@ -78,7 +97,7 @@ class NLopt(object): if method is None: method = 'SLSQP' - self.opt = get_opt(method) + self.optimizer = get_optimizer(method) # define subsolver to use (if we use the AUGLAG method) if method == 'AUGLAG': @@ -86,35 +105,52 @@ class NLopt(object): submethod = 'SLSQP' elif submethod == 'AUGLAG': raise ValueError("Submethod should be different from AUGLAG") - subopt = get_opt(submethod) + subopt = get_optimizer(submethod) subopt.set_lower_bounds(-1) subopt.set_upper_bounds(1) # subopt.set_ftol_rel(1e-2) # subopt.set_maxeval(100) - self.opt.set_local_optimizer(subopt) + self.optimizer.set_local_optimizer(subopt) - def optimize(self): + def optimize(self, parameters, loss, max_iters=1, verbose=False, *args, **kwargs): + """ + Optimize the given objective function using the optimizer. + + Args: + parameters (np.array): parameters to optimize. + loss (callable): callable objective / loss function to minimize. + bounds (tuple, list, np.array): parameter bounds. E.g. bounds=[0, np.inf] + max_iters (int): number of maximum iterations. + verbose (bool): if True, it will display information during the optimization process. + *args: list of arguments to give to the loss function if callable. + **kwargs: dictionary of arguments to give to the loss function if callable. + + Returns: + float, torch.Tensor, np.array: loss scalar value. + object: best parameters + """ # define objective function and its gradient def f(x, grad): + loss_value = loss(x) if grad.size > 0: - grad[:] = 2 * x.T.dot(C) - return x.T.dot(C).dot(x) + grad[:] = grad(loss, x) + return loss_value # define objective function to maximize - self.opt.set_max_objective(f) + self.optimizer.set_min_objective(f) # if nlopt.GN_ISRES, we can define the population size - self.opt.set_population(0) # by default for ISRES: pop=20*(M+1) + self.optimizer.set_population(0) # by default for ISRES: pop=20*(M+1) # define bound constraints (should be between -1 and 1 because the norm should be 1) - self.opt.set_lower_bounds(-1.) - self.opt.set_upper_bounds(1.) + self.optimizer.set_lower_bounds(-1.) + self.optimizer.set_upper_bounds(1.) # define norm constraint and its gradient def c1(x, grad): if grad.size > 0: grad[:] = 2 * x - return (x.T.dot(x) - 1) + return x.T.dot(x) - 1 # define orthogonal constraint class OrthogonalConstraint(object): @@ -125,18 +161,18 @@ class NLopt(object): def constraint(self, x, grad): if grad.size > 0: grad[:] = self.v - return (x.T.dot(self.v)) + return x.T.dot(self.v) # define equality constraints - self.opt.add_equality_constraint(c1, 0) + self.optimizer.add_equality_constraint(c1, 0) # opt.add_equality_mconstraint(constraints, tol) # define stopping criteria - # self.opt.set_stopval(stopval) - self.opt.set_ftol_rel(1e-8) + # self.optimizer.set_stopval(stopval) + self.optimizer.set_ftol_rel(1e-8) # opt.set_xtol_rel(1e-4) - self.opt.set_maxeval(100000) # nb of iteration - self.opt.set_maxtime(2) # time in secs + self.optimizer.set_maxeval(100000) # nb of iteration + self.optimizer.set_maxtime(2) # time in secs # define initial value x0 = np.array([0.1] * M) # important that the initial value != 0 for the computation of the grad! @@ -146,17 +182,17 @@ class NLopt(object): # add constraint if i > 0: c = OrthogonalConstraint(x) - self.opt.add_equality_constraint(c.constraint, 0) + self.optimizer.add_equality_constraint(c.constraint, 0) # optimize try: - x = self.opt.optimize(x0) + x = self.optimizer.optimize(x0) except nlopt.RoundoffLimited as e: pass # save values evecs.append(x) # param vector - evals.append(self.opt.last_optimum_value()) # max value - msgs[i] = nlopt_results[self.opt.last_optimize_result()] + evals.append(self.optimizer.last_optimum_value()) # max value + msgs[i] = nlopt_results[self.optimizer.last_optimize_result()] diff --git a/pyrobolearn/optimizers/objective.py b/pyrobolearn/optimizers/objective.py new file mode 100644 index 0000000..94c19b8 --- /dev/null +++ b/pyrobolearn/optimizers/objective.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +r"""Define the objective function in an optimization problem. + +A constrained optimization problem is generally given by: + +.. math:: \min_{x \in R^n} f(x) + +subject to + +.. math:: + + g_L \leq g(x) \leq g_U + x_L \leq x \leq x_U + +where :math:`f` is the objective function, :math:`x` are the variables that are being optimized, :math:`(x_L, x_U)` +are the lower and upper bound on these variables, :math:`g` is a constraint function that maps the variables :math:`x` +to another space, and :math:`(g_L, g_U)` are the lower and upper bound in that space. + +References: + [1] https://nlopt.readthedocs.io/en/latest/ +""" + +import torch +import numpy as np + + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2018, PyRoboLearn" +__credits__ = ["Brian Delhaisse"] +__license__ = "MIT" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class Objective(object): + r"""Objective function + + """ + + def __init__(self, loss): + """ + Initialize the objective function. + + Args: + loss (callable, object): loss function + """ + self._loss = loss + + def __call__(self, variables, *args, **kwargs): + if callable(self._loss): + return self._loss(variables, *args, **kwargs) + return self._loss diff --git a/pyrobolearn/optimizers/optimization_problem.py b/pyrobolearn/optimizers/optimization_problem.py new file mode 100644 index 0000000..34d2e2d --- /dev/null +++ b/pyrobolearn/optimizers/optimization_problem.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python +r"""Define the optimization problem. + +A constrained optimization problem is generally given by: + +.. math:: \min_{x \in R^n} f(x) + +subject to + +.. math:: + + g_L \leq g(x) \leq g_U + x_L \leq x \leq x_U + +where :math:`f` is the objective function, :math:`x` are the variables that are being optimized, :math:`(x_L, x_U)` +are the lower and upper bound on these variables, :math:`g` is a constraint function that maps the variables :math:`x` +to another space, and :math:`(g_L, g_U)` are the lower and upper bound in that space. + +References: + [1] https://nlopt.readthedocs.io/en/latest/ +""" + +import torch +import numpy as np + +from pyrobolearn.optimizers.objective import Objective +from pyrobolearn.optimizers.constraint import Constraint +from pyrobolearn.optimizers.bound import Bound + +__author__ = "Brian Delhaisse" +__copyright__ = "Copyright 2018, PyRoboLearn" +__credits__ = ["Brian Delhaisse"] +__license__ = "MIT" +__version__ = "1.0.0" +__maintainer__ = "Brian Delhaisse" +__email__ = "briandelhaisse@gmail.com" +__status__ = "Development" + + +class OptimizationProblem(object): + r"""Optimization problem + + A constrained optimization problem is generally given by: + + .. math:: \min_{x \in R^n} f(x) + + subject to + + .. math:: + + g_L \leq g(x) \leq g_U + x_L \leq x \leq x_U + + where :math:`f` is the objective function, :math:`x` are the variables that are being optimized, :math:`(x_L, x_U)` + are the lower and upper bound on these variables, :math:`g` is a constraint function that maps the variables + :math:`x` to another space, and :math:`(g_L, g_U)` are the lower and upper bound in that space. + """ + + def __init__(self, loss, bounds, constraints): + """ + Initialize the optimization problem. + + Args: + loss (Objective): objective / loss function. + bounds ((list of) Bound): bounds + constraints ((list of) Constaint): constraints + """ + # check loss function + if not isinstance(loss, Objective): + loss = Objective(loss) + self._loss = loss + + # check bounds + if not isinstance(bounds, list): + bounds = [bounds] + for i, bound in enumerate(bounds): + if isinstance(bound, tuple) and len(bound) == 2: + bounds[i] = Bound(lower=bound[0], upper=bound[1]) + elif not isinstance(bound, Bound): + raise TypeError("Expecting the given bound to be an instance of `Bound`, instead got: " + "{}".format(type(bound))) + self._bounds = bounds + + # check constraints + if not isinstance(constraints, list): + constraints = [constraints] + for i, constraint in enumerate(constraints): + if isinstance(constraint, tuple) and len(constraint) == 3: + constraint, lower, upper = constraint + constraints[i] = Constraint(constraint, lower=lower, upper=upper) + if not isinstance(constraint, Constraint): + raise TypeError("Expecting the given constraint to be an instance of `Constraint`, instead got: " + "{}".format(type(constraint))) + self._constraints = constraints + + @property + def objective(self): + """Return the objective function.""" + return self._loss + + loss = objective + + @property + def bounds(self): + """Return the bounds.""" + return self._bounds + + @property + def constraints(self): + """Return the constraints.""" + return self._constraints + + def __call__(self, variables, *args, **kwargs): + """Return the objective function.""" + return self.loss(variables, *args, **kwargs) diff --git a/pyrobolearn/optimizers/optimizer.py b/pyrobolearn/optimizers/optimizer.py index 3fb67e9..50616a0 100644 --- a/pyrobolearn/optimizers/optimizer.py +++ b/pyrobolearn/optimizers/optimizer.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -"""Provide the abstract optimizer class. +r"""Provide the abstract optimizer class. Optimizers allows to optimize (i.e. minimize or maximize) a utility function (also known as objective function, fitness function, loss, etc.) with or without constraints and bounds. Notably, it assumes the functions have some @@ -89,9 +89,10 @@ class Optimizer(object): """ Initialize the optimizer. """ + self.optimizer = None + self.is_minimizing = True self.best_parameters = None self.best_result = None - self.is_maximizing = True ############## # Properties # @@ -99,37 +100,88 @@ class Optimizer(object): @property def best_parameters(self): + """Return the best parameters.""" return self._best_parameters @best_parameters.setter def best_parameters(self, params): + """Set the best parameters.""" self._best_parameters = params @property def best_result(self): + """Return the best value.""" return self._best_result @best_result.setter def best_result(self, result): + """Set the optimal value.""" self._best_result = result + @property + def is_minimizing(self): + """Return if the optimizer is used to minimize an objective / loss function.""" + return self._is_minimizing + + @is_minimizing.setter + def is_minimizing(self, boolean): + """Set if the optimizer is used to minimize an objective / loss function.""" + self._is_minimizing = boolean + @property def is_maximizing(self): - return self._is_maximizing + """Return if we are maximizing. If False, we are minimizing.""" + return not self.is_minimizing @is_maximizing.setter def is_maximizing(self, boolean): - self._is_maximizing = bool(boolean) - - @property - def is_minimizing(self): - return not self.is_maximizing + """Setting if the optimizer is used to maximize an objective function.""" + self.is_minimizing = not bool(boolean) ########### # Methods # ########### - def optimize(self, *args, **kwargs): + def convert_from(self, parameters): + """ + Convert the parameters to the desired form for the optimizer. This should be implemented in the child classes. + + Args: + parameters: initial parameters. + + Returns: + object: parameters in the desired form. + """ + return parameters + + def convert_to(self, parameters): + """ + Convert back the optimized parameters to the initial form. This should be implemented in the child classes. + + Args: + parameters: optimized parameters. + + Returns: + object: parameters in the initial form. + """ + return parameters + + def optimize(self, parameters, loss, max_iters=1, verbose=False, *args, **kwargs): + """ + Optimize the given objective function using the optimizer. This should be implemented in the child classes. + + Args: + parameters: parameters. + loss: callable objective / loss function to minimize. + max_iters (int): number of maximum iterations. + verbose (bool): if True, it will display information during the optimization process. + *args: list of arguments to give to the loss function if callable. + **kwargs: dictionary of arguments to give to the loss function if callable. + + Returns: + float, torch.Tensor, np.array: loss scalar value. + object: best parameters + """ pass ############# @@ -137,10 +189,13 @@ class Optimizer(object): ############# def __repr__(self): + """Return a representation string of the object.""" return self.__class__.__name__ def __str__(self): + """Return a string describing the object.""" return self.__class__.__name__ def __call__(self, *args, **kwargs): + """Optimize the given objective function using the optimizer.""" return self.optimize(*args, **kwargs) diff --git a/pyrobolearn/optimizers/qpsolvers_optimizer.py b/pyrobolearn/optimizers/qpsolvers_optimizer.py index 6aa8014..454a1bf 100644 --- a/pyrobolearn/optimizers/qpsolvers_optimizer.py +++ b/pyrobolearn/optimizers/qpsolvers_optimizer.py @@ -20,7 +20,8 @@ try: except ImportError as e: raise ImportError(e.__str__() + "\n HINT: you can install qpsolvers directly via 'pip install qpsolvers'.") -from optimizer import Optimizer +from pyrobolearn.optimizers import Optimizer + __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" @@ -62,7 +63,7 @@ __status__ = "Development" # """ # pass -class QP(object): +class QP(Optimizer): r"""Quadratic Programming solvers This class uses the `qpsolvers` which is a unified Python interface for multiple QP solvers [1,2]. @@ -105,13 +106,15 @@ class QP(object): [2] Github repo: https://github.com/stephane-caron/qpsolvers """ - def __init__(self, method='quadprog'): + def __init__(self, method='quadprog', *args, **kwargs): """ Initialize the QP solver. Args: method (str): ['cvxopt', 'cvxpy', 'ecos', 'gurobi', 'mosek', 'osqp', 'qpoases', 'quadprog'] """ + super(QP, self).__init__(*args, **kwargs) + solvers = set(qpsolvers.available_solvers) if len(solvers) == 0: raise ValueError("No QP solvers have been found on this computer. Please install one of the QP modules") diff --git a/pyrobolearn/optimizers/scipy_optimizer.py b/pyrobolearn/optimizers/scipy_optimizer.py index 61e4975..87d8101 100644 --- a/pyrobolearn/optimizers/scipy_optimizer.py +++ b/pyrobolearn/optimizers/scipy_optimizer.py @@ -7,8 +7,10 @@ References: import numpy as np import scipy +import scipy.optimize + +from pyrobolearn.optimizers import Optimizer -from optimizer import Optimizer __author__ = "Brian Delhaisse" __copyright__ = "Copyright 2018, PyRoboLearn" @@ -20,7 +22,7 @@ __email__ = "briandelhaisse@gmail.com" __status__ = "Development" -class Scipy(object): +class Scipy(Optimizer): r"""Scipy optimizer This uses the `scipy.optimize.minimize` to optimize a given objective function under various bounds and @@ -53,7 +55,7 @@ class Scipy(object): [1] scipy.optimize.minimize: https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html """ - def __init__(self, method='SLSQP'): + def __init__(self, method='SLSQP', *args, **kwargs): """ Initialize the scipy method @@ -76,37 +78,54 @@ class Scipy(object): # define optimization method # By default, it will be 'BFGS', 'L-BFGS-B', or 'SLSQP' depending on the constraints and bounds # If constraints, it can only be 'COBYLA' or 'SLSQP'. COBYLA only supports inequality constraints. + super(Scipy, self).__init__(*args, **kwargs) self.method = method - def optimize(self, maxiter=1e6, verbose=True): - # define objective function to MINIMIZE - # f = lambda x: -(x.T.dot(C)).dot(x) - def f(x): - return -(x.T.dot(C)).dot(x) + def optimize(self, parameters, loss, max_iters=1e6, verbose=False, *args, **kwargs): + """ + Optimize the given objective function using the optimizer. + + Args: + parameters (np.array): parameters to optimize. + loss (callable): callable objective / loss function to minimize. + bounds (tuple, list, np.array): parameter bounds. E.g. bounds=[0, np.inf] + max_iters (int): number of maximum iterations. + verbose (bool): if True, it will display information during the optimization process. + *args: list of arguments to give to the loss function if callable. + **kwargs: dictionary of arguments to give to the loss function if callable. + + Returns: + float, torch.Tensor, np.array: loss scalar value. + object: best parameters + """ + N = len(parameters) # define initial guess - x0 = np.ones((M,)) # np.zeros((M,)) + x0 = np.ones((N,)) # np.zeros((N,)) # define 1st constraints: norm of 1 constraints = [{'type': 'eq', 'fun': lambda x: x.T.dot(x) - 1, 'jac': None, 'args': ()}] # define bounds: each vector u have a norm of 1 thus each parameter is between -1 and 1 - bounds = [(-1., 1.)] * M + bounds = [(-1., 1.)] * N # optimize recursively evals, evecs = [], [] messages = {} - options = {'maxiter': maxiter, 'disp': verbose} - for i in range(M): + options = {'maxiter': max_iters, 'disp': verbose} + for i in range(N): if i != 0: # add orthogonality constraint constraints.append({'type': 'eq', 'fun': lambda u: u1.T.dot(u)}) # minimize --> it returns an instance of OptimizeResult - result = scipy.optimize.minimize(f, x0, args=(), method=self.method, jac=None, hess=None, bounds=bounds, - constraints=constraints, tol=None, callback=None, options=options) + result = scipy.optimize.minimize(loss, x0, args=(), method=self.method, jac=None, hess=None, bounds=bounds, + constraints=constraints, tol=None, callback=None, options=options) - print(result.success) - print(result.message) - print(result.fun) - print(result.x) + if verbose: + print(result.success) + print(result.message) + print(result.fun) + print(result.x) + + return