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)