def test_exp(self, n, power_affine, tangent_vec, base_point, expected): metric = SPDMetricAffine(n, power_affine) self.assertAllClose( metric.exp(gs.array(tangent_vec), gs.array(base_point)), gs.array(expected) )
def __init__(self, points=None): self.center = gs.array([0., 0.]) self.points = [] if points is not None: self.add_points(points)
def test_random_uniform_and_belongs(self): point = self.space.random_uniform() result = self.space.belongs(point) expected = gs.array([[True]]) self.assertAllClose(result, expected)
def test_squared_norm_vectorization(self): n_samples = 3 n_points = gs.array([[-1., 0.], [1., 0.], [2., math.sqrt(3)]]) result = self.metric.squared_norm(n_points) self.assertAllClose(gs.shape(result), (n_samples, 1))
def test_inner_product_matrix(self): result = self.metric.inner_product_matrix() expected = gs.array([[-1., 0.], [0., 1.]]) self.assertAllClose(result, expected)
def test_belongs(self, n, mat, expected): space = self.space(n) self.assertAllClose(space.belongs(gs.array(mat)), gs.array(expected))
def test_random_and_belongs_vector(self): n_samples = 5 data = self.space_vector.random_uniform(n_samples) result = self.space_vector.belongs(data) expected = gs.array([[True] * n_samples]).transpose(1, 0) self.assertAllClose(result, expected)
def test_log(self, n, power_euclidean, point, base_point, expected): metric = SPDMetricEuclidean(n) result = metric.log(gs.array(point), gs.array(base_point)) self.assertAllClose(result, gs.array(expected))
def test_log_exp_composition(self, n, power_euclidean, point, base_point): metric = SPDMetricEuclidean(n, power_euclidean) log = metric.log(gs.array(point), base_point=gs.array(base_point)) result = metric.exp(tangent_vec=log, base_point=gs.array(base_point)) self.assertAllClose(result, point, atol=gs.atol * 1000)
def test_log(self, n, point, base_point, expected): metric = SPDMetricBuresWasserstein(n) result = metric.log(gs.array(point), gs.array(base_point)) self.assertAllClose(result, expected)
def test_exp_domain(self, n, power_euclidean, tangent_vec, base_point, expected): metric = SPDMetricEuclidean(n, power_euclidean) result = metric.exp_domain( gs.array(tangent_vec), gs.array(base_point), expected ) self.assertAllClose(result, gs.array(expected))
def test_exp(self, n, tangent_vec, base_point, expected): metric = SPDMetricBuresWasserstein(n) result = metric.exp(gs.array(tangent_vec), gs.array(base_point)) self.assertAllClose(result, gs.array(expected))
def test_inner_product(self, n, tangent_vec_a, tangent_vec_b, base_point, expected): metric = SPDMetricBuresWasserstein(n) result = metric.inner_product( gs.array(tangent_vec_a), gs.array(tangent_vec_b), gs.array(base_point) ) self.assertAllClose(result, gs.array(expected))
def test_log(self, n, power_affine, point, base_point, expected): metric = SPDMetricAffine(n, power_affine) self.assertAllClose( metric.log(gs.array(point), gs.array(base_point)), gs.array(expected) )
def test_ball_extrinsic_ball(self): x = gs.array([[0.5, 0.2]]) x_e = self.ball_manifold.to_coordinates(x, to_point_type='extrinsic') x2 = self.extrinsic_manifold.to_coordinates(x_e, to_point_type='ball') self.assertAllClose(x, x2, atol=1e-10)
def test_belongs(self): result = self.space_landmarks_in_sphere_2d.belongs(self.landmarks_a) expected = gs.array([[True]]) self.assertAllClose(result, expected)
def test_belongs_ball(self): x = gs.array([[0.5, 0.2]]) belong = self.ball_manifold.belongs(x) assert (belong[0])
def test_compute_coordinates_spd2(self): point = gs.eye(2) ellipsis = visualization.Ellipses(n_sampling_points=4) x, y = ellipsis.compute_coordinates(point) self.assertAllClose(x, gs.array([1, 0, -1, 0, 1])) self.assertAllClose(y, gs.array([0, 1, 0, -1, 0]))
def test_permute(self, n, graph, permutation, expected): space = self.space(n) result = space.permute(gs.array(graph), permutation) self.assertAllClose(result, expected)
def random_von_mises_fisher( self, mu=None, kappa=10, n_samples=1, max_iter=100): """Sample with the von Mises-Fisher distribution. This distribution corresponds to the maximum entropy distribution given a mean. In dimension 2, a closed form expression is available. In larger dimension, rejection sampling is used according to [Wood94]_ References ---------- https://en.wikipedia.org/wiki/Von_Mises-Fisher_distribution .. [Wood94] Wood, Andrew T. A. “Simulation of the von Mises Fisher Distribution.” Communications in Statistics - Simulation and Computation, June 27, 2007. https://doi.org/10.1080/03610919408813161. Parameters ---------- mu : array-like, shape=[dim] Mean parameter of the distribution. kappa : float Kappa parameter of the von Mises distribution. Optional, default: 10. n_samples : int Number of samples. Optional, default: 1. Returns ------- point : array-like, shape=[..., 3] Points sampled on the sphere in extrinsic coordinates in Euclidean space of dimension 3. """ dim = self.dim if dim == 2: angle = 2. * gs.pi * gs.random.rand(n_samples) angle = gs.to_ndarray(angle, to_ndim=2, axis=1) unit_vector = gs.hstack((gs.cos(angle), gs.sin(angle))) scalar = gs.random.rand(n_samples) coord_z = 1. + 1. / kappa * gs.log( scalar + (1. - scalar) * gs.exp(gs.array(-2. * kappa))) coord_z = gs.to_ndarray(coord_z, to_ndim=2, axis=1) coord_xy = gs.sqrt(1. - coord_z ** 2) * unit_vector sample = gs.hstack((coord_xy, coord_z)) if mu is not None: rot_vec = gs.cross( gs.array([0., 0., 1.]), mu) rot_vec *= gs.arccos(mu[-1]) / gs.linalg.norm(rot_vec) rot = SpecialOrthogonal( 3, 'vector').matrix_from_rotation_vector(rot_vec) sample = gs.matmul(sample, gs.transpose(rot)) else: if mu is None: mu = gs.array([0.] * dim + [1.]) # rejection sampling in the general case sqrt = gs.sqrt(4 * kappa ** 2. + dim ** 2) envelop_param = (-2 * kappa + sqrt) / dim node = (1. - envelop_param) / (1. + envelop_param) correction = kappa * node + dim * gs.log(1. - node ** 2) n_accepted, n_iter = 0, 0 result = [] while (n_accepted < n_samples) and (n_iter < max_iter): sym_beta = beta.rvs( dim / 2, dim / 2, size=n_samples - n_accepted) coord_z = (1 - (1 + envelop_param) * sym_beta) / ( 1 - (1 - envelop_param) * sym_beta) accept_tol = gs.random.rand(n_samples - n_accepted) criterion = ( kappa * coord_z + dim * gs.log(1 - node * coord_z) - correction) > gs.log(accept_tol) result.append(coord_z[criterion]) n_accepted += gs.sum(criterion) n_iter += 1 if n_accepted < n_samples: logging.warning( 'Maximum number of iteration reached in rejection ' 'sampling before n_samples were accepted.') coord_z = gs.concatenate(result) coord_rest = self.random_uniform(n_accepted) coord_rest = self.to_tangent(coord_rest, mu) coord_rest = self.projection(coord_rest) coord_rest = gs.einsum( '...,...i->...i', gs.sqrt(1 - coord_z ** 2), coord_rest) sample = coord_rest + coord_z[:, None] * mu[None, :] return sample if n_samples > 1 else sample[0]
def _sphere_metric_matrix(base_point): """Return sphere's metric in spherical coordinates.""" theta = base_point[..., 0] mat = gs.array([[1.0, 0.0], [0.0, gs.sin(theta)**2]]) return mat
def test_symmetric_matrix_from_vector(self): vector_2 = gs.array([1, 2, 3, 4, 5, 6]) result = self.space.from_vector(vector_2) expected = gs.array([[1., 2., 3.], [2., 4., 5.], [3., 5., 6.]]) self.assertAllClose(result, expected)
def test_belongs(self): point = gs.array([-1., 3.]) result = self.space.belongs(point) expected = gs.array([[True]]) self.assertAllClose(result, expected)
def log(self, point, base_point): """Compute the Riemannian logarithm of a point. Parameters ---------- point : array-like, shape=[..., dim + 1] Point on the hypersphere. base_point : array-like, shape=[..., dim + 1] Point on the hypersphere. Returns ------- log : array-like, shape=[..., dim + 1] Tangent vector at the base point equal to the Riemannian logarithm of point at the base point. """ norm_base_point = self.embedding_metric.norm(base_point) norm_point = self.embedding_metric.norm(point) inner_prod = self.embedding_metric.inner_product(base_point, point) cos_angle = inner_prod / (norm_base_point * norm_point) cos_angle = gs.clip(cos_angle, -1., 1.) angle = gs.arccos(cos_angle) angle = gs.to_ndarray(angle, to_ndim=1) angle = gs.to_ndarray(angle, to_ndim=2, axis=1) mask_0 = gs.isclose(angle, 0.) mask_else = gs.equal(mask_0, gs.array(False)) mask_0_float = gs.cast(mask_0, gs.float32) mask_else_float = gs.cast(mask_else, gs.float32) coef_1 = gs.zeros_like(angle) coef_2 = gs.zeros_like(angle) coef_1 += mask_0_float * (1. + INV_SIN_TAYLOR_COEFFS[1] * angle**2 + INV_SIN_TAYLOR_COEFFS[3] * angle**4 + INV_SIN_TAYLOR_COEFFS[5] * angle**6 + INV_SIN_TAYLOR_COEFFS[7] * angle**8) coef_2 += mask_0_float * (1. + INV_TAN_TAYLOR_COEFFS[1] * angle**2 + INV_TAN_TAYLOR_COEFFS[3] * angle**4 + INV_TAN_TAYLOR_COEFFS[5] * angle**6 + INV_TAN_TAYLOR_COEFFS[7] * angle**8) # This avoids division by 0. angle += mask_0_float * 1. coef_1 += mask_else_float * angle / gs.sin(angle) coef_2 += mask_else_float * angle / gs.tan(angle) log = (gs.einsum('...i,...j->...j', coef_1, point) - gs.einsum('...i,...j->...j', coef_2, base_point)) mask_same_values = gs.isclose(point, base_point) mask_else = gs.equal(mask_same_values, gs.array(False)) mask_else_float = gs.cast(mask_else, gs.float32) mask_else_float = gs.to_ndarray(mask_else_float, to_ndim=1) mask_else_float = gs.to_ndarray(mask_else_float, to_ndim=2) mask_not_same_points = gs.sum(mask_else_float, axis=1) mask_same_points = gs.isclose(mask_not_same_points, 0.) mask_same_points = gs.cast(mask_same_points, gs.float32) mask_same_points = gs.to_ndarray(mask_same_points, to_ndim=2, axis=1) mask_same_points_float = gs.cast(mask_same_points, gs.float32) log -= mask_same_points_float * log return log
def test_squared_norm(self): point = gs.array([-2., 4.]) result = self.metric.squared_norm(point) expected = gs.array([[12.]]) self.assertAllClose(result, expected)
def online_kmeans(X, metric, n_clusters, n_repetitions=20, tolerance=1e-5, max_iter=5e4): """Perform online K-means clustering. Perform online version of k-means algorithm on data contained in X. The data points are treated sequentially and the cluster centers are updated one at a time. This version of k-means avoids computing the mean of each cluster at each iteration and is therefore less computationally intensive than the offline version. In the setting of quantization of probability distributions, this algorithm is also known as Competitive Learning Riemannian Quantization. It computes the closest approximation of the empirical distribution of data by a discrete distribution supported by a smaller number of points with respect to the Wasserstein distance. This smaller number of points is n_clusters. Parameters ---------- X : array-like, shape=[..., n_features] Input data. It is treated sequentially by the algorithm, i.e. one datum is chosen randomly at each iteration. metric : object Metric of the space in which the data lives. At each iteration, one of the cluster centers is moved in the direction of the new datum, according the exponential map of the underlying space, which is a method of metric. n_clusters : int Number of clusters of the k-means clustering, or number of desired atoms of the quantized distribution. n_repetitions : int, default=20 The cluster centers are updated using decreasing step sizes, each of which stays constant for n_repetitions iterations to allow a better exploration of the data points. max_iter : int, default=5e4 Maximum number of iterations. If it is reached, the quantization may be inacurate. Returns ------- cluster_centers : array, shape=[n_clusters, n_features] Coordinates of cluster centers. labels : array, shape=[n_samples] Cluster labels for each point. """ n_samples = X.shape[0] random_indices = gs.random.randint(low=0, high=n_samples, size=(n_clusters,)) cluster_centers = gs.get_slice(X, gs.cast(random_indices, gs.int32)) gap = 1.0 iteration = 0 while iteration < max_iter: iteration += 1 step_size = gs.floor(gs.array(iteration / n_repetitions)) + 1 random_index = gs.random.randint(low=0, high=n_samples, size=(1,)) point = gs.get_slice(X, gs.cast(random_index, gs.int32)) index_to_update = metric.closest_neighbor_index(point, cluster_centers) center_to_update = gs.copy( gs.get_slice(cluster_centers, index_to_update)) tangent_vec_update = metric.log( point=point, base_point=center_to_update ) / (step_size + 1) new_center = metric.exp( tangent_vec=tangent_vec_update, base_point=center_to_update ) gap = metric.dist(center_to_update, new_center) if gap == 0 and iteration == 1: gap = gs.array(1.0) cluster_centers[index_to_update, :] = new_center if gs.isclose(gap, 0, atol=tolerance): break if iteration == max_iter - 1: logging.warning( 'Maximum number of iterations {} reached. The' 'clustering may be inaccurate'.format(max_iter)) labels = gs.zeros(n_samples) for i in range(n_samples): labels[i] = int(metric.closest_neighbor_index(X[i], cluster_centers)) return cluster_centers, labels
def log(self, point, base_point): """Compute Riemannian logarithm of a point wrt a base point. Parameters ---------- point : array-like, shape=[n_samples, dimension + 1] or shape=[1, dimension + 1] base_point : array-like, shape=[n_samples, dimension + 1] or shape=[1, dimension + 1] Returns ------- log : array-like, shape=[n_samples, dimension + 1] or shape=[1, dimension + 1] """ point = gs.to_ndarray(point, to_ndim=2) base_point = gs.to_ndarray(base_point, to_ndim=2) norm_base_point = self.embedding_metric.norm(base_point) norm_point = self.embedding_metric.norm(point) inner_prod = self.embedding_metric.inner_product(base_point, point) cos_angle = inner_prod / (norm_base_point * norm_point) cos_angle = gs.clip(cos_angle, -1., 1.) angle = gs.arccos(cos_angle) angle = gs.to_ndarray(angle, to_ndim=1) angle = gs.to_ndarray(angle, to_ndim=2, axis=1) mask_0 = gs.isclose(angle, 0.) mask_else = gs.equal(mask_0, gs.array(False)) mask_0_float = gs.cast(mask_0, gs.float32) mask_else_float = gs.cast(mask_else, gs.float32) coef_1 = gs.zeros_like(angle) coef_2 = gs.zeros_like(angle) coef_1 += mask_0_float * (1. + INV_SIN_TAYLOR_COEFFS[1] * angle**2 + INV_SIN_TAYLOR_COEFFS[3] * angle**4 + INV_SIN_TAYLOR_COEFFS[5] * angle**6 + INV_SIN_TAYLOR_COEFFS[7] * angle**8) coef_2 += mask_0_float * (1. + INV_TAN_TAYLOR_COEFFS[1] * angle**2 + INV_TAN_TAYLOR_COEFFS[3] * angle**4 + INV_TAN_TAYLOR_COEFFS[5] * angle**6 + INV_TAN_TAYLOR_COEFFS[7] * angle**8) # This avoids division by 0. angle += mask_0_float * 1. coef_1 += mask_else_float * angle / gs.sin(angle) coef_2 += mask_else_float * angle / gs.tan(angle) log = (gs.einsum('ni,nj->nj', coef_1, point) - gs.einsum('ni,nj->nj', coef_2, base_point)) mask_same_values = gs.isclose(point, base_point) mask_else = gs.equal(mask_same_values, gs.array(False)) mask_else_float = gs.cast(mask_else, gs.float32) mask_else_float = gs.to_ndarray(mask_else_float, to_ndim=1) mask_else_float = gs.to_ndarray(mask_else_float, to_ndim=2) mask_not_same_points = gs.sum(mask_else_float, axis=1) mask_same_points = gs.isclose(mask_not_same_points, 0.) mask_same_points = gs.cast(mask_same_points, gs.float32) mask_same_points = gs.to_ndarray(mask_same_points, to_ndim=2, axis=1) mask_same_points_float = gs.cast(mask_same_points, gs.float32) log -= mask_same_points_float * log return log
def geodesic(self, initial_point, end_point=None, initial_tangent_vec=None, point_type='vector'): """Generate parameterized function for the geodesic curve. Geodesic curve defined by either: - an initial point and an initial tangent vector, - an initial point and an end point. Parameters ---------- initial_point : array-like, shape=[n_samples, dimension] Point on the manifold, initial point of the geodesic. end_point : array-like, shape=[n_samples, dimension], optional Point on the manifold, end point of the geodesic. If None, an initial tangent vector must be given. initial_tangent_vec : array-like, shape=[n_samples, dimension], optional Tangent vector at base point, the initial speed of the geodesics. If None, an end point must be given and a logarithm is computed. point_type : str, {'vector', 'matrix'} The type of point. Returns ------- path : callable The time parameterized geodesic curve. """ point_ndim = 1 if point_type == 'matrix': point_ndim = 2 initial_point = gs.to_ndarray(initial_point, to_ndim=point_ndim + 1) if end_point is None and initial_tangent_vec is None: raise ValueError('Specify an end point or an initial tangent ' 'vector to define the geodesic.') if end_point is not None: end_point = gs.to_ndarray(end_point, to_ndim=point_ndim + 1) shooting_tangent_vec = self.log(point=end_point, base_point=initial_point) if initial_tangent_vec is not None: assert gs.allclose(shooting_tangent_vec, initial_tangent_vec) initial_tangent_vec = shooting_tangent_vec initial_tangent_vec = gs.array(initial_tangent_vec) initial_tangent_vec = gs.to_ndarray(initial_tangent_vec, to_ndim=point_ndim + 1) def path(t): """Generate parameterized function for geodesic curve. Parameters ---------- t : array-like, shape=[n_points,] Times at which to compute points of the geodesics. """ t = gs.array(t) t = gs.cast(t, gs.float32) t = gs.to_ndarray(t, to_ndim=1) t = gs.to_ndarray(t, to_ndim=2, axis=1) new_initial_point = gs.to_ndarray(initial_point, to_ndim=point_ndim + 1) new_initial_tangent_vec = gs.to_ndarray(initial_tangent_vec, to_ndim=point_ndim + 1) if point_type == 'vector': tangent_vecs = gs.einsum('il,nk->ik', t, new_initial_tangent_vec) elif point_type == 'matrix': tangent_vecs = gs.einsum('il,nkm->ikm', t, new_initial_tangent_vec) point_at_time_t = self.exp(tangent_vec=tangent_vecs, base_point=new_initial_point) return point_at_time_t return path
def test_is_vector_vectorized(self): vector = gs.array([1.3, 3.3]) result = self.is_vector_vectorized(vector) expected = True self.assertAllClose(result, expected)
def test_cholesky_factor_belongs(self, n, mat): result = SPDMatrices(n).cholesky_factor(gs.array(mat)) self.assertAllClose( gs.all(PositiveLowerTriangularMatrices(n).belongs(result)), True )