Skip to content

Check that the matrix of (Sq)Mahalanobis is positive-definite? #174

@devmotion

Description

@devmotion

Currently, upon creation of (Sq)Mahalanobis(Q) it is not checked if Q is actually positive (semi-)definite, and hence in particular symmetric, as shown in the following example:

julia> x = rand(2);

julia> sqmahalanobis(x, -x, Matrix(-I, 2, 2))
-1.0767684441054513

julia> mahalanobis(x, -x, Matrix(-I, (2, 2)))
ERROR: DomainError with -1.0767684441054513:
sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
 [1] throw_complex_domainerror(::Symbol, ::Float64) at ./math.jl:33
 [2] sqrt at ./math.jl:573 [inlined]
 [3] Mahalanobis at /home/david/.julia/dev/Distances/src/mahalanobis.jl:88 [inlined]
 [4] mahalanobis(::Array{Float64,1}, ::Array{Float64,1}, ::Array{Int64,2}) at /home/david/.julia/dev/Distances/src/mahalanobis.jl:91
 [5] top-level scope at REPL[33]:1

However, the implementation of pairwise!(::SqMahalanobis, X, Y) is based on the identity (x-y)'*Q*(x-y) = x'*Q*x + y'*Q*y - x'*Q*y - y'*Q*x = x'*Q*x + y'*Q*y - 2 * x'*Q*y which holds only if Q is symmetric.
This leads to the surprising behaviour

julia> X = rand(2, 3);

julia> Y = rand(2, 4);

julia> Q = rand(2, 2);

julia> pairwise(SqMahalanobis(Q), X, Y; dims=2)
3×4 Array{Float64,2}:
 -0.0807376   0.0288264  -0.0246195  0.00500487
 -0.00275356  0.0291969   0.088852   0.0368224
 -0.0131345   0.0303736   0.0619867  0.0329054

julia> pairwise(SqMahalanobis(Array(Q')), X, Y; dims=2)
3×4 Array{Float64,2}:
 0.0284595   -0.0490699  0.0981837   0.000274926
 0.00392172  -0.154261   0.0615561  -0.0740337
 0.0106349   -0.144796   0.0579041  -0.0660179

julia> map(Iterators.product(eachcol(X), eachcol(Y))) do (x, y)
           z = x - y
           dot(z, Q, z)
       end
3×4 Array{Float64,2}:
 -0.026139     -0.0101218  0.0367821   0.0026399
  0.000584081  -0.0625319  0.0752041  -0.0186056
 -0.0012498    -0.0572112  0.0599454  -0.0165563

julia> map(Iterators.product(eachcol(X), eachcol(Y))) do (x, y)
           z = x - y
           dot(z, Q', z)
       end
3×4 Array{Float64,2}:
 -0.026139     -0.0101218  0.0367821   0.0026399
  0.000584081  -0.0625319  0.0752041  -0.0186056
 -0.0012498    -0.0572112  0.0599454  -0.0165563

although

julia> sqmahalanobis(X[:, 1], Y[:, 1], Q)
-0.026139038969147838

julia> sqmahalanobis(X[:, 1], Y[:, 1], Array(Q'))
-0.026139038969147838

Hence, due to the missing check and the inconsistencies in pairwise!, I am a bit worried that users might not be aware of these assumptions and their impact on pairwise! and might silently obtain incorrect values (see also JuliaGaussianProcesses/KernelFunctions.jl#154 (comment)).

Maybe it would be best to check if the matrix of (Sq)Mahalanobis is positive-definite in the constructor? Or at least mention this assumption prominently in the README? Alternatively, one could neglect the mathematical motivation completely and make the results consistent by not assuming symmetry in the pairwise! computations, i.e., one could just calculate x'*Q*x + y'*Q*y - x'*Q*y - y'*Q*x.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions