Source code for sklvq.distances._local_adaptive_squared_euclidean

from . import DistanceBaseClass

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

from ._adaptive_squared_euclidean import (
    _nan_mahalanobis,
    _prototype_gradient,
    _omega_gradient,
)

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from ..models import LGMLVQ


[docs]class LocalAdaptiveSquaredEuclidean(DistanceBaseClass): """Local adaptive squared Euclidean distance Class that holds the localized 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, AdaptiveSquaredEuclidean Notes ----- Compatible with the :class:`.LGMLVQ` 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: "LGMLVQ") -> np.ndarray: r"""Computes the local variant of the adaptive squared Euclidean distance: .. math:: d^{\Lambda}(\mathbf{w}, \mathbf{x}) = (\mathbf{x} - \mathbf{w})^{\top} \Omega_j^{\top} \Omega_j (\mathbf{x} - \mathbf{w}) with :math:`\Omega_j` depending on the localization setting of the model and :math:`\Lambda_j = \Omega_j^{\top} \Omega_j`. The localization can be either per prototype or per class, see the documentation of :class:`.LGMLVQ`. 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 : LGMLVQ A LGMLVQ model instance, containing the prototypes and relevance matrices. Returns ------- ndarray with shape (n_samples, n_prototypes) Evaluation of the distance between each sample in the data and prototype of the model. """ prototypes_, omegas_ = model.get_model_params() prototypes_labels_ = model.prototypes_labels_ distance_function = "mahalanobis" kwarg_str = "VI" if model.force_all_finite == "allow-nan": distance_function = _nan_mahalanobis # RM because VI is filtered out of the kwargs by cdist... kwarg_str = "RM" cdists = np.zeros((data.shape[0], model._prototypes_shape[0])) if model.relevance_localization == "prototypes": for i, (prototype, omega) in enumerate(zip(prototypes_, omegas_)): cdists[:, i] = cdist( data, np.atleast_2d(prototype), distance_function, **{kwarg_str: model._compute_lambda(omega)}, ).squeeze() if model.relevance_localization == "class": for i, omega in enumerate(omegas_): # Prototype labels are indices to model.classes_ so all prototypes with 'index' # i as label have the same class. prototypes = prototypes_[i == prototypes_labels_, :] cdists[:, i == prototypes_labels_] = cdist( data, np.atleast_2d(prototypes), distance_function, **{kwarg_str: model._compute_lambda(omega)}, ) return cdists ** 2
[docs] def gradient( self, data: np.ndarray, model: "LGMLVQ", i_prototype: int ) -> np.ndarray: r"""Computes the gradient of the localized adaptive squared euclidean distance function with respect to a specified prototype: .. math:: \frac{\partial d}{\partial \mathbf{w}_i} = -2 \Lambda_j (\mathbf{x} - \mathbf{w}_i) and implicitly coupled 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 X for which the distance gradient to the prototypes of the model need to be computed. model : LGMLVQ The LGMLVQ model instance, containing the prototypes and relevance matrices. 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, omegas) = model.get_model_params() prototype = prototypes[i_prototype, :] omega = omegas[model.prototypes_labels_[i_prototype], :, :] num_samples, _ = data.shape distance_gradient = np.empty( (num_samples, prototype.size + omega.size), dtype="float64", order="C" ) # Difference we need for both the prototype and omega part. difference = data - prototype if model.force_all_finite == "allow-nan": difference[np.isnan(difference)] = 0.0 _prototype_gradient( difference, omega, out=distance_gradient[:, : prototype.size] ) distance_gradient_omega_view = distance_gradient[:, prototype.size :].reshape( (num_samples, *omega.shape) ) # Omega gradient part _omega_gradient(difference, omega, out=distance_gradient_omega_view) return distance_gradient