diff --git a/metric_learn/base_metric.py b/metric_learn/base_metric.py index 721d7ba0..21506011 100644 --- a/metric_learn/base_metric.py +++ b/metric_learn/base_metric.py @@ -602,7 +602,7 @@ def predict(self, triplets): prediction : `numpy.ndarray` of floats, shape=(n_constraints,) Predictions of the ordering of pairs, for each triplet. """ - return np.sign(self.decision_function(triplets)) + return 2 * (self.decision_function(triplets) > 0) - 1 def decision_function(self, triplets): """Predicts differences between sample distances in input triplets. diff --git a/test/test_triplets_classifiers.py b/test/test_triplets_classifiers.py index 0f0bf7df..f2d5c015 100644 --- a/test/test_triplets_classifiers.py +++ b/test/test_triplets_classifiers.py @@ -6,6 +6,7 @@ from metric_learn.sklearn_shims import set_random_state from sklearn import clone import numpy as np +from numpy.testing import assert_array_equal @pytest.mark.parametrize('with_preprocessor', [True, False]) @@ -26,6 +27,49 @@ def test_predict_only_one_or_minus_one(estimator, build_dataset, assert len(not_valid) == 0 +@pytest.mark.parametrize('estimator, build_dataset', triplets_learners, + ids=ids_triplets_learners) +def test_no_zero_prediction(estimator, build_dataset): + """ + Test that all predicted values are not zero, even when the + distance d(x,y) and d(x,z) is the same for a triplet of the + form (x, y, z). i.e border cases. + """ + triplets, _, _, X = build_dataset(with_preprocessor=False) + # Force 3 dimentions only, to use cross product and get easy orthogonal vec. + triplets = np.array([[t[0][:3], t[1][:3], t[2][:3]] for t in triplets]) + X = X[:, :3] + # Dummy fit + estimator = clone(estimator) + set_random_state(estimator) + estimator.fit(triplets) + # We force the transformation to be identity, to force euclidean distance + estimator.components_ = np.eye(X.shape[1]) + + # Get two orthogonal vectors in respect to X[1] + k = X[1] / np.linalg.norm(X[1]) # Normalize first vector + x = X[2] - X[2].dot(k) * k # Get random orthogonal vector + x /= np.linalg.norm(x) # Normalize + y = np.cross(k, x) # Get orthogonal vector to x + # Assert these orthogonal vectors are different + with pytest.raises(AssertionError): + assert_array_equal(X[1], x) + with pytest.raises(AssertionError): + assert_array_equal(X[1], y) + # Assert the distance is the same for both + assert estimator.get_metric()(X[1], x) == estimator.get_metric()(X[1], y) + + # Form the three scenarios where predict() gives 0 with numpy.sign + triplets_test = np.array( # Critical examples + [[X[0], X[2], X[2]], + [X[1], X[1], X[1]], + [X[1], x, y]]) + # Predict + predictions = estimator.predict(triplets_test) + # Check there are no zero values + assert np.sum(predictions == 0) == 0 + + @pytest.mark.parametrize('with_preprocessor', [True, False]) @pytest.mark.parametrize('estimator, build_dataset', triplets_learners, ids=ids_triplets_learners)