Repository navigation
[MRG] Use the weights w in ot.dist for all metrics #884
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -505,10 +505,13 @@ def dist( | |
| if w is not None: | ||
| return nx.from_numpy(cdist(x1, x2, metric=metric, w=w)) | ||
| return nx.from_numpy(cdist(x1, x2, metric=metric)) | ||
| elif metric == "sqeuclidean": | ||
| return euclidean_distances(x1, x2, squared=True, nx=nx) | ||
| elif metric == "euclidean": | ||
| return euclidean_distances(x1, x2, squared=False, nx=nx) | ||
| elif metric in ("sqeuclidean", "euclidean"): | ||
| if w is not None: | ||
| # sum_k w_k (x1_k - x2_k)^2 is the squared distance between the | ||
| # samples scaled by sqrt(w) | ||
| x1 = x1 * nx.sqrt(w)[None, :] | ||
| x2 = x2 * nx.sqrt(w)[None, :] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should instead preserve the function behaviour by doing: sqrt_w = nx.sqrt(w) |
||
| return euclidean_distances(x1, x2, squared=metric == "sqeuclidean", nx=nx) | ||
| elif metric == "cityblock": | ||
| if w is None: | ||
| if use_tensor: | ||
|
|
@@ -557,13 +560,17 @@ def dist( | |
| for i in range(x1.shape[1]): | ||
| M += w[i] * nx.abs(x1[:, i][:, None] - x2[:, i][None, :]) ** p | ||
| return M ** (1 / p) | ||
| elif metric == "cosine": | ||
| nx1 = nx.sqrt(nx.einsum("ij,ij->i", x1, x1)) | ||
| nx2 = nx.sqrt(nx.einsum("ij,ij->i", x2, x2)) | ||
| return 1.0 - (nx.dot(x1, nx.transpose(x2)) / nx1[:, None] / nx2[None, :]) | ||
| elif metric == "correlation": | ||
| x1 = x1 - nx.mean(x1, axis=1)[:, None] | ||
| x2 = x2 - nx.mean(x2, axis=1)[:, None] | ||
| elif metric in ("cosine", "correlation"): | ||
| if metric == "correlation": | ||
| if w is None: | ||
| x1 = x1 - nx.mean(x1, axis=1)[:, None] | ||
| x2 = x2 - nx.mean(x2, axis=1)[:, None] | ||
| else: | ||
| x1 = x1 - (nx.dot(x1, w) / nx.sum(w))[:, None] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nx.sum(w) should be computed only once and tested for zero value in which case we can raise a ValueError |
||
| x2 = x2 - (nx.dot(x2, w) / nx.sum(w))[:, None] | ||
| if w is not None: | ||
| x1 = x1 * nx.sqrt(w)[None, :] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. compute sqrt only once |
||
| x2 = x2 * nx.sqrt(w)[None, :] | ||
| nx1 = nx.sqrt(nx.einsum("ij,ij->i", x1, x1)) | ||
| nx2 = nx.sqrt(nx.einsum("ij,ij->i", x2, x2)) | ||
| return 1.0 - (nx.dot(x1, nx.transpose(x2)) / nx1[:, None] / nx2[None, :]) | ||
|
|
@@ -573,9 +580,7 @@ def dist( | |
| else: | ||
| if isinstance(metric, str) and metric.endswith("minkowski"): | ||
| return cdist(x1, x2, metric=metric, p=p, w=w) | ||
| # Only pass w parameter for metrics that support it | ||
| # According to SciPy docs, only 'minkowski' and 'wminkowski' support w | ||
| if w is not None and metric in ["minkowski", "wminkowski"]: | ||
| if w is not None: | ||
| return cdist(x1, x2, metric=metric, w=w) | ||
| return cdist(x1, x2, metric=metric) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -320,6 +320,45 @@ def test_dist_weighted_cityblock(use_tensor): | |
| np.testing.assert_allclose(D, expected, atol=1e-12) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "metric", | ||
| [ | ||
| "sqeuclidean", | ||
| "euclidean", | ||
| "cosine", | ||
| "correlation", | ||
| "braycurtis", | ||
| "canberra", | ||
| ], | ||
| ) | ||
| def test_dist_weighted_vs_cdist(metric): | ||
| rng = np.random.RandomState(0) | ||
| x1 = rng.randn(5, 3) | ||
| x2 = rng.randn(4, 3) | ||
| w = rng.rand(3) | ||
|
|
||
| expected = scipy.spatial.distance.cdist(x1, x2, metric=metric, w=w) | ||
| D = ot.dist(x1, x2, metric=metric, w=w) | ||
|
|
||
| np.testing.assert_allclose(D, expected, atol=1e-12) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "metric", ["sqeuclidean", "euclidean", "cosine", "correlation"] | ||
| ) | ||
| def test_dist_weighted_backends(nx, metric): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. these 2 tests can be merged, with different cases depending on the backend |
||
| rng = np.random.RandomState(0) | ||
| x1 = rng.randn(5, 3) | ||
| x2 = rng.randn(4, 3) | ||
| w = rng.rand(3) | ||
|
|
||
| expected = scipy.spatial.distance.cdist(x1, x2, metric=metric, w=w) | ||
| D = ot.dist(nx.from_numpy(x1), nx.from_numpy(x2), metric=metric, w=nx.from_numpy(w)) | ||
|
|
||
| # low atol because jax forces float32 | ||
| np.testing.assert_allclose(nx.to_numpy(D), expected, atol=1e-5) | ||
|
|
||
|
|
||
| def test_sparse_ot_dist_uses_pair_weights(): | ||
| x1 = np.array([[0.0], [1.0]]) | ||
| x2 = np.array([[0.0], [5.0]]) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move to the end of the itemize please to try to follow PR order