From a42f29b393a3b700f459a8c29497dedbf9b70233 Mon Sep 17 00:00:00 2001 From: hrntsm Date: Sat, 28 Sep 2024 15:14:45 +0900 Subject: [PATCH 1/3] Add nsgaii sampler --- .../nsgaii_with_initial_trials/LICENSE | 21 + .../nsgaii_with_initial_trials/README.md | 81 ++++ .../nsgaii_with_initial_trials/__init__.py | 4 + .../nsgaii_with_initial_trials/example.py | 202 +++++++++ .../nsgaii_with_initial_trials.py | 385 ++++++++++++++++++ 5 files changed, 693 insertions(+) create mode 100644 package/samplers/nsgaii_with_initial_trials/LICENSE create mode 100644 package/samplers/nsgaii_with_initial_trials/README.md create mode 100644 package/samplers/nsgaii_with_initial_trials/__init__.py create mode 100644 package/samplers/nsgaii_with_initial_trials/example.py create mode 100644 package/samplers/nsgaii_with_initial_trials/nsgaii_with_initial_trials.py diff --git a/package/samplers/nsgaii_with_initial_trials/LICENSE b/package/samplers/nsgaii_with_initial_trials/LICENSE new file mode 100644 index 00000000..31726532 --- /dev/null +++ b/package/samplers/nsgaii_with_initial_trials/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/package/samplers/nsgaii_with_initial_trials/README.md b/package/samplers/nsgaii_with_initial_trials/README.md new file mode 100644 index 00000000..2c96998f --- /dev/null +++ b/package/samplers/nsgaii_with_initial_trials/README.md @@ -0,0 +1,81 @@ +--- +author: Hiroaki Natsume +title: MOEA/D sampler +description: Sampler using MOEA/D algorithm. MOEA/D stands for "Multi-Objective Evolutionary Algorithm based on Decomposition. +tags: [Sampler, Multi-Objective Optimization, Evolutionary Algorithms] +optuna_versions: [4.0.0] +license: MIT License +--- + +## Abstract + +Sampler using MOEA/D algorithm. MOEA/D stands for "Multi-Objective Evolutionary Algorithm based on Decomposition. + +This sampler is specialized for multiobjective optimization. The objective function is internally decomposed into multiple single-objective subproblems to perform optimization. + +It may not work well with multi-threading. Check results carefully. + +## Class or Function Names + +- MOEADSampler + +## Installation + +``` +pip install scipy +``` + +or + +``` +pip install -r https://hub.optuna.org/samplers/moead/requirements.txt +``` + +## Example + +```python +import optuna +import optunahub + +def objective(trial: optuna.Trial) -> tuple[float, float]: + x = trial.suggest_float("x", 0, 5) + y = trial.suggest_float("y", 0, 3) + + v0 = 4 * x**2 + 4 * y**2 + v1 = (x - 5) ** 2 + (y - 5) ** 2 + return v0, v1 + + +population_size = 100 +n_trials = 1000 + +mod = optunahub.load_module("samplers/moead") +sampler = mod.MOEADSampler( + population_size=population_size, + scalar_aggregation_func="tchebycheff", + n_neighbors=population_size // 10, +) +study = optuna.create_study(sampler=sampler) +study.optimize(objective, n_trials=n_trials) +``` + +## Others + +Comparison between Random, NSGAII and MOEA/D with ZDT1 as the objective function. +See `compare_2objective.py` in moead directory for details. + +### Pareto Front Plot + +| MOEA/D | NSGAII | Random | +| --------------------------- | ---------------------------- | ---------------------------- | +| ![MOEA/D](images/moead.png) | ![NSGAII](images/nsgaii.png) | ![Random](images/random.png) | + +### Compare + +![Compare](images/compare_pareto_front.png) + +### Reference + +Q. Zhang and H. Li, +"MOEA/D: A Multiobjective Evolutionary Algorithm Based on Decomposition," in IEEE Transactions on Evolutionary Computation, vol. 11, no. 6, pp. 712-731, Dec. 2007, +[doi: 10.1109/TEVC.2007.892759](https://doi.org/10.1109/TEVC.2007.892759). diff --git a/package/samplers/nsgaii_with_initial_trials/__init__.py b/package/samplers/nsgaii_with_initial_trials/__init__.py new file mode 100644 index 00000000..96538b83 --- /dev/null +++ b/package/samplers/nsgaii_with_initial_trials/__init__.py @@ -0,0 +1,4 @@ +from .nsgaii_with_initial_trials import NSGAIIwITSampler + + +__all__ = ["NSGAIIwITSampler"] diff --git a/package/samplers/nsgaii_with_initial_trials/example.py b/package/samplers/nsgaii_with_initial_trials/example.py new file mode 100644 index 00000000..8c94b3f8 --- /dev/null +++ b/package/samplers/nsgaii_with_initial_trials/example.py @@ -0,0 +1,202 @@ +import numpy as np +import optuna +from optuna.samplers import NSGAIISampler +from optuna.samplers.nsgaii import BLXAlphaCrossover +import optuna.storages.journal +import optunahub + +from nsgaii_with_initial_trials import NSGAIIwITSampler + + +file_path = "./journal.log" +lock_obj = optuna.storages.journal.JournalFileOpenLock(file_path) + +storage = optuna.storages.JournalStorage( + optuna.storages.journal.JournalFileBackend(file_path, lock_obj=lock_obj), +) + +storage = optuna.storages.InMemoryStorage() + + +def objective(trial: optuna.Trial) -> tuple[float, float]: + # ZDT1 + n_variables = 30 + + x = np.array([trial.suggest_float(f"x{i}", 0, 1) for i in range(n_variables)]) + g = 1 + 9 * np.sum(x[1:]) / (n_variables - 1) + f1 = x[0] + f2 = g * (1 - (f1 / g) ** 0.5) + + return f1, f2 + + +population_size = 50 +n_trials = 1000 +seed = 42 + + +samplers = [ + NSGAIISampler( + population_size=population_size, + seed=seed, + crossover=BLXAlphaCrossover(), + ), + # NSGAIIwITSampler( + # population_size=population_size, + # seed=seed, + # crossover=BLXAlphaCrossover(), + # ), + NSGAIIwITSampler( + population_size=population_size, + seed=seed, + crossover=BLXAlphaCrossover(), + ), +] + +studies = [] +title = ["NSGAII", "NSGAIIwInitialTrials"] +for i, sampler in enumerate(samplers): + study = optuna.create_study( + sampler=sampler, + study_name=title[i], + directions=["minimize", "minimize"], + storage=storage, + ) + + if i == 1: + study.enqueue_trial( + { + "x0": 0, + "x1": 1, + "x2": 0, + "x3": 0, + "x4": 0, + "x5": 0, + "x6": 0, + "x7": 0, + "x8": 0, + "x9": 0, + "x10": 0, + "x11": 0, + "x12": 0, + "x13": 0, + "x14": 0, + "x15": 0, + "x16": 0, + "x17": 0, + "x18": 0, + "x19": 0, + "x20": 0, + "x21": 0, + "x22": 0, + "x23": 0, + "x24": 0, + "x25": 0, + "x26": 0, + "x27": 0, + "x28": 0, + "x29": 0, + } + ) + study.enqueue_trial( + { + "x0": 0.5, + "x1": 1, + "x2": 0, + "x3": 0, + "x4": 0, + "x5": 0, + "x6": 0, + "x7": 0, + "x8": 0, + "x9": 0, + "x10": 0, + "x11": 0, + "x12": 0, + "x13": 0, + "x14": 0, + "x15": 0, + "x16": 0, + "x17": 0, + "x18": 0, + "x19": 0, + "x20": 0, + "x21": 0, + "x22": 0, + "x23": 0, + "x24": 0, + "x25": 0, + "x26": 0, + "x27": 0, + "x28": 0, + "x29": 0, + } + ) + study.enqueue_trial( + { + "x0": 1, + "x1": 1, + "x2": 0, + "x3": 0, + "x4": 0, + "x5": 0, + "x6": 0, + "x7": 0, + "x8": 0, + "x9": 0, + "x10": 0, + "x11": 0, + "x12": 0, + "x13": 0, + "x14": 0, + "x15": 0, + "x16": 0, + "x17": 0, + "x18": 0, + "x19": 0, + "x20": 0, + "x21": 0, + "x22": 0, + "x23": 0, + "x24": 0, + "x25": 0, + "x26": 0, + "x27": 0, + "x28": 0, + "x29": 0, + } + ) + + study.optimize(objective, n_trials=n_trials) + studies.append(study) + + optuna.visualization.plot_pareto_front(study).show() + +sampler1 = optuna.samplers.QMCSampler(seed=seed, qmc_type="halton", scramble=True) +study = optuna.create_study( + sampler=sampler1, + study_name="Random+NSGAII", + directions=["minimize", "minimize"], + storage=storage, +) +study.optimize(objective, n_trials=2 * n_trials) +sampler2 = NSGAIIwITSampler( + population_size=population_size, + seed=seed, + crossover=BLXAlphaCrossover(), +) +study = optuna.create_study( + sampler=sampler2, + study_name="Random+NSGAII", + directions=["minimize", "minimize"], + storage=storage, + load_if_exists=True, +) +study.optimize(objective, n_trials=n_trials // 2) +optuna.visualization.plot_pareto_front(study).show() +studies.append(study) + + +m = optunahub.load_module("visualization/plot_pareto_front_multi") +fig = m.plot_pareto_front(studies) +fig.show() diff --git a/package/samplers/nsgaii_with_initial_trials/nsgaii_with_initial_trials.py b/package/samplers/nsgaii_with_initial_trials/nsgaii_with_initial_trials.py new file mode 100644 index 00000000..08ed0077 --- /dev/null +++ b/package/samplers/nsgaii_with_initial_trials/nsgaii_with_initial_trials.py @@ -0,0 +1,385 @@ +from __future__ import annotations + +from collections import defaultdict +from collections.abc import Callable +from collections.abc import Sequence +import hashlib +from typing import Any +from typing import TYPE_CHECKING + +import optuna +from optuna._experimental import warn_experimental_argument +from optuna.distributions import BaseDistribution +from optuna.samplers._base import BaseSampler +from optuna.samplers._lazy_random_state import LazyRandomState +from optuna.samplers._random import RandomSampler +from optuna.samplers.nsgaii._after_trial_strategy import NSGAIIAfterTrialStrategy +from optuna.samplers.nsgaii._child_generation_strategy import NSGAIIChildGenerationStrategy +from optuna.samplers.nsgaii._crossovers._base import BaseCrossover +from optuna.samplers.nsgaii._crossovers._uniform import UniformCrossover +from optuna.samplers.nsgaii._elite_population_selection_strategy import ( + NSGAIIElitePopulationSelectionStrategy, +) +from optuna.search_space import IntersectionSearchSpace +from optuna.trial import FrozenTrial +from optuna.trial import TrialState + + +if TYPE_CHECKING: + from optuna.study import Study + + +# Define key names of `Trial.system_attrs`. +_GENERATION_KEY = "nsga2wit:generation" +_POPULATION_CACHE_KEY_PREFIX = "nsga2wit:population" + + +class NSGAIIwITSampler(BaseSampler): + """Multi-objective sampler using the NSGA-II algorithm. + + NSGA-II stands for "Nondominated Sorting Genetic Algorithm II", + which is a well known, fast and elitist multi-objective genetic algorithm. + + For further information about NSGA-II, please refer to the following paper: + + - `A fast and elitist multiobjective genetic algorithm: NSGA-II + `__ + + .. note:: + :class:`~optuna.samplers.TPESampler` became much faster in v4.0.0 and supports several + features not supported by ``NSGAIISampler`` such as handling of dynamic search + space and categorical distance. To use :class:`~optuna.samplers.TPESampler`, you need to + explicitly specify the sampler as follows: + + .. testcode:: + + import optuna + + + def objective(trial): + x = trial.suggest_float("x", -100, 100) + y = trial.suggest_categorical("y", [-1, 0, 1]) + f1 = x**2 + y + f2 = -((x - 2) ** 2 + y) + return f1, f2 + + + # We minimize the first objective and maximize the second objective. + sampler = optuna.samplers.TPESampler() + study = optuna.create_study(directions=["minimize", "maximize"], sampler=sampler) + study.optimize(objective, n_trials=100) + + Please also check `our article + `__ + for more details of the speedup in v4.0.0. + + Args: + population_size: + Number of individuals (trials) in a generation. + ``population_size`` must be greater than or equal to ``crossover.n_parents``. + For :class:`~optuna.samplers.nsgaii.UNDXCrossover` and + :class:`~optuna.samplers.nsgaii.SPXCrossover`, ``n_parents=3``, and for the other + algorithms, ``n_parents=2``. + + mutation_prob: + Probability of mutating each parameter when creating a new individual. + If :obj:`None` is specified, the value ``1.0 / len(parent_trial.params)`` is used + where ``parent_trial`` is the parent trial of the target individual. + + crossover: + Crossover to be applied when creating child individuals. + The available crossovers are listed here: + https://optuna.readthedocs.io/en/stable/reference/samplers/nsgaii.html. + + :class:`~optuna.samplers.nsgaii.UniformCrossover` is always applied to parameters + sampled from :class:`~optuna.distributions.CategoricalDistribution`, and by + default for parameters sampled from other distributions unless this argument + is specified. + + For more information on each of the crossover method, please refer to + specific crossover documentation. + + crossover_prob: + Probability that a crossover (parameters swapping between parents) will occur + when creating a new individual. + + swapping_prob: + Probability of swapping each parameter of the parents during crossover. + + seed: + Seed for random number generator. + + constraints_func: + An optional function that computes the objective constraints. It must take a + :class:`~optuna.trial.FrozenTrial` and return the constraints. The return value must + be a sequence of :obj:`float` s. A value strictly larger than 0 means that a + constraints is violated. A value equal to or smaller than 0 is considered feasible. + If ``constraints_func`` returns more than one value for a trial, that trial is + considered feasible if and only if all values are equal to 0 or smaller. + + The ``constraints_func`` will be evaluated after each successful trial. + The function won't be called when trials fail or they are pruned, but this behavior is + subject to change in the future releases. + + The constraints are handled by the constrained domination. A trial x is said to + constrained-dominate a trial y, if any of the following conditions is true: + + 1. Trial x is feasible and trial y is not. + 2. Trial x and y are both infeasible, but trial x has a smaller overall violation. + 3. Trial x and y are feasible and trial x dominates trial y. + + .. note:: + Added in v2.5.0 as an experimental feature. The interface may change in newer + versions without prior notice. See + https://github.com/optuna/optuna/releases/tag/v2.5.0. + + elite_population_selection_strategy: + The selection strategy for determining the individuals to survive from the current + population pool. Default to :obj:`None`. + + .. note:: + The arguments ``elite_population_selection_strategy`` was added in v3.3.0 as an + experimental feature. The interface may change in newer versions without prior + notice. + See https://github.com/optuna/optuna/releases/tag/v3.3.0. + + child_generation_strategy: + The strategy for generating child parameters from parent trials. Defaults to + :obj:`None`. + + .. note:: + The arguments ``child_generation_strategy`` was added in v3.3.0 as an experimental + feature. The interface may change in newer versions without prior notice. + See https://github.com/optuna/optuna/releases/tag/v3.3.0. + + after_trial_strategy: + A set of procedure to be conducted after each trial. Defaults to :obj:`None`. + + .. note:: + The arguments ``after_trial_strategy`` was added in v3.3.0 as an experimental + feature. The interface may change in newer versions without prior notice. + See https://github.com/optuna/optuna/releases/tag/v3.3.0. + """ + + def __init__( + self, + *, + n_init_trials: int = 50, + population_size: int = 50, + mutation_prob: float | None = None, + crossover: BaseCrossover | None = None, + crossover_prob: float = 0.9, + swapping_prob: float = 0.5, + seed: int | None = None, + constraints_func: Callable[[FrozenTrial], Sequence[float]] | None = None, + elite_population_selection_strategy: ( + Callable[[Study, list[FrozenTrial]], list[FrozenTrial]] | None + ) = None, + child_generation_strategy: ( + Callable[[Study, dict[str, BaseDistribution], list[FrozenTrial]], dict[str, Any]] + | None + ) = None, + after_trial_strategy: ( + Callable[[Study, FrozenTrial, TrialState, Sequence[float] | None], None] | None + ) = None, + ) -> None: + # TODO(ohta): Reconsider the default value of each parameter. + + if population_size < 2: + raise ValueError("`population_size` must be greater than or equal to 2.") + + if constraints_func is not None: + warn_experimental_argument("constraints_func") + if after_trial_strategy is not None: + warn_experimental_argument("after_trial_strategy") + + if child_generation_strategy is not None: + warn_experimental_argument("child_generation_strategy") + + if elite_population_selection_strategy is not None: + warn_experimental_argument("elite_population_selection_strategy") + + if crossover is None: + crossover = UniformCrossover(swapping_prob) + + if not isinstance(crossover, BaseCrossover): + raise ValueError( + f"'{crossover}' is not a valid crossover." + " For valid crossovers see" + " https://optuna.readthedocs.io/en/stable/reference/samplers.html." + ) + + if population_size < crossover.n_parents: + raise ValueError( + f"Using {crossover}," + f" the population size should be greater than or equal to {crossover.n_parents}." + f" The specified `population_size` is {population_size}." + ) + + self._n_init_trials = n_init_trials + self._population_size = population_size + self._random_sampler = RandomSampler(seed=seed) + self._rng = LazyRandomState(seed) + self._constraints_func = constraints_func + self._search_space = IntersectionSearchSpace() + + self._elite_population_selection_strategy = ( + elite_population_selection_strategy + or NSGAIIElitePopulationSelectionStrategy( + population_size=population_size, constraints_func=constraints_func + ) + ) + self._child_generation_strategy = ( + child_generation_strategy + or NSGAIIChildGenerationStrategy( + crossover_prob=crossover_prob, + mutation_prob=mutation_prob, + swapping_prob=swapping_prob, + crossover=crossover, + constraints_func=constraints_func, + rng=self._rng, + ) + ) + self._after_trial_strategy = after_trial_strategy or NSGAIIAfterTrialStrategy( + constraints_func=constraints_func + ) + + def reseed_rng(self) -> None: + self._random_sampler.reseed_rng() + self._rng.rng.seed() + + def infer_relative_search_space( + self, study: Study, trial: FrozenTrial + ) -> dict[str, BaseDistribution]: + search_space: dict[str, BaseDistribution] = {} + for name, distribution in self._search_space.calculate(study).items(): + if distribution.single(): + # The `untransform` method of `optuna._transform._SearchSpaceTransform` + # does not assume a single value, + # so single value objects are not sampled with the `sample_relative` method, + # but with the `sample_independent` method. + continue + search_space[name] = distribution + return search_space + + def sample_relative( + self, + study: Study, + trial: FrozenTrial, + search_space: dict[str, BaseDistribution], + ) -> dict[str, Any]: + parent_generation, parent_population = self._collect_parent_population(study) + + generation = parent_generation + 1 + study._storage.set_trial_system_attr(trial._trial_id, _GENERATION_KEY, generation) + + if parent_generation < 0: + return {} + + return self._child_generation_strategy(study, search_space, parent_population) + + def sample_independent( + self, + study: Study, + trial: FrozenTrial, + param_name: str, + param_distribution: BaseDistribution, + ) -> Any: + # Following parameters are randomly sampled here. + # 1. A parameter in the initial population/first generation. + # 2. A parameter to mutate. + # 3. A parameter excluded from the intersection search space. + + return self._random_sampler.sample_independent( + study, trial, param_name, param_distribution + ) + + def _collect_parent_population(self, study: Study) -> tuple[int, list[FrozenTrial]]: + trials = study._get_trials(deepcopy=False, use_cache=True) + + generation_to_runnings = defaultdict(list) + generation_to_population = defaultdict(list) + for trial in trials: + if _GENERATION_KEY not in trial.system_attrs: + generation = 0 + else: + generation = trial.system_attrs[_GENERATION_KEY] + + if trial.state != optuna.trial.TrialState.COMPLETE: + if trial.state == optuna.trial.TrialState.RUNNING: + generation_to_runnings[generation].append(trial) + continue + + # Do not use trials whose states are not COMPLETE, or `constraint` will be unavailable. + generation_to_population[generation].append(trial) + + hasher = hashlib.sha256() + parent_population: list[FrozenTrial] = [] + parent_generation = -1 + while True: + generation = parent_generation + 1 + population = generation_to_population[generation] + + # Under multi-worker settings, the population size might become larger than + # `self._population_size`. + if len(population) < self._population_size: + break + + # [NOTE] + # It's generally safe to assume that once the above condition is satisfied, + # there are no additional individuals added to the generation (i.e., the members of + # the generation have been fixed). + # If the number of parallel workers is huge, this assumption can be broken, but + # this is a very rare case and doesn't significantly impact optimization performance. + # So we can ignore the case. + + # The cache key is calculated based on the key of the previous generation and + # the remaining running trials in the current population. + # If there are no running trials, the new cache key becomes exactly the same as + # the previous one, and the cached content will be overwritten. This allows us to + # skip redundant cache key calculations when this method is called for the subsequent + # trials. + for trial in generation_to_runnings[generation]: + hasher.update(bytes(str(trial.number), "utf-8")) + + cache_key = "{}:{}".format(_POPULATION_CACHE_KEY_PREFIX, hasher.hexdigest()) + study_system_attrs = study._storage.get_study_system_attrs(study._study_id) + cached_generation, cached_population_numbers = study_system_attrs.get( + cache_key, (-1, []) + ) + if cached_generation >= generation: + generation = cached_generation + population = [trials[n] for n in cached_population_numbers] + else: + population.extend(parent_population) + population = self._elite_population_selection_strategy(study, population) + + # To reduce the number of system attribute entries, + # we cache the population information only if there are no running trials + # (i.e., the information of the population has been fixed). + # Usually, if there are no too delayed running trials, the single entry + # will be used. + if len(generation_to_runnings[generation]) == 0: + population_numbers = [t.number for t in population] + study._storage.set_study_system_attr( + study._study_id, cache_key, (generation, population_numbers) + ) + + parent_generation = generation + parent_population = population + + return parent_generation, parent_population + + def before_trial(self, study: Study, trial: FrozenTrial) -> None: + self._random_sampler.before_trial(study, trial) + + def after_trial( + self, + study: Study, + trial: FrozenTrial, + state: TrialState, + values: Sequence[float] | None, + ) -> None: + assert state in [TrialState.COMPLETE, TrialState.FAIL, TrialState.PRUNED] + self._after_trial_strategy(study, trial, state, values) + self._random_sampler.after_trial(study, trial, state, values) From 3f5883f596696079eddadf02416476c37feb143d Mon Sep 17 00:00:00 2001 From: hrntsm Date: Fri, 18 Oct 2024 17:35:20 +0900 Subject: [PATCH 2/3] Update README --- .../nsgaii_with_initial_trials/README.md | 90 ++++---- .../nsgaii_with_initial_trials/example.py | 210 +++--------------- 2 files changed, 69 insertions(+), 231 deletions(-) diff --git a/package/samplers/nsgaii_with_initial_trials/README.md b/package/samplers/nsgaii_with_initial_trials/README.md index 2c96998f..25ac50dd 100644 --- a/package/samplers/nsgaii_with_initial_trials/README.md +++ b/package/samplers/nsgaii_with_initial_trials/README.md @@ -1,35 +1,24 @@ --- author: Hiroaki Natsume -title: MOEA/D sampler -description: Sampler using MOEA/D algorithm. MOEA/D stands for "Multi-Objective Evolutionary Algorithm based on Decomposition. -tags: [Sampler, Multi-Objective Optimization, Evolutionary Algorithms] +title: NSGAII sampler with Initial Trials +description: Sampler using NSGAII algorithm with initial trials. +tags: [Sampler, Multi-Objective, Genetic Algorithm] optuna_versions: [4.0.0] license: MIT License --- ## Abstract -Sampler using MOEA/D algorithm. MOEA/D stands for "Multi-Objective Evolutionary Algorithm based on Decomposition. +If Optuna's built-in NSGAII has a study obtained from another sampler, but continues with that study, it cannot be used as the first generation, and optimization starts from zero. +This means that even if you already know good individuals, you cannot use it in the GA. +In this implementation, the already sampled results are included in the initial individuals of the GA to perform the optimization. -This sampler is specialized for multiobjective optimization. The objective function is internally decomposed into multiple single-objective subproblems to perform optimization. - -It may not work well with multi-threading. Check results carefully. +Note, however, that this has the effect that the implementation does not necessarily support multi-threading in the generation of the initial generation. +After the initial generation, the implementation is similar to the built-in NSGAII. ## Class or Function Names -- MOEADSampler - -## Installation - -``` -pip install scipy -``` - -or - -``` -pip install -r https://hub.optuna.org/samplers/moead/requirements.txt -``` +- NSGAIIwITSampler ## Example @@ -37,45 +26,52 @@ pip install -r https://hub.optuna.org/samplers/moead/requirements.txt import optuna import optunahub + def objective(trial: optuna.Trial) -> tuple[float, float]: x = trial.suggest_float("x", 0, 5) y = trial.suggest_float("y", 0, 3) - v0 = 4 * x**2 + 4 * y**2 v1 = (x - 5) ** 2 + (y - 5) ** 2 return v0, v1 -population_size = 100 -n_trials = 1000 +storage = optuna.storages.InMemoryStorage() -mod = optunahub.load_module("samplers/moead") -sampler = mod.MOEADSampler( - population_size=population_size, - scalar_aggregation_func="tchebycheff", - n_neighbors=population_size // 10, +# Sampling 0 generation using enqueueing & qmc sampler +study = optuna.create_study( + directions=["minimize", "minimize"], + sampler=optuna.samplers.QMCSampler(seed=42), + study_name="test", + storage=storage, +) +study.enqueue_trial( + { + "x": 0, + "y": 0, + } +) +study.optimize(objective, n_trials=128) + +# Using sampling results as the initial generation +sampler = optunahub.load_module( + "samplers/nsgaii_with_initial_trials", +).NSGAIIwITSampler(population_size=25, seed=42) + +study = optuna.create_study( + directions=["minimize", "minimize"], + sampler=sampler, + study_name="test", + storage=storage, + load_if_exists=True, ) -study = optuna.create_study(sampler=sampler) -study.optimize(objective, n_trials=n_trials) +study.optimize(objective, n_trials=100) + +optuna.visualization.plot_pareto_front(study).show() ``` ## Others -Comparison between Random, NSGAII and MOEA/D with ZDT1 as the objective function. -See `compare_2objective.py` in moead directory for details. - -### Pareto Front Plot - -| MOEA/D | NSGAII | Random | -| --------------------------- | ---------------------------- | ---------------------------- | -| ![MOEA/D](images/moead.png) | ![NSGAII](images/nsgaii.png) | ![Random](images/random.png) | - -### Compare - -![Compare](images/compare_pareto_front.png) - -### Reference +The implementation is similar to Optuna's NSGAII except for the handling of initial generations. The license and documentation are below. -Q. Zhang and H. Li, -"MOEA/D: A Multiobjective Evolutionary Algorithm Based on Decomposition," in IEEE Transactions on Evolutionary Computation, vol. 11, no. 6, pp. 712-731, Dec. 2007, -[doi: 10.1109/TEVC.2007.892759](https://doi.org/10.1109/TEVC.2007.892759). +- [Documentation](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.NSGAIISampler.html) +- [License](https://github.com/optuna/optuna/blob/master/LICENSE) diff --git a/package/samplers/nsgaii_with_initial_trials/example.py b/package/samplers/nsgaii_with_initial_trials/example.py index 8c94b3f8..db74bf68 100644 --- a/package/samplers/nsgaii_with_initial_trials/example.py +++ b/package/samplers/nsgaii_with_initial_trials/example.py @@ -1,202 +1,44 @@ -import numpy as np import optuna -from optuna.samplers import NSGAIISampler -from optuna.samplers.nsgaii import BLXAlphaCrossover -import optuna.storages.journal import optunahub -from nsgaii_with_initial_trials import NSGAIIwITSampler - - -file_path = "./journal.log" -lock_obj = optuna.storages.journal.JournalFileOpenLock(file_path) - -storage = optuna.storages.JournalStorage( - optuna.storages.journal.JournalFileBackend(file_path, lock_obj=lock_obj), -) - -storage = optuna.storages.InMemoryStorage() - def objective(trial: optuna.Trial) -> tuple[float, float]: - # ZDT1 - n_variables = 30 - - x = np.array([trial.suggest_float(f"x{i}", 0, 1) for i in range(n_variables)]) - g = 1 + 9 * np.sum(x[1:]) / (n_variables - 1) - f1 = x[0] - f2 = g * (1 - (f1 / g) ** 0.5) - - return f1, f2 - - -population_size = 50 -n_trials = 1000 -seed = 42 + x = trial.suggest_float("x", 0, 5) + y = trial.suggest_float("y", 0, 3) + v0 = 4 * x**2 + 4 * y**2 + v1 = (x - 5) ** 2 + (y - 5) ** 2 + return v0, v1 -samplers = [ - NSGAIISampler( - population_size=population_size, - seed=seed, - crossover=BLXAlphaCrossover(), - ), - # NSGAIIwITSampler( - # population_size=population_size, - # seed=seed, - # crossover=BLXAlphaCrossover(), - # ), - NSGAIIwITSampler( - population_size=population_size, - seed=seed, - crossover=BLXAlphaCrossover(), - ), -] - -studies = [] -title = ["NSGAII", "NSGAIIwInitialTrials"] -for i, sampler in enumerate(samplers): - study = optuna.create_study( - sampler=sampler, - study_name=title[i], - directions=["minimize", "minimize"], - storage=storage, - ) - - if i == 1: - study.enqueue_trial( - { - "x0": 0, - "x1": 1, - "x2": 0, - "x3": 0, - "x4": 0, - "x5": 0, - "x6": 0, - "x7": 0, - "x8": 0, - "x9": 0, - "x10": 0, - "x11": 0, - "x12": 0, - "x13": 0, - "x14": 0, - "x15": 0, - "x16": 0, - "x17": 0, - "x18": 0, - "x19": 0, - "x20": 0, - "x21": 0, - "x22": 0, - "x23": 0, - "x24": 0, - "x25": 0, - "x26": 0, - "x27": 0, - "x28": 0, - "x29": 0, - } - ) - study.enqueue_trial( - { - "x0": 0.5, - "x1": 1, - "x2": 0, - "x3": 0, - "x4": 0, - "x5": 0, - "x6": 0, - "x7": 0, - "x8": 0, - "x9": 0, - "x10": 0, - "x11": 0, - "x12": 0, - "x13": 0, - "x14": 0, - "x15": 0, - "x16": 0, - "x17": 0, - "x18": 0, - "x19": 0, - "x20": 0, - "x21": 0, - "x22": 0, - "x23": 0, - "x24": 0, - "x25": 0, - "x26": 0, - "x27": 0, - "x28": 0, - "x29": 0, - } - ) - study.enqueue_trial( - { - "x0": 1, - "x1": 1, - "x2": 0, - "x3": 0, - "x4": 0, - "x5": 0, - "x6": 0, - "x7": 0, - "x8": 0, - "x9": 0, - "x10": 0, - "x11": 0, - "x12": 0, - "x13": 0, - "x14": 0, - "x15": 0, - "x16": 0, - "x17": 0, - "x18": 0, - "x19": 0, - "x20": 0, - "x21": 0, - "x22": 0, - "x23": 0, - "x24": 0, - "x25": 0, - "x26": 0, - "x27": 0, - "x28": 0, - "x29": 0, - } - ) - - study.optimize(objective, n_trials=n_trials) - studies.append(study) - - optuna.visualization.plot_pareto_front(study).show() - -sampler1 = optuna.samplers.QMCSampler(seed=seed, qmc_type="halton", scramble=True) +storage = optuna.storages.InMemoryStorage() +# Sampling 0 generation using enqueueing & qmc sampler study = optuna.create_study( - sampler=sampler1, - study_name="Random+NSGAII", directions=["minimize", "minimize"], + sampler=optuna.samplers.QMCSampler(seed=42), + study_name="test", storage=storage, ) -study.optimize(objective, n_trials=2 * n_trials) -sampler2 = NSGAIIwITSampler( - population_size=population_size, - seed=seed, - crossover=BLXAlphaCrossover(), +study.enqueue_trial( + { + "x": 0, + "y": 0, + } ) +study.optimize(objective, n_trials=128) + +# Using previous sampling results as the initial generation, +# sampled by NSGAII. +sampler = optunahub.load_module( + "samplers/nsgaii_with_initial_trials", +).NSGAIIwITSampler(population_size=25, seed=42) + study = optuna.create_study( - sampler=sampler2, - study_name="Random+NSGAII", directions=["minimize", "minimize"], + sampler=sampler, + study_name="test", storage=storage, load_if_exists=True, ) -study.optimize(objective, n_trials=n_trials // 2) -optuna.visualization.plot_pareto_front(study).show() -studies.append(study) +study.optimize(objective, n_trials=100) - -m = optunahub.load_module("visualization/plot_pareto_front_multi") -fig = m.plot_pareto_front(studies) -fig.show() +optuna.visualization.plot_pareto_front(study).show() From 24e248cd9ed05d97ec19fafc117f93beba6cd620 Mon Sep 17 00:00:00 2001 From: hrntsm Date: Tue, 22 Oct 2024 10:07:26 +0900 Subject: [PATCH 3/3] Clear unused argument --- .../nsgaii_with_initial_trials/nsgaii_with_initial_trials.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/package/samplers/nsgaii_with_initial_trials/nsgaii_with_initial_trials.py b/package/samplers/nsgaii_with_initial_trials/nsgaii_with_initial_trials.py index 08ed0077..87a3f4ca 100644 --- a/package/samplers/nsgaii_with_initial_trials/nsgaii_with_initial_trials.py +++ b/package/samplers/nsgaii_with_initial_trials/nsgaii_with_initial_trials.py @@ -164,7 +164,6 @@ def objective(trial): def __init__( self, *, - n_init_trials: int = 50, population_size: int = 50, mutation_prob: float | None = None, crossover: BaseCrossover | None = None, @@ -216,7 +215,6 @@ def __init__( f" The specified `population_size` is {population_size}." ) - self._n_init_trials = n_init_trials self._population_size = population_size self._random_sampler = RandomSampler(seed=seed) self._rng = LazyRandomState(seed)