def exp(self, tangent_vec, base_point, **kwargs): """Compute the Riemannian exponential of a tangent vector. Parameters ---------- tangent_vec : array-like, shape=[..., dim] Tangent vector at a base point. base_point : array-like, shape=[..., dim] Point in the Poincare ball. Returns ------- exp : array-like, shape=[..., dim] Point in the Poincare ball equal to the Riemannian exponential of tangent_vec at the base point. """ squared_norm_bp = gs.sum(base_point**2, axis=-1) norm_tan = gs.linalg.norm(tangent_vec, axis=-1) lambda_base_point = 1 / (1 - squared_norm_bp) # This avoids dividing by 0 norm_tan_eps = gs.where(gs.isclose(norm_tan, 0.0), EPSILON, norm_tan) direction = gs.einsum("...i,...->...i", tangent_vec, 1 / norm_tan_eps) factor = gs.tanh(lambda_base_point * norm_tan) exp = self.mobius_add( base_point, gs.einsum("...,...i->...i", factor, direction) ) return exp
def __init__(self, group, inner_product_mat_at_identity=None, left_or_right='left'): if inner_product_mat_at_identity is None: inner_product_mat_at_identity = gs.eye(self.group.dimension) inner_product_mat_at_identity = gs.to_ndarray( inner_product_mat_at_identity, to_ndim=3) mat_shape = inner_product_mat_at_identity.shape assert mat_shape == (1, ) + (group.dimension, ) * 2, mat_shape assert left_or_right in ('left', 'right') eigenvalues = gs.linalg.eigvalsh(inner_product_mat_at_identity) mask_pos_eigval = gs.greater(eigenvalues, 0.) n_pos_eigval = gs.sum(gs.cast(mask_pos_eigval, gs.int32)) mask_neg_eigval = gs.less(eigenvalues, 0.) n_neg_eigval = gs.sum(gs.cast(mask_neg_eigval, gs.int32)) mask_null_eigval = gs.isclose(eigenvalues, 0.) n_null_eigval = gs.sum(gs.cast(mask_null_eigval, gs.int32)) self.group = group if inner_product_mat_at_identity is None: inner_product_mat_at_identity = gs.eye(self.group.dimension) self.inner_product_mat_at_identity = inner_product_mat_at_identity self.left_or_right = left_or_right self.signature = (n_pos_eigval, n_null_eigval, n_neg_eigval)
def quaternion_from_rotation_vector(self, rot_vec): """ Convert a rotation vector into a unit quaternion. """ assert self.n == 3, ('The quaternion representation does not exist' ' for rotations in %d dimensions.' % self.n) rot_vec = self.regularize(rot_vec, point_type='vector') n_rot_vecs, _ = rot_vec.shape angle = gs.linalg.norm(rot_vec, axis=1) angle = gs.to_ndarray(angle, to_ndim=2, axis=1) rotation_axis = gs.zeros_like(rot_vec) mask_0 = gs.isclose(angle, 0) mask_0 = gs.squeeze(mask_0, axis=1) mask_not_0 = ~mask_0 rotation_axis[mask_not_0] = rot_vec[mask_not_0] / angle[mask_not_0] n_quaternions, _ = rot_vec.shape quaternion = gs.zeros((n_quaternions, 4)) quaternion[:, :1] = gs.cos(angle / 2) quaternion[:, 1:] = gs.sin(angle / 2) * rotation_axis[:] return quaternion
def is_vertical(self, tangent_vec, base_point, atol=gs.atol): """Evaluate if the tangent vector is vertical at base_point. Parameters ---------- tangent_vec : array-like, shape=[..., {total_space.dim, [n, m]}] Tangent vector. base_point : array-like, shape=[..., {total_space.dim, [n, m]}] Point on the manifold. Optional, default: None. atol : float Absolute tolerance. Optional, default: backend atol. Returns ------- is_vertical : bool Boolean denoting if tangent vector is vertical. """ return gs.all( gs.isclose( tangent_vec, self.vertical_projection(tangent_vec, base_point), atol=atol, ), axis=(-2, -1), )
def random_uniform(self, n_samples=1, tol=1e-6): """Sample in GL(n) from the uniform distribution. Parameters ---------- n_samples : int, optional Number of samples. tol: float, optional Threshold for the absolute value of the determinant of the returned matrix. Returns ------- samples : array-like, shape=[..., n, n] Point sampled on GL(n). """ samples = gs.random.rand(n_samples, self.n, self.n) while True: dets = gs.linalg.det(samples) indcs = gs.isclose(dets, 0.0, atol=tol) num_bad_samples = gs.sum(indcs) if num_bad_samples == 0: break new_samples = gs.random.rand(num_bad_samples, self.n, self.n) samples = self._replace_values(samples, new_samples, indcs) if n_samples == 1: samples = gs.squeeze(samples, axis=0) return samples
def random_uniform(self, n_samples=1): """Sample in the hypersphere from the uniform distribution. Parameters ---------- n_samples : int, optional Number of samples. Returns ------- samples : array-like, shape=[n_samples, dimension + 1] Points sampled on the hypersphere. """ size = (n_samples, self.dimension + 1) samples = gs.random.normal(size=size) while True: norms = gs.linalg.norm(samples, axis=1) indcs = gs.isclose(norms, 0.0) num_bad_samples = gs.sum(indcs) if num_bad_samples == 0: break samples[indcs, :] = gs.random.normal(size=(num_bad_samples, self.dimension + 1)) return gs.einsum('n, ni->ni', 1 / norms, samples)
def align(self, point, base_point, **kwargs): """Align point to base_point. Find the optimal rotation R in SO(m) such that the base point and R.point are well positioned. Parameters ---------- point : array-like, shape=[..., k_landmarks, m_ambient] Point on the manifold. base_point : array-like, shape=[..., k_landmarks, m_ambient] Point on the manifold. Returns ------- aligned : array-like, shape=[..., k_landmarks, m_ambient] R.point. """ mat = gs.matmul(Matrices.transpose(point), base_point) left, singular_values, right = gs.linalg.svd(mat) det = gs.linalg.det(mat) conditioning = ((singular_values[..., -2] + gs.sign(det) * singular_values[..., -1]) / singular_values[..., 0]) if gs.any(conditioning < gs.atol): logging.warning(f'Singularity close, ill-conditioned matrix ' f'encountered: {conditioning}') if gs.any(gs.isclose(conditioning, 0.)): logging.warning("Alignment matrix is not unique.") flipped = flip_determinant(Matrices.transpose(right), det) return Matrices.mul(point, left, Matrices.transpose(flipped))
def test_exp_and_log_and_projection_to_tangent_space_general_case(self): """Test Log and Exp. Test that the Riemannian exponential and the Riemannian logarithm are inverse. Expect their composition to give the identity function. NB: points on the n-dimensional sphere are (n+1)-D vectors of norm 1. """ # Riemannian Exp then Riemannian Log # General case # NB: Riemannian log gives a regularized tangent vector, # so we take the norm modulo 2 * pi. base_point = gs.array([0., -3., 0., 3., 4.]) base_point = base_point / gs.linalg.norm(base_point) vector = gs.array([3., 2., 0., 0., -1.]) vector = self.space.to_tangent(vector=vector, base_point=base_point) exp = self.metric.exp(tangent_vec=vector, base_point=base_point) result = self.metric.log(point=exp, base_point=base_point) expected = vector norm_expected = gs.linalg.norm(expected) regularized_norm_expected = gs.mod(norm_expected, 2 * gs.pi) expected = expected / norm_expected * regularized_norm_expected # The Log can be the opposite vector on the tangent space, # whose Exp gives the base_point are_close = gs.allclose(result, expected) norm_2pi = gs.isclose(gs.linalg.norm(result - expected), 2 * gs.pi) self.assertTrue(are_close or norm_2pi)
def rotation_vector_from_quaternion(self, quaternion): """ Convert a unit quaternion into a rotation vector. """ assert self.n == 3, ('The quaternion representation does not exist' ' for rotations in %d dimensions.' % self.n) quaternion = gs.to_ndarray(quaternion, to_ndim=2) n_quaternions, _ = quaternion.shape cos_half_angle = quaternion[:, 0] cos_half_angle = gs.clip(cos_half_angle, -1, 1) half_angle = gs.arccos(cos_half_angle) half_angle = gs.to_ndarray(half_angle, to_ndim=2, axis=1) assert half_angle.shape == (n_quaternions, 1) rot_vec = gs.zeros_like(quaternion[:, 1:]) mask_0 = gs.isclose(half_angle, 0) mask_0 = gs.squeeze(mask_0, axis=1) mask_not_0 = ~mask_0 rotation_axis = (quaternion[mask_not_0, 1:] / gs.sin(half_angle[mask_not_0])) rot_vec[mask_not_0] = (2 * half_angle[mask_not_0] * rotation_axis) rot_vec = self.regularize(rot_vec, point_type='vector') return rot_vec
def align_matrices(cls, point, base_point): """Align matrices. Find the optimal rotation R in SO(m) such that the base point and R.point are well positioned. Parameters ---------- point : array-like, shape=[..., m, n] Point on the manifold. base_point : array-like, shape=[..., m, n] Point on the manifold. Returns ------- aligned : array-like, shape=[..., m, n] R.point. """ mat = gs.matmul(cls.transpose(point), base_point) left, singular_values, right = gs.linalg.svd(mat, full_matrices=False) det = gs.linalg.det(mat) conditioning = (singular_values[..., -2] + gs.sign(det) * singular_values[..., -1]) / singular_values[..., 0] if gs.any(conditioning < gs.atol): logging.warning(f"Singularity close, ill-conditioned matrix " f"encountered: " f"{conditioning[conditioning < 1e-10]}") if gs.any(gs.isclose(conditioning, 0.0)): logging.warning("Alignment matrix is not unique.") flipped = flip_determinant(cls.transpose(right), det) return Matrices.mul(point, left, cls.transpose(flipped))
def is_tangent(self, vector, base_point, tangent_atol=gs.atol): r"""Check if the vector belongs to the tangent space. Parameters ---------- vector : array-like, shape=[..., n, n] Matrix to check if it belongs to the tangent space. base_point : array-like, shape=[..., n, n] Base point of the tangent space. Optional, default: None. tangent_atol: float Absolute tolerance. Optional, default: backend atol. Returns ------- belongs : array-like, shape=[...,] Boolean denoting if vector belongs to tangent space at base_point. """ vector_sym = Matrices(self.n, self.n).to_symmetric(vector) _, r = gs.linalg.eigh(base_point) r_ort = r[..., :, self.n - self.rank:self.n] r_ort_t = Matrices.transpose(r_ort) rr = gs.matmul(r_ort, r_ort_t) candidates = Matrices.mul(rr, vector_sym, rr) result = gs.all(gs.isclose(candidates, 0.0, tangent_atol), axis=(-2, -1)) return result
def is_tangent(self, vector, base_point, atol=gs.atol): """Check whether the vector is tangent at base_point. Parameters ---------- vector : array-like, shape=[..., dim] Vector. base_point : array-like, shape=[..., dim] Point on the manifold. atol : float Absolute tolerance. Optional, default: backend atol. Returns ------- is_tangent : bool Boolean denoting if vector is a tangent vector at the base point. """ belongs = self.embedding_space.is_tangent(vector, base_point, atol) tangent_sub_applied = self.tangent_submersion(vector, base_point) constraint = gs.isclose(tangent_sub_applied, 0.0, atol=atol) value = self.value if value.ndim == 2: constraint = gs.all(constraint, axis=(-2, -1)) elif value.ndim == 1: constraint = gs.all(constraint, axis=-1) return gs.logical_and(belongs, constraint)
def quaternion_from_rotation_vector(self, rot_vec): """Convert a rotation vector into a unit quaternion. Parameters ---------- rot_vec : array-like, shape=[..., 3] Returns ------- quaternion : array-like, shape=[..., 4] """ rot_vec = self.regularize(rot_vec) angle = gs.linalg.norm(rot_vec, axis=1) angle = gs.to_ndarray(angle, to_ndim=2, axis=1) mask_0 = gs.isclose(angle, 0.) mask_not_0 = ~mask_0 rotation_axis = gs.divide( rot_vec, angle * gs.cast(mask_not_0, gs.float32) + gs.cast(mask_0, gs.float32)) quaternion = gs.concatenate( (gs.cos(angle / 2), gs.sin(angle / 2) * rotation_axis[:]), axis=1) return quaternion
def regularize(self, point): """Regularize a point to the canonical representation. Regularize a point to the canonical representation chosen for the hyperbolic space, to avoid numerical issues. Parameters ---------- point : array-like, shape=[..., dim + 1] Point. Returns ------- projected_point : array-like, shape=[..., dim + 1] Point in hyperbolic space in canonical representation in extrinsic coordinates. """ if self.coords_type == 'intrinsic': point = self.intrinsic_to_extrinsic_coords(point) point = gs.to_ndarray(point, to_ndim=2) sq_norm = self.embedding_metric.squared_norm(point) real_norm = gs.sqrt(gs.abs(sq_norm)) mask_0 = gs.isclose(real_norm, 0.) mask_not_0 = ~mask_0 mask_not_0_float = gs.cast(mask_not_0, gs.float32) projected_point = point normalized_point = gs.einsum('...,...i->...i', 1. / real_norm, point) projected_point = gs.einsum('...,...i->...i', mask_not_0_float, normalized_point) return projected_point
def belongs(self, point, atol=gs.atol): """Evaluate if a point belongs to the manifold. Parameters ---------- point : array-like, shape=[..., dim] Point to evaluate. atol : float Absolute tolerance. Optional, default: backend atol. Returns ------- belongs : array-like, shape=[...,] Boolean evaluating if point belongs to the manifold. """ belongs = self.embedding_space.belongs(point, atol) if not gs.any(belongs): return belongs value = self.value constraint = gs.isclose(self.submersion(point), value, atol=atol) if value.ndim == 2: constraint = gs.all(constraint, axis=(-2, -1)) elif value.ndim == 1: constraint = gs.all(constraint, axis=-1) return gs.logical_and(belongs, constraint)
def test_isclose(self): base_list = [[[22. + 1e-5, 22. + 1e-7], [22. + 1e-6, 88. + 1e-4]]] np_array = _np.array(base_list) gs_array = gs.array(base_list) np_result = _np.isclose(np_array, 22.) gs_result = gs.isclose(gs_array, 22.) self.assertAllCloseToNp(gs_result, np_result) np_result = _np.isclose(np_array, 22., atol=1e-8) gs_result = gs.isclose(gs_array, 22., atol=1e-8) self.assertAllCloseToNp(gs_result, np_result) np_result = _np.isclose(np_array, 22., rtol=1e-8, atol=1e-7) gs_result = gs.isclose(gs_array, 22., rtol=1e-8, atol=1e-7) self.assertAllCloseToNp(gs_result, np_result)
def test_kendall_sectional_curvature(self, k_landmarks, m_ambient, tangent_vec_a, tangent_vec_b, base_point): """Sectional curvature of Kendall shape space is larger than 1. The sectional curvature always increase by taking the quotient in a Riemannian submersion. Thus, it should larger in kendall shape space thane the sectional curvature of the pre-shape space which is 1 as it a hypersphere. The sectional curvature is computed here with the generic directional_curvature and sectional curvature methods. """ space = self.space(k_landmarks, m_ambient) metric = self.metric(k_landmarks, m_ambient) hor_a = space.horizontal_projection(tangent_vec_a, base_point) hor_b = space.horizontal_projection(tangent_vec_b, base_point) tidal_force = metric.directional_curvature(hor_a, hor_b, base_point) numerator = metric.inner_product(tidal_force, hor_a, base_point) denominator = (metric.inner_product(hor_a, hor_a, base_point) * metric.inner_product(hor_b, hor_b, base_point) - metric.inner_product(hor_a, hor_b, base_point)**2) condition = ~gs.isclose(denominator, 0.0, atol=gs.atol * 100) kappa = numerator[condition] / denominator[condition] kappa_direct = metric.sectional_curvature(hor_a, hor_b, base_point)[condition] self.assertAllClose(kappa, kappa_direct) result = kappa > 1.0 - 1e-10 self.assertTrue(gs.all(result))
def belongs(self, point): """Check whether point is of the form rotation, translation. Parameters ---------- point : array-like, shape=[..., n, n]. Point to be checked. Returns ------- belongs : array-like, shape=[...,] Boolean denoting if point belongs to the group. """ point_dim1, point_dim2 = point.shape[-2:] belongs = (point_dim1 == point_dim2 == self.n + 1) rotation = point[..., :self.n, :self.n] rot_belongs = self.rotations.belongs(rotation) belongs = gs.logical_and(belongs, rot_belongs) last_line_except_last_term = point[..., self.n:, :-1] all_but_last_zeros = ~ gs.any( last_line_except_last_term, axis=(-2, -1)) belongs = gs.logical_and(belongs, all_but_last_zeros) last_term = point[..., self.n, self.n] belongs = gs.logical_and(belongs, gs.isclose(last_term, 1.)) if point.ndim == 2: return gs.squeeze(belongs) return gs.flatten(belongs)
def regularize(self, point): """Regularize a point to the canonical representation. Regularize a point to the canonical representation chosen for the Hyperbolic space, to avoid numerical issues. Parameters ---------- point : array-like, shape=[n_samples, dimension + 1] Input points. TODO: confusing: singular or plural Returns ------- projected_point : array-like, shape=[n_samples, dimension + 1] """ point = gs.to_ndarray(point, to_ndim=2) sq_norm = self.embedding_metric.squared_norm(point) real_norm = gs.sqrt(gs.abs(sq_norm)) mask_0 = gs.isclose(real_norm, 0.) mask_not_0 = ~mask_0 mask_not_0_float = gs.cast(mask_not_0, gs.float32) projected_point = point projected_point = mask_not_0_float * (point / real_norm) return projected_point
def rotation_vector_from_quaternion(self, quaternion): """Convert a unit quaternion into a rotation vector. Parameters ---------- quaternion : array-like, shape=[..., 4] Returns ------- rot_vec : array-like, shape=[..., 3] """ cos_half_angle = quaternion[:, 0] cos_half_angle = gs.clip(cos_half_angle, -1, 1) half_angle = gs.arccos(cos_half_angle) half_angle = gs.to_ndarray(half_angle, to_ndim=2, axis=1) mask_0 = gs.isclose(half_angle, 0.) mask_not_0 = ~mask_0 rotation_axis = gs.divide( quaternion[:, 1:], gs.sin(half_angle) * gs.cast(mask_not_0, gs.float32) + gs.cast(mask_0, gs.float32)) rot_vec = gs.array( 2 * half_angle * rotation_axis * gs.cast(mask_not_0, gs.float32)) rot_vec = self.regularize(rot_vec) return rot_vec
def random_point(self, n_samples=1, bound=1.): """Sample in GL(n) from the uniform distribution. Parameters ---------- n_samples : int Number of samples. Optional, default: 1. bound: float Bound of the interval in which to sample each matrix entry. Optional, default: 1. Returns ------- samples : array-like, shape=[..., n, n] Point sampled on GL(n). """ samples = gs.random.normal(size=(n_samples, self.n, self.n)) while True: dets = gs.linalg.det(samples) indcs = gs.isclose(dets, 0.0) num_bad_samples = gs.sum(indcs) if num_bad_samples == 0: break new_samples = gs.random.normal(size=(num_bad_samples, self.n, self.n)) samples = self._replace_values(samples, new_samples, indcs) if n_samples == 1: samples = gs.squeeze(samples, axis=0) return samples
def log(self, point, base_point): """ Riemannian logarithm of a point wrt a base point. """ point = gs.to_ndarray(point, to_ndim=2) base_point = gs.to_ndarray(base_point, to_ndim=2) angle = self.dist(base_point, point) angle = gs.to_ndarray(angle, to_ndim=1) angle = gs.to_ndarray(angle, to_ndim=2) mask_0 = gs.isclose(angle, 0) mask_else = ~mask_0 coef_1 = gs.zeros_like(angle) coef_2 = gs.zeros_like(angle) coef_1[mask_0] = (1. + INV_SINH_TAYLOR_COEFFS[1] * angle[mask_0]**2 + INV_SINH_TAYLOR_COEFFS[3] * angle[mask_0]**4 + INV_SINH_TAYLOR_COEFFS[5] * angle[mask_0]**6 + INV_SINH_TAYLOR_COEFFS[7] * angle[mask_0]**8) coef_2[mask_0] = (1. + INV_TANH_TAYLOR_COEFFS[1] * angle[mask_0]**2 + INV_TANH_TAYLOR_COEFFS[3] * angle[mask_0]**4 + INV_TANH_TAYLOR_COEFFS[5] * angle[mask_0]**6 + INV_TANH_TAYLOR_COEFFS[7] * angle[mask_0]**8) coef_1[mask_else] = angle[mask_else] / gs.sinh(angle[mask_else]) coef_2[mask_else] = angle[mask_else] / gs.tanh(angle[mask_else]) log = (gs.einsum('ni,nj->nj', coef_1, point) - gs.einsum('ni,nj->nj', coef_2, base_point)) return log
def __init__(self, group, inner_product_mat_at_identity=None, left_or_right='left', **kwargs): super(InvariantMetric, self).__init__(dim=group.dim, **kwargs) self.group = group if inner_product_mat_at_identity is None: inner_product_mat_at_identity = gs.eye(self.group.dim) geomstats.errors.check_parameter_accepted_values( left_or_right, 'left_or_right', ['left', 'right']) eigenvalues = gs.linalg.eigvalsh(inner_product_mat_at_identity) mask_pos_eigval = gs.greater(eigenvalues, 0.) n_pos_eigval = gs.sum(gs.cast(mask_pos_eigval, gs.int32)) mask_neg_eigval = gs.less(eigenvalues, 0.) n_neg_eigval = gs.sum(gs.cast(mask_neg_eigval, gs.int32)) mask_null_eigval = gs.isclose(eigenvalues, 0.) n_null_eigval = gs.sum(gs.cast(mask_null_eigval, gs.int32)) self.inner_product_mat_at_identity = inner_product_mat_at_identity self.left_or_right = left_or_right self.signature = (n_pos_eigval, n_null_eigval, n_neg_eigval)
def is_tangent(self, vector, base_point=None, atol=TOLERANCE): r"""Check if a vector is tangent to the manifold at the base point. Check if the (n,n)-matrix :math: `Y` is symmetric and verifies the relation :math: PY + YP = Y where :math: `P` represents the base point and :math: `Y` the vector. Parameters ---------- vector : array-like, shape=[..., n, n] Matrix to be checked. base_point : array-like, shape=[..., n, n] Base point. atol : int Optional, default: 1e-5. Returns ------- belongs : array-like, shape=[...,] Boolean evaluating if `vector` is tangent to the Grassmannian at `base_point`. """ diff = Matrices.mul(base_point, vector) + Matrices.mul( vector, base_point) - vector is_close = gs.all(gs.isclose(diff, 0., atol=atol)) return gs.logical_and(Matrices.is_symmetric(vector), is_close)
def random_uniform(self, n_samples=1): """Sample in the hypersphere from the uniform distribution. Parameters ---------- n_samples : int Number of samples. Optional, default: 1. Returns ------- samples : array-like, shape=[..., dim + 1] Points sampled on the hypersphere. """ size = (n_samples, self.dim + 1) samples = gs.random.normal(size=size) while True: norms = gs.linalg.norm(samples, axis=1) indcs = gs.isclose(norms, 0.0, atol=TOLERANCE) num_bad_samples = gs.sum(indcs) if num_bad_samples == 0: break new_samples = gs.random.normal(size=(num_bad_samples, self.dim + 1)) samples = self._replace_values(samples, new_samples, indcs) samples = gs.einsum('..., ...i->...i', 1 / norms, samples) if n_samples == 1: samples = gs.squeeze(samples, axis=0) return samples
def regularize(self, point, point_type=None): """ In 3D, regularize the norm of the rotation vector, to be between 0 and pi, following the axis-angle representation's convention. If the angle angle is between pi and 2pi, the function computes its complementary in 2pi and inverts the direction of the rotation axis. """ if point_type is None: point_type = self.default_point_type if point_type == 'vector': point = gs.to_ndarray(point, to_ndim=2) assert self.belongs(point, point_type) n_points, _ = point.shape regularized_point = gs.copy(point) if self.n == 3: angle = gs.linalg.norm(regularized_point, axis=1) mask_0 = gs.isclose(angle, 0) mask_not_0 = ~mask_0 mask_pi = gs.isclose(angle, gs.pi) k = gs.floor(angle / (2 * gs.pi) + .5) norms_ratio = gs.zeros_like(angle) norms_ratio[mask_not_0] = ( 1. - 2. * gs.pi * k[mask_not_0] / angle[mask_not_0]) norms_ratio[mask_0] = 1 norms_ratio[mask_pi] = gs.pi / angle[mask_pi] for i in range(n_points): regularized_point[i, :] = (norms_ratio[i] * regularized_point[i, :]) else: # TODO(nina): regularization needed in nD? regularized_point = gs.copy(point) assert gs.ndim(regularized_point) == 2 elif point_type == 'matrix': point = gs.to_ndarray(point, to_ndim=3) # TODO(nina): regularization for matrices? regularized_point = gs.copy(point) return regularized_point
def test_value_and_grad_loss_hypersphere(self): gr = GeodesicRegression( self.sphere, metric=self.sphere.metric, center_X=False, method="extrinsic", max_iter=50, init_step_size=0.1, verbose=True, regularization=0, ) def loss_of_param(param): return gr._loss(self.X_sphere, self.y_sphere, param, self.shape_sphere) # Without numpy conversion objective_with_grad = gs.autodiff.value_and_grad(loss_of_param) loss_value, loss_grad = objective_with_grad(self.param_sphere_guess) expected_grad_shape = (2, self.dim_sphere + 1) self.assertAllClose(loss_value.shape, ()) self.assertAllClose(loss_grad.shape, expected_grad_shape) self.assertFalse(gs.isclose(loss_value, 0.0)) self.assertFalse(gs.isnan(loss_value)) self.assertFalse( gs.all(gs.isclose(loss_grad, gs.zeros(expected_grad_shape)))) self.assertTrue(gs.all(~gs.isnan(loss_grad))) # With numpy conversion objective_with_grad = gs.autodiff.value_and_grad(loss_of_param, to_numpy=True) loss_value, loss_grad = objective_with_grad(self.param_sphere_guess) # Convert back to arrays/tensors loss_value = gs.array(loss_value) loss_grad = gs.array(loss_grad) expected_grad_shape = (2, self.dim_sphere + 1) self.assertAllClose(loss_value.shape, ()) self.assertAllClose(loss_grad.shape, expected_grad_shape) self.assertFalse(gs.isclose(loss_value, 0.0)) self.assertFalse(gs.isnan(loss_value)) self.assertFalse( gs.all(gs.isclose(loss_grad, gs.zeros(expected_grad_shape)))) self.assertTrue(gs.all(~gs.isnan(loss_grad)))
def _default_gradient_descent(points, metric, weights, max_iter, point_type, epsilon, verbose): 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 mean(self, points, weights=None, n_max_iterations=32, epsilon=EPSILON): """ Frechet mean of (weighted) points. """ # TODO(nina): profile this code to study performance, # i.e. what to do with sq_dists_between_iterates. if isinstance(points, list): points = gs.vstack(points) n_points = gs.shape(points)[0] if isinstance(weights, list): weights = gs.vstack(weights) if weights is None: weights = gs.ones((n_points, 1)) weights = gs.array(weights) weights = gs.to_ndarray(weights, to_ndim=2, axis=1) sum_weights = gs.sum(weights) mean = points[0] if n_points == 1: return mean sq_dists_between_iterates = [] iteration = 0 while iteration < n_max_iterations: a_tangent_vector = self.log(mean, mean) tangent_mean = gs.zeros_like(a_tangent_vector) logs = self.log(point=points, base_point=mean) tangent_mean += gs.einsum('nk,nj->j', weights, logs) tangent_mean /= sum_weights mean_next = self.exp(tangent_vec=tangent_mean, base_point=mean) sq_dist = self.squared_dist(mean_next, mean) sq_dists_between_iterates.append(sq_dist) variance = self.variance(points=points, weights=weights, base_point=mean_next) if gs.isclose(variance, 0.)[0, 0]: break if (sq_dist <= epsilon * variance)[0, 0]: break mean = mean_next iteration += 1 if iteration is n_max_iterations: print('Maximum number of iterations {} reached.' 'The mean may be inaccurate'.format(n_max_iterations)) mean = gs.to_ndarray(mean, to_ndim=2) return mean
def regularize_tangent_vec_at_identity(self, tangent_vec, metric=None): """ In 3D, regularize a tangent_vector by getting its norm at the identity, determined by the metric, to be less than pi, following the regularization convention. """ assert self.point_representation in ('vector', 'matrix') if self.point_representation is 'vector': tangent_vec = gs.to_ndarray(tangent_vec, to_ndim=2) _, vec_dim = tangent_vec.shape if metric is None: metric = self.left_canonical_metric tangent_vec_metric_norm = metric.norm(tangent_vec) tangent_vec_canonical_norm = gs.linalg.norm(tangent_vec, axis=1) if tangent_vec_canonical_norm.ndim == 1: tangent_vec_canonical_norm = gs.expand_dims( tangent_vec_canonical_norm, axis=1) mask_norm_0 = gs.isclose(tangent_vec_metric_norm, 0) mask_canonical_norm_0 = gs.isclose(tangent_vec_canonical_norm, 0) mask_0 = mask_norm_0 | mask_canonical_norm_0 mask_else = ~mask_0 mask_0 = gs.squeeze(mask_0, axis=1) mask_else = gs.squeeze(mask_else, axis=1) coef = gs.empty_like(tangent_vec_metric_norm) regularized_vec = tangent_vec regularized_vec[mask_0] = tangent_vec[mask_0] coef[mask_else] = (tangent_vec_metric_norm[mask_else] / tangent_vec_canonical_norm[mask_else]) regularized_vec[mask_else] = self.regularize( coef[mask_else] * tangent_vec[mask_else]) regularized_vec[mask_else] = (regularized_vec[mask_else] / coef[mask_else]) else: # TODO(nina): regularization needed in nD? regularized_vec = tangent_vec return regularized_vec