def _fit_extrinsic(self, X, y, weights=None, compute_training_score=False): """Estimate the parameters using the extrinsic gradient descent. Estimate the intercept and the coefficient defining the geodesic regression model, using the extrinsic gradient. Parameters ---------- X : {array-like, sparse matrix}, shape=[...,}] Training input samples. y : array-like, shape=[..., {dim, [n,n]}] Training target values. weights : array-like, shape=[...,] Weights associated to the points. Optional, default: None. compute_training_score : bool Whether to compute R^2. Optional, default: False. Returns ------- self : object Returns self. """ shape = ( y.shape[-1:] if self.space.default_point_type == "vector" else y.shape[-2:] ) intercept_init, coef_init = self.initialize_parameters(y) intercept_hat = self.space.projection(intercept_init) coef_hat = self.space.to_tangent(coef_init, intercept_hat) initial_guess = gs.vstack([gs.flatten(intercept_hat), gs.flatten(coef_hat)]) objective_with_grad = gs.autodiff.value_and_grad( lambda param: self._loss(X, y, param, shape, weights), to_numpy=True ) res = minimize( objective_with_grad, initial_guess, method="CG", jac=True, options={"disp": self.verbose, "maxiter": self.max_iter}, tol=self.tol, ) intercept_hat, coef_hat = gs.split(gs.array(res.x), 2) intercept_hat = gs.reshape(intercept_hat, shape) intercept_hat = gs.cast(intercept_hat, dtype=y.dtype) coef_hat = gs.reshape(coef_hat, shape) coef_hat = gs.cast(coef_hat, dtype=y.dtype) self.intercept_ = self.space.projection(intercept_hat) self.coef_ = self.space.to_tangent(coef_hat, self.intercept_) if compute_training_score: variance = gs.sum(self.metric.squared_dist(y, self.intercept_)) self.training_score_ = 1 - 2 * res.fun / variance return self
def setup_method(self): gs.random.seed(1234) self.n_samples = 20 # Set up for hypersphere self.dim_sphere = 4 self.shape_sphere = (self.dim_sphere + 1, ) self.sphere = Hypersphere(dim=self.dim_sphere) X = gs.random.rand(self.n_samples) self.X_sphere = X - gs.mean(X) self.intercept_sphere_true = self.sphere.random_point() self.coef_sphere_true = self.sphere.projection( gs.random.rand(self.dim_sphere + 1)) self.y_sphere = self.sphere.metric.exp( self.X_sphere[:, None] * self.coef_sphere_true, base_point=self.intercept_sphere_true, ) self.param_sphere_true = gs.vstack( [self.intercept_sphere_true, self.coef_sphere_true]) self.param_sphere_guess = gs.vstack([ self.y_sphere[0], self.sphere.to_tangent(gs.random.normal(size=self.shape_sphere), self.y_sphere[0]), ]) # Set up for special euclidean self.se2 = SpecialEuclidean(n=2) self.metric_se2 = self.se2.left_canonical_metric self.metric_se2.default_point_type = "matrix" self.shape_se2 = (3, 3) X = gs.random.rand(self.n_samples) self.X_se2 = X - gs.mean(X) self.intercept_se2_true = self.se2.random_point() self.coef_se2_true = self.se2.to_tangent( 5.0 * gs.random.rand(*self.shape_se2), self.intercept_se2_true) self.y_se2 = self.metric_se2.exp( self.X_se2[:, None, None] * self.coef_se2_true[None], self.intercept_se2_true, ) self.param_se2_true = gs.vstack([ gs.flatten(self.intercept_se2_true), gs.flatten(self.coef_se2_true), ]) self.param_se2_guess = gs.vstack([ gs.flatten(self.y_se2[0]), gs.flatten( self.se2.to_tangent(gs.random.normal(size=self.shape_se2), self.y_se2[0])), ])
def belongs(self, point, point_type=None): """Evaluate if a point belongs to SE(n). Parameters ---------- point : array-like, shape=[n_samples, {dim, [n + 1, n + 1]}] the point of which to check whether it belongs to SE(n) point_type : str, {'vector', 'matrix'}, optional default: self.default_point_type Returns ------- belongs : array-like, shape=[n_samples, 1] array of booleans indicating whether point belongs to SE(n) """ if point_type == 'vector': n_points, vec_dim = gs.shape(point) belongs = vec_dim == self.dimension belongs = gs.tile([belongs], (point.shape[0], )) belongs = gs.logical_and(belongs, self.rotations.belongs(point[:, :self.n])) return gs.flatten(belongs) if point_type == 'matrix': n_points, point_dim1, point_dim2 = point.shape belongs = (point_dim1 == point_dim2 == self.n + 1) belongs = [belongs] * n_points rotation = point[:, :self.n, :self.n] rot_belongs = self.rotations.belongs(rotation, point_type=point_type) 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=(1, 2)) belongs = gs.logical_and(belongs, all_but_last_zeros) last_term = point[:, self.n:, self.n:] belongs = gs.logical_and(belongs, gs.all(last_term == 1, axis=(1, 2))) return gs.flatten(belongs) raise ValueError('Invalid point_type, expected \'vector\' or ' '\'matrix\'.')
def test_dist_broadcast(self): point_a = gs.array([[0.2, 0.5], [0.3, 0.1]]) point_b = gs.array([[0.3, -0.5], [0.2, 0.2]]) point_c = gs.array([[0.2, 0.3], [0.5, 0.5], [-0.4, 0.1]]) point_d = gs.array([0.1, 0.2, 0.3]) dist_a_b =\ self.manifold.metric.dist_broadcast(point_a, point_b) dist_b_c = gs.flatten( self.manifold.metric.dist_broadcast(point_b, point_c)) result_vect = gs.concatenate((dist_a_b, dist_b_c), axis=0) result_a_b =\ [self.manifold.metric.dist_broadcast(point_a[i], point_b[i]) for i in range(len(point_b))] result_b_c = \ [self.manifold.metric.dist_broadcast(point_b[i], point_c[j]) for i in range(len(point_b)) for j in range(len(point_c)) ] result = result_a_b + result_b_c result = gs.stack(result, axis=0) self.assertAllClose(result_vect, result) with self.assertRaises(ValueError): self.manifold.metric.dist_broadcast(point_a, point_d)
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.all(last_term == 1, axis=(-2, -1))) if point.ndim == 2: return gs.squeeze(belongs) return gs.flatten(belongs)
def _replace_values(self, samples, new_samples, indcs): replaced_indices = [ i for i, is_replaced in enumerate(indcs) if is_replaced ] value_indices = list( product(replaced_indices, range(self.n), range(self.n))) return gs.assignment(samples, gs.flatten(new_samples), value_indices)
def log(self, point, base_point, n_steps=N_STEPS, step='euler', max_iter=25, verbose=False, tol=1e-6): """Compute logarithm map associated to the affine connection. Solve the boundary value problem associated to the geodesic equation using the Christoffel symbols and conjugate gradient descent. Parameters ---------- point : array-like, shape=[..., dim] Point on the manifold. base_point : array-like, shape=[..., dim] Point on the manifold. n_steps : int Number of discrete time steps to take in the integration. Optional, default: N_STEPS. step : str, {'euler', 'rk4'} Numerical scheme to use for integration. Optional, default: 'euler'. max_iter verbose tol Returns ------- tangent_vec : array-like, shape=[..., dim] Tangent vector at the base point. """ max_shape = point.shape if point.ndim == 3 else base_point.shape def objective(velocity): """Define the objective function.""" velocity = gs.array(velocity) velocity = gs.cast(velocity, dtype=base_point.dtype) velocity = gs.reshape(velocity, max_shape) delta = self.exp(velocity, base_point, n_steps, step) - point return gs.sum(delta**2) objective_with_grad = gs.autograd.value_and_grad(objective) tangent_vec = gs.flatten(gs.random.rand(*max_shape)) res = minimize(objective_with_grad, tangent_vec, method='L-BFGS-B', jac=True, options={ 'disp': verbose, 'maxiter': max_iter }, tol=tol) tangent_vec = gs.array(res.x) tangent_vec = gs.reshape(tangent_vec, max_shape) tangent_vec = gs.cast(tangent_vec, dtype=base_point.dtype) return tangent_vec
def test_loss_minimization_extrinsic_se2(self): gr = GeodesicRegression( self.se2, metric=self.metric_se2, center_X=False, method="extrinsic", max_iter=50, init_step_size=0.1, verbose=True, ) def loss_of_param(param): return gr._loss(self.X_se2, self.y_se2, param, self.shape_se2) objective_with_grad = gs.autodiff.value_and_grad(loss_of_param, to_numpy=True) res = minimize( objective_with_grad, gs.flatten(self.param_se2_guess), method="CG", jac=True, options={ "disp": True, "maxiter": 50 }, ) self.assertAllClose(gs.array(res.x).shape, (18, )) self.assertAllClose(res.fun, 0.0, atol=1e-6) # Cast required because minimization happens in scipy in float64 param_hat = gs.cast(gs.array(res.x), self.param_se2_true.dtype) intercept_hat, coef_hat = gs.split(param_hat, 2) intercept_hat = gs.reshape(intercept_hat, self.shape_se2) coef_hat = gs.reshape(coef_hat, self.shape_se2) intercept_hat = self.se2.projection(intercept_hat) coef_hat = self.se2.to_tangent(coef_hat, intercept_hat) self.assertAllClose(intercept_hat, self.intercept_se2_true, atol=1e-4) tangent_vec_of_transport = self.se2.metric.log( self.intercept_se2_true, base_point=intercept_hat) transported_coef_hat = self.se2.metric.parallel_transport( tangent_vec=coef_hat, base_point=intercept_hat, direction=tangent_vec_of_transport, ) self.assertAllClose(transported_coef_hat, self.coef_se2_true, atol=0.6)
def test_loss_minimization_extrinsic_hypersphere(self): """Minimize loss from noiseless data.""" gr = GeodesicRegression(self.sphere, regularization=0) def loss_of_param(param): return gr._loss(self.X_sphere, self.y_sphere, param, self.shape_sphere) objective_with_grad = gs.autodiff.value_and_grad(loss_of_param, to_numpy=True) initial_guess = gs.flatten(self.param_sphere_guess) res = minimize( objective_with_grad, initial_guess, method="CG", jac=True, tol=10 * gs.atol, options={ "disp": True, "maxiter": 50 }, ) self.assertAllClose( gs.array(res.x).shape, ((self.dim_sphere + 1) * 2, )) self.assertAllClose(res.fun, 0.0, atol=5e-3) # Cast required because minimization happens in scipy in float64 param_hat = gs.cast(gs.array(res.x), self.param_sphere_true.dtype) intercept_hat, coef_hat = gs.split(param_hat, 2) intercept_hat = self.sphere.projection(intercept_hat) coef_hat = self.sphere.to_tangent(coef_hat, intercept_hat) self.assertAllClose(intercept_hat, self.intercept_sphere_true, atol=5e-2) tangent_vec_of_transport = self.sphere.metric.log( self.intercept_sphere_true, base_point=intercept_hat) transported_coef_hat = self.sphere.metric.parallel_transport( tangent_vec=coef_hat, base_point=intercept_hat, direction=tangent_vec_of_transport, ) self.assertAllClose(transported_coef_hat, self.coef_sphere_true, atol=0.6)
def align(self, point, base_point, max_iter=25, verbose=False, tol=gs.atol): """Align point to base_point. Find the optimal group element g such that the base point and point.g are well positioned, meaning that the total space distance is minimized. This also means that the geodesic joining the base point and the aligned point is horizontal. By default, this is solved by a gradient descent in the Lie algebra. Parameters ---------- point : array-like, shape=[..., {ambient_dim, [n, n]}] Point on the manifold. base_point : array-like, shape=[..., {ambient_dim, [n, n]}] Point on the manifold. max_iter : int Maximum number of gradient steps. Optional, default : 25. verbose : bool Verbosity level. Optional, default : False. tol : float Tolerance for the stopping criterion. Optional, default : backend atol Returns ------- aligned : array-like, shape=[..., {ambient_dim, [n, n]}] Action of the optimal g on point. """ group = self.group initial_distance = self.ambient_metric.squared_dist(point, base_point) if isinstance(initial_distance, float) or initial_distance.shape == (): n_samples = 1 else: n_samples = len(initial_distance) max_shape = (n_samples, group.dim) if n_samples > 1 else \ (group.dim, ) def wrap(param): """Wrap a parameter vector to a group element.""" algebra_elt = gs.array(param) algebra_elt = gs.cast(algebra_elt, dtype=base_point.dtype) algebra_elt = group.lie_algebra.matrix_representation(algebra_elt) group_elt = group.exp(algebra_elt) return self.group_action(point, group_elt) objective_with_grad = gs.autograd.value_and_grad( lambda param: self.ambient_metric.squared_dist( wrap(param), base_point)) tangent_vec = gs.flatten(gs.random.rand(*max_shape)) res = minimize(objective_with_grad, tangent_vec, method='L-BFGS-B', jac=True, options={ 'disp': verbose, 'maxiter': max_iter }, tol=tol) return wrap(res.x)
def embed(self, graph): """Compute embedding. Optimize a loss function to obtain a representable embedding. Parameters ---------- graph : object An instance of the Graph class. Returns ------- embeddings : array-like, shape=[n_samples, dim] Return the embedding of the data. Each data sample is represented as a point belonging to the manifold. """ nb_vertices_by_edges = [len(e_2) for _, e_2 in graph.edges.items()] logging.info("Number of edges: %s", len(graph.edges)) logging.info( "Mean vertices by edges: %s", (sum(nb_vertices_by_edges, 0) / len(graph.edges)), ) negative_table_parameter = 5 negative_sampling_table = [] for i, nb_v in enumerate(nb_vertices_by_edges): negative_sampling_table += ([i] * int( (nb_v**(3.0 / 4.0))) * negative_table_parameter) negative_sampling_table = gs.array(negative_sampling_table) random_walks = graph.random_walk() embeddings = gs.random.normal(size=(graph.n_nodes, self.manifold.dim)) embeddings = embeddings * 0.2 for epoch in range(self.max_epochs): total_loss = [] for path in random_walks: for example_index, one_path in enumerate(path): context_index = path[ max(0, example_index - self.n_context):min(example_index + self.n_context, len(path))] negative_index = gs.random.randint( negative_sampling_table.shape[0], size=(len(context_index), self.n_negative), ) negative_index = gs.expand_dims(gs.flatten(negative_index), axis=-1) negative_index = gs.get_slice(negative_sampling_table, negative_index) example_embedding = embeddings[gs.cast(one_path, dtype=gs.int64)] for one_context_i, one_negative_i in zip( context_index, negative_index): context_embedding = embeddings[one_context_i] negative_embedding = gs.get_slice( embeddings, gs.squeeze(gs.cast(one_negative_i, dtype=gs.int64)), ) l, g_ex = self.loss(example_embedding, context_embedding, negative_embedding) total_loss.append(l) example_to_update = embeddings[one_path] valeur = self.manifold.metric.exp( -self.lr * g_ex, example_to_update) embeddings = gs.assignment( embeddings, valeur, gs.to_ndarray(one_path, to_ndim=1), axis=1, ) logging.info( "iteration %d loss_value %f", epoch, sum(total_loss, 0) / len(total_loss), ) return embeddings
def _fit_riemannian(self, X, y, weights=None, compute_training_score=False): """Estimate the parameters using a Riemannian gradient descent. Estimate the intercept and the coefficient defining the geodesic regression model, using the Riemannian gradient. Parameters ---------- X : {array-like, sparse matrix}, shape=[...,}] Training input samples. y : array-like, shape=[..., {dim, [n,n]}] Training target values. weights : array-like, shape=[...,] Weights associated to the points. Optional, default: None. compute_training_score : bool Whether to compute R^2. Optional, default: False. Returns ------- self : object Returns self. """ shape = (y.shape[-1:] if self.space.default_point_type == "vector" else y.shape[-2:]) if hasattr(self.metric, "parallel_transport"): def vector_transport(tan_a, tan_b, base_point, _): return self.metric.parallel_transport(tan_a, base_point, tan_b) else: def vector_transport(tan_a, _, __, point): return self.space.to_tangent(tan_a, point) objective_with_grad = gs.autodiff.value_and_grad( lambda params: self._loss(X, y, params, shape, weights)) lr = self.init_step_size intercept_init, coef_init = self.initialize_parameters(y) intercept_hat = intercept_hat_new = self.space.projection( intercept_init) coef_hat = coef_hat_new = self.space.to_tangent( coef_init, intercept_hat) param = gs.vstack([gs.flatten(intercept_hat), gs.flatten(coef_hat)]) current_loss = [math.inf] current_grad = gs.zeros_like(param) current_iter = i = 0 for i in range(self.max_iter): loss, grad = objective_with_grad(param) if gs.any(gs.isnan(grad)): logging.warning( f"NaN encountered in gradient at iter {current_iter}") lr /= 2 grad = current_grad elif loss >= current_loss[-1] and i > 0: lr /= 2 else: if not current_iter % 5: lr *= 2 coef_hat = coef_hat_new intercept_hat = intercept_hat_new current_iter += 1 if abs(loss - current_loss[-1]) < self.tol: if self.verbose: logging.info( f"Tolerance threshold reached at iter {current_iter}") break grad_intercept, grad_coef = gs.split(grad, 2) riem_grad_intercept = self.space.to_tangent( gs.reshape(grad_intercept, shape), intercept_hat) riem_grad_coef = self.space.to_tangent( gs.reshape(grad_coef, shape), intercept_hat) intercept_hat_new = self.metric.exp(-lr * riem_grad_intercept, intercept_hat) coef_hat_new = vector_transport( coef_hat - lr * riem_grad_coef, -lr * riem_grad_intercept, intercept_hat, intercept_hat_new, ) param = gs.vstack( [gs.flatten(intercept_hat_new), gs.flatten(coef_hat_new)]) current_loss.append(loss) current_grad = grad self.intercept_ = self.space.projection(intercept_hat) self.coef_ = self.space.to_tangent(coef_hat, self.intercept_) if self.verbose: logging.info(f"Number of gradient evaluations: {i}, " f"Number of gradient iterations: {current_iter}" f" loss at termination: {current_loss[-1]}") if compute_training_score: variance = gs.sum(self.metric.squared_dist(y, self.intercept_)) self.training_score_ = 1 - 2 * current_loss[-1] / variance return self
def setup_method(self): gs.random.seed(1234) self.n_samples = 20 # Set up for euclidean self.dim_eucl = 3 self.shape_eucl = (self.dim_eucl, ) self.eucl = Euclidean(dim=self.dim_eucl) X = gs.random.rand(self.n_samples) self.X_eucl = X - gs.mean(X) self.intercept_eucl_true = self.eucl.random_point() self.coef_eucl_true = self.eucl.random_point() self.y_eucl = (self.intercept_eucl_true + self.X_eucl[:, None] * self.coef_eucl_true) self.param_eucl_true = gs.vstack( [self.intercept_eucl_true, self.coef_eucl_true]) self.param_eucl_guess = gs.vstack([ self.y_eucl[0], self.y_eucl[0] + gs.random.normal(size=self.shape_eucl) ]) # Set up for hypersphere self.dim_sphere = 4 self.shape_sphere = (self.dim_sphere + 1, ) self.sphere = Hypersphere(dim=self.dim_sphere) X = gs.random.rand(self.n_samples) self.X_sphere = X - gs.mean(X) self.intercept_sphere_true = self.sphere.random_point() self.coef_sphere_true = self.sphere.projection( gs.random.rand(self.dim_sphere + 1)) self.y_sphere = self.sphere.metric.exp( self.X_sphere[:, None] * self.coef_sphere_true, base_point=self.intercept_sphere_true, ) self.param_sphere_true = gs.vstack( [self.intercept_sphere_true, self.coef_sphere_true]) self.param_sphere_guess = gs.vstack([ self.y_sphere[0], self.sphere.to_tangent(gs.random.normal(size=self.shape_sphere), self.y_sphere[0]), ]) # Set up for special euclidean self.se2 = SpecialEuclidean(n=2) self.metric_se2 = self.se2.left_canonical_metric self.metric_se2.default_point_type = "matrix" self.shape_se2 = (3, 3) X = gs.random.rand(self.n_samples) self.X_se2 = X - gs.mean(X) self.intercept_se2_true = self.se2.random_point() self.coef_se2_true = self.se2.to_tangent( 5.0 * gs.random.rand(*self.shape_se2), self.intercept_se2_true) self.y_se2 = self.metric_se2.exp( self.X_se2[:, None, None] * self.coef_se2_true[None], self.intercept_se2_true, ) self.param_se2_true = gs.vstack([ gs.flatten(self.intercept_se2_true), gs.flatten(self.coef_se2_true), ]) self.param_se2_guess = gs.vstack([ gs.flatten(self.y_se2[0]), gs.flatten( self.se2.to_tangent(gs.random.normal(size=self.shape_se2), self.y_se2[0])), ]) # Set up for discrete curves n_sampling_points = 8 self.curves_2d = DiscreteCurves(R2) self.metric_curves_2d = self.curves_2d.srv_metric self.metric_curves_2d.default_point_type = "matrix" self.shape_curves_2d = (n_sampling_points, 2) X = gs.random.rand(self.n_samples) self.X_curves_2d = X - gs.mean(X) self.intercept_curves_2d_true = self.curves_2d.random_point( n_sampling_points=n_sampling_points) self.coef_curves_2d_true = self.curves_2d.to_tangent( 5.0 * gs.random.rand(*self.shape_curves_2d), self.intercept_curves_2d_true) # Added because of GitHub issue #1575 intercept_curves_2d_true_repeated = gs.tile( gs.expand_dims(self.intercept_curves_2d_true, axis=0), (self.n_samples, 1, 1), ) self.y_curves_2d = self.metric_curves_2d.exp( self.X_curves_2d[:, None, None] * self.coef_curves_2d_true[None], intercept_curves_2d_true_repeated, ) self.param_curves_2d_true = gs.vstack([ gs.flatten(self.intercept_curves_2d_true), gs.flatten(self.coef_curves_2d_true), ]) self.param_curves_2d_guess = gs.vstack([ gs.flatten(self.y_curves_2d[0]), gs.flatten( self.curves_2d.to_tangent( gs.random.normal(size=self.shape_curves_2d), self.y_curves_2d[0])), ])