Source code for secml.ml.features.normalization.c_normalizer_unitnorm

"""
.. module:: CNormalizerUnitNorm
   :synopsis: Normalize patterns individually to unit norm.

.. moduleauthor:: Ambra Demontis <ambra.demontis@unica.it>
.. moduleauthor:: Battista Biggio <battista.biggio@unica.it>

"""
from secml.array import CArray
from secml.ml.features.normalization import CNormalizer
from secml.core.constants import inf


[docs]class CNormalizerUnitNorm(CNormalizer): """Normalize patterns individually to unit norm. Each pattern (i.e. each row of the data matrix) with at least one non zero component is rescaled independently of other patterns so that its norm (l1 or l2 or max) equals one. For the Row normalizer, no training routine is needed, so using fit_normalize() method is suggested for clarity. Use fit() method, which does nothing, only to streamline a pipelined environment. Parameters ---------- norm : {'l1', 'l2', 'max'}, optional Order of the norm to normalize each pattern with.'l2' is the default. preprocess : CPreProcess or str or None, optional Features preprocess to be applied to input data. Can be a CPreProcess subclass or a string with the type of the desired preprocessor. If None, input data is used as is. Attributes ---------- class_type : 'unit-norm' Notes ----- Differently from numpy, we manage flat vectors as 2-Dimensional of shape (1, array.size). This means that normalizing a flat vector is equivalent to transform array.atleast_2d(). To obtain a numpy-style normalization of flat vectors, transpose array first. Examples -------- >>> from secml.array import CArray >>> from secml.ml.features.normalization import CNormalizerUnitNorm >>> array = CArray([[1., -1., 2.], [2., 0., 0.], [0., 1., -1.]]) >>> dense_normalized = CNormalizerUnitNorm(norm="l2").fit_transform(array) >>> print(dense_normalized) CArray([[ 0.408248 -0.408248 0.816497] [ 1. 0. 0. ] [ 0. 0.707107 -0.707107]]) >>> dense_normalized =(CNormalizerUnitNorm(norm="l1").fit_transform(array)) >>> print(dense_normalized) CArray([[ 0.25 -0.25 0.5 ] [ 1. 0. 0. ] [ 0. 0.5 -0.5 ]]) >>> print(array / array.norm_2d(order=1, axis=1, keepdims=True)) CArray([[ 0.25 -0.25 0.5 ] [ 1. 0. 0. ] [ 0. 0.5 -0.5 ]]) """ __class_type = 'unit-norm' def __init__(self, norm="l2", preprocess=None): """Class constructor""" self._order = None self.norm = norm super(CNormalizerUnitNorm, self).__init__(preprocess=preprocess) @property def norm(self): """Return the norm of each training array's patterns.""" return self._norm @norm.setter def norm(self, value): """Set the norm that must be used to normalize each row.""" self._norm = value if self._norm == 'l2': self._order = 2 elif self._norm == 'l1': self._order = 1 elif self._norm == "max": self._order = inf else: raise ValueError("unknown norm") def _check_is_fitted(self): """Check if the preprocessor is trained (fitted). Raises ------ NotFittedError If the preprocessor is not fitted. """ pass # This preprocessor does not require training def _fit(self, x, y=None): """Fit the normalizer. For the Row normalizer, no training routine is needed, so using fit_transform() method is suggested for clarity. Use fit() method, which does nothing, only to streamline a pipelined environment. Parameters ---------- x : CArray Array to be used as training set. Each row must correspond to one different pattern. y : CArray or None, optional Flat array with the label of each pattern. Can be None if not required by the preprocessing algorithm. Returns ------- CNormalizerRow Instance of the trained normalizer. """ return self def _compute_x_norm(self, x): """Compute the norm of x: ||x||.""" x_norm = x.norm_2d(axis=1, keepdims=True, order=self._order) x_norm[x_norm == 0] = 1 # to avoid nan values return x_norm def _compute_norm_gradient(self, x, x_norm): """Compute the gradient of the chosen norm on x. Parameters ---------- x : CArray The input sample. x_norm : CArray Array containing its pre-computed norm ||x||. Returns ------- CArray The derivative d||x||/dx of the chosen norm. """ d = x.size # number of features if self.norm == "l2": grad_norm_x = x / x_norm elif self.norm == "l1": sign = x.sign() grad_norm_x = sign elif self.norm == 'max': grad_norm_x = CArray.zeros(d, sparse=x.issparse) abs_x = x.abs() # take absolute values of x... max_abs_x = abs_x.max() # ... and the maximum absolute value max_abs_x -= 1e-8 # add small tolerance max_idx = abs_x >= max_abs_x # find idx of maximum values grad_norm_x[max_idx] = x[max_idx].sign() else: raise ValueError("Unsupported norm.") # return the gradient of ||x|| return grad_norm_x def _forward(self, x): """Transform array patterns to have unit norm. Parameters ---------- x : CArray Array to be normalized, 2-Dimensional. Returns ------- CArray Array with patterns normalized to have unit norm. Examples -------- >>> from secml.array import CArray >>> from secml.ml.features.normalization import CNormalizerUnitNorm >>> array = CArray([[1., -1., 2.], [2., 0., 0.], [0., 1., -1.]]) >>> array = array.tosparse() >>> normalizer = CNormalizerUnitNorm().fit(array) >>> array_normalized = normalizer.transform(array) >>> print(array_normalized) # doctest: +NORMALIZE_WHITESPACE CArray([[ 0.408248 -0.408248 0.816497] [ 1. 0. 0. ] [ 0. 0.707107 -0.707107]]) >>> print(array_normalized.todense().norm_2d(order=2, axis=1)) CArray([[1.] [1.] [1.]]) """ x_norm = self._compute_x_norm(x) # fixme: if you do x/x_norm with x sparse, result is dense # this needs patching in CArray return 1.0 / x_norm * x def _backward(self, w=None): """ Compute the gradient w.r.t. the input cached during the forward pass. Parameters ---------- w : CArray or None, optional If CArray, will be left-multiplied to the gradient of the preprocessor. Returns ------- gradient : CArray Gradient of the normalizer wrt input data. it will have dimensionality shape (w.shape[0], x.shape[1]) if `w` is passed as input (x.shape[1], x.shape[1]) otherwise. """ x = self._cached_x d = self._cached_x.size # get the number of features # compute the norm of x: ||x|| x_norm = self._compute_x_norm(x) # compute the gradient of the given norm: d||x||/dx grad_norm_x = self._compute_norm_gradient(x, x_norm) # this is the derivative of the ratio x/||x|| grad = CArray.eye(d, d) * x_norm.item() - grad_norm_x.T.dot(x) grad /= (x_norm ** 2) return grad if w is None else w.dot(grad)