Skip to content

Commit b5ef405

Browse files
Merge branch 'master' into tree
2 parents e9388c1 + 98d09a1 commit b5ef405

9 files changed

Lines changed: 639 additions & 10 deletions

File tree

‎README.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,3 +488,8 @@ Artificial Intelligence.
488488

489489
\[96] Rakhmanov, E. A., Saff, E. B., & Zhou, Y. M. (1994). [Minimal Discrete Energy on the Sphere](https://www.math.vanderbilt.edu/~esaff/texts/155.pdf). Mathematical Research Letters, 1(6), 647-662.
490490

491+
\[97] Rowland, M., Hron, J., Tang, Y., Choromanski, K., Sarlos, T., & Weller, A. (2019). [Orthogonal Estimation of Wasserstein Distances](https://proceedings.mlr.press/v89/rowland19a.html). Proceedings of the 22nd International Conference on Artificial Intelligence and Statistics (AISTATS), PMLR 89:186-195.
492+
493+
\[98] Petrovic, V., Bardenet, R., & Desolneux, A. (2026). [Repulsive Monte Carlo on the sphere for the sliced Wasserstein distance](https://openreview.net/forum?id=JSiTmB6Ehu). Transactions on Machine Learning Research.
494+
495+
\[99] Sisouk, K., Delon, J., & Tierny, J. (2025). [A User's Guide to Sampling Strategies for Sliced Optimal Transport](https://openreview.net/forum?id=ECBepTWAFG). Transactions on Machine Learning Research.

‎RELEASES.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@
99
- Add Quasi-Monte Carlo sliced Wasserstein sampling (QSW/RQSW) via generalized
1010
spiral points, selectable with `sampling_slices` in `sliced_wasserstein_distance`,
1111
as described in [95] (PR #838)
12+
- Add UnifOrtho sliced Wasserstein sampling via independent orthogonal
13+
blocks, selectable with `sampling_slices` in `sliced_wasserstein_distance`,
14+
as described in [97] (PR #853)
1215

1316
#### Closed issues
1417

18+
- Remove a leftover debug `print` from `ot.utils.projection_sparse_simplex` with `axis=1`, and make the `ot.datasets.make_gauss_hd` docstring a raw string so importing `ot` no longer emits a `SyntaxWarning` (PR #860)
1519
- Fix `ot.dist` ignoring the weights `w` for `metric="cityblock"`, which returned the unweighted distance although the weights are documented for this metric (PR #859)
1620
- Fix swapped arguments to `div_to_product` in `ot.gromov.fused_unbalanced_across_spaces_cost`: with `reg_type="independent"` (UCOOT) the entropic terms used the plan marginals as the reference measures and vice versa (PR #855, Issue #854)
1721
- Fix device placement in `ot.batch.bregman_projection_batch` so `ot.solve_batch(..., method="sinkhorn")` no longer crashes on GPU when the torch default device is CPU (PR #851)
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
=========================================================
4+
UnifOrtho Sliced Wasserstein in high dimension
5+
=========================================================
6+
7+
This example illustrates the UnifOrtho sampling scheme for Sliced
8+
Wasserstein directions, introduced in [97] and recommended for large
9+
dimensions by a recent numerical and theoretical study [98], and compares
10+
it to the default uniform (Monte Carlo) sampling of slicing directions.
11+
12+
Sliced Wasserstein (SWD) approximates the Wasserstein distance by averaging
13+
1D Wasserstein distances over projections onto random directions
14+
:math:`\\theta` drawn uniformly on the sphere. By default these directions
15+
are sampled purely at random (Monte Carlo), which introduces some variance
16+
in the estimate for a given number of projections.
17+
18+
UnifOrtho takes a different route that works in *any* dimension:
19+
instead of drawing directions independently, it draws them in blocks of ``dim`` directions,
20+
each block is a random orthonormal basis (a draw from the Haar measure on the
21+
special orthogonal group :math:`\\mathrm{SO}(\\mathrm{dim})`). Directions within a block are
22+
therefore exactly mutually orthogonal, spreading them out much more evenly
23+
than independent draws would.
24+
25+
We first visualize this block structure on the ordinary 3D sphere, purely
26+
for intuition -- dimension 3 is precisely where QSW/RQSW should be
27+
preferred in practice, not UnifOrtho, as the convergence comparison right
28+
after the visualization confirms numerically. We then measure convergence
29+
to the true Sliced Wasserstein distance in a genuinely high dimension,
30+
where UnifOrtho is the recommended choice.
31+
32+
.. [97] Rowland, M., Hron, J., Tang, Y., Choromanski, K., Sarlos, T., &
33+
Weller, A. (2019). Orthogonal Estimation of Wasserstein Distances.
34+
Proceedings of the 22nd International Conference on Artificial
35+
Intelligence and Statistics (AISTATS), PMLR 89.
36+
.. [98] Petrovic, V., Bardenet, R., & Desolneux, A. (2026). Repulsive
37+
Monte Carlo on the sphere for the sliced Wasserstein distance.
38+
Transactions on Machine Learning Research.
39+
.. [99] Sisouk, K., Delon, J., & Tierny, J. (2025). A User's Guide to
40+
Sampling Strategies for Sliced Optimal Transport. Transactions on
41+
Machine Learning Research.
42+
"""
43+
44+
# Author: Samuel Vangu <samuelvangu0@gmail.com>
45+
#
46+
# License: MIT License
47+
48+
# sphinx_gallery_thumbnail_number = 1
49+
50+
import numpy as np
51+
import matplotlib.pylab as pl
52+
from mpl_toolkits.mplot3d import Axes3D # noqa: F401 (registers the 3D projection)
53+
54+
import ot
55+
from ot.sliced import get_random_projections, get_random_orthogonal_directions
56+
57+
##############################################################################
58+
# Visualize the block structure on the sphere (d=3, for intuition only)
59+
# -----------------------------------------------------------------------
60+
# We draw 15 directions on :math:`S^2` with each scheme:
61+
#
62+
# - ``uniform``: directions are Gaussian vectors normalized to unit norm
63+
# (standard Monte Carlo sampling of the sphere) -- no structure between
64+
# the points.
65+
# - ``unif_ortho``: 5 independent blocks of 3 mutually orthogonal
66+
# directions each. Each block is colored separately below, so that
67+
# same-colored points are exactly orthogonal to one another -- this is
68+
# the structure that is not visible in the uniform sample.
69+
#
70+
# A translucent sphere is drawn behind the points purely to make the
71+
# geometry easier to read.
72+
#
73+
# Dimension 3 is used here only because it is the one human beings can
74+
# actually look at. It is *not* the dimension UnifOrtho is recommended
75+
# for -- see the convergence experiment below.
76+
77+
d = 3
78+
n_blocks = 15
79+
n_projections = n_blocks * d
80+
seed = 42
81+
82+
theta_uniform = get_random_projections(d, n_projections, seed=seed)
83+
theta_uniortho = get_random_orthogonal_directions(d, n_projections, seed=seed)
84+
85+
# A plain unit sphere surface, drawn behind the scattered points below.
86+
u_sphere = np.linspace(0, 2 * np.pi, 40)
87+
v_sphere = np.linspace(0, np.pi, 20)
88+
sphere_x = np.outer(np.cos(u_sphere), np.sin(v_sphere))
89+
sphere_y = np.outer(np.sin(u_sphere), np.sin(v_sphere))
90+
sphere_z = np.outer(np.ones_like(u_sphere), np.cos(v_sphere))
91+
92+
fig = pl.figure(1, figsize=(10, 5))
93+
94+
ax1 = fig.add_subplot(1, 2, 1, projection="3d")
95+
ax1.scatter(
96+
theta_uniform[0], theta_uniform[1], theta_uniform[2], c="gray", s=25, alpha=0.8
97+
)
98+
ax1.set_title("Uniform (Monte Carlo)")
99+
100+
ax2 = fig.add_subplot(1, 2, 2, projection="3d")
101+
block_ids = np.repeat(np.arange(n_blocks), d)
102+
ax2.scatter(
103+
theta_uniortho[0],
104+
theta_uniortho[1],
105+
theta_uniortho[2],
106+
c=block_ids,
107+
cmap="tab10",
108+
s=25,
109+
alpha=0.9,
110+
)
111+
ax2.set_title("UnifOrtho (one color per orthogonal block)")
112+
113+
for ax in (ax1, ax2):
114+
ax.plot_surface(
115+
sphere_x, sphere_y, sphere_z, color="lightgray", alpha=0.15, linewidth=0
116+
)
117+
ax.set_box_aspect([1, 1, 1])
118+
ax.view_init(elev=20, azim=45)
119+
ax.set_xticks([])
120+
ax.set_yticks([])
121+
ax.set_zticks([])
122+
123+
pl.tight_layout()
124+
pl.show()
125+
126+
# Every triple of same-colored points on the right is an exact orthogonal
127+
# basis of R^3 -- three mutually perpendicular directions. The uniform
128+
# sample on the left has no such guarantee: any two of its points can end
129+
# up arbitrarily close to each other.
130+
131+
##############################################################################
132+
# Convergence at d=3: UnifOrtho is not the right tool here
133+
# ------------------------------------------------------------
134+
# Before moving to high dimension, it is worth checking numerically that
135+
# UnifOrtho is indeed NOT the best choice at d=3 -- randomized spiral
136+
# points (RQSW, see the companion example "Quasi-Monte Carlo Sliced
137+
# Wasserstein in 3D") are. We use the same exact closed-form reference as
138+
# below (a pure translation).
139+
140+
from ot.sliced import get_projections_spiral # noqa: E402 (kept local to this section)
141+
142+
d3 = 3
143+
rng3 = np.random.RandomState(1)
144+
delta3 = rng3.normal(size=d3) * 1.2
145+
Xs3 = rng3.uniform(-2, 2, (100, d3))
146+
Xt3 = Xs3 + delta3
147+
sw_true3 = np.linalg.norm(delta3) / np.sqrt(d3)
148+
149+
n_proj_list3 = [10, 20, 50, 100]
150+
n_trials3 = 10
151+
152+
errors3 = {"uniform": [], "unif_ortho": [], "randomized_spiral_qmc": []}
153+
for n_proj in n_proj_list3:
154+
row = {k: [] for k in errors3}
155+
for t in range(n_trials3):
156+
for method in errors3:
157+
val = ot.sliced_wasserstein_distance(
158+
Xs3, Xt3, n_projections=n_proj, sampling_slices=method, seed=t
159+
)
160+
row[method].append(abs(val - sw_true3))
161+
for method in errors3:
162+
errors3[method].append(np.mean(row[method]))
163+
164+
pl.figure(2, figsize=(6, 5))
165+
for method, marker, label in [
166+
("uniform", "o-", "Uniform (MC)"),
167+
("unif_ortho", "s-", "UnifOrtho"),
168+
("randomized_spiral_qmc", "^-", "RQSW"),
169+
]:
170+
pl.plot(n_proj_list3, errors3[method], marker, label=label)
171+
pl.xscale("log")
172+
pl.yscale("log")
173+
pl.xlabel("Number of projections")
174+
pl.ylabel("Absolute error to the true SWD")
175+
pl.title("Convergence of the Sliced Wasserstein estimate (d=3)")
176+
pl.legend()
177+
pl.show()
178+
179+
# RQSW is consistently the most accurate here, UnifOrtho a clear second
180+
# (still better than plain uniform sampling, but not the recommended
181+
# choice), and uniform sampling the least accurate -- exactly the
182+
# low-dimensional ordering the literature [99] describes.
183+
184+
##############################################################################
185+
# Convergence to the true Sliced Wasserstein distance, in high dimension
186+
# --------------------------------------------------------------------------
187+
# As with the QSW/RQSW example, we build ``Xt`` as a pure translation of
188+
# ``Xs`` by a fixed vector :math:`\delta`, which makes the true Sliced
189+
# Wasserstein distance known exactly, with zero approximation error left
190+
# except from the number of projections:
191+
#
192+
# .. math::
193+
# \mathcal{SWD}_2(\mu, \nu) = \frac{\|\delta\|}{\sqrt{d}}
194+
#
195+
# This time we work in dimension 30 -- the regime UnifOrtho is recommended
196+
# for . The list of projection counts below is deliberately chosen to never be a multiple of ``d``:
197+
# when ``n_projections`` is an exact multiple of 30, UnifOrtho draws a
198+
# whole number of *complete* orthogonal bases, which for this particular
199+
# translation experiment happens to recover the exact answer up to
200+
# floating-point precision (an identity, not an approximation) -- an
201+
# interesting fact in its own right, but not representative of the
202+
# typical case we want to illustrate here.
203+
204+
d = 30
205+
rng = np.random.RandomState(0)
206+
207+
n_samples = 200
208+
delta = rng.normal(size=d) * 1.2
209+
Xs = rng.uniform(-2, 2, (n_samples, d))
210+
Xt = Xs + delta
211+
212+
# Exact reference: no approximation at all, at any cost.
213+
sw_true = np.linalg.norm(delta) / np.sqrt(d)
214+
215+
n_proj_list = [35, 65, 95, 190, 380, 760] # all >= d, none a multiple of d
216+
n_trials = 10
217+
218+
errors_uniform = np.zeros((n_trials, len(n_proj_list)))
219+
errors_uniortho = np.zeros((n_trials, len(n_proj_list)))
220+
221+
for j, n_proj in enumerate(n_proj_list):
222+
for t in range(n_trials):
223+
sw_uniform = ot.sliced_wasserstein_distance(
224+
Xs, Xt, n_projections=n_proj, sampling_slices="uniform", seed=t
225+
)
226+
sw_uniortho = ot.sliced_wasserstein_distance(
227+
Xs, Xt, n_projections=n_proj, sampling_slices="unif_ortho", seed=t
228+
)
229+
errors_uniform[t, j] = np.abs(sw_uniform - sw_true)
230+
errors_uniortho[t, j] = np.abs(sw_uniortho - sw_true)
231+
232+
mean_err_uniform = errors_uniform.mean(axis=0)
233+
std_err_uniform = errors_uniform.std(axis=0)
234+
mean_err_uniortho = errors_uniortho.mean(axis=0)
235+
std_err_uniortho = errors_uniortho.std(axis=0)
236+
237+
pl.figure(3, figsize=(6, 5))
238+
pl.plot(n_proj_list, mean_err_uniform, "o-", label="Uniform (MC)")
239+
pl.fill_between(
240+
n_proj_list,
241+
mean_err_uniform - std_err_uniform,
242+
mean_err_uniform + std_err_uniform,
243+
alpha=0.3,
244+
)
245+
pl.plot(n_proj_list, mean_err_uniortho, "s-", label="UnifOrtho")
246+
pl.fill_between(
247+
n_proj_list,
248+
mean_err_uniortho - std_err_uniortho,
249+
mean_err_uniortho + std_err_uniortho,
250+
alpha=0.3,
251+
)
252+
pl.xscale("log")
253+
pl.yscale("log")
254+
pl.xlabel("Number of projections")
255+
pl.ylabel("Absolute error to the true SWD")
256+
pl.title(f"Convergence of the Sliced Wasserstein estimate (d={d})")
257+
pl.legend()
258+
pl.show()
259+
260+
# UnifOrtho consistently reaches a given accuracy with markedly fewer
261+
# projections than uniform sampling in this high-dimensional setting --
262+
# the opposite of the low-dimensional case above, where RQSW is the
263+
# better choice. As a rule of thumb from the literature [98, 99]: prefer
264+
# RQSW in low dimension, UnifOrtho in high dimension, and either may do
265+
# in between. Since UnifOrtho remains an unbiased, stochastic estimator
266+
# (like RQSW), it is also a drop-in replacement for uniform sampling in
267+
# stochastic optimization settings.
268+
269+
# %%

‎ot/datasets.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ def get_data_classif(dataset, n, nz=0.5, theta=0, random_state=None, **kwargs):
186186
def make_gauss_hd(
187187
ns, nt, p=100, dim=5, m_diff=3.0, a=(10.0, 15.0), b=(3.0, 3.0), sub_the_same=False
188188
):
189-
"""Generation of source and target domains from Gaussian HD distributions
189+
r"""Generation of source and target domains from Gaussian HD distributions
190190
191191
Parameters
192192
----------

‎ot/sliced/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
projection_sphere_to_circle,
1818
get_projections_spiral,
1919
projection_sphere_to_ball,
20+
get_random_orthogonal_directions,
2021
)
2122
from ._sliced_distances import (
2223
sliced_wasserstein_distance,
@@ -46,4 +47,5 @@
4647
"linear_sliced_wasserstein_sphere",
4748
"get_projections_spiral",
4849
"stereographic_sliced_wasserstein_sphere",
50+
"get_random_orthogonal_directions",
4951
]

‎ot/sliced/_sliced_distances.py‎

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@
1111

1212
from ..backend import get_backend
1313
from ..utils import list_to_array, apply_scaler
14-
from ._utils import get_random_projections, get_projections_spiral
14+
from ._utils import (
15+
get_random_projections,
16+
get_projections_spiral,
17+
get_random_orthogonal_directions,
18+
)
1519
from ..lp import wasserstein_1d
1620

1721

@@ -40,11 +44,18 @@ def sliced_wasserstein_distance(
4044
- :math:`\theta_\# \mu` stands for the pushforwards of the projection :math:`X \in \mathbb{R}^d \mapsto \langle \theta, X \rangle`
4145
4246
By default, the projection directions :math:`\theta` are sampled uniformly
43-
at random. Setting ``sampling_slices`` to ``"spiral_qmc"`` or ``"randomized_spiral_qmc"`` instead
44-
uses Quasi-Monte Carlo point sets on the sphere (generalized spiral
45-
points), which can reduce the approximation error for a given
46-
``n_projections`` [95]. These two options are
47-
only implemented for ``dim == 3``.
47+
at random. Two families of alternatives are available through
48+
``sampling_slices``, each better suited to a different regime:
49+
50+
- ``"spiral_qmc"`` / ``"randomized_spiral_qmc"`` use a deterministic,
51+
low-discrepancy point set on the sphere (generalized spiral points),
52+
only defined for ``dim == 3`` [95].
53+
- ``"unif_ortho"`` uses independent blocks of mutually orthogonal
54+
directions (UnifOrtho), defined for any dimension.
55+
56+
Recent numerical and theoretical studies [98, 99] recommend
57+
``"randomized_spiral_qmc"`` in low dimensions and
58+
``"unif_ortho"`` for large ``dim``, with no clear winner in between.
4859
4960
Parameters
5061
----------
@@ -95,6 +106,12 @@ def sliced_wasserstein_distance(
95106
point set as ``"spiral_qmc"``, with a random rotation applied, giving an
96107
unbiased estimator suitable for stochastic optimization. Only
97108
implemented for ``dim == 3``.
109+
- ``"unif_ortho"``: UnifOrtho [97] -- independent blocks of
110+
mutually orthogonal directions, each block drawn from the Haar
111+
measure on :math:`\mathrm{SO}(\mathrm{dim})`. Defined for any
112+
``dim``, and recommended in particular for large ``dim`` [98, 99].
113+
See :any:`get_random_orthogonal_directions` for details, including
114+
how ``n_projections`` not being a multiple of ``dim`` is handled.
98115
99116
Returns
100117
-------
@@ -118,6 +135,9 @@ def sliced_wasserstein_distance(
118135
.. [31] Bonneel, Nicolas, et al. "Sliced and radon wasserstein barycenters of measures." Journal of Mathematical Imaging and Vision 51.1 (2015): 22-45
119136
.. [95] Nguyen, K., Bariletto, N., & Ho, N. (2024). "Quasi-Monte Carlo for 3D Sliced Wasserstein." International Conference on Learning Representations (ICLR).
120137
.. [96] Rakhmanov, E. A., Saff, E. B., & Zhou, Y. M. (1994). "Minimal Discrete Energy on the Sphere." Mathematical Research Letters, 1(6), 647-662.
138+
.. [97] Rowland, M., Hron, J., Tang, Y., Choromanski, K., Sarlos, T., & Weller, A. (2019). "Orthogonal Estimation of Wasserstein Distances." Proceedings of the 22nd International Conference on Artificial Intelligence and Statistics (AISTATS), PMLR 89.
139+
.. [98] Petrovic, V., Bardenet, R., & Desolneux, A. (2026). "Repulsive Monte Carlo on the sphere for the sliced Wasserstein distance." Transactions on Machine Learning Research.
140+
.. [99] Sisouk, K., Delon, J., & Tierny, J. (2025). "A User's Guide to Sampling Strategies for Sliced Optimal Transport." Transactions on Machine Learning Research.
121141
"""
122142

123143
X_s, X_t = list_to_array(X_s, X_t)
@@ -160,10 +180,18 @@ def sliced_wasserstein_distance(
160180
backend=nx,
161181
type_as=X_s,
162182
)
183+
elif method == "unif_ortho":
184+
projections = get_random_orthogonal_directions(
185+
d,
186+
n_projections,
187+
seed=seed,
188+
backend=nx,
189+
type_as=X_s,
190+
)
163191
else:
164192
raise ValueError(
165193
f"Unknown sampling_slices method '{sampling_slices}', "
166-
"must be one of 'uniform', 'spiral_qmc', 'randomized_spiral_qmc'"
194+
"must be one of 'uniform', 'spiral_qmc', 'randomized_spiral_qmc', 'unif_ortho' "
167195
)
168196
else:
169197
n_projections = projections.shape[1]

0 commit comments

Comments
 (0)