Skip to content

Commit 76bedcd

Browse files
authored
Merge branch 'master' into sgot_update
2 parents 030f50b + f1e0c5c commit 76bedcd

8 files changed

Lines changed: 135 additions & 104 deletions

File tree

.github/labeler.yml

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ CI:
1818

1919
ot.bregman:
2020
- changed-files:
21-
- any-glob-to-any-file: ot/bergman/**
21+
- any-glob-to-any-file: ot/bregman/**
2222

2323
ot.gromov:
2424
- changed-files:
@@ -32,6 +32,10 @@ ot.lp:
3232
- changed-files:
3333
- any-glob-to-any-file: ot/lp/**
3434

35+
ot.bsp:
36+
- changed-files:
37+
- any-glob-to-any-file: ot/bsp/**
38+
3539
ot.utils:
3640
- changed-files:
3741
- any-glob-to-any-file: ot/utils.py
@@ -68,17 +72,21 @@ ot.lowrank:
6872
- changed-files:
6973
- any-glob-to-any-file: ot/lowrank.py
7074

75+
ot.sliced:
76+
- changed-files:
77+
- any-glob-to-any-file: ot/sliced/**
78+
7179
ot.solvers:
7280
- changed-files:
73-
- any-glob-to-any-file: ot/solvers.py
81+
- any-glob-to-any-file: ot/solvers/**
7482

75-
ot.partial:
83+
ot.batch:
7684
- changed-files:
77-
- any-glob-to-any-file: ot/partial/**
85+
- any-glob-to-any-file: ot/batch/**
7886

79-
ot.sliced:
87+
ot.partial:
8088
- changed-files:
81-
- any-glob-to-any-file: ot/sliced.py
89+
- any-glob-to-any-file: ot/partial/**
8290

8391
ot.smooth:
8492
- changed-files:
@@ -95,3 +103,7 @@ ot.dr:
95103
ot.gnn:
96104
- changed-files:
97105
- any-glob-to-any-file: ot/gnn/**
106+
107+
ot.sgot:
108+
- changed-files:
109+
- any-glob-to-any-file: ot/sgot.py

.github/requirements_doctests.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ tensorflow; python_version < '3.14'
1212
pytest
1313
torch_geometric
1414
cvxpy
15-
geomloss
15+
geomloss>=0.3.1
1616
pykeops

RELEASES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ This new release adds support for sparse cost matrices and a new lazy exact OT s
3535
- Wrapper for barycenter solvers with free support `ot.solvers.bary_free_support` (PR #730)
3636
- Build wheels on ubuntu ARM to avoid QEMU emulation (PR #818)
3737
- Add new methods to compute the linear transport map and the related 2-Wasserstein distance betweeen high-dimensional (HD) Gaussian distributions as described in [88], implemented in `ot.gaussian.bures_wasserstein_mapping_hd` and `ot.gaussian.bures_wasserstein_distance_hd`, respectively. Two additional methods estimate the same quantities from the source and destination observed data and are implemented in `ot.gaussian.empirical_bures_wasserstein_mapping_hd` and `ot.gaussian.empirical_bures_wasserstein_distance_hd`, respectively (PR #814)
38+
- Update the geomloss wrapper to the new version and API (PR #826)
3839
- Fix docstrings for `lowrank_gromov_wasserstein_samples` and `lowrank_sinkhorn` (PR #823)
3940
- Reorganize all tests per backend (PR #828)
4041

@@ -58,6 +59,7 @@ This new release adds support for sparse cost matrices and a new lazy exact OT s
5859
- Fix entropic regularization in `gcg`(PR #817, Issue #758)
5960
- Fix documentation build on master with submodules (PR #818)
6061
- Fix failing test for unbalanced solver with generic regularization (PR #824)
62+
- Fix docstrings for `lowrank_gromov_wasserstein_samples` and `lowrank_sinkhorn` (PR #823)
6163

6264

6365
## 0.9.6.post1

ot/bregman/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646

4747
from ._dictionary import unmix
4848

49-
from ._geomloss import empirical_sinkhorn2_geomloss, geomloss
49+
from ._geomloss import empirical_sinkhorn2_geomloss, geomloss, old_geomloss
5050

5151

5252
__all__ = [
@@ -76,6 +76,7 @@
7676
"empirical_sinkhorn_nystroem",
7777
"empirical_sinkhorn_nystroem2",
7878
"geomloss",
79+
"old_geomloss",
7980
"screenkhorn",
8081
"unmix",
8182
]

ot/bregman/_geomloss.py

Lines changed: 69 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,15 @@
1515
import torch
1616
from torch.autograd import grad
1717
from ..utils import get_backend, LazyTensor, dist
18+
19+
if geomloss.__version__ < "0.3.1":
20+
old_geomloss = True
21+
else:
22+
old_geomloss = False
23+
1824
except ImportError:
1925
geomloss = False
26+
old_geomloss = False
2027

2128

2229
def get_sinkhorn_geomloss_lazytensor(
@@ -127,6 +134,12 @@ def empirical_sinkhorn2_geomloss(
127134
better stability and epsilon-scaling. The solution is computed in a lazy way
128135
using the Geomloss [60]_ and the KeOps library [61]_.
129136
137+
.. warning::
138+
The Geomloss library is required for this function to work. Also
139+
when setting `log=True`, the dual potentials are computed using autograd and
140+
may be slow for large problems and prevent computing backward gradients. Use
141+
the fynction :func:`ot.solve_sample` with `method='geomloss'` for better performance and gradient computation.
142+
130143
Parameters
131144
----------
132145
X_s : array-like, shape (n_samples_a, dim)
@@ -178,63 +191,66 @@ def empirical_sinkhorn2_geomloss(
178191
179192
"""
180193

181-
if geomloss:
182-
nx = get_backend(X_s, X_t, a, b)
194+
if not geomloss:
195+
raise ImportError("geomloss not installed")
183196

184-
if nx.__name__ not in ["torch", "numpy"]:
185-
raise ValueError("geomloss only support torch or numpy backend")
197+
nx = get_backend(X_s, X_t, a, b)
186198

187-
if a is None:
188-
a = nx.ones(X_s.shape[0], type_as=X_s) / X_s.shape[0]
189-
if b is None:
190-
b = nx.ones(X_t.shape[0], type_as=X_t) / X_t.shape[0]
199+
if nx.__name__ not in ["torch", "numpy"]:
200+
raise ValueError("geomloss only support torch or numpy backend")
191201

192-
if nx.__name__ == "numpy":
193-
X_s_torch = torch.tensor(X_s)
194-
X_t_torch = torch.tensor(X_t)
202+
if a is None:
203+
a = nx.ones(X_s.shape[0], type_as=X_s) / X_s.shape[0]
204+
if b is None:
205+
b = nx.ones(X_t.shape[0], type_as=X_t) / X_t.shape[0]
195206

196-
a_torch = torch.tensor(a)
197-
b_torch = torch.tensor(b)
207+
if nx.__name__ == "numpy":
208+
X_s_torch = torch.tensor(X_s)
209+
X_t_torch = torch.tensor(X_t)
198210

199-
else:
200-
X_s_torch = X_s
201-
X_t_torch = X_t
211+
a_torch = torch.tensor(a)
212+
b_torch = torch.tensor(b)
202213

203-
a_torch = a
204-
b_torch = b
214+
else:
215+
X_s_torch = X_s
216+
X_t_torch = X_t
205217

206-
# after that we are all in torch
218+
a_torch = a
219+
b_torch = b
207220

208-
# set blur value and p
209-
if metric == "sqeuclidean":
210-
p = 2
211-
blur = np.sqrt(reg / 2) # because geomloss divides cost by two
212-
elif metric == "euclidean":
213-
p = 1
214-
blur = np.sqrt(reg)
215-
else:
216-
raise ValueError("geomloss only supports sqeuclidean and euclidean metrics")
221+
# after that we are all in torch
217222

223+
# set blur value and p
224+
if metric == "sqeuclidean":
225+
p = 2
226+
blur = np.sqrt(reg / 2) # because geomloss divides cost by two
227+
elif metric == "euclidean":
228+
p = 1
229+
blur = np.sqrt(reg)
230+
else:
231+
raise ValueError("geomloss only supports sqeuclidean and euclidean metrics")
232+
233+
if log:
218234
# force gradients for computing dual
219235
a_torch.requires_grad = True
220236
b_torch.requires_grad = True
221237

222-
loss = SamplesLoss(
223-
loss="sinkhorn",
224-
p=p,
225-
blur=blur,
226-
backend=backend,
227-
debias=debias,
228-
scaling=scaling,
229-
verbose=verbose,
230-
)
238+
loss = SamplesLoss(
239+
loss="sinkhorn",
240+
p=p,
241+
blur=blur,
242+
backend=backend,
243+
debias=debias,
244+
scaling=scaling,
245+
verbose=verbose,
246+
)
231247

232-
# compute value
233-
value = loss(
234-
a_torch, X_s_torch, b_torch, X_t_torch
235-
) # linear + entropic/KL reg?
248+
# compute value
249+
value = loss(a_torch, X_s_torch, b_torch, X_t_torch) # linear + entropic/KL reg?
236250

237-
# get dual potentials
251+
# get dual potentials
252+
253+
if log: # recover dual potentials
238254
f, g = grad(value, [a_torch, b_torch])
239255

240256
if metric == "sqeuclidean":
@@ -245,20 +261,18 @@ def empirical_sinkhorn2_geomloss(
245261
g = g.cpu().detach().numpy()
246262
value = value.cpu().detach().numpy()
247263

248-
if log:
249-
log = {}
250-
log["f"] = f
251-
log["g"] = g
252-
log["value"] = value
253-
254-
log["lazy_plan"] = get_sinkhorn_geomloss_lazytensor(
255-
X_s, X_t, f, g, a, b, metric=metric, blur=blur, nx=nx
256-
)
264+
log = {}
265+
log["f"] = f
266+
log["g"] = g
267+
log["value"] = value
257268

258-
return value, log
269+
log["lazy_plan"] = get_sinkhorn_geomloss_lazytensor(
270+
X_s, X_t, f, g, a, b, metric=metric, blur=blur, nx=nx
271+
)
259272

260-
else:
261-
return value
273+
return value, log
262274

263275
else:
264-
raise ImportError("geomloss not installed")
276+
if nx.__name__ == "numpy":
277+
value = value.cpu().detach().numpy()
278+
return value

ot/solvers/_linear.py

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@
1515
from ..bregman import (
1616
sinkhorn_log,
1717
empirical_sinkhorn2,
18-
empirical_sinkhorn2_geomloss,
1918
empirical_sinkhorn_nystroem2,
19+
old_geomloss,
2020
)
2121
from ..smooth import smooth_ot_dual
2222
from ..gaussian import empirical_bures_wasserstein_distance
2323
from ..factored import factored_optimal_transport
2424
from ..lowrank import lowrank_sinkhorn
2525
from ..optim import cg
26+
from warnings import warn
2627

2728

2829
lst_method_lazy = [
@@ -790,17 +791,9 @@ def solve_sample(
790791
# automatic solver
791792
res = ot.solve_sample(xa, xb, a, b, reg=1.0, method='geomloss')
792793
793-
# force O(n) memory efficient solver
794-
res = ot.solve_sample(xa, xb, a, b, reg=1.0, method='geomloss_online')
795-
796-
# force pre-computed cost matrix
797-
res = ot.solve_sample(xa, xb, a, b, reg=1.0, method='geomloss_tensorized')
798-
799-
# use multiscale solver
800-
res = ot.solve_sample(xa, xb, a, b, reg=1.0, method='geomloss_multiscale')
801-
802-
# One can play with speed (small scaling factor) and precision (scaling close to 1)
803-
res = ot.solve_sample(xa, xb, a, b, reg=1.0, method='geomloss', scaling=0.5)
794+
.. warning::
795+
The geomloss solver is a thin wrapper around the `geomloss.ot.solve_sample`
796+
function. The API is still under development and some features might be missing. Please refer to the `geomloss` documentation for more information.
804797
805798
- **Quadratic regularized OT [17]** (when ``reg!=None`` and ``reg_type="L2"``):
806799
@@ -1175,29 +1168,36 @@ def solve_sample(
11751168
backend = "online"
11761169
else:
11771170
backend = "tensorized"
1171+
if lazy0 is None:
1172+
warn(
1173+
f"geomloss backend is set to '{backend}' but is not yet supported by unified geomloss API yet."
1174+
)
11781175

1179-
value, log = empirical_sinkhorn2_geomloss(
1180-
X_a,
1181-
X_b,
1182-
reg=reg,
1183-
a=a,
1184-
b=b,
1185-
metric=metric,
1186-
log=True,
1187-
verbose=verbose,
1188-
scaling=scaling,
1189-
backend=backend,
1190-
)
1176+
if old_geomloss: # old wrapper for old geomloss versions
1177+
raise NotImplementedError(
1178+
"geomloss version >= 0.3.1 required for ot.solve_sample() geomloss backend."
1179+
)
11911180

1192-
lazy_plan = log["lazy_plan"]
1193-
if not lazy0: # store plan if not lazy
1194-
plan = lazy_plan[:]
1181+
else: # new geomloss wrapper
1182+
import geomloss.ot as got
11951183

1196-
# return scaled potentials (to be consistent with other solvers)
1197-
potentials = (
1198-
log["f"] / (lazy_plan.blur**2),
1199-
log["g"] / (lazy_plan.blur**2),
1200-
)
1184+
if max_iter is None:
1185+
max_iter = 1000
1186+
1187+
res = got.solve_sample(
1188+
X_a,
1189+
X_b,
1190+
a=a,
1191+
b=b,
1192+
reg=reg,
1193+
cost=metric,
1194+
unbalanced=unbalanced,
1195+
unbalanced_type=unbalanced_type,
1196+
max_iter=max_iter,
1197+
tol=tol,
1198+
)
1199+
res.value_linear = None
1200+
return res
12011201

12021202
elif reg is None or reg == 0: # exact OT
12031203
if unbalanced is None: # balanced EMD solver not available for lazy
@@ -1210,7 +1210,7 @@ def solve_sample(
12101210
else:
12111211
raise (
12121212
NotImplementedError(
1213-
'Non regularized solver with unbalanced_type="{}" not implemented'.format(
1213+
'Non regularized lazy solver with unbalanced_type="{}" not implemented'.format(
12141214
unbalanced_type
12151215
)
12161216
)

setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@
129129
"dr": ["scikit-learn", "pymanopt", "autograd"],
130130
"gnn": ["torch", "torch_geometric"],
131131
"plot": ["matplotlib"],
132+
"geomloss": ["geomloss"],
132133
"doc": [
133134
"sphinx",
134135
"sphinx-rtd-theme",
@@ -152,6 +153,7 @@
152153
"autograd",
153154
"torch_geometric",
154155
"matplotlib",
156+
"geomloss",
155157
],
156158
},
157159
python_requires=">=3.7",

0 commit comments

Comments
 (0)