-
Using Julia to compute the Kantorovich distance
2014-04-09
SourceIn the article 'Using R to compute the Kantorovich distance' I have shown how to use the cddlibb C library through R with the help of the rccd R package to compute the Kantorovich distance between two probability measures (on a finite set). In the present article I show how to do so using three different ways with Julia:
GLPK: Similarly to the R way, this Julia way uses a C library, the GLPK (GNU Linear Programming Kit) library, wrapped in a Julia package, named GLPK too.
RationalSimplex: Using Iain Dunning's RationalSimplex module: Pure Julia implementation of the simplex algorithm for rational numbers. This way is the only one allowing to get the exact value when dealing with rational numbers.
In the current version of this article, I will only detail the
GLPKmethod. I express my gratitude to all guys on julia-users who kindly provided me their unvaluable help for this article.GLPK library
I will try to explain in details the approach using
GLPK, without assuming the reader has some knowledge about Julia.Setting the data of the problem
First, we define the probability measures \(\mu\) and \(\nu\) between which we want the Kantorovich distance to be computed.
mu = [1/7, 2/7, 4/7] nu = [1/4, 1/4, 1/2]Recall that the Kantorovich distance is defined from an initial distance which we take to be the \(0-1\) distance, and is represented in the \(D\) matrix defined as follows with Julia:
n = length(mu) D = 1.-eye(n);julia> D 3x3 Array{Float64,2}: 0.0 1.0 1.0 1.0 0.0 1.0 1.0 1.0 0.0Thus, the Julia
eyefunction is similar to the Rdiagfunction.Note that we have defined \(D\) by typing "
1.-eye(n)" and not "1-eye(n)" which is ambiguous because "1" and "eye(n)" have not the same size...
I'm afraid you are puzzled because you don't know whether "1.-eye(n)" is1. - eye(n)or
1 .- eye(n)? Don't worry, this is very easy to know with Julia, you can get the structure of "
1.-eye(n)" as an expression:julia> :(1.-eye(n)) :(.-(1,eye(n)))That means the operator "
.-" acts on the integer "1" and the matrix "eye(n)", whereas "1. - eye(n)" is the expression for the operator "-" acting on the float "1." and "eye(n)":julia> :(1. - eye(n)) :(-(1.0,eye(n)))Constraint matrix
The constraints matrix is \[ A=\begin{pmatrix} M_1 \\ M_2 \end{pmatrix} \] where \[ M_1 = \begin{pmatrix} 1 & 1 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 1 & 1 & 1 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 1 & 1 & 1 \end{pmatrix} \] defines the linear equality constraints corresponding to \(\mu\) and \[ M_2 = \begin{pmatrix} 1 & 0 & 0 & 1 & 0 & 0 & 1 & 0 & 0 \\ 0 & 1 & 0 & 0 & 1 & 0 & 0 & 1 & 0 \\ 0 & 0 & 1 & 0 & 0 & 1 & 0 & 0 & 1 \end{pmatrix}. \] defines the linear equality constraints corresponding to \(\nu\).
I define these matrices as follows in Julia:
M1=zeros(n,n*n) for i in 1:n M1[i,(1:n)+n*(i-1)]=1 end M2=repmat(eye(n),1,n) A=vcat(M1,M2);julia> A 6x9 Array{Float64,2}: 1.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 1.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 1.0 1.0 1.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 0.0 0.0 1.0 0.0 0.0 1.0Recall that the constraints of our problem are the linear equality constraints \[M_1 P = \begin{pmatrix} \mu(a_1) \\ \mu(a_2) \\ \mu(a_3) \end{pmatrix} \qquad \text{and} \qquad M_2 P = \begin{pmatrix} \nu(a_1) \\ \nu(a_2) \\ \nu(a_3) \end{pmatrix}\] where \(P\) is the vector formed by the variables \(p_{ij} \geq 0\).
GLPK in action
First load the package, initialize the problem, and give it a name:
import GLPK lp = GLPK.Prob() GLPK.set_prob_name(lp, "kanto")Computing the Kantorovich distance is a minimization problem, declared as follows:
GLPK.set_obj_dir(lp, GLPK.MIN)(
objrefers to objective function, the function to be optimized).Now we use the
GLPK.set_row_bndsfunction to set the hand side vector (the bounds) of the linear constraints and to specify the type of our constraints. Here these are equality constraints and this type is specified byGLPK.FX:GLPK.add_rows(lp, size(A,1)) for i in 1:n GLPK.set_row_bnds(lp, i, GLPK.FX, mu[i], mu[i]) GLPK.set_row_bnds(lp, i+n, GLPK.FX, nu[i], nu[i]) endNow we specify the positivity constraints \(p_{ij} \geq 0\) about the variables \(p_{ij}\) corresponding to the columns of the constraint matrix, and we attach to each column the corresponding coefficient of the objective function, given here by the matrix \(D\):
GLPK.add_cols(lp, size(A,2)) k=0 for i in 1:n, j in 1:n k = k+1 GLPK.set_col_bnds(lp, k, GLPK.LO, 0.0, 0.0) GLPK.set_obj_coef(lp, k, D[i,j]) endWe are ready ! Load the matrix, run the algorithm :
GLPK.load_matrix(lp, sparse(A)) GLPK.simplex(lp);and get the solution:
julia> GLPK.get_obj_val(lp) 0.10714285714285715As we have seen in the previous article, the exact Kantorovich distance between \(\mu\) and \(\nu\) is \(\dfrac{3}{28}\):
julia> 3/28 0.10714285714285714Have you noticed the results are not exactly the same:
julia> GLPK.get_obj_val(lp) - 3/28 1.3877787807814457e-17Thus, the
GLPK.simplexmethod does not achieve the best approximation of \(3/28\) in the 64 bit precision. A better precision is achieved by theGLPK.exactfunction:GLPK.exact(lp);julia> GLPK.get_obj_val(lp) - 3/28 0.0However, unfortunately, it is not possible to get the rational number \(3/28\) with
GLPK.JuMP library
The
JuMPlibrary allows a very convenient syntax. As compared toGLPK, no matrix gymnastics is needed. Watch this concision:using JuMP mu = [1/7, 2/7, 4/7] nu = [1/4, 1/4, 1/2] n = length(mu) m = Model() @defVar(m, p[1:n,1:n] >= 0) @setObjective(m, Min, sum{p[i,j], i in 1:n, j in 1:n; i != j}) for k in 1:n @addConstraint(m, sum(p[k,:]) == mu[k]) @addConstraint(m, sum(p[:,k]) == nu[k]) end solve(m)julia> println("Optimal objective value is:", getObjectiveValue(m)) Optimal objective value is:0.10714285714285715 julia> 3/28 0.10714285714285714As you can see, the best 64-bit precision approximation is not achieved. But it is possible to get it.
JuMPuses a solver, and we have not specified any solver. ThenJuMP(actuallyMathProgBase)will search for an available solver and pick one by default. We can useGLPK.exactby calling theGLPKMathProgInterfacelibrary and specifying the solver in theModelfunction:using GLPKMathProgInterface m = Model(solver=GLPKSolverLP(method=:Exact))Then re-run the rest of the code, and you'll get:
julia> getObjectiveValue(m) 0.10714285714285714RationalSimplex
The
RationalSimplexmodule allows to get the exact Kantorovich distance as a rational number as long as \(\mu\) and \(\nu\) have rational weights. Rational numbers are specified in Julia with a double slash:mu= [1//7, 2//7, 4//7] nu = [1//4, 1//4, 1//2]We will not use matrix gymnastics to construct the constraint matrix \(A\) with rational entries, we define it in Julia with our bare hands below.
include("myfolder/RationalSimplex.jl") using RationalSimplex using Base.Test b = [mu, nu] # 'row bounds' c = [0//1, 1//1, 1//1, 1//1, 0//1, 1//1, 1//1, 1//1, 0//1] # objective coefficients attached to the columns (D matrix in stacked form) A = [1//1 1//1 1//1 0//1 0//1 0//1 0//1 0//1 0//1; 0//1 0//1 0//1 1//1 1//1 1//1 0//1 0//1 0//1; 0//1 0//1 0//1 0//1 0//1 0//1 1//1 1//1 1//1; 1//1 0//1 0//1 1//1 0//1 0//1 1//1 0//1 0//1; 0//1 1//1 0//1 0//1 1//1 0//1 0//1 1//1 0//1; 0//1 0//1 1//1 0//1 0//1 1//1 0//1 0//1 1//1] x = status, simplex(c, :Min, A, b, ['=','=','=','=','=','=']);The
simplexfunction provides the solution of the linear programming problem, that is, the values of \(p_{ij}\) achieving the Kantorovich distance:julia> x 9-element Array{Rational{Int64},1}: 1//7 0//1 0//1 1//28 1//4 0//1 1//14 0//1 1//2The Kantorovich distance is then obtained as the scalar product of
cwith the solution:julia> dot(c,x) 3//28
