From 9549549891b502f4a041c9eef61540525570c8a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Barz?= Date: Tue, 16 May 2017 15:46:33 +0200 Subject: [PATCH 1/2] Define distance consistently as `(x-y)^T*M*(x-y)` Fixes the transformes returned by ITML and LSML. The following now holds also for ITML, LSML, SDML and the covariance method: learner.transformer().T.dot(learner.transformer()) == learner.metric() --- README.rst | 4 ++-- metric_learn/base_metric.py | 6 +++--- metric_learn/covariance.py | 6 +++++- metric_learn/lsml.py | 8 ++++++-- metric_learn/sdml.py | 6 +++--- test/metric_learn_test.py | 2 +- 6 files changed, 20 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index 9bb762b4..38d2f951 100644 --- a/README.rst +++ b/README.rst @@ -42,9 +42,9 @@ default implementations for the methods ``metric``, ``transformer``, and For an instance of a metric learner named ``foo`` learning from a set of ``d``-dimensional points, ``foo.metric()`` returns a ``d x d`` matrix ``M`` such that the distance between vectors ``x`` and ``y`` is -expressed ``sqrt((x-y).dot(inv(M)).dot(x-y))``. +expressed ``sqrt((x-y).dot(M).dot(x-y))``. Using scipy's ``pdist`` function, this would look like -``pdist(X, metric='mahalanobis', VI=inv(foo.metric()))``. +``pdist(X, metric='mahalanobis', VI=foo.metric())``. In the same scenario, ``foo.transformer()`` returns a ``d x d`` matrix ``L`` such that a vector ``x`` can be represented in the learned diff --git a/metric_learn/base_metric.py b/metric_learn/base_metric.py index abd2d0f7..02519de1 100644 --- a/metric_learn/base_metric.py +++ b/metric_learn/base_metric.py @@ -22,13 +22,13 @@ def metric(self): def transformer(self): """Computes the transformation matrix from the Mahalanobis matrix. - L = inv(cholesky(M)) + L = cholesky(M).T Returns ------- - L : (d x d) matrix + L : upper triangular (d x d) matrix """ - return inv(cholesky(self.metric())) + return cholesky(self.metric()).T def transform(self, X=None): """Applies the metric transformation. diff --git a/metric_learn/covariance.py b/metric_learn/covariance.py index 0e230d43..8fc07873 100644 --- a/metric_learn/covariance.py +++ b/metric_learn/covariance.py @@ -28,5 +28,9 @@ def fit(self, X, y=None): y : unused """ self.X_ = check_array(X, ensure_min_samples=2) - self.M_ = np.cov(self.X_.T) + self.M_ = np.cov(self.X_, rowvar = False) + if self.M_.ndim == 0: + self.M_ = 1./self.M_ + else: + self.M_ = np.linalg.inv(self.M_) return self diff --git a/metric_learn/lsml.py b/metric_learn/lsml.py index f329fe5e..85b60fdd 100644 --- a/metric_learn/lsml.py +++ b/metric_learn/lsml.py @@ -26,7 +26,7 @@ def __init__(self, tol=1e-3, max_iter=1000, prior=None, verbose=False): tol : float, optional max_iter : int, optional prior : (d x d) matrix, optional - guess at a metric [default: covariance(X)] + guess at a metric [default: inv(covariance(X))] verbose : bool, optional if True, prints information while learning """ @@ -48,7 +48,11 @@ def _prepare_inputs(self, X, constraints, weights): self.w_ = weights self.w_ /= self.w_.sum() # weights must sum to 1 if self.prior is None: - self.M_ = np.cov(X.T) + self.M_ = np.cov(X, rowvar = False) + if self.M_.ndim == 0: + self.M_ = 1./self.M_ + else: + self.M_ = np.linalg.inv(self.M_) else: self.M_ = self.prior diff --git a/metric_learn/sdml.py b/metric_learn/sdml.py index d353f524..93280334 100644 --- a/metric_learn/sdml.py +++ b/metric_learn/sdml.py @@ -47,7 +47,7 @@ def _prepare_inputs(self, X, W): W = check_array(W, accept_sparse=True) # set up prior M if self.use_cov: - self.M_ = np.cov(X.T) + self.M_ = pinvh(np.cov(X, rowvar = False)) else: self.M_ = np.identity(X.shape[1]) L = laplacian(W, normed=False) @@ -72,11 +72,11 @@ def fit(self, X, W): Returns the instance. """ loss_matrix = self._prepare_inputs(X, W) - P = pinvh(self.M_) + self.balance_param * loss_matrix + P = self.M_ + self.balance_param * loss_matrix emp_cov = pinvh(P) # hack: ensure positive semidefinite emp_cov = emp_cov.T.dot(emp_cov) - self.M_, _ = graph_lasso(emp_cov, self.sparsity_param, verbose=self.verbose) + _, self.M_ = graph_lasso(emp_cov, self.sparsity_param, verbose=self.verbose) return self diff --git a/test/metric_learn_test.py b/test/metric_learn_test.py index 1e7f31fe..e1dc426e 100644 --- a/test/metric_learn_test.py +++ b/test/metric_learn_test.py @@ -57,7 +57,7 @@ def test_iris(self): itml.fit(self.iris_points, self.iris_labels) csep = class_separation(itml.transform(), self.iris_labels) - self.assertLess(csep, 0.4) # it's not great + self.assertLess(csep, 0.2) class TestLMNN(MetricTestCase): From 45551056ca978b282a7e6d6579d95e54f02f206e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Barz?= Date: Tue, 23 May 2017 11:28:12 +0200 Subject: [PATCH 2/2] Added unit test for transformer-metric conversion --- test/test_transformer_metric_conversion.py | 80 ++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 test/test_transformer_metric_conversion.py diff --git a/test/test_transformer_metric_conversion.py b/test/test_transformer_metric_conversion.py new file mode 100644 index 00000000..e027d176 --- /dev/null +++ b/test/test_transformer_metric_conversion.py @@ -0,0 +1,80 @@ +import unittest +import numpy as np +from sklearn.datasets import load_iris +from numpy.testing import assert_array_almost_equal + +from metric_learn import ( + LMNN, NCA, LFDA, Covariance, MLKR, + LSML_Supervised, ITML_Supervised, SDML_Supervised, RCA_Supervised) + + +class TestTransformerMetricConversion(unittest.TestCase): + @classmethod + def setUpClass(self): + # runs once per test class + iris_data = load_iris() + self.X = iris_data['data'] + self.y = iris_data['target'] + + def test_cov(self): + cov = Covariance() + cov.fit(self.X) + L = cov.transformer() + assert_array_almost_equal(L.T.dot(L), cov.metric()) + + def test_lsml_supervised(self): + seed = np.random.RandomState(1234) + lsml = LSML_Supervised(num_constraints=200) + lsml.fit(self.X, self.y, random_state=seed) + L = lsml.transformer() + assert_array_almost_equal(L.T.dot(L), lsml.metric()) + + def test_itml_supervised(self): + seed = np.random.RandomState(1234) + itml = ITML_Supervised(num_constraints=200) + itml.fit(self.X, self.y, random_state=seed) + L = itml.transformer() + assert_array_almost_equal(L.T.dot(L), itml.metric()) + + def test_lmnn(self): + lmnn = LMNN(k=5, learn_rate=1e-6, verbose=False) + lmnn.fit(self.X, self.y) + L = lmnn.transformer() + assert_array_almost_equal(L.T.dot(L), lmnn.metric()) + + def test_sdml_supervised(self): + seed = np.random.RandomState(1234) + sdml = SDML_Supervised(num_constraints=1500) + sdml.fit(self.X, self.y, random_state=seed) + L = sdml.transformer() + assert_array_almost_equal(L.T.dot(L), sdml.metric()) + + def test_nca(self): + n = self.X.shape[0] + nca = NCA(max_iter=(100000//n), learning_rate=0.01) + nca.fit(self.X, self.y) + L = nca.transformer() + assert_array_almost_equal(L.T.dot(L), nca.metric()) + + def test_lfda(self): + lfda = LFDA(k=2, num_dims=2) + lfda.fit(self.X, self.y) + L = lfda.transformer() + assert_array_almost_equal(L.T.dot(L), lfda.metric()) + + def test_rca_supervised(self): + seed = np.random.RandomState(1234) + rca = RCA_Supervised(num_dims=2, num_chunks=30, chunk_size=2) + rca.fit(self.X, self.y, random_state=seed) + L = rca.transformer() + assert_array_almost_equal(L.T.dot(L), rca.metric()) + + def test_mlkr(self): + mlkr = MLKR(num_dims=2) + mlkr.fit(self.X, self.y) + L = mlkr.transformer() + assert_array_almost_equal(L.T.dot(L), mlkr.metric()) + + +if __name__ == '__main__': + unittest.main()