Esempio n. 1
0
    def _build_optimizer(self) -> None:
        """
        Build the TensorFlow optimizer, wrapper to the scipy optimization
        algorithms.
        """
        # Extract the TF variables that get optimized in the risk minimization
        t_vars = tf.trainable_variables()
        risk_vars = [var for var in t_vars if 'risk_main' in var.name]
        # Dictionary containing the bounds on the TensorFlow Variables
        var_to_bounds = {
            risk_vars[0]: (self.trainable.parameter_lower_bounds,
                           self.trainable.parameter_upper_bounds),
            risk_vars[1]: (self.state_lower_bounds, self.state_upper_bounds)
        }
        if self.train_gamma:
            var_to_bounds[risk_vars[2]] = (self.gamma_bounds[0],
                                           self.gamma_bounds[1])
        if self.train_gamma_prime:
            var_to_bounds[risk_vars[3]] = (self.gamma_prime_bounds[0],
                                           self.gamma_prime_bounds[1])

        self.risk_optimizer = ExtendedScipyOptimizerInterface(
            loss=self.risk,
            method=self.optimizer,
            var_list=risk_vars,
            var_to_bounds=var_to_bounds)
        return
 def _train_data_based_gp(self, session: tf.Session()) -> None:
     """
     Performs the GP regression on the data of the system. For each state
     of the system we train a different GP by maximum likelihood to tune
     the kernel hyper-parameters.
     :param session: TensorFlow session used during the optimization.
     """
     # Extract TF variables and select the GP ones
     t_vars = tf.trainable_variables()
     gp_vars = [var for var in t_vars if 'gaussian_process' in var.name]
     # Build the bounds for the GP hyper-parameters
     var_to_bounds = self._build_var_to_bounds_gp()
     # Initialize the TF/scipy optimizer
     self.data_gp_optimizer = ExtendedScipyOptimizerInterface(
         self.negative_data_loglikelihood, method="L-BFGS-B",
         var_list=gp_vars, var_to_bounds=var_to_bounds)
     # Optimize
     self.data_gp_optimizer.basinhopping(session, n_iter=50, stepsize=0.05)
     return
class GPApproxRiskMinimization(object):
    """
    Class that implements Approximate (i.e QFFs are used) marginal likelihood minimization for hyperparameter training of GP.
    """

    def __init__(self, system_data: np.array, t_data: np.array,
                 gp_kernel: str = 'RBF',
                 single_gp: bool = False,
                 state_normalization: bool = True,
                 time_normalization: bool = False,
                 QFF_approx : int = 40,
                 Approx_method: str = "QFF"):
        """
        Constructor
        :param system_data: numpy array containing the noisy observations of the state values of the system, size is [n_states, n_points];
        :param t_data: numpy array containing the time stamps corresponding to the observations passed as system_data;
        :param gp_kernel: string indicating which kernel to use in the GP. Valid options are ONLY 'RBF' for the current implementation;
        :param single_gp: boolean, indicates whether to use a single set of GP hyperparameters for each state;
        :param state_normalization: boolean, indicates whether to normalize the states values;
        :param time_normalization: boolean, indicates whether to normalize the time stamps;
        :param QFF_approx: int, the order of the quadrature scheme
        """
        # Save arguments
        self.Approx_method = Approx_method
        self.system_data = np.copy(system_data)
        self.t_data = np.copy(t_data).reshape(-1, 1)
        self.dim, self.n_p = system_data.shape
        self.gp_kernel = gp_kernel
        if self.gp_kernel != 'RBF':
            raise NotImplementedError("Only RBF kernel is currently implemented for use with QFFs")

        self.single_gp = single_gp

        # Compute the data for the standardization (means and standard deviations)
        self._compute_standardization_data(state_normalization,
                                           time_normalization)
        # Build the necessary TensorFlow tensors
        self._build_tf_data()

        # Initialization of TF operations
        self.init = None
        self.negative_data_loglikelihood = None
        self.QFF_approx = QFF_approx
        return

    def _compute_standardization_data(self, state_normalization: bool,
                                      time_normalization: bool) -> None:
        """
        Compute the means and the standard deviations for data standardization,
        used in the GP hyperparameter training.
        """
        # Compute mean and std dev of the state and time values
        if state_normalization:
            self.system_data_means = np.mean(self.system_data,
                                             axis=1).reshape(self.dim, 1)
            self.system_data_std_dev = np.std(self.system_data,
                                              axis=1).reshape(self.dim, 1)
        else:
            self.system_data_means = np.zeros([self.dim, 1])
            self.system_data_std_dev = np.ones([self.dim, 1])
        if time_normalization:
            self.t_data_mean = np.mean(self.t_data)
            self.t_data_std_dev = np.std(self.t_data)
        else:
            self.t_data_mean = 0.0
            self.t_data_std_dev = 1.0
        # Normalize states and time
        self.normalized_states = (self.system_data - self.system_data_means) / \
            self.system_data_std_dev
        self.normalized_t_data = (self.t_data - self.t_data_mean) / \
            self.t_data_std_dev
        return

    def _build_tf_data(self) -> None:
        """
        Initialize all the TensorFlow constants needed by the pipeline.
        """
        self.system = tf.constant(self.normalized_states, dtype=tf.float64)
        self.t = tf.constant(self.normalized_t_data, dtype=tf.float64)
        self.system_means = tf.constant(self.system_data_means,
                                        dtype=tf.float64,
                                        shape=[self.dim, 1])
        self.system_std_dev = tf.constant(self.system_data_std_dev,
                                          dtype=tf.float64,
                                          shape=[self.dim, 1])
        self.t_mean = tf.constant(self.t_data_mean, dtype=tf.float64)
        self.t_std_dev = tf.constant(self.t_data_std_dev, dtype=tf.float64)
        self.n_points = tf.constant(self.n_p, dtype=tf.float64)
        self.dimensionality = tf.constant(self.dim, dtype=tf.int32)
        return

    @staticmethod
    def _build_var_to_bounds_gp() -> dict:
        """
        Builds the dictionary containing the bounds that will be applied to the
        variable in the Gaussian Process model.
        :return: the dictionary variables to bounds.
        """
        # Extract TF variables and select the GP ones
        t_vars = tf.trainable_variables()
        gp_vars = [var for var in t_vars if 'gaussian_process' in var.name]
        # Bounds for the GP hyper-parameters
        gp_kern_lengthscale_bounds = (np.log(1e-6), np.log(100.0))
        gp_kern_variance_bounds = (np.log(1e-6), np.log(100.0))
        gp_kern_likelihood_bounds = (np.log(1e-6), np.log(100.0))
        # Dictionary construction
        var_to_bounds = {gp_vars[0]: gp_kern_lengthscale_bounds,
                         gp_vars[1]: gp_kern_variance_bounds,
                         gp_vars[2]: gp_kern_likelihood_bounds}
        return var_to_bounds

    def _train_data_based_gp(self, session: tf.Session()) -> None:
        """
        Performs the GP regression on the data of the system. For each state
        of the system we train a different GP by maximum likelihood to tune
        the kernel hyper-parameters.
        :param session: TensorFlow session used during the optimization.
        """
        # Extract TF variables and select the GP ones
        t_vars = tf.trainable_variables()
        gp_vars = [var for var in t_vars if 'gaussian_process' in var.name]
        # Build the bounds for the GP hyper-parameters
        var_to_bounds = self._build_var_to_bounds_gp()
        # Initialize the TF/scipy optimizer
        self.data_gp_optimizer = ExtendedScipyOptimizerInterface(
            self.negative_data_loglikelihood, method="L-BFGS-B",
            var_list=gp_vars, var_to_bounds=var_to_bounds)
        # Optimize
        self.data_gp_optimizer.basinhopping(session, n_iter=50, stepsize=0.05)
        return

    def build_model(self) -> None:
        """
        Builds Some common part of the computational graph for the optimization.
        """
        # Gaussian Process Interpolation
        with tf.variable_scope('gaussian_process_kernel'):
            if self.single_gp:
                self.log_lengthscale = tf.Variable(np.log(1.0),
                                                   dtype=tf.float64,
                                                   trainable=True,
                                                   name='log_lengthscale')
                self.log_variance = tf.Variable(np.log(1.0),
                                                dtype=tf.float64,
                                                trainable=True,
                                                name='log_variance')
                self.lengthscales = \
                    tf.exp(self.log_lengthscale)\
                    * tf.ones([self.dimensionality, 1, 1], dtype=tf.float64)
                self.variances = \
                    tf.exp(self.log_variance)\
                    * tf.ones([self.dimensionality, 1, 1], dtype=tf.float64)
                self.likelihood_logvariance = tf.Variable(
                    np.log(1.0), dtype=tf.float64, trainable=True,
                    name='variance_loglik')
                self.likelihood_logvariances =\
                    self.likelihood_logvariance * tf.ones([self.dimensionality,
                                                           1, 1],
                                                          dtype=tf.float64)
            else:
                self.log_lengthscales = tf.Variable(
                    np.log(1.0) * tf.ones([self.dimensionality, 1, 1],
                                          dtype=tf.float64),
                    dtype=tf.float64, trainable=True, name='lengthscales')
                self.log_variances = tf.Variable(
                    tf.ones([self.dimensionality, 1, 1],
                            dtype=tf.float64),
                    dtype=tf.float64, trainable=True, name='variances')
                self.variances = tf.exp(self.log_variances)
                self.lengthscales = tf.exp(self.log_lengthscales)
                self.likelihood_logvariances = tf.Variable(
                    np.log(1.0) * tf.ones([self.dimensionality, 1, 1],
                                          dtype=tf.float64),
                    dtype=tf.float64, trainable=True,
                    name='variances_loglik')
            self.likelihood_variances = tf.exp(self.likelihood_logvariances)
        if self.Approx_method == "QFF": #be careful for inverse lengthscales and sqrt variances
            Z = self.variances*hermite_embeding(self.QFF_approx,self.lengthscales,self.t)
        elif self.Approx_method == "RFF":
            Z = self.variances*RFF_embeding(self.QFF_approx,self.lengthscales,self.t)
        elif self.Approx_method == "RFF_bias":
            Z = self.variances*RFF_embeding_bias(self.QFF_approx,self.lengthscales,self.t)

        Z_t_y=tf.matmul(Z,tf.expand_dims(self.system,-1),transpose_a=True,name='Z_t_y')
        Kernel_inner_dim=tf.matmul(Z,Z,transpose_a=True,name='Kernel_inner_dim') + self.likelihood_variances *tf.eye(self.QFF_approx,dtype=tf.float64)
        inv_Z_t_y=tf.linalg.solve(Kernel_inner_dim,Z_t_y,name='inv_Z_t_y')

        a_vector = tf.matmul(Z_t_y,inv_Z_t_y,transpose_a=True,name='reg_risk_main_term')
        first_term = tf.reduce_sum(tf.reduce_sum(self.system * self.system,axis=1)/tf.squeeze(self.likelihood_variances)) - tf.reduce_sum(a_vector/self.likelihood_variances)

        second_term = tf.reduce_sum(tf.linalg.logdet(Kernel_inner_dim)) + (self.n_points-self.QFF_approx) * tf.reduce_sum( tf.log(self.likelihood_variances))

        self.negative_data_loglikelihood = (0.5 * first_term + 0.5 * second_term)/self.n_points
        return

    def _initialize_variables(self) -> None:
        """
        Initialize all the variables and placeholders in the graph.
        """
        self.init = tf.global_variables_initializer()
        return

    def train(self) -> [int, np.array, np.array, np.array]:
        """
        Trains the GP, i.e tuning the hyperparameters
        Returns the time needed for the optimization, as well as the hyperpameters found
        """
        self._initialize_variables()
        session = tf.Session()
        with session:
            # Start the session
            session.run(self.init)
            # Train the GP
            secs=time.time()
            self._train_data_based_gp(session)
            secs=time.time() -secs
            print("Likelihood is ",session.run(self.negative_data_loglikelihood))
            # Print GP hyperparameters
            print("GP trained ------------------------------------------------")
            lengthscales=1/session.run(self.lengthscales)
            variances=session.run(self.variances)**2
            likelihood_variances=session.run(self.likelihood_variances)
            print("lengthscales:",lengthscales)
            print("variances:",variances)
            print("likelihood_variances:",likelihood_variances)
            res = [secs,lengthscales,variances,likelihood_variances]
            print("-----------------------------------------------------------")
        tf.reset_default_graph()
        return res
Esempio n. 4
0
class ODIN(object):
    """
    Class that implements the main ODIN regression algorithm.
    """
    def __init__(self,
                 trainable: TrainableModel,
                 system_data: np.array,
                 t_data: np.array,
                 gp_kernel: str = 'RBF',
                 use_sec_grads: bool = False,
                 optimizer: str = 'L-BFGS-B',
                 initial_gamma: float = 0.3,
                 initial_gamma_prime: float = 0.3,
                 train_gamma: bool = True,
                 train_gamma_prime: bool = False,
                 gamma_bounds: Union[np.array, list, Tuple] = (1e-6, 100.0),
                 gamma_prime_bounds: Union[np.array, list,
                                           Tuple] = (1e-6, 100.0),
                 state_bounds: np.array = None,
                 basinhopping: bool = True,
                 basinhopping_options: dict = None,
                 single_gp: bool = False,
                 state_normalization: bool = True,
                 time_normalization: bool = True):
        """
        Constructor.
        :param trainable: Trainable model class, as explained and implemented in
        utils.trainable_models;
        :param system_data: numpy array containing the noisy observations of
        the state values of the system, size is [n_states, n_points];
        :param t_data: numpy array containing the time stamps corresponding to
        the observations passed as system_data;
        :param gp_kernel: string indicating which kernel to use in the GP.
        Valid options are 'RBF', 'Matern52', 'Matern32', 'RationalQuadratic',
        'Sigmoid';
        :param use_sec_grads: boolean, indicates whether to use second gradient
        :param optimizer: string indicating which scipy optimizer to use. The
        valid ones are the same that can be passed to scipy.optimize.minimize.
        Notice that some of them will ignore bounds;
        :param initial_gamma: initial value for the gamma parameter.
        :param initial_gamma_prime: initial value for the gamma_prime parameter.
        :param train_gamma: boolean, indicates whether to train of not the
        variable gamma;
        :param train_gamma_prime:boolean, indicates whether to train of not the
        variable gamma_prime;
        :param gamma_bounds: bounds for gamma (a lower bound of at least 1e-6
        is always applied to overcome numerical instabilities);
        :param gamma_prime_bounds: bounds for gamma_prime (a lower bound of at least 1e-6
        is always applied to overcome numerical instabilities);
        :param state_bounds: bounds for the state optimization;
        :param basinhopping: boolean, indicates whether to turn on the scipy
        basinhopping;
        :param basinhopping_options: dictionary containing options for the
        basinhooping algorithm (syntax is the same as scipy's one);
        :param single_gp: boolean, indicates whether to use a single set of GP
        hyperparameters for each state;
        :param state_normalization: boolean, indicates whether to normalize the
        states values before the optimization (notice the parameter values
        theta won't change);
        :param time_normalization: boolean, indicates whether to normalize the
        time stamps before the optimization (notice the parameter values
        theta won't change).
        """
        # Save arguments
        self.trainable = trainable
        self.use_sec_grads = use_sec_grads
        self.system_data = np.copy(system_data)
        self.t_data = np.copy(t_data).reshape(-1, 1)
        self.n_states, self.n_p = system_data.shape
        self.gp_kernel = gp_kernel
        self.optimizer = optimizer
        self.initial_gamma = initial_gamma
        self.initial_gamma_prime = initial_gamma_prime
        self.train_gamma = train_gamma
        self.train_gamma_prime = train_gamma_prime
        self.basinhopping = basinhopping
        self.basinhopping_options = {
            'n_iter': 10,
            'temperature': 1.0,
            'stepsize': 0.05
        }
        if basinhopping_options:
            self.basinhopping_options.update(basinhopping_options)
        self.single_gp = single_gp
        # Build bounds for the states and for gamma
        self._compute_state_bounds(state_bounds)
        self._compute_gamma_bounds(gamma_bounds)
        self._compute_gamma_prime_bounds(gamma_prime_bounds)
        # Compute the data for the standardization (means and standard
        # deviations)
        self._compute_standardization_data(state_normalization,
                                           time_normalization)
        # Build the necessary TensorFlow tensors
        self._build_tf_data()
        # Initialize the Gaussian Process for the derivative model
        self.gaussian_process = GaussianProcess(self.n_states, self.n_p,
                                                self.gp_kernel, self.single_gp,
                                                self.use_sec_grads)
        # Initialization of TF operations
        self.init = None
        self.model_gp_loglikelihood = None
        self.negative_data_loglikelihood = None
        return

    def _compute_state_bounds(self, bounds: np.array) -> None:
        """
        Builds the numpy array that defines the bounds for the states.
        :param bounds: numpy array, sized [n_dim, 2], in which for each
        dimensions we can find respectively lower and upper bounds.
        """
        if bounds is None:
            self.state_bounds = np.inf * np.ones([self.n_states, 2])
            self.state_bounds[:, 0] = -self.state_bounds[:, 0]
        else:
            self.state_bounds = np.array(bounds)
        return

    def _compute_gamma_bounds(self, bounds: Union[np.array, list, Tuple])\
            -> None:
        """
        Builds the numpy array that defines the bounds for gamma.
        :param bounds: of the form (lower_bound, upper_bound).
        """
        self.gamma_bounds = np.array([1.0, 1.0])
        if bounds is None:
            self.gamma_bounds[0] = np.log(1e-6)
            self.gamma_bounds[1] = np.inf
        else:
            self.gamma_bounds[0] = np.log(np.array(bounds[0]))
            self.gamma_bounds[1] = np.log(np.array(bounds[1]))
        return

    def _compute_gamma_prime_bounds(self, bounds: Union[np.array, list, Tuple])\
            -> None:
        """
        Builds the numpy array that defines the bounds for gamma prime.
        :param bounds: of the form (lower_bound, upper_bound).
        """
        self.gamma_prime_bounds = np.array([1.0, 1.0])
        if bounds is None:
            self.gamma_prime_bounds[0] = np.log(1e-6)
            self.gamma_prime_bounds[1] = np.inf
        else:
            self.gamma_prime_bounds[0] = np.log(np.array(bounds[0]))
            self.gamma_prime_bounds[1] = np.log(np.array(bounds[1]))
        return

    def _compute_standardization_data(self, state_normalization: bool,
                                      time_normalization: bool) -> None:
        """
        Compute the means and the standard deviations for data standardization,
        used in the GP regression.
        """
        # Compute mean and std dev of the state and time values
        if state_normalization:
            self.system_data_means = np.mean(self.system_data,
                                             axis=1).reshape(self.n_states, 1)
            self.system_data_std_dev = np.std(self.system_data,
                                              axis=1).reshape(
                                                  self.n_states, 1)
        else:
            self.system_data_means = np.zeros([self.n_states, 1])
            self.system_data_std_dev = np.ones([self.n_states, 1])
        if time_normalization:
            self.t_data_mean = np.mean(self.t_data)
            self.t_data_std_dev = np.std(self.t_data)
        else:
            self.t_data_mean = 0.0
            self.t_data_std_dev = 1.0
        # For the sigmoid kernel the input time values must be positive, i.e.
        # we only divide by the standard deviation
        if self.gp_kernel == 'Sigmoid':
            self.t_data_mean = 0.0
        # Normalize states and time
        self.normalized_states = (self.system_data - self.system_data_means) / \
            self.system_data_std_dev
        self.normalized_t_data = (self.t_data - self.t_data_mean) / \
            self.t_data_std_dev
        return

    def _build_tf_data(self) -> None:
        """
        Initialize all the TensorFlow constants needed in the pipeline.
        """
        self.system = tf.constant(self.normalized_states, dtype=tf.float64)
        self.t = tf.constant(self.normalized_t_data, dtype=tf.float64)
        self.system_means = tf.constant(self.system_data_means,
                                        dtype=tf.float64,
                                        shape=[self.n_states, 1])
        self.system_std_dev = tf.constant(self.system_data_std_dev,
                                          dtype=tf.float64,
                                          shape=[self.n_states, 1])
        self.t_mean = tf.constant(self.t_data_mean, dtype=tf.float64)
        self.t_std_dev = tf.constant(self.t_data_std_dev, dtype=tf.float64)
        self.n_points = tf.constant(self.n_p, dtype=tf.int32)
        self.dimensionality = tf.constant(self.n_states, dtype=tf.int32)
        return

    @staticmethod
    def _build_var_to_bounds_gp() -> dict:
        """
        Builds the dictionary containing the bounds that will be applied to the
        variable in the Gaussian Process model.
        :return: the dictionary variables to bounds.
        """
        # Extract TF variables and select the GP ones
        t_vars = tf.trainable_variables()
        gp_vars = [var for var in t_vars if 'gaussian_process' in var.name]
        # Bounds for the GP hyper-parameters
        gp_kern_lengthscale_bounds = (np.log(1e-6), np.log(100.0))
        gp_kern_variance_bounds = (np.log(1e-6), np.log(100.0))
        gp_kern_likelihood_bounds = (np.log(1e-6), np.log(100.0))
        # Dictionary construction
        var_to_bounds = {
            gp_vars[0]: gp_kern_lengthscale_bounds,
            gp_vars[1]: gp_kern_variance_bounds,
            gp_vars[2]: gp_kern_likelihood_bounds
        }
        return var_to_bounds

    @staticmethod
    def _build_var_to_bounds_gp_sigmoid() -> dict:
        """
        Builds the dictionary containing the bounds that will be applied to the
        variable in the Gaussian Process model (specific for the sigmoid
        kernel).
        :return: the dictionary variables to bounds.
        """
        # Extract TF variables and select the GP ones
        t_vars = tf.trainable_variables()
        gp_vars = [var for var in t_vars if 'gaussian_process' in var.name]
        # Bounds for the GP hyper-parameters
        gp_kern_a_bounds = (np.log(1e-6), np.log(100.0))
        gp_kern_b_bounds = (np.log(1e-6), np.log(100.0))
        gp_kern_variance_bounds = (np.log(1e-6), np.log(100.0))
        gp_kern_likelihood_bounds = (np.log(1e-6), np.log(100.0))
        # Dictionary construction
        var_to_bounds = {
            gp_vars[0]: gp_kern_a_bounds,
            gp_vars[1]: gp_kern_b_bounds,
            gp_vars[2]: gp_kern_variance_bounds,
            gp_vars[3]: gp_kern_likelihood_bounds
        }
        return var_to_bounds

    def _train_data_based_gp(self, session: tf.Session()) -> None:
        """
        Performs a classic GP regression on the data of the system. For each
        state of the system we train a different GP by maximum likelihood to fix
        the kernel hyper-parameters.
        :param session: TensorFlow session used during the optimization.
        """
        # Extract TF variables and select the GP ones
        t_vars = tf.trainable_variables()
        gp_vars = [var for var in t_vars if 'gaussian_process' in var.name]
        # Build the bounds for the GP hyper-parameters
        if self.gp_kernel == 'Sigmoid':
            var_to_bounds = self._build_var_to_bounds_gp_sigmoid()
        else:
            var_to_bounds = self._build_var_to_bounds_gp()
        # Initialize the TF/scipy optimizer
        self.data_gp_optimizer = ExtendedScipyOptimizerInterface(
            self.negative_data_loglikelihood,
            method="L-BFGS-B",
            var_list=gp_vars,
            var_to_bounds=var_to_bounds)
        # Optimize
        self.data_gp_optimizer.basinhopping(session, n_iter=50, stepsize=0.05)
        return

    def _build_states_bounds(self) -> None:
        """
        Builds the tensors for the normalized states that will containing the
        bounds for the constrained optimization.
        """
        # Tile the bounds to get the right dimensions
        state_lower_bounds = self.state_bounds[:, 0].reshape(self.n_states, 1)
        state_lower_bounds = np.tile(state_lower_bounds, [1, self.n_p])
        state_lower_bounds = (state_lower_bounds - self.system_data_means)\
            / self.system_data_std_dev
        state_lower_bounds = state_lower_bounds.reshape(
            [self.n_states, self.n_p])
        state_upper_bounds = self.state_bounds[:, 1].reshape(self.n_states, 1)
        state_upper_bounds = np.tile(state_upper_bounds, [1, self.n_p])
        state_upper_bounds = (state_upper_bounds - self.system_data_means)\
            / self.system_data_std_dev
        state_upper_bounds = state_upper_bounds.reshape(
            [self.n_states, self.n_p])
        self.state_lower_bounds = state_lower_bounds
        self.state_upper_bounds = state_upper_bounds
        return

    def _build_variables(self) -> None:
        """
        Builds the TensorFlow variables with the state values and the gamma
        that will later be optimized.
        """
        with tf.variable_scope('risk_main'):
            self.x = tf.Variable(self.system,
                                 dtype=tf.float64,
                                 trainable=True,
                                 name='states')
            if self.single_gp:
                self.log_gamma = tf.Variable(np.log(self.initial_gamma),
                                             dtype=tf.float64,
                                             trainable=self.train_gamma,
                                             name='log_gamma')
                self.gamma = tf.exp(self.log_gamma)\
                    * tf.ones([self.dimensionality, 1, 1], dtype=tf.float64)
            else:
                self.log_gamma =\
                    tf.Variable(np.log(self.initial_gamma)
                                * tf.ones([self.dimensionality, 1, 1],
                                          dtype=tf.float64),
                                trainable=self.train_gamma,
                                dtype=tf.float64,
                                name='log_gamma')
                self.gamma = tf.exp(self.log_gamma)
            # gamma_prime
            if self.single_gp:
                self.log_gamma_prime = tf.Variable(
                    np.log(self.initial_gamma_prime),
                    dtype=tf.float64,
                    trainable=self.train_gamma_prime,
                    name='log_gamma_prime')
                self.gamma_prime = tf.exp(self.log_gamma_prime)\
                    * tf.ones([self.dimensionality, 1, 1], dtype=tf.float64)
            else:
                self.log_gamma_prime =\
                    tf.Variable(np.log(self.initial_gamma_prime)
                                * tf.ones([self.dimensionality, 1, 1],
                                          dtype=tf.float64),
                                trainable=self.train_gamma_prime,
                                dtype=tf.float64,
                                name='log_gamma_prime')
                self.gamma_prime = tf.exp(self.log_gamma_prime)
        return

    def _build_regularization_risk_term(self) -> tf.Tensor:
        """
        Build the first term of the risk, connected to regularization.
        :return: the TensorFlow Tensor that contains the term.
        """
        a_vector = tf.linalg.solve(
            self.gaussian_process.c_phi_matrices_noiseless,
            tf.expand_dims(self.x, -1))
        risk_term = 0.5 * tf.reduce_sum(self.x * tf.squeeze(a_vector))
        return tf.reduce_sum(risk_term)

    def _build_states_risk_term(self) -> tf.Tensor:
        """
        Build the second term of the risk, connected with the value of the
        states.
        :return: the TensorFlow Tensor that contains the term.
        """
        states_difference = self.system - self.x
        risk_term = tf.reduce_sum(states_difference * states_difference, 1)
        risk_term = risk_term * 0.5 / tf.squeeze(
            self.gaussian_process.likelihood_variances)
        return tf.reduce_sum(risk_term)

    def _build_derivatives_risk_term(self) -> tf.Tensor:
        """
        Build the third term of the risk, connected with the value of the
        derivatives.
        :return: the TensorFlow Tensor that contains the term.
        """
        # Compute model and data-based derivatives
        unnormalized_states = self.x * self.system_std_dev + self.system_means
        model_derivatives = tf.expand_dims(
            self.trainable.compute_gradients(unnormalized_states) /
            self.system_std_dev * self.t_std_dev, -1)
        data_derivatives =\
            self.gaussian_process.compute_posterior_derivative_mean(self.x)
        derivatives_difference = model_derivatives - data_derivatives
        # Compute log_variance on the derivatives
        post_variance =\
            self.gaussian_process.compute_posterior_derivative_variance() +\
            self.gamma * tf.expand_dims(tf.eye(self.n_points,
                                               dtype=tf.float64), 0)
        # Compute risk term
        a_vector = tf.linalg.solve(post_variance, derivatives_difference)
        risk_term = 0.5 * tf.reduce_sum(a_vector * derivatives_difference)
        return risk_term

    def _build_second_derivative_mean(self) -> tf.Tensor:
        """
        Build second order derivative mean.
        :return: the TensorFlow Tensor that contains the mean.
        """
        # compute F
        unnormalized_states = self.x * self.system_std_dev + self.system_means
        grads = self.trainable.compute_gradients(unnormalized_states)

        F = tf.expand_dims(grads / self.system_std_dev * self.t_std_dev,
                           -1)  # F(x, theta)

        # compute m_c
        A_inv = tf.linalg.inv(
            self.gaussian_process.compute_posterior_derivative_variance())
        m_c_1 = tf.expand_dims(tf.eye(self.n_points, dtype=tf.float64),
                               0) * tf.pow(self.gamma, -1.0) + A_inv
        m_c_2 = F / self.gamma + tf.matmul(
            A_inv,
            self.gaussian_process.compute_posterior_derivative_mean(self.x))
        m_c = tf.linalg.solve(m_c_1, m_c_2)

        # m_c_x = [m_c; x]
        m_c_x = tf.concat([tf.expand_dims(self.x, axis=-1), m_c], 1)

        # compute components of D' (D' = s * m^-1)
        s, m = self.gaussian_process.compute_D_prime_parts()

        # D'_1*x + D'_2 * m_c
        post_sec_dev_mean = tf.matmul(s, tf.linalg.solve(m, m_c_x))
        return post_sec_dev_mean

    def _build_second_derivative_variance(self) -> tf.Tensor:
        """
        Build second order derivative variance.
        :return: the TensorFlow Tensor that contains the variance.
        """
        A_prime = self.gaussian_process.compute_A_prime()
        s, m = self.gaussian_process.compute_D_prime_parts()
        D_prime = tf.matmul(s, tf.linalg.inv(m))
        A_inv = tf.linalg.inv(
            self.gaussian_process.compute_posterior_derivative_variance())
        sigma_c_inv = tf.expand_dims(tf.eye(self.n_points, dtype=tf.float64),
                                     0) * tf.pow(self.gamma, -1.0) + A_inv
        D_prime_2 = tf.gather(D_prime,
                              [i for i in range(self.n_p, 2 * self.n_p)],
                              axis=2)
        q = tf.matmul(
            D_prime_2,
            tf.linalg.solve(sigma_c_inv, tf.transpose(D_prime_2,
                                                      perm=[0, 2, 1])))
        post_sec_dev_var = A_prime + self.gamma_prime * tf.expand_dims(
            tf.eye(self.n_points, dtype=tf.float64), 0)
        post_sec_dev_var += q
        return post_sec_dev_var

    def _build_second_derivative_risk_term(self):
        """
        Build second order derivative risk term.
        :return: the TensorFlow Tensor that contains the risk term.
        """
        unnormalized_states = self.x * self.system_std_dev + self.system_means
        sec_grads = self.trainable.compute_second_gradients(
            unnormalized_states)

        model_sec_derivatives = tf.expand_dims(
            sec_grads / self.system_std_dev * tf.pow(self.t_std_dev, 2), -1)
        data_sec_derivatives = self._build_second_derivative_mean()
        derivatives_difference = model_sec_derivatives - data_sec_derivatives
        # Compute log_variance on the derivatives
        post_variance = self._build_second_derivative_variance()
        # Compute risk term
        a_vector = tf.linalg.solve(post_variance, derivatives_difference)
        risk_term = 0.5 * tf.reduce_sum(a_vector * derivatives_difference)
        return risk_term

    def _build_gamma_risk_term(self) -> tf.Tensor:
        """
        Build the term associated with gamma.
        :return: the TensorFlow Tensor that contains the terms
        """
        # Compute log_variance on the derivatives
        post_variance =\
            self.gaussian_process.compute_posterior_derivative_variance() +\
            self.gamma * tf.expand_dims(tf.eye(self.n_points,
                                               dtype=tf.float64), 0)
        risk_term = 0.5 * tf.linalg.logdet(post_variance)
        return tf.reduce_sum(risk_term)

    def _build_gamma_prime_risk_term(self) -> tf.Tensor:
        """
        Build the term associated with gamma prime.
        :return: the TensorFlow Tensor that contains the terms
        """
        # Compute log_variance on the derivatives
        post_variance = self._build_second_derivative_variance()
        risk_term = 0.5 * tf.linalg.logdet(post_variance)
        return tf.reduce_sum(risk_term)

    def _build_risk(self) -> None:
        """
        Build the risk tensor by summing up the single terms.
        """
        self.risk_term1 = self._build_regularization_risk_term()
        self.risk_term2 = self._build_states_risk_term()
        self.risk_term3 = self._build_derivatives_risk_term()
        self.risk = self.risk_term1 + self.risk_term2
        if self.use_sec_grads:
            self.risk *= 2
            self.risk += self.risk_term3
        else:
            self.risk += self.risk_term3
        if self.train_gamma:
            self.risk += self._build_gamma_risk_term()
        if self.use_sec_grads:
            self.risk_term4 = self._build_second_derivative_risk_term() / 5
            self.risk += self.risk_term4
            if self.train_gamma_prime:
                self.gamma_prime_risk = self._build_gamma_prime_risk_term() / 5
                self.risk += self.gamma_prime_risk
        return

    def _build_optimizer(self) -> None:
        """
        Build the TensorFlow optimizer, wrapper to the scipy optimization
        algorithms.
        """
        # Extract the TF variables that get optimized in the risk minimization
        t_vars = tf.trainable_variables()
        risk_vars = [var for var in t_vars if 'risk_main' in var.name]
        # Dictionary containing the bounds on the TensorFlow Variables
        var_to_bounds = {
            risk_vars[0]: (self.trainable.parameter_lower_bounds,
                           self.trainable.parameter_upper_bounds),
            risk_vars[1]: (self.state_lower_bounds, self.state_upper_bounds)
        }
        if self.train_gamma:
            var_to_bounds[risk_vars[2]] = (self.gamma_bounds[0],
                                           self.gamma_bounds[1])
        if self.train_gamma_prime:
            var_to_bounds[risk_vars[3]] = (self.gamma_prime_bounds[0],
                                           self.gamma_prime_bounds[1])

        self.risk_optimizer = ExtendedScipyOptimizerInterface(
            loss=self.risk,
            method=self.optimizer,
            var_list=risk_vars,
            var_to_bounds=var_to_bounds)
        return

    def build_model(self) -> None:
        """
        Builds Some common part of the computational graph for the optimization.
        """
        self.gaussian_process.build_supporting_covariance_matrices(
            self.t, self.t)
        self.negative_data_loglikelihood = \
            - self.gaussian_process.compute_average_log_likelihood(self.system)
        self._build_states_bounds()
        self._build_variables()
        self._build_risk()
        self._build_optimizer()
        return

    def _initialize_variables(self) -> None:
        """
        Initialize all the variables and placeholders in the graph.
        """
        self.init = tf.global_variables_initializer()
        return

    def _initialize_states_with_mean_gp(self, session: tf.Session) -> None:
        """
        Before optimizing the risk, we initialize the x to be the mean
        predicted by the Gaussian Process for an easier task later.
        :param session: TensorFlow session, used in the fit function.
        """
        mean_prediction = self.gaussian_process.compute_posterior_mean(
            self.system)
        assign_states_mean = tf.assign(self.x, tf.squeeze(mean_prediction))
        session.run(assign_states_mean)
        self.x = tf.clip_by_value(
            self.x,
            clip_value_min=tf.constant(self.state_lower_bounds),
            clip_value_max=tf.constant(self.state_upper_bounds))
        return

    def fit(self) -> [np.array, np.array, np.array]:
        """
        Fits the model.
        :return numpy arrays containing the system parameters theta, the gamma
        hyperparameters and the predicted states.
        """
        self._initialize_variables()
        session = tf.Session()
        with session:
            session.run(self.init)
            self._train_data_based_gp(session)
            self._initialize_states_with_mean_gp(session)
            if self.basinhopping:
                self.risk_optimizer.basinhopping(session,
                                                 **self.basinhopping_options)
            else:
                self.risk_optimizer.minimize(session)
            theta = session.run(self.trainable.theta)
            gamma = session.run(self.gamma).reshape(-1)
            x = session.run(
                tf.squeeze(self.x) * self.system_std_dev + self.system_means)
        tf.reset_default_graph()
        return theta, gamma, x
Esempio n. 5
0
class GPRiskMinimization(object):
    """
    Class that implements marginal likelihood minimization for hyperparameter training of GP.
    """
    def __init__(self,
                 system_data: np.array,
                 t_data: np.array,
                 gp_kernel: str = 'RBF',
                 single_gp: bool = False,
                 state_normalization: bool = True,
                 time_normalization: bool = True):
        """
        Constructor
        :param system_data: numpy array containing the noisy observations of the state values of the system, size is [dim, n_points];
        :param t_data: numpy array containing the time stamps corresponding to the observations passed as system_data;
        :param gp_kernel: string indicating which kernel to use in the GP. Valid options are ONLY 'RBF' for the current implementation;
        :param single_gp: boolean, indicates whether to use a single set of GP hyperparameters for each state;
        :param state_normalization: boolean, indicates whether to normalize the states values;
        :param time_normalization: boolean, indicates whether to normalize the time stamps;
        """
        # Save arguments
        self.system_data = np.copy(system_data)
        self.t_data = np.copy(t_data).reshape(-1, 1)
        self.dim, self.n_p = system_data.shape
        self.gp_kernel = gp_kernel
        self.single_gp = single_gp

        # Initialize utils
        self._compute_standardization_data(state_normalization,
                                           time_normalization)
        # TensorFlow placeholders and constants
        self._build_tf_data()
        # Initialize the Gaussian Process for the derivative model
        self.gaussian_process = GaussianProcess(self.dim, self.n_p,
                                                self.gp_kernel, self.single_gp)
        # Initialization of TF operations
        self.init = None
        self.negative_data_loglikelihood = None
        return

    def _compute_standardization_data(self, state_normalization: bool,
                                      time_normalization: bool) -> None:
        """
        Compute the means and the standard deviations for data standardization,
        used in the GP training.
        """
        # Compute mean and std dev of the state and time values
        if state_normalization:
            self.system_data_means = np.mean(self.system_data,
                                             axis=1).reshape(self.dim, 1)
            self.system_data_std_dev = np.std(self.system_data,
                                              axis=1).reshape(self.dim, 1)
        else:
            self.system_data_means = np.zeros([self.dim, 1])
            self.system_data_std_dev = np.ones([self.dim, 1])
        if time_normalization:
            self.t_data_mean = np.mean(self.t_data)
            self.t_data_std_dev = np.std(self.t_data)
        else:
            self.t_data_mean = 0.0
            self.t_data_std_dev = 1.0
        # For the sigmoid kernel the input time values must be positive, i.e.
        # we only divide by the standard deviation
        if self.gp_kernel == 'Sigmoid':
            self.t_data_mean = 0.0
        # Normalize states and time
        self.normalized_states = (self.system_data - self.system_data_means) / \
            self.system_data_std_dev
        self.normalized_t_data = (self.t_data - self.t_data_mean) / \
            self.t_data_std_dev
        return

    def _build_tf_data(self) -> None:
        """
        Initialize all the TensorFlow constants needed by the pipeline.
        """
        self.system = tf.constant(self.normalized_states, dtype=tf.float64)
        self.t = tf.constant(self.normalized_t_data, dtype=tf.float64)
        self.system_means = tf.constant(self.system_data_means,
                                        dtype=tf.float64,
                                        shape=[self.dim, 1])
        self.system_std_dev = tf.constant(self.system_data_std_dev,
                                          dtype=tf.float64,
                                          shape=[self.dim, 1])
        self.t_mean = tf.constant(self.t_data_mean, dtype=tf.float64)
        self.t_std_dev = tf.constant(self.t_data_std_dev, dtype=tf.float64)
        self.n_points = tf.constant(self.n_p, dtype=tf.int32)
        self.dimensionality = tf.constant(self.dim, dtype=tf.int32)
        return

    @staticmethod
    def _build_var_to_bounds_gp() -> dict:
        """
        Builds the dictionary containing the bounds that will be applied to the
        variable in the Gaussian Process model.
        :return: the dictionary variables to bounds.
        """
        # Extract TF variables and select the GP ones
        t_vars = tf.trainable_variables()
        gp_vars = [var for var in t_vars if 'gaussian_process' in var.name]
        # Bounds for the GP hyper-parameters
        gp_kern_lengthscale_bounds = (np.log(1e-6), np.log(100.0))
        gp_kern_variance_bounds = (np.log(1e-6), np.log(100.0))
        gp_kern_likelihood_bounds = (np.log(1e-6), np.log(100.0))
        # Dictionary construction
        var_to_bounds = {
            gp_vars[0]: gp_kern_lengthscale_bounds,
            gp_vars[1]: gp_kern_variance_bounds,
            gp_vars[2]: gp_kern_likelihood_bounds
        }
        return var_to_bounds

    @staticmethod
    def _build_var_to_bounds_gp_sigmoid() -> dict:
        """
        Builds the dictionary containing the bounds that will be applied to the
        variable in the Gaussian Process model (specific for the sigmoid
        kernel).
        :return: the dictionary variables to bounds.
        """
        # Extract TF variables and select the GP ones
        t_vars = tf.trainable_variables()
        gp_vars = [var for var in t_vars if 'gaussian_process' in var.name]
        # Bounds for the GP hyper-parameters
        gp_kern_a_bounds = (np.log(1e-6), np.log(100.0))
        gp_kern_b_bounds = (np.log(1e-6), np.log(100.0))
        gp_kern_variance_bounds = (np.log(1e-6), np.log(100.0))
        gp_kern_likelihood_bounds = (np.log(1e-6), np.log(100.0))
        # Dictionary construction
        var_to_bounds = {
            gp_vars[0]: gp_kern_a_bounds,
            gp_vars[1]: gp_kern_b_bounds,
            gp_vars[2]: gp_kern_variance_bounds,
            gp_vars[3]: gp_kern_likelihood_bounds
        }
        return var_to_bounds

    def _train_data_based_gp(self, session: tf.Session()) -> None:
        """
        Performs a classic GP regression on the data of the system. For each
        state of the system we train a different GP by maximum likelihood to fix
        the kernel hyper-parameters.
        :param session: TensorFlow session used during the optimization.
        """
        # Extract TF variables and select the GP ones
        t_vars = tf.trainable_variables()
        gp_vars = [var for var in t_vars if 'gaussian_process' in var.name]
        # Build the bounds for the GP hyper-parameters
        if self.gp_kernel == 'Sigmoid':
            var_to_bounds = self._build_var_to_bounds_gp_sigmoid()
        else:
            var_to_bounds = self._build_var_to_bounds_gp()
        # Initialize the TF/scipy optimizer
        self.data_gp_optimizer = ExtendedScipyOptimizerInterface(
            self.negative_data_loglikelihood,
            method="L-BFGS-B",
            var_list=gp_vars,
            var_to_bounds=var_to_bounds)
        # Optimize
        self.data_gp_optimizer.basinhopping(session, n_iter=50, stepsize=0.05)
        return

    def build_model(self) -> None:
        """
        Builds Some common part of the computational graph for the optimization.
        """
        self.gaussian_process.build_supporting_covariance_matrices(
            self.t, self.t)
        self.negative_data_loglikelihood = \
            - self.gaussian_process.compute_average_log_likelihood(self.system)
        return

    def _initialize_variables(self) -> None:
        """
        Initialize all the variables and placeholders in the graph.
        """
        self.init = tf.global_variables_initializer()
        return

    def train(self) -> [int, np.array, np.array, np.array]:
        """
        Trains the GP, i.e tuning the hyperparameters
        Returns the time needed for the optimization, as well as the hyperpameters found
        """
        self._initialize_variables()
        session = tf.Session()
        with session:
            # Start the session
            session.run(self.init)
            # Train the GP
            secs = time.time()
            self._train_data_based_gp(session)
            secs = time.time() - secs
            print("Likelihood is ",
                  session.run(self.negative_data_loglikelihood))
            # Print GP hyperparameters
            print(
                "GP trained ------------------------------------------------")
            if self.gp_kernel == 'Sigmoid':
                a = session.run(self.gaussian_process.kernel.a)
                b = session.run(self.gaussian_process.kernel.b)
                variances = session.run(
                    self.gaussian_process.diff_kernel.variances)
                likelihood_variances = session.run(
                    self.gaussian_process.likelihood_variances)
                print("a:", a)
                print("b:", b)
                print("variances:", variances)
                res = [secs, a, b, variances, likelihood_variances]
            else:
                lengthscales = session.run(
                    self.gaussian_process.kernel.lengthscales)
                variances = session.run(self.gaussian_process.kernel.variances)
                likelihood_variances = session.run(
                    self.gaussian_process.likelihood_variances)
                print("lengthscales:", lengthscales)
                print("variances:", variances)
                print("likelihood_variances:", likelihood_variances)
                res = [secs, lengthscales, variances, likelihood_variances]
            print(
                "-----------------------------------------------------------")
        tf.reset_default_graph()
        return res
Esempio n. 6
0
class ODEApproxRiskMinimization(object):
    """
    Class that implements approximate ODIN risk minimization
    """
    def __init__(self,
                 trainable: TrainableModel,
                 system_data: np.array,
                 t_data: np.array,
                 gp_kernel: str = 'RBF',
                 optimizer: str = 'L-BFGS-B',
                 initial_gamma: float = 0.3,
                 train_gamma: bool = True,
                 gamma_bounds: Union[np.array, list,
                                     Tuple] = (1e-6 + 1e-4, 10.0),
                 state_bounds: np.array = None,
                 basinhopping: bool = True,
                 basinhopping_options: dict = None,
                 single_gp: bool = False,
                 state_normalization: bool = True,
                 time_normalization: bool = False,
                 tensorboard_summary_dir: str = None,
                 runtime_prof_dir: str = None,
                 QFF_features: int = 40,
                 Approx_method: str = "QFF"):
        """
        Constructor.
        :param trainable: Trainable model class, as explained and implemented in
        utils.trainable_models;
        :param system_data: numpy array containing the noisy observations of
        the state values of the system, size is [n_states, n_points];
        :param t_data: numpy array containing the time stamps corresponding to
        the observations passed as system_data;
        :param gp_kernel: string indicating which kernel to use in the GP.
        Valid options are 'RBF', 'Matern52', 'Matern32', 'RationalQuadratic',
        'Sigmoid';
        :param optimizer: string indicating which scipy optimizer to use. The
        valid ones are the same that can be passed to scipy.optimize.minimize.
        Notice that some of them will ignore bounds;
        :param initial_gamma: initial value for the gamma parameter.
        :param train_gamma: boolean, indicates whether to train of not the
        variable gamma;
        :param gamma_bounds: bounds for gamma (a lower bound of at least 1e-6
        is always applied to overcome numerical instabilities);
        :param state_bounds: bounds for the state optimization;
        :param basinhopping: boolean, indicates whether to turn on the scipy
        basinhopping;
        :param basinhopping_options: dictionary containing options for the
        basinhooping algorithm (syntax is the same as scipy's one);
        :param single_gp: boolean, indicates whether to use a single set of GP
        hyperparameters for each state;
        :param state_normalization: boolean, indicates whether to normalize the
        states values before the optimization (notice the parameter values
        theta won't change);
        :param time_normalization: boolean, indicates whether to normalize the
        time stamps before the optimization (notice the parameter values
        theta won't change);
        :param QFF_features: int, the order of the quadrature scheme
        :param tensorboard_summary_dir, runtime_prof_dir: str, logging directories
        """
        # Save arguments
        self.Approx_method = Approx_method
        self.QFF_approx = QFF_features
        self.lamda = 1e-4
        self.trainable = trainable
        self.system_data = np.copy(system_data)
        self.t_data = np.copy(t_data).reshape(-1, 1)
        self.dim, self.n_p = system_data.shape
        self.gp_kernel = gp_kernel
        if self.gp_kernel != 'RBF':
            raise NotImplementedError(
                "Only RBF kernel is currently implemented for use with QFFs")
        self.optimizer = optimizer
        self.initial_gamma = initial_gamma
        self.train_gamma = train_gamma
        self.basinhopping = basinhopping
        self.basinhopping_options = {
            'n_iter': 10,
            'temperature': 1.0,
            'stepsize': 0.05
        }
        self.state_normalization = state_normalization
        if basinhopping_options:
            self.basinhopping_options.update(basinhopping_options)
        self.single_gp = single_gp
        # Build bounds for the states and for gamma
        self._compute_state_bounds(state_bounds)
        self._compute_gamma_bounds(gamma_bounds)
        # Initialize utils
        self._compute_standardization_data(state_normalization,
                                           time_normalization)
        # Build the necessary TensorFlow tensors
        self._build_tf_data()
        # Initialize the Gaussian Process for the derivative model
        self.gaussian_process = GaussianProcess(self.dim, self.n_p,
                                                self.gp_kernel, self.single_gp)

        #initialize logging variables
        if tensorboard_summary_dir:
            self.writer = tf.summary.FileWriter(tensorboard_summary_dir)
            self.theta_sum = tf.summary.histogram('Theta_summary',
                                                  self.trainable.theta)
        else:
            self.writer = None

        self.runtime_prof_dir = runtime_prof_dir
        # Initialization of TF operations
        self.init = None
        return

    def _compute_gamma_bounds(self, bounds: Union[np.array, list, Tuple])\
            -> None:
        """
        Builds the numpy array that defines the bounds for gamma.
        :param bounds: of the form (lower_bound, upper_bound).
        """
        self.gamma_bounds = np.array([1.0, 1.0])
        if bounds is None:
            self.gamma_bounds[0] = np.log(1e-6 + 1e-4)
            self.gamma_bounds[1] = np.inf
        else:
            self.gamma_bounds[0] = np.log(np.array(bounds[0]))
            self.gamma_bounds[1] = np.log(np.array(bounds[1]))
        return

    def _compute_state_bounds(self, bounds: np.array) -> None:
        """
        Builds the numpy array that defines the bounds for the states.
        :param bounds: numpy array, sized [n_dim, 2], in which for each
        dimensions we can find respectively lower and upper bounds.
        """
        if bounds is None:
            self.state_bounds = np.inf * np.ones([self.dim, 2])
            self.state_bounds[:, 0] = -self.state_bounds[:, 0]
        else:
            self.state_bounds = np.array(bounds)
        return

    def _compute_standardization_data(self, state_normalization: bool,
                                      time_normalization: bool) -> None:
        """
        Compute the means and the standard deviations for data standardization,
        used in the GP regression.
        """
        # Compute mean and std dev of the state and time values
        if state_normalization:
            self.system_data_means = np.mean(self.system_data,
                                             axis=1).reshape(self.dim, 1)
            self.system_data_std_dev = np.std(self.system_data,
                                              axis=1).reshape(self.dim, 1)
        else:
            self.system_data_means = np.zeros([self.dim, 1])
            self.system_data_std_dev = np.ones([self.dim, 1])
        if time_normalization:
            self.t_data_mean = np.mean(self.t_data)
            self.t_data_std_dev = np.std(self.t_data)
        else:
            self.t_data_mean = 0.0
            self.t_data_std_dev = 1.0
        # Normalize states and time
        self.normalized_states = (self.system_data - self.system_data_means) / \
            self.system_data_std_dev
        self.normalized_t_data = (self.t_data - self.t_data_mean) / \
            self.t_data_std_dev
        return

    def _build_tf_data(self) -> None:
        """
        Initialize all the TensorFlow constants needed in the pipeline.
        """
        self.system = tf.constant(self.normalized_states, dtype=tf.float64)
        self.t = tf.constant(self.normalized_t_data, dtype=tf.float64)
        self.system_means = tf.constant(self.system_data_means,
                                        dtype=tf.float64,
                                        shape=[self.dim, 1])
        self.system_std_dev = tf.constant(self.system_data_std_dev,
                                          dtype=tf.float64,
                                          shape=[self.dim, 1])
        self.t_mean = tf.constant(self.t_data_mean, dtype=tf.float64)
        self.t_std_dev = tf.constant(self.t_data_std_dev, dtype=tf.float64)
        self.n_points = tf.constant(self.n_p, dtype=tf.int32)
        self.dimensionality = tf.constant(self.dim, dtype=tf.int32)
        return

    def _build_states_bounds(self) -> None:
        """
        Builds the tensors for the normalized states that will containing the
        bounds for the constrained optimization.
        """
        # Tile the bounds to get the right dimensions
        state_lower_bounds = self.state_bounds[:, 0].reshape(self.dim, 1)
        state_lower_bounds = np.tile(state_lower_bounds, [1, self.n_p])
        state_lower_bounds = (state_lower_bounds - self.system_data_means)\
            / self.system_data_std_dev
        state_lower_bounds = state_lower_bounds.reshape([self.dim, self.n_p])
        state_upper_bounds = self.state_bounds[:, 1].reshape(self.dim, 1)
        state_upper_bounds = np.tile(state_upper_bounds, [1, self.n_p])
        state_upper_bounds = (state_upper_bounds - self.system_data_means)\
            / self.system_data_std_dev
        state_upper_bounds = state_upper_bounds.reshape([self.dim, self.n_p])
        self.state_lower_bounds = state_lower_bounds
        self.state_upper_bounds = state_upper_bounds
        return

    def _build_variables(self) -> None:
        """
        Builds the TensorFlow variables with the state values and the gamma
        that will later be optimized.
        """
        self.Z = tf.Variable(tf.zeros([self.dim, self.n_p, self.QFF_approx],
                                      dtype=tf.float64),
                             dtype=tf.float64,
                             trainable=False,
                             name='Z')
        self.Z_prime = tf.Variable(tf.zeros(
            [self.dim, self.n_p, self.QFF_approx], dtype=tf.float64),
                                   dtype=tf.float64,
                                   trainable=False,
                                   name='Z_prime')
        with tf.variable_scope('risk_main'):
            self.x = tf.Variable(self.system,
                                 dtype=tf.float64,
                                 trainable=True,
                                 name='states')
            if self.single_gp:
                self.log_gamma_single = tf.Variable(np.log(self.initial_gamma),
                                                    dtype=tf.float64,
                                                    trainable=self.train_gamma,
                                                    name='gamma')
                self.gamma =\
                    tf.exp(self.log_gamma_single)\
                    * tf.ones([self.dimensionality, 1, 1], dtype=tf.float64)
            else:
                self.log_gamma = tf.Variable(
                    np.log(self.initial_gamma) *
                    tf.ones([self.dimensionality, 1, 1], dtype=tf.float64),
                    trainable=self.train_gamma,
                    dtype=tf.float64,
                    name='log_gamma')
                self.gamma = tf.exp(self.log_gamma)
        return

    def _build_regularization_risk_term(self) -> tf.Tensor:
        """
        Build the first term of the risk, connected to regularization.
        :return: the TensorFlow Tensor that contains the term.
        """
        a_vector = tf.matmul(self.Z_t_x,
                             self.inv_Z_t_x,
                             transpose_a=True,
                             name='reg_risk_main_term')
        risk_term = 0.5 / self.lamda * (tf.reduce_sum(self.x * self.x) -
                                        tf.reduce_sum(a_vector))
        return risk_term

    def _build_states_risk_term(self) -> tf.Tensor:
        """
        Build the second term of the risk, connected with the value of the
        states.
        :return: the TensorFlow Tensor that contains the term.
        """
        states_difference = self.system - self.x
        risk_term = tf.reduce_sum(states_difference * states_difference, 1)
        risk_term = risk_term * 0.5 / tf.squeeze(
            self.gaussian_process.likelihood_variances)
        return tf.reduce_sum(risk_term)

    def _build_derivatives_risk_term(self) -> tf.Tensor:
        """
        Build the third term of the risk, connected with the value of the
        derivatives.
        :return: the TensorFlow Tensor that contains the term.
        """
        # Compute model and data-based derivatives
        unnormalized_states = self.x * self.system_std_dev + self.system_means
        model_derivatives = tf.expand_dims(
            self.trainable.compute_gradients(unnormalized_states) /
            self.system_std_dev * self.t_std_dev, -1)

        self.data_derivatives = tf.matmul(self.Z_prime,
                                          self.inv_Z_t_x,
                                          name='Dx')

        derivatives_difference = model_derivatives - self.data_derivatives

        Z_prime_t_der_dif = tf.matmul(self.Z_prime,
                                      derivatives_difference,
                                      transpose_a=True,
                                      name='Z_prime_t_der_dif')
        self.Hess_inner_dim = tf.matmul(self.Z_prime,
                                        self.Z_prime,
                                        transpose_a=True,
                                        name='Hess_inner_dim')
        temp = self.Hess_inner_dim + self.gamma * self.Z_t_Z_lamda / self.lamda
        temp1 = tf.linalg.solve(temp,
                                Z_prime_t_der_dif,
                                name='inverse_of_der_risk_term')
        second_term = tf.matmul(Z_prime_t_der_dif, temp1, transpose_a=True)
        first_term = tf.matmul(derivatives_difference,
                               derivatives_difference,
                               transpose_a=True)
        risk_term = (first_term - second_term) / self.gamma
        risk_term = 0.5 * tf.reduce_sum(risk_term)
        return risk_term

    def _build_gamma_risk_term(self) -> tf.Tensor:
        """
        Build the terms associated with gamma
        :return: the TensorFlow Tensor that contains the terms
        """
        # Compute log_variance on the derivatives
        self.A_inner_dim = tf.Variable(tf.zeros(
            [self.dim, self.QFF_approx, self.QFF_approx], dtype=tf.float64),
                                       dtype=tf.float64,
                                       trainable=False,
                                       name='A_inner_dim')
        risk_term = 0.5 * (
            tf.linalg.logdet(self.A_inner_dim + self.gamma *
                             tf.eye(self.QFF_approx, dtype=tf.float64)) +
            (self.n_p - self.QFF_approx) * tf.squeeze(tf.log(self.gamma)))
        return tf.reduce_sum(risk_term)

    def _build_risk(self) -> None:
        """
        Build the risk tensor by summing up the single terms.
        """
        self.risk_term1 = self._build_regularization_risk_term()
        self.risk_term2 = self._build_states_risk_term()
        self.risk_term3 = self._build_derivatives_risk_term()
        self.risk_term4 = self._build_gamma_risk_term()
        self.risk = self.risk_term1 + self.risk_term2 + self.risk_term3
        if self.train_gamma:
            self.risk += self.risk_term4
        if self.writer:
            loss_sum = tf.summary.scalar(name='loss_sum', tensor=self.risk)
        return

    def _build_optimizer(self) -> None:
        """
        Build the TensorFlow optimizer, wrapper to the scipy optimization
        algorithms.
        """
        # Extract the TF variables that get optimized in the risk minimization
        t_vars = tf.trainable_variables()
        risk_vars = [var for var in t_vars if 'risk_main' in var.name]
        # Dictionary containing the bounds on the TensorFlow Variables
        var_to_bounds = {
            risk_vars[0]: (self.trainable.parameter_lower_bounds,
                           self.trainable.parameter_upper_bounds),
            risk_vars[1]: (self.state_lower_bounds, self.state_upper_bounds)
        }
        if self.train_gamma:
            var_to_bounds[risk_vars[2]] = (self.gamma_bounds[0],
                                           self.gamma_bounds[1])

        self.risk_optimizer = ExtendedScipyOptimizerInterface(
            loss=self.risk,
            method=self.optimizer,
            var_list=risk_vars,
            var_to_bounds=var_to_bounds,
            file_writer=self.writer,
            dir_prof_name=self.runtime_prof_dir)
        return

    def build_model(self) -> None:
        """
        Builds Some common part of the computational graph for the optimization.
        """
        #self.gaussian_process.build_supporting_covariance_matrices(self.t, self.t)
        self._build_states_bounds()
        self._build_variables()

        self.Z_t_x = tf.matmul(self.Z,
                               tf.expand_dims(self.x, -1),
                               transpose_a=True,
                               name='Z_t_x')
        #self.prox=tf.matmul(self.Z,self.Z,transpose_b=True)
        self.Kernel_inner_dim = tf.matmul(self.Z,
                                          self.Z,
                                          transpose_a=True,
                                          name='Kernel_inner_dim')
        self.Z_t_Z_lamda = self.Kernel_inner_dim + self.lamda * tf.eye(
            self.QFF_approx, dtype=tf.float64)
        self.inv_Z_t_x = tf.linalg.solve(self.Z_t_Z_lamda,
                                         self.Z_t_x,
                                         name='inv_Z_t_x')

        self._build_risk()
        if self.writer:
            self.merged_sum = tf.summary.merge_all()
        self._build_optimizer()
        return

    def _initialize_variables(self) -> None:
        """
        Initialize all the variables and placeholders in the graph.
        """
        self.init = tf.global_variables_initializer()
        return

    def _initialize_states_with_mean_gp(self, session: tf.Session,
                                        compute_dict: dict) -> None:
        """
        Before optimizing the risk, we initialize the x to be the mean
        predicted by the Gaussian Process for an easier task later.
        :param session: TensorFlow session, used in the fit function.
        """
        #self.mean_prediction = self.gaussian_process.compute_posterior_mean(self.system)
        assign_states_mean = tf.assign(self.x,
                                       tf.squeeze(self.mean_prediction))
        session.run(assign_states_mean, feed_dict=compute_dict)
        self.X = self.x
        self.x = tf.clip_by_value(
            self.x,
            clip_value_min=tf.constant(self.state_lower_bounds),
            clip_value_max=tf.constant(self.state_upper_bounds))
        return

    def _initialize_constants_for_risk(self, lengthscales, variances,
                                       noise_var):
        lengthscales = np.reshape(lengthscales, [-1, 1, 1])
        variances = np.reshape(variances, [-1, 1, 1])
        if self.Approx_method == "QFF":
            Z = hermite_embeding(self.QFF_approx, lengthscales,
                                 self.t_data) * np.sqrt(variances)
            Z_prime = hermite_embeding_derivative(
                self.QFF_approx, lengthscales,
                self.t_data) * np.sqrt(variances)
        elif self.Approx_method == "RFF":
            Z = RFF_embeding(self.QFF_approx, lengthscales,
                             self.t_data) * np.sqrt(variances)
            Z_prime = RFF_embeding_derivative(self.QFF_approx, lengthscales,
                                              self.t_data) * np.sqrt(variances)
        elif self.Approx_method == "RFF_bias":
            Z = RFF_embeding_bias(self.QFF_approx, lengthscales,
                                  self.t_data) * np.sqrt(variances)
            Z_prime = RFF_embeding_derivative_bias(
                self.QFF_approx, lengthscales,
                self.t_data) * np.sqrt(variances)

        Kernel_inner_dim = np.matmul(np.transpose(Z, [0, 2, 1]), Z)
        u, s, v = np.linalg.svd(Kernel_inner_dim)
        D = np.array([
            np.diag(1 / np.sqrt(s[i] + self.lamda)) for i in range(s.shape[0])
        ])
        inv_sqrt_Kernel_inner_dim = np.matmul(u, np.matmul(D, v))
        U = np.matmul(Z_prime, inv_sqrt_Kernel_inner_dim)
        A_inner_dim = self.lamda * np.matmul(np.transpose(U, [0, 2, 1]), U)

        #np.save("A_inner_dim",A_inner_dim)

        self.mean_prediction = np.matmul(
            Z,
            np.linalg.solve(
                Kernel_inner_dim + noise_var * np.eye(self.QFF_approx),
                np.matmul(np.transpose(Z, [0, 2, 1]),
                          np.expand_dims(self.normalized_states, -1))))

        comp_dict = {
            self.Z:
            Z,
            self.Z_prime:
            Z_prime,
            self.Kernel_inner_dim:
            Kernel_inner_dim,
            self.Hess_inner_dim:
            np.matmul(np.transpose(Z_prime, [0, 2, 1]), Z_prime),
            self.A_inner_dim:
            A_inner_dim
        }
        return comp_dict

    def train(self, gp_parameters):
        """
        Trains the model and returns thetas
        :param gp_parameters: values of hyperparameters of GP
        """
        compute_dict = {
            self.gaussian_process.kernel.lengthscales: gp_parameters[0],
            self.gaussian_process.kernel.variances: gp_parameters[1],
            self.gaussian_process.likelihood_variances: gp_parameters[2]
        }
        compute_dict.update(
            self._initialize_constants_for_risk(gp_parameters[0],
                                                gp_parameters[1],
                                                gp_parameters[2]))

        self._initialize_variables()
        session = tf.Session()
        with session:
            # Start the session
            session.run(self.init)

            # Initialize x as the mean of the GP
            self._initialize_states_with_mean_gp(session,
                                                 compute_dict=compute_dict)

            # Print initial theta
            theta = session.run(self.trainable.theta, feed_dict=compute_dict)
            print("Initialized Theta", theta)

            # Print initial gamma
            gamma = session.run(self.gamma, feed_dict=compute_dict)
            print("Initialized Gamma", gamma)

            # Print the terms of the Risk before the optimization
            print("Risk 1: ",
                  session.run(self.risk_term1, feed_dict=compute_dict))
            print("Risk 2: ",
                  session.run(self.risk_term2, feed_dict=compute_dict))
            print("Risk 3: ",
                  session.run(self.risk_term3, feed_dict=compute_dict))
            print("Risk: ", session.run(self.risk, feed_dict=compute_dict))

            if self.writer:
                self.writer.add_graph(session.graph)

                def summary_funct(merged_sum):
                    summary_funct.step += 1
                    self.writer.add_summary(merged_sum, summary_funct.step)

                summary_funct.step = -1

            result = []
            # Optimize
            if self.basinhopping:
                secs = time.time()
                result = self.risk_optimizer.basinhopping(
                    session,
                    feed_dict=compute_dict,
                    **self.basinhopping_options)
                secs = time.time() - secs
            else:
                if self.writer:
                    secs = time.time()
                    result = self.risk_optimizer.minimize(
                        session,
                        feed_dict=compute_dict,
                        loss_callback=summary_funct,
                        fetches=[self.merged_sum])
                    secs = time.time() - secs
                else:
                    secs = time.time()
                    result = self.risk_optimizer.minimize(
                        session, feed_dict=compute_dict)
                    secs = time.time() - secs
            print("Elapsed time is ", secs)
            # Print the terms of the Risk after the optimization
            print("risk 1: ",
                  session.run(self.risk_term1, feed_dict=compute_dict))
            print("risk 2: ",
                  session.run(self.risk_term2, feed_dict=compute_dict))
            print("risk 3: ",
                  session.run(self.risk_term3, feed_dict=compute_dict))
            if self.train_gamma:
                print("risk 4: ",
                      session.run(self.risk_term4, feed_dict=compute_dict))
            found_risk = session.run(self.risk, feed_dict=compute_dict)
            print("risk: ", found_risk)

            unnormalized_states = tf.squeeze(self.x) * self.system_std_dev + \
                self.system_means
            states_after = session.run(unnormalized_states,
                                       feed_dict=compute_dict)

            # Print final theta
            theta = session.run(self.trainable.theta, feed_dict=compute_dict)
            print("Final Theta", theta)

            # Print final gamma
            gamma = session.run(self.gamma, feed_dict=compute_dict)
            print("Final Gamma", gamma)

        tf.reset_default_graph()
        return theta, secs