From 2a32e2ea64d0d5096953a9b8259b0507fa58dca5 Mon Sep 17 00:00:00 2001 From: Kilian Date: Wed, 13 Nov 2019 13:55:24 +0100 Subject: fix log bug in gromov_wasserstein2 --- test/test_gromov.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'test') diff --git a/test/test_gromov.py b/test/test_gromov.py index 70fa83f..43da9fc 100644 --- a/test/test_gromov.py +++ b/test/test_gromov.py @@ -44,10 +44,14 @@ def test_gromov(): gw, log = ot.gromov.gromov_wasserstein2(C1, C2, p, q, 'kl_loss', log=True) + gw_val = ot.gromov.gromov_wasserstein2(C1, C2, p, q, 'kl_loss', log=False) + G = log['T'] np.testing.assert_allclose(gw, 0, atol=1e-1, rtol=1e-1) + np.testing.assert_allclose(gw, gw_val, atol=1e-1, rtol=1e-1) # cf log=False + # check constratints np.testing.assert_allclose( p, G.sum(1), atol=1e-04) # cf convergence gromov -- cgit v1.2.3 From 0280a3441b09c781035cda3b74213ec92026ff9e Mon Sep 17 00:00:00 2001 From: Kilian Date: Fri, 15 Nov 2019 16:10:37 +0100 Subject: fix bug numItermax emd in cg --- ot/optim.py | 6 ++++-- test/test_optim.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) (limited to 'test') diff --git a/ot/optim.py b/ot/optim.py index 0abd9e9..4012e0d 100644 --- a/ot/optim.py +++ b/ot/optim.py @@ -134,7 +134,7 @@ def solve_linesearch(cost, G, deltaG, Mi, f_val, return alpha, fc, f_val -def cg(a, b, M, reg, f, df, G0=None, numItermax=200, +def cg(a, b, M, reg, f, df, G0=None, numItermax=200, numItermaxEmd=100000, stopThr=1e-9, stopThr2=1e-9, verbose=False, log=False, **kwargs): """ Solve the general regularized OT problem with conditional gradient @@ -172,6 +172,8 @@ def cg(a, b, M, reg, f, df, G0=None, numItermax=200, initial guess (default is indep joint density) numItermax : int, optional Max number of iterations + numItermaxEmd : int, optional + Max number of iterations for emd stopThr : float, optional Stop threshol on the relative variation (>0) stopThr2 : float, optional @@ -238,7 +240,7 @@ def cg(a, b, M, reg, f, df, G0=None, numItermax=200, Mi += Mi.min() # solve linear program - Gc = emd(a, b, Mi) + Gc = emd(a, b, Mi, numItermax=numItermaxEmd) deltaG = Gc - G diff --git a/test/test_optim.py b/test/test_optim.py index ae31e1f..aade36e 100644 --- a/test/test_optim.py +++ b/test/test_optim.py @@ -37,6 +37,39 @@ def test_conditional_gradient(): np.testing.assert_allclose(b, G.sum(0)) +def test_conditional_gradient2(): + n = 4000 # nb samples + + mu_s = np.array([0, 0]) + cov_s = np.array([[1, 0], [0, 1]]) + + mu_t = np.array([4, 4]) + cov_t = np.array([[1, -.8], [-.8, 1]]) + + xs = ot.datasets.make_2D_samples_gauss(n, mu_s, cov_s) + xt = ot.datasets.make_2D_samples_gauss(n, mu_t, cov_t) + + a, b = np.ones((n,)) / n, np.ones((n,)) / n + + # loss matrix + M = ot.dist(xs, xt) + M /= M.max() + + def f(G): + return 0.5 * np.sum(G**2) + + def df(G): + return G + + reg = 1e-1 + + G, log = ot.optim.cg(a, b, M, reg, f, df, numItermaxEmd=200000, + verbose=True, log=True) + + np.testing.assert_allclose(a, G.sum(1)) + np.testing.assert_allclose(b, G.sum(0)) + + def test_generalized_conditional_gradient(): n_bins = 100 # nb bins -- cgit v1.2.3 From 57321bd0172c97b77dfc8b14972c18d063b6dda8 Mon Sep 17 00:00:00 2001 From: Rémi Flamary Date: Mon, 2 Dec 2019 11:13:07 +0100 Subject: add awesome sparse solver --- ot/lp/EMD_wrapper.cpp | 65 ++++++++++++++++++++++++++++++++++++--------------- ot/lp/emd_wrap.pyx | 2 +- test/test_ot.py | 20 ++++++++++++++++ 3 files changed, 67 insertions(+), 20 deletions(-) (limited to 'test') diff --git a/ot/lp/EMD_wrapper.cpp b/ot/lp/EMD_wrapper.cpp index 3ca7319..2aa44c1 100644 --- a/ot/lp/EMD_wrapper.cpp +++ b/ot/lp/EMD_wrapper.cpp @@ -111,23 +111,19 @@ int EMD_wrap_return_sparse(int n1, int n2, double *X, double *Y, double *D, long *iG, long *jG, double *G, double* alpha, double* beta, double *cost, int maxIter) { // beware M and C anre strored in row major C style!!! - int n, m, i, cur; + + // Get the number of non zero coordinates for r and c and vectors + int n, m, i, cur; typedef FullBipartiteDigraph Digraph; DIGRAPH_TYPEDEFS(FullBipartiteDigraph); - std::vector indI(n), indJ(m); - std::vector weights1(n), weights2(m); - Digraph di(n, m); - NetworkSimplexSimple net(di, true, n+m, n*m, maxIter); - - // Get the number of non zero coordinates for r and c and vectors + // Get the number of non zero coordinates for r and c n=0; for (int i=0; i0) { - weights1[ n ] = val; - indI[n++]=i; + n++; }else if(val<0){ return INFEASIBLE; } @@ -136,13 +132,41 @@ int EMD_wrap_return_sparse(int n1, int n2, double *X, double *Y, double *D, for (int i=0; i0) { - weights2[ m ] = -val; - indJ[m++]=i; + m++; }else if(val<0){ return INFEASIBLE; } } + // Define the graph + + std::vector indI(n), indJ(m); + std::vector weights1(n), weights2(m); + Digraph di(n, m); + NetworkSimplexSimple net(di, true, n+m, n*m, maxIter); + + // Set supply and demand, don't account for 0 values (faster) + + cur=0; + for (int i=0; i0) { + weights1[ cur ] = val; + indI[cur++]=i; + } + } + + // Demand is actually negative supply... + + cur=0; + for (int i=0; i0) { + weights2[ cur ] = -val; + indJ[cur++]=i; + } + } + // Define the graph net.supplyMap(&weights1[0], n, &weights2[0], m); @@ -166,14 +190,17 @@ int EMD_wrap_return_sparse(int n1, int n2, double *X, double *Y, double *D, int i = di.source(a); int j = di.target(a); double flow = net.flow(a); - *cost += flow * (*(D+indI[i]*n2+indJ[j-n])); - - *(G+cur) = flow; - *(iG+cur) = indI[i]; - *(jG+cur) = indJ[j]; - *(alpha + indI[i]) = -net.potential(i); - *(beta + indJ[j-n]) = net.potential(j); - cur++; + if (flow>0) + { + *cost += flow * (*(D+indI[i]*n2+indJ[j-n])); + + *(G+cur) = flow; + *(iG+cur) = indI[i]; + *(jG+cur) = indJ[j-n]; + *(alpha + indI[i]) = -net.potential(i); + *(beta + indJ[j-n]) = net.potential(j); + cur++; + } } } diff --git a/ot/lp/emd_wrap.pyx b/ot/lp/emd_wrap.pyx index 345cb66..f183995 100644 --- a/ot/lp/emd_wrap.pyx +++ b/ot/lp/emd_wrap.pyx @@ -111,7 +111,7 @@ def emd_c(np.ndarray[double, ndim=1, mode="c"] a, np.ndarray[double, ndim=1, mod jG=np.zeros(nmax,dtype=np.int) - result_code = EMD_wrap_return_sparse(n1, n2, a.data, b.data, M.data, iG.data, jG.data, G.data, alpha.data, beta.data, &cost, max_iter) + result_code = EMD_wrap_return_sparse(n1, n2, a.data, b.data, M.data, iG.data, jG.data, Gv.data, alpha.data, beta.data, &cost, max_iter) return Gv, iG, jG, cost, alpha, beta, result_code diff --git a/test/test_ot.py b/test/test_ot.py index dacae0a..4d59e12 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -118,6 +118,26 @@ def test_emd_empty(): np.testing.assert_allclose(w, 0) +def test_emd_sparse(): + + n = 100 + rng = np.random.RandomState(0) + + x = rng.randn(n, 2) + x2 = rng.randn(n, 2) + u = ot.utils.unif(n) + + M = ot.dist(x, x2) + + G = ot.emd([], [], M) + + Gs = ot.emd([], [], M, sparse=True) + + # check G is the same + np.testing.assert_allclose(G, Gs.todense()) + # check constraints + + def test_emd2_multi(): n = 500 # nb bins -- cgit v1.2.3 From a6a654de5e78dd388a793fbd26f60045b05d519c Mon Sep 17 00:00:00 2001 From: Rémi Flamary Date: Mon, 2 Dec 2019 11:31:32 +0100 Subject: proper documentation and parameter --- ot/lp/EMD.h | 2 +- ot/lp/EMD_wrapper.cpp | 3 ++- ot/lp/__init__.py | 16 ++++++++++++++-- ot/lp/emd_wrap.pyx | 10 ++++++---- test/test_ot.py | 2 +- 5 files changed, 24 insertions(+), 9 deletions(-) (limited to 'test') diff --git a/ot/lp/EMD.h b/ot/lp/EMD.h index bc513d2..9896091 100644 --- a/ot/lp/EMD.h +++ b/ot/lp/EMD.h @@ -33,7 +33,7 @@ enum ProblemType { int EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double* alpha, double* beta, double *cost, int maxIter); int EMD_wrap_return_sparse(int n1, int n2, double *X, double *Y, double *D, - long *iG, long *jG, double *G, + long *iG, long *jG, double *G, long * nG, double* alpha, double* beta, double *cost, int maxIter); #endif diff --git a/ot/lp/EMD_wrapper.cpp b/ot/lp/EMD_wrapper.cpp index 2aa44c1..9be2cdc 100644 --- a/ot/lp/EMD_wrapper.cpp +++ b/ot/lp/EMD_wrapper.cpp @@ -108,7 +108,7 @@ int EMD_wrap(int n1, int n2, double *X, double *Y, double *D, double *G, int EMD_wrap_return_sparse(int n1, int n2, double *X, double *Y, double *D, - long *iG, long *jG, double *G, + long *iG, long *jG, double *G, long * nG, double* alpha, double* beta, double *cost, int maxIter) { // beware M and C anre strored in row major C style!!! @@ -202,6 +202,7 @@ int EMD_wrap_return_sparse(int n1, int n2, double *X, double *Y, double *D, cur++; } } + *nG=cur; // nb of value +1 for numpy indexing } diff --git a/ot/lp/__init__.py b/ot/lp/__init__.py index 4fec7d9..d476071 100644 --- a/ot/lp/__init__.py +++ b/ot/lp/__init__.py @@ -27,7 +27,7 @@ __all__=['emd', 'emd2', 'barycenter', 'free_support_barycenter', 'cvx', 'emd_1d', 'emd2_1d', 'wasserstein_1d'] -def emd(a, b, M, numItermax=100000, log=False, sparse=False): +def emd(a, b, M, numItermax=100000, log=False, dense=True): r"""Solves the Earth Movers distance problem and returns the OT matrix @@ -62,6 +62,10 @@ def emd(a, b, M, numItermax=100000, log=False, sparse=False): log: bool, optional (default=False) If True, returns a dictionary containing the cost and dual variables. Otherwise returns only the optimal transportation matrix. + dense: boolean, optional (default=True) + If True, returns math:`\gamma` as a dense ndarray of shape (ns, nt). + Otherwise returns a sparse representation using scipy's `coo_matrix` + format. Returns ------- @@ -103,6 +107,8 @@ def emd(a, b, M, numItermax=100000, log=False, sparse=False): b = np.asarray(b, dtype=np.float64) M = np.asarray(M, dtype=np.float64) + sparse= not dense + # if empty array given then use uniform distributions if len(a) == 0: a = np.ones((M.shape[0],), dtype=np.float64) / M.shape[0] @@ -128,7 +134,7 @@ def emd(a, b, M, numItermax=100000, log=False, sparse=False): def emd2(a, b, M, processes=multiprocessing.cpu_count(), - numItermax=100000, log=False, sparse=False, return_matrix=False): + numItermax=100000, log=False, dense=True, return_matrix=False): r"""Solves the Earth Movers distance problem and returns the loss .. math:: @@ -166,6 +172,10 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), variables. Otherwise returns only the optimal transportation cost. return_matrix: boolean, optional (default=False) If True, returns the optimal transportation matrix in the log. + dense: boolean, optional (default=True) + If True, returns math:`\gamma` as a dense ndarray of shape (ns, nt). + Otherwise returns a sparse representation using scipy's `coo_matrix` + format. Returns ------- @@ -207,6 +217,8 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), b = np.asarray(b, dtype=np.float64) M = np.asarray(M, dtype=np.float64) + sparse=not dense + # problem with pikling Forks if sys.platform.endswith('win32'): processes=1 diff --git a/ot/lp/emd_wrap.pyx b/ot/lp/emd_wrap.pyx index f183995..4b6cdce 100644 --- a/ot/lp/emd_wrap.pyx +++ b/ot/lp/emd_wrap.pyx @@ -21,7 +21,7 @@ import warnings cdef extern from "EMD.h": int EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double* alpha, double* beta, double *cost, int maxIter) int EMD_wrap_return_sparse(int n1, int n2, double *X, double *Y, double *D, - long *iG, long *jG, double *G, + long *iG, long *jG, double *G, long * nG, double* alpha, double* beta, double *cost, int maxIter) cdef enum ProblemType: INFEASIBLE, OPTIMAL, UNBOUNDED, MAX_ITER_REACHED @@ -75,7 +75,8 @@ def emd_c(np.ndarray[double, ndim=1, mode="c"] a, np.ndarray[double, ndim=1, mod max_iter : int The maximum number of iterations before stopping the optimization algorithm if it has not converged. - + sparse : bool + Returning a sparse transport matrix if set to True Returns ------- @@ -87,6 +88,7 @@ def emd_c(np.ndarray[double, ndim=1, mode="c"] a, np.ndarray[double, ndim=1, mod cdef int n2= M.shape[1] cdef int nmax=n1+n2-1 cdef int result_code = 0 + cdef int nG=0 cdef double cost=0 cdef np.ndarray[double, ndim=1, mode="c"] alpha=np.zeros(n1) @@ -111,10 +113,10 @@ def emd_c(np.ndarray[double, ndim=1, mode="c"] a, np.ndarray[double, ndim=1, mod jG=np.zeros(nmax,dtype=np.int) - result_code = EMD_wrap_return_sparse(n1, n2, a.data, b.data, M.data, iG.data, jG.data, Gv.data, alpha.data, beta.data, &cost, max_iter) + result_code = EMD_wrap_return_sparse(n1, n2, a.data, b.data, M.data, iG.data, jG.data, Gv.data, &nG, alpha.data, beta.data, &cost, max_iter) - return Gv, iG, jG, cost, alpha, beta, result_code + return Gv[:nG], iG[:nG], jG[:nG], cost, alpha, beta, result_code else: diff --git a/test/test_ot.py b/test/test_ot.py index 4d59e12..7b44fd1 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -131,7 +131,7 @@ def test_emd_sparse(): G = ot.emd([], [], M) - Gs = ot.emd([], [], M, sparse=True) + Gs = ot.emd([], [], M, dense=False) # check G is the same np.testing.assert_allclose(G, Gs.todense()) -- cgit v1.2.3 From 127adbaf4eef7a6dffbdcd4f930fc6301587f861 Mon Sep 17 00:00:00 2001 From: Rémi Flamary Date: Mon, 2 Dec 2019 11:41:13 +0100 Subject: remove useless variable --- test/test_ot.py | 1 - 1 file changed, 1 deletion(-) (limited to 'test') diff --git a/test/test_ot.py b/test/test_ot.py index 7b44fd1..8602022 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -125,7 +125,6 @@ def test_emd_sparse(): x = rng.randn(n, 2) x2 = rng.randn(n, 2) - u = ot.utils.unif(n) M = ot.dist(x, x2) -- cgit v1.2.3 From 84384dd9e5dc78ed5cc867a53bd1de31c05d77fc Mon Sep 17 00:00:00 2001 From: Rémi Flamary Date: Mon, 2 Dec 2019 13:34:05 +0100 Subject: add test emd2 --- test/test_ot.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'test') diff --git a/test/test_ot.py b/test/test_ot.py index 8602022..507d188 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -132,9 +132,12 @@ def test_emd_sparse(): Gs = ot.emd([], [], M, dense=False) + ws = ot.emd2([], [], M, dense=False) + # check G is the same np.testing.assert_allclose(G, Gs.todense()) - # check constraints + # check value + np.testing.assert_allclose(Gs.multiply(M).sum(), ws, rtol=1e-6) def test_emd2_multi(): -- cgit v1.2.3 From 7371b2f4f931db8f67ec2967253be8d95ff9fe80 Mon Sep 17 00:00:00 2001 From: Rémi Flamary Date: Mon, 2 Dec 2019 13:34:55 +0100 Subject: add test emd2 --- test/test_ot.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'test') diff --git a/test/test_ot.py b/test/test_ot.py index 507d188..48ea87f 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -171,7 +171,12 @@ def test_emd2_multi(): emdn = ot.emd2(a, b, M) ot.toc('multi proc : {} s') + ot.tic() + emdn2 = ot.emd2(a, b, M, dense = False) + ot.toc('multi proc : {} s') + np.testing.assert_allclose(emd1, emdn) + np.testing.assert_allclose(emd1, emdn2) # emd loss multipro proc with log ot.tic() -- cgit v1.2.3 From dfaba55affcca606e8e041bdbd0fc5a7735c2b07 Mon Sep 17 00:00:00 2001 From: Rémi Flamary Date: Mon, 2 Dec 2019 13:36:08 +0100 Subject: add test emd2 multi --- test/test_ot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'test') diff --git a/test/test_ot.py b/test/test_ot.py index 48ea87f..470fd0f 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -176,7 +176,7 @@ def test_emd2_multi(): ot.toc('multi proc : {} s') np.testing.assert_allclose(emd1, emdn) - np.testing.assert_allclose(emd1, emdn2) + np.testing.assert_allclose(emd1, emdn2, rtol=1e-6) # emd loss multipro proc with log ot.tic() -- cgit v1.2.3 From c439e3efb920086154c741b41f65d99165e875d8 Mon Sep 17 00:00:00 2001 From: Rémi Flamary Date: Mon, 2 Dec 2019 13:57:13 +0100 Subject: pep8 --- test/test_ot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'test') diff --git a/test/test_ot.py b/test/test_ot.py index 470fd0f..fbacd8b 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -172,8 +172,8 @@ def test_emd2_multi(): ot.toc('multi proc : {} s') ot.tic() - emdn2 = ot.emd2(a, b, M, dense = False) - ot.toc('multi proc : {} s') + emdn2 = ot.emd2(a, b, M, dense=False) + ot.toc('multi proc : {} s') np.testing.assert_allclose(emd1, emdn) np.testing.assert_allclose(emd1, emdn2, rtol=1e-6) -- cgit v1.2.3 From 92233f79e098f1930248d815e66c0a929508af59 Mon Sep 17 00:00:00 2001 From: Kilian Date: Mon, 9 Dec 2019 15:56:48 +0100 Subject: add assert for emd dimension mismatch --- ot/lp/__init__.py | 6 ++++++ test/test_ot.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) (limited to 'test') diff --git a/ot/lp/__init__.py b/ot/lp/__init__.py index 0c92810..f77c3d7 100644 --- a/ot/lp/__init__.py +++ b/ot/lp/__init__.py @@ -109,6 +109,9 @@ def emd(a, b, M, numItermax=100000, log=False): if len(b) == 0: b = np.ones((M.shape[1],), dtype=np.float64) / M.shape[1] + assert (a.shape[0] == M.shape[0] or b.shape[0] == M.shape[1]), \ + "Dimension mismatch, check dimensions of M with a and b" + G, cost, u, v, result_code = emd_c(a, b, M, numItermax) result_code_string = check_result(result_code) if log: @@ -212,6 +215,9 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), if len(b) == 0: b = np.ones((M.shape[1],), dtype=np.float64) / M.shape[1] + assert (a.shape[0] == M.shape[0] or b.shape[0] == M.shape[1]), \ + "Dimension mismatch, check dimensions of M with a and b" + if log or return_matrix: def f(b): G, cost, u, v, resultCode = emd_c(a, b, M, numItermax) diff --git a/test/test_ot.py b/test/test_ot.py index dacae0a..1343604 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -14,6 +14,22 @@ from ot.datasets import make_1D_gauss as gauss import pytest +def test_emd_dimension_mismatch(): + # test emd and emd2 for simple identity + n_samples = 100 + n_features = 2 + rng = np.random.RandomState(0) + + x = rng.randn(n_samples, n_features) + a = ot.utils.unif(n_samples + 1) + + M = ot.dist(x, x) + + np.testing.assert_raises(AssertionError, emd, a, a, M) + + np.testing.assert_raises(AssertionError, emd2, a, a, M) + + def test_emd_emd2(): # test emd and emd2 for simple identity n = 100 -- cgit v1.2.3 From 428b44e15591071cfcd69af365d878cfd876f9d3 Mon Sep 17 00:00:00 2001 From: Kilian Date: Mon, 9 Dec 2019 16:35:49 +0100 Subject: calling ot.emd test --- test/test_ot.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'test') diff --git a/test/test_ot.py b/test/test_ot.py index 1343604..25cdfd4 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -14,8 +14,9 @@ from ot.datasets import make_1D_gauss as gauss import pytest + def test_emd_dimension_mismatch(): - # test emd and emd2 for simple identity + # test emd and emd2 for dimension mismatch n_samples = 100 n_features = 2 rng = np.random.RandomState(0) @@ -25,9 +26,9 @@ def test_emd_dimension_mismatch(): M = ot.dist(x, x) - np.testing.assert_raises(AssertionError, emd, a, a, M) + np.testing.assert_raises(AssertionError, ot.emd, a, a, M) - np.testing.assert_raises(AssertionError, emd2, a, a, M) + np.testing.assert_raises(AssertionError, ot.emd2, a, a, M) def test_emd_emd2(): -- cgit v1.2.3 From 92dbe259032d340a259209e477e9aac74897689e Mon Sep 17 00:00:00 2001 From: Kilian Date: Mon, 9 Dec 2019 16:43:54 +0100 Subject: pep8 --- test/test_ot.py | 1 - 1 file changed, 1 deletion(-) (limited to 'test') diff --git a/test/test_ot.py b/test/test_ot.py index 25cdfd4..42a3d0a 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -14,7 +14,6 @@ from ot.datasets import make_1D_gauss as gauss import pytest - def test_emd_dimension_mismatch(): # test emd and emd2 for dimension mismatch n_samples = 100 -- cgit v1.2.3 From d97f81dd731c4b1132939500076fd48c89f19d1f Mon Sep 17 00:00:00 2001 From: Rémi Flamary Date: Wed, 18 Dec 2019 10:17:31 +0100 Subject: update test --- test/test_ot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'test') diff --git a/test/test_ot.py b/test/test_ot.py index fbacd8b..3dd544c 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -128,7 +128,7 @@ def test_emd_sparse(): M = ot.dist(x, x2) - G = ot.emd([], [], M) + G = ot.emd([], [], M, dense=True) Gs = ot.emd([], [], M, dense=False) -- cgit v1.2.3 From 365adbccc73f7fea28811b16cbbbdbb77761e55c Mon Sep 17 00:00:00 2001 From: "Mokhtar Z. Alaya" Date: Fri, 10 Jan 2020 13:01:42 +0100 Subject: add simple test for screenkhorn --- test/test_bregman.py | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'test') diff --git a/test/test_bregman.py b/test/test_bregman.py index f70df10..eb74a9f 100644 --- a/test/test_bregman.py +++ b/test/test_bregman.py @@ -337,3 +337,14 @@ def test_implemented_methods(): ot.bregman.sinkhorn(a, b, M, epsilon, method=method) with pytest.raises(ValueError): ot.bregman.sinkhorn2(a, b, M, epsilon, method=method) + +def test_screenkhorn(): + # test screenkhorn + rng = np.random.RandomState(0) + n = 100 + a = ot.unif(n) + b = ot.unif(n) + + x = rng.randn(n, 2) + M = ot.dist(x, x) + G_screen = ot.bregman.screenkhorn(a, b, M, 1e-1) \ No newline at end of file -- cgit v1.2.3 From 18242437e73aba9cf131fafc1571e376b57f25f6 Mon Sep 17 00:00:00 2001 From: "Mokhtar Z. Alaya" Date: Mon, 13 Jan 2020 09:50:49 +0100 Subject: fix simple test of screenkhorn in test/ --- test/test_bregman.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'test') diff --git a/test/test_bregman.py b/test/test_bregman.py index eb74a9f..bc8f6ae 100644 --- a/test/test_bregman.py +++ b/test/test_bregman.py @@ -347,4 +347,4 @@ def test_screenkhorn(): x = rng.randn(n, 2) M = ot.dist(x, x) - G_screen = ot.bregman.screenkhorn(a, b, M, 1e-1) \ No newline at end of file + G_screen = ot.bregman.screenkhorn(a, b, M, 1e-2, uniform=True, verbose=True) \ No newline at end of file -- cgit v1.2.3 From 4918d2c619aaa654c524c9c5dc7f4dc82b838f82 Mon Sep 17 00:00:00 2001 From: "Mokhtar Z. Alaya" Date: Thu, 16 Jan 2020 16:44:40 +0100 Subject: update readme --- README.md | 2 +- test/test_bregman.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'test') diff --git a/README.md b/README.md index 987adf1..c115776 100644 --- a/README.md +++ b/README.md @@ -256,4 +256,4 @@ You can also post bug reports and feature requests in Github issues. Make sure t [25] Frogner C., Zhang C., Mobahi H., Araya-Polo M., Poggio T. (2015). [Learning with a Wasserstein Loss](http://cbcl.mit.edu/wasserstein/) Advances in Neural Information Processing Systems (NIPS). -[26] Alaya M. Z., Bérar M., Gasso G., Rakotomamonjy A. (2019). [Screening Sinkhorn Algorithm for Regularized Optimal Transport](https://papers.nips.cc/paper/9386-screening-sinkhorn-algorithm-for-regularized-optimal-transport), Advances in Neural Information Processing Systems 33 (NIPS). +[26] Alaya M. Z., Bérar M., Gasso G., Rakotomamonjy A. (2019). [Screening Sinkhorn Algorithm for Regularized Optimal Transport](https://papers.nips.cc/paper/9386-screening-sinkhorn-algorithm-for-regularized-optimal-transport), Advances in Neural Information Processing Systems 33 (NeurIPS). diff --git a/test/test_bregman.py b/test/test_bregman.py index bc8f6ae..52e9fb2 100644 --- a/test/test_bregman.py +++ b/test/test_bregman.py @@ -338,6 +338,7 @@ def test_implemented_methods(): with pytest.raises(ValueError): ot.bregman.sinkhorn2(a, b, M, epsilon, method=method) + def test_screenkhorn(): # test screenkhorn rng = np.random.RandomState(0) @@ -347,4 +348,4 @@ def test_screenkhorn(): x = rng.randn(n, 2) M = ot.dist(x, x) - G_screen = ot.bregman.screenkhorn(a, b, M, 1e-2, uniform=True, verbose=True) \ No newline at end of file + G_screen = ot.bregman.screenkhorn(a, b, M, 1e-2, uniform=True, verbose=True) -- cgit v1.2.3 From 936b5e1eb965e1d8c71b7b26cfa5238face1aaa3 Mon Sep 17 00:00:00 2001 From: "Mokhtar Z. Alaya" Date: Thu, 16 Jan 2020 17:13:01 +0100 Subject: update --- .idea/POT.iml | 11 +++++++++++ test/test_bregman.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 .idea/POT.iml (limited to 'test') diff --git a/.idea/POT.iml b/.idea/POT.iml new file mode 100644 index 0000000..6711606 --- /dev/null +++ b/.idea/POT.iml @@ -0,0 +1,11 @@ + + + + + + + + + + \ No newline at end of file diff --git a/test/test_bregman.py b/test/test_bregman.py index 52e9fb2..bcec095 100644 --- a/test/test_bregman.py +++ b/test/test_bregman.py @@ -348,4 +348,4 @@ def test_screenkhorn(): x = rng.randn(n, 2) M = ot.dist(x, x) - G_screen = ot.bregman.screenkhorn(a, b, M, 1e-2, uniform=True, verbose=True) + G_screen = ot.bregman.screenkhorn(a, b, M, 1e-2, uniform=True, verbose=True) \ No newline at end of file -- cgit v1.2.3 From 3be0c215143e16c59ddd3be902416e91c3292937 Mon Sep 17 00:00:00 2001 From: "Mokhtar Z. Alaya" Date: Sat, 18 Jan 2020 07:15:09 +0100 Subject: clean --- examples/plot_screenkhorn_1D.py | 2 +- test/test_bregman.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) (limited to 'test') diff --git a/examples/plot_screenkhorn_1D.py b/examples/plot_screenkhorn_1D.py index 103d54c..7c0de82 100644 --- a/examples/plot_screenkhorn_1D.py +++ b/examples/plot_screenkhorn_1D.py @@ -59,7 +59,7 @@ ot.plot.plot1D_mat(a, b, M, 'Cost matrix M') # ----------------------- # Screenkhorn -lambd = 1e-3 # entropy parameter +lambd = 1e-03 # entropy parameter ns_budget = 30 # budget number of points to be keeped in the source distribution nt_budget = 30 # budget number of points to be keeped in the target distribution diff --git a/test/test_bregman.py b/test/test_bregman.py index bcec095..2398d45 100644 --- a/test/test_bregman.py +++ b/test/test_bregman.py @@ -348,4 +348,7 @@ def test_screenkhorn(): x = rng.randn(n, 2) M = ot.dist(x, x) - G_screen = ot.bregman.screenkhorn(a, b, M, 1e-2, uniform=True, verbose=True) \ No newline at end of file + G_sink = ot.sinkhorn(a, b, M, 1e-03) + G_screen = ot.bregman.screenkhorn(a, b, M, 1e-03, uniform=True, verbose=True) + np.testing.assert_allclose(G_sink.sum(0), G_screen.sum(0), atol=1e-02) + np.testing.assert_allclose(G_sink.sum(1), G_screen.sum(1), atol=1e-02) \ No newline at end of file -- cgit v1.2.3 From b3fb1ef40a482f0989686b79373060d764b62d38 Mon Sep 17 00:00:00 2001 From: "Mokhtar Z. Alaya" Date: Sat, 18 Jan 2020 07:45:34 +0100 Subject: clean --- ot/bregman.py | 3 ++- test/test_bregman.py | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) (limited to 'test') diff --git a/ot/bregman.py b/ot/bregman.py index aff9f8c..c304b5d 100644 --- a/ot/bregman.py +++ b/ot/bregman.py @@ -2117,10 +2117,11 @@ def screenkhorn(a, b, M, reg, ns_budget=None, nt_budget=None, uniform=False, res log['v'] = vsc_full log['Isel'] = Isel log['Jsel'] = Jsel + gamma = usc_full[:, None] * K * vsc_full[None, :] gamma = gamma / gamma.sum() if log: return gamma, log else: - return gamma \ No newline at end of file + return gamma diff --git a/test/test_bregman.py b/test/test_bregman.py index 2398d45..e376715 100644 --- a/test/test_bregman.py +++ b/test/test_bregman.py @@ -348,7 +348,7 @@ def test_screenkhorn(): x = rng.randn(n, 2) M = ot.dist(x, x) - G_sink = ot.sinkhorn(a, b, M, 1e-03) - G_screen = ot.bregman.screenkhorn(a, b, M, 1e-03, uniform=True, verbose=True) - np.testing.assert_allclose(G_sink.sum(0), G_screen.sum(0), atol=1e-02) - np.testing.assert_allclose(G_sink.sum(1), G_screen.sum(1), atol=1e-02) \ No newline at end of file + G_s = ot.sinkhorn(a, b, M, 1e-03) + G_sc = ot.bregman.screenkhorn(a, b, M, 1e-03, uniform=True, verbose=True) + np.testing.assert_allclose(G_s.sum(0), G_sc.sum(0), atol=1e-02) + np.testing.assert_allclose(G_s.sum(1), G_sc.sum(1), atol=1e-02) \ No newline at end of file -- cgit v1.2.3 From 7f7b1c547b54b394db975f4ff9d0287904a7b820 Mon Sep 17 00:00:00 2001 From: "Mokhtar Z. Alaya" Date: Sat, 18 Jan 2020 09:04:48 +0100 Subject: make autopep --- test/test_bregman.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) (limited to 'test') diff --git a/test/test_bregman.py b/test/test_bregman.py index e376715..fd0679b 100644 --- a/test/test_bregman.py +++ b/test/test_bregman.py @@ -106,7 +106,6 @@ def test_sinkhorn_variants_log(): @pytest.mark.parametrize("method", ["sinkhorn", "sinkhorn_stabilized"]) def test_barycenter(method): - n_bins = 100 # nb bins # Gaussian distributions @@ -133,7 +132,6 @@ def test_barycenter(method): def test_barycenter_stabilization(): - n_bins = 100 # nb bins # Gaussian distributions @@ -161,7 +159,6 @@ def test_barycenter_stabilization(): def test_wasserstein_bary_2d(): - size = 100 # size of a square image a1 = np.random.randn(size, size) a1 += a1.min() @@ -185,7 +182,6 @@ def test_wasserstein_bary_2d(): def test_unmix(): - n_bins = 50 # nb bins # Gaussian distributions @@ -207,7 +203,7 @@ def test_unmix(): # wasserstein reg = 1e-3 - um = ot.bregman.unmix(a, D, M, M0, h0, reg, 1, alpha=0.01,) + um = ot.bregman.unmix(a, D, M, M0, h0, reg, 1, alpha=0.01, ) np.testing.assert_allclose(1, np.sum(um), rtol=1e-03, atol=1e-03) np.testing.assert_allclose([0.5, 0.5], um, rtol=1e-03, atol=1e-03) @@ -256,7 +252,7 @@ def test_empirical_sinkhorn(): def test_empirical_sinkhorn_divergence(): - #Test sinkhorn divergence + # Test sinkhorn divergence n = 10 a = ot.unif(n) b = ot.unif(n) @@ -348,7 +344,10 @@ def test_screenkhorn(): x = rng.randn(n, 2) M = ot.dist(x, x) - G_s = ot.sinkhorn(a, b, M, 1e-03) - G_sc = ot.bregman.screenkhorn(a, b, M, 1e-03, uniform=True, verbose=True) - np.testing.assert_allclose(G_s.sum(0), G_sc.sum(0), atol=1e-02) - np.testing.assert_allclose(G_s.sum(1), G_sc.sum(1), atol=1e-02) \ No newline at end of file + # sinkhorn + G_sink = ot.sinkhorn(a, b, M, 1e-03) + # screenkhorn + G_screen = ot.bregman.screenkhorn(a, b, M, 1e-03, uniform=True, verbose=True) + # check marginals + np.testing.assert_allclose(G_sink.sum(0), G_screen.sum(0), atol=1e-02) + np.testing.assert_allclose(G_s.sum(1), G_screen.sum(1), atol=1e-02) -- cgit v1.2.3 From a1747a10e80751eacca4273af61083a853fb9dd4 Mon Sep 17 00:00:00 2001 From: "Mokhtar Z. Alaya" Date: Sat, 18 Jan 2020 09:12:55 +0100 Subject: make autopep --- test/test_bregman.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'test') diff --git a/test/test_bregman.py b/test/test_bregman.py index fd0679b..f54ba9f 100644 --- a/test/test_bregman.py +++ b/test/test_bregman.py @@ -350,4 +350,4 @@ def test_screenkhorn(): G_screen = ot.bregman.screenkhorn(a, b, M, 1e-03, uniform=True, verbose=True) # check marginals np.testing.assert_allclose(G_sink.sum(0), G_screen.sum(0), atol=1e-02) - np.testing.assert_allclose(G_s.sum(1), G_screen.sum(1), atol=1e-02) + np.testing.assert_allclose(G_sink.sum(1), G_screen.sum(1), atol=1e-02) -- cgit v1.2.3 From 3844639d3dd4e0dd360ebef34dd657d26664039e Mon Sep 17 00:00:00 2001 From: Rémi Flamary Date: Mon, 27 Jan 2020 09:35:19 +0100 Subject: add test for constraint viuolation of duals --- test/test_ot.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'test') diff --git a/test/test_ot.py b/test/test_ot.py index 18b6294..c756e51 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -337,7 +337,10 @@ def test_dual_variables(): # Check that both cost computations are equivalent np.testing.assert_almost_equal(cost1, log['cost']) check_duality_gap(a, b, M, G, log['u'], log['v'], log['cost']) - + + viol=log['u'][:,None]+log['v'][None,:]-M + + assert viol.max()<1e-8 def check_duality_gap(a, b, M, G, u, v, cost): cost_dual = np.vdot(a, u) + np.vdot(b, v) -- cgit v1.2.3 From 30fc233f7f62d571a562971a945d68c3782f0780 Mon Sep 17 00:00:00 2001 From: Rémi Flamary Date: Mon, 27 Jan 2020 09:37:42 +0100 Subject: correct pep8 --- test/test_ot.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'test') diff --git a/test/test_ot.py b/test/test_ot.py index c756e51..245a107 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -337,10 +337,11 @@ def test_dual_variables(): # Check that both cost computations are equivalent np.testing.assert_almost_equal(cost1, log['cost']) check_duality_gap(a, b, M, G, log['u'], log['v'], log['cost']) - - viol=log['u'][:,None]+log['v'][None,:]-M - - assert viol.max()<1e-8 + + viol = log['u'][:, None] + log['v'][None, :] - M + + assert viol.max() < 1e-8 + def check_duality_gap(a, b, M, G, u, v, cost): cost_dual = np.vdot(a, u) + np.vdot(b, v) -- cgit v1.2.3 From f65073faa73b36280a19ff8b9c383e66f8bdbd2b Mon Sep 17 00:00:00 2001 From: Rémi Flamary Date: Thu, 30 Jan 2020 08:04:36 +0100 Subject: comlete documentation --- ot/lp/__init__.py | 30 +++++++++++++++++++----------- ot/lp/emd_wrap.pyx | 6 ++++++ test/test_ot.py | 4 ++-- 3 files changed, 27 insertions(+), 13 deletions(-) (limited to 'test') diff --git a/ot/lp/__init__.py b/ot/lp/__init__.py index aa3166f..cdd505d 100644 --- a/ot/lp/__init__.py +++ b/ot/lp/__init__.py @@ -28,10 +28,10 @@ __all__ = ['emd', 'emd2', 'barycenter', 'free_support_barycenter', 'cvx', def center_ot_dual(alpha0, beta0, a=None, b=None): - r"""Center dual OT potentials wrt theirs weights + r"""Center dual OT potentials w.r.t. theirs weights The main idea of this function is to find unique dual potentials - that ensure some kind of centering/fairness. It will help have + that ensure some kind of centering/fairness. The main idea is to find dual potentials that lead to the same final objective value for both source and targets (see below for more details). It will help having stability when multiple calling of the OT solver with small changes. Basically we add another constraint to the potential that will not @@ -91,7 +91,15 @@ def center_ot_dual(alpha0, beta0, a=None, b=None): def estimate_dual_null_weights(alpha0, beta0, a, b, M): r"""Estimate feasible values for 0-weighted dual potentials - The feasible values are computed efficiently bjt rather coarsely. + The feasible values are computed efficiently but rather coarsely. + + .. warning:: + This function is necessary because the C++ solver in emd_c + discards all samples in the distributions with + zeros weights. This means that while the primal variable (transport + matrix) is exact, the solver only returns feasible dual potentials + on the samples with weights different from zero. + First we compute the constraints violations: .. math:: @@ -113,11 +121,11 @@ def estimate_dual_null_weights(alpha0, beta0, a, b, M): \beta_j = \beta_j -v^b_j \quad \text{ if } b_j=0 \text{ and } v^b_j>0 - In the end the dual potential are centred using function + In the end the dual potentials are centered using function :ref:`center_ot_dual`. Note that all those updates do not change the objective value of the - solution but provide dual potential that do not violate the constraints. + solution but provide dual potentials that do not violate the constraints. Parameters ---------- @@ -130,9 +138,9 @@ def estimate_dual_null_weights(alpha0, beta0, a, b, M): beta0 : (nt,) numpy.ndarray, float64 Target dual potential a : (ns,) numpy.ndarray, float64 - Source histogram (uniform weight if empty list) + Source distribution (uniform weights if empty list) b : (nt,) numpy.ndarray, float64 - Target histogram (uniform weight if empty list) + Target distribution (uniform weights if empty list) M : (ns,nt) numpy.ndarray, float64 Loss matrix (c-order array with type float64) @@ -150,11 +158,11 @@ def estimate_dual_null_weights(alpha0, beta0, a, b, M): bsel = b != 0 # compute dual constraints violation - Viol = alpha0[:, None] + beta0[None, :] - M + constraint_violation = alpha0[:, None] + beta0[None, :] - M - # Compute worst violation per line and columns - aviol = np.max(Viol, 1) - bviol = np.max(Viol, 0) + # Compute largest violation per line and columns + aviol = np.max(constraint_violation, 1) + bviol = np.max(constraint_violation, 0) # update corrects violation of alpha_up = -1 * ~asel * np.maximum(aviol, 0) diff --git a/ot/lp/emd_wrap.pyx b/ot/lp/emd_wrap.pyx index a4987f4..d345fd4 100644 --- a/ot/lp/emd_wrap.pyx +++ b/ot/lp/emd_wrap.pyx @@ -66,6 +66,12 @@ def emd_c(np.ndarray[double, ndim=1, mode="c"] a, np.ndarray[double, ndim=1, mod .. warning:: Note that the M matrix needs to be a C-order :py.cls:`numpy.array` + .. warning:: + The C++ solver discards all samples in the distributions with + zeros weights. This means that while the primal variable (transport + matrix) is exact, the solver only returns feasible dual potentials + on the samples with weights different from zero. + Parameters ---------- a : (ns,) numpy.ndarray, float64 diff --git a/test/test_ot.py b/test/test_ot.py index 245a107..47df946 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -338,9 +338,9 @@ def test_dual_variables(): np.testing.assert_almost_equal(cost1, log['cost']) check_duality_gap(a, b, M, G, log['u'], log['v'], log['cost']) - viol = log['u'][:, None] + log['v'][None, :] - M + constraint_violation = log['u'][:, None] + log['v'][None, :] - M - assert viol.max() < 1e-8 + assert constraint_violation.max() < 1e-8 def check_duality_gap(a, b, M, G, u, v, cost): -- cgit v1.2.3 From d82e6eb1af99a982a4934d6bc019a9ab4ad5c880 Mon Sep 17 00:00:00 2001 From: Alex Tong Date: Thu, 5 Mar 2020 12:05:16 -0500 Subject: Fix convolutional_barycenter kernel for non-symmetric images Add authorship --- ot/bregman.py | 8 +++++++- test/test_bregman.py | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) (limited to 'test') diff --git a/ot/bregman.py b/ot/bregman.py index 2707b7c..d5e3563 100644 --- a/ot/bregman.py +++ b/ot/bregman.py @@ -9,6 +9,7 @@ Bregman projections for regularized OT # Titouan Vayer # Hicham Janati # Mokhtar Z. Alaya +# Alexander Tong # # License: MIT License @@ -1346,12 +1347,17 @@ def convolutional_barycenter2d(A, reg, weights=None, numItermax=10000, err = 1 # build the convolution operator + # this is equivalent to blurring on horizontal then vertical directions t = np.linspace(0, 1, A.shape[1]) [Y, X] = np.meshgrid(t, t) xi1 = np.exp(-(X - Y)**2 / reg) + t = np.linspace(0, 1, A.shape[2]) + [Y, X] = np.meshgrid(t, t) + xi2 = np.exp(-(X - Y)**2 / reg) + def K(x): - return np.dot(np.dot(xi1, x), xi1) + return np.dot(np.dot(xi1, x), xi2) while (err > stopThr and cpt < numItermax): diff --git a/test/test_bregman.py b/test/test_bregman.py index f54ba9f..ec4388d 100644 --- a/test/test_bregman.py +++ b/test/test_bregman.py @@ -351,3 +351,10 @@ def test_screenkhorn(): # check marginals np.testing.assert_allclose(G_sink.sum(0), G_screen.sum(0), atol=1e-02) np.testing.assert_allclose(G_sink.sum(1), G_screen.sum(1), atol=1e-02) + + +def test_convolutional_barycenter_non_square(): + # test for image with height not equal width + A = np.ones((2, 2, 3)) / (2 * 3) + b = ot.bregman.convolutional_barycenter2d(A, 1e-03) + np.testing.assert_allclose(np.ones((2, 3)) / (2 * 3), b, atol=1e-02) -- cgit v1.2.3 From 6aa0f1f4e275098948d4b312530119e5d95b8884 Mon Sep 17 00:00:00 2001 From: ievred Date: Tue, 31 Mar 2020 17:12:28 +0200 Subject: v1 jcpot example test --- examples/plot_otda_jcpot.py | 185 +++++++++++++++++++++++++++++++ ot/da.py | 263 ++++++++++++++++++++++++++------------------ test/test_da.py | 63 ++++++++++- 3 files changed, 404 insertions(+), 107 deletions(-) create mode 100644 examples/plot_otda_jcpot.py (limited to 'test') diff --git a/examples/plot_otda_jcpot.py b/examples/plot_otda_jcpot.py new file mode 100644 index 0000000..5e5fff8 --- /dev/null +++ b/examples/plot_otda_jcpot.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +""" +======================== +OT for multi-source target shift +======================== + +This example introduces a target shift problem with two 2D source and 1 target domain. + +""" + +# Authors: Remi Flamary +# Ievgen Redko +# +# License: MIT License + +import pylab as pl +import numpy as np +import ot + +############################################################################## +# Generate data +# ------------- +n = 50 +sigma = 0.3 +np.random.seed(1985) + + +def get_data(n, p, dec): + y = np.concatenate((np.ones(int(p * n)), np.zeros(int((1 - p) * n)))) + x = np.hstack((0 * y[:, None] - 0, 1 - 2 * y[:, None])) + sigma * np.random.randn(len(y), 2) + + x[:, 0] += dec[0] + x[:, 1] += dec[1] + + return x, y + + +p1 = .2 +dec1 = [0, 2] + +p2 = .9 +dec2 = [0, -2] + +pt = .4 +dect = [4, 0] + +xs1, ys1 = get_data(n, p1, dec1) +xs2, ys2 = get_data(n + 1, p2, dec2) +xt, yt = get_data(n, pt, dect) +all_Xr = [xs1, xs2] +all_Yr = [ys1, ys2] +# %% +da = 1.5 + + +def plot_ax(dec, name): + pl.plot([dec[0], dec[0]], [dec[1] - da, dec[1] + da], 'k', alpha=0.5) + pl.plot([dec[0] - da, dec[0] + da], [dec[1], dec[1]], 'k', alpha=0.5) + pl.text(dec[0] - .5, dec[1] + 2, name) + + +############################################################################## +# Fig 1 : plots source and target samples +# --------------------------------------- + +pl.figure(1) +pl.clf() +plot_ax(dec1, 'Source 1') +plot_ax(dec2, 'Source 2') +plot_ax(dect, 'Target') +pl.scatter(xs1[:, 0], xs1[:, 1], c=ys1, s=35, marker='x', cmap='Set1', vmax=9, label='Source 1 (0.8,0.2)') +pl.scatter(xs2[:, 0], xs2[:, 1], c=ys2, s=35, marker='+', cmap='Set1', vmax=9, label='Source 2 (0.1,0.9)') +pl.scatter(xt[:, 0], xt[:, 1], c=yt, s=35, marker='o', cmap='Set1', vmax=9, label='Target (0.6,0.4)') +pl.title('Data') + +pl.legend() +pl.axis('equal') +pl.axis('off') + + +############################################################################## +# Instantiate Sinkhorn transport algorithm and fit them for all source domains +# ---------------------------------------------------------------------------- +ot_sinkhorn = ot.da.SinkhornTransport(reg_e=1e-2, metric='euclidean') + +M1 = ot.dist(xs1, xt, 'euclidean') +M2 = ot.dist(xs2, xt, 'euclidean') + + +def print_G(G, xs, ys, xt): + for i in range(G.shape[0]): + for j in range(G.shape[1]): + if G[i, j] > 5e-4: + if ys[i]: + c = 'b' + else: + c = 'r' + pl.plot([xs[i, 0], xt[j, 0]], [xs[i, 1], xt[j, 1]], c, alpha=.2) + + +############################################################################## +# Fig 2 : plot optimal couplings and transported samples +# ------------------------------------------------------ +pl.figure(2) +pl.clf() +plot_ax(dec1, 'Source 1') +plot_ax(dec2, 'Source 2') +plot_ax(dect, 'Target') +print_G(ot_sinkhorn.fit(Xs=xs1, Xt=xt).coupling_, xs1, ys1, xt) +print_G(ot_sinkhorn.fit(Xs=xs2, Xt=xt).coupling_, xs2, ys2, xt) +pl.scatter(xs1[:, 0], xs1[:, 1], c=ys1, s=35, marker='x', cmap='Set1', vmax=9) +pl.scatter(xs2[:, 0], xs2[:, 1], c=ys2, s=35, marker='+', cmap='Set1', vmax=9) +pl.scatter(xt[:, 0], xt[:, 1], c=yt, s=35, marker='o', cmap='Set1', vmax=9) + +pl.plot([], [], 'r', alpha=.2, label='Mass from Class 1') +pl.plot([], [], 'b', alpha=.2, label='Mass from Class 2') + +pl.title('Independent OT') + +pl.legend() +pl.axis('equal') +pl.axis('off') + + +############################################################################## +# Instantiate JCPOT adaptation algorithm and fit it +# ---------------------------------------------------------------------------- +otda = ot.da.JCPOTTransport(reg_e=1e-2, max_iter=1000, tol=1e-9, verbose=True, log=True) +otda.fit(all_Xr, all_Yr, xt) + +ws1 = otda.proportions_.dot(otda.log_['all_domains'][0]['D2']) +ws2 = otda.proportions_.dot(otda.log_['all_domains'][1]['D2']) + +pl.figure(3) +pl.clf() +plot_ax(dec1, 'Source 1') +plot_ax(dec2, 'Source 2') +plot_ax(dect, 'Target') +print_G(ot.bregman.sinkhorn(ws1, [], M1, reg=1e-2), xs1, ys1, xt) +print_G(ot.bregman.sinkhorn(ws2, [], M2, reg=1e-2), xs2, ys2, xt) +pl.scatter(xs1[:, 0], xs1[:, 1], c=ys1, s=35, marker='x', cmap='Set1', vmax=9) +pl.scatter(xs2[:, 0], xs2[:, 1], c=ys2, s=35, marker='+', cmap='Set1', vmax=9) +pl.scatter(xt[:, 0], xt[:, 1], c=yt, s=35, marker='o', cmap='Set1', vmax=9) + +pl.plot([], [], 'r', alpha=.2, label='Mass from Class 1') +pl.plot([], [], 'b', alpha=.2, label='Mass from Class 2') + +pl.title('OT with prop estimation ({:1.3f},{:1.3f})'.format(otda.proportions_[0], otda.proportions_[1])) + +pl.legend() +pl.axis('equal') +pl.axis('off') + +############################################################################## +# Run oracle transport algorithm with known proportions +# ---------------------------------------------------------------------------- + +otda = ot.da.JCPOTTransport(reg_e=0.01, max_iter=1000, tol=1e-9, verbose=True, log=True) +otda.fit(all_Xr, all_Yr, xt) + +h_res = np.array([1 - pt, pt]) + +ws1 = h_res.dot(otda.log_['all_domains'][0]['D2']) +ws2 = h_res.dot(otda.log_['all_domains'][1]['D2']) + +pl.figure(4) +pl.clf() +plot_ax(dec1, 'Source 1') +plot_ax(dec2, 'Source 2') +plot_ax(dect, 'Target') +print_G(ot.bregman.sinkhorn(ws1, [], M1, reg=1e-2), xs1, ys1, xt) +print_G(ot.bregman.sinkhorn(ws2, [], M2, reg=1e-2), xs2, ys2, xt) +pl.scatter(xs1[:, 0], xs1[:, 1], c=ys1, s=35, marker='x', cmap='Set1', vmax=9) +pl.scatter(xs2[:, 0], xs2[:, 1], c=ys2, s=35, marker='+', cmap='Set1', vmax=9) +pl.scatter(xt[:, 0], xt[:, 1], c=yt, s=35, marker='o', cmap='Set1', vmax=9) + +pl.plot([], [], 'r', alpha=.2, label='Mass from Class 1') +pl.plot([], [], 'b', alpha=.2, label='Mass from Class 2') + +pl.title('OT with known proportion ({:1.1f},{:1.1f})'.format(h_res[0], h_res[1])) + +pl.legend() +pl.axis('equal') +pl.axis('off') +pl.show() diff --git a/ot/da.py b/ot/da.py index fd5da4b..a3da8c1 100644 --- a/ot/da.py +++ b/ot/da.py @@ -748,79 +748,58 @@ def OT_mapping_linear(xs, xt, reg=1e-6, ws=None, def jcpot_barycenter(Xs, Ys, Xt, reg, metric='sqeuclidean', numItermax=100, stopThr=1e-6, verbose=False, log=False, **kwargs): - """Joint OT and proportion estimation as proposed in [27] + r'''Joint OT and proportion estimation for multi-source target shift as proposed in [27] The function solves the following optimization problem: .. math:: - \mathbf{h} = \argmin_{\mathbf{h} \in \Delta_C}\quad \sum_{k=1}^K \lambda_k - W_{reg}\left((\mathbf{D}_2^{(k)} \mathbf{h})^T \mathbf{\delta}_{\mathbf{X}^{(k)}}, \mu\right) + \mathbf{h} = arg\min_{\mathbf{h}}\quad \sum_{k=1}^{K} \lambda_k + W_{reg}((\mathbf{D}_2^{(k)} \mathbf{h})^T, \mathbf{a}) + s.t. \ \forall k, \mathbf{D}_1^{(k)} \gamma_k \mathbf{1}_n= \mathbf{h} - s.t. \gamma^T_k \mathbf{1}_n = \mathbf{1}_n/n - - \mathbf{D}_1^{(k)} \gamma_k \mathbf{1}_n= \mathbf{h} - - \gamma\geq 0 where : - - M is the (ns,nt) squared euclidean cost matrix between samples in - Xs and Xt (scaled by ns) - - :math:`L` is a ns x d linear operator on a kernel matrix that - approximates the barycentric mapping - - a and b are uniform source and target weights - - The problem consist in solving jointly an optimal transport matrix - :math:`\gamma` and the nonlinear mapping that fits the barycentric mapping - :math:`n_s\gamma X_t`. + - :math:`\lambda_k` is the weight of k-th source domain + - :math:`W_{reg}(\cdot,\cdot)` is the entropic regularized Wasserstein distance (see ot.bregman.sinkhorn) + - :math:`\mathbf{D}_2^{(k)}` is a matrix of weights related to k-th source domain defined as in [p. 5, 27], its expected shape is `(n_k, C)` where `n_k` is the number of elements in the k-th source domain and `C` is the number of classes + - :math:`\mathbf{h}` is a vector of estimated proportions in the target domain of size C + - :math:`\mathbf{a}` is a uniform vector of weights in the target domain of size `n` + - :math:`\mathbf{D}_1^{(k)}` is a matrix of class assignments defined as in [p. 5, 27], its expected shape is `(n_k, C)` - One can also estimate a mapping with constant bias (see supplementary - material of [8]) using the bias optional argument. - - The algorithm used for solving the problem is the block coordinate - descent that alternates between updates of G (using conditional gradient) - and the update of L using a classical kernel least square solver. + The problem consist in solving a Wasserstein barycenter problem to estimate the proportions :math:`\mathbf{h}` in the target domain. + The algorithm used for solving the problem is the Iterative Bregman projections algorithm + with two sets of marginal constraints related to the unknown vector :math:`\mathbf{h}` and uniform tarhet distribution. Parameters ---------- - xs : np.ndarray (ns,d) - samples in the source domain - xt : np.ndarray (nt,d) + Xs : list of K np.ndarray(nsk,d) + features of all source domains' samples + Ys : list of K np.ndarray(nsk,) + labels of all source domains' samples + Xt : np.ndarray (nt,d) samples in the target domain - mu : float,optional - Weight for the linear OT loss (>0) - eta : float, optional - Regularization term for the linear mapping L (>0) - kerneltype : str,optional - kernel used by calling function ot.utils.kernel (gaussian by default) - sigma : float, optional - Gaussian kernel bandwidth. - bias : bool,optional - Estimate linear mapping with constant bias - verbose : bool, optional - Print information along iterations - verbose2 : bool, optional - Print information along iterations + reg : float + Regularization term > 0 + metric : string, optional (default="sqeuclidean") + The ground metric for the Wasserstein problem numItermax : int, optional - Max number of BCD iterations - numInnerItermax : int, optional - Max number of iterations (inner CG solver) - stopInnerThr : float, optional - Stop threshold on error (inner CG solver) (>0) + Max number of iterations stopThr : float, optional - Stop threshold on relative loss decrease (>0) + Stop threshold on relative change in the barycenter (>0) log : bool, optional record log if True - + verbose : bool, optional (default=False) + Controls the verbosity of the optimization algorithm Returns ------- - gamma : (ns x nt) ndarray - Optimal transportation matrix for the given parameters - L : (ns x d) ndarray - Nonlinear mapping matrix (ns+1 x d if bias) + gamma : List of K (nsk x nt) ndarrays + Optimal transportation matrices for the given parameters for each pair of source and target domains + h : (C,) ndarray + proportion estimation in the target domain log : dict log dictionary return only if log==True in parameters @@ -828,62 +807,59 @@ def jcpot_barycenter(Xs, Ys, Xt, reg, metric='sqeuclidean', numItermax=100, References ---------- - .. [8] M. Perrot, N. Courty, R. Flamary, A. Habrard, - "Mapping estimation for discrete optimal transport", - Neural Information Processing Systems (NIPS), 2016. - - See Also - -------- - ot.lp.emd : Unregularized OT - ot.optim.cg : General regularized OT + .. [27] Ievgen Redko, Nicolas Courty, Rémi Flamary, Devis Tuia + "Optimal transport for multi-source domain adaptation under target shift", + International Conference on Artificial Intelligence and Statistics (AISTATS), 2019. - """ + ''' nbclasses = len(np.unique(Ys[0])) nbdomains = len(Xs) - # we then build, for each source domain, specific information + # For each source domain, build cost matrices M, Gibbs kernels K and corresponding matrices D_1 and D_2 all_domains = [] + + # log dictionary + if log: + log = {'niter': 0, 'err': [], 'all_domains': []} + for d in range(nbdomains): dom = {} - # get number of elements for this domain - nb_elem = Xs[d].shape[0] - dom['nbelem'] = nb_elem - classes = np.unique(Ys[d]) + nsk = Xs[d].shape[0] # get number of elements for this domain + dom['nbelem'] = nsk + classes = np.unique(Ys[d]) # get number of classes for this domain + # format classes to start from 0 for convenience if np.min(classes) != 0: Ys[d] = Ys[d] - np.min(classes) classes = np.unique(Ys[d]) - # build the corresponding D matrix - D1 = np.zeros((nbclasses, nb_elem)) - D2 = np.zeros((nbclasses, nb_elem)) - classes_d = np.zeros(nbclasses) - - classes_d[np.unique(Ys[d]).astype(int)] = 1 - dom['classes'] = classes_d + # build the corresponding D_1 and D_2 matrices + D1 = np.zeros((nbclasses, nsk)) + D2 = np.zeros((nbclasses, nsk)) for c in classes: nbelemperclass = np.sum(Ys[d] == c) if nbelemperclass != 0: D1[int(c), Ys[d] == c] = 1. - D2[int(c), Ys[d] == c] = 1. / (nbelemperclass) # *nbclasses_d) + D2[int(c), Ys[d] == c] = 1. / (nbelemperclass) dom['D1'] = D1 dom['D2'] = D2 - # build the distance matrix + # build the cost matrix and the Gibbs kernel M = dist(Xs[d], Xt, metric=metric) M = M / np.median(M) - dom['K'] = np.exp(-M/reg) + K = np.empty(M.shape, dtype=M.dtype) + np.divide(M, -reg, out=K) + np.exp(K, out=K) + dom['K'] = K all_domains.append(dom) - distribT = unif(np.shape(Xt)[0]) - - if log: - log = {'niter': 0, 'err': []} + # uniform target distribution + a = unif(np.shape(Xt)[0]) - cpt = 0 + cpt = 0 # iterations count err = 1 old_bary = np.ones((nbclasses)) @@ -891,13 +867,15 @@ def jcpot_barycenter(Xs, Ys, Xt, reg, metric='sqeuclidean', numItermax=100, bary = np.zeros((nbclasses)) + # update coupling matrices for marginal constraints w.r.t. uniform target distribution for d in range(nbdomains): - all_domains[d]['K'] = projC(all_domains[d]['K'], distribT) + all_domains[d]['K'] = projC(all_domains[d]['K'], a) other = np.sum(all_domains[d]['K'], axis=1) bary = bary + np.log(np.dot(all_domains[d]['D1'], other)) / nbdomains bary = np.exp(bary) + # update coupling matrices for marginal constraints w.r.t. unknown proportions based on [Prop 4., 27] for d in range(nbdomains): new = np.dot(all_domains[d]['D2'].T, bary) all_domains[d]['K'] = projR(all_domains[d]['K'], new) @@ -915,12 +893,14 @@ def jcpot_barycenter(Xs, Ys, Xt, reg, metric='sqeuclidean', numItermax=100, print('{:5d}|{:8e}|'.format(cpt, err)) bary = bary / np.sum(bary) + couplings = [all_domains[d]['K'] for d in range(nbdomains)] if log: log['niter'] = cpt - return bary, log + log['all_domains'] = all_domains + return couplings, bary, log else: - return bary + return couplings, bary def distribution_estimation_uniform(X): @@ -2093,9 +2073,10 @@ class UnbalancedSinkhornTransport(BaseTransport): return self + class JCPOTTransport(BaseTransport): - """Domain Adapatation OT method for target shift based on sinkhorn algorithm. + """Domain Adapatation OT method for multi-source target shift based on Wasserstein barycenter algorithm. Parameters ---------- @@ -2104,8 +2085,6 @@ class JCPOTTransport(BaseTransport): max_iter : int, float, optional (default=10) The minimum number of iteration before stopping the optimization algorithm if no it has not converged - max_inner_iter : int, float, optional (default=200) - The number of iteration in the inner loop tol : float, optional (default=10e-9) Stop threshold on error (inner sinkhorn solver) (>0) verbose : bool, optional (default=False) @@ -2126,21 +2105,20 @@ class JCPOTTransport(BaseTransport): Attributes ---------- - coupling_ : array-like, shape (n_source_samples, n_target_samples) - The optimal coupling + coupling_ : list of array-like objects, shape K x (n_source_samples, n_target_samples) + A set of optimal couplings between each source domain and the target domain + proportions_ : array-like, shape (n_classes,) + Estimated class proportions in the target domain log_ : dictionary The dictionary of log, empty dic if parameter log is not True References ---------- - .. [1] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, - "Optimal Transport for Domain Adaptation," in IEEE - Transactions on Pattern Analysis and Machine Intelligence , - vol.PP, no.99, pp.1-1 - .. [2] Rakotomamonjy, A., Flamary, R., & Courty, N. (2015). - Generalized conditional gradient: analysis of convergence - and applications. arXiv preprint arXiv:1510.06567. + .. [1] Ievgen Redko, Nicolas Courty, Rémi Flamary, Devis Tuia + "Optimal transport for multi-source domain adaptation under target shift", + International Conference on Artificial Intelligence and Statistics (AISTATS), + vol. 89, p.849-858, 2019. """ @@ -2156,20 +2134,18 @@ class JCPOTTransport(BaseTransport): self.verbose = verbose self.log = log self.metric = metric - self.norm = norm - self.distribution_estimation = distribution_estimation self.out_of_sample_map = out_of_sample_map def fit(self, Xs, ys=None, Xt=None, yt=None): - """Build a coupling matrix from source and target sets of samples + """Building coupling matrices from a list of source and target sets of samples (Xs, ys) and (Xt, yt) Parameters ---------- - Xs : array-like, shape (n_source_samples, n_features) - The training input samples. - ys : array-like, shape (n_source_samples,) - The class labels + Xs : list of K array-like objects, shape K x (nk_source_samples, n_features) + A list of the training input samples. + ys : list of K array-like objects, shape K x (nk_source_samples,) + A list of the class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. yt : array-like, shape (n_target_samples,) @@ -2188,15 +2164,90 @@ class JCPOTTransport(BaseTransport): # check the necessary inputs parameters are here if check_params(Xs=Xs, Xt=Xt, ys=ys): - returned_ = jcpot_barycenter(Xs=Xs, Ys=ys, Xt=Xt, reg = self.reg_e, - metric=self.metric, numItermax=self.max_iter, stopThr=self.tol, + self.xs_ = Xs + self.xt_ = Xt + + returned_ = jcpot_barycenter(Xs=Xs, Ys=ys, Xt=Xt, reg=self.reg_e, + metric=self.metric, distrinumItermax=self.max_iter, stopThr=self.tol, verbose=self.verbose, log=self.log) # deal with the value of log if self.log: - self.coupling_, self.log_ = returned_ + self.coupling_, self.proportions_, self.log_ = returned_ else: - self.coupling_ = returned_ + self.coupling_, self.proportions_ = returned_ self.log_ = dict() return self + + def transform(self, Xs=None, ys=None, Xt=None, yt=None, batch_size=128): + """Transports source samples Xs onto target ones Xt + + Parameters + ---------- + Xs : array-like, shape (n_source_samples, n_features) + The training input samples. + ys : array-like, shape (n_source_samples,) + The class labels + Xt : array-like, shape (n_target_samples, n_features) + The training input samples. + yt : array-like, shape (n_target_samples,) + The class labels. If some target samples are unlabeled, fill the + yt's elements with -1. + + Warning: Note that, due to this convention -1 cannot be used as a + class label + batch_size : int, optional (default=128) + The batch size for out of sample inverse transform + """ + + transp_Xs = [] + + # check the necessary inputs parameters are here + if check_params(Xs=Xs): + + if all([np.allclose(x, y) for x, y in zip(self.xs_, Xs)]): + + # perform standard barycentric mapping for each source domain + + for coupling in self.coupling_: + transp = coupling / np.sum(coupling, 1)[:, None] + + # set nans to 0 + transp[~ np.isfinite(transp)] = 0 + + # compute transported samples + transp_Xs.append(np.dot(transp, self.xt_)) + else: + + # perform out of sample mapping + indices = np.arange(Xs.shape[0]) + batch_ind = [ + indices[i:i + batch_size] + for i in range(0, len(indices), batch_size)] + + transp_Xs = [] + + for bi in batch_ind: + transp_Xs_ = [] + + # get the nearest neighbor in the sources domains + xs = np.concatenate(self.xs_, axis=0) + idx = np.argmin(dist(Xs[bi], xs), axis=1) + + # transport the source samples + for coupling in self.coupling_: + transp = coupling / np.sum( + coupling, 1)[:, None] + transp[~ np.isfinite(transp)] = 0 + transp_Xs_.append(np.dot(transp, self.xt_)) + + transp_Xs_ = np.concatenate(transp_Xs_, axis=0) + + # define the transported points + transp_Xs_ = transp_Xs_[idx, :] + Xs[bi] - xs[idx, :] + transp_Xs.append(transp_Xs_) + + transp_Xs = np.concatenate(transp_Xs, axis=0) + + return transp_Xs diff --git a/test/test_da.py b/test/test_da.py index 2a5e50e..a8c258a 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -5,7 +5,7 @@ # License: MIT License import numpy as np -from numpy.testing.utils import assert_allclose, assert_equal +from numpy.testing import assert_allclose, assert_equal import ot from ot.datasets import make_data_classif @@ -549,3 +549,64 @@ def test_linear_mapping_class(): Cst = np.cov(Xst.T) np.testing.assert_allclose(Ct, Cst, rtol=1e-2, atol=1e-2) + + +def test_jcpot_transport_class(): + """test_jcpot_transport + """ + + ns1 = 150 + ns2 = 150 + nt = 200 + + Xs1, ys1 = make_data_classif('3gauss', ns1) + Xs2, ys2 = make_data_classif('3gauss', ns2) + + Xt, yt = make_data_classif('3gauss2', nt) + + Xs = [Xs1, Xs2] + ys = [ys1, ys2] + + otda = ot.da.JCPOTTransport(reg_e=0.01, max_iter=1000, tol=1e-9, verbose=True) + + # test its computed + otda.fit(Xs=Xs, ys=ys, Xt=Xt) + print(otda.proportions_) + + assert hasattr(otda, "coupling_") + assert hasattr(otda, "proportions_") + assert hasattr(otda, "log_") + + # test dimensions of coupling + for i, xs in enumerate(Xs): + assert_equal(otda.coupling_[i].shape, ((xs.shape[0], Xt.shape[0]))) + + # test all margin constraints + mu_t = unif(nt) + + for i in range(len(Xs)): + # test margin constraints w.r.t. uniform target weights for each coupling matrix + assert_allclose( + np.sum(otda.coupling_[i], axis=0), mu_t, rtol=1e-3, atol=1e-3) + + # test margin constraints w.r.t. modified source weights for each source domain + + D1 = np.zeros((len(np.unique(ys[i])), len(ys[i]))) + for c in np.unique(ys[i]): + nbelemperclass = np.sum(ys[i] == c) + if nbelemperclass != 0: + D1[int(c), ys[i] == c] = 1. + + assert_allclose( + np.dot(D1, np.sum(otda.coupling_[i], axis=1)), otda.proportions_, rtol=1e-3, atol=1e-3) + + # test transform + transp_Xs = otda.transform(Xs=Xs) + [assert_equal(x.shape, y.shape) for x, y in zip(transp_Xs, Xs)] + #assert_equal(transp_Xs.shape, Xs.shape) + + Xs_new, _ = make_data_classif('3gauss', ns1 + 1) + transp_Xs_new = otda.transform(Xs_new) + + # check that the oos method is working + assert_equal(transp_Xs_new.shape, Xs_new.shape) -- cgit v1.2.3 From ba493aa5488507937b7f9707faa17128c9aa1872 Mon Sep 17 00:00:00 2001 From: ievred Date: Tue, 31 Mar 2020 17:36:00 +0200 Subject: readme move to bregman --- README.md | 3 + ot/bregman.py | 157 +++++++++++++++++++++++++++++++++++++++++++++- ot/da.py | 190 ++++---------------------------------------------------- test/test_da.py | 2 +- 4 files changed, 171 insertions(+), 181 deletions(-) (limited to 'test') diff --git a/README.md b/README.md index c115776..f439405 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ It provides the following solvers: * Non regularized free support Wasserstein barycenters [20]. * Unbalanced OT with KL relaxation distance and barycenter [10, 25]. * Screening Sinkhorn Algorithm for OT [26]. +* JCPOT algorithm for multi-source target shift [27]. Some demonstrations (both in Python and Jupyter Notebook format) are available in the examples folder. @@ -257,3 +258,5 @@ You can also post bug reports and feature requests in Github issues. Make sure t [25] Frogner C., Zhang C., Mobahi H., Araya-Polo M., Poggio T. (2015). [Learning with a Wasserstein Loss](http://cbcl.mit.edu/wasserstein/) Advances in Neural Information Processing Systems (NIPS). [26] Alaya M. Z., Bérar M., Gasso G., Rakotomamonjy A. (2019). [Screening Sinkhorn Algorithm for Regularized Optimal Transport](https://papers.nips.cc/paper/9386-screening-sinkhorn-algorithm-for-regularized-optimal-transport), Advances in Neural Information Processing Systems 33 (NeurIPS). + +[27] Redko I., Courty N., Flamary R., Tuia D. (2019). [Optimal Transport for Multi-source Domain Adaptation under Target Shift](http://proceedings.mlr.press/v89/redko19a.html), Proceedings of the Twenty-Second International Conference on Artificial Intelligence and Statistics (AISTATS) 22, 2019. \ No newline at end of file diff --git a/ot/bregman.py b/ot/bregman.py index d5e3563..d17aaf0 100644 --- a/ot/bregman.py +++ b/ot/bregman.py @@ -10,6 +10,7 @@ Bregman projections for regularized OT # Hicham Janati # Mokhtar Z. Alaya # Alexander Tong +# Ievgen Redko # # License: MIT License @@ -18,7 +19,6 @@ import warnings from .utils import unif, dist from scipy.optimize import fmin_l_bfgs_b - def sinkhorn(a, b, M, reg, method='sinkhorn', numItermax=1000, stopThr=1e-9, verbose=False, log=False, **kwargs): r""" @@ -1501,6 +1501,161 @@ def unmix(a, D, M, M0, h0, reg, reg0, alpha, numItermax=1000, else: return np.sum(K0, axis=1) +def jcpot_barycenter(Xs, Ys, Xt, reg, metric='sqeuclidean', numItermax=100, + stopThr=1e-6, verbose=False, log=False, **kwargs): + r'''Joint OT and proportion estimation for multi-source target shift as proposed in [27] + + The function solves the following optimization problem: + + .. math:: + + \mathbf{h} = arg\min_{\mathbf{h}}\quad \sum_{k=1}^{K} \lambda_k + W_{reg}((\mathbf{D}_2^{(k)} \mathbf{h})^T, \mathbf{a}) + + s.t. \ \forall k, \mathbf{D}_1^{(k)} \gamma_k \mathbf{1}_n= \mathbf{h} + + where : + + - :math:`\lambda_k` is the weight of k-th source domain + - :math:`W_{reg}(\cdot,\cdot)` is the entropic regularized Wasserstein distance (see ot.bregman.sinkhorn) + - :math:`\mathbf{D}_2^{(k)}` is a matrix of weights related to k-th source domain defined as in [p. 5, 27], its expected shape is `(n_k, C)` where `n_k` is the number of elements in the k-th source domain and `C` is the number of classes + - :math:`\mathbf{h}` is a vector of estimated proportions in the target domain of size C + - :math:`\mathbf{a}` is a uniform vector of weights in the target domain of size `n` + - :math:`\mathbf{D}_1^{(k)}` is a matrix of class assignments defined as in [p. 5, 27], its expected shape is `(n_k, C)` + + The problem consist in solving a Wasserstein barycenter problem to estimate the proportions :math:`\mathbf{h}` in the target domain. + + The algorithm used for solving the problem is the Iterative Bregman projections algorithm + with two sets of marginal constraints related to the unknown vector :math:`\mathbf{h}` and uniform tarhet distribution. + + Parameters + ---------- + Xs : list of K np.ndarray(nsk,d) + features of all source domains' samples + Ys : list of K np.ndarray(nsk,) + labels of all source domains' samples + Xt : np.ndarray (nt,d) + samples in the target domain + reg : float + Regularization term > 0 + metric : string, optional (default="sqeuclidean") + The ground metric for the Wasserstein problem + numItermax : int, optional + Max number of iterations + stopThr : float, optional + Stop threshold on relative change in the barycenter (>0) + log : bool, optional + record log if True + verbose : bool, optional (default=False) + Controls the verbosity of the optimization algorithm + + Returns + ------- + gamma : List of K (nsk x nt) ndarrays + Optimal transportation matrices for the given parameters for each pair of source and target domains + h : (C,) ndarray + proportion estimation in the target domain + log : dict + log dictionary return only if log==True in parameters + + + References + ---------- + + .. [27] Ievgen Redko, Nicolas Courty, Rémi Flamary, Devis Tuia + "Optimal transport for multi-source domain adaptation under target shift", + International Conference on Artificial Intelligence and Statistics (AISTATS), 2019. + + ''' + nbclasses = len(np.unique(Ys[0])) + nbdomains = len(Xs) + + # For each source domain, build cost matrices M, Gibbs kernels K and corresponding matrices D_1 and D_2 + all_domains = [] + + # log dictionary + if log: + log = {'niter': 0, 'err': [], 'all_domains': []} + + for d in range(nbdomains): + dom = {} + nsk = Xs[d].shape[0] # get number of elements for this domain + dom['nbelem'] = nsk + classes = np.unique(Ys[d]) # get number of classes for this domain + + # format classes to start from 0 for convenience + if np.min(classes) != 0: + Ys[d] = Ys[d] - np.min(classes) + classes = np.unique(Ys[d]) + + # build the corresponding D_1 and D_2 matrices + D1 = np.zeros((nbclasses, nsk)) + D2 = np.zeros((nbclasses, nsk)) + + for c in classes: + nbelemperclass = np.sum(Ys[d] == c) + if nbelemperclass != 0: + D1[int(c), Ys[d] == c] = 1. + D2[int(c), Ys[d] == c] = 1. / (nbelemperclass) + dom['D1'] = D1 + dom['D2'] = D2 + + # build the cost matrix and the Gibbs kernel + M = dist(Xs[d], Xt, metric=metric) + M = M / np.median(M) + + K = np.empty(M.shape, dtype=M.dtype) + np.divide(M, -reg, out=K) + np.exp(K, out=K) + dom['K'] = K + + all_domains.append(dom) + + # uniform target distribution + a = unif(np.shape(Xt)[0]) + + cpt = 0 # iterations count + err = 1 + old_bary = np.ones((nbclasses)) + + while (err > stopThr and cpt < numItermax): + + bary = np.zeros((nbclasses)) + + # update coupling matrices for marginal constraints w.r.t. uniform target distribution + for d in range(nbdomains): + all_domains[d]['K'] = projC(all_domains[d]['K'], a) + other = np.sum(all_domains[d]['K'], axis=1) + bary = bary + np.log(np.dot(all_domains[d]['D1'], other)) / nbdomains + + bary = np.exp(bary) + + # update coupling matrices for marginal constraints w.r.t. unknown proportions based on [Prop 4., 27] + for d in range(nbdomains): + new = np.dot(all_domains[d]['D2'].T, bary) + all_domains[d]['K'] = projR(all_domains[d]['K'], new) + + err = np.linalg.norm(bary - old_bary) + cpt = cpt + 1 + old_bary = bary + + if log: + log['err'].append(err) + + if verbose: + if cpt % 200 == 0: + print('{:5s}|{:12s}'.format('It.', 'Err') + '\n' + '-' * 19) + print('{:5d}|{:8e}|'.format(cpt, err)) + + bary = bary / np.sum(bary) + couplings = [all_domains[d]['K'] for d in range(nbdomains)] + + if log: + log['niter'] = cpt + log['all_domains'] = all_domains + return couplings, bary, log + else: + return couplings, bary def empirical_sinkhorn(X_s, X_t, reg, a=None, b=None, metric='sqeuclidean', numIterMax=10000, stopThr=1e-9, verbose=False, diff --git a/ot/da.py b/ot/da.py index a3da8c1..a9c3cea 100644 --- a/ot/da.py +++ b/ot/da.py @@ -7,20 +7,20 @@ Domain adaptation with optimal transport # Nicolas Courty # Michael Perrot # Nathalie Gayraud +# Ievgen Redko # # License: MIT License import numpy as np import scipy.linalg as linalg -from .bregman import sinkhorn, projR, projC +from .bregman import sinkhorn from .lp import emd from .utils import unif, dist, kernel, cost_normalization from .utils import check_params, BaseEstimator from .unbalanced import sinkhorn_unbalanced from .optim import cg from .optim import gcg -from functools import reduce def sinkhorn_lpl1_mm(a, labels_a, b, M, reg, eta=0.1, numItermax=10, @@ -128,7 +128,7 @@ def sinkhorn_lpl1_mm(a, labels_a, b, M, reg, eta=0.1, numItermax=10, W = np.ones(M.shape) for (i, c) in enumerate(classes): majs = np.sum(transp[indices_labels[i]], axis=0) - majs = p * ((majs + epsilon)**(p - 1)) + majs = p * ((majs + epsilon) ** (p - 1)) W[indices_labels[i]] = majs return transp @@ -360,8 +360,8 @@ def joint_OT_mapping_linear(xs, xt, mu=1, eta=0.001, bias=False, verbose=False, def loss(L, G): """Compute full loss""" - return np.sum((xs1.dot(L) - ns * G.dot(xt))**2) + mu * \ - np.sum(G * M) + eta * np.sum(sel(L - I0)**2) + return np.sum((xs1.dot(L) - ns * G.dot(xt)) ** 2) + mu * \ + np.sum(G * M) + eta * np.sum(sel(L - I0) ** 2) def solve_L(G): """ solve L problem with fixed G (least square)""" @@ -373,10 +373,11 @@ def joint_OT_mapping_linear(xs, xt, mu=1, eta=0.001, bias=False, verbose=False, xsi = xs1.dot(L) def f(G): - return np.sum((xsi - ns * G.dot(xt))**2) + return np.sum((xsi - ns * G.dot(xt)) ** 2) def df(G): return -2 * ns * (xsi - ns * G.dot(xt)).dot(xt.T) + G = cg(a, b, M, 1.0 / mu, f, df, G0=G0, numItermax=numInnerItermax, stopThr=stopInnerThr) return G @@ -563,8 +564,8 @@ def joint_OT_mapping_kernel(xs, xt, mu=1, eta=0.001, kerneltype='gaussian', def loss(L, G): """Compute full loss""" - return np.sum((K1.dot(L) - ns * G.dot(xt))**2) + mu * \ - np.sum(G * M) + eta * np.trace(L.T.dot(Kreg).dot(L)) + return np.sum((K1.dot(L) - ns * G.dot(xt)) ** 2) + mu * \ + np.sum(G * M) + eta * np.trace(L.T.dot(Kreg).dot(L)) def solve_L_nobias(G): """ solve L problem with fixed G (least square)""" @@ -581,10 +582,11 @@ def joint_OT_mapping_kernel(xs, xt, mu=1, eta=0.001, kerneltype='gaussian', xsi = K1.dot(L) def f(G): - return np.sum((xsi - ns * G.dot(xt))**2) + return np.sum((xsi - ns * G.dot(xt)) ** 2) def df(G): return -2 * ns * (xsi - ns * G.dot(xt)).dot(xt.T) + G = cg(a, b, M, 1.0 / mu, f, df, G0=G0, numItermax=numInnerItermax, stopThr=stopInnerThr) return G @@ -746,163 +748,6 @@ def OT_mapping_linear(xs, xt, reg=1e-6, ws=None, return A, b -def jcpot_barycenter(Xs, Ys, Xt, reg, metric='sqeuclidean', numItermax=100, - stopThr=1e-6, verbose=False, log=False, **kwargs): - r'''Joint OT and proportion estimation for multi-source target shift as proposed in [27] - - The function solves the following optimization problem: - - .. math:: - - \mathbf{h} = arg\min_{\mathbf{h}}\quad \sum_{k=1}^{K} \lambda_k - W_{reg}((\mathbf{D}_2^{(k)} \mathbf{h})^T, \mathbf{a}) - - s.t. \ \forall k, \mathbf{D}_1^{(k)} \gamma_k \mathbf{1}_n= \mathbf{h} - - where : - - - :math:`\lambda_k` is the weight of k-th source domain - - :math:`W_{reg}(\cdot,\cdot)` is the entropic regularized Wasserstein distance (see ot.bregman.sinkhorn) - - :math:`\mathbf{D}_2^{(k)}` is a matrix of weights related to k-th source domain defined as in [p. 5, 27], its expected shape is `(n_k, C)` where `n_k` is the number of elements in the k-th source domain and `C` is the number of classes - - :math:`\mathbf{h}` is a vector of estimated proportions in the target domain of size C - - :math:`\mathbf{a}` is a uniform vector of weights in the target domain of size `n` - - :math:`\mathbf{D}_1^{(k)}` is a matrix of class assignments defined as in [p. 5, 27], its expected shape is `(n_k, C)` - - The problem consist in solving a Wasserstein barycenter problem to estimate the proportions :math:`\mathbf{h}` in the target domain. - - The algorithm used for solving the problem is the Iterative Bregman projections algorithm - with two sets of marginal constraints related to the unknown vector :math:`\mathbf{h}` and uniform tarhet distribution. - - Parameters - ---------- - Xs : list of K np.ndarray(nsk,d) - features of all source domains' samples - Ys : list of K np.ndarray(nsk,) - labels of all source domains' samples - Xt : np.ndarray (nt,d) - samples in the target domain - reg : float - Regularization term > 0 - metric : string, optional (default="sqeuclidean") - The ground metric for the Wasserstein problem - numItermax : int, optional - Max number of iterations - stopThr : float, optional - Stop threshold on relative change in the barycenter (>0) - log : bool, optional - record log if True - verbose : bool, optional (default=False) - Controls the verbosity of the optimization algorithm - - Returns - ------- - gamma : List of K (nsk x nt) ndarrays - Optimal transportation matrices for the given parameters for each pair of source and target domains - h : (C,) ndarray - proportion estimation in the target domain - log : dict - log dictionary return only if log==True in parameters - - - References - ---------- - - .. [27] Ievgen Redko, Nicolas Courty, Rémi Flamary, Devis Tuia - "Optimal transport for multi-source domain adaptation under target shift", - International Conference on Artificial Intelligence and Statistics (AISTATS), 2019. - - ''' - nbclasses = len(np.unique(Ys[0])) - nbdomains = len(Xs) - - # For each source domain, build cost matrices M, Gibbs kernels K and corresponding matrices D_1 and D_2 - all_domains = [] - - # log dictionary - if log: - log = {'niter': 0, 'err': [], 'all_domains': []} - - for d in range(nbdomains): - dom = {} - nsk = Xs[d].shape[0] # get number of elements for this domain - dom['nbelem'] = nsk - classes = np.unique(Ys[d]) # get number of classes for this domain - - # format classes to start from 0 for convenience - if np.min(classes) != 0: - Ys[d] = Ys[d] - np.min(classes) - classes = np.unique(Ys[d]) - - # build the corresponding D_1 and D_2 matrices - D1 = np.zeros((nbclasses, nsk)) - D2 = np.zeros((nbclasses, nsk)) - - for c in classes: - nbelemperclass = np.sum(Ys[d] == c) - if nbelemperclass != 0: - D1[int(c), Ys[d] == c] = 1. - D2[int(c), Ys[d] == c] = 1. / (nbelemperclass) - dom['D1'] = D1 - dom['D2'] = D2 - - # build the cost matrix and the Gibbs kernel - M = dist(Xs[d], Xt, metric=metric) - M = M / np.median(M) - - K = np.empty(M.shape, dtype=M.dtype) - np.divide(M, -reg, out=K) - np.exp(K, out=K) - dom['K'] = K - - all_domains.append(dom) - - # uniform target distribution - a = unif(np.shape(Xt)[0]) - - cpt = 0 # iterations count - err = 1 - old_bary = np.ones((nbclasses)) - - while (err > stopThr and cpt < numItermax): - - bary = np.zeros((nbclasses)) - - # update coupling matrices for marginal constraints w.r.t. uniform target distribution - for d in range(nbdomains): - all_domains[d]['K'] = projC(all_domains[d]['K'], a) - other = np.sum(all_domains[d]['K'], axis=1) - bary = bary + np.log(np.dot(all_domains[d]['D1'], other)) / nbdomains - - bary = np.exp(bary) - - # update coupling matrices for marginal constraints w.r.t. unknown proportions based on [Prop 4., 27] - for d in range(nbdomains): - new = np.dot(all_domains[d]['D2'].T, bary) - all_domains[d]['K'] = projR(all_domains[d]['K'], new) - - err = np.linalg.norm(bary - old_bary) - cpt = cpt + 1 - old_bary = bary - - if log: - log['err'].append(err) - - if verbose: - if cpt % 200 == 0: - print('{:5s}|{:12s}'.format('It.', 'Err') + '\n' + '-' * 19) - print('{:5d}|{:8e}|'.format(cpt, err)) - - bary = bary / np.sum(bary) - couplings = [all_domains[d]['K'] for d in range(nbdomains)] - - if log: - log['niter'] = cpt - log['all_domains'] = all_domains - return couplings, bary, log - else: - return couplings, bary - - def distribution_estimation_uniform(X): """estimates a uniform distribution from an array of samples X @@ -921,7 +766,6 @@ def distribution_estimation_uniform(X): class BaseTransport(BaseEstimator): - """Base class for OTDA objects Notes @@ -1079,7 +923,6 @@ class BaseTransport(BaseEstimator): transp_Xs = [] for bi in batch_ind: - # get the nearest neighbor in the source domain D0 = dist(Xs[bi], self.xs_) idx = np.argmin(D0, axis=1) @@ -1148,7 +991,6 @@ class BaseTransport(BaseEstimator): transp_Xt = [] for bi in batch_ind: - D0 = dist(Xt[bi], self.xt_) idx = np.argmin(D0, axis=1) @@ -1294,7 +1136,6 @@ class LinearTransport(BaseTransport): # check the necessary inputs parameters are here if check_params(Xs=Xs): - transp_Xs = Xs.dot(self.A_) + self.B_ return transp_Xs @@ -1328,14 +1169,12 @@ class LinearTransport(BaseTransport): # check the necessary inputs parameters are here if check_params(Xt=Xt): - transp_Xt = Xt.dot(self.A1_) + self.B1_ return transp_Xt class SinkhornTransport(BaseTransport): - """Domain Adapatation OT method based on Sinkhorn Algorithm Parameters @@ -1445,7 +1284,6 @@ class SinkhornTransport(BaseTransport): class EMDTransport(BaseTransport): - """Domain Adapatation OT method based on Earth Mover's Distance Parameters @@ -1537,7 +1375,6 @@ class EMDTransport(BaseTransport): class SinkhornLpl1Transport(BaseTransport): - """Domain Adapatation OT method based on sinkhorn algorithm + LpL1 class regularization. @@ -1639,7 +1476,6 @@ class SinkhornLpl1Transport(BaseTransport): # check the necessary inputs parameters are here if check_params(Xs=Xs, Xt=Xt, ys=ys): - super(SinkhornLpl1Transport, self).fit(Xs, ys, Xt, yt) returned_ = sinkhorn_lpl1_mm( @@ -1658,7 +1494,6 @@ class SinkhornLpl1Transport(BaseTransport): class SinkhornL1l2Transport(BaseTransport): - """Domain Adapatation OT method based on sinkhorn algorithm + l1l2 class regularization. @@ -1782,7 +1617,6 @@ class SinkhornL1l2Transport(BaseTransport): class MappingTransport(BaseEstimator): - """MappingTransport: DA methods that aims at jointly estimating a optimal transport coupling and the associated mapping @@ -1956,7 +1790,6 @@ class MappingTransport(BaseEstimator): class UnbalancedSinkhornTransport(BaseTransport): - """Domain Adapatation unbalanced OT method based on sinkhorn algorithm Parameters @@ -2075,7 +1908,6 @@ class UnbalancedSinkhornTransport(BaseTransport): class JCPOTTransport(BaseTransport): - """Domain Adapatation OT method for multi-source target shift based on Wasserstein barycenter algorithm. Parameters diff --git a/test/test_da.py b/test/test_da.py index a8c258a..958df7b 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -8,6 +8,7 @@ import numpy as np from numpy.testing import assert_allclose, assert_equal import ot +from ot.bregman import jcpot_barycenter from ot.datasets import make_data_classif from ot.utils import unif @@ -603,7 +604,6 @@ def test_jcpot_transport_class(): # test transform transp_Xs = otda.transform(Xs=Xs) [assert_equal(x.shape, y.shape) for x, y in zip(transp_Xs, Xs)] - #assert_equal(transp_Xs.shape, Xs.shape) Xs_new, _ = make_data_classif('3gauss', ns1 + 1) transp_Xs_new = otda.transform(Xs_new) -- cgit v1.2.3 From 439860609df786a877383775dd901afe28480cc9 Mon Sep 17 00:00:00 2001 From: ievred Date: Wed, 1 Apr 2020 09:00:03 +0200 Subject: fix imports remove checks --- ot/da.py | 5 ++--- test/test_da.py | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) (limited to 'test') diff --git a/ot/da.py b/ot/da.py index a9c3cea..e62e495 100644 --- a/ot/da.py +++ b/ot/da.py @@ -14,7 +14,7 @@ Domain adaptation with optimal transport import numpy as np import scipy.linalg as linalg -from .bregman import sinkhorn +from .bregman import sinkhorn, jcpot_barycenter from .lp import emd from .utils import unif, dist, kernel, cost_normalization from .utils import check_params, BaseEstimator @@ -1956,8 +1956,7 @@ class JCPOTTransport(BaseTransport): def __init__(self, reg_e=.1, max_iter=10, tol=10e-9, verbose=False, log=False, - metric="sqeuclidean", norm=None, - distribution_estimation=distribution_estimation_uniform, + metric="sqeuclidean", out_of_sample_map='ferradans'): self.reg_e = reg_e diff --git a/test/test_da.py b/test/test_da.py index 958df7b..7526f30 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -572,7 +572,6 @@ def test_jcpot_transport_class(): # test its computed otda.fit(Xs=Xs, ys=ys, Xt=Xt) - print(otda.proportions_) assert hasattr(otda, "coupling_") assert hasattr(otda, "proportions_") @@ -610,3 +609,6 @@ def test_jcpot_transport_class(): # check that the oos method is working assert_equal(transp_Xs_new.shape, Xs_new.shape) + + +test_jcpot_transport_class() \ No newline at end of file -- cgit v1.2.3 From 547a03ef87e4aa92edc1e89ee2db04114e1a8ad5 Mon Sep 17 00:00:00 2001 From: ievred Date: Wed, 1 Apr 2020 09:13:58 +0200 Subject: fix test example add M to log --- examples/plot_otda_jcpot.py | 20 ++++++-------------- ot/bregman.py | 1 + test/test_da.py | 13 ++----------- 3 files changed, 9 insertions(+), 25 deletions(-) (limited to 'test') diff --git a/examples/plot_otda_jcpot.py b/examples/plot_otda_jcpot.py index 5e5fff8..1641fb0 100644 --- a/examples/plot_otda_jcpot.py +++ b/examples/plot_otda_jcpot.py @@ -81,11 +81,7 @@ pl.axis('off') ############################################################################## # Instantiate Sinkhorn transport algorithm and fit them for all source domains # ---------------------------------------------------------------------------- -ot_sinkhorn = ot.da.SinkhornTransport(reg_e=1e-2, metric='euclidean') - -M1 = ot.dist(xs1, xt, 'euclidean') -M2 = ot.dist(xs2, xt, 'euclidean') - +ot_sinkhorn = ot.da.SinkhornTransport(reg_e=1e-1, metric='sqeuclidean') def print_G(G, xs, ys, xt): for i in range(G.shape[0]): @@ -125,7 +121,7 @@ pl.axis('off') ############################################################################## # Instantiate JCPOT adaptation algorithm and fit it # ---------------------------------------------------------------------------- -otda = ot.da.JCPOTTransport(reg_e=1e-2, max_iter=1000, tol=1e-9, verbose=True, log=True) +otda = ot.da.JCPOTTransport(reg_e=1e-2, max_iter=1000, metric='sqeuclidean', tol=1e-9, verbose=True, log=True) otda.fit(all_Xr, all_Yr, xt) ws1 = otda.proportions_.dot(otda.log_['all_domains'][0]['D2']) @@ -136,8 +132,8 @@ pl.clf() plot_ax(dec1, 'Source 1') plot_ax(dec2, 'Source 2') plot_ax(dect, 'Target') -print_G(ot.bregman.sinkhorn(ws1, [], M1, reg=1e-2), xs1, ys1, xt) -print_G(ot.bregman.sinkhorn(ws2, [], M2, reg=1e-2), xs2, ys2, xt) +print_G(ot.bregman.sinkhorn(ws1, [], otda.log_['all_domains'][0]['M'], reg=1e-2), xs1, ys1, xt) +print_G(ot.bregman.sinkhorn(ws2, [], otda.log_['all_domains'][1]['M'], reg=1e-2), xs2, ys2, xt) pl.scatter(xs1[:, 0], xs1[:, 1], c=ys1, s=35, marker='x', cmap='Set1', vmax=9) pl.scatter(xs2[:, 0], xs2[:, 1], c=ys2, s=35, marker='+', cmap='Set1', vmax=9) pl.scatter(xt[:, 0], xt[:, 1], c=yt, s=35, marker='o', cmap='Set1', vmax=9) @@ -154,10 +150,6 @@ pl.axis('off') ############################################################################## # Run oracle transport algorithm with known proportions # ---------------------------------------------------------------------------- - -otda = ot.da.JCPOTTransport(reg_e=0.01, max_iter=1000, tol=1e-9, verbose=True, log=True) -otda.fit(all_Xr, all_Yr, xt) - h_res = np.array([1 - pt, pt]) ws1 = h_res.dot(otda.log_['all_domains'][0]['D2']) @@ -168,8 +160,8 @@ pl.clf() plot_ax(dec1, 'Source 1') plot_ax(dec2, 'Source 2') plot_ax(dect, 'Target') -print_G(ot.bregman.sinkhorn(ws1, [], M1, reg=1e-2), xs1, ys1, xt) -print_G(ot.bregman.sinkhorn(ws2, [], M2, reg=1e-2), xs2, ys2, xt) +print_G(ot.bregman.sinkhorn(ws1, [], otda.log_['all_domains'][0]['M'], reg=1e-2), xs1, ys1, xt) +print_G(ot.bregman.sinkhorn(ws2, [], otda.log_['all_domains'][1]['M'], reg=1e-2), xs2, ys2, xt) pl.scatter(xs1[:, 0], xs1[:, 1], c=ys1, s=35, marker='x', cmap='Set1', vmax=9) pl.scatter(xs2[:, 0], xs2[:, 1], c=ys2, s=35, marker='+', cmap='Set1', vmax=9) pl.scatter(xt[:, 0], xt[:, 1], c=yt, s=35, marker='o', cmap='Set1', vmax=9) diff --git a/ot/bregman.py b/ot/bregman.py index d17aaf0..fb959e9 100644 --- a/ot/bregman.py +++ b/ot/bregman.py @@ -1603,6 +1603,7 @@ def jcpot_barycenter(Xs, Ys, Xt, reg, metric='sqeuclidean', numItermax=100, # build the cost matrix and the Gibbs kernel M = dist(Xs[d], Xt, metric=metric) M = M / np.median(M) + dom['M'] = M K = np.empty(M.shape, dtype=M.dtype) np.divide(M, -reg, out=K) diff --git a/test/test_da.py b/test/test_da.py index 7526f30..a13550c 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -568,7 +568,7 @@ def test_jcpot_transport_class(): Xs = [Xs1, Xs2] ys = [ys1, ys2] - otda = ot.da.JCPOTTransport(reg_e=0.01, max_iter=1000, tol=1e-9, verbose=True) + otda = ot.da.JCPOTTransport(reg_e=0.01, max_iter=1000, tol=1e-9, verbose=True, log = True) # test its computed otda.fit(Xs=Xs, ys=ys, Xt=Xt) @@ -591,14 +591,8 @@ def test_jcpot_transport_class(): # test margin constraints w.r.t. modified source weights for each source domain - D1 = np.zeros((len(np.unique(ys[i])), len(ys[i]))) - for c in np.unique(ys[i]): - nbelemperclass = np.sum(ys[i] == c) - if nbelemperclass != 0: - D1[int(c), ys[i] == c] = 1. - assert_allclose( - np.dot(D1, np.sum(otda.coupling_[i], axis=1)), otda.proportions_, rtol=1e-3, atol=1e-3) + np.dot(otda.log_['all_domains'][i]['D1'], np.sum(otda.coupling_[i], axis=1)), otda.proportions_, rtol=1e-3, atol=1e-3) # test transform transp_Xs = otda.transform(Xs=Xs) @@ -609,6 +603,3 @@ def test_jcpot_transport_class(): # check that the oos method is working assert_equal(transp_Xs_new.shape, Xs_new.shape) - - -test_jcpot_transport_class() \ No newline at end of file -- cgit v1.2.3 From 6b8477d1c08696a08a1b71642712d83e560f9623 Mon Sep 17 00:00:00 2001 From: ievred Date: Wed, 1 Apr 2020 09:49:24 +0200 Subject: pep8 --- examples/plot_otda_jcpot.py | 20 ++++++++++++-------- test/test_da.py | 7 +++---- 2 files changed, 15 insertions(+), 12 deletions(-) (limited to 'test') diff --git a/examples/plot_otda_jcpot.py b/examples/plot_otda_jcpot.py index 579ad2a..ce6b88f 100644 --- a/examples/plot_otda_jcpot.py +++ b/examples/plot_otda_jcpot.py @@ -34,15 +34,17 @@ dec2 = [0, -2] pt = .4 dect = [4, 0] -xs1, ys1 = make_data_classif('2gauss_prop', n, nz=sigma, p = p1, bias = dec1) -xs2, ys2 = make_data_classif('2gauss_prop', n+1, nz=sigma, p = p2, bias = dec2) -xt, yt = make_data_classif('2gauss_prop', n, nz=sigma, p = pt, bias = dect) +xs1, ys1 = make_data_classif('2gauss_prop', n, nz=sigma, p=p1, bias=dec1) +xs2, ys2 = make_data_classif('2gauss_prop', n + 1, nz=sigma, p=p2, bias=dec2) +xt, yt = make_data_classif('2gauss_prop', n, nz=sigma, p=pt, bias=dect) all_Xr = [xs1, xs2] all_Yr = [ys1, ys2] # %% da = 1.5 + + def plot_ax(dec, name): pl.plot([dec[0], dec[0]], [dec[1] - da, dec[1] + da], 'k', alpha=0.5) pl.plot([dec[0] - da, dec[0] + da], [dec[1], dec[1]], 'k', alpha=0.5) @@ -58,21 +60,24 @@ pl.clf() plot_ax(dec1, 'Source 1') plot_ax(dec2, 'Source 2') plot_ax(dect, 'Target') -pl.scatter(xs1[:, 0], xs1[:, 1], c=ys1, s=35, marker='x', cmap='Set1', vmax=9, label='Source 1 ({:1.2f}, {:1.2f})'.format(1-p1, p1)) -pl.scatter(xs2[:, 0], xs2[:, 1], c=ys2, s=35, marker='+', cmap='Set1', vmax=9, label='Source 2 ({:1.2f}, {:1.2f})'.format(1-p2, p2)) -pl.scatter(xt[:, 0], xt[:, 1], c=yt, s=35, marker='o', cmap='Set1', vmax=9, label='Target ({:1.2f}, {:1.2f})'.format(1-pt, pt)) +pl.scatter(xs1[:, 0], xs1[:, 1], c=ys1, s=35, marker='x', cmap='Set1', vmax=9, + label='Source 1 ({:1.2f}, {:1.2f})'.format(1 - p1, p1)) +pl.scatter(xs2[:, 0], xs2[:, 1], c=ys2, s=35, marker='+', cmap='Set1', vmax=9, + label='Source 2 ({:1.2f}, {:1.2f})'.format(1 - p2, p2)) +pl.scatter(xt[:, 0], xt[:, 1], c=yt, s=35, marker='o', cmap='Set1', vmax=9, + label='Target ({:1.2f}, {:1.2f})'.format(1 - pt, pt)) pl.title('Data') pl.legend() pl.axis('equal') pl.axis('off') - ############################################################################## # Instantiate Sinkhorn transport algorithm and fit them for all source domains # ---------------------------------------------------------------------------- ot_sinkhorn = ot.da.SinkhornTransport(reg_e=1e-1, metric='sqeuclidean') + def print_G(G, xs, ys, xt): for i in range(G.shape[0]): for j in range(G.shape[1]): @@ -107,7 +112,6 @@ pl.legend() pl.axis('equal') pl.axis('off') - ############################################################################## # Instantiate JCPOT adaptation algorithm and fit it # ---------------------------------------------------------------------------- diff --git a/test/test_da.py b/test/test_da.py index a13550c..f700df9 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -511,7 +511,6 @@ def test_mapping_transport_class(): def test_linear_mapping(): - ns = 150 nt = 200 @@ -529,7 +528,6 @@ def test_linear_mapping(): def test_linear_mapping_class(): - ns = 150 nt = 200 @@ -568,7 +566,7 @@ def test_jcpot_transport_class(): Xs = [Xs1, Xs2] ys = [ys1, ys2] - otda = ot.da.JCPOTTransport(reg_e=0.01, max_iter=1000, tol=1e-9, verbose=True, log = True) + otda = ot.da.JCPOTTransport(reg_e=0.01, max_iter=1000, tol=1e-9, verbose=True, log=True) # test its computed otda.fit(Xs=Xs, ys=ys, Xt=Xt) @@ -592,7 +590,8 @@ def test_jcpot_transport_class(): # test margin constraints w.r.t. modified source weights for each source domain assert_allclose( - np.dot(otda.log_['all_domains'][i]['D1'], np.sum(otda.coupling_[i], axis=1)), otda.proportions_, rtol=1e-3, atol=1e-3) + np.dot(otda.log_['all_domains'][i]['D1'], np.sum(otda.coupling_[i], axis=1)), otda.proportions_, rtol=1e-3, + atol=1e-3) # test transform transp_Xs = otda.transform(Xs=Xs) -- cgit v1.2.3 From 592f933085d5b521a440eb91eccc283c43732170 Mon Sep 17 00:00:00 2001 From: AdrienCorenflos Date: Wed, 1 Apr 2020 12:14:42 +0100 Subject: Fix ordering --- ot/lp/__init__.py | 2 +- test/test_ot.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) (limited to 'test') diff --git a/ot/lp/__init__.py b/ot/lp/__init__.py index cdd505d..4c968ca 100644 --- a/ot/lp/__init__.py +++ b/ot/lp/__init__.py @@ -656,7 +656,7 @@ def emd_1d(x_a, x_b, a=None, b=None, metric='sqeuclidean', p=1., dense=True, perm_a = np.argsort(x_a_1d) perm_b = np.argsort(x_b_1d) - G_sorted, indices, cost = emd_1d_sorted(a, b, + G_sorted, indices, cost = emd_1d_sorted(a[perm_a.flatten()], b[perm_b.flatten()], x_a_1d[perm_a], x_b_1d[perm_b], metric=metric, p=p) G = coo_matrix((G_sorted, (perm_a[indices[:, 0]], perm_b[indices[:, 1]])), diff --git a/test/test_ot.py b/test/test_ot.py index 47df946..7afdae3 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -91,6 +91,44 @@ def test_emd_1d_emd2_1d(): with pytest.raises(AssertionError): ot.emd_1d(u, v, [], []) +def test_emd_1d_emd2_1d_with_weights(): + + # test emd1d gives similar results as emd + n = 20 + m = 30 + rng = np.random.RandomState(0) + u = rng.randn(n, 1) + v = rng.randn(m, 1) + + w_u = rng.uniform(0., 1., n) + w_u = w_u / w_u.sum() + + w_v = rng.uniform(0., 1., m) + w_v = w_v / w_v.sum() + + M = ot.dist(u, v, metric='sqeuclidean') + + G, log = ot.emd(w_u, w_v, M, log=True) + wass = log["cost"] + G_1d, log = ot.emd_1d(u, v, w_u, w_v, metric='sqeuclidean', log=True) + wass1d = log["cost"] + wass1d_emd2 = ot.emd2_1d(u, v, w_u, w_v, metric='sqeuclidean', log=False) + wass1d_euc = ot.emd2_1d(u, v, w_u, w_v, metric='euclidean', log=False) + + # check loss is similar + np.testing.assert_allclose(wass, wass1d) + np.testing.assert_allclose(wass, wass1d_emd2) + + # check loss is similar to scipy's implementation for Euclidean metric + wass_sp = wasserstein_distance(u.reshape((-1,)), v.reshape((-1,))) + np.testing.assert_allclose(wass_sp, wass1d_euc) + + # check constraints + np.testing.assert_allclose(w_u, G.sum(1)) + np.testing.assert_allclose(w_v, G.sum(0)) + + + def test_wass_1d(): # test emd1d gives similar results as emd -- cgit v1.2.3 From 1e2e118e3a30224932ed2f012bb8f9f0f374ef2c Mon Sep 17 00:00:00 2001 From: AdrienCorenflos Date: Thu, 2 Apr 2020 10:39:55 +0100 Subject: Fix test --- test/test_ot.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) (limited to 'test') diff --git a/test/test_ot.py b/test/test_ot.py index 7afdae3..0f1357f 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -7,11 +7,11 @@ import warnings import numpy as np +import pytest from scipy.stats import wasserstein_distance import ot from ot.datasets import make_1D_gauss as gauss -import pytest def test_emd_dimension_mismatch(): @@ -75,12 +75,12 @@ def test_emd_1d_emd2_1d(): np.testing.assert_allclose(wass, wass1d_emd2) # check loss is similar to scipy's implementation for Euclidean metric - wass_sp = wasserstein_distance(u.reshape((-1, )), v.reshape((-1, ))) + wass_sp = wasserstein_distance(u.reshape((-1,)), v.reshape((-1,))) np.testing.assert_allclose(wass_sp, wass1d_euc) # check constraints - np.testing.assert_allclose(np.ones((n, )) / n, G.sum(1)) - np.testing.assert_allclose(np.ones((m, )) / m, G.sum(0)) + np.testing.assert_allclose(np.ones((n,)) / n, G.sum(1)) + np.testing.assert_allclose(np.ones((m,)) / m, G.sum(0)) # check G is similar np.testing.assert_allclose(G, G_1d) @@ -91,8 +91,8 @@ def test_emd_1d_emd2_1d(): with pytest.raises(AssertionError): ot.emd_1d(u, v, [], []) -def test_emd_1d_emd2_1d_with_weights(): +def test_emd_1d_emd2_1d_with_weights(): # test emd1d gives similar results as emd n = 20 m = 30 @@ -120,7 +120,7 @@ def test_emd_1d_emd2_1d_with_weights(): np.testing.assert_allclose(wass, wass1d_emd2) # check loss is similar to scipy's implementation for Euclidean metric - wass_sp = wasserstein_distance(u.reshape((-1,)), v.reshape((-1,))) + wass_sp = wasserstein_distance(u.reshape((-1,)), v.reshape((-1,)), w_u, w_v) np.testing.assert_allclose(wass_sp, wass1d_euc) # check constraints @@ -128,8 +128,6 @@ def test_emd_1d_emd2_1d_with_weights(): np.testing.assert_allclose(w_v, G.sum(0)) - - def test_wass_1d(): # test emd1d gives similar results as emd n = 20 @@ -173,7 +171,6 @@ def test_emd_empty(): def test_emd_sparse(): - n = 100 rng = np.random.RandomState(0) @@ -249,7 +246,6 @@ def test_emd2_multi(): def test_lp_barycenter(): - a1 = np.array([1.0, 0, 0])[:, None] a2 = np.array([0, 0, 1.0])[:, None] @@ -266,7 +262,6 @@ def test_lp_barycenter(): def test_free_support_barycenter(): - measures_locations = [np.array([-1.]).reshape((1, 1)), np.array([1.]).reshape((1, 1))] measures_weights = [np.array([1.]), np.array([1.])] @@ -282,7 +277,6 @@ def test_free_support_barycenter(): @pytest.mark.skipif(not ot.lp.cvx.cvxopt, reason="No cvxopt available") def test_lp_barycenter_cvxopt(): - a1 = np.array([1.0, 0, 0])[:, None] a2 = np.array([0, 0, 1.0])[:, None] -- cgit v1.2.3 From 90f5d5f60af9ef25d7aba715e2398946e5ee16da Mon Sep 17 00:00:00 2001 From: ievred Date: Fri, 3 Apr 2020 16:02:39 +0200 Subject: laplace emd+sinkhorn --- examples/plot_otda_laplacian.py | 149 +++ ot1/da.py | 2551 +++++++++++++++++++++++++++++++++++++++ test/test_da.py | 107 +- 3 files changed, 2806 insertions(+), 1 deletion(-) create mode 100644 examples/plot_otda_laplacian.py create mode 100644 ot1/da.py (limited to 'test') diff --git a/examples/plot_otda_laplacian.py b/examples/plot_otda_laplacian.py new file mode 100644 index 0000000..d9ae280 --- /dev/null +++ b/examples/plot_otda_laplacian.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- +""" +======================== +OT for domain adaptation +======================== + +This example introduces a domain adaptation in a 2D setting and OTDA +approaches with Laplacian regularization. + +""" + +# Authors: Ievgen Redko + +# License: MIT License + +import matplotlib.pylab as pl +import ot + +############################################################################## +# Generate data +# ------------- + +n_source_samples = 150 +n_target_samples = 150 + +Xs, ys = ot.datasets.make_data_classif('3gauss', n_source_samples) +Xt, yt = ot.datasets.make_data_classif('3gauss2', n_target_samples) + + +############################################################################## +# Instantiate the different transport algorithms and fit them +# ----------------------------------------------------------- + +# EMD Transport +ot_emd = ot.da.EMDTransport() +ot_emd.fit(Xs=Xs, Xt=Xt) + +# Sinkhorn Transport +ot_sinkhorn = ot.da.SinkhornTransport(reg_e=.5) +ot_sinkhorn.fit(Xs=Xs, Xt=Xt) + +# EMD Transport with Laplacian regularization +ot_emd_laplace = ot.da.EMDLaplaceTransport(reg_lap=100, reg_src=1) +ot_emd_laplace.fit(Xs=Xs, Xt=Xt) + +# Sinkhorn Transport with Laplacian regularization +ot_sinkhorn_laplace = ot.da.SinkhornLaplaceTransport(reg_e=.5, reg_lap=100, reg_src=1) +ot_sinkhorn_laplace.fit(Xs=Xs, Xt=Xt) + +# transport source samples onto target samples +transp_Xs_emd = ot_emd.transform(Xs=Xs) +transp_Xs_sinkhorn = ot_sinkhorn.transform(Xs=Xs) +transp_Xs_emd_laplace = ot_emd_laplace.transform(Xs=Xs) +transp_Xs_sinkhorn_laplace = ot_sinkhorn_laplace.transform(Xs=Xs) + +############################################################################## +# Fig 1 : plots source and target samples +# --------------------------------------- + +pl.figure(1, figsize=(10, 5)) +pl.subplot(1, 2, 1) +pl.scatter(Xs[:, 0], Xs[:, 1], c=ys, marker='+', label='Source samples') +pl.xticks([]) +pl.yticks([]) +pl.legend(loc=0) +pl.title('Source samples') + +pl.subplot(1, 2, 2) +pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', label='Target samples') +pl.xticks([]) +pl.yticks([]) +pl.legend(loc=0) +pl.title('Target samples') +pl.tight_layout() + + +############################################################################## +# Fig 2 : plot optimal couplings and transported samples +# ------------------------------------------------------ + +param_img = {'interpolation': 'nearest'} + +n_plots = 2 + +pl.figure(2, figsize=(15, 8)) +pl.subplot(2, 2*n_plots, 1) +pl.imshow(ot_emd.coupling_, **param_img) +pl.xticks([]) +pl.yticks([]) +pl.title('Optimal coupling\nEMDTransport') + +pl.figure(2, figsize=(15, 8)) +pl.subplot(2, 2*n_plots, 2) +pl.imshow(ot_sinkhorn.coupling_, **param_img) +pl.xticks([]) +pl.yticks([]) +pl.title('Optimal coupling\nSinkhornTransport') + +pl.subplot(2, 2*n_plots, 3) +pl.imshow(ot_emd_laplace.coupling_, **param_img) +pl.xticks([]) +pl.yticks([]) +pl.title('Optimal coupling\nEMDLaplaceTransport') + +pl.subplot(2, 2*n_plots, 4) +pl.imshow(ot_emd_laplace.coupling_, **param_img) +pl.xticks([]) +pl.yticks([]) +pl.title('Optimal coupling\nSinkhornLaplaceTransport') + +pl.subplot(2, 2*n_plots, 5) +pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', + label='Target samples', alpha=0.3) +pl.scatter(transp_Xs_emd[:, 0], transp_Xs_emd[:, 1], c=ys, + marker='+', label='Transp samples', s=30) +pl.xticks([]) +pl.yticks([]) +pl.title('Transported samples\nEmdTransport') +pl.legend(loc="lower left") + +pl.subplot(2, 2*n_plots, 6) +pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', + label='Target samples', alpha=0.3) +pl.scatter(transp_Xs_sinkhorn[:, 0], transp_Xs_sinkhorn[:, 1], c=ys, + marker='+', label='Transp samples', s=30) +pl.xticks([]) +pl.yticks([]) +pl.title('Transported samples\nSinkhornTransport') + +pl.subplot(2, 2*n_plots, 7) +pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', + label='Target samples', alpha=0.3) +pl.scatter(transp_Xs_emd_laplace[:, 0], transp_Xs_emd_laplace[:, 1], c=ys, + marker='+', label='Transp samples', s=30) +pl.xticks([]) +pl.yticks([]) +pl.title('Transported samples\nEMDLaplaceTransport') + +pl.subplot(2, 2*n_plots, 8) +pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', + label='Target samples', alpha=0.3) +pl.scatter(transp_Xs_sinkhorn_laplace[:, 0], transp_Xs_sinkhorn_laplace[:, 1], c=ys, + marker='+', label='Transp samples', s=30) +pl.xticks([]) +pl.yticks([]) +pl.title('Transported samples\nSinkhornLaplaceTransport') +pl.tight_layout() + +pl.show() diff --git a/ot1/da.py b/ot1/da.py new file mode 100644 index 0000000..39e8c4c --- /dev/null +++ b/ot1/da.py @@ -0,0 +1,2551 @@ +# -*- coding: utf-8 -*- +""" +Domain adaptation with optimal transport +""" + +# Author: Remi Flamary +# Nicolas Courty +# Michael Perrot +# Nathalie Gayraud +# Ievgen Redko +# +# License: MIT License + +import numpy as np +import scipy.linalg as linalg + +from .bregman import sinkhorn, jcpot_barycenter +from .lp import emd +from .utils import unif, dist, kernel, cost_normalization, laplacian +from .utils import check_params, BaseEstimator +from .unbalanced import sinkhorn_unbalanced +from .optim import cg +from .optim import gcg + + +def sinkhorn_lpl1_mm(a, labels_a, b, M, reg, eta=0.1, numItermax=10, + numInnerItermax=200, stopInnerThr=1e-9, verbose=False, + log=False): + """ + Solve the entropic regularization optimal transport problem with nonconvex + group lasso regularization + + The function solves the following optimization problem: + + .. math:: + \gamma = arg\min_\gamma <\gamma,M>_F + reg\cdot\Omega_e(\gamma) + + \eta \Omega_g(\gamma) + + s.t. \gamma 1 = a + + \gamma^T 1= b + + \gamma\geq 0 + where : + + - M is the (ns,nt) metric cost matrix + - :math:`\Omega_e` is the entropic regularization term :math:`\Omega_e + (\gamma)=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` + - :math:`\Omega_g` is the group lasso regularization term + :math:`\Omega_g(\gamma)=\sum_{i,c} \|\gamma_{i,\mathcal{I}_c}\|^{1/2}_1` + where :math:`\mathcal{I}_c` are the index of samples from class c + in the source domain. + - a and b are source and target weights (sum to 1) + + The algorithm used for solving the problem is the generalized conditional + gradient as proposed in [5]_ [7]_ + + + Parameters + ---------- + a : np.ndarray (ns,) + samples weights in the source domain + labels_a : np.ndarray (ns,) + labels of samples in the source domain + b : np.ndarray (nt,) + samples weights in the target domain + M : np.ndarray (ns,nt) + loss matrix + reg : float + Regularization term for entropic regularization >0 + eta : float, optional + Regularization term for group lasso regularization >0 + numItermax : int, optional + Max number of iterations + numInnerItermax : int, optional + Max number of iterations (inner sinkhorn solver) + stopInnerThr : float, optional + Stop threshold on error (inner sinkhorn solver) (>0) + verbose : bool, optional + Print information along iterations + log : bool, optional + record log if True + + + Returns + ------- + gamma : (ns x nt) ndarray + Optimal transportation matrix for the given parameters + log : dict + log dictionary return only if log==True in parameters + + + References + ---------- + + .. [5] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, + "Optimal Transport for Domain Adaptation," in IEEE + Transactions on Pattern Analysis and Machine Intelligence , + vol.PP, no.99, pp.1-1 + .. [7] Rakotomamonjy, A., Flamary, R., & Courty, N. (2015). + Generalized conditional gradient: analysis of convergence + and applications. arXiv preprint arXiv:1510.06567. + + See Also + -------- + ot.lp.emd : Unregularized OT + ot.bregman.sinkhorn : Entropic regularized OT + ot.optim.cg : General regularized OT + + """ + p = 0.5 + epsilon = 1e-3 + + indices_labels = [] + classes = np.unique(labels_a) + for c in classes: + idxc, = np.where(labels_a == c) + indices_labels.append(idxc) + + W = np.zeros(M.shape) + + for cpt in range(numItermax): + Mreg = M + eta * W + transp = sinkhorn(a, b, Mreg, reg, numItermax=numInnerItermax, + stopThr=stopInnerThr) + # the transport has been computed. Check if classes are really + # separated + W = np.ones(M.shape) + for (i, c) in enumerate(classes): + majs = np.sum(transp[indices_labels[i]], axis=0) + majs = p * ((majs + epsilon) ** (p - 1)) + W[indices_labels[i]] = majs + + return transp + + +def sinkhorn_l1l2_gl(a, labels_a, b, M, reg, eta=0.1, numItermax=10, + numInnerItermax=200, stopInnerThr=1e-9, verbose=False, + log=False): + """ + Solve the entropic regularization optimal transport problem with group + lasso regularization + + The function solves the following optimization problem: + + .. math:: + \gamma = arg\min_\gamma <\gamma,M>_F + reg\cdot\Omega_e(\gamma)+ + \eta \Omega_g(\gamma) + + s.t. \gamma 1 = a + + \gamma^T 1= b + + \gamma\geq 0 + where : + + - M is the (ns,nt) metric cost matrix + - :math:`\Omega_e` is the entropic regularization term + :math:`\Omega_e(\gamma)=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` + - :math:`\Omega_g` is the group lasso regulaization term + :math:`\Omega_g(\gamma)=\sum_{i,c} \|\gamma_{i,\mathcal{I}_c}\|^2` + where :math:`\mathcal{I}_c` are the index of samples from class + c in the source domain. + - a and b are source and target weights (sum to 1) + + The algorithm used for solving the problem is the generalised conditional + gradient as proposed in [5]_ [7]_ + + + Parameters + ---------- + a : np.ndarray (ns,) + samples weights in the source domain + labels_a : np.ndarray (ns,) + labels of samples in the source domain + b : np.ndarray (nt,) + samples in the target domain + M : np.ndarray (ns,nt) + loss matrix + reg : float + Regularization term for entropic regularization >0 + eta : float, optional + Regularization term for group lasso regularization >0 + numItermax : int, optional + Max number of iterations + numInnerItermax : int, optional + Max number of iterations (inner sinkhorn solver) + stopInnerThr : float, optional + Stop threshold on error (inner sinkhorn solver) (>0) + verbose : bool, optional + Print information along iterations + log : bool, optional + record log if True + + + Returns + ------- + gamma : (ns x nt) ndarray + Optimal transportation matrix for the given parameters + log : dict + log dictionary return only if log==True in parameters + + + References + ---------- + + .. [5] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, + "Optimal Transport for Domain Adaptation," in IEEE Transactions + on Pattern Analysis and Machine Intelligence , vol.PP, no.99, pp.1-1 + .. [7] Rakotomamonjy, A., Flamary, R., & Courty, N. (2015). + Generalized conditional gradient: analysis of convergence and + applications. arXiv preprint arXiv:1510.06567. + + See Also + -------- + ot.optim.gcg : Generalized conditional gradient for OT problems + + """ + lstlab = np.unique(labels_a) + + def f(G): + res = 0 + for i in range(G.shape[1]): + for lab in lstlab: + temp = G[labels_a == lab, i] + res += np.linalg.norm(temp) + return res + + def df(G): + W = np.zeros(G.shape) + for i in range(G.shape[1]): + for lab in lstlab: + temp = G[labels_a == lab, i] + n = np.linalg.norm(temp) + if n: + W[labels_a == lab, i] = temp / n + return W + + return gcg(a, b, M, reg, eta, f, df, G0=None, numItermax=numItermax, + numInnerItermax=numInnerItermax, stopThr=stopInnerThr, + verbose=verbose, log=log) + + +def joint_OT_mapping_linear(xs, xt, mu=1, eta=0.001, bias=False, verbose=False, + verbose2=False, numItermax=100, numInnerItermax=10, + stopInnerThr=1e-6, stopThr=1e-5, log=False, + **kwargs): + """Joint OT and linear mapping estimation as proposed in [8] + + The function solves the following optimization problem: + + .. math:: + \min_{\gamma,L}\quad \|L(X_s) -n_s\gamma X_t\|^2_F + + \mu<\gamma,M>_F + \eta \|L -I\|^2_F + + s.t. \gamma 1 = a + + \gamma^T 1= b + + \gamma\geq 0 + where : + + - M is the (ns,nt) squared euclidean cost matrix between samples in + Xs and Xt (scaled by ns) + - :math:`L` is a dxd linear operator that approximates the barycentric + mapping + - :math:`I` is the identity matrix (neutral linear mapping) + - a and b are uniform source and target weights + + The problem consist in solving jointly an optimal transport matrix + :math:`\gamma` and a linear mapping that fits the barycentric mapping + :math:`n_s\gamma X_t`. + + One can also estimate a mapping with constant bias (see supplementary + material of [8]) using the bias optional argument. + + The algorithm used for solving the problem is the block coordinate + descent that alternates between updates of G (using conditionnal gradient) + and the update of L using a classical least square solver. + + + Parameters + ---------- + xs : np.ndarray (ns,d) + samples in the source domain + xt : np.ndarray (nt,d) + samples in the target domain + mu : float,optional + Weight for the linear OT loss (>0) + eta : float, optional + Regularization term for the linear mapping L (>0) + bias : bool,optional + Estimate linear mapping with constant bias + numItermax : int, optional + Max number of BCD iterations + stopThr : float, optional + Stop threshold on relative loss decrease (>0) + numInnerItermax : int, optional + Max number of iterations (inner CG solver) + stopInnerThr : float, optional + Stop threshold on error (inner CG solver) (>0) + verbose : bool, optional + Print information along iterations + log : bool, optional + record log if True + + + Returns + ------- + gamma : (ns x nt) ndarray + Optimal transportation matrix for the given parameters + L : (d x d) ndarray + Linear mapping matrix (d+1 x d if bias) + log : dict + log dictionary return only if log==True in parameters + + + References + ---------- + + .. [8] M. Perrot, N. Courty, R. Flamary, A. Habrard, + "Mapping estimation for discrete optimal transport", + Neural Information Processing Systems (NIPS), 2016. + + See Also + -------- + ot.lp.emd : Unregularized OT + ot.optim.cg : General regularized OT + + """ + + ns, nt, d = xs.shape[0], xt.shape[0], xt.shape[1] + + if bias: + xs1 = np.hstack((xs, np.ones((ns, 1)))) + xstxs = xs1.T.dot(xs1) + Id = np.eye(d + 1) + Id[-1] = 0 + I0 = Id[:, :-1] + + def sel(x): + return x[:-1, :] + else: + xs1 = xs + xstxs = xs1.T.dot(xs1) + Id = np.eye(d) + I0 = Id + + def sel(x): + return x + + if log: + log = {'err': []} + + a, b = unif(ns), unif(nt) + M = dist(xs, xt) * ns + G = emd(a, b, M) + + vloss = [] + + def loss(L, G): + """Compute full loss""" + return np.sum((xs1.dot(L) - ns * G.dot(xt)) ** 2) + mu * \ + np.sum(G * M) + eta * np.sum(sel(L - I0) ** 2) + + def solve_L(G): + """ solve L problem with fixed G (least square)""" + xst = ns * G.dot(xt) + return np.linalg.solve(xstxs + eta * Id, xs1.T.dot(xst) + eta * I0) + + def solve_G(L, G0): + """Update G with CG algorithm""" + xsi = xs1.dot(L) + + def f(G): + return np.sum((xsi - ns * G.dot(xt)) ** 2) + + def df(G): + return -2 * ns * (xsi - ns * G.dot(xt)).dot(xt.T) + + G = cg(a, b, M, 1.0 / mu, f, df, G0=G0, + numItermax=numInnerItermax, stopThr=stopInnerThr) + return G + + L = solve_L(G) + + vloss.append(loss(L, G)) + + if verbose: + print('{:5s}|{:12s}|{:8s}'.format( + 'It.', 'Loss', 'Delta loss') + '\n' + '-' * 32) + print('{:5d}|{:8e}|{:8e}'.format(0, vloss[-1], 0)) + + # init loop + if numItermax > 0: + loop = 1 + else: + loop = 0 + it = 0 + + while loop: + + it += 1 + + # update G + G = solve_G(L, G) + + # update L + L = solve_L(G) + + vloss.append(loss(L, G)) + + if it >= numItermax: + loop = 0 + + if abs(vloss[-1] - vloss[-2]) / abs(vloss[-2]) < stopThr: + loop = 0 + + if verbose: + if it % 20 == 0: + print('{:5s}|{:12s}|{:8s}'.format( + 'It.', 'Loss', 'Delta loss') + '\n' + '-' * 32) + print('{:5d}|{:8e}|{:8e}'.format( + it, vloss[-1], (vloss[-1] - vloss[-2]) / abs(vloss[-2]))) + if log: + log['loss'] = vloss + return G, L, log + else: + return G, L + + +def joint_OT_mapping_kernel(xs, xt, mu=1, eta=0.001, kerneltype='gaussian', + sigma=1, bias=False, verbose=False, verbose2=False, + numItermax=100, numInnerItermax=10, + stopInnerThr=1e-6, stopThr=1e-5, log=False, + **kwargs): + """Joint OT and nonlinear mapping estimation with kernels as proposed in [8] + + The function solves the following optimization problem: + + .. math:: + \min_{\gamma,L\in\mathcal{H}}\quad \|L(X_s) - + n_s\gamma X_t\|^2_F + \mu<\gamma,M>_F + \eta \|L\|^2_\mathcal{H} + + s.t. \gamma 1 = a + + \gamma^T 1= b + + \gamma\geq 0 + where : + + - M is the (ns,nt) squared euclidean cost matrix between samples in + Xs and Xt (scaled by ns) + - :math:`L` is a ns x d linear operator on a kernel matrix that + approximates the barycentric mapping + - a and b are uniform source and target weights + + The problem consist in solving jointly an optimal transport matrix + :math:`\gamma` and the nonlinear mapping that fits the barycentric mapping + :math:`n_s\gamma X_t`. + + One can also estimate a mapping with constant bias (see supplementary + material of [8]) using the bias optional argument. + + The algorithm used for solving the problem is the block coordinate + descent that alternates between updates of G (using conditionnal gradient) + and the update of L using a classical kernel least square solver. + + + Parameters + ---------- + xs : np.ndarray (ns,d) + samples in the source domain + xt : np.ndarray (nt,d) + samples in the target domain + mu : float,optional + Weight for the linear OT loss (>0) + eta : float, optional + Regularization term for the linear mapping L (>0) + kerneltype : str,optional + kernel used by calling function ot.utils.kernel (gaussian by default) + sigma : float, optional + Gaussian kernel bandwidth. + bias : bool,optional + Estimate linear mapping with constant bias + verbose : bool, optional + Print information along iterations + verbose2 : bool, optional + Print information along iterations + numItermax : int, optional + Max number of BCD iterations + numInnerItermax : int, optional + Max number of iterations (inner CG solver) + stopInnerThr : float, optional + Stop threshold on error (inner CG solver) (>0) + stopThr : float, optional + Stop threshold on relative loss decrease (>0) + log : bool, optional + record log if True + + + Returns + ------- + gamma : (ns x nt) ndarray + Optimal transportation matrix for the given parameters + L : (ns x d) ndarray + Nonlinear mapping matrix (ns+1 x d if bias) + log : dict + log dictionary return only if log==True in parameters + + + References + ---------- + + .. [8] M. Perrot, N. Courty, R. Flamary, A. Habrard, + "Mapping estimation for discrete optimal transport", + Neural Information Processing Systems (NIPS), 2016. + + See Also + -------- + ot.lp.emd : Unregularized OT + ot.optim.cg : General regularized OT + + """ + + ns, nt = xs.shape[0], xt.shape[0] + + K = kernel(xs, xs, method=kerneltype, sigma=sigma) + if bias: + K1 = np.hstack((K, np.ones((ns, 1)))) + Id = np.eye(ns + 1) + Id[-1] = 0 + Kp = np.eye(ns + 1) + Kp[:ns, :ns] = K + + # ls regu + # K0 = K1.T.dot(K1)+eta*I + # Kreg=I + + # RKHS regul + K0 = K1.T.dot(K1) + eta * Kp + Kreg = Kp + + else: + K1 = K + Id = np.eye(ns) + + # ls regul + # K0 = K1.T.dot(K1)+eta*I + # Kreg=I + + # proper kernel ridge + K0 = K + eta * Id + Kreg = K + + if log: + log = {'err': []} + + a, b = unif(ns), unif(nt) + M = dist(xs, xt) * ns + G = emd(a, b, M) + + vloss = [] + + def loss(L, G): + """Compute full loss""" + return np.sum((K1.dot(L) - ns * G.dot(xt)) ** 2) + mu * \ + np.sum(G * M) + eta * np.trace(L.T.dot(Kreg).dot(L)) + + def solve_L_nobias(G): + """ solve L problem with fixed G (least square)""" + xst = ns * G.dot(xt) + return np.linalg.solve(K0, xst) + + def solve_L_bias(G): + """ solve L problem with fixed G (least square)""" + xst = ns * G.dot(xt) + return np.linalg.solve(K0, K1.T.dot(xst)) + + def solve_G(L, G0): + """Update G with CG algorithm""" + xsi = K1.dot(L) + + def f(G): + return np.sum((xsi - ns * G.dot(xt)) ** 2) + + def df(G): + return -2 * ns * (xsi - ns * G.dot(xt)).dot(xt.T) + + G = cg(a, b, M, 1.0 / mu, f, df, G0=G0, + numItermax=numInnerItermax, stopThr=stopInnerThr) + return G + + if bias: + solve_L = solve_L_bias + else: + solve_L = solve_L_nobias + + L = solve_L(G) + + vloss.append(loss(L, G)) + + if verbose: + print('{:5s}|{:12s}|{:8s}'.format( + 'It.', 'Loss', 'Delta loss') + '\n' + '-' * 32) + print('{:5d}|{:8e}|{:8e}'.format(0, vloss[-1], 0)) + + # init loop + if numItermax > 0: + loop = 1 + else: + loop = 0 + it = 0 + + while loop: + + it += 1 + + # update G + G = solve_G(L, G) + + # update L + L = solve_L(G) + + vloss.append(loss(L, G)) + + if it >= numItermax: + loop = 0 + + if abs(vloss[-1] - vloss[-2]) / abs(vloss[-2]) < stopThr: + loop = 0 + + if verbose: + if it % 20 == 0: + print('{:5s}|{:12s}|{:8s}'.format( + 'It.', 'Loss', 'Delta loss') + '\n' + '-' * 32) + print('{:5d}|{:8e}|{:8e}'.format( + it, vloss[-1], (vloss[-1] - vloss[-2]) / abs(vloss[-2]))) + if log: + log['loss'] = vloss + return G, L, log + else: + return G, L + + +def OT_mapping_linear(xs, xt, reg=1e-6, ws=None, + wt=None, bias=True, log=False): + """ return OT linear operator between samples + + The function estimates the optimal linear operator that aligns the two + empirical distributions. This is equivalent to estimating the closed + form mapping between two Gaussian distributions :math:`N(\mu_s,\Sigma_s)` + and :math:`N(\mu_t,\Sigma_t)` as proposed in [14] and discussed in remark + 2.29 in [15]. + + The linear operator from source to target :math:`M` + + .. math:: + M(x)=Ax+b + + where : + + .. math:: + A=\Sigma_s^{-1/2}(\Sigma_s^{1/2}\Sigma_t\Sigma_s^{1/2})^{1/2} + \Sigma_s^{-1/2} + .. math:: + b=\mu_t-A\mu_s + + Parameters + ---------- + xs : np.ndarray (ns,d) + samples in the source domain + xt : np.ndarray (nt,d) + samples in the target domain + reg : float,optional + regularization added to the diagonals of convariances (>0) + ws : np.ndarray (ns,1), optional + weights for the source samples + wt : np.ndarray (ns,1), optional + weights for the target samples + bias: boolean, optional + estimate bias b else b=0 (default:True) + log : bool, optional + record log if True + + + Returns + ------- + A : (d x d) ndarray + Linear operator + b : (1 x d) ndarray + bias + log : dict + log dictionary return only if log==True in parameters + + + References + ---------- + + .. [14] Knott, M. and Smith, C. S. "On the optimal mapping of + distributions", Journal of Optimization Theory and Applications + Vol 43, 1984 + + .. [15] Peyré, G., & Cuturi, M. (2017). "Computational Optimal + Transport", 2018. + + + """ + + d = xs.shape[1] + + if bias: + mxs = xs.mean(0, keepdims=True) + mxt = xt.mean(0, keepdims=True) + + xs = xs - mxs + xt = xt - mxt + else: + mxs = np.zeros((1, d)) + mxt = np.zeros((1, d)) + + if ws is None: + ws = np.ones((xs.shape[0], 1)) / xs.shape[0] + + if wt is None: + wt = np.ones((xt.shape[0], 1)) / xt.shape[0] + + Cs = (xs * ws).T.dot(xs) / ws.sum() + reg * np.eye(d) + Ct = (xt * wt).T.dot(xt) / wt.sum() + reg * np.eye(d) + + Cs12 = linalg.sqrtm(Cs) + Cs_12 = linalg.inv(Cs12) + + M0 = linalg.sqrtm(Cs12.dot(Ct.dot(Cs12))) + + A = Cs_12.dot(M0.dot(Cs_12)) + + b = mxt - mxs.dot(A) + + if log: + log = {} + log['Cs'] = Cs + log['Ct'] = Ct + log['Cs12'] = Cs12 + log['Cs_12'] = Cs_12 + return A, b, log + else: + return A, b + + +def emd_laplace(a, b, xs, xt, M, eta=1., alpha=0.5, + numItermax=1000, stopThr=1e-5, numInnerItermax=1000, + stopInnerThr=1e-6, log=False, verbose=False, **kwargs): + r"""Solve the optimal transport problem (OT) with Laplacian regularization + + .. math:: + \gamma = arg\min_\gamma <\gamma,M>_F + eta\Omega_\alpha(\gamma) + + s.t.\ \gamma 1 = a + + \gamma^T 1= b + + \gamma\geq 0 + + where: + + - a and b are source and target weights (sum to 1) + - xs and xt are source and target samples + - M is the (ns,nt) metric cost matrix + - :math:`\Omega_\alpha` is the Laplacian regularization term + :math:`\Omega_\alpha = (1-\alpha)/n_s^2\sum_{i,j}S^s_{i,j}\|T(\mathbf{x}^s_i)-T(\mathbf{x}^s_j)\|^2+\alpha/n_t^2\sum_{i,j}S^t_{i,j}^'\|T(\mathbf{x}^t_i)-T(\mathbf{x}^t_j)\|^2` + with :math:`S^s_{i,j}, S^t_{i,j}` denoting source and target similarity matrices and :math:`T(\cdot)` being a barycentric mapping + + The algorithm used for solving the problem is the conditional gradient algorithm as proposed in [5]. + + Parameters + ---------- + a : np.ndarray (ns,) + samples weights in the source domain + b : np.ndarray (nt,) + samples weights in the target domain + xs : np.ndarray (ns,d) + samples in the source domain + xt : np.ndarray (nt,d) + samples in the target domain + M : np.ndarray (ns,nt) + loss matrix + eta : float + Regularization term for Laplacian regularization + alpha : float + Regularization term for source domain's importance in regularization + numItermax : int, optional + Max number of iterations + stopThr : float, optional + Stop threshold on error (inner emd solver) (>0) + numInnerItermax : int, optional + Max number of iterations (inner CG solver) + stopInnerThr : float, optional + Stop threshold on error (inner CG solver) (>0) + verbose : bool, optional + Print information along iterations + log : bool, optional + record log if True + + + Returns + ------- + gamma : (ns x nt) ndarray + Optimal transportation matrix for the given parameters + log : dict + log dictionary return only if log==True in parameters + + + References + ---------- + + .. [5] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, + "Optimal Transport for Domain Adaptation," in IEEE + Transactions on Pattern Analysis and Machine Intelligence , + vol.PP, no.99, pp.1-1 + + See Also + -------- + ot.lp.emd : Unregularized OT + ot.optim.cg : General regularized OT + + """ + if 'sim' not in kwargs: + kwargs['sim'] = 'knn' + + if kwargs['sim'] == 'gauss': + if 'rbfparam' not in kwargs: + kwargs['rbfparam'] = 1 / (2 * (np.mean(dist(xs, xs, 'sqeuclidean')) ** 2)) + sS = kernel(xs, xs, method=kwargs['sim'], sigma=kwargs['rbfparam']) + sT = kernel(xt, xt, method=kwargs['sim'], sigma=kwargs['rbfparam']) + + elif kwargs['sim'] == 'knn': + if 'nn' not in kwargs: + kwargs['nn'] = 5 + + from sklearn.neighbors import kneighbors_graph + + sS = kneighbors_graph(xs, kwargs['nn']).toarray() + sS = (sS + sS.T) / 2 + sT = kneighbors_graph(xt, kwargs['nn']).toarray() + sT = (sT + sT.T) / 2 + + lS = laplacian(sS) + lT = laplacian(sT) + + def f(G): + return alpha*np.trace(np.dot(xt.T, np.dot(G.T, np.dot(lS, np.dot(G, xt))))) \ + + (1-alpha)*np.trace(np.dot(xs.T, np.dot(G, np.dot(lT, np.dot(G.T, xs))))) + + def df(G): + return alpha*np.dot(lS + lS.T, np.dot(G, np.dot(xt, xt.T)))\ + +(1-alpha)*np.dot(xs, np.dot(xs.T, np.dot(G, lT + lT.T))) + + return cg(a, b, M, reg=eta, f=f, df=df, G0=None, numItermax=numItermax, numItermaxEmd=numInnerItermax, + stopThr=stopThr, stopThr2=stopInnerThr, verbose=verbose, log=log) + +def sinkhorn_laplace(a, b, xs, xt, M, reg=.1, eta=1., alpha=0.5, + numItermax=1000, stopThr=1e-5, numInnerItermax=1000, + stopInnerThr=1e-6, log=False, verbose=False, **kwargs): + r"""Solve the entropic regularized optimal transport problem (OT) with Laplacian regularization + + .. math:: + \gamma = arg\min_\gamma <\gamma,M>_F + reg\Omega_e(\gamma) + eta\Omega_\alpha(\gamma) + + s.t.\ \gamma 1 = a + + \gamma^T 1= b + + \gamma\geq 0 + + where: + + - a and b are source and target weights (sum to 1) + - xs and xt are source and target samples + - M is the (ns,nt) metric cost matrix + - :math:`\Omega_e` is the entropic regularization term :math:`\Omega_e + (\gamma)=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` + - :math:`\Omega_\alpha` is the Laplacian regularization term + :math:`\Omega_\alpha = (1-\alpha)/n_s^2\sum_{i,j}S^s_{i,j}\|T(\mathbf{x}^s_i)-T(\mathbf{x}^s_j)\|^2+\alpha/n_t^2\sum_{i,j}S^t_{i,j}^'\|T(\mathbf{x}^t_i)-T(\mathbf{x}^t_j)\|^2` + with :math:`S^s_{i,j}, S^t_{i,j}` denoting source and target similarity matrices and :math:`T(\cdot)` being a barycentric mapping + + The algorithm used for solving the problem is the conditional gradient algorithm as proposed in [5]. + + Parameters + ---------- + a : np.ndarray (ns,) + samples weights in the source domain + b : np.ndarray (nt,) + samples weights in the target domain + xs : np.ndarray (ns,d) + samples in the source domain + xt : np.ndarray (nt,d) + samples in the target domain + M : np.ndarray (ns,nt) + loss matrix + reg : float + Regularization term for entropic regularization >0 + eta : float + Regularization term for Laplacian regularization + alpha : float + Regularization term for source domain's importance in regularization + numItermax : int, optional + Max number of iterations + stopThr : float, optional + Stop threshold on error (inner sinkhorn solver) (>0) + numInnerItermax : int, optional + Max number of iterations (inner CG solver) + stopInnerThr : float, optional + Stop threshold on error (inner CG solver) (>0) + verbose : bool, optional + Print information along iterations + log : bool, optional + record log if True + + + Returns + ------- + gamma : (ns x nt) ndarray + Optimal transportation matrix for the given parameters + log : dict + log dictionary return only if log==True in parameters + + + References + ---------- + + .. [5] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, + "Optimal Transport for Domain Adaptation," in IEEE + Transactions on Pattern Analysis and Machine Intelligence , + vol.PP, no.99, pp.1-1 + + See Also + -------- + ot.lp.emd : Unregularized OT + ot.optim.cg : General regularized OT + + """ + if 'sim' not in kwargs: + kwargs['sim'] = 'knn' + + if kwargs['sim'] == 'gauss': + if 'rbfparam' not in kwargs: + kwargs['rbfparam'] = 1 / (2 * (np.mean(dist(xs, xs, 'sqeuclidean')) ** 2)) + sS = kernel(xs, xs, method=kwargs['sim'], sigma=kwargs['rbfparam']) + sT = kernel(xt, xt, method=kwargs['sim'], sigma=kwargs['rbfparam']) + + elif kwargs['sim'] == 'knn': + if 'nn' not in kwargs: + kwargs['nn'] = 5 + + from sklearn.neighbors import kneighbors_graph + + sS = kneighbors_graph(xs, kwargs['nn']).toarray() + sS = (sS + sS.T) / 2 + sT = kneighbors_graph(xt, kwargs['nn']).toarray() + sT = (sT + sT.T) / 2 + + lS = laplacian(sS) + lT = laplacian(sT) + + def f(G): + return alpha*np.trace(np.dot(xt.T, np.dot(G.T, np.dot(lS, np.dot(G, xt))))) \ + + (1-alpha)*np.trace(np.dot(xs.T, np.dot(G, np.dot(lT, np.dot(G.T, xs))))) + + def df(G): + return alpha*np.dot(lS + lS.T, np.dot(G, np.dot(xt, xt.T)))\ + +(1-alpha)*np.dot(xs, np.dot(xs.T, np.dot(G, lT + lT.T))) + + return gcg(a, b, M, reg, eta, f, df, G0=None, numItermax=numItermax, stopThr=stopThr, + numInnerItermax=numInnerItermax, stopThr2=stopInnerThr, + verbose=verbose, log=log) + +def distribution_estimation_uniform(X): + """estimates a uniform distribution from an array of samples X + + Parameters + ---------- + X : array-like, shape (n_samples, n_features) + The array of samples + + Returns + ------- + mu : array-like, shape (n_samples,) + The uniform distribution estimated from X + """ + + + return unif(X.shape[0]) + + +class BaseTransport(BaseEstimator): + + """Base class for OTDA objects + + Notes + ----- + All estimators should specify all the parameters that can be set + at the class level in their ``__init__`` as explicit keyword + arguments (no ``*args`` or ``**kwargs``). + + fit method should: + - estimate a cost matrix and store it in a `cost_` attribute + - estimate a coupling matrix and store it in a `coupling_` + attribute + - estimate distributions from source and target data and store them in + mu_s and mu_t attributes + - store Xs and Xt in attributes to be used later on in transform and + inverse_transform methods + + transform method should always get as input a Xs parameter + inverse_transform method should always get as input a Xt parameter + """ + + + def fit(self, Xs=None, ys=None, Xt=None, yt=None): + """Build a coupling matrix from source and target sets of samples + (Xs, ys) and (Xt, yt) + + Parameters + ---------- + Xs : array-like, shape (n_source_samples, n_features) + The training input samples. + ys : array-like, shape (n_source_samples,) + The class labels + Xt : array-like, shape (n_target_samples, n_features) + The training input samples. + yt : array-like, shape (n_target_samples,) + The class labels. If some target samples are unlabeled, fill the + yt's elements with -1. + + Warning: Note that, due to this convention -1 cannot be used as a + class label + + Returns + ------- + self : object + Returns self. + """ + + # check the necessary inputs parameters are here + if check_params(Xs=Xs, Xt=Xt): + + # pairwise distance + self.cost_ = dist(Xs, Xt, metric=self.metric) + self.cost_ = cost_normalization(self.cost_, self.norm) + + if (ys is not None) and (yt is not None): + + if self.limit_max != np.infty: + self.limit_max = self.limit_max * np.max(self.cost_) + + # assumes labeled source samples occupy the first rows + # and labeled target samples occupy the first columns + classes = [c for c in np.unique(ys) if c != -1] + for c in classes: + idx_s = np.where((ys != c) & (ys != -1)) + idx_t = np.where(yt == c) + + # all the coefficients corresponding to a source sample + # and a target sample : + # with different labels get a infinite + for j in idx_t[0]: + self.cost_[idx_s[0], j] = self.limit_max + + # distribution estimation + self.mu_s = self.distribution_estimation(Xs) + self.mu_t = self.distribution_estimation(Xt) + + # store arrays of samples + self.xs_ = Xs + self.xt_ = Xt + + return self + + + def fit_transform(self, Xs=None, ys=None, Xt=None, yt=None): + """Build a coupling matrix from source and target sets of samples + (Xs, ys) and (Xt, yt) and transports source samples Xs onto target + ones Xt + + Parameters + ---------- + Xs : array-like, shape (n_source_samples, n_features) + The training input samples. + ys : array-like, shape (n_source_samples,) + The class labels + Xt : array-like, shape (n_target_samples, n_features) + The training input samples. + yt : array-like, shape (n_target_samples,) + The class labels. If some target samples are unlabeled, fill the + yt's elements with -1. + + Warning: Note that, due to this convention -1 cannot be used as a + class label + + Returns + ------- + transp_Xs : array-like, shape (n_source_samples, n_features) + The source samples samples. + """ + + return self.fit(Xs, ys, Xt, yt).transform(Xs, ys, Xt, yt) + + + def transform(self, Xs=None, ys=None, Xt=None, yt=None, batch_size=128): + """Transports source samples Xs onto target ones Xt + + Parameters + ---------- + Xs : array-like, shape (n_source_samples, n_features) + The training input samples. + ys : array-like, shape (n_source_samples,) + The class labels + Xt : array-like, shape (n_target_samples, n_features) + The training input samples. + yt : array-like, shape (n_target_samples,) + The class labels. If some target samples are unlabeled, fill the + yt's elements with -1. + + Warning: Note that, due to this convention -1 cannot be used as a + class label + batch_size : int, optional (default=128) + The batch size for out of sample inverse transform + + Returns + ------- + transp_Xs : array-like, shape (n_source_samples, n_features) + The transport source samples. + """ + + # check the necessary inputs parameters are here + if check_params(Xs=Xs): + + if np.array_equal(self.xs_, Xs): + + # perform standard barycentric mapping + transp = self.coupling_ / np.sum(self.coupling_, 1)[:, None] + + # set nans to 0 + transp[~ np.isfinite(transp)] = 0 + + # compute transported samples + transp_Xs = np.dot(transp, self.xt_) + else: + # perform out of sample mapping + indices = np.arange(Xs.shape[0]) + batch_ind = [ + indices[i:i + batch_size] + for i in range(0, len(indices), batch_size)] + + transp_Xs = [] + for bi in batch_ind: + # get the nearest neighbor in the source domain + D0 = dist(Xs[bi], self.xs_) + idx = np.argmin(D0, axis=1) + + # transport the source samples + transp = self.coupling_ / np.sum( + self.coupling_, 1)[:, None] + transp[~ np.isfinite(transp)] = 0 + transp_Xs_ = np.dot(transp, self.xt_) + + # define the transported points + transp_Xs_ = transp_Xs_[idx, :] + Xs[bi] - self.xs_[idx, :] + + transp_Xs.append(transp_Xs_) + + transp_Xs = np.concatenate(transp_Xs, axis=0) + + return transp_Xs + + + def inverse_transform(self, Xs=None, ys=None, Xt=None, yt=None, + batch_size=128): + """Transports target samples Xt onto target samples Xs + + Parameters + ---------- + Xs : array-like, shape (n_source_samples, n_features) + The training input samples. + ys : array-like, shape (n_source_samples,) + The class labels + Xt : array-like, shape (n_target_samples, n_features) + The training input samples. + yt : array-like, shape (n_target_samples,) + The class labels. If some target samples are unlabeled, fill the + yt's elements with -1. + + Warning: Note that, due to this convention -1 cannot be used as a + class label + batch_size : int, optional (default=128) + The batch size for out of sample inverse transform + + Returns + ------- + transp_Xt : array-like, shape (n_source_samples, n_features) + The transported target samples. + """ + + # check the necessary inputs parameters are here + if check_params(Xt=Xt): + + if np.array_equal(self.xt_, Xt): + + # perform standard barycentric mapping + transp_ = self.coupling_.T / np.sum(self.coupling_, 0)[:, None] + + # set nans to 0 + transp_[~ np.isfinite(transp_)] = 0 + + # compute transported samples + transp_Xt = np.dot(transp_, self.xs_) + else: + # perform out of sample mapping + indices = np.arange(Xt.shape[0]) + batch_ind = [ + indices[i:i + batch_size] + for i in range(0, len(indices), batch_size)] + + transp_Xt = [] + for bi in batch_ind: + D0 = dist(Xt[bi], self.xt_) + idx = np.argmin(D0, axis=1) + + # transport the target samples + transp_ = self.coupling_.T / np.sum( + self.coupling_, 0)[:, None] + transp_[~ np.isfinite(transp_)] = 0 + transp_Xt_ = np.dot(transp_, self.xs_) + + # define the transported points + transp_Xt_ = transp_Xt_[idx, :] + Xt[bi] - self.xt_[idx, :] + + transp_Xt.append(transp_Xt_) + + transp_Xt = np.concatenate(transp_Xt, axis=0) + + return transp_Xt + + +class LinearTransport(BaseTransport): + + """ OT linear operator between empirical distributions + + The function estimates the optimal linear operator that aligns the two + empirical distributions. This is equivalent to estimating the closed + form mapping between two Gaussian distributions :math:`N(\mu_s,\Sigma_s)` + and :math:`N(\mu_t,\Sigma_t)` as proposed in [14] and discussed in + remark 2.29 in [15]. + + The linear operator from source to target :math:`M` + + .. math:: + M(x)=Ax+b + + where : + + .. math:: + A=\Sigma_s^{-1/2}(\Sigma_s^{1/2}\Sigma_t\Sigma_s^{1/2})^{1/2} + \Sigma_s^{-1/2} + .. math:: + b=\mu_t-A\mu_s + + Parameters + ---------- + reg : float,optional + regularization added to the daigonals of convariances (>0) + bias: boolean, optional + estimate bias b else b=0 (default:True) + log : bool, optional + record log if True + + References + ---------- + + .. [14] Knott, M. and Smith, C. S. "On the optimal mapping of + distributions", Journal of Optimization Theory and Applications + Vol 43, 1984 + + .. [15] Peyré, G., & Cuturi, M. (2017). "Computational Optimal + Transport", 2018. + + """ + + + def __init__(self, reg=1e-8, bias=True, log=False, + distribution_estimation=distribution_estimation_uniform): + self.bias = bias + self.log = log + self.reg = reg + self.distribution_estimation = distribution_estimation + + + def fit(self, Xs=None, ys=None, Xt=None, yt=None): + """Build a coupling matrix from source and target sets of samples + (Xs, ys) and (Xt, yt) + + Parameters + ---------- + Xs : array-like, shape (n_source_samples, n_features) + The training input samples. + ys : array-like, shape (n_source_samples,) + The class labels + Xt : array-like, shape (n_target_samples, n_features) + The training input samples. + yt : array-like, shape (n_target_samples,) + The class labels. If some target samples are unlabeled, fill the + yt's elements with -1. + + Warning: Note that, due to this convention -1 cannot be used as a + class label + + Returns + ------- + self : object + Returns self. + """ + + self.mu_s = self.distribution_estimation(Xs) + self.mu_t = self.distribution_estimation(Xt) + + # coupling estimation + returned_ = OT_mapping_linear(Xs, Xt, reg=self.reg, + ws=self.mu_s.reshape((-1, 1)), + wt=self.mu_t.reshape((-1, 1)), + bias=self.bias, log=self.log) + + # deal with the value of log + if self.log: + self.A_, self.B_, self.log_ = returned_ + else: + self.A_, self.B_, = returned_ + self.log_ = dict() + + # re compute inverse mapping + self.A1_ = linalg.inv(self.A_) + self.B1_ = -self.B_.dot(self.A1_) + + return self + + + def transform(self, Xs=None, ys=None, Xt=None, yt=None, batch_size=128): + """Transports source samples Xs onto target ones Xt + + Parameters + ---------- + Xs : array-like, shape (n_source_samples, n_features) + The training input samples. + ys : array-like, shape (n_source_samples,) + The class labels + Xt : array-like, shape (n_target_samples, n_features) + The training input samples. + yt : array-like, shape (n_target_samples,) + The class labels. If some target samples are unlabeled, fill the + yt's elements with -1. + + Warning: Note that, due to this convention -1 cannot be used as a + class label + batch_size : int, optional (default=128) + The batch size for out of sample inverse transform + + Returns + ------- + transp_Xs : array-like, shape (n_source_samples, n_features) + The transport source samples. + """ + + # check the necessary inputs parameters are here + if check_params(Xs=Xs): + transp_Xs = Xs.dot(self.A_) + self.B_ + + return transp_Xs + + + def inverse_transform(self, Xs=None, ys=None, Xt=None, yt=None, + batch_size=128): + """Transports target samples Xt onto target samples Xs + + Parameters + ---------- + Xs : array-like, shape (n_source_samples, n_features) + The training input samples. + ys : array-like, shape (n_source_samples,) + The class labels + Xt : array-like, shape (n_target_samples, n_features) + The training input samples. + yt : array-like, shape (n_target_samples,) + The class labels. If some target samples are unlabeled, fill the + yt's elements with -1. + + Warning: Note that, due to this convention -1 cannot be used as a + class label + batch_size : int, optional (default=128) + The batch size for out of sample inverse transform + + Returns + ------- + transp_Xt : array-like, shape (n_source_samples, n_features) + The transported target samples. + """ + + # check the necessary inputs parameters are here + if check_params(Xt=Xt): + transp_Xt = Xt.dot(self.A1_) + self.B1_ + + return transp_Xt + + +class SinkhornTransport(BaseTransport): + + """Domain Adapatation OT method based on Sinkhorn Algorithm + + Parameters + ---------- + reg_e : float, optional (default=1) + Entropic regularization parameter + max_iter : int, float, optional (default=1000) + The minimum number of iteration before stopping the optimization + algorithm if no it has not converged + tol : float, optional (default=10e-9) + The precision required to stop the optimization algorithm. + verbose : bool, optional (default=False) + Controls the verbosity of the optimization algorithm + log : int, optional (default=False) + Controls the logs of the optimization algorithm + metric : string, optional (default="sqeuclidean") + The ground metric for the Wasserstein problem + norm : string, optional (default=None) + If given, normalize the ground metric to avoid numerical errors that + can occur with large metric values. + distribution_estimation : callable, optional (defaults to the uniform) + The kind of distribution estimation to employ + out_of_sample_map : string, optional (default="ferradans") + The kind of out of sample mapping to apply to transport samples + from a domain into another one. Currently the only possible option is + "ferradans" which uses the method proposed in [6]. + limit_max: float, optional (defaul=np.infty) + Controls the semi supervised mode. Transport between labeled source + and target samples of different classes will exhibit an cost defined + by this variable + + Attributes + ---------- + coupling_ : array-like, shape (n_source_samples, n_target_samples) + The optimal coupling + log_ : dictionary + The dictionary of log, empty dic if parameter log is not True + + References + ---------- + .. [1] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, + "Optimal Transport for Domain Adaptation," in IEEE Transactions + on Pattern Analysis and Machine Intelligence , vol.PP, no.99, pp.1-1 + .. [2] M. Cuturi, Sinkhorn Distances : Lightspeed Computation of Optimal + Transport, Advances in Neural Information Processing Systems (NIPS) + 26, 2013 + """ + + + def __init__(self, reg_e=1., max_iter=1000, + tol=10e-9, verbose=False, log=False, + metric="sqeuclidean", norm=None, + distribution_estimation=distribution_estimation_uniform, + out_of_sample_map='ferradans', limit_max=np.infty): + self.reg_e = reg_e + self.max_iter = max_iter + self.tol = tol + self.verbose = verbose + self.log = log + self.metric = metric + self.norm = norm + self.limit_max = limit_max + self.distribution_estimation = distribution_estimation + self.out_of_sample_map = out_of_sample_map + + + def fit(self, Xs=None, ys=None, Xt=None, yt=None): + """Build a coupling matrix from source and target sets of samples + (Xs, ys) and (Xt, yt) + + Parameters + ---------- + Xs : array-like, shape (n_source_samples, n_features) + The training input samples. + ys : array-like, shape (n_source_samples,) + The class labels + Xt : array-like, shape (n_target_samples, n_features) + The training input samples. + yt : array-like, shape (n_target_samples,) + The class labels. If some target samples are unlabeled, fill the + yt's elements with -1. + + Warning: Note that, due to this convention -1 cannot be used as a + class label + + Returns + ------- + self : object + Returns self. + """ + + super(SinkhornTransport, self).fit(Xs, ys, Xt, yt) + + # coupling estimation + returned_ = sinkhorn( + a=self.mu_s, b=self.mu_t, M=self.cost_, reg=self.reg_e, + numItermax=self.max_iter, stopThr=self.tol, + verbose=self.verbose, log=self.log) + + # deal with the value of log + if self.log: + self.coupling_, self.log_ = returned_ + else: + self.coupling_ = returned_ + self.log_ = dict() + + return self + + +class EMDTransport(BaseTransport): + + """Domain Adapatation OT method based on Earth Mover's Distance + + Parameters + ---------- + metric : string, optional (default="sqeuclidean") + The ground metric for the Wasserstein problem + norm : string, optional (default=None) + If given, normalize the ground metric to avoid numerical errors that + can occur with large metric values. + log : int, optional (default=False) + Controls the logs of the optimization algorithm + distribution_estimation : callable, optional (defaults to the uniform) + The kind of distribution estimation to employ + out_of_sample_map : string, optional (default="ferradans") + The kind of out of sample mapping to apply to transport samples + from a domain into another one. Currently the only possible option is + "ferradans" which uses the method proposed in [6]. + limit_max: float, optional (default=10) + Controls the semi supervised mode. Transport between labeled source + and target samples of different classes will exhibit an infinite cost + (10 times the maximum value of the cost matrix) + max_iter : int, optional (default=100000) + The maximum number of iterations before stopping the optimization + algorithm if it has not converged. + + Attributes + ---------- + coupling_ : array-like, shape (n_source_samples, n_target_samples) + The optimal coupling + + References + ---------- + .. [1] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, + "Optimal Transport for Domain Adaptation," in IEEE Transactions + on Pattern Analysis and Machine Intelligence , vol.PP, no.99, pp.1-1 + """ + + + def __init__(self, metric="sqeuclidean", norm=None, log=False, + distribution_estimation=distribution_estimation_uniform, + out_of_sample_map='ferradans', limit_max=10, + max_iter=100000): + self.metric = metric + self.norm = norm + self.log = log + self.limit_max = limit_max + self.distribution_estimation = distribution_estimation + self.out_of_sample_map = out_of_sample_map + self.max_iter = max_iter + + + def fit(self, Xs, ys=None, Xt=None, yt=None): + """Build a coupling matrix from source and target sets of samples + (Xs, ys) and (Xt, yt) + + Parameters + ---------- + Xs : array-like, shape (n_source_samples, n_features) + The training input samples. + ys : array-like, shape (n_source_samples,) + The class labels + Xt : array-like, shape (n_target_samples, n_features) + The training input samples. + yt : array-like, shape (n_target_samples,) + The class labels. If some target samples are unlabeled, fill the + yt's elements with -1. + + Warning: Note that, due to this convention -1 cannot be used as a + class label + + Returns + ------- + self : object + Returns self. + """ + + super(EMDTransport, self).fit(Xs, ys, Xt, yt) + + returned_ = emd( + a=self.mu_s, b=self.mu_t, M=self.cost_, numItermax=self.max_iter, + log=self.log) + + # coupling estimation + if self.log: + self.coupling_, self.log_ = returned_ + else: + self.coupling_ = returned_ + self.log_ = dict() + return self + + +class SinkhornLpl1Transport(BaseTransport): + + """Domain Adapatation OT method based on sinkhorn algorithm + + LpL1 class regularization. + + Parameters + ---------- + reg_e : float, optional (default=1) + Entropic regularization parameter + reg_cl : float, optional (default=0.1) + Class regularization parameter + max_iter : int, float, optional (default=10) + The minimum number of iteration before stopping the optimization + algorithm if no it has not converged + max_inner_iter : int, float, optional (default=200) + The number of iteration in the inner loop + log : bool, optional (default=False) + Controls the logs of the optimization algorithm + tol : float, optional (default=10e-9) + Stop threshold on error (inner sinkhorn solver) (>0) + verbose : bool, optional (default=False) + Controls the verbosity of the optimization algorithm + metric : string, optional (default="sqeuclidean") + The ground metric for the Wasserstein problem + norm : string, optional (default=None) + If given, normalize the ground metric to avoid numerical errors that + can occur with large metric values. + distribution_estimation : callable, optional (defaults to the uniform) + The kind of distribution estimation to employ + out_of_sample_map : string, optional (default="ferradans") + The kind of out of sample mapping to apply to transport samples + from a domain into another one. Currently the only possible option is + "ferradans" which uses the method proposed in [6]. + limit_max: float, optional (defaul=np.infty) + Controls the semi supervised mode. Transport between labeled source + and target samples of different classes will exhibit a cost defined by + limit_max. + + Attributes + ---------- + coupling_ : array-like, shape (n_source_samples, n_target_samples) + The optimal coupling + + References + ---------- + + .. [1] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, + "Optimal Transport for Domain Adaptation," in IEEE + Transactions on Pattern Analysis and Machine Intelligence , + vol.PP, no.99, pp.1-1 + .. [2] Rakotomamonjy, A., Flamary, R., & Courty, N. (2015). + Generalized conditional gradient: analysis of convergence + and applications. arXiv preprint arXiv:1510.06567. + + """ + + + def __init__(self, reg_e=1., reg_cl=0.1, + max_iter=10, max_inner_iter=200, log=False, + tol=10e-9, verbose=False, + metric="sqeuclidean", norm=None, + distribution_estimation=distribution_estimation_uniform, + out_of_sample_map='ferradans', limit_max=np.infty): + self.reg_e = reg_e + self.reg_cl = reg_cl + self.max_iter = max_iter + self.max_inner_iter = max_inner_iter + self.tol = tol + self.log = log + self.verbose = verbose + self.metric = metric + self.norm = norm + self.distribution_estimation = distribution_estimation + self.out_of_sample_map = out_of_sample_map + self.limit_max = limit_max + + + def fit(self, Xs, ys=None, Xt=None, yt=None): + """Build a coupling matrix from source and target sets of samples + (Xs, ys) and (Xt, yt) + + Parameters + ---------- + Xs : array-like, shape (n_source_samples, n_features) + The training input samples. + ys : array-like, shape (n_source_samples,) + The class labels + Xt : array-like, shape (n_target_samples, n_features) + The training input samples. + yt : array-like, shape (n_target_samples,) + The class labels. If some target samples are unlabeled, fill the + yt's elements with -1. + + Warning: Note that, due to this convention -1 cannot be used as a + class label + + Returns + ------- + self : object + Returns self. + """ + + # check the necessary inputs parameters are here + if check_params(Xs=Xs, Xt=Xt, ys=ys): + super(SinkhornLpl1Transport, self).fit(Xs, ys, Xt, yt) + + returned_ = sinkhorn_lpl1_mm( + a=self.mu_s, labels_a=ys, b=self.mu_t, M=self.cost_, + reg=self.reg_e, eta=self.reg_cl, numItermax=self.max_iter, + numInnerItermax=self.max_inner_iter, stopInnerThr=self.tol, + verbose=self.verbose, log=self.log) + + # deal with the value of log + if self.log: + self.coupling_, self.log_ = returned_ + else: + self.coupling_ = returned_ + self.log_ = dict() + return self + + +class EMDLaplaceTransport(BaseTransport): + + """Domain Adapatation OT method based on Earth Mover's Distance with Laplacian regularization + + Parameters + ---------- + reg_lap : float, optional (default=1) + Laplacian regularization parameter + reg_src : float, optional (default=0.5) + Source relative importance in regularization + metric : string, optional (default="sqeuclidean") + The ground metric for the Wasserstein problem + norm : string, optional (default=None) + If given, normalize the ground metric to avoid numerical errors that + can occur with large metric values. + max_iter : int, optional (default=100) + Max number of BCD iterations + tol : float, optional (default=1e-5) + Stop threshold on relative loss decrease (>0) + max_inner_iter : int, optional (default=10) + Max number of iterations (inner CG solver) + inner_tol : float, optional (default=1e-6) + Stop threshold on error (inner CG solver) (>0) + log : int, optional (default=False) + Controls the logs of the optimization algorithm + distribution_estimation : callable, optional (defaults to the uniform) + The kind of distribution estimation to employ + out_of_sample_map : string, optional (default="ferradans") + The kind of out of sample mapping to apply to transport samples + from a domain into another one. Currently the only possible option is + "ferradans" which uses the method proposed in [6]. + + Attributes + ---------- + coupling_ : array-like, shape (n_source_samples, n_target_samples) + The optimal coupling + + References + ---------- + .. [1] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, + "Optimal Transport for Domain Adaptation," in IEEE Transactions + on Pattern Analysis and Machine Intelligence , vol.PP, no.99, pp.1-1 + """ + + + def __init__(self, reg_lap = 1., reg_src=1., alpha=0.5, + metric="sqeuclidean", norm=None, max_iter=100, tol=1e-5, + max_inner_iter=100000, inner_tol=1e-6, log=False, verbose=False, + distribution_estimation=distribution_estimation_uniform, + out_of_sample_map='ferradans'): + self.reg_lap = reg_lap + self.reg_src = reg_src + self.alpha = alpha + self.metric = metric + self.norm = norm + self.max_iter = max_iter + self.tol = tol + self.max_inner_iter = max_inner_iter + self.inner_tol = inner_tol + self.log = log + self.verbose = verbose + self.distribution_estimation = distribution_estimation + self.out_of_sample_map = out_of_sample_map + + + def fit(self, Xs, ys=None, Xt=None, yt=None): + """Build a coupling matrix from source and target sets of samples + (Xs, ys) and (Xt, yt) + + Parameters + ---------- + Xs : array-like, shape (n_source_samples, n_features) + The training input samples. + ys : array-like, shape (n_source_samples,) + The class labels + Xt : array-like, shape (n_target_samples, n_features) + The training input samples. + yt : array-like, shape (n_target_samples,) + The class labels. If some target samples are unlabeled, fill the + yt's elements with -1. + + Warning: Note that, due to this convention -1 cannot be used as a + class label + + Returns + ------- + self : object + Returns self. + """ + + super(EMDLaplaceTransport, self).fit(Xs, ys, Xt, yt) + + returned_ = emd_laplace(a=self.mu_s, b=self.mu_t, xs=self.xs_, + xt=self.xt_, M=self.cost_, eta=self.reg_lap, alpha=self.reg_src, + numItermax=self.max_iter, stopThr=self.tol, numInnerItermax=self.max_inner_iter, + stopInnerThr=self.inner_tol, log=self.log, verbose=self.verbose) + + # coupling estimation + if self.log: + self.coupling_, self.log_ = returned_ + else: + self.coupling_ = returned_ + self.log_ = dict() + return self + +class SinkhornLaplaceTransport(BaseTransport): + + """Domain Adapatation OT method based on entropic regularized OT with Laplacian regularization + + Parameters + ---------- + reg_e : float, optional (default=1) + Entropic regularization parameter + reg_lap : float, optional (default=1) + Laplacian regularization parameter + reg_src : float, optional (default=0.5) + Source relative importance in regularization + metric : string, optional (default="sqeuclidean") + The ground metric for the Wasserstein problem + norm : string, optional (default=None) + If given, normalize the ground metric to avoid numerical errors that + can occur with large metric values. + max_iter : int, optional (default=100) + Max number of BCD iterations + tol : float, optional (default=1e-5) + Stop threshold on relative loss decrease (>0) + max_inner_iter : int, optional (default=10) + Max number of iterations (inner CG solver) + inner_tol : float, optional (default=1e-6) + Stop threshold on error (inner CG solver) (>0) + log : int, optional (default=False) + Controls the logs of the optimization algorithm + distribution_estimation : callable, optional (defaults to the uniform) + The kind of distribution estimation to employ + out_of_sample_map : string, optional (default="ferradans") + The kind of out of sample mapping to apply to transport samples + from a domain into another one. Currently the only possible option is + "ferradans" which uses the method proposed in [6]. + + Attributes + ---------- + coupling_ : array-like, shape (n_source_samples, n_target_samples) + The optimal coupling + + References + ---------- + .. [1] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, + "Optimal Transport for Domain Adaptation," in IEEE Transactions + on Pattern Analysis and Machine Intelligence , vol.PP, no.99, pp.1-1 + """ + + + def __init__(self, reg_e=1., reg_lap=1., reg_src=0.5, + metric="sqeuclidean", norm=None, max_iter=100, tol=1e-9, + max_inner_iter=200, inner_tol=1e-6, log=False, verbose=False, + distribution_estimation=distribution_estimation_uniform, + out_of_sample_map='ferradans'): + + self.reg_e = reg_e + self.reg_lap = reg_lap + self.reg_src = reg_src + self.metric = metric + self.norm = norm + self.max_iter = max_iter + self.tol = tol + self.max_inner_iter = max_inner_iter + self.inner_tol = inner_tol + self.log = log + self.verbose = verbose + self.distribution_estimation = distribution_estimation + self.out_of_sample_map = out_of_sample_map + + + def fit(self, Xs, ys=None, Xt=None, yt=None): + """Build a coupling matrix from source and target sets of samples + (Xs, ys) and (Xt, yt) + + Parameters + ---------- + Xs : array-like, shape (n_source_samples, n_features) + The training input samples. + ys : array-like, shape (n_source_samples,) + The class labels + Xt : array-like, shape (n_target_samples, n_features) + The training input samples. + yt : array-like, shape (n_target_samples,) + The class labels. If some target samples are unlabeled, fill the + yt's elements with -1. + + Warning: Note that, due to this convention -1 cannot be used as a + class label + + Returns + ------- + self : object + Returns self. + """ + + super(SinkhornLaplaceTransport, self).fit(Xs, ys, Xt, yt) + + returned_ = sinkhorn_laplace(a=self.mu_s, b=self.mu_t, xs=self.xs_, + xt=self.xt_, M=self.cost_, reg=self.reg_e, eta=self.reg_lap, alpha=self.reg_src, + numItermax=self.max_iter, stopThr=self.tol, numInnerItermax=self.max_inner_iter, + stopInnerThr=self.inner_tol, log=self.log, verbose=self.verbose) + + # coupling estimation + if self.log: + self.coupling_, self.log_ = returned_ + else: + self.coupling_ = returned_ + self.log_ = dict() + return self + + +class SinkhornL1l2Transport(BaseTransport): + + """Domain Adapatation OT method based on sinkhorn algorithm + + l1l2 class regularization. + + Parameters + ---------- + reg_e : float, optional (default=1) + Entropic regularization parameter + reg_cl : float, optional (default=0.1) + Class regularization parameter + max_iter : int, float, optional (default=10) + The minimum number of iteration before stopping the optimization + algorithm if no it has not converged + max_inner_iter : int, float, optional (default=200) + The number of iteration in the inner loop + tol : float, optional (default=10e-9) + Stop threshold on error (inner sinkhorn solver) (>0) + verbose : bool, optional (default=False) + Controls the verbosity of the optimization algorithm + log : bool, optional (default=False) + Controls the logs of the optimization algorithm + metric : string, optional (default="sqeuclidean") + The ground metric for the Wasserstein problem + norm : string, optional (default=None) + If given, normalize the ground metric to avoid numerical errors that + can occur with large metric values. + distribution_estimation : callable, optional (defaults to the uniform) + The kind of distribution estimation to employ + out_of_sample_map : string, optional (default="ferradans") + The kind of out of sample mapping to apply to transport samples + from a domain into another one. Currently the only possible option is + "ferradans" which uses the method proposed in [6]. + limit_max: float, optional (default=10) + Controls the semi supervised mode. Transport between labeled source + and target samples of different classes will exhibit an infinite cost + (10 times the maximum value of the cost matrix) + + Attributes + ---------- + coupling_ : array-like, shape (n_source_samples, n_target_samples) + The optimal coupling + log_ : dictionary + The dictionary of log, empty dic if parameter log is not True + + References + ---------- + + .. [1] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, + "Optimal Transport for Domain Adaptation," in IEEE + Transactions on Pattern Analysis and Machine Intelligence , + vol.PP, no.99, pp.1-1 + .. [2] Rakotomamonjy, A., Flamary, R., & Courty, N. (2015). + Generalized conditional gradient: analysis of convergence + and applications. arXiv preprint arXiv:1510.06567. + + """ + + + def __init__(self, reg_e=1., reg_cl=0.1, + max_iter=10, max_inner_iter=200, + tol=10e-9, verbose=False, log=False, + metric="sqeuclidean", norm=None, + distribution_estimation=distribution_estimation_uniform, + out_of_sample_map='ferradans', limit_max=10): + self.reg_e = reg_e + self.reg_cl = reg_cl + self.max_iter = max_iter + self.max_inner_iter = max_inner_iter + self.tol = tol + self.verbose = verbose + self.log = log + self.metric = metric + self.norm = norm + self.distribution_estimation = distribution_estimation + self.out_of_sample_map = out_of_sample_map + self.limit_max = limit_max + + + def fit(self, Xs, ys=None, Xt=None, yt=None): + """Build a coupling matrix from source and target sets of samples + (Xs, ys) and (Xt, yt) + + Parameters + ---------- + Xs : array-like, shape (n_source_samples, n_features) + The training input samples. + ys : array-like, shape (n_source_samples,) + The class labels + Xt : array-like, shape (n_target_samples, n_features) + The training input samples. + yt : array-like, shape (n_target_samples,) + The class labels. If some target samples are unlabeled, fill the + yt's elements with -1. + + Warning: Note that, due to this convention -1 cannot be used as a + class label + + Returns + ------- + self : object + Returns self. + """ + + # check the necessary inputs parameters are here + if check_params(Xs=Xs, Xt=Xt, ys=ys): + + super(SinkhornL1l2Transport, self).fit(Xs, ys, Xt, yt) + + returned_ = sinkhorn_l1l2_gl( + a=self.mu_s, labels_a=ys, b=self.mu_t, M=self.cost_, + reg=self.reg_e, eta=self.reg_cl, numItermax=self.max_iter, + numInnerItermax=self.max_inner_iter, stopInnerThr=self.tol, + verbose=self.verbose, log=self.log) + + # deal with the value of log + if self.log: + self.coupling_, self.log_ = returned_ + else: + self.coupling_ = returned_ + self.log_ = dict() + + return self + + +class MappingTransport(BaseEstimator): + + """MappingTransport: DA methods that aims at jointly estimating a optimal + transport coupling and the associated mapping + + Parameters + ---------- + mu : float, optional (default=1) + Weight for the linear OT loss (>0) + eta : float, optional (default=0.001) + Regularization term for the linear mapping L (>0) + bias : bool, optional (default=False) + Estimate linear mapping with constant bias + metric : string, optional (default="sqeuclidean") + The ground metric for the Wasserstein problem + norm : string, optional (default=None) + If given, normalize the ground metric to avoid numerical errors that + can occur with large metric values. + kernel : string, optional (default="linear") + The kernel to use either linear or gaussian + sigma : float, optional (default=1) + The gaussian kernel parameter + max_iter : int, optional (default=100) + Max number of BCD iterations + tol : float, optional (default=1e-5) + Stop threshold on relative loss decrease (>0) + max_inner_iter : int, optional (default=10) + Max number of iterations (inner CG solver) + inner_tol : float, optional (default=1e-6) + Stop threshold on error (inner CG solver) (>0) + log : bool, optional (default=False) + record log if True + verbose : bool, optional (default=False) + Print information along iterations + verbose2 : bool, optional (default=False) + Print information along iterations + + Attributes + ---------- + coupling_ : array-like, shape (n_source_samples, n_target_samples) + The optimal coupling + mapping_ : array-like, shape (n_features (+ 1), n_features) + (if bias) for kernel == linear + The associated mapping + array-like, shape (n_source_samples (+ 1), n_features) + (if bias) for kernel == gaussian + log_ : dictionary + The dictionary of log, empty dic if parameter log is not True + + References + ---------- + + .. [8] M. Perrot, N. Courty, R. Flamary, A. Habrard, + "Mapping estimation for discrete optimal transport", + Neural Information Processing Systems (NIPS), 2016. + + """ + + + def __init__(self, mu=1, eta=0.001, bias=False, metric="sqeuclidean", + norm=None, kernel="linear", sigma=1, max_iter=100, tol=1e-5, + max_inner_iter=10, inner_tol=1e-6, log=False, verbose=False, + verbose2=False): + self.metric = metric + self.norm = norm + self.mu = mu + self.eta = eta + self.bias = bias + self.kernel = kernel + self.sigma = sigma + self.max_iter = max_iter + self.tol = tol + self.max_inner_iter = max_inner_iter + self.inner_tol = inner_tol + self.log = log + self.verbose = verbose + self.verbose2 = verbose2 + + + def fit(self, Xs=None, ys=None, Xt=None, yt=None): + """Builds an optimal coupling and estimates the associated mapping + from source and target sets of samples (Xs, ys) and (Xt, yt) + + Parameters + ---------- + Xs : array-like, shape (n_source_samples, n_features) + The training input samples. + ys : array-like, shape (n_source_samples,) + The class labels + Xt : array-like, shape (n_target_samples, n_features) + The training input samples. + yt : array-like, shape (n_target_samples,) + The class labels. If some target samples are unlabeled, fill the + yt's elements with -1. + + Warning: Note that, due to this convention -1 cannot be used as a + class label + + Returns + ------- + self : object + Returns self + """ + + # check the necessary inputs parameters are here + if check_params(Xs=Xs, Xt=Xt): + + self.xs_ = Xs + self.xt_ = Xt + + if self.kernel == "linear": + returned_ = joint_OT_mapping_linear( + Xs, Xt, mu=self.mu, eta=self.eta, bias=self.bias, + verbose=self.verbose, verbose2=self.verbose2, + numItermax=self.max_iter, + numInnerItermax=self.max_inner_iter, stopThr=self.tol, + stopInnerThr=self.inner_tol, log=self.log) + + elif self.kernel == "gaussian": + returned_ = joint_OT_mapping_kernel( + Xs, Xt, mu=self.mu, eta=self.eta, bias=self.bias, + sigma=self.sigma, verbose=self.verbose, + verbose2=self.verbose, numItermax=self.max_iter, + numInnerItermax=self.max_inner_iter, + stopInnerThr=self.inner_tol, stopThr=self.tol, + log=self.log) + + # deal with the value of log + if self.log: + self.coupling_, self.mapping_, self.log_ = returned_ + else: + self.coupling_, self.mapping_ = returned_ + self.log_ = dict() + + return self + + + def transform(self, Xs): + """Transports source samples Xs onto target ones Xt + + Parameters + ---------- + Xs : array-like, shape (n_source_samples, n_features) + The training input samples. + + Returns + ------- + transp_Xs : array-like, shape (n_source_samples, n_features) + The transport source samples. + """ + + # check the necessary inputs parameters are here + if check_params(Xs=Xs): + + if np.array_equal(self.xs_, Xs): + # perform standard barycentric mapping + transp = self.coupling_ / np.sum(self.coupling_, 1)[:, None] + + # set nans to 0 + transp[~ np.isfinite(transp)] = 0 + + # compute transported samples + transp_Xs = np.dot(transp, self.xt_) + else: + if self.kernel == "gaussian": + K = kernel(Xs, self.xs_, method=self.kernel, + sigma=self.sigma) + elif self.kernel == "linear": + K = Xs + if self.bias: + K = np.hstack((K, np.ones((Xs.shape[0], 1)))) + transp_Xs = K.dot(self.mapping_) + + return transp_Xs + + +class UnbalancedSinkhornTransport(BaseTransport): + + """Domain Adapatation unbalanced OT method based on sinkhorn algorithm + + Parameters + ---------- + reg_e : float, optional (default=1) + Entropic regularization parameter + reg_m : float, optional (default=0.1) + Mass regularization parameter + method : str + method used for the solver either 'sinkhorn', 'sinkhorn_stabilized' or + 'sinkhorn_epsilon_scaling', see those function for specific parameters + max_iter : int, float, optional (default=10) + The minimum number of iteration before stopping the optimization + algorithm if no it has not converged + tol : float, optional (default=10e-9) + Stop threshold on error (inner sinkhorn solver) (>0) + verbose : bool, optional (default=False) + Controls the verbosity of the optimization algorithm + log : bool, optional (default=False) + Controls the logs of the optimization algorithm + metric : string, optional (default="sqeuclidean") + The ground metric for the Wasserstein problem + norm : string, optional (default=None) + If given, normalize the ground metric to avoid numerical errors that + can occur with large metric values. + distribution_estimation : callable, optional (defaults to the uniform) + The kind of distribution estimation to employ + out_of_sample_map : string, optional (default="ferradans") + The kind of out of sample mapping to apply to transport samples + from a domain into another one. Currently the only possible option is + "ferradans" which uses the method proposed in [6]. + limit_max: float, optional (default=10) + Controls the semi supervised mode. Transport between labeled source + and target samples of different classes will exhibit an infinite cost + (10 times the maximum value of the cost matrix) + + Attributes + ---------- + coupling_ : array-like, shape (n_source_samples, n_target_samples) + The optimal coupling + log_ : dictionary + The dictionary of log, empty dic if parameter log is not True + + References + ---------- + + .. [1] Chizat, L., Peyré, G., Schmitzer, B., & Vialard, F. X. (2016). + Scaling algorithms for unbalanced transport problems. arXiv preprint + arXiv:1607.05816. + + """ + + + def __init__(self, reg_e=1., reg_m=0.1, method='sinkhorn', + max_iter=10, tol=1e-9, verbose=False, log=False, + metric="sqeuclidean", norm=None, + distribution_estimation=distribution_estimation_uniform, + out_of_sample_map='ferradans', limit_max=10): + self.reg_e = reg_e + self.reg_m = reg_m + self.method = method + self.max_iter = max_iter + self.tol = tol + self.verbose = verbose + self.log = log + self.metric = metric + self.norm = norm + self.distribution_estimation = distribution_estimation + self.out_of_sample_map = out_of_sample_map + self.limit_max = limit_max + + + def fit(self, Xs, ys=None, Xt=None, yt=None): + """Build a coupling matrix from source and target sets of samples + (Xs, ys) and (Xt, yt) + + Parameters + ---------- + Xs : array-like, shape (n_source_samples, n_features) + The training input samples. + ys : array-like, shape (n_source_samples,) + The class labels + Xt : array-like, shape (n_target_samples, n_features) + The training input samples. + yt : array-like, shape (n_target_samples,) + The class labels. If some target samples are unlabeled, fill the + yt's elements with -1. + + Warning: Note that, due to this convention -1 cannot be used as a + class label + + Returns + ------- + self : object + Returns self. + """ + + # check the necessary inputs parameters are here + if check_params(Xs=Xs, Xt=Xt): + + super(UnbalancedSinkhornTransport, self).fit(Xs, ys, Xt, yt) + + returned_ = sinkhorn_unbalanced( + a=self.mu_s, b=self.mu_t, M=self.cost_, + reg=self.reg_e, reg_m=self.reg_m, method=self.method, + numItermax=self.max_iter, stopThr=self.tol, + verbose=self.verbose, log=self.log) + + # deal with the value of log + if self.log: + self.coupling_, self.log_ = returned_ + else: + self.coupling_ = returned_ + self.log_ = dict() + + return self + + +class JCPOTTransport(BaseTransport): + + """Domain Adapatation OT method for multi-source target shift based on Wasserstein barycenter algorithm. + + Parameters + ---------- + reg_e : float, optional (default=1) + Entropic regularization parameter + max_iter : int, float, optional (default=10) + The minimum number of iteration before stopping the optimization + algorithm if no it has not converged + tol : float, optional (default=10e-9) + Stop threshold on error (inner sinkhorn solver) (>0) + verbose : bool, optional (default=False) + Controls the verbosity of the optimization algorithm + log : bool, optional (default=False) + Controls the logs of the optimization algorithm + metric : string, optional (default="sqeuclidean") + The ground metric for the Wasserstein problem + norm : string, optional (default=None) + If given, normalize the ground metric to avoid numerical errors that + can occur with large metric values. + distribution_estimation : callable, optional (defaults to the uniform) + The kind of distribution estimation to employ + out_of_sample_map : string, optional (default="ferradans") + The kind of out of sample mapping to apply to transport samples + from a domain into another one. Currently the only possible option is + "ferradans" which uses the method proposed in [6]. + + Attributes + ---------- + coupling_ : list of array-like objects, shape K x (n_source_samples, n_target_samples) + A set of optimal couplings between each source domain and the target domain + proportions_ : array-like, shape (n_classes,) + Estimated class proportions in the target domain + log_ : dictionary + The dictionary of log, empty dic if parameter log is not True + + References + ---------- + + .. [1] Ievgen Redko, Nicolas Courty, Rémi Flamary, Devis Tuia + "Optimal transport for multi-source domain adaptation under target shift", + International Conference on Artificial Intelligence and Statistics (AISTATS), + vol. 89, p.849-858, 2019. + + """ + + + def __init__(self, reg_e=.1, max_iter=10, + tol=10e-9, verbose=False, log=False, + metric="sqeuclidean", + out_of_sample_map='ferradans'): + self.reg_e = reg_e + self.max_iter = max_iter + self.tol = tol + self.verbose = verbose + self.log = log + self.metric = metric + self.out_of_sample_map = out_of_sample_map + + + def fit(self, Xs, ys=None, Xt=None, yt=None): + """Building coupling matrices from a list of source and target sets of samples + (Xs, ys) and (Xt, yt) + + Parameters + ---------- + Xs : list of K array-like objects, shape K x (nk_source_samples, n_features) + A list of the training input samples. + ys : list of K array-like objects, shape K x (nk_source_samples,) + A list of the class labels + Xt : array-like, shape (n_target_samples, n_features) + The training input samples. + yt : array-like, shape (n_target_samples,) + The class labels. If some target samples are unlabeled, fill the + yt's elements with -1. + + Warning: Note that, due to this convention -1 cannot be used as a + class label + + Returns + ------- + self : object + Returns self. + """ + + # check the necessary inputs parameters are here + if check_params(Xs=Xs, Xt=Xt, ys=ys): + + self.xs_ = Xs + self.xt_ = Xt + + returned_ = jcpot_barycenter(Xs=Xs, Ys=ys, Xt=Xt, reg=self.reg_e, + metric=self.metric, distrinumItermax=self.max_iter, stopThr=self.tol, + verbose=self.verbose, log=self.log) + + # deal with the value of log + if self.log: + self.coupling_, self.proportions_, self.log_ = returned_ + else: + self.coupling_, self.proportions_ = returned_ + self.log_ = dict() + + return self + + + def transform(self, Xs=None, ys=None, Xt=None, yt=None, batch_size=128): + """Transports source samples Xs onto target ones Xt + + Parameters + ---------- + Xs : array-like, shape (n_source_samples, n_features) + The training input samples. + ys : array-like, shape (n_source_samples,) + The class labels + Xt : array-like, shape (n_target_samples, n_features) + The training input samples. + yt : array-like, shape (n_target_samples,) + The class labels. If some target samples are unlabeled, fill the + yt's elements with -1. + + Warning: Note that, due to this convention -1 cannot be used as a + class label + batch_size : int, optional (default=128) + The batch size for out of sample inverse transform + """ + + transp_Xs = [] + + # check the necessary inputs parameters are here + if check_params(Xs=Xs): + + if all([np.allclose(x, y) for x, y in zip(self.xs_, Xs)]): + + # perform standard barycentric mapping for each source domain + + for coupling in self.coupling_: + transp = coupling / np.sum(coupling, 1)[:, None] + + # set nans to 0 + transp[~ np.isfinite(transp)] = 0 + + # compute transported samples + transp_Xs.append(np.dot(transp, self.xt_)) + else: + + # perform out of sample mapping + indices = np.arange(Xs.shape[0]) + batch_ind = [ + indices[i:i + batch_size] + for i in range(0, len(indices), batch_size)] + + transp_Xs = [] + + for bi in batch_ind: + transp_Xs_ = [] + + # get the nearest neighbor in the sources domains + xs = np.concatenate(self.xs_, axis=0) + idx = np.argmin(dist(Xs[bi], xs), axis=1) + + # transport the source samples + for coupling in self.coupling_: + transp = coupling / np.sum( + coupling, 1)[:, None] + transp[~ np.isfinite(transp)] = 0 + transp_Xs_.append(np.dot(transp, self.xt_)) + + transp_Xs_ = np.concatenate(transp_Xs_, axis=0) + + # define the transported points + transp_Xs_ = transp_Xs_[idx, :] + Xs[bi] - xs[idx, :] + transp_Xs.append(transp_Xs_) + + transp_Xs = np.concatenate(transp_Xs, axis=0) + + return transp_Xs diff --git a/test/test_da.py b/test/test_da.py index f700df9..15f4308 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -8,7 +8,6 @@ import numpy as np from numpy.testing import assert_allclose, assert_equal import ot -from ot.bregman import jcpot_barycenter from ot.datasets import make_data_classif from ot.utils import unif @@ -602,3 +601,109 @@ def test_jcpot_transport_class(): # check that the oos method is working assert_equal(transp_Xs_new.shape, Xs_new.shape) + +def test_emd_laplace_class(): + """test_emd_laplace_transport + """ + ns = 150 + nt = 200 + + Xs, ys = make_data_classif('3gauss', ns) + Xt, yt = make_data_classif('3gauss2', nt) + + otda = ot.da.EMDLaplaceTransport(reg_lap=0.01, max_iter=1000, tol=1e-9, verbose=False, log=True) + + # test its computed + otda.fit(Xs=Xs, ys=ys, Xt=Xt) + + assert hasattr(otda, "coupling_") + assert hasattr(otda, "log_") + + # test dimensions of coupling + assert_equal(otda.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) + + # test all margin constraints + mu_s = unif(ns) + mu_t = unif(nt) + + assert_allclose( + np.sum(otda.coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) + assert_allclose( + np.sum(otda.coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + + # test transform + transp_Xs = otda.transform(Xs=Xs) + [assert_equal(x.shape, y.shape) for x, y in zip(transp_Xs, Xs)] + + Xs_new, _ = make_data_classif('3gauss', ns + 1) + transp_Xs_new = otda.transform(Xs_new) + + # check that the oos method is working + assert_equal(transp_Xs_new.shape, Xs_new.shape) + + # test inverse transform + transp_Xt = otda.inverse_transform(Xt=Xt) + assert_equal(transp_Xt.shape, Xt.shape) + + Xt_new, _ = make_data_classif('3gauss2', nt + 1) + transp_Xt_new = otda.inverse_transform(Xt=Xt_new) + + # check that the oos method is working + assert_equal(transp_Xt_new.shape, Xt_new.shape) + + # test fit_transform + transp_Xs = otda.fit_transform(Xs=Xs, Xt=Xt) + assert_equal(transp_Xs.shape, Xs.shape) + +def test_sinkhorn_laplace_class(): + """test_sinkhorn_laplace_transport + """ + ns = 150 + nt = 200 + + Xs, ys = make_data_classif('3gauss', ns) + Xt, yt = make_data_classif('3gauss2', nt) + + otda = ot.da.SinkhornLaplaceTransport(reg_e = 1, reg_lap=0.01, max_iter=1000, tol=1e-9, verbose=False, log=True) + + # test its computed + otda.fit(Xs=Xs, ys=ys, Xt=Xt) + + assert hasattr(otda, "coupling_") + assert hasattr(otda, "log_") + + # test dimensions of coupling + assert_equal(otda.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) + + # test all margin constraints + mu_s = unif(ns) + mu_t = unif(nt) + + assert_allclose( + np.sum(otda.coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) + assert_allclose( + np.sum(otda.coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + + # test transform + transp_Xs = otda.transform(Xs=Xs) + [assert_equal(x.shape, y.shape) for x, y in zip(transp_Xs, Xs)] + + Xs_new, _ = make_data_classif('3gauss', ns + 1) + transp_Xs_new = otda.transform(Xs_new) + + # check that the oos method is working + assert_equal(transp_Xs_new.shape, Xs_new.shape) + + # test inverse transform + transp_Xt = otda.inverse_transform(Xt=Xt) + assert_equal(transp_Xt.shape, Xt.shape) + + Xt_new, _ = make_data_classif('3gauss2', nt + 1) + transp_Xt_new = otda.inverse_transform(Xt=Xt_new) + + # check that the oos method is working + assert_equal(transp_Xt_new.shape, Xt_new.shape) + + # test fit_transform + transp_Xs = otda.fit_transform(Xs=Xs, Xt=Xt) + assert_equal(transp_Xs.shape, Xs.shape) \ No newline at end of file -- cgit v1.2.3 From 98b68f1edc916d3802eeb24a19d0e10d855e01c6 Mon Sep 17 00:00:00 2001 From: ievred Date: Fri, 3 Apr 2020 17:29:13 +0200 Subject: autopep+remove sinkhorn+add simtype --- examples/plot_otda_laplacian.py | 38 ++---- ot/da.py | 286 +++------------------------------------- ot/utils.py | 1 + test/test_da.py | 54 +------- 4 files changed, 28 insertions(+), 351 deletions(-) (limited to 'test') diff --git a/examples/plot_otda_laplacian.py b/examples/plot_otda_laplacian.py index d9ae280..965380c 100644 --- a/examples/plot_otda_laplacian.py +++ b/examples/plot_otda_laplacian.py @@ -5,7 +5,7 @@ OT for domain adaptation ======================== This example introduces a domain adaptation in a 2D setting and OTDA -approaches with Laplacian regularization. +approache with Laplacian regularization. """ @@ -36,22 +36,17 @@ ot_emd = ot.da.EMDTransport() ot_emd.fit(Xs=Xs, Xt=Xt) # Sinkhorn Transport -ot_sinkhorn = ot.da.SinkhornTransport(reg_e=.5) +ot_sinkhorn = ot.da.SinkhornTransport(reg_e=.01) ot_sinkhorn.fit(Xs=Xs, Xt=Xt) # EMD Transport with Laplacian regularization ot_emd_laplace = ot.da.EMDLaplaceTransport(reg_lap=100, reg_src=1) ot_emd_laplace.fit(Xs=Xs, Xt=Xt) -# Sinkhorn Transport with Laplacian regularization -ot_sinkhorn_laplace = ot.da.SinkhornLaplaceTransport(reg_e=.5, reg_lap=100, reg_src=1) -ot_sinkhorn_laplace.fit(Xs=Xs, Xt=Xt) - # transport source samples onto target samples transp_Xs_emd = ot_emd.transform(Xs=Xs) transp_Xs_sinkhorn = ot_sinkhorn.transform(Xs=Xs) transp_Xs_emd_laplace = ot_emd_laplace.transform(Xs=Xs) -transp_Xs_sinkhorn_laplace = ot_sinkhorn_laplace.transform(Xs=Xs) ############################################################################## # Fig 1 : plots source and target samples @@ -80,35 +75,27 @@ pl.tight_layout() param_img = {'interpolation': 'nearest'} -n_plots = 2 - pl.figure(2, figsize=(15, 8)) -pl.subplot(2, 2*n_plots, 1) +pl.subplot(2, 3, 1) pl.imshow(ot_emd.coupling_, **param_img) pl.xticks([]) pl.yticks([]) pl.title('Optimal coupling\nEMDTransport') pl.figure(2, figsize=(15, 8)) -pl.subplot(2, 2*n_plots, 2) +pl.subplot(2, 3, 2) pl.imshow(ot_sinkhorn.coupling_, **param_img) pl.xticks([]) pl.yticks([]) pl.title('Optimal coupling\nSinkhornTransport') -pl.subplot(2, 2*n_plots, 3) +pl.subplot(2, 3, 3) pl.imshow(ot_emd_laplace.coupling_, **param_img) pl.xticks([]) pl.yticks([]) pl.title('Optimal coupling\nEMDLaplaceTransport') -pl.subplot(2, 2*n_plots, 4) -pl.imshow(ot_emd_laplace.coupling_, **param_img) -pl.xticks([]) -pl.yticks([]) -pl.title('Optimal coupling\nSinkhornLaplaceTransport') - -pl.subplot(2, 2*n_plots, 5) +pl.subplot(2, 3, 4) pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', label='Target samples', alpha=0.3) pl.scatter(transp_Xs_emd[:, 0], transp_Xs_emd[:, 1], c=ys, @@ -118,7 +105,7 @@ pl.yticks([]) pl.title('Transported samples\nEmdTransport') pl.legend(loc="lower left") -pl.subplot(2, 2*n_plots, 6) +pl.subplot(2, 3, 5) pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', label='Target samples', alpha=0.3) pl.scatter(transp_Xs_sinkhorn[:, 0], transp_Xs_sinkhorn[:, 1], c=ys, @@ -127,7 +114,7 @@ pl.xticks([]) pl.yticks([]) pl.title('Transported samples\nSinkhornTransport') -pl.subplot(2, 2*n_plots, 7) +pl.subplot(2, 3, 6) pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', label='Target samples', alpha=0.3) pl.scatter(transp_Xs_emd_laplace[:, 0], transp_Xs_emd_laplace[:, 1], c=ys, @@ -135,15 +122,6 @@ pl.scatter(transp_Xs_emd_laplace[:, 0], transp_Xs_emd_laplace[:, 1], c=ys, pl.xticks([]) pl.yticks([]) pl.title('Transported samples\nEMDLaplaceTransport') - -pl.subplot(2, 2*n_plots, 8) -pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', - label='Target samples', alpha=0.3) -pl.scatter(transp_Xs_sinkhorn_laplace[:, 0], transp_Xs_sinkhorn_laplace[:, 1], c=ys, - marker='+', label='Transp samples', s=30) -pl.xticks([]) -pl.yticks([]) -pl.title('Transported samples\nSinkhornLaplaceTransport') pl.tight_layout() pl.show() diff --git a/ot/da.py b/ot/da.py index 39e8c4c..0fdd3be 100644 --- a/ot/da.py +++ b/ot/da.py @@ -361,7 +361,7 @@ def joint_OT_mapping_linear(xs, xt, mu=1, eta=0.001, bias=False, verbose=False, def loss(L, G): """Compute full loss""" return np.sum((xs1.dot(L) - ns * G.dot(xt)) ** 2) + mu * \ - np.sum(G * M) + eta * np.sum(sel(L - I0) ** 2) + np.sum(G * M) + eta * np.sum(sel(L - I0) ** 2) def solve_L(G): """ solve L problem with fixed G (least square)""" @@ -565,7 +565,7 @@ def joint_OT_mapping_kernel(xs, xt, mu=1, eta=0.001, kerneltype='gaussian', def loss(L, G): """Compute full loss""" return np.sum((K1.dot(L) - ns * G.dot(xt)) ** 2) + mu * \ - np.sum(G * M) + eta * np.trace(L.T.dot(Kreg).dot(L)) + np.sum(G * M) + eta * np.trace(L.T.dot(Kreg).dot(L)) def solve_L_nobias(G): """ solve L problem with fixed G (least square)""" @@ -748,9 +748,9 @@ def OT_mapping_linear(xs, xt, reg=1e-6, ws=None, return A, b -def emd_laplace(a, b, xs, xt, M, eta=1., alpha=0.5, - numItermax=1000, stopThr=1e-5, numInnerItermax=1000, - stopInnerThr=1e-6, log=False, verbose=False, **kwargs): +def emd_laplace(a, b, xs, xt, M, sim, eta, alpha, + numItermax, stopThr, numInnerItermax, + stopInnerThr, log=False, verbose=False, **kwargs): r"""Solve the optimal transport problem (OT) with Laplacian regularization .. math:: @@ -825,16 +825,13 @@ def emd_laplace(a, b, xs, xt, M, eta=1., alpha=0.5, ot.optim.cg : General regularized OT """ - if 'sim' not in kwargs: - kwargs['sim'] = 'knn' - - if kwargs['sim'] == 'gauss': + if sim == 'gauss': if 'rbfparam' not in kwargs: kwargs['rbfparam'] = 1 / (2 * (np.mean(dist(xs, xs, 'sqeuclidean')) ** 2)) sS = kernel(xs, xs, method=kwargs['sim'], sigma=kwargs['rbfparam']) sT = kernel(xt, xt, method=kwargs['sim'], sigma=kwargs['rbfparam']) - elif kwargs['sim'] == 'knn': + elif sim == 'knn': if 'nn' not in kwargs: kwargs['nn'] = 5 @@ -849,131 +846,16 @@ def emd_laplace(a, b, xs, xt, M, eta=1., alpha=0.5, lT = laplacian(sT) def f(G): - return alpha*np.trace(np.dot(xt.T, np.dot(G.T, np.dot(lS, np.dot(G, xt))))) \ - + (1-alpha)*np.trace(np.dot(xs.T, np.dot(G, np.dot(lT, np.dot(G.T, xs))))) + return alpha * np.trace(np.dot(xt.T, np.dot(G.T, np.dot(lS, np.dot(G, xt))))) \ + + (1 - alpha) * np.trace(np.dot(xs.T, np.dot(G, np.dot(lT, np.dot(G.T, xs))))) def df(G): - return alpha*np.dot(lS + lS.T, np.dot(G, np.dot(xt, xt.T)))\ - +(1-alpha)*np.dot(xs, np.dot(xs.T, np.dot(G, lT + lT.T))) + return alpha * np.dot(lS + lS.T, np.dot(G, np.dot(xt, xt.T)))\ + + (1 - alpha) * np.dot(xs, np.dot(xs.T, np.dot(G, lT + lT.T))) return cg(a, b, M, reg=eta, f=f, df=df, G0=None, numItermax=numItermax, numItermaxEmd=numInnerItermax, stopThr=stopThr, stopThr2=stopInnerThr, verbose=verbose, log=log) -def sinkhorn_laplace(a, b, xs, xt, M, reg=.1, eta=1., alpha=0.5, - numItermax=1000, stopThr=1e-5, numInnerItermax=1000, - stopInnerThr=1e-6, log=False, verbose=False, **kwargs): - r"""Solve the entropic regularized optimal transport problem (OT) with Laplacian regularization - - .. math:: - \gamma = arg\min_\gamma <\gamma,M>_F + reg\Omega_e(\gamma) + eta\Omega_\alpha(\gamma) - - s.t.\ \gamma 1 = a - - \gamma^T 1= b - - \gamma\geq 0 - - where: - - - a and b are source and target weights (sum to 1) - - xs and xt are source and target samples - - M is the (ns,nt) metric cost matrix - - :math:`\Omega_e` is the entropic regularization term :math:`\Omega_e - (\gamma)=\sum_{i,j} \gamma_{i,j}\log(\gamma_{i,j})` - - :math:`\Omega_\alpha` is the Laplacian regularization term - :math:`\Omega_\alpha = (1-\alpha)/n_s^2\sum_{i,j}S^s_{i,j}\|T(\mathbf{x}^s_i)-T(\mathbf{x}^s_j)\|^2+\alpha/n_t^2\sum_{i,j}S^t_{i,j}^'\|T(\mathbf{x}^t_i)-T(\mathbf{x}^t_j)\|^2` - with :math:`S^s_{i,j}, S^t_{i,j}` denoting source and target similarity matrices and :math:`T(\cdot)` being a barycentric mapping - - The algorithm used for solving the problem is the conditional gradient algorithm as proposed in [5]. - - Parameters - ---------- - a : np.ndarray (ns,) - samples weights in the source domain - b : np.ndarray (nt,) - samples weights in the target domain - xs : np.ndarray (ns,d) - samples in the source domain - xt : np.ndarray (nt,d) - samples in the target domain - M : np.ndarray (ns,nt) - loss matrix - reg : float - Regularization term for entropic regularization >0 - eta : float - Regularization term for Laplacian regularization - alpha : float - Regularization term for source domain's importance in regularization - numItermax : int, optional - Max number of iterations - stopThr : float, optional - Stop threshold on error (inner sinkhorn solver) (>0) - numInnerItermax : int, optional - Max number of iterations (inner CG solver) - stopInnerThr : float, optional - Stop threshold on error (inner CG solver) (>0) - verbose : bool, optional - Print information along iterations - log : bool, optional - record log if True - - - Returns - ------- - gamma : (ns x nt) ndarray - Optimal transportation matrix for the given parameters - log : dict - log dictionary return only if log==True in parameters - - - References - ---------- - - .. [5] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, - "Optimal Transport for Domain Adaptation," in IEEE - Transactions on Pattern Analysis and Machine Intelligence , - vol.PP, no.99, pp.1-1 - - See Also - -------- - ot.lp.emd : Unregularized OT - ot.optim.cg : General regularized OT - - """ - if 'sim' not in kwargs: - kwargs['sim'] = 'knn' - - if kwargs['sim'] == 'gauss': - if 'rbfparam' not in kwargs: - kwargs['rbfparam'] = 1 / (2 * (np.mean(dist(xs, xs, 'sqeuclidean')) ** 2)) - sS = kernel(xs, xs, method=kwargs['sim'], sigma=kwargs['rbfparam']) - sT = kernel(xt, xt, method=kwargs['sim'], sigma=kwargs['rbfparam']) - - elif kwargs['sim'] == 'knn': - if 'nn' not in kwargs: - kwargs['nn'] = 5 - - from sklearn.neighbors import kneighbors_graph - - sS = kneighbors_graph(xs, kwargs['nn']).toarray() - sS = (sS + sS.T) / 2 - sT = kneighbors_graph(xt, kwargs['nn']).toarray() - sT = (sT + sT.T) / 2 - - lS = laplacian(sS) - lT = laplacian(sT) - - def f(G): - return alpha*np.trace(np.dot(xt.T, np.dot(G.T, np.dot(lS, np.dot(G, xt))))) \ - + (1-alpha)*np.trace(np.dot(xs.T, np.dot(G, np.dot(lT, np.dot(G.T, xs))))) - - def df(G): - return alpha*np.dot(lS + lS.T, np.dot(G, np.dot(xt, xt.T)))\ - +(1-alpha)*np.dot(xs, np.dot(xs.T, np.dot(G, lT + lT.T))) - - return gcg(a, b, M, reg, eta, f, df, G0=None, numItermax=numItermax, stopThr=stopThr, - numInnerItermax=numInnerItermax, stopThr2=stopInnerThr, - verbose=verbose, log=log) def distribution_estimation_uniform(X): """estimates a uniform distribution from an array of samples X @@ -989,7 +871,6 @@ def distribution_estimation_uniform(X): The uniform distribution estimated from X """ - return unif(X.shape[0]) @@ -1016,7 +897,6 @@ class BaseTransport(BaseEstimator): inverse_transform method should always get as input a Xt parameter """ - def fit(self, Xs=None, ys=None, Xt=None, yt=None): """Build a coupling matrix from source and target sets of samples (Xs, ys) and (Xt, yt) @@ -1077,7 +957,6 @@ class BaseTransport(BaseEstimator): return self - def fit_transform(self, Xs=None, ys=None, Xt=None, yt=None): """Build a coupling matrix from source and target sets of samples (Xs, ys) and (Xt, yt) and transports source samples Xs onto target @@ -1106,7 +985,6 @@ class BaseTransport(BaseEstimator): return self.fit(Xs, ys, Xt, yt).transform(Xs, ys, Xt, yt) - def transform(self, Xs=None, ys=None, Xt=None, yt=None, batch_size=128): """Transports source samples Xs onto target ones Xt @@ -1174,7 +1052,6 @@ class BaseTransport(BaseEstimator): return transp_Xs - def inverse_transform(self, Xs=None, ys=None, Xt=None, yt=None, batch_size=128): """Transports target samples Xt onto target samples Xs @@ -1287,7 +1164,6 @@ class LinearTransport(BaseTransport): """ - def __init__(self, reg=1e-8, bias=True, log=False, distribution_estimation=distribution_estimation_uniform): self.bias = bias @@ -1295,7 +1171,6 @@ class LinearTransport(BaseTransport): self.reg = reg self.distribution_estimation = distribution_estimation - def fit(self, Xs=None, ys=None, Xt=None, yt=None): """Build a coupling matrix from source and target sets of samples (Xs, ys) and (Xt, yt) @@ -1343,7 +1218,6 @@ class LinearTransport(BaseTransport): return self - def transform(self, Xs=None, ys=None, Xt=None, yt=None, batch_size=128): """Transports source samples Xs onto target ones Xt @@ -1376,7 +1250,6 @@ class LinearTransport(BaseTransport): return transp_Xs - def inverse_transform(self, Xs=None, ys=None, Xt=None, yt=None, batch_size=128): """Transports target samples Xt onto target samples Xs @@ -1461,7 +1334,6 @@ class SinkhornTransport(BaseTransport): 26, 2013 """ - def __init__(self, reg_e=1., max_iter=1000, tol=10e-9, verbose=False, log=False, metric="sqeuclidean", norm=None, @@ -1478,7 +1350,6 @@ class SinkhornTransport(BaseTransport): self.distribution_estimation = distribution_estimation self.out_of_sample_map = out_of_sample_map - def fit(self, Xs=None, ys=None, Xt=None, yt=None): """Build a coupling matrix from source and target sets of samples (Xs, ys) and (Xt, yt) @@ -1561,7 +1432,6 @@ class EMDTransport(BaseTransport): on Pattern Analysis and Machine Intelligence , vol.PP, no.99, pp.1-1 """ - def __init__(self, metric="sqeuclidean", norm=None, log=False, distribution_estimation=distribution_estimation_uniform, out_of_sample_map='ferradans', limit_max=10, @@ -1574,7 +1444,6 @@ class EMDTransport(BaseTransport): self.out_of_sample_map = out_of_sample_map self.max_iter = max_iter - def fit(self, Xs, ys=None, Xt=None, yt=None): """Build a coupling matrix from source and target sets of samples (Xs, ys) and (Xt, yt) @@ -1671,7 +1540,6 @@ class SinkhornLpl1Transport(BaseTransport): """ - def __init__(self, reg_e=1., reg_cl=0.1, max_iter=10, max_inner_iter=200, log=False, tol=10e-9, verbose=False, @@ -1691,7 +1559,6 @@ class SinkhornLpl1Transport(BaseTransport): self.out_of_sample_map = out_of_sample_map self.limit_max = limit_max - def fit(self, Xs, ys=None, Xt=None, yt=None): """Build a coupling matrix from source and target sets of samples (Xs, ys) and (Xt, yt) @@ -1751,6 +1618,8 @@ class EMDLaplaceTransport(BaseTransport): norm : string, optional (default=None) If given, normalize the ground metric to avoid numerical errors that can occur with large metric values. + similarity : string, optional (default="knn") + The similarity to use either knn or gaussian max_iter : int, optional (default=100) Max number of BCD iterations tol : float, optional (default=1e-5) @@ -1780,10 +1649,9 @@ class EMDLaplaceTransport(BaseTransport): on Pattern Analysis and Machine Intelligence , vol.PP, no.99, pp.1-1 """ - - def __init__(self, reg_lap = 1., reg_src=1., alpha=0.5, - metric="sqeuclidean", norm=None, max_iter=100, tol=1e-5, - max_inner_iter=100000, inner_tol=1e-6, log=False, verbose=False, + def __init__(self, reg_lap=1., reg_src=1., alpha=0.5, + metric="sqeuclidean", norm=None, similarity="knn", max_iter=100, tol=1e-9, + max_inner_iter=100000, inner_tol=1e-9, log=False, verbose=False, distribution_estimation=distribution_estimation_uniform, out_of_sample_map='ferradans'): self.reg_lap = reg_lap @@ -1791,6 +1659,7 @@ class EMDLaplaceTransport(BaseTransport): self.alpha = alpha self.metric = metric self.norm = norm + self.similarity = similarity self.max_iter = max_iter self.tol = tol self.max_inner_iter = max_inner_iter @@ -1800,7 +1669,6 @@ class EMDLaplaceTransport(BaseTransport): self.distribution_estimation = distribution_estimation self.out_of_sample_map = out_of_sample_map - def fit(self, Xs, ys=None, Xt=None, yt=None): """Build a coupling matrix from source and target sets of samples (Xs, ys) and (Xt, yt) @@ -1829,115 +1697,7 @@ class EMDLaplaceTransport(BaseTransport): super(EMDLaplaceTransport, self).fit(Xs, ys, Xt, yt) returned_ = emd_laplace(a=self.mu_s, b=self.mu_t, xs=self.xs_, - xt=self.xt_, M=self.cost_, eta=self.reg_lap, alpha=self.reg_src, - numItermax=self.max_iter, stopThr=self.tol, numInnerItermax=self.max_inner_iter, - stopInnerThr=self.inner_tol, log=self.log, verbose=self.verbose) - - # coupling estimation - if self.log: - self.coupling_, self.log_ = returned_ - else: - self.coupling_ = returned_ - self.log_ = dict() - return self - -class SinkhornLaplaceTransport(BaseTransport): - - """Domain Adapatation OT method based on entropic regularized OT with Laplacian regularization - - Parameters - ---------- - reg_e : float, optional (default=1) - Entropic regularization parameter - reg_lap : float, optional (default=1) - Laplacian regularization parameter - reg_src : float, optional (default=0.5) - Source relative importance in regularization - metric : string, optional (default="sqeuclidean") - The ground metric for the Wasserstein problem - norm : string, optional (default=None) - If given, normalize the ground metric to avoid numerical errors that - can occur with large metric values. - max_iter : int, optional (default=100) - Max number of BCD iterations - tol : float, optional (default=1e-5) - Stop threshold on relative loss decrease (>0) - max_inner_iter : int, optional (default=10) - Max number of iterations (inner CG solver) - inner_tol : float, optional (default=1e-6) - Stop threshold on error (inner CG solver) (>0) - log : int, optional (default=False) - Controls the logs of the optimization algorithm - distribution_estimation : callable, optional (defaults to the uniform) - The kind of distribution estimation to employ - out_of_sample_map : string, optional (default="ferradans") - The kind of out of sample mapping to apply to transport samples - from a domain into another one. Currently the only possible option is - "ferradans" which uses the method proposed in [6]. - - Attributes - ---------- - coupling_ : array-like, shape (n_source_samples, n_target_samples) - The optimal coupling - - References - ---------- - .. [1] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, - "Optimal Transport for Domain Adaptation," in IEEE Transactions - on Pattern Analysis and Machine Intelligence , vol.PP, no.99, pp.1-1 - """ - - - def __init__(self, reg_e=1., reg_lap=1., reg_src=0.5, - metric="sqeuclidean", norm=None, max_iter=100, tol=1e-9, - max_inner_iter=200, inner_tol=1e-6, log=False, verbose=False, - distribution_estimation=distribution_estimation_uniform, - out_of_sample_map='ferradans'): - - self.reg_e = reg_e - self.reg_lap = reg_lap - self.reg_src = reg_src - self.metric = metric - self.norm = norm - self.max_iter = max_iter - self.tol = tol - self.max_inner_iter = max_inner_iter - self.inner_tol = inner_tol - self.log = log - self.verbose = verbose - self.distribution_estimation = distribution_estimation - self.out_of_sample_map = out_of_sample_map - - - def fit(self, Xs, ys=None, Xt=None, yt=None): - """Build a coupling matrix from source and target sets of samples - (Xs, ys) and (Xt, yt) - - Parameters - ---------- - Xs : array-like, shape (n_source_samples, n_features) - The training input samples. - ys : array-like, shape (n_source_samples,) - The class labels - Xt : array-like, shape (n_target_samples, n_features) - The training input samples. - yt : array-like, shape (n_target_samples,) - The class labels. If some target samples are unlabeled, fill the - yt's elements with -1. - - Warning: Note that, due to this convention -1 cannot be used as a - class label - - Returns - ------- - self : object - Returns self. - """ - - super(SinkhornLaplaceTransport, self).fit(Xs, ys, Xt, yt) - - returned_ = sinkhorn_laplace(a=self.mu_s, b=self.mu_t, xs=self.xs_, - xt=self.xt_, M=self.cost_, reg=self.reg_e, eta=self.reg_lap, alpha=self.reg_src, + xt=self.xt_, M=self.cost_, sim=self.similarity, eta=self.reg_lap, alpha=self.reg_src, numItermax=self.max_iter, stopThr=self.tol, numInnerItermax=self.max_inner_iter, stopInnerThr=self.inner_tol, log=self.log, verbose=self.verbose) @@ -2008,7 +1768,6 @@ class SinkhornL1l2Transport(BaseTransport): """ - def __init__(self, reg_e=1., reg_cl=0.1, max_iter=10, max_inner_iter=200, tol=10e-9, verbose=False, log=False, @@ -2028,7 +1787,6 @@ class SinkhornL1l2Transport(BaseTransport): self.out_of_sample_map = out_of_sample_map self.limit_max = limit_max - def fit(self, Xs, ys=None, Xt=None, yt=None): """Build a coupling matrix from source and target sets of samples (Xs, ys) and (Xt, yt) @@ -2133,7 +1891,6 @@ class MappingTransport(BaseEstimator): """ - def __init__(self, mu=1, eta=0.001, bias=False, metric="sqeuclidean", norm=None, kernel="linear", sigma=1, max_iter=100, tol=1e-5, max_inner_iter=10, inner_tol=1e-6, log=False, verbose=False, @@ -2153,7 +1910,6 @@ class MappingTransport(BaseEstimator): self.verbose = verbose self.verbose2 = verbose2 - def fit(self, Xs=None, ys=None, Xt=None, yt=None): """Builds an optimal coupling and estimates the associated mapping from source and target sets of samples (Xs, ys) and (Xt, yt) @@ -2211,7 +1967,6 @@ class MappingTransport(BaseEstimator): return self - def transform(self, Xs): """Transports source samples Xs onto target ones Xt @@ -2305,7 +2060,6 @@ class UnbalancedSinkhornTransport(BaseTransport): """ - def __init__(self, reg_e=1., reg_m=0.1, method='sinkhorn', max_iter=10, tol=1e-9, verbose=False, log=False, metric="sqeuclidean", norm=None, @@ -2324,7 +2078,6 @@ class UnbalancedSinkhornTransport(BaseTransport): self.out_of_sample_map = out_of_sample_map self.limit_max = limit_max - def fit(self, Xs, ys=None, Xt=None, yt=None): """Build a coupling matrix from source and target sets of samples (Xs, ys) and (Xt, yt) @@ -2419,7 +2172,6 @@ class JCPOTTransport(BaseTransport): """ - def __init__(self, reg_e=.1, max_iter=10, tol=10e-9, verbose=False, log=False, metric="sqeuclidean", @@ -2432,7 +2184,6 @@ class JCPOTTransport(BaseTransport): self.metric = metric self.out_of_sample_map = out_of_sample_map - def fit(self, Xs, ys=None, Xt=None, yt=None): """Building coupling matrices from a list of source and target sets of samples (Xs, ys) and (Xt, yt) @@ -2477,7 +2228,6 @@ class JCPOTTransport(BaseTransport): return self - def transform(self, Xs=None, ys=None, Xt=None, yt=None, batch_size=128): """Transports source samples Xs onto target ones Xt diff --git a/ot/utils.py b/ot/utils.py index b8a6f44..a633be2 100644 --- a/ot/utils.py +++ b/ot/utils.py @@ -48,6 +48,7 @@ def kernel(x1, x2, method='gaussian', sigma=1, **kwargs): K = np.exp(-dist(x1, x2) / (2 * sigma**2)) return K + def laplacian(x): """Compute Laplacian matrix""" L = np.diag(np.sum(x, axis=0)) - x diff --git a/test/test_da.py b/test/test_da.py index 15f4308..372ebd4 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -602,6 +602,7 @@ def test_jcpot_transport_class(): # check that the oos method is working assert_equal(transp_Xs_new.shape, Xs_new.shape) + def test_emd_laplace_class(): """test_emd_laplace_transport """ @@ -654,56 +655,3 @@ def test_emd_laplace_class(): # test fit_transform transp_Xs = otda.fit_transform(Xs=Xs, Xt=Xt) assert_equal(transp_Xs.shape, Xs.shape) - -def test_sinkhorn_laplace_class(): - """test_sinkhorn_laplace_transport - """ - ns = 150 - nt = 200 - - Xs, ys = make_data_classif('3gauss', ns) - Xt, yt = make_data_classif('3gauss2', nt) - - otda = ot.da.SinkhornLaplaceTransport(reg_e = 1, reg_lap=0.01, max_iter=1000, tol=1e-9, verbose=False, log=True) - - # test its computed - otda.fit(Xs=Xs, ys=ys, Xt=Xt) - - assert hasattr(otda, "coupling_") - assert hasattr(otda, "log_") - - # test dimensions of coupling - assert_equal(otda.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) - - # test all margin constraints - mu_s = unif(ns) - mu_t = unif(nt) - - assert_allclose( - np.sum(otda.coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) - assert_allclose( - np.sum(otda.coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) - - # test transform - transp_Xs = otda.transform(Xs=Xs) - [assert_equal(x.shape, y.shape) for x, y in zip(transp_Xs, Xs)] - - Xs_new, _ = make_data_classif('3gauss', ns + 1) - transp_Xs_new = otda.transform(Xs_new) - - # check that the oos method is working - assert_equal(transp_Xs_new.shape, Xs_new.shape) - - # test inverse transform - transp_Xt = otda.inverse_transform(Xt=Xt) - assert_equal(transp_Xt.shape, Xt.shape) - - Xt_new, _ = make_data_classif('3gauss2', nt + 1) - transp_Xt_new = otda.inverse_transform(Xt=Xt_new) - - # check that the oos method is working - assert_equal(transp_Xt_new.shape, Xt_new.shape) - - # test fit_transform - transp_Xs = otda.fit_transform(Xs=Xs, Xt=Xt) - assert_equal(transp_Xs.shape, Xs.shape) \ No newline at end of file -- cgit v1.2.3 From 2c9f992157844d6253a302905417e86580ac6b12 Mon Sep 17 00:00:00 2001 From: ievred Date: Tue, 7 Apr 2020 13:50:11 +0200 Subject: upd --- examples/plot_otda_classes.py | 1 - examples/plot_otda_jcpot.py | 16 ++++++++-------- ot/bregman.py | 2 +- test/test_da.py | 2 +- 4 files changed, 10 insertions(+), 11 deletions(-) (limited to 'test') diff --git a/examples/plot_otda_classes.py b/examples/plot_otda_classes.py index c311fbd..f028022 100644 --- a/examples/plot_otda_classes.py +++ b/examples/plot_otda_classes.py @@ -17,7 +17,6 @@ approaches currently supported in POT. import matplotlib.pylab as pl import ot - ############################################################################## # Generate data # ------------- diff --git a/examples/plot_otda_jcpot.py b/examples/plot_otda_jcpot.py index ce6b88f..316fa8b 100644 --- a/examples/plot_otda_jcpot.py +++ b/examples/plot_otda_jcpot.py @@ -118,16 +118,16 @@ pl.axis('off') otda = ot.da.JCPOTTransport(reg_e=1e-2, max_iter=1000, metric='sqeuclidean', tol=1e-9, verbose=True, log=True) otda.fit(all_Xr, all_Yr, xt) -ws1 = otda.proportions_.dot(otda.log_['all_domains'][0]['D2']) -ws2 = otda.proportions_.dot(otda.log_['all_domains'][1]['D2']) +ws1 = otda.proportions_.dot(otda.log_['D2'][0]) +ws2 = otda.proportions_.dot(otda.log_['D2'][1]) pl.figure(3) pl.clf() plot_ax(dec1, 'Source 1') plot_ax(dec2, 'Source 2') plot_ax(dect, 'Target') -print_G(ot.bregman.sinkhorn(ws1, [], otda.log_['all_domains'][0]['M'], reg=1e-2), xs1, ys1, xt) -print_G(ot.bregman.sinkhorn(ws2, [], otda.log_['all_domains'][1]['M'], reg=1e-2), xs2, ys2, xt) +print_G(ot.bregman.sinkhorn(ws1, [], otda.log_['M'][0], reg=1e-2), xs1, ys1, xt) +print_G(ot.bregman.sinkhorn(ws2, [], otda.log_['M'][1], reg=1e-2), xs2, ys2, xt) pl.scatter(xs1[:, 0], xs1[:, 1], c=ys1, s=35, marker='x', cmap='Set1', vmax=9) pl.scatter(xs2[:, 0], xs2[:, 1], c=ys2, s=35, marker='+', cmap='Set1', vmax=9) pl.scatter(xt[:, 0], xt[:, 1], c=yt, s=35, marker='o', cmap='Set1', vmax=9) @@ -146,16 +146,16 @@ pl.axis('off') # ---------------------------------------------------------------------------- h_res = np.array([1 - pt, pt]) -ws1 = h_res.dot(otda.log_['all_domains'][0]['D2']) -ws2 = h_res.dot(otda.log_['all_domains'][1]['D2']) +ws1 = h_res.dot(otda.log_['D2'][0]) +ws2 = h_res.dot(otda.log_['D2'][1]) pl.figure(4) pl.clf() plot_ax(dec1, 'Source 1') plot_ax(dec2, 'Source 2') plot_ax(dect, 'Target') -print_G(ot.bregman.sinkhorn(ws1, [], otda.log_['all_domains'][0]['M'], reg=1e-2), xs1, ys1, xt) -print_G(ot.bregman.sinkhorn(ws2, [], otda.log_['all_domains'][1]['M'], reg=1e-2), xs2, ys2, xt) +print_G(ot.bregman.sinkhorn(ws1, [], otda.log_['M'][0], reg=1e-2), xs1, ys1, xt) +print_G(ot.bregman.sinkhorn(ws2, [], otda.log_['M'][1], reg=1e-2), xs2, ys2, xt) pl.scatter(xs1[:, 0], xs1[:, 1], c=ys1, s=35, marker='x', cmap='Set1', vmax=9) pl.scatter(xs2[:, 0], xs2[:, 1], c=ys2, s=35, marker='+', cmap='Set1', vmax=9) pl.scatter(xt[:, 0], xt[:, 1], c=yt, s=35, marker='o', cmap='Set1', vmax=9) diff --git a/ot/bregman.py b/ot/bregman.py index ec81924..61dfa52 100644 --- a/ot/bregman.py +++ b/ot/bregman.py @@ -1608,7 +1608,7 @@ def jcpot_barycenter(Xs, Ys, Xt, reg, metric='sqeuclidean', numItermax=100, # build the cost matrix and the Gibbs kernel Mtmp = dist(Xs[d], Xt, metric=metric) Mtmp = Mtmp / np.median(Mtmp) - M.append(M) + M.append(Mtmp) Ktmp = np.empty(Mtmp.shape, dtype=Mtmp.dtype) np.divide(Mtmp, -reg, out=Ktmp) diff --git a/test/test_da.py b/test/test_da.py index 372ebd4..4eaf193 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -589,7 +589,7 @@ def test_jcpot_transport_class(): # test margin constraints w.r.t. modified source weights for each source domain assert_allclose( - np.dot(otda.log_['all_domains'][i]['D1'], np.sum(otda.coupling_[i], axis=1)), otda.proportions_, rtol=1e-3, + np.dot(otda.log_['D1'][i], np.sum(otda.coupling_[i], axis=1)), otda.proportions_, rtol=1e-3, atol=1e-3) # test transform -- cgit v1.2.3 From c68b52d1623683e86555484bf9a4875a66957bb6 Mon Sep 17 00:00:00 2001 From: ievred Date: Wed, 8 Apr 2020 10:08:47 +0200 Subject: remove laplace from jcpot --- examples/plot_otda_jcpot.py | 10 +- examples/plot_otda_laplacian.py | 127 ----------------------- ot/bregman.py | 1 - ot/da.py | 216 ---------------------------------------- test/test_da.py | 54 ---------- 5 files changed, 5 insertions(+), 403 deletions(-) delete mode 100644 examples/plot_otda_laplacian.py (limited to 'test') diff --git a/examples/plot_otda_jcpot.py b/examples/plot_otda_jcpot.py index 316fa8b..c495690 100644 --- a/examples/plot_otda_jcpot.py +++ b/examples/plot_otda_jcpot.py @@ -115,7 +115,7 @@ pl.axis('off') ############################################################################## # Instantiate JCPOT adaptation algorithm and fit it # ---------------------------------------------------------------------------- -otda = ot.da.JCPOTTransport(reg_e=1e-2, max_iter=1000, metric='sqeuclidean', tol=1e-9, verbose=True, log=True) +otda = ot.da.JCPOTTransport(reg_e=1, max_iter=1000, metric='sqeuclidean', tol=1e-9, verbose=True, log=True) otda.fit(all_Xr, all_Yr, xt) ws1 = otda.proportions_.dot(otda.log_['D2'][0]) @@ -126,8 +126,8 @@ pl.clf() plot_ax(dec1, 'Source 1') plot_ax(dec2, 'Source 2') plot_ax(dect, 'Target') -print_G(ot.bregman.sinkhorn(ws1, [], otda.log_['M'][0], reg=1e-2), xs1, ys1, xt) -print_G(ot.bregman.sinkhorn(ws2, [], otda.log_['M'][1], reg=1e-2), xs2, ys2, xt) +print_G(ot.bregman.sinkhorn(ws1, [], otda.log_['M'][0], reg=1e-1), xs1, ys1, xt) +print_G(ot.bregman.sinkhorn(ws2, [], otda.log_['M'][1], reg=1e-1), xs2, ys2, xt) pl.scatter(xs1[:, 0], xs1[:, 1], c=ys1, s=35, marker='x', cmap='Set1', vmax=9) pl.scatter(xs2[:, 0], xs2[:, 1], c=ys2, s=35, marker='+', cmap='Set1', vmax=9) pl.scatter(xt[:, 0], xt[:, 1], c=yt, s=35, marker='o', cmap='Set1', vmax=9) @@ -154,8 +154,8 @@ pl.clf() plot_ax(dec1, 'Source 1') plot_ax(dec2, 'Source 2') plot_ax(dect, 'Target') -print_G(ot.bregman.sinkhorn(ws1, [], otda.log_['M'][0], reg=1e-2), xs1, ys1, xt) -print_G(ot.bregman.sinkhorn(ws2, [], otda.log_['M'][1], reg=1e-2), xs2, ys2, xt) +print_G(ot.bregman.sinkhorn(ws1, [], otda.log_['M'][0], reg=1e-1), xs1, ys1, xt) +print_G(ot.bregman.sinkhorn(ws2, [], otda.log_['M'][1], reg=1e-1), xs2, ys2, xt) pl.scatter(xs1[:, 0], xs1[:, 1], c=ys1, s=35, marker='x', cmap='Set1', vmax=9) pl.scatter(xs2[:, 0], xs2[:, 1], c=ys2, s=35, marker='+', cmap='Set1', vmax=9) pl.scatter(xt[:, 0], xt[:, 1], c=yt, s=35, marker='o', cmap='Set1', vmax=9) diff --git a/examples/plot_otda_laplacian.py b/examples/plot_otda_laplacian.py deleted file mode 100644 index 965380c..0000000 --- a/examples/plot_otda_laplacian.py +++ /dev/null @@ -1,127 +0,0 @@ -# -*- coding: utf-8 -*- -""" -======================== -OT for domain adaptation -======================== - -This example introduces a domain adaptation in a 2D setting and OTDA -approache with Laplacian regularization. - -""" - -# Authors: Ievgen Redko - -# License: MIT License - -import matplotlib.pylab as pl -import ot - -############################################################################## -# Generate data -# ------------- - -n_source_samples = 150 -n_target_samples = 150 - -Xs, ys = ot.datasets.make_data_classif('3gauss', n_source_samples) -Xt, yt = ot.datasets.make_data_classif('3gauss2', n_target_samples) - - -############################################################################## -# Instantiate the different transport algorithms and fit them -# ----------------------------------------------------------- - -# EMD Transport -ot_emd = ot.da.EMDTransport() -ot_emd.fit(Xs=Xs, Xt=Xt) - -# Sinkhorn Transport -ot_sinkhorn = ot.da.SinkhornTransport(reg_e=.01) -ot_sinkhorn.fit(Xs=Xs, Xt=Xt) - -# EMD Transport with Laplacian regularization -ot_emd_laplace = ot.da.EMDLaplaceTransport(reg_lap=100, reg_src=1) -ot_emd_laplace.fit(Xs=Xs, Xt=Xt) - -# transport source samples onto target samples -transp_Xs_emd = ot_emd.transform(Xs=Xs) -transp_Xs_sinkhorn = ot_sinkhorn.transform(Xs=Xs) -transp_Xs_emd_laplace = ot_emd_laplace.transform(Xs=Xs) - -############################################################################## -# Fig 1 : plots source and target samples -# --------------------------------------- - -pl.figure(1, figsize=(10, 5)) -pl.subplot(1, 2, 1) -pl.scatter(Xs[:, 0], Xs[:, 1], c=ys, marker='+', label='Source samples') -pl.xticks([]) -pl.yticks([]) -pl.legend(loc=0) -pl.title('Source samples') - -pl.subplot(1, 2, 2) -pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', label='Target samples') -pl.xticks([]) -pl.yticks([]) -pl.legend(loc=0) -pl.title('Target samples') -pl.tight_layout() - - -############################################################################## -# Fig 2 : plot optimal couplings and transported samples -# ------------------------------------------------------ - -param_img = {'interpolation': 'nearest'} - -pl.figure(2, figsize=(15, 8)) -pl.subplot(2, 3, 1) -pl.imshow(ot_emd.coupling_, **param_img) -pl.xticks([]) -pl.yticks([]) -pl.title('Optimal coupling\nEMDTransport') - -pl.figure(2, figsize=(15, 8)) -pl.subplot(2, 3, 2) -pl.imshow(ot_sinkhorn.coupling_, **param_img) -pl.xticks([]) -pl.yticks([]) -pl.title('Optimal coupling\nSinkhornTransport') - -pl.subplot(2, 3, 3) -pl.imshow(ot_emd_laplace.coupling_, **param_img) -pl.xticks([]) -pl.yticks([]) -pl.title('Optimal coupling\nEMDLaplaceTransport') - -pl.subplot(2, 3, 4) -pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', - label='Target samples', alpha=0.3) -pl.scatter(transp_Xs_emd[:, 0], transp_Xs_emd[:, 1], c=ys, - marker='+', label='Transp samples', s=30) -pl.xticks([]) -pl.yticks([]) -pl.title('Transported samples\nEmdTransport') -pl.legend(loc="lower left") - -pl.subplot(2, 3, 5) -pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', - label='Target samples', alpha=0.3) -pl.scatter(transp_Xs_sinkhorn[:, 0], transp_Xs_sinkhorn[:, 1], c=ys, - marker='+', label='Transp samples', s=30) -pl.xticks([]) -pl.yticks([]) -pl.title('Transported samples\nSinkhornTransport') - -pl.subplot(2, 3, 6) -pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', - label='Target samples', alpha=0.3) -pl.scatter(transp_Xs_emd_laplace[:, 0], transp_Xs_emd_laplace[:, 1], c=ys, - marker='+', label='Transp samples', s=30) -pl.xticks([]) -pl.yticks([]) -pl.title('Transported samples\nEMDLaplaceTransport') -pl.tight_layout() - -pl.show() diff --git a/ot/bregman.py b/ot/bregman.py index 61dfa52..410ae85 100644 --- a/ot/bregman.py +++ b/ot/bregman.py @@ -1607,7 +1607,6 @@ def jcpot_barycenter(Xs, Ys, Xt, reg, metric='sqeuclidean', numItermax=100, # build the cost matrix and the Gibbs kernel Mtmp = dist(Xs[d], Xt, metric=metric) - Mtmp = Mtmp / np.median(Mtmp) M.append(Mtmp) Ktmp = np.empty(Mtmp.shape, dtype=Mtmp.dtype) diff --git a/ot/da.py b/ot/da.py index 0fdd3be..90e9e92 100644 --- a/ot/da.py +++ b/ot/da.py @@ -748,115 +748,6 @@ def OT_mapping_linear(xs, xt, reg=1e-6, ws=None, return A, b -def emd_laplace(a, b, xs, xt, M, sim, eta, alpha, - numItermax, stopThr, numInnerItermax, - stopInnerThr, log=False, verbose=False, **kwargs): - r"""Solve the optimal transport problem (OT) with Laplacian regularization - - .. math:: - \gamma = arg\min_\gamma <\gamma,M>_F + eta\Omega_\alpha(\gamma) - - s.t.\ \gamma 1 = a - - \gamma^T 1= b - - \gamma\geq 0 - - where: - - - a and b are source and target weights (sum to 1) - - xs and xt are source and target samples - - M is the (ns,nt) metric cost matrix - - :math:`\Omega_\alpha` is the Laplacian regularization term - :math:`\Omega_\alpha = (1-\alpha)/n_s^2\sum_{i,j}S^s_{i,j}\|T(\mathbf{x}^s_i)-T(\mathbf{x}^s_j)\|^2+\alpha/n_t^2\sum_{i,j}S^t_{i,j}^'\|T(\mathbf{x}^t_i)-T(\mathbf{x}^t_j)\|^2` - with :math:`S^s_{i,j}, S^t_{i,j}` denoting source and target similarity matrices and :math:`T(\cdot)` being a barycentric mapping - - The algorithm used for solving the problem is the conditional gradient algorithm as proposed in [5]. - - Parameters - ---------- - a : np.ndarray (ns,) - samples weights in the source domain - b : np.ndarray (nt,) - samples weights in the target domain - xs : np.ndarray (ns,d) - samples in the source domain - xt : np.ndarray (nt,d) - samples in the target domain - M : np.ndarray (ns,nt) - loss matrix - eta : float - Regularization term for Laplacian regularization - alpha : float - Regularization term for source domain's importance in regularization - numItermax : int, optional - Max number of iterations - stopThr : float, optional - Stop threshold on error (inner emd solver) (>0) - numInnerItermax : int, optional - Max number of iterations (inner CG solver) - stopInnerThr : float, optional - Stop threshold on error (inner CG solver) (>0) - verbose : bool, optional - Print information along iterations - log : bool, optional - record log if True - - - Returns - ------- - gamma : (ns x nt) ndarray - Optimal transportation matrix for the given parameters - log : dict - log dictionary return only if log==True in parameters - - - References - ---------- - - .. [5] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, - "Optimal Transport for Domain Adaptation," in IEEE - Transactions on Pattern Analysis and Machine Intelligence , - vol.PP, no.99, pp.1-1 - - See Also - -------- - ot.lp.emd : Unregularized OT - ot.optim.cg : General regularized OT - - """ - if sim == 'gauss': - if 'rbfparam' not in kwargs: - kwargs['rbfparam'] = 1 / (2 * (np.mean(dist(xs, xs, 'sqeuclidean')) ** 2)) - sS = kernel(xs, xs, method=kwargs['sim'], sigma=kwargs['rbfparam']) - sT = kernel(xt, xt, method=kwargs['sim'], sigma=kwargs['rbfparam']) - - elif sim == 'knn': - if 'nn' not in kwargs: - kwargs['nn'] = 5 - - from sklearn.neighbors import kneighbors_graph - - sS = kneighbors_graph(xs, kwargs['nn']).toarray() - sS = (sS + sS.T) / 2 - sT = kneighbors_graph(xt, kwargs['nn']).toarray() - sT = (sT + sT.T) / 2 - - lS = laplacian(sS) - lT = laplacian(sT) - - def f(G): - return alpha * np.trace(np.dot(xt.T, np.dot(G.T, np.dot(lS, np.dot(G, xt))))) \ - + (1 - alpha) * np.trace(np.dot(xs.T, np.dot(G, np.dot(lT, np.dot(G.T, xs))))) - - def df(G): - return alpha * np.dot(lS + lS.T, np.dot(G, np.dot(xt, xt.T)))\ - + (1 - alpha) * np.dot(xs, np.dot(xs.T, np.dot(G, lT + lT.T))) - - return cg(a, b, M, reg=eta, f=f, df=df, G0=None, numItermax=numItermax, numItermaxEmd=numInnerItermax, - stopThr=stopThr, stopThr2=stopInnerThr, verbose=verbose, log=log) - - def distribution_estimation_uniform(X): """estimates a uniform distribution from an array of samples X @@ -1603,113 +1494,6 @@ class SinkhornLpl1Transport(BaseTransport): return self -class EMDLaplaceTransport(BaseTransport): - - """Domain Adapatation OT method based on Earth Mover's Distance with Laplacian regularization - - Parameters - ---------- - reg_lap : float, optional (default=1) - Laplacian regularization parameter - reg_src : float, optional (default=0.5) - Source relative importance in regularization - metric : string, optional (default="sqeuclidean") - The ground metric for the Wasserstein problem - norm : string, optional (default=None) - If given, normalize the ground metric to avoid numerical errors that - can occur with large metric values. - similarity : string, optional (default="knn") - The similarity to use either knn or gaussian - max_iter : int, optional (default=100) - Max number of BCD iterations - tol : float, optional (default=1e-5) - Stop threshold on relative loss decrease (>0) - max_inner_iter : int, optional (default=10) - Max number of iterations (inner CG solver) - inner_tol : float, optional (default=1e-6) - Stop threshold on error (inner CG solver) (>0) - log : int, optional (default=False) - Controls the logs of the optimization algorithm - distribution_estimation : callable, optional (defaults to the uniform) - The kind of distribution estimation to employ - out_of_sample_map : string, optional (default="ferradans") - The kind of out of sample mapping to apply to transport samples - from a domain into another one. Currently the only possible option is - "ferradans" which uses the method proposed in [6]. - - Attributes - ---------- - coupling_ : array-like, shape (n_source_samples, n_target_samples) - The optimal coupling - - References - ---------- - .. [1] N. Courty; R. Flamary; D. Tuia; A. Rakotomamonjy, - "Optimal Transport for Domain Adaptation," in IEEE Transactions - on Pattern Analysis and Machine Intelligence , vol.PP, no.99, pp.1-1 - """ - - def __init__(self, reg_lap=1., reg_src=1., alpha=0.5, - metric="sqeuclidean", norm=None, similarity="knn", max_iter=100, tol=1e-9, - max_inner_iter=100000, inner_tol=1e-9, log=False, verbose=False, - distribution_estimation=distribution_estimation_uniform, - out_of_sample_map='ferradans'): - self.reg_lap = reg_lap - self.reg_src = reg_src - self.alpha = alpha - self.metric = metric - self.norm = norm - self.similarity = similarity - self.max_iter = max_iter - self.tol = tol - self.max_inner_iter = max_inner_iter - self.inner_tol = inner_tol - self.log = log - self.verbose = verbose - self.distribution_estimation = distribution_estimation - self.out_of_sample_map = out_of_sample_map - - def fit(self, Xs, ys=None, Xt=None, yt=None): - """Build a coupling matrix from source and target sets of samples - (Xs, ys) and (Xt, yt) - - Parameters - ---------- - Xs : array-like, shape (n_source_samples, n_features) - The training input samples. - ys : array-like, shape (n_source_samples,) - The class labels - Xt : array-like, shape (n_target_samples, n_features) - The training input samples. - yt : array-like, shape (n_target_samples,) - The class labels. If some target samples are unlabeled, fill the - yt's elements with -1. - - Warning: Note that, due to this convention -1 cannot be used as a - class label - - Returns - ------- - self : object - Returns self. - """ - - super(EMDLaplaceTransport, self).fit(Xs, ys, Xt, yt) - - returned_ = emd_laplace(a=self.mu_s, b=self.mu_t, xs=self.xs_, - xt=self.xt_, M=self.cost_, sim=self.similarity, eta=self.reg_lap, alpha=self.reg_src, - numItermax=self.max_iter, stopThr=self.tol, numInnerItermax=self.max_inner_iter, - stopInnerThr=self.inner_tol, log=self.log, verbose=self.verbose) - - # coupling estimation - if self.log: - self.coupling_, self.log_ = returned_ - else: - self.coupling_ = returned_ - self.log_ = dict() - return self - - class SinkhornL1l2Transport(BaseTransport): """Domain Adapatation OT method based on sinkhorn algorithm + diff --git a/test/test_da.py b/test/test_da.py index 4eaf193..1517cec 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -601,57 +601,3 @@ def test_jcpot_transport_class(): # check that the oos method is working assert_equal(transp_Xs_new.shape, Xs_new.shape) - - -def test_emd_laplace_class(): - """test_emd_laplace_transport - """ - ns = 150 - nt = 200 - - Xs, ys = make_data_classif('3gauss', ns) - Xt, yt = make_data_classif('3gauss2', nt) - - otda = ot.da.EMDLaplaceTransport(reg_lap=0.01, max_iter=1000, tol=1e-9, verbose=False, log=True) - - # test its computed - otda.fit(Xs=Xs, ys=ys, Xt=Xt) - - assert hasattr(otda, "coupling_") - assert hasattr(otda, "log_") - - # test dimensions of coupling - assert_equal(otda.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) - - # test all margin constraints - mu_s = unif(ns) - mu_t = unif(nt) - - assert_allclose( - np.sum(otda.coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) - assert_allclose( - np.sum(otda.coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) - - # test transform - transp_Xs = otda.transform(Xs=Xs) - [assert_equal(x.shape, y.shape) for x, y in zip(transp_Xs, Xs)] - - Xs_new, _ = make_data_classif('3gauss', ns + 1) - transp_Xs_new = otda.transform(Xs_new) - - # check that the oos method is working - assert_equal(transp_Xs_new.shape, Xs_new.shape) - - # test inverse transform - transp_Xt = otda.inverse_transform(Xt=Xt) - assert_equal(transp_Xt.shape, Xt.shape) - - Xt_new, _ = make_data_classif('3gauss2', nt + 1) - transp_Xt_new = otda.inverse_transform(Xt=Xt_new) - - # check that the oos method is working - assert_equal(transp_Xt_new.shape, Xt_new.shape) - - # test fit_transform - transp_Xs = otda.fit_transform(Xs=Xs, Xt=Xt) - assert_equal(transp_Xs.shape, Xs.shape) -- cgit v1.2.3 From 55926517470df6ced506a934b8b9b5e23e023464 Mon Sep 17 00:00:00 2001 From: ievred Date: Wed, 8 Apr 2020 10:18:02 +0200 Subject: test+utils+readme --- README.md | 2 +- ot/da.py | 2 +- test/test_da.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'test') diff --git a/README.md b/README.md index f439405..b6baf14 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ It provides the following solvers: * Non regularized free support Wasserstein barycenters [20]. * Unbalanced OT with KL relaxation distance and barycenter [10, 25]. * Screening Sinkhorn Algorithm for OT [26]. -* JCPOT algorithm for multi-source target shift [27]. +* JCPOT algorithm for multi-source domain adaptation with target shift [27]. Some demonstrations (both in Python and Jupyter Notebook format) are available in the examples folder. diff --git a/ot/da.py b/ot/da.py index 90e9e92..3a458eb 100644 --- a/ot/da.py +++ b/ot/da.py @@ -16,7 +16,7 @@ import scipy.linalg as linalg from .bregman import sinkhorn, jcpot_barycenter from .lp import emd -from .utils import unif, dist, kernel, cost_normalization, laplacian +from .utils import unif, dist, kernel, cost_normalization from .utils import check_params, BaseEstimator from .unbalanced import sinkhorn_unbalanced from .optim import cg diff --git a/test/test_da.py b/test/test_da.py index 1517cec..b58cf51 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -565,7 +565,7 @@ def test_jcpot_transport_class(): Xs = [Xs1, Xs2] ys = [ys1, ys2] - otda = ot.da.JCPOTTransport(reg_e=0.01, max_iter=1000, tol=1e-9, verbose=True, log=True) + otda = ot.da.JCPOTTransport(reg_e=1, max_iter=10000, tol=1e-9, verbose=True, log=True) # test its computed otda.fit(Xs=Xs, ys=ys, Xt=Xt) -- cgit v1.2.3 From d6ef8676cc3f94ba5d80acc9fd9745c9ed91819a Mon Sep 17 00:00:00 2001 From: ievred Date: Wed, 8 Apr 2020 10:28:57 +0200 Subject: remove jcpot from laplace --- examples/plot_otda_jcpot.py | 171 ----------------------------------------- ot/bregman.py | 160 --------------------------------------- ot/da.py | 181 +------------------------------------------- test/test_da.py | 56 +------------- 4 files changed, 3 insertions(+), 565 deletions(-) delete mode 100644 examples/plot_otda_jcpot.py (limited to 'test') diff --git a/examples/plot_otda_jcpot.py b/examples/plot_otda_jcpot.py deleted file mode 100644 index 316fa8b..0000000 --- a/examples/plot_otda_jcpot.py +++ /dev/null @@ -1,171 +0,0 @@ -# -*- coding: utf-8 -*- -""" -======================== -OT for multi-source target shift -======================== - -This example introduces a target shift problem with two 2D source and 1 target domain. - -""" - -# Authors: Remi Flamary -# Ievgen Redko -# -# License: MIT License - -import pylab as pl -import numpy as np -import ot -from ot.datasets import make_data_classif - -############################################################################## -# Generate data -# ------------- -n = 50 -sigma = 0.3 -np.random.seed(1985) - -p1 = .2 -dec1 = [0, 2] - -p2 = .9 -dec2 = [0, -2] - -pt = .4 -dect = [4, 0] - -xs1, ys1 = make_data_classif('2gauss_prop', n, nz=sigma, p=p1, bias=dec1) -xs2, ys2 = make_data_classif('2gauss_prop', n + 1, nz=sigma, p=p2, bias=dec2) -xt, yt = make_data_classif('2gauss_prop', n, nz=sigma, p=pt, bias=dect) - -all_Xr = [xs1, xs2] -all_Yr = [ys1, ys2] -# %% - -da = 1.5 - - -def plot_ax(dec, name): - pl.plot([dec[0], dec[0]], [dec[1] - da, dec[1] + da], 'k', alpha=0.5) - pl.plot([dec[0] - da, dec[0] + da], [dec[1], dec[1]], 'k', alpha=0.5) - pl.text(dec[0] - .5, dec[1] + 2, name) - - -############################################################################## -# Fig 1 : plots source and target samples -# --------------------------------------- - -pl.figure(1) -pl.clf() -plot_ax(dec1, 'Source 1') -plot_ax(dec2, 'Source 2') -plot_ax(dect, 'Target') -pl.scatter(xs1[:, 0], xs1[:, 1], c=ys1, s=35, marker='x', cmap='Set1', vmax=9, - label='Source 1 ({:1.2f}, {:1.2f})'.format(1 - p1, p1)) -pl.scatter(xs2[:, 0], xs2[:, 1], c=ys2, s=35, marker='+', cmap='Set1', vmax=9, - label='Source 2 ({:1.2f}, {:1.2f})'.format(1 - p2, p2)) -pl.scatter(xt[:, 0], xt[:, 1], c=yt, s=35, marker='o', cmap='Set1', vmax=9, - label='Target ({:1.2f}, {:1.2f})'.format(1 - pt, pt)) -pl.title('Data') - -pl.legend() -pl.axis('equal') -pl.axis('off') - -############################################################################## -# Instantiate Sinkhorn transport algorithm and fit them for all source domains -# ---------------------------------------------------------------------------- -ot_sinkhorn = ot.da.SinkhornTransport(reg_e=1e-1, metric='sqeuclidean') - - -def print_G(G, xs, ys, xt): - for i in range(G.shape[0]): - for j in range(G.shape[1]): - if G[i, j] > 5e-4: - if ys[i]: - c = 'b' - else: - c = 'r' - pl.plot([xs[i, 0], xt[j, 0]], [xs[i, 1], xt[j, 1]], c, alpha=.2) - - -############################################################################## -# Fig 2 : plot optimal couplings and transported samples -# ------------------------------------------------------ -pl.figure(2) -pl.clf() -plot_ax(dec1, 'Source 1') -plot_ax(dec2, 'Source 2') -plot_ax(dect, 'Target') -print_G(ot_sinkhorn.fit(Xs=xs1, Xt=xt).coupling_, xs1, ys1, xt) -print_G(ot_sinkhorn.fit(Xs=xs2, Xt=xt).coupling_, xs2, ys2, xt) -pl.scatter(xs1[:, 0], xs1[:, 1], c=ys1, s=35, marker='x', cmap='Set1', vmax=9) -pl.scatter(xs2[:, 0], xs2[:, 1], c=ys2, s=35, marker='+', cmap='Set1', vmax=9) -pl.scatter(xt[:, 0], xt[:, 1], c=yt, s=35, marker='o', cmap='Set1', vmax=9) - -pl.plot([], [], 'r', alpha=.2, label='Mass from Class 1') -pl.plot([], [], 'b', alpha=.2, label='Mass from Class 2') - -pl.title('Independent OT') - -pl.legend() -pl.axis('equal') -pl.axis('off') - -############################################################################## -# Instantiate JCPOT adaptation algorithm and fit it -# ---------------------------------------------------------------------------- -otda = ot.da.JCPOTTransport(reg_e=1e-2, max_iter=1000, metric='sqeuclidean', tol=1e-9, verbose=True, log=True) -otda.fit(all_Xr, all_Yr, xt) - -ws1 = otda.proportions_.dot(otda.log_['D2'][0]) -ws2 = otda.proportions_.dot(otda.log_['D2'][1]) - -pl.figure(3) -pl.clf() -plot_ax(dec1, 'Source 1') -plot_ax(dec2, 'Source 2') -plot_ax(dect, 'Target') -print_G(ot.bregman.sinkhorn(ws1, [], otda.log_['M'][0], reg=1e-2), xs1, ys1, xt) -print_G(ot.bregman.sinkhorn(ws2, [], otda.log_['M'][1], reg=1e-2), xs2, ys2, xt) -pl.scatter(xs1[:, 0], xs1[:, 1], c=ys1, s=35, marker='x', cmap='Set1', vmax=9) -pl.scatter(xs2[:, 0], xs2[:, 1], c=ys2, s=35, marker='+', cmap='Set1', vmax=9) -pl.scatter(xt[:, 0], xt[:, 1], c=yt, s=35, marker='o', cmap='Set1', vmax=9) - -pl.plot([], [], 'r', alpha=.2, label='Mass from Class 1') -pl.plot([], [], 'b', alpha=.2, label='Mass from Class 2') - -pl.title('OT with prop estimation ({:1.3f},{:1.3f})'.format(otda.proportions_[0], otda.proportions_[1])) - -pl.legend() -pl.axis('equal') -pl.axis('off') - -############################################################################## -# Run oracle transport algorithm with known proportions -# ---------------------------------------------------------------------------- -h_res = np.array([1 - pt, pt]) - -ws1 = h_res.dot(otda.log_['D2'][0]) -ws2 = h_res.dot(otda.log_['D2'][1]) - -pl.figure(4) -pl.clf() -plot_ax(dec1, 'Source 1') -plot_ax(dec2, 'Source 2') -plot_ax(dect, 'Target') -print_G(ot.bregman.sinkhorn(ws1, [], otda.log_['M'][0], reg=1e-2), xs1, ys1, xt) -print_G(ot.bregman.sinkhorn(ws2, [], otda.log_['M'][1], reg=1e-2), xs2, ys2, xt) -pl.scatter(xs1[:, 0], xs1[:, 1], c=ys1, s=35, marker='x', cmap='Set1', vmax=9) -pl.scatter(xs2[:, 0], xs2[:, 1], c=ys2, s=35, marker='+', cmap='Set1', vmax=9) -pl.scatter(xt[:, 0], xt[:, 1], c=yt, s=35, marker='o', cmap='Set1', vmax=9) - -pl.plot([], [], 'r', alpha=.2, label='Mass from Class 1') -pl.plot([], [], 'b', alpha=.2, label='Mass from Class 2') - -pl.title('OT with known proportion ({:1.1f},{:1.1f})'.format(h_res[0], h_res[1])) - -pl.legend() -pl.axis('equal') -pl.axis('off') -pl.show() diff --git a/ot/bregman.py b/ot/bregman.py index 61dfa52..f737e81 100644 --- a/ot/bregman.py +++ b/ot/bregman.py @@ -1503,166 +1503,6 @@ def unmix(a, D, M, M0, h0, reg, reg0, alpha, numItermax=1000, return np.sum(K0, axis=1) -def jcpot_barycenter(Xs, Ys, Xt, reg, metric='sqeuclidean', numItermax=100, - stopThr=1e-6, verbose=False, log=False, **kwargs): - r'''Joint OT and proportion estimation for multi-source target shift as proposed in [27] - - The function solves the following optimization problem: - - .. math:: - - \mathbf{h} = arg\min_{\mathbf{h}}\quad \sum_{k=1}^{K} \lambda_k - W_{reg}((\mathbf{D}_2^{(k)} \mathbf{h})^T, \mathbf{a}) - - s.t. \ \forall k, \mathbf{D}_1^{(k)} \gamma_k \mathbf{1}_n= \mathbf{h} - - where : - - - :math:`\lambda_k` is the weight of k-th source domain - - :math:`W_{reg}(\cdot,\cdot)` is the entropic regularized Wasserstein distance (see ot.bregman.sinkhorn) - - :math:`\mathbf{D}_2^{(k)}` is a matrix of weights related to k-th source domain defined as in [p. 5, 27], its expected shape is `(n_k, C)` where `n_k` is the number of elements in the k-th source domain and `C` is the number of classes - - :math:`\mathbf{h}` is a vector of estimated proportions in the target domain of size C - - :math:`\mathbf{a}` is a uniform vector of weights in the target domain of size `n` - - :math:`\mathbf{D}_1^{(k)}` is a matrix of class assignments defined as in [p. 5, 27], its expected shape is `(n_k, C)` - - The problem consist in solving a Wasserstein barycenter problem to estimate the proportions :math:`\mathbf{h}` in the target domain. - - The algorithm used for solving the problem is the Iterative Bregman projections algorithm - with two sets of marginal constraints related to the unknown vector :math:`\mathbf{h}` and uniform tarhet distribution. - - Parameters - ---------- - Xs : list of K np.ndarray(nsk,d) - features of all source domains' samples - Ys : list of K np.ndarray(nsk,) - labels of all source domains' samples - Xt : np.ndarray (nt,d) - samples in the target domain - reg : float - Regularization term > 0 - metric : string, optional (default="sqeuclidean") - The ground metric for the Wasserstein problem - numItermax : int, optional - Max number of iterations - stopThr : float, optional - Stop threshold on relative change in the barycenter (>0) - log : bool, optional - record log if True - verbose : bool, optional (default=False) - Controls the verbosity of the optimization algorithm - - Returns - ------- - gamma : List of K (nsk x nt) ndarrays - Optimal transportation matrices for the given parameters for each pair of source and target domains - h : (C,) ndarray - proportion estimation in the target domain - log : dict - log dictionary return only if log==True in parameters - - - References - ---------- - - .. [27] Ievgen Redko, Nicolas Courty, Rémi Flamary, Devis Tuia - "Optimal transport for multi-source domain adaptation under target shift", - International Conference on Artificial Intelligence and Statistics (AISTATS), 2019. - - ''' - nbclasses = len(np.unique(Ys[0])) - nbdomains = len(Xs) - - # log dictionary - if log: - log = {'niter': 0, 'err': [], 'M': [], 'D1': [], 'D2': []} - - K = [] - M = [] - D1 = [] - D2 = [] - - # For each source domain, build cost matrices M, Gibbs kernels K and corresponding matrices D_1 and D_2 - for d in range(nbdomains): - dom = {} - nsk = Xs[d].shape[0] # get number of elements for this domain - dom['nbelem'] = nsk - classes = np.unique(Ys[d]) # get number of classes for this domain - - # format classes to start from 0 for convenience - if np.min(classes) != 0: - Ys[d] = Ys[d] - np.min(classes) - classes = np.unique(Ys[d]) - - # build the corresponding D_1 and D_2 matrices - Dtmp1 = np.zeros((nbclasses, nsk)) - Dtmp2 = np.zeros((nbclasses, nsk)) - - for c in classes: - nbelemperclass = np.sum(Ys[d] == c) - if nbelemperclass != 0: - Dtmp1[int(c), Ys[d] == c] = 1. - Dtmp2[int(c), Ys[d] == c] = 1. / (nbelemperclass) - D1.append(Dtmp1) - D2.append(Dtmp2) - - # build the cost matrix and the Gibbs kernel - Mtmp = dist(Xs[d], Xt, metric=metric) - Mtmp = Mtmp / np.median(Mtmp) - M.append(Mtmp) - - Ktmp = np.empty(Mtmp.shape, dtype=Mtmp.dtype) - np.divide(Mtmp, -reg, out=Ktmp) - np.exp(Ktmp, out=Ktmp) - K.append(Ktmp) - - # uniform target distribution - a = unif(np.shape(Xt)[0]) - - cpt = 0 # iterations count - err = 1 - old_bary = np.ones((nbclasses)) - - while (err > stopThr and cpt < numItermax): - - bary = np.zeros((nbclasses)) - - # update coupling matrices for marginal constraints w.r.t. uniform target distribution - for d in range(nbdomains): - K[d] = projC(K[d], a) - other = np.sum(K[d], axis=1) - bary = bary + np.log(np.dot(D1[d], other)) / nbdomains - - bary = np.exp(bary) - - # update coupling matrices for marginal constraints w.r.t. unknown proportions based on [Prop 4., 27] - for d in range(nbdomains): - new = np.dot(D2[d].T, bary) - K[d] = projR(K[d], new) - - err = np.linalg.norm(bary - old_bary) - cpt = cpt + 1 - old_bary = bary - - if log: - log['err'].append(err) - - if verbose: - if cpt % 200 == 0: - print('{:5s}|{:12s}'.format('It.', 'Err') + '\n' + '-' * 19) - print('{:5d}|{:8e}|'.format(cpt, err)) - - bary = bary / np.sum(bary) - - if log: - log['niter'] = cpt - log['M'] = M - log['D1'] = D1 - log['D2'] = D2 - return K, bary, log - else: - return K, bary - - def empirical_sinkhorn(X_s, X_t, reg, a=None, b=None, metric='sqeuclidean', numIterMax=10000, stopThr=1e-9, verbose=False, log=False, **kwargs): diff --git a/ot/da.py b/ot/da.py index 0fdd3be..474c944 100644 --- a/ot/da.py +++ b/ot/da.py @@ -14,7 +14,7 @@ Domain adaptation with optimal transport import numpy as np import scipy.linalg as linalg -from .bregman import sinkhorn, jcpot_barycenter +from .bregman import sinkhorn from .lp import emd from .utils import unif, dist, kernel, cost_normalization, laplacian from .utils import check_params, BaseEstimator @@ -2121,181 +2121,4 @@ class UnbalancedSinkhornTransport(BaseTransport): self.coupling_ = returned_ self.log_ = dict() - return self - - -class JCPOTTransport(BaseTransport): - - """Domain Adapatation OT method for multi-source target shift based on Wasserstein barycenter algorithm. - - Parameters - ---------- - reg_e : float, optional (default=1) - Entropic regularization parameter - max_iter : int, float, optional (default=10) - The minimum number of iteration before stopping the optimization - algorithm if no it has not converged - tol : float, optional (default=10e-9) - Stop threshold on error (inner sinkhorn solver) (>0) - verbose : bool, optional (default=False) - Controls the verbosity of the optimization algorithm - log : bool, optional (default=False) - Controls the logs of the optimization algorithm - metric : string, optional (default="sqeuclidean") - The ground metric for the Wasserstein problem - norm : string, optional (default=None) - If given, normalize the ground metric to avoid numerical errors that - can occur with large metric values. - distribution_estimation : callable, optional (defaults to the uniform) - The kind of distribution estimation to employ - out_of_sample_map : string, optional (default="ferradans") - The kind of out of sample mapping to apply to transport samples - from a domain into another one. Currently the only possible option is - "ferradans" which uses the method proposed in [6]. - - Attributes - ---------- - coupling_ : list of array-like objects, shape K x (n_source_samples, n_target_samples) - A set of optimal couplings between each source domain and the target domain - proportions_ : array-like, shape (n_classes,) - Estimated class proportions in the target domain - log_ : dictionary - The dictionary of log, empty dic if parameter log is not True - - References - ---------- - - .. [1] Ievgen Redko, Nicolas Courty, Rémi Flamary, Devis Tuia - "Optimal transport for multi-source domain adaptation under target shift", - International Conference on Artificial Intelligence and Statistics (AISTATS), - vol. 89, p.849-858, 2019. - - """ - - def __init__(self, reg_e=.1, max_iter=10, - tol=10e-9, verbose=False, log=False, - metric="sqeuclidean", - out_of_sample_map='ferradans'): - self.reg_e = reg_e - self.max_iter = max_iter - self.tol = tol - self.verbose = verbose - self.log = log - self.metric = metric - self.out_of_sample_map = out_of_sample_map - - def fit(self, Xs, ys=None, Xt=None, yt=None): - """Building coupling matrices from a list of source and target sets of samples - (Xs, ys) and (Xt, yt) - - Parameters - ---------- - Xs : list of K array-like objects, shape K x (nk_source_samples, n_features) - A list of the training input samples. - ys : list of K array-like objects, shape K x (nk_source_samples,) - A list of the class labels - Xt : array-like, shape (n_target_samples, n_features) - The training input samples. - yt : array-like, shape (n_target_samples,) - The class labels. If some target samples are unlabeled, fill the - yt's elements with -1. - - Warning: Note that, due to this convention -1 cannot be used as a - class label - - Returns - ------- - self : object - Returns self. - """ - - # check the necessary inputs parameters are here - if check_params(Xs=Xs, Xt=Xt, ys=ys): - - self.xs_ = Xs - self.xt_ = Xt - - returned_ = jcpot_barycenter(Xs=Xs, Ys=ys, Xt=Xt, reg=self.reg_e, - metric=self.metric, distrinumItermax=self.max_iter, stopThr=self.tol, - verbose=self.verbose, log=self.log) - - # deal with the value of log - if self.log: - self.coupling_, self.proportions_, self.log_ = returned_ - else: - self.coupling_, self.proportions_ = returned_ - self.log_ = dict() - - return self - - def transform(self, Xs=None, ys=None, Xt=None, yt=None, batch_size=128): - """Transports source samples Xs onto target ones Xt - - Parameters - ---------- - Xs : array-like, shape (n_source_samples, n_features) - The training input samples. - ys : array-like, shape (n_source_samples,) - The class labels - Xt : array-like, shape (n_target_samples, n_features) - The training input samples. - yt : array-like, shape (n_target_samples,) - The class labels. If some target samples are unlabeled, fill the - yt's elements with -1. - - Warning: Note that, due to this convention -1 cannot be used as a - class label - batch_size : int, optional (default=128) - The batch size for out of sample inverse transform - """ - - transp_Xs = [] - - # check the necessary inputs parameters are here - if check_params(Xs=Xs): - - if all([np.allclose(x, y) for x, y in zip(self.xs_, Xs)]): - - # perform standard barycentric mapping for each source domain - - for coupling in self.coupling_: - transp = coupling / np.sum(coupling, 1)[:, None] - - # set nans to 0 - transp[~ np.isfinite(transp)] = 0 - - # compute transported samples - transp_Xs.append(np.dot(transp, self.xt_)) - else: - - # perform out of sample mapping - indices = np.arange(Xs.shape[0]) - batch_ind = [ - indices[i:i + batch_size] - for i in range(0, len(indices), batch_size)] - - transp_Xs = [] - - for bi in batch_ind: - transp_Xs_ = [] - - # get the nearest neighbor in the sources domains - xs = np.concatenate(self.xs_, axis=0) - idx = np.argmin(dist(Xs[bi], xs), axis=1) - - # transport the source samples - for coupling in self.coupling_: - transp = coupling / np.sum( - coupling, 1)[:, None] - transp[~ np.isfinite(transp)] = 0 - transp_Xs_.append(np.dot(transp, self.xt_)) - - transp_Xs_ = np.concatenate(transp_Xs_, axis=0) - - # define the transported points - transp_Xs_ = transp_Xs_[idx, :] + Xs[bi] - xs[idx, :] - transp_Xs.append(transp_Xs_) - - transp_Xs = np.concatenate(transp_Xs, axis=0) - - return transp_Xs + return self \ No newline at end of file diff --git a/test/test_da.py b/test/test_da.py index 4eaf193..0e31f26 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -549,60 +549,6 @@ def test_linear_mapping_class(): np.testing.assert_allclose(Ct, Cst, rtol=1e-2, atol=1e-2) -def test_jcpot_transport_class(): - """test_jcpot_transport - """ - - ns1 = 150 - ns2 = 150 - nt = 200 - - Xs1, ys1 = make_data_classif('3gauss', ns1) - Xs2, ys2 = make_data_classif('3gauss', ns2) - - Xt, yt = make_data_classif('3gauss2', nt) - - Xs = [Xs1, Xs2] - ys = [ys1, ys2] - - otda = ot.da.JCPOTTransport(reg_e=0.01, max_iter=1000, tol=1e-9, verbose=True, log=True) - - # test its computed - otda.fit(Xs=Xs, ys=ys, Xt=Xt) - - assert hasattr(otda, "coupling_") - assert hasattr(otda, "proportions_") - assert hasattr(otda, "log_") - - # test dimensions of coupling - for i, xs in enumerate(Xs): - assert_equal(otda.coupling_[i].shape, ((xs.shape[0], Xt.shape[0]))) - - # test all margin constraints - mu_t = unif(nt) - - for i in range(len(Xs)): - # test margin constraints w.r.t. uniform target weights for each coupling matrix - assert_allclose( - np.sum(otda.coupling_[i], axis=0), mu_t, rtol=1e-3, atol=1e-3) - - # test margin constraints w.r.t. modified source weights for each source domain - - assert_allclose( - np.dot(otda.log_['D1'][i], np.sum(otda.coupling_[i], axis=1)), otda.proportions_, rtol=1e-3, - atol=1e-3) - - # test transform - transp_Xs = otda.transform(Xs=Xs) - [assert_equal(x.shape, y.shape) for x, y in zip(transp_Xs, Xs)] - - Xs_new, _ = make_data_classif('3gauss', ns1 + 1) - transp_Xs_new = otda.transform(Xs_new) - - # check that the oos method is working - assert_equal(transp_Xs_new.shape, Xs_new.shape) - - def test_emd_laplace_class(): """test_emd_laplace_transport """ @@ -654,4 +600,4 @@ def test_emd_laplace_class(): # test fit_transform transp_Xs = otda.fit_transform(Xs=Xs, Xt=Xt) - assert_equal(transp_Xs.shape, Xs.shape) + assert_equal(transp_Xs.shape, Xs.shape) \ No newline at end of file -- cgit v1.2.3 From 4d77cc99ae5dd2cf3521ff2f136ff783c7d1d7ef Mon Sep 17 00:00:00 2001 From: ievred Date: Wed, 8 Apr 2020 10:41:53 +0200 Subject: pep test da --- test/test_da.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'test') diff --git a/test/test_da.py b/test/test_da.py index 0e31f26..befec43 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -600,4 +600,4 @@ def test_emd_laplace_class(): # test fit_transform transp_Xs = otda.fit_transform(Xs=Xs, Xt=Xt) - assert_equal(transp_Xs.shape, Xs.shape) \ No newline at end of file + assert_equal(transp_Xs.shape, Xs.shape) -- cgit v1.2.3 From 25cad1942166d25d2d305cf93937c1d5edc91716 Mon Sep 17 00:00:00 2001 From: ievred Date: Wed, 8 Apr 2020 10:54:56 +0200 Subject: pep test da --- test/test_da.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'test') diff --git a/test/test_da.py b/test/test_da.py index befec43..0e31f26 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -600,4 +600,4 @@ def test_emd_laplace_class(): # test fit_transform transp_Xs = otda.fit_transform(Xs=Xs, Xt=Xt) - assert_equal(transp_Xs.shape, Xs.shape) + assert_equal(transp_Xs.shape, Xs.shape) \ No newline at end of file -- cgit v1.2.3 From 37412f59a94e8607fdbd5f7f29434a70ebe18688 Mon Sep 17 00:00:00 2001 From: ievred Date: Wed, 8 Apr 2020 11:05:50 +0200 Subject: remove blank line --- ot/da.py | 2 +- test/test_da.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'test') diff --git a/ot/da.py b/ot/da.py index 474c944..108609f 100644 --- a/ot/da.py +++ b/ot/da.py @@ -2121,4 +2121,4 @@ class UnbalancedSinkhornTransport(BaseTransport): self.coupling_ = returned_ self.log_ = dict() - return self \ No newline at end of file + return self diff --git a/test/test_da.py b/test/test_da.py index 0e31f26..befec43 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -600,4 +600,4 @@ def test_emd_laplace_class(): # test fit_transform transp_Xs = otda.fit_transform(Xs=Xs, Xt=Xt) - assert_equal(transp_Xs.shape, Xs.shape) \ No newline at end of file + assert_equal(transp_Xs.shape, Xs.shape) -- cgit v1.2.3 From bc51793333994a1bf6263c9e9c111d754172fc82 Mon Sep 17 00:00:00 2001 From: ievred Date: Wed, 8 Apr 2020 14:35:00 +0200 Subject: added test barycenter + modif target --- ot/bregman.py | 2 +- test/test_da.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) (limited to 'test') diff --git a/ot/bregman.py b/ot/bregman.py index 410ae85..c44c141 100644 --- a/ot/bregman.py +++ b/ot/bregman.py @@ -1528,7 +1528,7 @@ def jcpot_barycenter(Xs, Ys, Xt, reg, metric='sqeuclidean', numItermax=100, The problem consist in solving a Wasserstein barycenter problem to estimate the proportions :math:`\mathbf{h}` in the target domain. The algorithm used for solving the problem is the Iterative Bregman projections algorithm - with two sets of marginal constraints related to the unknown vector :math:`\mathbf{h}` and uniform tarhet distribution. + with two sets of marginal constraints related to the unknown vector :math:`\mathbf{h}` and uniform target distribution. Parameters ---------- diff --git a/test/test_da.py b/test/test_da.py index b58cf51..c54dab7 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -601,3 +601,31 @@ def test_jcpot_transport_class(): # check that the oos method is working assert_equal(transp_Xs_new.shape, Xs_new.shape) + + +def test_jcpot_barycenter(): + """test_jcpot_barycenter + """ + + ns1 = 150 + ns2 = 150 + nt = 200 + + sigma = 0.1 + np.random.seed(1985) + + ps1 = .2 + ps2 = .9 + pt = .4 + + Xs1, ys1 = make_data_classif('2gauss_prop', ns1, nz=sigma, p=ps1) + Xs2, ys2 = make_data_classif('2gauss_prop', ns2, nz=sigma, p=ps2) + Xt, yt = make_data_classif('2gauss_prop', nt, nz=sigma, p=pt) + + Xs = [Xs1, Xs2] + ys = [ys1, ys2] + + _, prop, = ot.bregman.jcpot_barycenter(Xs, ys, Xt, reg=.5, metric='sqeuclidean', + numItermax=10000, stopThr=1e-9, verbose=False, log=False) + + np.testing.assert_allclose(prop, [1 - pt, pt], rtol=1e-3, atol=1e-3) -- cgit v1.2.3 From 0b402fd7c7e07043afd3a9df9d75bc424730b06f Mon Sep 17 00:00:00 2001 From: ievred Date: Wed, 8 Apr 2020 16:05:01 +0200 Subject: add label prop + inverse --- ot/da.py | 171 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- test/test_da.py | 47 ++++++++++++++++ 2 files changed, 214 insertions(+), 4 deletions(-) (limited to 'test') diff --git a/ot/da.py b/ot/da.py index 3a458eb..29b0a8b 100644 --- a/ot/da.py +++ b/ot/da.py @@ -943,6 +943,46 @@ class BaseTransport(BaseEstimator): return transp_Xs + def transform_labels(self, ys=None): + """Propagate source labels ys to obtain estimated target labels + + Parameters + ---------- + ys : array-like, shape (n_source_samples,) + The class labels + + Returns + ------- + transp_ys : array-like, shape (n_target_samples,) + Estimated target labels. + """ + + # check the necessary inputs parameters are here + if check_params(ys=ys): + + classes = np.unique(ys) + n = len(classes) + D1 = np.zeros((n, len(ys))) + + # perform label propagation + transp = self.coupling_ / np.sum(self.coupling_, 1)[:, None] + + # set nans to 0 + transp[~ np.isfinite(transp)] = 0 + + if np.min(classes) != 0: + ys = ys - np.min(classes) + classes = np.unique(ys) + + for c in classes: + D1[int(c), ys == c] = 1 + + # compute transported samples + transp_ys = np.dot(D1, transp) + + return np.argmax(transp_ys,axis=0) + + def inverse_transform(self, Xs=None, ys=None, Xt=None, yt=None, batch_size=128): """Transports target samples Xt onto target samples Xs @@ -1010,6 +1050,44 @@ class BaseTransport(BaseEstimator): return transp_Xt + def inverse_transform_labels(self, yt=None): + """Propagate target labels yt to obtain estimated source labels ys + + Parameters + ---------- + yt : array-like, shape (n_target_samples,) + + Returns + ------- + transp_ys : array-like, shape (n_source_samples,) + Estimated source labels. + """ + + # check the necessary inputs parameters are here + if check_params(yt=yt): + + classes = np.unique(yt) + n = len(classes) + D1 = np.zeros((n, len(yt))) + + # perform label propagation + transp = self.coupling_ / np.sum(self.coupling_, 1)[:, None] + + # set nans to 0 + transp[~ np.isfinite(transp)] = 0 + + if np.min(classes) != 0: + yt = yt - np.min(classes) + classes = np.unique(yt) + + for c in classes: + D1[int(c), yt == c] = 1 + + # compute transported samples + transp_ys = np.dot(D1, transp.T) + + return np.argmax(transp_ys,axis=0) + class LinearTransport(BaseTransport): @@ -2017,10 +2095,10 @@ class JCPOTTransport(BaseTransport): Parameters ---------- - Xs : array-like, shape (n_source_samples, n_features) - The training input samples. - ys : array-like, shape (n_source_samples,) - The class labels + Xs : list of K array-like objects, shape K x (nk_source_samples, n_features) + A list of the training input samples. + ys : list of K array-like objects, shape K x (nk_source_samples,) + A list of the class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. yt : array-like, shape (n_target_samples,) @@ -2083,3 +2161,88 @@ class JCPOTTransport(BaseTransport): transp_Xs = np.concatenate(transp_Xs, axis=0) return transp_Xs + + def transform_labels(self, ys=None): + """Propagate source labels ys to obtain target labels + + Parameters + ---------- + ys : list of K array-like objects, shape K x (nk_source_samples,) + A list of the class labels + + Returns + ------- + yt : array-like, shape (n_target_samples,) + Estimated target labels. + """ + + # check the necessary inputs parameters are here + if check_params(ys=ys): + yt = np.zeros((len(np.unique(np.concatenate(ys))),self.xt_.shape[0])) + for i in range(len(ys)): + classes = np.unique(ys[i]) + n = len(classes) + ns = len(ys[i]) + + # perform label propagation + transp = self.coupling_[i] / np.sum(self.coupling_[i], 1)[:, None] + + # set nans to 0 + transp[~ np.isfinite(transp)] = 0 + + if self.log: + D1 = self.log_['D1'][i] + else: + D1 = np.zeros((n, ns)) + + if np.min(classes) != 0: + ys = ys - np.min(classes) + classes = np.unique(ys) + + for c in classes: + D1[int(c), ys == c] = 1 + # compute transported samples + yt = yt + np.dot(D1, transp)/len(ys) + + return np.argmax(yt,axis=0) + + def inverse_transform_labels(self, yt=None): + """Propagate source labels ys to obtain target labels + + Parameters + ---------- + yt : array-like, shape (n_source_samples,) + The target class labels + + Returns + ------- + transp_ys : list of K array-like objects, shape K x (nk_source_samples,) + A list of estimated source labels + """ + + # check the necessary inputs parameters are here + if check_params(yt=yt): + transp_ys = [] + classes = np.unique(yt) + n = len(classes) + D1 = np.zeros((n, len(yt))) + + if np.min(classes) != 0: + yt = yt - np.min(classes) + classes = np.unique(yt) + + for c in classes: + D1[int(c), yt == c] = 1 + + for i in range(len(self.xs_)): + + # perform label propagation + transp = self.coupling_[i] / np.sum(self.coupling_[i], 1)[:, None] + + # set nans to 0 + transp[~ np.isfinite(transp)] = 0 + + # compute transported labels + transp_ys.append(np.argmax(np.dot(D1, transp.T),axis=0)) + + return transp_ys diff --git a/test/test_da.py b/test/test_da.py index c54dab7..4eb6de0 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -65,6 +65,14 @@ def test_sinkhorn_lpl1_transport_class(): transp_Xs = otda.fit_transform(Xs=Xs, ys=ys, Xt=Xt) assert_equal(transp_Xs.shape, Xs.shape) + # check label propagation + transp_yt = otda.transform_labels(ys) + assert_equal(transp_yt.shape[0], yt.shape[0]) + + # check inverse label propagation + transp_ys = otda.inverse_transform_labels(yt) + assert_equal(transp_ys.shape[0], ys.shape[0]) + # test unsupervised vs semi-supervised mode otda_unsup = ot.da.SinkhornLpl1Transport() otda_unsup.fit(Xs=Xs, ys=ys, Xt=Xt) @@ -129,6 +137,14 @@ def test_sinkhorn_l1l2_transport_class(): transp_Xt = otda.inverse_transform(Xt=Xt) assert_equal(transp_Xt.shape, Xt.shape) + # check label propagation + transp_yt = otda.transform_labels(ys) + assert_equal(transp_yt.shape[0], yt.shape[0]) + + # check inverse label propagation + transp_ys = otda.inverse_transform_labels(yt) + assert_equal(transp_ys.shape[0], ys.shape[0]) + Xt_new, _ = make_data_classif('3gauss2', nt + 1) transp_Xt_new = otda.inverse_transform(Xt=Xt_new) @@ -210,6 +226,14 @@ def test_sinkhorn_transport_class(): transp_Xt = otda.inverse_transform(Xt=Xt) assert_equal(transp_Xt.shape, Xt.shape) + # check label propagation + transp_yt = otda.transform_labels(ys) + assert_equal(transp_yt.shape[0], yt.shape[0]) + + # check inverse label propagation + transp_ys = otda.inverse_transform_labels(yt) + assert_equal(transp_ys.shape[0], ys.shape[0]) + Xt_new, _ = make_data_classif('3gauss2', nt + 1) transp_Xt_new = otda.inverse_transform(Xt=Xt_new) @@ -271,6 +295,14 @@ def test_unbalanced_sinkhorn_transport_class(): transp_Xs = otda.transform(Xs=Xs) assert_equal(transp_Xs.shape, Xs.shape) + # check label propagation + transp_yt = otda.transform_labels(ys) + assert_equal(transp_yt.shape[0], yt.shape[0]) + + # check inverse label propagation + transp_ys = otda.inverse_transform_labels(yt) + assert_equal(transp_ys.shape[0], ys.shape[0]) + Xs_new, _ = make_data_classif('3gauss', ns + 1) transp_Xs_new = otda.transform(Xs_new) @@ -353,6 +385,14 @@ def test_emd_transport_class(): transp_Xt = otda.inverse_transform(Xt=Xt) assert_equal(transp_Xt.shape, Xt.shape) + # check label propagation + transp_yt = otda.transform_labels(ys) + assert_equal(transp_yt.shape[0], yt.shape[0]) + + # check inverse label propagation + transp_ys = otda.inverse_transform_labels(yt) + assert_equal(transp_ys.shape[0], ys.shape[0]) + Xt_new, _ = make_data_classif('3gauss2', nt + 1) transp_Xt_new = otda.inverse_transform(Xt=Xt_new) @@ -602,6 +642,13 @@ def test_jcpot_transport_class(): # check that the oos method is working assert_equal(transp_Xs_new.shape, Xs_new.shape) + # check label propagation + transp_yt = otda.transform_labels(ys) + assert_equal(transp_yt.shape[0], yt.shape[0]) + + # check inverse label propagation + transp_ys = otda.inverse_transform_labels(yt) + [assert_equal(x.shape, y.shape) for x, y in zip(transp_ys, ys)] def test_jcpot_barycenter(): """test_jcpot_barycenter -- cgit v1.2.3 From 1a4c264cc9b2cb0bb89840ee9175177e86eef3ef Mon Sep 17 00:00:00 2001 From: ievred Date: Wed, 8 Apr 2020 16:34:39 +0200 Subject: added label normalization to utils --- ot/da.py | 75 ++++++++++++++++++++++++++++----------------------------- ot/utils.py | 22 +++++++++++++++++ test/test_da.py | 1 + 3 files changed, 60 insertions(+), 38 deletions(-) (limited to 'test') diff --git a/ot/da.py b/ot/da.py index 29b0a8b..4318c0d 100644 --- a/ot/da.py +++ b/ot/da.py @@ -16,7 +16,7 @@ import scipy.linalg as linalg from .bregman import sinkhorn, jcpot_barycenter from .lp import emd -from .utils import unif, dist, kernel, cost_normalization +from .utils import unif, dist, kernel, cost_normalization, label_normalization from .utils import check_params, BaseEstimator from .unbalanced import sinkhorn_unbalanced from .optim import cg @@ -786,6 +786,9 @@ class BaseTransport(BaseEstimator): transform method should always get as input a Xs parameter inverse_transform method should always get as input a Xt parameter + + transform_labels method should always get as input a ys parameter + inverse_transform_labels method should always get as input a yt parameter """ def fit(self, Xs=None, ys=None, Xt=None, yt=None): @@ -944,7 +947,7 @@ class BaseTransport(BaseEstimator): return transp_Xs def transform_labels(self, ys=None): - """Propagate source labels ys to obtain estimated target labels + """Propagate source labels ys to obtain estimated target labels as in [27] Parameters ---------- @@ -955,14 +958,23 @@ class BaseTransport(BaseEstimator): ------- transp_ys : array-like, shape (n_target_samples,) Estimated target labels. + + References + ---------- + + .. [27] Ievgen Redko, Nicolas Courty, Rémi Flamary, Devis Tuia + "Optimal transport for multi-source domain adaptation under target shift", + International Conference on Artificial Intelligence and Statistics (AISTATS), 2019. + """ # check the necessary inputs parameters are here if check_params(ys=ys): - classes = np.unique(ys) + ysTemp = label_normalization(np.copy(ys)) + classes = np.unique(ysTemp) n = len(classes) - D1 = np.zeros((n, len(ys))) + D1 = np.zeros((n, len(ysTemp))) # perform label propagation transp = self.coupling_ / np.sum(self.coupling_, 1)[:, None] @@ -970,18 +982,13 @@ class BaseTransport(BaseEstimator): # set nans to 0 transp[~ np.isfinite(transp)] = 0 - if np.min(classes) != 0: - ys = ys - np.min(classes) - classes = np.unique(ys) - for c in classes: - D1[int(c), ys == c] = 1 + D1[int(c), ysTemp == c] = 1 # compute transported samples transp_ys = np.dot(D1, transp) - return np.argmax(transp_ys,axis=0) - + return np.argmax(transp_ys, axis=0) def inverse_transform(self, Xs=None, ys=None, Xt=None, yt=None, batch_size=128): @@ -1066,9 +1073,10 @@ class BaseTransport(BaseEstimator): # check the necessary inputs parameters are here if check_params(yt=yt): - classes = np.unique(yt) + ytTemp = label_normalization(np.copy(yt)) + classes = np.unique(ytTemp) n = len(classes) - D1 = np.zeros((n, len(yt))) + D1 = np.zeros((n, len(ytTemp))) # perform label propagation transp = self.coupling_ / np.sum(self.coupling_, 1)[:, None] @@ -1076,17 +1084,13 @@ class BaseTransport(BaseEstimator): # set nans to 0 transp[~ np.isfinite(transp)] = 0 - if np.min(classes) != 0: - yt = yt - np.min(classes) - classes = np.unique(yt) - for c in classes: - D1[int(c), yt == c] = 1 + D1[int(c), ytTemp == c] = 1 # compute transported samples transp_ys = np.dot(D1, transp.T) - return np.argmax(transp_ys,axis=0) + return np.argmax(transp_ys, axis=0) class LinearTransport(BaseTransport): @@ -2163,7 +2167,7 @@ class JCPOTTransport(BaseTransport): return transp_Xs def transform_labels(self, ys=None): - """Propagate source labels ys to obtain target labels + """Propagate source labels ys to obtain target labels as in [27] Parameters ---------- @@ -2178,11 +2182,12 @@ class JCPOTTransport(BaseTransport): # check the necessary inputs parameters are here if check_params(ys=ys): - yt = np.zeros((len(np.unique(np.concatenate(ys))),self.xt_.shape[0])) + yt = np.zeros((len(np.unique(np.concatenate(ys))), self.xt_.shape[0])) for i in range(len(ys)): - classes = np.unique(ys[i]) + ysTemp = label_normalization(np.copy(ys[i])) + classes = np.unique(ysTemp) n = len(classes) - ns = len(ys[i]) + ns = len(ysTemp) # perform label propagation transp = self.coupling_[i] / np.sum(self.coupling_[i], 1)[:, None] @@ -2195,16 +2200,13 @@ class JCPOTTransport(BaseTransport): else: D1 = np.zeros((n, ns)) - if np.min(classes) != 0: - ys = ys - np.min(classes) - classes = np.unique(ys) - for c in classes: - D1[int(c), ys == c] = 1 + D1[int(c), ysTemp == c] = 1 + # compute transported samples - yt = yt + np.dot(D1, transp)/len(ys) + yt = yt + np.dot(D1, transp) / len(ys) - return np.argmax(yt,axis=0) + return np.argmax(yt, axis=0) def inverse_transform_labels(self, yt=None): """Propagate source labels ys to obtain target labels @@ -2223,16 +2225,13 @@ class JCPOTTransport(BaseTransport): # check the necessary inputs parameters are here if check_params(yt=yt): transp_ys = [] - classes = np.unique(yt) + ytTemp = label_normalization(np.copy(yt)) + classes = np.unique(ytTemp) n = len(classes) - D1 = np.zeros((n, len(yt))) - - if np.min(classes) != 0: - yt = yt - np.min(classes) - classes = np.unique(yt) + D1 = np.zeros((n, len(ytTemp))) for c in classes: - D1[int(c), yt == c] = 1 + D1[int(c), ytTemp == c] = 1 for i in range(len(self.xs_)): @@ -2243,6 +2242,6 @@ class JCPOTTransport(BaseTransport): transp[~ np.isfinite(transp)] = 0 # compute transported labels - transp_ys.append(np.argmax(np.dot(D1, transp.T),axis=0)) + transp_ys.append(np.argmax(np.dot(D1, transp.T), axis=0)) return transp_ys diff --git a/ot/utils.py b/ot/utils.py index b71458b..c154f99 100644 --- a/ot/utils.py +++ b/ot/utils.py @@ -200,6 +200,28 @@ def dots(*args): return reduce(np.dot, args) +def label_normalization(y, start=0): + """ Transform labels to start at a given value + + Parameters + ---------- + y : array-like, shape (n, ) + The vector of labels to be normalized. + start : int + Desired value for the smallest label in y (default=0) + + Returns + ------- + y : array-like, shape (n1, ) + The input vector of labels normalized according to given start value. + """ + + diff = np.min(np.unique(y)) - start + if diff != 0: + y -= diff + return y + + def fun(f, q_in, q_out): """ Utility function for parmap with no serializing problems """ while True: diff --git a/test/test_da.py b/test/test_da.py index 4eb6de0..d96046d 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -650,6 +650,7 @@ def test_jcpot_transport_class(): transp_ys = otda.inverse_transform_labels(yt) [assert_equal(x.shape, y.shape) for x, y in zip(transp_ys, ys)] + def test_jcpot_barycenter(): """test_jcpot_barycenter """ -- cgit v1.2.3 From 749378a50abd763c87f5cf24a4b2e0dff2a6ec6a Mon Sep 17 00:00:00 2001 From: ievred Date: Wed, 15 Apr 2020 11:12:23 +0200 Subject: fix soft labels, remove gammas from jcpot --- ot/bregman.py | 9 ++++----- ot/da.py | 40 +++++++++++++++++++++------------------- test/test_da.py | 14 +++++++++++++- 3 files changed, 38 insertions(+), 25 deletions(-) (limited to 'test') diff --git a/ot/bregman.py b/ot/bregman.py index c44c141..543dbaa 100644 --- a/ot/bregman.py +++ b/ot/bregman.py @@ -1553,8 +1553,6 @@ def jcpot_barycenter(Xs, Ys, Xt, reg, metric='sqeuclidean', numItermax=100, Returns ------- - gamma : List of K (nsk x nt) ndarrays - Optimal transportation matrices for the given parameters for each pair of source and target domains h : (C,) ndarray proportion estimation in the target domain log : dict @@ -1574,7 +1572,7 @@ def jcpot_barycenter(Xs, Ys, Xt, reg, metric='sqeuclidean', numItermax=100, # log dictionary if log: - log = {'niter': 0, 'err': [], 'M': [], 'D1': [], 'D2': []} + log = {'niter': 0, 'err': [], 'M': [], 'D1': [], 'D2': [], 'gamma': []} K = [] M = [] @@ -1657,9 +1655,10 @@ def jcpot_barycenter(Xs, Ys, Xt, reg, metric='sqeuclidean', numItermax=100, log['M'] = M log['D1'] = D1 log['D2'] = D2 - return K, bary, log + log['gamma'] = K + return bary, log else: - return K, bary + return bary def empirical_sinkhorn(X_s, X_t, reg, a=None, b=None, metric='sqeuclidean', diff --git a/ot/da.py b/ot/da.py index 4318c0d..30e5a61 100644 --- a/ot/da.py +++ b/ot/da.py @@ -956,8 +956,8 @@ class BaseTransport(BaseEstimator): Returns ------- - transp_ys : array-like, shape (n_target_samples,) - Estimated target labels. + transp_ys : array-like, shape (n_target_samples, nb_classes) + Estimated soft target labels. References ---------- @@ -985,10 +985,10 @@ class BaseTransport(BaseEstimator): for c in classes: D1[int(c), ysTemp == c] = 1 - # compute transported samples + # compute propagated labels transp_ys = np.dot(D1, transp) - return np.argmax(transp_ys, axis=0) + return transp_ys.T def inverse_transform(self, Xs=None, ys=None, Xt=None, yt=None, batch_size=128): @@ -1066,8 +1066,8 @@ class BaseTransport(BaseEstimator): Returns ------- - transp_ys : array-like, shape (n_source_samples,) - Estimated source labels. + transp_ys : array-like, shape (n_source_samples, nb_classes) + Estimated soft source labels. """ # check the necessary inputs parameters are here @@ -1087,10 +1087,10 @@ class BaseTransport(BaseEstimator): for c in classes: D1[int(c), ytTemp == c] = 1 - # compute transported samples + # compute propagated samples transp_ys = np.dot(D1, transp.T) - return np.argmax(transp_ys, axis=0) + return transp_ys.T class LinearTransport(BaseTransport): @@ -2083,13 +2083,15 @@ class JCPOTTransport(BaseTransport): returned_ = jcpot_barycenter(Xs=Xs, Ys=ys, Xt=Xt, reg=self.reg_e, metric=self.metric, distrinumItermax=self.max_iter, stopThr=self.tol, - verbose=self.verbose, log=self.log) + verbose=self.verbose, log=True) + + self.coupling_ = returned_[1]['gamma'] # deal with the value of log if self.log: - self.coupling_, self.proportions_, self.log_ = returned_ + self.proportions_, self.log_ = returned_ else: - self.coupling_, self.proportions_ = returned_ + self.proportions_ = returned_ self.log_ = dict() return self @@ -2176,8 +2178,8 @@ class JCPOTTransport(BaseTransport): Returns ------- - yt : array-like, shape (n_target_samples,) - Estimated target labels. + yt : array-like, shape (n_target_samples, nb_classes) + Estimated soft target labels. """ # check the necessary inputs parameters are here @@ -2203,10 +2205,10 @@ class JCPOTTransport(BaseTransport): for c in classes: D1[int(c), ysTemp == c] = 1 - # compute transported samples + # compute propagated labels yt = yt + np.dot(D1, transp) / len(ys) - return np.argmax(yt, axis=0) + return yt.T def inverse_transform_labels(self, yt=None): """Propagate source labels ys to obtain target labels @@ -2218,8 +2220,8 @@ class JCPOTTransport(BaseTransport): Returns ------- - transp_ys : list of K array-like objects, shape K x (nk_source_samples,) - A list of estimated source labels + transp_ys : list of K array-like objects, shape K x (nk_source_samples, nb_classes) + A list of estimated soft source labels """ # check the necessary inputs parameters are here @@ -2241,7 +2243,7 @@ class JCPOTTransport(BaseTransport): # set nans to 0 transp[~ np.isfinite(transp)] = 0 - # compute transported labels - transp_ys.append(np.argmax(np.dot(D1, transp.T), axis=0)) + # compute propagated labels + transp_ys.append(np.dot(D1, transp.T).T) return transp_ys diff --git a/test/test_da.py b/test/test_da.py index d96046d..70296bf 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -68,10 +68,12 @@ def test_sinkhorn_lpl1_transport_class(): # check label propagation transp_yt = otda.transform_labels(ys) assert_equal(transp_yt.shape[0], yt.shape[0]) + assert_equal(transp_yt.shape[1], len(np.unique(ys))) # check inverse label propagation transp_ys = otda.inverse_transform_labels(yt) assert_equal(transp_ys.shape[0], ys.shape[0]) + assert_equal(transp_ys.shape[1], len(np.unique(yt))) # test unsupervised vs semi-supervised mode otda_unsup = ot.da.SinkhornLpl1Transport() @@ -140,10 +142,12 @@ def test_sinkhorn_l1l2_transport_class(): # check label propagation transp_yt = otda.transform_labels(ys) assert_equal(transp_yt.shape[0], yt.shape[0]) + assert_equal(transp_yt.shape[1], len(np.unique(ys))) # check inverse label propagation transp_ys = otda.inverse_transform_labels(yt) assert_equal(transp_ys.shape[0], ys.shape[0]) + assert_equal(transp_ys.shape[1], len(np.unique(yt))) Xt_new, _ = make_data_classif('3gauss2', nt + 1) transp_Xt_new = otda.inverse_transform(Xt=Xt_new) @@ -229,10 +233,12 @@ def test_sinkhorn_transport_class(): # check label propagation transp_yt = otda.transform_labels(ys) assert_equal(transp_yt.shape[0], yt.shape[0]) + assert_equal(transp_yt.shape[1], len(np.unique(ys))) # check inverse label propagation transp_ys = otda.inverse_transform_labels(yt) assert_equal(transp_ys.shape[0], ys.shape[0]) + assert_equal(transp_ys.shape[1], len(np.unique(yt))) Xt_new, _ = make_data_classif('3gauss2', nt + 1) transp_Xt_new = otda.inverse_transform(Xt=Xt_new) @@ -298,10 +304,12 @@ def test_unbalanced_sinkhorn_transport_class(): # check label propagation transp_yt = otda.transform_labels(ys) assert_equal(transp_yt.shape[0], yt.shape[0]) + assert_equal(transp_yt.shape[1], len(np.unique(ys))) # check inverse label propagation transp_ys = otda.inverse_transform_labels(yt) assert_equal(transp_ys.shape[0], ys.shape[0]) + assert_equal(transp_ys.shape[1], len(np.unique(yt))) Xs_new, _ = make_data_classif('3gauss', ns + 1) transp_Xs_new = otda.transform(Xs_new) @@ -388,10 +396,12 @@ def test_emd_transport_class(): # check label propagation transp_yt = otda.transform_labels(ys) assert_equal(transp_yt.shape[0], yt.shape[0]) + assert_equal(transp_yt.shape[1], len(np.unique(ys))) # check inverse label propagation transp_ys = otda.inverse_transform_labels(yt) assert_equal(transp_ys.shape[0], ys.shape[0]) + assert_equal(transp_ys.shape[1], len(np.unique(yt))) Xt_new, _ = make_data_classif('3gauss2', nt + 1) transp_Xt_new = otda.inverse_transform(Xt=Xt_new) @@ -645,10 +655,12 @@ def test_jcpot_transport_class(): # check label propagation transp_yt = otda.transform_labels(ys) assert_equal(transp_yt.shape[0], yt.shape[0]) + assert_equal(transp_yt.shape[1], len(np.unique(ys))) # check inverse label propagation transp_ys = otda.inverse_transform_labels(yt) - [assert_equal(x.shape, y.shape) for x, y in zip(transp_ys, ys)] + [assert_equal(x.shape[0], y.shape[0]) for x, y in zip(transp_ys, ys)] + [assert_equal(x.shape[1], len(np.unique(y))) for x, y in zip(transp_ys, ys)] def test_jcpot_barycenter(): -- cgit v1.2.3 From 54a129f8f17cbdbfa03c3caa296f99423536cc32 Mon Sep 17 00:00:00 2001 From: ievred Date: Wed, 15 Apr 2020 11:20:14 +0200 Subject: fix jcpot_bary test --- test/test_da.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'test') diff --git a/test/test_da.py b/test/test_da.py index 70296bf..472dc19 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -685,7 +685,7 @@ def test_jcpot_barycenter(): Xs = [Xs1, Xs2] ys = [ys1, ys2] - _, prop, = ot.bregman.jcpot_barycenter(Xs, ys, Xt, reg=.5, metric='sqeuclidean', + prop = ot.bregman.jcpot_barycenter(Xs, ys, Xt, reg=.5, metric='sqeuclidean', numItermax=10000, stopThr=1e-9, verbose=False, log=False) np.testing.assert_allclose(prop, [1 - pt, pt], rtol=1e-3, atol=1e-3) -- cgit v1.2.3 From 7889484b79a425ebf3632444547a6092e814bf20 Mon Sep 17 00:00:00 2001 From: ievred Date: Wed, 15 Apr 2020 11:27:25 +0200 Subject: fix indent test_da --- test/test_da.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'test') diff --git a/test/test_da.py b/test/test_da.py index 472dc19..7d0fdda 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -686,6 +686,6 @@ def test_jcpot_barycenter(): ys = [ys1, ys2] prop = ot.bregman.jcpot_barycenter(Xs, ys, Xt, reg=.5, metric='sqeuclidean', - numItermax=10000, stopThr=1e-9, verbose=False, log=False) + numItermax=10000, stopThr=1e-9, verbose=False, log=False) np.testing.assert_allclose(prop, [1 - pt, pt], rtol=1e-3, atol=1e-3) -- cgit v1.2.3 From 13444cabb8318a7759e2d0941baf4aba67308a51 Mon Sep 17 00:00:00 2001 From: Laetitia Chapel Date: Wed, 15 Apr 2020 15:35:16 +0200 Subject: partial with tests --- test/test_partial.py | 141 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100755 test/test_partial.py (limited to 'test') diff --git a/test/test_partial.py b/test/test_partial.py new file mode 100755 index 0000000..fbcd3c2 --- /dev/null +++ b/test/test_partial.py @@ -0,0 +1,141 @@ +"""Tests for module partial """ + +# Author: +# Laetitia Chapel +# +# License: MIT License + +import numpy as np +import scipy as sp +import ot + + +def test_partial_wasserstein(): + + n_samples = 20 # nb samples (gaussian) + n_noise = 20 # nb of samples (noise) + + mu = np.array([0, 0]) + cov = np.array([[1, 0], [0, 2]]) + + xs = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov) + xs = np.append(xs, (np.random.rand(n_noise, 2) + 1) * 4).reshape((-1, 2)) + xt = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov) + xt = np.append(xt, (np.random.rand(n_noise, 2) + 1) * -3).reshape((-1, 2)) + + M = ot.dist(xs, xt) + + p = ot.unif(n_samples + n_noise) + q = ot.unif(n_samples + n_noise) + + m = 0.5 + + w0, log0 = ot.partial.partial_wasserstein(p, q, M, m=m, log=True) + w, log = ot.partial.entropic_partial_wasserstein(p, q, M, reg=1, m=m, + log=True) + + # check constratints + np.testing.assert_equal( + w0.sum(1) - p <= 1e-5, [True] * len(p)) # cf convergence wasserstein + np.testing.assert_equal( + w0.sum(0) - q <= 1e-5, [True] * len(q)) # cf convergence wasserstein + np.testing.assert_equal( + w.sum(1) - p <= 1e-5, [True] * len(p)) # cf convergence wasserstein + np.testing.assert_equal( + w.sum(0) - q <= 1e-5, [True] * len(q)) # cf convergence wasserstein + + # check transported mass + np.testing.assert_allclose( + np.sum(w0), m, atol=1e-04) + np.testing.assert_allclose( + np.sum(w), m, atol=1e-04) + + w0, log0 = ot.partial.partial_wasserstein2(p, q, M, m=m, log=True) + w0_val = ot.partial.partial_wasserstein2(p, q, M, m=m, log=False) + + G = log0['T'] + + np.testing.assert_allclose(w0, w0_val, atol=1e-1, rtol=1e-1) + + # check constratints + np.testing.assert_equal( + G.sum(1) <= p, [True] * len(p)) # cf convergence wasserstein + np.testing.assert_equal( + G.sum(0) <= q, [True] * len(q)) # cf convergence wasserstein + np.testing.assert_allclose( + np.sum(G), m, atol=1e-04) + + +def test_partial_gromov_wasserstein(): + n_samples = 20 # nb samples + n_noise = 10 # nb of samples (noise) + + p = ot.unif(n_samples + n_noise) + q = ot.unif(n_samples + n_noise) + + mu_s = np.array([0, 0]) + cov_s = np.array([[1, 0], [0, 1]]) + + mu_t = np.array([0, 0, 0]) + cov_t = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) + + xs = ot.datasets.make_2D_samples_gauss(n_samples, mu_s, cov_s) + xs = np.concatenate((xs, ((np.random.rand(n_noise, 2) + 1) * 4)), axis=0) + P = sp.linalg.sqrtm(cov_t) + xt = np.random.randn(n_samples, 3).dot(P) + mu_t + xt = np.concatenate((xt, ((np.random.rand(n_noise, 3) + 1) * 10)), axis=0) + xt2 = xs[::-1].copy() + + C1 = ot.dist(xs, xs) + C2 = ot.dist(xt, xt) + C3 = ot.dist(xt2, xt2) + + m = 2 / 3 + res0, log0 = ot.partial.partial_gromov_wasserstein(C1, C3, p, q, m=m, + log=True) + res, log = ot.partial.entropic_partial_gromov_wasserstein(C1, C3, p, q, 10, + m=m, log=True) + np.testing.assert_allclose(res0, 0, atol=1e-1, rtol=1e-1) + np.testing.assert_allclose(res, 0, atol=1e-1, rtol=1e-1) + + C1 = sp.spatial.distance.cdist(xs, xs) + C2 = sp.spatial.distance.cdist(xt, xt) + + m = 1 + res0, log0 = ot.partial.partial_gromov_wasserstein(C1, C2, p, q, m=m, + log=True) + G = ot.gromov.gromov_wasserstein(C1, C2, p, q, 'square_loss') + np.testing.assert_allclose(G, res0, atol=1e-04) + + res, log = ot.partial.entropic_partial_gromov_wasserstein(C1, C2, p, q, 10, + m=m, log=True) + G = ot.gromov.entropic_gromov_wasserstein( + C1, C2, p, q, 'square_loss', epsilon=10) + np.testing.assert_allclose(G, res, atol=1e-02) + + w0, log0 = ot.partial.partial_gromov_wasserstein2(C1, C2, p, q, m=m, + log=True) + w0_val = ot.partial.partial_gromov_wasserstein2(C1, C2, p, q, m=m, + log=False) + G = log0['T'] + np.testing.assert_allclose(w0, w0_val, atol=1e-1, rtol=1e-1) + + m = 2 / 3 + res0, log0 = ot.partial.partial_gromov_wasserstein(C1, C2, p, q, m=m, + log=True) + res, log = ot.partial.entropic_partial_gromov_wasserstein(C1, C2, p, q, 10, + m=m, log=True) + # check constratints + np.testing.assert_equal( + res0.sum(1) <= p, [True] * len(p)) # cf convergence wasserstein + np.testing.assert_equal( + res0.sum(0) <= q, [True] * len(q)) # cf convergence wasserstein + np.testing.assert_allclose( + np.sum(res0), m, atol=1e-04) + + np.testing.assert_equal( + res.sum(1) <= p, [True] * len(p)) # cf convergence wasserstein + np.testing.assert_equal( + res.sum(0) <= q, [True] * len(q)) # cf convergence wasserstein + np.testing.assert_allclose( + np.sum(res), m, atol=1e-04) -- cgit v1.2.3 From 2571a3ead11a7fc010ed20b1af6faeef464565a1 Mon Sep 17 00:00:00 2001 From: ievred Date: Wed, 15 Apr 2020 17:00:32 +0200 Subject: conflict test_da --- test/test_da.py | 132 ++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 110 insertions(+), 22 deletions(-) (limited to 'test') diff --git a/test/test_da.py b/test/test_da.py index befec43..7d0fdda 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -65,6 +65,16 @@ def test_sinkhorn_lpl1_transport_class(): transp_Xs = otda.fit_transform(Xs=Xs, ys=ys, Xt=Xt) assert_equal(transp_Xs.shape, Xs.shape) + # check label propagation + transp_yt = otda.transform_labels(ys) + assert_equal(transp_yt.shape[0], yt.shape[0]) + assert_equal(transp_yt.shape[1], len(np.unique(ys))) + + # check inverse label propagation + transp_ys = otda.inverse_transform_labels(yt) + assert_equal(transp_ys.shape[0], ys.shape[0]) + assert_equal(transp_ys.shape[1], len(np.unique(yt))) + # test unsupervised vs semi-supervised mode otda_unsup = ot.da.SinkhornLpl1Transport() otda_unsup.fit(Xs=Xs, ys=ys, Xt=Xt) @@ -129,6 +139,16 @@ def test_sinkhorn_l1l2_transport_class(): transp_Xt = otda.inverse_transform(Xt=Xt) assert_equal(transp_Xt.shape, Xt.shape) + # check label propagation + transp_yt = otda.transform_labels(ys) + assert_equal(transp_yt.shape[0], yt.shape[0]) + assert_equal(transp_yt.shape[1], len(np.unique(ys))) + + # check inverse label propagation + transp_ys = otda.inverse_transform_labels(yt) + assert_equal(transp_ys.shape[0], ys.shape[0]) + assert_equal(transp_ys.shape[1], len(np.unique(yt))) + Xt_new, _ = make_data_classif('3gauss2', nt + 1) transp_Xt_new = otda.inverse_transform(Xt=Xt_new) @@ -210,6 +230,16 @@ def test_sinkhorn_transport_class(): transp_Xt = otda.inverse_transform(Xt=Xt) assert_equal(transp_Xt.shape, Xt.shape) + # check label propagation + transp_yt = otda.transform_labels(ys) + assert_equal(transp_yt.shape[0], yt.shape[0]) + assert_equal(transp_yt.shape[1], len(np.unique(ys))) + + # check inverse label propagation + transp_ys = otda.inverse_transform_labels(yt) + assert_equal(transp_ys.shape[0], ys.shape[0]) + assert_equal(transp_ys.shape[1], len(np.unique(yt))) + Xt_new, _ = make_data_classif('3gauss2', nt + 1) transp_Xt_new = otda.inverse_transform(Xt=Xt_new) @@ -271,6 +301,16 @@ def test_unbalanced_sinkhorn_transport_class(): transp_Xs = otda.transform(Xs=Xs) assert_equal(transp_Xs.shape, Xs.shape) + # check label propagation + transp_yt = otda.transform_labels(ys) + assert_equal(transp_yt.shape[0], yt.shape[0]) + assert_equal(transp_yt.shape[1], len(np.unique(ys))) + + # check inverse label propagation + transp_ys = otda.inverse_transform_labels(yt) + assert_equal(transp_ys.shape[0], ys.shape[0]) + assert_equal(transp_ys.shape[1], len(np.unique(yt))) + Xs_new, _ = make_data_classif('3gauss', ns + 1) transp_Xs_new = otda.transform(Xs_new) @@ -353,6 +393,16 @@ def test_emd_transport_class(): transp_Xt = otda.inverse_transform(Xt=Xt) assert_equal(transp_Xt.shape, Xt.shape) + # check label propagation + transp_yt = otda.transform_labels(ys) + assert_equal(transp_yt.shape[0], yt.shape[0]) + assert_equal(transp_yt.shape[1], len(np.unique(ys))) + + # check inverse label propagation + transp_ys = otda.inverse_transform_labels(yt) + assert_equal(transp_ys.shape[0], ys.shape[0]) + assert_equal(transp_ys.shape[1], len(np.unique(yt))) + Xt_new, _ = make_data_classif('3gauss2', nt + 1) transp_Xt_new = otda.inverse_transform(Xt=Xt_new) @@ -549,55 +599,93 @@ def test_linear_mapping_class(): np.testing.assert_allclose(Ct, Cst, rtol=1e-2, atol=1e-2) -def test_emd_laplace_class(): - """test_emd_laplace_transport +def test_jcpot_transport_class(): + """test_jcpot_transport """ - ns = 150 + + ns1 = 150 + ns2 = 150 nt = 200 - Xs, ys = make_data_classif('3gauss', ns) + Xs1, ys1 = make_data_classif('3gauss', ns1) + Xs2, ys2 = make_data_classif('3gauss', ns2) + Xt, yt = make_data_classif('3gauss2', nt) - otda = ot.da.EMDLaplaceTransport(reg_lap=0.01, max_iter=1000, tol=1e-9, verbose=False, log=True) + Xs = [Xs1, Xs2] + ys = [ys1, ys2] + + otda = ot.da.JCPOTTransport(reg_e=1, max_iter=10000, tol=1e-9, verbose=True, log=True) # test its computed otda.fit(Xs=Xs, ys=ys, Xt=Xt) assert hasattr(otda, "coupling_") + assert hasattr(otda, "proportions_") assert hasattr(otda, "log_") # test dimensions of coupling - assert_equal(otda.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) + for i, xs in enumerate(Xs): + assert_equal(otda.coupling_[i].shape, ((xs.shape[0], Xt.shape[0]))) # test all margin constraints - mu_s = unif(ns) mu_t = unif(nt) - assert_allclose( - np.sum(otda.coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) - assert_allclose( - np.sum(otda.coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + for i in range(len(Xs)): + # test margin constraints w.r.t. uniform target weights for each coupling matrix + assert_allclose( + np.sum(otda.coupling_[i], axis=0), mu_t, rtol=1e-3, atol=1e-3) + + # test margin constraints w.r.t. modified source weights for each source domain + + assert_allclose( + np.dot(otda.log_['D1'][i], np.sum(otda.coupling_[i], axis=1)), otda.proportions_, rtol=1e-3, + atol=1e-3) # test transform transp_Xs = otda.transform(Xs=Xs) [assert_equal(x.shape, y.shape) for x, y in zip(transp_Xs, Xs)] - Xs_new, _ = make_data_classif('3gauss', ns + 1) + Xs_new, _ = make_data_classif('3gauss', ns1 + 1) transp_Xs_new = otda.transform(Xs_new) # check that the oos method is working assert_equal(transp_Xs_new.shape, Xs_new.shape) - # test inverse transform - transp_Xt = otda.inverse_transform(Xt=Xt) - assert_equal(transp_Xt.shape, Xt.shape) + # check label propagation + transp_yt = otda.transform_labels(ys) + assert_equal(transp_yt.shape[0], yt.shape[0]) + assert_equal(transp_yt.shape[1], len(np.unique(ys))) - Xt_new, _ = make_data_classif('3gauss2', nt + 1) - transp_Xt_new = otda.inverse_transform(Xt=Xt_new) + # check inverse label propagation + transp_ys = otda.inverse_transform_labels(yt) + [assert_equal(x.shape[0], y.shape[0]) for x, y in zip(transp_ys, ys)] + [assert_equal(x.shape[1], len(np.unique(y))) for x, y in zip(transp_ys, ys)] - # check that the oos method is working - assert_equal(transp_Xt_new.shape, Xt_new.shape) - # test fit_transform - transp_Xs = otda.fit_transform(Xs=Xs, Xt=Xt) - assert_equal(transp_Xs.shape, Xs.shape) +def test_jcpot_barycenter(): + """test_jcpot_barycenter + """ + + ns1 = 150 + ns2 = 150 + nt = 200 + + sigma = 0.1 + np.random.seed(1985) + + ps1 = .2 + ps2 = .9 + pt = .4 + + Xs1, ys1 = make_data_classif('2gauss_prop', ns1, nz=sigma, p=ps1) + Xs2, ys2 = make_data_classif('2gauss_prop', ns2, nz=sigma, p=ps2) + Xt, yt = make_data_classif('2gauss_prop', nt, nz=sigma, p=pt) + + Xs = [Xs1, Xs2] + ys = [ys1, ys2] + + prop = ot.bregman.jcpot_barycenter(Xs, ys, Xt, reg=.5, metric='sqeuclidean', + numItermax=10000, stopThr=1e-9, verbose=False, log=False) + + np.testing.assert_allclose(prop, [1 - pt, pt], rtol=1e-3, atol=1e-3) -- cgit v1.2.3 From 1c60175fee4eb7f29b49f693e91f59720369edb1 Mon Sep 17 00:00:00 2001 From: ievred Date: Wed, 15 Apr 2020 17:06:02 +0200 Subject: conflict test_da --- test/test_da.py | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) (limited to 'test') diff --git a/test/test_da.py b/test/test_da.py index 7d0fdda..3b28119 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -689,3 +689,67 @@ def test_jcpot_barycenter(): numItermax=10000, stopThr=1e-9, verbose=False, log=False) np.testing.assert_allclose(prop, [1 - pt, pt], rtol=1e-3, atol=1e-3) + + +def test_emd_laplace_class(): + """test_emd_laplace_transport + """ + ns = 150 + nt = 200 + + Xs, ys = make_data_classif('3gauss', ns) + Xt, yt = make_data_classif('3gauss2', nt) + + otda = ot.da.EMDLaplaceTransport(reg_lap=0.01, max_iter=1000, tol=1e-9, verbose=False, log=True) + + # test its computed + otda.fit(Xs=Xs, ys=ys, Xt=Xt) + + assert hasattr(otda, "coupling_") + assert hasattr(otda, "log_") + + # test dimensions of coupling + assert_equal(otda.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) + + # test all margin constraints + mu_s = unif(ns) + mu_t = unif(nt) + + assert_allclose( + np.sum(otda.coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) + assert_allclose( + np.sum(otda.coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + + # test transform + transp_Xs = otda.transform(Xs=Xs) + [assert_equal(x.shape, y.shape) for x, y in zip(transp_Xs, Xs)] + + Xs_new, _ = make_data_classif('3gauss', ns + 1) + transp_Xs_new = otda.transform(Xs_new) + + # check that the oos method is working + assert_equal(transp_Xs_new.shape, Xs_new.shape) + + # test inverse transform + transp_Xt = otda.inverse_transform(Xt=Xt) + assert_equal(transp_Xt.shape, Xt.shape) + + Xt_new, _ = make_data_classif('3gauss2', nt + 1) + transp_Xt_new = otda.inverse_transform(Xt=Xt_new) + + # check that the oos method is working + assert_equal(transp_Xt_new.shape, Xt_new.shape) + + # test fit_transform + transp_Xs = otda.fit_transform(Xs=Xs, Xt=Xt) + assert_equal(transp_Xs.shape, Xs.shape) + + # check label propagation + transp_yt = otda.transform_labels(ys) + assert_equal(transp_yt.shape[0], yt.shape[0]) + assert_equal(transp_yt.shape[1], len(np.unique(ys))) + + # check inverse label propagation + transp_ys = otda.inverse_transform_labels(yt) + assert_equal(transp_ys.shape[0], ys.shape[0]) + assert_equal(transp_ys.shape[1], len(np.unique(yt))) -- cgit v1.2.3 From ef7c11a5df3cf6c82864472f0cfa65d6b2036f2f Mon Sep 17 00:00:00 2001 From: Laetitia Chapel Date: Thu, 16 Apr 2020 15:52:00 +0200 Subject: partial with python 3.8 --- .travis.yml | 2 +- ot/partial.py | 12 ++++++------ test/test_partial.py | 9 ++++----- 3 files changed, 11 insertions(+), 12 deletions(-) (limited to 'test') diff --git a/.travis.yml b/.travis.yml index 072bc55..7ff1b3c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ matrix: python: 3.7 - os: linux sudo: required - python: 2.7 + python: 3.8 # - os: osx # sudo: required # language: generic diff --git a/ot/partial.py b/ot/partial.py index 8698d9d..726a590 100755 --- a/ot/partial.py +++ b/ot/partial.py @@ -232,7 +232,7 @@ def partial_wasserstein(a, b, M, m=None, nb_dummies=1, log=False, **kwargs): b_extended = np.append(b, [(np.sum(a) - m) / nb_dummies] * nb_dummies) a_extended = np.append(a, [(np.sum(b) - m) / nb_dummies] * nb_dummies) - M_extended = np.ones((len(a_extended), len(b_extended))) * 0 + M_extended = np.zeros((len(a_extended), len(b_extended))) M_extended[-1, -1] = np.max(M) * 1e5 M_extended[:len(a), :len(b)] = M @@ -510,9 +510,9 @@ def partial_gromov_wasserstein(C1, C2, p, q, m=None, nb_dummies=1, G0=None, Gprev = G0 M = gwgrad_partial(C1, C2, G0) - M[M < eps] = np.quantile(M[M > eps], thres) + M[M < eps] = np.quantile(M, thres) - M_emd = np.ones(dim_G_extended) * np.max(M) * 1e2 + M_emd = np.zeros(dim_G_extended) M_emd[:len(p), :len(q)] = M M_emd[-nb_dummies:, -nb_dummies:] = np.max(M) * 1e5 M_emd = np.asarray(M_emd, dtype=np.float64) @@ -729,8 +729,8 @@ def entropic_partial_wasserstein(a, b, M, reg, m=None, numItermax=1000, M = np.asarray(M, dtype=np.float64) dim_a, dim_b = M.shape - dx = np.ones(dim_a) - dy = np.ones(dim_b) + dx = np.ones(dim_a, dtype=np.float64) + dy = np.ones(dim_b, dtype=np.float64) if len(a) == 0: a = np.ones(dim_a, dtype=np.float64) / dim_a @@ -738,7 +738,7 @@ def entropic_partial_wasserstein(a, b, M, reg, m=None, numItermax=1000, b = np.ones(dim_b, dtype=np.float64) / dim_b if m is None: - m = np.min((np.sum(a), np.sum(b))) + m = np.min((np.sum(a), np.sum(b))) * 1.0 if m < 0: raise ValueError("Problem infeasible. Parameter m should be greater" " than 0.") diff --git a/test/test_partial.py b/test/test_partial.py index fbcd3c2..1799fd4 100755 --- a/test/test_partial.py +++ b/test/test_partial.py @@ -93,10 +93,7 @@ def test_partial_gromov_wasserstein(): m = 2 / 3 res0, log0 = ot.partial.partial_gromov_wasserstein(C1, C3, p, q, m=m, log=True) - res, log = ot.partial.entropic_partial_gromov_wasserstein(C1, C3, p, q, 10, - m=m, log=True) np.testing.assert_allclose(res0, 0, atol=1e-1, rtol=1e-1) - np.testing.assert_allclose(res, 0, atol=1e-1, rtol=1e-1) C1 = sp.spatial.distance.cdist(xs, xs) C2 = sp.spatial.distance.cdist(xt, xt) @@ -123,8 +120,10 @@ def test_partial_gromov_wasserstein(): m = 2 / 3 res0, log0 = ot.partial.partial_gromov_wasserstein(C1, C2, p, q, m=m, log=True) - res, log = ot.partial.entropic_partial_gromov_wasserstein(C1, C2, p, q, 10, - m=m, log=True) + res, log = ot.partial.entropic_partial_gromov_wasserstein(C1, C2, p, q, + 100, m=m, + log=True) + # check constratints np.testing.assert_equal( res0.sum(1) <= p, [True] * len(p)) # cf convergence wasserstein -- cgit v1.2.3 From 47306ad23d0c9943c14149ffd85d1c3d0544a3df Mon Sep 17 00:00:00 2001 From: Laetitia Chapel Date: Thu, 16 Apr 2020 16:25:16 +0200 Subject: partial with python 3.8 --- test/test_partial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'test') diff --git a/test/test_partial.py b/test/test_partial.py index 1799fd4..ce363bd 100755 --- a/test/test_partial.py +++ b/test/test_partial.py @@ -123,7 +123,7 @@ def test_partial_gromov_wasserstein(): res, log = ot.partial.entropic_partial_gromov_wasserstein(C1, C2, p, q, 100, m=m, log=True) - + # check constratints np.testing.assert_equal( res0.sum(1) <= p, [True] * len(p)) # cf convergence wasserstein -- cgit v1.2.3 From d2ecce4a79228cd10f4beba8b6b2b28239be796d Mon Sep 17 00:00:00 2001 From: Laetitia Chapel Date: Thu, 16 Apr 2020 16:42:59 +0200 Subject: partial with python 3.8 --- test/test_partial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'test') diff --git a/test/test_partial.py b/test/test_partial.py index ce363bd..8b1ca89 100755 --- a/test/test_partial.py +++ b/test/test_partial.py @@ -123,7 +123,7 @@ def test_partial_gromov_wasserstein(): res, log = ot.partial.entropic_partial_gromov_wasserstein(C1, C2, p, q, 100, m=m, log=True) - + # check constratints np.testing.assert_equal( res0.sum(1) <= p, [True] * len(p)) # cf convergence wasserstein -- cgit v1.2.3 From ef12867f1425ee86b3cfddef4287b52d46114e83 Mon Sep 17 00:00:00 2001 From: Nicolas Courty Date: Thu, 23 Apr 2020 13:03:28 +0200 Subject: [WIP] Issue with sparse emd and adding tests on macos (#158) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * First commit-warning removal * remove dense feature * pep8 * pep8 * EMD.h * pep8 again * tic toc tolerance Co-authored-by: Rémi Flamary --- .github/workflows/pythonpackage.yml | 48 +++++----- ot/lp/EMD.h | 3 - ot/lp/EMD_wrapper.cpp | 182 ------------------------------------ ot/lp/__init__.py | 45 +++------ ot/lp/emd_wrap.pyx | 38 ++------ ot/lp/network_simplex_simple.h | 5 +- test/test_ot.py | 26 ------ test/test_utils.py | 4 +- 8 files changed, 46 insertions(+), 305 deletions(-) (limited to 'test') diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 394f453..9c35afa 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -47,31 +47,31 @@ jobs: run: | codecov - # macos: - # runs-on: macOS-latest - # strategy: - # max-parallel: 4 - # matrix: - # python-version: [3.7] + macos: + runs-on: macOS-latest + strategy: + max-parallel: 4 + matrix: + python-version: [3.7] - # steps: - # - uses: actions/checkout@v1 - # - name: Set up Python ${{ matrix.python-version }} - # uses: actions/setup-python@v1 - # with: - # python-version: ${{ matrix.python-version }} - # - name: Install dependencies - # run: | - # python -m pip install --upgrade pip - # pip install -r requirements.txt - # pip install pytest "pytest-cov<2.6" - # pip install -U "sklearn" - # - name: Install POT - # run: | - # pip install -e . - # - name: Run tests - # run: | - # python -m pytest -v test/ ot/ --doctest-modules --ignore ot/gpu/ --cov=ot + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest "pytest-cov<2.6" + pip install -U "sklearn" + - name: Install POT + run: | + pip install -e . + - name: Run tests + run: | + python -m pytest -v test/ ot/ --doctest-modules --ignore ot/gpu/ --cov=ot windows: diff --git a/ot/lp/EMD.h b/ot/lp/EMD.h index 2adaace..c0fe7a3 100644 --- a/ot/lp/EMD.h +++ b/ot/lp/EMD.h @@ -32,9 +32,6 @@ enum ProblemType { int EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double* alpha, double* beta, double *cost, int maxIter); -int EMD_wrap_return_sparse(int n1, int n2, double *X, double *Y, double *D, - long *iG, long *jG, double *G, long * nG, - double* alpha, double* beta, double *cost, int maxIter); #endif diff --git a/ot/lp/EMD_wrapper.cpp b/ot/lp/EMD_wrapper.cpp index 28e4af2..bc873ed 100644 --- a/ot/lp/EMD_wrapper.cpp +++ b/ot/lp/EMD_wrapper.cpp @@ -106,185 +106,3 @@ int EMD_wrap(int n1, int n2, double *X, double *Y, double *D, double *G, return ret; } - -int EMD_wrap_return_sparse(int n1, int n2, double *X, double *Y, double *D, - long *iG, long *jG, double *G, long * nG, - double* alpha, double* beta, double *cost, int maxIter) { - // beware M and C anre strored in row major C style!!! - - // Get the number of non zero coordinates for r and c and vectors - int n, m, i, cur; - - typedef FullBipartiteDigraph Digraph; - DIGRAPH_TYPEDEFS(FullBipartiteDigraph); - - // Get the number of non zero coordinates for r and c - n=0; - for (int i=0; i0) { - n++; - }else if(val<0){ - return INFEASIBLE; - } - } - m=0; - for (int i=0; i0) { - m++; - }else if(val<0){ - return INFEASIBLE; - } - } - - // Define the graph - - std::vector indI(n), indJ(m); - std::vector weights1(n), weights2(m); - Digraph di(n, m); - NetworkSimplexSimple net(di, true, n+m, n*m, maxIter); - - // Set supply and demand, don't account for 0 values (faster) - - cur=0; - for (int i=0; i0) { - weights1[ cur ] = val; - indI[cur++]=i; - } - } - - // Demand is actually negative supply... - - cur=0; - for (int i=0; i0) { - weights2[ cur ] = -val; - indJ[cur++]=i; - } - } - - // Define the graph - net.supplyMap(&weights1[0], n, &weights2[0], m); - - // Set the cost of each edge - for (int i=0; i0) - { - *cost += flow * (*(D+indI[i]*n2+indJ[j-n])); - - *(G+cur) = flow; - *(iG+cur) = indI[i]; - *(jG+cur) = indJ[j-n]; - *(alpha + indI[i]) = -net.potential(i); - *(beta + indJ[j-n]) = net.potential(j); - cur++; - } - } - *nG=cur; // nb of value +1 for numpy indexing - - } - - - return ret; -} - -int EMD_wrap_all_sparse(int n1, int n2, double *X, double *Y, - long *iD, long *jD, double *D, long nD, - long *iG, long *jG, double *G, long * nG, - double* alpha, double* beta, double *cost, int maxIter) { - // beware M and C anre strored in row major C style!!! - - // Get the number of non zero coordinates for r and c and vectors - int n, m, cur; - - typedef FullBipartiteDigraph Digraph; - DIGRAPH_TYPEDEFS(FullBipartiteDigraph); - - n=n1; - m=n2; - - - // Define the graph - - - std::vector weights2(m); - Digraph di(n, m); - NetworkSimplexSimple net(di, true, n+m, n*m, maxIter); - - // Set supply and demand, don't account for 0 values (faster) - - - // Demand is actually negative supply... - - cur=0; - for (int i=0; i0) { - weights2[ cur ] = -val; - } - } - - // Define the graph - net.supplyMap(X, n, &weights2[0], m); - - // Set the cost of each edge - for (int k=0; k0) - { - - *(G+cur) = flow; - *(iG+cur) = i; - *(jG+cur) = j-n; - *(alpha + i) = -net.potential(i); - *(beta + j-n) = net.potential(j); - cur++; - } - } - *nG=cur; // nb of value +1 for numpy indexing - - } - - - return ret; -} - diff --git a/ot/lp/__init__.py b/ot/lp/__init__.py index 8d1baa0..ad390c5 100644 --- a/ot/lp/__init__.py +++ b/ot/lp/__init__.py @@ -172,7 +172,7 @@ def estimate_dual_null_weights(alpha0, beta0, a, b, M): return center_ot_dual(alpha, beta, a, b) -def emd(a, b, M, numItermax=100000, log=False, dense=True, center_dual=True): +def emd(a, b, M, numItermax=100000, log=False, center_dual=True): r"""Solves the Earth Movers distance problem and returns the OT matrix @@ -207,10 +207,6 @@ def emd(a, b, M, numItermax=100000, log=False, dense=True, center_dual=True): log: bool, optional (default=False) If True, returns a dictionary containing the cost and dual variables. Otherwise returns only the optimal transportation matrix. - dense: boolean, optional (default=True) - If True, returns math:`\gamma` as a dense ndarray of shape (ns, nt). - Otherwise returns a sparse representation using scipy's `coo_matrix` - format. center_dual: boolean, optional (default=True) If True, centers the dual potential using function :ref:`center_ot_dual`. @@ -267,25 +263,14 @@ def emd(a, b, M, numItermax=100000, log=False, dense=True, center_dual=True): asel = a != 0 bsel = b != 0 - if dense: - G, cost, u, v, result_code = emd_c(a, b, M, numItermax, dense) - - if center_dual: - u, v = center_ot_dual(u, v, a, b) + G, cost, u, v, result_code = emd_c(a, b, M, numItermax) - if np.any(~asel) or np.any(~bsel): - u, v = estimate_dual_null_weights(u, v, a, b, M) - - else: - Gv, iG, jG, cost, u, v, result_code = emd_c(a, b, M, numItermax, dense) - G = coo_matrix((Gv, (iG, jG)), shape=(a.shape[0], b.shape[0])) - - if center_dual: - u, v = center_ot_dual(u, v, a, b) - - if np.any(~asel) or np.any(~bsel): - u, v = estimate_dual_null_weights(u, v, a, b, M) + if center_dual: + u, v = center_ot_dual(u, v, a, b) + if np.any(~asel) or np.any(~bsel): + u, v = estimate_dual_null_weights(u, v, a, b, M) + result_code_string = check_result(result_code) if log: log = {} @@ -299,7 +284,7 @@ def emd(a, b, M, numItermax=100000, log=False, dense=True, center_dual=True): def emd2(a, b, M, processes=multiprocessing.cpu_count(), - numItermax=100000, log=False, dense=True, return_matrix=False, + numItermax=100000, log=False, return_matrix=False, center_dual=True): r"""Solves the Earth Movers distance problem and returns the loss @@ -404,11 +389,8 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), if log or return_matrix: def f(b): bsel = b != 0 - if dense: - G, cost, u, v, result_code = emd_c(a, b, M, numItermax, dense) - else: - Gv, iG, jG, cost, u, v, result_code = emd_c(a, b, M, numItermax, dense) - G = coo_matrix((Gv, (iG, jG)), shape=(a.shape[0], b.shape[0])) + + G, cost, u, v, result_code = emd_c(a, b, M, numItermax) if center_dual: u, v = center_ot_dual(u, v, a, b) @@ -428,11 +410,7 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), else: def f(b): bsel = b != 0 - if dense: - G, cost, u, v, result_code = emd_c(a, b, M, numItermax, dense) - else: - Gv, iG, jG, cost, u, v, result_code = emd_c(a, b, M, numItermax, dense) - G = coo_matrix((Gv, (iG, jG)), shape=(a.shape[0], b.shape[0])) + G, cost, u, v, result_code = emd_c(a, b, M, numItermax) if center_dual: u, v = center_ot_dual(u, v, a, b) @@ -440,7 +418,6 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), if np.any(~asel) or np.any(~bsel): u, v = estimate_dual_null_weights(u, v, a, b, M) - result_code_string = check_result(result_code) check_result(result_code) return cost diff --git a/ot/lp/emd_wrap.pyx b/ot/lp/emd_wrap.pyx index d345fd4..b6bda47 100644 --- a/ot/lp/emd_wrap.pyx +++ b/ot/lp/emd_wrap.pyx @@ -20,9 +20,6 @@ import warnings cdef extern from "EMD.h": int EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double* alpha, double* beta, double *cost, int maxIter) - int EMD_wrap_return_sparse(int n1, int n2, double *X, double *Y, double *D, - long *iG, long *jG, double *G, long * nG, - double* alpha, double* beta, double *cost, int maxIter) cdef enum ProblemType: INFEASIBLE, OPTIMAL, UNBOUNDED, MAX_ITER_REACHED @@ -38,13 +35,10 @@ def check_result(result_code): message = "numItermax reached before optimality. Try to increase numItermax." warnings.warn(message) return message - - - - + @cython.boundscheck(False) @cython.wraparound(False) -def emd_c(np.ndarray[double, ndim=1, mode="c"] a, np.ndarray[double, ndim=1, mode="c"] b, np.ndarray[double, ndim=2, mode="c"] M, int max_iter, bint dense): +def emd_c(np.ndarray[double, ndim=1, mode="c"] a, np.ndarray[double, ndim=1, mode="c"] b, np.ndarray[double, ndim=2, mode="c"] M, int max_iter): """ Solves the Earth Movers distance problem and returns the optimal transport matrix @@ -83,8 +77,6 @@ def emd_c(np.ndarray[double, ndim=1, mode="c"] a, np.ndarray[double, ndim=1, mod max_iter : int The maximum number of iterations before stopping the optimization algorithm if it has not converged. - dense : bool - Return a sparse transport matrix if set to False Returns ------- @@ -114,29 +106,13 @@ def emd_c(np.ndarray[double, ndim=1, mode="c"] a, np.ndarray[double, ndim=1, mod if not len(b): b=np.ones((n2,))/n2 - if dense: - # init OT matrix - G=np.zeros([n1, n2]) - - # calling the function - result_code = EMD_wrap(n1, n2, a.data, b.data, M.data, G.data, alpha.data, beta.data, &cost, max_iter) - - return G, cost, alpha, beta, result_code - - - else: - - # init sparse OT matrix - Gv=np.zeros(nmax) - iG=np.zeros(nmax,dtype=np.int) - jG=np.zeros(nmax,dtype=np.int) - - - result_code = EMD_wrap_return_sparse(n1, n2, a.data, b.data, M.data, iG.data, jG.data, Gv.data, &nG, alpha.data, beta.data, &cost, max_iter) - + # init OT matrix + G=np.zeros([n1, n2]) - return Gv[:nG], iG[:nG], jG[:nG], cost, alpha, beta, result_code + # calling the function + result_code = EMD_wrap(n1, n2, a.data, b.data, M.data, G.data, alpha.data, beta.data, &cost, max_iter) + return G, cost, alpha, beta, result_code @cython.boundscheck(False) diff --git a/ot/lp/network_simplex_simple.h b/ot/lp/network_simplex_simple.h index 498e921..5d93040 100644 --- a/ot/lp/network_simplex_simple.h +++ b/ot/lp/network_simplex_simple.h @@ -875,7 +875,7 @@ namespace lemon { c += Number(it->second) * Number(_cost[it->first]); return c;*/ - for (int i=0; i<_flow.size(); i++) + for (unsigned long i=0; i<_flow.size(); i++) c += _flow[i] * Number(_cost[i]); return c; @@ -1257,7 +1257,7 @@ namespace lemon { u = w; } _pred[u_in] = in_arc; - _forward[u_in] = (u_in == _source[in_arc]); + _forward[u_in] = ((unsigned int)u_in == _source[in_arc]); _succ_num[u_in] = old_succ_num; // Set limits for updating _last_succ form v_in and v_out @@ -1418,7 +1418,6 @@ namespace lemon { template ProblemType start() { PivotRuleImpl pivot(*this); - double prevCost=-1; ProblemType retVal = OPTIMAL; // Perform heuristic initial pivots diff --git a/test/test_ot.py b/test/test_ot.py index 0f1357f..b7306f6 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -170,27 +170,6 @@ def test_emd_empty(): np.testing.assert_allclose(w, 0) -def test_emd_sparse(): - n = 100 - rng = np.random.RandomState(0) - - x = rng.randn(n, 2) - x2 = rng.randn(n, 2) - - M = ot.dist(x, x2) - - G = ot.emd([], [], M, dense=True) - - Gs = ot.emd([], [], M, dense=False) - - ws = ot.emd2([], [], M, dense=False) - - # check G is the same - np.testing.assert_allclose(G, Gs.todense()) - # check value - np.testing.assert_allclose(Gs.multiply(M).sum(), ws, rtol=1e-6) - - def test_emd2_multi(): n = 500 # nb bins @@ -222,12 +201,7 @@ def test_emd2_multi(): emdn = ot.emd2(a, b, M) ot.toc('multi proc : {} s') - ot.tic() - emdn2 = ot.emd2(a, b, M, dense=False) - ot.toc('multi proc : {} s') - np.testing.assert_allclose(emd1, emdn) - np.testing.assert_allclose(emd1, emdn2, rtol=1e-6) # emd loss multipro proc with log ot.tic() diff --git a/test/test_utils.py b/test/test_utils.py index 640598d..db9cda6 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -36,10 +36,10 @@ def test_tic_toc(): t2 = ot.toq() # test timing - np.testing.assert_allclose(0.5, t, rtol=1e-2, atol=1e-2) + np.testing.assert_allclose(0.5, t, rtol=1e-1, atol=1e-1) # test toc vs toq - np.testing.assert_allclose(t, t2, rtol=1e-2, atol=1e-2) + np.testing.assert_allclose(t, t2, rtol=1e-1, atol=1e-1) def test_kernel(): -- cgit v1.2.3 From 53b063ed6b6aa15d6cb103a9304bbd169678b2e9 Mon Sep 17 00:00:00 2001 From: Rémi Flamary Date: Fri, 24 Apr 2020 12:39:38 +0200 Subject: better coverage options verbose and log --- ot/bregman.py | 5 ----- test/test_bregman.py | 9 ++++++--- test/test_optim.py | 2 +- test/test_partial.py | 26 +++++++++++++++++++++++++- test/test_stochastic.py | 8 ++++---- test/test_unbalanced.py | 9 ++++++--- 6 files changed, 42 insertions(+), 17 deletions(-) (limited to 'test') diff --git a/ot/bregman.py b/ot/bregman.py index 543dbaa..b4365d0 100644 --- a/ot/bregman.py +++ b/ot/bregman.py @@ -909,11 +909,6 @@ def sinkhorn_epsilon_scaling(a, b, M, reg, numItermax=100, epsilon0=1e4, else: alpha, beta = warmstart - def get_K(alpha, beta): - """log space computation""" - return np.exp(-(M - alpha.reshape((dim_a, 1)) - - beta.reshape((1, dim_b))) / reg) - # print(np.min(K)) def get_reg(n): # exponential decreasing return (epsilon0 - reg) * np.exp(-n) + reg diff --git a/test/test_bregman.py b/test/test_bregman.py index ec4388d..6aa4e08 100644 --- a/test/test_bregman.py +++ b/test/test_bregman.py @@ -57,6 +57,9 @@ def test_sinkhorn_empty(): np.testing.assert_allclose(u, G.sum(1), atol=1e-05) np.testing.assert_allclose(u, G.sum(0), atol=1e-05) + # test empty weights greenkhorn + ot.sinkhorn([], [], M, 1, method='greenkhorn', stopThr=1e-10, log=True) + def test_sinkhorn_variants(): # test sinkhorn @@ -124,7 +127,7 @@ def test_barycenter(method): # wasserstein reg = 1e-2 - bary_wass = ot.bregman.barycenter(A, M, reg, weights, method=method) + bary_wass, log = ot.bregman.barycenter(A, M, reg, weights, method=method, log=True) np.testing.assert_allclose(1, np.sum(bary_wass)) @@ -152,9 +155,9 @@ def test_barycenter_stabilization(): reg = 1e-2 bar_stable = ot.bregman.barycenter(A, M, reg, weights, method="sinkhorn_stabilized", - stopThr=1e-8) + stopThr=1e-8, verbose=True) bar = ot.bregman.barycenter(A, M, reg, weights, method="sinkhorn", - stopThr=1e-8) + stopThr=1e-8, verbose=True) np.testing.assert_allclose(bar, bar_stable) diff --git a/test/test_optim.py b/test/test_optim.py index aade36e..87b0268 100644 --- a/test/test_optim.py +++ b/test/test_optim.py @@ -38,7 +38,7 @@ def test_conditional_gradient(): def test_conditional_gradient2(): - n = 4000 # nb samples + n = 1000 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) diff --git a/test/test_partial.py b/test/test_partial.py index 8b1ca89..5960e4e 100755 --- a/test/test_partial.py +++ b/test/test_partial.py @@ -9,6 +9,30 @@ import numpy as np import scipy as sp import ot +def test_partial_wasserstein_lagrange(): + + n_samples = 20 # nb samples (gaussian) + n_noise = 20 # nb of samples (noise) + + mu = np.array([0, 0]) + cov = np.array([[1, 0], [0, 2]]) + + xs = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov) + xs = np.append(xs, (np.random.rand(n_noise, 2) + 1) * 4).reshape((-1, 2)) + xt = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov) + xt = np.append(xt, (np.random.rand(n_noise, 2) + 1) * -3).reshape((-1, 2)) + + M = ot.dist(xs, xt) + + p = ot.unif(n_samples + n_noise) + q = ot.unif(n_samples + n_noise) + + m = 0.5 + + w0, log0 = ot.partial.partial_wasserstein_lagrange(p, q, M, 1, log=True) + + + def test_partial_wasserstein(): @@ -32,7 +56,7 @@ def test_partial_wasserstein(): w0, log0 = ot.partial.partial_wasserstein(p, q, M, m=m, log=True) w, log = ot.partial.entropic_partial_wasserstein(p, q, M, reg=1, m=m, - log=True) + log=True, verbose=True) # check constratints np.testing.assert_equal( diff --git a/test/test_stochastic.py b/test/test_stochastic.py index f0f3fc8..8ddf485 100644 --- a/test/test_stochastic.py +++ b/test/test_stochastic.py @@ -70,8 +70,8 @@ def test_stochastic_asgd(): M = ot.dist(x, x) - G = ot.stochastic.solve_semi_dual_entropic(u, u, M, reg, "asgd", - numItermax=numItermax) + G, log = ot.stochastic.solve_semi_dual_entropic(u, u, M, reg, "asgd", + numItermax=numItermax, log=True) # check constratints np.testing.assert_allclose( @@ -145,8 +145,8 @@ def test_stochastic_dual_sgd(): M = ot.dist(x, x) - G = ot.stochastic.solve_dual_entropic(u, u, M, reg, batch_size, - numItermax=numItermax) + G, log = ot.stochastic.solve_dual_entropic(u, u, M, reg, batch_size, + numItermax=numItermax, log=True) # check constratints np.testing.assert_allclose( diff --git a/test/test_unbalanced.py b/test/test_unbalanced.py index ca1efba..d5bae42 100644 --- a/test/test_unbalanced.py +++ b/test/test_unbalanced.py @@ -31,9 +31,11 @@ def test_unbalanced_convergence(method): G, log = ot.unbalanced.sinkhorn_unbalanced(a, b, M, reg=epsilon, reg_m=reg_m, method=method, - log=True) + log=True, + verbose=True) loss = ot.unbalanced.sinkhorn_unbalanced2(a, b, M, epsilon, reg_m, - method=method) + method=method, + verbose=True) # check fixed point equations # in log-domain fi = reg_m / (reg_m + epsilon) @@ -73,7 +75,8 @@ def test_unbalanced_multiple_inputs(method): loss, log = ot.unbalanced.sinkhorn_unbalanced(a, b, M, reg=epsilon, reg_m=reg_m, method=method, - log=True) + log=True, + verbose=True) # check fixed point equations # in log-domain fi = reg_m / (reg_m + epsilon) -- cgit v1.2.3 From 90bd408e86eccb03b02d57a0cd7963e0c848a1fc Mon Sep 17 00:00:00 2001 From: Rémi Flamary Date: Fri, 24 Apr 2020 13:59:42 +0200 Subject: pep8 --- test/test_partial.py | 5 ++--- test/test_stochastic.py | 4 ++-- test/test_unbalanced.py | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) (limited to 'test') diff --git a/test/test_partial.py b/test/test_partial.py index 5960e4e..b533a9c 100755 --- a/test/test_partial.py +++ b/test/test_partial.py @@ -9,6 +9,7 @@ import numpy as np import scipy as sp import ot + def test_partial_wasserstein_lagrange(): n_samples = 20 # nb samples (gaussian) @@ -29,9 +30,7 @@ def test_partial_wasserstein_lagrange(): m = 0.5 - w0, log0 = ot.partial.partial_wasserstein_lagrange(p, q, M, 1, log=True) - - + w0, log0 = ot.partial.partial_wasserstein_lagrange(p, q, M, 1, log=True) def test_partial_wasserstein(): diff --git a/test/test_stochastic.py b/test/test_stochastic.py index 8ddf485..155622c 100644 --- a/test/test_stochastic.py +++ b/test/test_stochastic.py @@ -71,7 +71,7 @@ def test_stochastic_asgd(): M = ot.dist(x, x) G, log = ot.stochastic.solve_semi_dual_entropic(u, u, M, reg, "asgd", - numItermax=numItermax, log=True) + numItermax=numItermax, log=True) # check constratints np.testing.assert_allclose( @@ -146,7 +146,7 @@ def test_stochastic_dual_sgd(): M = ot.dist(x, x) G, log = ot.stochastic.solve_dual_entropic(u, u, M, reg, batch_size, - numItermax=numItermax, log=True) + numItermax=numItermax, log=True) # check constratints np.testing.assert_allclose( diff --git a/test/test_unbalanced.py b/test/test_unbalanced.py index d5bae42..dfeaad9 100644 --- a/test/test_unbalanced.py +++ b/test/test_unbalanced.py @@ -35,7 +35,7 @@ def test_unbalanced_convergence(method): verbose=True) loss = ot.unbalanced.sinkhorn_unbalanced2(a, b, M, epsilon, reg_m, method=method, - verbose=True) + verbose=True) # check fixed point equations # in log-domain fi = reg_m / (reg_m + epsilon) -- cgit v1.2.3 From 17d388be57cb5b0b2492c6b0ad8940e58b36016a Mon Sep 17 00:00:00 2001 From: Rémi Flamary Date: Fri, 24 Apr 2020 14:18:41 +0200 Subject: test raise un partial ot --- ot/datasets.py | 4 ++-- test/test_partial.py | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 3 deletions(-) (limited to 'test') diff --git a/ot/datasets.py b/ot/datasets.py index a1ca7b6..daca1ae 100644 --- a/ot/datasets.py +++ b/ot/datasets.py @@ -147,8 +147,8 @@ def make_data_classif(dataset, n, nz=.5, theta=0, p=.5, random_state=None, **kwa n2 = np.sum(y == 2) x = np.zeros((n, 2)) - x[y == 1, :] = get_2D_samples_gauss(n1, m1, nz, random_state=generator) - x[y == 2, :] = get_2D_samples_gauss(n2, m2, nz, random_state=generator) + x[y == 1, :] = make_2D_samples_gauss(n1, m1, nz, random_state=generator) + x[y == 2, :] = make_2D_samples_gauss(n2, m2, nz, random_state=generator) x = x.dot(rot) diff --git a/test/test_partial.py b/test/test_partial.py index b533a9c..eb3b76e 100755 --- a/test/test_partial.py +++ b/test/test_partial.py @@ -8,6 +8,53 @@ import numpy as np import scipy as sp import ot +import pytest + + +def test_raise_errors(): + + n_samples = 20 # nb samples (gaussian) + n_noise = 20 # nb of samples (noise) + + mu = np.array([0, 0]) + cov = np.array([[1, 0], [0, 2]]) + + xs = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov) + xs = np.append(xs, (np.random.rand(n_noise, 2) + 1) * 4).reshape((-1, 2)) + xt = ot.datasets.make_2D_samples_gauss(n_samples, mu, cov) + xt = np.append(xt, (np.random.rand(n_noise, 2) + 1) * -3).reshape((-1, 2)) + + M = ot.dist(xs, xt) + + p = ot.unif(n_samples + n_noise) + q = ot.unif(n_samples + n_noise) + + with pytest.raises(ValueError): + ot.partial.partial_wasserstein_lagrange(p + 1, q, M, 1, log=True) + + with pytest.raises(ValueError): + ot.partial.partial_wasserstein(p, q, M, m=2, log=True) + + with pytest.raises(ValueError): + ot.partial.partial_wasserstein(p, q, M, m=-1, log=True) + + with pytest.raises(ValueError): + ot.partial.entropic_partial_wasserstein(p, q, M, reg=1, m=2, log=True) + + with pytest.raises(ValueError): + ot.partial.entropic_partial_wasserstein(p, q, M, reg=1, m=-1, log=True) + + with pytest.raises(ValueError): + ot.partial.partial_gromov_wasserstein(M, M, p, q, m=2, log=True) + + with pytest.raises(ValueError): + ot.partial.partial_gromov_wasserstein(M, M, p, q, m=-1, log=True) + + with pytest.raises(ValueError): + ot.partial.entropic_partial_gromov_wasserstein(M, M, p, q, reg=1, m=2, log=True) + + with pytest.raises(ValueError): + ot.partial.entropic_partial_gromov_wasserstein(M, M, p, q, reg=1, m=-1, log=True) def test_partial_wasserstein_lagrange(): @@ -115,7 +162,7 @@ def test_partial_gromov_wasserstein(): m = 2 / 3 res0, log0 = ot.partial.partial_gromov_wasserstein(C1, C3, p, q, m=m, - log=True) + log=True, verbose=True) np.testing.assert_allclose(res0, 0, atol=1e-1, rtol=1e-1) C1 = sp.spatial.distance.cdist(xs, xs) -- cgit v1.2.3 From eb3a70af671736c940c8aceaff8547b057d1335a Mon Sep 17 00:00:00 2001 From: Rémi Flamary Date: Fri, 24 Apr 2020 14:20:33 +0200 Subject: left some unused variable... --- test/test_partial.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'test') diff --git a/test/test_partial.py b/test/test_partial.py index eb3b76e..510e081 100755 --- a/test/test_partial.py +++ b/test/test_partial.py @@ -75,8 +75,6 @@ def test_partial_wasserstein_lagrange(): p = ot.unif(n_samples + n_noise) q = ot.unif(n_samples + n_noise) - m = 0.5 - w0, log0 = ot.partial.partial_wasserstein_lagrange(p, q, M, 1, log=True) -- cgit v1.2.3