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)
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
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)
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
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
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.')
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)
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)
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)
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)
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
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)
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
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
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