28

I am trying to figure out the fastest method to find the determinant of sparse symmetric and real matrices in python. using scipy sparse module but really surprised that there is no determinant function. I am aware I could use LU factorization to compute determinant but don't see a easy way to do it because the return of scipy.sparse.linalg.splu is an object and instantiating a dense L and U matrix is not worth it - I may as well do sp.linalg.det(A.todense()) where A is my scipy sparse matrix.

I am also a bit surprised why others have not faced the problem of efficient determinant computation within scipy. How would one use splu to compute determinant?

I looked into pySparse and scikits.sparse.chlmod. The latter is not practical right now for me - needs package installations and also not sure sure how fast the code is before I go into all the trouble. Any solutions? Thanks in advance.

Saullo G. P. Castro
  • 56,802
  • 26
  • 179
  • 234
swagatam
  • 283
  • 3
  • 4
  • A lot of how you would want to do this depends on the data in your matrix. If you just want to know whether or not the matrix is singular than something like `eigs(A, 1, which="SM", return_eigenvectors=False)` or `svds(A, 1, which="SM", return_singular_vectors=False)` may be a good indicator of whether or not your matrix is singular. I'm reluctant to say that it will always work though... – IanH Oct 02 '13 at 01:17
  • Firstly, eigs can only return < rank(A)-1 eigenvalues, so det cannot be computed. My matrices are not singular. But more importantly, both eigs and svds are much slower--does a lot more work than what is needed to simply compute determinant. This is true even with return eignevectors set False. For a 100x100 matrix of tri-diagonal form, it is 66 times slower compared to the conversion to todense() and computing determinant using scipy linalg (not numpy). So not what I am looking for. – swagatam Oct 02 '13 at 03:44
  • Yes, I suggested that specifically for the case that you were trying to see if the array was singular. If one of the eigenvalues is 0, the determinant should be zero as well. This only works if the data isn't too wild though, since you would want to see whether or not you are within a some tolerance of 0. – IanH Oct 02 '13 at 05:36
  • 2
    you said you looked into `pySparce`, it has a `superlu` interface, which implements LU-factorisation via partial pivoting, why does it not feet your needs? – alko Oct 26 '13 at 21:58
  • 1
    Because, as I mentioned in my question above, instantiating a L and U matrix is not a practical way to solve the problem, L and U will be dense. The whole point of sparse matrix methods is not to do that. – swagatam Nov 23 '13 at 17:13

4 Answers4

8

Here are some references I provided as part of an answer here. I think they address the actual problem you are trying to solve:

  • notes for an implementation in the Shogun library
  • Erlend Aune, Daniel P. Simpson: Parameter estimation in high dimensional Gaussian distributions, particularly section 2.1 (arxiv:1105.5256)
  • Ilse C.F. Ipsen, Dean J. Lee: Determinant Approximations (arxiv:1105.0437)
  • Arnold Reusken: Approximation of the determinant of large sparse symmetric positive definite matrices (arxiv:hep-lat/0008007)

Quoting from the Shogun notes:

The usual technique for computing the log-determinant term in the likelihood expression relies on Cholesky factorization of the matrix, i.e. Σ=LLT, (L is the lower triangular Cholesky factor) and then using the diagonal entries of the factor to compute log(det(Σ))=2∑ni=1log(Lii). However, for sparse matrices, as covariance matrices usually are, the Cholesky factors often suffer from fill-in phenomena - they turn out to be not so sparse themselves. Therefore, for large dimensions this technique becomes infeasible because of a massive memory requirement for storing all these irrelevant non-diagonal co-efficients of the factor. While ordering techniques have been developed to permute the rows and columns beforehand in order to reduce fill-in, e.g. approximate minimum degree (AMD) reordering, these techniques depend largely on the sparsity pattern and therefore not guaranteed to give better result.

Recent research shows that using a number of techniques from complex analysis, numerical linear algebra and greedy graph coloring, we can, however, approximate the log-determinant up to an arbitrary precision [Aune et. al., 2012]. The main trick lies within the observation that we can write log(det(Σ)) as trace(log(Σ)), where log(Σ) is the matrix-logarithm.

Community
  • 1
  • 1
Daniel Mahler
  • 7,653
  • 5
  • 51
  • 90
7

The "standard" way to solve this problem is with a cholesky decomposition, but if you're not up to using any new compiled code, then you're out of luck. The best sparse cholesky implementation is Tim Davis's CHOLMOD, which is licensed under the LGPL and thus not available in scipy proper (scipy is BSD).

Saullo G. P. Castro
  • 56,802
  • 26
  • 179
  • 234
Robert T. McGibbon
  • 5,075
  • 3
  • 37
  • 45
  • It's very simple, you just multiply the diagonal entries on both the L and the U matrices. But if the scipy.sparse.linalg.splu function returns dense L and U matrices, then that doesn't satisfy the OPs memory demans. – Robert T. McGibbon Oct 27 '13 at 11:02
  • 2
    I am aware that real symmetric matrix determinant is computed using Cholesky---in fact, I did mention chlmod in from scikit.sparse in my post above. The point again is, not to instantiate the dense upper and lower triangular matrices. I think I am repeating myself. See my other comments please. – swagatam Nov 23 '13 at 17:17
7

You can use scipy.sparse.linalg.splu to obtain sparse matrices for the lower (L) and upper (U) triangular matrices of an M=LU decomposition:

from scipy.sparse.linalg import splu

lu = splu(M)

The determinant det(M) can be then represented as:

det(M) = det(LU) = det(L)det(U)

The determinant of triangular matrices is just the product of the diagonal terms:

diagL = lu.L.diagonal()
diagU = lu.U.diagonal()
d = diagL.prod()*diagU.prod()

However, for large matrices underflow or overflow commonly occurs, which can be avoided by working with the logarithms.

diagL = diagL.astype(np.complex128)
diagU = diagU.astype(np.complex128)
logdet = np.log(diagL).sum() + np.log(diagU).sum()

Note that I invoke complex arithmetic to account for negative numbers that might appear in the diagonals. Now, from logdet you can recover the determinant:

det = np.exp(logdet) # usually underflows/overflows for large matrices

whereas the sign of the determinant can be calculated directly from diagL and diagU (important for example when implementing Crisfield's arc-length method):

sign = swap_sign*np.sign(diagL).prod()*np.sign(diagU).prod()

where swap_sign is a term to consider the number of permutations in the LU decomposition. Thanks to @Luiz Felippe Rodrigues, it can be calculated:

swap_sign = (-1)**minimumSwaps(lu.perm_r)

def minimumSwaps(arr): 
    """
    Minimum number of swaps needed to order a
    permutation array
    """
    # from https://www.thepoorcoder.com/hackerrank-minimum-swaps-2-solution/
    a = dict(enumerate(arr))
    b = {v:k for k,v in a.items()}
    count = 0
    for i in a:
        x = a[i]
        if x!=i:
            y = b[i]
            a[y] = x
            b[x] = y
            count+=1
    return count
Galactic Ketchup
  • 408
  • 4
  • 12
Saullo G. P. Castro
  • 56,802
  • 26
  • 179
  • 234
  • Which can be counted to as, for example: `number_of_permutations = ((lu.perm_r != np.arange(lu.perm_r.size)).sum() + (lu.perm_c != np.arange(lu.perm_c.size)).sum())//2` – LuizFelippe Sep 30 '20 at 06:31
  • @LuizFelippeRodrigues good question. Perhaps his must be included in the proposed algorithm. Did you compare the result with and without considering the number of permutations of SPLU? – Saullo G. P. Castro Sep 30 '20 at 06:57
  • It does make a difference in my tests: unfortunately, the last line in your suggestion does not seem to lead to the correct sign for me. However, my previous suggestion of how to compute the number of permutations from `lu.perm_r`/`lu.perm_c` is wrong. I am trying to come up with a solution. – LuizFelippe Sep 30 '20 at 09:04
  • @LuizFelippeRodrigues any progress on this? – Saullo G. P. Castro Oct 05 '20 at 21:22
  • 1
    I get correct results in my tests if I multiply the sign given by your code by the (-1) to the number of row permutations used by `splu`. I've implemented this in [this gist](https://gist.github.com/luizfelippesr/5965a536d202b913beda9878a2f8ef3e). I still wonder, however, whether there is a more efficient solution.. – LuizFelippe Oct 06 '20 at 06:22
  • @LuizFelippeRodrigues I incorporated your solution for the number of permutations into the answer. Thank you again – Saullo G. P. Castro Nov 24 '20 at 07:14
1

Things start to go wrong with the determinant of sparse tridiagonal (-1 2 -1) around N=1e6 using both SuperLU and CHOLMOD...

The determinant should be N+1.

It's probably propagation of error when calculating the product of the U diagonal:

from scipy.sparse import diags
from scipy.sparse.linalg import splu
from sksparse.cholmod import cholesky
from math import exp

n=int(5e6)
K = diags([-1.],-1,shape=(n,n)) + diags([2.],shape=(n,n)) + diags([-1.],1,shape=(n,n))
lu = splu(K.tocsc())
diagL = lu.L.diagonal()
diagU = lu.U.diagonal()
det=diagL.prod()*diagU.prod()
print(det)

factor = cholesky(K.tocsc())
ld = factor.logdet()
print(exp(ld))

Output:

4999993.625461911

4999993.625461119

Even if U is 10-13 digit accurate, this might be expected:

n=int(5e6)
print(n*diags([1-0.00000000000025],0,shape=(n,n)).diagonal().prod())

4999993.749444371

jtlz2
  • 7,700
  • 9
  • 64
  • 114