Source code for gpry.surrogate

"""
Class holding the surrogate model.

The surrogate model consists of different parts, some of them optional, acting in
descending order:

- Preprocessors: defined using kwargs ``preprocessing_X|y``, they define the
  transformation between the input space and the space in which the GP Regressor and the
  infinities classifiers are trained on. See the :doc:`documentation of the preprocessing
  module<module_preprocessing>`.

- Infinities classifier(s): a class handling one or more classifiers that divide the
  space into regions where the log-posterior is expected to be finite or not. Its purpose
  is both to focus the GP Regressor into areas where the posterior is more regular, and to
  save time by avoiding evaluating the GP Regressor in uninsteresting regions. See
  :doc:`its documentation<module_infinities_classifier>`.

- GP Regressor: It is the heart of the surrogate model and thus the only non-optionally
  trivial part. It is described in more detail in :doc:`the documentation of its
  module<module_gpr>`.

- Clipper: an optional post-processor that clips the output of the GP Regressor whenever
  it is above some factor of the maximum log-posterior difference in the training set.

The :class:`~gpry.surrogate.SurrogateModel` class provides the
:func:`~gpry.surrogate.SurrogateModel.append` method to add new data to the model and
optionally fit its different components (including their hyperparameters if present).

Surrogate model predictions can be obtained using the
:func:`~gpry.surrogate.SurrogateModel.logp` method, or more generally the
:func:`~gpry.surrogate.SurrogateModel.predict` method. Classification of finite vs
infinite log-posterior values can be obtained with
:func:`~gpry.surrogate.SurrogateModel.is_finite_X` (for predicting with the classifier(s)
based on some input) or :func:`~gpry.surrogate.SurrogateModel.is_finite_y` (comparison
against a set threshold). Do not call methods of the GP Regressor or the classifiers
directly, since they need to be passed preprocessed input.

To extract the current training set there are a number of class properties defined,
depending on whether one wants all the training points, just the last ones added, just the
ones used (or not used) by the GP Regressor, etc. Alternatively, one can get a list of
all the points and their properties with
:func:`~gpry.surrogate.SurrogateModel.training_set_as_df`.
"""

# Builtin
import warnings
from copy import deepcopy
from typing import Mapping, Sequence
from numbers import Number

# External
import numpy as np
import pandas as pd  # type: ignore
from sklearn.utils.validation import check_array  # type: ignore

# Local
from gpry.gpr import GaussianProcessRegressor
from gpry.preprocessing import DummyPreprocessor
from gpry.tools import delta_logp_of_1d_nstd, generic_params_names, get_Xnumber
from gpry.infinities_classifier import InfinitiesClassifiers


[docs] class SurrogateModel: r""" Object holding the Gaussian Process Regressor, and, if applicable, the input/output preprocessing layer and the infinities classifier. Attributes ending with an underscore possess values in the respective preprocessed spaces. Parameters ---------- bounds : array-like, shape=(n_dims,2) Array of bounds of the prior [lower, upper] along each dimension. preprocessing_X : X-preprocessor, Pipeline_X, optional (default: None) Single preprocessor or pipeline of preprocessors for X. If None is passed the data is not preprocessed. preprocessing_y : y-preprocessor or Pipeline_y, optional (default: None) Single preprocessor or pipeline of preprocessors for y. If None is passed the data is not preprocessed. regressor : dict Dictionary of options to iniitalise the :class:`gpr.GaussianProcessRegressor``. clip_factor : float, optional (default: 1.1) Factor for upper clipping of the regressor's predictions, to avoid overshoots. infinities_classifier : str, Sequence, dict Dictionary of options to initialise the :class:`infinities_classifier.InfinitiesClassifiers`, used for selecting a subset of points to be used to train the regressor, based on their target value. Alternative, the name of a single classifier, or a list of names, can be passed. random_state : int or numpy.random.Generator, optional The generator used to perform random operations of the GPR. If an integer is given, it is used as a seed for the default global numpy random number generator. verbose : 1, 2, 3, optional (default: 1) Level of verbosity of the GP. 3 prints Infos, Warnings and Errors, 2 Warnings and Errors, and 1 only Errors. Should be set to 2 or 3 if problems arise. """ def __init__( self, bounds=None, preprocessing_X=None, preprocessing_y=None, regressor=None, infinities_classifier=None, clip_factor=1.1, random_state=None, verbose=1, ): self._bounds = np.atleast_2d(bounds) self.preprocessing_X = ( DummyPreprocessor if preprocessing_X is None else preprocessing_X ) self.preprocessing_y = ( DummyPreprocessor if preprocessing_y is None else preprocessing_y ) self.n_eval = 0 self.verbose = verbose if verbose is not None else 3 self._fitted = False # Arrays containing the evaluations used to train the model, # regardless of whether they are used and for what. self._X = np.empty((0, self.d), dtype=float) self._y = np.empty((0,), dtype=float) self._i_y_sorted = np.empty((0,), dtype=int) # Same, in the transformed space self._X_, self._y_ = None, None # Iteration index, use if specified only self._i_iter = np.empty((0,), dtype=int) # Custom properties for the input points self._properties = {} # Masks for being used at any time for which part of the model self._i_regress = np.empty((0,), dtype=bool) # Trackers for last-appended points self.n_last_appended = 0 self.n_last_appended_finite = 0 # Dealing with infinities: upper and lower caps, and classifier(s) self.inf_value = np.inf self.minus_inf_value = -np.inf if not infinities_classifier: self.infinities_classifier = None else: if not getattr(self.preprocessing_y, "is_linear", False): warnings.warn( "If using a standard classifier for infinities, the y-preprocessor " "needs to be linear (declare an attr ``is_linear=True``). This may " "lead to errors further in the pipeline." ) if isinstance(infinities_classifier, str): infinities_classifier = {infinities_classifier.lower(): None} elif isinstance(infinities_classifier, Sequence): infinities_classifier = {k: None for k in infinities_classifier} elif not isinstance(infinities_classifier, Mapping): raise TypeError( "'infinities_classifier': must be dict of '{classifier: {options}'." ) # The infinities classifier lives in the transformed space: bounds_ = self.preprocessing_X.transform_bounds(self._bounds) # In this first call, if preprocessor not fitted, use dummy one if self.preprocessing_y.fitted: preprocessing_y = self.preprocessing_y else: preprocessing_y = DummyPreprocessor nstd_calculator = lambda nsigma: preprocessing_y.transform_scale( delta_logp_of_1d_nstd(nsigma, self.d) ) try: self.infinities_classifier = InfinitiesClassifiers( bounds=bounds_, nstd_calculator=nstd_calculator, random_state=random_state, **infinities_classifier, ) except Exception as excpt: raise TypeError(f"Error initialising infinities classifier: {excpt}") # Initialising the GP Regressor # NB: hyperparameter priors and noise level are understood in transformed space # Make sure the length scale prior is unfolded per-dimensionality, since the GPR # will read the dimensionality from there. length_scale_prior = np.atleast_2d(regressor["length_scale_prior"]) if len(length_scale_prior) == 1: length_scale_prior = np.array(list([length_scale_prior[0]]) * self.d) elif len(length_scale_prior) != self.d: raise TypeError( "length_scale_prior needs to define 1d bounds (common for all dimensions)" "or as many bounds as dimensions" ) regressor["length_scale_prior"] = length_scale_prior # This is the default "noise_level" of the regressor, understood as the common # sigma_y of the training samples. self._noise_level = float(regressor["noise_level"]) # For now, the y preprocessor is not fitted self._noise_level_ = preprocessing_y.transform_scale(self._noise_level) kwargs_regressor = deepcopy(regressor) kwargs_regressor["noise_level"] = self._noise_level_ kwargs_regressor["random_state"] = random_state self.gpr = GaussianProcessRegressor(**kwargs_regressor) # Regressor post-processing: clip too high values self.clipper = Clipper(clip_factor) def __str__(self): return ( f"* X-preprocessor: {self.preprocessing_X.__class__.__name__}\n" + f"* y-preprocessor: {self.preprocessing_y.__class__.__name__}\n" + "* GPR kernel:\n" + f" {str(self.gpr.kernel)}\n" + " with hyperparameters (in transformed scale):\n" + " -" + "\n -".join(str(h) for h in self.gpr.kernel.hyperparameters) + "\n" + ( f"* Noise level: {self._noise_level}\n" if not self.gpr.is_noise_in_kernel else "" ) + f"* Classifiers for infinities: {self.infinities_classifier}" ) @property def d(self): """Dimensionality of the training data.""" return len(self._bounds) @property def bounds(self): """Copy of the problem's prior bounds.""" return np.copy(self._bounds) @property def trust_bounds(self): """ The bounds of a smaller trust region where the log-posterior is expected to be finite, possibly defined by a classifier (in particular if :class:`infinities_classifier.TrustRegion` is being used). Otherwise returns a copy of the original prior bounds. """ if self.infinities_classifier is not None: return self.preprocessing_X.inverse_transform_bounds( self.infinities_classifier.trust_bounds ) return np.copy(self._bounds) @property def fitted(self): """ True whenever the the surrogate model has already been fitted to some points. """ return self._fitted @property def n_total(self): """ Number of points/features in the training set of the model, including points with target values classified as infinite. """ return len(self._y) @property def X(self): """ Coordinates of all the points used to train the model (returns a copy). Intended to be used when one wants to access the training data for any purpose. """ return np.copy(self._X) @property def y(self): """ Log-posterior of all the points used to train the model (returns a copy). Intended to be used when one wants to access the training data for any purpose. """ return np.copy(self._y) @property def X_last_appended(self): """ Coordinates of the training points added to the surrogate model in the last call to :func:`~gpry.surrogate.Surrogate.append`, regardless of classification (returns a copy). """ return np.copy(self._X[-self.n_last_appended :]) @property def y_last_appended(self): """ Log-posterior of the training points added to the surrogate model in the last call to :func:`~gpry.surrogate.Surrogate.append`, regardless of classification (returns a copy). """ return np.copy(self._y[-self.n_last_appended :]) @property def n_init(self): """ Number of points passed at initialisation. """ return len(np.where(self._i_iter == 0)[0]) @property def X_init(self): """ Coordinates of the points passed at initialisaion (returns a copy). """ return np.copy(self._X[np.where(self._i_iter == 0)]) @property def y_init(self): """ Log-posterior of the points passed at initialisaion (returns a copy). """ return np.copy(self._y[np.where(self._i_iter == 0)]) @property def n_regress(self): """ Number of points currently in use to train the GP Regressor. """ return len(self._i_regress) @property def X_regress(self): """ Coordinates of the points currently in use to train the GP Regressor (returns a copy). """ return np.copy(self._X[self._i_regress]) @property def y_regress(self): """ Log-posterior of the points currently in use to train the GP Regressor (returns a copy). """ return np.copy(self._y[self._i_regress]) @property def X_last_appended_regress(self): """ Coordinates of the training points added to the GP Regressor in the last call to :func:`~gpry.surrogate.Surrogate.append` (returns a copy). """ return np.copy(self._X[self._i_regress][-self.n_last_appended_finite :]) @property def y_last_appended_regress(self): """ Log-posterior of the training points added to the GP Regressor in the last call to :func:`~gpry.surrogate.Surrogate.append` (returns a copy). """ return np.copy(self._y[self._i_regress][-self.n_last_appended_finite :]) @property def _i_finite_y(self): """ Indices of the elements in the training set whose log-posterior has been classified as non-finite. NB: Intentionally separated from ``self._i_regress`` since there may be finite points not in use to train the GP Regressor. """ if self.infinities_classifier is None: return np.arange(self.n_total, dtype=int) else: return self.infinities_classifier.i_finite_y( self._y_, i_sorted=self._i_y_sorted, validate=False ) @property def X_infinite(self, validate=True): """ Coordinates of the training points classified as infinitely small (returns a copy). """ return np.copy(np.delete(self._X, self._i_finite_y, axis=0)) @property def y_infinite(self, validate=True): """ Log-posterior of the training points classified as infinitely small (returns a copy). """ return np.copy(np.delete(self._y, self._i_finite_y)) @property def y_max(self): """ The maximum log-posterior value in the training set (returns a copy). Returns -inf (regularized if applicable) if not points have been added. """ try: return np.copy(self._y[self._i_y_sorted[-1]]) except IndexError: return self.minus_inf_value @property def scales(self): """ GPR Kernel scales as ``(output_scale, (length_scale_1, ...))`` in non-transformed coordinates. """ output_scale, length_scales = self.gpr.scales if self.preprocessing_y.fitted: return ( self.preprocessing_y.inverse_transform_scale(output_scale), tuple(self.preprocessing_X.inverse_transform_scale(length_scales)), ) else: return (output_scale, length_scales) @property def noise_level(self): """ The noise level (square-root of the variance) of the uncorrelated training data, in non-transformed coordinates. """ if self.preprocessing_y.fitted: return self.preprocessing_y.inverse_transform_scale(self.gpr.noise_level) else: return self.gpr.noise_level @property def abs_finite_threshold(self): """ Absolute threshold(s) for ``y`` values to be considered finite. """ if self.infinities_classifier is None: return self.minus_inf_value return self.y_max - self.preprocessing_y.inverse_transform_scale( self.infinities_classifier.get_highest_current_threshold() )
[docs] def is_finite_y(self, y, validate=True): """ Returns the classification of y (target) values as finite (True) or not, by comparing them with the current threshold(s). Notes ----- Use this method instead of the equivalent one of the 'infinities_classifier' attribute, since the arguments of that one may need to be transformed first. If calling with an argument which is not either the training set or a subset of it results may be inconsistent, since new values may modify the threshold. """ if self.infinities_classifier is None: return np.isfinite(y) return self.infinities_classifier.is_finite_y( self.preprocessing_y.transform(y), validate=validate )
[docs] def is_finite_X(self, X, validate=True): """ Returns a prediction for the classification of the target value at some given parameters. Notes ----- Use this method instead of the equivalent one of the 'infinities_classifier' attribute, since the arguments of that one may need to be transformed first. """ if self.infinities_classifier is None: return np.full(shape=(len(X),), fill_value=True) return self.infinities_classifier.predict( self.preprocessing_X.transform(X), validate=validate )
[docs] def training_set_as_df(self, param_names=None): """ Returns the training set as a pandas DataFrame (created on-the-fly and not saved). If ``param_names`` are not passed, generic ones will be used. """ use_X = self._X.copy() use_y = self._y.copy() if param_names is None: param_names = generic_params_names(self.d) data = dict(zip(param_names, use_X.T)) data["y"] = use_y data["added_iter"] = self._i_iter data["is_regress"] = np.full_like(use_y, False, dtype=bool) data["is_regress"][self._i_regress] = True if self.infinities_classifier is None: data["is_finite"] = np.isfinite(data["y"]) else: data["is_finite"] = self.is_finite_y(self.y) for k, v in self._properties.items(): data[f"property:{k}"] = v return pd.DataFrame(data)
[docs] def set_random_state(self, random_state): """ (Re)sets the random state. """ self.random_state = random_state self.gpr.random_state = random_state if self.infinities_classifier is not None: self.infinities_classifier.set_random_state(random_state)
def _validate_noise_level(self, noise_level, n_train): """ Checks for type and inconsistencies for the given noise level. Separated from the update method to be performed before updating class attributes. Returns a validated value that can be used directly. """ if n_train == 0 and noise_level is not None: raise ValueError("noise_level must be None if not fitting to new points.") if np.iterable(noise_level): noise_level = np.atleast_1d(noise_level) if noise_level.shape[0] != n_train: raise ValueError( "noise_level must be an array with same number of entries as y, but " f"len(n)={noise_level.shape[0]} != len(y)={n_train})" ) elif isinstance(noise_level, Number): if np.iterable(self._noise_level): noise_level = np.full(fill_value=noise_level, shape=(n_train,)) elif noise_level is None: if np.iterable(self._noise_level): raise ValueError( "Need to pass non-null noise_level (scalar or array) because concrete" " values were given earlier for the training points." ) else: raise ValueError( "noise_level needs to be an iterable, number or None. " f"Got type(noise_level)={type(noise_level)}" ) return noise_level def _update_noise_level(self, noise_level): """ Updates the noise level of the training set with the new values (or the lack thereof). Assumes possible inconsistencies dealt with by ``_validate_noise_level``. """ if np.iterable(noise_level): if not np.iterable(self._noise_level): if self.verbose > 1: warnings.warn( "A new noise level has been assigned to the updated training set " "while the old training set has a single scalar noise level: " f"{self._noise_level}. Converting to individual levels!" ) self._noise_level = np.full( fill_value=self._noise_level, shape=(len(self._y),) ) self._noise_level = np.append(self._noise_level, noise_level, axis=0) elif isinstance(noise_level, Number): # NB at validation new=scalar has been converted to array if old=array assert not np.iterable(self._noise_level) if not np.isclose(noise_level, self._noise_level): if self.verbose > 1: warnings.warn( "Overwriting the noise level with a scalar. Make sure that " "kernel's hyperparamters are refitted." ) self._noise_level = noise_level elif noise_level is None: pass # keep old level for new points. def _update_properties(self, properties): if properties is None: return err_msg = ( "`properties` must be a dict of {property: [values1, value2, ...]}, with as " "many values as points passed to `append`." ) if not isinstance(properties, Mapping): raise TypeError(f"{err_msg} Got {properties}.") for k, v in properties.items(): if not hasattr(v, "__len__"): raise TypeError(f"{err_msg} Got {v} (not a Sequence) for property {k}.") if len(v) != self.n_last_appended: raise TypeError( f"{err_msg} Wrong number of values in {k}:{v}: should be " f"{self.n_last_appended}." ) # New property -> assign NaN to previous entries if k not in self._properties: self._properties[k] = [np.nan] * (self.n_total - self.n_last_appended) self._properties[k] += list(v)
[docs] def append( self, X, y, noise_level=None, fit_gpr=True, fit_classifier=True, validate=True, i_iter=None, properties=None, ): r""" Append newly acquired data to the GPR and the infinities classifier, and updates them and the preprocessors. Here updating of the GPR refers to the re-calculation of the the GPR inverse matrix :math:`(K(X,X)+\sigma_n^2 I)^{-1}` which is needed for predictions. The highest cost incurred by this method is the refitting of the GPR kernel hyperparameters :math:`\theta`. It can be useful to disable it (``fit_gpr=False``) in cases where it is worth saving the computational expense in exchange for a loss of information, such as when performing parallelized active sampling (NB: this is only possible when the GPR hyperparameters have been fit at least once). For an additional speed boost, the refitting of the infinities classifier (if present) can be disabled with ``fit_classifier=False`` (if a GPR refit is requested this value is overridden). If called with ``X=None, y=None``, it re-fits the model without adding new points. The following calls should then be equivalent: .. code-block:: python fit_gpr_kwargs = {"n_restarts": 10} # A gpr.append(new_X, new_y, fit_gpr=fit_gpr_kwargs) # B gpr.append(new_X, new_y, fit_gpr=False) gpr.fit_gpr_hyperparameters(**fit_gpr_kwargs) # C gpr.append(new_X, new_y, fit_gpr=False, fit_classifier=False) gpr.append(None, None, fit_gpr=fit_gpr_kwargs) Parameters ---------- X : array-like, shape = (n_samples, n_features), or None Training data to append to the model. y : array-like, shape = (n_samples), or None Target values to append to the data noise_level : number, array-like, shape = (n_samples) Uncorrelated standard deviation(s) to add to the diagonal part of the covariance matrix. If an array is passed, it needs to have the same number of entries as y. If None, the noise_level set in the instance is used (notice that passing None is not recommended if a preprocessor for the GPR training samples may have been refit after initialization). If a non-None value is passed, it is advisable to refit the hyperparameters of the kernel. fit_gpr : Bool or dict (default: True) Whether the GPR :math:`\theta`-parameters should be optimised. It can be passed a dict, which is always interpreted as ``True``, containing arguments for the ``_fit_hyperparameters`` method, namely ``n_restarts`` of the optimizer, ``start_from_current`` if the first optimizer run should start from the last optimum, or different ``hyperparameters_bounds``. E.g. to perform a single GPR optimization run starting at the last hyperparameter optimum: ``fit_gpr={'start_from_current': True, 'n_restarts': 1}``. fit_classifier: Bool (default: True) Whether the infinities classifier is refit. Overridden to ``True`` if ``fit_gpr`` is not ``False``. i_iter : int or array-like, shape = (n_samples), optional An index to be assigned to the newly-appended points. If None, assigns the last entry +1. properies : dict {str: Sequence}, optional A dictionary of property names, with a list of the same length as ``X`` as values. These properties have no effect, beyond being kept track of. Returns ------- self Returns an instance of self. """ if validate and ( self.gpr.kernel is None or self.gpr.kernel.requires_vector_input ): X = check_array(X, ensure_2d=True, dtype="numeric", ensure_all_finite=False) y = check_array( y, ensure_2d=False, dtype="numeric", ensure_all_finite=False ) elif validate: X = check_array(X, ensure_2d=False, dtype=None, ensure_all_finite=False) y = check_array(y, ensure_2d=False, dtype=None, ensure_all_finite=False) if X.shape[0] != y.shape[0]: raise TypeError( f"Different numbers of points in X (shape {X.shape}) and y (shape " f"{y.shape})." ) # Ensure fit_gpr --> fit_classifier --> fit_preprocessors fit_preprocessors = False if not isinstance(fit_gpr, (bool, Mapping)): raise TypeError( f"'fit_gpr' kwarg must be bool|dict|'simple', but was {fit_gpr}" ) if fit_gpr is not False: fit_classifier = True if fit_classifier: fit_preprocessors = True force_fit_gpr = False # to avoid skipping fit if no points added if X,y = None if X is None and y is None: X, y = np.empty((0, self.d)), np.empty((0,)) force_fit_gpr = fit_gpr is not False if noise_level is not None: raise ValueError("Cannot give a noise level if X and y are not given.") # self._i_iter does not change elif X is None or y is None: # (None, None) already excluded raise ValueError("If passing X, y needs to be passed too, and viceversa.") else: # new points to be added if i_iter is None: if not len(self._i_iter): i_iter = 0 else: i_iter = self._i_iter[-1] + 1 self._i_iter = np.append(self._i_iter, len(y) * [i_iter]) noise_level_valid = self._validate_noise_level(noise_level, len(y)) # NB: if called with X, y = None, None, we could also have adopted the convention # that the "last"-named variables refer to the last call with non-null X, y, # but for now they are reset at every call, turning into 0 if no points given. self.n_last_appended = len(y) self._X = np.append(self._X, X, axis=0) self._y = np.append(self._y, y) self._i_y_sorted = None self._update_noise_level(noise_level_valid) self._update_properties(properties) # 1. Fit preprocessors with finite points and select finite points in the process, # and create transformed training set and noises. # NB: which points are finite does not change after classifier is refit (as long # as the y-preprocessor is a linear transf.), so we can select them now. if self.infinities_classifier is None: self._i_regress = np.arange(len(self._y)) else: # The point of this: select points early, even before fitting preprocessors! # precisely because we need this selection to fit the preprocessors! # Use the manual method for non-preprocessed input. # Make sure that the threshold is such that there is a min of finite ones. if self.preprocessing_y.fitted: use_y = self.preprocessing_y.transform(self._y) else: # first call, preprocessors not fit yet. use_y = self._y self._i_y_sorted = np.argsort(use_y) # To keep track of the order of appeding of samples, we sort self._i_regress # (the infinities classifier returns y-sorting indices) self._i_regress = np.sort( self.infinities_classifier._i_finite_y_prefit( use_y, i_sorted=self._i_y_sorted, validate=False ) ) nstd_calculator = None if fit_preprocessors: self.preprocessing_X.fit(self._X[self._i_regress], self._y[self._i_regress]) self.preprocessing_y.fit(self._X[self._i_regress], self._y[self._i_regress]) # We need to update the thresholds even if not fitting the inf class if self.infinities_classifier is not None: nstd_calculator = lambda nsigma: self.preprocessing_y.transform_scale( delta_logp_of_1d_nstd(nsigma, self.d) ) self.infinities_classifier.update_threshold_definition(nstd_calculator) self._X_ = self.preprocessing_X.transform(self._X) self._y_ = self.preprocessing_y.transform(self._y) # The transformed noise level is always an array if noise is not in the kernel self._noise_level_ = self.preprocessing_y.transform_scale(self._noise_level) # 2. Fit the classifiers: SVM in the transformed space, and trust region if self.infinities_classifier is None: i_finite_predict = np.arange(len(self._y)) else: if fit_classifier: i_finite_predict = self.infinities_classifier.fit( self._X_, self._y_, nstd_calculator=nstd_calculator, i_sorted=self._i_y_sorted, validate=False, ) elif validate: # there is a classifier, but we are not fitting it i_finite_predict = self.infinities_classifier.i_finite_y( self._y_, i_sorted=self._i_y_sorted, validate=False ) if validate: # NB: self._i_regress: appending order; i_finite_predict: y-ascending assert set(self._i_regress) == set(i_finite_predict), ( "Infinities classifier miss-classified at least 1 point." ) # Get number of newly added finite points -- relies on self._i_regress sorted. self.n_last_appended_finite = len(self._i_regress) - np.searchsorted( self._i_regress, self.n_total - self.n_last_appended ) # 3. Re-fit the GPR in the transformed space, and maybe hyperparameters # If all added values are infinite there's no need to refit the GPR, # unless an explicit call for that with X, y = None was made noise_level_passed = ( self._noise_level_ if (self.gpr.is_noise_in_kernel or not np.iterable(self._noise_level_)) else self._noise_level_[self._i_regress] ) if self.n_last_appended_finite != 0 or force_fit_gpr: self.gpr.fit( X=self._X_[self._i_regress], y=self._y_[self._i_regress], noise_level=noise_level_passed, fit_hyperparameters=fit_gpr, validate=False, ) self._fitted = True return self
[docs] def predict( self, X, return_std=False, return_mean_grad=False, return_std_grad=False, validate=True, ignore_classifier=None, ): """ Predict output for X. In addition to the mean of the predictive distribution, also its standard deviation (return_std=True), the gradient of the mean and the standard-deviation with respect to X can be optionally provided. Parameters ---------- X : array-like, shape = (n_samples, n_features) Query points where the GP is evaluated. return_std : bool, default: False If True, the standard-deviation of the predictive distribution at the query points is returned along with the mean. return_mean_grad : bool, default: False Whether or not to return the gradient of the mean. Only valid when X is a single point. return_std_grad : bool, default: False Whether or not to return the gradient of the std. Only valid when X is a single point. validate : bool, default: True If False, ``X`` is assumed to be correctly formatted (2-d float array, with points as rows and dimensions/features as columns, C-contiguous), and no checks are performed on it. Reduces overhead. Use only for repeated calls when the input is programmatically generated to be correct at each stage. ignore_classifier : list, optional (default: None) If defined as a list, the classifiers with names on that list are not used to discard the given points whenever all inputs are predicted to be negative infinity. .. note:: Note that in contrast to the sklearn GP Regressor our implementation cannot return the full covariance matrix. This is to save on some complexity and since the full covariance cannot be calculated if either values are infinite or a y-preprocessor is used. Returns ------- y_mean : array, shape = (n_samples) Mean of GPR's predictive distribution a query points y_std : array, shape = (n_samples,), optional Standard deviation of GPR's predictive distribution at query points. Only returned when return_std is True. y_mean_grad : shape = (n_samples, n_features), optional The gradient of GPR's the predicted mean. y_std_grad : shape = (n_samples, n_features), optional The gradient of GPR's the predicted std. """ self.n_eval += len(X) if return_std_grad and not (return_std and return_mean_grad): raise ValueError( "Not returning std_gradient without returning " "the std and the mean grad." ) if X.shape[0] != 1 and (return_mean_grad or return_std_grad): raise ValueError("Mean grad and std grad not implemented for n_samples > 1") if validate: if self.gpr.kernel is None or self.gpr.kernel.requires_vector_input: X = check_array(X, ensure_2d=True, dtype="numeric") else: X = check_array(X, ensure_2d=False, dtype=None) # Create placeholders with default values X = np.copy(X) # copy since preprocessors might change it n_samples = X.shape[0] X_ = self.preprocessing_X.transform(X) return_dict = {"mean": np.full(n_samples, self.minus_inf_value)} if return_std: return_dict["std"] = np.zeros(n_samples) # std is zero when mu is -inf if return_mean_grad: return_dict["mean_grad"] = np.full((n_samples, self.d), self.inf_value) if return_std_grad: return_dict["std_grad"] = np.zeros((n_samples, self.d)) # TODO: At the moment is is not checked if the point is within prior bounds. # Could be checked even before calling classifier, if validate=True. # Idem for predict_std # First check if either SVM or the trust region say that the value should be -inf finite = self.infinities_classifier.is_finite_X( X_, ignore=ignore_classifier, validate=validate ) # If all values are infinite no need to run the prediction through the GP if np.all(~finite): if len(return_dict) == 1: return return_dict["mean"] return list(return_dict.values()) # else: at least some finite points --> GPR predict for them only return_gpr_ = self.gpr.predict( X_[finite], return_std=return_std, return_mean_grad=return_mean_grad, return_std_grad=return_std_grad, validate=validate, ) return_gpr_ = self._regressor_output_to_dict( return_gpr_, return_std, return_mean_grad, return_std_grad ) for k, v in return_gpr_.items(): if k == "mean": return_dict[k][finite] = self.preprocessing_y.inverse_transform(v) else: return_dict[k][finite] = self.preprocessing_y.inverse_transform_scale(v) # Apply inverse transformation twice for std grad if "std_grad" in return_dict: return_dict["std_grad"][finite] = ( self.preprocessing_y.inverse_transform_scale( return_dict["std_grad"][finite] ) ) # Upper clipping to avoid overshoots # The 'trivial' check avoids wasting computation in finding the y extremes if not self.clipper.trivial: return_dict["mean"] = self.clipper( return_dict["mean"], self._y[self._i_regress].min(), self._y[self._i_regress].max(), ) if len(return_dict) == 1: return return_dict["mean"] return list(return_dict.values())
@staticmethod def _regressor_output_to_dict( return_value, return_std, return_mean_grad, return_std_grad ): """ Convert the return value of the regressor's ``predict`` method into a dict with with the requested products as keys. """ return_dict = {"mean": return_value[0]} i = 1 if return_std: return_dict["std"] = return_value[i] i += 1 if return_mean_grad: return_dict["mean_grad"] = return_value[i] i += 1 if return_std_grad: return_dict["std_grad"] = return_value[i] return return_dict
[docs] def logp( self, X, validate=True, ignore_classifier=None, ): """ Returns the surrogate log-posterior. Alias of :func:`~gpry.surrogate.Surrogate.predict` for the GP Regressor's mean only. Parameters ---------- X : array-like, shape = (n_samples, n_features) Query points where the GP is evaluated. validate : bool, default: True If False, ``X`` is assumed to be correctly formatted (2-d float array, with points as rows and dimensions/features as columns, C-contiguous), and no checks are performed on it. Reduces overhead. Use only for repeated calls when the input is programmatically generated to be correct at each stage. ignore_classifier : list, optional (default: None) If defined as a list, the classifiers with names on that list are not used to discard the given points whenever all inputs are predicted to be negative infinity. Returns ------- log-posterior : array, shape = (n_samples) Predicted log-posterior. """ if validate: X = np.atleast_2d(X) if self.gpr.kernel is None or self.gpr.kernel.requires_vector_input: X = check_array(X, ensure_2d=True, dtype="numeric") else: X = check_array(X, ensure_2d=False, dtype=None) return self.predict(X, validate=False, ignore_classifier=ignore_classifier)
[docs] def predict_std(self, X, ignore_classifier=None, validate=True): """ Predict output standart deviation for X. Parameters ---------- X : array-like, shape = (n_samples, n_features) Query points where the GP is evaluated. ignore_classifier : list, optional (default: None) If defined as a list, the classifiers with names on that list are not used to discard the given points whenever all inputs are predicted to be negative infinity. validate : bool, default: True If False, ``X`` is assumed to be correctly formatted (2-d float array, with points as rows and dimensions/features as columns, C-contiguous), and no checks are performed on it. Reduces overhead. Use only for repeated calls when the input is programmatically generated to be correct at each stage. Returns ------- y_std : array, shape = (n_samples,), optional Standard deviation of predictive distribution at query points. Only returned when return_std is True. """ self.n_eval += len(X) if validate: X = np.atleast_2d(X) if self.gpr.kernel is None or self.gpr.kernel.requires_vector_input: X = check_array(X, ensure_2d=True, dtype="numeric") else: X = check_array(X, ensure_2d=False, dtype=None) # Create placeholders with default values X = np.copy(X) # copy since preprocessors might change it n_samples = X.shape[0] X_ = self.preprocessing_X.transform(X) std = np.zeros(n_samples) # std is zero when mu is -inf # First check if either SVM or the trust region say that the value should be -inf finite = self.infinities_classifier.is_finite_X( X_, ignore=ignore_classifier, validate=validate ) # If all values are infinite no need to run the prediction through the GP if np.all(~finite): return std # else: at least some finite points --> GPR predict for them only std_ = self.gpr.predict_std(X_[finite], validate=validate) std[finite] = self.preprocessing_y.inverse_transform_scale(std_) return std
[docs] def fraction_prior_finite(self, n_prior="100d"): """ Returns the faction of the prior that is classified as finite, assuming uniform density. Performs the calculation by drawing and classifying ``n`` uniform prior samples. Parameters ---------- n_prior: int, str Number of samples to be drawn from the prior for getting the finite fraction. Returns ------- fraction_prior_finite: float The fraction of the prior volume classified as finite. """ if not self.infinities_classifier: return 1 n_prior = get_Xnumber(n_prior, "d", X_value=self.d, dtype=int, varname="n_prior") X = self.gpr.random_state.uniform( self.bounds[:, 0], self.bounds[:, 1], (n_prior, self.d) ) X_ = self.preprocessing_X.transform(X) n_finite = np.sum(self.infinities_classifier.is_finite_X(X_, validate=False)) return n_finite / n_prior
[docs] class Clipper: """ Handles the upper clipping of the y-output of the regressor, as a factor of the difference in its range. """ def __init__(self, clip_factor): if clip_factor is not None and clip_factor < 1: raise ValueError("'clip_factor' must be >= 1, or None for no clippling.") self.clip_factor = clip_factor @property def trivial(self): return self.clip_factor is None def __call__(self, y, y_min, y_max=None): if self.trivial: return y upper = self.clip_factor * y_max - (self.clip_factor - 1) * y_min return np.clip(y, None, upper)