Skip to content

Disallow 0 on Triplets predictions #331

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Oct 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion metric_learn/base_metric.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
44 changes: 44 additions & 0 deletions test/test_triplets_classifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand All @@ -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)
Expand Down