def log(self, point, base_point, **kwargs): """Compute the affine-invariant logarithm map. Compute the Riemannian logarithm at point base_point, of point wrt the metric defined in inner_product. This gives a tangent vector at point base_point. Parameters ---------- point : array-like, shape=[..., n, n] Point. base_point : array-like, shape=[..., n, n] Base point. Returns ------- log : array-like, shape=[..., n, n] Riemannian logarithm of point at base_point. """ power_affine = self.power_affine if power_affine == 1: powers = SymmetricMatrices.powerm(base_point, [1. / 2, -1. / 2]) log = self._aux_log(point, powers[0], powers[1]) else: power_point = SymmetricMatrices.powerm(point, power_affine) powers = SymmetricMatrices.powerm( base_point, [power_affine / 2, -power_affine / 2]) log = self._aux_log( power_point, powers[0], powers[1]) log = self.space.inverse_differential_power( power_affine, log, base_point) return log
def log(self, point, base_point): """Compute the affine-invariant logarithm map. Compute the Riemannian logarithm at point base_point, of point wrt the metric defined in inner_product. This gives a tangent vector at point base_point. Parameters ---------- point : array-like, shape=[..., n, n] base_point : array-like, shape=[..., n, n] Returns ------- log : array-like, shape=[..., n, n] """ power_affine = self.power_affine if power_affine == 1: sqrt_base_point = SymmetricMatrices.powerm(base_point, 1. / 2) inv_sqrt_base_point = SymmetricMatrices.powerm(sqrt_base_point, -1) log = self._aux_log(point, sqrt_base_point, inv_sqrt_base_point) else: power_point = SymmetricMatrices.powerm(point, power_affine) power_sqrt_base_point = SymmetricMatrices.powerm( base_point, power_affine / 2) power_inv_sqrt_base_point = gs.linalg.inv(power_sqrt_base_point) log = self._aux_log(power_point, power_sqrt_base_point, power_inv_sqrt_base_point) log = self.space.inverse_differential_power( power_affine, log, base_point) return log
def log(self, point, base_point, **kwargs): """Compute the Bures-Wasserstein logarithm map. Compute the Riemannian logarithm at point base_point, of point wrt the Bures-Wasserstein metric. This gives a tangent vector at point base_point. Parameters ---------- point : array-like, shape=[..., n, n] Point. base_point : array-like, shape=[..., n, n] Base point. Returns ------- log : array-like, shape=[..., n, n] Riemannian logarithm. """ # compute B^1/2(B^-1/2 A B^-1/2)B^-1/2 instead of sqrtm(AB^-1) sqrt_bp, inv_sqrt_bp = SymmetricMatrices.powerm( base_point, [0.5, -0.5]) pdt = SymmetricMatrices.powerm(Matrices.mul(sqrt_bp, point, sqrt_bp), 0.5) sqrt_product = Matrices.mul(sqrt_bp, pdt, inv_sqrt_bp) transp_sqrt_product = Matrices.transpose(sqrt_product) return sqrt_product + transp_sqrt_product - 2 * base_point
def exp(self, tangent_vec, base_point): """Compute the affine-invariant exponential map. Compute the Riemannian exponential at point base_point of tangent vector tangent_vec wrt the metric defined in inner_product. This gives a symmetric positive definite matrix. Parameters ---------- tangent_vec : array-like, shape=[..., n, n] base_point : array-like, shape=[..., n, n] Returns ------- exp : array-like, shape=[..., n, n] """ power_affine = self.power_affine if power_affine == 1: sqrt_base_point = SymmetricMatrices.powerm(base_point, 1. / 2) inv_sqrt_base_point = SymmetricMatrices.powerm(sqrt_base_point, -1) exp = self._aux_exp(tangent_vec, sqrt_base_point, inv_sqrt_base_point) else: modified_tangent_vec = self.space.differential_power( power_affine, tangent_vec, base_point) power_sqrt_base_point = SymmetricMatrices.powerm( base_point, power_affine / 2) power_inv_sqrt_base_point = GeneralLinear.inverse( power_sqrt_base_point) exp = self._aux_exp(modified_tangent_vec, power_sqrt_base_point, power_inv_sqrt_base_point) exp = SymmetricMatrices.powerm(exp, 1 / power_affine) return exp
def christoffels(self, base_point): """Compute the Christoffel symbols. Compute the Christoffel symbols of the Fisher information metric on Beta. Parameters ---------- base_point : array-like, shape=[..., 2] Base point. Returns ------- christoffels : array-like, shape=[..., 2, 2, 2] Christoffel symbols. """ def coefficients(param_a, param_b): metric_det = 2 * self.metric_det(param_a, param_b) poly_2_ab = gs.polygamma(2, param_a + param_b) poly_1_ab = gs.polygamma(1, param_a + param_b) poly_1_b = gs.polygamma(1, param_b) c1 = (gs.polygamma(2, param_a) * (poly_1_b - poly_1_ab) - poly_1_b * poly_2_ab) / metric_det c2 = - poly_1_b * poly_2_ab / metric_det c3 = (gs.polygamma(2, param_b) * poly_1_ab - poly_1_b * poly_2_ab) / metric_det return c1, c2, c3 point_a, point_b = base_point[..., 0], base_point[..., 1] c4, c5, c6 = coefficients(point_b, point_a) vector_0 = gs.stack(coefficients(point_a, point_b), axis=-1) vector_1 = gs.stack([c6, c5, c4], axis=-1) gamma_0 = SymmetricMatrices.from_vector(vector_0) gamma_1 = SymmetricMatrices.from_vector(vector_1) return gs.stack([gamma_0, gamma_1], axis=-3)
def test_christoffels(self): """Test Christoffel symbols in dimension 2. Check the Christoffel symbols in dimension 2. """ gs.random.seed(123) dirichlet2 = DirichletDistributions(2) points = dirichlet2.random_point(self.n_points) result = dirichlet2.metric.christoffels(points) def coefficients(param_a, param_b): """Christoffel coefficients for the beta distributions.""" poly1a = gs.polygamma(1, param_a) poly2a = gs.polygamma(2, param_a) poly1b = gs.polygamma(1, param_b) poly2b = gs.polygamma(2, param_b) poly1ab = gs.polygamma(1, param_a + param_b) poly2ab = gs.polygamma(2, param_a + param_b) metric_det = 2 * (poly1a * poly1b - poly1ab * (poly1a + poly1b)) c1 = (poly2a * (poly1b - poly1ab) - poly1b * poly2ab) / metric_det c2 = -poly1b * poly2ab / metric_det c3 = (poly2b * poly1ab - poly1b * poly2ab) / metric_det return c1, c2, c3 param_a, param_b = points[:, 0], points[:, 1] c1, c2, c3 = coefficients(param_a, param_b) c4, c5, c6 = coefficients(param_b, param_a) vector_0 = gs.stack([c1, c2, c3], axis=-1) vector_1 = gs.stack([c6, c5, c4], axis=-1) gamma_0 = SymmetricMatrices.from_vector(vector_0) gamma_1 = SymmetricMatrices.from_vector(vector_1) expected = gs.stack([gamma_0, gamma_1], axis=-3) self.assertAllClose(result, expected)
def christoffels_dim_2_test_data(self): def coefficients(param_a, param_b): """Christoffel coefficients for the beta distributions.""" poly1a = gs.polygamma(1, param_a) poly2a = gs.polygamma(2, param_a) poly1b = gs.polygamma(1, param_b) poly2b = gs.polygamma(2, param_b) poly1ab = gs.polygamma(1, param_a + param_b) poly2ab = gs.polygamma(2, param_a + param_b) metric_det = 2 * (poly1a * poly1b - poly1ab * (poly1a + poly1b)) c1 = (poly2a * (poly1b - poly1ab) - poly1b * poly2ab) / metric_det c2 = -poly1b * poly2ab / metric_det c3 = (poly2b * poly1ab - poly1b * poly2ab) / metric_det return c1, c2, c3 gs.random.seed(123) n_points = 3 points = self.space(2).random_point(n_points) param_a, param_b = points[:, 0], points[:, 1] c1, c2, c3 = coefficients(param_a, param_b) c4, c5, c6 = coefficients(param_b, param_a) vector_0 = gs.stack([c1, c2, c3], axis=-1) vector_1 = gs.stack([c6, c5, c4], axis=-1) gamma_0 = SymmetricMatrices.from_vector(vector_0) gamma_1 = SymmetricMatrices.from_vector(vector_1) random_data = [ dict(point=points, expected=gs.stack([gamma_0, gamma_1], axis=-3)) ] return self.generate_tests([], random_data)
def exp(self, tangent_vec, base_point, **kwargs): """Compute the Euclidean exponential map. Compute the Euclidean exponential at point base_point of tangent vector tangent_vec. This gives a symmetric positive definite matrix. Parameters ---------- tangent_vec : array-like, shape=[..., n, n] Tangent vector at base point. base_point : array-like, shape=[..., n, n] Base point. Returns ------- exp : array-like, shape=[..., n, n] Euclidean exponential. """ power_euclidean = self.power_euclidean if power_euclidean == 1: exp = tangent_vec + base_point else: exp = SymmetricMatrices.powerm( SymmetricMatrices.powerm(base_point, power_euclidean) + SPDMatrices.differential_power(power_euclidean, tangent_vec, base_point), 1 / power_euclidean, ) return exp
def log(self, point, base_point, **kwargs): """Compute the Euclidean logarithm map. Compute the Euclidean logarithm at point base_point, of point. This gives a tangent vector at point base_point. Parameters ---------- point : array-like, shape=[..., n, n] Point. base_point : array-like, shape=[..., n, n] Base point. Returns ------- log : array-like, shape=[..., n, n] Euclidean logarithm. """ power_euclidean = self.power_euclidean if power_euclidean == 1: log = point - base_point else: log = SPDMatrices.inverse_differential_power( power_euclidean, SymmetricMatrices.powerm(point, power_euclidean) - SymmetricMatrices.powerm(base_point, power_euclidean), base_point, ) return log
def setUp(self): """Set up the test.""" warnings.simplefilter('ignore', category=ImportWarning) gs.random.seed(1234) self.n = 3 self.space = SymmetricMatrices(self.n)
def __init__(self, n, k, **kwargs): super(RankKPSDMatrices, self).__init__( **kwargs, dim=int(k * n - k * (k + 1) / 2), shape=(n, n), ) self.n = n self.rank = k self.sym = SymmetricMatrices(self.n)
def test_expm(self): """Test of expm method.""" sym_n = SymmetricMatrices(self.n) v = gs.array([[0., 1., 0.], [1., 0., 0.], [0., 0., 1.]]) result = sym_n.expm(v) c = math.cosh(1) s = math.sinh(1) e = math.exp(1) expected = gs.array([[c, s, 0.], [s, c, 0.], [0., 0., e]]) self.assertAllClose(result, expected)
def setUp(self): r"""Set up the test.""" warnings.simplefilter("ignore", category=ImportWarning) gs.random.seed(1234) self.n = 3 self.k = 2 self.space = PSDMatrices(self.n, self.k) self.sym = SymmetricMatrices(self.n)
def __init__(self, n, k, **kwargs): kwargs.setdefault("metric", PSDMetricBuresWasserstein(n, k)) super(RankKPSDMatrices, self).__init__( **kwargs, dim=int(k * n - k * (k + 1) / 2), shape=(n, n), ) self.n = n self.rank = k self.sym = SymmetricMatrices(self.n)
def test_powerm(self): """Test of powerm method.""" sym_n = SymmetricMatrices(self.n) expected = gs.array( [[[1, 1. / 4., 0.], [1. / 4, 2., 0.], [0., 0., 1.]]]) expected = gs.cast(expected, gs.float64) power = gs.array(1. / 2) power = gs.cast(power, gs.float64) result = sym_n.powerm(expected, power) result = gs.matmul(result, gs.transpose(result, (0, 2, 1))) self.assertAllClose(result, expected)
def test_powerm(self): """Test of powerm method.""" sym_n = SymmetricMatrices(self.n) expected = gs.array( [[[1, 1.0 / 4.0, 0.0], [1.0 / 4, 2.0, 0.0], [0.0, 0.0, 1.0]]] ) power = gs.array(1.0 / 2.0) result = sym_n.powerm(expected, power) result = gs.matmul(result, gs.transpose(result, (0, 2, 1))) self.assertAllClose(result, expected)
def inverse_transform(self, X): """Low-dimensional reconstruction of X. The reconstruction will match X_original whose transform would be X if `n_components=min(n_samples, n_features)`. Parameters ---------- X : array-like, shape=[..., n_components] New data, where n_samples is the number of samples and n_components is the number of components. Returns ------- X_original : array-like, shape=[..., n_features] Original data. """ scores = self.mean_ + gs.matmul(X, self.components_) if self.point_type == 'matrix': if Matrices.is_symmetric(self.base_point_fit).all(): scores = SymmetricMatrices( self.base_point_fit.shape[-1]).from_vector(scores) else: dim = self.base_point_fit.shape[-1] scores = gs.reshape(scores, (len(scores), dim, dim)) return self.metric.exp(scores, self.base_point_fit)
def transform(self, X, y=None): """Project X on the principal components. Parameters ---------- X : array-like, shape=[..., n_features] Data, where n_samples is the number of samples and n_features is the number of features. y : Ignored (Compliance with scikit-learn interface) Returns ------- X_new : array-like, shape=[..., n_components] Projected data. """ tangent_vecs = self.metric.log(X, base_point=self.base_point_fit) if self.point_type == 'matrix': if Matrices.is_symmetric(tangent_vecs).all(): X = SymmetricMatrices.to_vector(tangent_vecs) else: X = gs.reshape(tangent_vecs, (len(X), -1)) else: X = tangent_vecs X = X - self.mean_ X_transformed = gs.matmul(X, gs.transpose(self.components_)) return X_transformed
def metric_matrix(self, base_point=None): """Compute inner-product matrix at the tangent space at base point. Parameters ---------- base_point : array-like, shape=[..., 2] Base point. Returns ------- mat : array-like, shape=[..., 2, 2] Inner-product matrix. """ if base_point is None: raise ValueError('A base point must be given to compute the ' 'metric matrix') param_a = base_point[..., 0] param_b = base_point[..., 1] polygamma_ab = gs.polygamma(1, param_a + param_b) polygamma_a = gs.polygamma(1, param_a) polygamma_b = gs.polygamma(1, param_b) vector = gs.stack( [polygamma_a - polygamma_ab, - polygamma_ab, polygamma_b - polygamma_ab], axis=-1) return SymmetricMatrices.from_vector(vector)
def random_uniform(self, n_samples=1): r"""Sample on St(n,p) from the uniform distribution. If :math:`Z(p,n) \sim N(0,1)`, then :math:`St(n,p) \sim U`, according to Haar measure: :math:`St(n,p) := Z(Z^TZ)^{-1/2}`. Parameters ---------- n_samples : int Number of samples. Optional, default: 1. Returns ------- samples : array-like, shape=[..., n, p] Samples on the Stiefel manifold. """ n, p = self.n, self.p size = (n_samples, n, p) if n_samples != 1 else (n, p) std_normal = gs.random.normal(size=size) std_normal_transpose = Matrices.transpose(std_normal) aux = Matrices.mul(std_normal_transpose, std_normal) inv_sqrt_aux = SymmetricMatrices.powerm(aux, -1.0 / 2) samples = Matrices.mul(std_normal, inv_sqrt_aux) return samples
def test_expm(self): """Test of expm method.""" sym_n = SymmetricMatrices(self.n) v = gs.array([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]) result = sym_n.expm(v) c = math.cosh(1) s = math.sinh(1) e = math.exp(1) expected = gs.array([[c, s, 0.0], [s, c, 0.0], [0.0, 0.0, e]]) four_dim_v = gs.broadcast_to(v, (2, 2) + v.shape) four_dim_expected = gs.broadcast_to(expected, (2, 2) + expected.shape) four_dim_result = sym_n.expm(four_dim_v) self.assertAllClose(result, expected) self.assertAllClose(four_dim_result, four_dim_expected)
def exp_domain(tangent_vec, base_point): """Compute the domain of the Euclidean exponential map. Compute the real interval of time where the Euclidean geodesic starting at point `base_point` in direction `tangent_vec` is defined. Parameters ---------- tangent_vec : array-like, shape=[..., n, n] Tangent vector at base point. base_point : array-like, shape=[..., n, n] Base point. Returns ------- exp_domain : array-like, shape=[..., 2] Interval of time where the geodesic is defined. """ invsqrt_base_point = SymmetricMatrices.powerm(base_point, -.5) reduced_vec = gs.matmul(invsqrt_base_point, tangent_vec) reduced_vec = gs.matmul(reduced_vec, invsqrt_base_point) eigvals = gs.linalg.eigvalsh(reduced_vec) min_eig = gs.amin(eigvals, axis=1) max_eig = gs.amax(eigvals, axis=1) inf_value = gs.where( max_eig <= 0., gs.array(-math.inf), - 1. / max_eig) inf_value = gs.to_ndarray(inf_value, to_ndim=2) sup_value = gs.where( min_eig >= 0., gs.array(-math.inf), - 1. / min_eig) sup_value = gs.to_ndarray(sup_value, to_ndim=2) domain = gs.concatenate((inf_value, sup_value), axis=1) return domain
def transform(self, X, y=None): """Project X on the principal components. Parameters ---------- X : array-like, shape=[n_samples, n_features] Data, where n_samples is the number of samples and n_features is the number of features. y : Ignored (Compliance with scikit-learn interface) Returns ------- X_new : array-like, shape=[n_samples, n_components] """ tangent_vecs = self.metric.log(X, base_point=self.base_point_fit) if self.point_type == 'matrix': if Matrices.is_symmetric(tangent_vecs).all(): X = SymmetricMatrices.vector_from_symmetric_matrix( tangent_vecs) else: X = gs.reshape(tangent_vecs, (len(X), -1)) else: X = tangent_vecs return super(TangentPCA, self).transform(X)
def exp(self, tangent_vec, base_point, **kwargs): """Compute the Log-Euclidean exponential map. Compute the Riemannian exponential at point base_point of tangent vector tangent_vec wrt the Log-Euclidean metric. This gives a symmetric positive definite matrix. Parameters ---------- tangent_vec : array-like, shape=[..., n, n] Tangent vector at base point. base_point : array-like, shape=[..., n, n] Base point. Returns ------- exp : array-like, shape=[..., n, n] Riemannian exponential. """ log_base_point = SPDMatrices.logm(base_point) dlog_tangent_vec = SPDMatrices.differential_log( tangent_vec, base_point) exp = SymmetricMatrices.expm(log_base_point + dlog_tangent_vec) return exp
def parallel_transport(self, tangent_vec, base_point, direction=None, end_point=None): r"""Parallel transport of a tangent vector. Closed-form solution for the parallel transport of a tangent vector along the geodesic between two points `base_point` and `end_point` or alternatively defined by :math:`t \mapsto exp_{(base\_point)}( t*direction)`. Denoting `tangent_vec_a` by `S`, `base_point` by `A`, and `end_point` by `B` or `B = Exp_A(tangent_vec_b)` and :math:`E = (BA^{- 1})^{( 1 / 2)}`. Then the parallel transport to `B` is: .. math:: S' = ESE^T Parameters ---------- tangent_vec : array-like, shape=[..., n, n] Tangent vector at base point to be transported. base_point : array-like, shape=[..., n, n] Point on the manifold of SPD matrices. Point to transport from direction : array-like, shape=[..., n, n] Tangent vector at base point, initial speed of the geodesic along which the parallel transport is computed. Unused if `end_point` is given. Optional, default: None. end_point : array-like, shape=[..., n, n] Point on the manifold of SPD matrices. Point to transport to. Optional, default: None. Returns ------- transported_tangent_vec: array-like, shape=[..., n, n] Transported tangent vector at exp_(base_point)(tangent_vec_b). """ if end_point is None: end_point = self.exp(direction, base_point) # compute B^1/2(B^-1/2 A B^-1/2)B^-1/2 instead of sqrtm(AB^-1) sqrt_bp, inv_sqrt_bp = SymmetricMatrices.powerm( base_point, [1.0 / 2, -1.0 / 2]) pdt = SymmetricMatrices.powerm( Matrices.mul(inv_sqrt_bp, end_point, inv_sqrt_bp), 1.0 / 2) congruence_mat = Matrices.mul(sqrt_bp, pdt, inv_sqrt_bp) return Matrices.congruent(tangent_vec, congruence_mat)
def __init__(self, n, **kwargs): super(SPDMatrices, self).__init__( dim=int(n * (n + 1) / 2), metric=SPDMetricAffine(n), ambient_space=SymmetricMatrices(n), **kwargs ) self.n = n
def vertical_projection_tangent_submersion_test_data(self): random_data = [] for n in self.n_list: bundle = CorrelationMatricesBundle(n) mat = bundle.random_point(2) vec = SymmetricMatrices(n).random_point(2) random_data.append(dict(n=n, vec=vec, mat=mat)) return self.generate_tests([], random_data)
def unary_op_like_np_test_data(self): smoke_data = [ dict(func_name="trace", a=rand(2, 2)), dict(func_name="trace", a=rand(3, 3)), dict(func_name="linalg.cholesky", a=SPDMatrices(3).random_point()), dict(func_name="linalg.eigvalsh", a=SymmetricMatrices(3).random_point()), ] return self.generate_tests(smoke_data)
def test_basis(self): """Test of belongs method.""" sym_n = SymmetricMatrices(2) mat_sym_1 = gs.array([[1., 0.], [0, 0]]) mat_sym_2 = gs.array([[0, 1.], [1., 0]]) mat_sym_3 = gs.array([[0, 0.], [0, 1.]]) expected = gs.stack([mat_sym_1, mat_sym_2, mat_sym_3]) result = sym_n.basis self.assertAllClose(result, expected)
def unary_op_vec_test_data(self): smoke_data = [ dict(func_name="trace", a=rand(3, 3)), dict(func_name="linalg.cholesky", a=SPDMatrices(3).random_point()), dict(func_name="linalg.eigvalsh", a=SymmetricMatrices(3).random_point()), ] smoke_data += self._logm_expm_data() smoke_data += self._logm_expm_data("linalg.expm") return self.generate_tests(smoke_data)