Source code for optuna.samplers.nsgaii._crossovers._vsbx

from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np

from optuna._experimental import experimental_class
from optuna.samplers.nsgaii._crossovers._base import BaseCrossover


if TYPE_CHECKING:
    from optuna.study import Study


[docs] @experimental_class("3.0.0") class VSBXCrossover(BaseCrossover): """Modified Simulated Binary Crossover operation used by :class:`~optuna.samplers.NSGAIISampler`. vSBX generates child individuals without excluding any region of the parameter space, while maintaining the excellent properties of SBX. In the paper, vSBX has only one argument, ``eta``, and generate two child individuals. However, Optuna can only return one child individual in one crossover operation, so it uses the ``uniform_crossover_prob`` and ``use_child_gene_prob`` arguments to make two individuals into one. - `Pedro J. Ballester, Jonathan N. Carter. Real-Parameter Genetic Algorithms for Finding Multiple Optimal Solutions in Multi-modal Optimization. GECCO 2003: 706-717 <https://doi.org/10.1007/3-540-45105-6_86>`__ Args: eta: Distribution index. A small value of ``eta`` allows distant solutions to be selected as children solutions. If not specified, takes default value of ``2`` for single objective functions and ``20`` for multi objective. uniform_crossover_prob: ``uniform_crossover_prob`` is the probability of uniform crossover between two individuals selected as candidate child individuals. This argument is whether or not two individuals are crossover to make one child individual. If the ``uniform_crossover_prob`` exceeds 0.5, the result is equivalent to ``1-uniform_crossover_prob``, because it returns one of the two individuals of the crossover result. If not specified, takes default value of ``0.5``. The range of values is ``[0.0, 1.0]``. use_child_gene_prob: ``use_child_gene_prob`` is the probability of using the value of the generated child variable rather than the value of the parent. This probability is applied to each variable individually. where ``1-use_chile_gene_prob`` is the probability of using the parent's values as it is. If not specified, takes default value of ``0.5``. The range of values is ``(0.0, 1.0]``. """ n_parents = 2 def __init__( self, eta: float | None = None, uniform_crossover_prob: float = 0.5, use_child_gene_prob: float = 0.5, ) -> None: if (eta is not None) and (eta < 0.0): raise ValueError("The value of `eta` must be greater than or equal to 0.0.") self._eta = eta if uniform_crossover_prob < 0.0 or uniform_crossover_prob > 1.0: raise ValueError( "The value of `uniform_crossover_prob` must be in the range [0.0, 1.0]." ) if use_child_gene_prob <= 0.0 or use_child_gene_prob > 1.0: raise ValueError("The value of `use_child_gene_prob` must be in the range (0.0, 1.0].") self._uniform_crossover_prob = uniform_crossover_prob self._use_child_gene_prob = use_child_gene_prob
[docs] def crossover( self, parents_params: np.ndarray, rng: np.random.RandomState, study: Study, search_space_bounds: np.ndarray, ) -> np.ndarray: # https://doi.org/10.1007/3-540-45105-6_86 # Section 3.2 Crossover Schemes (vSBX) if self._eta is None: eta = 20.0 if study._is_multi_objective() else 2.0 else: eta = self._eta eps = 1e-10 us = rng.rand(len(search_space_bounds)) beta_1 = np.power(1 / np.maximum((2 * us), eps), 1 / (eta + 1)) beta_2 = np.power(1 / np.maximum((2 * (1 - us)), eps), 1 / (eta + 1)) u_1 = rng.rand() if u_1 <= 0.5: c1 = 0.5 * ((1 + beta_1) * parents_params[0] + (1 - beta_2) * parents_params[1]) else: c1 = 0.5 * ((1 - beta_1) * parents_params[0] + (1 + beta_2) * parents_params[1]) u_2 = rng.rand() if u_2 <= 0.5: c2 = 0.5 * ((3 - beta_1) * parents_params[0] - (1 - beta_2) * parents_params[1]) else: c2 = 0.5 * (-(1 - beta_1) * parents_params[0] + (3 - beta_2) * parents_params[1]) # vSBX applies crossover with use_child_gene_prob and uniform_crossover_prob. # the gene of the parent individual is the gene of the child individual. # The original vSBX creates two child individuals, # but optuna's implementation creates only one child individual. # Therefore, when there is no crossover, # the gene is selected with equal probability from the parent individuals x1 and x2. child1_params_list = [] child2_params_list = [] for c1_i, c2_i, x1_i, x2_i in zip(c1, c2, parents_params[0], parents_params[1]): if rng.rand() < self._use_child_gene_prob: if rng.rand() >= self._uniform_crossover_prob: child1_params_list.append(c1_i) child2_params_list.append(c2_i) else: child1_params_list.append(c2_i) child2_params_list.append(c1_i) else: if rng.rand() >= self._uniform_crossover_prob: child1_params_list.append(x1_i) child2_params_list.append(x2_i) else: child1_params_list.append(x2_i) child2_params_list.append(x1_i) child_params_list = child1_params_list if rng.rand() < 0.5 else child2_params_list child_params = np.array(child_params_list) return child_params