Source code for scm.glompo.analysis.kernels

from abc import abstractmethod

import numpy as np

from ..core.nosettingsobject import NoSettingsObject

__all__ = ("ThresholdKernel", "ConjunctiveGaussianKernel", "GaussianKernel", "PolynomialKernel", "LinearKernel")


[docs]class BaseKernel(NoSettingsObject): PARAMETERS = set() def __str__(self) -> str: args = "" for k, v in self.__dict__.items(): args += f"{k}={v}," return type(self).__name__ + "(" + args[:-1] + ")"
[docs] def __call__(self, *args): """Returns the kernel value or the derivative depending on the number of arguments. :See Also: :meth:`compute` :meth:`derivative` """ if len(args) == 1: calc = self.compute(*args) else: calc = self.derivative(*args) calc[np.diag_indices_from(calc)] = 0 return calc
[docs] @abstractmethod def compute(self, data: np.ndarray) -> np.ndarray: """Returns the kernel value given a set of data. :Parameters: data Vector of data onto which the kernel will be applied. The kernel will be applied to the outer combination of ``data`` elements. """
[docs] def derivative(self, y: np.ndarray, dy: np.ndarray) -> np.ndarray: """Returns the first derivative of the kernel with respect to the weights used in the loss function. :Parameters: y Vector of data points. The outer combination of all points will be calculated. dy First derivative of ``y`` with respect to the weights used in the loss function. """ raise NotImplementedError(f"{self.__class__.__name__} does not have a derivative.")
[docs]class ThresholdKernel(BaseKernel): r""" Simple linear kernel which first toggles the data to zero or one. Places the distribution over the smallest function values. .. math:: g(x) = \begin{cases} 1 &\text{ if } x < q_\alpha \\ 0 &\text{ if } x \geq q_\alpha \\ \end{cases} .. math:: k(x, y) = g(x) g(y) .. figure:: ../_static/threshold.png :width: 50% :align: center :Parameter: alpha Sets all values below the ``alpha`` quantile in ``data`` to 1 and all those above to 0 (:math:`0 < \alpha < 1`). :References: Spagnol, A., Riche, R. Le, & Veiga, S. Da. (2019). Global Sensitivity Analysis for Optimization with Variable Selection. SIAM/ASA Journal on Uncertainty Quantification, 7(2), 417–443. https://doi.org/10.1137/18M1167978 """ PARAMETERS = {"alpha"} def __init__(self, alpha: float): self.alpha = alpha def compute(self, data: np.ndarray) -> np.ndarray: thresholded = (data < self.alpha).astype(int) calc = np.multiply.outer(thresholded, thresholded) return calc
[docs]class ConjunctiveGaussianKernel(BaseKernel): r"""Sigmoid type kernel, similar to the :class:`.ThresholdKernel`, but continuous. Places the bulk of the distribution on the ``data`` points with the lowest values. .. math:: k(x, y) = \exp\left[ - \frac{x^2 + y^2}{2\gamma^2} \right] .. figure:: ../_static/conjunctivegaussian.png :width: 50% :align: center :Parameters: gamma Higher values extend the distribution over more points (:math:`0 < \gamma < 1`). """ PARAMETERS = {"gamma"} def __str__(self) -> str: return f"ConjunctiveGaussianKernel(gamma={self.gamma})" @property def gamma(self) -> float: return self._gamma @gamma.setter def gamma(self, value): self._gamma = value self._phi = -0.5 * value**-2 def __init__(self, gamma: float = 0.1): self.gamma = gamma def compute(self, data: np.ndarray) -> np.ndarray: sq = data**2 calc = np.add.outer(sq, sq) np.multiply(calc, self._phi, out=calc) np.exp(calc, out=calc) return calc def derivative(self, y: np.ndarray, dy: np.ndarray) -> np.ndarray: calc = self.compute(y) np.multiply(calc, 2 * self._phi, out=calc) term = np.multiply.outer(dy, y) np.add(term, term.T, out=term) np.multiply(calc, term, out=calc) return calc
[docs]class GaussianKernel(BaseKernel): r"""Traditional Gaussian kernel used to measure the similarity between inputs. .. math:: k(x, y) = \exp\left[ - \frac{(x - y)^2}{2\sigma^2} \right] .. figure:: ../_static/gaussian.png :width: 50% :align: center :Parameters: sigma Normal distribution bandwidth parameter (:math:`0 < \sigma < 1`) """ PARAMETERS = {"sigma"} def __str__(self) -> str: return f"GaussianKernel(sigma={self.sigma})" @property def sigma(self) -> float: return self._sigma @sigma.setter def sigma(self, value): self._sigma = value self._phi = -0.5 * value**-2 def __init__(self, sigma: float = 0.3): self.sigma = sigma def compute(self, data: np.ndarray) -> np.ndarray: calc = np.subtract.outer(data, data) np.power(calc, 2, out=calc) np.multiply(calc, self._phi, out=calc) np.exp(calc, out=calc) return calc def derivative(self, y: np.ndarray, dy: np.ndarray) -> np.ndarray: y1y2 = np.subtract.outer(y, y) d1d2 = np.subtract.outer(dy, dy) calc = np.exp(self._phi * y1y2**2) np.multiply(calc, y1y2, out=calc) np.multiply(calc, d1d2, out=calc) np.multiply(calc, 2 * self._phi, out=calc) return calc
[docs]class PolynomialKernel(BaseKernel): r"""Polynomial kernel which places the distribution bulk on the highest ``data`` values. .. math:: k(x, y) = (x y + c)^d .. figure:: ../_static/polynomial.png :width: 100% :align: center :Parameters: order Degree (:math:`d`) of the polynomial function (:math:`d \geq 1`) shift Vertical shift (:math:`c`) of the distribution to temper the effects of ``order`` """ PARAMETERS = {"order", "shift"} def __init__(self, order: int = 1, shift: float = 0): self.order = order self.shift = shift def compute(self, data: np.ndarray) -> np.ndarray: calc = np.multiply.outer(data, data) np.add(calc, self.shift, out=calc) np.power(calc, self.order, out=calc) return calc def derivative(self, y: np.ndarray, dy: np.ndarray) -> np.ndarray: calc = np.multiply.outer(y, y) np.add(calc, self.shift, out=calc) np.power(calc, self.order - 1, out=calc) np.multiply(calc, self.order, out=calc) term = np.multiply.outer(y, dy) np.add(term, term.T, out=term) np.multiply(calc, term, out=calc) return calc
[docs]class LinearKernel(PolynomialKernel): r"""Special instance of :class:`.PolynomialKernel` of degree 1 and shift 0. .. figure:: ../_static/linear.png :width: 50% :align: center .. math:: k(x, y) = x y """ PARAMETERS = set() def __str__(self) -> str: return f"LinearKernel()" def __init__(self): super().__init__(1, 0)