Ejemplo n.º 1
0
    def belongs(self, point, tolerance=TOLERANCE):
        """Test if a point belongs to the hypersphere.

        This tests whether the point's squared norm in Euclidean space is 1.

        Parameters
        ----------
        point : array-like, shape=[n_samples, dim + 1]
            Points in Euclidean space.
        tolerance : float, optional
            Tolerance at which to evaluate norm == 1 (default: TOLERANCE).

        Returns
        -------
        belongs : array-like, shape=[n_samples, 1]
            Array of booleans evaluating if each point belongs to
            the hypersphere.
        """
        point_dim = gs.shape(point)[-1]
        if point_dim != self.dim + 1:
            if point_dim is self.dim:
                logging.warning('Use the extrinsic coordinates to '
                                'represent points on the hypersphere.')
            belongs = False
            if gs.ndim(point) == 2:
                belongs = gs.tile([belongs], (point.shape[0], ))
            return belongs
        sq_norm = self.embedding_metric.squared_norm(point)
        diff = gs.abs(sq_norm - 1)
        return gs.less_equal(diff, tolerance)
Ejemplo n.º 2
0
def _default_gradient_descent(points, metric, weights, max_iter, point_type,
                              epsilon, verbose):
    """Perform default gradient descent."""
    if point_type == 'vector':
        points = gs.to_ndarray(points, to_ndim=2)
        einsum_str = 'n,nj->j'
    if point_type == 'matrix':
        points = gs.to_ndarray(points, to_ndim=3)
        einsum_str = 'n,nij->ij'
    n_points = gs.shape(points)[0]

    if weights is None:
        weights = gs.ones((n_points, ))

    mean = points[0]

    if n_points == 1:
        return mean

    sum_weights = gs.sum(weights)
    sq_dists_between_iterates = []
    iteration = 0
    sq_dist = 0.
    var = 0.

    while iteration < max_iter:
        var_is_0 = gs.isclose(var, 0.)
        sq_dist_is_small = gs.less_equal(sq_dist, epsilon * var)
        condition = ~gs.logical_or(var_is_0, sq_dist_is_small)
        if not (condition or iteration == 0):
            break

        logs = metric.log(point=points, base_point=mean)

        tangent_mean = gs.einsum(einsum_str, weights, logs)
        tangent_mean /= sum_weights

        estimate_next = metric.exp(tangent_vec=tangent_mean, base_point=mean)

        sq_dist = metric.squared_dist(estimate_next, mean)
        sq_dists_between_iterates.append(sq_dist)

        var = variance(points=points,
                       weights=weights,
                       metric=metric,
                       base_point=estimate_next,
                       point_type=point_type)

        mean = estimate_next
        iteration += 1

    if iteration == max_iter:
        logging.warning('Maximum number of iterations {} reached. '
                        'The mean may be inaccurate'.format(max_iter))

    if verbose:
        logging.info('n_iter: {}, final variance: {}, final dist: {}'.format(
            iteration, var, sq_dist))

    return mean
Ejemplo n.º 3
0
    def belongs(self, point, tolerance=TOLERANCE):
        """Test if a point belongs to the hypersphere.

        This tests whether the point's squared norm in Euclidean space is 1.

        Parameters
        ----------
        point : array-like, shape=[..., dim + 1]
            Point in Euclidean space.
        tolerance : float
            Tolerance at which to evaluate norm == 1.
            Optional, default: 1e-6.

        Returns
        -------
        belongs : array-like, shape=[...,]
            Boolean evaluating if point belongs to the hypersphere.
        """
        point_dim = gs.shape(point)[-1]
        if point_dim != self.dim + 1:
            if point_dim is self.dim:
                logging.warning(
                    'Use the extrinsic coordinates to '
                    'represent points on the hypersphere.')
            belongs = False
            if gs.ndim(point) == 2:
                belongs = gs.tile([belongs], (point.shape[0],))
            return belongs
        sq_norm = gs.sum(point ** 2, axis=-1)
        diff = gs.abs(sq_norm - 1)
        return gs.less_equal(diff, tolerance)
Ejemplo n.º 4
0
    def belongs(self, point, atol=1e-5):
        """Test if a point belongs to St(n,p).

        Test whether the point is a p-frame in n-dimensional space,
        and it is orthonormal.

        Parameters
        ----------
        point : array-like, shape=[..., n, p]
            Point.
        atol : float, optional
            Tolerance at which to evaluate.
            Optional, default: 1e-5.

        Returns
        -------
        belongs : array-like, shape=[...,]
            Array of booleans evaluating if the corresponding points
            belong to the Stiefel manifold.
        """
        right_shape = self.embedding_manifold.belongs(point)
        if not right_shape:
            return right_shape

        point_transpose = Matrices.transpose(point)
        identity = gs.eye(self.p)
        diff = Matrices.mul(point_transpose, point) - identity

        diff_norm = gs.linalg.norm(diff, axis=(-2, -1))
        belongs = gs.less_equal(diff_norm, 1e-5)
        return belongs
Ejemplo n.º 5
0
    def belongs(self, point, tolerance=TOLERANCE):
        """Test if a point belongs to St(n,p).

        Test whether the point is a p-frame in n-dimensional space,
        and it is orthonormal.

        Parameters
        ----------
        point : array-like, shape=[n_samples, n, p]
            Point.
        tolerance : float, optional
            Tolerance at which to evaluate.

        Returns
        -------
        belongs : array-like, shape=[n_samples, 1]
            Array of booleans evaluating if the corresponding points
            belong to the Stiefel manifold.
        """
        n_points, n, p = point.shape

        if (n, p) != (self.n, self.p):
            return gs.array([False] * n_points)

        point_transpose = Matrices.transpose(point)
        identity = gs.eye(p)
        diff = gs.einsum('...ij,...jk->...ik', point_transpose,
                         point) - identity

        diff_norm = gs.linalg.norm(diff, axis=(-2, -1))
        belongs = gs.less_equal(diff_norm, tolerance)
        belongs = gs.to_ndarray(belongs, to_ndim=1)
        return belongs
Ejemplo n.º 6
0
    def belongs(self, point, tolerance=TOLERANCE):
        """
        Check if an (n,n)-matrix is an orthogonal projector
        onto a subspace of rank k.
        """
        point = gs.to_ndarray(point, to_ndim=3)
        n_points, n, k = point.shape

        if (n, k) != (self.n, self.k):
            return gs.array([[False]] * n_points)

        point_transpose = gs.transpose(point, axes=(0, 2, 1))
        identity = gs.to_ndarray(gs.eye(k), to_ndim=3)
        identity = gs.tile(identity, (n_points, 1, 1))
        diff = gs.einsum('nij,njk->nik', point_transpose, point) - identity

        diff_norm = gs.linalg.norm(diff, axis=(1, 2))
        belongs = gs.less_equal(diff_norm, tolerance)

        belongs = gs.to_ndarray(belongs, to_ndim=1)
        belongs = gs.to_ndarray(belongs, to_ndim=2, axis=1)
        return belongs

        raise NotImplementedError(
            'The Grassmann `belongs` is not implemented.'
            'It shall test whether p*=p, p^2 = p and rank(p) = k.')
Ejemplo n.º 7
0
 def belongs(self, point, tolerance=TOLERANCE):
     """
     Evaluate if a point belongs to the Hypersphere,
     i.e. evaluate if its squared norm in the Euclidean space is 1.
     """
     point = gs.asarray(point)
     point_dim = point.shape[-1]
     if point_dim != self.dimension + 1:
         if point_dim is self.dimension:
             logging.warning('Use the extrinsic coordinates to '
                             'represent points on the hypersphere.')
         return gs.array([[False]])
     sq_norm = self.embedding_metric.squared_norm(point)
     diff = gs.abs(sq_norm - 1)
     return gs.less_equal(diff, tolerance)
Ejemplo n.º 8
0
    def _check_idempotent(point, atol):
        """Check that a point is idempotent.

        Parameters
        ----------
        point
        atol

        Returns
        -------
        belongs : bool
        """
        diff = gs.einsum('...ij,...jk->...ik', point, point) - point
        diff_norm = gs.linalg.norm(diff, axis=(-2, -1))

        return gs.less_equal(diff_norm, atol)
Ejemplo n.º 9
0
 def belongs(self, point, tolerance=TOLERANCE):
     """
     By definition, a point on the Hypersphere has squared norm 1
     in the embedding Euclidean space.
     Note: point must be given in extrinsic coordinates.
     """
     point = gs.asarray(point)
     point_dim = point.shape[-1]
     if point_dim != self.dimension + 1:
         if point_dim is self.dimension:
             logging.warning('Use the extrinsic coordinates to '
                             'represent points on the hypersphere.')
         return False
     sq_norm = self.embedding_metric.squared_norm(point)
     diff = gs.abs(sq_norm - 1)
     return gs.less_equal(diff, tolerance)
Ejemplo n.º 10
0
    def _check_idempotent(point, atol):
        """Check that a point is idempotent.

        Parameters
        ----------
        point
        atol

        Returns
        -------
        belongs : bool
        """
        diff = Matrices.mul(point, point) - point
        diff_norm = gs.linalg.norm(diff, axis=(-2, -1))

        return gs.less_equal(diff_norm, atol)
Ejemplo n.º 11
0
    def belongs(self, point, tolerance=TOLERANCE):
        """
        Evaluate if a point belongs to St(n,p),
        i.e. if it is a p-frame in n-dimensional space,
        and it is orthonormal.
        """
        point = gs.to_ndarray(point, to_ndim=3)
        n_points, n, p = point.shape

        if (n, p) != (self.n, self.p):
            return gs.array([[False]] * n_points)

        point_transpose = gs.transpose(point, axes=(0, 2, 1))
        identity = gs.to_ndarray(gs.eye(p), to_ndim=3)
        identity = gs.tile(identity, (n_points, 1, 1))
        diff = gs.einsum('nij,njk->nik', point_transpose, point) - identity

        diff_norm = gs.linalg.norm(diff, axis=(1, 2))
        belongs = gs.less_equal(diff_norm, tolerance)

        belongs = gs.to_ndarray(belongs, to_ndim=1)
        belongs = gs.to_ndarray(belongs, to_ndim=2, axis=1)
        return belongs
Ejemplo n.º 12
0
    def belongs(self, point, atol=TOLERANCE):
        """Test if a point belongs to the pre-shape space.

        This tests whether the point is centered and whether the point's
        Frobenius norm is 1.

        Parameters
        ----------
        point : array-like, shape=[..., k_landmarks, m_ambient]
            Point in Matrices space.
        atol : float
            Tolerance at which to evaluate norm == 1 and mean == 0.
            Optional, default: 1e-6.

        Returns
        -------
        belongs : array-like, shape=[...,]
            Boolean evaluating if point belongs to the pre-shape space.
        """
        shape = point.shape[-2:] == (self.k_landmarks, self.m_ambient)
        frob_norm = self.ambient_metric.norm(point)
        diff = gs.abs(frob_norm - 1)
        is_centered = gs.logical_and(self.is_centered(point, atol), shape)
        return gs.logical_and(gs.less_equal(diff, atol), is_centered)
Ejemplo n.º 13
0
 def while_loop_cond(iteration, mean, var, sq_dist):
     result = ~gs.logical_or(gs.isclose(var, 0.),
                             gs.less_equal(sq_dist, epsilon * var))
     return result[0, 0] or iteration == 0
Ejemplo n.º 14
0
def _default_gradient_descent(points, metric, weights, max_iter, point_type,
                              epsilon, initial_step_size, verbose):
    """Perform default gradient descent."""
    if point_type == 'vector':
        points = gs.to_ndarray(points, to_ndim=2)
        einsum_str = 'n,nj->j'
    else:
        points = gs.to_ndarray(points, to_ndim=3)
        einsum_str = 'n,nij->ij'
    n_points = gs.shape(points)[0]

    if weights is None:
        weights = gs.ones((n_points, ))

    mean = points[0]

    if n_points == 1:
        return mean

    sum_weights = gs.sum(weights)
    sq_dists_between_iterates = []
    iteration = 0
    sq_dist = 0.
    var = 0.

    norm_old = gs.linalg.norm(points)
    step = initial_step_size

    while iteration < max_iter:
        logs = metric.log(point=points, base_point=mean)

        var = gs.sum(
            metric.squared_norm(logs, mean) * weights) / gs.sum(weights)

        tangent_mean = gs.einsum(einsum_str, weights, logs)
        tangent_mean /= sum_weights
        norm = gs.linalg.norm(tangent_mean)

        sq_dist = metric.squared_norm(tangent_mean, mean)
        sq_dists_between_iterates.append(sq_dist)

        var_is_0 = gs.isclose(var, 0.)
        sq_dist_is_small = gs.less_equal(sq_dist, epsilon * metric.dim)
        condition = ~gs.logical_or(var_is_0, sq_dist_is_small)
        if not (condition or iteration == 0):
            break

        estimate_next = metric.exp(step * tangent_mean, mean)
        mean = estimate_next
        iteration += 1

        if norm < norm_old:
            norm_old = norm
        elif norm > norm_old:
            step = step / 2.

    if iteration == max_iter:
        logging.warning('Maximum number of iterations {} reached. '
                        'The mean may be inaccurate'.format(max_iter))

    if verbose:
        logging.info('n_iter: {}, final variance: {}, final dist: {}'.format(
            iteration, var, sq_dist))

    return mean
Ejemplo n.º 15
0
    def log(self, point, base_point, max_iter=30, tol=1e-6):
        """Compute the Riemannian logarithm of a point.

        Based on [ZR2017]_.

        References
        ----------
        .. [ZR2017] Zimmermann, Ralf. "A Matrix-Algebraic Algorithm for the
          Riemannian Logarithm on the Stiefel Manifold under the Canonical
          Metric" SIAM J. Matrix Anal. & Appl., 38(2), 322–342, 2017.
          https://arxiv.org/pdf/1604.05054.pdf

        Parameters
        ----------
        point : array-like, shape=[..., n, p]
            Point in the Stiefel manifold.
        base_point : array-like, shape=[..., n, p]
            Point in the Stiefel manifold.
        max_iter: int
            Maximum number of iterations to perform during the algorithm.
            Optional, default: 30.
        tol: float
            Tolerance to reach convergence. The matrix 2-norm is used as
            criterion.
            Optional, default: 1e-6.

        Returns
        -------
        log : array-like, shape=[..., dim + 1]
            Tangent vector at the base point equal to the Riemannian logarithm
            of point at the base point.
        """
        p = base_point.shape[-1]

        transpose_base_point = Matrices.transpose(base_point)
        matrix_m = gs.matmul(transpose_base_point, point)

        matrix_q, matrix_n = StiefelCanonicalMetric._normal_component_qr(
            point, base_point, matrix_m)

        matrix_v = StiefelCanonicalMetric._orthogonal_completion(
            matrix_m, matrix_n)

        matrix_v = StiefelCanonicalMetric._procrustes_preprocessing(
            p, matrix_v, matrix_m, matrix_n)

        for _ in range(max_iter):
            matrix_lv = gs.linalg.logm(matrix_v)

            matrix_c = matrix_lv[:, p:2 * p, p:2 * p]

            norm_matrix_c = gs.linalg.norm(matrix_c)

            if gs.less_equal(norm_matrix_c, tol):
                break

            matrix_phi = gs.linalg.expm(-matrix_c)

            aux_matrix = gs.matmul(
                matrix_v[:, :, p:2 * p], matrix_phi)

            matrix_v = gs.concatenate(
                [matrix_v[:, :, 0:p],
                 aux_matrix],
                axis=2)

        matrix_xv = gs.matmul(base_point, matrix_lv[:, 0:p, 0:p])
        matrix_qv = gs.matmul(matrix_q, matrix_lv[:, p:2 * p, 0:p])

        return matrix_xv + matrix_qv