Skip to content

Bug fixes #196

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

Merged
merged 4 commits into from
May 10, 2023
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
2 changes: 1 addition & 1 deletion .github/workflows/main_py310.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Testing PyGAD using PyTest
name: Python 3.10 Testing PyGAD using PyTest

on:
push:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/main_py311.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Testing PyGAD using PyTest
name: Python 3.11 Testing PyGAD using PyTest

on:
push:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/main_py37.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Testing PyGAD using PyTest
name: Python 3.7 Testing PyGAD using PyTest

on:
push:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/main_py38.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Testing PyGAD using PyTest
name: Python 3.8 Testing PyGAD using PyTest

on:
push:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/main_py39.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Testing PyGAD using PyTest
name: Python 3.9 Testing PyGAD using PyTest

on:
push:
Expand Down
338 changes: 274 additions & 64 deletions pygad/cnn/cnn.py

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pygad/gann/gann.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from ..nn import nn
import warnings

def validate_network_parameters(num_neurons_input,
num_neurons_output,
Expand Down Expand Up @@ -81,7 +82,7 @@ def validate_network_parameters(num_neurons_input,
else:
raise TypeError(unexpected_activation_type.format(activations_type=type(hidden_activations)))
else: # In case there are no hidden layers (num_hidden_layers == 0)
print("WARNING: There are no hidden layers however a value is assigned to the parameter 'hidden_activations'. It will be reset to [].")
warnings.warn("WARNING: There are no hidden layers however a value is assigned to the parameter 'hidden_activations'. It will be reset to [].")
hidden_activations = []

# If the value passed to the 'hidden_activations' parameter is actually a list, then its elements are checked to make sure the listed name(s) of the activation function(s) are supported.
Expand Down
112 changes: 90 additions & 22 deletions pygad/pygad.py
Original file line number Diff line number Diff line change
Expand Up @@ -1531,8 +1531,7 @@ def cal_pop_fitness(self):
elif (self.keep_elitism > 0) and (self.last_generation_elitism is not None) and (len(self.last_generation_elitism) > 0) and (list(sol) in last_generation_elitism_as_list):
# Return the index of the elitism from the elitism array 'self.last_generation_elitism'.
# This is not its index within the population. It is just its index in the 'self.last_generation_elitism' array.
elitism_idx = last_generation_elitism_as_list.index(
list(sol))
elitism_idx = last_generation_elitism_as_list.index(list(sol))
# Use the returned elitism index to return its index in the last population.
elitism_idx = self.last_generation_elitism_indices[elitism_idx]
# Use the elitism's index to return its pre-calculated fitness value.
Expand All @@ -1544,8 +1543,7 @@ def cal_pop_fitness(self):
# Index of the parent in the 'self.last_generation_parents' array.
# This is not its index within the population. It is just its index in the 'self.last_generation_parents' array.
# parent_idx = numpy.where(numpy.all(self.last_generation_parents == sol, axis=1))[0][0]
parent_idx = last_generation_parents_as_list.index(
list(sol))
parent_idx = last_generation_parents_as_list.index(list(sol))
# Use the returned parent index to return its index in the last population.
parent_idx = self.last_generation_parents_indices[parent_idx]
# Use the parent's index to return its pre-calculated fitness value.
Expand Down Expand Up @@ -1573,13 +1571,11 @@ def cal_pop_fitness(self):
solutions_indices = numpy.where(
numpy.array(pop_fitness) == "undefined")[0]
# Number of batches.
num_batches = int(numpy.ceil(
len(solutions_indices) / self.fitness_batch_size))
num_batches = int(numpy.ceil(len(solutions_indices) / self.fitness_batch_size))
# For each batch, get its indices and call the fitness function.
for batch_idx in range(num_batches):
batch_first_index = batch_idx * self.fitness_batch_size
batch_last_index = (batch_idx + 1) * \
self.fitness_batch_size
batch_last_index = (batch_idx + 1) * self.fitness_batch_size
batch_indices = solutions_indices[batch_first_index:batch_last_index]
batch_solutions = self.population[batch_indices, :]

Expand Down Expand Up @@ -1660,17 +1656,15 @@ def cal_pop_fitness(self):
# Reaching this block means that batch processing is used. The fitness values are calculated in batches.

# Number of batches.
num_batches = int(numpy.ceil(
len(solutions_to_submit_indices) / self.fitness_batch_size))
num_batches = int(numpy.ceil(len(solutions_to_submit_indices) / self.fitness_batch_size))
# Each element of the `batches_solutions` list represents the solutions in one batch.
batches_solutions = []
# Each element of the `batches_indices` list represents the solutions' indices in one batch.
batches_indices = []
# For each batch, get its indices and call the fitness function.
for batch_idx in range(num_batches):
batch_first_index = batch_idx * self.fitness_batch_size
batch_last_index = (batch_idx + 1) * \
self.fitness_batch_size
batch_last_index = (batch_idx + 1) * self.fitness_batch_size
batch_indices = solutions_to_submit_indices[batch_first_index:batch_last_index]
batch_solutions = self.population[batch_indices, :]

Expand All @@ -1693,7 +1687,7 @@ def cal_pop_fitness(self):
pop_fitness = numpy.array(pop_fitness)
except Exception as ex:
self.logger.exception(ex)
exit(-1)
sys.exit(-1)
return pop_fitness

def run(self):
Expand Down Expand Up @@ -1738,16 +1732,28 @@ def run(self):
# Measuring the fitness of each chromosome in the population. Save the fitness in the last_generation_fitness attribute.
self.last_generation_fitness = self.cal_pop_fitness()

best_solution, best_solution_fitness, best_match_idx = self.best_solution(
pop_fitness=self.last_generation_fitness)
best_solution, best_solution_fitness, best_match_idx = self.best_solution(pop_fitness=self.last_generation_fitness)

# Appending the best solution in the initial population to the best_solutions list.
if self.save_best_solutions:
self.best_solutions.append(best_solution)

for generation in range(generation_first_idx, generation_last_idx):
if not (self.on_fitness is None):
self.on_fitness(self, self.last_generation_fitness)
on_fitness_output = self.on_fitness(self,
self.last_generation_fitness)

if on_fitness_output is None:
pass
else:
if type(on_fitness_output) in [tuple, list, numpy.ndarray, range]:
on_fitness_output = numpy.array(on_fitness_output)
if on_fitness_output.shape == self.last_generation_fitness.shape:
self.last_generation_fitness = on_fitness_output
else:
raise ValueError(f"Size mismatch between the output of on_fitness() {on_fitness_output.shape} and the expected fitness output {self.last_generation_fitness.shape}.")
else:
raise ValueError(f"The output of on_fitness() is expected to be tuple/list/range/numpy.ndarray but {type(on_fitness_output)} found.")

# Appending the fitness value of the best solution in the current generation to the best_solutions_fitness attribute.
self.best_solutions_fitness.append(best_solution_fitness)
Expand Down Expand Up @@ -1788,7 +1794,45 @@ def run(self):
raise ValueError(f"The iterable holding the selected parents indices is expected to have ({self.num_parents_mating}) values but ({len(self.last_generation_parents_indices)}) found.")

if not (self.on_parents is None):
self.on_parents(self, self.last_generation_parents)
on_parents_output = self.on_parents(self,
self.last_generation_parents)

if on_parents_output is None:
pass
elif type(on_parents_output) in [list, tuple, numpy.ndarray]:
if len(on_parents_output) == 2:
on_parents_selected_parents, on_parents_selected_parents_indices = on_parents_output
else:
raise ValueError(f"The output of on_parents() is expected to be tuple/list/numpy.ndarray of length 2 but {type(on_parents_output)} of length {len(on_parents_output)} found.")

# Validate the parents.
if on_parents_selected_parents is None:
raise ValueError("The returned outputs of on_parents() cannot be None but the first output is None.")
else:
if type(on_parents_selected_parents) in [tuple, list, numpy.ndarray]:
on_parents_selected_parents = numpy.array(on_parents_selected_parents)
if on_parents_selected_parents.shape == self.last_generation_parents.shape:
self.last_generation_parents = on_parents_selected_parents
else:
raise ValueError(f"Size mismatch between the parents retrned by on_parents() {on_parents_selected_parents.shape} and the expected parents shape {self.last_generation_parents.shape}.")
else:
raise ValueError(f"The output of on_parents() is expected to be tuple/list/numpy.ndarray but the first output type is {type(on_parents_selected_parents)}.")

# Validate the parents indices.
if on_parents_selected_parents_indices is None:
raise ValueError("The returned outputs of on_parents() cannot be None but the second output is None.")
else:
if type(on_parents_selected_parents_indices) in [tuple, list, numpy.ndarray, range]:
on_parents_selected_parents_indices = numpy.array(on_parents_selected_parents_indices)
if on_parents_selected_parents_indices.shape == self.last_generation_parents_indices.shape:
self.last_generation_parents_indices = on_parents_selected_parents_indices
else:
raise ValueError(f"Size mismatch between the parents indices returned by on_parents() {on_parents_selected_parents_indices.shape} and the expected crossover output {self.last_generation_parents_indices.shape}.")
else:
raise ValueError(f"The output of on_parents() is expected to be tuple/list/range/numpy.ndarray but the second output type is {type(on_parents_selected_parents_indices)}.")

else:
raise TypeError(f"The output of on_parents() is expected to be tuple/list/numpy.ndarray but {type(on_parents_output)} found.")

# If self.crossover_type=None, then no crossover is applied and thus no offspring will be created in the next generations. The next generation will use the solutions in the current population.
if self.crossover_type is None:
Expand Down Expand Up @@ -1832,8 +1876,19 @@ def run(self):

# PyGAD 2.18.2 // The on_crossover() callback function is called even if crossover_type is None.
if not (self.on_crossover is None):
self.on_crossover(
self, self.last_generation_offspring_crossover)
on_crossover_output = self.on_crossover(self,
self.last_generation_offspring_crossover)
if on_crossover_output is None:
pass
else:
if type(on_crossover_output) in [tuple, list, numpy.ndarray]:
on_crossover_output = numpy.array(on_crossover_output)
if on_crossover_output.shape == self.last_generation_offspring_crossover.shape:
self.last_generation_offspring_crossover = on_crossover_output
else:
raise ValueError(f"Size mismatch between the output of on_crossover() {on_crossover_output.shape} and the expected crossover output {self.last_generation_offspring_crossover.shape}.")
else:
raise ValueError(f"The output of on_crossover() is expected to be tuple/list/numpy.ndarray but {type(on_crossover_output)} found.")

# If self.mutation_type=None, then no mutation is applied and thus no changes are applied to the offspring created using the crossover operation. The offspring will be used unchanged in the next generation.
if self.mutation_type is None:
Expand All @@ -1857,7 +1912,20 @@ def run(self):

# PyGAD 2.18.2 // The on_mutation() callback function is called even if mutation_type is None.
if not (self.on_mutation is None):
self.on_mutation(self, self.last_generation_offspring_mutation)
on_mutation_output = self.on_mutation(self,
self.last_generation_offspring_mutation)

if on_mutation_output is None:
pass
else:
if type(on_mutation_output) in [tuple, list, numpy.ndarray]:
on_mutation_output = numpy.array(on_mutation_output)
if on_mutation_output.shape == self.last_generation_offspring_mutation.shape:
self.last_generation_offspring_mutation = on_mutation_output
else:
raise ValueError(f"Size mismatch between the output of on_mutation() {on_mutation_output.shape} and the expected mutation output {self.last_generation_offspring_mutation.shape}.")
else:
raise ValueError(f"The output of on_mutation() is expected to be tuple/list/numpy.ndarray but {type(on_mutation_output)} found.")

# Update the population attribute according to the offspring generated.
if self.keep_elitism == 0:
Expand Down Expand Up @@ -1954,7 +2022,7 @@ def run(self):
# self.solutions = numpy.array(self.solutions)
except Exception as ex:
self.logger.exception(ex)
exit(-1)
sys.exit(-1)

def best_solution(self, pop_fitness=None):
"""
Expand Down Expand Up @@ -1989,7 +2057,7 @@ def best_solution(self, pop_fitness=None):
best_solution_fitness = pop_fitness[best_match_idx]
except Exception as ex:
self.logger.exception(ex)
exit(-1)
sys.exit(-1)

return best_solution, best_solution_fitness, best_match_idx

Expand Down
35 changes: 33 additions & 2 deletions pygad/utils/mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,8 +438,39 @@ def adaptive_mutation_population_fitness(self, offspring):

fitness[:self.last_generation_parents.shape[0]] = self.last_generation_fitness[self.last_generation_parents_indices]

for idx in range(len(parents_to_keep), fitness.shape[0]):
fitness[idx] = self.fitness_func(self, temp_population[idx], None)
first_idx = len(parents_to_keep)
last_idx = fitness.shape[0]
fitness[first_idx:last_idx] = [0]*(last_idx - first_idx)

if self.fitness_batch_size in [1, None]:
# Calculate the fitness for each individual solution.
for idx in range(first_idx, last_idx):
fitness[idx] = self.fitness_func(self,
temp_population[idx],
None)
else:
# Calculate the fitness for batch of solutions.

# Number of batches.
num_batches = int(numpy.ceil((last_idx - first_idx) / self.fitness_batch_size))

for batch_idx in range(num_batches):
# The index of the first solution in the current batch.
batch_first_index = first_idx + batch_idx * self.fitness_batch_size
# The index of the last solution in the current batch.
if batch_idx == (num_batches - 1):
batch_last_index = last_idx
else:
batch_last_index = first_idx + (batch_idx + 1) * self.fitness_batch_size

# Calculate the fitness values for the batch.
fitness_temp = self.fitness_func(self,
temp_population[batch_first_index:batch_last_index],
None)
# Insert the fitness of each solution at the proper index.
for idx in range(batch_first_index, batch_last_index):
fitness[idx] = fitness_temp[idx - batch_first_index]

average_fitness = numpy.mean(fitness)

return average_fitness, fitness[len(parents_to_keep):]
Expand Down