"""
.. module:: CKernelEuclidean
:synopsis: Euclidean distance kernel.
.. moduleauthor:: Marco Melis <marco.melis@unica.it>
.. moduleauthor:: Battista Biggio <battista.biggio@unica.it>
.. moduleauthor:: Angelo Sotgiu <angelo.sotgiu@unica.it>
"""
from sklearn import metrics
from secml.array import CArray
from secml.ml.kernels import CKernel
[docs]class CKernelEuclidean(CKernel):
"""Euclidean distance kernel.
Given matrices X and RV, this is computed as the negative Euclidean dist.::
K(x, rv) = -sqrt(dot(x, x) - 2 * dot(x, rv) + dot(rv, rv))
for each pair of rows in X and in RV.
If parameter squared is True (default False), sqrt() operation is avoided.
Parameters
----------
squared : bool, optional
If True, return squared Euclidean distances. Default False.
Attributes
----------
class_type : 'euclidean'
Examples
--------
>>> from secml.array import CArray
>>> from secml.ml.kernels.c_kernel_euclidean import CKernelEuclidean
>>> print(CKernelEuclidean().k(CArray([[1,2],[3,4]]), CArray([[10,20],[30,40]])))
CArray([[-20.124612 -47.801674]
[-17.464249 -45. ]])
>>> print(CKernelEuclidean().k(CArray([[1,2],[3,4]])))
CArray([[0. -2.828427]
[-2.828427 0. ]])
"""
__class_type = 'euclidean'
def __init__(self, squared=False):
self._squared = squared
self._x_norm_squared = None
self._rv_norm_squared = None
super(CKernelEuclidean, self).__init__()
@property
def squared(self):
"""If True, squared Euclidean distances are computed."""
return self._squared
@squared.setter
def squared(self, value):
"""Sets the squared parameter.
Parameters
----------
value : bool
If True, squared Euclidean distances are computed.
"""
self._squared = value
@property
def x_norm_squared(self):
"""Pre-computed dot-products of vectors in x
(e.g., (x**2).sum(axis=1)).
"""
return self._x_norm_squared
@x_norm_squared.setter
def x_norm_squared(self, value):
"""Sets the pre-computed dot-products of vectors in x.
Parameters
----------
value : CArray
Pre-computed dot-products of vectors in x.
"""
self._x_norm_squared = value
@property
def rv_norm_squared(self):
"""Pre-computed dot-products of vectors in rv
(e.g., (rv**2).sum(axis=1)).
"""
return self._rv_norm_squared
@rv_norm_squared.setter
def rv_norm_squared(self, value):
"""Sets the pre-computed dot-products of vectors in rv.
Parameters
----------
value : CArray
Pre-computed dot-products of vectors in rv.
"""
self._rv_norm_squared = value
def _forward(self, x):
"""Compute this kernel as the negative Euclidean dist. between x and
cached rv.
Parameters
----------
x : CArray
Array of shape (n_x, n_features).
Returns
-------
kernel : CArray
Kernel between x and cached rv, shape (n_x, n_rv).
"""
k = -CArray(metrics.pairwise.euclidean_distances(
x.get_data(), self._rv.get_data(), squared=self._squared,
X_norm_squared=self._x_norm_squared,
Y_norm_squared=self._rv_norm_squared))
self._cached_kernel = None if self._cached_x is None or self._squared \
else k
return k
def _backward(self, w=None):
"""Compute the kernel gradient wrt cached vector 'x'.
The gradient of Euclidean distance kernel is given by::
dK(rv,x)/dx = - (rv - x) / k(rv,x) if squared = False (default)
dK(rv,x)/dx = 2 * (rv - x) if squared = True
Parameters
----------
w : CArray of shape (1, n_rv) or None
if CArray, it is pre-multiplied to the gradient
of the module, as in standard reverse-mode autodiff.
Returns
-------
kernel_gradient : CArray
Kernel gradient of rv with respect to vector x,
shape (n_rv, n_features) if n_rv > 1 and w is None,
else (1, n_features).
"""
# Checking if cached x is a vector
if not self._cached_x.is_vector_like:
raise ValueError(
"kernel gradient can be computed only wrt vector-like arrays.")
if self._rv is None or (not self._squared
and self._cached_kernel is None):
raise ValueError("Please run forward with caching=True first.")
# Format of output array should be the same as cached x
self._rv = self._rv.tosparse() if self._cached_x.issparse \
else self._rv.todense()
if self._squared is True: # 2 * (rv - x)
diff = (self._rv - self._cached_x)
return 2 * diff if w is None else w.dot(2 * diff)
diff = (self._rv - self._cached_x)
k_grad = self._cached_kernel.T
k_grad[k_grad == 0] = 1.0 # To avoid nans later
# Casting the kernel to sparse if needed for efficient broadcasting
if diff.issparse is True:
k_grad = k_grad.tosparse()
# - (rv - x) / k(rv,x)
grad = -diff / k_grad
grad = grad if w is None else w.dot(grad)
# Casting to sparse if necessary
return grad.tosparse() if diff.issparse else grad
[docs] def gradient(self, x, w=None):
"""Compute gradient at x by doing a forward and a backward pass.
The gradient is pre-multiplied by w.
"""
self._cached_kernel = None
self.forward(x)
return self.backward(w)