From 0e86d1bdbc0dcf7ffdb943637f62df5de4612ad0 Mon Sep 17 00:00:00 2001 From: Antoine Rolet Date: Thu, 13 Jul 2017 15:19:42 +0900 Subject: Removed references to matlab Also: - added error message when maxiter is reached - added debug logs --- ot/lp/network_simplex_simple.h | 53 +++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/ot/lp/network_simplex_simple.h b/ot/lp/network_simplex_simple.h index 64856a0..125c818 100644 --- a/ot/lp/network_simplex_simple.h +++ b/ot/lp/network_simplex_simple.h @@ -28,6 +28,12 @@ #ifndef LEMON_NETWORK_SIMPLEX_SIMPLE_H #define LEMON_NETWORK_SIMPLEX_SIMPLE_H #define DEBUG_LVL 0 + +#if DEBUG_LVL>0 +#include +#endif + + #define EPSILON 10*2.2204460492503131e-016 #define MAX_DEBUG_ITER 100000 @@ -220,7 +226,7 @@ namespace lemon { /// mixed order in the internal data structure. /// In special cases, it could lead to better overall performance, /// but it is usually slower. Therefore it is disabled by default. - NetworkSimplexSimple(const GR& graph, bool arc_mixing, int nbnodes, long long nb_arcs,double maxiters) : + NetworkSimplexSimple(const GR& graph, bool arc_mixing, int nbnodes, long long nb_arcs,int maxiters) : _graph(graph), //_arc_id(graph), _arc_mixing(arc_mixing), _init_nb_nodes(nbnodes), _init_nb_arcs(nb_arcs), MAX(std::numeric_limits::max()), @@ -278,7 +284,7 @@ namespace lemon { private: - double max_iter; + int max_iter; TEMPLATE_DIGRAPH_TYPEDEFS(GR); typedef std::vector IntVector; @@ -676,14 +682,12 @@ namespace lemon { /// \see resetParams(), reset() ProblemType run() { #if DEBUG_LVL>0 - mexPrintf("OPTIMAL = %d\nINFEASIBLE = %d\nUNBOUNDED = %d\n",OPTIMAL,INFEASIBLE,UNBOUNDED); - mexEvalString("drawnow;"); + std::cout << "OPTIMAL = " << OPTIMAL << "\nINFEASIBLE = " << INFEASIBLE << "nUNBOUNDED = " << UNBOUNDED << "\n"; #endif if (!init()) return INFEASIBLE; #if DEBUG_LVL>0 - mexPrintf("Init done, starting iterations\n"); - mexEvalString("drawnow;"); + std::cout << "Init done, starting iterations\n"; #endif return start(); } @@ -1424,8 +1428,8 @@ namespace lemon { while (pivot.findEnteringArc()) { if(++iter_number>=max_iter&&max_iter>0){ char errMess[1000]; - // sprintf( errMess, "RESULT MIGHT BE INACURATE\nMax number of iteration reached, currently \%d. Sometimes iterations go on in cycle even though the solution has been reached, to check if it's the case here have a look at the minimal reduced cost. If it is very close to machine precision, you might actually have the correct solution, if not try setting the maximum number of iterations a bit higher",iter_number ); - // mexWarnMsgTxt(errMess); + sprintf( errMess, "RESULT MIGHT BE INACURATE\nMax number of iteration reached, currently \%d. Sometimes iterations go on in cycle even though the solution has been reached, to check if it's the case here have a look at the minimal reduced cost. If it is very close to machine precision, you might actually have the correct solution, if not try setting the maximum number of iterations a bit higher\n",iter_number ); + std::cerr << errMess; break; } #if DEBUG_LVL>0 @@ -1440,12 +1444,13 @@ namespace lemon { for (int i=0; i<_flow.size(); i++) { sumFlow+=_state[i]*_flow[i]; } - mexPrintf("Sum of the flow %.100f\n%d iterations, current cost=%.20f\nReduced cost=%.30f\nPrecision =%.30f\n",sumFlow,niter, curCost,_state[in_arc] * (_cost[in_arc] + _pi[_source[in_arc]] -_pi[_target[in_arc]]), -EPSILON*(a)); - mexPrintf("Arc in = (%d,%d)\n",_node_id(_source[in_arc]),_node_id(_target[in_arc])); - mexPrintf("Supplies = (%f,%f)\n",_supply[_source[in_arc]],_supply[_target[in_arc]]); - - mexPrintf("%.30f\n%.30f\n%.30f\n%.30f\n%",_cost[in_arc],_pi[_source[in_arc]],_pi[_target[in_arc]],a); - mexEvalString("drawnow;"); + std::cout << "Sum of the flow " << std::setprecision(20) << sumFlow << "\n" << niter << " iterations, current cost=" << curCost << "\nReduced cost=" << _state[in_arc] * (_cost[in_arc] + _pi[_source[in_arc]] -_pi[_target[in_arc]]) << "\nPrecision = "<< -EPSILON*(a) << "\n"; + std::cout << "Arc in = (" << _node_id(_source[in_arc]) << ", " << _node_id(_target[in_arc]) <<")\n"; + std::cout << "Supplies = (" << _supply[_source[in_arc]] << ", " << _supply[_target[in_arc]] << ")\n"; + std::cout << _cost[in_arc] << "\n"; + std::cout << _pi[_source[in_arc]] << "\n"; + std::cout << _pi[_target[in_arc]] << "\n"; + std::cout << a << "\n"; } #endif @@ -1459,11 +1464,11 @@ namespace lemon { } #if DEBUG_LVL>0 else{ - mexPrintf("No change\n"); + std::cout << "No change\n"; } #endif #if DEBUG_LVL>1 - mexPrintf("Arc in = (%d,%d)\n",_source[in_arc],_target[in_arc]); + std::cout << "Arc in = (" << _source[in_arc] << ", " << _target[in_arc] << ")\n"; #endif } @@ -1478,23 +1483,23 @@ namespace lemon { for (int i=0; i<_flow.size(); i++) { sumFlow+=_state[i]*_flow[i]; } - mexPrintf("Sum of the flow %.100f\n%d iterations, current cost=%.20f\nReduced cost=%.30f\nPrecision =%.30f",sumFlow,niter, curCost,_state[in_arc] * (_cost[in_arc] + _pi[_source[in_arc]] -_pi[_target[in_arc]]), -EPSILON*(a)); - mexPrintf("Arc in = (%d,%d)\n",_node_id(_source[in_arc]),_node_id(_target[in_arc])); - mexPrintf("Supplies = (%f,%f)\n",_supply[_source[in_arc]],_supply[_target[in_arc]]); + + std::cout << "Sum of the flow " << std::setprecision(20) << sumFlow << "\n" << niter << " iterations, current cost=" << curCost << "\nReduced cost=" << _state[in_arc] * (_cost[in_arc] + _pi[_source[in_arc]] -_pi[_target[in_arc]]) << "\nPrecision = "<< -EPSILON*(a) << "\n"; + + std::cout << "Arc in = (" << _node_id(_source[in_arc]) << ", " << _node_id(_target[in_arc]) <<")\n"; + std::cout << "Supplies = (" << _supply[_source[in_arc]] << ", " << _supply[_target[in_arc]] << ")\n"; - mexEvalString("drawnow;"); #endif #if DEBUG_LVL>1 - double sumFlow=0; + sumFlow=0; for (int i=0; i<_flow.size(); i++) { sumFlow+=_state[i]*_flow[i]; if (_state[i]==STATE_TREE) { - mexPrintf("Non zero value at (%d,%d)\n",_node_num+1-_source[i],_node_num+1-_target[i]); + std::cout << "Non zero value at (" << _node_num+1-_source[i] << ", " << _node_num+1-_target[i] << ")\n"; } } - mexPrintf("Sum of the flow %.100f\n%d iterations, current cost=%.20f\n",sumFlow,niter, totalCost()); - mexEvalString("drawnow;"); + std::cout << "Sum of the flow " << sumFlow << "\n"<< niter <<" iterations, current cost=" << totalCost() << "\n"; #endif // Check feasibility for (int e = _search_arc_num; e != _all_arc_num; ++e) { -- cgit v1.2.3 From 55a38f8253e5831105d2c329f4d8ed77686d1330 Mon Sep 17 00:00:00 2001 From: Antoine Rolet Date: Thu, 13 Jul 2017 15:30:39 +0900 Subject: Added optional maximal number of iteration --- ot/lp/EMD.h | 2 +- ot/lp/EMD_wrapper.cpp | 3 +-- ot/lp/__init__.py | 10 +++++----- ot/lp/emd_wrap.pyx | 10 +++++----- ot/lp/network_simplex_simple.h | 2 +- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/ot/lp/EMD.h b/ot/lp/EMD.h index 40d7192..59a5af8 100644 --- a/ot/lp/EMD.h +++ b/ot/lp/EMD.h @@ -24,6 +24,6 @@ using namespace lemon; typedef unsigned int node_id_type; -void EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double *cost); +void EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double *cost, int max_iter); #endif diff --git a/ot/lp/EMD_wrapper.cpp b/ot/lp/EMD_wrapper.cpp index cad4750..2d448a0 100644 --- a/ot/lp/EMD_wrapper.cpp +++ b/ot/lp/EMD_wrapper.cpp @@ -15,11 +15,10 @@ #include "EMD.h" -void EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double *cost) { +void EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double *cost, int max_iter) { // beware M and C anre strored in row major C style!!! int n, m, i,cur; double max; - int max_iter=10000; typedef FullBipartiteDigraph Digraph; DIGRAPH_TYPEDEFS(FullBipartiteDigraph); diff --git a/ot/lp/__init__.py b/ot/lp/__init__.py index db3da78..673242d 100644 --- a/ot/lp/__init__.py +++ b/ot/lp/__init__.py @@ -11,7 +11,7 @@ import multiprocessing -def emd(a, b, M): +def emd(a, b, M, max_iter=-1): """Solves the Earth Movers distance problem and returns the OT matrix @@ -80,9 +80,9 @@ def emd(a, b, M): if len(b) == 0: b = np.ones((M.shape[1], ), dtype=np.float64)/M.shape[1] - return emd_c(a, b, M) + return emd_c(a, b, M, max_iter) -def emd2(a, b, M,processes=multiprocessing.cpu_count()): +def emd2(a, b, M,processes=multiprocessing.cpu_count(), max_iter=-1): """Solves the Earth Movers distance problem and returns the loss .. math:: @@ -151,12 +151,12 @@ def emd2(a, b, M,processes=multiprocessing.cpu_count()): b = np.ones((M.shape[1], ), dtype=np.float64)/M.shape[1] if len(b.shape)==1: - return emd2_c(a, b, M) + return emd2_c(a, b, M, max_iter) else: nb=b.shape[1] #res=[emd2_c(a,b[:,i].copy(),M) for i in range(nb)] def f(b): - return emd2_c(a,b,M) + return emd2_c(a,b,M, max_iter) res= parmap(f, [b[:,i] for i in range(nb)],processes) return np.array(res) diff --git a/ot/lp/emd_wrap.pyx b/ot/lp/emd_wrap.pyx index 46794ab..e8fdba4 100644 --- a/ot/lp/emd_wrap.pyx +++ b/ot/lp/emd_wrap.pyx @@ -12,13 +12,13 @@ cimport cython cdef extern from "EMD.h": - void EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double *cost) + void EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double *cost, int max_iter) @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): +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 maxiter): """ Solves the Earth Movers distance problem and returns the optimal transport matrix @@ -66,13 +66,13 @@ def emd_c( np.ndarray[double, ndim=1, mode="c"] a,np.ndarray[double, ndim=1, mod b=np.ones((n2,))/n2 # calling the function - EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, &cost) + EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, &cost, maxiter) return G @cython.boundscheck(False) @cython.wraparound(False) -def emd2_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): +def emd2_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 maxiter): """ Solves the Earth Movers distance problem and returns the optimal transport loss @@ -120,7 +120,7 @@ def emd2_c( np.ndarray[double, ndim=1, mode="c"] a,np.ndarray[double, ndim=1, mo b=np.ones((n2,))/n2 # calling the function - EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, &cost) + EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, &cost, maxiter) cost=0 for i in range(n1): diff --git a/ot/lp/network_simplex_simple.h b/ot/lp/network_simplex_simple.h index 125c818..08449f6 100644 --- a/ot/lp/network_simplex_simple.h +++ b/ot/lp/network_simplex_simple.h @@ -1426,7 +1426,7 @@ namespace lemon { //pivot.setDantzig(true); // Execute the Network Simplex algorithm while (pivot.findEnteringArc()) { - if(++iter_number>=max_iter&&max_iter>0){ + if(max_iter > 0 && ++iter_number>=max_iter&&max_iter>0){ char errMess[1000]; sprintf( errMess, "RESULT MIGHT BE INACURATE\nMax number of iteration reached, currently \%d. Sometimes iterations go on in cycle even though the solution has been reached, to check if it's the case here have a look at the minimal reduced cost. If it is very close to machine precision, you might actually have the correct solution, if not try setting the maximum number of iterations a bit higher\n",iter_number ); std::cerr << errMess; -- cgit v1.2.3 From cd9909cff342bb46c4233a0ead348dabebe9efdf Mon Sep 17 00:00:00 2001 From: arolet Date: Fri, 14 Jul 2017 15:18:57 +0900 Subject: Added a test for single process EMD The multiprocess one does not seem to work on windows --- test/test_emd.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 test/test_emd.py diff --git a/test/test_emd.py b/test/test_emd.py new file mode 100644 index 0000000..3729d5d --- /dev/null +++ b/test/test_emd.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- + +import numpy as np +import pylab as pl +import ot + +from ot.datasets import get_1D_gauss as gauss +reload(ot.lp) + +#%% parameters + +n=5000 # nb bins + +# bin positions +x=np.arange(n,dtype=np.float64) + +# Gaussian distributions +a=gauss(n,m=20,s=5) # m= mean, s= std + +b=gauss(n,m=30,s=10) + +# loss matrix +M=ot.dist(x.reshape((n,1)),x.reshape((n,1))) +#M/=M.max() + +#%% + +print('Computing {} EMD '.format(1)) + +# emd loss 1 proc +ot.tic() +emd_loss4 = ot.emd(a,b,M) +ot.toc('1 proc : {} s') + -- cgit v1.2.3 From 0faef7fde7e64705b4f0ed6618a0cfd25319bdc7 Mon Sep 17 00:00:00 2001 From: arolet Date: Fri, 14 Jul 2017 15:19:55 +0900 Subject: Removed unused variable max Probably a legacy normalization variable --- ot/lp/EMD_wrapper.cpp | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/ot/lp/EMD_wrapper.cpp b/ot/lp/EMD_wrapper.cpp index 2d448a0..d97ba46 100644 --- a/ot/lp/EMD_wrapper.cpp +++ b/ot/lp/EMD_wrapper.cpp @@ -15,10 +15,10 @@ #include "EMD.h" -void EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double *cost, int max_iter) { +void EMD_wrap(int n1, int n2, double *X, double *Y, + double *D, double *G, double *cost, int max_iter) { // beware M and C anre strored in row major C style!!! int n, m, i,cur; - double max; typedef FullBipartiteDigraph Digraph; DIGRAPH_TYPEDEFS(FullBipartiteDigraph); @@ -39,7 +39,6 @@ void EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double * } } - // Define the graph std::vector indI(n), indJ(m); @@ -49,28 +48,23 @@ void EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double * // Set supply and demand, don't account for 0 values (faster) - max=0; cur=0; for (node_id_type i=0; i0) { weights1[ di.nodeFromId(cur) ] = val; - max+=val; indI[cur++]=i; } } // Demand is actually negative supply... - max=0; cur=0; for (node_id_type i=0; i0) { weights2[ di.nodeFromId(cur) ] = -val; indJ[cur++]=i; - - max-=val; } } @@ -78,14 +72,10 @@ void EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double * net.supplyMap(&weights1[0], n, &weights2[0], m); // Set the cost of each edge - max=0; for (node_id_type i=0; imax) { - max=val; - } } } -- cgit v1.2.3 From d59e91450272c78dd0fdd3c6bd9bf48776f10070 Mon Sep 17 00:00:00 2001 From: arolet Date: Fri, 14 Jul 2017 15:37:46 +0900 Subject: Added a test based on closed form solution for gaussians --- test/test_emd.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/test/test_emd.py b/test/test_emd.py index 3729d5d..eb1c5c5 100644 --- a/test/test_emd.py +++ b/test/test_emd.py @@ -11,17 +11,25 @@ reload(ot.lp) #%% parameters n=5000 # nb bins +m=6000 # nb bins + +mean1 = 1000 +mean2 = 1100 + +tol = 1e-6 # bin positions x=np.arange(n,dtype=np.float64) +y=np.arange(m,dtype=np.float64) # Gaussian distributions -a=gauss(n,m=20,s=5) # m= mean, s= std +a=gauss(n,m=mean1,s=5) # m= mean, s= std -b=gauss(n,m=30,s=10) +b=gauss(m,m=mean2,s=10) # loss matrix -M=ot.dist(x.reshape((n,1)),x.reshape((n,1))) +M=ot.dist(x.reshape((-1,1)), y.reshape((-1,1))) ** (1./2) +print M[0,:] #M/=M.max() #%% @@ -30,6 +38,16 @@ print('Computing {} EMD '.format(1)) # emd loss 1 proc ot.tic() -emd_loss4 = ot.emd(a,b,M) +G = ot.emd(a,b,M) ot.toc('1 proc : {} s') +cost1 = (G * M).sum() + +ot.tic() +G = ot.emd(b, a, np.ascontiguousarray(M.T)) +ot.toc('1 proc : {} s') + +cost2 = (G * M.T).sum() + +assert np.abs(cost1-cost2) < tol +assert np.abs(cost1-np.abs(mean1-mean2)) < tol -- cgit v1.2.3 From 1fcb7d0ffbc5b00ed20b5ded2e7f1001dc914d6e Mon Sep 17 00:00:00 2001 From: arolet Date: Fri, 14 Jul 2017 15:38:20 +0900 Subject: Removed some references to node_id_type node_id_type is really always int, it makes code hard to read though. In lemon they needed the typedef because they have more complicated graphs. --- ot/lp/EMD_wrapper.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ot/lp/EMD_wrapper.cpp b/ot/lp/EMD_wrapper.cpp index d97ba46..d719c6e 100644 --- a/ot/lp/EMD_wrapper.cpp +++ b/ot/lp/EMD_wrapper.cpp @@ -25,14 +25,14 @@ void EMD_wrap(int n1, int n2, double *X, double *Y, // Get the number of non zero coordinates for r and c n=0; - for (node_id_type i=0; i0) { n++; } } m=0; - for (node_id_type i=0; i0) { m++; @@ -49,10 +49,10 @@ void EMD_wrap(int n1, int n2, double *X, double *Y, // Set supply and demand, don't account for 0 values (faster) cur=0; - for (node_id_type i=0; i0) { - weights1[ di.nodeFromId(cur) ] = val; + weights1[ cur ] = val; indI[cur++]=i; } } @@ -60,10 +60,10 @@ void EMD_wrap(int n1, int n2, double *X, double *Y, // Demand is actually negative supply... cur=0; - for (node_id_type i=0; i0) { - weights2[ di.nodeFromId(cur) ] = -val; + weights2[ cur ] = -val; indJ[cur++]=i; } } @@ -72,8 +72,8 @@ void EMD_wrap(int n1, int n2, double *X, double *Y, net.supplyMap(&weights1[0], n, &weights2[0], m); // Set the cost of each edge - for (node_id_type i=0; i Date: Fri, 21 Jul 2017 12:12:21 +0900 Subject: Cleaned optimal plan and optimal cost computation --- ot/lp/EMD_wrapper.cpp | 13 ++++++------- ot/lp/emd_wrap.pyx | 5 ----- test/test_emd.py | 10 ++++++++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/ot/lp/EMD_wrapper.cpp b/ot/lp/EMD_wrapper.cpp index d719c6e..cc13230 100644 --- a/ot/lp/EMD_wrapper.cpp +++ b/ot/lp/EMD_wrapper.cpp @@ -93,14 +93,13 @@ void EMD_wrap(int n1, int n2, double *X, double *Y, } } else { - for (node_id_type i=0; i a.data, b.data, M.data, G.data, &cost, maxiter) - - cost=0 - for i in range(n1): - for j in range(n2): - cost+=G[i,j]*M[i,j] return cost diff --git a/test/test_emd.py b/test/test_emd.py index eb1c5c5..4757cd1 100644 --- a/test/test_emd.py +++ b/test/test_emd.py @@ -43,11 +43,17 @@ ot.toc('1 proc : {} s') cost1 = (G * M).sum() +# emd loss 1 proc +ot.tic() +cost_emd2 = ot.emd2(a,b,M) +ot.toc('1 proc : {} s') + ot.tic() G = ot.emd(b, a, np.ascontiguousarray(M.T)) ot.toc('1 proc : {} s') cost2 = (G * M.T).sum() -assert np.abs(cost1-cost2) < tol -assert np.abs(cost1-np.abs(mean1-mean2)) < tol +assert np.abs(cost1-cost_emd2)/np.abs(cost1) < tol +assert np.abs(cost1-cost2)/np.abs(cost1) < tol +assert np.abs(cost1-np.abs(mean1-mean2))/np.abs(cost1) < tol -- cgit v1.2.3 From 88c62c39a9623e8b58ebb776a9deddc96b43b4a0 Mon Sep 17 00:00:00 2001 From: arolet Date: Fri, 21 Jul 2017 12:12:48 +0900 Subject: Added dual variables computations --- ot/lp/EMD.h | 3 ++- ot/lp/EMD_wrapper.cpp | 6 ++++-- ot/lp/__init__.py | 11 +++++++---- ot/lp/emd_wrap.pyx | 22 ++++++++++++++++------ 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/ot/lp/EMD.h b/ot/lp/EMD.h index 59a5af8..fb7feca 100644 --- a/ot/lp/EMD.h +++ b/ot/lp/EMD.h @@ -24,6 +24,7 @@ using namespace lemon; typedef unsigned int node_id_type; -void EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double *cost, int max_iter); +void EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, + double* alpha, double* beta, double *cost, int max_iter); #endif diff --git a/ot/lp/EMD_wrapper.cpp b/ot/lp/EMD_wrapper.cpp index cc13230..6bda6a7 100644 --- a/ot/lp/EMD_wrapper.cpp +++ b/ot/lp/EMD_wrapper.cpp @@ -15,8 +15,8 @@ #include "EMD.h" -void EMD_wrap(int n1, int n2, double *X, double *Y, - double *D, double *G, double *cost, int max_iter) { +void EMD_wrap(int n1, int n2, double *X, double *Y, double *D, double *G, + double* alpha, double* beta, double *cost, int max_iter) { // beware M and C anre strored in row major C style!!! int n, m, i,cur; @@ -99,6 +99,8 @@ void EMD_wrap(int n1, int n2, double *X, double *Y, int i = di.source(a); int j = di.target(a); *(G+indI[i]*n2+indJ[j-n]) = net.flow(a); + *(alpha + indI[i]) = net.potential(i); + *(beta + indJ[j-n]) = net.potential(j); } }; diff --git a/ot/lp/__init__.py b/ot/lp/__init__.py index 673242d..915a18c 100644 --- a/ot/lp/__init__.py +++ b/ot/lp/__init__.py @@ -11,7 +11,7 @@ import multiprocessing -def emd(a, b, M, max_iter=-1): +def emd(a, b, M, dual_variables=False, max_iter=-1): """Solves the Earth Movers distance problem and returns the OT matrix @@ -80,7 +80,10 @@ def emd(a, b, M, max_iter=-1): if len(b) == 0: b = np.ones((M.shape[1], ), dtype=np.float64)/M.shape[1] - return emd_c(a, b, M, max_iter) + G, alpha, beta = emd_c(a, b, M, max_iter) + if dual_variables: + return G, alpha, beta + return G def emd2(a, b, M,processes=multiprocessing.cpu_count(), max_iter=-1): """Solves the Earth Movers distance problem and returns the loss @@ -151,12 +154,12 @@ def emd2(a, b, M,processes=multiprocessing.cpu_count(), max_iter=-1): b = np.ones((M.shape[1], ), dtype=np.float64)/M.shape[1] if len(b.shape)==1: - return emd2_c(a, b, M, max_iter) + return emd2_c(a, b, M, max_iter)[0] else: nb=b.shape[1] #res=[emd2_c(a,b[:,i].copy(),M) for i in range(nb)] def f(b): - return emd2_c(a,b,M, max_iter) + return emd2_c(a,b,M, max_iter)[0] res= parmap(f, [b[:,i] for i in range(nb)],processes) return np.array(res) diff --git a/ot/lp/emd_wrap.pyx b/ot/lp/emd_wrap.pyx index c4ba125..813596f 100644 --- a/ot/lp/emd_wrap.pyx +++ b/ot/lp/emd_wrap.pyx @@ -12,7 +12,8 @@ cimport cython cdef extern from "EMD.h": - void EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double *cost, int max_iter) + void EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double *cost, + double* alpha, double* beta, int max_iter) @@ -58,6 +59,8 @@ def emd_c( np.ndarray[double, ndim=1, mode="c"] a,np.ndarray[double, ndim=1, mod cdef float cost=0 cdef np.ndarray[double, ndim=2, mode="c"] G=np.zeros([n1, n2]) + cdef np.ndarray[double, ndim=1, mode="c"] alpha=np.zeros(n1) + cdef np.ndarray[double, ndim=1, mode="c"] beta=np.zeros(n2) if not len(a): a=np.ones((n1,))/n1 @@ -65,10 +68,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 + print alpha.size + print beta.size # calling the function - EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, &cost, maxiter) + EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, + alpha.data, beta.data, &cost, maxiter) - return G + return G, alpha, beta @cython.boundscheck(False) @cython.wraparound(False) @@ -112,15 +118,19 @@ def emd2_c( np.ndarray[double, ndim=1, mode="c"] a,np.ndarray[double, ndim=1, mo cdef float cost=0 cdef np.ndarray[double, ndim=2, mode="c"] G=np.zeros([n1, n2]) + + cdef np.ndarray[double, ndim = 1, mode = "c"] alpha = np.zeros([n1]) + cdef np.ndarray[double, ndim = 1, mode = "c"] beta = np.zeros([n2]) if not len(a): a=np.ones((n1,))/n1 if not len(b): b=np.ones((n2,))/n2 - # calling the function - EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, &cost, maxiter) + EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, + alpha.data, beta.data, &cost, maxiter) + - return cost + return cost, alpha, beta -- cgit v1.2.3 From db2a70b1f5146d6374af57f4bea66ab95b202e77 Mon Sep 17 00:00:00 2001 From: arolet Date: Fri, 21 Jul 2017 13:33:44 +0900 Subject: Compute cost with primal --- ot/lp/EMD_wrapper.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ot/lp/EMD_wrapper.cpp b/ot/lp/EMD_wrapper.cpp index 6bda6a7..c6cbb04 100644 --- a/ot/lp/EMD_wrapper.cpp +++ b/ot/lp/EMD_wrapper.cpp @@ -93,12 +93,14 @@ void EMD_wrap(int n1, int n2, double *X, double *Y, double *D, double *G, } } else { - *cost = net.totalCost(); + *cost = 0; Arc a; di.first(a); for (; a != INVALID; di.next(a)) { int i = di.source(a); int j = di.target(a); - *(G+indI[i]*n2+indJ[j-n]) = net.flow(a); + double flow = net.flow(a); + *cost += flow * (*(D+indI[i]*n2+indJ[j-n])); + *(G+indI[i]*n2+indJ[j-n]) = flow; *(alpha + indI[i]) = net.potential(i); *(beta + indJ[j-n]) = net.potential(j); } -- cgit v1.2.3 From c1980a414c879dd1bc1d8904fd43426326741385 Mon Sep 17 00:00:00 2001 From: arolet Date: Fri, 21 Jul 2017 13:34:09 +0900 Subject: Added and passed tests for dual variables --- ot/lp/EMD_wrapper.cpp | 2 +- ot/lp/emd_wrap.pyx | 4 ++-- test/test_emd.py | 28 +++++++++++++++++++--------- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/ot/lp/EMD_wrapper.cpp b/ot/lp/EMD_wrapper.cpp index c6cbb04..0977e75 100644 --- a/ot/lp/EMD_wrapper.cpp +++ b/ot/lp/EMD_wrapper.cpp @@ -101,7 +101,7 @@ void EMD_wrap(int n1, int n2, double *X, double *Y, double *D, double *G, double flow = net.flow(a); *cost += flow * (*(D+indI[i]*n2+indJ[j-n])); *(G+indI[i]*n2+indJ[j-n]) = flow; - *(alpha + indI[i]) = net.potential(i); + *(alpha + indI[i]) = -net.potential(i); *(beta + indJ[j-n]) = net.potential(j); } diff --git a/ot/lp/emd_wrap.pyx b/ot/lp/emd_wrap.pyx index 813596f..435a270 100644 --- a/ot/lp/emd_wrap.pyx +++ b/ot/lp/emd_wrap.pyx @@ -57,7 +57,7 @@ def emd_c( np.ndarray[double, ndim=1, mode="c"] a,np.ndarray[double, ndim=1, mod cdef int n1= M.shape[0] cdef int n2= M.shape[1] - cdef float cost=0 + cdef double cost=0 cdef np.ndarray[double, ndim=2, mode="c"] G=np.zeros([n1, n2]) cdef np.ndarray[double, ndim=1, mode="c"] alpha=np.zeros(n1) cdef np.ndarray[double, ndim=1, mode="c"] beta=np.zeros(n2) @@ -116,7 +116,7 @@ def emd2_c( np.ndarray[double, ndim=1, mode="c"] a,np.ndarray[double, ndim=1, mo cdef int n1= M.shape[0] cdef int n2= M.shape[1] - cdef float cost=0 + cdef double cost=0 cdef np.ndarray[double, ndim=2, mode="c"] G=np.zeros([n1, n2]) cdef np.ndarray[double, ndim = 1, mode = "c"] alpha = np.zeros([n1]) diff --git a/test/test_emd.py b/test/test_emd.py index 4757cd1..3bf6fa2 100644 --- a/test/test_emd.py +++ b/test/test_emd.py @@ -2,7 +2,6 @@ # -*- coding: utf-8 -*- import numpy as np -import pylab as pl import ot from ot.datasets import get_1D_gauss as gauss @@ -16,8 +15,6 @@ m=6000 # nb bins mean1 = 1000 mean2 = 1100 -tol = 1e-6 - # bin positions x=np.arange(n,dtype=np.float64) y=np.arange(m,dtype=np.float64) @@ -38,10 +35,11 @@ print('Computing {} EMD '.format(1)) # emd loss 1 proc ot.tic() -G = ot.emd(a,b,M) +G, alpha, beta = ot.emd(a,b,M, dual_variables=True) ot.toc('1 proc : {} s') cost1 = (G * M).sum() +cost_dual = np.vdot(a, alpha) + np.vdot(b, beta) # emd loss 1 proc ot.tic() @@ -49,11 +47,23 @@ cost_emd2 = ot.emd2(a,b,M) ot.toc('1 proc : {} s') ot.tic() -G = ot.emd(b, a, np.ascontiguousarray(M.T)) +G2 = ot.emd(b, a, np.ascontiguousarray(M.T)) ot.toc('1 proc : {} s') -cost2 = (G * M.T).sum() +cost2 = (G2 * M.T).sum() + +M_reduced = M - alpha.reshape(-1,1) - beta.reshape(1, -1) + +# Check that both cost computations are equivalent +np.testing.assert_almost_equal(cost1, cost_emd2) +# Check that dual and primal cost are equal +np.testing.assert_almost_equal(cost1, cost_dual) +# Check symmetry +np.testing.assert_almost_equal(cost1, cost2) +# Check with closed-form solution for gaussians +np.testing.assert_almost_equal(cost1, np.abs(mean1-mean2)) + +[ind1, ind2] = np.nonzero(G) -assert np.abs(cost1-cost_emd2)/np.abs(cost1) < tol -assert np.abs(cost1-cost2)/np.abs(cost1) < tol -assert np.abs(cost1-np.abs(mean1-mean2))/np.abs(cost1) < tol +# Check that reduced cost is zero on transport arcs +np.testing.assert_array_almost_equal((M - alpha.reshape(-1, 1) - beta.reshape(1, -1))[ind1, ind2], np.zeros(ind1.size)) \ No newline at end of file -- cgit v1.2.3 From 7ab9037f1e4a08439083d1bc5705be5ed2e9e10a Mon Sep 17 00:00:00 2001 From: Nicolas Courty Date: Mon, 28 Aug 2017 14:41:09 +0200 Subject: Gromov-Wasserstein distance --- README.md | 4 +- examples/plot_gromov.py | 98 ++++++++++ ot/__init__.py | 6 +- ot/gromov.py | 482 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 588 insertions(+), 2 deletions(-) create mode 100644 examples/plot_gromov.py create mode 100644 ot/gromov.py diff --git a/README.md b/README.md index 7a65106..a42f17b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ It provides the following solvers: * Conditional gradient [6] and Generalized conditional gradient for regularized OT [7]. * Joint OT matrix and mapping estimation [8]. * Wasserstein Discriminant Analysis [11] (requires autograd + pymanopt). - +* Gromov-Wasserstein distances [12] Some demonstrations (both in Python and Jupyter Notebook format) are available in the examples folder. @@ -182,3 +182,5 @@ You can also post bug reports and feature requests in Github issues. Make sure t [10] Chizat, L., Peyré, G., Schmitzer, B., & Vialard, F. X. (2016). [Scaling algorithms for unbalanced transport problems](https://arxiv.org/pdf/1607.05816.pdf). arXiv preprint arXiv:1607.05816. [11] Flamary, R., Cuturi, M., Courty, N., & Rakotomamonjy, A. (2016). [Wasserstein Discriminant Analysis](https://arxiv.org/pdf/1608.08063.pdf). arXiv preprint arXiv:1608.08063. + +[12] Peyré, Gabriel, Marco Cuturi, and Justin Solomon, [Gromov-Wasserstein averaging of kernel and distance matrices](http://proceedings.mlr.press/v48/peyre16.html) International Conference on Machine Learning (ICML). 2016. diff --git a/examples/plot_gromov.py b/examples/plot_gromov.py new file mode 100644 index 0000000..11e5336 --- /dev/null +++ b/examples/plot_gromov.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +""" +==================== +Gromov-Wasserstein example +==================== + +This example is designed to show how to use the Gromov-Wassertsein distance +computation in POT. + + +""" + +# Author: Erwan Vautier +# Nicolas Courty +# +# License: MIT License + +import scipy as sp +import numpy as np + +import ot +import matplotlib.pylab as pl +from mpl_toolkits.mplot3d import Axes3D + + + +""" +Sample two Gaussian distributions (2D and 3D) +==================== + +The Gromov-Wasserstein distance allows to compute distances with samples that do not belong to the same metric space. For +demonstration purpose, we sample two Gaussian distributions in 2- and 3-dimensional spaces. + +""" +n=30 # nb samples + +mu_s=np.array([0,0]) +cov_s=np.array([[1,0],[0,1]]) + +mu_t=np.array([4,4,4]) +cov_t=np.array([[1,0,0],[0,1,0],[0,0,1]]) + + + +xs=ot.datasets.get_2D_samples_gauss(n,mu_s,cov_s) +P=sp.linalg.sqrtm(cov_t) +xt= np.random.randn(n,3).dot(P)+mu_t + + + +""" +Plotting the distributions +==================== +""" +fig=pl.figure() +ax1=fig.add_subplot(121) +ax1.plot(xs[:,0],xs[:,1],'+b',label='Source samples') +ax2=fig.add_subplot(122,projection='3d') +ax2.scatter(xt[:,0],xt[:,1],xt[:,2],color='r') +pl.show() + + +""" +Compute distance kernels, normalize them and then display +==================== +""" + +C1=sp.spatial.distance.cdist(xs,xs) +C2=sp.spatial.distance.cdist(xt,xt) + +C1/=C1.max() +C2/=C2.max() + +pl.figure() +pl.subplot(121) +pl.imshow(C1) +pl.subplot(122) +pl.imshow(C2) +pl.show() + +""" +Compute Gromov-Wasserstein plans and distance +==================== +""" + +p=ot.unif(n) +q=ot.unif(n) + +gw=ot.gromov_wasserstein(C1,C2,p,q,'square_loss',epsilon=5e-4) +gw_dist=ot.gromov_wasserstein2(C1,C2,p,q,'square_loss',epsilon=5e-4) + +print('Gromov-Wasserstein distances between the distribution: '+str(gw_dist)) + +pl.figure() +pl.imshow(gw,cmap='jet') +pl.colorbar() +pl.show() + diff --git a/ot/__init__.py b/ot/__init__.py index 6d4c4c6..a295e1b 100644 --- a/ot/__init__.py +++ b/ot/__init__.py @@ -5,6 +5,7 @@ """ # Author: Remi Flamary +# Nicolas Courty # # License: MIT License @@ -17,11 +18,13 @@ from . import utils from . import datasets from . import plot from . import da +from . import gromov # OT functions from .lp import emd, emd2 from .bregman import sinkhorn, sinkhorn2, barycenter from .da import sinkhorn_lpl1_mm +from .gromov import gromov_wasserstein, gromov_wasserstein2 # utils functions from .utils import dist, unif, tic, toc, toq @@ -30,4 +33,5 @@ __version__ = "0.3.1" __all__ = ["emd", "emd2", "sinkhorn", "sinkhorn2", "utils", 'datasets', 'bregman', 'lp', 'plot', 'tic', 'toc', 'toq', - 'dist', 'unif', 'barycenter', 'sinkhorn_lpl1_mm', 'da', 'optim'] + 'dist', 'unif', 'barycenter', 'sinkhorn_lpl1_mm', 'da', 'optim', + 'gromov_wasserstein','gromov_wasserstein2'] diff --git a/ot/gromov.py b/ot/gromov.py new file mode 100644 index 0000000..f3c62c9 --- /dev/null +++ b/ot/gromov.py @@ -0,0 +1,482 @@ + +# -*- coding: utf-8 -*- +""" +Gromov-Wasserstein transport method +=================================== + + +""" + +# Author: Erwan Vautier +# Nicolas Courty +# +# License: MIT License + +import numpy as np + +from .bregman import sinkhorn +from .utils import dist + +def square_loss(a,b): + """ + Returns the value of L(a,b)=(1/2)*|a-b|^2 + """ + + return (1/2)*(a-b)**2 + +def kl_loss(a,b): + """ + Returns the value of L(a,b)=a*log(a/b)-a+b + """ + + return a*np.log(a/b)-a+b + +def tensor_square_loss(C1,C2,T): + """ + Returns the value of \mathcal{L}(C1,C2) \otimes T with the square loss + + function as the loss function of Gromow-Wasserstein discrepancy. + + Where : + + C1 : Metric cost matrix in the source space + C2 : Metric cost matrix in the target space + T : A coupling between those two spaces + + The square-loss function L(a,b)=(1/2)*|a-b|^2 is read as : + L(a,b) = f1(a)+f2(b)-h1(a)*h2(b) with : + f1(a)=(a^2)/2 + f2(b)=(b^2)/2 + h1(a)=a + h2(b)=b + + Parameters + ---------- + C1 : np.ndarray(ns,ns) + Metric cost matrix in the source space + C2 : np.ndarray(nt,nt) + Metric costfr matrix in the target space + T : np.ndarray(ns,nt) + Coupling between source and target spaces + + + Returns + ------- + tens : (ns*nt) ndarray + \mathcal{L}(C1,C2) \otimes T tensor-matrix multiplication result + + + """ + + + C1=np.asarray(C1,dtype=np.float64) + C2=np.asarray(C2,dtype=np.float64) + T=np.asarray(T,dtype=np.float64) + + + def f1(a): + return (a**2)/2 + + def f2(b): + return (b**2)/2 + + def h1(a): + return a + + def h2(b): + return b + + tens=-np.dot(h1(C1),T).dot(h2(C2).T) + tens=tens-tens.min() + + + return np.array(tens) + + +def tensor_kl_loss(C1,C2,T): + """ + Returns the value of \mathcal{L}(C1,C2) \otimes T with the square loss + + function as the loss function of Gromow-Wasserstein discrepancy. + + Where : + + C1 : Metric cost matrix in the source space + C2 : Metric cost matrix in the target space + T : A coupling between those two spaces + + The square-loss function L(a,b)=(1/2)*|a-b|^2 is read as : + L(a,b) = f1(a)+f2(b)-h1(a)*h2(b) with : + f1(a)=a*log(a)-a + f2(b)=b + h1(a)=a + h2(b)=log(b) + + Parameters + ---------- + C1 : np.ndarray(ns,ns) + Metric cost matrix in the source space + C2 : np.ndarray(nt,nt) + Metric costfr matrix in the target space + T : np.ndarray(ns,nt) + Coupling between source and target spaces + + + Returns + ------- + tens : (ns*nt) ndarray + \mathcal{L}(C1,C2) \otimes T tensor-matrix multiplication result + + References + ---------- + + .. [12] Peyré, Gabriel, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. + + """ + + + C1=np.asarray(C1,dtype=np.float64) + C2=np.asarray(C2,dtype=np.float64) + T=np.asarray(T,dtype=np.float64) + + + def f1(a): + return a*np.log(a+1e-15)-a + + def f2(b): + return b + + def h1(a): + return a + + def h2(b): + return np.log(b+1e-15) + + tens=-np.dot(h1(C1),T).dot(h2(C2).T) + tens=tens-tens.min() + + + + return np.array(tens) + +def update_square_loss(p,lambdas,T,Cs): + """ + Updates C according to the L2 Loss kernel with the S Ts couplings calculated at each iteration + + + Parameters + ---------- + p : np.ndarray(N,) + weights in the targeted barycenter + lambdas : list of the S spaces' weights + T : list of S np.ndarray(ns,N) + the S Ts couplings calculated at each iteration + Cs : Cs : list of S np.ndarray(ns,ns) + Metric cost matrices + + Returns + ---------- + C updated + + + """ + tmpsum=np.sum([lambdas[s]*np.dot(T[s].T,Cs[s]).dot(T[s]) for s in range(len(T))]) + ppt=np.dot(p,p.T) + + return(np.divide(tmpsum,ppt)) + +def update_kl_loss(p,lambdas,T,Cs): + """ + Updates C according to the KL Loss kernel with the S Ts couplings calculated at each iteration + + + Parameters + ---------- + p : np.ndarray(N,) + weights in the targeted barycenter + lambdas : list of the S spaces' weights + T : list of S np.ndarray(ns,N) + the S Ts couplings calculated at each iteration + Cs : Cs : list of S np.ndarray(ns,ns) + Metric cost matrices + + Returns + ---------- + C updated + + + """ + tmpsum=np.sum([lambdas[s]*np.dot(T[s].T,Cs[s]).dot(T[s]) for s in range(len(T))]) + ppt=np.dot(p,p.T) + + return(np.exp(np.divide(tmpsum,ppt))) + + +def gromov_wasserstein(C1,C2,p,q,loss_fun,epsilon,numItermax = 1000, stopThr=1e-9,verbose=False, log=False): + """ + Returns the gromov-wasserstein coupling between the two measured similarity matrices + + (C1,p) and (C2,q) + + The function solves the following optimization problem: + + .. math:: + \GW = arg\min_T \sum_{i,j,k,l} L(C1_{i,k},C2_{j,l})*T_{i,j}*T_{k,l}-\epsilon(H(T)) + + s.t. \GW 1 = p + + \GW^T 1= q + + \GW\geq 0 + + Where : + + C1 : Metric cost matrix in the source space + C2 : Metric cost matrix in the target space + p : distribution in the source space + q : distribution in the target space + L : loss function to account for the misfit between the similarity matrices + H : entropy + + + Parameters + ---------- + C1 : np.ndarray(ns,ns) + Metric cost matrix in the source space + C2 : np.ndarray(nt,nt) + Metric costfr matrix in the target space + p : np.ndarray(ns,) + distribution in the source space + q : np.ndarray(nt) + distribution in the target space + loss_fun : loss function used for the solver either 'square_loss' or 'kl_loss' + epsilon : float + Regularization term >0 + numItermax : int, optional + Max number of iterations + stopThr : float, optional + Stop threshold on error (>0) + verbose : bool, optional + Print information along iterations + log : bool, optional + record log if True + forcing : np.ndarray(N,2) + list of forced couplings (where N is the number of forcing) + + Returns + ------- + T : coupling between the two spaces that minimizes : + \sum_{i,j,k,l} L(C1_{i,k},C2_{j,l})*T_{i,j}*T_{k,l}-\epsilon(H(T)) + + """ + + + C1=np.asarray(C1,dtype=np.float64) + C2=np.asarray(C2,dtype=np.float64) + + T=np.dot(p,q.T) #Initialization + + cpt = 0 + err=1 + + while (err>stopThr and cpt0 + numItermax : int, optional + Max number of iterations + stopThr : float, optional + Stop threshold on error (>0) + verbose : bool, optional + Print information along iterations + log : bool, optional + record log if True + forcing : np.ndarray(N,2) + list of forced couplings (where N is the number of forcing) + + Returns + ------- + T : coupling between the two spaces that minimizes : + \sum_{i,j,k,l} L(C1_{i,k},C2_{j,l})*T_{i,j}*T_{k,l}-\epsilon(H(T)) + + """ + + if log: + gw,logv=gromov_wasserstein(C1,C2,p,q,loss_fun,epsilon,numItermax,stopThr,verbose,log) + else: + gw=gromov_wasserstein(C1,C2,p,q,loss_fun,epsilon,numItermax,stopThr,verbose,log) + + if loss_fun=='square_loss': + gw_dist=np.sum(gw*tensor_square_loss(C1,C2,gw)) + + + elif loss_fun=='kl_loss': + gw_dist=np.sum(gw*tensor_kl_loss(C1,C2,gw)) + + if log: + return gw_dist, logv + else: + return gw_dist + + +def gromov_barycenters(N,Cs,ps,p,lambdas,loss_fun,epsilon,numItermax = 1000, stopThr=1e-9, verbose=False, log=False): + """ + Returns the gromov-wasserstein barycenters of S measured similarity matrices + + (Cs)_{s=1}^{s=S} + + The function solves the following optimization problem: + + .. math:: + C = argmin_C\in R^NxN \sum_s \lambda_s GW(C,Cs,p,ps) + + + Where : + + Cs : metric cost matrix + ps : distribution + + Parameters + ---------- + N : Integer + Size of the targeted barycenter + Cs : list of S np.ndarray(ns,ns) + Metric cost matrices + ps : list of S np.ndarray(ns,) + sample weights in the S spaces + p : np.ndarray(N,) + weights in the targeted barycenter + lambdas : list of the S spaces' weights + L : tensor-matrix multiplication function based on specific loss function + update : function(p,lambdas,T,Cs) that updates C according to a specific Kernel + with the S Ts couplings calculated at each iteration + epsilon : float + Regularization term >0 + numItermax : int, optional + Max number of iterations + stopThr : float, optional + Stop threshol on error (>0) + verbose : bool, optional + Print information along iterations + log : bool, optional + record log if True + + Returns + ------- + C : Similarity matrix in the barycenter space (permutated arbitrarily) + + """ + + + S=len(Cs) + + Cs=[np.asarray(Cs[s],dtype=np.float64) for s in range(S)] + lambdas=np.asarray(lambdas,dtype=np.float64) + + T=[0 for s in range(S)] + + #Initialization of C : random SPD matrix + xalea=np.random.randn(N,2) + C=dist(xalea,xalea) + C/=C.max() + + cpt=0 + err=1 + + error=[] + + while(err>stopThr and cpt Date: Mon, 28 Aug 2017 15:04:04 +0200 Subject: gromov:flake8 and other --- examples/plot_gromov.py | 63 ++++---- ot/gromov.py | 382 ++++++++++++++++++++++++------------------------ 2 files changed, 216 insertions(+), 229 deletions(-) diff --git a/examples/plot_gromov.py b/examples/plot_gromov.py index 11e5336..a33fde1 100644 --- a/examples/plot_gromov.py +++ b/examples/plot_gromov.py @@ -3,11 +3,8 @@ ==================== Gromov-Wasserstein example ==================== - -This example is designed to show how to use the Gromov-Wassertsein distance -computation in POT. - - +This example is designed to show how to use the Gromov-Wassertsein distance +computation in POT. """ # Author: Erwan Vautier @@ -20,43 +17,38 @@ import numpy as np import ot import matplotlib.pylab as pl -from mpl_toolkits.mplot3d import Axes3D - """ Sample two Gaussian distributions (2D and 3D) ==================== - -The Gromov-Wasserstein distance allows to compute distances with samples that do not belong to the same metric space. For -demonstration purpose, we sample two Gaussian distributions in 2- and 3-dimensional spaces. - +The Gromov-Wasserstein distance allows to compute distances with samples that do not belong to the same metric space. +For demonstration purpose, we sample two Gaussian distributions in 2- and 3-dimensional spaces. """ -n=30 # nb samples -mu_s=np.array([0,0]) -cov_s=np.array([[1,0],[0,1]]) +n = 30 # nb samples -mu_t=np.array([4,4,4]) -cov_t=np.array([[1,0,0],[0,1,0],[0,0,1]]) +mu_s = np.array([0, 0]) +cov_s = np.array([[1, 0], [0, 1]]) +mu_t = np.array([4, 4, 4]) +cov_t = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) -xs=ot.datasets.get_2D_samples_gauss(n,mu_s,cov_s) -P=sp.linalg.sqrtm(cov_t) -xt= np.random.randn(n,3).dot(P)+mu_t - +xs = ot.datasets.get_2D_samples_gauss(n, mu_s, cov_s) +P = sp.linalg.sqrtm(cov_t) +xt = np.random.randn(n, 3).dot(P) + mu_t """ Plotting the distributions ==================== """ -fig=pl.figure() -ax1=fig.add_subplot(121) -ax1.plot(xs[:,0],xs[:,1],'+b',label='Source samples') -ax2=fig.add_subplot(122,projection='3d') -ax2.scatter(xt[:,0],xt[:,1],xt[:,2],color='r') +fig = pl.figure() +ax1 = fig.add_subplot(121) +ax1.plot(xs[:, 0], xs[:, 1], '+b', label='Source samples') +ax2 = fig.add_subplot(122, projection='3d') +ax2.scatter(xt[:, 0], xt[:, 1], xt[:, 2], color='r') pl.show() @@ -65,11 +57,11 @@ Compute distance kernels, normalize them and then display ==================== """ -C1=sp.spatial.distance.cdist(xs,xs) -C2=sp.spatial.distance.cdist(xt,xt) +C1 = sp.spatial.distance.cdist(xs, xs) +C2 = sp.spatial.distance.cdist(xt, xt) -C1/=C1.max() -C2/=C2.max() +C1 /= C1.max() +C2 /= C2.max() pl.figure() pl.subplot(121) @@ -83,16 +75,15 @@ Compute Gromov-Wasserstein plans and distance ==================== """ -p=ot.unif(n) -q=ot.unif(n) +p = ot.unif(n) +q = ot.unif(n) -gw=ot.gromov_wasserstein(C1,C2,p,q,'square_loss',epsilon=5e-4) -gw_dist=ot.gromov_wasserstein2(C1,C2,p,q,'square_loss',epsilon=5e-4) +gw = ot.gromov_wasserstein(C1, C2, p, q, 'square_loss', epsilon=5e-4) +gw_dist = ot.gromov_wasserstein2(C1, C2, p, q, 'square_loss', epsilon=5e-4) -print('Gromov-Wasserstein distances between the distribution: '+str(gw_dist)) +print('Gromov-Wasserstein distances between the distribution: ' + str(gw_dist)) pl.figure() -pl.imshow(gw,cmap='jet') +pl.imshow(gw, cmap='jet') pl.colorbar() pl.show() - diff --git a/ot/gromov.py b/ot/gromov.py index f3c62c9..7cf3b42 100644 --- a/ot/gromov.py +++ b/ot/gromov.py @@ -17,39 +17,41 @@ import numpy as np from .bregman import sinkhorn from .utils import dist -def square_loss(a,b): + +def square_loss(a, b): """ Returns the value of L(a,b)=(1/2)*|a-b|^2 """ - - return (1/2)*(a-b)**2 -def kl_loss(a,b): + return (1 / 2) * (a - b)**2 + + +def kl_loss(a, b): """ Returns the value of L(a,b)=a*log(a/b)-a+b """ - - return a*np.log(a/b)-a+b -def tensor_square_loss(C1,C2,T): + return a * np.log(a / b) - a + b + + +def tensor_square_loss(C1, C2, T): """ - Returns the value of \mathcal{L}(C1,C2) \otimes T with the square loss - + Returns the value of \mathcal{L}(C1,C2) \otimes T with the square loss function as the loss function of Gromow-Wasserstein discrepancy. - + Where : - + C1 : Metric cost matrix in the source space C2 : Metric cost matrix in the target space T : A coupling between those two spaces - + The square-loss function L(a,b)=(1/2)*|a-b|^2 is read as : L(a,b) = f1(a)+f2(b)-h1(a)*h2(b) with : f1(a)=(a^2)/2 f2(b)=(b^2)/2 h1(a)=a h2(b)=b - + Parameters ---------- C1 : np.ndarray(ns,ns) @@ -64,54 +66,50 @@ def tensor_square_loss(C1,C2,T): ------- tens : (ns*nt) ndarray \mathcal{L}(C1,C2) \otimes T tensor-matrix multiplication result - - + + """ - - C1=np.asarray(C1,dtype=np.float64) - C2=np.asarray(C2,dtype=np.float64) - T=np.asarray(T,dtype=np.float64) - - + C1 = np.asarray(C1, dtype=np.float64) + C2 = np.asarray(C2, dtype=np.float64) + T = np.asarray(T, dtype=np.float64) + def f1(a): - return (a**2)/2 - + return (a**2) / 2 + def f2(b): - return (b**2)/2 - + return (b**2) / 2 + def h1(a): return a - + def h2(b): return b - - tens=-np.dot(h1(C1),T).dot(h2(C2).T) - tens=tens-tens.min() - + tens = -np.dot(h1(C1), T).dot(h2(C2).T) + tens = tens - tens.min() + return np.array(tens) - - -def tensor_kl_loss(C1,C2,T): + + +def tensor_kl_loss(C1, C2, T): """ - Returns the value of \mathcal{L}(C1,C2) \otimes T with the square loss - + Returns the value of \mathcal{L}(C1,C2) \otimes T with the square loss function as the loss function of Gromow-Wasserstein discrepancy. - + Where : - + C1 : Metric cost matrix in the source space C2 : Metric cost matrix in the target space T : A coupling between those two spaces - + The square-loss function L(a,b)=(1/2)*|a-b|^2 is read as : L(a,b) = f1(a)+f2(b)-h1(a)*h2(b) with : f1(a)=a*log(a)-a f2(b)=b h1(a)=a h2(b)=log(b) - + Parameters ---------- C1 : np.ndarray(ns,ns) @@ -126,44 +124,41 @@ def tensor_kl_loss(C1,C2,T): ------- tens : (ns*nt) ndarray \mathcal{L}(C1,C2) \otimes T tensor-matrix multiplication result - + References ---------- - + .. [12] Peyré, Gabriel, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. - + """ - - C1=np.asarray(C1,dtype=np.float64) - C2=np.asarray(C2,dtype=np.float64) - T=np.asarray(T,dtype=np.float64) - - + C1 = np.asarray(C1, dtype=np.float64) + C2 = np.asarray(C2, dtype=np.float64) + T = np.asarray(T, dtype=np.float64) + def f1(a): - return a*np.log(a+1e-15)-a - + return a * np.log(a + 1e-15) - a + def f2(b): return b - + def h1(a): return a - + def h2(b): - return np.log(b+1e-15) - - tens=-np.dot(h1(C1),T).dot(h2(C2).T) - tens=tens-tens.min() + return np.log(b + 1e-15) + + tens = -np.dot(h1(C1), T).dot(h2(C2).T) + tens = tens - tens.min() - - return np.array(tens) -def update_square_loss(p,lambdas,T,Cs): + +def update_square_loss(p, lambdas, T, Cs): """ Updates C according to the L2 Loss kernel with the S Ts couplings calculated at each iteration - - + + Parameters ---------- p : np.ndarray(N,) @@ -173,23 +168,25 @@ def update_square_loss(p,lambdas,T,Cs): the S Ts couplings calculated at each iteration Cs : Cs : list of S np.ndarray(ns,ns) Metric cost matrices - - Returns + + Returns ---------- C updated - - + + """ - tmpsum=np.sum([lambdas[s]*np.dot(T[s].T,Cs[s]).dot(T[s]) for s in range(len(T))]) - ppt=np.dot(p,p.T) - - return(np.divide(tmpsum,ppt)) + tmpsum = np.sum([lambdas[s] * np.dot(T[s].T, Cs[s]).dot(T[s]) + for s in range(len(T))]) + ppt = np.dot(p, p.T) -def update_kl_loss(p,lambdas,T,Cs): + return(np.divide(tmpsum, ppt)) + + +def update_kl_loss(p, lambdas, T, Cs): """ Updates C according to the KL Loss kernel with the S Ts couplings calculated at each iteration - - + + Parameters ---------- p : np.ndarray(N,) @@ -199,25 +196,26 @@ def update_kl_loss(p,lambdas,T,Cs): the S Ts couplings calculated at each iteration Cs : Cs : list of S np.ndarray(ns,ns) Metric cost matrices - - Returns + + Returns ---------- C updated - - + + """ - tmpsum=np.sum([lambdas[s]*np.dot(T[s].T,Cs[s]).dot(T[s]) for s in range(len(T))]) - ppt=np.dot(p,p.T) - - return(np.exp(np.divide(tmpsum,ppt))) + tmpsum = np.sum([lambdas[s] * np.dot(T[s].T, Cs[s]).dot(T[s]) + for s in range(len(T))]) + ppt = np.dot(p, p.T) + + return(np.exp(np.divide(tmpsum, ppt))) -def gromov_wasserstein(C1,C2,p,q,loss_fun,epsilon,numItermax = 1000, stopThr=1e-9,verbose=False, log=False): +def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopThr=1e-9, verbose=False, log=False): """ Returns the gromov-wasserstein coupling between the two measured similarity matrices - + (C1,p) and (C2,q) - + The function solves the following optimization problem: .. math:: @@ -228,17 +226,17 @@ def gromov_wasserstein(C1,C2,p,q,loss_fun,epsilon,numItermax = 1000, stopThr=1e- \GW^T 1= q \GW\geq 0 - + Where : - + C1 : Metric cost matrix in the source space C2 : Metric cost matrix in the target space p : distribution in the source space - q : distribution in the target space + q : distribution in the target space L : loss function to account for the misfit between the similarity matrices H : entropy - - + + Parameters ---------- C1 : np.ndarray(ns,ns) @@ -259,80 +257,80 @@ def gromov_wasserstein(C1,C2,p,q,loss_fun,epsilon,numItermax = 1000, stopThr=1e- verbose : bool, optional Print information along iterations log : bool, optional - record log if True + record log if True forcing : np.ndarray(N,2) list of forced couplings (where N is the number of forcing) - + Returns ------- T : coupling between the two spaces that minimizes : \sum_{i,j,k,l} L(C1_{i,k},C2_{j,l})*T_{i,j}*T_{k,l}-\epsilon(H(T)) - + """ - - C1=np.asarray(C1,dtype=np.float64) - C2=np.asarray(C2,dtype=np.float64) + C1 = np.asarray(C1, dtype=np.float64) + C2 = np.asarray(C2, dtype=np.float64) + + T = np.dot(p, q.T) # Initialization - T=np.dot(p,q.T) #Initialization - cpt = 0 - err=1 - - while (err>stopThr and cpt stopThr and cpt < numItermax): + + Tprev = T + + if loss_fun == 'square_loss': + tens = tensor_square_loss(C1, C2, T) + + elif loss_fun == 'kl_loss': + tens = tensor_kl_loss(C1, C2, T) + + T = sinkhorn(p, q, tens, epsilon) + + if cpt % 10 == 0: # we can speed up the process by checking for the error only all the 10th iterations - err=np.linalg.norm(T-Tprev) - + err = np.linalg.norm(T - Tprev) + 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)) - - cpt=cpt+1 + if cpt % 200 == 0: + print('{:5s}|{:12s}'.format( + 'It.', 'Err') + '\n' + '-' * 19) + print('{:5d}|{:8e}|'.format(cpt, err)) + + cpt = cpt + 1 if log: - return T,log + return T, log else: return T -def gromov_wasserstein2(C1,C2,p,q,loss_fun,epsilon,numItermax = 1000, stopThr=1e-9,verbose=False, log=False): +def gromov_wasserstein2(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopThr=1e-9, verbose=False, log=False): """ Returns the gromov-wasserstein discrepancy between the two measured similarity matrices - + (C1,p) and (C2,q) - + The function solves the following optimization problem: .. math:: \GW_Dist = \min_T \sum_{i,j,k,l} L(C1_{i,k},C2_{j,l})*T_{i,j}*T_{k,l}-\epsilon(H(T)) - + Where : - + C1 : Metric cost matrix in the source space C2 : Metric cost matrix in the target space p : distribution in the source space - q : distribution in the target space + q : distribution in the target space L : loss function to account for the misfit between the similarity matrices H : entropy - - + + Parameters ---------- C1 : np.ndarray(ns,ns) @@ -353,55 +351,56 @@ def gromov_wasserstein2(C1,C2,p,q,loss_fun,epsilon,numItermax = 1000, stopThr=1e verbose : bool, optional Print information along iterations log : bool, optional - record log if True + record log if True forcing : np.ndarray(N,2) list of forced couplings (where N is the number of forcing) - + Returns ------- T : coupling between the two spaces that minimizes : \sum_{i,j,k,l} L(C1_{i,k},C2_{j,l})*T_{i,j}*T_{k,l}-\epsilon(H(T)) - + """ - + if log: - gw,logv=gromov_wasserstein(C1,C2,p,q,loss_fun,epsilon,numItermax,stopThr,verbose,log) + gw, logv = gromov_wasserstein( + C1, C2, p, q, loss_fun, epsilon, numItermax, stopThr, verbose, log) else: - gw=gromov_wasserstein(C1,C2,p,q,loss_fun,epsilon,numItermax,stopThr,verbose,log) + gw = gromov_wasserstein(C1, C2, p, q, loss_fun, + epsilon, numItermax, stopThr, verbose, log) + + if loss_fun == 'square_loss': + gw_dist = np.sum(gw * tensor_square_loss(C1, C2, gw)) - if loss_fun=='square_loss': - gw_dist=np.sum(gw*tensor_square_loss(C1,C2,gw)) - - - elif loss_fun=='kl_loss': - gw_dist=np.sum(gw*tensor_kl_loss(C1,C2,gw)) + elif loss_fun == 'kl_loss': + gw_dist = np.sum(gw * tensor_kl_loss(C1, C2, gw)) if log: return gw_dist, logv - else: + else: return gw_dist -def gromov_barycenters(N,Cs,ps,p,lambdas,loss_fun,epsilon,numItermax = 1000, stopThr=1e-9, verbose=False, log=False): +def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, numItermax=1000, stopThr=1e-9, verbose=False, log=False): """ Returns the gromov-wasserstein barycenters of S measured similarity matrices - + (Cs)_{s=1}^{s=S} - + The function solves the following optimization problem: .. math:: C = argmin_C\in R^NxN \sum_s \lambda_s GW(C,Cs,p,ps) - + Where : - + Cs : metric cost matrix ps : distribution - + Parameters ---------- - N : Integer + N : Integer Size of the targeted barycenter Cs : list of S np.ndarray(ns,ns) Metric cost matrices @@ -422,61 +421,58 @@ def gromov_barycenters(N,Cs,ps,p,lambdas,loss_fun,epsilon,numItermax = 1000, sto verbose : bool, optional Print information along iterations log : bool, optional - record log if True - + record log if True + Returns ------- C : Similarity matrix in the barycenter space (permutated arbitrarily) - + """ - - - S=len(Cs) - - Cs=[np.asarray(Cs[s],dtype=np.float64) for s in range(S)] - lambdas=np.asarray(lambdas,dtype=np.float64) - - T=[0 for s in range(S)] - - #Initialization of C : random SPD matrix - xalea=np.random.randn(N,2) - C=dist(xalea,xalea) - C/=C.max() - - cpt=0 - err=1 - - error=[] - - while(err>stopThr and cpt stopThr and cpt < numItermax): + + Cprev = C + + T = [gromov_wasserstein(Cs[s], C, ps[s], p, loss_fun, epsilon, + numItermax, 1e-5, verbose, log) for s in range(S)] + + if loss_fun == 'square_loss': + C = update_square_loss(p, lambdas, T, Cs) + + elif loss_fun == 'kl_loss': + C = update_kl_loss(p, lambdas, T, Cs) + + if cpt % 10 == 0: # we can speed up the process by checking for the error only all the 10th iterations - err=np.linalg.norm(C-Cprev) + err = np.linalg.norm(C - Cprev) error.append(err) - + 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)) - - cpt=cpt+1 - - return C - + if cpt % 200 == 0: + print('{:5s}|{:12s}'.format( + 'It.', 'Err') + '\n' + '-' * 19) + print('{:5d}|{:8e}|'.format(cpt, err)) + cpt = cpt + 1 - \ No newline at end of file + return C -- cgit v1.2.3 From 3007f1da1094f93fa4216386666085cf60316b04 Mon Sep 17 00:00:00 2001 From: Nicolas Courty Date: Thu, 31 Aug 2017 16:44:18 +0200 Subject: Minor corrections suggested by @agramfort + new barycenter example + test function --- README.md | 2 +- data/carre.png | Bin 0 -> 168 bytes data/coeur.png | Bin 0 -> 225 bytes data/rond.png | Bin 0 -> 230 bytes data/triangle.png | Bin 0 -> 254 bytes examples/plot_gromov.py | 14 +-- examples/plot_gromov_barycenter.py | 240 +++++++++++++++++++++++++++++++++++++ ot/gromov.py | 36 +++--- test/test_gromov.py | 38 ++++++ 9 files changed, 302 insertions(+), 28 deletions(-) create mode 100755 data/carre.png create mode 100755 data/coeur.png create mode 100755 data/rond.png create mode 100755 data/triangle.png create mode 100755 examples/plot_gromov_barycenter.py create mode 100644 test/test_gromov.py diff --git a/README.md b/README.md index a42f17b..53672ae 100644 --- a/README.md +++ b/README.md @@ -183,4 +183,4 @@ You can also post bug reports and feature requests in Github issues. Make sure t [11] Flamary, R., Cuturi, M., Courty, N., & Rakotomamonjy, A. (2016). [Wasserstein Discriminant Analysis](https://arxiv.org/pdf/1608.08063.pdf). arXiv preprint arXiv:1608.08063. -[12] Peyré, Gabriel, Marco Cuturi, and Justin Solomon, [Gromov-Wasserstein averaging of kernel and distance matrices](http://proceedings.mlr.press/v48/peyre16.html) International Conference on Machine Learning (ICML). 2016. +[12] Gabriel Peyré, Marco Cuturi, and Justin Solomon, [Gromov-Wasserstein averaging of kernel and distance matrices](http://proceedings.mlr.press/v48/peyre16.html) International Conference on Machine Learning (ICML). 2016. diff --git a/data/carre.png b/data/carre.png new file mode 100755 index 0000000..45ff0ef Binary files /dev/null and b/data/carre.png differ diff --git a/data/coeur.png b/data/coeur.png new file mode 100755 index 0000000..3f511a6 Binary files /dev/null and b/data/coeur.png differ diff --git a/data/rond.png b/data/rond.png new file mode 100755 index 0000000..1c1a068 Binary files /dev/null and b/data/rond.png differ diff --git a/data/triangle.png b/data/triangle.png new file mode 100755 index 0000000..ca36d09 Binary files /dev/null and b/data/triangle.png differ diff --git a/examples/plot_gromov.py b/examples/plot_gromov.py index a33fde1..9bbdbde 100644 --- a/examples/plot_gromov.py +++ b/examples/plot_gromov.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- """ -==================== +========================== Gromov-Wasserstein example -==================== +========================== This example is designed to show how to use the Gromov-Wassertsein distance computation in POT. """ @@ -14,14 +14,14 @@ computation in POT. import scipy as sp import numpy as np +import matplotlib.pylab as pl import ot -import matplotlib.pylab as pl """ Sample two Gaussian distributions (2D and 3D) -==================== +============================================= The Gromov-Wasserstein distance allows to compute distances with samples that do not belong to the same metric space. For demonstration purpose, we sample two Gaussian distributions in 2- and 3-dimensional spaces. """ @@ -42,7 +42,7 @@ xt = np.random.randn(n, 3).dot(P) + mu_t """ Plotting the distributions -==================== +========================== """ fig = pl.figure() ax1 = fig.add_subplot(121) @@ -54,7 +54,7 @@ pl.show() """ Compute distance kernels, normalize them and then display -==================== +========================================================= """ C1 = sp.spatial.distance.cdist(xs, xs) @@ -72,7 +72,7 @@ pl.show() """ Compute Gromov-Wasserstein plans and distance -==================== +============================================= """ p = ot.unif(n) diff --git a/examples/plot_gromov_barycenter.py b/examples/plot_gromov_barycenter.py new file mode 100755 index 0000000..6a72b3b --- /dev/null +++ b/examples/plot_gromov_barycenter.py @@ -0,0 +1,240 @@ +# -*- coding: utf-8 -*- +""" +===================================== +Gromov-Wasserstein Barycenter example +===================================== +This example is designed to show how to use the Gromov-Wassertsein distance +computation in POT. +""" + +# Author: Erwan Vautier +# Nicolas Courty +# +# License: MIT License + + +import numpy as np +import scipy as sp + +import scipy.ndimage as spi +import matplotlib.pylab as pl +from sklearn import manifold +from sklearn.decomposition import PCA + +import ot + +""" + +Smacof MDS +========== +This function allows to find an embedding of points given a dissimilarity matrix +that will be given by the output of the algorithm +""" + + +def smacof_mds(C, dim, maxIter=3000, eps=1e-9): + """ + Returns an interpolated point cloud following the dissimilarity matrix C using SMACOF + multidimensional scaling (MDS) in specific dimensionned target space + + Parameters + ---------- + C : np.ndarray(ns,ns) + dissimilarity matrix + dim : Integer + dimension of the targeted space + maxIter : Maximum number of iterations of the SMACOF algorithm for a single run + + eps : relative tolerance w.r.t stress to declare converge + + + Returns + ------- + npos : R**dim ndarray + Embedded coordinates of the interpolated point cloud (defined with one isometry) + + + """ + + seed = np.random.RandomState(seed=3) + + mds = manifold.MDS( + dim, + max_iter=3000, + eps=1e-9, + dissimilarity='precomputed', + n_init=1) + pos = mds.fit(C).embedding_ + + nmds = manifold.MDS( + 2, + max_iter=3000, + eps=1e-9, + dissimilarity="precomputed", + random_state=seed, + n_init=1) + npos = nmds.fit_transform(C, init=pos) + + return npos + + +""" +Data preparation +================ +The four distributions are constructed from 4 simple images +""" + + +def im2mat(I): + """Converts and image to matrix (one pixel per line)""" + return I.reshape((I.shape[0] * I.shape[1], I.shape[2])) + + +carre = spi.imread('../data/carre.png').astype(np.float64) / 256 +rond = spi.imread('../data/rond.png').astype(np.float64) / 256 +triangle = spi.imread('../data/triangle.png').astype(np.float64) / 256 +fleche = spi.imread('../data/coeur.png').astype(np.float64) / 256 + +shapes = [carre, rond, triangle, fleche] + +S = 4 +xs = [[] for i in range(S)] + + +for nb in range(4): + for i in range(8): + for j in range(8): + if shapes[nb][i, j] < 0.95: + xs[nb].append([j, 8 - i]) + +xs = np.array([np.array(xs[0]), np.array(xs[1]), + np.array(xs[2]), np.array(xs[3])]) + + +""" +Barycenter computation +====================== +The four distributions are constructed from 4 simple images +""" +ns = [len(xs[s]) for s in range(S)] +N = 30 + +"""Compute all distances matrices for the four shapes""" +Cs = [sp.spatial.distance.cdist(xs[s], xs[s]) for s in range(S)] +Cs = [cs / cs.max() for cs in Cs] + +ps = [ot.unif(ns[s]) for s in range(S)] +p = ot.unif(N) + + +lambdast = [[float(i) / 3, float(3 - i) / 3] for i in [1, 2]] + +Ct01 = [0 for i in range(2)] +for i in range(2): + Ct01[i] = ot.gromov.gromov_barycenters(N, [Cs[0], Cs[1]], [ + ps[0], ps[1]], p, lambdast[i], 'square_loss', 5e-4, numItermax=100, stopThr=1e-3) + +Ct02 = [0 for i in range(2)] +for i in range(2): + Ct02[i] = ot.gromov.gromov_barycenters(N, [Cs[0], Cs[2]], [ + ps[0], ps[2]], p, lambdast[i], 'square_loss', 5e-4, numItermax=100, stopThr=1e-3) + +Ct13 = [0 for i in range(2)] +for i in range(2): + Ct13[i] = ot.gromov.gromov_barycenters(N, [Cs[1], Cs[3]], [ + ps[1], ps[3]], p, lambdast[i], 'square_loss', 5e-4, numItermax=100, stopThr=1e-3) + +Ct23 = [0 for i in range(2)] +for i in range(2): + Ct23[i] = ot.gromov.gromov_barycenters(N, [Cs[2], Cs[3]], [ + ps[2], ps[3]], p, lambdast[i], 'square_loss', 5e-4, numItermax=100, stopThr=1e-3) + +""" +Visualization +============= +""" + +"""The PCA helps in getting consistency between the rotations""" + +clf = PCA(n_components=2) +npos = [0, 0, 0, 0] +npos = [smacof_mds(Cs[s], 2) for s in range(S)] + +npost01 = [0, 0] +npost01 = [smacof_mds(Ct01[s], 2) for s in range(2)] +npost01 = [clf.fit_transform(npost01[s]) for s in range(2)] + +npost02 = [0, 0] +npost02 = [smacof_mds(Ct02[s], 2) for s in range(2)] +npost02 = [clf.fit_transform(npost02[s]) for s in range(2)] + +npost13 = [0, 0] +npost13 = [smacof_mds(Ct13[s], 2) for s in range(2)] +npost13 = [clf.fit_transform(npost13[s]) for s in range(2)] + +npost23 = [0, 0] +npost23 = [smacof_mds(Ct23[s], 2) for s in range(2)] +npost23 = [clf.fit_transform(npost23[s]) for s in range(2)] + + +fig = pl.figure(figsize=(10, 10)) + +ax1 = pl.subplot2grid((4, 4), (0, 0)) +pl.xlim((-1, 1)) +pl.ylim((-1, 1)) +ax1.scatter(npos[0][:, 0], npos[0][:, 1], color='r') + +ax2 = pl.subplot2grid((4, 4), (0, 1)) +pl.xlim((-1, 1)) +pl.ylim((-1, 1)) +ax2.scatter(npost01[1][:, 0], npost01[1][:, 1], color='b') + +ax3 = pl.subplot2grid((4, 4), (0, 2)) +pl.xlim((-1, 1)) +pl.ylim((-1, 1)) +ax3.scatter(npost01[0][:, 0], npost01[0][:, 1], color='b') + +ax4 = pl.subplot2grid((4, 4), (0, 3)) +pl.xlim((-1, 1)) +pl.ylim((-1, 1)) +ax4.scatter(npos[1][:, 0], npos[1][:, 1], color='r') + +ax5 = pl.subplot2grid((4, 4), (1, 0)) +pl.xlim((-1, 1)) +pl.ylim((-1, 1)) +ax5.scatter(npost02[1][:, 0], npost02[1][:, 1], color='b') + +ax6 = pl.subplot2grid((4, 4), (1, 3)) +pl.xlim((-1, 1)) +pl.ylim((-1, 1)) +ax6.scatter(npost13[1][:, 0], npost13[1][:, 1], color='b') + +ax7 = pl.subplot2grid((4, 4), (2, 0)) +pl.xlim((-1, 1)) +pl.ylim((-1, 1)) +ax7.scatter(npost02[0][:, 0], npost02[0][:, 1], color='b') + +ax8 = pl.subplot2grid((4, 4), (2, 3)) +pl.xlim((-1, 1)) +pl.ylim((-1, 1)) +ax8.scatter(npost13[0][:, 0], npost13[0][:, 1], color='b') + +ax9 = pl.subplot2grid((4, 4), (3, 0)) +pl.xlim((-1, 1)) +pl.ylim((-1, 1)) +ax9.scatter(npos[2][:, 0], npos[2][:, 1], color='r') + +ax10 = pl.subplot2grid((4, 4), (3, 1)) +pl.xlim((-1, 1)) +pl.ylim((-1, 1)) +ax10.scatter(npost23[1][:, 0], npost23[1][:, 1], color='b') + +ax11 = pl.subplot2grid((4, 4), (3, 2)) +pl.xlim((-1, 1)) +pl.ylim((-1, 1)) +ax11.scatter(npost23[0][:, 0], npost23[0][:, 1], color='b') + +ax12 = pl.subplot2grid((4, 4), (3, 3)) +pl.xlim((-1, 1)) +pl.ylim((-1, 1)) +ax12.scatter(npos[3][:, 0], npos[3][:, 1], color='r') diff --git a/ot/gromov.py b/ot/gromov.py index 7cf3b42..421ed3f 100644 --- a/ot/gromov.py +++ b/ot/gromov.py @@ -23,7 +23,7 @@ def square_loss(a, b): Returns the value of L(a,b)=(1/2)*|a-b|^2 """ - return (1 / 2) * (a - b)**2 + return 0.5 * (a - b)**2 def kl_loss(a, b): @@ -54,9 +54,9 @@ def tensor_square_loss(C1, C2, T): Parameters ---------- - C1 : np.ndarray(ns,ns) + C1 : ndarray, shape (ns, ns) Metric cost matrix in the source space - C2 : np.ndarray(nt,nt) + C2 : ndarray, shape (nt, nt) Metric costfr matrix in the target space T : np.ndarray(ns,nt) Coupling between source and target spaces @@ -87,7 +87,7 @@ def tensor_square_loss(C1, C2, T): return b tens = -np.dot(h1(C1), T).dot(h2(C2).T) - tens = tens - tens.min() + tens -= tens.min() return np.array(tens) @@ -112,9 +112,9 @@ def tensor_kl_loss(C1, C2, T): Parameters ---------- - C1 : np.ndarray(ns,ns) + C1 : ndarray, shape (ns, ns) Metric cost matrix in the source space - C2 : np.ndarray(nt,nt) + C2 : ndarray, shape (nt, nt) Metric costfr matrix in the target space T : np.ndarray(ns,nt) Coupling between source and target spaces @@ -149,7 +149,7 @@ def tensor_kl_loss(C1, C2, T): return np.log(b + 1e-15) tens = -np.dot(h1(C1), T).dot(h2(C2).T) - tens = tens - tens.min() + tens -= tens.min() return np.array(tens) @@ -175,9 +175,8 @@ def update_square_loss(p, lambdas, T, Cs): """ - tmpsum = np.sum([lambdas[s] * np.dot(T[s].T, Cs[s]).dot(T[s]) - for s in range(len(T))]) - ppt = np.dot(p, p.T) + tmpsum = sum([lambdas[s] * np.dot(T[s].T, Cs[s]).dot(T[s]) for s in range(len(T))]) + ppt = np.outer(p, p) return(np.divide(tmpsum, ppt)) @@ -203,9 +202,8 @@ def update_kl_loss(p, lambdas, T, Cs): """ - tmpsum = np.sum([lambdas[s] * np.dot(T[s].T, Cs[s]).dot(T[s]) - for s in range(len(T))]) - ppt = np.dot(p, p.T) + tmpsum = sum([lambdas[s] * np.dot(T[s].T, Cs[s]).dot(T[s]) for s in range(len(T))]) + ppt = np.outer(p, p) return(np.exp(np.divide(tmpsum, ppt))) @@ -239,9 +237,9 @@ def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopThr Parameters ---------- - C1 : np.ndarray(ns,ns) + C1 : ndarray, shape (ns, ns) Metric cost matrix in the source space - C2 : np.ndarray(nt,nt) + C2 : ndarray, shape (nt, nt) Metric costfr matrix in the target space p : np.ndarray(ns,) distribution in the source space @@ -271,7 +269,7 @@ def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopThr C1 = np.asarray(C1, dtype=np.float64) C2 = np.asarray(C2, dtype=np.float64) - T = np.dot(p, q.T) # Initialization + T = np.outer(p, q) # Initialization cpt = 0 err = 1 @@ -333,9 +331,9 @@ def gromov_wasserstein2(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopTh Parameters ---------- - C1 : np.ndarray(ns,ns) + C1 : ndarray, shape (ns, ns) Metric cost matrix in the source space - C2 : np.ndarray(nt,nt) + C2 : ndarray, shape (nt, nt) Metric costfr matrix in the target space p : np.ndarray(ns,) distribution in the source space @@ -434,8 +432,6 @@ def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, numItermax=1000 Cs = [np.asarray(Cs[s], dtype=np.float64) for s in range(S)] lambdas = np.asarray(lambdas, dtype=np.float64) - T = [0 for s in range(S)] - # Initialization of C : random SPD matrix xalea = np.random.randn(N, 2) C = dist(xalea, xalea) diff --git a/test/test_gromov.py b/test/test_gromov.py new file mode 100644 index 0000000..75eeaab --- /dev/null +++ b/test/test_gromov.py @@ -0,0 +1,38 @@ +"""Tests for module gromov """ + +# Author: Erwan Vautier +# Nicolas Courty +# +# License: MIT License + +import numpy as np +import ot + + +def test_gromov(): + n = 50 # nb samples + + mu_s = np.array([0, 0]) + cov_s = np.array([[1, 0], [0, 1]]) + + xs = ot.datasets.get_2D_samples_gauss(n, mu_s, cov_s) + + xt = [xs[n - (i + 1)] for i in range(n)] + xt = np.array(xt) + + p = ot.unif(n) + q = ot.unif(n) + + C1 = ot.dist(xs, xs) + C2 = ot.dist(xt, xt) + + C1 /= C1.max() + C2 /= C2.max() + + G = ot.gromov_wasserstein(C1, C2, p, q, 'square_loss', epsilon=5e-4) + + # check constratints + np.testing.assert_allclose( + p, G.sum(1), atol=1e-04) # cf convergence gromov + np.testing.assert_allclose( + q, G.sum(0), atol=1e-04) # cf convergence gromov -- cgit v1.2.3 From bc68cc3e8b23ad7d542518ba8ffa665094d57663 Mon Sep 17 00:00:00 2001 From: Nicolas Courty Date: Thu, 31 Aug 2017 17:17:30 +0200 Subject: minor corrections --- examples/plot_gromov_barycenter.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/plot_gromov_barycenter.py b/examples/plot_gromov_barycenter.py index 6a72b3b..da52768 100755 --- a/examples/plot_gromov_barycenter.py +++ b/examples/plot_gromov_barycenter.py @@ -32,18 +32,19 @@ that will be given by the output of the algorithm """ -def smacof_mds(C, dim, maxIter=3000, eps=1e-9): +def smacof_mds(C, dim, max_iter=3000, eps=1e-9): """ Returns an interpolated point cloud following the dissimilarity matrix C using SMACOF multidimensional scaling (MDS) in specific dimensionned target space Parameters ---------- - C : np.ndarray(ns,ns) + C : ndarray, shape (ns, ns) dissimilarity matrix - dim : Integer + dim : int dimension of the targeted space - maxIter : Maximum number of iterations of the SMACOF algorithm for a single run + max_iter : int + Maximum number of iterations of the SMACOF algorithm for a single run eps : relative tolerance w.r.t stress to declare converge @@ -60,7 +61,7 @@ def smacof_mds(C, dim, maxIter=3000, eps=1e-9): mds = manifold.MDS( dim, - max_iter=3000, + max_iter=max_iter, eps=1e-9, dissimilarity='precomputed', n_init=1) @@ -68,7 +69,7 @@ def smacof_mds(C, dim, maxIter=3000, eps=1e-9): nmds = manifold.MDS( 2, - max_iter=3000, + max_iter=max_iter, eps=1e-9, dissimilarity="precomputed", random_state=seed, -- cgit v1.2.3 From e89f09dae0f66df4a8c1c7ac761a71611c2d676c Mon Sep 17 00:00:00 2001 From: Slasnista Date: Fri, 28 Jul 2017 08:00:35 +0200 Subject: remove linewidth error message --- ot/da.py | 150 +++++++++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 108 insertions(+), 42 deletions(-) diff --git a/ot/da.py b/ot/da.py index 4f9bce5..1dd4011 100644 --- a/ot/da.py +++ b/ot/da.py @@ -17,14 +17,18 @@ 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): +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 + 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) + \gamma = arg\min_\gamma <\gamma,M>_F + reg\cdot\Omega_e(\gamma) + + \eta \Omega_g(\gamma) s.t. \gamma 1 = a @@ -34,11 +38,16 @@ def sinkhorn_lpl1_mm(a, labels_a, b, M, reg, eta=0.1, numItermax=10, numInnerIte 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}\|^{1/2}_1` where :math:`\mathcal{I}_c` are the index of samples from class c in the source domain. + - :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}\|^{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 generalised conditional gradient as proposed in [5]_ [7]_ + The algorithm used for solving the problem is the generalised conditional + gradient as proposed in [5]_ [7]_ Parameters @@ -78,8 +87,13 @@ def sinkhorn_lpl1_mm(a, labels_a, b, M, reg, eta=0.1, numItermax=10, numInnerIte 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. + .. [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 -------- @@ -114,14 +128,18 @@ def sinkhorn_lpl1_mm(a, labels_a, b, M, reg, eta=0.1, numItermax=10, numInnerIte 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): +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 + 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) + \gamma = arg\min_\gamma <\gamma,M>_F + reg\cdot\Omega_e(\gamma)+ + \eta \Omega_g(\gamma) s.t. \gamma 1 = a @@ -131,11 +149,16 @@ def sinkhorn_l1l2_gl(a, labels_a, b, M, reg, eta=0.1, numItermax=10, numInnerIte 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. + - :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]_ + The algorithm used for solving the problem is the generalised conditional + gradient as proposed in [5]_ [7]_ Parameters @@ -175,8 +198,12 @@ def sinkhorn_l1l2_gl(a, labels_a, b, M, reg, eta=0.1, numItermax=10, numInnerIte 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. + .. [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 -------- @@ -203,16 +230,22 @@ def sinkhorn_l1l2_gl(a, labels_a, b, M, reg, eta=0.1, numItermax=10, numInnerIte 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) + 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): +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 + \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 @@ -221,8 +254,10 @@ def joint_OT_mapping_linear(xs, xt, mu=1, eta=0.001, bias=False, verbose=False, \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 + - 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 @@ -277,7 +312,9 @@ def joint_OT_mapping_linear(xs, xt, mu=1, eta=0.001, bias=False, verbose=False, References ---------- - .. [8] M. Perrot, N. Courty, R. Flamary, A. Habrard, "Mapping estimation for discrete optimal transport", Neural Information Processing Systems (NIPS), 2016. + .. [8] M. Perrot, N. Courty, R. Flamary, A. Habrard, + "Mapping estimation for discrete optimal transport", + Neural Information Processing Systems (NIPS), 2016. See Also -------- @@ -384,13 +421,18 @@ def joint_OT_mapping_linear(xs, xt, mu=1, eta=0.001, bias=False, verbose=False, 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): +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} + \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 @@ -399,8 +441,10 @@ def joint_OT_mapping_kernel(xs, xt, mu=1, eta=0.001, kerneltype='gaussian', sigm \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 + - 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 @@ -458,7 +502,9 @@ def joint_OT_mapping_kernel(xs, xt, mu=1, eta=0.001, kerneltype='gaussian', sigm References ---------- - .. [8] M. Perrot, N. Courty, R. Flamary, A. Habrard, "Mapping estimation for discrete optimal transport", Neural Information Processing Systems (NIPS), 2016. + .. [8] M. Perrot, N. Courty, R. Flamary, A. Habrard, + "Mapping estimation for discrete optimal transport", + Neural Information Processing Systems (NIPS), 2016. See Also -------- @@ -593,7 +639,9 @@ class OTDA(object): 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 + .. [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 """ @@ -606,7 +654,8 @@ class OTDA(object): self.computed = False def fit(self, xs, xt, ws=None, wt=None, norm=None): - """ Fit domain adaptation between samples is xs and xt (with optional weights)""" + """Fit domain adaptation between samples is xs and xt + (with optional weights)""" self.xs = xs self.xt = xt @@ -669,7 +718,9 @@ class OTDA(object): References ---------- - .. [6] Ferradans, S., Papadakis, N., Peyré, G., & Aujol, J. F. (2014). Regularized discrete optimal transport. SIAM Journal on Imaging Sciences, 7(3), 1853-1882. + .. [6] Ferradans, S., Papadakis, N., Peyré, G., & Aujol, J. F. (2014). + Regularized discrete optimal transport. SIAM Journal on Imaging + Sciences, 7(3), 1853-1882. """ if direction > 0: # >0 then source to target @@ -708,10 +759,12 @@ class OTDA(object): class OTDA_sinkhorn(OTDA): - """Class for domain adaptation with optimal transport with entropic regularization""" + """Class for domain adaptation with optimal transport with entropic + regularization""" def fit(self, xs, xt, reg=1, ws=None, wt=None, norm=None, **kwargs): - """ Fit regularized domain adaptation between samples is xs and xt (with optional weights)""" + """Fit regularized domain adaptation between samples is xs and xt + (with optional weights)""" self.xs = xs self.xt = xt @@ -731,10 +784,14 @@ class OTDA_sinkhorn(OTDA): class OTDA_lpl1(OTDA): - """Class for domain adaptation with optimal transport with entropic and group regularization""" + """Class for domain adaptation with optimal transport with entropic and + group regularization""" - def fit(self, xs, ys, xt, reg=1, eta=1, ws=None, wt=None, norm=None, **kwargs): - """ Fit regularized domain adaptation between samples is xs and xt (with optional weights), See ot.da.sinkhorn_lpl1_mm for fit parameters""" + def fit(self, xs, ys, xt, reg=1, eta=1, ws=None, wt=None, norm=None, + **kwargs): + """Fit regularized domain adaptation between samples is xs and xt + (with optional weights), See ot.da.sinkhorn_lpl1_mm for fit + parameters""" self.xs = xs self.xt = xt @@ -754,10 +811,14 @@ class OTDA_lpl1(OTDA): class OTDA_l1l2(OTDA): - """Class for domain adaptation with optimal transport with entropic and group lasso regularization""" + """Class for domain adaptation with optimal transport with entropic + and group lasso regularization""" - def fit(self, xs, ys, xt, reg=1, eta=1, ws=None, wt=None, norm=None, **kwargs): - """ Fit regularized domain adaptation between samples is xs and xt (with optional weights), See ot.da.sinkhorn_lpl1_gl for fit parameters""" + def fit(self, xs, ys, xt, reg=1, eta=1, ws=None, wt=None, norm=None, + **kwargs): + """Fit regularized domain adaptation between samples is xs and xt + (with optional weights), See ot.da.sinkhorn_lpl1_gl for fit + parameters""" self.xs = xs self.xt = xt @@ -777,7 +838,9 @@ class OTDA_l1l2(OTDA): class OTDA_mapping_linear(OTDA): - """Class for optimal transport with joint linear mapping estimation as in [8]""" + """Class for optimal transport with joint linear mapping estimation as in + [8] + """ def __init__(self): """ Class initialization""" @@ -820,9 +883,11 @@ class OTDA_mapping_linear(OTDA): class OTDA_mapping_kernel(OTDA_mapping_linear): - """Class for optimal transport with joint nonlinear mapping estimation as in [8]""" + """Class for optimal transport with joint nonlinear mapping + estimation as in [8]""" - def fit(self, xs, xt, mu=1, eta=1, bias=False, kerneltype='gaussian', sigma=1, **kwargs): + def fit(self, xs, xt, mu=1, eta=1, bias=False, kerneltype='gaussian', + sigma=1, **kwargs): """ Fit domain adaptation between samples is xs and xt """ self.xs = xs self.xt = xt @@ -843,7 +908,8 @@ class OTDA_mapping_kernel(OTDA_mapping_linear): if self.computed: K = kernel( - x, self.xs, method=self.kernel, sigma=self.sigma, **self.kwargs) + x, self.xs, method=self.kernel, sigma=self.sigma, + **self.kwargs) if self.bias: K = np.hstack((K, np.ones((x.shape[0], 1)))) return K.dot(self.L) -- cgit v1.2.3 From f469205cf19915a34366c9410ccf6c8e13038673 Mon Sep 17 00:00:00 2001 From: Slasnista Date: Fri, 28 Jul 2017 08:49:10 +0200 Subject: first proposal for OT wrappers --- ot/da.py | 179 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) diff --git a/ot/da.py b/ot/da.py index 1dd4011..f534bf5 100644 --- a/ot/da.py +++ b/ot/da.py @@ -916,3 +916,182 @@ class OTDA_mapping_kernel(OTDA_mapping_linear): else: print("Warning, model not fitted yet, returning None") return None + +############################################################################## +# proposal +############################################################################## + +from sklearn.base import BaseEstimator +from sklearn.metrics import pairwise_distances + +""" +- all methods have the same input parameters: Xs, Xt, ys, yt (what order ?) +- ref: is the entropic reg parameter +- eta: is the second reg parameter +- gamma_: is the optimal coupling +- mapping barycentric for the moment + +Questions: +- Cost matrix estimation: from sklearn or from internal function ? +- distribution estimation ? Look at Nathalie's approach +- should everything been done into the fit from BaseTransport ? +""" + + +class BaseTransport(BaseEstimator): + + def fit(self, Xs=None, ys=None, Xt=None, yt=None, method="sinkhorn"): + """fit: estimates the optimal coupling + + Parameters: + ----------- + - Xs: source samples, (ns samples, d features) numpy-like array + - ys: source labels + - Xt: target samples (nt samples, d features) numpy-like array + - yt: target labels + - method: algorithm to use to compute optimal coupling + (default: sinkhorn) + + Returns: + -------- + - self + """ + + # pairwise distance + Cost = pairwise_distances(Xs, Xt, metric=self.metric) + + if self.mode == "semisupervised": + print("TODO: modify cost matrix accordingly") + pass + + # distribution estimation: should we change it ? + mu_s = np.ones(Xs.shape[0]) / float(Xs.shape[0]) + mu_t = np.ones(Xt.shape[0]) / float(Xt.shape[0]) + + if method == "sinkhorn": + self.gamma_ = sinkhorn( + a=mu_s, b=mu_t, M=Cost, reg=self.reg, + numItermax=self.max_iter, stopThr=self.tol, + verbose=self.verbose, log=self.log) + else: + print("TODO: implement the other methods") + + return self + + def fit_transform(self, Xs=None, ys=None, Xt=None, yt=None): + """fit_transform + + Parameters: + ----------- + - Xs: source samples, (ns samples, d features) numpy-like array + - ys: source labels + - Xt: target samples (nt samples, d features) numpy-like array + - yt: target labels + + Returns: + -------- + - transp_Xt + """ + + return self.fit(Xs, ys, Xt, yt, self.method).transform(Xs, ys, Xt, yt) + + def transform(self, Xs=None, ys=None, Xt=None, yt=None): + """transform: as a convention transports source samples + onto target samples + + Parameters: + ----------- + - Xs: source samples, (ns samples, d features) numpy-like array + - ys: source labels + - Xt: target samples (nt samples, d features) numpy-like array + - yt: target labels + + Returns: + -------- + - transp_Xt + """ + + if self.mapping == "barycentric": + transp = self.gamma_ / np.sum(self.gamma_, 1)[:, None] + + # set nans to 0 + transp[~ np.isfinite(transp)] = 0 + + # compute transported samples + transp_Xs = np.dot(transp, Xt) + + return transp_Xs + + def inverse_transform(self, Xs=None, ys=None, Xt=None, yt=None): + """inverse_transform: as a convention transports target samples + onto source samples + + Parameters: + ----------- + - Xs: source samples, (ns samples, d features) numpy-like array + - ys: source labels + - Xt: target samples (nt samples, d features) numpy-like array + - yt: target labels + + Returns: + -------- + - transp_Xt + """ + + if self.mapping == "barycentric": + transp_ = self.gamma_.T / np.sum(self.gamma_, 0)[:, None] + + # set nans to 0 + transp_[~ np.isfinite(transp_)] = 0 + + # compute transported samples + transp_Xt = np.dot(transp_, Xs) + else: + print("mapping not yet implemented") + + return transp_Xt + + +class SinkhornTransport(BaseTransport): + """SinkhornTransport: class wrapper for optimal transport based on + Sinkhorn's algorithm + + Parameters + ---------- + - reg : parameter for entropic regularization + - mode: unsupervised (default) or semi supervised: controls whether + labels are taken into accout to construct the optimal coupling + - max_iter : maximum number of iterations + - tol : precision + - verbose : control verbosity + - log : control log + + Attributes + ---------- + - gamma_: optimal coupling estimated by the fit function + """ + + def __init__(self, reg=1., mode="unsupervised", max_iter=1000, + tol=10e-9, verbose=False, log=False, mapping="barycentric", + metric="sqeuclidean"): + self.reg = reg + self.mode = mode + self.max_iter = max_iter + self.tol = tol + self.verbose = verbose + self.log = log + self.mapping = mapping + self.metric = metric + self.method = "sinkhorn" + + def fit(self, Xs=None, ys=None, Xt=None, yt=None): + """_fit + """ + return super(SinkhornTransport, self).fit( + Xs, ys, Xt, yt, method=self.method) + + +if __name__ == "__main__": + print("Small test") + + st = SinkhornTransport() -- cgit v1.2.3 From fa36e775ff2c1fe17cf1323d430a196774a6d2a5 Mon Sep 17 00:00:00 2001 From: Slasnista Date: Fri, 28 Jul 2017 14:52:36 +0200 Subject: small modifs according to NG proposals --- ot/da.py | 43 +++++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/ot/da.py b/ot/da.py index f534bf5..a422f7c 100644 --- a/ot/da.py +++ b/ot/da.py @@ -926,8 +926,8 @@ from sklearn.metrics import pairwise_distances """ - all methods have the same input parameters: Xs, Xt, ys, yt (what order ?) -- ref: is the entropic reg parameter -- eta: is the second reg parameter +- reg_e: is the entropic reg parameter +- reg_cl: is the second reg parameter - gamma_: is the optimal coupling - mapping barycentric for the moment @@ -940,7 +940,7 @@ Questions: class BaseTransport(BaseEstimator): - def fit(self, Xs=None, ys=None, Xt=None, yt=None, method="sinkhorn"): + def fit(self, Xs=None, ys=None, Xt=None, yt=None, method=None): """fit: estimates the optimal coupling Parameters: @@ -964,13 +964,17 @@ class BaseTransport(BaseEstimator): print("TODO: modify cost matrix accordingly") pass - # distribution estimation: should we change it ? - mu_s = np.ones(Xs.shape[0]) / float(Xs.shape[0]) - mu_t = np.ones(Xt.shape[0]) / float(Xt.shape[0]) + # distribution estimation + if self.distribution == "uniform": + mu_s = np.ones(Xs.shape[0]) / float(Xs.shape[0]) + mu_t = np.ones(Xt.shape[0]) / float(Xt.shape[0]) + else: + print("TODO: implement kernelized approach") + # coupling estimation if method == "sinkhorn": self.gamma_ = sinkhorn( - a=mu_s, b=mu_t, M=Cost, reg=self.reg, + a=mu_s, b=mu_t, M=Cost, reg=self.reg_e, numItermax=self.max_iter, stopThr=self.tol, verbose=self.verbose, log=self.log) else: @@ -1058,7 +1062,7 @@ class SinkhornTransport(BaseTransport): Parameters ---------- - - reg : parameter for entropic regularization + - reg_e : parameter for entropic regularization - mode: unsupervised (default) or semi supervised: controls whether labels are taken into accout to construct the optimal coupling - max_iter : maximum number of iterations @@ -1071,10 +1075,10 @@ class SinkhornTransport(BaseTransport): - gamma_: optimal coupling estimated by the fit function """ - def __init__(self, reg=1., mode="unsupervised", max_iter=1000, + def __init__(self, reg_e=1., mode="unsupervised", max_iter=1000, tol=10e-9, verbose=False, log=False, mapping="barycentric", - metric="sqeuclidean"): - self.reg = reg + metric="sqeuclidean", distribution="uniform"): + self.reg_e = reg_e self.mode = mode self.max_iter = max_iter self.tol = tol @@ -1082,11 +1086,26 @@ class SinkhornTransport(BaseTransport): self.log = log self.mapping = mapping self.metric = metric + self.distribution = distribution self.method = "sinkhorn" def fit(self, Xs=None, ys=None, Xt=None, yt=None): - """_fit + """fit + + Parameters: + ----------- + - Xs: source samples, (ns samples, d features) numpy-like array + - ys: source labels + - Xt: target samples (nt samples, d features) numpy-like array + - yt: target labels + - method: algorithm to use to compute optimal coupling + (default: sinkhorn) + + Returns: + -------- + - self """ + return super(SinkhornTransport, self).fit( Xs, ys, Xt, yt, method=self.method) -- cgit v1.2.3 From aa19b6adef8e41ec57f94353d80ebd80d49edc29 Mon Sep 17 00:00:00 2001 From: Slasnista Date: Fri, 28 Jul 2017 15:34:36 +0200 Subject: integrate AG comments --- ot/da.py | 202 +++++++++++++++++++++++++++++++++++++-------------------------- 1 file changed, 120 insertions(+), 82 deletions(-) diff --git a/ot/da.py b/ot/da.py index a422f7c..828efc2 100644 --- a/ot/da.py +++ b/ot/da.py @@ -940,21 +940,23 @@ Questions: class BaseTransport(BaseEstimator): - def fit(self, Xs=None, ys=None, Xt=None, yt=None, method=None): - """fit: estimates the optimal coupling - - Parameters: - ----------- - - Xs: source samples, (ns samples, d features) numpy-like array - - ys: source labels - - Xt: target samples (nt samples, d features) numpy-like array - - yt: target labels - - method: algorithm to use to compute optimal coupling - (default: sinkhorn) - - Returns: - -------- - - self + 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 of shape = [n_source_samples, n_features] + The training input samples. + ys : array-like, shape = [n_source_samples] + The class labels + Xt : array-like of shape = [n_target_samples, n_features] + The training input samples. + yt : array-like, shape = [n_labeled_target_samples] + The class labels + Returns + ------- + self : object + Returns self. """ # pairwise distance @@ -972,7 +974,7 @@ class BaseTransport(BaseEstimator): print("TODO: implement kernelized approach") # coupling estimation - if method == "sinkhorn": + if self.method == "sinkhorn": self.gamma_ = sinkhorn( a=mu_s, b=mu_t, M=Cost, reg=self.reg_e, numItermax=self.max_iter, stopThr=self.tol, @@ -983,36 +985,43 @@ class BaseTransport(BaseEstimator): return self def fit_transform(self, Xs=None, ys=None, Xt=None, yt=None): - """fit_transform - - Parameters: - ----------- - - Xs: source samples, (ns samples, d features) numpy-like array - - ys: source labels - - Xt: target samples (nt samples, d features) numpy-like array - - yt: target labels - - Returns: - -------- - - transp_Xt + """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 of shape = [n_source_samples, n_features] + The training input samples. + ys : array-like, shape = [n_source_samples] + The class labels + Xt : array-like of shape = [n_target_samples, n_features] + The training input samples. + yt : array-like, shape = [n_labeled_target_samples] + The class labels + Returns + ------- + transp_Xs : array-like of shape = [n_source_samples, n_features] + The source samples samples. """ - return self.fit(Xs, ys, Xt, yt, self.method).transform(Xs, ys, Xt, yt) + return self.fit(Xs, ys, Xt, yt).transform(Xs, ys, Xt, yt) def transform(self, Xs=None, ys=None, Xt=None, yt=None): - """transform: as a convention transports source samples - onto target samples - - Parameters: - ----------- - - Xs: source samples, (ns samples, d features) numpy-like array - - ys: source labels - - Xt: target samples (nt samples, d features) numpy-like array - - yt: target labels - - Returns: - -------- - - transp_Xt + """Transports source samples Xs onto target ones Xt + Parameters + ---------- + Xs : array-like of shape = [n_source_samples, n_features] + The training input samples. + ys : array-like, shape = [n_source_samples] + The class labels + Xt : array-like of shape = [n_target_samples, n_features] + The training input samples. + yt : array-like, shape = [n_labeled_target_samples] + The class labels + Returns + ------- + transp_Xs : array-like of shape = [n_source_samples, n_features] + The transport source samples. """ if self.mapping == "barycentric": @@ -1027,19 +1036,21 @@ class BaseTransport(BaseEstimator): return transp_Xs def inverse_transform(self, Xs=None, ys=None, Xt=None, yt=None): - """inverse_transform: as a convention transports target samples - onto source samples - - Parameters: - ----------- - - Xs: source samples, (ns samples, d features) numpy-like array - - ys: source labels - - Xt: target samples (nt samples, d features) numpy-like array - - yt: target labels - - Returns: - -------- - - transp_Xt + """Transports target samples Xt onto target samples Xs + Parameters + ---------- + Xs : array-like of shape = [n_source_samples, n_features] + The training input samples. + ys : array-like, shape = [n_source_samples] + The class labels + Xt : array-like of shape = [n_target_samples, n_features] + The training input samples. + yt : array-like, shape = [n_labeled_target_samples] + The class labels + Returns + ------- + transp_Xt : array-like of shape = [n_source_samples, n_features] + The transported target samples. """ if self.mapping == "barycentric": @@ -1057,22 +1068,48 @@ class BaseTransport(BaseEstimator): class SinkhornTransport(BaseTransport): - """SinkhornTransport: class wrapper for optimal transport based on - Sinkhorn's algorithm + """Domain Adapatation OT method based on Sinkhorn Algorithm Parameters ---------- - - reg_e : parameter for entropic regularization - - mode: unsupervised (default) or semi supervised: controls whether - labels are taken into accout to construct the optimal coupling - - max_iter : maximum number of iterations - - tol : precision - - verbose : control verbosity - - log : control log - + reg_e : float, optional (default=1) + Entropic regularization parameter + mode : string, optional (default="unsupervised") + The DA mode. If "unsupervised" no target labels are taken into account + to modify the cost matrix. If "semisupervised" the target labels + are taken into account to set coefficients of the pairwise distance + matrix to 0 for row and columns indices that correspond to source and + target samples which share the same labels. + 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. + mapping : string, optional (default="barycentric") + The kind of mapping to apply to transport samples from a domain into + another one. + if "barycentric" only the samples used to estimate the coupling can + be transported from a domain to another one. + metric : string, optional (default="sqeuclidean") + The ground metric for the Wasserstein problem + distribution : string, optional (default="uniform") + The kind of distribution estimation to employ + verbose : int, optional (default=0) + Controls the verbosity of the optimization algorithm + log : int, optional (default=0) + Controls the logs of the optimization algorithm Attributes ---------- - - gamma_: optimal coupling estimated by the fit function + gamma_ : 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] M. Cuturi, Sinkhorn Distances : Lightspeed Computation of Optimal + Transport, Advances in Neural Information Processing Systems (NIPS) + 26, 2013 """ def __init__(self, reg_e=1., mode="unsupervised", max_iter=1000, @@ -1090,24 +1127,25 @@ class SinkhornTransport(BaseTransport): self.method = "sinkhorn" def fit(self, Xs=None, ys=None, Xt=None, yt=None): - """fit - - Parameters: - ----------- - - Xs: source samples, (ns samples, d features) numpy-like array - - ys: source labels - - Xt: target samples (nt samples, d features) numpy-like array - - yt: target labels - - method: algorithm to use to compute optimal coupling - (default: sinkhorn) - - Returns: - -------- - - self + """Build a coupling matrix from source and target sets of samples + (Xs, ys) and (Xt, yt) + Parameters + ---------- + Xs : array-like of shape = [n_source_samples, n_features] + The training input samples. + ys : array-like, shape = [n_source_samples] + The class labels + Xt : array-like of shape = [n_target_samples, n_features] + The training input samples. + yt : array-like, shape = [n_labeled_target_samples] + The class labels + Returns + ------- + self : object + Returns self. """ - return super(SinkhornTransport, self).fit( - Xs, ys, Xt, yt, method=self.method) + return super(SinkhornTransport, self).fit(Xs, ys, Xt, yt) if __name__ == "__main__": -- cgit v1.2.3 From 5ab50354e60ed94d9d799927fd4b680fb8447304 Mon Sep 17 00:00:00 2001 From: Slasnista Date: Mon, 31 Jul 2017 10:54:28 +0200 Subject: own BaseEstimator class written + rflamary comments addressed --- ot/da.py | 199 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 172 insertions(+), 27 deletions(-) diff --git a/ot/da.py b/ot/da.py index 828efc2..d30c821 100644 --- a/ot/da.py +++ b/ot/da.py @@ -921,21 +921,153 @@ class OTDA_mapping_kernel(OTDA_mapping_linear): # proposal ############################################################################## -from sklearn.base import BaseEstimator -from sklearn.metrics import pairwise_distances +# from sklearn.base import BaseEstimator +# from sklearn.metrics import pairwise_distances + +############################################################################## +# adapted from scikit-learn + +import warnings +# from .externals.six import string_types, iteritems -""" -- all methods have the same input parameters: Xs, Xt, ys, yt (what order ?) -- reg_e: is the entropic reg parameter -- reg_cl: is the second reg parameter -- gamma_: is the optimal coupling -- mapping barycentric for the moment - -Questions: -- Cost matrix estimation: from sklearn or from internal function ? -- distribution estimation ? Look at Nathalie's approach -- should everything been done into the fit from BaseTransport ? -""" + +class BaseEstimator(object): + """Base class for all estimators in scikit-learn + 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``). + """ + + @classmethod + def _get_param_names(cls): + """Get parameter names for the estimator""" + try: + from inspect import signature + except ImportError: + from .externals.funcsigs import signature + # fetch the constructor or the original constructor before + # deprecation wrapping if any + init = getattr(cls.__init__, 'deprecated_original', cls.__init__) + if init is object.__init__: + # No explicit constructor to introspect + return [] + + # introspect the constructor arguments to find the model parameters + # to represent + init_signature = signature(init) + # Consider the constructor parameters excluding 'self' + parameters = [p for p in init_signature.parameters.values() + if p.name != 'self' and p.kind != p.VAR_KEYWORD] + for p in parameters: + if p.kind == p.VAR_POSITIONAL: + raise RuntimeError("scikit-learn estimators should always " + "specify their parameters in the signature" + " of their __init__ (no varargs)." + " %s with constructor %s doesn't " + " follow this convention." + % (cls, init_signature)) + # Extract and sort argument names excluding 'self' + return sorted([p.name for p in parameters]) + + def get_params(self, deep=True): + """Get parameters for this estimator. + Parameters + ---------- + deep : boolean, optional + If True, will return the parameters for this estimator and + contained subobjects that are estimators. + Returns + ------- + params : mapping of string to any + Parameter names mapped to their values. + """ + out = dict() + for key in self._get_param_names(): + # We need deprecation warnings to always be on in order to + # catch deprecated param values. + # This is set in utils/__init__.py but it gets overwritten + # when running under python3 somehow. + warnings.simplefilter("always", DeprecationWarning) + try: + with warnings.catch_warnings(record=True) as w: + value = getattr(self, key, None) + if len(w) and w[0].category == DeprecationWarning: + # if the parameter is deprecated, don't show it + continue + finally: + warnings.filters.pop(0) + + # XXX: should we rather test if instance of estimator? + if deep and hasattr(value, 'get_params'): + deep_items = value.get_params().items() + out.update((key + '__' + k, val) for k, val in deep_items) + out[key] = value + return out + + def set_params(self, **params): + """Set the parameters of this estimator. + The method works on simple estimators as well as on nested objects + (such as pipelines). The latter have parameters of the form + ``__`` so that it's possible to update each + component of a nested object. + Returns + ------- + self + """ + if not params: + # Simple optimisation to gain speed (inspect is slow) + return self + valid_params = self.get_params(deep=True) + # for key, value in iteritems(params): + for key, value in params.items(): + split = key.split('__', 1) + if len(split) > 1: + # nested objects case + name, sub_name = split + if name not in valid_params: + raise ValueError('Invalid parameter %s for estimator %s. ' + 'Check the list of available parameters ' + 'with `estimator.get_params().keys()`.' % + (name, self)) + sub_object = valid_params[name] + sub_object.set_params(**{sub_name: value}) + else: + # simple objects case + if key not in valid_params: + raise ValueError('Invalid parameter %s for estimator %s. ' + 'Check the list of available parameters ' + 'with `estimator.get_params().keys()`.' % + (key, self.__class__.__name__)) + setattr(self, key, value) + return self + + def __repr__(self): + from sklearn.base import _pprint + class_name = self.__class__.__name__ + return '%s(%s)' % (class_name, _pprint(self.get_params(deep=False), + offset=len(class_name),),) + + # __getstate__ and __setstate__ are omitted because they only contain + # conditionals that are not satisfied by our objects (e.g., + # ``if type(self).__module__.startswith('sklearn.')``. + + +def distribution_estimation_uniform(X): + """estimates a uniform distribution from an array of samples X + + Parameters + ---------- + X : array-like of shape = [n_samples, n_features] + The array of samples + Returns + ------- + mu : array-like, shape = [n_samples,] + The uniform distribution estimated from X + """ + + return np.ones(X.shape[0]) / float(X.shape[0]) class BaseTransport(BaseEstimator): @@ -960,18 +1092,19 @@ class BaseTransport(BaseEstimator): """ # pairwise distance - Cost = pairwise_distances(Xs, Xt, metric=self.metric) + Cost = dist(Xs, Xt, metric=self.metric) if self.mode == "semisupervised": print("TODO: modify cost matrix accordingly") pass # distribution estimation - if self.distribution == "uniform": - mu_s = np.ones(Xs.shape[0]) / float(Xs.shape[0]) - mu_t = np.ones(Xt.shape[0]) / float(Xt.shape[0]) - else: - print("TODO: implement kernelized approach") + mu_s = self.distribution_estimation(Xs) + mu_t = self.distribution_estimation(Xt) + + # store arrays of samples + self.Xs = Xs + self.Xt = Xt # coupling estimation if self.method == "sinkhorn": @@ -1024,14 +1157,19 @@ class BaseTransport(BaseEstimator): The transport source samples. """ - if self.mapping == "barycentric": + # TODO: check whether Xs is new or not + if self.Xs == Xs: + # perform standard barycentric mapping transp = self.gamma_ / np.sum(self.gamma_, 1)[:, None] # set nans to 0 transp[~ np.isfinite(transp)] = 0 # compute transported samples - transp_Xs = np.dot(transp, Xt) + transp_Xs = np.dot(transp, self.Xt) + else: + # perform out of sample mapping + print("out of sample mapping not yet implemented") return transp_Xs @@ -1053,16 +1191,19 @@ class BaseTransport(BaseEstimator): The transported target samples. """ - if self.mapping == "barycentric": + # TODO: check whether Xt is new or not + if self.Xt == Xt: + # perform standard barycentric mapping transp_ = self.gamma_.T / np.sum(self.gamma_, 0)[:, None] # set nans to 0 transp_[~ np.isfinite(transp_)] = 0 # compute transported samples - transp_Xt = np.dot(transp_, Xs) + transp_Xt = np.dot(transp_, self.Xs) else: - print("mapping not yet implemented") + # perform out of sample mapping + print("out of sample mapping not yet implemented") return transp_Xt @@ -1114,7 +1255,10 @@ class SinkhornTransport(BaseTransport): def __init__(self, reg_e=1., mode="unsupervised", max_iter=1000, tol=10e-9, verbose=False, log=False, mapping="barycentric", - metric="sqeuclidean", distribution="uniform"): + metric="sqeuclidean", + distribution_estimation=distribution_estimation_uniform, + out_of_sample_map='ferradans'): + self.reg_e = reg_e self.mode = mode self.max_iter = max_iter @@ -1123,8 +1267,9 @@ class SinkhornTransport(BaseTransport): self.log = log self.mapping = mapping self.metric = metric - self.distribution = distribution + self.distribution_estimation = distribution_estimation self.method = "sinkhorn" + 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 -- cgit v1.2.3 From c7eaaf4caa03d759c4255bdf8b6eebd10ee539a5 Mon Sep 17 00:00:00 2001 From: Slasnista Date: Tue, 1 Aug 2017 10:42:09 +0200 Subject: update SinkhornTransport class + added test for class --- ot/da.py | 56 +++++++++++++++++++++----------------------------------- test/test_da.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 35 deletions(-) diff --git a/ot/da.py b/ot/da.py index d30c821..6b98a17 100644 --- a/ot/da.py +++ b/ot/da.py @@ -15,6 +15,7 @@ from .lp import emd from .utils import unif, dist, kernel from .optim import cg from .optim import gcg +import warnings def sinkhorn_lpl1_mm(a, labels_a, b, M, reg, eta=0.1, numItermax=10, @@ -921,15 +922,8 @@ class OTDA_mapping_kernel(OTDA_mapping_linear): # proposal ############################################################################## -# from sklearn.base import BaseEstimator -# from sklearn.metrics import pairwise_distances - -############################################################################## -# adapted from scikit-learn - -import warnings -# from .externals.six import string_types, iteritems +# adapted from sklearn class BaseEstimator(object): """Base class for all estimators in scikit-learn @@ -1067,7 +1061,7 @@ def distribution_estimation_uniform(X): The uniform distribution estimated from X """ - return np.ones(X.shape[0]) / float(X.shape[0]) + return unif(X.shape[0]) class BaseTransport(BaseEstimator): @@ -1092,29 +1086,20 @@ class BaseTransport(BaseEstimator): """ # pairwise distance - Cost = dist(Xs, Xt, metric=self.metric) + self.Cost = dist(Xs, Xt, metric=self.metric) if self.mode == "semisupervised": print("TODO: modify cost matrix accordingly") pass # distribution estimation - mu_s = self.distribution_estimation(Xs) - mu_t = self.distribution_estimation(Xt) + self.mu_s = self.distribution_estimation(Xs) + self.mu_t = self.distribution_estimation(Xt) # store arrays of samples self.Xs = Xs self.Xt = Xt - # coupling estimation - if self.method == "sinkhorn": - self.gamma_ = sinkhorn( - a=mu_s, b=mu_t, M=Cost, reg=self.reg_e, - numItermax=self.max_iter, stopThr=self.tol, - verbose=self.verbose, log=self.log) - else: - print("TODO: implement the other methods") - return self def fit_transform(self, Xs=None, ys=None, Xt=None, yt=None): @@ -1157,8 +1142,7 @@ class BaseTransport(BaseEstimator): The transport source samples. """ - # TODO: check whether Xs is new or not - if self.Xs == Xs: + if np.array_equal(self.Xs, Xs): # perform standard barycentric mapping transp = self.gamma_ / np.sum(self.gamma_, 1)[:, None] @@ -1169,7 +1153,9 @@ class BaseTransport(BaseEstimator): transp_Xs = np.dot(transp, self.Xt) else: # perform out of sample mapping - print("out of sample mapping not yet implemented") + print("Warning: out of sample mapping not yet implemented") + print("input data will be returned") + transp_Xs = Xs return transp_Xs @@ -1191,8 +1177,7 @@ class BaseTransport(BaseEstimator): The transported target samples. """ - # TODO: check whether Xt is new or not - if self.Xt == Xt: + if np.array_equal(self.Xt, Xt): # perform standard barycentric mapping transp_ = self.gamma_.T / np.sum(self.gamma_, 0)[:, None] @@ -1203,7 +1188,9 @@ class BaseTransport(BaseEstimator): transp_Xt = np.dot(transp_, self.Xs) else: # perform out of sample mapping - print("out of sample mapping not yet implemented") + print("Warning: out of sample mapping not yet implemented") + print("input data will be returned") + transp_Xt = Xt return transp_Xt @@ -1254,7 +1241,7 @@ class SinkhornTransport(BaseTransport): """ def __init__(self, reg_e=1., mode="unsupervised", max_iter=1000, - tol=10e-9, verbose=False, log=False, mapping="barycentric", + tol=10e-9, verbose=False, log=False, metric="sqeuclidean", distribution_estimation=distribution_estimation_uniform, out_of_sample_map='ferradans'): @@ -1265,7 +1252,6 @@ class SinkhornTransport(BaseTransport): self.tol = tol self.verbose = verbose self.log = log - self.mapping = mapping self.metric = metric self.distribution_estimation = distribution_estimation self.method = "sinkhorn" @@ -1290,10 +1276,10 @@ class SinkhornTransport(BaseTransport): Returns self. """ - return super(SinkhornTransport, self).fit(Xs, ys, Xt, yt) - + self = super(SinkhornTransport, self).fit(Xs, ys, Xt, yt) -if __name__ == "__main__": - print("Small test") - - st = SinkhornTransport() + # coupling estimation + self.gamma_ = 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) diff --git a/test/test_da.py b/test/test_da.py index dfba83f..e7b4ed1 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -6,6 +6,57 @@ import numpy as np import ot +from numpy.testing.utils import assert_allclose, assert_equal +from ot.datasets import get_data_classif +from ot.utils import unif + +np.random.seed(42) + + +def test_sinkhorn_transport(): + """test_sinkhorn_transport + """ + + ns = 150 + nt = 200 + + Xs, ys = get_data_classif('3gauss', ns) + Xt, yt = get_data_classif('3gauss2', nt) + + clf = ot.da.SinkhornTransport() + + # test its computed + clf.fit(Xs=Xs, Xt=Xt) + + # test dimensions of coupling + assert_equal(clf.Cost.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(clf.gamma_.shape, ((Xs.shape[0], Xt.shape[0]))) + + # test margin constraints + mu_s = unif(ns) + mu_t = unif(nt) + assert_allclose(np.sum(clf.gamma_, axis=0), mu_t, rtol=1e-3, atol=1e-3) + assert_allclose(np.sum(clf.gamma_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + + # test transform + transp_Xs = clf.transform(Xs=Xs) + assert_equal(transp_Xs.shape, Xs.shape) + + Xs_new, _ = get_data_classif('3gauss', ns + 1) + transp_Xs_new = clf.transform(Xs_new) + + # check that the oos method is not working + assert_equal(transp_Xs_new, Xs_new) + + # test inverse transform + transp_Xt = clf.inverse_transform(Xt=Xt) + assert_equal(transp_Xt.shape, Xt.shape) + + Xt_new, _ = get_data_classif('3gauss2', nt + 1) + transp_Xt_new = clf.inverse_transform(Xt=Xt_new) + + # check that the oos method is not working and returns the input data + assert_equal(transp_Xt_new, Xt_new) def test_otda(): -- cgit v1.2.3 From d5c6cc178a731d955e5eb85e9f477805fa086518 Mon Sep 17 00:00:00 2001 From: Slasnista Date: Tue, 1 Aug 2017 13:13:50 +0200 Subject: added EMDTransport Class from NG's code + added dedicated test --- ot/da.py | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- test/test_da.py | 59 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 135 insertions(+), 10 deletions(-) diff --git a/ot/da.py b/ot/da.py index 6b98a17..fb2fd36 100644 --- a/ot/da.py +++ b/ot/da.py @@ -1144,7 +1144,7 @@ class BaseTransport(BaseEstimator): if np.array_equal(self.Xs, Xs): # perform standard barycentric mapping - transp = self.gamma_ / np.sum(self.gamma_, 1)[:, None] + transp = self.Coupling_ / np.sum(self.Coupling_, 1)[:, None] # set nans to 0 transp[~ np.isfinite(transp)] = 0 @@ -1179,7 +1179,7 @@ class BaseTransport(BaseEstimator): if np.array_equal(self.Xt, Xt): # perform standard barycentric mapping - transp_ = self.gamma_.T / np.sum(self.gamma_, 0)[:, None] + transp_ = self.Coupling_.T / np.sum(self.Coupling_, 0)[:, None] # set nans to 0 transp_[~ np.isfinite(transp_)] = 0 @@ -1228,7 +1228,7 @@ class SinkhornTransport(BaseTransport): Controls the logs of the optimization algorithm Attributes ---------- - gamma_ : the optimal coupling + Coupling_ : the optimal coupling References ---------- @@ -1254,7 +1254,6 @@ class SinkhornTransport(BaseTransport): self.log = log self.metric = metric self.distribution_estimation = distribution_estimation - self.method = "sinkhorn" self.out_of_sample_map = out_of_sample_map def fit(self, Xs=None, ys=None, Xt=None, yt=None): @@ -1276,10 +1275,85 @@ class SinkhornTransport(BaseTransport): Returns self. """ - self = super(SinkhornTransport, self).fit(Xs, ys, Xt, yt) + super(SinkhornTransport, self).fit(Xs, ys, Xt, yt) # coupling estimation - self.gamma_ = sinkhorn( + self.Coupling_ = 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) + + +class EMDTransport(BaseTransport): + """Domain Adapatation OT method based on Earth Mover's Distance + Parameters + ---------- + mode : string, optional (default="unsupervised") + The DA mode. If "unsupervised" no target labels are taken into account + to modify the cost matrix. If "semisupervised" the target labels + are taken into account to set coefficients of the pairwise distance + matrix to 0 for row and columns indices that correspond to source and + target samples which share the same labels. + mapping : string, optional (default="barycentric") + The kind of mapping to apply to transport samples from a domain into + another one. + if "barycentric" only the samples used to estimate the coupling can + be transported from a domain to another one. + metric : string, optional (default="sqeuclidean") + The ground metric for the Wasserstein problem + distribution : string, optional (default="uniform") + The kind of distribution estimation to employ + verbose : int, optional (default=0) + Controls the verbosity of the optimization algorithm + log : int, optional (default=0) + Controls the logs of the optimization algorithm + Attributes + ---------- + Coupling_ : 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, mode="unsupervised", verbose=False, + log=False, metric="sqeuclidean", + distribution_estimation=distribution_estimation_uniform, + out_of_sample_map='ferradans'): + + self.mode = mode + self.verbose = verbose + self.log = log + self.metric = metric + 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 of shape = [n_source_samples, n_features] + The training input samples. + ys : array-like, shape = [n_source_samples] + The class labels + Xt : array-like of shape = [n_target_samples, n_features] + The training input samples. + yt : array-like, shape = [n_labeled_target_samples] + The class labels + Returns + ------- + self : object + Returns self. + """ + + super(EMDTransport, self).fit(Xs, ys, Xt, yt) + + # coupling estimation + self.Coupling_ = emd( + a=self.mu_s, b=self.mu_t, M=self.Cost, + # verbose=self.verbose, + # log=self.log + ) diff --git a/test/test_da.py b/test/test_da.py index e7b4ed1..33b3695 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -13,7 +13,7 @@ from ot.utils import unif np.random.seed(42) -def test_sinkhorn_transport(): +def test_sinkhorn_transport_class(): """test_sinkhorn_transport """ @@ -30,13 +30,59 @@ def test_sinkhorn_transport(): # test dimensions of coupling assert_equal(clf.Cost.shape, ((Xs.shape[0], Xt.shape[0]))) - assert_equal(clf.gamma_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(clf.Coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) # test margin constraints mu_s = unif(ns) mu_t = unif(nt) - assert_allclose(np.sum(clf.gamma_, axis=0), mu_t, rtol=1e-3, atol=1e-3) - assert_allclose(np.sum(clf.gamma_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + assert_allclose(np.sum(clf.Coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) + assert_allclose(np.sum(clf.Coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + + # test transform + transp_Xs = clf.transform(Xs=Xs) + assert_equal(transp_Xs.shape, Xs.shape) + + Xs_new, _ = get_data_classif('3gauss', ns + 1) + transp_Xs_new = clf.transform(Xs_new) + + # check that the oos method is not working + assert_equal(transp_Xs_new, Xs_new) + + # test inverse transform + transp_Xt = clf.inverse_transform(Xt=Xt) + assert_equal(transp_Xt.shape, Xt.shape) + + Xt_new, _ = get_data_classif('3gauss2', nt + 1) + transp_Xt_new = clf.inverse_transform(Xt=Xt_new) + + # check that the oos method is not working and returns the input data + assert_equal(transp_Xt_new, Xt_new) + + +def test_emd_transport_class(): + """test_sinkhorn_transport + """ + + ns = 150 + nt = 200 + + Xs, ys = get_data_classif('3gauss', ns) + Xt, yt = get_data_classif('3gauss2', nt) + + clf = ot.da.EMDTransport() + + # test its computed + clf.fit(Xs=Xs, Xt=Xt) + + # test dimensions of coupling + assert_equal(clf.Cost.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(clf.Coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) + + # test margin constraints + mu_s = unif(ns) + mu_t = unif(nt) + assert_allclose(np.sum(clf.Coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) + assert_allclose(np.sum(clf.Coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) # test transform transp_Xs = clf.transform(Xs=Xs) @@ -119,3 +165,8 @@ def test_otda(): da_emd = ot.da.OTDA_mapping_kernel() # init class da_emd.fit(xs, xt, numItermax=10) # fit distributions da_emd.predict(xs) # interpolation of source samples + + +if __name__ == "__main__": + test_sinkhorn_transport_class() + test_emd_transport_class() -- cgit v1.2.3 From cd4fa7275dc65e04f7b256dec4208d68006abc25 Mon Sep 17 00:00:00 2001 From: Slasnista Date: Fri, 4 Aug 2017 11:16:30 +0200 Subject: added test for fit_transform + correction of fit_transform bug (missing return self) --- ot/da.py | 4 ++++ test/test_da.py | 13 ++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/ot/da.py b/ot/da.py index fb2fd36..80649a7 100644 --- a/ot/da.py +++ b/ot/da.py @@ -1283,6 +1283,8 @@ class SinkhornTransport(BaseTransport): numItermax=self.max_iter, stopThr=self.tol, verbose=self.verbose, log=self.log) + return self + class EMDTransport(BaseTransport): """Domain Adapatation OT method based on Earth Mover's Distance @@ -1357,3 +1359,5 @@ class EMDTransport(BaseTransport): # verbose=self.verbose, # log=self.log ) + + return self diff --git a/test/test_da.py b/test/test_da.py index 33b3695..68807ec 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -58,6 +58,10 @@ def test_sinkhorn_transport_class(): # check that the oos method is not working and returns the input data assert_equal(transp_Xt_new, Xt_new) + # test fit_transform + transp_Xs = clf.fit_transform(Xs=Xs, Xt=Xt) + assert_equal(transp_Xs.shape, Xs.shape) + def test_emd_transport_class(): """test_sinkhorn_transport @@ -104,6 +108,10 @@ def test_emd_transport_class(): # check that the oos method is not working and returns the input data assert_equal(transp_Xt_new, Xt_new) + # test fit_transform + transp_Xs = clf.fit_transform(Xs=Xs, Xt=Xt) + assert_equal(transp_Xs.shape, Xs.shape) + def test_otda(): @@ -165,8 +173,3 @@ def test_otda(): da_emd = ot.da.OTDA_mapping_kernel() # init class da_emd.fit(xs, xt, numItermax=10) # fit distributions da_emd.predict(xs) # interpolation of source samples - - -if __name__ == "__main__": - test_sinkhorn_transport_class() - test_emd_transport_class() -- cgit v1.2.3 From 0659abe79c15f786a017b62e2a1313f0625af329 Mon Sep 17 00:00:00 2001 From: Slasnista Date: Fri, 4 Aug 2017 11:34:21 +0200 Subject: added new class SinkhornLpl1Transport() + dedicated test --- ot/da.py | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/test_da.py | 50 +++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+) diff --git a/ot/da.py b/ot/da.py index 80649a7..3031f63 100644 --- a/ot/da.py +++ b/ot/da.py @@ -1361,3 +1361,94 @@ class EMDTransport(BaseTransport): ) return self + + +class SinkhornLpl1Transport(BaseTransport): + """Domain Adapatation OT method based on sinkhorn algorithm + + LpL1 class regularization. + + Parameters + ---------- + mode : string, optional (default="unsupervised") + The DA mode. If "unsupervised" no target labels are taken into account + to modify the cost matrix. If "semisupervised" the target labels + are taken into account to set coefficients of the pairwise distance + matrix to 0 for row and columns indices that correspond to source and + target samples which share the same labels. + mapping : string, optional (default="barycentric") + The kind of mapping to apply to transport samples from a domain into + another one. + if "barycentric" only the samples used to estimate the coupling can + be transported from a domain to another one. + metric : string, optional (default="sqeuclidean") + The ground metric for the Wasserstein problem + distribution : string, optional (default="uniform") + The kind of distribution estimation to employ + verbose : int, optional (default=0) + Controls the verbosity of the optimization algorithm + log : int, optional (default=0) + Controls the logs of the optimization algorithm + Attributes + ---------- + Coupling_ : 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, mode="unsupervised", + max_iter=10, max_inner_iter=200, + tol=10e-9, verbose=False, log=False, + metric="sqeuclidean", + distribution_estimation=distribution_estimation_uniform, + out_of_sample_map='ferradans'): + + self.reg_e = reg_e + self.reg_cl = reg_cl + self.mode = mode + 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.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 of shape = [n_source_samples, n_features] + The training input samples. + ys : array-like, shape = [n_source_samples] + The class labels + Xt : array-like of shape = [n_target_samples, n_features] + The training input samples. + yt : array-like, shape = [n_labeled_target_samples] + The class labels + Returns + ------- + self : object + Returns self. + """ + + super(SinkhornLpl1Transport, self).fit(Xs, ys, Xt, yt) + + self.Coupling_ = 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) + + return self diff --git a/test/test_da.py b/test/test_da.py index 68807ec..7d00cfb 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -13,6 +13,56 @@ from ot.utils import unif np.random.seed(42) +def test_sinkhorn_lpl1_transport_class(): + """test_sinkhorn_transport + """ + + ns = 150 + nt = 200 + + Xs, ys = get_data_classif('3gauss', ns) + Xt, yt = get_data_classif('3gauss2', nt) + + clf = ot.da.SinkhornLpl1Transport() + + # test its computed + clf.fit(Xs=Xs, ys=ys, Xt=Xt) + + # test dimensions of coupling + assert_equal(clf.Cost.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(clf.Coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) + + # test margin constraints + mu_s = unif(ns) + mu_t = unif(nt) + assert_allclose(np.sum(clf.Coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) + assert_allclose(np.sum(clf.Coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + + # test transform + transp_Xs = clf.transform(Xs=Xs) + assert_equal(transp_Xs.shape, Xs.shape) + + Xs_new, _ = get_data_classif('3gauss', ns + 1) + transp_Xs_new = clf.transform(Xs_new) + + # check that the oos method is not working + assert_equal(transp_Xs_new, Xs_new) + + # test inverse transform + transp_Xt = clf.inverse_transform(Xt=Xt) + assert_equal(transp_Xt.shape, Xt.shape) + + Xt_new, _ = get_data_classif('3gauss2', nt + 1) + transp_Xt_new = clf.inverse_transform(Xt=Xt_new) + + # check that the oos method is not working and returns the input data + assert_equal(transp_Xt_new, Xt_new) + + # test fit_transform + transp_Xs = clf.fit_transform(Xs=Xs, ys=ys, Xt=Xt) + assert_equal(transp_Xs.shape, Xs.shape) + + def test_sinkhorn_transport_class(): """test_sinkhorn_transport """ -- cgit v1.2.3 From 2005a09548a6f6d42cd9aafadbb4583e4029936c Mon Sep 17 00:00:00 2001 From: Slasnista Date: Fri, 4 Aug 2017 11:40:44 +0200 Subject: added new class SinkhornL1l2Transport() + dedicated test --- ot/da.py | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/test_da.py | 50 ++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) diff --git a/ot/da.py b/ot/da.py index 3031f63..6100d15 100644 --- a/ot/da.py +++ b/ot/da.py @@ -1369,6 +1369,10 @@ class SinkhornLpl1Transport(BaseTransport): Parameters ---------- + reg_e : float, optional (default=1) + Entropic regularization parameter + reg_cl : float, optional (default=0.1) + Class regularization parameter mode : string, optional (default="unsupervised") The DA mode. If "unsupervised" no target labels are taken into account to modify the cost matrix. If "semisupervised" the target labels @@ -1384,6 +1388,11 @@ class SinkhornLpl1Transport(BaseTransport): The ground metric for the Wasserstein problem distribution : string, optional (default="uniform") The kind of distribution estimation to employ + 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 verbose : int, optional (default=0) Controls the verbosity of the optimization algorithm log : int, optional (default=0) @@ -1452,3 +1461,103 @@ class SinkhornLpl1Transport(BaseTransport): verbose=self.verbose, log=self.log) 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 + mode : string, optional (default="unsupervised") + The DA mode. If "unsupervised" no target labels are taken into account + to modify the cost matrix. If "semisupervised" the target labels + are taken into account to set coefficients of the pairwise distance + matrix to 0 for row and columns indices that correspond to source and + target samples which share the same labels. + mapping : string, optional (default="barycentric") + The kind of mapping to apply to transport samples from a domain into + another one. + if "barycentric" only the samples used to estimate the coupling can + be transported from a domain to another one. + metric : string, optional (default="sqeuclidean") + The ground metric for the Wasserstein problem + distribution : string, optional (default="uniform") + The kind of distribution estimation to employ + 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 + verbose : int, optional (default=0) + Controls the verbosity of the optimization algorithm + log : int, optional (default=0) + Controls the logs of the optimization algorithm + Attributes + ---------- + Coupling_ : 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, mode="unsupervised", + max_iter=10, max_inner_iter=200, + tol=10e-9, verbose=False, log=False, + metric="sqeuclidean", + distribution_estimation=distribution_estimation_uniform, + out_of_sample_map='ferradans'): + + self.reg_e = reg_e + self.reg_cl = reg_cl + self.mode = mode + 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.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 of shape = [n_source_samples, n_features] + The training input samples. + ys : array-like, shape = [n_source_samples] + The class labels + Xt : array-like of shape = [n_target_samples, n_features] + The training input samples. + yt : array-like, shape = [n_labeled_target_samples] + The class labels + Returns + ------- + self : object + Returns self. + """ + + super(SinkhornL1l2Transport, self).fit(Xs, ys, Xt, yt) + + self.Coupling_ = 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) + + return self diff --git a/test/test_da.py b/test/test_da.py index 7d00cfb..68d1958 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -63,6 +63,56 @@ def test_sinkhorn_lpl1_transport_class(): assert_equal(transp_Xs.shape, Xs.shape) +def test_sinkhorn_l1l2_transport_class(): + """test_sinkhorn_transport + """ + + ns = 150 + nt = 200 + + Xs, ys = get_data_classif('3gauss', ns) + Xt, yt = get_data_classif('3gauss2', nt) + + clf = ot.da.SinkhornL1l2Transport() + + # test its computed + clf.fit(Xs=Xs, ys=ys, Xt=Xt) + + # test dimensions of coupling + assert_equal(clf.Cost.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(clf.Coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) + + # test margin constraints + mu_s = unif(ns) + mu_t = unif(nt) + assert_allclose(np.sum(clf.Coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) + assert_allclose(np.sum(clf.Coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + + # test transform + transp_Xs = clf.transform(Xs=Xs) + assert_equal(transp_Xs.shape, Xs.shape) + + Xs_new, _ = get_data_classif('3gauss', ns + 1) + transp_Xs_new = clf.transform(Xs_new) + + # check that the oos method is not working + assert_equal(transp_Xs_new, Xs_new) + + # test inverse transform + transp_Xt = clf.inverse_transform(Xt=Xt) + assert_equal(transp_Xt.shape, Xt.shape) + + Xt_new, _ = get_data_classif('3gauss2', nt + 1) + transp_Xt_new = clf.inverse_transform(Xt=Xt_new) + + # check that the oos method is not working and returns the input data + assert_equal(transp_Xt_new, Xt_new) + + # test fit_transform + transp_Xs = clf.fit_transform(Xs=Xs, ys=ys, Xt=Xt) + assert_equal(transp_Xs.shape, Xs.shape) + + def test_sinkhorn_transport_class(): """test_sinkhorn_transport """ -- cgit v1.2.3 From 4e562a1ce24119b8c9c1efb9d078762904c5d78a Mon Sep 17 00:00:00 2001 From: Slasnista Date: Fri, 4 Aug 2017 12:04:04 +0200 Subject: semi supervised mode supported --- ot/da.py | 21 +++++++++++++++++++-- test/test_da.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/ot/da.py b/ot/da.py index 6100d15..8294e8d 100644 --- a/ot/da.py +++ b/ot/da.py @@ -1089,8 +1089,25 @@ class BaseTransport(BaseEstimator): self.Cost = dist(Xs, Xt, metric=self.metric) if self.mode == "semisupervised": - print("TODO: modify cost matrix accordingly") - pass + + if (ys is not None) and (yt is not None): + + # assumes labeled source samples occupy the first rows + # and labeled target samples occupy the first columns + classes = np.unique(ys) + for c in classes: + ids = np.where(ys == c) + idt = np.where(yt == c) + + # all the coefficients corresponding to a source sample + # and a target sample with the same label gets a 0 + # transport cost + for j in idt[0]: + self.Cost[ids[0], j] = 0 + else: + print("Warning: using unsupervised mode\ + \nto use semisupervised mode, please provide ys and yt") + pass # distribution estimation self.mu_s = self.distribution_estimation(Xs) diff --git a/test/test_da.py b/test/test_da.py index 68d1958..497a8ee 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -62,6 +62,19 @@ def test_sinkhorn_lpl1_transport_class(): transp_Xs = clf.fit_transform(Xs=Xs, ys=ys, Xt=Xt) assert_equal(transp_Xs.shape, Xs.shape) + # test semi supervised mode + clf = ot.da.SinkhornTransport(mode="semisupervised") + clf.fit(Xs=Xs, Xt=Xt) + n_unsup = np.sum(clf.Cost) + + # test semi supervised mode + clf = ot.da.SinkhornTransport(mode="semisupervised") + clf.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) + assert_equal(clf.Cost.shape, ((Xs.shape[0], Xt.shape[0]))) + n_semisup = np.sum(clf.Cost) + + assert n_unsup != n_semisup, "semisupervised mode not working" + def test_sinkhorn_l1l2_transport_class(): """test_sinkhorn_transport @@ -112,6 +125,19 @@ def test_sinkhorn_l1l2_transport_class(): transp_Xs = clf.fit_transform(Xs=Xs, ys=ys, Xt=Xt) assert_equal(transp_Xs.shape, Xs.shape) + # test semi supervised mode + clf = ot.da.SinkhornTransport(mode="semisupervised") + clf.fit(Xs=Xs, Xt=Xt) + n_unsup = np.sum(clf.Cost) + + # test semi supervised mode + clf = ot.da.SinkhornTransport(mode="semisupervised") + clf.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) + assert_equal(clf.Cost.shape, ((Xs.shape[0], Xt.shape[0]))) + n_semisup = np.sum(clf.Cost) + + assert n_unsup != n_semisup, "semisupervised mode not working" + def test_sinkhorn_transport_class(): """test_sinkhorn_transport @@ -162,6 +188,19 @@ def test_sinkhorn_transport_class(): transp_Xs = clf.fit_transform(Xs=Xs, Xt=Xt) assert_equal(transp_Xs.shape, Xs.shape) + # test semi supervised mode + clf = ot.da.SinkhornTransport(mode="semisupervised") + clf.fit(Xs=Xs, Xt=Xt) + n_unsup = np.sum(clf.Cost) + + # test semi supervised mode + clf = ot.da.SinkhornTransport(mode="semisupervised") + clf.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) + assert_equal(clf.Cost.shape, ((Xs.shape[0], Xt.shape[0]))) + n_semisup = np.sum(clf.Cost) + + assert n_unsup != n_semisup, "semisupervised mode not working" + def test_emd_transport_class(): """test_sinkhorn_transport @@ -212,6 +251,19 @@ def test_emd_transport_class(): transp_Xs = clf.fit_transform(Xs=Xs, Xt=Xt) assert_equal(transp_Xs.shape, Xs.shape) + # test semi supervised mode + clf = ot.da.SinkhornTransport(mode="semisupervised") + clf.fit(Xs=Xs, Xt=Xt) + n_unsup = np.sum(clf.Cost) + + # test semi supervised mode + clf = ot.da.SinkhornTransport(mode="semisupervised") + clf.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) + assert_equal(clf.Cost.shape, ((Xs.shape[0], Xt.shape[0]))) + n_semisup = np.sum(clf.Cost) + + assert n_unsup != n_semisup, "semisupervised mode not working" + def test_otda(): -- cgit v1.2.3 From 62b40a9993e9ccca27d1677aa1294fff6246e904 Mon Sep 17 00:00:00 2001 From: Slasnista Date: Fri, 4 Aug 2017 13:56:51 +0200 Subject: correction of semi supervised mode --- ot/da.py | 77 +++++++++++++++++++++++++++++++++------------------------ test/test_da.py | 20 +++++++-------- 2 files changed, 55 insertions(+), 42 deletions(-) diff --git a/ot/da.py b/ot/da.py index 8294e8d..08e8a8d 100644 --- a/ot/da.py +++ b/ot/da.py @@ -1088,26 +1088,23 @@ class BaseTransport(BaseEstimator): # pairwise distance self.Cost = dist(Xs, Xt, metric=self.metric) - if self.mode == "semisupervised": - - if (ys is not None) and (yt is not None): - - # assumes labeled source samples occupy the first rows - # and labeled target samples occupy the first columns - classes = np.unique(ys) - for c in classes: - ids = np.where(ys == c) - idt = np.where(yt == c) - - # all the coefficients corresponding to a source sample - # and a target sample with the same label gets a 0 - # transport cost - for j in idt[0]: - self.Cost[ids[0], j] = 0 - else: - print("Warning: using unsupervised mode\ - \nto use semisupervised mode, please provide ys and yt") - pass + 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 = np.unique(ys) + 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) @@ -1243,6 +1240,9 @@ class SinkhornTransport(BaseTransport): Controls the verbosity of the optimization algorithm log : int, optional (default=0) Controls the logs of the optimization algorithm + 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 infinite cost Attributes ---------- Coupling_ : the optimal coupling @@ -1257,19 +1257,19 @@ class SinkhornTransport(BaseTransport): 26, 2013 """ - def __init__(self, reg_e=1., mode="unsupervised", max_iter=1000, + def __init__(self, reg_e=1., max_iter=1000, tol=10e-9, verbose=False, log=False, metric="sqeuclidean", distribution_estimation=distribution_estimation_uniform, - out_of_sample_map='ferradans'): + out_of_sample_map='ferradans', limit_max=np.infty): self.reg_e = reg_e - self.mode = mode self.max_iter = max_iter self.tol = tol self.verbose = verbose self.log = log self.metric = metric + self.limit_max = limit_max self.distribution_estimation = distribution_estimation self.out_of_sample_map = out_of_sample_map @@ -1326,6 +1326,10 @@ class EMDTransport(BaseTransport): Controls the verbosity of the optimization algorithm log : int, optional (default=0) Controls the logs of the optimization algorithm + 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_ : the optimal coupling @@ -1337,15 +1341,15 @@ class EMDTransport(BaseTransport): on Pattern Analysis and Machine Intelligence , vol.PP, no.99, pp.1-1 """ - def __init__(self, mode="unsupervised", verbose=False, + def __init__(self, verbose=False, log=False, metric="sqeuclidean", distribution_estimation=distribution_estimation_uniform, - out_of_sample_map='ferradans'): + out_of_sample_map='ferradans', limit_max=10): - self.mode = mode self.verbose = verbose self.log = log self.metric = metric + self.limit_max = limit_max self.distribution_estimation = distribution_estimation self.out_of_sample_map = out_of_sample_map @@ -1414,6 +1418,10 @@ class SinkhornLpl1Transport(BaseTransport): Controls the verbosity of the optimization algorithm log : int, optional (default=0) Controls the logs of the optimization algorithm + 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 infinite cost + Attributes ---------- Coupling_ : the optimal coupling @@ -1431,16 +1439,15 @@ class SinkhornLpl1Transport(BaseTransport): """ - def __init__(self, reg_e=1., reg_cl=0.1, mode="unsupervised", + 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", distribution_estimation=distribution_estimation_uniform, - out_of_sample_map='ferradans'): + out_of_sample_map='ferradans', limit_max=np.infty): self.reg_e = reg_e self.reg_cl = reg_cl - self.mode = mode self.max_iter = max_iter self.max_inner_iter = max_inner_iter self.tol = tol @@ -1449,6 +1456,7 @@ class SinkhornLpl1Transport(BaseTransport): self.metric = metric 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 @@ -1514,6 +1522,11 @@ class SinkhornL1l2Transport(BaseTransport): Controls the verbosity of the optimization algorithm log : int, optional (default=0) Controls the logs of the optimization algorithm + 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_ : the optimal coupling @@ -1531,16 +1544,15 @@ class SinkhornL1l2Transport(BaseTransport): """ - def __init__(self, reg_e=1., reg_cl=0.1, mode="unsupervised", + 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", distribution_estimation=distribution_estimation_uniform, - out_of_sample_map='ferradans'): + out_of_sample_map='ferradans', limit_max=10): self.reg_e = reg_e self.reg_cl = reg_cl - self.mode = mode self.max_iter = max_iter self.max_inner_iter = max_inner_iter self.tol = tol @@ -1549,6 +1561,7 @@ class SinkhornL1l2Transport(BaseTransport): self.metric = metric 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 diff --git a/test/test_da.py b/test/test_da.py index 497a8ee..ecd2a3a 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -63,12 +63,12 @@ def test_sinkhorn_lpl1_transport_class(): assert_equal(transp_Xs.shape, Xs.shape) # test semi supervised mode - clf = ot.da.SinkhornTransport(mode="semisupervised") - clf.fit(Xs=Xs, Xt=Xt) + clf = ot.da.SinkhornLpl1Transport() + clf.fit(Xs=Xs, ys=ys, Xt=Xt) n_unsup = np.sum(clf.Cost) # test semi supervised mode - clf = ot.da.SinkhornTransport(mode="semisupervised") + clf = ot.da.SinkhornLpl1Transport() clf.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) assert_equal(clf.Cost.shape, ((Xs.shape[0], Xt.shape[0]))) n_semisup = np.sum(clf.Cost) @@ -126,12 +126,12 @@ def test_sinkhorn_l1l2_transport_class(): assert_equal(transp_Xs.shape, Xs.shape) # test semi supervised mode - clf = ot.da.SinkhornTransport(mode="semisupervised") - clf.fit(Xs=Xs, Xt=Xt) + clf = ot.da.SinkhornL1l2Transport() + clf.fit(Xs=Xs, ys=ys, Xt=Xt) n_unsup = np.sum(clf.Cost) # test semi supervised mode - clf = ot.da.SinkhornTransport(mode="semisupervised") + clf = ot.da.SinkhornL1l2Transport() clf.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) assert_equal(clf.Cost.shape, ((Xs.shape[0], Xt.shape[0]))) n_semisup = np.sum(clf.Cost) @@ -189,12 +189,12 @@ def test_sinkhorn_transport_class(): assert_equal(transp_Xs.shape, Xs.shape) # test semi supervised mode - clf = ot.da.SinkhornTransport(mode="semisupervised") + clf = ot.da.SinkhornTransport() clf.fit(Xs=Xs, Xt=Xt) n_unsup = np.sum(clf.Cost) # test semi supervised mode - clf = ot.da.SinkhornTransport(mode="semisupervised") + clf = ot.da.SinkhornTransport() clf.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) assert_equal(clf.Cost.shape, ((Xs.shape[0], Xt.shape[0]))) n_semisup = np.sum(clf.Cost) @@ -252,12 +252,12 @@ def test_emd_transport_class(): assert_equal(transp_Xs.shape, Xs.shape) # test semi supervised mode - clf = ot.da.SinkhornTransport(mode="semisupervised") + clf = ot.da.EMDTransport() clf.fit(Xs=Xs, Xt=Xt) n_unsup = np.sum(clf.Cost) # test semi supervised mode - clf = ot.da.SinkhornTransport(mode="semisupervised") + clf = ot.da.EMDTransport() clf.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) assert_equal(clf.Cost.shape, ((Xs.shape[0], Xt.shape[0]))) n_semisup = np.sum(clf.Cost) -- cgit v1.2.3 From 266abb6c9a0fa53e419d72b99d1906cdf78a8009 Mon Sep 17 00:00:00 2001 From: Slasnista Date: Fri, 4 Aug 2017 14:02:06 +0200 Subject: reformat doc strings + remove useless log / verbose parameters for emd --- ot/da.py | 81 ++++++++++++++++++++++++++++++---------------------------------- 1 file changed, 38 insertions(+), 43 deletions(-) diff --git a/ot/da.py b/ot/da.py index 08e8a8d..92a8f12 100644 --- a/ot/da.py +++ b/ot/da.py @@ -1053,11 +1053,11 @@ def distribution_estimation_uniform(X): Parameters ---------- - X : array-like of shape = [n_samples, n_features] + X : array-like of shape = (n_samples, n_features) The array of samples Returns ------- - mu : array-like, shape = [n_samples,] + mu : array-like, shape = (n_samples,) The uniform distribution estimated from X """ @@ -1071,13 +1071,13 @@ class BaseTransport(BaseEstimator): (Xs, ys) and (Xt, yt) Parameters ---------- - Xs : array-like of shape = [n_source_samples, n_features] + Xs : array-like of shape = (n_source_samples, n_features) The training input samples. - ys : array-like, shape = [n_source_samples] + ys : array-like, shape = (n_source_samples,) The class labels - Xt : array-like of shape = [n_target_samples, n_features] + Xt : array-like of shape = (n_target_samples, n_features) The training input samples. - yt : array-like, shape = [n_labeled_target_samples] + yt : array-like, shape = (n_labeled_target_samples,) The class labels Returns ------- @@ -1122,17 +1122,17 @@ class BaseTransport(BaseEstimator): ones Xt Parameters ---------- - Xs : array-like of shape = [n_source_samples, n_features] + Xs : array-like of shape = (n_source_samples, n_features) The training input samples. - ys : array-like, shape = [n_source_samples] + ys : array-like, shape = (n_source_samples,) The class labels - Xt : array-like of shape = [n_target_samples, n_features] + Xt : array-like of shape = (n_target_samples, n_features) The training input samples. - yt : array-like, shape = [n_labeled_target_samples] + yt : array-like, shape = (n_labeled_target_samples,) The class labels Returns ------- - transp_Xs : array-like of shape = [n_source_samples, n_features] + transp_Xs : array-like of shape = (n_source_samples, n_features) The source samples samples. """ @@ -1142,17 +1142,17 @@ class BaseTransport(BaseEstimator): """Transports source samples Xs onto target ones Xt Parameters ---------- - Xs : array-like of shape = [n_source_samples, n_features] + Xs : array-like of shape = (n_source_samples, n_features) The training input samples. - ys : array-like, shape = [n_source_samples] + ys : array-like, shape = (n_source_samples,) The class labels - Xt : array-like of shape = [n_target_samples, n_features] + Xt : array-like of shape = (n_target_samples, n_features) The training input samples. - yt : array-like, shape = [n_labeled_target_samples] + yt : array-like, shape = (n_labeled_target_samples,) The class labels Returns ------- - transp_Xs : array-like of shape = [n_source_samples, n_features] + transp_Xs : array-like of shape = (n_source_samples, n_features) The transport source samples. """ @@ -1177,17 +1177,17 @@ class BaseTransport(BaseEstimator): """Transports target samples Xt onto target samples Xs Parameters ---------- - Xs : array-like of shape = [n_source_samples, n_features] + Xs : array-like of shape = (n_source_samples, n_features) The training input samples. - ys : array-like, shape = [n_source_samples] + ys : array-like, shape = (n_source_samples,) The class labels - Xt : array-like of shape = [n_target_samples, n_features] + Xt : array-like of shape = (n_target_samples, n_features) The training input samples. - yt : array-like, shape = [n_labeled_target_samples] + yt : array-like, shape = (n_labeled_target_samples,) The class labels Returns ------- - transp_Xt : array-like of shape = [n_source_samples, n_features] + transp_Xt : array-like of shape = (n_source_samples, n_features) The transported target samples. """ @@ -1278,13 +1278,13 @@ class SinkhornTransport(BaseTransport): (Xs, ys) and (Xt, yt) Parameters ---------- - Xs : array-like of shape = [n_source_samples, n_features] + Xs : array-like of shape = (n_source_samples, n_features) The training input samples. - ys : array-like, shape = [n_source_samples] + ys : array-like, shape = (n_source_samples,) The class labels - Xt : array-like of shape = [n_target_samples, n_features] + Xt : array-like of shape = (n_target_samples, n_features) The training input samples. - yt : array-like, shape = [n_labeled_target_samples] + yt : array-like, shape = (n_labeled_target_samples,) The class labels Returns ------- @@ -1341,13 +1341,10 @@ class EMDTransport(BaseTransport): on Pattern Analysis and Machine Intelligence , vol.PP, no.99, pp.1-1 """ - def __init__(self, verbose=False, - log=False, metric="sqeuclidean", + def __init__(self, metric="sqeuclidean", distribution_estimation=distribution_estimation_uniform, out_of_sample_map='ferradans', limit_max=10): - self.verbose = verbose - self.log = log self.metric = metric self.limit_max = limit_max self.distribution_estimation = distribution_estimation @@ -1358,13 +1355,13 @@ class EMDTransport(BaseTransport): (Xs, ys) and (Xt, yt) Parameters ---------- - Xs : array-like of shape = [n_source_samples, n_features] + Xs : array-like of shape = (n_source_samples, n_features) The training input samples. - ys : array-like, shape = [n_source_samples] + ys : array-like, shape = (n_source_samples,) The class labels - Xt : array-like of shape = [n_target_samples, n_features] + Xt : array-like of shape = (n_target_samples, n_features) The training input samples. - yt : array-like, shape = [n_labeled_target_samples] + yt : array-like, shape = (n_labeled_target_samples,) The class labels Returns ------- @@ -1377,8 +1374,6 @@ class EMDTransport(BaseTransport): # coupling estimation self.Coupling_ = emd( a=self.mu_s, b=self.mu_t, M=self.Cost, - # verbose=self.verbose, - # log=self.log ) return self @@ -1463,13 +1458,13 @@ class SinkhornLpl1Transport(BaseTransport): (Xs, ys) and (Xt, yt) Parameters ---------- - Xs : array-like of shape = [n_source_samples, n_features] + Xs : array-like of shape = (n_source_samples, n_features) The training input samples. - ys : array-like, shape = [n_source_samples] + ys : array-like, shape = (n_source_samples,) The class labels - Xt : array-like of shape = [n_target_samples, n_features] + Xt : array-like of shape = (n_target_samples, n_features) The training input samples. - yt : array-like, shape = [n_labeled_target_samples] + yt : array-like, shape = (n_labeled_target_samples,) The class labels Returns ------- @@ -1568,13 +1563,13 @@ class SinkhornL1l2Transport(BaseTransport): (Xs, ys) and (Xt, yt) Parameters ---------- - Xs : array-like of shape = [n_source_samples, n_features] + Xs : array-like of shape = (n_source_samples, n_features) The training input samples. - ys : array-like, shape = [n_source_samples] + ys : array-like, shape = (n_source_samples,) The class labels - Xt : array-like of shape = [n_target_samples, n_features] + Xt : array-like of shape = (n_target_samples, n_features) The training input samples. - yt : array-like, shape = [n_labeled_target_samples] + yt : array-like, shape = (n_labeled_target_samples,) The class labels Returns ------- -- cgit v1.2.3 From b8672f67639e9daa3f91e555581256f984115f56 Mon Sep 17 00:00:00 2001 From: Slasnista Date: Fri, 4 Aug 2017 14:55:54 +0200 Subject: out of samples by Ferradans supported for transform and inverse_transform --- ot/da.py | 29 +++++++++++++++++++++++------ test/test_da.py | 32 ++++++++++++++++---------------- 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/ot/da.py b/ot/da.py index 92a8f12..87d056d 100644 --- a/ot/da.py +++ b/ot/da.py @@ -1167,9 +1167,18 @@ class BaseTransport(BaseEstimator): transp_Xs = np.dot(transp, self.Xt) else: # perform out of sample mapping - print("Warning: out of sample mapping not yet implemented") - print("input data will be returned") - transp_Xs = Xs + + # get the nearest neighbor in the source domain + D0 = dist(Xs, 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 - self.Xs[idx, :] return transp_Xs @@ -1202,9 +1211,17 @@ class BaseTransport(BaseEstimator): transp_Xt = np.dot(transp_, self.Xs) else: # perform out of sample mapping - print("Warning: out of sample mapping not yet implemented") - print("input data will be returned") - transp_Xt = Xt + + D0 = dist(Xt, 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 - self.Xt[idx, :] return transp_Xt diff --git a/test/test_da.py b/test/test_da.py index ecd2a3a..aed9f61 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -45,8 +45,8 @@ def test_sinkhorn_lpl1_transport_class(): Xs_new, _ = get_data_classif('3gauss', ns + 1) transp_Xs_new = clf.transform(Xs_new) - # check that the oos method is not working - assert_equal(transp_Xs_new, Xs_new) + # check that the oos method is working + assert_equal(transp_Xs_new.shape, Xs_new.shape) # test inverse transform transp_Xt = clf.inverse_transform(Xt=Xt) @@ -55,8 +55,8 @@ def test_sinkhorn_lpl1_transport_class(): Xt_new, _ = get_data_classif('3gauss2', nt + 1) transp_Xt_new = clf.inverse_transform(Xt=Xt_new) - # check that the oos method is not working and returns the input data - assert_equal(transp_Xt_new, Xt_new) + # check that the oos method is working + assert_equal(transp_Xt_new.shape, Xt_new.shape) # test fit_transform transp_Xs = clf.fit_transform(Xs=Xs, ys=ys, Xt=Xt) @@ -108,8 +108,8 @@ def test_sinkhorn_l1l2_transport_class(): Xs_new, _ = get_data_classif('3gauss', ns + 1) transp_Xs_new = clf.transform(Xs_new) - # check that the oos method is not working - assert_equal(transp_Xs_new, Xs_new) + # check that the oos method is working + assert_equal(transp_Xs_new.shape, Xs_new.shape) # test inverse transform transp_Xt = clf.inverse_transform(Xt=Xt) @@ -118,8 +118,8 @@ def test_sinkhorn_l1l2_transport_class(): Xt_new, _ = get_data_classif('3gauss2', nt + 1) transp_Xt_new = clf.inverse_transform(Xt=Xt_new) - # check that the oos method is not working and returns the input data - assert_equal(transp_Xt_new, Xt_new) + # check that the oos method is working + assert_equal(transp_Xt_new.shape, Xt_new.shape) # test fit_transform transp_Xs = clf.fit_transform(Xs=Xs, ys=ys, Xt=Xt) @@ -171,8 +171,8 @@ def test_sinkhorn_transport_class(): Xs_new, _ = get_data_classif('3gauss', ns + 1) transp_Xs_new = clf.transform(Xs_new) - # check that the oos method is not working - assert_equal(transp_Xs_new, Xs_new) + # check that the oos method is working + assert_equal(transp_Xs_new.shape, Xs_new.shape) # test inverse transform transp_Xt = clf.inverse_transform(Xt=Xt) @@ -181,8 +181,8 @@ def test_sinkhorn_transport_class(): Xt_new, _ = get_data_classif('3gauss2', nt + 1) transp_Xt_new = clf.inverse_transform(Xt=Xt_new) - # check that the oos method is not working and returns the input data - assert_equal(transp_Xt_new, Xt_new) + # check that the oos method is working + assert_equal(transp_Xt_new.shape, Xt_new.shape) # test fit_transform transp_Xs = clf.fit_transform(Xs=Xs, Xt=Xt) @@ -234,8 +234,8 @@ def test_emd_transport_class(): Xs_new, _ = get_data_classif('3gauss', ns + 1) transp_Xs_new = clf.transform(Xs_new) - # check that the oos method is not working - assert_equal(transp_Xs_new, Xs_new) + # check that the oos method is working + assert_equal(transp_Xs_new.shape, Xs_new.shape) # test inverse transform transp_Xt = clf.inverse_transform(Xt=Xt) @@ -244,8 +244,8 @@ def test_emd_transport_class(): Xt_new, _ = get_data_classif('3gauss2', nt + 1) transp_Xt_new = clf.inverse_transform(Xt=Xt_new) - # check that the oos method is not working and returns the input data - assert_equal(transp_Xt_new, Xt_new) + # check that the oos method is working + assert_equal(transp_Xt_new.shape, Xt_new.shape) # test fit_transform transp_Xs = clf.fit_transform(Xs=Xs, Xt=Xt) -- cgit v1.2.3 From 117cd337d54625e492162a44e37cc18bedef990e Mon Sep 17 00:00:00 2001 From: Slasnista Date: Fri, 4 Aug 2017 15:49:42 +0200 Subject: added new class MappingTransport to support linear and kernel mapping, not yet tested --- ot/da.py | 158 +++++++++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 134 insertions(+), 24 deletions(-) diff --git a/ot/da.py b/ot/da.py index 87d056d..0616d17 100644 --- a/ot/da.py +++ b/ot/da.py @@ -1233,12 +1233,6 @@ class SinkhornTransport(BaseTransport): ---------- reg_e : float, optional (default=1) Entropic regularization parameter - mode : string, optional (default="unsupervised") - The DA mode. If "unsupervised" no target labels are taken into account - to modify the cost matrix. If "semisupervised" the target labels - are taken into account to set coefficients of the pairwise distance - matrix to 0 for row and columns indices that correspond to source and - target samples which share the same labels. max_iter : int, float, optional (default=1000) The minimum number of iteration before stopping the optimization algorithm if no it has not converged @@ -1324,12 +1318,6 @@ class EMDTransport(BaseTransport): """Domain Adapatation OT method based on Earth Mover's Distance Parameters ---------- - mode : string, optional (default="unsupervised") - The DA mode. If "unsupervised" no target labels are taken into account - to modify the cost matrix. If "semisupervised" the target labels - are taken into account to set coefficients of the pairwise distance - matrix to 0 for row and columns indices that correspond to source and - target samples which share the same labels. mapping : string, optional (default="barycentric") The kind of mapping to apply to transport samples from a domain into another one. @@ -1406,12 +1394,6 @@ class SinkhornLpl1Transport(BaseTransport): Entropic regularization parameter reg_cl : float, optional (default=0.1) Class regularization parameter - mode : string, optional (default="unsupervised") - The DA mode. If "unsupervised" no target labels are taken into account - to modify the cost matrix. If "semisupervised" the target labels - are taken into account to set coefficients of the pairwise distance - matrix to 0 for row and columns indices that correspond to source and - target samples which share the same labels. mapping : string, optional (default="barycentric") The kind of mapping to apply to transport samples from a domain into another one. @@ -1510,12 +1492,6 @@ class SinkhornL1l2Transport(BaseTransport): Entropic regularization parameter reg_cl : float, optional (default=0.1) Class regularization parameter - mode : string, optional (default="unsupervised") - The DA mode. If "unsupervised" no target labels are taken into account - to modify the cost matrix. If "semisupervised" the target labels - are taken into account to set coefficients of the pairwise distance - matrix to 0 for row and columns indices that correspond to source and - target samples which share the same labels. mapping : string, optional (default="barycentric") The kind of mapping to apply to transport samples from a domain into another one. @@ -1603,3 +1579,137 @@ class SinkhornL1l2Transport(BaseTransport): verbose=self.verbose, log=self.log) 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 + 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) + verbose : bool, optional (default=False) + Print information along iterations + log : bool, optional (default=False) + record log if True + + Attributes + ---------- + Coupling_ : the optimal coupling + Mapping_ : the mapping associated + + 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", + kernel="linear", sigma=1, max_iter=100, tol=1e-5, + max_inner_iter=10, inner_tol=1e-6, log=False, verbose=False): + + self.metric = metric + self.mu = mu + self.eta = eta + self.bias = bias + self.kernel = kernel + self.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 + + 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 of shape = (n_source_samples, n_features) + The training input samples. + ys : array-like, shape = (n_source_samples,) + The class labels + Xt : array-like of shape = (n_target_samples, n_features) + The training input samples. + yt : array-like, shape = (n_labeled_target_samples,) + The class labels + Returns + ------- + self : object + Returns self. + """ + + self.Xs = Xs + self.Xt = Xt + + if self.kernel == "linear": + self.Coupling_, self.Mapping_ = 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": + self.Coupling_, self.Mapping_ = 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) + + return self + + def transform(self, Xs): + """Transports source samples Xs onto target ones Xt + Parameters + ---------- + Xs : array-like of shape = (n_source_samples, n_features) + The training input samples. + + Returns + ------- + transp_Xs : array-like of shape = (n_source_samples, n_features) + The transport source samples. + """ + + 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 -- cgit v1.2.3 From d20a067f91dcca318e2841ac52a8c578c78b89b2 Mon Sep 17 00:00:00 2001 From: Slasnista Date: Wed, 23 Aug 2017 11:45:06 +0200 Subject: make doc strings compliant with numpy / modif according to AG review --- ot/da.py | 139 +++++++++++++++++++++++++++++++++----------------------- test/test_da.py | 13 ++++-- 2 files changed, 93 insertions(+), 59 deletions(-) diff --git a/ot/da.py b/ot/da.py index 0616d17..044d567 100644 --- a/ot/da.py +++ b/ot/da.py @@ -967,11 +967,13 @@ class BaseEstimator(object): def get_params(self, deep=True): """Get parameters for this estimator. + Parameters ---------- deep : boolean, optional If True, will return the parameters for this estimator and contained subobjects that are estimators. + Returns ------- params : mapping of string to any @@ -1002,10 +1004,12 @@ class BaseEstimator(object): def set_params(self, **params): """Set the parameters of this estimator. + The method works on simple estimators as well as on nested objects (such as pipelines). The latter have parameters of the form ``__`` so that it's possible to update each component of a nested object. + Returns ------- self @@ -1053,11 +1057,12 @@ def distribution_estimation_uniform(X): Parameters ---------- - X : array-like of shape = (n_samples, n_features) + X : array-like, shape (n_samples, n_features) The array of samples + Returns ------- - mu : array-like, shape = (n_samples,) + mu : array-like, shape (n_samples,) The uniform distribution estimated from X """ @@ -1069,16 +1074,18 @@ class BaseTransport(BaseEstimator): 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 of shape = (n_source_samples, n_features) + Xs : array-like, shape (n_source_samples, n_features) The training input samples. - ys : array-like, shape = (n_source_samples,) + ys : array-like, shape (n_source_samples,) The class labels - Xt : array-like of shape = (n_target_samples, n_features) + Xt : array-like, shape (n_target_samples, n_features) The training input samples. - yt : array-like, shape = (n_labeled_target_samples,) + yt : array-like, shape (n_labeled_target_samples,) The class labels + Returns ------- self : object @@ -1086,12 +1093,12 @@ class BaseTransport(BaseEstimator): """ # pairwise distance - self.Cost = dist(Xs, Xt, metric=self.metric) + self.cost_ = dist(Xs, Xt, metric=self.metric) 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) + 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 @@ -1104,7 +1111,7 @@ class BaseTransport(BaseEstimator): # 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 + self.cost_[idx_s[0], j] = self.limit_max # distribution estimation self.mu_s = self.distribution_estimation(Xs) @@ -1120,19 +1127,21 @@ class BaseTransport(BaseEstimator): """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 of shape = (n_source_samples, n_features) + Xs : array-like, shape (n_source_samples, n_features) The training input samples. - ys : array-like, shape = (n_source_samples,) + ys : array-like, shape (n_source_samples,) The class labels - Xt : array-like of shape = (n_target_samples, n_features) + Xt : array-like, shape (n_target_samples, n_features) The training input samples. - yt : array-like, shape = (n_labeled_target_samples,) + yt : array-like, shape (n_labeled_target_samples,) The class labels + Returns ------- - transp_Xs : array-like of shape = (n_source_samples, n_features) + transp_Xs : array-like, shape (n_source_samples, n_features) The source samples samples. """ @@ -1140,25 +1149,27 @@ class BaseTransport(BaseEstimator): def transform(self, Xs=None, ys=None, Xt=None, yt=None): """Transports source samples Xs onto target ones Xt + Parameters ---------- - Xs : array-like of shape = (n_source_samples, n_features) + Xs : array-like, shape (n_source_samples, n_features) The training input samples. - ys : array-like, shape = (n_source_samples,) + ys : array-like, shape (n_source_samples,) The class labels - Xt : array-like of shape = (n_target_samples, n_features) + Xt : array-like, shape (n_target_samples, n_features) The training input samples. - yt : array-like, shape = (n_labeled_target_samples,) + yt : array-like, shape (n_labeled_target_samples,) The class labels + Returns ------- - transp_Xs : array-like of shape = (n_source_samples, n_features) + transp_Xs : array-like, shape (n_source_samples, n_features) The transport source samples. """ if np.array_equal(self.Xs, Xs): # perform standard barycentric mapping - transp = self.Coupling_ / np.sum(self.Coupling_, 1)[:, None] + transp = self.coupling_ / np.sum(self.coupling_, 1)[:, None] # set nans to 0 transp[~ np.isfinite(transp)] = 0 @@ -1173,7 +1184,7 @@ class BaseTransport(BaseEstimator): idx = np.argmin(D0, axis=1) # transport the source samples - transp = self.Coupling_ / np.sum(self.Coupling_, 1)[:, None] + transp = self.coupling_ / np.sum(self.coupling_, 1)[:, None] transp[~ np.isfinite(transp)] = 0 transp_Xs_ = np.dot(transp, self.Xt) @@ -1184,25 +1195,27 @@ class BaseTransport(BaseEstimator): def inverse_transform(self, Xs=None, ys=None, Xt=None, yt=None): """Transports target samples Xt onto target samples Xs + Parameters ---------- - Xs : array-like of shape = (n_source_samples, n_features) + 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 of shape = (n_target_samples, n_features) + Xt : array-like, shape (n_target_samples, n_features) The training input samples. yt : array-like, shape = (n_labeled_target_samples,) The class labels + Returns ------- - transp_Xt : array-like of shape = (n_source_samples, n_features) + transp_Xt : array-like, shape (n_source_samples, n_features) The transported target samples. """ if np.array_equal(self.Xt, Xt): # perform standard barycentric mapping - transp_ = self.Coupling_.T / np.sum(self.Coupling_, 0)[:, None] + transp_ = self.coupling_.T / np.sum(self.coupling_, 0)[:, None] # set nans to 0 transp_[~ np.isfinite(transp_)] = 0 @@ -1216,7 +1229,7 @@ class BaseTransport(BaseEstimator): idx = np.argmin(D0, axis=1) # transport the target samples - transp_ = self.Coupling_.T / np.sum(self.Coupling_, 0)[:, None] + transp_ = self.coupling_.T / np.sum(self.coupling_, 0)[:, None] transp_[~ np.isfinite(transp_)] = 0 transp_Xt_ = np.dot(transp_, self.Xs) @@ -1254,9 +1267,10 @@ class SinkhornTransport(BaseTransport): 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 infinite cost + Attributes ---------- - Coupling_ : the optimal coupling + coupling_ : the optimal coupling References ---------- @@ -1287,16 +1301,18 @@ class SinkhornTransport(BaseTransport): 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 of shape = (n_source_samples, n_features) + 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 of shape = (n_target_samples, n_features) + Xt : array-like, shape (n_target_samples, n_features) The training input samples. yt : array-like, shape = (n_labeled_target_samples,) The class labels + Returns ------- self : object @@ -1306,8 +1322,8 @@ class SinkhornTransport(BaseTransport): super(SinkhornTransport, self).fit(Xs, ys, Xt, yt) # coupling estimation - self.Coupling_ = sinkhorn( - a=self.mu_s, b=self.mu_t, M=self.Cost, reg=self.reg_e, + self.coupling_ = 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) @@ -1316,6 +1332,7 @@ class SinkhornTransport(BaseTransport): class EMDTransport(BaseTransport): """Domain Adapatation OT method based on Earth Mover's Distance + Parameters ---------- mapping : string, optional (default="barycentric") @@ -1335,9 +1352,10 @@ class EMDTransport(BaseTransport): 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_ : the optimal coupling + coupling_ : the optimal coupling References ---------- @@ -1358,16 +1376,18 @@ class EMDTransport(BaseTransport): 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 of shape = (n_source_samples, n_features) + 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 of shape = (n_target_samples, n_features) + Xt : array-like, shape (n_target_samples, n_features) The training input samples. yt : array-like, shape = (n_labeled_target_samples,) The class labels + Returns ------- self : object @@ -1377,8 +1397,8 @@ class EMDTransport(BaseTransport): super(EMDTransport, self).fit(Xs, ys, Xt, yt) # coupling estimation - self.Coupling_ = emd( - a=self.mu_s, b=self.mu_t, M=self.Cost, + self.coupling_ = emd( + a=self.mu_s, b=self.mu_t, M=self.cost_, ) return self @@ -1418,7 +1438,7 @@ class SinkhornLpl1Transport(BaseTransport): Attributes ---------- - Coupling_ : the optimal coupling + coupling_ : the optimal coupling References ---------- @@ -1455,16 +1475,18 @@ class SinkhornLpl1Transport(BaseTransport): 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 of shape = (n_source_samples, n_features) + 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 of shape = (n_target_samples, n_features) + Xt : array-like, shape (n_target_samples, n_features) The training input samples. yt : array-like, shape = (n_labeled_target_samples,) The class labels + Returns ------- self : object @@ -1473,8 +1495,8 @@ class SinkhornLpl1Transport(BaseTransport): super(SinkhornLpl1Transport, self).fit(Xs, ys, Xt, yt) - self.Coupling_ = sinkhorn_lpl1_mm( - a=self.mu_s, labels_a=ys, b=self.mu_t, M=self.Cost, + self.coupling_ = 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) @@ -1517,7 +1539,7 @@ class SinkhornL1l2Transport(BaseTransport): Attributes ---------- - Coupling_ : the optimal coupling + coupling_ : the optimal coupling References ---------- @@ -1554,16 +1576,18 @@ class SinkhornL1l2Transport(BaseTransport): 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 of shape = (n_source_samples, n_features) + 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 of shape = (n_target_samples, n_features) + Xt : array-like, shape (n_target_samples, n_features) The training input samples. yt : array-like, shape = (n_labeled_target_samples,) The class labels + Returns ------- self : object @@ -1572,8 +1596,8 @@ class SinkhornL1l2Transport(BaseTransport): super(SinkhornL1l2Transport, self).fit(Xs, ys, Xt, yt) - self.Coupling_ = sinkhorn_l1l2_gl( - a=self.mu_s, labels_a=ys, b=self.mu_t, M=self.Cost, + self.coupling_ = 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) @@ -1614,8 +1638,8 @@ class MappingTransport(BaseEstimator): Attributes ---------- - Coupling_ : the optimal coupling - Mapping_ : the mapping associated + coupling_ : the optimal coupling + mapping_ : the mapping associated References ---------- @@ -1646,16 +1670,18 @@ class MappingTransport(BaseEstimator): 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 of shape = (n_source_samples, n_features) + 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 of shape = (n_target_samples, n_features) + Xt : array-like, shape (n_target_samples, n_features) The training input samples. yt : array-like, shape = (n_labeled_target_samples,) The class labels + Returns ------- self : object @@ -1666,14 +1692,14 @@ class MappingTransport(BaseEstimator): self.Xt = Xt if self.kernel == "linear": - self.Coupling_, self.Mapping_ = joint_OT_mapping_linear( + self.coupling_, self.mapping_ = 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": - self.Coupling_, self.Mapping_ = joint_OT_mapping_kernel( + self.coupling_, self.mapping_ = 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, @@ -1683,20 +1709,21 @@ class MappingTransport(BaseEstimator): def transform(self, Xs): """Transports source samples Xs onto target ones Xt + Parameters ---------- - Xs : array-like of shape = (n_source_samples, n_features) + Xs : array-like, shape (n_source_samples, n_features) The training input samples. Returns ------- - transp_Xs : array-like of shape = (n_source_samples, n_features) + transp_Xs : array-like, shape (n_source_samples, n_features) The transport source samples. """ if np.array_equal(self.Xs, Xs): # perform standard barycentric mapping - transp = self.Coupling_ / np.sum(self.Coupling_, 1)[:, None] + transp = self.coupling_ / np.sum(self.coupling_, 1)[:, None] # set nans to 0 transp[~ np.isfinite(transp)] = 0 @@ -1710,6 +1737,6 @@ class MappingTransport(BaseEstimator): K = Xs if self.bias: K = np.hstack((K, np.ones((Xs.shape[0], 1)))) - transp_Xs = K.dot(self.Mapping_) + transp_Xs = K.dot(self.mapping_) return transp_Xs diff --git a/test/test_da.py b/test/test_da.py index aed9f61..93f7e83 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -5,13 +5,12 @@ # License: MIT License import numpy as np -import ot from numpy.testing.utils import assert_allclose, assert_equal + +import ot from ot.datasets import get_data_classif from ot.utils import unif -np.random.seed(42) - def test_sinkhorn_lpl1_transport_class(): """test_sinkhorn_transport @@ -325,3 +324,11 @@ def test_otda(): da_emd = ot.da.OTDA_mapping_kernel() # init class da_emd.fit(xs, xt, numItermax=10) # fit distributions da_emd.predict(xs) # interpolation of source samples + + +if __name__ == "__main__": + + test_sinkhorn_transport_class() + test_emd_transport_class() + test_sinkhorn_l1l2_transport_class() + test_sinkhorn_lpl1_transport_class() -- cgit v1.2.3 From 8d19d365446efc00d8443c6ddb5b93fded3fa5ab Mon Sep 17 00:00:00 2001 From: Slasnista Date: Wed, 23 Aug 2017 13:50:24 +0200 Subject: out of samples transform and inverse transform by batch --- ot/da.py | 89 +++++++++++++++++++++++++++++++++++++-------------------- test/test_da.py | 66 +++++++++++++++++++++--------------------- 2 files changed, 91 insertions(+), 64 deletions(-) diff --git a/ot/da.py b/ot/da.py index 044d567..0c83ae6 100644 --- a/ot/da.py +++ b/ot/da.py @@ -1147,7 +1147,7 @@ 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): + def transform(self, Xs=None, ys=None, Xt=None, yt=None, batch_size=128): """Transports source samples Xs onto target ones Xt Parameters @@ -1160,6 +1160,8 @@ class BaseTransport(BaseEstimator): The training input samples. yt : array-like, shape (n_labeled_target_samples,) The class labels + batch_size : int, optional (default=128) + The batch size for out of sample inverse transform Returns ------- @@ -1178,34 +1180,48 @@ class BaseTransport(BaseEstimator): 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)] - # get the nearest neighbor in the source domain - D0 = dist(Xs, self.Xs) - idx = np.argmin(D0, axis=1) + transp_Xs = [] + for bi in batch_ind: - # 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) + # 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 - self.Xs[idx, :] + # 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): + 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,) + 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_labeled_target_samples,) + yt : array-like, shape (n_labeled_target_samples,) The class labels + batch_size : int, optional (default=128) + The batch size for out of sample inverse transform Returns ------- @@ -1224,17 +1240,28 @@ class BaseTransport(BaseEstimator): 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)] - D0 = dist(Xt, self.Xt) - idx = np.argmin(D0, axis=1) + transp_Xt = [] + for bi in batch_ind: - # 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) + 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, :] - # define the transported points - transp_Xt = transp_Xt_[idx, :] + Xt - self.Xt[idx, :] + transp_Xt.append(transp_Xt_) + + transp_Xt = np.concatenate(transp_Xt, axis=0) return transp_Xt @@ -1306,11 +1333,11 @@ class SinkhornTransport(BaseTransport): ---------- Xs : array-like, shape (n_source_samples, n_features) The training input samples. - ys : array-like, shape = (n_source_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_labeled_target_samples,) + yt : array-like, shape (n_labeled_target_samples,) The class labels Returns @@ -1381,11 +1408,11 @@ class EMDTransport(BaseTransport): ---------- Xs : array-like, shape (n_source_samples, n_features) The training input samples. - ys : array-like, shape = (n_source_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_labeled_target_samples,) + yt : array-like, shape (n_labeled_target_samples,) The class labels Returns @@ -1480,11 +1507,11 @@ class SinkhornLpl1Transport(BaseTransport): ---------- Xs : array-like, shape (n_source_samples, n_features) The training input samples. - ys : array-like, shape = (n_source_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_labeled_target_samples,) + yt : array-like, shape (n_labeled_target_samples,) The class labels Returns @@ -1581,11 +1608,11 @@ class SinkhornL1l2Transport(BaseTransport): ---------- Xs : array-like, shape (n_source_samples, n_features) The training input samples. - ys : array-like, shape = (n_source_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_labeled_target_samples,) + yt : array-like, shape (n_labeled_target_samples,) The class labels Returns @@ -1675,11 +1702,11 @@ class MappingTransport(BaseEstimator): ---------- Xs : array-like, shape (n_source_samples, n_features) The training input samples. - ys : array-like, shape = (n_source_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_labeled_target_samples,) + yt : array-like, shape (n_labeled_target_samples,) The class labels Returns diff --git a/test/test_da.py b/test/test_da.py index 93f7e83..196f4c4 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -28,14 +28,14 @@ def test_sinkhorn_lpl1_transport_class(): clf.fit(Xs=Xs, ys=ys, Xt=Xt) # test dimensions of coupling - assert_equal(clf.Cost.shape, ((Xs.shape[0], Xt.shape[0]))) - assert_equal(clf.Coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(clf.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(clf.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) # test margin constraints mu_s = unif(ns) mu_t = unif(nt) - assert_allclose(np.sum(clf.Coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) - assert_allclose(np.sum(clf.Coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + assert_allclose(np.sum(clf.coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) + assert_allclose(np.sum(clf.coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) # test transform transp_Xs = clf.transform(Xs=Xs) @@ -64,13 +64,13 @@ def test_sinkhorn_lpl1_transport_class(): # test semi supervised mode clf = ot.da.SinkhornLpl1Transport() clf.fit(Xs=Xs, ys=ys, Xt=Xt) - n_unsup = np.sum(clf.Cost) + n_unsup = np.sum(clf.cost_) # test semi supervised mode clf = ot.da.SinkhornLpl1Transport() clf.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) - assert_equal(clf.Cost.shape, ((Xs.shape[0], Xt.shape[0]))) - n_semisup = np.sum(clf.Cost) + assert_equal(clf.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) + n_semisup = np.sum(clf.cost_) assert n_unsup != n_semisup, "semisupervised mode not working" @@ -91,14 +91,14 @@ def test_sinkhorn_l1l2_transport_class(): clf.fit(Xs=Xs, ys=ys, Xt=Xt) # test dimensions of coupling - assert_equal(clf.Cost.shape, ((Xs.shape[0], Xt.shape[0]))) - assert_equal(clf.Coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(clf.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(clf.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) # test margin constraints mu_s = unif(ns) mu_t = unif(nt) - assert_allclose(np.sum(clf.Coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) - assert_allclose(np.sum(clf.Coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + assert_allclose(np.sum(clf.coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) + assert_allclose(np.sum(clf.coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) # test transform transp_Xs = clf.transform(Xs=Xs) @@ -127,13 +127,13 @@ def test_sinkhorn_l1l2_transport_class(): # test semi supervised mode clf = ot.da.SinkhornL1l2Transport() clf.fit(Xs=Xs, ys=ys, Xt=Xt) - n_unsup = np.sum(clf.Cost) + n_unsup = np.sum(clf.cost_) # test semi supervised mode clf = ot.da.SinkhornL1l2Transport() clf.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) - assert_equal(clf.Cost.shape, ((Xs.shape[0], Xt.shape[0]))) - n_semisup = np.sum(clf.Cost) + assert_equal(clf.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) + n_semisup = np.sum(clf.cost_) assert n_unsup != n_semisup, "semisupervised mode not working" @@ -154,14 +154,14 @@ def test_sinkhorn_transport_class(): clf.fit(Xs=Xs, Xt=Xt) # test dimensions of coupling - assert_equal(clf.Cost.shape, ((Xs.shape[0], Xt.shape[0]))) - assert_equal(clf.Coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(clf.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(clf.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) # test margin constraints mu_s = unif(ns) mu_t = unif(nt) - assert_allclose(np.sum(clf.Coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) - assert_allclose(np.sum(clf.Coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + assert_allclose(np.sum(clf.coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) + assert_allclose(np.sum(clf.coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) # test transform transp_Xs = clf.transform(Xs=Xs) @@ -190,13 +190,13 @@ def test_sinkhorn_transport_class(): # test semi supervised mode clf = ot.da.SinkhornTransport() clf.fit(Xs=Xs, Xt=Xt) - n_unsup = np.sum(clf.Cost) + n_unsup = np.sum(clf.cost_) # test semi supervised mode clf = ot.da.SinkhornTransport() clf.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) - assert_equal(clf.Cost.shape, ((Xs.shape[0], Xt.shape[0]))) - n_semisup = np.sum(clf.Cost) + assert_equal(clf.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) + n_semisup = np.sum(clf.cost_) assert n_unsup != n_semisup, "semisupervised mode not working" @@ -217,14 +217,14 @@ def test_emd_transport_class(): clf.fit(Xs=Xs, Xt=Xt) # test dimensions of coupling - assert_equal(clf.Cost.shape, ((Xs.shape[0], Xt.shape[0]))) - assert_equal(clf.Coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(clf.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(clf.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) # test margin constraints mu_s = unif(ns) mu_t = unif(nt) - assert_allclose(np.sum(clf.Coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) - assert_allclose(np.sum(clf.Coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + assert_allclose(np.sum(clf.coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) + assert_allclose(np.sum(clf.coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) # test transform transp_Xs = clf.transform(Xs=Xs) @@ -253,13 +253,13 @@ def test_emd_transport_class(): # test semi supervised mode clf = ot.da.EMDTransport() clf.fit(Xs=Xs, Xt=Xt) - n_unsup = np.sum(clf.Cost) + n_unsup = np.sum(clf.cost_) # test semi supervised mode clf = ot.da.EMDTransport() clf.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) - assert_equal(clf.Cost.shape, ((Xs.shape[0], Xt.shape[0]))) - n_semisup = np.sum(clf.Cost) + assert_equal(clf.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) + n_semisup = np.sum(clf.cost_) assert n_unsup != n_semisup, "semisupervised mode not working" @@ -326,9 +326,9 @@ def test_otda(): da_emd.predict(xs) # interpolation of source samples -if __name__ == "__main__": +# if __name__ == "__main__": - test_sinkhorn_transport_class() - test_emd_transport_class() - test_sinkhorn_l1l2_transport_class() - test_sinkhorn_lpl1_transport_class() +# test_sinkhorn_transport_class() +# test_emd_transport_class() +# test_sinkhorn_l1l2_transport_class() +# test_sinkhorn_lpl1_transport_class() -- cgit v1.2.3 From c8ae5843ae64dbf841deb3ad8c10024a94a93eec Mon Sep 17 00:00:00 2001 From: Slasnista Date: Wed, 23 Aug 2017 14:11:13 +0200 Subject: test functions for MappingTransport Class --- ot/da.py | 18 ++++++--- test/test_da.py | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 125 insertions(+), 10 deletions(-) diff --git a/ot/da.py b/ot/da.py index 0c83ae6..3ccb1b3 100644 --- a/ot/da.py +++ b/ot/da.py @@ -1665,8 +1665,14 @@ class MappingTransport(BaseEstimator): Attributes ---------- - coupling_ : the optimal coupling - mapping_ : the mapping associated + coupling_ : array-like, shape (n_source_samples, n_features) + 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 References ---------- @@ -1679,20 +1685,22 @@ class MappingTransport(BaseEstimator): def __init__(self, mu=1, eta=0.001, bias=False, metric="sqeuclidean", kernel="linear", sigma=1, max_iter=100, tol=1e-5, - max_inner_iter=10, inner_tol=1e-6, log=False, verbose=False): + max_inner_iter=10, inner_tol=1e-6, log=False, verbose=False, + verbose2=False): self.metric = metric self.mu = mu self.eta = eta self.bias = bias self.kernel = kernel - self.sigma + 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 @@ -1712,7 +1720,7 @@ class MappingTransport(BaseEstimator): Returns ------- self : object - Returns self. + Returns self """ self.Xs = Xs diff --git a/test/test_da.py b/test/test_da.py index 196f4c4..162f681 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -264,6 +264,112 @@ def test_emd_transport_class(): assert n_unsup != n_semisup, "semisupervised mode not working" +def test_mapping_transport_class(): + """test_mapping_transport + """ + + ns = 150 + nt = 200 + + Xs, ys = get_data_classif('3gauss', ns) + Xt, yt = get_data_classif('3gauss2', nt) + Xs_new, _ = get_data_classif('3gauss', ns + 1) + + ########################################################################## + # kernel == linear mapping tests + ########################################################################## + + # check computation and dimensions if bias == False + clf = ot.da.MappingTransport(kernel="linear", bias=False) + clf.fit(Xs=Xs, Xt=Xt) + + assert_equal(clf.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(clf.mapping_.shape, ((Xs.shape[1], Xt.shape[1]))) + + # test margin constraints + mu_s = unif(ns) + mu_t = unif(nt) + assert_allclose(np.sum(clf.coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) + assert_allclose(np.sum(clf.coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + + # test transform + transp_Xs = clf.transform(Xs=Xs) + assert_equal(transp_Xs.shape, Xs.shape) + + transp_Xs_new = clf.transform(Xs_new) + + # check that the oos method is working + assert_equal(transp_Xs_new.shape, Xs_new.shape) + + # check computation and dimensions if bias == True + clf = ot.da.MappingTransport(kernel="linear", bias=True) + clf.fit(Xs=Xs, Xt=Xt) + assert_equal(clf.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(clf.mapping_.shape, ((Xs.shape[1] + 1, Xt.shape[1]))) + + # test margin constraints + mu_s = unif(ns) + mu_t = unif(nt) + assert_allclose(np.sum(clf.coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) + assert_allclose(np.sum(clf.coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + + # test transform + transp_Xs = clf.transform(Xs=Xs) + assert_equal(transp_Xs.shape, Xs.shape) + + transp_Xs_new = clf.transform(Xs_new) + + # check that the oos method is working + assert_equal(transp_Xs_new.shape, Xs_new.shape) + + ########################################################################## + # kernel == gaussian mapping tests + ########################################################################## + + # check computation and dimensions if bias == False + clf = ot.da.MappingTransport(kernel="gaussian", bias=False) + clf.fit(Xs=Xs, Xt=Xt) + + assert_equal(clf.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(clf.mapping_.shape, ((Xs.shape[0], Xt.shape[1]))) + + # test margin constraints + mu_s = unif(ns) + mu_t = unif(nt) + assert_allclose(np.sum(clf.coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) + assert_allclose(np.sum(clf.coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + + # test transform + transp_Xs = clf.transform(Xs=Xs) + assert_equal(transp_Xs.shape, Xs.shape) + + transp_Xs_new = clf.transform(Xs_new) + + # check that the oos method is working + assert_equal(transp_Xs_new.shape, Xs_new.shape) + + # check computation and dimensions if bias == True + clf = ot.da.MappingTransport(kernel="gaussian", bias=True) + clf.fit(Xs=Xs, Xt=Xt) + assert_equal(clf.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(clf.mapping_.shape, ((Xs.shape[0] + 1, Xt.shape[1]))) + + # test margin constraints + mu_s = unif(ns) + mu_t = unif(nt) + assert_allclose(np.sum(clf.coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) + assert_allclose(np.sum(clf.coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + + # test transform + transp_Xs = clf.transform(Xs=Xs) + assert_equal(transp_Xs.shape, Xs.shape) + + transp_Xs_new = clf.transform(Xs_new) + + # check that the oos method is working + assert_equal(transp_Xs_new.shape, Xs_new.shape) + + def test_otda(): n_samples = 150 # nb samples @@ -326,9 +432,10 @@ def test_otda(): da_emd.predict(xs) # interpolation of source samples -# if __name__ == "__main__": +if __name__ == "__main__": -# test_sinkhorn_transport_class() -# test_emd_transport_class() -# test_sinkhorn_l1l2_transport_class() -# test_sinkhorn_lpl1_transport_class() + # test_sinkhorn_transport_class() + # test_emd_transport_class() + # test_sinkhorn_l1l2_transport_class() + # test_sinkhorn_lpl1_transport_class() + test_mapping_transport_class() -- cgit v1.2.3 From fc58f39fc730a9e1bb2215ef063e37c50f0ebc1f Mon Sep 17 00:00:00 2001 From: Slasnista Date: Wed, 23 Aug 2017 15:09:08 +0200 Subject: added deprecation warning on old classes --- ot/da.py | 22 ++++++++++-- ot/deprecation.py | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/test_da.py | 5 +-- 3 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 ot/deprecation.py diff --git a/ot/da.py b/ot/da.py index 3ccb1b3..8fa1895 100644 --- a/ot/da.py +++ b/ot/da.py @@ -10,12 +10,14 @@ Domain adaptation with optimal transport # License: MIT License import numpy as np +import warnings + from .bregman import sinkhorn from .lp import emd from .utils import unif, dist, kernel from .optim import cg from .optim import gcg -import warnings +from .deprecation import deprecated def sinkhorn_lpl1_mm(a, labels_a, b, M, reg, eta=0.1, numItermax=10, @@ -632,6 +634,9 @@ def joint_OT_mapping_kernel(xs, xt, mu=1, eta=0.001, kerneltype='gaussian', return G, L +@deprecated("The class OTDA is deprecated in 0.3.1 and will be " + "removed in 0.5" + "\n\tfor standard transport use class EMDTransport instead.") class OTDA(object): """Class for domain adaptation with optimal transport as proposed in [5] @@ -758,10 +763,15 @@ class OTDA(object): self.M = np.log(1 + np.log(1 + self.M)) +@deprecated("The class OTDA_sinkhorn is deprecated in 0.3.1 and will be" + " removed in 0.5 \nUse class SinkhornTransport instead.") class OTDA_sinkhorn(OTDA): """Class for domain adaptation with optimal transport with entropic - regularization""" + regularization + + + """ def fit(self, xs, xt, reg=1, ws=None, wt=None, norm=None, **kwargs): """Fit regularized domain adaptation between samples is xs and xt @@ -783,6 +793,8 @@ class OTDA_sinkhorn(OTDA): self.computed = True +@deprecated("The class OTDA_lpl1 is deprecated in 0.3.1 and will be" + " removed in 0.5 \nUse class SinkhornLpl1Transport instead.") class OTDA_lpl1(OTDA): """Class for domain adaptation with optimal transport with entropic and @@ -810,6 +822,8 @@ class OTDA_lpl1(OTDA): self.computed = True +@deprecated("The class OTDA_l1L2 is deprecated in 0.3.1 and will be" + " removed in 0.5 \nUse class SinkhornL1l2Transport instead.") class OTDA_l1l2(OTDA): """Class for domain adaptation with optimal transport with entropic @@ -837,6 +851,8 @@ class OTDA_l1l2(OTDA): self.computed = True +@deprecated("The class OTDA_mapping_linear is deprecated in 0.3.1 and will be" + " removed in 0.5 \nUse class MappingTransport instead.") class OTDA_mapping_linear(OTDA): """Class for optimal transport with joint linear mapping estimation as in @@ -882,6 +898,8 @@ class OTDA_mapping_linear(OTDA): return None +@deprecated("The class OTDA_mapping_kernel is deprecated in 0.3.1 and will be" + " removed in 0.5 \nUse class MappingTransport instead.") class OTDA_mapping_kernel(OTDA_mapping_linear): """Class for optimal transport with joint nonlinear mapping diff --git a/ot/deprecation.py b/ot/deprecation.py new file mode 100644 index 0000000..2b16427 --- /dev/null +++ b/ot/deprecation.py @@ -0,0 +1,103 @@ +""" + deprecated class from scikit-learn package + https://github.com/scikit-learn/scikit-learn/blob/master/sklearn/utils/deprecation.py +""" + +import sys +import warnings + +__all__ = ["deprecated", ] + + +class deprecated(object): + """Decorator to mark a function or class as deprecated. + Issue a warning when the function is called/the class is instantiated and + adds a warning to the docstring. + The optional extra argument will be appended to the deprecation message + and the docstring. Note: to use this with the default value for extra, put + in an empty of parentheses: + >>> from ot.deprecation import deprecated + >>> @deprecated() + ... def some_function(): pass + + Parameters + ---------- + extra : string + to be added to the deprecation messages + """ + + # Adapted from http://wiki.python.org/moin/PythonDecoratorLibrary, + # but with many changes. + + def __init__(self, extra=''): + self.extra = extra + + def __call__(self, obj): + """Call method + Parameters + ---------- + obj : object + """ + if isinstance(obj, type): + return self._decorate_class(obj) + else: + return self._decorate_fun(obj) + + def _decorate_class(self, cls): + msg = "Class %s is deprecated" % cls.__name__ + if self.extra: + msg += "; %s" % self.extra + + # FIXME: we should probably reset __new__ for full generality + init = cls.__init__ + + def wrapped(*args, **kwargs): + warnings.warn(msg, category=DeprecationWarning) + return init(*args, **kwargs) + + cls.__init__ = wrapped + + wrapped.__name__ = '__init__' + wrapped.__doc__ = self._update_doc(init.__doc__) + wrapped.deprecated_original = init + + return cls + + def _decorate_fun(self, fun): + """Decorate function fun""" + + msg = "Function %s is deprecated" % fun.__name__ + if self.extra: + msg += "; %s" % self.extra + + def wrapped(*args, **kwargs): + warnings.warn(msg, category=DeprecationWarning) + return fun(*args, **kwargs) + + wrapped.__name__ = fun.__name__ + wrapped.__dict__ = fun.__dict__ + wrapped.__doc__ = self._update_doc(fun.__doc__) + + return wrapped + + def _update_doc(self, olddoc): + newdoc = "DEPRECATED" + if self.extra: + newdoc = "%s: %s" % (newdoc, self.extra) + if olddoc: + newdoc = "%s\n\n%s" % (newdoc, olddoc) + return newdoc + + +def _is_deprecated(func): + """Helper to check if func is wraped by our deprecated decorator""" + if sys.version_info < (3, 5): + raise NotImplementedError("This is only available for python3.5 " + "or above") + closures = getattr(func, '__closure__', []) + if closures is None: + closures = [] + is_deprecated = ('deprecated' in ''.join([c.cell_contents + for c in closures + if isinstance(c.cell_contents, str)])) + return is_deprecated diff --git a/test/test_da.py b/test/test_da.py index 162f681..9578b3d 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -432,10 +432,11 @@ def test_otda(): da_emd.predict(xs) # interpolation of source samples -if __name__ == "__main__": +# if __name__ == "__main__": + # test_otda() # test_sinkhorn_transport_class() # test_emd_transport_class() # test_sinkhorn_l1l2_transport_class() # test_sinkhorn_lpl1_transport_class() - test_mapping_transport_class() + # test_mapping_transport_class() -- cgit v1.2.3 From 6167f34a721886d4b9038a8b1746a2c8c81132ce Mon Sep 17 00:00:00 2001 From: Slasnista Date: Fri, 25 Aug 2017 10:29:41 +0200 Subject: solving log issues to avoid errors and adding further tests --- ot/da.py | 57 ++++++++++++++++++++++++++++++++++++++++++--------------- test/test_da.py | 39 +++++++++++++++++++++++++++++++++------ 2 files changed, 75 insertions(+), 21 deletions(-) diff --git a/ot/da.py b/ot/da.py index 8fa1895..5a34979 100644 --- a/ot/da.py +++ b/ot/da.py @@ -1315,7 +1315,10 @@ class SinkhornTransport(BaseTransport): Attributes ---------- - coupling_ : the optimal coupling + 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 ---------- @@ -1367,11 +1370,18 @@ class SinkhornTransport(BaseTransport): super(SinkhornTransport, self).fit(Xs, ys, Xt, yt) # coupling estimation - self.coupling_ = sinkhorn( + 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 @@ -1400,7 +1410,8 @@ class EMDTransport(BaseTransport): Attributes ---------- - coupling_ : the optimal coupling + coupling_ : array-like, shape (n_source_samples, n_target_samples) + The optimal coupling References ---------- @@ -1475,15 +1486,14 @@ class SinkhornLpl1Transport(BaseTransport): The number of iteration in the inner loop verbose : int, optional (default=0) Controls the verbosity of the optimization algorithm - log : int, optional (default=0) - Controls the logs of the optimization algorithm 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 infinite cost Attributes ---------- - coupling_ : the optimal coupling + coupling_ : array-like, shape (n_source_samples, n_target_samples) + The optimal coupling References ---------- @@ -1500,7 +1510,7 @@ class SinkhornLpl1Transport(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, + tol=10e-9, verbose=False, metric="sqeuclidean", distribution_estimation=distribution_estimation_uniform, out_of_sample_map='ferradans', limit_max=np.infty): @@ -1511,7 +1521,6 @@ class SinkhornLpl1Transport(BaseTransport): self.max_inner_iter = max_inner_iter self.tol = tol self.verbose = verbose - self.log = log self.metric = metric self.distribution_estimation = distribution_estimation self.out_of_sample_map = out_of_sample_map @@ -1544,7 +1553,7 @@ class SinkhornLpl1Transport(BaseTransport): 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) + verbose=self.verbose) return self @@ -1584,7 +1593,10 @@ class SinkhornL1l2Transport(BaseTransport): Attributes ---------- - coupling_ : the optimal coupling + 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 ---------- @@ -1641,12 +1653,19 @@ class SinkhornL1l2Transport(BaseTransport): super(SinkhornL1l2Transport, self).fit(Xs, ys, Xt, yt) - self.coupling_ = sinkhorn_l1l2_gl( + 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 @@ -1683,14 +1702,15 @@ class MappingTransport(BaseEstimator): Attributes ---------- - coupling_ : array-like, shape (n_source_samples, n_features) + 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 ---------- @@ -1745,19 +1765,26 @@ class MappingTransport(BaseEstimator): self.Xt = Xt if self.kernel == "linear": - self.coupling_, self.mapping_ = joint_OT_mapping_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": - self.coupling_, self.mapping_ = joint_OT_mapping_kernel( + 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): diff --git a/test/test_da.py b/test/test_da.py index 9578b3d..104a798 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -26,6 +26,8 @@ def test_sinkhorn_lpl1_transport_class(): # test its computed clf.fit(Xs=Xs, ys=ys, Xt=Xt) + assert hasattr(clf, "cost_") + assert hasattr(clf, "coupling_") # test dimensions of coupling assert_equal(clf.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) @@ -89,6 +91,9 @@ def test_sinkhorn_l1l2_transport_class(): # test its computed clf.fit(Xs=Xs, ys=ys, Xt=Xt) + assert hasattr(clf, "cost_") + assert hasattr(clf, "coupling_") + assert hasattr(clf, "log_") # test dimensions of coupling assert_equal(clf.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) @@ -137,6 +142,11 @@ def test_sinkhorn_l1l2_transport_class(): assert n_unsup != n_semisup, "semisupervised mode not working" + # check everything runs well with log=True + clf = ot.da.SinkhornL1l2Transport(log=True) + clf.fit(Xs=Xs, ys=ys, Xt=Xt) + assert len(clf.log_.keys()) != 0 + def test_sinkhorn_transport_class(): """test_sinkhorn_transport @@ -152,6 +162,9 @@ def test_sinkhorn_transport_class(): # test its computed clf.fit(Xs=Xs, Xt=Xt) + assert hasattr(clf, "cost_") + assert hasattr(clf, "coupling_") + assert hasattr(clf, "log_") # test dimensions of coupling assert_equal(clf.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) @@ -200,6 +213,11 @@ def test_sinkhorn_transport_class(): assert n_unsup != n_semisup, "semisupervised mode not working" + # check everything runs well with log=True + clf = ot.da.SinkhornTransport(log=True) + clf.fit(Xs=Xs, ys=ys, Xt=Xt) + assert len(clf.log_.keys()) != 0 + def test_emd_transport_class(): """test_sinkhorn_transport @@ -215,6 +233,8 @@ def test_emd_transport_class(): # test its computed clf.fit(Xs=Xs, Xt=Xt) + assert hasattr(clf, "cost_") + assert hasattr(clf, "coupling_") # test dimensions of coupling assert_equal(clf.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) @@ -282,6 +302,9 @@ def test_mapping_transport_class(): # check computation and dimensions if bias == False clf = ot.da.MappingTransport(kernel="linear", bias=False) clf.fit(Xs=Xs, Xt=Xt) + assert hasattr(clf, "coupling_") + assert hasattr(clf, "mapping_") + assert hasattr(clf, "log_") assert_equal(clf.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) assert_equal(clf.mapping_.shape, ((Xs.shape[1], Xt.shape[1]))) @@ -369,6 +392,11 @@ def test_mapping_transport_class(): # check that the oos method is working assert_equal(transp_Xs_new.shape, Xs_new.shape) + # check everything runs well with log=True + clf = ot.da.MappingTransport(kernel="gaussian", log=True) + clf.fit(Xs=Xs, Xt=Xt) + assert len(clf.log_.keys()) != 0 + def test_otda(): @@ -434,9 +462,8 @@ def test_otda(): # if __name__ == "__main__": - # test_otda() - # test_sinkhorn_transport_class() - # test_emd_transport_class() - # test_sinkhorn_l1l2_transport_class() - # test_sinkhorn_lpl1_transport_class() - # test_mapping_transport_class() +# test_sinkhorn_transport_class() +# test_emd_transport_class() +# test_sinkhorn_l1l2_transport_class() +# test_sinkhorn_lpl1_transport_class() +# test_mapping_transport_class() -- cgit v1.2.3 From 181fcd3275e378668b4bb35e3584c5b245fbe896 Mon Sep 17 00:00:00 2001 From: Slasnista Date: Fri, 25 Aug 2017 15:12:20 +0200 Subject: refactoring examples according to new DA classes --- examples/da/plot_otda_classes.py | 142 +++++++++++++++++++++ examples/da/plot_otda_color_images.py | 151 ++++++++++++++++++++++ examples/da/plot_otda_d2.py | 163 ++++++++++++++++++++++++ examples/da/plot_otda_mapping.py | 119 +++++++++++++++++ examples/da/plot_otda_mapping_colors_images.py | 169 +++++++++++++++++++++++++ 5 files changed, 744 insertions(+) create mode 100644 examples/da/plot_otda_classes.py create mode 100644 examples/da/plot_otda_color_images.py create mode 100644 examples/da/plot_otda_d2.py create mode 100644 examples/da/plot_otda_mapping.py create mode 100644 examples/da/plot_otda_mapping_colors_images.py diff --git a/examples/da/plot_otda_classes.py b/examples/da/plot_otda_classes.py new file mode 100644 index 0000000..1bfe2bb --- /dev/null +++ b/examples/da/plot_otda_classes.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +""" +======================== +OT for domain adaptation +======================== + +This example introduces a domain adaptation in a 2D setting and the 4 OTDA +approaches currently supported in POT. + +""" + +# Authors: Remi Flamary +# Stanilslas Chambon +# +# License: MIT License + +import matplotlib.pylab as pl +import ot + + +# number of source and target points to generate +ns = 150 +nt = 150 + +Xs, ys = ot.datasets.get_data_classif('3gauss', ns) +Xt, yt = ot.datasets.get_data_classif('3gauss2', nt) + +# 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=1e-1) +ot_sinkhorn.fit(Xs=Xs, Xt=Xt) + +# Sinkhorn Transport with Group lasso regularization +ot_lpl1 = ot.da.SinkhornLpl1Transport(reg_e=1e-1, reg_cl=1e0) +ot_lpl1.fit(Xs=Xs, ys=ys, Xt=Xt) + +# Sinkhorn Transport with Group lasso regularization l1l2 +ot_l1l2 = ot.da.SinkhornL1l2Transport(reg_e=1e-1, reg_cl=2e0, max_iter=20, + verbose=True) +ot_l1l2.fit(Xs=Xs, ys=ys, 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_lpl1 = ot_lpl1.transform(Xs=Xs) +transp_Xs_l1l2 = ot_l1l2.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', 'cmap': 'spectral'} + +pl.figure(2, figsize=(15, 8)) +pl.subplot(2, 4, 1) +pl.imshow(ot_emd.coupling_, **param_img) +pl.xticks([]) +pl.yticks([]) +pl.title('Optimal coupling\nEMDTransport') + +pl.subplot(2, 4, 2) +pl.imshow(ot_sinkhorn.coupling_, **param_img) +pl.xticks([]) +pl.yticks([]) +pl.title('Optimal coupling\nSinkhornTransport') + +pl.subplot(2, 4, 3) +pl.imshow(ot_lpl1.coupling_, **param_img) +pl.xticks([]) +pl.yticks([]) +pl.title('Optimal coupling\nSinkhornLpl1Transport') + +pl.subplot(2, 4, 4) +pl.imshow(ot_l1l2.coupling_, **param_img) +pl.xticks([]) +pl.yticks([]) +pl.title('Optimal coupling\nSinkhornL1l2Transport') + +pl.subplot(2, 4, 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, 4, 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, 4, 7) +pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', + label='Target samples', alpha=0.3) +pl.scatter(transp_Xs_lpl1[:, 0], transp_Xs_lpl1[:, 1], c=ys, + marker='+', label='Transp samples', s=30) +pl.xticks([]) +pl.yticks([]) +pl.title('Transported samples\nSinkhornLpl1Transport') + +pl.subplot(2, 4, 8) +pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', + label='Target samples', alpha=0.3) +pl.scatter(transp_Xs_l1l2[:, 0], transp_Xs_l1l2[:, 1], c=ys, + marker='+', label='Transp samples', s=30) +pl.xticks([]) +pl.yticks([]) +pl.title('Transported samples\nSinkhornL1l2Transport') +pl.tight_layout() + +pl.show() diff --git a/examples/da/plot_otda_color_images.py b/examples/da/plot_otda_color_images.py new file mode 100644 index 0000000..a46ac29 --- /dev/null +++ b/examples/da/plot_otda_color_images.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- +""" +======================================================== +OT for domain adaptation with image color adaptation [6] +======================================================== + +This example presents a way of transferring colors between two image +with Optimal Transport as introduced in [6] + +[6] Ferradans, S., Papadakis, N., Peyre, G., & Aujol, J. F. (2014). +Regularized discrete optimal transport. +SIAM Journal on Imaging Sciences, 7(3), 1853-1882. +""" + +# Authors: Remi Flamary +# Stanilslas Chambon +# +# License: MIT License + +import numpy as np +from scipy import ndimage +import matplotlib.pylab as pl + +import ot + + +def im2mat(I): + """Converts and image to matrix (one pixel per line)""" + return I.reshape((I.shape[0] * I.shape[1], I.shape[2])) + + +def mat2im(X, shape): + """Converts back a matrix to an image""" + return X.reshape(shape) + + +def minmax(I): + return np.clip(I, 0, 1) + + +# Loading images +I1 = ndimage.imread('../../data/ocean_day.jpg').astype(np.float64) / 256 +I2 = ndimage.imread('../../data/ocean_sunset.jpg').astype(np.float64) / 256 + +X1 = im2mat(I1) +X2 = im2mat(I2) + +# training samples +nb = 1000 +idx1 = np.random.randint(X1.shape[0], size=(nb,)) +idx2 = np.random.randint(X2.shape[0], size=(nb,)) + +Xs = X1[idx1, :] +Xt = X2[idx2, :] + +# EMDTransport +ot_emd = ot.da.EMDTransport() +ot_emd.fit(Xs=Xs, Xt=Xt) + +# SinkhornTransport +ot_sinkhorn = ot.da.SinkhornTransport(reg_e=1e-1) +ot_sinkhorn.fit(Xs=Xs, Xt=Xt) + +# prediction between images (using out of sample prediction as in [6]) +transp_Xs_emd = ot_emd.transform(Xs=X1) +transp_Xt_emd = ot_emd.inverse_transform(Xt=X2) + +transp_Xs_sinkhorn = ot_emd.transform(Xs=X1) +transp_Xt_sinkhorn = ot_emd.inverse_transform(Xt=X2) + +I1t = minmax(mat2im(transp_Xs_emd, I1.shape)) +I2t = minmax(mat2im(transp_Xt_emd, I2.shape)) + +I1te = minmax(mat2im(transp_Xs_sinkhorn, I1.shape)) +I2te = minmax(mat2im(transp_Xt_sinkhorn, I2.shape)) + +############################################################################## +# plot original image +############################################################################## + +pl.figure(1, figsize=(6.4, 3)) + +pl.subplot(1, 2, 1) +pl.imshow(I1) +pl.axis('off') +pl.title('Image 1') + +pl.subplot(1, 2, 2) +pl.imshow(I2) +pl.axis('off') +pl.title('Image 2') + +############################################################################## +# scatter plot of colors +############################################################################## + +pl.figure(2, figsize=(6.4, 3)) + +pl.subplot(1, 2, 1) +pl.scatter(Xs[:, 0], Xs[:, 2], c=Xs) +pl.axis([0, 1, 0, 1]) +pl.xlabel('Red') +pl.ylabel('Blue') +pl.title('Image 1') + +pl.subplot(1, 2, 2) +pl.scatter(Xt[:, 0], Xt[:, 2], c=Xt) +pl.axis([0, 1, 0, 1]) +pl.xlabel('Red') +pl.ylabel('Blue') +pl.title('Image 2') +pl.tight_layout() + +############################################################################## +# plot new images +############################################################################## + +pl.figure(3, figsize=(8, 4)) + +pl.subplot(2, 3, 1) +pl.imshow(I1) +pl.axis('off') +pl.title('Image 1') + +pl.subplot(2, 3, 2) +pl.imshow(I1t) +pl.axis('off') +pl.title('Image 1 Adapt') + +pl.subplot(2, 3, 3) +pl.imshow(I1te) +pl.axis('off') +pl.title('Image 1 Adapt (reg)') + +pl.subplot(2, 3, 4) +pl.imshow(I2) +pl.axis('off') +pl.title('Image 2') + +pl.subplot(2, 3, 5) +pl.imshow(I2t) +pl.axis('off') +pl.title('Image 2 Adapt') + +pl.subplot(2, 3, 6) +pl.imshow(I2te) +pl.axis('off') +pl.title('Image 2 Adapt (reg)') +pl.tight_layout() + +pl.show() diff --git a/examples/da/plot_otda_d2.py b/examples/da/plot_otda_d2.py new file mode 100644 index 0000000..78c0372 --- /dev/null +++ b/examples/da/plot_otda_d2.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +""" +============================== +OT for empirical distributions +============================== + +This example introduces a domain adaptation in a 2D setting. It explicits +the problem of domain adaptation and introduces some optimal transport +approaches to solve it. + +Quantities such as optimal couplings, greater coupling coefficients and +transported samples are represented in order to give a visual understanding +of what the transport methods are doing. +""" + +# Authors: Remi Flamary +# Stanilslas Chambon +# +# License: MIT License + +import matplotlib.pylab as pl +import ot + +# number of source and target points to generate +ns = 150 +nt = 150 + +Xs, ys = ot.datasets.get_data_classif('3gauss', ns) +Xt, yt = ot.datasets.get_data_classif('3gauss2', nt) + +# Cost matrix +M = ot.dist(Xs, Xt) + +# 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=1e-1) +ot_sinkhorn.fit(Xs=Xs, Xt=Xt) + +# Sinkhorn Transport with Group lasso regularization +ot_lpl1 = ot.da.SinkhornLpl1Transport(reg_e=1e-1, reg_cl=1e0) +ot_lpl1.fit(Xs=Xs, ys=ys, 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_lpl1 = ot_lpl1.transform(Xs=Xs) + +############################################################################## +# Fig 1 : plots source and target samples + matrix of pairwise distance +############################################################################## + +pl.figure(1, figsize=(10, 10)) +pl.subplot(2, 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(2, 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.subplot(2, 2, 3) +pl.imshow(M, interpolation='nearest') +pl.xticks([]) +pl.yticks([]) +pl.title('Matrix of pairwise distances') +pl.tight_layout() + +############################################################################## +# Fig 2 : plots optimal couplings for the different methods +############################################################################## + +pl.figure(2, figsize=(10, 6)) + +pl.subplot(2, 3, 1) +pl.imshow(ot_emd.coupling_, interpolation='nearest') +pl.xticks([]) +pl.yticks([]) +pl.title('Optimal coupling\nEMDTransport') + +pl.subplot(2, 3, 2) +pl.imshow(ot_sinkhorn.coupling_, interpolation='nearest') +pl.xticks([]) +pl.yticks([]) +pl.title('Optimal coupling\nSinkhornTransport') + +pl.subplot(2, 3, 3) +pl.imshow(ot_lpl1.coupling_, interpolation='nearest') +pl.xticks([]) +pl.yticks([]) +pl.title('Optimal coupling\nSinkhornLpl1Transport') + +pl.subplot(2, 3, 4) +ot.plot.plot2D_samples_mat(Xs, Xt, ot_emd.coupling_, c=[.5, .5, 1]) +pl.scatter(Xs[:, 0], Xs[:, 1], c=ys, marker='+', label='Source samples') +pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', label='Target samples') +pl.xticks([]) +pl.yticks([]) +pl.title('Main coupling coefficients\nEMDTransport') + +pl.subplot(2, 3, 5) +ot.plot.plot2D_samples_mat(Xs, Xt, ot_sinkhorn.coupling_, c=[.5, .5, 1]) +pl.scatter(Xs[:, 0], Xs[:, 1], c=ys, marker='+', label='Source samples') +pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', label='Target samples') +pl.xticks([]) +pl.yticks([]) +pl.title('Main coupling coefficients\nSinkhornTransport') + +pl.subplot(2, 3, 6) +ot.plot.plot2D_samples_mat(Xs, Xt, ot_lpl1.coupling_, c=[.5, .5, 1]) +pl.scatter(Xs[:, 0], Xs[:, 1], c=ys, marker='+', label='Source samples') +pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', label='Target samples') +pl.xticks([]) +pl.yticks([]) +pl.title('Main coupling coefficients\nSinkhornLpl1Transport') +pl.tight_layout() + +############################################################################## +# Fig 3 : plot transported samples +############################################################################## + +# display transported samples +pl.figure(4, figsize=(10, 4)) +pl.subplot(1, 3, 1) +pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', + label='Target samples', alpha=0.5) +pl.scatter(transp_Xs_emd[:, 0], transp_Xs_emd[:, 1], c=ys, + marker='+', label='Transp samples', s=30) +pl.title('Transported samples\nEmdTransport') +pl.legend(loc=0) +pl.xticks([]) +pl.yticks([]) + +pl.subplot(1, 3, 2) +pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', + label='Target samples', alpha=0.5) +pl.scatter(transp_Xs_sinkhorn[:, 0], transp_Xs_sinkhorn[:, 1], c=ys, + marker='+', label='Transp samples', s=30) +pl.title('Transported samples\nSinkhornTransport') +pl.xticks([]) +pl.yticks([]) + +pl.subplot(1, 3, 3) +pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', + label='Target samples', alpha=0.5) +pl.scatter(transp_Xs_lpl1[:, 0], transp_Xs_lpl1[:, 1], c=ys, + marker='+', label='Transp samples', s=30) +pl.title('Transported samples\nSinkhornLpl1Transport') +pl.xticks([]) +pl.yticks([]) + +pl.tight_layout() +pl.show() diff --git a/examples/da/plot_otda_mapping.py b/examples/da/plot_otda_mapping.py new file mode 100644 index 0000000..ed234f5 --- /dev/null +++ b/examples/da/plot_otda_mapping.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +""" +=============================================== +OT mapping estimation for domain adaptation [8] +=============================================== + +This example presents how to use MappingTransport to estimate at the same +time both the coupling transport and approximate the transport map with either +a linear or a kernelized mapping as introduced in [8] + +[8] M. Perrot, N. Courty, R. Flamary, A. Habrard, + "Mapping estimation for discrete optimal transport", + Neural Information Processing Systems (NIPS), 2016. +""" + +# Authors: Remi Flamary +# Stanilslas Chambon +# +# License: MIT License + +import numpy as np +import matplotlib.pylab as pl +import ot + + +np.random.seed(0) + +############################################################################## +# generate +############################################################################## + +n = 100 # nb samples in source and target datasets +theta = 2 * np.pi / 20 +nz = 0.1 +Xs, ys = ot.datasets.get_data_classif('gaussrot', n, nz=nz) +Xs_new, _ = ot.datasets.get_data_classif('gaussrot', n, nz=nz) +Xt, yt = ot.datasets.get_data_classif('gaussrot', n, theta=theta, nz=nz) + +# one of the target mode changes its variance (no linear mapping) +Xt[yt == 2] *= 3 +Xt = Xt + 4 + + +# MappingTransport with linear kernel +ot_mapping_linear = ot.da.MappingTransport( + kernel="linear", mu=1e0, eta=1e-8, bias=True, + max_iter=20, verbose=True) + +ot_mapping_linear.fit( + Xs=Xs, Xt=Xt) + +# for original source samples, transform applies barycentric mapping +transp_Xs_linear = ot_mapping_linear.transform(Xs=Xs) + +# for out of source samples, transform applies the linear mapping +transp_Xs_linear_new = ot_mapping_linear.transform(Xs=Xs_new) + + +# MappingTransport with gaussian kernel +ot_mapping_gaussian = ot.da.MappingTransport( + kernel="gaussian", eta=1e-5, mu=1e-1, bias=True, sigma=1, + max_iter=10, verbose=True) +ot_mapping_gaussian.fit(Xs=Xs, Xt=Xt) + +# for original source samples, transform applies barycentric mapping +transp_Xs_gaussian = ot_mapping_gaussian.transform(Xs=Xs) + +# for out of source samples, transform applies the gaussian mapping +transp_Xs_gaussian_new = ot_mapping_gaussian.transform(Xs=Xs_new) + + +############################################################################## +# plot data +############################################################################## + +pl.figure(1, (10, 5)) +pl.clf() +pl.scatter(Xs[:, 0], Xs[:, 1], c=ys, marker='+', label='Source samples') +pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', label='Target samples') +pl.legend(loc=0) +pl.title('Source and target distributions') + +############################################################################## +# plot transported samples +############################################################################## + +pl.figure(2) +pl.clf() +pl.subplot(2, 2, 1) +pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', + label='Target samples', alpha=.2) +pl.scatter(transp_Xs_linear[:, 0], transp_Xs_linear[:, 1], c=ys, marker='+', + label='Mapped source samples') +pl.title("Bary. mapping (linear)") +pl.legend(loc=0) + +pl.subplot(2, 2, 2) +pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', + label='Target samples', alpha=.2) +pl.scatter(transp_Xs_linear_new[:, 0], transp_Xs_linear_new[:, 1], + c=ys, marker='+', label='Learned mapping') +pl.title("Estim. mapping (linear)") + +pl.subplot(2, 2, 3) +pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', + label='Target samples', alpha=.2) +pl.scatter(transp_Xs_gaussian[:, 0], transp_Xs_gaussian[:, 1], c=ys, + marker='+', label='barycentric mapping') +pl.title("Bary. mapping (kernel)") + +pl.subplot(2, 2, 4) +pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', + label='Target samples', alpha=.2) +pl.scatter(transp_Xs_gaussian_new[:, 0], transp_Xs_gaussian_new[:, 1], c=ys, + marker='+', label='Learned mapping') +pl.title("Estim. mapping (kernel)") +pl.tight_layout() + +pl.show() diff --git a/examples/da/plot_otda_mapping_colors_images.py b/examples/da/plot_otda_mapping_colors_images.py new file mode 100644 index 0000000..56b5a6f --- /dev/null +++ b/examples/da/plot_otda_mapping_colors_images.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +""" +==================================================================================== +OT for domain adaptation with image color adaptation [6] with mapping estimation [8] +==================================================================================== + +[6] Ferradans, S., Papadakis, N., Peyre, G., & Aujol, J. F. (2014). Regularized + discrete optimal transport. SIAM Journal on Imaging Sciences, 7(3), + 1853-1882. +[8] M. Perrot, N. Courty, R. Flamary, A. Habrard, "Mapping estimation for + discrete optimal transport", Neural Information Processing Systems (NIPS), + 2016. + +""" + +# Authors: Remi Flamary +# Stanilslas Chambon +# +# License: MIT License + +import numpy as np +from scipy import ndimage +import matplotlib.pylab as pl +import ot + + +def im2mat(I): + """Converts and image to matrix (one pixel per line)""" + return I.reshape((I.shape[0] * I.shape[1], I.shape[2])) + + +def mat2im(X, shape): + """Converts back a matrix to an image""" + return X.reshape(shape) + + +def minmax(I): + return np.clip(I, 0, 1) + + +############################################################################## +# Generate data +############################################################################## + +# Loading images +# I1 = ndimage.imread('../../data/ocean_day.jpg').astype(np.float64) / 256 +# I2 = ndimage.imread('../../data/ocean_sunset.jpg').astype(np.float64) / 256 + +I1 = ndimage.imread('data/ocean_day.jpg').astype(np.float64) / 256 +I2 = ndimage.imread('data/ocean_sunset.jpg').astype(np.float64) / 256 + + +X1 = im2mat(I1) +X2 = im2mat(I2) + +# training samples +nb = 1000 +idx1 = np.random.randint(X1.shape[0], size=(nb,)) +idx2 = np.random.randint(X2.shape[0], size=(nb,)) + +Xs = X1[idx1, :] +Xt = X2[idx2, :] + + +############################################################################## +# Domain adaptation for pixel distribution transfer +############################################################################## + +# EMDTransport +ot_emd = ot.da.EMDTransport() +ot_emd.fit(Xs=Xs, Xt=Xt) +transp_Xs_emd = ot_emd.transform(Xs=X1) +Image_emd = minmax(mat2im(transp_Xs_emd, I1.shape)) + +# SinkhornTransport +ot_sinkhorn = ot.da.SinkhornTransport(reg_e=1e-1) +ot_sinkhorn.fit(Xs=Xs, Xt=Xt) +transp_Xs_sinkhorn = ot_emd.transform(Xs=X1) +Image_sinkhorn = minmax(mat2im(transp_Xs_sinkhorn, I1.shape)) + +ot_mapping_linear = ot.da.MappingTransport( + mu=1e0, eta=1e-8, bias=True, max_iter=20, verbose=True) +ot_mapping_linear.fit(Xs=Xs, Xt=Xt) + +X1tl = ot_mapping_linear.transform(X1) +Image_mapping_linear = minmax(mat2im(X1tl, I1.shape)) + +ot_mapping_gaussian = ot.da.MappingTransport( + mu=1e0, eta=1e-2, sigma=1, bias=False, max_iter=10, verbose=True) +ot_mapping_gaussian.fit(Xs=Xs, Xt=Xt) + +X1tn = ot_mapping_gaussian.transform(X1) # use the estimated mapping +Image_mapping_gaussian = minmax(mat2im(X1tn, I1.shape)) + +############################################################################## +# plot original images +############################################################################## + +pl.figure(1, figsize=(6.4, 3)) +pl.subplot(1, 2, 1) +pl.imshow(I1) +pl.axis('off') +pl.title('Image 1') + +pl.subplot(1, 2, 2) +pl.imshow(I2) +pl.axis('off') +pl.title('Image 2') +pl.tight_layout() + +############################################################################## +# plot pixel values distribution +############################################################################## + +pl.figure(2, figsize=(6.4, 5)) + +pl.subplot(1, 2, 1) +pl.scatter(Xs[:, 0], Xs[:, 2], c=Xs) +pl.axis([0, 1, 0, 1]) +pl.xlabel('Red') +pl.ylabel('Blue') +pl.title('Image 1') + +pl.subplot(1, 2, 2) +pl.scatter(Xt[:, 0], Xt[:, 2], c=Xt) +pl.axis([0, 1, 0, 1]) +pl.xlabel('Red') +pl.ylabel('Blue') +pl.title('Image 2') +pl.tight_layout() + +############################################################################## +# plot transformed images +############################################################################## + +pl.figure(2, figsize=(10, 5)) + +pl.subplot(2, 3, 1) +pl.imshow(I1) +pl.axis('off') +pl.title('Im. 1') + +pl.subplot(2, 3, 4) +pl.imshow(I2) +pl.axis('off') +pl.title('Im. 2') + +pl.subplot(2, 3, 2) +pl.imshow(Image_emd) +pl.axis('off') +pl.title('EmdTransport') + +pl.subplot(2, 3, 5) +pl.imshow(Image_sinkhorn) +pl.axis('off') +pl.title('SinkhornTransport') + +pl.subplot(2, 3, 3) +pl.imshow(Image_mapping_linear) +pl.axis('off') +pl.title('MappingTransport (linear)') + +pl.subplot(2, 3, 6) +pl.imshow(Image_mapping_gaussian) +pl.axis('off') +pl.title('MappingTransport (gaussian)') +pl.tight_layout() + +pl.show() -- cgit v1.2.3 From e1a3984b63ce429b82e71dfb685d018788737068 Mon Sep 17 00:00:00 2001 From: Slasnista Date: Fri, 25 Aug 2017 15:15:52 +0200 Subject: small corrections for examples --- examples/da/plot_otda_classes.py | 2 +- examples/da/plot_otda_color_images.py | 2 +- examples/da/plot_otda_d2.py | 2 +- examples/da/plot_otda_mapping.py | 2 +- examples/da/plot_otda_mapping_colors_images.py | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/da/plot_otda_classes.py b/examples/da/plot_otda_classes.py index 1bfe2bb..e5c82fb 100644 --- a/examples/da/plot_otda_classes.py +++ b/examples/da/plot_otda_classes.py @@ -10,7 +10,7 @@ approaches currently supported in POT. """ # Authors: Remi Flamary -# Stanilslas Chambon +# Stanislas Chambon # # License: MIT License diff --git a/examples/da/plot_otda_color_images.py b/examples/da/plot_otda_color_images.py index a46ac29..bca7350 100644 --- a/examples/da/plot_otda_color_images.py +++ b/examples/da/plot_otda_color_images.py @@ -13,7 +13,7 @@ SIAM Journal on Imaging Sciences, 7(3), 1853-1882. """ # Authors: Remi Flamary -# Stanilslas Chambon +# Stanislas Chambon # # License: MIT License diff --git a/examples/da/plot_otda_d2.py b/examples/da/plot_otda_d2.py index 78c0372..1d2192f 100644 --- a/examples/da/plot_otda_d2.py +++ b/examples/da/plot_otda_d2.py @@ -14,7 +14,7 @@ of what the transport methods are doing. """ # Authors: Remi Flamary -# Stanilslas Chambon +# Stanislas Chambon # # License: MIT License diff --git a/examples/da/plot_otda_mapping.py b/examples/da/plot_otda_mapping.py index ed234f5..6d83507 100644 --- a/examples/da/plot_otda_mapping.py +++ b/examples/da/plot_otda_mapping.py @@ -14,7 +14,7 @@ a linear or a kernelized mapping as introduced in [8] """ # Authors: Remi Flamary -# Stanilslas Chambon +# Stanislas Chambon # # License: MIT License diff --git a/examples/da/plot_otda_mapping_colors_images.py b/examples/da/plot_otda_mapping_colors_images.py index 56b5a6f..05d9046 100644 --- a/examples/da/plot_otda_mapping_colors_images.py +++ b/examples/da/plot_otda_mapping_colors_images.py @@ -14,7 +14,7 @@ OT for domain adaptation with image color adaptation [6] with mapping estimation """ # Authors: Remi Flamary -# Stanilslas Chambon +# Stanislas Chambon # # License: MIT License @@ -82,14 +82,14 @@ ot_mapping_linear = ot.da.MappingTransport( mu=1e0, eta=1e-8, bias=True, max_iter=20, verbose=True) ot_mapping_linear.fit(Xs=Xs, Xt=Xt) -X1tl = ot_mapping_linear.transform(X1) +X1tl = ot_mapping_linear.transform(Xs=X1) Image_mapping_linear = minmax(mat2im(X1tl, I1.shape)) ot_mapping_gaussian = ot.da.MappingTransport( mu=1e0, eta=1e-2, sigma=1, bias=False, max_iter=10, verbose=True) ot_mapping_gaussian.fit(Xs=Xs, Xt=Xt) -X1tn = ot_mapping_gaussian.transform(X1) # use the estimated mapping +X1tn = ot_mapping_gaussian.transform(Xs=X1) # use the estimated mapping Image_mapping_gaussian = minmax(mat2im(X1tn, I1.shape)) ############################################################################## -- cgit v1.2.3 From 4f802cf3de02fd3ff64fd9a8fcc5c7f1daf3cbea Mon Sep 17 00:00:00 2001 From: Slasnista Date: Fri, 25 Aug 2017 15:18:05 +0200 Subject: set properly path of data --- examples/da/plot_otda_mapping_colors_images.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/examples/da/plot_otda_mapping_colors_images.py b/examples/da/plot_otda_mapping_colors_images.py index 05d9046..4209020 100644 --- a/examples/da/plot_otda_mapping_colors_images.py +++ b/examples/da/plot_otda_mapping_colors_images.py @@ -43,11 +43,8 @@ def minmax(I): ############################################################################## # Loading images -# I1 = ndimage.imread('../../data/ocean_day.jpg').astype(np.float64) / 256 -# I2 = ndimage.imread('../../data/ocean_sunset.jpg').astype(np.float64) / 256 - -I1 = ndimage.imread('data/ocean_day.jpg').astype(np.float64) / 256 -I2 = ndimage.imread('data/ocean_sunset.jpg').astype(np.float64) / 256 +I1 = ndimage.imread('../../data/ocean_day.jpg').astype(np.float64) / 256 +I2 = ndimage.imread('../../data/ocean_sunset.jpg').astype(np.float64) / 256 X1 = im2mat(I1) -- cgit v1.2.3 From e1606c1025c0acc4608254d72af6c127ebe1c03f Mon Sep 17 00:00:00 2001 From: Slasnista Date: Mon, 28 Aug 2017 10:43:31 +0200 Subject: move no da objects into utils.py --- README.md | 2 + ot/da.py | 136 +-------------------------------- ot/deprecation.py | 103 ------------------------- ot/utils.py | 219 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 222 insertions(+), 238 deletions(-) delete mode 100644 ot/deprecation.py diff --git a/README.md b/README.md index 7a65106..27b4643 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,8 @@ The contributors to this library are: * [Laetitia Chapel](http://people.irisa.fr/Laetitia.Chapel/) * [Michael Perrot](http://perso.univ-st-etienne.fr/pem82055/) (Mapping estimation) * [Léo Gautheron](https://github.com/aje) (GPU implementation) +* [Nathalie Gayraud]() +* [Stanislas Chambon](https://slasnista.github.io/) This toolbox benefit a lot from open source research and we would like to thank the following persons for providing some code (in various languages): diff --git a/ot/da.py b/ot/da.py index 5a34979..8c62669 100644 --- a/ot/da.py +++ b/ot/da.py @@ -10,14 +10,13 @@ Domain adaptation with optimal transport # License: MIT License import numpy as np -import warnings from .bregman import sinkhorn from .lp import emd from .utils import unif, dist, kernel +from .utils import deprecated, BaseEstimator from .optim import cg from .optim import gcg -from .deprecation import deprecated def sinkhorn_lpl1_mm(a, labels_a, b, M, reg, eta=0.1, numItermax=10, @@ -936,139 +935,6 @@ class OTDA_mapping_kernel(OTDA_mapping_linear): print("Warning, model not fitted yet, returning None") return None -############################################################################## -# proposal -############################################################################## - - -# adapted from sklearn - -class BaseEstimator(object): - """Base class for all estimators in scikit-learn - 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``). - """ - - @classmethod - def _get_param_names(cls): - """Get parameter names for the estimator""" - try: - from inspect import signature - except ImportError: - from .externals.funcsigs import signature - # fetch the constructor or the original constructor before - # deprecation wrapping if any - init = getattr(cls.__init__, 'deprecated_original', cls.__init__) - if init is object.__init__: - # No explicit constructor to introspect - return [] - - # introspect the constructor arguments to find the model parameters - # to represent - init_signature = signature(init) - # Consider the constructor parameters excluding 'self' - parameters = [p for p in init_signature.parameters.values() - if p.name != 'self' and p.kind != p.VAR_KEYWORD] - for p in parameters: - if p.kind == p.VAR_POSITIONAL: - raise RuntimeError("scikit-learn estimators should always " - "specify their parameters in the signature" - " of their __init__ (no varargs)." - " %s with constructor %s doesn't " - " follow this convention." - % (cls, init_signature)) - # Extract and sort argument names excluding 'self' - return sorted([p.name for p in parameters]) - - def get_params(self, deep=True): - """Get parameters for this estimator. - - Parameters - ---------- - deep : boolean, optional - If True, will return the parameters for this estimator and - contained subobjects that are estimators. - - Returns - ------- - params : mapping of string to any - Parameter names mapped to their values. - """ - out = dict() - for key in self._get_param_names(): - # We need deprecation warnings to always be on in order to - # catch deprecated param values. - # This is set in utils/__init__.py but it gets overwritten - # when running under python3 somehow. - warnings.simplefilter("always", DeprecationWarning) - try: - with warnings.catch_warnings(record=True) as w: - value = getattr(self, key, None) - if len(w) and w[0].category == DeprecationWarning: - # if the parameter is deprecated, don't show it - continue - finally: - warnings.filters.pop(0) - - # XXX: should we rather test if instance of estimator? - if deep and hasattr(value, 'get_params'): - deep_items = value.get_params().items() - out.update((key + '__' + k, val) for k, val in deep_items) - out[key] = value - return out - - def set_params(self, **params): - """Set the parameters of this estimator. - - The method works on simple estimators as well as on nested objects - (such as pipelines). The latter have parameters of the form - ``__`` so that it's possible to update each - component of a nested object. - - Returns - ------- - self - """ - if not params: - # Simple optimisation to gain speed (inspect is slow) - return self - valid_params = self.get_params(deep=True) - # for key, value in iteritems(params): - for key, value in params.items(): - split = key.split('__', 1) - if len(split) > 1: - # nested objects case - name, sub_name = split - if name not in valid_params: - raise ValueError('Invalid parameter %s for estimator %s. ' - 'Check the list of available parameters ' - 'with `estimator.get_params().keys()`.' % - (name, self)) - sub_object = valid_params[name] - sub_object.set_params(**{sub_name: value}) - else: - # simple objects case - if key not in valid_params: - raise ValueError('Invalid parameter %s for estimator %s. ' - 'Check the list of available parameters ' - 'with `estimator.get_params().keys()`.' % - (key, self.__class__.__name__)) - setattr(self, key, value) - return self - - def __repr__(self): - from sklearn.base import _pprint - class_name = self.__class__.__name__ - return '%s(%s)' % (class_name, _pprint(self.get_params(deep=False), - offset=len(class_name),),) - - # __getstate__ and __setstate__ are omitted because they only contain - # conditionals that are not satisfied by our objects (e.g., - # ``if type(self).__module__.startswith('sklearn.')``. - def distribution_estimation_uniform(X): """estimates a uniform distribution from an array of samples X diff --git a/ot/deprecation.py b/ot/deprecation.py deleted file mode 100644 index 2b16427..0000000 --- a/ot/deprecation.py +++ /dev/null @@ -1,103 +0,0 @@ -""" - deprecated class from scikit-learn package - https://github.com/scikit-learn/scikit-learn/blob/master/sklearn/utils/deprecation.py -""" - -import sys -import warnings - -__all__ = ["deprecated", ] - - -class deprecated(object): - """Decorator to mark a function or class as deprecated. - Issue a warning when the function is called/the class is instantiated and - adds a warning to the docstring. - The optional extra argument will be appended to the deprecation message - and the docstring. Note: to use this with the default value for extra, put - in an empty of parentheses: - >>> from ot.deprecation import deprecated - >>> @deprecated() - ... def some_function(): pass - - Parameters - ---------- - extra : string - to be added to the deprecation messages - """ - - # Adapted from http://wiki.python.org/moin/PythonDecoratorLibrary, - # but with many changes. - - def __init__(self, extra=''): - self.extra = extra - - def __call__(self, obj): - """Call method - Parameters - ---------- - obj : object - """ - if isinstance(obj, type): - return self._decorate_class(obj) - else: - return self._decorate_fun(obj) - - def _decorate_class(self, cls): - msg = "Class %s is deprecated" % cls.__name__ - if self.extra: - msg += "; %s" % self.extra - - # FIXME: we should probably reset __new__ for full generality - init = cls.__init__ - - def wrapped(*args, **kwargs): - warnings.warn(msg, category=DeprecationWarning) - return init(*args, **kwargs) - - cls.__init__ = wrapped - - wrapped.__name__ = '__init__' - wrapped.__doc__ = self._update_doc(init.__doc__) - wrapped.deprecated_original = init - - return cls - - def _decorate_fun(self, fun): - """Decorate function fun""" - - msg = "Function %s is deprecated" % fun.__name__ - if self.extra: - msg += "; %s" % self.extra - - def wrapped(*args, **kwargs): - warnings.warn(msg, category=DeprecationWarning) - return fun(*args, **kwargs) - - wrapped.__name__ = fun.__name__ - wrapped.__dict__ = fun.__dict__ - wrapped.__doc__ = self._update_doc(fun.__doc__) - - return wrapped - - def _update_doc(self, olddoc): - newdoc = "DEPRECATED" - if self.extra: - newdoc = "%s: %s" % (newdoc, self.extra) - if olddoc: - newdoc = "%s\n\n%s" % (newdoc, olddoc) - return newdoc - - -def _is_deprecated(func): - """Helper to check if func is wraped by our deprecated decorator""" - if sys.version_info < (3, 5): - raise NotImplementedError("This is only available for python3.5 " - "or above") - closures = getattr(func, '__closure__', []) - if closures is None: - closures = [] - is_deprecated = ('deprecated' in ''.join([c.cell_contents - for c in closures - if isinstance(c.cell_contents, str)])) - return is_deprecated diff --git a/ot/utils.py b/ot/utils.py index 2b2f8b3..29ad536 100644 --- a/ot/utils.py +++ b/ot/utils.py @@ -13,6 +13,9 @@ import time import numpy as np from scipy.spatial.distance import cdist +import sys +import warnings + __time_tic_toc = time.time() @@ -163,3 +166,219 @@ def parmap(f, X, nprocs=multiprocessing.cpu_count()): [p.join() for p in proc] return [x for i, x in sorted(res)] + + +class deprecated(object): + """Decorator to mark a function or class as deprecated. + + deprecated class from scikit-learn package + https://github.com/scikit-learn/scikit-learn/blob/master/sklearn/utils/deprecation.py + Issue a warning when the function is called/the class is instantiated and + adds a warning to the docstring. + The optional extra argument will be appended to the deprecation message + and the docstring. Note: to use this with the default value for extra, put + in an empty of parentheses: + >>> from ot.deprecation import deprecated + >>> @deprecated() + ... def some_function(): pass + + Parameters + ---------- + extra : string + to be added to the deprecation messages + """ + + # Adapted from http://wiki.python.org/moin/PythonDecoratorLibrary, + # but with many changes. + + def __init__(self, extra=''): + self.extra = extra + + def __call__(self, obj): + """Call method + Parameters + ---------- + obj : object + """ + if isinstance(obj, type): + return self._decorate_class(obj) + else: + return self._decorate_fun(obj) + + def _decorate_class(self, cls): + msg = "Class %s is deprecated" % cls.__name__ + if self.extra: + msg += "; %s" % self.extra + + # FIXME: we should probably reset __new__ for full generality + init = cls.__init__ + + def wrapped(*args, **kwargs): + warnings.warn(msg, category=DeprecationWarning) + return init(*args, **kwargs) + + cls.__init__ = wrapped + + wrapped.__name__ = '__init__' + wrapped.__doc__ = self._update_doc(init.__doc__) + wrapped.deprecated_original = init + + return cls + + def _decorate_fun(self, fun): + """Decorate function fun""" + + msg = "Function %s is deprecated" % fun.__name__ + if self.extra: + msg += "; %s" % self.extra + + def wrapped(*args, **kwargs): + warnings.warn(msg, category=DeprecationWarning) + return fun(*args, **kwargs) + + wrapped.__name__ = fun.__name__ + wrapped.__dict__ = fun.__dict__ + wrapped.__doc__ = self._update_doc(fun.__doc__) + + return wrapped + + def _update_doc(self, olddoc): + newdoc = "DEPRECATED" + if self.extra: + newdoc = "%s: %s" % (newdoc, self.extra) + if olddoc: + newdoc = "%s\n\n%s" % (newdoc, olddoc) + return newdoc + + +def _is_deprecated(func): + """Helper to check if func is wraped by our deprecated decorator""" + if sys.version_info < (3, 5): + raise NotImplementedError("This is only available for python3.5 " + "or above") + closures = getattr(func, '__closure__', []) + if closures is None: + closures = [] + is_deprecated = ('deprecated' in ''.join([c.cell_contents + for c in closures + if isinstance(c.cell_contents, str)])) + return is_deprecated + + +class BaseEstimator(object): + """Base class for most objects in POT + adapted from sklearn BaseEstimator class + + 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``). + """ + + @classmethod + def _get_param_names(cls): + """Get parameter names for the estimator""" + try: + from inspect import signature + except ImportError: + from .externals.funcsigs import signature + # fetch the constructor or the original constructor before + # deprecation wrapping if any + init = getattr(cls.__init__, 'deprecated_original', cls.__init__) + if init is object.__init__: + # No explicit constructor to introspect + return [] + + # introspect the constructor arguments to find the model parameters + # to represent + init_signature = signature(init) + # Consider the constructor parameters excluding 'self' + parameters = [p for p in init_signature.parameters.values() + if p.name != 'self' and p.kind != p.VAR_KEYWORD] + for p in parameters: + if p.kind == p.VAR_POSITIONAL: + raise RuntimeError("POT estimators should always " + "specify their parameters in the signature" + " of their __init__ (no varargs)." + " %s with constructor %s doesn't " + " follow this convention." + % (cls, init_signature)) + # Extract and sort argument names excluding 'self' + return sorted([p.name for p in parameters]) + + def get_params(self, deep=True): + """Get parameters for this estimator. + + Parameters + ---------- + deep : boolean, optional + If True, will return the parameters for this estimator and + contained subobjects that are estimators. + + Returns + ------- + params : mapping of string to any + Parameter names mapped to their values. + """ + out = dict() + for key in self._get_param_names(): + # We need deprecation warnings to always be on in order to + # catch deprecated param values. + # This is set in utils/__init__.py but it gets overwritten + # when running under python3 somehow. + warnings.simplefilter("always", DeprecationWarning) + try: + with warnings.catch_warnings(record=True) as w: + value = getattr(self, key, None) + if len(w) and w[0].category == DeprecationWarning: + # if the parameter is deprecated, don't show it + continue + finally: + warnings.filters.pop(0) + + # XXX: should we rather test if instance of estimator? + if deep and hasattr(value, 'get_params'): + deep_items = value.get_params().items() + out.update((key + '__' + k, val) for k, val in deep_items) + out[key] = value + return out + + def set_params(self, **params): + """Set the parameters of this estimator. + + The method works on simple estimators as well as on nested objects + (such as pipelines). The latter have parameters of the form + ``__`` so that it's possible to update each + component of a nested object. + + Returns + ------- + self + """ + if not params: + # Simple optimisation to gain speed (inspect is slow) + return self + valid_params = self.get_params(deep=True) + # for key, value in iteritems(params): + for key, value in params.items(): + split = key.split('__', 1) + if len(split) > 1: + # nested objects case + name, sub_name = split + if name not in valid_params: + raise ValueError('Invalid parameter %s for estimator %s. ' + 'Check the list of available parameters ' + 'with `estimator.get_params().keys()`.' % + (name, self)) + sub_object = valid_params[name] + sub_object.set_params(**{sub_name: value}) + else: + # simple objects case + if key not in valid_params: + raise ValueError('Invalid parameter %s for estimator %s. ' + 'Check the list of available parameters ' + 'with `estimator.get_params().keys()`.' % + (key, self.__class__.__name__)) + setattr(self, key, value) + return self -- cgit v1.2.3 From f79f4831e9d70217ceab2758733abe62dc93208b Mon Sep 17 00:00:00 2001 From: Slasnista Date: Mon, 28 Aug 2017 11:03:28 +0200 Subject: handling input arguments in fit, transform... methods + remove old examples --- examples/plot_OTDA_2D.py | 126 ----------------- examples/plot_OTDA_classes.py | 117 ---------------- examples/plot_OTDA_color_images.py | 152 -------------------- examples/plot_OTDA_mapping.py | 124 ----------------- examples/plot_OTDA_mapping_color_images.py | 169 ---------------------- ot/da.py | 217 ++++++++++++++++------------- 6 files changed, 121 insertions(+), 784 deletions(-) delete mode 100644 examples/plot_OTDA_2D.py delete mode 100644 examples/plot_OTDA_classes.py delete mode 100644 examples/plot_OTDA_color_images.py delete mode 100644 examples/plot_OTDA_mapping.py delete mode 100644 examples/plot_OTDA_mapping_color_images.py diff --git a/examples/plot_OTDA_2D.py b/examples/plot_OTDA_2D.py deleted file mode 100644 index f2108c6..0000000 --- a/examples/plot_OTDA_2D.py +++ /dev/null @@ -1,126 +0,0 @@ -# -*- coding: utf-8 -*- -""" -============================== -OT for empirical distributions -============================== - -""" - -# Author: Remi Flamary -# -# License: MIT License - -import numpy as np -import matplotlib.pylab as pl -import ot - - -#%% parameters - -n = 150 # nb bins - -xs, ys = ot.datasets.get_data_classif('3gauss', n) -xt, yt = ot.datasets.get_data_classif('3gauss2', n) - -a, b = ot.unif(n), ot.unif(n) -# loss matrix -M = ot.dist(xs, xt) -# M/=M.max() - -#%% plot samples - -pl.figure(1) -pl.subplot(2, 2, 1) -pl.scatter(xs[:, 0], xs[:, 1], c=ys, marker='+', label='Source samples') -pl.legend(loc=0) -pl.title('Source distributions') - -pl.subplot(2, 2, 2) -pl.scatter(xt[:, 0], xt[:, 1], c=yt, marker='o', label='Target samples') -pl.legend(loc=0) -pl.title('target distributions') - -pl.figure(2) -pl.imshow(M, interpolation='nearest') -pl.title('Cost matrix M') - - -#%% OT estimation - -# EMD -G0 = ot.emd(a, b, M) - -# sinkhorn -lambd = 1e-1 -Gs = ot.sinkhorn(a, b, M, lambd) - - -# Group lasso regularization -reg = 1e-1 -eta = 1e0 -Gg = ot.da.sinkhorn_lpl1_mm(a, ys.astype(np.int), b, M, reg, eta) - - -#%% visu matrices - -pl.figure(3) - -pl.subplot(2, 3, 1) -pl.imshow(G0, interpolation='nearest') -pl.title('OT matrix ') - -pl.subplot(2, 3, 2) -pl.imshow(Gs, interpolation='nearest') -pl.title('OT matrix Sinkhorn') - -pl.subplot(2, 3, 3) -pl.imshow(Gg, interpolation='nearest') -pl.title('OT matrix Group lasso') - -pl.subplot(2, 3, 4) -ot.plot.plot2D_samples_mat(xs, xt, G0, c=[.5, .5, 1]) -pl.scatter(xs[:, 0], xs[:, 1], c=ys, marker='+', label='Source samples') -pl.scatter(xt[:, 0], xt[:, 1], c=yt, marker='o', label='Target samples') - - -pl.subplot(2, 3, 5) -ot.plot.plot2D_samples_mat(xs, xt, Gs, c=[.5, .5, 1]) -pl.scatter(xs[:, 0], xs[:, 1], c=ys, marker='+', label='Source samples') -pl.scatter(xt[:, 0], xt[:, 1], c=yt, marker='o', label='Target samples') - -pl.subplot(2, 3, 6) -ot.plot.plot2D_samples_mat(xs, xt, Gg, c=[.5, .5, 1]) -pl.scatter(xs[:, 0], xs[:, 1], c=ys, marker='+', label='Source samples') -pl.scatter(xt[:, 0], xt[:, 1], c=yt, marker='o', label='Target samples') -pl.tight_layout() - -#%% sample interpolation - -xst0 = n * G0.dot(xt) -xsts = n * Gs.dot(xt) -xstg = n * Gg.dot(xt) - -pl.figure(4, figsize=(8, 3)) -pl.subplot(1, 3, 1) -pl.scatter(xt[:, 0], xt[:, 1], c=yt, marker='o', - label='Target samples', alpha=0.5) -pl.scatter(xst0[:, 0], xst0[:, 1], c=ys, - marker='+', label='Transp samples', s=30) -pl.title('Interp samples') -pl.legend(loc=0) - -pl.subplot(1, 3, 2) -pl.scatter(xt[:, 0], xt[:, 1], c=yt, marker='o', - label='Target samples', alpha=0.5) -pl.scatter(xsts[:, 0], xsts[:, 1], c=ys, - marker='+', label='Transp samples', s=30) -pl.title('Interp samples Sinkhorn') - -pl.subplot(1, 3, 3) -pl.scatter(xt[:, 0], xt[:, 1], c=yt, marker='o', - label='Target samples', alpha=0.5) -pl.scatter(xstg[:, 0], xstg[:, 1], c=ys, - marker='+', label='Transp samples', s=30) -pl.title('Interp samples Grouplasso') -pl.tight_layout() -pl.show() diff --git a/examples/plot_OTDA_classes.py b/examples/plot_OTDA_classes.py deleted file mode 100644 index 53e4bae..0000000 --- a/examples/plot_OTDA_classes.py +++ /dev/null @@ -1,117 +0,0 @@ -# -*- coding: utf-8 -*- -""" -======================== -OT for domain adaptation -======================== - -""" - -# Author: Remi Flamary -# -# License: MIT License - -import matplotlib.pylab as pl -import ot - - -#%% parameters - -n = 150 # nb samples in source and target datasets - -xs, ys = ot.datasets.get_data_classif('3gauss', n) -xt, yt = ot.datasets.get_data_classif('3gauss2', n) - - -#%% plot samples - -pl.figure(1, figsize=(6.4, 3)) - -pl.subplot(1, 2, 1) -pl.scatter(xs[:, 0], xs[:, 1], c=ys, marker='+', label='Source samples') -pl.legend(loc=0) -pl.title('Source distributions') - -pl.subplot(1, 2, 2) -pl.scatter(xt[:, 0], xt[:, 1], c=yt, marker='o', label='Target samples') -pl.legend(loc=0) -pl.title('target distributions') - - -#%% OT estimation - -# LP problem -da_emd = ot.da.OTDA() # init class -da_emd.fit(xs, xt) # fit distributions -xst0 = da_emd.interp() # interpolation of source samples - -# sinkhorn regularization -lambd = 1e-1 -da_entrop = ot.da.OTDA_sinkhorn() -da_entrop.fit(xs, xt, reg=lambd) -xsts = da_entrop.interp() - -# non-convex Group lasso regularization -reg = 1e-1 -eta = 1e0 -da_lpl1 = ot.da.OTDA_lpl1() -da_lpl1.fit(xs, ys, xt, reg=reg, eta=eta) -xstg = da_lpl1.interp() - -# True Group lasso regularization -reg = 1e-1 -eta = 2e0 -da_l1l2 = ot.da.OTDA_l1l2() -da_l1l2.fit(xs, ys, xt, reg=reg, eta=eta, numItermax=20, verbose=True) -xstgl = da_l1l2.interp() - -#%% plot interpolated source samples - -param_img = {'interpolation': 'nearest', 'cmap': 'spectral'} - -pl.figure(2, figsize=(8, 4.5)) -pl.subplot(2, 4, 1) -pl.imshow(da_emd.G, **param_img) -pl.title('OT matrix') - -pl.subplot(2, 4, 2) -pl.imshow(da_entrop.G, **param_img) -pl.title('OT matrix\nsinkhorn') - -pl.subplot(2, 4, 3) -pl.imshow(da_lpl1.G, **param_img) -pl.title('OT matrix\nnon-convex Group Lasso') - -pl.subplot(2, 4, 4) -pl.imshow(da_l1l2.G, **param_img) -pl.title('OT matrix\nGroup Lasso') - -pl.subplot(2, 4, 5) -pl.scatter(xt[:, 0], xt[:, 1], c=yt, marker='o', - label='Target samples', alpha=0.3) -pl.scatter(xst0[:, 0], xst0[:, 1], c=ys, - marker='+', label='Transp samples', s=30) -pl.title('Interp samples') -pl.legend(loc=0) - -pl.subplot(2, 4, 6) -pl.scatter(xt[:, 0], xt[:, 1], c=yt, marker='o', - label='Target samples', alpha=0.3) -pl.scatter(xsts[:, 0], xsts[:, 1], c=ys, - marker='+', label='Transp samples', s=30) -pl.title('Interp samples\nSinkhorn') - -pl.subplot(2, 4, 7) -pl.scatter(xt[:, 0], xt[:, 1], c=yt, marker='o', - label='Target samples', alpha=0.3) -pl.scatter(xstg[:, 0], xstg[:, 1], c=ys, - marker='+', label='Transp samples', s=30) -pl.title('Interp samples\nnon-convex Group Lasso') - -pl.subplot(2, 4, 8) -pl.scatter(xt[:, 0], xt[:, 1], c=yt, marker='o', - label='Target samples', alpha=0.3) -pl.scatter(xstgl[:, 0], xstgl[:, 1], c=ys, - marker='+', label='Transp samples', s=30) -pl.title('Interp samples\nGroup Lasso') -pl.tight_layout() -pl.show() diff --git a/examples/plot_OTDA_color_images.py b/examples/plot_OTDA_color_images.py deleted file mode 100644 index c5ff873..0000000 --- a/examples/plot_OTDA_color_images.py +++ /dev/null @@ -1,152 +0,0 @@ -# -*- coding: utf-8 -*- -""" -======================================================== -OT for domain adaptation with image color adaptation [6] -======================================================== - -[6] Ferradans, S., Papadakis, N., Peyre, G., & Aujol, J. F. (2014). -Regularized discrete optimal transport. -SIAM Journal on Imaging Sciences, 7(3), 1853-1882. -""" - -# Author: Remi Flamary -# -# License: MIT License - -import numpy as np -from scipy import ndimage -import matplotlib.pylab as pl -import ot - - -#%% Loading images - -I1 = ndimage.imread('../data/ocean_day.jpg').astype(np.float64) / 256 -I2 = ndimage.imread('../data/ocean_sunset.jpg').astype(np.float64) / 256 - -#%% Plot images - -pl.figure(1, figsize=(6.4, 3)) - -pl.subplot(1, 2, 1) -pl.imshow(I1) -pl.axis('off') -pl.title('Image 1') - -pl.subplot(1, 2, 2) -pl.imshow(I2) -pl.axis('off') -pl.title('Image 2') - -pl.show() - -#%% Image conversion and dataset generation - - -def im2mat(I): - """Converts and image to matrix (one pixel per line)""" - return I.reshape((I.shape[0] * I.shape[1], I.shape[2])) - - -def mat2im(X, shape): - """Converts back a matrix to an image""" - return X.reshape(shape) - - -X1 = im2mat(I1) -X2 = im2mat(I2) - -# training samples -nb = 1000 -idx1 = np.random.randint(X1.shape[0], size=(nb,)) -idx2 = np.random.randint(X2.shape[0], size=(nb,)) - -xs = X1[idx1, :] -xt = X2[idx2, :] - -#%% Plot image distributions - - -pl.figure(2, figsize=(6.4, 3)) - -pl.subplot(1, 2, 1) -pl.scatter(xs[:, 0], xs[:, 2], c=xs) -pl.axis([0, 1, 0, 1]) -pl.xlabel('Red') -pl.ylabel('Blue') -pl.title('Image 1') - -pl.subplot(1, 2, 2) -pl.scatter(xt[:, 0], xt[:, 2], c=xt) -pl.axis([0, 1, 0, 1]) -pl.xlabel('Red') -pl.ylabel('Blue') -pl.title('Image 2') -pl.tight_layout() - -#%% domain adaptation between images - -# LP problem -da_emd = ot.da.OTDA() # init class -da_emd.fit(xs, xt) # fit distributions - -# sinkhorn regularization -lambd = 1e-1 -da_entrop = ot.da.OTDA_sinkhorn() -da_entrop.fit(xs, xt, reg=lambd) - -#%% prediction between images (using out of sample prediction as in [6]) - -X1t = da_emd.predict(X1) -X2t = da_emd.predict(X2, -1) - -X1te = da_entrop.predict(X1) -X2te = da_entrop.predict(X2, -1) - - -def minmax(I): - return np.clip(I, 0, 1) - - -I1t = minmax(mat2im(X1t, I1.shape)) -I2t = minmax(mat2im(X2t, I2.shape)) - -I1te = minmax(mat2im(X1te, I1.shape)) -I2te = minmax(mat2im(X2te, I2.shape)) - -#%% plot all images - -pl.figure(2, figsize=(8, 4)) - -pl.subplot(2, 3, 1) -pl.imshow(I1) -pl.axis('off') -pl.title('Image 1') - -pl.subplot(2, 3, 2) -pl.imshow(I1t) -pl.axis('off') -pl.title('Image 1 Adapt') - -pl.subplot(2, 3, 3) -pl.imshow(I1te) -pl.axis('off') -pl.title('Image 1 Adapt (reg)') - -pl.subplot(2, 3, 4) -pl.imshow(I2) -pl.axis('off') -pl.title('Image 2') - -pl.subplot(2, 3, 5) -pl.imshow(I2t) -pl.axis('off') -pl.title('Image 2 Adapt') - -pl.subplot(2, 3, 6) -pl.imshow(I2te) -pl.axis('off') -pl.title('Image 2 Adapt (reg)') -pl.tight_layout() - -pl.show() diff --git a/examples/plot_OTDA_mapping.py b/examples/plot_OTDA_mapping.py deleted file mode 100644 index a0d7f8b..0000000 --- a/examples/plot_OTDA_mapping.py +++ /dev/null @@ -1,124 +0,0 @@ -# -*- coding: utf-8 -*- -""" -=============================================== -OT mapping estimation for domain adaptation [8] -=============================================== - -[8] M. Perrot, N. Courty, R. Flamary, A. Habrard, - "Mapping estimation for discrete optimal transport", - Neural Information Processing Systems (NIPS), 2016. -""" - -# Author: Remi Flamary -# -# License: MIT License - -import numpy as np -import matplotlib.pylab as pl -import ot - - -#%% dataset generation - -np.random.seed(0) # makes example reproducible - -n = 100 # nb samples in source and target datasets -theta = 2 * np.pi / 20 -nz = 0.1 -xs, ys = ot.datasets.get_data_classif('gaussrot', n, nz=nz) -xt, yt = ot.datasets.get_data_classif('gaussrot', n, theta=theta, nz=nz) - -# one of the target mode changes its variance (no linear mapping) -xt[yt == 2] *= 3 -xt = xt + 4 - - -#%% plot samples - -pl.figure(1, (6.4, 3)) -pl.clf() -pl.scatter(xs[:, 0], xs[:, 1], c=ys, marker='+', label='Source samples') -pl.scatter(xt[:, 0], xt[:, 1], c=yt, marker='o', label='Target samples') -pl.legend(loc=0) -pl.title('Source and target distributions') - - -#%% OT linear mapping estimation - -eta = 1e-8 # quadratic regularization for regression -mu = 1e0 # weight of the OT linear term -bias = True # estimate a bias - -ot_mapping = ot.da.OTDA_mapping_linear() -ot_mapping.fit(xs, xt, mu=mu, eta=eta, bias=bias, numItermax=20, verbose=True) - -xst = ot_mapping.predict(xs) # use the estimated mapping -xst0 = ot_mapping.interp() # use barycentric mapping - - -pl.figure(2) -pl.clf() -pl.subplot(2, 2, 1) -pl.scatter(xt[:, 0], xt[:, 1], c=yt, marker='o', - label='Target samples', alpha=.3) -pl.scatter(xst0[:, 0], xst0[:, 1], c=ys, - marker='+', label='barycentric mapping') -pl.title("barycentric mapping") - -pl.subplot(2, 2, 2) -pl.scatter(xt[:, 0], xt[:, 1], c=yt, marker='o', - label='Target samples', alpha=.3) -pl.scatter(xst[:, 0], xst[:, 1], c=ys, marker='+', label='Learned mapping') -pl.title("Learned mapping") -pl.tight_layout() - -#%% Kernel mapping estimation - -eta = 1e-5 # quadratic regularization for regression -mu = 1e-1 # weight of the OT linear term -bias = True # estimate a bias -sigma = 1 # sigma bandwidth fot gaussian kernel - - -ot_mapping_kernel = ot.da.OTDA_mapping_kernel() -ot_mapping_kernel.fit( - xs, xt, mu=mu, eta=eta, sigma=sigma, bias=bias, numItermax=10, verbose=True) - -xst_kernel = ot_mapping_kernel.predict(xs) # use the estimated mapping -xst0_kernel = ot_mapping_kernel.interp() # use barycentric mapping - - -#%% Plotting the mapped samples - -pl.figure(2) -pl.clf() -pl.subplot(2, 2, 1) -pl.scatter(xt[:, 0], xt[:, 1], c=yt, marker='o', - label='Target samples', alpha=.2) -pl.scatter(xst0[:, 0], xst0[:, 1], c=ys, marker='+', - label='Mapped source samples') -pl.title("Bary. mapping (linear)") -pl.legend(loc=0) - -pl.subplot(2, 2, 2) -pl.scatter(xt[:, 0], xt[:, 1], c=yt, marker='o', - label='Target samples', alpha=.2) -pl.scatter(xst[:, 0], xst[:, 1], c=ys, marker='+', label='Learned mapping') -pl.title("Estim. mapping (linear)") - -pl.subplot(2, 2, 3) -pl.scatter(xt[:, 0], xt[:, 1], c=yt, marker='o', - label='Target samples', alpha=.2) -pl.scatter(xst0_kernel[:, 0], xst0_kernel[:, 1], c=ys, - marker='+', label='barycentric mapping') -pl.title("Bary. mapping (kernel)") - -pl.subplot(2, 2, 4) -pl.scatter(xt[:, 0], xt[:, 1], c=yt, marker='o', - label='Target samples', alpha=.2) -pl.scatter(xst_kernel[:, 0], xst_kernel[:, 1], c=ys, - marker='+', label='Learned mapping') -pl.title("Estim. mapping (kernel)") -pl.tight_layout() - -pl.show() diff --git a/examples/plot_OTDA_mapping_color_images.py b/examples/plot_OTDA_mapping_color_images.py deleted file mode 100644 index 8064b25..0000000 --- a/examples/plot_OTDA_mapping_color_images.py +++ /dev/null @@ -1,169 +0,0 @@ -# -*- coding: utf-8 -*- -""" -==================================================================================== -OT for domain adaptation with image color adaptation [6] with mapping estimation [8] -==================================================================================== - -[6] Ferradans, S., Papadakis, N., Peyre, G., & Aujol, J. F. (2014). Regularized - discrete optimal transport. SIAM Journal on Imaging Sciences, 7(3), 1853-1882. -[8] M. Perrot, N. Courty, R. Flamary, A. Habrard, "Mapping estimation for - discrete optimal transport", Neural Information Processing Systems (NIPS), 2016. - -""" - -# Author: Remi Flamary -# -# License: MIT License - -import numpy as np -from scipy import ndimage -import matplotlib.pylab as pl -import ot - - -#%% Loading images - -I1 = ndimage.imread('../data/ocean_day.jpg').astype(np.float64) / 256 -I2 = ndimage.imread('../data/ocean_sunset.jpg').astype(np.float64) / 256 - -#%% Plot images - -pl.figure(1, figsize=(6.4, 3)) -pl.subplot(1, 2, 1) -pl.imshow(I1) -pl.axis('off') -pl.title('Image 1') - -pl.subplot(1, 2, 2) -pl.imshow(I2) -pl.axis('off') -pl.title('Image 2') -pl.tight_layout() - - -#%% Image conversion and dataset generation - -def im2mat(I): - """Converts and image to matrix (one pixel per line)""" - return I.reshape((I.shape[0] * I.shape[1], I.shape[2])) - - -def mat2im(X, shape): - """Converts back a matrix to an image""" - return X.reshape(shape) - - -X1 = im2mat(I1) -X2 = im2mat(I2) - -# training samples -nb = 1000 -idx1 = np.random.randint(X1.shape[0], size=(nb,)) -idx2 = np.random.randint(X2.shape[0], size=(nb,)) - -xs = X1[idx1, :] -xt = X2[idx2, :] - -#%% Plot image distributions - - -pl.figure(2, figsize=(6.4, 5)) - -pl.subplot(1, 2, 1) -pl.scatter(xs[:, 0], xs[:, 2], c=xs) -pl.axis([0, 1, 0, 1]) -pl.xlabel('Red') -pl.ylabel('Blue') -pl.title('Image 1') - -pl.subplot(1, 2, 2) -pl.scatter(xt[:, 0], xt[:, 2], c=xt) -pl.axis([0, 1, 0, 1]) -pl.xlabel('Red') -pl.ylabel('Blue') -pl.title('Image 2') -pl.tight_layout() - - -#%% domain adaptation between images - -def minmax(I): - return np.clip(I, 0, 1) - - -# LP problem -da_emd = ot.da.OTDA() # init class -da_emd.fit(xs, xt) # fit distributions - -X1t = da_emd.predict(X1) # out of sample -I1t = minmax(mat2im(X1t, I1.shape)) - -# sinkhorn regularization -lambd = 1e-1 -da_entrop = ot.da.OTDA_sinkhorn() -da_entrop.fit(xs, xt, reg=lambd) - -X1te = da_entrop.predict(X1) -I1te = minmax(mat2im(X1te, I1.shape)) - -# linear mapping estimation -eta = 1e-8 # quadratic regularization for regression -mu = 1e0 # weight of the OT linear term -bias = True # estimate a bias - -ot_mapping = ot.da.OTDA_mapping_linear() -ot_mapping.fit(xs, xt, mu=mu, eta=eta, bias=bias, numItermax=20, verbose=True) - -X1tl = ot_mapping.predict(X1) # use the estimated mapping -I1tl = minmax(mat2im(X1tl, I1.shape)) - -# nonlinear mapping estimation -eta = 1e-2 # quadratic regularization for regression -mu = 1e0 # weight of the OT linear term -bias = False # estimate a bias -sigma = 1 # sigma bandwidth fot gaussian kernel - - -ot_mapping_kernel = ot.da.OTDA_mapping_kernel() -ot_mapping_kernel.fit( - xs, xt, mu=mu, eta=eta, sigma=sigma, bias=bias, numItermax=10, verbose=True) - -X1tn = ot_mapping_kernel.predict(X1) # use the estimated mapping -I1tn = minmax(mat2im(X1tn, I1.shape)) - -#%% plot images - -pl.figure(2, figsize=(8, 4)) - -pl.subplot(2, 3, 1) -pl.imshow(I1) -pl.axis('off') -pl.title('Im. 1') - -pl.subplot(2, 3, 2) -pl.imshow(I2) -pl.axis('off') -pl.title('Im. 2') - -pl.subplot(2, 3, 3) -pl.imshow(I1t) -pl.axis('off') -pl.title('Im. 1 Interp LP') - -pl.subplot(2, 3, 4) -pl.imshow(I1te) -pl.axis('off') -pl.title('Im. 1 Interp Entrop') - -pl.subplot(2, 3, 5) -pl.imshow(I1tl) -pl.axis('off') -pl.title('Im. 1 Linear mapping') - -pl.subplot(2, 3, 6) -pl.imshow(I1tn) -pl.axis('off') -pl.title('Im. 1 nonlinear mapping') -pl.tight_layout() - -pl.show() diff --git a/ot/da.py b/ot/da.py index 8c62669..369b6a2 100644 --- a/ot/da.py +++ b/ot/da.py @@ -976,36 +976,41 @@ class BaseTransport(BaseEstimator): Returns self. """ - # pairwise distance - self.cost_ = dist(Xs, Xt, metric=self.metric) + if Xs is not None and Xt is not None: + # pairwise distance + self.cost_ = dist(Xs, Xt, metric=self.metric) - if (ys is not None) and (yt is not None): + 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_) + 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 = np.unique(ys) - for c in classes: - idx_s = np.where((ys != c) & (ys != -1)) - idx_t = np.where(yt == c) + # assumes labeled source samples occupy the first rows + # and labeled target samples occupy the first columns + classes = np.unique(ys) + 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 + # 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) + # 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 + # store arrays of samples + self.Xs = Xs + self.Xt = Xt - return self + return self + else: + print("POT-Warning") + print("Please provide both Xs and Xt arguments when calling") + print("fit method") def fit_transform(self, Xs=None, ys=None, Xt=None, yt=None): """Build a coupling matrix from source and target sets of samples @@ -1053,42 +1058,47 @@ class BaseTransport(BaseEstimator): The transport source samples. """ - if np.array_equal(self.Xs, Xs): - # perform standard barycentric mapping - transp = self.coupling_ / np.sum(self.coupling_, 1)[:, None] + if Xs is not None: + 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 + # 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)] + # 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: + 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) + # 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) + # 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, :] + # define the transported points + transp_Xs_ = transp_Xs_[idx, :] + Xs[bi] - self.Xs[idx, :] - transp_Xs.append(transp_Xs_) + transp_Xs.append(transp_Xs_) - transp_Xs = np.concatenate(transp_Xs, axis=0) + transp_Xs = np.concatenate(transp_Xs, axis=0) - return transp_Xs + return transp_Xs + else: + print("POT-Warning") + print("Please provide Xs argument when calling transform method") def inverse_transform(self, Xs=None, ys=None, Xt=None, yt=None, batch_size=128): @@ -1113,41 +1123,46 @@ class BaseTransport(BaseEstimator): The transported target samples. """ - if np.array_equal(self.Xt, Xt): - # perform standard barycentric mapping - transp_ = self.coupling_.T / np.sum(self.coupling_, 0)[:, None] + if Xt is not None: + 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 + # 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)] + # 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: + transp_Xt = [] + for bi in batch_ind: - D0 = dist(Xt[bi], self.Xt) - idx = np.argmin(D0, axis=1) + 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) + # 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, :] + # define the transported points + transp_Xt_ = transp_Xt_[idx, :] + Xt[bi] - self.Xt[idx, :] - transp_Xt.append(transp_Xt_) + transp_Xt.append(transp_Xt_) - transp_Xt = np.concatenate(transp_Xt, axis=0) + transp_Xt = np.concatenate(transp_Xt, axis=0) - return transp_Xt + return transp_Xt + else: + print("POT-Warning") + print("Please provide Xt argument when calling inverse_transform") class SinkhornTransport(BaseTransport): @@ -1413,15 +1428,20 @@ class SinkhornLpl1Transport(BaseTransport): Returns self. """ - super(SinkhornLpl1Transport, self).fit(Xs, ys, Xt, yt) + if Xs is not None and Xt is not None and ys is not None: - self.coupling_ = 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) + super(SinkhornLpl1Transport, self).fit(Xs, ys, Xt, yt) - return self + self.coupling_ = 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) + + return self + else: + print("POT-Warning") + print("Please provide both Xs, Xt, ys arguments to fit method") class SinkhornL1l2Transport(BaseTransport): @@ -1517,22 +1537,27 @@ class SinkhornL1l2Transport(BaseTransport): Returns self. """ - super(SinkhornL1l2Transport, self).fit(Xs, ys, Xt, yt) + if Xs is not None and Xt is not None and ys is not None: - 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) + super(SinkhornL1l2Transport, self).fit(Xs, ys, Xt, yt) - # deal with the value of log - if self.log: - self.coupling_, self.log_ = returned_ - else: - self.coupling_ = returned_ - self.log_ = dict() + 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) - return self + # deal with the value of log + if self.log: + self.coupling_, self.log_ = returned_ + else: + self.coupling_ = returned_ + self.log_ = dict() + + return self + else: + print("POT-Warning") + print("Please, provide both Xs, Xt and ys argument to fit method") class MappingTransport(BaseEstimator): -- cgit v1.2.3 From 84e56a0637f3194c5b1b160bd4f89ccd0ffe661d Mon Sep 17 00:00:00 2001 From: Slasnista Date: Mon, 28 Aug 2017 14:07:55 +0200 Subject: check input parameters with helper functions --- ot/da.py | 174 ++++++++++++++++++++++++++++++++++-------------------------- ot/utils.py | 21 ++++++++ 2 files changed, 120 insertions(+), 75 deletions(-) diff --git a/ot/da.py b/ot/da.py index 369b6a2..78dc150 100644 --- a/ot/da.py +++ b/ot/da.py @@ -14,7 +14,7 @@ import numpy as np from .bregman import sinkhorn from .lp import emd from .utils import unif, dist, kernel -from .utils import deprecated, BaseEstimator +from .utils import check_params, deprecated, BaseEstimator from .optim import cg from .optim import gcg @@ -954,6 +954,26 @@ def distribution_estimation_uniform(X): 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 @@ -976,7 +996,9 @@ class BaseTransport(BaseEstimator): Returns self. """ - if Xs is not None and Xt is not None: + # check the necessary inputs parameters are here + if check_params(Xs=Xs, Xt=Xt): + # pairwise distance self.cost_ = dist(Xs, Xt, metric=self.metric) @@ -1003,14 +1025,10 @@ class BaseTransport(BaseEstimator): self.mu_t = self.distribution_estimation(Xt) # store arrays of samples - self.Xs = Xs - self.Xt = Xt + self.xs_ = Xs + self.xt_ = Xt - return self - else: - print("POT-Warning") - print("Please provide both Xs and Xt arguments when calling") - print("fit method") + 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 @@ -1058,8 +1076,11 @@ class BaseTransport(BaseEstimator): The transport source samples. """ - if Xs is not None: - if np.array_equal(self.Xs, Xs): + # 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] @@ -1067,7 +1088,7 @@ class BaseTransport(BaseEstimator): transp[~ np.isfinite(transp)] = 0 # compute transported samples - transp_Xs = np.dot(transp, self.Xt) + transp_Xs = np.dot(transp, self.xt_) else: # perform out of sample mapping indices = np.arange(Xs.shape[0]) @@ -1079,26 +1100,23 @@ class BaseTransport(BaseEstimator): for bi in batch_ind: # get the nearest neighbor in the source domain - D0 = dist(Xs[bi], self.Xs) + 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) + transp_Xs_ = np.dot(transp, self.xt_) # define the transported points - transp_Xs_ = transp_Xs_[idx, :] + Xs[bi] - self.Xs[idx, :] + 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 - else: - print("POT-Warning") - print("Please provide Xs argument when calling transform method") def inverse_transform(self, Xs=None, ys=None, Xt=None, yt=None, batch_size=128): @@ -1123,8 +1141,11 @@ class BaseTransport(BaseEstimator): The transported target samples. """ - if Xt is not None: - if np.array_equal(self.Xt, Xt): + # 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] @@ -1132,7 +1153,7 @@ class BaseTransport(BaseEstimator): transp_[~ np.isfinite(transp_)] = 0 # compute transported samples - transp_Xt = np.dot(transp_, self.Xs) + transp_Xt = np.dot(transp_, self.xs_) else: # perform out of sample mapping indices = np.arange(Xt.shape[0]) @@ -1143,26 +1164,23 @@ class BaseTransport(BaseEstimator): transp_Xt = [] for bi in batch_ind: - D0 = dist(Xt[bi], self.Xt) + 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) + transp_Xt_ = np.dot(transp_, self.xs_) # define the transported points - transp_Xt_ = transp_Xt_[idx, :] + Xt[bi] - self.Xt[idx, :] + 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 - else: - print("POT-Warning") - print("Please provide Xt argument when calling inverse_transform") class SinkhornTransport(BaseTransport): @@ -1428,7 +1446,8 @@ class SinkhornLpl1Transport(BaseTransport): Returns self. """ - if Xs is not None and Xt is not None and ys is not None: + # check the necessary inputs parameters are here + if check_params(Xs=Xs, Xt=Xt, ys=ys): super(SinkhornLpl1Transport, self).fit(Xs, ys, Xt, yt) @@ -1438,10 +1457,7 @@ class SinkhornLpl1Transport(BaseTransport): numInnerItermax=self.max_inner_iter, stopInnerThr=self.tol, verbose=self.verbose) - return self - else: - print("POT-Warning") - print("Please provide both Xs, Xt, ys arguments to fit method") + return self class SinkhornL1l2Transport(BaseTransport): @@ -1537,7 +1553,8 @@ class SinkhornL1l2Transport(BaseTransport): Returns self. """ - if Xs is not None and Xt is not None and ys is not None: + # check the necessary inputs parameters are here + if check_params(Xs=Xs, Xt=Xt, ys=ys): super(SinkhornL1l2Transport, self).fit(Xs, ys, Xt, yt) @@ -1554,10 +1571,7 @@ class SinkhornL1l2Transport(BaseTransport): self.coupling_ = returned_ self.log_ = dict() - return self - else: - print("POT-Warning") - print("Please, provide both Xs, Xt and ys argument to fit method") + return self class MappingTransport(BaseEstimator): @@ -1652,29 +1666,35 @@ class MappingTransport(BaseEstimator): Returns self """ - 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) + # 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) - 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() + # 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 @@ -1692,22 +1712,26 @@ class MappingTransport(BaseEstimator): The transport source samples. """ - if np.array_equal(self.Xs, Xs): - # perform standard barycentric mapping - transp = self.coupling_ / np.sum(self.coupling_, 1)[:, None] + # check the necessary inputs parameters are here + if check_params(Xs=Xs): - # set nans to 0 - transp[~ np.isfinite(transp)] = 0 + if np.array_equal(self.xs_, Xs): + # perform standard barycentric mapping + transp = self.coupling_ / np.sum(self.coupling_, 1)[:, None] - # 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_) + # set nans to 0 + transp[~ np.isfinite(transp)] = 0 - return transp_Xs + # 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 diff --git a/ot/utils.py b/ot/utils.py index 29ad536..01f2a67 100644 --- a/ot/utils.py +++ b/ot/utils.py @@ -168,6 +168,27 @@ def parmap(f, X, nprocs=multiprocessing.cpu_count()): return [x for i, x in sorted(res)] +def check_params(**kwargs): + """check_params: check whether some parameters are missing + """ + + missing_params = [] + check = True + + for param in kwargs: + if kwargs[param] is None: + missing_params.append(param) + + if len(missing_params) > 0: + print("POT - Warning: following necessary parameters are missing") + for p in missing_params: + print("\n", p) + + check = False + + return check + + class deprecated(object): """Decorator to mark a function or class as deprecated. -- cgit v1.2.3 From 59640017434427bac54d9eb668a1d7fc862ccdce Mon Sep 17 00:00:00 2001 From: Slasnista Date: Mon, 28 Aug 2017 14:08:03 +0200 Subject: update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 27b4643..33eea6e 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ The contributors to this library are: * [Laetitia Chapel](http://people.irisa.fr/Laetitia.Chapel/) * [Michael Perrot](http://perso.univ-st-etienne.fr/pem82055/) (Mapping estimation) * [Léo Gautheron](https://github.com/aje) (GPU implementation) -* [Nathalie Gayraud]() +* [Nathalie Gayraud](https://www.linkedin.com/in/nathalie-t-h-gayraud/?ppe=1) * [Stanislas Chambon](https://slasnista.github.io/) This toolbox benefit a lot from open source research and we would like to thank the following persons for providing some code (in various languages): -- cgit v1.2.3 From 24362ecde2a64353e568d3980a52ea5ddfdbe930 Mon Sep 17 00:00:00 2001 From: Nicolas Courty Date: Mon, 28 Aug 2017 14:41:09 +0200 Subject: Gromov-Wasserstein distance --- README.md | 4 +- examples/plot_gromov.py | 98 ++++++++++ ot/__init__.py | 6 +- ot/gromov.py | 482 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 588 insertions(+), 2 deletions(-) create mode 100644 examples/plot_gromov.py create mode 100644 ot/gromov.py diff --git a/README.md b/README.md index 33eea6e..257244b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ It provides the following solvers: * Conditional gradient [6] and Generalized conditional gradient for regularized OT [7]. * Joint OT matrix and mapping estimation [8]. * Wasserstein Discriminant Analysis [11] (requires autograd + pymanopt). - +* Gromov-Wasserstein distances [12] Some demonstrations (both in Python and Jupyter Notebook format) are available in the examples folder. @@ -184,3 +184,5 @@ You can also post bug reports and feature requests in Github issues. Make sure t [10] Chizat, L., Peyré, G., Schmitzer, B., & Vialard, F. X. (2016). [Scaling algorithms for unbalanced transport problems](https://arxiv.org/pdf/1607.05816.pdf). arXiv preprint arXiv:1607.05816. [11] Flamary, R., Cuturi, M., Courty, N., & Rakotomamonjy, A. (2016). [Wasserstein Discriminant Analysis](https://arxiv.org/pdf/1608.08063.pdf). arXiv preprint arXiv:1608.08063. + +[12] Peyré, Gabriel, Marco Cuturi, and Justin Solomon, [Gromov-Wasserstein averaging of kernel and distance matrices](http://proceedings.mlr.press/v48/peyre16.html) International Conference on Machine Learning (ICML). 2016. diff --git a/examples/plot_gromov.py b/examples/plot_gromov.py new file mode 100644 index 0000000..11e5336 --- /dev/null +++ b/examples/plot_gromov.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +""" +==================== +Gromov-Wasserstein example +==================== + +This example is designed to show how to use the Gromov-Wassertsein distance +computation in POT. + + +""" + +# Author: Erwan Vautier +# Nicolas Courty +# +# License: MIT License + +import scipy as sp +import numpy as np + +import ot +import matplotlib.pylab as pl +from mpl_toolkits.mplot3d import Axes3D + + + +""" +Sample two Gaussian distributions (2D and 3D) +==================== + +The Gromov-Wasserstein distance allows to compute distances with samples that do not belong to the same metric space. For +demonstration purpose, we sample two Gaussian distributions in 2- and 3-dimensional spaces. + +""" +n=30 # nb samples + +mu_s=np.array([0,0]) +cov_s=np.array([[1,0],[0,1]]) + +mu_t=np.array([4,4,4]) +cov_t=np.array([[1,0,0],[0,1,0],[0,0,1]]) + + + +xs=ot.datasets.get_2D_samples_gauss(n,mu_s,cov_s) +P=sp.linalg.sqrtm(cov_t) +xt= np.random.randn(n,3).dot(P)+mu_t + + + +""" +Plotting the distributions +==================== +""" +fig=pl.figure() +ax1=fig.add_subplot(121) +ax1.plot(xs[:,0],xs[:,1],'+b',label='Source samples') +ax2=fig.add_subplot(122,projection='3d') +ax2.scatter(xt[:,0],xt[:,1],xt[:,2],color='r') +pl.show() + + +""" +Compute distance kernels, normalize them and then display +==================== +""" + +C1=sp.spatial.distance.cdist(xs,xs) +C2=sp.spatial.distance.cdist(xt,xt) + +C1/=C1.max() +C2/=C2.max() + +pl.figure() +pl.subplot(121) +pl.imshow(C1) +pl.subplot(122) +pl.imshow(C2) +pl.show() + +""" +Compute Gromov-Wasserstein plans and distance +==================== +""" + +p=ot.unif(n) +q=ot.unif(n) + +gw=ot.gromov_wasserstein(C1,C2,p,q,'square_loss',epsilon=5e-4) +gw_dist=ot.gromov_wasserstein2(C1,C2,p,q,'square_loss',epsilon=5e-4) + +print('Gromov-Wasserstein distances between the distribution: '+str(gw_dist)) + +pl.figure() +pl.imshow(gw,cmap='jet') +pl.colorbar() +pl.show() + diff --git a/ot/__init__.py b/ot/__init__.py index 6d4c4c6..a295e1b 100644 --- a/ot/__init__.py +++ b/ot/__init__.py @@ -5,6 +5,7 @@ """ # Author: Remi Flamary +# Nicolas Courty # # License: MIT License @@ -17,11 +18,13 @@ from . import utils from . import datasets from . import plot from . import da +from . import gromov # OT functions from .lp import emd, emd2 from .bregman import sinkhorn, sinkhorn2, barycenter from .da import sinkhorn_lpl1_mm +from .gromov import gromov_wasserstein, gromov_wasserstein2 # utils functions from .utils import dist, unif, tic, toc, toq @@ -30,4 +33,5 @@ __version__ = "0.3.1" __all__ = ["emd", "emd2", "sinkhorn", "sinkhorn2", "utils", 'datasets', 'bregman', 'lp', 'plot', 'tic', 'toc', 'toq', - 'dist', 'unif', 'barycenter', 'sinkhorn_lpl1_mm', 'da', 'optim'] + 'dist', 'unif', 'barycenter', 'sinkhorn_lpl1_mm', 'da', 'optim', + 'gromov_wasserstein','gromov_wasserstein2'] diff --git a/ot/gromov.py b/ot/gromov.py new file mode 100644 index 0000000..f3c62c9 --- /dev/null +++ b/ot/gromov.py @@ -0,0 +1,482 @@ + +# -*- coding: utf-8 -*- +""" +Gromov-Wasserstein transport method +=================================== + + +""" + +# Author: Erwan Vautier +# Nicolas Courty +# +# License: MIT License + +import numpy as np + +from .bregman import sinkhorn +from .utils import dist + +def square_loss(a,b): + """ + Returns the value of L(a,b)=(1/2)*|a-b|^2 + """ + + return (1/2)*(a-b)**2 + +def kl_loss(a,b): + """ + Returns the value of L(a,b)=a*log(a/b)-a+b + """ + + return a*np.log(a/b)-a+b + +def tensor_square_loss(C1,C2,T): + """ + Returns the value of \mathcal{L}(C1,C2) \otimes T with the square loss + + function as the loss function of Gromow-Wasserstein discrepancy. + + Where : + + C1 : Metric cost matrix in the source space + C2 : Metric cost matrix in the target space + T : A coupling between those two spaces + + The square-loss function L(a,b)=(1/2)*|a-b|^2 is read as : + L(a,b) = f1(a)+f2(b)-h1(a)*h2(b) with : + f1(a)=(a^2)/2 + f2(b)=(b^2)/2 + h1(a)=a + h2(b)=b + + Parameters + ---------- + C1 : np.ndarray(ns,ns) + Metric cost matrix in the source space + C2 : np.ndarray(nt,nt) + Metric costfr matrix in the target space + T : np.ndarray(ns,nt) + Coupling between source and target spaces + + + Returns + ------- + tens : (ns*nt) ndarray + \mathcal{L}(C1,C2) \otimes T tensor-matrix multiplication result + + + """ + + + C1=np.asarray(C1,dtype=np.float64) + C2=np.asarray(C2,dtype=np.float64) + T=np.asarray(T,dtype=np.float64) + + + def f1(a): + return (a**2)/2 + + def f2(b): + return (b**2)/2 + + def h1(a): + return a + + def h2(b): + return b + + tens=-np.dot(h1(C1),T).dot(h2(C2).T) + tens=tens-tens.min() + + + return np.array(tens) + + +def tensor_kl_loss(C1,C2,T): + """ + Returns the value of \mathcal{L}(C1,C2) \otimes T with the square loss + + function as the loss function of Gromow-Wasserstein discrepancy. + + Where : + + C1 : Metric cost matrix in the source space + C2 : Metric cost matrix in the target space + T : A coupling between those two spaces + + The square-loss function L(a,b)=(1/2)*|a-b|^2 is read as : + L(a,b) = f1(a)+f2(b)-h1(a)*h2(b) with : + f1(a)=a*log(a)-a + f2(b)=b + h1(a)=a + h2(b)=log(b) + + Parameters + ---------- + C1 : np.ndarray(ns,ns) + Metric cost matrix in the source space + C2 : np.ndarray(nt,nt) + Metric costfr matrix in the target space + T : np.ndarray(ns,nt) + Coupling between source and target spaces + + + Returns + ------- + tens : (ns*nt) ndarray + \mathcal{L}(C1,C2) \otimes T tensor-matrix multiplication result + + References + ---------- + + .. [12] Peyré, Gabriel, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. + + """ + + + C1=np.asarray(C1,dtype=np.float64) + C2=np.asarray(C2,dtype=np.float64) + T=np.asarray(T,dtype=np.float64) + + + def f1(a): + return a*np.log(a+1e-15)-a + + def f2(b): + return b + + def h1(a): + return a + + def h2(b): + return np.log(b+1e-15) + + tens=-np.dot(h1(C1),T).dot(h2(C2).T) + tens=tens-tens.min() + + + + return np.array(tens) + +def update_square_loss(p,lambdas,T,Cs): + """ + Updates C according to the L2 Loss kernel with the S Ts couplings calculated at each iteration + + + Parameters + ---------- + p : np.ndarray(N,) + weights in the targeted barycenter + lambdas : list of the S spaces' weights + T : list of S np.ndarray(ns,N) + the S Ts couplings calculated at each iteration + Cs : Cs : list of S np.ndarray(ns,ns) + Metric cost matrices + + Returns + ---------- + C updated + + + """ + tmpsum=np.sum([lambdas[s]*np.dot(T[s].T,Cs[s]).dot(T[s]) for s in range(len(T))]) + ppt=np.dot(p,p.T) + + return(np.divide(tmpsum,ppt)) + +def update_kl_loss(p,lambdas,T,Cs): + """ + Updates C according to the KL Loss kernel with the S Ts couplings calculated at each iteration + + + Parameters + ---------- + p : np.ndarray(N,) + weights in the targeted barycenter + lambdas : list of the S spaces' weights + T : list of S np.ndarray(ns,N) + the S Ts couplings calculated at each iteration + Cs : Cs : list of S np.ndarray(ns,ns) + Metric cost matrices + + Returns + ---------- + C updated + + + """ + tmpsum=np.sum([lambdas[s]*np.dot(T[s].T,Cs[s]).dot(T[s]) for s in range(len(T))]) + ppt=np.dot(p,p.T) + + return(np.exp(np.divide(tmpsum,ppt))) + + +def gromov_wasserstein(C1,C2,p,q,loss_fun,epsilon,numItermax = 1000, stopThr=1e-9,verbose=False, log=False): + """ + Returns the gromov-wasserstein coupling between the two measured similarity matrices + + (C1,p) and (C2,q) + + The function solves the following optimization problem: + + .. math:: + \GW = arg\min_T \sum_{i,j,k,l} L(C1_{i,k},C2_{j,l})*T_{i,j}*T_{k,l}-\epsilon(H(T)) + + s.t. \GW 1 = p + + \GW^T 1= q + + \GW\geq 0 + + Where : + + C1 : Metric cost matrix in the source space + C2 : Metric cost matrix in the target space + p : distribution in the source space + q : distribution in the target space + L : loss function to account for the misfit between the similarity matrices + H : entropy + + + Parameters + ---------- + C1 : np.ndarray(ns,ns) + Metric cost matrix in the source space + C2 : np.ndarray(nt,nt) + Metric costfr matrix in the target space + p : np.ndarray(ns,) + distribution in the source space + q : np.ndarray(nt) + distribution in the target space + loss_fun : loss function used for the solver either 'square_loss' or 'kl_loss' + epsilon : float + Regularization term >0 + numItermax : int, optional + Max number of iterations + stopThr : float, optional + Stop threshold on error (>0) + verbose : bool, optional + Print information along iterations + log : bool, optional + record log if True + forcing : np.ndarray(N,2) + list of forced couplings (where N is the number of forcing) + + Returns + ------- + T : coupling between the two spaces that minimizes : + \sum_{i,j,k,l} L(C1_{i,k},C2_{j,l})*T_{i,j}*T_{k,l}-\epsilon(H(T)) + + """ + + + C1=np.asarray(C1,dtype=np.float64) + C2=np.asarray(C2,dtype=np.float64) + + T=np.dot(p,q.T) #Initialization + + cpt = 0 + err=1 + + while (err>stopThr and cpt0 + numItermax : int, optional + Max number of iterations + stopThr : float, optional + Stop threshold on error (>0) + verbose : bool, optional + Print information along iterations + log : bool, optional + record log if True + forcing : np.ndarray(N,2) + list of forced couplings (where N is the number of forcing) + + Returns + ------- + T : coupling between the two spaces that minimizes : + \sum_{i,j,k,l} L(C1_{i,k},C2_{j,l})*T_{i,j}*T_{k,l}-\epsilon(H(T)) + + """ + + if log: + gw,logv=gromov_wasserstein(C1,C2,p,q,loss_fun,epsilon,numItermax,stopThr,verbose,log) + else: + gw=gromov_wasserstein(C1,C2,p,q,loss_fun,epsilon,numItermax,stopThr,verbose,log) + + if loss_fun=='square_loss': + gw_dist=np.sum(gw*tensor_square_loss(C1,C2,gw)) + + + elif loss_fun=='kl_loss': + gw_dist=np.sum(gw*tensor_kl_loss(C1,C2,gw)) + + if log: + return gw_dist, logv + else: + return gw_dist + + +def gromov_barycenters(N,Cs,ps,p,lambdas,loss_fun,epsilon,numItermax = 1000, stopThr=1e-9, verbose=False, log=False): + """ + Returns the gromov-wasserstein barycenters of S measured similarity matrices + + (Cs)_{s=1}^{s=S} + + The function solves the following optimization problem: + + .. math:: + C = argmin_C\in R^NxN \sum_s \lambda_s GW(C,Cs,p,ps) + + + Where : + + Cs : metric cost matrix + ps : distribution + + Parameters + ---------- + N : Integer + Size of the targeted barycenter + Cs : list of S np.ndarray(ns,ns) + Metric cost matrices + ps : list of S np.ndarray(ns,) + sample weights in the S spaces + p : np.ndarray(N,) + weights in the targeted barycenter + lambdas : list of the S spaces' weights + L : tensor-matrix multiplication function based on specific loss function + update : function(p,lambdas,T,Cs) that updates C according to a specific Kernel + with the S Ts couplings calculated at each iteration + epsilon : float + Regularization term >0 + numItermax : int, optional + Max number of iterations + stopThr : float, optional + Stop threshol on error (>0) + verbose : bool, optional + Print information along iterations + log : bool, optional + record log if True + + Returns + ------- + C : Similarity matrix in the barycenter space (permutated arbitrarily) + + """ + + + S=len(Cs) + + Cs=[np.asarray(Cs[s],dtype=np.float64) for s in range(S)] + lambdas=np.asarray(lambdas,dtype=np.float64) + + T=[0 for s in range(S)] + + #Initialization of C : random SPD matrix + xalea=np.random.randn(N,2) + C=dist(xalea,xalea) + C/=C.max() + + cpt=0 + err=1 + + error=[] + + while(err>stopThr and cpt Date: Mon, 28 Aug 2017 15:04:04 +0200 Subject: gromov:flake8 and other --- examples/plot_gromov.py | 63 ++++---- ot/gromov.py | 382 ++++++++++++++++++++++++------------------------ 2 files changed, 216 insertions(+), 229 deletions(-) diff --git a/examples/plot_gromov.py b/examples/plot_gromov.py index 11e5336..a33fde1 100644 --- a/examples/plot_gromov.py +++ b/examples/plot_gromov.py @@ -3,11 +3,8 @@ ==================== Gromov-Wasserstein example ==================== - -This example is designed to show how to use the Gromov-Wassertsein distance -computation in POT. - - +This example is designed to show how to use the Gromov-Wassertsein distance +computation in POT. """ # Author: Erwan Vautier @@ -20,43 +17,38 @@ import numpy as np import ot import matplotlib.pylab as pl -from mpl_toolkits.mplot3d import Axes3D - """ Sample two Gaussian distributions (2D and 3D) ==================== - -The Gromov-Wasserstein distance allows to compute distances with samples that do not belong to the same metric space. For -demonstration purpose, we sample two Gaussian distributions in 2- and 3-dimensional spaces. - +The Gromov-Wasserstein distance allows to compute distances with samples that do not belong to the same metric space. +For demonstration purpose, we sample two Gaussian distributions in 2- and 3-dimensional spaces. """ -n=30 # nb samples -mu_s=np.array([0,0]) -cov_s=np.array([[1,0],[0,1]]) +n = 30 # nb samples -mu_t=np.array([4,4,4]) -cov_t=np.array([[1,0,0],[0,1,0],[0,0,1]]) +mu_s = np.array([0, 0]) +cov_s = np.array([[1, 0], [0, 1]]) +mu_t = np.array([4, 4, 4]) +cov_t = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) -xs=ot.datasets.get_2D_samples_gauss(n,mu_s,cov_s) -P=sp.linalg.sqrtm(cov_t) -xt= np.random.randn(n,3).dot(P)+mu_t - +xs = ot.datasets.get_2D_samples_gauss(n, mu_s, cov_s) +P = sp.linalg.sqrtm(cov_t) +xt = np.random.randn(n, 3).dot(P) + mu_t """ Plotting the distributions ==================== """ -fig=pl.figure() -ax1=fig.add_subplot(121) -ax1.plot(xs[:,0],xs[:,1],'+b',label='Source samples') -ax2=fig.add_subplot(122,projection='3d') -ax2.scatter(xt[:,0],xt[:,1],xt[:,2],color='r') +fig = pl.figure() +ax1 = fig.add_subplot(121) +ax1.plot(xs[:, 0], xs[:, 1], '+b', label='Source samples') +ax2 = fig.add_subplot(122, projection='3d') +ax2.scatter(xt[:, 0], xt[:, 1], xt[:, 2], color='r') pl.show() @@ -65,11 +57,11 @@ Compute distance kernels, normalize them and then display ==================== """ -C1=sp.spatial.distance.cdist(xs,xs) -C2=sp.spatial.distance.cdist(xt,xt) +C1 = sp.spatial.distance.cdist(xs, xs) +C2 = sp.spatial.distance.cdist(xt, xt) -C1/=C1.max() -C2/=C2.max() +C1 /= C1.max() +C2 /= C2.max() pl.figure() pl.subplot(121) @@ -83,16 +75,15 @@ Compute Gromov-Wasserstein plans and distance ==================== """ -p=ot.unif(n) -q=ot.unif(n) +p = ot.unif(n) +q = ot.unif(n) -gw=ot.gromov_wasserstein(C1,C2,p,q,'square_loss',epsilon=5e-4) -gw_dist=ot.gromov_wasserstein2(C1,C2,p,q,'square_loss',epsilon=5e-4) +gw = ot.gromov_wasserstein(C1, C2, p, q, 'square_loss', epsilon=5e-4) +gw_dist = ot.gromov_wasserstein2(C1, C2, p, q, 'square_loss', epsilon=5e-4) -print('Gromov-Wasserstein distances between the distribution: '+str(gw_dist)) +print('Gromov-Wasserstein distances between the distribution: ' + str(gw_dist)) pl.figure() -pl.imshow(gw,cmap='jet') +pl.imshow(gw, cmap='jet') pl.colorbar() pl.show() - diff --git a/ot/gromov.py b/ot/gromov.py index f3c62c9..7cf3b42 100644 --- a/ot/gromov.py +++ b/ot/gromov.py @@ -17,39 +17,41 @@ import numpy as np from .bregman import sinkhorn from .utils import dist -def square_loss(a,b): + +def square_loss(a, b): """ Returns the value of L(a,b)=(1/2)*|a-b|^2 """ - - return (1/2)*(a-b)**2 -def kl_loss(a,b): + return (1 / 2) * (a - b)**2 + + +def kl_loss(a, b): """ Returns the value of L(a,b)=a*log(a/b)-a+b """ - - return a*np.log(a/b)-a+b -def tensor_square_loss(C1,C2,T): + return a * np.log(a / b) - a + b + + +def tensor_square_loss(C1, C2, T): """ - Returns the value of \mathcal{L}(C1,C2) \otimes T with the square loss - + Returns the value of \mathcal{L}(C1,C2) \otimes T with the square loss function as the loss function of Gromow-Wasserstein discrepancy. - + Where : - + C1 : Metric cost matrix in the source space C2 : Metric cost matrix in the target space T : A coupling between those two spaces - + The square-loss function L(a,b)=(1/2)*|a-b|^2 is read as : L(a,b) = f1(a)+f2(b)-h1(a)*h2(b) with : f1(a)=(a^2)/2 f2(b)=(b^2)/2 h1(a)=a h2(b)=b - + Parameters ---------- C1 : np.ndarray(ns,ns) @@ -64,54 +66,50 @@ def tensor_square_loss(C1,C2,T): ------- tens : (ns*nt) ndarray \mathcal{L}(C1,C2) \otimes T tensor-matrix multiplication result - - + + """ - - C1=np.asarray(C1,dtype=np.float64) - C2=np.asarray(C2,dtype=np.float64) - T=np.asarray(T,dtype=np.float64) - - + C1 = np.asarray(C1, dtype=np.float64) + C2 = np.asarray(C2, dtype=np.float64) + T = np.asarray(T, dtype=np.float64) + def f1(a): - return (a**2)/2 - + return (a**2) / 2 + def f2(b): - return (b**2)/2 - + return (b**2) / 2 + def h1(a): return a - + def h2(b): return b - - tens=-np.dot(h1(C1),T).dot(h2(C2).T) - tens=tens-tens.min() - + tens = -np.dot(h1(C1), T).dot(h2(C2).T) + tens = tens - tens.min() + return np.array(tens) - - -def tensor_kl_loss(C1,C2,T): + + +def tensor_kl_loss(C1, C2, T): """ - Returns the value of \mathcal{L}(C1,C2) \otimes T with the square loss - + Returns the value of \mathcal{L}(C1,C2) \otimes T with the square loss function as the loss function of Gromow-Wasserstein discrepancy. - + Where : - + C1 : Metric cost matrix in the source space C2 : Metric cost matrix in the target space T : A coupling between those two spaces - + The square-loss function L(a,b)=(1/2)*|a-b|^2 is read as : L(a,b) = f1(a)+f2(b)-h1(a)*h2(b) with : f1(a)=a*log(a)-a f2(b)=b h1(a)=a h2(b)=log(b) - + Parameters ---------- C1 : np.ndarray(ns,ns) @@ -126,44 +124,41 @@ def tensor_kl_loss(C1,C2,T): ------- tens : (ns*nt) ndarray \mathcal{L}(C1,C2) \otimes T tensor-matrix multiplication result - + References ---------- - + .. [12] Peyré, Gabriel, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. - + """ - - C1=np.asarray(C1,dtype=np.float64) - C2=np.asarray(C2,dtype=np.float64) - T=np.asarray(T,dtype=np.float64) - - + C1 = np.asarray(C1, dtype=np.float64) + C2 = np.asarray(C2, dtype=np.float64) + T = np.asarray(T, dtype=np.float64) + def f1(a): - return a*np.log(a+1e-15)-a - + return a * np.log(a + 1e-15) - a + def f2(b): return b - + def h1(a): return a - + def h2(b): - return np.log(b+1e-15) - - tens=-np.dot(h1(C1),T).dot(h2(C2).T) - tens=tens-tens.min() + return np.log(b + 1e-15) + + tens = -np.dot(h1(C1), T).dot(h2(C2).T) + tens = tens - tens.min() - - return np.array(tens) -def update_square_loss(p,lambdas,T,Cs): + +def update_square_loss(p, lambdas, T, Cs): """ Updates C according to the L2 Loss kernel with the S Ts couplings calculated at each iteration - - + + Parameters ---------- p : np.ndarray(N,) @@ -173,23 +168,25 @@ def update_square_loss(p,lambdas,T,Cs): the S Ts couplings calculated at each iteration Cs : Cs : list of S np.ndarray(ns,ns) Metric cost matrices - - Returns + + Returns ---------- C updated - - + + """ - tmpsum=np.sum([lambdas[s]*np.dot(T[s].T,Cs[s]).dot(T[s]) for s in range(len(T))]) - ppt=np.dot(p,p.T) - - return(np.divide(tmpsum,ppt)) + tmpsum = np.sum([lambdas[s] * np.dot(T[s].T, Cs[s]).dot(T[s]) + for s in range(len(T))]) + ppt = np.dot(p, p.T) -def update_kl_loss(p,lambdas,T,Cs): + return(np.divide(tmpsum, ppt)) + + +def update_kl_loss(p, lambdas, T, Cs): """ Updates C according to the KL Loss kernel with the S Ts couplings calculated at each iteration - - + + Parameters ---------- p : np.ndarray(N,) @@ -199,25 +196,26 @@ def update_kl_loss(p,lambdas,T,Cs): the S Ts couplings calculated at each iteration Cs : Cs : list of S np.ndarray(ns,ns) Metric cost matrices - - Returns + + Returns ---------- C updated - - + + """ - tmpsum=np.sum([lambdas[s]*np.dot(T[s].T,Cs[s]).dot(T[s]) for s in range(len(T))]) - ppt=np.dot(p,p.T) - - return(np.exp(np.divide(tmpsum,ppt))) + tmpsum = np.sum([lambdas[s] * np.dot(T[s].T, Cs[s]).dot(T[s]) + for s in range(len(T))]) + ppt = np.dot(p, p.T) + + return(np.exp(np.divide(tmpsum, ppt))) -def gromov_wasserstein(C1,C2,p,q,loss_fun,epsilon,numItermax = 1000, stopThr=1e-9,verbose=False, log=False): +def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopThr=1e-9, verbose=False, log=False): """ Returns the gromov-wasserstein coupling between the two measured similarity matrices - + (C1,p) and (C2,q) - + The function solves the following optimization problem: .. math:: @@ -228,17 +226,17 @@ def gromov_wasserstein(C1,C2,p,q,loss_fun,epsilon,numItermax = 1000, stopThr=1e- \GW^T 1= q \GW\geq 0 - + Where : - + C1 : Metric cost matrix in the source space C2 : Metric cost matrix in the target space p : distribution in the source space - q : distribution in the target space + q : distribution in the target space L : loss function to account for the misfit between the similarity matrices H : entropy - - + + Parameters ---------- C1 : np.ndarray(ns,ns) @@ -259,80 +257,80 @@ def gromov_wasserstein(C1,C2,p,q,loss_fun,epsilon,numItermax = 1000, stopThr=1e- verbose : bool, optional Print information along iterations log : bool, optional - record log if True + record log if True forcing : np.ndarray(N,2) list of forced couplings (where N is the number of forcing) - + Returns ------- T : coupling between the two spaces that minimizes : \sum_{i,j,k,l} L(C1_{i,k},C2_{j,l})*T_{i,j}*T_{k,l}-\epsilon(H(T)) - + """ - - C1=np.asarray(C1,dtype=np.float64) - C2=np.asarray(C2,dtype=np.float64) + C1 = np.asarray(C1, dtype=np.float64) + C2 = np.asarray(C2, dtype=np.float64) + + T = np.dot(p, q.T) # Initialization - T=np.dot(p,q.T) #Initialization - cpt = 0 - err=1 - - while (err>stopThr and cpt stopThr and cpt < numItermax): + + Tprev = T + + if loss_fun == 'square_loss': + tens = tensor_square_loss(C1, C2, T) + + elif loss_fun == 'kl_loss': + tens = tensor_kl_loss(C1, C2, T) + + T = sinkhorn(p, q, tens, epsilon) + + if cpt % 10 == 0: # we can speed up the process by checking for the error only all the 10th iterations - err=np.linalg.norm(T-Tprev) - + err = np.linalg.norm(T - Tprev) + 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)) - - cpt=cpt+1 + if cpt % 200 == 0: + print('{:5s}|{:12s}'.format( + 'It.', 'Err') + '\n' + '-' * 19) + print('{:5d}|{:8e}|'.format(cpt, err)) + + cpt = cpt + 1 if log: - return T,log + return T, log else: return T -def gromov_wasserstein2(C1,C2,p,q,loss_fun,epsilon,numItermax = 1000, stopThr=1e-9,verbose=False, log=False): +def gromov_wasserstein2(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopThr=1e-9, verbose=False, log=False): """ Returns the gromov-wasserstein discrepancy between the two measured similarity matrices - + (C1,p) and (C2,q) - + The function solves the following optimization problem: .. math:: \GW_Dist = \min_T \sum_{i,j,k,l} L(C1_{i,k},C2_{j,l})*T_{i,j}*T_{k,l}-\epsilon(H(T)) - + Where : - + C1 : Metric cost matrix in the source space C2 : Metric cost matrix in the target space p : distribution in the source space - q : distribution in the target space + q : distribution in the target space L : loss function to account for the misfit between the similarity matrices H : entropy - - + + Parameters ---------- C1 : np.ndarray(ns,ns) @@ -353,55 +351,56 @@ def gromov_wasserstein2(C1,C2,p,q,loss_fun,epsilon,numItermax = 1000, stopThr=1e verbose : bool, optional Print information along iterations log : bool, optional - record log if True + record log if True forcing : np.ndarray(N,2) list of forced couplings (where N is the number of forcing) - + Returns ------- T : coupling between the two spaces that minimizes : \sum_{i,j,k,l} L(C1_{i,k},C2_{j,l})*T_{i,j}*T_{k,l}-\epsilon(H(T)) - + """ - + if log: - gw,logv=gromov_wasserstein(C1,C2,p,q,loss_fun,epsilon,numItermax,stopThr,verbose,log) + gw, logv = gromov_wasserstein( + C1, C2, p, q, loss_fun, epsilon, numItermax, stopThr, verbose, log) else: - gw=gromov_wasserstein(C1,C2,p,q,loss_fun,epsilon,numItermax,stopThr,verbose,log) + gw = gromov_wasserstein(C1, C2, p, q, loss_fun, + epsilon, numItermax, stopThr, verbose, log) + + if loss_fun == 'square_loss': + gw_dist = np.sum(gw * tensor_square_loss(C1, C2, gw)) - if loss_fun=='square_loss': - gw_dist=np.sum(gw*tensor_square_loss(C1,C2,gw)) - - - elif loss_fun=='kl_loss': - gw_dist=np.sum(gw*tensor_kl_loss(C1,C2,gw)) + elif loss_fun == 'kl_loss': + gw_dist = np.sum(gw * tensor_kl_loss(C1, C2, gw)) if log: return gw_dist, logv - else: + else: return gw_dist -def gromov_barycenters(N,Cs,ps,p,lambdas,loss_fun,epsilon,numItermax = 1000, stopThr=1e-9, verbose=False, log=False): +def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, numItermax=1000, stopThr=1e-9, verbose=False, log=False): """ Returns the gromov-wasserstein barycenters of S measured similarity matrices - + (Cs)_{s=1}^{s=S} - + The function solves the following optimization problem: .. math:: C = argmin_C\in R^NxN \sum_s \lambda_s GW(C,Cs,p,ps) - + Where : - + Cs : metric cost matrix ps : distribution - + Parameters ---------- - N : Integer + N : Integer Size of the targeted barycenter Cs : list of S np.ndarray(ns,ns) Metric cost matrices @@ -422,61 +421,58 @@ def gromov_barycenters(N,Cs,ps,p,lambdas,loss_fun,epsilon,numItermax = 1000, sto verbose : bool, optional Print information along iterations log : bool, optional - record log if True - + record log if True + Returns ------- C : Similarity matrix in the barycenter space (permutated arbitrarily) - + """ - - - S=len(Cs) - - Cs=[np.asarray(Cs[s],dtype=np.float64) for s in range(S)] - lambdas=np.asarray(lambdas,dtype=np.float64) - - T=[0 for s in range(S)] - - #Initialization of C : random SPD matrix - xalea=np.random.randn(N,2) - C=dist(xalea,xalea) - C/=C.max() - - cpt=0 - err=1 - - error=[] - - while(err>stopThr and cpt stopThr and cpt < numItermax): + + Cprev = C + + T = [gromov_wasserstein(Cs[s], C, ps[s], p, loss_fun, epsilon, + numItermax, 1e-5, verbose, log) for s in range(S)] + + if loss_fun == 'square_loss': + C = update_square_loss(p, lambdas, T, Cs) + + elif loss_fun == 'kl_loss': + C = update_kl_loss(p, lambdas, T, Cs) + + if cpt % 10 == 0: # we can speed up the process by checking for the error only all the 10th iterations - err=np.linalg.norm(C-Cprev) + err = np.linalg.norm(C - Cprev) error.append(err) - + 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)) - - cpt=cpt+1 - - return C - + if cpt % 200 == 0: + print('{:5s}|{:12s}'.format( + 'It.', 'Err') + '\n' + '-' * 19) + print('{:5d}|{:8e}|'.format(cpt, err)) + cpt = cpt + 1 - \ No newline at end of file + return C -- cgit v1.2.3 From 3730779200896ee7de533eb7c5d7fa19e09eeb25 Mon Sep 17 00:00:00 2001 From: Slasnista Date: Tue, 29 Aug 2017 09:05:01 +0200 Subject: addressed AG comments + adding random seed --- examples/da/plot_otda_classes.py | 2 ++ examples/da/plot_otda_color_images.py | 3 ++- examples/da/plot_otda_d2.py | 14 ++++++++------ examples/da/plot_otda_mapping.py | 14 +++++++------- examples/da/plot_otda_mapping_colors_images.py | 2 ++ 5 files changed, 21 insertions(+), 14 deletions(-) diff --git a/examples/da/plot_otda_classes.py b/examples/da/plot_otda_classes.py index e5c82fb..6870fa4 100644 --- a/examples/da/plot_otda_classes.py +++ b/examples/da/plot_otda_classes.py @@ -15,8 +15,10 @@ approaches currently supported in POT. # License: MIT License import matplotlib.pylab as pl +import numpy as np import ot +np.random.seed(42) # number of source and target points to generate ns = 150 diff --git a/examples/da/plot_otda_color_images.py b/examples/da/plot_otda_color_images.py index bca7350..805d0b0 100644 --- a/examples/da/plot_otda_color_images.py +++ b/examples/da/plot_otda_color_images.py @@ -20,9 +20,10 @@ SIAM Journal on Imaging Sciences, 7(3), 1853-1882. import numpy as np from scipy import ndimage import matplotlib.pylab as pl - import ot +np.random.seed(42) + def im2mat(I): """Converts and image to matrix (one pixel per line)""" diff --git a/examples/da/plot_otda_d2.py b/examples/da/plot_otda_d2.py index 1d2192f..8833eb2 100644 --- a/examples/da/plot_otda_d2.py +++ b/examples/da/plot_otda_d2.py @@ -19,17 +19,19 @@ of what the transport methods are doing. # License: MIT License import matplotlib.pylab as pl +import numpy as np import ot -# number of source and target points to generate -ns = 150 -nt = 150 +np.random.seed(42) -Xs, ys = ot.datasets.get_data_classif('3gauss', ns) -Xt, yt = ot.datasets.get_data_classif('3gauss2', nt) +n_samples_source = 150 +n_samples_target = 150 + +Xs, ys = ot.datasets.get_data_classif('3gauss', n_samples_source) +Xt, yt = ot.datasets.get_data_classif('3gauss2', n_samples_target) # Cost matrix -M = ot.dist(Xs, Xt) +M = ot.dist(Xs, Xt, metric='sqeuclidean') # Instantiate the different transport algorithms and fit them diff --git a/examples/da/plot_otda_mapping.py b/examples/da/plot_otda_mapping.py index 6d83507..aea7f09 100644 --- a/examples/da/plot_otda_mapping.py +++ b/examples/da/plot_otda_mapping.py @@ -23,7 +23,7 @@ import matplotlib.pylab as pl import ot -np.random.seed(0) +np.random.seed(42) ############################################################################## # generate @@ -31,10 +31,11 @@ np.random.seed(0) n = 100 # nb samples in source and target datasets theta = 2 * np.pi / 20 -nz = 0.1 -Xs, ys = ot.datasets.get_data_classif('gaussrot', n, nz=nz) -Xs_new, _ = ot.datasets.get_data_classif('gaussrot', n, nz=nz) -Xt, yt = ot.datasets.get_data_classif('gaussrot', n, theta=theta, nz=nz) +noise_level = 0.1 +Xs, ys = ot.datasets.get_data_classif('gaussrot', n, nz=noise_level) +Xs_new, _ = ot.datasets.get_data_classif('gaussrot', n, nz=noise_level) +Xt, yt = ot.datasets.get_data_classif( + 'gaussrot', n, theta=theta, nz=noise_level) # one of the target mode changes its variance (no linear mapping) Xt[yt == 2] *= 3 @@ -46,8 +47,7 @@ ot_mapping_linear = ot.da.MappingTransport( kernel="linear", mu=1e0, eta=1e-8, bias=True, max_iter=20, verbose=True) -ot_mapping_linear.fit( - Xs=Xs, Xt=Xt) +ot_mapping_linear.fit(Xs=Xs, Xt=Xt) # for original source samples, transform applies barycentric mapping transp_Xs_linear = ot_mapping_linear.transform(Xs=Xs) diff --git a/examples/da/plot_otda_mapping_colors_images.py b/examples/da/plot_otda_mapping_colors_images.py index 4209020..6c024ea 100644 --- a/examples/da/plot_otda_mapping_colors_images.py +++ b/examples/da/plot_otda_mapping_colors_images.py @@ -23,6 +23,8 @@ from scipy import ndimage import matplotlib.pylab as pl import ot +np.random.seed(42) + def im2mat(I): """Converts and image to matrix (one pixel per line)""" -- cgit v1.2.3 From 5a9795f08341458bd9e3befe0c2c6ea6fa891323 Mon Sep 17 00:00:00 2001 From: Slasnista Date: Tue, 29 Aug 2017 13:34:34 +0200 Subject: pass on examples | introduced RandomState --- examples/da/plot_otda_classes.py | 20 +++++++++++++------- examples/da/plot_otda_color_images.py | 19 ++++++++++++++++--- examples/da/plot_otda_d2.py | 12 ++++++++++-- examples/da/plot_otda_mapping.py | 21 ++++++++++++++------- examples/da/plot_otda_mapping_colors_images.py | 9 ++++++--- 5 files changed, 59 insertions(+), 22 deletions(-) diff --git a/examples/da/plot_otda_classes.py b/examples/da/plot_otda_classes.py index 6870fa4..ec57a37 100644 --- a/examples/da/plot_otda_classes.py +++ b/examples/da/plot_otda_classes.py @@ -15,19 +15,23 @@ approaches currently supported in POT. # License: MIT License import matplotlib.pylab as pl -import numpy as np import ot -np.random.seed(42) -# number of source and target points to generate -ns = 150 -nt = 150 +############################################################################## +# generate data +############################################################################## + +n_source_samples = 150 +n_target_samples = 150 + +Xs, ys = ot.datasets.get_data_classif('3gauss', n_source_samples) +Xt, yt = ot.datasets.get_data_classif('3gauss2', n_target_samples) -Xs, ys = ot.datasets.get_data_classif('3gauss', ns) -Xt, yt = ot.datasets.get_data_classif('3gauss2', nt) +############################################################################## # Instantiate the different transport algorithms and fit them +############################################################################## # EMD Transport ot_emd = ot.da.EMDTransport() @@ -52,6 +56,7 @@ transp_Xs_sinkhorn = ot_sinkhorn.transform(Xs=Xs) transp_Xs_lpl1 = ot_lpl1.transform(Xs=Xs) transp_Xs_l1l2 = ot_l1l2.transform(Xs=Xs) + ############################################################################## # Fig 1 : plots source and target samples ############################################################################## @@ -72,6 +77,7 @@ pl.legend(loc=0) pl.title('Target samples') pl.tight_layout() + ############################################################################## # Fig 2 : plot optimal couplings and transported samples ############################################################################## diff --git a/examples/da/plot_otda_color_images.py b/examples/da/plot_otda_color_images.py index 805d0b0..3984afb 100644 --- a/examples/da/plot_otda_color_images.py +++ b/examples/da/plot_otda_color_images.py @@ -22,7 +22,8 @@ from scipy import ndimage import matplotlib.pylab as pl import ot -np.random.seed(42) + +r = np.random.RandomState(42) def im2mat(I): @@ -39,6 +40,10 @@ def minmax(I): return np.clip(I, 0, 1) +############################################################################## +# generate data +############################################################################## + # Loading images I1 = ndimage.imread('../../data/ocean_day.jpg').astype(np.float64) / 256 I2 = ndimage.imread('../../data/ocean_sunset.jpg').astype(np.float64) / 256 @@ -48,12 +53,17 @@ X2 = im2mat(I2) # training samples nb = 1000 -idx1 = np.random.randint(X1.shape[0], size=(nb,)) -idx2 = np.random.randint(X2.shape[0], size=(nb,)) +idx1 = r.randint(X1.shape[0], size=(nb,)) +idx2 = r.randint(X2.shape[0], size=(nb,)) Xs = X1[idx1, :] Xt = X2[idx2, :] + +############################################################################## +# Instantiate the different transport algorithms and fit them +############################################################################## + # EMDTransport ot_emd = ot.da.EMDTransport() ot_emd.fit(Xs=Xs, Xt=Xt) @@ -75,6 +85,7 @@ I2t = minmax(mat2im(transp_Xt_emd, I2.shape)) I1te = minmax(mat2im(transp_Xs_sinkhorn, I1.shape)) I2te = minmax(mat2im(transp_Xt_sinkhorn, I2.shape)) + ############################################################################## # plot original image ############################################################################## @@ -91,6 +102,7 @@ pl.imshow(I2) pl.axis('off') pl.title('Image 2') + ############################################################################## # scatter plot of colors ############################################################################## @@ -112,6 +124,7 @@ pl.ylabel('Blue') pl.title('Image 2') pl.tight_layout() + ############################################################################## # plot new images ############################################################################## diff --git a/examples/da/plot_otda_d2.py b/examples/da/plot_otda_d2.py index 8833eb2..3daa0a6 100644 --- a/examples/da/plot_otda_d2.py +++ b/examples/da/plot_otda_d2.py @@ -19,10 +19,12 @@ of what the transport methods are doing. # License: MIT License import matplotlib.pylab as pl -import numpy as np import ot -np.random.seed(42) + +############################################################################## +# generate data +############################################################################## n_samples_source = 150 n_samples_target = 150 @@ -33,7 +35,10 @@ Xt, yt = ot.datasets.get_data_classif('3gauss2', n_samples_target) # Cost matrix M = ot.dist(Xs, Xt, metric='sqeuclidean') + +############################################################################## # Instantiate the different transport algorithms and fit them +############################################################################## # EMD Transport ot_emd = ot.da.EMDTransport() @@ -52,6 +57,7 @@ transp_Xs_emd = ot_emd.transform(Xs=Xs) transp_Xs_sinkhorn = ot_sinkhorn.transform(Xs=Xs) transp_Xs_lpl1 = ot_lpl1.transform(Xs=Xs) + ############################################################################## # Fig 1 : plots source and target samples + matrix of pairwise distance ############################################################################## @@ -78,6 +84,7 @@ pl.yticks([]) pl.title('Matrix of pairwise distances') pl.tight_layout() + ############################################################################## # Fig 2 : plots optimal couplings for the different methods ############################################################################## @@ -127,6 +134,7 @@ pl.yticks([]) pl.title('Main coupling coefficients\nSinkhornLpl1Transport') pl.tight_layout() + ############################################################################## # Fig 3 : plot transported samples ############################################################################## diff --git a/examples/da/plot_otda_mapping.py b/examples/da/plot_otda_mapping.py index aea7f09..09d2cb4 100644 --- a/examples/da/plot_otda_mapping.py +++ b/examples/da/plot_otda_mapping.py @@ -23,25 +23,31 @@ import matplotlib.pylab as pl import ot -np.random.seed(42) - ############################################################################## -# generate +# generate data ############################################################################## -n = 100 # nb samples in source and target datasets +n_source_samples = 100 +n_target_samples = 100 theta = 2 * np.pi / 20 noise_level = 0.1 -Xs, ys = ot.datasets.get_data_classif('gaussrot', n, nz=noise_level) -Xs_new, _ = ot.datasets.get_data_classif('gaussrot', n, nz=noise_level) + +Xs, ys = ot.datasets.get_data_classif( + 'gaussrot', n_source_samples, nz=noise_level) +Xs_new, _ = ot.datasets.get_data_classif( + 'gaussrot', n_source_samples, nz=noise_level) Xt, yt = ot.datasets.get_data_classif( - 'gaussrot', n, theta=theta, nz=noise_level) + 'gaussrot', n_target_samples, theta=theta, nz=noise_level) # one of the target mode changes its variance (no linear mapping) Xt[yt == 2] *= 3 Xt = Xt + 4 +############################################################################## +# Instantiate the different transport algorithms and fit them +############################################################################## + # MappingTransport with linear kernel ot_mapping_linear = ot.da.MappingTransport( kernel="linear", mu=1e0, eta=1e-8, bias=True, @@ -80,6 +86,7 @@ pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', label='Target samples') pl.legend(loc=0) pl.title('Source and target distributions') + ############################################################################## # plot transported samples ############################################################################## diff --git a/examples/da/plot_otda_mapping_colors_images.py b/examples/da/plot_otda_mapping_colors_images.py index 6c024ea..a628b05 100644 --- a/examples/da/plot_otda_mapping_colors_images.py +++ b/examples/da/plot_otda_mapping_colors_images.py @@ -23,7 +23,7 @@ from scipy import ndimage import matplotlib.pylab as pl import ot -np.random.seed(42) +r = np.random.RandomState(42) def im2mat(I): @@ -54,8 +54,8 @@ X2 = im2mat(I2) # training samples nb = 1000 -idx1 = np.random.randint(X1.shape[0], size=(nb,)) -idx2 = np.random.randint(X2.shape[0], size=(nb,)) +idx1 = r.randint(X1.shape[0], size=(nb,)) +idx2 = r.randint(X2.shape[0], size=(nb,)) Xs = X1[idx1, :] Xt = X2[idx2, :] @@ -91,6 +91,7 @@ ot_mapping_gaussian.fit(Xs=Xs, Xt=Xt) X1tn = ot_mapping_gaussian.transform(Xs=X1) # use the estimated mapping Image_mapping_gaussian = minmax(mat2im(X1tn, I1.shape)) + ############################################################################## # plot original images ############################################################################## @@ -107,6 +108,7 @@ pl.axis('off') pl.title('Image 2') pl.tight_layout() + ############################################################################## # plot pixel values distribution ############################################################################## @@ -128,6 +130,7 @@ pl.ylabel('Blue') pl.title('Image 2') pl.tight_layout() + ############################################################################## # plot transformed images ############################################################################## -- cgit v1.2.3 From 6ae3ad7bb48b1fa8964cfd2791bdb86267776495 Mon Sep 17 00:00:00 2001 From: aje Date: Tue, 29 Aug 2017 15:38:11 +0200 Subject: Changes to LP solver: - Allow to modify the maximal number of iterations - Display an error message in the python console if the solver encountered an issue --- ot/da.py | 4 ++-- ot/lp/EMD.h | 7 ++++++- ot/lp/EMD_wrapper.cpp | 7 +++---- ot/lp/__init__.py | 16 ++++++++++------ ot/lp/emd_wrap.pyx | 25 ++++++++++++++++++++----- 5 files changed, 41 insertions(+), 18 deletions(-) diff --git a/ot/da.py b/ot/da.py index 78dc150..0dfd02f 100644 --- a/ot/da.py +++ b/ot/da.py @@ -658,7 +658,7 @@ class OTDA(object): self.metric = metric self.computed = False - def fit(self, xs, xt, ws=None, wt=None, norm=None): + def fit(self, xs, xt, ws=None, wt=None, norm=None, numItermax=10000): """Fit domain adaptation between samples is xs and xt (with optional weights)""" self.xs = xs @@ -674,7 +674,7 @@ class OTDA(object): self.M = dist(xs, xt, metric=self.metric) self.normalizeM(norm) - self.G = emd(ws, wt, self.M) + self.G = emd(ws, wt, self.M, numItermax) self.computed = True def interp(self, direction=1): diff --git a/ot/lp/EMD.h b/ot/lp/EMD.h index 40d7192..956830c 100644 --- a/ot/lp/EMD.h +++ b/ot/lp/EMD.h @@ -23,7 +23,12 @@ using namespace lemon; typedef unsigned int node_id_type; +enum ProblemType { + INFEASIBLE, + OPTIMAL, + UNBOUNDED +}; -void EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double *cost); +int EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double *cost, int numItermax); #endif diff --git a/ot/lp/EMD_wrapper.cpp b/ot/lp/EMD_wrapper.cpp index cad4750..83ed56c 100644 --- a/ot/lp/EMD_wrapper.cpp +++ b/ot/lp/EMD_wrapper.cpp @@ -15,11 +15,10 @@ #include "EMD.h" -void EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double *cost) { +int EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double *cost, int numItermax) { // beware M and C anre strored in row major C style!!! int n, m, i,cur; double max; - int max_iter=10000; typedef FullBipartiteDigraph Digraph; DIGRAPH_TYPEDEFS(FullBipartiteDigraph); @@ -46,7 +45,7 @@ void EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double * std::vector indI(n), indJ(m); std::vector weights1(n), weights2(m); Digraph di(n, m); - NetworkSimplexSimple net(di, true, n+m, n*m,max_iter); + NetworkSimplexSimple net(di, true, n+m, n*m, numItermax); // Set supply and demand, don't account for 0 values (faster) @@ -116,5 +115,5 @@ void EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double * }; - + return ret; } diff --git a/ot/lp/__init__.py b/ot/lp/__init__.py index 6e0bdb8..62afe76 100644 --- a/ot/lp/__init__.py +++ b/ot/lp/__init__.py @@ -15,7 +15,7 @@ import multiprocessing -def emd(a, b, M): +def emd(a, b, M, numItermax=10000): """Solves the Earth Movers distance problem and returns the OT matrix @@ -40,6 +40,8 @@ def emd(a, b, M): Target histogram (uniform weigth if empty list) M : (ns,nt) ndarray, float64 loss matrix + numItermax : int + Maximum number of iterations made by the LP solver. Returns ------- @@ -84,9 +86,9 @@ def emd(a, b, M): if len(b) == 0: b = np.ones((M.shape[1], ), dtype=np.float64)/M.shape[1] - return emd_c(a, b, M) + return emd_c(a, b, M, numItermax) -def emd2(a, b, M,processes=multiprocessing.cpu_count()): +def emd2(a, b, M, numItermax=10000, processes=multiprocessing.cpu_count()): """Solves the Earth Movers distance problem and returns the loss .. math:: @@ -110,6 +112,8 @@ def emd2(a, b, M,processes=multiprocessing.cpu_count()): Target histogram (uniform weigth if empty list) M : (ns,nt) ndarray, float64 loss matrix + numItermax : int + Maximum number of iterations made by the LP solver. Returns ------- @@ -155,12 +159,12 @@ def emd2(a, b, M,processes=multiprocessing.cpu_count()): b = np.ones((M.shape[1], ), dtype=np.float64)/M.shape[1] if len(b.shape)==1: - return emd2_c(a, b, M) + return emd2_c(a, b, M, numItermax) else: nb=b.shape[1] - #res=[emd2_c(a,b[:,i].copy(),M) for i in range(nb)] + #res=[emd2_c(a,b[:,i].copy(),M, numItermax) for i in range(nb)] def f(b): - return emd2_c(a,b,M) + return emd2_c(a,b,M, numItermax) res= parmap(f, [b[:,i] for i in range(nb)],processes) return np.array(res) diff --git a/ot/lp/emd_wrap.pyx b/ot/lp/emd_wrap.pyx index 46c96c1..ed8c416 100644 --- a/ot/lp/emd_wrap.pyx +++ b/ot/lp/emd_wrap.pyx @@ -15,13 +15,14 @@ cimport cython cdef extern from "EMD.h": - void EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double *cost) + int EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double *cost, int numItermax) + cdef enum ProblemType: INFEASIBLE, OPTIMAL, UNBOUNDED @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): +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 numItermax): """ Solves the Earth Movers distance problem and returns the optimal transport matrix @@ -48,6 +49,8 @@ def emd_c( np.ndarray[double, ndim=1, mode="c"] a,np.ndarray[double, ndim=1, mod target histogram M : (ns,nt) ndarray, float64 loss matrix + numItermax : int + Maximum number of iterations made by the LP solver. Returns @@ -69,13 +72,18 @@ def emd_c( np.ndarray[double, ndim=1, mode="c"] a,np.ndarray[double, ndim=1, mod b=np.ones((n2,))/n2 # calling the function - EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, &cost) + cdef int resultSolver = EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, &cost, numItermax) + if resultSolver != OPTIMAL: + if resultSolver == INFEASIBLE: + print("Problem infeasible. Try to inscrease numItermax.") + elif resultSolver == UNBOUNDED: + print("Problem unbounded") return G @cython.boundscheck(False) @cython.wraparound(False) -def emd2_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): +def emd2_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 numItermax): """ Solves the Earth Movers distance problem and returns the optimal transport loss @@ -102,6 +110,8 @@ def emd2_c( np.ndarray[double, ndim=1, mode="c"] a,np.ndarray[double, ndim=1, mo target histogram M : (ns,nt) ndarray, float64 loss matrix + numItermax : int + Maximum number of iterations made by the LP solver. Returns @@ -123,7 +133,12 @@ def emd2_c( np.ndarray[double, ndim=1, mode="c"] a,np.ndarray[double, ndim=1, mo b=np.ones((n2,))/n2 # calling the function - EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, &cost) + cdef int resultSolver = EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, &cost, numItermax) + if resultSolver != OPTIMAL: + if resultSolver == INFEASIBLE: + print("Problem infeasible. Try to inscrease numItermax.") + elif resultSolver == UNBOUNDED: + print("Problem unbounded") cost=0 for i in range(n1): -- cgit v1.2.3 From b5629270b07499524072053530a993b1c52c1a1a Mon Sep 17 00:00:00 2001 From: aje Date: Tue, 29 Aug 2017 16:53:06 +0200 Subject: Fix param order --- ot/lp/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ot/lp/__init__.py b/ot/lp/__init__.py index 62afe76..5143a70 100644 --- a/ot/lp/__init__.py +++ b/ot/lp/__init__.py @@ -88,7 +88,7 @@ def emd(a, b, M, numItermax=10000): return emd_c(a, b, M, numItermax) -def emd2(a, b, M, numItermax=10000, processes=multiprocessing.cpu_count()): +def emd2(a, b, M, processes=multiprocessing.cpu_count(), numItermax=10000): """Solves the Earth Movers distance problem and returns the loss .. math:: -- cgit v1.2.3 From 0f7cd9237f8ac3596c0a7dfdd4d543345a34ae6b Mon Sep 17 00:00:00 2001 From: aje Date: Tue, 29 Aug 2017 17:01:17 +0200 Subject: Type print --- ot/lp/emd_wrap.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ot/lp/emd_wrap.pyx b/ot/lp/emd_wrap.pyx index ed8c416..6039e1f 100644 --- a/ot/lp/emd_wrap.pyx +++ b/ot/lp/emd_wrap.pyx @@ -75,7 +75,7 @@ def emd_c( np.ndarray[double, ndim=1, mode="c"] a,np.ndarray[double, ndim=1, mod cdef int resultSolver = EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, &cost, numItermax) if resultSolver != OPTIMAL: if resultSolver == INFEASIBLE: - print("Problem infeasible. Try to inscrease numItermax.") + print("Problem infeasible. Try to increase numItermax.") elif resultSolver == UNBOUNDED: print("Problem unbounded") -- cgit v1.2.3 From ceeb063541fa71d4ddd7b13b043f985dc5bcab14 Mon Sep 17 00:00:00 2001 From: aje Date: Wed, 30 Aug 2017 09:56:37 +0200 Subject: Changes: - Rename numItermax to max_iter - Default value to 100000 instead of 10000 - Add max_iter to class SinkhornTransport(BaseTransport) - Add norm to all BaseTransport --- ot/da.py | 64 ++++++++++++++++++++++++++++++++++------ ot/lp/EMD.h | 2 +- ot/lp/EMD_wrapper.cpp | 4 +-- ot/lp/__init__.py | 46 ++++++++++++++--------------- ot/lp/emd_wrap.pyx | 82 ++++++++++++++++++++++++++------------------------- 5 files changed, 123 insertions(+), 75 deletions(-) diff --git a/ot/da.py b/ot/da.py index 0dfd02f..5871aba 100644 --- a/ot/da.py +++ b/ot/da.py @@ -658,7 +658,7 @@ class OTDA(object): self.metric = metric self.computed = False - def fit(self, xs, xt, ws=None, wt=None, norm=None, numItermax=10000): + def fit(self, xs, xt, ws=None, wt=None, norm=None, max_iter=100000): """Fit domain adaptation between samples is xs and xt (with optional weights)""" self.xs = xs @@ -674,7 +674,7 @@ class OTDA(object): self.M = dist(xs, xt, metric=self.metric) self.normalizeM(norm) - self.G = emd(ws, wt, self.M, numItermax) + self.G = emd(ws, wt, self.M, max_iter) self.computed = True def interp(self, direction=1): @@ -1001,6 +1001,7 @@ class BaseTransport(BaseEstimator): # pairwise distance self.cost_ = dist(Xs, Xt, metric=self.metric) + self.normalizeCost_(self.norm) if (ys is not None) and (yt is not None): @@ -1182,6 +1183,26 @@ class BaseTransport(BaseEstimator): return transp_Xt + def normalizeCost_(self, norm): + """ Apply normalization to the loss matrix + + + Parameters + ---------- + norm : str + type of normalization from 'median','max','log','loglog' + + """ + + if norm == "median": + self.cost_ /= float(np.median(self.cost_)) + elif norm == "max": + self.cost_ /= float(np.max(self.cost_)) + elif norm == "log": + self.cost_ = np.log(1 + self.cost_) + elif norm == "loglog": + self.cost_ = np.log(1 + np.log(1 + self.cost_)) + class SinkhornTransport(BaseTransport): """Domain Adapatation OT method based on Sinkhorn Algorithm @@ -1202,6 +1223,9 @@ class SinkhornTransport(BaseTransport): be transported from a domain to another one. 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 : string, optional (default="uniform") The kind of distribution estimation to employ verbose : int, optional (default=0) @@ -1231,7 +1255,7 @@ class SinkhornTransport(BaseTransport): def __init__(self, reg_e=1., max_iter=1000, tol=10e-9, verbose=False, log=False, - metric="sqeuclidean", + metric="sqeuclidean", norm=None, distribution_estimation=distribution_estimation_uniform, out_of_sample_map='ferradans', limit_max=np.infty): @@ -1241,6 +1265,7 @@ class SinkhornTransport(BaseTransport): 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 @@ -1296,6 +1321,9 @@ class EMDTransport(BaseTransport): be transported from a domain to another one. 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 : string, optional (default="uniform") The kind of distribution estimation to employ verbose : int, optional (default=0) @@ -1306,6 +1334,9 @@ class EMDTransport(BaseTransport): 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 ---------- @@ -1319,14 +1350,17 @@ class EMDTransport(BaseTransport): on Pattern Analysis and Machine Intelligence , vol.PP, no.99, pp.1-1 """ - def __init__(self, metric="sqeuclidean", + def __init__(self, metric="sqeuclidean", norm=None, distribution_estimation=distribution_estimation_uniform, - out_of_sample_map='ferradans', limit_max=10): + out_of_sample_map='ferradans', limit_max=10, + max_iter=100000): 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 + 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 @@ -1353,7 +1387,7 @@ class EMDTransport(BaseTransport): # coupling estimation self.coupling_ = emd( - a=self.mu_s, b=self.mu_t, M=self.cost_, + a=self.mu_s, b=self.mu_t, M=self.cost_, max_iter=self.max_iter ) return self @@ -1376,6 +1410,9 @@ class SinkhornLpl1Transport(BaseTransport): be transported from a domain to another one. 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 : string, optional (default="uniform") The kind of distribution estimation to employ max_iter : int, float, optional (default=10) @@ -1410,7 +1447,7 @@ class SinkhornLpl1Transport(BaseTransport): def __init__(self, reg_e=1., reg_cl=0.1, max_iter=10, max_inner_iter=200, tol=10e-9, verbose=False, - metric="sqeuclidean", + metric="sqeuclidean", norm=None, distribution_estimation=distribution_estimation_uniform, out_of_sample_map='ferradans', limit_max=np.infty): @@ -1421,6 +1458,7 @@ class SinkhornLpl1Transport(BaseTransport): self.tol = tol 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 @@ -1477,6 +1515,9 @@ class SinkhornL1l2Transport(BaseTransport): be transported from a domain to another one. 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 : string, optional (default="uniform") The kind of distribution estimation to employ max_iter : int, float, optional (default=10) @@ -1516,7 +1557,7 @@ 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, - metric="sqeuclidean", + metric="sqeuclidean", norm=None, distribution_estimation=distribution_estimation_uniform, out_of_sample_map='ferradans', limit_max=10): @@ -1528,6 +1569,7 @@ class SinkhornL1l2Transport(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 self.limit_max = limit_max @@ -1588,6 +1630,9 @@ class MappingTransport(BaseEstimator): 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) @@ -1627,11 +1672,12 @@ class MappingTransport(BaseEstimator): """ def __init__(self, mu=1, eta=0.001, bias=False, metric="sqeuclidean", - kernel="linear", sigma=1, max_iter=100, tol=1e-5, + 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 diff --git a/ot/lp/EMD.h b/ot/lp/EMD.h index 956830c..aa92441 100644 --- a/ot/lp/EMD.h +++ b/ot/lp/EMD.h @@ -29,6 +29,6 @@ enum ProblemType { UNBOUNDED }; -int EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double *cost, int numItermax); +int EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double *cost, int max_iter); #endif diff --git a/ot/lp/EMD_wrapper.cpp b/ot/lp/EMD_wrapper.cpp index 83ed56c..c8c2eb3 100644 --- a/ot/lp/EMD_wrapper.cpp +++ b/ot/lp/EMD_wrapper.cpp @@ -15,7 +15,7 @@ #include "EMD.h" -int EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double *cost, int numItermax) { +int EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double *cost, int max_iter) { // beware M and C anre strored in row major C style!!! int n, m, i,cur; double max; @@ -45,7 +45,7 @@ int EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double *c std::vector indI(n), indJ(m); std::vector weights1(n), weights2(m); Digraph di(n, m); - NetworkSimplexSimple net(di, true, n+m, n*m, numItermax); + NetworkSimplexSimple net(di, true, n+m, n*m, max_iter); // Set supply and demand, don't account for 0 values (faster) diff --git a/ot/lp/__init__.py b/ot/lp/__init__.py index 5143a70..7bef648 100644 --- a/ot/lp/__init__.py +++ b/ot/lp/__init__.py @@ -14,8 +14,7 @@ from ..utils import parmap import multiprocessing - -def emd(a, b, M, numItermax=10000): +def emd(a, b, M, max_iter=100000): """Solves the Earth Movers distance problem and returns the OT matrix @@ -40,8 +39,9 @@ def emd(a, b, M, numItermax=10000): Target histogram (uniform weigth if empty list) M : (ns,nt) ndarray, float64 loss matrix - numItermax : int - Maximum number of iterations made by the LP solver. + max_iter : int, optional (default=100000) + The maximum number of iterations before stopping the optimization + algorithm if it has not converged. Returns ------- @@ -54,7 +54,7 @@ def emd(a, b, M, numItermax=10000): Simple example with obvious solution. The function emd accepts lists and perform automatic conversion to numpy arrays - + >>> import ot >>> a=[.5,.5] >>> b=[.5,.5] @@ -86,10 +86,11 @@ def emd(a, b, M, numItermax=10000): if len(b) == 0: b = np.ones((M.shape[1], ), dtype=np.float64)/M.shape[1] - return emd_c(a, b, M, numItermax) + return emd_c(a, b, M, max_iter) + -def emd2(a, b, M, processes=multiprocessing.cpu_count(), numItermax=10000): - """Solves the Earth Movers distance problem and returns the loss +def emd2(a, b, M, processes=multiprocessing.cpu_count(), max_iter=100000): + """Solves the Earth Movers distance problem and returns the loss .. math:: \gamma = arg\min_\gamma <\gamma,M>_F @@ -112,8 +113,9 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), numItermax=10000): Target histogram (uniform weigth if empty list) M : (ns,nt) ndarray, float64 loss matrix - numItermax : int - Maximum number of iterations made by the LP solver. + max_iter : int, optional (default=100000) + The maximum number of iterations before stopping the optimization + algorithm if it has not converged. Returns ------- @@ -126,15 +128,15 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), numItermax=10000): Simple example with obvious solution. The function emd accepts lists and perform automatic conversion to numpy arrays - - + + >>> import ot >>> a=[.5,.5] >>> b=[.5,.5] >>> M=[[0.,1.],[1.,0.]] >>> ot.emd2(a,b,M) 0.0 - + References ---------- @@ -157,16 +159,14 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), numItermax=10000): a = np.ones((M.shape[0], ), dtype=np.float64)/M.shape[0] if len(b) == 0: b = np.ones((M.shape[1], ), dtype=np.float64)/M.shape[1] - - if len(b.shape)==1: - return emd2_c(a, b, M, numItermax) + + if len(b.shape) == 1: + return emd2_c(a, b, M, max_iter) else: - nb=b.shape[1] - #res=[emd2_c(a,b[:,i].copy(),M, numItermax) for i in range(nb)] + nb = b.shape[1] + # res = [emd2_c(a, b[:, i].copy(), M, max_iter) for i in range(nb)] + def f(b): - return emd2_c(a,b,M, numItermax) - res= parmap(f, [b[:,i] for i in range(nb)],processes) + return emd2_c(a, b, M, max_iter) + res = parmap(f, [b[:, i] for i in range(nb)], processes) return np.array(res) - - - \ No newline at end of file diff --git a/ot/lp/emd_wrap.pyx b/ot/lp/emd_wrap.pyx index 6039e1f..26d3330 100644 --- a/ot/lp/emd_wrap.pyx +++ b/ot/lp/emd_wrap.pyx @@ -15,56 +15,57 @@ cimport cython cdef extern from "EMD.h": - int EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double *cost, int numItermax) + int EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double *cost, int max_iter) cdef enum ProblemType: INFEASIBLE, OPTIMAL, UNBOUNDED @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 numItermax): +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 - + gamm=emd(a,b,M) - + .. math:: - \gamma = arg\min_\gamma <\gamma,M>_F - + \gamma = arg\min_\gamma <\gamma,M>_F + s.t. \gamma 1 = a - - \gamma^T 1= b - + + \gamma^T 1= b + \gamma\geq 0 where : - + - M is the metric cost matrix - a and b are the sample weights - + Parameters ---------- a : (ns,) ndarray, float64 - source histogram + source histogram b : (nt,) ndarray, float64 target histogram M : (ns,nt) ndarray, float64 - loss matrix - numItermax : int - Maximum number of iterations made by the LP solver. - - + loss matrix + max_iter : int + The maximum number of iterations before stopping the optimization + algorithm if it has not converged. + + Returns ------- gamma: (ns x nt) ndarray Optimal transportation matrix for the given parameters - + """ cdef int n1= M.shape[0] cdef int n2= M.shape[1] cdef float cost=0 cdef np.ndarray[double, ndim=2, mode="c"] G=np.zeros([n1, n2]) - + if not len(a): a=np.ones((n1,))/n1 @@ -72,7 +73,7 @@ def emd_c( np.ndarray[double, ndim=1, mode="c"] a,np.ndarray[double, ndim=1, mod b=np.ones((n2,))/n2 # calling the function - cdef int resultSolver = EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, &cost, numItermax) + cdef int resultSolver = EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, &cost, max_iter) if resultSolver != OPTIMAL: if resultSolver == INFEASIBLE: print("Problem infeasible. Try to increase numItermax.") @@ -83,49 +84,50 @@ def emd_c( np.ndarray[double, ndim=1, mode="c"] a,np.ndarray[double, ndim=1, mod @cython.boundscheck(False) @cython.wraparound(False) -def emd2_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 numItermax): +def emd2_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 loss - + gamm=emd(a,b,M) - + .. math:: - \gamma = arg\min_\gamma <\gamma,M>_F - + \gamma = arg\min_\gamma <\gamma,M>_F + s.t. \gamma 1 = a - - \gamma^T 1= b - + + \gamma^T 1= b + \gamma\geq 0 where : - + - M is the metric cost matrix - a and b are the sample weights - + Parameters ---------- a : (ns,) ndarray, float64 - source histogram + source histogram b : (nt,) ndarray, float64 target histogram M : (ns,nt) ndarray, float64 - loss matrix - numItermax : int - Maximum number of iterations made by the LP solver. - - + loss matrix + max_iter : int + The maximum number of iterations before stopping the optimization + algorithm if it has not converged. + + Returns ------- gamma: (ns x nt) ndarray Optimal transportation matrix for the given parameters - + """ cdef int n1= M.shape[0] cdef int n2= M.shape[1] cdef float cost=0 cdef np.ndarray[double, ndim=2, mode="c"] G=np.zeros([n1, n2]) - + if not len(a): a=np.ones((n1,))/n1 @@ -133,13 +135,13 @@ def emd2_c( np.ndarray[double, ndim=1, mode="c"] a,np.ndarray[double, ndim=1, mo b=np.ones((n2,))/n2 # calling the function - cdef int resultSolver = EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, &cost, numItermax) + cdef int resultSolver = EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, &cost, max_iter) if resultSolver != OPTIMAL: if resultSolver == INFEASIBLE: print("Problem infeasible. Try to inscrease numItermax.") elif resultSolver == UNBOUNDED: print("Problem unbounded") - + cost=0 for i in range(n1): for j in range(n2): -- cgit v1.2.3 From 8875f653e57aa11c8d62d291abb16fdbeff65511 Mon Sep 17 00:00:00 2001 From: aje Date: Wed, 30 Aug 2017 10:06:41 +0200 Subject: Rename for emd and emd2 --- ot/lp/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ot/lp/__init__.py b/ot/lp/__init__.py index 7bef648..de91e74 100644 --- a/ot/lp/__init__.py +++ b/ot/lp/__init__.py @@ -14,7 +14,7 @@ from ..utils import parmap import multiprocessing -def emd(a, b, M, max_iter=100000): +def emd(a, b, M, numItermax=100000): """Solves the Earth Movers distance problem and returns the OT matrix @@ -39,7 +39,7 @@ def emd(a, b, M, max_iter=100000): Target histogram (uniform weigth if empty list) M : (ns,nt) ndarray, float64 loss matrix - max_iter : int, optional (default=100000) + numItermax : int, optional (default=100000) The maximum number of iterations before stopping the optimization algorithm if it has not converged. @@ -86,10 +86,10 @@ def emd(a, b, M, max_iter=100000): if len(b) == 0: b = np.ones((M.shape[1], ), dtype=np.float64)/M.shape[1] - return emd_c(a, b, M, max_iter) + return emd_c(a, b, M, numItermax) -def emd2(a, b, M, processes=multiprocessing.cpu_count(), max_iter=100000): +def emd2(a, b, M, processes=multiprocessing.cpu_count(), numItermax=100000): """Solves the Earth Movers distance problem and returns the loss .. math:: @@ -113,7 +113,7 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), max_iter=100000): Target histogram (uniform weigth if empty list) M : (ns,nt) ndarray, float64 loss matrix - max_iter : int, optional (default=100000) + numItermax : int, optional (default=100000) The maximum number of iterations before stopping the optimization algorithm if it has not converged. @@ -161,12 +161,12 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), max_iter=100000): b = np.ones((M.shape[1], ), dtype=np.float64)/M.shape[1] if len(b.shape) == 1: - return emd2_c(a, b, M, max_iter) + return emd2_c(a, b, M, numItermax) else: nb = b.shape[1] - # res = [emd2_c(a, b[:, i].copy(), M, max_iter) for i in range(nb)] + # res = [emd2_c(a, b[:, i].copy(), M, numItermax) for i in range(nb)] def f(b): - return emd2_c(a, b, M, max_iter) + return emd2_c(a, b, M, numItermax) res = parmap(f, [b[:, i] for i in range(nb)], processes) return np.array(res) -- cgit v1.2.3 From 5076131b3954e3e7951f65f4c05d959b68072b97 Mon Sep 17 00:00:00 2001 From: aje Date: Wed, 30 Aug 2017 10:22:48 +0200 Subject: Fix name error --- ot/da.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ot/da.py b/ot/da.py index 5871aba..b4a69b1 100644 --- a/ot/da.py +++ b/ot/da.py @@ -1387,7 +1387,7 @@ class EMDTransport(BaseTransport): # coupling estimation self.coupling_ = emd( - a=self.mu_s, b=self.mu_t, M=self.cost_, max_iter=self.max_iter + a=self.mu_s, b=self.mu_t, M=self.cost_, numItermax=self.max_iter ) return self -- cgit v1.2.3 From 6d602304c08b6dbbb310814f83c5cf03da35cafd Mon Sep 17 00:00:00 2001 From: aje Date: Wed, 30 Aug 2017 10:53:31 +0200 Subject: Move normalize function in utils.py --- ot/da.py | 52 ++++++---------------------------------------------- ot/utils.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 46 deletions(-) diff --git a/ot/da.py b/ot/da.py index b4a69b1..61a3ba0 100644 --- a/ot/da.py +++ b/ot/da.py @@ -13,7 +13,7 @@ import numpy as np from .bregman import sinkhorn from .lp import emd -from .utils import unif, dist, kernel +from .utils import unif, dist, kernel, cost_normalization from .utils import check_params, deprecated, BaseEstimator from .optim import cg from .optim import gcg @@ -673,7 +673,7 @@ class OTDA(object): self.wt = wt self.M = dist(xs, xt, metric=self.metric) - self.normalizeM(norm) + self.M = cost_normalization(self.M, norm) self.G = emd(ws, wt, self.M, max_iter) self.computed = True @@ -741,26 +741,6 @@ class OTDA(object): # aply the delta to the interpolation return xf[idx, :] + x - x0[idx, :] - def normalizeM(self, norm): - """ Apply normalization to the loss matrix - - - Parameters - ---------- - norm : str - type of normalization from 'median','max','log','loglog' - - """ - - if norm == "median": - self.M /= float(np.median(self.M)) - elif norm == "max": - self.M /= float(np.max(self.M)) - elif norm == "log": - self.M = np.log(1 + self.M) - elif norm == "loglog": - self.M = np.log(1 + np.log(1 + self.M)) - @deprecated("The class OTDA_sinkhorn is deprecated in 0.3.1 and will be" " removed in 0.5 \nUse class SinkhornTransport instead.") @@ -787,7 +767,7 @@ class OTDA_sinkhorn(OTDA): self.wt = wt self.M = dist(xs, xt, metric=self.metric) - self.normalizeM(norm) + self.M = cost_normalization(self.M, norm) self.G = sinkhorn(ws, wt, self.M, reg, **kwargs) self.computed = True @@ -816,7 +796,7 @@ class OTDA_lpl1(OTDA): self.wt = wt self.M = dist(xs, xt, metric=self.metric) - self.normalizeM(norm) + self.M = cost_normalization(self.M, norm) self.G = sinkhorn_lpl1_mm(ws, ys, wt, self.M, reg, eta, **kwargs) self.computed = True @@ -845,7 +825,7 @@ class OTDA_l1l2(OTDA): self.wt = wt self.M = dist(xs, xt, metric=self.metric) - self.normalizeM(norm) + self.M = cost_normalization(self.M, norm) self.G = sinkhorn_l1l2_gl(ws, ys, wt, self.M, reg, eta, **kwargs) self.computed = True @@ -1001,7 +981,7 @@ class BaseTransport(BaseEstimator): # pairwise distance self.cost_ = dist(Xs, Xt, metric=self.metric) - self.normalizeCost_(self.norm) + self.cost_ = cost_normalization(self.cost_, self.norm) if (ys is not None) and (yt is not None): @@ -1183,26 +1163,6 @@ class BaseTransport(BaseEstimator): return transp_Xt - def normalizeCost_(self, norm): - """ Apply normalization to the loss matrix - - - Parameters - ---------- - norm : str - type of normalization from 'median','max','log','loglog' - - """ - - if norm == "median": - self.cost_ /= float(np.median(self.cost_)) - elif norm == "max": - self.cost_ /= float(np.max(self.cost_)) - elif norm == "log": - self.cost_ = np.log(1 + self.cost_) - elif norm == "loglog": - self.cost_ = np.log(1 + np.log(1 + self.cost_)) - class SinkhornTransport(BaseTransport): """Domain Adapatation OT method based on Sinkhorn Algorithm diff --git a/ot/utils.py b/ot/utils.py index 01f2a67..31a002b 100644 --- a/ot/utils.py +++ b/ot/utils.py @@ -134,6 +134,39 @@ def dist0(n, method='lin_square'): return res +def cost_normalization(C, norm=None): + """ Apply normalization to the loss matrix + + + Parameters + ---------- + C : np.array (n1, n2) + The cost matrix to normalize. + norm : str + type of normalization from 'median','max','log','loglog'. Any other + value do not normalize. + + + Returns + ------- + + C : np.array (n1, n2) + The input cost matrix normalized according to given norm. + + """ + + if norm == "median": + C /= float(np.median(C)) + elif norm == "max": + C /= float(np.max(C)) + elif norm == "log": + C = np.log(1 + C) + elif norm == "loglog": + C = np.log(1 + np.log(1 + C)) + + return C + + def dots(*args): """ dots function for multiple matrix multiply """ return reduce(np.dot, args) -- cgit v1.2.3 From 93dee553a3dd5d6e3c5a5d325bb6333e8eb24dee Mon Sep 17 00:00:00 2001 From: aje Date: Wed, 30 Aug 2017 11:25:03 +0200 Subject: Move norm out of fit to init for deprecated OTDA --- ot/da.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/ot/da.py b/ot/da.py index 61a3ba0..564c7b7 100644 --- a/ot/da.py +++ b/ot/da.py @@ -650,15 +650,16 @@ class OTDA(object): """ - def __init__(self, metric='sqeuclidean'): + def __init__(self, metric='sqeuclidean', norm=None): """ Class initialization""" self.xs = 0 self.xt = 0 self.G = 0 self.metric = metric + self.norm = norm self.computed = False - def fit(self, xs, xt, ws=None, wt=None, norm=None, max_iter=100000): + def fit(self, xs, xt, ws=None, wt=None, max_iter=100000): """Fit domain adaptation between samples is xs and xt (with optional weights)""" self.xs = xs @@ -673,7 +674,7 @@ class OTDA(object): self.wt = wt self.M = dist(xs, xt, metric=self.metric) - self.M = cost_normalization(self.M, norm) + self.M = cost_normalization(self.M, self.norm) self.G = emd(ws, wt, self.M, max_iter) self.computed = True @@ -752,7 +753,7 @@ class OTDA_sinkhorn(OTDA): """ - def fit(self, xs, xt, reg=1, ws=None, wt=None, norm=None, **kwargs): + def fit(self, xs, xt, reg=1, ws=None, wt=None, **kwargs): """Fit regularized domain adaptation between samples is xs and xt (with optional weights)""" self.xs = xs @@ -767,7 +768,7 @@ class OTDA_sinkhorn(OTDA): self.wt = wt self.M = dist(xs, xt, metric=self.metric) - self.M = cost_normalization(self.M, norm) + self.M = cost_normalization(self.M, self.norm) self.G = sinkhorn(ws, wt, self.M, reg, **kwargs) self.computed = True @@ -779,8 +780,7 @@ class OTDA_lpl1(OTDA): """Class for domain adaptation with optimal transport with entropic and group regularization""" - def fit(self, xs, ys, xt, reg=1, eta=1, ws=None, wt=None, norm=None, - **kwargs): + def fit(self, xs, ys, xt, reg=1, eta=1, ws=None, wt=None, **kwargs): """Fit regularized domain adaptation between samples is xs and xt (with optional weights), See ot.da.sinkhorn_lpl1_mm for fit parameters""" @@ -796,7 +796,7 @@ class OTDA_lpl1(OTDA): self.wt = wt self.M = dist(xs, xt, metric=self.metric) - self.M = cost_normalization(self.M, norm) + self.M = cost_normalization(self.M, self.norm) self.G = sinkhorn_lpl1_mm(ws, ys, wt, self.M, reg, eta, **kwargs) self.computed = True @@ -808,8 +808,7 @@ class OTDA_l1l2(OTDA): """Class for domain adaptation with optimal transport with entropic and group lasso regularization""" - def fit(self, xs, ys, xt, reg=1, eta=1, ws=None, wt=None, norm=None, - **kwargs): + def fit(self, xs, ys, xt, reg=1, eta=1, ws=None, wt=None, **kwargs): """Fit regularized domain adaptation between samples is xs and xt (with optional weights), See ot.da.sinkhorn_lpl1_gl for fit parameters""" @@ -825,7 +824,7 @@ class OTDA_l1l2(OTDA): self.wt = wt self.M = dist(xs, xt, metric=self.metric) - self.M = cost_normalization(self.M, norm) + self.M = cost_normalization(self.M, self.norm) self.G = sinkhorn_l1l2_gl(ws, ys, wt, self.M, reg, eta, **kwargs) self.computed = True -- cgit v1.2.3 From 8c525174bb664cafa98dfff73dce9d42d7818f71 Mon Sep 17 00:00:00 2001 From: Nicolas Courty Date: Thu, 31 Aug 2017 16:44:18 +0200 Subject: Minor corrections suggested by @agramfort + new barycenter example + test function --- README.md | 2 +- data/carre.png | Bin 0 -> 168 bytes data/coeur.png | Bin 0 -> 225 bytes data/rond.png | Bin 0 -> 230 bytes data/triangle.png | Bin 0 -> 254 bytes examples/plot_gromov.py | 14 +-- examples/plot_gromov_barycenter.py | 240 +++++++++++++++++++++++++++++++++++++ ot/gromov.py | 36 +++--- test/test_gromov.py | 38 ++++++ 9 files changed, 302 insertions(+), 28 deletions(-) create mode 100755 data/carre.png create mode 100755 data/coeur.png create mode 100755 data/rond.png create mode 100755 data/triangle.png create mode 100755 examples/plot_gromov_barycenter.py create mode 100644 test/test_gromov.py diff --git a/README.md b/README.md index 257244b..22b20a4 100644 --- a/README.md +++ b/README.md @@ -185,4 +185,4 @@ You can also post bug reports and feature requests in Github issues. Make sure t [11] Flamary, R., Cuturi, M., Courty, N., & Rakotomamonjy, A. (2016). [Wasserstein Discriminant Analysis](https://arxiv.org/pdf/1608.08063.pdf). arXiv preprint arXiv:1608.08063. -[12] Peyré, Gabriel, Marco Cuturi, and Justin Solomon, [Gromov-Wasserstein averaging of kernel and distance matrices](http://proceedings.mlr.press/v48/peyre16.html) International Conference on Machine Learning (ICML). 2016. +[12] Gabriel Peyré, Marco Cuturi, and Justin Solomon, [Gromov-Wasserstein averaging of kernel and distance matrices](http://proceedings.mlr.press/v48/peyre16.html) International Conference on Machine Learning (ICML). 2016. diff --git a/data/carre.png b/data/carre.png new file mode 100755 index 0000000..45ff0ef Binary files /dev/null and b/data/carre.png differ diff --git a/data/coeur.png b/data/coeur.png new file mode 100755 index 0000000..3f511a6 Binary files /dev/null and b/data/coeur.png differ diff --git a/data/rond.png b/data/rond.png new file mode 100755 index 0000000..1c1a068 Binary files /dev/null and b/data/rond.png differ diff --git a/data/triangle.png b/data/triangle.png new file mode 100755 index 0000000..ca36d09 Binary files /dev/null and b/data/triangle.png differ diff --git a/examples/plot_gromov.py b/examples/plot_gromov.py index a33fde1..9bbdbde 100644 --- a/examples/plot_gromov.py +++ b/examples/plot_gromov.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- """ -==================== +========================== Gromov-Wasserstein example -==================== +========================== This example is designed to show how to use the Gromov-Wassertsein distance computation in POT. """ @@ -14,14 +14,14 @@ computation in POT. import scipy as sp import numpy as np +import matplotlib.pylab as pl import ot -import matplotlib.pylab as pl """ Sample two Gaussian distributions (2D and 3D) -==================== +============================================= The Gromov-Wasserstein distance allows to compute distances with samples that do not belong to the same metric space. For demonstration purpose, we sample two Gaussian distributions in 2- and 3-dimensional spaces. """ @@ -42,7 +42,7 @@ xt = np.random.randn(n, 3).dot(P) + mu_t """ Plotting the distributions -==================== +========================== """ fig = pl.figure() ax1 = fig.add_subplot(121) @@ -54,7 +54,7 @@ pl.show() """ Compute distance kernels, normalize them and then display -==================== +========================================================= """ C1 = sp.spatial.distance.cdist(xs, xs) @@ -72,7 +72,7 @@ pl.show() """ Compute Gromov-Wasserstein plans and distance -==================== +============================================= """ p = ot.unif(n) diff --git a/examples/plot_gromov_barycenter.py b/examples/plot_gromov_barycenter.py new file mode 100755 index 0000000..6a72b3b --- /dev/null +++ b/examples/plot_gromov_barycenter.py @@ -0,0 +1,240 @@ +# -*- coding: utf-8 -*- +""" +===================================== +Gromov-Wasserstein Barycenter example +===================================== +This example is designed to show how to use the Gromov-Wassertsein distance +computation in POT. +""" + +# Author: Erwan Vautier +# Nicolas Courty +# +# License: MIT License + + +import numpy as np +import scipy as sp + +import scipy.ndimage as spi +import matplotlib.pylab as pl +from sklearn import manifold +from sklearn.decomposition import PCA + +import ot + +""" + +Smacof MDS +========== +This function allows to find an embedding of points given a dissimilarity matrix +that will be given by the output of the algorithm +""" + + +def smacof_mds(C, dim, maxIter=3000, eps=1e-9): + """ + Returns an interpolated point cloud following the dissimilarity matrix C using SMACOF + multidimensional scaling (MDS) in specific dimensionned target space + + Parameters + ---------- + C : np.ndarray(ns,ns) + dissimilarity matrix + dim : Integer + dimension of the targeted space + maxIter : Maximum number of iterations of the SMACOF algorithm for a single run + + eps : relative tolerance w.r.t stress to declare converge + + + Returns + ------- + npos : R**dim ndarray + Embedded coordinates of the interpolated point cloud (defined with one isometry) + + + """ + + seed = np.random.RandomState(seed=3) + + mds = manifold.MDS( + dim, + max_iter=3000, + eps=1e-9, + dissimilarity='precomputed', + n_init=1) + pos = mds.fit(C).embedding_ + + nmds = manifold.MDS( + 2, + max_iter=3000, + eps=1e-9, + dissimilarity="precomputed", + random_state=seed, + n_init=1) + npos = nmds.fit_transform(C, init=pos) + + return npos + + +""" +Data preparation +================ +The four distributions are constructed from 4 simple images +""" + + +def im2mat(I): + """Converts and image to matrix (one pixel per line)""" + return I.reshape((I.shape[0] * I.shape[1], I.shape[2])) + + +carre = spi.imread('../data/carre.png').astype(np.float64) / 256 +rond = spi.imread('../data/rond.png').astype(np.float64) / 256 +triangle = spi.imread('../data/triangle.png').astype(np.float64) / 256 +fleche = spi.imread('../data/coeur.png').astype(np.float64) / 256 + +shapes = [carre, rond, triangle, fleche] + +S = 4 +xs = [[] for i in range(S)] + + +for nb in range(4): + for i in range(8): + for j in range(8): + if shapes[nb][i, j] < 0.95: + xs[nb].append([j, 8 - i]) + +xs = np.array([np.array(xs[0]), np.array(xs[1]), + np.array(xs[2]), np.array(xs[3])]) + + +""" +Barycenter computation +====================== +The four distributions are constructed from 4 simple images +""" +ns = [len(xs[s]) for s in range(S)] +N = 30 + +"""Compute all distances matrices for the four shapes""" +Cs = [sp.spatial.distance.cdist(xs[s], xs[s]) for s in range(S)] +Cs = [cs / cs.max() for cs in Cs] + +ps = [ot.unif(ns[s]) for s in range(S)] +p = ot.unif(N) + + +lambdast = [[float(i) / 3, float(3 - i) / 3] for i in [1, 2]] + +Ct01 = [0 for i in range(2)] +for i in range(2): + Ct01[i] = ot.gromov.gromov_barycenters(N, [Cs[0], Cs[1]], [ + ps[0], ps[1]], p, lambdast[i], 'square_loss', 5e-4, numItermax=100, stopThr=1e-3) + +Ct02 = [0 for i in range(2)] +for i in range(2): + Ct02[i] = ot.gromov.gromov_barycenters(N, [Cs[0], Cs[2]], [ + ps[0], ps[2]], p, lambdast[i], 'square_loss', 5e-4, numItermax=100, stopThr=1e-3) + +Ct13 = [0 for i in range(2)] +for i in range(2): + Ct13[i] = ot.gromov.gromov_barycenters(N, [Cs[1], Cs[3]], [ + ps[1], ps[3]], p, lambdast[i], 'square_loss', 5e-4, numItermax=100, stopThr=1e-3) + +Ct23 = [0 for i in range(2)] +for i in range(2): + Ct23[i] = ot.gromov.gromov_barycenters(N, [Cs[2], Cs[3]], [ + ps[2], ps[3]], p, lambdast[i], 'square_loss', 5e-4, numItermax=100, stopThr=1e-3) + +""" +Visualization +============= +""" + +"""The PCA helps in getting consistency between the rotations""" + +clf = PCA(n_components=2) +npos = [0, 0, 0, 0] +npos = [smacof_mds(Cs[s], 2) for s in range(S)] + +npost01 = [0, 0] +npost01 = [smacof_mds(Ct01[s], 2) for s in range(2)] +npost01 = [clf.fit_transform(npost01[s]) for s in range(2)] + +npost02 = [0, 0] +npost02 = [smacof_mds(Ct02[s], 2) for s in range(2)] +npost02 = [clf.fit_transform(npost02[s]) for s in range(2)] + +npost13 = [0, 0] +npost13 = [smacof_mds(Ct13[s], 2) for s in range(2)] +npost13 = [clf.fit_transform(npost13[s]) for s in range(2)] + +npost23 = [0, 0] +npost23 = [smacof_mds(Ct23[s], 2) for s in range(2)] +npost23 = [clf.fit_transform(npost23[s]) for s in range(2)] + + +fig = pl.figure(figsize=(10, 10)) + +ax1 = pl.subplot2grid((4, 4), (0, 0)) +pl.xlim((-1, 1)) +pl.ylim((-1, 1)) +ax1.scatter(npos[0][:, 0], npos[0][:, 1], color='r') + +ax2 = pl.subplot2grid((4, 4), (0, 1)) +pl.xlim((-1, 1)) +pl.ylim((-1, 1)) +ax2.scatter(npost01[1][:, 0], npost01[1][:, 1], color='b') + +ax3 = pl.subplot2grid((4, 4), (0, 2)) +pl.xlim((-1, 1)) +pl.ylim((-1, 1)) +ax3.scatter(npost01[0][:, 0], npost01[0][:, 1], color='b') + +ax4 = pl.subplot2grid((4, 4), (0, 3)) +pl.xlim((-1, 1)) +pl.ylim((-1, 1)) +ax4.scatter(npos[1][:, 0], npos[1][:, 1], color='r') + +ax5 = pl.subplot2grid((4, 4), (1, 0)) +pl.xlim((-1, 1)) +pl.ylim((-1, 1)) +ax5.scatter(npost02[1][:, 0], npost02[1][:, 1], color='b') + +ax6 = pl.subplot2grid((4, 4), (1, 3)) +pl.xlim((-1, 1)) +pl.ylim((-1, 1)) +ax6.scatter(npost13[1][:, 0], npost13[1][:, 1], color='b') + +ax7 = pl.subplot2grid((4, 4), (2, 0)) +pl.xlim((-1, 1)) +pl.ylim((-1, 1)) +ax7.scatter(npost02[0][:, 0], npost02[0][:, 1], color='b') + +ax8 = pl.subplot2grid((4, 4), (2, 3)) +pl.xlim((-1, 1)) +pl.ylim((-1, 1)) +ax8.scatter(npost13[0][:, 0], npost13[0][:, 1], color='b') + +ax9 = pl.subplot2grid((4, 4), (3, 0)) +pl.xlim((-1, 1)) +pl.ylim((-1, 1)) +ax9.scatter(npos[2][:, 0], npos[2][:, 1], color='r') + +ax10 = pl.subplot2grid((4, 4), (3, 1)) +pl.xlim((-1, 1)) +pl.ylim((-1, 1)) +ax10.scatter(npost23[1][:, 0], npost23[1][:, 1], color='b') + +ax11 = pl.subplot2grid((4, 4), (3, 2)) +pl.xlim((-1, 1)) +pl.ylim((-1, 1)) +ax11.scatter(npost23[0][:, 0], npost23[0][:, 1], color='b') + +ax12 = pl.subplot2grid((4, 4), (3, 3)) +pl.xlim((-1, 1)) +pl.ylim((-1, 1)) +ax12.scatter(npos[3][:, 0], npos[3][:, 1], color='r') diff --git a/ot/gromov.py b/ot/gromov.py index 7cf3b42..421ed3f 100644 --- a/ot/gromov.py +++ b/ot/gromov.py @@ -23,7 +23,7 @@ def square_loss(a, b): Returns the value of L(a,b)=(1/2)*|a-b|^2 """ - return (1 / 2) * (a - b)**2 + return 0.5 * (a - b)**2 def kl_loss(a, b): @@ -54,9 +54,9 @@ def tensor_square_loss(C1, C2, T): Parameters ---------- - C1 : np.ndarray(ns,ns) + C1 : ndarray, shape (ns, ns) Metric cost matrix in the source space - C2 : np.ndarray(nt,nt) + C2 : ndarray, shape (nt, nt) Metric costfr matrix in the target space T : np.ndarray(ns,nt) Coupling between source and target spaces @@ -87,7 +87,7 @@ def tensor_square_loss(C1, C2, T): return b tens = -np.dot(h1(C1), T).dot(h2(C2).T) - tens = tens - tens.min() + tens -= tens.min() return np.array(tens) @@ -112,9 +112,9 @@ def tensor_kl_loss(C1, C2, T): Parameters ---------- - C1 : np.ndarray(ns,ns) + C1 : ndarray, shape (ns, ns) Metric cost matrix in the source space - C2 : np.ndarray(nt,nt) + C2 : ndarray, shape (nt, nt) Metric costfr matrix in the target space T : np.ndarray(ns,nt) Coupling between source and target spaces @@ -149,7 +149,7 @@ def tensor_kl_loss(C1, C2, T): return np.log(b + 1e-15) tens = -np.dot(h1(C1), T).dot(h2(C2).T) - tens = tens - tens.min() + tens -= tens.min() return np.array(tens) @@ -175,9 +175,8 @@ def update_square_loss(p, lambdas, T, Cs): """ - tmpsum = np.sum([lambdas[s] * np.dot(T[s].T, Cs[s]).dot(T[s]) - for s in range(len(T))]) - ppt = np.dot(p, p.T) + tmpsum = sum([lambdas[s] * np.dot(T[s].T, Cs[s]).dot(T[s]) for s in range(len(T))]) + ppt = np.outer(p, p) return(np.divide(tmpsum, ppt)) @@ -203,9 +202,8 @@ def update_kl_loss(p, lambdas, T, Cs): """ - tmpsum = np.sum([lambdas[s] * np.dot(T[s].T, Cs[s]).dot(T[s]) - for s in range(len(T))]) - ppt = np.dot(p, p.T) + tmpsum = sum([lambdas[s] * np.dot(T[s].T, Cs[s]).dot(T[s]) for s in range(len(T))]) + ppt = np.outer(p, p) return(np.exp(np.divide(tmpsum, ppt))) @@ -239,9 +237,9 @@ def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopThr Parameters ---------- - C1 : np.ndarray(ns,ns) + C1 : ndarray, shape (ns, ns) Metric cost matrix in the source space - C2 : np.ndarray(nt,nt) + C2 : ndarray, shape (nt, nt) Metric costfr matrix in the target space p : np.ndarray(ns,) distribution in the source space @@ -271,7 +269,7 @@ def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopThr C1 = np.asarray(C1, dtype=np.float64) C2 = np.asarray(C2, dtype=np.float64) - T = np.dot(p, q.T) # Initialization + T = np.outer(p, q) # Initialization cpt = 0 err = 1 @@ -333,9 +331,9 @@ def gromov_wasserstein2(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopTh Parameters ---------- - C1 : np.ndarray(ns,ns) + C1 : ndarray, shape (ns, ns) Metric cost matrix in the source space - C2 : np.ndarray(nt,nt) + C2 : ndarray, shape (nt, nt) Metric costfr matrix in the target space p : np.ndarray(ns,) distribution in the source space @@ -434,8 +432,6 @@ def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, numItermax=1000 Cs = [np.asarray(Cs[s], dtype=np.float64) for s in range(S)] lambdas = np.asarray(lambdas, dtype=np.float64) - T = [0 for s in range(S)] - # Initialization of C : random SPD matrix xalea = np.random.randn(N, 2) C = dist(xalea, xalea) diff --git a/test/test_gromov.py b/test/test_gromov.py new file mode 100644 index 0000000..75eeaab --- /dev/null +++ b/test/test_gromov.py @@ -0,0 +1,38 @@ +"""Tests for module gromov """ + +# Author: Erwan Vautier +# Nicolas Courty +# +# License: MIT License + +import numpy as np +import ot + + +def test_gromov(): + n = 50 # nb samples + + mu_s = np.array([0, 0]) + cov_s = np.array([[1, 0], [0, 1]]) + + xs = ot.datasets.get_2D_samples_gauss(n, mu_s, cov_s) + + xt = [xs[n - (i + 1)] for i in range(n)] + xt = np.array(xt) + + p = ot.unif(n) + q = ot.unif(n) + + C1 = ot.dist(xs, xs) + C2 = ot.dist(xt, xt) + + C1 /= C1.max() + C2 /= C2.max() + + G = ot.gromov_wasserstein(C1, C2, p, q, 'square_loss', epsilon=5e-4) + + # check constratints + np.testing.assert_allclose( + p, G.sum(1), atol=1e-04) # cf convergence gromov + np.testing.assert_allclose( + q, G.sum(0), atol=1e-04) # cf convergence gromov -- cgit v1.2.3 From 4ec5b339ef527d4d49a022ddf57b38dff037548c Mon Sep 17 00:00:00 2001 From: Nicolas Courty Date: Thu, 31 Aug 2017 17:17:30 +0200 Subject: minor corrections --- examples/plot_gromov_barycenter.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/plot_gromov_barycenter.py b/examples/plot_gromov_barycenter.py index 6a72b3b..da52768 100755 --- a/examples/plot_gromov_barycenter.py +++ b/examples/plot_gromov_barycenter.py @@ -32,18 +32,19 @@ that will be given by the output of the algorithm """ -def smacof_mds(C, dim, maxIter=3000, eps=1e-9): +def smacof_mds(C, dim, max_iter=3000, eps=1e-9): """ Returns an interpolated point cloud following the dissimilarity matrix C using SMACOF multidimensional scaling (MDS) in specific dimensionned target space Parameters ---------- - C : np.ndarray(ns,ns) + C : ndarray, shape (ns, ns) dissimilarity matrix - dim : Integer + dim : int dimension of the targeted space - maxIter : Maximum number of iterations of the SMACOF algorithm for a single run + max_iter : int + Maximum number of iterations of the SMACOF algorithm for a single run eps : relative tolerance w.r.t stress to declare converge @@ -60,7 +61,7 @@ def smacof_mds(C, dim, maxIter=3000, eps=1e-9): mds = manifold.MDS( dim, - max_iter=3000, + max_iter=max_iter, eps=1e-9, dissimilarity='precomputed', n_init=1) @@ -68,7 +69,7 @@ def smacof_mds(C, dim, maxIter=3000, eps=1e-9): nmds = manifold.MDS( 2, - max_iter=3000, + max_iter=max_iter, eps=1e-9, dissimilarity="precomputed", random_state=seed, -- cgit v1.2.3 From ab6ed1df93cd78bb7f1a54282103d4d830e68bcb Mon Sep 17 00:00:00 2001 From: Nicolas Courty Date: Fri, 1 Sep 2017 11:20:34 +0200 Subject: docstrings and naming --- examples/plot_gromov.py | 10 +++++----- examples/plot_gromov_barycenter.py | 20 ++++++++++---------- ot/gromov.py | 18 +++++++++--------- test/test_gromov.py | 10 +++++----- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/examples/plot_gromov.py b/examples/plot_gromov.py index 9bbdbde..92312ae 100644 --- a/examples/plot_gromov.py +++ b/examples/plot_gromov.py @@ -26,7 +26,7 @@ The Gromov-Wasserstein distance allows to compute distances with samples that do For demonstration purpose, we sample two Gaussian distributions in 2- and 3-dimensional spaces. """ -n = 30 # nb samples +n_samples = 30 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) @@ -35,9 +35,9 @@ mu_t = np.array([4, 4, 4]) cov_t = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) -xs = ot.datasets.get_2D_samples_gauss(n, mu_s, cov_s) +xs = ot.datasets.get_2D_samples_gauss(n_samples, mu_s, cov_s) P = sp.linalg.sqrtm(cov_t) -xt = np.random.randn(n, 3).dot(P) + mu_t +xt = np.random.randn(n_samples, 3).dot(P) + mu_t """ @@ -75,8 +75,8 @@ Compute Gromov-Wasserstein plans and distance ============================================= """ -p = ot.unif(n) -q = ot.unif(n) +p = ot.unif(n_samples) +q = ot.unif(n_samples) gw = ot.gromov_wasserstein(C1, C2, p, q, 'square_loss', epsilon=5e-4) gw_dist = ot.gromov_wasserstein2(C1, C2, p, q, 'square_loss', epsilon=5e-4) diff --git a/examples/plot_gromov_barycenter.py b/examples/plot_gromov_barycenter.py index da52768..f0657e1 100755 --- a/examples/plot_gromov_barycenter.py +++ b/examples/plot_gromov_barycenter.py @@ -91,12 +91,12 @@ def im2mat(I): return I.reshape((I.shape[0] * I.shape[1], I.shape[2])) -carre = spi.imread('../data/carre.png').astype(np.float64) / 256 -rond = spi.imread('../data/rond.png').astype(np.float64) / 256 +square = spi.imread('../data/carre.png').astype(np.float64) / 256 +circle = spi.imread('../data/rond.png').astype(np.float64) / 256 triangle = spi.imread('../data/triangle.png').astype(np.float64) / 256 -fleche = spi.imread('../data/coeur.png').astype(np.float64) / 256 +arrow = spi.imread('../data/coeur.png').astype(np.float64) / 256 -shapes = [carre, rond, triangle, fleche] +shapes = [square, circle, triangle, arrow] S = 4 xs = [[] for i in range(S)] @@ -118,36 +118,36 @@ Barycenter computation The four distributions are constructed from 4 simple images """ ns = [len(xs[s]) for s in range(S)] -N = 30 +n_samples = 30 """Compute all distances matrices for the four shapes""" Cs = [sp.spatial.distance.cdist(xs[s], xs[s]) for s in range(S)] Cs = [cs / cs.max() for cs in Cs] ps = [ot.unif(ns[s]) for s in range(S)] -p = ot.unif(N) +p = ot.unif(n_samples) lambdast = [[float(i) / 3, float(3 - i) / 3] for i in [1, 2]] Ct01 = [0 for i in range(2)] for i in range(2): - Ct01[i] = ot.gromov.gromov_barycenters(N, [Cs[0], Cs[1]], [ + Ct01[i] = ot.gromov.gromov_barycenters(n_samples, [Cs[0], Cs[1]], [ ps[0], ps[1]], p, lambdast[i], 'square_loss', 5e-4, numItermax=100, stopThr=1e-3) Ct02 = [0 for i in range(2)] for i in range(2): - Ct02[i] = ot.gromov.gromov_barycenters(N, [Cs[0], Cs[2]], [ + Ct02[i] = ot.gromov.gromov_barycenters(n_samples, [Cs[0], Cs[2]], [ ps[0], ps[2]], p, lambdast[i], 'square_loss', 5e-4, numItermax=100, stopThr=1e-3) Ct13 = [0 for i in range(2)] for i in range(2): - Ct13[i] = ot.gromov.gromov_barycenters(N, [Cs[1], Cs[3]], [ + Ct13[i] = ot.gromov.gromov_barycenters(n_samples, [Cs[1], Cs[3]], [ ps[1], ps[3]], p, lambdast[i], 'square_loss', 5e-4, numItermax=100, stopThr=1e-3) Ct23 = [0 for i in range(2)] for i in range(2): - Ct23[i] = ot.gromov.gromov_barycenters(N, [Cs[2], Cs[3]], [ + Ct23[i] = ot.gromov.gromov_barycenters(n_samples, [Cs[2], Cs[3]], [ ps[2], ps[3]], p, lambdast[i], 'square_loss', 5e-4, numItermax=100, stopThr=1e-3) """ diff --git a/ot/gromov.py b/ot/gromov.py index 421ed3f..ad85fcd 100644 --- a/ot/gromov.py +++ b/ot/gromov.py @@ -208,7 +208,7 @@ def update_kl_loss(p, lambdas, T, Cs): return(np.exp(np.divide(tmpsum, ppt))) -def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopThr=1e-9, verbose=False, log=False): +def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, stopThr=1e-9, verbose=False, log=False): """ Returns the gromov-wasserstein coupling between the two measured similarity matrices @@ -248,7 +248,7 @@ def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopThr loss_fun : loss function used for the solver either 'square_loss' or 'kl_loss' epsilon : float Regularization term >0 - numItermax : int, optional + max_iter : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) @@ -274,7 +274,7 @@ def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopThr cpt = 0 err = 1 - while (err > stopThr and cpt < numItermax): + while (err > stopThr and cpt < max_iter): Tprev = T @@ -307,7 +307,7 @@ def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopThr return T -def gromov_wasserstein2(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopThr=1e-9, verbose=False, log=False): +def gromov_wasserstein2(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, stopThr=1e-9, verbose=False, log=False): """ Returns the gromov-wasserstein discrepancy between the two measured similarity matrices @@ -362,10 +362,10 @@ def gromov_wasserstein2(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopTh if log: gw, logv = gromov_wasserstein( - C1, C2, p, q, loss_fun, epsilon, numItermax, stopThr, verbose, log) + C1, C2, p, q, loss_fun, epsilon, max_iter, stopThr, verbose, log) else: gw = gromov_wasserstein(C1, C2, p, q, loss_fun, - epsilon, numItermax, stopThr, verbose, log) + epsilon, max_iter, stopThr, verbose, log) if loss_fun == 'square_loss': gw_dist = np.sum(gw * tensor_square_loss(C1, C2, gw)) @@ -379,7 +379,7 @@ def gromov_wasserstein2(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopTh return gw_dist -def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, numItermax=1000, stopThr=1e-9, verbose=False, log=False): +def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, max_iter=1000, stopThr=1e-9, verbose=False, log=False): """ Returns the gromov-wasserstein barycenters of S measured similarity matrices @@ -442,12 +442,12 @@ def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, numItermax=1000 error = [] - while(err > stopThr and cpt < numItermax): + while(err > stopThr and cpt < max_iter): Cprev = C T = [gromov_wasserstein(Cs[s], C, ps[s], p, loss_fun, epsilon, - numItermax, 1e-5, verbose, log) for s in range(S)] + max_iter, 1e-5, verbose, log) for s in range(S)] if loss_fun == 'square_loss': C = update_square_loss(p, lambdas, T, Cs) diff --git a/test/test_gromov.py b/test/test_gromov.py index 75eeaab..c26d898 100644 --- a/test/test_gromov.py +++ b/test/test_gromov.py @@ -10,18 +10,18 @@ import ot def test_gromov(): - n = 50 # nb samples + n_samples = 50 # nb samples mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) - xs = ot.datasets.get_2D_samples_gauss(n, mu_s, cov_s) + xs = ot.datasets.get_2D_samples_gauss(n_samples, mu_s, cov_s) - xt = [xs[n - (i + 1)] for i in range(n)] + xt = [xs[n_samples - (i + 1)] for i in range(n_samples)] xt = np.array(xt) - p = ot.unif(n) - q = ot.unif(n) + p = ot.unif(n_samples) + q = ot.unif(n_samples) C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) -- cgit v1.2.3 From 46fc12a298c49b715ac953cff391b18b54dab0f0 Mon Sep 17 00:00:00 2001 From: Nicolas Courty Date: Fri, 1 Sep 2017 11:43:51 +0200 Subject: solving conflicts :/ --- examples/plot_gromov.py | 15 ------------- examples/plot_gromov_barycenter.py | 33 ----------------------------- ot/gromov.py | 43 +++++--------------------------------- test/test_gromov.py | 14 ------------- 4 files changed, 5 insertions(+), 100 deletions(-) diff --git a/examples/plot_gromov.py b/examples/plot_gromov.py index 99aaf81..92312ae 100644 --- a/examples/plot_gromov.py +++ b/examples/plot_gromov.py @@ -26,11 +26,7 @@ The Gromov-Wasserstein distance allows to compute distances with samples that do For demonstration purpose, we sample two Gaussian distributions in 2- and 3-dimensional spaces. """ -<<<<<<< HEAD n_samples = 30 # nb samples -======= -n = 30 # nb samples ->>>>>>> 986f46ddde3ce2f550cb56f66620df377326423d mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) @@ -39,15 +35,9 @@ mu_t = np.array([4, 4, 4]) cov_t = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) -<<<<<<< HEAD xs = ot.datasets.get_2D_samples_gauss(n_samples, mu_s, cov_s) P = sp.linalg.sqrtm(cov_t) xt = np.random.randn(n_samples, 3).dot(P) + mu_t -======= -xs = ot.datasets.get_2D_samples_gauss(n, mu_s, cov_s) -P = sp.linalg.sqrtm(cov_t) -xt = np.random.randn(n, 3).dot(P) + mu_t ->>>>>>> 986f46ddde3ce2f550cb56f66620df377326423d """ @@ -85,13 +75,8 @@ Compute Gromov-Wasserstein plans and distance ============================================= """ -<<<<<<< HEAD p = ot.unif(n_samples) q = ot.unif(n_samples) -======= -p = ot.unif(n) -q = ot.unif(n) ->>>>>>> 986f46ddde3ce2f550cb56f66620df377326423d gw = ot.gromov_wasserstein(C1, C2, p, q, 'square_loss', epsilon=5e-4) gw_dist = ot.gromov_wasserstein2(C1, C2, p, q, 'square_loss', epsilon=5e-4) diff --git a/examples/plot_gromov_barycenter.py b/examples/plot_gromov_barycenter.py index 46ec4bc..f0657e1 100755 --- a/examples/plot_gromov_barycenter.py +++ b/examples/plot_gromov_barycenter.py @@ -91,21 +91,12 @@ def im2mat(I): return I.reshape((I.shape[0] * I.shape[1], I.shape[2])) -<<<<<<< HEAD square = spi.imread('../data/carre.png').astype(np.float64) / 256 circle = spi.imread('../data/rond.png').astype(np.float64) / 256 triangle = spi.imread('../data/triangle.png').astype(np.float64) / 256 arrow = spi.imread('../data/coeur.png').astype(np.float64) / 256 shapes = [square, circle, triangle, arrow] -======= -carre = spi.imread('../data/carre.png').astype(np.float64) / 256 -rond = spi.imread('../data/rond.png').astype(np.float64) / 256 -triangle = spi.imread('../data/triangle.png').astype(np.float64) / 256 -fleche = spi.imread('../data/coeur.png').astype(np.float64) / 256 - -shapes = [carre, rond, triangle, fleche] ->>>>>>> 986f46ddde3ce2f550cb56f66620df377326423d S = 4 xs = [[] for i in range(S)] @@ -127,60 +118,36 @@ Barycenter computation The four distributions are constructed from 4 simple images """ ns = [len(xs[s]) for s in range(S)] -<<<<<<< HEAD n_samples = 30 -======= -N = 30 ->>>>>>> 986f46ddde3ce2f550cb56f66620df377326423d """Compute all distances matrices for the four shapes""" Cs = [sp.spatial.distance.cdist(xs[s], xs[s]) for s in range(S)] Cs = [cs / cs.max() for cs in Cs] ps = [ot.unif(ns[s]) for s in range(S)] -<<<<<<< HEAD p = ot.unif(n_samples) -======= -p = ot.unif(N) ->>>>>>> 986f46ddde3ce2f550cb56f66620df377326423d lambdast = [[float(i) / 3, float(3 - i) / 3] for i in [1, 2]] Ct01 = [0 for i in range(2)] for i in range(2): -<<<<<<< HEAD Ct01[i] = ot.gromov.gromov_barycenters(n_samples, [Cs[0], Cs[1]], [ -======= - Ct01[i] = ot.gromov.gromov_barycenters(N, [Cs[0], Cs[1]], [ ->>>>>>> 986f46ddde3ce2f550cb56f66620df377326423d ps[0], ps[1]], p, lambdast[i], 'square_loss', 5e-4, numItermax=100, stopThr=1e-3) Ct02 = [0 for i in range(2)] for i in range(2): -<<<<<<< HEAD Ct02[i] = ot.gromov.gromov_barycenters(n_samples, [Cs[0], Cs[2]], [ -======= - Ct02[i] = ot.gromov.gromov_barycenters(N, [Cs[0], Cs[2]], [ ->>>>>>> 986f46ddde3ce2f550cb56f66620df377326423d ps[0], ps[2]], p, lambdast[i], 'square_loss', 5e-4, numItermax=100, stopThr=1e-3) Ct13 = [0 for i in range(2)] for i in range(2): -<<<<<<< HEAD Ct13[i] = ot.gromov.gromov_barycenters(n_samples, [Cs[1], Cs[3]], [ -======= - Ct13[i] = ot.gromov.gromov_barycenters(N, [Cs[1], Cs[3]], [ ->>>>>>> 986f46ddde3ce2f550cb56f66620df377326423d ps[1], ps[3]], p, lambdast[i], 'square_loss', 5e-4, numItermax=100, stopThr=1e-3) Ct23 = [0 for i in range(2)] for i in range(2): -<<<<<<< HEAD Ct23[i] = ot.gromov.gromov_barycenters(n_samples, [Cs[2], Cs[3]], [ -======= - Ct23[i] = ot.gromov.gromov_barycenters(N, [Cs[2], Cs[3]], [ ->>>>>>> 986f46ddde3ce2f550cb56f66620df377326423d ps[2], ps[3]], p, lambdast[i], 'square_loss', 5e-4, numItermax=100, stopThr=1e-3) """ diff --git a/ot/gromov.py b/ot/gromov.py index 197e3ea..9dbf463 100644 --- a/ot/gromov.py +++ b/ot/gromov.py @@ -208,11 +208,7 @@ def update_kl_loss(p, lambdas, T, Cs): return(np.exp(np.divide(tmpsum, ppt))) -<<<<<<< HEAD def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, stopThr=1e-9, verbose=False, log=False): -======= -def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopThr=1e-9, verbose=False, log=False): ->>>>>>> 986f46ddde3ce2f550cb56f66620df377326423d """ Returns the gromov-wasserstein coupling between the two measured similarity matrices @@ -252,11 +248,11 @@ def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopThr loss_fun : loss function used for the solver either 'square_loss' or 'kl_loss' epsilon : float Regularization term >0 -<<<<<<< HEAD +<<<<<<< HEAD max_iter : int, optional -======= +======= numItermax : int, optional ->>>>>>> 986f46ddde3ce2f550cb56f66620df377326423d +>>>>>>> 986f46ddde3ce2f550cb56f66620df377326423d Max number of iterations stopThr : float, optional Stop threshold on error (>0) @@ -282,11 +278,7 @@ def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopThr cpt = 0 err = 1 -<<<<<<< HEAD while (err > stopThr and cpt < max_iter): -======= - while (err > stopThr and cpt < numItermax): ->>>>>>> 986f46ddde3ce2f550cb56f66620df377326423d Tprev = T @@ -319,11 +311,7 @@ def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopThr return T -<<<<<<< HEAD def gromov_wasserstein2(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, stopThr=1e-9, verbose=False, log=False): -======= -def gromov_wasserstein2(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopThr=1e-9, verbose=False, log=False): ->>>>>>> 986f46ddde3ce2f550cb56f66620df377326423d """ Returns the gromov-wasserstein discrepancy between the two measured similarity matrices @@ -358,7 +346,7 @@ def gromov_wasserstein2(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopTh loss_fun : loss function used for the solver either 'square_loss' or 'kl_loss' epsilon : float Regularization term >0 - numItermax : int, optional + max_iter : int, optional Max number of iterations stopThr : float, optional Stop threshold on error (>0) @@ -378,17 +366,10 @@ def gromov_wasserstein2(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopTh if log: gw, logv = gromov_wasserstein( -<<<<<<< HEAD C1, C2, p, q, loss_fun, epsilon, max_iter, stopThr, verbose, log) else: gw = gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, max_iter, stopThr, verbose, log) -======= - C1, C2, p, q, loss_fun, epsilon, numItermax, stopThr, verbose, log) - else: - gw = gromov_wasserstein(C1, C2, p, q, loss_fun, - epsilon, numItermax, stopThr, verbose, log) ->>>>>>> 986f46ddde3ce2f550cb56f66620df377326423d if loss_fun == 'square_loss': gw_dist = np.sum(gw * tensor_square_loss(C1, C2, gw)) @@ -402,11 +383,7 @@ def gromov_wasserstein2(C1, C2, p, q, loss_fun, epsilon, numItermax=1000, stopTh return gw_dist -<<<<<<< HEAD def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, max_iter=1000, stopThr=1e-9, verbose=False, log=False): -======= -def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, numItermax=1000, stopThr=1e-9, verbose=False, log=False): ->>>>>>> 986f46ddde3ce2f550cb56f66620df377326423d """ Returns the gromov-wasserstein barycenters of S measured similarity matrices @@ -439,7 +416,7 @@ def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, numItermax=1000 with the S Ts couplings calculated at each iteration epsilon : float Regularization term >0 - numItermax : int, optional + max_iter : int, optional Max number of iterations stopThr : float, optional Stop threshol on error (>0) @@ -469,21 +446,11 @@ def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, numItermax=1000 error = [] -<<<<<<< HEAD while(err > stopThr and cpt < max_iter): -======= - while(err > stopThr and cpt < numItermax): ->>>>>>> 986f46ddde3ce2f550cb56f66620df377326423d - Cprev = C T = [gromov_wasserstein(Cs[s], C, ps[s], p, loss_fun, epsilon, -<<<<<<< HEAD max_iter, 1e-5, verbose, log) for s in range(S)] -======= - numItermax, 1e-5, verbose, log) for s in range(S)] ->>>>>>> 986f46ddde3ce2f550cb56f66620df377326423d - if loss_fun == 'square_loss': C = update_square_loss(p, lambdas, T, Cs) diff --git a/test/test_gromov.py b/test/test_gromov.py index a6c89f2..c26d898 100644 --- a/test/test_gromov.py +++ b/test/test_gromov.py @@ -10,16 +10,11 @@ import ot def test_gromov(): -<<<<<<< HEAD n_samples = 50 # nb samples -======= - n = 50 # nb samples ->>>>>>> 986f46ddde3ce2f550cb56f66620df377326423d mu_s = np.array([0, 0]) cov_s = np.array([[1, 0], [0, 1]]) -<<<<<<< HEAD xs = ot.datasets.get_2D_samples_gauss(n_samples, mu_s, cov_s) xt = [xs[n_samples - (i + 1)] for i in range(n_samples)] @@ -27,15 +22,6 @@ def test_gromov(): p = ot.unif(n_samples) q = ot.unif(n_samples) -======= - xs = ot.datasets.get_2D_samples_gauss(n, mu_s, cov_s) - - xt = [xs[n - (i + 1)] for i in range(n)] - xt = np.array(xt) - - p = ot.unif(n) - q = ot.unif(n) ->>>>>>> 986f46ddde3ce2f550cb56f66620df377326423d C1 = ot.dist(xs, xs) C2 = ot.dist(xt, xt) -- cgit v1.2.3 From f12322c1a288baedffd5b6aedcff15747aadac8e Mon Sep 17 00:00:00 2001 From: Nicolas Courty Date: Fri, 1 Sep 2017 14:07:28 +0200 Subject: add barycenters to Readme.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 22b20a4..a1debf6 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ It provides the following solvers: * Conditional gradient [6] and Generalized conditional gradient for regularized OT [7]. * Joint OT matrix and mapping estimation [8]. * Wasserstein Discriminant Analysis [11] (requires autograd + pymanopt). -* Gromov-Wasserstein distances [12] +* Gromov-Wasserstein distances and barycenters [12] Some demonstrations (both in Python and Jupyter Notebook format) are available in the examples folder. -- cgit v1.2.3 From 53e1115349ddbdff83b74c5dd15fc4b258c46cd4 Mon Sep 17 00:00:00 2001 From: Nicolas Courty Date: Fri, 1 Sep 2017 15:37:09 +0200 Subject: docstrings + naming --- examples/plot_gromov_barycenter.py | 34 ++++++++------ ot/gromov.py | 92 +++++++++++++++++++------------------- test/test_gromov.py | 2 +- 3 files changed, 68 insertions(+), 60 deletions(-) diff --git a/examples/plot_gromov_barycenter.py b/examples/plot_gromov_barycenter.py index f0657e1..4f17117 100755 --- a/examples/plot_gromov_barycenter.py +++ b/examples/plot_gromov_barycenter.py @@ -45,19 +45,19 @@ def smacof_mds(C, dim, max_iter=3000, eps=1e-9): dimension of the targeted space max_iter : int Maximum number of iterations of the SMACOF algorithm for a single run - - eps : relative tolerance w.r.t stress to declare converge + eps : float + relative tolerance w.r.t stress to declare converge Returns ------- - npos : R**dim ndarray + npos : ndarray, shape (R, dim) Embedded coordinates of the interpolated point cloud (defined with one isometry) """ - seed = np.random.RandomState(seed=3) + rng = np.random.RandomState(seed=3) mds = manifold.MDS( dim, @@ -72,7 +72,7 @@ def smacof_mds(C, dim, max_iter=3000, eps=1e-9): max_iter=max_iter, eps=1e-9, dissimilarity="precomputed", - random_state=seed, + random_state=rng, n_init=1) npos = nmds.fit_transform(C, init=pos) @@ -132,23 +132,31 @@ lambdast = [[float(i) / 3, float(3 - i) / 3] for i in [1, 2]] Ct01 = [0 for i in range(2)] for i in range(2): - Ct01[i] = ot.gromov.gromov_barycenters(n_samples, [Cs[0], Cs[1]], [ - ps[0], ps[1]], p, lambdast[i], 'square_loss', 5e-4, numItermax=100, stopThr=1e-3) + Ct01[i] = ot.gromov.gromov_barycenters(n_samples, [Cs[0], Cs[1]], + [ps[0], ps[1] + ], p, lambdast[i], 'square_loss', 5e-4, + max_iter=100, stopThr=1e-3) Ct02 = [0 for i in range(2)] for i in range(2): - Ct02[i] = ot.gromov.gromov_barycenters(n_samples, [Cs[0], Cs[2]], [ - ps[0], ps[2]], p, lambdast[i], 'square_loss', 5e-4, numItermax=100, stopThr=1e-3) + Ct02[i] = ot.gromov.gromov_barycenters(n_samples, [Cs[0], Cs[2]], + [ps[0], ps[2] + ], p, lambdast[i], 'square_loss', 5e-4, + max_iter=100, stopThr=1e-3) Ct13 = [0 for i in range(2)] for i in range(2): - Ct13[i] = ot.gromov.gromov_barycenters(n_samples, [Cs[1], Cs[3]], [ - ps[1], ps[3]], p, lambdast[i], 'square_loss', 5e-4, numItermax=100, stopThr=1e-3) + Ct13[i] = ot.gromov.gromov_barycenters(n_samples, [Cs[1], Cs[3]], + [ps[1], ps[3] + ], p, lambdast[i], 'square_loss', 5e-4, + max_iter=100, stopThr=1e-3) Ct23 = [0 for i in range(2)] for i in range(2): - Ct23[i] = ot.gromov.gromov_barycenters(n_samples, [Cs[2], Cs[3]], [ - ps[2], ps[3]], p, lambdast[i], 'square_loss', 5e-4, numItermax=100, stopThr=1e-3) + Ct23[i] = ot.gromov.gromov_barycenters(n_samples, [Cs[2], Cs[3]], + [ps[2], ps[3] + ], p, lambdast[i], 'square_loss', 5e-4, + max_iter=100, stopThr=1e-3) """ Visualization diff --git a/ot/gromov.py b/ot/gromov.py index 9dbf463..cf9c4da 100644 --- a/ot/gromov.py +++ b/ot/gromov.py @@ -58,13 +58,13 @@ def tensor_square_loss(C1, C2, T): Metric cost matrix in the source space C2 : ndarray, shape (nt, nt) Metric costfr matrix in the target space - T : np.ndarray(ns,nt) + T : ndarray, shape (ns, nt) Coupling between source and target spaces Returns ------- - tens : (ns*nt) ndarray + tens : ndarray, shape (ns, nt) \mathcal{L}(C1,C2) \otimes T tensor-matrix multiplication result @@ -89,7 +89,7 @@ def tensor_square_loss(C1, C2, T): tens = -np.dot(h1(C1), T).dot(h2(C2).T) tens -= tens.min() - return np.array(tens) + return tens def tensor_kl_loss(C1, C2, T): @@ -116,13 +116,13 @@ def tensor_kl_loss(C1, C2, T): Metric cost matrix in the source space C2 : ndarray, shape (nt, nt) Metric costfr matrix in the target space - T : np.ndarray(ns,nt) + T : ndarray, shape (ns, nt) Coupling between source and target spaces Returns ------- - tens : (ns*nt) ndarray + tens : ndarray, shape (ns, nt) \mathcal{L}(C1,C2) \otimes T tensor-matrix multiplication result References @@ -151,34 +151,36 @@ def tensor_kl_loss(C1, C2, T): tens = -np.dot(h1(C1), T).dot(h2(C2).T) tens -= tens.min() - return np.array(tens) + return tens def update_square_loss(p, lambdas, T, Cs): """ - Updates C according to the L2 Loss kernel with the S Ts couplings calculated at each iteration + Updates C according to the L2 Loss kernel with the S Ts couplings + calculated at each iteration Parameters ---------- - p : np.ndarray(N,) + p : ndarray, shape (N,) weights in the targeted barycenter lambdas : list of the S spaces' weights T : list of S np.ndarray(ns,N) the S Ts couplings calculated at each iteration - Cs : Cs : list of S np.ndarray(ns,ns) + Cs : list of S ndarray, shape(ns,ns) Metric cost matrices Returns ---------- - C updated + C : ndarray, shape (nt,nt) + updated C matrix """ tmpsum = sum([lambdas[s] * np.dot(T[s].T, Cs[s]).dot(T[s]) for s in range(len(T))]) ppt = np.outer(p, p) - return(np.divide(tmpsum, ppt)) + return np.divide(tmpsum, ppt) def update_kl_loss(p, lambdas, T, Cs): @@ -188,27 +190,28 @@ def update_kl_loss(p, lambdas, T, Cs): Parameters ---------- - p : np.ndarray(N,) + p : ndarray, shape (N,) weights in the targeted barycenter lambdas : list of the S spaces' weights T : list of S np.ndarray(ns,N) the S Ts couplings calculated at each iteration - Cs : Cs : list of S np.ndarray(ns,ns) + Cs : list of S ndarray, shape(ns,ns) Metric cost matrices Returns ---------- - C updated + C : ndarray, shape (ns,ns) + updated C matrix """ tmpsum = sum([lambdas[s] * np.dot(T[s].T, Cs[s]).dot(T[s]) for s in range(len(T))]) ppt = np.outer(p, p) - return(np.exp(np.divide(tmpsum, ppt))) + return np.exp(np.divide(tmpsum, ppt)) -def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, stopThr=1e-9, verbose=False, log=False): +def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, tol=1e-9, verbose=False, log=False): """ Returns the gromov-wasserstein coupling between the two measured similarity matrices @@ -241,31 +244,28 @@ def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, stopThr=1 Metric cost matrix in the source space C2 : ndarray, shape (nt, nt) Metric costfr matrix in the target space - p : np.ndarray(ns,) + p : ndarray, shape (ns,) distribution in the source space - q : np.ndarray(nt) + q : ndarray, shape (nt,) distribution in the target space - loss_fun : loss function used for the solver either 'square_loss' or 'kl_loss' + loss_fun : string + loss function used for the solver either 'square_loss' or 'kl_loss' epsilon : float Regularization term >0 -<<<<<<< HEAD max_iter : int, optional -======= - numItermax : int, optional ->>>>>>> 986f46ddde3ce2f550cb56f66620df377326423d - Max number of iterations - stopThr : float, optional + Max number of iterations + tol : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True - forcing : np.ndarray(N,2) - list of forced couplings (where N is the number of forcing) + Returns ------- - T : coupling between the two spaces that minimizes : + T : ndarray, shape (ns, nt) + coupling between the two spaces that minimizes : \sum_{i,j,k,l} L(C1_{i,k},C2_{j,l})*T_{i,j}*T_{k,l}-\epsilon(H(T)) """ @@ -278,7 +278,7 @@ def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, stopThr=1 cpt = 0 err = 1 - while (err > stopThr and cpt < max_iter): + while (err > tol and cpt < max_iter): Tprev = T @@ -303,7 +303,7 @@ def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, stopThr=1 'It.', 'Err') + '\n' + '-' * 19) print('{:5d}|{:8e}|'.format(cpt, err)) - cpt = cpt + 1 + cpt += 1 if log: return T, log @@ -311,7 +311,7 @@ def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, stopThr=1 return T -def gromov_wasserstein2(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, stopThr=1e-9, verbose=False, log=False): +def gromov_wasserstein2(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, tol=1e-9, verbose=False, log=False): """ Returns the gromov-wasserstein discrepancy between the two measured similarity matrices @@ -339,37 +339,36 @@ def gromov_wasserstein2(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, stopThr= Metric cost matrix in the source space C2 : ndarray, shape (nt, nt) Metric costfr matrix in the target space - p : np.ndarray(ns,) + p : ndarray, shape (ns,) distribution in the source space - q : np.ndarray(nt) + q : ndarray, shape (nt,) distribution in the target space - loss_fun : loss function used for the solver either 'square_loss' or 'kl_loss' + loss_fun : string + loss function used for the solver either 'square_loss' or 'kl_loss' epsilon : float Regularization term >0 max_iter : int, optional Max number of iterations - stopThr : float, optional + tol : float, optional Stop threshold on error (>0) verbose : bool, optional Print information along iterations log : bool, optional record log if True - forcing : np.ndarray(N,2) - list of forced couplings (where N is the number of forcing) Returns ------- - T : coupling between the two spaces that minimizes : - \sum_{i,j,k,l} L(C1_{i,k},C2_{j,l})*T_{i,j}*T_{k,l}-\epsilon(H(T)) + gw_dist : float + Gromov-Wasserstein distance """ if log: gw, logv = gromov_wasserstein( - C1, C2, p, q, loss_fun, epsilon, max_iter, stopThr, verbose, log) + C1, C2, p, q, loss_fun, epsilon, max_iter, tol, verbose, log) else: gw = gromov_wasserstein(C1, C2, p, q, loss_fun, - epsilon, max_iter, stopThr, verbose, log) + epsilon, max_iter, tol, verbose, log) if loss_fun == 'square_loss': gw_dist = np.sum(gw * tensor_square_loss(C1, C2, gw)) @@ -383,7 +382,7 @@ def gromov_wasserstein2(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, stopThr= return gw_dist -def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, max_iter=1000, stopThr=1e-9, verbose=False, log=False): +def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, max_iter=1000, tol=1e-9, verbose=False, log=False): """ Returns the gromov-wasserstein barycenters of S measured similarity matrices @@ -408,7 +407,7 @@ def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, max_iter=1000, Metric cost matrices ps : list of S np.ndarray(ns,) sample weights in the S spaces - p : np.ndarray(N,) + p : ndarray, shape(N,) weights in the targeted barycenter lambdas : list of the S spaces' weights L : tensor-matrix multiplication function based on specific loss function @@ -418,7 +417,7 @@ def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, max_iter=1000, Regularization term >0 max_iter : int, optional Max number of iterations - stopThr : float, optional + tol : float, optional Stop threshol on error (>0) verbose : bool, optional Print information along iterations @@ -427,7 +426,8 @@ def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, max_iter=1000, Returns ------- - C : Similarity matrix in the barycenter space (permutated arbitrarily) + C : ndarray, shape (N, N) + Similarity matrix in the barycenter space (permutated arbitrarily) """ @@ -446,7 +446,7 @@ def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, max_iter=1000, error = [] - while(err > stopThr and cpt < max_iter): + while(err > tol and cpt < max_iter): Cprev = C T = [gromov_wasserstein(Cs[s], C, ps[s], p, loss_fun, epsilon, diff --git a/test/test_gromov.py b/test/test_gromov.py index c26d898..28495e1 100644 --- a/test/test_gromov.py +++ b/test/test_gromov.py @@ -17,7 +17,7 @@ def test_gromov(): xs = ot.datasets.get_2D_samples_gauss(n_samples, mu_s, cov_s) - xt = [xs[n_samples - (i + 1)] for i in range(n_samples)] + xt = xs[::-1] xt = np.array(xt) p = ot.unif(n_samples) -- cgit v1.2.3 From 8ea74ad41d660629a12f7d8d0d8816a23d385a92 Mon Sep 17 00:00:00 2001 From: Nicolas Courty Date: Fri, 1 Sep 2017 15:38:11 +0200 Subject: docstrings + naming --- ot/gromov.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ot/gromov.py b/ot/gromov.py index cf9c4da..1726f5e 100644 --- a/ot/gromov.py +++ b/ot/gromov.py @@ -471,6 +471,6 @@ def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, max_iter=1000, 'It.', 'Err') + '\n' + '-' * 19) print('{:5d}|{:8e}|'.format(cpt, err)) - cpt = cpt + 1 + cpt += 1 return C -- cgit v1.2.3 From 30bfc5ce5acd98991b3d01e313d0c14f0e600b14 Mon Sep 17 00:00:00 2001 From: Slasnista Date: Mon, 4 Sep 2017 08:46:36 +0200 Subject: correction semi supervised case --- ot/da.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ot/da.py b/ot/da.py index 564c7b7..e694668 100644 --- a/ot/da.py +++ b/ot/da.py @@ -989,7 +989,7 @@ class BaseTransport(BaseEstimator): # assumes labeled source samples occupy the first rows # and labeled target samples occupy the first columns - classes = np.unique(ys) + 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) -- cgit v1.2.3 From 363c5f92a4865527320edcff97036e62a7ca28c9 Mon Sep 17 00:00:00 2001 From: Slasnista Date: Mon, 4 Sep 2017 09:12:32 +0200 Subject: doc string + example --- examples/da/plot_otda_semi_supervised.py | 142 +++++++++++++++++++++++++++++++ ot/da.py | 72 ++++++++++++---- 2 files changed, 196 insertions(+), 18 deletions(-) create mode 100644 examples/da/plot_otda_semi_supervised.py diff --git a/examples/da/plot_otda_semi_supervised.py b/examples/da/plot_otda_semi_supervised.py new file mode 100644 index 0000000..6e6296b --- /dev/null +++ b/examples/da/plot_otda_semi_supervised.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +""" +============================================ +OTDA unsupervised vs semi-supervised setting +============================================ + +This example introduces a semi supervised domain adaptation in a 2D setting. +It explicits the problem of semi supervised domain adaptation and introduces +some optimal transport approaches to solve it. + +Quantities such as optimal couplings, greater coupling coefficients and +transported samples are represented in order to give a visual understanding +of what the transport methods are doing. +""" + +# Authors: Remi Flamary +# Stanislas Chambon +# +# License: MIT License + +import matplotlib.pylab as pl +import ot + + +############################################################################## +# generate data +############################################################################## + +n_samples_source = 150 +n_samples_target = 150 + +Xs, ys = ot.datasets.get_data_classif('3gauss', n_samples_source) +Xt, yt = ot.datasets.get_data_classif('3gauss2', n_samples_target) + +# Cost matrix +M = ot.dist(Xs, Xt, metric='sqeuclidean') + + +############################################################################## +# Transport source samples onto target samples +############################################################################## + +# unsupervised domain adaptation +ot_sinkhorn_un = ot.da.SinkhornTransport(reg_e=1e-1) +ot_sinkhorn_un.fit(Xs=Xs, Xt=Xt) +transp_Xs_sinkhorn_un = ot_sinkhorn_un.transform(Xs=Xs) + +# semi-supervised domain adaptation +ot_sinkhorn_semi = ot.da.SinkhornTransport(reg_e=1e-1) +ot_sinkhorn_semi.fit(Xs=Xs, Xt=Xt, ys=ys, yt=yt) +transp_Xs_sinkhorn_semi = ot_sinkhorn_semi.transform(Xs=Xs) + +# semi supervised DA uses available labaled target samples to modify the cost +# matrix involved in the OT problem. The cost of transporting a source sample +# of class A onto a target sample of class B != A is set to infinite, or a +# very large value + + +############################################################################## +# Fig 1 : plots source and target samples + matrix of pairwise distance +############################################################################## + +pl.figure(1, figsize=(10, 10)) +pl.subplot(2, 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(2, 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.subplot(2, 2, 3) +pl.imshow(ot_sinkhorn_un.cost_, interpolation='nearest') +pl.xticks([]) +pl.yticks([]) +pl.title('Cost matrix - unsupervised DA') + +pl.subplot(2, 2, 4) +pl.imshow(ot_sinkhorn_semi.cost_, interpolation='nearest') +pl.xticks([]) +pl.yticks([]) +pl.title('Cost matrix - semisupervised DA') + +pl.tight_layout() + +# the optimal coupling in the semi-supervised DA case will exhibit " shape +# similar" to the cost matrix, (block diagonal matrix) + +############################################################################## +# Fig 2 : plots optimal couplings for the different methods +############################################################################## + +pl.figure(2, figsize=(8, 4)) + +pl.subplot(1, 2, 1) +pl.imshow(ot_sinkhorn_un.coupling_, interpolation='nearest') +pl.xticks([]) +pl.yticks([]) +pl.title('Optimal coupling\nUnsupervised DA') + +pl.subplot(1, 2, 2) +pl.imshow(ot_sinkhorn_semi.coupling_, interpolation='nearest') +pl.xticks([]) +pl.yticks([]) +pl.title('Optimal coupling\nSemi-supervised DA') + +pl.tight_layout() + + +############################################################################## +# Fig 3 : plot transported samples +############################################################################## + +# display transported samples +pl.figure(4, figsize=(8, 4)) +pl.subplot(1, 2, 1) +pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', + label='Target samples', alpha=0.5) +pl.scatter(transp_Xs_sinkhorn_un[:, 0], transp_Xs_sinkhorn_un[:, 1], c=ys, + marker='+', label='Transp samples', s=30) +pl.title('Transported samples\nEmdTransport') +pl.legend(loc=0) +pl.xticks([]) +pl.yticks([]) + +pl.subplot(1, 2, 2) +pl.scatter(Xt[:, 0], Xt[:, 1], c=yt, marker='o', + label='Target samples', alpha=0.5) +pl.scatter(transp_Xs_sinkhorn_semi[:, 0], transp_Xs_sinkhorn_semi[:, 1], c=ys, + marker='+', label='Transp samples', s=30) +pl.title('Transported samples\nSinkhornTransport') +pl.xticks([]) +pl.yticks([]) + +pl.tight_layout() +pl.show() diff --git a/ot/da.py b/ot/da.py index e694668..1d3d0ba 100644 --- a/ot/da.py +++ b/ot/da.py @@ -966,8 +966,12 @@ class BaseTransport(BaseEstimator): The class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. - yt : array-like, shape (n_labeled_target_samples,) - The class labels + 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 ------- @@ -1023,8 +1027,12 @@ class BaseTransport(BaseEstimator): The class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. - yt : array-like, shape (n_labeled_target_samples,) - The class labels + 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 ------- @@ -1045,8 +1053,12 @@ class BaseTransport(BaseEstimator): The class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. - yt : array-like, shape (n_labeled_target_samples,) - The class labels + 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 @@ -1110,8 +1122,12 @@ class BaseTransport(BaseEstimator): The class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. - yt : array-like, shape (n_labeled_target_samples,) - The class labels + 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 @@ -1241,8 +1257,12 @@ class SinkhornTransport(BaseTransport): The class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. - yt : array-like, shape (n_labeled_target_samples,) - The class labels + 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 ------- @@ -1333,8 +1353,12 @@ class EMDTransport(BaseTransport): The class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. - yt : array-like, shape (n_labeled_target_samples,) - The class labels + 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 ------- @@ -1434,8 +1458,12 @@ class SinkhornLpl1Transport(BaseTransport): The class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. - yt : array-like, shape (n_labeled_target_samples,) - The class labels + 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 ------- @@ -1545,8 +1573,12 @@ class SinkhornL1l2Transport(BaseTransport): The class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. - yt : array-like, shape (n_labeled_target_samples,) - The class labels + 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 ------- @@ -1662,8 +1694,12 @@ class MappingTransport(BaseEstimator): The class labels Xt : array-like, shape (n_target_samples, n_features) The training input samples. - yt : array-like, shape (n_labeled_target_samples,) - The class labels + 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 ------- -- cgit v1.2.3 From 669a6bee4200f6a2f1f6bbf597712684ff7272a8 Mon Sep 17 00:00:00 2001 From: Slasnista Date: Mon, 4 Sep 2017 09:17:59 +0200 Subject: commenting the example --- examples/da/plot_otda_semi_supervised.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/examples/da/plot_otda_semi_supervised.py b/examples/da/plot_otda_semi_supervised.py index 6e6296b..8095c4d 100644 --- a/examples/da/plot_otda_semi_supervised.py +++ b/examples/da/plot_otda_semi_supervised.py @@ -32,9 +32,6 @@ n_samples_target = 150 Xs, ys = ot.datasets.get_data_classif('3gauss', n_samples_source) Xt, yt = ot.datasets.get_data_classif('3gauss2', n_samples_target) -# Cost matrix -M = ot.dist(Xs, Xt, metric='sqeuclidean') - ############################################################################## # Transport source samples onto target samples @@ -55,6 +52,13 @@ transp_Xs_sinkhorn_semi = ot_sinkhorn_semi.transform(Xs=Xs) # of class A onto a target sample of class B != A is set to infinite, or a # very large value +# note that in the present case we consider that all the target samples are +# labeled. For daily applications, some target sample might not have labels, +# in this case the element of yt corresponding to these samples should be +# filled with -1. + +# Warning: we recall that -1 cannot be used as a class label + ############################################################################## # Fig 1 : plots source and target samples + matrix of pairwise distance @@ -92,6 +96,7 @@ pl.tight_layout() # the optimal coupling in the semi-supervised DA case will exhibit " shape # similar" to the cost matrix, (block diagonal matrix) + ############################################################################## # Fig 2 : plots optimal couplings for the different methods ############################################################################## -- cgit v1.2.3 From 185eb3e2ef34b5ce6b8f90a28a5bcc78432b7fd3 Mon Sep 17 00:00:00 2001 From: Antoine Rolet Date: Tue, 5 Sep 2017 15:10:44 +0900 Subject: Removed prints --- ot/lp/emd_wrap.pyx | 2 -- 1 file changed, 2 deletions(-) diff --git a/ot/lp/emd_wrap.pyx b/ot/lp/emd_wrap.pyx index 435a270..4febb32 100644 --- a/ot/lp/emd_wrap.pyx +++ b/ot/lp/emd_wrap.pyx @@ -68,8 +68,6 @@ 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 - print alpha.size - print beta.size # calling the function EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, alpha.data, beta.data, &cost, maxiter) -- cgit v1.2.3 From 0bb8ec8bf8061aa7ad2299b04b8368b46b56be41 Mon Sep 17 00:00:00 2001 From: Antoine Rolet Date: Tue, 5 Sep 2017 15:36:03 +0900 Subject: Removed declaration of unused variable --- ot/lp/EMD_wrapper.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ot/lp/EMD_wrapper.cpp b/ot/lp/EMD_wrapper.cpp index 8ac43c7..8e74462 100644 --- a/ot/lp/EMD_wrapper.cpp +++ b/ot/lp/EMD_wrapper.cpp @@ -18,8 +18,7 @@ int EMD_wrap(int n1, int n2, double *X, double *Y, double *D, double *G, double* alpha, double* beta, double *cost, int max_iter) { // beware M and C anre strored in row major C style!!! - int n, m, i, cur; - double max; + int n, m, i, cur; typedef FullBipartiteDigraph Digraph; DIGRAPH_TYPEDEFS(FullBipartiteDigraph); -- cgit v1.2.3 From b12edc59c0a94e1f426ae314baa006e06c062923 Mon Sep 17 00:00:00 2001 From: Slasnista Date: Tue, 5 Sep 2017 09:42:32 +0200 Subject: integrated test for semi supervised case --- test/test_da.py | 96 +++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 60 insertions(+), 36 deletions(-) diff --git a/test/test_da.py b/test/test_da.py index 104a798..a757d0a 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -63,19 +63,25 @@ def test_sinkhorn_lpl1_transport_class(): transp_Xs = clf.fit_transform(Xs=Xs, ys=ys, Xt=Xt) assert_equal(transp_Xs.shape, Xs.shape) - # test semi supervised mode - clf = ot.da.SinkhornLpl1Transport() - clf.fit(Xs=Xs, ys=ys, Xt=Xt) - n_unsup = np.sum(clf.cost_) + # test unsupervised vs semi-supervised mode + clf_unsup = ot.da.SinkhornTransport() + clf_unsup.fit(Xs=Xs, Xt=Xt) + n_unsup = np.sum(clf_unsup.cost_) - # test semi supervised mode - clf = ot.da.SinkhornLpl1Transport() - clf.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) - assert_equal(clf.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) - n_semisup = np.sum(clf.cost_) + clf_semi = ot.da.SinkhornTransport() + clf_semi.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) + assert_equal(clf_semi.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) + n_semisup = np.sum(clf_semi.cost_) + # check that the cost matrix norms are indeed different assert n_unsup != n_semisup, "semisupervised mode not working" + # check that the coupling forbids mass transport between labeled source + # and labeled target samples + mass_semi = np.sum( + clf_semi.coupling_[clf_semi.cost_ == clf_semi.limit_max]) + assert mass_semi == 0, "semisupervised mode not working" + def test_sinkhorn_l1l2_transport_class(): """test_sinkhorn_transport @@ -129,19 +135,25 @@ def test_sinkhorn_l1l2_transport_class(): transp_Xs = clf.fit_transform(Xs=Xs, ys=ys, Xt=Xt) assert_equal(transp_Xs.shape, Xs.shape) - # test semi supervised mode - clf = ot.da.SinkhornL1l2Transport() - clf.fit(Xs=Xs, ys=ys, Xt=Xt) - n_unsup = np.sum(clf.cost_) + # test unsupervised vs semi-supervised mode + clf_unsup = ot.da.SinkhornTransport() + clf_unsup.fit(Xs=Xs, Xt=Xt) + n_unsup = np.sum(clf_unsup.cost_) - # test semi supervised mode - clf = ot.da.SinkhornL1l2Transport() - clf.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) - assert_equal(clf.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) - n_semisup = np.sum(clf.cost_) + clf_semi = ot.da.SinkhornTransport() + clf_semi.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) + assert_equal(clf_semi.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) + n_semisup = np.sum(clf_semi.cost_) + # check that the cost matrix norms are indeed different assert n_unsup != n_semisup, "semisupervised mode not working" + # check that the coupling forbids mass transport between labeled source + # and labeled target samples + mass_semi = np.sum( + clf_semi.coupling_[clf_semi.cost_ == clf_semi.limit_max]) + assert mass_semi == 0, "semisupervised mode not working" + # check everything runs well with log=True clf = ot.da.SinkhornL1l2Transport(log=True) clf.fit(Xs=Xs, ys=ys, Xt=Xt) @@ -200,19 +212,25 @@ def test_sinkhorn_transport_class(): transp_Xs = clf.fit_transform(Xs=Xs, Xt=Xt) assert_equal(transp_Xs.shape, Xs.shape) - # test semi supervised mode - clf = ot.da.SinkhornTransport() - clf.fit(Xs=Xs, Xt=Xt) - n_unsup = np.sum(clf.cost_) + # test unsupervised vs semi-supervised mode + clf_unsup = ot.da.SinkhornTransport() + clf_unsup.fit(Xs=Xs, Xt=Xt) + n_unsup = np.sum(clf_unsup.cost_) - # test semi supervised mode - clf = ot.da.SinkhornTransport() - clf.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) - assert_equal(clf.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) - n_semisup = np.sum(clf.cost_) + clf_semi = ot.da.SinkhornTransport() + clf_semi.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) + assert_equal(clf_semi.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) + n_semisup = np.sum(clf_semi.cost_) + # check that the cost matrix norms are indeed different assert n_unsup != n_semisup, "semisupervised mode not working" + # check that the coupling forbids mass transport between labeled source + # and labeled target samples + mass_semi = np.sum( + clf_semi.coupling_[clf_semi.cost_ == clf_semi.limit_max]) + assert mass_semi == 0, "semisupervised mode not working" + # check everything runs well with log=True clf = ot.da.SinkhornTransport(log=True) clf.fit(Xs=Xs, ys=ys, Xt=Xt) @@ -270,19 +288,25 @@ def test_emd_transport_class(): transp_Xs = clf.fit_transform(Xs=Xs, Xt=Xt) assert_equal(transp_Xs.shape, Xs.shape) - # test semi supervised mode - clf = ot.da.EMDTransport() - clf.fit(Xs=Xs, Xt=Xt) - n_unsup = np.sum(clf.cost_) + # test unsupervised vs semi-supervised mode + clf_unsup = ot.da.SinkhornTransport() + clf_unsup.fit(Xs=Xs, Xt=Xt) + n_unsup = np.sum(clf_unsup.cost_) - # test semi supervised mode - clf = ot.da.EMDTransport() - clf.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) - assert_equal(clf.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) - n_semisup = np.sum(clf.cost_) + clf_semi = ot.da.SinkhornTransport() + clf_semi.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) + assert_equal(clf_semi.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) + n_semisup = np.sum(clf_semi.cost_) + # check that the cost matrix norms are indeed different assert n_unsup != n_semisup, "semisupervised mode not working" + # check that the coupling forbids mass transport between labeled source + # and labeled target samples + mass_semi = np.sum( + clf_semi.coupling_[clf_semi.cost_ == clf_semi.limit_max]) + assert mass_semi == 0, "semisupervised mode not working" + def test_mapping_transport_class(): """test_mapping_transport -- cgit v1.2.3 From 8e4a7930cf1ff80edeb30021acaf7337a02d18a5 Mon Sep 17 00:00:00 2001 From: Slasnista Date: Tue, 5 Sep 2017 09:47:24 +0200 Subject: change name of otda object in test script: clf => otda --- test/test_da.py | 260 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 130 insertions(+), 130 deletions(-) diff --git a/test/test_da.py b/test/test_da.py index a757d0a..9fc42a3 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -22,56 +22,56 @@ def test_sinkhorn_lpl1_transport_class(): Xs, ys = get_data_classif('3gauss', ns) Xt, yt = get_data_classif('3gauss2', nt) - clf = ot.da.SinkhornLpl1Transport() + otda = ot.da.SinkhornLpl1Transport() # test its computed - clf.fit(Xs=Xs, ys=ys, Xt=Xt) - assert hasattr(clf, "cost_") - assert hasattr(clf, "coupling_") + otda.fit(Xs=Xs, ys=ys, Xt=Xt) + assert hasattr(otda, "cost_") + assert hasattr(otda, "coupling_") # test dimensions of coupling - assert_equal(clf.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) - assert_equal(clf.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(otda.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(otda.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) # test margin constraints mu_s = unif(ns) mu_t = unif(nt) - assert_allclose(np.sum(clf.coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) - assert_allclose(np.sum(clf.coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + 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 = clf.transform(Xs=Xs) + transp_Xs = otda.transform(Xs=Xs) assert_equal(transp_Xs.shape, Xs.shape) Xs_new, _ = get_data_classif('3gauss', ns + 1) - transp_Xs_new = clf.transform(Xs_new) + 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 = clf.inverse_transform(Xt=Xt) + transp_Xt = otda.inverse_transform(Xt=Xt) assert_equal(transp_Xt.shape, Xt.shape) Xt_new, _ = get_data_classif('3gauss2', nt + 1) - transp_Xt_new = clf.inverse_transform(Xt=Xt_new) + 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 = clf.fit_transform(Xs=Xs, ys=ys, Xt=Xt) + transp_Xs = otda.fit_transform(Xs=Xs, ys=ys, Xt=Xt) assert_equal(transp_Xs.shape, Xs.shape) # test unsupervised vs semi-supervised mode - clf_unsup = ot.da.SinkhornTransport() - clf_unsup.fit(Xs=Xs, Xt=Xt) - n_unsup = np.sum(clf_unsup.cost_) + otda_unsup = ot.da.SinkhornTransport() + otda_unsup.fit(Xs=Xs, Xt=Xt) + n_unsup = np.sum(otda_unsup.cost_) - clf_semi = ot.da.SinkhornTransport() - clf_semi.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) - assert_equal(clf_semi.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) - n_semisup = np.sum(clf_semi.cost_) + otda_semi = ot.da.SinkhornTransport() + otda_semi.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) + assert_equal(otda_semi.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) + n_semisup = np.sum(otda_semi.cost_) # check that the cost matrix norms are indeed different assert n_unsup != n_semisup, "semisupervised mode not working" @@ -79,7 +79,7 @@ def test_sinkhorn_lpl1_transport_class(): # check that the coupling forbids mass transport between labeled source # and labeled target samples mass_semi = np.sum( - clf_semi.coupling_[clf_semi.cost_ == clf_semi.limit_max]) + otda_semi.coupling_[otda_semi.cost_ == otda_semi.limit_max]) assert mass_semi == 0, "semisupervised mode not working" @@ -93,57 +93,57 @@ def test_sinkhorn_l1l2_transport_class(): Xs, ys = get_data_classif('3gauss', ns) Xt, yt = get_data_classif('3gauss2', nt) - clf = ot.da.SinkhornL1l2Transport() + otda = ot.da.SinkhornL1l2Transport() # test its computed - clf.fit(Xs=Xs, ys=ys, Xt=Xt) - assert hasattr(clf, "cost_") - assert hasattr(clf, "coupling_") - assert hasattr(clf, "log_") + otda.fit(Xs=Xs, ys=ys, Xt=Xt) + assert hasattr(otda, "cost_") + assert hasattr(otda, "coupling_") + assert hasattr(otda, "log_") # test dimensions of coupling - assert_equal(clf.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) - assert_equal(clf.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(otda.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(otda.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) # test margin constraints mu_s = unif(ns) mu_t = unif(nt) - assert_allclose(np.sum(clf.coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) - assert_allclose(np.sum(clf.coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + 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 = clf.transform(Xs=Xs) + transp_Xs = otda.transform(Xs=Xs) assert_equal(transp_Xs.shape, Xs.shape) Xs_new, _ = get_data_classif('3gauss', ns + 1) - transp_Xs_new = clf.transform(Xs_new) + 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 = clf.inverse_transform(Xt=Xt) + transp_Xt = otda.inverse_transform(Xt=Xt) assert_equal(transp_Xt.shape, Xt.shape) Xt_new, _ = get_data_classif('3gauss2', nt + 1) - transp_Xt_new = clf.inverse_transform(Xt=Xt_new) + 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 = clf.fit_transform(Xs=Xs, ys=ys, Xt=Xt) + transp_Xs = otda.fit_transform(Xs=Xs, ys=ys, Xt=Xt) assert_equal(transp_Xs.shape, Xs.shape) # test unsupervised vs semi-supervised mode - clf_unsup = ot.da.SinkhornTransport() - clf_unsup.fit(Xs=Xs, Xt=Xt) - n_unsup = np.sum(clf_unsup.cost_) + otda_unsup = ot.da.SinkhornTransport() + otda_unsup.fit(Xs=Xs, Xt=Xt) + n_unsup = np.sum(otda_unsup.cost_) - clf_semi = ot.da.SinkhornTransport() - clf_semi.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) - assert_equal(clf_semi.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) - n_semisup = np.sum(clf_semi.cost_) + otda_semi = ot.da.SinkhornTransport() + otda_semi.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) + assert_equal(otda_semi.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) + n_semisup = np.sum(otda_semi.cost_) # check that the cost matrix norms are indeed different assert n_unsup != n_semisup, "semisupervised mode not working" @@ -151,13 +151,13 @@ def test_sinkhorn_l1l2_transport_class(): # check that the coupling forbids mass transport between labeled source # and labeled target samples mass_semi = np.sum( - clf_semi.coupling_[clf_semi.cost_ == clf_semi.limit_max]) + otda_semi.coupling_[otda_semi.cost_ == otda_semi.limit_max]) assert mass_semi == 0, "semisupervised mode not working" # check everything runs well with log=True - clf = ot.da.SinkhornL1l2Transport(log=True) - clf.fit(Xs=Xs, ys=ys, Xt=Xt) - assert len(clf.log_.keys()) != 0 + otda = ot.da.SinkhornL1l2Transport(log=True) + otda.fit(Xs=Xs, ys=ys, Xt=Xt) + assert len(otda.log_.keys()) != 0 def test_sinkhorn_transport_class(): @@ -170,57 +170,57 @@ def test_sinkhorn_transport_class(): Xs, ys = get_data_classif('3gauss', ns) Xt, yt = get_data_classif('3gauss2', nt) - clf = ot.da.SinkhornTransport() + otda = ot.da.SinkhornTransport() # test its computed - clf.fit(Xs=Xs, Xt=Xt) - assert hasattr(clf, "cost_") - assert hasattr(clf, "coupling_") - assert hasattr(clf, "log_") + otda.fit(Xs=Xs, Xt=Xt) + assert hasattr(otda, "cost_") + assert hasattr(otda, "coupling_") + assert hasattr(otda, "log_") # test dimensions of coupling - assert_equal(clf.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) - assert_equal(clf.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(otda.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(otda.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) # test margin constraints mu_s = unif(ns) mu_t = unif(nt) - assert_allclose(np.sum(clf.coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) - assert_allclose(np.sum(clf.coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + 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 = clf.transform(Xs=Xs) + transp_Xs = otda.transform(Xs=Xs) assert_equal(transp_Xs.shape, Xs.shape) Xs_new, _ = get_data_classif('3gauss', ns + 1) - transp_Xs_new = clf.transform(Xs_new) + 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 = clf.inverse_transform(Xt=Xt) + transp_Xt = otda.inverse_transform(Xt=Xt) assert_equal(transp_Xt.shape, Xt.shape) Xt_new, _ = get_data_classif('3gauss2', nt + 1) - transp_Xt_new = clf.inverse_transform(Xt=Xt_new) + 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 = clf.fit_transform(Xs=Xs, Xt=Xt) + transp_Xs = otda.fit_transform(Xs=Xs, Xt=Xt) assert_equal(transp_Xs.shape, Xs.shape) # test unsupervised vs semi-supervised mode - clf_unsup = ot.da.SinkhornTransport() - clf_unsup.fit(Xs=Xs, Xt=Xt) - n_unsup = np.sum(clf_unsup.cost_) + otda_unsup = ot.da.SinkhornTransport() + otda_unsup.fit(Xs=Xs, Xt=Xt) + n_unsup = np.sum(otda_unsup.cost_) - clf_semi = ot.da.SinkhornTransport() - clf_semi.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) - assert_equal(clf_semi.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) - n_semisup = np.sum(clf_semi.cost_) + otda_semi = ot.da.SinkhornTransport() + otda_semi.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) + assert_equal(otda_semi.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) + n_semisup = np.sum(otda_semi.cost_) # check that the cost matrix norms are indeed different assert n_unsup != n_semisup, "semisupervised mode not working" @@ -228,13 +228,13 @@ def test_sinkhorn_transport_class(): # check that the coupling forbids mass transport between labeled source # and labeled target samples mass_semi = np.sum( - clf_semi.coupling_[clf_semi.cost_ == clf_semi.limit_max]) + otda_semi.coupling_[otda_semi.cost_ == otda_semi.limit_max]) assert mass_semi == 0, "semisupervised mode not working" # check everything runs well with log=True - clf = ot.da.SinkhornTransport(log=True) - clf.fit(Xs=Xs, ys=ys, Xt=Xt) - assert len(clf.log_.keys()) != 0 + otda = ot.da.SinkhornTransport(log=True) + otda.fit(Xs=Xs, ys=ys, Xt=Xt) + assert len(otda.log_.keys()) != 0 def test_emd_transport_class(): @@ -247,56 +247,56 @@ def test_emd_transport_class(): Xs, ys = get_data_classif('3gauss', ns) Xt, yt = get_data_classif('3gauss2', nt) - clf = ot.da.EMDTransport() + otda = ot.da.EMDTransport() # test its computed - clf.fit(Xs=Xs, Xt=Xt) - assert hasattr(clf, "cost_") - assert hasattr(clf, "coupling_") + otda.fit(Xs=Xs, Xt=Xt) + assert hasattr(otda, "cost_") + assert hasattr(otda, "coupling_") # test dimensions of coupling - assert_equal(clf.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) - assert_equal(clf.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(otda.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(otda.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) # test margin constraints mu_s = unif(ns) mu_t = unif(nt) - assert_allclose(np.sum(clf.coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) - assert_allclose(np.sum(clf.coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + 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 = clf.transform(Xs=Xs) + transp_Xs = otda.transform(Xs=Xs) assert_equal(transp_Xs.shape, Xs.shape) Xs_new, _ = get_data_classif('3gauss', ns + 1) - transp_Xs_new = clf.transform(Xs_new) + 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 = clf.inverse_transform(Xt=Xt) + transp_Xt = otda.inverse_transform(Xt=Xt) assert_equal(transp_Xt.shape, Xt.shape) Xt_new, _ = get_data_classif('3gauss2', nt + 1) - transp_Xt_new = clf.inverse_transform(Xt=Xt_new) + 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 = clf.fit_transform(Xs=Xs, Xt=Xt) + transp_Xs = otda.fit_transform(Xs=Xs, Xt=Xt) assert_equal(transp_Xs.shape, Xs.shape) # test unsupervised vs semi-supervised mode - clf_unsup = ot.da.SinkhornTransport() - clf_unsup.fit(Xs=Xs, Xt=Xt) - n_unsup = np.sum(clf_unsup.cost_) + otda_unsup = ot.da.SinkhornTransport() + otda_unsup.fit(Xs=Xs, Xt=Xt) + n_unsup = np.sum(otda_unsup.cost_) - clf_semi = ot.da.SinkhornTransport() - clf_semi.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) - assert_equal(clf_semi.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) - n_semisup = np.sum(clf_semi.cost_) + otda_semi = ot.da.SinkhornTransport() + otda_semi.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) + assert_equal(otda_semi.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) + n_semisup = np.sum(otda_semi.cost_) # check that the cost matrix norms are indeed different assert n_unsup != n_semisup, "semisupervised mode not working" @@ -304,7 +304,7 @@ def test_emd_transport_class(): # check that the coupling forbids mass transport between labeled source # and labeled target samples mass_semi = np.sum( - clf_semi.coupling_[clf_semi.cost_ == clf_semi.limit_max]) + otda_semi.coupling_[otda_semi.cost_ == otda_semi.limit_max]) assert mass_semi == 0, "semisupervised mode not working" @@ -324,47 +324,47 @@ def test_mapping_transport_class(): ########################################################################## # check computation and dimensions if bias == False - clf = ot.da.MappingTransport(kernel="linear", bias=False) - clf.fit(Xs=Xs, Xt=Xt) - assert hasattr(clf, "coupling_") - assert hasattr(clf, "mapping_") - assert hasattr(clf, "log_") + otda = ot.da.MappingTransport(kernel="linear", bias=False) + otda.fit(Xs=Xs, Xt=Xt) + assert hasattr(otda, "coupling_") + assert hasattr(otda, "mapping_") + assert hasattr(otda, "log_") - assert_equal(clf.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) - assert_equal(clf.mapping_.shape, ((Xs.shape[1], Xt.shape[1]))) + assert_equal(otda.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(otda.mapping_.shape, ((Xs.shape[1], Xt.shape[1]))) # test margin constraints mu_s = unif(ns) mu_t = unif(nt) - assert_allclose(np.sum(clf.coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) - assert_allclose(np.sum(clf.coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + 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 = clf.transform(Xs=Xs) + transp_Xs = otda.transform(Xs=Xs) assert_equal(transp_Xs.shape, Xs.shape) - transp_Xs_new = clf.transform(Xs_new) + transp_Xs_new = otda.transform(Xs_new) # check that the oos method is working assert_equal(transp_Xs_new.shape, Xs_new.shape) # check computation and dimensions if bias == True - clf = ot.da.MappingTransport(kernel="linear", bias=True) - clf.fit(Xs=Xs, Xt=Xt) - assert_equal(clf.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) - assert_equal(clf.mapping_.shape, ((Xs.shape[1] + 1, Xt.shape[1]))) + otda = ot.da.MappingTransport(kernel="linear", bias=True) + otda.fit(Xs=Xs, Xt=Xt) + assert_equal(otda.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(otda.mapping_.shape, ((Xs.shape[1] + 1, Xt.shape[1]))) # test margin constraints mu_s = unif(ns) mu_t = unif(nt) - assert_allclose(np.sum(clf.coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) - assert_allclose(np.sum(clf.coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + 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 = clf.transform(Xs=Xs) + transp_Xs = otda.transform(Xs=Xs) assert_equal(transp_Xs.shape, Xs.shape) - transp_Xs_new = clf.transform(Xs_new) + transp_Xs_new = otda.transform(Xs_new) # check that the oos method is working assert_equal(transp_Xs_new.shape, Xs_new.shape) @@ -374,52 +374,52 @@ def test_mapping_transport_class(): ########################################################################## # check computation and dimensions if bias == False - clf = ot.da.MappingTransport(kernel="gaussian", bias=False) - clf.fit(Xs=Xs, Xt=Xt) + otda = ot.da.MappingTransport(kernel="gaussian", bias=False) + otda.fit(Xs=Xs, Xt=Xt) - assert_equal(clf.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) - assert_equal(clf.mapping_.shape, ((Xs.shape[0], Xt.shape[1]))) + assert_equal(otda.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(otda.mapping_.shape, ((Xs.shape[0], Xt.shape[1]))) # test margin constraints mu_s = unif(ns) mu_t = unif(nt) - assert_allclose(np.sum(clf.coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) - assert_allclose(np.sum(clf.coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + 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 = clf.transform(Xs=Xs) + transp_Xs = otda.transform(Xs=Xs) assert_equal(transp_Xs.shape, Xs.shape) - transp_Xs_new = clf.transform(Xs_new) + transp_Xs_new = otda.transform(Xs_new) # check that the oos method is working assert_equal(transp_Xs_new.shape, Xs_new.shape) # check computation and dimensions if bias == True - clf = ot.da.MappingTransport(kernel="gaussian", bias=True) - clf.fit(Xs=Xs, Xt=Xt) - assert_equal(clf.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) - assert_equal(clf.mapping_.shape, ((Xs.shape[0] + 1, Xt.shape[1]))) + otda = ot.da.MappingTransport(kernel="gaussian", bias=True) + otda.fit(Xs=Xs, Xt=Xt) + assert_equal(otda.coupling_.shape, ((Xs.shape[0], Xt.shape[0]))) + assert_equal(otda.mapping_.shape, ((Xs.shape[0] + 1, Xt.shape[1]))) # test margin constraints mu_s = unif(ns) mu_t = unif(nt) - assert_allclose(np.sum(clf.coupling_, axis=0), mu_t, rtol=1e-3, atol=1e-3) - assert_allclose(np.sum(clf.coupling_, axis=1), mu_s, rtol=1e-3, atol=1e-3) + 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 = clf.transform(Xs=Xs) + transp_Xs = otda.transform(Xs=Xs) assert_equal(transp_Xs.shape, Xs.shape) - transp_Xs_new = clf.transform(Xs_new) + transp_Xs_new = otda.transform(Xs_new) # check that the oos method is working assert_equal(transp_Xs_new.shape, Xs_new.shape) # check everything runs well with log=True - clf = ot.da.MappingTransport(kernel="gaussian", log=True) - clf.fit(Xs=Xs, Xt=Xt) - assert len(clf.log_.keys()) != 0 + otda = ot.da.MappingTransport(kernel="gaussian", log=True) + otda.fit(Xs=Xs, Xt=Xt) + assert len(otda.log_.keys()) != 0 def test_otda(): -- cgit v1.2.3 From 3baa34b5504dfbccd6800b59f1f3830a7edf3f20 Mon Sep 17 00:00:00 2001 From: Antoine Rolet Date: Tue, 5 Sep 2017 16:53:55 +0900 Subject: Added include cstdio --- ot/lp/network_simplex_simple.h | 1 + 1 file changed, 1 insertion(+) diff --git a/ot/lp/network_simplex_simple.h b/ot/lp/network_simplex_simple.h index 08449f6..a7743ee 100644 --- a/ot/lp/network_simplex_simple.h +++ b/ot/lp/network_simplex_simple.h @@ -49,6 +49,7 @@ #include #include #include +#include #ifdef HASHMAP #include #else -- cgit v1.2.3 From d43ce6fdca486fdb0fe049ab3cae4daf8652f5d0 Mon Sep 17 00:00:00 2001 From: Antoine Rolet Date: Tue, 5 Sep 2017 16:58:10 +0900 Subject: Removed print --- test/test_emd.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_emd.py b/test/test_emd.py index 3bf6fa2..0025583 100644 --- a/test/test_emd.py +++ b/test/test_emd.py @@ -26,7 +26,6 @@ b=gauss(m,m=mean2,s=10) # loss matrix M=ot.dist(x.reshape((-1,1)), y.reshape((-1,1))) ** (1./2) -print M[0,:] #M/=M.max() #%% -- cgit v1.2.3 From 49c100de34583329058b39d414d2aa49b7fd15bf Mon Sep 17 00:00:00 2001 From: Slasnista Date: Tue, 5 Sep 2017 10:00:01 +0200 Subject: test semi supervised mode ok written for all class | need different tolerance for EMDTransport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_da.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/test/test_da.py b/test/test_da.py index 9fc42a3..3602db9 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -64,11 +64,11 @@ def test_sinkhorn_lpl1_transport_class(): assert_equal(transp_Xs.shape, Xs.shape) # test unsupervised vs semi-supervised mode - otda_unsup = ot.da.SinkhornTransport() - otda_unsup.fit(Xs=Xs, Xt=Xt) + otda_unsup = ot.da.SinkhornLpl1Transport() + otda_unsup.fit(Xs=Xs, ys=ys, Xt=Xt) n_unsup = np.sum(otda_unsup.cost_) - otda_semi = ot.da.SinkhornTransport() + otda_semi = ot.da.SinkhornLpl1Transport() otda_semi.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) assert_equal(otda_semi.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) n_semisup = np.sum(otda_semi.cost_) @@ -136,11 +136,11 @@ def test_sinkhorn_l1l2_transport_class(): assert_equal(transp_Xs.shape, Xs.shape) # test unsupervised vs semi-supervised mode - otda_unsup = ot.da.SinkhornTransport() - otda_unsup.fit(Xs=Xs, Xt=Xt) + otda_unsup = ot.da.SinkhornL1l2Transport() + otda_unsup.fit(Xs=Xs, ys=ys, Xt=Xt) n_unsup = np.sum(otda_unsup.cost_) - otda_semi = ot.da.SinkhornTransport() + otda_semi = ot.da.SinkhornL1l2Transport() otda_semi.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) assert_equal(otda_semi.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) n_semisup = np.sum(otda_semi.cost_) @@ -152,7 +152,9 @@ def test_sinkhorn_l1l2_transport_class(): # and labeled target samples mass_semi = np.sum( otda_semi.coupling_[otda_semi.cost_ == otda_semi.limit_max]) - assert mass_semi == 0, "semisupervised mode not working" + mass_semi = otda_semi.coupling_[otda_semi.cost_ == otda_semi.limit_max] + assert_allclose(mass_semi, np.zeros_like(mass_semi), + rtol=1e-9, atol=1e-9) # check everything runs well with log=True otda = ot.da.SinkhornL1l2Transport(log=True) @@ -289,11 +291,11 @@ def test_emd_transport_class(): assert_equal(transp_Xs.shape, Xs.shape) # test unsupervised vs semi-supervised mode - otda_unsup = ot.da.SinkhornTransport() - otda_unsup.fit(Xs=Xs, Xt=Xt) + otda_unsup = ot.da.EMDTransport() + otda_unsup.fit(Xs=Xs, ys=ys, Xt=Xt) n_unsup = np.sum(otda_unsup.cost_) - otda_semi = ot.da.SinkhornTransport() + otda_semi = ot.da.EMDTransport() otda_semi.fit(Xs=Xs, ys=ys, Xt=Xt, yt=yt) assert_equal(otda_semi.cost_.shape, ((Xs.shape[0], Xt.shape[0]))) n_semisup = np.sum(otda_semi.cost_) @@ -305,7 +307,11 @@ def test_emd_transport_class(): # and labeled target samples mass_semi = np.sum( otda_semi.coupling_[otda_semi.cost_ == otda_semi.limit_max]) - assert mass_semi == 0, "semisupervised mode not working" + mass_semi = otda_semi.coupling_[otda_semi.cost_ == otda_semi.limit_max] + + # we need to use a small tolerance here, otherwise the test breaks + assert_allclose(mass_semi, np.zeros_like(mass_semi), + rtol=1e-2, atol=1e-2) def test_mapping_transport_class(): @@ -491,3 +497,4 @@ def test_otda(): # test_sinkhorn_l1l2_transport_class() # test_sinkhorn_lpl1_transport_class() # test_mapping_transport_class() + -- cgit v1.2.3 From 2097116c7db725a88876d617e20a94f32627f7c9 Mon Sep 17 00:00:00 2001 From: Slasnista Date: Tue, 5 Sep 2017 10:09:55 +0200 Subject: solving pb --- test/test_da.py | 61 ++++++++++++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/test/test_da.py b/test/test_da.py index 3602db9..593dc53 100644 --- a/test/test_da.py +++ b/test/test_da.py @@ -36,8 +36,10 @@ def test_sinkhorn_lpl1_transport_class(): # test 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) + 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) @@ -108,8 +110,10 @@ def test_sinkhorn_l1l2_transport_class(): # test 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) + 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) @@ -187,8 +191,10 @@ def test_sinkhorn_transport_class(): # test 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) + 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) @@ -263,8 +269,10 @@ def test_emd_transport_class(): # test 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) + 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) @@ -342,8 +350,10 @@ def test_mapping_transport_class(): # test 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) + 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) @@ -363,8 +373,10 @@ def test_mapping_transport_class(): # test 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) + 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) @@ -389,8 +401,10 @@ def test_mapping_transport_class(): # test 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) + 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) @@ -410,8 +424,10 @@ def test_mapping_transport_class(): # test 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) + 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) @@ -454,7 +470,8 @@ def test_otda(): da_entrop.interp() da_entrop.predict(xs) - np.testing.assert_allclose(a, np.sum(da_entrop.G, 1), rtol=1e-3, atol=1e-3) + np.testing.assert_allclose( + a, np.sum(da_entrop.G, 1), rtol=1e-3, atol=1e-3) np.testing.assert_allclose(b, np.sum(da_entrop.G, 0), rtol=1e-3, atol=1e-3) # non-convex Group lasso regularization @@ -488,13 +505,3 @@ def test_otda(): da_emd = ot.da.OTDA_mapping_kernel() # init class da_emd.fit(xs, xt, numItermax=10) # fit distributions da_emd.predict(xs) # interpolation of source samples - - -# if __name__ == "__main__": - -# test_sinkhorn_transport_class() -# test_emd_transport_class() -# test_sinkhorn_l1l2_transport_class() -# test_sinkhorn_lpl1_transport_class() -# test_mapping_transport_class() - -- cgit v1.2.3 From d52b4ea415d9bb669be04ccd0940f9b3d258d0e1 Mon Sep 17 00:00:00 2001 From: Antoine Rolet Date: Tue, 5 Sep 2017 17:15:45 +0900 Subject: Fixed typo and merged emd tests --- ot/lp/__init__.py | 2 +- test/test_emd.py | 68 ------------------------------------------------------- test/test_ot.py | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 64 insertions(+), 72 deletions(-) delete mode 100644 test/test_emd.py diff --git a/ot/lp/__init__.py b/ot/lp/__init__.py index a14d4e4..6048f60 100644 --- a/ot/lp/__init__.py +++ b/ot/lp/__init__.py @@ -168,6 +168,6 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), numItermax=100000): # res = [emd2_c(a, b[:, i].copy(), M, numItermax) for i in range(nb)] def f(b): - return emd2_c(a,b,M, max_iter)[0] + return emd2_c(a,b,M, numItermax)[0] res= parmap(f, [b[:,i] for i in range(nb)],processes) return np.array(res) diff --git a/test/test_emd.py b/test/test_emd.py deleted file mode 100644 index 0025583..0000000 --- a/test/test_emd.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env python2 -# -*- coding: utf-8 -*- - -import numpy as np -import ot - -from ot.datasets import get_1D_gauss as gauss -reload(ot.lp) - -#%% parameters - -n=5000 # nb bins -m=6000 # nb bins - -mean1 = 1000 -mean2 = 1100 - -# bin positions -x=np.arange(n,dtype=np.float64) -y=np.arange(m,dtype=np.float64) - -# Gaussian distributions -a=gauss(n,m=mean1,s=5) # m= mean, s= std - -b=gauss(m,m=mean2,s=10) - -# loss matrix -M=ot.dist(x.reshape((-1,1)), y.reshape((-1,1))) ** (1./2) -#M/=M.max() - -#%% - -print('Computing {} EMD '.format(1)) - -# emd loss 1 proc -ot.tic() -G, alpha, beta = ot.emd(a,b,M, dual_variables=True) -ot.toc('1 proc : {} s') - -cost1 = (G * M).sum() -cost_dual = np.vdot(a, alpha) + np.vdot(b, beta) - -# emd loss 1 proc -ot.tic() -cost_emd2 = ot.emd2(a,b,M) -ot.toc('1 proc : {} s') - -ot.tic() -G2 = ot.emd(b, a, np.ascontiguousarray(M.T)) -ot.toc('1 proc : {} s') - -cost2 = (G2 * M.T).sum() - -M_reduced = M - alpha.reshape(-1,1) - beta.reshape(1, -1) - -# Check that both cost computations are equivalent -np.testing.assert_almost_equal(cost1, cost_emd2) -# Check that dual and primal cost are equal -np.testing.assert_almost_equal(cost1, cost_dual) -# Check symmetry -np.testing.assert_almost_equal(cost1, cost2) -# Check with closed-form solution for gaussians -np.testing.assert_almost_equal(cost1, np.abs(mean1-mean2)) - -[ind1, ind2] = np.nonzero(G) - -# Check that reduced cost is zero on transport arcs -np.testing.assert_array_almost_equal((M - alpha.reshape(-1, 1) - beta.reshape(1, -1))[ind1, ind2], np.zeros(ind1.size)) \ No newline at end of file diff --git a/test/test_ot.py b/test/test_ot.py index acd8718..ded6c9f 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -6,6 +6,8 @@ import numpy as np import ot + +from ot.datasets import get_1D_gauss as gauss def test_doctest(): @@ -66,9 +68,6 @@ def test_emd_empty(): def test_emd2_multi(): - - from ot.datasets import get_1D_gauss as gauss - n = 1000 # nb bins # bin positions @@ -100,3 +99,64 @@ def test_emd2_multi(): ot.toc('multi proc : {} s') np.testing.assert_allclose(emd1, emdn) + +def test_dual_variables(): + #%% parameters + + n=5000 # nb bins + m=6000 # nb bins + + mean1 = 1000 + mean2 = 1100 + + # bin positions + x=np.arange(n,dtype=np.float64) + y=np.arange(m,dtype=np.float64) + + # Gaussian distributions + a=gauss(n,m=mean1,s=5) # m= mean, s= std + + b=gauss(m,m=mean2,s=10) + + # loss matrix + M=ot.dist(x.reshape((-1,1)), y.reshape((-1,1))) ** (1./2) + #M/=M.max() + + #%% + + print('Computing {} EMD '.format(1)) + + # emd loss 1 proc + ot.tic() + G, alpha, beta = ot.emd(a,b,M, dual_variables=True) + ot.toc('1 proc : {} s') + + cost1 = (G * M).sum() + cost_dual = np.vdot(a, alpha) + np.vdot(b, beta) + + # emd loss 1 proc + ot.tic() + cost_emd2 = ot.emd2(a,b,M) + ot.toc('1 proc : {} s') + + ot.tic() + G2 = ot.emd(b, a, np.ascontiguousarray(M.T)) + ot.toc('1 proc : {} s') + + cost2 = (G2 * M.T).sum() + + M_reduced = M - alpha.reshape(-1,1) - beta.reshape(1, -1) + + # Check that both cost computations are equivalent + np.testing.assert_almost_equal(cost1, cost_emd2) + # Check that dual and primal cost are equal + np.testing.assert_almost_equal(cost1, cost_dual) + # Check symmetry + np.testing.assert_almost_equal(cost1, cost2) + # Check with closed-form solution for gaussians + np.testing.assert_almost_equal(cost1, np.abs(mean1-mean2)) + + [ind1, ind2] = np.nonzero(G) + + # Check that reduced cost is zero on transport arcs + np.testing.assert_array_almost_equal((M - alpha.reshape(-1, 1) - beta.reshape(1, -1))[ind1, ind2], np.zeros(ind1.size)) -- cgit v1.2.3 From a3497b123b4802c7960a07a899ac7ce4525c5995 Mon Sep 17 00:00:00 2001 From: Antoine Rolet Date: Tue, 5 Sep 2017 17:51:58 +0900 Subject: Reformat --- test/test_ot.py | 67 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/test/test_ot.py b/test/test_ot.py index ded6c9f..6f0f7c9 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -5,13 +5,12 @@ # License: MIT License import numpy as np + import ot - from ot.datasets import get_1D_gauss as gauss def test_doctest(): - import doctest # test lp solver @@ -100,53 +99,52 @@ def test_emd2_multi(): np.testing.assert_allclose(emd1, emdn) + def test_dual_variables(): - #%% parameters - - n=5000 # nb bins - m=6000 # nb bins - + # %% parameters + + n = 5000 # nb bins + m = 6000 # nb bins + mean1 = 1000 mean2 = 1100 - + # bin positions - x=np.arange(n,dtype=np.float64) - y=np.arange(m,dtype=np.float64) - + x = np.arange(n, dtype=np.float64) + y = np.arange(m, dtype=np.float64) + # Gaussian distributions - a=gauss(n,m=mean1,s=5) # m= mean, s= std - - b=gauss(m,m=mean2,s=10) - + a = gauss(n, m=mean1, s=5) # m= mean, s= std + + b = gauss(m, m=mean2, s=10) + # loss matrix - M=ot.dist(x.reshape((-1,1)), y.reshape((-1,1))) ** (1./2) - #M/=M.max() - - #%% - + M = ot.dist(x.reshape((-1, 1)), y.reshape((-1, 1))) ** (1. / 2) + # M/=M.max() + + # %% + print('Computing {} EMD '.format(1)) - + # emd loss 1 proc ot.tic() - G, alpha, beta = ot.emd(a,b,M, dual_variables=True) + G, alpha, beta = ot.emd(a, b, M, dual_variables=True) ot.toc('1 proc : {} s') - + cost1 = (G * M).sum() cost_dual = np.vdot(a, alpha) + np.vdot(b, beta) - + # emd loss 1 proc ot.tic() - cost_emd2 = ot.emd2(a,b,M) + cost_emd2 = ot.emd2(a, b, M) ot.toc('1 proc : {} s') - + ot.tic() G2 = ot.emd(b, a, np.ascontiguousarray(M.T)) ot.toc('1 proc : {} s') - + cost2 = (G2 * M.T).sum() - - M_reduced = M - alpha.reshape(-1,1) - beta.reshape(1, -1) - + # Check that both cost computations are equivalent np.testing.assert_almost_equal(cost1, cost_emd2) # Check that dual and primal cost are equal @@ -154,9 +152,10 @@ def test_dual_variables(): # Check symmetry np.testing.assert_almost_equal(cost1, cost2) # Check with closed-form solution for gaussians - np.testing.assert_almost_equal(cost1, np.abs(mean1-mean2)) - + np.testing.assert_almost_equal(cost1, np.abs(mean1 - mean2)) + [ind1, ind2] = np.nonzero(G) - + # Check that reduced cost is zero on transport arcs - np.testing.assert_array_almost_equal((M - alpha.reshape(-1, 1) - beta.reshape(1, -1))[ind1, ind2], np.zeros(ind1.size)) + np.testing.assert_array_almost_equal((M - alpha.reshape(-1, 1) - beta.reshape(1, -1))[ind1, ind2], + np.zeros(ind1.size)) -- cgit v1.2.3 From f8c1c8740f9974dcf4aaf191851d62149dceb91c Mon Sep 17 00:00:00 2001 From: Antoine Rolet Date: Thu, 7 Sep 2017 13:29:46 +0900 Subject: Added MAX_ITER_REACHED flag and warning --- ot/lp/EMD.h | 3 ++- ot/lp/EMD_wrapper.cpp | 21 +++++++---------- ot/lp/emd_wrap.pyx | 29 +++++++++++++---------- ot/lp/network_simplex_simple.h | 46 ++++++++++++++++++++----------------- test/test_ot.py | 52 ++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 102 insertions(+), 49 deletions(-) diff --git a/ot/lp/EMD.h b/ot/lp/EMD.h index 15e9115..bb486de 100644 --- a/ot/lp/EMD.h +++ b/ot/lp/EMD.h @@ -26,7 +26,8 @@ typedef unsigned int node_id_type; enum ProblemType { INFEASIBLE, OPTIMAL, - UNBOUNDED + UNBOUNDED, + MAX_ITER_REACHED }; int EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double* alpha, double* beta, double *cost, int max_iter); diff --git a/ot/lp/EMD_wrapper.cpp b/ot/lp/EMD_wrapper.cpp index 8e74462..92663dc 100644 --- a/ot/lp/EMD_wrapper.cpp +++ b/ot/lp/EMD_wrapper.cpp @@ -29,14 +29,18 @@ int EMD_wrap(int n1, int n2, double *X, double *Y, double *D, double *G, double val=*(X+i); if (val>0) { n++; - } + }else if(val<0){ + return INFEASIBLE; + } } m=0; for (int i=0; i0) { m++; - } + }else if(val<0){ + return INFEASIBLE; + } } // Define the graph @@ -83,16 +87,7 @@ int EMD_wrap(int n1, int n2, double *X, double *Y, double *D, double *G, // Solve the problem with the network simplex algorithm int ret=net.run(); - if (ret!=(int)net.OPTIMAL) { - if (ret==(int)net.INFEASIBLE) { - std::cout << "Infeasible problem"; - } - if (ret==(int)net.UNBOUNDED) - { - std::cout << "Unbounded problem"; - } - } else - { + if (ret==(int)net.OPTIMAL || ret==(int)net.MAX_ITER_REACHED) { *cost = 0; Arc a; di.first(a); for (; a != INVALID; di.next(a)) { @@ -105,7 +100,7 @@ int EMD_wrap(int n1, int n2, double *X, double *Y, double *D, double *G, *(beta + indJ[j-n]) = net.potential(j); } - }; + } return ret; diff --git a/ot/lp/emd_wrap.pyx b/ot/lp/emd_wrap.pyx index 7056e0e..9bea154 100644 --- a/ot/lp/emd_wrap.pyx +++ b/ot/lp/emd_wrap.pyx @@ -7,6 +7,7 @@ Cython linker with C solver # # License: MIT License +import warnings import numpy as np cimport numpy as np @@ -15,14 +16,14 @@ cimport cython 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 max_iter) - cdef enum ProblemType: INFEASIBLE, OPTIMAL, UNBOUNDED + int EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double* alpha, double* beta, double *cost, int numItermax) + cdef enum ProblemType: INFEASIBLE, OPTIMAL, UNBOUNDED, MAX_ITER_REACHED @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): +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 numItermax): """ Solves the Earth Movers distance problem and returns the optimal transport matrix @@ -49,7 +50,7 @@ def emd_c( np.ndarray[double, ndim=1, mode="c"] a,np.ndarray[double, ndim=1, mod target histogram M : (ns,nt) ndarray, float64 loss matrix - max_iter : int + numItermax : int The maximum number of iterations before stopping the optimization algorithm if it has not converged. @@ -76,18 +77,20 @@ def emd_c( np.ndarray[double, ndim=1, mode="c"] a,np.ndarray[double, ndim=1, mod b=np.ones((n2,))/n2 # calling the function - cdef int resultSolver = EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, alpha.data, beta.data, &cost, max_iter) + cdef int resultSolver = EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, alpha.data, beta.data, &cost, numItermax) if resultSolver != OPTIMAL: if resultSolver == INFEASIBLE: - print("Problem infeasible. Try to increase numItermax.") + warnings.warn("Problem infeasible. Check that a and b are in the simplex") elif resultSolver == UNBOUNDED: - print("Problem unbounded") + warnings.warn("Problem unbounded") + elif resultSolver == MAX_ITER_REACHED: + warnings.warn("numItermax reached before optimality. Try to increase numItermax.") return G, alpha, beta @cython.boundscheck(False) @cython.wraparound(False) -def emd2_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): +def emd2_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 numItermax): """ Solves the Earth Movers distance problem and returns the optimal transport loss @@ -114,7 +117,7 @@ def emd2_c( np.ndarray[double, ndim=1, mode="c"] a,np.ndarray[double, ndim=1, mo target histogram M : (ns,nt) ndarray, float64 loss matrix - max_iter : int + numItermax : int The maximum number of iterations before stopping the optimization algorithm if it has not converged. @@ -140,12 +143,14 @@ def emd2_c( np.ndarray[double, ndim=1, mode="c"] a,np.ndarray[double, ndim=1, mo if not len(b): b=np.ones((n2,))/n2 # calling the function - cdef int resultSolver = EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, alpha.data, beta.data, &cost, max_iter) + cdef int resultSolver = EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, alpha.data, beta.data, &cost, numItermax) if resultSolver != OPTIMAL: if resultSolver == INFEASIBLE: - print("Problem infeasible. Try to inscrease numItermax.") + warnings.warn("Problem infeasible. Check that a and b are in the simplex") elif resultSolver == UNBOUNDED: - print("Problem unbounded") + warnings.warn("Problem unbounded") + elif resultSolver == MAX_ITER_REACHED: + warnings.warn("numItermax reached before optimality. Try to increase numItermax.") return cost, alpha, beta diff --git a/ot/lp/network_simplex_simple.h b/ot/lp/network_simplex_simple.h index a7743ee..7c6a4ce 100644 --- a/ot/lp/network_simplex_simple.h +++ b/ot/lp/network_simplex_simple.h @@ -34,7 +34,8 @@ #endif -#define EPSILON 10*2.2204460492503131e-016 +#define EPSILON 2.2204460492503131e-15 +#define _EPSILON 1e-8 #define MAX_DEBUG_ITER 100000 @@ -260,7 +261,9 @@ namespace lemon { /// The objective function of the problem is unbounded, i.e. /// there is a directed cycle having negative total cost and /// infinite upper bound. - UNBOUNDED + UNBOUNDED, + /// The maximum number of iteration has been reached + MAX_ITER_REACHED }; /// \brief Constants for selecting the type of the supply constraints. @@ -683,7 +686,7 @@ namespace lemon { /// \see resetParams(), reset() ProblemType run() { #if DEBUG_LVL>0 - std::cout << "OPTIMAL = " << OPTIMAL << "\nINFEASIBLE = " << INFEASIBLE << "nUNBOUNDED = " << UNBOUNDED << "\n"; + std::cout << "OPTIMAL = " << OPTIMAL << "\nINFEASIBLE = " << INFEASIBLE << "\nUNBOUNDED = " << UNBOUNDED << "\nMAX_ITER_REACHED" << MAX_ITER_REACHED\n"; #endif if (!init()) return INFEASIBLE; @@ -941,15 +944,15 @@ namespace lemon { // Initialize internal data structures bool init() { if (_node_num == 0) return false; - /* + // Check the sum of supply values _sum_supply = 0; for (int i = 0; i != _node_num; ++i) { _sum_supply += _supply[i]; } - if ( !((_stype == GEQ && _sum_supply <= _epsilon ) || - (_stype == LEQ && _sum_supply >= -_epsilon )) ) return false; - */ + if ( fabs(_sum_supply) > _EPSILON ) return false; + + _sum_supply = 0; // Initialize artifical cost Cost ART_COST; @@ -1416,13 +1419,11 @@ namespace lemon { ProblemType start() { PivotRuleImpl pivot(*this); double prevCost=-1; + ProblemType retVal = OPTIMAL; // Perform heuristic initial pivots if (!initialPivots()) return UNBOUNDED; -#if DEBUG_LVL>0 - int niter=0; -#endif int iter_number=0; //pivot.setDantzig(true); // Execute the Network Simplex algorithm @@ -1431,12 +1432,13 @@ namespace lemon { char errMess[1000]; sprintf( errMess, "RESULT MIGHT BE INACURATE\nMax number of iteration reached, currently \%d. Sometimes iterations go on in cycle even though the solution has been reached, to check if it's the case here have a look at the minimal reduced cost. If it is very close to machine precision, you might actually have the correct solution, if not try setting the maximum number of iterations a bit higher\n",iter_number ); std::cerr << errMess; + retVal = MAX_ITER_REACHED; break; } #if DEBUG_LVL>0 - if(niter>MAX_DEBUG_ITER) + if(iter_number>MAX_DEBUG_ITER) break; - if(++niter%1000==0||niter%1000==1){ + if(iter_number%1000==0||iter_number%1000==1){ double curCost=totalCost(); double sumFlow=0; double a; @@ -1445,7 +1447,7 @@ namespace lemon { for (int i=0; i<_flow.size(); i++) { sumFlow+=_state[i]*_flow[i]; } - std::cout << "Sum of the flow " << std::setprecision(20) << sumFlow << "\n" << niter << " iterations, current cost=" << curCost << "\nReduced cost=" << _state[in_arc] * (_cost[in_arc] + _pi[_source[in_arc]] -_pi[_target[in_arc]]) << "\nPrecision = "<< -EPSILON*(a) << "\n"; + std::cout << "Sum of the flow " << std::setprecision(20) << sumFlow << "\n" << iter_number << " iterations, current cost=" << curCost << "\nReduced cost=" << _state[in_arc] * (_cost[in_arc] + _pi[_source[in_arc]] -_pi[_target[in_arc]]) << "\nPrecision = "<< -EPSILON*(a) << "\n"; std::cout << "Arc in = (" << _node_id(_source[in_arc]) << ", " << _node_id(_target[in_arc]) <<")\n"; std::cout << "Supplies = (" << _supply[_source[in_arc]] << ", " << _supply[_target[in_arc]] << ")\n"; std::cout << _cost[in_arc] << "\n"; @@ -1503,15 +1505,17 @@ namespace lemon { std::cout << "Sum of the flow " << sumFlow << "\n"<< niter <<" iterations, current cost=" << totalCost() << "\n"; #endif // Check feasibility - for (int e = _search_arc_num; e != _all_arc_num; ++e) { - if (_flow[e] != 0){ - if (abs(_flow[e]) > EPSILON) - return INFEASIBLE; - else - _flow[e]=0; + if( retVal == OPTIMAL){ + for (int e = _search_arc_num; e != _all_arc_num; ++e) { + if (_flow[e] != 0){ + if (abs(_flow[e]) > EPSILON) + return INFEASIBLE; + else + _flow[e]=0; + } } - } + } // Shift potentials to meet the requirements of the GEQ/LEQ type // optimality conditions @@ -1537,7 +1541,7 @@ namespace lemon { } } - return OPTIMAL; + return retVal; } }; //class NetworkSimplexSimple diff --git a/test/test_ot.py b/test/test_ot.py index 6f0f7c9..8a19cf6 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -8,6 +8,7 @@ import numpy as np import ot from ot.datasets import get_1D_gauss as gauss +import warnings def test_doctest(): @@ -100,9 +101,56 @@ def test_emd2_multi(): np.testing.assert_allclose(emd1, emdn) -def test_dual_variables(): - # %% parameters +def test_warnings(): + n = 100 # nb bins + m = 100 # nb bins + + mean1 = 30 + mean2 = 50 + + # bin positions + x = np.arange(n, dtype=np.float64) + y = np.arange(m, dtype=np.float64) + + # Gaussian distributions + a = gauss(n, m=mean1, s=5) # m= mean, s= std + + b = gauss(m, m=mean2, s=10) + # loss matrix + M = ot.dist(x.reshape((-1, 1)), y.reshape((-1, 1))) ** (1. / 2) + # M/=M.max() + + # %% + + print('Computing {} EMD '.format(1)) + G, alpha, beta = ot.emd(a, b, M, dual_variables=True) + with warnings.catch_warnings(record=True) as w: + # Cause all warnings to always be triggered. + warnings.simplefilter("always") + # Trigger a warning. + print('Computing {} EMD '.format(1)) + G, alpha, beta = ot.emd(a, b, M, dual_variables=True, numItermax=1) + # Verify some things + assert "numItermax" in str(w[-1].message) + assert len(w) == 1 + # Trigger a warning. + a[0]=100 + print('Computing {} EMD '.format(2)) + G, alpha, beta = ot.emd(a, b, M, dual_variables=True) + # Verify some things + assert "infeasible" in str(w[-1].message) + assert len(w) == 2 + # Trigger a warning. + a[0]=-1 + print('Computing {} EMD '.format(2)) + G, alpha, beta = ot.emd(a, b, M, dual_variables=True) + # Verify some things + assert "infeasible" in str(w[-1].message) + assert len(w) == 3 + + +def test_dual_variables(): n = 5000 # nb bins m = 6000 # nb bins -- cgit v1.2.3 From 12d9b3ff72e9669ccc0162e82b7a33beb51d3e25 Mon Sep 17 00:00:00 2001 From: Antoine Rolet Date: Thu, 7 Sep 2017 13:50:41 +0900 Subject: Return dual variables in an optional dictionary Also removed some code duplication --- ot/lp/__init__.py | 24 +++++++++++++------ ot/lp/emd_wrap.pyx | 69 +----------------------------------------------------- test/test_ot.py | 20 ++++++---------- 3 files changed, 25 insertions(+), 88 deletions(-) diff --git a/ot/lp/__init__.py b/ot/lp/__init__.py index 6048f60..c15e6b9 100644 --- a/ot/lp/__init__.py +++ b/ot/lp/__init__.py @@ -9,12 +9,12 @@ Solvers for the original linear program OT problem import numpy as np # import compiled emd -from .emd_wrap import emd_c, emd2_c +from .emd_wrap import emd_c from ..utils import parmap import multiprocessing -def emd(a, b, M, numItermax=100000, dual_variables=False): +def emd(a, b, M, numItermax=100000, log=False): """Solves the Earth Movers distance problem and returns the OT matrix @@ -42,11 +42,17 @@ def emd(a, b, M, numItermax=100000, dual_variables=False): numItermax : int, optional (default=100000) The maximum number of iterations before stopping the optimization algorithm if it has not converged. + log: boolean, optional (default=False) + If True, returns a dictionary containing the cost and dual + variables. Otherwise returns only the optimal transportation matrix. Returns ------- gamma: (ns x nt) ndarray Optimal transportation matrix for the given parameters + log: dict + If input log is true, a dictionary containing the cost and dual + variables Examples @@ -86,9 +92,13 @@ def emd(a, b, M, numItermax=100000, dual_variables=False): if len(b) == 0: b = np.ones((M.shape[1], ), dtype=np.float64)/M.shape[1] - G, alpha, beta = emd_c(a, b, M, numItermax) - if dual_variables: - return G, alpha, beta + G, cost, u, v = emd_c(a, b, M, numItermax) + if log: + log = {} + log['cost'] = cost + log['u'] = u + log['v'] = v + return G, log return G def emd2(a, b, M, processes=multiprocessing.cpu_count(), numItermax=100000): @@ -163,11 +173,11 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), numItermax=100000): b = np.ones((M.shape[1], ), dtype=np.float64)/M.shape[1] if len(b.shape)==1: - return emd2_c(a, b, M, numItermax)[0] + return emd_c(a, b, M, numItermax)[1] nb = b.shape[1] # res = [emd2_c(a, b[:, i].copy(), M, numItermax) for i in range(nb)] def f(b): - return emd2_c(a,b,M, numItermax)[0] + return emd_c(a,b,M, numItermax)[1] res= parmap(f, [b[:,i] for i in range(nb)],processes) return np.array(res) diff --git a/ot/lp/emd_wrap.pyx b/ot/lp/emd_wrap.pyx index 9bea154..5618dfc 100644 --- a/ot/lp/emd_wrap.pyx +++ b/ot/lp/emd_wrap.pyx @@ -86,71 +86,4 @@ def emd_c( np.ndarray[double, ndim=1, mode="c"] a,np.ndarray[double, ndim=1, mod elif resultSolver == MAX_ITER_REACHED: warnings.warn("numItermax reached before optimality. Try to increase numItermax.") - return G, alpha, beta - -@cython.boundscheck(False) -@cython.wraparound(False) -def emd2_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 numItermax): - """ - Solves the Earth Movers distance problem and returns the optimal transport loss - - gamm=emd(a,b,M) - - .. math:: - \gamma = arg\min_\gamma <\gamma,M>_F - - s.t. \gamma 1 = a - - \gamma^T 1= b - - \gamma\geq 0 - where : - - - M is the metric cost matrix - - a and b are the sample weights - - Parameters - ---------- - a : (ns,) ndarray, float64 - source histogram - b : (nt,) ndarray, float64 - target histogram - M : (ns,nt) ndarray, float64 - loss matrix - numItermax : int - The maximum number of iterations before stopping the optimization - algorithm if it has not converged. - - - Returns - ------- - gamma: (ns x nt) ndarray - Optimal transportation matrix for the given parameters - - """ - cdef int n1= M.shape[0] - cdef int n2= M.shape[1] - - cdef double cost=0 - cdef np.ndarray[double, ndim=2, mode="c"] G=np.zeros([n1, n2]) - - cdef np.ndarray[double, ndim = 1, mode = "c"] alpha = np.zeros([n1]) - cdef np.ndarray[double, ndim = 1, mode = "c"] beta = np.zeros([n2]) - - if not len(a): - a=np.ones((n1,))/n1 - - if not len(b): - b=np.ones((n2,))/n2 - # calling the function - cdef int resultSolver = EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, alpha.data, beta.data, &cost, numItermax) - if resultSolver != OPTIMAL: - if resultSolver == INFEASIBLE: - warnings.warn("Problem infeasible. Check that a and b are in the simplex") - elif resultSolver == UNBOUNDED: - warnings.warn("Problem unbounded") - elif resultSolver == MAX_ITER_REACHED: - warnings.warn("numItermax reached before optimality. Try to increase numItermax.") - - return cost, alpha, beta - + return G, cost, alpha, beta diff --git a/test/test_ot.py b/test/test_ot.py index 8a19cf6..78f64ab 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -124,27 +124,26 @@ def test_warnings(): # %% print('Computing {} EMD '.format(1)) - G, alpha, beta = ot.emd(a, b, M, dual_variables=True) with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") # Trigger a warning. print('Computing {} EMD '.format(1)) - G, alpha, beta = ot.emd(a, b, M, dual_variables=True, numItermax=1) + G = ot.emd(a, b, M, numItermax=1) # Verify some things assert "numItermax" in str(w[-1].message) assert len(w) == 1 # Trigger a warning. a[0]=100 print('Computing {} EMD '.format(2)) - G, alpha, beta = ot.emd(a, b, M, dual_variables=True) + G = ot.emd(a, b, M) # Verify some things assert "infeasible" in str(w[-1].message) assert len(w) == 2 # Trigger a warning. a[0]=-1 print('Computing {} EMD '.format(2)) - G, alpha, beta = ot.emd(a, b, M, dual_variables=True) + G = ot.emd(a, b, M) # Verify some things assert "infeasible" in str(w[-1].message) assert len(w) == 3 @@ -176,16 +175,11 @@ def test_dual_variables(): # emd loss 1 proc ot.tic() - G, alpha, beta = ot.emd(a, b, M, dual_variables=True) + G, log = ot.emd(a, b, M, log=True) ot.toc('1 proc : {} s') cost1 = (G * M).sum() - cost_dual = np.vdot(a, alpha) + np.vdot(b, beta) - - # emd loss 1 proc - ot.tic() - cost_emd2 = ot.emd2(a, b, M) - ot.toc('1 proc : {} s') + cost_dual = np.vdot(a, log['u']) + np.vdot(b, log['v']) ot.tic() G2 = ot.emd(b, a, np.ascontiguousarray(M.T)) @@ -194,7 +188,7 @@ def test_dual_variables(): cost2 = (G2 * M.T).sum() # Check that both cost computations are equivalent - np.testing.assert_almost_equal(cost1, cost_emd2) + np.testing.assert_almost_equal(cost1, log['cost']) # Check that dual and primal cost are equal np.testing.assert_almost_equal(cost1, cost_dual) # Check symmetry @@ -205,5 +199,5 @@ def test_dual_variables(): [ind1, ind2] = np.nonzero(G) # Check that reduced cost is zero on transport arcs - np.testing.assert_array_almost_equal((M - alpha.reshape(-1, 1) - beta.reshape(1, -1))[ind1, ind2], + np.testing.assert_array_almost_equal((M - log['u'].reshape(-1, 1) - log['v'].reshape(1, -1))[ind1, ind2], np.zeros(ind1.size)) -- cgit v1.2.3 From ab65f86304b03a967054eeeaf73b8c8277618d65 Mon Sep 17 00:00:00 2001 From: Antoine Rolet Date: Thu, 7 Sep 2017 14:35:35 +0900 Subject: Added log option to muliprocess emd --- ot/lp/__init__.py | 39 ++++++++++++++++++++++++------------- test/test_ot.py | 57 ++++++++++++++++++++++++++++++------------------------- 2 files changed, 57 insertions(+), 39 deletions(-) diff --git a/ot/lp/__init__.py b/ot/lp/__init__.py index c15e6b9..8edd8ec 100644 --- a/ot/lp/__init__.py +++ b/ot/lp/__init__.py @@ -7,11 +7,13 @@ Solvers for the original linear program OT problem # # License: MIT License +import multiprocessing + import numpy as np + # import compiled emd from .emd_wrap import emd_c from ..utils import parmap -import multiprocessing def emd(a, b, M, numItermax=100000, log=False): @@ -88,9 +90,9 @@ def emd(a, b, M, numItermax=100000, log=False): # if empty array given then use unifor distributions if len(a) == 0: - a = np.ones((M.shape[0], ), dtype=np.float64)/M.shape[0] + a = np.ones((M.shape[0],), dtype=np.float64) / M.shape[0] if len(b) == 0: - b = np.ones((M.shape[1], ), dtype=np.float64)/M.shape[1] + b = np.ones((M.shape[1],), dtype=np.float64) / M.shape[1] G, cost, u, v = emd_c(a, b, M, numItermax) if log: @@ -101,7 +103,8 @@ def emd(a, b, M, numItermax=100000, log=False): return G, log return G -def emd2(a, b, M, processes=multiprocessing.cpu_count(), numItermax=100000): + +def emd2(a, b, M, processes=multiprocessing.cpu_count(), numItermax=100000, log=False): """Solves the Earth Movers distance problem and returns the loss .. math:: @@ -168,16 +171,26 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), numItermax=100000): # if empty array given then use unifor distributions if len(a) == 0: - a = np.ones((M.shape[0], ), dtype=np.float64)/M.shape[0] + a = np.ones((M.shape[0],), dtype=np.float64) / M.shape[0] if len(b) == 0: - b = np.ones((M.shape[1], ), dtype=np.float64)/M.shape[1] - - if len(b.shape)==1: - return emd_c(a, b, M, numItermax)[1] + b = np.ones((M.shape[1],), dtype=np.float64) / M.shape[1] + + if log: + def f(b): + G, cost, u, v = emd_c(a, b, M, numItermax) + log = {} + log['G'] = G + log['u'] = u + log['v'] = v + return [cost, log] + else: + def f(b): + return emd_c(a, b, M, numItermax)[1] + + if len(b.shape) == 1: + return f(b) nb = b.shape[1] # res = [emd2_c(a, b[:, i].copy(), M, numItermax) for i in range(nb)] - def f(b): - return emd_c(a,b,M, numItermax)[1] - res= parmap(f, [b[:,i] for i in range(nb)],processes) - return np.array(res) + res = parmap(f, [b[:, i] for i in range(nb)], processes) + return res diff --git a/test/test_ot.py b/test/test_ot.py index 78f64ab..feadef4 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -4,11 +4,12 @@ # # License: MIT License +import warnings + import numpy as np import ot from ot.datasets import get_1D_gauss as gauss -import warnings def test_doctest(): @@ -100,6 +101,21 @@ def test_emd2_multi(): np.testing.assert_allclose(emd1, emdn) + # emd loss multipro proc with log + ot.tic() + emdn = ot.emd2(a, b, M, log=True) + ot.toc('multi proc : {} s') + + for i in range(len(emdn)): + emd = emdn[i] + log = emd[1] + cost = emd[0] + check_duality_gap(a, b[:, i], M, log['G'], log['u'], log['v'], cost) + emdn[i] = cost + + emdn = np.array(emdn) + np.testing.assert_allclose(emd1, emdn) + def test_warnings(): n = 100 # nb bins @@ -119,32 +135,22 @@ def test_warnings(): # loss matrix M = ot.dist(x.reshape((-1, 1)), y.reshape((-1, 1))) ** (1. / 2) - # M/=M.max() - - # %% print('Computing {} EMD '.format(1)) with warnings.catch_warnings(record=True) as w: - # Cause all warnings to always be triggered. warnings.simplefilter("always") - # Trigger a warning. print('Computing {} EMD '.format(1)) G = ot.emd(a, b, M, numItermax=1) - # Verify some things assert "numItermax" in str(w[-1].message) assert len(w) == 1 - # Trigger a warning. - a[0]=100 + a[0] = 100 print('Computing {} EMD '.format(2)) G = ot.emd(a, b, M) - # Verify some things assert "infeasible" in str(w[-1].message) assert len(w) == 2 - # Trigger a warning. - a[0]=-1 + a[0] = -1 print('Computing {} EMD '.format(2)) G = ot.emd(a, b, M) - # Verify some things assert "infeasible" in str(w[-1].message) assert len(w) == 3 @@ -167,9 +173,6 @@ def test_dual_variables(): # loss matrix M = ot.dist(x.reshape((-1, 1)), y.reshape((-1, 1))) ** (1. / 2) - # M/=M.max() - - # %% print('Computing {} EMD '.format(1)) @@ -178,26 +181,28 @@ def test_dual_variables(): G, log = ot.emd(a, b, M, log=True) ot.toc('1 proc : {} s') - cost1 = (G * M).sum() - cost_dual = np.vdot(a, log['u']) + np.vdot(b, log['v']) - ot.tic() G2 = ot.emd(b, a, np.ascontiguousarray(M.T)) ot.toc('1 proc : {} s') - cost2 = (G2 * M.T).sum() + cost1 = (G * M).sum() + # Check symmetry + np.testing.assert_array_almost_equal(cost1, (M * G2.T).sum()) + # Check with closed-form solution for gaussians + np.testing.assert_almost_equal(cost1, np.abs(mean1 - mean2)) # 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']) + + +def check_duality_gap(a, b, M, G, u, v, cost): + cost_dual = np.vdot(a, u) + np.vdot(b, v) # Check that dual and primal cost are equal - np.testing.assert_almost_equal(cost1, cost_dual) - # Check symmetry - np.testing.assert_almost_equal(cost1, cost2) - # Check with closed-form solution for gaussians - np.testing.assert_almost_equal(cost1, np.abs(mean1 - mean2)) + np.testing.assert_almost_equal(cost_dual, cost) [ind1, ind2] = np.nonzero(G) # Check that reduced cost is zero on transport arcs - np.testing.assert_array_almost_equal((M - log['u'].reshape(-1, 1) - log['v'].reshape(1, -1))[ind1, ind2], + np.testing.assert_array_almost_equal((M - u.reshape(-1, 1) - v.reshape(1, -1))[ind1, ind2], np.zeros(ind1.size)) -- cgit v1.2.3 From a37e52e64f300fa0165a58932d5ac0ef1dd8c6f7 Mon Sep 17 00:00:00 2001 From: Antoine Rolet Date: Thu, 7 Sep 2017 14:38:53 +0900 Subject: Removed unused variable declaration --- test/test_ot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/test_ot.py b/test/test_ot.py index feadef4..cf5839e 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -140,17 +140,17 @@ def test_warnings(): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") print('Computing {} EMD '.format(1)) - G = ot.emd(a, b, M, numItermax=1) + ot.emd(a, b, M, numItermax=1) assert "numItermax" in str(w[-1].message) assert len(w) == 1 a[0] = 100 print('Computing {} EMD '.format(2)) - G = ot.emd(a, b, M) + ot.emd(a, b, M) assert "infeasible" in str(w[-1].message) assert len(w) == 2 a[0] = -1 print('Computing {} EMD '.format(2)) - G = ot.emd(a, b, M) + ot.emd(a, b, M) assert "infeasible" in str(w[-1].message) assert len(w) == 3 -- cgit v1.2.3 From e58cd780ccf87736265e4e1a39afa3a167325ccc Mon Sep 17 00:00:00 2001 From: Antoine Rolet Date: Sat, 9 Sep 2017 12:37:56 +0900 Subject: Added convergence status to the log --- ot/lp/__init__.py | 16 ++++++++++++---- ot/lp/emd_wrap.pyx | 28 +++++++++++++++++----------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/ot/lp/__init__.py b/ot/lp/__init__.py index 8edd8ec..0f40c19 100644 --- a/ot/lp/__init__.py +++ b/ot/lp/__init__.py @@ -12,7 +12,7 @@ import multiprocessing import numpy as np # import compiled emd -from .emd_wrap import emd_c +from .emd_wrap import emd_c, checkResult from ..utils import parmap @@ -94,12 +94,15 @@ 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] - G, cost, u, v = emd_c(a, b, M, numItermax) + G, cost, u, v, resultCode = emd_c(a, b, M, numItermax) + resultCodeString = checkResult(resultCode) if log: log = {} log['cost'] = cost log['u'] = u log['v'] = v + log['warning'] = resultCodeString + log['resultCode'] = resultCode return G, log return G @@ -177,15 +180,20 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), numItermax=100000, log= if log: def f(b): - G, cost, u, v = emd_c(a, b, M, numItermax) + G, cost, u, v, resultCode = emd_c(a, b, M, numItermax) + resultCodeString = checkResult(resultCode) log = {} log['G'] = G log['u'] = u log['v'] = v + log['warning'] = resultCodeString + log['resultCode'] = resultCode return [cost, log] else: def f(b): - return emd_c(a, b, M, numItermax)[1] + G, cost, u, v, resultCode = emd_c(a, b, M, numItermax) + checkResult(resultCode) + return cost if len(b.shape) == 1: return f(b) diff --git a/ot/lp/emd_wrap.pyx b/ot/lp/emd_wrap.pyx index 5618dfc..19bcdd8 100644 --- a/ot/lp/emd_wrap.pyx +++ b/ot/lp/emd_wrap.pyx @@ -7,12 +7,12 @@ Cython linker with C solver # # License: MIT License -import warnings import numpy as np cimport numpy as np cimport cython +import warnings cdef extern from "EMD.h": @@ -20,6 +20,19 @@ cdef extern from "EMD.h": cdef enum ProblemType: INFEASIBLE, OPTIMAL, UNBOUNDED, MAX_ITER_REACHED +def checkResult(resultCode): + if resultCode == OPTIMAL: + return None + + if resultCode == INFEASIBLE: + message = "Problem infeasible. Check that a and b are in the simplex" + elif resultCode == UNBOUNDED: + message = "Problem unbounded" + elif resultCode == MAX_ITER_REACHED: + message = "numItermax reached before optimality. Try to increase numItermax." + warnings.warn(message) + return message + @cython.boundscheck(False) @cython.wraparound(False) @@ -77,13 +90,6 @@ def emd_c( np.ndarray[double, ndim=1, mode="c"] a,np.ndarray[double, ndim=1, mod b=np.ones((n2,))/n2 # calling the function - cdef int resultSolver = EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, alpha.data, beta.data, &cost, numItermax) - if resultSolver != OPTIMAL: - if resultSolver == INFEASIBLE: - warnings.warn("Problem infeasible. Check that a and b are in the simplex") - elif resultSolver == UNBOUNDED: - warnings.warn("Problem unbounded") - elif resultSolver == MAX_ITER_REACHED: - warnings.warn("numItermax reached before optimality. Try to increase numItermax.") - - return G, cost, alpha, beta + cdef int resultCode = EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, alpha.data, beta.data, &cost, numItermax) + + return G, cost, alpha, beta, resultCode -- cgit v1.2.3 From 430d9d88f2d09327ee3e72f65d742a0d69eaf16a Mon Sep 17 00:00:00 2001 From: Antoine Rolet Date: Sat, 9 Sep 2017 12:43:47 +0900 Subject: Added self to list of contributors and removed from acknowlegments --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 33eea6e..8d54f49 100644 --- a/README.md +++ b/README.md @@ -138,12 +138,12 @@ The contributors to this library are: * [Léo Gautheron](https://github.com/aje) (GPU implementation) * [Nathalie Gayraud](https://www.linkedin.com/in/nathalie-t-h-gayraud/?ppe=1) * [Stanislas Chambon](https://slasnista.github.io/) +* [Antoine Rolet](https://arolet.github.io/) This toolbox benefit a lot from open source research and we would like to thank the following persons for providing some code (in various languages): * [Gabriel Peyré](http://gpeyre.github.io/) (Wasserstein Barycenters in Matlab) * [Nicolas Bonneel](http://liris.cnrs.fr/~nbonneel/) ( C++ code for EMD) -* [Antoine Rolet](https://arolet.github.io/) ( Mex file for EMD ) * [Marco Cuturi](http://marcocuturi.net/) (Sinkhorn Knopp in Matlab/Cuda) -- cgit v1.2.3 -- cgit v1.2.3 From 85c56d96f609c4ad458f0963a068386cc910c66c Mon Sep 17 00:00:00 2001 From: Antoine Rolet Date: Sat, 9 Sep 2017 17:28:38 +0900 Subject: Renamed variables --- ot/da.py | 2 +- ot/lp/__init__.py | 31 +++++++++++++++++-------------- ot/lp/emd_wrap.pyx | 18 +++++++++--------- test/test_ot.py | 2 +- 4 files changed, 28 insertions(+), 25 deletions(-) diff --git a/ot/da.py b/ot/da.py index 1d3d0ba..eb70305 100644 --- a/ot/da.py +++ b/ot/da.py @@ -1370,7 +1370,7 @@ class EMDTransport(BaseTransport): # coupling estimation self.coupling_ = emd( - a=self.mu_s, b=self.mu_t, M=self.cost_, numItermax=self.max_iter + a=self.mu_s, b=self.mu_t, M=self.cost_, num_iter_max=self.max_iter ) return self diff --git a/ot/lp/__init__.py b/ot/lp/__init__.py index 0f40c19..ab7cb97 100644 --- a/ot/lp/__init__.py +++ b/ot/lp/__init__.py @@ -12,11 +12,11 @@ import multiprocessing import numpy as np # import compiled emd -from .emd_wrap import emd_c, checkResult +from .emd_wrap import emd_c, check_result from ..utils import parmap -def emd(a, b, M, numItermax=100000, log=False): +def emd(a, b, M, num_iter_max=100000, log=False): """Solves the Earth Movers distance problem and returns the OT matrix @@ -41,7 +41,7 @@ def emd(a, b, M, numItermax=100000, log=False): Target histogram (uniform weigth if empty list) M : (ns,nt) ndarray, float64 loss matrix - numItermax : int, optional (default=100000) + num_iter_max : int, optional (default=100000) The maximum number of iterations before stopping the optimization algorithm if it has not converged. log: boolean, optional (default=False) @@ -54,7 +54,7 @@ def emd(a, b, M, numItermax=100000, log=False): Optimal transportation matrix for the given parameters log: dict If input log is true, a dictionary containing the cost and dual - variables + variables and exit status Examples @@ -94,20 +94,20 @@ 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] - G, cost, u, v, resultCode = emd_c(a, b, M, numItermax) - resultCodeString = checkResult(resultCode) + G, cost, u, v, result_code = emd_c(a, b, M, num_iter_max) + resultCodeString = check_result(result_code) if log: log = {} log['cost'] = cost log['u'] = u log['v'] = v log['warning'] = resultCodeString - log['resultCode'] = resultCode + log['result_code'] = result_code return G, log return G -def emd2(a, b, M, processes=multiprocessing.cpu_count(), numItermax=100000, log=False): +def emd2(a, b, M, processes=multiprocessing.cpu_count(), num_iter_max=100000, log=False): """Solves the Earth Movers distance problem and returns the loss .. math:: @@ -131,7 +131,7 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), numItermax=100000, log= Target histogram (uniform weigth if empty list) M : (ns,nt) ndarray, float64 loss matrix - numItermax : int, optional (default=100000) + num_iter_max : int, optional (default=100000) The maximum number of iterations before stopping the optimization algorithm if it has not converged. @@ -139,6 +139,9 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), numItermax=100000, log= ------- gamma: (ns x nt) ndarray Optimal transportation matrix for the given parameters + log: dict + If input log is true, a dictionary containing the cost and dual + variables and exit status Examples @@ -180,19 +183,19 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), numItermax=100000, log= if log: def f(b): - G, cost, u, v, resultCode = emd_c(a, b, M, numItermax) - resultCodeString = checkResult(resultCode) + G, cost, u, v, resultCode = emd_c(a, b, M, num_iter_max) + resultCodeString = check_result(resultCode) log = {} log['G'] = G log['u'] = u log['v'] = v log['warning'] = resultCodeString - log['resultCode'] = resultCode + log['result_code'] = resultCode return [cost, log] else: def f(b): - G, cost, u, v, resultCode = emd_c(a, b, M, numItermax) - checkResult(resultCode) + G, cost, u, v, result_code = emd_c(a, b, M, num_iter_max) + check_result(result_code) return cost if len(b.shape) == 1: diff --git a/ot/lp/emd_wrap.pyx b/ot/lp/emd_wrap.pyx index 19bcdd8..7ebdd2a 100644 --- a/ot/lp/emd_wrap.pyx +++ b/ot/lp/emd_wrap.pyx @@ -20,15 +20,15 @@ cdef extern from "EMD.h": cdef enum ProblemType: INFEASIBLE, OPTIMAL, UNBOUNDED, MAX_ITER_REACHED -def checkResult(resultCode): - if resultCode == OPTIMAL: +def check_result(result_code): + if result_code == OPTIMAL: return None - if resultCode == INFEASIBLE: + if result_code == INFEASIBLE: message = "Problem infeasible. Check that a and b are in the simplex" - elif resultCode == UNBOUNDED: + elif result_code == UNBOUNDED: message = "Problem unbounded" - elif resultCode == MAX_ITER_REACHED: + elif result_code == MAX_ITER_REACHED: message = "numItermax reached before optimality. Try to increase numItermax." warnings.warn(message) return message @@ -36,7 +36,7 @@ def checkResult(resultCode): @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 numItermax): +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 num_iter_max): """ Solves the Earth Movers distance problem and returns the optimal transport matrix @@ -63,7 +63,7 @@ def emd_c( np.ndarray[double, ndim=1, mode="c"] a,np.ndarray[double, ndim=1, mod target histogram M : (ns,nt) ndarray, float64 loss matrix - numItermax : int + num_iter_max : int The maximum number of iterations before stopping the optimization algorithm if it has not converged. @@ -90,6 +90,6 @@ def emd_c( np.ndarray[double, ndim=1, mode="c"] a,np.ndarray[double, ndim=1, mod b=np.ones((n2,))/n2 # calling the function - cdef int resultCode = EMD_wrap(n1,n2, a.data, b.data, M.data, G.data, alpha.data, beta.data, &cost, numItermax) + cdef int result_code = EMD_wrap(n1, n2, a.data, b.data, M.data, G.data, alpha.data, beta.data, &cost, num_iter_max) - return G, cost, alpha, beta, resultCode + return G, cost, alpha, beta, result_code diff --git a/test/test_ot.py b/test/test_ot.py index cf5839e..c9b5154 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -140,7 +140,7 @@ def test_warnings(): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") print('Computing {} EMD '.format(1)) - ot.emd(a, b, M, numItermax=1) + ot.emd(a, b, M, num_iter_max=1) assert "numItermax" in str(w[-1].message) assert len(w) == 1 a[0] = 100 -- cgit v1.2.3 From 1ba2c837d54ce963ad63ddf8df2e47230800b747 Mon Sep 17 00:00:00 2001 From: Antoine Rolet Date: Sat, 9 Sep 2017 17:30:23 +0900 Subject: Renamed variables --- ot/lp/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ot/lp/__init__.py b/ot/lp/__init__.py index ab7cb97..1238cdb 100644 --- a/ot/lp/__init__.py +++ b/ot/lp/__init__.py @@ -95,13 +95,13 @@ def emd(a, b, M, num_iter_max=100000, log=False): b = np.ones((M.shape[1],), dtype=np.float64) / M.shape[1] G, cost, u, v, result_code = emd_c(a, b, M, num_iter_max) - resultCodeString = check_result(result_code) + result_code_string = check_result(result_code) if log: log = {} log['cost'] = cost log['u'] = u log['v'] = v - log['warning'] = resultCodeString + log['warning'] = result_code_string log['result_code'] = result_code return G, log return G @@ -184,12 +184,12 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), num_iter_max=100000, lo if log: def f(b): G, cost, u, v, resultCode = emd_c(a, b, M, num_iter_max) - resultCodeString = check_result(resultCode) + result_code_string = check_result(resultCode) log = {} log['G'] = G log['u'] = u log['v'] = v - log['warning'] = resultCodeString + log['warning'] = result_code_string log['result_code'] = resultCode return [cost, log] else: -- cgit v1.2.3 From cd8c04246b6d1f15b68d6433741e8c808fd517d8 Mon Sep 17 00:00:00 2001 From: Antoine Rolet Date: Sat, 9 Sep 2017 17:38:31 +0900 Subject: Renamed variable --- ot/da.py | 2 +- ot/lp/EMD.h | 2 +- ot/lp/EMD_wrapper.cpp | 4 ++-- ot/lp/__init__.py | 14 +++++++------- ot/lp/emd_wrap.pyx | 8 ++++---- test/test_ot.py | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/ot/da.py b/ot/da.py index eb70305..f3e7433 100644 --- a/ot/da.py +++ b/ot/da.py @@ -1370,7 +1370,7 @@ class EMDTransport(BaseTransport): # coupling estimation self.coupling_ = emd( - a=self.mu_s, b=self.mu_t, M=self.cost_, num_iter_max=self.max_iter + a=self.mu_s, b=self.mu_t, M=self.cost_, max_iter=self.max_iter ) return self diff --git a/ot/lp/EMD.h b/ot/lp/EMD.h index bb486de..f42e222 100644 --- a/ot/lp/EMD.h +++ b/ot/lp/EMD.h @@ -30,6 +30,6 @@ enum ProblemType { MAX_ITER_REACHED }; -int EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double* alpha, double* beta, double *cost, int max_iter); +int EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double* alpha, double* beta, double *cost, int maxIter); #endif diff --git a/ot/lp/EMD_wrapper.cpp b/ot/lp/EMD_wrapper.cpp index 92663dc..fc7ca63 100644 --- a/ot/lp/EMD_wrapper.cpp +++ b/ot/lp/EMD_wrapper.cpp @@ -16,7 +16,7 @@ int EMD_wrap(int n1, int n2, double *X, double *Y, double *D, double *G, - double* alpha, double* beta, double *cost, int max_iter) { + double* alpha, double* beta, double *cost, int maxIter) { // beware M and C anre strored in row major C style!!! int n, m, i, cur; @@ -48,7 +48,7 @@ int EMD_wrap(int n1, int n2, double *X, double *Y, double *D, double *G, std::vector indI(n), indJ(m); std::vector weights1(n), weights2(m); Digraph di(n, m); - NetworkSimplexSimple net(di, true, n+m, n*m, max_iter); + NetworkSimplexSimple net(di, true, n+m, n*m, maxIter); // Set supply and demand, don't account for 0 values (faster) diff --git a/ot/lp/__init__.py b/ot/lp/__init__.py index 1238cdb..9a0cb1c 100644 --- a/ot/lp/__init__.py +++ b/ot/lp/__init__.py @@ -16,7 +16,7 @@ from .emd_wrap import emd_c, check_result from ..utils import parmap -def emd(a, b, M, num_iter_max=100000, log=False): +def emd(a, b, M, max_iter=100000, log=False): """Solves the Earth Movers distance problem and returns the OT matrix @@ -41,7 +41,7 @@ def emd(a, b, M, num_iter_max=100000, log=False): Target histogram (uniform weigth if empty list) M : (ns,nt) ndarray, float64 loss matrix - num_iter_max : int, optional (default=100000) + max_iter : int, optional (default=100000) The maximum number of iterations before stopping the optimization algorithm if it has not converged. log: boolean, optional (default=False) @@ -94,7 +94,7 @@ def emd(a, b, M, num_iter_max=100000, log=False): if len(b) == 0: b = np.ones((M.shape[1],), dtype=np.float64) / M.shape[1] - G, cost, u, v, result_code = emd_c(a, b, M, num_iter_max) + G, cost, u, v, result_code = emd_c(a, b, M, max_iter) result_code_string = check_result(result_code) if log: log = {} @@ -107,7 +107,7 @@ def emd(a, b, M, num_iter_max=100000, log=False): return G -def emd2(a, b, M, processes=multiprocessing.cpu_count(), num_iter_max=100000, log=False): +def emd2(a, b, M, processes=multiprocessing.cpu_count(), max_iter=100000, log=False): """Solves the Earth Movers distance problem and returns the loss .. math:: @@ -131,7 +131,7 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), num_iter_max=100000, lo Target histogram (uniform weigth if empty list) M : (ns,nt) ndarray, float64 loss matrix - num_iter_max : int, optional (default=100000) + max_iter : int, optional (default=100000) The maximum number of iterations before stopping the optimization algorithm if it has not converged. @@ -183,7 +183,7 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), num_iter_max=100000, lo if log: def f(b): - G, cost, u, v, resultCode = emd_c(a, b, M, num_iter_max) + G, cost, u, v, resultCode = emd_c(a, b, M, max_iter) result_code_string = check_result(resultCode) log = {} log['G'] = G @@ -194,7 +194,7 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), num_iter_max=100000, lo return [cost, log] else: def f(b): - G, cost, u, v, result_code = emd_c(a, b, M, num_iter_max) + G, cost, u, v, result_code = emd_c(a, b, M, max_iter) check_result(result_code) return cost diff --git a/ot/lp/emd_wrap.pyx b/ot/lp/emd_wrap.pyx index 7ebdd2a..83ee6aa 100644 --- a/ot/lp/emd_wrap.pyx +++ b/ot/lp/emd_wrap.pyx @@ -16,7 +16,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 numItermax) + int EMD_wrap(int n1,int n2, double *X, double *Y,double *D, double *G, double* alpha, double* beta, double *cost, int maxIter) cdef enum ProblemType: INFEASIBLE, OPTIMAL, UNBOUNDED, MAX_ITER_REACHED @@ -36,7 +36,7 @@ def check_result(result_code): @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 num_iter_max): +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 @@ -63,7 +63,7 @@ def emd_c(np.ndarray[double, ndim=1, mode="c"] a, np.ndarray[double, ndim=1, mod target histogram M : (ns,nt) ndarray, float64 loss matrix - num_iter_max : int + max_iter : int The maximum number of iterations before stopping the optimization algorithm if it has not converged. @@ -90,6 +90,6 @@ def emd_c(np.ndarray[double, ndim=1, mode="c"] a, np.ndarray[double, ndim=1, mod b=np.ones((n2,))/n2 # calling the function - cdef int result_code = EMD_wrap(n1, n2, a.data, b.data, M.data, G.data, alpha.data, beta.data, &cost, num_iter_max) + cdef int 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 diff --git a/test/test_ot.py b/test/test_ot.py index c9b5154..ca921c5 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -140,7 +140,7 @@ def test_warnings(): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") print('Computing {} EMD '.format(1)) - ot.emd(a, b, M, num_iter_max=1) + ot.emd(a, b, M, max_iter=1) assert "numItermax" in str(w[-1].message) assert len(w) == 1 a[0] = 100 -- cgit v1.2.3 From 8cc04ef5ae8806c81811b2081b1880b46ca063a3 Mon Sep 17 00:00:00 2001 From: Antoine Rolet Date: Sat, 9 Sep 2017 18:05:12 +0900 Subject: Renamed variable in string --- ot/lp/__init__.py | 1 - ot/lp/emd_wrap.pyx | 2 +- test/test_ot.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ot/lp/__init__.py b/ot/lp/__init__.py index 9a0cb1c..ae5b08c 100644 --- a/ot/lp/__init__.py +++ b/ot/lp/__init__.py @@ -201,7 +201,6 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), max_iter=100000, log=Fa if len(b.shape) == 1: return f(b) nb = b.shape[1] - # res = [emd2_c(a, b[:, i].copy(), M, numItermax) for i in range(nb)] res = parmap(f, [b[:, i] for i in range(nb)], processes) return res diff --git a/ot/lp/emd_wrap.pyx b/ot/lp/emd_wrap.pyx index 83ee6aa..2fcc0e4 100644 --- a/ot/lp/emd_wrap.pyx +++ b/ot/lp/emd_wrap.pyx @@ -29,7 +29,7 @@ def check_result(result_code): elif result_code == UNBOUNDED: message = "Problem unbounded" elif result_code == MAX_ITER_REACHED: - message = "numItermax reached before optimality. Try to increase numItermax." + message = "max_iter reached before optimality. Try to increase max_iter." warnings.warn(message) return message diff --git a/test/test_ot.py b/test/test_ot.py index ca921c5..46fc634 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -141,7 +141,7 @@ def test_warnings(): warnings.simplefilter("always") print('Computing {} EMD '.format(1)) ot.emd(a, b, M, max_iter=1) - assert "numItermax" in str(w[-1].message) + assert "max_iter" in str(w[-1].message) assert len(w) == 1 a[0] = 100 print('Computing {} EMD '.format(2)) -- cgit v1.2.3 From 06429e5a34790ec51eb1c921293b24c37b81b952 Mon Sep 17 00:00:00 2001 From: Antoine Rolet Date: Sat, 9 Sep 2017 18:23:05 +0900 Subject: Returned to old variable name to follow repo convention --- ot/da.py | 2 +- ot/lp/__init__.py | 12 ++++++------ ot/lp/emd_wrap.pyx | 2 +- test/test_ot.py | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ot/da.py b/ot/da.py index f3e7433..eb70305 100644 --- a/ot/da.py +++ b/ot/da.py @@ -1370,7 +1370,7 @@ class EMDTransport(BaseTransport): # coupling estimation self.coupling_ = emd( - a=self.mu_s, b=self.mu_t, M=self.cost_, max_iter=self.max_iter + a=self.mu_s, b=self.mu_t, M=self.cost_, num_iter_max=self.max_iter ) return self diff --git a/ot/lp/__init__.py b/ot/lp/__init__.py index ae5b08c..17f5bb4 100644 --- a/ot/lp/__init__.py +++ b/ot/lp/__init__.py @@ -16,7 +16,7 @@ from .emd_wrap import emd_c, check_result from ..utils import parmap -def emd(a, b, M, max_iter=100000, log=False): +def emd(a, b, M, num_iter_max=100000, log=False): """Solves the Earth Movers distance problem and returns the OT matrix @@ -41,7 +41,7 @@ def emd(a, b, M, max_iter=100000, log=False): Target histogram (uniform weigth if empty list) M : (ns,nt) ndarray, float64 loss matrix - max_iter : int, optional (default=100000) + num_iter_max : int, optional (default=100000) The maximum number of iterations before stopping the optimization algorithm if it has not converged. log: boolean, optional (default=False) @@ -94,7 +94,7 @@ def emd(a, b, M, max_iter=100000, log=False): if len(b) == 0: b = np.ones((M.shape[1],), dtype=np.float64) / M.shape[1] - G, cost, u, v, result_code = emd_c(a, b, M, max_iter) + G, cost, u, v, result_code = emd_c(a, b, M, num_iter_max) result_code_string = check_result(result_code) if log: log = {} @@ -107,7 +107,7 @@ def emd(a, b, M, max_iter=100000, log=False): return G -def emd2(a, b, M, processes=multiprocessing.cpu_count(), max_iter=100000, log=False): +def emd2(a, b, M, processes=multiprocessing.cpu_count(), num_iter_max=100000, log=False): """Solves the Earth Movers distance problem and returns the loss .. math:: @@ -183,7 +183,7 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), max_iter=100000, log=Fa if log: def f(b): - G, cost, u, v, resultCode = emd_c(a, b, M, max_iter) + G, cost, u, v, resultCode = emd_c(a, b, M, num_iter_max) result_code_string = check_result(resultCode) log = {} log['G'] = G @@ -194,7 +194,7 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), max_iter=100000, log=Fa return [cost, log] else: def f(b): - G, cost, u, v, result_code = emd_c(a, b, M, max_iter) + G, cost, u, v, result_code = emd_c(a, b, M, num_iter_max) check_result(result_code) return cost diff --git a/ot/lp/emd_wrap.pyx b/ot/lp/emd_wrap.pyx index 2fcc0e4..45fc988 100644 --- a/ot/lp/emd_wrap.pyx +++ b/ot/lp/emd_wrap.pyx @@ -29,7 +29,7 @@ def check_result(result_code): elif result_code == UNBOUNDED: message = "Problem unbounded" elif result_code == MAX_ITER_REACHED: - message = "max_iter reached before optimality. Try to increase max_iter." + message = "num_iter_max reached before optimality. Try to increase num_iter_max." warnings.warn(message) return message diff --git a/test/test_ot.py b/test/test_ot.py index 46fc634..e05e8aa 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -140,8 +140,8 @@ def test_warnings(): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") print('Computing {} EMD '.format(1)) - ot.emd(a, b, M, max_iter=1) - assert "max_iter" in str(w[-1].message) + ot.emd(a, b, M, num_iter_max=1) + assert "num_iter_max" in str(w[-1].message) assert len(w) == 1 a[0] = 100 print('Computing {} EMD '.format(2)) -- cgit v1.2.3 From 7c6169222979a7e82a83c118bc7117684258d0de Mon Sep 17 00:00:00 2001 From: Antoine Rolet Date: Sat, 9 Sep 2017 18:29:32 +0900 Subject: Updated variable name in docstring --- ot/lp/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ot/lp/__init__.py b/ot/lp/__init__.py index 17f5bb4..f2eaa2b 100644 --- a/ot/lp/__init__.py +++ b/ot/lp/__init__.py @@ -131,7 +131,7 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), num_iter_max=100000, lo Target histogram (uniform weigth if empty list) M : (ns,nt) ndarray, float64 loss matrix - max_iter : int, optional (default=100000) + num_iter_max : int, optional (default=100000) The maximum number of iterations before stopping the optimization algorithm if it has not converged. -- cgit v1.2.3 From dd6f8260d01ce173ef3fe0c900112f0ed5288950 Mon Sep 17 00:00:00 2001 From: Antoine Rolet Date: Tue, 12 Sep 2017 19:58:46 +0900 Subject: Made the return of the matrix optional in emd2 --- ot/lp/__init__.py | 12 +++++++++--- test/test_ot.py | 6 +++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/ot/lp/__init__.py b/ot/lp/__init__.py index f2eaa2b..d0f682b 100644 --- a/ot/lp/__init__.py +++ b/ot/lp/__init__.py @@ -107,7 +107,7 @@ def emd(a, b, M, num_iter_max=100000, log=False): return G -def emd2(a, b, M, processes=multiprocessing.cpu_count(), num_iter_max=100000, log=False): +def emd2(a, b, M, processes=multiprocessing.cpu_count(), num_iter_max=100000, log=False, return_matrix=False): """Solves the Earth Movers distance problem and returns the loss .. math:: @@ -134,6 +134,11 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), num_iter_max=100000, lo num_iter_max : int, optional (default=100000) The maximum number of iterations before stopping the optimization algorithm if it has not converged. + log: boolean, optional (default=False) + If True, returns a dictionary containing the cost and dual + variables. Otherwise returns only the optimal transportation cost. + return_matrix: boolean, optional (default=False) + If True, returns the optimal transportation matrix in the log. Returns ------- @@ -181,12 +186,13 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), num_iter_max=100000, lo if len(b) == 0: b = np.ones((M.shape[1],), dtype=np.float64) / M.shape[1] - if log: + if log or return_matrix: def f(b): G, cost, u, v, resultCode = emd_c(a, b, M, num_iter_max) result_code_string = check_result(resultCode) log = {} - log['G'] = G + if return_matrix: + log['G'] = G log['u'] = u log['v'] = v log['warning'] = result_code_string diff --git a/test/test_ot.py b/test/test_ot.py index e05e8aa..ea6d9dc 100644 --- a/test/test_ot.py +++ b/test/test_ot.py @@ -103,7 +103,7 @@ def test_emd2_multi(): # emd loss multipro proc with log ot.tic() - emdn = ot.emd2(a, b, M, log=True) + emdn = ot.emd2(a, b, M, log=True, return_matrix=True) ot.toc('multi proc : {} s') for i in range(len(emdn)): @@ -140,8 +140,8 @@ def test_warnings(): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") print('Computing {} EMD '.format(1)) - ot.emd(a, b, M, num_iter_max=1) - assert "num_iter_max" in str(w[-1].message) + ot.emd(a, b, M, numItermax=1) + assert "numItermax" in str(w[-1].message) assert len(w) == 1 a[0] = 100 print('Computing {} EMD '.format(2)) -- cgit v1.2.3 From e52b6eb41228a7f8e381cf73c06e0dffba5773be Mon Sep 17 00:00:00 2001 From: Antoine Rolet Date: Tue, 12 Sep 2017 20:00:14 +0900 Subject: Renaming --- ot/da.py | 2 +- ot/lp/__init__.py | 14 +++++++------- ot/lp/emd_wrap.pyx | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ot/da.py b/ot/da.py index eb70305..1d3d0ba 100644 --- a/ot/da.py +++ b/ot/da.py @@ -1370,7 +1370,7 @@ class EMDTransport(BaseTransport): # coupling estimation self.coupling_ = emd( - a=self.mu_s, b=self.mu_t, M=self.cost_, num_iter_max=self.max_iter + a=self.mu_s, b=self.mu_t, M=self.cost_, numItermax=self.max_iter ) return self diff --git a/ot/lp/__init__.py b/ot/lp/__init__.py index d0f682b..5c09da2 100644 --- a/ot/lp/__init__.py +++ b/ot/lp/__init__.py @@ -16,7 +16,7 @@ from .emd_wrap import emd_c, check_result from ..utils import parmap -def emd(a, b, M, num_iter_max=100000, log=False): +def emd(a, b, M, numItermax=100000, log=False): """Solves the Earth Movers distance problem and returns the OT matrix @@ -41,7 +41,7 @@ def emd(a, b, M, num_iter_max=100000, log=False): Target histogram (uniform weigth if empty list) M : (ns,nt) ndarray, float64 loss matrix - num_iter_max : int, optional (default=100000) + numItermax : int, optional (default=100000) The maximum number of iterations before stopping the optimization algorithm if it has not converged. log: boolean, optional (default=False) @@ -94,7 +94,7 @@ def emd(a, b, M, num_iter_max=100000, log=False): if len(b) == 0: b = np.ones((M.shape[1],), dtype=np.float64) / M.shape[1] - G, cost, u, v, result_code = emd_c(a, b, M, num_iter_max) + G, cost, u, v, result_code = emd_c(a, b, M, numItermax) result_code_string = check_result(result_code) if log: log = {} @@ -107,7 +107,7 @@ def emd(a, b, M, num_iter_max=100000, log=False): return G -def emd2(a, b, M, processes=multiprocessing.cpu_count(), num_iter_max=100000, log=False, return_matrix=False): +def emd2(a, b, M, processes=multiprocessing.cpu_count(), numItermax=100000, log=False, return_matrix=False): """Solves the Earth Movers distance problem and returns the loss .. math:: @@ -131,7 +131,7 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), num_iter_max=100000, lo Target histogram (uniform weigth if empty list) M : (ns,nt) ndarray, float64 loss matrix - num_iter_max : int, optional (default=100000) + numItermax : int, optional (default=100000) The maximum number of iterations before stopping the optimization algorithm if it has not converged. log: boolean, optional (default=False) @@ -188,7 +188,7 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), num_iter_max=100000, lo if log or return_matrix: def f(b): - G, cost, u, v, resultCode = emd_c(a, b, M, num_iter_max) + G, cost, u, v, resultCode = emd_c(a, b, M, numItermax) result_code_string = check_result(resultCode) log = {} if return_matrix: @@ -200,7 +200,7 @@ def emd2(a, b, M, processes=multiprocessing.cpu_count(), num_iter_max=100000, lo return [cost, log] else: def f(b): - G, cost, u, v, result_code = emd_c(a, b, M, num_iter_max) + G, cost, u, v, result_code = emd_c(a, b, M, numItermax) check_result(result_code) return cost diff --git a/ot/lp/emd_wrap.pyx b/ot/lp/emd_wrap.pyx index 45fc988..83ee6aa 100644 --- a/ot/lp/emd_wrap.pyx +++ b/ot/lp/emd_wrap.pyx @@ -29,7 +29,7 @@ def check_result(result_code): elif result_code == UNBOUNDED: message = "Problem unbounded" elif result_code == MAX_ITER_REACHED: - message = "num_iter_max reached before optimality. Try to increase num_iter_max." + message = "numItermax reached before optimality. Try to increase numItermax." warnings.warn(message) return message -- cgit v1.2.3 From 36bf599552ff15d1ca1c6b505507e65a333fa55e Mon Sep 17 00:00:00 2001 From: Nicolas Courty Date: Tue, 12 Sep 2017 18:07:17 +0200 Subject: Corrections on Gromov --- data/carre.png | Bin 168 -> 0 bytes data/coeur.png | Bin 225 -> 0 bytes data/cross.png | Bin 0 -> 230 bytes data/rond.png | Bin 230 -> 0 bytes data/square.png | Bin 0 -> 168 bytes data/star.png | Bin 0 -> 225 bytes examples/plot_gromov.py | 5 +++-- examples/plot_gromov_barycenter.py | 13 +++++-------- test/test_gromov.py | 3 +-- 9 files changed, 9 insertions(+), 12 deletions(-) delete mode 100755 data/carre.png delete mode 100755 data/coeur.png create mode 100755 data/cross.png delete mode 100755 data/rond.png create mode 100755 data/square.png create mode 100755 data/star.png diff --git a/data/carre.png b/data/carre.png deleted file mode 100755 index 45ff0ef..0000000 Binary files a/data/carre.png and /dev/null differ diff --git a/data/coeur.png b/data/coeur.png deleted file mode 100755 index 3f511a6..0000000 Binary files a/data/coeur.png and /dev/null differ diff --git a/data/cross.png b/data/cross.png new file mode 100755 index 0000000..1c1a068 Binary files /dev/null and b/data/cross.png differ diff --git a/data/rond.png b/data/rond.png deleted file mode 100755 index 1c1a068..0000000 Binary files a/data/rond.png and /dev/null differ diff --git a/data/square.png b/data/square.png new file mode 100755 index 0000000..45ff0ef Binary files /dev/null and b/data/square.png differ diff --git a/data/star.png b/data/star.png new file mode 100755 index 0000000..3f511a6 Binary files /dev/null and b/data/star.png differ diff --git a/examples/plot_gromov.py b/examples/plot_gromov.py index 92312ae..0f839a3 100644 --- a/examples/plot_gromov.py +++ b/examples/plot_gromov.py @@ -22,8 +22,9 @@ import ot """ Sample two Gaussian distributions (2D and 3D) ============================================= -The Gromov-Wasserstein distance allows to compute distances with samples that do not belong to the same metric space. -For demonstration purpose, we sample two Gaussian distributions in 2- and 3-dimensional spaces. +The Gromov-Wasserstein distance allows to compute distances with samples that +do not belong to the same metric space. For demonstration purpose, we sample +two Gaussian distributions in 2- and 3-dimensional spaces. """ n_samples = 30 # nb samples diff --git a/examples/plot_gromov_barycenter.py b/examples/plot_gromov_barycenter.py index 4f17117..c138031 100755 --- a/examples/plot_gromov_barycenter.py +++ b/examples/plot_gromov_barycenter.py @@ -48,13 +48,10 @@ def smacof_mds(C, dim, max_iter=3000, eps=1e-9): eps : float relative tolerance w.r.t stress to declare converge - Returns ------- npos : ndarray, shape (R, dim) Embedded coordinates of the interpolated point cloud (defined with one isometry) - - """ rng = np.random.RandomState(seed=3) @@ -91,12 +88,12 @@ def im2mat(I): return I.reshape((I.shape[0] * I.shape[1], I.shape[2])) -square = spi.imread('../data/carre.png').astype(np.float64) / 256 -circle = spi.imread('../data/rond.png').astype(np.float64) / 256 -triangle = spi.imread('../data/triangle.png').astype(np.float64) / 256 -arrow = spi.imread('../data/coeur.png').astype(np.float64) / 256 +square = spi.imread('../data/square.png').astype(np.float64)[:,:,2] / 256 +cross = spi.imread('../data/cross.png').astype(np.float64)[:,:,2] / 256 +triangle = spi.imread('../data/triangle.png').astype(np.float64)[:,:,2] / 256 +star = spi.imread('../data/star.png').astype(np.float64)[:,:,2] / 256 -shapes = [square, circle, triangle, arrow] +shapes = [square, cross, triangle, star] S = 4 xs = [[] for i in range(S)] diff --git a/test/test_gromov.py b/test/test_gromov.py index 28495e1..e808292 100644 --- a/test/test_gromov.py +++ b/test/test_gromov.py @@ -17,8 +17,7 @@ def test_gromov(): xs = ot.datasets.get_2D_samples_gauss(n_samples, mu_s, cov_s) - xt = xs[::-1] - xt = np.array(xt) + xt = xs[::-1].copy() p = ot.unif(n_samples) q = ot.unif(n_samples) -- cgit v1.2.3 From 24784eda59cf591746bf4ba62f325c5612ada430 Mon Sep 17 00:00:00 2001 From: Nicolas Courty Date: Tue, 12 Sep 2017 22:08:29 +0200 Subject: Corrections on Gromov --- ot/gromov.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/ot/gromov.py b/ot/gromov.py index 1726f5e..82e3fd3 100644 --- a/ot/gromov.py +++ b/ot/gromov.py @@ -40,7 +40,6 @@ def tensor_square_loss(C1, C2, T): function as the loss function of Gromow-Wasserstein discrepancy. Where : - C1 : Metric cost matrix in the source space C2 : Metric cost matrix in the target space T : A coupling between those two spaces @@ -61,13 +60,10 @@ def tensor_square_loss(C1, C2, T): T : ndarray, shape (ns, nt) Coupling between source and target spaces - Returns ------- tens : ndarray, shape (ns, nt) \mathcal{L}(C1,C2) \otimes T tensor-matrix multiplication result - - """ C1 = np.asarray(C1, dtype=np.float64) @@ -119,7 +115,6 @@ def tensor_kl_loss(C1, C2, T): T : ndarray, shape (ns, nt) Coupling between source and target spaces - Returns ------- tens : ndarray, shape (ns, nt) @@ -127,7 +122,6 @@ def tensor_kl_loss(C1, C2, T): References ---------- - .. [12] Peyré, Gabriel, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. """ @@ -159,7 +153,6 @@ def update_square_loss(p, lambdas, T, Cs): Updates C according to the L2 Loss kernel with the S Ts couplings calculated at each iteration - Parameters ---------- p : ndarray, shape (N,) @@ -174,8 +167,6 @@ def update_square_loss(p, lambdas, T, Cs): ---------- C : ndarray, shape (nt,nt) updated C matrix - - """ tmpsum = sum([lambdas[s] * np.dot(T[s].T, Cs[s]).dot(T[s]) for s in range(len(T))]) ppt = np.outer(p, p) @@ -202,8 +193,6 @@ def update_kl_loss(p, lambdas, T, Cs): ---------- C : ndarray, shape (ns,ns) updated C matrix - - """ tmpsum = sum([lambdas[s] * np.dot(T[s].T, Cs[s]).dot(T[s]) for s in range(len(T))]) ppt = np.outer(p, p) @@ -229,7 +218,6 @@ def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, tol=1e-9, \GW\geq 0 Where : - C1 : Metric cost matrix in the source space C2 : Metric cost matrix in the target space p : distribution in the source space @@ -237,7 +225,6 @@ def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, tol=1e-9, L : loss function to account for the misfit between the similarity matrices H : entropy - Parameters ---------- C1 : ndarray, shape (ns, ns) @@ -261,13 +248,11 @@ def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, tol=1e-9, log : bool, optional record log if True - Returns ------- T : ndarray, shape (ns, nt) coupling between the two spaces that minimizes : \sum_{i,j,k,l} L(C1_{i,k},C2_{j,l})*T_{i,j}*T_{k,l}-\epsilon(H(T)) - """ C1 = np.asarray(C1, dtype=np.float64) @@ -322,9 +307,7 @@ def gromov_wasserstein2(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, tol=1e-9 .. math:: \GW_Dist = \min_T \sum_{i,j,k,l} L(C1_{i,k},C2_{j,l})*T_{i,j}*T_{k,l}-\epsilon(H(T)) - Where : - C1 : Metric cost matrix in the source space C2 : Metric cost matrix in the target space p : distribution in the source space @@ -332,7 +315,6 @@ def gromov_wasserstein2(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, tol=1e-9 L : loss function to account for the misfit between the similarity matrices H : entropy - Parameters ---------- C1 : ndarray, shape (ns, ns) @@ -360,7 +342,6 @@ def gromov_wasserstein2(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, tol=1e-9 ------- gw_dist : float Gromov-Wasserstein distance - """ if log: @@ -428,7 +409,6 @@ def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, max_iter=1000, ------- C : ndarray, shape (N, N) Similarity matrix in the barycenter space (permutated arbitrarily) - """ S = len(Cs) -- cgit v1.2.3 From 84c272394d41d159d07174306b324590b3ffe40c Mon Sep 17 00:00:00 2001 From: Nicolas Courty Date: Wed, 13 Sep 2017 01:03:21 +0200 Subject: Corrections on Gromov --- examples/plot_gromov.py | 4 ++-- examples/plot_gromov_barycenter.py | 18 +++++++++------- ot/gromov.py | 44 ++++++++++++++++++++++++++------------ 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/examples/plot_gromov.py b/examples/plot_gromov.py index 0f839a3..dce66c4 100644 --- a/examples/plot_gromov.py +++ b/examples/plot_gromov.py @@ -22,8 +22,8 @@ import ot """ Sample two Gaussian distributions (2D and 3D) ============================================= -The Gromov-Wasserstein distance allows to compute distances with samples that -do not belong to the same metric space. For demonstration purpose, we sample +The Gromov-Wasserstein distance allows to compute distances with samples that +do not belong to the same metric space. For demonstration purpose, we sample two Gaussian distributions in 2- and 3-dimensional spaces. """ diff --git a/examples/plot_gromov_barycenter.py b/examples/plot_gromov_barycenter.py index c138031..52f4966 100755 --- a/examples/plot_gromov_barycenter.py +++ b/examples/plot_gromov_barycenter.py @@ -3,7 +3,7 @@ ===================================== Gromov-Wasserstein Barycenter example ===================================== -This example is designed to show how to use the Gromov-Wassertsein distance +This example is designed to show how to use the Gromov-Wasserstein distance computation in POT. """ @@ -34,8 +34,9 @@ that will be given by the output of the algorithm def smacof_mds(C, dim, max_iter=3000, eps=1e-9): """ - Returns an interpolated point cloud following the dissimilarity matrix C using SMACOF - multidimensional scaling (MDS) in specific dimensionned target space + Returns an interpolated point cloud following the dissimilarity matrix C + using SMACOF multidimensional scaling (MDS) in specific dimensionned + target space Parameters ---------- @@ -51,7 +52,8 @@ def smacof_mds(C, dim, max_iter=3000, eps=1e-9): Returns ------- npos : ndarray, shape (R, dim) - Embedded coordinates of the interpolated point cloud (defined with one isometry) + Embedded coordinates of the interpolated point cloud (defined with + one isometry) """ rng = np.random.RandomState(seed=3) @@ -88,10 +90,10 @@ def im2mat(I): return I.reshape((I.shape[0] * I.shape[1], I.shape[2])) -square = spi.imread('../data/square.png').astype(np.float64)[:,:,2] / 256 -cross = spi.imread('../data/cross.png').astype(np.float64)[:,:,2] / 256 -triangle = spi.imread('../data/triangle.png').astype(np.float64)[:,:,2] / 256 -star = spi.imread('../data/star.png').astype(np.float64)[:,:,2] / 256 +square = spi.imread('../data/square.png').astype(np.float64)[:, :, 2] / 256 +cross = spi.imread('../data/cross.png').astype(np.float64)[:, :, 2] / 256 +triangle = spi.imread('../data/triangle.png').astype(np.float64)[:, :, 2] / 256 +star = spi.imread('../data/star.png').astype(np.float64)[:, :, 2] / 256 shapes = [square, cross, triangle, star] diff --git a/ot/gromov.py b/ot/gromov.py index 82e3fd3..7968e5e 100644 --- a/ot/gromov.py +++ b/ot/gromov.py @@ -122,7 +122,9 @@ def tensor_kl_loss(C1, C2, T): References ---------- - .. [12] Peyré, Gabriel, Marco Cuturi, and Justin Solomon, "Gromov-Wasserstein averaging of kernel and distance matrices." International Conference on Machine Learning (ICML). 2016. + .. [12] Peyré, Gabriel, Marco Cuturi, and Justin Solomon, + "Gromov-Wasserstein averaging of kernel and distance matrices." + International Conference on Machine Learning (ICML). 2016. """ @@ -157,7 +159,8 @@ def update_square_loss(p, lambdas, T, Cs): ---------- p : ndarray, shape (N,) weights in the targeted barycenter - lambdas : list of the S spaces' weights + lambdas : list of float + list of the S spaces' weights T : list of S np.ndarray(ns,N) the S Ts couplings calculated at each iteration Cs : list of S ndarray, shape(ns,ns) @@ -168,7 +171,8 @@ def update_square_loss(p, lambdas, T, Cs): C : ndarray, shape (nt,nt) updated C matrix """ - tmpsum = sum([lambdas[s] * np.dot(T[s].T, Cs[s]).dot(T[s]) for s in range(len(T))]) + tmpsum = sum([lambdas[s] * np.dot(T[s].T, Cs[s]).dot(T[s]) + for s in range(len(T))]) ppt = np.outer(p, p) return np.divide(tmpsum, ppt) @@ -194,13 +198,15 @@ def update_kl_loss(p, lambdas, T, Cs): C : ndarray, shape (ns,ns) updated C matrix """ - tmpsum = sum([lambdas[s] * np.dot(T[s].T, Cs[s]).dot(T[s]) for s in range(len(T))]) + tmpsum = sum([lambdas[s] * np.dot(T[s].T, Cs[s]).dot(T[s]) + for s in range(len(T))]) ppt = np.outer(p, p) return np.exp(np.divide(tmpsum, ppt)) -def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, tol=1e-9, verbose=False, log=False): +def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, + max_iter=1000, tol=1e-9, verbose=False, log=False): """ Returns the gromov-wasserstein coupling between the two measured similarity matrices @@ -276,7 +282,8 @@ def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, tol=1e-9, T = sinkhorn(p, q, tens, epsilon) if cpt % 10 == 0: - # we can speed up the process by checking for the error only all the 10th iterations + # we can speed up the process by checking for the error only all + # the 10th iterations err = np.linalg.norm(T - Tprev) if log: @@ -296,7 +303,8 @@ def gromov_wasserstein(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, tol=1e-9, return T -def gromov_wasserstein2(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, tol=1e-9, verbose=False, log=False): +def gromov_wasserstein2(C1, C2, p, q, loss_fun, epsilon, + max_iter=1000, tol=1e-9, verbose=False, log=False): """ Returns the gromov-wasserstein discrepancy between the two measured similarity matrices @@ -363,7 +371,8 @@ def gromov_wasserstein2(C1, C2, p, q, loss_fun, epsilon, max_iter=1000, tol=1e-9 return gw_dist -def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, max_iter=1000, tol=1e-9, verbose=False, log=False): +def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, + max_iter=1000, tol=1e-9, verbose=False, log=False, init_C=None): """ Returns the gromov-wasserstein barycenters of S measured similarity matrices @@ -390,7 +399,8 @@ def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, max_iter=1000, sample weights in the S spaces p : ndarray, shape(N,) weights in the targeted barycenter - lambdas : list of the S spaces' weights + lambdas : list of float + list of the S spaces' weights L : tensor-matrix multiplication function based on specific loss function update : function(p,lambdas,T,Cs) that updates C according to a specific Kernel with the S Ts couplings calculated at each iteration @@ -404,6 +414,8 @@ def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, max_iter=1000, Print information along iterations log : bool, optional record log if True + init_C : bool, ndarray, shape(N,N) + random initial value for the C matrix provided by user Returns ------- @@ -416,10 +428,13 @@ def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, max_iter=1000, Cs = [np.asarray(Cs[s], dtype=np.float64) for s in range(S)] lambdas = np.asarray(lambdas, dtype=np.float64) - # Initialization of C : random SPD matrix - xalea = np.random.randn(N, 2) - C = dist(xalea, xalea) - C /= C.max() + # Initialization of C : random SPD matrix (if not provided by user) + if init_C is None: + xalea = np.random.randn(N, 2) + C = dist(xalea, xalea) + C /= C.max() + else: + C = init_C cpt = 0 err = 1 @@ -438,7 +453,8 @@ def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, max_iter=1000, C = update_kl_loss(p, lambdas, T, Cs) if cpt % 10 == 0: - # we can speed up the process by checking for the error only all the 10th iterations + # we can speed up the process by checking for the error only all + # the 10th iterations err = np.linalg.norm(C - Cprev) error.append(err) -- cgit v1.2.3 From 55db3508917c73c4811a82933f892fc3017300f2 Mon Sep 17 00:00:00 2001 From: Nicolas Courty Date: Wed, 13 Sep 2017 01:09:26 +0200 Subject: Corrections on Gromov --- ot/gromov.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ot/gromov.py b/ot/gromov.py index 7968e5e..20bf7ee 100644 --- a/ot/gromov.py +++ b/ot/gromov.py @@ -158,7 +158,7 @@ def update_square_loss(p, lambdas, T, Cs): Parameters ---------- p : ndarray, shape (N,) - weights in the targeted barycenter + masses in the targeted barycenter lambdas : list of float list of the S spaces' weights T : list of S np.ndarray(ns,N) @@ -401,7 +401,7 @@ def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, weights in the targeted barycenter lambdas : list of float list of the S spaces' weights - L : tensor-matrix multiplication function based on specific loss function + loss_fun : tensor-matrix multiplication function based on specific loss function update : function(p,lambdas,T,Cs) that updates C according to a specific Kernel with the S Ts couplings calculated at each iteration epsilon : float -- cgit v1.2.3 From 5a2ebfab92a655e636efee1d91d44c3023e25828 Mon Sep 17 00:00:00 2001 From: ncourty Date: Wed, 13 Sep 2017 10:07:47 +0200 Subject: Corrections on Gromov --- ot/gromov.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ot/gromov.py b/ot/gromov.py index 7968e5e..20bf7ee 100644 --- a/ot/gromov.py +++ b/ot/gromov.py @@ -158,7 +158,7 @@ def update_square_loss(p, lambdas, T, Cs): Parameters ---------- p : ndarray, shape (N,) - weights in the targeted barycenter + masses in the targeted barycenter lambdas : list of float list of the S spaces' weights T : list of S np.ndarray(ns,N) @@ -401,7 +401,7 @@ def gromov_barycenters(N, Cs, ps, p, lambdas, loss_fun, epsilon, weights in the targeted barycenter lambdas : list of float list of the S spaces' weights - L : tensor-matrix multiplication function based on specific loss function + loss_fun : tensor-matrix multiplication function based on specific loss function update : function(p,lambdas,T,Cs) that updates C according to a specific Kernel with the S Ts couplings calculated at each iteration epsilon : float -- cgit v1.2.3