From 120ae377c893309f5706b246591705eb8d9e99d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= Date: Mon, 7 Jan 2019 18:04:36 +0100 Subject: [PATCH 1/3] move method to util and update classes accordingly --- metric_learn/_util.py | 27 +++++++++++++++++++++++++++ metric_learn/base_metric.py | 27 --------------------------- metric_learn/covariance.py | 5 +++-- metric_learn/itml.py | 8 ++++---- metric_learn/lsml.py | 7 ++++--- metric_learn/mmc.py | 10 +++++----- metric_learn/sdml.py | 7 ++++--- 7 files changed, 47 insertions(+), 44 deletions(-) diff --git a/metric_learn/_util.py b/metric_learn/_util.py index 27707be9..09906bc6 100644 --- a/metric_learn/_util.py +++ b/metric_learn/_util.py @@ -322,3 +322,30 @@ def check_collapsed_pairs(pairs): raise ValueError("{} collapsed pairs found (where the left element is " "the same as the right element), out of {} pairs " "in total.".format(num_ident, pairs.shape[0])) + + +def transformer_from_metric(self, metric): + """Computes the transformation matrix from the Mahalanobis matrix. + + Since by definition the metric `M` is positive semi-definite (PSD), it + admits a Cholesky decomposition: L = cholesky(M).T. However, currently the + computation of the Cholesky decomposition used does not support + non-definite matrices. If the metric is not definite, this method will + return L = V.T w^( -1/2), with M = V*w*V.T being the eigenvector + decomposition of M with the eigenvalues in the diagonal matrix w and the + columns of V being the eigenvectors. If M is diagonal, this method will + just return its elementwise square root (since the diagonalization of + the matrix is itself). + + Returns + ------- + L : (d x d) matrix + """ + + if np.allclose(metric, np.diag(np.diag(metric))): + return np.sqrt(metric) + elif not np.isclose(np.linalg.det(metric), 0): + return np.linlalg.cholesky(metric).T + else: + w, V = np.linalg.eigh(metric) + return V.T * np.sqrt(np.maximum(0, w[:, None])) diff --git a/metric_learn/base_metric.py b/metric_learn/base_metric.py index 9af79ecc..bfec1264 100644 --- a/metric_learn/base_metric.py +++ b/metric_learn/base_metric.py @@ -1,4 +1,3 @@ -from numpy.linalg import cholesky from sklearn.base import BaseEstimator from sklearn.utils.validation import _is_arraylike from sklearn.metrics import roc_auc_score @@ -181,32 +180,6 @@ def transform(self, X): def metric(self): return self.transformer_.T.dot(self.transformer_) - def transformer_from_metric(self, metric): - """Computes the transformation matrix from the Mahalanobis matrix. - - Since by definition the metric `M` is positive semi-definite (PSD), it - admits a Cholesky decomposition: L = cholesky(M).T. However, currently the - computation of the Cholesky decomposition used does not support - non-definite matrices. If the metric is not definite, this method will - return L = V.T w^( -1/2), with M = V*w*V.T being the eigenvector - decomposition of M with the eigenvalues in the diagonal matrix w and the - columns of V being the eigenvectors. If M is diagonal, this method will - just return its elementwise square root (since the diagonalization of - the matrix is itself). - - Returns - ------- - L : (d x d) matrix - """ - - if np.allclose(metric, np.diag(np.diag(metric))): - return np.sqrt(metric) - elif not np.isclose(np.linalg.det(metric), 0): - return cholesky(metric).T - else: - w, V = np.linalg.eigh(metric) - return V.T * np.sqrt(np.maximum(0, w[:, None])) - class _PairsClassifierMixin(BaseMetricLearner): diff --git a/metric_learn/covariance.py b/metric_learn/covariance.py index 10bc9582..7a04923d 100644 --- a/metric_learn/covariance.py +++ b/metric_learn/covariance.py @@ -13,6 +13,7 @@ from sklearn.base import TransformerMixin from .base_metric import MahalanobisMixin +from ._util import transformer_from_metric class Covariance(MahalanobisMixin, TransformerMixin): @@ -22,7 +23,7 @@ class Covariance(MahalanobisMixin, TransformerMixin): ---------- transformer_ : `numpy.ndarray`, shape=(num_dims, n_features) The linear transformation ``L`` deduced from the learned Mahalanobis - metric (See :meth:`transformer_from_metric`.) + metric (See function `transformer_from_metric`.) """ def __init__(self, preprocessor=None): @@ -40,5 +41,5 @@ def fit(self, X, y=None): else: M = np.linalg.inv(M) - self.transformer_ = self.transformer_from_metric(np.atleast_2d(M)) + self.transformer_ = transformer_from_metric(np.atleast_2d(M)) return self diff --git a/metric_learn/itml.py b/metric_learn/itml.py index 8a251fe0..158ec4d3 100644 --- a/metric_learn/itml.py +++ b/metric_learn/itml.py @@ -22,7 +22,7 @@ from sklearn.base import TransformerMixin from .base_metric import _PairsClassifierMixin, MahalanobisMixin from .constraints import Constraints, wrap_pairs -from ._util import vector_norm +from ._util import vector_norm, transformer_from_metric class _BaseITML(MahalanobisMixin): @@ -125,7 +125,7 @@ def _fit(self, pairs, y, bounds=None): print('itml converged at iter: %d, conv = %f' % (it, conv)) self.n_iter_ = it - self.transformer_ = self.transformer_from_metric(self.A_) + self.transformer_ = transformer_from_metric(self.A_) return self @@ -136,7 +136,7 @@ class ITML(_BaseITML, _PairsClassifierMixin): ---------- transformer_ : `numpy.ndarray`, shape=(num_dims, n_features) The linear transformation ``L`` deduced from the learned Mahalanobis - metric (See :meth:`transformer_from_metric`.) + metric (See function `transformer_from_metric`.) """ def fit(self, pairs, y, bounds=None): @@ -169,7 +169,7 @@ class ITML_Supervised(_BaseITML, TransformerMixin): ---------- transformer_ : `numpy.ndarray`, shape=(num_dims, n_features) The linear transformation ``L`` deduced from the learned Mahalanobis - metric (See `transformer_from_metric`.) + metric (See function `transformer_from_metric`.) """ def __init__(self, gamma=1., max_iter=1000, convergence_threshold=1e-3, diff --git a/metric_learn/lsml.py b/metric_learn/lsml.py index 9090a431..50fcfa3e 100644 --- a/metric_learn/lsml.py +++ b/metric_learn/lsml.py @@ -16,6 +16,7 @@ from .base_metric import _QuadrupletsClassifierMixin, MahalanobisMixin from .constraints import Constraints +from ._util import transformer_from_metric class _BaseLSML(MahalanobisMixin): @@ -101,7 +102,7 @@ def _fit(self, quadruplets, y=None, weights=None): print("Didn't converge after", it, "iterations. Final loss:", s_best) self.n_iter_ = it - self.transformer_ = self.transformer_from_metric(self.M_) + self.transformer_ = transformer_from_metric(self.M_) return self def _comparison_loss(self, metric): @@ -137,7 +138,7 @@ class LSML(_BaseLSML, _QuadrupletsClassifierMixin): ---------- transformer_ : `numpy.ndarray`, shape=(num_dims, n_features) The linear transformation ``L`` deduced from the learned Mahalanobis - metric (See :meth:`transformer_from_metric`.) + metric (See function `transformer_from_metric`.) """ def fit(self, quadruplets, weights=None): @@ -170,7 +171,7 @@ class LSML_Supervised(_BaseLSML, TransformerMixin): ---------- transformer_ : `numpy.ndarray`, shape=(num_dims, n_features) The linear transformation ``L`` deduced from the learned Mahalanobis - metric (See :meth:`transformer_from_metric`.) + metric (See function `transformer_from_metric`.) """ def __init__(self, tol=1e-3, max_iter=1000, prior=None, diff --git a/metric_learn/mmc.py b/metric_learn/mmc.py index 6d929d6e..b806a97e 100644 --- a/metric_learn/mmc.py +++ b/metric_learn/mmc.py @@ -25,7 +25,7 @@ from .base_metric import _PairsClassifierMixin, MahalanobisMixin from .constraints import Constraints, wrap_pairs -from ._util import vector_norm +from ._util import vector_norm, transformer_from_metric class _BaseMMC(MahalanobisMixin): @@ -206,7 +206,7 @@ def _fit_full(self, pairs, y): self.A_[:] = A_old self.n_iter_ = cycle - self.transformer_ = self.transformer_from_metric(self.A_) + self.transformer_ = transformer_from_metric(self.A_) return self def _fit_diag(self, pairs, y): @@ -267,7 +267,7 @@ def _fit_diag(self, pairs, y): self.A_ = np.diag(w) - self.transformer_ = self.transformer_from_metric(self.A_) + self.transformer_ = transformer_from_metric(self.A_) return self def _fD(self, neg_pairs, A): @@ -355,7 +355,7 @@ class MMC(_BaseMMC, _PairsClassifierMixin): ---------- transformer_ : `numpy.ndarray`, shape=(num_dims, n_features) The linear transformation ``L`` deduced from the learned Mahalanobis - metric (See :meth:`transformer_from_metric`.) + metric (See function `transformer_from_metric`.) """ def fit(self, pairs, y): @@ -386,7 +386,7 @@ class MMC_Supervised(_BaseMMC, TransformerMixin): ---------- transformer_ : `numpy.ndarray`, shape=(num_dims, n_features) The linear transformation ``L`` deduced from the learned Mahalanobis - metric (See :meth:`transformer_from_metric`.) + metric (See function `transformer_from_metric`.) """ def __init__(self, max_iter=100, max_proj=10000, convergence_threshold=1e-6, diff --git a/metric_learn/sdml.py b/metric_learn/sdml.py index b1421736..40fd5727 100644 --- a/metric_learn/sdml.py +++ b/metric_learn/sdml.py @@ -17,6 +17,7 @@ from .base_metric import MahalanobisMixin, _PairsClassifierMixin from .constraints import Constraints, wrap_pairs +from ._util import transformer_from_metric class _BaseSDML(MahalanobisMixin): @@ -68,7 +69,7 @@ def _fit(self, pairs, y): emp_cov = emp_cov.T.dot(emp_cov) _, self.M_ = graph_lasso(emp_cov, self.sparsity_param, verbose=self.verbose) - self.transformer_ = self.transformer_from_metric(self.M_) + self.transformer_ = transformer_from_metric(self.M_) return self @@ -79,7 +80,7 @@ class SDML(_BaseSDML, _PairsClassifierMixin): ---------- transformer_ : `numpy.ndarray`, shape=(num_dims, n_features) The linear transformation ``L`` deduced from the learned Mahalanobis - metric (See :meth:`transformer_from_metric`.) + metric (See function `transformer_from_metric`.) """ def fit(self, pairs, y): @@ -110,7 +111,7 @@ class SDML_Supervised(_BaseSDML, TransformerMixin): ---------- transformer_ : `numpy.ndarray`, shape=(num_dims, n_features) The linear transformation ``L`` deduced from the learned Mahalanobis - metric (See :meth:`transformer_from_metric`.) + metric (See function `transformer_from_metric`.) """ def __init__(self, balance_param=0.5, sparsity_param=0.01, use_cov=True, From 2468a78c1c8f90e1f5a376a48424c178df529881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= Date: Mon, 7 Jan 2019 18:18:44 +0100 Subject: [PATCH 2/3] remove forgotten self --- metric_learn/_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metric_learn/_util.py b/metric_learn/_util.py index 09906bc6..8b52a9d9 100644 --- a/metric_learn/_util.py +++ b/metric_learn/_util.py @@ -324,7 +324,7 @@ def check_collapsed_pairs(pairs): "in total.".format(num_ident, pairs.shape[0])) -def transformer_from_metric(self, metric): +def transformer_from_metric(metric): """Computes the transformation matrix from the Mahalanobis matrix. Since by definition the metric `M` is positive semi-definite (PSD), it From 9f78db574f13e0a8e9a0ae5ea09ade8293d1d23c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= Date: Mon, 7 Jan 2019 18:26:55 +0100 Subject: [PATCH 3/3] typo --- metric_learn/_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metric_learn/_util.py b/metric_learn/_util.py index 8b52a9d9..3bc303f9 100644 --- a/metric_learn/_util.py +++ b/metric_learn/_util.py @@ -345,7 +345,7 @@ def transformer_from_metric(metric): if np.allclose(metric, np.diag(np.diag(metric))): return np.sqrt(metric) elif not np.isclose(np.linalg.det(metric), 0): - return np.linlalg.cholesky(metric).T + return np.linalg.cholesky(metric).T else: w, V = np.linalg.eigh(metric) return V.T * np.sqrt(np.maximum(0, w[:, None]))