pyriemann/utils/median.py
"""Medians of SPD/HPD matrices."""
import warnings
import numpy as np
from .base import sqrtm, invsqrtm, logm, expm
from .distance import distance
from .mean import mean_euclid
from .utils import check_weights
def median_euclid(X, *, tol=10e-6, maxiter=50, init=None, weights=None):
r"""Euclidean geometric median of matrices.
The Euclidean geometric median minimizes the sum of Euclidean distances
:math:`d_E` to all matrices [1]_ [2]_:
.. math::
\arg \min_{\mathbf{M}} \sum_i w_i \ d_E (\mathbf{M}, \mathbf{X}_i)
Geometric median is different from the marginal median provided by NumPy
[3]_.
Parameters
----------
X : ndarray, shape (n_matrices, n, m)
Set of matrices.
tol : float, default=10e-6
The tolerance to stop the iterative algorithm.
maxiter : int, default=50
The maximum number of iterations.
init : None | ndarray, shape (n, m), default=None
A matrix used to initialize the iterative algorithm.
If None, the weighted Euclidean mean is used.
weights : None | ndarray, shape (n_matrices,), default=None
Weights for each matrix. If None, it uses equal weights.
Returns
-------
M : ndarray, shape (n, m)
Euclidean geometric median.
Notes
-----
.. versionadded:: 0.4
References
----------
.. [1] `Sur le point pour lequel la somme des distances de n points donnés
est minimum
<https://www.jstage.jst.go.jp/article/tmj1911/43/0/43_0_355/_pdf>`_
E Weiszfeld. Tohoku Mathematical Journal, 1937, 43, pp. 355-386.
.. [2] `The multivariate L1-median and associated data depth
<https://www.pnas.org/doi/pdf/10.1073/pnas.97.4.1423>`_
Y Vardi and C-H Zhan. Proceedings of the National Academy of Sciences,
2000, vol. 97, no 4, p. 1423-1426
.. [3] https://numpy.org/doc/stable/reference/generated/numpy.median.html
"""
n_matrices, _, _ = X.shape
weights = check_weights(weights, n_matrices)
if init is None:
M = mean_euclid(X, sample_weight=weights)
else:
M = init
for _ in range(maxiter):
dists = distance(X, M, metric='euclid')[:, 0]
is_zero = (dists == 0)
w = weights[~is_zero] / dists[~is_zero]
Mnew = mean_euclid(X[~is_zero], sample_weight=w) # Eq(2.4) of [2]
n_zeros = np.sum(is_zero)
if n_zeros > 0:
R = np.einsum('a,abc->bc', w, X[~is_zero] - M) # Eq(2.7)
r = np.linalg.norm(R, ord='fro')
rinv = 0 if r == 0 else np.mean(weights[is_zero]) / r
Mnew = max(0, 1 - rinv) * Mnew + min(1, rinv) * M # Eq(2.6)
crit = np.linalg.norm(Mnew - M, ord='fro')
M = Mnew
if crit <= tol:
break
else:
warnings.warn("Convergence not reached")
return M
def median_riemann(X, *, tol=10e-6, maxiter=50, init=None, weights=None,
step_size=1):
r"""Affine-invariant Riemannian geometric median of SPD/HPD matrices.
The affine-invariant Riemannian geometric median minimizes the sum of
affine-invariant Riemannian distances :math:`d_R` to all SPD/HPD matrices
[1]_:
.. math::
\arg \min_{\mathbf{M}} \sum_i w_i \ d_R (\mathbf{M}, \mathbf{X}_i)
Parameters
----------
X : ndarray, shape (n_matrices, n, n)
Set of SPD/HPD matrices.
tol : float, default=10e-6
The tolerance to stop the gradient descent.
maxiter : int, default=50
The maximum number of iterations.
init : None | ndarray, shape (n, n), default=None
A SPD/HPD matrix used to initialize the gradient descent.
If None, the weighted Euclidean mean is used.
weights : None | ndarray, shape (n_matrices,), default=None
Weights for each matrix. If None, it uses equal weights.
step_size : float, default=1.0
The step size of the gradient descent, in (0,2].
Returns
-------
M : ndarray, shape (n, n)
Affine-invariant Riemannian geometric median.
Notes
-----
.. versionadded:: 0.4
References
----------
.. [1] `The geometric median on Riemannian manifolds with application to
robust atlas estimation
<https://www.ncbi.nlm.nih.gov/pmc/articles/PMC2735114/>`_
PT. Fletcher, S. Venkatasubramanian S and S. Joshi.
NeuroImage, 2009, 45(1), S143-S152
.. [2] `Riemannian median, geometry of covariance matrices and radar target
detection
<https://ieeexplore.ieee.org/abstract/document/5615027>`_
L Yang, M Arnaudon and F Barbaresco. 7th European Radar Conference,
2010, pp. 415-418
"""
if not 0 < step_size <= 2:
raise ValueError(
f"Value step_size must be included in (0, 2] (Got {step_size})")
n_matrices, _, _ = X.shape
weights = check_weights(weights, n_matrices)
if init is None:
M = mean_euclid(X, sample_weight=weights)
else:
M = init
for _ in range(maxiter):
dists = distance(X, M, metric='riemann')[:, 0]
is_zero = (dists == 0)
w = weights[~is_zero] / dists[~is_zero]
# Eq(11) of [1]
M12, Mm12 = sqrtm(M), invsqrtm(M)
tangvecs = logm(Mm12 @ X[~is_zero] @ Mm12)
J = np.einsum('a,abc->bc', w / np.sum(w), tangvecs)
M = M12 @ expm(step_size * J) @ M12
crit = np.linalg.norm(J, ord='fro')
if crit <= tol:
break
else:
warnings.warn("Convergence not reached")
return M