from typing import List, Optional, Union import numpy as np import tensorflow as tf from ..base import Parameter from ..utilities import positive from ..utilities.ops import difference_matrix from .base import Kernel from .stationaries import Stationary, IsotropicStationary class Periodic(Kernel): """ The periodic family of kernels. Can be used to wrap any Stationary kernel to transform it into a periodic version. The canonical form (based on the SquaredExponential kernel) can be found in Equation (47) of D.J.C.MacKay. Introduction to Gaussian processes. In C.M.Bishop, editor, Neural Networks and Machine Learning, pages 133--165. Springer, 1998. The derivation can be achieved by mapping the original inputs through the transformation u = (cos(x), sin(x)). For the SquaredExponential base kernel, the result can be expressed as: k(r) = σ² exp{ -0.5 sin²(π r / γ) / ℓ²} where: r is the Euclidean distance between the input points ℓ is the lengthscales parameter, σ² is the variance parameter, γ is the period parameter. NOTE: usually we have a factor of 4 instead of 0.5 in front but this is absorbed into the lengthscales hyperparameter. NOTE: periodic kernel uses `active_dims` of a base kernel, therefore the constructor doesn't have it as an argument. """ def __init__(self, base_kernel: IsotropicStationary, period: Union[float, List[float]] = 1.0): """ :param base_kernel: the base kernel to make periodic; must inherit from Stationary Note that `active_dims` should be specified in the base kernel. :param period: the period; to induce a different period per active dimension this must be initialized with an array the same length as the number of active dimensions e.g. [1., 1., 1.] """ if not isinstance(base_kernel, IsotropicStationary): raise TypeError("Periodic requires an IsotropicStationary kernel as the `base_kernel`") super().__init__() self.base_kernel = base_kernel self.period = Parameter(period, transform=positive()) self.base_kernel._validate_ard_active_dims(self.period) @property def active_dims(self): return self.base_kernel.active_dims @active_dims.setter def active_dims(self, value): self.base_kernel.active_dims = value def K_diag(self, X: tf.Tensor) -> tf.Tensor: return self.base_kernel.K_diag(X) def K(self, X: tf.Tensor, X2: Optional[tf.Tensor] = None) -> tf.Tensor: r = np.pi * (difference_matrix(X, X2)) / self.period scaled_sine = tf.sin(r) / self.base_kernel.lengthscales if hasattr(self.base_kernel, "K_r"): sine_r = tf.reduce_sum(tf.abs(scaled_sine), -1) K = self.base_kernel.K_r(sine_r) else: sine_r2 = tf.reduce_sum(tf.square(scaled_sine), -1) K = self.base_kernel.K_r2(sine_r2) return K