Source code for evomap.mapping._optim

"""
Core functions shared within mapping module. Mostly related to different
optimization routines implemented and adjusted for mapping. 
"""

import numpy as np
EPSILON = 1e-12

[docs] class Error(Exception): """Base class for other exceptions""" pass
[docs] class DivergingGradientError(Error): """Raised when the input value is too small""" pass
[docs] def gradient_descent_with_momentum(objective, init, n_iter, start_iter = 0, n_iter_check = 50, momentum = .8, eta = 50, min_grad_norm = 1e-7, verbose = 0, method_str = "", args = None, kwargs = None): """ Gradient descent with momentum. Optimize the objective function using momentum-based gradient descent, as used, for instance, in t-SNE. Parameters ---------- objective : callable Function to be optimized. Expected to return the function value and the gradient when called. See examples for exact syntax. init : ndarray of shape (n_samples, n_dims) _description_ n_iter : int Total number of gradient descent iterations. start_iter : int, optional Startint iteration, if optimization (re-)starts at a later stage , by default 0 n_iter_check : int, optional Interval in which cost values are reported, by default 50 momentum : float, optional Momentum factor, by default .8 eta : int, optional Learning rate, by default 50 min_grad_norm : float, optional Error tolerance, by default 1e-7 verbose : int, optional Level of verbosity, by default 0 method_str : str, optional Method label, by default "" args : list, optional Arguments passed to the objective function, by default None kwargs : dict, optional Keyword arguments passed to the objective function, by default None Returns ------- ndarray of shape (n_samples, n_dims) Final map coordinates float Final cost function value """ if args is None: args = [] if kwargs is None: kwargs = {} #------------- Initialize Optimization Variables ---------------------# Y = init.copy() iY = np.zeros_like(Y) # Step sizes gains = np.ones_like(Y) if verbose > 1: print("[{0}] Gradient descent with Momentum: {1}".format(method_str, momentum)) #------------- Run Gradient Descent ---------------------# for iter in range(start_iter, n_iter): report_progress = (iter + 1) % n_iter_check == 0 kwargs["compute_error"] = True kwargs["compute_grad"] = True error, grad = objective(Y, *args, **kwargs) if np.any(grad >= 1/EPSILON): print('[{0}] Diverging gradient norm at iteration {1}'.format(method_str, iter+1)) raise DivergingGradientError() dec = np.sign(iY) == np.sign(grad) inc = np.invert(dec) gains[inc] += .2 gains[dec] *= .8 iY = momentum * iY - eta * (gains * grad) Y = Y + iY grad_norm = np.linalg.norm(grad) if report_progress: if verbose > 1: report_optim_progress( iter = iter, method_str = method_str, cost = error, grad_norm = grad_norm) if grad_norm <= min_grad_norm: if verbose > 0: print("[{0}] Iteration {1}: gradient norm vanished.".format(method_str, iter+1, grad_norm)) break elif iter==n_iter-1: if verbose > 0: print("[{0}] Maximum number of iterations reached. Final cost: {1:.2f}".format(method_str, error)) return Y, error
[docs] def report_optim_progress(iter, method_str, cost, grad_norm = None): """Print optimization progress. Parameters ---------- iter : int Current iteration. method_str : str Method label. cost : float Current cost function value grad_norm : float, optional Gradient norm, by default None """ if cost < 1e3: outstr = "[{0}] Iteration {1} -- Cost: {2:.2f}".format( method_str, iter+1, cost) else: outstr = "[{0}] Iteration {1} -- Cost: {2:.2e}".format( method_str, iter+1, cost) if not grad_norm is None: if grad_norm < 1e3: outstr += " -- Gradient Norm: {0:.4f}".format(grad_norm) else: outstr += " -- Gradient Norm: {0:.4e}".format(grad_norm) print(outstr)