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

Limiting fitness evaluations #238

Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 13 additions & 0 deletions cgp/ea/mu_plus_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ def __init__(
self.local_search = local_search
self.k_local_search = k_local_search

self.n_objective_calls: int = 0

def initialize_fitness_parents(
self, pop: Population, objective: Callable[[IndividualBase], IndividualBase]
) -> None:
Expand Down Expand Up @@ -145,6 +147,9 @@ def _create_new_offspring_generation(self, pop: Population) -> List[IndividualBa
def _compute_fitness(
self, combined: List[IndividualBase], objective: Callable[[IndividualBase], IndividualBase]
) -> List[IndividualBase]:

self.update_n_objective_calls(combined)

# computes fitness on all individuals, objective functions
# should return immediately if fitness is not None
if self.n_processes == 1:
Expand Down Expand Up @@ -182,3 +187,11 @@ def _create_new_parent_population(

"""
return combined[:n_parents]

def update_n_objective_calls(self, combined: List[IndividualBase]) -> None:
"""Increase n_objective_calls by the number of individuals with fitness=None,
i.e., for which the objective function will be evaluated.
"""
for individual in combined:
if individual.fitness is None:
self.n_objective_calls += 1
36 changes: 27 additions & 9 deletions cgp/hl_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ def evolve(
pop: Population,
objective: Callable[[IndividualBase], IndividualBase],
ea: MuPlusLambda,
max_generations: int,
min_fitness: float,
max_generations: int = np.inf,
max_objective_calls: int = np.inf,
print_progress: Optional[bool] = False,
callback: Optional[Callable[[Population], None]] = None,
n_processes: int = 1,
Expand All @@ -31,10 +32,15 @@ def evolve(
ea : EA algorithm instance
The evolution algorithm. Needs to be a class instance with an
`initialize_fitness_parents` and `step` method.
max_generations : int
Maximum number of generations.
min_fitness : float
Minimum fitness at which the evolution is stopped.
max_generations : int
Maximum number of generations.
Defaults to positive infinity.
Either this or `max_objective_calls` needs to be set to a finite value.
max_objective_calls: int
Maximum number of function evaluations.
Defaults to positive infinity.
print_progress : boolean, optional
Switch to print out the progress of the algorithm. Defaults to False.
callback : callable, optional
Expand All @@ -49,6 +55,8 @@ def evolve(
-------
None
"""
if np.isinf(max_generations) and np.isinf(max_objective_calls):
raise ValueError("Either max_generations or max_objective_calls must be finite.")

ea.initialize_fitness_parents(pop, objective)
if callback is not None:
Expand All @@ -57,7 +65,7 @@ def evolve(
# perform evolution
max_fitness = np.finfo(np.float).min
# Main loop: -1 offset since the last loop iteration will still increase generation by one
while pop.generation < max_generations - 1:
while pop.generation < max_generations - 1 and ea.n_objective_calls < max_objective_calls:

pop = ea.step(pop, objective)

Expand All @@ -69,11 +77,21 @@ def evolve(
max_fitness = pop.champion.fitness

if print_progress:
print(
f"\r[{pop.generation + 1}/{max_generations}] max fitness: {max_fitness}\033[K",
end="",
flush=True,
)
if np.isfinite(max_generations):
print(
f"\r[{pop.generation + 1}/{max_generations}] max fitness: {max_fitness}\033[K",
end="",
flush=True,
)
elif np.isfinite(max_objective_calls):
print(
f"\r[{ea.n_objective_calls}/{max_objective_calls}] "
f"max fitness: {max_fitness}\033[K",
end="",
flush=True,
)
else:
assert False # should never be reached

if callback is not None:
callback(pop)
Expand Down
40 changes: 40 additions & 0 deletions test/test_ea_mu_plus_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,43 @@ def test_create_new_parent_population(population_params, genome_params, ea_param
# we picked the first three individuals
new_parents = ea._create_new_parent_population(3, pop.parents)
assert new_parents == pop.parents[:3]


def test_update_n_objective_calls(population_params, genome_params, ea_params):
def objective(individual):
individual.fitness = float(individual.idx)
return individual

n_objective_calls_expected = 0
pop = cgp.Population(**population_params, genome_params=genome_params)
ea = cgp.ea.MuPlusLambda(**ea_params)
assert ea.n_objective_calls == n_objective_calls_expected

ea.initialize_fitness_parents(pop, objective)
n_objective_calls_expected = population_params["n_parents"]
assert ea.n_objective_calls == n_objective_calls_expected

n_generations = 100
for _ in range(n_generations):
offsprings = ea._create_new_offspring_generation(pop)
combined = offsprings + pop.parents
n_objective_calls_expected += sum([1 for ind in combined if ind.fitness is None])
combined = ea._compute_fitness(combined, objective)
assert n_objective_calls_expected == ea.n_objective_calls


def test_update_n_objective_calls_mutation_rate_one(population_params, genome_params, ea_params):
def objective(individual):
individual.fitness = float(individual.idx)
return individual

population_params["mutation_rate"] = 1.0
pop = cgp.Population(**population_params, genome_params=genome_params)
ea = cgp.ea.MuPlusLambda(**ea_params)
ea.initialize_fitness_parents(pop, objective)
n_objective_calls_expected = population_params["n_parents"]
n_step_calls = 100
for idx_current_step in range(n_step_calls):
ea.step(pop, objective)
n_objective_calls_expected += ea_params["n_offsprings"]
assert ea.n_objective_calls == n_objective_calls_expected
18 changes: 18 additions & 0 deletions test/test_hl_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,24 @@ def f1(x):
assert abs(pop.champion.fitness) == pytest.approx(0.0)


def test_finite_max_generations_or_max_objective_calls(
population_params, genome_params, ea_params
):
def objective(individual):
individual.fitness = float(individual.idx)
return individual

pop = cgp.Population(**population_params, genome_params=genome_params)
ea = cgp.ea.MuPlusLambda(**ea_params)
evolve_params = {
"max_generations": np.inf,
"min_fitness": 0,
"max_objective_calls": np.inf,
}
with pytest.raises(ValueError):
cgp.evolve(pop, objective, ea, **evolve_params)


def _objective_speedup_parallel_evolve(individual):

time.sleep(0.25)
Expand Down