def _iterate_over_manifolds(self, func, args, intrinsic=False): cum_index = ( gs.cumsum(self.dims)[:-1] if intrinsic else gs.cumsum([k + 1 for k in self.dims]) ) arguments = {} float_args = {} for key, value in args.items(): if not isinstance(value, float): arguments[key] = gs.split(value, cum_index, axis=-1) else: float_args[key] = value args_list = [ {key: arguments[key][j] for key in arguments} for j in range(len(self.manifolds)) ] pool = joblib.Parallel(n_jobs=self.n_jobs) out = pool( joblib.delayed(self._get_method)( self.manifolds[i], func, {**args_list[i], **float_args} ) for i in range(len(self.manifolds)) ) return out
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 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 _iterate_over_metrics( self, func, args, intrinsic=False): cum_index = gs.cumsum(self.dims, axis=0)[:-1] if intrinsic else \ gs.cumsum(gs.array([k + 1 for k in self.dims]), axis=0) arguments = { key: gs.split(args[key], cum_index, axis=1) for key in args.keys()} args_list = [{key: arguments[key][j] for key in args.keys()} for j in range(self.n_metrics)] pool = joblib.Parallel(n_jobs=self.n_jobs, prefer='threads') out = pool( joblib.delayed(self._get_method)( self.metrics[i], func, args_list[i]) for i in range( self.n_metrics)) return out
def _iterate_over_manifolds( self, func, args, intrinsic=False): cum_index = gs.cumsum(self.dims)[:-1] if intrinsic else \ gs.cumsum([k + 1 for k in self.dims]) arguments = {key: gs.split( args[key], cum_index, axis=1) for key in args.keys()} args_list = [{key: arguments[key][j] for key in args.keys()} for j in range(len(self.manifolds))] pool = joblib.Parallel(n_jobs=self.n_jobs) out = pool( joblib.delayed(self._get_method)( self.manifolds[i], func, args_list[i]) for i in range( len(self.manifolds))) return out
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 _loss(self, X, y, param, shape, weights=None): """Compute the loss associated to the geodesic regression. Parameters ---------- X : {array-like, sparse matrix}, shape=[...,}] Training input samples. y : array-like, shape=[..., {dim, [n,n]}] Training target values. param : array-like, shape=[2, {dim, [n,n]}] Parameters intercept and coef of the geodesic regression, vertically stacked. weights : array-like, shape=[...,] Weights associated to the points. Optional, default: None. Returns ------- _ : float Loss. """ intercept, coef = gs.split(param, 2) intercept = gs.reshape(intercept, shape) coef = gs.reshape(coef, shape) intercept = gs.cast(intercept, dtype=y.dtype) coef = gs.cast(coef, dtype=y.dtype) if self.method == "extrinsic": base_point = self.space.projection(intercept) penalty = self.regularization * gs.sum((base_point - intercept)**2) else: base_point = intercept penalty = 0 tangent_vec = self.space.to_tangent(coef, base_point) distances = self.metric.squared_dist( self._model(X, tangent_vec, base_point), y) if weights is None: weights = 1.0 return 1.0 / 2.0 * gs.sum(weights * distances) + penalty
def test_split(self): x = gs.array([0.1, 0.2, 0.3, 0.4]) result = gs.split(x, 2) expected = _np.split(x, 2) for res, exp in zip(result, expected): self.assertAllClose(res, exp)
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