Skip to content

Commit fcc3548

Browse files
committed
add tests
1 parent 37719e6 commit fcc3548

3 files changed

Lines changed: 113 additions & 21 deletions

File tree

RELEASES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ This new release adds support for sparse cost matrices and a new lazy exact OT s
3838
- Update the geomloss wrapper to the new version and API (PR #826)
3939
- Fix docstrings for `lowrank_gromov_wasserstein_samples` and `lowrank_sinkhorn` (PR #823)
4040
- Reorganize all tests per backend (PR #828)
41-
41+
- Implement debiased OT solvers in `ot.solve_sample`
4242

4343
#### Closed issues
4444

ot/solvers/_linear.py

Lines changed: 87 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,7 @@ def solve_sample(
619619
verbose=False,
620620
grad="autodiff",
621621
random_state=None,
622-
debiased=False,
622+
debias=False,
623623
):
624624
r"""Solve the discrete optimal transport problem using the samples in the source and target domains.
625625
@@ -714,7 +714,7 @@ def solve_sample(
714714
detached. This is useful for memory saving when only the value is needed.
715715
random_state : int, optional
716716
The random state for sampling the components in each distribution for method='nystroem'.
717-
debiased : bool, optional
717+
debias : bool, optional
718718
Whether to use the debiased version of the OT problem, by default False
719719
if True, the value returned is the Sinkhorn divergence but the plan is
720720
still the Sinkhorn plan. The results for all pairs of problems and
@@ -975,7 +975,7 @@ def solve_sample(
975975
976976
"""
977977

978-
if debiased:
978+
if debias:
979979
dict_params = dict(
980980
metric=metric,
981981
reg=reg,
@@ -997,39 +997,106 @@ def solve_sample(
997997
verbose=verbose,
998998
grad=grad,
999999
random_state=random_state,
1000-
debiased=False,
1000+
debias=False,
10011001
)
10021002

1003-
if isinstance(debiased, str) and debiased.lower() == "split":
1004-
raise NotImplementedError(
1005-
"Debiased OT with split is not implemented yet. Please use debiased=True for now."
1003+
nx = get_backend(X_a, X_b, a, b)
1004+
1005+
if isinstance(debias, str) and debias.lower() == "split":
1006+
# split the samples into two halves with each half the mass
1007+
1008+
n_a = X_a.shape[0]
1009+
n_b = X_b.shape[0]
1010+
1011+
if a is None:
1012+
a = nx.ones(n_a, type_as=X_a) / n_a
1013+
if b is None:
1014+
b = nx.ones(n_b, type_as=X_b) / n_b
1015+
1016+
# find the split indices
1017+
acs = nx.cumsum(a)
1018+
bcs = nx.cumsum(b)
1019+
1020+
thr_a = 0.5 * nx.sum(a)
1021+
thr_b = 0.5 * nx.sum(b)
1022+
1023+
idx_a = nx.searchsorted(acs, thr_a)
1024+
idx_b = nx.searchsorted(bcs, thr_b)
1025+
1026+
# split the samples and weights
1027+
X_a1, X_a2 = X_a[: idx_a + 1], X_a[idx_a:]
1028+
X_b1, X_b2 = X_b[: idx_b + 1], X_b[idx_b:]
1029+
1030+
# compute weights for each half and adjust the last/first weight to sum to 0.5
1031+
a1, a2 = a[: idx_a + 1], a[idx_a:]
1032+
a1[-1] = a1[-1] * nx.detach((thr_a - nx.sum(a1[:-1])) / a1[-1])
1033+
a2[0] = a2[0] * nx.detach((thr_a - nx.sum(a2[1:])) / a2[0])
1034+
b1, b2 = b[: idx_b + 1], b[idx_b:]
1035+
b1[-1] = b1[-1] * nx.detach((thr_b - nx.sum(b1[:-1])) / b1[-1])
1036+
b2[0] = b2[0] * nx.detach((thr_b - nx.sum(b2[1:])) / b2[0])
1037+
1038+
# compute the four OT problems
1039+
resaa = solve_sample(X_a1, X_a2, a=a1, b=a2, **dict_params)
1040+
resbb = solve_sample(X_b1, X_b2, a=b1, b=b2, **dict_params)
1041+
resab1 = solve_sample(X_a1, X_b1, a=a1, b=b1, **dict_params)
1042+
resab2 = solve_sample(X_a2, X_b2, a=a2, b=b2, **dict_params)
1043+
1044+
# compute debiased values
1045+
value = 0.5 * (resab1.value + resab2.value) - 0.5 * (
1046+
resaa.value + resbb.value
10061047
)
1048+
value_linear = 0.5 * (resab1.value_linear + resab2.value_linear) - 0.5 * (
1049+
resaa.value_linear + resbb.value_linear
1050+
)
1051+
1052+
log = {
1053+
"res_aa": resaa,
1054+
"res_bb": resbb,
1055+
"res_ab1": resab1,
1056+
"res_ab2": resab2,
1057+
}
1058+
1059+
res = OTResult(
1060+
value=value,
1061+
value_linear=value_linear,
1062+
plan=None, # no plan for debiased version
1063+
potentials=None, # no potentials for debiased version
1064+
sparse_plan=None, # no sparse plan for debiased version
1065+
lazy_plan=None, # no lazy plan for debiased version
1066+
status="Debiased",
1067+
log=log,
1068+
backend=nx,
1069+
)
1070+
10071071
else:
10081072
# standard debiasing à la sinkhorn divergence
10091073

1010-
res11 = solve_sample(X_a, X_a, a=a, b=a, **dict_params)
1074+
resaa = solve_sample(X_a, X_a, a=a, b=a, **dict_params)
10111075

1012-
res22 = solve_sample(X_b, X_b, a=b, b=b, **dict_params)
1076+
resbb = solve_sample(X_b, X_b, a=b, b=b, **dict_params)
10131077

1014-
res12 = solve_sample(X_a, X_b, a=a, b=b, **dict_params)
1015-
value = res12.value - 0.5 * (res11.value + res22.value)
1016-
value_linear = res12.value_linear - 0.5 * (
1017-
res11.value_linear + res22.value_linear
1078+
resab = solve_sample(X_a, X_b, a=a, b=b, **dict_params)
1079+
1080+
# compute debiased values
1081+
value = resab.value - 0.5 * (resaa.value + resbb.value)
1082+
value_linear = resab.value_linear - 0.5 * (
1083+
resaa.value_linear + resbb.value_linear
10181084
)
10191085

1020-
log = {"res11": res11, "res22": res22, "res12": res12}
1086+
log = {"res_aa": resaa, "res_bb": resbb, "res_ab": resab}
10211087

10221088
res = OTResult(
10231089
value=value,
10241090
value_linear=value_linear,
1025-
plan=res12.plan,
1026-
potentials=res12.potentials,
1027-
sparse_plan=res12.sparse_plan,
1028-
lazy_plan=res12.lazy_plan,
1029-
status=res12.status,
1091+
plan=resab.plan,
1092+
potentials=resab.potentials,
1093+
sparse_plan=resab.sparse_plan,
1094+
lazy_plan=resab.lazy_plan,
1095+
status=resab.status,
10301096
log=log,
1031-
backend=res12.backend,
1097+
backend=nx,
10321098
)
1099+
10331100
# return debiased result
10341101
return res
10351102

test/test_solvers.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,31 @@ def test_solve_sample_methods(nx, method_params):
797797
np.testing.assert_allclose(sol2.value, 0, atol=1e-10)
798798

799799

800+
@pytest.mark.parametrize("debias", [True, False, "split"])
801+
def test_solve_sample_debias(nx, debias):
802+
n_samples_s = 10
803+
n_samples_t = 9
804+
n_features = 2
805+
rng = np.random.RandomState(42)
806+
807+
x = rng.randn(n_samples_s, n_features)
808+
y = rng.randn(n_samples_t, n_features)
809+
a = ot.utils.unif(n_samples_s)
810+
b = ot.utils.unif(n_samples_t)
811+
812+
xb, yb, ab, bb = nx.from_numpy(x, y, a, b)
813+
814+
sol = ot.solve_sample(x, y, reg=10, debias=debias)
815+
solb = ot.solve_sample(xb, yb, ab, bb, reg=10, debias=debias)
816+
817+
# check some attributes (no need )
818+
assert_allclose_sol(sol, solb)
819+
820+
sol2 = ot.solve_sample(x, x, reg=10, debias=True)
821+
822+
np.testing.assert_allclose(sol2.value, 0, atol=1e-10)
823+
824+
800825
@pytest.mark.parametrize("method_params", lst_parameters_solve_sample_NotImplemented)
801826
def test_solve_sample_NotImplemented(nx, method_params):
802827
n_samples_s = 20

0 commit comments

Comments
 (0)