Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GeneticAlgorithmEmitter with Internal Operator Support #427

Merged
merged 10 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#### API

- Add GeneticAlgorithmEmitter with Internal Operator Support ({pr} `427`)
- Support custom data fields in archive ({pr}`421`)
- **Backwards-incompatible:** Remove `_batch` from parameter names ({pr}`422`,
{pr}`424`, {pr}`425`)
Expand Down
3 changes: 3 additions & 0 deletions ribs/emitters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

ribs.emitters.EvolutionStrategyEmitter
ribs.emitters.GaussianEmitter
ribs.emitters.GeneticAlgorithmEmitter
ribs.emitters.GradientArborescenceEmitter
ribs.emitters.GradientOperatorEmitter
ribs.emitters.IsoLineEmitter
Expand All @@ -25,6 +26,7 @@
from ribs.emitters._emitter_base import EmitterBase
from ribs.emitters._evolution_strategy_emitter import EvolutionStrategyEmitter
from ribs.emitters._gaussian_emitter import GaussianEmitter
from ribs.emitters._genetic_algorithm_emitter import GeneticAlgorithmEmitter
from ribs.emitters._gradient_arborescence_emitter import \
GradientArborescenceEmitter
from ribs.emitters._gradient_operator_emitter import GradientOperatorEmitter
Expand All @@ -33,6 +35,7 @@
__all__ = [
"EvolutionStrategyEmitter",
"GaussianEmitter",
"GeneticAlgorithmEmitter",
"GradientArborescenceEmitter",
"GradientOperatorEmitter",
"IsoLineEmitter",
Expand Down
151 changes: 151 additions & 0 deletions ribs/emitters/_genetic_algorithm_emitter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
"""Provides the GeneticAlgorithmEmitter."""
import numpy as np

from ribs._utils import check_batch_shape, check_shape
from ribs.emitters._emitter_base import EmitterBase
from ribs.emitters.operators import _get_op


class GeneticAlgorithmEmitter(EmitterBase):
"""Emits solutions by using operator provided.

If the archive is empty and ``self._initial_solutions`` is set, a call to
:meth:`ask` will return ``self._initial_solutions``. If
``self._initial_solutions`` is not set, we operate on self.x0.


Args:
archive (ribs.archives.ArchiveBase): An archive to use when creating and
inserting solutions. For instance, this can be
:class:`ribs.archives.GridArchive`.
x0 (numpy.ndarray): Initial solution.
operator (str): Internal Operator Class used to Mutate Solutions
in ask method.
operator_kwargs (dict): Additional arguments to pass to the operator.
See :mod:`ribs.emitters.operators` for the arguments allowed by each
operator.
initial_solutions (array-like): An (n, solution_dim) array of solutions
to be used when the archive is empty. If this argument is None, then
solutions will be sampled from a Gaussian distribution centered at
``x0`` with standard deviation ``sigma``.
bounds (None or array-like): Bounds of the solution space. Solutions are
clipped to these bounds. Pass None to indicate there are no bounds.
Alternatively, pass an array-like to specify the bounds for each
dim. Each element in this array-like can be None to indicate no
bound, or a tuple of ``(lower_bound, upper_bound)``, where
``lower_bound`` or ``upper_bound`` may be None to indicate no bound.
batch_size (int): Number of solutions to return in :meth:`ask`.
Raises:
ValueError: There is an error in x0 or initial_solutions.
ValueError: There is an error in the bounds configuration.
"""

def __init__(self,
archive,
*,
x0=None,
initial_solutions=None,
bounds=None,
batch_size=64,
operator_kwargs=None,
svott03 marked this conversation as resolved.
Show resolved Hide resolved
operator=None):
self._batch_size = batch_size
self._x0 = x0
self._initial_solutions = None

if operator is None:
raise ValueError("Operator must be provided.")

if x0 is None and initial_solutions is None:
raise ValueError("Either x0 or initial_solutions must be provided.")
if x0 is not None and initial_solutions is not None:
raise ValueError(
"x0 and initial_solutions cannot both be provided.")

if x0 is not None:
self._x0 = np.array(x0, dtype=archive.dtype)
check_shape(self._x0, "x0", archive.solution_dim,
"archive.solution_dim")
elif initial_solutions is not None:
self._initial_solutions = np.asarray(initial_solutions,
dtype=archive.dtype)
check_batch_shape(self._initial_solutions, "initial_solutions",
archive.solution_dim, "archive.solution_dim")

EmitterBase.__init__(
svott03 marked this conversation as resolved.
Show resolved Hide resolved
self,
archive,
solution_dim=archive.solution_dim,
bounds=bounds,
)

supported_operators = ['gaussian', 'isoline']
svott03 marked this conversation as resolved.
Show resolved Hide resolved

if operator not in supported_operators:
raise ValueError(f"{operator} is not a supported operator.")

self._operator = _get_op(operator)(
lower_bounds=self._lower_bounds,
upper_bounds=self._upper_bounds,
**(operator_kwargs if operator_kwargs is not None else {}))

@property
def x0(self):
"""numpy.ndarray: Initial Solution (if initial_solutions is not
set)."""
return self._x0

@property
svott03 marked this conversation as resolved.
Show resolved Hide resolved
def initial_solutions(self):
"""numpy.ndarray: The initial solutions which are returned when the
archive is empty (if x0 is not set)."""
return self._initial_solutions

@property
def batch_size(self):
"""int: Number of solutions to return in :meth:`ask`."""
return self._batch_size

def ask(self):
"""Creates solutions with operator provided.

svott03 marked this conversation as resolved.
Show resolved Hide resolved

If the archive is empty and ``self._initial_solutions`` is set, we
return ``self._initial_solutions``. If ``self._initial_solutions`` is
not set and the archive is still empty, we operate on the initial
solution (x0) provided. Otherwise, we sample parents from the archive
to be used as input to our operator
svott03 marked this conversation as resolved.
Show resolved Hide resolved

Returns:
If the archive is not empty, ``(batch_size, solution_dim)`` array
svott03 marked this conversation as resolved.
Show resolved Hide resolved
-- contains ``batch_size`` new solutions to evaluate. If the
archive is empty, we return ``self._initial_solutions``, which
might not have ``batch_size`` solutions.
"""

if self.archive.empty and self._initial_solutions is not None:
return np.clip(self._initial_solutions, self.lower_bounds,
self.upper_bounds)

if self._operator.parent_type == 2: # isoline
svott03 marked this conversation as resolved.
Show resolved Hide resolved
if self.archive.empty:
parents = np.repeat(np.repeat(self.x0[None],
svott03 marked this conversation as resolved.
Show resolved Hide resolved
repeats=self._batch_size,
axis=0)[None],
2,
axis=0)
else:
parents = self.archive.sample_elites(
2 * self._batch_size)["solution"]
return self._operator.ask(
parents=parents.reshape(2, self._batch_size, -1))
else: # self._operator.parent_type == 1, gaussian
if self.archive.empty:
parents = np.repeat(self.x0[None],
repeats=self._batch_size,
axis=0)
else:
parents = self.archive.sample_elites(
self._batch_size)["solution"]

return self._operator.ask(parents=parents)
16 changes: 7 additions & 9 deletions ribs/emitters/_iso_line_emitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,15 +152,13 @@ def ask(self):
self.upper_bounds)

if self.archive.empty:
iso_gaussian = self._rng.normal(
scale=self._iso_sigma,
size=(self._batch_size, self.solution_dim),
).astype(self.archive.dtype)

solution_batch = np.expand_dims(self._x0, axis=0) + iso_gaussian
return np.clip(solution_batch, self.lower_bounds, self.upper_bounds)
parents = np.repeat(np.repeat(self.x0[None],
svott03 marked this conversation as resolved.
Show resolved Hide resolved
repeats=self._batch_size,
axis=0)[None],
2,
axis=0)
else:
parents = self.archive.sample_elites(2 *
self._batch_size)["solution"]
return self._operator.ask(
parents=parents.reshape(2, self._batch_size, -1))
return self._operator.ask(
parents=parents.reshape(2, self._batch_size, -1))
18 changes: 15 additions & 3 deletions ribs/emitters/operators/_gaussian.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,34 @@ class GaussianOperator(OperatorBase):
by emitter
upper_bounds (array-like): Upper bounds of the solution space. Passed in
by emitter
parent_type (int): Exposed data member so the selector class knows
appropriate parent dimensions.
seed (int): Value to seed the random number generator. Set to None to
avoid a fixed seed.
"""

def __init__(self, sigma, seed, lower_bounds, upper_bounds):

def __init__(self,
sigma,
lower_bounds,
upper_bounds,
parent_type=1,
svott03 marked this conversation as resolved.
Show resolved Hide resolved
seed=None):
self._sigma = sigma
self._lower_bounds = lower_bounds
self._upper_bounds = upper_bounds
self._parent_type = parent_type

self._rng = np.random.default_rng(seed)

@property
def parent_type(self):
"""int: Parent Type to be used by selector."""
svott03 marked this conversation as resolved.
Show resolved Hide resolved
return self._parent_type

def ask(self, parents):
"""Adds Gaussian noise to parents.

Args:
Args:
parents (array-like): (batch_size, solution_dim) array of
solutions to be mutated.

Expand Down
23 changes: 20 additions & 3 deletions ribs/emitters/operators/_iso_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,47 @@ class IsoLineOperator(OperatorBase):
<https://arxiv.org/abs/1804.03906>`_.

Args:
iso_sigma (float): Scale factor for the isotropic distribution used to
generate solutions.
line_sigma (float): Scale factor for the line distribution used when
generating solutions.
sigma (float or array-like): Standard deviation of the Gaussian
distribution. Note we assume the Gaussian is diagonal, so if this
argument is an array, it must be 1D.
lower_bounds (array-like): Upper bounds of the solution space. Passed in
by emitter
upper_bounds (array-like): Upper bounds of the solution space. Passed in
by emitter
parent_type (int): Exposed data member so the selector class knows
appropriate parent dimensions.
seed (int): Value to seed the random number generator. Set to None to
avoid a fixed seed.
"""

def __init__(self, iso_sigma, line_sigma, lower_bounds, upper_bounds, seed):

def __init__(self,
lower_bounds,
upper_bounds,
parent_type=2,
svott03 marked this conversation as resolved.
Show resolved Hide resolved
iso_sigma=0.01,
svott03 marked this conversation as resolved.
Show resolved Hide resolved
line_sigma=0.2,
seed=None):
self._iso_sigma = iso_sigma
self._line_sigma = line_sigma
self._lower_bounds = lower_bounds
self._upper_bounds = upper_bounds
self._parent_type = parent_type

self._rng = np.random.default_rng(seed)

@property
def parent_type(self):
"""int: Parent Type to be used by selector."""
return self._parent_type

def ask(self, parents):
""" Adds Isotropic Guassian noise and directional noise to parents.

Args:
Args:
parents (array-like): (2, batch_size, solution_dim)
parents[0] array of solutions selected by emitter
parents[1] array of second batch of solutions passed by
Expand Down
Loading