Source code for sklvq.distances._adaptive_squared_euclidean

from . import DistanceBaseClass

import numpy as np
from scipy.spatial.distance import cdist

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from ..models import GMLVQ


[docs]class AdaptiveSquaredEuclidean(DistanceBaseClass): """Adaptive squared Euclidean distance Class that holds the adaptive squared Euclidean distance function and its gradient as described in `[1]`_ and `[2]`_. Parameters ---------- force_all_finite : {True, False, "allow-nan"} Parameter to indicate that NaNLVQ distance variant should be used. If true no nans are allowed. If False or "allow-nan", nans are allowed. See also -------- Euclidean, SquaredEuclidean, LocalAdaptiveSquaredEuclidean Notes ----- Compatible with the :class:`.GMLVQ` algorithm (only). References ---------- _`[1]` Schneider, P. (2010). Advanced methods for prototype-based classification. Groningen. _`[2]` Schneider, P., Biehl, M., & Hammer, B. (2009). Adaptive Relevance Matrices in Learning Vector Quantization. Neural Computation, 21(12), 3532–3561.""" __slots__ = ()
[docs] def __call__(self, data: np.ndarray, model: "GMLVQ") -> np.ndarray: r"""Computes the adaptive squared Euclidean distance: .. math:: d^{\Lambda}(\mathbf{w}, \mathbf{x}) = (\mathbf{x} - \mathbf{w})^{\top} \Lambda (\mathbf{x} - \mathbf{w}) with the relevance matrix :math:`\Lambda = \Omega^{\top} \Omega`, the prototype :math:`\mathbf{w}`, and sample :math:`\mathbf{x}`. Parameters ---------- data : ndarray with shape (n_samples, n_features) The data for which the distance gradient to the prototypes of the model need to be computed. model : GMLVQ The model instance, containing the prototypes and relevance matrix. Returns ------- ndarray with shape (n_samples, n_prototypes) Evaluation of the distance between each sample in the data and prototype of the model. """ if model.force_all_finite == "allow-nan": # RM because VI is filtered out of the kwargs by cdist... return cdist( data, model.prototypes_, _nan_mahalanobis, RM=model._compute_lambda(model.omega_), ) return ( cdist( data, model.prototypes_, "mahalanobis", VI=model._compute_lambda(model.omega_), ) ** 2 )
[docs] def gradient( self, data: np.ndarray, model: "GMLVQ", i_prototype: int ) -> np.ndarray: r"""Computes the gradient of the adaptive squared euclidean distance function, with respect to a single prototype: .. math:: \frac{\partial d}{\partial \mathbf{w_i}} = -2 \Lambda (\mathbf{x} - \mathbf{w_i}), and the omega matrix (per element): .. math:: \frac{\partial d}{\partial \Omega_{lm}} = 2 \sum_i (x^i - w^i) \Omega_{li} (x^m - w^m) Parameters ---------- data : ndarray with shape (n_samples, n_features) The data for which the distance gradient to the prototypes of the model need to be computed. model : GMLVQ The model instance, containing the prototypes and relevance matrix. i_prototype : int An integer index value of the relevant prototype Returns ------- ndarray with shape (n_samples, n_features + n_omega_elements) The gradient of the prototype and omega matrix with respect to each data sample. """ (prototypes, omega) = model.get_model_params() prototype = prototypes[i_prototype, :] (num_samples, _) = data.shape distance_gradient = np.empty( (num_samples, prototype.size + omega.size), dtype="float64", order="C" ) difference = data - prototype if model.force_all_finite == "allow-nan": difference[np.isnan(difference)] = 0.0 # Prototype gradient directly computed in the distance_gradient _prototype_gradient( difference, omega, out=distance_gradient[:, : prototype.size] ) # Omega view created in to hold the shape the _omega_gradient functions needs to output in. distance_gradient_omega_view = distance_gradient[:, prototype.size :].reshape( (num_samples, *omega.shape) ) # Omega gradient indirectly computed in the distance_gradient via the view of the memory # in a different shape. _omega_gradient(difference, omega, out=distance_gradient_omega_view) return distance_gradient
def _nan_mahalanobis(sample, prototype, RM=None): # The NaNLVQ variant of the mahalanobis distance difference = sample - prototype difference[np.isnan(difference)] = 0.0 return difference.dot(RM).dot(difference.T) # return np.einsum("i, ij, j ->", difference, relevance_matrix, difference) def _prototype_gradient( difference: np.ndarray, omega: np.ndarray, out=None ) -> np.ndarray: # The gradient with respect to a prototype. Equivalent to np.dot(-2.0 * difference.dot( # omega.T.dot(omega)). return np.einsum("ji,ik ->jk", -2.0 * difference, np.dot(omega.T, omega), out=out) def _omega_gradient(difference: np.ndarray, omega: np.ndarray, out=None) -> np.ndarray: # The gradient with respect to the omega matrix. return np.einsum( "ij,jk->jik", np.dot(omega, difference.T), (2.0 * difference), out=out )