Skip to content

Commit d4b98a3

Browse files
authored
Merge pull request ahmedfgad#196 from ahmedfgad/github-actions
Fix a bug when `fitness_batch_size` is used with adaptive mutation: ahmedfgad#195
2 parents 0f9a3c1 + 464bfd6 commit d4b98a3

File tree

9 files changed

+404
-94
lines changed

9 files changed

+404
-94
lines changed

.github/workflows/main_py310.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Testing PyGAD using PyTest
1+
name: Python 3.10 Testing PyGAD using PyTest
22

33
on:
44
push:

.github/workflows/main_py311.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Testing PyGAD using PyTest
1+
name: Python 3.11 Testing PyGAD using PyTest
22

33
on:
44
push:

.github/workflows/main_py37.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Testing PyGAD using PyTest
1+
name: Python 3.7 Testing PyGAD using PyTest
22

33
on:
44
push:

.github/workflows/main_py38.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Testing PyGAD using PyTest
1+
name: Python 3.8 Testing PyGAD using PyTest
22

33
on:
44
push:

.github/workflows/main_py39.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Testing PyGAD using PyTest
1+
name: Python 3.9 Testing PyGAD using PyTest
22

33
on:
44
push:

pygad/cnn/cnn.py

Lines changed: 274 additions & 64 deletions
Large diffs are not rendered by default.

pygad/gann/gann.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from ..nn import nn
2+
import warnings
23

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

8788
# 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.

pygad/pygad.py

Lines changed: 90 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1531,8 +1531,7 @@ def cal_pop_fitness(self):
15311531
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):
15321532
# Return the index of the elitism from the elitism array 'self.last_generation_elitism'.
15331533
# This is not its index within the population. It is just its index in the 'self.last_generation_elitism' array.
1534-
elitism_idx = last_generation_elitism_as_list.index(
1535-
list(sol))
1534+
elitism_idx = last_generation_elitism_as_list.index(list(sol))
15361535
# Use the returned elitism index to return its index in the last population.
15371536
elitism_idx = self.last_generation_elitism_indices[elitism_idx]
15381537
# Use the elitism's index to return its pre-calculated fitness value.
@@ -1544,8 +1543,7 @@ def cal_pop_fitness(self):
15441543
# Index of the parent in the 'self.last_generation_parents' array.
15451544
# This is not its index within the population. It is just its index in the 'self.last_generation_parents' array.
15461545
# parent_idx = numpy.where(numpy.all(self.last_generation_parents == sol, axis=1))[0][0]
1547-
parent_idx = last_generation_parents_as_list.index(
1548-
list(sol))
1546+
parent_idx = last_generation_parents_as_list.index(list(sol))
15491547
# Use the returned parent index to return its index in the last population.
15501548
parent_idx = self.last_generation_parents_indices[parent_idx]
15511549
# Use the parent's index to return its pre-calculated fitness value.
@@ -1573,13 +1571,11 @@ def cal_pop_fitness(self):
15731571
solutions_indices = numpy.where(
15741572
numpy.array(pop_fitness) == "undefined")[0]
15751573
# Number of batches.
1576-
num_batches = int(numpy.ceil(
1577-
len(solutions_indices) / self.fitness_batch_size))
1574+
num_batches = int(numpy.ceil(len(solutions_indices) / self.fitness_batch_size))
15781575
# For each batch, get its indices and call the fitness function.
15791576
for batch_idx in range(num_batches):
15801577
batch_first_index = batch_idx * self.fitness_batch_size
1581-
batch_last_index = (batch_idx + 1) * \
1582-
self.fitness_batch_size
1578+
batch_last_index = (batch_idx + 1) * self.fitness_batch_size
15831579
batch_indices = solutions_indices[batch_first_index:batch_last_index]
15841580
batch_solutions = self.population[batch_indices, :]
15851581

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

16621658
# Number of batches.
1663-
num_batches = int(numpy.ceil(
1664-
len(solutions_to_submit_indices) / self.fitness_batch_size))
1659+
num_batches = int(numpy.ceil(len(solutions_to_submit_indices) / self.fitness_batch_size))
16651660
# Each element of the `batches_solutions` list represents the solutions in one batch.
16661661
batches_solutions = []
16671662
# Each element of the `batches_indices` list represents the solutions' indices in one batch.
16681663
batches_indices = []
16691664
# For each batch, get its indices and call the fitness function.
16701665
for batch_idx in range(num_batches):
16711666
batch_first_index = batch_idx * self.fitness_batch_size
1672-
batch_last_index = (batch_idx + 1) * \
1673-
self.fitness_batch_size
1667+
batch_last_index = (batch_idx + 1) * self.fitness_batch_size
16741668
batch_indices = solutions_to_submit_indices[batch_first_index:batch_last_index]
16751669
batch_solutions = self.population[batch_indices, :]
16761670

@@ -1693,7 +1687,7 @@ def cal_pop_fitness(self):
16931687
pop_fitness = numpy.array(pop_fitness)
16941688
except Exception as ex:
16951689
self.logger.exception(ex)
1696-
exit(-1)
1690+
sys.exit(-1)
16971691
return pop_fitness
16981692

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

1741-
best_solution, best_solution_fitness, best_match_idx = self.best_solution(
1742-
pop_fitness=self.last_generation_fitness)
1735+
best_solution, best_solution_fitness, best_match_idx = self.best_solution(pop_fitness=self.last_generation_fitness)
17431736

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

17481741
for generation in range(generation_first_idx, generation_last_idx):
17491742
if not (self.on_fitness is None):
1750-
self.on_fitness(self, self.last_generation_fitness)
1743+
on_fitness_output = self.on_fitness(self,
1744+
self.last_generation_fitness)
1745+
1746+
if on_fitness_output is None:
1747+
pass
1748+
else:
1749+
if type(on_fitness_output) in [tuple, list, numpy.ndarray, range]:
1750+
on_fitness_output = numpy.array(on_fitness_output)
1751+
if on_fitness_output.shape == self.last_generation_fitness.shape:
1752+
self.last_generation_fitness = on_fitness_output
1753+
else:
1754+
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}.")
1755+
else:
1756+
raise ValueError(f"The output of on_fitness() is expected to be tuple/list/range/numpy.ndarray but {type(on_fitness_output)} found.")
17511757

17521758
# Appending the fitness value of the best solution in the current generation to the best_solutions_fitness attribute.
17531759
self.best_solutions_fitness.append(best_solution_fitness)
@@ -1788,7 +1794,45 @@ def run(self):
17881794
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.")
17891795

17901796
if not (self.on_parents is None):
1791-
self.on_parents(self, self.last_generation_parents)
1797+
on_parents_output = self.on_parents(self,
1798+
self.last_generation_parents)
1799+
1800+
if on_parents_output is None:
1801+
pass
1802+
elif type(on_parents_output) in [list, tuple, numpy.ndarray]:
1803+
if len(on_parents_output) == 2:
1804+
on_parents_selected_parents, on_parents_selected_parents_indices = on_parents_output
1805+
else:
1806+
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.")
1807+
1808+
# Validate the parents.
1809+
if on_parents_selected_parents is None:
1810+
raise ValueError("The returned outputs of on_parents() cannot be None but the first output is None.")
1811+
else:
1812+
if type(on_parents_selected_parents) in [tuple, list, numpy.ndarray]:
1813+
on_parents_selected_parents = numpy.array(on_parents_selected_parents)
1814+
if on_parents_selected_parents.shape == self.last_generation_parents.shape:
1815+
self.last_generation_parents = on_parents_selected_parents
1816+
else:
1817+
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}.")
1818+
else:
1819+
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)}.")
1820+
1821+
# Validate the parents indices.
1822+
if on_parents_selected_parents_indices is None:
1823+
raise ValueError("The returned outputs of on_parents() cannot be None but the second output is None.")
1824+
else:
1825+
if type(on_parents_selected_parents_indices) in [tuple, list, numpy.ndarray, range]:
1826+
on_parents_selected_parents_indices = numpy.array(on_parents_selected_parents_indices)
1827+
if on_parents_selected_parents_indices.shape == self.last_generation_parents_indices.shape:
1828+
self.last_generation_parents_indices = on_parents_selected_parents_indices
1829+
else:
1830+
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}.")
1831+
else:
1832+
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)}.")
1833+
1834+
else:
1835+
raise TypeError(f"The output of on_parents() is expected to be tuple/list/numpy.ndarray but {type(on_parents_output)} found.")
17921836

17931837
# 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.
17941838
if self.crossover_type is None:
@@ -1832,8 +1876,19 @@ def run(self):
18321876

18331877
# PyGAD 2.18.2 // The on_crossover() callback function is called even if crossover_type is None.
18341878
if not (self.on_crossover is None):
1835-
self.on_crossover(
1836-
self, self.last_generation_offspring_crossover)
1879+
on_crossover_output = self.on_crossover(self,
1880+
self.last_generation_offspring_crossover)
1881+
if on_crossover_output is None:
1882+
pass
1883+
else:
1884+
if type(on_crossover_output) in [tuple, list, numpy.ndarray]:
1885+
on_crossover_output = numpy.array(on_crossover_output)
1886+
if on_crossover_output.shape == self.last_generation_offspring_crossover.shape:
1887+
self.last_generation_offspring_crossover = on_crossover_output
1888+
else:
1889+
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}.")
1890+
else:
1891+
raise ValueError(f"The output of on_crossover() is expected to be tuple/list/numpy.ndarray but {type(on_crossover_output)} found.")
18371892

18381893
# 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.
18391894
if self.mutation_type is None:
@@ -1857,7 +1912,20 @@ def run(self):
18571912

18581913
# PyGAD 2.18.2 // The on_mutation() callback function is called even if mutation_type is None.
18591914
if not (self.on_mutation is None):
1860-
self.on_mutation(self, self.last_generation_offspring_mutation)
1915+
on_mutation_output = self.on_mutation(self,
1916+
self.last_generation_offspring_mutation)
1917+
1918+
if on_mutation_output is None:
1919+
pass
1920+
else:
1921+
if type(on_mutation_output) in [tuple, list, numpy.ndarray]:
1922+
on_mutation_output = numpy.array(on_mutation_output)
1923+
if on_mutation_output.shape == self.last_generation_offspring_mutation.shape:
1924+
self.last_generation_offspring_mutation = on_mutation_output
1925+
else:
1926+
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}.")
1927+
else:
1928+
raise ValueError(f"The output of on_mutation() is expected to be tuple/list/numpy.ndarray but {type(on_mutation_output)} found.")
18611929

18621930
# Update the population attribute according to the offspring generated.
18631931
if self.keep_elitism == 0:
@@ -1954,7 +2022,7 @@ def run(self):
19542022
# self.solutions = numpy.array(self.solutions)
19552023
except Exception as ex:
19562024
self.logger.exception(ex)
1957-
exit(-1)
2025+
sys.exit(-1)
19582026

19592027
def best_solution(self, pop_fitness=None):
19602028
"""
@@ -1989,7 +2057,7 @@ def best_solution(self, pop_fitness=None):
19892057
best_solution_fitness = pop_fitness[best_match_idx]
19902058
except Exception as ex:
19912059
self.logger.exception(ex)
1992-
exit(-1)
2060+
sys.exit(-1)
19932061

19942062
return best_solution, best_solution_fitness, best_match_idx
19952063

pygad/utils/mutation.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -438,8 +438,39 @@ def adaptive_mutation_population_fitness(self, offspring):
438438

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

441-
for idx in range(len(parents_to_keep), fitness.shape[0]):
442-
fitness[idx] = self.fitness_func(self, temp_population[idx], None)
441+
first_idx = len(parents_to_keep)
442+
last_idx = fitness.shape[0]
443+
fitness[first_idx:last_idx] = [0]*(last_idx - first_idx)
444+
445+
if self.fitness_batch_size in [1, None]:
446+
# Calculate the fitness for each individual solution.
447+
for idx in range(first_idx, last_idx):
448+
fitness[idx] = self.fitness_func(self,
449+
temp_population[idx],
450+
None)
451+
else:
452+
# Calculate the fitness for batch of solutions.
453+
454+
# Number of batches.
455+
num_batches = int(numpy.ceil((last_idx - first_idx) / self.fitness_batch_size))
456+
457+
for batch_idx in range(num_batches):
458+
# The index of the first solution in the current batch.
459+
batch_first_index = first_idx + batch_idx * self.fitness_batch_size
460+
# The index of the last solution in the current batch.
461+
if batch_idx == (num_batches - 1):
462+
batch_last_index = last_idx
463+
else:
464+
batch_last_index = first_idx + (batch_idx + 1) * self.fitness_batch_size
465+
466+
# Calculate the fitness values for the batch.
467+
fitness_temp = self.fitness_func(self,
468+
temp_population[batch_first_index:batch_last_index],
469+
None)
470+
# Insert the fitness of each solution at the proper index.
471+
for idx in range(batch_first_index, batch_last_index):
472+
fitness[idx] = fitness_temp[idx - batch_first_index]
473+
443474
average_fitness = numpy.mean(fitness)
444475

445476
return average_fitness, fitness[len(parents_to_keep):]

0 commit comments

Comments
 (0)