-
Notifications
You must be signed in to change notification settings - Fork 563
[MRG]Add UnifOrtho sampling of slicing directions for Sliced Wasserstein #853
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
Open
Samuel-Vangu
wants to merge
8
commits into
PythonOT:master
Choose a base branch
from
Samuel-Vangu:feature/add-uniortho-sampling
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
7aa7ec1
Add UnifOrtho sampling of slicing directions for Sliced Wasserstein
Samuel-Vangu a8e3b79
Add UnifOrtho sampling of slicing directions for Sliced Wasserstein
Samuel-Vangu 20a01a6
Added the PR number in the RELEASES.md file
Samuel-Vangu 407fa2d
Merge branch 'master' into feature/add-uniortho-sampling
clbonet 6fdc646
Merge branch 'master' into feature/add-uniortho-sampling
rflamary d087658
Changing things as requested in the review
Samuel-Vangu 2cbde2a
Merge branch 'feature/add-uniortho-sampling' of https://github.com/Sa…
Samuel-Vangu 7768615
Changing things again as requested in the review
Samuel-Vangu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,269 @@ | ||
| # -*- coding: utf-8 -*- | ||
| """ | ||
| ========================================================= | ||
| UnifOrtho Sliced Wasserstein in high dimension | ||
| ========================================================= | ||
|
|
||
| This example illustrates the UnifOrtho sampling scheme for Sliced | ||
| Wasserstein directions, introduced in [97] and recommended for large | ||
| dimensions by a recent numerical and theoretical study [98], and compares | ||
| it to the default uniform (Monte Carlo) sampling of slicing directions. | ||
|
|
||
| Sliced Wasserstein (SWD) approximates the Wasserstein distance by averaging | ||
| 1D Wasserstein distances over projections onto random directions | ||
| :math:`\\theta` drawn uniformly on the sphere. By default these directions | ||
| are sampled purely at random (Monte Carlo), which introduces some variance | ||
| in the estimate for a given number of projections. | ||
|
|
||
| UnifOrtho takes a different route that works in *any* dimension: | ||
| instead of drawing directions independently, it draws them in blocks of ``dim`` directions, | ||
| each block is a random orthonormal basis (a draw from the Haar measure on the | ||
| special orthogonal group :math:`\\mathrm{SO}(\\mathrm{dim})`). Directions within a block are | ||
| therefore exactly mutually orthogonal, spreading them out much more evenly | ||
| than independent draws would. | ||
|
|
||
| We first visualize this block structure on the ordinary 3D sphere, purely | ||
| for intuition -- dimension 3 is precisely where QSW/RQSW should be | ||
| preferred in practice, not UnifOrtho, as the convergence comparison right | ||
| after the visualization confirms numerically. We then measure convergence | ||
| to the true Sliced Wasserstein distance in a genuinely high dimension, | ||
| where UnifOrtho is the recommended choice. | ||
|
|
||
| .. [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. | ||
| .. [98] Petrovic, V., Bardenet, R., & Desolneux, A. (2026). Repulsive | ||
| Monte Carlo on the sphere for the sliced Wasserstein distance. | ||
| Transactions on Machine Learning Research. | ||
| .. [99] Sisouk, K., Delon, J., & Tierny, J. (2025). A User's Guide to | ||
| Sampling Strategies for Sliced Optimal Transport. Transactions on | ||
| Machine Learning Research. | ||
| """ | ||
|
|
||
| # Author: Samuel Vangu <samuelvangu0@gmail.com> | ||
| # | ||
| # License: MIT License | ||
|
|
||
| # sphinx_gallery_thumbnail_number = 1 | ||
|
|
||
| import numpy as np | ||
| import matplotlib.pylab as pl | ||
| from mpl_toolkits.mplot3d import Axes3D # noqa: F401 (registers the 3D projection) | ||
|
|
||
| import ot | ||
| from ot.sliced import get_random_projections, get_random_orthogonal_directions | ||
|
|
||
| ############################################################################## | ||
| # Visualize the block structure on the sphere (d=3, for intuition only) | ||
| # ----------------------------------------------------------------------- | ||
| # We draw 15 directions on :math:`S^2` with each scheme: | ||
| # | ||
| # - ``uniform``: directions are Gaussian vectors normalized to unit norm | ||
| # (standard Monte Carlo sampling of the sphere) -- no structure between | ||
| # the points. | ||
| # - ``unif_ortho``: 5 independent blocks of 3 mutually orthogonal | ||
| # directions each. Each block is colored separately below, so that | ||
| # same-colored points are exactly orthogonal to one another -- this is | ||
| # the structure that is not visible in the uniform sample. | ||
| # | ||
| # A translucent sphere is drawn behind the points purely to make the | ||
| # geometry easier to read. | ||
| # | ||
| # Dimension 3 is used here only because it is the one human beings can | ||
| # actually look at. It is *not* the dimension UnifOrtho is recommended | ||
| # for -- see the convergence experiment below. | ||
|
|
||
| d = 3 | ||
| n_blocks = 15 | ||
| n_projections = n_blocks * d | ||
| seed = 42 | ||
|
|
||
| theta_uniform = get_random_projections(d, n_projections, seed=seed) | ||
| theta_uniortho = get_random_orthogonal_directions(d, n_projections, seed=seed) | ||
|
|
||
| # A plain unit sphere surface, drawn behind the scattered points below. | ||
| u_sphere = np.linspace(0, 2 * np.pi, 40) | ||
| v_sphere = np.linspace(0, np.pi, 20) | ||
| sphere_x = np.outer(np.cos(u_sphere), np.sin(v_sphere)) | ||
| sphere_y = np.outer(np.sin(u_sphere), np.sin(v_sphere)) | ||
| sphere_z = np.outer(np.ones_like(u_sphere), np.cos(v_sphere)) | ||
|
|
||
| fig = pl.figure(1, figsize=(10, 5)) | ||
|
|
||
| ax1 = fig.add_subplot(1, 2, 1, projection="3d") | ||
| ax1.scatter( | ||
| theta_uniform[0], theta_uniform[1], theta_uniform[2], c="gray", s=25, alpha=0.8 | ||
| ) | ||
| ax1.set_title("Uniform (Monte Carlo)") | ||
|
|
||
| ax2 = fig.add_subplot(1, 2, 2, projection="3d") | ||
| block_ids = np.repeat(np.arange(n_blocks), d) | ||
| ax2.scatter( | ||
| theta_uniortho[0], | ||
| theta_uniortho[1], | ||
| theta_uniortho[2], | ||
| c=block_ids, | ||
| cmap="tab10", | ||
| s=25, | ||
| alpha=0.9, | ||
| ) | ||
| ax2.set_title("UnifOrtho (one color per orthogonal block)") | ||
|
|
||
| for ax in (ax1, ax2): | ||
| ax.plot_surface( | ||
| sphere_x, sphere_y, sphere_z, color="lightgray", alpha=0.15, linewidth=0 | ||
| ) | ||
| ax.set_box_aspect([1, 1, 1]) | ||
| ax.view_init(elev=20, azim=45) | ||
| ax.set_xticks([]) | ||
| ax.set_yticks([]) | ||
| ax.set_zticks([]) | ||
|
|
||
| pl.tight_layout() | ||
| pl.show() | ||
|
clbonet marked this conversation as resolved.
|
||
|
|
||
| # Every triple of same-colored points on the right is an exact orthogonal | ||
| # basis of R^3 -- three mutually perpendicular directions. The uniform | ||
| # sample on the left has no such guarantee: any two of its points can end | ||
| # up arbitrarily close to each other. | ||
|
|
||
| ############################################################################## | ||
| # Convergence at d=3: UnifOrtho is not the right tool here | ||
| # ------------------------------------------------------------ | ||
| # Before moving to high dimension, it is worth checking numerically that | ||
| # UnifOrtho is indeed NOT the best choice at d=3 -- randomized spiral | ||
| # points (RQSW, see the companion example "Quasi-Monte Carlo Sliced | ||
| # Wasserstein in 3D") are. We use the same exact closed-form reference as | ||
| # below (a pure translation). | ||
|
|
||
| from ot.sliced import get_projections_spiral # noqa: E402 (kept local to this section) | ||
|
|
||
| d3 = 3 | ||
| rng3 = np.random.RandomState(1) | ||
| delta3 = rng3.normal(size=d3) * 1.2 | ||
| Xs3 = rng3.uniform(-2, 2, (100, d3)) | ||
| Xt3 = Xs3 + delta3 | ||
| sw_true3 = np.linalg.norm(delta3) / np.sqrt(d3) | ||
|
|
||
| n_proj_list3 = [10, 20, 50, 100] | ||
| n_trials3 = 10 | ||
|
|
||
| errors3 = {"uniform": [], "unif_ortho": [], "randomized_spiral_qmc": []} | ||
| for n_proj in n_proj_list3: | ||
| row = {k: [] for k in errors3} | ||
| for t in range(n_trials3): | ||
| for method in errors3: | ||
| val = ot.sliced_wasserstein_distance( | ||
| Xs3, Xt3, n_projections=n_proj, sampling_slices=method, seed=t | ||
| ) | ||
| row[method].append(abs(val - sw_true3)) | ||
| for method in errors3: | ||
| errors3[method].append(np.mean(row[method])) | ||
|
|
||
| pl.figure(2, figsize=(6, 5)) | ||
| for method, marker, label in [ | ||
| ("uniform", "o-", "Uniform (MC)"), | ||
| ("unif_ortho", "s-", "UnifOrtho"), | ||
| ("randomized_spiral_qmc", "^-", "RQSW"), | ||
| ]: | ||
| pl.plot(n_proj_list3, errors3[method], marker, label=label) | ||
| pl.xscale("log") | ||
| pl.yscale("log") | ||
| pl.xlabel("Number of projections") | ||
| pl.ylabel("Absolute error to the true SWD") | ||
| pl.title("Convergence of the Sliced Wasserstein estimate (d=3)") | ||
| pl.legend() | ||
| pl.show() | ||
|
|
||
| # RQSW is consistently the most accurate here, UnifOrtho a clear second | ||
| # (still better than plain uniform sampling, but not the recommended | ||
| # choice), and uniform sampling the least accurate -- exactly the | ||
| # low-dimensional ordering the literature [99] describes. | ||
|
|
||
| ############################################################################## | ||
| # Convergence to the true Sliced Wasserstein distance, in high dimension | ||
| # -------------------------------------------------------------------------- | ||
| # As with the QSW/RQSW example, we build ``Xt`` as a pure translation of | ||
| # ``Xs`` by a fixed vector :math:`\delta`, which makes the true Sliced | ||
| # Wasserstein distance known exactly, with zero approximation error left | ||
| # except from the number of projections: | ||
| # | ||
| # .. math:: | ||
| # \mathcal{SWD}_2(\mu, \nu) = \frac{\|\delta\|}{\sqrt{d}} | ||
| # | ||
| # This time we work in dimension 30 -- the regime UnifOrtho is recommended | ||
| # for . The list of projection counts below is deliberately chosen to never be a multiple of ``d``: | ||
| # when ``n_projections`` is an exact multiple of 30, UnifOrtho draws a | ||
| # whole number of *complete* orthogonal bases, which for this particular | ||
| # translation experiment happens to recover the exact answer up to | ||
| # floating-point precision (an identity, not an approximation) -- an | ||
| # interesting fact in its own right, but not representative of the | ||
| # typical case we want to illustrate here. | ||
|
|
||
| d = 30 | ||
| rng = np.random.RandomState(0) | ||
|
|
||
| n_samples = 200 | ||
| delta = rng.normal(size=d) * 1.2 | ||
| Xs = rng.uniform(-2, 2, (n_samples, d)) | ||
| Xt = Xs + delta | ||
|
|
||
| # Exact reference: no approximation at all, at any cost. | ||
| sw_true = np.linalg.norm(delta) / np.sqrt(d) | ||
|
|
||
| n_proj_list = [35, 65, 95, 190, 380, 760] # all >= d, none a multiple of d | ||
| n_trials = 10 | ||
|
|
||
| errors_uniform = np.zeros((n_trials, len(n_proj_list))) | ||
| errors_uniortho = np.zeros((n_trials, len(n_proj_list))) | ||
|
|
||
| for j, n_proj in enumerate(n_proj_list): | ||
| for t in range(n_trials): | ||
| sw_uniform = ot.sliced_wasserstein_distance( | ||
| Xs, Xt, n_projections=n_proj, sampling_slices="uniform", seed=t | ||
| ) | ||
| sw_uniortho = ot.sliced_wasserstein_distance( | ||
| Xs, Xt, n_projections=n_proj, sampling_slices="unif_ortho", seed=t | ||
| ) | ||
| errors_uniform[t, j] = np.abs(sw_uniform - sw_true) | ||
| errors_uniortho[t, j] = np.abs(sw_uniortho - sw_true) | ||
|
|
||
| mean_err_uniform = errors_uniform.mean(axis=0) | ||
| std_err_uniform = errors_uniform.std(axis=0) | ||
| mean_err_uniortho = errors_uniortho.mean(axis=0) | ||
| std_err_uniortho = errors_uniortho.std(axis=0) | ||
|
|
||
| pl.figure(3, figsize=(6, 5)) | ||
| pl.plot(n_proj_list, mean_err_uniform, "o-", label="Uniform (MC)") | ||
| pl.fill_between( | ||
| n_proj_list, | ||
| mean_err_uniform - std_err_uniform, | ||
| mean_err_uniform + std_err_uniform, | ||
| alpha=0.3, | ||
| ) | ||
| pl.plot(n_proj_list, mean_err_uniortho, "s-", label="UnifOrtho") | ||
| pl.fill_between( | ||
| n_proj_list, | ||
| mean_err_uniortho - std_err_uniortho, | ||
| mean_err_uniortho + std_err_uniortho, | ||
| alpha=0.3, | ||
| ) | ||
| pl.xscale("log") | ||
| pl.yscale("log") | ||
| pl.xlabel("Number of projections") | ||
| pl.ylabel("Absolute error to the true SWD") | ||
| pl.title(f"Convergence of the Sliced Wasserstein estimate (d={d})") | ||
| pl.legend() | ||
| pl.show() | ||
|
|
||
| # UnifOrtho consistently reaches a given accuracy with markedly fewer | ||
| # projections than uniform sampling in this high-dimensional setting -- | ||
| # the opposite of the low-dimensional case above, where RQSW is the | ||
| # better choice. As a rule of thumb from the literature [98, 99]: prefer | ||
| # RQSW in low dimension, UnifOrtho in high dimension, and either may do | ||
| # in between. Since UnifOrtho remains an unbiased, stochastic estimator | ||
| # (like RQSW), it is also a drop-in replacement for uniform sampling in | ||
| # stochastic optimization settings. | ||
|
|
||
| # %% | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.