pyuncertainnumber.opt.bo ======================== .. py:module:: pyuncertainnumber.opt.bo Classes ------- .. autoapisummary:: pyuncertainnumber.opt.bo.BayesOpt_args_signature pyuncertainnumber.opt.bo.BayesOpt_iterable_signature pyuncertainnumber.opt.bo.BayesOpt_vectorised_signature pyuncertainnumber.opt.bo.BayesOpt Functions --------- .. autoapisummary:: pyuncertainnumber.opt.bo.check_argument_count pyuncertainnumber.opt.bo.transform_func pyuncertainnumber.opt.bo.rekey_bounds_by_func pyuncertainnumber.opt.bo.as_bayes_opt pyuncertainnumber.opt.bo.as_bayes_opt_2d pyuncertainnumber.opt.bo.as_bayes_opt_auto Module Contents --------------- .. py:class:: BayesOpt_args_signature(f, design_bounds: dict, task, acquisition_function='UCB', num_explorations=100, num_iterations=100) Bayesian Optimisation class with original function signature (arguments signature). It requires the ``design_bound`` to be a dictionary, e.g. {'x1': (0, 1), 'x2': (0, 1)}. :param f: the target function to be optimised, should have individual function signature. See notes. :type f: callable :param design_bounds: the bounds for the design space, e.g. {'x1': (0, 1), 'x2': (0, 1)} :type design_bounds: dict :param task: either 'minimisation' or 'maximisation' :type task: str :param acquisition_function: the acquisition function to be used, e.g. 'UCB', 'EI', 'PI'. If None, defaults to 'UCB'. :type acquisition_function: str or callable, optional :param num_explorations: the number of initial exploration points. Defaults to 100. :type num_explorations: int, optional :param num_iterations: the number of iterations to run the optimisation. Defaults to 100. :type num_iterations: int, optional .. note:: Acquisition functions can be either a string (e.g. 'UCB', 'EI', 'PI') or a callable function. 'UCB' stands for Upper Confidence Bound, 'EI' for Expected Improvement, and 'PI' for Probability of Improvement. If a string is provided, the parameter for the acquisition function can be passed as an additional argument to the class constructor. For example, for 'UCB', you can pass a `kappa` value, and for 'EI' or 'PI', you can pass an `xi` value. For low-level controls, if a callable function is provided, it should already be parameterised. About the function signature of $f$, by default it should be expecting individual arguments in the form of $f(x_0, x_1, \ldots, x_n)$, often one needs to write a wrapper function to unpack the input arguments when working with a black-box model, which typically has vectorisation calling signature. Also, one can specify the `xc_bound` accordingly. When `EpistemicDomain` is used as a shortcut to specify the `xc_bound`, the keys will be automatically mapped to the corresponding arguments of the function. .. rubric:: Example >>> import numpy as np >>> from pyuncertainnumber.opt.bo import BayesOpt_args_signature >>> def black_box_function(x): ... return np.exp(-(x - 2)**2) + np.exp(-(x - 6)**2 / 10) + 1 / (x**2 + 1) >>> bo = BayesOpt_args_signature( ... f=black_box_function, ... dimension=1, ... design_bounds={'x': (-2, 10)}, ... task='maximisation', ... num_explorations=3, ... num_iterations=20 ... ) >>> bo.run(verbose=True) >>> print(bo.optimal) # get the optimal parameters and target value .. admonition:: Implementation This represents the original function signature. The range of the design space, defined in `design_bound`, is a dictionary mapping parameter names to their bounds. However, in later versions, such as the `BayesOpt` class, the design bounds can be specified as a list or 2D numpy array of shape (n, 2), and the function signature can be automatically detected. example: >>> ed = EpistemicDomain(pba.I(-5, 5), pba.I(-5, 5)) >>> BayesOpt(f=foo, ... design_bounds= ed.to_BayesOptBounds(func_signature="arguments"), # the trick ... task='maximisation', ... num_explorations=3, ... num_iterations=20, ... ) .. py:attribute:: task .. py:attribute:: num_explorations :value: 100 .. py:attribute:: num_iterations :value: 100 .. py:attribute:: design_bounds .. py:attribute:: acquisition_function .. py:property:: f return the function to be optimised .. py:method:: parse_acq(acq, parameter=None) parse the acquisition function :param acq: the acquisition function to be used. See notes above. :type acq: str or callable :param parameter: parameter for the acquisition function, e.g. kappa for UCB, xi for EI and PI :type parameter: float, optional .. py:method:: get_results() inspect the results, to save or not .. py:method:: run(**kwargs) run the Bayesian optimisation process. :param verbose: whether to print the progress. Defaults to False. Use 'verbose=True' to see the progress. :type verbose: bool, optional :param \*\*kwargs: additional low--level arguments to be passed to the BayesianOptimization constructor. .. rubric:: Example >>> foo.run(verbose=True) .. py:property:: optimal :type: dict return the optimal parameters and target value as a dictionary .. py:property:: optimal_xc :type: numpy.ndarray return the optimal design points (xc) .. py:property:: optimal_target :type: numpy.ndarray return the optimal target value f(xc*) .. py:class:: BayesOpt_iterable_signature(f, design_bounds: list[list[float]], dimension, task, acquisition_function='UCB', num_explorations=100, num_iterations=100) Bases: :py:obj:`BayesOpt_args_signature` Bayesian Optimisation class for iterable function style See `BayesOpt` for additional details. .. py:class:: BayesOpt_vectorised_signature(f, design_bounds: list[list[float]], task, acquisition_function='UCB', num_explorations=100, num_iterations=100) Bases: :py:obj:`BayesOpt_args_signature` Bayesian Optimisation class for vectorised function style See `BayesOpt` for additional details. .. py:class:: BayesOpt(f, design_bounds: list | numpy.ndarray, task, acquisition_function='UCB', num_explorations=100, num_iterations=100) Bases: :py:obj:`BayesOpt_args_signature` Bayesian Optimisation class with automatic function signature detection The go to class for Bayesian Optimisation :param f: the target function to be optimised, it could be vectorised or iterable signature but NOT arguments signature. See notes. :type f: callable :param design_bounds: the bounds for the design space, e.g. [[0, 1], [0, 1]] :type design_bounds: list | np.ndarray :param task: either 'minimisation' or 'maximisation' :type task: str :param acquisition_function: the acquisition function to be used, e.g. 'UCB', 'EI', 'PI'. If None, defaults to 'UCB'. :type acquisition_function: str or callable, optional :param num_explorations: the number of initial exploration points. Defaults to 100. :type num_explorations: int, optional :param num_iterations: the number of iterations to run the optimisation. Defaults to 100. :type num_iterations: int, optional .. note:: Acquisition functions can be either a string (e.g. 'UCB', 'EI', 'PI') or a callable function. 'UCB' stands for Upper Confidence Bound, 'EI' for Expected Improvement, and 'PI' for Probability of Improvement. If a string is provided, the parameter for the acquisition function can be passed as an additional argument to the class constructor. For example, for 'UCB', you can pass a `kappa` value, and for 'EI' or 'PI', you can pass an `xi` value. For low-level controls, if a callable function is provided, it should already be parameterised. About the function signature of $f$, by default it should be expecting individual arguments in the form of $f(x_0, x_1, \ldots, x_n)$, often one needs to write a wrapper function to unpack the input arguments when working with a black-box model, which typically has vectorisation calling signature. Also, one can specify the `xc_bound` accordingly. When `EpistemicDomain` is used as a shortcut to specify the `xc_bound`, the keys will be automatically mapped to the corresponding arguments of the function. The function is assumed to be a vectorised or iterable signature, indicating a f(x) with only one argument. .. rubric:: Example >>> import numpy as np >>> from pyuncertainnumber.opt.bo import BayesOpt >>> def foo_vec(x): ... return x[:, 0] ** 3 + x[:, 1] + x[:, 2] >>> bo = BayesOpt( ... f=foo_vec, ... design_bounds=[(-2, 2), (-3, 3), (-1, 1)], ... task='maximisation', ... num_explorations=3, ... num_iterations=20 ... ) >>> bo.run(verbose=True) >>> print(bo.optimal) # get the optimal parameters and target value .. admonition:: Implementation The range of the design space is defined by `design_bounds`, which is a 2D numpy array with shape (n, 2), where n is the number of parameters. For consistency, it is recommended to use the class `EpistemicDomain.to_BayesOptBounds()` to automatically take care of the format of the bounds. example: >>> BayesOpt(f=foo_vec, ... design_bounds= [(-2, 2), (-3, 3), (-1, 1)], ... task='maximisation', ... num_explorations=3, ... num_iterations=20, ... ) .. py:function:: check_argument_count(func) .. py:function:: transform_func(fb, **kwargs) .. py:function:: rekey_bounds_by_func(bounds_indexed: dict, func, *, allow_extras=False) Convert bounds like {'0': (lo,hi), '1': (lo,hi), ...} into {'param_name': (lo,hi), ...} using func's signature. .. py:function:: as_bayes_opt(f_vec, bounds_list, prefix='x') Wrap a vector/array objective f_vec(x) so it can be used by bayes_opt, which calls f(**kwargs) with named scalar params. :param f_vec: Objective expecting a 1-D array-like x of length d. May also be vectorized and return a numpy array; we coerce to float. :type f_vec: callable :param bounds_list: [(low_0, high_0), ..., (low_{d-1}, high_{d-1})] :type bounds_list: list[tuple[float, float]] :param prefix: Name prefix for parameters (x0, x1, ...). :type prefix: str :returns: * **wrapped** (*callable(**kwargs) -> float*) * **pbounds** (*dict[str, tuple[float, float]]*) * **names** (*list[str]*) .. py:function:: as_bayes_opt_2d(f_vec2d, bounds_list, prefix='x') Wrap a strict 2-D objective f_vec2d(X) for bayes_opt. :param f_vec2d: Objective expecting a 2-D array X of shape (m, d) and returning an array of shape (m,) (or something squeezable to that). Example: def f_vec2d(X): return X[:, 0]**3 + X[:, 1] + X[:, 2] :type f_vec2d: callable :param bounds_list: Bounds in order for each of the d dimensions. :type bounds_list: list[tuple[float, float]] :param prefix: Parameter name prefix for kwargs: x0, x1, ... :type prefix: str :returns: * **wrapped** (*callable(**kwargs) -> float*) -- Callable compatible with BayesianOptimization (single-point evaluation). * **pbounds** (*dict[str, tuple[float, float]]*) -- Name→bounds mapping for bayes_opt. * **names** (*list[str]*) -- The ordered parameter names used by wrapped. .. py:function:: as_bayes_opt_auto(f_any, bounds_list, prefix='x', prefer_2d=False) Wrap an objective that might accept either a 1-D vector x (shape (d,)) or a strict 2-D matrix X (shape (m, d), here m=1 per BO call). The wrapper auto-detects the expected shape on the *first* call by trying one shape then the other, caches the mode, and always returns a scalar float. :param f_any: Objective. Examples: - def f_vec(x: np.ndarray): return x[0]**3 + x[1] + x[2] # expects (d,) - def f_mat(X: np.ndarray): return X[:,0]**3 + X[:,1] + X[:,2] # expects (m,d)->(m,) :type f_any: callable :param bounds_list: [(low_0, high_0), ..., (low_{d-1}, high_{d-1})] :type bounds_list: list[tuple[float, float]] :param prefix: Name prefix for kwargs (e.g., x0, x1, ...). :type prefix: str :param prefer_2d: If True, try (1, d) first, else try (d,) first. Useful when you *expect* strict 2-D but still want auto fallback. :type prefer_2d: bool :returns: * **wrapped** (*callable(**kwargs) -> float*) * **pbounds** (*dict[str, tuple[float, float]]*) * **names** (*list[str]*)