Skip to content

DifferentiableObjective

sgptools.methods.DifferentiableObjective

Bases: Method

Implements informative sensor placement/path planning optimization by directly differentiating through the objective function.

This method leverages TensorFlow's automatic differentiation capabilities to optimize the sensing locations (or path waypoints) by treating them as trainable variables and minimizing a given objective function (e.g., Mutual Information). This approach can be more efficient than black-box methods like Bayesian Optimization or CMA-ES, especially when the objective function is smooth. However, the method is also more prone to getting stuck in local minima.

Attributes:

Name Type Description
transform Optional[Transform]

Transform object to apply to the solution.

X_sol Variable

The solution (e.g., sensor locations) being optimized.

objective Objective

The objective function to be optimized.

Source code in sgptools/methods.py
class DifferentiableObjective(Method):
    """
    Implements informative sensor placement/path planning optimization by directly
    differentiating through the objective function.

    This method leverages TensorFlow's automatic differentiation capabilities to
    optimize the sensing locations (or path waypoints) by treating them as
    trainable variables and minimizing a given objective function (e.g., Mutual
    Information). This approach can be more efficient than black-box methods like
    Bayesian Optimization or CMA-ES, especially when the objective function is
    smooth. However, the method is also more prone to getting stuck in local minima.

    Attributes:
        transform (Optional[Transform]): Transform object to apply to the solution.
        X_sol (tf.Variable): The solution (e.g., sensor locations) being optimized.
        objective (Objective): The objective function to be optimized.
    """
    def __init__(self,
                 num_sensing: int,
                 X_objective: np.ndarray,
                 kernel: gpflow.kernels.Kernel,
                 noise_variance: float,
                 transform: Optional[Transform] = None,
                 num_robots: int = 1,
                 X_candidates: Optional[np.ndarray] = None,
                 num_dim: Optional[int] = None,
                 objective: Union[str, Objective] = 'SLogMI',
                 X_init: Optional[np.ndarray] = None,
                 X_time: Optional[np.ndarray] = None,
                 orientation: bool = False,
                 **kwargs: Any):
        """
        Initializes the DifferentiableObjective optimizer.

        Args:
            num_sensing (int): Number of sensing locations to optimize.
            X_objective (np.ndarray): (n, d); Data points used to define the objective function.
            kernel (gpflow.kernels.Kernel): GPflow kernel function.
            noise_variance (float): Data noise variance.
            transform (Optional[Transform]): Transform object to apply to inducing points. Defaults to None.
            num_robots (int): Number of robots/agents. Defaults to 1.
            X_candidates (Optional[np.ndarray]): (c, d); Discrete set of candidate locations for sensor placement.
                                                 Defaults to None.
            num_dim (Optional[int]): Dimensionality of the sensing locations. Defaults to dimensonality of X_objective.
            objective (Union[str, Objective]): The objective function to use. Can be a string (e.g., 'SLogMI', 'MI')
                                         or an instance of an objective class. Defaults to 'SLogMI'.
            X_init (Optional[np.ndarray]): (num_sensing * num_robots, d); Initial solution.
                                            If None, initial points are randomly selected from X_objective.
            X_time (Optional[np.ndarray]): (m, d); Temporal dimensions of the inducing points, used when
                                            modeling spatio-temporal IPP. Defaults to None.
            orientation (bool): If True, adds an additional dimension to model sensor FoV rotation angle
                                when selecting initial inducing points. Defaults to False.
            **kwargs: Additional keyword arguments passed to the objective function.
        """
        super().__init__(num_sensing, X_objective, kernel, noise_variance,
                         transform, num_robots, X_candidates, num_dim)
        self.transform = transform
        if X_candidates is None:
            self.X_candidates = X_objective  # Default candidates to objective points

        if X_init is None:
            X_init = get_inducing_pts(X_objective,
                                      num_sensing * self.num_robots,
                                      orientation=orientation)
        else:
            # override num_dim with initial inducing points dim, in case it differes from X_objective dim
            self.num_dim = X_init.shape[-1]
        self.X_sol = tf.Variable(X_init, dtype=X_init.dtype)

        if isinstance(objective, str):
            self.objective = get_objective(objective)(X_objective, kernel,
                                                      noise_variance, **kwargs)
        else:
            self.objective = objective

    def update(self, kernel: gpflow.kernels.Kernel,
               noise_variance: float) -> None:
        """
        Updates the kernel and noise variance parameters of the objective function.

        Args:
            kernel (gpflow.kernels.Kernel): Updated GPflow kernel function.
            noise_variance (float): Updated data noise variance.
        """
        self.objective.update(kernel, noise_variance)

    def get_hyperparameters(self) -> Tuple[gpflow.kernels.Kernel, float]:
        """
        Retrieves the current kernel and noise variance hyperparameters from the objective.

        Returns:
            Tuple[gpflow.kernels.Kernel, float]: A tuple containing a deep copy of the kernel and the noise variance.
        """
        return deepcopy(self.objective.kernel), \
               self.objective.noise_variance

    def optimize(self,
                 max_steps: int = 500,
                 optimizer: str = 'scipy.L-BFGS-B',
                 verbose: bool = False,
                 **kwargs: Any) -> np.ndarray:
        """
        Optimizes the sensor placement/path by differentiating through the objective function.

        Args:
            max_steps (int): Maximum number of optimization steps. Defaults to 500.
            optimizer (str): Optimizer "<backend>.<method>" to use for training (e.g., 'scipy.L-BFGS-B', 'tf.adam').
                             Defaults to 'scipy.L-BFGS-B'.
            verbose (bool): Verbosity, if True additional details will by reported. Defaults to False.
            **kwargs: Additional keyword arguments for the optimizer.

        Returns:
            np.ndarray: (num_robots, num_sensing, num_dim); Optimized sensing locations.

        Usage:
            ```python
            # Assuming X_train, candidates, kernel_opt, noise_variance_opt are defined
            diff_obj_method = DifferentiableObjective(
                num_sensing=10,
                X_objective=X_train,
                kernel=kernel_opt,
                noise_variance=noise_variance_opt,
                transform=IPPTransform(num_robots=1), # Example transform
                X_candidates=candidates
            )
            optimized_solution = diff_obj_method.optimize(max_steps=500, optimizer='scipy.L-BFGS-B')
            ```
        """
        _ = optimize_model(
            training_loss = self._objective,
            max_steps=max_steps,
            trainable_variables=[self.X_sol],
            optimizer=optimizer,
            verbose=verbose,
            **kwargs)

        sol: tf.Tensor = self.X_sol
        try:
            sol_expanded = self.transform.expand(sol,
                                                 expand_sensor_model=False)
        except TypeError:
            sol_expanded = sol
        if not isinstance(sol_expanded, np.ndarray):
            sol_np = sol_expanded.numpy()
        else:
            sol_np = sol_expanded

        # Map solution locations to candidates set locations if X_candidates is provided
        if self.X_candidates is not None:
            sol_np = cont2disc(sol_np, self.X_candidates)

        sol_np = sol_np.reshape(self.num_robots, -1, self.num_dim)
        return sol_np

    def _objective(self) -> float:
        """
        Internal objective function to be minimized by the optimizer.

        This function applies any specified transformations to the current solution,
        calculates the objective value, and applies a penalty for constraint
        violations.

        Returns:
            float: The objective value (reward + constraint penalty).
        """
        constraint_penality: float = 0.0
        if self.transform is not None:
            X_expanded = self.transform.expand(self.X_sol)
            constraint_penality = self.transform.constraints(self.X_sol)
            reward = self.objective(X_expanded)  # maximize
        else:
            reward = self.objective(self.X_sol)  # maximize

        reward += constraint_penality  # minimize (large negative value when constraint is unsatisfied)
        return reward

__init__(num_sensing, X_objective, kernel, noise_variance, transform=None, num_robots=1, X_candidates=None, num_dim=None, objective='SLogMI', X_init=None, X_time=None, orientation=False, **kwargs)

Initializes the DifferentiableObjective optimizer.

Parameters:

Name Type Description Default
num_sensing int

Number of sensing locations to optimize.

required
X_objective ndarray

(n, d); Data points used to define the objective function.

required
kernel Kernel

GPflow kernel function.

required
noise_variance float

Data noise variance.

required
transform Optional[Transform]

Transform object to apply to inducing points. Defaults to None.

None
num_robots int

Number of robots/agents. Defaults to 1.

1
X_candidates Optional[ndarray]

(c, d); Discrete set of candidate locations for sensor placement. Defaults to None.

None
num_dim Optional[int]

Dimensionality of the sensing locations. Defaults to dimensonality of X_objective.

None
objective Union[str, Objective]

The objective function to use. Can be a string (e.g., 'SLogMI', 'MI') or an instance of an objective class. Defaults to 'SLogMI'.

'SLogMI'
X_init Optional[ndarray]

(num_sensing * num_robots, d); Initial solution. If None, initial points are randomly selected from X_objective.

None
X_time Optional[ndarray]

(m, d); Temporal dimensions of the inducing points, used when modeling spatio-temporal IPP. Defaults to None.

None
orientation bool

If True, adds an additional dimension to model sensor FoV rotation angle when selecting initial inducing points. Defaults to False.

False
**kwargs Any

Additional keyword arguments passed to the objective function.

{}
Source code in sgptools/methods.py
def __init__(self,
             num_sensing: int,
             X_objective: np.ndarray,
             kernel: gpflow.kernels.Kernel,
             noise_variance: float,
             transform: Optional[Transform] = None,
             num_robots: int = 1,
             X_candidates: Optional[np.ndarray] = None,
             num_dim: Optional[int] = None,
             objective: Union[str, Objective] = 'SLogMI',
             X_init: Optional[np.ndarray] = None,
             X_time: Optional[np.ndarray] = None,
             orientation: bool = False,
             **kwargs: Any):
    """
    Initializes the DifferentiableObjective optimizer.

    Args:
        num_sensing (int): Number of sensing locations to optimize.
        X_objective (np.ndarray): (n, d); Data points used to define the objective function.
        kernel (gpflow.kernels.Kernel): GPflow kernel function.
        noise_variance (float): Data noise variance.
        transform (Optional[Transform]): Transform object to apply to inducing points. Defaults to None.
        num_robots (int): Number of robots/agents. Defaults to 1.
        X_candidates (Optional[np.ndarray]): (c, d); Discrete set of candidate locations for sensor placement.
                                             Defaults to None.
        num_dim (Optional[int]): Dimensionality of the sensing locations. Defaults to dimensonality of X_objective.
        objective (Union[str, Objective]): The objective function to use. Can be a string (e.g., 'SLogMI', 'MI')
                                     or an instance of an objective class. Defaults to 'SLogMI'.
        X_init (Optional[np.ndarray]): (num_sensing * num_robots, d); Initial solution.
                                        If None, initial points are randomly selected from X_objective.
        X_time (Optional[np.ndarray]): (m, d); Temporal dimensions of the inducing points, used when
                                        modeling spatio-temporal IPP. Defaults to None.
        orientation (bool): If True, adds an additional dimension to model sensor FoV rotation angle
                            when selecting initial inducing points. Defaults to False.
        **kwargs: Additional keyword arguments passed to the objective function.
    """
    super().__init__(num_sensing, X_objective, kernel, noise_variance,
                     transform, num_robots, X_candidates, num_dim)
    self.transform = transform
    if X_candidates is None:
        self.X_candidates = X_objective  # Default candidates to objective points

    if X_init is None:
        X_init = get_inducing_pts(X_objective,
                                  num_sensing * self.num_robots,
                                  orientation=orientation)
    else:
        # override num_dim with initial inducing points dim, in case it differes from X_objective dim
        self.num_dim = X_init.shape[-1]
    self.X_sol = tf.Variable(X_init, dtype=X_init.dtype)

    if isinstance(objective, str):
        self.objective = get_objective(objective)(X_objective, kernel,
                                                  noise_variance, **kwargs)
    else:
        self.objective = objective

get_hyperparameters()

Retrieves the current kernel and noise variance hyperparameters from the objective.

Returns:

Type Description
Tuple[Kernel, float]

Tuple[gpflow.kernels.Kernel, float]: A tuple containing a deep copy of the kernel and the noise variance.

Source code in sgptools/methods.py
def get_hyperparameters(self) -> Tuple[gpflow.kernels.Kernel, float]:
    """
    Retrieves the current kernel and noise variance hyperparameters from the objective.

    Returns:
        Tuple[gpflow.kernels.Kernel, float]: A tuple containing a deep copy of the kernel and the noise variance.
    """
    return deepcopy(self.objective.kernel), \
           self.objective.noise_variance

optimize(max_steps=500, optimizer='scipy.L-BFGS-B', verbose=False, **kwargs)

Optimizes the sensor placement/path by differentiating through the objective function.

Parameters:

Name Type Description Default
max_steps int

Maximum number of optimization steps. Defaults to 500.

500
optimizer str

Optimizer "." to use for training (e.g., 'scipy.L-BFGS-B', 'tf.adam'). Defaults to 'scipy.L-BFGS-B'.

'scipy.L-BFGS-B'
verbose bool

Verbosity, if True additional details will by reported. Defaults to False.

False
**kwargs Any

Additional keyword arguments for the optimizer.

{}

Returns:

Type Description
ndarray

np.ndarray: (num_robots, num_sensing, num_dim); Optimized sensing locations.

Usage
# Assuming X_train, candidates, kernel_opt, noise_variance_opt are defined
diff_obj_method = DifferentiableObjective(
    num_sensing=10,
    X_objective=X_train,
    kernel=kernel_opt,
    noise_variance=noise_variance_opt,
    transform=IPPTransform(num_robots=1), # Example transform
    X_candidates=candidates
)
optimized_solution = diff_obj_method.optimize(max_steps=500, optimizer='scipy.L-BFGS-B')
Source code in sgptools/methods.py
def optimize(self,
             max_steps: int = 500,
             optimizer: str = 'scipy.L-BFGS-B',
             verbose: bool = False,
             **kwargs: Any) -> np.ndarray:
    """
    Optimizes the sensor placement/path by differentiating through the objective function.

    Args:
        max_steps (int): Maximum number of optimization steps. Defaults to 500.
        optimizer (str): Optimizer "<backend>.<method>" to use for training (e.g., 'scipy.L-BFGS-B', 'tf.adam').
                         Defaults to 'scipy.L-BFGS-B'.
        verbose (bool): Verbosity, if True additional details will by reported. Defaults to False.
        **kwargs: Additional keyword arguments for the optimizer.

    Returns:
        np.ndarray: (num_robots, num_sensing, num_dim); Optimized sensing locations.

    Usage:
        ```python
        # Assuming X_train, candidates, kernel_opt, noise_variance_opt are defined
        diff_obj_method = DifferentiableObjective(
            num_sensing=10,
            X_objective=X_train,
            kernel=kernel_opt,
            noise_variance=noise_variance_opt,
            transform=IPPTransform(num_robots=1), # Example transform
            X_candidates=candidates
        )
        optimized_solution = diff_obj_method.optimize(max_steps=500, optimizer='scipy.L-BFGS-B')
        ```
    """
    _ = optimize_model(
        training_loss = self._objective,
        max_steps=max_steps,
        trainable_variables=[self.X_sol],
        optimizer=optimizer,
        verbose=verbose,
        **kwargs)

    sol: tf.Tensor = self.X_sol
    try:
        sol_expanded = self.transform.expand(sol,
                                             expand_sensor_model=False)
    except TypeError:
        sol_expanded = sol
    if not isinstance(sol_expanded, np.ndarray):
        sol_np = sol_expanded.numpy()
    else:
        sol_np = sol_expanded

    # Map solution locations to candidates set locations if X_candidates is provided
    if self.X_candidates is not None:
        sol_np = cont2disc(sol_np, self.X_candidates)

    sol_np = sol_np.reshape(self.num_robots, -1, self.num_dim)
    return sol_np

update(kernel, noise_variance)

Updates the kernel and noise variance parameters of the objective function.

Parameters:

Name Type Description Default
kernel Kernel

Updated GPflow kernel function.

required
noise_variance float

Updated data noise variance.

required
Source code in sgptools/methods.py
def update(self, kernel: gpflow.kernels.Kernel,
           noise_variance: float) -> None:
    """
    Updates the kernel and noise variance parameters of the objective function.

    Args:
        kernel (gpflow.kernels.Kernel): Updated GPflow kernel function.
        noise_variance (float): Updated data noise variance.
    """
    self.objective.update(kernel, noise_variance)