Exemple #1
0
def test_pos_semidef_inv(ndim, dtype, n, deficient, reduce_rank, psdef, func):
    """Test positive semidefinite matrix inverses."""
    if LooseVersion(np.__version__) >= LooseVersion('1.19'):
        svd = np.linalg.svd
    else:
        from mne.fixes import svd
    # make n-dimensional matrix
    n_extra = 2  # how many we add along the other dims
    rng = np.random.RandomState(73)
    shape = (n_extra, ) * (ndim - 2) + (n, n)
    mat = rng.randn(*shape) + 1j * rng.randn(*shape)
    proj = np.eye(n)
    if deficient:
        vec = np.ones(n) / np.sqrt(n)
        proj -= np.outer(vec, vec)
    with pytest.warns(None):  # intentionally discard imag
        mat = mat.astype(dtype)
    # now make it conjugate symmetric or positive semi-definite
    if psdef:
        mat = np.matmul(mat, mat.swapaxes(-2, -1).conj())
    else:
        mat += mat.swapaxes(-2, -1).conj()
    assert_allclose(mat, mat.swapaxes(-2, -1).conj(), atol=1e-6)
    s = svd(mat, hermitian=True)[1]
    assert (s >= 0).all()
    # make it rank deficient (maybe)
    if deficient:
        mat = np.matmul(np.matmul(proj, mat), proj)
    # if the dtype is complex, the conjugate transpose != transpose
    kwargs = dict(atol=1e-10, rtol=1e-10)
    orig_eq_t = np.allclose(mat, mat.swapaxes(-2, -1), **kwargs)
    t_eq_ct = np.allclose(mat.swapaxes(-2, -1),
                          mat.conj().swapaxes(-2, -1), **kwargs)
    if np.iscomplexobj(mat):
        assert not orig_eq_t
        assert not t_eq_ct
    else:
        assert t_eq_ct
        assert orig_eq_t
    assert mat.shape == shape
    # ensure pos-semidef
    s = np.linalg.svd(mat, compute_uv=False)
    assert s.shape == shape[:-1]
    rank = (s > s[..., :1] * 1e-12).sum(-1)
    want_rank = n - deficient
    assert_array_equal(rank, want_rank)
    # assert equiv with NumPy
    mat_pinv = np.linalg.pinv(mat)
    if func is _sym_mat_pow:
        if not psdef:
            with pytest.raises(ValueError, match='not positive semi-'):
                func(mat, -1)
            return
        mat_symv = func(mat, -1, reduce_rank=reduce_rank)
        mat_sqrt = func(mat, 0.5)
        if ndim == 2:
            mat_sqrt_scipy = linalg.sqrtm(mat)
            assert_allclose(mat_sqrt, mat_sqrt_scipy, atol=1e-6)
        mat_2 = np.matmul(mat_sqrt, mat_sqrt)
        assert_allclose(mat, mat_2, atol=1e-6)
        mat_symv_2 = func(mat, -0.5, reduce_rank=reduce_rank)
        mat_symv_2 = np.matmul(mat_symv_2, mat_symv_2)
        assert_allclose(mat_symv_2, mat_symv, atol=1e-6)
    else:
        assert func is _reg_pinv
        mat_symv, _, _ = func(mat, rank=None)
    assert_allclose(mat_pinv, mat_symv, **kwargs)
    want = np.dot(proj, np.eye(n))
    if deficient:
        want -= want.mean(axis=0)
    for _ in range(ndim - 2):
        want = np.repeat(want[np.newaxis], n_extra, axis=0)
    assert_allclose(np.matmul(mat_symv, mat), want, **kwargs)
    assert_allclose(np.matmul(mat, mat_symv), want, **kwargs)
Exemple #2
0
def _reg_pinv(x, reg=0, rank='full', rcond=1e-15):
    """Compute a regularized pseudoinverse of Hermitian matrices.

    Regularization is performed by adding a constant value to each diagonal
    element of the matrix before inversion. This is known as "diagonal
    loading". The loading factor is computed as ``reg * np.trace(x) / len(x)``.

    The pseudo-inverse is computed through SVD decomposition and inverting the
    singular values. When the matrix is rank deficient, some singular values
    will be close to zero and will not be used during the inversion. The number
    of singular values to use can either be manually specified or automatically
    estimated.

    Parameters
    ----------
    x : ndarray, shape (..., n, n)
        Square, Hermitian matrices to invert.
    reg : float
        Regularization parameter. Defaults to 0.
    rank : int | None | 'full'
        This controls the effective rank of the covariance matrix when
        computing the inverse. The rank can be set explicitly by specifying an
        integer value. If ``None``, the rank will be automatically estimated.
        Since applying regularization will always make the covariance matrix
        full rank, the rank is estimated before regularization in this case. If
        'full', the rank will be estimated after regularization and hence
        will mean using the full rank, unless ``reg=0`` is used.
        Defaults to 'full'.
    rcond : float | 'auto'
        Cutoff for detecting small singular values when attempting to estimate
        the rank of the matrix (``rank='auto'``). Singular values smaller than
        the cutoff are set to zero. When set to 'auto', a cutoff based on
        floating point precision will be used. Defaults to 1e-15.

    Returns
    -------
    x_inv : ndarray, shape (..., n, n)
        The inverted matrix.
    loading_factor : float
        Value added to the diagonal of the matrix during regularization.
    rank : int
        If ``rank`` was set to an integer value, this value is returned,
        else the estimated rank of the matrix, before regularization, is
        returned.
    """
    from ..rank import _estimate_rank_from_s
    if rank is not None and rank != 'full':
        rank = int(operator.index(rank))
    if x.ndim < 2 or x.shape[-2] != x.shape[-1]:
        raise ValueError('Input matrix must be square.')
    if not np.allclose(x, x.conj().swapaxes(-2, -1)):
        raise ValueError('Input matrix must be Hermitian (symmetric)')
    assert x.ndim >= 2 and x.shape[-2] == x.shape[-1]
    n = x.shape[-1]

    # Decompose the matrix, not necessarily positive semidefinite
    from mne.fixes import svd
    U, s, Vh = svd(x, hermitian=True)

    # Estimate the rank before regularization
    tol = 'auto' if rcond == 'auto' else rcond * s[..., :1]
    rank_before = _estimate_rank_from_s(s, tol)

    # Decompose the matrix again after regularization
    loading_factor = reg * np.mean(s, axis=-1)
    if reg:
        U, s, Vh = svd(x +
                       loading_factor[..., np.newaxis, np.newaxis] * np.eye(n),
                       hermitian=True)

    # Estimate the rank after regularization
    tol = 'auto' if rcond == 'auto' else rcond * s[..., :1]
    rank_after = _estimate_rank_from_s(s, tol)

    # Warn the user if both all parameters were kept at their defaults and the
    # matrix is rank deficient.
    if (rank_after < n).any() and reg == 0 and \
            rank == 'full' and rcond == 1e-15:
        warn('Covariance matrix is rank-deficient and no regularization is '
             'done.')
    elif isinstance(rank, int) and rank > n:
        raise ValueError('Invalid value for the rank parameter (%d) given '
                         'the shape of the input matrix (%d x %d).' %
                         (rank, x.shape[0], x.shape[1]))

    # Pick the requested number of singular values
    mask = np.arange(s.shape[-1]).reshape((1, ) * (x.ndim - 2) + (-1, ))
    if rank is None:
        cmp = ret = rank_before
    elif rank == 'full':
        cmp = rank_after
        ret = rank_before
    else:
        cmp = ret = rank
    mask = mask < np.asarray(cmp)[..., np.newaxis]
    mask &= s > 0

    # Invert only non-zero singular values
    s_inv = np.zeros(s.shape)
    s_inv[mask] = 1. / s[mask]

    # Compute the pseudo inverse
    x_inv = np.matmul(U * s_inv[..., np.newaxis, :], Vh)

    return x_inv, loading_factor, ret