Skip to content

BayesianOpt

sgptools.methods.BayesianOpt

Bases: Method

Implements informative sensor placement/path optimization using Bayesian Optimization.

This method optimizes a given objective function (e.g., Mutual Information) by sampling and evaluating points in the search space, building a surrogate model, and using an acquisition function to guide further sampling.

Refer to the following papers for more details
  • UAV route planning for active disease classification [Vivaldini et al., 2019]
  • Occupancy map building through Bayesian exploration [Francis et al., 2019]

Attributes:

Name Type Description
objective object

The objective function to be optimized.

transform Optional[Transform]

Transform object applied to inducing points.

pbounds Dict[str, Tuple[float, float]]

Dictionary defining the search space bounds.

Source code in sgptools/methods.py
class BayesianOpt(Method):
    """
    Implements informative sensor placement/path optimization using Bayesian Optimization.

    This method optimizes a given objective function (e.g., Mutual Information)
    by sampling and evaluating points in the search space, building a surrogate
    model, and using an acquisition function to guide further sampling.

    Refer to the following papers for more details:
        - UAV route planning for active disease classification [Vivaldini et al., 2019]
        - Occupancy map building through Bayesian exploration [Francis et al., 2019]

    Attributes:
        objective (object): The objective function to be optimized.
        transform (Optional[Transform]): Transform object applied to inducing points.
        pbounds (Dict[str, Tuple[float, float]]): Dictionary defining the search space bounds.
    """

    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, Any] = 'SLogMI',
                 **kwargs: Any):
        """
        Initializes the BayesianOpt 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, Any]): The objective function to use. Can be a string ('SLogMI', 'MI')
                                         or an instance of an objective class. Defaults to 'SLogMI'.
            **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 isinstance(objective, str):
            self.objective = get_objective(objective)(X_objective, kernel,
                                                      noise_variance, **kwargs)
        else:
            self.objective = objective

        # Use the boundaries of the X_objective area as the search space limits
        pbounds_dims: List[Tuple[float, float]] = []
        for i in range(self.num_dim):
            pbounds_dims.append(
                (np.min(X_objective[:, i]), np.max(X_objective[:, i])))
        self.pbounds: Dict[str, Tuple[float, float]] = {}
        for i in range(self.num_dim * self.num_sensing * self.num_robots):
            self.pbounds[f'x{i}'] = pbounds_dims[i % self.num_dim]

    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 = 50,
                 init_points: int = 10,
                 verbose: bool = False,
                 seed: Optional[int] = None,
                 **kwargs: Any) -> np.ndarray:
        """
        Optimizes the sensor placement/path using Bayesian Optimization.

        Args:
            max_steps (int): Maximum number of optimization steps (iterations). Defaults to 50.
            init_points (int): Number of random exploration steps before Bayesian Optimization starts. Defaults to 10.
            verbose (bool): Verbosity, if True additional details will by reported. Defaults to False.
            seed (Optional[int]): Random seed for reproducibility. Defaults to None.
            **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
            bo_method = BayesianOpt(
                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 = bo_method.optimize(max_steps=50, init_points=10)
            ```
        """
        verbose = 1 if verbose else 0
        optimizer = BayesianOptimization(f=self._objective,
                                         pbounds=self.pbounds,
                                         verbose=verbose,
                                         random_state=seed,
                                         allow_duplicate_points=True)
        optimizer.maximize(init_points=init_points, n_iter=max_steps)

        sol: List[float] = []
        for i in range(self.num_dim * self.num_sensing * self.num_robots):
            sol.append(optimizer.max['params'][f'x{i}'])

        sol_np = np.array(sol).reshape(-1, self.num_dim)
        if self.transform is not None:
            try:
                sol_np = self.transform.expand(sol_np,
                                               expand_sensor_model=False)
            except TypeError:
                pass

            if not isinstance(sol_np, np.ndarray):
                sol_np = sol_np.numpy()

        # 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, **kwargs: float) -> float:
        """
        Internal objective function to be maximized by the Bayesian Optimization.

        This function reshapes the input parameters from the optimizer, applies
        any specified transformations, calculates the objective value, and
        applies a penalty for constraint violations.

        Args:
            **kwargs: Keyword arguments where keys are 'x0', 'x1', ..., representing
                      the flattened sensor placement coordinates.

        Returns:
            float: The objective value (reward - constraint penalty) to be maximized.
        """
        X_list: List[float] = []
        for i in range(len(kwargs)):
            X_list.append(kwargs[f'x{i}'])
        X = np.array(X_list).reshape(-1, self.num_dim)

        constraint_penality: float = 0.0
        if self.transform is not None:
            X_expanded = self.transform.expand(X)
            constraint_penality = self.transform.constraints(X)
            reward = self.objective(X_expanded)  # maximize
        else:
            reward = self.objective(X)  # maximize

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

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

Initializes the BayesianOpt 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, Any]

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

'SLogMI'
**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, Any] = 'SLogMI',
             **kwargs: Any):
    """
    Initializes the BayesianOpt 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, Any]): The objective function to use. Can be a string ('SLogMI', 'MI')
                                     or an instance of an objective class. Defaults to 'SLogMI'.
        **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 isinstance(objective, str):
        self.objective = get_objective(objective)(X_objective, kernel,
                                                  noise_variance, **kwargs)
    else:
        self.objective = objective

    # Use the boundaries of the X_objective area as the search space limits
    pbounds_dims: List[Tuple[float, float]] = []
    for i in range(self.num_dim):
        pbounds_dims.append(
            (np.min(X_objective[:, i]), np.max(X_objective[:, i])))
    self.pbounds: Dict[str, Tuple[float, float]] = {}
    for i in range(self.num_dim * self.num_sensing * self.num_robots):
        self.pbounds[f'x{i}'] = pbounds_dims[i % self.num_dim]

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=50, init_points=10, verbose=False, seed=None, **kwargs)

Optimizes the sensor placement/path using Bayesian Optimization.

Parameters:

Name Type Description Default
max_steps int

Maximum number of optimization steps (iterations). Defaults to 50.

50
init_points int

Number of random exploration steps before Bayesian Optimization starts. Defaults to 10.

10
verbose bool

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

False
seed Optional[int]

Random seed for reproducibility. Defaults to None.

None
**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
bo_method = BayesianOpt(
    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 = bo_method.optimize(max_steps=50, init_points=10)
Source code in sgptools/methods.py
def optimize(self,
             max_steps: int = 50,
             init_points: int = 10,
             verbose: bool = False,
             seed: Optional[int] = None,
             **kwargs: Any) -> np.ndarray:
    """
    Optimizes the sensor placement/path using Bayesian Optimization.

    Args:
        max_steps (int): Maximum number of optimization steps (iterations). Defaults to 50.
        init_points (int): Number of random exploration steps before Bayesian Optimization starts. Defaults to 10.
        verbose (bool): Verbosity, if True additional details will by reported. Defaults to False.
        seed (Optional[int]): Random seed for reproducibility. Defaults to None.
        **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
        bo_method = BayesianOpt(
            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 = bo_method.optimize(max_steps=50, init_points=10)
        ```
    """
    verbose = 1 if verbose else 0
    optimizer = BayesianOptimization(f=self._objective,
                                     pbounds=self.pbounds,
                                     verbose=verbose,
                                     random_state=seed,
                                     allow_duplicate_points=True)
    optimizer.maximize(init_points=init_points, n_iter=max_steps)

    sol: List[float] = []
    for i in range(self.num_dim * self.num_sensing * self.num_robots):
        sol.append(optimizer.max['params'][f'x{i}'])

    sol_np = np.array(sol).reshape(-1, self.num_dim)
    if self.transform is not None:
        try:
            sol_np = self.transform.expand(sol_np,
                                           expand_sensor_model=False)
        except TypeError:
            pass

        if not isinstance(sol_np, np.ndarray):
            sol_np = sol_np.numpy()

    # 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)