From 82858551e20e06749c1b5941fada60efe66ea02f Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Wed, 8 Apr 2026 16:30:39 -0400 Subject: [PATCH 01/42] Mention release date --- docs/md/releases.md | 2 ++ docs/source/releases.rst | 19 ++++++++----------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/md/releases.md b/docs/md/releases.md index 7b0f12a..7089f97 100644 --- a/docs/md/releases.md +++ b/docs/md/releases.md @@ -616,6 +616,8 @@ Release Date 08 July 2025 ## PyGAD 3.6.0 +Release Date April 8, 2026 + 1. Support passing a class to the fitness, crossover, and mutation. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/342 2. A new class called `Validation` is created in the new `pygad/utils/validation.py` script. It has a method called `validate_parameters()` to validate all the parameters passed while instantiating the `pygad.GA` class. 3. Refactoring the `pygad.py` script by moving a lot of functions and methods to other classes in other scripts. diff --git a/docs/source/releases.rst b/docs/source/releases.rst index def4d0a..4138d6f 100644 --- a/docs/source/releases.rst +++ b/docs/source/releases.rst @@ -1791,6 +1791,8 @@ Release Date 08 July 2025 PyGAD 3.6.0 ----------- +Release Date April 8, 2026 + 1. Support passing a class to the fitness, crossover, and mutation. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/342 @@ -1882,17 +1884,12 @@ PyGAD 3.6.0 the ``pygad/utils/parent_selection.py`` script to initialize the parents array. -20. Add more tests about: - - 1. Operators (crossover, mutation, and parent selection). - - 2. The ``best_solution()`` method. - - 3. Parallel processing. - - 4. The ``GANN`` module. - - 5. The plots created by the ``visualize``. +20. | Add more tests about: + | 1. Operators (crossover, mutation, and parent selection). + | 2. The ``best_solution()`` method. + | 3. Parallel processing. + | 4. The ``GANN`` module. + | 5. The plots created by the ``visualize``. 21. Instead of using repeated code for converting the data type and rounding the genes during crossover and mutation, the From 30c8e3c544959aed69f732512ade22bc1b8ed673 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 9 Apr 2026 16:24:04 -0400 Subject: [PATCH 02/42] Refactor the validation logic --- pygad/utils/validation.py | 334 +++++++++++++++++++++++++++----------- 1 file changed, 239 insertions(+), 95 deletions(-) diff --git a/pygad/utils/validation.py b/pygad/utils/validation.py index 6c61bea..3fb9888 100644 --- a/pygad/utils/validation.py +++ b/pygad/utils/validation.py @@ -5,49 +5,14 @@ import logging class Validation: - def validate_parameters(self, - num_generations, - num_parents_mating, - fitness_func, - fitness_batch_size, - initial_population, - sol_per_pop, - num_genes, - init_range_low, - init_range_high, - gene_type, - parent_selection_type, - keep_parents, - keep_elitism, - K_tournament, - crossover_type, - crossover_probability, - mutation_type, - mutation_probability, - mutation_by_replacement, - mutation_percent_genes, - mutation_num_genes, - random_mutation_min_val, - random_mutation_max_val, - gene_space, - gene_constraint, - sample_size, - allow_duplicate_genes, - on_start, - on_fitness, - on_parents, - on_crossover, - on_mutation, - on_generation, - on_stop, - save_best_solutions, - save_solutions, - suppress_warnings, - stop_criteria, - parallel_processing, - random_seed, - logger): - + + def _validate_header(self, + logger, + random_seed, + suppress_warnings, + mutation_by_replacement, + sample_size, + allow_duplicate_genes): # If no logger is passed, then create a logger that logs only the messages to the console. if logger is None: # Create a logger named with the module name. @@ -122,7 +87,9 @@ def validate_parameters(self, raise TypeError(f"The expected type of the 'allow_duplicate_genes' parameter is bool but {type(allow_duplicate_genes)} found.") self.allow_duplicate_genes = allow_duplicate_genes - + + def _validate_gene_space(self, + gene_space): # Validate gene_space self.gene_space_nested = False if type(gene_space) is type(None): @@ -192,6 +159,11 @@ def validate_parameters(self, self.gene_space = gene_space + def _validate_init_range(self, + init_range_low, + init_range_high, + num_genes, + initial_population): # Validate init_range_low and init_range_high if type(init_range_low) in self.supported_int_float_types: if type(init_range_high) in self.supported_int_float_types: @@ -245,7 +217,11 @@ def validate_parameters(self, self.init_range_low = init_range_low self.init_range_high = init_range_high - + + def _validate_gene_type(self, + gene_type, + num_genes, + initial_population): # Validate gene_type if gene_type in self.supported_int_float_types: self.gene_type = [gene_type, None] @@ -308,11 +284,15 @@ def validate_parameters(self, else: self.valid_parameters = False raise ValueError(f"The value passed to the 'gene_type' parameter must be either a single integer, floating-point, list, tuple, or numpy.ndarray but ({gene_type}) of type {type(gene_type)} found.") - - # Call the unpack_gene_space() method in the pygad.helper.unique.Unique class. - self.gene_space_unpacked = self.unpack_gene_space(range_min=self.init_range_low, - range_max=self.init_range_high) - + + + def _build_initial_population(self, + initial_population, + sol_per_pop, + num_genes, + gene_space, + allow_duplicate_genes, + gene_constraint): # Build the initial population if initial_population is None: if (sol_per_pop is None) or (num_genes is None): @@ -393,13 +373,10 @@ def validate_parameters(self, # Change the data type and round all genes within the initial population. self.initial_population = self.change_population_dtype_and_round(self.initial_population) self.population = self.initial_population.copy() - - # In case the 'gene_space' parameter is nested, then make sure the number of its elements equals to the number of genes. - if self.gene_space_nested: - if len(gene_space) != self.num_genes: - self.valid_parameters = False - raise ValueError(f"When the parameter 'gene_space' is nested, then its length must be equal to the value passed to the 'num_genes' parameter. Instead, length of gene_space ({len(gene_space)}) != num_genes ({self.num_genes})") - + + def _validate_mutation_range(self, + random_mutation_min_val, + random_mutation_max_val): # Validate random_mutation_min_val and random_mutation_max_val if type(random_mutation_min_val) in self.supported_int_float_types: if type(random_mutation_max_val) in self.supported_int_float_types: @@ -446,7 +423,10 @@ def validate_parameters(self, self.random_mutation_min_val = random_mutation_min_val self.random_mutation_max_val = random_mutation_max_val - + + + def _validate_gene_constraint(self, + gene_constraint): # Validate that gene_constraint is a list or tuple and every element inside it is either None or callable. if gene_constraint: if type(gene_constraint) in [list, tuple]: @@ -477,19 +457,10 @@ def validate_parameters(self, pass self.gene_constraint = gene_constraint - - # Validating the number of parents to be selected for mating (num_parents_mating) - if num_parents_mating <= 0: - self.valid_parameters = False - raise ValueError(f"The number of parents mating (num_parents_mating) parameter must be > 0 but ({num_parents_mating}) found. \nThe following parameters must be > 0: \n1) Population size (i.e. number of solutions per population) (sol_per_pop).\n2) Number of selected parents in the mating pool (num_parents_mating).\n") - - # Validating the number of parents to be selected for mating: num_parents_mating - if num_parents_mating > self.sol_per_pop: - self.valid_parameters = False - raise ValueError(f"The number of parents to select for mating ({num_parents_mating}) cannot be greater than the number of solutions in the population ({self.sol_per_pop}) (i.e., num_parents_mating must always be <= sol_per_pop).\n") - - self.num_parents_mating = num_parents_mating - + + def _validate_crossover(self, + crossover_type, + crossover_probability): # crossover: Refers to the method that applies the crossover operator based on the selected type of crossover in the crossover_type property. # Validating the crossover type: crossover_type if crossover_type is None: @@ -554,7 +525,12 @@ def validate_parameters(self, else: self.valid_parameters = False raise TypeError(f"Unexpected type for the 'crossover_probability' parameter. Float is expected but ({crossover_probability}) of type {type(crossover_probability)} found.") - + + def _validate_mutation(self, + mutation_type, + mutation_probability, + mutation_num_genes, + mutation_percent_genes): # mutation: Refers to the method that applies the mutation operator based on the selected type of mutation in the mutation_type property. # Validating the mutation type: mutation_type # "adaptive" mutation is supported starting from PyGAD 2.10.0 @@ -777,7 +753,12 @@ def validate_parameters(self, if (self.mutation_type is None) and (self.crossover_type is None): if not self.suppress_warnings: warnings.warn("The 2 parameters mutation_type and crossover_type are None. This disables any type of evolution the genetic algorithm can make. As a result, the genetic algorithm cannot find a better solution that the best solution in the initial population.") - + + def _validate_parent_selection(self, + parent_selection_type, + K_tournament, + keep_parents, + keep_elitism): # select_parents: Refers to a method that selects the parents based on the parent selection type specified in the parent_selection_type attribute. # Validating the selected type of parent selection: parent_selection_type if inspect.ismethod(parent_selection_type): @@ -888,7 +869,10 @@ def validate_parameters(self, self.num_offspring = self.sol_per_pop - self.keep_parents else: self.num_offspring = self.sol_per_pop - self.keep_elitism - + + def _validate_fitness_func(self, + fitness_func, + fitness_batch_size): # Check if the fitness_func is a method. if inspect.ismethod(fitness_func): # Check if the fitness method accepts 3 parameters. @@ -932,7 +916,15 @@ def validate_parameters(self, raise ValueError(f"The value assigned to the fitness_batch_size parameter must be:\n1) Greater than 0.\n2) Less than or equal to sol_per_pop ({self.sol_per_pop}).\nBut the value ({fitness_batch_size}) found.") self.fitness_batch_size = fitness_batch_size - + + def _validate_callbacks(self, + on_start, + on_fitness, + on_parents, + on_crossover, + on_mutation, + on_generation, + on_stop): # Check if the on_start exists. if not (on_start is None): if inspect.ismethod(on_start): @@ -1192,24 +1184,8 @@ def validate_parameters(self, else: self.on_stop = None - # Validate save_best_solutions - if type(save_best_solutions) is bool: - if save_best_solutions == True: - if not self.suppress_warnings: - warnings.warn("Use the 'save_best_solutions' parameter with caution as it may cause memory overflow when either the number of generations or number of genes is large.") - else: - self.valid_parameters = False - raise TypeError(f"The value passed to the 'save_best_solutions' parameter must be of type bool but {type(save_best_solutions)} found.") - - # Validate save_solutions - if type(save_solutions) is bool: - if save_solutions == True: - if not self.suppress_warnings: - warnings.warn("Use the 'save_solutions' parameter with caution as it may cause memory overflow when either the number of generations, number of genes, or number of solutions in population is large.") - else: - self.valid_parameters = False - raise TypeError(f"The value passed to the 'save_solutions' parameter must be of type bool but {type(save_solutions)} found.") - + def _validate_stop_criteria(self, + stop_criteria): self.stop_criteria = [] self.supported_stop_words = ["reach", "saturate"] if stop_criteria is None: @@ -1281,7 +1257,10 @@ def validate_parameters(self, else: self.valid_parameters = False raise TypeError(f"The expected value of the 'stop_criteria' is a single string or a list/tuple/numpy.ndarray of strings but the value ({stop_criteria}) of type {type(stop_criteria)} found.") - + + def _validate_parallel_processing(self, + parallel_processing): + # Validate the parallel_processing parameter. if parallel_processing is None: self.parallel_processing = None elif type(parallel_processing) in self.supported_int_types: @@ -1317,6 +1296,24 @@ def validate_parameters(self, self.valid_parameters = False raise ValueError(f"Unexpected value ({parallel_processing}) of type ({type(parallel_processing)}) assigned to the 'parallel_processing' parameter. The accepted values for this parameter are:\n1) None: (Default) It means no parallel processing is used.\n2) A positive integer referring to the number of threads to be used (i.e. threads, not processes, are used.\n3) list/tuple: If a list or a tuple of exactly 2 elements is assigned, then:\n\t*1) The first element can be either 'process' or 'thread' to specify whether processes or threads are used, respectively.\n\t*2) The second element can be:\n\t\t**1) A positive integer to select the maximum number of processes or threads to be used.\n\t\t**2) 0 to indicate that parallel processing is not used. This is identical to setting 'parallel_processing=None'.\n\t\t**3) None to use the default value as calculated by the concurrent.futures module.") + def _validate_footer(self, + num_generations, + parent_selection_type, + mutation_percent_genes, + mutation_num_genes, + save_best_solutions, + save_solutions): + + # In case the 'gene_space' parameter is nested, then make sure the number of its elements equals to the number of genes. + if type(num_generations) in self.supported_int_types: + if num_generations > 0: + self.num_generations = num_generations + else: + raise ValueError(f"The value assigned to the 'num_generations' parameter must be a positive integer > 0. But the value {num_generations} found.") + else: + self.valid_parameters = False + raise ValueError(f"Unexpected value ({num_generations}) of type ({type(num_generations)}) assigned to the 'num_generations' parameter. It must be assigned a positive integer.") + # Set the `run_completed` property to False. It is set to `True` only after the `run()` method is complete. self.run_completed = False @@ -1328,7 +1325,6 @@ def validate_parameters(self, self.valid_parameters = True # Parameters of the genetic algorithm. - self.num_generations = abs(num_generations) self.parent_selection_type = parent_selection_type # Parameters of the mutation operation. @@ -1366,6 +1362,154 @@ def validate_parameters(self, self.last_generation_elitism_indices = None # Supported in PyGAD 3.2.0. It holds the pareto fronts when solving a multi-objective problem. self.pareto_fronts = None + + def validate_parameters(self, + num_generations, + num_parents_mating, + fitness_func, + fitness_batch_size, + initial_population, + sol_per_pop, + num_genes, + init_range_low, + init_range_high, + gene_type, + parent_selection_type, + keep_parents, + keep_elitism, + K_tournament, + crossover_type, + crossover_probability, + mutation_type, + mutation_probability, + mutation_by_replacement, + mutation_percent_genes, + mutation_num_genes, + random_mutation_min_val, + random_mutation_max_val, + gene_space, + gene_constraint, + sample_size, + allow_duplicate_genes, + on_start, + on_fitness, + on_parents, + on_crossover, + on_mutation, + on_generation, + on_stop, + save_best_solutions, + save_solutions, + suppress_warnings, + stop_criteria, + parallel_processing, + random_seed, + logger): + + self._validate_header(logger, + random_seed, + suppress_warnings, + mutation_by_replacement, + sample_size, + allow_duplicate_genes) + + self._validate_gene_space(gene_space) + + self._validate_init_range(init_range_low, + init_range_high, + num_genes, + initial_population) + + self._validate_gene_type(gene_type, + num_genes, + initial_population) + + # Call the unpack_gene_space() method in the pygad.helper.unique.Unique class. + self.gene_space_unpacked = self.unpack_gene_space(range_min=self.init_range_low, + range_max=self.init_range_high) + + self._build_initial_population(initial_population, + sol_per_pop, + num_genes, + gene_space, + allow_duplicate_genes, + gene_constraint) + + # In case the 'gene_space' parameter is nested, then make sure the number of its elements equals to the number of genes. + if self.gene_space_nested: + if len(gene_space) != self.num_genes: + self.valid_parameters = False + raise ValueError(f"When the parameter 'gene_space' is nested, then its length must be equal to the value passed to the 'num_genes' parameter. Instead, length of gene_space ({len(gene_space)}) != num_genes ({self.num_genes})") + + self._validate_mutation_range(random_mutation_min_val, + random_mutation_max_val) + + self._validate_gene_constraint(gene_constraint) + + # Validating the number of parents to be selected for mating (num_parents_mating) + if num_parents_mating <= 0: + self.valid_parameters = False + raise ValueError(f"The number of parents mating (num_parents_mating) parameter must be > 0 but ({num_parents_mating}) found. \nThe following parameters must be > 0: \n1) Population size (i.e. number of solutions per population) (sol_per_pop).\n2) Number of selected parents in the mating pool (num_parents_mating).\n") + + # Validating the number of parents to be selected for mating: num_parents_mating + if num_parents_mating > self.sol_per_pop: + self.valid_parameters = False + raise ValueError(f"The number of parents to select for mating ({num_parents_mating}) cannot be greater than the number of solutions in the population ({self.sol_per_pop}) (i.e., num_parents_mating must always be <= sol_per_pop).\n") + + self.num_parents_mating = num_parents_mating + + self._validate_crossover(crossover_type, + crossover_probability) + + self._validate_mutation(mutation_type, + mutation_probability, + mutation_num_genes, + mutation_percent_genes) + + self._validate_parent_selection(parent_selection_type, + K_tournament, + keep_parents, + keep_elitism) + + self._validate_fitness_func(fitness_func, + fitness_batch_size) + + self._validate_callbacks(on_start, + on_fitness, + on_parents, + on_crossover, + on_mutation, + on_generation, + on_stop) + + # Validate save_best_solutions + if type(save_best_solutions) is bool: + if save_best_solutions == True: + if not self.suppress_warnings: + warnings.warn("Use the 'save_best_solutions' parameter with caution as it may cause memory overflow when either the number of generations or number of genes is large.") + else: + self.valid_parameters = False + raise TypeError(f"The value passed to the 'save_best_solutions' parameter must be of type bool but {type(save_best_solutions)} found.") + + # Validate save_solutions + if type(save_solutions) is bool: + if save_solutions == True: + if not self.suppress_warnings: + warnings.warn("Use the 'save_solutions' parameter with caution as it may cause memory overflow when either the number of generations, number of genes, or number of solutions in population is large.") + else: + self.valid_parameters = False + raise TypeError(f"The value passed to the 'save_solutions' parameter must be of type bool but {type(save_solutions)} found.") + + self._validate_stop_criteria(stop_criteria) + + self._validate_parallel_processing(parallel_processing) + + self._validate_footer(num_generations, + parent_selection_type, + mutation_percent_genes, + mutation_num_genes, + save_best_solutions, + save_solutions) def validate_multi_stop_criteria(self, stop_word, number): if stop_word == 'reach': From 0a518fea1b2ae58737d70ad5e9dbbc8b241c55e3 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 9 Apr 2026 16:30:30 -0400 Subject: [PATCH 03/42] Update validate mutation params --- pygad/utils/validation.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pygad/utils/validation.py b/pygad/utils/validation.py index 3fb9888..b196caa 100644 --- a/pygad/utils/validation.py +++ b/pygad/utils/validation.py @@ -753,7 +753,8 @@ def _validate_mutation(self, if (self.mutation_type is None) and (self.crossover_type is None): if not self.suppress_warnings: warnings.warn("The 2 parameters mutation_type and crossover_type are None. This disables any type of evolution the genetic algorithm can make. As a result, the genetic algorithm cannot find a better solution that the best solution in the initial population.") - + return mutation_num_genes, mutation_percent_genes + def _validate_parent_selection(self, parent_selection_type, K_tournament, @@ -1325,7 +1326,7 @@ def _validate_footer(self, self.valid_parameters = True # Parameters of the genetic algorithm. - self.parent_selection_type = parent_selection_type + self.parent_selection_type = parent_selection_type.lower() # Parameters of the mutation operation. self.mutation_percent_genes = mutation_percent_genes @@ -1461,10 +1462,10 @@ def validate_parameters(self, self._validate_crossover(crossover_type, crossover_probability) - self._validate_mutation(mutation_type, - mutation_probability, - mutation_num_genes, - mutation_percent_genes) + mutation_num_genes, mutation_percent_genes = self._validate_mutation(mutation_type, + mutation_probability, + mutation_num_genes, + mutation_percent_genes) self._validate_parent_selection(parent_selection_type, K_tournament, From 260be03527f43296cdfa92ee4930cd492add37e5 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 9 Apr 2026 16:32:58 -0400 Subject: [PATCH 04/42] Update validated parent selection params --- pygad/utils/validation.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pygad/utils/validation.py b/pygad/utils/validation.py index b196caa..587dd8c 100644 --- a/pygad/utils/validation.py +++ b/pygad/utils/validation.py @@ -870,7 +870,9 @@ def _validate_parent_selection(self, self.num_offspring = self.sol_per_pop - self.keep_parents else: self.num_offspring = self.sol_per_pop - self.keep_elitism - + + return parent_selection_type + def _validate_fitness_func(self, fitness_func, fitness_batch_size): @@ -1326,7 +1328,7 @@ def _validate_footer(self, self.valid_parameters = True # Parameters of the genetic algorithm. - self.parent_selection_type = parent_selection_type.lower() + self.parent_selection_type = parent_selection_type # Parameters of the mutation operation. self.mutation_percent_genes = mutation_percent_genes @@ -1467,10 +1469,10 @@ def validate_parameters(self, mutation_num_genes, mutation_percent_genes) - self._validate_parent_selection(parent_selection_type, - K_tournament, - keep_parents, - keep_elitism) + parent_selection_type = self._validate_parent_selection(parent_selection_type, + K_tournament, + keep_parents, + keep_elitism) self._validate_fitness_func(fitness_func, fitness_batch_size) From 042b886fcd2ddb520a992351caca746cbd251059 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 9 Apr 2026 17:13:22 -0400 Subject: [PATCH 05/42] Move best_solutions validation to footer --- pygad/utils/validation.py | 44 +++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/pygad/utils/validation.py b/pygad/utils/validation.py index 587dd8c..f249eeb 100644 --- a/pygad/utils/validation.py +++ b/pygad/utils/validation.py @@ -1307,15 +1307,33 @@ def _validate_footer(self, save_best_solutions, save_solutions): - # In case the 'gene_space' parameter is nested, then make sure the number of its elements equals to the number of genes. + # Validate num_generations if type(num_generations) in self.supported_int_types: - if num_generations > 0: + if num_generations >= 0: self.num_generations = num_generations else: - raise ValueError(f"The value assigned to the 'num_generations' parameter must be a positive integer > 0. But the value {num_generations} found.") + raise ValueError(f"The value assigned to the 'num_generations' parameter must be a non-negative integer >= 0. But the value {num_generations} found.") + else: + self.valid_parameters = False + raise ValueError(f"Unexpected value ({num_generations}) of type ({type(num_generations)}) assigned to the 'num_generations' parameter. It must be assigned a non-negative integer.") + + # Validate save_best_solutions + if type(save_best_solutions) is bool: + if save_best_solutions == True: + if not self.suppress_warnings: + warnings.warn("Use the 'save_best_solutions' parameter with caution as it may cause memory overflow when either the number of generations or number of genes is large.") else: self.valid_parameters = False - raise ValueError(f"Unexpected value ({num_generations}) of type ({type(num_generations)}) assigned to the 'num_generations' parameter. It must be assigned a positive integer.") + raise TypeError(f"The value passed to the 'save_best_solutions' parameter must be of type bool but {type(save_best_solutions)} found.") + + # Validate save_solutions + if type(save_solutions) is bool: + if save_solutions == True: + if not self.suppress_warnings: + warnings.warn("Use the 'save_solutions' parameter with caution as it may cause memory overflow when either the number of generations, number of genes, or number of solutions in population is large.") + else: + self.valid_parameters = False + raise TypeError(f"The value passed to the 'save_solutions' parameter must be of type bool but {type(save_solutions)} found.") # Set the `run_completed` property to False. It is set to `True` only after the `run()` method is complete. self.run_completed = False @@ -1485,24 +1503,6 @@ def validate_parameters(self, on_generation, on_stop) - # Validate save_best_solutions - if type(save_best_solutions) is bool: - if save_best_solutions == True: - if not self.suppress_warnings: - warnings.warn("Use the 'save_best_solutions' parameter with caution as it may cause memory overflow when either the number of generations or number of genes is large.") - else: - self.valid_parameters = False - raise TypeError(f"The value passed to the 'save_best_solutions' parameter must be of type bool but {type(save_best_solutions)} found.") - - # Validate save_solutions - if type(save_solutions) is bool: - if save_solutions == True: - if not self.suppress_warnings: - warnings.warn("Use the 'save_solutions' parameter with caution as it may cause memory overflow when either the number of generations, number of genes, or number of solutions in population is large.") - else: - self.valid_parameters = False - raise TypeError(f"The value passed to the 'save_solutions' parameter must be of type bool but {type(save_solutions)} found.") - self._validate_stop_criteria(stop_criteria) self._validate_parallel_processing(parallel_processing) From 2f7914ce62b2601a5983553433388ad359b411aa Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 9 Apr 2026 17:14:48 -0400 Subject: [PATCH 06/42] on_start error indicate 1 parameter --- pygad/utils/validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygad/utils/validation.py b/pygad/utils/validation.py index f249eeb..1d65299 100644 --- a/pygad/utils/validation.py +++ b/pygad/utils/validation.py @@ -936,7 +936,7 @@ def _validate_callbacks(self, self.on_start = on_start else: self.valid_parameters = False - raise ValueError(f"The method assigned to the on_start parameter must accept only 2 parameters:\n1) The instance of the genetic algorithm.\nThe passed method named '{on_start.__code__.co_name}' accepts {len(inspect.signature(on_start).parameters)} parameter(s).") + raise ValueError(f"The method assigned to the on_start parameter must accept only 1 parameter representing the instance of the genetic algorithm. The passed method named '{on_start.__code__.co_name}' accepts {len(inspect.signature(on_start).parameters)} parameter(s).") # Check if the on_start is a function. elif inspect.isfunction(on_start): # Check if the on_start function accepts only a single parameter. From b58d524dba9b6d3d92727a3751507853b347618e Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Sun, 12 Apr 2026 15:46:41 -0400 Subject: [PATCH 07/42] Enhancements --- pygad/cnn/cnn.py | 53 ++------------------------------- pygad/helper/activations.py | 47 +++++++++++++++++++++++++++++ pygad/nn/nn.py | 53 ++------------------------------- pygad/utils/crossover.py | 14 ++++----- pygad/utils/engine.py | 13 ++++---- pygad/utils/parent_selection.py | 33 ++++++-------------- 6 files changed, 75 insertions(+), 138 deletions(-) create mode 100644 pygad/helper/activations.py diff --git a/pygad/cnn/cnn.py b/pygad/cnn/cnn.py index 0e425bb..c7f92eb 100644 --- a/pygad/cnn/cnn.py +++ b/pygad/cnn/cnn.py @@ -1,6 +1,7 @@ import numpy import functools import logging +from ..helper.activations import sigmoid, relu, softmax """ Convolutional neural network implementation using NumPy @@ -14,56 +15,8 @@ # Supported activation functions by the cnn.py module. supported_activation_functions = ("sigmoid", "relu", "softmax") -def sigmoid(sop): - - """ - Applies the sigmoid function. - - sop: The input to which the sigmoid function is applied. - - Returns the result of the sigmoid function. - """ - - if type(sop) in [list, tuple]: - sop = numpy.array(sop) - - return 1.0 / (1 + numpy.exp(-1 * sop)) - -def relu(sop): - - """ - Applies the rectified linear unit (ReLU) function. - - sop: The input to which the relu function is applied. - - Returns the result of the ReLU function. - """ - - if not (type(sop) in [list, tuple, numpy.ndarray]): - if sop < 0: - return 0 - else: - return sop - elif type(sop) in [list, tuple]: - sop = numpy.array(sop) - - result = sop - result[sop < 0] = 0 - - return result - -def softmax(layer_outputs): - - """ - Applies the sotmax function. - - sop: The input to which the softmax function is applied. - - Returns the result of the softmax function. - """ - return layer_outputs / (numpy.sum(layer_outputs) + 0.000001) - -def layers_weights(model, initial=True): +def layers_weights(model, + initial=True): """ Creates a list holding the weights of all layers in the CNN. diff --git a/pygad/helper/activations.py b/pygad/helper/activations.py new file mode 100644 index 0000000..f656249 --- /dev/null +++ b/pygad/helper/activations.py @@ -0,0 +1,47 @@ +import numpy + +def sigmoid(sop): + """ + Applies the sigmoid function. + + sop: The input to which the sigmoid function is applied. + + Returns the result of the sigmoid function. + """ + + if type(sop) in [list, tuple]: + sop = numpy.array(sop) + + return 1.0 / (1 + numpy.exp(-1 * sop)) + +def relu(sop): + """ + Applies the ReLU function. + + sop: The input to which the relu function is applied. + + Returns the result of the ReLU function. + """ + + if not (type(sop) in [list, tuple, numpy.ndarray]): + if sop < 0: + return 0 + else: + return sop + elif type(sop) in [list, tuple]: + sop = numpy.array(sop) + + result = sop + result[sop < 0] = 0 + + return result + +def softmax(layer_outputs): + """ + Applies the softmax function. + + sop: The input to which the softmax function is applied. + + Returns the result of the softmax function. + """ + return layer_outputs / (numpy.sum(layer_outputs) + 0.000001) diff --git a/pygad/nn/nn.py b/pygad/nn/nn.py index d14d039..dd426de 100644 --- a/pygad/nn/nn.py +++ b/pygad/nn/nn.py @@ -1,5 +1,6 @@ import numpy import functools +from ..helper.activations import sigmoid, relu, softmax """ This project creates a neural network where the architecture has input and dense layers only. More layers will be added in the future. @@ -140,57 +141,7 @@ def layers_activations(last_layer): activations.reverse() return activations -def sigmoid(sop): - - """ - Applies the sigmoid function. - - sop: The input to which the sigmoid function is applied. - - Returns the result of the sigmoid function. - """ - - if type(sop) in [list, tuple]: - sop = numpy.array(sop) - - return 1.0 / (1 + numpy.exp(-1 * sop)) - -def relu(sop): - - """ - Applies the rectified linear unit (ReLU) function. - - sop: The input to which the relu function is applied. - - Returns the result of the ReLU function. - """ - - if not (type(sop) in [list, tuple, numpy.ndarray]): - if sop < 0: - return 0 - else: - return sop - elif type(sop) in [list, tuple]: - sop = numpy.array(sop) - - result = sop - result[sop < 0] = 0 - - return result - -def softmax(layer_outputs): - - """ - Applies the sotmax function. - - sop: The input to which the softmax function is applied. - - Returns the result of the softmax function. - """ - return layer_outputs / (numpy.sum(layer_outputs) + 0.000001) - -def train(num_epochs, - last_layer, +def train(num_epochs, last_layer, data_inputs, data_outputs, problem_type="classification", diff --git a/pygad/utils/crossover.py b/pygad/utils/crossover.py index 86d92f6..f8c8ff4 100644 --- a/pygad/utils/crossover.py +++ b/pygad/utils/crossover.py @@ -198,13 +198,11 @@ def uniform_crossover(self, parents, offspring_size): # Index of the second parent to mate. parent2_idx = (k+1) % parents.shape[0] - for gene_idx in range(offspring_size[1]): - if (genes_sources[k, gene_idx] == 0): - # The gene will be copied from the first parent if the current gene index is 0. - offspring[k, gene_idx] = parents[parent1_idx, gene_idx] - elif (genes_sources[k, gene_idx] == 1): - # The gene will be copied from the second parent if the current gene index is 1. - offspring[k, gene_idx] = parents[parent2_idx, gene_idx] + # The gene will be copied from the first parent if the current gene index is 0. + # The gene will be copied from the second parent if the current gene index is 1. + offspring[k, :] = numpy.where(genes_sources[k] == 0, + parents[parent1_idx, :], + parents[parent2_idx, :]) if self.allow_duplicate_genes == False: if self.gene_space is None: @@ -268,6 +266,8 @@ def scattered_crossover(self, parents, offspring_size): # Index of the second parent to mate. parent2_idx = (k+1) % parents.shape[0] + # The gene will be copied from the first parent if the current gene index is 0. + # The gene will be copied from the second parent if the current gene index is 1. offspring[k, :] = numpy.where(genes_sources[k] == 0, parents[parent1_idx, :], parents[parent2_idx, :]) diff --git a/pygad/utils/engine.py b/pygad/utils/engine.py index 91d9ae8..eca1e8f 100644 --- a/pygad/utils/engine.py +++ b/pygad/utils/engine.py @@ -6,12 +6,12 @@ class GAEngine: def round_genes(self, solutions): - for gene_idx in range(self.num_genes): - if self.gene_type_single: - if not self.gene_type[1] is None: - solutions[:, gene_idx] = numpy.round(solutions[:, gene_idx], - self.gene_type[1]) - else: + if self.gene_type_single: + if not self.gene_type[1] is None: + solutions = numpy.round(numpy.asarray(solutions, dtype=self.gene_type[0]), + self.gene_type[1]) + else: + for gene_idx in range(self.num_genes): if not self.gene_type[gene_idx][1] is None: solutions[:, gene_idx] = numpy.round(numpy.asarray(solutions[:, gene_idx], dtype=self.gene_type[gene_idx][0]), @@ -77,6 +77,7 @@ def initialize_population(self, sample_size=1) # 2) Change the data type and round all genes within the initial population. + # This step is necessary before applying the gene constraints since the right gene value must be used for accuracy. self.population = self.change_population_dtype_and_round(self.population) # Note that gene_constraint is not validated yet. diff --git a/pygad/utils/parent_selection.py b/pygad/utils/parent_selection.py index bd04b30..52e8547 100644 --- a/pygad/utils/parent_selection.py +++ b/pygad/utils/parent_selection.py @@ -112,12 +112,14 @@ def tournament_selection(self, fitness, num_parents): parents = self.initialize_parents_array((num_parents, self.population.shape[1])) parents_indices = [] + rank_lookup = {sol_idx: rank for rank, sol_idx in enumerate(fitness_sorted)} + for parent_num in range(num_parents): # Generate random indices for the candidate solutions. rand_indices = numpy.random.randint(low=0, high=len(fitness), size=self.K_tournament) # Find the rank of the candidate solutions. The lower the rank, the better the solution. - rand_indices_rank = [fitness_sorted.index(rand_idx) for rand_idx in rand_indices] + rand_indices_rank = [rank_lookup[rand_idx] for rand_idx in rand_indices] # Select the solution with the lowest rank as a parent. selected_parent_idx = rand_indices_rank.index(min(rand_indices_rank)) @@ -196,17 +198,10 @@ def wheel_cumulative_probs(self, probs, num_parents): probs_start = numpy.zeros(probs.shape, dtype=float) # An array holding the start values of the ranges of probabilities. probs_end = numpy.zeros(probs.shape, dtype=float) # An array holding the end values of the ranges of probabilities. - curr = 0.0 - - # Calculating the probabilities of the solutions to form a roulette wheel. - for _ in range(probs.shape[0]): - min_probs_idx = numpy.where(probs == numpy.min(probs))[0][0] - probs_start[min_probs_idx] = curr - curr = curr + probs[min_probs_idx] - probs_end[min_probs_idx] = curr - # Replace 99999999999 by float('inf') - # probs[min_probs_idx] = 99999999999 - probs[min_probs_idx] = float('inf') + sorted_indices = numpy.argsort(probs) + cumulative = numpy.cumsum(probs[sorted_indices]) + probs_start[sorted_indices] = numpy.concatenate([[0.0], cumulative[:-1]]) + probs_end[sorted_indices] = cumulative # Selecting the best individuals in the current generation as parents for producing the offspring of the next generation. parents = self.initialize_parents_array((num_parents, self.population.shape[1])) @@ -248,18 +243,8 @@ def stochastic_universal_selection(self, fitness, num_parents): probs = fitness / fitness_sum - probs_start = numpy.zeros(probs.shape, dtype=float) # An array holding the start values of the ranges of probabilities. - probs_end = numpy.zeros(probs.shape, dtype=float) # An array holding the end values of the ranges of probabilities. - - curr = 0.0 - - # Calculating the probabilities of the solutions to form a roulette wheel. - for _ in range(probs.shape[0]): - min_probs_idx = numpy.where(probs == numpy.min(probs))[0][0] - probs_start[min_probs_idx] = curr - curr = curr + probs[min_probs_idx] - probs_end[min_probs_idx] = curr - probs[min_probs_idx] = float('inf') + probs_start, probs_end, parents = self.wheel_cumulative_probs(probs=probs.copy(), + num_parents=num_parents) pointers_distance = 1.0 / self.num_parents_mating # Distance between different pointers. first_pointer = numpy.random.uniform(low=0.0, From 7627dcb3f29e62b498d55e463f1da0d260523715 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Sun, 12 Apr 2026 16:12:47 -0400 Subject: [PATCH 08/42] more tests --- pygad/cnn/cnn.py | 2 +- tests/test_cnn.py | 49 +++++++++++++++++++++++++++++++++++++ tests/test_gacnn.py | 57 +++++++++++++++++++++++++++++++++++++++++++ tests/test_kerasga.py | 46 ++++++++++++++++++++++++++++++++++ tests/test_torchga.py | 45 ++++++++++++++++++++++++++++++++++ 5 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 tests/test_cnn.py create mode 100644 tests/test_gacnn.py create mode 100644 tests/test_kerasga.py create mode 100644 tests/test_torchga.py diff --git a/pygad/cnn/cnn.py b/pygad/cnn/cnn.py index c7f92eb..d0c4a27 100644 --- a/pygad/cnn/cnn.py +++ b/pygad/cnn/cnn.py @@ -78,7 +78,7 @@ def layers_weights_as_matrix(model, vector_weights): layer_weights_size = layer.initial_weights.size weights_vector=vector_weights[start:start + layer_weights_size] - # matrix = pygad.nn.DenseLayer.to_array(vector=weights_vector, shape=layer_weights_shape) + # matrix = pygad.nn.DenseLayer.to_array(vector=weights_vector, shape=layer_weights_shape) matrix = numpy.reshape(weights_vector, (layer_weights_shape)) network_weights.append(matrix) diff --git a/tests/test_cnn.py b/tests/test_cnn.py new file mode 100644 index 0000000..7566150 --- /dev/null +++ b/tests/test_cnn.py @@ -0,0 +1,49 @@ +import pygad.cnn +import numpy + +def test_cnn_layers_and_model(): + """Test pygad.cnn layers and Model class.""" + # Dummy data + data_inputs = numpy.random.uniform(0, 1, (4, 10, 10, 3)) + # The test do not care about the outputs predicted by the network. + # data_outputs = numpy.array([0, 1, 1, 0]) + + input_layer = pygad.cnn.Input2D(input_shape=(10, 10, 3)) + conv_layer = pygad.cnn.Conv2D(num_filters=2, + kernel_size=3, + previous_layer=input_layer, + activation_function="relu") + max_pooling_layer = pygad.cnn.MaxPooling2D(pool_size=2, + previous_layer=conv_layer, + stride=2) + flatten_layer = pygad.cnn.Flatten(previous_layer=max_pooling_layer) + dense_layer = pygad.cnn.Dense(num_neurons=2, + previous_layer=flatten_layer, + activation_function="softmax") + + model = pygad.cnn.Model(last_layer=dense_layer, + epochs=1, + learning_rate=0.01) + + # Test predict + predictions = model.predict(data_inputs=data_inputs) + assert len(predictions) == 4 + + # Test summary (just to ensure it doesn't crash) + model.summary() + + # Test layers_weights + weights = pygad.cnn.layers_weights(model) + assert isinstance(weights, list) + assert len(weights) > 0 + + # Test layers_weights_as_vector + weights_vector = pygad.cnn.layers_weights_as_vector(model) + assert isinstance(weights_vector, numpy.ndarray) + assert weights_vector.ndim == 1 + + print("test_cnn_layers_and_model passed.") + +if __name__ == "__main__": + test_cnn_layers_and_model() + print("\nAll CNN tests passed!") diff --git a/tests/test_gacnn.py b/tests/test_gacnn.py new file mode 100644 index 0000000..1874af4 --- /dev/null +++ b/tests/test_gacnn.py @@ -0,0 +1,57 @@ +import pygad.cnn +import pygad.gacnn +import pygad +import numpy + +def test_gacnn_evolution(): + """Test pygad.gacnn with pygad.GA.""" + # Small dummy data + data_inputs = numpy.random.uniform(0, 1, (4, 10, 10, 3)) + data_outputs = numpy.array([0, 1, 1, 0]) + + input_layer = pygad.cnn.Input2D(input_shape=(10, 10, 3)) + conv_layer = pygad.cnn.Conv2D(num_filters=2, + kernel_size=3, + previous_layer=input_layer, + activation_function="relu") + flatten_layer = pygad.cnn.Flatten(previous_layer=conv_layer) + dense_layer = pygad.cnn.Dense(num_neurons=2, + previous_layer=flatten_layer, + activation_function="softmax") + + model = pygad.cnn.Model(last_layer=dense_layer, + epochs=1, + learning_rate=0.01) + + gacnn_instance = pygad.gacnn.GACNN(model=model, + num_solutions=4) + + def fitness_func(ga_instance, solution, sol_idx): + predictions = gacnn_instance.population_networks[sol_idx].predict(data_inputs=data_inputs) + correct_predictions = numpy.where(predictions == data_outputs)[0].size + solution_fitness = (correct_predictions/data_outputs.size)*100 + return solution_fitness + + def callback_generation(ga_instance): + population_matrices = pygad.gacnn.population_as_matrices(population_networks=gacnn_instance.population_networks, + population_vectors=ga_instance.population) + gacnn_instance.update_population_trained_weights(population_trained_weights=population_matrices) + + initial_population = pygad.gacnn.population_as_vectors(population_networks=gacnn_instance.population_networks) + + ga_instance = pygad.GA(num_generations=2, + num_parents_mating=2, + initial_population=initial_population, + fitness_func=fitness_func, + on_generation=callback_generation, + suppress_warnings=True) + + ga_instance.run() + assert ga_instance.run_completed + assert ga_instance.generations_completed == 2 + + print("test_gacnn_evolution passed.") + +if __name__ == "__main__": + test_gacnn_evolution() + print("\nAll GACNN tests passed!") diff --git a/tests/test_kerasga.py b/tests/test_kerasga.py new file mode 100644 index 0000000..c3557ff --- /dev/null +++ b/tests/test_kerasga.py @@ -0,0 +1,46 @@ +import numpy +import pygad +import pygad.kerasga +import tensorflow.keras + +def test_kerasga_evolution(): + """Test pygad.kerasga with pygad.GA.""" + + # XOR data + data_inputs = numpy.array([[0, 0], [0, 1], [1, 0], [1, 1]]) + data_outputs = numpy.array([[1, 0], [0, 1], [0, 1], [1, 0]]) # One-hot encoded + + input_layer = tensorflow.keras.layers.Input(2) + dense_layer = tensorflow.keras.layers.Dense(4, activation="relu")(input_layer) + output_layer = tensorflow.keras.layers.Dense(2, activation="softmax")(dense_layer) + + model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) + + keras_ga = pygad.kerasga.KerasGA(model=model, num_solutions=10) + + def fitness_func(ga_instance, solution, solution_idx): + model_weights_matrix = pygad.kerasga.model_weights_as_matrix(model=model, + weights_vector=solution) + model.set_weights(weights=model_weights_matrix) + predictions = model.predict(data_inputs, verbose=0) + + cce = tensorflow.keras.losses.CategoricalCrossentropy() + loss = cce(data_outputs, predictions).numpy() + fitness = 1.0 / (loss + 0.00000001) + return fitness + + ga_instance = pygad.GA(num_generations=2, + num_parents_mating=5, + initial_population=keras_ga.population_weights, + fitness_func=fitness_func, + suppress_warnings=True) + + ga_instance.run() + assert ga_instance.run_completed + assert ga_instance.generations_completed == 2 + + print("test_kerasga_evolution passed.") + +if __name__ == "__main__": + test_kerasga_evolution() + print("\nAll KerasGA tests passed!") diff --git a/tests/test_torchga.py b/tests/test_torchga.py new file mode 100644 index 0000000..bbb8fd2 --- /dev/null +++ b/tests/test_torchga.py @@ -0,0 +1,45 @@ +import pygad +import torch + +def test_torchga_evolution(): + """Test pygad.torchga with pygad.GA.""" + + # XOR data + data_inputs = torch.tensor([[0.0, 0.0], [0.1, 0.6], [1.0, 0.0], [1.1, 1.3]]) + data_outputs = torch.tensor([[1.0, 0.0], [0.0, 1.0], [0.0, 1.0], [1.0, 0.0]]) # One-hot encoded + + model = torch.nn.Sequential( + torch.nn.Linear(2, 4), + torch.nn.ReLU(), + torch.nn.Linear(4, 2), + torch.nn.Softmax(dim=1) + ) + + torch_ga = pygad.torchga.TorchGA(model=model, num_solutions=10) + + def fitness_func(ga_instance, solution, solution_idx): + model_weights_dict = pygad.torchga.model_weights_as_dict(model=model, + weights_vector=solution) + model.load_state_dict(model_weights_dict) + predictions = model(data_inputs) + + loss_func = torch.nn.CrossEntropyLoss() + loss = loss_func(predictions, data_outputs).detach().numpy() + fitness = 1.0 / (loss + 0.00000001) + return fitness + + ga_instance = pygad.GA(num_generations=2, + num_parents_mating=5, + initial_population=torch_ga.population_weights, + fitness_func=fitness_func, + suppress_warnings=True) + + ga_instance.run() + assert ga_instance.run_completed + assert ga_instance.generations_completed == 2 + + print("test_torchga_evolution passed.") + +if __name__ == "__main__": + test_torchga_evolution() + print("\nAll TorchGA tests passed!") From 3c8a62ba03e7b83ce36e2d97a0ee28456a780e9f Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Sun, 12 Apr 2026 16:24:24 -0400 Subject: [PATCH 09/42] Add dependencies for testing --- .github/workflows/main.yml | 1 + pyproject.toml | 2 +- setup.py | 6 +++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 464ffbf..0484b56 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -48,6 +48,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt + pip install keras tensorflow torch # Build the PyGAD package distribution (generating .tar.gz and .whl files). # This ensures the package build process is valid on this Python version. diff --git a/pyproject.toml b/pyproject.toml index 42d372d..1a22907 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ dependencies = [ "Donation Paypal" = "http://paypal.me/ahmedfgad" [project.optional-dependencies] -deep_learning = ["keras", "torch"] +deep_learning = ["keras", "tensorflow", "torch"] # PyTest Configuration. Later, PyTest will support the [tool.pytest] table. [tool.pytest.ini_options] diff --git a/setup.py b/setup.py index 8fe7dc9..9626c37 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,11 @@ version="3.6.0", author="Ahmed Fawzy Gad", install_requires=["numpy", "matplotlib", "cloudpickle",], - author_email="ahmed.f.gad@gmail.com", + extras_require={ + "deep_learning": ["keras", "tensorflow", "torch"], + }, + author_email="ahmed.f.gad@gmail.com", + description="PyGAD: A Python Library for Building the Genetic Algorithm and Training Machine Learning Algoithms (Keras & PyTorch).", long_description=long_description, long_description_content_type="text/markdown", From 69deb4c8ce3b3a94f7902233560d9c5d58021508 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Sun, 12 Apr 2026 16:30:10 -0400 Subject: [PATCH 10/42] Separate keras and tensorflow --- .github/workflows/main.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0484b56..caacfbb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -48,7 +48,9 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt - pip install keras tensorflow torch + pip install tensorflow + pip install --upgrade keras + pip install torch # Build the PyGAD package distribution (generating .tar.gz and .whl files). # This ensures the package build process is valid on this Python version. From ad1f5a1dfe8992048372ed3a981e0f9e11ed999b Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Sun, 12 Apr 2026 16:34:55 -0400 Subject: [PATCH 11/42] Exclude tensorflow from Python 3.14 --- .github/workflows/main.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index caacfbb..44152ff 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -48,9 +48,11 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt - pip install tensorflow - pip install --upgrade keras - pip install torch + if [ "${{ matrix.python-version }}" != "3.14" ]; then + pip install tensorflow + pip install --upgrade keras + pip install torch + fi # Build the PyGAD package distribution (generating .tar.gz and .whl files). # This ensures the package build process is valid on this Python version. @@ -72,4 +74,8 @@ jobs: # This includes our new tests for visualization, operators, parallel processing, etc. - name: Run Tests run: | - pytest + if [ "${{ matrix.python-version }}" == "3.14" ]; then + pytest --ignore=tests/test_kerasga.py --ignore=tests/test_torchga.py + else + pytest + fi From d79b3963b76e4be6b0de8dfc802e7248e3270827 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Sun, 12 Apr 2026 16:43:03 -0400 Subject: [PATCH 12/42] ignore python 3.8 --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 44152ff..d0e0ef6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -48,7 +48,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt - if [ "${{ matrix.python-version }}" != "3.14" ]; then + if [ "${{ matrix.python-version }}" != "3.14" ] && [ "${{ matrix.python-version }}" != "3.8" ]; then pip install tensorflow pip install --upgrade keras pip install torch @@ -74,7 +74,7 @@ jobs: # This includes our new tests for visualization, operators, parallel processing, etc. - name: Run Tests run: | - if [ "${{ matrix.python-version }}" == "3.14" ]; then + if [ "${{ matrix.python-version }}" == "3.14" ] || [ "${{ matrix.python-version }}" == "3.8" ]; then pytest --ignore=tests/test_kerasga.py --ignore=tests/test_torchga.py else pytest From 8e3c4618e5f81b5d8d7b6b6fdec72043e6325139 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Sun, 12 Apr 2026 16:47:10 -0400 Subject: [PATCH 13/42] Return weights as list --- pygad/cnn/cnn.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygad/cnn/cnn.py b/pygad/cnn/cnn.py index d0c4a27..5b16152 100644 --- a/pygad/cnn/cnn.py +++ b/pygad/cnn/cnn.py @@ -54,7 +54,7 @@ def layers_weights(model, # Currently, the weights of the layers are in the reverse order. In other words, the weights of the first layer are at the last index of the 'network_weights' list while the weights of the last layer are at the first index. # Reversing the 'network_weights' list to order the layers' weights according to their location in the network architecture (i.e. the weights of the first layer appears at index 0 of the list). network_weights.reverse() - return numpy.array(network_weights) + return network_weights def layers_weights_as_matrix(model, vector_weights): @@ -140,7 +140,7 @@ def layers_weights_as_vector(model, initial=True): # Currently, the weights of the layers are in the reverse order. In other words, the weights of the first layer are at the last index of the 'network_weights' list while the weights of the last layer are at the first index. # Reversing the 'network_weights' list to order the layers' weights according to their location in the network architecture (i.e. the weights of the first layer appears at index 0 of the list). network_weights.reverse() - return numpy.array(network_weights) + return network_weights def update_layers_trained_weights(model, final_weights): From 5d0de5387b5d5bb76015ffa98e6030b462090aff Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Sun, 12 Apr 2026 16:56:41 -0400 Subject: [PATCH 14/42] Return as NumPy array --- pygad/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 198 bytes pygad/__pycache__/pygad.cpython-314.pyc | Bin 0 -> 17419 bytes pygad/cnn/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 200 bytes pygad/cnn/__pycache__/cnn.cpython-314.pyc | Bin 0 -> 42589 bytes pygad/cnn/cnn.py | 2 +- .../helper/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 252 bytes .../__pycache__/activations.cpython-314.pyc | Bin 0 -> 1634 bytes pygad/helper/__pycache__/misc.cpython-314.pyc | Bin 0 -> 36681 bytes pygad/helper/__pycache__/unique.cpython-314.pyc | Bin 0 -> 21154 bytes .../utils/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 390 bytes .../utils/__pycache__/crossover.cpython-314.pyc | Bin 0 -> 12312 bytes pygad/utils/__pycache__/engine.cpython-314.pyc | Bin 0 -> 50983 bytes .../utils/__pycache__/mutation.cpython-314.pyc | Bin 0 -> 28807 bytes pygad/utils/__pycache__/nsga2.cpython-314.pyc | Bin 0 -> 10920 bytes .../parent_selection.cpython-314.pyc | Bin 0 -> 21091 bytes .../__pycache__/validation.cpython-314.pyc | Bin 0 -> 94393 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 228 bytes .../visualize/__pycache__/plot.cpython-314.pyc | Bin 0 -> 23529 bytes 18 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 pygad/__pycache__/__init__.cpython-314.pyc create mode 100644 pygad/__pycache__/pygad.cpython-314.pyc create mode 100644 pygad/cnn/__pycache__/__init__.cpython-314.pyc create mode 100644 pygad/cnn/__pycache__/cnn.cpython-314.pyc create mode 100644 pygad/helper/__pycache__/__init__.cpython-314.pyc create mode 100644 pygad/helper/__pycache__/activations.cpython-314.pyc create mode 100644 pygad/helper/__pycache__/misc.cpython-314.pyc create mode 100644 pygad/helper/__pycache__/unique.cpython-314.pyc create mode 100644 pygad/utils/__pycache__/__init__.cpython-314.pyc create mode 100644 pygad/utils/__pycache__/crossover.cpython-314.pyc create mode 100644 pygad/utils/__pycache__/engine.cpython-314.pyc create mode 100644 pygad/utils/__pycache__/mutation.cpython-314.pyc create mode 100644 pygad/utils/__pycache__/nsga2.cpython-314.pyc create mode 100644 pygad/utils/__pycache__/parent_selection.cpython-314.pyc create mode 100644 pygad/utils/__pycache__/validation.cpython-314.pyc create mode 100644 pygad/visualize/__pycache__/__init__.cpython-314.pyc create mode 100644 pygad/visualize/__pycache__/plot.cpython-314.pyc diff --git a/pygad/__pycache__/__init__.cpython-314.pyc b/pygad/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ca4acef2e5fe5d11eecbbc98e4c83772647bdef0 GIT binary patch literal 198 zcmdPq=W@QFVmY0k` zDNV*(j9OK!#(HLY27a1Mw^$1*(-Tu}amUA(r4|)u=I6!7uVnZPGVGSNt5uA9YF=td zX0l^WdVW!6Nk(o!Wl2VUUJO`MOniK1US>&ryk0@&Ee@O9{FKt1RJ$Thpk9zo#k@e` V12ZEd;~fT(2YjMU+(qm_5dimBF(d#0 literal 0 HcmV?d00001 diff --git a/pygad/__pycache__/pygad.cpython-314.pyc b/pygad/__pycache__/pygad.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..338097c11b6914bb9b521e87a4b49b5e7eb8dfc4 GIT binary patch literal 17419 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 literal 0 HcmV?d00001 diff --git a/pygad/cnn/__pycache__/__init__.cpython-314.pyc b/pygad/cnn/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bd005769b34d70b0abce1365d52c938bb4f07f3a GIT binary patch literal 200 zcmdPqn#IW$FXz#~=<2FhLogMSzS3hB(F`Mo$J)CV7S+rV_>=W@QFVmY0k` zDNV*(j9OK!hI&Bgr^$4SIXN%y7I%DnS!z*nW`16L{7Qz;Afs;SyIRG#r{<-WWF|Z2 zq~{l9mSp4xRF-7q=fxCMrYEMv05!(M$7kkcmc+;F6;$5hu*uC&Da}c>E8+y|2iaB3 Y3nV@;Gcq#XVGw%2C)&ha#10ez04+5!n*aa+ literal 0 HcmV?d00001 diff --git a/pygad/cnn/__pycache__/cnn.cpython-314.pyc b/pygad/cnn/__pycache__/cnn.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e00dfd6db5fe6fd323f1584df7d0f4bf9052783e GIT binary patch literal 42589 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

G?*P%l8j8OiNE)03;8 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 literal 0 HcmV?d00001 diff --git a/pygad/cnn/cnn.py b/pygad/cnn/cnn.py index 5b16152..ac6f37b 100644 --- a/pygad/cnn/cnn.py +++ b/pygad/cnn/cnn.py @@ -140,7 +140,7 @@ def layers_weights_as_vector(model, initial=True): # Currently, the weights of the layers are in the reverse order. In other words, the weights of the first layer are at the last index of the 'network_weights' list while the weights of the last layer are at the first index. # Reversing the 'network_weights' list to order the layers' weights according to their location in the network architecture (i.e. the weights of the first layer appears at index 0 of the list). network_weights.reverse() - return network_weights + return numpy.array(network_weights) def update_layers_trained_weights(model, final_weights): diff --git a/pygad/helper/__pycache__/__init__.cpython-314.pyc b/pygad/helper/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..45e053e5cc47a189851173980d9ca91bdfc88db8 GIT binary patch literal 252 zcmdPq$`u^Q?b>lyfIvfSb+s7y~x(aT89DM&3U zVg@QNVgVAjxZ~r?Qj3Z+^Yh~4S2BDCnRCm?)hfn4H7~U!GubgGJ-;ZkBqKMVvLquv zF9vK%48)X}`1s7c%#!$cy@JYH95%W6DWy57c10j>F#>V10Fd~=%*e=imqGq6gX{x7 L(I)O94xk_aWI{id literal 0 HcmV?d00001 diff --git a/pygad/helper/__pycache__/activations.cpython-314.pyc b/pygad/helper/__pycache__/activations.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cccd07649bf75d2635088a2da4013a1496fa1644 GIT binary patch literal 1634 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 literal 0 HcmV?d00001 diff --git a/pygad/helper/__pycache__/unique.cpython-314.pyc b/pygad/helper/__pycache__/unique.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6d3678d89622a8dfd4654c2c1606aeb7a1e90718 GIT binary patch literal 21154 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 literal 0 HcmV?d00001 diff --git a/pygad/utils/__pycache__/crossover.cpython-314.pyc b/pygad/utils/__pycache__/crossover.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc798be03c50184cee6085cfd931aad7ef5e86b2 GIT binary patch literal 12312 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 R=a#cu`3}qln8OnI{{Td`i)#P? literal 0 HcmV?d00001 diff --git a/pygad/utils/__pycache__/mutation.cpython-314.pyc b/pygad/utils/__pycache__/mutation.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d3a92e544e4cade8699010c13ec9e39329e10f64 GIT binary patch literal 28807 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 literal 0 HcmV?d00001 diff --git a/pygad/utils/__pycache__/nsga2.cpython-314.pyc b/pygad/utils/__pycache__/nsga2.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1c5e5934422e605fcc40e235b0ff2ae09112a65a GIT binary patch literal 10920 zcmc&)Yit`=cD}>6C{m(ckz`BOP_iD>gSH-)Y{`$6C0Vg$E6PZwlrDME?s!h422~$ zjVR(Q;-z1LC(4i=QJQi@X}dI1PhLY2b;zOn3Rg$HQloTF_dqXBL!E}%-bOq@Azm}B z#zjg*Pfm!kl|n*wbe#uoYcP|zJ- zoTGj22=5PY?jScucR0g~g306Y2mQRqBNT{hI^{ZEhSQ5{Ju+EdKmmE>jp9l^`~y9f z7yk(hFCixiYv5n&R%mt+UFot7oq3GVH5knfQAF6VOP`;o`&^$KK01Yv4`#1IBUUUx zydb{DbkeW-ZbxpTTzBWmC6|7hXx%5*5OSHa>#|s78s*<`o-2>iW{u3Fbn5!iy9kBN zd-nQ1^vZXc$1tCAuC8>wG5zan#XEK7kHKDZub2A1hWk>Qu)(DnE6nW;`_#)jsX;a9 zf^Os&2<4#z$YqdMo0ItZFVI)J*9;^7CXJq~>(s!()YH8nQu8pwll(kGa_}3PC4-@0 zn=iEB57Ip2Be_rjj#wzjk+W=QLG0&^uq+eg<^DE5v&64pg)E%Ov96DZMgV za++bU9_7I+hv#rbBxK^u9nG&sM@PTHFUB1Wi{;1S!|gB$c}y^}z_7qWK@TN(HT5*h z(u;yF;OBTjd&3_Vv~(ZhL34xx z?SjqEd4eH6voY~$hxoKZu=3YJ8DR+eIo88m5j1lwXMv>~8n9TTi^XrBP&UW#FkKFx zg#D6w+3_JB$Nd&=OX%rARJniI`qY4`Yoeyg$NTEmM!($G`6uU=2e)eKqL!-12O8H4 zzC6(LBlC)Gv!wjP*nc^>;d{{jcXRRfGx65pIQjaD z^{E9_Hmv!Q6)o$3v@wx9bUIdXdPTokR=d`fENfanu~C|AITAC*= zYajXT&gew9q7&U%XN%VuJ{37vr}h}M_+b7jbYW!g6RmiPqD@xJy z^MtBZv-cVnFt8n3c~A2MwjwTKC#@vwL9j5uC+Tt*DWwv*G>u?QCy07 zmuU!VDT9-6m&+reN7{~sC&HdqUU{R%`u~7)IygxWchORQK zXf0;_e30Qd612LPWk4T_tznvFu%2f_LD2FuGzX?86jV(m*`D-Vvyy$1P0_A~MqW#8 z1A?Tecd=!|W*{62M*=jqXkcU{%4jbe0s=u9lcAY+7%%U?%8;{>AQVEu9qf%nyC+Ti ztJYF)5+)MO?P!j9mA2^TVEe#Jumb@<@Z#D$6C^`n-oN0#f!Vnb3Gn{540&NTG!tMJ zIGFWxgePf|gI)nf)gwJv+G@HxIU9WR@2{x?CY@}$y{9f`Z9f&Zn z)I^AkaLh}@sF!Lbz2GePeIR{lI!rws1Y-`y5J<*y0E?Fv2TbOpd79)G!wlDJZg*hg zXzp#4`Z`=26IeOV89s)W`<*-Go5h>bPpbladZb#LgP zbxb#jf2MSA>3)Z6-_^l@5#1%+cuwcWKW?2D|5W?%qY?&}t#CI;?voi+lAl=q-13ux zW$ShUs;vI$_^q-V?RfP_yyCp*Zq+xgb=}pjUx+vLChK~m zCh~Dz)9t>SebGb1@w(Tc4pWy`F6TckC|YTY7m#c1iGqWhmh#p1q@`+k1fI4WN;MD0 zng^p3bhLRe-aHetG%jCwyr2A}@8iC;_rB;(>_5GHe$!H!vXC(gxmK01IJd~w^@$|e zks^=9$YWpB#mSRVOZ8^&P_(`~Zt2-7tcn(PM@`+5$D~+(kc5Ii{|mex@R*=Uy2jEL zPOjr$z=(?V2LC|ulQMmB{DZWGwV{jJ2;ka?=1eg{SK6t3wLNrY;lxqR2)qCoYeh6+ z#cK%bT)Ms2n+ji0Q4$8@4W9UR4$LUp&{Z72VRfCrK1R!p;M!#AZ*Ndm2L&|W>(7hfi9ywqx|bK z;=S$EL+#IXrB9YTDr|HaLH4G!>l%1?Y%zx>HxwJI_I>MSk`c#}%hgwlmAZmfL8i&A0&Ve90h?2|sN z!JgXWBg(4gxYVlRTAobdwB{&;(tL2zA!4E5(L+`GiV=t(WaKohmV?p@{9e^g#d3qoCCyUCgvn2LZ&9r+@^?Kog$^ULu#b}`ZRrY%=@nYnxBxjY3^C$xj z5N2e&H$R4mk*n!>ls)bImS^HP@J{W~H^X6-#1xs!l2hq2IL}v(0Fof4HJ!OSEPbA{ zbXb9kf+~d=r2uHB00O8+g%gFU{yS@91?gnoXP2HTP|M6!D4zv+2l8*YP$h*z$^=r# zqs%?&cNm^3l|i{R|jt9-sqWj7lVI1aRb<54+7iaE9ri7d-V4E5Z}S6C(XV(S?^o zacVdPe)`ysVK}u{r4Ex#>=cB1(3XoDC4v~*JFf&8b61o6T#S-M*Ie{|)yrc(rrlf_vSy^-#4;GW0z3um z4BI1R7K^hwMDPuanpu<{?zMBk!Vn4k!%&jJ)!%{)F;be94#8>#o7)!{bSxnk$`0?> zL2`rzRfbq2-0lzpaa~G~NZ!Bccz|A*@zEy*hZL5S>iY688HZ}6JiWlq?A34SHa$A zgGebm2t{@XZokG4FpgP58}^Ok!+IXT_Y^eLW$w-h=)J z^gj)X>rFT`EZAiLjw}NK8;)VYmLL&ivJNOYw9;NOk?leeXiC`dFz(xqZ(aCy1mCb< zBqB<-14@EHI$weo@(FOGuM2t+i%L*Z3$K@>ScH&lA0F6^$7mszEtIRZ6~vzvr1;v- z8bdJRc05uG1;NNbbOe$Vf*CibiO9!gAjAXOLy#);b*bWGf>O!@P_Yq2F1A{;j*(E5 zRrLVvLTz@_PA|8hC=W^9y!_H}fJFe#sv_@V{9QWQ*^A<%ER^qZ#+P6kP6uWa0Btt{ zxYu&CB|%L_r+lfYcVbiTMBfc0rxp@aFzyV+sNloSjltxh{s(obLuaCg&Ol4d8H(42 zqox`_f0nvbep4*JX}u_!-?Dxsk$?C}Nd=Ceud*MUx;1orhu=yKUyco5PSC#iFcU3dq80`wtUVYt9e7e+6|L?{RrkiKdpFvX z)hANrCzppeiyiCs^~v?-Wbu*Zfo(H_JOKC8BezP{#y_q2$*J{$`}X@KpO0^p{MFPi ztK#J+E)PVDyS5EzAGxNzb0AT9bom_A_8(ZC zTcg*@*WO7UXpisfSRQ%2uR6DBb6V12X?7DyB0sW8l$)49A7A1Q|lH|yW zb(3sMl6@=It@@7D&W{S$h`6oxSw5=h+_vG(nLjH?RGwNsw`r+OSsXEoW4$?LIr8ix zfhtaIPZ6lL@%H|k`=f0Gar+?f{BcF~C;1=e!%0X?U5dKd_!O6*`1rv{jN;)83_K`$ z(7$muRevU04~Kw{9gM^)t}a_2SJ^)~`|;Veg$;Y6>Nw!ZEy==A&1!uO-5_rH05QG*VJiEp0QqLL%uJU>sM`l|$o17-IrF4UX;p{?{n zv*Dl1N-rETdDS5YtT3T&c`*;xhbGi%nsFi=;HjmDIeiu@G%8eTQi zI6)Dz<9|(uAn9C#*z1`jO$IE7O^|R_AhK$H1y0wX5eTx0>1u7ADhakLcd_vouQ42x z%(=#PNX&@oD=-?6zEYAa?rM3UTSVf9YSosFd%?1zL)Vu zScsbYXJ^4)XEG;{Fo0oF%0-U$LsH@8v;h0c@V(P~b{1mwBn-_-IkaMGcgxVwkX4XP zq2eP$;)RhD8cuUR4|`%czIB%*53DFBKX9rjKy#dbR&r!BEP{}8h*X0eNPqx?Brv4z zCISG_5f^Q;NQ&eIKn4S|Qlu7qWf^11jF!K4@QX=|thLA~02kNjMGjIN3k*5K0Q$fv zBLrCtfJ@RsQ!VYg0s*QXEIIK79Fou}&GW2(CIaXcMlwFg^Z+R=hg#QM-(jrNrby1cWpr zCB|^ZZ3fLvob+5+|Rb*hZpcobo8-5&z{FOC)^x) zlG`qN?8Pr1Rl$}kX=yR6u|@d_gonKh6%NOv8)nnqujKTW?!cv=Z^bN~%NPE=(7t6Wg|9^#I#cyMvHG5P{jn8Y z!dCkmvTen(b^JT2<5cW86}=otUA_{#d?kLF+q^XMREtiG6E6^P!bikx$5x1y_7(a` zXHWF#RJ`-8sO|8UjZE1ZW46X<)9YW_&OWmuyYuM*_^<^0?Cl~{P^=`DZk64x_*q5D zJ`l4HB&-8ZYU&{mHE`ef^YihBV~JDaiJA#ax}_>*IT*7XOjsI#SZN+TDye!3#`+D- zHsqHy6B?*o)VT4_+nQ<3?{Lkdd4Zo@#*J@iX#7KK=I~GTJbqN-$7QQulSZPVyha_w z8Am?;tS|^4cEM+bW{+nf#{;SZK56tQs)K{+iaZ`s?L$n2_cDQi#{+QKs%nH`AY$|#=~V^i1hM#r|G6f( z{r->95_+y9^w#HwKK78;00EF7MUVtv;!>h0fe-L4QL-$FmMB`HL_v@QIu0ZR1VIuO2&{Gi z$x@x#ZPS!)e?)bXv7I(!W|B5?nvRv3PNTM&iPLF1N;+*80U@wkPKPt~OsD@S$(c0$ z)1Gtp32-ILsncf0zC&K@<37%PopbJY&fT8sDhCDOpYH!l!opJ2zu}D%Eb_&}F1UDw z3Q`&B5*4IR&>6a(>Y#$AMk;6?F-fU1rXanO@>y;??>N60XV~it(K&xMlSuM@Hp<0Q znFt?G#$%a8I>jud=d#H-vl!)>XfnxUApLAMk<9cZQjCjwz(! zFD>6Z48uhf2)syTXdu#b$(AuUP-!5wk;+(*%#1Z?hPWbVf!G$bLTnFKKwKHLLF@?H zA$A5UA$A2F5LX4And%UgaR*({s4CGNAZ<9OG+_%NW{R>utN=o=pCAi8{Wir};r<~~ zDej**5T1;u;+aHjJh_nO5}C!N!1c^xIu#~86(&6uF7GKndtJ0gB8gNY6N!l4G9vuS zn@XsH$I4!z{@L#0kc=1*qy~Hi_pXN)=(AU-5EW4RPEA5lAv#F!lb^F@NTV-7PB+5@ zEV?Z6WdJKI3zz8LX6@0iX+$a661FSP17zwcchYYM6*31+ArnohIjlg7L9HPe(_xdo zmOxc$S$H=hKMGlb7M#X26T>DL9&2Hv zbrUiZk1eJWSF&+G15-W0Gw?Tio#E45M$MT{!hohz@FL2`=NOptILYS|nN*yINuQdN z%ai7QoX^vOgVV9x6(x*APpq5kwdDU4UihXskH$FdwQ zVTMh!*<=)3=9dafWtSo{ZBl{oU^dQSK?=!0=&aIx@i{+I$Hg;QE~OAOo?@aL7rn05 zP8J#Tnp}ch6_T4s%_U+wI+c_v!G6cvv}lu?IQ`et)6;L8IGh$F^4UZW4pT%;A4%~G z(IL@`D=ZOBCT_%)=0pfhqC;!4Xyq59Y+STL8)02qr398(n!WC;5G}y5<~bKsB|3Gz zB38@9K%HDBJ}27bR*7!;0#-L^6pyQ@tE&sfh_hia4buFJkol)^Pq8ad%8UHQ>5vru z21Kt=n>(oH?Zu{{T+`5-h`}=6gF2_1`@7T#-*&*;gZi9o74%kvRwS8Y9THwZY#g58V)RvWxF(30kcjYN63lX$YsX6` zDHwS^mk}$a8_bQ9pTju}5=m00VJ?W3SQnv1G$-a>6dg-ZsBA6**L*c3mc$8)eGHOV zsm985;$(ZdI4OOPi;u4ZWeLQ{b^|f;eg#!i|Am)+|D`v^-^di(`*ZF6x9LLrVBS5n zY}u&sd@1|-_1CVic)s{@p{8ruy3y?~c2DHGC+<%C|C*daB68=cE8u$zT&*s+`D{o-P`t->rGeD>&tn4d2i41iS@?TH!i&O={G-p ztM;px?ltz_G_QHuZk;Q14Bqn$-cH;-`a}P{y#Xj_$3UTDzQK1QZ%6Jtf4BC| z#X{dyu`iJ83*`HPHz#k=Z=B9~y4O5Ai=J+{xOHIFvsd&qJ#s!vRI9JZ(y(5BqM6i!MM!tDSj{y&a-|{fS1XP)(y))8rAE`x~!>Ys!7|1XqITfTs zv|$}35+Q{03OGInkd>4XrJzfH7|q)(Yu+QW=6wOW#8C6b$(&wFUiUM`}rcsqv+*%SYBQcj6X$*Djg)v|+Fv?nrX()luKm-iN`y?u9W9#cL{ozZ+#-3bb z&u#Oa-QVx~tG>H*;lN4ZY*=XQ$v0kHb}1^V>vr^(yWkxwdY{gDpU!&^>on9(!QQ!b z1*7T>^WXK5gEjhO8Y-+3dBs3pHNYyjs5k1xBQ#Z=cB0=agQjQ=kXg}IYN!hp z7i?K`Kb18d2a2x&t=z)Q)mhUNq(&WxwX+qhEv)LRA-$IQ3astQFm4b?2;j6R3jvf} zW#JMPR&~!1t(<<$9a@zdlTakc%fol`%Odv9`_$dCUArqN2?4My<4_k5#DAVB*9|8aTc%TJD zFXV<{fOHJ9M$!pOxujSb2aOA%$T-)D=~b@D?Sorjxbc+enu=&cBU)i+AAeCaFC|jk zZp?z~L}IOmdZ4D{aeJ^x8|tB`);WQzUXCQw>C0J`>%l_C@V#B)#E_s?&yypVTC9^M z1}KBFzW6wu(4$eUF=nx|oOK(e`V3IQGeq0jY&FmiH?}o=5Xi9B+zP7RfurwEuO2wJ z+HgL1;N077w>&qmz4lqSTz2Lf&TER`do9dc2j4tcZ0XOn^xq!;-qCk$e{ogw`2eJ zb_$2iP?ztxJ*Msi&zL$bJ5d}%-oA-VQ_~RE3 zcT$ZP=wCdjraCUrJo4Kg)KnewSN!en+G8UX#t^MQ#7un(tTHGgAp`7;zJw63TpCNCrdysMk5Usr!sbiV*mF=D%;i=Dk)KrYR7Foa6;>hI zp!#%?o~bJVT2Sp&;SsInA&ZevQkknMXja-rfR+$z1Mz9si7d57fIBio1Zq8oOS3E( zi6Nyn0BJN)XfYb0M*=OtY*^>apOlKgGN9#8%ifj%Eg`!h6yj7yia;TpB;uy7%A!td zsZP~U8Vqy8uHCIp-2r6LVm$yDmNEw!-L+>CHIe{^yaXPeJewj~jrD!iUuA_xlS!))CcshEi*Gkft_ zA4G%`>!A^8@8VS^U_?TDRLx4AP=ACUT^s2K1 zpc2@-BwkCb*ltBv8wOsTM3Gagdz^3Bv+P`RHoVcX>TFqa)qG*<)v2PZEeB$2JG<(- zvc7Gn(0Mh#?V8}awo%hqsM+;@kEm!U$?&SDA5jvUf|9&n{Yy&9t=lk!H^?^Z8>BbU z#480OUW>BLXtSk(Bs#fg{s_ym=*lvXhDevnSFO6VhKA3XdZXP-TTy%Oim(+ssJFHf$y))!#xMKh7-1 zlK`GF^VyVS?FW217EQ(wIh8n##NQFyjLs&KV0Z(xdp^w}7Xn^1#DKYSF+C@n2M2x3 zqgER+K@P$D5ziK>zZPEERoJmFUq80&Snuj7cJ2FV*SJz;l2C^)Cc)dP| zI|S4*%0kI5!mis7J)w3Vw1(`kN2*W&@KPO~t%O=t7)q-exTt|nZ6v22%|&6S;t01X zWtM<-ak5|dqmaE9u~s>tE~5n2rOWx@_aap@KMYmrB~RI2l> zjS*T9v>k-F4RAWuiwAtAKm(Ai8!(h0FPAj2Chay&sbVyq<_57}a(JSRI4MBEjTh^AS^IJ;e&coB8t=^#0O z`A+2V>GMzx{|CRW`;j(grBedp3a$GEdy{J8?ag_6Z`Z929bFq5-n3FpeGe&9)9A+N z-k(`bO=HVdn~o(G?Ox6$*7QV^Bnvg<(_BqUb_kOhgt)f?hwFFdYY=hqceV zsMjIb%9%vTz22z}!I`p9BK4g?E9-*)D)_I~QL7@HfO;bi4dnn1?JS)IkV9J?hBeK6 zB~(53ELGz6p!5UlI(;LI9GZlXS|jTYt7xaDp9K1ql-h5|t(H|2s*x#$IoC=-z3%dQ z%V@7U{nQgszoitlMwhO)R5Phe0~lVRw_IClG@W;|)uF1mH2WXUTq+s2+EA?_-ze$5 zk{T+v)bJyi^@CauDOXVP<7PSP;qpjc1J1&3W9wKCTdy03dbQp4As3jK8)OqR^fucN zst*9tULHaXN**ve!x|<==SE1~sLPM@xgk^+YS8yYDW`5Xi`$hy$1R%jtitVqE}6=r-iLL-L9J_j8ePy8$g?6SDq_`8nY5Lscc;AT=#E&1KDK0t(U{pEh6 z*#On+6C6glILXK*X0upw6pDkx2G}zI!GRxXD#QCFuYYn)GN7-iq)dWD5mYPtn*tih_lwHS8NJkjTQ^G-pxy1ZDGzE__=(~gvBz1vO6BA1> z!T2m-r!6tjlJsx{BeTdz4V4-R9iz|zAmRKLBIPp_Josyrlq6TYWzeywRlsyk8ZnhJ zQag4qx^_Vi8s;t`2NN6g20q?G5}AdI(W|mW2r1rDflM|)tBSud(5v*nt|zpOvHFlS zA7ihh*GR&bBjCrzA)w3ZGq;|v_yAxlj;zoHyxE16+_W&-`QNiG5nmrZ+8 zw9=crT)G0MVl7G7Vj8U_ZIzWk)$o`sAfx90s*m5C=gt8WkD`y~d6;23j;QcJ?3xn4 zCFOu0&iiyK3CK3fCKJHGfb*o7G@D5*C2rtq1n28aqAxxB0v;oTIf@8+GQI?i!+{)F z0sD0+Css(EDq1j? zXh8x*E2&?!$rX!De3oU&877#TnFui-kXm@r5yInR#F8u4gRF|E;*Feog4Pq;u2`j~ znJ1tOah{U!okKuA0c6AqfIi@S9d`tth}Ia4S4ON=7?BFNaEI}=c`5oL7%vvna4<}> z7W5a3!mq1%5-)D=);xEsr9k!?;CkW-|dQ<#s#G9@f#)RT<85kJNnXOx$;5%JR_kq9%@A9(M!D6Jj_bt~CCLcyh|8M00# zNLKu2jpb;#nS9KzJ3x5PIpq|u|IumPz`*$Ln-P!M9H3k^MJJPUGAqYcoqN_BT8Itm zsqebK<1P+NU;|P0M@H&Z|;7h!AbhvD}{j*Id31_?R%CMyu%yr&Y!vm)@nSP)fCfLXg_h&1wHLyigmkkb-Pxg zg}Obr##ih5)_R}1GhXOD1T{k5Gp2Kt14nZMM}=e0=LbG5K>un^m%`4m zRoDKFUA}Kdz7`QiPu?AenON90UEFmpx9ePf*M;TD75e7sRo9ODmrZykhcy`;%K62E z-PpKaJg7jLiLcL(+ncBD6+b@QG+p%wugf0;!te?53hWiau;~-zRq6>PHbP#{n^@Z= zz&+u=G6Yv+*8t_9=Kv!GG^aA$q2$#L5sU#E4JqKnA7Fu>D#3FcA)79TYX3w_?A#4Z zAKs~yXOvDsl)$eQY+>MC`e&D56iaGwJp#{hZSm@SJe~tSpor%Hex#zv3T5zA#T?tg zqZAM`)JWyP+NS~%VrJad-bN)Qw7_=y2GEOkxv~KE=Eu+npRlm%F+=WOSvQL)EETfR z6!gw9DErr;Cyq-!F>|f7)uGC=KC7Idrv{*3^_bb7vMzz!a?H$E5&g*r;$zbQLmg9L zsBPVF*)wf@~qO1zSyNmmuME zijprOOt=x15G72bVrr!4meN;o_n2kD5dprYBV#G}2y^EG0*ifiFtXK4<5&}e4GFqC4dN)ngZ|S_?!-!i>0|zXcSYyK70<3zt7?KO#rY% z*YN}(EgH%2Qupx@t+xpl^9i)};lB{`3AFbA4XvG&p|wx&w2uT&LluQvfXG+RCGffq zqZcr$#Ry#pxXTzNF-uaDlg74}4u0!|Q58s?z>pgO}y}decMIc73}}=o`=bo+;EkldIVUUR6`3Gl)z~nWBgy;1BHl)25?lc$hXli*N=0 z0Ai3fokY0e1=AITD=;O%6;mb-^VL3U5^B1IqjAW*NIz?W=J}`TyHOz!&-cwEc<{e% zLKWp-pf|Tc6ACql1Rxzy1UO?;aCPKet=OB7_*RFH=5S$7`)csBdU%2wKC9=5M5GTA z!nG?Bxsr`0Cd*jBt;}&0+Kh7<~~U zu|7h+-<60(qM3|jndPCL+DIgpjPg8u`70BPCzFwgj~1(R43FtWiv ze+;7k{5@Dl4=l8`>OqCqy63@2mv!_(fUdFbdeGSk_owI~h!^QbYyX3x1915w41{&` z=jSb-rmdr!6vo6`ofPXci?#@$_AzK9;eVt;*g{ZJ8mv~dAC$W35cgHMMe2C$icK?3 a)Avm#+Vap&(N#aUEZOMMhZM#n^Zx+pCK=BF literal 0 HcmV?d00001 diff --git a/pygad/utils/__pycache__/validation.cpython-314.pyc b/pygad/utils/__pycache__/validation.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0591d0c5b41a6689c9f45a45025aca83b19e5245 GIT binary patch literal 94393 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 literal 0 HcmV?d00001 diff --git a/pygad/visualize/__pycache__/plot.cpython-314.pyc b/pygad/visualize/__pycache__/plot.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..92b809f2149b943317afc577f6f040eebecd2c35 GIT binary patch literal 23529 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 literal 0 HcmV?d00001 From 1c1c8fb6b96852d2833575ad865b06b0edf47280 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Sun, 12 Apr 2026 16:57:33 -0400 Subject: [PATCH 15/42] Remove pyc files --- pygad/__pycache__/__init__.cpython-314.pyc | Bin 198 -> 0 bytes pygad/__pycache__/pygad.cpython-314.pyc | Bin 17419 -> 0 bytes pygad/cnn/__pycache__/__init__.cpython-314.pyc | Bin 200 -> 0 bytes pygad/cnn/__pycache__/cnn.cpython-314.pyc | Bin 42589 -> 0 bytes .../helper/__pycache__/__init__.cpython-314.pyc | Bin 252 -> 0 bytes .../__pycache__/activations.cpython-314.pyc | Bin 1634 -> 0 bytes pygad/helper/__pycache__/misc.cpython-314.pyc | Bin 36681 -> 0 bytes pygad/helper/__pycache__/unique.cpython-314.pyc | Bin 21154 -> 0 bytes .../utils/__pycache__/__init__.cpython-314.pyc | Bin 390 -> 0 bytes .../utils/__pycache__/crossover.cpython-314.pyc | Bin 12312 -> 0 bytes pygad/utils/__pycache__/engine.cpython-314.pyc | Bin 50983 -> 0 bytes .../utils/__pycache__/mutation.cpython-314.pyc | Bin 28807 -> 0 bytes pygad/utils/__pycache__/nsga2.cpython-314.pyc | Bin 10920 -> 0 bytes .../parent_selection.cpython-314.pyc | Bin 21091 -> 0 bytes .../__pycache__/validation.cpython-314.pyc | Bin 94393 -> 0 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 228 -> 0 bytes .../visualize/__pycache__/plot.cpython-314.pyc | Bin 23529 -> 0 bytes 17 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 pygad/__pycache__/__init__.cpython-314.pyc delete mode 100644 pygad/__pycache__/pygad.cpython-314.pyc delete mode 100644 pygad/cnn/__pycache__/__init__.cpython-314.pyc delete mode 100644 pygad/cnn/__pycache__/cnn.cpython-314.pyc delete mode 100644 pygad/helper/__pycache__/__init__.cpython-314.pyc delete mode 100644 pygad/helper/__pycache__/activations.cpython-314.pyc delete mode 100644 pygad/helper/__pycache__/misc.cpython-314.pyc delete mode 100644 pygad/helper/__pycache__/unique.cpython-314.pyc delete mode 100644 pygad/utils/__pycache__/__init__.cpython-314.pyc delete mode 100644 pygad/utils/__pycache__/crossover.cpython-314.pyc delete mode 100644 pygad/utils/__pycache__/engine.cpython-314.pyc delete mode 100644 pygad/utils/__pycache__/mutation.cpython-314.pyc delete mode 100644 pygad/utils/__pycache__/nsga2.cpython-314.pyc delete mode 100644 pygad/utils/__pycache__/parent_selection.cpython-314.pyc delete mode 100644 pygad/utils/__pycache__/validation.cpython-314.pyc delete mode 100644 pygad/visualize/__pycache__/__init__.cpython-314.pyc delete mode 100644 pygad/visualize/__pycache__/plot.cpython-314.pyc diff --git a/pygad/__pycache__/__init__.cpython-314.pyc b/pygad/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index ca4acef2e5fe5d11eecbbc98e4c83772647bdef0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 198 zcmdPq=W@QFVmY0k` zDNV*(j9OK!#(HLY27a1Mw^$1*(-Tu}amUA(r4|)u=I6!7uVnZPGVGSNt5uA9YF=td zX0l^WdVW!6Nk(o!Wl2VUUJO`MOniK1US>&ryk0@&Ee@O9{FKt1RJ$Thpk9zo#k@e` V12ZEd;~fT(2YjMU+(qm_5dimBF(d#0 diff --git a/pygad/__pycache__/pygad.cpython-314.pyc b/pygad/__pycache__/pygad.cpython-314.pyc deleted file mode 100644 index 338097c11b6914bb9b521e87a4b49b5e7eb8dfc4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17419 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 diff --git a/pygad/cnn/__pycache__/__init__.cpython-314.pyc b/pygad/cnn/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index bd005769b34d70b0abce1365d52c938bb4f07f3a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 200 zcmdPqn#IW$FXz#~=<2FhLogMSzS3hB(F`Mo$J)CV7S+rV_>=W@QFVmY0k` zDNV*(j9OK!hI&Bgr^$4SIXN%y7I%DnS!z*nW`16L{7Qz;Afs;SyIRG#r{<-WWF|Z2 zq~{l9mSp4xRF-7q=fxCMrYEMv05!(M$7kkcmc+;F6;$5hu*uC&Da}c>E8+y|2iaB3 Y3nV@;Gcq#XVGw%2C)&ha#10ez04+5!n*aa+ diff --git a/pygad/cnn/__pycache__/cnn.cpython-314.pyc b/pygad/cnn/__pycache__/cnn.cpython-314.pyc deleted file mode 100644 index e00dfd6db5fe6fd323f1584df7d0f4bf9052783e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42589 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

G?*P%l8j8OiNE)03;8 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 diff --git a/pygad/helper/__pycache__/__init__.cpython-314.pyc b/pygad/helper/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index 45e053e5cc47a189851173980d9ca91bdfc88db8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 252 zcmdPq$`u^Q?b>lyfIvfSb+s7y~x(aT89DM&3U zVg@QNVgVAjxZ~r?Qj3Z+^Yh~4S2BDCnRCm?)hfn4H7~U!GubgGJ-;ZkBqKMVvLquv zF9vK%48)X}`1s7c%#!$cy@JYH95%W6DWy57c10j>F#>V10Fd~=%*e=imqGq6gX{x7 L(I)O94xk_aWI{id diff --git a/pygad/helper/__pycache__/activations.cpython-314.pyc b/pygad/helper/__pycache__/activations.cpython-314.pyc deleted file mode 100644 index cccd07649bf75d2635088a2da4013a1496fa1644..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1634 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 diff --git a/pygad/helper/__pycache__/unique.cpython-314.pyc b/pygad/helper/__pycache__/unique.cpython-314.pyc deleted file mode 100644 index 6d3678d89622a8dfd4654c2c1606aeb7a1e90718..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21154 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 diff --git a/pygad/utils/__pycache__/crossover.cpython-314.pyc b/pygad/utils/__pycache__/crossover.cpython-314.pyc deleted file mode 100644 index bc798be03c50184cee6085cfd931aad7ef5e86b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12312 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 R=a#cu`3}qln8OnI{{Td`i)#P? diff --git a/pygad/utils/__pycache__/mutation.cpython-314.pyc b/pygad/utils/__pycache__/mutation.cpython-314.pyc deleted file mode 100644 index d3a92e544e4cade8699010c13ec9e39329e10f64..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28807 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 diff --git a/pygad/utils/__pycache__/nsga2.cpython-314.pyc b/pygad/utils/__pycache__/nsga2.cpython-314.pyc deleted file mode 100644 index 1c5e5934422e605fcc40e235b0ff2ae09112a65a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10920 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 a)Avm#+Vap&(N#aUEZOMMhZM#n^Zx+pCK=BF diff --git a/pygad/utils/__pycache__/validation.cpython-314.pyc b/pygad/utils/__pycache__/validation.cpython-314.pyc deleted file mode 100644 index 0591d0c5b41a6689c9f45a45025aca83b19e5245..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 94393 zcmeFa33%Jdl_!Riq(nj_2vQWq3%m)61P@VX>mtSb7D~wZE`2YfH&=_u(U#92F&SsOe-QJm< z{*p|7uj+pR1Sm+R?4FGINvQu(_3G8DSM{&=>b+No_8Io7@b^diHx@p9s#g6O-H3lW z;o`FvT)eKDPz|ZOR34tHJ!*EZ@n~48^=MhD^XRfw#i}7qj%rBTq~Q=e`XP0x%A)%V zy6vCl5BaZJxcThYaA89&*Hj+$kP1+fJ=*;$C+f^md2|F1P_+0>8q(o6c}S1nl%XX2 z?iotP@7|#l{H6}=!Ef5oUi=z{Qt`WQC=I{qLk9e24DG|OaVQCc_ z$sSdC_79nWXI6OcsBLj!#^zaYI;N8q=%(OWt5T(W76ffxSBF!jUzYzYZBVKtR7VlDj^H+6_BW|s6VlE5o#V}{T7*F&=7e~cTK{hA|;mlw&yFB zaxN#2CjUIRDcX<`tDYgX*pB5C)MVP8XEa63!CIOfBS*5Fqwm~!j+kRqt*HHg_*70s zj;s_O&7i;XReeQ6no*TQqTYCcSYk*!UL;6qDJluBr3 zIsC5iGV$49g?J^WgBJN(yc$&vmWbDKD%OeKl;8|j$?wIhV%2lY%k7d{GF~l~?^ex# z|MFF1^^X1GvmL0US5QfU7EL8Z9-Y_@WsP%YsxfX)B*iKpUB}othr|nt{CcrIP^DgM zkDQ*sH-`cHpEz z$3d|UMgAla{}-TFYOM0x=ylzZC)PKl!rYhE?@cYQTC%&{wmG}S-R^B2nYWwmcbDzc z9{Y^hv$||IJ7>)vJgi)F&du3fmF8ue%eG|q*j?rYH-Ea|n6vf@Z>Lr~=FgMOv(6RA zjMaNSJ~;Qv^0EsRPu{V)9Dv}Cf;{DPE}99Nw<|u7r4lqG20A8%>^AwBNx;BEQq|wg6 z791WsD7X#aUJIi!(c0S)zpb{#MdzK#nU&?mg=w3|J~?N1*y9q-k{?c{4g+Dsd*z~Z*nr6;>JV!ZTn(a-`du4u_K%^Z*$Bn+FkCjetOYvbA=7V9+%y=)X5)* z_q01*OEwSsJ)DNJ^4(tB%#4_6?}B^5;r7@Z)An$ZP-g}Rw=gp)v5h;tX9R6+bGe+Z zu#U(VP9jtRIMs#9oJ*5#yL~3CcdRTeuZENOv#^dm)Dc^RjoX+foD+u?!Y0b=bb*~G z(E*cGpPPO0R|FJN6}=YDj!9+99;$wE)|HFVmBB<$LTSbRwBle|>Cci>h2^1w#`S{6KtaaazTNjNlxakcTyyAvW&MC@$~X5H`ATMkb2oj9uE3nz zH|z0v?)l931N$HNG9Ca{PQh=aZsk=3Qx0sT?q5$W+(^s#B+dBc-hbFD)G-q(wE7FJ z!L+(fw6Evad*0NA4pjLMR0Y#4s5>pgmr)!@EqQAukXpS_e)L&d$yS-=jn-gV@n&Y8 zuh15pn)S`y4NR^2Y~D{*s(b1d4U&TMn$J`!Q!8B?S~UP<%=Ts11Tt#h%L-&1*-(O* z3(hYhn?v13Wfs)!RA^q^K}mnP5s8!|rivAT|GqE@VFLHy;nC z9^Xhm_~wP*coab*v#-?do4w_;yL>LMulQbI|9xM^{fK5v`(~WJ>1E%tH+b(6>TTBa zQjPc3eN^Q=HN&D`bCDr4%8(gj$c$+?b%hpI2Qm)5?FnQYj%cAhIC}$N7S+vEa*mT| zR(+lzoY!uPd>7LC$vFjCQF1a%QuI0Y%TSrs1``D1)5@p#7Zw7O|_#MTBpRq4bSzBiuKZ{Hbd3k zrBW>`bIYqsiC<*iu&S{Jl-9Ij3skBa`BKH&z&NOXdkLg^IkA^a0!2~WwHr6wvM^2l%uTN1zlDx+wxgN4hz|*K_xY!B zd&>m9%?@KMF4wCQYF8xJ?^qM-H<9a~pHt-e=jY@UaPAHT;zG$%aD76w*~aye-yWGy zvtw}`o7<)NI#S=mG@JX|`@N?sN&cF2FGJ+4G*8xT$$(3k-uf4=WTAO+kxQ*^ z)WL*!_sZ1bf*WW-N?VwL@JfQcvLO)gTnmu8?Gnb2!tAbvX-Pzd5LGw3=(Kt2`kf9t z6K1Piq`h<{;b$bQg_IiB%`eQ&d%rO@Z+Dn&4zpcq>Gm2WxscQeq-!8KcxGYRQ$u+j zAR;x~Ja4qSk84}BNOJb9lO$(I!e&;M zsU}XEABWl>4vHrrfwGj(m92MPjv9famNdmWRYt&SO+%VlF=@L&oeJ}72} zpgy-~H}fZ8Sg~6zHD+K!eQWi8g%M{Ple|cmF=`Nm-Rd{*#g|5E$3BX!d5IVtU{E4) zjhRuG(J7L`q^`0U!$x^TO+rV5PI8fQI;>~qFhcAO7p+NMCG=EBFkPgV3nvR*72ab9 zV7m)yp0J*EXgD`gjlo7BS7x|K1sc}TkO}J<*M)UN?C@S04U66d!AIqKjk4<5gcH_4 z_oIbM!L7%`iVCyY>>|~2I7?O~$N<7cF?9`chN~#*{&!d#K;Mw0Dye)!7fdPKOih2h zH;|h54bPL-uX_WjmLI2OY#NM@s{@9Dx3U9vs;0hoQolY#x$ zeHqt(qE}^*V7xDrmnnN&|Bm5124C^fXNIHCEBr}u_1ei$@zLMvdfc{|llNxJSMI*m zMv9B~nm)+>k=|F@9?WUq%qx7;_4Q*Y4_r(hJ&Q?O#!HN&6F?0G|umEA(zqdzF&Lal*0k8eJD(({2`+pJaaf zz_$+k;lZy@zN-!vH0)5TZ}_tB+GOb3jQ`q9;M#0(cy`zD*pj;zh}91h#YZ$u<#9=K zMT16N(p=Y|QIwzomo!t9jA*8b{=>Y(W;8Ckv$|Q|v+gSJXf_&-kc!E=Sy=L<@$2(z zWufX$e|2Z5uydmxZ-NJEHw!C$)h9n_`cU&Pjz6vTwO;d0O#80Q0F+%lL4>kQ648pT z({=83?Pq6ICC7f&qL#QmD`+TZ%pNk8`c0()Q+Xi0Vhv;F&_-77n^(R$`MZ;W0&6hK z`tImc?++fm|0r;FFnDBeBkRCcO~d2XV3uX`U>RUn-6Q<(Yuc$7?`wLf7b!vI_ceV~ z`#nuRB^3koERS`1_MmptnDzKdD7)OBT^`7;3>d4{nuEsL9qaauUGv#)hHT4z+j79> z3XZvU4I5ck2u0Z4;Oc!K{7~IVo%j%nEwqUe;QvrPNQqZ{fttOl9-?Fj9gL)e4d5I$ zfQ|S~Qk7TBT0lPPR|2!QefB%PI}d&Nj{wNjOeGD?nyrInznShjgc)y|=1%dCRIU)> zQ$8EP)7Mqw;$}CtZ$%LtVilIm(SK}tewzt5av4b=-l0qbl^M%q;;)6Skrz{N^q;u> zIhVugpmiGi8>D4h)E_qURU)=U5cB$u4MBzCklHyeZJQ2hVzm&9XRYG7)CxS;DaAfT z9*H`#Dr!7KL~A@#yc(1gP~sDn>adn*2ZhLwSnIX3H(08hHBlNDb7_NZUaj0r7Tc|O zrkhaJNJ1#)L7{($;&X>%)FF!&$qFb^3rVz4JdXoCnpS;{iZT+cQ6>P+mA)JnquS_N z6g?vK+ROIHjGwTIg|*_DR14!@E=nGX>NA<1dlV%l8lVkY#d8T_blXPNP#m7ccU7-w zrKDKgVT5s|9EA8NyyQ`^mu>f^#)1nh(v&<;O2SdiE5pR_Vwm)G$8FP#j_iHPR>p;z zuAq^!2h)@aDi~Ktv>h`$Bo+FQ9^2I!-hWJ@4;HN}b}lU9u2J5%gFGI0k6J>WuJucf6q}eKk z2pl1rK>&x0>~=5AImoWT$xzwGEK)manC?ap#lahbIRF#NpbH^6k4VcH(fs)yNpHb) zAl`qli+FKi)v`Vl;5L*N0PJn<`?<8?Tq~0PShEUWK zuX`9pE$K{5b{)efo$arOQ$*X4u#T>{(2PZl%7t&Q%5{`}NN{#F(N81&Fp+v+B!h<2 z7579?OA;4{JE;WrlT_h7K$4GwaHcS>6~oGP26^1HZem;b+XZDAejGtr&hQ5W>VL&| z<#pB1(y$->cwZp3=t)zkwAo+U987KAOgB9q2&9*-=|WZQ{;Kw1di#c{IB06x$SVrv zHQ=8wuVM3m`J0B{GhjJd*`b9jliR8NYk6IaJ@hNVtCPO#j=)u?@5(Z=-NFJH`MNZ0 z1GryvjaJ58nh9Fbc4{UmndB?u;>eXTiR;#Yq4=$tx7*%3^8M4_JMAlPer9OiNz~L z91C8$>KnfixOCGuMw{nL>MMlqRgU1K<`yC7(zrGMk?{0T1y?mI^ze%2HYK+;cPOb~ z(sLdYp0n|*R~_t8Lw~S8_szmUcGX%}F#Cv7&rmuX92xUnnhlK1`G)5KU_m`faK<<- zMm09Nvq2g}QZh~T<}l&iIHUPQ=%}}|-d=gn{{5xzE%~ZigXyiCru~l}222%im##Sj z)n|gHGm)Z?uW~YY9UJtnz;(B8(gVON>RxJXpN1I(T;L*TAFsQY*FD80(TW*HTc$Y3 zwrf61Qsq}aK2OSx^Iutg(i$pj@t3s(b6Oz$Z^_apsyHN2kkRaxNV7*&?Ky2L$$V|v zP7?agX}jp|k*1rHZtVq1EJI8{9j5EtVeL;+QSE0Ls{BKV)y>3aH?P#iJL-<+9Pw?l zwvAx6qf*q}tYwv+)ArEA9_UW(ao62Rg`0-m!( z*TyAngbl?5;P;3dHMDAmi0+Tn!^8&<)gzRQXs%LHFix*b*XY`CO|w;46HLk5D6)Q> zlFvm%r9eiz=}bg?9To_$tHv@0@gVw-iGwCuK##&U+Df*nl32(VSCR-xHhjvx5m7u5 zc6vuRj7Sj%i`^JvAFJ~eTeM5o4x{P`bu8fr5U3qde*|F;&k5_ua zt%!re3_b|Ejsvut3}%Ed7@rO^=PAEzHxf+|v7h2Yh(k#xz%$EJi3i5h#f})#j;fj9 zqu9Gr;vm)|a?viEkFVJ&O#-p!Z z+`101F{o^fG=_n7u96~_gZ(4z-iuw_EZZ&aud_{Dn5Iv|mf7jBng?MG?Y5g&@SkKp zY?Lx9Zukq?#La4C_F&VOOrK$yOX8NR1q0HB&w$GNk7J4MY~zf!x|zhzHwhy*yk$A< zD%fJ9vsgkg= zn5(?|E#~6W;{LD%sLDk%x%Z#^G8)Cj?PZAZ3K?D!+!RLt+Z&n98UspIN%^Ub3te0_ z!?$^Pe@LWa{b80U&UWZoS1_8IB?gf71F%yN*+Wzjh}-#}?={;zc#BPavo{Oo3{XcX zV1vHiRNfKH0K$8FXUqf?J0zz*U0o2oISZQjMpHI zDl-4^KIJN*_`qVRr=;PiU~OrV0P(q1jm&;vBB9r7ct60zqjuSeXElQ10<$HEVYndC znp><8`GwYa|2}DbVvf96=t+GgFsNi_@{))V<5V%N8ni>lF!PW%3+%3Fkc2a&OZe}Q zH49S#g|lT-38k3?nP9kz>8FH#AXaSCAnm7TSYxYPNAM$RiLhuwhK$F#Uz&LHR?uL6a(V66e=+{iI)lvLjhy+O-(<$G!q!M`xcgf;tlu|07XiG;t2OJXx9u7 zrh~u~D7Hf;2a4_3zL%X7@_j`f-^xRuhnybHY1oEsVG9$`nRkNWyoMB7mM=}dmG#!j z+xB;szO&>rpL%9E^+Nir%U>^i`>?OB`NQI8b*+IzZNc0&g*ppWSGAKyp+n1d(A&8Z z<$`vOR37tMrg>P@u8`_sUVEEz&S~#Za!1>zLzaHs1s(2&bTgFLb+bCOcStvUajJDl%gi;d zY2AdMhof>=+e|P$+7<%p)}E*2yzUYumvvK=T+&TbGOfEwNy#GR&sm~t56XW~`+lvjrt?{L z=O;M_-#i`2v8-JP=2$oKs@9Cp@(!(eLUmpKx~^be*Je@alfHM0pOrNQijF+Z3Z1y% zKXD;gbm7NJ!<9NaSK9wcy=l?JVz)c`ayu zS-U`Ym$Wx1xq&W2a+}klRo6z{a$DO@End-fP|~66q@y8I1Y!Gf-hqMEe}&x-2Si;iu_d4aIJ!4+i-FbKlkT}=zseV%hvi^x%SdN_-Y zL*fB?Kxme8lwIT~)_>=9gM`Zk-4Nk?UN=m4ExHj(Msyb`X&7ZVkI{AUm`=8sIrOPU zRoY2NbZMA!w38dojA`cR;XJ42oTyV|tXn*!`%J4YrE1g#_)StZz)FeP!c?J5%_S;% z8UE5pE^#<>Q1XKPTpZ3F^a@Dm(lGttEtsqzXFKeVkYz+WMt7IAcFHosdu$k^3lD{X zbXVI(?YP4`r(MT7r@E6`Sky&ta=Ua#B~7SsI>YQO zYaH{D%gpr5)0#!1%amq`k|po~khrV4Pj?SM2>`vT=^&KuXgVqB)DBQmI7qKdOk-=f zpyk>}N$-Mh3d!ACGEgaj29V+zw5ey|0}d^M#ts7yq@qTLOg@mA|H(GeO8=*E$s}Y{ z+IAnOEfV$zPpfq%>&v+$tyDy=AMpC&DRdIvSNQe{W*Z_P)i{~eus`Uxn5U3(yLc2@ zodininYlxfvLt-nW!z$FG%JOAXF6C*BzbmJ@&bZpfIQfu{*K>4OKNv)NwTc9lc7OA zO^FQ=Z}uo9*DBoB!7?7@{0Y9(^`yQNyasU`QkG<<$A|Ca|~JvZNS%SU#z+qJ`#4& zKg#t$OBVfy_z4o0gQzX~585v3(<3qPkhwGc=c@x4XDIn|yi~+O!7CD$qP7agByYg1 zB{_EPNIjQWModw57G+ap`iJLwwq8{OP$b{!o8|3$R3Y~fM z-%!>uVRpWQenOrYtr*=?fnr6f<GB%i^0Ip}PePMXU#SWQf;til;HY8;42D zX%=%KPb~h8*>58Ep}f>HaVU@={aAF6{r&AW4b-kIRD*dM(I7nJMZ>kA(Y8+~_wJ3Q z6Wnf0vHLbkd!eF$`wO|iG@7~pkDF9nP~Ab9C#r3?BivvyH#3u<_^paC6^gdCk~D>+ zZv^WTd#LYUe|@FN5VMePM_^B{~+^T*W70V~m&8 zBQD$W$u*m+KF<)<{k*KXDy+6xv@EcR>jEaFa5~FAjZXKtY*6QU`-udS2AHX>p@Ezt zgEL}H1>K{#T*3Aj&v7X)pmPh8*|ZQm8-$ zzc^NTbF;azm1#+%Ay8Yk&tfvSGURR8Oh~V7wQvZ6n;#>PFb_bLtN^6oP|+w)P>^EA z5R~ZKRI$vSB^*geAV3nwovI7q>W3)@|~aMAaJ^%sjv1HZ4yhzIO_*f_Paut@Q6 z*qlg)ZkA$&aUFA3(*hx`DeWc*_4GqalW?k{W5d}342(&oy&N%2EDdW|&s-71I>*ey zl8aVAT&Y_;jhH!eN)VGX)Mu0M?3{E_EGedY<=lv6xZufF2vly{qhlhG`7fdfEv~vc zDJR+YF=B+{WJl~MHvGc6X(!_B?3L%Tu--jygQ7VFk64H(OBy#EBt@`mhJF}v=%dPU zo4{2#*@Zw&q#I72hP5qSWQn8}dyYNLh<+MYPrAvFLW&e5IpyU}ZtF6^<~Uq7s!%4! z%T)~98+R3!Har|qVIZ}9BO`A;qjW8I-EvY^4i-U=P<$ihGidT|rr_FmGi5V#|Kods z%+j~qfz0anvff?zXGK3cA86>_|^W1G6TNB%OBnIU77Kf+XDw@eOa^QL3rq^`;cTE2$?MS=QCM0p%Xm$ zl}Br3?^wTM4dxt|-K&^Nshw0c9o&MfM|+V}AsyOL(u=fnebE@#5Y1~B$^D5GuRqzY zf4xfus#mFWk(aJ>@tdS7s|#kB5rij{QSHyD_BD@g969m*iSJEpyw(k6tMSlZsx-!4 z^`|KjW@*d8TPuKY2ij|Zb8%C$TU_1Vs_7!FPB)JSG@!XeS-LfsNypO#l{S(q+yJnc zd7|YoM>ONKW)p#j_E!armbYi#Yy05H4^F>-+E?2ZGL>66(B|W21d29* zAK;3%e$6~RoQK~bl7eNX9cSi&hFj2%XMXwWuV3}$9ts$0LWTyvq2b*NLBmlQI#le} zgSOd_ZE@YU=vyMks3`;j0!gP3>IfvI1b}T~#3QCNEF5FiFukAQ5(QAoFMiA9D?b(} zKK^muiO0#CMP+Xd`z)_LZGL7s6DU9XanZTQ9WZS4 zZ914U9ckj|tG%G}U62uUYAE0M*?tsTkY zklitp!y6iAbGV?nNPKXS>&h+H7;7-Mz1#`yXQ?W4v-&4mRsP9OGgXy`LKVIKie6ve zWT4_Yxs#QGcfd927Js^B?P4&!aq~>aN4ikwq`!0W<1^R&Stos2RU0{Z|LD|FnA8ZI6JnBDqG;r|v8#>Xw50bLJiRZv5>%S#;rC2t1 zv2-@&KlduN#ITZ+cy7$>koG8s3~-m*=lvZ? zjEb#^sN_rKNhx@yAWsOYgl%uRkb$?*s>Jl4o3@dioXtGDKoM<3u%tzi{pYOh-sX`6 zz2vrGz~!Avv$u%5%WcabqUdVDNcWx~0>~0CiD2>DEXk!L0=inMd~>4CY1{CS5dAVY z{h>%UFXyEe7qo=$3%RSg+R!jAW$!*}BK2PmHSauimn^;$laTbDc&|bC!J`$q;+tBL zyx(uhh;lY_wG&ye)u&QO!iVXWl((qkoYNZy*2y??`09W=GY@iKczi{{)%nn*9R>EH}GP2J#u zK<(U2XNC(p=i#`7GSi}!z#|d>rs4L30#iJoNwbz2H58wxHFXX%!!WjLVW8v7v;+** zZ+qUm_(9tb`rhyJS-YPZfOMwMR2@i%AqcJPg6XHD!=jD{uU+@q?gp-{`o?+mRwn8! zU?&}`;05fYV`)qYaF`bjE10ubiI>pJVF5CX_cRiH-phJ#=K@vqY6SZjf>lRvoBzj$5M4ik2wM z=AnXZ^yjgC{_GAdxq2N=%-xE`jJc%| zAj>e1<6^nW1RQ1k+5 zb@Es}9o%ANmRn{NIG9Y!#At(4h~m3ZdFJDkGH!dJlm#T)i{Hc3*Rc;NZtfs>1=L5_ zVT|#$AD18~Vy?2du`_?e$|PgbV1E%SVrlnDd@HPo-)ChA6ZR*U!LH?+uxnWg8xX#V zL%vb1h~Kp#}ws2q)cf&BGvKAtV+gQY)N zeL$5H6S8DeAm&%8Xr&$}cMLp`Td(y0Ve2s&k6aNh3RF~P4fa< zDXf~8>@adCK=NJ%{NM#ccd1d5eYiKpOfm5hLGbgvSlP0$dTuiexWzV9xe>9^L3nRl znZ%22DA%ko-KJF~i>m+)zfGoYmEAgLHP5a%rfEmb!clb%fbSG5wS!c>yxk76s*fk*fWvW~6$xW3zepytheTX{vlck0La=6_pI{MP)M z=i81yaBLKne)GZaJ_r>Z_ZJ<1+8iu8{gWhBad}iwHXB(MPU)C|p^aMt-_otpL$B_k z4l5q7u0xL%4<+nwfZq*}GJaJzNXel73ME(dla%GEex9;W!tOl$&O=$Q=vOFN(YGWa zX*{2V?_y;uT^F||Da}PysygNkJ;7bquW{G)Y5vi4a685&%>(+pk~t$jwjJE#yBF52 z0NTN=Tif~aWJr6NmM67W=v65TGFNa_`_lqdLk}qpYa1U60nU7t%|9yr&>3hM^)-*7 z#3eP;na@DojkZwmHnfZqyqiWS0VHm2yIOIZKID09S(trCqX?vcJf+NtyoH;`H^b;0 z&xYXo4YbF3lmMHC>Cne{l!B|g1{aUuR~)&ngc{Od+)_A)sVi~H4<|Y$71Tj9A z%)MK}133N``#cE-N1&9fn9v|7a)y*ux){uqE~N{6^0e|ta(8eB%n0?10&132v2>xN z#4m}K>c0OyE-m$6Fk0q_gp-$+64m~MaiFxIjit7YMeVtFSUE|tiI50M+m7}6#d5d0 zuOx1dB%?=S7_tr)ija*#>VYUo<>GrNP-B!w z+alDIpo+){VnL)$!ZjU_HgSZ`L2;6pXzfdon@G=$xH(FT0pIATu3Wb~yz@ zyu%y{xp>p*R&qQuc3_jWquD&Y2&AohnNgeIqbxN{#8MD6T=&sepYM~=BoV{bj)i1` z1QDf$*d!6(zG|W*NE4FK5LeH&faAuXv?nKBC#>`kCVz3^CQe3#Yj;KBta53hs@57 z?OWggIt)>q+Q)pG!+Skwb2D0{=N*h+{KEgMxhfGk4#b@Djx2W0CCUwZ24Z%P0ZF3Hof%0 zc72_GzJVV%$)MPI;?e&V{^}BpaN(%J#8A?MNbmnAe7M{A{SC55EW$UK0CldivQ^<7IZ#o-DKlfpCFuiT7xbAEF zWbxY!QM>Fq=`(CPC+Rb6y1P0E@szN;bEKZI>CTZ};<~Pxl4gC69-?`#et?oe{TL}S z2K6@mPkDkYQ~G7HNgUMQ;_vPgltKM{df2OfKu~)04=H)5?da^HMs9QJGy?CP>1l@_cfSgm`o&*FaVR!xfj@9PS4^X88Ntct*nkz}yD49sI zQ_hJbXA&Ak39>9FJtQP2k{Txk|5c#_8HP zp1f(uevCdUUpJJ$J@=0DyUwSMkhO2!+V@F1!WlmChSD3?(;GiA?f>eXP)^l)PSsjr zAm?}}=e$4Xe8ANDkveGV+A2T%wf-o#TILtKq`OBQwxnw#Qd7e2`uSZy5o1xuv{D25 z%hYdI^w%hv(Az2Jgx*QrM+tH+qi?9C`bYd-H_>ZC-%ZWGqVFMMT;~1On{_|#+F@9f(!7Cku?H1dZh{lzE6hbMx@ z8kX~zzxbH=@U@`P${wEZ7oVVqa#IlUbKbXb$2U)AkgMuhB2@7l(J5!1u8s409+?mt zsI=ns)T#|jEjb`pu-(?mfI;3JHgs>KXZW%#AE#T_E`-vp8?`MTXH?5ou~ErRv+RU{ zC17{(D4i30bF5^J?WD4eSL3`!v9|^qBDaf^i~^G4t`iD1bo8)z7Q+%19_GfGL)=2u z$vwgg?y%i9<_&MStKmCrdQ4s7pQJwf57=YbL4EdrD^xzeT=f}0k1v+`?7LASmMo6_ zN_{5wH%lc|WPDoc;?nY0>N7^enOCDei`OISzSrXR$gk9AtVdqC`po;n>N8Tfy_oup z6mBo3J|l%&dW6EQK4P%5n*{vKQE9YY689y#I zY5b&gbxOR7mehqi(DV?MhQd=HQ{jmX zC_&+=>1hybOk!}k46B$5`&rCxY_kJ- zqW`2`$Hq|tyzr55a(8G=iF@@y>cH_3Nein_zEbf6?>LxSH{^v+g@tU89R!mtHzEh zVevU=j*YFJKcdab9ltQutoNVyg{c~gz!w~8j;s|=qUIouThD(GBD+K#dxn`MA_mJY ze-SN!AN@tFj;<3pA5zYnFkv|pH9qCq~VORLT=vmTOpPTG`3ZT)gB;uTwU4zm%lJx*GZm`h=tV`X2EI_2y=Rv(KW*_WV< zAjhRrJ%iN{o1a1NMw#C8h(y=-Yf+G7svKsJl=Gp~Zop|$!coi?Ql6hJJtc7=2|DKG+2`C>i?&EUtYddmrgy&U{Q?fY*>* zmv}X65#F5>pUA0%51SjGE8(0GSJ{wTnhtB==Xe&TBp>i?Zpm4SqK7=sk_3U!l^gnv z^8T)DHWF52v!sQE`kwNcg|+-;QGVF=e^ zc35&y`ve;ex>CH6TFPI`w~9O>T|_GnP9-H2kw`p;bSJ@p7048IU>qwDG2f)Zy5Oaa zK(59$0(nj~R&%p-h$Tl<8T?DO22mKa#5e@>IhF7k6kPutgBGtdRx@lbNx=k~<~>av zG`rw(d&Hw4BtM|%wFz|cR?Ek<;;`C9oSp#78#px45Ov$(I5Q)GwOKIRT46?sa!Ym_ z!v0Yvu@bWj9?0x;6naYMYu3`yy08n99KBeeH+rM2B(l6PeZfjbDh-n=w@@pKE8u3p zaj2}@wSpn;q|;UyCIs2My-plMi!))DY&U6m10N6Wc%U&d_G2Yc-S&dgnLjAX10+UL zIl3W{)UtZ#*;+(w9-DJdSBhh-QuP)W60rkT8pR&rvEiz`CX4t47Nq=0 z6|47+Sd2(r^StV;=0p&Wdxq<_!52=3-JNY^(c?Wig6d)OISq?J+Z;2L;|^yqD`PZ3 zL`RtxJRgrkc-ot5fsvgXP+-4@UgLJW)T342#}r&Cb)|GfiYeCBtKm z6V=4st}FNidjFoHm-*yJrgO@O31TgD-ZK%N!&Xew z2ny+e-6Bq|61^UYON7s1rOECwRCr4hU{dcn$`m;rh|ZVKr8li_s=PIp#3$Y= zTQz20aOdBdsduk%+}cY{y*Cn>!HCNhb735w!6?O-b%)9Ke~#u+;S=_J(1}g738Lwq zcdjhrTTKqr+!k9r=A8TC)-fmCI?Q&8v#Z#_agE}bWy9G!Osp+?1Sd9cNt39B0-2IU zG?q%3t%SLXoRfJpRCDYJmcj*z1Lrxj>t1j7&xc`+Jn*W#b(R_h#&ZA5O&!c9qbfL|b29Dqwo(<( zolF%q$E5rC%R@u)MKP`mqsPzA+>^(&QqBy+8%rVR_JIKq6(_yAYI|46-=dv8(G+h@LsPSZZMPNPsWb!t4U;o<$#vxo(=>ogV6B8;;;8CNVevlu%fQ;r=W77iDT#&VVrUmLOU z?Y^7Nd?hkeX}RmE=8b$ad?os1Ux}rVp=RAs^GSy3s~w@N^7X9pCrg2>rcl;df7aPR z2Kh>4v~8Jdzh;QC%ATP}33jcE0vy}5_b9+ICG74zc~02**<~}@R`RN7)%DYHa|1XP z49T$W3K{7SoYONq{*nP^bk1T+ z7zg#TuY@6Fu&x`dFWx8?M!G{kLe~Br@CyI}l(0KHzq6Bxd%K=lz0bnC1FgBG_fWC| zw+`f7fpY~KMG10tB#n?qz>5AN`AV$luagG>C3tvCZ=)>B`YB4Ll9nkcxkayXTy$-8 z!2u-uG5V-%-B9-S*gKQooqT#NRNJ#&+e5w*51!lzr8lgnHz<823IaLDLOCt|oR)y; z{D-%Krp~RhhOhNS`ARSX4(P`T;{mh=SW&|6ocxY?K=h-D3`Z|D@gDpkkUT=$kn@p# zkXlL!vRp`-Aj}`>C;7V@)Y3=#8}#s={w8Jd>K7?lO!87vagSc*-KXp9`_aA<%}IY0B8F!XD- z@-3nKI)8p$Aiv>_zK!(!Cn>@7stt2l$lT;NH^JG#uwg8GQV}%rhmC%7qxi6zd?hI7 zQNQ`9`0!BBSj!$B^P9P^2fl{Bps5dm*nN3*ft>nxn}az={l<1&g{iO3T(X z|K+JQ+qX~sxU6dHRP&S8VA+Yy>Lws{{3@X~&fP|?YJ|Al>@>ln^Mv3bb^>Dgf<_K_ z!wz#4j>of8@hLGD+cG(j+!D`pyvsO_2VYs%uuynj9&NCJ#oDW6hdv_YAi_?U9{V&) zm0$m^`dMD1??~52#m|oP2J?D15GlI!1pfI-Pi&g2zHRsegRg!GRXV}XNLn>*6j$8I zU4R^-3lIyT?m|EtWa-d$QY3Mgri<=w@$ll^S~odc)p?SEaPdkq`u^Frz8|61T;!d1 z86k5(jf>nJW<+#{VL=nCSwi|z^5OYs`5m8W)z&Vx??5%+=NV;7G?4;1)h?7FCEIv>viw^`hBw%ko`ZV_kMei2{PIL#0x;IX| z<})9~h-p0b{rrELzfpYb2m9XN_pG@6BkePD4_e*Wrv_G3t`R5{9c3JL%3peFy|i`X zP+jOyGyeGwHE&kczhn5W!FSvRgggkCj>Juo+~JYP=K(tpUO6KMuk4~Z=~y>?04zQF zjUz{W=ElF4r;|#&J;Y9vT+%ScUle0Z_7gJ<@X(JV-~#lxzywd3l<6PUCE?9MX(|Q~@of2X)vcOKg;>^fq!;?-LDs{_5D+_-0s>840 zJciRPYGL9zj2Fik>)haTDMbavv*`0RS)9_lJWlByF-|EvX7flFk>XG{8zbFBir3Dm ze^B$!Yc|SXdscq#!>m6p{9)m<@&Vt4k!NKi==Rq})#wgDXi7a`a82;D;TC!Ol>Nd`t^_3kBl)d(oVpUC_ z`qL8d=DW6MMaO-wUGNPJKYMK?STqu2Wcix-7h0fbKh<=b&IiCs2nPco85B<(W#K_b zaHIyZJkYQsHSTNJ5v*g{^K?ka;nox&Q{I;H*J$XGlgdGLg{HHse5Rko91Q|q<^FtE zI49rXbECC~Xj|G#v@Ph>A|zTUugRa+w4QfL9;S!zvhD$8d8lPa*FMm)qiZkft`SXY zCumh#I!V_Ble(YiRr#mE0+~hfx41fBV#f$~`0F}?rj8Bqgkkyn>pS!i#n16LUysym zQW(}uQl-_`*-)_2b*@djk$dPJ$9EjT+*68^NPLI*ksoaNaGQ_dZvHj$;&Y1ynirv1 zu?V9Ca8~&jLI})S-Op-Zr@7`1Xwia$)B-8SS*fprj#hc3W@mLjR13k##)-(+_;Ddq z8V?cq8vh=Sb6Vf#QDDo@5%|1jsyMru7??isM3eV)@7X`N_=9WjU-Q-VJTvvMPke14 z$NFw@Fz4`}mNZ3t;?MgJoevhB-zY6#Gro0fL;kk=j@|R!f9Si%Pv2&pd9;t}Jk(Eh zmiKEn$}8FNE#=1*#qjF*2(jEn9Do86UBuD{B%%ZlN5pZ?4sTxMAH$pC$B;$uJ<6i2 z9b(aY%kODGw~7;>+lezzbV2ix%?h@7uo18+4$Q{mx!M5*q+15f@NNYh30#-?-0I}L zw#sLMhGuF;C68C!qWwv>s`SXG2k;eJvpvhJ_Z@Eg(Dv+bXE3ib=HlU|Z;^9VGYzvP z9w&K`uO`lmaq(DWWD1FlEDk-+)v`7dvUcMiqB(Dva^!P;G7kJB!*3e^saTlyu2XLeaelUG z*vaEXtu)^majG*#&Q zV#;yaB&z)hGvyRB#5@!w7PaTzVdWBuuD8pri_6{H4Z&sZeo^<}UB)?@yW@_+zn#(b zBx?2j4{$h0B)Xnl;%$^j#64Ip@fDOv#64IpK~BqwxChH6$Z0tv!lAfP8Y0pK9E#-< zP_`q?M{|%lKujCZ& zsM*ZvbARkQFrIQ`R|7;PJV(x}EK(unx*c3g;N*(3xk@^1bJdb(je{(Ap29UmilhiA z`7#FcYB<756jtPy+&<8bIJBNCje&8wNQm?Vfm!^IxbE{HPUxK-f`sdA@BhmT11hn@O=fa}g`v|C!&)Bu-8R8_Lkbg# z?Ps#QVFw48V@8FXUGTt^)h$|eh?e>*E~-zt&r9ch)9yAlC54l=IZkvev>k_nTlXG* znK+~z2zL1?1S;9h@+f%Xxd$v9DR)0vfw|d>5d#K1u3Fd%viORzd{VYmE@t{7((P4w zX~ZW&gjWk4E0QqzZMzt^^cT;euZq$;`@_8c%BUOPFGe{fSiP5Ne8={S;Y_k^{=s9M zPeKMHFuW0t*q9d2W`S93WNcKe+wBf(U3c)~UU74GnY|w8ii;S{;R$ER*OLzSoUJje z=ZOw>A2+?lveV2qo+V3}lV;nuz@$5dPK`H_$_+^{eCl=#?9=m(g;(=Tk9Z|h+FYf$R+g3@^UgSFt{Rd0N(RQfEBc2!nrpkd&>+@f z$SOP`TQ2&L$bey~Skxniqm}EBbHEO{*J2h-M+K|R9qj%@6D5!xworndk{q}h(5}Hc zvIlP$7s=ppdU0WS8TR_z{4vgvGcwQ^0P&LS*4f$gXidY;tx=jlMp(&r(P$Gbymp2m z+M~y@(EL2r!U#$h_Coa9fiB0i6W?%Ht%+5TQ5QGqjE@TdV@Sb#l$TEC@|i$FAh0+50Cu4p>Do88!ZSMr>Pz zzJ-+>pwBuN;V(n37vvoTdse5zjnUU&G56ByhItQRK9agy(TB9KiJgh;uLQD8E}=5V z9IbIWl=iqvn6(X7Zr8|1do3UB!T_kk+y}0fmN^t=+~z@X5D)8=8S{+Yy^JG(2vrYk z7=?iYb3UABdegp)aKhXz$YHc-{8+u5!X5~ypQ*b*x9>et+=1_!6f4Y`O_IbAkO!bY`#~#DU@gW zNol56yw4EqA0;*lXUQ|DBHzJSPlZovI-8*tnfK9mL%!$si8C1P(PoWlJ(DuraOU>W z5l&N#R=02`^nBPr`6ZWii=LT1hO_zbkgI2cjq87;>Y+GRx&AeN+@u6!#*I7>^g-FQ zF}irTJf?|bbhf`FQ^)>f+Ee-2K2@42l$y7mn&-=J2&Fczr#330Ci}`y228JoOznPC zdmz2zqXwL`xmEGn#~G!vfffrpJ4buF^STFQj5x1rC!07**d2=|Jg;L$baT4IqY1Z> z#oGk8o}18LAPYH4@bHR$i0t4l>xU^BPGXjUB~$b&XPO=vr{!SKKJ$@~;pn>I=*zTg zf}l+%dB{p{GU)-~Knc5Rqe#w^No}+RIFZy&Nqh1jZ3SLP9--u7@^~_GUQC`%hUFh6 z$TF9FpW-`TOn$)Mb*2Ey#pKQuJiL(HMcej+$=#H6r(B|>y z4N>14`~KuVo%FR`2_3n*e&i~w9DO-8YcO*>v7UZHVdm(woC)Nd4drzEbGiejo{wPX zIJi}P_T#Kd**vKX;H$c*x!p<3D6u@ z+(k`lNgk%=Qi3ca$qUrnmgF1!-3k${C3%G&K1{kzSsozcq<}@3Hn9lPh6w~jJ`pNA>n}SSC~Nkm6>p@Mt?7g5wHp=Hp^9dIMe}3bvy9RW zW5rrI#f7G)=lvDuw?FOjS9D2FtAfTxmiIM(#cR^jnxOG0dwSMiaaMflt46@-Q|xh@ zzoJciyjB@BHn68B{S^p9Eg%7d%X;?oguenIr}0##6ig=1`&uvi&R;<*ud3ULl*Jv4 zi#iz>;g_OKKj{so*3+4Iy@B-NCnKTq)Bf_)bb7C;C{)tuFKG;#8rin_VSnl2$GtI5 zAof*te3bSfZK02<1slLQG8CX=+|bX$X>MftfGi%(FwGK`Bee6Idr|kFHnNMKTnJ`Y zY}7P7md=X1MeT)1o z;dZhW=+H28tX^2?p~E|1uz-1Jl;3U`j5GEsWWZ#{(H9 zPi8_DXZ#gsf*EH-R@x+1TJkx(zNLGBb03z4&#me{nd;peR#1|#78OKn5h`x* z7dO1y8ft3uH?=)0ZhNCOm{z!zUWR(P0Rt&6#XnN+;y&fG-^MrmZ-D`Wy6>~NTc^J= zU=VwirQ(Lj4%_8@acTLt$$;Ts(ME&92nAbR(SI=w82$+>x3dAmud_U~zGZ)kS|qa% z@!boX=dev79OWiK@B7c<_Q&5s^M$CkNSmWL_ODc=nQV@N&9um4|6rp}wC}sE$SrNY zV&kVwgdwN2aUF9rRjxaYZRi?e!DJXnaYC_uE^H{z83~Uu^I(NslX#UtEW>@kHAYF) zmdQW_0~GxNo=ZgW|NyVIExVXl^w(S{v)F^)+;AwYZ}ssVSh% zfr58>Wsx+QO!K1LKzjbhlX&J`v@X0-j7?T4o>m97Otb8O7*Q{se-^PVAZ-Q42& z0ud$}mB^T7cXpd9iEhoXbI)UIZ;<$$;RH31%90&lJD4W6od&^6=mv8DpC2nv-w79~*20-0OqrhI>1XSdDS)5I z)6e$HN-wc2;S(;7sqj)}S;PX|{pa|BvMWVWcD>mfO0}$~S`<2=C*uKAW5{&cZ#o@F zKl5R6F#Y^SS^dWu#j;?{R9$ySR=KNdCFRy#T_35sC}DRO`Q1fw{<)(YrDRlpot%Jd z`WZ^>`bElV*WcDdM?wkZ)Uz|&?fO>IwAgX97+_KYxzEP^MXy8gg`5|lnn6;+JYsX0 z+RJE@Rb5Zgwjbt}V5^vDKBBu$3cL~B0(lct!tQSIyIa)oVfY;*ap~aL1=S5)jFB|! zJE>L8`XOo+C6p5<*P{W=Xg_;*lW5bdzex`t=oTr#JMnEB5Xxb>J;MJKk(b&(1en)L)~V*OHiTZ^a_@ZQc?+%3g~0=Do_) z6ii2D9G4F2Vrs4Xz~mu!Q#h{~pnMOxQ_utMdUAm~C>C61>IbI$FkI1Wr53EGmT#nG zFol^VkXcP8|LuNjdoZ(I7SEyUh~;B*jdUIpT!k{tU4XYlrN6XEEyXkdO8I8mF3sj>7uda$dV1n}nb9Fk$ zT_i`e<4kpMjr z{5RtE$KN?=dia4leSk2EGnwF$3|Bp6a3 ze(43riv%HoKIG%2Ue@33xH`%h)xOAhd4ahnA)H@$?vbGW(r6ifb!7a4(NdnFwo@&! z8ERX(Z8Ma17iK7_mISz)&rk~P-eHEyM1Q=R87d)+qj@`O_K_tNQvx$omSTpIYDWzb zdlhVk$`WVaQC-3_l;leE(xXa(kU$^u8S0!PTf}t787g~J_ljnygm8}TMa7y+qO{l! zw(}-I{iV?|{tUJM3r0(MhT2ZG#Ac{%<+jaGx?Px|q*@Z-ZazaPxO;~gDhK`XYG$Z} zFplQ!xHHs&1ZJpQ#SA6Y9&?70#{H;1;TcNW4}IxTB|%7_5BUr==%8q%>~F^z>fosU z70ply;T+wI&z+$psJ}E?#-E|`zF@SJXQ=H|OKgVPR&Lu21ryz$TgsDaNr1cg45i@i z9cHL}^vA22p%TJ4nz!T5Pz4FhP=$&aN~%5P3?+^G(WHcDs3Z}CmmXCTgdO`(skgoE zkoa@Q8LDVB=@rdT3E>>wi_e{*B&feMTE?HD%wI5CN*pk{oob2AP}|CFo1v0+VTO`w zNr1cg45i@i9cHLv^vA22p%TJ4nz!T5P$lC0u!A0|R53$Iwa1*HxfroZ-~j1DED_0EOc|7fo$=M6c!`5RaDD}R#>ttNOvj=v^0pm+ z!M#_PQ8CW7|fGq*U;i!dYFgi=>!0=e+I ztgSo;hS@9bf2=5* zZ1s*3Ys4KaVvosuk>GH7-bevJOzDc~65Rh^h)ktK@m2In{E_nh^G3=S2JsyRh=Ak& zZwJV(v~PC?$aZ!}bb$N|;;bT#L!wW1FRDXSU_##Z^&2k*ek~eJCi8vZtcfW`XAoB?%nn8o|FN zUQ2doB)xc(wvDR)OBz)o_Ft?)m0%N<-L~`U?e;D=IMMON4tb3`zC_|H20^03>-Tpa zUL>aQfKv+CO19h;Ig@aRDaU)i^}JE@3rBF<82FbGOUStF$|%?s(k9N@^0#BR+7X|* zBjk7%eljj^%g87=Fj4Lf?#$f?yS1!8;o^*j*BX$<9@_UD`qn8;Vx1Yg~n zZRfTM&VyZa&~CGKh>rK7gxy`?cUQ=L@SOH4C0B875?mc;aReih8`@ix^9Ii8g{LPa z$Z{VC^up`#hPI7dV{d2&$u*V|Je<{DAlF#Cc8HQ8_|hV&u+gi$DZ0*{(tTn)5Hgmo z8%v*bhK#lA#@ZKeR2$*ZuI(q}+O=cU3`*GDB)?;)qP1zSQ_kx+LlCXGsdZ4|)ZV6? zPVG55OPCU5Y1VZSl1^f7x@oO=Iqx zRlHe(r*3Dcrft2Z?Gw|%H;=%C3x5g+9FE_Xp7`F9ORKyv=ROC*iq5h?RuI0yF?WCj=XJ{iC2L8x0ted4Ql(0J&zhf@#LpVwsIo;p}Bo!?T zamEYT=k>(m)gfc~y0QH2xp$o3bw0fjvUaUoyNJb4KJkW3jq9dHC5vAPkEZB;k@etHy(_fwSvy32&lfNq-5p@iKn@;k=3{kkQ}xum;8eREfLkCOXf zRph*n=mY2?N|2==uIwQBecfgLZiZO=zHWvd-qo=Kx$o$N1G(8D%GK-;ugW)8d`P)OJjN=s-%?-8o_kJD;~_v@C93vp8S}h$#kjW5hm#Ivb5! zO3;l{nEud5gWw|e?k+(Y)ZOKSd6lyC=)9D8b)Ag;yBPa-({*V2aBsGJPMNcCb68yg)|6OXmyHhy#{tuEvPm*%8l{i?py?bf02Cpo7iLR<)u3rcq=g98K~snx z4w%A}<$-aLqD9kvirODgQo{flBdHlE%3A<@OyjWU&#G&=3&3iE9m*b+#wgE+dZudb z3SY%%B=&jgnx-T>yS%ramo}ZvWH7UiR)E@gWm9$vlqM~m8k%svpe z9esK>VH--h_dMUxoC{!EX0Y57gJnnzk0CKw#>E}4vbV&Tp~V(WcM${uqN^D6Xu+$- zFbx)ps9mtU0_v-gRsRgeZPf{~o6yL`S7&^!~Y?`L1m6u!x51gtCnkg`vw03V; zbBxryAy!P7-2)8+2=0Ng00b2A-34)Xfnfd6aFL>m#sHxc1bv2R4uc*!=kQ4fRum!2 zUD)FW0&~EF(*GbIe)w9Zg|M}Qtx1$YW{B(1F**4B@2|K5Y& zeemhs)G-dW$4wDoS-dbLyFQrxjW=@^VT2IKYFgxP^Ac2t_dqcp*tee|dpr$n{<>y3e+pdXDc~c!G*!ObDX@}Q6((~= zDbHO!d+lD;k5S$+iRm8KIi3PM9jsnyv!RKmKrKyy`gmnab_$dyEnORyF6|Vki`(}l z?Z;yFV{zN@r!d=nD&^Vxd`D~E6d0oK6sg+jzE?r!Hs(%TL6xJWzx@6&L9{sph^*P-Uj~#tb)c9G(CdG z37P^F;i2EiyzMp2edL^kfi6VteoAU!lUFtG<+A~=Sq1I_l6(52G~$m5lT`PFNvaQX zOj5bnB$b^_Qdwb=%GJWAlW7`Me`Lwdg5sp5ZNt*09WVB{y(eiu7_%RY+YUXoCTz!2 zP3`~ablxNwAheGf`HOkf$X`r~fc;T?@_{`?$T9$G0nr`i)RjQ&#G&hb>L*u+PPuPS>4_YsbKX&^ij zzeyZ5=z{;(5G<66pI1MNH{g+#o+=M<*QCoDwaA(QzKk-(Z$x)OA2>sAdi4`H7@Gf^ zRFC7s-z-e^cD|L(v1ss0lg6AUM)*F}Vv)(rzHadxhK8FaOet#tbv-0Nu6&oHt+(P3mFb0Ta~)9@np&5HsyF?!Sqs1x-I*ja@Ci) zP!CD~ADEQO9s*Vm3ojbs0e36h%h>;Exu&nw_ke$e7IUh}VcE!)DvK9d}-wWE#_q9mU9y3U@$Us=H&nHGwg(Z z6tjPe{-HVc_TS|k12j`a)NS##-OgswE3~sLFR$`R(@omEMd)ko*GV1cc)r7HLRhj# zJuY)YU!tq$@6DDaR`DFymZ;7=l$G$O~v75d5 zxmV3@;oPrFK8C{6y0m`R>P&VoIO|}K>QjqR)E2e`K4lZO8n(a@oo*#l; z>e;XiUweGq!AT0L^KjWH@Pc#TjFQCGwP8EjP3ZZAn_>mIBY(?wS}n@766;_6KLBy{ zL)LV7ZC{c}Z}lvP-?_BBI3L_iDki&u?BYsSDBzc6|B7mZA6^;fgXCB>qILrOv7$*M zj11K!_W9dnT3Am`R&v=fnP`bP?1w zgP}mW7U<3eFpaSLKfg35N>?V0+>27=y%f;;C=-1HKefRnxu0%{%J38moq#{5q!M*h zrb`%p0yAAC#-02ldWU?lz*! z6Xv&4E%0|1ZQf5eu2gCDMyZoI%jaeFFUqZtcE!t`KRBK!cZ1EWX-!sl#Hu^ikH@RK z9}cIgU2CrIl|3BZtg3%3|N51OLn%vjvbH}~+n=!Xrz&@3_qyP2Y9<=Ekqq361@1*> zNZ|88(D)}M-n|`?&Jz23Rydt%WKO}(+rrjM5=@Lg+W*b_Yu1FNakHjjP5-@`M@1=H z&36ue`|w)RnjE)5Zm=h2+w-LNQ;EIporu{^ByA@*Y$spTHLmUb?v-y3IeM-9`-K6NIBvaHHVR;t&YdHY;}|D;r~#jmb(+tkSc- zKiYoidFA03)*X*7d{n>YN?4mxHFe3F1F@O|pYD&>i~;)mSJpc|Eqc~|_<7}#oK_P% z;3HJ&hswSn1KT*oGBNhXW<4kUOzEnrkNZDq`SGqF?us@KJS!jgS*`O2wzVs9S8u$w z@8Q^s+LpD2WNqKK&5sN(Z1%^7kM{q-kgzqSoK3$|^8J$arr)f1G@NpJ)?L3z^L} zjl=6r>vG(&C+Rp4a~$}z_o)&bc4I7okIiVzw)HxY0Xx&d5(U_{szzaudg z5sA@|rF)d+o-{_nvT?&%ipHSMgJ{yg2EtpW$RxPq96hQ!XZZXbU0wgn!@8Ds5;eKt z%ni>QSSWi=m@}Ue9XA7OQt_s73sGr3?pki0ai)x+?W1cyRDof)7^f_X1)q zKd$gsmn)UBM|6RItKg>1Ia$GYKjs>><>|G#l=5EFa^QVQ%5gR4J<*QW!AllQJCBW1 zZsYv-W}REV5fGA?FZDE>P$|gj3iOaoNd(oL2PJ6YB5fb#;-KJ;T*X>R0n38a7SoIX zbAE5&&7AR&_huHdk^*n$LRPSs-x#uOg~rq`fi63;OL=6MX8G8i|% zS}^98L~GpeW2Ed+T1YS`ZU1BYIe0r4j^&khkpC5ikAFKRw>Mqj!Ch9;}ijI+vo3VA#n3LbE_4F+fZ;1@j3VE`NShawsG>^xnZ+37;Vmt47L zRK?W^pv(+8I2)9S|025s)U|?F5c=~pAK|Ay?8MYzfADQ{UBND7=jvoyb=ZDS2PDW=hZZ6)nJ9_JK5%N-gs zm_&fb!X@>IP^f z?F8J>?1x2ZSE?CPYesmQTJYbQTUcJuG;TpKvMr6OQ8X&=rsjv&xVahO+YV9)<>R9? zJ#`+MjXHx{9{}T){sFRBGUK18GQc#l^^!v0u0u4a+@el&9o^9@D2<0iK#aA_X!l35 z&h`S%_f|LyzH&3mG99Pynq7uiCh#upVC z#FHB-=%s+UU*e`^VzdR%D+!+W7kChX&=$0TQ@NwTKRqMN+mG66D*&emE#x3sk zo@bU$@K{ms%SWG8cg1bHKk1Fz_NMIZNV4yW_6%Vk??tUM>gs!P_L*y6y!P!+2a^Y< zVh5)ZwNudx@Xs82b|D;3P=B|EV?6aA5PDBWuRz&bQHA2hZqnk!71Y@b+$IaU)VjCE}C; zMMWg0-DQb23PzwOflALAE)Wra(Xc>K$PlKS7oiye(*GP(|M76jAX6k8Rw&xFN=Z$8 z4503=A&&{lFeQG<`JQ2h3L7`DBIg)R72sh|RMZ}#QW{vby(VnPV~N=J(E|7nyl~WT zj7nLMj?>+|G)U1P^!@?-8Nu&N8LrdA8#u6loKw()K-p)Y@QsJ#hIxJux`DWBS)wGz zGF?|K8}dfX8R~@zAxD@3Lx!vqf+0I2%~7Asp)(LQ$~=B}ELkI}XQ~(5;kCL4EG0(8 z)0>G&Z*=;`tLLP;rPOU{MO z7^Qt0bYl&9nAnte;TJaBqAGm(?V!n{iuw!SQ1q4KuWrvc& zcU8*WWEsj+?XvP6iCX!`chxy}H92?cMkwc0w>9}r-5TY)y6k)KdRrmWyjYzhazoJ9 zs$3mwF=!X38?>3`SXe~M3Mm{8b~M7FU7_H0Kl3>XIdb@rv%AZg>tvGmH-_ZDMy*W1 zu3juay>4A)7n7dLU?FHD1-#ky5D3`9^>tC`Bx>BhLQhb@LU180uQ_L&|Ag+Qz zJ64O~MRJ!+Z462=(-fy)-xO*A+V0x26~^U%jgjT4&r)`2OWtS_R36iNq$(gcP%uQn zNeXDrrOV}I7z_z7U|$J)h3P6yzh)oH|DN*ykb)=l($3uBrJTV`SLZy}%%!xgKxd~b za%WviV@F`Cy{- zV9G*!A(p0;d+#%x-Pp;#U|Hrpj-3}!HDeN=lDaCf{J(LQNhKC;Td(JlN>6r2dyYNr z_-P>e?tAg^ndn#m?+5h{sF->dKHFIKTz=H>1-OFBXk}BpwE1IWytI=zfI!01p0YcV z_Fec#?Ymy=sQu2^ua4nm2>e0GRo#<*vDxfVgx=BmaAI)@sUKh`14P&@v6Hc=?8xvg zQXQYvvaxWyd`jYKA63Ll8-CCiFLkFZb!$5kmXM2+>~nU zOPCvw?)Ff66P|lI=&EJ2X>Y>pOw~K-&aqkFi7-{;cxe1q#iolZs7L0uPD*ax1fRum z?>>sVH}~wNc=u+@mdT4xZeoK(9?HG&7k0g;yx*9=3LgufVP)RK% z3yx)YylvhVug&|G*U^{J!)u_tcr*X!Rb5kX4%G!+HlNq*E%6q6O??@nmGh}2t|s3$ z&XuWybE$GuO)lMpN-*U-Rmr35(@EXF3|S4*MXeEdsH9Lfuh--)LjBcVhqopNXO&RO z=2eK^0KUjMnsjPgpbnK(%D(sZZ4mss=p8lyt3rG^OAtvKy*#xvkc}Yv3zdkJ9H)55 zr4mrtdc8Znc11sq+m+UoydSX6j0EI>7UkOq04o#qTf%B${hT5#b29CmaOM?^Tiyq1 zel6a`T$UN|7kD37mT1`*5W{K@;tf^%EnBnK;;qcVSn0BC7M<7X-R`aO?oh@hY052D zE$%>xV0iL>-coOcx6E6v+&}CWkx3V z%{1A$N@RcNI+QKuLh7ae z&X9Zw1<98wxPoAHj3}WjIh9$UL5Q*pt&QlI%rl>O=Fy016_V&Mq<19VZ<8-jnVp5p zWZ4``3Q6j#)UZ0}0m&O8p&rQV>kzn-iLa#_5PB}Ns zkpc*xbn<_vpaPTF(=H@|OoQ-?8c!PEJygqTo*`NK^0`1urQ04-}}Bk)U=- zZAT_Krc8XJT!SFej(`cKHwuL0(*~ANs1adY`eBnNDCb)DD3E@WXse$xZS~Tt*Oinf z&2=$z-Q(F*y(d+_J5}G$7rkGUno9a#R&WMa%OBh zY$)0LavNuK#>~#Onv|{I(_p+JuT`^kOKrwYs}mlb??Xd!j!u^cT%->Vkmu z(SdJ1SZn^BuJ3mxEZr&fKs?if_lf*^AT$P!8^%cH_COjZq1%1w3`J)QY@6d6M1w!q z!8S*T&M{GhG#_M;h{BLTB5J%vZ`)Y{X_XD!3N0~nOSJV+s`bzp27Sp94e)CZ66Srl zFRO>1QE8*b&w8{0qFAE978DAMPcGJv&@l)bA9*cDNUtWH-7#l(qO?2b_0j0*tI<&( zO1P#E5b+j-Oc1~a;R+tWQ-p^B;Sv4=iN%feEN-;1xUn4KMyA*p1@5i-lxr_xKtkNT z2a^4#WBsRpawRq9PmTFfV>6!@8}}+}=~S~?GIcChSDkV}y#@bWDOYF8)s=F!eNkRi(vGgBJk2q4^V*e! zxt9TKkD1$}9fuRbq0*J*oQ6zsA7m5_F)JdMm+n!oB%(T3bK| z>L+z9>chat35cD%<|B*vWp}Hw>rMhmgLci==$5Ox{FS<)_cOZZebBkitZ+(s6i?zr zm$FV4OfS-;6SR51zRbO{um>uc7E{G%ScNn6%yd&L@{ZzrwI- zy! zq6c*^l`YD#@P`&SH&(ov8*a5IWt&_nx=p5rX0>4XXLR*v2v&o%#Io?LE#k=oigXrP z%lMarr7YlB=1FDqOWFb?6ldufSm5k3)=y9Wl7jz4!GETJ1ce@xSfelvabHS>4BSQCVSkXN@Pha5+f-Ao& z`Kk5`Lf`AWqVL6Ok&QQ*N9Ro!>%8<|OLaBvFUxe5J0F(w1vXK|3$$t1xzI{<-lYC69}c0xr~A$3d|Ig zP*6+(E!pyKQ{bfF2?al-fXN3YB$ztEq}0j(j)Fg=fN29J6__?)DuIaxrXiSAV6uSe zlya&lM6bS&jJ(}=?xPFOmO@KJ!5Gb4c~E(U8avo zdYjKDc5QvWbeYe0YuP_fmhW+A@j-e8b&>hGP%sqs`3lj6g34Q=qQ6eT83du&QoC5kn zMD8Q_#oyO`P4~(;p*OwrDj=QEn_6CtOZ!c`UwxoIsyCfAygG$%y=nieMXB3#)+Lz z8ehFH4M?W0pAQ<#O*>!e5b&U7fILM)jk}g09HQVKP(Yu2PXA{*k+%&G(&mGrFAmEt pAV>H`oCsbT^m_egy0Xu7<)7&+pXn;_|G6<@*YEsXM?GA)7hV-N=hn4pZ$Vn9X%LmWd8qbGw0V+o@?Ll9F5QxLN>2WCb_#=8s(5BNlzxQp0q6J9Gd5 diff --git a/pygad/visualize/__pycache__/plot.cpython-314.pyc b/pygad/visualize/__pycache__/plot.cpython-314.pyc deleted file mode 100644 index 92b809f2149b943317afc577f6f040eebecd2c35..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23529 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 From 9165b5cb1f8fde7bcd00ca228349eb040c0093d0 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Mon, 13 Apr 2026 11:22:41 -0400 Subject: [PATCH 16/42] Safe TensorFlow installation --- .github/workflows/main.yml | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d0e0ef6..5496523 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,6 +27,7 @@ jobs: # The Strategy Matrix defines the environments to test. # GitHub Actions will spawn a separate, parallel job for each version in the list. strategy: + fail-fast: false matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] @@ -35,11 +36,11 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Setup the specific Python version defined in the current matrix iteration. - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -47,9 +48,15 @@ jobs: - name: Install Dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt + # Added timeout and no-cache to prevent SSL/Network decryption errors + pip install --default-timeout=100 --no-cache-dir -r requirements.txt + if [ "${{ matrix.python-version }}" != "3.14" ] && [ "${{ matrix.python-version }}" != "3.8" ]; then - pip install tensorflow + # Using a retry loop for large DL frameworks to handle transient network issues + for i in {1..3}; do + pip install --default-timeout=100 --no-cache-dir tensorflow && break || \ + (echo "Retry $i failed, waiting 10s..." && sleep 10) + done pip install --upgrade keras pip install torch fi @@ -64,7 +71,7 @@ jobs: # Install the newly built .whl file to verify the package is installable. - name: Install PyGAD from Wheel run: | - find ./dist/*.whl | xargs pip install + pip install dist/*.whl - name: Install PyTest run: pip install pytest @@ -78,4 +85,4 @@ jobs: pytest --ignore=tests/test_kerasga.py --ignore=tests/test_torchga.py else pytest - fi + fi \ No newline at end of file From da471cc32f755e504a2b0ae82afa23f9d0073233 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Mon, 13 Apr 2026 11:31:17 -0400 Subject: [PATCH 17/42] Pass shape --- tests/test_kerasga.py | 2 +- tests/test_torchga.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_kerasga.py b/tests/test_kerasga.py index c3557ff..215a996 100644 --- a/tests/test_kerasga.py +++ b/tests/test_kerasga.py @@ -10,7 +10,7 @@ def test_kerasga_evolution(): data_inputs = numpy.array([[0, 0], [0, 1], [1, 0], [1, 1]]) data_outputs = numpy.array([[1, 0], [0, 1], [0, 1], [1, 0]]) # One-hot encoded - input_layer = tensorflow.keras.layers.Input(2) + input_layer = tensorflow.keras.layers.Input(shape=(2,)) dense_layer = tensorflow.keras.layers.Dense(4, activation="relu")(input_layer) output_layer = tensorflow.keras.layers.Dense(2, activation="softmax")(dense_layer) diff --git a/tests/test_torchga.py b/tests/test_torchga.py index bbb8fd2..435c094 100644 --- a/tests/test_torchga.py +++ b/tests/test_torchga.py @@ -1,4 +1,5 @@ import pygad +import pygad.torchga import torch def test_torchga_evolution(): From c112afb071b477b699539838485dd12e53e688c1 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Mon, 13 Apr 2026 11:42:07 -0400 Subject: [PATCH 18/42] Update license info --- pyproject.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1a22907..97e5fe7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,9 @@ version = "3.6.0" description = "PyGAD: A Python Library for Building the Genetic Algorithm and Training Machine Learning Algoithms (Keras & PyTorch)." readme = {file = "README.md", content-type = "text/markdown"} requires-python = ">=3" -license = {file = "LICENSE"} +# license = {file = "LICENSE"} +license = "BSD-3-Clause" +license-files = ["LICENSE"] authors = [ {name = "Ahmed Gad", email = "ahmed.f.gad@gmail.com"}, ] @@ -21,7 +23,7 @@ maintainers = [ {name = "Ahmed Gad", email = "ahmed.f.gad@gmail.com"} ] classifiers = [ - "License :: OSI Approved :: BSD License", + # "License :: OSI Approved :: BSD License", "Programming Language :: Python", "Programming Language :: Python :: 3", "Natural Language :: English", From 579b0063cb1b1970775702f510ae33124104ea92 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Mon, 13 Apr 2026 12:07:47 -0400 Subject: [PATCH 19/42] Revert back license to work with Py 3.8 --- .github/workflows/main.yml | 11 +++++++++++ pyproject.toml | 6 +++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5496523..8642b97 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -52,15 +52,26 @@ jobs: pip install --default-timeout=100 --no-cache-dir -r requirements.txt if [ "${{ matrix.python-version }}" != "3.14" ] && [ "${{ matrix.python-version }}" != "3.8" ]; then + # This block runs if the version IS NOT 3.14 or 3.8 # Using a retry loop for large DL frameworks to handle transient network issues + echo "Installing deep learning libraries." for i in {1..3}; do pip install --default-timeout=100 --no-cache-dir tensorflow && break || \ (echo "Retry $i failed, waiting 10s..." && sleep 10) done pip install --upgrade keras pip install torch + else + # This block runs if the version IS 3.14 or 3.8 + echo "Skipping heavy deep learning libraries for Python ${{ matrix.python-version }}." fi + # Verify the core deep learning frameworks + echo "Verifying installations..." + python -c "import tensorflow; print('TensorFlow version:', tensorflow.__version__)" || echo "TensorFlow not installed" + python -c "import keras; print('Keras version:', keras.__version__)" || echo "Keras not installed" + python -c "import torch; print('PyTorch version:', torch.__version__)" || echo "PyTorch not installed" + # Build the PyGAD package distribution (generating .tar.gz and .whl files). # This ensures the package build process is valid on this Python version. - name: Build PyGAD diff --git a/pyproject.toml b/pyproject.toml index 97e5fe7..2f7c604 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,9 +13,9 @@ version = "3.6.0" description = "PyGAD: A Python Library for Building the Genetic Algorithm and Training Machine Learning Algoithms (Keras & PyTorch)." readme = {file = "README.md", content-type = "text/markdown"} requires-python = ">=3" -# license = {file = "LICENSE"} -license = "BSD-3-Clause" -license-files = ["LICENSE"] +license = {file = "LICENSE"} +# license = "BSD-3-Clause" +# license-files = ["LICENSE"] authors = [ {name = "Ahmed Gad", email = "ahmed.f.gad@gmail.com"}, ] From b682ec3f26da38bea1dec92c9f4a1956f98502dd Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 21 May 2026 13:12:19 -0400 Subject: [PATCH 20/42] Fix inconsistent lists --- pygad/utils/engine.py | 3 --- tests/test_save_solutions.py | 50 ++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/pygad/utils/engine.py b/pygad/utils/engine.py index eca1e8f..5f406f8 100644 --- a/pygad/utils/engine.py +++ b/pygad/utils/engine.py @@ -493,9 +493,6 @@ def run(self): if not (self.on_generation is None): r = self.on_generation(self) if type(r) is str and r.lower() == "stop": - # Before aborting the loop, save the fitness value of the best solution. - # _, best_solution_fitness, _ = self.best_solution() - self.best_solutions_fitness.append(best_solution_fitness) break if not self.stop_criteria is None: diff --git a/tests/test_save_solutions.py b/tests/test_save_solutions.py index 537f4ea..1052c13 100644 --- a/tests/test_save_solutions.py +++ b/tests/test_save_solutions.py @@ -1050,6 +1050,56 @@ def test_save_solutions_both_keep_adaptive_mutation_save_solutions_multi_objecti +#### List length consistency + +def run_for_list_lengths(multi_objective=False, + save_solutions=False, + save_best_solutions=False, + stop_at=None): + + def fitness_func_single(ga, solution, idx): + return random.random() + + def fitness_func_multi(ga, solution, idx): + return [random.random(), random.random()] + + fitness_func = fitness_func_multi if multi_objective else fitness_func_single + + on_generation = None + if stop_at is not None: + def on_generation(ga): + if ga.generations_completed >= stop_at: + return "stop" + + ga_optimizer = pygad.GA(num_generations=num_generations, + sol_per_pop=sol_per_pop, + num_genes=6, + num_parents_mating=num_parents_mating, + fitness_func=fitness_func, + on_generation=on_generation, + save_best_solutions=save_best_solutions, + save_solutions=save_solutions, + random_seed=42, + suppress_warnings=True) + ga_optimizer.run() + return ga_optimizer + +def test_list_lengths_best_solutions_on_generation_stop(): + ga = run_for_list_lengths(save_best_solutions=True, stop_at=10) + assert len(ga.best_solutions) == len(ga.best_solutions_fitness) + +def test_list_lengths_best_solutions_on_generation_stop_multi_objective(): + ga = run_for_list_lengths(multi_objective=True, save_best_solutions=True, stop_at=10) + assert len(ga.best_solutions) == len(ga.best_solutions_fitness) + +def test_list_lengths_best_solutions_normal_completion(): + ga = run_for_list_lengths(save_best_solutions=True) + assert len(ga.best_solutions) == len(ga.best_solutions_fitness) + +def test_list_lengths_solutions_on_generation_stop(): + ga = run_for_list_lengths(save_solutions=True, stop_at=10) + assert len(ga.solutions) == len(ga.solutions_fitness) + if __name__ == "__main__": #### Single Objective print() From 19375537e27a4ca35a2c1c242220dade967014df Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 21 May 2026 14:49:27 -0400 Subject: [PATCH 21/42] docs: build from Markdown (MyST), switch to Furo, add diagrams, regroup nav Build pipeline: - Read the docs directly from Markdown via myst-parser; drop the Typora .md -> .rst conversion. Move the page sources into docs/source and delete the generated .rst files. - Switch the HTML theme to Furo; add docs/requirements.txt and enable it in .readthedocs.yaml. Keep myst_heading_anchors off so heading anchors match the live site. Navigation: - Convert index.rst to index.md and group the toctree into five captioned sections. Re-level every page to a single top-level heading so the sidebar is a compact, grouped page list (maxdepth 1) instead of one flat list. Diagrams (committed SVG with PNG fallback for PDF/ePub): - keep_elitism/keep_parents offspring decision tree, population assembly, GA life cycle, crossover types, and mutation. Content: - Plain-English pass on README, index, pygad, pygad_more (elitism), utils, and visualize. - Fix broken cross-links (utils vs pygad_more anchors, legacy Footer.html), equation typos, and the wrong utils page title. --- .gitignore | 7 + .readthedocs.yaml | 9 +- README.md | 36 +- docs/requirements.txt | 8 + docs/source/_static/custom.css | 33 + docs/{md => source}/cnn.md | 54 +- docs/source/cnn.rst | 748 ----- docs/source/conf.py | 153 +- docs/{md => source}/gacnn.md | 52 +- docs/source/gacnn.rst | 662 ---- docs/{md => source}/gann.md | 56 +- docs/source/gann.rst | 1267 -------- docs/{md => source}/helper.md | 0 docs/source/helper.rst | 114 - docs/source/images/crossover_types.png | Bin 0 -> 225770 bytes docs/source/images/crossover_types.svg | 33 + docs/source/images/ga_lifecycle.png | Bin 0 -> 136091 bytes docs/source/images/ga_lifecycle.svg | 69 + docs/source/images/mutation.png | Bin 0 -> 84083 bytes docs/source/images/mutation.svg | 26 + .../source/images/offspring_decision_tree.png | Bin 0 -> 151102 bytes .../source/images/offspring_decision_tree.svg | 96 + docs/source/images/population_assembly.png | Bin 0 -> 99846 bytes docs/source/images/population_assembly.svg | 62 + docs/{md/HEADER.md => source/index.md} | 99 +- docs/source/index.rst | 429 --- docs/{md => source}/kerasga.md | 50 +- docs/source/kerasga.rst | 1078 ------- docs/{md => source}/nn.md | 64 +- docs/source/nn.rst | 976 ------ docs/{md => source}/pygad.md | 168 +- docs/source/pygad.rst | 1581 ---------- docs/{md => source}/pygad_more.md | 152 +- docs/source/pygad_more.rst | 2645 ---------------- docs/{md => source}/releases.md | 34 +- docs/source/releases.rst | 2662 ----------------- docs/{md => source}/torchga.md | 48 +- docs/source/torchga.rst | 944 ------ docs/{md => source}/utils.md | 108 +- docs/source/utils.rst | 953 ------ docs/{md => source}/visualize.md | 84 +- docs/source/visualize.rst | 511 ---- 42 files changed, 985 insertions(+), 15086 deletions(-) create mode 100644 docs/requirements.txt create mode 100644 docs/source/_static/custom.css rename docs/{md => source}/cnn.md (96%) delete mode 100644 docs/source/cnn.rst rename docs/{md => source}/gacnn.md (95%) delete mode 100644 docs/source/gacnn.rst rename docs/{md => source}/gann.md (97%) delete mode 100644 docs/source/gann.rst rename docs/{md => source}/helper.md (100%) delete mode 100644 docs/source/helper.rst create mode 100644 docs/source/images/crossover_types.png create mode 100644 docs/source/images/crossover_types.svg create mode 100644 docs/source/images/ga_lifecycle.png create mode 100644 docs/source/images/ga_lifecycle.svg create mode 100644 docs/source/images/mutation.png create mode 100644 docs/source/images/mutation.svg create mode 100644 docs/source/images/offspring_decision_tree.png create mode 100644 docs/source/images/offspring_decision_tree.svg create mode 100644 docs/source/images/population_assembly.png create mode 100644 docs/source/images/population_assembly.svg rename docs/{md/HEADER.md => source/index.md} (54%) delete mode 100644 docs/source/index.rst rename docs/{md => source}/kerasga.md (97%) delete mode 100644 docs/source/kerasga.rst rename docs/{md => source}/nn.md (96%) delete mode 100644 docs/source/nn.rst rename docs/{md => source}/pygad.md (85%) delete mode 100644 docs/source/pygad.rst rename docs/{md => source}/pygad_more.md (94%) delete mode 100644 docs/source/pygad_more.rst rename docs/{md => source}/releases.md (98%) delete mode 100644 docs/source/releases.rst rename docs/{md => source}/torchga.md (97%) delete mode 100644 docs/source/torchga.rst rename docs/{md => source}/utils.md (94%) delete mode 100644 docs/source/utils.rst rename docs/{md => source}/visualize.md (77%) delete mode 100644 docs/source/visualize.rst diff --git a/.gitignore b/.gitignore index 4a84706..2945cf9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,10 @@ __pycache__/ *.py[cod] *$py.class + +# ============================================================================= +# Documentation build output and local doc-build virtual environments. +# These are generated by Sphinx and should not be committed. +# ============================================================================= +docs/build/ +.venv-docs/ diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 73f9a19..28e5433 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -27,9 +27,8 @@ formats: - pdf - epub -# Optional but recommended, declare the Python requirements required -# to build your documentation +# Declare the Python requirements needed to build the documentation. # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html -# python: -# install: -# - requirements: docs/requirements.txt \ No newline at end of file +python: + install: + - requirements: docs/requirements.txt \ No newline at end of file diff --git a/README.md b/README.md index 33710b4..3679e8e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# PyGAD: Genetic Algorithm in Python +# PyGAD: Genetic Algorithm in Python [PyGAD](https://pypi.org/project/pygad) is an open-source easy-to-use Python 3 library for building the genetic algorithm and optimizing machine learning algorithms. It supports Keras and PyTorch. PyGAD supports optimizing both single-objective and multi-objective problems. -> Try the [Optimization Gadget](https://optimgadget.com), a free cloud-based tool powered by PyGAD. It simplifies optimization by reducing or eliminating the need for coding while providing insightful visualizations. +> Try the [Optimization Gadget](https://optimgadget.com), a free cloud-based tool powered by PyGAD. It makes optimization easier by reducing or removing the need for coding, and it shows helpful visualizations. -Check documentation of the [PyGAD](https://pygad.readthedocs.io/en/latest). +Read the [PyGAD documentation](https://pygad.readthedocs.io/en/latest). [![PyPI Downloads](https://pepy.tech/badge/pygad)](https://pepy.tech/project/pygad) [![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/pygad.svg?label=Conda%20Downloads)]( https://anaconda.org/conda-forge/PyGAD) [![PyPI version](https://badge.fury.io/py/pygad.svg)](https://badge.fury.io/py/pygad)![Docs](https://readthedocs.org/projects/pygad/badge)[![PyGAD PyTest / Python 3.13](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py313.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py313.yml) [![PyGAD PyTest / Python 3.12](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py312.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py312.yml) [![PyGAD PyTest / Python 3.11](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py311.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py311.yml) [![PyGAD PyTest / Python 3.10](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py310.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py310.yml) [![PyGAD PyTest / Python 3.9](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py39.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py39.yml) [![PyGAD PyTest / Python 3.8](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py38.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py38.yml) [![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) [![Translation](https://hosted.weblate.org/widgets/weblate/-/svg-badge.svg)](https://hosted.weblate.org/engage/weblate/) [![REUSE](https://api.reuse.software/badge/github.com/WeblateOrg/weblate)](https://api.reuse.software/info/github.com/WeblateOrg/weblate) [![Stack Overflow](https://img.shields.io/badge/stackoverflow-Ask%20questions-blue.svg)]( @@ -12,7 +12,7 @@ https://stackoverflow.com/questions/tagged/pygad) [![OpenSSF Scorecard](https:// ![PYGAD-LOGO](https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png) -[PyGAD](https://pypi.org/project/pygad) supports different types of crossover, mutation, and parent selection. [PyGAD](https://pypi.org/project/pygad) allows different types of problems to be optimized using the genetic algorithm by customizing the fitness function. +[PyGAD](https://pypi.org/project/pygad) supports different types of crossover, mutation, and parent selection. It lets you optimize many types of problems with the genetic algorithm by writing your own fitness function. The library is under active development and more features are added regularly. If you want a feature to be supported, please check the **Contact Us** section to send a request. @@ -25,19 +25,19 @@ The library is under active development and more features are added regularly. I # Installation -To install [PyGAD](https://pypi.org/project/pygad), simply use pip to download and install the library from [PyPI](https://pypi.org/project/pygad) (Python Package Index). The library is at PyPI at this page https://pypi.org/project/pygad. +To install [PyGAD](https://pypi.org/project/pygad), use pip to download and install the library from [PyPI](https://pypi.org/project/pygad) (Python Package Index). The library is available on PyPI at this page: https://pypi.org/project/pygad. Install PyGAD with the following command: -```python +``` pip install pygad ``` -To get started with PyGAD, please read the documentation at [Read The Docs](https://pygad.readthedocs.io/) https://pygad.readthedocs.io. +To get started with PyGAD, read the documentation at [Read the Docs](https://pygad.readthedocs.io). # PyGAD Source Code -The source code of the PyGAD' modules is found in the following GitHub projects: +The source code of the PyGAD modules is in the following GitHub projects: - [pygad](https://github.com/ahmedfgad/GeneticAlgorithmPython): (https://github.com/ahmedfgad/GeneticAlgorithmPython) - [pygad.nn](https://github.com/ahmedfgad/NumPyANN): https://github.com/ahmedfgad/NumPyANN @@ -47,13 +47,11 @@ The source code of the PyGAD' modules is found in the following GitHub projects: - [pygad.kerasga](https://github.com/ahmedfgad/KerasGA): https://github.com/ahmedfgad/KerasGA - [pygad.torchga](https://github.com/ahmedfgad/TorchGA): https://github.com/ahmedfgad/TorchGA -The documentation of PyGAD is available at [Read The Docs](https://pygad.readthedocs.io/) https://pygad.readthedocs.io. - # PyGAD Documentation -The documentation of the PyGAD library is available at [Read The Docs](https://pygad.readthedocs.io) at this link: https://pygad.readthedocs.io. It discusses the modules supported by PyGAD, all its classes, methods, attribute, and functions. For each module, a number of examples are given. +The PyGAD documentation is available at [Read the Docs](https://pygad.readthedocs.io) at this link: https://pygad.readthedocs.io. It explains the modules supported by PyGAD and all its classes, methods, attributes, and functions. For each module, several examples are given. -If there is an issue using PyGAD, feel free to post at issue in this [GitHub repository](https://github.com/ahmedfgad/GeneticAlgorithmPython) https://github.com/ahmedfgad/GeneticAlgorithmPython or by sending an e-mail to ahmed.f.gad@gmail.com. +If you have an issue using PyGAD, feel free to post an issue in this [GitHub repository](https://github.com/ahmedfgad/GeneticAlgorithmPython) or send an e-mail to ahmed.f.gad@gmail.com. If you built a project that uses PyGAD, then please drop an e-mail to ahmed.f.gad@gmail.com with the following information so that your project is included in the documentation. @@ -65,7 +63,7 @@ Please check the **Contact Us** section for more contact details. # Life Cycle of PyGAD -The next figure lists the different stages in the lifecycle of an instance of the `pygad.GA` class. Note that PyGAD stops when either all generations are completed or when the function passed to the `on_generation` parameter returns the string `stop`. +The next figure shows the main stages in the life cycle of a `pygad.GA` instance. PyGAD stops when all generations are completed or when the function passed to the `on_generation` parameter returns the string `stop`. ![PyGAD Lifecycle](https://user-images.githubusercontent.com/16560492/220486073-c5b6089d-81e4-44d9-a53c-385f479a7273.jpg) @@ -122,7 +120,7 @@ ga_instance = pygad.GA(num_generations=3, ga_instance.run() ``` -Based on the used 3 generations as assigned to the `num_generations` argument, here is the output. +Because `num_generations` is set to 3, here is the output. ``` on_start() @@ -158,7 +156,7 @@ import numpy """ Given the following function: - y = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + 6wx6 + y = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + w6x6 where (x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7) and y=44 What are the best values for the 6 weights (w1 to w6)? We are going to use the genetic algorithm to optimize this function. """ @@ -168,7 +166,7 @@ desired_output = 44 # Function output. def fitness_func(ga_instance, solution, solution_idx): # Calculating the fitness value of each solution in the current population. - # The fitness function calulates the sum of products between each input and its corresponding weight. + # The fitness function calculates the sum of products between each input and its corresponding weight. output = numpy.sum(solution*function_inputs) fitness = 1.0 / numpy.abs(output - desired_output) return fitness @@ -203,7 +201,7 @@ ga_instance = pygad.GA(num_generations=num_generations, # Running the GA to optimize the parameters of the function. ga_instance.run() -# After the generations complete, some plots are showed that summarize the how the outputs/fitenss values evolve over generations. +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. ga_instance.plot_fitness() # Returning the details of the best solution. @@ -229,7 +227,7 @@ loaded_ga_instance.plot_fitness() # For More Information -There are different resources that can be used to get started with the genetic algorithm and building it in Python. +Here are some resources to help you get started with the genetic algorithm and build it in Python. ## Tutorial: Implementing Genetic Algorithm in Python @@ -239,7 +237,7 @@ To start with coding the genetic algorithm, you can check the tutorial titled [* - [Towards Data Science](https://towardsdatascience.com/genetic-algorithm-implementation-in-python-5ab67bb124a6) - [KDnuggets](https://www.kdnuggets.com/2018/07/genetic-algorithm-implementation-python.html) -[This tutorial](https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad) is prepared based on a previous version of the project but it still a good resource to start with coding the genetic algorithm. +[This tutorial](https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad) is based on an earlier version of the project, but it is still a good resource to start coding the genetic algorithm. [![Genetic Algorithm Implementation in Python](https://user-images.githubusercontent.com/16560492/78830052-a3c19300-79e7-11ea-8b9b-4b343ea4049c.png)](https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad) diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..26c1a64 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,8 @@ +# Dependencies for building the PyGAD documentation with Sphinx. +# Read the Docs installs these (see .readthedocs.yaml). +sphinx==7.4.7 +myst-parser==3.0.1 +furo==2024.8.6 +sphinx-design==0.6.1 +sphinx-copybutton==0.5.2 +linkify-it-py==2.0.3 diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css new file mode 100644 index 0000000..a65a6d8 --- /dev/null +++ b/docs/source/_static/custom.css @@ -0,0 +1,33 @@ +/* Custom styles for the PyGAD documentation (Furo theme). */ + +/* Center figures and keep diagrams from stretching too wide. */ +figure { + text-align: center; + margin: 1.5rem auto; +} + +figure img { + max-width: 100%; + height: auto; +} + +/* The diagrams have a light card background baked in, so give them a soft + border and rounded corners that look right in both light and dark mode. */ +img.pygad-diagram { + border: 1px solid rgba(128, 128, 128, 0.25); + border-radius: 8px; + background: #ffffff; +} + +/* Figure captions: a touch smaller and muted. */ +figure figcaption { + font-size: 0.9rem; + color: var(--color-foreground-secondary); + margin-top: 0.5rem; +} + +/* Make tables a little easier to scan. */ +table.docutils td, +table.docutils th { + padding: 0.4rem 0.7rem; +} diff --git a/docs/md/cnn.md b/docs/source/cnn.md similarity index 96% rename from docs/md/cnn.md rename to docs/source/cnn.md index c57ab00..879b9ca 100644 --- a/docs/md/cnn.md +++ b/docs/source/cnn.md @@ -6,7 +6,7 @@ Using the **pygad.cnn** module, convolutional neural networks (CNNs) are created Later, the **pygad.gacnn** module is used to train the **pygad.cnn** network using the genetic algorithm built in the **pygad** module. -# Supported Layers +## Supported Layers Each layer supported by the **pygad.cnn** module has a corresponding class. The layers and their classes are: @@ -30,7 +30,7 @@ Except for the input layer, all of listed layers has 4 instance attributes that In addition to such attributes, the layers may have some additional attributes. The next subsections discuss such layers. -## `pygad.cnn.Input2D` Class +### `pygad.cnn.Input2D` Class The `pygad.cnn.Input2D` class creates the input layer for the convolutional neural network. For each network, there is only a single input layer. The network architecture must start with an input layer. @@ -59,7 +59,7 @@ print("Input2D Output shape =", layer_output_size) This is everything about the input layer. -## `pygad.cnn.Conv2D` Class +### `pygad.cnn.Conv2D` Class Using the `pygad.cnn.Conv2D` class, convolution (conv) layers can be created. To create a convolution layer, just create a new instance of the class. The constructor accepts the following parameters: @@ -133,7 +133,7 @@ input_shape = input_layer.num_neurons print("Input shape =", input_shape) ``` -## `pygad.cnn.MaxPooling2D` Class +### `pygad.cnn.MaxPooling2D` Class The `pygad.cnn.MaxPooling2D` class builds a max pooling layer for the CNN architecture. The constructor of this class accepts the following parameter: @@ -147,11 +147,11 @@ Within the constructor, the accepted parameters are used as instance attributes. - `layer_output_size` - `layer_output` -## `pygad.cnn.AveragePooling2D` Class +### `pygad.cnn.AveragePooling2D` Class The `pygad.cnn.AveragePooling2D` class is similar to the `pygad.cnn.MaxPooling2D` class except that it applies the max pooling operation rather than average pooling. -## `pygad.cnn.Flatten` Class +### `pygad.cnn.Flatten` Class The `pygad.cnn.Flatten` class implements the flatten layer which converts the output of the previous layer into a 1D vector. The constructor accepts only the `previous_layer` parameter. @@ -162,7 +162,7 @@ The following instance attributes exist: * `layer_output_size` * `layer_output` -## `pygad.cnn.ReLU` Class +### `pygad.cnn.ReLU` Class The `pygad.cnn.ReLU` class implements the ReLU layer which applies the ReLU activation function to the output of the previous layer. @@ -175,11 +175,11 @@ The following instance attributes exist: * `layer_output_size` * `layer_output` -## `pygad.cnn.Sigmoid` Class +### `pygad.cnn.Sigmoid` Class The `pygad.cnn.Sigmoid` class is similar to the `pygad.cnn.ReLU` class except that it applies the sigmoid function rather than the ReLU function. -## `pygad.cnn.Dense` Class +### `pygad.cnn.Dense` Class The `pygad.cnn.Dense` class implement the dense layer. Its constructor accepts the following parameters: @@ -195,7 +195,7 @@ Within the constructor, the accepted parameters are used as instance attributes. * `layer_output_size` * `layer_output` -# `pygad.cnn.Model` Class +## `pygad.cnn.Model` Class An instance of the `pygad.cnn.Model` class represents a CNN model. The constructor of this class accepts the following parameters: @@ -207,11 +207,11 @@ Within the constructor, the accepted parameters are used as instance attributes. There are a number of methods in the `pygad.cnn.Model` class which serves in training, testing, and retrieving information about the model. These methods are discussed in the next subsections. -### `get_layers()` +#### `get_layers()` Creates a list of all layers in the CNN model. It accepts no parameters. -### `train()` +#### `train()` Trains the CNN model. @@ -225,15 +225,15 @@ This method trains the CNN model according to the number of epochs specified in It is important to note that no learning algorithm is used for training the pygad.cnn. Just the learning rate is used for making some changes which is better than leaving the weights unchanged. -### `feed_sample()` +#### `feed_sample()` Feeds a sample in the CNN layers and returns results of the last layer in the pygad.cnn. -### `update_weights()` +#### `update_weights()` Updates the CNN weights using the learning rate. It is important to note that no learning algorithm is used for training the pygad.cnn. Just the learning rate is used for making some changes which is better than leaving the weights unchanged. -### `predict()` +#### `predict()` Uses the trained CNN for making predictions. @@ -243,11 +243,11 @@ Accepts the following parameter: It returns a list holding the samples predictions. -### `summary()` +#### `summary()` Prints a summary of the CNN architecture. -# Supported Activation Functions +## Supported Activation Functions The supported activation functions in the convolution layer are: @@ -256,7 +256,7 @@ The supported activation functions in the convolution layer are: The dense layer supports these functions besides the `softmax` function implemented in the `pygad.cnn.softmax()` function. -# Steps to Build a Neural Network +## Steps to Build a Neural Network This section discusses how to use the `pygad.cnn` module for building a neural network. The summary of the steps are as follows: @@ -268,7 +268,7 @@ This section discusses how to use the `pygad.cnn` module for building a neural n - Making Predictions - Calculating Some Statistics -## Reading the Data +### Reading the Data Before building the network architecture, the first thing to do is to prepare the data that will be used for training the network. @@ -299,7 +299,7 @@ train_outputs = numpy.load("dataset_outputs.npy") After the data is prepared, next is to create the network architecture. -## Building the Network Architecture +### Building the Network Architecture The input layer is created by instantiating the `pygad.cnn.Input2D` class according to the next code. A network can only have a single input layer. @@ -351,7 +351,7 @@ dense_layer2 = pygad.cnn.Dense(num_neurons=4, After the network architecture is prepared, the next step is to create a CNN model. -## Building Model +### Building Model The CNN model is created as an instance of the `pygad.cnn.Model` class. Here is an example. @@ -363,7 +363,7 @@ model = pygad.cnn.Model(last_layer=dense_layer2, After the model is created, a summary of the model architecture can be printed. -## Model Summary +### Model Summary The `summary()` method in the `pygad.cnn.Model` class prints a summary of the CNN model. @@ -388,7 +388,7 @@ model.summary() ---------------------------------------- ``` -## Training the Network +### Training the Network After the model and the data are prepared, then the model can be trained using the the `pygad.cnn.train()` method. @@ -399,7 +399,7 @@ model.train(train_inputs=train_inputs, After training the network, the next step is to make predictions. -## Making Predictions +### Making Predictions The `pygad.cnn.predict()` method uses the trained network for making predictions. Here is an example. @@ -409,7 +409,7 @@ predictions = model.predict(data_inputs=train_inputs) It is not expected to have high accuracy in the predictions because no training algorithm is used. -## Calculating Some Statistics +### Calculating Some Statistics Based on the predictions the network made, some statistics can be calculated such as the number of correct and wrong predictions in addition to the classification accuracy. @@ -424,11 +424,11 @@ print(f"Classification accuracy : {accuracy}.") It is very important to note that it is not expected that the classification accuracy is high because no training algorithm is used. Please check the documentation of the `pygad.gacnn` module for training the CNN using the genetic algorithm. -# Examples +## Examples This section gives the complete code of some examples that build neural networks using `pygad.cnn`. Each subsection builds a different network. -## Image Classification +### Image Classification This example is discussed in the **Steps to Build a Convolutional Neural Network** section and its complete code is listed below. diff --git a/docs/source/cnn.rst b/docs/source/cnn.rst deleted file mode 100644 index eabe5a1..0000000 --- a/docs/source/cnn.rst +++ /dev/null @@ -1,748 +0,0 @@ -.. _pygadcnn-module: - -``pygad.cnn`` Module -==================== - -This section of the PyGAD's library documentation discusses the -**pygad.cnn** module. - -Using the **pygad.cnn** module, convolutional neural networks (CNNs) are -created. The purpose of this module is to only implement the **forward -pass** of a convolutional neural network without using a training -algorithm. The **pygad.cnn** module builds the network layers, -implements the activations functions, trains the network, makes -predictions, and more. - -Later, the **pygad.gacnn** module is used to train the **pygad.cnn** -network using the genetic algorithm built in the **pygad** module. - -Supported Layers -================ - -Each layer supported by the **pygad.cnn** module has a corresponding -class. The layers and their classes are: - -1. **Input**: Implemented using the ``pygad.cnn.Input2D`` class. - -2. **Convolution**: Implemented using the ``pygad.cnn.Conv2D`` class. - -3. **Max Pooling**: Implemented using the ``pygad.cnn.MaxPooling2D`` - class. - -4. **Average Pooling**: Implemented using the - ``pygad.cnn.AveragePooling2D`` class. - -5. **Flatten**: Implemented using the ``pygad.cnn.Flatten`` class. - -6. **ReLU**: Implemented using the ``pygad.cnn.ReLU`` class. - -7. **Sigmoid**: Implemented using the ``pygad.cnn.Sigmoid`` class. - -8. **Dense** (Fully Connected): Implemented using the - ``pygad.cnn.Dense`` class. - -In the future, more layers will be added. - -Except for the input layer, all of listed layers has 4 instance -attributes that do the same function which are: - -1. ``previous_layer``: A reference to the previous layer in the CNN - architecture. - -2. ``layer_input_size``: The size of the input to the layer. - -3. ``layer_output_size``: The size of the output from the layer. - -4. ``layer_output``: The latest output generated from the layer. It - default to ``None``. - -In addition to such attributes, the layers may have some additional -attributes. The next subsections discuss such layers. - -.. _pygadcnninput2d-class: - -``pygad.cnn.Input2D`` Class ---------------------------- - -The ``pygad.cnn.Input2D`` class creates the input layer for the -convolutional neural network. For each network, there is only a single -input layer. The network architecture must start with an input layer. - -This class has no methods or class attributes. All it has is a -constructor that accepts a parameter named ``input_shape`` representing -the shape of the input. - -The instances from the ``Input2D`` class has the following attributes: - -1. ``input_shape``: The shape of the input to the pygad.cnn. - -2. ``layer_output_size`` - -Here is an example of building an input layer with shape -``(50, 50, 3)``. - -.. code:: python - - input_layer = pygad.cnn.Input2D(input_shape=(50, 50, 3)) - -Here is how to access the attributes within the instance of the -``pygad.cnn.Input2D`` class. - -.. code:: python - - input_shape = input_layer.input_shape - layer_output_size = input_layer.layer_output_size - - print("Input2D Input shape =", input_shape) - print("Input2D Output shape =", layer_output_size) - -This is everything about the input layer. - -.. _pygadcnnconv2d-class: - -``pygad.cnn.Conv2D`` Class --------------------------- - -Using the ``pygad.cnn.Conv2D`` class, convolution (conv) layers can be -created. To create a convolution layer, just create a new instance of -the class. The constructor accepts the following parameters: - -- ``num_filters``: Number of filters. - -- ``kernel_size``: Filter kernel size. - -- ``previous_layer``: A reference to the previous layer. Using the - ``previous_layer`` attribute, a linked list is created that connects - all network layers. For more information about this attribute, please - check the **previous_layer** attribute section of the ``pygad.nn`` - module documentation. - -- ``activation_function=None``: A string representing the activation - function to be used in this layer. Defaults to ``None`` which means no - activation function is applied while applying the convolution layer. - An activation layer can be added separately in this case. The - supported activation functions in the conv layer are ``relu`` and - ``sigmoid``. - -Within the constructor, the accepted parameters are used as instance -attributes. Besides the parameters, some new instance attributes are -created which are: - -- ``filter_bank_size``: Size of the filter bank in this layer. - -- ``initial_weights``: The initial weights for the conv layer. - -- ``trained_weights``: The trained weights of the conv layer. This - attribute is initialized by the value in the ``initial_weights`` - attribute. - -- ``layer_input_size`` - -- ``layer_output_size`` - -- ``layer_output`` - -Here is an example for creating a conv layer with 2 filters and a kernel -size of 3. Note that the ``previous_layer`` parameter is assigned to the -input layer ``input_layer``. - -.. code:: python - - conv_layer = pygad.cnn.Conv2D(num_filters=2, - kernel_size=3, - previous_layer=input_layer, - activation_function=None) - -Here is how to access some attributes in the dense layer: - -.. code:: python - - filter_bank_size = conv_layer.filter_bank_size - conv_initail_weights = conv_layer.initial_weights - - print("Filter bank size attributes =", filter_bank_size) - print("Initial weights of the conv layer :", conv_initail_weights) - -Because ``conv_layer`` holds a reference to the input layer, then the -number of input neurons can be accessed. - -.. code:: python - - input_layer = conv_layer.previous_layer - input_shape = input_layer.num_neurons - - print("Input shape =", input_shape) - -Here is another conv layer where its ``previous_layer`` attribute points -to the previously created conv layer and it uses the ``ReLU`` activation -function. - -.. code:: python - - conv_layer2 = pygad.cnn.Conv2D(num_filters=2, - kernel_size=3, - previous_layer=conv_layer, - activation_function="relu") - -Because ``conv_layer2`` holds a reference to ``conv_layer`` in its -``previous_layer`` attribute, then the attributes in ``conv_layer`` can -be accessed. - -.. code:: python - - conv_layer = conv_layer2.previous_layer - filter_bank_size = conv_layer.filter_bank_size - - print("Filter bank size attributes =", filter_bank_size) - -After getting the reference to ``conv_layer``, we can use it to access -the number of input neurons. - -.. code:: python - - conv_layer = conv_layer2.previous_layer - input_layer = conv_layer.previous_layer - input_shape = input_layer.num_neurons - - print("Input shape =", input_shape) - -.. _pygadcnnmaxpooling2d-class: - -``pygad.cnn.MaxPooling2D`` Class --------------------------------- - -The ``pygad.cnn.MaxPooling2D`` class builds a max pooling layer for the -CNN architecture. The constructor of this class accepts the following -parameter: - -- ``pool_size``: Size of the window. - -- ``previous_layer``: A reference to the previous layer in the CNN - architecture. - -- ``stride=2``: A stride that default to 2. - -Within the constructor, the accepted parameters are used as instance -attributes. Besides the parameters, some new instance attributes are -created which are: - -- ``layer_input_size`` - -- ``layer_output_size`` - -- ``layer_output`` - -.. _pygadcnnaveragepooling2d-class: - -``pygad.cnn.AveragePooling2D`` Class ------------------------------------- - -The ``pygad.cnn.AveragePooling2D`` class is similar to the -``pygad.cnn.MaxPooling2D`` class except that it applies the max pooling -operation rather than average pooling. - -.. _pygadcnnflatten-class: - -``pygad.cnn.Flatten`` Class ---------------------------- - -The ``pygad.cnn.Flatten`` class implements the flatten layer which -converts the output of the previous layer into a 1D vector. The -constructor accepts only the ``previous_layer`` parameter. - -The following instance attributes exist: - -- ``previous_layer`` - -- ``layer_input_size`` - -- ``layer_output_size`` - -- ``layer_output`` - -.. _pygadcnnrelu-class: - -``pygad.cnn.ReLU`` Class ------------------------- - -The ``pygad.cnn.ReLU`` class implements the ReLU layer which applies the -ReLU activation function to the output of the previous layer. - -The constructor accepts only the ``previous_layer`` parameter. - -The following instance attributes exist: - -- ``previous_layer`` - -- ``layer_input_size`` - -- ``layer_output_size`` - -- ``layer_output`` - -.. _pygadcnnsigmoid-class: - -``pygad.cnn.Sigmoid`` Class ---------------------------- - -The ``pygad.cnn.Sigmoid`` class is similar to the ``pygad.cnn.ReLU`` -class except that it applies the sigmoid function rather than the ReLU -function. - -.. _pygadcnndense-class: - -``pygad.cnn.Dense`` Class -------------------------- - -The ``pygad.cnn.Dense`` class implement the dense layer. Its constructor -accepts the following parameters: - -- ``num_neurons``: Number of neurons in the dense layer. - -- ``previous_layer``: A reference to the previous layer. - -- ``activation_function``: A string representing the activation function - to be used in this layer. Defaults to ``"sigmoid"``. Currently, the - supported activation functions in the dense layer are ``"sigmoid"``, - ``"relu"``, and ``softmax``. - -Within the constructor, the accepted parameters are used as instance -attributes. Besides the parameters, some new instance attributes are -created which are: - -- ``initial_weights``: The initial weights for the dense layer. - -- ``trained_weights``: The trained weights of the dense layer. This - attribute is initialized by the value in the ``initial_weights`` - attribute. - -- ``layer_input_size`` - -- ``layer_output_size`` - -- ``layer_output`` - -.. _pygadcnnmodel-class: - -``pygad.cnn.Model`` Class -========================= - -An instance of the ``pygad.cnn.Model`` class represents a CNN model. The -constructor of this class accepts the following parameters: - -- ``last_layer``: A reference to the last layer in the CNN architecture - (i.e. dense layer). - -- ``epochs=10``: Number of epochs. - -- ``learning_rate=0.01``: Learning rate. - -Within the constructor, the accepted parameters are used as instance -attributes. Besides the parameters, a new instance attribute named -``network_layers`` is created which holds a list with references to the -CNN layers. Such a list is returned using the ``get_layers()`` method in -the ``pygad.cnn.Model`` class. - -There are a number of methods in the ``pygad.cnn.Model`` class which -serves in training, testing, and retrieving information about the model. -These methods are discussed in the next subsections. - -.. _getlayers: - -``get_layers()`` ----------------- - -Creates a list of all layers in the CNN model. It accepts no parameters. - -``train()`` ------------ - -Trains the CNN model. - -Accepts the following parameters: - -- ``train_inputs``: Training data inputs. - -- ``train_outputs``: Training data outputs. - -This method trains the CNN model according to the number of epochs -specified in the constructor of the ``pygad.cnn.Model`` class. - -It is important to note that no learning algorithm is used for training -the pygad.cnn. Just the learning rate is used for making some changes -which is better than leaving the weights unchanged. - -.. _feedsample: - -``feed_sample()`` ------------------ - -Feeds a sample in the CNN layers and returns results of the last layer -in the pygad.cnn. - -.. _updateweights: - -``update_weights()`` --------------------- - -Updates the CNN weights using the learning rate. It is important to note -that no learning algorithm is used for training the pygad.cnn. Just the -learning rate is used for making some changes which is better than -leaving the weights unchanged. - -``predict()`` -------------- - -Uses the trained CNN for making predictions. - -Accepts the following parameter: - -- ``data_inputs``: The inputs to predict their label. - -It returns a list holding the samples predictions. - -``summary()`` -------------- - -Prints a summary of the CNN architecture. - -Supported Activation Functions -============================== - -The supported activation functions in the convolution layer are: - -1. Sigmoid: Implemented using the ``pygad.cnn.sigmoid()`` function. - -2. Rectified Linear Unit (ReLU): Implemented using the - ``pygad.cnn.relu()`` function. - -The dense layer supports these functions besides the ``softmax`` -function implemented in the ``pygad.cnn.softmax()`` function. - -Steps to Build a Neural Network -=============================== - -This section discusses how to use the ``pygad.cnn`` module for building -a neural network. The summary of the steps are as follows: - -- Reading the Data - -- Building the CNN Architecture - -- Building Model - -- Model Summary - -- Training the CNN - -- Making Predictions - -- Calculating Some Statistics - -Reading the Data ----------------- - -Before building the network architecture, the first thing to do is to -prepare the data that will be used for training the network. - -In this example, 4 classes of the **Fruits360** dataset are used for -preparing the training data. The 4 classes are: - -1. `Apple - Braeburn `__: - This class's data is available at - https://github.com/ahmedfgad/NumPyANN/tree/master/apple - -2. `Lemon - Meyer `__: - This class's data is available at - https://github.com/ahmedfgad/NumPyANN/tree/master/lemon - -3. `Mango `__: - This class's data is available at - https://github.com/ahmedfgad/NumPyANN/tree/master/mango - -4. `Raspberry `__: - This class's data is available at - https://github.com/ahmedfgad/NumPyANN/tree/master/raspberry - -Just 20 samples from each of the 4 classes are saved into a NumPy array -available in the -`dataset_inputs.npy `__ -file: -https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_inputs.npy - -The shape of this array is ``(80, 100, 100, 3)`` where the shape of the -single image is ``(100, 100, 3)``. - -The -`dataset_outputs.npy `__ -file -(https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_outputs.npy) -has the class labels for the 4 classes: - -1. `Apple - Braeburn `__: - Class label is **0** - -2. `Lemon - Meyer `__: - Class label is **1** - -3. `Mango `__: - Class label is **2** - -4. `Raspberry `__: - Class label is **3** - -Simply, download and reach the 2 files to return the NumPy arrays -according to the next 2 lines: - -.. code:: python - - train_inputs = numpy.load("dataset_inputs.npy") - train_outputs = numpy.load("dataset_outputs.npy") - -After the data is prepared, next is to create the network architecture. - -Building the Network Architecture ---------------------------------- - -The input layer is created by instantiating the ``pygad.cnn.Input2D`` -class according to the next code. A network can only have a single input -layer. - -.. code:: python - - import pygad.cnn - sample_shape = train_inputs.shape[1:] - - input_layer = pygad.cnn.Input2D(input_shape=sample_shape) - -After the input layer is created, next is to create a number of layers -layers according to the next code. Normally, the last dense layer is -regarded as the output layer. Note that the output layer has a number of -neurons equal to the number of classes in the dataset which is 4. - -.. code:: python - - conv_layer1 = pygad.cnn.Conv2D(num_filters=2, - kernel_size=3, - previous_layer=input_layer, - activation_function=None) - relu_layer1 = pygad.cnn.Sigmoid(previous_layer=conv_layer1) - average_pooling_layer = pygad.cnn.AveragePooling2D(pool_size=2, - previous_layer=relu_layer1, - stride=2) - - conv_layer2 = pygad.cnn.Conv2D(num_filters=3, - kernel_size=3, - previous_layer=average_pooling_layer, - activation_function=None) - relu_layer2 = pygad.cnn.ReLU(previous_layer=conv_layer2) - max_pooling_layer = pygad.cnn.MaxPooling2D(pool_size=2, - previous_layer=relu_layer2, - stride=2) - - conv_layer3 = pygad.cnn.Conv2D(num_filters=1, - kernel_size=3, - previous_layer=max_pooling_layer, - activation_function=None) - relu_layer3 = pygad.cnn.ReLU(previous_layer=conv_layer3) - pooling_layer = pygad.cnn.AveragePooling2D(pool_size=2, - previous_layer=relu_layer3, - stride=2) - - flatten_layer = pygad.cnn.Flatten(previous_layer=pooling_layer) - dense_layer1 = pygad.cnn.Dense(num_neurons=100, - previous_layer=flatten_layer, - activation_function="relu") - dense_layer2 = pygad.cnn.Dense(num_neurons=4, - previous_layer=dense_layer1, - activation_function="softmax") - -After the network architecture is prepared, the next step is to create a -CNN model. - -Building Model --------------- - -The CNN model is created as an instance of the ``pygad.cnn.Model`` -class. Here is an example. - -.. code:: python - - model = pygad.cnn.Model(last_layer=dense_layer2, - epochs=5, - learning_rate=0.01) - -After the model is created, a summary of the model architecture can be -printed. - -Model Summary -------------- - -The ``summary()`` method in the ``pygad.cnn.Model`` class prints a -summary of the CNN model. - -.. code:: python - - model.summary() - -.. code:: python - - ----------Network Architecture---------- - - - - - - - - - - - - - ---------------------------------------- - -Training the Network --------------------- - -After the model and the data are prepared, then the model can be trained -using the the ``pygad.cnn.train()`` method. - -.. code:: python - - model.train(train_inputs=train_inputs, - train_outputs=train_outputs) - -After training the network, the next step is to make predictions. - -Making Predictions ------------------- - -The ``pygad.cnn.predict()`` method uses the trained network for making -predictions. Here is an example. - -.. code:: python - - predictions = model.predict(data_inputs=train_inputs) - -It is not expected to have high accuracy in the predictions because no -training algorithm is used. - -Calculating Some Statistics ---------------------------- - -Based on the predictions the network made, some statistics can be -calculated such as the number of correct and wrong predictions in -addition to the classification accuracy. - -.. code:: python - - num_wrong = numpy.where(predictions != train_outputs)[0] - num_correct = train_outputs.size - num_wrong.size - accuracy = 100 * (num_correct/train_outputs.size) - print(f"Number of correct classifications : {num_correct}.") - print(f"Number of wrong classifications : {num_wrong.size}.") - print(f"Classification accuracy : {accuracy}.") - -It is very important to note that it is not expected that the -classification accuracy is high because no training algorithm is used. -Please check the documentation of the ``pygad.gacnn`` module for -training the CNN using the genetic algorithm. - -Examples -======== - -This section gives the complete code of some examples that build neural -networks using ``pygad.cnn``. Each subsection builds a different -network. - -Image Classification --------------------- - -This example is discussed in the **Steps to Build a Convolutional Neural -Network** section and its complete code is listed below. - -Remember to either download or create the -`dataset_features.npy `__ -and -`dataset_outputs.npy `__ -files before running this code. - -.. code:: python - - import numpy - import pygad.cnn - - """ - Convolutional neural network implementation using NumPy - A tutorial that helps to get started (Building Convolutional Neural Network using NumPy from Scratch) available in these links: - https://www.linkedin.com/pulse/building-convolutional-neural-network-using-numpy-from-ahmed-gad - https://towardsdatascience.com/building-convolutional-neural-network-using-numpy-from-scratch-b30aac50e50a - https://www.kdnuggets.com/2018/04/building-convolutional-neural-network-numpy-scratch.html - It is also translated into Chinese: http://m.aliyun.com/yunqi/articles/585741 - """ - - train_inputs = numpy.load("dataset_inputs.npy") - train_outputs = numpy.load("dataset_outputs.npy") - - sample_shape = train_inputs.shape[1:] - num_classes = 4 - - input_layer = pygad.cnn.Input2D(input_shape=sample_shape) - conv_layer1 = pygad.cnn.Conv2D(num_filters=2, - kernel_size=3, - previous_layer=input_layer, - activation_function=None) - relu_layer1 = pygad.cnn.Sigmoid(previous_layer=conv_layer1) - average_pooling_layer = pygad.cnn.AveragePooling2D(pool_size=2, - previous_layer=relu_layer1, - stride=2) - - conv_layer2 = pygad.cnn.Conv2D(num_filters=3, - kernel_size=3, - previous_layer=average_pooling_layer, - activation_function=None) - relu_layer2 = pygad.cnn.ReLU(previous_layer=conv_layer2) - max_pooling_layer = pygad.cnn.MaxPooling2D(pool_size=2, - previous_layer=relu_layer2, - stride=2) - - conv_layer3 = pygad.cnn.Conv2D(num_filters=1, - kernel_size=3, - previous_layer=max_pooling_layer, - activation_function=None) - relu_layer3 = pygad.cnn.ReLU(previous_layer=conv_layer3) - pooling_layer = pygad.cnn.AveragePooling2D(pool_size=2, - previous_layer=relu_layer3, - stride=2) - - flatten_layer = pygad.cnn.Flatten(previous_layer=pooling_layer) - dense_layer1 = pygad.cnn.Dense(num_neurons=100, - previous_layer=flatten_layer, - activation_function="relu") - dense_layer2 = pygad.cnn.Dense(num_neurons=num_classes, - previous_layer=dense_layer1, - activation_function="softmax") - - model = pygad.cnn.Model(last_layer=dense_layer2, - epochs=1, - learning_rate=0.01) - - model.summary() - - model.train(train_inputs=train_inputs, - train_outputs=train_outputs) - - predictions = model.predict(data_inputs=train_inputs) - print(predictions) - - num_wrong = numpy.where(predictions != train_outputs)[0] - num_correct = train_outputs.size - num_wrong.size - accuracy = 100 * (num_correct/train_outputs.size) - print(f"Number of correct classifications : {num_correct}.") - print(f"Number of wrong classifications : {num_wrong.size}.") - print(f"Classification accuracy : {accuracy}.") diff --git a/docs/source/conf.py b/docs/source/conf.py index 1dabc36..4cc2296 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,67 +1,86 @@ -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# http://www.sphinx-doc.org/en/master/config - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - - -# -- Project information ----------------------------------------------------- - -project = 'PyGAD' -copyright = '2026, Ahmed Fawzy Gad' -author = 'Ahmed Fawzy Gad' - -# The full version, including alpha/beta/rc tags -release = '3.6.0' - -master_doc = 'index' - -latex_engine = 'xelatex' -latex_elements = { - 'inputenc': '', - 'utf8extra': '', - 'preamble': ''' -\\usepackage{kotex} -\\usepackage{fontspec} -\setsansfont{Arial} -\setromanfont{Arial} -''', -} - -# -- General configuration --------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [] # Add 'sphinx.ext.autodoc' to enabe creeate modindex and enable automodule - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = [] - - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'alabaster' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +# Configuration file for the Sphinx documentation builder. +# +# For the full list of options, see: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- + +project = 'PyGAD' +copyright = '2026, Ahmed Fawzy Gad' +author = 'Ahmed Fawzy Gad' + +# The full version, including alpha/beta/rc tags. +release = '3.6.0' + +master_doc = 'index' + +# -- General configuration --------------------------------------------------- + +# The documentation is written in Markdown and read directly by Sphinx +# through the MyST parser. There is no Markdown-to-reStructuredText step. +extensions = [ + 'myst_parser', + 'sphinx_design', + 'sphinx_copybutton', +] + +# Read both Markdown and reStructuredText. Markdown is the source of truth. +# The .rst mapping lets pages that are not migrated yet keep building. +source_suffix = { + '.md': 'markdown', + '.rst': 'restructuredtext', +} + +# _templates is not used. +templates_path = [] + +# Files and directories to skip when looking for source files. +exclude_patterns = ['build', 'Thumbs.db', '.DS_Store'] + +# -- MyST configuration ------------------------------------------------------ + +myst_enable_extensions = [ + 'colon_fence', + 'deflist', + 'linkify', + 'substitution', + 'tasklist', + 'dollarmath', +] + +# Do NOT set myst_heading_anchors. Leaving it unset keeps Sphinx using the +# docutils section IDs (for example "PyGAD 2.18.0" -> "pygad-2-18-0"), which +# are the anchors the live site already links to. Turning it on would switch +# to GitHub-style slugs and break those links. + +# -- Options for HTML output ------------------------------------------------- + +html_theme = 'furo' +html_title = 'PyGAD' +html_static_path = ['_static'] +html_css_files = ['custom.css'] + +html_theme_options = { + 'light_css_variables': { + 'color-brand-primary': '#0b6e4f', + 'color-brand-content': '#0b6e4f', + }, + 'dark_css_variables': { + 'color-brand-primary': '#27ae60', + 'color-brand-content': '#27ae60', + }, +} + +# -- Options for LaTeX / PDF output (xelatex) -------------------------------- + +latex_engine = 'xelatex' +latex_elements = { + 'inputenc': '', + 'utf8extra': '', + 'preamble': r''' +\usepackage{kotex} +\usepackage{fontspec} +\setsansfont{Arial} +\setromanfont{Arial} +''', +} diff --git a/docs/md/gacnn.md b/docs/source/gacnn.md similarity index 95% rename from docs/md/gacnn.md rename to docs/source/gacnn.md index 132a625..bf98b32 100644 --- a/docs/md/gacnn.md +++ b/docs/source/gacnn.md @@ -4,11 +4,11 @@ This section of the PyGAD's library documentation discusses the **pygad.gacnn** The `pygad.gacnn` module trains convolutional neural networks using the genetic algorithm. It makes use of the 2 modules `pygad` and `pygad.cnn`. -# `pygad.gacnn.GACNN` Class +## `pygad.gacnn.GACNN` Class The `pygad.gacnn` module has a class named `pygad.gacnn.GACNN` for training convolutional neural networks (CNNs) using the genetic algorithm. The constructor, methods, function, and attributes within the class are discussed in this section. -## `__init__()` +### `__init__()` In order to train a CNN using the genetic algorithm, the first thing to do is to create an instance of the `pygad.gacnn.GACNN` class. @@ -17,23 +17,23 @@ The `pygad.gacnn.GACNN` class constructor accepts the following parameters: - `model`: model: An instance of the pygad.cnn.Model class representing the architecture of all solutions in the population. - `num_solutions`: Number of CNNs (i.e. solutions) in the population. Based on the value passed to this parameter, a number of identical CNNs are created where their parameters are optimized using the genetic algorithm. -## Instance Attributes +### Instance Attributes All the parameters in the `pygad.gacnn.GACNN` class constructor are used as instance attributes. Besides such attributes, there is an extra attribute added to the instances from the `pygad.gacnn.GACNN` class which is: - `population_networks`: A list holding references to all the solutions (i.e. CNNs) used in the population. -## Methods in the GACNN Class +### Methods in the GACNN Class This section discusses the methods available for instances of the `pygad.gacnn.GACNN` class. -### `create_population()` +#### `create_population()` The `create_population()` method creates the initial population of the genetic algorithm as a list of CNNs (i.e. solutions). All the networks are copied from the CNN model passed to constructor of the GACNN class. The list of networks is assigned to the `population_networks` attribute of the instance. -### `update_population_trained_weights()` +#### `update_population_trained_weights()` The `update_population_trained_weights()` method updates the `trained_weights` attribute of the layers of each network (check the documentation of the `pygad.cnn` module) for more information) according to the weights passed in the `population_trained_weights` parameter. @@ -41,11 +41,11 @@ Accepts the following parameters: - `population_trained_weights`: A list holding the trained weights of all networks as matrices. Such matrices are to be assigned to the `trained_weights` attribute of all layers of all networks. -# Functions in the `pygad.gacnn` Module +## Functions in the `pygad.gacnn` Module This section discusses the functions in the `pygad.gacnn` module. -## `pygad.gacnn.population_as_vectors()` +### `pygad.gacnn.population_as_vectors()` Accepts the population as a list of references to the `pygad.cnn.Model` class and returns a list holding all weights of the layers of each solution (i.e. network) in the population as a vector. @@ -57,7 +57,7 @@ Accepts the following parameters: Returns a list holding the weights vectors for all solutions (i.e. networks). -## `pygad.gacnn.population_as_matrices()` +### `pygad.gacnn.population_as_matrices()` Accepts the population as both networks and weights vectors and returns the weights of all layers of each solution (i.e. network) in the population as a matrix. @@ -70,7 +70,7 @@ Accepts the following parameters: Returns a list holding the weights matrices for all solutions (i.e. networks). -# Steps to Build and Train CNN using Genetic Algorithm +## Steps to Build and Train CNN using Genetic Algorithm The steps to use this project for building and training a neural network using the genetic algorithm are as follows: @@ -88,7 +88,7 @@ The steps to use this project for building and training a neural network using t Let's start covering all of these steps. -## Prepare the Training Data +### Prepare the Training Data Before building and training neural networks, the training data (input and output) is to be prepared. The inputs and the outputs of the training data are NumPy arrays. @@ -112,7 +112,7 @@ For the output array, each element must be a single number representing the clas Note that the project only supports that each sample is assigned to only one class. -## Building the Network Architecture +### Building the Network Architecture Here is an example for a CNN architecture. @@ -136,7 +136,7 @@ dense_layer = pygad.cnn.Dense(num_neurons=4, After the network architecture is prepared, the next step is to create a CNN model. -## Building Model +### Building Model The CNN model is created as an instance of the `pygad.cnn.Model` class. Here is an example. @@ -148,7 +148,7 @@ model = pygad.cnn.Model(last_layer=dense_layer, After the model is created, a summary of the model architecture can be printed. -## Model Summary +### Model Summary The `summary()` method in the `pygad.cnn.Model` class prints a summary of the CNN model. @@ -167,7 +167,7 @@ model.summary() The next step is to create an instance of the `pygad.gacnn.GACNN` class. -## Create an Instance of the `pygad.gacnn.GACNN` Class +### Create an Instance of the `pygad.gacnn.GACNN` Class After preparing the input data and building the CNN model, an instance of the `pygad.gacnn.GACNN` class is created by passing the appropriate parameters. @@ -182,7 +182,7 @@ GACNN_instance = pygad.gacnn.GACNN(model=model, After creating the instance of the `pygad.gacnn.GACNN` class, next is to fetch the weights of the population as a list of vectors. -## Fetch the Population Weights as Vectors +### Fetch the Population Weights as Vectors For the genetic algorithm, the parameters (i.e. genes) of each solution are represented as a single vector. @@ -205,7 +205,7 @@ After preparing the population weights as a set of vectors, next is to prepare 2 1. Fitness function. 2. Callback function after each generation. -## Prepare the Fitness Function +### Prepare the Fitness Function The PyGAD library works by allowing the users to customize the genetic algorithm for their own problems. Because the problems differ in how the fitness values are calculated, then PyGAD allows the user to use a custom function as a maximization fitness function. This function must accept 2 positional parameters representing the following: @@ -231,7 +231,7 @@ def fitness_func(ga_instance, solution, sol_idx): return solution_fitness ``` -## Prepare the Generation Callback Function +### Prepare the Generation Callback Function After each generation of the genetic algorithm, the fitness function will be called to calculate the fitness value of each solution. Within the fitness function, the `pygad.cnn.predict()` function is used for predicting the outputs based on the current solution's `trained_weights` attribute. Thus, it is required that such an attribute is updated by weights evolved by the genetic algorithm after each generation. @@ -257,7 +257,7 @@ def callback_generation(ga_instance): After preparing the fitness and callback function, next is to create an instance of the `pygad.GA` class. -## Create an Instance of the `pygad.GA` Class +### Create an Instance of the `pygad.GA` Class Once the parameters of the genetic algorithm are prepared, an instance of the `pygad.GA` class can be created. Here is an example where the number of generations is 10. @@ -280,7 +280,7 @@ ga_instance = pygad.GA(num_generations=num_generations, The last step for training the neural networks using the genetic algorithm is calling the `run()` method. -## Run the Created Instance of the `pygad.GA` Class +### Run the Created Instance of the `pygad.GA` Class By calling the `run()` method from the `pygad.GA` instance, the genetic algorithm will iterate through the number of generations specified in its `num_generations` parameter. @@ -288,7 +288,7 @@ By calling the `run()` method from the `pygad.GA` instance, the genetic algorith ga_instance.run() ``` -## Plot the Fitness Values +### Plot the Fitness Values After the `run()` method completes, the `plot_fitness()` method can be called to show how the fitness values evolve by generation. @@ -298,7 +298,7 @@ ga_instance.plot_fitness() ![GACNN_Fitness](https://user-images.githubusercontent.com/16560492/83429675-ab744580-a434-11ea-8f21-9d3804b50d15.png) -## Information about the Best Solution +### Information about the Best Solution The following information about the best solution in the last population is returned using the `best_solution()` method in the `pygad.GA` class. @@ -322,7 +322,7 @@ Index of the best solution : 0 Best fitness value reached after 4 generations. ``` -## Making Predictions using the Trained Weights +### Making Predictions using the Trained Weights The `pygad.cnn.predict()` function can be used to make predictions using the trained network. As printed, the network is able to predict the labels correctly. @@ -331,7 +331,7 @@ predictions = pygad.cnn.predict(last_layer=GANN_instance.population_networks[sol print(f"Predictions of the trained network : {predictions}") ``` -## Calculating Some Statistics +### Calculating Some Statistics Based on the predictions the network made, some statistics can be calculated such as the number of correct and wrong predictions in addition to the classification accuracy. @@ -350,11 +350,11 @@ Number of wrong classifications : 13. Classification accuracy : 83.75. ``` -# Examples +## Examples This section gives the complete code of some examples that build and train neural networks using the genetic algorithm. Each subsection builds a different network. -## Image Classification +### Image Classification This example is discussed in the **Steps to Build and Train CNN using Genetic Algorithm** section that builds the an image classifier. Its complete code is listed below. diff --git a/docs/source/gacnn.rst b/docs/source/gacnn.rst deleted file mode 100644 index e9f89ba..0000000 --- a/docs/source/gacnn.rst +++ /dev/null @@ -1,662 +0,0 @@ -.. _pygadgacnn-module: - -``pygad.gacnn`` Module -====================== - -This section of the PyGAD's library documentation discusses the -**pygad.gacnn** module. - -The ``pygad.gacnn`` module trains convolutional neural networks using -the genetic algorithm. It makes use of the 2 modules ``pygad`` and -``pygad.cnn``. - -.. _pygadgacnngacnn-class: - -``pygad.gacnn.GACNN`` Class -=========================== - -The ``pygad.gacnn`` module has a class named ``pygad.gacnn.GACNN`` for -training convolutional neural networks (CNNs) using the genetic -algorithm. The constructor, methods, function, and attributes within the -class are discussed in this section. - -.. _init: - -``__init__()`` --------------- - -In order to train a CNN using the genetic algorithm, the first thing to -do is to create an instance of the ``pygad.gacnn.GACNN`` class. - -The ``pygad.gacnn.GACNN`` class constructor accepts the following -parameters: - -- ``model``: model: An instance of the pygad.cnn.Model class - representing the architecture of all solutions in the population. - -- ``num_solutions``: Number of CNNs (i.e. solutions) in the population. - Based on the value passed to this parameter, a number of identical - CNNs are created where their parameters are optimized using the - genetic algorithm. - -Instance Attributes -------------------- - -All the parameters in the ``pygad.gacnn.GACNN`` class constructor are -used as instance attributes. Besides such attributes, there is an extra -attribute added to the instances from the ``pygad.gacnn.GACNN`` class -which is: - -- ``population_networks``: A list holding references to all the - solutions (i.e. CNNs) used in the population. - -Methods in the GACNN Class --------------------------- - -This section discusses the methods available for instances of the -``pygad.gacnn.GACNN`` class. - -.. _createpopulation: - -``create_population()`` -~~~~~~~~~~~~~~~~~~~~~~~ - -The ``create_population()`` method creates the initial population of the -genetic algorithm as a list of CNNs (i.e. solutions). All the networks -are copied from the CNN model passed to constructor of the GACNN class. - -The list of networks is assigned to the ``population_networks`` -attribute of the instance. - -.. _updatepopulationtrainedweights: - -``update_population_trained_weights()`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The ``update_population_trained_weights()`` method updates the -``trained_weights`` attribute of the layers of each network (check the -documentation of the ``pygad.cnn`` module) for more information) -according to the weights passed in the ``population_trained_weights`` -parameter. - -Accepts the following parameters: - -- ``population_trained_weights``: A list holding the trained weights of - all networks as matrices. Such matrices are to be assigned to the - ``trained_weights`` attribute of all layers of all networks. - -.. _functions-in-the-pygadgacnn-module: - -Functions in the ``pygad.gacnn`` Module -======================================= - -This section discusses the functions in the ``pygad.gacnn`` module. - -.. _pygadgacnnpopulationasvectors: - -``pygad.gacnn.population_as_vectors()`` ----------------------------------------- - -Accepts the population as a list of references to the -``pygad.cnn.Model`` class and returns a list holding all weights of the -layers of each solution (i.e. network) in the population as a vector. - -For example, if the population has 6 solutions (i.e. networks), this -function accepts references to such networks and returns a list with 6 -vectors, one for each network (i.e. solution). Each vector holds the -weights for all layers for a single network. - -Accepts the following parameters: - -- ``population_networks``: A list holding references to the - ``pygad.cnn.Model`` class of the networks used in the population. - -Returns a list holding the weights vectors for all solutions (i.e. -networks). - -.. _pygadgacnnpopulationasmatrices: - -``pygad.gacnn.population_as_matrices()`` ----------------------------------------- - -Accepts the population as both networks and weights vectors and returns -the weights of all layers of each solution (i.e. network) in the -population as a matrix. - -For example, if the population has 6 solutions (i.e. networks), this -function returns a list with 6 matrices, one for each network holding -its weights for all layers. - -Accepts the following parameters: - -- ``population_networks``: A list holding references to the - ``pygad.cnn.Model`` class of the networks used in the population. - -- ``population_vectors``: A list holding the weights of all networks as - vectors. Such vectors are to be converted into matrices. - -Returns a list holding the weights matrices for all solutions (i.e. -networks). - -Steps to Build and Train CNN using Genetic Algorithm -==================================================== - -The steps to use this project for building and training a neural network -using the genetic algorithm are as follows: - -- Prepare the training data. - -- Create an instance of the ``pygad.gacnn.GACNN`` class. - -- Fetch the population weights as vectors. - -- Prepare the fitness function. - -- Prepare the generation callback function. - -- Create an instance of the ``pygad.GA`` class. - -- Run the created instance of the ``pygad.GA`` class. - -- Plot the Fitness Values - -- Information about the best solution. - -- Making predictions using the trained weights. - -- Calculating some statistics. - -Let's start covering all of these steps. - -Prepare the Training Data -------------------------- - -Before building and training neural networks, the training data (input -and output) is to be prepared. The inputs and the outputs of the -training data are NumPy arrays. - -The data used in this example is available as 2 files: - -1. `dataset_inputs.npy `__: - Data inputs. - https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_inputs.npy - -2. `dataset_outputs.npy `__: - Class labels. - https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_outputs.npy - -The data consists of 4 classes of images. The image shape is -``(100, 100, 3)`` and there are 20 images per class. For more -information about the dataset, check the **Reading the Data** section of -the ``pygad.cnn`` module. - -Simply download these 2 files and read them according to the next code. - -.. code:: python - - import numpy - - train_inputs = numpy.load("dataset_inputs.npy") - train_outputs = numpy.load("dataset_outputs.npy") - -For the output array, each element must be a single number representing -the class label of the sample. The class labels must start at ``0``. So, -if there are 80 samples, then the shape of the output array is ``(80)``. -If there are 5 classes in the data, then the values of all the 200 -elements in the output array must range from 0 to 4 inclusive. -Generally, the class labels start from ``0`` to ``N-1`` where ``N`` is -the number of classes. - -Note that the project only supports that each sample is assigned to only -one class. - -Building the Network Architecture ---------------------------------- - -Here is an example for a CNN architecture. - -.. code:: python - - import pygad.cnn - - input_layer = pygad.cnn.Input2D(input_shape=(80, 80, 3)) - conv_layer = pygad.cnn.Conv2D(num_filters=2, - kernel_size=3, - previous_layer=input_layer, - activation_function="relu") - average_pooling_layer = pygad.cnn.AveragePooling2D(pool_size=5, - previous_layer=conv_layer, - stride=3) - - flatten_layer = pygad.cnn.Flatten(previous_layer=average_pooling_layer) - dense_layer = pygad.cnn.Dense(num_neurons=4, - previous_layer=flatten_layer, - activation_function="softmax") - -After the network architecture is prepared, the next step is to create a -CNN model. - -Building Model --------------- - -The CNN model is created as an instance of the ``pygad.cnn.Model`` -class. Here is an example. - -.. code:: python - - model = pygad.cnn.Model(last_layer=dense_layer, - epochs=5, - learning_rate=0.01) - -After the model is created, a summary of the model architecture can be -printed. - -Model Summary -------------- - -The ``summary()`` method in the ``pygad.cnn.Model`` class prints a -summary of the CNN model. - -.. code:: python - - model.summary() - -.. code:: python - - ----------Network Architecture---------- - - - - - ---------------------------------------- - -The next step is to create an instance of the ``pygad.gacnn.GACNN`` -class. - -.. _create-an-instance-of-the-pygadgacnngacnn-class: - -Create an Instance of the ``pygad.gacnn.GACNN`` Class ------------------------------------------------------ - -After preparing the input data and building the CNN model, an instance -of the ``pygad.gacnn.GACNN`` class is created by passing the appropriate -parameters. - -Here is an example where the ``num_solutions`` parameter is set to 4 -which means the genetic algorithm population will have 6 solutions (i.e. -networks). All of these 6 CNNs will have the same architectures as -specified by the ``model`` parameter. - -.. code:: python - - import pygad.gacnn - - GACNN_instance = pygad.gacnn.GACNN(model=model, - num_solutions=4) - -After creating the instance of the ``pygad.gacnn.GACNN`` class, next is -to fetch the weights of the population as a list of vectors. - -Fetch the Population Weights as Vectors ---------------------------------------- - -For the genetic algorithm, the parameters (i.e. genes) of each solution -are represented as a single vector. - -For this task, the weights of each CNN must be available as a single -vector. In other words, the weights of all layers of a CNN must be -grouped into a vector. - -To create a list holding the population weights as vectors, one for each -network, the ``pygad.gacnn.population_as_vectors()`` function is used. - -.. code:: python - - population_vectors = gacnn.population_as_vectors(population_networks=GACNN_instance.population_networks) - -Such population of vectors is used as the initial population. - -.. code:: python - - initial_population = population_vectors.copy() - -After preparing the population weights as a set of vectors, next is to -prepare 2 functions which are: - -1. Fitness function. - -2. Callback function after each generation. - -Prepare the Fitness Function ----------------------------- - -The PyGAD library works by allowing the users to customize the genetic -algorithm for their own problems. Because the problems differ in how the -fitness values are calculated, then PyGAD allows the user to use a -custom function as a maximization fitness function. This function must -accept 2 positional parameters representing the following: - -- The solution. - -- The solution index in the population. - -The fitness function must return a single number representing the -fitness. The higher the fitness value, the better the solution. - -Here is the implementation of the fitness function for training a CNN. - -It uses the ``pygad.cnn.predict()`` function to predict the class labels -based on the current solution's weights. The ``pygad.cnn.predict()`` -function uses the trained weights available in the ``trained_weights`` -attribute of each layer of the network for making predictions. - -Based on such predictions, the classification accuracy is calculated. -This accuracy is used as the fitness value of the solution. Finally, the -fitness value is returned. - -.. code:: python - - def fitness_func(ga_instance, solution, sol_idx): - global GACNN_instance, data_inputs, data_outputs - - predictions = GACNN_instance.population_networks[sol_idx].predict(data_inputs=data_inputs) - correct_predictions = numpy.where(predictions == data_outputs)[0].size - solution_fitness = (correct_predictions/data_outputs.size)*100 - - return solution_fitness - -Prepare the Generation Callback Function ----------------------------------------- - -After each generation of the genetic algorithm, the fitness function -will be called to calculate the fitness value of each solution. Within -the fitness function, the ``pygad.cnn.predict()`` function is used for -predicting the outputs based on the current solution's -``trained_weights`` attribute. Thus, it is required that such an -attribute is updated by weights evolved by the genetic algorithm after -each generation. - -PyGAD has a parameter accepted by the ``pygad.GA`` class constructor -named ``on_generation``. It could be assigned to a function that is -called after each generation. The function must accept a single -parameter representing the instance of the ``pygad.GA`` class. - -This callback function can be used to update the ``trained_weights`` -attribute of layers of each network in the population. - -Here is the implementation for a function that updates the -``trained_weights`` attribute of the layers of the population networks. - -It works by converting the current population from the vector form to -the matric form using the ``pygad.gacnn.population_as_matrices()`` -function. It accepts the population as vectors and returns it as -matrices. - -The population matrices are then passed to the -``update_population_trained_weights()`` method in the ``pygad.gacnn`` -module to update the ``trained_weights`` attribute of all layers for all -solutions within the population. - -.. code:: python - - def callback_generation(ga_instance): - global GACNN_instance, last_fitness - - population_matrices = gacnn.population_as_matrices(population_networks=GACNN_instance.population_networks, population_vectors=ga_instance.population) - GACNN_instance.update_population_trained_weights(population_trained_weights=population_matrices) - - print(f"Generation = {ga_instance.generations_completed}") - -After preparing the fitness and callback function, next is to create an -instance of the ``pygad.GA`` class. - -.. _create-an-instance-of-the-pygadga-class: - -Create an Instance of the ``pygad.GA`` Class --------------------------------------------- - -Once the parameters of the genetic algorithm are prepared, an instance -of the ``pygad.GA`` class can be created. Here is an example where the -number of generations is 10. - -.. code:: python - - import pygad - - num_parents_mating = 4 - - num_generations = 10 - - mutation_percent_genes = 5 - - ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - mutation_percent_genes=mutation_percent_genes, - on_generation=callback_generation) - -The last step for training the neural networks using the genetic -algorithm is calling the ``run()`` method. - -.. _run-the-created-instance-of-the-pygadga-class: - -Run the Created Instance of the ``pygad.GA`` Class --------------------------------------------------- - -By calling the ``run()`` method from the ``pygad.GA`` instance, the -genetic algorithm will iterate through the number of generations -specified in its ``num_generations`` parameter. - -.. code:: python - - ga_instance.run() - -Plot the Fitness Values ------------------------ - -After the ``run()`` method completes, the ``plot_fitness()`` method can -be called to show how the fitness values evolve by generation. - -.. code:: python - - ga_instance.plot_fitness() - -.. image:: https://user-images.githubusercontent.com/16560492/83429675-ab744580-a434-11ea-8f21-9d3804b50d15.png - :alt: - -Information about the Best Solution ------------------------------------ - -The following information about the best solution in the last population -is returned using the ``best_solution()`` method in the ``pygad.GA`` -class. - -- Solution - -- Fitness value of the solution - -- Index of the solution within the population - -Here is how such information is returned. - -.. code:: python - - solution, solution_fitness, solution_idx = ga_instance.best_solution() - print(f"Parameters of the best solution : {solution}") - print(f"Fitness value of the best solution = {solution_fitness}") - print(f"Index of the best solution : {solution_idx}") - -.. code:: - - ... - Fitness value of the best solution = 83.75 - Index of the best solution : 0 - Best fitness value reached after 4 generations. - -Making Predictions using the Trained Weights --------------------------------------------- - -The ``pygad.cnn.predict()`` function can be used to make predictions -using the trained network. As printed, the network is able to predict -the labels correctly. - -.. code:: python - - predictions = pygad.cnn.predict(last_layer=GANN_instance.population_networks[solution_idx], data_inputs=data_inputs) - print(f"Predictions of the trained network : {predictions}") - -Calculating Some Statistics ---------------------------- - -Based on the predictions the network made, some statistics can be -calculated such as the number of correct and wrong predictions in -addition to the classification accuracy. - -.. code:: python - - num_wrong = numpy.where(predictions != data_outputs)[0] - num_correct = data_outputs.size - num_wrong.size - accuracy = 100 * (num_correct/data_outputs.size) - print(f"Number of correct classifications : {num_correct}.") - print(f"Number of wrong classifications : {num_wrong.size}.") - print(f"Classification accuracy : {accuracy}.") - -.. code:: - - Number of correct classifications : 67. - Number of wrong classifications : 13. - Classification accuracy : 83.75. - -Examples -======== - -This section gives the complete code of some examples that build and -train neural networks using the genetic algorithm. Each subsection -builds a different network. - -Image Classification --------------------- - -This example is discussed in the **Steps to Build and Train CNN using -Genetic Algorithm** section that builds the an image classifier. Its -complete code is listed below. - -.. code:: python - - import numpy - import pygad.cnn - import pygad.gacnn - import pygad - - """ - Convolutional neural network implementation using NumPy - A tutorial that helps to get started (Building Convolutional Neural Network using NumPy from Scratch) available in these links: - https://www.linkedin.com/pulse/building-convolutional-neural-network-using-numpy-from-ahmed-gad - https://towardsdatascience.com/building-convolutional-neural-network-using-numpy-from-scratch-b30aac50e50a - https://www.kdnuggets.com/2018/04/building-convolutional-neural-network-numpy-scratch.html - It is also translated into Chinese: http://m.aliyun.com/yunqi/articles/585741 - """ - - def fitness_func(ga_instance, solution, sol_idx): - global GACNN_instance, data_inputs, data_outputs - - predictions = GACNN_instance.population_networks[sol_idx].predict(data_inputs=data_inputs) - correct_predictions = numpy.where(predictions == data_outputs)[0].size - solution_fitness = (correct_predictions/data_outputs.size)*100 - - return solution_fitness - - def callback_generation(ga_instance): - global GACNN_instance, last_fitness - - population_matrices = pygad.gacnn.population_as_matrices(population_networks=GACNN_instance.population_networks, - population_vectors=ga_instance.population) - - GACNN_instance.update_population_trained_weights(population_trained_weights=population_matrices) - - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solutions_fitness}") - - data_inputs = numpy.load("dataset_inputs.npy") - data_outputs = numpy.load("dataset_outputs.npy") - - sample_shape = data_inputs.shape[1:] - num_classes = 4 - - data_inputs = data_inputs - data_outputs = data_outputs - - input_layer = pygad.cnn.Input2D(input_shape=sample_shape) - conv_layer1 = pygad.cnn.Conv2D(num_filters=2, - kernel_size=3, - previous_layer=input_layer, - activation_function="relu") - average_pooling_layer = pygad.cnn.AveragePooling2D(pool_size=5, - previous_layer=conv_layer1, - stride=3) - - flatten_layer = pygad.cnn.Flatten(previous_layer=average_pooling_layer) - dense_layer2 = pygad.cnn.Dense(num_neurons=num_classes, - previous_layer=flatten_layer, - activation_function="softmax") - - model = pygad.cnn.Model(last_layer=dense_layer2, - epochs=1, - learning_rate=0.01) - - model.summary() - - - GACNN_instance = pygad.gacnn.GACNN(model=model, - num_solutions=4) - - # GACNN_instance.update_population_trained_weights(population_trained_weights=population_matrices) - - # population does not hold the numerical weights of the network instead it holds a list of references to each last layer of each network (i.e. solution) in the population. A solution or a network can be used interchangeably. - # If there is a population with 3 solutions (i.e. networks), then the population is a list with 3 elements. Each element is a reference to the last layer of each network. Using such a reference, all details of the network can be accessed. - population_vectors = pygad.gacnn.population_as_vectors(population_networks=GACNN_instance.population_networks) - - # To prepare the initial population, there are 2 ways: - # 1) Prepare it yourself and pass it to the initial_population parameter. This way is useful when the user wants to start the genetic algorithm with a custom initial population. - # 2) Assign valid integer values to the sol_per_pop and num_genes parameters. If the initial_population parameter exists, then the sol_per_pop and num_genes parameters are useless. - initial_population = population_vectors.copy() - - num_parents_mating = 2 # Number of solutions to be selected as parents in the mating pool. - - num_generations = 10 # Number of generations. - - mutation_percent_genes = 0.1 # Percentage of genes to mutate. This parameter has no action if the parameter mutation_num_genes exists. - - ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - mutation_percent_genes=mutation_percent_genes, - on_generation=callback_generation) - - ga_instance.run() - - # After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. - ga_instance.plot_fitness() - - # Returning the details of the best solution. - solution, solution_fitness, solution_idx = ga_instance.best_solution() - print(f"Parameters of the best solution : {solution}") - print(f"Fitness value of the best solution = {solution_fitness}") - print(f"Index of the best solution : {solution_idx}") - - if ga_instance.best_solution_generation != -1: - print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") - - # Predicting the outputs of the data using the best solution. - predictions = GACNN_instance.population_networks[solution_idx].predict(data_inputs=data_inputs) - print(f"Predictions of the trained network : {predictions}") - - # Calculating some statistics - num_wrong = numpy.where(predictions != data_outputs)[0] - num_correct = data_outputs.size - num_wrong.size - accuracy = 100 * (num_correct/data_outputs.size) - print(f"Number of correct classifications : {num_correct}.") - print(f"Number of wrong classifications : {num_wrong.size}.") - print(f"Classification accuracy : {accuracy}.") diff --git a/docs/md/gann.md b/docs/source/gann.md similarity index 97% rename from docs/md/gann.md rename to docs/source/gann.md index 19fc73d..12eb9c2 100644 --- a/docs/md/gann.md +++ b/docs/source/gann.md @@ -4,11 +4,11 @@ This section of the PyGAD's library documentation discusses the **pygad.gann** m The `pygad.gann` module trains neural networks (for either classification or regression) using the genetic algorithm. It makes use of the 2 modules `pygad` and `pygad.nn`. -# `pygad.gann.GANN` Class +## `pygad.gann.GANN` Class The `pygad.gann` module has a class named `pygad.gann.GANN` for training neural networks using the genetic algorithm. The constructor, methods, function, and attributes within the class are discussed in this section. -## `__init__()` +### `__init__()` In order to train a neural network using the genetic algorithm, the first thing to do is to create an instance of the `pygad.gann.GANN` class. @@ -23,7 +23,7 @@ The `pygad.gann.GANN` class constructor accepts the following parameters: In order to validate the parameters passed to the `pygad.gann.GANN` class constructor, the `pygad.gann.validate_network_parameters()` function is called. -## Instance Attributes +### Instance Attributes All the parameters in the `pygad.gann.GANN` class constructor are used as instance attributes. Besides such attributes, there are other attributes added to the instances from the `pygad.gann.GANN` class which are: @@ -31,11 +31,11 @@ All the parameters in the `pygad.gann.GANN` class constructor are used as instan - `population_networks`: A list holding references to all the solutions (i.e. neural networks) used in the population. -## Methods in the GANN Class +### Methods in the GANN Class This section discusses the methods available for instances of the `pygad.gann.GANN` class. -### `create_population()` +#### `create_population()` The `create_population()` method creates the initial population of the genetic algorithm as a list of neural networks (i.e. solutions). For each network to be created, the `pygad.gann.create_network()` function is called. @@ -43,7 +43,7 @@ Each element in the list holds a reference to the last (i.e. output) layer for t The method returns the list holding the references to the networks. This list is later assigned to the `population_networks` attribute of the instance. -### `update_population_trained_weights()` +#### `update_population_trained_weights()` The `update_population_trained_weights()` method updates the `trained_weights` attribute of the layers of each network (check the [documentation of the pygad.nn.DenseLayer class](https://github.com/ahmedfgad/NumPyANN#nndenselayer-class) for more information) according to the weights passed in the `population_trained_weights` parameter. @@ -51,11 +51,11 @@ Accepts the following parameters: - `population_trained_weights`: A list holding the trained weights of all networks as matrices. Such matrices are to be assigned to the `trained_weights` attribute of all layers of all networks. -# Functions in the `pygad.gann` Module +## Functions in the `pygad.gann` Module This section discusses the functions in the `pygad.gann` module. -## `pygad.gann.validate_network_parameters()` +### `pygad.gann.validate_network_parameters()` Validates the parameters passed to the constructor of the `pygad.gann.GANN` class. If at least one an invalid parameter exists, an exception is raised and the execution stops. The function accepts the same parameters passed to the constructor of the `pygad.gann.GANN` class. Please check the documentation of such parameters in the section discussing the class constructor. @@ -66,7 +66,7 @@ If the value passed to the `hidden_activations` parameter is a string, not a lis Returns a list holding the name(s) of the activation function(s) of the hidden layer(s). -## `pygad.gann.create_network()` +### `pygad.gann.create_network()` Creates a neural network as a linked list between the input, hidden, and output layers where the layer at index N (which is the last/output layer) references the layer at index N-1 (which is a hidden layer) using its previous_layer attribute. The input layer does not reference any layer because it is the last layer in the linked list. @@ -76,7 +76,7 @@ In addition to the `parameters_validated` parameter, this function accepts the s Returns the reference to the last layer in the network architecture which is the output layer. Based on such a reference, all network layers can be fetched. -## `pygad.gann.population_as_vectors()` +### `pygad.gann.population_as_vectors()` Accepts the population as networks and returns a list holding all weights of the layers of each solution (i.e. network) in the population as a vector. @@ -88,7 +88,7 @@ Accepts the following parameters: Returns a list holding the weights vectors for all solutions (i.e. networks). -## `pygad.gann.population_as_matrices()` +### `pygad.gann.population_as_matrices()` Accepts the population as both networks and weights vectors and returns the weights of all layers of each solution (i.e. network) in the population as a matrix. @@ -101,7 +101,7 @@ Accepts the following parameters: Returns a list holding the weights matrices for all solutions (i.e. networks). -# Steps to Build and Train Neural Networks using Genetic Algorithm +## Steps to Build and Train Neural Networks using Genetic Algorithm The steps to use this project for building and training a neural network using the genetic algorithm are as follows: @@ -119,7 +119,7 @@ The steps to use this project for building and training a neural network using t Let's start covering all of these steps. -## Prepare the Training Data +### Prepare the Training Data Before building and training neural networks, the training data (input and output) is to be prepared. The inputs and the outputs of the training data are NumPy arrays. @@ -147,7 +147,7 @@ For the XOR example, there are 2 classes and thus their labels are 0 and 1. The Note that the project only supports classification problems where each sample is assigned to only one class. -## Create an Instance of the `pygad.gann.GANN` Class +### Create an Instance of the `pygad.gann.GANN` Class After preparing the input data, an instance of the `pygad.gann.GANN` class is created by passing the appropriate parameters. @@ -183,7 +183,7 @@ The activation function used for the output layer is `softmax`. The `relu` activ After creating the instance of the `pygad.gann.GANN` class next is to fetch the weights of the population as a list of vectors. -## Fetch the Population Weights as Vectors +### Fetch the Population Weights as Vectors For the genetic algorithm, the parameters (i.e. genes) of each solution are represented as a single vector. @@ -200,7 +200,7 @@ After preparing the population weights as a set of vectors, next is to prepare 2 1. Fitness function. 2. Callback function after each generation. -## Prepare the Fitness Function +### Prepare the Fitness Function The PyGAD library works by allowing the users to customize the genetic algorithm for their own problems. Because the problems differ in how the fitness values are calculated, then PyGAD allows the user to use a custom function as a maximization fitness function. This function must accept 2 positional parameters representing the following: @@ -225,7 +225,7 @@ def fitness_func(ga_instance, solution, sol_idx): return solution_fitness ``` -## Prepare the Generation Callback Function +### Prepare the Generation Callback Function After each generation of the genetic algorithm, the fitness function will be called to calculate the fitness value of each solution. Within the fitness function, the `pygad.nn.predict()` function is used for predicting the outputs based on the current solution's `trained_weights` attribute. Thus, it is required that such an attribute is updated by weights evolved by the genetic algorithm after each generation. @@ -252,7 +252,7 @@ def callback_generation(ga_instance): After preparing the fitness and callback function, next is to create an instance of the `pygad.GA` class. -## Create an Instance of the `pygad.GA` Class +### Create an Instance of the `pygad.GA` Class Once the parameters of the genetic algorithm are prepared, an instance of the `pygad.GA` class can be created. @@ -294,7 +294,7 @@ ga_instance = pygad.GA(num_generations=num_generations, The last step for training the neural networks using the genetic algorithm is calling the `run()` method. -## Run the Created Instance of the `pygad.GA` Class +### Run the Created Instance of the `pygad.GA` Class By calling the `run()` method from the `pygad.GA` instance, the genetic algorithm will iterate through the number of generations specified in its `num_generations` parameter. @@ -302,7 +302,7 @@ By calling the `run()` method from the `pygad.GA` instance, the genetic algorith ga_instance.run() ``` -## Plot the Fitness Values +### Plot the Fitness Values After the `run()` method completes, the `plot_fitness()` method can be called to show how the fitness values evolve by generation. A fitness value (i.e. accuracy) of 100 is reached after around 180 generations. @@ -314,7 +314,7 @@ ga_instance.plot_fitness() By running the code again, a different initial population is created and thus a classification accuracy of 100 can be reached using a less number of generations. On the other hand, a different initial population might cause 100% accuracy to be reached using more generations or not reached at all. -## Information about the Best Solution +### Information about the Best Solution The following information about the best solution in the last population is returned using the `best_solution()` method in the `pygad.GA` class. @@ -348,7 +348,7 @@ if ga_instance.best_solution_generation != -1: Best solution reached after 182 generations. ``` -## Making Predictions using the Trained Weights +### Making Predictions using the Trained Weights The `pygad.nn.predict()` function can be used to make predictions using the trained network. As printed, the network is able to predict the labels correctly. @@ -361,7 +361,7 @@ print(f"Predictions of the trained network : {predictions}") Predictions of the trained network : [0. 1. 1. 0.] ``` -## Calculating Some Statistics +### Calculating Some Statistics Based on the predictions the network made, some statistics can be calculated such as the number of correct and wrong predictions in addition to the classification accuracy. @@ -380,11 +380,11 @@ print("Number of wrong classifications : 0 Classification accuracy : 100 ``` -# Examples +## Examples This section gives the complete code of some examples that build and train neural networks using the genetic algorithm. Each subsection builds a different network. -## XOR Classification +### XOR Classification This example is discussed in the **Steps to Build and Train Neural Networks using Genetic Algorithm** section that builds the XOR gate and its complete code is listed below. @@ -519,7 +519,7 @@ print(f"Number of wrong classifications : {num_wrong.size}.") print(f"Classification accuracy : {accuracy}.") ``` -## Image Classification +### Image Classification In the documentation of the `pygad.nn` module, a neural network is created for classifying images from the Fruits360 dataset without being trained using an optimization algorithm. This section discusses how to train such a classifier using the genetic algorithm with the help of the `pygad.gann` module. @@ -666,7 +666,7 @@ The next figure shows how fitness value evolves by generation. ![Training Neural Networks using Genetic Algorithm](https://user-images.githubusercontent.com/16560492/82152993-21898180-9865-11ea-8387-b995f88b83f7.png) -## Regression Example 1 +### Regression Example 1 To train a neural network for regression, follow these instructions: @@ -820,7 +820,7 @@ The next figure shows how the fitness value changes for the generations used. ![example_regression](https://user-images.githubusercontent.com/16560492/92948154-3cf24b00-f459-11ea-94ea-952b66ab2145.png) -## Regression Example 2 - Fish Weight Prediction +### Regression Example 2 - Fish Weight Prediction This example uses the Fish Market Dataset available at Kaggle (https://www.kaggle.com/aungpyaeap/fish-market). Simply download the CSV dataset from [this link](https://www.kaggle.com/aungpyaeap/fish-market/download) (https://www.kaggle.com/aungpyaeap/fish-market/download). The dataset is also available at the [GitHub project of the pygad.gann module](https://github.com/ahmedfgad/NeuralGenetic): https://github.com/ahmedfgad/NeuralGenetic diff --git a/docs/source/gann.rst b/docs/source/gann.rst deleted file mode 100644 index c3c85b3..0000000 --- a/docs/source/gann.rst +++ /dev/null @@ -1,1267 +0,0 @@ -.. _pygadgann-module: - -``pygad.gann`` Module -===================== - -This section of the PyGAD's library documentation discusses the -**pygad.gann** module. - -The ``pygad.gann`` module trains neural networks (for either -classification or regression) using the genetic algorithm. It makes use -of the 2 modules ``pygad`` and ``pygad.nn``. - -.. _pygadganngann-class: - -``pygad.gann.GANN`` Class -========================= - -The ``pygad.gann`` module has a class named ``pygad.gann.GANN`` for -training neural networks using the genetic algorithm. The constructor, -methods, function, and attributes within the class are discussed in this -section. - -.. _init: - -``__init__()`` --------------- - -In order to train a neural network using the genetic algorithm, the -first thing to do is to create an instance of the ``pygad.gann.GANN`` -class. - -The ``pygad.gann.GANN`` class constructor accepts the following -parameters: - -- ``num_solutions``: Number of neural networks (i.e. solutions) in the - population. Based on the value passed to this parameter, a number of - identical neural networks are created where their parameters are - optimized using the genetic algorithm. - -- ``num_neurons_input``: Number of neurons in the input layer. - -- ``num_neurons_output``: Number of neurons in the output layer. - -- ``num_neurons_hidden_layers=[]``: A list holding the number of - neurons in the hidden layer(s). If empty ``[]``, then no hidden - layers are used. For each ``int`` value it holds, then a hidden layer - is created with a number of hidden neurons specified by the - corresponding ``int`` value. For example, - ``num_neurons_hidden_layers=[10]`` creates a single hidden layer with - **10** neurons. ``num_neurons_hidden_layers=[10, 5]`` creates 2 - hidden layers with 10 neurons for the first and 5 neurons for the - second hidden layer. - -- ``output_activation="softmax"``: The name of the activation function - of the output layer which defaults to ``"softmax"``. - -- ``hidden_activations="relu"``: The name(s) of the activation - function(s) of the hidden layer(s). It defaults to ``"relu"``. If - passed as a string, this means the specified activation function will - be used across all the hidden layers. If passed as a list, then it - must have the same length as the length of the - ``num_neurons_hidden_layers`` list. An exception is raised if their - lengths are different. When ``hidden_activations`` is a list, a - one-to-one mapping between the ``num_neurons_hidden_layers`` and - ``hidden_activations`` lists occurs. - -In order to validate the parameters passed to the ``pygad.gann.GANN`` -class constructor, the ``pygad.gann.validate_network_parameters()`` -function is called. - -Instance Attributes -------------------- - -All the parameters in the ``pygad.gann.GANN`` class constructor are used -as instance attributes. Besides such attributes, there are other -attributes added to the instances from the ``pygad.gann.GANN`` class -which are: - -- ``parameters_validated``: If ``True``, then the parameters passed to - the GANN class constructor are valid. Its initial value is ``False``. - -- ``population_networks``: A list holding references to all the - solutions (i.e. neural networks) used in the population. - -Methods in the GANN Class -------------------------- - -This section discusses the methods available for instances of the -``pygad.gann.GANN`` class. - -.. _createpopulation: - -``create_population()`` -~~~~~~~~~~~~~~~~~~~~~~~ - -The ``create_population()`` method creates the initial population of the -genetic algorithm as a list of neural networks (i.e. solutions). For -each network to be created, the ``pygad.gann.create_network()`` function -is called. - -Each element in the list holds a reference to the last (i.e. output) -layer for the network. The method does not accept any parameter and it -accesses all the required details from the ``pygad.gann.GANN`` instance. - -The method returns the list holding the references to the networks. This -list is later assigned to the ``population_networks`` attribute of the -instance. - -.. _updatepopulationtrainedweights: - -``update_population_trained_weights()`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The ``update_population_trained_weights()`` method updates the -``trained_weights`` attribute of the layers of each network (check the -`documentation of the pygad.nn.DenseLayer -class `__ for -more information) according to the weights passed in the -``population_trained_weights`` parameter. - -Accepts the following parameters: - -- ``population_trained_weights``: A list holding the trained weights of - all networks as matrices. Such matrices are to be assigned to the - ``trained_weights`` attribute of all layers of all networks. - -.. _functions-in-the-pygadgann-module: - -Functions in the ``pygad.gann`` Module -====================================== - -This section discusses the functions in the ``pygad.gann`` module. - -.. _pygadgannvalidatenetworkparameters: - -``pygad.gann.validate_network_parameters()`` --------------------------------------------- - -Validates the parameters passed to the constructor of the -``pygad.gann.GANN`` class. If at least one an invalid parameter exists, -an exception is raised and the execution stops. - -The function accepts the same parameters passed to the constructor of -the ``pygad.gann.GANN`` class. Please check the documentation of such -parameters in the section discussing the class constructor. - -The reason why this function sets a default value to the -``num_solutions`` parameter is differentiating whether a population of -networks or a single network is to be created. If ``None``, then a -single network will be created. If not ``None``, then a population of -networks is to be created. - -If the value passed to the ``hidden_activations`` parameter is a string, -not a list, then a list is created by replicating the passed name of the -activation function a number of times equal to the number of hidden -layers (i.e. the length of the ``num_neurons_hidden_layers`` parameter). - -Returns a list holding the name(s) of the activation function(s) of the -hidden layer(s). - -.. _pygadganncreatenetwork: - -``pygad.gann.create_network()`` -------------------------------- - -Creates a neural network as a linked list between the input, hidden, and -output layers where the layer at index N (which is the last/output -layer) references the layer at index N-1 (which is a hidden layer) using -its previous_layer attribute. The input layer does not reference any -layer because it is the last layer in the linked list. - -In addition to the ``parameters_validated`` parameter, this function -accepts the same parameters passed to the constructor of the -``pygad.gann.GANN`` class except for the ``num_solutions`` parameter -because only a single network is created out of the ``create_network()`` -function. - -``parameters_validated``: If ``False``, then the parameters are not -validated and a call to the ``validate_network_parameters()`` function -is made. - -Returns the reference to the last layer in the network architecture -which is the output layer. Based on such a reference, all network layers -can be fetched. - -.. _pygadgannpopulationasvectors: - -``pygad.gann.population_as_vectors()`` ---------------------------------------- - -Accepts the population as networks and returns a list holding all -weights of the layers of each solution (i.e. network) in the population -as a vector. - -For example, if the population has 6 solutions (i.e. networks), this -function accepts references to such networks and returns a list with 6 -vectors, one for each network (i.e. solution). Each vector holds the -weights for all layers for a single network. - -Accepts the following parameters: - -- ``population_networks``: A list holding references to the output - (last) layers of the neural networks used in the population. - -Returns a list holding the weights vectors for all solutions (i.e. -networks). - -.. _pygadgannpopulationasmatrices: - -``pygad.gann.population_as_matrices()`` ---------------------------------------- - -Accepts the population as both networks and weights vectors and returns -the weights of all layers of each solution (i.e. network) in the -population as a matrix. - -For example, if the population has 6 solutions (i.e. networks), this -function returns a list with 6 matrices, one for each network holding -its weights for all layers. - -Accepts the following parameters: - -- ``population_networks``: A list holding references to the output - (last) layers of the neural networks used in the population. - -- ``population_vectors``: A list holding the weights of all networks as - vectors. Such vectors are to be converted into matrices. - -Returns a list holding the weights matrices for all solutions (i.e. -networks). - -Steps to Build and Train Neural Networks using Genetic Algorithm -================================================================ - -The steps to use this project for building and training a neural network -using the genetic algorithm are as follows: - -- Prepare the training data. - -- Create an instance of the ``pygad.gann.GANN`` class. - -- Fetch the population weights as vectors. - -- Prepare the fitness function. - -- Prepare the generation callback function. - -- Create an instance of the ``pygad.GA`` class. - -- Run the created instance of the ``pygad.GA`` class. - -- Plot the Fitness Values - -- Information about the best solution. - -- Making predictions using the trained weights. - -- Calculating some statistics. - -Let's start covering all of these steps. - -Prepare the Training Data -------------------------- - -Before building and training neural networks, the training data (input -and output) is to be prepared. The inputs and the outputs of the -training data are NumPy arrays. - -Here is an example of preparing the training data for the XOR problem. - -For the input array, each element must be a list representing the inputs -(i.e. features) for the sample. If there are 200 samples and each sample -has 50 features, then the shape of the inputs array is ``(200, 50)``. -The variable ``num_inputs`` holds the length of each sample which is 2 -in this example. - -.. code:: python - - data_inputs = numpy.array([[1, 1], - [1, 0], - [0, 1], - [0, 0]]) - - data_outputs = numpy.array([0, - 1, - 1, - 0]) - - num_inputs = data_inputs.shape[1] - -For the output array, each element must be a single number representing -the class label of the sample. The class labels must start at ``0``. So, -if there are 200 samples, then the shape of the output array is -``(200)``. If there are 5 classes in the data, then the values of all -the 200 elements in the output array must range from 0 to 4 inclusive. -Generally, the class labels start from ``0`` to ``N-1`` where ``N`` is -the number of classes. - -For the XOR example, there are 2 classes and thus their labels are 0 and -1. The ``num_classes`` variable is assigned to 2. - -Note that the project only supports classification problems where each -sample is assigned to only one class. - -.. _create-an-instance-of-the-pygadganngann-class: - -Create an Instance of the ``pygad.gann.GANN`` Class ---------------------------------------------------- - -After preparing the input data, an instance of the ``pygad.gann.GANN`` -class is created by passing the appropriate parameters. - -Here is an example that creates a network for the XOR problem. The -``num_solutions`` parameter is set to 6 which means the genetic -algorithm population will have 6 solutions (i.e. networks). All of these -6 neural networks will have the same architectures as specified by the -other parameters. - -The output layer has 2 neurons because there are only 2 classes (0 and -1). - -.. code:: python - - import pygad.gann - import pygad.nn - - num_solutions = 6 - GANN_instance = pygad.gann.GANN(num_solutions=num_solutions, - num_neurons_input=num_inputs, - num_neurons_hidden_layers=[2], - num_neurons_output=2, - hidden_activations=["relu"], - output_activation="softmax") - -The architecture of the created network has the following layers: - -- An input layer with 2 neurons (i.e. inputs) - -- A single hidden layer with 2 neurons. - -- An output layer with 2 neurons (i.e. classes). - -The weights of the network are as follows: - -- Between the input and the hidden layer, there is a weights matrix of - size equal to ``(number inputs x number of hidden neurons) = (2x2)``. - -- Between the hidden and the output layer, there is a weights matrix of - size equal to - ``(number of hidden neurons x number of outputs) = (2x2)``. - -The activation function used for the output layer is ``softmax``. The -``relu`` activation function is used for the hidden layer. - -After creating the instance of the ``pygad.gann.GANN`` class next is to -fetch the weights of the population as a list of vectors. - -Fetch the Population Weights as Vectors ---------------------------------------- - -For the genetic algorithm, the parameters (i.e. genes) of each solution -are represented as a single vector. - -For the task of training the network for the XOR problem, the weights of -each network in the population are not represented as a vector but 2 -matrices each of size 2x2. - -To create a list holding the population weights as vectors, one for each -network, the ``pygad.gann.population_as_vectors()`` function is used. - -.. code:: python - - population_vectors = pygad.gann.population_as_vectors(population_networks=GANN_instance.population_networks) - -After preparing the population weights as a set of vectors, next is to -prepare 2 functions which are: - -1. Fitness function. - -2. Callback function after each generation. - -Prepare the Fitness Function ----------------------------- - -The PyGAD library works by allowing the users to customize the genetic -algorithm for their own problems. Because the problems differ in how the -fitness values are calculated, then PyGAD allows the user to use a -custom function as a maximization fitness function. This function must -accept 2 positional parameters representing the following: - -- The solution. - -- The solution index in the population. - -The fitness function must return a single number representing the -fitness. The higher the fitness value, the better the solution. - -Here is the implementation of the fitness function for training a neural -network. It uses the ``pygad.nn.predict()`` function to predict the -class labels based on the current solution's weights. The -``pygad.nn.predict()`` function uses the trained weights available in -the ``trained_weights`` attribute of each layer of the network for -making predictions. - -Based on such predictions, the classification accuracy is calculated. -This accuracy is used as the fitness value of the solution. Finally, the -fitness value is returned. - -.. code:: python - - def fitness_func(ga_instance, solution, sol_idx): - global GANN_instance, data_inputs, data_outputs - - predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[sol_idx], - data_inputs=data_inputs) - correct_predictions = numpy.where(predictions == data_outputs)[0].size - solution_fitness = (correct_predictions/data_outputs.size)*100 - - return solution_fitness - -Prepare the Generation Callback Function ----------------------------------------- - -After each generation of the genetic algorithm, the fitness function -will be called to calculate the fitness value of each solution. Within -the fitness function, the ``pygad.nn.predict()`` function is used for -predicting the outputs based on the current solution's -``trained_weights`` attribute. Thus, it is required that such an -attribute is updated by weights evolved by the genetic algorithm after -each generation. - -PyGAD 2.0.0 and higher has a new parameter accepted by the ``pygad.GA`` -class constructor named ``on_generation``. It could be assigned to a -function that is called after each generation. The function must accept -a single parameter representing the instance of the ``pygad.GA`` class. - -This callback function can be used to update the ``trained_weights`` -attribute of layers of each network in the population. - -Here is the implementation for a function that updates the -``trained_weights`` attribute of the layers of the population networks. - -It works by converting the current population from the vector form to -the matric form using the ``pygad.gann.population_as_matrices()`` -function. It accepts the population as vectors and returns it as -matrices. - -The population matrices are then passed to the -``update_population_trained_weights()`` method in the ``pygad.gann`` -module to update the ``trained_weights`` attribute of all layers for all -solutions within the population. - -.. code:: python - - def callback_generation(ga_instance): - global GANN_instance - - population_matrices = pygad.gann.population_as_matrices(population_networks=GANN_instance.population_networks, population_vectors=ga_instance.population) - GANN_instance.update_population_trained_weights(population_trained_weights=population_matrices) - - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution()[1]}") - -After preparing the fitness and callback function, next is to create an -instance of the ``pygad.GA`` class. - -.. _create-an-instance-of-the-pygadga-class: - -Create an Instance of the ``pygad.GA`` Class --------------------------------------------- - -Once the parameters of the genetic algorithm are prepared, an instance -of the ``pygad.GA`` class can be created. - -Here is an example. - -.. code:: python - - initial_population = population_vectors.copy() - - num_parents_mating = 4 - - num_generations = 500 - - mutation_percent_genes = 5 - - parent_selection_type = "sss" - - crossover_type = "single_point" - - mutation_type = "random" - - keep_parents = 1 - - init_range_low = -2 - init_range_high = 5 - - ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - mutation_percent_genes=mutation_percent_genes, - init_range_low=init_range_low, - init_range_high=init_range_high, - parent_selection_type=parent_selection_type, - crossover_type=crossover_type, - mutation_type=mutation_type, - keep_parents=keep_parents, - on_generation=callback_generation) - -The last step for training the neural networks using the genetic -algorithm is calling the ``run()`` method. - -.. _run-the-created-instance-of-the-pygadga-class: - -Run the Created Instance of the ``pygad.GA`` Class --------------------------------------------------- - -By calling the ``run()`` method from the ``pygad.GA`` instance, the -genetic algorithm will iterate through the number of generations -specified in its ``num_generations`` parameter. - -.. code:: python - - ga_instance.run() - -Plot the Fitness Values ------------------------ - -After the ``run()`` method completes, the ``plot_fitness()`` method can -be called to show how the fitness values evolve by generation. A fitness -value (i.e. accuracy) of 100 is reached after around 180 generations. - -.. code:: python - - ga_instance.plot_fitness() - -.. image:: https://user-images.githubusercontent.com/16560492/82078638-c11e0700-96e1-11ea-8aa9-c36761c5e9c7.png - :alt: - -By running the code again, a different initial population is created and -thus a classification accuracy of 100 can be reached using a less number -of generations. On the other hand, a different initial population might -cause 100% accuracy to be reached using more generations or not reached -at all. - -Information about the Best Solution ------------------------------------ - -The following information about the best solution in the last population -is returned using the ``best_solution()`` method in the ``pygad.GA`` -class. - -- Solution - -- Fitness value of the solution - -- Index of the solution within the population - -Here is how such information is returned. The fitness value (i.e. -accuracy) is 100. - -.. code:: python - - solution, solution_fitness, solution_idx = ga_instance.best_solution() - print(f"Parameters of the best solution : {solution}") - print(f"Fitness value of the best solution = {solution_fitness}") - print(f"Index of the best solution : {solution_idx}") - -.. code:: - - Parameters of the best solution : [3.55081391 -3.21562011 -14.2617784 0.68044231 -1.41258145 -3.2979315 1.58136006 -7.83726169] - Fitness value of the best solution = 100.0 - Index of the best solution : 0 - -Using the ``best_solution_generation`` attribute of the instance from -the ``pygad.GA`` class, the generation number at which the **best -fitness** is reached could be fetched. According to the result, the best -fitness value is reached after 182 generations. - -.. code:: python - - if ga_instance.best_solution_generation != -1: - print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") - -.. code:: - - Best solution reached after 182 generations. - -Making Predictions using the Trained Weights --------------------------------------------- - -The ``pygad.nn.predict()`` function can be used to make predictions -using the trained network. As printed, the network is able to predict -the labels correctly. - -.. code:: python - - predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[solution_idx], data_inputs=data_inputs) - print(f"Predictions of the trained network : {predictions}") - -.. code:: - - Predictions of the trained network : [0. 1. 1. 0.] - -Calculating Some Statistics ---------------------------- - -Based on the predictions the network made, some statistics can be -calculated such as the number of correct and wrong predictions in -addition to the classification accuracy. - -.. code:: python - - num_wrong = numpy.where(predictions != data_outputs)[0] - num_correct = data_outputs.size - num_wrong.size - accuracy = 100 * (num_correct/data_outputs.size) - print(f"Number of correct classifications : {num_correct}.") - print(f"Number of wrong classifications : {num_wrong.size}.") - print(f"Classification accuracy : {accuracy}.") - -.. code:: - - Number of correct classifications : 4 - print("Number of wrong classifications : 0 - Classification accuracy : 100 - -Examples -======== - -This section gives the complete code of some examples that build and -train neural networks using the genetic algorithm. Each subsection -builds a different network. - -XOR Classification ------------------- - -This example is discussed in the **Steps to Build and Train Neural -Networks using Genetic Algorithm** section that builds the XOR gate and -its complete code is listed below. - -.. code:: python - - import numpy - import pygad - import pygad.nn - import pygad.gann - - def fitness_func(ga_instance, solution, sol_idx): - global GANN_instance, data_inputs, data_outputs - - # If adaptive mutation is used, sometimes sol_idx is None. - if sol_idx == None: - sol_idx = 1 - - predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[sol_idx], - data_inputs=data_inputs) - correct_predictions = numpy.where(predictions == data_outputs)[0].size - solution_fitness = (correct_predictions/data_outputs.size)*100 - - return solution_fitness - - def callback_generation(ga_instance): - global GANN_instance, last_fitness - - population_matrices = pygad.gann.population_as_matrices(population_networks=GANN_instance.population_networks, - population_vectors=ga_instance.population) - - GANN_instance.update_population_trained_weights(population_trained_weights=population_matrices) - - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution()[1]}") - print(f"Change = {ga_instance.best_solution()[1] - last_fitness}") - - last_fitness = ga_instance.best_solution()[1].copy() - - # Holds the fitness value of the previous generation. - last_fitness = 0 - - # Preparing the NumPy array of the inputs. - data_inputs = numpy.array([[1, 1], - [1, 0], - [0, 1], - [0, 0]]) - - # Preparing the NumPy array of the outputs. - data_outputs = numpy.array([0, - 1, - 1, - 0]) - - # The length of the input vector for each sample (i.e. number of neurons in the input layer). - num_inputs = data_inputs.shape[1] - # The number of neurons in the output layer (i.e. number of classes). - num_classes = 2 - - # Creating an initial population of neural networks. The return of the initial_population() function holds references to the networks, not their weights. Using such references, the weights of all networks can be fetched. - num_solutions = 6 # A solution or a network can be used interchangeably. - GANN_instance = pygad.gann.GANN(num_solutions=num_solutions, - num_neurons_input=num_inputs, - num_neurons_hidden_layers=[2], - num_neurons_output=num_classes, - hidden_activations=["relu"], - output_activation="softmax") - - # population does not hold the numerical weights of the network instead it holds a list of references to each last layer of each network (i.e. solution) in the population. A solution or a network can be used interchangeably. - # If there is a population with 3 solutions (i.e. networks), then the population is a list with 3 elements. Each element is a reference to the last layer of each network. Using such a reference, all details of the network can be accessed. - population_vectors = pygad.gann.population_as_vectors(population_networks=GANN_instance.population_networks) - - # To prepare the initial population, there are 2 ways: - # 1) Prepare it yourself and pass it to the initial_population parameter. This way is useful when the user wants to start the genetic algorithm with a custom initial population. - # 2) Assign valid integer values to the sol_per_pop and num_genes parameters. If the initial_population parameter exists, then the sol_per_pop and num_genes parameters are useless. - initial_population = population_vectors.copy() - - num_parents_mating = 4 # Number of solutions to be selected as parents in the mating pool. - - num_generations = 500 # Number of generations. - - mutation_percent_genes = [5, 10] # Percentage of genes to mutate. This parameter has no action if the parameter mutation_num_genes exists. - - parent_selection_type = "sss" # Type of parent selection. - - crossover_type = "single_point" # Type of the crossover operator. - - mutation_type = "adaptive" # Type of the mutation operator. - - keep_parents = 1 # Number of parents to keep in the next population. -1 means keep all parents and 0 means keep nothing. - - init_range_low = -2 - init_range_high = 5 - - ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - mutation_percent_genes=mutation_percent_genes, - init_range_low=init_range_low, - init_range_high=init_range_high, - parent_selection_type=parent_selection_type, - crossover_type=crossover_type, - mutation_type=mutation_type, - keep_parents=keep_parents, - suppress_warnings=True, - on_generation=callback_generation) - - ga_instance.run() - - # After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. - ga_instance.plot_fitness() - - # Returning the details of the best solution. - solution, solution_fitness, solution_idx = ga_instance.best_solution() - print(f"Parameters of the best solution : {solution}") - print(f"Fitness value of the best solution = {solution_fitness}") - print(f"Index of the best solution : {solution_idx}") - - if ga_instance.best_solution_generation != -1: - print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") - - # Predicting the outputs of the data using the best solution. - predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[solution_idx], - data_inputs=data_inputs) - print(f"Predictions of the trained network : {predictions}") - - # Calculating some statistics - num_wrong = numpy.where(predictions != data_outputs)[0] - num_correct = data_outputs.size - num_wrong.size - accuracy = 100 * (num_correct/data_outputs.size) - print(f"Number of correct classifications : {num_correct}.") - print(f"Number of wrong classifications : {num_wrong.size}.") - print(f"Classification accuracy : {accuracy}.") - -Image Classification --------------------- - -In the documentation of the ``pygad.nn`` module, a neural network is -created for classifying images from the Fruits360 dataset without being -trained using an optimization algorithm. This section discusses how to -train such a classifier using the genetic algorithm with the help of the -``pygad.gann`` module. - -Please make sure that the training data files -`dataset_features.npy `__ -and -`outputs.npy `__ -are available. For downloading them, use these links: - -1. `dataset_features.npy `__: - The features - https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy - -2. `outputs.npy `__: - The class labels - https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy - -After the data is available, here is the complete code that builds and -trains a neural network using the genetic algorithm for classifying -images from 4 classes of the Fruits360 dataset. - -Because there are 4 classes, the output layer is assigned has 4 neurons -according to the ``num_neurons_output`` parameter of the -``pygad.gann.GANN`` class constructor. - -.. code:: python - - import numpy - import pygad - import pygad.nn - import pygad.gann - - def fitness_func(ga_instance, solution, sol_idx): - global GANN_instance, data_inputs, data_outputs - - predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[sol_idx], - data_inputs=data_inputs) - correct_predictions = numpy.where(predictions == data_outputs)[0].size - solution_fitness = (correct_predictions/data_outputs.size)*100 - - return solution_fitness - - def callback_generation(ga_instance): - global GANN_instance, last_fitness - - population_matrices = pygad.gann.population_as_matrices(population_networks=GANN_instance.population_networks, - population_vectors=ga_instance.population) - - GANN_instance.update_population_trained_weights(population_trained_weights=population_matrices) - - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution()[1]}") - print(f"Change = {ga_instance.best_solution()[1] - last_fitness}") - - last_fitness = ga_instance.best_solution()[1].copy() - - # Holds the fitness value of the previous generation. - last_fitness = 0 - - # Reading the input data. - data_inputs = numpy.load("dataset_features.npy") # Download from https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy - - # Optional step of filtering the input data using the standard deviation. - features_STDs = numpy.std(a=data_inputs, axis=0) - data_inputs = data_inputs[:, features_STDs>50] - - # Reading the output data. - data_outputs = numpy.load("outputs.npy") # Download from https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy - - # The length of the input vector for each sample (i.e. number of neurons in the input layer). - num_inputs = data_inputs.shape[1] - # The number of neurons in the output layer (i.e. number of classes). - num_classes = 4 - - # Creating an initial population of neural networks. The return of the initial_population() function holds references to the networks, not their weights. Using such references, the weights of all networks can be fetched. - num_solutions = 8 # A solution or a network can be used interchangeably. - GANN_instance = pygad.gann.GANN(num_solutions=num_solutions, - num_neurons_input=num_inputs, - num_neurons_hidden_layers=[150, 50], - num_neurons_output=num_classes, - hidden_activations=["relu", "relu"], - output_activation="softmax") - - # population does not hold the numerical weights of the network instead it holds a list of references to each last layer of each network (i.e. solution) in the population. A solution or a network can be used interchangeably. - # If there is a population with 3 solutions (i.e. networks), then the population is a list with 3 elements. Each element is a reference to the last layer of each network. Using such a reference, all details of the network can be accessed. - population_vectors = pygad.gann.population_as_vectors(population_networks=GANN_instance.population_networks) - - # To prepare the initial population, there are 2 ways: - # 1) Prepare it yourself and pass it to the initial_population parameter. This way is useful when the user wants to start the genetic algorithm with a custom initial population. - # 2) Assign valid integer values to the sol_per_pop and num_genes parameters. If the initial_population parameter exists, then the sol_per_pop and num_genes parameters are useless. - initial_population = population_vectors.copy() - - num_parents_mating = 4 # Number of solutions to be selected as parents in the mating pool. - - num_generations = 500 # Number of generations. - - mutation_percent_genes = 10 # Percentage of genes to mutate. This parameter has no action if the parameter mutation_num_genes exists. - - parent_selection_type = "sss" # Type of parent selection. - - crossover_type = "single_point" # Type of the crossover operator. - - mutation_type = "random" # Type of the mutation operator. - - keep_parents = -1 # Number of parents to keep in the next population. -1 means keep all parents and 0 means keep nothing. - - ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - mutation_percent_genes=mutation_percent_genes, - parent_selection_type=parent_selection_type, - crossover_type=crossover_type, - mutation_type=mutation_type, - keep_parents=keep_parents, - on_generation=callback_generation) - - ga_instance.run() - - # After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. - ga_instance.plot_fitness() - - # Returning the details of the best solution. - solution, solution_fitness, solution_idx = ga_instance.best_solution() - print(f"Parameters of the best solution : {solution}") - print(f"Fitness value of the best solution = {solution_fitness}") - print(f"Index of the best solution : {solution_idx}") - - if ga_instance.best_solution_generation != -1: - print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") - - # Predicting the outputs of the data using the best solution. - predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[solution_idx], - data_inputs=data_inputs) - print(f"Predictions of the trained network : {predictions}") - - # Calculating some statistics - num_wrong = numpy.where(predictions != data_outputs)[0] - num_correct = data_outputs.size - num_wrong.size - accuracy = 100 * (num_correct/data_outputs.size) - print(f"Number of correct classifications : {num_correct}.") - print(f"Number of wrong classifications : {num_wrong.size}.") - print(f"Classification accuracy : {accuracy}.") - -After training completes, here are the outputs of the print statements. -The number of wrong classifications is only 1 and the accuracy is -99.949%. This accuracy is reached after 482 generations. - -.. code:: - - Fitness value of the best solution = 99.94903160040775 - Index of the best solution : 0 - Best fitness value reached after 482 generations. - Number of correct classifications : 1961. - Number of wrong classifications : 1. - Classification accuracy : 99.94903160040775. - -The next figure shows how fitness value evolves by generation. - -.. image:: https://user-images.githubusercontent.com/16560492/82152993-21898180-9865-11ea-8387-b995f88b83f7.png - :alt: - -Regression Example 1 --------------------- - -To train a neural network for regression, follow these instructions: - -1. Set the ``output_activation`` parameter in the constructor of the - ``pygad.gann.GANN`` class to ``"None"``. It is possible to use the - ReLU function if all outputs are nonnegative. - -.. code:: python - - GANN_instance = pygad.gann.GANN(... - output_activation="None") - -1. Wherever the ``pygad.nn.predict()`` function is used, set the - ``problem_type`` parameter to ``"regression"``. - -.. code:: python - - predictions = pygad.nn.predict(..., - problem_type="regression") - -1. Design the fitness function to calculate the error (e.g. mean - absolute error). - -.. code:: python - - def fitness_func(ga_instance, solution, sol_idx): - ... - - predictions = pygad.nn.predict(..., - problem_type="regression") - - solution_fitness = 1.0/numpy.mean(numpy.abs(predictions - data_outputs)) - - return solution_fitness - -The next code builds a complete example for building a neural network -for regression. - -.. code:: python - - import numpy - import pygad - import pygad.nn - import pygad.gann - - def fitness_func(ga_instance, solution, sol_idx): - global GANN_instance, data_inputs, data_outputs - - predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[sol_idx], - data_inputs=data_inputs, problem_type="regression") - solution_fitness = 1.0/numpy.mean(numpy.abs(predictions - data_outputs)) - - return solution_fitness - - def callback_generation(ga_instance): - global GANN_instance, last_fitness - - population_matrices = pygad.gann.population_as_matrices(population_networks=GANN_instance.population_networks, - population_vectors=ga_instance.population) - - GANN_instance.update_population_trained_weights(population_trained_weights=population_matrices) - - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1]}") - print(f"Change = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1] - last_fitness}") - - last_fitness = ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1].copy() - - # Holds the fitness value of the previous generation. - last_fitness = 0 - - # Preparing the NumPy array of the inputs. - data_inputs = numpy.array([[2, 5, -3, 0.1], - [8, 15, 20, 13]]) - - # Preparing the NumPy array of the outputs. - data_outputs = numpy.array([[0.1, 0.2], - [1.8, 1.5]]) - - # The length of the input vector for each sample (i.e. number of neurons in the input layer). - num_inputs = data_inputs.shape[1] - - # Creating an initial population of neural networks. The return of the initial_population() function holds references to the networks, not their weights. Using such references, the weights of all networks can be fetched. - num_solutions = 6 # A solution or a network can be used interchangeably. - GANN_instance = pygad.gann.GANN(num_solutions=num_solutions, - num_neurons_input=num_inputs, - num_neurons_hidden_layers=[2], - num_neurons_output=2, - hidden_activations=["relu"], - output_activation="None") - - # population does not hold the numerical weights of the network instead it holds a list of references to each last layer of each network (i.e. solution) in the population. A solution or a network can be used interchangeably. - # If there is a population with 3 solutions (i.e. networks), then the population is a list with 3 elements. Each element is a reference to the last layer of each network. Using such a reference, all details of the network can be accessed. - population_vectors = pygad.gann.population_as_vectors(population_networks=GANN_instance.population_networks) - - # To prepare the initial population, there are 2 ways: - # 1) Prepare it yourself and pass it to the initial_population parameter. This way is useful when the user wants to start the genetic algorithm with a custom initial population. - # 2) Assign valid integer values to the sol_per_pop and num_genes parameters. If the initial_population parameter exists, then the sol_per_pop and num_genes parameters are useless. - initial_population = population_vectors.copy() - - num_parents_mating = 4 # Number of solutions to be selected as parents in the mating pool. - - num_generations = 500 # Number of generations. - - mutation_percent_genes = 5 # Percentage of genes to mutate. This parameter has no action if the parameter mutation_num_genes exists. - - parent_selection_type = "sss" # Type of parent selection. - - crossover_type = "single_point" # Type of the crossover operator. - - mutation_type = "random" # Type of the mutation operator. - - keep_parents = 1 # Number of parents to keep in the next population. -1 means keep all parents and 0 means keep nothing. - - init_range_low = -1 - init_range_high = 1 - - ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - mutation_percent_genes=mutation_percent_genes, - init_range_low=init_range_low, - init_range_high=init_range_high, - parent_selection_type=parent_selection_type, - crossover_type=crossover_type, - mutation_type=mutation_type, - keep_parents=keep_parents, - on_generation=callback_generation) - - ga_instance.run() - - # After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. - ga_instance.plot_fitness() - - # Returning the details of the best solution. - solution, solution_fitness, solution_idx = ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness) - print(f"Parameters of the best solution : {solution}") - print(f"Fitness value of the best solution = {solution_fitness}") - print(f"Index of the best solution : {solution_idx}") - - if ga_instance.best_solution_generation != -1: - print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") - - # Predicting the outputs of the data using the best solution. - predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[solution_idx], - data_inputs=data_inputs, - problem_type="regression") - print(f"Predictions of the trained network : {predictions}") - - # Calculating some statistics - abs_error = numpy.mean(numpy.abs(predictions - data_outputs)) - print(f"Absolute error : {abs_error}.") - -The next figure shows how the fitness value changes for the generations -used. - -.. image:: https://user-images.githubusercontent.com/16560492/92948154-3cf24b00-f459-11ea-94ea-952b66ab2145.png - :alt: - -Regression Example 2 - Fish Weight Prediction ---------------------------------------------- - -This example uses the Fish Market Dataset available at Kaggle -(https://www.kaggle.com/aungpyaeap/fish-market). Simply download the CSV -dataset from `this -link `__ -(https://www.kaggle.com/aungpyaeap/fish-market/download). The dataset is -also available at the `GitHub project of the pygad.gann -module `__: -https://github.com/ahmedfgad/NeuralGenetic - -Using the Pandas library, the dataset is read using the ``read_csv()`` -function. - -.. code:: python - - data = numpy.array(pandas.read_csv("Fish.csv")) - -The last 5 columns in the dataset are used as inputs and the **Weight** -column is used as output. - -.. code:: python - - # Preparing the NumPy array of the inputs. - data_inputs = numpy.asarray(data[:, 2:], dtype=numpy.float32) - - # Preparing the NumPy array of the outputs. - data_outputs = numpy.asarray(data[:, 1], dtype=numpy.float32) # Fish Weight - -Note how the activation function at the last layer is set to ``"None"``. -Moreover, the ``problem_type`` parameter in the ``pygad.nn.train()`` and -``pygad.nn.predict()`` functions is set to ``"regression"``. Remember to -design an appropriate fitness function for the regression problem. In -this example, the fitness value is calculated based on the mean absolute -error. - -.. code:: python - - solution_fitness = 1.0/numpy.mean(numpy.abs(predictions - data_outputs)) - -Here is the complete code. - -.. code:: python - - import numpy - import pygad - import pygad.nn - import pygad.gann - import pandas - - def fitness_func(ga_instance, solution, sol_idx): - global GANN_instance, data_inputs, data_outputs - - predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[sol_idx], - data_inputs=data_inputs, problem_type="regression") - solution_fitness = 1.0/numpy.mean(numpy.abs(predictions - data_outputs)) - - return solution_fitness - - def callback_generation(ga_instance): - global GANN_instance, last_fitness - - population_matrices = pygad.gann.population_as_matrices(population_networks=GANN_instance.population_networks, - population_vectors=ga_instance.population) - - GANN_instance.update_population_trained_weights(population_trained_weights=population_matrices) - - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1]}") - print(f"Change = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1] - last_fitness}") - - last_fitness = ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1].copy() - - # Holds the fitness value of the previous generation. - last_fitness = 0 - - data = numpy.array(pandas.read_csv("../data/Fish.csv")) - - # Preparing the NumPy array of the inputs. - data_inputs = numpy.asarray(data[:, 2:], dtype=numpy.float32) - - # Preparing the NumPy array of the outputs. - data_outputs = numpy.asarray(data[:, 1], dtype=numpy.float32) - - # The length of the input vector for each sample (i.e. number of neurons in the input layer). - num_inputs = data_inputs.shape[1] - - # Creating an initial population of neural networks. The return of the initial_population() function holds references to the networks, not their weights. Using such references, the weights of all networks can be fetched. - num_solutions = 6 # A solution or a network can be used interchangeably. - GANN_instance = pygad.gann.GANN(num_solutions=num_solutions, - num_neurons_input=num_inputs, - num_neurons_hidden_layers=[2], - num_neurons_output=1, - hidden_activations=["relu"], - output_activation="None") - - # population does not hold the numerical weights of the network instead it holds a list of references to each last layer of each network (i.e. solution) in the population. A solution or a network can be used interchangeably. - # If there is a population with 3 solutions (i.e. networks), then the population is a list with 3 elements. Each element is a reference to the last layer of each network. Using such a reference, all details of the network can be accessed. - population_vectors = pygad.gann.population_as_vectors(population_networks=GANN_instance.population_networks) - - # To prepare the initial population, there are 2 ways: - # 1) Prepare it yourself and pass it to the initial_population parameter. This way is useful when the user wants to start the genetic algorithm with a custom initial population. - # 2) Assign valid integer values to the sol_per_pop and num_genes parameters. If the initial_population parameter exists, then the sol_per_pop and num_genes parameters are useless. - initial_population = population_vectors.copy() - - num_parents_mating = 4 # Number of solutions to be selected as parents in the mating pool. - - num_generations = 500 # Number of generations. - - mutation_percent_genes = 5 # Percentage of genes to mutate. This parameter has no action if the parameter mutation_num_genes exists. - - parent_selection_type = "sss" # Type of parent selection. - - crossover_type = "single_point" # Type of the crossover operator. - - mutation_type = "random" # Type of the mutation operator. - - keep_parents = 1 # Number of parents to keep in the next population. -1 means keep all parents and 0 means keep nothing. - - init_range_low = -1 - init_range_high = 1 - - ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - mutation_percent_genes=mutation_percent_genes, - init_range_low=init_range_low, - init_range_high=init_range_high, - parent_selection_type=parent_selection_type, - crossover_type=crossover_type, - mutation_type=mutation_type, - keep_parents=keep_parents, - on_generation=callback_generation) - - ga_instance.run() - - # After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. - ga_instance.plot_fitness() - - # Returning the details of the best solution. - solution, solution_fitness, solution_idx = ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness) - print(f"Parameters of the best solution : {solution}") - print(f"Fitness value of the best solution = {solution_fitness}") - print(f"Index of the best solution : {solution_idx}") - - if ga_instance.best_solution_generation != -1: - print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") - - # Predicting the outputs of the data using the best solution. - predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[solution_idx], - data_inputs=data_inputs, - problem_type="regression") - print(f"Predictions of the trained network : {predictions}") - - # Calculating some statistics - abs_error = numpy.mean(numpy.abs(predictions - data_outputs)) - print(f"Absolute error : {abs_error}.") - -The next figure shows how the fitness value changes for the 500 -generations used. - -.. image:: https://user-images.githubusercontent.com/16560492/92948486-bbe78380-f459-11ea-9e31-0d4c7269d606.png - :alt: diff --git a/docs/md/helper.md b/docs/source/helper.md similarity index 100% rename from docs/md/helper.md rename to docs/source/helper.md diff --git a/docs/source/helper.rst b/docs/source/helper.rst deleted file mode 100644 index dd4a7ed..0000000 --- a/docs/source/helper.rst +++ /dev/null @@ -1,114 +0,0 @@ -.. _`pygadhelper`-module: - -``pygad.helper`` Module -======================= - -This section of the PyGAD's library documentation discusses the -``pygad.helper`` module. - -The ``pygad.helper`` module has 2 submodules: - -1. ``pygad.helper.unique``: A module of methods for creating unique - genes. - -2. ``pygad.helper.misc``: A module of miscellaneous helper methods. - -.. _pygadhelperunique-module: - -``pygad.helper.unique`` Module ------------------------------- - -The ``pygad.helper.unique`` module has a class named ``Unique`` with the -following helper methods. Such methods help to check and fix duplicate -values in the genes of a solution. - -1. ``solve_duplicate_genes_randomly()``: Solves the duplicates in a - solution by randomly selecting new values for the duplicating genes. - -2. ``solve_duplicate_genes_by_space()``: Solves the duplicates in a - solution by selecting values for the duplicating genes from the gene - space - -3. ``unique_int_gene_from_range()``: Finds a unique integer value for - the gene out of a range defined by start and end points. - -4. ``unique_float_gene_from_range()``: Finds a unique float value for - the gene out of a range defined by start and end points. - -5. ``select_unique_value()``: Selects a unique value (if possible) from - a list of gene values. - -6. ``unique_genes_by_space()``: Loops through all the duplicating genes - to find unique values that from their gene spaces to solve the - duplicates. For each duplicating gene, a call to the - ``unique_gene_by_space()`` is made. - -7. ``unique_gene_by_space()``: Returns a unique gene value for a single - gene based on its value space to solve the duplicates. - -8. ``find_two_duplicates()``: Identifies the first occurrence of a - duplicate gene in the solution. - -9. ``unpack_gene_space()``: Unpacks the gene space for selecting a - value to resolve duplicates by converting ranges into lists of - values. - -10. ``solve_duplicates_deeply()``: Sometimes it is impossible to solve - the duplicate genes by simply randomly selecting another value for - either genes. This function solve the duplicates between 2 genes by - searching for a third gene that can make assist in the solution. - -.. _pygadhelpermisc-module: - -``pygad.helper.misc`` Module ----------------------------- - -The ``pygad.helper.misc`` module has a class called ``Helper`` with some -methods to help in different stages of the GA pipeline. It is introduced -in `PyGAD -3.5.0 `__. - -1. ``change_population_dtype_and_round()``: For each gene in the - population, round the gene value and change the data type. - -2. ``change_gene_dtype_and_round()``: Round the change the data type of - a single gene. - -3. ``mutation_change_gene_dtype_and_round()``: Decides whether mutation - is done by replacement or not. Then it rounds and change the data - type of the new gene value. - -4. ``validate_gene_constraint_callable_output()``: Validates the output - of the user-defined callable/function that checks whether the gene - constraint defined in the ``gene_constraint`` parameter is satisfied - or not. - -5. ``get_gene_dtype()``: Returns the gene data type from the - ``gene_type`` instance attribute. - -6. ``get_random_mutation_range()``: Returns the random mutation range - using the ``random_mutation_min_val`` and - ``random_mutation_min_val`` instance attributes. - -7. ``get_initial_population_range()``: Returns the initial population - values range using the ``init_range_low`` and ``init_range_high`` - instance attributes. - -8. ``generate_gene_value_from_space()``: Generates/selects a value for - a gene using the ``gene_space`` instance attribute. - -9. ``generate_gene_value_randomly()``: Generates a random value for the - gene. Only used if ``gene_space`` is ``None``. - -10. ``generate_gene_value()``: Generates a value for the gene. It checks - whether ``gene_space`` is ``None`` and calls either - ``generate_gene_value_randomly()`` or - ``generate_gene_value_from_space()``. - -11. ``filter_gene_values_by_constraint()``: Receives a list of values - for a gene. Then it filters such values using the gene constraint. - -12. ``get_valid_gene_constraint_values()``: Selects one valid gene value - that satisfy the gene constraint. It simply calls - ``generate_gene_value()`` to generate some gene values then it - filters such values using ``filter_gene_values_by_constraint()``. diff --git a/docs/source/images/crossover_types.png b/docs/source/images/crossover_types.png new file mode 100644 index 0000000000000000000000000000000000000000..502c6b07116ee477921031d9649a3822f01e59e5 GIT binary patch literal 225770 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%0ArR0^) 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 z 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 c7zKVr)hvznY`bTZYp;)sfoTM|~#FmRuQ=;Y>Z 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 literal 0 HcmV?d00001 diff --git a/docs/source/images/crossover_types.svg b/docs/source/images/crossover_types.svg new file mode 100644 index 0000000..a40980a --- /dev/null +++ b/docs/source/images/crossover_types.svg @@ -0,0 +1,33 @@ + + +Crossover types +a = gene from Parent 1, b = gene from Parent 2 +1. Single-point crossover +Parent 1 +Parent 2 +Child +a1a2a3a4a5a6a7a8 +b1b2b3b4b5b6b7b8 +a1a2a3a4b5b6b7b8 + + +2. Two-point crossover +Parent 1 +Parent 2 +Child +a1a2a3a4a5a6a7a8 +b1b2b3b4b5b6b7b8 +a1a2b3b4b5a6a7a8 + + + + +3. Uniform crossover +Parent 1 +Parent 2 +Child +a1a2a3a4a5a6a7a8 +b1b2b3b4b5b6b7b8 +a1b2a3b4b5a6b7a8 +each gene at random + \ No newline at end of file diff --git a/docs/source/images/ga_lifecycle.png b/docs/source/images/ga_lifecycle.png new file mode 100644 index 0000000000000000000000000000000000000000..4d03969e08424b3a90afeb449a6ea71a00759dcc GIT binary patch literal 136091 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 + + + + + + + + + + + + + + The PyGAD genetic algorithm life cycle + + + + + + + + + + + + + + + yes + no + + repeat for each generation + + + + + Create the initial population + + + + Calculate the fitness of every solution + + + + Stop criteria met? + + + + Return the + best solution + + + + Select the parents + + + + Apply crossover to make offspring + + + + Apply mutation to the offspring + + + + Build the next generation + keep elites or parents, then add the offspring + diff --git a/docs/source/images/mutation.png b/docs/source/images/mutation.png new file mode 100644 index 0000000000000000000000000000000000000000..041f6fb826039bcbdab895f5239eac2054d08000 GIT binary patch literal 84083 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�q|&M{V#0m@6pMNu`xa@x;jI%Wo0}bZI66$mD)Y=1}t8&d`j}kCF^bG z^sE*Cw#Y0XPTqgYvsgQ%#&$kYrHsd?jXq)c7dDnVet!GLLIUSv)1R9qb-)FWZ@9<8 z%%_;}2h+PFgA_`>CeIG$jcZQ^Q;vNCH4fsr9nt2^%qxF6;_sfoj{B!~^dg)mr6#+b zyfRuk3+rJFoS+Zk1I1H})&0H)94xObICZceZ!Mu`?#TkafMR#-JPm1H^l672`M356 zUAoiOTcW~(m?>FZk3;rYyDa3NOqgIq69A>L*Hh6eq+~Q0@Hv(;p+Zl=OxcE z-f)yFS7kbTPJMNm;7xU2^60?GnRSzn-z8K&bok_!U0i>2@gf|XK5#tE6CS-8i!&coTK$`U#K-q^$}rzas(HIs z+nO0nN`g_k_zx%WgagYZcl>?@$Q^tKCfwk#IirNg_zude%6C6 zWP3k9Q5Q5&g%aKoNcnO%7Jj|+$8AAiPc2#K_-0qmFtt|q-@m_@Ouej^_g$VP)}}v5 zhDk9Hj-Th$jrA|7R26-RE3+Jkt zBq#;QMwzeH6bsW)TW@QA@Iq)$1C}s>2cwN-$NfEqi_JVLlK%cDs>x1O8|{5SPF|i& z=cEwG1aa@zFxphGXyyRKb3V?BNJ}D^fM=J~3H^@L)FHWExX2Ka;2`Ayi=Fd(*TqNw9Y9vj4gGU-q3>81;iFES}4(K z`1#-~<3T~_LJKNlywpWIaVr4umw9##<}Y2$;OuT2uoY)J8#l9uDi3zX#Y8Ko$1Zg{ z-groku(hB)GQ)+ytr`nCicsU8)2Q)UZ*3Pyg`{D{4d7p#2;{`WJZ@WZK2(I2&8zD7 zxQq1R26NkqI-B%TC`D+~o^ITA_QLiR;!%4S5WSt5I78!4szurr47H~j5kJew6}6)h zI?7(}N&YxqDWP(;m?9~_X!oxL-!Ka*5Vr+kKGcH`LyrcxqmS+rJ-}-WJG-bCBk{FB2By@+qs$m_O znW?X8lq#to$z*UH&j*rCap?D#%?%betY}D})MW-z=Z^AGC`gBiSj3|dO`%+WN}Gyf zndH) XA}56*8CRnI{G|R&=V{TCmv8?cU`xxU literal 0 HcmV?d00001 diff --git a/docs/source/images/mutation.svg b/docs/source/images/mutation.svg new file mode 100644 index 0000000..5dc53f8 --- /dev/null +++ b/docs/source/images/mutation.svg @@ -0,0 +1,26 @@ + + + +Mutation: change a few genes at random +Before +After +2.1 +2.1 +-0.5 +-0.5 +3.0 +0.7 + +1.2 +1.2 +-2.4 +-2.4 +0.8 +-3.6 + +4.3 +4.3 +-1.1 +-1.1 +Only the highlighted genes change. Mutation explores new values so the search does not get stuck. + \ No newline at end of file diff --git a/docs/source/images/offspring_decision_tree.png b/docs/source/images/offspring_decision_tree.png new file mode 100644 index 0000000000000000000000000000000000000000..9f9cd2d21c3bdfc3859fd81077acc229ce779ebf GIT binary patch literal 151102 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%0A*B9IRw#7x`P0rK9r? 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 aEvNk*iWC%Lw)9nW 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 literal 0 HcmV?d00001 diff --git a/docs/source/images/offspring_decision_tree.svg b/docs/source/images/offspring_decision_tree.svg new file mode 100644 index 0000000..23431b3 --- /dev/null +++ b/docs/source/images/offspring_decision_tree.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + How is the number of offspring decided? + + + + + + + + + + + + + + + + + + yes + + no + + −1 + + 0 + + > 0 + + + + + + + keep_elitism > 0 ? + + + + keep_parents = ? + + + + + Keep Ke elites + + kept = Ke + offspring = N − Ke + + keep_parents is ignored + + + + Keep all parents + + kept = P + offspring = N − P + + every parent is kept + + + + Keep no parents + + kept = 0 + offspring = N + + a whole new generation + + + + Keep Kp parents + + kept = Kp + offspring = N − Kp + + best Kp parents are kept + + + + + N = sol_per_pop + Ke = keep_elitism + Kp = keep_parents + P = num_parents_mating + + diff --git a/docs/source/images/population_assembly.png b/docs/source/images/population_assembly.png new file mode 100644 index 0000000000000000000000000000000000000000..4057a2a9ee1d4e14d11bd44f48df87da96d33f86 GIT binary patch literal 99846 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

;{1wjk~ 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 literal 0 HcmV?d00001 diff --git a/docs/source/images/population_assembly.svg b/docs/source/images/population_assembly.svg new file mode 100644 index 0000000..4231397 --- /dev/null +++ b/docs/source/images/population_assembly.svg @@ -0,0 +1,62 @@ + + + + + + How the next population is built + example: sol_per_pop = 8, kept = 2 + + + + + + + 0 + + elite / parent + + 1 + + elite / parent + + + 2 + + offspring + + 3 + + offspring + + 4 + + offspring + + 5 + + offspring + + 6 + + offspring + + 7 + + offspring + + + + + + Kept solutions (Ke or Kp) + copied to the top, unchanged + + + + Offspring = N − kept + created by crossover, then mutation + + + + The number kept comes from the keep_elitism / keep_parents decision tree. + diff --git a/docs/md/HEADER.md b/docs/source/index.md similarity index 54% rename from docs/md/HEADER.md rename to docs/source/index.md index 195b397..90807f0 100644 --- a/docs/md/HEADER.md +++ b/docs/source/index.md @@ -1,62 +1,64 @@ -[PyGAD](https://github.com/ahmedfgad/GeneticAlgorithmPython) is an open-source Python library for building the genetic algorithm and optimizing machine learning algorithms. It works with [Keras](https://keras.io) and [PyTorch](https://pytorch.org). +# PyGAD - Python Genetic Algorithm! -> Try the [Optimization Gadget](https://optimgadget.com), a free cloud-based tool powered by PyGAD. It simplifies optimization by reducing or eliminating the need for coding while providing insightful visualizations. +[PyGAD](https://github.com/ahmedfgad/GeneticAlgorithmPython) is an open-source Python library for building the genetic algorithm and optimizing machine learning algorithms. It works with [Keras](https://keras.io) and [PyTorch](https://pytorch.org). -[PyGAD](https://github.com/ahmedfgad/GeneticAlgorithmPython) supports different types of crossover, mutation, and parent selection operators. [PyGAD](https://github.com/ahmedfgad/GeneticAlgorithmPython) allows different types of problems to be optimized using the genetic algorithm by customizing the fitness function. It works with both single-objective and multi-objective optimization problems. +> Try the [Optimization Gadget](https://optimgadget.com), a free cloud-based tool powered by PyGAD. It makes optimization easier by reducing or removing the need for coding, and it shows helpful visualizations. + +[PyGAD](https://github.com/ahmedfgad/GeneticAlgorithmPython) supports different types of crossover, mutation, and parent selection operators. It lets you optimize many types of problems with the genetic algorithm by writing your own fitness function. It works with both single-objective and multi-objective optimization problems. ![PYGAD-LOGO](https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png) *Logo designed by [Asmaa Kabil](https://www.linkedin.com/in/asmaa-kabil-9901b7b6)* -Besides building the genetic algorithm, it builds and optimizes machine learning algorithms. Currently, [PyGAD](https://pypi.org/project/pygad) supports building and training (using genetic algorithm) artificial neural networks for classification problems. +Besides building the genetic algorithm, PyGAD builds and optimizes machine learning algorithms. At the moment, [PyGAD](https://pypi.org/project/pygad) supports building and training (using the genetic algorithm) artificial neural networks for classification problems. -The library is under active development and more features added regularly. Please contact us if you want a feature to be supported. +The library is under active development, and new features are added often. Please contact us if you want a feature to be supported. # Donation & Support -You can donate to PyGAD via: +You can donate to PyGAD through: - [Credit/Debit Card](https://donate.stripe.com/eVa5kO866elKgM0144): https://donate.stripe.com/eVa5kO866elKgM0144 - [Open Collective](https://opencollective.com/pygad): [opencollective.com/pygad](https://opencollective.com/pygad) -- PayPal: Use either this link: [paypal.me/ahmedfgad](https://paypal.me/ahmedfgad) or the e-mail address ahmed.f.gad@gmail.com -- Interac e-Transfer: Use e-mail address ahmed.f.gad@gmail.com +- PayPal: Use either this link [paypal.me/ahmedfgad](https://paypal.me/ahmedfgad) or the e-mail address ahmed.f.gad@gmail.com +- Interac e-Transfer: Use the e-mail address ahmed.f.gad@gmail.com - Buy a product at [Teespring](https://pygad.creator-spring.com/): [pygad.creator-spring.com](https://pygad.creator-spring.com) # Installation -To install [PyGAD](https://pypi.org/project/pygad), simply use pip to download and install the library from [PyPI](https://pypi.org/project/pygad) (Python Package Index). The library lives a PyPI at this page https://pypi.org/project/pygad. +To install [PyGAD](https://pypi.org/project/pygad), use pip to download and install the library from [PyPI](https://pypi.org/project/pygad) (Python Package Index). The library is available on PyPI at this page: https://pypi.org/project/pygad. Install PyGAD with the following command: -```python +``` pip3 install pygad ``` # Quick Start -To get started with [PyGAD](https://pypi.org/project/pygad), simply import it. +To get started with [PyGAD](https://pypi.org/project/pygad), import it. ```python import pygad ``` -Using [PyGAD](https://pypi.org/project/pygad), a wide range of problems can be optimized. A quick and simple problem to be optimized using the [PyGAD](https://pypi.org/project/pygad) is finding the best set of weights that satisfy the following function: +[PyGAD](https://pypi.org/project/pygad) can optimize a wide range of problems. As a quick and simple example, let us find the best set of weights that satisfy the following function: ``` y = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + w6x6 where (x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7) and y=44 ``` -The first step is to prepare the inputs and the outputs of this equation. +The first step is to prepare the inputs and the output of this equation. ```python function_inputs = [4,-2,3.5,5,-11,-4.7] desired_output = 44 ``` -A very important step is to implement the fitness function that will be used for calculating the fitness value for each solution. Here is one. +The next step is to write the fitness function that calculates a fitness value for each solution. Here is one example. -If the fitness function returns a number, then the problem is single-objective. If a `list`, `tuple`, or `numpy.ndarray` is returned, then it is a multi-objective problem (applicable even if a single element exists). +If the fitness function returns a number, then the problem is single-objective. If it returns a `list`, `tuple`, or `numpy.ndarray`, then it is a multi-objective problem (even if it has a single element). ```python def fitness_func(ga_instance, solution, solution_idx): @@ -65,7 +67,7 @@ def fitness_func(ga_instance, solution, solution_idx): return fitness ``` -Next is to prepare the parameters of [PyGAD](https://pypi.org/project/pygad). Here is an example for a set of parameters. +Next, prepare the parameters of [PyGAD](https://pypi.org/project/pygad). Here is an example set of parameters. ```python fitness_function = fitness_func @@ -88,7 +90,7 @@ mutation_type = "random" mutation_percent_genes = 10 ``` -After the parameters are prepared, an instance of the **pygad.GA** class is created. +After the parameters are ready, create an instance of the **pygad.GA** class. ```python ga_instance = pygad.GA(num_generations=num_generations, @@ -105,13 +107,13 @@ ga_instance = pygad.GA(num_generations=num_generations, mutation_percent_genes=mutation_percent_genes) ``` -After creating the instance, the `run()` method is called to start the optimization. +After creating the instance, call the `run()` method to start the optimization. ```python ga_instance.run() ``` -After the `run()` method completes, information about the best solution found by PyGAD can be accessed. +After the `run()` method completes, you can access information about the best solution found by PyGAD. ```python solution, solution_fitness, solution_idx = ga_instance.best_solution() @@ -128,24 +130,24 @@ Fitness value of the best solution = 157.37320042925006 Predicted output based on the best solution : 44.00635432206546 ``` -There is more to do using PyGAD. Read its documentation to explore the features of PyGAD. +There is much more you can do with PyGAD. Read the documentation to explore its features. # PyGAD's Modules [PyGAD](https://pypi.org/project/pygad) has the following modules: -1. The main module has the same name as the library `pygad` which is the main interface to build the genetic algorithm. -2. The `nn` module builds artificial neural networks. +1. The main module has the same name as the library, `pygad`. It is the main interface to build the genetic algorithm. +2. The `nn` module builds artificial neural networks. 3. The `gann` module optimizes neural networks (for classification and regression) using the genetic algorithm. 4. The `cnn` module builds convolutional neural networks. 5. The `gacnn` module optimizes convolutional neural networks using the genetic algorithm. -6. The `kerasga` module to train [Keras](https://keras.io) models using the genetic algorithm. -7. The `torchga` module to train [PyTorch](https://pytorch.org) models using the genetic algorithm. -8. The `visualize` module to visualize the results. -9. The `utils` module contains the operators (crossover, mutation, and parent selection) and the NSGA-II code. -10. The `helper` module has some helper functions. +6. The `kerasga` module trains [Keras](https://keras.io) models using the genetic algorithm. +7. The `torchga` module trains [PyTorch](https://pytorch.org) models using the genetic algorithm. +8. The `visualize` module visualizes the results. +9. The `utils` module holds the operators (crossover, mutation, and parent selection) and the NSGA-II code. +10. The `helper` module has some helper functions. -The documentation discusses these modules. +The documentation explains these modules. # PyGAD Citation - Bibtex Formatted @@ -162,3 +164,44 @@ If you used PyGAD, please consider citing its paper with the following details: } ``` +```{toctree} +:maxdepth: 1 +:caption: Genetic Algorithm + +pygad +pygad_more +``` + +```{toctree} +:maxdepth: 1 +:caption: Operators & Visualization + +utils +visualize +helper +``` + +```{toctree} +:maxdepth: 1 +:caption: Neural Networks + +nn +gann +cnn +gacnn +``` + +```{toctree} +:maxdepth: 1 +:caption: Keras & PyTorch + +kerasga +torchga +``` + +```{toctree} +:maxdepth: 1 +:caption: Releases + +releases +``` diff --git a/docs/source/index.rst b/docs/source/index.rst deleted file mode 100644 index b90ee31..0000000 --- a/docs/source/index.rst +++ /dev/null @@ -1,429 +0,0 @@ -.. PyGAD documentation master file, created by - sphinx-quickstart on Sat May 16 15:14:25 2020. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - - - - - -PyGAD - Python Genetic Algorithm! -================================= - -`PyGAD `__ is an -open-source Python library for building the genetic algorithm and -optimizing machine learning algorithms. It works with -`Keras `__ and `PyTorch `__. - - Try the `Optimization Gadget `__, a free - cloud-based tool powered by PyGAD. It simplifies optimization by - reducing or eliminating the need for coding while providing - insightful visualizations. - -`PyGAD `__ supports -different types of crossover, mutation, and parent selection operators. -`PyGAD `__ allows -different types of problems to be optimized using the genetic algorithm -by customizing the fitness function. It works with both single-objective -and multi-objective optimization problems. - -.. image:: https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png - :alt: - -*Logo designed by* `Asmaa -Kabil `__ - -Besides building the genetic algorithm, it builds and optimizes machine -learning algorithms. Currently, -`PyGAD `__ supports building and -training (using genetic algorithm) artificial neural networks for -classification problems. - -The library is under active development and more features added -regularly. Please contact us if you want a feature to be supported. - -.. _donation--support: - -Donation & Support -================== - -You can donate to PyGAD via: - -- `Credit/Debit Card `__: - https://donate.stripe.com/eVa5kO866elKgM0144 - -- `Open Collective `__: - `opencollective.com/pygad `__ - -- PayPal: Use either this link: - `paypal.me/ahmedfgad `__ or the e-mail - address ahmed.f.gad@gmail.com - -- Interac e-Transfer: Use e-mail address ahmed.f.gad@gmail.com - -- Buy a product at `Teespring `__: - `pygad.creator-spring.com `__ - -Installation -============ - -To install `PyGAD `__, simply use pip to -download and install the library from -`PyPI `__ (Python Package Index). The -library lives a PyPI at this page https://pypi.org/project/pygad. - -Install PyGAD with the following command: - -.. code:: python - - pip3 install pygad - -Quick Start -=========== - -To get started with `PyGAD `__, simply -import it. - -.. code:: python - - import pygad - -Using `PyGAD `__, a wide range of -problems can be optimized. A quick and simple problem to be optimized -using the `PyGAD `__ is finding the best -set of weights that satisfy the following function: - -.. code:: - - y = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + w6x6 - where (x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7) and y=44 - -The first step is to prepare the inputs and the outputs of this -equation. - -.. code:: python - - function_inputs = [4,-2,3.5,5,-11,-4.7] - desired_output = 44 - -A very important step is to implement the fitness function that will be -used for calculating the fitness value for each solution. Here is one. - -If the fitness function returns a number, then the problem is -single-objective. If a ``list``, ``tuple``, or ``numpy.ndarray`` is -returned, then it is a multi-objective problem (applicable even if a -single element exists). - -.. code:: python - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution*function_inputs) - fitness = 1.0 / numpy.abs(output - desired_output) - return fitness - -Next is to prepare the parameters of -`PyGAD `__. Here is an example for a set -of parameters. - -.. code:: python - - fitness_function = fitness_func - - num_generations = 50 - num_parents_mating = 4 - - sol_per_pop = 8 - num_genes = len(function_inputs) - - init_range_low = -2 - init_range_high = 5 - - parent_selection_type = "sss" - keep_parents = 1 - - crossover_type = "single_point" - - mutation_type = "random" - mutation_percent_genes = 10 - -After the parameters are prepared, an instance of the **pygad.GA** class -is created. - -.. code:: python - - ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - fitness_func=fitness_function, - sol_per_pop=sol_per_pop, - num_genes=num_genes, - init_range_low=init_range_low, - init_range_high=init_range_high, - parent_selection_type=parent_selection_type, - keep_parents=keep_parents, - crossover_type=crossover_type, - mutation_type=mutation_type, - mutation_percent_genes=mutation_percent_genes) - -After creating the instance, the ``run()`` method is called to start the -optimization. - -.. code:: python - - ga_instance.run() - -After the ``run()`` method completes, information about the best -solution found by PyGAD can be accessed. - -.. code:: python - - solution, solution_fitness, solution_idx = ga_instance.best_solution() - print("Parameters of the best solution : {solution}".format(solution=solution)) - print("Fitness value of the best solution = {solution_fitness}".format(solution_fitness=solution_fitness)) - - prediction = numpy.sum(numpy.array(function_inputs)*solution) - print("Predicted output based on the best solution : {prediction}".format(prediction=prediction)) - -.. code:: - - Parameters of the best solution : [3.92692328 -0.11554946 2.39873381 3.29579039 -0.74091476 1.05468517] - Fitness value of the best solution = 157.37320042925006 - Predicted output based on the best solution : 44.00635432206546 - -There is more to do using PyGAD. Read its documentation to explore the -features of PyGAD. - -PyGAD's Modules -=============== - -`PyGAD `__ has the following modules: - -1. The main module has the same name as the library ``pygad`` which is - the main interface to build the genetic algorithm. - -2. The ``nn`` module builds artificial neural networks. - -3. The ``gann`` module optimizes neural networks (for classification - and regression) using the genetic algorithm. - -4. The ``cnn`` module builds convolutional neural networks. - -5. The ``gacnn`` module optimizes convolutional neural networks using - the genetic algorithm. - -6. The ``kerasga`` module to train `Keras `__ models - using the genetic algorithm. - -7. The ``torchga`` module to train `PyTorch `__ - models using the genetic algorithm. - -8. The ``visualize`` module to visualize the results. - -9. The ``utils`` module contains the operators (crossover, mutation, - and parent selection) and the NSGA-II code. - -10. The ``helper`` module has some helper functions. - -The documentation discusses these modules. - -PyGAD Citation - Bibtex Formatted -================================= - -If you used PyGAD, please consider citing its paper with the following -details: - -.. code:: - - @article{gad2023pygad, - title={Pygad: An intuitive genetic algorithm python library}, - author={Gad, Ahmed Fawzy}, - journal={Multimedia Tools and Applications}, - pages={1--14}, - year={2023}, - publisher={Springer} - } - - -.. _header-n4: - -pygad Module -=============== - - -.. toctree:: - :maxdepth: 4 - :caption: pygad Module TOC - - pygad.rst - - - -.. _header-n5: - -More About pygad Module -=============== - - -.. toctree:: - :maxdepth: 4 - :caption: More About pygad Module TOC - - pygad_more.rst - - - - -.. _header-n6: - -utils Module -=============== - - -.. toctree:: - :maxdepth: 4 - :caption: utils Module TOC - - utils.rst - - - -.. _header-n7: - -visualize Module -=============== - - -.. toctree:: - :maxdepth: 4 - :caption: visualize Module TOC - - visualize.rst - - - -.. _header-n8: - -helper Module -=============== - - -.. toctree:: - :maxdepth: 4 - :caption: helper Module TOC - - helper.rst - - - - -.. _header-n9: - -pygad.nn Module -=============== - - -.. toctree:: - :maxdepth: 4 - :caption: pygad.nn Module TOC - - nn.rst - - - - - -.. _header-n10: - -pygad.gann Module -================= - - -.. toctree:: - :maxdepth: 4 - :caption: pygad.gann Module TOC - - gann.rst - - - - - - - - - -.. _header-n11: - -pygad.cnn Module -================= - - -.. toctree:: - :maxdepth: 4 - :caption: pygad.cnn Module TOC - - cnn.rst - - - -.. _header-n12: - -pygad.gacnn Module -================= - - -.. toctree:: - :maxdepth: 4 - :caption: pygad.gacnn Module TOC - - gacnn.rst - - - - -.. _header-n13: - -pygad.kerasga Module -================= - - -.. toctree:: - :maxdepth: 4 - :caption: pygad.kerasga Module TOC - - kerasga.rst - - - - -.. _header-n14: - -pygad.torchga Module -================= - - -.. toctree:: - :maxdepth: 4 - :caption: pygad.torchga Module TOC - - torchga.rst - - -.. _header-n15: - -Releases -================= - - -.. toctree:: - :maxdepth: 4 - :caption: Releases - - releases.rst - - - - -Indices and tables -================== - -* :ref:`search` diff --git a/docs/md/kerasga.md b/docs/source/kerasga.md similarity index 97% rename from docs/md/kerasga.md rename to docs/source/kerasga.md index cdf143a..955481b 100644 --- a/docs/md/kerasga.md +++ b/docs/source/kerasga.md @@ -13,7 +13,7 @@ The contents of this module are: More details are given in the next sections. -# Steps Summary +## Steps Summary The summary of the steps used to train a Keras model using PyGAD is as follows: @@ -24,7 +24,7 @@ The summary of the steps used to train a Keras model using PyGAD is as follows: 6. Create an instance of the `pygad.GA` class. 8. Run the genetic algorithm. -# Create Keras Model +## Create Keras Model Before discussing training a Keras model using PyGAD, the first thing to do is to create the Keras model. @@ -65,18 +65,18 @@ model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) Feel free to add the layers of your choice. -# `pygad.kerasga.KerasGA` Class +## `pygad.kerasga.KerasGA` Class The `pygad.kerasga` module has a class named `KerasGA` for creating an initial population for the genetic algorithm based on a Keras model. The constructor, methods, and attributes within the class are discussed in this section. -## `__init__()` +### `__init__()` The `pygad.kerasga.KerasGA` class constructor accepts the following parameters: - `model`: An instance of the Keras model. - `num_solutions`: Number of solutions in the population. Each solution has different parameters of the model. -## Instance Attributes +### Instance Attributes All parameters in the `pygad.kerasga.KerasGA` class constructor are used as instance attributes in addition to adding a new attribute called `population_weights`. @@ -86,19 +86,19 @@ Here is a list of all instance attributes: - `num_solutions` - `population_weights`: A nested list holding the weights of all solutions in the population. -## Methods in the `KerasGA` Class +### Methods in the `KerasGA` Class This section discusses the methods available for instances of the `pygad.kerasga.KerasGA` class. -### `create_population()` +#### `create_population()` The `create_population()` method creates the initial population of the genetic algorithm as a list of solutions where each solution represents different model parameters. The list of networks is assigned to the `population_weights` attribute of the instance. -# Functions in the `pygad.kerasga` Module +## Functions in the `pygad.kerasga` Module This section discusses the functions in the `pygad.kerasga` module. -## `pygad.kerasga.model_weights_as_vector()` +### `pygad.kerasga.model_weights_as_vector()` The `model_weights_as_vector()` function accepts a single parameter named `model` representing the Keras model. It returns a vector holding all model weights. The reason for representing the model weights as a vector is that the genetic algorithm expects all parameters of any solution to be in a 1D vector form. @@ -110,7 +110,7 @@ The function accepts the following parameters: It returns a 1D vector holding the model weights. -## `pygad.kerasga.model_weights_as_matrix()` +### `pygad.kerasga.model_weights_as_matrix()` The `model_weights_as_matrix()` function accepts the following parameters: @@ -119,7 +119,7 @@ The `model_weights_as_matrix()` function accepts the following parameters: It returns the restored model weights after reshaping the vector. -## `pygad.kerasga.predict()` +### `pygad.kerasga.predict()` The `predict()` function makes a prediction based on a solution. It accepts the following parameters: @@ -134,11 +134,11 @@ Check documentation of the [Keras Model.predict()](https://keras.io/api/models/m It returns the predictions of the data samples. -# Examples +## Examples This section gives the complete code of some examples that build and train a Keras model using PyGAD. Each subsection builds a different network. -## Example 1: Regression Example +### Example 1: Regression Example The next code builds a simple Keras model for regression. The next subsections discuss each part in the code. @@ -218,7 +218,7 @@ abs_error = mae(data_outputs, predictions).numpy() print(f"Absolute Error : {abs_error}") ``` -### Create a Keras Model +#### Create a Keras Model According to the steps mentioned previously, the first step is to create a Keras model. Here is the code that builds the model using the Functional API. @@ -245,7 +245,7 @@ model.add(dense_layer1) model.add(output_layer) ``` -### Create an Instance of the `pygad.kerasga.KerasGA` Class +#### Create an Instance of the `pygad.kerasga.KerasGA` Class The second step is to create an instance of the `pygad.kerasga.KerasGA` class. There are 10 solutions per population. Change this number according to your needs. @@ -256,7 +256,7 @@ keras_ga = pygad.kerasga.KerasGA(model=model, num_solutions=10) ``` -### Prepare the Training Data +#### Prepare the Training Data The third step is to prepare the training data inputs and outputs. Here is an example where there are 4 samples. Each sample has 3 inputs and 1 output. @@ -276,7 +276,7 @@ data_outputs = numpy.array([[0.1], [2.5]]) ``` -### Build the Fitness Function +#### Build the Fitness Function The fourth step is to build the fitness function. This function must accept 2 parameters representing the solution and its index within the population. @@ -297,7 +297,7 @@ def fitness_func(ga_instance, solution, sol_idx): return solution_fitness ``` -### Create an Instance of the `pygad.GA` Class +#### Create an Instance of the `pygad.GA` Class The fifth step is to instantiate the `pygad.GA` class. Note how the `initial_population` parameter is assigned to the initial weights of the Keras models. @@ -316,7 +316,7 @@ ga_instance = pygad.GA(num_generations=num_generations, on_generation=on_generation) ``` -### Run the Genetic Algorithm +#### Run the Genetic Algorithm The sixth and last step is to run the genetic algorithm by calling the `run()` method. @@ -378,7 +378,7 @@ print(f"Absolute Error : {abs_error}") Absolute Error : 0.013740465 ``` -## Example 2: XOR Binary Classification +### Example 2: XOR Binary Classification The next code creates a Keras model to build the XOR binary classification problem. Let's highlight the changes compared to the previous example. @@ -524,7 +524,7 @@ Binary Crossentropy : 0.0013527311 Accuracy : 1.0 ``` -## Example 3: Image Multi-Class Classification (Dense Layers) +### Example 3: Image Multi-Class Classification (Dense Layers) Here is the code. @@ -615,7 +615,7 @@ cce = tensorflow.keras.losses.CategoricalCrossentropy() solution_fitness = 1.0 / (cce(data_outputs, predictions).numpy() + 0.00000001) ``` -### Prepare the Training Data +#### Prepare the Training Data Before building and training neural networks, the training data (input and output) needs to be prepared. The inputs and the outputs of the training data are NumPy arrays. @@ -650,7 +650,7 @@ Categorical Crossentropy : 0.23823906 Accuracy : 0.9852192 ``` -## Example 4: Image Multi-Class Classification (Conv Layers) +### Example 4: Image Multi-Class Classification (Conv Layers) Compared to the previous example that uses only dense layers, this example uses convolutional layers to classify the same dataset. @@ -765,7 +765,7 @@ output_layer = tensorflow.keras.layers.Dense(4, activation="softmax")(dense_laye model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) ``` -### Prepare the Training Data +#### Prepare the Training Data The data used in this example is available as 2 files: @@ -805,7 +805,7 @@ To improve the model performance, you can do the following: - Use different parameters for the layers. - Use different parameters for the genetic algorithm (e.g. number of solution, number of generations, etc) -## Example 5: Image Classification using Data Generator +### Example 5: Image Classification using Data Generator This example uses the image data generator `tensorflow.keras.preprocessing.image.ImageDataGenerator` to feed data to the model. Instead of reading all the data in the memory, the data generator generates the data needed by the model and only save it in the memory instead of saving all the data. This frees the memory but adds more computational time. diff --git a/docs/source/kerasga.rst b/docs/source/kerasga.rst deleted file mode 100644 index f39ffec..0000000 --- a/docs/source/kerasga.rst +++ /dev/null @@ -1,1078 +0,0 @@ -.. _pygadkerasga-module: - -``pygad.kerasga`` Module -======================== - -This section of the PyGAD's library documentation discusses the -`pygad.kerasga `__ -module. - -The ``pygad.kerarsga`` module has helper a class and 2 functions to -train Keras models using the genetic algorithm (PyGAD). The Keras model -can be built either using the `Sequential -Model `__ or the `Functional -API `__. - -The contents of this module are: - -1. ``KerasGA``: A class for creating an initial population of all - parameters in the Keras model. - -2. ``model_weights_as_vector()``: A function to reshape the Keras model - weights to a single vector. - -3. ``model_weights_as_matrix()``: A function to restore the Keras model - weights from a vector. - -4. ``predict()``: A function to make predictions based on the Keras - model and a solution. - -More details are given in the next sections. - -Steps Summary -============= - -The summary of the steps used to train a Keras model using PyGAD is as -follows: - -1. Create a Keras model. - -2. Create an instance of the ``pygad.kerasga.KerasGA`` class. - -3. Prepare the training data. - -4. Build the fitness function. - -5. Create an instance of the ``pygad.GA`` class. - -6. Run the genetic algorithm. - -Create Keras Model -================== - -Before discussing training a Keras model using PyGAD, the first thing to -do is to create the Keras model. - -According to the `Keras library -documentation `__, there are 3 ways to -build a Keras model: - -1. `Sequential Model `__ - -2. `Functional API `__ - -3. `Model Subclassing `__ - -PyGAD supports training the models created either using the Sequential -Model or the Functional API. - -Here is an example of a model created using the Sequential Model. - -.. code:: python - - import tensorflow.keras - - input_layer = tensorflow.keras.layers.Input(3) - dense_layer1 = tensorflow.keras.layers.Dense(5, activation="relu") - output_layer = tensorflow.keras.layers.Dense(1, activation="linear") - - model = tensorflow.keras.Sequential() - model.add(input_layer) - model.add(dense_layer1) - model.add(output_layer) - -This is the same model created using the Functional API. - -.. code:: python - - input_layer = tensorflow.keras.layers.Input(3) - dense_layer1 = tensorflow.keras.layers.Dense(5, activation="relu")(input_layer) - output_layer = tensorflow.keras.layers.Dense(1, activation="linear")(dense_layer1) - - model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) - -Feel free to add the layers of your choice. - -.. _pygadkerasgakerasga-class: - -``pygad.kerasga.KerasGA`` Class -=============================== - -The ``pygad.kerasga`` module has a class named ``KerasGA`` for creating -an initial population for the genetic algorithm based on a Keras model. -The constructor, methods, and attributes within the class are discussed -in this section. - -.. _init: - -``__init__()`` --------------- - -The ``pygad.kerasga.KerasGA`` class constructor accepts the following -parameters: - -- ``model``: An instance of the Keras model. - -- ``num_solutions``: Number of solutions in the population. Each - solution has different parameters of the model. - -Instance Attributes -------------------- - -All parameters in the ``pygad.kerasga.KerasGA`` class constructor are -used as instance attributes in addition to adding a new attribute called -``population_weights``. - -Here is a list of all instance attributes: - -- ``model`` - -- ``num_solutions`` - -- ``population_weights``: A nested list holding the weights of all - solutions in the population. - -Methods in the ``KerasGA`` Class --------------------------------- - -This section discusses the methods available for instances of the -``pygad.kerasga.KerasGA`` class. - -.. _createpopulation: - -``create_population()`` -~~~~~~~~~~~~~~~~~~~~~~~ - -The ``create_population()`` method creates the initial population of the -genetic algorithm as a list of solutions where each solution represents -different model parameters. The list of networks is assigned to the -``population_weights`` attribute of the instance. - -.. _functions-in-the-pygadkerasga-module: - -Functions in the ``pygad.kerasga`` Module -========================================= - -This section discusses the functions in the ``pygad.kerasga`` module. - -.. _pygadkerasgamodelweightsasvector: - -``pygad.kerasga.model_weights_as_vector()`` --------------------------------------------- - -The ``model_weights_as_vector()`` function accepts a single parameter -named ``model`` representing the Keras model. It returns a vector -holding all model weights. The reason for representing the model weights -as a vector is that the genetic algorithm expects all parameters of any -solution to be in a 1D vector form. - -This function filters the layers based on the ``trainable`` attribute to -see whether the layer weights are trained or not. For each layer, if its -``trainable=False``, then its weights will not be evolved using the -genetic algorithm. Otherwise, it will be represented in the chromosome -and evolved. - -The function accepts the following parameters: - -- ``model``: The Keras model. - -It returns a 1D vector holding the model weights. - -.. _pygadkerasgamodelweightsasmatrix: - -``pygad.kerasga.model_weights_as_matrix()`` -------------------------------------------- - -The ``model_weights_as_matrix()`` function accepts the following -parameters: - -1. ``model``: The Keras model. - -2. ``weights_vector``: The model parameters as a vector. - -It returns the restored model weights after reshaping the vector. - -.. _pygadkerasgapredict: - -``pygad.kerasga.predict()`` ---------------------------- - -The ``predict()`` function makes a prediction based on a solution. It -accepts the following parameters: - -1. ``model``: The Keras model. - -2. ``solution``: The solution evolved. - -3. ``data``: The test data inputs. - -4. ``batch_size=None``: The batch size (i.e. number of samples per step - or batch). - -5. ``verbose=None``: Verbosity mode. - -6. ``steps=None``: The total number of steps (batches of samples). - -Check documentation of the `Keras -Model.predict() `__ -method for more information about the ``batch_size``, ``verbose``, and -``steps`` parameters. - -It returns the predictions of the data samples. - -Examples -======== - -This section gives the complete code of some examples that build and -train a Keras model using PyGAD. Each subsection builds a different -network. - -Example 1: Regression Example ------------------------------ - -The next code builds a simple Keras model for regression. The next -subsections discuss each part in the code. - -.. code:: python - - import tensorflow.keras - import pygad.kerasga - import numpy - import pygad - - def fitness_func(ga_instance, solution, sol_idx): - global data_inputs, data_outputs, keras_ga, model - - predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=data_inputs) - - mae = tensorflow.keras.losses.MeanAbsoluteError() - abs_error = mae(data_outputs, predictions).numpy() + 0.00000001 - solution_fitness = 1.0/abs_error - - return solution_fitness - - def on_generation(ga_instance): - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution()[1]}") - - input_layer = tensorflow.keras.layers.Input(3) - dense_layer1 = tensorflow.keras.layers.Dense(5, activation="relu")(input_layer) - output_layer = tensorflow.keras.layers.Dense(1, activation="linear")(dense_layer1) - - model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) - - keras_ga = pygad.kerasga.KerasGA(model=model, - num_solutions=10) - - # Data inputs - data_inputs = numpy.array([[0.02, 0.1, 0.15], - [0.7, 0.6, 0.8], - [1.5, 1.2, 1.7], - [3.2, 2.9, 3.1]]) - - # Data outputs - data_outputs = numpy.array([[0.1], - [0.6], - [1.3], - [2.5]]) - - # Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class - num_generations = 250 # Number of generations. - num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. - initial_population = keras_ga.population_weights # Initial population of network weights - - ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - on_generation=on_generation) - - ga_instance.run() - - # After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. - ga_instance.plot_fitness(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4) - - # Returning the details of the best solution. - solution, solution_fitness, solution_idx = ga_instance.best_solution() - print(f"Fitness value of the best solution = {solution_fitness}") - print(f"Index of the best solution : {solution_idx}") - - # Make prediction based on the best solution. - predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=data_inputs) - print(f"Predictions : \n{predictions}") - - mae = tensorflow.keras.losses.MeanAbsoluteError() - abs_error = mae(data_outputs, predictions).numpy() - print(f"Absolute Error : {abs_error}") - -Create a Keras Model -~~~~~~~~~~~~~~~~~~~~ - -According to the steps mentioned previously, the first step is to create -a Keras model. Here is the code that builds the model using the -Functional API. - -.. code:: python - - import tensorflow.keras - - input_layer = tensorflow.keras.layers.Input(3) - dense_layer1 = tensorflow.keras.layers.Dense(5, activation="relu")(input_layer) - output_layer = tensorflow.keras.layers.Dense(1, activation="linear")(dense_layer1) - - model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) - -The model can also be build using the Keras Sequential Model API. - -.. code:: python - - input_layer = tensorflow.keras.layers.Input(3) - dense_layer1 = tensorflow.keras.layers.Dense(5, activation="relu") - output_layer = tensorflow.keras.layers.Dense(1, activation="linear") - - model = tensorflow.keras.Sequential() - model.add(input_layer) - model.add(dense_layer1) - model.add(output_layer) - -.. _create-an-instance-of-the-pygadkerasgakerasga-class: - -Create an Instance of the ``pygad.kerasga.KerasGA`` Class -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The second step is to create an instance of the -``pygad.kerasga.KerasGA`` class. There are 10 solutions per population. -Change this number according to your needs. - -.. code:: python - - import pygad.kerasga - - keras_ga = pygad.kerasga.KerasGA(model=model, - num_solutions=10) - -Prepare the Training Data -~~~~~~~~~~~~~~~~~~~~~~~~~ - -The third step is to prepare the training data inputs and outputs. Here -is an example where there are 4 samples. Each sample has 3 inputs and 1 -output. - -.. code:: python - - import numpy - - # Data inputs - data_inputs = numpy.array([[0.02, 0.1, 0.15], - [0.7, 0.6, 0.8], - [1.5, 1.2, 1.7], - [3.2, 2.9, 3.1]]) - - # Data outputs - data_outputs = numpy.array([[0.1], - [0.6], - [1.3], - [2.5]]) - -Build the Fitness Function -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The fourth step is to build the fitness function. This function must -accept 2 parameters representing the solution and its index within the -population. - -The next fitness function returns the model predictions based on the -current solution using the ``predict()`` function. Then, it calculates -the mean absolute error (MAE) of the Keras model based on the parameters -in the solution. The reciprocal of the MAE is used as the fitness value. -Feel free to use any other loss function to calculate the fitness value. - -.. code:: python - - def fitness_func(ga_instance, solution, sol_idx): - global data_inputs, data_outputs, keras_ga, model - - predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=data_inputs) - - mae = tensorflow.keras.losses.MeanAbsoluteError() - abs_error = mae(data_outputs, predictions).numpy() + 0.00000001 - solution_fitness = 1.0/abs_error - - return solution_fitness - -.. _create-an-instance-of-the-pygadga-class: - -Create an Instance of the ``pygad.GA`` Class -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The fifth step is to instantiate the ``pygad.GA`` class. Note how the -``initial_population`` parameter is assigned to the initial weights of -the Keras models. - -For more information, please check the `parameters this class -accepts `__. - -.. code:: python - - # Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class - num_generations = 250 # Number of generations. - num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. - initial_population = keras_ga.population_weights # Initial population of network weights - - ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - on_generation=on_generation) - -Run the Genetic Algorithm -~~~~~~~~~~~~~~~~~~~~~~~~~ - -The sixth and last step is to run the genetic algorithm by calling the -``run()`` method. - -.. code:: python - - ga_instance.run() - -After the PyGAD completes its execution, then there is a figure that -shows how the fitness value changes by generation. Call the -``plot_fitness()`` method to show the figure. - -.. code:: python - - ga_instance.plot_fitness(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4) - -Here is the figure. - -.. image:: https://user-images.githubusercontent.com/16560492/93722638-ac261880-fb98-11ea-95d3-e773deb034f4.png - :alt: - -To get information about the best solution found by PyGAD, use the -``best_solution()`` method. - -.. code:: python - - # Returning the details of the best solution. - solution, solution_fitness, solution_idx = ga_instance.best_solution() - print(f"Fitness value of the best solution = {solution_fitness}") - print(f"Index of the best solution : {solution_idx}") - -.. code:: python - - Fitness value of the best solution = 72.77768757825352 - Index of the best solution : 0 - -The next code makes prediction using the ``predict()`` function to -return the model predictions based on the best solution. - -.. code:: python - - # Fetch the parameters of the best solution. - predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=data_inputs) - print(f"Predictions : \n{predictions}") - -.. code:: python - - Predictions : - [[0.09935353] - [0.63082725] - [1.2765523 ] - [2.4999595 ]] - -The next code measures the trained model error. - -.. code:: python - - mae = tensorflow.keras.losses.MeanAbsoluteError() - abs_error = mae(data_outputs, predictions).numpy() - print(f"Absolute Error : {abs_error}") - -.. code:: - - Absolute Error : 0.013740465 - -Example 2: XOR Binary Classification ------------------------------------- - -The next code creates a Keras model to build the XOR binary -classification problem. Let's highlight the changes compared to the -previous example. - -.. code:: python - - import tensorflow.keras - import pygad.kerasga - import numpy - import pygad - - def fitness_func(ga_instance, solution, sol_idx): - global data_inputs, data_outputs, keras_ga, model - - predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=data_inputs) - - bce = tensorflow.keras.losses.BinaryCrossentropy() - solution_fitness = 1.0 / (bce(data_outputs, predictions).numpy() + 0.00000001) - - return solution_fitness - - def on_generation(ga_instance): - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution()[1]}") - - # Build the keras model using the functional API. - input_layer = tensorflow.keras.layers.Input(2) - dense_layer = tensorflow.keras.layers.Dense(4, activation="relu")(input_layer) - output_layer = tensorflow.keras.layers.Dense(2, activation="softmax")(dense_layer) - - model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) - - # Create an instance of the pygad.kerasga.KerasGA class to build the initial population. - keras_ga = pygad.kerasga.KerasGA(model=model, - num_solutions=10) - - # XOR problem inputs - data_inputs = numpy.array([[0, 0], - [0, 1], - [1, 0], - [1, 1]]) - - # XOR problem outputs - data_outputs = numpy.array([[1, 0], - [0, 1], - [0, 1], - [1, 0]]) - - # Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class - num_generations = 250 # Number of generations. - num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. - initial_population = keras_ga.population_weights # Initial population of network weights. - - # Create an instance of the pygad.GA class - ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - on_generation=on_generation) - - # Start the genetic algorithm evolution. - ga_instance.run() - - # After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. - ga_instance.plot_fitness(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4) - - # Returning the details of the best solution. - solution, solution_fitness, solution_idx = ga_instance.best_solution() - print(f"Fitness value of the best solution = {solution_fitness}") - print(f"Index of the best solution : {solution_idx}") - - # Make predictions based on the best solution. - predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=data_inputs) - print(f"Predictions : \n{predictions}") - - # Calculate the binary crossentropy for the trained model. - bce = tensorflow.keras.losses.BinaryCrossentropy() - print("Binary Crossentropy : ", bce(data_outputs, predictions).numpy()) - - # Calculate the classification accuracy for the trained model. - ba = tensorflow.keras.metrics.BinaryAccuracy() - ba.update_state(data_outputs, predictions) - accuracy = ba.result().numpy() - print(f"Accuracy : {accuracy}") - -Compared to the previous regression example, here are the changes: - -- The Keras model is changed according to the nature of the problem. - Now, it has 2 inputs and 2 outputs with an in-between hidden layer of - 4 neurons. - -.. code:: python - - # Build the keras model using the functional API. - input_layer = tensorflow.keras.layers.Input(2) - dense_layer = tensorflow.keras.layers.Dense(4, activation="relu")(input_layer) - output_layer = tensorflow.keras.layers.Dense(2, activation="softmax")(dense_layer) - - model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) - -- The train data is changed. Note that the output of each sample is a - 1D vector of 2 values, 1 for each class. - -.. code:: python - - # XOR problem inputs - data_inputs = numpy.array([[0, 0], - [0, 1], - [1, 0], - [1, 1]]) - - # XOR problem outputs - data_outputs = numpy.array([[1, 0], - [0, 1], - [0, 1], - [1, 0]]) - -- The fitness value is calculated based on the binary cross entropy. - -.. code:: python - - bce = tensorflow.keras.losses.BinaryCrossentropy() - solution_fitness = 1.0 / (bce(data_outputs, predictions).numpy() + 0.00000001) - -After the previous code completes, the next figure shows how the fitness -value change by generation. - -.. image:: https://user-images.githubusercontent.com/16560492/93722639-b811da80-fb98-11ea-8951-f13a7a266c04.png - :alt: - -Here is some information about the trained model. Its fitness value is -``739.24``, loss is ``0.0013527311`` and accuracy is 100%. - -.. code:: python - - Fitness value of the best solution = 739.2397344644013 - Index of the best solution : 7 - - Predictions : - [[9.9694413e-01 3.0558957e-03] - [5.0176249e-04 9.9949825e-01] - [1.8470541e-03 9.9815291e-01] - [9.9999976e-01 2.0538971e-07]] - - Binary Crossentropy : 0.0013527311 - - Accuracy : 1.0 - -Example 3: Image Multi-Class Classification (Dense Layers) ----------------------------------------------------------- - -Here is the code. - -.. code:: python - - import tensorflow.keras - import pygad.kerasga - import numpy - import pygad - - def fitness_func(ga_instance, solution, sol_idx): - global data_inputs, data_outputs, keras_ga, model - - predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=data_inputs) - - cce = tensorflow.keras.losses.CategoricalCrossentropy() - solution_fitness = 1.0 / (cce(data_outputs, predictions).numpy() + 0.00000001) - - return solution_fitness - - def on_generation(ga_instance): - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution()[1]}") - - # Build the keras model using the functional API. - input_layer = tensorflow.keras.layers.Input(360) - dense_layer = tensorflow.keras.layers.Dense(50, activation="relu")(input_layer) - output_layer = tensorflow.keras.layers.Dense(4, activation="softmax")(dense_layer) - - model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) - - # Create an instance of the pygad.kerasga.KerasGA class to build the initial population. - keras_ga = pygad.kerasga.KerasGA(model=model, - num_solutions=10) - - # Data inputs - data_inputs = numpy.load("../data/dataset_features.npy") - - # Data outputs - data_outputs = numpy.load("../data/outputs.npy") - data_outputs = tensorflow.keras.utils.to_categorical(data_outputs) - - # Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class - num_generations = 100 # Number of generations. - num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. - initial_population = keras_ga.population_weights # Initial population of network weights. - - # Create an instance of the pygad.GA class - ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - on_generation=on_generation) - - # Start the genetic algorithm evolution. - ga_instance.run() - - # After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. - ga_instance.plot_fitness(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4) - - # Returning the details of the best solution. - solution, solution_fitness, solution_idx = ga_instance.best_solution() - print(f"Fitness value of the best solution = {solution_fitness}") - print(f"Index of the best solution : {solution_idx}") - - # Make predictions based on the best solution. - predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=data_inputs) - # print(f"Predictions : \n{predictions}") - - # Calculate the categorical crossentropy for the trained model. - cce = tensorflow.keras.losses.CategoricalCrossentropy() - print(f"Categorical Crossentropy : {cce(data_outputs, predictions).numpy()}") - - # Calculate the classification accuracy for the trained model. - ca = tensorflow.keras.metrics.CategoricalAccuracy() - ca.update_state(data_outputs, predictions) - accuracy = ca.result().numpy() - print(f"Accuracy : {accuracy}") - -Compared to the previous binary classification example, this example has -multiple classes (4) and thus the loss is measured using categorical -cross entropy. - -.. code:: python - - cce = tensorflow.keras.losses.CategoricalCrossentropy() - solution_fitness = 1.0 / (cce(data_outputs, predictions).numpy() + 0.00000001) - -.. _prepare-the-training-data-2: - -Prepare the Training Data -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Before building and training neural networks, the training data (input -and output) needs to be prepared. The inputs and the outputs of the -training data are NumPy arrays. - -The data used in this example is available as 2 files: - -1. `dataset_features.npy `__: - Data inputs. - https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy - -2. `outputs.npy `__: - Class labels. - https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy - -The data consists of 4 classes of images. The image shape is -``(100, 100, 3)``. The number of training samples is 1962. The feature -vector extracted from each image has a length 360. - -Simply download these 2 files and read them according to the next code. -Note that the class labels are one-hot encoded using the -``tensorflow.keras.utils.to_categorical()`` function. - -.. code:: python - - import numpy - - data_inputs = numpy.load("../data/dataset_features.npy") - - data_outputs = numpy.load("../data/outputs.npy") - data_outputs = tensorflow.keras.utils.to_categorical(data_outputs) - -The next figure shows how the fitness value changes. - -.. image:: https://user-images.githubusercontent.com/16560492/93722649-c2cc6f80-fb98-11ea-96e7-3f6ce3cfe1cf.png - :alt: - -Here are some statistics about the trained model. - -.. code:: - - Fitness value of the best solution = 4.197464252185969 - Index of the best solution : 0 - Categorical Crossentropy : 0.23823906 - Accuracy : 0.9852192 - -Example 4: Image Multi-Class Classification (Conv Layers) ---------------------------------------------------------- - -Compared to the previous example that uses only dense layers, this -example uses convolutional layers to classify the same dataset. - -Here is the complete code. - -.. code:: python - - import tensorflow.keras - import pygad.kerasga - import numpy - import pygad - - def fitness_func(ga_instance, solution, sol_idx): - global data_inputs, data_outputs, keras_ga, model - - predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=data_inputs) - - cce = tensorflow.keras.losses.CategoricalCrossentropy() - solution_fitness = 1.0 / (cce(data_outputs, predictions).numpy() + 0.00000001) - - return solution_fitness - - def on_generation(ga_instance): - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution()[1]}") - - # Build the keras model using the functional API. - input_layer = tensorflow.keras.layers.Input(shape=(100, 100, 3)) - conv_layer1 = tensorflow.keras.layers.Conv2D(filters=5, - kernel_size=7, - activation="relu")(input_layer) - max_pool1 = tensorflow.keras.layers.MaxPooling2D(pool_size=(5,5), - strides=5)(conv_layer1) - conv_layer2 = tensorflow.keras.layers.Conv2D(filters=3, - kernel_size=3, - activation="relu")(max_pool1) - flatten_layer = tensorflow.keras.layers.Flatten()(conv_layer2) - dense_layer = tensorflow.keras.layers.Dense(15, activation="relu")(flatten_layer) - output_layer = tensorflow.keras.layers.Dense(4, activation="softmax")(dense_layer) - - model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) - - # Create an instance of the pygad.kerasga.KerasGA class to build the initial population. - keras_ga = pygad.kerasga.KerasGA(model=model, - num_solutions=10) - - # Data inputs - data_inputs = numpy.load("../data/dataset_inputs.npy") - - # Data outputs - data_outputs = numpy.load("../data/dataset_outputs.npy") - data_outputs = tensorflow.keras.utils.to_categorical(data_outputs) - - # Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class - num_generations = 200 # Number of generations. - num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. - initial_population = keras_ga.population_weights # Initial population of network weights. - - # Create an instance of the pygad.GA class - ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - on_generation=on_generation) - - # Start the genetic algorithm evolution. - ga_instance.run() - - # After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. - ga_instance.plot_fitness(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4) - - # Returning the details of the best solution. - solution, solution_fitness, solution_idx = ga_instance.best_solution() - print(f"Fitness value of the best solution = {solution_fitness}") - print(f"Index of the best solution : {solution_idx}") - - # Make predictions based on the best solution. - predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=data_inputs) - # print(f"Predictions : \n{predictions}") - - # Calculate the categorical crossentropy for the trained model. - cce = tensorflow.keras.losses.CategoricalCrossentropy() - print(f"Categorical Crossentropy : {cce(data_outputs, predictions).numpy()}") - - # Calculate the classification accuracy for the trained model. - ca = tensorflow.keras.metrics.CategoricalAccuracy() - ca.update_state(data_outputs, predictions) - accuracy = ca.result().numpy() - print(f"Accuracy : {accuracy}") - -Compared to the previous example, the only change is that the -architecture uses convolutional and max-pooling layers. The shape of -each input sample is 100x100x3. - -.. code:: python - - # Build the keras model using the functional API. - input_layer = tensorflow.keras.layers.Input(shape=(100, 100, 3)) - conv_layer1 = tensorflow.keras.layers.Conv2D(filters=5, - kernel_size=7, - activation="relu")(input_layer) - max_pool1 = tensorflow.keras.layers.MaxPooling2D(pool_size=(5,5), - strides=5)(conv_layer1) - conv_layer2 = tensorflow.keras.layers.Conv2D(filters=3, - kernel_size=3, - activation="relu")(max_pool1) - flatten_layer = tensorflow.keras.layers.Flatten()(conv_layer2) - dense_layer = tensorflow.keras.layers.Dense(15, activation="relu")(flatten_layer) - output_layer = tensorflow.keras.layers.Dense(4, activation="softmax")(dense_layer) - - model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) - -.. _prepare-the-training-data-3: - -Prepare the Training Data -~~~~~~~~~~~~~~~~~~~~~~~~~ - -The data used in this example is available as 2 files: - -1. `dataset_inputs.npy `__: - Data inputs. - https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_inputs.npy - -2. `dataset_outputs.npy `__: - Class labels. - https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_outputs.npy - -The data consists of 4 classes of images. The image shape is -``(100, 100, 3)`` and there are 20 images per class for a total of 80 -training samples. For more information about the dataset, check the -`Reading the -Data `__ -section of the ``pygad.cnn`` module. - -Simply download these 2 files and read them according to the next code. -Note that the class labels are one-hot encoded using the -``tensorflow.keras.utils.to_categorical()`` function. - -.. code:: python - - import numpy - - data_inputs = numpy.load("../data/dataset_inputs.npy") - - data_outputs = numpy.load("../data/dataset_outputs.npy") - data_outputs = tensorflow.keras.utils.to_categorical(data_outputs) - -The next figure shows how the fitness value changes. - -.. image:: https://user-images.githubusercontent.com/16560492/93722654-cc55d780-fb98-11ea-8f95-7b65dc67f5c8.png - :alt: - -Here are some statistics about the trained model. The model accuracy is -75% after the 200 generations. Note that just running the code again may -give different results. - -.. code:: - - Fitness value of the best solution = 2.7462310258668805 - Index of the best solution : 0 - Categorical Crossentropy : 0.3641354 - Accuracy : 0.75 - -To improve the model performance, you can do the following: - -- Add more layers - -- Modify the existing layers. - -- Use different parameters for the layers. - -- Use different parameters for the genetic algorithm (e.g. number of - solution, number of generations, etc) - -Example 5: Image Classification using Data Generator ----------------------------------------------------- - -This example uses the image data generator -``tensorflow.keras.preprocessing.image.ImageDataGenerator`` to feed data -to the model. Instead of reading all the data in the memory, the data -generator generates the data needed by the model and only save it in the -memory instead of saving all the data. This frees the memory but adds -more computational time. - -.. code:: python - - import tensorflow as tf - import tensorflow.keras - import pygad.kerasga - import pygad - - def fitness_func(ga_instanse, solution, sol_idx): - global train_generator, data_outputs, keras_ga, model - - predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=train_generator) - - cce = tensorflow.keras.losses.CategoricalCrossentropy() - solution_fitness = 1.0 / (cce(data_outputs, predictions).numpy() + 0.00000001) - - return solution_fitness - - def on_generation(ga_instance): - print("Generation = {ga_instance.generations_completed}") - print("Fitness = {ga_instance.best_solution(ga_instance.last_generation_fitness)[1]}") - - # The dataset path. - dataset_path = r'../data/Skin_Cancer_Dataset' - - num_classes = 2 - img_size = 224 - - # Create a simple CNN. This does not gurantee high classification accuracy. - model = tf.keras.models.Sequential() - model.add(tf.keras.layers.Input(shape=(img_size, img_size, 3))) - model.add(tf.keras.layers.Conv2D(32, (3,3), activation="relu", padding="same")) - model.add(tf.keras.layers.MaxPooling2D((2, 2))) - model.add(tf.keras.layers.Flatten()) - model.add(tf.keras.layers.Dropout(rate=0.2)) - model.add(tf.keras.layers.Dense(num_classes, activation="softmax")) - - # Create an instance of the pygad.kerasga.KerasGA class to build the initial population. - keras_ga = pygad.kerasga.KerasGA(model=model, - num_solutions=10) - - data_generator = tf.keras.preprocessing.image.ImageDataGenerator() - train_generator = data_generator.flow_from_directory(dataset_path, - class_mode='categorical', - target_size=(224, 224), - batch_size=32, - shuffle=False) - # train_generator.class_indices - data_outputs = tf.keras.utils.to_categorical(train_generator.labels) - - # Check the documentation for more information about the parameters: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class - initial_population = keras_ga.population_weights # Initial population of network weights. - - # Create an instance of the pygad.GA class - ga_instance = pygad.GA(num_generations=10, - num_parents_mating=5, - initial_population=initial_population, - fitness_func=fitness_func, - on_generation=on_generation) - - # Start the genetic algorithm evolution. - ga_instance.run() - - # After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. - ga_instance.plot_fitness(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4) - - # Returning the details of the best solution. - solution, solution_fitness, solution_idx = ga_instance.best_solution(ga_instance.last_generation_fitness) - print(f"Fitness value of the best solution = {solution_fitness}") - print(f"Index of the best solution : {solution_idx}") - - predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=train_generator) - # print(f"Predictions : \n{predictions}") - - # Calculate the categorical crossentropy for the trained model. - cce = tensorflow.keras.losses.CategoricalCrossentropy() - print(f"Categorical Crossentropy : {cce(data_outputs, predictions).numpy()}") - - # Calculate the classification accuracy for the trained model. - ca = tensorflow.keras.metrics.CategoricalAccuracy() - ca.update_state(data_outputs, predictions) - accuracy = ca.result().numpy() - print(f"Accuracy : {accuracy}") diff --git a/docs/md/nn.md b/docs/source/nn.md similarity index 96% rename from docs/md/nn.md rename to docs/source/nn.md index 8fcd8b5..326d982 100644 --- a/docs/md/nn.md +++ b/docs/source/nn.md @@ -6,9 +6,9 @@ Using the **pygad.nn** module, artificial neural networks are created. The purpo Later, the **pygad.gann** module is used to train the **pygad.nn** network using the genetic algorithm built in the **pygad** module. -Starting from [PyGAD 2.7.1](https://pygad.readthedocs.io/en/latest/Footer.html#pygad-2-7-1), the **pygad.nn** module supports both classification and regression problems. For more information, check the `problem_type` parameter in the `pygad.nn.train()` and `pygad.nn.predict()` functions. +Starting from [PyGAD 2.7.1](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-7-1), the **pygad.nn** module supports both classification and regression problems. For more information, check the `problem_type` parameter in the `pygad.nn.train()` and `pygad.nn.predict()` functions. -# Supported Layers +## Supported Layers Each layer supported by the **pygad.nn** module has a corresponding class. The layers and their classes are: @@ -18,7 +18,7 @@ Each layer supported by the **pygad.nn** module has a corresponding class. The l In the future, more layers will be added. The next subsections discuss such layers. -## `pygad.nn.InputLayer` Class +### `pygad.nn.InputLayer` Class The `pygad.nn.InputLayer` class creates the input layer for the neural network. For each network, there is only a single input layer. The network architecture must start with an input layer. @@ -40,7 +40,7 @@ print("Number of input neurons =", num_input_neurons) This is everything about the input layer. -## `pygad.nn.DenseLayer` Class +### `pygad.nn.DenseLayer` Class Using the `pygad.nn.DenseLayer` class, dense (fully-connected) layers can be created. To create a dense layer, just create a new instance of the class. The constructor accepts the following parameters: @@ -109,7 +109,7 @@ print("Number of input neurons =", num_input_neurons) Assuming that `dense_layer2` is the last dense layer, then it is regarded as the output layer. -### `previous_layer` Attribute +#### `previous_layer` Attribute The `previous_layer` attribute in the `pygad.nn.DenseLayer` class creates a one way linked list between all the layers in the network architecture as described by the next figure. @@ -133,11 +133,11 @@ while "previous_layer" in layer.__init__.__code__.co_varnames: layer = layer.previous_layer ``` -# Functions to Manipulate Neural Networks +## Functions to Manipulate Neural Networks There are a number of functions existing in the `pygad.nn` module that helps to manipulate the neural network. -## `pygad.nn.layers_weights()` +### `pygad.nn.layers_weights()` Creates and returns a list holding the weights matrices of all layers in the neural network. @@ -148,7 +148,7 @@ Accepts the following parameters: The function uses a `while` loop to iterate through the layers using their `previous_layer` attribute. For each layer, either the initial weights or the trained weights are returned based on where the `initial` parameter is `True` or `False`. -## `pygad.nn.layers_weights_as_vector()` +### `pygad.nn.layers_weights_as_vector()` Creates and returns a list holding the weights **vectors** of all layers in the neural network. The weights array of each layer is reshaped to get a vector. @@ -161,7 +161,7 @@ Accepts the following parameters: The function uses a `while` loop to iterate through the layers using their `previous_layer` attribute. For each layer, either the initial weights or the trained weights are returned based on where the `initial` parameter is `True` or `False`. -## `pygad.nn.layers_weights_as_matrix()` +### `pygad.nn.layers_weights_as_matrix()` Converts the network weights from vectors to matrices. @@ -174,7 +174,7 @@ Accepts the following parameters: The function uses a `while` loop to iterate through the layers using their `previous_layer` attribute. For each layer, the shape of its weights array is returned. This shape is used to reshape the weights vector of the layer into a matrix. -## `pygad.nn.layers_activations()` +### `pygad.nn.layers_activations()` Creates and returns a list holding the names of the activation functions of all layers in the neural network. @@ -184,7 +184,7 @@ Accepts the following parameter: The function uses a `while` loop to iterate through the layers using their `previous_layer` attribute. For each layer, the name of the activation function used is returned using the layer's `activation_function` attribute. -## `pygad.nn.sigmoid()` +### `pygad.nn.sigmoid()` Applies the sigmoid function and returns its result. @@ -192,7 +192,7 @@ Accepts the following parameters: * `sop`: The input to which the sigmoid function is applied. -## `pygad.nn.relu()` +### `pygad.nn.relu()` Applies the rectified linear unit (ReLU) function and returns its result. @@ -200,7 +200,7 @@ Accepts the following parameters: * `sop`: The input to which the relu function is applied. -## `pygad.nn.softmax()` +### `pygad.nn.softmax()` Applies the softmax function and returns its result. @@ -208,7 +208,7 @@ Accepts the following parameters: * `sop`: The input to which the softmax function is applied. -## `pygad.nn.train()` +### `pygad.nn.train()` Trains the neural network. @@ -223,7 +223,7 @@ Accepts the following parameters: For each epoch, all the data samples are fed to the network to return their predictions. After each epoch, the weights are updated using only the learning rate. No learning algorithm is used because the purpose of this project is to only build the forward pass of training a neural network. -## `pygad.nn.update_weights()` +### `pygad.nn.update_weights()` Calculates and returns the updated weights. Even no training algorithm is used in this project, the weights are updated using the learning rate. It is not the best way to update the weights but it is better than keeping it as it is by making some small changes to the weights. @@ -233,7 +233,7 @@ Accepts the following parameters: - `network_error`: The network error. - `learning_rate`: The learning rate. -## `pygad.nn.update_layers_trained_weights()` +### `pygad.nn.update_layers_trained_weights()` After the network weights are trained, this function updates the `trained_weights` attribute of each layer by the weights calculated after passing all the epochs (such weights are passed in the `final_weights` parameter) @@ -246,7 +246,7 @@ Accepts the following parameters: The function uses a `while` loop to iterate through the layers using their `previous_layer` attribute. For each layer, its `trained_weights` attribute is assigned the weights of the layer from the `final_weights` parameter. -## `pygad.nn.predict()` +### `pygad.nn.predict()` Uses the trained weights for predicting the samples' outputs. It returns a list of the predicted outputs for all samples. @@ -258,11 +258,11 @@ Accepts the following parameters: All the data samples are fed to the network to return their predictions. -# Helper Functions +## Helper Functions There are functions in the `pygad.nn` module that does not directly manipulate the neural networks. -## `pygad.nn.to_vector()` +### `pygad.nn.to_vector()` Converts a passed NumPy array (of any dimensionality) to its `array` parameter into a 1D vector and returns the vector. @@ -270,7 +270,7 @@ Accepts the following parameters: * `array`: The NumPy array to be converted into a 1D vector. -## `pygad.nn.to_array()` +### `pygad.nn.to_array()` Converts a passed vector to its `vector` parameter into a NumPy array and returns the array. @@ -279,7 +279,7 @@ Accepts the following parameters: - `vector`: The 1D vector to be converted into an array. - `shape`: The target shape of the array. -# Supported Activation Functions +## Supported Activation Functions The supported activation functions are: @@ -287,7 +287,7 @@ The supported activation functions are: 2. Rectified Linear Unit (ReLU): Implemented using the `pygad.nn.relu()` function. 3. Softmax: Implemented using the `pygad.nn.softmax()` function. -# Steps to Build a Neural Network +## Steps to Build a Neural Network This section discusses how to use the `pygad.nn` module for building a neural network. The summary of the steps are as follows: @@ -297,7 +297,7 @@ This section discusses how to use the `pygad.nn` module for building a neural ne - Making Predictions - Calculating Some Statistics -## Reading the Data +### Reading the Data Before building the network architecture, the first thing to do is to prepare the data that will be used for training the network. @@ -366,7 +366,7 @@ data_outputs = numpy.load("outputs.npy") After the data is prepared, next is to create the network architecture. -## Building the Network Architecture +### Building the Network Architecture The input layer is created by instantiating the `pygad.nn.InputLayer` class according to the next code. A network can only have a single input layer. @@ -386,7 +386,7 @@ output_layer = pygad.nn.DenseLayer(num_neurons=4, previous_layer=hidden_layer2, After both the data and the network architecture are prepared, the next step is to train the network. -## Training the Network +### Training the Network Here is an example of using the `pygad.nn.train()` function. @@ -400,7 +400,7 @@ pygad.nn.train(num_epochs=10, After training the network, the next step is to make predictions. -## Making Predictions +### Making Predictions The `pygad.nn.predict()` function uses the trained network for making predictions. Here is an example. @@ -410,7 +410,7 @@ predictions = pygad.nn.predict(last_layer=output_layer, data_inputs=data_inputs) It is not expected to have high accuracy in the predictions because no training algorithm is used. -## Calculating Some Statistics +### Calculating Some Statistics Based on the predictions the network made, some statistics can be calculated such as the number of correct and wrong predictions in addition to the classification accuracy. @@ -425,11 +425,11 @@ print(f"Classification accuracy : {accuracy}.") It is very important to note that it is not expected that the classification accuracy is high because no training algorithm is used. Please check the documentation of the `pygad.gann` module for training the network using the genetic algorithm. -# Examples +## Examples This section gives the complete code of some examples that build neural networks using `pygad.nn`. Each subsection builds a different network. -## XOR Classification +### XOR Classification This is an example of building a network with 1 hidden layer with 2 neurons for building a network that simulates the XOR logic gate. Because the XOR problem has 2 classes (0 and 1), then the output layer has 2 neurons, one for each class. @@ -480,7 +480,7 @@ print(f"Number of wrong classifications : {num_wrong.size}.") print(f"Classification accuracy : {accuracy}.") ``` -## Image Classification +### Image Classification This example is discussed in the **Steps to Build a Neural Network** section and its complete code is listed below. @@ -533,7 +533,7 @@ print(f"Number of wrong classifications : {num_wrong.size}.") print(f"Classification accuracy : {accuracy}.") ``` -## Regression Example 1 +### Regression Example 1 The next code listing builds a neural network for regression. Here is what to do to make the code works for regression: @@ -604,7 +604,7 @@ abs_error = numpy.mean(numpy.abs(predictions - data_outputs)) print(f"Absolute error : {abs_error}.") ``` -## Regression Example 2 - Fish Weight Prediction +### Regression Example 2 - Fish Weight Prediction This example uses the Fish Market Dataset available at Kaggle (https://www.kaggle.com/aungpyaeap/fish-market). Simply download the CSV dataset from [this link](https://www.kaggle.com/aungpyaeap/fish-market/download) (https://www.kaggle.com/aungpyaeap/fish-market/download). The dataset is also available at the [GitHub project of the pygad.nn module](https://github.com/ahmedfgad/NumPyANN): https://github.com/ahmedfgad/NumPyANN diff --git a/docs/source/nn.rst b/docs/source/nn.rst deleted file mode 100644 index 26b0af6..0000000 --- a/docs/source/nn.rst +++ /dev/null @@ -1,976 +0,0 @@ -.. _pygadnn-module: - -``pygad.nn`` Module -=================== - -This section of the PyGAD's library documentation discusses the -**pygad.nn** module. - -Using the **pygad.nn** module, artificial neural networks are created. -The purpose of this module is to only implement the **forward pass** of -a neural network without using a training algorithm. The **pygad.nn** -module builds the network layers, implements the activations functions, -trains the network, makes predictions, and more. - -Later, the **pygad.gann** module is used to train the **pygad.nn** -network using the genetic algorithm built in the **pygad** module. - -Starting from `PyGAD -2.7.1 `__, -the **pygad.nn** module supports both classification and regression -problems. For more information, check the ``problem_type`` parameter in -the ``pygad.nn.train()`` and ``pygad.nn.predict()`` functions. - -Supported Layers -================ - -Each layer supported by the **pygad.nn** module has a corresponding -class. The layers and their classes are: - -1. **Input**: Implemented using the ``pygad.nn.InputLayer`` class. - -2. **Dense** (Fully Connected): Implemented using the - ``pygad.nn.DenseLayer`` class. - -In the future, more layers will be added. The next subsections discuss -such layers. - -.. _pygadnninputlayer-class: - -``pygad.nn.InputLayer`` Class ------------------------------ - -The ``pygad.nn.InputLayer`` class creates the input layer for the neural -network. For each network, there is only a single input layer. The -network architecture must start with an input layer. - -This class has no methods or class attributes. All it has is a -constructor that accepts a parameter named ``num_neurons`` representing -the number of neurons in the input layer. - -An instance attribute named ``num_neurons`` is created within the -constructor to keep such a number. Here is an example of building an -input layer with 20 neurons. - -.. code:: python - - input_layer = pygad.nn.InputLayer(num_neurons=20) - -Here is how the single attribute ``num_neurons`` within the instance of -the ``pygad.nn.InputLayer`` class can be accessed. - -.. code:: python - - num_input_neurons = input_layer.num_neurons - - print("Number of input neurons =", num_input_neurons) - -This is everything about the input layer. - -.. _pygadnndenselayer-class: - -``pygad.nn.DenseLayer`` Class ------------------------------ - -Using the ``pygad.nn.DenseLayer`` class, dense (fully-connected) layers -can be created. To create a dense layer, just create a new instance of -the class. The constructor accepts the following parameters: - -- ``num_neurons``: Number of neurons in the dense layer. - -- ``previous_layer``: A reference to the previous layer. Using the - ``previous_layer`` attribute, a linked list is created that connects - all network layers. - -- ``activation_function``: A string representing the activation - function to be used in this layer. Defaults to ``"sigmoid"``. - Currently, the supported values for the activation functions are - ``"sigmoid"``, ``"relu"``, ``"softmax"`` (supported in PyGAD 2.3.0 - and higher), and ``"None"`` (supported in PyGAD 2.7.0 and higher). - When a layer has its activation function set to ``"None"``, then it - means no activation function is applied. For a **regression - problem**, set the activation function of the output (last) layer to - ``"None"``. If all outputs in the regression problem are nonnegative, - then it is possible to use the ReLU function in the output layer. - -Within the constructor, the accepted parameters are used as instance -attributes. Besides the parameters, some new instance attributes are -created which are: - -- ``initial_weights``: The initial weights for the dense layer. - -- ``trained_weights``: The trained weights of the dense layer. This - attribute is initialized by the value in the ``initial_weights`` - attribute. - -Here is an example for creating a dense layer with 12 neurons. Note that -the ``previous_layer`` parameter is assigned to the input layer -``input_layer``. - -.. code:: python - - dense_layer = pygad.nn.DenseLayer(num_neurons=12, - previous_layer=input_layer, - activation_function="relu") - -Here is how to access some attributes in the dense layer: - -.. code:: python - - num_dense_neurons = dense_layer.num_neurons - dense_initail_weights = dense_layer.initial_weights - - print("Number of dense layer attributes =", num_dense_neurons) - print("Initial weights of the dense layer :", dense_initail_weights) - -Because ``dense_layer`` holds a reference to the input layer, then the -number of input neurons can be accessed. - -.. code:: python - - input_layer = dense_layer.previous_layer - num_input_neurons = input_layer.num_neurons - - print("Number of input neurons =", num_input_neurons) - -Here is another dense layer. This dense layer's ``previous_layer`` -attribute points to the previously created dense layer. - -.. code:: python - - dense_layer2 = pygad.nn.DenseLayer(num_neurons=5, - previous_layer=dense_layer, - activation_function="relu") - -Because ``dense_layer2`` holds a reference to ``dense_layer`` in its -``previous_layer`` attribute, then the number of neurons in -``dense_layer`` can be accessed. - -.. code:: python - - dense_layer = dense_layer2.previous_layer - dense_layer_neurons = dense_layer.num_neurons - - print("Number of dense neurons =", num_input_neurons) - -After getting the reference to ``dense_layer``, we can use it to access -the number of input neurons. - -.. code:: python - - dense_layer = dense_layer2.previous_layer - input_layer = dense_layer.previous_layer - num_input_neurons = input_layer.num_neurons - - print("Number of input neurons =", num_input_neurons) - -Assuming that ``dense_layer2`` is the last dense layer, then it is -regarded as the output layer. - -.. _previouslayer-attribute: - -``previous_layer`` Attribute -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The ``previous_layer`` attribute in the ``pygad.nn.DenseLayer`` class -creates a one way linked list between all the layers in the network -architecture as described by the next figure. - -The last (output) layer indexed N points to layer **N-1**, layer **N-1** -points to the layer **N-2**, the layer **N-2** points to the layer -**N-3**, and so on until reaching the end of the linked list which is -layer 1 (input layer). - -.. image:: https://user-images.githubusercontent.com/16560492/81918975-816af880-95d7-11ea-83e3-34d14c3316db.jpg - :alt: - -The one way linked list allows returning all properties of all layers in -the network architecture by just passing the last layer in the network. -The linked list moves from the output layer towards the input layer. - -Using the ``previous_layer`` attribute of layer **N**, the layer **N-1** -can be accessed. Using the ``previous_layer`` attribute of layer -**N-1**, layer **N-2** can be accessed. The process continues until -reaching a layer that does not have a ``previous_layer`` attribute -(which is the input layer). - -The properties of the layers include the weights (initial or trained), -activation functions, and more. Here is how a ``while`` loop is used to -iterate through all the layers. The ``while`` loop stops only when the -current layer does not have a ``previous_layer`` attribute. This layer -is the input layer. - -.. code:: python - - layer = dense_layer2 - - while "previous_layer" in layer.__init__.__code__.co_varnames: - print("Number of neurons =", layer.num_neurons) - - # Go to the previous layer. - layer = layer.previous_layer - -Functions to Manipulate Neural Networks -======================================= - -There are a number of functions existing in the ``pygad.nn`` module that -helps to manipulate the neural network. - -.. _pygadnnlayersweights: - -``pygad.nn.layers_weights()`` ------------------------------ - -Creates and returns a list holding the weights matrices of all layers in -the neural network. - -Accepts the following parameters: - -- ``last_layer``: A reference to the last (output) layer in the network - architecture. - -- ``initial``: When ``True`` (default), the function returns the - **initial** weights of the layers using the layers' - ``initial_weights`` attribute. When ``False``, it returns the - **trained** weights of the layers using the layers' - ``trained_weights`` attribute. The initial weights are only needed - before network training starts. The trained weights are needed to - predict the network outputs. - -The function uses a ``while`` loop to iterate through the layers using -their ``previous_layer`` attribute. For each layer, either the initial -weights or the trained weights are returned based on where the -``initial`` parameter is ``True`` or ``False``. - -.. _pygadnnlayersweightsasvector: - -``pygad.nn.layers_weights_as_vector()`` ---------------------------------------- - -Creates and returns a list holding the weights **vectors** of all layers -in the neural network. The weights array of each layer is reshaped to -get a vector. - -This function is similar to the ``layers_weights()`` function except -that it returns the weights of each layer as a vector, not as an array. - -Accepts the following parameters: - -- ``last_layer``: A reference to the last (output) layer in the network - architecture. - -- ``initial``: When ``True`` (default), the function returns the - **initial** weights of the layers using the layers' - ``initial_weights`` attribute. When ``False``, it returns the - **trained** weights of the layers using the layers' - ``trained_weights`` attribute. The initial weights are only needed - before network training starts. The trained weights are needed to - predict the network outputs. - -The function uses a ``while`` loop to iterate through the layers using -their ``previous_layer`` attribute. For each layer, either the initial -weights or the trained weights are returned based on where the -``initial`` parameter is ``True`` or ``False``. - -.. _pygadnnlayersweightsasmatrix: - -``pygad.nn.layers_weights_as_matrix()`` ---------------------------------------- - -Converts the network weights from vectors to matrices. - -Compared to the ``layers_weights_as_vectors()`` function that only -accepts a reference to the last layer and returns the network weights as -vectors, this function accepts a reference to the last layer in addition -to a list holding the weights as vectors. Such vectors are converted -into matrices. - -Accepts the following parameters: - -- ``last_layer``: A reference to the last (output) layer in the network - architecture. - -- ``vector_weights``: The network weights as vectors where the weights - of each layer form a single vector. - -The function uses a ``while`` loop to iterate through the layers using -their ``previous_layer`` attribute. For each layer, the shape of its -weights array is returned. This shape is used to reshape the weights -vector of the layer into a matrix. - -.. _pygadnnlayersactivations: - -``pygad.nn.layers_activations()`` ---------------------------------- - -Creates and returns a list holding the names of the activation functions -of all layers in the neural network. - -Accepts the following parameter: - -- ``last_layer``: A reference to the last (output) layer in the network - architecture. - -The function uses a ``while`` loop to iterate through the layers using -their ``previous_layer`` attribute. For each layer, the name of the -activation function used is returned using the layer's -``activation_function`` attribute. - -.. _pygadnnsigmoid: - -``pygad.nn.sigmoid()`` ----------------------- - -Applies the sigmoid function and returns its result. - -Accepts the following parameters: - -- ``sop``: The input to which the sigmoid function is applied. - -.. _pygadnnrelu: - -``pygad.nn.relu()`` -------------------- - -Applies the rectified linear unit (ReLU) function and returns its -result. - -Accepts the following parameters: - -- ``sop``: The input to which the relu function is applied. - -.. _pygadnnsoftmax: - -``pygad.nn.softmax()`` ----------------------- - -Applies the softmax function and returns its result. - -Accepts the following parameters: - -- ``sop``: The input to which the softmax function is applied. - -.. _pygadnntrain: - -``pygad.nn.train()`` --------------------- - -Trains the neural network. - -Accepts the following parameters: - -- ``num_epochs``: Number of epochs. - -- ``last_layer``: Reference to the last (output) layer in the network - architecture. - -- ``data_inputs``: Data features. - -- ``data_outputs``: Data outputs. - -- ``problem_type``: The type of the problem which can be either - ``"classification"`` or ``"regression"``. Added in PyGAD 2.7.0 and - higher. - -- ``learning_rate``: Learning rate. - -For each epoch, all the data samples are fed to the network to return -their predictions. After each epoch, the weights are updated using only -the learning rate. No learning algorithm is used because the purpose of -this project is to only build the forward pass of training a neural -network. - -.. _pygadnnupdateweights: - -``pygad.nn.update_weights()`` ------------------------------ - -Calculates and returns the updated weights. Even no training algorithm -is used in this project, the weights are updated using the learning -rate. It is not the best way to update the weights but it is better than -keeping it as it is by making some small changes to the weights. - -Accepts the following parameters: - -- ``weights``: The current weights of the network. - -- ``network_error``: The network error. - -- ``learning_rate``: The learning rate. - -.. _pygadnnupdatelayerstrainedweights: - -``pygad.nn.update_layers_trained_weights()`` --------------------------------------------- - -After the network weights are trained, this function updates the -``trained_weights`` attribute of each layer by the weights calculated -after passing all the epochs (such weights are passed in the -``final_weights`` parameter) - -By just passing a reference to the last layer in the network (i.e. -output layer) in addition to the final weights, this function updates -the ``trained_weights`` attribute of all layers. - -Accepts the following parameters: - -- ``last_layer``: A reference to the last (output) layer in the network - architecture. - -- ``final_weights``: An array of weights of all layers in the network - after passing through all the epochs. - -The function uses a ``while`` loop to iterate through the layers using -their ``previous_layer`` attribute. For each layer, its -``trained_weights`` attribute is assigned the weights of the layer from -the ``final_weights`` parameter. - -.. _pygadnnpredict: - -``pygad.nn.predict()`` ----------------------- - -Uses the trained weights for predicting the samples' outputs. It returns -a list of the predicted outputs for all samples. - -Accepts the following parameters: - -- ``last_layer``: A reference to the last (output) layer in the network - architecture. - -- ``data_inputs``: Data features. - -- ``problem_type``: The type of the problem which can be either - ``"classification"`` or ``"regression"``. Added in PyGAD 2.7.0 and - higher. - -All the data samples are fed to the network to return their predictions. - -Helper Functions -================ - -There are functions in the ``pygad.nn`` module that does not directly -manipulate the neural networks. - -.. _pygadnntovector: - -``pygad.nn.to_vector()`` ------------------------- - -Converts a passed NumPy array (of any dimensionality) to its ``array`` -parameter into a 1D vector and returns the vector. - -Accepts the following parameters: - -- ``array``: The NumPy array to be converted into a 1D vector. - -.. _pygadnntoarray: - -``pygad.nn.to_array()`` ------------------------ - -Converts a passed vector to its ``vector`` parameter into a NumPy array -and returns the array. - -Accepts the following parameters: - -- ``vector``: The 1D vector to be converted into an array. - -- ``shape``: The target shape of the array. - -Supported Activation Functions -============================== - -The supported activation functions are: - -1. Sigmoid: Implemented using the ``pygad.nn.sigmoid()`` function. - -2. Rectified Linear Unit (ReLU): Implemented using the - ``pygad.nn.relu()`` function. - -3. Softmax: Implemented using the ``pygad.nn.softmax()`` function. - -Steps to Build a Neural Network -=============================== - -This section discusses how to use the ``pygad.nn`` module for building a -neural network. The summary of the steps are as follows: - -- Reading the Data - -- Building the Network Architecture - -- Training the Network - -- Making Predictions - -- Calculating Some Statistics - -Reading the Data ----------------- - -Before building the network architecture, the first thing to do is to -prepare the data that will be used for training the network. - -In this example, 4 classes of the **Fruits360** dataset are used for -preparing the training data. The 4 classes are: - -1. `Apple - Braeburn `__: - This class's data is available at - https://github.com/ahmedfgad/NumPyANN/tree/master/apple - -2. `Lemon - Meyer `__: - This class's data is available at - https://github.com/ahmedfgad/NumPyANN/tree/master/lemon - -3. `Mango `__: - This class's data is available at - https://github.com/ahmedfgad/NumPyANN/tree/master/mango - -4. `Raspberry `__: - This class's data is available at - https://github.com/ahmedfgad/NumPyANN/tree/master/raspberry - -The features from such 4 classes are extracted according to the next -code. This code reads the raw images of the 4 classes of the dataset, -prepares the features and the outputs as NumPy arrays, and saves the -arrays in 2 files. - -This code extracts a feature vector from each image representing the -color histogram of the HSV space's hue channel. - -.. code:: python - - import numpy - import skimage.io, skimage.color, skimage.feature - import os - - fruits = ["apple", "raspberry", "mango", "lemon"] - # Number of samples in the datset used = 492+490+490+490=1,962 - # 360 is the length of the feature vector. - dataset_features = numpy.zeros(shape=(1962, 360)) - outputs = numpy.zeros(shape=(1962)) - - idx = 0 - class_label = 0 - for fruit_dir in fruits: - curr_dir = os.path.join(os.path.sep, fruit_dir) - all_imgs = os.listdir(os.getcwd()+curr_dir) - for img_file in all_imgs: - if img_file.endswith(".jpg"): # Ensures reading only JPG files. - fruit_data = skimage.io.imread(fname=os.path.sep.join([os.getcwd(), curr_dir, img_file]), as_gray=False) - fruit_data_hsv = skimage.color.rgb2hsv(rgb=fruit_data) - hist = numpy.histogram(a=fruit_data_hsv[:, :, 0], bins=360) - dataset_features[idx, :] = hist[0] - outputs[idx] = class_label - idx = idx + 1 - class_label = class_label + 1 - - # Saving the extracted features and the outputs as NumPy files. - numpy.save("dataset_features.npy", dataset_features) - numpy.save("outputs.npy", outputs) - -To save your time, the training data is already prepared and 2 files -created by the next code are available for download at these links: - -1. `dataset_features.npy `__: - The features - https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy - -2. `outputs.npy `__: - The class labels - https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy - -The -`outputs.npy `__ -file gives the following labels for the 4 classes: - -1. `Apple - Braeburn `__: - Class label is **0** - -2. `Lemon - Meyer `__: - Class label is **1** - -3. `Mango `__: - Class label is **2** - -4. `Raspberry `__: - Class label is **3** - -The project has 4 folders holding the images for the 4 classes. - -After the 2 files are created, then just read them to return the NumPy -arrays according to the next 2 lines: - -.. code:: python - - data_inputs = numpy.load("dataset_features.npy") - data_outputs = numpy.load("outputs.npy") - -After the data is prepared, next is to create the network architecture. - -Building the Network Architecture ---------------------------------- - -The input layer is created by instantiating the ``pygad.nn.InputLayer`` -class according to the next code. A network can only have a single input -layer. - -.. code:: python - - import pygad.nn - num_inputs = data_inputs.shape[1] - - input_layer = pygad.nn.InputLayer(num_inputs) - -After the input layer is created, next is to create a number of dense -layers according to the next code. Normally, the last dense layer is -regarded as the output layer. Note that the output layer has a number of -neurons equal to the number of classes in the dataset which is 4. - -.. code:: python - - hidden_layer = pygad.nn.DenseLayer(num_neurons=HL2_neurons, previous_layer=input_layer, activation_function="relu") - output_layer = pygad.nn.DenseLayer(num_neurons=4, previous_layer=hidden_layer2, activation_function="softmax") - -After both the data and the network architecture are prepared, the next -step is to train the network. - -Training the Network --------------------- - -Here is an example of using the ``pygad.nn.train()`` function. - -.. code:: python - - pygad.nn.train(num_epochs=10, - last_layer=output_layer, - data_inputs=data_inputs, - data_outputs=data_outputs, - learning_rate=0.01) - -After training the network, the next step is to make predictions. - -Making Predictions ------------------- - -The ``pygad.nn.predict()`` function uses the trained network for making -predictions. Here is an example. - -.. code:: python - - predictions = pygad.nn.predict(last_layer=output_layer, data_inputs=data_inputs) - -It is not expected to have high accuracy in the predictions because no -training algorithm is used. - -Calculating Some Statistics ---------------------------- - -Based on the predictions the network made, some statistics can be -calculated such as the number of correct and wrong predictions in -addition to the classification accuracy. - -.. code:: python - - num_wrong = numpy.where(predictions != data_outputs)[0] - num_correct = data_outputs.size - num_wrong.size - accuracy = 100 * (num_correct/data_outputs.size) - print(f"Number of correct classifications : {num_correct}.") - print(f"Number of wrong classifications : {num_wrong.size}.") - print(f"Classification accuracy : {accuracy}.") - -It is very important to note that it is not expected that the -classification accuracy is high because no training algorithm is used. -Please check the documentation of the ``pygad.gann`` module for training -the network using the genetic algorithm. - -Examples -======== - -This section gives the complete code of some examples that build neural -networks using ``pygad.nn``. Each subsection builds a different network. - -XOR Classification ------------------- - -This is an example of building a network with 1 hidden layer with 2 -neurons for building a network that simulates the XOR logic gate. -Because the XOR problem has 2 classes (0 and 1), then the output layer -has 2 neurons, one for each class. - -.. code:: python - - import numpy - import pygad.nn - - # Preparing the NumPy array of the inputs. - data_inputs = numpy.array([[1, 1], - [1, 0], - [0, 1], - [0, 0]]) - - # Preparing the NumPy array of the outputs. - data_outputs = numpy.array([0, - 1, - 1, - 0]) - - # The number of inputs (i.e. feature vector length) per sample - num_inputs = data_inputs.shape[1] - # Number of outputs per sample - num_outputs = 2 - - HL1_neurons = 2 - - # Building the network architecture. - input_layer = pygad.nn.InputLayer(num_inputs) - hidden_layer1 = pygad.nn.DenseLayer(num_neurons=HL1_neurons, previous_layer=input_layer, activation_function="relu") - output_layer = pygad.nn.DenseLayer(num_neurons=num_outputs, previous_layer=hidden_layer1, activation_function="softmax") - - # Training the network. - pygad.nn.train(num_epochs=10, - last_layer=output_layer, - data_inputs=data_inputs, - data_outputs=data_outputs, - learning_rate=0.01) - - # Using the trained network for predictions. - predictions = pygad.nn.predict(last_layer=output_layer, data_inputs=data_inputs) - - # Calculating some statistics - num_wrong = numpy.where(predictions != data_outputs)[0] - num_correct = data_outputs.size - num_wrong.size - accuracy = 100 * (num_correct/data_outputs.size) - print(f"Number of correct classifications : {num_correct}.") - print(f"Number of wrong classifications : {num_wrong.size}.") - print(f"Classification accuracy : {accuracy}.") - -Image Classification --------------------- - -This example is discussed in the **Steps to Build a Neural Network** -section and its complete code is listed below. - -Remember to either download or create the -`dataset_features.npy `__ -and -`outputs.npy `__ -files before running this code. - -.. code:: python - - import numpy - import pygad.nn - - # Reading the data features. Check the 'extract_features.py' script for extracting the features & preparing the outputs of the dataset. - data_inputs = numpy.load("dataset_features.npy") # Download from https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy - - # Optional step for filtering the features using the standard deviation. - features_STDs = numpy.std(a=data_inputs, axis=0) - data_inputs = data_inputs[:, features_STDs > 50] - - # Reading the data outputs. Check the 'extract_features.py' script for extracting the features & preparing the outputs of the dataset. - data_outputs = numpy.load("outputs.npy") # Download from https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy - - # The number of inputs (i.e. feature vector length) per sample - num_inputs = data_inputs.shape[1] - # Number of outputs per sample - num_outputs = 4 - - HL1_neurons = 150 - HL2_neurons = 60 - - # Building the network architecture. - input_layer = pygad.nn.InputLayer(num_inputs) - hidden_layer1 = pygad.nn.DenseLayer(num_neurons=HL1_neurons, previous_layer=input_layer, activation_function="relu") - hidden_layer2 = pygad.nn.DenseLayer(num_neurons=HL2_neurons, previous_layer=hidden_layer1, activation_function="relu") - output_layer = pygad.nn.DenseLayer(num_neurons=num_outputs, previous_layer=hidden_layer2, activation_function="softmax") - - # Training the network. - pygad.nn.train(num_epochs=10, - last_layer=output_layer, - data_inputs=data_inputs, - data_outputs=data_outputs, - learning_rate=0.01) - - # Using the trained network for predictions. - predictions = pygad.nn.predict(last_layer=output_layer, data_inputs=data_inputs) - - # Calculating some statistics - num_wrong = numpy.where(predictions != data_outputs)[0] - num_correct = data_outputs.size - num_wrong.size - accuracy = 100 * (num_correct/data_outputs.size) - print(f"Number of correct classifications : {num_correct}.") - print(f"Number of wrong classifications : {num_wrong.size}.") - print(f"Classification accuracy : {accuracy}.") - -Regression Example 1 --------------------- - -The next code listing builds a neural network for regression. Here is -what to do to make the code works for regression: - -1. Set the ``problem_type`` parameter in the ``pygad.nn.train()`` and - ``pygad.nn.predict()`` functions to the string ``"regression"``. - -.. code:: python - - pygad.nn.train(..., - problem_type="regression") - - predictions = pygad.nn.predict(..., - problem_type="regression") - -1. Set the activation function for the output layer to the string - ``"None"``. - -.. code:: python - - output_layer = pygad.nn.DenseLayer(num_neurons=num_outputs, previous_layer=hidden_layer1, activation_function="None") - -1. Calculate the prediction error according to your preferred error - function. Here is how the mean absolute error is calculated. - -.. code:: python - - abs_error = numpy.mean(numpy.abs(predictions - data_outputs)) - print(f"Absolute error : {abs_error}.") - -Here is the complete code. Yet, there is no algorithm used to train the -network and thus the network is expected to give bad results. Later, the -``pygad.gann`` module is used to train either a regression or -classification networks. - -.. code:: python - - import numpy - import pygad.nn - - # Preparing the NumPy array of the inputs. - data_inputs = numpy.array([[2, 5, -3, 0.1], - [8, 15, 20, 13]]) - - # Preparing the NumPy array of the outputs. - data_outputs = numpy.array([0.1, - 1.5]) - - # The number of inputs (i.e. feature vector length) per sample - num_inputs = data_inputs.shape[1] - # Number of outputs per sample - num_outputs = 1 - - HL1_neurons = 2 - - # Building the network architecture. - input_layer = pygad.nn.InputLayer(num_inputs) - hidden_layer1 = pygad.nn.DenseLayer(num_neurons=HL1_neurons, previous_layer=input_layer, activation_function="relu") - output_layer = pygad.nn.DenseLayer(num_neurons=num_outputs, previous_layer=hidden_layer1, activation_function="None") - - # Training the network. - pygad.nn.train(num_epochs=100, - last_layer=output_layer, - data_inputs=data_inputs, - data_outputs=data_outputs, - learning_rate=0.01, - problem_type="regression") - - # Using the trained network for predictions. - predictions = pygad.nn.predict(last_layer=output_layer, - data_inputs=data_inputs, - problem_type="regression") - - # Calculating some statistics - abs_error = numpy.mean(numpy.abs(predictions - data_outputs)) - print(f"Absolute error : {abs_error}.") - -Regression Example 2 - Fish Weight Prediction ---------------------------------------------- - -This example uses the Fish Market Dataset available at Kaggle -(https://www.kaggle.com/aungpyaeap/fish-market). Simply download the CSV -dataset from `this -link `__ -(https://www.kaggle.com/aungpyaeap/fish-market/download). The dataset is -also available at the `GitHub project of the pygad.nn -module `__: -https://github.com/ahmedfgad/NumPyANN - -Using the Pandas library, the dataset is read using the ``read_csv()`` -function. - -.. code:: python - - data = numpy.array(pandas.read_csv("Fish.csv")) - -The last 5 columns in the dataset are used as inputs and the **Weight** -column is used as output. - -.. code:: python - - # Preparing the NumPy array of the inputs. - data_inputs = numpy.asarray(data[:, 2:], dtype=numpy.float32) - - # Preparing the NumPy array of the outputs. - data_outputs = numpy.asarray(data[:, 1], dtype=numpy.float32) # Fish Weight - -Note how the activation function at the last layer is set to ``"None"``. -Moreover, the ``problem_type`` parameter in the ``pygad.nn.train()`` and -``pygad.nn.predict()`` functions is set to ``"regression"``. - -After the ``pygad.nn.train()`` function completes, the mean absolute -error is calculated. - -.. code:: python - - abs_error = numpy.mean(numpy.abs(predictions - data_outputs)) - print(f"Absolute error : {abs_error}.") - -Here is the complete code. - -.. code:: python - - import numpy - import pygad.nn - import pandas - - data = numpy.array(pandas.read_csv("Fish.csv")) - - # Preparing the NumPy array of the inputs. - data_inputs = numpy.asarray(data[:, 2:], dtype=numpy.float32) - - # Preparing the NumPy array of the outputs. - data_outputs = numpy.asarray(data[:, 1], dtype=numpy.float32) # Fish Weight - - # The number of inputs (i.e. feature vector length) per sample - num_inputs = data_inputs.shape[1] - # Number of outputs per sample - num_outputs = 1 - - HL1_neurons = 2 - - # Building the network architecture. - input_layer = pygad.nn.InputLayer(num_inputs) - hidden_layer1 = pygad.nn.DenseLayer(num_neurons=HL1_neurons, previous_layer=input_layer, activation_function="relu") - output_layer = pygad.nn.DenseLayer(num_neurons=num_outputs, previous_layer=hidden_layer1, activation_function="None") - - # Training the network. - pygad.nn.train(num_epochs=100, - last_layer=output_layer, - data_inputs=data_inputs, - data_outputs=data_outputs, - learning_rate=0.01, - problem_type="regression") - - # Using the trained network for predictions. - predictions = pygad.nn.predict(last_layer=output_layer, - data_inputs=data_inputs, - problem_type="regression") - - # Calculating some statistics - abs_error = numpy.mean(numpy.abs(predictions - data_outputs)) - print(f"Absolute error : {abs_error}.") diff --git a/docs/md/pygad.md b/docs/source/pygad.md similarity index 85% rename from docs/md/pygad.md rename to docs/source/pygad.md index 32bb484..53a21e0 100644 --- a/docs/md/pygad.md +++ b/docs/source/pygad.md @@ -1,21 +1,21 @@ # `pygad` Module -This section of the PyGAD's library documentation discusses the `pygad` module. +This section of the documentation discusses the `pygad` module. -Using the `pygad` module, instances of the genetic algorithm can be created, run, saved, and loaded. Single-objective and multi-objective optimization problems can be solved. +With the `pygad` module, you can create, run, save, and load instances of the genetic algorithm. It solves both single-objective and multi-objective optimization problems. -# `pygad.GA` Class +## `pygad.GA` Class -The first module available in PyGAD is named `pygad` and contains a class named `GA` for building the genetic algorithm. The constructor, methods, function, and attributes within the class are discussed in this section. +The `pygad` module has a class named `GA` for building the genetic algorithm. This section explains the class constructor, its methods, functions, and attributes. -## `__init__()` +### `__init__()` -For creating an instance of the `pygad.GA` class, the constructor accepts several parameters that allow the user to customize the genetic algorithm to different types of applications. +To create an instance of the `pygad.GA` class, the constructor accepts several parameters. These let you adjust the genetic algorithm for different types of applications. The `pygad.GA` class constructor supports the following parameters: - `num_generations`: Number of generations. -- `num_parents_mating `: Number of solutions to be selected as parents. +- `num_parents_mating`: Number of solutions to be selected as parents. - `fitness_func`: Accepts a function/method and returns the fitness value(s) of the solution. If a function is passed, then it must accept 3 parameters (1. the instance of the `pygad.GA` class, 2. a single solution, and 3. its index in the population). If method, then it accepts a fourth parameter representing the method's class instance. Check the [Preparing the fitness_func Parameter](https://pygad.readthedocs.io/en/latest/pygad.html#preparing-the-fitness-func-parameter) section for information about creating such a function. In [PyGAD 3.2.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-2-0), multi-objective optimization is supported. To consider the problem as multi-objective, just return a `list`, `tuple`, or `numpy.ndarray` from the fitness function. - `fitness_batch_size=None`: A new optional parameter called `fitness_batch_size` is supported to calculate the fitness function in batches. If it is assigned the value `1` or `None` (default), then the normal flow is used where the fitness function is called for each individual solution. If the `fitness_batch_size` parameter is assigned a value satisfying this condition `1 < fitness_batch_size <= sol_per_pop`, then the solutions are grouped into batches of size `fitness_batch_size` and the fitness function is called once for each batch. Check the [Batch Fitness Calculation](https://pygad.readthedocs.io/en/latest/pygad_more.html#batch-fitness-calculation) section for more details and examples. Added in from [PyGAD 2.19.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-19-0). - `initial_population`: A user-defined initial population. It is useful when the user wants to start the generations with a custom initial population. It defaults to `None` which means no initial population is specified by the user. In this case, [PyGAD](https://pypi.org/project/pygad) creates an initial population using the `sol_per_pop` and `num_genes` parameters. An exception is raised if the `initial_population` is `None` while any of the 2 parameters (`sol_per_pop` or `num_genes`) is also `None`. Introduced in [PyGAD 2.0.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-0-0) and higher. @@ -25,12 +25,12 @@ The `pygad.GA` class constructor supports the following parameters: - `init_range_low=-4`: The lower value of the random range from which the gene values in the initial population are selected. `init_range_low` defaults to `-4`. Available in [PyGAD 1.0.20](https://pygad.readthedocs.io/en/latest/releases.html#pygad-1-0-20) and higher. This parameter has no action if the `initial_population` parameter exists. - `init_range_high=4`: The upper value of the random range from which the gene values in the initial population are selected. `init_range_high` defaults to `+4`. Available in [PyGAD 1.0.20](https://pygad.readthedocs.io/en/latest/releases.html#pygad-1-0-20) and higher. This parameter has no action if the `initial_population` parameter exists. - `parent_selection_type="sss"`: The parent selection type. Supported types are `sss` (for steady-state selection), `rws` (for roulette wheel selection), `sus` (for stochastic universal selection), `rank` (for rank selection), `random` (for random selection), and `tournament` (for tournament selection). A custom parent selection function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/utils.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about building a user-defined parent selection function. -- `keep_parents=-1`: Number of parents to keep in the current population. `-1` (default) means to keep all parents in the next population. `0` means keep no parents in the next population. A value `greater than 0` means keeps the specified number of parents in the next population. Note that the value assigned to `keep_parents` cannot be `< - 1` or greater than the number of solutions within the population `sol_per_pop`. Starting from [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0), this parameter have an effect only when the `keep_elitism` parameter is `0`. Starting from [PyGAD 2.20.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-20-0), the parents' fitness from the last generation will not be re-used if `keep_parents=0`. -- `keep_elitism=1`: Added in [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0). It can take the value `0` or a positive integer that satisfies (`0 <= keep_elitism <= sol_per_pop`). It defaults to `1` which means only the best solution in the current generation is kept in the next generation. If assigned `0`, this means it has no effect. If assigned a positive integer `K`, then the best `K` solutions are kept in the next generation. It cannot be assigned a value greater than the value assigned to the `sol_per_pop` parameter. If this parameter has a value different than `0`, then the `keep_parents` parameter will have no effect. +- `keep_parents=-1`: The number of parents to keep in the next population. `-1` (default) means keep all the parents. `0` means keep no parents. A value greater than `0` means keep that number of parents. The value of `keep_parents` cannot be less than `-1` or greater than the number of solutions in the population (`sol_per_pop`). Starting from [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0), this parameter has an effect only when the `keep_elitism` parameter is `0`. Starting from PyGAD 2.20.0, the parents' fitness from the last generation is not re-used if `keep_parents=0`. To see how `keep_parents` and `keep_elitism` work together, check the [How the Number of Offspring Is Decided](https://pygad.readthedocs.io/en/latest/pygad_more.html#how-the-number-of-offspring-is-decided) section. +- `keep_elitism=1`: Added in [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0). It takes the value `0` or a positive integer that meets the condition `0 <= keep_elitism <= sol_per_pop`. It defaults to `1`, which means only the best solution in the current generation is kept in the next generation. If set to `0`, it has no effect. If set to a positive integer `K`, then the best `K` solutions are kept in the next generation. It cannot be greater than the value of the `sol_per_pop` parameter. If this parameter is not `0`, then the `keep_parents` parameter has no effect. To see how `keep_elitism` and `keep_parents` work together, check the [How the Number of Offspring Is Decided](https://pygad.readthedocs.io/en/latest/pygad_more.html#how-the-number-of-offspring-is-decided) section. - `K_tournament=3`: In case that the parent selection type is `tournament`, the `K_tournament` specifies the number of parents participating in the tournament selection. It defaults to `3`. -- `crossover_type="single_point"`: Type of the crossover operation. Supported types are `single_point` (for single-point crossover), `two_points` (for two points crossover), `uniform` (for uniform crossover), and `scattered` (for scattered crossover). Scattered crossover is supported from PyGAD [2.9.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-9-0) and higher. It defaults to `single_point`. A custom crossover function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/pygad_more.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about creating a user-defined crossover function. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, if `crossover_type=None`, then the crossover step is bypassed which means 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. +- `crossover_type="single_point"`: Type of the crossover operation. Supported types are `single_point` (for single-point crossover), `two_points` (for two points crossover), `uniform` (for uniform crossover), and `scattered` (for scattered crossover). Scattered crossover is supported from PyGAD [2.9.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-9-0) and higher. It defaults to `single_point`. A custom crossover function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/utils.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about creating a user-defined crossover function. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, if `crossover_type=None`, then the crossover step is bypassed which means 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. - `crossover_probability=None`: The probability of selecting a parent for applying the crossover operation. Its value must be between 0.0 and 1.0 inclusive. For each parent, a random value between 0.0 and 1.0 is generated. If this random value is less than or equal to the value assigned to the `crossover_probability` parameter, then the parent is selected. Added in [PyGAD 2.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-5-0) and higher. -- `mutation_type="random"`: Type of the mutation operation. Supported types are `random` (for random mutation), `swap` (for swap mutation), `inversion` (for inversion mutation), `scramble` (for scramble mutation), and `adaptive` (for adaptive mutation). It defaults to `random`. A custom mutation function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/pygad_more.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about creating a user-defined mutation function. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, if `mutation_type=None`, then the mutation step is bypassed which means 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. `Adaptive` mutation is supported starting from [PyGAD 2.10.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-10-0). For more information about adaptive mutation, go the the [Adaptive Mutation](https://pygad.readthedocs.io/en/latest/pygad_more.html#adaptive-mutation) section. For example about using adaptive mutation, check the [Use Adaptive Mutation in PyGAD](https://pygad.readthedocs.io/en/latest/pygad_more.html#use-adaptive-mutation-in-pygad) section. +- `mutation_type="random"`: Type of the mutation operation. Supported types are `random` (for random mutation), `swap` (for swap mutation), `inversion` (for inversion mutation), `scramble` (for scramble mutation), and `adaptive` (for adaptive mutation). It defaults to `random`. A custom mutation function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/utils.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about creating a user-defined mutation function. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, if `mutation_type=None`, then the mutation step is bypassed which means 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. `Adaptive` mutation is supported starting from [PyGAD 2.10.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-10-0). For more information about adaptive mutation, go the the [Adaptive Mutation](https://pygad.readthedocs.io/en/latest/utils.html#adaptive-mutation) section. For example about using adaptive mutation, check the [Use Adaptive Mutation in PyGAD](https://pygad.readthedocs.io/en/latest/utils.html#use-adaptive-mutation-in-pygad) section. - `mutation_probability=None`: The probability of selecting a gene for applying the mutation operation. Its value must be between 0.0 and 1.0 inclusive. For each gene in a solution, a random value between 0.0 and 1.0 is generated. If this random value is less than or equal to the value assigned to the `mutation_probability` parameter, then the gene is selected. If this parameter exists, then there is no need for the 2 parameters `mutation_percent_genes` and `mutation_num_genes`. Added in [PyGAD 2.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-5-0) and higher. - `mutation_by_replacement=False`: An optional bool parameter. It works only when the selected type of mutation is random (`mutation_type="random"`). In this case, `mutation_by_replacement=True` means replace the gene by the randomly generated value. If False, then it has no effect and random mutation works by adding the random value to the gene. Supported in [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher. Check the changes in [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) under the Release History section for an example. - `mutation_percent_genes="default"`: Percentage of genes to mutate. It defaults to the string `"default"` which is later translated into the integer `10` which means 10% of the genes will be mutated. It must be `>0` and `<=100`. Out of this percentage, the number of genes to mutate is deduced which is assigned to the `mutation_num_genes` parameter. The `mutation_percent_genes` parameter has no action if `mutation_probability` or `mutation_num_genes` exist. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, this parameter has no action if `mutation_type` is `None`. @@ -56,19 +56,19 @@ The `pygad.GA` class constructor supports the following parameters: - `random_seed=None`: Added in [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0). It defines the random seed to be used by the random function generators (we use random functions in the NumPy and random modules). This helps to reproduce the same results by setting the same random seed (e.g. `random_seed=2`). If given the value `None`, then it has no effect. - `logger=None`: Accepts an instance of the `logging.Logger` class to log the outputs. Any message is no longer printed using `print()` but logged. If `logger=None`, then a logger is created that uses `StreamHandler` to logs the messages to the console. Added in [PyGAD 3.0.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-0-0). Check the [Logging Outputs](https://pygad.readthedocs.io/en/latest/pygad_more.html#logging-outputs) for more information. -The user doesn't have to specify all of such parameters while creating an instance of the GA class. A very important parameter you must care about is `fitness_func` which defines the fitness function. +You do not have to set all of these parameters when you create an instance of the `GA` class. The most important one is `fitness_func`, which defines the fitness function. It is OK to set the value of any of the 2 parameters `init_range_low` and `init_range_high` to be equal, higher, or lower than the other parameter (i.e. `init_range_low` is not needed to be lower than `init_range_high`). The same holds for the `random_mutation_min_val` and `random_mutation_max_val` parameters. -If the 2 parameters `mutation_type` and `crossover_type` are `None`, this disables any type of evolution the genetic algorithm can make. As a result, the genetic algorithm cannot find a better solution that the best solution in the initial population. +If both the `mutation_type` and `crossover_type` parameters are `None`, then the genetic algorithm cannot evolve at all. As a result, it cannot find a solution better than the best solution in the initial population. -The parameters are validated by calling the `validate_parameters()` method of the `utils.validation.Validation` class within the constructor. If at least a parameter is not correct, an exception is thrown and the `valid_parameters` attribute is set to `False`. +The parameters are validated by calling the `validate_parameters()` method of the `utils.validation.Validation` class inside the constructor. If any parameter is not correct, an exception is raised and the `valid_parameters` attribute is set to `False`. -# Extended Classes +## Extended Classes -To make the library modular and structured, different scripts are created where each script has one or more classes. Each class has its own objective. +To keep the library modular and structured, the code is split into several scripts, where each script has one or more classes. Each class has its own purpose. -This is the list of scripts and classes within them where the `pygad.GA` class extends: +Here is the list of scripts and the classes that the `pygad.GA` class extends: 1. `utils/engine.py`: 1. `utils.engine.GAEngine`: @@ -91,13 +91,13 @@ This is the list of scripts and classes within them where the `pygad.GA` class e Since the `pygad.GA` class extends such classes, the attributes and methods inside them can be retrieved by instances of the `pygad.GA` class. -## Class Attributes +### Class Attributes * `supported_int_types`: A list of the supported types for the integer numbers. * `supported_float_types`: A list of the supported types for the floating-point numbers. * `supported_int_float_types`: A list of the supported types for all numbers. It just concatenates the previous 2 lists. -## Other Instance Attributes & Methods +### Other Instance Attributes & Methods All the parameters and functions passed to the `pygad.GA` class constructor are used as class attributes and methods in the instances of the `pygad.GA` class. In addition to such attributes, there are other attributes and methods added to the instances of the `pygad.GA` class: @@ -105,7 +105,7 @@ The next 2 subsections list such attributes and methods. > The `GA` class gains the attributes of its parent classes via inheritance, making them accessible through the `GA` object even if they are defined externally to its specific class body. -### Other Attributes +#### Other Attributes - `generations_completed`: Holds the number of the last completed generation. - `population`: A NumPy array that initially holds the initial population and is later updated after each generation. @@ -130,7 +130,7 @@ The next 2 subsections list such attributes and methods. Note that the attributes with names starting with `last_generation_` are updated after each generation. -### Other Methods +#### Other Methods - `cal_pop_fitness()`: A method that calculates the fitness values for all solutions within the population by calling the function passed to the `fitness_func` parameter for each solution. - `crossover()`: Refers to the method that applies the crossover operator based on the selected type of crossover in the `crossover_type` property. @@ -149,7 +149,7 @@ There are many methods that are not designed for user usage. Some of them are li The next sections discuss the methods available in the `pygad.GA` class. -## `save()` +### `save()` The `save()` method in the `pygad.GA` class saves the genetic algorithm instance as a pickled object. @@ -157,11 +157,11 @@ Accepts the following parameter: * `filename`: Name of the file to save the instance. No extension is needed. -# Functions in `pygad` +## Functions in `pygad` Besides the methods available in the `pygad.GA` class, this section discusses the functions available in `pygad`. Up to this time, there is only a single function named `load()`. -## `pygad.load()` +### `pygad.load()` Reads a saved instance of the genetic algorithm. This is not a method but a function that is indented under the `pygad` module. So, it could be called by the pygad module as follows: `pygad.load(filename)`. @@ -171,42 +171,42 @@ Accepts the following parameter: Returns the genetic algorithm instance. -# Steps to Use `pygad` +## Steps to Use `pygad` To use the `pygad` module, here is a summary of the required steps: -1. Preparing the `fitness_func` parameter. -2. Preparing Other Parameters. -4. Import `pygad`. -5. Create an Instance of the `pygad.GA` Class. -6. Run the Genetic Algorithm. -7. Plotting Results. -7. Information about the Best Solution. -8. Saving & Loading the Results. +1. Prepare the `fitness_func` parameter. +2. Prepare the other parameters. +3. Import `pygad`. +4. Create an instance of the `pygad.GA` class. +5. Run the genetic algorithm. +6. Plot the results. +7. Get information about the best solution. +8. Save and load the results. -Let's discuss how to do each of these steps. +The next sections explain each step. -## Preparing the `fitness_func` Parameter +### Preparing the `fitness_func` Parameter -Even though some steps in the genetic algorithm pipeline can work the same regardless of the problem being solved, one critical step is the calculation of the fitness value. There is no unique way of calculating the fitness value and it changes from one problem to another. +Some steps in the genetic algorithm work the same way for every problem, but the fitness calculation does not. There is no single way to calculate the fitness value, and it changes from one problem to another. -PyGAD has a parameter called `fitness_func` that allows the user to specify a custom function/method to use when calculating the fitness. This function/method must be a maximization function/method so that a solution with a high fitness value returned is selected compared to a solution with a low value. +PyGAD has a parameter called `fitness_func` that lets you pass your own function or method to calculate the fitness. This function must be a maximization function, so a solution with a higher fitness value is treated as better than a solution with a lower value. The fitness function is where the user can decide whether the optimization problem is single-objective or multi-objective. * If the fitness function returns a numeric value, then the problem is single-objective. The numeric data types supported by PyGAD are listed in the `supported_int_float_types` variable of the `pygad.GA` class. * If the fitness function returns a `list`, `tuple`, or `numpy.ndarray`, then the problem is multi-objective. Even if there is only one element, the problem is still considered multi-objective. Each element represents the fitness value of its corresponding objective. -Using a user-defined fitness function allows the user to freely use PyGAD to solve any problem by passing the appropriate fitness function/method. It is very important to understand the problem well before creating it. +A user-defined fitness function lets you use PyGAD to solve any problem by passing the right fitness function. It is very important to understand the problem well before you write the fitness function. -Let's discuss an example: +Here is an example: > Given the following function: -> y = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + 6wx6 +> y = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + w6x6 > where (x1,x2,x3,x4,x5,x6)=(4, -2, 3.5, 5, -11, -4.7) and y=44 > What are the best values for the 6 weights (w1 to w6)? We are going to use the genetic algorithm to optimize this function. -So, the task is about using the genetic algorithm to find the best values for the 6 weight `W1` to `W6`. Thinking of the problem, it is clear that the best solution is that returning an output that is close to the desired output `y=44`. So, the fitness function/method should return a value that gets higher when the solution's output is closer to `y=44`. Here is a function that does that: +So, the task is to use the genetic algorithm to find the best values for the 6 weights `w1` to `w6`. The best solution is the one whose output is closest to the desired output `y=44`. So, the fitness function should return a higher value when the solution's output is closer to `y=44`. Here is a function that does that: ```python function_inputs = [4, -2, 3.5, 5, -11, -4.7] # Function inputs. @@ -218,7 +218,7 @@ def fitness_func(ga_instance, solution, solution_idx): return fitness ``` -Because the fitness function returns a numeric value, then the problem is single-objective. +Because the fitness function returns a numeric value, the problem is single-objective. Such a user-defined function must accept 3 parameters: @@ -228,11 +228,11 @@ Such a user-defined function must accept 3 parameters: If a method is passed to the `fitness_func` parameter, then it accepts a fourth parameter representing the method's instance. -The `__code__` object is used to check if this function accepts the required number of parameters. If more or fewer parameters are passed, an exception is thrown. +The `__code__` object is used to check that this function accepts the required number of parameters. If more or fewer parameters are passed, an exception is raised. -By creating this function, you did a very important step towards using PyGAD. +By writing this function, you have completed a very important step toward using PyGAD. -### Preparing Other Parameters +#### Preparing Other Parameters Here is an example for preparing the other parameters: @@ -257,9 +257,9 @@ mutation_type = "random" mutation_percent_genes = 10 ``` -### The `on_generation` Parameter +#### The `on_generation` Parameter -An optional parameter named `on_generation` is supported which allows the user to call a function (with a single parameter) after each generation. Here is a simple function that just prints the current generation number and the fitness value of the best solution in the current generation. The `generations_completed` attribute of the GA class returns the number of the last completed generation. +The optional `on_generation` parameter lets you call a function (with a single parameter) after each generation. Here is a simple function that prints the current generation number and the fitness value of the best solution in the current generation. The `generations_completed` attribute of the `GA` class returns the number of the last completed generation. ```python def on_gen(ga_instance): @@ -277,7 +277,7 @@ ga_instance = pygad.GA(..., After the parameters are prepared, we can import PyGAD and build an instance of the `pygad.GA` class. -## Import `pygad` +### Import `pygad` The next step is to import PyGAD as follows: @@ -287,7 +287,7 @@ import pygad The `pygad.GA` class holds the implementation of all methods for running the genetic algorithm. -## Create an Instance of the `pygad.GA` Class +### Create an Instance of the `pygad.GA` Class The `pygad.GA` class is instantiated where the previously prepared parameters are fed to its constructor. The constructor is responsible for creating the initial population. @@ -306,7 +306,7 @@ ga_instance = pygad.GA(num_generations=num_generations, mutation_percent_genes=mutation_percent_genes) ``` -## Run the Genetic Algorithm +### Run the Genetic Algorithm After an instance of the `pygad.GA` class is created, the next step is to call the `run()` method as follows: @@ -314,14 +314,14 @@ After an instance of the `pygad.GA` class is created, the next step is to call t ga_instance.run() ``` -Inside this method, the genetic algorithm evolves over some generations by doing the following tasks: +Inside this method, the genetic algorithm evolves over the generations by doing the following tasks: -1. Calculating the fitness values of the solutions within the current population. +1. Calculate the fitness values of the solutions in the current population. 2. Select the best solutions as parents in the mating pool. -3. Apply the crossover & mutation operation -4. Repeat the process for the specified number of generations. +3. Apply the crossover and mutation operations. +4. Repeat the process for the given number of generations. -## Plotting Results +### Plotting Results There is a method named `plot_fitness()` which creates a figure summarizing how the fitness values of the solutions change with the generations. @@ -331,7 +331,7 @@ ga_instance.plot_fitness() ![Fig02](https://user-images.githubusercontent.com/16560492/78830005-93111d00-79e7-11ea-9d8e-a8d8325a6101.png) -## Information about the Best Solution +### Information about the Best Solution The following information about the best solution in the last population is returned using the `best_solution()` method. @@ -346,14 +346,14 @@ print(f"Fitness value of the best solution = {solution_fitness}") print(f"Index of the best solution : {solution_idx}") ``` -Using the `best_solution_generation` attribute of the instance from the `pygad.GA` class, the generation number at which the `best fitness` is reached could be fetched. +Using the `best_solution_generation` attribute of the `pygad.GA` instance, you can get the generation number at which the best fitness was reached. ```python if ga_instance.best_solution_generation != -1: print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") ``` -## Saving & Loading the Results +### Saving & Loading the Results After the `run()` method completes, it is possible to save the current instance of the genetic algorithm to avoid losing the progress made. The `save()` method is available for that purpose. Just pass the file name to it without an extension. According to the next code, a file named `genetic.pkl` will be created and saved in the current directory. @@ -374,9 +374,19 @@ After the instance is loaded, you can use it to run any method or access any pro print(loaded_ga_instance.best_solution()) ``` -# Life Cycle of PyGAD +## Life Cycle of PyGAD + +The next figure shows the main steps in the life cycle of a `pygad.GA` instance. The genetic algorithm starts from an initial population and repeats the same steps once per generation. It measures the fitness of every solution, selects the parents, applies crossover and mutation to make offspring, and then builds the next generation. PyGAD stops when all generations are done or when the function passed to the `on_generation` parameter returns the string `stop`. + +:::{figure} images/ga_lifecycle.* +:alt: The PyGAD genetic algorithm life cycle +:width: 480px +:align: center -The next figure lists the different stages in the lifecycle of an instance of the `pygad.GA` class. Note that PyGAD stops when either all generations are completed or when the function passed to the `on_generation` parameter returns the string `stop`. +The main steps of the genetic algorithm in PyGAD. +::: + +The next figure shows the same life cycle in more detail, including the callback functions that PyGAD calls at each stage. ![PyGAD Lifecycle](https://user-images.githubusercontent.com/16560492/220486073-c5b6089d-81e4-44d9-a53c-385f479a7273.jpg) @@ -459,11 +469,11 @@ on_generation() on_stop() ``` -# Examples +## Examples This section gives the complete code of some examples that use `pygad`. Each subsection builds a different example. -## Linear Model Optimization - Single Objective +### Linear Model Optimization - Single Objective This example is discussed in the [Steps to Use PyGAD](https://pygad.readthedocs.io/en/latest/pygad.html#steps-to-use-pygad) section which optimizes a linear model. Its complete code is listed below. @@ -473,7 +483,7 @@ import numpy """ Given the following function: - y = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + 6wx6 + y = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + w6x6 where (x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7) and y=44 What are the best values for the 6 weights (w1 to w6)? We are going to use the genetic algorithm to optimize this function. """ @@ -533,12 +543,12 @@ loaded_ga_instance = pygad.load(filename=filename) loaded_ga_instance.plot_fitness() ``` -## Linear Model Optimization - Multi-Objective +### Linear Model Optimization - Multi-Objective This is a multi-objective optimization example that optimizes these 2 functions: -1. `y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + 6wx6` -2. `y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + 6wx12` +1. `y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + w6x6` +2. `y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + w6x12` Where: @@ -557,8 +567,8 @@ import numpy """ Given these 2 functions: - y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + 6wx6 - y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + 6wx12 + y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + w6x6 + y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + w6x12 where (x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7) and y=50 and (x7,x8,x9,x10,x11,x12)=(-2,0.7,-9,1.4,3,5) and y=30 What are the best values for the 6 weights (w1 to w6)? We are going to use the genetic algorithm to optimize these 2 functions. @@ -622,7 +632,7 @@ This is the figure created by the `plot_fitness()` method. The fitness of the fi ![multi-objective-pygad](https://github.com/ahmedfgad/GeneticAlgorithmPython/assets/16560492/7896f8d8-01c5-4ff9-8d15-52191c309b63) -## Reproducing Images +### Reproducing Images This project reproduces a single image using PyGAD by evolving pixel values. This project works with both color and gray images. Check this project at [GitHub](https://github.com/ahmedfgad/GARI): https://github.com/ahmedfgad/GARI. @@ -631,7 +641,7 @@ For more information about this project, read this tutorial titled [Reproducing - [Heartbeat](https://heartbeat.fritz.ai/reproducing-images-using-a-genetic-algorithm-with-python-91fc701ff84): https://heartbeat.fritz.ai/reproducing-images-using-a-genetic-algorithm-with-python-91fc701ff84 - [LinkedIn](https://www.linkedin.com/pulse/reproducing-images-using-genetic-algorithm-python-ahmed-gad): https://www.linkedin.com/pulse/reproducing-images-using-genetic-algorithm-python-ahmed-gad -### Project Steps +#### Project Steps The steps to follow in order to reproduce an image are as follows: @@ -644,7 +654,7 @@ The steps to follow in order to reproduce an image are as follows: The next sections discusses the code of each of these steps. -### Read an Image +#### Read an Image There is an image named `fruit.jpg` in the [GARI project](https://github.com/ahmedfgad/GARI) which is read according to the next code. @@ -664,7 +674,7 @@ Based on the chromosome representation used in the example, the pixel values can Note that the range of pixel values affect other parameters like the range from which the random values are selected during mutation and also the range of the values used in the initial population. So, be consistent. -### Prepare the Fitness Function +#### Prepare the Fitness Function The next code creates a function that will be used as a fitness function for calculating the fitness value for each solution in the population. This function must be a maximization function that accepts 3 parameters representing the instance of the `pygad.GA` class, a solution, and its index. It returns a value representing the fitness value. @@ -700,7 +710,7 @@ def chromosome2img(vector, shape): return numpy.reshape(vector, shape) ``` -### Create an Instance of the `pygad.GA` Class +#### Create an Instance of the `pygad.GA` Class It is very important to use random mutation and set the `mutation_by_replacement` to `True`. Based on the range of pixel values, the values assigned to the `init_range_low`, `init_range_high`, `random_mutation_min_val`, and `random_mutation_max_val` parameters should be changed. @@ -725,7 +735,7 @@ ga_instance = pygad.GA(num_generations=20000, random_mutation_max_val=1.0) ``` -### Run PyGAD +#### Run PyGAD Simply, call the `run()` method to run PyGAD. @@ -733,7 +743,7 @@ Simply, call the `run()` method to run PyGAD. ga_instance.run() ``` -### Plot Results +#### Plot Results After the `run()` method completes, the fitness values of all generations can be viewed in a plot using the `plot_fitness()` method. @@ -745,7 +755,7 @@ Here is the plot after 20,000 generations. ![Fitness Values](https://user-images.githubusercontent.com/16560492/82232124-77762c00-992e-11ea-9fc6-14a1cd7a04ff.png) -### Calculate Some Statistics +#### Calculate Some Statistics Here is some information about the best solution. @@ -764,7 +774,7 @@ matplotlib.pyplot.title("PyGAD & GARI for Reproducing Images") matplotlib.pyplot.show() ``` -### Evolution by Generation +#### Evolution by Generation The solution reached after the 20,000 generations is shown below. @@ -806,13 +816,13 @@ Generation 20,000 ![solution](https://user-images.githubusercontent.com/16560492/82232405-e0f63a80-992e-11ea-984f-b6ed76465bd1.png) -## Clustering +### Clustering For a 2-cluster problem, the code is available [here](https://github.com/ahmedfgad/GeneticAlgorithmPython/blob/master/example_clustering_2.py). For a 3-cluster problem, the code is [here](https://github.com/ahmedfgad/GeneticAlgorithmPython/blob/master/example_clustering_3.py). The 2 examples are using artificial samples. Soon a tutorial will be published at [Paperspace](https://blog.paperspace.com/author/ahmed) to explain how clustering works using the genetic algorithm with examples in PyGAD. -## CoinTex Game Playing using PyGAD +### CoinTex Game Playing using PyGAD The code is available the [CoinTex GitHub project](https://github.com/ahmedfgad/CoinTex/tree/master/PlayerGA). CoinTex is an Android game written in Python using the Kivy framework. Find CoinTex at [Google Play](https://play.google.com/store/apps/details?id=coin.tex.cointexreactfast): https://play.google.com/store/apps/details?id=coin.tex.cointexreactfast diff --git a/docs/source/pygad.rst b/docs/source/pygad.rst deleted file mode 100644 index a3906ec..0000000 --- a/docs/source/pygad.rst +++ /dev/null @@ -1,1581 +0,0 @@ -``pygad`` Module -================ - -This section of the PyGAD's library documentation discusses the -``pygad`` module. - -Using the ``pygad`` module, instances of the genetic algorithm can be -created, run, saved, and loaded. Single-objective and multi-objective -optimization problems can be solved. - -.. _pygadga-class: - -``pygad.GA`` Class -================== - -The first module available in PyGAD is named ``pygad`` and contains a -class named ``GA`` for building the genetic algorithm. The constructor, -methods, function, and attributes within the class are discussed in this -section. - -.. _init: - -``__init__()`` --------------- - -For creating an instance of the ``pygad.GA`` class, the constructor -accepts several parameters that allow the user to customize the genetic -algorithm to different types of applications. - -The ``pygad.GA`` class constructor supports the following parameters: - -- ``num_generations``: Number of generations. - -- ``num_parents_mating``: Number of solutions to be selected as parents. - -- ``fitness_func``: Accepts a function/method and returns the fitness - value(s) of the solution. If a function is passed, then it must accept - 3 parameters (1. the instance of the ``pygad.GA`` class, 2. a single - solution, and 3. its index in the population). If method, then it - accepts a fourth parameter representing the method's class instance. - Check the `Preparing the fitness_func - Parameter `__ - section for information about creating such a function. In `PyGAD - 3.2.0 `__, - multi-objective optimization is supported. To consider the problem as - multi-objective, just return a ``list``, ``tuple``, or - ``numpy.ndarray`` from the fitness function. - -- ``fitness_batch_size=None``: A new optional parameter called - ``fitness_batch_size`` is supported to calculate the fitness function - in batches. If it is assigned the value ``1`` or ``None`` (default), - then the normal flow is used where the fitness function is called for - each individual solution. If the ``fitness_batch_size`` parameter is - assigned a value satisfying this condition - ``1 < fitness_batch_size <= sol_per_pop``, then the solutions are - grouped into batches of size ``fitness_batch_size`` and the fitness - function is called once for each batch. Check the `Batch Fitness - Calculation `__ - section for more details and examples. Added in from `PyGAD - 2.19.0 `__. - -- ``initial_population``: A user-defined initial population. It is - useful when the user wants to start the generations with a custom - initial population. It defaults to ``None`` which means no initial - population is specified by the user. In this case, - `PyGAD `__ creates an initial - population using the ``sol_per_pop`` and ``num_genes`` parameters. An - exception is raised if the ``initial_population`` is ``None`` while - any of the 2 parameters (``sol_per_pop`` or ``num_genes``) is also - ``None``. Introduced in `PyGAD - 2.0.0 `__ - and higher. - -- ``sol_per_pop``: Number of solutions (i.e. chromosomes) within the - population. This parameter has no action if ``initial_population`` - parameter exists. - -- ``num_genes``: Number of genes in the solution/chromosome. This - parameter is not needed if the user feeds the initial population to - the ``initial_population`` parameter. - -- ``gene_type=float``: Controls the gene type. It can be assigned to a - single data type that is applied to all genes or can specify the data - type of each individual gene. It defaults to ``float`` which means all - genes are of ``float`` data type. Starting from `PyGAD - 2.9.0 `__, - the ``gene_type`` parameter can be assigned to a numeric value of any - of these types: ``int``, ``float``, and - ``numpy.int/uint/float(8-64)``. Starting from `PyGAD - 2.14.0 `__, - it can be assigned to a ``list``, ``tuple``, or a ``numpy.ndarray`` - which hold a data type for each gene (e.g. - ``gene_type=[int, float, numpy.int8]``). This helps to control the - data type of each individual gene. In `PyGAD - 2.15.0 `__, - a precision for the ``float`` data types can be specified (e.g. - ``gene_type=[float, 2]``. - -- ``init_range_low=-4``: The lower value of the random range from which - the gene values in the initial population are selected. - ``init_range_low`` defaults to ``-4``. Available in `PyGAD - 1.0.20 `__ - and higher. This parameter has no action if the ``initial_population`` - parameter exists. - -- ``init_range_high=4``: The upper value of the random range from which - the gene values in the initial population are selected. - ``init_range_high`` defaults to ``+4``. Available in `PyGAD - 1.0.20 `__ - and higher. This parameter has no action if the ``initial_population`` - parameter exists. - -- ``parent_selection_type="sss"``: The parent selection type. Supported - types are ``sss`` (for steady-state selection), ``rws`` (for roulette - wheel selection), ``sus`` (for stochastic universal selection), - ``rank`` (for rank selection), ``random`` (for random selection), and - ``tournament`` (for tournament selection). A custom parent selection - function can be passed starting from `PyGAD - 2.16.0 `__. - Check the `User-Defined Crossover, Mutation, and Parent Selection - Operators `__ - section for more details about building a user-defined parent - selection function. - -- ``keep_parents=-1``: Number of parents to keep in the current - population. ``-1`` (default) means to keep all parents in the next - population. ``0`` means keep no parents in the next population. A - value ``greater than 0`` means keeps the specified number of parents - in the next population. Note that the value assigned to - ``keep_parents`` cannot be ``< - 1`` or greater than the number of - solutions within the population ``sol_per_pop``. Starting from `PyGAD - 2.18.0 `__, - this parameter have an effect only when the ``keep_elitism`` parameter - is ``0``. Starting from `PyGAD - 2.20.0 `__, - the parents' fitness from the last generation will not be re-used if - ``keep_parents=0``. - -- ``keep_elitism=1``: Added in `PyGAD - 2.18.0 `__. - It can take the value ``0`` or a positive integer that satisfies - (``0 <= keep_elitism <= sol_per_pop``). It defaults to ``1`` which - means only the best solution in the current generation is kept in the - next generation. If assigned ``0``, this means it has no effect. If - assigned a positive integer ``K``, then the best ``K`` solutions are - kept in the next generation. It cannot be assigned a value greater - than the value assigned to the ``sol_per_pop`` parameter. If this - parameter has a value different than ``0``, then the ``keep_parents`` - parameter will have no effect. - -- ``K_tournament=3``: In case that the parent selection type is - ``tournament``, the ``K_tournament`` specifies the number of parents - participating in the tournament selection. It defaults to ``3``. - -- ``crossover_type="single_point"``: Type of the crossover operation. - Supported types are ``single_point`` (for single-point crossover), - ``two_points`` (for two points crossover), ``uniform`` (for uniform - crossover), and ``scattered`` (for scattered crossover). Scattered - crossover is supported from PyGAD - `2.9.0 `__ - and higher. It defaults to ``single_point``. A custom crossover - function can be passed starting from `PyGAD - 2.16.0 `__. - Check the `User-Defined Crossover, Mutation, and Parent Selection - Operators `__ - section for more details about creating a user-defined crossover - function. Starting from `PyGAD - 2.2.2 `__ - and higher, if ``crossover_type=None``, then the crossover step is - bypassed which means 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. - -- ``crossover_probability=None``: The probability of selecting a parent - for applying the crossover operation. Its value must be between 0.0 - and 1.0 inclusive. For each parent, a random value between 0.0 and 1.0 - is generated. If this random value is less than or equal to the value - assigned to the ``crossover_probability`` parameter, then the parent - is selected. Added in `PyGAD - 2.5.0 `__ - and higher. - -- ``mutation_type="random"``: Type of the mutation operation. Supported - types are ``random`` (for random mutation), ``swap`` (for swap - mutation), ``inversion`` (for inversion mutation), ``scramble`` (for - scramble mutation), and ``adaptive`` (for adaptive mutation). It - defaults to ``random``. A custom mutation function can be passed - starting from `PyGAD - 2.16.0 `__. - Check the `User-Defined Crossover, Mutation, and Parent Selection - Operators `__ - section for more details about creating a user-defined mutation - function. Starting from `PyGAD - 2.2.2 `__ - and higher, if ``mutation_type=None``, then the mutation step is - bypassed which means 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. ``Adaptive`` - mutation is supported starting from `PyGAD - 2.10.0 `__. - For more information about adaptive mutation, go the the `Adaptive - Mutation `__ - section. For example about using adaptive mutation, check the `Use - Adaptive Mutation in - PyGAD `__ - section. - -- ``mutation_probability=None``: The probability of selecting a gene for - applying the mutation operation. Its value must be between 0.0 and 1.0 - inclusive. For each gene in a solution, a random value between 0.0 and - 1.0 is generated. If this random value is less than or equal to the - value assigned to the ``mutation_probability`` parameter, then the - gene is selected. If this parameter exists, then there is no need for - the 2 parameters ``mutation_percent_genes`` and - ``mutation_num_genes``. Added in `PyGAD - 2.5.0 `__ - and higher. - -- ``mutation_by_replacement=False``: An optional bool parameter. It - works only when the selected type of mutation is random - (``mutation_type="random"``). In this case, - ``mutation_by_replacement=True`` means replace the gene by the - randomly generated value. If False, then it has no effect and random - mutation works by adding the random value to the gene. Supported in - `PyGAD - 2.2.2 `__ - and higher. Check the changes in `PyGAD - 2.2.2 `__ - under the Release History section for an example. - -- ``mutation_percent_genes="default"``: Percentage of genes to mutate. - It defaults to the string ``"default"`` which is later translated into - the integer ``10`` which means 10% of the genes will be mutated. It - must be ``>0`` and ``<=100``. Out of this percentage, the number of - genes to mutate is deduced which is assigned to the - ``mutation_num_genes`` parameter. The ``mutation_percent_genes`` - parameter has no action if ``mutation_probability`` or - ``mutation_num_genes`` exist. Starting from `PyGAD - 2.2.2 `__ - and higher, this parameter has no action if ``mutation_type`` is - ``None``. - -- ``mutation_num_genes=None``: Number of genes to mutate which defaults - to ``None`` meaning that no number is specified. The - ``mutation_num_genes`` parameter has no action if the parameter - ``mutation_probability`` exists. Starting from `PyGAD - 2.2.2 `__ - and higher, this parameter has no action if ``mutation_type`` is - ``None``. - -- ``random_mutation_min_val=-1.0``: For ``random`` mutation, the - ``random_mutation_min_val`` parameter specifies the start value of the - range from which a random value is selected to be added to the gene. - It defaults to ``-1``. Starting from `PyGAD - 2.2.2 `__ - and higher, this parameter has no action if ``mutation_type`` is - ``None``. - -- ``random_mutation_max_val=1.0``: For ``random`` mutation, the - ``random_mutation_max_val`` parameter specifies the end value of the - range from which a random value is selected to be added to the gene. - It defaults to ``+1``. Starting from `PyGAD - 2.2.2 `__ - and higher, this parameter has no action if ``mutation_type`` is - ``None``. - -- ``gene_space=None``: It is used to specify the possible values for - each gene in case the user wants to restrict the gene values. It is - useful if the gene space is restricted to a certain range or to - discrete values. It accepts a ``list``, ``range``, or - ``numpy.ndarray``. When all genes have the same global space, specify - their values as a ``list``/``tuple``/``range``/``numpy.ndarray``. For - example, ``gene_space = [0.3, 5.2, -4, 8]`` restricts the gene values - to the 4 specified values. If each gene has its own space, then the - ``gene_space`` parameter can be nested like - ``[[0.4, -5], [0.5, -3.2, 8.2, -9], ...]`` where the first sublist - determines the values for the first gene, the second sublist for the - second gene, and so on. If the nested list/tuple has a ``None`` value, - then the gene's initial value is selected randomly from the range - specified by the 2 parameters ``init_range_low`` and - ``init_range_high`` and its mutation value is selected randomly from - the range specified by the 2 parameters ``random_mutation_min_val`` - and ``random_mutation_max_val``. ``gene_space`` is added in `PyGAD - 2.5.0 `__. - Check the `Release History of PyGAD - 2.5.0 `__ - section of the documentation for more details. In `PyGAD - 2.9.0 `__, - NumPy arrays can be assigned to the ``gene_space`` parameter. In - `PyGAD - 2.11.0 `__, - the ``gene_space`` parameter itself or any of its elements can be - assigned to a dictionary to specify the lower and upper limits of the - genes. For example, ``{'low': 2, 'high': 4}`` means the minimum and - maximum values are 2 and 4, respectively. In `PyGAD - 2.15.0 `__, - a new key called ``"step"`` is supported to specify the step of moving - from the start to the end of the range specified by the 2 existing - keys ``"low"`` and ``"high"``. - -- ``gene_constraint=None``: A list of callables (i.e. functions) acting - as constraints for the gene values. Before selecting a value for a - gene, the callable is called to ensure the candidate value is valid. - Added in `PyGAD - 3.5.0 `__. - Check the `Gene - Constraint `__ - section for more information. - -- ``sample_size=100``: In some cases where a gene value is to be - selected, this variable defines the size of the sample from which a - value is selected randomly. Useful if either ``allow_duplicate_genes`` - or ``gene_constraint`` is used. If PyGAD failed to find a unique value - or a value that meets a gene constraint, it is recommended to - increases this parameter's value. Added in `PyGAD - 3.5.0 `__. - Check the `sample_size - Parameter `__ - section for more information. - -- ``on_start=None``: Accepts a function/method to be called only once - before the genetic algorithm starts its evolution. If function, then - it must accept a single parameter representing the instance of the - genetic algorithm. If method, then it must accept 2 parameters where - the second one refers to the method's object. Added in `PyGAD - 2.6.0 `__. - -- ``on_fitness=None``: Accepts a function/method to be called after - calculating the fitness values of all solutions in the population. If - function, then it must accept 2 parameters: 1) a list of all - solutions' fitness values 2) the instance of the genetic algorithm. If - method, then it must accept 3 parameters where the third one refers to - the method's object. Added in `PyGAD - 2.6.0 `__. - -- ``on_parents=None``: Accepts a function/method to be called after - selecting the parents that mates. If function, then it must accept 2 - parameters: 1) the selected parents 2) the instance of the genetic - algorithm If method, then it must accept 3 parameters where the third - one refers to the method's object. Added in `PyGAD - 2.6.0 `__. - -- ``on_crossover=None``: Accepts a function to be called each time the - crossover operation is applied. This function must accept 2 - parameters: the first one represents the instance of the genetic - algorithm and the second one represents the offspring generated using - crossover. Added in `PyGAD - 2.6.0 `__. - -- ``on_mutation=None``: Accepts a function to be called each time the - mutation operation is applied. This function must accept 2 parameters: - the first one represents the instance of the genetic algorithm and the - second one represents the offspring after applying the mutation. Added - in `PyGAD - 2.6.0 `__. - -- ``on_generation=None``: Accepts a function to be called after each - generation. This function must accept a single parameter representing - the instance of the genetic algorithm. If the function returned the - string ``stop``, then the ``run()`` method stops without completing - the other generations. Added in `PyGAD - 2.6.0 `__. - -- ``on_stop=None``: Accepts a function to be called only once exactly - before the genetic algorithm stops or when it completes all the - generations. This function must accept 2 parameters: the first one - represents the instance of the genetic algorithm and the second one is - a list of fitness values of the last population's solutions. Added in - `PyGAD - 2.6.0 `__. - -- ``save_best_solutions=False``: When ``True``, then the best solution - after each generation is saved into an attribute named - ``best_solutions``. If ``False`` (default), then no solutions are - saved and the ``best_solutions`` attribute will be empty. Supported in - `PyGAD - 2.9.0 `__. - -- ``save_solutions=False``: If ``True``, then all solutions in each - generation are appended into an attribute called ``solutions`` which - is NumPy array. Supported in `PyGAD - 2.15.0 `__. - -- ``suppress_warnings=False``: A bool parameter to control whether the - warning messages are printed or not. It defaults to ``False``. - -- ``allow_duplicate_genes=True``: Added in `PyGAD - 2.13.0 `__. - If ``True``, then a solution/chromosome may have duplicate gene - values. If ``False``, then each gene will have a unique value in its - solution. - -- ``stop_criteria=None``: Some criteria to stop the evolution. Added in - `PyGAD - 2.15.0 `__. - Each criterion is passed as ``str`` which has a stop word. The current - 2 supported words are ``reach`` and ``saturate``. ``reach`` stops the - ``run()`` method if the fitness value is equal to or greater than a - given fitness value. An example for ``reach`` is ``"reach_40"`` which - stops the evolution if the fitness is >= 40. ``saturate`` means stop - the evolution if the fitness saturates for a given number of - consecutive generations. An example for ``saturate`` is - ``"saturate_7"`` which means stop the ``run()`` method if the fitness - does not change for 7 consecutive generations. - -- ``parallel_processing=None``: Added in `PyGAD - 2.17.0 `__. - If ``None`` (Default), this means no parallel processing is applied. - It can accept a list/tuple of 2 elements [1) Can be either - ``'process'`` or ``'thread'`` to indicate whether processes or threads - are used, respectively., 2) The number of processes or threads to - use.]. For example, ``parallel_processing=['process', 10]`` applies - parallel processing with 10 processes. If a positive integer is - assigned, then it is used as the number of threads. For example, - ``parallel_processing=5`` uses 5 threads which is equivalent to - ``parallel_processing=["thread", 5]``. For more information, check the - `Parallel Processing in - PyGAD `__ - section. - -- ``random_seed=None``: Added in `PyGAD - 2.18.0 `__. - It defines the random seed to be used by the random function - generators (we use random functions in the NumPy and random modules). - This helps to reproduce the same results by setting the same random - seed (e.g. ``random_seed=2``). If given the value ``None``, then it - has no effect. - -- ``logger=None``: Accepts an instance of the ``logging.Logger`` class - to log the outputs. Any message is no longer printed using ``print()`` - but logged. If ``logger=None``, then a logger is created that uses - ``StreamHandler`` to logs the messages to the console. Added in `PyGAD - 3.0.0 `__. - Check the `Logging - Outputs `__ - for more information. - -The user doesn't have to specify all of such parameters while creating -an instance of the GA class. A very important parameter you must care -about is ``fitness_func`` which defines the fitness function. - -It is OK to set the value of any of the 2 parameters ``init_range_low`` -and ``init_range_high`` to be equal, higher, or lower than the other -parameter (i.e. ``init_range_low`` is not needed to be lower than -``init_range_high``). The same holds for the ``random_mutation_min_val`` -and ``random_mutation_max_val`` parameters. - -If the 2 parameters ``mutation_type`` and ``crossover_type`` are -``None``, this disables any type of evolution the genetic algorithm can -make. As a result, the genetic algorithm cannot find a better solution -that the best solution in the initial population. - -The parameters are validated by calling the ``validate_parameters()`` -method of the ``utils.validation.Validation`` class within the -constructor. If at least a parameter is not correct, an exception is -thrown and the ``valid_parameters`` attribute is set to ``False``. - -Extended Classes -================ - -To make the library modular and structured, different scripts are -created where each script has one or more classes. Each class has its -own objective. - -This is the list of scripts and classes within them where the -``pygad.GA`` class extends: - -1. ``utils/engine.py``: - - 1. ``utils.engine.GAEngine``: - -2. ``utils/validation.py`` - - 1. ``utils.validation.Validation`` - -3. ``utils/parent_selection.py`` - - 1. ``utils.parent_selection.ParentSelection`` - -4. ``utils/crossover.py`` - - 1. ``utils.crossover.Crossover`` - -5. ``utils/mutation.py`` - - 1. ``utils.mutation.Mutation`` - -6. ``utils/nsga2.py`` - - 1. ``utils.nsga2.NSGA2`` - -7. ``helper/unique.py`` - - 1. ``helper.unique.Unique`` - -8. ``helper/misc.py`` - - 1. ``helper.misc.Helper`` - -9. ``visualize/plot.py`` - - 1. ``visualize.plot.Plot`` - -Since the ``pygad.GA`` class extends such classes, the attributes and -methods inside them can be retrieved by instances of the ``pygad.GA`` -class. - -Class Attributes ----------------- - -- ``supported_int_types``: A list of the supported types for the integer - numbers. - -- ``supported_float_types``: A list of the supported types for the - floating-point numbers. - -- ``supported_int_float_types``: A list of the supported types for all - numbers. It just concatenates the previous 2 lists. - -.. _other-instance-attributes--methods: - -Other Instance Attributes & Methods ------------------------------------ - -All the parameters and functions passed to the ``pygad.GA`` class -constructor are used as class attributes and methods in the instances of -the ``pygad.GA`` class. In addition to such attributes, there are other -attributes and methods added to the instances of the ``pygad.GA`` class: - -The next 2 subsections list such attributes and methods. - - The ``GA`` class gains the attributes of its parent classes via - inheritance, making them accessible through the ``GA`` object even if - they are defined externally to its specific class body. - -Other Attributes -~~~~~~~~~~~~~~~~ - -- ``generations_completed``: Holds the number of the last completed - generation. - -- ``population``: A NumPy array that initially holds the initial - population and is later updated after each generation. - -- ``valid_parameters``: Set to ``True`` when all the parameters passed - in the ``GA`` class constructor are valid. - -- ``run_completed``: Set to ``True`` only after the ``run()`` method - completes gracefully. - -- ``pop_size``: The population size. - -- ``best_solutions_fitness``: A list holding the fitness values of the - best solutions for all generations. - -- ``best_solution_generation``: The generation number at which the best - fitness value is reached. It is only assigned the generation number - after the ``run()`` method completes. Otherwise, its value is -1. - -- ``best_solutions``: A NumPy array holding the best solution per each - generation. It only exists when the ``save_best_solutions`` parameter - in the ``pygad.GA`` class constructor is set to ``True``. - -- ``last_generation_fitness``: The fitness values of the solutions in - the last generation. `Added in PyGAD - 2.12.0 `__. - -- ``previous_generation_fitness``: At the end of each generation, the - fitness of the most recent population is saved in the - ``last_generation_fitness`` attribute. The fitness of the population - exactly preceding this most recent population is saved in the - ``previous_generation_fitness`` attribute. This - ``previous_generation_fitness`` attribute is used to fetch the - pre-calculated fitness instead of calling the fitness function for - already explored solutions. `Added in PyGAD - 2.16.2 `__. - -- ``last_generation_parents``: The parents selected from the last - generation. `Added in PyGAD - 2.12.0 `__. - -- ``last_generation_offspring_crossover``: The offspring generated after - applying the crossover in the last generation. `Added in PyGAD - 2.12.0 `__. - -- ``last_generation_offspring_mutation``: The offspring generated after - applying the mutation in the last generation. `Added in PyGAD - 2.12.0 `__. - -- ``gene_type_single``: A flag that is set to ``True`` if the - ``gene_type`` parameter is assigned to a single data type that is - applied to all genes. If ``gene_type`` is assigned a ``list``, - ``tuple``, or ``numpy.ndarray``, then the value of - ``gene_type_single`` will be ``False``. `Added in PyGAD - 2.14.0 `__. - -- ``last_generation_parents_indices``: This attribute holds the indices - of the selected parents in the last generation. Supported in `PyGAD - 2.15.0 `__. - -- ``last_generation_elitism``: This attribute holds the elitism of the - last generation. It is effective only if the ``keep_elitism`` - parameter has a non-zero value. Supported in `PyGAD - 2.18.0 `__. - -- ``last_generation_elitism_indices``: This attribute holds the indices - of the elitism of the last generation. It is effective only if the - ``keep_elitism`` parameter has a non-zero value. Supported in `PyGAD - 2.19.0 `__. - -- ``logger``: This attribute holds the logger from the ``logging`` - module. Supported in `PyGAD - 3.0.0 `__. - -- ``gene_space_unpacked``: This is the unpacked version of the - ``gene_space`` parameter. For example, ``range(1, 5)`` is unpacked to - ``[1, 2, 3, 4]``. For an infinite range like - ``{'low': 2, 'high': 4}``, then it is unpacked to a limited number of - values (e.g. 100). Supported in `PyGAD - 3.1.0 `__. - -- ``pareto_fronts``: A new instance attribute named ``pareto_fronts`` - added to the ``pygad.GA`` instances that holds the pareto fronts when - solving a multi-objective problem. Supported in `PyGAD - 3.2.0 `__. - -Note that the attributes with names starting with ``last_generation_`` -are updated after each generation. - -Other Methods -~~~~~~~~~~~~~ - -- ``cal_pop_fitness()``: A method that calculates the fitness values for - all solutions within the population by calling the function passed to - the ``fitness_func`` parameter for each solution. - -- ``crossover()``: Refers to the method that applies the crossover - operator based on the selected type of crossover in the - ``crossover_type`` property. - -- ``mutation()``: Refers to the method that applies the mutation - operator based on the selected type of mutation in the - ``mutation_type`` property. - -- ``select_parents()``: Refers to a method that selects the parents - based on the parent selection type specified in the - ``parent_selection_type`` attribute. - -- ``adaptive_mutation_population_fitness()``: Returns the average - fitness value used in the adaptive mutation to filter the solutions. - -- ``summary()``: Prints a Keras-like summary of the PyGAD lifecycle. - This helps to have an overview of the architecture. Supported in - `PyGAD - 2.19.0 `__. - Check the `Print Lifecycle - Summary `__ - section for more details and examples. - -- 5 methods with names starting with ``run_``. Their purpose is to keep - the main loop inside the ``run()`` method clean. The details inside - the loop are moved to 4 individual methods. Generally, any method with - a name starting with ``run_`` is meant to be called by PyGAD from - inside the ``run()`` method. Supported in `PyGAD - 3.3.1 `__. - - 1. ``run_loop_head()``: The code before the loop starts. - - 2. ``run_select_parents(call_on_parents=True)``: Select the parents - and call the callable ``on_parents()`` if defined. If - ``call_on_parents`` is ``True``, then the callable ``on_parents()`` - is called. It must be ``False`` when the ``run_select_parents()`` - method is called to update the parents at the end of the ``run()`` - method. - - 3. ``run_crossover()``: Apply crossover and call the callable - ``on_crossover()`` if defined. - - 4. ``run_mutation()``: Apply mutation and call the callable - ``on_mutation()`` if defined. - - 5. ``run_update_population()``: Update the ``population`` attribute - after completing the processes of crossover and mutation. - -There are many methods that are not designed for user usage. Some of -them are listed above but this is not a comprehensive list. The `release -history `__ -section usually covers them. Moreover, you can check the `PyGAD GitHub -repository `__ to -find more. - -The next sections discuss the methods available in the ``pygad.GA`` -class. - -``save()`` ----------- - -The ``save()`` method in the ``pygad.GA`` class saves the genetic -algorithm instance as a pickled object. - -Accepts the following parameter: - -- ``filename``: Name of the file to save the instance. No extension is - needed. - -Functions in ``pygad`` -====================== - -Besides the methods available in the ``pygad.GA`` class, this section -discusses the functions available in ``pygad``. Up to this time, there -is only a single function named ``load()``. - -.. _pygadload: - -``pygad.load()`` ----------------- - -Reads a saved instance of the genetic algorithm. This is not a method -but a function that is indented under the ``pygad`` module. So, it could -be called by the pygad module as follows: ``pygad.load(filename)``. - -Accepts the following parameter: - -- ``filename``: Name of the file holding the saved instance of the - genetic algorithm. No extension is needed. - -Returns the genetic algorithm instance. - -Steps to Use ``pygad`` -====================== - -To use the ``pygad`` module, here is a summary of the required steps: - -1. Preparing the ``fitness_func`` parameter. - -2. Preparing Other Parameters. - -3. Import ``pygad``. - -4. Create an Instance of the ``pygad.GA`` Class. - -5. Run the Genetic Algorithm. - -6. Plotting Results. - -7. Information about the Best Solution. - -8. Saving & Loading the Results. - -Let's discuss how to do each of these steps. - -.. _preparing-the-fitnessfunc-parameter: - -Preparing the ``fitness_func`` Parameter ------------------------------------------ - -Even though some steps in the genetic algorithm pipeline can work the -same regardless of the problem being solved, one critical step is the -calculation of the fitness value. There is no unique way of calculating -the fitness value and it changes from one problem to another. - -PyGAD has a parameter called ``fitness_func`` that allows the user to -specify a custom function/method to use when calculating the fitness. -This function/method must be a maximization function/method so that a -solution with a high fitness value returned is selected compared to a -solution with a low value. - -The fitness function is where the user can decide whether the -optimization problem is single-objective or multi-objective. - -- If the fitness function returns a numeric value, then the problem is - single-objective. The numeric data types supported by PyGAD are listed - in the ``supported_int_float_types`` variable of the ``pygad.GA`` - class. - -- If the fitness function returns a ``list``, ``tuple``, or - ``numpy.ndarray``, then the problem is multi-objective. Even if there - is only one element, the problem is still considered multi-objective. - Each element represents the fitness value of its corresponding - objective. - -Using a user-defined fitness function allows the user to freely use -PyGAD to solve any problem by passing the appropriate fitness -function/method. It is very important to understand the problem well -before creating it. - -Let's discuss an example: - - | Given the following function: - | y = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + 6wx6 - | where (x1,x2,x3,x4,x5,x6)=(4, -2, 3.5, 5, -11, -4.7) and y=44 - | What are the best values for the 6 weights (w1 to w6)? We are going - to use the genetic algorithm to optimize this function. - -So, the task is about using the genetic algorithm to find the best -values for the 6 weight ``W1`` to ``W6``. Thinking of the problem, it is -clear that the best solution is that returning an output that is close -to the desired output ``y=44``. So, the fitness function/method should -return a value that gets higher when the solution's output is closer to -``y=44``. Here is a function that does that: - -.. code:: python - - function_inputs = [4, -2, 3.5, 5, -11, -4.7] # Function inputs. - desired_output = 44 # Function output. - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution*function_inputs) - fitness = 1.0 / numpy.abs(output - desired_output) - return fitness - -Because the fitness function returns a numeric value, then the problem -is single-objective. - -Such a user-defined function must accept 3 parameters: - -1. The instance of the ``pygad.GA`` class. This helps the user to fetch - any property that helps when calculating the fitness. - -2. The solution(s) to calculate the fitness value(s). Note that the - fitness function can accept multiple solutions only if the - ``fitness_batch_size`` is given a value greater than 1. - -3. The indices of the solutions in the population. The number of indices - also depends on the ``fitness_batch_size`` parameter. - -If a method is passed to the ``fitness_func`` parameter, then it accepts -a fourth parameter representing the method's instance. - -The ``__code__`` object is used to check if this function accepts the -required number of parameters. If more or fewer parameters are passed, -an exception is thrown. - -By creating this function, you did a very important step towards using -PyGAD. - -Preparing Other Parameters -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Here is an example for preparing the other parameters: - -.. code:: python - - num_generations = 50 - num_parents_mating = 4 - - fitness_function = fitness_func - - sol_per_pop = 8 - num_genes = len(function_inputs) - - init_range_low = -2 - init_range_high = 5 - - parent_selection_type = "sss" - keep_parents = 1 - - crossover_type = "single_point" - - mutation_type = "random" - mutation_percent_genes = 10 - -.. _the-ongeneration-parameter: - -The ``on_generation`` Parameter -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -An optional parameter named ``on_generation`` is supported which allows -the user to call a function (with a single parameter) after each -generation. Here is a simple function that just prints the current -generation number and the fitness value of the best solution in the -current generation. The ``generations_completed`` attribute of the GA -class returns the number of the last completed generation. - -.. code:: python - - def on_gen(ga_instance): - print("Generation : ", ga_instance.generations_completed) - print("Fitness of the best solution :", ga_instance.best_solution()[1]) - -After being defined, the function is assigned to the ``on_generation`` -parameter of the GA class constructor. By doing that, the ``on_gen()`` -function will be called after each generation. - -.. code:: python - - ga_instance = pygad.GA(..., - on_generation=on_gen, - ...) - -After the parameters are prepared, we can import PyGAD and build an -instance of the ``pygad.GA`` class. - -Import ``pygad`` ----------------- - -The next step is to import PyGAD as follows: - -.. code:: python - - import pygad - -The ``pygad.GA`` class holds the implementation of all methods for -running the genetic algorithm. - -.. _create-an-instance-of-the-pygadga-class: - -Create an Instance of the ``pygad.GA`` Class --------------------------------------------- - -The ``pygad.GA`` class is instantiated where the previously prepared -parameters are fed to its constructor. The constructor is responsible -for creating the initial population. - -.. code:: python - - ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - fitness_func=fitness_function, - sol_per_pop=sol_per_pop, - num_genes=num_genes, - init_range_low=init_range_low, - init_range_high=init_range_high, - parent_selection_type=parent_selection_type, - keep_parents=keep_parents, - crossover_type=crossover_type, - mutation_type=mutation_type, - mutation_percent_genes=mutation_percent_genes) - -Run the Genetic Algorithm -------------------------- - -After an instance of the ``pygad.GA`` class is created, the next step is -to call the ``run()`` method as follows: - -.. code:: python - - ga_instance.run() - -Inside this method, the genetic algorithm evolves over some generations -by doing the following tasks: - -1. Calculating the fitness values of the solutions within the current - population. - -2. Select the best solutions as parents in the mating pool. - -3. Apply the crossover & mutation operation - -4. Repeat the process for the specified number of generations. - -Plotting Results ----------------- - -There is a method named ``plot_fitness()`` which creates a figure -summarizing how the fitness values of the solutions change with the -generations. - -.. code:: python - - ga_instance.plot_fitness() - -|image1| - -Information about the Best Solution ------------------------------------ - -The following information about the best solution in the last population -is returned using the ``best_solution()`` method. - -- Solution - -- Fitness value of the solution - -- Index of the solution within the population - -.. code:: python - - solution, solution_fitness, solution_idx = ga_instance.best_solution() - print(f"Parameters of the best solution : {solution}") - print(f"Fitness value of the best solution = {solution_fitness}") - print(f"Index of the best solution : {solution_idx}") - -Using the ``best_solution_generation`` attribute of the instance from -the ``pygad.GA`` class, the generation number at which the -``best fitness`` is reached could be fetched. - -.. code:: python - - if ga_instance.best_solution_generation != -1: - print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") - -.. _saving--loading-the-results: - -Saving & Loading the Results ----------------------------- - -After the ``run()`` method completes, it is possible to save the current -instance of the genetic algorithm to avoid losing the progress made. The -``save()`` method is available for that purpose. Just pass the file name -to it without an extension. According to the next code, a file named -``genetic.pkl`` will be created and saved in the current directory. - -.. code:: python - - filename = 'genetic' - ga_instance.save(filename=filename) - -You can also load the saved model using the ``load()`` function and -continue using it. For example, you might run the genetic algorithm for -some generations, save its current state using the ``save()`` method, -load the model using the ``load()`` function, and then call the -``run()`` method again. - -.. code:: python - - loaded_ga_instance = pygad.load(filename=filename) - -After the instance is loaded, you can use it to run any method or access -any property. - -.. code:: python - - print(loaded_ga_instance.best_solution()) - -Life Cycle of PyGAD -=================== - -The next figure lists the different stages in the lifecycle of an -instance of the ``pygad.GA`` class. Note that PyGAD stops when either -all generations are completed or when the function passed to the -``on_generation`` parameter returns the string ``stop``. - -|image2| - -The next code implements all the callback functions to trace the -execution of the genetic algorithm. Each callback function prints its -name. - -.. code:: python - - import pygad - import numpy - - function_inputs = [4,-2,3.5,5,-11,-4.7] - desired_output = 44 - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution*function_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - - fitness_function = fitness_func - - def on_start(ga_instance): - print("on_start()") - - def on_fitness(ga_instance, population_fitness): - print("on_fitness()") - - def on_parents(ga_instance, selected_parents): - print("on_parents()") - - def on_crossover(ga_instance, offspring_crossover): - print("on_crossover()") - - def on_mutation(ga_instance, offspring_mutation): - print("on_mutation()") - - def on_generation(ga_instance): - print("on_generation()") - - def on_stop(ga_instance, last_population_fitness): - print("on_stop()") - - ga_instance = pygad.GA(num_generations=3, - num_parents_mating=5, - fitness_func=fitness_function, - sol_per_pop=10, - num_genes=len(function_inputs), - on_start=on_start, - on_fitness=on_fitness, - on_parents=on_parents, - on_crossover=on_crossover, - on_mutation=on_mutation, - on_generation=on_generation, - on_stop=on_stop) - - ga_instance.run() - -Based on the used 3 generations as assigned to the ``num_generations`` -argument, here is the output. - -.. code:: - - on_start() - - on_fitness() - on_parents() - on_crossover() - on_mutation() - on_generation() - - on_fitness() - on_parents() - on_crossover() - on_mutation() - on_generation() - - on_fitness() - on_parents() - on_crossover() - on_mutation() - on_generation() - - on_stop() - -Examples -======== - -This section gives the complete code of some examples that use -``pygad``. Each subsection builds a different example. - -Linear Model Optimization - Single Objective --------------------------------------------- - -This example is discussed in the `Steps to Use -PyGAD `__ -section which optimizes a linear model. Its complete code is listed -below. - -.. code:: python - - import pygad - import numpy - - """ - Given the following function: - y = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + 6wx6 - where (x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7) and y=44 - What are the best values for the 6 weights (w1 to w6)? We are going to use the genetic algorithm to optimize this function. - """ - - function_inputs = [4,-2,3.5,5,-11,-4.7] # Function inputs. - desired_output = 44 # Function output. - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution*function_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - - num_generations = 100 # Number of generations. - num_parents_mating = 10 # Number of solutions to be selected as parents in the mating pool. - - sol_per_pop = 20 # Number of solutions in the population. - num_genes = len(function_inputs) - - last_fitness = 0 - def on_generation(ga_instance): - global last_fitness - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1]}") - print(f"Change = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1] - last_fitness}") - last_fitness = ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1] - - ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - sol_per_pop=sol_per_pop, - num_genes=num_genes, - fitness_func=fitness_func, - on_generation=on_generation) - - # Running the GA to optimize the parameters of the function. - ga_instance.run() - - ga_instance.plot_fitness() - - # Returning the details of the best solution. - solution, solution_fitness, solution_idx = ga_instance.best_solution(ga_instance.last_generation_fitness) - print(f"Parameters of the best solution : {solution}") - print(f"Fitness value of the best solution = {solution_fitness}") - print(f"Index of the best solution : {solution_idx}") - - prediction = numpy.sum(numpy.array(function_inputs)*solution) - print(f"Predicted output based on the best solution : {prediction}") - - if ga_instance.best_solution_generation != -1: - print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") - - # Saving the GA instance. - filename = 'genetic' # The filename to which the instance is saved. The name is without extension. - ga_instance.save(filename=filename) - - # Loading the saved GA instance. - loaded_ga_instance = pygad.load(filename=filename) - loaded_ga_instance.plot_fitness() - -Linear Model Optimization - Multi-Objective -------------------------------------------- - -This is a multi-objective optimization example that optimizes these 2 -functions: - -1. ``y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + 6wx6`` - -2. ``y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + 6wx12`` - -Where: - -1. ``(x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7)`` and ``y=50`` - -2. ``(x7,x8,x9,x10,x11,x12)=(-2,0.7,-9,1.4,3,5)`` and ``y=30`` - -The 2 functions use the same parameters (weights) ``w1`` to ``w6``. - -The goal is to use PyGAD to find the optimal values for such weights -that satisfy the 2 functions ``y1`` and ``y2``. - -To use PyGAD to solve multi-objective problems, the only adjustment is -to return a ``list``, ``tuple``, or ``numpy.ndarray`` from the fitness -function. Each element represents the fitness of an objective in order. -That is the first element is the fitness of the first objective, the -second element is the fitness for the second objective, and so on. - -.. code:: python - - import pygad - import numpy - - """ - Given these 2 functions: - y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + 6wx6 - y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + 6wx12 - where (x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7) and y=50 - and (x7,x8,x9,x10,x11,x12)=(-2,0.7,-9,1.4,3,5) and y=30 - What are the best values for the 6 weights (w1 to w6)? We are going to use the genetic algorithm to optimize these 2 functions. - This is a multi-objective optimization problem. - - PyGAD considers the problem as multi-objective if the fitness function returns: - 1) List. - 2) Or tuple. - 3) Or numpy.ndarray. - """ - - function_inputs1 = [4,-2,3.5,5,-11,-4.7] # Function 1 inputs. - function_inputs2 = [-2,0.7,-9,1.4,3,5] # Function 2 inputs. - desired_output1 = 50 # Function 1 output. - desired_output2 = 30 # Function 2 output. - - def fitness_func(ga_instance, solution, solution_idx): - output1 = numpy.sum(solution*function_inputs1) - output2 = numpy.sum(solution*function_inputs2) - fitness1 = 1.0 / (numpy.abs(output1 - desired_output1) + 0.000001) - fitness2 = 1.0 / (numpy.abs(output2 - desired_output2) + 0.000001) - return [fitness1, fitness2] - - num_generations = 100 - num_parents_mating = 10 - - sol_per_pop = 20 - num_genes = len(function_inputs1) - - ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - sol_per_pop=sol_per_pop, - num_genes=num_genes, - fitness_func=fitness_func, - parent_selection_type='nsga2') - - ga_instance.run() - - ga_instance.plot_fitness(label=['Obj 1', 'Obj 2']) - - solution, solution_fitness, solution_idx = ga_instance.best_solution(ga_instance.last_generation_fitness) - print(f"Parameters of the best solution : {solution}") - print(f"Fitness value of the best solution = {solution_fitness}") - - prediction = numpy.sum(numpy.array(function_inputs1)*solution) - print(f"Predicted output 1 based on the best solution : {prediction}") - prediction = numpy.sum(numpy.array(function_inputs2)*solution) - print(f"Predicted output 2 based on the best solution : {prediction}") - -This is the result of the print statements. The predicted outputs are -close to the desired outputs. - -.. code:: - - Parameters of the best solution : [ 0.79676439 -2.98823386 -4.12677662 5.70539445 -2.02797016 -1.07243922] - Fitness value of the best solution = [ 1.68090829 349.8591915 ] - Predicted output 1 based on the best solution : 50.59491545442283 - Predicted output 2 based on the best solution : 29.99714270722312 - -This is the figure created by the ``plot_fitness()`` method. The fitness -of the first objective has the green color. The blue color is used for -the second objective fitness. - -|image3| - -Reproducing Images ------------------- - -This project reproduces a single image using PyGAD by evolving pixel -values. This project works with both color and gray images. Check this -project at `GitHub `__: -https://github.com/ahmedfgad/GARI. - -For more information about this project, read this tutorial titled -`Reproducing Images using a Genetic Algorithm with -Python `__ -available at these links: - -- `Heartbeat `__: - https://heartbeat.fritz.ai/reproducing-images-using-a-genetic-algorithm-with-python-91fc701ff84 - -- `LinkedIn `__: - https://www.linkedin.com/pulse/reproducing-images-using-genetic-algorithm-python-ahmed-gad - -Project Steps -~~~~~~~~~~~~~ - -The steps to follow in order to reproduce an image are as follows: - -- Read an image - -- Prepare the fitness function - -- Create an instance of the pygad.GA class with the appropriate - parameters - -- Run PyGAD - -- Plot results - -- Calculate some statistics - -The next sections discusses the code of each of these steps. - -Read an Image -~~~~~~~~~~~~~ - -There is an image named ``fruit.jpg`` in the `GARI -project `__ which is read according -to the next code. - -.. code:: python - - import imageio - import numpy - - target_im = imageio.imread('fruit.jpg') - target_im = numpy.asarray(target_im/255, dtype=float) - -Here is the read image. - -|image4| - -Based on the chromosome representation used in the example, the pixel -values can be either in the 0-255, 0-1, or any other ranges. - -Note that the range of pixel values affect other parameters like the -range from which the random values are selected during mutation and also -the range of the values used in the initial population. So, be -consistent. - -Prepare the Fitness Function -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The next code creates a function that will be used as a fitness function -for calculating the fitness value for each solution in the population. -This function must be a maximization function that accepts 3 parameters -representing the instance of the ``pygad.GA`` class, a solution, and its -index. It returns a value representing the fitness value. - -.. code:: python - - import gari - - target_chromosome = gari.img2chromosome(target_im) - - def fitness_fun(ga_instance, solution, solution_idx): - fitness = numpy.sum(numpy.abs(target_chromosome-solution)) - - # Negating the fitness value to make it increasing rather than decreasing. - fitness = numpy.sum(target_chromosome) - fitness - return fitness - -The fitness value is calculated using the sum of absolute difference -between genes values in the original and reproduced chromosomes. The -``gari.img2chromosome()`` function is called before the fitness function -to represent the image as a vector because the genetic algorithm can -work with 1D chromosomes. - -The implementation of the ``gari`` module is available at the `GARI -GitHub -project `__ and -its code is listed below. - -.. code:: python - - import numpy - import functools - import operator - - def img2chromosome(img_arr): - return numpy.reshape(img_arr, (functools.reduce(operator.mul, img_arr.shape))) - - def chromosome2img(vector, shape): - if len(vector) != functools.reduce(operator.mul, shape): - raise ValueError(f"A vector of length {len(vector)} into an array of shape {shape}.") - - return numpy.reshape(vector, shape) - -.. _create-an-instance-of-the-pygadga-class-2: - -Create an Instance of the ``pygad.GA`` Class -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -It is very important to use random mutation and set the -``mutation_by_replacement`` to ``True``. Based on the range of pixel -values, the values assigned to the ``init_range_low``, -``init_range_high``, ``random_mutation_min_val``, and -``random_mutation_max_val`` parameters should be changed. - -If the image pixel values range from 0 to 255, then set -``init_range_low`` and ``random_mutation_min_val`` to 0 as they are but -change ``init_range_high`` and ``random_mutation_max_val`` to 255. - -Feel free to change the other parameters or add other parameters. Please -check the `PyGAD's documentation `__ for -the full list of parameters. - -.. code:: python - - import pygad - - ga_instance = pygad.GA(num_generations=20000, - num_parents_mating=10, - fitness_func=fitness_fun, - sol_per_pop=20, - num_genes=target_im.size, - init_range_low=0.0, - init_range_high=1.0, - mutation_percent_genes=0.01, - mutation_type="random", - mutation_by_replacement=True, - random_mutation_min_val=0.0, - random_mutation_max_val=1.0) - -Run PyGAD -~~~~~~~~~ - -Simply, call the ``run()`` method to run PyGAD. - -.. code:: python - - ga_instance.run() - -Plot Results -~~~~~~~~~~~~ - -After the ``run()`` method completes, the fitness values of all -generations can be viewed in a plot using the ``plot_fitness()`` method. - -.. code:: python - - ga_instance.plot_fitness() - -Here is the plot after 20,000 generations. - -|image5| - -Calculate Some Statistics -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Here is some information about the best solution. - -.. code:: python - - # Returning the details of the best solution. - solution, solution_fitness, solution_idx = ga_instance.best_solution() - print(f"Fitness value of the best solution = {solution_fitness}") - print(f"Index of the best solution : {solution_idx}") - - if ga_instance.best_solution_generation != -1: - print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") - - result = gari.chromosome2img(solution, target_im.shape) - matplotlib.pyplot.imshow(result) - matplotlib.pyplot.title("PyGAD & GARI for Reproducing Images") - matplotlib.pyplot.show() - -Evolution by Generation -~~~~~~~~~~~~~~~~~~~~~~~ - -The solution reached after the 20,000 generations is shown below. - -|image6| - -After more generations, the result can be enhanced like what shown -below. - -|image7| - -The results can also be enhanced by changing the parameters passed to -the constructor of the ``pygad.GA`` class. - -Here is how the image is evolved from generation 0 to generation -20,000s. - -Generation 0 - -|image8| - -Generation 1,000 - -|image9| - -Generation 2,500 - -|image10| - -Generation 4,500 - -|image11| - -Generation 7,000 - -|image12| - -Generation 8,000 - -|image13| - -Generation 20,000 - -|image14| - -Clustering ----------- - -For a 2-cluster problem, the code is available -`here `__. -For a 3-cluster problem, the code is -`here `__. -The 2 examples are using artificial samples. - -Soon a tutorial will be published at -`Paperspace `__ to explain how -clustering works using the genetic algorithm with examples in PyGAD. - -CoinTex Game Playing using PyGAD --------------------------------- - -The code is available the `CoinTex GitHub -project `__. -CoinTex is an Android game written in Python using the Kivy framework. -Find CoinTex at `Google -Play `__: -https://play.google.com/store/apps/details?id=coin.tex.cointexreactfast - -Check this `Paperspace -tutorial `__ -for how the genetic algorithm plays CoinTex: -https://blog.paperspace.com/building-agent-for-cointex-using-genetic-algorithm. -Check also this `YouTube video `__ showing -the genetic algorithm while playing CoinTex. - -.. |image1| image:: https://user-images.githubusercontent.com/16560492/78830005-93111d00-79e7-11ea-9d8e-a8d8325a6101.png -.. |image2| image:: https://user-images.githubusercontent.com/16560492/220486073-c5b6089d-81e4-44d9-a53c-385f479a7273.jpg -.. |image3| image:: https://github.com/ahmedfgad/GeneticAlgorithmPython/assets/16560492/7896f8d8-01c5-4ff9-8d15-52191c309b63 -.. |image4| image:: https://user-images.githubusercontent.com/16560492/36948808-f0ac882e-1fe8-11e8-8d07-1307e3477fd0.jpg -.. |image5| image:: https://user-images.githubusercontent.com/16560492/82232124-77762c00-992e-11ea-9fc6-14a1cd7a04ff.png -.. |image6| image:: https://user-images.githubusercontent.com/16560492/82232405-e0f63a80-992e-11ea-984f-b6ed76465bd1.png -.. |image7| image:: https://user-images.githubusercontent.com/16560492/82232345-cf149780-992e-11ea-8390-bf1a57a19de7.png -.. |image8| image:: https://user-images.githubusercontent.com/16560492/36948589-b47276f0-1fe5-11e8-8efe-0cd1a225ea3a.png -.. |image9| image:: https://user-images.githubusercontent.com/16560492/36948823-16f490ee-1fe9-11e8-97db-3e8905ad5440.png -.. |image10| image:: https://user-images.githubusercontent.com/16560492/36948832-3f314b60-1fe9-11e8-8f4a-4d9a53b99f3d.png -.. |image11| image:: https://user-images.githubusercontent.com/16560492/36948837-53d1849a-1fe9-11e8-9b36-e9e9291e347b.png -.. |image12| image:: https://user-images.githubusercontent.com/16560492/36948852-66f1b176-1fe9-11e8-9f9b-460804e94004.png -.. |image13| image:: https://user-images.githubusercontent.com/16560492/36948865-7fbb5158-1fe9-11e8-8c04-8ac3c1f7b1b1.png -.. |image14| image:: https://user-images.githubusercontent.com/16560492/82232405-e0f63a80-992e-11ea-984f-b6ed76465bd1.png diff --git a/docs/md/pygad_more.md b/docs/source/pygad_more.md similarity index 94% rename from docs/md/pygad_more.md rename to docs/source/pygad_more.md index 631be60..fb816a8 100644 --- a/docs/md/pygad_more.md +++ b/docs/source/pygad_more.md @@ -1,8 +1,10 @@ # More About PyGAD -# Multi-Objective Optimization +This section covers the more advanced features of the `pygad` module. -In [PyGAD 3.2.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-2-0), the library supports multi-objective optimization using the non-dominated sorting genetic algorithm II (NSGA-II). The code is exactly similar to the regular code used for single-objective optimization except for 1 difference. It is the return value of the fitness function. +## Multi-Objective Optimization + +In [PyGAD 3.2.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-2-0), the library added support for multi-objective optimization using the non-dominated sorting genetic algorithm II (NSGA-II). The code is almost the same as the code for single-objective optimization, except for one difference: the return value of the fitness function. In single-objective optimization, the fitness function returns a single numeric value. In this example, the variable `fitness` is expected to be a numeric value. @@ -35,8 +37,8 @@ But it is recommended to use one of these 2 parent selection operators to solve This is a multi-objective optimization example that optimizes these 2 linear functions: -1. `y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + 6wx6` -2. `y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + 6wx12` +1. `y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + w6x6` +2. `y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + w6x12` Where: @@ -53,8 +55,8 @@ import numpy """ Given these 2 functions: - y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + 6wx6 - y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + 6wx12 + y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + w6x6 + y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + w6x12 where (x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7) and y=50 and (x7,x8,x9,x10,x11,x12)=(-2,0.7,-9,1.4,3,5) and y=30 What are the best values for the 6 weights (w1 to w6)? We are going to use the genetic algorithm to optimize these 2 functions. @@ -118,7 +120,7 @@ This is the figure created by the `plot_fitness()` method. The fitness of the fi ![multi-objective-pygad](https://github.com/ahmedfgad/GeneticAlgorithmPython/assets/16560492/7896f8d8-01c5-4ff9-8d15-52191c309b63) -# Limit the Gene Value Range using the `gene_space` Parameter +## Limit the Gene Value Range using the `gene_space` Parameter In [PyGAD 2.11.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-11-0), the `gene_space` parameter supported a new feature to allow customizing the range of accepted values for each gene. Let's take a quick review of the `gene_space` parameter to build over it. @@ -166,7 +168,7 @@ For a 3-gene problem, the next code creates a dictionary for each gene to restri gene_space = [{'low': 1, 'high': 5}, {'low': 0.3, 'high': 1.4}, {'low': -0.2, 'high': 4.5}] ``` -# More about the `gene_space` Parameter +## More about the `gene_space` Parameter The `gene_space` parameter customizes the space of values of each gene. @@ -225,7 +227,7 @@ gene_space = [range(5), None, numpy.linspace(10, 20, 300)] If the user did not assign the initial population to the `initial_population` parameter, the initial population is created randomly based on the `gene_space` parameter. Moreover, the mutation is applied based on this parameter. -## How Mutation Works with the `gene_space` Parameter? +### How Mutation Works with the `gene_space` Parameter? Mutation changes based on whether the `gene_space` has a continuous range or discrete set of values. @@ -265,7 +267,7 @@ If the dictionary has a step like the example below, then it is considered a dis Gene space: {'low': 1, 'high': 5, 'step': 0.5} ``` -# Gene Constraint +## Gene Constraint In [PyGAD 3.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-5-0), a new parameter called `gene_constraint` is added to the constructor of the `pygad.GA` class. An instance attribute of the same name is created for any instance of the `pygad.GA` class. @@ -349,11 +351,11 @@ Out of the range of *1000* numbers, all the 100 values might not be satisfying t > > PyGAD applies constraints sequentially, starting from the first gene to the last. To ensure correct behavior when genes depend on each other, structure your GA problem so that if gene X depends on gene Y, then gene Y appears earlier in the chromosome (solution) than gene X. As a result, its gene constraint will be earlier in the list. -## Full Example +### Full Example For a full example, please check the [`examples/example_gene_constraint.py` script](https://github.com/ahmedfgad/GeneticAlgorithmPython/blob/master/examples/example_gene_constraint.py). -# `sample_size` Parameter +## `sample_size` Parameter In [PyGAD 3.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-5-0), a new parameter called `sample_size`. It is used in some situations where PyGAD seeks a single value for a gene out of a range. Two of the important use cases are: @@ -371,7 +373,7 @@ If the objective is to find a unique value or enforce the gene constraint, then Sometimes 100 values is not enough and PyGAD sometimes fails to find a good value. In this case, it is highly recommended to increase the `sample_size` parameter. This is to create a larger sample to increase the chance of finding a value that meets our objectives. -# Stop at Any Generation +## Stop at Any Generation In [PyGAD 2.4.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-4-0), it is possible to stop the genetic algorithm after any generation. All you need to do it to return the string `"stop"` in the callback function `on_generation`. When this callback function is implemented and assigned to the `on_generation` parameter in the constructor of the `pygad.GA` class, then the algorithm immediately stops after completing its current generation. Let's discuss an example. @@ -385,7 +387,7 @@ def func_generation(ga_instance): return "stop" ``` -# Stop Criteria +## Stop Criteria In [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0), a new parameter named `stop_criteria` is added to the constructor of the `pygad.GA` class. It helps to stop the evolution based on some criteria. It can be assigned to one or more criterion. @@ -433,7 +435,7 @@ ga_instance.run() print(f"Number of generations passed is {ga_instance.generations_completed}") ``` -## Multi-Objective Stop Criteria +### Multi-Objective Stop Criteria When multi-objective is used, then there are 2 options to use the `stop_criteria` parameter with the `reach` keyword: @@ -457,11 +459,13 @@ More than one criterion can be used together. In this case, pass the `stop_crite stop_criteria=['reach_10_20_30', 'reach_90_-5.7_10'] ``` -# Elitism Selection +## Elitism Selection + +Starting from [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0), there is a parameter called `keep_elitism`. It takes an integer that sets how many of the best solutions (the elitism) are kept in the next generation. It defaults to `1`, so only the best solution is kept by default. -In [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0), a new parameter called `keep_elitism` is supported. It accepts an integer to define the number of elitism (i.e. best solutions) to keep in the next generation. This parameter defaults to `1` which means only the best solution is kept in the next generation. +The best solutions are copied to the next generation without any change. Crossover and mutation do not touch them. This makes sure the best solutions found so far are never lost. -In the next example, the `keep_elitism` parameter in the constructor of the `pygad.GA` class is set to 2. Thus, the best 2 solutions in each generation are kept in the next generation. +In the next example, the `keep_elitism` parameter in the constructor of the `pygad.GA` class is set to `2`. So, the best 2 solutions in each generation are kept in the next generation. ```python import numpy @@ -485,12 +489,12 @@ ga_instance = pygad.GA(num_generations=2, ga_instance.run() ``` -The value passed to the `keep_elitism` parameter must satisfy 2 conditions: +The value passed to the `keep_elitism` parameter must meet 2 conditions: 1. It must be `>= 0`. -2. It must be `<= sol_per_pop`. That is its value cannot exceed the number of solutions in the current population. +2. It must be `<= sol_per_pop`. Its value cannot be more than the number of solutions in the population. -In the previous example, if the `keep_elitism` parameter is set equal to the value passed to the `sol_per_pop` parameter, which is 5, then there will be no evolution at all as in the next figure. This is because all the 5 solutions are used as elitism in the next generation and no offspring will be created. +In the previous example, if `keep_elitism` is set equal to `sol_per_pop` (which is `5`), then there is no evolution at all, as shown in the next figure. This is because all the 5 solutions are kept as elitism in the next generation, so no offspring are created. ```python ... @@ -506,9 +510,51 @@ ga_instance.run() ![elitism_kills_evolution](https://user-images.githubusercontent.com/16560492/189273225-67ffad41-97ab-45e1-9324-429705e17b20.png) -Note that if the `keep_elitism` parameter is effective (i.e. is assigned a positive integer, not zero), then the `keep_parents` parameter will have no effect. Because the default value of the `keep_elitism` parameter is 1, then the `keep_parents` parameter has no effect by default. The `keep_parents` parameter is only effective when `keep_elitism=0`. +### How the Number of Offspring Is Decided + +PyGAD has two parameters that decide how many solutions are carried over to the next generation: + +- `keep_elitism`: keeps the best solutions (the elitism). +- `keep_parents`: keeps the selected parents. + +Only one of them is used at a time, and `keep_elitism` has priority. If `keep_elitism` is not zero, then `keep_parents` is ignored. Because `keep_elitism` defaults to `1`, the `keep_parents` parameter has no effect by default. To use `keep_parents`, set `keep_elitism=0`. + +The number of kept solutions decides how many offspring are created. The rest of the population is filled with new offspring: + +``` +number of offspring = sol_per_pop - (number of kept solutions) +``` + +The next tree shows how the two parameters decide the number of offspring. + +:::{figure} images/offspring_decision_tree.* +:alt: Decision tree showing how keep_elitism and keep_parents decide the number of offspring +:width: 680px +:align: center + +How `keep_elitism` and `keep_parents` decide the number of offspring. +::: + +There are four cases: + +| `keep_elitism` | `keep_parents` | What is kept | Number of offspring | +| --- | --- | --- | --- | +| `> 0` | ignored | the best `keep_elitism` solutions | `sol_per_pop - keep_elitism` | +| `0` | `-1` | all the parents | `sol_per_pop - num_parents_mating` | +| `0` | `0` | nothing | `sol_per_pop` | +| `0` | `> 0` | the best `keep_parents` parents | `sol_per_pop - keep_parents` | + +The kept solutions are placed at the top of the next population, starting at index 0. The offspring fill the slots that remain. + +:::{figure} images/population_assembly.* +:alt: The kept solutions sit at the top of the next population and the offspring fill the rest +:width: 620px +:align: center + +The kept solutions are copied to the top of the population. The offspring fill the rest. +::: -# Random Seed +## Random Seed In [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0), a new parameter called `random_seed` is supported. Its value is used as a seed for the random function generators. @@ -560,7 +606,7 @@ After running the code again, it will find the same result. 0.04872203136549972 ``` -# Continue without Losing Progress +## Continue without Losing Progress In [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0), and thanks for [Felix Bernhard](https://github.com/FeBe95) for opening [this GitHub issue](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/123#issuecomment-1203035106), the values of these 4 instance attributes are no longer reset after each call to the `run()` method. @@ -609,7 +655,7 @@ The plot created by the `plot_fitness()` method will show the data collected fro Note that the 2 attributes (`self.best_solutions` and `self.best_solutions_fitness`) only work if the `save_best_solutions` parameter is set to `True`. Also, the 2 attributes (`self.solutions` and `self.solutions_fitness`) only work if the `save_solutions` parameter is `True`. -# Change Population Size during Runtime +## Change Population Size during Runtime Starting from [PyGAD 3.3.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-3-0), the population size can changed during runtime. In other words, the number of solutions/chromosomes and number of genes can be changed. @@ -633,7 +679,7 @@ These are examples of the instance attributes that might be changed. The user sh 3. `last_generation_elitism` and `last_generation_elitism_indices`: Must be changed if `keep_elitism != 0`. The default value of `keep_elitism` is 1. Two NumPy arrays: 2D array representing the elitism and 1D array of the elitism indices. 2. `pop_size`: The population size. -# Prevent Duplicates in Gene Values +## Prevent Duplicates in Gene Values In [PyGAD 2.13.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-13-0), a new bool parameter called `allow_duplicate_genes` is supported to control whether duplicates are supported in the chromosome or not. In other words, whether 2 or more genes might have the same exact value. @@ -765,13 +811,13 @@ You should care of giving enough values for the genes so that PyGAD is able to f If PyGAD failed to find a unique gene while there is still room to find a unique value, one possible option is to set the `sample_size` parameter to a larger value. Check the [sample_size Parameter](https://pygad.readthedocs.io/en/latest/pygad_more.html#sample-size-parameter) section for more information. -## Limitation +### Limitation There might be 2 duplicate genes where changing either of the 2 duplicating genes will not solve the problem. For example, if `gene_space=[[3, 0, 1], [4, 1, 2], [0, 2], [3, 2, 0]]` and the solution is `[3 2 0 0]`, then the values of the last 2 genes duplicate. There are no possible changes in the last 2 genes to solve the problem. This problem can be solved by randomly changing one of the non-duplicating genes that may make a room for a unique value in one the 2 duplicating genes. For example, by changing the second gene from 2 to 4, then any of the last 2 genes can take the value 2 and solve the duplicates. The resultant gene is then `[3 4 2 0]`. But this option is not yet supported in PyGAD. -## Solve Duplicates using a Third Gene +### Solve Duplicates using a Third Gene When `allow_duplicate_genes=False` and a user-defined `gene_space` is used, it sometimes happen that there is no room to solve the duplicates between the 2 genes by simply replacing the value of one gene by another gene. In [PyGAD 3.1.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-0-1), the duplicates are solved by looking for a third gene that will help in solving the duplicates. The following examples explain how it works. @@ -835,7 +881,7 @@ The quick summary is: * Change the value of the first gene from 1 to 0. The solution becomes [0, 2, 2, 3]. * Change the value of the second gene from 2 to 1. The solution becomes [0, 1, 2, 3]. The duplicate is solved. -# More about the `gene_type` Parameter +## More about the `gene_type` Parameter The `gene_type` parameter allows the user to control the data type for all genes at once or each individual gene. In [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0), the `gene_type` parameter also supports customizing the precision for `float` data types. As a result, the `gene_type` parameter helps to: @@ -844,7 +890,7 @@ The `gene_type` parameter allows the user to control the data type for all genes Let's discuss things by examples. -## Data Type for All Genes without Precision +### Data Type for All Genes without Precision The data type for all genes can be specified by assigning the numeric data type directly to the `gene_type` parameter. This is an example to make all genes of `int` data types. @@ -902,7 +948,7 @@ Final Population [ 1 -1 2 2 0]] ``` -## Data Type for All Genes with Precision +### Data Type for All Genes with Precision A precision can only be specified for a `float` data type and cannot be specified for integers. Here is an example to use a precision of 3 for the `float` data type. In this case, all genes are of type `float` and their maximum precision is 3. @@ -957,7 +1003,7 @@ Final Population [ 1.714 -0.644 3.623 3.185 -2.362]] ``` -## Data Type for each Individual Gene without Precision +### Data Type for each Individual Gene without Precision In [PyGAD 2.14.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-14-0), the `gene_type` parameter allows customizing the gene type for each individual gene. This is by using a `list`/`tuple`/`numpy.ndarray` with number of elements equal to the number of genes. For each element, a type is specified for the corresponding gene. @@ -1013,7 +1059,7 @@ Final Population [3 3.7729827570110714 1.458 0 -0.14638754050305036]] ``` -## Data Type for each Individual Gene with Precision +### Data Type for each Individual Gene with Precision The precision can also be specified for the `float` data types as in the next line where the second gene precision is 2 and last gene precision is 1. @@ -1067,7 +1113,7 @@ Final Population [2 -3.73 3.47 3 -1.3]] ``` -# Parallel Processing in PyGAD +## Parallel Processing in PyGAD Starting from [PyGAD 2.17.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-17-0), parallel processing becomes supported. This section explains how to use parallel processing in PyGAD. @@ -1229,7 +1275,7 @@ Based on the second example, using parallel processing with 10 processes takes t *Before releasing [PyGAD 2.17.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-17-0), [László Fazekas](https://www.linkedin.com/in/l%C3%A1szl%C3%B3-fazekas-2429a912) wrote an article to parallelize the fitness function with PyGAD. Check it: [How Genetic Algorithms Can Compete with Gradient Descent and Backprop](https://hackernoon.com/how-genetic-algorithms-can-compete-with-gradient-descent-and-backprop-9m9t33bq)*. -# Print Lifecycle Summary +## Print Lifecycle Summary In [PyGAD 2.19.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-19-0), a new method called `summary()` is supported. It prints a Keras-like summary of the PyGAD lifecycle showing the steps, callback functions, parameters, etc. @@ -1348,7 +1394,7 @@ On Generation on_gen() None ====================================================================== ``` -# Logging Outputs +## Logging Outputs In [PyGAD 3.0.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-0-0), the `print()` statement is no longer used and the outputs are printed using the [logging](https://docs.python.org/3/library/logging.html) module. A a new parameter called `logger` is supported to accept the user-defined logger. @@ -1372,7 +1418,7 @@ Some advantages of using the the [logging](https://docs.python.org/3/library/log This section gives some quick examples to use the `logging` module and then gives an example to use the logger with PyGAD. -## Logging to the Console +### Logging to the Console This is an example to create a logger to log the messages to the console. @@ -1443,7 +1489,7 @@ Note that you may need to clear the handlers after finishing the execution. This logger.handlers.clear() ``` -## Logging to a File +### Logging to a File This is another example to log the messages to a file named `logfile.txt`. The formatter prints the following about each message: @@ -1485,7 +1531,7 @@ Consider clearing the handlers if necessary. logger.handlers.clear() ``` -## Log to Both the Console and a File +### Log to Both the Console and a File This is an example to create a single Logger associated with 2 handlers: @@ -1522,7 +1568,7 @@ Consider clearing the handlers if necessary. logger.handlers.clear() ``` -## PyGAD Example +### PyGAD Example To use the logger in PyGAD, just create your custom logger and pass it to the `logger` parameter. @@ -1599,7 +1645,7 @@ By executing this code, the logged messages are printed to the console and also 2023-04-03 19:04:27 INFO: Fitness = 0.000389832593101348 ``` -# Solve Non-Deterministic Problems +## Solve Non-Deterministic Problems PyGAD can be used to solve both deterministic and non-deterministic problems. Deterministic are those that return the same fitness for the same solution. For non-deterministic problems, a different fitness value would be returned for the same solution. @@ -1630,7 +1676,7 @@ ga_instance = pygad.GA(..., This way PyGAD will not save any explored solution and thus the fitness function have to be called for each individual solution. -# Reuse the Fitness instead of Calling the Fitness Function +## Reuse the Fitness instead of Calling the Fitness Function It may happen that a previously explored solution in generation X is explored again in another generation Y (where Y > X). For some problems, calling the fitness function takes much time. @@ -1640,23 +1686,23 @@ The parameters explored in this section can be set in the constructor of the `py The `cal_pop_fitness()` method of the `pygad.GA` class checks these parameters to see if there is a possibility of reusing the fitness instead of calling the fitness function. -## 1. `save_solutions` +### 1. `save_solutions` It defaults to `False`. If set to `True`, then the population of each generation is saved into the `solutions` attribute of the `pygad.GA` instance. In other words, every single solution is saved in the `solutions` attribute. -## 2. `save_best_solutions` +### 2. `save_best_solutions` It defaults to `False`. If `True`, then it only saves the best solution in every generation. -## 3. `keep_elitism` +### 3. `keep_elitism` It accepts an integer and defaults to 1. If set to a positive integer, then it keeps the elitism of one generation available in the next generation. -## 4. `keep_parents` +### 4. `keep_parents` It accepts an integer and defaults to -1. It set to `-1` or a positive integer, then it keeps the parents of one generation available in the next generation. -# Why the Fitness Function is not Called for Solution at Index 0? +## Why the Fitness Function is not Called for Solution at Index 0? PyGAD has a parameter called `keep_elitism` which defaults to 1. This parameter defines the number of best solutions in generation **X** to keep in the next generation **X+1**. The best solutions are just copied from generation **X** to generation **X+1** without making any change. @@ -1683,7 +1729,7 @@ ga_instance = pygad.GA(..., -# Batch Fitness Calculation +## Batch Fitness Calculation In [PyGAD 2.19.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-19-0), a new optional parameter called `fitness_batch_size` is supported. A new optional parameter called `fitness_batch_size` is supported to calculate the fitness function in batches. Thanks to [Linan Qiu](https://github.com/linanqiu) for opening the [GitHub issue #136](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/136). @@ -1692,7 +1738,7 @@ Its values can be: * `1` or `None`: If the `fitness_batch_size` parameter is assigned the value `1` or `None` (default), then the normal flow is used where the fitness function is called for each individual solution. That is if there are 15 solutions, then the fitness function is called 15 times. * `1 < fitness_batch_size <= sol_per_pop`: If the `fitness_batch_size` parameter is assigned a value satisfying this condition `1 < fitness_batch_size <= sol_per_pop`, then the solutions are grouped into batches of size `fitness_batch_size` and the fitness function is called once for each batch. In this case, the fitness function must return a list/tuple/numpy.ndarray with a length equal to the number of solutions passed. -## Example without `fitness_batch_size` Parameter +### Example without `fitness_batch_size` Parameter This is an example where the `fitness_batch_size` parameter is given the value `None` (which is the default value). This is equivalent to using the value `1`. In this case, the fitness function will be called for each solution. This means the fitness function `fitness_func` will receive only a single solution. This is an example of the passed arguments to the fitness function: @@ -1741,7 +1787,7 @@ print(number_of_calls) 120 ``` -## Example with `fitness_batch_size` Parameter +### Example with `fitness_batch_size` Parameter This is an example where the `fitness_batch_size` parameter is used and assigned the value `4`. This means the solutions will be grouped into batches of `4` solutions. The fitness function will be called once for each patch (i.e. called once for each 4 solutions). @@ -1799,7 +1845,7 @@ print(number_of_calls) When batch fitness calculation is used, then we saved `120 - 30 = 90` calls to the fitness function. -# Use Functions and Methods to Build Fitness and Callbacks +## Use Functions and Methods to Build Fitness and Callbacks In PyGAD 2.19.0, it is possible to pass user-defined functions or methods to the following parameters: @@ -1817,7 +1863,7 @@ This section gives 2 examples to assign these parameters user-defined: 1. Functions. 2. Methods. -## Assign Functions +### Assign Functions This is a dummy example where the fitness function returns a random value. Note that the instance of the `pygad.GA` class is passed as the last parameter of all functions. @@ -1865,7 +1911,7 @@ ga_instance = pygad.GA(num_generations=5, ga_instance.run() ``` -## Assign Methods +### Assign Methods The next example has all the method defined inside the class `Test`. All of the methods accept an additional parameter representing the method's object of the class `Test`. diff --git a/docs/source/pygad_more.rst b/docs/source/pygad_more.rst deleted file mode 100644 index 5334317..0000000 --- a/docs/source/pygad_more.rst +++ /dev/null @@ -1,2645 +0,0 @@ -More About PyGAD -================ - -Multi-Objective Optimization -============================ - -In `PyGAD -3.2.0 `__, -the library supports multi-objective optimization using the -non-dominated sorting genetic algorithm II (NSGA-II). The code is -exactly similar to the regular code used for single-objective -optimization except for 1 difference. It is the return value of the -fitness function. - -In single-objective optimization, the fitness function returns a single -numeric value. In this example, the variable ``fitness`` is expected to -be a numeric value. - -.. code:: python - - def fitness_func(ga_instance, solution, solution_idx): - ... - return fitness - -But in multi-objective optimization, the fitness function returns any of -these data types: - -1. ``list`` - -2. ``tuple`` - -3. ``numpy.ndarray`` - -.. code:: python - - def fitness_func(ga_instance, solution, solution_idx): - ... - return [fitness1, fitness2, ..., fitnessN] - -Whenever the fitness function returns an iterable of these data types, -then the problem is considered multi-objective. This holds even if there -is a single element in the returned iterable. - -Other than the fitness function, everything else could be the same in -both single and multi-objective problems. - -But it is recommended to use one of these 2 parent selection operators -to solve multi-objective problems: - -1. ``nsga2``: This selects the parents based on non-dominated sorting - and crowding distance. - -2. ``tournament_nsga2``: This selects the parents using tournament - selection which uses non-dominated sorting and crowding distance to - rank the solutions. - -This is a multi-objective optimization example that optimizes these 2 -linear functions: - -1. ``y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + 6wx6`` - -2. ``y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + 6wx12`` - -Where: - -1. ``(x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7)`` and ``y=50`` - -2. ``(x7,x8,x9,x10,x11,x12)=(-2,0.7,-9,1.4,3,5)`` and ``y=30`` - -The 2 functions use the same parameters (weights) ``w1`` to ``w6``. - -The goal is to use PyGAD to find the optimal values for such weights -that satisfy the 2 functions ``y1`` and ``y2``. - -.. code:: python - - import pygad - import numpy - - """ - Given these 2 functions: - y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + 6wx6 - y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + 6wx12 - where (x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7) and y=50 - and (x7,x8,x9,x10,x11,x12)=(-2,0.7,-9,1.4,3,5) and y=30 - What are the best values for the 6 weights (w1 to w6)? We are going to use the genetic algorithm to optimize these 2 functions. - This is a multi-objective optimization problem. - - PyGAD considers the problem as multi-objective if the fitness function returns: - 1) List. - 2) Or tuple. - 3) Or numpy.ndarray. - """ - - function_inputs1 = [4,-2,3.5,5,-11,-4.7] # Function 1 inputs. - function_inputs2 = [-2,0.7,-9,1.4,3,5] # Function 2 inputs. - desired_output1 = 50 # Function 1 output. - desired_output2 = 30 # Function 2 output. - - def fitness_func(ga_instance, solution, solution_idx): - output1 = numpy.sum(solution*function_inputs1) - output2 = numpy.sum(solution*function_inputs2) - fitness1 = 1.0 / (numpy.abs(output1 - desired_output1) + 0.000001) - fitness2 = 1.0 / (numpy.abs(output2 - desired_output2) + 0.000001) - return [fitness1, fitness2] - - num_generations = 100 - num_parents_mating = 10 - - sol_per_pop = 20 - num_genes = len(function_inputs1) - - ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - sol_per_pop=sol_per_pop, - num_genes=num_genes, - fitness_func=fitness_func, - parent_selection_type='nsga2') - - ga_instance.run() - - ga_instance.plot_fitness(label=['Obj 1', 'Obj 2']) - - solution, solution_fitness, solution_idx = ga_instance.best_solution(ga_instance.last_generation_fitness) - print(f"Parameters of the best solution : {solution}") - print(f"Fitness value of the best solution = {solution_fitness}") - - prediction = numpy.sum(numpy.array(function_inputs1)*solution) - print(f"Predicted output 1 based on the best solution : {prediction}") - prediction = numpy.sum(numpy.array(function_inputs2)*solution) - print(f"Predicted output 2 based on the best solution : {prediction}") - -This is the result of the print statements. The predicted outputs are -close to the desired outputs. - -.. code:: - - Parameters of the best solution : [ 0.79676439 -2.98823386 -4.12677662 5.70539445 -2.02797016 -1.07243922] - Fitness value of the best solution = [ 1.68090829 349.8591915 ] - Predicted output 1 based on the best solution : 50.59491545442283 - Predicted output 2 based on the best solution : 29.99714270722312 - -This is the figure created by the ``plot_fitness()`` method. The fitness -of the first objective has the green color. The blue color is used for -the second objective fitness. - -|image1| - -.. _limit-the-gene-value-range-using-the-genespace-parameter: - -Limit the Gene Value Range using the ``gene_space`` Parameter -============================================================= - -In `PyGAD -2.11.0 `__, -the ``gene_space`` parameter supported a new feature to allow -customizing the range of accepted values for each gene. Let's take a -quick review of the ``gene_space`` parameter to build over it. - -The ``gene_space`` parameter allows the user to feed the space of values -of each gene. This way the accepted values for each gene is retracted to -the user-defined values. Assume there is a problem that has 3 genes -where each gene has different set of values as follows: - -1. Gene 1: ``[0.4, 12, -5, 21.2]`` - -2. Gene 2: ``[-2, 0.3]`` - -3. Gene 3: ``[1.2, 63.2, 7.4]`` - -Then, the ``gene_space`` for this problem is as given below. Note that -the order is very important. - -.. code:: python - - gene_space = [[0.4, 12, -5, 21.2], - [-2, 0.3], - [1.2, 63.2, 7.4]] - -In case all genes share the same set of values, then simply feed a -single list to the ``gene_space`` parameter as follows. In this case, -all genes can only take values from this list of 6 values. - -.. code:: python - - gene_space = [33, 7, 0.5, 95. 6.3, 0.74] - -The previous example restricts the gene values to just a set of fixed -number of discrete values. In case you want to use a range of discrete -values to the gene, then you can use the ``range()`` function. For -example, ``range(1, 7)`` means the set of allowed values for the gene -are ``1, 2, 3, 4, 5, and 6``. You can also use the ``numpy.arange()`` or -``numpy.linspace()`` functions for the same purpose. - -The previous discussion only works with a range of discrete values not -continuous values. In `PyGAD -2.11.0 `__, -the ``gene_space`` parameter can be assigned a dictionary that allows -the gene to have values from a continuous range. - -Assuming you want to restrict the gene within this half-open range [1 to -5) where 1 is included and 5 is not. Then simply create a dictionary -with 2 items where the keys of the 2 items are: - -1. ``'low'``: The minimum value in the range which is 1 in the example. - -2. ``'high'``: The maximum value in the range which is 5 in the example. - -The dictionary will look like that: - -.. code:: python - - {'low': 1, - 'high': 5} - -It is not acceptable to add more than 2 items in the dictionary or use -other keys than ``'low'`` and ``'high'``. - -For a 3-gene problem, the next code creates a dictionary for each gene -to restrict its values in a continuous range. For the first gene, it can -take any floating-point value from the range that starts from 1 -(inclusive) and ends at 5 (exclusive). - -.. code:: python - - gene_space = [{'low': 1, 'high': 5}, {'low': 0.3, 'high': 1.4}, {'low': -0.2, 'high': 4.5}] - -.. _more-about-the-genespace-parameter: - -More about the ``gene_space`` Parameter -======================================= - -The ``gene_space`` parameter customizes the space of values of each -gene. - -Assuming that all genes have the same global space which include the -values 0.3, 5.2, -4, and 8, then those values can be assigned to the -``gene_space`` parameter as a list, tuple, or range. Here is a list -assigned to this parameter. By doing that, then the gene values are -restricted to those assigned to the ``gene_space`` parameter. - -.. code:: python - - gene_space = [0.3, 5.2, -4, 8] - -If some genes have different spaces, then ``gene_space`` should accept a -nested list or tuple. In this case, the elements could be: - -1. Number (of ``int``, ``float``, or ``NumPy`` data types): A single - value to be assigned to the gene. This means this gene will have the - same value across all generations. - -2. ``list``, ``tuple``, ``numpy.ndarray``, or any range like ``range``, - ``numpy.arange()``, or ``numpy.linspace``: It holds the space for - each individual gene. But this space is usually discrete. That is - there is a set of finite values to select from. - -3. ``dict``: To sample a value for a gene from a continuous range. The - dictionary must have 2 mandatory keys which are ``"low"`` and - ``"high"`` in addition to an optional key which is ``"step"``. A - random value is returned between the values assigned to the items - with ``"low"`` and ``"high"`` keys. If the ``"step"`` exists, then - this works as the previous options (i.e. discrete set of values). - -4. ``None``: A gene with its space set to ``None`` is initialized - randomly from the range specified by the 2 parameters - ``init_range_low`` and ``init_range_high``. For mutation, its value - is mutated based on a random value from the range specified by the 2 - parameters ``random_mutation_min_val`` and - ``random_mutation_max_val``. If all elements in the ``gene_space`` - parameter are ``None``, the parameter will not have any effect. - -Assuming that a chromosome has 2 genes and each gene has a different -value space. Then the ``gene_space`` could be assigned a nested -list/tuple where each element determines the space of a gene. - -According to the next code, the space of the first gene is ``[0.4, -5]`` -which has 2 values and the space for the second gene is -``[0.5, -3.2, 8.8, -9]`` which has 4 values. - -.. code:: python - - gene_space = [[0.4, -5], [0.5, -3.2, 8.2, -9]] - -For a 2 gene chromosome, if the first gene space is restricted to the -discrete values from 0 to 4 and the second gene is restricted to the -values from 10 to 19, then it could be specified according to the next -code. - -.. code:: python - - gene_space = [range(5), range(10, 20)] - -The ``gene_space`` can also be assigned to a single range, as given -below, where the values of all genes are sampled from the same range. - -.. code:: python - - gene_space = numpy.arange(15) - -The ``gene_space`` can be assigned a dictionary to sample a value from a -continuous range. - -.. code:: python - - gene_space = {"low": 4, "high": 30} - -A step also can be assigned to the dictionary. This works as if a range -is used. - -.. code:: python - - gene_space = {"low": 4, "high": 30, "step": 2.5} - -.. - - Setting a ``dict`` like ``{"low": 0, "high": 10}`` in the - ``gene_space`` means that random values from the continuous range [0, - 10) are sampled. Note that ``0`` is included but ``10`` is not - included while sampling. Thus, the maximum value that could be - returned is less than ``10`` like ``9.9999``. But if the user decided - to round the genes using, for example, ``[float, 2]``, then this - value will become 10. So, the user should be careful to the inputs. - -If a ``None`` is assigned to only a single gene, then its value will be -randomly generated initially using the ``init_range_low`` and -``init_range_high`` parameters in the ``pygad.GA`` class's constructor. -During mutation, the value are sampled from the range defined by the 2 -parameters ``random_mutation_min_val`` and ``random_mutation_max_val``. -This is an example where the second gene is given a ``None`` value. - -.. code:: python - - gene_space = [range(5), None, numpy.linspace(10, 20, 300)] - -If the user did not assign the initial population to the -``initial_population`` parameter, the initial population is created -randomly based on the ``gene_space`` parameter. Moreover, the mutation -is applied based on this parameter. - -.. _how-mutation-works-with-the-genespace-parameter: - -How Mutation Works with the ``gene_space`` Parameter? ------------------------------------------------------ - -Mutation changes based on whether the ``gene_space`` has a continuous -range or discrete set of values. - -If a gene has its **static/discrete space** defined in the -``gene_space`` parameter, then mutation works by replacing the gene -value by a value randomly selected from the gene space. This happens for -both ``int`` and ``float`` data types. - -For example, the following ``gene_space`` has the static space -``[1, 2, 3]`` defined for the first gene. So, this gene can only have a -value out of these 3 values. - -.. code:: python - - Gene space: [[1, 2, 3], - None] - Solution: [1, 5] - -For a solution like ``[1, 5]``, then mutation happens for the first gene -by simply replacing its current value by a randomly selected value -(other than its current value if possible). So, the value 1 will be -replaced by either 2 or 3. - -For the second gene, its space is set to ``None``. So, traditional -mutation happens for this gene by: - -1. Generating a random value from the range defined by the - ``random_mutation_min_val`` and ``random_mutation_max_val`` - parameters. - -2. Adding this random value to the current gene's value. - -If its current value is 5 and the random value is ``-0.5``, then the new -value is 4.5. If the gene type is integer, then the value will be -rounded. - -On the other hand, if a gene has a **continuous space** defined in the -``gene_space`` parameter, then mutation occurs by adding a random value -to the current gene value. - -For example, the following ``gene_space`` has the continuous space -defined by the dictionary ``{'low': 1, 'high': 5}``. This applies to all -genes. So, mutation is applied to one or more selected genes by adding a -random value to the current gene value. - -.. code:: python - - Gene space: {'low': 1, 'high': 5} - Solution: [1.5, 3.4] - -Assuming ``random_mutation_min_val=-1`` and -``random_mutation_max_val=1``, then a random value such as ``0.3`` can -be added to the gene(s) participating in mutation. If only the first -gene is mutated, then its new value changes from ``1.5`` to -``1.5+0.3=1.8``. Note that PyGAD verifies that the new value is within -the range. In the worst scenarios, the value will be set to either -boundary of the continuous range. For example, if the gene value is 1.5 -and the random value is -0.55, then the new value is 0.95 which smaller -than the lower boundary 1. Thus, the gene value will be rounded to 1. - -If the dictionary has a step like the example below, then it is -considered a discrete range and mutation occurs by randomly selecting a -value from the set of values. In other words, no random value is added -to the gene value. - -.. code:: python - - Gene space: {'low': 1, 'high': 5, 'step': 0.5} - -Gene Constraint -=============== - -In `PyGAD -3.5.0 `__, -a new parameter called ``gene_constraint`` is added to the constructor -of the ``pygad.GA`` class. An instance attribute of the same name is -created for any instance of the ``pygad.GA`` class. - -The ``gene_constraint`` parameter allows the users to define constraints -to be enforced (as much as possible) when selecting a value for a gene. -For example, this constraint is enforced when applying mutation to make -sure the new gene value after mutation meets the gene constraint. - -The default value of this parameter is ``None`` which means no genes -have constraints. It can be assigned a list but the length of this list -must be equal to the number of genes as specified by the ``num_gene`` -parameter. - -When assigned a list, the allowed values for each element are: - -1. ``None``: No constraint for the gene. - -2. ``callable``: A callable/function that accepts 2 parameters: - - 1. The solution where the gene exists. - - 2. A list or NumPy array of candidate values for the gene. - -It is the user's responsibility to build such callables to filter the -passed list of values and return a new list with the values that meets -the gene constraint. If no value meets the constraint, return an empty -list or NumPy array. - -For example, if the gene must be smaller than 5, then use this callable: - -.. code:: python - - lambda solution,values: [val for val in values if val<5] - -The first parameter is the solution where the target gene exists. It is -passed just in case you would like to compare the gene value with other -genes. The second parameter is the list of candidate values for the -gene. The objective of the lambda function is to filter the values and -return only the valid values that are less than 5. - -A lambda function is used in this case but we can use a regular -function: - -.. code:: python - - def constraint_func(solution,values): - return [val for val in values if val<5] - -Assuming ``num_genes`` is 2, then here is a valid value for the -``gene_constraint`` parameter. - -.. code:: python - - import pygad - - def fitness_func(...): - ... - return fitness - - ga_instance = pygad.GA( - num_genes=2, - sample_size=200, - ... - gene_constraint= - [ - lambda solution,values: [val for val in values if val<5], - lambda solution,values: [val for val in values if val>[solution[0]] - ] - ) - -The first lambda function filters the values for the first gene by only -considering the gene values that are less than 5. If the passed values -is ``[-5, 2, 6, 13, 3, 4, 0]``, then the returned filtered values will -be ``[-5, 2, 3, 4, 0]``. - -The constraint for the second gene makes sure the selected value is -larger than the value of the first gene. Assuming the values for the 2 -parameters are: - -1. ``solution=[1, 4]`` - -2. ``values=[17, 2, -1, 0.5, -2.1, 1.4]`` - -Then the value of the first gene in the passed solution is ``1``. By -filtering the passed values using the callable corresponding to the -second gene, then the returned values will be ``[17, 2, 1.4]`` because -these are the only values that are larger than the first gene value of -``1``. - -Sometimes it is normal for PyGAD to fail to find a gene value that -satisfies the constraint. For example, if the possible gene values are -only ``[20,30,40]`` and the gene constraint restricts the values to be -greater than 50, then it is impossible to meet the constraint. - -For some other cases, the constraint can be met but with some changes. -For example, increasing the range from which a value is sampled. If the -``gene_space`` is used and assigned ``range(10)``, then the gene -constraint can be met by using ``range(50)`` so that we can find values -greater than 50. - -Even if the the gene space is already assigned ``range(1000)``, it might -still not find values meeting the constraints This is because PyGAD -samples a number of values equal to the ``sample_size`` parameter which -defaults to *100*. - -Out of the range of *1000* numbers, all the 100 values might not be -satisfying the constraint. This issue could be solved by simply -assigning a larger value for the ``sample_size`` parameter. - - PyGAD does not yet handle the **dependencies** among the genes in the - ``gene_constraint`` parameter. - - This is an example where gene 0 depends on gene 1. To efficiently - enforce the constraints, the constraint for gene 1 must be enforced - first (if not ``None``) then the constraint for gene 0. - - .. code:: python - - gene_constraint= - [ - lambda solution,values: [val for val in values if val10] - ] - - PyGAD applies constraints sequentially, starting from the first gene - to the last. To ensure correct behavior when genes depend on each - other, structure your GA problem so that if gene X depends on gene Y, - then gene Y appears earlier in the chromosome (solution) than gene X. - As a result, its gene constraint will be earlier in the list. - -Full Example ------------- - -For a full example, please check the -```examples/example_gene_constraint.py`` -script `__. - -.. _samplesize-parameter: - -``sample_size`` Parameter -========================= - -In `PyGAD -3.5.0 `__, -a new parameter called ``sample_size``. It is used in some situations -where PyGAD seeks a single value for a gene out of a range. Two of the -important use cases are: - -1. Find a unique value for the gene. This is when the - ``allow_duplicate_genes`` parameter is set to ``False`` to reject the - duplicate gene values within the same solution. - -2. Find a value that satisfies the ``gene_constraint`` parameter. - -Given that we are sampling values from a continuous range as defined by -the 2 attributes: - -1. ``random_mutation_min_val=0`` - -2. ``random_mutation_max_val=100`` - -PyGAD samples a fixed number of values out of this continuous range. The -number of values in the sample is defined by the ``sample_size`` -parameter which defaults to ``100``. - -If the objective is to find a unique value or enforce the gene -constraint, then the 100 values are filtered to keep only the values -that keep the gene unique or meet the constraint. - -Sometimes 100 values is not enough and PyGAD sometimes fails to find a -good value. In this case, it is highly recommended to increase the -``sample_size`` parameter. This is to create a larger sample to increase -the chance of finding a value that meets our objectives. - -Stop at Any Generation -====================== - -In `PyGAD -2.4.0 `__, -it is possible to stop the genetic algorithm after any generation. All -you need to do it to return the string ``"stop"`` in the callback -function ``on_generation``. When this callback function is implemented -and assigned to the ``on_generation`` parameter in the constructor of -the ``pygad.GA`` class, then the algorithm immediately stops after -completing its current generation. Let's discuss an example. - -Assume that the user wants to stop algorithm either after the 100 -generations or if a condition is met. The user may assign a value of 100 -to the ``num_generations`` parameter of the ``pygad.GA`` class -constructor. - -The condition that stops the algorithm is written in a callback function -like the one in the next code. If the fitness value of the best solution -exceeds 70, then the string ``"stop"`` is returned. - -.. code:: python - - def func_generation(ga_instance): - if ga_instance.best_solution()[1] >= 70: - return "stop" - -Stop Criteria -============= - -In `PyGAD -2.15.0 `__, -a new parameter named ``stop_criteria`` is added to the constructor of -the ``pygad.GA`` class. It helps to stop the evolution based on some -criteria. It can be assigned to one or more criterion. - -Each criterion is passed as ``str`` that consists of 2 parts: - -1. Stop word. - -2. Number. - -It takes this form: - -.. code:: python - - "word_num" - -The current 2 supported words are ``reach`` and ``saturate``. - -The ``reach`` word stops the ``run()`` method if the fitness value is -equal to or greater than a given fitness value. An example for ``reach`` -is ``"reach_40"`` which stops the evolution if the fitness is >= 40. - -``saturate`` stops the evolution if the fitness saturates for a given -number of consecutive generations. An example for ``saturate`` is -``"saturate_7"`` which means stop the ``run()`` method if the fitness -does not change for 7 consecutive generations. - -Here is an example that stops the evolution if either the fitness value -reached ``127.4`` or if the fitness saturates for ``15`` generations. - -.. code:: python - - import pygad - import numpy - - equation_inputs = [4, -2, 3.5, 8, 9, 4] - desired_output = 44 - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - - return fitness - - ga_instance = pygad.GA(num_generations=200, - sol_per_pop=10, - num_parents_mating=4, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - stop_criteria=["reach_127.4", "saturate_15"]) - - ga_instance.run() - print(f"Number of generations passed is {ga_instance.generations_completed}") - -Multi-Objective Stop Criteria ------------------------------ - -When multi-objective is used, then there are 2 options to use the -``stop_criteria`` parameter with the ``reach`` keyword: - -1. Pass a single value to use along the ``reach`` keyword to use across - all the objectives. - -2. Pass multiple values along the ``reach`` keyword. But the number of - values must equal the number of objectives. - -For the ``saturate`` keyword, it is independent to the number of -objectives. - -Suppose there are 3 objectives, this is a working example. It stops when -the fitness value of the 3 objectives reach or exceed 10, 20, and 30, -respectively. - -.. code:: python - - stop_criteria='reach_10_20_30' - -More than one criterion can be used together. In this case, pass the -``stop_criteria`` parameter as an iterable. This is an example. It stops -when either of these 2 conditions hold: - -1. The fitness values of the 3 objectives reach or exceed 10, 20, and - 30, respectively. - -2. The fitness values of the 3 objectives reach or exceed 90, -5.7, and - 10, respectively. - -.. code:: python - - stop_criteria=['reach_10_20_30', 'reach_90_-5.7_10'] - -Elitism Selection -================= - -In `PyGAD -2.18.0 `__, -a new parameter called ``keep_elitism`` is supported. It accepts an -integer to define the number of elitism (i.e. best solutions) to keep in -the next generation. This parameter defaults to ``1`` which means only -the best solution is kept in the next generation. - -In the next example, the ``keep_elitism`` parameter in the constructor -of the ``pygad.GA`` class is set to 2. Thus, the best 2 solutions in -each generation are kept in the next generation. - -.. code:: python - - import numpy - import pygad - - function_inputs = [4,-2,3.5,5,-11,-4.7] - desired_output = 44 - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution*function_inputs) - fitness = 1.0 / numpy.abs(output - desired_output) - return fitness - - ga_instance = pygad.GA(num_generations=2, - num_parents_mating=3, - fitness_func=fitness_func, - num_genes=6, - sol_per_pop=5, - keep_elitism=2) - - ga_instance.run() - -The value passed to the ``keep_elitism`` parameter must satisfy 2 -conditions: - -1. It must be ``>= 0``. - -2. It must be ``<= sol_per_pop``. That is its value cannot exceed the - number of solutions in the current population. - -In the previous example, if the ``keep_elitism`` parameter is set equal -to the value passed to the ``sol_per_pop`` parameter, which is 5, then -there will be no evolution at all as in the next figure. This is because -all the 5 solutions are used as elitism in the next generation and no -offspring will be created. - -.. code:: python - - ... - - ga_instance = pygad.GA(..., - sol_per_pop=5, - keep_elitism=5) - - ga_instance.run() - -|image2| - -Note that if the ``keep_elitism`` parameter is effective (i.e. is -assigned a positive integer, not zero), then the ``keep_parents`` -parameter will have no effect. Because the default value of the -``keep_elitism`` parameter is 1, then the ``keep_parents`` parameter has -no effect by default. The ``keep_parents`` parameter is only effective -when ``keep_elitism=0``. - -Random Seed -=========== - -In `PyGAD -2.18.0 `__, -a new parameter called ``random_seed`` is supported. Its value is used -as a seed for the random function generators. - -PyGAD uses random functions in these 2 libraries: - -1. NumPy - -2. random - -The ``random_seed`` parameter defaults to ``None`` which means no seed -is used. As a result, different random numbers are generated for each -run of PyGAD. - -If this parameter is assigned a proper seed, then the results will be -reproducible. In the next example, the integer 2 is used as a random -seed. - -.. code:: python - - import numpy - import pygad - - function_inputs = [4,-2,3.5,5,-11,-4.7] - desired_output = 44 - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution*function_inputs) - fitness = 1.0 / numpy.abs(output - desired_output) - return fitness - - ga_instance = pygad.GA(num_generations=2, - num_parents_mating=3, - fitness_func=fitness_func, - sol_per_pop=5, - num_genes=6, - random_seed=2) - - ga_instance.run() - best_solution, best_solution_fitness, best_match_idx = ga_instance.best_solution() - print(best_solution) - print(best_solution_fitness) - -This is the best solution found and its fitness value. - -.. code:: - - [ 2.77249188 -4.06570662 0.04196872 -3.47770796 -0.57502138 -3.22775267] - 0.04872203136549972 - -After running the code again, it will find the same result. - -.. code:: - - [ 2.77249188 -4.06570662 0.04196872 -3.47770796 -0.57502138 -3.22775267] - 0.04872203136549972 - -Continue without Losing Progress -================================ - -In `PyGAD -2.18.0 `__, -and thanks for `Felix Bernhard `__ for -opening `this GitHub -issue `__, -the values of these 4 instance attributes are no longer reset after each -call to the ``run()`` method. - -1. ``self.best_solutions`` - -2. ``self.best_solutions_fitness`` - -3. ``self.solutions`` - -4. ``self.solutions_fitness`` - -This helps the user to continue where the last run stopped without -losing the values of these 4 attributes. - -Now, the user can save the model by calling the ``save()`` method. - -.. code:: python - - import pygad - - def fitness_func(ga_instance, solution, solution_idx): - ... - return fitness - - ga_instance = pygad.GA(...) - - ga_instance.run() - - ga_instance.plot_fitness() - - ga_instance.save("pygad_GA") - -Then the saved model is loaded by calling the ``load()`` function. After -calling the ``run()`` method over the loaded instance, then the data -from the previous 4 attributes are not reset but extended with the new -data. - -.. code:: python - - import pygad - - def fitness_func(ga_instance, solution, solution_idx): - ... - return fitness - - loaded_ga_instance = pygad.load("pygad_GA") - - loaded_ga_instance.run() - - loaded_ga_instance.plot_fitness() - -The plot created by the ``plot_fitness()`` method will show the data -collected from both the runs. - -Note that the 2 attributes (``self.best_solutions`` and -``self.best_solutions_fitness``) only work if the -``save_best_solutions`` parameter is set to ``True``. Also, the 2 -attributes (``self.solutions`` and ``self.solutions_fitness``) only work -if the ``save_solutions`` parameter is ``True``. - -Change Population Size during Runtime -===================================== - -Starting from `PyGAD -3.3.0 `__, -the population size can changed during runtime. In other words, the -number of solutions/chromosomes and number of genes can be changed. - -The user has to carefully arrange the list of *parameters* and *instance -attributes* that have to be changed to keep the GA consistent before and -after changing the population size. Generally, change everything that -would be used during the GA evolution. - - CAUTION: If the user failed to change a parameter or an instance - attributes necessary to keep the GA running after the population size - changed, errors will arise. - -These are examples of the parameters that the user should decide whether -to change. The user should check the `list of -parameters `__ -and decide what to change. - -1. ``population``: The population. It *must* be changed. - -2. ``num_offspring``: The number of offspring to produce out of the - crossover and mutation operations. Change this parameter if the - number of offspring have to be changed to be consistent with the new - population size. - -3. ``num_parents_mating``: The number of solutions to select as parents. - Change this parameter if the number of parents have to be changed to - be consistent with the new population size. - -4. ``fitness_func``: If the way of calculating the fitness changes after - the new population size, then the fitness function have to be - changed. - -5. ``sol_per_pop``: The number of solutions per population. It is not - critical to change it but it is recommended to keep this number - consistent with the number of solutions in the ``population`` - parameter. - -These are examples of the instance attributes that might be changed. The -user should check the `list of instance -attributes `__ -and decide what to change. - -1. All the ``last_generation_*`` parameters - - 1. ``last_generation_fitness``: A 1D NumPy array of fitness values of - the population. - - 2. ``last_generation_parents`` and - ``last_generation_parents_indices``: Two NumPy arrays: 2D array - representing the parents and 1D array of the parents indices. - - 3. ``last_generation_elitism`` and - ``last_generation_elitism_indices``: Must be changed if - ``keep_elitism != 0``. The default value of ``keep_elitism`` is 1. - Two NumPy arrays: 2D array representing the elitism and 1D array - of the elitism indices. - -2. ``pop_size``: The population size. - -Prevent Duplicates in Gene Values -================================= - -In `PyGAD -2.13.0 `__, -a new bool parameter called ``allow_duplicate_genes`` is supported to -control whether duplicates are supported in the chromosome or not. In -other words, whether 2 or more genes might have the same exact value. - -If ``allow_duplicate_genes=True`` (which is the default case), genes may -have the same value. If ``allow_duplicate_genes=False``, then no 2 genes -will have the same value given that there are enough unique values for -the genes. - -The next code gives an example to use the ``allow_duplicate_genes`` -parameter. A callback generation function is implemented to print the -population after each generation. - -.. code:: python - - import pygad - - def fitness_func(ga_instance, solution, solution_idx): - return 0 - - def on_generation(ga): - print("Generation", ga.generations_completed) - print(ga.population) - - ga_instance = pygad.GA(num_generations=5, - sol_per_pop=5, - num_genes=4, - mutation_num_genes=3, - random_mutation_min_val=-5, - random_mutation_max_val=5, - num_parents_mating=2, - fitness_func=fitness_func, - gene_type=int, - on_generation=on_generation, - sample_size=200, - allow_duplicate_genes=False) - ga_instance.run() - -Here are the population after the 5 generations. Note how there are no -duplicate values. - -.. code:: python - - Generation 1 - [[ 2 -2 -3 3] - [ 0 1 2 3] - [ 5 -3 6 3] - [-3 1 -2 4] - [-1 0 -2 3]] - Generation 2 - [[-1 0 -2 3] - [-3 1 -2 4] - [ 0 -3 -2 6] - [-3 0 -2 3] - [ 1 -4 2 4]] - Generation 3 - [[ 1 -4 2 4] - [-3 0 -2 3] - [ 4 0 -2 1] - [-4 0 -2 -3] - [-4 2 0 3]] - Generation 4 - [[-4 2 0 3] - [-4 0 -2 -3] - [-2 5 4 -3] - [-1 2 -4 4] - [-4 2 0 -3]] - Generation 5 - [[-4 2 0 -3] - [-1 2 -4 4] - [ 3 4 -4 0] - [-1 0 2 -2] - [-4 2 -1 1]] - -The ``allow_duplicate_genes`` parameter is configured with use with the -``gene_space`` parameter. Here is an example where each of the 4 genes -has the same space of values that consists of 4 values (1, 2, 3, and 4). - -.. code:: python - - import pygad - - def fitness_func(ga_instance, solution, solution_idx): - return 0 - - def on_generation(ga): - print("Generation", ga.generations_completed) - print(ga.population) - - ga_instance = pygad.GA(num_generations=1, - sol_per_pop=5, - num_genes=4, - num_parents_mating=2, - fitness_func=fitness_func, - gene_type=int, - gene_space=[[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]], - on_generation=on_generation, - sample_size=200, - allow_duplicate_genes=False) - ga_instance.run() - -Even that all the genes share the same space of values, no 2 genes -duplicate their values as provided by the next output. - -.. code:: python - - Generation 1 - [[2 3 1 4] - [2 3 1 4] - [2 4 1 3] - [2 3 1 4] - [1 3 2 4]] - Generation 2 - [[1 3 2 4] - [2 3 1 4] - [1 3 2 4] - [2 3 4 1] - [1 3 4 2]] - Generation 3 - [[1 3 4 2] - [2 3 4 1] - [1 3 4 2] - [3 1 4 2] - [3 2 4 1]] - Generation 4 - [[3 2 4 1] - [3 1 4 2] - [3 2 4 1] - [1 2 4 3] - [1 3 4 2]] - Generation 5 - [[1 3 4 2] - [1 2 4 3] - [2 1 4 3] - [1 2 4 3] - [1 2 4 3]] - -You should care of giving enough values for the genes so that PyGAD is -able to find alternatives for the gene value in case it duplicates with -another gene. - -If PyGAD failed to find a unique gene while there is still room to find -a unique value, one possible option is to set the ``sample_size`` -parameter to a larger value. Check the `sample_size -Parameter `__ -section for more information. - -Limitation ----------- - -There might be 2 duplicate genes where changing either of the 2 -duplicating genes will not solve the problem. For example, if -``gene_space=[[3, 0, 1], [4, 1, 2], [0, 2], [3, 2, 0]]`` and the -solution is ``[3 2 0 0]``, then the values of the last 2 genes -duplicate. There are no possible changes in the last 2 genes to solve -the problem. - -This problem can be solved by randomly changing one of the -non-duplicating genes that may make a room for a unique value in one the -2 duplicating genes. For example, by changing the second gene from 2 to -4, then any of the last 2 genes can take the value 2 and solve the -duplicates. The resultant gene is then ``[3 4 2 0]``. But this option is -not yet supported in PyGAD. - -Solve Duplicates using a Third Gene ------------------------------------ - -When ``allow_duplicate_genes=False`` and a user-defined ``gene_space`` -is used, it sometimes happen that there is no room to solve the -duplicates between the 2 genes by simply replacing the value of one gene -by another gene. In `PyGAD -3.1.0 `__, -the duplicates are solved by looking for a third gene that will help in -solving the duplicates. The following examples explain how it works. - -Example 1: - -Let's assume that this gene space is used and there is a solution with 2 -duplicate genes with the same value 4. - -.. code:: python - - Gene space: [[2, 3], - [3, 4], - [4, 5], - [5, 6]] - Solution: [3, 4, 4, 5] - -By checking the gene space, the second gene can have the values -``[3, 4]`` and the third gene can have the values ``[4, 5]``. To solve -the duplicates, we have the value of any of these 2 genes. - -If the value of the second gene changes from 4 to 3, then it will be -duplicate with the first gene. If we are to change the value of the -third gene from 4 to 5, then it will duplicate with the fourth gene. As -a conclusion, trying to just selecting a different gene value for either -the second or third genes will introduce new duplicating genes. - -When there are 2 duplicate genes but there is no way to solve their -duplicates, then the solution is to change a third gene that makes a -room to solve the duplicates between the 2 genes. - -In our example, duplicates between the second and third genes can be -solved by, for example,: - -- Changing the first gene from 3 to 2 then changing the second gene from - 4 to 3. - -- Or changing the fourth gene from 5 to 6 then changing the third gene - from 4 to 5. - -Generally, this is how to solve such duplicates: - -1. For any duplicate gene **GENE1**, select another value. - -2. Check which other gene **GENEX** has duplicate with this new value. - -3. Find if **GENEX** can have another value that will not cause any more - duplicates. If so, go to step 7. - -4. If all the other values of **GENEX** will cause duplicates, then try - another gene **GENEY**. - -5. Repeat steps 3 and 4 until exploring all the genes. - -6. If there is no possibility to solve the duplicates, then there is not - way to solve the duplicates and we have to keep the duplicate value. - -7. If a value for a gene **GENEM** is found that will not cause more - duplicates, then use this value for the gene **GENEM**. - -8. Replace the value of the gene **GENE1** by the old value of the gene - **GENEM**. This solves the duplicates. - -This is an example to solve the duplicate for the solution -``[3, 4, 4, 5]``: - -1. Let's use the second gene with value 4. Because the space of this - gene is ``[3, 4]``, then the only other value we can select is 3. - -2. The first gene also have the value 3. - -3. The first gene has another value 2 that will not cause more - duplicates in the solution. Then go to step 7. - -4. Skip. - -5. Skip. - -6. Skip. - -7. The value of the first gene 3 will be replaced by the new value 2. - The new solution is [2, 4, 4, 5]. - -8. Replace the value of the second gene 4 by the old value of the first - gene which is 3. The new solution is [2, 3, 4, 5]. The duplicate is - solved. - -Example 2: - -.. code:: python - - Gene space: [[0, 1], - [1, 2], - [2, 3], - [3, 4]] - Solution: [1, 2, 2, 3] - -The quick summary is: - -- Change the value of the first gene from 1 to 0. The solution becomes - [0, 2, 2, 3]. - -- Change the value of the second gene from 2 to 1. The solution becomes - [0, 1, 2, 3]. The duplicate is solved. - -.. _more-about-the-genetype-parameter: - -More about the ``gene_type`` Parameter -====================================== - -The ``gene_type`` parameter allows the user to control the data type for -all genes at once or each individual gene. In `PyGAD -2.15.0 `__, -the ``gene_type`` parameter also supports customizing the precision for -``float`` data types. As a result, the ``gene_type`` parameter helps to: - -1. Select a data type for all genes with or without precision. - -2. Select a data type for each individual gene with or without - precision. - -Let's discuss things by examples. - -Data Type for All Genes without Precision ------------------------------------------ - -The data type for all genes can be specified by assigning the numeric -data type directly to the ``gene_type`` parameter. This is an example to -make all genes of ``int`` data types. - -.. code:: python - - gene_type=int - -Given that the supported numeric data types of PyGAD include Python's -``int`` and ``float`` in addition to all numeric types of ``NumPy``, -then any of these types can be assigned to the ``gene_type`` parameter. - -If no precision is specified for a ``float`` data type, then the -complete floating-point number is kept. - -The next code uses an ``int`` data type for all genes where the genes in -the initial and final population are only integers. - -.. code:: python - - import pygad - import numpy - - equation_inputs = [4, -2, 3.5, 8, -2] - desired_output = 2671.1234 - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - - ga_instance = pygad.GA(num_generations=10, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - gene_type=int) - - print("Initial Population") - print(ga_instance.initial_population) - - ga_instance.run() - - print("Final Population") - print(ga_instance.population) - -.. code:: python - - Initial Population - [[ 1 -1 2 0 -3] - [ 0 -2 0 -3 -1] - [ 0 -1 -1 2 0] - [-2 3 -2 3 3] - [ 0 0 2 -2 -2]] - - Final Population - [[ 1 -1 2 2 0] - [ 1 -1 2 2 0] - [ 1 -1 2 2 0] - [ 1 -1 2 2 0] - [ 1 -1 2 2 0]] - -Data Type for All Genes with Precision --------------------------------------- - -A precision can only be specified for a ``float`` data type and cannot -be specified for integers. Here is an example to use a precision of 3 -for the ``float`` data type. In this case, all genes are of type -``float`` and their maximum precision is 3. - -.. code:: python - - gene_type=[float, 3] - -The next code uses prints the initial and final population where the -genes are of type ``float`` with precision 3. - -.. code:: python - - import pygad - import numpy - - equation_inputs = [4, -2, 3.5, 8, -2] - desired_output = 2671.1234 - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - - return fitness - - ga_instance = pygad.GA(num_generations=10, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - gene_type=[float, 3]) - - print("Initial Population") - print(ga_instance.initial_population) - - ga_instance.run() - - print("Final Population") - print(ga_instance.population) - -.. code:: python - - Initial Population - [[-2.417 -0.487 3.623 2.457 -2.362] - [-1.231 0.079 -1.63 1.629 -2.637] - [ 0.692 -2.098 0.705 0.914 -3.633] - [ 2.637 -1.339 -1.107 -0.781 -3.896] - [-1.495 1.378 -1.026 3.522 2.379]] - - Final Population - [[ 1.714 -1.024 3.623 3.185 -2.362] - [ 0.692 -1.024 3.623 3.185 -2.362] - [ 0.692 -1.024 3.623 3.375 -2.362] - [ 0.692 -1.024 4.041 3.185 -2.362] - [ 1.714 -0.644 3.623 3.185 -2.362]] - -Data Type for each Individual Gene without Precision ----------------------------------------------------- - -In `PyGAD -2.14.0 `__, -the ``gene_type`` parameter allows customizing the gene type for each -individual gene. This is by using a ``list``/``tuple``/``numpy.ndarray`` -with number of elements equal to the number of genes. For each element, -a type is specified for the corresponding gene. - -This is an example for a 5-gene problem where different types are -assigned to the genes. - -.. code:: python - - gene_type=[int, float, numpy.float16, numpy.int8, float] - -This is a complete code that prints the initial and final population for -a custom-gene data type. - -.. code:: python - - import pygad - import numpy - - equation_inputs = [4, -2, 3.5, 8, -2] - desired_output = 2671.1234 - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - - ga_instance = pygad.GA(num_generations=10, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - gene_type=[int, float, numpy.float16, numpy.int8, float]) - - print("Initial Population") - print(ga_instance.initial_population) - - ga_instance.run() - - print("Final Population") - print(ga_instance.population) - -.. code:: python - - Initial Population - [[0 0.8615522360026828 0.7021484375 -2 3.5301821368185866] - [-3 2.648189378595294 -3.830078125 1 -0.9586271572917742] - [3 3.7729827570110714 1.2529296875 -3 1.395741994211889] - [0 1.0490687178053282 1.51953125 -2 0.7243617940450235] - [0 -0.6550158436937226 -2.861328125 -2 1.8212734549263097]] - - Final Population - [[3 3.7729827570110714 2.055 0 0.7243617940450235] - [3 3.7729827570110714 1.458 0 -0.14638754050305036] - [3 3.7729827570110714 1.458 0 0.0869406120516778] - [3 3.7729827570110714 1.458 0 0.7243617940450235] - [3 3.7729827570110714 1.458 0 -0.14638754050305036]] - -Data Type for each Individual Gene with Precision -------------------------------------------------- - -The precision can also be specified for the ``float`` data types as in -the next line where the second gene precision is 2 and last gene -precision is 1. - -.. code:: python - - gene_type=[int, [float, 2], numpy.float16, numpy.int8, [float, 1]] - -This is a complete example where the initial and final populations are -printed where the genes comply with the data types and precisions -specified. - -.. code:: python - - import pygad - import numpy - - equation_inputs = [4, -2, 3.5, 8, -2] - desired_output = 2671.1234 - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - - ga_instance = pygad.GA(num_generations=10, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - gene_type=[int, [float, 2], numpy.float16, numpy.int8, [float, 1]]) - - print("Initial Population") - print(ga_instance.initial_population) - - ga_instance.run() - - print("Final Population") - print(ga_instance.population) - -.. code:: python - - Initial Population - [[-2 -1.22 1.716796875 -1 0.2] - [-1 -1.58 -3.091796875 0 -1.3] - [3 3.35 -0.107421875 1 -3.3] - [-2 -3.58 -1.779296875 0 0.6] - [2 -3.73 2.65234375 3 -0.5]] - - Final Population - [[2 -4.22 3.47 3 -1.3] - [2 -3.73 3.47 3 -1.3] - [2 -4.22 3.47 2 -1.3] - [2 -4.58 3.47 3 -1.3] - [2 -3.73 3.47 3 -1.3]] - -Parallel Processing in PyGAD -============================ - -Starting from `PyGAD -2.17.0 `__, -parallel processing becomes supported. This section explains how to use -parallel processing in PyGAD. - -According to the `PyGAD -lifecycle `__, -parallel processing can be parallelized in only 2 operations: - -1. Population fitness calculation. - -2. Mutation. - -The reason is that the calculations in these 2 operations are -independent (i.e. each solution/chromosome is handled independently from -the others) and can be distributed across different processes or -threads. - -For the mutation operation, it does not do intensive calculations on the -CPU. Its calculations are simple like flipping the values of some genes -from 0 to 1 or adding a random value to some genes. So, it does not take -much CPU processing time. Experiments proved that parallelizing the -mutation operation across the solutions increases the time instead of -reducing it. This is because running multiple processes or threads adds -overhead to manage them. Thus, parallel processing cannot be applied on -the mutation operation. - -For the population fitness calculation, parallel processing can help -make a difference and reduce the processing time. But this is -conditional on the type of calculations done in the fitness function. If -the fitness function makes intensive calculations and takes much -processing time from the CPU, then it is probably that parallel -processing will help to cut down the overall time. - -This section explains how parallel processing works in PyGAD and how to -use parallel processing in PyGAD - -How to Use Parallel Processing in PyGAD ---------------------------------------- - -Starting from `PyGAD -2.17.0 `__, -a new parameter called ``parallel_processing`` added to the constructor -of the ``pygad.GA`` class. - -.. code:: python - - import pygad - ... - ga_instance = pygad.GA(..., - parallel_processing=...) - ... - -This parameter allows the user to do the following: - -1. Enable parallel processing. - -2. Select whether processes or threads are used. - -3. Specify the number of processes or threads to be used. - -These are 3 possible values for the ``parallel_processing`` parameter: - -1. ``None``: (Default) It means no parallel processing is used. - -2. A positive integer referring to the number of threads to be used - (i.e. threads, not processes, are used. - -3. ``list``/``tuple``: If a list or a tuple of exactly 2 elements is - assigned, then: - - 1. The first element can be either ``'process'`` or ``'thread'`` to - specify whether processes or threads are used, respectively. - - 2. The second element can be: - - 1. A positive integer to select the maximum number of processes or - threads to be used - - 2. ``0`` to indicate that 0 processes or threads are used. It - means no parallel processing. This is identical to setting - ``parallel_processing=None``. - - 3. ``None`` to use the default value as calculated by the - ``concurrent.futures module``. - -These are examples of the values assigned to the ``parallel_processing`` -parameter: - -- ``parallel_processing=4``: Because the parameter is assigned a - positive integer, this means parallel processing is activated where 4 - threads are used. - -- ``parallel_processing=["thread", 5]``: Use parallel processing with 5 - threads. This is identical to ``parallel_processing=5``. - -- ``parallel_processing=["process", 8]``: Use parallel processing with 8 - processes. - -- ``parallel_processing=["process", 0]``: As the second element is given - the value 0, this means do not use parallel processing. This is - identical to ``parallel_processing=None``. - -Examples --------- - -The examples will help you know the difference between using processes -and threads. Moreover, it will give an idea when parallel processing -would make a difference and reduce the time. These are dummy examples -where the fitness function is made to always return 0. - -The first example uses 10 genes, 5 solutions in the population where -only 3 solutions mate, and 9999 generations. The fitness function uses a -``for`` loop with 100 iterations just to have some calculations. In the -constructor of the ``pygad.GA`` class, ``parallel_processing=None`` -means no parallel processing is used. - -.. code:: python - - import pygad - import time - - def fitness_func(ga_instance, solution, solution_idx): - for _ in range(99): - pass - return 0 - - ga_instance = pygad.GA(num_generations=9999, - num_parents_mating=3, - sol_per_pop=5, - num_genes=10, - fitness_func=fitness_func, - suppress_warnings=True, - parallel_processing=None) - - if __name__ == '__main__': - t1 = time.time() - - ga_instance.run() - - t2 = time.time() - print("Time is", t2-t1) - -When parallel processing is not used, the time it takes to run the -genetic algorithm is ``1.5`` seconds. - -In the comparison, let's do a second experiment where parallel -processing is used with 5 threads. In this case, it take ``5`` seconds. - -.. code:: python - - ... - ga_instance = pygad.GA(..., - parallel_processing=5) - ... - -For the third experiment, processes instead of threads are used. Also, -only 99 generations are used instead of 9999. The time it takes is -``99`` seconds. - -.. code:: python - - ... - ga_instance = pygad.GA(num_generations=99, - ..., - parallel_processing=["process", 5]) - ... - -This is the summary of the 3 experiments: - -1. No parallel processing & 9999 generations: 1.5 seconds. - -2. Parallel processing with 5 threads & 9999 generations: 5 seconds - -3. Parallel processing with 5 processes & 99 generations: 99 seconds - -Because the fitness function does not need much CPU time, the normal -processing takes the least time. Running processes for this simple -problem takes 99 compared to only 5 seconds for threads because managing -processes is much heavier than managing threads. Thus, most of the CPU -time is for swapping the processes instead of executing the code. - -In the second example, the loop makes 99999999 iterations and only 5 -generations are used. With no parallelization, it takes 22 seconds. - -.. code:: python - - import pygad - import time - - def fitness_func(ga_instance, solution, solution_idx): - for _ in range(99999999): - pass - return 0 - - ga_instance = pygad.GA(num_generations=5, - num_parents_mating=3, - sol_per_pop=5, - num_genes=10, - fitness_func=fitness_func, - suppress_warnings=True, - parallel_processing=None) - - if __name__ == '__main__': - t1 = time.time() - ga_instance.run() - t2 = time.time() - print("Time is", t2-t1) - -It takes 15 seconds when 10 processes are used. - -.. code:: python - - ... - ga_instance = pygad.GA(..., - parallel_processing=["process", 10]) - ... - -This is compared to 20 seconds when 10 threads are used. - -.. code:: python - - ... - ga_instance = pygad.GA(..., - parallel_processing=["thread", 10]) - ... - -Based on the second example, using parallel processing with 10 processes -takes the least time because there is much CPU work done. Generally, -processes are preferred over threads when most of the work in on the -CPU. Threads are preferred over processes in some situations like doing -input/output operations. - -*Before releasing* `PyGAD -2.17.0 `__\ *,* -`László -Fazekas `__ -*wrote an article to parallelize the fitness function with PyGAD. Check -it:* `How Genetic Algorithms Can Compete with Gradient Descent and -Backprop `__. - -Print Lifecycle Summary -======================= - -In `PyGAD -2.19.0 `__, -a new method called ``summary()`` is supported. It prints a Keras-like -summary of the PyGAD lifecycle showing the steps, callback functions, -parameters, etc. - -This method accepts the following parameters: - -- ``line_length=70``: An integer representing the length of the single - line in characters. - -- ``fill_character=" "``: A character to fill the lines. - -- ``line_character="-"``: A character for creating a line separator. - -- ``line_character2="="``: A secondary character to create a line - separator. - -- ``columns_equal_len=False``: The table rows are split into equal-sized - columns or split subjective to the width needed. - -- ``print_step_parameters=True``: Whether to print extra parameters - about each step inside the step. If ``print_step_parameters=False`` - and ``print_parameters_summary=True``, then the parameters of each - step are printed at the end of the table. - -- ``print_parameters_summary=True``: Whether to print parameters summary - at the end of the table. If ``print_step_parameters=False``, then the - parameters of each step are printed at the end of the table too. - -This is a quick example to create a PyGAD example. - -.. code:: python - - import pygad - import numpy - - function_inputs = [4,-2,3.5,5,-11,-4.7] - desired_output = 44 - - def genetic_fitness(solution, solution_idx): - output = numpy.sum(solution*function_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - - def on_gen(ga): - pass - - def on_crossover_callback(a, b): - pass - - ga_instance = pygad.GA(num_generations=100, - num_parents_mating=10, - sol_per_pop=20, - num_genes=len(function_inputs), - on_crossover=on_crossover_callback, - on_generation=on_gen, - parallel_processing=2, - stop_criteria="reach_10", - fitness_batch_size=4, - crossover_probability=0.4, - fitness_func=genetic_fitness) - -Then call the ``summary()`` method to print the summary with the default -parameters. Note that entries for the crossover and generation callback -function are created because their callback functions are implemented -through the ``on_crossover_callback()`` and ``on_gen()``, respectively. - -.. code:: python - - ga_instance.summary() - -.. code:: bash - - ---------------------------------------------------------------------- - PyGAD Lifecycle - ====================================================================== - Step Handler Output Shape - ====================================================================== - Fitness Function genetic_fitness() (1) - Fitness batch size: 4 - ---------------------------------------------------------------------- - Parent Selection steady_state_selection() (10, 6) - Number of Parents: 10 - ---------------------------------------------------------------------- - Crossover single_point_crossover() (10, 6) - Crossover probability: 0.4 - ---------------------------------------------------------------------- - On Crossover on_crossover_callback() None - ---------------------------------------------------------------------- - Mutation random_mutation() (10, 6) - Mutation Genes: 1 - Random Mutation Range: (-1.0, 1.0) - Mutation by Replacement: False - Allow Duplicated Genes: True - ---------------------------------------------------------------------- - On Generation on_gen() None - Stop Criteria: [['reach', 10.0]] - ---------------------------------------------------------------------- - ====================================================================== - Population Size: (20, 6) - Number of Generations: 100 - Initial Population Range: (-4, 4) - Keep Elitism: 1 - Gene DType: [, None] - Parallel Processing: ['thread', 2] - Save Best Solutions: False - Save Solutions: False - ====================================================================== - -We can set the ``print_step_parameters`` and -``print_parameters_summary`` parameters to ``False`` to not print the -parameters. - -.. code:: python - - ga_instance.summary(print_step_parameters=False, - print_parameters_summary=False) - -.. code:: bash - - ---------------------------------------------------------------------- - PyGAD Lifecycle - ====================================================================== - Step Handler Output Shape - ====================================================================== - Fitness Function genetic_fitness() (1) - ---------------------------------------------------------------------- - Parent Selection steady_state_selection() (10, 6) - ---------------------------------------------------------------------- - Crossover single_point_crossover() (10, 6) - ---------------------------------------------------------------------- - On Crossover on_crossover_callback() None - ---------------------------------------------------------------------- - Mutation random_mutation() (10, 6) - ---------------------------------------------------------------------- - On Generation on_gen() None - ---------------------------------------------------------------------- - ====================================================================== - -Logging Outputs -=============== - -In `PyGAD -3.0.0 `__, -the ``print()`` statement is no longer used and the outputs are printed -using the `logging `__ -module. A a new parameter called ``logger`` is supported to accept the -user-defined logger. - -.. code:: python - - import logging - - logger = ... - - ga_instance = pygad.GA(..., - logger=logger, - ...) - -The default value for this parameter is ``None``. If there is no logger -passed (i.e. ``logger=None``), then a default logger is created to log -the messages to the console exactly like how the ``print()`` statement -works. - -Some advantages of using the the -`logging `__ module -instead of the ``print()`` statement are: - -1. The user has more control over the printed messages specially if - there is a project that uses multiple modules where each module - prints its messages. A logger can organize the outputs. - -2. Using the proper ``Handler``, the user can log the output messages to - files and not only restricted to printing it to the console. So, it - is much easier to record the outputs. - -3. The format of the printed messages can be changed by customizing the - ``Formatter`` assigned to the Logger. - -This section gives some quick examples to use the ``logging`` module and -then gives an example to use the logger with PyGAD. - -Logging to the Console ----------------------- - -This is an example to create a logger to log the messages to the -console. - -.. code:: python - - import logging - - # Create a logger - logger = logging.getLogger(__name__) - - # Set the logger level to debug so that all the messages are printed. - logger.setLevel(logging.DEBUG) - - # Create a stream handler to log the messages to the console. - stream_handler = logging.StreamHandler() - - # Set the handler level to debug. - stream_handler.setLevel(logging.DEBUG) - - # Create a formatter - formatter = logging.Formatter('%(message)s') - - # Add the formatter to handler. - stream_handler.setFormatter(formatter) - - # Add the stream handler to the logger - logger.addHandler(stream_handler) - -Now, we can log messages to the console with the format specified in the -``Formatter``. - -.. code:: python - - logger.debug('Debug message.') - logger.info('Info message.') - logger.warning('Warn message.') - logger.error('Error message.') - logger.critical('Critical message.') - -The outputs are identical to those returned using the ``print()`` -statement. - -.. code:: - - Debug message. - Info message. - Warn message. - Error message. - Critical message. - -By changing the format of the output messages, we can have more -information about each message. - -.. code:: python - - formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S') - -This is a sample output. - -.. code:: python - - 2023-04-03 18:46:27 DEBUG: Debug message. - 2023-04-03 18:46:27 INFO: Info message. - 2023-04-03 18:46:27 WARNING: Warn message. - 2023-04-03 18:46:27 ERROR: Error message. - 2023-04-03 18:46:27 CRITICAL: Critical message. - -Note that you may need to clear the handlers after finishing the -execution. This is to make sure no cached handlers are used in the next -run. If the cached handlers are not cleared, then the single output -message may be repeated. - -.. code:: python - - logger.handlers.clear() - -Logging to a File ------------------ - -This is another example to log the messages to a file named -``logfile.txt``. The formatter prints the following about each message: - -1. The date and time at which the message is logged. - -2. The log level. - -3. The message. - -4. The path of the file. - -5. The lone number of the log message. - -.. code:: python - - import logging - - level = logging.DEBUG - name = 'logfile.txt' - - logger = logging.getLogger(name) - logger.setLevel(level) - - file_handler = logging.FileHandler(name, 'a+', 'utf-8') - file_handler.setLevel(logging.DEBUG) - file_format = logging.Formatter('%(asctime)s %(levelname)s: %(message)s - %(pathname)s:%(lineno)d', datefmt='%Y-%m-%d %H:%M:%S') - file_handler.setFormatter(file_format) - logger.addHandler(file_handler) - -This is how the outputs look like. - -.. code:: python - - 2023-04-03 18:54:03 DEBUG: Debug message. - c:\users\agad069\desktop\logger\example2.py:46 - 2023-04-03 18:54:03 INFO: Info message. - c:\users\agad069\desktop\logger\example2.py:47 - 2023-04-03 18:54:03 WARNING: Warn message. - c:\users\agad069\desktop\logger\example2.py:48 - 2023-04-03 18:54:03 ERROR: Error message. - c:\users\agad069\desktop\logger\example2.py:49 - 2023-04-03 18:54:03 CRITICAL: Critical message. - c:\users\agad069\desktop\logger\example2.py:50 - -Consider clearing the handlers if necessary. - -.. code:: python - - logger.handlers.clear() - -Log to Both the Console and a File ----------------------------------- - -This is an example to create a single Logger associated with 2 handlers: - -1. A file handler. - -2. A stream handler. - -.. code:: python - - import logging - - level = logging.DEBUG - name = 'logfile.txt' - - logger = logging.getLogger(name) - logger.setLevel(level) - - file_handler = logging.FileHandler(name,'a+','utf-8') - file_handler.setLevel(logging.DEBUG) - file_format = logging.Formatter('%(asctime)s %(levelname)s: %(message)s - %(pathname)s:%(lineno)d', datefmt='%Y-%m-%d %H:%M:%S') - file_handler.setFormatter(file_format) - logger.addHandler(file_handler) - - console_handler = logging.StreamHandler() - console_handler.setLevel(logging.INFO) - console_format = logging.Formatter('%(message)s') - console_handler.setFormatter(console_format) - logger.addHandler(console_handler) - -When a log message is executed, then it is both printed to the console -and saved in the ``logfile.txt``. - -Consider clearing the handlers if necessary. - -.. code:: python - - logger.handlers.clear() - -PyGAD Example -------------- - -To use the logger in PyGAD, just create your custom logger and pass it -to the ``logger`` parameter. - -.. code:: python - - import logging - import pygad - import numpy - - level = logging.DEBUG - name = 'logfile.txt' - - logger = logging.getLogger(name) - logger.setLevel(level) - - file_handler = logging.FileHandler(name,'a+','utf-8') - file_handler.setLevel(logging.DEBUG) - file_format = logging.Formatter('%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S') - file_handler.setFormatter(file_format) - logger.addHandler(file_handler) - - console_handler = logging.StreamHandler() - console_handler.setLevel(logging.INFO) - console_format = logging.Formatter('%(message)s') - console_handler.setFormatter(console_format) - logger.addHandler(console_handler) - - equation_inputs = [4, -2, 8] - desired_output = 2671.1234 - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - - def on_generation(ga_instance): - ga_instance.logger.info(f"Generation = {ga_instance.generations_completed}") - ga_instance.logger.info(f"Fitness = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1]}") - - ga_instance = pygad.GA(num_generations=10, - sol_per_pop=40, - num_parents_mating=2, - keep_parents=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - on_generation=on_generation, - logger=logger) - ga_instance.run() - - logger.handlers.clear() - -By executing this code, the logged messages are printed to the console -and also saved in the text file. - -.. code:: python - - 2023-04-03 19:04:27 INFO: Generation = 1 - 2023-04-03 19:04:27 INFO: Fitness = 0.00038086960368076276 - 2023-04-03 19:04:27 INFO: Generation = 2 - 2023-04-03 19:04:27 INFO: Fitness = 0.00038214871408010853 - 2023-04-03 19:04:27 INFO: Generation = 3 - 2023-04-03 19:04:27 INFO: Fitness = 0.0003832795907974678 - 2023-04-03 19:04:27 INFO: Generation = 4 - 2023-04-03 19:04:27 INFO: Fitness = 0.00038398612055017196 - 2023-04-03 19:04:27 INFO: Generation = 5 - 2023-04-03 19:04:27 INFO: Fitness = 0.00038442348890867516 - 2023-04-03 19:04:27 INFO: Generation = 6 - 2023-04-03 19:04:27 INFO: Fitness = 0.0003854406039137763 - 2023-04-03 19:04:27 INFO: Generation = 7 - 2023-04-03 19:04:27 INFO: Fitness = 0.00038646083174063284 - 2023-04-03 19:04:27 INFO: Generation = 8 - 2023-04-03 19:04:27 INFO: Fitness = 0.0003875169193024936 - 2023-04-03 19:04:27 INFO: Generation = 9 - 2023-04-03 19:04:27 INFO: Fitness = 0.0003888816727311021 - 2023-04-03 19:04:27 INFO: Generation = 10 - 2023-04-03 19:04:27 INFO: Fitness = 0.000389832593101348 - -Solve Non-Deterministic Problems -================================ - -PyGAD can be used to solve both deterministic and non-deterministic -problems. Deterministic are those that return the same fitness for the -same solution. For non-deterministic problems, a different fitness value -would be returned for the same solution. - -By default, PyGAD settings are set to solve deterministic problems. -PyGAD can save the explored solutions and their fitness to reuse in the -future. These instances attributes can save the solutions: - -1. ``solutions``: Exists if ``save_solutions=True``. - -2. ``best_solutions``: Exists if ``save_best_solutions=True``. - -3. ``last_generation_elitism``: Exists if ``keep_elitism`` > 0. - -4. ``last_generation_parents``: Exists if ``keep_parents`` > 0 or - ``keep_parents=-1``. - -To configure PyGAD for non-deterministic problems, we have to disable -saving the previous solutions. This is by setting these parameters: - -1. ``keep_elitism=0`` - -2. ``keep_parents=0`` - -3. ``keep_solutions=False`` - -4. ``keep_best_solutions=False`` - -.. code:: python - - import pygad - ... - ga_instance = pygad.GA(..., - keep_elitism=0, - keep_parents=0, - save_solutions=False, - save_best_solutions=False, - ...) - -This way PyGAD will not save any explored solution and thus the fitness -function have to be called for each individual solution. - -Reuse the Fitness instead of Calling the Fitness Function -========================================================= - -It may happen that a previously explored solution in generation X is -explored again in another generation Y (where Y > X). For some problems, -calling the fitness function takes much time. - -For deterministic problems, it is better to not call the fitness -function for an already explored solutions. Instead, reuse the fitness -of the old solution. PyGAD supports some options to help you save time -calling the fitness function for a previously explored solution. - -The parameters explored in this section can be set in the constructor of -the ``pygad.GA`` class. - -The ``cal_pop_fitness()`` method of the ``pygad.GA`` class checks these -parameters to see if there is a possibility of reusing the fitness -instead of calling the fitness function. - -.. _1-savesolutions: - -1. ``save_solutions`` ---------------------- - -It defaults to ``False``. If set to ``True``, then the population of -each generation is saved into the ``solutions`` attribute of the -``pygad.GA`` instance. In other words, every single solution is saved in -the ``solutions`` attribute. - -.. _2-savebestsolutions: - -2. ``save_best_solutions`` --------------------------- - -It defaults to ``False``. If ``True``, then it only saves the best -solution in every generation. - -.. _3-keepelitism: - -3. ``keep_elitism`` -------------------- - -It accepts an integer and defaults to 1. If set to a positive integer, -then it keeps the elitism of one generation available in the next -generation. - -.. _4-keepparents: - -4. ``keep_parents`` -------------------- - -It accepts an integer and defaults to -1. It set to ``-1`` or a positive -integer, then it keeps the parents of one generation available in the -next generation. - -Why the Fitness Function is not Called for Solution at Index 0? -=============================================================== - -PyGAD has a parameter called ``keep_elitism`` which defaults to 1. This -parameter defines the number of best solutions in generation **X** to -keep in the next generation **X+1**. The best solutions are just copied -from generation **X** to generation **X+1** without making any change. - -.. code:: python - - ga_instance = pygad.GA(..., - keep_elitism=1, - ...) - -The best solutions are copied at the beginning of the population. If -``keep_elitism=1``, this means the best solution in generation X is kept -in the next generation X+1 at index 0 of the population. If -``keep_elitism=2``, this means the 2 best solutions in generation X are -kept in the next generation X+1 at indices 0 and 1 of the population of -generation 1. - -Because the fitness of these best solutions are already calculated in -generation X, then their fitness values will not be recalculated at -generation X+1 (i.e. the fitness function will not be called for these -solutions again). Instead, their fitness values are just reused. This is -why you see that no solution with index 0 is passed to the fitness -function. - -To force calling the fitness function for each solution in every -generation, consider setting ``keep_elitism`` and ``keep_parents`` to 0. -Moreover, keep the 2 parameters ``save_solutions`` and -``save_best_solutions`` to their default value ``False``. - -.. code:: python - - ga_instance = pygad.GA(..., - keep_elitism=0, - keep_parents=0, - save_solutions=False, - save_best_solutions=False, - ...) - -Batch Fitness Calculation -========================= - -In `PyGAD -2.19.0 `__, -a new optional parameter called ``fitness_batch_size`` is supported. A -new optional parameter called ``fitness_batch_size`` is supported to -calculate the fitness function in batches. Thanks to `Linan -Qiu `__ for opening the `GitHub issue -#136 `__. - -Its values can be: - -- ``1`` or ``None``: If the ``fitness_batch_size`` parameter is assigned - the value ``1`` or ``None`` (default), then the normal flow is used - where the fitness function is called for each individual solution. - That is if there are 15 solutions, then the fitness function is called - 15 times. - -- ``1 < fitness_batch_size <= sol_per_pop``: If the - ``fitness_batch_size`` parameter is assigned a value satisfying this - condition ``1 < fitness_batch_size <= sol_per_pop``, then the - solutions are grouped into batches of size ``fitness_batch_size`` and - the fitness function is called once for each batch. In this case, the - fitness function must return a list/tuple/numpy.ndarray with a length - equal to the number of solutions passed. - -.. _example-without-fitnessbatchsize-parameter: - -Example without ``fitness_batch_size`` Parameter ------------------------------------------------- - -This is an example where the ``fitness_batch_size`` parameter is given -the value ``None`` (which is the default value). This is equivalent to -using the value ``1``. In this case, the fitness function will be called -for each solution. This means the fitness function ``fitness_func`` will -receive only a single solution. This is an example of the passed -arguments to the fitness function: - -.. code:: - - solution: [ 2.52860734, -0.94178795, 2.97545704, 0.84131987, -3.78447118, 2.41008358] - solution_idx: 3 - -The fitness function also must return a single numeric value as the -fitness for the passed solution. - -As we have a population of ``20`` solutions, then the fitness function -is called 20 times per generation. For 5 generations, then the fitness -function is called ``20*5 = 100`` times. In PyGAD, the fitness function -is called after the last generation too and this adds additional 20 -times. So, the total number of calls to the fitness function is -``20*5 + 20 = 120``. - -Note that the ``keep_elitism`` and ``keep_parents`` parameters are set -to ``0`` to make sure no fitness values are reused and to force calling -the fitness function for each individual solution. - -.. code:: python - - import pygad - import numpy - - function_inputs = [4,-2,3.5,5,-11,-4.7] - desired_output = 44 - - number_of_calls = 0 - - def fitness_func(ga_instance, solution, solution_idx): - global number_of_calls - number_of_calls = number_of_calls + 1 - output = numpy.sum(solution*function_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - - ga_instance = pygad.GA(num_generations=5, - num_parents_mating=10, - sol_per_pop=20, - fitness_func=fitness_func, - fitness_batch_size=None, - # fitness_batch_size=1, - num_genes=len(function_inputs), - keep_elitism=0, - keep_parents=0) - - ga_instance.run() - print(number_of_calls) - -.. code:: - - 120 - -.. _example-with-fitnessbatchsize-parameter: - -Example with ``fitness_batch_size`` Parameter ---------------------------------------------- - -This is an example where the ``fitness_batch_size`` parameter is used -and assigned the value ``4``. This means the solutions will be grouped -into batches of ``4`` solutions. The fitness function will be called -once for each patch (i.e. called once for each 4 solutions). - -This is an example of the arguments passed to it: - -.. code:: python - - solutions: - [[ 3.1129432 -0.69123589 1.93792414 2.23772968 -1.54616001 -0.53930799] - [ 3.38508121 0.19890812 1.93792414 2.23095014 -3.08955597 3.10194128] - [ 2.37079504 -0.88819803 2.97545704 1.41742256 -3.95594055 2.45028256] - [ 2.52860734 -0.94178795 2.97545704 0.84131987 -3.78447118 2.41008358]] - solutions_indices: - [16, 17, 18, 19] - -As we have 20 solutions, then there are ``20/4 = 5`` patches. As a -result, the fitness function is called only 5 times per generation -instead of 20. For each call to the fitness function, it receives a -batch of 4 solutions. - -As we have 5 generations, then the function will be called ``5*5 = 25`` -times. Given the call to the fitness function after the last generation, -then the total number of calls is ``5*5 + 5 = 30``. - -.. code:: python - - import pygad - import numpy - - function_inputs = [4,-2,3.5,5,-11,-4.7] - desired_output = 44 - - number_of_calls = 0 - - def fitness_func_batch(ga_instance, solutions, solutions_indices): - global number_of_calls - number_of_calls = number_of_calls + 1 - batch_fitness = [] - for solution in solutions: - output = numpy.sum(solution*function_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - batch_fitness.append(fitness) - return batch_fitness - - ga_instance = pygad.GA(num_generations=5, - num_parents_mating=10, - sol_per_pop=20, - fitness_func=fitness_func_batch, - fitness_batch_size=4, - num_genes=len(function_inputs), - keep_elitism=0, - keep_parents=0) - - ga_instance.run() - print(number_of_calls) - -.. code:: - - 30 - -When batch fitness calculation is used, then we saved ``120 - 30 = 90`` -calls to the fitness function. - -Use Functions and Methods to Build Fitness and Callbacks -======================================================== - -In PyGAD 2.19.0, it is possible to pass user-defined functions or -methods to the following parameters: - -1. ``fitness_func`` - -2. ``on_start`` - -3. ``on_fitness`` - -4. ``on_parents`` - -5. ``on_crossover`` - -6. ``on_mutation`` - -7. ``on_generation`` - -8. ``on_stop`` - -This section gives 2 examples to assign these parameters user-defined: - -1. Functions. - -2. Methods. - -Assign Functions ----------------- - -This is a dummy example where the fitness function returns a random -value. Note that the instance of the ``pygad.GA`` class is passed as the -last parameter of all functions. - -.. code:: python - - import pygad - import numpy - - def fitness_func(ga_instanse, solution, solution_idx): - return numpy.random.rand() - - def on_start(ga_instanse): - print("on_start") - - def on_fitness(ga_instanse, last_gen_fitness): - print("on_fitness") - - def on_parents(ga_instanse, last_gen_parents): - print("on_parents") - - def on_crossover(ga_instanse, last_gen_offspring): - print("on_crossover") - - def on_mutation(ga_instanse, last_gen_offspring): - print("on_mutation") - - def on_generation(ga_instanse): - print("on_generation\n") - - def on_stop(ga_instanse, last_gen_fitness): - print("on_stop") - - ga_instance = pygad.GA(num_generations=5, - num_parents_mating=4, - sol_per_pop=10, - num_genes=2, - on_start=on_start, - on_fitness=on_fitness, - on_parents=on_parents, - on_crossover=on_crossover, - on_mutation=on_mutation, - on_generation=on_generation, - on_stop=on_stop, - fitness_func=fitness_func) - - ga_instance.run() - -Assign Methods --------------- - -The next example has all the method defined inside the class ``Test``. -All of the methods accept an additional parameter representing the -method's object of the class ``Test``. - -All methods accept ``self`` as the first parameter and the instance of -the ``pygad.GA`` class as the last parameter. - -.. code:: python - - import pygad - import numpy - - class Test: - def fitness_func(self, ga_instanse, solution, solution_idx): - return numpy.random.rand() - - def on_start(self, ga_instanse): - print("on_start") - - def on_fitness(self, ga_instanse, last_gen_fitness): - print("on_fitness") - - def on_parents(self, ga_instanse, last_gen_parents): - print("on_parents") - - def on_crossover(self, ga_instanse, last_gen_offspring): - print("on_crossover") - - def on_mutation(self, ga_instanse, last_gen_offspring): - print("on_mutation") - - def on_generation(self, ga_instanse): - print("on_generation\n") - - def on_stop(self, ga_instanse, last_gen_fitness): - print("on_stop") - - ga_instance = pygad.GA(num_generations=5, - num_parents_mating=4, - sol_per_pop=10, - num_genes=2, - on_start=Test().on_start, - on_fitness=Test().on_fitness, - on_parents=Test().on_parents, - on_crossover=Test().on_crossover, - on_mutation=Test().on_mutation, - on_generation=Test().on_generation, - on_stop=Test().on_stop, - fitness_func=Test().fitness_func) - - ga_instance.run() - -.. |image1| image:: https://github.com/ahmedfgad/GeneticAlgorithmPython/assets/16560492/7896f8d8-01c5-4ff9-8d15-52191c309b63 -.. |image2| image:: https://user-images.githubusercontent.com/16560492/189273225-67ffad41-97ab-45e1-9324-429705e17b20.png diff --git a/docs/md/releases.md b/docs/source/releases.md similarity index 98% rename from docs/md/releases.md rename to docs/source/releases.md index 7089f97..45a5d4a 100644 --- a/docs/md/releases.md +++ b/docs/source/releases.md @@ -178,7 +178,7 @@ Release Date: 06 December 2020 Release Date: 03 January 2021 1. Support of a new module `pygad.torchga` to train PyTorch models using PyGAD. Check [its documentation](https://pygad.readthedocs.io/en/latest/torchga.html). -2. Support of adaptive mutation where the mutation rate is determined by the fitness value of each solution. Read the [Adaptive Mutation](https://pygad.readthedocs.io/en/latest/pygad_more.html#adaptive-mutation) section for more details. Also, read this paper: [Libelli, S. Marsili, and P. Alba. "Adaptive mutation in genetic algorithms." Soft computing 4.2 (2000): 76-80.](https://www.researchgate.net/publication/225642916_Adaptive_mutation_in_genetic_algorithms) +2. Support of adaptive mutation where the mutation rate is determined by the fitness value of each solution. Read the [Adaptive Mutation](https://pygad.readthedocs.io/en/latest/utils.html#adaptive-mutation) section for more details. Also, read this paper: [Libelli, S. Marsili, and P. Alba. "Adaptive mutation in genetic algorithms." Soft computing 4.2 (2000): 76-80.](https://www.researchgate.net/publication/225642916_Adaptive_mutation_in_genetic_algorithms) 3. Before the `run()` method completes or exits, the fitness value of the best solution in the current population is appended to the `best_solution_fitness` list attribute. Note that the fitness value of the best solution in the initial population is already saved at the beginning of the list. So, the fitness value of the best solution is saved before the genetic algorithm starts and after it ends. 4. When the parameter `parent_selection_type` is set to `sss` (steady-state selection), then a warning message is printed if the value of the `keep_parents` parameter is set to 0. 5. More validations to the user input parameters. @@ -208,7 +208,7 @@ Release Date: 15 January 2021 Release Date: 16 February 2021 -1. In the `gene_space` argument, the user can use a dictionary to specify the lower and upper limits of the gene. This dictionary must have only 2 items with keys `low` and `high` to specify the low and high limits of the gene, respectively. This way, PyGAD takes care of not exceeding the value limits of the gene. For a problem with only 2 genes, then using `gene_space=[{'low': 1, 'high': 5}, {'low': 0.2, 'high': 0.81}]` means the accepted values in the first gene start from 1 (inclusive) to 5 (exclusive) while the second one has values between 0.2 (inclusive) and 0.85 (exclusive). For more information, please check the [Limit the Gene Value Range](https://pygad.readthedocs.io/en/latest/pygad_more.html#limit-the-gene-value-range) section of the documentation. +1. In the `gene_space` argument, the user can use a dictionary to specify the lower and upper limits of the gene. This dictionary must have only 2 items with keys `low` and `high` to specify the low and high limits of the gene, respectively. This way, PyGAD takes care of not exceeding the value limits of the gene. For a problem with only 2 genes, then using `gene_space=[{'low': 1, 'high': 5}, {'low': 0.2, 'high': 0.81}]` means the accepted values in the first gene start from 1 (inclusive) to 5 (exclusive) while the second one has values between 0.2 (inclusive) and 0.85 (exclusive). For more information, please check the [Limit the Gene Value Range](https://pygad.readthedocs.io/en/latest/pygad_more.html#limit-the-gene-value-range-using-the-gene-space-parameter) section of the documentation. 2. The `plot_result()` method returns the figure so that the user can save it. 3. Bug fixes in copying elements from the gene space. 4. For a gene with a set of discrete values (more than 1 value) in the `gene_space` parameter like `[0, 1]`, it was possible that the gene value may not change after mutation. That is if the current value is 0, then the randomly selected value could also be 0. Now, it is verified that the new value is changed. So, if the current value is 0, then the new value after mutation will not be 0 but 1. @@ -293,7 +293,7 @@ Release Date: 18 June 2021 Release Date: 19 June 2021 -1. A user-defined function can be passed to the `mutation_type`, `crossover_type`, and `parent_selection_type` parameters in the `pygad.GA` class to create a custom mutation, crossover, and parent selection operators. Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/pygad_more.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details. https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/50 +1. A user-defined function can be passed to the `mutation_type`, `crossover_type`, and `parent_selection_type` parameters in the `pygad.GA` class to create a custom mutation, crossover, and parent selection operators. Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/utils.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details. https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/50 ## PyGAD 2.16.1 @@ -658,7 +658,7 @@ Release Date April 8, 2026 22. Fix some documentation issues. https://github.com/ahmedfgad/GeneticAlgorithmPython/pull/336 23. Update the documentation to reflect the recent additions and changes to the library structure. -# PyGAD Projects at GitHub +## PyGAD Projects at GitHub The PyGAD library is available at PyPI at this page https://pypi.org/project/pygad. PyGAD is built out of a number of open-source GitHub projects. A brief note about these projects is given in the next subsections. @@ -706,7 +706,7 @@ GitHub Link: https://github.com/ahmedfgad/TorchGA [pygad.torchga](https://github.com/ahmedfgad/TorchGA): https://github.com/ahmedfgad/TorchGA -# Stackoverflow Questions about PyGAD +## Stackoverflow Questions about PyGAD ## [How do I proceed to load a ga_instance as “.pkl” format in PyGad?](https://stackoverflow.com/questions/67424181/how-do-i-proceed-to-load-a-ga-instance-as-pkl-format-in-pygad) @@ -738,27 +738,27 @@ https://blog.csdn.net/sinat_38079265/article/details/108449614 -# Submitting Issues +## Submitting Issues If there is an issue using PyGAD, then use any of your preferred option to discuss that issue. One way is [submitting an issue](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/new) into this GitHub project ([github.com/ahmedfgad/GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython)) in case something is not working properly or to ask for questions. -If this is not a proper option for you, then check the [**Contact Us**](https://pygad.readthedocs.io/en/latest/Footer.html#contact-us) section for more contact details. +If this is not a proper option for you, then check the [**Contact Us**](https://pygad.readthedocs.io/en/latest/releases.html#contact-us) section for more contact details. -# Ask for Feature +## Ask for Feature PyGAD is actively developed with the goal of building a dynamic library for suporting a wide-range of problems to be optimized using the genetic algorithm. To ask for a new feature, either [submit an issue](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/new) into this GitHub project ([github.com/ahmedfgad/GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython)) or send an e-mail to ahmed.f.gad@gmail.com. -Also check the [**Contact Us**](https://pygad.readthedocs.io/en/latest/Footer.html#contact-us) section for more contact details. +Also check the [**Contact Us**](https://pygad.readthedocs.io/en/latest/releases.html#contact-us) section for more contact details. -# Projects Built using PyGAD +## Projects Built using PyGAD If you created a project that uses PyGAD, then we can support you by mentioning this project here in PyGAD's documentation. -To do that, please send a message at ahmed.f.gad@gmail.com or check the [**Contact Us**](https://pygad.readthedocs.io/en/latest/Footer.html#contact-us) section for more contact details. +To do that, please send a message at ahmed.f.gad@gmail.com or check the [**Contact Us**](https://pygad.readthedocs.io/en/latest/releases.html#contact-us) section for more contact details. Within your message, please send the following details: @@ -766,7 +766,7 @@ Within your message, please send the following details: - Brief description - Preferably, a link that directs the readers to your project -# Tutorials about PyGAD +## Tutorials about PyGAD ## [Adaptive Mutation in Genetic Algorithm with Python Examples](https://neptune.ai/blog/adaptive-mutation-in-genetic-algorithm-with-python-examples) @@ -830,7 +830,7 @@ So, in this tutorial, we’ll explore how to use PyGAD to train PyTorch models. ## [A Guide to Genetic ‘Learning’ Algorithms for Optimization](https://towardsdatascience.com/a-guide-to-genetic-learning-algorithms-for-optimization-e1067cdc77e7) -# PyGAD in Other Languages +## PyGAD in Other Languages ## French @@ -912,7 +912,7 @@ PyGAD разрабатывали на Python 3.7.3. Зависимости вк [![](https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png)](https://neurohive.io/ru/frameworki/pygad-biblioteka-dlya-implementacii-geneticheskogo-algoritma) -# Research Papers using PyGAD +## Research Papers using PyGAD A number of research papers used PyGAD and here are some of them: @@ -932,13 +932,13 @@ A number of research papers used PyGAD and here are some of them: * Zhu, Mingda. *Genetic Algorithm-based Parameter Identification for Ship Manoeuvring Model under Wind Disturbance*. MS thesis. NTNU, 2021. * Abdalrahman, Ahmed, and Weihua Zhuang. "Dynamic pricing for differentiated pev charging services using deep reinforcement learning." *IEEE Transactions on Intelligent Transportation Systems* (2020). -# More Links +## More Links https://rodriguezanton.com/identifying-contact-states-for-2d-objects-using-pygad-and/ https://torvaney.github.io/projects/t9-optimised -# For More Information +## For More Information There are different resources that can be used to get started with the genetic algorithm and building it in Python. @@ -1021,7 +1021,7 @@ Find the book at these links: ![Fig04](https://user-images.githubusercontent.com/16560492/78830077-ae7c2800-79e7-11ea-980b-53b6bd879eeb.jpg) -# Contact Us +## Contact Us * E-mail: ahmed.f.gad@gmail.com * [LinkedIn](https://www.linkedin.com/in/ahmedfgad) diff --git a/docs/source/releases.rst b/docs/source/releases.rst deleted file mode 100644 index 4138d6f..0000000 --- a/docs/source/releases.rst +++ /dev/null @@ -1,2662 +0,0 @@ -Release History -=============== - -|image1| - -.. _pygad-1017: - -PyGAD 1.0.17 ------------- - -Release Date: 15 April 2020 - -1. The **pygad.GA** class accepts a new argument named ``fitness_func`` - which accepts a function to be used for calculating the fitness - values for the solutions. This allows the project to be customized to - any problem by building the right fitness function. - -.. _pygad-1020: - -PyGAD 1.0.20 -------------- - -Release Date: 4 May 2020 - -1. The **pygad.GA** attributes are moved from the class scope to the - instance scope. - -2. Raising an exception for incorrect values of the passed parameters. - -3. Two new parameters are added to the **pygad.GA** class constructor - (``init_range_low`` and ``init_range_high``) allowing the user to - customize the range from which the genes values in the initial - population are selected. - -4. The code object ``__code__`` of the passed fitness function is - checked to ensure it has the right number of parameters. - -.. _pygad-200: - -PyGAD 2.0.0 ------------- - -Release Date: 13 May 2020 - -1. The fitness function accepts a new argument named ``sol_idx`` - representing the index of the solution within the population. - -2. A new parameter to the **pygad.GA** class constructor named - ``initial_population`` is supported to allow the user to use a custom - initial population to be used by the genetic algorithm. If not None, - then the passed population will be used. If ``None``, then the - genetic algorithm will create the initial population using the - ``sol_per_pop`` and ``num_genes`` parameters. - -3. The parameters ``sol_per_pop`` and ``num_genes`` are optional and set - to ``None`` by default. - -4. A new parameter named ``callback_generation`` is introduced in the - **pygad.GA** class constructor. It accepts a function with a single - parameter representing the **pygad.GA** class instance. This function - is called after each generation. This helps the user to do - post-processing or debugging operations after each generation. - -.. _pygad-210: - -PyGAD 2.1.0 ------------ - -Release Date: 14 May 2020 - -1. The ``best_solution()`` method in the **pygad.GA** class returns a - new output representing the index of the best solution within the - population. Now, it returns a total of 3 outputs and their order is: - best solution, best solution fitness, and best solution index. Here - is an example: - -.. code:: python - - solution, solution_fitness, solution_idx = ga_instance.best_solution() - print("Parameters of the best solution :", solution) - print("Fitness value of the best solution :", solution_fitness, "\n") - print("Index of the best solution :", solution_idx, "\n") - -1. | A new attribute named ``best_solution_generation`` is added to the - instances of the **pygad.GA** class. it holds the generation number - at which the best solution is reached. It is only assigned the - generation number after the ``run()`` method completes. Otherwise, - its value is -1. - | Example: - -.. code:: python - - print("Best solution reached after {best_solution_generation} generations.".format(best_solution_generation=ga_instance.best_solution_generation)) - -1. The ``best_solution_fitness`` attribute is renamed to - ``best_solutions_fitness`` (plural solution). - -2. Mutation is applied independently for the genes. - -.. _pygad-221: - -PyGAD 2.2.1 ------------ - -Release Date: 17 May 2020 - -1. Adding 2 extra modules (pygad.nn and pygad.gann) for building and - training neural networks with the genetic algorithm. - -.. _pygad-222: - -PyGAD 2.2.2 ------------ - -Release Date: 18 May 2020 - -1. The initial value of the ``generations_completed`` attribute of - instances from the pygad.GA class is ``0`` rather than ``None``. - -2. An optional bool parameter named ``mutation_by_replacement`` is added - to the constructor of the pygad.GA class. It works only when the - selected type of mutation is random (``mutation_type="random"``). In - this case, setting ``mutation_by_replacement=True`` means replace the - gene by the randomly generated value. If ``False``, then it has no - effect and random mutation works by adding the random value to the - gene. This parameter should be used when the gene falls within a - fixed range and its value must not go out of this range. Here are - some examples: - -Assume there is a gene with the value 0.5. - -If ``mutation_type="random"`` and ``mutation_by_replacement=False``, -then the generated random value (e.g. 0.1) will be added to the gene -value. The new gene value is **0.5+0.1=0.6**. - -If ``mutation_type="random"`` and ``mutation_by_replacement=True``, then -the generated random value (e.g. 0.1) will replace the gene value. The -new gene value is **0.1**. - -1. ``None`` value could be assigned to the ``mutation_type`` and - ``crossover_type`` parameters of the pygad.GA class constructor. When - ``None``, this means the step is bypassed and has no action. - -.. _pygad-230: - -PyGAD 2.3.0 ------------ - -Release date: 1 June 2020 - -1. A new module named ``pygad.cnn`` is supported for building - convolutional neural networks. - -2. A new module named ``pygad.gacnn`` is supported for training - convolutional neural networks using the genetic algorithm. - -3. The ``pygad.plot_result()`` method has 3 optional parameters named - ``title``, ``xlabel``, and ``ylabel`` to customize the plot title, - x-axis label, and y-axis label, respectively. - -4. The ``pygad.nn`` module supports the softmax activation function. - -5. The name of the ``pygad.nn.predict_outputs()`` function is changed to - ``pygad.nn.predict()``. - -6. The name of the ``pygad.nn.train_network()`` function is changed to - ``pygad.nn.train()``. - -.. _pygad-240: - -PyGAD 2.4.0 ------------ - -Release date: 5 July 2020 - -1. A new parameter named ``delay_after_gen`` is added which accepts a - non-negative number specifying the time in seconds to wait after a - generation completes and before going to the next generation. It - defaults to ``0.0`` which means no delay after the generation. - -2. The passed function to the ``callback_generation`` parameter of the - pygad.GA class constructor can terminate the execution of the genetic - algorithm if it returns the string ``stop``. This causes the - ``run()`` method to stop. - -One important use case for that feature is to stop the genetic algorithm -when a condition is met before passing though all the generations. The -user may assigned a value of 100 to the ``num_generations`` parameter of -the pygad.GA class constructor. Assuming that at generation 50, for -example, a condition is met and the user wants to stop the execution -before waiting the remaining 50 generations. To do that, just make the -function passed to the ``callback_generation`` parameter to return the -string ``stop``. - -Here is an example of a function to be passed to the -``callback_generation`` parameter which stops the execution if the -fitness value 70 is reached. The value 70 might be the best possible -fitness value. After being reached, then there is no need to pass -through more generations because no further improvement is possible. - -.. code:: python - - def func_generation(ga_instance): - if ga_instance.best_solution()[1] >= 70: - return "stop" - -.. _pygad-250: - -PyGAD 2.5.0 ------------ - -Release date: 19 July 2020 - -1. | 2 new optional parameters added to the constructor of the - ``pygad.GA`` class which are ``crossover_probability`` and - ``mutation_probability``. - | While applying the crossover operation, each parent has a random - value generated between 0.0 and 1.0. If this random value is less - than or equal to the value assigned to the - ``crossover_probability`` parameter, then the parent is selected - for the crossover operation. - | For the mutation operation, a random value between 0.0 and 1.0 is - generated for each gene in the solution. If this value is less than - or equal to the value assigned to the ``mutation_probability``, - then this gene is selected for mutation. - -2. A new optional parameter named ``linewidth`` is added to the - ``plot_result()`` method to specify the width of the curve in the - plot. It defaults to 3.0. - -3. Previously, the indices of the genes selected for mutation was - randomly generated once for all solutions within the generation. - Currently, the genes' indices are randomly generated for each - solution in the population. If the population has 4 solutions, the - indices are randomly generated 4 times inside the single generation, - 1 time for each solution. - -4. Previously, the position of the point(s) for the single-point and - two-points crossover was(were) randomly selected once for all - solutions within the generation. Currently, the position(s) is(are) - randomly selected for each solution in the population. If the - population has 4 solutions, the position(s) is(are) randomly - generated 4 times inside the single generation, 1 time for each - solution. - -5. A new optional parameter named ``gene_space`` as added to the - ``pygad.GA`` class constructor. It is used to specify the possible - values for each gene in case the user wants to restrict the gene - values. It is useful if the gene space is restricted to a certain - range or to discrete values. For more information, check the `More - about the ``gene_space`` - Parameter `__ - section. Thanks to `Prof. Tamer A. - Farrag `__ for requesting this useful - feature. - -.. _pygad-260: - -PyGAD 2.6.0 ------------- - -Release Date: 6 August 2020 - -1. A bug fix in assigning the value to the ``initial_population`` - parameter. - -2. A new parameter named ``gene_type`` is added to control the gene - type. It can be either ``int`` or ``float``. It has an effect only - when the parameter ``gene_space`` is ``None``. - -3. 7 new parameters that accept callback functions: ``on_start``, - ``on_fitness``, ``on_parents``, ``on_crossover``, ``on_mutation``, - ``on_generation``, and ``on_stop``. - -.. _pygad-270: - -PyGAD 2.7.0 ------------ - -Release Date: 11 September 2020 - -1. The ``learning_rate`` parameter in the ``pygad.nn.train()`` function - defaults to **0.01**. - -2. Added support of building neural networks for regression using the - new parameter named ``problem_type``. It is added as a parameter to - both ``pygad.nn.train()`` and ``pygad.nn.predict()`` functions. The - value of this parameter can be either **classification** or - **regression** to define the problem type. It defaults to - **classification**. - -3. The activation function for a layer can be set to the string - ``"None"`` to refer that there is no activation function at this - layer. As a result, the supported values for the activation function - are ``"sigmoid"``, ``"relu"``, ``"softmax"``, and ``"None"``. - -To build a regression network using the ``pygad.nn`` module, just do the -following: - -1. Set the ``problem_type`` parameter in the ``pygad.nn.train()`` and - ``pygad.nn.predict()`` functions to the string ``"regression"``. - -2. Set the activation function for the output layer to the string - ``"None"``. This sets no limits on the range of the outputs as it - will be from ``-infinity`` to ``+infinity``. If you are sure that all - outputs will be nonnegative values, then use the ReLU function. - -Check the documentation of the ``pygad.nn`` module for an example that -builds a neural network for regression. The regression example is also -available at `this GitHub -project `__: -https://github.com/ahmedfgad/NumPyANN - -To build and train a regression network using the ``pygad.gann`` module, -do the following: - -1. Set the ``problem_type`` parameter in the ``pygad.nn.train()`` and - ``pygad.nn.predict()`` functions to the string ``"regression"``. - -2. Set the ``output_activation`` parameter in the constructor of the - ``pygad.gann.GANN`` class to ``"None"``. - -Check the documentation of the ``pygad.gann`` module for an example that -builds and trains a neural network for regression. The regression -example is also available at `this GitHub -project `__: -https://github.com/ahmedfgad/NeuralGenetic - -To build a classification network, either ignore the ``problem_type`` -parameter or set it to ``"classification"`` (default value). In this -case, the activation function of the last layer can be set to any type -(e.g. softmax). - -.. _pygad-271: - -PyGAD 2.7.1 ------------ - -Release Date: 11 September 2020 - -1. A bug fix when the ``problem_type`` argument is set to - ``regression``. - -.. _pygad-272: - -PyGAD 2.7.2 ------------ - -Release Date: 14 September 2020 - -1. Bug fix to support building and training regression neural networks - with multiple outputs. - -.. _pygad-280: - -PyGAD 2.8.0 ------------ - -Release Date: 20 September 2020 - -1. Support of a new module named ``kerasga`` so that the Keras models - can be trained by the genetic algorithm using PyGAD. - -.. _pygad-281: - -PyGAD 2.8.1 ------------ - -Release Date: 3 October 2020 - -1. Bug fix in applying the crossover operation when the - ``crossover_probability`` parameter is used. Thanks to `Eng. Hamada - Kassem, Research and Teaching Assistant, Construction Engineering and - Management, Faculty of Engineering, Alexandria University, - Egypt `__. - -.. _pygad-290: - -PyGAD 2.9.0 ------------- - -Release Date: 06 December 2020 - -1. The fitness values of the initial population are considered in the - ``best_solutions_fitness`` attribute. - -2. An optional parameter named ``save_best_solutions`` is added. It - defaults to ``False``. When it is ``True``, then the best solution - after each generation is saved into an attribute named - ``best_solutions``. If ``False``, then no solutions are saved and the - ``best_solutions`` attribute will be empty. - -3. Scattered crossover is supported. To use it, assign the - ``crossover_type`` parameter the value ``"scattered"``. - -4. NumPy arrays are now supported by the ``gene_space`` parameter. - -5. The following parameters (``gene_type``, ``crossover_probability``, - ``mutation_probability``, ``delay_after_gen``) can be assigned to a - numeric value of any of these data types: ``int``, ``float``, - ``numpy.int``, ``numpy.int8``, ``numpy.int16``, ``numpy.int32``, - ``numpy.int64``, ``numpy.float``, ``numpy.float16``, - ``numpy.float32``, or ``numpy.float64``. - -.. _pygad-2100: - -PyGAD 2.10.0 ------------- - -Release Date: 03 January 2021 - -1. Support of a new module ``pygad.torchga`` to train PyTorch models - using PyGAD. Check `its - documentation `__. - -2. Support of adaptive mutation where the mutation rate is determined - by the fitness value of each solution. Read the `Adaptive - Mutation `__ - section for more details. Also, read this paper: `Libelli, S. - Marsili, and P. Alba. "Adaptive mutation in genetic algorithms." - Soft computing 4.2 (2000): - 76-80. `__ - -3. Before the ``run()`` method completes or exits, the fitness value of - the best solution in the current population is appended to the - ``best_solution_fitness`` list attribute. Note that the fitness - value of the best solution in the initial population is already - saved at the beginning of the list. So, the fitness value of the - best solution is saved before the genetic algorithm starts and after - it ends. - -4. When the parameter ``parent_selection_type`` is set to ``sss`` - (steady-state selection), then a warning message is printed if the - value of the ``keep_parents`` parameter is set to 0. - -5. More validations to the user input parameters. - -6. The default value of the ``mutation_percent_genes`` is set to the - string ``"default"`` rather than the integer 10. This change helps - to know whether the user explicitly passed a value to the - ``mutation_percent_genes`` parameter or it is left to its default - one. The ``"default"`` value is later translated into the integer - 10. - -7. The ``mutation_percent_genes`` parameter is no longer accepting the - value 0. It must be ``>0`` and ``<=100``. - -8. The built-in ``warnings`` module is used to show warning messages - rather than just using the ``print()`` function. - -9. A new ``bool`` parameter called ``suppress_warnings`` is added to - the constructor of the ``pygad.GA`` class. It allows the user to - control whether the warning messages are printed or not. It defaults - to ``False`` which means the messages are printed. - -10. A helper method called ``adaptive_mutation_population_fitness()`` is - created to calculate the average fitness value used in adaptive - mutation to filter the solutions. - -11. The ``best_solution()`` method accepts a new optional parameter - called ``pop_fitness``. It accepts a list of the fitness values of - the solutions in the population. If ``None``, then the - ``cal_pop_fitness()`` method is called to calculate the fitness - values of the population. - -.. _pygad-2101: - -PyGAD 2.10.1 ------------- - -Release Date: 10 January 2021 - -1. In the ``gene_space`` parameter, any ``None`` value (regardless of - its index or axis), is replaced by a randomly generated number based - on the 3 parameters ``init_range_low``, ``init_range_high``, and - ``gene_type``. So, the ``None`` value in ``[..., None, ...]`` or - ``[..., [..., None, ...], ...]`` are replaced with random values. - This gives more freedom in building the space of values for the - genes. - -2. All the numbers passed to the ``gene_space`` parameter are casted to - the type specified in the ``gene_type`` parameter. - -3. The ``numpy.uint`` data type is supported for the parameters that - accept integer values. - -4. In the ``pygad.kerasga`` module, the ``model_weights_as_vector()`` - function uses the ``trainable`` attribute of the model's layers to - only return the trainable weights in the network. So, only the - trainable layers with their ``trainable`` attribute set to ``True`` - (``trainable=True``), which is the default value, have their weights - evolved. All non-trainable layers with the ``trainable`` attribute - set to ``False`` (``trainable=False``) will not be evolved. Thanks to - `Prof. Tamer A. Farrag `__ for - pointing about that at - `GitHub `__. - -.. _pygad-2102: - -PyGAD 2.10.2 ------------- - -Release Date: 15 January 2021 - -1. A bug fix when ``save_best_solutions=True``. Refer to this issue for - more information: - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/25 - -.. _pygad-2110: - -PyGAD 2.11.0 ------------- - -Release Date: 16 February 2021 - -1. In the ``gene_space`` argument, the user can use a dictionary to - specify the lower and upper limits of the gene. This dictionary must - have only 2 items with keys ``low`` and ``high`` to specify the low - and high limits of the gene, respectively. This way, PyGAD takes care - of not exceeding the value limits of the gene. For a problem with - only 2 genes, then using - ``gene_space=[{'low': 1, 'high': 5}, {'low': 0.2, 'high': 0.81}]`` - means the accepted values in the first gene start from 1 (inclusive) - to 5 (exclusive) while the second one has values between 0.2 - (inclusive) and 0.85 (exclusive). For more information, please check - the `Limit the Gene Value - Range `__ - section of the documentation. - -2. The ``plot_result()`` method returns the figure so that the user can - save it. - -3. Bug fixes in copying elements from the gene space. - -4. For a gene with a set of discrete values (more than 1 value) in the - ``gene_space`` parameter like ``[0, 1]``, it was possible that the - gene value may not change after mutation. That is if the current - value is 0, then the randomly selected value could also be 0. Now, it - is verified that the new value is changed. So, if the current value - is 0, then the new value after mutation will not be 0 but 1. - -.. _pygad-2120: - -PyGAD 2.12.0 ------------- - -Release Date: 20 February 2021 - -1. 4 new instance attributes are added to hold temporary results after - each generation: ``last_generation_fitness`` holds the fitness values - of the solutions in the last generation, ``last_generation_parents`` - holds the parents selected from the last generation, - ``last_generation_offspring_crossover`` holds the offspring generated - after applying the crossover in the last generation, and - ``last_generation_offspring_mutation`` holds the offspring generated - after applying the mutation in the last generation. You can access - these attributes inside the ``on_generation()`` method for example. - -2. A bug fixed when the ``initial_population`` parameter is used. The - bug occurred due to a mismatch between the data type of the array - assigned to ``initial_population`` and the gene type in the - ``gene_type`` attribute. Assuming that the array assigned to the - ``initial_population`` parameter is - ``((1, 1), (3, 3), (5, 5), (7, 7))`` which has type ``int``. When - ``gene_type`` is set to ``float``, then the genes will not be float - but casted to ``int`` because the defined array has ``int`` type. The - bug is fixed by forcing the array assigned to ``initial_population`` - to have the data type in the ``gene_type`` attribute. Check the - `issue at - GitHub `__: - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/27 - -Thanks to Andrei Rozanski [PhD Bioinformatics Specialist, Department of -Tissue Dynamics and Regeneration, Max Planck Institute for Biophysical -Chemistry, Germany] for opening my eye to the first change. - -Thanks to `Marios -Giouvanakis `__, -a PhD candidate in Electrical & Computer Engineer, `Aristotle University -of Thessaloniki (Αριστοτέλειο Πανεπιστήμιο Θεσσαλονίκης), -Greece `__, for emailing me about the second -issue. - -.. _pygad-2130: - -PyGAD 2.13.0 -------------- - -Release Date: 12 March 2021 - -1. A new ``bool`` parameter called ``allow_duplicate_genes`` is - supported. If ``True``, which is the default, then a - solution/chromosome may have duplicate gene values. If ``False``, - then each gene will have a unique value in its solution. Check the - `Prevent Duplicates in Gene - Values `__ - section for more details. - -2. The ``last_generation_fitness`` is updated at the end of each - generation not at the beginning. This keeps the fitness values of the - most up-to-date population assigned to the - ``last_generation_fitness`` parameter. - -.. _pygad-2140: - -PyGAD 2.14.0 ------------- - -PyGAD 2.14.0 has an issue that is solved in PyGAD 2.14.1. Please -consider using 2.14.1 not 2.14.0. - -Release Date: 19 May 2021 - -1. `Issue - #40 `__ - is solved. Now, the ``None`` value works with the ``crossover_type`` - and ``mutation_type`` parameters: - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/40 - -2. The ``gene_type`` parameter supports accepting a - ``list/tuple/numpy.ndarray`` of numeric data types for the genes. - This helps to control the data type of each individual gene. - Previously, the ``gene_type`` can be assigned only to a single data - type that is applied for all genes. For more information, check the - `More about the ``gene_type`` - Parameter `__ - section. Thanks to `Rainer - Engel `__ - for asking about this feature in `this - discussion `__: - https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/43 - -3. A new ``bool`` attribute named ``gene_type_single`` is added to the - ``pygad.GA`` class. It is ``True`` when there is a single data type - assigned to the ``gene_type`` parameter. When the ``gene_type`` - parameter is assigned a ``list/tuple/numpy.ndarray``, then - ``gene_type_single`` is set to ``False``. - -4. The ``mutation_by_replacement`` flag now has no effect if - ``gene_space`` exists except for the genes with ``None`` values. For - example, for ``gene_space=[None, [5, 6]]`` the - ``mutation_by_replacement`` flag affects only the first gene which - has ``None`` for its value space. - -5. When an element has a value of ``None`` in the ``gene_space`` - parameter (e.g. ``gene_space=[None, [5, 6]]``), then its value will - be randomly generated for each solution rather than being generate - once for all solutions. Previously, the gene with ``None`` value in - ``gene_space`` is the same across all solutions - -6. Some changes in the documentation according to `issue - #32 `__: - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/32 - -.. _pygad-2142: - -PyGAD 2.14.2 ------------- - -Release Date: 27 May 2021 - -1. Some bug fixes when the ``gene_type`` parameter is nested. Thanks to - `Rainer - Engel `__ - for opening `a - discussion `__ - to report this bug: - https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/43#discussioncomment-763342 - -`Rainer -Engel `__ -helped a lot in suggesting new features and suggesting enhancements in -2.14.0 to 2.14.2 releases. - -.. _pygad-2143: - -PyGAD 2.14.3 ------------- - -Release Date: 6 June 2021 - -1. Some bug fixes when setting the ``save_best_solutions`` parameter to - ``True``. Previously, the best solution for generation ``i`` was - added into the ``best_solutions`` attribute at generation ``i+1``. - Now, the ``best_solutions`` attribute is updated by each best - solution at its exact generation. - -.. _pygad-2150: - -PyGAD 2.15.0 ------------- - -Release Date: 17 June 2021 - -1. Control the precision of all genes/individual genes. Thanks to - `Rainer `__ for asking about this - feature: - https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/43#discussioncomment-763452 - -2. A new attribute named ``last_generation_parents_indices`` holds the - indices of the selected parents in the last generation. - -3. In adaptive mutation, no need to recalculate the fitness values of - the parents selected in the last generation as these values can be - returned based on the ``last_generation_fitness`` and - ``last_generation_parents_indices`` attributes. This speeds-up the - adaptive mutation. - -4. When a sublist has a value of ``None`` in the ``gene_space`` - parameter (e.g. ``gene_space=[[1, 2, 3], [5, 6, None]]``), then its - value will be randomly generated for each solution rather than being - generated once for all solutions. Previously, a value of ``None`` in - a sublist of the ``gene_space`` parameter was identical across all - solutions. - -5. The dictionary assigned to the ``gene_space`` parameter itself or - one of its elements has a new key called ``"step"`` to specify the - step of moving from the start to the end of the range specified by - the 2 existing keys ``"low"`` and ``"high"``. An example is - ``{"low": 0, "high": 30, "step": 2}`` to have only even values for - the gene(s) starting from 0 to 30. For more information, check the - `More about the ``gene_space`` - Parameter `__ - section. - https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/48 - -6. A new function called ``predict()`` is added in both the - ``pygad.kerasga`` and ``pygad.torchga`` modules to make predictions. - This makes it easier than using custom code each time a prediction - is to be made. - -7. A new parameter called ``stop_criteria`` allows the user to specify - one or more stop criteria to stop the evolution based on some - conditions. Each criterion is passed as ``str`` which has a stop - word. The current 2 supported words are ``reach`` and ``saturate``. - ``reach`` stops the ``run()`` method if the fitness value is equal - to or greater than a given fitness value. An example for ``reach`` - is ``"reach_40"`` which stops the evolution if the fitness is >= 40. - ``saturate`` means stop the evolution if the fitness saturates for a - given number of consecutive generations. An example for ``saturate`` - is ``"saturate_7"`` which means stop the ``run()`` method if the - fitness does not change for 7 consecutive generations. Thanks to - `Rainer `__ for asking about this - feature: - https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/44 - -8. A new bool parameter, defaults to ``False``, named - ``save_solutions`` is added to the constructor of the ``pygad.GA`` - class. If ``True``, then all solutions in each generation are - appended into an attribute called ``solutions`` which is NumPy - array. - -9. The ``plot_result()`` method is renamed to ``plot_fitness()``. The - users should migrate to the new name as the old name will be removed - in the future. - -10. Four new optional parameters are added to the ``plot_fitness()`` - function in the ``pygad.GA`` class which are ``font_size=14``, - ``save_dir=None``, ``color="#3870FF"``, and ``plot_type="plot"``. - Use ``font_size`` to change the font of the plot title and labels. - ``save_dir`` accepts the directory to which the figure is saved. It - defaults to ``None`` which means do not save the figure. ``color`` - changes the color of the plot. ``plot_type`` changes the plot type - which can be either ``"plot"`` (default), ``"scatter"``, or - ``"bar"``. - https://github.com/ahmedfgad/GeneticAlgorithmPython/pull/47 - -11. The default value of the ``title`` parameter in the - ``plot_fitness()`` method is ``"PyGAD - Generation vs. Fitness"`` - rather than ``"PyGAD - Iteration vs. Fitness"``. - -12. A new method named ``plot_new_solution_rate()`` creates, shows, and - returns a figure showing the rate of new/unique solutions explored - in each generation. It accepts the same parameters as in the - ``plot_fitness()`` method. This method only works when - ``save_solutions=True`` in the ``pygad.GA`` class's constructor. - -13. A new method named ``plot_genes()`` creates, shows, and returns a - figure to show how each gene changes per each generation. It accepts - similar parameters like the ``plot_fitness()`` method in addition to - the ``graph_type``, ``fill_color``, and ``solutions`` parameters. - The ``graph_type`` parameter can be either ``"plot"`` (default), - ``"boxplot"``, or ``"histogram"``. ``fill_color`` accepts the fill - color which works when ``graph_type`` is either ``"boxplot"`` or - ``"histogram"``. ``solutions`` can be either ``"all"`` or ``"best"`` - to decide whether all solutions or only best solutions are used. - -14. The ``gene_type`` parameter now supports controlling the precision - of ``float`` data types. For a gene, rather than assigning just the - data type like ``float``, assign a - ``list``/``tuple``/``numpy.ndarray`` with 2 elements where the first - one is the type and the second one is the precision. For example, - ``[float, 2]`` forces a gene with a value like ``0.1234`` to be - ``0.12``. For more information, check the `More about the - ``gene_type`` - Parameter `__ - section. - -.. _pygad-2151: - -PyGAD 2.15.1 ------------- - -Release Date: 18 June 2021 - -1. Fix a bug when ``keep_parents`` is set to a positive integer. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/49 - -.. _pygad-2152: - -PyGAD 2.15.2 ------------- - -Release Date: 18 June 2021 - -1. Fix a bug when using the ``kerasga`` or ``torchga`` modules. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/51 - -.. _pygad-2160: - -PyGAD 2.16.0 ------------- - -Release Date: 19 June 2021 - -1. A user-defined function can be passed to the ``mutation_type``, - ``crossover_type``, and ``parent_selection_type`` parameters in the - ``pygad.GA`` class to create a custom mutation, crossover, and parent - selection operators. Check the `User-Defined Crossover, Mutation, and - Parent Selection - Operators `__ - section for more details. - https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/50 - -.. _pygad-2161: - -PyGAD 2.16.1 ------------- - -Release Date: 28 September 2021 - -1. The user can use the ``tqdm`` library to show a progress bar. - https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/50. - -.. code:: python - - import pygad - import numpy - import tqdm - - equation_inputs = [4,-2,3.5] - desired_output = 44 - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - - num_generations = 10000 - with tqdm.tqdm(total=num_generations) as pbar: - ga_instance = pygad.GA(num_generations=num_generations, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - on_generation=lambda _: pbar.update(1)) - - ga_instance.run() - - ga_instance.plot_result() - -But this work does not work if the ``ga_instance`` will be pickled (i.e. -the ``save()`` method will be called. - -.. code:: python - - ga_instance.save("test") - -To solve this issue, define a function and pass it to the -``on_generation`` parameter. In the next code, the -``on_generation_progress()`` function is defined which updates the -progress bar. - -.. code:: python - - import pygad - import numpy - import tqdm - - equation_inputs = [4,-2,3.5] - desired_output = 44 - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - - def on_generation_progress(ga): - pbar.update(1) - - num_generations = 100 - with tqdm.tqdm(total=num_generations) as pbar: - ga_instance = pygad.GA(num_generations=num_generations, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - on_generation=on_generation_progress) - - ga_instance.run() - - ga_instance.plot_result() - - ga_instance.save("test") - -1. Solved the issue of unequal length between the ``solutions`` and - ``solutions_fitness`` when the ``save_solutions`` parameter is set to - ``True``. Now, the fitness of the last population is appended to the - ``solutions_fitness`` array. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/64 - -2. There was an issue of getting the length of these 4 variables - (``solutions``, ``solutions_fitness``, ``best_solutions``, and - ``best_solutions_fitness``) doubled after each call of the ``run()`` - method. This is solved by resetting these variables at the beginning - of the ``run()`` method. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/62 - -3. Bug fixes when adaptive mutation is used - (``mutation_type="adaptive"``). - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/65 - -.. _pygad-2162: - -PyGAD 2.16.2 ------------- - -Release Date: 2 February 2022 - -1. A new instance attribute called ``previous_generation_fitness`` added - in the ``pygad.GA`` class. It holds the fitness values of one - generation before the fitness values saved in the - ``last_generation_fitness``. - -2. Issue in the ``cal_pop_fitness()`` method in getting the correct - indices of the previous parents. This is solved by using the previous - generation's fitness saved in the new attribute - ``previous_generation_fitness`` to return the parents' fitness - values. Thanks to Tobias Tischhauser (M.Sc. - `Mitarbeiter Institut - EMS, Departement Technik, OST – Ostschweizer Fachhochschule, - Switzerland `__) - for detecting this bug. - -.. _pygad-2163: - -PyGAD 2.16.3 ------------- - -Release Date: 2 February 2022 - -1. Validate the fitness value returned from the fitness function. An - exception is raised if something is wrong. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/67 - -.. _pygad-2170: - -PyGAD 2.17.0 ------------- - -Release Date: 8 July 2022 - -1. An issue is solved when the ``gene_space`` parameter is given a fixed - value. e.g. gene_space=[range(5), 4]. The second gene's value is - static (4) which causes an exception. - -2. Fixed the issue where the ``allow_duplicate_genes`` parameter did not - work when mutation is disabled (i.e. ``mutation_type=None``). This is - by checking for duplicates after crossover directly. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/39 - -3. Solve an issue in the ``tournament_selection()`` method as the - indices of the selected parents were incorrect. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/89 - -4. Reuse the fitness values of the previously explored solutions rather - than recalculating them. This feature only works if - ``save_solutions=True``. - -5. Parallel processing is supported. This is by the introduction of a - new parameter named ``parallel_processing`` in the constructor of the - ``pygad.GA`` class. Thanks to - `@windowshopr `__ for opening the - issue - `#78 `__ - at GitHub. Check the `Parallel Processing in - PyGAD `__ - section for more information and examples. - -.. _pygad-2180: - -PyGAD 2.18.0 ------------- - -Release Date: 9 September 2022 - -1. Raise an exception if the sum of fitness values is zero while either - roulette wheel or stochastic universal parent selection is used. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/129 - -2. Initialize the value of the ``run_completed`` property to ``False``. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/122 - -3. The values of these properties are no longer reset with each call to - the ``run()`` method - ``self.best_solutions, self.best_solutions_fitness, self.solutions, self.solutions_fitness``: - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/123. Now, - the user can have the flexibility of calling the ``run()`` method - more than once while extending the data collected after each - generation. Another advantage happens when the instance is loaded and - the ``run()`` method is called, as the old fitness value are shown on - the graph alongside with the new fitness values. Read more in this - section: `Continue without Losing - Progress `__ - -4. Thanks `Prof. Fernando Jiménez - Barrionuevo `__ (Dept. of Information and - Communications Engineering, University of Murcia, Murcia, Spain) for - editing this - `comment `__ - in the code. - https://github.com/ahmedfgad/GeneticAlgorithmPython/commit/5315bbec02777df96ce1ec665c94dece81c440f4 - -5. A bug fixed when ``crossover_type=None``. - -6. Support of elitism selection through a new parameter named - ``keep_elitism``. It defaults to 1 which means for each generation - keep only the best solution in the next generation. If assigned 0, - then it has no effect. Read more in this section: `Elitism - Selection `__. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/74 - -7. A new instance attribute named ``last_generation_elitism`` added to - hold the elitism in the last generation. - -8. A new parameter called ``random_seed`` added to accept a seed for the - random function generators. Credit to this issue - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/70 and - `Prof. Fernando Jiménez Barrionuevo `__. - Read more in this section: `Random - Seed `__. - -9. Editing the ``pygad.TorchGA`` module to make sure the tensor data is - moved from GPU to CPU. Thanks to Rasmus Johansson for opening this - pull request: https://github.com/ahmedfgad/TorchGA/pull/2 - -.. _pygad-2181: - -PyGAD 2.18.1 ------------- - -Release Date: 19 September 2022 - -1. A big fix when ``keep_elitism`` is used. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/132 - -.. _pygad-2182: - -PyGAD 2.18.2 ------------- - -Release Date: 14 February 2023 - -1. Remove ``numpy.int`` and ``numpy.float`` from the list of supported - data types. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/151 - https://github.com/ahmedfgad/GeneticAlgorithmPython/pull/152 - -2. Call the ``on_crossover()`` callback function even if - ``crossover_type`` is ``None``. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/138 - -3. Call the ``on_mutation()`` callback function even if - ``mutation_type`` is ``None``. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/138 - -.. _pygad-2183: - -PyGAD 2.18.3 ------------- - -Release Date: 14 February 2023 - -1. Bug fixes. - -.. _pygad-2190: - -PyGAD 2.19.0 ------------- - -Release Date: 22 February 2023 - -1. A new ``summary()`` method is supported to return a Keras-like - summary of the PyGAD lifecycle. - -2. A new optional parameter called ``fitness_batch_size`` is supported - to calculate the fitness in batches. If it is assigned the value - ``1`` or ``None`` (default), then the normal flow is used where the - fitness function is called for each individual solution. If the - ``fitness_batch_size`` parameter is assigned a value satisfying this - condition ``1 < fitness_batch_size <= sol_per_pop``, then the - solutions are grouped into batches of size ``fitness_batch_size`` - and the fitness function is called once for each batch. In this - case, the fitness function must return a list/tuple/numpy.ndarray - with a length equal to the number of solutions passed. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/136. - -3. The ``cloudpickle`` library - (https://github.com/cloudpipe/cloudpickle) is used instead of the - ``pickle`` library to pickle the ``pygad.GA`` objects. This solves - the issue of having to redefine the functions (e.g. fitness - function). The ``cloudpickle`` library is added as a dependency in - the ``requirements.txt`` file. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/159 - -4. Support of assigning methods to these parameters: ``fitness_func``, - ``crossover_type``, ``mutation_type``, ``parent_selection_type``, - ``on_start``, ``on_fitness``, ``on_parents``, ``on_crossover``, - ``on_mutation``, ``on_generation``, and ``on_stop``. - https://github.com/ahmedfgad/GeneticAlgorithmPython/pull/92 - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/138 - -5. Validating the output of the parent selection, crossover, and - mutation functions. - -6. The built-in parent selection operators return the parent's indices - as a NumPy array. - -7. The outputs of the parent selection, crossover, and mutation - operators must be NumPy arrays. - -8. Fix an issue when ``allow_duplicate_genes=True``. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/39 - -9. Fix an issue creating scatter plots of the solutions' fitness. - -10. Sampling from a ``set()`` is no longer supported in Python 3.11. - Instead, sampling happens from a ``list()``. Thanks ``Marco Brenna`` - for pointing to this issue. - -11. The lifecycle is updated to reflect that the new population's - fitness is calculated at the end of the lifecycle not at the - beginning. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/154#issuecomment-1438739483 - -12. There was an issue when ``save_solutions=True`` that causes the - fitness function to be called for solutions already explored and - have their fitness pre-calculated. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/160 - -13. A new instance attribute named ``last_generation_elitism_indices`` - added to hold the indices of the selected elitism. This attribute - helps to re-use the fitness of the elitism instead of calling the - fitness function. - -14. Fewer calls to the ``best_solution()`` method which in turns saves - some calls to the fitness function. - -15. Some updates in the documentation to give more details about the - ``cal_pop_fitness()`` method. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/79#issuecomment-1439605442 - -.. _pygad-2191: - -PyGAD 2.19.1 ------------- - -Release Date: 22 February 2023 - -1. Add the `cloudpickle `__ - library as a dependency. - -.. _pygad-2192: - -PyGAD 2.19.2 ------------- - -Release Date 23 February 2023 - -1. Fix an issue when parallel processing was used where the elitism - solutions' fitness values are not re-used. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/160#issuecomment-1441718184 - -.. _pygad-300: - -PyGAD 3.0.0 ------------ - -Release Date 8 April 2023 - -1. The structure of the library is changed and some methods defined in - the ``pygad.py`` module are moved to the ``pygad.utils``, - ``pygad.helper``, and ``pygad.visualize`` submodules. - -2. The ``pygad.utils.parent_selection`` module has a class named - ``ParentSelection`` where all the parent selection operators exist. - The ``pygad.GA`` class extends this class. - -3. The ``pygad.utils.crossover`` module has a class named ``Crossover`` - where all the crossover operators exist. The ``pygad.GA`` class - extends this class. - -4. The ``pygad.utils.mutation`` module has a class named ``Mutation`` - where all the mutation operators exist. The ``pygad.GA`` class - extends this class. - -5. The ``pygad.helper.unique`` module has a class named ``Unique`` some - helper methods exist to solve duplicate genes and make sure every - gene is unique. The ``pygad.GA`` class extends this class. - -6. The ``pygad.visualize.plot`` module has a class named ``Plot`` where - all the methods that create plots exist. The ``pygad.GA`` class - extends this class. - -7. Support of using the ``logging`` module to log the outputs to both - the console and text file instead of using the ``print()`` function. - This is by assigning the ``logging.Logger`` to the new ``logger`` - parameter. Check the `Logging - Outputs `__ - for more information. - -8. A new instance attribute called ``logger`` to save the logger. - -9. The function/method passed to the ``fitness_func`` parameter accepts - a new parameter that refers to the instance of the ``pygad.GA`` - class. Check this for an example: `Use Functions and Methods to - Build Fitness Function and - Callbacks `__. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/163 - -10. Update the documentation to include an example of using functions - and methods to calculate the fitness and build callbacks. Check this - for more details: `Use Functions and Methods to Build Fitness - Function and - Callbacks `__. - https://github.com/ahmedfgad/GeneticAlgorithmPython/pull/92#issuecomment-1443635003 - -11. Validate the value passed to the ``initial_population`` parameter. - -12. Validate the type and length of the ``pop_fitness`` parameter of the - ``best_solution()`` method. - -13. Some edits in the documentation. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/106 - -14. Fix an issue when building the initial population as (some) genes - have their value taken from the mutation range (defined by the - parameters ``random_mutation_min_val`` and - ``random_mutation_max_val``) instead of using the parameters - ``init_range_low`` and ``init_range_high``. - -15. The ``summary()`` method returns the summary as a single-line - string. Just log/print the returned string it to see it properly. - -16. The ``callback_generation`` parameter is removed. Use the - ``on_generation`` parameter instead. - -17. There was an issue when using the ``parallel_processing`` parameter - with Keras and PyTorch. As Keras/PyTorch are not thread-safe, the - ``predict()`` method gives incorrect and weird results when more - than 1 thread is used. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/145 - https://github.com/ahmedfgad/TorchGA/issues/5 - https://github.com/ahmedfgad/KerasGA/issues/6. Thanks to this - `StackOverflow - answer `__. - -18. Replace ``numpy.float`` by ``float`` in the 2 parent selection - operators roulette wheel and stochastic universal. - https://github.com/ahmedfgad/GeneticAlgorithmPython/pull/168 - -.. _pygad-301: - -PyGAD 3.0.1 ------------ - -Release Date 20 April 2023 - -1. Fix an issue with passing user-defined function/method for parent - selection. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/179 - -.. _pygad-310: - -PyGAD 3.1.0 ------------ - -Release Date 20 June 2023 - -1. Fix a bug when the initial population has duplciate genes if a - nested gene space is used. - -2. The ``gene_space`` parameter can no longer be assigned a tuple. - -3. Fix a bug when the ``gene_space`` parameter has a member of type - ``tuple``. - -4. A new instance attribute called ``gene_space_unpacked`` which has - the unpacked ``gene_space``. It is used to solve duplicates. For - infinite ranges in the ``gene_space``, they are unpacked to a - limited number of values (e.g. 100). - -5. Bug fixes when creating the initial population using ``gene_space`` - attribute. - -6. When a ``dict`` is used with the ``gene_space`` attribute, the new - gene value was calculated by summing 2 values: 1) the value sampled - from the ``dict`` 2) a random value returned from the random - mutation range defined by the 2 parameters - ``random_mutation_min_val`` and ``random_mutation_max_val``. This - might cause the gene value to exceed the range limit defined in the - ``gene_space``. To respect the ``gene_space`` range, this release - only returns the value from the ``dict`` without summing it to a - random value. - -7. Formatting the strings using f-string instead of the ``format()`` - method. https://github.com/ahmedfgad/GeneticAlgorithmPython/pull/189 - -8. In the ``__init__()`` of the ``pygad.GA`` class, the logged error - messages are handled using a ``try-except`` block instead of - repeating the ``logger.error()`` command. - https://github.com/ahmedfgad/GeneticAlgorithmPython/pull/189 - -9. A new class named ``CustomLogger`` is created in the ``pygad.cnn`` - module to create a default logger using the ``logging`` module - assigned to the ``logger`` attribute. This class is extended in all - other classes in the module. The constructors of these classes have - a new parameter named ``logger`` which defaults to ``None``. If no - logger is passed, then the default logger in the ``CustomLogger`` - class is used. - -10. Except for the ``pygad.nn`` module, the ``print()`` function in all - other modules are replaced by the ``logging`` module to log - messages. - -11. The callback functions/methods ``on_fitness()``, ``on_parents()``, - ``on_crossover()``, and ``on_mutation()`` can return values. These - returned values override the corresponding properties. The output of - ``on_fitness()`` overrides the population fitness. The - ``on_parents()`` function/method must return 2 values representing - the parents and their indices. The output of ``on_crossover()`` - overrides the crossover offspring. The output of ``on_mutation()`` - overrides the mutation offspring. - -12. Fix a bug when adaptive mutation is used while - ``fitness_batch_size``>1. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/195 - -13. When ``allow_duplicate_genes=False`` and a user-defined - ``gene_space`` is used, it sometimes happen that there is no room to - solve the duplicates between the 2 genes by simply replacing the - value of one gene by another gene. This release tries to solve such - duplicates by looking for a third gene that will help in solving the - duplicates. Check `this - section `__ - for more information. - -14. Use probabilities to select parents using the rank parent selection - method. - https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/205 - -15. The 2 parameters ``random_mutation_min_val`` and - ``random_mutation_max_val`` can accept iterables - (list/tuple/numpy.ndarray) with length equal to the number of genes. - This enables customizing the mutation range for each individual - gene. - https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/198 - -16. The 2 parameters ``init_range_low`` and ``init_range_high`` can - accept iterables (list/tuple/numpy.ndarray) with length equal to the - number of genes. This enables customizing the initial range for each - individual gene when creating the initial population. - -17. The ``data`` parameter in the ``predict()`` function of the - ``pygad.kerasga`` module can be assigned a data generator. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/115 - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/207 - -18. The ``predict()`` function of the ``pygad.kerasga`` module accepts 3 - optional parameters: 1) ``batch_size=None``, ``verbose=0``, and - ``steps=None``. Check documentation of the `Keras - Model.predict() `__ - method for more information. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/207 - -19. The documentation is updated to explain how mutation works when - ``gene_space`` is used with ``int`` or ``float`` data types. Check - `this - section `__. - https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/198 - -.. _pygad-320: - -PyGAD 3.2.0 ------------ - -Release Date 7 September 2023 - -1. A new module ``pygad.utils.nsga2`` is created that has the ``NSGA2`` - class that includes the functionalities of NSGA-II. The class has - these methods: 1) ``get_non_dominated_set()`` 2) - ``non_dominated_sorting()`` 3) ``crowding_distance()`` 4) - ``sort_solutions_nsga2()``. Check `this - section `__ - for an example. - -2. Support of multi-objective optimization using Non-Dominated Sorting - Genetic Algorithm II (NSGA-II) using the ``NSGA2`` class in the - ``pygad.utils.nsga2`` module. Just return a ``list``, ``tuple``, or - ``numpy.ndarray`` from the fitness function and the library will - consider the problem as multi-objective optimization. All the - objectives are expected to be maximization. Check `this - section `__ - for an example. - -3. The parent selection methods and adaptive mutation are edited to - support multi-objective optimization. - -4. Two new NSGA-II parent selection methods are supported in the - ``pygad.utils.parent_selection`` module: 1) Tournament selection for - NSGA-II 2) NSGA-II selection. - -5. The ``plot_fitness()`` method in the ``pygad.plot`` module has a new - optional parameter named ``label`` to accept the label of the plots. - This is only used for multi-objective problems. Otherwise, it is - ignored. It defaults to ``None`` and accepts a ``list``, ``tuple``, - or ``numpy.ndarray``. The labels are used in a legend inside the - plot. - -6. The default color in the methods of the ``pygad.plot`` module is - changed to the greenish ``#64f20c`` color. - -7. A new instance attribute named ``pareto_fronts`` added to the - ``pygad.GA`` instances that holds the pareto fronts when solving a - multi-objective problem. - -8. The ``gene_type`` accepts a ``list``, ``tuple``, or - ``numpy.ndarray`` for integer data types given that the precision is - set to ``None`` (e.g. ``gene_type=[float, [int, None]]``). - -9. In the ``cal_pop_fitness()`` method, the fitness value is re-used if - ``save_best_solutions=True`` and the solution is found in the - ``best_solutions`` attribute. These parameters also can help - re-using the fitness of a solution instead of calling the fitness - function: ``keep_elitism``, ``keep_parents``, and - ``save_solutions``. - -10. The value ``99999999999`` is replaced by ``float('inf')`` in the 2 - methods ``wheel_cumulative_probs()`` and - ``stochastic_universal_selection()`` inside the - ``pygad.utils.parent_selection.ParentSelection`` class. - -11. The ``plot_result()`` method in the ``pygad.visualize.plot.Plot`` - class is removed. Instead, please use the ``plot_fitness()`` if you - did not upgrade yet. - -.. _pygad-330: - -PyGAD 3.3.0 ------------ - -Release Date 29 January 2024 - -1. Solve bugs when multi-objective optimization is used. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/238 - -2. When the ``stop_ciiteria`` parameter is used with the ``reach`` - keyword, then multiple numeric values can be passed when solving a - multi-objective problem. For example, if a problem has 3 objective - functions, then ``stop_criteria="reach_10_20_30"`` means the GA - stops if the fitness of the 3 objectives are at least 10, 20, and - 30, respectively. The number values must match the number of - objective functions. If a single value found (e.g. - ``stop_criteria=reach_5``) when solving a multi-objective problem, - then it is used across all the objectives. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/238 - -3. The ``delay_after_gen`` parameter is now deprecated and will be - removed in a future release. If it is necessary to have a time delay - after each generation, then assign a callback function/method to the - ``on_generation`` parameter to pause the evolution. - -4. Parallel processing now supports calculating the fitness during - adaptive mutation. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/201 - -5. The population size can be changed during runtime by changing all - the parameters that would affect the size of any thing used by the - GA. For more information, check the `Change Population Size during - Runtime `__ - section. - https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/234 - -6. When a dictionary exists in the ``gene_space`` parameter without a - step, then mutation occurs by adding a random value to the gene - value. The random vaue is generated based on the 2 parameters - ``random_mutation_min_val`` and ``random_mutation_max_val``. For - more information, check the `How Mutation Works with the gene_space - Parameter? `__ - section. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/229 - -7. Add ``object`` as a supported data type for int - (GA.supported_int_types) and float (GA.supported_float_types). - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/174 - -8. Use the ``raise`` clause instead of the ``sys.exit(-1)`` to - terminate the execution. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/213 - -9. Fix a bug when multi-objective optimization is used with batch - fitness calculation (e.g. ``fitness_batch_size`` set to a non-zero - number). - -10. Fix a bug in the ``pygad.py`` script when finding the index of the - best solution. It does not work properly with multi-objective - optimization where ``self.best_solutions_fitness`` have multiple - columns. - -.. code:: python - - self.best_solution_generation = numpy.where(numpy.array( - self.best_solutions_fitness) == numpy.max(numpy.array(self.best_solutions_fitness)))[0][0] - -.. _pygad-331: - -PyGAD 3.3.1 ------------ - -Release Date 17 February 2024 - -1. After the last generation and before the ``run()`` method completes, - update the 2 instance attributes: 1) ``last_generation_parents`` 2) - ``last_generation_parents_indices``. This is to keep the list of - parents up-to-date with the latest population fitness - ``last_generation_fitness``. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/275 - -2. 5 methods with names starting with ``run_``. Their purpose is to keep - the main loop inside the ``run()`` method clean. Check the `Other - Methods `__ - section for more information. - - 1. ``run_loop_head()``: The code before the loop starts. - - 2. ``run_select_parents()``: The parent selection-related code. - - 3. ``run_crossover()``: The crossover-related code. - - 4. ``run_mutation()``: The mutation-related code. - - 5. ``run_update_population()``: Update the ``population`` instance - attribute after completing the processes of crossover and - mutation. - -.. _pygad-340: - -PyGAD 3.4.0 ------------ - -Release Date 07 January 2025 - -1. The ``delay_after_gen`` parameter is removed from the ``pygad.GA`` - class constructor. As a result, it is no longer an attribute of the - ``pygad.GA`` class instances. To add a delay after each generation, - apply it inside the ``on_generation`` callback. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/283 - -2. In the ``single_point_crossover()`` method of the - ``pygad.utils.crossover.Crossover`` class, all the random crossover - points are returned before the ``for`` loop. This is by calling the - ``numpy.random.randint()`` function only once before the loop to - generate all the K points (where K is the offspring size). This is - compared to calling the ``numpy.random.randint()`` function inside - the ``for`` loop K times, once for each individual offspring. - -3. Bug fix in the ``examples/example_custom_operators.py`` script. - https://github.com/ahmedfgad/GeneticAlgorithmPython/pull/285 - -4. While making prediction using the ``pygad.torchga.predict()`` - function, no gradients are calculated. - -5. The ``gene_type`` parameter of the - ``pygad.helper.unique.Unique.unique_int_gene_from_range()`` method - accepts the type of the current gene only instead of the full - gene_type list. - -6. Created a new method called ``unique_float_gene_from_range()`` - inside the ``pygad.helper.unique.Unique`` class to find a unique - floating-point number from a range. - -7. Fix a bug in the - ``pygad.helper.unique.Unique.unique_gene_by_space()`` method to - return the numeric value only instead of a NumPy array. - -8. Refactoring the ``pygad/helper/unique.py`` script to remove - duplicate codes and reformatting the docstrings. - -9. The ``plot_pareto_front_curve()`` method added to the - pygad.visualize.plot.Plot class to visualize the Pareto front for - multi-objective problems. It only supports 2 objectives. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/279 - -10. Fix a bug converting a nested NumPy array to a nested list. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/300 - -11. The ``Matplotlib`` library is only imported when a method inside the - ``pygad/visualize/plot.py`` script is used. This is more efficient - than using ``import matplotlib.pyplot`` at the module level as this - causes it to be imported when ``pygad`` is imported even when it is - not needed. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/292 - -12. Fix a bug when minus sign (-) is used inside the ``stop_criteria`` - parameter (e.g. ``stop_criteria=["saturate_10", "reach_-0.5"]``). - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/296 - -13. Make sure ``self.best_solutions`` is a list of lists inside the - ``cal_pop_fitness`` method. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/293 - -14. Fix a bug where the ``cal_pop_fitness()`` method was using the - ``previous_generation_fitness`` attribute to return the parents - fitness. This instance attribute was not using the fitness of the - latest population, instead the fitness of the population before the - last one. The issue is solved by updating the - ``previous_generation_fitness`` attribute to the latest population - fitness before the GA completes. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/291 - -.. _pygad-350: - -PyGAD 3.5.0 ------------ - -Release Date 08 July 2025 - -1. Fix a bug when minus sign (-) is used inside the ``stop_criteria`` - parameter for multi-objective problems. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/314 - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/323 - -2. Fix a bug when the ``stop_criteria`` parameter is passed as an - iterable (e.g. list) for multi-objective problems (e.g. - ``['reach_50_60', 'reach_20, 40']``). - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/314 - -3. Call the ``get_matplotlib()`` function from the ``plot_genes()`` - method inside the ``pygad.visualize.plot.Plot`` class to import the - matplotlib library. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/315 - -4. Create a new helper method called ``select_unique_value()`` inside - the ``pygad/helper/unique.py`` script to select a unique gene from - an array of values. - -5. Create a new helper method called ``get_random_mutation_range()`` - inside the ``pygad/utils/mutation.py`` script that returns the - random mutation range (min and max) for a single gene by its index. - -6. Create a new helper method called - ``change_random_mutation_value_dtype`` inside the - ``pygad/utils/mutation.py`` script that changes the data type of the - value used to apply random mutation. - -7. Create a new helper method called ``round_random_mutation_value()`` - inside the ``pygad/utils/mutation.py`` script that rounds the value - used to apply random mutation. - -8. Create the ``pygad/helper/misc.py`` script with a class called - ``Helper`` that has the following helper methods: - - 1. ``change_population_dtype_and_round()``: For each gene in the - population, round the gene value and change the data type. - - 2. ``change_gene_dtype_and_round()``: Round the change the data - type of a single gene. - - 3. ``mutation_change_gene_dtype_and_round()``: Decides whether - mutation is done by replacement or not. Then it rounds and - change the data type of the new gene value. - - 4. ``validate_gene_constraint_callable_output()``: Validates the - output of the user-defined callable/function that checks whether - the gene constraint defined in the ``gene_constraint`` parameter - is satisfied or not. - - 5. ``get_gene_dtype()``: Returns the gene data type from the - ``gene_type`` instance attribute. - - 6. ``get_random_mutation_range()``: Returns the random mutation - range using the ``random_mutation_min_val`` and - ``random_mutation_min_val`` instance attributes. - - 7. ``get_initial_population_range()``: Returns the initial - population values range using the ``init_range_low`` and - ``init_range_high`` instance attributes. - - 8. ``generate_gene_value_from_space()``: Generates/selects a value - for a gene using the ``gene_space`` instance attribute. - - 9. ``generate_gene_value_randomly()``: Generates a random value for - the gene. Only used if ``gene_space`` is ``None``. - - 10. ``generate_gene_value()``: Generates a value for the gene. It - checks whether ``gene_space`` is ``None`` and calls either - ``generate_gene_value_randomly()`` or - ``generate_gene_value_from_space()``. - - 11. ``filter_gene_values_by_constraint()``: Receives a list of - values for a gene. Then it filters such values using the gene - constraint. - - 12. ``get_valid_gene_constraint_values()``: Selects one valid gene - value that satisfy the gene constraint. It simply calls - ``generate_gene_value()`` to generate some gene values then it - filters such values using - ``filter_gene_values_by_constraint()``. - -9. Create a new helper method called - ``mutation_process_random_value()`` inside the - ``pygad/utils/mutation.py`` script that generates constrained random - values for mutation. It calls either ``generate_gene_value()`` or - ``get_valid_gene_constraint_values()`` based on whether the - ``gene_constraint`` parameter is used or not. - -10. A new parameter called ``gene_constraint`` is added. It accepts a - list of callables (i.e. functions) acting as constraints for the - gene values. Before selecting a value for a gene, the callable is - called to ensure the candidate value is valid. Check the `Gene - Constraint `__ - section for more information. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/119 - -11. A new parameter called ``sample_size`` is added. To select a gene - value that respects a constraint, this variable defines the size of - the sample from which a value is selected randomly. Useful if either - ``allow_duplicate_genes`` or ``gene_constraint`` is used. An - instance attribute of the same name is created in the instances of - the ``pygad.GA`` class. Check the `sample_size - Parameter `__ - section for more information. - -12. Use the ``sample_size`` parameter instead of ``num_trials`` in the - methods ``solve_duplicate_genes_randomly()`` and - ``unique_float_gene_from_range()`` inside the - ``pygad/helper/unique.py`` script. It is the maximum number of - values to generate as the search space when looking for a unique - float value out of a range. - -13. Fixed a bug in population initialization when - ``allow_duplicate_genes=False``. Previously, gene values were - checked for duplicates before rounding, which could allow - near-duplicates like 7.61 and 7.62 to pass. After rounding (e.g., - both becoming 7.6), this resulted in unintended duplicates. The fix - ensures gene values are now rounded before duplicate checks, - preventing such cases. - -14. More tests are created. - -15. More examples are created. - -16. Edited the ``sort_solutions_nsga2()`` method in the - ``pygad/utils/nsga2.py`` script to accept an optional parameter - called ``find_best_solution`` when calling this method just to find - the best solution. - -17. Fixed a bug while applying the non-dominated sorting in the - ``get_non_dominated_set()`` method inside the - ``pygad/utils/nsga2.py`` script. It was swapping the non-dominated - and dominated sets. In other words, it used the non-dominated set as - if it is the dominated set and vice versa. All the calls to this - method were edited accordingly. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/320. - -18. Fix a bug retrieving in the ``best_solution()`` method when - retrieving the best solution for multi-objective problems. - https://github.com/ahmedfgad/GeneticAlgorithmPython/pull/331 - -.. _pygad-360: - -PyGAD 3.6.0 ------------ - -Release Date April 8, 2026 - -1. Support passing a class to the fitness, crossover, and mutation. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/342 - -2. A new class called ``Validation`` is created in the new - ``pygad/utils/validation.py`` script. It has a method called - ``validate_parameters()`` to validate all the parameters passed - while instantiating the ``pygad.GA`` class. - -3. Refactoring the ``pygad.py`` script by moving a lot of functions and - methods to other classes in other scripts. - -4. The ``summary()`` method was moved to ``Helper`` class in the - ``pygad/helper/misc.py`` script. - -5. The validation code in the ``__init__()`` method of the ``pygad.GA`` - class is moved to the new ``validate_parameters()`` method in the - new ``Validation`` class in the new ``pygad/utils/validation.py`` - script. Moreover, the ``validate_multi_stop_criteria()`` method is - also moved to the same class. - -6. The GA main workflow is moved into the new ``GAEngine`` class in the - new ``pygad/utils/engine.py`` script. Specifically, these methods - are moved from the ``pygad.GA`` class to the new ``GAEngine`` class: - - 1. ``run()`` - - 1. ``run_loop_head()`` - - 2. ``run_select_parents()`` - - 3. ``run_crossover()`` - - 4. ``run_mutation()`` - - 5. ``run_update_population()`` - - 2. ``initialize_population()`` - - 3. ``cal_pop_fitness()`` - - 4. ``best_solution()`` - - 5. ``round_genes()`` - -7. The ``pygad.GA`` class now extends the two new classes - ``utils.validation.Validation`` and ``utils.engine.GAEngine``. - -8. The version of the ``pygad.utils`` submodule is upgraded from - ``1.3.0`` to ``1.4.0``. - -9. The version of the ``pygad.helper`` submodule is upgraded from - ``1.2.0`` to ``1.3.0``. - -10. The version of the ``pygad.visualize`` submodule is upgraded from - ``1.1.0`` to ``1.1.1``. - -11. The version of the ``pygad.nn`` submodule is upgraded from ``1.2.1`` - to ``1.2.2``. - -12. The version of the ``pygad.cnn`` submodule is upgraded from - ``1.1.0`` to ``1.1.1``. - -13. The version of the ``pygad.torchga`` submodule is upgraded from - ``1.4.0`` to ``1.4.1``. - -14. The version of the ``pygad.kerasga`` submodule is upgraded from - ``1.3.0`` to ``1.3.1``. - -15. Update the elitism after the evolution ends to fix issue where the - best solution returned by the ``best_solution()`` method is not - correct. - https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/337 - -16. Fix a bug in calling the ``numpy.reshape()`` function. The parameter - ``newshape`` is removed since it is no longer supported started from - NumPy ``2.4.0``. - https://numpy.org/doc/stable/release/2.4.0-notes.html#removed-newshape-parameter-from-numpy-reshape - -17. A minor change in the documentation is made to replace the - ``newshape`` parameter when calling ``numpy.reshape()``. - -18. Fix a bug in the ``visualize/plot.py`` script that causes a warning - to be given when the plot leged is used with single-objective - problems. - -19. A new method called ``initialize_parents_array()`` is added to the - ``Helper`` class in the ``pygad/helper/misc.py`` script. It is - usually called from the methods in the ``ParentSelection`` class in - the ``pygad/utils/parent_selection.py`` script to initialize the - parents array. - -20. | Add more tests about: - | 1. Operators (crossover, mutation, and parent selection). - | 2. The ``best_solution()`` method. - | 3. Parallel processing. - | 4. The ``GANN`` module. - | 5. The plots created by the ``visualize``. - -21. Instead of using repeated code for converting the data type and - rounding the genes during crossover and mutation, the - ``change_gene_dtype_and_round()`` method is called from the - ``pygad.helper.misc.Helper`` class. - -22. Fix some documentation issues. - https://github.com/ahmedfgad/GeneticAlgorithmPython/pull/336 - -23. Update the documentation to reflect the recent additions and changes - to the library structure. - -PyGAD Projects at GitHub -======================== - -The PyGAD library is available at PyPI at this page -https://pypi.org/project/pygad. PyGAD is built out of a number of -open-source GitHub projects. A brief note about these projects is given -in the next subsections. - -`GeneticAlgorithmPython `__ --------------------------------------------------------------------------------- - -GitHub Link: https://github.com/ahmedfgad/GeneticAlgorithmPython - -`GeneticAlgorithmPython `__ -is the first project which is an open-source Python 3 project for -implementing the genetic algorithm based on NumPy. - -`NumPyANN `__ ----------------------------------------------------- - -GitHub Link: https://github.com/ahmedfgad/NumPyANN - -`NumPyANN `__ builds artificial -neural networks in **Python 3** using **NumPy** from scratch. The -purpose of this project is to only implement the **forward pass** of a -neural network without using a training algorithm. Currently, it only -supports classification and later regression will be also supported. -Moreover, only one class is supported per sample. - -`NeuralGenetic `__ --------------------------------------------------------------- - -GitHub Link: https://github.com/ahmedfgad/NeuralGenetic - -`NeuralGenetic `__ trains -neural networks using the genetic algorithm based on the previous 2 -projects -`GeneticAlgorithmPython `__ -and `NumPyANN `__. - -`NumPyCNN `__ ----------------------------------------------------- - -GitHub Link: https://github.com/ahmedfgad/NumPyCNN - -`NumPyCNN `__ builds -convolutional neural networks using NumPy. The purpose of this project -is to only implement the **forward pass** of a convolutional neural -network without using a training algorithm. - -`CNNGenetic `__ --------------------------------------------------------- - -GitHub Link: https://github.com/ahmedfgad/CNNGenetic - -`CNNGenetic `__ trains -convolutional neural networks using the genetic algorithm. It uses the -`GeneticAlgorithmPython `__ -project for building the genetic algorithm. - -`KerasGA `__ --------------------------------------------------- - -GitHub Link: https://github.com/ahmedfgad/KerasGA - -`KerasGA `__ trains -`Keras `__ models using the genetic algorithm. It uses -the -`GeneticAlgorithmPython `__ -project for building the genetic algorithm. - -`TorchGA `__ --------------------------------------------------- - -GitHub Link: https://github.com/ahmedfgad/TorchGA - -`TorchGA `__ trains -`PyTorch `__ models using the genetic algorithm. It -uses the -`GeneticAlgorithmPython `__ -project for building the genetic algorithm. - -`pygad.torchga `__: -https://github.com/ahmedfgad/TorchGA - -Stackoverflow Questions about PyGAD -=================================== - -.. _how-do-i-proceed-to-load-a-gainstance-as-pkl-format-in-pygad: - -`How do I proceed to load a ga_instance as “.pkl” format in PyGad? `__ ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - -`Binary Classification NN Model Weights not being Trained in PyGAD `__ --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - -`How to solve TSP problem using pyGAD package? `__ ---------------------------------------------------------------------------------------------------------------------------------------------- - -`How can I save a matplotlib plot that is the output of a function in jupyter? `__ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - -`How do I query the best solution of a pyGAD GA instance? `__ -------------------------------------------------------------------------------------------------------------------------------------------------------------------- - -`Multi-Input Multi-Output in Genetic algorithm (python) `__ --------------------------------------------------------------------------------------------------------------------------------------------------------------- - -https://www.linkedin.com/pulse/validation-short-term-parametric-trading-model-genetic-landolfi - -https://itchef.ru/articles/397758 - -https://audhiaprilliant.medium.com/genetic-algorithm-based-clustering-algorithm-in-searching-robust-initial-centroids-for-k-means-e3b4d892a4be - -https://python.plainenglish.io/validation-of-a-short-term-parametric-trading-model-with-genetic-optimization-and-walk-forward-89708b789af6 - -https://ichi.pro/ko/pygadwa-hamkke-yujeon-algolijeum-eul-sayonghayeo-keras-model-eul-hunlyeonsikineun-bangbeob-173299286377169 - -https://ichi.pro/tr/pygad-ile-genetik-algoritmayi-kullanarak-keras-modelleri-nasil-egitilir-173299286377169 - -https://ichi.pro/ru/kak-obucit-modeli-keras-s-pomos-u-geneticeskogo-algoritma-s-pygad-173299286377169 - -https://blog.csdn.net/sinat_38079265/article/details/108449614 - -Submitting Issues -================= - -If there is an issue using PyGAD, then use any of your preferred option -to discuss that issue. - -One way is `submitting an -issue `__ -into this GitHub project -(`github.com/ahmedfgad/GeneticAlgorithmPython `__) -in case something is not working properly or to ask for questions. - -If this is not a proper option for you, then check the `Contact -Us `__ -section for more contact details. - -Ask for Feature -=============== - -PyGAD is actively developed with the goal of building a dynamic library -for suporting a wide-range of problems to be optimized using the genetic -algorithm. - -To ask for a new feature, either `submit an -issue `__ -into this GitHub project -(`github.com/ahmedfgad/GeneticAlgorithmPython `__) -or send an e-mail to ahmed.f.gad@gmail.com. - -Also check the `Contact -Us `__ -section for more contact details. - -Projects Built using PyGAD -========================== - -If you created a project that uses PyGAD, then we can support you by -mentioning this project here in PyGAD's documentation. - -To do that, please send a message at ahmed.f.gad@gmail.com or check the -`Contact -Us `__ -section for more contact details. - -Within your message, please send the following details: - -- Project title - -- Brief description - -- Preferably, a link that directs the readers to your project - -Tutorials about PyGAD -===================== - -`Adaptive Mutation in Genetic Algorithm with Python Examples `__ ------------------------------------------------------------------------------------------------------------------------------------------------------ - -In this tutorial, we’ll see why mutation with a fixed number of genes is -bad, and how to replace it with adaptive mutation. Using the `PyGAD -Python 3 library `__, we’ll discuss a few -examples that use both random and adaptive mutation. - -`Clustering Using the Genetic Algorithm in Python `__ -------------------------------------------------------------------------------------------------------------------------- - -This tutorial discusses how the genetic algorithm is used to cluster -data, starting from random clusters and running until the optimal -clusters are found. We'll start by briefly revising the K-means -clustering algorithm to point out its weak points, which are later -solved by the genetic algorithm. The code examples in this tutorial are -implemented in Python using the `PyGAD -library `__. - -`Working with Different Genetic Algorithm Representations in Python `__ --------------------------------------------------------------------------------------------------------------------------------------------------------------------- - -Depending on the nature of the problem being optimized, the genetic -algorithm (GA) supports two different gene representations: binary, and -decimal. The binary GA has only two values for its genes, which are 0 -and 1. This is easier to manage as its gene values are limited compared -to the decimal GA, for which we can use different formats like float or -integer, and limited or unlimited ranges. - -This tutorial discusses how the -`PyGAD `__ library supports the two GA -representations, binary and decimal. - -.. _5-genetic-algorithm-applications-using-pygad: - -`5 Genetic Algorithm Applications Using PyGAD `__ -------------------------------------------------------------------------------------------------------------------------- - -This tutorial introduces PyGAD, an open-source Python library for -implementing the genetic algorithm and training machine learning -algorithms. PyGAD supports 19 parameters for customizing the genetic -algorithm for various applications. - -Within this tutorial we'll discuss 5 different applications of the -genetic algorithm and build them using PyGAD. - -`Train Neural Networks Using a Genetic Algorithm in Python with PyGAD `__ -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - -The genetic algorithm (GA) is a biologically-inspired optimization -algorithm. It has in recent years gained importance, as it’s simple -while also solving complex problems like travel route optimization, -training machine learning algorithms, working with single and -multi-objective problems, game playing, and more. - -Deep neural networks are inspired by the idea of how the biological -brain works. It’s a universal function approximator, which is capable of -simulating any function, and is now used to solve the most complex -problems in machine learning. What’s more, they’re able to work with all -types of data (images, audio, video, and text). - -Both genetic algorithms (GAs) and neural networks (NNs) are similar, as -both are biologically-inspired techniques. This similarity motivates us -to create a hybrid of both to see whether a GA can train NNs with high -accuracy. - -This tutorial uses `PyGAD `__, a Python -library that supports building and training NNs using a GA. -`PyGAD `__ offers both classification and -regression NNs. - -`Building a Game-Playing Agent for CoinTex Using the Genetic Algorithm `__ ----------------------------------------------------------------------------------------------------------------------------------------------------------- - -In this tutorial we'll see how to build a game-playing agent using only -the genetic algorithm to play a game called -`CoinTex `__, -which is developed in the Kivy Python framework. The objective of -CoinTex is to collect the randomly distributed coins while avoiding -collision with fire and monsters (that move randomly). The source code -of CoinTex can be found `on -GitHub `__. - -The genetic algorithm is the only AI used here; there is no other -machine/deep learning model used with it. We'll implement the genetic -algorithm using -`PyGad `__. -This tutorial starts with a quick overview of CoinTex followed by a -brief explanation of the genetic algorithm, and how it can be used to -create the playing agent. Finally, we'll see how to implement these -ideas in Python. - -The source code of the genetic algorithm agent is available -`here `__, -and you can download the code used in this tutorial from -`here `__. - -`How To Train Keras Models Using the Genetic Algorithm with PyGAD `__ --------------------------------------------------------------------------------------------------------------------------------------------------------- - -PyGAD is an open-source Python library for building the genetic -algorithm and training machine learning algorithms. It offers a wide -range of parameters to customize the genetic algorithm to work with -different types of problems. - -PyGAD has its own modules that support building and training neural -networks (NNs) and convolutional neural networks (CNNs). Despite these -modules working well, they are implemented in Python without any -additional optimization measures. This leads to comparatively high -computational times for even simple problems. - -The latest PyGAD version, 2.8.0 (released on 20 September 2020), -supports a new module to train Keras models. Even though Keras is built -in Python, it's fast. The reason is that Keras uses TensorFlow as a -backend, and TensorFlow is highly optimized. - -This tutorial discusses how to train Keras models using PyGAD. The -discussion includes building Keras models using either the Sequential -Model or the Functional API, building an initial population of Keras -model parameters, creating an appropriate fitness function, and more. - -|image2| - -`Train PyTorch Models Using Genetic Algorithm with PyGAD `__ ---------------------------------------------------------------------------------------------------------------------------------------------- - -`PyGAD `__ is a genetic algorithm Python -3 library for solving optimization problems. One of these problems is -training machine learning algorithms. - -PyGAD has a module called -`pygad.kerasga `__. It trains -Keras models using the genetic algorithm. On January 3rd, 2021, a new -release of `PyGAD 2.10.0 `__ brought a -new module called -`pygad.torchga `__ to train -PyTorch models. It’s very easy to use, but there are a few tricky steps. - -So, in this tutorial, we’ll explore how to use PyGAD to train PyTorch -models. - -|image3| - -`A Guide to Genetic ‘Learning’ Algorithms for Optimization `__ -------------------------------------------------------------------------------------------------------------------------------------------------------------------- - -PyGAD in Other Languages -======================== - -French ------- - -`Cómo los algoritmos genéticos pueden competir con el descenso de -gradiente y el -backprop `__ - -Bien que la manière standard d'entraîner les réseaux de neurones soit la -descente de gradient et la rétropropagation, il y a d'autres joueurs -dans le jeu. L'un d'eux est les algorithmes évolutionnaires, tels que -les algorithmes génétiques. - -Utiliser un algorithme génétique pour former un réseau de neurones -simple pour résoudre le OpenAI CartPole Jeu. Dans cet article, nous -allons former un simple réseau de neurones pour résoudre le OpenAI -CartPole . J'utiliserai PyTorch et PyGAD . - -|image4| - -Spanish -------- - -`Cómo los algoritmos genéticos pueden competir con el descenso de -gradiente y el -backprop `__ - -Aunque la forma estandar de entrenar redes neuronales es el descenso de -gradiente y la retropropagacion, hay otros jugadores en el juego, uno de -ellos son los algoritmos evolutivos, como los algoritmos geneticos. - -Usa un algoritmo genetico para entrenar una red neuronal simple para -resolver el Juego OpenAI CartPole. En este articulo, entrenaremos una -red neuronal simple para resolver el OpenAI CartPole . Usare PyTorch y -PyGAD . - -|image5| - -Korean ------- - -`[PyGAD] Python 에서 Genetic Algorithm 을 사용해보기 `__ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -|image6| - -파이썬에서 genetic algorithm을 사용하는 패키지들을 다 사용해보진 -않았지만, 확장성이 있어보이고, 시도할 일이 있어서 살펴봤다. - -이 패키지에서 가장 인상 깊었던 것은 neural network에서 hyper parameter -탐색을 gradient descent 방식이 아닌 GA로도 할 수 있다는 것이다. - -개인적으로 이 부분이 어느정도 초기치를 잘 잡아줄 수 있는 역할로도 쓸 수 -있고, Loss가 gradient descent 하기 어려운 구조에서 대안으로 쓸 수 있을 -것으로도 생각된다. - -일단 큰 흐름은 다음과 같이 된다. - -사실 완전히 흐름이나 각 parameter에 대한 이해는 부족한 상황 - -Turkish -------- - -`PyGAD ile Genetik Algoritmayı Kullanarak Keras Modelleri Nasıl Eğitilir `__ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This is a translation of an original English tutorial published at -Paperspace: `How To Train Keras Models Using the Genetic Algorithm with -PyGAD `__ - -PyGAD, genetik algoritma oluşturmak ve makine öğrenimi algoritmalarını -eğitmek için kullanılan açık kaynaklı bir Python kitaplığıdır. Genetik -algoritmayı farklı problem türleri ile çalışacak şekilde özelleştirmek -için çok çeşitli parametreler sunar. - -PyGAD, sinir ağları (NN’ler) ve evrişimli sinir ağları (CNN’ler) -oluşturmayı ve eğitmeyi destekleyen kendi modüllerine sahiptir. Bu -modüllerin iyi çalışmasına rağmen, herhangi bir ek optimizasyon önlemi -olmaksızın Python’da uygulanırlar. Bu, basit problemler için bile -nispeten yüksek hesaplama sürelerine yol açar. - -En son PyGAD sürümü 2.8.0 (20 Eylül 2020'de piyasaya sürüldü), Keras -modellerini eğitmek için yeni bir modülü destekliyor. Keras Python’da -oluşturulmuş olsa da hızlıdır. Bunun nedeni, Keras’ın arka uç olarak -TensorFlow kullanması ve TensorFlow’un oldukça optimize edilmiş -olmasıdır. - -Bu öğreticide, PyGAD kullanılarak Keras modellerinin nasıl eğitileceği -anlatılmaktadır. Tartışma, Sıralı Modeli veya İşlevsel API’yi kullanarak -Keras modellerini oluşturmayı, Keras model parametrelerinin ilk -popülasyonunu oluşturmayı, uygun bir uygunluk işlevi oluşturmayı ve daha -fazlasını içerir. - -|image7| - -Hungarian ---------- - -.. _tensorflow-alapozó-10-neurális-hálózatok-tenyésztése-genetikus-algoritmussal-pygad-és-openai-gym-használatával: - -`Tensorflow alapozó 10. Neurális hálózatok tenyésztése genetikus algoritmussal PyGAD és OpenAI Gym használatával `__ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Hogy kontextusba helyezzem a genetikus algoritmusokat, ismételjük kicsit -át, hogy hogyan működik a gradient descent és a backpropagation, ami a -neurális hálók tanításának általános módszere. Az erről írt cikkemet itt -tudjátok elolvasni. - -A hálózatok tenyésztéséhez a -`PyGAD `__ nevű -programkönyvtárat használjuk, így mindenek előtt ezt kell telepítenünk, -valamint a Tensorflow-t és a Gym-et, amit Colabban már eleve telepítve -kapunk. - -Maga a PyGAD egy teljesen általános genetikus algoritmusok futtatására -képes rendszer. Ennek a kiterjesztése a KerasGA, ami az általános motor -Tensorflow (Keras) neurális hálókon történő futtatását segíti. A 47. -sorban létrehozott KerasGA objektum ennek a kiterjesztésnek a része és -arra szolgál, hogy a paraméterként átadott modellből a második -paraméterben megadott számosságú populációt hozzon létre. Mivel a -hálózatunk 386 állítható paraméterrel rendelkezik, ezért a DNS-ünk itt -386 elemből fog állni. A populáció mérete 10 egyed, így a kezdő -populációnk egy 10x386 elemű mátrix lesz. Ezt adjuk át az 51. sorban az -initial_population paraméterben. - -|image8| - -Russian -------- - -`PyGAD: библиотека для имплементации генетического алгоритма `__ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -PyGAD — это библиотека для имплементации генетического алгоритма. Кроме -того, библиотека предоставляет доступ к оптимизированным реализациям -алгоритмов машинного обучения. PyGAD разрабатывали на Python 3. - -Библиотека PyGAD поддерживает разные типы скрещивания, мутации и -селекции родителя. PyGAD позволяет оптимизировать проблемы с помощью -генетического алгоритма через кастомизацию целевой функции. - -Кроме генетического алгоритма, библиотека содержит оптимизированные -имплементации алгоритмов машинного обучения. На текущий момент PyGAD -поддерживает создание и обучение нейросетей для задач классификации. - -Библиотека находится в стадии активной разработки. Создатели планируют -добавление функционала для решения бинарных задач и имплементации новых -алгоритмов. - -PyGAD разрабатывали на Python 3.7.3. Зависимости включают в себя NumPy -для создания и манипуляции массивами и Matplotlib для визуализации. Один -из изкейсов использования инструмента — оптимизация весов, которые -удовлетворяют заданной функции. - -|image9| - -Research Papers using PyGAD -=========================== - -A number of research papers used PyGAD and here are some of them: - -- Alberto Meola, Manuel Winkler, Sören Weinrich, Metaheuristic - optimization of data preparation and machine learning hyperparameters - for prediction of dynamic methane production, Bioresource Technology, - Volume 372, 2023, 128604, ISSN 0960-8524. - -- Jaros, Marta, and Jiri Jaros. "Performance-Cost Optimization of - Moldable Scientific Workflows." - -- Thorat, Divya. "Enhanced genetic algorithm to reduce makespan of - multiple jobs in map-reduce application on serverless platform". Diss. - Dublin, National College of Ireland, 2020. - -- Koch, Chris, and Edgar Dobriban. "AttenGen: Generating Live Attenuated - Vaccine Candidates using Machine Learning." (2021). - -- Bhardwaj, Bhavya, et al. "Windfarm optimization using Nelder-Mead and - Particle Swarm optimization." *2021 7th International Conference on - Electrical Energy Systems (ICEES)*. IEEE, 2021. - -- Bernardo, Reginald Christian S. and J. Said. “Towards a - model-independent reconstruction approach for late-time Hubble data.” - (2021). - -- Duong, Tri Dung, Qian Li, and Guandong Xu. "Prototype-based - Counterfactual Explanation for Causal Classification." *arXiv preprint - arXiv:2105.00703* (2021). - -- Farrag, Tamer Ahmed, and Ehab E. Elattar. "Optimized Deep Stacked Long - Short-Term Memory Network for Long-Term Load Forecasting." *IEEE - Access* 9 (2021): 68511-68522. - -- Antunes, E. D. O., Caetano, M. F., Marotta, M. A., Araujo, A., Bondan, - L., Meneguette, R. I., & Rocha Filho, G. P. (2021, August). Soluções - Otimizadas para o Problema de Localização de Máxima Cobertura em Redes - Militarizadas 4G/LTE. In *Anais do XXVI Workshop de Gerência e - Operação de Redes e Serviços* (pp. 152-165). SBC. - -- M. Yani, F. Ardilla, A. A. Saputra and N. Kubota, "Gradient-Free Deep - Q-Networks Reinforcement learning: Benchmark and Evaluation," *2021 - IEEE Symposium Series on Computational Intelligence (SSCI)*, 2021, pp. - 1-5, doi: 10.1109/SSCI50451.2021.9659941. - -- Yani, Mohamad, and Naoyuki Kubota. "Deep Convolutional Networks with - Genetic Algorithm for Reinforcement Learning Problem." - -- Mahendra, Muhammad Ihza, and Isman Kurniawan. "Optimizing - Convolutional Neural Network by Using Genetic Algorithm for COVID-19 - Detection in Chest X-Ray Image." *2021 International Conference on - Data Science and Its Applications (ICoDSA)*. IEEE, 2021. - -- Glibota, Vjeko. *Umjeravanje mikroskopskog prometnog modela primjenom - genetskog algoritma*. Diss. University of Zagreb. Faculty of Transport - and Traffic Sciences. Division of Intelligent Transport Systems and - Logistics. Department of Intelligent Transport Systems, 2021. - -- Zhu, Mingda. *Genetic Algorithm-based Parameter Identification for - Ship Manoeuvring Model under Wind Disturbance*. MS thesis. NTNU, 2021. - -- Abdalrahman, Ahmed, and Weihua Zhuang. "Dynamic pricing for - differentiated pev charging services using deep reinforcement - learning." *IEEE Transactions on Intelligent Transportation Systems* - (2020). - -More Links -========== - -https://rodriguezanton.com/identifying-contact-states-for-2d-objects-using-pygad-and/ - -https://torvaney.github.io/projects/t9-optimised - -For More Information -==================== - -There are different resources that can be used to get started with the -genetic algorithm and building it in Python. - -Tutorial: Implementing Genetic Algorithm in Python --------------------------------------------------- - -To start with coding the genetic algorithm, you can check the tutorial -titled `Genetic Algorithm Implementation in -Python `__ -available at these links: - -- `LinkedIn `__ - -- `Towards Data - Science `__ - -- `KDnuggets `__ - -`This -tutorial `__ -is prepared based on a previous version of the project but it still a -good resource to start with coding the genetic algorithm. - -|image10| - -Tutorial: Introduction to Genetic Algorithm -------------------------------------------- - -Get started with the genetic algorithm by reading the tutorial titled -`Introduction to Optimization with Genetic -Algorithm `__ -which is available at these links: - -- `LinkedIn `__ - -- `Towards Data - Science `__ - -- `KDnuggets `__ - -|image11| - -Tutorial: Build Neural Networks in Python ------------------------------------------ - -Read about building neural networks in Python through the tutorial -titled `Artificial Neural Network Implementation using NumPy and -Classification of the Fruits360 Image -Dataset `__ -available at these links: - -- `LinkedIn `__ - -- `Towards Data - Science `__ - -- `KDnuggets `__ - -|image12| - -Tutorial: Optimize Neural Networks with Genetic Algorithm ---------------------------------------------------------- - -Read about training neural networks using the genetic algorithm through -the tutorial titled `Artificial Neural Networks Optimization using -Genetic Algorithm with -Python `__ -available at these links: - -- `LinkedIn `__ - -- `Towards Data - Science `__ - -- `KDnuggets `__ - -|image13| - -Tutorial: Building CNN in Python --------------------------------- - -To start with coding the genetic algorithm, you can check the tutorial -titled `Building Convolutional Neural Network using NumPy from -Scratch `__ -available at these links: - -- `LinkedIn `__ - -- `Towards Data - Science `__ - -- `KDnuggets `__ - -- `Chinese Translation `__ - -`This -tutorial `__) -is prepared based on a previous version of the project but it still a -good resource to start with coding CNNs. - -|image14| - -Tutorial: Derivation of CNN from FCNN -------------------------------------- - -Get started with the genetic algorithm by reading the tutorial titled -`Derivation of Convolutional Neural Network from Fully Connected Network -Step-By-Step `__ -which is available at these links: - -- `LinkedIn `__ - -- `Towards Data - Science `__ - -- `KDnuggets `__ - -|image15| - -Book: Practical Computer Vision Applications Using Deep Learning with CNNs --------------------------------------------------------------------------- - -You can also check my book cited as `Ahmed Fawzy Gad 'Practical Computer -Vision Applications Using Deep Learning with CNNs'. Dec. 2018, Apress, -978-1-4842-4167-7 `__ -which discusses neural networks, convolutional neural networks, deep -learning, genetic algorithm, and more. - -Find the book at these links: - -- `Amazon `__ - -- `Springer `__ - -- `Apress `__ - -- `O'Reilly `__ - -- `Google Books `__ - -|image16| - -Contact Us -========== - -- E-mail: ahmed.f.gad@gmail.com - -- `LinkedIn `__ - -- `Amazon Author Page `__ - -- `Heartbeat `__ - -- `Paperspace `__ - -- `KDnuggets `__ - -- `TowardsDataScience `__ - -- `GitHub `__ - -|image17| - -Thank you for using -`PyGAD `__ :) - -.. |image1| image:: https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png -.. |image2| image:: https://user-images.githubusercontent.com/16560492/111009628-2b372500-8362-11eb-90cf-01b47d831624.png - :target: https://blog.paperspace.com/train-keras-models-using-genetic-algorithm-with-pygad -.. |image3| image:: https://user-images.githubusercontent.com/16560492/111009678-5457b580-8362-11eb-899a-39e2f96984df.png - :target: https://neptune.ai/blog/train-pytorch-models-using-genetic-algorithm-with-pygad -.. |image4| image:: https://user-images.githubusercontent.com/16560492/111009275-3178d180-8361-11eb-9e86-7fb1519acde7.png - :target: https://www.hebergementwebs.com/nouvelles/comment-les-algorithmes-genetiques-peuvent-rivaliser-avec-la-descente-de-gradient-et-le-backprop -.. |image5| image:: https://user-images.githubusercontent.com/16560492/111009257-232ab580-8361-11eb-99a5-7226efbc3065.png - :target: https://www.hebergementwebs.com/noticias/como-los-algoritmos-geneticos-pueden-competir-con-el-descenso-de-gradiente-y-el-backprop -.. |image6| image:: https://user-images.githubusercontent.com/16560492/108586306-85bd0280-731b-11eb-874c-7ac4ce1326cd.jpg - :target: https://data-newbie.tistory.com/m/685 -.. |image7| image:: https://user-images.githubusercontent.com/16560492/108586601-85be0200-731d-11eb-98a4-161c75a1f099.jpg - :target: https://erencan34.medium.com/pygad-ile-genetik-algoritmay%C4%B1-kullanarak-keras-modelleri-nas%C4%B1l-e%C4%9Fitilir-cf92639a478c -.. |image8| image:: https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png - :target: https://thebojda.medium.com/tensorflow-alapoz%C3%B3-10-24f7767d4a2c -.. |image9| image:: https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png - :target: https://neurohive.io/ru/frameworki/pygad-biblioteka-dlya-implementacii-geneticheskogo-algoritma -.. |image10| image:: https://user-images.githubusercontent.com/16560492/78830052-a3c19300-79e7-11ea-8b9b-4b343ea4049c.png - :target: https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad -.. |image11| image:: https://user-images.githubusercontent.com/16560492/82078259-26252d00-96e1-11ea-9a02-52a99e1054b9.jpg - :target: https://www.linkedin.com/pulse/introduction-optimization-genetic-algorithm-ahmed-gad -.. |image12| image:: https://user-images.githubusercontent.com/16560492/82078281-30472b80-96e1-11ea-8017-6a1f4383d602.jpg - :target: https://www.linkedin.com/pulse/artificial-neural-network-implementation-using-numpy-fruits360-gad -.. |image13| image:: https://user-images.githubusercontent.com/16560492/82078300-376e3980-96e1-11ea-821c-aa6b8ceb44d4.jpg - :target: https://www.linkedin.com/pulse/artificial-neural-networks-optimization-using-genetic-ahmed-gad -.. |image14| image:: https://user-images.githubusercontent.com/16560492/82431022-6c3a1200-9a8e-11ea-8f1b-b055196d76e3.png - :target: https://www.linkedin.com/pulse/building-convolutional-neural-network-using-numpy-from-ahmed-gad -.. |image15| image:: https://user-images.githubusercontent.com/16560492/82431369-db176b00-9a8e-11ea-99bd-e845192873fc.png - :target: https://www.linkedin.com/pulse/derivation-convolutional-neural-network-from-fully-connected-gad -.. |image16| image:: https://user-images.githubusercontent.com/16560492/78830077-ae7c2800-79e7-11ea-980b-53b6bd879eeb.jpg -.. |image17| image:: https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png diff --git a/docs/md/torchga.md b/docs/source/torchga.md similarity index 97% rename from docs/md/torchga.md rename to docs/source/torchga.md index 251b409..a529f67 100644 --- a/docs/md/torchga.md +++ b/docs/source/torchga.md @@ -13,7 +13,7 @@ The contents of this module are: More details are given in the next sections. -# Steps Summary +## Steps Summary The summary of the steps used to train a PyTorch model using PyGAD is as follows: @@ -24,7 +24,7 @@ The summary of the steps used to train a PyTorch model using PyGAD is as follows 6. Create an instance of the `pygad.GA` class. 8. Run the genetic algorithm. -# Create PyTorch Model +## Create PyTorch Model Before discussing training a PyTorch model using PyGAD, the first thing to do is to create the PyTorch model. To get started, please check the [PyTorch library documentation](https://pytorch.org/docs/stable/index.html). @@ -44,18 +44,18 @@ model = torch.nn.Sequential(input_layer, Feel free to add the layers of your choice. -# `pygad.torchga.TorchGA` Class +## `pygad.torchga.TorchGA` Class The `pygad.torchga` module has a class named `TorchGA` for creating an initial population for the genetic algorithm based on a PyTorch model. The constructor, methods, and attributes within the class are discussed in this section. -## `__init__()` +### `__init__()` The `pygad.torchga.TorchGA` class constructor accepts the following parameters: - `model`: An instance of the PyTorch model. - `num_solutions`: Number of solutions in the population. Each solution has different parameters of the model. -## Instance Attributes +### Instance Attributes All parameters in the `pygad.torchga.TorchGA` class constructor are used as instance attributes in addition to adding a new attribute called `population_weights`. @@ -65,19 +65,19 @@ Here is a list of all instance attributes: - `num_solutions` - `population_weights`: A nested list holding the weights of all solutions in the population. -## Methods in the `TorchGA` Class +### Methods in the `TorchGA` Class This section discusses the methods available for instances of the `pygad.torchga.TorchGA` class. -### `create_population()` +#### `create_population()` The `create_population()` method creates the initial population of the genetic algorithm as a list of solutions where each solution represents different model parameters. The list of networks is assigned to the `population_weights` attribute of the instance. -# Functions in the `pygad.torchga` Module +## Functions in the `pygad.torchga` Module This section discusses the functions in the `pygad.torchga` module. -## `pygad.torchga.model_weights_as_vector()` +### `pygad.torchga.model_weights_as_vector()` The `model_weights_as_vector()` function accepts a single parameter named `model` representing the PyTorch model. It returns a vector holding all model weights. The reason for representing the model weights as a vector is that the genetic algorithm expects all parameters of any solution to be in a 1D vector form. @@ -87,7 +87,7 @@ The function accepts the following parameters: It returns a 1D vector holding the model weights. -## `pygad.torch.model_weights_as_dict()` +### `pygad.torch.model_weights_as_dict()` The `model_weights_as_dict()` function accepts the following parameters: @@ -96,7 +96,7 @@ The `model_weights_as_dict()` function accepts the following parameters: It returns the restored model weights in the same form used by the `state_dict()` method. The returned dictionary is ready to be passed to the `load_state_dict()` method for setting the PyTorch model's parameters. -## `pygad.torchga.predict()` +### `pygad.torchga.predict()` The `predict()` function makes a prediction based on a solution. It accepts the following parameters: @@ -106,11 +106,11 @@ The `predict()` function makes a prediction based on a solution. It accepts the It returns the predictions for the data samples. -# Examples +## Examples This section gives the complete code of some examples that build and train a PyTorch model using PyGAD. Each subsection builds a different network. -## Example 1: Regression Example +### Example 1: Regression Example The next code builds a simple PyTorch model for regression. The next subsections discuss each part in the code. @@ -195,7 +195,7 @@ abs_error = loss_function(predictions, data_outputs) print("Absolute Error : ", abs_error.detach().numpy()) ``` -### Create a PyTorch model +#### Create a PyTorch model According to the steps mentioned previously, the first step is to create a PyTorch model. Here is the code that builds the model using the Functional API. @@ -211,7 +211,7 @@ model = torch.nn.Sequential(input_layer, output_layer) ``` -### Create an Instance of the `pygad.torchga.TorchGA` Class +#### Create an Instance of the `pygad.torchga.TorchGA` Class The second step is to create an instance of the `pygad.torchga.TorchGA` class. There are 10 solutions per population. Change this number according to your needs. @@ -222,7 +222,7 @@ torch_ga = torchga.TorchGA(model=model, num_solutions=10) ``` -### Prepare the Training Data +#### Prepare the Training Data The third step is to prepare the training data inputs and outputs. Here is an example where there are 4 samples. Each sample has 3 inputs and 1 output. @@ -242,7 +242,7 @@ data_outputs = numpy.array([[0.1], [2.5]]) ``` -### Build the Fitness Function +#### Build the Fitness Function The fourth step is to build the fitness function. This function must accept 2 parameters representing the solution and its index within the population. @@ -265,7 +265,7 @@ def fitness_func(ga_instance, solution, sol_idx): return solution_fitness ``` -### Create an Instance of the `pygad.GA` Class +#### Create an Instance of the `pygad.GA` Class The fifth step is to instantiate the `pygad.GA` class. Note how the `initial_population` parameter is assigned to the initial weights of the PyTorch models. @@ -284,7 +284,7 @@ ga_instance = pygad.GA(num_generations=num_generations, on_generation=on_generation) ``` -### Run the Genetic Algorithm +#### Run the Genetic Algorithm The sixth and last step is to run the genetic algorithm by calling the `run()` method. @@ -344,7 +344,7 @@ print("Absolute Error : ", abs_error.detach().numpy()) Absolute Error : 0.006876422 ``` -## Example 2: XOR Binary Classification +### Example 2: XOR Binary Classification The next code creates a PyTorch model to build the XOR binary classification problem. Let's highlight the changes compared to the previous example. @@ -497,7 +497,7 @@ Binary Crossentropy : 0.0 Accuracy : 1.0 ``` -## Example 3: Image Multi-Class Classification (Dense Layers) +### Example 3: Image Multi-Class Classification (Dense Layers) Here is the code. @@ -592,7 +592,7 @@ Compared to the previous binary classification example, this example has multipl loss_function = torch.nn.CrossEntropyLoss() ``` -### Prepare the Training Data +#### Prepare the Training Data Before building and training neural networks, the training data (input and output) needs to be prepared. The inputs and the outputs of the training data are NumPy arrays. @@ -624,7 +624,7 @@ Crossentropy : 0.74366045 Accuracy : 1.0 ``` -## Example 4: Image Multi-Class Classification (Conv Layers) +### Example 4: Image Multi-Class Classification (Conv Layers) Compared to the previous example that uses only dense layers, this example uses convolutional layers to classify the same dataset. @@ -758,7 +758,7 @@ model = torch.nn.Sequential(input_layer, output_layer) ``` -### Prepare the Training Data +#### Prepare the Training Data The data used in this example is available as 2 files: diff --git a/docs/source/torchga.rst b/docs/source/torchga.rst deleted file mode 100644 index 27825e8..0000000 --- a/docs/source/torchga.rst +++ /dev/null @@ -1,944 +0,0 @@ -.. _pygadtorchga-module: - -``pygad.torchga`` Module -======================== - -This section of the PyGAD's library documentation discusses the -**pygad.torchga** module. - -The ``pygad.torchga`` module has a helper class and 2 functions to train -PyTorch models using the genetic algorithm (PyGAD). - -The contents of this module are: - -1. ``TorchGA``: A class for creating an initial population of all - parameters in the PyTorch model. - -2. ``model_weights_as_vector()``: A function to reshape the PyTorch - model weights to a single vector. - -3. ``model_weights_as_dict()``: A function to restore the PyTorch model - weights from a vector. - -4. ``predict()``: A function to make predictions based on the PyTorch - model and a solution. - -More details are given in the next sections. - -Steps Summary -============= - -The summary of the steps used to train a PyTorch model using PyGAD is as -follows: - -1. Create a PyTorch model. - -2. Create an instance of the ``pygad.torchga.TorchGA`` class. - -3. Prepare the training data. - -4. Build the fitness function. - -5. Create an instance of the ``pygad.GA`` class. - -6. Run the genetic algorithm. - -Create PyTorch Model -==================== - -Before discussing training a PyTorch model using PyGAD, the first thing -to do is to create the PyTorch model. To get started, please check the -`PyTorch library -documentation `__. - -Here is an example of a PyTorch model. - -.. code:: python - - import torch - - input_layer = torch.nn.Linear(3, 5) - relu_layer = torch.nn.ReLU() - output_layer = torch.nn.Linear(5, 1) - - model = torch.nn.Sequential(input_layer, - relu_layer, - output_layer) - -Feel free to add the layers of your choice. - -.. _pygadtorchgatorchga-class: - -``pygad.torchga.TorchGA`` Class -=============================== - -The ``pygad.torchga`` module has a class named ``TorchGA`` for creating -an initial population for the genetic algorithm based on a PyTorch -model. The constructor, methods, and attributes within the class are -discussed in this section. - -.. _init: - -``__init__()`` --------------- - -The ``pygad.torchga.TorchGA`` class constructor accepts the following -parameters: - -- ``model``: An instance of the PyTorch model. - -- ``num_solutions``: Number of solutions in the population. Each - solution has different parameters of the model. - -Instance Attributes -------------------- - -All parameters in the ``pygad.torchga.TorchGA`` class constructor are -used as instance attributes in addition to adding a new attribute called -``population_weights``. - -Here is a list of all instance attributes: - -- ``model`` - -- ``num_solutions`` - -- ``population_weights``: A nested list holding the weights of all - solutions in the population. - -Methods in the ``TorchGA`` Class --------------------------------- - -This section discusses the methods available for instances of the -``pygad.torchga.TorchGA`` class. - -.. _createpopulation: - -``create_population()`` -~~~~~~~~~~~~~~~~~~~~~~~ - -The ``create_population()`` method creates the initial population of the -genetic algorithm as a list of solutions where each solution represents -different model parameters. The list of networks is assigned to the -``population_weights`` attribute of the instance. - -.. _functions-in-the-pygadtorchga-module: - -Functions in the ``pygad.torchga`` Module -========================================= - -This section discusses the functions in the ``pygad.torchga`` module. - -.. _pygadtorchgamodelweightsasvector: - -``pygad.torchga.model_weights_as_vector()`` --------------------------------------------- - -The ``model_weights_as_vector()`` function accepts a single parameter -named ``model`` representing the PyTorch model. It returns a vector -holding all model weights. The reason for representing the model weights -as a vector is that the genetic algorithm expects all parameters of any -solution to be in a 1D vector form. - -The function accepts the following parameters: - -- ``model``: The PyTorch model. - -It returns a 1D vector holding the model weights. - -.. _pygadtorchmodelweightsasdict: - -``pygad.torch.model_weights_as_dict()`` ---------------------------------------- - -The ``model_weights_as_dict()`` function accepts the following -parameters: - -1. ``model``: The PyTorch model. - -2. ``weights_vector``: The model parameters as a vector. - -It returns the restored model weights in the same form used by the -``state_dict()`` method. The returned dictionary is ready to be passed -to the ``load_state_dict()`` method for setting the PyTorch model's -parameters. - -.. _pygadtorchgapredict: - -``pygad.torchga.predict()`` ---------------------------- - -The ``predict()`` function makes a prediction based on a solution. It -accepts the following parameters: - -1. ``model``: The PyTorch model. - -2. ``solution``: The solution evolved. - -3. ``data``: The test data inputs. - -It returns the predictions for the data samples. - -Examples -======== - -This section gives the complete code of some examples that build and -train a PyTorch model using PyGAD. Each subsection builds a different -network. - -Example 1: Regression Example ------------------------------ - -The next code builds a simple PyTorch model for regression. The next -subsections discuss each part in the code. - -.. code:: python - - import torch - import torchga - import pygad - - def fitness_func(ga_instance, solution, sol_idx): - global data_inputs, data_outputs, torch_ga, model, loss_function - - predictions = pygad.torchga.predict(model=model, - solution=solution, - data=data_inputs) - - abs_error = loss_function(predictions, data_outputs).detach().numpy() + 0.00000001 - - solution_fitness = 1.0 / abs_error - - return solution_fitness - - def on_generation(ga_instance): - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution()[1]}") - - # Create the PyTorch model. - input_layer = torch.nn.Linear(3, 5) - relu_layer = torch.nn.ReLU() - output_layer = torch.nn.Linear(5, 1) - - model = torch.nn.Sequential(input_layer, - relu_layer, - output_layer) - # print(model) - - # Create an instance of the pygad.torchga.TorchGA class to build the initial population. - torch_ga = torchga.TorchGA(model=model, - num_solutions=10) - - loss_function = torch.nn.L1Loss() - - # Data inputs - data_inputs = torch.tensor([[0.02, 0.1, 0.15], - [0.7, 0.6, 0.8], - [1.5, 1.2, 1.7], - [3.2, 2.9, 3.1]]) - - # Data outputs - data_outputs = torch.tensor([[0.1], - [0.6], - [1.3], - [2.5]]) - - # Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class - num_generations = 250 # Number of generations. - num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. - initial_population = torch_ga.population_weights # Initial population of network weights - - ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - on_generation=on_generation) - - ga_instance.run() - - # After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. - ga_instance.plot_fitness(title="PyGAD & PyTorch - Iteration vs. Fitness", linewidth=4) - - # Returning the details of the best solution. - solution, solution_fitness, solution_idx = ga_instance.best_solution() - print(f"Fitness value of the best solution = {solution_fitness}") - print(f"Index of the best solution : {solution_idx}") - - # Make predictions based on the best solution. - predictions = pygad.torchga.predict(model=model, - solution=solution, - data=data_inputs) - print("Predictions : \n", predictions.detach().numpy()) - - abs_error = loss_function(predictions, data_outputs) - print("Absolute Error : ", abs_error.detach().numpy()) - -Create a PyTorch model -~~~~~~~~~~~~~~~~~~~~~~ - -According to the steps mentioned previously, the first step is to create -a PyTorch model. Here is the code that builds the model using the -Functional API. - -.. code:: python - - import torch - - input_layer = torch.nn.Linear(3, 5) - relu_layer = torch.nn.ReLU() - output_layer = torch.nn.Linear(5, 1) - - model = torch.nn.Sequential(input_layer, - relu_layer, - output_layer) - -.. _create-an-instance-of-the-pygadtorchgatorchga-class: - -Create an Instance of the ``pygad.torchga.TorchGA`` Class -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The second step is to create an instance of the -``pygad.torchga.TorchGA`` class. There are 10 solutions per population. -Change this number according to your needs. - -.. code:: python - - import pygad.torchga - - torch_ga = torchga.TorchGA(model=model, - num_solutions=10) - -Prepare the Training Data -~~~~~~~~~~~~~~~~~~~~~~~~~ - -The third step is to prepare the training data inputs and outputs. Here -is an example where there are 4 samples. Each sample has 3 inputs and 1 -output. - -.. code:: python - - import numpy - - # Data inputs - data_inputs = numpy.array([[0.02, 0.1, 0.15], - [0.7, 0.6, 0.8], - [1.5, 1.2, 1.7], - [3.2, 2.9, 3.1]]) - - # Data outputs - data_outputs = numpy.array([[0.1], - [0.6], - [1.3], - [2.5]]) - -Build the Fitness Function -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The fourth step is to build the fitness function. This function must -accept 2 parameters representing the solution and its index within the -population. - -The next fitness function calculates the mean absolute error (MAE) of -the PyTorch model based on the parameters in the solution. The -reciprocal of the MAE is used as the fitness value. Feel free to use any -other loss function to calculate the fitness value. - -.. code:: python - - loss_function = torch.nn.L1Loss() - - def fitness_func(ga_instance, solution, sol_idx): - global data_inputs, data_outputs, torch_ga, model, loss_function - - predictions = pygad.torchga.predict(model=model, - solution=solution, - data=data_inputs) - - abs_error = loss_function(predictions, data_outputs).detach().numpy() + 0.00000001 - - solution_fitness = 1.0 / abs_error - - return solution_fitness - -.. _create-an-instance-of-the-pygadga-class: - -Create an Instance of the ``pygad.GA`` Class -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The fifth step is to instantiate the ``pygad.GA`` class. Note how the -``initial_population`` parameter is assigned to the initial weights of -the PyTorch models. - -For more information, please check the `parameters this class -accepts `__. - -.. code:: python - - # Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class - num_generations = 250 # Number of generations. - num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. - initial_population = torch_ga.population_weights # Initial population of network weights - - ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - on_generation=on_generation) - -Run the Genetic Algorithm -~~~~~~~~~~~~~~~~~~~~~~~~~ - -The sixth and last step is to run the genetic algorithm by calling the -``run()`` method. - -.. code:: python - - ga_instance.run() - -After the PyGAD completes its execution, then there is a figure that -shows how the fitness value changes by generation. Call the -``plot_fitness()`` method to show the figure. - -.. code:: python - - ga_instance.plot_fitness(title="PyGAD & PyTorch - Iteration vs. Fitness", linewidth=4) - -Here is the figure. - -.. image:: https://user-images.githubusercontent.com/16560492/103469779-22f5b480-4d37-11eb-80dc-95503065ebb1.png - :alt: - -To get information about the best solution found by PyGAD, use the -``best_solution()`` method. - -.. code:: python - - # Returning the details of the best solution. - solution, solution_fitness, solution_idx = ga_instance.best_solution() - print(f"Fitness value of the best solution = {solution_fitness}") - print(f"Index of the best solution : {solution_idx}") - -.. code:: python - - Fitness value of the best solution = 145.42425295191546 - Index of the best solution : 0 - -The next code restores the trained model weights using the -``model_weights_as_dict()`` function. The restored weights are used to -calculate the predicted values. - -.. code:: python - - predictions = pygad.torchga.predict(model=model, - solution=solution, - data=data_inputs) - print("Predictions : \n", predictions.detach().numpy()) - -.. code:: python - - Predictions : - [[0.08401088] - [0.60939324] - [1.3010881 ] - [2.5010352 ]] - -The next code measures the trained model error. - -.. code:: python - - abs_error = loss_function(predictions, data_outputs) - print("Absolute Error : ", abs_error.detach().numpy()) - -.. code:: - - Absolute Error : 0.006876422 - -Example 2: XOR Binary Classification ------------------------------------- - -The next code creates a PyTorch model to build the XOR binary -classification problem. Let's highlight the changes compared to the -previous example. - -.. code:: python - - import torch - import torchga - import pygad - - def fitness_func(ga_instance, solution, sol_idx): - global data_inputs, data_outputs, torch_ga, model, loss_function - - predictions = pygad.torchga.predict(model=model, - solution=solution, - data=data_inputs) - - solution_fitness = 1.0 / (loss_function(predictions, data_outputs).detach().numpy() + 0.00000001) - - return solution_fitness - - def on_generation(ga_instance): - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution()[1]}") - - # Create the PyTorch model. - input_layer = torch.nn.Linear(2, 4) - relu_layer = torch.nn.ReLU() - dense_layer = torch.nn.Linear(4, 2) - output_layer = torch.nn.Softmax(1) - - model = torch.nn.Sequential(input_layer, - relu_layer, - dense_layer, - output_layer) - # print(model) - - # Create an instance of the pygad.torchga.TorchGA class to build the initial population. - torch_ga = torchga.TorchGA(model=model, - num_solutions=10) - - loss_function = torch.nn.BCELoss() - - # XOR problem inputs - data_inputs = torch.tensor([[0.0, 0.0], - [0.0, 1.0], - [1.0, 0.0], - [1.0, 1.0]]) - - # XOR problem outputs - data_outputs = torch.tensor([[1.0, 0.0], - [0.0, 1.0], - [0.0, 1.0], - [1.0, 0.0]]) - - # Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class - num_generations = 250 # Number of generations. - num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. - initial_population = torch_ga.population_weights # Initial population of network weights. - - # Create an instance of the pygad.GA class - ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - on_generation=on_generation) - - # Start the genetic algorithm evolution. - ga_instance.run() - - # After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. - ga_instance.plot_fitness(title="PyGAD & PyTorch - Iteration vs. Fitness", linewidth=4) - - # Returning the details of the best solution. - solution, solution_fitness, solution_idx = ga_instance.best_solution() - print(f"Fitness value of the best solution = {solution_fitness}") - print(f"Index of the best solution : {solution_idx}") - - # Make predictions based on the best solution. - predictions = pygad.torchga.predict(model=model, - solution=solution, - data=data_inputs) - print("Predictions : \n", predictions.detach().numpy()) - - # Calculate the binary crossentropy for the trained model. - print("Binary Crossentropy : ", loss_function(predictions, data_outputs).detach().numpy()) - - # Calculate the classification accuracy of the trained model. - a = torch.max(predictions, axis=1) - b = torch.max(data_outputs, axis=1) - accuracy = torch.sum(a.indices == b.indices) / len(data_outputs) - print("Accuracy : ", accuracy.detach().numpy()) - -Compared to the previous regression example, here are the changes: - -- The PyTorch model is changed according to the nature of the problem. - Now, it has 2 inputs and 2 outputs with an in-between hidden layer of - 4 neurons. - -.. code:: python - - input_layer = torch.nn.Linear(2, 4) - relu_layer = torch.nn.ReLU() - dense_layer = torch.nn.Linear(4, 2) - output_layer = torch.nn.Softmax(1) - - model = torch.nn.Sequential(input_layer, - relu_layer, - dense_layer, - output_layer) - -- The train data is changed. Note that the output of each sample is a - 1D vector of 2 values, 1 for each class. - -.. code:: python - - # XOR problem inputs - data_inputs = torch.tensor([[0.0, 0.0], - [0.0, 1.0], - [1.0, 0.0], - [1.0, 1.0]]) - - # XOR problem outputs - data_outputs = torch.tensor([[1.0, 0.0], - [0.0, 1.0], - [0.0, 1.0], - [1.0, 0.0]]) - -- The fitness value is calculated based on the binary cross entropy. - -.. code:: python - - loss_function = torch.nn.BCELoss() - -After the previous code completes, the next figure shows how the fitness -value change by generation. - -.. image:: https://user-images.githubusercontent.com/16560492/103469818-c646c980-4d37-11eb-98c3-d9d591acd5e2.png - :alt: - -Here is some information about the trained model. Its fitness value is -``100000000.0``, loss is ``0.0`` and accuracy is 100%. - -.. code:: python - - Fitness value of the best solution = 100000000.0 - - Index of the best solution : 0 - - Predictions : - [[1.0000000e+00 1.3627675e-10] - [3.8521746e-09 1.0000000e+00] - [4.2789325e-10 1.0000000e+00] - [1.0000000e+00 3.3668417e-09]] - - Binary Crossentropy : 0.0 - - Accuracy : 1.0 - -Example 3: Image Multi-Class Classification (Dense Layers) ----------------------------------------------------------- - -Here is the code. - -.. code:: python - - import torch - import torchga - import pygad - import numpy - - def fitness_func(ga_instance, solution, sol_idx): - global data_inputs, data_outputs, torch_ga, model, loss_function - - predictions = pygad.torchga.predict(model=model, - solution=solution, - data=data_inputs) - - solution_fitness = 1.0 / (loss_function(predictions, data_outputs).detach().numpy() + 0.00000001) - - return solution_fitness - - def on_generation(ga_instance): - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution()[1]}") - - # Build the PyTorch model using the functional API. - input_layer = torch.nn.Linear(360, 50) - relu_layer = torch.nn.ReLU() - dense_layer = torch.nn.Linear(50, 4) - output_layer = torch.nn.Softmax(1) - - model = torch.nn.Sequential(input_layer, - relu_layer, - dense_layer, - output_layer) - - # Create an instance of the pygad.torchga.TorchGA class to build the initial population. - torch_ga = torchga.TorchGA(model=model, - num_solutions=10) - - loss_function = torch.nn.CrossEntropyLoss() - - # Data inputs - data_inputs = torch.from_numpy(numpy.load("dataset_features.npy")).float() - - # Data outputs - data_outputs = torch.from_numpy(numpy.load("outputs.npy")).long() - # The next 2 lines are equivelant to this Keras function to perform 1-hot encoding: tensorflow.keras.utils.to_categorical(data_outputs) - # temp_outs = numpy.zeros((data_outputs.shape[0], numpy.unique(data_outputs).size), dtype=numpy.uint8) - # temp_outs[numpy.arange(data_outputs.shape[0]), numpy.uint8(data_outputs)] = 1 - - # Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class - num_generations = 200 # Number of generations. - num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. - initial_population = torch_ga.population_weights # Initial population of network weights. - - # Create an instance of the pygad.GA class - ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - on_generation=on_generation) - - # Start the genetic algorithm evolution. - ga_instance.run() - - # After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. - ga_instance.plot_fitness(title="PyGAD & PyTorch - Iteration vs. Fitness", linewidth=4) - - # Returning the details of the best solution. - solution, solution_fitness, solution_idx = ga_instance.best_solution() - print(f"Fitness value of the best solution = {solution_fitness}") - print(f"Index of the best solution : {solution_idx}") - - # Fetch the parameters of the best solution. - best_solution_weights = torchga.model_weights_as_dict(model=model, - weights_vector=solution) - model.load_state_dict(best_solution_weights) - predictions = model(data_inputs) - # print("Predictions : \n", predictions) - - # Calculate the crossentropy loss of the trained model. - print("Crossentropy : ", loss_function(predictions, data_outputs).detach().numpy()) - - # Calculate the classification accuracy for the trained model. - accuracy = torch.sum(torch.max(predictions, axis=1).indices == data_outputs) / len(data_outputs) - print("Accuracy : ", accuracy.detach().numpy()) - -Compared to the previous binary classification example, this example has -multiple classes (4) and thus the loss is measured using cross entropy. - -.. code:: python - - loss_function = torch.nn.CrossEntropyLoss() - -.. _prepare-the-training-data-2: - -Prepare the Training Data -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Before building and training neural networks, the training data (input -and output) needs to be prepared. The inputs and the outputs of the -training data are NumPy arrays. - -The data used in this example is available as 2 files: - -1. `dataset_features.npy `__: - Data inputs. - https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy - -2. `outputs.npy `__: - Class labels. - https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy - -The data consists of 4 classes of images. The image shape is -``(100, 100, 3)``. The number of training samples is 1962. The feature -vector extracted from each image has a length 360. - -.. code:: python - - import numpy - - data_inputs = numpy.load("dataset_features.npy") - - data_outputs = numpy.load("outputs.npy") - -The next figure shows how the fitness value changes. - -.. image:: https://user-images.githubusercontent.com/16560492/103469855-5d138600-4d38-11eb-84b1-b5eff8faa7bc.png - :alt: - -Here are some statistics about the trained model. - -.. code:: - - Fitness value of the best solution = 1.3446997034434534 - Index of the best solution : 0 - Crossentropy : 0.74366045 - Accuracy : 1.0 - -Example 4: Image Multi-Class Classification (Conv Layers) ---------------------------------------------------------- - -Compared to the previous example that uses only dense layers, this -example uses convolutional layers to classify the same dataset. - -Here is the complete code. - -.. code:: python - - import torch - import torchga - import pygad - import numpy - - def fitness_func(ga_instance, solution, sol_idx): - global data_inputs, data_outputs, torch_ga, model, loss_function - - predictions = pygad.torchga.predict(model=model, - solution=solution, - data=data_inputs) - - solution_fitness = 1.0 / (loss_function(predictions, data_outputs).detach().numpy() + 0.00000001) - - return solution_fitness - - def on_generation(ga_instance): - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution()[1]}") - - # Build the PyTorch model. - input_layer = torch.nn.Conv2d(in_channels=3, out_channels=5, kernel_size=7) - relu_layer1 = torch.nn.ReLU() - max_pool1 = torch.nn.MaxPool2d(kernel_size=5, stride=5) - - conv_layer2 = torch.nn.Conv2d(in_channels=5, out_channels=3, kernel_size=3) - relu_layer2 = torch.nn.ReLU() - - flatten_layer1 = torch.nn.Flatten() - # The value 768 is pre-computed by tracing the sizes of the layers' outputs. - dense_layer1 = torch.nn.Linear(in_features=768, out_features=15) - relu_layer3 = torch.nn.ReLU() - - dense_layer2 = torch.nn.Linear(in_features=15, out_features=4) - output_layer = torch.nn.Softmax(1) - - model = torch.nn.Sequential(input_layer, - relu_layer1, - max_pool1, - conv_layer2, - relu_layer2, - flatten_layer1, - dense_layer1, - relu_layer3, - dense_layer2, - output_layer) - - # Create an instance of the pygad.torchga.TorchGA class to build the initial population. - torch_ga = torchga.TorchGA(model=model, - num_solutions=10) - - loss_function = torch.nn.CrossEntropyLoss() - - # Data inputs - data_inputs = torch.from_numpy(numpy.load("dataset_inputs.npy")).float() - data_inputs = data_inputs.reshape((data_inputs.shape[0], data_inputs.shape[3], data_inputs.shape[1], data_inputs.shape[2])) - - # Data outputs - data_outputs = torch.from_numpy(numpy.load("dataset_outputs.npy")).long() - - # Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class - num_generations = 200 # Number of generations. - num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. - initial_population = torch_ga.population_weights # Initial population of network weights. - - # Create an instance of the pygad.GA class - ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - on_generation=on_generation) - - # Start the genetic algorithm evolution. - ga_instance.run() - - # After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. - ga_instance.plot_fitness(title="PyGAD & PyTorch - Iteration vs. Fitness", linewidth=4) - - # Returning the details of the best solution. - solution, solution_fitness, solution_idx = ga_instance.best_solution() - print(f"Fitness value of the best solution = {solution_fitness}") - print(f"Index of the best solution : {solution_idx}") - - # Make predictions based on the best solution. - predictions = pygad.torchga.predict(model=model, - solution=solution, - data=data_inputs) - # print("Predictions : \n", predictions) - - # Calculate the crossentropy for the trained model. - print("Crossentropy : ", loss_function(predictions, data_outputs).detach().numpy()) - - # Calculate the classification accuracy for the trained model. - accuracy = torch.sum(torch.max(predictions, axis=1).indices == data_outputs) / len(data_outputs) - print("Accuracy : ", accuracy.detach().numpy()) - -Compared to the previous example, the only change is that the -architecture uses convolutional and max-pooling layers. The shape of -each input sample is 100x100x3. - -.. code:: python - - input_layer = torch.nn.Conv2d(in_channels=3, out_channels=5, kernel_size=7) - relu_layer1 = torch.nn.ReLU() - max_pool1 = torch.nn.MaxPool2d(kernel_size=5, stride=5) - - conv_layer2 = torch.nn.Conv2d(in_channels=5, out_channels=3, kernel_size=3) - relu_layer2 = torch.nn.ReLU() - - flatten_layer1 = torch.nn.Flatten() - # The value 768 is pre-computed by tracing the sizes of the layers' outputs. - dense_layer1 = torch.nn.Linear(in_features=768, out_features=15) - relu_layer3 = torch.nn.ReLU() - - dense_layer2 = torch.nn.Linear(in_features=15, out_features=4) - output_layer = torch.nn.Softmax(1) - - model = torch.nn.Sequential(input_layer, - relu_layer1, - max_pool1, - conv_layer2, - relu_layer2, - flatten_layer1, - dense_layer1, - relu_layer3, - dense_layer2, - output_layer) - -.. _prepare-the-training-data-3: - -Prepare the Training Data -~~~~~~~~~~~~~~~~~~~~~~~~~ - -The data used in this example is available as 2 files: - -1. `dataset_inputs.npy `__: - Data inputs. - https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_inputs.npy - -2. `dataset_outputs.npy `__: - Class labels. - https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_outputs.npy - -The data consists of 4 classes of images. The image shape is -``(100, 100, 3)`` and there are 20 images per class for a total of 80 -training samples. For more information about the dataset, check the -`Reading the -Data `__ -section of the ``pygad.cnn`` module. - -Simply download these 2 files and read them according to the next code. - -.. code:: python - - import numpy - - data_inputs = numpy.load("dataset_inputs.npy") - - data_outputs = numpy.load("dataset_outputs.npy") - -The next figure shows how the fitness value changes. - -.. image:: https://user-images.githubusercontent.com/16560492/103469887-c7c4c180-4d38-11eb-98a7-1c5e73e918d0.png - :alt: - -Here are some statistics about the trained model. The model accuracy is -97.5% after the 200 generations. Note that just running the code again -may give different results. - -.. code:: - - Fitness value of the best solution = 1.3009520689219258 - Index of the best solution : 0 - Crossentropy : 0.7686678 - Accuracy : 0.975 diff --git a/docs/md/utils.md b/docs/source/utils.md similarity index 94% rename from docs/md/utils.md rename to docs/source/utils.md index b80d1d6..04c50dc 100644 --- a/docs/md/utils.md +++ b/docs/source/utils.md @@ -1,6 +1,6 @@ -# `pygad.torchga` Module +# `pygad.utils` Module -This section of the PyGAD's library documentation discusses the **pygad.utils** module. +This section of the documentation discusses the **pygad.utils** module. PyGAD supports different types of operators for selecting the parents, applying the crossover, and mutation. More features will be added in the future. To ask for a new feature, please check the [Ask for Feature](https://pygad.readthedocs.io/en/latest/releases.html#ask-for-feature) section. @@ -16,7 +16,7 @@ Note that the `pygad.GA` class extends all of these classes. So, the user can ac The next sections discuss each submodule. -# `pygad.utils.engine` Submodule +## `pygad.utils.engine` Submodule The `pygad.utils.engine` module has the `GAEngine` class that implements the engine of the library. The methods in this class are: @@ -31,7 +31,7 @@ The `pygad.utils.engine` module has the `GAEngine` class that implements the eng 4. `best_solution()` 5. `round_genes()` -## `initialize_population()` +### `initialize_population()` It creates an initial population randomly as a NumPy array. The array is saved in the instance attribute named `population`. @@ -46,7 +46,7 @@ This method assigns the values of the following 3 instance attributes: 2. `population`: Initially, it holds the initial population and later updated after each generation. 3. `initial_population`: Keeping the initial population. -## `cal_pop_fitness()` +### `cal_pop_fitness()` The `cal_pop_fitness()` method calculates and returns the fitness values of the solutions in the current population. @@ -64,7 +64,7 @@ This function takes into consideration: It returns a vector of the solutions' fitness values. -## `run()` +### `run()` Runs the genetic algorithm. This is the main method in which the genetic algorithm is evolved through some generations. It accepts no parameters as it uses the instance to access all of its requirements. @@ -93,7 +93,7 @@ Note that the `run()` method is calling 5 different methods during the loop: 4. `run_mutation()` 5. `run_update_population()` -## `best_solution()` +### `best_solution()` Returns information about the best solution found by the genetic algorithm. @@ -109,11 +109,11 @@ It returns the following: * `best_match_idx`: Index of the best solution in the current population. -## `round_genes()` +### `round_genes()` A method to round the genes in the passed solutions. It loops through each gene across all the passed solutions and rounds their values if applicable. -# `pygad.utils.validation` Submodule +## `pygad.utils.validation` Submodule The `pygad.utils.validation` module has the `Validation` class that validates the arguments passed while instantiating the `pygad.GA` class. The methods in this class are: @@ -121,21 +121,31 @@ The `pygad.utils.validation` module has the `Validation` class that validates th An inner method called `validate_multi_stop_criteria()` exists to validate the `stop_criteria` argument. -# `pygad.utils.crossover` Submodule +## `pygad.utils.crossover` Submodule -The `pygad.utils.crossover` module has a class named `Crossover` with the supported crossover operations which are: +The `pygad.utils.crossover` module has a class named `Crossover` with the supported crossover operations: 1. Single point: Implemented using the `single_point_crossover()` method. 2. Two points: Implemented using the `two_points_crossover()` method. 3. Uniform: Implemented using the `uniform_crossover()` method. 4. Scattered: Implemented using the `scattered_crossover()` method. -All crossover methods accept this parameter: +Crossover takes two parents and builds a child by mixing their genes. The next figure shows how single-point, two-point, and uniform crossover do this. + +:::{figure} images/crossover_types.* +:alt: Single-point, two-point, and uniform crossover +:width: 560px +:align: center + +How single-point, two-point, and uniform crossover build a child from two parents. +::: + +All crossover methods accept these parameters: 1. `parents`: The parents to mate for producing the offspring. 2. `offspring_size`: The size of the offspring to produce. -## Crossover Methods +### Crossover Methods The `Crossover` class in the `pygad.utils.crossover` module supports several methods for applying crossover between the selected parents. All of these methods accept the same parameters which are: @@ -146,25 +156,25 @@ All of such methods return an array of the produced offspring. The next subsections list the supported methods for crossover. -### `single_point_crossover()` +#### `single_point_crossover()` Applies the single-point crossover. It selects a point randomly at which crossover takes place between the pairs of parents. -### `two_points_crossover()` +#### `two_points_crossover()` Applies the 2 points crossover. It selects the 2 points randomly at which crossover takes place between the pairs of parents. -### `uniform_crossover()` +#### `uniform_crossover()` Applies the uniform crossover. For each gene, a parent out of the 2 mating parents is selected randomly and the gene is copied from it. -### `scattered_crossover()` +#### `scattered_crossover()` Applies the scattered crossover. It randomly selects the gene from one of the 2 parents. -# `pygad.utils.mutation` Submodule +## `pygad.utils.mutation` Submodule -The `pygad.utils.mutation` module has a class named `Mutation` with the supported mutation operations which are: +The `pygad.utils.mutation` module has a class named `Mutation` with the supported mutation operations: 1. Random: Implemented using the `random_mutation()` method. 2. Swap: Implemented using the `swap_mutation()` method. @@ -172,11 +182,21 @@ The `pygad.utils.mutation` module has a class named `Mutation` with the supporte 4. Scramble: Implemented using the `scramble_mutation()` method. 5. Adaptive: Implemented using the `adaptive_mutation()` method. +Mutation makes small random changes to the offspring so the search can explore new values. The next figure shows random mutation, where a few genes are picked at random and their values are changed. + +:::{figure} images/mutation.* +:alt: Random mutation changes a few genes +:width: 560px +:align: center + +Random mutation changes the values of a few genes that are picked at random. +::: + All mutation methods accept this parameter: 1. `offspring`: The offspring to mutate. -## Mutation Methods +### Mutation Methods The `Mutation` class in the `pygad.utils.mutation` module supports several methods for applying mutation. All of these methods accept the same parameter which is: @@ -186,29 +206,29 @@ All of such methods return an array of the mutated offspring. The next subsections list the supported methods for mutation. -### `random_mutation()` +#### `random_mutation()` Applies the random mutation which changes the values of some genes randomly. The number of genes is specified according to either the `mutation_num_genes` or the `mutation_percent_genes` attributes. For each gene, a random value is selected according to the range specified by the 2 attributes `random_mutation_min_val` and `random_mutation_max_val`. The random value is added to the selected gene. -### `swap_mutation()` +#### `swap_mutation()` Applies the swap mutation which interchanges the values of 2 randomly selected genes. -### `inversion_mutation()` +#### `inversion_mutation()` Applies the inversion mutation which selects a subset of genes and inverts them. -### `scramble_mutation()` +#### `scramble_mutation()` Applies the scramble mutation which selects a subset of genes and shuffles their order randomly. -### `adaptive_mutation()` +#### `adaptive_mutation()` Applies the adaptive mutation which selects the number/percentage of genes to mutate based on the solution's fitness. If the fitness is high (i.e. solution quality is high), then small number/percentage of genes is mutated compared to a solution with a low fitness. -## Mutation Helper Methods +### Mutation Helper Methods The `pygad.utils.mutation` module has some helper methods to assist applying the mutation operation: @@ -223,7 +243,7 @@ The `pygad.utils.mutation` module has some helper methods to assist applying the 9. `adaptive_mutation_randomly()`: Applies the adaptive mutation based on randomly. A number of genes are selected randomly for mutation. This number depends on the fitness of the solution. The random values are selected based on the 2 parameters `andom_mutation_min_val` and `random_mutation_max_val`. 10. `adaptive_mutation_probs_randomly()`: Uses the mutation probabilities to decide which genes to apply the adaptive mutation randomly. -# Adaptive Mutation +## Adaptive Mutation In the regular genetic algorithm, the mutation works by selecting a single fixed mutation rate for all solutions regardless of their fitness values. So, regardless on whether this solution has high or low quality, the same number of genes are mutated all the time. @@ -252,7 +272,7 @@ The next figure summarizes the previous steps. This strategy is applied in PyGAD. -## Use Adaptive Mutation in PyGAD +### Use Adaptive Mutation in PyGAD In [PyGAD 2.10.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-10-0), adaptive mutation is supported. To use it, just follow the following 2 simple steps: @@ -330,7 +350,7 @@ ga_instance.run() ga_instance.plot_fitness(title="PyGAD with Adaptive Mutation", linewidth=5) ``` -# `pygad.utils.parent_selection` Submodule +## `pygad.utils.parent_selection` Submodule The `pygad.utils.parent_selection` module has a class named `ParentSelection` with the supported parent selection operations which are: @@ -352,7 +372,7 @@ It has the following helper methods: 1. `wheel_cumulative_probs()`: A helper function to calculate the wheel probabilities for these 2 methods: 1) `roulette_wheel_selection()` 2) `rank_selection()` -## Parent Selection Methods +### Parent Selection Methods The `ParentSelection` class in the `pygad.utils.parent_selection` module has several methods for selecting the parents that will mate to produce the offspring. All of such methods accept the same parameters which are: @@ -363,39 +383,39 @@ All of such methods return an array of the selected parents. The next subsections list the supported methods for parent selection. -### `steady_state_selection()` +#### `steady_state_selection()` Selects the parents using the steady-state selection technique. -### `rank_selection()` +#### `rank_selection()` Selects the parents using the rank selection technique. -### `random_selection()` +#### `random_selection()` Selects the parents randomly. -### `tournament_selection()` +#### `tournament_selection()` Selects the parents using the tournament selection technique. -### `roulette_wheel_selection()` +#### `roulette_wheel_selection()` Selects the parents using the roulette wheel selection technique. -### `stochastic_universal_selection()` +#### `stochastic_universal_selection()` Selects the parents using the stochastic universal selection technique. -### `nsga2_selection()` +#### `nsga2_selection()` Selects the parents for the NSGA-II algorithm to solve multi-objective optimization problems. It selects the parents by ranking them based on non-dominated sorting and crowding distance. -### `tournament_selection_nsga2()` +#### `tournament_selection_nsga2()` Selects the parents for the NSGA-II algorithm to solve multi-objective optimization problems. It selects the parents using the tournament selection technique applied based on non-dominated sorting and crowding distance. -# `pygad.utils.nsga2` Submodule +## `pygad.utils.nsga2` Submodule The `pygad.utils.nsga2` module has a class named `NSGA2` that implements NSGA-II. The methods inside this class are: @@ -404,7 +424,7 @@ The `pygad.utils.nsga2` module has a class named `NSGA2` that implements NSGA-II 3. `crowding_distance()`: Calculates the crowding distance for all solutions in the current pareto front. 4. `sort_solutions_nsga2()`: Sort the solutions. If the problem is single-objective, then the solutions are sorted by sorting the fitness values of the population. If it is multi-objective, then non-dominated sorting and crowding distance are applied to sort the solutions. -# User-Defined Crossover, Mutation, and Parent Selection Operators +## User-Defined Crossover, Mutation, and Parent Selection Operators Previously, the user can select the the type of the crossover, mutation, and parent selection operators by assigning the name of the operator to the following parameters of the `pygad.GA` class's constructor: @@ -442,7 +462,7 @@ ga_instance.plot_fitness() This section describes the expected input parameters and outputs. For simplicity, all of these custom functions all accept the instance of the `pygad.GA` class as the last parameter. -## User-Defined Crossover Operator +### User-Defined Crossover Operator The user-defined crossover function is a Python function that accepts 3 parameters: @@ -493,7 +513,7 @@ ga_instance = pygad.GA(num_generations=10, crossover_type=crossover_func) ``` -## User-Defined Mutation Operator +### User-Defined Mutation Operator A user-defined mutation function/operator can be created the same way a custom crossover operator/function is created. Simply, it is a Python function that accepts 2 parameters: @@ -546,7 +566,7 @@ and more. It all depends on your objective from building the mutation function. You may neglect or consider some of the considerations according to your objective. -## User-Defined Parent Selection Operator +### User-Defined Parent Selection Operator No much to mention about building a user-defined parent selection function as things are similar to building a crossover or mutation function. Just create a Python function that accepts 3 parameters: @@ -598,7 +618,7 @@ ga_instance = pygad.GA(num_generations=10, parent_selection_type=parent_selection_func) ``` -## Example +### Example By discussing how to customize the 3 operators, the next code uses the previous 3 user-defined functions instead of the built-in functions. diff --git a/docs/source/utils.rst b/docs/source/utils.rst deleted file mode 100644 index ef81bf6..0000000 --- a/docs/source/utils.rst +++ /dev/null @@ -1,953 +0,0 @@ -.. _pygadtorchga-module: - -``pygad.torchga`` Module -======================== - -This section of the PyGAD's library documentation discusses the -**pygad.utils** module. - -PyGAD supports different types of operators for selecting the parents, -applying the crossover, and mutation. More features will be added in the -future. To ask for a new feature, please check the `Ask for -Feature `__ -section. - -The submodules in the ``pygad.utils`` module are: - -1. ``engine``: The core engine of the library. It has the ``GAEngine`` - class implementing the main loop and related functions. - -2. ``crossover``: Has the ``Crossover`` class that implements the - crossover operators. - -3. ``mutation``: Has the ``Mutation`` class that implements the mutation - operators. - -4. ``parent_selection``: Has the ``ParentSelection`` class that - implements the parent selection operators. - -5. ``nsga2``: Has the ``NSGA2`` class that implements the Non-Dominated - Sorting Genetic Algorithm II (NSGA-II). - -Note that the ``pygad.GA`` class extends all of these classes. So, the -user can access any of the methods in such classes directly by the -instance/object of the ``pygad.GA`` class. - -The next sections discuss each submodule. - -.. _pygadutilsengine-submodule: - -``pygad.utils.engine`` Submodule -================================ - -The ``pygad.utils.engine`` module has the ``GAEngine`` class that -implements the engine of the library. The methods in this class are: - -1. ``initialize_population()`` - -2. ``cal_pop_fitness()`` - -3. ``run()`` - - 1. ``run_loop_head()`` - - 2. ``run_select_parents()`` - - 3. ``run_crossover()`` - - 4. ``run_mutation()`` - - 5. ``run_update_population()`` - -4. ``best_solution()`` - -5. ``round_genes()`` - -.. _initializepopulation: - -``initialize_population()`` ---------------------------- - -It creates an initial population randomly as a NumPy array. The array is -saved in the instance attribute named ``population``. - -Accepts the following parameters: - -- ``low``: The lower value of the random range from which the gene - values in the initial population are selected. It defaults to -4. - Available in PyGAD 1.0.20 and higher. - -- ``high``: The upper value of the random range from which the gene - values in the initial population are selected. It defaults to -4. - Available in PyGAD 1.0.20. - -This method assigns the values of the following 3 instance attributes: - -1. ``pop_size``: Size of the population. - -2. ``population``: Initially, it holds the initial population and later - updated after each generation. - -3. ``initial_population``: Keeping the initial population. - -.. _calpopfitness: - -``cal_pop_fitness()`` ---------------------- - -The ``cal_pop_fitness()`` method calculates and returns the fitness -values of the solutions in the current population. - -This function is optimized to save time by making fewer calls the -fitness function. It follows this process: - -1. If the ``save_solutions`` parameter is set to ``True``, then it - checks if the solution is already explored and saved in the - ``solutions`` instance attribute. If so, then it just retrieves its - fitness from the ``solutions_fitness`` instance attribute without - calling the fitness function. - -2. If ``save_solutions`` is set to ``False`` or if it is ``True`` but - the solution was not explored yet, then the ``cal_pop_fitness()`` - method checks if the ``keep_elitism`` parameter is set to a positive - integer. If so, then it checks if the solution is saved into the - ``last_generation_elitism`` instance attribute. If so, then it - retrieves its fitness from the ``previous_generation_fitness`` - instance attribute. - -3. If neither of the above 3 conditions apply (1. ``save_solutions`` is - set to ``False`` or 2. if it is ``True`` but the solution was not - explored yet or 3. ``keep_elitism`` is set to zero), then the - ``cal_pop_fitness()`` method checks if the ``keep_parents`` parameter - is set to ``-1`` or a positive integer. If so, then it checks if the - solution is saved into the ``last_generation_parents`` instance - attribute. If so, then it retrieves its fitness from the - ``previous_generation_fitness`` instance attribute. - -4. If neither of the above 4 conditions apply, then we have to call the - fitness function to calculate the fitness for the solution. This is - by calling the function assigned to the ``fitness_func`` parameter. - -This function takes into consideration: - -1. The ``parallel_processing`` parameter to check whether parallel - processing is in effect. - -2. The ``fitness_batch_size`` parameter to check if the fitness should - be calculated in batches of solutions. - -It returns a vector of the solutions' fitness values. - -``run()`` ---------- - -Runs the genetic algorithm. This is the main method in which the genetic -algorithm is evolved through some generations. It accepts no parameters -as it uses the instance to access all of its requirements. - -For each generation, the fitness values of all solutions within the -population are calculated according to the ``cal_pop_fitness()`` method -which internally just calls the function assigned to the -``fitness_func`` parameter in the ``pygad.GA`` class constructor for -each solution. - -According to the fitness values of all solutions, the parents are -selected using the ``select_parents()`` method. This method behavior is -determined according to the parent selection type in the -``parent_selection_type`` parameter in the ``pygad.GA`` class -constructor - -Based on the selected parents, offspring are generated by applying the -crossover and mutation operations using the ``crossover()`` and -``mutation()`` methods. The behavior of such 2 methods is defined -according to the ``crossover_type`` and ``mutation_type`` parameters in -the ``pygad.GA`` class constructor. - -After the generation completes, the following takes place: - -- The ``population`` attribute is updated by the new population. - -- The ``generations_completed`` attribute is assigned by the number of - the last completed generation. - -- If there is a callback function assigned to the ``on_generation`` - attribute, then it will be called. - -After the ``run()`` method completes, the following takes place: - -- The ``best_solution_generation`` is assigned the generation number at - which the best fitness value is reached. - -- The ``run_completed`` attribute is set to ``True``. - -Note that the ``run()`` method is calling 5 different methods during the -loop: - -1. ``run_loop_head()`` - -2. ``run_select_parents()`` - -3. ``run_crossover()`` - -4. ``run_mutation()`` - -5. ``run_update_population()`` - -.. _bestsolution: - -``best_solution()`` -------------------- - -Returns the following information about the best solution in the latest -population: - -1. Solution - -2. Fitness - -3. Index within the population - -The best solution is determined based on the fitness values. To save -time calling the fitness function, the user is allowed to pass the -fitness based on which the best solution is determined. If not passed, -it will call the fitness function to calculate the fitness of all -solutions within the latest population. - -.. _roundgenes: - -``round_genes()`` ------------------ - -A method to round the genes in the passed solutions. It loops through -each gene across all the passed solutions and rounds their values if -applicable. - -.. _pygadutilscrossover-submodule: - -``pygad.utils.crossover`` Submodule -=================================== - -The ``pygad.utils.crossover`` module has a class named ``Crossover`` -with the supported crossover operations which are: - -1. Single point: Implemented using the ``single_point_crossover()`` - method. - -2. Two points: Implemented using the ``two_points_crossover()`` method. - -3. Uniform: Implemented using the ``uniform_crossover()`` method. - -4. Scattered: Implemented using the ``scattered_crossover()`` method. - -All crossover methods accept this parameter: - -1. ``parents``: The parents to mate for producing the offspring. - -2. ``offspring_size``: The size of the offspring to produce. - -.. _pygadutilsmutation-submodule: - -``pygad.utils.mutation`` Submodule -================================== - -The ``pygad.utils.mutation`` module has a class named ``Mutation`` with -the supported mutation operations which are: - -1. Random: Implemented using the ``random_mutation()`` method. - -2. Swap: Implemented using the ``swap_mutation()`` method. - -3. Inversion: Implemented using the ``inversion_mutation()`` method. - -4. Scramble: Implemented using the ``scramble_mutation()`` method. - -5. Adaptive: Implemented using the ``adaptive_mutation()`` method. - -All mutation methods accept this parameter: - -1. ``offspring``: The offspring to mutate. - -The ``pygad.utils.mutation`` module has some helper methods to assist -applying the mutation operation: - -1. ``mutation_by_space()``: Applies the mutation using the - ``gene_space`` parameter. - -2. ``mutation_probs_by_space()``: Uses the mutation probabilities in - the ``mutation_probabilities`` instance attribute to apply the - mutation using the ``gene_space`` parameter. For each gene, if its - probability is <= that the mutation probability, then it will be - mutated based on the mutation space. - -3. ``mutation_process_gene_value()``: Generate/select values for the - gene that satisfy the constraint. The values could be generated - randomly or from the gene space. - -4. ``mutation_randomly()``: Applies the random mutation. - -5. ``mutation_probs_randomly()``: Uses the mutation probabilities in - the ``mutation_probabilities`` instance attribute to apply the - random mutation. For each gene, if its probability is <= that the - mutation probability, then it will be mutated randomly. - -6. ``adaptive_mutation_population_fitness()``: A helper method to - calculate the average fitness of the solutions before applying the - adaptive mutation. - -7. ``adaptive_mutation_by_space()``: Applies the adaptive mutation - based on the ``gene_space`` parameter. A number of genes are - selected randomly for mutation. This number depends on the fitness - of the solution. The random values are selected from the - ``gene_space`` parameter. - -8. ``adaptive_mutation_probs_by_space()``: Uses the mutation - probabilities to decide which genes to apply the adaptive mutation - by space. - -9. ``adaptive_mutation_randomly()``: Applies the adaptive mutation - based on randomly. A number of genes are selected randomly for - mutation. This number depends on the fitness of the solution. The - random values are selected based on the 2 parameters - ``andom_mutation_min_val`` and ``random_mutation_max_val``. - -10. ``adaptive_mutation_probs_randomly()``: Uses the mutation - probabilities to decide which genes to apply the adaptive mutation - randomly. - -Adaptive Mutation -================= - -In the regular genetic algorithm, the mutation works by selecting a -single fixed mutation rate for all solutions regardless of their fitness -values. So, regardless on whether this solution has high or low quality, -the same number of genes are mutated all the time. - -The pitfalls of using a constant mutation rate for all solutions are -summarized in this paper `Libelli, S. Marsili, and P. Alba. "Adaptive -mutation in genetic algorithms." Soft computing 4.2 (2000): -76-80 `__ -as follows: - - The weak point of "classical" GAs is the total randomness of - mutation, which is applied equally to all chromosomes, irrespective - of their fitness. Thus a very good chromosome is equally likely to be - disrupted by mutation as a bad one. - - On the other hand, bad chromosomes are less likely to produce good - ones through crossover, because of their lack of building blocks, - until they remain unchanged. They would benefit the most from - mutation and could be used to spread throughout the parameter space - to increase the search thoroughness. So there are two conflicting - needs in determining the best probability of mutation. - - Usually, a reasonable compromise in the case of a constant mutation - is to keep the probability low to avoid disruption of good - chromosomes, but this would prevent a high mutation rate of - low-fitness chromosomes. Thus a constant probability of mutation - would probably miss both goals and result in a slow improvement of - the population. - -According to `Libelli, S. Marsili, and P. -Alba. `__ -work, the adaptive mutation solves the problems of constant mutation. - -Adaptive mutation works as follows: - -1. Calculate the average fitness value of the population (``f_avg``). - -2. For each chromosome, calculate its fitness value (``f``). - -3. If ``ff_avg``, then this solution is regarded as a high-quality - solution and thus the mutation rate should be kept low to avoid - disrupting this high quality solution. - -In PyGAD, if ``f=f_avg``, then the solution is regarded of high quality. - -The next figure summarizes the previous steps. - -|image1| - -This strategy is applied in PyGAD. - -Use Adaptive Mutation in PyGAD ------------------------------- - -In `PyGAD -2.10.0 `__, -adaptive mutation is supported. To use it, just follow the following 2 -simple steps: - -1. In the constructor of the ``pygad.GA`` class, set - ``mutation_type="adaptive"`` to specify that the type of mutation is - adaptive. - -2. Specify the mutation rates for the low and high quality solutions - using one of these 3 parameters according to your preference: - ``mutation_probability``, ``mutation_num_genes``, and - ``mutation_percent_genes``. Please check the `documentation of each - of these - parameters `__ - for more information. - -When adaptive mutation is used, then the value assigned to any of the 3 -parameters can be of any of these data types: - -1. ``list`` - -2. ``tuple`` - -3. ``numpy.ndarray`` - -Whatever the data type used, the length of the ``list``, ``tuple``, or -the ``numpy.ndarray`` must be exactly 2. That is there are just 2 -values: - -1. The first value is the mutation rate for the low-quality solutions. - -2. The second value is the mutation rate for the high-quality solutions. - -PyGAD expects that the first value is higher than the second value and -thus a warning is printed in case the first value is lower than the -second one. - -Here are some examples to feed the mutation rates: - -.. code:: python - - # mutation_probability - mutation_probability = [0.25, 0.1] - mutation_probability = (0.35, 0.17) - mutation_probability = numpy.array([0.15, 0.05]) - - # mutation_num_genes - mutation_num_genes = [4, 2] - mutation_num_genes = (3, 1) - mutation_num_genes = numpy.array([7, 2]) - - # mutation_percent_genes - mutation_percent_genes = [25, 12] - mutation_percent_genes = (15, 8) - mutation_percent_genes = numpy.array([21, 13]) - -Assume that the average fitness is 12 and the fitness values of 2 -solutions are 15 and 7. If the mutation probabilities are specified as -follows: - -.. code:: python - - mutation_probability = [0.25, 0.1] - -Then the mutation probability of the first solution is 0.1 because its -fitness is 15 which is higher than the average fitness 12. The mutation -probability of the second solution is 0.25 because its fitness is 7 -which is lower than the average fitness 12. - -Here is an example that uses adaptive mutation. - -.. code:: python - - import pygad - import numpy - - function_inputs = [4,-2,3.5,5,-11,-4.7] # Function inputs. - desired_output = 44 # Function output. - - def fitness_func(ga_instance, solution, solution_idx): - # The fitness function calulates the sum of products between each input and its corresponding weight. - output = numpy.sum(solution*function_inputs) - # The value 0.000001 is used to avoid the Inf value when the denominator numpy.abs(output - desired_output) is 0.0. - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - - # Creating an instance of the GA class inside the ga module. Some parameters are initialized within the constructor. - ga_instance = pygad.GA(num_generations=200, - fitness_func=fitness_func, - num_parents_mating=10, - sol_per_pop=20, - num_genes=len(function_inputs), - mutation_type="adaptive", - mutation_num_genes=(3, 1)) - - # Running the GA to optimize the parameters of the function. - ga_instance.run() - - ga_instance.plot_fitness(title="PyGAD with Adaptive Mutation", linewidth=5) - -.. _pygadutilsparentselection-submodule: - -``pygad.utils.parent_selection`` Submodule -========================================== - -The ``pygad.utils.parent_selection`` module has a class named -``ParentSelection`` with the supported parent selection operations which -are: - -1. Steady-state: Implemented using the ``steady_state_selection()`` - method. - -2. Roulette wheel: Implemented using the ``roulette_wheel_selection()`` - method. - -3. Stochastic universal: Implemented using the - ``stochastic_universal_selection()``\ method. - -4. Rank: Implemented using the ``rank_selection()`` method. - -5. Random: Implemented using the ``random_selection()`` method. - -6. Tournament: Implemented using the ``tournament_selection()`` method. - -7. NSGA-II: Implemented using the ``nsga2_selection()`` method. - -8. NSGA-II Tournament: Implemented using the - ``tournament_selection_nsga2()`` method. - -All parent selection methods accept these parameters: - -1. ``fitness``: The fitness of the entire population. - -2. ``num_parents``: The number of parents to select. - -It has the following helper methods: - -1. ``wheel_cumulative_probs()``: A helper function to calculate the - wheel probabilities for these 2 methods: 1) - ``roulette_wheel_selection()`` 2) ``rank_selection()`` - -.. _pygadutilsnsga2-submodule: - -``pygad.utils.nsga2`` Submodule -=============================== - -The ``pygad.utils.nsga2`` module has a class named ``NSGA2`` that -implements NSGA-II. The methods inside this class are: - -1. ``non_dominated_sorting()``: Returns all the pareto fronts by - applying non-dominated sorting over the solutions. - -2. ``get_non_dominated_set()``: Returns the 2 sets of non-dominated - solutions and dominated solutions from the passed solutions. Note - that the Pareto front consists of the solutions in the non-dominated - set. - -3. ``crowding_distance()``: Calculates the crowding distance for all - solutions in the current pareto front. - -4. ``sort_solutions_nsga2()``: Sort the solutions. If the problem is - single-objective, then the solutions are sorted by sorting the - fitness values of the population. If it is multi-objective, then - non-dominated sorting and crowding distance are applied to sort the - solutions. - -User-Defined Crossover, Mutation, and Parent Selection Operators -================================================================ - -Previously, the user can select the the type of the crossover, mutation, -and parent selection operators by assigning the name of the operator to -the following parameters of the ``pygad.GA`` class's constructor: - -1. ``crossover_type`` - -2. ``mutation_type`` - -3. ``parent_selection_type`` - -This way, the user can only use the built-in functions for each of these -operators. - -Starting from `PyGAD -2.16.0 `__, -the user can create a custom crossover, mutation, and parent selection -operators and assign these functions to the above parameters. Thus, a -new operator can be plugged easily into the `PyGAD -Lifecycle `__. - -This is a sample code that does not use any custom function. - -.. code:: python - - import pygad - import numpy - - equation_inputs = [4,-2,3.5] - desired_output = 44 - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - - ga_instance = pygad.GA(num_generations=10, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func) - - ga_instance.run() - ga_instance.plot_fitness() - -This section describes the expected input parameters and outputs. For -simplicity, all of these custom functions all accept the instance of the -``pygad.GA`` class as the last parameter. - -User-Defined Crossover Operator -------------------------------- - -The user-defined crossover function is a Python function that accepts 3 -parameters: - -1. The selected parents. - -2. The size of the offspring as a tuple of 2 numbers: (the offspring - size, number of genes). - -3. The instance from the ``pygad.GA`` class. This instance helps to - retrieve any property like ``population``, ``gene_type``, - ``gene_space``, etc. - -This function should return a NumPy array of shape equal to the value -passed to the second parameter. - -The next code creates a template for the user-defined crossover -operator. You can use any names for the parameters. Note how a NumPy -array is returned. - -.. code:: python - - def crossover_func(parents, offspring_size, ga_instance): - offspring = ... - ... - return numpy.array(offspring) - -As an example, the next code creates a single-point crossover function. -By randomly generating a random point (i.e. index of a gene), the -function simply uses 2 parents to produce an offspring by copying the -genes before the point from the first parent and the remaining from the -second parent. - -.. code:: python - - def crossover_func(parents, offspring_size, ga_instance): - offspring = [] - idx = 0 - while len(offspring) != offspring_size[0]: - parent1 = parents[idx % parents.shape[0], :].copy() - parent2 = parents[(idx + 1) % parents.shape[0], :].copy() - - random_split_point = numpy.random.choice(range(offspring_size[1])) - - parent1[random_split_point:] = parent2[random_split_point:] - - offspring.append(parent1) - - idx += 1 - - return numpy.array(offspring) - -To use this user-defined function, simply assign its name to the -``crossover_type`` parameter in the constructor of the ``pygad.GA`` -class. The next code gives an example. In this case, the custom function -will be called in each generation rather than calling the built-in -crossover functions defined in PyGAD. - -.. code:: python - - ga_instance = pygad.GA(num_generations=10, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - crossover_type=crossover_func) - -User-Defined Mutation Operator ------------------------------- - -A user-defined mutation function/operator can be created the same way a -custom crossover operator/function is created. Simply, it is a Python -function that accepts 2 parameters: - -1. The offspring to be mutated. - -2. The instance from the ``pygad.GA`` class. This instance helps to - retrieve any property like ``population``, ``gene_type``, - ``gene_space``, etc. - -The template for the user-defined mutation function is given in the next -code. According to the user preference, the function should make some -random changes to the genes. - -.. code:: python - - def mutation_func(offspring, ga_instance): - ... - return offspring - -The next code builds the random mutation where a single gene from each -chromosome is mutated by adding a random number between 0 and 1 to the -gene's value. - -.. code:: python - - def mutation_func(offspring, ga_instance): - - for chromosome_idx in range(offspring.shape[0]): - random_gene_idx = numpy.random.choice(range(offspring.shape[1])) - - offspring[chromosome_idx, random_gene_idx] += numpy.random.random() - - return offspring - -Here is how this function is assigned to the ``mutation_type`` -parameter. - -.. code:: python - - ga_instance = pygad.GA(num_generations=10, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - crossover_type=crossover_func, - mutation_type=mutation_func) - -Note that there are other things to take into consideration like: - -- Making sure that each gene conforms to the data type(s) listed in the - ``gene_type`` parameter. - -- If the ``gene_space`` parameter is used, then the new value for the - gene should conform to the values/ranges listed. - -- Mutating a number of genes that conforms to the parameters - ``mutation_percent_genes``, ``mutation_probability``, and - ``mutation_num_genes``. - -- Whether mutation happens with or without replacement based on the - ``mutation_by_replacement`` parameter. - -- The minimum and maximum values from which a random value is generated - based on the ``random_mutation_min_val`` and - ``random_mutation_max_val`` parameters. - -- Whether duplicates are allowed or not in the chromosome based on the - ``allow_duplicate_genes`` parameter. - -and more. - -It all depends on your objective from building the mutation function. -You may neglect or consider some of the considerations according to your -objective. - -User-Defined Parent Selection Operator --------------------------------------- - -No much to mention about building a user-defined parent selection -function as things are similar to building a crossover or mutation -function. Just create a Python function that accepts 3 parameters: - -1. The fitness values of the current population. - -2. The number of parents needed. - -3. The instance from the ``pygad.GA`` class. This instance helps to - retrieve any property like ``population``, ``gene_type``, - ``gene_space``, etc. - -The function should return 2 outputs: - -1. The selected parents as a NumPy array. Its shape is equal to (the - number of selected parents, ``num_genes``). Note that the number of - selected parents is equal to the value assigned to the second input - parameter. - -2. The indices of the selected parents inside the population. It is a 1D - list with length equal to the number of selected parents. - -The outputs must be of type ``numpy.ndarray``. - -Here is a template for building a custom parent selection function. - -.. code:: python - - def parent_selection_func(fitness, num_parents, ga_instance): - ... - return parents, fitness_sorted[:num_parents] - -The next code builds the steady-state parent selection where the best -parents are selected. The number of parents is equal to the value in the -``num_parents`` parameter. - -.. code:: python - - def parent_selection_func(fitness, num_parents, ga_instance): - - fitness_sorted = sorted(range(len(fitness)), key=lambda k: fitness[k]) - fitness_sorted.reverse() - - parents = numpy.empty((num_parents, ga_instance.population.shape[1])) - - for parent_num in range(num_parents): - parents[parent_num, :] = ga_instance.population[fitness_sorted[parent_num], :].copy() - - return parents, numpy.array(fitness_sorted[:num_parents]) - -Finally, the defined function is assigned to the -``parent_selection_type`` parameter as in the next code. - -.. code:: python - - ga_instance = pygad.GA(num_generations=10, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - crossover_type=crossover_func, - mutation_type=mutation_func, - parent_selection_type=parent_selection_func) - -Example -------- - -By discussing how to customize the 3 operators, the next code uses the -previous 3 user-defined functions instead of the built-in functions. - -.. code:: python - - import pygad - import numpy - - equation_inputs = [4,-2,3.5] - desired_output = 44 - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - - return fitness - - def parent_selection_func(fitness, num_parents, ga_instance): - - fitness_sorted = sorted(range(len(fitness)), key=lambda k: fitness[k]) - fitness_sorted.reverse() - - parents = numpy.empty((num_parents, ga_instance.population.shape[1])) - - for parent_num in range(num_parents): - parents[parent_num, :] = ga_instance.population[fitness_sorted[parent_num], :].copy() - - return parents, numpy.array(fitness_sorted[:num_parents]) - - def crossover_func(parents, offspring_size, ga_instance): - - offspring = [] - idx = 0 - while len(offspring) != offspring_size[0]: - parent1 = parents[idx % parents.shape[0], :].copy() - parent2 = parents[(idx + 1) % parents.shape[0], :].copy() - - random_split_point = numpy.random.choice(range(offspring_size[1])) - - parent1[random_split_point:] = parent2[random_split_point:] - - offspring.append(parent1) - - idx += 1 - - return numpy.array(offspring) - - def mutation_func(offspring, ga_instance): - - for chromosome_idx in range(offspring.shape[0]): - random_gene_idx = numpy.random.choice(range(offspring.shape[0])) - - offspring[chromosome_idx, random_gene_idx] += numpy.random.random() - - return offspring - - ga_instance = pygad.GA(num_generations=10, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - crossover_type=crossover_func, - mutation_type=mutation_func, - parent_selection_type=parent_selection_func) - - ga_instance.run() - ga_instance.plot_fitness() - -This is the same example but using methods instead of functions. - -.. code:: python - - import pygad - import numpy - - equation_inputs = [4,-2,3.5] - desired_output = 44 - - class Test: - def fitness_func(self, ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - - return fitness - - def parent_selection_func(self, fitness, num_parents, ga_instance): - - fitness_sorted = sorted(range(len(fitness)), key=lambda k: fitness[k]) - fitness_sorted.reverse() - - parents = numpy.empty((num_parents, ga_instance.population.shape[1])) - - for parent_num in range(num_parents): - parents[parent_num, :] = ga_instance.population[fitness_sorted[parent_num], :].copy() - - return parents, numpy.array(fitness_sorted[:num_parents]) - - def crossover_func(self, parents, offspring_size, ga_instance): - - offspring = [] - idx = 0 - while len(offspring) != offspring_size[0]: - parent1 = parents[idx % parents.shape[0], :].copy() - parent2 = parents[(idx + 1) % parents.shape[0], :].copy() - - random_split_point = numpy.random.choice(range(offspring_size[0])) - - parent1[random_split_point:] = parent2[random_split_point:] - - offspring.append(parent1) - - idx += 1 - - return numpy.array(offspring) - - def mutation_func(self, offspring, ga_instance): - - for chromosome_idx in range(offspring.shape[0]): - random_gene_idx = numpy.random.choice(range(offspring.shape[1])) - - offspring[chromosome_idx, random_gene_idx] += numpy.random.random() - - return offspring - - ga_instance = pygad.GA(num_generations=10, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=Test().fitness_func, - parent_selection_type=Test().parent_selection_func, - crossover_type=Test().crossover_func, - mutation_type=Test().mutation_func) - - ga_instance.run() - ga_instance.plot_fitness() - -.. |image1| image:: https://user-images.githubusercontent.com/16560492/103468973-e3c26600-4d2c-11eb-8af3-b3bb39b50540.jpg diff --git a/docs/md/visualize.md b/docs/source/visualize.md similarity index 77% rename from docs/md/visualize.md rename to docs/source/visualize.md index 82ee74d..5d2889d 100644 --- a/docs/md/visualize.md +++ b/docs/source/visualize.md @@ -1,13 +1,13 @@ # `pygad.visualize` Module -This section of the PyGAD's library documentation discusses the **pygad.visualize** module. It offers the methods for results visualization in PyGAD. +This section of the documentation discusses the **pygad.visualize** module. It offers methods to visualize the results in PyGAD. -This section discusses the different options to visualize the results in PyGAD through these methods: +This section explains the different ways to visualize the results in PyGAD through these methods: -1. `plot_fitness()`: Creates plots for the fitness to show how the fitness evolves by generation. . -2. `plot_genes()`: Creates plots for the genes to show how the gene value changes for each generation. -3. `plot_new_solution_rate()`: Creates plots for the new solution rate to show how the number of new solutions explored in each solution. -4. `plot_pareto_front_curve()`: Creates plots for the pareto front for multi-objective problems. +1. `plot_fitness()`: Creates plots that show how the fitness evolves over the generations. +2. `plot_genes()`: Creates plots that show how the gene values change over the generations. +3. `plot_new_solution_rate()`: Creates plots that show how many new solutions are explored in each generation. +4. `plot_pareto_front_curve()`: Creates the Pareto front plot for multi-objective problems. In the following code, the `save_solutions` flag is set to `True` which means all solutions are saved in the `solutions` attribute. The code runs for only 10 generations. @@ -35,15 +35,15 @@ ga_instance = pygad.GA(num_generations=10, ga_instance.run() ``` -Let's explore how to visualize the results by the above mentioned methods. +The next sections explain how to visualize the results with these methods. -# Fitness +## Fitness -## `plot_fitness()` +### `plot_fitness()` The `plot_fitness()` method shows the fitness value for each generation. It creates, shows, and returns a figure that summarizes how the fitness value(s) evolve(s) by generation. It was previously named `plot_result()`. -It works only after completing at least 1 generation. If no generation is completed (at least 1), an exception is raised. +It works only after at least 1 generation is completed. If no generation is completed, an exception is raised. This method accepts the following parameters: @@ -57,7 +57,7 @@ This method accepts the following parameters: 8. `label`: The label used for the legend in the figures of multi-objective problems. It is not used for single-objective problems. It defaults to `None` which means no labels used. 9. `save_dir`: Directory to save the figure. -### `plot_type="plot"` +#### `plot_type="plot"` The simplest way to call this method is as follows leaving the `plot_type` with its default value `"plot"` to create a continuous line connecting the fitness values across all generations: @@ -68,7 +68,7 @@ ga_instance.plot_fitness() ![plot_fitness_plot](https://user-images.githubusercontent.com/16560492/122472609-d02f5280-cf8e-11eb-88a7-f9366ff6e7c6.png) -### `plot_type="scatter"` +#### `plot_type="scatter"` The `plot_type` can also be set to `"scatter"` to create a scatter graph with each individual fitness represented as a dot. The size of these dots can be changed using the `linewidth` parameter. @@ -78,7 +78,7 @@ ga_instance.plot_fitness(plot_type="scatter") ![plot_fitness_scatter](https://user-images.githubusercontent.com/16560492/122473159-75e2c180-cf8f-11eb-942d-31279b286dbd.png) -### `plot_type="bar"` +#### `plot_type="bar"` The third value for the `plot_type` parameter is `"bar"` to create a bar graph with each individual fitness represented as a bar. @@ -88,15 +88,15 @@ ga_instance.plot_fitness(plot_type="bar") ![plot_fitness_bar](https://user-images.githubusercontent.com/16560492/122473340-b7736c80-cf8f-11eb-89c5-4f7db3b653cc.png) -# New Solution Rate +## New Solution Rate -## `plot_new_solution_rate()` +### `plot_new_solution_rate()` -The `plot_new_solution_rate()` method presents the number of new solutions explored in each generation. This helps to figure out if the genetic algorithm is able to find new solutions as an indication of more possible evolution. If no new solutions are explored, this is an indication that no further evolution is possible. +The `plot_new_solution_rate()` method shows the number of new solutions explored in each generation. This helps you see whether the genetic algorithm is still finding new solutions. If no new solutions are explored, then no further evolution is possible. -It works only after completing at least 1 generation. If no generation is completed (at least 1), an exception is raised. +It works only after at least 1 generation is completed. If no generation is completed, an exception is raised. -The `plot_new_solution_rate()` method accepts the same parameters as in the `plot_fitness()` method (it also have 3 possible values for `plot_type` parameter). Here are all the parameters it accepts: +The `plot_new_solution_rate()` method accepts the same parameters as the `plot_fitness()` method (it also has 3 possible values for the `plot_type` parameter). Here are all the parameters it accepts: 1. `title`: Title of the figure. 2. `xlabel`: X-axis label. @@ -107,7 +107,7 @@ The `plot_new_solution_rate()` method accepts the same parameters as in the `plo 7. `color`: Color of the plot which defaults to `"#3870FF"`. 8. `save_dir`: Directory to save the figure. -### `plot_type="plot"` +#### `plot_type="plot"` The default value for the `plot_type` parameter is `"plot"`. @@ -116,11 +116,11 @@ ga_instance.plot_new_solution_rate() # ga_instance.plot_new_solution_rate(plot_type="plot") ``` -The next figure shows that, for example, generation 6 has the least number of new solutions which is 4. The number of new solutions in the first generation is always equal to the number of solutions in the population (i.e. the value assigned to the `sol_per_pop` parameter in the constructor of the `pygad.GA` class) which is 10 in this example. +The next figure shows that, for example, generation 6 has the least number of new solutions, which is 4. The number of new solutions in the first generation is always equal to the number of solutions in the population (the value of the `sol_per_pop` parameter in the constructor of the `pygad.GA` class), which is 10 in this example. ![plot_new_solution_rate_plot](https://user-images.githubusercontent.com/16560492/122475815-3322e880-cf93-11eb-9648-bf66f823234b.png) -### `plot_type="scatter"` +#### `plot_type="scatter"` The previous graph can be represented as scattered points by setting `plot_type="scatter"`. @@ -130,9 +130,9 @@ ga_instance.plot_new_solution_rate(plot_type="scatter") ![plot_new_solution_rate_scatter](https://user-images.githubusercontent.com/16560492/122476108-adec0380-cf93-11eb-80ac-7588bf90492f.png) -### `plot_type="bar"` +#### `plot_type="bar"` -By setting `plot_type="scatter"`, each value is represented as a vertical bar. +By setting `plot_type="bar"`, each value is represented as a vertical bar. ```python ga_instance.plot_new_solution_rate(plot_type="bar") @@ -140,9 +140,9 @@ ga_instance.plot_new_solution_rate(plot_type="bar") ![plot_new_solution_rate_bar](https://user-images.githubusercontent.com/16560492/122476173-c2c89700-cf93-11eb-9e77-d39737cd3a96.png) -# Genes +## Genes -## `plot_genes()` +### `plot_genes()` The `plot_genes()` method is the third option to visualize the PyGAD results. The `plot_genes()` method creates, shows, and returns a figure that describes each gene. It has different options to create the figures which helps to: @@ -150,7 +150,7 @@ The `plot_genes()` method is the third option to visualize the PyGAD results. Th 2. Create a histogram for each gene. 3. Create a boxplot. -It works only after completing at least 1 generation. If no generation is completed, an exception is raised. If no generation is completed (at least 1), an exception is raised. +It works only after at least 1 generation is completed. If no generation is completed, an exception is raised. This method accepts the following parameters: @@ -172,7 +172,7 @@ This method has 3 control variables: 2. `plot_type="plot"`: Identical to the `plot_type` parameter explored in the `plot_fitness()` and `plot_new_solution_rate()` methods. 3. `solutions="all"`: Can be `"all"` (default) or `"best"`. -These 3 parameters controls the style of the output figure. +These 3 parameters control the style of the output figure. The `graph_type` parameter selects the type of the graph which helps to explore the gene values as: @@ -186,14 +186,14 @@ The `solutions` parameter selects whether the genes come from all solutions in t An exception is raised if: -* `solutions="all"` while `save_solutions=False` in the constructor of the `pygad.GA` class. . -* `solutions="best"` while `save_best_solutions=False` in the constructor of the `pygad.GA` class. . +* `solutions="all"` while `save_solutions=False` in the constructor of the `pygad.GA` class. +* `solutions="best"` while `save_best_solutions=False` in the constructor of the `pygad.GA` class. -### `graph_type="plot"` +#### `graph_type="plot"` When `graph_type="plot"`, then the figure creates a normal graph where the relationship between the gene values and the generation numbers is represented as a continuous plot, scattered points, or bars. -#### `plot_type="plot"` +##### `plot_type="plot"` Because the default value for both `graph_type` and `plot_type` is `"plot"`, then all of the lines below creates the same figure. This figure is helpful to know whether a gene value lasts for more generations as an indication of the best value for this gene. For example, the value 16 for the gene with index 5 (at column 2 and row 2 of the next graph) lasted for 83 generations. @@ -226,7 +226,7 @@ ga_instance.plot_genes(graph_type="plot", solutions="all") ``` -#### `plot_type="scatter"` +##### `plot_type="scatter"` The following calls of the `plot_genes()` method create the same scatter plot. @@ -240,7 +240,7 @@ ga_instance.plot_genes(graph_type="plot", ![plot_genes_scatter](https://user-images.githubusercontent.com/16560492/122477273-73836600-cf95-11eb-828f-f357c7b0f815.png) -#### `plot_type="bar"` +##### `plot_type="bar"` ```python ga_instance.plot_genes(plot_type="bar") @@ -252,7 +252,7 @@ ga_instance.plot_genes(graph_type="plot", ![plot_genes_bar](https://user-images.githubusercontent.com/16560492/122477370-99106f80-cf95-11eb-8643-865b55e6b844.png) -### `graph_type="boxplot"` +#### `graph_type="boxplot"` By setting `graph_type` to `"boxplot"`, then a box and whisker graph is created. Now, the `plot_type` parameter has no effect. @@ -267,9 +267,9 @@ ga_instance.plot_genes(graph_type="boxplot", ![plot_genes_boxplot](https://user-images.githubusercontent.com/16560492/122479260-beeb4380-cf98-11eb-8f08-23707929b12c.png) -### `graph_type="histogram"` +#### `graph_type="histogram"` -For `graph_type="boxplot"`, then a histogram is created for each gene. Similar to `graph_type="boxplot"`, the `plot_type` parameter has no effect. +For `graph_type="histogram"`, a histogram is created for each gene. As with `graph_type="boxplot"`, the `plot_type` parameter has no effect. The following 2 calls of the `plot_genes()` method create the same figure as the default value for the `solutions` parameter is `"all"`. @@ -284,13 +284,13 @@ ga_instance.plot_genes(graph_type="histogram", All the previous figures can be created for only the best solutions by setting `solutions="best"`. -# Pareto Front +## Pareto Front -## `plot_pareto_front_curve()` +### `plot_pareto_front_curve()` The `plot_pareto_front_curve()` method creates the Pareto front curve for multi-objective optimization problems. It creates, shows, and returns a figure that shows the Pareto front curve and points representing the fitness. It only works when 2 objectives are used. -It works only after completing at least 1 generation. If no generation is completed (at least 1), an exception is raised. +It works only after at least 1 generation is completed. If no generation is completed, an exception is raised. This method accepts the following parameters: @@ -300,8 +300,8 @@ This method accepts the following parameters: 4. `linewidth`: Line width of the plot. Defaults to `3`. 5. `font_size`: Font size for the labels and title. Defaults to `14`. 6. `label`: The label used for the legend. -7. `color`: Color of the plot which defaults to the royal blue color `#FF6347`. -8. `color_fitness`: Color of the fitness points which defaults to the tomato red color `#4169E1`. +7. `color`: Color of the plot which defaults to the tomato color `#FF6347`. +8. `color_fitness`: Color of the fitness points which defaults to the royal blue color `#4169E1`. 9. `grid`: Either `True` or `False` to control the visibility of the grid. 10. `alpha`: The transparency of the pareto front curve. 11. `marker`: The marker of the fitness points. @@ -313,5 +313,5 @@ This is an example of calling the `plot_pareto_front_curve()` method. ga_instance.plot_pareto_front_curve() ``` -![plot_fitness_bar](https://github.com/user-attachments/assets/606d853c-7370-41a0-8ddb-857a4c6c7fb9) +![plot_pareto_front_curve](https://github.com/user-attachments/assets/606d853c-7370-41a0-8ddb-857a4c6c7fb9) diff --git a/docs/source/visualize.rst b/docs/source/visualize.rst deleted file mode 100644 index 0f7e0c1..0000000 --- a/docs/source/visualize.rst +++ /dev/null @@ -1,511 +0,0 @@ -.. _pygadvisualize-module: - -``pygad.visualize`` Module -========================== - -This section of the PyGAD's library documentation discusses the -**pygad.visualize** module. It offers the methods for results -visualization in PyGAD. - -This section discusses the different options to visualize the results in -PyGAD through these methods: - -1. ``plot_fitness()``: Creates plots for the fitness to show how the - fitness evolves by generation. . - -2. ``plot_genes()``: Creates plots for the genes to show how the gene - value changes for each generation. - -3. ``plot_new_solution_rate()``: Creates plots for the new solution rate - to show how the number of new solutions explored in each solution. - -4. ``plot_pareto_front_curve()``: Creates plots for the pareto front for - multi-objective problems. - -In the following code, the ``save_solutions`` flag is set to ``True`` -which means all solutions are saved in the ``solutions`` attribute. The -code runs for only 10 generations. - -.. code:: python - - import pygad - import numpy - - equation_inputs = [4, -2, 3.5, 8, -2, 3.5, 8] - desired_output = 2671.1234 - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - - ga_instance = pygad.GA(num_generations=10, - sol_per_pop=10, - num_parents_mating=5, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - gene_space=[range(1, 10), range(10, 20), range(15, 30), range(20, 40), range(25, 50), range(10, 30), range(20, 50)], - gene_type=int, - save_solutions=True) - - ga_instance.run() - -Let's explore how to visualize the results by the above mentioned -methods. - -Fitness -======= - -.. _plotfitness: - -``plot_fitness()`` ------------------- - -The ``plot_fitness()`` method shows the fitness value for each -generation. It creates, shows, and returns a figure that summarizes how -the fitness value(s) evolve(s) by generation. - -It works only after completing at least 1 generation. If no generation -is completed (at least 1), an exception is raised. - -This method accepts the following parameters: - -1. ``title``: Title of the figure. - -2. ``xlabel``: X-axis label. - -3. ``ylabel``: Y-axis label. - -4. ``linewidth``: Line width of the plot. Defaults to ``3``. - -5. ``font_size``: Font size for the labels and title. Defaults to - ``14``. - -6. ``plot_type``: Type of the plot which can be either ``"plot"`` - (default), ``"scatter"``, or ``"bar"``. - -7. ``color``: Color of the plot which defaults to the greenish color - ``"#64f20c"``. - -8. ``label``: The label used for the legend in the figures of - multi-objective problems. It is not used for single-objective - problems. It defaults to ``None`` which means no labels used. - -9. ``save_dir``: Directory to save the figure. - -.. _plottypeplot: - -``plot_type="plot"`` -~~~~~~~~~~~~~~~~~~~~ - -The simplest way to call this method is as follows leaving the -``plot_type`` with its default value ``"plot"`` to create a continuous -line connecting the fitness values across all generations: - -.. code:: python - - ga_instance.plot_fitness() - # ga_instance.plot_fitness(plot_type="plot") - -|image1| - -.. _plottypescatter: - -``plot_type="scatter"`` -~~~~~~~~~~~~~~~~~~~~~~~ - -The ``plot_type`` can also be set to ``"scatter"`` to create a scatter -graph with each individual fitness represented as a dot. The size of -these dots can be changed using the ``linewidth`` parameter. - -.. code:: python - - ga_instance.plot_fitness(plot_type="scatter") - -|image2| - -.. _plottypebar: - -``plot_type="bar"`` -~~~~~~~~~~~~~~~~~~~ - -The third value for the ``plot_type`` parameter is ``"bar"`` to create a -bar graph with each individual fitness represented as a bar. - -.. code:: python - - ga_instance.plot_fitness(plot_type="bar") - -|image3| - -New Solution Rate -================= - -.. _plotnewsolutionrate: - -``plot_new_solution_rate()`` ----------------------------- - -The ``plot_new_solution_rate()`` method presents the number of new -solutions explored in each generation. This helps to figure out if the -genetic algorithm is able to find new solutions as an indication of more -possible evolution. If no new solutions are explored, this is an -indication that no further evolution is possible. - -It works only after completing at least 1 generation. If no generation -is completed (at least 1), an exception is raised. - -The ``plot_new_solution_rate()`` method accepts the same parameters as -in the ``plot_fitness()`` method (it also have 3 possible values for -``plot_type`` parameter). Here are all the parameters it accepts: - -1. ``title``: Title of the figure. - -2. ``xlabel``: X-axis label. - -3. ``ylabel``: Y-axis label. - -4. ``linewidth``: Line width of the plot. Defaults to ``3``. - -5. ``font_size``: Font size for the labels and title. Defaults to - ``14``. - -6. ``plot_type``: Type of the plot which can be either ``"plot"`` - (default), ``"scatter"``, or ``"bar"``. - -7. ``color``: Color of the plot which defaults to ``"#3870FF"``. - -8. ``save_dir``: Directory to save the figure. - -.. _plottypeplot-2: - -``plot_type="plot"`` -~~~~~~~~~~~~~~~~~~~~ - -The default value for the ``plot_type`` parameter is ``"plot"``. - -.. code:: python - - ga_instance.plot_new_solution_rate() - # ga_instance.plot_new_solution_rate(plot_type="plot") - -The next figure shows that, for example, generation 6 has the least -number of new solutions which is 4. The number of new solutions in the -first generation is always equal to the number of solutions in the -population (i.e. the value assigned to the ``sol_per_pop`` parameter in -the constructor of the ``pygad.GA`` class) which is 10 in this example. - -|image4| - -.. _plottypescatter-2: - -``plot_type="scatter"`` -~~~~~~~~~~~~~~~~~~~~~~~ - -The previous graph can be represented as scattered points by setting -``plot_type="scatter"``. - -.. code:: python - - ga_instance.plot_new_solution_rate(plot_type="scatter") - -|image5| - -.. _plottypebar-2: - -``plot_type="bar"`` -~~~~~~~~~~~~~~~~~~~ - -By setting ``plot_type="scatter"``, each value is represented as a -vertical bar. - -.. code:: python - - ga_instance.plot_new_solution_rate(plot_type="bar") - -|image6| - -Genes -===== - -.. _plotgenes: - -``plot_genes()`` ----------------- - -The ``plot_genes()`` method is the third option to visualize the PyGAD -results. The ``plot_genes()`` method creates, shows, and returns a -figure that describes each gene. It has different options to create the -figures which helps to: - -1. Explore the gene value for each generation by creating a normal plot. - -2. Create a histogram for each gene. - -3. Create a boxplot. - -It works only after completing at least 1 generation. If no generation -is completed, an exception is raised. If no generation is completed (at -least 1), an exception is raised. - -This method accepts the following parameters: - -1. ``title``: Title of the figure. - -2. ``xlabel``: X-axis label. - -3. ``ylabel``: Y-axis label. - -4. ``linewidth``: Line width of the plot. Defaults to ``3``. - -5. ``font_size``: Font size for the labels and title. Defaults to - ``14``. - -6. ``plot_type``: Type of the plot which can be either ``"plot"`` - (default), ``"scatter"``, or ``"bar"``. - -7. ``graph_type``: Type of the graph which can be either ``"plot"`` - (default), ``"boxplot"``, or ``"histogram"``. - -8. ``fill_color``: Fill color of the graph which defaults to - ``"#3870FF"``. This has no effect if ``graph_type="plot"``. - -9. ``color``: Color of the plot which defaults to ``"#3870FF"``. - -10. ``solutions``: Defaults to ``"all"`` which means use all solutions. - If ``"best"`` then only the best solutions are used. - -11. ``save_dir``: Directory to save the figure. - -This method has 3 control variables: - -1. ``graph_type="plot"``: Can be ``"plot"`` (default), ``"boxplot"``, or - ``"histogram"``. - -2. ``plot_type="plot"``: Identical to the ``plot_type`` parameter - explored in the ``plot_fitness()`` and ``plot_new_solution_rate()`` - methods. - -3. ``solutions="all"``: Can be ``"all"`` (default) or ``"best"``. - -These 3 parameters controls the style of the output figure. - -The ``graph_type`` parameter selects the type of the graph which helps -to explore the gene values as: - -1. A normal plot. - -2. A histogram. - -3. A box and whisker plot. - -The ``plot_type`` parameter works only when the type of the graph is set -to ``"plot"``. - -The ``solutions`` parameter selects whether the genes come from all -solutions in the population or from just the best solutions. - -An exception is raised if: - -- ``solutions="all"`` while ``save_solutions=False`` in the constructor - of the ``pygad.GA`` class. . - -- ``solutions="best"`` while ``save_best_solutions=False`` in the - constructor of the ``pygad.GA`` class. . - -.. _graphtypeplot: - -``graph_type="plot"`` -~~~~~~~~~~~~~~~~~~~~~ - -When ``graph_type="plot"``, then the figure creates a normal graph where -the relationship between the gene values and the generation numbers is -represented as a continuous plot, scattered points, or bars. - -.. _plottypeplot-3: - -``plot_type="plot"`` -^^^^^^^^^^^^^^^^^^^^ - -Because the default value for both ``graph_type`` and ``plot_type`` is -``"plot"``, then all of the lines below creates the same figure. This -figure is helpful to know whether a gene value lasts for more -generations as an indication of the best value for this gene. For -example, the value 16 for the gene with index 5 (at column 2 and row 2 -of the next graph) lasted for 83 generations. - -.. code:: python - - ga_instance.plot_genes() - - ga_instance.plot_genes(graph_type="plot") - - ga_instance.plot_genes(plot_type="plot") - - ga_instance.plot_genes(graph_type="plot", - plot_type="plot") - -|image7| - -As the default value for the ``solutions`` parameter is ``"all"``, then -the following method calls generate the same plot. - -.. code:: python - - ga_instance.plot_genes(solutions="all") - - ga_instance.plot_genes(graph_type="plot", - solutions="all") - - ga_instance.plot_genes(plot_type="plot", - solutions="all") - - ga_instance.plot_genes(graph_type="plot", - plot_type="plot", - solutions="all") - -.. _plottypescatter-3: - -``plot_type="scatter"`` -^^^^^^^^^^^^^^^^^^^^^^^ - -The following calls of the ``plot_genes()`` method create the same -scatter plot. - -.. code:: python - - ga_instance.plot_genes(plot_type="scatter") - - ga_instance.plot_genes(graph_type="plot", - plot_type="scatter", - solutions='all') - -|image8| - -.. _plottypebar-3: - -``plot_type="bar"`` -^^^^^^^^^^^^^^^^^^^ - -.. code:: python - - ga_instance.plot_genes(plot_type="bar") - - ga_instance.plot_genes(graph_type="plot", - plot_type="bar", - solutions='all') - -|image9| - -.. _graphtypeboxplot: - -``graph_type="boxplot"`` -~~~~~~~~~~~~~~~~~~~~~~~~ - -By setting ``graph_type`` to ``"boxplot"``, then a box and whisker graph -is created. Now, the ``plot_type`` parameter has no effect. - -The following 2 calls of the ``plot_genes()`` method create the same -figure as the default value for the ``solutions`` parameter is -``"all"``. - -.. code:: python - - ga_instance.plot_genes(graph_type="boxplot") - - ga_instance.plot_genes(graph_type="boxplot", - solutions='all') - -|image10| - -.. _graphtypehistogram: - -``graph_type="histogram"`` -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -For ``graph_type="boxplot"``, then a histogram is created for each gene. -Similar to ``graph_type="boxplot"``, the ``plot_type`` parameter has no -effect. - -The following 2 calls of the ``plot_genes()`` method create the same -figure as the default value for the ``solutions`` parameter is -``"all"``. - -.. code:: python - - ga_instance.plot_genes(graph_type="histogram") - - ga_instance.plot_genes(graph_type="histogram", - solutions='all') - -|image11| - -All the previous figures can be created for only the best solutions by -setting ``solutions="best"``. - -Pareto Front -============ - -.. _plotparetofrontcurve: - -``plot_pareto_front_curve()`` ------------------------------ - -The ``plot_pareto_front_curve()`` method creates the Pareto front curve -for multi-objective optimization problems. It creates, shows, and -returns a figure that shows the Pareto front curve and points -representing the fitness. It only works when 2 objectives are used. - -It works only after completing at least 1 generation. If no generation -is completed (at least 1), an exception is raised. - -This method accepts the following parameters: - -1. ``title``: Title of the figure. - -2. ``xlabel``: X-axis label. - -3. ``ylabel``: Y-axis label. - -4. ``linewidth``: Line width of the plot. Defaults to ``3``. - -5. ``font_size``: Font size for the labels and title. Defaults to - ``14``. - -6. ``label``: The label used for the legend. - -7. ``color``: Color of the plot which defaults to the royal blue color - ``#FF6347``. - -8. ``color_fitness``: Color of the fitness points which defaults to the - tomato red color ``#4169E1``. - -9. ``grid``: Either ``True`` or ``False`` to control the visibility of - the grid. - -10. ``alpha``: The transparency of the pareto front curve. - -11. ``marker``: The marker of the fitness points. - -12. ``save_dir``: Directory to save the figure. - -This is an example of calling the ``plot_pareto_front_curve()`` method. - -.. code:: python - - ga_instance.plot_pareto_front_curve() - -|image12| - -.. |image1| image:: https://user-images.githubusercontent.com/16560492/122472609-d02f5280-cf8e-11eb-88a7-f9366ff6e7c6.png -.. |image2| image:: https://user-images.githubusercontent.com/16560492/122473159-75e2c180-cf8f-11eb-942d-31279b286dbd.png -.. |image3| image:: https://user-images.githubusercontent.com/16560492/122473340-b7736c80-cf8f-11eb-89c5-4f7db3b653cc.png -.. |image4| image:: https://user-images.githubusercontent.com/16560492/122475815-3322e880-cf93-11eb-9648-bf66f823234b.png -.. |image5| image:: https://user-images.githubusercontent.com/16560492/122476108-adec0380-cf93-11eb-80ac-7588bf90492f.png -.. |image6| image:: https://user-images.githubusercontent.com/16560492/122476173-c2c89700-cf93-11eb-9e77-d39737cd3a96.png -.. |image7| image:: https://user-images.githubusercontent.com/16560492/122477158-4a62d580-cf95-11eb-8c93-9b6e74cb814c.png -.. |image8| image:: https://user-images.githubusercontent.com/16560492/122477273-73836600-cf95-11eb-828f-f357c7b0f815.png -.. |image9| image:: https://user-images.githubusercontent.com/16560492/122477370-99106f80-cf95-11eb-8643-865b55e6b844.png -.. |image10| image:: https://user-images.githubusercontent.com/16560492/122479260-beeb4380-cf98-11eb-8f08-23707929b12c.png -.. |image11| image:: https://user-images.githubusercontent.com/16560492/122477314-8007be80-cf95-11eb-9c95-da3f49204151.png -.. |image12| image:: https://github.com/user-attachments/assets/606d853c-7370-41a0-8ddb-857a4c6c7fb9 From c69aef941dc4f0b611522fa45a9f9c9dcf6f5ee2 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 21 May 2026 14:53:38 -0400 Subject: [PATCH 22/42] docs: plain-English pass on pygad_more.md (first half) Copyedit the gene_space, gene constraint, sample_size, stop criteria, "continue without losing progress", and "change population size" sections: fix subject-verb agreement, a code typo in a gene_space example ([... 95. 6.3 ...] -> [... 95, 6.3 ...]), "retracted" -> "restricted", num_gene -> num_genes, criterion -> criteria, and a malformed (indented) code fence. Wording of load-bearing headings is unchanged. --- docs/source/pygad_more.md | 46 +++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/source/pygad_more.md b/docs/source/pygad_more.md index fb816a8..41326fb 100644 --- a/docs/source/pygad_more.md +++ b/docs/source/pygad_more.md @@ -122,9 +122,9 @@ This is the figure created by the `plot_fitness()` method. The fitness of the fi ## Limit the Gene Value Range using the `gene_space` Parameter -In [PyGAD 2.11.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-11-0), the `gene_space` parameter supported a new feature to allow customizing the range of accepted values for each gene. Let's take a quick review of the `gene_space` parameter to build over it. +In [PyGAD 2.11.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-11-0), the `gene_space` parameter added a new feature that lets you customize the range of accepted values for each gene. Let us first review the `gene_space` parameter and build on it. -The `gene_space` parameter allows the user to feed the space of values of each gene. This way the accepted values for each gene is retracted to the user-defined values. Assume there is a problem that has 3 genes where each gene has different set of values as follows: +The `gene_space` parameter lets you set the space of values for each gene. This way, the accepted values for each gene are restricted to the user-defined values. Assume there is a problem with 3 genes, where each gene has a different set of values: 1. Gene 1: `[0.4, 12, -5, 21.2]` 2. Gene 2: `[-2, 0.3]` @@ -138,15 +138,15 @@ gene_space = [[0.4, 12, -5, 21.2], [1.2, 63.2, 7.4]] ``` -In case all genes share the same set of values, then simply feed a single list to the `gene_space` parameter as follows. In this case, all genes can only take values from this list of 6 values. +If all genes share the same set of values, then pass a single list to the `gene_space` parameter as follows. In this case, all genes can only take values from this list of 6 values. ```python -gene_space = [33, 7, 0.5, 95. 6.3, 0.74] +gene_space = [33, 7, 0.5, 95, 6.3, 0.74] ``` -The previous example restricts the gene values to just a set of fixed number of discrete values. In case you want to use a range of discrete values to the gene, then you can use the `range()` function. For example, `range(1, 7)` means the set of allowed values for the gene are `1, 2, 3, 4, 5, and 6`. You can also use the `numpy.arange()` or `numpy.linspace()` functions for the same purpose. +The previous example restricts the gene values to a fixed set of discrete values. If you want to use a range of discrete values for the gene, then you can use the `range()` function. For example, `range(1, 7)` means the allowed values for the gene are `1, 2, 3, 4, 5, and 6`. You can also use the `numpy.arange()` or `numpy.linspace()` functions for the same purpose. -The previous discussion only works with a range of discrete values not continuous values. In [PyGAD 2.11.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-11-0), the `gene_space` parameter can be assigned a dictionary that allows the gene to have values from a continuous range. +The previous examples only work with discrete values, not continuous ones. In [PyGAD 2.11.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-11-0), the `gene_space` parameter can be assigned a dictionary that allows the gene to take values from a continuous range. Assuming you want to restrict the gene within this half-open range [1 to 5) where 1 is included and 5 is not. Then simply create a dictionary with 2 items where the keys of the 2 items are: @@ -219,7 +219,7 @@ gene_space = {"low": 4, "high": 30, "step": 2.5} > Setting a `dict` like `{"low": 0, "high": 10}` in the `gene_space` means that random values from the continuous range [0, 10) are sampled. Note that `0` is included but `10` is not included while sampling. Thus, the maximum value that could be returned is less than `10` like `9.9999`. But if the user decided to round the genes using, for example, `[float, 2]`, then this value will become 10. So, the user should be careful to the inputs. -If a `None` is assigned to only a single gene, then its value will be randomly generated initially using the `init_range_low` and `init_range_high` parameters in the `pygad.GA` class's constructor. During mutation, the value are sampled from the range defined by the 2 parameters `random_mutation_min_val` and `random_mutation_max_val`. This is an example where the second gene is given a `None` value. +If a `None` is assigned to only a single gene, then its value will be randomly generated initially using the `init_range_low` and `init_range_high` parameters in the `pygad.GA` class's constructor. During mutation, the value is sampled from the range defined by the 2 parameters `random_mutation_min_val` and `random_mutation_max_val`. This is an example where the second gene is given a `None` value. ```python gene_space = [range(5), None, numpy.linspace(10, 20, 300)] @@ -259,7 +259,7 @@ Gene space: {'low': 1, 'high': 5} Solution: [1.5, 3.4] ``` -Assuming `random_mutation_min_val=-1` and `random_mutation_max_val=1`, then a random value such as `0.3` can be added to the gene(s) participating in mutation. If only the first gene is mutated, then its new value changes from `1.5` to `1.5+0.3=1.8`. Note that PyGAD verifies that the new value is within the range. In the worst scenarios, the value will be set to either boundary of the continuous range. For example, if the gene value is 1.5 and the random value is -0.55, then the new value is 0.95 which smaller than the lower boundary 1. Thus, the gene value will be rounded to 1. +Assuming `random_mutation_min_val=-1` and `random_mutation_max_val=1`, then a random value such as `0.3` can be added to the gene(s) participating in mutation. If only the first gene is mutated, then its new value changes from `1.5` to `1.5+0.3=1.8`. Note that PyGAD verifies that the new value is within the range. In the worst scenarios, the value will be set to either boundary of the continuous range. For example, if the gene value is 1.5 and the random value is -0.55, then the new value is 0.95, which is smaller than the lower boundary 1. So, the gene value will be set to 1. If the dictionary has a step like the example below, then it is considered a discrete range and mutation occurs by randomly selecting a value from the set of values. In other words, no random value is added to the gene value. @@ -273,7 +273,7 @@ In [PyGAD 3.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-5- The `gene_constraint` parameter allows the users to define constraints to be enforced (as much as possible) when selecting a value for a gene. For example, this constraint is enforced when applying mutation to make sure the new gene value after mutation meets the gene constraint. -The default value of this parameter is `None` which means no genes have constraints. It can be assigned a list but the length of this list must be equal to the number of genes as specified by the `num_gene` parameter. +The default value of this parameter is `None` which means no genes have constraints. It can be assigned a list but the length of this list must be equal to the number of genes as specified by the `num_genes` parameter. When assigned a list, the allowed values for each element are: @@ -282,7 +282,7 @@ When assigned a list, the allowed values for each element are: 1. The solution where the gene exists. 2. A list or NumPy array of candidate values for the gene. -It is the user's responsibility to build such callables to filter the passed list of values and return a new list with the values that meets the gene constraint. If no value meets the constraint, return an empty list or NumPy array. +It is the user's responsibility to build such callables to filter the passed list of values and return a new list with the values that meet the gene constraint. If no value meets the constraint, return an empty list or NumPy array. For example, if the gene must be smaller than 5, then use this callable: @@ -375,21 +375,21 @@ Sometimes 100 values is not enough and PyGAD sometimes fails to find a good valu ## Stop at Any Generation -In [PyGAD 2.4.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-4-0), it is possible to stop the genetic algorithm after any generation. All you need to do it to return the string `"stop"` in the callback function `on_generation`. When this callback function is implemented and assigned to the `on_generation` parameter in the constructor of the `pygad.GA` class, then the algorithm immediately stops after completing its current generation. Let's discuss an example. +In [PyGAD 2.4.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-4-0), it is possible to stop the genetic algorithm after any generation. All you need to do is return the string `"stop"` in the `on_generation` callback function. When this callback function is implemented and assigned to the `on_generation` parameter in the constructor of the `pygad.GA` class, the algorithm stops right after it completes its current generation. Here is an example. -Assume that the user wants to stop algorithm either after the 100 generations or if a condition is met. The user may assign a value of 100 to the `num_generations` parameter of the `pygad.GA` class constructor. +Assume the user wants to stop the algorithm either after 100 generations or when a condition is met. The user can assign a value of 100 to the `num_generations` parameter of the `pygad.GA` class constructor. The condition that stops the algorithm is written in a callback function like the one in the next code. If the fitness value of the best solution exceeds 70, then the string `"stop"` is returned. - ```python +```python def func_generation(ga_instance): if ga_instance.best_solution()[1] >= 70: return "stop" - ``` +``` ## Stop Criteria -In [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0), a new parameter named `stop_criteria` is added to the constructor of the `pygad.GA` class. It helps to stop the evolution based on some criteria. It can be assigned to one or more criterion. +In [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0), a new parameter named `stop_criteria` is added to the constructor of the `pygad.GA` class. It helps to stop the evolution based on some criteria. It can be assigned one or more criteria. Each criterion is passed as `str` that consists of 2 parts: @@ -442,9 +442,9 @@ When multi-objective is used, then there are 2 options to use the `stop_criteria 1. Pass a single value to use along the `reach` keyword to use across all the objectives. 2. Pass multiple values along the `reach` keyword. But the number of values must equal the number of objectives. -For the `saturate` keyword, it is independent to the number of objectives. +For the `saturate` keyword, it is independent of the number of objectives. -Suppose there are 3 objectives, this is a working example. It stops when the fitness value of the 3 objectives reach or exceed 10, 20, and 30, respectively. +Suppose there are 3 objectives. Here is a working example. It stops when the fitness values of the 3 objectives reach or exceed 10, 20, and 30, respectively. ```python stop_criteria='reach_10_20_30' @@ -657,23 +657,23 @@ Note that the 2 attributes (`self.best_solutions` and `self.best_solutions_fitne ## Change Population Size during Runtime -Starting from [PyGAD 3.3.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-3-0), the population size can changed during runtime. In other words, the number of solutions/chromosomes and number of genes can be changed. +Starting from [PyGAD 3.3.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-3-0), the population size can be changed during runtime. In other words, the number of solutions/chromosomes and the number of genes can be changed. The user has to carefully arrange the list of *parameters* and *instance attributes* that have to be changed to keep the GA consistent before and after changing the population size. Generally, change everything that would be used during the GA evolution. -> CAUTION: If the user failed to change a parameter or an instance attributes necessary to keep the GA running after the population size changed, errors will arise. +> CAUTION: If the user fails to change a parameter or an instance attribute that is needed to keep the GA running after the population size changes, errors will arise. These are examples of the parameters that the user should decide whether to change. The user should check the [list of parameters](https://pygad.readthedocs.io/en/latest/pygad.html#init) and decide what to change. 1. `population`: The population. It *must* be changed. -2. `num_offspring`: The number of offspring to produce out of the crossover and mutation operations. Change this parameter if the number of offspring have to be changed to be consistent with the new population size. -3. `num_parents_mating`: The number of solutions to select as parents. Change this parameter if the number of parents have to be changed to be consistent with the new population size. -4. `fitness_func`: If the way of calculating the fitness changes after the new population size, then the fitness function have to be changed. +2. `num_offspring`: The number of offspring to produce from the crossover and mutation operations. Change this parameter if the number of offspring has to change to match the new population size. +3. `num_parents_mating`: The number of solutions to select as parents. Change this parameter if the number of parents has to change to match the new population size. +4. `fitness_func`: If the way of calculating the fitness changes with the new population size, then the fitness function has to be changed. 5. `sol_per_pop`: The number of solutions per population. It is not critical to change it but it is recommended to keep this number consistent with the number of solutions in the `population` parameter. These are examples of the instance attributes that might be changed. The user should check the [list of instance attributes](https://pygad.readthedocs.io/en/latest/pygad.html#other-instance-attributes-methods) and decide what to change. -1. All the `last_generation_*` parameters +1. All the `last_generation_*` attributes 1. `last_generation_fitness`: A 1D NumPy array of fitness values of the population. 2. `last_generation_parents` and `last_generation_parents_indices`: Two NumPy arrays: 2D array representing the parents and 1D array of the parents indices. 3. `last_generation_elitism` and `last_generation_elitism_indices`: Must be changed if `keep_elitism != 0`. The default value of `keep_elitism` is 1. Two NumPy arrays: 2D array representing the elitism and 1D array of the elitism indices. From 7431422e794e66fdefbf7939953da80a5492b8f0 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 21 May 2026 15:01:52 -0400 Subject: [PATCH 23/42] docs: finish plain-English pass on pygad_more.md (second half) Copyedit the duplicate-prevention, gene_type, parallel processing, print-lifecycle-summary, logging, non-deterministic, reuse-fitness, and batch-fitness sections. Notable fixes: wrong parameter names (keep_solutions/keep_best_solutions -> save_solutions/save_best_solutions), a version anchor mismatch (third-gene fix is 3.1.0, not 3.0.1), patch -> batch, ga_instanse -> ga_instance, "the the" / "A a" duplications, and several subject-verb agreement errors. Load-bearing headings unchanged. --- docs/source/pygad_more.md | 120 +++++++++++++++++++------------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/docs/source/pygad_more.md b/docs/source/pygad_more.md index 41326fb..6e3dfe1 100644 --- a/docs/source/pygad_more.md +++ b/docs/source/pygad_more.md @@ -747,7 +747,7 @@ Generation 5 [-4 2 -1 1]] ``` -The `allow_duplicate_genes` parameter is configured with use with the `gene_space` parameter. Here is an example where each of the 4 genes has the same space of values that consists of 4 values (1, 2, 3, and 4). +The `allow_duplicate_genes` parameter can be used together with the `gene_space` parameter. Here is an example where each of the 4 genes has the same space of 4 values (1, 2, 3, and 4). ```python import pygad @@ -772,7 +772,7 @@ ga_instance = pygad.GA(num_generations=1, ga_instance.run() ``` -Even that all the genes share the same space of values, no 2 genes duplicate their values as provided by the next output. +Even though all the genes share the same space of values, no 2 genes have the same value, as shown in the next output. ```python Generation 1 @@ -807,19 +807,19 @@ Generation 5 [1 2 4 3]] ``` -You should care of giving enough values for the genes so that PyGAD is able to find alternatives for the gene value in case it duplicates with another gene. +You should give enough values for the genes so that PyGAD can find an alternative when a gene value duplicates another gene. -If PyGAD failed to find a unique gene while there is still room to find a unique value, one possible option is to set the `sample_size` parameter to a larger value. Check the [sample_size Parameter](https://pygad.readthedocs.io/en/latest/pygad_more.html#sample-size-parameter) section for more information. +If PyGAD fails to find a unique gene value while there is still room to find one, then set the `sample_size` parameter to a larger value. Check the [sample_size Parameter](https://pygad.readthedocs.io/en/latest/pygad_more.html#sample-size-parameter) section for more information. ### Limitation There might be 2 duplicate genes where changing either of the 2 duplicating genes will not solve the problem. For example, if `gene_space=[[3, 0, 1], [4, 1, 2], [0, 2], [3, 2, 0]]` and the solution is `[3 2 0 0]`, then the values of the last 2 genes duplicate. There are no possible changes in the last 2 genes to solve the problem. -This problem can be solved by randomly changing one of the non-duplicating genes that may make a room for a unique value in one the 2 duplicating genes. For example, by changing the second gene from 2 to 4, then any of the last 2 genes can take the value 2 and solve the duplicates. The resultant gene is then `[3 4 2 0]`. But this option is not yet supported in PyGAD. +This problem can be solved by randomly changing one of the non-duplicating genes to make room for a unique value in one of the 2 duplicating genes. For example, by changing the second gene from 2 to 4, then any of the last 2 genes can take the value 2 and solve the duplicates. The resultant gene is then `[3 4 2 0]`. But this option is not yet supported in PyGAD. ### Solve Duplicates using a Third Gene -When `allow_duplicate_genes=False` and a user-defined `gene_space` is used, it sometimes happen that there is no room to solve the duplicates between the 2 genes by simply replacing the value of one gene by another gene. In [PyGAD 3.1.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-0-1), the duplicates are solved by looking for a third gene that will help in solving the duplicates. The following examples explain how it works. +When `allow_duplicate_genes=False` and a user-defined `gene_space` is used, it sometimes happens that there is no room to solve the duplicates between the 2 genes by simply replacing the value of one gene with another. In [PyGAD 3.1.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-1-0), the duplicates are solved by looking for a third gene that helps solve them. The following examples explain how it works. Example 1: @@ -833,9 +833,9 @@ Gene space: [[2, 3], Solution: [3, 4, 4, 5] ``` -By checking the gene space, the second gene can have the values `[3, 4]` and the third gene can have the values `[4, 5]`. To solve the duplicates, we have the value of any of these 2 genes. +By checking the gene space, the second gene can have the values `[3, 4]` and the third gene can have the values `[4, 5]`. To solve the duplicates, we change the value of one of these 2 genes. -If the value of the second gene changes from 4 to 3, then it will be duplicate with the first gene. If we are to change the value of the third gene from 4 to 5, then it will duplicate with the fourth gene. As a conclusion, trying to just selecting a different gene value for either the second or third genes will introduce new duplicating genes. +If the value of the second gene changes from 4 to 3, then it will duplicate the first gene. If we change the value of the third gene from 4 to 5, then it will duplicate the fourth gene. In short, simply selecting a different value for either the second or third gene will introduce new duplicate genes. When there are 2 duplicate genes but there is no way to solve their duplicates, then the solution is to change a third gene that makes a room to solve the duplicates between the 2 genes. @@ -851,14 +851,14 @@ Generally, this is how to solve such duplicates: 3. Find if **GENEX** can have another value that will not cause any more duplicates. If so, go to step 7. 4. If all the other values of **GENEX** will cause duplicates, then try another gene **GENEY**. 5. Repeat steps 3 and 4 until exploring all the genes. -6. If there is no possibility to solve the duplicates, then there is not way to solve the duplicates and we have to keep the duplicate value. +6. If there is no way to solve the duplicates, then we have to keep the duplicate value. 7. If a value for a gene **GENEM** is found that will not cause more duplicates, then use this value for the gene **GENEM**. 8. Replace the value of the gene **GENE1** by the old value of the gene **GENEM**. This solves the duplicates. This is an example to solve the duplicate for the solution `[3, 4, 4, 5]`: 1. Let's use the second gene with value 4. Because the space of this gene is `[3, 4]`, then the only other value we can select is 3. -2. The first gene also have the value 3. +2. The first gene also has the value 3. 3. The first gene has another value 2 that will not cause more duplicates in the solution. Then go to step 7. 4. Skip. 5. Skip. @@ -888,7 +888,7 @@ The `gene_type` parameter allows the user to control the data type for all genes 1. Select a data type for all genes with or without precision. 2. Select a data type for each individual gene with or without precision. -Let's discuss things by examples. +Let us look at some examples. ### Data Type for All Genes without Precision @@ -956,7 +956,7 @@ A precision can only be specified for a `float` data type and cannot be specifie gene_type=[float, 3] ``` -The next code uses prints the initial and final population where the genes are of type `float` with precision 3. +The next code prints the initial and final population where the genes are of type `float` with precision 3. ```python import pygad @@ -1115,9 +1115,9 @@ Final Population ## Parallel Processing in PyGAD -Starting from [PyGAD 2.17.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-17-0), parallel processing becomes supported. This section explains how to use parallel processing in PyGAD. +Starting from [PyGAD 2.17.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-17-0), parallel processing is supported. This section explains how to use parallel processing in PyGAD. -According to the [PyGAD lifecycle](https://pygad.readthedocs.io/en/latest/pygad.html#life-cycle-of-pygad), parallel processing can be parallelized in only 2 operations: +According to the [PyGAD life cycle](https://pygad.readthedocs.io/en/latest/pygad.html#life-cycle-of-pygad), the computation can be parallelized in only 2 operations: 1. Population fitness calculation. 2. Mutation. @@ -1126,13 +1126,13 @@ The reason is that the calculations in these 2 operations are independent (i.e. For the mutation operation, it does not do intensive calculations on the CPU. Its calculations are simple like flipping the values of some genes from 0 to 1 or adding a random value to some genes. So, it does not take much CPU processing time. Experiments proved that parallelizing the mutation operation across the solutions increases the time instead of reducing it. This is because running multiple processes or threads adds overhead to manage them. Thus, parallel processing cannot be applied on the mutation operation. -For the population fitness calculation, parallel processing can help make a difference and reduce the processing time. But this is conditional on the type of calculations done in the fitness function. If the fitness function makes intensive calculations and takes much processing time from the CPU, then it is probably that parallel processing will help to cut down the overall time. +For the population fitness calculation, parallel processing can make a difference and reduce the processing time. But this depends on the type of calculations done in the fitness function. If the fitness function makes intensive calculations and takes much CPU time, then parallel processing will probably help cut down the overall time. -This section explains how parallel processing works in PyGAD and how to use parallel processing in PyGAD +This section explains how parallel processing works in PyGAD and how to use it. ### How to Use Parallel Processing in PyGAD -Starting from [PyGAD 2.17.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-17-0), a new parameter called `parallel_processing` added to the constructor of the `pygad.GA` class. +Starting from [PyGAD 2.17.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-17-0), a new parameter called `parallel_processing` was added to the constructor of the `pygad.GA` class. ```python import pygad @@ -1151,13 +1151,13 @@ This parameter allows the user to do the following: These are 3 possible values for the `parallel_processing` parameter: 1) `None`: (Default) It means no parallel processing is used. -2) A positive integer referring to the number of threads to be used (i.e. threads, not processes, are used. +2) A positive integer referring to the number of threads to be used (threads, not processes). 3) `list`/`tuple`: If a list or a tuple of exactly 2 elements is assigned, then: 1) The first element can be either `'process'` or `'thread'` to specify whether processes or threads are used, respectively. 2) The second element can be: 1) A positive integer to select the maximum number of processes or threads to be used 2) `0` to indicate that 0 processes or threads are used. It means no parallel processing. This is identical to setting `parallel_processing=None`. - 3) `None` to use the default value as calculated by the `concurrent.futures module`. + 3) `None` to use the default value as calculated by the `concurrent.futures` module. These are examples of the values assigned to the `parallel_processing` parameter: @@ -1168,7 +1168,7 @@ These are examples of the values assigned to the `parallel_processing` parameter ### Examples -The examples will help you know the difference between using processes and threads. Moreover, it will give an idea when parallel processing would make a difference and reduce the time. These are dummy examples where the fitness function is made to always return 0. +These examples will help you see the difference between using processes and threads. They also give an idea of when parallel processing makes a difference and reduces the time. These are dummy examples where the fitness function always returns 0. The first example uses 10 genes, 5 solutions in the population where only 3 solutions mate, and 9999 generations. The fitness function uses a `for` loop with 100 iterations just to have some calculations. In the constructor of the `pygad.GA` class, `parallel_processing=None` means no parallel processing is used. @@ -1200,7 +1200,7 @@ if __name__ == '__main__': When parallel processing is not used, the time it takes to run the genetic algorithm is `1.5` seconds. -In the comparison, let's do a second experiment where parallel processing is used with 5 threads. In this case, it take `5` seconds. +For comparison, let us run a second experiment where parallel processing is used with 5 threads. In this case, it takes `5` seconds. ```python ... @@ -1271,7 +1271,7 @@ ga_instance = pygad.GA(..., ... ``` -Based on the second example, using parallel processing with 10 processes takes the least time because there is much CPU work done. Generally, processes are preferred over threads when most of the work in on the CPU. Threads are preferred over processes in some situations like doing input/output operations. +Based on the second example, using parallel processing with 10 processes takes the least time because there is a lot of CPU work. Generally, processes are preferred over threads when most of the work is on the CPU. Threads are preferred over processes in some situations, like doing input/output operations. *Before releasing [PyGAD 2.17.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-17-0), [László Fazekas](https://www.linkedin.com/in/l%C3%A1szl%C3%B3-fazekas-2429a912) wrote an article to parallelize the fitness function with PyGAD. Check it: [How Genetic Algorithms Can Compete with Gradient Descent and Backprop](https://hackernoon.com/how-genetic-algorithms-can-compete-with-gradient-descent-and-backprop-9m9t33bq)*. @@ -1289,7 +1289,7 @@ In [PyGAD 2.19.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-1 - `print_step_parameters=True`: Whether to print extra parameters about each step inside the step. If `print_step_parameters=False` and `print_parameters_summary=True`, then the parameters of each step are printed at the end of the table. - `print_parameters_summary=True`: Whether to print parameters summary at the end of the table. If `print_step_parameters=False`, then the parameters of each step are printed at the end of the table too. -This is a quick example to create a PyGAD example. +Here is a quick example. ```python import pygad @@ -1322,7 +1322,7 @@ ga_instance = pygad.GA(num_generations=100, fitness_func=genetic_fitness) ``` -Then call the `summary()` method to print the summary with the default parameters. Note that entries for the crossover and generation callback function are created because their callback functions are implemented through the `on_crossover_callback()` and `on_gen()`, respectively. +Then call the `summary()` method to print the summary with the default parameters. Note that entries for the crossover and generation callbacks are created because they are implemented through `on_crossover_callback()` and `on_gen()`, respectively. ```python ga_instance.summary() @@ -1396,7 +1396,7 @@ On Generation on_gen() None ## Logging Outputs -In [PyGAD 3.0.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-0-0), the `print()` statement is no longer used and the outputs are printed using the [logging](https://docs.python.org/3/library/logging.html) module. A a new parameter called `logger` is supported to accept the user-defined logger. +In [PyGAD 3.0.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-0-0), the `print()` statement is no longer used and the outputs are printed using the [logging](https://docs.python.org/3/library/logging.html) module. A new parameter called `logger` is supported to accept a user-defined logger. ```python import logging @@ -1410,10 +1410,10 @@ ga_instance = pygad.GA(..., The default value for this parameter is `None`. If there is no logger passed (i.e. `logger=None`), then a default logger is created to log the messages to the console exactly like how the `print()` statement works. -Some advantages of using the the [logging](https://docs.python.org/3/library/logging.html) module instead of the `print()` statement are: +Some advantages of using the [logging](https://docs.python.org/3/library/logging.html) module instead of the `print()` statement are: -1. The user has more control over the printed messages specially if there is a project that uses multiple modules where each module prints its messages. A logger can organize the outputs. -2. Using the proper `Handler`, the user can log the output messages to files and not only restricted to printing it to the console. So, it is much easier to record the outputs. +1. The user has more control over the printed messages, especially in a project that uses multiple modules where each module prints its messages. A logger can organize the outputs. +2. Using the proper `Handler`, the user can log the output messages to files, not only to the console. So, it is much easier to record the outputs. 3. The format of the printed messages can be changed by customizing the `Formatter` assigned to the Logger. This section gives some quick examples to use the `logging` module and then gives an example to use the logger with PyGAD. @@ -1497,7 +1497,7 @@ This is another example to log the messages to a file named `logfile.txt`. The f 2. The log level. 3. The message. 4. The path of the file. -5. The lone number of the log message. +5. The line number of the log message. ```python import logging @@ -1515,7 +1515,7 @@ file_handler.setFormatter(file_format) logger.addHandler(file_handler) ``` -This is how the outputs look like. +This is what the outputs look like. ```python 2023-04-03 18:54:03 DEBUG: Debug message. - c:\users\agad069\desktop\logger\example2.py:46 @@ -1647,9 +1647,9 @@ By executing this code, the logged messages are printed to the console and also ## Solve Non-Deterministic Problems -PyGAD can be used to solve both deterministic and non-deterministic problems. Deterministic are those that return the same fitness for the same solution. For non-deterministic problems, a different fitness value would be returned for the same solution. +PyGAD can be used to solve both deterministic and non-deterministic problems. Deterministic problems are those that return the same fitness for the same solution. For non-deterministic problems, a different fitness value may be returned for the same solution. -By default, PyGAD settings are set to solve deterministic problems. PyGAD can save the explored solutions and their fitness to reuse in the future. These instances attributes can save the solutions: +By default, PyGAD settings are set to solve deterministic problems. PyGAD can save the explored solutions and their fitness to reuse them in the future. These instance attributes can save the solutions: 1. `solutions`: Exists if `save_solutions=True`. 2. `best_solutions`: Exists if `save_best_solutions=True`. @@ -1660,8 +1660,8 @@ To configure PyGAD for non-deterministic problems, we have to disable saving the 1. `keep_elitism=0` 2. `keep_parents=0` -3. `keep_solutions=False` -4. `keep_best_solutions=False` +3. `save_solutions=False` +4. `save_best_solutions=False` ```python import pygad @@ -1674,13 +1674,13 @@ ga_instance = pygad.GA(..., ...) ``` -This way PyGAD will not save any explored solution and thus the fitness function have to be called for each individual solution. +This way, PyGAD will not save any explored solution, so the fitness function has to be called for each individual solution. ## Reuse the Fitness instead of Calling the Fitness Function It may happen that a previously explored solution in generation X is explored again in another generation Y (where Y > X). For some problems, calling the fitness function takes much time. -For deterministic problems, it is better to not call the fitness function for an already explored solutions. Instead, reuse the fitness of the old solution. PyGAD supports some options to help you save time calling the fitness function for a previously explored solution. +For deterministic problems, it is better not to call the fitness function for an already explored solution. Instead, reuse the fitness of the old solution. PyGAD supports some options to help you save the time of calling the fitness function for a previously explored solution. The parameters explored in this section can be set in the constructor of the `pygad.GA` class. @@ -1700,7 +1700,7 @@ It accepts an integer and defaults to 1. If set to a positive integer, then it k ### 4. `keep_parents` -It accepts an integer and defaults to -1. It set to `-1` or a positive integer, then it keeps the parents of one generation available in the next generation. +It accepts an integer and defaults to -1. If set to `-1` or a positive integer, then it keeps the parents of one generation available in the next generation. ## Why the Fitness Function is not Called for Solution at Index 0? @@ -1712,9 +1712,9 @@ ga_instance = pygad.GA(..., ...) ``` -The best solutions are copied at the beginning of the population. If `keep_elitism=1`, this means the best solution in generation X is kept in the next generation X+1 at index 0 of the population. If `keep_elitism=2`, this means the 2 best solutions in generation X are kept in the next generation X+1 at indices 0 and 1 of the population of generation 1. +The best solutions are copied at the beginning of the population. If `keep_elitism=1`, this means the best solution in generation X is kept in the next generation X+1 at index 0 of the population. If `keep_elitism=2`, this means the 2 best solutions in generation X are kept in the next generation X+1 at indices 0 and 1 of the population. -Because the fitness of these best solutions are already calculated in generation X, then their fitness values will not be recalculated at generation X+1 (i.e. the fitness function will not be called for these solutions again). Instead, their fitness values are just reused. This is why you see that no solution with index 0 is passed to the fitness function. +Because the fitness values of these best solutions are already calculated in generation X, they are not recalculated at generation X+1 (the fitness function is not called for these solutions again). Instead, their fitness values are reused. This is why no solution with index 0 is passed to the fitness function. To force calling the fitness function for each solution in every generation, consider setting `keep_elitism` and `keep_parents` to 0. Moreover, keep the 2 parameters `save_solutions` and `save_best_solutions` to their default value `False`. @@ -1731,7 +1731,7 @@ ga_instance = pygad.GA(..., ## Batch Fitness Calculation -In [PyGAD 2.19.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-19-0), a new optional parameter called `fitness_batch_size` is supported. A new optional parameter called `fitness_batch_size` is supported to calculate the fitness function in batches. Thanks to [Linan Qiu](https://github.com/linanqiu) for opening the [GitHub issue #136](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/136). +In [PyGAD 2.19.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-19-0), a new optional parameter called `fitness_batch_size` is supported to calculate the fitness function in batches. Thanks to [Linan Qiu](https://github.com/linanqiu) for opening the [GitHub issue #136](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/136). Its values can be: @@ -1789,7 +1789,7 @@ print(number_of_calls) ### Example with `fitness_batch_size` Parameter -This is an example where the `fitness_batch_size` parameter is used and assigned the value `4`. This means the solutions will be grouped into batches of `4` solutions. The fitness function will be called once for each patch (i.e. called once for each 4 solutions). +This is an example where the `fitness_batch_size` parameter is used and assigned the value `4`. This means the solutions will be grouped into batches of `4` solutions. The fitness function will be called once for each batch (called once for every 4 solutions). This is an example of the arguments passed to it: @@ -1803,7 +1803,7 @@ solutions_indices: [16, 17, 18, 19] ``` -As we have 20 solutions, then there are `20/4 = 5` patches. As a result, the fitness function is called only 5 times per generation instead of 20. For each call to the fitness function, it receives a batch of 4 solutions. +As we have 20 solutions, then there are `20/4 = 5` batches. As a result, the fitness function is called only 5 times per generation instead of 20. For each call, the fitness function receives a batch of 4 solutions. As we have 5 generations, then the function will be called `5*5 = 25` times. Given the call to the fitness function after the last generation, then the total number of calls is `5*5 + 5 = 30`. @@ -1858,7 +1858,7 @@ In PyGAD 2.19.0, it is possible to pass user-defined functions or methods to the 7. `on_generation` 8. `on_stop` -This section gives 2 examples to assign these parameters user-defined: +This section gives 2 examples of how to build these handlers using: 1. Functions. 2. Methods. @@ -1871,28 +1871,28 @@ This is a dummy example where the fitness function returns a random value. Note import pygad import numpy -def fitness_func(ga_instanse, solution, solution_idx): +def fitness_func(ga_instance, solution, solution_idx): return numpy.random.rand() -def on_start(ga_instanse): +def on_start(ga_instance): print("on_start") -def on_fitness(ga_instanse, last_gen_fitness): +def on_fitness(ga_instance, last_gen_fitness): print("on_fitness") -def on_parents(ga_instanse, last_gen_parents): +def on_parents(ga_instance, last_gen_parents): print("on_parents") -def on_crossover(ga_instanse, last_gen_offspring): +def on_crossover(ga_instance, last_gen_offspring): print("on_crossover") -def on_mutation(ga_instanse, last_gen_offspring): +def on_mutation(ga_instance, last_gen_offspring): print("on_mutation") -def on_generation(ga_instanse): +def on_generation(ga_instance): print("on_generation\n") -def on_stop(ga_instanse, last_gen_fitness): +def on_stop(ga_instance, last_gen_fitness): print("on_stop") ga_instance = pygad.GA(num_generations=5, @@ -1913,7 +1913,7 @@ ga_instance.run() ### Assign Methods -The next example has all the method defined inside the class `Test`. All of the methods accept an additional parameter representing the method's object of the class `Test`. +The next example has all the methods defined inside the class `Test`. All of the methods accept an additional parameter representing the method's object of the class `Test`. All methods accept `self` as the first parameter and the instance of the `pygad.GA` class as the last parameter. @@ -1922,28 +1922,28 @@ import pygad import numpy class Test: - def fitness_func(self, ga_instanse, solution, solution_idx): + def fitness_func(self, ga_instance, solution, solution_idx): return numpy.random.rand() - def on_start(self, ga_instanse): + def on_start(self, ga_instance): print("on_start") - def on_fitness(self, ga_instanse, last_gen_fitness): + def on_fitness(self, ga_instance, last_gen_fitness): print("on_fitness") - def on_parents(self, ga_instanse, last_gen_parents): + def on_parents(self, ga_instance, last_gen_parents): print("on_parents") - def on_crossover(self, ga_instanse, last_gen_offspring): + def on_crossover(self, ga_instance, last_gen_offspring): print("on_crossover") - def on_mutation(self, ga_instanse, last_gen_offspring): + def on_mutation(self, ga_instance, last_gen_offspring): print("on_mutation") - def on_generation(self, ga_instanse): + def on_generation(self, ga_instance): print("on_generation\n") - def on_stop(self, ga_instanse, last_gen_fitness): + def on_stop(self, ga_instance, last_gen_fitness): print("on_stop") ga_instance = pygad.GA(num_generations=5, From 59d68d729f9219814377087418edf9a479554986 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 21 May 2026 15:05:56 -0400 Subject: [PATCH 24/42] docs: plain-English pass on utils.md (engine, validation, mutation, parent selection) Fix a default-value error (initialize_population high default -4 -> +4), "<= that the" -> "<= the", "the the" duplication, "andom_mutation_min_val" -> "random_mutation_min_val", a calulates typo, "method behavior" -> "method's behavior", and wordy phrasing. Load-bearing headings unchanged. --- docs/source/utils.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/source/utils.md b/docs/source/utils.md index 04c50dc..af1a04f 100644 --- a/docs/source/utils.md +++ b/docs/source/utils.md @@ -38,19 +38,19 @@ It creates an initial population randomly as a NumPy array. The array is saved i Accepts the following parameters: - `low`: The lower value of the random range from which the gene values in the initial population are selected. It defaults to -4. Available in PyGAD 1.0.20 and higher. -- `high`: The upper value of the random range from which the gene values in the initial population are selected. It defaults to -4. Available in PyGAD 1.0.20. +- `high`: The upper value of the random range from which the gene values in the initial population are selected. It defaults to +4. Available in PyGAD 1.0.20 and higher. This method assigns the values of the following 3 instance attributes: 1. `pop_size`: Size of the population. -2. `population`: Initially, it holds the initial population and later updated after each generation. -3. `initial_population`: Keeping the initial population. +2. `population`: Initially, it holds the initial population and is later updated after each generation. +3. `initial_population`: Holds the initial population. ### `cal_pop_fitness()` The `cal_pop_fitness()` method calculates and returns the fitness values of the solutions in the current population. -This function is optimized to save time by making fewer calls the fitness function. It follows this process: +This function is optimized to save time by making fewer calls to the fitness function. It follows this process: 1. If the `save_solutions` parameter is set to `True`, then it checks if the solution is already explored and saved in the `solutions` instance attribute. If so, then it just retrieves its fitness from the `solutions_fitness` instance attribute without calling the fitness function. 2. If `save_solutions` is set to `False` or if it is `True` but the solution was not explored yet, then the `cal_pop_fitness()` method checks if the `keep_elitism` parameter is set to a positive integer. If so, then it checks if the solution is saved into the `last_generation_elitism` instance attribute. If so, then it retrieves its fitness from the `previous_generation_fitness` instance attribute. @@ -70,14 +70,14 @@ Runs the genetic algorithm. This is the main method in which the genetic algorit For each generation, the fitness values of all solutions within the population are calculated according to the `cal_pop_fitness()` method which internally just calls the function assigned to the `fitness_func` parameter in the `pygad.GA` class constructor for each solution. -According to the fitness values of all solutions, the parents are selected using the `select_parents()` method. This method behavior is determined according to the parent selection type in the `parent_selection_type` parameter in the `pygad.GA` class constructor +According to the fitness values of all solutions, the parents are selected using the `select_parents()` method. This method's behavior is determined by the parent selection type in the `parent_selection_type` parameter in the `pygad.GA` class constructor. Based on the selected parents, offspring are generated by applying the crossover and mutation operations using the `crossover()` and `mutation()` methods. The behavior of such 2 methods is defined according to the `crossover_type` and `mutation_type` parameters in the `pygad.GA` class constructor. After the generation completes, the following takes place: - The `population` attribute is updated by the new population. -- The `generations_completed` attribute is assigned by the number of the last completed generation. +- The `generations_completed` attribute is assigned the number of the last completed generation. - If there is a callback function assigned to the `on_generation` attribute, then it will be called. After the `run()` method completes, the following takes place: @@ -226,26 +226,26 @@ Applies the scramble mutation which selects a subset of genes and shuffles their #### `adaptive_mutation()` -Applies the adaptive mutation which selects the number/percentage of genes to mutate based on the solution's fitness. If the fitness is high (i.e. solution quality is high), then small number/percentage of genes is mutated compared to a solution with a low fitness. +Applies the adaptive mutation, which selects the number/percentage of genes to mutate based on the solution's fitness. If the fitness is high (the solution quality is high), then a smaller number/percentage of genes is mutated compared to a solution with low fitness. ### Mutation Helper Methods The `pygad.utils.mutation` module has some helper methods to assist applying the mutation operation: 1. `mutation_by_space()`: Applies the mutation using the `gene_space` parameter. -2. `mutation_probs_by_space()`: Uses the mutation probabilities in the `mutation_probabilities` instance attribute to apply the mutation using the `gene_space` parameter. For each gene, if its probability is <= that the mutation probability, then it will be mutated based on the mutation space. +2. `mutation_probs_by_space()`: Uses the mutation probabilities in the `mutation_probabilities` instance attribute to apply the mutation using the `gene_space` parameter. For each gene, if its probability is <= the mutation probability, then it will be mutated based on the gene space. 3. `mutation_process_gene_value()`: Generate/select values for the gene that satisfy the constraint. The values could be generated randomly or from the gene space. 4. `mutation_randomly()`: Applies the random mutation. -5. `mutation_probs_randomly()`: Uses the mutation probabilities in the `mutation_probabilities` instance attribute to apply the random mutation. For each gene, if its probability is <= that the mutation probability, then it will be mutated randomly. +5. `mutation_probs_randomly()`: Uses the mutation probabilities in the `mutation_probabilities` instance attribute to apply the random mutation. For each gene, if its probability is <= the mutation probability, then it will be mutated randomly. 6. `adaptive_mutation_population_fitness()`: A helper method to calculate the average fitness of the solutions before applying the adaptive mutation. 7. `adaptive_mutation_by_space()`: Applies the adaptive mutation based on the `gene_space` parameter. A number of genes are selected randomly for mutation. This number depends on the fitness of the solution. The random values are selected from the `gene_space` parameter. 8. `adaptive_mutation_probs_by_space()`: Uses the mutation probabilities to decide which genes to apply the adaptive mutation by space. -9. `adaptive_mutation_randomly()`: Applies the adaptive mutation based on randomly. A number of genes are selected randomly for mutation. This number depends on the fitness of the solution. The random values are selected based on the 2 parameters `andom_mutation_min_val` and `random_mutation_max_val`. +9. `adaptive_mutation_randomly()`: Applies the adaptive mutation randomly. A number of genes are selected randomly for mutation. This number depends on the fitness of the solution. The random values are selected based on the 2 parameters `random_mutation_min_val` and `random_mutation_max_val`. 10. `adaptive_mutation_probs_randomly()`: Uses the mutation probabilities to decide which genes to apply the adaptive mutation randomly. ## Adaptive Mutation -In the regular genetic algorithm, the mutation works by selecting a single fixed mutation rate for all solutions regardless of their fitness values. So, regardless on whether this solution has high or low quality, the same number of genes are mutated all the time. +In the regular genetic algorithm, mutation uses a single fixed mutation rate for all solutions, regardless of their fitness values. So, no matter whether a solution has high or low quality, the same number of genes is mutated every time. The pitfalls of using a constant mutation rate for all solutions are summarized in this paper [Libelli, S. Marsili, and P. Alba. "Adaptive mutation in genetic algorithms." *Soft computing* 4.2 (2000): 76-80](https://idp.springer.com/authorize/casa?redirect_uri=https://link.springer.com/content/pdf/10.1007/s005000000042.pdf&casa_token=IT4NfJUvslcAAAAA:VegHW6tm2fe3e0R9cRKjuGKkKWXJTQSfNMT6z0kGbMsAllyK1NrEY3cEWg8bj7AJWEQPaqWIJxmHNBHg) as follows: @@ -264,7 +264,7 @@ Adaptive mutation works as follows: 3. If `ff_avg`, then this solution is regarded as a high-quality solution and thus the mutation rate should be kept low to avoid disrupting this high quality solution. -In PyGAD, if `f=f_avg`, then the solution is regarded of high quality. +In PyGAD, if `f=f_avg`, then the solution is regarded as high quality. The next figure summarizes the previous steps. @@ -329,7 +329,7 @@ function_inputs = [4,-2,3.5,5,-11,-4.7] # Function inputs. desired_output = 44 # Function output. def fitness_func(ga_instance, solution, solution_idx): - # The fitness function calulates the sum of products between each input and its corresponding weight. + # The fitness function calculates the sum of products between each input and its corresponding weight. output = numpy.sum(solution*function_inputs) # The value 0.000001 is used to avoid the Inf value when the denominator numpy.abs(output - desired_output) is 0.0. fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) @@ -352,11 +352,11 @@ ga_instance.plot_fitness(title="PyGAD with Adaptive Mutation", linewidth=5) ## `pygad.utils.parent_selection` Submodule -The `pygad.utils.parent_selection` module has a class named `ParentSelection` with the supported parent selection operations which are: +The `pygad.utils.parent_selection` module has a class named `ParentSelection` with the supported parent selection operations: 1. Steady-state: Implemented using the `steady_state_selection()` method. 2. Roulette wheel: Implemented using the `roulette_wheel_selection()` method. -3. Stochastic universal: Implemented using the `stochastic_universal_selection()`method. +3. Stochastic universal: Implemented using the `stochastic_universal_selection()` method. 4. Rank: Implemented using the `rank_selection()` method. 5. Random: Implemented using the `random_selection()` method. 6. Tournament: Implemented using the `tournament_selection()` method. @@ -426,7 +426,7 @@ The `pygad.utils.nsga2` module has a class named `NSGA2` that implements NSGA-II ## User-Defined Crossover, Mutation, and Parent Selection Operators -Previously, the user can select the the type of the crossover, mutation, and parent selection operators by assigning the name of the operator to the following parameters of the `pygad.GA` class's constructor: +Previously, the user could select the type of the crossover, mutation, and parent selection operators by assigning the name of the operator to the following parameters of the `pygad.GA` class's constructor: 1. `crossover_type` 2. `mutation_type` @@ -460,7 +460,7 @@ ga_instance.run() ga_instance.plot_fitness() ``` -This section describes the expected input parameters and outputs. For simplicity, all of these custom functions all accept the instance of the `pygad.GA` class as the last parameter. +This section describes the expected input parameters and outputs. For simplicity, all of these custom functions accept the instance of the `pygad.GA` class as the last parameter. ### User-Defined Crossover Operator @@ -564,11 +564,11 @@ Note that there are other things to take into consideration like: and more. -It all depends on your objective from building the mutation function. You may neglect or consider some of the considerations according to your objective. +It all depends on your goal in building the mutation function. You may ignore or apply some of these points depending on your goal. ### User-Defined Parent Selection Operator -No much to mention about building a user-defined parent selection function as things are similar to building a crossover or mutation function. Just create a Python function that accepts 3 parameters: +There is not much to add about building a user-defined parent selection function, as it is similar to building a crossover or mutation function. Just create a Python function that accepts 3 parameters: 1. The fitness values of the current population. 2. The number of parents needed. @@ -620,7 +620,7 @@ ga_instance = pygad.GA(num_generations=10, ### Example -By discussing how to customize the 3 operators, the next code uses the previous 3 user-defined functions instead of the built-in functions. +Now that we have seen how to customize the 3 operators, the next code uses the previous 3 user-defined functions instead of the built-in ones. ```python import pygad From 54f1399f474b9ea95a3ff8c6f873b44457e9e8e9 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 21 May 2026 15:07:34 -0400 Subject: [PATCH 25/42] docs: finish plain-English pass on pygad.md (examples section) Copyedit the examples (single/multi-objective, reproducing images, clustering, CoinTex): "sections discusses" -> "discuss", "range ... affect" -> "affects", "available the" -> "available at the", "20,000s" -> "20,000", and clearer multi-objective color wording. --- docs/source/pygad.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/source/pygad.md b/docs/source/pygad.md index 53a21e0..58995c0 100644 --- a/docs/source/pygad.md +++ b/docs/source/pygad.md @@ -628,7 +628,7 @@ Predicted output 1 based on the best solution : 50.59491545442283 Predicted output 2 based on the best solution : 29.99714270722312 ``` -This is the figure created by the `plot_fitness()` method. The fitness of the first objective has the green color. The blue color is used for the second objective fitness. +This is the figure created by the `plot_fitness()` method. The fitness of the first objective is shown in green, and the fitness of the second objective is shown in blue. ![multi-objective-pygad](https://github.com/ahmedfgad/GeneticAlgorithmPython/assets/16560492/7896f8d8-01c5-4ff9-8d15-52191c309b63) @@ -652,7 +652,7 @@ The steps to follow in order to reproduce an image are as follows: - Plot results - Calculate some statistics -The next sections discusses the code of each of these steps. +The next sections discuss the code of each step. #### Read an Image @@ -670,9 +670,9 @@ Here is the read image. ![fruit](https://user-images.githubusercontent.com/16560492/36948808-f0ac882e-1fe8-11e8-8d07-1307e3477fd0.jpg) -Based on the chromosome representation used in the example, the pixel values can be either in the 0-255, 0-1, or any other ranges. +Based on the chromosome representation used in the example, the pixel values can be in the 0-255 range, the 0-1 range, or any other range. -Note that the range of pixel values affect other parameters like the range from which the random values are selected during mutation and also the range of the values used in the initial population. So, be consistent. +Note that the range of pixel values affects other parameters, like the range from which random values are selected during mutation and the range of values used in the initial population. So, be consistent. #### Prepare the Fitness Function @@ -780,13 +780,13 @@ The solution reached after the 20,000 generations is shown below. ![solution](https://user-images.githubusercontent.com/16560492/82232405-e0f63a80-992e-11ea-984f-b6ed76465bd1.png) -After more generations, the result can be enhanced like what shown below. +After more generations, the result can be improved, as shown below. ![solution](https://user-images.githubusercontent.com/16560492/82232345-cf149780-992e-11ea-8390-bf1a57a19de7.png) The results can also be enhanced by changing the parameters passed to the constructor of the `pygad.GA` class. -Here is how the image is evolved from generation 0 to generation 20,000s. +Here is how the image evolves from generation 0 to generation 20,000. Generation 0 @@ -824,7 +824,7 @@ Soon a tutorial will be published at [Paperspace](https://blog.paperspace.com/au ### CoinTex Game Playing using PyGAD -The code is available the [CoinTex GitHub project](https://github.com/ahmedfgad/CoinTex/tree/master/PlayerGA). CoinTex is an Android game written in Python using the Kivy framework. Find CoinTex at [Google Play](https://play.google.com/store/apps/details?id=coin.tex.cointexreactfast): https://play.google.com/store/apps/details?id=coin.tex.cointexreactfast +The code is available at the [CoinTex GitHub project](https://github.com/ahmedfgad/CoinTex/tree/master/PlayerGA). CoinTex is an Android game written in Python using the Kivy framework. Find CoinTex at [Google Play](https://play.google.com/store/apps/details?id=coin.tex.cointexreactfast): https://play.google.com/store/apps/details?id=coin.tex.cointexreactfast Check this [Paperspace tutorial](https://blog.paperspace.com/building-agent-for-cointex-using-genetic-algorithm) for how the genetic algorithm plays CoinTex: https://blog.paperspace.com/building-agent-for-cointex-using-genetic-algorithm. Check also this [YouTube video](https://youtu.be/Sp_0RGjaL-0) showing the genetic algorithm while playing CoinTex. From 6baf5b5fcbb8623a5962441a5f31208753a98f76 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 21 May 2026 15:11:25 -0400 Subject: [PATCH 26/42] docs: plain-English pass on cnn.md and gacnn.md Fix the H2->H4 heading jumps under the cnn Model class (methods now H3), relabel the "Network Architecture" output blocks as text (clearing the Pygments lexing warnings), fix "the the" duplications, "activations functions" -> "activation functions", "It default" -> "It defaults", a duplicated "model: model:", and a 4-vs-6 num_solutions inconsistency. --- docs/source/cnn.md | 36 ++++++++++++++++++------------------ docs/source/gacnn.md | 8 ++++---- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/source/cnn.md b/docs/source/cnn.md index 879b9ca..fbf711b 100644 --- a/docs/source/cnn.md +++ b/docs/source/cnn.md @@ -1,8 +1,8 @@ # `pygad.cnn` Module -This section of the PyGAD's library documentation discusses the **pygad.cnn** module. +This section of the documentation discusses the **pygad.cnn** module. -Using the **pygad.cnn** module, convolutional neural networks (CNNs) are created. The purpose of this module is to only implement the **forward pass** of a convolutional neural network without using a training algorithm. The **pygad.cnn** module builds the network layers, implements the activations functions, trains the network, makes predictions, and more. +Using the **pygad.cnn** module, convolutional neural networks (CNNs) are created. The purpose of this module is to only implement the **forward pass** of a convolutional neural network without using a training algorithm. The **pygad.cnn** module builds the network layers, implements the activation functions, trains the network, makes predictions, and more. Later, the **pygad.gacnn** module is used to train the **pygad.cnn** network using the genetic algorithm built in the **pygad** module. @@ -21,12 +21,12 @@ Each layer supported by the **pygad.cnn** module has a corresponding class. The In the future, more layers will be added. -Except for the input layer, all of listed layers has 4 instance attributes that do the same function which are: +Except for the input layer, all of the listed layers have 4 instance attributes that do the same job: 1. `previous_layer`: A reference to the previous layer in the CNN architecture. 2. `layer_input_size`: The size of the input to the layer. 3. `layer_output_size`: The size of the output from the layer. -4. `layer_output`: The latest output generated from the layer. It default to `None`. +4. `layer_output`: The latest output generated from the layer. It defaults to `None`. In addition to such attributes, the layers may have some additional attributes. The next subsections discuss such layers. @@ -207,11 +207,11 @@ Within the constructor, the accepted parameters are used as instance attributes. There are a number of methods in the `pygad.cnn.Model` class which serves in training, testing, and retrieving information about the model. These methods are discussed in the next subsections. -#### `get_layers()` +### `get_layers()` -Creates a list of all layers in the CNN model. It accepts no parameters. +Creates a list of all layers in the CNN model. It accepts no parameters. -#### `train()` +### `train()` Trains the CNN model. @@ -223,17 +223,17 @@ Accepts the following parameters: This method trains the CNN model according to the number of epochs specified in the constructor of the `pygad.cnn.Model` class. -It is important to note that no learning algorithm is used for training the pygad.cnn. Just the learning rate is used for making some changes which is better than leaving the weights unchanged. +It is important to note that no learning algorithm is used for training `pygad.cnn`. The learning rate is just used to make some changes, which is better than leaving the weights unchanged. -#### `feed_sample()` +### `feed_sample()` -Feeds a sample in the CNN layers and returns results of the last layer in the pygad.cnn. +Feeds a sample into the CNN layers and returns the results of the last layer in `pygad.cnn`. -#### `update_weights()` +### `update_weights()` -Updates the CNN weights using the learning rate. It is important to note that no learning algorithm is used for training the pygad.cnn. Just the learning rate is used for making some changes which is better than leaving the weights unchanged. +Updates the CNN weights using the learning rate. It is important to note that no learning algorithm is used for training `pygad.cnn`. The learning rate is just used to make some changes, which is better than leaving the weights unchanged. -#### `predict()` +### `predict()` Uses the trained CNN for making predictions. @@ -241,9 +241,9 @@ Accepts the following parameter: * `data_inputs`: The inputs to predict their label. -It returns a list holding the samples predictions. +It returns a list holding the samples' predictions. -#### `summary()` +### `summary()` Prints a summary of the CNN architecture. @@ -258,7 +258,7 @@ The dense layer supports these functions besides the `softmax` function implemen ## Steps to Build a Neural Network -This section discusses how to use the `pygad.cnn` module for building a neural network. The summary of the steps are as follows: +This section discusses how to use the `pygad.cnn` module to build a neural network. The steps are summarized as follows: - Reading the Data - Building the CNN Architecture @@ -371,7 +371,7 @@ The `summary()` method in the `pygad.cnn.Model` class prints a summary of the CN model.summary() ``` -```python +```text ----------Network Architecture---------- @@ -390,7 +390,7 @@ model.summary() ### Training the Network -After the model and the data are prepared, then the model can be trained using the the `pygad.cnn.train()` method. +After the model and the data are prepared, then the model can be trained using the `pygad.cnn.train()` method. ```python model.train(train_inputs=train_inputs, diff --git a/docs/source/gacnn.md b/docs/source/gacnn.md index bf98b32..612c06a 100644 --- a/docs/source/gacnn.md +++ b/docs/source/gacnn.md @@ -1,6 +1,6 @@ # `pygad.gacnn` Module -This section of the PyGAD's library documentation discusses the **pygad.gacnn** module. +This section of the documentation discusses the **pygad.gacnn** module. The `pygad.gacnn` module trains convolutional neural networks using the genetic algorithm. It makes use of the 2 modules `pygad` and `pygad.cnn`. @@ -14,7 +14,7 @@ In order to train a CNN using the genetic algorithm, the first thing to do is to The `pygad.gacnn.GACNN` class constructor accepts the following parameters: -- `model`: model: An instance of the pygad.cnn.Model class representing the architecture of all solutions in the population. +- `model`: An instance of the `pygad.cnn.Model` class that represents the architecture of all solutions in the population. - `num_solutions`: Number of CNNs (i.e. solutions) in the population. Based on the value passed to this parameter, a number of identical CNNs are created where their parameters are optimized using the genetic algorithm. ### Instance Attributes @@ -156,7 +156,7 @@ The `summary()` method in the `pygad.cnn.Model` class prints a summary of the CN model.summary() ``` -```python +```text ----------Network Architecture---------- @@ -171,7 +171,7 @@ The next step is to create an instance of the `pygad.gacnn.GACNN` class. After preparing the input data and building the CNN model, an instance of the `pygad.gacnn.GACNN` class is created by passing the appropriate parameters. -Here is an example where the `num_solutions` parameter is set to 4 which means the genetic algorithm population will have 6 solutions (i.e. networks). All of these 6 CNNs will have the same architectures as specified by the `model` parameter. +Here is an example where the `num_solutions` parameter is set to 4, which means the genetic algorithm population will have 4 solutions (i.e. networks). All of these 4 CNNs will have the same architecture as specified by the `model` parameter. ```python import pygad.gacnn From 3e8f7df091c5ce82b46620110415e2e474a21dc6 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 21 May 2026 15:13:36 -0400 Subject: [PATCH 27/42] docs: plain-English pass on nn, gann, kerasga, torchga (intros and steps) Fix the recurring "PyGAD's library documentation" intro phrase, an "activations functions" typo, a "kerarsga"/"has helper a class" typo, an "at least one an" slip, and the broken steps-summary numbering (1,2,4,5,6,8) in kerasga.md and torchga.md. --- docs/source/gann.md | 4 ++-- docs/source/kerasga.md | 14 +++++++------- docs/source/nn.md | 4 ++-- docs/source/torchga.md | 12 ++++++------ 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/source/gann.md b/docs/source/gann.md index 12eb9c2..b9f6c49 100644 --- a/docs/source/gann.md +++ b/docs/source/gann.md @@ -1,6 +1,6 @@ # `pygad.gann` Module -This section of the PyGAD's library documentation discusses the **pygad.gann** module. +This section of the documentation discusses the **pygad.gann** module. The `pygad.gann` module trains neural networks (for either classification or regression) using the genetic algorithm. It makes use of the 2 modules `pygad` and `pygad.nn`. @@ -56,7 +56,7 @@ Accepts the following parameters: This section discusses the functions in the `pygad.gann` module. ### `pygad.gann.validate_network_parameters()` -Validates the parameters passed to the constructor of the `pygad.gann.GANN` class. If at least one an invalid parameter exists, an exception is raised and the execution stops. +Validates the parameters passed to the constructor of the `pygad.gann.GANN` class. If at least one invalid parameter exists, an exception is raised and execution stops. The function accepts the same parameters passed to the constructor of the `pygad.gann.GANN` class. Please check the documentation of such parameters in the section discussing the class constructor. diff --git a/docs/source/kerasga.md b/docs/source/kerasga.md index 955481b..6d4291e 100644 --- a/docs/source/kerasga.md +++ b/docs/source/kerasga.md @@ -1,8 +1,8 @@ # `pygad.kerasga` Module -This section of the PyGAD's library documentation discusses the [**pygad.kerasga**](https://pygad.readthedocs.io/en/latest/kerasga.html) module. +This section of the documentation discusses the [**pygad.kerasga**](https://pygad.readthedocs.io/en/latest/kerasga.html) module. -The `pygad.kerarsga` module has helper a class and 2 functions to train Keras models using the genetic algorithm (PyGAD). The Keras model can be built either using the [Sequential Model](https://keras.io/guides/sequential_model) or the [Functional API](https://keras.io/guides/functional_api). +The `pygad.kerasga` module has a helper class and 2 functions to train Keras models using the genetic algorithm (PyGAD). The Keras model can be built using either the [Sequential Model](https://keras.io/guides/sequential_model) or the [Functional API](https://keras.io/guides/functional_api). The contents of this module are: @@ -15,14 +15,14 @@ More details are given in the next sections. ## Steps Summary -The summary of the steps used to train a Keras model using PyGAD is as follows: +The steps used to train a Keras model using PyGAD are summarized as follows: 1. Create a Keras model. 2. Create an instance of the `pygad.kerasga.KerasGA` class. -4. Prepare the training data. -5. Build the fitness function. -6. Create an instance of the `pygad.GA` class. -8. Run the genetic algorithm. +3. Prepare the training data. +4. Build the fitness function. +5. Create an instance of the `pygad.GA` class. +6. Run the genetic algorithm. ## Create Keras Model diff --git a/docs/source/nn.md b/docs/source/nn.md index 326d982..6c801d7 100644 --- a/docs/source/nn.md +++ b/docs/source/nn.md @@ -1,8 +1,8 @@ # `pygad.nn` Module -This section of the PyGAD's library documentation discusses the **pygad.nn** module. +This section of the documentation discusses the **pygad.nn** module. -Using the **pygad.nn** module, artificial neural networks are created. The purpose of this module is to only implement the **forward pass** of a neural network without using a training algorithm. The **pygad.nn** module builds the network layers, implements the activations functions, trains the network, makes predictions, and more. +Using the **pygad.nn** module, artificial neural networks are created. The purpose of this module is to only implement the **forward pass** of a neural network without using a training algorithm. The **pygad.nn** module builds the network layers, implements the activation functions, trains the network, makes predictions, and more. Later, the **pygad.gann** module is used to train the **pygad.nn** network using the genetic algorithm built in the **pygad** module. diff --git a/docs/source/torchga.md b/docs/source/torchga.md index a529f67..b97d538 100644 --- a/docs/source/torchga.md +++ b/docs/source/torchga.md @@ -1,6 +1,6 @@ # `pygad.torchga` Module -This section of the PyGAD's library documentation discusses the **pygad.torchga** module. +This section of the documentation discusses the **pygad.torchga** module. The `pygad.torchga` module has a helper class and 2 functions to train PyTorch models using the genetic algorithm (PyGAD). @@ -15,14 +15,14 @@ More details are given in the next sections. ## Steps Summary -The summary of the steps used to train a PyTorch model using PyGAD is as follows: +The steps used to train a PyTorch model using PyGAD are summarized as follows: 1. Create a PyTorch model. 2. Create an instance of the `pygad.torchga.TorchGA` class. -4. Prepare the training data. -5. Build the fitness function. -6. Create an instance of the `pygad.GA` class. -8. Run the genetic algorithm. +3. Prepare the training data. +4. Build the fitness function. +5. Create an instance of the `pygad.GA` class. +6. Run the genetic algorithm. ## Create PyTorch Model From b8464c6260b3f779accd0e501dc5d319279ceb71 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 21 May 2026 15:22:37 -0400 Subject: [PATCH 28/42] docs: plain-English pass on gann/gacnn/torchga bodies Fix a real bug (input layer described as "last" instead of "first" layer), a corrupted output block (stray print( leaked into shown output), an unmatched parenthesis, a pygad.torch -> pygad.torchga heading typo, a matric -> matrix typo, "less number" -> "fewer", normalized broken step lists, and the recurring "some plots are showed" comment across ML pages. --- docs/source/gacnn.md | 20 ++++++++++---------- docs/source/gann.md | 38 +++++++++++++++++++------------------- docs/source/kerasga.md | 10 +++++----- docs/source/torchga.md | 10 +++++----- 4 files changed, 39 insertions(+), 39 deletions(-) diff --git a/docs/source/gacnn.md b/docs/source/gacnn.md index 612c06a..b2faf61 100644 --- a/docs/source/gacnn.md +++ b/docs/source/gacnn.md @@ -35,7 +35,7 @@ The list of networks is assigned to the `population_networks` attribute of the i #### `update_population_trained_weights()` -The `update_population_trained_weights()` method updates the `trained_weights` attribute of the layers of each network (check the documentation of the `pygad.cnn` module) for more information) according to the weights passed in the `population_trained_weights` parameter. +The `update_population_trained_weights()` method updates the `trained_weights` attribute of the layers of each network (check the documentation of the `pygad.cnn` module for more information) according to the weights passed in the `population_trained_weights` parameter. Accepts the following parameters: @@ -72,7 +72,7 @@ Returns a list holding the weights matrices for all solutions (i.e. networks). ## Steps to Build and Train CNN using Genetic Algorithm -The steps to use this project for building and training a neural network using the genetic algorithm are as follows: +The steps to use this project for building and training a CNN using the genetic algorithm are as follows: - Prepare the training data. - Create an instance of the `pygad.gacnn.GACNN` class. @@ -81,12 +81,12 @@ The steps to use this project for building and training a neural network using t - Prepare the generation callback function. - Create an instance of the `pygad.GA` class. - Run the created instance of the `pygad.GA` class. -- Plot the Fitness Values -- Information about the best solution. -- Making predictions using the trained weights. -- Calculating some statistics. +- Plot the fitness values. +- Get information about the best solution. +- Make predictions using the trained weights. +- Calculate some statistics. -Let's start covering all of these steps. +The next sections cover each step. ### Prepare the Training Data @@ -108,9 +108,9 @@ train_inputs = numpy.load("dataset_inputs.npy") train_outputs = numpy.load("dataset_outputs.npy") ``` -For the output array, each element must be a single number representing the class label of the sample. The class labels must start at `0`. So, if there are 80 samples, then the shape of the output array is `(80)`. If there are 5 classes in the data, then the values of all the 200 elements in the output array must range from 0 to 4 inclusive. Generally, the class labels start from `0` to `N-1` where `N` is the number of classes. +For the output array, each element must be a single number representing the class label of the sample. The class labels must start at `0`. So, if there are 80 samples, then the shape of the output array is `(80,)`. If there are 5 classes in the data, then the values of all the 80 elements in the output array must range from 0 to 4 inclusive. Generally, the class labels start from `0` to `N-1` where `N` is the number of classes. -Note that the project only supports that each sample is assigned to only one class. +Note that the project only supports assigning each sample to one class. ### Building the Network Architecture @@ -452,7 +452,7 @@ ga_instance = pygad.GA(num_generations=num_generations, ga_instance.run() -# After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. ga_instance.plot_fitness() # Returning the details of the best solution. diff --git a/docs/source/gann.md b/docs/source/gann.md index b9f6c49..681375a 100644 --- a/docs/source/gann.md +++ b/docs/source/gann.md @@ -68,7 +68,7 @@ Returns a list holding the name(s) of the activation function(s) of the hidden l ### `pygad.gann.create_network()` -Creates a neural network as a linked list between the input, hidden, and output layers where the layer at index N (which is the last/output layer) references the layer at index N-1 (which is a hidden layer) using its previous_layer attribute. The input layer does not reference any layer because it is the last layer in the linked list. +Creates a neural network as a linked list between the input, hidden, and output layers, where the layer at index N (the last/output layer) references the layer at index N-1 (a hidden layer) using its `previous_layer` attribute. The input layer does not reference any layer because it is the first layer in the linked list. In addition to the `parameters_validated` parameter, this function accepts the same parameters passed to the constructor of the `pygad.gann.GANN` class except for the `num_solutions` parameter because only a single network is created out of the `create_network()` function. @@ -112,12 +112,12 @@ The steps to use this project for building and training a neural network using t - Prepare the generation callback function. - Create an instance of the `pygad.GA` class. - Run the created instance of the `pygad.GA` class. -- Plot the Fitness Values -- Information about the best solution. -- Making predictions using the trained weights. -- Calculating some statistics. +- Plot the fitness values. +- Get information about the best solution. +- Make predictions using the trained weights. +- Calculate some statistics. -Let's start covering all of these steps. +The next sections cover each step. ### Prepare the Training Data @@ -181,7 +181,7 @@ The weights of the network are as follows: The activation function used for the output layer is `softmax`. The `relu` activation function is used for the hidden layer. -After creating the instance of the `pygad.gann.GANN` class next is to fetch the weights of the population as a list of vectors. +After creating the instance of the `pygad.gann.GANN` class, the next step is to fetch the weights of the population as a list of vectors. ### Fetch the Population Weights as Vectors @@ -195,7 +195,7 @@ To create a list holding the population weights as vectors, one for each network population_vectors = pygad.gann.population_as_vectors(population_networks=GANN_instance.population_networks) ``` -After preparing the population weights as a set of vectors, next is to prepare 2 functions which are: +After preparing the population weights as a set of vectors, the next step is to prepare 2 functions: 1. Fitness function. 2. Callback function after each generation. @@ -235,7 +235,7 @@ This callback function can be used to update the `trained_weights` attribute of Here is the implementation for a function that updates the `trained_weights` attribute of the layers of the population networks. -It works by converting the current population from the vector form to the matric form using the `pygad.gann.population_as_matrices()` function. It accepts the population as vectors and returns it as matrices. +It works by converting the current population from the vector form to the matrix form using the `pygad.gann.population_as_matrices()` function. It accepts the population as vectors and returns it as matrices. The population matrices are then passed to the `update_population_trained_weights()` method in the `pygad.gann` module to update the `trained_weights` attribute of all layers for all solutions within the population. @@ -250,7 +250,7 @@ def callback_generation(ga_instance): print(f"Fitness = {ga_instance.best_solution()[1]}") ``` -After preparing the fitness and callback function, next is to create an instance of the `pygad.GA` class. +After preparing the fitness and callback functions, the next step is to create an instance of the `pygad.GA` class. ### Create an Instance of the `pygad.GA` Class @@ -312,7 +312,7 @@ ga_instance.plot_fitness() ![XOR_Fitness](https://user-images.githubusercontent.com/16560492/82078638-c11e0700-96e1-11ea-8aa9-c36761c5e9c7.png) -By running the code again, a different initial population is created and thus a classification accuracy of 100 can be reached using a less number of generations. On the other hand, a different initial population might cause 100% accuracy to be reached using more generations or not reached at all. +By running the code again, a different initial population is created, so a classification accuracy of 100 can be reached using fewer generations. On the other hand, a different initial population might cause 100% accuracy to be reached using more generations or not reached at all. ### Information about the Best Solution @@ -337,7 +337,7 @@ Fitness value of the best solution = 100.0 Index of the best solution : 0 ``` -Using the `best_solution_generation` attribute of the instance from the `pygad.GA` class, the generation number at which the **best fitness** is reached could be fetched. According to the result, the best fitness value is reached after 182 generations. +Using the `best_solution_generation` attribute of the `pygad.GA` instance, you can get the generation number at which the best fitness was reached. In this run, the best fitness value is reached after 182 generations. ```python if ga_instance.best_solution_generation != -1: @@ -375,9 +375,9 @@ print(f"Classification accuracy : {accuracy}.") ``` ``` -Number of correct classifications : 4 -print("Number of wrong classifications : 0 -Classification accuracy : 100 +Number of correct classifications : 4. +Number of wrong classifications : 0. +Classification accuracy : 100.0. ``` ## Examples @@ -493,7 +493,7 @@ ga_instance = pygad.GA(num_generations=num_generations, ga_instance.run() -# After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. ga_instance.plot_fitness() # Returning the details of the best solution. @@ -625,7 +625,7 @@ ga_instance = pygad.GA(num_generations=num_generations, ga_instance.run() -# After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. ga_instance.plot_fitness() # Returning the details of the best solution. @@ -793,7 +793,7 @@ ga_instance = pygad.GA(num_generations=num_generations, ga_instance.run() -# After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. ga_instance.plot_fitness() # Returning the details of the best solution. @@ -942,7 +942,7 @@ ga_instance = pygad.GA(num_generations=num_generations, ga_instance.run() -# After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. ga_instance.plot_fitness() # Returning the details of the best solution. diff --git a/docs/source/kerasga.md b/docs/source/kerasga.md index 6d4291e..d0630ec 100644 --- a/docs/source/kerasga.md +++ b/docs/source/kerasga.md @@ -199,7 +199,7 @@ ga_instance = pygad.GA(num_generations=num_generations, ga_instance.run() -# After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. ga_instance.plot_fitness(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4) # Returning the details of the best solution. @@ -442,7 +442,7 @@ ga_instance = pygad.GA(num_generations=num_generations, # Start the genetic algorithm evolution. ga_instance.run() -# After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. ga_instance.plot_fitness(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4) # Returning the details of the best solution. @@ -583,7 +583,7 @@ ga_instance = pygad.GA(num_generations=num_generations, # Start the genetic algorithm evolution. ga_instance.run() -# After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. ga_instance.plot_fitness(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4) # Returning the details of the best solution. @@ -720,7 +720,7 @@ ga_instance = pygad.GA(num_generations=num_generations, # Start the genetic algorithm evolution. ga_instance.run() -# After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. ga_instance.plot_fitness(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4) # Returning the details of the best solution. @@ -872,7 +872,7 @@ ga_instance = pygad.GA(num_generations=10, # Start the genetic algorithm evolution. ga_instance.run() -# After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. ga_instance.plot_fitness(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4) # Returning the details of the best solution. diff --git a/docs/source/torchga.md b/docs/source/torchga.md index b97d538..c99b51c 100644 --- a/docs/source/torchga.md +++ b/docs/source/torchga.md @@ -87,7 +87,7 @@ The function accepts the following parameters: It returns a 1D vector holding the model weights. -### `pygad.torch.model_weights_as_dict()` +### `pygad.torchga.model_weights_as_dict()` The `model_weights_as_dict()` function accepts the following parameters: @@ -177,7 +177,7 @@ ga_instance = pygad.GA(num_generations=num_generations, ga_instance.run() -# After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. ga_instance.plot_fitness(title="PyGAD & PyTorch - Iteration vs. Fitness", linewidth=4) # Returning the details of the best solution. @@ -413,7 +413,7 @@ ga_instance = pygad.GA(num_generations=num_generations, # Start the genetic algorithm evolution. ga_instance.run() -# After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. ga_instance.plot_fitness(title="PyGAD & PyTorch - Iteration vs. Fitness", linewidth=4) # Returning the details of the best solution. @@ -563,7 +563,7 @@ ga_instance = pygad.GA(num_generations=num_generations, # Start the genetic algorithm evolution. ga_instance.run() -# After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. ga_instance.plot_fitness(title="PyGAD & PyTorch - Iteration vs. Fitness", linewidth=4) # Returning the details of the best solution. @@ -706,7 +706,7 @@ ga_instance = pygad.GA(num_generations=num_generations, # Start the genetic algorithm evolution. ga_instance.run() -# After the generations complete, some plots are showed that summarize how the outputs/fitness values evolve over generations. +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. ga_instance.plot_fitness(title="PyGAD & PyTorch - Iteration vs. Fitness", linewidth=4) # Returning the details of the best solution. From d37e878805192db435a68d89c96d2c3670c4aab9 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 21 May 2026 15:23:43 -0400 Subject: [PATCH 29/42] docs: final sweep on nn/cnn/gacnn/helper plus recurring fixes Fix a factual error (AveragePooling2D described as applying max pooling), the last "PyGAD's library documentation" intro (helper.md), "the the" and "go the the" -> "go to the", a "layers layers" duplication, an incomplete sentence in pygad_more, and reword the awkward "next is to" across nn, cnn, and gacnn. --- docs/source/cnn.md | 6 +++--- docs/source/gacnn.md | 6 +++--- docs/source/helper.md | 2 +- docs/source/nn.md | 4 ++-- docs/source/pygad.md | 2 +- docs/source/pygad_more.md | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/source/cnn.md b/docs/source/cnn.md index fbf711b..9999b74 100644 --- a/docs/source/cnn.md +++ b/docs/source/cnn.md @@ -149,7 +149,7 @@ Within the constructor, the accepted parameters are used as instance attributes. ### `pygad.cnn.AveragePooling2D` Class -The `pygad.cnn.AveragePooling2D` class is similar to the `pygad.cnn.MaxPooling2D` class except that it applies the max pooling operation rather than average pooling. +The `pygad.cnn.AveragePooling2D` class is similar to the `pygad.cnn.MaxPooling2D` class except that it applies the average pooling operation rather than max pooling. ### `pygad.cnn.Flatten` Class @@ -297,7 +297,7 @@ train_inputs = numpy.load("dataset_inputs.npy") train_outputs = numpy.load("dataset_outputs.npy") ``` -After the data is prepared, next is to create the network architecture. +After the data is prepared, the next step is to create the network architecture. ### Building the Network Architecture @@ -310,7 +310,7 @@ sample_shape = train_inputs.shape[1:] input_layer = pygad.cnn.Input2D(input_shape=sample_shape) ``` -After the input layer is created, next is to create a number of layers layers according to the next code. Normally, the last dense layer is regarded as the output layer. Note that the output layer has a number of neurons equal to the number of classes in the dataset which is 4. +After the input layer is created, the next step is to create a number of layers according to the next code. Normally, the last dense layer is regarded as the output layer. Note that the output layer has a number of neurons equal to the number of classes in the dataset which is 4. ```python conv_layer1 = pygad.cnn.Conv2D(num_filters=2, diff --git a/docs/source/gacnn.md b/docs/source/gacnn.md index b2faf61..64d2ec4 100644 --- a/docs/source/gacnn.md +++ b/docs/source/gacnn.md @@ -180,7 +180,7 @@ GACNN_instance = pygad.gacnn.GACNN(model=model, num_solutions=4) ``` -After creating the instance of the `pygad.gacnn.GACNN` class, next is to fetch the weights of the population as a list of vectors. +After creating the instance of the `pygad.gacnn.GACNN` class, the next step is to fetch the weights of the population as a list of vectors. ### Fetch the Population Weights as Vectors @@ -200,7 +200,7 @@ Such population of vectors is used as the initial population. initial_population = population_vectors.copy() ``` -After preparing the population weights as a set of vectors, next is to prepare 2 functions which are: +After preparing the population weights as a set of vectors, the next step is to prepare 2 functions which are: 1. Fitness function. 2. Callback function after each generation. @@ -255,7 +255,7 @@ def callback_generation(ga_instance): print(f"Generation = {ga_instance.generations_completed}") ``` -After preparing the fitness and callback function, next is to create an instance of the `pygad.GA` class. +After preparing the fitness and callback function, the next step is to create an instance of the `pygad.GA` class. ### Create an Instance of the `pygad.GA` Class diff --git a/docs/source/helper.md b/docs/source/helper.md index a2a393a..ad0317b 100644 --- a/docs/source/helper.md +++ b/docs/source/helper.md @@ -1,6 +1,6 @@ # `pygad.helper` Module -This section of the PyGAD's library documentation discusses the `pygad.helper` module. +This section of the documentation discusses the `pygad.helper` module. The `pygad.helper` module has 2 submodules: diff --git a/docs/source/nn.md b/docs/source/nn.md index 6c801d7..29750ab 100644 --- a/docs/source/nn.md +++ b/docs/source/nn.md @@ -364,7 +364,7 @@ data_inputs = numpy.load("dataset_features.npy") data_outputs = numpy.load("outputs.npy") ``` -After the data is prepared, next is to create the network architecture. +After the data is prepared, the next step is to create the network architecture. ### Building the Network Architecture @@ -377,7 +377,7 @@ num_inputs = data_inputs.shape[1] input_layer = pygad.nn.InputLayer(num_inputs) ``` -After the input layer is created, next is to create a number of dense layers according to the next code. Normally, the last dense layer is regarded as the output layer. Note that the output layer has a number of neurons equal to the number of classes in the dataset which is 4. +After the input layer is created, the next step is to create a number of dense layers according to the next code. Normally, the last dense layer is regarded as the output layer. Note that the output layer has a number of neurons equal to the number of classes in the dataset which is 4. ```python hidden_layer = pygad.nn.DenseLayer(num_neurons=HL2_neurons, previous_layer=input_layer, activation_function="relu") diff --git a/docs/source/pygad.md b/docs/source/pygad.md index 58995c0..7d86326 100644 --- a/docs/source/pygad.md +++ b/docs/source/pygad.md @@ -30,7 +30,7 @@ The `pygad.GA` class constructor supports the following parameters: - `K_tournament=3`: In case that the parent selection type is `tournament`, the `K_tournament` specifies the number of parents participating in the tournament selection. It defaults to `3`. - `crossover_type="single_point"`: Type of the crossover operation. Supported types are `single_point` (for single-point crossover), `two_points` (for two points crossover), `uniform` (for uniform crossover), and `scattered` (for scattered crossover). Scattered crossover is supported from PyGAD [2.9.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-9-0) and higher. It defaults to `single_point`. A custom crossover function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/utils.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about creating a user-defined crossover function. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, if `crossover_type=None`, then the crossover step is bypassed which means 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. - `crossover_probability=None`: The probability of selecting a parent for applying the crossover operation. Its value must be between 0.0 and 1.0 inclusive. For each parent, a random value between 0.0 and 1.0 is generated. If this random value is less than or equal to the value assigned to the `crossover_probability` parameter, then the parent is selected. Added in [PyGAD 2.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-5-0) and higher. -- `mutation_type="random"`: Type of the mutation operation. Supported types are `random` (for random mutation), `swap` (for swap mutation), `inversion` (for inversion mutation), `scramble` (for scramble mutation), and `adaptive` (for adaptive mutation). It defaults to `random`. A custom mutation function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/utils.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about creating a user-defined mutation function. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, if `mutation_type=None`, then the mutation step is bypassed which means 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. `Adaptive` mutation is supported starting from [PyGAD 2.10.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-10-0). For more information about adaptive mutation, go the the [Adaptive Mutation](https://pygad.readthedocs.io/en/latest/utils.html#adaptive-mutation) section. For example about using adaptive mutation, check the [Use Adaptive Mutation in PyGAD](https://pygad.readthedocs.io/en/latest/utils.html#use-adaptive-mutation-in-pygad) section. +- `mutation_type="random"`: Type of the mutation operation. Supported types are `random` (for random mutation), `swap` (for swap mutation), `inversion` (for inversion mutation), `scramble` (for scramble mutation), and `adaptive` (for adaptive mutation). It defaults to `random`. A custom mutation function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/utils.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about creating a user-defined mutation function. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, if `mutation_type=None`, then the mutation step is bypassed which means 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. `Adaptive` mutation is supported starting from [PyGAD 2.10.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-10-0). For more information about adaptive mutation, go to the [Adaptive Mutation](https://pygad.readthedocs.io/en/latest/utils.html#adaptive-mutation) section. For example about using adaptive mutation, check the [Use Adaptive Mutation in PyGAD](https://pygad.readthedocs.io/en/latest/utils.html#use-adaptive-mutation-in-pygad) section. - `mutation_probability=None`: The probability of selecting a gene for applying the mutation operation. Its value must be between 0.0 and 1.0 inclusive. For each gene in a solution, a random value between 0.0 and 1.0 is generated. If this random value is less than or equal to the value assigned to the `mutation_probability` parameter, then the gene is selected. If this parameter exists, then there is no need for the 2 parameters `mutation_percent_genes` and `mutation_num_genes`. Added in [PyGAD 2.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-5-0) and higher. - `mutation_by_replacement=False`: An optional bool parameter. It works only when the selected type of mutation is random (`mutation_type="random"`). In this case, `mutation_by_replacement=True` means replace the gene by the randomly generated value. If False, then it has no effect and random mutation works by adding the random value to the gene. Supported in [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher. Check the changes in [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) under the Release History section for an example. - `mutation_percent_genes="default"`: Percentage of genes to mutate. It defaults to the string `"default"` which is later translated into the integer `10` which means 10% of the genes will be mutated. It must be `>0` and `<=100`. Out of this percentage, the number of genes to mutate is deduced which is assigned to the `mutation_num_genes` parameter. The `mutation_percent_genes` parameter has no action if `mutation_probability` or `mutation_num_genes` exist. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, this parameter has no action if `mutation_type` is `None`. diff --git a/docs/source/pygad_more.md b/docs/source/pygad_more.md index 6e3dfe1..890cb92 100644 --- a/docs/source/pygad_more.md +++ b/docs/source/pygad_more.md @@ -333,7 +333,7 @@ Sometimes it is normal for PyGAD to fail to find a gene value that satisfies the For some other cases, the constraint can be met but with some changes. For example, increasing the range from which a value is sampled. If the `gene_space` is used and assigned `range(10)`, then the gene constraint can be met by using `range(50)` so that we can find values greater than 50. -Even if the the gene space is already assigned `range(1000)`, it might still not find values meeting the constraints This is because PyGAD samples a number of values equal to the `sample_size` parameter which defaults to *100*. +Even if the gene space is already assigned `range(1000)`, it might still not find values that meet the constraints. This is because PyGAD samples a number of values equal to the `sample_size` parameter which defaults to *100*. Out of the range of *1000* numbers, all the 100 values might not be satisfying the constraint. This issue could be solved by simply assigning a larger value for the `sample_size` parameter. From 0b675514564e6481bec9289432aad6e7f65d5fd8 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 21 May 2026 15:25:48 -0400 Subject: [PATCH 30/42] docs: finish plain-English pass on nn.md (functions, helpers, steps) Fix "based on where" -> "based on whether", "Even no training algorithm", "functions that does not", awkward to_vector/to_array descriptions, and "summary of the steps are". Completes the line-by-line pass of the nn page. --- docs/source/nn.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/source/nn.md b/docs/source/nn.md index 29750ab..faf75c5 100644 --- a/docs/source/nn.md +++ b/docs/source/nn.md @@ -146,7 +146,7 @@ Accepts the following parameters: - `last_layer`: A reference to the last (output) layer in the network architecture. - `initial`: When `True` (default), the function returns the **initial** weights of the layers using the layers' `initial_weights` attribute. When `False`, it returns the **trained** weights of the layers using the layers' `trained_weights` attribute. The initial weights are only needed before network training starts. The trained weights are needed to predict the network outputs. -The function uses a `while` loop to iterate through the layers using their `previous_layer` attribute. For each layer, either the initial weights or the trained weights are returned based on where the `initial` parameter is `True` or `False`. +The function uses a `while` loop to iterate through the layers using their `previous_layer` attribute. For each layer, either the initial weights or the trained weights are returned based on whether the `initial` parameter is `True` or `False`. ### `pygad.nn.layers_weights_as_vector()` @@ -159,7 +159,7 @@ Accepts the following parameters: - `last_layer`: A reference to the last (output) layer in the network architecture. - `initial`: When `True` (default), the function returns the **initial** weights of the layers using the layers' `initial_weights` attribute. When `False`, it returns the **trained** weights of the layers using the layers' `trained_weights` attribute. The initial weights are only needed before network training starts. The trained weights are needed to predict the network outputs. -The function uses a `while` loop to iterate through the layers using their `previous_layer` attribute. For each layer, either the initial weights or the trained weights are returned based on where the `initial` parameter is `True` or `False`. +The function uses a `while` loop to iterate through the layers using their `previous_layer` attribute. For each layer, either the initial weights or the trained weights are returned based on whether the `initial` parameter is `True` or `False`. ### `pygad.nn.layers_weights_as_matrix()` @@ -225,7 +225,7 @@ For each epoch, all the data samples are fed to the network to return their pred ### `pygad.nn.update_weights()` -Calculates and returns the updated weights. Even no training algorithm is used in this project, the weights are updated using the learning rate. It is not the best way to update the weights but it is better than keeping it as it is by making some small changes to the weights. +Calculates and returns the updated weights. Even though no training algorithm is used in this project, the weights are updated using the learning rate. It is not the best way to update the weights, but making small changes is better than keeping them as they are. Accepts the following parameters: @@ -260,11 +260,11 @@ All the data samples are fed to the network to return their predictions. ## Helper Functions -There are functions in the `pygad.nn` module that does not directly manipulate the neural networks. +There are functions in the `pygad.nn` module that do not directly manipulate the neural networks. ### `pygad.nn.to_vector()` -Converts a passed NumPy array (of any dimensionality) to its `array` parameter into a 1D vector and returns the vector. +Converts a NumPy array (of any dimensionality) passed to its `array` parameter into a 1D vector and returns the vector. Accepts the following parameters: @@ -272,7 +272,7 @@ Accepts the following parameters: ### `pygad.nn.to_array()` -Converts a passed vector to its `vector` parameter into a NumPy array and returns the array. +Converts a vector passed to its `vector` parameter into a NumPy array and returns the array. Accepts the following parameters: @@ -289,7 +289,7 @@ The supported activation functions are: ## Steps to Build a Neural Network -This section discusses how to use the `pygad.nn` module for building a neural network. The summary of the steps are as follows: +This section discusses how to use the `pygad.nn` module to build a neural network. The steps are summarized as follows: - Reading the Data - Building the Network Architecture From cd077229553157171bc521881ad26e4e448afa9c Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 21 May 2026 15:27:09 -0400 Subject: [PATCH 31/42] docs: polish kerasga/torchga example prose Reword "After the PyGAD completes its execution, then there is a figure..." to "After PyGAD completes its execution, a figure shows...". --- docs/source/kerasga.md | 2 +- docs/source/torchga.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/kerasga.md b/docs/source/kerasga.md index d0630ec..ee3317f 100644 --- a/docs/source/kerasga.md +++ b/docs/source/kerasga.md @@ -324,7 +324,7 @@ The sixth and last step is to run the genetic algorithm by calling the `run()` m ga_instance.run() ``` -After the PyGAD completes its execution, then there is a figure that shows how the fitness value changes by generation. Call the `plot_fitness()` method to show the figure. +After PyGAD completes its execution, a figure shows how the fitness value changes by generation. Call the `plot_fitness()` method to show the figure. ```python ga_instance.plot_fitness(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4) diff --git a/docs/source/torchga.md b/docs/source/torchga.md index c99b51c..daed089 100644 --- a/docs/source/torchga.md +++ b/docs/source/torchga.md @@ -292,7 +292,7 @@ The sixth and last step is to run the genetic algorithm by calling the `run()` m ga_instance.run() ``` -After the PyGAD completes its execution, then there is a figure that shows how the fitness value changes by generation. Call the `plot_fitness()` method to show the figure. +After PyGAD completes its execution, a figure shows how the fitness value changes by generation. Call the `plot_fitness()` method to show the figure. ```python ga_instance.plot_fitness(title="PyGAD & PyTorch - Iteration vs. Fitness", linewidth=4) From 2406aa77f5040ff34dc511781ef02643528382e4 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 21 May 2026 15:47:01 -0400 Subject: [PATCH 32/42] docs: split the releases page into Release History and Help & Resources Move the non-version content (PyGAD projects, Stack Overflow questions, submitting issues, feature requests, projects/papers built with PyGAD, tutorials, translations, links, and contact) out of releases.md into a new help.md titled "Help & Resources". releases.md now contains only the version history (1.0.17 to 3.6.0). Repoint the ask-for-feature and contact-us links and add a "Help & Resources" group to the toctree. --- docs/source/help.md | 381 ++++++++++++++++++++++++++++++++++++++++ docs/source/index.md | 7 + docs/source/releases.md | 378 --------------------------------------- docs/source/utils.md | 2 +- 4 files changed, 389 insertions(+), 379 deletions(-) create mode 100644 docs/source/help.md diff --git a/docs/source/help.md b/docs/source/help.md new file mode 100644 index 0000000..4282fe0 --- /dev/null +++ b/docs/source/help.md @@ -0,0 +1,381 @@ +# Help & Resources + +This page collects extra information about PyGAD: where to get help, how to contribute, and links to tutorials, research papers, and projects that use PyGAD. + +## PyGAD Projects at GitHub + +The PyGAD library is available at PyPI at this page https://pypi.org/project/pygad. PyGAD is built out of a number of open-source GitHub projects. A brief note about these projects is given in the next subsections. + +### [GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython) + +GitHub Link: https://github.com/ahmedfgad/GeneticAlgorithmPython + +[**GeneticAlgorithmPython**](https://github.com/ahmedfgad/GeneticAlgorithmPython) is the first project which is an open-source Python 3 project for implementing the genetic algorithm based on NumPy. + +### [NumPyANN](https://github.com/ahmedfgad/NumPyANN) + +GitHub Link: https://github.com/ahmedfgad/NumPyANN + +[**NumPyANN**](https://github.com/ahmedfgad/NumPyANN) builds artificial neural networks in **Python 3** using **NumPy** from scratch. The purpose of this project is to only implement the **forward pass** of a neural network without using a training algorithm. Currently, it only supports classification and later regression will be also supported. Moreover, only one class is supported per sample. + +### [NeuralGenetic](https://github.com/ahmedfgad/NeuralGenetic) + +GitHub Link: https://github.com/ahmedfgad/NeuralGenetic + +[NeuralGenetic](https://github.com/ahmedfgad/NeuralGenetic) trains neural networks using the genetic algorithm based on the previous 2 projects [GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython) and [NumPyANN](https://github.com/ahmedfgad/NumPyANN). + +### [NumPyCNN](https://github.com/ahmedfgad/NumPyCNN) + +GitHub Link: https://github.com/ahmedfgad/NumPyCNN + +[NumPyCNN](https://github.com/ahmedfgad/NumPyCNN) builds convolutional neural networks using NumPy. The purpose of this project is to only implement the **forward pass** of a convolutional neural network without using a training algorithm. + +### [CNNGenetic](https://github.com/ahmedfgad/CNNGenetic) + +GitHub Link: https://github.com/ahmedfgad/CNNGenetic + +[CNNGenetic](https://github.com/ahmedfgad/CNNGenetic) trains convolutional neural networks using the genetic algorithm. It uses the [GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython) project for building the genetic algorithm. + +### [KerasGA](https://github.com/ahmedfgad/KerasGA) + +GitHub Link: https://github.com/ahmedfgad/KerasGA + +[KerasGA](https://github.com/ahmedfgad/KerasGA) trains [Keras](https://keras.io) models using the genetic algorithm. It uses the [GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython) project for building the genetic algorithm. + +### [TorchGA](https://github.com/ahmedfgad/TorchGA) + +GitHub Link: https://github.com/ahmedfgad/TorchGA + +[TorchGA](https://github.com/ahmedfgad/TorchGA) trains [PyTorch](https://pytorch.org) models using the genetic algorithm. It uses the [GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython) project for building the genetic algorithm. + +[pygad.torchga](https://github.com/ahmedfgad/TorchGA): https://github.com/ahmedfgad/TorchGA + +## Stackoverflow Questions about PyGAD + +### [How do I proceed to load a ga_instance as “.pkl” format in PyGad?](https://stackoverflow.com/questions/67424181/how-do-i-proceed-to-load-a-ga-instance-as-pkl-format-in-pygad) + +### [Binary Classification NN Model Weights not being Trained in PyGAD](https://stackoverflow.com/questions/67276696/binary-classification-nn-model-weights-not-being-trained-in-pygad) + +### [How to solve TSP problem using pyGAD package?](https://stackoverflow.com/questions/66298595/how-to-solve-tsp-problem-using-pygad-package) + +### [How can I save a matplotlib plot that is the output of a function in jupyter?](https://stackoverflow.com/questions/66055330/how-can-i-save-a-matplotlib-plot-that-is-the-output-of-a-function-in-jupyter) + +### [How do I query the best solution of a pyGAD GA instance?](https://stackoverflow.com/questions/65757722/how-do-i-query-the-best-solution-of-a-pygad-ga-instance) + +### [Multi-Input Multi-Output in Genetic algorithm (python)](https://stackoverflow.com/questions/64943711/multi-input-multi-output-in-genetic-algorithm-python) + +https://www.linkedin.com/pulse/validation-short-term-parametric-trading-model-genetic-landolfi + +https://itchef.ru/articles/397758 + +https://audhiaprilliant.medium.com/genetic-algorithm-based-clustering-algorithm-in-searching-robust-initial-centroids-for-k-means-e3b4d892a4be + +https://python.plainenglish.io/validation-of-a-short-term-parametric-trading-model-with-genetic-optimization-and-walk-forward-89708b789af6 + +https://ichi.pro/ko/pygadwa-hamkke-yujeon-algolijeum-eul-sayonghayeo-keras-model-eul-hunlyeonsikineun-bangbeob-173299286377169 + +https://ichi.pro/tr/pygad-ile-genetik-algoritmayi-kullanarak-keras-modelleri-nasil-egitilir-173299286377169 + +https://ichi.pro/ru/kak-obucit-modeli-keras-s-pomos-u-geneticeskogo-algoritma-s-pygad-173299286377169 + +https://blog.csdn.net/sinat_38079265/article/details/108449614 + + + +## Submitting Issues + +If there is an issue using PyGAD, then use any of your preferred option to discuss that issue. + +One way is [submitting an issue](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/new) into this GitHub project ([github.com/ahmedfgad/GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython)) in case something is not working properly or to ask for questions. + +If this is not a proper option for you, then check the [**Contact Us**](https://pygad.readthedocs.io/en/latest/help.html#contact-us) section for more contact details. + +## Ask for Feature + +PyGAD is actively developed with the goal of building a dynamic library for suporting a wide-range of problems to be optimized using the genetic algorithm. + +To ask for a new feature, either [submit an issue](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/new) into this GitHub project ([github.com/ahmedfgad/GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython)) or send an e-mail to ahmed.f.gad@gmail.com. + +Also check the [**Contact Us**](https://pygad.readthedocs.io/en/latest/help.html#contact-us) section for more contact details. + +## Projects Built using PyGAD + +If you created a project that uses PyGAD, then we can support you by mentioning this project here in PyGAD's documentation. + +To do that, please send a message at ahmed.f.gad@gmail.com or check the [**Contact Us**](https://pygad.readthedocs.io/en/latest/help.html#contact-us) section for more contact details. + +Within your message, please send the following details: + +- Project title +- Brief description +- Preferably, a link that directs the readers to your project + +## Tutorials about PyGAD + +### [Adaptive Mutation in Genetic Algorithm with Python Examples](https://neptune.ai/blog/adaptive-mutation-in-genetic-algorithm-with-python-examples) + +In this tutorial, we’ll see why mutation with a fixed number of genes is bad, and how to replace it with adaptive mutation. Using the [PyGAD Python 3 library](https://pygad.readthedocs.io/), we’ll discuss a few examples that use both random and adaptive mutation. + +### [Clustering Using the Genetic Algorithm in Python](https://blog.paperspace.com/clustering-using-the-genetic-algorithm) + +This tutorial discusses how the genetic algorithm is used to cluster data, starting from random clusters and running until the optimal clusters are found. We'll start by briefly revising the K-means clustering algorithm to point out its weak points, which are later solved by the genetic algorithm. The code examples in this tutorial are implemented in Python using the [PyGAD library](https://pygad.readthedocs.io/). + +### [Working with Different Genetic Algorithm Representations in Python](https://blog.paperspace.com/working-with-different-genetic-algorithm-representations-python) + +Depending on the nature of the problem being optimized, the genetic algorithm (GA) supports two different gene representations: binary, and decimal. The binary GA has only two values for its genes, which are 0 and 1. This is easier to manage as its gene values are limited compared to the decimal GA, for which we can use different formats like float or integer, and limited or unlimited ranges. + +This tutorial discusses how the [PyGAD](https://pygad.readthedocs.io/) library supports the two GA representations, binary and decimal. + +### [5 Genetic Algorithm Applications Using PyGAD](https://blog.paperspace.com/genetic-algorithm-applications-using-pygad) + +This tutorial introduces PyGAD, an open-source Python library for implementing the genetic algorithm and training machine learning algorithms. PyGAD supports 19 parameters for customizing the genetic algorithm for various applications. + +Within this tutorial we'll discuss 5 different applications of the genetic algorithm and build them using PyGAD. + +### [Train Neural Networks Using a Genetic Algorithm in Python with PyGAD](https://heartbeat.fritz.ai/train-neural-networks-using-a-genetic-algorithm-in-python-with-pygad-862905048429?gi=ba58ee6b4bbd) + +The genetic algorithm (GA) is a biologically-inspired optimization algorithm. It has in recent years gained importance, as it’s simple while also solving complex problems like travel route optimization, training machine learning algorithms, working with single and multi-objective problems, game playing, and more. + +Deep neural networks are inspired by the idea of how the biological brain works. It’s a universal function approximator, which is capable of simulating any function, and is now used to solve the most complex problems in machine learning. What’s more, they’re able to work with all types of data (images, audio, video, and text). + +Both genetic algorithms (GAs) and neural networks (NNs) are similar, as both are biologically-inspired techniques. This similarity motivates us to create a hybrid of both to see whether a GA can train NNs with high accuracy. + +This tutorial uses [PyGAD](https://pygad.readthedocs.io/), a Python library that supports building and training NNs using a GA. [PyGAD](https://pygad.readthedocs.io/) offers both classification and regression NNs. + +### [Building a Game-Playing Agent for CoinTex Using the Genetic Algorithm](https://blog.paperspace.com/building-agent-for-cointex-using-genetic-algorithm) + +In this tutorial we'll see how to build a game-playing agent using only the genetic algorithm to play a game called [CoinTex](https://play.google.com/store/apps/details?id=coin.tex.cointexreactfast&hl=en), which is developed in the Kivy Python framework. The objective of CoinTex is to collect the randomly distributed coins while avoiding collision with fire and monsters (that move randomly). The source code of CoinTex can be found [on GitHub](https://github.com/ahmedfgad/CoinTex). + +The genetic algorithm is the only AI used here; there is no other machine/deep learning model used with it. We'll implement the genetic algorithm using [PyGad](https://blog.paperspace.com/genetic-algorithm-applications-using-pygad/). This tutorial starts with a quick overview of CoinTex followed by a brief explanation of the genetic algorithm, and how it can be used to create the playing agent. Finally, we'll see how to implement these ideas in Python. + +The source code of the genetic algorithm agent is available [here](https://github.com/ahmedfgad/CoinTex/tree/master/PlayerGA), and you can download the code used in this tutorial from [here](https://github.com/ahmedfgad/CoinTex/tree/master/PlayerGA/TutorialProject). + +### [How To Train Keras Models Using the Genetic Algorithm with PyGAD](https://blog.paperspace.com/train-keras-models-using-genetic-algorithm-with-pygad) + +PyGAD is an open-source Python library for building the genetic algorithm and training machine learning algorithms. It offers a wide range of parameters to customize the genetic algorithm to work with different types of problems. + +PyGAD has its own modules that support building and training neural networks (NNs) and convolutional neural networks (CNNs). Despite these modules working well, they are implemented in Python without any additional optimization measures. This leads to comparatively high computational times for even simple problems. + +The latest PyGAD version, 2.8.0 (released on 20 September 2020), supports a new module to train Keras models. Even though Keras is built in Python, it's fast. The reason is that Keras uses TensorFlow as a backend, and TensorFlow is highly optimized. + +This tutorial discusses how to train Keras models using PyGAD. The discussion includes building Keras models using either the Sequential Model or the Functional API, building an initial population of Keras model parameters, creating an appropriate fitness function, and more. + +[![PyGAD+Keras](https://user-images.githubusercontent.com/16560492/111009628-2b372500-8362-11eb-90cf-01b47d831624.png)](https://blog.paperspace.com/train-keras-models-using-genetic-algorithm-with-pygad) + +### [Train PyTorch Models Using Genetic Algorithm with PyGAD](https://neptune.ai/blog/train-pytorch-models-using-genetic-algorithm-with-pygad) + +[PyGAD](https://pygad.readthedocs.io/) is a genetic algorithm Python 3 library for solving optimization problems. One of these problems is training machine learning algorithms. + +PyGAD has a module called [pygad.kerasga](https://github.com/ahmedfgad/KerasGA). It trains Keras models using the genetic algorithm. On January 3rd, 2021, a new release of [PyGAD 2.10.0](https://pygad.readthedocs.io/) brought a new module called [pygad.torchga](https://github.com/ahmedfgad/TorchGA) to train PyTorch models. It’s very easy to use, but there are a few tricky steps. + +So, in this tutorial, we’ll explore how to use PyGAD to train PyTorch models. + +[![PyGAD+PyTorch](https://user-images.githubusercontent.com/16560492/111009678-5457b580-8362-11eb-899a-39e2f96984df.png)](https://neptune.ai/blog/train-pytorch-models-using-genetic-algorithm-with-pygad) + +### [A Guide to Genetic ‘Learning’ Algorithms for Optimization](https://towardsdatascience.com/a-guide-to-genetic-learning-algorithms-for-optimization-e1067cdc77e7) + +## PyGAD in Other Languages + +### French + +[Cómo los algoritmos genéticos pueden competir con el descenso de gradiente y el backprop](https://www.hebergementwebs.com/nouvelles/comment-les-algorithmes-genetiques-peuvent-rivaliser-avec-la-descente-de-gradient-et-le-backprop) + +Bien que la manière standard d'entraîner les réseaux de neurones soit la descente de gradient et la rétropropagation, il y a d'autres joueurs dans le jeu. L'un d'eux est les algorithmes évolutionnaires, tels que les algorithmes génétiques. + +Utiliser un algorithme génétique pour former un réseau de neurones simple pour résoudre le OpenAI CartPole Jeu. Dans cet article, nous allons former un simple réseau de neurones pour résoudre le OpenAI CartPole . J'utiliserai PyTorch et PyGAD . + +[![Cómo los algoritmos genéticos pueden competir con el descenso de gradiente y el backprop](https://user-images.githubusercontent.com/16560492/111009275-3178d180-8361-11eb-9e86-7fb1519acde7.png)](https://www.hebergementwebs.com/nouvelles/comment-les-algorithmes-genetiques-peuvent-rivaliser-avec-la-descente-de-gradient-et-le-backprop) + +### Spanish + +[Cómo los algoritmos genéticos pueden competir con el descenso de gradiente y el backprop](https://www.hebergementwebs.com/noticias/como-los-algoritmos-geneticos-pueden-competir-con-el-descenso-de-gradiente-y-el-backprop) + +Aunque la forma estandar de entrenar redes neuronales es el descenso de gradiente y la retropropagacion, hay otros jugadores en el juego, uno de ellos son los algoritmos evolutivos, como los algoritmos geneticos. + +Usa un algoritmo genetico para entrenar una red neuronal simple para resolver el Juego OpenAI CartPole. En este articulo, entrenaremos una red neuronal simple para resolver el OpenAI CartPole . Usare PyTorch y PyGAD . + +[![Cómo los algoritmos genéticos pueden competir con el descenso de gradiente y el backprop](https://user-images.githubusercontent.com/16560492/111009257-232ab580-8361-11eb-99a5-7226efbc3065.png)](https://www.hebergementwebs.com/noticias/como-los-algoritmos-geneticos-pueden-competir-con-el-descenso-de-gradiente-y-el-backprop) + +### Korean + +#### [[PyGAD] Python 에서 Genetic Algorithm 을 사용해보기](https://data-newbie.tistory.com/m/685) + +[![Korean-1](https://user-images.githubusercontent.com/16560492/108586306-85bd0280-731b-11eb-874c-7ac4ce1326cd.jpg)](https://data-newbie.tistory.com/m/685) + +파이썬에서 genetic algorithm을 사용하는 패키지들을 다 사용해보진 않았지만, 확장성이 있어보이고, 시도할 일이 있어서 살펴봤다. + +이 패키지에서 가장 인상 깊었던 것은 neural network에서 hyper parameter 탐색을 gradient descent 방식이 아닌 GA로도 할 수 있다는 것이다. + +개인적으로 이 부분이 어느정도 초기치를 잘 잡아줄 수 있는 역할로도 쓸 수 있고, Loss가 gradient descent 하기 어려운 구조에서 대안으로 쓸 수 있을 것으로도 생각된다. + +일단 큰 흐름은 다음과 같이 된다. + +사실 완전히 흐름이나 각 parameter에 대한 이해는 부족한 상황 + +### Turkish + +#### [PyGAD ile Genetik Algoritmayı Kullanarak Keras Modelleri Nasıl Eğitilir](https://erencan34.medium.com/pygad-ile-genetik-algoritmay%C4%B1-kullanarak-keras-modelleri-nas%C4%B1l-e%C4%9Fitilir-cf92639a478c) + +This is a translation of an original English tutorial published at Paperspace: [How To Train Keras Models Using the Genetic Algorithm with PyGAD](https://blog.paperspace.com/train-keras-models-using-genetic-algorithm-with-pygad) + +PyGAD, genetik algoritma oluşturmak ve makine öğrenimi algoritmalarını eğitmek için kullanılan açık kaynaklı bir Python kitaplığıdır. Genetik algoritmayı farklı problem türleri ile çalışacak şekilde özelleştirmek için çok çeşitli parametreler sunar. + +PyGAD, sinir ağları (NN’ler) ve evrişimli sinir ağları (CNN’ler) oluşturmayı ve eğitmeyi destekleyen kendi modüllerine sahiptir. Bu modüllerin iyi çalışmasına rağmen, herhangi bir ek optimizasyon önlemi olmaksızın Python’da uygulanırlar. Bu, basit problemler için bile nispeten yüksek hesaplama sürelerine yol açar. + +En son PyGAD sürümü 2.8.0 (20 Eylül 2020'de piyasaya sürüldü), Keras modellerini eğitmek için yeni bir modülü destekliyor. Keras Python’da oluşturulmuş olsa da hızlıdır. Bunun nedeni, Keras’ın arka uç olarak TensorFlow kullanması ve TensorFlow’un oldukça optimize edilmiş olmasıdır. + +Bu öğreticide, PyGAD kullanılarak Keras modellerinin nasıl eğitileceği anlatılmaktadır. Tartışma, Sıralı Modeli veya İşlevsel API’yi kullanarak Keras modellerini oluşturmayı, Keras model parametrelerinin ilk popülasyonunu oluşturmayı, uygun bir uygunluk işlevi oluşturmayı ve daha fazlasını içerir. + +[![national-cancer-institute-zz_3tCcrk7o-unsplash](https://user-images.githubusercontent.com/16560492/108586601-85be0200-731d-11eb-98a4-161c75a1f099.jpg)](https://erencan34.medium.com/pygad-ile-genetik-algoritmay%C4%B1-kullanarak-keras-modelleri-nas%C4%B1l-e%C4%9Fitilir-cf92639a478c) + +### Hungarian + +#### [Tensorflow alapozó 10. Neurális hálózatok tenyésztése genetikus algoritmussal PyGAD és OpenAI Gym használatával](https://thebojda.medium.com/tensorflow-alapoz%C3%B3-10-24f7767d4a2c) + +Hogy kontextusba helyezzem a genetikus algoritmusokat, ismételjük kicsit át, hogy hogyan működik a gradient descent és a backpropagation, ami a neurális hálók tanításának általános módszere. Az erről írt cikkemet itt tudjátok elolvasni. + +A hálózatok tenyésztéséhez a [PyGAD](https://pygad.readthedocs.io/en/latest/) nevű programkönyvtárat használjuk, így mindenek előtt ezt kell telepítenünk, valamint a Tensorflow-t és a Gym-et, amit Colabban már eleve telepítve kapunk. + +Maga a PyGAD egy teljesen általános genetikus algoritmusok futtatására képes rendszer. Ennek a kiterjesztése a KerasGA, ami az általános motor Tensorflow (Keras) neurális hálókon történő futtatását segíti. A 47. sorban létrehozott KerasGA objektum ennek a kiterjesztésnek a része és arra szolgál, hogy a paraméterként átadott modellből a második paraméterben megadott számosságú populációt hozzon létre. Mivel a hálózatunk 386 állítható paraméterrel rendelkezik, ezért a DNS-ünk itt 386 elemből fog állni. A populáció mérete 10 egyed, így a kezdő populációnk egy 10x386 elemű mátrix lesz. Ezt adjuk át az 51. sorban az initial_population paraméterben. + +[![](https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png)](https://thebojda.medium.com/tensorflow-alapoz%C3%B3-10-24f7767d4a2c) + +### Russian + +#### [PyGAD: библиотека для имплементации генетического алгоритма](https://neurohive.io/ru/frameworki/pygad-biblioteka-dlya-implementacii-geneticheskogo-algoritma) + +PyGAD — это библиотека для имплементации генетического алгоритма. Кроме того, библиотека предоставляет доступ к оптимизированным реализациям алгоритмов машинного обучения. PyGAD разрабатывали на Python 3. + +Библиотека PyGAD поддерживает разные типы скрещивания, мутации и селекции родителя. PyGAD позволяет оптимизировать проблемы с помощью генетического алгоритма через кастомизацию целевой функции. + +Кроме генетического алгоритма, библиотека содержит оптимизированные имплементации алгоритмов машинного обучения. На текущий момент PyGAD поддерживает создание и обучение нейросетей для задач классификации. + +Библиотека находится в стадии активной разработки. Создатели планируют добавление функционала для решения бинарных задач и имплементации новых алгоритмов. + +PyGAD разрабатывали на Python 3.7.3. Зависимости включают в себя NumPy для создания и манипуляции массивами и Matplotlib для визуализации. Один из изкейсов использования инструмента — оптимизация весов, которые удовлетворяют заданной функции. + +[![](https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png)](https://neurohive.io/ru/frameworki/pygad-biblioteka-dlya-implementacii-geneticheskogo-algoritma) + +## Research Papers using PyGAD + +A number of research papers used PyGAD and here are some of them: + +* Alberto Meola, Manuel Winkler, Sören Weinrich, Metaheuristic optimization of data preparation and machine learning hyperparameters for prediction of dynamic methane production, Bioresource Technology, Volume 372, 2023, 128604, ISSN 0960-8524. +* Jaros, Marta, and Jiri Jaros. "Performance-Cost Optimization of Moldable Scientific Workflows." +* Thorat, Divya. "Enhanced genetic algorithm to reduce makespan of multiple jobs in map-reduce application on serverless platform". Diss. Dublin, National College of Ireland, 2020. +* Koch, Chris, and Edgar Dobriban. "AttenGen: Generating Live Attenuated Vaccine Candidates using Machine Learning." (2021). +* Bhardwaj, Bhavya, et al. "Windfarm optimization using Nelder-Mead and Particle Swarm optimization." *2021 7th International Conference on Electrical Energy Systems (ICEES)*. IEEE, 2021. +* Bernardo, Reginald Christian S. and J. Said. “Towards a model-independent reconstruction approach for late-time Hubble data.” (2021). +* Duong, Tri Dung, Qian Li, and Guandong Xu. "Prototype-based Counterfactual Explanation for Causal Classification." *arXiv preprint arXiv:2105.00703* (2021). +* Farrag, Tamer Ahmed, and Ehab E. Elattar. "Optimized Deep Stacked Long Short-Term Memory Network for Long-Term Load Forecasting." *IEEE Access* 9 (2021): 68511-68522. +* Antunes, E. D. O., Caetano, M. F., Marotta, M. A., Araujo, A., Bondan, L., Meneguette, R. I., & Rocha Filho, G. P. (2021, August). Soluções Otimizadas para o Problema de Localização de Máxima Cobertura em Redes Militarizadas 4G/LTE. In *Anais do XXVI Workshop de Gerência e Operação de Redes e Serviços* (pp. 152-165). SBC. +* M. Yani, F. Ardilla, A. A. Saputra and N. Kubota, "Gradient-Free Deep Q-Networks Reinforcement learning: Benchmark and Evaluation," *2021 IEEE Symposium Series on Computational Intelligence (SSCI)*, 2021, pp. 1-5, doi: 10.1109/SSCI50451.2021.9659941. +* Yani, Mohamad, and Naoyuki Kubota. "Deep Convolutional Networks with Genetic Algorithm for Reinforcement Learning Problem." +* Mahendra, Muhammad Ihza, and Isman Kurniawan. "Optimizing Convolutional Neural Network by Using Genetic Algorithm for COVID-19 Detection in Chest X-Ray Image." *2021 International Conference on Data Science and Its Applications (ICoDSA)*. IEEE, 2021. +* Glibota, Vjeko. *Umjeravanje mikroskopskog prometnog modela primjenom genetskog algoritma*. Diss. University of Zagreb. Faculty of Transport and Traffic Sciences. Division of Intelligent Transport Systems and Logistics. Department of Intelligent Transport Systems, 2021. +* Zhu, Mingda. *Genetic Algorithm-based Parameter Identification for Ship Manoeuvring Model under Wind Disturbance*. MS thesis. NTNU, 2021. +* Abdalrahman, Ahmed, and Weihua Zhuang. "Dynamic pricing for differentiated pev charging services using deep reinforcement learning." *IEEE Transactions on Intelligent Transportation Systems* (2020). + +## More Links + +https://rodriguezanton.com/identifying-contact-states-for-2d-objects-using-pygad-and/ + +https://torvaney.github.io/projects/t9-optimised + +## For More Information + +There are different resources that can be used to get started with the genetic algorithm and building it in Python. + +### Tutorial: Implementing Genetic Algorithm in Python + +To start with coding the genetic algorithm, you can check the tutorial titled [**Genetic Algorithm Implementation in Python**](https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad) available at these links: + +- [LinkedIn](https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad) +- [Towards Data Science](https://towardsdatascience.com/genetic-algorithm-implementation-in-python-5ab67bb124a6) +- [KDnuggets](https://www.kdnuggets.com/2018/07/genetic-algorithm-implementation-python.html) + +[This tutorial](https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad) is prepared based on a previous version of the project but it still a good resource to start with coding the genetic algorithm. + +[![Genetic Algorithm Implementation in Python](https://user-images.githubusercontent.com/16560492/78830052-a3c19300-79e7-11ea-8b9b-4b343ea4049c.png)](https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad) + +### Tutorial: Introduction to Genetic Algorithm + +Get started with the genetic algorithm by reading the tutorial titled [**Introduction to Optimization with Genetic Algorithm**](https://www.linkedin.com/pulse/introduction-optimization-genetic-algorithm-ahmed-gad) which is available at these links: + +* [LinkedIn](https://www.linkedin.com/pulse/introduction-optimization-genetic-algorithm-ahmed-gad) +* [Towards Data Science](https://www.kdnuggets.com/2018/03/introduction-optimization-with-genetic-algorithm.html) +* [KDnuggets](https://towardsdatascience.com/introduction-to-optimization-with-genetic-algorithm-2f5001d9964b) + +[![Introduction to Genetic Algorithm](https://user-images.githubusercontent.com/16560492/82078259-26252d00-96e1-11ea-9a02-52a99e1054b9.jpg)](https://www.linkedin.com/pulse/introduction-optimization-genetic-algorithm-ahmed-gad) + +### Tutorial: Build Neural Networks in Python + +Read about building neural networks in Python through the tutorial titled [**Artificial Neural Network Implementation using NumPy and Classification of the Fruits360 Image Dataset**](https://www.linkedin.com/pulse/artificial-neural-network-implementation-using-numpy-fruits360-gad) available at these links: + +* [LinkedIn](https://www.linkedin.com/pulse/artificial-neural-network-implementation-using-numpy-fruits360-gad) +* [Towards Data Science](https://towardsdatascience.com/artificial-neural-network-implementation-using-numpy-and-classification-of-the-fruits360-image-3c56affa4491) +* [KDnuggets](https://www.kdnuggets.com/2019/02/artificial-neural-network-implementation-using-numpy-and-image-classification.html) + +[![Building Neural Networks Python](https://user-images.githubusercontent.com/16560492/82078281-30472b80-96e1-11ea-8017-6a1f4383d602.jpg)](https://www.linkedin.com/pulse/artificial-neural-network-implementation-using-numpy-fruits360-gad) + +### Tutorial: Optimize Neural Networks with Genetic Algorithm + +Read about training neural networks using the genetic algorithm through the tutorial titled [**Artificial Neural Networks Optimization using Genetic Algorithm with Python**](https://www.linkedin.com/pulse/artificial-neural-networks-optimization-using-genetic-ahmed-gad) available at these links: + +- [LinkedIn](https://www.linkedin.com/pulse/artificial-neural-networks-optimization-using-genetic-ahmed-gad) +- [Towards Data Science](https://towardsdatascience.com/artificial-neural-networks-optimization-using-genetic-algorithm-with-python-1fe8ed17733e) +- [KDnuggets](https://www.kdnuggets.com/2019/03/artificial-neural-networks-optimization-genetic-algorithm-python.html) + +[![Training Neural Networks using Genetic Algorithm Python](https://user-images.githubusercontent.com/16560492/82078300-376e3980-96e1-11ea-821c-aa6b8ceb44d4.jpg)](https://www.linkedin.com/pulse/artificial-neural-networks-optimization-using-genetic-ahmed-gad) + +### Tutorial: Building CNN in Python + +To start with coding the genetic algorithm, you can check the tutorial titled [**Building Convolutional Neural Network using NumPy from Scratch**](https://www.linkedin.com/pulse/building-convolutional-neural-network-using-numpy-from-ahmed-gad) available at these links: + +- [LinkedIn](https://www.linkedin.com/pulse/building-convolutional-neural-network-using-numpy-from-ahmed-gad) +- [Towards Data Science](https://towardsdatascience.com/building-convolutional-neural-network-using-numpy-from-scratch-b30aac50e50a) +- [KDnuggets](https://www.kdnuggets.com/2018/04/building-convolutional-neural-network-numpy-scratch.html) +- [Chinese Translation](http://m.aliyun.com/yunqi/articles/585741) + +[This tutorial](https://www.linkedin.com/pulse/building-convolutional-neural-network-using-numpy-from-ahmed-gad)) is prepared based on a previous version of the project but it still a good resource to start with coding CNNs. + +[![Building CNN in Python](https://user-images.githubusercontent.com/16560492/82431022-6c3a1200-9a8e-11ea-8f1b-b055196d76e3.png)](https://www.linkedin.com/pulse/building-convolutional-neural-network-using-numpy-from-ahmed-gad) + +### Tutorial: Derivation of CNN from FCNN + +Get started with the genetic algorithm by reading the tutorial titled [**Derivation of Convolutional Neural Network from Fully Connected Network Step-By-Step**](https://www.linkedin.com/pulse/derivation-convolutional-neural-network-from-fully-connected-gad) which is available at these links: + +* [LinkedIn](https://www.linkedin.com/pulse/derivation-convolutional-neural-network-from-fully-connected-gad) +* [Towards Data Science](https://towardsdatascience.com/derivation-of-convolutional-neural-network-from-fully-connected-network-step-by-step-b42ebafa5275) +* [KDnuggets](https://www.kdnuggets.com/2018/04/derivation-convolutional-neural-network-fully-connected-step-by-step.html) + +[![Derivation of CNN from FCNN](https://user-images.githubusercontent.com/16560492/82431369-db176b00-9a8e-11ea-99bd-e845192873fc.png)](https://www.linkedin.com/pulse/derivation-convolutional-neural-network-from-fully-connected-gad) + +### Book: Practical Computer Vision Applications Using Deep Learning with CNNs + +You can also check my book cited as [**Ahmed Fawzy Gad 'Practical Computer Vision Applications Using Deep Learning with CNNs'. Dec. 2018, Apress, 978-1-4842-4167-7**](https://www.amazon.com/Practical-Computer-Vision-Applications-Learning/dp/1484241665) which discusses neural networks, convolutional neural networks, deep learning, genetic algorithm, and more. + +Find the book at these links: + +- [Amazon](https://www.amazon.com/Practical-Computer-Vision-Applications-Learning/dp/1484241665) +- [Springer](https://link.springer.com/book/10.1007/978-1-4842-4167-7) +- [Apress](https://www.apress.com/gp/book/9781484241660) +- [O'Reilly](https://www.oreilly.com/library/view/practical-computer-vision/9781484241677) +- [Google Books](https://books.google.com.eg/books?id=xLd9DwAAQBAJ) + +![Fig04](https://user-images.githubusercontent.com/16560492/78830077-ae7c2800-79e7-11ea-980b-53b6bd879eeb.jpg) + +## Contact Us + +* E-mail: ahmed.f.gad@gmail.com +* [LinkedIn](https://www.linkedin.com/in/ahmedfgad) +* [Amazon Author Page](https://amazon.com/author/ahmedgad) +* [Heartbeat](https://heartbeat.fritz.ai/@ahmedfgad) +* [Paperspace](https://blog.paperspace.com/author/ahmed) +* [KDnuggets](https://kdnuggets.com/author/ahmed-gad) +* [TowardsDataScience](https://towardsdatascience.com/@ahmedfgad) +* [GitHub](https://github.com/ahmedfgad) + +![PYGAD-LOGO](https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png) + +Thank you for using [PyGAD](https://github.com/ahmedfgad/GeneticAlgorithmPython) :) diff --git a/docs/source/index.md b/docs/source/index.md index 90807f0..9b8f750 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -205,3 +205,10 @@ torchga releases ``` + +```{toctree} +:maxdepth: 1 +:caption: Help & Resources + +help +``` diff --git a/docs/source/releases.md b/docs/source/releases.md index 45a5d4a..9c19869 100644 --- a/docs/source/releases.md +++ b/docs/source/releases.md @@ -657,381 +657,3 @@ Release Date April 8, 2026 21. Instead of using repeated code for converting the data type and rounding the genes during crossover and mutation, the `change_gene_dtype_and_round()` method is called from the `pygad.helper.misc.Helper` class. 22. Fix some documentation issues. https://github.com/ahmedfgad/GeneticAlgorithmPython/pull/336 23. Update the documentation to reflect the recent additions and changes to the library structure. - -## PyGAD Projects at GitHub - -The PyGAD library is available at PyPI at this page https://pypi.org/project/pygad. PyGAD is built out of a number of open-source GitHub projects. A brief note about these projects is given in the next subsections. - -## [GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython) - -GitHub Link: https://github.com/ahmedfgad/GeneticAlgorithmPython - -[**GeneticAlgorithmPython**](https://github.com/ahmedfgad/GeneticAlgorithmPython) is the first project which is an open-source Python 3 project for implementing the genetic algorithm based on NumPy. - -## [NumPyANN](https://github.com/ahmedfgad/NumPyANN) - -GitHub Link: https://github.com/ahmedfgad/NumPyANN - -[**NumPyANN**](https://github.com/ahmedfgad/NumPyANN) builds artificial neural networks in **Python 3** using **NumPy** from scratch. The purpose of this project is to only implement the **forward pass** of a neural network without using a training algorithm. Currently, it only supports classification and later regression will be also supported. Moreover, only one class is supported per sample. - -## [NeuralGenetic](https://github.com/ahmedfgad/NeuralGenetic) - -GitHub Link: https://github.com/ahmedfgad/NeuralGenetic - -[NeuralGenetic](https://github.com/ahmedfgad/NeuralGenetic) trains neural networks using the genetic algorithm based on the previous 2 projects [GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython) and [NumPyANN](https://github.com/ahmedfgad/NumPyANN). - -## [NumPyCNN](https://github.com/ahmedfgad/NumPyCNN) - -GitHub Link: https://github.com/ahmedfgad/NumPyCNN - -[NumPyCNN](https://github.com/ahmedfgad/NumPyCNN) builds convolutional neural networks using NumPy. The purpose of this project is to only implement the **forward pass** of a convolutional neural network without using a training algorithm. - -## [CNNGenetic](https://github.com/ahmedfgad/CNNGenetic) - -GitHub Link: https://github.com/ahmedfgad/CNNGenetic - -[CNNGenetic](https://github.com/ahmedfgad/CNNGenetic) trains convolutional neural networks using the genetic algorithm. It uses the [GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython) project for building the genetic algorithm. - -## [KerasGA](https://github.com/ahmedfgad/KerasGA) - -GitHub Link: https://github.com/ahmedfgad/KerasGA - -[KerasGA](https://github.com/ahmedfgad/KerasGA) trains [Keras](https://keras.io) models using the genetic algorithm. It uses the [GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython) project for building the genetic algorithm. - -## [TorchGA](https://github.com/ahmedfgad/TorchGA) - -GitHub Link: https://github.com/ahmedfgad/TorchGA - -[TorchGA](https://github.com/ahmedfgad/TorchGA) trains [PyTorch](https://pytorch.org) models using the genetic algorithm. It uses the [GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython) project for building the genetic algorithm. - -[pygad.torchga](https://github.com/ahmedfgad/TorchGA): https://github.com/ahmedfgad/TorchGA - -## Stackoverflow Questions about PyGAD - -## [How do I proceed to load a ga_instance as “.pkl” format in PyGad?](https://stackoverflow.com/questions/67424181/how-do-i-proceed-to-load-a-ga-instance-as-pkl-format-in-pygad) - -## [Binary Classification NN Model Weights not being Trained in PyGAD](https://stackoverflow.com/questions/67276696/binary-classification-nn-model-weights-not-being-trained-in-pygad) - -## [How to solve TSP problem using pyGAD package?](https://stackoverflow.com/questions/66298595/how-to-solve-tsp-problem-using-pygad-package) - -## [How can I save a matplotlib plot that is the output of a function in jupyter?](https://stackoverflow.com/questions/66055330/how-can-i-save-a-matplotlib-plot-that-is-the-output-of-a-function-in-jupyter) - -## [How do I query the best solution of a pyGAD GA instance?](https://stackoverflow.com/questions/65757722/how-do-i-query-the-best-solution-of-a-pygad-ga-instance) - -## [Multi-Input Multi-Output in Genetic algorithm (python)](https://stackoverflow.com/questions/64943711/multi-input-multi-output-in-genetic-algorithm-python) - -https://www.linkedin.com/pulse/validation-short-term-parametric-trading-model-genetic-landolfi - -https://itchef.ru/articles/397758 - -https://audhiaprilliant.medium.com/genetic-algorithm-based-clustering-algorithm-in-searching-robust-initial-centroids-for-k-means-e3b4d892a4be - -https://python.plainenglish.io/validation-of-a-short-term-parametric-trading-model-with-genetic-optimization-and-walk-forward-89708b789af6 - -https://ichi.pro/ko/pygadwa-hamkke-yujeon-algolijeum-eul-sayonghayeo-keras-model-eul-hunlyeonsikineun-bangbeob-173299286377169 - -https://ichi.pro/tr/pygad-ile-genetik-algoritmayi-kullanarak-keras-modelleri-nasil-egitilir-173299286377169 - -https://ichi.pro/ru/kak-obucit-modeli-keras-s-pomos-u-geneticeskogo-algoritma-s-pygad-173299286377169 - -https://blog.csdn.net/sinat_38079265/article/details/108449614 - - - -## Submitting Issues - -If there is an issue using PyGAD, then use any of your preferred option to discuss that issue. - -One way is [submitting an issue](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/new) into this GitHub project ([github.com/ahmedfgad/GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython)) in case something is not working properly or to ask for questions. - -If this is not a proper option for you, then check the [**Contact Us**](https://pygad.readthedocs.io/en/latest/releases.html#contact-us) section for more contact details. - -## Ask for Feature - -PyGAD is actively developed with the goal of building a dynamic library for suporting a wide-range of problems to be optimized using the genetic algorithm. - -To ask for a new feature, either [submit an issue](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/new) into this GitHub project ([github.com/ahmedfgad/GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython)) or send an e-mail to ahmed.f.gad@gmail.com. - -Also check the [**Contact Us**](https://pygad.readthedocs.io/en/latest/releases.html#contact-us) section for more contact details. - -## Projects Built using PyGAD - -If you created a project that uses PyGAD, then we can support you by mentioning this project here in PyGAD's documentation. - -To do that, please send a message at ahmed.f.gad@gmail.com or check the [**Contact Us**](https://pygad.readthedocs.io/en/latest/releases.html#contact-us) section for more contact details. - -Within your message, please send the following details: - -- Project title -- Brief description -- Preferably, a link that directs the readers to your project - -## Tutorials about PyGAD - -## [Adaptive Mutation in Genetic Algorithm with Python Examples](https://neptune.ai/blog/adaptive-mutation-in-genetic-algorithm-with-python-examples) - -In this tutorial, we’ll see why mutation with a fixed number of genes is bad, and how to replace it with adaptive mutation. Using the [PyGAD Python 3 library](https://pygad.readthedocs.io/), we’ll discuss a few examples that use both random and adaptive mutation. - -## [Clustering Using the Genetic Algorithm in Python](https://blog.paperspace.com/clustering-using-the-genetic-algorithm) - -This tutorial discusses how the genetic algorithm is used to cluster data, starting from random clusters and running until the optimal clusters are found. We'll start by briefly revising the K-means clustering algorithm to point out its weak points, which are later solved by the genetic algorithm. The code examples in this tutorial are implemented in Python using the [PyGAD library](https://pygad.readthedocs.io/). - -## [Working with Different Genetic Algorithm Representations in Python](https://blog.paperspace.com/working-with-different-genetic-algorithm-representations-python) - -Depending on the nature of the problem being optimized, the genetic algorithm (GA) supports two different gene representations: binary, and decimal. The binary GA has only two values for its genes, which are 0 and 1. This is easier to manage as its gene values are limited compared to the decimal GA, for which we can use different formats like float or integer, and limited or unlimited ranges. - -This tutorial discusses how the [PyGAD](https://pygad.readthedocs.io/) library supports the two GA representations, binary and decimal. - -## [5 Genetic Algorithm Applications Using PyGAD](https://blog.paperspace.com/genetic-algorithm-applications-using-pygad) - -This tutorial introduces PyGAD, an open-source Python library for implementing the genetic algorithm and training machine learning algorithms. PyGAD supports 19 parameters for customizing the genetic algorithm for various applications. - -Within this tutorial we'll discuss 5 different applications of the genetic algorithm and build them using PyGAD. - -## [Train Neural Networks Using a Genetic Algorithm in Python with PyGAD](https://heartbeat.fritz.ai/train-neural-networks-using-a-genetic-algorithm-in-python-with-pygad-862905048429?gi=ba58ee6b4bbd) - -The genetic algorithm (GA) is a biologically-inspired optimization algorithm. It has in recent years gained importance, as it’s simple while also solving complex problems like travel route optimization, training machine learning algorithms, working with single and multi-objective problems, game playing, and more. - -Deep neural networks are inspired by the idea of how the biological brain works. It’s a universal function approximator, which is capable of simulating any function, and is now used to solve the most complex problems in machine learning. What’s more, they’re able to work with all types of data (images, audio, video, and text). - -Both genetic algorithms (GAs) and neural networks (NNs) are similar, as both are biologically-inspired techniques. This similarity motivates us to create a hybrid of both to see whether a GA can train NNs with high accuracy. - -This tutorial uses [PyGAD](https://pygad.readthedocs.io/), a Python library that supports building and training NNs using a GA. [PyGAD](https://pygad.readthedocs.io/) offers both classification and regression NNs. - -## [Building a Game-Playing Agent for CoinTex Using the Genetic Algorithm](https://blog.paperspace.com/building-agent-for-cointex-using-genetic-algorithm) - -In this tutorial we'll see how to build a game-playing agent using only the genetic algorithm to play a game called [CoinTex](https://play.google.com/store/apps/details?id=coin.tex.cointexreactfast&hl=en), which is developed in the Kivy Python framework. The objective of CoinTex is to collect the randomly distributed coins while avoiding collision with fire and monsters (that move randomly). The source code of CoinTex can be found [on GitHub](https://github.com/ahmedfgad/CoinTex). - -The genetic algorithm is the only AI used here; there is no other machine/deep learning model used with it. We'll implement the genetic algorithm using [PyGad](https://blog.paperspace.com/genetic-algorithm-applications-using-pygad/). This tutorial starts with a quick overview of CoinTex followed by a brief explanation of the genetic algorithm, and how it can be used to create the playing agent. Finally, we'll see how to implement these ideas in Python. - -The source code of the genetic algorithm agent is available [here](https://github.com/ahmedfgad/CoinTex/tree/master/PlayerGA), and you can download the code used in this tutorial from [here](https://github.com/ahmedfgad/CoinTex/tree/master/PlayerGA/TutorialProject). - -## [How To Train Keras Models Using the Genetic Algorithm with PyGAD](https://blog.paperspace.com/train-keras-models-using-genetic-algorithm-with-pygad) - -PyGAD is an open-source Python library for building the genetic algorithm and training machine learning algorithms. It offers a wide range of parameters to customize the genetic algorithm to work with different types of problems. - -PyGAD has its own modules that support building and training neural networks (NNs) and convolutional neural networks (CNNs). Despite these modules working well, they are implemented in Python without any additional optimization measures. This leads to comparatively high computational times for even simple problems. - -The latest PyGAD version, 2.8.0 (released on 20 September 2020), supports a new module to train Keras models. Even though Keras is built in Python, it's fast. The reason is that Keras uses TensorFlow as a backend, and TensorFlow is highly optimized. - -This tutorial discusses how to train Keras models using PyGAD. The discussion includes building Keras models using either the Sequential Model or the Functional API, building an initial population of Keras model parameters, creating an appropriate fitness function, and more. - -[![PyGAD+Keras](https://user-images.githubusercontent.com/16560492/111009628-2b372500-8362-11eb-90cf-01b47d831624.png)](https://blog.paperspace.com/train-keras-models-using-genetic-algorithm-with-pygad) - -## [Train PyTorch Models Using Genetic Algorithm with PyGAD](https://neptune.ai/blog/train-pytorch-models-using-genetic-algorithm-with-pygad) - -[PyGAD](https://pygad.readthedocs.io/) is a genetic algorithm Python 3 library for solving optimization problems. One of these problems is training machine learning algorithms. - -PyGAD has a module called [pygad.kerasga](https://github.com/ahmedfgad/KerasGA). It trains Keras models using the genetic algorithm. On January 3rd, 2021, a new release of [PyGAD 2.10.0](https://pygad.readthedocs.io/) brought a new module called [pygad.torchga](https://github.com/ahmedfgad/TorchGA) to train PyTorch models. It’s very easy to use, but there are a few tricky steps. - -So, in this tutorial, we’ll explore how to use PyGAD to train PyTorch models. - -[![PyGAD+PyTorch](https://user-images.githubusercontent.com/16560492/111009678-5457b580-8362-11eb-899a-39e2f96984df.png)](https://neptune.ai/blog/train-pytorch-models-using-genetic-algorithm-with-pygad) - -## [A Guide to Genetic ‘Learning’ Algorithms for Optimization](https://towardsdatascience.com/a-guide-to-genetic-learning-algorithms-for-optimization-e1067cdc77e7) - -## PyGAD in Other Languages - -## French - -[Cómo los algoritmos genéticos pueden competir con el descenso de gradiente y el backprop](https://www.hebergementwebs.com/nouvelles/comment-les-algorithmes-genetiques-peuvent-rivaliser-avec-la-descente-de-gradient-et-le-backprop) - -Bien que la manière standard d'entraîner les réseaux de neurones soit la descente de gradient et la rétropropagation, il y a d'autres joueurs dans le jeu. L'un d'eux est les algorithmes évolutionnaires, tels que les algorithmes génétiques. - -Utiliser un algorithme génétique pour former un réseau de neurones simple pour résoudre le OpenAI CartPole Jeu. Dans cet article, nous allons former un simple réseau de neurones pour résoudre le OpenAI CartPole . J'utiliserai PyTorch et PyGAD . - -[![Cómo los algoritmos genéticos pueden competir con el descenso de gradiente y el backprop](https://user-images.githubusercontent.com/16560492/111009275-3178d180-8361-11eb-9e86-7fb1519acde7.png)](https://www.hebergementwebs.com/nouvelles/comment-les-algorithmes-genetiques-peuvent-rivaliser-avec-la-descente-de-gradient-et-le-backprop) - -## Spanish - -[Cómo los algoritmos genéticos pueden competir con el descenso de gradiente y el backprop](https://www.hebergementwebs.com/noticias/como-los-algoritmos-geneticos-pueden-competir-con-el-descenso-de-gradiente-y-el-backprop) - -Aunque la forma estandar de entrenar redes neuronales es el descenso de gradiente y la retropropagacion, hay otros jugadores en el juego, uno de ellos son los algoritmos evolutivos, como los algoritmos geneticos. - -Usa un algoritmo genetico para entrenar una red neuronal simple para resolver el Juego OpenAI CartPole. En este articulo, entrenaremos una red neuronal simple para resolver el OpenAI CartPole . Usare PyTorch y PyGAD . - -[![Cómo los algoritmos genéticos pueden competir con el descenso de gradiente y el backprop](https://user-images.githubusercontent.com/16560492/111009257-232ab580-8361-11eb-99a5-7226efbc3065.png)](https://www.hebergementwebs.com/noticias/como-los-algoritmos-geneticos-pueden-competir-con-el-descenso-de-gradiente-y-el-backprop) - -## Korean - -### [[PyGAD] Python 에서 Genetic Algorithm 을 사용해보기](https://data-newbie.tistory.com/m/685) - -[![Korean-1](https://user-images.githubusercontent.com/16560492/108586306-85bd0280-731b-11eb-874c-7ac4ce1326cd.jpg)](https://data-newbie.tistory.com/m/685) - -파이썬에서 genetic algorithm을 사용하는 패키지들을 다 사용해보진 않았지만, 확장성이 있어보이고, 시도할 일이 있어서 살펴봤다. - -이 패키지에서 가장 인상 깊었던 것은 neural network에서 hyper parameter 탐색을 gradient descent 방식이 아닌 GA로도 할 수 있다는 것이다. - -개인적으로 이 부분이 어느정도 초기치를 잘 잡아줄 수 있는 역할로도 쓸 수 있고, Loss가 gradient descent 하기 어려운 구조에서 대안으로 쓸 수 있을 것으로도 생각된다. - -일단 큰 흐름은 다음과 같이 된다. - -사실 완전히 흐름이나 각 parameter에 대한 이해는 부족한 상황 - -## Turkish - -### [PyGAD ile Genetik Algoritmayı Kullanarak Keras Modelleri Nasıl Eğitilir](https://erencan34.medium.com/pygad-ile-genetik-algoritmay%C4%B1-kullanarak-keras-modelleri-nas%C4%B1l-e%C4%9Fitilir-cf92639a478c) - -This is a translation of an original English tutorial published at Paperspace: [How To Train Keras Models Using the Genetic Algorithm with PyGAD](https://blog.paperspace.com/train-keras-models-using-genetic-algorithm-with-pygad) - -PyGAD, genetik algoritma oluşturmak ve makine öğrenimi algoritmalarını eğitmek için kullanılan açık kaynaklı bir Python kitaplığıdır. Genetik algoritmayı farklı problem türleri ile çalışacak şekilde özelleştirmek için çok çeşitli parametreler sunar. - -PyGAD, sinir ağları (NN’ler) ve evrişimli sinir ağları (CNN’ler) oluşturmayı ve eğitmeyi destekleyen kendi modüllerine sahiptir. Bu modüllerin iyi çalışmasına rağmen, herhangi bir ek optimizasyon önlemi olmaksızın Python’da uygulanırlar. Bu, basit problemler için bile nispeten yüksek hesaplama sürelerine yol açar. - -En son PyGAD sürümü 2.8.0 (20 Eylül 2020'de piyasaya sürüldü), Keras modellerini eğitmek için yeni bir modülü destekliyor. Keras Python’da oluşturulmuş olsa da hızlıdır. Bunun nedeni, Keras’ın arka uç olarak TensorFlow kullanması ve TensorFlow’un oldukça optimize edilmiş olmasıdır. - -Bu öğreticide, PyGAD kullanılarak Keras modellerinin nasıl eğitileceği anlatılmaktadır. Tartışma, Sıralı Modeli veya İşlevsel API’yi kullanarak Keras modellerini oluşturmayı, Keras model parametrelerinin ilk popülasyonunu oluşturmayı, uygun bir uygunluk işlevi oluşturmayı ve daha fazlasını içerir. - -[![national-cancer-institute-zz_3tCcrk7o-unsplash](https://user-images.githubusercontent.com/16560492/108586601-85be0200-731d-11eb-98a4-161c75a1f099.jpg)](https://erencan34.medium.com/pygad-ile-genetik-algoritmay%C4%B1-kullanarak-keras-modelleri-nas%C4%B1l-e%C4%9Fitilir-cf92639a478c) - -## Hungarian - -### [Tensorflow alapozó 10. Neurális hálózatok tenyésztése genetikus algoritmussal PyGAD és OpenAI Gym használatával](https://thebojda.medium.com/tensorflow-alapoz%C3%B3-10-24f7767d4a2c) - -Hogy kontextusba helyezzem a genetikus algoritmusokat, ismételjük kicsit át, hogy hogyan működik a gradient descent és a backpropagation, ami a neurális hálók tanításának általános módszere. Az erről írt cikkemet itt tudjátok elolvasni. - -A hálózatok tenyésztéséhez a [PyGAD](https://pygad.readthedocs.io/en/latest/) nevű programkönyvtárat használjuk, így mindenek előtt ezt kell telepítenünk, valamint a Tensorflow-t és a Gym-et, amit Colabban már eleve telepítve kapunk. - -Maga a PyGAD egy teljesen általános genetikus algoritmusok futtatására képes rendszer. Ennek a kiterjesztése a KerasGA, ami az általános motor Tensorflow (Keras) neurális hálókon történő futtatását segíti. A 47. sorban létrehozott KerasGA objektum ennek a kiterjesztésnek a része és arra szolgál, hogy a paraméterként átadott modellből a második paraméterben megadott számosságú populációt hozzon létre. Mivel a hálózatunk 386 állítható paraméterrel rendelkezik, ezért a DNS-ünk itt 386 elemből fog állni. A populáció mérete 10 egyed, így a kezdő populációnk egy 10x386 elemű mátrix lesz. Ezt adjuk át az 51. sorban az initial_population paraméterben. - -[![](https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png)](https://thebojda.medium.com/tensorflow-alapoz%C3%B3-10-24f7767d4a2c) - -## Russian - -### [PyGAD: библиотека для имплементации генетического алгоритма](https://neurohive.io/ru/frameworki/pygad-biblioteka-dlya-implementacii-geneticheskogo-algoritma) - -PyGAD — это библиотека для имплементации генетического алгоритма. Кроме того, библиотека предоставляет доступ к оптимизированным реализациям алгоритмов машинного обучения. PyGAD разрабатывали на Python 3. - -Библиотека PyGAD поддерживает разные типы скрещивания, мутации и селекции родителя. PyGAD позволяет оптимизировать проблемы с помощью генетического алгоритма через кастомизацию целевой функции. - -Кроме генетического алгоритма, библиотека содержит оптимизированные имплементации алгоритмов машинного обучения. На текущий момент PyGAD поддерживает создание и обучение нейросетей для задач классификации. - -Библиотека находится в стадии активной разработки. Создатели планируют добавление функционала для решения бинарных задач и имплементации новых алгоритмов. - -PyGAD разрабатывали на Python 3.7.3. Зависимости включают в себя NumPy для создания и манипуляции массивами и Matplotlib для визуализации. Один из изкейсов использования инструмента — оптимизация весов, которые удовлетворяют заданной функции. - -[![](https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png)](https://neurohive.io/ru/frameworki/pygad-biblioteka-dlya-implementacii-geneticheskogo-algoritma) - -## Research Papers using PyGAD - -A number of research papers used PyGAD and here are some of them: - -* Alberto Meola, Manuel Winkler, Sören Weinrich, Metaheuristic optimization of data preparation and machine learning hyperparameters for prediction of dynamic methane production, Bioresource Technology, Volume 372, 2023, 128604, ISSN 0960-8524. -* Jaros, Marta, and Jiri Jaros. "Performance-Cost Optimization of Moldable Scientific Workflows." -* Thorat, Divya. "Enhanced genetic algorithm to reduce makespan of multiple jobs in map-reduce application on serverless platform". Diss. Dublin, National College of Ireland, 2020. -* Koch, Chris, and Edgar Dobriban. "AttenGen: Generating Live Attenuated Vaccine Candidates using Machine Learning." (2021). -* Bhardwaj, Bhavya, et al. "Windfarm optimization using Nelder-Mead and Particle Swarm optimization." *2021 7th International Conference on Electrical Energy Systems (ICEES)*. IEEE, 2021. -* Bernardo, Reginald Christian S. and J. Said. “Towards a model-independent reconstruction approach for late-time Hubble data.” (2021). -* Duong, Tri Dung, Qian Li, and Guandong Xu. "Prototype-based Counterfactual Explanation for Causal Classification." *arXiv preprint arXiv:2105.00703* (2021). -* Farrag, Tamer Ahmed, and Ehab E. Elattar. "Optimized Deep Stacked Long Short-Term Memory Network for Long-Term Load Forecasting." *IEEE Access* 9 (2021): 68511-68522. -* Antunes, E. D. O., Caetano, M. F., Marotta, M. A., Araujo, A., Bondan, L., Meneguette, R. I., & Rocha Filho, G. P. (2021, August). Soluções Otimizadas para o Problema de Localização de Máxima Cobertura em Redes Militarizadas 4G/LTE. In *Anais do XXVI Workshop de Gerência e Operação de Redes e Serviços* (pp. 152-165). SBC. -* M. Yani, F. Ardilla, A. A. Saputra and N. Kubota, "Gradient-Free Deep Q-Networks Reinforcement learning: Benchmark and Evaluation," *2021 IEEE Symposium Series on Computational Intelligence (SSCI)*, 2021, pp. 1-5, doi: 10.1109/SSCI50451.2021.9659941. -* Yani, Mohamad, and Naoyuki Kubota. "Deep Convolutional Networks with Genetic Algorithm for Reinforcement Learning Problem." -* Mahendra, Muhammad Ihza, and Isman Kurniawan. "Optimizing Convolutional Neural Network by Using Genetic Algorithm for COVID-19 Detection in Chest X-Ray Image." *2021 International Conference on Data Science and Its Applications (ICoDSA)*. IEEE, 2021. -* Glibota, Vjeko. *Umjeravanje mikroskopskog prometnog modela primjenom genetskog algoritma*. Diss. University of Zagreb. Faculty of Transport and Traffic Sciences. Division of Intelligent Transport Systems and Logistics. Department of Intelligent Transport Systems, 2021. -* Zhu, Mingda. *Genetic Algorithm-based Parameter Identification for Ship Manoeuvring Model under Wind Disturbance*. MS thesis. NTNU, 2021. -* Abdalrahman, Ahmed, and Weihua Zhuang. "Dynamic pricing for differentiated pev charging services using deep reinforcement learning." *IEEE Transactions on Intelligent Transportation Systems* (2020). - -## More Links - -https://rodriguezanton.com/identifying-contact-states-for-2d-objects-using-pygad-and/ - -https://torvaney.github.io/projects/t9-optimised - -## For More Information - -There are different resources that can be used to get started with the genetic algorithm and building it in Python. - -## Tutorial: Implementing Genetic Algorithm in Python - -To start with coding the genetic algorithm, you can check the tutorial titled [**Genetic Algorithm Implementation in Python**](https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad) available at these links: - -- [LinkedIn](https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad) -- [Towards Data Science](https://towardsdatascience.com/genetic-algorithm-implementation-in-python-5ab67bb124a6) -- [KDnuggets](https://www.kdnuggets.com/2018/07/genetic-algorithm-implementation-python.html) - -[This tutorial](https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad) is prepared based on a previous version of the project but it still a good resource to start with coding the genetic algorithm. - -[![Genetic Algorithm Implementation in Python](https://user-images.githubusercontent.com/16560492/78830052-a3c19300-79e7-11ea-8b9b-4b343ea4049c.png)](https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad) - -## Tutorial: Introduction to Genetic Algorithm - -Get started with the genetic algorithm by reading the tutorial titled [**Introduction to Optimization with Genetic Algorithm**](https://www.linkedin.com/pulse/introduction-optimization-genetic-algorithm-ahmed-gad) which is available at these links: - -* [LinkedIn](https://www.linkedin.com/pulse/introduction-optimization-genetic-algorithm-ahmed-gad) -* [Towards Data Science](https://www.kdnuggets.com/2018/03/introduction-optimization-with-genetic-algorithm.html) -* [KDnuggets](https://towardsdatascience.com/introduction-to-optimization-with-genetic-algorithm-2f5001d9964b) - -[![Introduction to Genetic Algorithm](https://user-images.githubusercontent.com/16560492/82078259-26252d00-96e1-11ea-9a02-52a99e1054b9.jpg)](https://www.linkedin.com/pulse/introduction-optimization-genetic-algorithm-ahmed-gad) - -## Tutorial: Build Neural Networks in Python - -Read about building neural networks in Python through the tutorial titled [**Artificial Neural Network Implementation using NumPy and Classification of the Fruits360 Image Dataset**](https://www.linkedin.com/pulse/artificial-neural-network-implementation-using-numpy-fruits360-gad) available at these links: - -* [LinkedIn](https://www.linkedin.com/pulse/artificial-neural-network-implementation-using-numpy-fruits360-gad) -* [Towards Data Science](https://towardsdatascience.com/artificial-neural-network-implementation-using-numpy-and-classification-of-the-fruits360-image-3c56affa4491) -* [KDnuggets](https://www.kdnuggets.com/2019/02/artificial-neural-network-implementation-using-numpy-and-image-classification.html) - -[![Building Neural Networks Python](https://user-images.githubusercontent.com/16560492/82078281-30472b80-96e1-11ea-8017-6a1f4383d602.jpg)](https://www.linkedin.com/pulse/artificial-neural-network-implementation-using-numpy-fruits360-gad) - -## Tutorial: Optimize Neural Networks with Genetic Algorithm - -Read about training neural networks using the genetic algorithm through the tutorial titled [**Artificial Neural Networks Optimization using Genetic Algorithm with Python**](https://www.linkedin.com/pulse/artificial-neural-networks-optimization-using-genetic-ahmed-gad) available at these links: - -- [LinkedIn](https://www.linkedin.com/pulse/artificial-neural-networks-optimization-using-genetic-ahmed-gad) -- [Towards Data Science](https://towardsdatascience.com/artificial-neural-networks-optimization-using-genetic-algorithm-with-python-1fe8ed17733e) -- [KDnuggets](https://www.kdnuggets.com/2019/03/artificial-neural-networks-optimization-genetic-algorithm-python.html) - -[![Training Neural Networks using Genetic Algorithm Python](https://user-images.githubusercontent.com/16560492/82078300-376e3980-96e1-11ea-821c-aa6b8ceb44d4.jpg)](https://www.linkedin.com/pulse/artificial-neural-networks-optimization-using-genetic-ahmed-gad) - -## Tutorial: Building CNN in Python - -To start with coding the genetic algorithm, you can check the tutorial titled [**Building Convolutional Neural Network using NumPy from Scratch**](https://www.linkedin.com/pulse/building-convolutional-neural-network-using-numpy-from-ahmed-gad) available at these links: - -- [LinkedIn](https://www.linkedin.com/pulse/building-convolutional-neural-network-using-numpy-from-ahmed-gad) -- [Towards Data Science](https://towardsdatascience.com/building-convolutional-neural-network-using-numpy-from-scratch-b30aac50e50a) -- [KDnuggets](https://www.kdnuggets.com/2018/04/building-convolutional-neural-network-numpy-scratch.html) -- [Chinese Translation](http://m.aliyun.com/yunqi/articles/585741) - -[This tutorial](https://www.linkedin.com/pulse/building-convolutional-neural-network-using-numpy-from-ahmed-gad)) is prepared based on a previous version of the project but it still a good resource to start with coding CNNs. - -[![Building CNN in Python](https://user-images.githubusercontent.com/16560492/82431022-6c3a1200-9a8e-11ea-8f1b-b055196d76e3.png)](https://www.linkedin.com/pulse/building-convolutional-neural-network-using-numpy-from-ahmed-gad) - -## Tutorial: Derivation of CNN from FCNN - -Get started with the genetic algorithm by reading the tutorial titled [**Derivation of Convolutional Neural Network from Fully Connected Network Step-By-Step**](https://www.linkedin.com/pulse/derivation-convolutional-neural-network-from-fully-connected-gad) which is available at these links: - -* [LinkedIn](https://www.linkedin.com/pulse/derivation-convolutional-neural-network-from-fully-connected-gad) -* [Towards Data Science](https://towardsdatascience.com/derivation-of-convolutional-neural-network-from-fully-connected-network-step-by-step-b42ebafa5275) -* [KDnuggets](https://www.kdnuggets.com/2018/04/derivation-convolutional-neural-network-fully-connected-step-by-step.html) - -[![Derivation of CNN from FCNN](https://user-images.githubusercontent.com/16560492/82431369-db176b00-9a8e-11ea-99bd-e845192873fc.png)](https://www.linkedin.com/pulse/derivation-convolutional-neural-network-from-fully-connected-gad) - -## Book: Practical Computer Vision Applications Using Deep Learning with CNNs - -You can also check my book cited as [**Ahmed Fawzy Gad 'Practical Computer Vision Applications Using Deep Learning with CNNs'. Dec. 2018, Apress, 978-1-4842-4167-7**](https://www.amazon.com/Practical-Computer-Vision-Applications-Learning/dp/1484241665) which discusses neural networks, convolutional neural networks, deep learning, genetic algorithm, and more. - -Find the book at these links: - -- [Amazon](https://www.amazon.com/Practical-Computer-Vision-Applications-Learning/dp/1484241665) -- [Springer](https://link.springer.com/book/10.1007/978-1-4842-4167-7) -- [Apress](https://www.apress.com/gp/book/9781484241660) -- [O'Reilly](https://www.oreilly.com/library/view/practical-computer-vision/9781484241677) -- [Google Books](https://books.google.com.eg/books?id=xLd9DwAAQBAJ) - -![Fig04](https://user-images.githubusercontent.com/16560492/78830077-ae7c2800-79e7-11ea-980b-53b6bd879eeb.jpg) - -## Contact Us - -* E-mail: ahmed.f.gad@gmail.com -* [LinkedIn](https://www.linkedin.com/in/ahmedfgad) -* [Amazon Author Page](https://amazon.com/author/ahmedgad) -* [Heartbeat](https://heartbeat.fritz.ai/@ahmedfgad) -* [Paperspace](https://blog.paperspace.com/author/ahmed) -* [KDnuggets](https://kdnuggets.com/author/ahmed-gad) -* [TowardsDataScience](https://towardsdatascience.com/@ahmedfgad) -* [GitHub](https://github.com/ahmedfgad) - -![PYGAD-LOGO](https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png) - -Thank you for using [PyGAD](https://github.com/ahmedfgad/GeneticAlgorithmPython) :) \ No newline at end of file diff --git a/docs/source/utils.md b/docs/source/utils.md index af1a04f..e80ffbb 100644 --- a/docs/source/utils.md +++ b/docs/source/utils.md @@ -2,7 +2,7 @@ This section of the documentation discusses the **pygad.utils** module. -PyGAD supports different types of operators for selecting the parents, applying the crossover, and mutation. More features will be added in the future. To ask for a new feature, please check the [Ask for Feature](https://pygad.readthedocs.io/en/latest/releases.html#ask-for-feature) section. +PyGAD supports different types of operators for selecting the parents, applying the crossover, and mutation. More features will be added in the future. To ask for a new feature, please check the [Ask for Feature](https://pygad.readthedocs.io/en/latest/help.html#ask-for-feature) section. The submodules in the `pygad.utils` module are: From 6959c25de80ace3fd66c33a43ab5ca3d16a4db97 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 21 May 2026 15:50:00 -0400 Subject: [PATCH 33/42] docs: split pygad_more into a landing page and six themed pages Break the 1964-line "More About PyGAD" page into a short landing page (intro plus a card grid and a nested toctree) and six focused pages: Multi-Objective Optimization, Controlling Gene Values, Controlling Generations, Fitness Calculation and Performance, Logging and the Lifecycle Summary, and User-Defined Functions and Callbacks. Repoint every internal pygad_more.html#... link to the page that now hosts each section. Heading text is unchanged, so the section anchors are preserved. --- docs/source/custom_functions.md | 116 ++ docs/source/fitness_calculation.md | 363 +++++ docs/source/gene_values.md | 690 ++++++++++ docs/source/generations.md | 309 +++++ docs/source/logging.md | 373 ++++++ docs/source/multi_objective.md | 117 ++ docs/source/pygad.md | 16 +- docs/source/pygad_more.md | 1985 +--------------------------- docs/source/releases.md | 42 +- 9 files changed, 2038 insertions(+), 1973 deletions(-) create mode 100644 docs/source/custom_functions.md create mode 100644 docs/source/fitness_calculation.md create mode 100644 docs/source/gene_values.md create mode 100644 docs/source/generations.md create mode 100644 docs/source/logging.md create mode 100644 docs/source/multi_objective.md diff --git a/docs/source/custom_functions.md b/docs/source/custom_functions.md new file mode 100644 index 0000000..3d547f9 --- /dev/null +++ b/docs/source/custom_functions.md @@ -0,0 +1,116 @@ +# Use Functions and Methods to Build Fitness and Callbacks + +In PyGAD 2.19.0, it is possible to pass user-defined functions or methods to the following parameters: + +1. `fitness_func` +2. `on_start` +3. `on_fitness` +4. `on_parents` +5. `on_crossover` +6. `on_mutation` +7. `on_generation` +8. `on_stop` + +This section gives 2 examples of how to build these handlers using: + +1. Functions. +2. Methods. + +## Assign Functions + +This is a dummy example where the fitness function returns a random value. Note that the instance of the `pygad.GA` class is passed as the last parameter of all functions. + +```python +import pygad +import numpy + +def fitness_func(ga_instance, solution, solution_idx): + return numpy.random.rand() + +def on_start(ga_instance): + print("on_start") + +def on_fitness(ga_instance, last_gen_fitness): + print("on_fitness") + +def on_parents(ga_instance, last_gen_parents): + print("on_parents") + +def on_crossover(ga_instance, last_gen_offspring): + print("on_crossover") + +def on_mutation(ga_instance, last_gen_offspring): + print("on_mutation") + +def on_generation(ga_instance): + print("on_generation\n") + +def on_stop(ga_instance, last_gen_fitness): + print("on_stop") + +ga_instance = pygad.GA(num_generations=5, + num_parents_mating=4, + sol_per_pop=10, + num_genes=2, + on_start=on_start, + on_fitness=on_fitness, + on_parents=on_parents, + on_crossover=on_crossover, + on_mutation=on_mutation, + on_generation=on_generation, + on_stop=on_stop, + fitness_func=fitness_func) + +ga_instance.run() +``` + +## Assign Methods + +The next example has all the methods defined inside the class `Test`. All of the methods accept an additional parameter representing the method's object of the class `Test`. + +All methods accept `self` as the first parameter and the instance of the `pygad.GA` class as the last parameter. + +```python +import pygad +import numpy + +class Test: + def fitness_func(self, ga_instance, solution, solution_idx): + return numpy.random.rand() + + def on_start(self, ga_instance): + print("on_start") + + def on_fitness(self, ga_instance, last_gen_fitness): + print("on_fitness") + + def on_parents(self, ga_instance, last_gen_parents): + print("on_parents") + + def on_crossover(self, ga_instance, last_gen_offspring): + print("on_crossover") + + def on_mutation(self, ga_instance, last_gen_offspring): + print("on_mutation") + + def on_generation(self, ga_instance): + print("on_generation\n") + + def on_stop(self, ga_instance, last_gen_fitness): + print("on_stop") + +ga_instance = pygad.GA(num_generations=5, + num_parents_mating=4, + sol_per_pop=10, + num_genes=2, + on_start=Test().on_start, + on_fitness=Test().on_fitness, + on_parents=Test().on_parents, + on_crossover=Test().on_crossover, + on_mutation=Test().on_mutation, + on_generation=Test().on_generation, + on_stop=Test().on_stop, + fitness_func=Test().fitness_func) + +ga_instance.run() +``` diff --git a/docs/source/fitness_calculation.md b/docs/source/fitness_calculation.md new file mode 100644 index 0000000..a053ae4 --- /dev/null +++ b/docs/source/fitness_calculation.md @@ -0,0 +1,363 @@ +# Fitness Calculation and Performance + +This page covers how PyGAD calculates the fitness efficiently: parallel processing, non-deterministic problems, reusing fitness values, and batch fitness calculation. + +## Parallel Processing in PyGAD + +Starting from [PyGAD 2.17.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-17-0), parallel processing is supported. This section explains how to use parallel processing in PyGAD. + +According to the [PyGAD life cycle](https://pygad.readthedocs.io/en/latest/pygad.html#life-cycle-of-pygad), the computation can be parallelized in only 2 operations: + +1. Population fitness calculation. +2. Mutation. + +The reason is that the calculations in these 2 operations are independent (i.e. each solution/chromosome is handled independently from the others) and can be distributed across different processes or threads. + +For the mutation operation, it does not do intensive calculations on the CPU. Its calculations are simple like flipping the values of some genes from 0 to 1 or adding a random value to some genes. So, it does not take much CPU processing time. Experiments proved that parallelizing the mutation operation across the solutions increases the time instead of reducing it. This is because running multiple processes or threads adds overhead to manage them. Thus, parallel processing cannot be applied on the mutation operation. + +For the population fitness calculation, parallel processing can make a difference and reduce the processing time. But this depends on the type of calculations done in the fitness function. If the fitness function makes intensive calculations and takes much CPU time, then parallel processing will probably help cut down the overall time. + +This section explains how parallel processing works in PyGAD and how to use it. + +### How to Use Parallel Processing in PyGAD + +Starting from [PyGAD 2.17.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-17-0), a new parameter called `parallel_processing` was added to the constructor of the `pygad.GA` class. + +```python +import pygad +... +ga_instance = pygad.GA(..., + parallel_processing=...) +... +``` + +This parameter allows the user to do the following: + +1. Enable parallel processing. +2. Select whether processes or threads are used. +3. Specify the number of processes or threads to be used. + +These are 3 possible values for the `parallel_processing` parameter: + +1) `None`: (Default) It means no parallel processing is used. +2) A positive integer referring to the number of threads to be used (threads, not processes). +3) `list`/`tuple`: If a list or a tuple of exactly 2 elements is assigned, then: + 1) The first element can be either `'process'` or `'thread'` to specify whether processes or threads are used, respectively. + 2) The second element can be: + 1) A positive integer to select the maximum number of processes or threads to be used + 2) `0` to indicate that 0 processes or threads are used. It means no parallel processing. This is identical to setting `parallel_processing=None`. + 3) `None` to use the default value as calculated by the `concurrent.futures` module. + +These are examples of the values assigned to the `parallel_processing` parameter: + +* `parallel_processing=4`: Because the parameter is assigned a positive integer, this means parallel processing is activated where 4 threads are used. +* `parallel_processing=["thread", 5]`: Use parallel processing with 5 threads. This is identical to `parallel_processing=5`. +* `parallel_processing=["process", 8]`: Use parallel processing with 8 processes. +* `parallel_processing=["process", 0]`: As the second element is given the value 0, this means do not use parallel processing. This is identical to `parallel_processing=None`. + +### Examples + +These examples will help you see the difference between using processes and threads. They also give an idea of when parallel processing makes a difference and reduces the time. These are dummy examples where the fitness function always returns 0. + +The first example uses 10 genes, 5 solutions in the population where only 3 solutions mate, and 9999 generations. The fitness function uses a `for` loop with 100 iterations just to have some calculations. In the constructor of the `pygad.GA` class, `parallel_processing=None` means no parallel processing is used. + +```python +import pygad +import time + +def fitness_func(ga_instance, solution, solution_idx): + for _ in range(99): + pass + return 0 + +ga_instance = pygad.GA(num_generations=9999, + num_parents_mating=3, + sol_per_pop=5, + num_genes=10, + fitness_func=fitness_func, + suppress_warnings=True, + parallel_processing=None) + +if __name__ == '__main__': + t1 = time.time() + + ga_instance.run() + + t2 = time.time() + print("Time is", t2-t1) +``` + +When parallel processing is not used, the time it takes to run the genetic algorithm is `1.5` seconds. + +For comparison, let us run a second experiment where parallel processing is used with 5 threads. In this case, it takes `5` seconds. + +```python +... +ga_instance = pygad.GA(..., + parallel_processing=5) +... +``` + +For the third experiment, processes instead of threads are used. Also, only 99 generations are used instead of 9999. The time it takes is `99` seconds. + +```python +... +ga_instance = pygad.GA(num_generations=99, + ..., + parallel_processing=["process", 5]) +... +``` + +This is the summary of the 3 experiments: + +1. No parallel processing & 9999 generations: 1.5 seconds. +2. Parallel processing with 5 threads & 9999 generations: 5 seconds +3. Parallel processing with 5 processes & 99 generations: 99 seconds + +Because the fitness function does not need much CPU time, the normal processing takes the least time. Running processes for this simple problem takes 99 compared to only 5 seconds for threads because managing processes is much heavier than managing threads. Thus, most of the CPU time is for swapping the processes instead of executing the code. + +In the second example, the loop makes 99999999 iterations and only 5 generations are used. With no parallelization, it takes 22 seconds. + +```python +import pygad +import time + +def fitness_func(ga_instance, solution, solution_idx): + for _ in range(99999999): + pass + return 0 + +ga_instance = pygad.GA(num_generations=5, + num_parents_mating=3, + sol_per_pop=5, + num_genes=10, + fitness_func=fitness_func, + suppress_warnings=True, + parallel_processing=None) + +if __name__ == '__main__': + t1 = time.time() + ga_instance.run() + t2 = time.time() + print("Time is", t2-t1) +``` + +It takes 15 seconds when 10 processes are used. + +```python +... +ga_instance = pygad.GA(..., + parallel_processing=["process", 10]) +... +``` + +This is compared to 20 seconds when 10 threads are used. + +```python +... +ga_instance = pygad.GA(..., + parallel_processing=["thread", 10]) +... +``` + +Based on the second example, using parallel processing with 10 processes takes the least time because there is a lot of CPU work. Generally, processes are preferred over threads when most of the work is on the CPU. Threads are preferred over processes in some situations, like doing input/output operations. + +*Before releasing [PyGAD 2.17.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-17-0), [László Fazekas](https://www.linkedin.com/in/l%C3%A1szl%C3%B3-fazekas-2429a912) wrote an article to parallelize the fitness function with PyGAD. Check it: [How Genetic Algorithms Can Compete with Gradient Descent and Backprop](https://hackernoon.com/how-genetic-algorithms-can-compete-with-gradient-descent-and-backprop-9m9t33bq)*. + +## Solve Non-Deterministic Problems + +PyGAD can be used to solve both deterministic and non-deterministic problems. Deterministic problems are those that return the same fitness for the same solution. For non-deterministic problems, a different fitness value may be returned for the same solution. + +By default, PyGAD settings are set to solve deterministic problems. PyGAD can save the explored solutions and their fitness to reuse them in the future. These instance attributes can save the solutions: + +1. `solutions`: Exists if `save_solutions=True`. +2. `best_solutions`: Exists if `save_best_solutions=True`. +3. `last_generation_elitism`: Exists if `keep_elitism` > 0. +4. `last_generation_parents`: Exists if `keep_parents` > 0 or `keep_parents=-1`. + +To configure PyGAD for non-deterministic problems, we have to disable saving the previous solutions. This is by setting these parameters: + +1. `keep_elitism=0` +2. `keep_parents=0` +3. `save_solutions=False` +4. `save_best_solutions=False` + +```python +import pygad +... +ga_instance = pygad.GA(..., + keep_elitism=0, + keep_parents=0, + save_solutions=False, + save_best_solutions=False, + ...) +``` + +This way, PyGAD will not save any explored solution, so the fitness function has to be called for each individual solution. + +## Reuse the Fitness instead of Calling the Fitness Function + +It may happen that a previously explored solution in generation X is explored again in another generation Y (where Y > X). For some problems, calling the fitness function takes much time. + +For deterministic problems, it is better not to call the fitness function for an already explored solution. Instead, reuse the fitness of the old solution. PyGAD supports some options to help you save the time of calling the fitness function for a previously explored solution. + +The parameters explored in this section can be set in the constructor of the `pygad.GA` class. + +The `cal_pop_fitness()` method of the `pygad.GA` class checks these parameters to see if there is a possibility of reusing the fitness instead of calling the fitness function. + +### 1. `save_solutions` + +It defaults to `False`. If set to `True`, then the population of each generation is saved into the `solutions` attribute of the `pygad.GA` instance. In other words, every single solution is saved in the `solutions` attribute. + +### 2. `save_best_solutions` + +It defaults to `False`. If `True`, then it only saves the best solution in every generation. + +### 3. `keep_elitism` + +It accepts an integer and defaults to 1. If set to a positive integer, then it keeps the elitism of one generation available in the next generation. + +### 4. `keep_parents` + +It accepts an integer and defaults to -1. If set to `-1` or a positive integer, then it keeps the parents of one generation available in the next generation. + +## Why the Fitness Function is not Called for Solution at Index 0? + +PyGAD has a parameter called `keep_elitism` which defaults to 1. This parameter defines the number of best solutions in generation **X** to keep in the next generation **X+1**. The best solutions are just copied from generation **X** to generation **X+1** without making any change. + +```python +ga_instance = pygad.GA(..., + keep_elitism=1, + ...) +``` + +The best solutions are copied at the beginning of the population. If `keep_elitism=1`, this means the best solution in generation X is kept in the next generation X+1 at index 0 of the population. If `keep_elitism=2`, this means the 2 best solutions in generation X are kept in the next generation X+1 at indices 0 and 1 of the population. + +Because the fitness values of these best solutions are already calculated in generation X, they are not recalculated at generation X+1 (the fitness function is not called for these solutions again). Instead, their fitness values are reused. This is why no solution with index 0 is passed to the fitness function. + +To force calling the fitness function for each solution in every generation, consider setting `keep_elitism` and `keep_parents` to 0. Moreover, keep the 2 parameters `save_solutions` and `save_best_solutions` to their default value `False`. + +```python +ga_instance = pygad.GA(..., + keep_elitism=0, + keep_parents=0, + save_solutions=False, + save_best_solutions=False, + ...) +``` + +## Batch Fitness Calculation + +In [PyGAD 2.19.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-19-0), a new optional parameter called `fitness_batch_size` is supported to calculate the fitness function in batches. Thanks to [Linan Qiu](https://github.com/linanqiu) for opening the [GitHub issue #136](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/136). + +Its values can be: + +* `1` or `None`: If the `fitness_batch_size` parameter is assigned the value `1` or `None` (default), then the normal flow is used where the fitness function is called for each individual solution. That is if there are 15 solutions, then the fitness function is called 15 times. +* `1 < fitness_batch_size <= sol_per_pop`: If the `fitness_batch_size` parameter is assigned a value satisfying this condition `1 < fitness_batch_size <= sol_per_pop`, then the solutions are grouped into batches of size `fitness_batch_size` and the fitness function is called once for each batch. In this case, the fitness function must return a list/tuple/numpy.ndarray with a length equal to the number of solutions passed. + +### Example without `fitness_batch_size` Parameter + +This is an example where the `fitness_batch_size` parameter is given the value `None` (which is the default value). This is equivalent to using the value `1`. In this case, the fitness function will be called for each solution. This means the fitness function `fitness_func` will receive only a single solution. This is an example of the passed arguments to the fitness function: + +``` +solution: [ 2.52860734, -0.94178795, 2.97545704, 0.84131987, -3.78447118, 2.41008358] +solution_idx: 3 +``` + +The fitness function also must return a single numeric value as the fitness for the passed solution. + +As we have a population of `20` solutions, then the fitness function is called 20 times per generation. For 5 generations, then the fitness function is called `20*5 = 100` times. In PyGAD, the fitness function is called after the last generation too and this adds additional 20 times. So, the total number of calls to the fitness function is `20*5 + 20 = 120`. + +Note that the `keep_elitism` and `keep_parents` parameters are set to `0` to make sure no fitness values are reused and to force calling the fitness function for each individual solution. + +```python +import pygad +import numpy + +function_inputs = [4,-2,3.5,5,-11,-4.7] +desired_output = 44 + +number_of_calls = 0 + +def fitness_func(ga_instance, solution, solution_idx): + global number_of_calls + number_of_calls = number_of_calls + 1 + output = numpy.sum(solution*function_inputs) + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + return fitness + +ga_instance = pygad.GA(num_generations=5, + num_parents_mating=10, + sol_per_pop=20, + fitness_func=fitness_func, + fitness_batch_size=None, + # fitness_batch_size=1, + num_genes=len(function_inputs), + keep_elitism=0, + keep_parents=0) + +ga_instance.run() +print(number_of_calls) +``` + +``` +120 +``` + +### Example with `fitness_batch_size` Parameter + +This is an example where the `fitness_batch_size` parameter is used and assigned the value `4`. This means the solutions will be grouped into batches of `4` solutions. The fitness function will be called once for each batch (called once for every 4 solutions). + +This is an example of the arguments passed to it: + +```python +solutions: + [[ 3.1129432 -0.69123589 1.93792414 2.23772968 -1.54616001 -0.53930799] + [ 3.38508121 0.19890812 1.93792414 2.23095014 -3.08955597 3.10194128] + [ 2.37079504 -0.88819803 2.97545704 1.41742256 -3.95594055 2.45028256] + [ 2.52860734 -0.94178795 2.97545704 0.84131987 -3.78447118 2.41008358]] +solutions_indices: + [16, 17, 18, 19] +``` + +As we have 20 solutions, then there are `20/4 = 5` batches. As a result, the fitness function is called only 5 times per generation instead of 20. For each call, the fitness function receives a batch of 4 solutions. + +As we have 5 generations, then the function will be called `5*5 = 25` times. Given the call to the fitness function after the last generation, then the total number of calls is `5*5 + 5 = 30`. + +```python +import pygad +import numpy + +function_inputs = [4,-2,3.5,5,-11,-4.7] +desired_output = 44 + +number_of_calls = 0 + +def fitness_func_batch(ga_instance, solutions, solutions_indices): + global number_of_calls + number_of_calls = number_of_calls + 1 + batch_fitness = [] + for solution in solutions: + output = numpy.sum(solution*function_inputs) + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + batch_fitness.append(fitness) + return batch_fitness + +ga_instance = pygad.GA(num_generations=5, + num_parents_mating=10, + sol_per_pop=20, + fitness_func=fitness_func_batch, + fitness_batch_size=4, + num_genes=len(function_inputs), + keep_elitism=0, + keep_parents=0) + +ga_instance.run() +print(number_of_calls) +``` + +``` +30 +``` + +When batch fitness calculation is used, then we saved `120 - 30 = 90` calls to the fitness function. diff --git a/docs/source/gene_values.md b/docs/source/gene_values.md new file mode 100644 index 0000000..58752a4 --- /dev/null +++ b/docs/source/gene_values.md @@ -0,0 +1,690 @@ +# Controlling Gene Values + +This page covers the parameters that control the values a gene can take: the `gene_space` and `gene_type` parameters, gene constraints, the `sample_size` parameter, and preventing duplicate genes. + +## Limit the Gene Value Range using the `gene_space` Parameter + +In [PyGAD 2.11.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-11-0), the `gene_space` parameter added a new feature that lets you customize the range of accepted values for each gene. Let us first review the `gene_space` parameter and build on it. + +The `gene_space` parameter lets you set the space of values for each gene. This way, the accepted values for each gene are restricted to the user-defined values. Assume there is a problem with 3 genes, where each gene has a different set of values: + +1. Gene 1: `[0.4, 12, -5, 21.2]` +2. Gene 2: `[-2, 0.3]` +3. Gene 3: `[1.2, 63.2, 7.4]` + +Then, the `gene_space` for this problem is as given below. Note that the order is very important. + +```python +gene_space = [[0.4, 12, -5, 21.2], + [-2, 0.3], + [1.2, 63.2, 7.4]] +``` + +If all genes share the same set of values, then pass a single list to the `gene_space` parameter as follows. In this case, all genes can only take values from this list of 6 values. + +```python +gene_space = [33, 7, 0.5, 95, 6.3, 0.74] +``` + +The previous example restricts the gene values to a fixed set of discrete values. If you want to use a range of discrete values for the gene, then you can use the `range()` function. For example, `range(1, 7)` means the allowed values for the gene are `1, 2, 3, 4, 5, and 6`. You can also use the `numpy.arange()` or `numpy.linspace()` functions for the same purpose. + +The previous examples only work with discrete values, not continuous ones. In [PyGAD 2.11.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-11-0), the `gene_space` parameter can be assigned a dictionary that allows the gene to take values from a continuous range. + +Assuming you want to restrict the gene within this half-open range [1 to 5) where 1 is included and 5 is not. Then simply create a dictionary with 2 items where the keys of the 2 items are: + +1. `'low'`: The minimum value in the range which is 1 in the example. +2. `'high'`: The maximum value in the range which is 5 in the example. + +The dictionary will look like that: + +```python +{'low': 1, + 'high': 5} +``` + +It is not acceptable to add more than 2 items in the dictionary or use other keys than `'low'` and `'high'`. + +For a 3-gene problem, the next code creates a dictionary for each gene to restrict its values in a continuous range. For the first gene, it can take any floating-point value from the range that starts from 1 (inclusive) and ends at 5 (exclusive). + +```python +gene_space = [{'low': 1, 'high': 5}, {'low': 0.3, 'high': 1.4}, {'low': -0.2, 'high': 4.5}] +``` + +## More about the `gene_space` Parameter + +The `gene_space` parameter customizes the space of values of each gene. + +Assuming that all genes have the same global space which include the values 0.3, 5.2, -4, and 8, then those values can be assigned to the `gene_space` parameter as a list, tuple, or range. Here is a list assigned to this parameter. By doing that, then the gene values are restricted to those assigned to the `gene_space` parameter. + +```python +gene_space = [0.3, 5.2, -4, 8] +``` + +If some genes have different spaces, then `gene_space` should accept a nested list or tuple. In this case, the elements could be: + +1. Number (of `int`, `float`, or `NumPy` data types): A single value to be assigned to the gene. This means this gene will have the same value across all generations. +2. `list`, `tuple`, `numpy.ndarray`, or any range like `range`, `numpy.arange()`, or `numpy.linspace`: It holds the space for each individual gene. But this space is usually discrete. That is there is a set of finite values to select from. +3. `dict`: To sample a value for a gene from a continuous range. The dictionary must have 2 mandatory keys which are `"low"` and `"high"` in addition to an optional key which is `"step"`. A random value is returned between the values assigned to the items with `"low"` and `"high"` keys. If the `"step"` exists, then this works as the previous options (i.e. discrete set of values). +4. `None`: A gene with its space set to `None` is initialized randomly from the range specified by the 2 parameters `init_range_low` and `init_range_high`. For mutation, its value is mutated based on a random value from the range specified by the 2 parameters `random_mutation_min_val` and `random_mutation_max_val`. If all elements in the `gene_space` parameter are `None`, the parameter will not have any effect. + +Assuming that a chromosome has 2 genes and each gene has a different value space. Then the `gene_space` could be assigned a nested list/tuple where each element determines the space of a gene. + +According to the next code, the space of the first gene is `[0.4, -5]` which has 2 values and the space for the second gene is `[0.5, -3.2, 8.8, -9]` which has 4 values. + +```python +gene_space = [[0.4, -5], [0.5, -3.2, 8.2, -9]] +``` + +For a 2 gene chromosome, if the first gene space is restricted to the discrete values from 0 to 4 and the second gene is restricted to the values from 10 to 19, then it could be specified according to the next code. + +```python +gene_space = [range(5), range(10, 20)] +``` + +The `gene_space` can also be assigned to a single range, as given below, where the values of all genes are sampled from the same range. + +```python +gene_space = numpy.arange(15) +``` + + The `gene_space` can be assigned a dictionary to sample a value from a continuous range. + +```python +gene_space = {"low": 4, "high": 30} +``` + + A step also can be assigned to the dictionary. This works as if a range is used. + +```python +gene_space = {"low": 4, "high": 30, "step": 2.5} +``` + +> Setting a `dict` like `{"low": 0, "high": 10}` in the `gene_space` means that random values from the continuous range [0, 10) are sampled. Note that `0` is included but `10` is not included while sampling. Thus, the maximum value that could be returned is less than `10` like `9.9999`. But if the user decided to round the genes using, for example, `[float, 2]`, then this value will become 10. So, the user should be careful to the inputs. + +If a `None` is assigned to only a single gene, then its value will be randomly generated initially using the `init_range_low` and `init_range_high` parameters in the `pygad.GA` class's constructor. During mutation, the value is sampled from the range defined by the 2 parameters `random_mutation_min_val` and `random_mutation_max_val`. This is an example where the second gene is given a `None` value. + +```python +gene_space = [range(5), None, numpy.linspace(10, 20, 300)] +``` + +If the user did not assign the initial population to the `initial_population` parameter, the initial population is created randomly based on the `gene_space` parameter. Moreover, the mutation is applied based on this parameter. + +### How Mutation Works with the `gene_space` Parameter? + +Mutation changes based on whether the `gene_space` has a continuous range or discrete set of values. + +If a gene has its **static/discrete space** defined in the `gene_space` parameter, then mutation works by replacing the gene value by a value randomly selected from the gene space. This happens for both `int` and `float` data types. + +For example, the following `gene_space` has the static space `[1, 2, 3]` defined for the first gene. So, this gene can only have a value out of these 3 values. + +```python +Gene space: [[1, 2, 3], + None] +Solution: [1, 5] +``` + +For a solution like `[1, 5]`, then mutation happens for the first gene by simply replacing its current value by a randomly selected value (other than its current value if possible). So, the value 1 will be replaced by either 2 or 3. + +For the second gene, its space is set to `None`. So, traditional mutation happens for this gene by: + +1. Generating a random value from the range defined by the `random_mutation_min_val` and `random_mutation_max_val` parameters. +2. Adding this random value to the current gene's value. + +If its current value is 5 and the random value is `-0.5`, then the new value is 4.5. If the gene type is integer, then the value will be rounded. + +On the other hand, if a gene has a **continuous space** defined in the `gene_space` parameter, then mutation occurs by adding a random value to the current gene value. + +For example, the following `gene_space` has the continuous space defined by the dictionary `{'low': 1, 'high': 5}`. This applies to all genes. So, mutation is applied to one or more selected genes by adding a random value to the current gene value. + +```python +Gene space: {'low': 1, 'high': 5} +Solution: [1.5, 3.4] +``` + +Assuming `random_mutation_min_val=-1` and `random_mutation_max_val=1`, then a random value such as `0.3` can be added to the gene(s) participating in mutation. If only the first gene is mutated, then its new value changes from `1.5` to `1.5+0.3=1.8`. Note that PyGAD verifies that the new value is within the range. In the worst scenarios, the value will be set to either boundary of the continuous range. For example, if the gene value is 1.5 and the random value is -0.55, then the new value is 0.95, which is smaller than the lower boundary 1. So, the gene value will be set to 1. + +If the dictionary has a step like the example below, then it is considered a discrete range and mutation occurs by randomly selecting a value from the set of values. In other words, no random value is added to the gene value. + +```python +Gene space: {'low': 1, 'high': 5, 'step': 0.5} +``` + +## Gene Constraint + +In [PyGAD 3.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-5-0), a new parameter called `gene_constraint` is added to the constructor of the `pygad.GA` class. An instance attribute of the same name is created for any instance of the `pygad.GA` class. + +The `gene_constraint` parameter allows the users to define constraints to be enforced (as much as possible) when selecting a value for a gene. For example, this constraint is enforced when applying mutation to make sure the new gene value after mutation meets the gene constraint. + +The default value of this parameter is `None` which means no genes have constraints. It can be assigned a list but the length of this list must be equal to the number of genes as specified by the `num_genes` parameter. + +When assigned a list, the allowed values for each element are: + +1. `None`: No constraint for the gene. +2. `callable`: A callable/function that accepts 2 parameters: + 1. The solution where the gene exists. + 2. A list or NumPy array of candidate values for the gene. + +It is the user's responsibility to build such callables to filter the passed list of values and return a new list with the values that meet the gene constraint. If no value meets the constraint, return an empty list or NumPy array. + +For example, if the gene must be smaller than 5, then use this callable: + +```python +lambda solution,values: [val for val in values if val<5] +``` + +The first parameter is the solution where the target gene exists. It is passed just in case you would like to compare the gene value with other genes. The second parameter is the list of candidate values for the gene. The objective of the lambda function is to filter the values and return only the valid values that are less than 5. + +A lambda function is used in this case but we can use a regular function: + +```python +def constraint_func(solution,values): + return [val for val in values if val<5] +``` + +Assuming `num_genes` is 2, then here is a valid value for the `gene_constraint` parameter. + +```python +import pygad + +def fitness_func(...): + ... + return fitness + +ga_instance = pygad.GA( + num_genes=2, + sample_size=200, + ... + gene_constraint= + [ + lambda solution,values: [val for val in values if val<5], + lambda solution,values: [val for val in values if val>[solution[0]] + ] +) +``` + +The first lambda function filters the values for the first gene by only considering the gene values that are less than 5. If the passed values is `[-5, 2, 6, 13, 3, 4, 0]`, then the returned filtered values will be `[-5, 2, 3, 4, 0]`. + +The constraint for the second gene makes sure the selected value is larger than the value of the first gene. Assuming the values for the 2 parameters are: + +1. `solution=[1, 4]` +2. `values=[17, 2, -1, 0.5, -2.1, 1.4]` + +Then the value of the first gene in the passed solution is `1`. By filtering the passed values using the callable corresponding to the second gene, then the returned values will be `[17, 2, 1.4]` because these are the only values that are larger than the first gene value of `1`. + +Sometimes it is normal for PyGAD to fail to find a gene value that satisfies the constraint. For example, if the possible gene values are only `[20,30,40]` and the gene constraint restricts the values to be greater than 50, then it is impossible to meet the constraint. + +For some other cases, the constraint can be met but with some changes. For example, increasing the range from which a value is sampled. If the `gene_space` is used and assigned `range(10)`, then the gene constraint can be met by using `range(50)` so that we can find values greater than 50. + +Even if the gene space is already assigned `range(1000)`, it might still not find values that meet the constraints. This is because PyGAD samples a number of values equal to the `sample_size` parameter which defaults to *100*. + +Out of the range of *1000* numbers, all the 100 values might not be satisfying the constraint. This issue could be solved by simply assigning a larger value for the `sample_size` parameter. + +> PyGAD does not yet handle the **dependencies** among the genes in the `gene_constraint` parameter. +> +> This is an example where gene 0 depends on gene 1. To efficiently enforce the constraints, the constraint for gene 1 must be enforced first (if not `None`) then the constraint for gene 0. +> +> ```python +> gene_constraint= +> [ +> lambda solution,values: [val for val in values if val lambda solution,values: [val for val in values if val>10] +> ] +> ``` +> +> PyGAD applies constraints sequentially, starting from the first gene to the last. To ensure correct behavior when genes depend on each other, structure your GA problem so that if gene X depends on gene Y, then gene Y appears earlier in the chromosome (solution) than gene X. As a result, its gene constraint will be earlier in the list. + +### Full Example + +For a full example, please check the [`examples/example_gene_constraint.py` script](https://github.com/ahmedfgad/GeneticAlgorithmPython/blob/master/examples/example_gene_constraint.py). + +## `sample_size` Parameter + +In [PyGAD 3.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-5-0), a new parameter called `sample_size`. It is used in some situations where PyGAD seeks a single value for a gene out of a range. Two of the important use cases are: + +1. Find a unique value for the gene. This is when the `allow_duplicate_genes` parameter is set to `False` to reject the duplicate gene values within the same solution. +2. Find a value that satisfies the `gene_constraint` parameter. + +Given that we are sampling values from a continuous range as defined by the 2 attributes: + +1. `random_mutation_min_val=0` +2. `random_mutation_max_val=100` + +PyGAD samples a fixed number of values out of this continuous range. The number of values in the sample is defined by the `sample_size` parameter which defaults to `100`. + +If the objective is to find a unique value or enforce the gene constraint, then the 100 values are filtered to keep only the values that keep the gene unique or meet the constraint. + +Sometimes 100 values is not enough and PyGAD sometimes fails to find a good value. In this case, it is highly recommended to increase the `sample_size` parameter. This is to create a larger sample to increase the chance of finding a value that meets our objectives. + +## Prevent Duplicates in Gene Values + +In [PyGAD 2.13.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-13-0), a new bool parameter called `allow_duplicate_genes` is supported to control whether duplicates are supported in the chromosome or not. In other words, whether 2 or more genes might have the same exact value. + +If `allow_duplicate_genes=True` (which is the default case), genes may have the same value. If `allow_duplicate_genes=False`, then no 2 genes will have the same value given that there are enough unique values for the genes. + +The next code gives an example to use the `allow_duplicate_genes` parameter. A callback generation function is implemented to print the population after each generation. + +```python +import pygad + +def fitness_func(ga_instance, solution, solution_idx): + return 0 + +def on_generation(ga): + print("Generation", ga.generations_completed) + print(ga.population) + +ga_instance = pygad.GA(num_generations=5, + sol_per_pop=5, + num_genes=4, + mutation_num_genes=3, + random_mutation_min_val=-5, + random_mutation_max_val=5, + num_parents_mating=2, + fitness_func=fitness_func, + gene_type=int, + on_generation=on_generation, + sample_size=200, + allow_duplicate_genes=False) +ga_instance.run() +``` + +Here are the population after the 5 generations. Note how there are no duplicate values. + +```python +Generation 1 +[[ 2 -2 -3 3] + [ 0 1 2 3] + [ 5 -3 6 3] + [-3 1 -2 4] + [-1 0 -2 3]] +Generation 2 +[[-1 0 -2 3] + [-3 1 -2 4] + [ 0 -3 -2 6] + [-3 0 -2 3] + [ 1 -4 2 4]] +Generation 3 +[[ 1 -4 2 4] + [-3 0 -2 3] + [ 4 0 -2 1] + [-4 0 -2 -3] + [-4 2 0 3]] +Generation 4 +[[-4 2 0 3] + [-4 0 -2 -3] + [-2 5 4 -3] + [-1 2 -4 4] + [-4 2 0 -3]] +Generation 5 +[[-4 2 0 -3] + [-1 2 -4 4] + [ 3 4 -4 0] + [-1 0 2 -2] + [-4 2 -1 1]] +``` + +The `allow_duplicate_genes` parameter can be used together with the `gene_space` parameter. Here is an example where each of the 4 genes has the same space of 4 values (1, 2, 3, and 4). + +```python +import pygad + +def fitness_func(ga_instance, solution, solution_idx): + return 0 + +def on_generation(ga): + print("Generation", ga.generations_completed) + print(ga.population) + +ga_instance = pygad.GA(num_generations=1, + sol_per_pop=5, + num_genes=4, + num_parents_mating=2, + fitness_func=fitness_func, + gene_type=int, + gene_space=[[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]], + on_generation=on_generation, + sample_size=200, + allow_duplicate_genes=False) +ga_instance.run() +``` + +Even though all the genes share the same space of values, no 2 genes have the same value, as shown in the next output. + +```python +Generation 1 +[[2 3 1 4] + [2 3 1 4] + [2 4 1 3] + [2 3 1 4] + [1 3 2 4]] +Generation 2 +[[1 3 2 4] + [2 3 1 4] + [1 3 2 4] + [2 3 4 1] + [1 3 4 2]] +Generation 3 +[[1 3 4 2] + [2 3 4 1] + [1 3 4 2] + [3 1 4 2] + [3 2 4 1]] +Generation 4 +[[3 2 4 1] + [3 1 4 2] + [3 2 4 1] + [1 2 4 3] + [1 3 4 2]] +Generation 5 +[[1 3 4 2] + [1 2 4 3] + [2 1 4 3] + [1 2 4 3] + [1 2 4 3]] +``` + +You should give enough values for the genes so that PyGAD can find an alternative when a gene value duplicates another gene. + +If PyGAD fails to find a unique gene value while there is still room to find one, then set the `sample_size` parameter to a larger value. Check the [sample_size Parameter](https://pygad.readthedocs.io/en/latest/gene_values.html#sample-size-parameter) section for more information. + +### Limitation + +There might be 2 duplicate genes where changing either of the 2 duplicating genes will not solve the problem. For example, if `gene_space=[[3, 0, 1], [4, 1, 2], [0, 2], [3, 2, 0]]` and the solution is `[3 2 0 0]`, then the values of the last 2 genes duplicate. There are no possible changes in the last 2 genes to solve the problem. + +This problem can be solved by randomly changing one of the non-duplicating genes to make room for a unique value in one of the 2 duplicating genes. For example, by changing the second gene from 2 to 4, then any of the last 2 genes can take the value 2 and solve the duplicates. The resultant gene is then `[3 4 2 0]`. But this option is not yet supported in PyGAD. + +### Solve Duplicates using a Third Gene + +When `allow_duplicate_genes=False` and a user-defined `gene_space` is used, it sometimes happens that there is no room to solve the duplicates between the 2 genes by simply replacing the value of one gene with another. In [PyGAD 3.1.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-1-0), the duplicates are solved by looking for a third gene that helps solve them. The following examples explain how it works. + +Example 1: + +Let's assume that this gene space is used and there is a solution with 2 duplicate genes with the same value 4. + +```python +Gene space: [[2, 3], + [3, 4], + [4, 5], + [5, 6]] +Solution: [3, 4, 4, 5] +``` + +By checking the gene space, the second gene can have the values `[3, 4]` and the third gene can have the values `[4, 5]`. To solve the duplicates, we change the value of one of these 2 genes. + +If the value of the second gene changes from 4 to 3, then it will duplicate the first gene. If we change the value of the third gene from 4 to 5, then it will duplicate the fourth gene. In short, simply selecting a different value for either the second or third gene will introduce new duplicate genes. + +When there are 2 duplicate genes but there is no way to solve their duplicates, then the solution is to change a third gene that makes a room to solve the duplicates between the 2 genes. + +In our example, duplicates between the second and third genes can be solved by, for example,: + +* Changing the first gene from 3 to 2 then changing the second gene from 4 to 3. +* Or changing the fourth gene from 5 to 6 then changing the third gene from 4 to 5. + +Generally, this is how to solve such duplicates: + +1. For any duplicate gene **GENE1**, select another value. +2. Check which other gene **GENEX** has duplicate with this new value. +3. Find if **GENEX** can have another value that will not cause any more duplicates. If so, go to step 7. +4. If all the other values of **GENEX** will cause duplicates, then try another gene **GENEY**. +5. Repeat steps 3 and 4 until exploring all the genes. +6. If there is no way to solve the duplicates, then we have to keep the duplicate value. +7. If a value for a gene **GENEM** is found that will not cause more duplicates, then use this value for the gene **GENEM**. +8. Replace the value of the gene **GENE1** by the old value of the gene **GENEM**. This solves the duplicates. + +This is an example to solve the duplicate for the solution `[3, 4, 4, 5]`: + +1. Let's use the second gene with value 4. Because the space of this gene is `[3, 4]`, then the only other value we can select is 3. +2. The first gene also has the value 3. +3. The first gene has another value 2 that will not cause more duplicates in the solution. Then go to step 7. +4. Skip. +5. Skip. +6. Skip. +7. The value of the first gene 3 will be replaced by the new value 2. The new solution is [2, 4, 4, 5]. +8. Replace the value of the second gene 4 by the old value of the first gene which is 3. The new solution is [2, 3, 4, 5]. The duplicate is solved. + +Example 2: + +```python +Gene space: [[0, 1], + [1, 2], + [2, 3], + [3, 4]] +Solution: [1, 2, 2, 3] +``` + +The quick summary is: + +* Change the value of the first gene from 1 to 0. The solution becomes [0, 2, 2, 3]. +* Change the value of the second gene from 2 to 1. The solution becomes [0, 1, 2, 3]. The duplicate is solved. + +## More about the `gene_type` Parameter + +The `gene_type` parameter allows the user to control the data type for all genes at once or each individual gene. In [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0), the `gene_type` parameter also supports customizing the precision for `float` data types. As a result, the `gene_type` parameter helps to: + +1. Select a data type for all genes with or without precision. +2. Select a data type for each individual gene with or without precision. + +Let us look at some examples. + +### Data Type for All Genes without Precision + +The data type for all genes can be specified by assigning the numeric data type directly to the `gene_type` parameter. This is an example to make all genes of `int` data types. + +```python +gene_type=int +``` + +Given that the supported numeric data types of PyGAD include Python's `int` and `float` in addition to all numeric types of `NumPy`, then any of these types can be assigned to the `gene_type` parameter. + +If no precision is specified for a `float` data type, then the complete floating-point number is kept. + +The next code uses an `int` data type for all genes where the genes in the initial and final population are only integers. + +```python +import pygad +import numpy + +equation_inputs = [4, -2, 3.5, 8, -2] +desired_output = 2671.1234 + +def fitness_func(ga_instance, solution, solution_idx): + output = numpy.sum(solution * equation_inputs) + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + return fitness + +ga_instance = pygad.GA(num_generations=10, + sol_per_pop=5, + num_parents_mating=2, + num_genes=len(equation_inputs), + fitness_func=fitness_func, + gene_type=int) + +print("Initial Population") +print(ga_instance.initial_population) + +ga_instance.run() + +print("Final Population") +print(ga_instance.population) +``` + +```python +Initial Population +[[ 1 -1 2 0 -3] + [ 0 -2 0 -3 -1] + [ 0 -1 -1 2 0] + [-2 3 -2 3 3] + [ 0 0 2 -2 -2]] + +Final Population +[[ 1 -1 2 2 0] + [ 1 -1 2 2 0] + [ 1 -1 2 2 0] + [ 1 -1 2 2 0] + [ 1 -1 2 2 0]] +``` + +### Data Type for All Genes with Precision + +A precision can only be specified for a `float` data type and cannot be specified for integers. Here is an example to use a precision of 3 for the `float` data type. In this case, all genes are of type `float` and their maximum precision is 3. + +```python +gene_type=[float, 3] +``` + +The next code prints the initial and final population where the genes are of type `float` with precision 3. + +```python +import pygad +import numpy + +equation_inputs = [4, -2, 3.5, 8, -2] +desired_output = 2671.1234 + +def fitness_func(ga_instance, solution, solution_idx): + output = numpy.sum(solution * equation_inputs) + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + + return fitness + +ga_instance = pygad.GA(num_generations=10, + sol_per_pop=5, + num_parents_mating=2, + num_genes=len(equation_inputs), + fitness_func=fitness_func, + gene_type=[float, 3]) + +print("Initial Population") +print(ga_instance.initial_population) + +ga_instance.run() + +print("Final Population") +print(ga_instance.population) +``` + +```python +Initial Population +[[-2.417 -0.487 3.623 2.457 -2.362] + [-1.231 0.079 -1.63 1.629 -2.637] + [ 0.692 -2.098 0.705 0.914 -3.633] + [ 2.637 -1.339 -1.107 -0.781 -3.896] + [-1.495 1.378 -1.026 3.522 2.379]] + +Final Population +[[ 1.714 -1.024 3.623 3.185 -2.362] + [ 0.692 -1.024 3.623 3.185 -2.362] + [ 0.692 -1.024 3.623 3.375 -2.362] + [ 0.692 -1.024 4.041 3.185 -2.362] + [ 1.714 -0.644 3.623 3.185 -2.362]] +``` + +### Data Type for each Individual Gene without Precision + +In [PyGAD 2.14.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-14-0), the `gene_type` parameter allows customizing the gene type for each individual gene. This is by using a `list`/`tuple`/`numpy.ndarray` with number of elements equal to the number of genes. For each element, a type is specified for the corresponding gene. + +This is an example for a 5-gene problem where different types are assigned to the genes. + +```python +gene_type=[int, float, numpy.float16, numpy.int8, float] +``` + +This is a complete code that prints the initial and final population for a custom-gene data type. + +```python +import pygad +import numpy + +equation_inputs = [4, -2, 3.5, 8, -2] +desired_output = 2671.1234 + +def fitness_func(ga_instance, solution, solution_idx): + output = numpy.sum(solution * equation_inputs) + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + return fitness + +ga_instance = pygad.GA(num_generations=10, + sol_per_pop=5, + num_parents_mating=2, + num_genes=len(equation_inputs), + fitness_func=fitness_func, + gene_type=[int, float, numpy.float16, numpy.int8, float]) + +print("Initial Population") +print(ga_instance.initial_population) + +ga_instance.run() + +print("Final Population") +print(ga_instance.population) +``` + +```python +Initial Population +[[0 0.8615522360026828 0.7021484375 -2 3.5301821368185866] + [-3 2.648189378595294 -3.830078125 1 -0.9586271572917742] + [3 3.7729827570110714 1.2529296875 -3 1.395741994211889] + [0 1.0490687178053282 1.51953125 -2 0.7243617940450235] + [0 -0.6550158436937226 -2.861328125 -2 1.8212734549263097]] + +Final Population +[[3 3.7729827570110714 2.055 0 0.7243617940450235] + [3 3.7729827570110714 1.458 0 -0.14638754050305036] + [3 3.7729827570110714 1.458 0 0.0869406120516778] + [3 3.7729827570110714 1.458 0 0.7243617940450235] + [3 3.7729827570110714 1.458 0 -0.14638754050305036]] +``` + +### Data Type for each Individual Gene with Precision + +The precision can also be specified for the `float` data types as in the next line where the second gene precision is 2 and last gene precision is 1. + +```python +gene_type=[int, [float, 2], numpy.float16, numpy.int8, [float, 1]] +``` + +This is a complete example where the initial and final populations are printed where the genes comply with the data types and precisions specified. + +```python +import pygad +import numpy + +equation_inputs = [4, -2, 3.5, 8, -2] +desired_output = 2671.1234 + +def fitness_func(ga_instance, solution, solution_idx): + output = numpy.sum(solution * equation_inputs) + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + return fitness + +ga_instance = pygad.GA(num_generations=10, + sol_per_pop=5, + num_parents_mating=2, + num_genes=len(equation_inputs), + fitness_func=fitness_func, + gene_type=[int, [float, 2], numpy.float16, numpy.int8, [float, 1]]) + +print("Initial Population") +print(ga_instance.initial_population) + +ga_instance.run() + +print("Final Population") +print(ga_instance.population) +``` + +```python +Initial Population +[[-2 -1.22 1.716796875 -1 0.2] + [-1 -1.58 -3.091796875 0 -1.3] + [3 3.35 -0.107421875 1 -3.3] + [-2 -3.58 -1.779296875 0 0.6] + [2 -3.73 2.65234375 3 -0.5]] + +Final Population +[[2 -4.22 3.47 3 -1.3] + [2 -3.73 3.47 3 -1.3] + [2 -4.22 3.47 2 -1.3] + [2 -4.58 3.47 3 -1.3] + [2 -3.73 3.47 3 -1.3]] +``` diff --git a/docs/source/generations.md b/docs/source/generations.md new file mode 100644 index 0000000..ac37938 --- /dev/null +++ b/docs/source/generations.md @@ -0,0 +1,309 @@ +# Controlling Generations + +This page covers how PyGAD controls evolution across generations: when to stop, elitism, the random seed, saving and continuing progress, and changing the population size. + +## Stop at Any Generation + +In [PyGAD 2.4.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-4-0), it is possible to stop the genetic algorithm after any generation. All you need to do is return the string `"stop"` in the `on_generation` callback function. When this callback function is implemented and assigned to the `on_generation` parameter in the constructor of the `pygad.GA` class, the algorithm stops right after it completes its current generation. Here is an example. + +Assume the user wants to stop the algorithm either after 100 generations or when a condition is met. The user can assign a value of 100 to the `num_generations` parameter of the `pygad.GA` class constructor. + +The condition that stops the algorithm is written in a callback function like the one in the next code. If the fitness value of the best solution exceeds 70, then the string `"stop"` is returned. + +```python +def func_generation(ga_instance): + if ga_instance.best_solution()[1] >= 70: + return "stop" +``` + +## Stop Criteria + +In [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0), a new parameter named `stop_criteria` is added to the constructor of the `pygad.GA` class. It helps to stop the evolution based on some criteria. It can be assigned one or more criteria. + +Each criterion is passed as `str` that consists of 2 parts: + +1. Stop word. +2. Number. + +It takes this form: + +```python +"word_num" +``` + +The current 2 supported words are `reach` and `saturate`. + +The `reach` word stops the `run()` method if the fitness value is equal to or greater than a given fitness value. An example for `reach` is `"reach_40"` which stops the evolution if the fitness is >= 40. + +`saturate` stops the evolution if the fitness saturates for a given number of consecutive generations. An example for `saturate` is `"saturate_7"` which means stop the `run()` method if the fitness does not change for 7 consecutive generations. + +Here is an example that stops the evolution if either the fitness value reached `127.4` or if the fitness saturates for `15` generations. + +```python +import pygad +import numpy + +equation_inputs = [4, -2, 3.5, 8, 9, 4] +desired_output = 44 + +def fitness_func(ga_instance, solution, solution_idx): + output = numpy.sum(solution * equation_inputs) + + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + + return fitness + +ga_instance = pygad.GA(num_generations=200, + sol_per_pop=10, + num_parents_mating=4, + num_genes=len(equation_inputs), + fitness_func=fitness_func, + stop_criteria=["reach_127.4", "saturate_15"]) + +ga_instance.run() +print(f"Number of generations passed is {ga_instance.generations_completed}") +``` + +### Multi-Objective Stop Criteria + +When multi-objective is used, then there are 2 options to use the `stop_criteria` parameter with the `reach` keyword: + +1. Pass a single value to use along the `reach` keyword to use across all the objectives. +2. Pass multiple values along the `reach` keyword. But the number of values must equal the number of objectives. + +For the `saturate` keyword, it is independent of the number of objectives. + +Suppose there are 3 objectives. Here is a working example. It stops when the fitness values of the 3 objectives reach or exceed 10, 20, and 30, respectively. + +```python +stop_criteria='reach_10_20_30' +``` + +More than one criterion can be used together. In this case, pass the `stop_criteria` parameter as an iterable. This is an example. It stops when either of these 2 conditions hold: + +1. The fitness values of the 3 objectives reach or exceed 10, 20, and 30, respectively. +2. The fitness values of the 3 objectives reach or exceed 90, -5.7, and 10, respectively. + +```python +stop_criteria=['reach_10_20_30', 'reach_90_-5.7_10'] +``` + +## Elitism Selection + +Starting from [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0), there is a parameter called `keep_elitism`. It takes an integer that sets how many of the best solutions (the elitism) are kept in the next generation. It defaults to `1`, so only the best solution is kept by default. + +The best solutions are copied to the next generation without any change. Crossover and mutation do not touch them. This makes sure the best solutions found so far are never lost. + +In the next example, the `keep_elitism` parameter in the constructor of the `pygad.GA` class is set to `2`. So, the best 2 solutions in each generation are kept in the next generation. + +```python +import numpy +import pygad + +function_inputs = [4,-2,3.5,5,-11,-4.7] +desired_output = 44 + +def fitness_func(ga_instance, solution, solution_idx): + output = numpy.sum(solution*function_inputs) + fitness = 1.0 / numpy.abs(output - desired_output) + return fitness + +ga_instance = pygad.GA(num_generations=2, + num_parents_mating=3, + fitness_func=fitness_func, + num_genes=6, + sol_per_pop=5, + keep_elitism=2) + +ga_instance.run() +``` + +The value passed to the `keep_elitism` parameter must meet 2 conditions: + +1. It must be `>= 0`. +2. It must be `<= sol_per_pop`. Its value cannot be more than the number of solutions in the population. + +In the previous example, if `keep_elitism` is set equal to `sol_per_pop` (which is `5`), then there is no evolution at all, as shown in the next figure. This is because all the 5 solutions are kept as elitism in the next generation, so no offspring are created. + +```python +... + +ga_instance = pygad.GA(..., + sol_per_pop=5, + keep_elitism=5) + +ga_instance.run() +``` + + + +![elitism_kills_evolution](https://user-images.githubusercontent.com/16560492/189273225-67ffad41-97ab-45e1-9324-429705e17b20.png) + +### How the Number of Offspring Is Decided + +PyGAD has two parameters that decide how many solutions are carried over to the next generation: + +- `keep_elitism`: keeps the best solutions (the elitism). +- `keep_parents`: keeps the selected parents. + +Only one of them is used at a time, and `keep_elitism` has priority. If `keep_elitism` is not zero, then `keep_parents` is ignored. Because `keep_elitism` defaults to `1`, the `keep_parents` parameter has no effect by default. To use `keep_parents`, set `keep_elitism=0`. + +The number of kept solutions decides how many offspring are created. The rest of the population is filled with new offspring: + +``` +number of offspring = sol_per_pop - (number of kept solutions) +``` + +The next tree shows how the two parameters decide the number of offspring. + +:::{figure} images/offspring_decision_tree.* +:alt: Decision tree showing how keep_elitism and keep_parents decide the number of offspring +:width: 680px +:align: center + +How `keep_elitism` and `keep_parents` decide the number of offspring. +::: + +There are four cases: + +| `keep_elitism` | `keep_parents` | What is kept | Number of offspring | +| --- | --- | --- | --- | +| `> 0` | ignored | the best `keep_elitism` solutions | `sol_per_pop - keep_elitism` | +| `0` | `-1` | all the parents | `sol_per_pop - num_parents_mating` | +| `0` | `0` | nothing | `sol_per_pop` | +| `0` | `> 0` | the best `keep_parents` parents | `sol_per_pop - keep_parents` | + +The kept solutions are placed at the top of the next population, starting at index 0. The offspring fill the slots that remain. + +:::{figure} images/population_assembly.* +:alt: The kept solutions sit at the top of the next population and the offspring fill the rest +:width: 620px +:align: center + +The kept solutions are copied to the top of the population. The offspring fill the rest. +::: + +## Random Seed + +In [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0), a new parameter called `random_seed` is supported. Its value is used as a seed for the random function generators. + + PyGAD uses random functions in these 2 libraries: + +1. NumPy +2. random + +The `random_seed` parameter defaults to `None` which means no seed is used. As a result, different random numbers are generated for each run of PyGAD. + +If this parameter is assigned a proper seed, then the results will be reproducible. In the next example, the integer 2 is used as a random seed. + +```python +import numpy +import pygad + +function_inputs = [4,-2,3.5,5,-11,-4.7] +desired_output = 44 + +def fitness_func(ga_instance, solution, solution_idx): + output = numpy.sum(solution*function_inputs) + fitness = 1.0 / numpy.abs(output - desired_output) + return fitness + +ga_instance = pygad.GA(num_generations=2, + num_parents_mating=3, + fitness_func=fitness_func, + sol_per_pop=5, + num_genes=6, + random_seed=2) + +ga_instance.run() +best_solution, best_solution_fitness, best_match_idx = ga_instance.best_solution() +print(best_solution) +print(best_solution_fitness) +``` + +This is the best solution found and its fitness value. + +``` +[ 2.77249188 -4.06570662 0.04196872 -3.47770796 -0.57502138 -3.22775267] +0.04872203136549972 +``` + +After running the code again, it will find the same result. + +``` +[ 2.77249188 -4.06570662 0.04196872 -3.47770796 -0.57502138 -3.22775267] +0.04872203136549972 +``` + +## Continue without Losing Progress + +In [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0), and thanks for [Felix Bernhard](https://github.com/FeBe95) for opening [this GitHub issue](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/123#issuecomment-1203035106), the values of these 4 instance attributes are no longer reset after each call to the `run()` method. + +1. `self.best_solutions` +2. `self.best_solutions_fitness` +3. `self.solutions` +4. `self.solutions_fitness` + +This helps the user to continue where the last run stopped without losing the values of these 4 attributes. + +Now, the user can save the model by calling the `save()` method. + +```python +import pygad + +def fitness_func(ga_instance, solution, solution_idx): + ... + return fitness + +ga_instance = pygad.GA(...) + +ga_instance.run() + +ga_instance.plot_fitness() + +ga_instance.save("pygad_GA") +``` + +Then the saved model is loaded by calling the `load()` function. After calling the `run()` method over the loaded instance, then the data from the previous 4 attributes are not reset but extended with the new data. + +```python +import pygad + +def fitness_func(ga_instance, solution, solution_idx): + ... + return fitness + +loaded_ga_instance = pygad.load("pygad_GA") + +loaded_ga_instance.run() + +loaded_ga_instance.plot_fitness() +``` + +The plot created by the `plot_fitness()` method will show the data collected from both the runs. + +Note that the 2 attributes (`self.best_solutions` and `self.best_solutions_fitness`) only work if the `save_best_solutions` parameter is set to `True`. Also, the 2 attributes (`self.solutions` and `self.solutions_fitness`) only work if the `save_solutions` parameter is `True`. + +## Change Population Size during Runtime + +Starting from [PyGAD 3.3.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-3-0), the population size can be changed during runtime. In other words, the number of solutions/chromosomes and the number of genes can be changed. + +The user has to carefully arrange the list of *parameters* and *instance attributes* that have to be changed to keep the GA consistent before and after changing the population size. Generally, change everything that would be used during the GA evolution. + +> CAUTION: If the user fails to change a parameter or an instance attribute that is needed to keep the GA running after the population size changes, errors will arise. + +These are examples of the parameters that the user should decide whether to change. The user should check the [list of parameters](https://pygad.readthedocs.io/en/latest/pygad.html#init) and decide what to change. + +1. `population`: The population. It *must* be changed. +2. `num_offspring`: The number of offspring to produce from the crossover and mutation operations. Change this parameter if the number of offspring has to change to match the new population size. +3. `num_parents_mating`: The number of solutions to select as parents. Change this parameter if the number of parents has to change to match the new population size. +4. `fitness_func`: If the way of calculating the fitness changes with the new population size, then the fitness function has to be changed. +5. `sol_per_pop`: The number of solutions per population. It is not critical to change it but it is recommended to keep this number consistent with the number of solutions in the `population` parameter. + +These are examples of the instance attributes that might be changed. The user should check the [list of instance attributes](https://pygad.readthedocs.io/en/latest/pygad.html#other-instance-attributes-methods) and decide what to change. + +1. All the `last_generation_*` attributes + 1. `last_generation_fitness`: A 1D NumPy array of fitness values of the population. + 2. `last_generation_parents` and `last_generation_parents_indices`: Two NumPy arrays: 2D array representing the parents and 1D array of the parents indices. + 3. `last_generation_elitism` and `last_generation_elitism_indices`: Must be changed if `keep_elitism != 0`. The default value of `keep_elitism` is 1. Two NumPy arrays: 2D array representing the elitism and 1D array of the elitism indices. +2. `pop_size`: The population size. diff --git a/docs/source/logging.md b/docs/source/logging.md new file mode 100644 index 0000000..f70ff3b --- /dev/null +++ b/docs/source/logging.md @@ -0,0 +1,373 @@ +# Logging and the Lifecycle Summary + +This page covers how to see what PyGAD is doing: printing a lifecycle summary and logging the outputs. + +## Print Lifecycle Summary + +In [PyGAD 2.19.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-19-0), a new method called `summary()` is supported. It prints a Keras-like summary of the PyGAD lifecycle showing the steps, callback functions, parameters, etc. + + This method accepts the following parameters: + +- `line_length=70`: An integer representing the length of the single line in characters. +- `fill_character=" "`: A character to fill the lines. +- `line_character="-"`: A character for creating a line separator. +- `line_character2="="`: A secondary character to create a line separator. +- `columns_equal_len=False`: The table rows are split into equal-sized columns or split subjective to the width needed. +- `print_step_parameters=True`: Whether to print extra parameters about each step inside the step. If `print_step_parameters=False` and `print_parameters_summary=True`, then the parameters of each step are printed at the end of the table. +- `print_parameters_summary=True`: Whether to print parameters summary at the end of the table. If `print_step_parameters=False`, then the parameters of each step are printed at the end of the table too. + +Here is a quick example. + +```python +import pygad +import numpy + +function_inputs = [4,-2,3.5,5,-11,-4.7] +desired_output = 44 + +def genetic_fitness(solution, solution_idx): + output = numpy.sum(solution*function_inputs) + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + return fitness + +def on_gen(ga): + pass + +def on_crossover_callback(a, b): + pass + +ga_instance = pygad.GA(num_generations=100, + num_parents_mating=10, + sol_per_pop=20, + num_genes=len(function_inputs), + on_crossover=on_crossover_callback, + on_generation=on_gen, + parallel_processing=2, + stop_criteria="reach_10", + fitness_batch_size=4, + crossover_probability=0.4, + fitness_func=genetic_fitness) +``` + +Then call the `summary()` method to print the summary with the default parameters. Note that entries for the crossover and generation callbacks are created because they are implemented through `on_crossover_callback()` and `on_gen()`, respectively. + +```python +ga_instance.summary() +``` + +```bash +---------------------------------------------------------------------- + PyGAD Lifecycle +====================================================================== +Step Handler Output Shape +====================================================================== +Fitness Function genetic_fitness() (1) +Fitness batch size: 4 +---------------------------------------------------------------------- +Parent Selection steady_state_selection() (10, 6) +Number of Parents: 10 +---------------------------------------------------------------------- +Crossover single_point_crossover() (10, 6) +Crossover probability: 0.4 +---------------------------------------------------------------------- +On Crossover on_crossover_callback() None +---------------------------------------------------------------------- +Mutation random_mutation() (10, 6) +Mutation Genes: 1 +Random Mutation Range: (-1.0, 1.0) +Mutation by Replacement: False +Allow Duplicated Genes: True +---------------------------------------------------------------------- +On Generation on_gen() None +Stop Criteria: [['reach', 10.0]] +---------------------------------------------------------------------- +====================================================================== +Population Size: (20, 6) +Number of Generations: 100 +Initial Population Range: (-4, 4) +Keep Elitism: 1 +Gene DType: [, None] +Parallel Processing: ['thread', 2] +Save Best Solutions: False +Save Solutions: False +====================================================================== +``` + +We can set the `print_step_parameters` and `print_parameters_summary` parameters to `False` to not print the parameters. + +```python +ga_instance.summary(print_step_parameters=False, + print_parameters_summary=False) +``` + +```bash +---------------------------------------------------------------------- + PyGAD Lifecycle +====================================================================== +Step Handler Output Shape +====================================================================== +Fitness Function genetic_fitness() (1) +---------------------------------------------------------------------- +Parent Selection steady_state_selection() (10, 6) +---------------------------------------------------------------------- +Crossover single_point_crossover() (10, 6) +---------------------------------------------------------------------- +On Crossover on_crossover_callback() None +---------------------------------------------------------------------- +Mutation random_mutation() (10, 6) +---------------------------------------------------------------------- +On Generation on_gen() None +---------------------------------------------------------------------- +====================================================================== +``` + +## Logging Outputs + +In [PyGAD 3.0.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-0-0), the `print()` statement is no longer used and the outputs are printed using the [logging](https://docs.python.org/3/library/logging.html) module. A new parameter called `logger` is supported to accept a user-defined logger. + +```python +import logging + +logger = ... + +ga_instance = pygad.GA(..., + logger=logger, + ...) +``` + +The default value for this parameter is `None`. If there is no logger passed (i.e. `logger=None`), then a default logger is created to log the messages to the console exactly like how the `print()` statement works. + +Some advantages of using the [logging](https://docs.python.org/3/library/logging.html) module instead of the `print()` statement are: + +1. The user has more control over the printed messages, especially in a project that uses multiple modules where each module prints its messages. A logger can organize the outputs. +2. Using the proper `Handler`, the user can log the output messages to files, not only to the console. So, it is much easier to record the outputs. +3. The format of the printed messages can be changed by customizing the `Formatter` assigned to the Logger. + +This section gives some quick examples to use the `logging` module and then gives an example to use the logger with PyGAD. + +### Logging to the Console + +This is an example to create a logger to log the messages to the console. + +```python +import logging + +# Create a logger +logger = logging.getLogger(__name__) + +# Set the logger level to debug so that all the messages are printed. +logger.setLevel(logging.DEBUG) + +# Create a stream handler to log the messages to the console. +stream_handler = logging.StreamHandler() + +# Set the handler level to debug. +stream_handler.setLevel(logging.DEBUG) + +# Create a formatter +formatter = logging.Formatter('%(message)s') + +# Add the formatter to handler. +stream_handler.setFormatter(formatter) + +# Add the stream handler to the logger +logger.addHandler(stream_handler) +``` + +Now, we can log messages to the console with the format specified in the `Formatter`. + +```python +logger.debug('Debug message.') +logger.info('Info message.') +logger.warning('Warn message.') +logger.error('Error message.') +logger.critical('Critical message.') +``` + +The outputs are identical to those returned using the `print()` statement. + +``` +Debug message. +Info message. +Warn message. +Error message. +Critical message. +``` + +By changing the format of the output messages, we can have more information about each message. + +```python +formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S') +``` + +This is a sample output. + +```python +2023-04-03 18:46:27 DEBUG: Debug message. +2023-04-03 18:46:27 INFO: Info message. +2023-04-03 18:46:27 WARNING: Warn message. +2023-04-03 18:46:27 ERROR: Error message. +2023-04-03 18:46:27 CRITICAL: Critical message. +``` + +Note that you may need to clear the handlers after finishing the execution. This is to make sure no cached handlers are used in the next run. If the cached handlers are not cleared, then the single output message may be repeated. + +```python +logger.handlers.clear() +``` + +### Logging to a File + +This is another example to log the messages to a file named `logfile.txt`. The formatter prints the following about each message: + +1. The date and time at which the message is logged. +2. The log level. +3. The message. +4. The path of the file. +5. The line number of the log message. + +```python +import logging + +level = logging.DEBUG +name = 'logfile.txt' + +logger = logging.getLogger(name) +logger.setLevel(level) + +file_handler = logging.FileHandler(name, 'a+', 'utf-8') +file_handler.setLevel(logging.DEBUG) +file_format = logging.Formatter('%(asctime)s %(levelname)s: %(message)s - %(pathname)s:%(lineno)d', datefmt='%Y-%m-%d %H:%M:%S') +file_handler.setFormatter(file_format) +logger.addHandler(file_handler) +``` + +This is what the outputs look like. + +```python +2023-04-03 18:54:03 DEBUG: Debug message. - c:\users\agad069\desktop\logger\example2.py:46 +2023-04-03 18:54:03 INFO: Info message. - c:\users\agad069\desktop\logger\example2.py:47 +2023-04-03 18:54:03 WARNING: Warn message. - c:\users\agad069\desktop\logger\example2.py:48 +2023-04-03 18:54:03 ERROR: Error message. - c:\users\agad069\desktop\logger\example2.py:49 +2023-04-03 18:54:03 CRITICAL: Critical message. - c:\users\agad069\desktop\logger\example2.py:50 +``` + +Consider clearing the handlers if necessary. + +```python +logger.handlers.clear() +``` + +### Log to Both the Console and a File + +This is an example to create a single Logger associated with 2 handlers: + +1. A file handler. +2. A stream handler. + +```python +import logging + +level = logging.DEBUG +name = 'logfile.txt' + +logger = logging.getLogger(name) +logger.setLevel(level) + +file_handler = logging.FileHandler(name,'a+','utf-8') +file_handler.setLevel(logging.DEBUG) +file_format = logging.Formatter('%(asctime)s %(levelname)s: %(message)s - %(pathname)s:%(lineno)d', datefmt='%Y-%m-%d %H:%M:%S') +file_handler.setFormatter(file_format) +logger.addHandler(file_handler) + +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.INFO) +console_format = logging.Formatter('%(message)s') +console_handler.setFormatter(console_format) +logger.addHandler(console_handler) +``` + +When a log message is executed, then it is both printed to the console and saved in the `logfile.txt`. + +Consider clearing the handlers if necessary. + +```python +logger.handlers.clear() +``` + +### PyGAD Example + +To use the logger in PyGAD, just create your custom logger and pass it to the `logger` parameter. + +```python +import logging +import pygad +import numpy + +level = logging.DEBUG +name = 'logfile.txt' + +logger = logging.getLogger(name) +logger.setLevel(level) + +file_handler = logging.FileHandler(name,'a+','utf-8') +file_handler.setLevel(logging.DEBUG) +file_format = logging.Formatter('%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S') +file_handler.setFormatter(file_format) +logger.addHandler(file_handler) + +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.INFO) +console_format = logging.Formatter('%(message)s') +console_handler.setFormatter(console_format) +logger.addHandler(console_handler) + +equation_inputs = [4, -2, 8] +desired_output = 2671.1234 + +def fitness_func(ga_instance, solution, solution_idx): + output = numpy.sum(solution * equation_inputs) + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + return fitness + +def on_generation(ga_instance): + ga_instance.logger.info(f"Generation = {ga_instance.generations_completed}") + ga_instance.logger.info(f"Fitness = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1]}") + +ga_instance = pygad.GA(num_generations=10, + sol_per_pop=40, + num_parents_mating=2, + keep_parents=2, + num_genes=len(equation_inputs), + fitness_func=fitness_func, + on_generation=on_generation, + logger=logger) +ga_instance.run() + +logger.handlers.clear() +``` + +By executing this code, the logged messages are printed to the console and also saved in the text file. + +```python +2023-04-03 19:04:27 INFO: Generation = 1 +2023-04-03 19:04:27 INFO: Fitness = 0.00038086960368076276 +2023-04-03 19:04:27 INFO: Generation = 2 +2023-04-03 19:04:27 INFO: Fitness = 0.00038214871408010853 +2023-04-03 19:04:27 INFO: Generation = 3 +2023-04-03 19:04:27 INFO: Fitness = 0.0003832795907974678 +2023-04-03 19:04:27 INFO: Generation = 4 +2023-04-03 19:04:27 INFO: Fitness = 0.00038398612055017196 +2023-04-03 19:04:27 INFO: Generation = 5 +2023-04-03 19:04:27 INFO: Fitness = 0.00038442348890867516 +2023-04-03 19:04:27 INFO: Generation = 6 +2023-04-03 19:04:27 INFO: Fitness = 0.0003854406039137763 +2023-04-03 19:04:27 INFO: Generation = 7 +2023-04-03 19:04:27 INFO: Fitness = 0.00038646083174063284 +2023-04-03 19:04:27 INFO: Generation = 8 +2023-04-03 19:04:27 INFO: Fitness = 0.0003875169193024936 +2023-04-03 19:04:27 INFO: Generation = 9 +2023-04-03 19:04:27 INFO: Fitness = 0.0003888816727311021 +2023-04-03 19:04:27 INFO: Generation = 10 +2023-04-03 19:04:27 INFO: Fitness = 0.000389832593101348 +``` diff --git a/docs/source/multi_objective.md b/docs/source/multi_objective.md new file mode 100644 index 0000000..03c855c --- /dev/null +++ b/docs/source/multi_objective.md @@ -0,0 +1,117 @@ +# Multi-Objective Optimization + +In [PyGAD 3.2.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-2-0), the library added support for multi-objective optimization using the non-dominated sorting genetic algorithm II (NSGA-II). The code is almost the same as the code for single-objective optimization, except for one difference: the return value of the fitness function. + +In single-objective optimization, the fitness function returns a single numeric value. In this example, the variable `fitness` is expected to be a numeric value. + +```python +def fitness_func(ga_instance, solution, solution_idx): + ... + return fitness +``` + +But in multi-objective optimization, the fitness function returns any of these data types: + +1. `list` +2. `tuple` +3. `numpy.ndarray` + +```python +def fitness_func(ga_instance, solution, solution_idx): + ... + return [fitness1, fitness2, ..., fitnessN] +``` + +Whenever the fitness function returns an iterable of these data types, then the problem is considered multi-objective. This holds even if there is a single element in the returned iterable. + +Other than the fitness function, everything else could be the same in both single and multi-objective problems. + +But it is recommended to use one of these 2 parent selection operators to solve multi-objective problems: + +1. `nsga2`: This selects the parents based on non-dominated sorting and crowding distance. +2. `tournament_nsga2`: This selects the parents using tournament selection which uses non-dominated sorting and crowding distance to rank the solutions. + +This is a multi-objective optimization example that optimizes these 2 linear functions: + +1. `y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + w6x6` +2. `y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + w6x12` + +Where: + +1. `(x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7)` and `y=50` +2. `(x7,x8,x9,x10,x11,x12)=(-2,0.7,-9,1.4,3,5)` and `y=30` + +The 2 functions use the same parameters (weights) `w1` to `w6`. + +The goal is to use PyGAD to find the optimal values for such weights that satisfy the 2 functions `y1` and `y2`. + +```python +import pygad +import numpy + +""" +Given these 2 functions: + y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + w6x6 + y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + w6x12 + where (x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7) and y=50 + and (x7,x8,x9,x10,x11,x12)=(-2,0.7,-9,1.4,3,5) and y=30 +What are the best values for the 6 weights (w1 to w6)? We are going to use the genetic algorithm to optimize these 2 functions. +This is a multi-objective optimization problem. + +PyGAD considers the problem as multi-objective if the fitness function returns: + 1) List. + 2) Or tuple. + 3) Or numpy.ndarray. +""" + +function_inputs1 = [4,-2,3.5,5,-11,-4.7] # Function 1 inputs. +function_inputs2 = [-2,0.7,-9,1.4,3,5] # Function 2 inputs. +desired_output1 = 50 # Function 1 output. +desired_output2 = 30 # Function 2 output. + +def fitness_func(ga_instance, solution, solution_idx): + output1 = numpy.sum(solution*function_inputs1) + output2 = numpy.sum(solution*function_inputs2) + fitness1 = 1.0 / (numpy.abs(output1 - desired_output1) + 0.000001) + fitness2 = 1.0 / (numpy.abs(output2 - desired_output2) + 0.000001) + return [fitness1, fitness2] + +num_generations = 100 +num_parents_mating = 10 + +sol_per_pop = 20 +num_genes = len(function_inputs1) + +ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=num_parents_mating, + sol_per_pop=sol_per_pop, + num_genes=num_genes, + fitness_func=fitness_func, + parent_selection_type='nsga2') + +ga_instance.run() + +ga_instance.plot_fitness(label=['Obj 1', 'Obj 2']) + +solution, solution_fitness, solution_idx = ga_instance.best_solution(ga_instance.last_generation_fitness) +print(f"Parameters of the best solution : {solution}") +print(f"Fitness value of the best solution = {solution_fitness}") + +prediction = numpy.sum(numpy.array(function_inputs1)*solution) +print(f"Predicted output 1 based on the best solution : {prediction}") +prediction = numpy.sum(numpy.array(function_inputs2)*solution) +print(f"Predicted output 2 based on the best solution : {prediction}") +``` + +This is the result of the print statements. The predicted outputs are close to the desired outputs. + +``` +Parameters of the best solution : [ 0.79676439 -2.98823386 -4.12677662 5.70539445 -2.02797016 -1.07243922] +Fitness value of the best solution = [ 1.68090829 349.8591915 ] +Predicted output 1 based on the best solution : 50.59491545442283 +Predicted output 2 based on the best solution : 29.99714270722312 +``` + +This is the figure created by the `plot_fitness()` method. The fitness of the first objective has the green color. The blue color is used for the second objective fitness. + +![multi-objective-pygad](https://github.com/ahmedfgad/GeneticAlgorithmPython/assets/16560492/7896f8d8-01c5-4ff9-8d15-52191c309b63) diff --git a/docs/source/pygad.md b/docs/source/pygad.md index 7d86326..928a384 100644 --- a/docs/source/pygad.md +++ b/docs/source/pygad.md @@ -17,7 +17,7 @@ The `pygad.GA` class constructor supports the following parameters: - `num_generations`: Number of generations. - `num_parents_mating`: Number of solutions to be selected as parents. - `fitness_func`: Accepts a function/method and returns the fitness value(s) of the solution. If a function is passed, then it must accept 3 parameters (1. the instance of the `pygad.GA` class, 2. a single solution, and 3. its index in the population). If method, then it accepts a fourth parameter representing the method's class instance. Check the [Preparing the fitness_func Parameter](https://pygad.readthedocs.io/en/latest/pygad.html#preparing-the-fitness-func-parameter) section for information about creating such a function. In [PyGAD 3.2.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-2-0), multi-objective optimization is supported. To consider the problem as multi-objective, just return a `list`, `tuple`, or `numpy.ndarray` from the fitness function. -- `fitness_batch_size=None`: A new optional parameter called `fitness_batch_size` is supported to calculate the fitness function in batches. If it is assigned the value `1` or `None` (default), then the normal flow is used where the fitness function is called for each individual solution. If the `fitness_batch_size` parameter is assigned a value satisfying this condition `1 < fitness_batch_size <= sol_per_pop`, then the solutions are grouped into batches of size `fitness_batch_size` and the fitness function is called once for each batch. Check the [Batch Fitness Calculation](https://pygad.readthedocs.io/en/latest/pygad_more.html#batch-fitness-calculation) section for more details and examples. Added in from [PyGAD 2.19.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-19-0). +- `fitness_batch_size=None`: A new optional parameter called `fitness_batch_size` is supported to calculate the fitness function in batches. If it is assigned the value `1` or `None` (default), then the normal flow is used where the fitness function is called for each individual solution. If the `fitness_batch_size` parameter is assigned a value satisfying this condition `1 < fitness_batch_size <= sol_per_pop`, then the solutions are grouped into batches of size `fitness_batch_size` and the fitness function is called once for each batch. Check the [Batch Fitness Calculation](https://pygad.readthedocs.io/en/latest/fitness_calculation.html#batch-fitness-calculation) section for more details and examples. Added in from [PyGAD 2.19.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-19-0). - `initial_population`: A user-defined initial population. It is useful when the user wants to start the generations with a custom initial population. It defaults to `None` which means no initial population is specified by the user. In this case, [PyGAD](https://pypi.org/project/pygad) creates an initial population using the `sol_per_pop` and `num_genes` parameters. An exception is raised if the `initial_population` is `None` while any of the 2 parameters (`sol_per_pop` or `num_genes`) is also `None`. Introduced in [PyGAD 2.0.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-0-0) and higher. - `sol_per_pop`: Number of solutions (i.e. chromosomes) within the population. This parameter has no action if `initial_population` parameter exists. - `num_genes`: Number of genes in the solution/chromosome. This parameter is not needed if the user feeds the initial population to the `initial_population` parameter. @@ -25,8 +25,8 @@ The `pygad.GA` class constructor supports the following parameters: - `init_range_low=-4`: The lower value of the random range from which the gene values in the initial population are selected. `init_range_low` defaults to `-4`. Available in [PyGAD 1.0.20](https://pygad.readthedocs.io/en/latest/releases.html#pygad-1-0-20) and higher. This parameter has no action if the `initial_population` parameter exists. - `init_range_high=4`: The upper value of the random range from which the gene values in the initial population are selected. `init_range_high` defaults to `+4`. Available in [PyGAD 1.0.20](https://pygad.readthedocs.io/en/latest/releases.html#pygad-1-0-20) and higher. This parameter has no action if the `initial_population` parameter exists. - `parent_selection_type="sss"`: The parent selection type. Supported types are `sss` (for steady-state selection), `rws` (for roulette wheel selection), `sus` (for stochastic universal selection), `rank` (for rank selection), `random` (for random selection), and `tournament` (for tournament selection). A custom parent selection function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/utils.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about building a user-defined parent selection function. -- `keep_parents=-1`: The number of parents to keep in the next population. `-1` (default) means keep all the parents. `0` means keep no parents. A value greater than `0` means keep that number of parents. The value of `keep_parents` cannot be less than `-1` or greater than the number of solutions in the population (`sol_per_pop`). Starting from [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0), this parameter has an effect only when the `keep_elitism` parameter is `0`. Starting from PyGAD 2.20.0, the parents' fitness from the last generation is not re-used if `keep_parents=0`. To see how `keep_parents` and `keep_elitism` work together, check the [How the Number of Offspring Is Decided](https://pygad.readthedocs.io/en/latest/pygad_more.html#how-the-number-of-offspring-is-decided) section. -- `keep_elitism=1`: Added in [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0). It takes the value `0` or a positive integer that meets the condition `0 <= keep_elitism <= sol_per_pop`. It defaults to `1`, which means only the best solution in the current generation is kept in the next generation. If set to `0`, it has no effect. If set to a positive integer `K`, then the best `K` solutions are kept in the next generation. It cannot be greater than the value of the `sol_per_pop` parameter. If this parameter is not `0`, then the `keep_parents` parameter has no effect. To see how `keep_elitism` and `keep_parents` work together, check the [How the Number of Offspring Is Decided](https://pygad.readthedocs.io/en/latest/pygad_more.html#how-the-number-of-offspring-is-decided) section. +- `keep_parents=-1`: The number of parents to keep in the next population. `-1` (default) means keep all the parents. `0` means keep no parents. A value greater than `0` means keep that number of parents. The value of `keep_parents` cannot be less than `-1` or greater than the number of solutions in the population (`sol_per_pop`). Starting from [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0), this parameter has an effect only when the `keep_elitism` parameter is `0`. Starting from PyGAD 2.20.0, the parents' fitness from the last generation is not re-used if `keep_parents=0`. To see how `keep_parents` and `keep_elitism` work together, check the [How the Number of Offspring Is Decided](https://pygad.readthedocs.io/en/latest/generations.html#how-the-number-of-offspring-is-decided) section. +- `keep_elitism=1`: Added in [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0). It takes the value `0` or a positive integer that meets the condition `0 <= keep_elitism <= sol_per_pop`. It defaults to `1`, which means only the best solution in the current generation is kept in the next generation. If set to `0`, it has no effect. If set to a positive integer `K`, then the best `K` solutions are kept in the next generation. It cannot be greater than the value of the `sol_per_pop` parameter. If this parameter is not `0`, then the `keep_parents` parameter has no effect. To see how `keep_elitism` and `keep_parents` work together, check the [How the Number of Offspring Is Decided](https://pygad.readthedocs.io/en/latest/generations.html#how-the-number-of-offspring-is-decided) section. - `K_tournament=3`: In case that the parent selection type is `tournament`, the `K_tournament` specifies the number of parents participating in the tournament selection. It defaults to `3`. - `crossover_type="single_point"`: Type of the crossover operation. Supported types are `single_point` (for single-point crossover), `two_points` (for two points crossover), `uniform` (for uniform crossover), and `scattered` (for scattered crossover). Scattered crossover is supported from PyGAD [2.9.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-9-0) and higher. It defaults to `single_point`. A custom crossover function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/utils.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about creating a user-defined crossover function. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, if `crossover_type=None`, then the crossover step is bypassed which means 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. - `crossover_probability=None`: The probability of selecting a parent for applying the crossover operation. Its value must be between 0.0 and 1.0 inclusive. For each parent, a random value between 0.0 and 1.0 is generated. If this random value is less than or equal to the value assigned to the `crossover_probability` parameter, then the parent is selected. Added in [PyGAD 2.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-5-0) and higher. @@ -38,8 +38,8 @@ The `pygad.GA` class constructor supports the following parameters: - `random_mutation_min_val=-1.0`: For `random` mutation, the `random_mutation_min_val` parameter specifies the start value of the range from which a random value is selected to be added to the gene. It defaults to `-1`. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, this parameter has no action if `mutation_type` is `None`. - `random_mutation_max_val=1.0`: For `random` mutation, the `random_mutation_max_val` parameter specifies the end value of the range from which a random value is selected to be added to the gene. It defaults to `+1`. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, this parameter has no action if `mutation_type` is `None`. - `gene_space=None`: It is used to specify the possible values for each gene in case the user wants to restrict the gene values. It is useful if the gene space is restricted to a certain range or to discrete values. It accepts a `list`, `range`, or `numpy.ndarray`. When all genes have the same global space, specify their values as a `list`/`tuple`/`range`/`numpy.ndarray`. For example, `gene_space = [0.3, 5.2, -4, 8]` restricts the gene values to the 4 specified values. If each gene has its own space, then the `gene_space` parameter can be nested like `[[0.4, -5], [0.5, -3.2, 8.2, -9], ...]` where the first sublist determines the values for the first gene, the second sublist for the second gene, and so on. If the nested list/tuple has a `None` value, then the gene's initial value is selected randomly from the range specified by the 2 parameters `init_range_low` and `init_range_high` and its mutation value is selected randomly from the range specified by the 2 parameters `random_mutation_min_val` and `random_mutation_max_val`. `gene_space` is added in [PyGAD 2.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-5-0). Check the [Release History of PyGAD 2.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-5-0) section of the documentation for more details. In [PyGAD 2.9.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-9-0), NumPy arrays can be assigned to the `gene_space` parameter. In [PyGAD 2.11.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-11-0), the `gene_space` parameter itself or any of its elements can be assigned to a dictionary to specify the lower and upper limits of the genes. For example, `{'low': 2, 'high': 4}` means the minimum and maximum values are 2 and 4, respectively. In [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0), a new key called `"step"` is supported to specify the step of moving from the start to the end of the range specified by the 2 existing keys `"low"` and `"high"`. -- `gene_constraint=None`: A list of callables (i.e. functions) acting as constraints for the gene values. Before selecting a value for a gene, the callable is called to ensure the candidate value is valid. Added in [PyGAD 3.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-5-0). Check the [Gene Constraint](https://pygad.readthedocs.io/en/latest/pygad_more.html#gene-constraint) section for more information. -- `sample_size=100`: In some cases where a gene value is to be selected, this variable defines the size of the sample from which a value is selected randomly. Useful if either `allow_duplicate_genes` or `gene_constraint` is used. If PyGAD failed to find a unique value or a value that meets a gene constraint, it is recommended to increases this parameter's value. Added in [PyGAD 3.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-5-0). Check the [sample_size Parameter](https://pygad.readthedocs.io/en/latest/pygad_more.html#sample-size-parameter) section for more information. +- `gene_constraint=None`: A list of callables (i.e. functions) acting as constraints for the gene values. Before selecting a value for a gene, the callable is called to ensure the candidate value is valid. Added in [PyGAD 3.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-5-0). Check the [Gene Constraint](https://pygad.readthedocs.io/en/latest/gene_values.html#gene-constraint) section for more information. +- `sample_size=100`: In some cases where a gene value is to be selected, this variable defines the size of the sample from which a value is selected randomly. Useful if either `allow_duplicate_genes` or `gene_constraint` is used. If PyGAD failed to find a unique value or a value that meets a gene constraint, it is recommended to increases this parameter's value. Added in [PyGAD 3.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-5-0). Check the [sample_size Parameter](https://pygad.readthedocs.io/en/latest/gene_values.html#sample-size-parameter) section for more information. - `on_start=None`: Accepts a function/method to be called only once before the genetic algorithm starts its evolution. If function, then it must accept a single parameter representing the instance of the genetic algorithm. If method, then it must accept 2 parameters where the second one refers to the method's object. Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). - `on_fitness=None`: Accepts a function/method to be called after calculating the fitness values of all solutions in the population. If function, then it must accept 2 parameters: 1) a list of all solutions' fitness values 2) the instance of the genetic algorithm. If method, then it must accept 3 parameters where the third one refers to the method's object. Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). - `on_parents=None`: Accepts a function/method to be called after selecting the parents that mates. If function, then it must accept 2 parameters: 1) the selected parents 2) the instance of the genetic algorithm If method, then it must accept 3 parameters where the third one refers to the method's object. Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). @@ -52,9 +52,9 @@ The `pygad.GA` class constructor supports the following parameters: - `suppress_warnings=False`: A bool parameter to control whether the warning messages are printed or not. It defaults to `False`. - `allow_duplicate_genes=True`: Added in [PyGAD 2.13.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-13-0). If `True`, then a solution/chromosome may have duplicate gene values. If `False`, then each gene will have a unique value in its solution. - `stop_criteria=None`: Some criteria to stop the evolution. Added in [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0). Each criterion is passed as `str` which has a stop word. The current 2 supported words are `reach` and `saturate`. `reach` stops the `run()` method if the fitness value is equal to or greater than a given fitness value. An example for `reach` is `"reach_40"` which stops the evolution if the fitness is >= 40. `saturate` means stop the evolution if the fitness saturates for a given number of consecutive generations. An example for `saturate` is `"saturate_7"` which means stop the `run()` method if the fitness does not change for 7 consecutive generations. -- `parallel_processing=None`: Added in [PyGAD 2.17.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-17-0). If `None` (Default), this means no parallel processing is applied. It can accept a list/tuple of 2 elements [1) Can be either `'process'` or `'thread'` to indicate whether processes or threads are used, respectively., 2) The number of processes or threads to use.]. For example, `parallel_processing=['process', 10]` applies parallel processing with 10 processes. If a positive integer is assigned, then it is used as the number of threads. For example, `parallel_processing=5` uses 5 threads which is equivalent to `parallel_processing=["thread", 5]`. For more information, check the [Parallel Processing in PyGAD](https://pygad.readthedocs.io/en/latest/pygad_more.html#parallel-processing-in-pygad) section. +- `parallel_processing=None`: Added in [PyGAD 2.17.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-17-0). If `None` (Default), this means no parallel processing is applied. It can accept a list/tuple of 2 elements [1) Can be either `'process'` or `'thread'` to indicate whether processes or threads are used, respectively., 2) The number of processes or threads to use.]. For example, `parallel_processing=['process', 10]` applies parallel processing with 10 processes. If a positive integer is assigned, then it is used as the number of threads. For example, `parallel_processing=5` uses 5 threads which is equivalent to `parallel_processing=["thread", 5]`. For more information, check the [Parallel Processing in PyGAD](https://pygad.readthedocs.io/en/latest/fitness_calculation.html#parallel-processing-in-pygad) section. - `random_seed=None`: Added in [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0). It defines the random seed to be used by the random function generators (we use random functions in the NumPy and random modules). This helps to reproduce the same results by setting the same random seed (e.g. `random_seed=2`). If given the value `None`, then it has no effect. -- `logger=None`: Accepts an instance of the `logging.Logger` class to log the outputs. Any message is no longer printed using `print()` but logged. If `logger=None`, then a logger is created that uses `StreamHandler` to logs the messages to the console. Added in [PyGAD 3.0.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-0-0). Check the [Logging Outputs](https://pygad.readthedocs.io/en/latest/pygad_more.html#logging-outputs) for more information. +- `logger=None`: Accepts an instance of the `logging.Logger` class to log the outputs. Any message is no longer printed using `print()` but logged. If `logger=None`, then a logger is created that uses `StreamHandler` to logs the messages to the console. Added in [PyGAD 3.0.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-0-0). Check the [Logging Outputs](https://pygad.readthedocs.io/en/latest/logging.html#logging-outputs) for more information. You do not have to set all of these parameters when you create an instance of the `GA` class. The most important one is `fitness_func`, which defines the fitness function. @@ -137,7 +137,7 @@ Note that the attributes with names starting with `last_generation_` are updated - `mutation()`: Refers to the method that applies the mutation operator based on the selected type of mutation in the `mutation_type` property. - `select_parents()`: Refers to a method that selects the parents based on the parent selection type specified in the `parent_selection_type` attribute. - `adaptive_mutation_population_fitness()`: Returns the average fitness value used in the adaptive mutation to filter the solutions. -- `summary()`: Prints a Keras-like summary of the PyGAD lifecycle. This helps to have an overview of the architecture. Supported in [PyGAD 2.19.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-19-0). Check the [Print Lifecycle Summary](https://pygad.readthedocs.io/en/latest/pygad_more.html#print-lifecycle-summary) section for more details and examples. +- `summary()`: Prints a Keras-like summary of the PyGAD lifecycle. This helps to have an overview of the architecture. Supported in [PyGAD 2.19.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-19-0). Check the [Print Lifecycle Summary](https://pygad.readthedocs.io/en/latest/logging.html#print-lifecycle-summary) section for more details and examples. - 5 methods with names starting with `run_`. Their purpose is to keep the main loop inside the `run()` method clean. The details inside the loop are moved to 4 individual methods. Generally, any method with a name starting with `run_` is meant to be called by PyGAD from inside the `run()` method. Supported in [PyGAD 3.3.1](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-3-1). 1. `run_loop_head()`: The code before the loop starts. 2. `run_select_parents(call_on_parents=True)`: Select the parents and call the callable `on_parents()` if defined. If `call_on_parents` is `True`, then the callable `on_parents()` is called. It must be `False` when the `run_select_parents()` method is called to update the parents at the end of the `run()` method. diff --git a/docs/source/pygad_more.md b/docs/source/pygad_more.md index 890cb92..89611e0 100644 --- a/docs/source/pygad_more.md +++ b/docs/source/pygad_more.md @@ -1,1964 +1,61 @@ # More About PyGAD -This section covers the more advanced features of the `pygad` module. +This section covers the more advanced features of the `pygad` module. Pick a topic: -## Multi-Objective Optimization +::::{grid} 1 2 2 3 +:gutter: 3 -In [PyGAD 3.2.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-2-0), the library added support for multi-objective optimization using the non-dominated sorting genetic algorithm II (NSGA-II). The code is almost the same as the code for single-objective optimization, except for one difference: the return value of the fitness function. +:::{grid-item-card} Multi-Objective Optimization +:link: multi_objective +:link-type: doc -In single-objective optimization, the fitness function returns a single numeric value. In this example, the variable `fitness` is expected to be a numeric value. - -```python -def fitness_func(ga_instance, solution, solution_idx): - ... - return fitness -``` - -But in multi-objective optimization, the fitness function returns any of these data types: - -1. `list` -2. `tuple` -3. `numpy.ndarray` - -```python -def fitness_func(ga_instance, solution, solution_idx): - ... - return [fitness1, fitness2, ..., fitnessN] -``` - -Whenever the fitness function returns an iterable of these data types, then the problem is considered multi-objective. This holds even if there is a single element in the returned iterable. - -Other than the fitness function, everything else could be the same in both single and multi-objective problems. - -But it is recommended to use one of these 2 parent selection operators to solve multi-objective problems: - -1. `nsga2`: This selects the parents based on non-dominated sorting and crowding distance. -2. `tournament_nsga2`: This selects the parents using tournament selection which uses non-dominated sorting and crowding distance to rank the solutions. - -This is a multi-objective optimization example that optimizes these 2 linear functions: - -1. `y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + w6x6` -2. `y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + w6x12` - -Where: - -1. `(x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7)` and `y=50` -2. `(x7,x8,x9,x10,x11,x12)=(-2,0.7,-9,1.4,3,5)` and `y=30` - -The 2 functions use the same parameters (weights) `w1` to `w6`. - -The goal is to use PyGAD to find the optimal values for such weights that satisfy the 2 functions `y1` and `y2`. - -```python -import pygad -import numpy - -""" -Given these 2 functions: - y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + w6x6 - y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + w6x12 - where (x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7) and y=50 - and (x7,x8,x9,x10,x11,x12)=(-2,0.7,-9,1.4,3,5) and y=30 -What are the best values for the 6 weights (w1 to w6)? We are going to use the genetic algorithm to optimize these 2 functions. -This is a multi-objective optimization problem. - -PyGAD considers the problem as multi-objective if the fitness function returns: - 1) List. - 2) Or tuple. - 3) Or numpy.ndarray. -""" - -function_inputs1 = [4,-2,3.5,5,-11,-4.7] # Function 1 inputs. -function_inputs2 = [-2,0.7,-9,1.4,3,5] # Function 2 inputs. -desired_output1 = 50 # Function 1 output. -desired_output2 = 30 # Function 2 output. - -def fitness_func(ga_instance, solution, solution_idx): - output1 = numpy.sum(solution*function_inputs1) - output2 = numpy.sum(solution*function_inputs2) - fitness1 = 1.0 / (numpy.abs(output1 - desired_output1) + 0.000001) - fitness2 = 1.0 / (numpy.abs(output2 - desired_output2) + 0.000001) - return [fitness1, fitness2] - -num_generations = 100 -num_parents_mating = 10 - -sol_per_pop = 20 -num_genes = len(function_inputs1) - -ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - sol_per_pop=sol_per_pop, - num_genes=num_genes, - fitness_func=fitness_func, - parent_selection_type='nsga2') - -ga_instance.run() - -ga_instance.plot_fitness(label=['Obj 1', 'Obj 2']) - -solution, solution_fitness, solution_idx = ga_instance.best_solution(ga_instance.last_generation_fitness) -print(f"Parameters of the best solution : {solution}") -print(f"Fitness value of the best solution = {solution_fitness}") - -prediction = numpy.sum(numpy.array(function_inputs1)*solution) -print(f"Predicted output 1 based on the best solution : {prediction}") -prediction = numpy.sum(numpy.array(function_inputs2)*solution) -print(f"Predicted output 2 based on the best solution : {prediction}") -``` - -This is the result of the print statements. The predicted outputs are close to the desired outputs. - -``` -Parameters of the best solution : [ 0.79676439 -2.98823386 -4.12677662 5.70539445 -2.02797016 -1.07243922] -Fitness value of the best solution = [ 1.68090829 349.8591915 ] -Predicted output 1 based on the best solution : 50.59491545442283 -Predicted output 2 based on the best solution : 29.99714270722312 -``` - -This is the figure created by the `plot_fitness()` method. The fitness of the first objective has the green color. The blue color is used for the second objective fitness. - -![multi-objective-pygad](https://github.com/ahmedfgad/GeneticAlgorithmPython/assets/16560492/7896f8d8-01c5-4ff9-8d15-52191c309b63) - -## Limit the Gene Value Range using the `gene_space` Parameter - -In [PyGAD 2.11.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-11-0), the `gene_space` parameter added a new feature that lets you customize the range of accepted values for each gene. Let us first review the `gene_space` parameter and build on it. - -The `gene_space` parameter lets you set the space of values for each gene. This way, the accepted values for each gene are restricted to the user-defined values. Assume there is a problem with 3 genes, where each gene has a different set of values: - -1. Gene 1: `[0.4, 12, -5, 21.2]` -2. Gene 2: `[-2, 0.3]` -3. Gene 3: `[1.2, 63.2, 7.4]` - -Then, the `gene_space` for this problem is as given below. Note that the order is very important. - -```python -gene_space = [[0.4, 12, -5, 21.2], - [-2, 0.3], - [1.2, 63.2, 7.4]] -``` - -If all genes share the same set of values, then pass a single list to the `gene_space` parameter as follows. In this case, all genes can only take values from this list of 6 values. - -```python -gene_space = [33, 7, 0.5, 95, 6.3, 0.74] -``` - -The previous example restricts the gene values to a fixed set of discrete values. If you want to use a range of discrete values for the gene, then you can use the `range()` function. For example, `range(1, 7)` means the allowed values for the gene are `1, 2, 3, 4, 5, and 6`. You can also use the `numpy.arange()` or `numpy.linspace()` functions for the same purpose. - -The previous examples only work with discrete values, not continuous ones. In [PyGAD 2.11.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-11-0), the `gene_space` parameter can be assigned a dictionary that allows the gene to take values from a continuous range. - -Assuming you want to restrict the gene within this half-open range [1 to 5) where 1 is included and 5 is not. Then simply create a dictionary with 2 items where the keys of the 2 items are: - -1. `'low'`: The minimum value in the range which is 1 in the example. -2. `'high'`: The maximum value in the range which is 5 in the example. - -The dictionary will look like that: - -```python -{'low': 1, - 'high': 5} -``` - -It is not acceptable to add more than 2 items in the dictionary or use other keys than `'low'` and `'high'`. - -For a 3-gene problem, the next code creates a dictionary for each gene to restrict its values in a continuous range. For the first gene, it can take any floating-point value from the range that starts from 1 (inclusive) and ends at 5 (exclusive). - -```python -gene_space = [{'low': 1, 'high': 5}, {'low': 0.3, 'high': 1.4}, {'low': -0.2, 'high': 4.5}] -``` - -## More about the `gene_space` Parameter - -The `gene_space` parameter customizes the space of values of each gene. - -Assuming that all genes have the same global space which include the values 0.3, 5.2, -4, and 8, then those values can be assigned to the `gene_space` parameter as a list, tuple, or range. Here is a list assigned to this parameter. By doing that, then the gene values are restricted to those assigned to the `gene_space` parameter. - -```python -gene_space = [0.3, 5.2, -4, 8] -``` - -If some genes have different spaces, then `gene_space` should accept a nested list or tuple. In this case, the elements could be: - -1. Number (of `int`, `float`, or `NumPy` data types): A single value to be assigned to the gene. This means this gene will have the same value across all generations. -2. `list`, `tuple`, `numpy.ndarray`, or any range like `range`, `numpy.arange()`, or `numpy.linspace`: It holds the space for each individual gene. But this space is usually discrete. That is there is a set of finite values to select from. -3. `dict`: To sample a value for a gene from a continuous range. The dictionary must have 2 mandatory keys which are `"low"` and `"high"` in addition to an optional key which is `"step"`. A random value is returned between the values assigned to the items with `"low"` and `"high"` keys. If the `"step"` exists, then this works as the previous options (i.e. discrete set of values). -4. `None`: A gene with its space set to `None` is initialized randomly from the range specified by the 2 parameters `init_range_low` and `init_range_high`. For mutation, its value is mutated based on a random value from the range specified by the 2 parameters `random_mutation_min_val` and `random_mutation_max_val`. If all elements in the `gene_space` parameter are `None`, the parameter will not have any effect. - -Assuming that a chromosome has 2 genes and each gene has a different value space. Then the `gene_space` could be assigned a nested list/tuple where each element determines the space of a gene. - -According to the next code, the space of the first gene is `[0.4, -5]` which has 2 values and the space for the second gene is `[0.5, -3.2, 8.8, -9]` which has 4 values. - -```python -gene_space = [[0.4, -5], [0.5, -3.2, 8.2, -9]] -``` - -For a 2 gene chromosome, if the first gene space is restricted to the discrete values from 0 to 4 and the second gene is restricted to the values from 10 to 19, then it could be specified according to the next code. - -```python -gene_space = [range(5), range(10, 20)] -``` - -The `gene_space` can also be assigned to a single range, as given below, where the values of all genes are sampled from the same range. - -```python -gene_space = numpy.arange(15) -``` - - The `gene_space` can be assigned a dictionary to sample a value from a continuous range. - -```python -gene_space = {"low": 4, "high": 30} -``` - - A step also can be assigned to the dictionary. This works as if a range is used. - -```python -gene_space = {"low": 4, "high": 30, "step": 2.5} -``` - -> Setting a `dict` like `{"low": 0, "high": 10}` in the `gene_space` means that random values from the continuous range [0, 10) are sampled. Note that `0` is included but `10` is not included while sampling. Thus, the maximum value that could be returned is less than `10` like `9.9999`. But if the user decided to round the genes using, for example, `[float, 2]`, then this value will become 10. So, the user should be careful to the inputs. - -If a `None` is assigned to only a single gene, then its value will be randomly generated initially using the `init_range_low` and `init_range_high` parameters in the `pygad.GA` class's constructor. During mutation, the value is sampled from the range defined by the 2 parameters `random_mutation_min_val` and `random_mutation_max_val`. This is an example where the second gene is given a `None` value. - -```python -gene_space = [range(5), None, numpy.linspace(10, 20, 300)] -``` - -If the user did not assign the initial population to the `initial_population` parameter, the initial population is created randomly based on the `gene_space` parameter. Moreover, the mutation is applied based on this parameter. - -### How Mutation Works with the `gene_space` Parameter? - -Mutation changes based on whether the `gene_space` has a continuous range or discrete set of values. - -If a gene has its **static/discrete space** defined in the `gene_space` parameter, then mutation works by replacing the gene value by a value randomly selected from the gene space. This happens for both `int` and `float` data types. - -For example, the following `gene_space` has the static space `[1, 2, 3]` defined for the first gene. So, this gene can only have a value out of these 3 values. - -```python -Gene space: [[1, 2, 3], - None] -Solution: [1, 5] -``` - -For a solution like `[1, 5]`, then mutation happens for the first gene by simply replacing its current value by a randomly selected value (other than its current value if possible). So, the value 1 will be replaced by either 2 or 3. - -For the second gene, its space is set to `None`. So, traditional mutation happens for this gene by: - -1. Generating a random value from the range defined by the `random_mutation_min_val` and `random_mutation_max_val` parameters. -2. Adding this random value to the current gene's value. - -If its current value is 5 and the random value is `-0.5`, then the new value is 4.5. If the gene type is integer, then the value will be rounded. - -On the other hand, if a gene has a **continuous space** defined in the `gene_space` parameter, then mutation occurs by adding a random value to the current gene value. - -For example, the following `gene_space` has the continuous space defined by the dictionary `{'low': 1, 'high': 5}`. This applies to all genes. So, mutation is applied to one or more selected genes by adding a random value to the current gene value. - -```python -Gene space: {'low': 1, 'high': 5} -Solution: [1.5, 3.4] -``` - -Assuming `random_mutation_min_val=-1` and `random_mutation_max_val=1`, then a random value such as `0.3` can be added to the gene(s) participating in mutation. If only the first gene is mutated, then its new value changes from `1.5` to `1.5+0.3=1.8`. Note that PyGAD verifies that the new value is within the range. In the worst scenarios, the value will be set to either boundary of the continuous range. For example, if the gene value is 1.5 and the random value is -0.55, then the new value is 0.95, which is smaller than the lower boundary 1. So, the gene value will be set to 1. - -If the dictionary has a step like the example below, then it is considered a discrete range and mutation occurs by randomly selecting a value from the set of values. In other words, no random value is added to the gene value. - -```python -Gene space: {'low': 1, 'high': 5, 'step': 0.5} -``` - -## Gene Constraint - -In [PyGAD 3.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-5-0), a new parameter called `gene_constraint` is added to the constructor of the `pygad.GA` class. An instance attribute of the same name is created for any instance of the `pygad.GA` class. - -The `gene_constraint` parameter allows the users to define constraints to be enforced (as much as possible) when selecting a value for a gene. For example, this constraint is enforced when applying mutation to make sure the new gene value after mutation meets the gene constraint. - -The default value of this parameter is `None` which means no genes have constraints. It can be assigned a list but the length of this list must be equal to the number of genes as specified by the `num_genes` parameter. - -When assigned a list, the allowed values for each element are: - -1. `None`: No constraint for the gene. -2. `callable`: A callable/function that accepts 2 parameters: - 1. The solution where the gene exists. - 2. A list or NumPy array of candidate values for the gene. - -It is the user's responsibility to build such callables to filter the passed list of values and return a new list with the values that meet the gene constraint. If no value meets the constraint, return an empty list or NumPy array. - -For example, if the gene must be smaller than 5, then use this callable: - -```python -lambda solution,values: [val for val in values if val<5] -``` - -The first parameter is the solution where the target gene exists. It is passed just in case you would like to compare the gene value with other genes. The second parameter is the list of candidate values for the gene. The objective of the lambda function is to filter the values and return only the valid values that are less than 5. - -A lambda function is used in this case but we can use a regular function: - -```python -def constraint_func(solution,values): - return [val for val in values if val<5] -``` - -Assuming `num_genes` is 2, then here is a valid value for the `gene_constraint` parameter. - -```python -import pygad - -def fitness_func(...): - ... - return fitness - -ga_instance = pygad.GA( - num_genes=2, - sample_size=200, - ... - gene_constraint= - [ - lambda solution,values: [val for val in values if val<5], - lambda solution,values: [val for val in values if val>[solution[0]] - ] -) -``` - -The first lambda function filters the values for the first gene by only considering the gene values that are less than 5. If the passed values is `[-5, 2, 6, 13, 3, 4, 0]`, then the returned filtered values will be `[-5, 2, 3, 4, 0]`. - -The constraint for the second gene makes sure the selected value is larger than the value of the first gene. Assuming the values for the 2 parameters are: - -1. `solution=[1, 4]` -2. `values=[17, 2, -1, 0.5, -2.1, 1.4]` - -Then the value of the first gene in the passed solution is `1`. By filtering the passed values using the callable corresponding to the second gene, then the returned values will be `[17, 2, 1.4]` because these are the only values that are larger than the first gene value of `1`. - -Sometimes it is normal for PyGAD to fail to find a gene value that satisfies the constraint. For example, if the possible gene values are only `[20,30,40]` and the gene constraint restricts the values to be greater than 50, then it is impossible to meet the constraint. - -For some other cases, the constraint can be met but with some changes. For example, increasing the range from which a value is sampled. If the `gene_space` is used and assigned `range(10)`, then the gene constraint can be met by using `range(50)` so that we can find values greater than 50. - -Even if the gene space is already assigned `range(1000)`, it might still not find values that meet the constraints. This is because PyGAD samples a number of values equal to the `sample_size` parameter which defaults to *100*. - -Out of the range of *1000* numbers, all the 100 values might not be satisfying the constraint. This issue could be solved by simply assigning a larger value for the `sample_size` parameter. - -> PyGAD does not yet handle the **dependencies** among the genes in the `gene_constraint` parameter. -> -> This is an example where gene 0 depends on gene 1. To efficiently enforce the constraints, the constraint for gene 1 must be enforced first (if not `None`) then the constraint for gene 0. -> -> ```python -> gene_constraint= -> [ -> lambda solution,values: [val for val in values if val lambda solution,values: [val for val in values if val>10] -> ] -> ``` -> -> PyGAD applies constraints sequentially, starting from the first gene to the last. To ensure correct behavior when genes depend on each other, structure your GA problem so that if gene X depends on gene Y, then gene Y appears earlier in the chromosome (solution) than gene X. As a result, its gene constraint will be earlier in the list. - -### Full Example - -For a full example, please check the [`examples/example_gene_constraint.py` script](https://github.com/ahmedfgad/GeneticAlgorithmPython/blob/master/examples/example_gene_constraint.py). - -## `sample_size` Parameter - -In [PyGAD 3.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-5-0), a new parameter called `sample_size`. It is used in some situations where PyGAD seeks a single value for a gene out of a range. Two of the important use cases are: - -1. Find a unique value for the gene. This is when the `allow_duplicate_genes` parameter is set to `False` to reject the duplicate gene values within the same solution. -2. Find a value that satisfies the `gene_constraint` parameter. - -Given that we are sampling values from a continuous range as defined by the 2 attributes: - -1. `random_mutation_min_val=0` -2. `random_mutation_max_val=100` - -PyGAD samples a fixed number of values out of this continuous range. The number of values in the sample is defined by the `sample_size` parameter which defaults to `100`. - -If the objective is to find a unique value or enforce the gene constraint, then the 100 values are filtered to keep only the values that keep the gene unique or meet the constraint. - -Sometimes 100 values is not enough and PyGAD sometimes fails to find a good value. In this case, it is highly recommended to increase the `sample_size` parameter. This is to create a larger sample to increase the chance of finding a value that meets our objectives. - -## Stop at Any Generation - -In [PyGAD 2.4.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-4-0), it is possible to stop the genetic algorithm after any generation. All you need to do is return the string `"stop"` in the `on_generation` callback function. When this callback function is implemented and assigned to the `on_generation` parameter in the constructor of the `pygad.GA` class, the algorithm stops right after it completes its current generation. Here is an example. - -Assume the user wants to stop the algorithm either after 100 generations or when a condition is met. The user can assign a value of 100 to the `num_generations` parameter of the `pygad.GA` class constructor. - -The condition that stops the algorithm is written in a callback function like the one in the next code. If the fitness value of the best solution exceeds 70, then the string `"stop"` is returned. - -```python -def func_generation(ga_instance): - if ga_instance.best_solution()[1] >= 70: - return "stop" -``` - -## Stop Criteria - -In [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0), a new parameter named `stop_criteria` is added to the constructor of the `pygad.GA` class. It helps to stop the evolution based on some criteria. It can be assigned one or more criteria. - -Each criterion is passed as `str` that consists of 2 parts: - -1. Stop word. -2. Number. - -It takes this form: - -```python -"word_num" -``` - -The current 2 supported words are `reach` and `saturate`. - -The `reach` word stops the `run()` method if the fitness value is equal to or greater than a given fitness value. An example for `reach` is `"reach_40"` which stops the evolution if the fitness is >= 40. - -`saturate` stops the evolution if the fitness saturates for a given number of consecutive generations. An example for `saturate` is `"saturate_7"` which means stop the `run()` method if the fitness does not change for 7 consecutive generations. - -Here is an example that stops the evolution if either the fitness value reached `127.4` or if the fitness saturates for `15` generations. - -```python -import pygad -import numpy - -equation_inputs = [4, -2, 3.5, 8, 9, 4] -desired_output = 44 - -def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - - return fitness - -ga_instance = pygad.GA(num_generations=200, - sol_per_pop=10, - num_parents_mating=4, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - stop_criteria=["reach_127.4", "saturate_15"]) - -ga_instance.run() -print(f"Number of generations passed is {ga_instance.generations_completed}") -``` - -### Multi-Objective Stop Criteria - -When multi-objective is used, then there are 2 options to use the `stop_criteria` parameter with the `reach` keyword: - -1. Pass a single value to use along the `reach` keyword to use across all the objectives. -2. Pass multiple values along the `reach` keyword. But the number of values must equal the number of objectives. - -For the `saturate` keyword, it is independent of the number of objectives. - -Suppose there are 3 objectives. Here is a working example. It stops when the fitness values of the 3 objectives reach or exceed 10, 20, and 30, respectively. - -```python -stop_criteria='reach_10_20_30' -``` - -More than one criterion can be used together. In this case, pass the `stop_criteria` parameter as an iterable. This is an example. It stops when either of these 2 conditions hold: - -1. The fitness values of the 3 objectives reach or exceed 10, 20, and 30, respectively. -2. The fitness values of the 3 objectives reach or exceed 90, -5.7, and 10, respectively. - -```python -stop_criteria=['reach_10_20_30', 'reach_90_-5.7_10'] -``` - -## Elitism Selection - -Starting from [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0), there is a parameter called `keep_elitism`. It takes an integer that sets how many of the best solutions (the elitism) are kept in the next generation. It defaults to `1`, so only the best solution is kept by default. - -The best solutions are copied to the next generation without any change. Crossover and mutation do not touch them. This makes sure the best solutions found so far are never lost. - -In the next example, the `keep_elitism` parameter in the constructor of the `pygad.GA` class is set to `2`. So, the best 2 solutions in each generation are kept in the next generation. - -```python -import numpy -import pygad - -function_inputs = [4,-2,3.5,5,-11,-4.7] -desired_output = 44 - -def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution*function_inputs) - fitness = 1.0 / numpy.abs(output - desired_output) - return fitness - -ga_instance = pygad.GA(num_generations=2, - num_parents_mating=3, - fitness_func=fitness_func, - num_genes=6, - sol_per_pop=5, - keep_elitism=2) - -ga_instance.run() -``` - -The value passed to the `keep_elitism` parameter must meet 2 conditions: - -1. It must be `>= 0`. -2. It must be `<= sol_per_pop`. Its value cannot be more than the number of solutions in the population. - -In the previous example, if `keep_elitism` is set equal to `sol_per_pop` (which is `5`), then there is no evolution at all, as shown in the next figure. This is because all the 5 solutions are kept as elitism in the next generation, so no offspring are created. - -```python -... - -ga_instance = pygad.GA(..., - sol_per_pop=5, - keep_elitism=5) - -ga_instance.run() -``` - - - -![elitism_kills_evolution](https://user-images.githubusercontent.com/16560492/189273225-67ffad41-97ab-45e1-9324-429705e17b20.png) - -### How the Number of Offspring Is Decided - -PyGAD has two parameters that decide how many solutions are carried over to the next generation: - -- `keep_elitism`: keeps the best solutions (the elitism). -- `keep_parents`: keeps the selected parents. - -Only one of them is used at a time, and `keep_elitism` has priority. If `keep_elitism` is not zero, then `keep_parents` is ignored. Because `keep_elitism` defaults to `1`, the `keep_parents` parameter has no effect by default. To use `keep_parents`, set `keep_elitism=0`. - -The number of kept solutions decides how many offspring are created. The rest of the population is filled with new offspring: - -``` -number of offspring = sol_per_pop - (number of kept solutions) -``` - -The next tree shows how the two parameters decide the number of offspring. - -:::{figure} images/offspring_decision_tree.* -:alt: Decision tree showing how keep_elitism and keep_parents decide the number of offspring -:width: 680px -:align: center - -How `keep_elitism` and `keep_parents` decide the number of offspring. +Optimize several objectives at once using NSGA-II. ::: -There are four cases: +:::{grid-item-card} Controlling Gene Values +:link: gene_values +:link-type: doc -| `keep_elitism` | `keep_parents` | What is kept | Number of offspring | -| --- | --- | --- | --- | -| `> 0` | ignored | the best `keep_elitism` solutions | `sol_per_pop - keep_elitism` | -| `0` | `-1` | all the parents | `sol_per_pop - num_parents_mating` | -| `0` | `0` | nothing | `sol_per_pop` | -| `0` | `> 0` | the best `keep_parents` parents | `sol_per_pop - keep_parents` | - -The kept solutions are placed at the top of the next population, starting at index 0. The offspring fill the slots that remain. - -:::{figure} images/population_assembly.* -:alt: The kept solutions sit at the top of the next population and the offspring fill the rest -:width: 620px -:align: center - -The kept solutions are copied to the top of the population. The offspring fill the rest. +Restrict gene values with `gene_space`, `gene_type`, constraints, `sample_size`, and duplicate prevention. ::: -## Random Seed - -In [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0), a new parameter called `random_seed` is supported. Its value is used as a seed for the random function generators. - - PyGAD uses random functions in these 2 libraries: - -1. NumPy -2. random - -The `random_seed` parameter defaults to `None` which means no seed is used. As a result, different random numbers are generated for each run of PyGAD. - -If this parameter is assigned a proper seed, then the results will be reproducible. In the next example, the integer 2 is used as a random seed. - -```python -import numpy -import pygad - -function_inputs = [4,-2,3.5,5,-11,-4.7] -desired_output = 44 - -def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution*function_inputs) - fitness = 1.0 / numpy.abs(output - desired_output) - return fitness - -ga_instance = pygad.GA(num_generations=2, - num_parents_mating=3, - fitness_func=fitness_func, - sol_per_pop=5, - num_genes=6, - random_seed=2) - -ga_instance.run() -best_solution, best_solution_fitness, best_match_idx = ga_instance.best_solution() -print(best_solution) -print(best_solution_fitness) -``` - -This is the best solution found and its fitness value. - -``` -[ 2.77249188 -4.06570662 0.04196872 -3.47770796 -0.57502138 -3.22775267] -0.04872203136549972 -``` - -After running the code again, it will find the same result. +:::{grid-item-card} Controlling Generations +:link: generations +:link-type: doc -``` -[ 2.77249188 -4.06570662 0.04196872 -3.47770796 -0.57502138 -3.22775267] -0.04872203136549972 -``` - -## Continue without Losing Progress - -In [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0), and thanks for [Felix Bernhard](https://github.com/FeBe95) for opening [this GitHub issue](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/123#issuecomment-1203035106), the values of these 4 instance attributes are no longer reset after each call to the `run()` method. - -1. `self.best_solutions` -2. `self.best_solutions_fitness` -3. `self.solutions` -4. `self.solutions_fitness` - -This helps the user to continue where the last run stopped without losing the values of these 4 attributes. - -Now, the user can save the model by calling the `save()` method. - -```python -import pygad - -def fitness_func(ga_instance, solution, solution_idx): - ... - return fitness - -ga_instance = pygad.GA(...) - -ga_instance.run() - -ga_instance.plot_fitness() - -ga_instance.save("pygad_GA") -``` - -Then the saved model is loaded by calling the `load()` function. After calling the `run()` method over the loaded instance, then the data from the previous 4 attributes are not reset but extended with the new data. - -```python -import pygad - -def fitness_func(ga_instance, solution, solution_idx): - ... - return fitness - -loaded_ga_instance = pygad.load("pygad_GA") - -loaded_ga_instance.run() - -loaded_ga_instance.plot_fitness() -``` - -The plot created by the `plot_fitness()` method will show the data collected from both the runs. - -Note that the 2 attributes (`self.best_solutions` and `self.best_solutions_fitness`) only work if the `save_best_solutions` parameter is set to `True`. Also, the 2 attributes (`self.solutions` and `self.solutions_fitness`) only work if the `save_solutions` parameter is `True`. - -## Change Population Size during Runtime - -Starting from [PyGAD 3.3.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-3-0), the population size can be changed during runtime. In other words, the number of solutions/chromosomes and the number of genes can be changed. - -The user has to carefully arrange the list of *parameters* and *instance attributes* that have to be changed to keep the GA consistent before and after changing the population size. Generally, change everything that would be used during the GA evolution. - -> CAUTION: If the user fails to change a parameter or an instance attribute that is needed to keep the GA running after the population size changes, errors will arise. - -These are examples of the parameters that the user should decide whether to change. The user should check the [list of parameters](https://pygad.readthedocs.io/en/latest/pygad.html#init) and decide what to change. - -1. `population`: The population. It *must* be changed. -2. `num_offspring`: The number of offspring to produce from the crossover and mutation operations. Change this parameter if the number of offspring has to change to match the new population size. -3. `num_parents_mating`: The number of solutions to select as parents. Change this parameter if the number of parents has to change to match the new population size. -4. `fitness_func`: If the way of calculating the fitness changes with the new population size, then the fitness function has to be changed. -5. `sol_per_pop`: The number of solutions per population. It is not critical to change it but it is recommended to keep this number consistent with the number of solutions in the `population` parameter. - -These are examples of the instance attributes that might be changed. The user should check the [list of instance attributes](https://pygad.readthedocs.io/en/latest/pygad.html#other-instance-attributes-methods) and decide what to change. - -1. All the `last_generation_*` attributes - 1. `last_generation_fitness`: A 1D NumPy array of fitness values of the population. - 2. `last_generation_parents` and `last_generation_parents_indices`: Two NumPy arrays: 2D array representing the parents and 1D array of the parents indices. - 3. `last_generation_elitism` and `last_generation_elitism_indices`: Must be changed if `keep_elitism != 0`. The default value of `keep_elitism` is 1. Two NumPy arrays: 2D array representing the elitism and 1D array of the elitism indices. -2. `pop_size`: The population size. - -## Prevent Duplicates in Gene Values - -In [PyGAD 2.13.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-13-0), a new bool parameter called `allow_duplicate_genes` is supported to control whether duplicates are supported in the chromosome or not. In other words, whether 2 or more genes might have the same exact value. - -If `allow_duplicate_genes=True` (which is the default case), genes may have the same value. If `allow_duplicate_genes=False`, then no 2 genes will have the same value given that there are enough unique values for the genes. - -The next code gives an example to use the `allow_duplicate_genes` parameter. A callback generation function is implemented to print the population after each generation. - -```python -import pygad - -def fitness_func(ga_instance, solution, solution_idx): - return 0 - -def on_generation(ga): - print("Generation", ga.generations_completed) - print(ga.population) - -ga_instance = pygad.GA(num_generations=5, - sol_per_pop=5, - num_genes=4, - mutation_num_genes=3, - random_mutation_min_val=-5, - random_mutation_max_val=5, - num_parents_mating=2, - fitness_func=fitness_func, - gene_type=int, - on_generation=on_generation, - sample_size=200, - allow_duplicate_genes=False) -ga_instance.run() -``` - -Here are the population after the 5 generations. Note how there are no duplicate values. - -```python -Generation 1 -[[ 2 -2 -3 3] - [ 0 1 2 3] - [ 5 -3 6 3] - [-3 1 -2 4] - [-1 0 -2 3]] -Generation 2 -[[-1 0 -2 3] - [-3 1 -2 4] - [ 0 -3 -2 6] - [-3 0 -2 3] - [ 1 -4 2 4]] -Generation 3 -[[ 1 -4 2 4] - [-3 0 -2 3] - [ 4 0 -2 1] - [-4 0 -2 -3] - [-4 2 0 3]] -Generation 4 -[[-4 2 0 3] - [-4 0 -2 -3] - [-2 5 4 -3] - [-1 2 -4 4] - [-4 2 0 -3]] -Generation 5 -[[-4 2 0 -3] - [-1 2 -4 4] - [ 3 4 -4 0] - [-1 0 2 -2] - [-4 2 -1 1]] -``` - -The `allow_duplicate_genes` parameter can be used together with the `gene_space` parameter. Here is an example where each of the 4 genes has the same space of 4 values (1, 2, 3, and 4). - -```python -import pygad - -def fitness_func(ga_instance, solution, solution_idx): - return 0 - -def on_generation(ga): - print("Generation", ga.generations_completed) - print(ga.population) - -ga_instance = pygad.GA(num_generations=1, - sol_per_pop=5, - num_genes=4, - num_parents_mating=2, - fitness_func=fitness_func, - gene_type=int, - gene_space=[[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]], - on_generation=on_generation, - sample_size=200, - allow_duplicate_genes=False) -ga_instance.run() -``` - -Even though all the genes share the same space of values, no 2 genes have the same value, as shown in the next output. - -```python -Generation 1 -[[2 3 1 4] - [2 3 1 4] - [2 4 1 3] - [2 3 1 4] - [1 3 2 4]] -Generation 2 -[[1 3 2 4] - [2 3 1 4] - [1 3 2 4] - [2 3 4 1] - [1 3 4 2]] -Generation 3 -[[1 3 4 2] - [2 3 4 1] - [1 3 4 2] - [3 1 4 2] - [3 2 4 1]] -Generation 4 -[[3 2 4 1] - [3 1 4 2] - [3 2 4 1] - [1 2 4 3] - [1 3 4 2]] -Generation 5 -[[1 3 4 2] - [1 2 4 3] - [2 1 4 3] - [1 2 4 3] - [1 2 4 3]] -``` - -You should give enough values for the genes so that PyGAD can find an alternative when a gene value duplicates another gene. - -If PyGAD fails to find a unique gene value while there is still room to find one, then set the `sample_size` parameter to a larger value. Check the [sample_size Parameter](https://pygad.readthedocs.io/en/latest/pygad_more.html#sample-size-parameter) section for more information. - -### Limitation - -There might be 2 duplicate genes where changing either of the 2 duplicating genes will not solve the problem. For example, if `gene_space=[[3, 0, 1], [4, 1, 2], [0, 2], [3, 2, 0]]` and the solution is `[3 2 0 0]`, then the values of the last 2 genes duplicate. There are no possible changes in the last 2 genes to solve the problem. - -This problem can be solved by randomly changing one of the non-duplicating genes to make room for a unique value in one of the 2 duplicating genes. For example, by changing the second gene from 2 to 4, then any of the last 2 genes can take the value 2 and solve the duplicates. The resultant gene is then `[3 4 2 0]`. But this option is not yet supported in PyGAD. - -### Solve Duplicates using a Third Gene - -When `allow_duplicate_genes=False` and a user-defined `gene_space` is used, it sometimes happens that there is no room to solve the duplicates between the 2 genes by simply replacing the value of one gene with another. In [PyGAD 3.1.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-1-0), the duplicates are solved by looking for a third gene that helps solve them. The following examples explain how it works. - -Example 1: - -Let's assume that this gene space is used and there is a solution with 2 duplicate genes with the same value 4. - -```python -Gene space: [[2, 3], - [3, 4], - [4, 5], - [5, 6]] -Solution: [3, 4, 4, 5] -``` - -By checking the gene space, the second gene can have the values `[3, 4]` and the third gene can have the values `[4, 5]`. To solve the duplicates, we change the value of one of these 2 genes. - -If the value of the second gene changes from 4 to 3, then it will duplicate the first gene. If we change the value of the third gene from 4 to 5, then it will duplicate the fourth gene. In short, simply selecting a different value for either the second or third gene will introduce new duplicate genes. - -When there are 2 duplicate genes but there is no way to solve their duplicates, then the solution is to change a third gene that makes a room to solve the duplicates between the 2 genes. - -In our example, duplicates between the second and third genes can be solved by, for example,: - -* Changing the first gene from 3 to 2 then changing the second gene from 4 to 3. -* Or changing the fourth gene from 5 to 6 then changing the third gene from 4 to 5. - -Generally, this is how to solve such duplicates: +Elitism, stopping criteria, random seed, saving and continuing, and population size. +::: -1. For any duplicate gene **GENE1**, select another value. -2. Check which other gene **GENEX** has duplicate with this new value. -3. Find if **GENEX** can have another value that will not cause any more duplicates. If so, go to step 7. -4. If all the other values of **GENEX** will cause duplicates, then try another gene **GENEY**. -5. Repeat steps 3 and 4 until exploring all the genes. -6. If there is no way to solve the duplicates, then we have to keep the duplicate value. -7. If a value for a gene **GENEM** is found that will not cause more duplicates, then use this value for the gene **GENEM**. -8. Replace the value of the gene **GENE1** by the old value of the gene **GENEM**. This solves the duplicates. +:::{grid-item-card} Fitness Calculation and Performance +:link: fitness_calculation +:link-type: doc -This is an example to solve the duplicate for the solution `[3, 4, 4, 5]`: +Parallel processing, batch fitness, reusing fitness, and non-deterministic problems. +::: -1. Let's use the second gene with value 4. Because the space of this gene is `[3, 4]`, then the only other value we can select is 3. -2. The first gene also has the value 3. -3. The first gene has another value 2 that will not cause more duplicates in the solution. Then go to step 7. -4. Skip. -5. Skip. -6. Skip. -7. The value of the first gene 3 will be replaced by the new value 2. The new solution is [2, 4, 4, 5]. -8. Replace the value of the second gene 4 by the old value of the first gene which is 3. The new solution is [2, 3, 4, 5]. The duplicate is solved. +:::{grid-item-card} Logging and the Lifecycle Summary +:link: logging +:link-type: doc -Example 2: +Print a Keras-like summary and log the outputs. +::: -```python -Gene space: [[0, 1], - [1, 2], - [2, 3], - [3, 4]] -Solution: [1, 2, 2, 3] -``` +:::{grid-item-card} User-Defined Functions and Callbacks +:link: custom_functions +:link-type: doc -The quick summary is: +Pass your own functions or methods for the fitness and callbacks. +::: -* Change the value of the first gene from 1 to 0. The solution becomes [0, 2, 2, 3]. -* Change the value of the second gene from 2 to 1. The solution becomes [0, 1, 2, 3]. The duplicate is solved. +:::: -## More about the `gene_type` Parameter - -The `gene_type` parameter allows the user to control the data type for all genes at once or each individual gene. In [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0), the `gene_type` parameter also supports customizing the precision for `float` data types. As a result, the `gene_type` parameter helps to: - -1. Select a data type for all genes with or without precision. -2. Select a data type for each individual gene with or without precision. - -Let us look at some examples. - -### Data Type for All Genes without Precision - -The data type for all genes can be specified by assigning the numeric data type directly to the `gene_type` parameter. This is an example to make all genes of `int` data types. - -```python -gene_type=int -``` - -Given that the supported numeric data types of PyGAD include Python's `int` and `float` in addition to all numeric types of `NumPy`, then any of these types can be assigned to the `gene_type` parameter. - -If no precision is specified for a `float` data type, then the complete floating-point number is kept. - -The next code uses an `int` data type for all genes where the genes in the initial and final population are only integers. - -```python -import pygad -import numpy - -equation_inputs = [4, -2, 3.5, 8, -2] -desired_output = 2671.1234 - -def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - -ga_instance = pygad.GA(num_generations=10, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - gene_type=int) - -print("Initial Population") -print(ga_instance.initial_population) - -ga_instance.run() - -print("Final Population") -print(ga_instance.population) -``` - -```python -Initial Population -[[ 1 -1 2 0 -3] - [ 0 -2 0 -3 -1] - [ 0 -1 -1 2 0] - [-2 3 -2 3 3] - [ 0 0 2 -2 -2]] - -Final Population -[[ 1 -1 2 2 0] - [ 1 -1 2 2 0] - [ 1 -1 2 2 0] - [ 1 -1 2 2 0] - [ 1 -1 2 2 0]] -``` - -### Data Type for All Genes with Precision - -A precision can only be specified for a `float` data type and cannot be specified for integers. Here is an example to use a precision of 3 for the `float` data type. In this case, all genes are of type `float` and their maximum precision is 3. - -```python -gene_type=[float, 3] -``` - -The next code prints the initial and final population where the genes are of type `float` with precision 3. - -```python -import pygad -import numpy - -equation_inputs = [4, -2, 3.5, 8, -2] -desired_output = 2671.1234 - -def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - - return fitness - -ga_instance = pygad.GA(num_generations=10, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - gene_type=[float, 3]) - -print("Initial Population") -print(ga_instance.initial_population) - -ga_instance.run() - -print("Final Population") -print(ga_instance.population) -``` - -```python -Initial Population -[[-2.417 -0.487 3.623 2.457 -2.362] - [-1.231 0.079 -1.63 1.629 -2.637] - [ 0.692 -2.098 0.705 0.914 -3.633] - [ 2.637 -1.339 -1.107 -0.781 -3.896] - [-1.495 1.378 -1.026 3.522 2.379]] - -Final Population -[[ 1.714 -1.024 3.623 3.185 -2.362] - [ 0.692 -1.024 3.623 3.185 -2.362] - [ 0.692 -1.024 3.623 3.375 -2.362] - [ 0.692 -1.024 4.041 3.185 -2.362] - [ 1.714 -0.644 3.623 3.185 -2.362]] -``` - -### Data Type for each Individual Gene without Precision - -In [PyGAD 2.14.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-14-0), the `gene_type` parameter allows customizing the gene type for each individual gene. This is by using a `list`/`tuple`/`numpy.ndarray` with number of elements equal to the number of genes. For each element, a type is specified for the corresponding gene. - -This is an example for a 5-gene problem where different types are assigned to the genes. - -```python -gene_type=[int, float, numpy.float16, numpy.int8, float] -``` - -This is a complete code that prints the initial and final population for a custom-gene data type. - -```python -import pygad -import numpy - -equation_inputs = [4, -2, 3.5, 8, -2] -desired_output = 2671.1234 - -def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - -ga_instance = pygad.GA(num_generations=10, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - gene_type=[int, float, numpy.float16, numpy.int8, float]) - -print("Initial Population") -print(ga_instance.initial_population) - -ga_instance.run() - -print("Final Population") -print(ga_instance.population) -``` - -```python -Initial Population -[[0 0.8615522360026828 0.7021484375 -2 3.5301821368185866] - [-3 2.648189378595294 -3.830078125 1 -0.9586271572917742] - [3 3.7729827570110714 1.2529296875 -3 1.395741994211889] - [0 1.0490687178053282 1.51953125 -2 0.7243617940450235] - [0 -0.6550158436937226 -2.861328125 -2 1.8212734549263097]] - -Final Population -[[3 3.7729827570110714 2.055 0 0.7243617940450235] - [3 3.7729827570110714 1.458 0 -0.14638754050305036] - [3 3.7729827570110714 1.458 0 0.0869406120516778] - [3 3.7729827570110714 1.458 0 0.7243617940450235] - [3 3.7729827570110714 1.458 0 -0.14638754050305036]] -``` - -### Data Type for each Individual Gene with Precision - -The precision can also be specified for the `float` data types as in the next line where the second gene precision is 2 and last gene precision is 1. - -```python -gene_type=[int, [float, 2], numpy.float16, numpy.int8, [float, 1]] -``` - -This is a complete example where the initial and final populations are printed where the genes comply with the data types and precisions specified. - -```python -import pygad -import numpy - -equation_inputs = [4, -2, 3.5, 8, -2] -desired_output = 2671.1234 - -def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - -ga_instance = pygad.GA(num_generations=10, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - gene_type=[int, [float, 2], numpy.float16, numpy.int8, [float, 1]]) - -print("Initial Population") -print(ga_instance.initial_population) - -ga_instance.run() - -print("Final Population") -print(ga_instance.population) -``` - -```python -Initial Population -[[-2 -1.22 1.716796875 -1 0.2] - [-1 -1.58 -3.091796875 0 -1.3] - [3 3.35 -0.107421875 1 -3.3] - [-2 -3.58 -1.779296875 0 0.6] - [2 -3.73 2.65234375 3 -0.5]] - -Final Population -[[2 -4.22 3.47 3 -1.3] - [2 -3.73 3.47 3 -1.3] - [2 -4.22 3.47 2 -1.3] - [2 -4.58 3.47 3 -1.3] - [2 -3.73 3.47 3 -1.3]] -``` - -## Parallel Processing in PyGAD - -Starting from [PyGAD 2.17.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-17-0), parallel processing is supported. This section explains how to use parallel processing in PyGAD. - -According to the [PyGAD life cycle](https://pygad.readthedocs.io/en/latest/pygad.html#life-cycle-of-pygad), the computation can be parallelized in only 2 operations: - -1. Population fitness calculation. -2. Mutation. - -The reason is that the calculations in these 2 operations are independent (i.e. each solution/chromosome is handled independently from the others) and can be distributed across different processes or threads. - -For the mutation operation, it does not do intensive calculations on the CPU. Its calculations are simple like flipping the values of some genes from 0 to 1 or adding a random value to some genes. So, it does not take much CPU processing time. Experiments proved that parallelizing the mutation operation across the solutions increases the time instead of reducing it. This is because running multiple processes or threads adds overhead to manage them. Thus, parallel processing cannot be applied on the mutation operation. - -For the population fitness calculation, parallel processing can make a difference and reduce the processing time. But this depends on the type of calculations done in the fitness function. If the fitness function makes intensive calculations and takes much CPU time, then parallel processing will probably help cut down the overall time. - -This section explains how parallel processing works in PyGAD and how to use it. - -### How to Use Parallel Processing in PyGAD - -Starting from [PyGAD 2.17.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-17-0), a new parameter called `parallel_processing` was added to the constructor of the `pygad.GA` class. - -```python -import pygad -... -ga_instance = pygad.GA(..., - parallel_processing=...) -... -``` - -This parameter allows the user to do the following: - -1. Enable parallel processing. -2. Select whether processes or threads are used. -3. Specify the number of processes or threads to be used. - -These are 3 possible values for the `parallel_processing` parameter: - -1) `None`: (Default) It means no parallel processing is used. -2) A positive integer referring to the number of threads to be used (threads, not processes). -3) `list`/`tuple`: If a list or a tuple of exactly 2 elements is assigned, then: - 1) The first element can be either `'process'` or `'thread'` to specify whether processes or threads are used, respectively. - 2) The second element can be: - 1) A positive integer to select the maximum number of processes or threads to be used - 2) `0` to indicate that 0 processes or threads are used. It means no parallel processing. This is identical to setting `parallel_processing=None`. - 3) `None` to use the default value as calculated by the `concurrent.futures` module. - -These are examples of the values assigned to the `parallel_processing` parameter: - -* `parallel_processing=4`: Because the parameter is assigned a positive integer, this means parallel processing is activated where 4 threads are used. -* `parallel_processing=["thread", 5]`: Use parallel processing with 5 threads. This is identical to `parallel_processing=5`. -* `parallel_processing=["process", 8]`: Use parallel processing with 8 processes. -* `parallel_processing=["process", 0]`: As the second element is given the value 0, this means do not use parallel processing. This is identical to `parallel_processing=None`. - -### Examples - -These examples will help you see the difference between using processes and threads. They also give an idea of when parallel processing makes a difference and reduces the time. These are dummy examples where the fitness function always returns 0. - -The first example uses 10 genes, 5 solutions in the population where only 3 solutions mate, and 9999 generations. The fitness function uses a `for` loop with 100 iterations just to have some calculations. In the constructor of the `pygad.GA` class, `parallel_processing=None` means no parallel processing is used. - -```python -import pygad -import time - -def fitness_func(ga_instance, solution, solution_idx): - for _ in range(99): - pass - return 0 - -ga_instance = pygad.GA(num_generations=9999, - num_parents_mating=3, - sol_per_pop=5, - num_genes=10, - fitness_func=fitness_func, - suppress_warnings=True, - parallel_processing=None) - -if __name__ == '__main__': - t1 = time.time() - - ga_instance.run() - - t2 = time.time() - print("Time is", t2-t1) -``` - -When parallel processing is not used, the time it takes to run the genetic algorithm is `1.5` seconds. - -For comparison, let us run a second experiment where parallel processing is used with 5 threads. In this case, it takes `5` seconds. - -```python -... -ga_instance = pygad.GA(..., - parallel_processing=5) -... -``` - -For the third experiment, processes instead of threads are used. Also, only 99 generations are used instead of 9999. The time it takes is `99` seconds. - -```python -... -ga_instance = pygad.GA(num_generations=99, - ..., - parallel_processing=["process", 5]) -... -``` - -This is the summary of the 3 experiments: - -1. No parallel processing & 9999 generations: 1.5 seconds. -2. Parallel processing with 5 threads & 9999 generations: 5 seconds -3. Parallel processing with 5 processes & 99 generations: 99 seconds - -Because the fitness function does not need much CPU time, the normal processing takes the least time. Running processes for this simple problem takes 99 compared to only 5 seconds for threads because managing processes is much heavier than managing threads. Thus, most of the CPU time is for swapping the processes instead of executing the code. - -In the second example, the loop makes 99999999 iterations and only 5 generations are used. With no parallelization, it takes 22 seconds. - -```python -import pygad -import time - -def fitness_func(ga_instance, solution, solution_idx): - for _ in range(99999999): - pass - return 0 - -ga_instance = pygad.GA(num_generations=5, - num_parents_mating=3, - sol_per_pop=5, - num_genes=10, - fitness_func=fitness_func, - suppress_warnings=True, - parallel_processing=None) - -if __name__ == '__main__': - t1 = time.time() - ga_instance.run() - t2 = time.time() - print("Time is", t2-t1) -``` - -It takes 15 seconds when 10 processes are used. - -```python -... -ga_instance = pygad.GA(..., - parallel_processing=["process", 10]) -... -``` - -This is compared to 20 seconds when 10 threads are used. - -```python -... -ga_instance = pygad.GA(..., - parallel_processing=["thread", 10]) -... -``` - -Based on the second example, using parallel processing with 10 processes takes the least time because there is a lot of CPU work. Generally, processes are preferred over threads when most of the work is on the CPU. Threads are preferred over processes in some situations, like doing input/output operations. - -*Before releasing [PyGAD 2.17.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-17-0), [László Fazekas](https://www.linkedin.com/in/l%C3%A1szl%C3%B3-fazekas-2429a912) wrote an article to parallelize the fitness function with PyGAD. Check it: [How Genetic Algorithms Can Compete with Gradient Descent and Backprop](https://hackernoon.com/how-genetic-algorithms-can-compete-with-gradient-descent-and-backprop-9m9t33bq)*. - -## Print Lifecycle Summary - -In [PyGAD 2.19.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-19-0), a new method called `summary()` is supported. It prints a Keras-like summary of the PyGAD lifecycle showing the steps, callback functions, parameters, etc. - - This method accepts the following parameters: - -- `line_length=70`: An integer representing the length of the single line in characters. -- `fill_character=" "`: A character to fill the lines. -- `line_character="-"`: A character for creating a line separator. -- `line_character2="="`: A secondary character to create a line separator. -- `columns_equal_len=False`: The table rows are split into equal-sized columns or split subjective to the width needed. -- `print_step_parameters=True`: Whether to print extra parameters about each step inside the step. If `print_step_parameters=False` and `print_parameters_summary=True`, then the parameters of each step are printed at the end of the table. -- `print_parameters_summary=True`: Whether to print parameters summary at the end of the table. If `print_step_parameters=False`, then the parameters of each step are printed at the end of the table too. - -Here is a quick example. - -```python -import pygad -import numpy - -function_inputs = [4,-2,3.5,5,-11,-4.7] -desired_output = 44 - -def genetic_fitness(solution, solution_idx): - output = numpy.sum(solution*function_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - -def on_gen(ga): - pass - -def on_crossover_callback(a, b): - pass - -ga_instance = pygad.GA(num_generations=100, - num_parents_mating=10, - sol_per_pop=20, - num_genes=len(function_inputs), - on_crossover=on_crossover_callback, - on_generation=on_gen, - parallel_processing=2, - stop_criteria="reach_10", - fitness_batch_size=4, - crossover_probability=0.4, - fitness_func=genetic_fitness) -``` - -Then call the `summary()` method to print the summary with the default parameters. Note that entries for the crossover and generation callbacks are created because they are implemented through `on_crossover_callback()` and `on_gen()`, respectively. - -```python -ga_instance.summary() -``` - -```bash ----------------------------------------------------------------------- - PyGAD Lifecycle -====================================================================== -Step Handler Output Shape -====================================================================== -Fitness Function genetic_fitness() (1) -Fitness batch size: 4 ----------------------------------------------------------------------- -Parent Selection steady_state_selection() (10, 6) -Number of Parents: 10 ----------------------------------------------------------------------- -Crossover single_point_crossover() (10, 6) -Crossover probability: 0.4 ----------------------------------------------------------------------- -On Crossover on_crossover_callback() None ----------------------------------------------------------------------- -Mutation random_mutation() (10, 6) -Mutation Genes: 1 -Random Mutation Range: (-1.0, 1.0) -Mutation by Replacement: False -Allow Duplicated Genes: True ----------------------------------------------------------------------- -On Generation on_gen() None -Stop Criteria: [['reach', 10.0]] ----------------------------------------------------------------------- -====================================================================== -Population Size: (20, 6) -Number of Generations: 100 -Initial Population Range: (-4, 4) -Keep Elitism: 1 -Gene DType: [, None] -Parallel Processing: ['thread', 2] -Save Best Solutions: False -Save Solutions: False -====================================================================== -``` - -We can set the `print_step_parameters` and `print_parameters_summary` parameters to `False` to not print the parameters. - -```python -ga_instance.summary(print_step_parameters=False, - print_parameters_summary=False) -``` - -```bash ----------------------------------------------------------------------- - PyGAD Lifecycle -====================================================================== -Step Handler Output Shape -====================================================================== -Fitness Function genetic_fitness() (1) ----------------------------------------------------------------------- -Parent Selection steady_state_selection() (10, 6) ----------------------------------------------------------------------- -Crossover single_point_crossover() (10, 6) ----------------------------------------------------------------------- -On Crossover on_crossover_callback() None ----------------------------------------------------------------------- -Mutation random_mutation() (10, 6) ----------------------------------------------------------------------- -On Generation on_gen() None ----------------------------------------------------------------------- -====================================================================== -``` - -## Logging Outputs - -In [PyGAD 3.0.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-0-0), the `print()` statement is no longer used and the outputs are printed using the [logging](https://docs.python.org/3/library/logging.html) module. A new parameter called `logger` is supported to accept a user-defined logger. - -```python -import logging - -logger = ... - -ga_instance = pygad.GA(..., - logger=logger, - ...) -``` - -The default value for this parameter is `None`. If there is no logger passed (i.e. `logger=None`), then a default logger is created to log the messages to the console exactly like how the `print()` statement works. - -Some advantages of using the [logging](https://docs.python.org/3/library/logging.html) module instead of the `print()` statement are: - -1. The user has more control over the printed messages, especially in a project that uses multiple modules where each module prints its messages. A logger can organize the outputs. -2. Using the proper `Handler`, the user can log the output messages to files, not only to the console. So, it is much easier to record the outputs. -3. The format of the printed messages can be changed by customizing the `Formatter` assigned to the Logger. - -This section gives some quick examples to use the `logging` module and then gives an example to use the logger with PyGAD. - -### Logging to the Console - -This is an example to create a logger to log the messages to the console. - -```python -import logging - -# Create a logger -logger = logging.getLogger(__name__) - -# Set the logger level to debug so that all the messages are printed. -logger.setLevel(logging.DEBUG) - -# Create a stream handler to log the messages to the console. -stream_handler = logging.StreamHandler() - -# Set the handler level to debug. -stream_handler.setLevel(logging.DEBUG) - -# Create a formatter -formatter = logging.Formatter('%(message)s') - -# Add the formatter to handler. -stream_handler.setFormatter(formatter) - -# Add the stream handler to the logger -logger.addHandler(stream_handler) -``` - -Now, we can log messages to the console with the format specified in the `Formatter`. - -```python -logger.debug('Debug message.') -logger.info('Info message.') -logger.warning('Warn message.') -logger.error('Error message.') -logger.critical('Critical message.') -``` - -The outputs are identical to those returned using the `print()` statement. - -``` -Debug message. -Info message. -Warn message. -Error message. -Critical message. -``` - -By changing the format of the output messages, we can have more information about each message. - -```python -formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S') -``` - -This is a sample output. - -```python -2023-04-03 18:46:27 DEBUG: Debug message. -2023-04-03 18:46:27 INFO: Info message. -2023-04-03 18:46:27 WARNING: Warn message. -2023-04-03 18:46:27 ERROR: Error message. -2023-04-03 18:46:27 CRITICAL: Critical message. -``` - -Note that you may need to clear the handlers after finishing the execution. This is to make sure no cached handlers are used in the next run. If the cached handlers are not cleared, then the single output message may be repeated. - -```python -logger.handlers.clear() -``` - -### Logging to a File - -This is another example to log the messages to a file named `logfile.txt`. The formatter prints the following about each message: - -1. The date and time at which the message is logged. -2. The log level. -3. The message. -4. The path of the file. -5. The line number of the log message. - -```python -import logging - -level = logging.DEBUG -name = 'logfile.txt' - -logger = logging.getLogger(name) -logger.setLevel(level) - -file_handler = logging.FileHandler(name, 'a+', 'utf-8') -file_handler.setLevel(logging.DEBUG) -file_format = logging.Formatter('%(asctime)s %(levelname)s: %(message)s - %(pathname)s:%(lineno)d', datefmt='%Y-%m-%d %H:%M:%S') -file_handler.setFormatter(file_format) -logger.addHandler(file_handler) -``` - -This is what the outputs look like. - -```python -2023-04-03 18:54:03 DEBUG: Debug message. - c:\users\agad069\desktop\logger\example2.py:46 -2023-04-03 18:54:03 INFO: Info message. - c:\users\agad069\desktop\logger\example2.py:47 -2023-04-03 18:54:03 WARNING: Warn message. - c:\users\agad069\desktop\logger\example2.py:48 -2023-04-03 18:54:03 ERROR: Error message. - c:\users\agad069\desktop\logger\example2.py:49 -2023-04-03 18:54:03 CRITICAL: Critical message. - c:\users\agad069\desktop\logger\example2.py:50 -``` - -Consider clearing the handlers if necessary. - -```python -logger.handlers.clear() -``` - -### Log to Both the Console and a File - -This is an example to create a single Logger associated with 2 handlers: - -1. A file handler. -2. A stream handler. - -```python -import logging - -level = logging.DEBUG -name = 'logfile.txt' - -logger = logging.getLogger(name) -logger.setLevel(level) - -file_handler = logging.FileHandler(name,'a+','utf-8') -file_handler.setLevel(logging.DEBUG) -file_format = logging.Formatter('%(asctime)s %(levelname)s: %(message)s - %(pathname)s:%(lineno)d', datefmt='%Y-%m-%d %H:%M:%S') -file_handler.setFormatter(file_format) -logger.addHandler(file_handler) - -console_handler = logging.StreamHandler() -console_handler.setLevel(logging.INFO) -console_format = logging.Formatter('%(message)s') -console_handler.setFormatter(console_format) -logger.addHandler(console_handler) -``` - -When a log message is executed, then it is both printed to the console and saved in the `logfile.txt`. - -Consider clearing the handlers if necessary. - -```python -logger.handlers.clear() -``` - -### PyGAD Example - -To use the logger in PyGAD, just create your custom logger and pass it to the `logger` parameter. - -```python -import logging -import pygad -import numpy - -level = logging.DEBUG -name = 'logfile.txt' - -logger = logging.getLogger(name) -logger.setLevel(level) - -file_handler = logging.FileHandler(name,'a+','utf-8') -file_handler.setLevel(logging.DEBUG) -file_format = logging.Formatter('%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S') -file_handler.setFormatter(file_format) -logger.addHandler(file_handler) - -console_handler = logging.StreamHandler() -console_handler.setLevel(logging.INFO) -console_format = logging.Formatter('%(message)s') -console_handler.setFormatter(console_format) -logger.addHandler(console_handler) - -equation_inputs = [4, -2, 8] -desired_output = 2671.1234 - -def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - -def on_generation(ga_instance): - ga_instance.logger.info(f"Generation = {ga_instance.generations_completed}") - ga_instance.logger.info(f"Fitness = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1]}") - -ga_instance = pygad.GA(num_generations=10, - sol_per_pop=40, - num_parents_mating=2, - keep_parents=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - on_generation=on_generation, - logger=logger) -ga_instance.run() - -logger.handlers.clear() -``` - -By executing this code, the logged messages are printed to the console and also saved in the text file. - -```python -2023-04-03 19:04:27 INFO: Generation = 1 -2023-04-03 19:04:27 INFO: Fitness = 0.00038086960368076276 -2023-04-03 19:04:27 INFO: Generation = 2 -2023-04-03 19:04:27 INFO: Fitness = 0.00038214871408010853 -2023-04-03 19:04:27 INFO: Generation = 3 -2023-04-03 19:04:27 INFO: Fitness = 0.0003832795907974678 -2023-04-03 19:04:27 INFO: Generation = 4 -2023-04-03 19:04:27 INFO: Fitness = 0.00038398612055017196 -2023-04-03 19:04:27 INFO: Generation = 5 -2023-04-03 19:04:27 INFO: Fitness = 0.00038442348890867516 -2023-04-03 19:04:27 INFO: Generation = 6 -2023-04-03 19:04:27 INFO: Fitness = 0.0003854406039137763 -2023-04-03 19:04:27 INFO: Generation = 7 -2023-04-03 19:04:27 INFO: Fitness = 0.00038646083174063284 -2023-04-03 19:04:27 INFO: Generation = 8 -2023-04-03 19:04:27 INFO: Fitness = 0.0003875169193024936 -2023-04-03 19:04:27 INFO: Generation = 9 -2023-04-03 19:04:27 INFO: Fitness = 0.0003888816727311021 -2023-04-03 19:04:27 INFO: Generation = 10 -2023-04-03 19:04:27 INFO: Fitness = 0.000389832593101348 -``` - -## Solve Non-Deterministic Problems - -PyGAD can be used to solve both deterministic and non-deterministic problems. Deterministic problems are those that return the same fitness for the same solution. For non-deterministic problems, a different fitness value may be returned for the same solution. - -By default, PyGAD settings are set to solve deterministic problems. PyGAD can save the explored solutions and their fitness to reuse them in the future. These instance attributes can save the solutions: - -1. `solutions`: Exists if `save_solutions=True`. -2. `best_solutions`: Exists if `save_best_solutions=True`. -3. `last_generation_elitism`: Exists if `keep_elitism` > 0. -4. `last_generation_parents`: Exists if `keep_parents` > 0 or `keep_parents=-1`. - -To configure PyGAD for non-deterministic problems, we have to disable saving the previous solutions. This is by setting these parameters: - -1. `keep_elitism=0` -2. `keep_parents=0` -3. `save_solutions=False` -4. `save_best_solutions=False` - -```python -import pygad -... -ga_instance = pygad.GA(..., - keep_elitism=0, - keep_parents=0, - save_solutions=False, - save_best_solutions=False, - ...) -``` - -This way, PyGAD will not save any explored solution, so the fitness function has to be called for each individual solution. - -## Reuse the Fitness instead of Calling the Fitness Function - -It may happen that a previously explored solution in generation X is explored again in another generation Y (where Y > X). For some problems, calling the fitness function takes much time. - -For deterministic problems, it is better not to call the fitness function for an already explored solution. Instead, reuse the fitness of the old solution. PyGAD supports some options to help you save the time of calling the fitness function for a previously explored solution. - -The parameters explored in this section can be set in the constructor of the `pygad.GA` class. - -The `cal_pop_fitness()` method of the `pygad.GA` class checks these parameters to see if there is a possibility of reusing the fitness instead of calling the fitness function. - -### 1. `save_solutions` - -It defaults to `False`. If set to `True`, then the population of each generation is saved into the `solutions` attribute of the `pygad.GA` instance. In other words, every single solution is saved in the `solutions` attribute. - -### 2. `save_best_solutions` - -It defaults to `False`. If `True`, then it only saves the best solution in every generation. - -### 3. `keep_elitism` - -It accepts an integer and defaults to 1. If set to a positive integer, then it keeps the elitism of one generation available in the next generation. - -### 4. `keep_parents` - -It accepts an integer and defaults to -1. If set to `-1` or a positive integer, then it keeps the parents of one generation available in the next generation. - -## Why the Fitness Function is not Called for Solution at Index 0? - -PyGAD has a parameter called `keep_elitism` which defaults to 1. This parameter defines the number of best solutions in generation **X** to keep in the next generation **X+1**. The best solutions are just copied from generation **X** to generation **X+1** without making any change. - -```python -ga_instance = pygad.GA(..., - keep_elitism=1, - ...) -``` - -The best solutions are copied at the beginning of the population. If `keep_elitism=1`, this means the best solution in generation X is kept in the next generation X+1 at index 0 of the population. If `keep_elitism=2`, this means the 2 best solutions in generation X are kept in the next generation X+1 at indices 0 and 1 of the population. - -Because the fitness values of these best solutions are already calculated in generation X, they are not recalculated at generation X+1 (the fitness function is not called for these solutions again). Instead, their fitness values are reused. This is why no solution with index 0 is passed to the fitness function. - -To force calling the fitness function for each solution in every generation, consider setting `keep_elitism` and `keep_parents` to 0. Moreover, keep the 2 parameters `save_solutions` and `save_best_solutions` to their default value `False`. - -```python -ga_instance = pygad.GA(..., - keep_elitism=0, - keep_parents=0, - save_solutions=False, - save_best_solutions=False, - ...) -``` - - - -## Batch Fitness Calculation - -In [PyGAD 2.19.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-19-0), a new optional parameter called `fitness_batch_size` is supported to calculate the fitness function in batches. Thanks to [Linan Qiu](https://github.com/linanqiu) for opening the [GitHub issue #136](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/136). - -Its values can be: - -* `1` or `None`: If the `fitness_batch_size` parameter is assigned the value `1` or `None` (default), then the normal flow is used where the fitness function is called for each individual solution. That is if there are 15 solutions, then the fitness function is called 15 times. -* `1 < fitness_batch_size <= sol_per_pop`: If the `fitness_batch_size` parameter is assigned a value satisfying this condition `1 < fitness_batch_size <= sol_per_pop`, then the solutions are grouped into batches of size `fitness_batch_size` and the fitness function is called once for each batch. In this case, the fitness function must return a list/tuple/numpy.ndarray with a length equal to the number of solutions passed. - -### Example without `fitness_batch_size` Parameter - -This is an example where the `fitness_batch_size` parameter is given the value `None` (which is the default value). This is equivalent to using the value `1`. In this case, the fitness function will be called for each solution. This means the fitness function `fitness_func` will receive only a single solution. This is an example of the passed arguments to the fitness function: - -``` -solution: [ 2.52860734, -0.94178795, 2.97545704, 0.84131987, -3.78447118, 2.41008358] -solution_idx: 3 -``` - -The fitness function also must return a single numeric value as the fitness for the passed solution. - -As we have a population of `20` solutions, then the fitness function is called 20 times per generation. For 5 generations, then the fitness function is called `20*5 = 100` times. In PyGAD, the fitness function is called after the last generation too and this adds additional 20 times. So, the total number of calls to the fitness function is `20*5 + 20 = 120`. - -Note that the `keep_elitism` and `keep_parents` parameters are set to `0` to make sure no fitness values are reused and to force calling the fitness function for each individual solution. - -```python -import pygad -import numpy - -function_inputs = [4,-2,3.5,5,-11,-4.7] -desired_output = 44 - -number_of_calls = 0 - -def fitness_func(ga_instance, solution, solution_idx): - global number_of_calls - number_of_calls = number_of_calls + 1 - output = numpy.sum(solution*function_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - -ga_instance = pygad.GA(num_generations=5, - num_parents_mating=10, - sol_per_pop=20, - fitness_func=fitness_func, - fitness_batch_size=None, - # fitness_batch_size=1, - num_genes=len(function_inputs), - keep_elitism=0, - keep_parents=0) - -ga_instance.run() -print(number_of_calls) -``` - -``` -120 -``` - -### Example with `fitness_batch_size` Parameter - -This is an example where the `fitness_batch_size` parameter is used and assigned the value `4`. This means the solutions will be grouped into batches of `4` solutions. The fitness function will be called once for each batch (called once for every 4 solutions). - -This is an example of the arguments passed to it: - -```python -solutions: - [[ 3.1129432 -0.69123589 1.93792414 2.23772968 -1.54616001 -0.53930799] - [ 3.38508121 0.19890812 1.93792414 2.23095014 -3.08955597 3.10194128] - [ 2.37079504 -0.88819803 2.97545704 1.41742256 -3.95594055 2.45028256] - [ 2.52860734 -0.94178795 2.97545704 0.84131987 -3.78447118 2.41008358]] -solutions_indices: - [16, 17, 18, 19] -``` - -As we have 20 solutions, then there are `20/4 = 5` batches. As a result, the fitness function is called only 5 times per generation instead of 20. For each call, the fitness function receives a batch of 4 solutions. - -As we have 5 generations, then the function will be called `5*5 = 25` times. Given the call to the fitness function after the last generation, then the total number of calls is `5*5 + 5 = 30`. - -```python -import pygad -import numpy - -function_inputs = [4,-2,3.5,5,-11,-4.7] -desired_output = 44 - -number_of_calls = 0 - -def fitness_func_batch(ga_instance, solutions, solutions_indices): - global number_of_calls - number_of_calls = number_of_calls + 1 - batch_fitness = [] - for solution in solutions: - output = numpy.sum(solution*function_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - batch_fitness.append(fitness) - return batch_fitness - -ga_instance = pygad.GA(num_generations=5, - num_parents_mating=10, - sol_per_pop=20, - fitness_func=fitness_func_batch, - fitness_batch_size=4, - num_genes=len(function_inputs), - keep_elitism=0, - keep_parents=0) - -ga_instance.run() -print(number_of_calls) -``` - -``` -30 -``` - -When batch fitness calculation is used, then we saved `120 - 30 = 90` calls to the fitness function. - -## Use Functions and Methods to Build Fitness and Callbacks - -In PyGAD 2.19.0, it is possible to pass user-defined functions or methods to the following parameters: - -1. `fitness_func` -2. `on_start` -3. `on_fitness` -4. `on_parents` -5. `on_crossover` -6. `on_mutation` -7. `on_generation` -8. `on_stop` - -This section gives 2 examples of how to build these handlers using: - -1. Functions. -2. Methods. - -### Assign Functions - -This is a dummy example where the fitness function returns a random value. Note that the instance of the `pygad.GA` class is passed as the last parameter of all functions. - -```python -import pygad -import numpy - -def fitness_func(ga_instance, solution, solution_idx): - return numpy.random.rand() - -def on_start(ga_instance): - print("on_start") - -def on_fitness(ga_instance, last_gen_fitness): - print("on_fitness") - -def on_parents(ga_instance, last_gen_parents): - print("on_parents") - -def on_crossover(ga_instance, last_gen_offspring): - print("on_crossover") - -def on_mutation(ga_instance, last_gen_offspring): - print("on_mutation") - -def on_generation(ga_instance): - print("on_generation\n") - -def on_stop(ga_instance, last_gen_fitness): - print("on_stop") - -ga_instance = pygad.GA(num_generations=5, - num_parents_mating=4, - sol_per_pop=10, - num_genes=2, - on_start=on_start, - on_fitness=on_fitness, - on_parents=on_parents, - on_crossover=on_crossover, - on_mutation=on_mutation, - on_generation=on_generation, - on_stop=on_stop, - fitness_func=fitness_func) - -ga_instance.run() -``` - -### Assign Methods - -The next example has all the methods defined inside the class `Test`. All of the methods accept an additional parameter representing the method's object of the class `Test`. - -All methods accept `self` as the first parameter and the instance of the `pygad.GA` class as the last parameter. - -```python -import pygad -import numpy - -class Test: - def fitness_func(self, ga_instance, solution, solution_idx): - return numpy.random.rand() - - def on_start(self, ga_instance): - print("on_start") - - def on_fitness(self, ga_instance, last_gen_fitness): - print("on_fitness") - - def on_parents(self, ga_instance, last_gen_parents): - print("on_parents") - - def on_crossover(self, ga_instance, last_gen_offspring): - print("on_crossover") - - def on_mutation(self, ga_instance, last_gen_offspring): - print("on_mutation") - - def on_generation(self, ga_instance): - print("on_generation\n") - - def on_stop(self, ga_instance, last_gen_fitness): - print("on_stop") - -ga_instance = pygad.GA(num_generations=5, - num_parents_mating=4, - sol_per_pop=10, - num_genes=2, - on_start=Test().on_start, - on_fitness=Test().on_fitness, - on_parents=Test().on_parents, - on_crossover=Test().on_crossover, - on_mutation=Test().on_mutation, - on_generation=Test().on_generation, - on_stop=Test().on_stop, - fitness_func=Test().fitness_func) - -ga_instance.run() -``` +:::{toctree} +:hidden: +multi_objective +gene_values +generations +fitness_calculation +logging +custom_functions +::: diff --git a/docs/source/releases.md b/docs/source/releases.md index 9c19869..461d09b 100644 --- a/docs/source/releases.md +++ b/docs/source/releases.md @@ -107,7 +107,7 @@ Release date: 19 July 2020 2. A new optional parameter named `linewidth` is added to the `plot_result()` method to specify the width of the curve in the plot. It defaults to 3.0. 3. Previously, the indices of the genes selected for mutation was randomly generated once for all solutions within the generation. Currently, the genes' indices are randomly generated for each solution in the population. If the population has 4 solutions, the indices are randomly generated 4 times inside the single generation, 1 time for each solution. 4. Previously, the position of the point(s) for the single-point and two-points crossover was(were) randomly selected once for all solutions within the generation. Currently, the position(s) is(are) randomly selected for each solution in the population. If the population has 4 solutions, the position(s) is(are) randomly generated 4 times inside the single generation, 1 time for each solution. -5. A new optional parameter named `gene_space` as added to the `pygad.GA` class constructor. It is used to specify the possible values for each gene in case the user wants to restrict the gene values. It is useful if the gene space is restricted to a certain range or to discrete values. For more information, check the [More about the `gene_space` Parameter](https://pygad.readthedocs.io/en/latest/pygad_more.html#more-about-the-gene-space-parameter) section. Thanks to [Prof. Tamer A. Farrag](https://github.com/tfarrag2000) for requesting this useful feature. +5. A new optional parameter named `gene_space` as added to the `pygad.GA` class constructor. It is used to specify the possible values for each gene in case the user wants to restrict the gene values. It is useful if the gene space is restricted to a certain range or to discrete values. For more information, check the [More about the `gene_space` Parameter](https://pygad.readthedocs.io/en/latest/gene_values.html#more-about-the-gene-space-parameter) section. Thanks to [Prof. Tamer A. Farrag](https://github.com/tfarrag2000) for requesting this useful feature. ## PyGAD 2.6.0 @@ -208,7 +208,7 @@ Release Date: 15 January 2021 Release Date: 16 February 2021 -1. In the `gene_space` argument, the user can use a dictionary to specify the lower and upper limits of the gene. This dictionary must have only 2 items with keys `low` and `high` to specify the low and high limits of the gene, respectively. This way, PyGAD takes care of not exceeding the value limits of the gene. For a problem with only 2 genes, then using `gene_space=[{'low': 1, 'high': 5}, {'low': 0.2, 'high': 0.81}]` means the accepted values in the first gene start from 1 (inclusive) to 5 (exclusive) while the second one has values between 0.2 (inclusive) and 0.85 (exclusive). For more information, please check the [Limit the Gene Value Range](https://pygad.readthedocs.io/en/latest/pygad_more.html#limit-the-gene-value-range-using-the-gene-space-parameter) section of the documentation. +1. In the `gene_space` argument, the user can use a dictionary to specify the lower and upper limits of the gene. This dictionary must have only 2 items with keys `low` and `high` to specify the low and high limits of the gene, respectively. This way, PyGAD takes care of not exceeding the value limits of the gene. For a problem with only 2 genes, then using `gene_space=[{'low': 1, 'high': 5}, {'low': 0.2, 'high': 0.81}]` means the accepted values in the first gene start from 1 (inclusive) to 5 (exclusive) while the second one has values between 0.2 (inclusive) and 0.85 (exclusive). For more information, please check the [Limit the Gene Value Range](https://pygad.readthedocs.io/en/latest/gene_values.html#limit-the-gene-value-range-using-the-gene-space-parameter) section of the documentation. 2. The `plot_result()` method returns the figure so that the user can save it. 3. Bug fixes in copying elements from the gene space. 4. For a gene with a set of discrete values (more than 1 value) in the `gene_space` parameter like `[0, 1]`, it was possible that the gene value may not change after mutation. That is if the current value is 0, then the randomly selected value could also be 0. Now, it is verified that the new value is changed. So, if the current value is 0, then the new value after mutation will not be 0 but 1. @@ -228,7 +228,7 @@ Thanks to [Marios Giouvanakis](https://www.researchgate.net/profile/Marios-Giouv Release Date: 12 March 2021 -1. A new `bool` parameter called `allow_duplicate_genes` is supported. If `True`, which is the default, then a solution/chromosome may have duplicate gene values. If `False`, then each gene will have a unique value in its solution. Check the [Prevent Duplicates in Gene Values](https://pygad.readthedocs.io/en/latest/pygad_more.html#prevent-duplicates-in-gene-values) section for more details. +1. A new `bool` parameter called `allow_duplicate_genes` is supported. If `True`, which is the default, then a solution/chromosome may have duplicate gene values. If `False`, then each gene will have a unique value in its solution. Check the [Prevent Duplicates in Gene Values](https://pygad.readthedocs.io/en/latest/gene_values.html#prevent-duplicates-in-gene-values) section for more details. 2. The `last_generation_fitness` is updated at the end of each generation not at the beginning. This keeps the fitness values of the most up-to-date population assigned to the `last_generation_fitness` parameter. ## PyGAD 2.14.0 @@ -238,7 +238,7 @@ PyGAD 2.14.0 has an issue that is solved in PyGAD 2.14.1. Please consider using Release Date: 19 May 2021 1. [Issue #40](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/40) is solved. Now, the `None` value works with the `crossover_type` and `mutation_type` parameters: https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/40 -2. The `gene_type` parameter supports accepting a `list/tuple/numpy.ndarray` of numeric data types for the genes. This helps to control the data type of each individual gene. Previously, the `gene_type` can be assigned only to a single data type that is applied for all genes. For more information, check the [More about the `gene_type` Parameter](https://pygad.readthedocs.io/en/latest/pygad_more.html#more-about-the-gene-type-parameter) section. Thanks to [Rainer Engel](https://www.linkedin.com/in/rainer-matthias-engel-5ba47a9) for asking about this feature in [this discussion](https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/43): https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/43 +2. The `gene_type` parameter supports accepting a `list/tuple/numpy.ndarray` of numeric data types for the genes. This helps to control the data type of each individual gene. Previously, the `gene_type` can be assigned only to a single data type that is applied for all genes. For more information, check the [More about the `gene_type` Parameter](https://pygad.readthedocs.io/en/latest/gene_values.html#more-about-the-gene-type-parameter) section. Thanks to [Rainer Engel](https://www.linkedin.com/in/rainer-matthias-engel-5ba47a9) for asking about this feature in [this discussion](https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/43): https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/43 3. A new `bool` attribute named `gene_type_single` is added to the `pygad.GA` class. It is `True` when there is a single data type assigned to the `gene_type` parameter. When the `gene_type` parameter is assigned a `list/tuple/numpy.ndarray`, then `gene_type_single` is set to `False`. 4. The `mutation_by_replacement` flag now has no effect if `gene_space` exists except for the genes with `None` values. For example, for `gene_space=[None, [5, 6]]` the `mutation_by_replacement` flag affects only the first gene which has `None` for its value space. 5. When an element has a value of `None` in the `gene_space` parameter (e.g. `gene_space=[None, [5, 6]]`), then its value will be randomly generated for each solution rather than being generate once for all solutions. Previously, the gene with `None` value in `gene_space` is the same across all solutions @@ -266,7 +266,7 @@ Release Date: 17 June 2021 2. A new attribute named `last_generation_parents_indices` holds the indices of the selected parents in the last generation. 3. In adaptive mutation, no need to recalculate the fitness values of the parents selected in the last generation as these values can be returned based on the `last_generation_fitness` and `last_generation_parents_indices` attributes. This speeds-up the adaptive mutation. 4. When a sublist has a value of `None` in the `gene_space` parameter (e.g. `gene_space=[[1, 2, 3], [5, 6, None]]`), then its value will be randomly generated for each solution rather than being generated once for all solutions. Previously, a value of `None` in a sublist of the `gene_space` parameter was identical across all solutions. -5. The dictionary assigned to the `gene_space` parameter itself or one of its elements has a new key called `"step"` to specify the step of moving from the start to the end of the range specified by the 2 existing keys `"low"` and `"high"`. An example is `{"low": 0, "high": 30, "step": 2}` to have only even values for the gene(s) starting from 0 to 30. For more information, check the [More about the `gene_space` Parameter](https://pygad.readthedocs.io/en/latest/pygad_more.html#more-about-the-gene-space-parameter) section. https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/48 +5. The dictionary assigned to the `gene_space` parameter itself or one of its elements has a new key called `"step"` to specify the step of moving from the start to the end of the range specified by the 2 existing keys `"low"` and `"high"`. An example is `{"low": 0, "high": 30, "step": 2}` to have only even values for the gene(s) starting from 0 to 30. For more information, check the [More about the `gene_space` Parameter](https://pygad.readthedocs.io/en/latest/gene_values.html#more-about-the-gene-space-parameter) section. https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/48 6. A new function called `predict()` is added in both the `pygad.kerasga` and `pygad.torchga` modules to make predictions. This makes it easier than using custom code each time a prediction is to be made. 7. A new parameter called `stop_criteria` allows the user to specify one or more stop criteria to stop the evolution based on some conditions. Each criterion is passed as `str` which has a stop word. The current 2 supported words are `reach` and `saturate`. `reach` stops the `run()` method if the fitness value is equal to or greater than a given fitness value. An example for `reach` is `"reach_40"` which stops the evolution if the fitness is >= 40. `saturate` means stop the evolution if the fitness saturates for a given number of consecutive generations. An example for `saturate` is `"saturate_7"` which means stop the `run()` method if the fitness does not change for 7 consecutive generations. Thanks to [Rainer](https://github.com/rengel8) for asking about this feature: https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/44 8. A new bool parameter, defaults to `False`, named `save_solutions` is added to the constructor of the `pygad.GA` class. If `True`, then all solutions in each generation are appended into an attribute called `solutions` which is NumPy array. @@ -275,7 +275,7 @@ Release Date: 17 June 2021 11. The default value of the `title` parameter in the `plot_fitness()` method is `"PyGAD - Generation vs. Fitness"` rather than `"PyGAD - Iteration vs. Fitness"`. 12. A new method named `plot_new_solution_rate()` creates, shows, and returns a figure showing the rate of new/unique solutions explored in each generation. It accepts the same parameters as in the `plot_fitness()` method. This method only works when `save_solutions=True` in the `pygad.GA` class's constructor. 13. A new method named `plot_genes()` creates, shows, and returns a figure to show how each gene changes per each generation. It accepts similar parameters like the `plot_fitness()` method in addition to the `graph_type`, `fill_color`, and `solutions` parameters. The `graph_type` parameter can be either `"plot"` (default), `"boxplot"`, or `"histogram"`. `fill_color` accepts the fill color which works when `graph_type` is either `"boxplot"` or `"histogram"`. `solutions` can be either `"all"` or `"best"` to decide whether all solutions or only best solutions are used. -14. The `gene_type` parameter now supports controlling the precision of `float` data types. For a gene, rather than assigning just the data type like `float`, assign a `list`/`tuple`/`numpy.ndarray` with 2 elements where the first one is the type and the second one is the precision. For example, `[float, 2]` forces a gene with a value like `0.1234` to be `0.12`. For more information, check the [More about the `gene_type` Parameter](https://pygad.readthedocs.io/en/latest/pygad_more.html#more-about-the-gene-type-parameter) section. +14. The `gene_type` parameter now supports controlling the precision of `float` data types. For a gene, rather than assigning just the data type like `float`, assign a `list`/`tuple`/`numpy.ndarray` with 2 elements where the first one is the type and the second one is the precision. For example, `[float, 2]` forces a gene with a value like `0.1234` to be `0.12`. For more information, check the [More about the `gene_type` Parameter](https://pygad.readthedocs.io/en/latest/gene_values.html#more-about-the-gene-type-parameter) section. ## PyGAD 2.15.1 @@ -393,19 +393,19 @@ Release Date: 8 July 2022 2. Fixed the issue where the `allow_duplicate_genes` parameter did not work when mutation is disabled (i.e. `mutation_type=None`). This is by checking for duplicates after crossover directly. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/39 3. Solve an issue in the `tournament_selection()` method as the indices of the selected parents were incorrect. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/89 4. Reuse the fitness values of the previously explored solutions rather than recalculating them. This feature only works if `save_solutions=True`. -4. Parallel processing is supported. This is by the introduction of a new parameter named `parallel_processing` in the constructor of the `pygad.GA` class. Thanks to [@windowshopr](https://github.com/windowshopr) for opening the issue [#78](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/78) at GitHub. Check the [Parallel Processing in PyGAD](https://pygad.readthedocs.io/en/latest/pygad_more.html#parallel-processing-in-pygad) section for more information and examples. +4. Parallel processing is supported. This is by the introduction of a new parameter named `parallel_processing` in the constructor of the `pygad.GA` class. Thanks to [@windowshopr](https://github.com/windowshopr) for opening the issue [#78](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/78) at GitHub. Check the [Parallel Processing in PyGAD](https://pygad.readthedocs.io/en/latest/fitness_calculation.html#parallel-processing-in-pygad) section for more information and examples. ## PyGAD 2.18.0 Release Date: 9 September 2022 1. Raise an exception if the sum of fitness values is zero while either roulette wheel or stochastic universal parent selection is used. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/129 2. Initialize the value of the `run_completed` property to `False`. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/122 -3. The values of these properties are no longer reset with each call to the `run()` method `self.best_solutions, self.best_solutions_fitness, self.solutions, self.solutions_fitness`: https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/123. Now, the user can have the flexibility of calling the `run()` method more than once while extending the data collected after each generation. Another advantage happens when the instance is loaded and the `run()` method is called, as the old fitness value are shown on the graph alongside with the new fitness values. Read more in this section: [Continue without Losing Progress](https://pygad.readthedocs.io/en/latest/pygad_more.html#continue-without-losing-progress) +3. The values of these properties are no longer reset with each call to the `run()` method `self.best_solutions, self.best_solutions_fitness, self.solutions, self.solutions_fitness`: https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/123. Now, the user can have the flexibility of calling the `run()` method more than once while extending the data collected after each generation. Another advantage happens when the instance is loaded and the `run()` method is called, as the old fitness value are shown on the graph alongside with the new fitness values. Read more in this section: [Continue without Losing Progress](https://pygad.readthedocs.io/en/latest/generations.html#continue-without-losing-progress) 4. Thanks [Prof. Fernando Jiménez Barrionuevo](http://webs.um.es/fernan) (Dept. of Information and Communications Engineering, University of Murcia, Murcia, Spain) for editing this [comment](https://github.com/ahmedfgad/GeneticAlgorithmPython/blob/5315bbec02777df96ce1ec665c94dece81c440f4/pygad.py#L73) in the code. https://github.com/ahmedfgad/GeneticAlgorithmPython/commit/5315bbec02777df96ce1ec665c94dece81c440f4 5. A bug fixed when `crossover_type=None`. -6. Support of elitism selection through a new parameter named `keep_elitism`. It defaults to 1 which means for each generation keep only the best solution in the next generation. If assigned 0, then it has no effect. Read more in this section: [Elitism Selection](https://pygad.readthedocs.io/en/latest/pygad_more.html#elitism-selection). https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/74 +6. Support of elitism selection through a new parameter named `keep_elitism`. It defaults to 1 which means for each generation keep only the best solution in the next generation. If assigned 0, then it has no effect. Read more in this section: [Elitism Selection](https://pygad.readthedocs.io/en/latest/generations.html#elitism-selection). https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/74 7. A new instance attribute named `last_generation_elitism` added to hold the elitism in the last generation. -8. A new parameter called `random_seed` added to accept a seed for the random function generators. Credit to this issue https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/70 and [Prof. Fernando Jiménez Barrionuevo](http://webs.um.es/fernan). Read more in this section: [Random Seed](https://pygad.readthedocs.io/en/latest/pygad_more.html#random-seed). +8. A new parameter called `random_seed` added to accept a seed for the random function generators. Credit to this issue https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/70 and [Prof. Fernando Jiménez Barrionuevo](http://webs.um.es/fernan). Read more in this section: [Random Seed](https://pygad.readthedocs.io/en/latest/generations.html#random-seed). 9. Editing the `pygad.TorchGA` module to make sure the tensor data is moved from GPU to CPU. Thanks to Rasmus Johansson for opening this pull request: https://github.com/ahmedfgad/TorchGA/pull/2 ## PyGAD 2.18.1 @@ -468,10 +468,10 @@ Release Date 8 April 2023 4. The `pygad.utils.mutation` module has a class named `Mutation` where all the mutation operators exist. The `pygad.GA` class extends this class. 5. The `pygad.helper.unique` module has a class named `Unique` some helper methods exist to solve duplicate genes and make sure every gene is unique. The `pygad.GA` class extends this class. 6. The `pygad.visualize.plot` module has a class named `Plot` where all the methods that create plots exist. The `pygad.GA` class extends this class. - 7. Support of using the `logging` module to log the outputs to both the console and text file instead of using the `print()` function. This is by assigning the `logging.Logger` to the new `logger` parameter. Check the [Logging Outputs](https://pygad.readthedocs.io/en/latest/pygad_more.html#logging-outputs) for more information. + 7. Support of using the `logging` module to log the outputs to both the console and text file instead of using the `print()` function. This is by assigning the `logging.Logger` to the new `logger` parameter. Check the [Logging Outputs](https://pygad.readthedocs.io/en/latest/logging.html#logging-outputs) for more information. 8. A new instance attribute called `logger` to save the logger. - 9. The function/method passed to the `fitness_func` parameter accepts a new parameter that refers to the instance of the `pygad.GA` class. Check this for an example: [Use Functions and Methods to Build Fitness Function and Callbacks](https://pygad.readthedocs.io/en/latest/pygad_more.html#use-functions-and-methods-to-build-fitness-and-callbacks). https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/163 - 10. Update the documentation to include an example of using functions and methods to calculate the fitness and build callbacks. Check this for more details: [Use Functions and Methods to Build Fitness Function and Callbacks](https://pygad.readthedocs.io/en/latest/pygad_more.html#use-functions-and-methods-to-build-fitness-and-callbacks). https://github.com/ahmedfgad/GeneticAlgorithmPython/pull/92#issuecomment-1443635003 + 9. The function/method passed to the `fitness_func` parameter accepts a new parameter that refers to the instance of the `pygad.GA` class. Check this for an example: [Use Functions and Methods to Build Fitness Function and Callbacks](https://pygad.readthedocs.io/en/latest/custom_functions.html#use-functions-and-methods-to-build-fitness-and-callbacks). https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/163 + 10. Update the documentation to include an example of using functions and methods to calculate the fitness and build callbacks. Check this for more details: [Use Functions and Methods to Build Fitness Function and Callbacks](https://pygad.readthedocs.io/en/latest/custom_functions.html#use-functions-and-methods-to-build-fitness-and-callbacks). https://github.com/ahmedfgad/GeneticAlgorithmPython/pull/92#issuecomment-1443635003 11. Validate the value passed to the `initial_population` parameter. 12. Validate the type and length of the `pop_fitness` parameter of the `best_solution()` method. 13. Some edits in the documentation. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/106 @@ -503,20 +503,20 @@ Release Date 20 June 2023 10. Except for the `pygad.nn` module, the `print()` function in all other modules are replaced by the `logging` module to log messages. 11. The callback functions/methods `on_fitness()`, `on_parents()`, `on_crossover()`, and `on_mutation()` can return values. These returned values override the corresponding properties. The output of `on_fitness()` overrides the population fitness. The `on_parents()` function/method must return 2 values representing the parents and their indices. The output of `on_crossover()` overrides the crossover offspring. The output of `on_mutation()` overrides the mutation offspring. 12. Fix a bug when adaptive mutation is used while `fitness_batch_size`>1. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/195 -13. When `allow_duplicate_genes=False` and a user-defined `gene_space` is used, it sometimes happen that there is no room to solve the duplicates between the 2 genes by simply replacing the value of one gene by another gene. This release tries to solve such duplicates by looking for a third gene that will help in solving the duplicates. Check [this section](https://pygad.readthedocs.io/en/latest/pygad_more.html#prevent-duplicates-in-gene-values) for more information. +13. When `allow_duplicate_genes=False` and a user-defined `gene_space` is used, it sometimes happen that there is no room to solve the duplicates between the 2 genes by simply replacing the value of one gene by another gene. This release tries to solve such duplicates by looking for a third gene that will help in solving the duplicates. Check [this section](https://pygad.readthedocs.io/en/latest/gene_values.html#prevent-duplicates-in-gene-values) for more information. 14. Use probabilities to select parents using the rank parent selection method. https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/205 15. The 2 parameters `random_mutation_min_val` and `random_mutation_max_val` can accept iterables (list/tuple/numpy.ndarray) with length equal to the number of genes. This enables customizing the mutation range for each individual gene. https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/198 16. The 2 parameters `init_range_low` and `init_range_high` can accept iterables (list/tuple/numpy.ndarray) with length equal to the number of genes. This enables customizing the initial range for each individual gene when creating the initial population. 17. The `data` parameter in the `predict()` function of the `pygad.kerasga` module can be assigned a data generator. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/115 https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/207 18. The `predict()` function of the `pygad.kerasga` module accepts 3 optional parameters: 1) `batch_size=None`, `verbose=0`, and `steps=None`. Check documentation of the [Keras Model.predict()](https://keras.io/api/models/model_training_apis) method for more information. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/207 -19. The documentation is updated to explain how mutation works when `gene_space` is used with `int` or `float` data types. Check [this section](https://pygad.readthedocs.io/en/latest/pygad_more.html#limit-the-gene-value-range-using-the-gene-space-parameter). https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/198 +19. The documentation is updated to explain how mutation works when `gene_space` is used with `int` or `float` data types. Check [this section](https://pygad.readthedocs.io/en/latest/gene_values.html#limit-the-gene-value-range-using-the-gene-space-parameter). https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/198 ## PyGAD 3.2.0 Release Date 7 September 2023 -1. A new module `pygad.utils.nsga2` is created that has the `NSGA2` class that includes the functionalities of NSGA-II. The class has these methods: 1) `get_non_dominated_set()` 2) `non_dominated_sorting()` 3) `crowding_distance()` 4) `sort_solutions_nsga2()`. Check [this section](https://pygad.readthedocs.io/en/latest/pygad_more.html#multi-objective-optimization) for an example. -2. Support of multi-objective optimization using Non-Dominated Sorting Genetic Algorithm II (NSGA-II) using the `NSGA2` class in the `pygad.utils.nsga2` module. Just return a `list`, `tuple`, or `numpy.ndarray` from the fitness function and the library will consider the problem as multi-objective optimization. All the objectives are expected to be maximization. Check [this section](https://pygad.readthedocs.io/en/latest/pygad_more.html#multi-objective-optimization) for an example. +1. A new module `pygad.utils.nsga2` is created that has the `NSGA2` class that includes the functionalities of NSGA-II. The class has these methods: 1) `get_non_dominated_set()` 2) `non_dominated_sorting()` 3) `crowding_distance()` 4) `sort_solutions_nsga2()`. Check [this section](https://pygad.readthedocs.io/en/latest/multi_objective.html#multi-objective-optimization) for an example. +2. Support of multi-objective optimization using Non-Dominated Sorting Genetic Algorithm II (NSGA-II) using the `NSGA2` class in the `pygad.utils.nsga2` module. Just return a `list`, `tuple`, or `numpy.ndarray` from the fitness function and the library will consider the problem as multi-objective optimization. All the objectives are expected to be maximization. Check [this section](https://pygad.readthedocs.io/en/latest/multi_objective.html#multi-objective-optimization) for an example. 3. The parent selection methods and adaptive mutation are edited to support multi-objective optimization. 4. Two new NSGA-II parent selection methods are supported in the `pygad.utils.parent_selection` module: 1) Tournament selection for NSGA-II 2) NSGA-II selection. 5. The `plot_fitness()` method in the `pygad.plot` module has a new optional parameter named `label` to accept the label of the plots. This is only used for multi-objective problems. Otherwise, it is ignored. It defaults to `None` and accepts a `list`, `tuple`, or `numpy.ndarray`. The labels are used in a legend inside the plot. @@ -535,8 +535,8 @@ Release Date 29 January 2024 2. When the `stop_ciiteria` parameter is used with the `reach` keyword, then multiple numeric values can be passed when solving a multi-objective problem. For example, if a problem has 3 objective functions, then `stop_criteria="reach_10_20_30"` means the GA stops if the fitness of the 3 objectives are at least 10, 20, and 30, respectively. The number values must match the number of objective functions. If a single value found (e.g. `stop_criteria=reach_5`) when solving a multi-objective problem, then it is used across all the objectives. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/238 3. The `delay_after_gen` parameter is now deprecated and will be removed in a future release. If it is necessary to have a time delay after each generation, then assign a callback function/method to the `on_generation` parameter to pause the evolution. 4. Parallel processing now supports calculating the fitness during adaptive mutation. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/201 -5. The population size can be changed during runtime by changing all the parameters that would affect the size of any thing used by the GA. For more information, check the [Change Population Size during Runtime](https://pygad.readthedocs.io/en/latest/pygad_more.html#change-population-size-during-runtime) section. https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/234 -6. When a dictionary exists in the `gene_space` parameter without a step, then mutation occurs by adding a random value to the gene value. The random vaue is generated based on the 2 parameters `random_mutation_min_val` and `random_mutation_max_val`. For more information, check the [How Mutation Works with the gene_space Parameter?](https://pygad.readthedocs.io/en/latest/pygad_more.html#how-mutation-works-with-the-gene-space-parameter) section. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/229 +5. The population size can be changed during runtime by changing all the parameters that would affect the size of any thing used by the GA. For more information, check the [Change Population Size during Runtime](https://pygad.readthedocs.io/en/latest/generations.html#change-population-size-during-runtime) section. https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/234 +6. When a dictionary exists in the `gene_space` parameter without a step, then mutation occurs by adding a random value to the gene value. The random vaue is generated based on the 2 parameters `random_mutation_min_val` and `random_mutation_max_val`. For more information, check the [How Mutation Works with the gene_space Parameter?](https://pygad.readthedocs.io/en/latest/gene_values.html#how-mutation-works-with-the-gene-space-parameter) section. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/229 7. Add `object` as a supported data type for int (GA.supported_int_types) and float (GA.supported_float_types). https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/174 8. Use the `raise` clause instead of the `sys.exit(-1)` to terminate the execution. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/213 9. Fix a bug when multi-objective optimization is used with batch fitness calculation (e.g. `fitness_batch_size` set to a non-zero number). @@ -604,8 +604,8 @@ Release Date 08 July 2025 11. `filter_gene_values_by_constraint()`: Receives a list of values for a gene. Then it filters such values using the gene constraint. 12. `get_valid_gene_constraint_values()`: Selects one valid gene value that satisfy the gene constraint. It simply calls `generate_gene_value()` to generate some gene values then it filters such values using `filter_gene_values_by_constraint()`. 9. Create a new helper method called `mutation_process_random_value()` inside the `pygad/utils/mutation.py` script that generates constrained random values for mutation. It calls either `generate_gene_value()` or `get_valid_gene_constraint_values()` based on whether the `gene_constraint` parameter is used or not. -10. A new parameter called `gene_constraint` is added. It accepts a list of callables (i.e. functions) acting as constraints for the gene values. Before selecting a value for a gene, the callable is called to ensure the candidate value is valid. Check the [Gene Constraint](https://pygad.readthedocs.io/en/latest/pygad_more.html#gene-constraint) section for more information. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/119 -11. A new parameter called `sample_size` is added. To select a gene value that respects a constraint, this variable defines the size of the sample from which a value is selected randomly. Useful if either `allow_duplicate_genes` or `gene_constraint` is used. An instance attribute of the same name is created in the instances of the `pygad.GA` class. Check the [sample_size Parameter](https://pygad.readthedocs.io/en/latest/pygad_more.html#sample-size-parameter) section for more information. +10. A new parameter called `gene_constraint` is added. It accepts a list of callables (i.e. functions) acting as constraints for the gene values. Before selecting a value for a gene, the callable is called to ensure the candidate value is valid. Check the [Gene Constraint](https://pygad.readthedocs.io/en/latest/gene_values.html#gene-constraint) section for more information. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/119 +11. A new parameter called `sample_size` is added. To select a gene value that respects a constraint, this variable defines the size of the sample from which a value is selected randomly. Useful if either `allow_duplicate_genes` or `gene_constraint` is used. An instance attribute of the same name is created in the instances of the `pygad.GA` class. Check the [sample_size Parameter](https://pygad.readthedocs.io/en/latest/gene_values.html#sample-size-parameter) section for more information. 12. Use the `sample_size` parameter instead of `num_trials` in the methods `solve_duplicate_genes_randomly()` and `unique_float_gene_from_range()` inside the `pygad/helper/unique.py` script. It is the maximum number of values to generate as the search space when looking for a unique float value out of a range. 13. Fixed a bug in population initialization when `allow_duplicate_genes=False`. Previously, gene values were checked for duplicates before rounding, which could allow near-duplicates like 7.61 and 7.62 to pass. After rounding (e.g., both becoming 7.6), this resulted in unintended duplicates. The fix ensures gene values are now rounded before duplicate checks, preventing such cases. 14. More tests are created. From f5477e3c4cfe586b30f375ce3a3ba8cc49ed9356 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 21 May 2026 15:51:33 -0400 Subject: [PATCH 34/42] docs: split the long ML tutorial pages into one page per example For nn, gann, kerasga, and torchga, keep the module reference (intro, classes, functions, and steps) on the main page and move each worked example to its own page, linked from an Examples card grid and a nested toctree. cnn and gacnn (single example each) are unchanged. No internal links break: example sections are not referenced, and cnn.html#reading-the-data stays on the cnn page. --- docs/source/gann.md | 617 +---------------- docs/source/gann_image_classification.md | 146 +++++ docs/source/gann_regression_1.md | 153 +++++ docs/source/gann_regression_2.md | 148 +++++ docs/source/gann_xor.md | 134 ++++ docs/source/kerasga.md | 799 ++--------------------- docs/source/kerasga_image_conv.md | 154 +++++ docs/source/kerasga_image_datagen.md | 90 +++ docs/source/kerasga_image_dense.md | 125 ++++ docs/source/kerasga_regression.md | 239 +++++++ docs/source/kerasga_xor.md | 145 ++++ docs/source/nn.md | 282 +------- docs/source/nn_image_classification.md | 52 ++ docs/source/nn_regression_1.md | 70 ++ docs/source/nn_regression_2.md | 73 +++ docs/source/nn_xor.md | 50 ++ docs/source/torchga.md | 713 +------------------- docs/source/torchga_image_conv.md | 165 +++++ docs/source/torchga_image_dense.md | 126 ++++ docs/source/torchga_regression.md | 233 +++++++ docs/source/torchga_xor.md | 152 +++++ 21 files changed, 2393 insertions(+), 2273 deletions(-) create mode 100644 docs/source/gann_image_classification.md create mode 100644 docs/source/gann_regression_1.md create mode 100644 docs/source/gann_regression_2.md create mode 100644 docs/source/gann_xor.md create mode 100644 docs/source/kerasga_image_conv.md create mode 100644 docs/source/kerasga_image_datagen.md create mode 100644 docs/source/kerasga_image_dense.md create mode 100644 docs/source/kerasga_regression.md create mode 100644 docs/source/kerasga_xor.md create mode 100644 docs/source/nn_image_classification.md create mode 100644 docs/source/nn_regression_1.md create mode 100644 docs/source/nn_regression_2.md create mode 100644 docs/source/nn_xor.md create mode 100644 docs/source/torchga_image_conv.md create mode 100644 docs/source/torchga_image_dense.md create mode 100644 docs/source/torchga_regression.md create mode 100644 docs/source/torchga_xor.md diff --git a/docs/source/gann.md b/docs/source/gann.md index 681375a..547523d 100644 --- a/docs/source/gann.md +++ b/docs/source/gann.md @@ -384,587 +384,36 @@ Classification accuracy : 100.0. This section gives the complete code of some examples that build and train neural networks using the genetic algorithm. Each subsection builds a different network. -### XOR Classification - -This example is discussed in the **Steps to Build and Train Neural Networks using Genetic Algorithm** section that builds the XOR gate and its complete code is listed below. - -```python -import numpy -import pygad -import pygad.nn -import pygad.gann - -def fitness_func(ga_instance, solution, sol_idx): - global GANN_instance, data_inputs, data_outputs - - # If adaptive mutation is used, sometimes sol_idx is None. - if sol_idx == None: - sol_idx = 1 - - predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[sol_idx], - data_inputs=data_inputs) - correct_predictions = numpy.where(predictions == data_outputs)[0].size - solution_fitness = (correct_predictions/data_outputs.size)*100 - - return solution_fitness - -def callback_generation(ga_instance): - global GANN_instance, last_fitness - - population_matrices = pygad.gann.population_as_matrices(population_networks=GANN_instance.population_networks, - population_vectors=ga_instance.population) - - GANN_instance.update_population_trained_weights(population_trained_weights=population_matrices) - - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution()[1]}") - print(f"Change = {ga_instance.best_solution()[1] - last_fitness}") - - last_fitness = ga_instance.best_solution()[1].copy() - -# Holds the fitness value of the previous generation. -last_fitness = 0 - -# Preparing the NumPy array of the inputs. -data_inputs = numpy.array([[1, 1], - [1, 0], - [0, 1], - [0, 0]]) - -# Preparing the NumPy array of the outputs. -data_outputs = numpy.array([0, - 1, - 1, - 0]) - -# The length of the input vector for each sample (i.e. number of neurons in the input layer). -num_inputs = data_inputs.shape[1] -# The number of neurons in the output layer (i.e. number of classes). -num_classes = 2 - -# Creating an initial population of neural networks. The return of the initial_population() function holds references to the networks, not their weights. Using such references, the weights of all networks can be fetched. -num_solutions = 6 # A solution or a network can be used interchangeably. -GANN_instance = pygad.gann.GANN(num_solutions=num_solutions, - num_neurons_input=num_inputs, - num_neurons_hidden_layers=[2], - num_neurons_output=num_classes, - hidden_activations=["relu"], - output_activation="softmax") - -# population does not hold the numerical weights of the network instead it holds a list of references to each last layer of each network (i.e. solution) in the population. A solution or a network can be used interchangeably. -# If there is a population with 3 solutions (i.e. networks), then the population is a list with 3 elements. Each element is a reference to the last layer of each network. Using such a reference, all details of the network can be accessed. -population_vectors = pygad.gann.population_as_vectors(population_networks=GANN_instance.population_networks) - -# To prepare the initial population, there are 2 ways: -# 1) Prepare it yourself and pass it to the initial_population parameter. This way is useful when the user wants to start the genetic algorithm with a custom initial population. -# 2) Assign valid integer values to the sol_per_pop and num_genes parameters. If the initial_population parameter exists, then the sol_per_pop and num_genes parameters are useless. -initial_population = population_vectors.copy() - -num_parents_mating = 4 # Number of solutions to be selected as parents in the mating pool. - -num_generations = 500 # Number of generations. - -mutation_percent_genes = [5, 10] # Percentage of genes to mutate. This parameter has no action if the parameter mutation_num_genes exists. - -parent_selection_type = "sss" # Type of parent selection. - -crossover_type = "single_point" # Type of the crossover operator. - -mutation_type = "adaptive" # Type of the mutation operator. - -keep_parents = 1 # Number of parents to keep in the next population. -1 means keep all parents and 0 means keep nothing. - -init_range_low = -2 -init_range_high = 5 - -ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - mutation_percent_genes=mutation_percent_genes, - init_range_low=init_range_low, - init_range_high=init_range_high, - parent_selection_type=parent_selection_type, - crossover_type=crossover_type, - mutation_type=mutation_type, - keep_parents=keep_parents, - suppress_warnings=True, - on_generation=callback_generation) - -ga_instance.run() - -# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. -ga_instance.plot_fitness() - -# Returning the details of the best solution. -solution, solution_fitness, solution_idx = ga_instance.best_solution() -print(f"Parameters of the best solution : {solution}") -print(f"Fitness value of the best solution = {solution_fitness}") -print(f"Index of the best solution : {solution_idx}") - -if ga_instance.best_solution_generation != -1: - print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") - -# Predicting the outputs of the data using the best solution. -predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[solution_idx], - data_inputs=data_inputs) -print(f"Predictions of the trained network : {predictions}") - -# Calculating some statistics -num_wrong = numpy.where(predictions != data_outputs)[0] -num_correct = data_outputs.size - num_wrong.size -accuracy = 100 * (num_correct/data_outputs.size) -print(f"Number of correct classifications : {num_correct}.") -print(f"Number of wrong classifications : {num_wrong.size}.") -print(f"Classification accuracy : {accuracy}.") -``` - -### Image Classification - -In the documentation of the `pygad.nn` module, a neural network is created for classifying images from the Fruits360 dataset without being trained using an optimization algorithm. This section discusses how to train such a classifier using the genetic algorithm with the help of the `pygad.gann` module. - -Please make sure that the training data files [dataset_features.npy](https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy) and [outputs.npy](https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy) are available. For downloading them, use these links: - -1. [dataset_features.npy](https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy): The features https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy -2. [outputs.npy](https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy): The class labels https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy - -After the data is available, here is the complete code that builds and trains a neural network using the genetic algorithm for classifying images from 4 classes of the Fruits360 dataset. - -Because there are 4 classes, the output layer is assigned has 4 neurons according to the `num_neurons_output` parameter of the `pygad.gann.GANN` class constructor. - -```python -import numpy -import pygad -import pygad.nn -import pygad.gann - -def fitness_func(ga_instance, solution, sol_idx): - global GANN_instance, data_inputs, data_outputs - - predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[sol_idx], - data_inputs=data_inputs) - correct_predictions = numpy.where(predictions == data_outputs)[0].size - solution_fitness = (correct_predictions/data_outputs.size)*100 - - return solution_fitness - -def callback_generation(ga_instance): - global GANN_instance, last_fitness - - population_matrices = pygad.gann.population_as_matrices(population_networks=GANN_instance.population_networks, - population_vectors=ga_instance.population) - - GANN_instance.update_population_trained_weights(population_trained_weights=population_matrices) - - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution()[1]}") - print(f"Change = {ga_instance.best_solution()[1] - last_fitness}") - - last_fitness = ga_instance.best_solution()[1].copy() - -# Holds the fitness value of the previous generation. -last_fitness = 0 - -# Reading the input data. -data_inputs = numpy.load("dataset_features.npy") # Download from https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy - -# Optional step of filtering the input data using the standard deviation. -features_STDs = numpy.std(a=data_inputs, axis=0) -data_inputs = data_inputs[:, features_STDs>50] - -# Reading the output data. -data_outputs = numpy.load("outputs.npy") # Download from https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy - -# The length of the input vector for each sample (i.e. number of neurons in the input layer). -num_inputs = data_inputs.shape[1] -# The number of neurons in the output layer (i.e. number of classes). -num_classes = 4 - -# Creating an initial population of neural networks. The return of the initial_population() function holds references to the networks, not their weights. Using such references, the weights of all networks can be fetched. -num_solutions = 8 # A solution or a network can be used interchangeably. -GANN_instance = pygad.gann.GANN(num_solutions=num_solutions, - num_neurons_input=num_inputs, - num_neurons_hidden_layers=[150, 50], - num_neurons_output=num_classes, - hidden_activations=["relu", "relu"], - output_activation="softmax") - -# population does not hold the numerical weights of the network instead it holds a list of references to each last layer of each network (i.e. solution) in the population. A solution or a network can be used interchangeably. -# If there is a population with 3 solutions (i.e. networks), then the population is a list with 3 elements. Each element is a reference to the last layer of each network. Using such a reference, all details of the network can be accessed. -population_vectors = pygad.gann.population_as_vectors(population_networks=GANN_instance.population_networks) - -# To prepare the initial population, there are 2 ways: -# 1) Prepare it yourself and pass it to the initial_population parameter. This way is useful when the user wants to start the genetic algorithm with a custom initial population. -# 2) Assign valid integer values to the sol_per_pop and num_genes parameters. If the initial_population parameter exists, then the sol_per_pop and num_genes parameters are useless. -initial_population = population_vectors.copy() - -num_parents_mating = 4 # Number of solutions to be selected as parents in the mating pool. - -num_generations = 500 # Number of generations. - -mutation_percent_genes = 10 # Percentage of genes to mutate. This parameter has no action if the parameter mutation_num_genes exists. - -parent_selection_type = "sss" # Type of parent selection. - -crossover_type = "single_point" # Type of the crossover operator. - -mutation_type = "random" # Type of the mutation operator. - -keep_parents = -1 # Number of parents to keep in the next population. -1 means keep all parents and 0 means keep nothing. - -ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - mutation_percent_genes=mutation_percent_genes, - parent_selection_type=parent_selection_type, - crossover_type=crossover_type, - mutation_type=mutation_type, - keep_parents=keep_parents, - on_generation=callback_generation) - -ga_instance.run() - -# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. -ga_instance.plot_fitness() - -# Returning the details of the best solution. -solution, solution_fitness, solution_idx = ga_instance.best_solution() -print(f"Parameters of the best solution : {solution}") -print(f"Fitness value of the best solution = {solution_fitness}") -print(f"Index of the best solution : {solution_idx}") - -if ga_instance.best_solution_generation != -1: - print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") - -# Predicting the outputs of the data using the best solution. -predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[solution_idx], - data_inputs=data_inputs) -print(f"Predictions of the trained network : {predictions}") - -# Calculating some statistics -num_wrong = numpy.where(predictions != data_outputs)[0] -num_correct = data_outputs.size - num_wrong.size -accuracy = 100 * (num_correct/data_outputs.size) -print(f"Number of correct classifications : {num_correct}.") -print(f"Number of wrong classifications : {num_wrong.size}.") -print(f"Classification accuracy : {accuracy}.") -``` - -After training completes, here are the outputs of the print statements. The number of wrong classifications is only 1 and the accuracy is 99.949%. This accuracy is reached after 482 generations. - -``` -Fitness value of the best solution = 99.94903160040775 -Index of the best solution : 0 -Best fitness value reached after 482 generations. -Number of correct classifications : 1961. -Number of wrong classifications : 1. -Classification accuracy : 99.94903160040775. -``` - -The next figure shows how fitness value evolves by generation. - -![Training Neural Networks using Genetic Algorithm](https://user-images.githubusercontent.com/16560492/82152993-21898180-9865-11ea-8387-b995f88b83f7.png) - -### Regression Example 1 - -To train a neural network for regression, follow these instructions: - -1. Set the `output_activation` parameter in the constructor of the `pygad.gann.GANN` class to `"None"`. It is possible to use the ReLU function if all outputs are nonnegative. - -```python -GANN_instance = pygad.gann.GANN(... - output_activation="None") -``` - -2. Wherever the `pygad.nn.predict()` function is used, set the `problem_type` parameter to `"regression"`. - -```python -predictions = pygad.nn.predict(..., - problem_type="regression") -``` - -3. Design the fitness function to calculate the error (e.g. mean absolute error). - -```python -def fitness_func(ga_instance, solution, sol_idx): - ... - - predictions = pygad.nn.predict(..., - problem_type="regression") - - solution_fitness = 1.0/numpy.mean(numpy.abs(predictions - data_outputs)) - - return solution_fitness -``` - -The next code builds a complete example for building a neural network for regression. - -```python -import numpy -import pygad -import pygad.nn -import pygad.gann - -def fitness_func(ga_instance, solution, sol_idx): - global GANN_instance, data_inputs, data_outputs - - predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[sol_idx], - data_inputs=data_inputs, problem_type="regression") - solution_fitness = 1.0/numpy.mean(numpy.abs(predictions - data_outputs)) - - return solution_fitness - -def callback_generation(ga_instance): - global GANN_instance, last_fitness - - population_matrices = pygad.gann.population_as_matrices(population_networks=GANN_instance.population_networks, - population_vectors=ga_instance.population) - - GANN_instance.update_population_trained_weights(population_trained_weights=population_matrices) - - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1]}") - print(f"Change = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1] - last_fitness}") - - last_fitness = ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1].copy() - -# Holds the fitness value of the previous generation. -last_fitness = 0 - -# Preparing the NumPy array of the inputs. -data_inputs = numpy.array([[2, 5, -3, 0.1], - [8, 15, 20, 13]]) - -# Preparing the NumPy array of the outputs. -data_outputs = numpy.array([[0.1, 0.2], - [1.8, 1.5]]) - -# The length of the input vector for each sample (i.e. number of neurons in the input layer). -num_inputs = data_inputs.shape[1] - -# Creating an initial population of neural networks. The return of the initial_population() function holds references to the networks, not their weights. Using such references, the weights of all networks can be fetched. -num_solutions = 6 # A solution or a network can be used interchangeably. -GANN_instance = pygad.gann.GANN(num_solutions=num_solutions, - num_neurons_input=num_inputs, - num_neurons_hidden_layers=[2], - num_neurons_output=2, - hidden_activations=["relu"], - output_activation="None") - -# population does not hold the numerical weights of the network instead it holds a list of references to each last layer of each network (i.e. solution) in the population. A solution or a network can be used interchangeably. -# If there is a population with 3 solutions (i.e. networks), then the population is a list with 3 elements. Each element is a reference to the last layer of each network. Using such a reference, all details of the network can be accessed. -population_vectors = pygad.gann.population_as_vectors(population_networks=GANN_instance.population_networks) - -# To prepare the initial population, there are 2 ways: -# 1) Prepare it yourself and pass it to the initial_population parameter. This way is useful when the user wants to start the genetic algorithm with a custom initial population. -# 2) Assign valid integer values to the sol_per_pop and num_genes parameters. If the initial_population parameter exists, then the sol_per_pop and num_genes parameters are useless. -initial_population = population_vectors.copy() - -num_parents_mating = 4 # Number of solutions to be selected as parents in the mating pool. - -num_generations = 500 # Number of generations. - -mutation_percent_genes = 5 # Percentage of genes to mutate. This parameter has no action if the parameter mutation_num_genes exists. - -parent_selection_type = "sss" # Type of parent selection. - -crossover_type = "single_point" # Type of the crossover operator. - -mutation_type = "random" # Type of the mutation operator. - -keep_parents = 1 # Number of parents to keep in the next population. -1 means keep all parents and 0 means keep nothing. - -init_range_low = -1 -init_range_high = 1 - -ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - mutation_percent_genes=mutation_percent_genes, - init_range_low=init_range_low, - init_range_high=init_range_high, - parent_selection_type=parent_selection_type, - crossover_type=crossover_type, - mutation_type=mutation_type, - keep_parents=keep_parents, - on_generation=callback_generation) - -ga_instance.run() - -# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. -ga_instance.plot_fitness() - -# Returning the details of the best solution. -solution, solution_fitness, solution_idx = ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness) -print(f"Parameters of the best solution : {solution}") -print(f"Fitness value of the best solution = {solution_fitness}") -print(f"Index of the best solution : {solution_idx}") - -if ga_instance.best_solution_generation != -1: - print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") - -# Predicting the outputs of the data using the best solution. -predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[solution_idx], - data_inputs=data_inputs, - problem_type="regression") -print(f"Predictions of the trained network : {predictions}") - -# Calculating some statistics -abs_error = numpy.mean(numpy.abs(predictions - data_outputs)) -print(f"Absolute error : {abs_error}.") -``` - -The next figure shows how the fitness value changes for the generations used. - -![example_regression](https://user-images.githubusercontent.com/16560492/92948154-3cf24b00-f459-11ea-94ea-952b66ab2145.png) - -### Regression Example 2 - Fish Weight Prediction - -This example uses the Fish Market Dataset available at Kaggle (https://www.kaggle.com/aungpyaeap/fish-market). Simply download the CSV dataset from [this link](https://www.kaggle.com/aungpyaeap/fish-market/download) (https://www.kaggle.com/aungpyaeap/fish-market/download). The dataset is also available at the [GitHub project of the pygad.gann module](https://github.com/ahmedfgad/NeuralGenetic): https://github.com/ahmedfgad/NeuralGenetic - -Using the Pandas library, the dataset is read using the `read_csv()` function. - -```python -data = numpy.array(pandas.read_csv("Fish.csv")) -``` - -The last 5 columns in the dataset are used as inputs and the **Weight** column is used as output. - -```python -# Preparing the NumPy array of the inputs. -data_inputs = numpy.asarray(data[:, 2:], dtype=numpy.float32) - -# Preparing the NumPy array of the outputs. -data_outputs = numpy.asarray(data[:, 1], dtype=numpy.float32) # Fish Weight -``` - -Note how the activation function at the last layer is set to `"None"`. Moreover, the `problem_type` parameter in the `pygad.nn.train()` and `pygad.nn.predict()` functions is set to `"regression"`. Remember to design an appropriate fitness function for the regression problem. In this example, the fitness value is calculated based on the mean absolute error. - -```python -solution_fitness = 1.0/numpy.mean(numpy.abs(predictions - data_outputs)) -``` - -Here is the complete code. - -```python -import numpy -import pygad -import pygad.nn -import pygad.gann -import pandas - -def fitness_func(ga_instance, solution, sol_idx): - global GANN_instance, data_inputs, data_outputs - - predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[sol_idx], - data_inputs=data_inputs, problem_type="regression") - solution_fitness = 1.0/numpy.mean(numpy.abs(predictions - data_outputs)) - - return solution_fitness - -def callback_generation(ga_instance): - global GANN_instance, last_fitness - - population_matrices = pygad.gann.population_as_matrices(population_networks=GANN_instance.population_networks, - population_vectors=ga_instance.population) - - GANN_instance.update_population_trained_weights(population_trained_weights=population_matrices) - - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1]}") - print(f"Change = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1] - last_fitness}") - - last_fitness = ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1].copy() - -# Holds the fitness value of the previous generation. -last_fitness = 0 - -data = numpy.array(pandas.read_csv("../data/Fish.csv")) - -# Preparing the NumPy array of the inputs. -data_inputs = numpy.asarray(data[:, 2:], dtype=numpy.float32) - -# Preparing the NumPy array of the outputs. -data_outputs = numpy.asarray(data[:, 1], dtype=numpy.float32) - -# The length of the input vector for each sample (i.e. number of neurons in the input layer). -num_inputs = data_inputs.shape[1] - -# Creating an initial population of neural networks. The return of the initial_population() function holds references to the networks, not their weights. Using such references, the weights of all networks can be fetched. -num_solutions = 6 # A solution or a network can be used interchangeably. -GANN_instance = pygad.gann.GANN(num_solutions=num_solutions, - num_neurons_input=num_inputs, - num_neurons_hidden_layers=[2], - num_neurons_output=1, - hidden_activations=["relu"], - output_activation="None") - -# population does not hold the numerical weights of the network instead it holds a list of references to each last layer of each network (i.e. solution) in the population. A solution or a network can be used interchangeably. -# If there is a population with 3 solutions (i.e. networks), then the population is a list with 3 elements. Each element is a reference to the last layer of each network. Using such a reference, all details of the network can be accessed. -population_vectors = pygad.gann.population_as_vectors(population_networks=GANN_instance.population_networks) - -# To prepare the initial population, there are 2 ways: -# 1) Prepare it yourself and pass it to the initial_population parameter. This way is useful when the user wants to start the genetic algorithm with a custom initial population. -# 2) Assign valid integer values to the sol_per_pop and num_genes parameters. If the initial_population parameter exists, then the sol_per_pop and num_genes parameters are useless. -initial_population = population_vectors.copy() - -num_parents_mating = 4 # Number of solutions to be selected as parents in the mating pool. - -num_generations = 500 # Number of generations. - -mutation_percent_genes = 5 # Percentage of genes to mutate. This parameter has no action if the parameter mutation_num_genes exists. - -parent_selection_type = "sss" # Type of parent selection. - -crossover_type = "single_point" # Type of the crossover operator. - -mutation_type = "random" # Type of the mutation operator. - -keep_parents = 1 # Number of parents to keep in the next population. -1 means keep all parents and 0 means keep nothing. - -init_range_low = -1 -init_range_high = 1 - -ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - mutation_percent_genes=mutation_percent_genes, - init_range_low=init_range_low, - init_range_high=init_range_high, - parent_selection_type=parent_selection_type, - crossover_type=crossover_type, - mutation_type=mutation_type, - keep_parents=keep_parents, - on_generation=callback_generation) - -ga_instance.run() - -# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. -ga_instance.plot_fitness() - -# Returning the details of the best solution. -solution, solution_fitness, solution_idx = ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness) -print(f"Parameters of the best solution : {solution}") -print(f"Fitness value of the best solution = {solution_fitness}") -print(f"Index of the best solution : {solution_idx}") - -if ga_instance.best_solution_generation != -1: - print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") - -# Predicting the outputs of the data using the best solution. -predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[solution_idx], - data_inputs=data_inputs, - problem_type="regression") -print(f"Predictions of the trained network : {predictions}") - -# Calculating some statistics -abs_error = numpy.mean(numpy.abs(predictions - data_outputs)) -print(f"Absolute error : {abs_error}.") -``` - -The next figure shows how the fitness value changes for the 500 generations used. - -![example_regression_fish](https://user-images.githubusercontent.com/16560492/92948486-bbe78380-f459-11ea-9e31-0d4c7269d606.png) \ No newline at end of file +::::{grid} 1 2 2 2 +:gutter: 3 + +:::{grid-item-card} XOR Classification +:link: gann_xor +:link-type: doc +::: + +:::{grid-item-card} Image Classification +:link: gann_image_classification +:link-type: doc +::: + +:::{grid-item-card} Regression Example 1 +:link: gann_regression_1 +:link-type: doc +::: + +:::{grid-item-card} Regression Example 2 - Fish Weight Prediction +:link: gann_regression_2 +:link-type: doc +::: + +:::: + +:::{toctree} +:hidden: + +gann_xor +gann_image_classification +gann_regression_1 +gann_regression_2 +::: diff --git a/docs/source/gann_image_classification.md b/docs/source/gann_image_classification.md new file mode 100644 index 0000000..4009677 --- /dev/null +++ b/docs/source/gann_image_classification.md @@ -0,0 +1,146 @@ +# Image Classification + +In the documentation of the `pygad.nn` module, a neural network is created for classifying images from the Fruits360 dataset without being trained using an optimization algorithm. This section discusses how to train such a classifier using the genetic algorithm with the help of the `pygad.gann` module. + +Please make sure that the training data files [dataset_features.npy](https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy) and [outputs.npy](https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy) are available. For downloading them, use these links: + +1. [dataset_features.npy](https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy): The features https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy +2. [outputs.npy](https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy): The class labels https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy + +After the data is available, here is the complete code that builds and trains a neural network using the genetic algorithm for classifying images from 4 classes of the Fruits360 dataset. + +Because there are 4 classes, the output layer is assigned has 4 neurons according to the `num_neurons_output` parameter of the `pygad.gann.GANN` class constructor. + +```python +import numpy +import pygad +import pygad.nn +import pygad.gann + +def fitness_func(ga_instance, solution, sol_idx): + global GANN_instance, data_inputs, data_outputs + + predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[sol_idx], + data_inputs=data_inputs) + correct_predictions = numpy.where(predictions == data_outputs)[0].size + solution_fitness = (correct_predictions/data_outputs.size)*100 + + return solution_fitness + +def callback_generation(ga_instance): + global GANN_instance, last_fitness + + population_matrices = pygad.gann.population_as_matrices(population_networks=GANN_instance.population_networks, + population_vectors=ga_instance.population) + + GANN_instance.update_population_trained_weights(population_trained_weights=population_matrices) + + print(f"Generation = {ga_instance.generations_completed}") + print(f"Fitness = {ga_instance.best_solution()[1]}") + print(f"Change = {ga_instance.best_solution()[1] - last_fitness}") + + last_fitness = ga_instance.best_solution()[1].copy() + +# Holds the fitness value of the previous generation. +last_fitness = 0 + +# Reading the input data. +data_inputs = numpy.load("dataset_features.npy") # Download from https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy + +# Optional step of filtering the input data using the standard deviation. +features_STDs = numpy.std(a=data_inputs, axis=0) +data_inputs = data_inputs[:, features_STDs>50] + +# Reading the output data. +data_outputs = numpy.load("outputs.npy") # Download from https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy + +# The length of the input vector for each sample (i.e. number of neurons in the input layer). +num_inputs = data_inputs.shape[1] +# The number of neurons in the output layer (i.e. number of classes). +num_classes = 4 + +# Creating an initial population of neural networks. The return of the initial_population() function holds references to the networks, not their weights. Using such references, the weights of all networks can be fetched. +num_solutions = 8 # A solution or a network can be used interchangeably. +GANN_instance = pygad.gann.GANN(num_solutions=num_solutions, + num_neurons_input=num_inputs, + num_neurons_hidden_layers=[150, 50], + num_neurons_output=num_classes, + hidden_activations=["relu", "relu"], + output_activation="softmax") + +# population does not hold the numerical weights of the network instead it holds a list of references to each last layer of each network (i.e. solution) in the population. A solution or a network can be used interchangeably. +# If there is a population with 3 solutions (i.e. networks), then the population is a list with 3 elements. Each element is a reference to the last layer of each network. Using such a reference, all details of the network can be accessed. +population_vectors = pygad.gann.population_as_vectors(population_networks=GANN_instance.population_networks) + +# To prepare the initial population, there are 2 ways: +# 1) Prepare it yourself and pass it to the initial_population parameter. This way is useful when the user wants to start the genetic algorithm with a custom initial population. +# 2) Assign valid integer values to the sol_per_pop and num_genes parameters. If the initial_population parameter exists, then the sol_per_pop and num_genes parameters are useless. +initial_population = population_vectors.copy() + +num_parents_mating = 4 # Number of solutions to be selected as parents in the mating pool. + +num_generations = 500 # Number of generations. + +mutation_percent_genes = 10 # Percentage of genes to mutate. This parameter has no action if the parameter mutation_num_genes exists. + +parent_selection_type = "sss" # Type of parent selection. + +crossover_type = "single_point" # Type of the crossover operator. + +mutation_type = "random" # Type of the mutation operator. + +keep_parents = -1 # Number of parents to keep in the next population. -1 means keep all parents and 0 means keep nothing. + +ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=num_parents_mating, + initial_population=initial_population, + fitness_func=fitness_func, + mutation_percent_genes=mutation_percent_genes, + parent_selection_type=parent_selection_type, + crossover_type=crossover_type, + mutation_type=mutation_type, + keep_parents=keep_parents, + on_generation=callback_generation) + +ga_instance.run() + +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. +ga_instance.plot_fitness() + +# Returning the details of the best solution. +solution, solution_fitness, solution_idx = ga_instance.best_solution() +print(f"Parameters of the best solution : {solution}") +print(f"Fitness value of the best solution = {solution_fitness}") +print(f"Index of the best solution : {solution_idx}") + +if ga_instance.best_solution_generation != -1: + print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") + +# Predicting the outputs of the data using the best solution. +predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[solution_idx], + data_inputs=data_inputs) +print(f"Predictions of the trained network : {predictions}") + +# Calculating some statistics +num_wrong = numpy.where(predictions != data_outputs)[0] +num_correct = data_outputs.size - num_wrong.size +accuracy = 100 * (num_correct/data_outputs.size) +print(f"Number of correct classifications : {num_correct}.") +print(f"Number of wrong classifications : {num_wrong.size}.") +print(f"Classification accuracy : {accuracy}.") +``` + +After training completes, here are the outputs of the print statements. The number of wrong classifications is only 1 and the accuracy is 99.949%. This accuracy is reached after 482 generations. + +``` +Fitness value of the best solution = 99.94903160040775 +Index of the best solution : 0 +Best fitness value reached after 482 generations. +Number of correct classifications : 1961. +Number of wrong classifications : 1. +Classification accuracy : 99.94903160040775. +``` + +The next figure shows how fitness value evolves by generation. + +![Training Neural Networks using Genetic Algorithm](https://user-images.githubusercontent.com/16560492/82152993-21898180-9865-11ea-8387-b995f88b83f7.png) diff --git a/docs/source/gann_regression_1.md b/docs/source/gann_regression_1.md new file mode 100644 index 0000000..d35dd24 --- /dev/null +++ b/docs/source/gann_regression_1.md @@ -0,0 +1,153 @@ +# Regression Example 1 + +To train a neural network for regression, follow these instructions: + +1. Set the `output_activation` parameter in the constructor of the `pygad.gann.GANN` class to `"None"`. It is possible to use the ReLU function if all outputs are nonnegative. + +```python +GANN_instance = pygad.gann.GANN(... + output_activation="None") +``` + +2. Wherever the `pygad.nn.predict()` function is used, set the `problem_type` parameter to `"regression"`. + +```python +predictions = pygad.nn.predict(..., + problem_type="regression") +``` + +3. Design the fitness function to calculate the error (e.g. mean absolute error). + +```python +def fitness_func(ga_instance, solution, sol_idx): + ... + + predictions = pygad.nn.predict(..., + problem_type="regression") + + solution_fitness = 1.0/numpy.mean(numpy.abs(predictions - data_outputs)) + + return solution_fitness +``` + +The next code builds a complete example for building a neural network for regression. + +```python +import numpy +import pygad +import pygad.nn +import pygad.gann + +def fitness_func(ga_instance, solution, sol_idx): + global GANN_instance, data_inputs, data_outputs + + predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[sol_idx], + data_inputs=data_inputs, problem_type="regression") + solution_fitness = 1.0/numpy.mean(numpy.abs(predictions - data_outputs)) + + return solution_fitness + +def callback_generation(ga_instance): + global GANN_instance, last_fitness + + population_matrices = pygad.gann.population_as_matrices(population_networks=GANN_instance.population_networks, + population_vectors=ga_instance.population) + + GANN_instance.update_population_trained_weights(population_trained_weights=population_matrices) + + print(f"Generation = {ga_instance.generations_completed}") + print(f"Fitness = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1]}") + print(f"Change = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1] - last_fitness}") + + last_fitness = ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1].copy() + +# Holds the fitness value of the previous generation. +last_fitness = 0 + +# Preparing the NumPy array of the inputs. +data_inputs = numpy.array([[2, 5, -3, 0.1], + [8, 15, 20, 13]]) + +# Preparing the NumPy array of the outputs. +data_outputs = numpy.array([[0.1, 0.2], + [1.8, 1.5]]) + +# The length of the input vector for each sample (i.e. number of neurons in the input layer). +num_inputs = data_inputs.shape[1] + +# Creating an initial population of neural networks. The return of the initial_population() function holds references to the networks, not their weights. Using such references, the weights of all networks can be fetched. +num_solutions = 6 # A solution or a network can be used interchangeably. +GANN_instance = pygad.gann.GANN(num_solutions=num_solutions, + num_neurons_input=num_inputs, + num_neurons_hidden_layers=[2], + num_neurons_output=2, + hidden_activations=["relu"], + output_activation="None") + +# population does not hold the numerical weights of the network instead it holds a list of references to each last layer of each network (i.e. solution) in the population. A solution or a network can be used interchangeably. +# If there is a population with 3 solutions (i.e. networks), then the population is a list with 3 elements. Each element is a reference to the last layer of each network. Using such a reference, all details of the network can be accessed. +population_vectors = pygad.gann.population_as_vectors(population_networks=GANN_instance.population_networks) + +# To prepare the initial population, there are 2 ways: +# 1) Prepare it yourself and pass it to the initial_population parameter. This way is useful when the user wants to start the genetic algorithm with a custom initial population. +# 2) Assign valid integer values to the sol_per_pop and num_genes parameters. If the initial_population parameter exists, then the sol_per_pop and num_genes parameters are useless. +initial_population = population_vectors.copy() + +num_parents_mating = 4 # Number of solutions to be selected as parents in the mating pool. + +num_generations = 500 # Number of generations. + +mutation_percent_genes = 5 # Percentage of genes to mutate. This parameter has no action if the parameter mutation_num_genes exists. + +parent_selection_type = "sss" # Type of parent selection. + +crossover_type = "single_point" # Type of the crossover operator. + +mutation_type = "random" # Type of the mutation operator. + +keep_parents = 1 # Number of parents to keep in the next population. -1 means keep all parents and 0 means keep nothing. + +init_range_low = -1 +init_range_high = 1 + +ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=num_parents_mating, + initial_population=initial_population, + fitness_func=fitness_func, + mutation_percent_genes=mutation_percent_genes, + init_range_low=init_range_low, + init_range_high=init_range_high, + parent_selection_type=parent_selection_type, + crossover_type=crossover_type, + mutation_type=mutation_type, + keep_parents=keep_parents, + on_generation=callback_generation) + +ga_instance.run() + +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. +ga_instance.plot_fitness() + +# Returning the details of the best solution. +solution, solution_fitness, solution_idx = ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness) +print(f"Parameters of the best solution : {solution}") +print(f"Fitness value of the best solution = {solution_fitness}") +print(f"Index of the best solution : {solution_idx}") + +if ga_instance.best_solution_generation != -1: + print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") + +# Predicting the outputs of the data using the best solution. +predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[solution_idx], + data_inputs=data_inputs, + problem_type="regression") +print(f"Predictions of the trained network : {predictions}") + +# Calculating some statistics +abs_error = numpy.mean(numpy.abs(predictions - data_outputs)) +print(f"Absolute error : {abs_error}.") +``` + +The next figure shows how the fitness value changes for the generations used. + +![example_regression](https://user-images.githubusercontent.com/16560492/92948154-3cf24b00-f459-11ea-94ea-952b66ab2145.png) diff --git a/docs/source/gann_regression_2.md b/docs/source/gann_regression_2.md new file mode 100644 index 0000000..1a8c850 --- /dev/null +++ b/docs/source/gann_regression_2.md @@ -0,0 +1,148 @@ +# Regression Example 2 - Fish Weight Prediction + +This example uses the Fish Market Dataset available at Kaggle (https://www.kaggle.com/aungpyaeap/fish-market). Simply download the CSV dataset from [this link](https://www.kaggle.com/aungpyaeap/fish-market/download) (https://www.kaggle.com/aungpyaeap/fish-market/download). The dataset is also available at the [GitHub project of the pygad.gann module](https://github.com/ahmedfgad/NeuralGenetic): https://github.com/ahmedfgad/NeuralGenetic + +Using the Pandas library, the dataset is read using the `read_csv()` function. + +```python +data = numpy.array(pandas.read_csv("Fish.csv")) +``` + +The last 5 columns in the dataset are used as inputs and the **Weight** column is used as output. + +```python +# Preparing the NumPy array of the inputs. +data_inputs = numpy.asarray(data[:, 2:], dtype=numpy.float32) + +# Preparing the NumPy array of the outputs. +data_outputs = numpy.asarray(data[:, 1], dtype=numpy.float32) # Fish Weight +``` + +Note how the activation function at the last layer is set to `"None"`. Moreover, the `problem_type` parameter in the `pygad.nn.train()` and `pygad.nn.predict()` functions is set to `"regression"`. Remember to design an appropriate fitness function for the regression problem. In this example, the fitness value is calculated based on the mean absolute error. + +```python +solution_fitness = 1.0/numpy.mean(numpy.abs(predictions - data_outputs)) +``` + +Here is the complete code. + +```python +import numpy +import pygad +import pygad.nn +import pygad.gann +import pandas + +def fitness_func(ga_instance, solution, sol_idx): + global GANN_instance, data_inputs, data_outputs + + predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[sol_idx], + data_inputs=data_inputs, problem_type="regression") + solution_fitness = 1.0/numpy.mean(numpy.abs(predictions - data_outputs)) + + return solution_fitness + +def callback_generation(ga_instance): + global GANN_instance, last_fitness + + population_matrices = pygad.gann.population_as_matrices(population_networks=GANN_instance.population_networks, + population_vectors=ga_instance.population) + + GANN_instance.update_population_trained_weights(population_trained_weights=population_matrices) + + print(f"Generation = {ga_instance.generations_completed}") + print(f"Fitness = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1]}") + print(f"Change = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1] - last_fitness}") + + last_fitness = ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1].copy() + +# Holds the fitness value of the previous generation. +last_fitness = 0 + +data = numpy.array(pandas.read_csv("../data/Fish.csv")) + +# Preparing the NumPy array of the inputs. +data_inputs = numpy.asarray(data[:, 2:], dtype=numpy.float32) + +# Preparing the NumPy array of the outputs. +data_outputs = numpy.asarray(data[:, 1], dtype=numpy.float32) + +# The length of the input vector for each sample (i.e. number of neurons in the input layer). +num_inputs = data_inputs.shape[1] + +# Creating an initial population of neural networks. The return of the initial_population() function holds references to the networks, not their weights. Using such references, the weights of all networks can be fetched. +num_solutions = 6 # A solution or a network can be used interchangeably. +GANN_instance = pygad.gann.GANN(num_solutions=num_solutions, + num_neurons_input=num_inputs, + num_neurons_hidden_layers=[2], + num_neurons_output=1, + hidden_activations=["relu"], + output_activation="None") + +# population does not hold the numerical weights of the network instead it holds a list of references to each last layer of each network (i.e. solution) in the population. A solution or a network can be used interchangeably. +# If there is a population with 3 solutions (i.e. networks), then the population is a list with 3 elements. Each element is a reference to the last layer of each network. Using such a reference, all details of the network can be accessed. +population_vectors = pygad.gann.population_as_vectors(population_networks=GANN_instance.population_networks) + +# To prepare the initial population, there are 2 ways: +# 1) Prepare it yourself and pass it to the initial_population parameter. This way is useful when the user wants to start the genetic algorithm with a custom initial population. +# 2) Assign valid integer values to the sol_per_pop and num_genes parameters. If the initial_population parameter exists, then the sol_per_pop and num_genes parameters are useless. +initial_population = population_vectors.copy() + +num_parents_mating = 4 # Number of solutions to be selected as parents in the mating pool. + +num_generations = 500 # Number of generations. + +mutation_percent_genes = 5 # Percentage of genes to mutate. This parameter has no action if the parameter mutation_num_genes exists. + +parent_selection_type = "sss" # Type of parent selection. + +crossover_type = "single_point" # Type of the crossover operator. + +mutation_type = "random" # Type of the mutation operator. + +keep_parents = 1 # Number of parents to keep in the next population. -1 means keep all parents and 0 means keep nothing. + +init_range_low = -1 +init_range_high = 1 + +ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=num_parents_mating, + initial_population=initial_population, + fitness_func=fitness_func, + mutation_percent_genes=mutation_percent_genes, + init_range_low=init_range_low, + init_range_high=init_range_high, + parent_selection_type=parent_selection_type, + crossover_type=crossover_type, + mutation_type=mutation_type, + keep_parents=keep_parents, + on_generation=callback_generation) + +ga_instance.run() + +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. +ga_instance.plot_fitness() + +# Returning the details of the best solution. +solution, solution_fitness, solution_idx = ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness) +print(f"Parameters of the best solution : {solution}") +print(f"Fitness value of the best solution = {solution_fitness}") +print(f"Index of the best solution : {solution_idx}") + +if ga_instance.best_solution_generation != -1: + print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") + +# Predicting the outputs of the data using the best solution. +predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[solution_idx], + data_inputs=data_inputs, + problem_type="regression") +print(f"Predictions of the trained network : {predictions}") + +# Calculating some statistics +abs_error = numpy.mean(numpy.abs(predictions - data_outputs)) +print(f"Absolute error : {abs_error}.") +``` + +The next figure shows how the fitness value changes for the 500 generations used. + +![example_regression_fish](https://user-images.githubusercontent.com/16560492/92948486-bbe78380-f459-11ea-9e31-0d4c7269d606.png) diff --git a/docs/source/gann_xor.md b/docs/source/gann_xor.md new file mode 100644 index 0000000..db2c6d1 --- /dev/null +++ b/docs/source/gann_xor.md @@ -0,0 +1,134 @@ +# XOR Classification + +This example is discussed in the **Steps to Build and Train Neural Networks using Genetic Algorithm** section that builds the XOR gate and its complete code is listed below. + +```python +import numpy +import pygad +import pygad.nn +import pygad.gann + +def fitness_func(ga_instance, solution, sol_idx): + global GANN_instance, data_inputs, data_outputs + + # If adaptive mutation is used, sometimes sol_idx is None. + if sol_idx == None: + sol_idx = 1 + + predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[sol_idx], + data_inputs=data_inputs) + correct_predictions = numpy.where(predictions == data_outputs)[0].size + solution_fitness = (correct_predictions/data_outputs.size)*100 + + return solution_fitness + +def callback_generation(ga_instance): + global GANN_instance, last_fitness + + population_matrices = pygad.gann.population_as_matrices(population_networks=GANN_instance.population_networks, + population_vectors=ga_instance.population) + + GANN_instance.update_population_trained_weights(population_trained_weights=population_matrices) + + print(f"Generation = {ga_instance.generations_completed}") + print(f"Fitness = {ga_instance.best_solution()[1]}") + print(f"Change = {ga_instance.best_solution()[1] - last_fitness}") + + last_fitness = ga_instance.best_solution()[1].copy() + +# Holds the fitness value of the previous generation. +last_fitness = 0 + +# Preparing the NumPy array of the inputs. +data_inputs = numpy.array([[1, 1], + [1, 0], + [0, 1], + [0, 0]]) + +# Preparing the NumPy array of the outputs. +data_outputs = numpy.array([0, + 1, + 1, + 0]) + +# The length of the input vector for each sample (i.e. number of neurons in the input layer). +num_inputs = data_inputs.shape[1] +# The number of neurons in the output layer (i.e. number of classes). +num_classes = 2 + +# Creating an initial population of neural networks. The return of the initial_population() function holds references to the networks, not their weights. Using such references, the weights of all networks can be fetched. +num_solutions = 6 # A solution or a network can be used interchangeably. +GANN_instance = pygad.gann.GANN(num_solutions=num_solutions, + num_neurons_input=num_inputs, + num_neurons_hidden_layers=[2], + num_neurons_output=num_classes, + hidden_activations=["relu"], + output_activation="softmax") + +# population does not hold the numerical weights of the network instead it holds a list of references to each last layer of each network (i.e. solution) in the population. A solution or a network can be used interchangeably. +# If there is a population with 3 solutions (i.e. networks), then the population is a list with 3 elements. Each element is a reference to the last layer of each network. Using such a reference, all details of the network can be accessed. +population_vectors = pygad.gann.population_as_vectors(population_networks=GANN_instance.population_networks) + +# To prepare the initial population, there are 2 ways: +# 1) Prepare it yourself and pass it to the initial_population parameter. This way is useful when the user wants to start the genetic algorithm with a custom initial population. +# 2) Assign valid integer values to the sol_per_pop and num_genes parameters. If the initial_population parameter exists, then the sol_per_pop and num_genes parameters are useless. +initial_population = population_vectors.copy() + +num_parents_mating = 4 # Number of solutions to be selected as parents in the mating pool. + +num_generations = 500 # Number of generations. + +mutation_percent_genes = [5, 10] # Percentage of genes to mutate. This parameter has no action if the parameter mutation_num_genes exists. + +parent_selection_type = "sss" # Type of parent selection. + +crossover_type = "single_point" # Type of the crossover operator. + +mutation_type = "adaptive" # Type of the mutation operator. + +keep_parents = 1 # Number of parents to keep in the next population. -1 means keep all parents and 0 means keep nothing. + +init_range_low = -2 +init_range_high = 5 + +ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=num_parents_mating, + initial_population=initial_population, + fitness_func=fitness_func, + mutation_percent_genes=mutation_percent_genes, + init_range_low=init_range_low, + init_range_high=init_range_high, + parent_selection_type=parent_selection_type, + crossover_type=crossover_type, + mutation_type=mutation_type, + keep_parents=keep_parents, + suppress_warnings=True, + on_generation=callback_generation) + +ga_instance.run() + +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. +ga_instance.plot_fitness() + +# Returning the details of the best solution. +solution, solution_fitness, solution_idx = ga_instance.best_solution() +print(f"Parameters of the best solution : {solution}") +print(f"Fitness value of the best solution = {solution_fitness}") +print(f"Index of the best solution : {solution_idx}") + +if ga_instance.best_solution_generation != -1: + print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") + +# Predicting the outputs of the data using the best solution. +predictions = pygad.nn.predict(last_layer=GANN_instance.population_networks[solution_idx], + data_inputs=data_inputs) +print(f"Predictions of the trained network : {predictions}") + +# Calculating some statistics +num_wrong = numpy.where(predictions != data_outputs)[0] +num_correct = data_outputs.size - num_wrong.size +accuracy = 100 * (num_correct/data_outputs.size) +print(f"Number of correct classifications : {num_correct}.") +print(f"Number of wrong classifications : {num_wrong.size}.") +print(f"Classification accuracy : {accuracy}.") +``` diff --git a/docs/source/kerasga.md b/docs/source/kerasga.md index ee3317f..59348f2 100644 --- a/docs/source/kerasga.md +++ b/docs/source/kerasga.md @@ -138,763 +138,42 @@ It returns the predictions of the data samples. This section gives the complete code of some examples that build and train a Keras model using PyGAD. Each subsection builds a different network. -### Example 1: Regression Example - -The next code builds a simple Keras model for regression. The next subsections discuss each part in the code. - -```python -import tensorflow.keras -import pygad.kerasga -import numpy -import pygad - -def fitness_func(ga_instance, solution, sol_idx): - global data_inputs, data_outputs, keras_ga, model - - predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=data_inputs) - - mae = tensorflow.keras.losses.MeanAbsoluteError() - abs_error = mae(data_outputs, predictions).numpy() + 0.00000001 - solution_fitness = 1.0/abs_error - - return solution_fitness - -def on_generation(ga_instance): - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution()[1]}") - -input_layer = tensorflow.keras.layers.Input(3) -dense_layer1 = tensorflow.keras.layers.Dense(5, activation="relu")(input_layer) -output_layer = tensorflow.keras.layers.Dense(1, activation="linear")(dense_layer1) - -model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) - -keras_ga = pygad.kerasga.KerasGA(model=model, - num_solutions=10) - -# Data inputs -data_inputs = numpy.array([[0.02, 0.1, 0.15], - [0.7, 0.6, 0.8], - [1.5, 1.2, 1.7], - [3.2, 2.9, 3.1]]) - -# Data outputs -data_outputs = numpy.array([[0.1], - [0.6], - [1.3], - [2.5]]) - -# Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class -num_generations = 250 # Number of generations. -num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. -initial_population = keras_ga.population_weights # Initial population of network weights - -ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - on_generation=on_generation) - -ga_instance.run() - -# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. -ga_instance.plot_fitness(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4) - -# Returning the details of the best solution. -solution, solution_fitness, solution_idx = ga_instance.best_solution() -print(f"Fitness value of the best solution = {solution_fitness}") -print(f"Index of the best solution : {solution_idx}") - -# Make prediction based on the best solution. -predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=data_inputs) -print(f"Predictions : \n{predictions}") - -mae = tensorflow.keras.losses.MeanAbsoluteError() -abs_error = mae(data_outputs, predictions).numpy() -print(f"Absolute Error : {abs_error}") -``` - -#### Create a Keras Model - -According to the steps mentioned previously, the first step is to create a Keras model. Here is the code that builds the model using the Functional API. - -```python -import tensorflow.keras - -input_layer = tensorflow.keras.layers.Input(3) -dense_layer1 = tensorflow.keras.layers.Dense(5, activation="relu")(input_layer) -output_layer = tensorflow.keras.layers.Dense(1, activation="linear")(dense_layer1) - -model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) -``` - -The model can also be build using the Keras Sequential Model API. - -```python -input_layer = tensorflow.keras.layers.Input(3) -dense_layer1 = tensorflow.keras.layers.Dense(5, activation="relu") -output_layer = tensorflow.keras.layers.Dense(1, activation="linear") - -model = tensorflow.keras.Sequential() -model.add(input_layer) -model.add(dense_layer1) -model.add(output_layer) -``` - -#### Create an Instance of the `pygad.kerasga.KerasGA` Class - -The second step is to create an instance of the `pygad.kerasga.KerasGA` class. There are 10 solutions per population. Change this number according to your needs. - -```python -import pygad.kerasga - -keras_ga = pygad.kerasga.KerasGA(model=model, - num_solutions=10) -``` - -#### Prepare the Training Data - -The third step is to prepare the training data inputs and outputs. Here is an example where there are 4 samples. Each sample has 3 inputs and 1 output. - -```python -import numpy - -# Data inputs -data_inputs = numpy.array([[0.02, 0.1, 0.15], - [0.7, 0.6, 0.8], - [1.5, 1.2, 1.7], - [3.2, 2.9, 3.1]]) - -# Data outputs -data_outputs = numpy.array([[0.1], - [0.6], - [1.3], - [2.5]]) -``` - -#### Build the Fitness Function - -The fourth step is to build the fitness function. This function must accept 2 parameters representing the solution and its index within the population. - -The next fitness function returns the model predictions based on the current solution using the `predict()` function. Then, it calculates the mean absolute error (MAE) of the Keras model based on the parameters in the solution. The reciprocal of the MAE is used as the fitness value. Feel free to use any other loss function to calculate the fitness value. - -```python -def fitness_func(ga_instance, solution, sol_idx): - global data_inputs, data_outputs, keras_ga, model - - predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=data_inputs) - - mae = tensorflow.keras.losses.MeanAbsoluteError() - abs_error = mae(data_outputs, predictions).numpy() + 0.00000001 - solution_fitness = 1.0/abs_error - - return solution_fitness -``` - -#### Create an Instance of the `pygad.GA` Class - -The fifth step is to instantiate the `pygad.GA` class. Note how the `initial_population` parameter is assigned to the initial weights of the Keras models. - -For more information, please check the [parameters this class accepts](https://pygad.readthedocs.io/en/latest/pygad.html#init). - -```python -# Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class -num_generations = 250 # Number of generations. -num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. -initial_population = keras_ga.population_weights # Initial population of network weights - -ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - on_generation=on_generation) -``` - -#### Run the Genetic Algorithm - -The sixth and last step is to run the genetic algorithm by calling the `run()` method. - -```python -ga_instance.run() -``` - -After PyGAD completes its execution, a figure shows how the fitness value changes by generation. Call the `plot_fitness()` method to show the figure. - -```python -ga_instance.plot_fitness(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4) -``` - -Here is the figure. - -![pygad_keras_image_regression](https://user-images.githubusercontent.com/16560492/93722638-ac261880-fb98-11ea-95d3-e773deb034f4.png) - -To get information about the best solution found by PyGAD, use the `best_solution()` method. - -```python -# Returning the details of the best solution. -solution, solution_fitness, solution_idx = ga_instance.best_solution() -print(f"Fitness value of the best solution = {solution_fitness}") -print(f"Index of the best solution : {solution_idx}") -``` - -```python -Fitness value of the best solution = 72.77768757825352 -Index of the best solution : 0 -``` - -The next code makes prediction using the `predict()` function to return the model predictions based on the best solution. - -```python -# Fetch the parameters of the best solution. -predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=data_inputs) -print(f"Predictions : \n{predictions}") -``` - -```python -Predictions : -[[0.09935353] - [0.63082725] - [1.2765523 ] - [2.4999595 ]] -``` - -The next code measures the trained model error. - -```python -mae = tensorflow.keras.losses.MeanAbsoluteError() -abs_error = mae(data_outputs, predictions).numpy() -print(f"Absolute Error : {abs_error}") -``` - -``` -Absolute Error : 0.013740465 -``` - -### Example 2: XOR Binary Classification - -The next code creates a Keras model to build the XOR binary classification problem. Let's highlight the changes compared to the previous example. - -```python -import tensorflow.keras -import pygad.kerasga -import numpy -import pygad - -def fitness_func(ga_instance, solution, sol_idx): - global data_inputs, data_outputs, keras_ga, model - - predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=data_inputs) - - bce = tensorflow.keras.losses.BinaryCrossentropy() - solution_fitness = 1.0 / (bce(data_outputs, predictions).numpy() + 0.00000001) - - return solution_fitness - -def on_generation(ga_instance): - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution()[1]}") - -# Build the keras model using the functional API. -input_layer = tensorflow.keras.layers.Input(2) -dense_layer = tensorflow.keras.layers.Dense(4, activation="relu")(input_layer) -output_layer = tensorflow.keras.layers.Dense(2, activation="softmax")(dense_layer) - -model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) - -# Create an instance of the pygad.kerasga.KerasGA class to build the initial population. -keras_ga = pygad.kerasga.KerasGA(model=model, - num_solutions=10) - -# XOR problem inputs -data_inputs = numpy.array([[0, 0], - [0, 1], - [1, 0], - [1, 1]]) - -# XOR problem outputs -data_outputs = numpy.array([[1, 0], - [0, 1], - [0, 1], - [1, 0]]) - -# Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class -num_generations = 250 # Number of generations. -num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. -initial_population = keras_ga.population_weights # Initial population of network weights. - -# Create an instance of the pygad.GA class -ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - on_generation=on_generation) - -# Start the genetic algorithm evolution. -ga_instance.run() - -# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. -ga_instance.plot_fitness(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4) - -# Returning the details of the best solution. -solution, solution_fitness, solution_idx = ga_instance.best_solution() -print(f"Fitness value of the best solution = {solution_fitness}") -print(f"Index of the best solution : {solution_idx}") - -# Make predictions based on the best solution. -predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=data_inputs) -print(f"Predictions : \n{predictions}") - -# Calculate the binary crossentropy for the trained model. -bce = tensorflow.keras.losses.BinaryCrossentropy() -print("Binary Crossentropy : ", bce(data_outputs, predictions).numpy()) - -# Calculate the classification accuracy for the trained model. -ba = tensorflow.keras.metrics.BinaryAccuracy() -ba.update_state(data_outputs, predictions) -accuracy = ba.result().numpy() -print(f"Accuracy : {accuracy}") -``` - -Compared to the previous regression example, here are the changes: - -* The Keras model is changed according to the nature of the problem. Now, it has 2 inputs and 2 outputs with an in-between hidden layer of 4 neurons. - -```python -# Build the keras model using the functional API. -input_layer = tensorflow.keras.layers.Input(2) -dense_layer = tensorflow.keras.layers.Dense(4, activation="relu")(input_layer) -output_layer = tensorflow.keras.layers.Dense(2, activation="softmax")(dense_layer) - -model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) -``` - -* The train data is changed. Note that the output of each sample is a 1D vector of 2 values, 1 for each class. - -```python -# XOR problem inputs -data_inputs = numpy.array([[0, 0], - [0, 1], - [1, 0], - [1, 1]]) - -# XOR problem outputs -data_outputs = numpy.array([[1, 0], - [0, 1], - [0, 1], - [1, 0]]) -``` - -* The fitness value is calculated based on the binary cross entropy. - -```python -bce = tensorflow.keras.losses.BinaryCrossentropy() -solution_fitness = 1.0 / (bce(data_outputs, predictions).numpy() + 0.00000001) -``` - -After the previous code completes, the next figure shows how the fitness value change by generation. - -![pygad_keras_image_classification_XOR](https://user-images.githubusercontent.com/16560492/93722639-b811da80-fb98-11ea-8951-f13a7a266c04.png) - -Here is some information about the trained model. Its fitness value is `739.24`, loss is `0.0013527311` and accuracy is 100%. - -```python -Fitness value of the best solution = 739.2397344644013 -Index of the best solution : 7 - -Predictions : -[[9.9694413e-01 3.0558957e-03] - [5.0176249e-04 9.9949825e-01] - [1.8470541e-03 9.9815291e-01] - [9.9999976e-01 2.0538971e-07]] - -Binary Crossentropy : 0.0013527311 - -Accuracy : 1.0 -``` - -### Example 3: Image Multi-Class Classification (Dense Layers) - -Here is the code. - -```python -import tensorflow.keras -import pygad.kerasga -import numpy -import pygad - -def fitness_func(ga_instance, solution, sol_idx): - global data_inputs, data_outputs, keras_ga, model - - predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=data_inputs) - - cce = tensorflow.keras.losses.CategoricalCrossentropy() - solution_fitness = 1.0 / (cce(data_outputs, predictions).numpy() + 0.00000001) - - return solution_fitness - -def on_generation(ga_instance): - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution()[1]}") - -# Build the keras model using the functional API. -input_layer = tensorflow.keras.layers.Input(360) -dense_layer = tensorflow.keras.layers.Dense(50, activation="relu")(input_layer) -output_layer = tensorflow.keras.layers.Dense(4, activation="softmax")(dense_layer) - -model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) - -# Create an instance of the pygad.kerasga.KerasGA class to build the initial population. -keras_ga = pygad.kerasga.KerasGA(model=model, - num_solutions=10) - -# Data inputs -data_inputs = numpy.load("../data/dataset_features.npy") - -# Data outputs -data_outputs = numpy.load("../data/outputs.npy") -data_outputs = tensorflow.keras.utils.to_categorical(data_outputs) - -# Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class -num_generations = 100 # Number of generations. -num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. -initial_population = keras_ga.population_weights # Initial population of network weights. - -# Create an instance of the pygad.GA class -ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - on_generation=on_generation) - -# Start the genetic algorithm evolution. -ga_instance.run() - -# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. -ga_instance.plot_fitness(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4) - -# Returning the details of the best solution. -solution, solution_fitness, solution_idx = ga_instance.best_solution() -print(f"Fitness value of the best solution = {solution_fitness}") -print(f"Index of the best solution : {solution_idx}") - -# Make predictions based on the best solution. -predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=data_inputs) -# print(f"Predictions : \n{predictions}") - -# Calculate the categorical crossentropy for the trained model. -cce = tensorflow.keras.losses.CategoricalCrossentropy() -print(f"Categorical Crossentropy : {cce(data_outputs, predictions).numpy()}") - -# Calculate the classification accuracy for the trained model. -ca = tensorflow.keras.metrics.CategoricalAccuracy() -ca.update_state(data_outputs, predictions) -accuracy = ca.result().numpy() -print(f"Accuracy : {accuracy}") -``` - -Compared to the previous binary classification example, this example has multiple classes (4) and thus the loss is measured using categorical cross entropy. - -```python -cce = tensorflow.keras.losses.CategoricalCrossentropy() -solution_fitness = 1.0 / (cce(data_outputs, predictions).numpy() + 0.00000001) -``` - -#### Prepare the Training Data - -Before building and training neural networks, the training data (input and output) needs to be prepared. The inputs and the outputs of the training data are NumPy arrays. - -The data used in this example is available as 2 files: - -1. [dataset_features.npy](https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy): Data inputs. https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy -2. [outputs.npy](https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy): Class labels. https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy - -The data consists of 4 classes of images. The image shape is `(100, 100, 3)`. The number of training samples is 1962. The feature vector extracted from each image has a length 360. - -Simply download these 2 files and read them according to the next code. Note that the class labels are one-hot encoded using the `tensorflow.keras.utils.to_categorical()` function. - -```python -import numpy - -data_inputs = numpy.load("../data/dataset_features.npy") - -data_outputs = numpy.load("../data/outputs.npy") -data_outputs = tensorflow.keras.utils.to_categorical(data_outputs) -``` - -The next figure shows how the fitness value changes. - -![pygad_keras_image_classification](https://user-images.githubusercontent.com/16560492/93722649-c2cc6f80-fb98-11ea-96e7-3f6ce3cfe1cf.png) - -Here are some statistics about the trained model. - -``` -Fitness value of the best solution = 4.197464252185969 -Index of the best solution : 0 -Categorical Crossentropy : 0.23823906 -Accuracy : 0.9852192 -``` - -### Example 4: Image Multi-Class Classification (Conv Layers) - -Compared to the previous example that uses only dense layers, this example uses convolutional layers to classify the same dataset. - -Here is the complete code. - -```python -import tensorflow.keras -import pygad.kerasga -import numpy -import pygad - -def fitness_func(ga_instance, solution, sol_idx): - global data_inputs, data_outputs, keras_ga, model - - predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=data_inputs) - - cce = tensorflow.keras.losses.CategoricalCrossentropy() - solution_fitness = 1.0 / (cce(data_outputs, predictions).numpy() + 0.00000001) - - return solution_fitness - -def on_generation(ga_instance): - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution()[1]}") - -# Build the keras model using the functional API. -input_layer = tensorflow.keras.layers.Input(shape=(100, 100, 3)) -conv_layer1 = tensorflow.keras.layers.Conv2D(filters=5, - kernel_size=7, - activation="relu")(input_layer) -max_pool1 = tensorflow.keras.layers.MaxPooling2D(pool_size=(5,5), - strides=5)(conv_layer1) -conv_layer2 = tensorflow.keras.layers.Conv2D(filters=3, - kernel_size=3, - activation="relu")(max_pool1) -flatten_layer = tensorflow.keras.layers.Flatten()(conv_layer2) -dense_layer = tensorflow.keras.layers.Dense(15, activation="relu")(flatten_layer) -output_layer = tensorflow.keras.layers.Dense(4, activation="softmax")(dense_layer) - -model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) - -# Create an instance of the pygad.kerasga.KerasGA class to build the initial population. -keras_ga = pygad.kerasga.KerasGA(model=model, - num_solutions=10) - -# Data inputs -data_inputs = numpy.load("../data/dataset_inputs.npy") - -# Data outputs -data_outputs = numpy.load("../data/dataset_outputs.npy") -data_outputs = tensorflow.keras.utils.to_categorical(data_outputs) - -# Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class -num_generations = 200 # Number of generations. -num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. -initial_population = keras_ga.population_weights # Initial population of network weights. - -# Create an instance of the pygad.GA class -ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - on_generation=on_generation) - -# Start the genetic algorithm evolution. -ga_instance.run() - -# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. -ga_instance.plot_fitness(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4) - -# Returning the details of the best solution. -solution, solution_fitness, solution_idx = ga_instance.best_solution() -print(f"Fitness value of the best solution = {solution_fitness}") -print(f"Index of the best solution : {solution_idx}") - -# Make predictions based on the best solution. -predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=data_inputs) -# print(f"Predictions : \n{predictions}") - -# Calculate the categorical crossentropy for the trained model. -cce = tensorflow.keras.losses.CategoricalCrossentropy() -print(f"Categorical Crossentropy : {cce(data_outputs, predictions).numpy()}") - -# Calculate the classification accuracy for the trained model. -ca = tensorflow.keras.metrics.CategoricalAccuracy() -ca.update_state(data_outputs, predictions) -accuracy = ca.result().numpy() -print(f"Accuracy : {accuracy}") -``` - -Compared to the previous example, the only change is that the architecture uses convolutional and max-pooling layers. The shape of each input sample is 100x100x3. - -```python -# Build the keras model using the functional API. -input_layer = tensorflow.keras.layers.Input(shape=(100, 100, 3)) -conv_layer1 = tensorflow.keras.layers.Conv2D(filters=5, - kernel_size=7, - activation="relu")(input_layer) -max_pool1 = tensorflow.keras.layers.MaxPooling2D(pool_size=(5,5), - strides=5)(conv_layer1) -conv_layer2 = tensorflow.keras.layers.Conv2D(filters=3, - kernel_size=3, - activation="relu")(max_pool1) -flatten_layer = tensorflow.keras.layers.Flatten()(conv_layer2) -dense_layer = tensorflow.keras.layers.Dense(15, activation="relu")(flatten_layer) -output_layer = tensorflow.keras.layers.Dense(4, activation="softmax")(dense_layer) - -model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) -``` - -#### Prepare the Training Data - -The data used in this example is available as 2 files: - -1. [dataset_inputs.npy](https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_inputs.npy): Data inputs. https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_inputs.npy -2. [dataset_outputs.npy](https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_outputs.npy): Class labels. https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_outputs.npy - -The data consists of 4 classes of images. The image shape is `(100, 100, 3)` and there are 20 images per class for a total of 80 training samples. For more information about the dataset, check the [Reading the Data](https://pygad.readthedocs.io/en/latest/cnn.html#reading-the-data) section of the `pygad.cnn` module. - -Simply download these 2 files and read them according to the next code. Note that the class labels are one-hot encoded using the `tensorflow.keras.utils.to_categorical()` function. - -```python -import numpy - -data_inputs = numpy.load("../data/dataset_inputs.npy") - -data_outputs = numpy.load("../data/dataset_outputs.npy") -data_outputs = tensorflow.keras.utils.to_categorical(data_outputs) -``` - -The next figure shows how the fitness value changes. - -![pygad_keras_image_classification_Conv](https://user-images.githubusercontent.com/16560492/93722654-cc55d780-fb98-11ea-8f95-7b65dc67f5c8.png) - -Here are some statistics about the trained model. The model accuracy is 75% after the 200 generations. Note that just running the code again may give different results. - -``` -Fitness value of the best solution = 2.7462310258668805 -Index of the best solution : 0 -Categorical Crossentropy : 0.3641354 -Accuracy : 0.75 -``` - -To improve the model performance, you can do the following: - -- Add more layers -- Modify the existing layers. -- Use different parameters for the layers. -- Use different parameters for the genetic algorithm (e.g. number of solution, number of generations, etc) - -### Example 5: Image Classification using Data Generator - -This example uses the image data generator `tensorflow.keras.preprocessing.image.ImageDataGenerator` to feed data to the model. Instead of reading all the data in the memory, the data generator generates the data needed by the model and only save it in the memory instead of saving all the data. This frees the memory but adds more computational time. - -```python -import tensorflow as tf -import tensorflow.keras -import pygad.kerasga -import pygad - -def fitness_func(ga_instanse, solution, sol_idx): - global train_generator, data_outputs, keras_ga, model - - predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=train_generator) - - cce = tensorflow.keras.losses.CategoricalCrossentropy() - solution_fitness = 1.0 / (cce(data_outputs, predictions).numpy() + 0.00000001) - - return solution_fitness - -def on_generation(ga_instance): - print("Generation = {ga_instance.generations_completed}") - print("Fitness = {ga_instance.best_solution(ga_instance.last_generation_fitness)[1]}") - -# The dataset path. -dataset_path = r'../data/Skin_Cancer_Dataset' - -num_classes = 2 -img_size = 224 - -# Create a simple CNN. This does not gurantee high classification accuracy. -model = tf.keras.models.Sequential() -model.add(tf.keras.layers.Input(shape=(img_size, img_size, 3))) -model.add(tf.keras.layers.Conv2D(32, (3,3), activation="relu", padding="same")) -model.add(tf.keras.layers.MaxPooling2D((2, 2))) -model.add(tf.keras.layers.Flatten()) -model.add(tf.keras.layers.Dropout(rate=0.2)) -model.add(tf.keras.layers.Dense(num_classes, activation="softmax")) - -# Create an instance of the pygad.kerasga.KerasGA class to build the initial population. -keras_ga = pygad.kerasga.KerasGA(model=model, - num_solutions=10) - -data_generator = tf.keras.preprocessing.image.ImageDataGenerator() -train_generator = data_generator.flow_from_directory(dataset_path, - class_mode='categorical', - target_size=(224, 224), - batch_size=32, - shuffle=False) -# train_generator.class_indices -data_outputs = tf.keras.utils.to_categorical(train_generator.labels) - -# Check the documentation for more information about the parameters: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class -initial_population = keras_ga.population_weights # Initial population of network weights. - -# Create an instance of the pygad.GA class -ga_instance = pygad.GA(num_generations=10, - num_parents_mating=5, - initial_population=initial_population, - fitness_func=fitness_func, - on_generation=on_generation) - -# Start the genetic algorithm evolution. -ga_instance.run() - -# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. -ga_instance.plot_fitness(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4) - -# Returning the details of the best solution. -solution, solution_fitness, solution_idx = ga_instance.best_solution(ga_instance.last_generation_fitness) -print(f"Fitness value of the best solution = {solution_fitness}") -print(f"Index of the best solution : {solution_idx}") - -predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=train_generator) -# print(f"Predictions : \n{predictions}") - -# Calculate the categorical crossentropy for the trained model. -cce = tensorflow.keras.losses.CategoricalCrossentropy() -print(f"Categorical Crossentropy : {cce(data_outputs, predictions).numpy()}") - -# Calculate the classification accuracy for the trained model. -ca = tensorflow.keras.metrics.CategoricalAccuracy() -ca.update_state(data_outputs, predictions) -accuracy = ca.result().numpy() -print(f"Accuracy : {accuracy}") -``` - - - +::::{grid} 1 2 2 2 +:gutter: 3 + +:::{grid-item-card} Example 1: Regression Example +:link: kerasga_regression +:link-type: doc +::: + +:::{grid-item-card} Example 2: XOR Binary Classification +:link: kerasga_xor +:link-type: doc +::: + +:::{grid-item-card} Example 3: Image Multi-Class Classification (Dense Layers) +:link: kerasga_image_dense +:link-type: doc +::: + +:::{grid-item-card} Example 4: Image Multi-Class Classification (Conv Layers) +:link: kerasga_image_conv +:link-type: doc +::: + +:::{grid-item-card} Example 5: Image Classification using Data Generator +:link: kerasga_image_datagen +:link-type: doc +::: + +:::: + +:::{toctree} +:hidden: + +kerasga_regression +kerasga_xor +kerasga_image_dense +kerasga_image_conv +kerasga_image_datagen +::: diff --git a/docs/source/kerasga_image_conv.md b/docs/source/kerasga_image_conv.md new file mode 100644 index 0000000..52f2a09 --- /dev/null +++ b/docs/source/kerasga_image_conv.md @@ -0,0 +1,154 @@ +# Example 4: Image Multi-Class Classification (Conv Layers) + +Compared to the previous example that uses only dense layers, this example uses convolutional layers to classify the same dataset. + +Here is the complete code. + +```python +import tensorflow.keras +import pygad.kerasga +import numpy +import pygad + +def fitness_func(ga_instance, solution, sol_idx): + global data_inputs, data_outputs, keras_ga, model + + predictions = pygad.kerasga.predict(model=model, + solution=solution, + data=data_inputs) + + cce = tensorflow.keras.losses.CategoricalCrossentropy() + solution_fitness = 1.0 / (cce(data_outputs, predictions).numpy() + 0.00000001) + + return solution_fitness + +def on_generation(ga_instance): + print(f"Generation = {ga_instance.generations_completed}") + print(f"Fitness = {ga_instance.best_solution()[1]}") + +# Build the keras model using the functional API. +input_layer = tensorflow.keras.layers.Input(shape=(100, 100, 3)) +conv_layer1 = tensorflow.keras.layers.Conv2D(filters=5, + kernel_size=7, + activation="relu")(input_layer) +max_pool1 = tensorflow.keras.layers.MaxPooling2D(pool_size=(5,5), + strides=5)(conv_layer1) +conv_layer2 = tensorflow.keras.layers.Conv2D(filters=3, + kernel_size=3, + activation="relu")(max_pool1) +flatten_layer = tensorflow.keras.layers.Flatten()(conv_layer2) +dense_layer = tensorflow.keras.layers.Dense(15, activation="relu")(flatten_layer) +output_layer = tensorflow.keras.layers.Dense(4, activation="softmax")(dense_layer) + +model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) + +# Create an instance of the pygad.kerasga.KerasGA class to build the initial population. +keras_ga = pygad.kerasga.KerasGA(model=model, + num_solutions=10) + +# Data inputs +data_inputs = numpy.load("../data/dataset_inputs.npy") + +# Data outputs +data_outputs = numpy.load("../data/dataset_outputs.npy") +data_outputs = tensorflow.keras.utils.to_categorical(data_outputs) + +# Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class +num_generations = 200 # Number of generations. +num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. +initial_population = keras_ga.population_weights # Initial population of network weights. + +# Create an instance of the pygad.GA class +ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=num_parents_mating, + initial_population=initial_population, + fitness_func=fitness_func, + on_generation=on_generation) + +# Start the genetic algorithm evolution. +ga_instance.run() + +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. +ga_instance.plot_fitness(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4) + +# Returning the details of the best solution. +solution, solution_fitness, solution_idx = ga_instance.best_solution() +print(f"Fitness value of the best solution = {solution_fitness}") +print(f"Index of the best solution : {solution_idx}") + +# Make predictions based on the best solution. +predictions = pygad.kerasga.predict(model=model, + solution=solution, + data=data_inputs) +# print(f"Predictions : \n{predictions}") + +# Calculate the categorical crossentropy for the trained model. +cce = tensorflow.keras.losses.CategoricalCrossentropy() +print(f"Categorical Crossentropy : {cce(data_outputs, predictions).numpy()}") + +# Calculate the classification accuracy for the trained model. +ca = tensorflow.keras.metrics.CategoricalAccuracy() +ca.update_state(data_outputs, predictions) +accuracy = ca.result().numpy() +print(f"Accuracy : {accuracy}") +``` + +Compared to the previous example, the only change is that the architecture uses convolutional and max-pooling layers. The shape of each input sample is 100x100x3. + +```python +# Build the keras model using the functional API. +input_layer = tensorflow.keras.layers.Input(shape=(100, 100, 3)) +conv_layer1 = tensorflow.keras.layers.Conv2D(filters=5, + kernel_size=7, + activation="relu")(input_layer) +max_pool1 = tensorflow.keras.layers.MaxPooling2D(pool_size=(5,5), + strides=5)(conv_layer1) +conv_layer2 = tensorflow.keras.layers.Conv2D(filters=3, + kernel_size=3, + activation="relu")(max_pool1) +flatten_layer = tensorflow.keras.layers.Flatten()(conv_layer2) +dense_layer = tensorflow.keras.layers.Dense(15, activation="relu")(flatten_layer) +output_layer = tensorflow.keras.layers.Dense(4, activation="softmax")(dense_layer) + +model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) +``` + +## Prepare the Training Data + +The data used in this example is available as 2 files: + +1. [dataset_inputs.npy](https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_inputs.npy): Data inputs. https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_inputs.npy +2. [dataset_outputs.npy](https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_outputs.npy): Class labels. https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_outputs.npy + +The data consists of 4 classes of images. The image shape is `(100, 100, 3)` and there are 20 images per class for a total of 80 training samples. For more information about the dataset, check the [Reading the Data](https://pygad.readthedocs.io/en/latest/cnn.html#reading-the-data) section of the `pygad.cnn` module. + +Simply download these 2 files and read them according to the next code. Note that the class labels are one-hot encoded using the `tensorflow.keras.utils.to_categorical()` function. + +```python +import numpy + +data_inputs = numpy.load("../data/dataset_inputs.npy") + +data_outputs = numpy.load("../data/dataset_outputs.npy") +data_outputs = tensorflow.keras.utils.to_categorical(data_outputs) +``` + +The next figure shows how the fitness value changes. + +![pygad_keras_image_classification_Conv](https://user-images.githubusercontent.com/16560492/93722654-cc55d780-fb98-11ea-8f95-7b65dc67f5c8.png) + +Here are some statistics about the trained model. The model accuracy is 75% after the 200 generations. Note that just running the code again may give different results. + +``` +Fitness value of the best solution = 2.7462310258668805 +Index of the best solution : 0 +Categorical Crossentropy : 0.3641354 +Accuracy : 0.75 +``` + +To improve the model performance, you can do the following: + +- Add more layers +- Modify the existing layers. +- Use different parameters for the layers. +- Use different parameters for the genetic algorithm (e.g. number of solution, number of generations, etc) diff --git a/docs/source/kerasga_image_datagen.md b/docs/source/kerasga_image_datagen.md new file mode 100644 index 0000000..da3fc1b --- /dev/null +++ b/docs/source/kerasga_image_datagen.md @@ -0,0 +1,90 @@ +# Example 5: Image Classification using Data Generator + +This example uses the image data generator `tensorflow.keras.preprocessing.image.ImageDataGenerator` to feed data to the model. Instead of reading all the data in the memory, the data generator generates the data needed by the model and only save it in the memory instead of saving all the data. This frees the memory but adds more computational time. + +```python +import tensorflow as tf +import tensorflow.keras +import pygad.kerasga +import pygad + +def fitness_func(ga_instanse, solution, sol_idx): + global train_generator, data_outputs, keras_ga, model + + predictions = pygad.kerasga.predict(model=model, + solution=solution, + data=train_generator) + + cce = tensorflow.keras.losses.CategoricalCrossentropy() + solution_fitness = 1.0 / (cce(data_outputs, predictions).numpy() + 0.00000001) + + return solution_fitness + +def on_generation(ga_instance): + print("Generation = {ga_instance.generations_completed}") + print("Fitness = {ga_instance.best_solution(ga_instance.last_generation_fitness)[1]}") + +# The dataset path. +dataset_path = r'../data/Skin_Cancer_Dataset' + +num_classes = 2 +img_size = 224 + +# Create a simple CNN. This does not gurantee high classification accuracy. +model = tf.keras.models.Sequential() +model.add(tf.keras.layers.Input(shape=(img_size, img_size, 3))) +model.add(tf.keras.layers.Conv2D(32, (3,3), activation="relu", padding="same")) +model.add(tf.keras.layers.MaxPooling2D((2, 2))) +model.add(tf.keras.layers.Flatten()) +model.add(tf.keras.layers.Dropout(rate=0.2)) +model.add(tf.keras.layers.Dense(num_classes, activation="softmax")) + +# Create an instance of the pygad.kerasga.KerasGA class to build the initial population. +keras_ga = pygad.kerasga.KerasGA(model=model, + num_solutions=10) + +data_generator = tf.keras.preprocessing.image.ImageDataGenerator() +train_generator = data_generator.flow_from_directory(dataset_path, + class_mode='categorical', + target_size=(224, 224), + batch_size=32, + shuffle=False) +# train_generator.class_indices +data_outputs = tf.keras.utils.to_categorical(train_generator.labels) + +# Check the documentation for more information about the parameters: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class +initial_population = keras_ga.population_weights # Initial population of network weights. + +# Create an instance of the pygad.GA class +ga_instance = pygad.GA(num_generations=10, + num_parents_mating=5, + initial_population=initial_population, + fitness_func=fitness_func, + on_generation=on_generation) + +# Start the genetic algorithm evolution. +ga_instance.run() + +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. +ga_instance.plot_fitness(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4) + +# Returning the details of the best solution. +solution, solution_fitness, solution_idx = ga_instance.best_solution(ga_instance.last_generation_fitness) +print(f"Fitness value of the best solution = {solution_fitness}") +print(f"Index of the best solution : {solution_idx}") + +predictions = pygad.kerasga.predict(model=model, + solution=solution, + data=train_generator) +# print(f"Predictions : \n{predictions}") + +# Calculate the categorical crossentropy for the trained model. +cce = tensorflow.keras.losses.CategoricalCrossentropy() +print(f"Categorical Crossentropy : {cce(data_outputs, predictions).numpy()}") + +# Calculate the classification accuracy for the trained model. +ca = tensorflow.keras.metrics.CategoricalAccuracy() +ca.update_state(data_outputs, predictions) +accuracy = ca.result().numpy() +print(f"Accuracy : {accuracy}") +``` diff --git a/docs/source/kerasga_image_dense.md b/docs/source/kerasga_image_dense.md new file mode 100644 index 0000000..fa6ff37 --- /dev/null +++ b/docs/source/kerasga_image_dense.md @@ -0,0 +1,125 @@ +# Example 3: Image Multi-Class Classification (Dense Layers) + +Here is the code. + +```python +import tensorflow.keras +import pygad.kerasga +import numpy +import pygad + +def fitness_func(ga_instance, solution, sol_idx): + global data_inputs, data_outputs, keras_ga, model + + predictions = pygad.kerasga.predict(model=model, + solution=solution, + data=data_inputs) + + cce = tensorflow.keras.losses.CategoricalCrossentropy() + solution_fitness = 1.0 / (cce(data_outputs, predictions).numpy() + 0.00000001) + + return solution_fitness + +def on_generation(ga_instance): + print(f"Generation = {ga_instance.generations_completed}") + print(f"Fitness = {ga_instance.best_solution()[1]}") + +# Build the keras model using the functional API. +input_layer = tensorflow.keras.layers.Input(360) +dense_layer = tensorflow.keras.layers.Dense(50, activation="relu")(input_layer) +output_layer = tensorflow.keras.layers.Dense(4, activation="softmax")(dense_layer) + +model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) + +# Create an instance of the pygad.kerasga.KerasGA class to build the initial population. +keras_ga = pygad.kerasga.KerasGA(model=model, + num_solutions=10) + +# Data inputs +data_inputs = numpy.load("../data/dataset_features.npy") + +# Data outputs +data_outputs = numpy.load("../data/outputs.npy") +data_outputs = tensorflow.keras.utils.to_categorical(data_outputs) + +# Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class +num_generations = 100 # Number of generations. +num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. +initial_population = keras_ga.population_weights # Initial population of network weights. + +# Create an instance of the pygad.GA class +ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=num_parents_mating, + initial_population=initial_population, + fitness_func=fitness_func, + on_generation=on_generation) + +# Start the genetic algorithm evolution. +ga_instance.run() + +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. +ga_instance.plot_fitness(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4) + +# Returning the details of the best solution. +solution, solution_fitness, solution_idx = ga_instance.best_solution() +print(f"Fitness value of the best solution = {solution_fitness}") +print(f"Index of the best solution : {solution_idx}") + +# Make predictions based on the best solution. +predictions = pygad.kerasga.predict(model=model, + solution=solution, + data=data_inputs) +# print(f"Predictions : \n{predictions}") + +# Calculate the categorical crossentropy for the trained model. +cce = tensorflow.keras.losses.CategoricalCrossentropy() +print(f"Categorical Crossentropy : {cce(data_outputs, predictions).numpy()}") + +# Calculate the classification accuracy for the trained model. +ca = tensorflow.keras.metrics.CategoricalAccuracy() +ca.update_state(data_outputs, predictions) +accuracy = ca.result().numpy() +print(f"Accuracy : {accuracy}") +``` + +Compared to the previous binary classification example, this example has multiple classes (4) and thus the loss is measured using categorical cross entropy. + +```python +cce = tensorflow.keras.losses.CategoricalCrossentropy() +solution_fitness = 1.0 / (cce(data_outputs, predictions).numpy() + 0.00000001) +``` + +## Prepare the Training Data + +Before building and training neural networks, the training data (input and output) needs to be prepared. The inputs and the outputs of the training data are NumPy arrays. + +The data used in this example is available as 2 files: + +1. [dataset_features.npy](https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy): Data inputs. https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy +2. [outputs.npy](https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy): Class labels. https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy + +The data consists of 4 classes of images. The image shape is `(100, 100, 3)`. The number of training samples is 1962. The feature vector extracted from each image has a length 360. + +Simply download these 2 files and read them according to the next code. Note that the class labels are one-hot encoded using the `tensorflow.keras.utils.to_categorical()` function. + +```python +import numpy + +data_inputs = numpy.load("../data/dataset_features.npy") + +data_outputs = numpy.load("../data/outputs.npy") +data_outputs = tensorflow.keras.utils.to_categorical(data_outputs) +``` + +The next figure shows how the fitness value changes. + +![pygad_keras_image_classification](https://user-images.githubusercontent.com/16560492/93722649-c2cc6f80-fb98-11ea-96e7-3f6ce3cfe1cf.png) + +Here are some statistics about the trained model. + +``` +Fitness value of the best solution = 4.197464252185969 +Index of the best solution : 0 +Categorical Crossentropy : 0.23823906 +Accuracy : 0.9852192 +``` diff --git a/docs/source/kerasga_regression.md b/docs/source/kerasga_regression.md new file mode 100644 index 0000000..dfba226 --- /dev/null +++ b/docs/source/kerasga_regression.md @@ -0,0 +1,239 @@ +# Example 1: Regression Example + +The next code builds a simple Keras model for regression. The next subsections discuss each part in the code. + +```python +import tensorflow.keras +import pygad.kerasga +import numpy +import pygad + +def fitness_func(ga_instance, solution, sol_idx): + global data_inputs, data_outputs, keras_ga, model + + predictions = pygad.kerasga.predict(model=model, + solution=solution, + data=data_inputs) + + mae = tensorflow.keras.losses.MeanAbsoluteError() + abs_error = mae(data_outputs, predictions).numpy() + 0.00000001 + solution_fitness = 1.0/abs_error + + return solution_fitness + +def on_generation(ga_instance): + print(f"Generation = {ga_instance.generations_completed}") + print(f"Fitness = {ga_instance.best_solution()[1]}") + +input_layer = tensorflow.keras.layers.Input(3) +dense_layer1 = tensorflow.keras.layers.Dense(5, activation="relu")(input_layer) +output_layer = tensorflow.keras.layers.Dense(1, activation="linear")(dense_layer1) + +model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) + +keras_ga = pygad.kerasga.KerasGA(model=model, + num_solutions=10) + +# Data inputs +data_inputs = numpy.array([[0.02, 0.1, 0.15], + [0.7, 0.6, 0.8], + [1.5, 1.2, 1.7], + [3.2, 2.9, 3.1]]) + +# Data outputs +data_outputs = numpy.array([[0.1], + [0.6], + [1.3], + [2.5]]) + +# Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class +num_generations = 250 # Number of generations. +num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. +initial_population = keras_ga.population_weights # Initial population of network weights + +ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=num_parents_mating, + initial_population=initial_population, + fitness_func=fitness_func, + on_generation=on_generation) + +ga_instance.run() + +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. +ga_instance.plot_fitness(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4) + +# Returning the details of the best solution. +solution, solution_fitness, solution_idx = ga_instance.best_solution() +print(f"Fitness value of the best solution = {solution_fitness}") +print(f"Index of the best solution : {solution_idx}") + +# Make prediction based on the best solution. +predictions = pygad.kerasga.predict(model=model, + solution=solution, + data=data_inputs) +print(f"Predictions : \n{predictions}") + +mae = tensorflow.keras.losses.MeanAbsoluteError() +abs_error = mae(data_outputs, predictions).numpy() +print(f"Absolute Error : {abs_error}") +``` + +## Create a Keras Model + +According to the steps mentioned previously, the first step is to create a Keras model. Here is the code that builds the model using the Functional API. + +```python +import tensorflow.keras + +input_layer = tensorflow.keras.layers.Input(3) +dense_layer1 = tensorflow.keras.layers.Dense(5, activation="relu")(input_layer) +output_layer = tensorflow.keras.layers.Dense(1, activation="linear")(dense_layer1) + +model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) +``` + +The model can also be build using the Keras Sequential Model API. + +```python +input_layer = tensorflow.keras.layers.Input(3) +dense_layer1 = tensorflow.keras.layers.Dense(5, activation="relu") +output_layer = tensorflow.keras.layers.Dense(1, activation="linear") + +model = tensorflow.keras.Sequential() +model.add(input_layer) +model.add(dense_layer1) +model.add(output_layer) +``` + +## Create an Instance of the `pygad.kerasga.KerasGA` Class + +The second step is to create an instance of the `pygad.kerasga.KerasGA` class. There are 10 solutions per population. Change this number according to your needs. + +```python +import pygad.kerasga + +keras_ga = pygad.kerasga.KerasGA(model=model, + num_solutions=10) +``` + +## Prepare the Training Data + +The third step is to prepare the training data inputs and outputs. Here is an example where there are 4 samples. Each sample has 3 inputs and 1 output. + +```python +import numpy + +# Data inputs +data_inputs = numpy.array([[0.02, 0.1, 0.15], + [0.7, 0.6, 0.8], + [1.5, 1.2, 1.7], + [3.2, 2.9, 3.1]]) + +# Data outputs +data_outputs = numpy.array([[0.1], + [0.6], + [1.3], + [2.5]]) +``` + +## Build the Fitness Function + +The fourth step is to build the fitness function. This function must accept 2 parameters representing the solution and its index within the population. + +The next fitness function returns the model predictions based on the current solution using the `predict()` function. Then, it calculates the mean absolute error (MAE) of the Keras model based on the parameters in the solution. The reciprocal of the MAE is used as the fitness value. Feel free to use any other loss function to calculate the fitness value. + +```python +def fitness_func(ga_instance, solution, sol_idx): + global data_inputs, data_outputs, keras_ga, model + + predictions = pygad.kerasga.predict(model=model, + solution=solution, + data=data_inputs) + + mae = tensorflow.keras.losses.MeanAbsoluteError() + abs_error = mae(data_outputs, predictions).numpy() + 0.00000001 + solution_fitness = 1.0/abs_error + + return solution_fitness +``` + +## Create an Instance of the `pygad.GA` Class + +The fifth step is to instantiate the `pygad.GA` class. Note how the `initial_population` parameter is assigned to the initial weights of the Keras models. + +For more information, please check the [parameters this class accepts](https://pygad.readthedocs.io/en/latest/pygad.html#init). + +```python +# Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class +num_generations = 250 # Number of generations. +num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. +initial_population = keras_ga.population_weights # Initial population of network weights + +ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=num_parents_mating, + initial_population=initial_population, + fitness_func=fitness_func, + on_generation=on_generation) +``` + +## Run the Genetic Algorithm + +The sixth and last step is to run the genetic algorithm by calling the `run()` method. + +```python +ga_instance.run() +``` + +After PyGAD completes its execution, a figure shows how the fitness value changes by generation. Call the `plot_fitness()` method to show the figure. + +```python +ga_instance.plot_fitness(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4) +``` + +Here is the figure. + +![pygad_keras_image_regression](https://user-images.githubusercontent.com/16560492/93722638-ac261880-fb98-11ea-95d3-e773deb034f4.png) + +To get information about the best solution found by PyGAD, use the `best_solution()` method. + +```python +# Returning the details of the best solution. +solution, solution_fitness, solution_idx = ga_instance.best_solution() +print(f"Fitness value of the best solution = {solution_fitness}") +print(f"Index of the best solution : {solution_idx}") +``` + +```python +Fitness value of the best solution = 72.77768757825352 +Index of the best solution : 0 +``` + +The next code makes prediction using the `predict()` function to return the model predictions based on the best solution. + +```python +# Fetch the parameters of the best solution. +predictions = pygad.kerasga.predict(model=model, + solution=solution, + data=data_inputs) +print(f"Predictions : \n{predictions}") +``` + +```python +Predictions : +[[0.09935353] + [0.63082725] + [1.2765523 ] + [2.4999595 ]] +``` + +The next code measures the trained model error. + +```python +mae = tensorflow.keras.losses.MeanAbsoluteError() +abs_error = mae(data_outputs, predictions).numpy() +print(f"Absolute Error : {abs_error}") +``` + +``` +Absolute Error : 0.013740465 +``` diff --git a/docs/source/kerasga_xor.md b/docs/source/kerasga_xor.md new file mode 100644 index 0000000..2753958 --- /dev/null +++ b/docs/source/kerasga_xor.md @@ -0,0 +1,145 @@ +# Example 2: XOR Binary Classification + +The next code creates a Keras model to build the XOR binary classification problem. Let's highlight the changes compared to the previous example. + +```python +import tensorflow.keras +import pygad.kerasga +import numpy +import pygad + +def fitness_func(ga_instance, solution, sol_idx): + global data_inputs, data_outputs, keras_ga, model + + predictions = pygad.kerasga.predict(model=model, + solution=solution, + data=data_inputs) + + bce = tensorflow.keras.losses.BinaryCrossentropy() + solution_fitness = 1.0 / (bce(data_outputs, predictions).numpy() + 0.00000001) + + return solution_fitness + +def on_generation(ga_instance): + print(f"Generation = {ga_instance.generations_completed}") + print(f"Fitness = {ga_instance.best_solution()[1]}") + +# Build the keras model using the functional API. +input_layer = tensorflow.keras.layers.Input(2) +dense_layer = tensorflow.keras.layers.Dense(4, activation="relu")(input_layer) +output_layer = tensorflow.keras.layers.Dense(2, activation="softmax")(dense_layer) + +model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) + +# Create an instance of the pygad.kerasga.KerasGA class to build the initial population. +keras_ga = pygad.kerasga.KerasGA(model=model, + num_solutions=10) + +# XOR problem inputs +data_inputs = numpy.array([[0, 0], + [0, 1], + [1, 0], + [1, 1]]) + +# XOR problem outputs +data_outputs = numpy.array([[1, 0], + [0, 1], + [0, 1], + [1, 0]]) + +# Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class +num_generations = 250 # Number of generations. +num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. +initial_population = keras_ga.population_weights # Initial population of network weights. + +# Create an instance of the pygad.GA class +ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=num_parents_mating, + initial_population=initial_population, + fitness_func=fitness_func, + on_generation=on_generation) + +# Start the genetic algorithm evolution. +ga_instance.run() + +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. +ga_instance.plot_fitness(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4) + +# Returning the details of the best solution. +solution, solution_fitness, solution_idx = ga_instance.best_solution() +print(f"Fitness value of the best solution = {solution_fitness}") +print(f"Index of the best solution : {solution_idx}") + +# Make predictions based on the best solution. +predictions = pygad.kerasga.predict(model=model, + solution=solution, + data=data_inputs) +print(f"Predictions : \n{predictions}") + +# Calculate the binary crossentropy for the trained model. +bce = tensorflow.keras.losses.BinaryCrossentropy() +print("Binary Crossentropy : ", bce(data_outputs, predictions).numpy()) + +# Calculate the classification accuracy for the trained model. +ba = tensorflow.keras.metrics.BinaryAccuracy() +ba.update_state(data_outputs, predictions) +accuracy = ba.result().numpy() +print(f"Accuracy : {accuracy}") +``` + +Compared to the previous regression example, here are the changes: + +* The Keras model is changed according to the nature of the problem. Now, it has 2 inputs and 2 outputs with an in-between hidden layer of 4 neurons. + +```python +# Build the keras model using the functional API. +input_layer = tensorflow.keras.layers.Input(2) +dense_layer = tensorflow.keras.layers.Dense(4, activation="relu")(input_layer) +output_layer = tensorflow.keras.layers.Dense(2, activation="softmax")(dense_layer) + +model = tensorflow.keras.Model(inputs=input_layer, outputs=output_layer) +``` + +* The train data is changed. Note that the output of each sample is a 1D vector of 2 values, 1 for each class. + +```python +# XOR problem inputs +data_inputs = numpy.array([[0, 0], + [0, 1], + [1, 0], + [1, 1]]) + +# XOR problem outputs +data_outputs = numpy.array([[1, 0], + [0, 1], + [0, 1], + [1, 0]]) +``` + +* The fitness value is calculated based on the binary cross entropy. + +```python +bce = tensorflow.keras.losses.BinaryCrossentropy() +solution_fitness = 1.0 / (bce(data_outputs, predictions).numpy() + 0.00000001) +``` + +After the previous code completes, the next figure shows how the fitness value change by generation. + +![pygad_keras_image_classification_XOR](https://user-images.githubusercontent.com/16560492/93722639-b811da80-fb98-11ea-8951-f13a7a266c04.png) + +Here is some information about the trained model. Its fitness value is `739.24`, loss is `0.0013527311` and accuracy is 100%. + +```python +Fitness value of the best solution = 739.2397344644013 +Index of the best solution : 7 + +Predictions : +[[9.9694413e-01 3.0558957e-03] + [5.0176249e-04 9.9949825e-01] + [1.8470541e-03 9.9815291e-01] + [9.9999976e-01 2.0538971e-07]] + +Binary Crossentropy : 0.0013527311 + +Accuracy : 1.0 +``` diff --git a/docs/source/nn.md b/docs/source/nn.md index faf75c5..612331d 100644 --- a/docs/source/nn.md +++ b/docs/source/nn.md @@ -429,252 +429,36 @@ It is very important to note that it is not expected that the classification acc This section gives the complete code of some examples that build neural networks using `pygad.nn`. Each subsection builds a different network. -### XOR Classification - -This is an example of building a network with 1 hidden layer with 2 neurons for building a network that simulates the XOR logic gate. Because the XOR problem has 2 classes (0 and 1), then the output layer has 2 neurons, one for each class. - -```python -import numpy -import pygad.nn - -# Preparing the NumPy array of the inputs. -data_inputs = numpy.array([[1, 1], - [1, 0], - [0, 1], - [0, 0]]) - -# Preparing the NumPy array of the outputs. -data_outputs = numpy.array([0, - 1, - 1, - 0]) - -# The number of inputs (i.e. feature vector length) per sample -num_inputs = data_inputs.shape[1] -# Number of outputs per sample -num_outputs = 2 - -HL1_neurons = 2 - -# Building the network architecture. -input_layer = pygad.nn.InputLayer(num_inputs) -hidden_layer1 = pygad.nn.DenseLayer(num_neurons=HL1_neurons, previous_layer=input_layer, activation_function="relu") -output_layer = pygad.nn.DenseLayer(num_neurons=num_outputs, previous_layer=hidden_layer1, activation_function="softmax") - -# Training the network. -pygad.nn.train(num_epochs=10, - last_layer=output_layer, - data_inputs=data_inputs, - data_outputs=data_outputs, - learning_rate=0.01) - -# Using the trained network for predictions. -predictions = pygad.nn.predict(last_layer=output_layer, data_inputs=data_inputs) - -# Calculating some statistics -num_wrong = numpy.where(predictions != data_outputs)[0] -num_correct = data_outputs.size - num_wrong.size -accuracy = 100 * (num_correct/data_outputs.size) -print(f"Number of correct classifications : {num_correct}.") -print(f"Number of wrong classifications : {num_wrong.size}.") -print(f"Classification accuracy : {accuracy}.") -``` - -### Image Classification - -This example is discussed in the **Steps to Build a Neural Network** section and its complete code is listed below. - -Remember to either download or create the [dataset_features.npy](https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy) and [outputs.npy](https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy) files before running this code. - -```python -import numpy -import pygad.nn - -# Reading the data features. Check the 'extract_features.py' script for extracting the features & preparing the outputs of the dataset. -data_inputs = numpy.load("dataset_features.npy") # Download from https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy - -# Optional step for filtering the features using the standard deviation. -features_STDs = numpy.std(a=data_inputs, axis=0) -data_inputs = data_inputs[:, features_STDs > 50] - -# Reading the data outputs. Check the 'extract_features.py' script for extracting the features & preparing the outputs of the dataset. -data_outputs = numpy.load("outputs.npy") # Download from https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy - -# The number of inputs (i.e. feature vector length) per sample -num_inputs = data_inputs.shape[1] -# Number of outputs per sample -num_outputs = 4 - -HL1_neurons = 150 -HL2_neurons = 60 - -# Building the network architecture. -input_layer = pygad.nn.InputLayer(num_inputs) -hidden_layer1 = pygad.nn.DenseLayer(num_neurons=HL1_neurons, previous_layer=input_layer, activation_function="relu") -hidden_layer2 = pygad.nn.DenseLayer(num_neurons=HL2_neurons, previous_layer=hidden_layer1, activation_function="relu") -output_layer = pygad.nn.DenseLayer(num_neurons=num_outputs, previous_layer=hidden_layer2, activation_function="softmax") - -# Training the network. -pygad.nn.train(num_epochs=10, - last_layer=output_layer, - data_inputs=data_inputs, - data_outputs=data_outputs, - learning_rate=0.01) - -# Using the trained network for predictions. -predictions = pygad.nn.predict(last_layer=output_layer, data_inputs=data_inputs) - -# Calculating some statistics -num_wrong = numpy.where(predictions != data_outputs)[0] -num_correct = data_outputs.size - num_wrong.size -accuracy = 100 * (num_correct/data_outputs.size) -print(f"Number of correct classifications : {num_correct}.") -print(f"Number of wrong classifications : {num_wrong.size}.") -print(f"Classification accuracy : {accuracy}.") -``` - -### Regression Example 1 - -The next code listing builds a neural network for regression. Here is what to do to make the code works for regression: - -1. Set the `problem_type` parameter in the `pygad.nn.train()` and `pygad.nn.predict()` functions to the string `"regression"`. - -```python -pygad.nn.train(..., - problem_type="regression") - -predictions = pygad.nn.predict(..., - problem_type="regression") -``` - -2. Set the activation function for the output layer to the string `"None"`. - -```python -output_layer = pygad.nn.DenseLayer(num_neurons=num_outputs, previous_layer=hidden_layer1, activation_function="None") -``` - -3. Calculate the prediction error according to your preferred error function. Here is how the mean absolute error is calculated. - -```python -abs_error = numpy.mean(numpy.abs(predictions - data_outputs)) -print(f"Absolute error : {abs_error}.") -``` - -Here is the complete code. Yet, there is no algorithm used to train the network and thus the network is expected to give bad results. Later, the `pygad.gann` module is used to train either a regression or classification networks. - -```python -import numpy -import pygad.nn - -# Preparing the NumPy array of the inputs. -data_inputs = numpy.array([[2, 5, -3, 0.1], - [8, 15, 20, 13]]) - -# Preparing the NumPy array of the outputs. -data_outputs = numpy.array([0.1, - 1.5]) - -# The number of inputs (i.e. feature vector length) per sample -num_inputs = data_inputs.shape[1] -# Number of outputs per sample -num_outputs = 1 - -HL1_neurons = 2 - -# Building the network architecture. -input_layer = pygad.nn.InputLayer(num_inputs) -hidden_layer1 = pygad.nn.DenseLayer(num_neurons=HL1_neurons, previous_layer=input_layer, activation_function="relu") -output_layer = pygad.nn.DenseLayer(num_neurons=num_outputs, previous_layer=hidden_layer1, activation_function="None") - -# Training the network. -pygad.nn.train(num_epochs=100, - last_layer=output_layer, - data_inputs=data_inputs, - data_outputs=data_outputs, - learning_rate=0.01, - problem_type="regression") - -# Using the trained network for predictions. -predictions = pygad.nn.predict(last_layer=output_layer, - data_inputs=data_inputs, - problem_type="regression") - -# Calculating some statistics -abs_error = numpy.mean(numpy.abs(predictions - data_outputs)) -print(f"Absolute error : {abs_error}.") -``` - -### Regression Example 2 - Fish Weight Prediction - -This example uses the Fish Market Dataset available at Kaggle (https://www.kaggle.com/aungpyaeap/fish-market). Simply download the CSV dataset from [this link](https://www.kaggle.com/aungpyaeap/fish-market/download) (https://www.kaggle.com/aungpyaeap/fish-market/download). The dataset is also available at the [GitHub project of the pygad.nn module](https://github.com/ahmedfgad/NumPyANN): https://github.com/ahmedfgad/NumPyANN - -Using the Pandas library, the dataset is read using the `read_csv()` function. - -```python -data = numpy.array(pandas.read_csv("Fish.csv")) -``` - -The last 5 columns in the dataset are used as inputs and the **Weight** column is used as output. - -```python -# Preparing the NumPy array of the inputs. -data_inputs = numpy.asarray(data[:, 2:], dtype=numpy.float32) - -# Preparing the NumPy array of the outputs. -data_outputs = numpy.asarray(data[:, 1], dtype=numpy.float32) # Fish Weight -``` - -Note how the activation function at the last layer is set to `"None"`. Moreover, the `problem_type` parameter in the `pygad.nn.train()` and `pygad.nn.predict()` functions is set to `"regression"`. - -After the `pygad.nn.train()` function completes, the mean absolute error is calculated. - -```python -abs_error = numpy.mean(numpy.abs(predictions - data_outputs)) -print(f"Absolute error : {abs_error}.") -``` - -Here is the complete code. - -```python -import numpy -import pygad.nn -import pandas - -data = numpy.array(pandas.read_csv("Fish.csv")) - -# Preparing the NumPy array of the inputs. -data_inputs = numpy.asarray(data[:, 2:], dtype=numpy.float32) - -# Preparing the NumPy array of the outputs. -data_outputs = numpy.asarray(data[:, 1], dtype=numpy.float32) # Fish Weight - -# The number of inputs (i.e. feature vector length) per sample -num_inputs = data_inputs.shape[1] -# Number of outputs per sample -num_outputs = 1 - -HL1_neurons = 2 - -# Building the network architecture. -input_layer = pygad.nn.InputLayer(num_inputs) -hidden_layer1 = pygad.nn.DenseLayer(num_neurons=HL1_neurons, previous_layer=input_layer, activation_function="relu") -output_layer = pygad.nn.DenseLayer(num_neurons=num_outputs, previous_layer=hidden_layer1, activation_function="None") - -# Training the network. -pygad.nn.train(num_epochs=100, - last_layer=output_layer, - data_inputs=data_inputs, - data_outputs=data_outputs, - learning_rate=0.01, - problem_type="regression") - -# Using the trained network for predictions. -predictions = pygad.nn.predict(last_layer=output_layer, - data_inputs=data_inputs, - problem_type="regression") - -# Calculating some statistics -abs_error = numpy.mean(numpy.abs(predictions - data_outputs)) -print(f"Absolute error : {abs_error}.") -``` - +::::{grid} 1 2 2 2 +:gutter: 3 + +:::{grid-item-card} XOR Classification +:link: nn_xor +:link-type: doc +::: + +:::{grid-item-card} Image Classification +:link: nn_image_classification +:link-type: doc +::: + +:::{grid-item-card} Regression Example 1 +:link: nn_regression_1 +:link-type: doc +::: + +:::{grid-item-card} Regression Example 2 - Fish Weight Prediction +:link: nn_regression_2 +:link-type: doc +::: + +:::: + +:::{toctree} +:hidden: + +nn_xor +nn_image_classification +nn_regression_1 +nn_regression_2 +::: diff --git a/docs/source/nn_image_classification.md b/docs/source/nn_image_classification.md new file mode 100644 index 0000000..5e2ca76 --- /dev/null +++ b/docs/source/nn_image_classification.md @@ -0,0 +1,52 @@ +# Image Classification + +This example is discussed in the **Steps to Build a Neural Network** section and its complete code is listed below. + +Remember to either download or create the [dataset_features.npy](https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy) and [outputs.npy](https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy) files before running this code. + +```python +import numpy +import pygad.nn + +# Reading the data features. Check the 'extract_features.py' script for extracting the features & preparing the outputs of the dataset. +data_inputs = numpy.load("dataset_features.npy") # Download from https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy + +# Optional step for filtering the features using the standard deviation. +features_STDs = numpy.std(a=data_inputs, axis=0) +data_inputs = data_inputs[:, features_STDs > 50] + +# Reading the data outputs. Check the 'extract_features.py' script for extracting the features & preparing the outputs of the dataset. +data_outputs = numpy.load("outputs.npy") # Download from https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy + +# The number of inputs (i.e. feature vector length) per sample +num_inputs = data_inputs.shape[1] +# Number of outputs per sample +num_outputs = 4 + +HL1_neurons = 150 +HL2_neurons = 60 + +# Building the network architecture. +input_layer = pygad.nn.InputLayer(num_inputs) +hidden_layer1 = pygad.nn.DenseLayer(num_neurons=HL1_neurons, previous_layer=input_layer, activation_function="relu") +hidden_layer2 = pygad.nn.DenseLayer(num_neurons=HL2_neurons, previous_layer=hidden_layer1, activation_function="relu") +output_layer = pygad.nn.DenseLayer(num_neurons=num_outputs, previous_layer=hidden_layer2, activation_function="softmax") + +# Training the network. +pygad.nn.train(num_epochs=10, + last_layer=output_layer, + data_inputs=data_inputs, + data_outputs=data_outputs, + learning_rate=0.01) + +# Using the trained network for predictions. +predictions = pygad.nn.predict(last_layer=output_layer, data_inputs=data_inputs) + +# Calculating some statistics +num_wrong = numpy.where(predictions != data_outputs)[0] +num_correct = data_outputs.size - num_wrong.size +accuracy = 100 * (num_correct/data_outputs.size) +print(f"Number of correct classifications : {num_correct}.") +print(f"Number of wrong classifications : {num_wrong.size}.") +print(f"Classification accuracy : {accuracy}.") +``` diff --git a/docs/source/nn_regression_1.md b/docs/source/nn_regression_1.md new file mode 100644 index 0000000..22d1806 --- /dev/null +++ b/docs/source/nn_regression_1.md @@ -0,0 +1,70 @@ +# Regression Example 1 + +The next code listing builds a neural network for regression. Here is what to do to make the code works for regression: + +1. Set the `problem_type` parameter in the `pygad.nn.train()` and `pygad.nn.predict()` functions to the string `"regression"`. + +```python +pygad.nn.train(..., + problem_type="regression") + +predictions = pygad.nn.predict(..., + problem_type="regression") +``` + +2. Set the activation function for the output layer to the string `"None"`. + +```python +output_layer = pygad.nn.DenseLayer(num_neurons=num_outputs, previous_layer=hidden_layer1, activation_function="None") +``` + +3. Calculate the prediction error according to your preferred error function. Here is how the mean absolute error is calculated. + +```python +abs_error = numpy.mean(numpy.abs(predictions - data_outputs)) +print(f"Absolute error : {abs_error}.") +``` + +Here is the complete code. Yet, there is no algorithm used to train the network and thus the network is expected to give bad results. Later, the `pygad.gann` module is used to train either a regression or classification networks. + +```python +import numpy +import pygad.nn + +# Preparing the NumPy array of the inputs. +data_inputs = numpy.array([[2, 5, -3, 0.1], + [8, 15, 20, 13]]) + +# Preparing the NumPy array of the outputs. +data_outputs = numpy.array([0.1, + 1.5]) + +# The number of inputs (i.e. feature vector length) per sample +num_inputs = data_inputs.shape[1] +# Number of outputs per sample +num_outputs = 1 + +HL1_neurons = 2 + +# Building the network architecture. +input_layer = pygad.nn.InputLayer(num_inputs) +hidden_layer1 = pygad.nn.DenseLayer(num_neurons=HL1_neurons, previous_layer=input_layer, activation_function="relu") +output_layer = pygad.nn.DenseLayer(num_neurons=num_outputs, previous_layer=hidden_layer1, activation_function="None") + +# Training the network. +pygad.nn.train(num_epochs=100, + last_layer=output_layer, + data_inputs=data_inputs, + data_outputs=data_outputs, + learning_rate=0.01, + problem_type="regression") + +# Using the trained network for predictions. +predictions = pygad.nn.predict(last_layer=output_layer, + data_inputs=data_inputs, + problem_type="regression") + +# Calculating some statistics +abs_error = numpy.mean(numpy.abs(predictions - data_outputs)) +print(f"Absolute error : {abs_error}.") +``` diff --git a/docs/source/nn_regression_2.md b/docs/source/nn_regression_2.md new file mode 100644 index 0000000..d78971a --- /dev/null +++ b/docs/source/nn_regression_2.md @@ -0,0 +1,73 @@ +# Regression Example 2 - Fish Weight Prediction + +This example uses the Fish Market Dataset available at Kaggle (https://www.kaggle.com/aungpyaeap/fish-market). Simply download the CSV dataset from [this link](https://www.kaggle.com/aungpyaeap/fish-market/download) (https://www.kaggle.com/aungpyaeap/fish-market/download). The dataset is also available at the [GitHub project of the pygad.nn module](https://github.com/ahmedfgad/NumPyANN): https://github.com/ahmedfgad/NumPyANN + +Using the Pandas library, the dataset is read using the `read_csv()` function. + +```python +data = numpy.array(pandas.read_csv("Fish.csv")) +``` + +The last 5 columns in the dataset are used as inputs and the **Weight** column is used as output. + +```python +# Preparing the NumPy array of the inputs. +data_inputs = numpy.asarray(data[:, 2:], dtype=numpy.float32) + +# Preparing the NumPy array of the outputs. +data_outputs = numpy.asarray(data[:, 1], dtype=numpy.float32) # Fish Weight +``` + +Note how the activation function at the last layer is set to `"None"`. Moreover, the `problem_type` parameter in the `pygad.nn.train()` and `pygad.nn.predict()` functions is set to `"regression"`. + +After the `pygad.nn.train()` function completes, the mean absolute error is calculated. + +```python +abs_error = numpy.mean(numpy.abs(predictions - data_outputs)) +print(f"Absolute error : {abs_error}.") +``` + +Here is the complete code. + +```python +import numpy +import pygad.nn +import pandas + +data = numpy.array(pandas.read_csv("Fish.csv")) + +# Preparing the NumPy array of the inputs. +data_inputs = numpy.asarray(data[:, 2:], dtype=numpy.float32) + +# Preparing the NumPy array of the outputs. +data_outputs = numpy.asarray(data[:, 1], dtype=numpy.float32) # Fish Weight + +# The number of inputs (i.e. feature vector length) per sample +num_inputs = data_inputs.shape[1] +# Number of outputs per sample +num_outputs = 1 + +HL1_neurons = 2 + +# Building the network architecture. +input_layer = pygad.nn.InputLayer(num_inputs) +hidden_layer1 = pygad.nn.DenseLayer(num_neurons=HL1_neurons, previous_layer=input_layer, activation_function="relu") +output_layer = pygad.nn.DenseLayer(num_neurons=num_outputs, previous_layer=hidden_layer1, activation_function="None") + +# Training the network. +pygad.nn.train(num_epochs=100, + last_layer=output_layer, + data_inputs=data_inputs, + data_outputs=data_outputs, + learning_rate=0.01, + problem_type="regression") + +# Using the trained network for predictions. +predictions = pygad.nn.predict(last_layer=output_layer, + data_inputs=data_inputs, + problem_type="regression") + +# Calculating some statistics +abs_error = numpy.mean(numpy.abs(predictions - data_outputs)) +print(f"Absolute error : {abs_error}.") +``` diff --git a/docs/source/nn_xor.md b/docs/source/nn_xor.md new file mode 100644 index 0000000..78480c2 --- /dev/null +++ b/docs/source/nn_xor.md @@ -0,0 +1,50 @@ +# XOR Classification + +This is an example of building a network with 1 hidden layer with 2 neurons for building a network that simulates the XOR logic gate. Because the XOR problem has 2 classes (0 and 1), then the output layer has 2 neurons, one for each class. + +```python +import numpy +import pygad.nn + +# Preparing the NumPy array of the inputs. +data_inputs = numpy.array([[1, 1], + [1, 0], + [0, 1], + [0, 0]]) + +# Preparing the NumPy array of the outputs. +data_outputs = numpy.array([0, + 1, + 1, + 0]) + +# The number of inputs (i.e. feature vector length) per sample +num_inputs = data_inputs.shape[1] +# Number of outputs per sample +num_outputs = 2 + +HL1_neurons = 2 + +# Building the network architecture. +input_layer = pygad.nn.InputLayer(num_inputs) +hidden_layer1 = pygad.nn.DenseLayer(num_neurons=HL1_neurons, previous_layer=input_layer, activation_function="relu") +output_layer = pygad.nn.DenseLayer(num_neurons=num_outputs, previous_layer=hidden_layer1, activation_function="softmax") + +# Training the network. +pygad.nn.train(num_epochs=10, + last_layer=output_layer, + data_inputs=data_inputs, + data_outputs=data_outputs, + learning_rate=0.01) + +# Using the trained network for predictions. +predictions = pygad.nn.predict(last_layer=output_layer, data_inputs=data_inputs) + +# Calculating some statistics +num_wrong = numpy.where(predictions != data_outputs)[0] +num_correct = data_outputs.size - num_wrong.size +accuracy = 100 * (num_correct/data_outputs.size) +print(f"Number of correct classifications : {num_correct}.") +print(f"Number of wrong classifications : {num_wrong.size}.") +print(f"Classification accuracy : {accuracy}.") +``` diff --git a/docs/source/torchga.md b/docs/source/torchga.md index daed089..2546362 100644 --- a/docs/source/torchga.md +++ b/docs/source/torchga.md @@ -110,683 +110,36 @@ It returns the predictions for the data samples. This section gives the complete code of some examples that build and train a PyTorch model using PyGAD. Each subsection builds a different network. -### Example 1: Regression Example - -The next code builds a simple PyTorch model for regression. The next subsections discuss each part in the code. - -```python -import torch -import torchga -import pygad - -def fitness_func(ga_instance, solution, sol_idx): - global data_inputs, data_outputs, torch_ga, model, loss_function - - predictions = pygad.torchga.predict(model=model, - solution=solution, - data=data_inputs) - - abs_error = loss_function(predictions, data_outputs).detach().numpy() + 0.00000001 - - solution_fitness = 1.0 / abs_error - - return solution_fitness - -def on_generation(ga_instance): - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution()[1]}") - -# Create the PyTorch model. -input_layer = torch.nn.Linear(3, 5) -relu_layer = torch.nn.ReLU() -output_layer = torch.nn.Linear(5, 1) - -model = torch.nn.Sequential(input_layer, - relu_layer, - output_layer) -# print(model) - -# Create an instance of the pygad.torchga.TorchGA class to build the initial population. -torch_ga = torchga.TorchGA(model=model, - num_solutions=10) - -loss_function = torch.nn.L1Loss() - -# Data inputs -data_inputs = torch.tensor([[0.02, 0.1, 0.15], - [0.7, 0.6, 0.8], - [1.5, 1.2, 1.7], - [3.2, 2.9, 3.1]]) - -# Data outputs -data_outputs = torch.tensor([[0.1], - [0.6], - [1.3], - [2.5]]) - -# Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class -num_generations = 250 # Number of generations. -num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. -initial_population = torch_ga.population_weights # Initial population of network weights - -ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - on_generation=on_generation) - -ga_instance.run() - -# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. -ga_instance.plot_fitness(title="PyGAD & PyTorch - Iteration vs. Fitness", linewidth=4) - -# Returning the details of the best solution. -solution, solution_fitness, solution_idx = ga_instance.best_solution() -print(f"Fitness value of the best solution = {solution_fitness}") -print(f"Index of the best solution : {solution_idx}") - -# Make predictions based on the best solution. -predictions = pygad.torchga.predict(model=model, - solution=solution, - data=data_inputs) -print("Predictions : \n", predictions.detach().numpy()) - -abs_error = loss_function(predictions, data_outputs) -print("Absolute Error : ", abs_error.detach().numpy()) -``` - -#### Create a PyTorch model - -According to the steps mentioned previously, the first step is to create a PyTorch model. Here is the code that builds the model using the Functional API. - -```python -import torch - -input_layer = torch.nn.Linear(3, 5) -relu_layer = torch.nn.ReLU() -output_layer = torch.nn.Linear(5, 1) - -model = torch.nn.Sequential(input_layer, - relu_layer, - output_layer) -``` - -#### Create an Instance of the `pygad.torchga.TorchGA` Class - -The second step is to create an instance of the `pygad.torchga.TorchGA` class. There are 10 solutions per population. Change this number according to your needs. - -```python -import pygad.torchga - -torch_ga = torchga.TorchGA(model=model, - num_solutions=10) -``` - -#### Prepare the Training Data - -The third step is to prepare the training data inputs and outputs. Here is an example where there are 4 samples. Each sample has 3 inputs and 1 output. - -```python -import numpy - -# Data inputs -data_inputs = numpy.array([[0.02, 0.1, 0.15], - [0.7, 0.6, 0.8], - [1.5, 1.2, 1.7], - [3.2, 2.9, 3.1]]) - -# Data outputs -data_outputs = numpy.array([[0.1], - [0.6], - [1.3], - [2.5]]) -``` - -#### Build the Fitness Function - -The fourth step is to build the fitness function. This function must accept 2 parameters representing the solution and its index within the population. - -The next fitness function calculates the mean absolute error (MAE) of the PyTorch model based on the parameters in the solution. The reciprocal of the MAE is used as the fitness value. Feel free to use any other loss function to calculate the fitness value. - -```python -loss_function = torch.nn.L1Loss() - -def fitness_func(ga_instance, solution, sol_idx): - global data_inputs, data_outputs, torch_ga, model, loss_function - - predictions = pygad.torchga.predict(model=model, - solution=solution, - data=data_inputs) - - abs_error = loss_function(predictions, data_outputs).detach().numpy() + 0.00000001 - - solution_fitness = 1.0 / abs_error - - return solution_fitness -``` - -#### Create an Instance of the `pygad.GA` Class - -The fifth step is to instantiate the `pygad.GA` class. Note how the `initial_population` parameter is assigned to the initial weights of the PyTorch models. - -For more information, please check the [parameters this class accepts](https://pygad.readthedocs.io/en/latest/pygad.html#init). - -```python -# Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class -num_generations = 250 # Number of generations. -num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. -initial_population = torch_ga.population_weights # Initial population of network weights - -ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - on_generation=on_generation) -``` - -#### Run the Genetic Algorithm - -The sixth and last step is to run the genetic algorithm by calling the `run()` method. - -```python -ga_instance.run() -``` - -After PyGAD completes its execution, a figure shows how the fitness value changes by generation. Call the `plot_fitness()` method to show the figure. - -```python -ga_instance.plot_fitness(title="PyGAD & PyTorch - Iteration vs. Fitness", linewidth=4) -``` - -Here is the figure. - -![PyTorch PyGAD XOR Regression 250 Generations](https://user-images.githubusercontent.com/16560492/103469779-22f5b480-4d37-11eb-80dc-95503065ebb1.png) - -To get information about the best solution found by PyGAD, use the `best_solution()` method. - -```python -# Returning the details of the best solution. -solution, solution_fitness, solution_idx = ga_instance.best_solution() -print(f"Fitness value of the best solution = {solution_fitness}") -print(f"Index of the best solution : {solution_idx}") -``` - -```python -Fitness value of the best solution = 145.42425295191546 -Index of the best solution : 0 -``` - -The next code restores the trained model weights using the `model_weights_as_dict()` function. The restored weights are used to calculate the predicted values. - -```python -predictions = pygad.torchga.predict(model=model, - solution=solution, - data=data_inputs) -print("Predictions : \n", predictions.detach().numpy()) -``` - -```python -Predictions : -[[0.08401088] - [0.60939324] - [1.3010881 ] - [2.5010352 ]] -``` - -The next code measures the trained model error. - -```python -abs_error = loss_function(predictions, data_outputs) -print("Absolute Error : ", abs_error.detach().numpy()) -``` - -``` -Absolute Error : 0.006876422 -``` - -### Example 2: XOR Binary Classification - -The next code creates a PyTorch model to build the XOR binary classification problem. Let's highlight the changes compared to the previous example. - -```python -import torch -import torchga -import pygad - -def fitness_func(ga_instance, solution, sol_idx): - global data_inputs, data_outputs, torch_ga, model, loss_function - - predictions = pygad.torchga.predict(model=model, - solution=solution, - data=data_inputs) - - solution_fitness = 1.0 / (loss_function(predictions, data_outputs).detach().numpy() + 0.00000001) - - return solution_fitness - -def on_generation(ga_instance): - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution()[1]}") - -# Create the PyTorch model. -input_layer = torch.nn.Linear(2, 4) -relu_layer = torch.nn.ReLU() -dense_layer = torch.nn.Linear(4, 2) -output_layer = torch.nn.Softmax(1) - -model = torch.nn.Sequential(input_layer, - relu_layer, - dense_layer, - output_layer) -# print(model) - -# Create an instance of the pygad.torchga.TorchGA class to build the initial population. -torch_ga = torchga.TorchGA(model=model, - num_solutions=10) - -loss_function = torch.nn.BCELoss() - -# XOR problem inputs -data_inputs = torch.tensor([[0.0, 0.0], - [0.0, 1.0], - [1.0, 0.0], - [1.0, 1.0]]) - -# XOR problem outputs -data_outputs = torch.tensor([[1.0, 0.0], - [0.0, 1.0], - [0.0, 1.0], - [1.0, 0.0]]) - -# Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class -num_generations = 250 # Number of generations. -num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. -initial_population = torch_ga.population_weights # Initial population of network weights. - -# Create an instance of the pygad.GA class -ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - on_generation=on_generation) - -# Start the genetic algorithm evolution. -ga_instance.run() - -# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. -ga_instance.plot_fitness(title="PyGAD & PyTorch - Iteration vs. Fitness", linewidth=4) - -# Returning the details of the best solution. -solution, solution_fitness, solution_idx = ga_instance.best_solution() -print(f"Fitness value of the best solution = {solution_fitness}") -print(f"Index of the best solution : {solution_idx}") - -# Make predictions based on the best solution. -predictions = pygad.torchga.predict(model=model, - solution=solution, - data=data_inputs) -print("Predictions : \n", predictions.detach().numpy()) - -# Calculate the binary crossentropy for the trained model. -print("Binary Crossentropy : ", loss_function(predictions, data_outputs).detach().numpy()) - -# Calculate the classification accuracy of the trained model. -a = torch.max(predictions, axis=1) -b = torch.max(data_outputs, axis=1) -accuracy = torch.sum(a.indices == b.indices) / len(data_outputs) -print("Accuracy : ", accuracy.detach().numpy()) -``` - -Compared to the previous regression example, here are the changes: - -* The PyTorch model is changed according to the nature of the problem. Now, it has 2 inputs and 2 outputs with an in-between hidden layer of 4 neurons. - -```python -input_layer = torch.nn.Linear(2, 4) -relu_layer = torch.nn.ReLU() -dense_layer = torch.nn.Linear(4, 2) -output_layer = torch.nn.Softmax(1) - -model = torch.nn.Sequential(input_layer, - relu_layer, - dense_layer, - output_layer) -``` - -* The train data is changed. Note that the output of each sample is a 1D vector of 2 values, 1 for each class. - -```python -# XOR problem inputs -data_inputs = torch.tensor([[0.0, 0.0], - [0.0, 1.0], - [1.0, 0.0], - [1.0, 1.0]]) - -# XOR problem outputs -data_outputs = torch.tensor([[1.0, 0.0], - [0.0, 1.0], - [0.0, 1.0], - [1.0, 0.0]]) -``` - -* The fitness value is calculated based on the binary cross entropy. - -```python -loss_function = torch.nn.BCELoss() -``` - -After the previous code completes, the next figure shows how the fitness value change by generation. - -![PyTorch PyGAD XOR Classification 250 Generations](https://user-images.githubusercontent.com/16560492/103469818-c646c980-4d37-11eb-98c3-d9d591acd5e2.png) - -Here is some information about the trained model. Its fitness value is `100000000.0`, loss is `0.0` and accuracy is 100%. - -```python -Fitness value of the best solution = 100000000.0 - -Index of the best solution : 0 - -Predictions : -[[1.0000000e+00 1.3627675e-10] - [3.8521746e-09 1.0000000e+00] - [4.2789325e-10 1.0000000e+00] - [1.0000000e+00 3.3668417e-09]] - -Binary Crossentropy : 0.0 - -Accuracy : 1.0 -``` - -### Example 3: Image Multi-Class Classification (Dense Layers) - -Here is the code. - -```python -import torch -import torchga -import pygad -import numpy - -def fitness_func(ga_instance, solution, sol_idx): - global data_inputs, data_outputs, torch_ga, model, loss_function - - predictions = pygad.torchga.predict(model=model, - solution=solution, - data=data_inputs) - - solution_fitness = 1.0 / (loss_function(predictions, data_outputs).detach().numpy() + 0.00000001) - - return solution_fitness - -def on_generation(ga_instance): - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution()[1]}") - -# Build the PyTorch model using the functional API. -input_layer = torch.nn.Linear(360, 50) -relu_layer = torch.nn.ReLU() -dense_layer = torch.nn.Linear(50, 4) -output_layer = torch.nn.Softmax(1) - -model = torch.nn.Sequential(input_layer, - relu_layer, - dense_layer, - output_layer) - -# Create an instance of the pygad.torchga.TorchGA class to build the initial population. -torch_ga = torchga.TorchGA(model=model, - num_solutions=10) - -loss_function = torch.nn.CrossEntropyLoss() - -# Data inputs -data_inputs = torch.from_numpy(numpy.load("dataset_features.npy")).float() - -# Data outputs -data_outputs = torch.from_numpy(numpy.load("outputs.npy")).long() -# The next 2 lines are equivelant to this Keras function to perform 1-hot encoding: tensorflow.keras.utils.to_categorical(data_outputs) -# temp_outs = numpy.zeros((data_outputs.shape[0], numpy.unique(data_outputs).size), dtype=numpy.uint8) -# temp_outs[numpy.arange(data_outputs.shape[0]), numpy.uint8(data_outputs)] = 1 - -# Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class -num_generations = 200 # Number of generations. -num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. -initial_population = torch_ga.population_weights # Initial population of network weights. - -# Create an instance of the pygad.GA class -ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - on_generation=on_generation) - -# Start the genetic algorithm evolution. -ga_instance.run() - -# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. -ga_instance.plot_fitness(title="PyGAD & PyTorch - Iteration vs. Fitness", linewidth=4) - -# Returning the details of the best solution. -solution, solution_fitness, solution_idx = ga_instance.best_solution() -print(f"Fitness value of the best solution = {solution_fitness}") -print(f"Index of the best solution : {solution_idx}") - -# Fetch the parameters of the best solution. -best_solution_weights = torchga.model_weights_as_dict(model=model, - weights_vector=solution) -model.load_state_dict(best_solution_weights) -predictions = model(data_inputs) -# print("Predictions : \n", predictions) - -# Calculate the crossentropy loss of the trained model. -print("Crossentropy : ", loss_function(predictions, data_outputs).detach().numpy()) - -# Calculate the classification accuracy for the trained model. -accuracy = torch.sum(torch.max(predictions, axis=1).indices == data_outputs) / len(data_outputs) -print("Accuracy : ", accuracy.detach().numpy()) -``` - -Compared to the previous binary classification example, this example has multiple classes (4) and thus the loss is measured using cross entropy. - -```python -loss_function = torch.nn.CrossEntropyLoss() -``` - -#### Prepare the Training Data - -Before building and training neural networks, the training data (input and output) needs to be prepared. The inputs and the outputs of the training data are NumPy arrays. - -The data used in this example is available as 2 files: - -1. [dataset_features.npy](https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy): Data inputs. https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy -2. [outputs.npy](https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy): Class labels. https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy - -The data consists of 4 classes of images. The image shape is `(100, 100, 3)`. The number of training samples is 1962. The feature vector extracted from each image has a length 360. - -```python -import numpy - -data_inputs = numpy.load("dataset_features.npy") - -data_outputs = numpy.load("outputs.npy") -``` - -The next figure shows how the fitness value changes. - -![PyTorch PyGAD Dense Image Classification 200 Generations](https://user-images.githubusercontent.com/16560492/103469855-5d138600-4d38-11eb-84b1-b5eff8faa7bc.png) - -Here are some statistics about the trained model. - -``` -Fitness value of the best solution = 1.3446997034434534 -Index of the best solution : 0 -Crossentropy : 0.74366045 -Accuracy : 1.0 -``` - -### Example 4: Image Multi-Class Classification (Conv Layers) - -Compared to the previous example that uses only dense layers, this example uses convolutional layers to classify the same dataset. - -Here is the complete code. - -```python -import torch -import torchga -import pygad -import numpy - -def fitness_func(ga_instance, solution, sol_idx): - global data_inputs, data_outputs, torch_ga, model, loss_function - - predictions = pygad.torchga.predict(model=model, - solution=solution, - data=data_inputs) - - solution_fitness = 1.0 / (loss_function(predictions, data_outputs).detach().numpy() + 0.00000001) - - return solution_fitness - -def on_generation(ga_instance): - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution()[1]}") - -# Build the PyTorch model. -input_layer = torch.nn.Conv2d(in_channels=3, out_channels=5, kernel_size=7) -relu_layer1 = torch.nn.ReLU() -max_pool1 = torch.nn.MaxPool2d(kernel_size=5, stride=5) - -conv_layer2 = torch.nn.Conv2d(in_channels=5, out_channels=3, kernel_size=3) -relu_layer2 = torch.nn.ReLU() - -flatten_layer1 = torch.nn.Flatten() -# The value 768 is pre-computed by tracing the sizes of the layers' outputs. -dense_layer1 = torch.nn.Linear(in_features=768, out_features=15) -relu_layer3 = torch.nn.ReLU() - -dense_layer2 = torch.nn.Linear(in_features=15, out_features=4) -output_layer = torch.nn.Softmax(1) - -model = torch.nn.Sequential(input_layer, - relu_layer1, - max_pool1, - conv_layer2, - relu_layer2, - flatten_layer1, - dense_layer1, - relu_layer3, - dense_layer2, - output_layer) - -# Create an instance of the pygad.torchga.TorchGA class to build the initial population. -torch_ga = torchga.TorchGA(model=model, - num_solutions=10) - -loss_function = torch.nn.CrossEntropyLoss() - -# Data inputs -data_inputs = torch.from_numpy(numpy.load("dataset_inputs.npy")).float() -data_inputs = data_inputs.reshape((data_inputs.shape[0], data_inputs.shape[3], data_inputs.shape[1], data_inputs.shape[2])) - -# Data outputs -data_outputs = torch.from_numpy(numpy.load("dataset_outputs.npy")).long() - -# Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class -num_generations = 200 # Number of generations. -num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. -initial_population = torch_ga.population_weights # Initial population of network weights. - -# Create an instance of the pygad.GA class -ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - initial_population=initial_population, - fitness_func=fitness_func, - on_generation=on_generation) - -# Start the genetic algorithm evolution. -ga_instance.run() - -# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. -ga_instance.plot_fitness(title="PyGAD & PyTorch - Iteration vs. Fitness", linewidth=4) - -# Returning the details of the best solution. -solution, solution_fitness, solution_idx = ga_instance.best_solution() -print(f"Fitness value of the best solution = {solution_fitness}") -print(f"Index of the best solution : {solution_idx}") - -# Make predictions based on the best solution. -predictions = pygad.torchga.predict(model=model, - solution=solution, - data=data_inputs) -# print("Predictions : \n", predictions) - -# Calculate the crossentropy for the trained model. -print("Crossentropy : ", loss_function(predictions, data_outputs).detach().numpy()) - -# Calculate the classification accuracy for the trained model. -accuracy = torch.sum(torch.max(predictions, axis=1).indices == data_outputs) / len(data_outputs) -print("Accuracy : ", accuracy.detach().numpy()) -``` - -Compared to the previous example, the only change is that the architecture uses convolutional and max-pooling layers. The shape of each input sample is 100x100x3. - -```python -input_layer = torch.nn.Conv2d(in_channels=3, out_channels=5, kernel_size=7) -relu_layer1 = torch.nn.ReLU() -max_pool1 = torch.nn.MaxPool2d(kernel_size=5, stride=5) - -conv_layer2 = torch.nn.Conv2d(in_channels=5, out_channels=3, kernel_size=3) -relu_layer2 = torch.nn.ReLU() - -flatten_layer1 = torch.nn.Flatten() -# The value 768 is pre-computed by tracing the sizes of the layers' outputs. -dense_layer1 = torch.nn.Linear(in_features=768, out_features=15) -relu_layer3 = torch.nn.ReLU() - -dense_layer2 = torch.nn.Linear(in_features=15, out_features=4) -output_layer = torch.nn.Softmax(1) - -model = torch.nn.Sequential(input_layer, - relu_layer1, - max_pool1, - conv_layer2, - relu_layer2, - flatten_layer1, - dense_layer1, - relu_layer3, - dense_layer2, - output_layer) -``` - -#### Prepare the Training Data - -The data used in this example is available as 2 files: - -1. [dataset_inputs.npy](https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_inputs.npy): Data inputs. https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_inputs.npy -2. [dataset_outputs.npy](https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_outputs.npy): Class labels. https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_outputs.npy - -The data consists of 4 classes of images. The image shape is `(100, 100, 3)` and there are 20 images per class for a total of 80 training samples. For more information about the dataset, check the [Reading the Data](https://pygad.readthedocs.io/en/latest/cnn.html#reading-the-data) section of the `pygad.cnn` module. - -Simply download these 2 files and read them according to the next code. - -```python -import numpy - -data_inputs = numpy.load("dataset_inputs.npy") - -data_outputs = numpy.load("dataset_outputs.npy") -``` - -The next figure shows how the fitness value changes. - -![PyTorch PyGAD CNN Image Classification 200 Generations](https://user-images.githubusercontent.com/16560492/103469887-c7c4c180-4d38-11eb-98a7-1c5e73e918d0.png) - -Here are some statistics about the trained model. The model accuracy is 97.5% after the 200 generations. Note that just running the code again may give different results. - -``` -Fitness value of the best solution = 1.3009520689219258 -Index of the best solution : 0 -Crossentropy : 0.7686678 -Accuracy : 0.975 -``` - +::::{grid} 1 2 2 2 +:gutter: 3 + +:::{grid-item-card} Example 1: Regression Example +:link: torchga_regression +:link-type: doc +::: + +:::{grid-item-card} Example 2: XOR Binary Classification +:link: torchga_xor +:link-type: doc +::: + +:::{grid-item-card} Example 3: Image Multi-Class Classification (Dense Layers) +:link: torchga_image_dense +:link-type: doc +::: + +:::{grid-item-card} Example 4: Image Multi-Class Classification (Conv Layers) +:link: torchga_image_conv +:link-type: doc +::: + +:::: + +:::{toctree} +:hidden: + +torchga_regression +torchga_xor +torchga_image_dense +torchga_image_conv +::: diff --git a/docs/source/torchga_image_conv.md b/docs/source/torchga_image_conv.md new file mode 100644 index 0000000..951dd6c --- /dev/null +++ b/docs/source/torchga_image_conv.md @@ -0,0 +1,165 @@ +# Example 4: Image Multi-Class Classification (Conv Layers) + +Compared to the previous example that uses only dense layers, this example uses convolutional layers to classify the same dataset. + +Here is the complete code. + +```python +import torch +import torchga +import pygad +import numpy + +def fitness_func(ga_instance, solution, sol_idx): + global data_inputs, data_outputs, torch_ga, model, loss_function + + predictions = pygad.torchga.predict(model=model, + solution=solution, + data=data_inputs) + + solution_fitness = 1.0 / (loss_function(predictions, data_outputs).detach().numpy() + 0.00000001) + + return solution_fitness + +def on_generation(ga_instance): + print(f"Generation = {ga_instance.generations_completed}") + print(f"Fitness = {ga_instance.best_solution()[1]}") + +# Build the PyTorch model. +input_layer = torch.nn.Conv2d(in_channels=3, out_channels=5, kernel_size=7) +relu_layer1 = torch.nn.ReLU() +max_pool1 = torch.nn.MaxPool2d(kernel_size=5, stride=5) + +conv_layer2 = torch.nn.Conv2d(in_channels=5, out_channels=3, kernel_size=3) +relu_layer2 = torch.nn.ReLU() + +flatten_layer1 = torch.nn.Flatten() +# The value 768 is pre-computed by tracing the sizes of the layers' outputs. +dense_layer1 = torch.nn.Linear(in_features=768, out_features=15) +relu_layer3 = torch.nn.ReLU() + +dense_layer2 = torch.nn.Linear(in_features=15, out_features=4) +output_layer = torch.nn.Softmax(1) + +model = torch.nn.Sequential(input_layer, + relu_layer1, + max_pool1, + conv_layer2, + relu_layer2, + flatten_layer1, + dense_layer1, + relu_layer3, + dense_layer2, + output_layer) + +# Create an instance of the pygad.torchga.TorchGA class to build the initial population. +torch_ga = torchga.TorchGA(model=model, + num_solutions=10) + +loss_function = torch.nn.CrossEntropyLoss() + +# Data inputs +data_inputs = torch.from_numpy(numpy.load("dataset_inputs.npy")).float() +data_inputs = data_inputs.reshape((data_inputs.shape[0], data_inputs.shape[3], data_inputs.shape[1], data_inputs.shape[2])) + +# Data outputs +data_outputs = torch.from_numpy(numpy.load("dataset_outputs.npy")).long() + +# Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class +num_generations = 200 # Number of generations. +num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. +initial_population = torch_ga.population_weights # Initial population of network weights. + +# Create an instance of the pygad.GA class +ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=num_parents_mating, + initial_population=initial_population, + fitness_func=fitness_func, + on_generation=on_generation) + +# Start the genetic algorithm evolution. +ga_instance.run() + +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. +ga_instance.plot_fitness(title="PyGAD & PyTorch - Iteration vs. Fitness", linewidth=4) + +# Returning the details of the best solution. +solution, solution_fitness, solution_idx = ga_instance.best_solution() +print(f"Fitness value of the best solution = {solution_fitness}") +print(f"Index of the best solution : {solution_idx}") + +# Make predictions based on the best solution. +predictions = pygad.torchga.predict(model=model, + solution=solution, + data=data_inputs) +# print("Predictions : \n", predictions) + +# Calculate the crossentropy for the trained model. +print("Crossentropy : ", loss_function(predictions, data_outputs).detach().numpy()) + +# Calculate the classification accuracy for the trained model. +accuracy = torch.sum(torch.max(predictions, axis=1).indices == data_outputs) / len(data_outputs) +print("Accuracy : ", accuracy.detach().numpy()) +``` + +Compared to the previous example, the only change is that the architecture uses convolutional and max-pooling layers. The shape of each input sample is 100x100x3. + +```python +input_layer = torch.nn.Conv2d(in_channels=3, out_channels=5, kernel_size=7) +relu_layer1 = torch.nn.ReLU() +max_pool1 = torch.nn.MaxPool2d(kernel_size=5, stride=5) + +conv_layer2 = torch.nn.Conv2d(in_channels=5, out_channels=3, kernel_size=3) +relu_layer2 = torch.nn.ReLU() + +flatten_layer1 = torch.nn.Flatten() +# The value 768 is pre-computed by tracing the sizes of the layers' outputs. +dense_layer1 = torch.nn.Linear(in_features=768, out_features=15) +relu_layer3 = torch.nn.ReLU() + +dense_layer2 = torch.nn.Linear(in_features=15, out_features=4) +output_layer = torch.nn.Softmax(1) + +model = torch.nn.Sequential(input_layer, + relu_layer1, + max_pool1, + conv_layer2, + relu_layer2, + flatten_layer1, + dense_layer1, + relu_layer3, + dense_layer2, + output_layer) +``` + +## Prepare the Training Data + +The data used in this example is available as 2 files: + +1. [dataset_inputs.npy](https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_inputs.npy): Data inputs. https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_inputs.npy +2. [dataset_outputs.npy](https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_outputs.npy): Class labels. https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_outputs.npy + +The data consists of 4 classes of images. The image shape is `(100, 100, 3)` and there are 20 images per class for a total of 80 training samples. For more information about the dataset, check the [Reading the Data](https://pygad.readthedocs.io/en/latest/cnn.html#reading-the-data) section of the `pygad.cnn` module. + +Simply download these 2 files and read them according to the next code. + +```python +import numpy + +data_inputs = numpy.load("dataset_inputs.npy") + +data_outputs = numpy.load("dataset_outputs.npy") +``` + +The next figure shows how the fitness value changes. + +![PyTorch PyGAD CNN Image Classification 200 Generations](https://user-images.githubusercontent.com/16560492/103469887-c7c4c180-4d38-11eb-98a7-1c5e73e918d0.png) + +Here are some statistics about the trained model. The model accuracy is 97.5% after the 200 generations. Note that just running the code again may give different results. + +``` +Fitness value of the best solution = 1.3009520689219258 +Index of the best solution : 0 +Crossentropy : 0.7686678 +Accuracy : 0.975 +``` diff --git a/docs/source/torchga_image_dense.md b/docs/source/torchga_image_dense.md new file mode 100644 index 0000000..f9f530b --- /dev/null +++ b/docs/source/torchga_image_dense.md @@ -0,0 +1,126 @@ +# Example 3: Image Multi-Class Classification (Dense Layers) + +Here is the code. + +```python +import torch +import torchga +import pygad +import numpy + +def fitness_func(ga_instance, solution, sol_idx): + global data_inputs, data_outputs, torch_ga, model, loss_function + + predictions = pygad.torchga.predict(model=model, + solution=solution, + data=data_inputs) + + solution_fitness = 1.0 / (loss_function(predictions, data_outputs).detach().numpy() + 0.00000001) + + return solution_fitness + +def on_generation(ga_instance): + print(f"Generation = {ga_instance.generations_completed}") + print(f"Fitness = {ga_instance.best_solution()[1]}") + +# Build the PyTorch model using the functional API. +input_layer = torch.nn.Linear(360, 50) +relu_layer = torch.nn.ReLU() +dense_layer = torch.nn.Linear(50, 4) +output_layer = torch.nn.Softmax(1) + +model = torch.nn.Sequential(input_layer, + relu_layer, + dense_layer, + output_layer) + +# Create an instance of the pygad.torchga.TorchGA class to build the initial population. +torch_ga = torchga.TorchGA(model=model, + num_solutions=10) + +loss_function = torch.nn.CrossEntropyLoss() + +# Data inputs +data_inputs = torch.from_numpy(numpy.load("dataset_features.npy")).float() + +# Data outputs +data_outputs = torch.from_numpy(numpy.load("outputs.npy")).long() +# The next 2 lines are equivelant to this Keras function to perform 1-hot encoding: tensorflow.keras.utils.to_categorical(data_outputs) +# temp_outs = numpy.zeros((data_outputs.shape[0], numpy.unique(data_outputs).size), dtype=numpy.uint8) +# temp_outs[numpy.arange(data_outputs.shape[0]), numpy.uint8(data_outputs)] = 1 + +# Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class +num_generations = 200 # Number of generations. +num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. +initial_population = torch_ga.population_weights # Initial population of network weights. + +# Create an instance of the pygad.GA class +ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=num_parents_mating, + initial_population=initial_population, + fitness_func=fitness_func, + on_generation=on_generation) + +# Start the genetic algorithm evolution. +ga_instance.run() + +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. +ga_instance.plot_fitness(title="PyGAD & PyTorch - Iteration vs. Fitness", linewidth=4) + +# Returning the details of the best solution. +solution, solution_fitness, solution_idx = ga_instance.best_solution() +print(f"Fitness value of the best solution = {solution_fitness}") +print(f"Index of the best solution : {solution_idx}") + +# Fetch the parameters of the best solution. +best_solution_weights = torchga.model_weights_as_dict(model=model, + weights_vector=solution) +model.load_state_dict(best_solution_weights) +predictions = model(data_inputs) +# print("Predictions : \n", predictions) + +# Calculate the crossentropy loss of the trained model. +print("Crossentropy : ", loss_function(predictions, data_outputs).detach().numpy()) + +# Calculate the classification accuracy for the trained model. +accuracy = torch.sum(torch.max(predictions, axis=1).indices == data_outputs) / len(data_outputs) +print("Accuracy : ", accuracy.detach().numpy()) +``` + +Compared to the previous binary classification example, this example has multiple classes (4) and thus the loss is measured using cross entropy. + +```python +loss_function = torch.nn.CrossEntropyLoss() +``` + +## Prepare the Training Data + +Before building and training neural networks, the training data (input and output) needs to be prepared. The inputs and the outputs of the training data are NumPy arrays. + +The data used in this example is available as 2 files: + +1. [dataset_features.npy](https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy): Data inputs. https://github.com/ahmedfgad/NumPyANN/blob/master/dataset_features.npy +2. [outputs.npy](https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy): Class labels. https://github.com/ahmedfgad/NumPyANN/blob/master/outputs.npy + +The data consists of 4 classes of images. The image shape is `(100, 100, 3)`. The number of training samples is 1962. The feature vector extracted from each image has a length 360. + +```python +import numpy + +data_inputs = numpy.load("dataset_features.npy") + +data_outputs = numpy.load("outputs.npy") +``` + +The next figure shows how the fitness value changes. + +![PyTorch PyGAD Dense Image Classification 200 Generations](https://user-images.githubusercontent.com/16560492/103469855-5d138600-4d38-11eb-84b1-b5eff8faa7bc.png) + +Here are some statistics about the trained model. + +``` +Fitness value of the best solution = 1.3446997034434534 +Index of the best solution : 0 +Crossentropy : 0.74366045 +Accuracy : 1.0 +``` diff --git a/docs/source/torchga_regression.md b/docs/source/torchga_regression.md new file mode 100644 index 0000000..d66ad83 --- /dev/null +++ b/docs/source/torchga_regression.md @@ -0,0 +1,233 @@ +# Example 1: Regression Example + +The next code builds a simple PyTorch model for regression. The next subsections discuss each part in the code. + +```python +import torch +import torchga +import pygad + +def fitness_func(ga_instance, solution, sol_idx): + global data_inputs, data_outputs, torch_ga, model, loss_function + + predictions = pygad.torchga.predict(model=model, + solution=solution, + data=data_inputs) + + abs_error = loss_function(predictions, data_outputs).detach().numpy() + 0.00000001 + + solution_fitness = 1.0 / abs_error + + return solution_fitness + +def on_generation(ga_instance): + print(f"Generation = {ga_instance.generations_completed}") + print(f"Fitness = {ga_instance.best_solution()[1]}") + +# Create the PyTorch model. +input_layer = torch.nn.Linear(3, 5) +relu_layer = torch.nn.ReLU() +output_layer = torch.nn.Linear(5, 1) + +model = torch.nn.Sequential(input_layer, + relu_layer, + output_layer) +# print(model) + +# Create an instance of the pygad.torchga.TorchGA class to build the initial population. +torch_ga = torchga.TorchGA(model=model, + num_solutions=10) + +loss_function = torch.nn.L1Loss() + +# Data inputs +data_inputs = torch.tensor([[0.02, 0.1, 0.15], + [0.7, 0.6, 0.8], + [1.5, 1.2, 1.7], + [3.2, 2.9, 3.1]]) + +# Data outputs +data_outputs = torch.tensor([[0.1], + [0.6], + [1.3], + [2.5]]) + +# Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class +num_generations = 250 # Number of generations. +num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. +initial_population = torch_ga.population_weights # Initial population of network weights + +ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=num_parents_mating, + initial_population=initial_population, + fitness_func=fitness_func, + on_generation=on_generation) + +ga_instance.run() + +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. +ga_instance.plot_fitness(title="PyGAD & PyTorch - Iteration vs. Fitness", linewidth=4) + +# Returning the details of the best solution. +solution, solution_fitness, solution_idx = ga_instance.best_solution() +print(f"Fitness value of the best solution = {solution_fitness}") +print(f"Index of the best solution : {solution_idx}") + +# Make predictions based on the best solution. +predictions = pygad.torchga.predict(model=model, + solution=solution, + data=data_inputs) +print("Predictions : \n", predictions.detach().numpy()) + +abs_error = loss_function(predictions, data_outputs) +print("Absolute Error : ", abs_error.detach().numpy()) +``` + +## Create a PyTorch model + +According to the steps mentioned previously, the first step is to create a PyTorch model. Here is the code that builds the model using the Functional API. + +```python +import torch + +input_layer = torch.nn.Linear(3, 5) +relu_layer = torch.nn.ReLU() +output_layer = torch.nn.Linear(5, 1) + +model = torch.nn.Sequential(input_layer, + relu_layer, + output_layer) +``` + +## Create an Instance of the `pygad.torchga.TorchGA` Class + +The second step is to create an instance of the `pygad.torchga.TorchGA` class. There are 10 solutions per population. Change this number according to your needs. + +```python +import pygad.torchga + +torch_ga = torchga.TorchGA(model=model, + num_solutions=10) +``` + +## Prepare the Training Data + +The third step is to prepare the training data inputs and outputs. Here is an example where there are 4 samples. Each sample has 3 inputs and 1 output. + +```python +import numpy + +# Data inputs +data_inputs = numpy.array([[0.02, 0.1, 0.15], + [0.7, 0.6, 0.8], + [1.5, 1.2, 1.7], + [3.2, 2.9, 3.1]]) + +# Data outputs +data_outputs = numpy.array([[0.1], + [0.6], + [1.3], + [2.5]]) +``` + +## Build the Fitness Function + +The fourth step is to build the fitness function. This function must accept 2 parameters representing the solution and its index within the population. + +The next fitness function calculates the mean absolute error (MAE) of the PyTorch model based on the parameters in the solution. The reciprocal of the MAE is used as the fitness value. Feel free to use any other loss function to calculate the fitness value. + +```python +loss_function = torch.nn.L1Loss() + +def fitness_func(ga_instance, solution, sol_idx): + global data_inputs, data_outputs, torch_ga, model, loss_function + + predictions = pygad.torchga.predict(model=model, + solution=solution, + data=data_inputs) + + abs_error = loss_function(predictions, data_outputs).detach().numpy() + 0.00000001 + + solution_fitness = 1.0 / abs_error + + return solution_fitness +``` + +## Create an Instance of the `pygad.GA` Class + +The fifth step is to instantiate the `pygad.GA` class. Note how the `initial_population` parameter is assigned to the initial weights of the PyTorch models. + +For more information, please check the [parameters this class accepts](https://pygad.readthedocs.io/en/latest/pygad.html#init). + +```python +# Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class +num_generations = 250 # Number of generations. +num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. +initial_population = torch_ga.population_weights # Initial population of network weights + +ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=num_parents_mating, + initial_population=initial_population, + fitness_func=fitness_func, + on_generation=on_generation) +``` + +## Run the Genetic Algorithm + +The sixth and last step is to run the genetic algorithm by calling the `run()` method. + +```python +ga_instance.run() +``` + +After PyGAD completes its execution, a figure shows how the fitness value changes by generation. Call the `plot_fitness()` method to show the figure. + +```python +ga_instance.plot_fitness(title="PyGAD & PyTorch - Iteration vs. Fitness", linewidth=4) +``` + +Here is the figure. + +![PyTorch PyGAD XOR Regression 250 Generations](https://user-images.githubusercontent.com/16560492/103469779-22f5b480-4d37-11eb-80dc-95503065ebb1.png) + +To get information about the best solution found by PyGAD, use the `best_solution()` method. + +```python +# Returning the details of the best solution. +solution, solution_fitness, solution_idx = ga_instance.best_solution() +print(f"Fitness value of the best solution = {solution_fitness}") +print(f"Index of the best solution : {solution_idx}") +``` + +```python +Fitness value of the best solution = 145.42425295191546 +Index of the best solution : 0 +``` + +The next code restores the trained model weights using the `model_weights_as_dict()` function. The restored weights are used to calculate the predicted values. + +```python +predictions = pygad.torchga.predict(model=model, + solution=solution, + data=data_inputs) +print("Predictions : \n", predictions.detach().numpy()) +``` + +```python +Predictions : +[[0.08401088] + [0.60939324] + [1.3010881 ] + [2.5010352 ]] +``` + +The next code measures the trained model error. + +```python +abs_error = loss_function(predictions, data_outputs) +print("Absolute Error : ", abs_error.detach().numpy()) +``` + +``` +Absolute Error : 0.006876422 +``` diff --git a/docs/source/torchga_xor.md b/docs/source/torchga_xor.md new file mode 100644 index 0000000..19017f7 --- /dev/null +++ b/docs/source/torchga_xor.md @@ -0,0 +1,152 @@ +# Example 2: XOR Binary Classification + +The next code creates a PyTorch model to build the XOR binary classification problem. Let's highlight the changes compared to the previous example. + +```python +import torch +import torchga +import pygad + +def fitness_func(ga_instance, solution, sol_idx): + global data_inputs, data_outputs, torch_ga, model, loss_function + + predictions = pygad.torchga.predict(model=model, + solution=solution, + data=data_inputs) + + solution_fitness = 1.0 / (loss_function(predictions, data_outputs).detach().numpy() + 0.00000001) + + return solution_fitness + +def on_generation(ga_instance): + print(f"Generation = {ga_instance.generations_completed}") + print(f"Fitness = {ga_instance.best_solution()[1]}") + +# Create the PyTorch model. +input_layer = torch.nn.Linear(2, 4) +relu_layer = torch.nn.ReLU() +dense_layer = torch.nn.Linear(4, 2) +output_layer = torch.nn.Softmax(1) + +model = torch.nn.Sequential(input_layer, + relu_layer, + dense_layer, + output_layer) +# print(model) + +# Create an instance of the pygad.torchga.TorchGA class to build the initial population. +torch_ga = torchga.TorchGA(model=model, + num_solutions=10) + +loss_function = torch.nn.BCELoss() + +# XOR problem inputs +data_inputs = torch.tensor([[0.0, 0.0], + [0.0, 1.0], + [1.0, 0.0], + [1.0, 1.0]]) + +# XOR problem outputs +data_outputs = torch.tensor([[1.0, 0.0], + [0.0, 1.0], + [0.0, 1.0], + [1.0, 0.0]]) + +# Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class +num_generations = 250 # Number of generations. +num_parents_mating = 5 # Number of solutions to be selected as parents in the mating pool. +initial_population = torch_ga.population_weights # Initial population of network weights. + +# Create an instance of the pygad.GA class +ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=num_parents_mating, + initial_population=initial_population, + fitness_func=fitness_func, + on_generation=on_generation) + +# Start the genetic algorithm evolution. +ga_instance.run() + +# After the generations complete, a plot is shown that summarizes how the fitness values evolve over the generations. +ga_instance.plot_fitness(title="PyGAD & PyTorch - Iteration vs. Fitness", linewidth=4) + +# Returning the details of the best solution. +solution, solution_fitness, solution_idx = ga_instance.best_solution() +print(f"Fitness value of the best solution = {solution_fitness}") +print(f"Index of the best solution : {solution_idx}") + +# Make predictions based on the best solution. +predictions = pygad.torchga.predict(model=model, + solution=solution, + data=data_inputs) +print("Predictions : \n", predictions.detach().numpy()) + +# Calculate the binary crossentropy for the trained model. +print("Binary Crossentropy : ", loss_function(predictions, data_outputs).detach().numpy()) + +# Calculate the classification accuracy of the trained model. +a = torch.max(predictions, axis=1) +b = torch.max(data_outputs, axis=1) +accuracy = torch.sum(a.indices == b.indices) / len(data_outputs) +print("Accuracy : ", accuracy.detach().numpy()) +``` + +Compared to the previous regression example, here are the changes: + +* The PyTorch model is changed according to the nature of the problem. Now, it has 2 inputs and 2 outputs with an in-between hidden layer of 4 neurons. + +```python +input_layer = torch.nn.Linear(2, 4) +relu_layer = torch.nn.ReLU() +dense_layer = torch.nn.Linear(4, 2) +output_layer = torch.nn.Softmax(1) + +model = torch.nn.Sequential(input_layer, + relu_layer, + dense_layer, + output_layer) +``` + +* The train data is changed. Note that the output of each sample is a 1D vector of 2 values, 1 for each class. + +```python +# XOR problem inputs +data_inputs = torch.tensor([[0.0, 0.0], + [0.0, 1.0], + [1.0, 0.0], + [1.0, 1.0]]) + +# XOR problem outputs +data_outputs = torch.tensor([[1.0, 0.0], + [0.0, 1.0], + [0.0, 1.0], + [1.0, 0.0]]) +``` + +* The fitness value is calculated based on the binary cross entropy. + +```python +loss_function = torch.nn.BCELoss() +``` + +After the previous code completes, the next figure shows how the fitness value change by generation. + +![PyTorch PyGAD XOR Classification 250 Generations](https://user-images.githubusercontent.com/16560492/103469818-c646c980-4d37-11eb-98c3-d9d591acd5e2.png) + +Here is some information about the trained model. Its fitness value is `100000000.0`, loss is `0.0` and accuracy is 100%. + +```python +Fitness value of the best solution = 100000000.0 + +Index of the best solution : 0 + +Predictions : +[[1.0000000e+00 1.3627675e-10] + [3.8521746e-09 1.0000000e+00] + [4.2789325e-10 1.0000000e+00] + [1.0000000e+00 3.3668417e-09]] + +Binary Crossentropy : 0.0 + +Accuracy : 1.0 +``` From e6940a93554534afeef6d8e27e79a9b123a794ba Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 21 May 2026 16:01:10 -0400 Subject: [PATCH 35/42] docs: document passing a class for the fitness, operators, and callbacks Extend the "User-Defined Functions and Callbacks" page with an "Assign a Class" section. A class whose instances implement __call__ can be passed for fitness_func, the crossover/mutation/parent-selection operators, and the on_* callbacks. The __call__ method takes the same parameters as the function form (self does not count), and a class instance can keep state across generations because the same instance is reused. Includes the example from examples/example_lifecycle_classes.py. --- docs/source/custom_functions.md | 79 ++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/docs/source/custom_functions.md b/docs/source/custom_functions.md index 3d547f9..1644670 100644 --- a/docs/source/custom_functions.md +++ b/docs/source/custom_functions.md @@ -11,10 +11,13 @@ In PyGAD 2.19.0, it is possible to pass user-defined functions or methods to the 7. `on_generation` 8. `on_stop` -This section gives 2 examples of how to build these handlers using: +You can also pass a class instance for any of these parameters. The same 3 options (function, method, or class) work for the operator parameters `crossover_type`, `mutation_type`, and `parent_selection_type`. See the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/utils.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more about the operators. + +This section gives 3 examples of how to build these handlers using: 1. Functions. 2. Methods. +3. Classes. ## Assign Functions @@ -114,3 +117,77 @@ ga_instance = pygad.GA(num_generations=5, ga_instance.run() ``` + +## Assign a Class + +Besides functions and methods, you can pass an instance of a class. The class must implement the `__call__()` method, which makes its instances callable like a function. PyGAD calls the instance the same way it calls a function. + +The `__call__()` method must accept the same parameters as the matching function. The `self` parameter does not count. For example, the `__call__()` method of a fitness class accepts `self` plus the same 3 parameters as a fitness function: the instance of the `pygad.GA` class, a solution, and its index. + +A class is useful when the handler needs to keep state across generations. Because the same instance is reused for every call, any data you store in its attributes (for example, in the `__init__()` method) stays available across all the generations. + +The next example builds the fitness function, the crossover and mutation operators, and all the callbacks as classes. An instance of each class is passed to the matching parameter. + +```python +import pygad +import numpy + +class Fitness: + def __call__(self, ga_instance, solution, solution_idx): + fitness = numpy.sum(solution) + return fitness + +class Crossover: + def __call__(self, parents, offspring_size, ga_instance): + return numpy.random.rand(offspring_size[0], offspring_size[1]) + +class Mutation: + def __call__(self, offspring, ga_instance): + return offspring + +class OnStart: + def __call__(self, ga_instance): + print("on_start") + +class OnFitness: + def __call__(self, ga_instance, fitness): + print("on_fitness") + +class OnParents: + def __call__(self, ga_instance, parents): + print("on_parents") + +class OnCrossover: + def __call__(self, ga_instance, offspring): + print("on_crossover") + +class OnMutation: + def __call__(self, ga_instance, offspring): + print("on_mutation") + +class OnGeneration: + def __call__(self, ga_instance): + print("on_generation") + +class OnStop: + def __call__(self, ga_instance, fitness): + print("on_stop") + +ga_instance = pygad.GA(num_generations=10, + num_parents_mating=5, + sol_per_pop=10, + num_genes=5, + fitness_func=Fitness(), + crossover_type=Crossover(), + mutation_type=Mutation(), + on_start=OnStart(), + on_fitness=OnFitness(), + on_parents=OnParents(), + on_crossover=OnCrossover(), + on_mutation=OnMutation(), + on_generation=OnGeneration(), + on_stop=OnStop(), + suppress_warnings=True) + +ga_instance.run() +``` From bfa230a5170fa1c40dcaa23b638aeac4e2da95f2 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 21 May 2026 16:04:33 -0400 Subject: [PATCH 36/42] docs: rename the custom-functions page title to include classes Rename the page heading to "Use Functions, Methods, and Classes to Build Fitness and Callbacks" so the title reflects the new class option. Repoint the two release-note links to the updated anchor and update the matching card on the More About PyGAD landing page. --- docs/source/custom_functions.md | 2 +- docs/source/pygad_more.md | 4 ++-- docs/source/releases.md | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/source/custom_functions.md b/docs/source/custom_functions.md index 1644670..23a0233 100644 --- a/docs/source/custom_functions.md +++ b/docs/source/custom_functions.md @@ -1,4 +1,4 @@ -# Use Functions and Methods to Build Fitness and Callbacks +# Use Functions, Methods, and Classes to Build Fitness and Callbacks In PyGAD 2.19.0, it is possible to pass user-defined functions or methods to the following parameters: diff --git a/docs/source/pygad_more.md b/docs/source/pygad_more.md index 89611e0..ea17ddd 100644 --- a/docs/source/pygad_more.md +++ b/docs/source/pygad_more.md @@ -40,11 +40,11 @@ Parallel processing, batch fitness, reusing fitness, and non-deterministic probl Print a Keras-like summary and log the outputs. ::: -:::{grid-item-card} User-Defined Functions and Callbacks +:::{grid-item-card} User-Defined Functions, Methods, and Classes :link: custom_functions :link-type: doc -Pass your own functions or methods for the fitness and callbacks. +Pass your own functions, methods, or classes for the fitness and callbacks. ::: :::: diff --git a/docs/source/releases.md b/docs/source/releases.md index 461d09b..e5e1ceb 100644 --- a/docs/source/releases.md +++ b/docs/source/releases.md @@ -470,8 +470,8 @@ Release Date 8 April 2023 6. The `pygad.visualize.plot` module has a class named `Plot` where all the methods that create plots exist. The `pygad.GA` class extends this class. 7. Support of using the `logging` module to log the outputs to both the console and text file instead of using the `print()` function. This is by assigning the `logging.Logger` to the new `logger` parameter. Check the [Logging Outputs](https://pygad.readthedocs.io/en/latest/logging.html#logging-outputs) for more information. 8. A new instance attribute called `logger` to save the logger. - 9. The function/method passed to the `fitness_func` parameter accepts a new parameter that refers to the instance of the `pygad.GA` class. Check this for an example: [Use Functions and Methods to Build Fitness Function and Callbacks](https://pygad.readthedocs.io/en/latest/custom_functions.html#use-functions-and-methods-to-build-fitness-and-callbacks). https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/163 - 10. Update the documentation to include an example of using functions and methods to calculate the fitness and build callbacks. Check this for more details: [Use Functions and Methods to Build Fitness Function and Callbacks](https://pygad.readthedocs.io/en/latest/custom_functions.html#use-functions-and-methods-to-build-fitness-and-callbacks). https://github.com/ahmedfgad/GeneticAlgorithmPython/pull/92#issuecomment-1443635003 + 9. The function/method passed to the `fitness_func` parameter accepts a new parameter that refers to the instance of the `pygad.GA` class. Check this for an example: [Use Functions and Methods to Build Fitness Function and Callbacks](https://pygad.readthedocs.io/en/latest/custom_functions.html#use-functions-methods-and-classes-to-build-fitness-and-callbacks). https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/163 + 10. Update the documentation to include an example of using functions and methods to calculate the fitness and build callbacks. Check this for more details: [Use Functions and Methods to Build Fitness Function and Callbacks](https://pygad.readthedocs.io/en/latest/custom_functions.html#use-functions-methods-and-classes-to-build-fitness-and-callbacks). https://github.com/ahmedfgad/GeneticAlgorithmPython/pull/92#issuecomment-1443635003 11. Validate the value passed to the `initial_population` parameter. 12. Validate the type and length of the `pop_fitness` parameter of the `best_solution()` method. 13. Some edits in the documentation. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/106 From 8df05b45dec74909dbeb70268581f5a917652a40 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 21 May 2026 16:16:27 -0400 Subject: [PATCH 37/42] docs: scroll the Furo sidebar to the active page Add _static/scroll-sidebar.js (registered via html_js_files) so the left sidebar scrolls to center the current page on load, instead of always showing the top. It adjusts only the sidebar's own scroll position. --- docs/source/_static/scroll-sidebar.js | 14 ++++++++++++++ docs/source/conf.py | 1 + 2 files changed, 15 insertions(+) create mode 100644 docs/source/_static/scroll-sidebar.js diff --git a/docs/source/_static/scroll-sidebar.js b/docs/source/_static/scroll-sidebar.js new file mode 100644 index 0000000..833f363 --- /dev/null +++ b/docs/source/_static/scroll-sidebar.js @@ -0,0 +1,14 @@ +// Scroll the Furo left sidebar so the active page is centered in view. +// Furo keeps the sidebar at the top by default, so on a tall sidebar the +// current page can be off-screen. This adjusts only the sidebar's own +// scroll position (not the main window). +window.addEventListener("DOMContentLoaded", function () { + var box = document.querySelector(".sidebar-scroll"); + var active = document.querySelector(".sidebar-tree .current-page"); + if (!box || !active) { + return; + } + var target = active.querySelector("a") || active; + var delta = target.getBoundingClientRect().top - box.getBoundingClientRect().top; + box.scrollTop += delta - box.clientHeight / 2 + target.clientHeight / 2; +}); diff --git a/docs/source/conf.py b/docs/source/conf.py index 4cc2296..412c3ee 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -59,6 +59,7 @@ html_title = 'PyGAD' html_static_path = ['_static'] html_css_files = ['custom.css'] +html_js_files = ['scroll-sidebar.js'] html_theme_options = { 'light_css_variables': { From 247a379adf7562ba87f888a3423855a180882f2e Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 21 May 2026 16:23:30 -0400 Subject: [PATCH 38/42] docs: split pygad, utils, and help into focused pages Restructure the three remaining long pages, following the established landing-plus-children pattern: - pygad.md keeps the pygad.GA class API reference and module functions; the "Steps to Use PyGAD" walkthrough, "Life Cycle of PyGAD", and the three worked examples move to their own pages, reached from card grids and a nested toctree. Clustering and CoinTex stay as short pointers. - utils.md keeps the operator submodule reference; "Adaptive Mutation" and "User-Defined Crossover, Mutation, and Parent Selection Operators" move to their own pages. - help.md becomes a short landing page linking to four themed pages: Getting Help, Tutorials and Resources, Projects and Research, and PyGAD in Other Languages. All internal links to moved sections are repointed to their new pages; moved heading text is unchanged so section anchors are preserved. --- docs/source/adaptive_mutation.md | 106 +++ docs/source/custom_functions.md | 2 +- docs/source/fitness_calculation.md | 2 +- docs/source/help.md | 398 +---------- docs/source/help_languages.md | 81 +++ docs/source/help_projects.md | 83 +++ docs/source/help_support.md | 64 ++ docs/source/help_tutorials.md | 156 ++++ docs/source/lifecycle.md | 94 +++ docs/source/pygad.md | 673 ++---------------- docs/source/pygad_example_linear.md | 69 ++ docs/source/pygad_example_multi_objective.md | 88 +++ .../pygad_example_reproducing_images.md | 183 +++++ docs/source/releases.md | 4 +- docs/source/steps_to_use.md | 202 ++++++ docs/source/user_defined_operators.md | 332 +++++++++ docs/source/utils.md | 453 +----------- 17 files changed, 1554 insertions(+), 1436 deletions(-) create mode 100644 docs/source/adaptive_mutation.md create mode 100644 docs/source/help_languages.md create mode 100644 docs/source/help_projects.md create mode 100644 docs/source/help_support.md create mode 100644 docs/source/help_tutorials.md create mode 100644 docs/source/lifecycle.md create mode 100644 docs/source/pygad_example_linear.md create mode 100644 docs/source/pygad_example_multi_objective.md create mode 100644 docs/source/pygad_example_reproducing_images.md create mode 100644 docs/source/steps_to_use.md create mode 100644 docs/source/user_defined_operators.md diff --git a/docs/source/adaptive_mutation.md b/docs/source/adaptive_mutation.md new file mode 100644 index 0000000..6c52f81 --- /dev/null +++ b/docs/source/adaptive_mutation.md @@ -0,0 +1,106 @@ +# Adaptive Mutation + +In the regular genetic algorithm, mutation uses a single fixed mutation rate for all solutions, regardless of their fitness values. So, no matter whether a solution has high or low quality, the same number of genes is mutated every time. + +The pitfalls of using a constant mutation rate for all solutions are summarized in this paper [Libelli, S. Marsili, and P. Alba. "Adaptive mutation in genetic algorithms." *Soft computing* 4.2 (2000): 76-80](https://idp.springer.com/authorize/casa?redirect_uri=https://link.springer.com/content/pdf/10.1007/s005000000042.pdf&casa_token=IT4NfJUvslcAAAAA:VegHW6tm2fe3e0R9cRKjuGKkKWXJTQSfNMT6z0kGbMsAllyK1NrEY3cEWg8bj7AJWEQPaqWIJxmHNBHg) as follows: + +> The weak point of "classical" GAs is the total randomness of mutation, which is applied equally to all chromosomes, irrespective of their fitness. Thus a very good chromosome is equally likely to be disrupted by mutation as a bad one. +> +> On the other hand, bad chromosomes are less likely to produce good ones through crossover, because of their lack of building blocks, until they remain unchanged. They would benefit the most from mutation and could be used to spread throughout the parameter space to increase the search thoroughness. So there are two conflicting needs in determining the best probability of mutation. +> +> Usually, a reasonable compromise in the case of a constant mutation is to keep the probability low to avoid disruption of good chromosomes, but this would prevent a high mutation rate of low-fitness chromosomes. Thus a constant probability of mutation would probably miss both goals and result in a slow improvement of the population. + +According to [Libelli, S. Marsili, and P. Alba.](https://idp.springer.com/authorize/casa?redirect_uri=https://link.springer.com/content/pdf/10.1007/s005000000042.pdf&casa_token=IT4NfJUvslcAAAAA:VegHW6tm2fe3e0R9cRKjuGKkKWXJTQSfNMT6z0kGbMsAllyK1NrEY3cEWg8bj7AJWEQPaqWIJxmHNBHg) work, the adaptive mutation solves the problems of constant mutation. + +Adaptive mutation works as follows: + +1. Calculate the average fitness value of the population (`f_avg`). +2. For each chromosome, calculate its fitness value (`f`). +3. If `ff_avg`, then this solution is regarded as a high-quality solution and thus the mutation rate should be kept low to avoid disrupting this high quality solution. + +In PyGAD, if `f=f_avg`, then the solution is regarded as high quality. + +The next figure summarizes the previous steps. + +![Adaptive-Mutation](https://user-images.githubusercontent.com/16560492/103468973-e3c26600-4d2c-11eb-8af3-b3bb39b50540.jpg) + +This strategy is applied in PyGAD. + +## Use Adaptive Mutation in PyGAD + +In [PyGAD 2.10.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-10-0), adaptive mutation is supported. To use it, just follow the following 2 simple steps: + +1. In the constructor of the `pygad.GA` class, set `mutation_type="adaptive"` to specify that the type of mutation is adaptive. +2. Specify the mutation rates for the low and high quality solutions using one of these 3 parameters according to your preference: `mutation_probability`, `mutation_num_genes`, and `mutation_percent_genes`. Please check the [documentation of each of these parameters](https://pygad.readthedocs.io/en/latest/pygad.html#init) for more information. + +When adaptive mutation is used, then the value assigned to any of the 3 parameters can be of any of these data types: + +1. `list` +2. `tuple` +3. `numpy.ndarray` + +Whatever the data type used, the length of the `list`, `tuple`, or the `numpy.ndarray` must be exactly 2. That is there are just 2 values: + +1. The first value is the mutation rate for the low-quality solutions. +2. The second value is the mutation rate for the high-quality solutions. + +PyGAD expects that the first value is higher than the second value and thus a warning is printed in case the first value is lower than the second one. + +Here are some examples to feed the mutation rates: + +```python +# mutation_probability +mutation_probability = [0.25, 0.1] +mutation_probability = (0.35, 0.17) +mutation_probability = numpy.array([0.15, 0.05]) + +# mutation_num_genes +mutation_num_genes = [4, 2] +mutation_num_genes = (3, 1) +mutation_num_genes = numpy.array([7, 2]) + +# mutation_percent_genes +mutation_percent_genes = [25, 12] +mutation_percent_genes = (15, 8) +mutation_percent_genes = numpy.array([21, 13]) +``` + +Assume that the average fitness is 12 and the fitness values of 2 solutions are 15 and 7. If the mutation probabilities are specified as follows: + +```python +mutation_probability = [0.25, 0.1] +``` + +Then the mutation probability of the first solution is 0.1 because its fitness is 15 which is higher than the average fitness 12. The mutation probability of the second solution is 0.25 because its fitness is 7 which is lower than the average fitness 12. + +Here is an example that uses adaptive mutation. + +```python +import pygad +import numpy + +function_inputs = [4,-2,3.5,5,-11,-4.7] # Function inputs. +desired_output = 44 # Function output. + +def fitness_func(ga_instance, solution, solution_idx): + # The fitness function calculates the sum of products between each input and its corresponding weight. + output = numpy.sum(solution*function_inputs) + # The value 0.000001 is used to avoid the Inf value when the denominator numpy.abs(output - desired_output) is 0.0. + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + return fitness + +# Creating an instance of the GA class inside the ga module. Some parameters are initialized within the constructor. +ga_instance = pygad.GA(num_generations=200, + fitness_func=fitness_func, + num_parents_mating=10, + sol_per_pop=20, + num_genes=len(function_inputs), + mutation_type="adaptive", + mutation_num_genes=(3, 1)) + +# Running the GA to optimize the parameters of the function. +ga_instance.run() + +ga_instance.plot_fitness(title="PyGAD with Adaptive Mutation", linewidth=5) +``` diff --git a/docs/source/custom_functions.md b/docs/source/custom_functions.md index 23a0233..6b3297e 100644 --- a/docs/source/custom_functions.md +++ b/docs/source/custom_functions.md @@ -11,7 +11,7 @@ In PyGAD 2.19.0, it is possible to pass user-defined functions or methods to the 7. `on_generation` 8. `on_stop` -You can also pass a class instance for any of these parameters. The same 3 options (function, method, or class) work for the operator parameters `crossover_type`, `mutation_type`, and `parent_selection_type`. See the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/utils.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more about the operators. +You can also pass a class instance for any of these parameters. The same 3 options (function, method, or class) work for the operator parameters `crossover_type`, `mutation_type`, and `parent_selection_type`. See the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/user_defined_operators.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more about the operators. This section gives 3 examples of how to build these handlers using: diff --git a/docs/source/fitness_calculation.md b/docs/source/fitness_calculation.md index a053ae4..00ca848 100644 --- a/docs/source/fitness_calculation.md +++ b/docs/source/fitness_calculation.md @@ -6,7 +6,7 @@ This page covers how PyGAD calculates the fitness efficiently: parallel processi Starting from [PyGAD 2.17.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-17-0), parallel processing is supported. This section explains how to use parallel processing in PyGAD. -According to the [PyGAD life cycle](https://pygad.readthedocs.io/en/latest/pygad.html#life-cycle-of-pygad), the computation can be parallelized in only 2 operations: +According to the [PyGAD life cycle](https://pygad.readthedocs.io/en/latest/lifecycle.html#life-cycle-of-pygad), the computation can be parallelized in only 2 operations: 1. Population fitness calculation. 2. Mutation. diff --git a/docs/source/help.md b/docs/source/help.md index 4282fe0..25edf0a 100644 --- a/docs/source/help.md +++ b/docs/source/help.md @@ -1,381 +1,45 @@ # Help & Resources -This page collects extra information about PyGAD: where to get help, how to contribute, and links to tutorials, research papers, and projects that use PyGAD. +This section collects extra information about PyGAD: where to get help, how to contribute, and resources that use or explain PyGAD. Pick a topic: -## PyGAD Projects at GitHub +::::{grid} 1 2 2 2 +:gutter: 3 -The PyGAD library is available at PyPI at this page https://pypi.org/project/pygad. PyGAD is built out of a number of open-source GitHub projects. A brief note about these projects is given in the next subsections. +:::{grid-item-card} Getting Help +:link: help_support +:link-type: doc -### [GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython) +Submit issues, request features, ask on Stack Overflow, and contact us. +::: -GitHub Link: https://github.com/ahmedfgad/GeneticAlgorithmPython +:::{grid-item-card} Tutorials and Resources +:link: help_tutorials +:link-type: doc -[**GeneticAlgorithmPython**](https://github.com/ahmedfgad/GeneticAlgorithmPython) is the first project which is an open-source Python 3 project for implementing the genetic algorithm based on NumPy. +Tutorials, articles, and a book about PyGAD. +::: -### [NumPyANN](https://github.com/ahmedfgad/NumPyANN) +:::{grid-item-card} Projects and Research +:link: help_projects +:link-type: doc -GitHub Link: https://github.com/ahmedfgad/NumPyANN +PyGAD projects, projects built with PyGAD, and research papers. +::: -[**NumPyANN**](https://github.com/ahmedfgad/NumPyANN) builds artificial neural networks in **Python 3** using **NumPy** from scratch. The purpose of this project is to only implement the **forward pass** of a neural network without using a training algorithm. Currently, it only supports classification and later regression will be also supported. Moreover, only one class is supported per sample. +:::{grid-item-card} PyGAD in Other Languages +:link: help_languages +:link-type: doc -### [NeuralGenetic](https://github.com/ahmedfgad/NeuralGenetic) +Read about PyGAD in several languages. +::: -GitHub Link: https://github.com/ahmedfgad/NeuralGenetic +:::: -[NeuralGenetic](https://github.com/ahmedfgad/NeuralGenetic) trains neural networks using the genetic algorithm based on the previous 2 projects [GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython) and [NumPyANN](https://github.com/ahmedfgad/NumPyANN). +:::{toctree} +:hidden: -### [NumPyCNN](https://github.com/ahmedfgad/NumPyCNN) - -GitHub Link: https://github.com/ahmedfgad/NumPyCNN - -[NumPyCNN](https://github.com/ahmedfgad/NumPyCNN) builds convolutional neural networks using NumPy. The purpose of this project is to only implement the **forward pass** of a convolutional neural network without using a training algorithm. - -### [CNNGenetic](https://github.com/ahmedfgad/CNNGenetic) - -GitHub Link: https://github.com/ahmedfgad/CNNGenetic - -[CNNGenetic](https://github.com/ahmedfgad/CNNGenetic) trains convolutional neural networks using the genetic algorithm. It uses the [GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython) project for building the genetic algorithm. - -### [KerasGA](https://github.com/ahmedfgad/KerasGA) - -GitHub Link: https://github.com/ahmedfgad/KerasGA - -[KerasGA](https://github.com/ahmedfgad/KerasGA) trains [Keras](https://keras.io) models using the genetic algorithm. It uses the [GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython) project for building the genetic algorithm. - -### [TorchGA](https://github.com/ahmedfgad/TorchGA) - -GitHub Link: https://github.com/ahmedfgad/TorchGA - -[TorchGA](https://github.com/ahmedfgad/TorchGA) trains [PyTorch](https://pytorch.org) models using the genetic algorithm. It uses the [GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython) project for building the genetic algorithm. - -[pygad.torchga](https://github.com/ahmedfgad/TorchGA): https://github.com/ahmedfgad/TorchGA - -## Stackoverflow Questions about PyGAD - -### [How do I proceed to load a ga_instance as “.pkl” format in PyGad?](https://stackoverflow.com/questions/67424181/how-do-i-proceed-to-load-a-ga-instance-as-pkl-format-in-pygad) - -### [Binary Classification NN Model Weights not being Trained in PyGAD](https://stackoverflow.com/questions/67276696/binary-classification-nn-model-weights-not-being-trained-in-pygad) - -### [How to solve TSP problem using pyGAD package?](https://stackoverflow.com/questions/66298595/how-to-solve-tsp-problem-using-pygad-package) - -### [How can I save a matplotlib plot that is the output of a function in jupyter?](https://stackoverflow.com/questions/66055330/how-can-i-save-a-matplotlib-plot-that-is-the-output-of-a-function-in-jupyter) - -### [How do I query the best solution of a pyGAD GA instance?](https://stackoverflow.com/questions/65757722/how-do-i-query-the-best-solution-of-a-pygad-ga-instance) - -### [Multi-Input Multi-Output in Genetic algorithm (python)](https://stackoverflow.com/questions/64943711/multi-input-multi-output-in-genetic-algorithm-python) - -https://www.linkedin.com/pulse/validation-short-term-parametric-trading-model-genetic-landolfi - -https://itchef.ru/articles/397758 - -https://audhiaprilliant.medium.com/genetic-algorithm-based-clustering-algorithm-in-searching-robust-initial-centroids-for-k-means-e3b4d892a4be - -https://python.plainenglish.io/validation-of-a-short-term-parametric-trading-model-with-genetic-optimization-and-walk-forward-89708b789af6 - -https://ichi.pro/ko/pygadwa-hamkke-yujeon-algolijeum-eul-sayonghayeo-keras-model-eul-hunlyeonsikineun-bangbeob-173299286377169 - -https://ichi.pro/tr/pygad-ile-genetik-algoritmayi-kullanarak-keras-modelleri-nasil-egitilir-173299286377169 - -https://ichi.pro/ru/kak-obucit-modeli-keras-s-pomos-u-geneticeskogo-algoritma-s-pygad-173299286377169 - -https://blog.csdn.net/sinat_38079265/article/details/108449614 - - - -## Submitting Issues - -If there is an issue using PyGAD, then use any of your preferred option to discuss that issue. - -One way is [submitting an issue](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/new) into this GitHub project ([github.com/ahmedfgad/GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython)) in case something is not working properly or to ask for questions. - -If this is not a proper option for you, then check the [**Contact Us**](https://pygad.readthedocs.io/en/latest/help.html#contact-us) section for more contact details. - -## Ask for Feature - -PyGAD is actively developed with the goal of building a dynamic library for suporting a wide-range of problems to be optimized using the genetic algorithm. - -To ask for a new feature, either [submit an issue](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/new) into this GitHub project ([github.com/ahmedfgad/GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython)) or send an e-mail to ahmed.f.gad@gmail.com. - -Also check the [**Contact Us**](https://pygad.readthedocs.io/en/latest/help.html#contact-us) section for more contact details. - -## Projects Built using PyGAD - -If you created a project that uses PyGAD, then we can support you by mentioning this project here in PyGAD's documentation. - -To do that, please send a message at ahmed.f.gad@gmail.com or check the [**Contact Us**](https://pygad.readthedocs.io/en/latest/help.html#contact-us) section for more contact details. - -Within your message, please send the following details: - -- Project title -- Brief description -- Preferably, a link that directs the readers to your project - -## Tutorials about PyGAD - -### [Adaptive Mutation in Genetic Algorithm with Python Examples](https://neptune.ai/blog/adaptive-mutation-in-genetic-algorithm-with-python-examples) - -In this tutorial, we’ll see why mutation with a fixed number of genes is bad, and how to replace it with adaptive mutation. Using the [PyGAD Python 3 library](https://pygad.readthedocs.io/), we’ll discuss a few examples that use both random and adaptive mutation. - -### [Clustering Using the Genetic Algorithm in Python](https://blog.paperspace.com/clustering-using-the-genetic-algorithm) - -This tutorial discusses how the genetic algorithm is used to cluster data, starting from random clusters and running until the optimal clusters are found. We'll start by briefly revising the K-means clustering algorithm to point out its weak points, which are later solved by the genetic algorithm. The code examples in this tutorial are implemented in Python using the [PyGAD library](https://pygad.readthedocs.io/). - -### [Working with Different Genetic Algorithm Representations in Python](https://blog.paperspace.com/working-with-different-genetic-algorithm-representations-python) - -Depending on the nature of the problem being optimized, the genetic algorithm (GA) supports two different gene representations: binary, and decimal. The binary GA has only two values for its genes, which are 0 and 1. This is easier to manage as its gene values are limited compared to the decimal GA, for which we can use different formats like float or integer, and limited or unlimited ranges. - -This tutorial discusses how the [PyGAD](https://pygad.readthedocs.io/) library supports the two GA representations, binary and decimal. - -### [5 Genetic Algorithm Applications Using PyGAD](https://blog.paperspace.com/genetic-algorithm-applications-using-pygad) - -This tutorial introduces PyGAD, an open-source Python library for implementing the genetic algorithm and training machine learning algorithms. PyGAD supports 19 parameters for customizing the genetic algorithm for various applications. - -Within this tutorial we'll discuss 5 different applications of the genetic algorithm and build them using PyGAD. - -### [Train Neural Networks Using a Genetic Algorithm in Python with PyGAD](https://heartbeat.fritz.ai/train-neural-networks-using-a-genetic-algorithm-in-python-with-pygad-862905048429?gi=ba58ee6b4bbd) - -The genetic algorithm (GA) is a biologically-inspired optimization algorithm. It has in recent years gained importance, as it’s simple while also solving complex problems like travel route optimization, training machine learning algorithms, working with single and multi-objective problems, game playing, and more. - -Deep neural networks are inspired by the idea of how the biological brain works. It’s a universal function approximator, which is capable of simulating any function, and is now used to solve the most complex problems in machine learning. What’s more, they’re able to work with all types of data (images, audio, video, and text). - -Both genetic algorithms (GAs) and neural networks (NNs) are similar, as both are biologically-inspired techniques. This similarity motivates us to create a hybrid of both to see whether a GA can train NNs with high accuracy. - -This tutorial uses [PyGAD](https://pygad.readthedocs.io/), a Python library that supports building and training NNs using a GA. [PyGAD](https://pygad.readthedocs.io/) offers both classification and regression NNs. - -### [Building a Game-Playing Agent for CoinTex Using the Genetic Algorithm](https://blog.paperspace.com/building-agent-for-cointex-using-genetic-algorithm) - -In this tutorial we'll see how to build a game-playing agent using only the genetic algorithm to play a game called [CoinTex](https://play.google.com/store/apps/details?id=coin.tex.cointexreactfast&hl=en), which is developed in the Kivy Python framework. The objective of CoinTex is to collect the randomly distributed coins while avoiding collision with fire and monsters (that move randomly). The source code of CoinTex can be found [on GitHub](https://github.com/ahmedfgad/CoinTex). - -The genetic algorithm is the only AI used here; there is no other machine/deep learning model used with it. We'll implement the genetic algorithm using [PyGad](https://blog.paperspace.com/genetic-algorithm-applications-using-pygad/). This tutorial starts with a quick overview of CoinTex followed by a brief explanation of the genetic algorithm, and how it can be used to create the playing agent. Finally, we'll see how to implement these ideas in Python. - -The source code of the genetic algorithm agent is available [here](https://github.com/ahmedfgad/CoinTex/tree/master/PlayerGA), and you can download the code used in this tutorial from [here](https://github.com/ahmedfgad/CoinTex/tree/master/PlayerGA/TutorialProject). - -### [How To Train Keras Models Using the Genetic Algorithm with PyGAD](https://blog.paperspace.com/train-keras-models-using-genetic-algorithm-with-pygad) - -PyGAD is an open-source Python library for building the genetic algorithm and training machine learning algorithms. It offers a wide range of parameters to customize the genetic algorithm to work with different types of problems. - -PyGAD has its own modules that support building and training neural networks (NNs) and convolutional neural networks (CNNs). Despite these modules working well, they are implemented in Python without any additional optimization measures. This leads to comparatively high computational times for even simple problems. - -The latest PyGAD version, 2.8.0 (released on 20 September 2020), supports a new module to train Keras models. Even though Keras is built in Python, it's fast. The reason is that Keras uses TensorFlow as a backend, and TensorFlow is highly optimized. - -This tutorial discusses how to train Keras models using PyGAD. The discussion includes building Keras models using either the Sequential Model or the Functional API, building an initial population of Keras model parameters, creating an appropriate fitness function, and more. - -[![PyGAD+Keras](https://user-images.githubusercontent.com/16560492/111009628-2b372500-8362-11eb-90cf-01b47d831624.png)](https://blog.paperspace.com/train-keras-models-using-genetic-algorithm-with-pygad) - -### [Train PyTorch Models Using Genetic Algorithm with PyGAD](https://neptune.ai/blog/train-pytorch-models-using-genetic-algorithm-with-pygad) - -[PyGAD](https://pygad.readthedocs.io/) is a genetic algorithm Python 3 library for solving optimization problems. One of these problems is training machine learning algorithms. - -PyGAD has a module called [pygad.kerasga](https://github.com/ahmedfgad/KerasGA). It trains Keras models using the genetic algorithm. On January 3rd, 2021, a new release of [PyGAD 2.10.0](https://pygad.readthedocs.io/) brought a new module called [pygad.torchga](https://github.com/ahmedfgad/TorchGA) to train PyTorch models. It’s very easy to use, but there are a few tricky steps. - -So, in this tutorial, we’ll explore how to use PyGAD to train PyTorch models. - -[![PyGAD+PyTorch](https://user-images.githubusercontent.com/16560492/111009678-5457b580-8362-11eb-899a-39e2f96984df.png)](https://neptune.ai/blog/train-pytorch-models-using-genetic-algorithm-with-pygad) - -### [A Guide to Genetic ‘Learning’ Algorithms for Optimization](https://towardsdatascience.com/a-guide-to-genetic-learning-algorithms-for-optimization-e1067cdc77e7) - -## PyGAD in Other Languages - -### French - -[Cómo los algoritmos genéticos pueden competir con el descenso de gradiente y el backprop](https://www.hebergementwebs.com/nouvelles/comment-les-algorithmes-genetiques-peuvent-rivaliser-avec-la-descente-de-gradient-et-le-backprop) - -Bien que la manière standard d'entraîner les réseaux de neurones soit la descente de gradient et la rétropropagation, il y a d'autres joueurs dans le jeu. L'un d'eux est les algorithmes évolutionnaires, tels que les algorithmes génétiques. - -Utiliser un algorithme génétique pour former un réseau de neurones simple pour résoudre le OpenAI CartPole Jeu. Dans cet article, nous allons former un simple réseau de neurones pour résoudre le OpenAI CartPole . J'utiliserai PyTorch et PyGAD . - -[![Cómo los algoritmos genéticos pueden competir con el descenso de gradiente y el backprop](https://user-images.githubusercontent.com/16560492/111009275-3178d180-8361-11eb-9e86-7fb1519acde7.png)](https://www.hebergementwebs.com/nouvelles/comment-les-algorithmes-genetiques-peuvent-rivaliser-avec-la-descente-de-gradient-et-le-backprop) - -### Spanish - -[Cómo los algoritmos genéticos pueden competir con el descenso de gradiente y el backprop](https://www.hebergementwebs.com/noticias/como-los-algoritmos-geneticos-pueden-competir-con-el-descenso-de-gradiente-y-el-backprop) - -Aunque la forma estandar de entrenar redes neuronales es el descenso de gradiente y la retropropagacion, hay otros jugadores en el juego, uno de ellos son los algoritmos evolutivos, como los algoritmos geneticos. - -Usa un algoritmo genetico para entrenar una red neuronal simple para resolver el Juego OpenAI CartPole. En este articulo, entrenaremos una red neuronal simple para resolver el OpenAI CartPole . Usare PyTorch y PyGAD . - -[![Cómo los algoritmos genéticos pueden competir con el descenso de gradiente y el backprop](https://user-images.githubusercontent.com/16560492/111009257-232ab580-8361-11eb-99a5-7226efbc3065.png)](https://www.hebergementwebs.com/noticias/como-los-algoritmos-geneticos-pueden-competir-con-el-descenso-de-gradiente-y-el-backprop) - -### Korean - -#### [[PyGAD] Python 에서 Genetic Algorithm 을 사용해보기](https://data-newbie.tistory.com/m/685) - -[![Korean-1](https://user-images.githubusercontent.com/16560492/108586306-85bd0280-731b-11eb-874c-7ac4ce1326cd.jpg)](https://data-newbie.tistory.com/m/685) - -파이썬에서 genetic algorithm을 사용하는 패키지들을 다 사용해보진 않았지만, 확장성이 있어보이고, 시도할 일이 있어서 살펴봤다. - -이 패키지에서 가장 인상 깊었던 것은 neural network에서 hyper parameter 탐색을 gradient descent 방식이 아닌 GA로도 할 수 있다는 것이다. - -개인적으로 이 부분이 어느정도 초기치를 잘 잡아줄 수 있는 역할로도 쓸 수 있고, Loss가 gradient descent 하기 어려운 구조에서 대안으로 쓸 수 있을 것으로도 생각된다. - -일단 큰 흐름은 다음과 같이 된다. - -사실 완전히 흐름이나 각 parameter에 대한 이해는 부족한 상황 - -### Turkish - -#### [PyGAD ile Genetik Algoritmayı Kullanarak Keras Modelleri Nasıl Eğitilir](https://erencan34.medium.com/pygad-ile-genetik-algoritmay%C4%B1-kullanarak-keras-modelleri-nas%C4%B1l-e%C4%9Fitilir-cf92639a478c) - -This is a translation of an original English tutorial published at Paperspace: [How To Train Keras Models Using the Genetic Algorithm with PyGAD](https://blog.paperspace.com/train-keras-models-using-genetic-algorithm-with-pygad) - -PyGAD, genetik algoritma oluşturmak ve makine öğrenimi algoritmalarını eğitmek için kullanılan açık kaynaklı bir Python kitaplığıdır. Genetik algoritmayı farklı problem türleri ile çalışacak şekilde özelleştirmek için çok çeşitli parametreler sunar. - -PyGAD, sinir ağları (NN’ler) ve evrişimli sinir ağları (CNN’ler) oluşturmayı ve eğitmeyi destekleyen kendi modüllerine sahiptir. Bu modüllerin iyi çalışmasına rağmen, herhangi bir ek optimizasyon önlemi olmaksızın Python’da uygulanırlar. Bu, basit problemler için bile nispeten yüksek hesaplama sürelerine yol açar. - -En son PyGAD sürümü 2.8.0 (20 Eylül 2020'de piyasaya sürüldü), Keras modellerini eğitmek için yeni bir modülü destekliyor. Keras Python’da oluşturulmuş olsa da hızlıdır. Bunun nedeni, Keras’ın arka uç olarak TensorFlow kullanması ve TensorFlow’un oldukça optimize edilmiş olmasıdır. - -Bu öğreticide, PyGAD kullanılarak Keras modellerinin nasıl eğitileceği anlatılmaktadır. Tartışma, Sıralı Modeli veya İşlevsel API’yi kullanarak Keras modellerini oluşturmayı, Keras model parametrelerinin ilk popülasyonunu oluşturmayı, uygun bir uygunluk işlevi oluşturmayı ve daha fazlasını içerir. - -[![national-cancer-institute-zz_3tCcrk7o-unsplash](https://user-images.githubusercontent.com/16560492/108586601-85be0200-731d-11eb-98a4-161c75a1f099.jpg)](https://erencan34.medium.com/pygad-ile-genetik-algoritmay%C4%B1-kullanarak-keras-modelleri-nas%C4%B1l-e%C4%9Fitilir-cf92639a478c) - -### Hungarian - -#### [Tensorflow alapozó 10. Neurális hálózatok tenyésztése genetikus algoritmussal PyGAD és OpenAI Gym használatával](https://thebojda.medium.com/tensorflow-alapoz%C3%B3-10-24f7767d4a2c) - -Hogy kontextusba helyezzem a genetikus algoritmusokat, ismételjük kicsit át, hogy hogyan működik a gradient descent és a backpropagation, ami a neurális hálók tanításának általános módszere. Az erről írt cikkemet itt tudjátok elolvasni. - -A hálózatok tenyésztéséhez a [PyGAD](https://pygad.readthedocs.io/en/latest/) nevű programkönyvtárat használjuk, így mindenek előtt ezt kell telepítenünk, valamint a Tensorflow-t és a Gym-et, amit Colabban már eleve telepítve kapunk. - -Maga a PyGAD egy teljesen általános genetikus algoritmusok futtatására képes rendszer. Ennek a kiterjesztése a KerasGA, ami az általános motor Tensorflow (Keras) neurális hálókon történő futtatását segíti. A 47. sorban létrehozott KerasGA objektum ennek a kiterjesztésnek a része és arra szolgál, hogy a paraméterként átadott modellből a második paraméterben megadott számosságú populációt hozzon létre. Mivel a hálózatunk 386 állítható paraméterrel rendelkezik, ezért a DNS-ünk itt 386 elemből fog állni. A populáció mérete 10 egyed, így a kezdő populációnk egy 10x386 elemű mátrix lesz. Ezt adjuk át az 51. sorban az initial_population paraméterben. - -[![](https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png)](https://thebojda.medium.com/tensorflow-alapoz%C3%B3-10-24f7767d4a2c) - -### Russian - -#### [PyGAD: библиотека для имплементации генетического алгоритма](https://neurohive.io/ru/frameworki/pygad-biblioteka-dlya-implementacii-geneticheskogo-algoritma) - -PyGAD — это библиотека для имплементации генетического алгоритма. Кроме того, библиотека предоставляет доступ к оптимизированным реализациям алгоритмов машинного обучения. PyGAD разрабатывали на Python 3. - -Библиотека PyGAD поддерживает разные типы скрещивания, мутации и селекции родителя. PyGAD позволяет оптимизировать проблемы с помощью генетического алгоритма через кастомизацию целевой функции. - -Кроме генетического алгоритма, библиотека содержит оптимизированные имплементации алгоритмов машинного обучения. На текущий момент PyGAD поддерживает создание и обучение нейросетей для задач классификации. - -Библиотека находится в стадии активной разработки. Создатели планируют добавление функционала для решения бинарных задач и имплементации новых алгоритмов. - -PyGAD разрабатывали на Python 3.7.3. Зависимости включают в себя NumPy для создания и манипуляции массивами и Matplotlib для визуализации. Один из изкейсов использования инструмента — оптимизация весов, которые удовлетворяют заданной функции. - -[![](https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png)](https://neurohive.io/ru/frameworki/pygad-biblioteka-dlya-implementacii-geneticheskogo-algoritma) - -## Research Papers using PyGAD - -A number of research papers used PyGAD and here are some of them: - -* Alberto Meola, Manuel Winkler, Sören Weinrich, Metaheuristic optimization of data preparation and machine learning hyperparameters for prediction of dynamic methane production, Bioresource Technology, Volume 372, 2023, 128604, ISSN 0960-8524. -* Jaros, Marta, and Jiri Jaros. "Performance-Cost Optimization of Moldable Scientific Workflows." -* Thorat, Divya. "Enhanced genetic algorithm to reduce makespan of multiple jobs in map-reduce application on serverless platform". Diss. Dublin, National College of Ireland, 2020. -* Koch, Chris, and Edgar Dobriban. "AttenGen: Generating Live Attenuated Vaccine Candidates using Machine Learning." (2021). -* Bhardwaj, Bhavya, et al. "Windfarm optimization using Nelder-Mead and Particle Swarm optimization." *2021 7th International Conference on Electrical Energy Systems (ICEES)*. IEEE, 2021. -* Bernardo, Reginald Christian S. and J. Said. “Towards a model-independent reconstruction approach for late-time Hubble data.” (2021). -* Duong, Tri Dung, Qian Li, and Guandong Xu. "Prototype-based Counterfactual Explanation for Causal Classification." *arXiv preprint arXiv:2105.00703* (2021). -* Farrag, Tamer Ahmed, and Ehab E. Elattar. "Optimized Deep Stacked Long Short-Term Memory Network for Long-Term Load Forecasting." *IEEE Access* 9 (2021): 68511-68522. -* Antunes, E. D. O., Caetano, M. F., Marotta, M. A., Araujo, A., Bondan, L., Meneguette, R. I., & Rocha Filho, G. P. (2021, August). Soluções Otimizadas para o Problema de Localização de Máxima Cobertura em Redes Militarizadas 4G/LTE. In *Anais do XXVI Workshop de Gerência e Operação de Redes e Serviços* (pp. 152-165). SBC. -* M. Yani, F. Ardilla, A. A. Saputra and N. Kubota, "Gradient-Free Deep Q-Networks Reinforcement learning: Benchmark and Evaluation," *2021 IEEE Symposium Series on Computational Intelligence (SSCI)*, 2021, pp. 1-5, doi: 10.1109/SSCI50451.2021.9659941. -* Yani, Mohamad, and Naoyuki Kubota. "Deep Convolutional Networks with Genetic Algorithm for Reinforcement Learning Problem." -* Mahendra, Muhammad Ihza, and Isman Kurniawan. "Optimizing Convolutional Neural Network by Using Genetic Algorithm for COVID-19 Detection in Chest X-Ray Image." *2021 International Conference on Data Science and Its Applications (ICoDSA)*. IEEE, 2021. -* Glibota, Vjeko. *Umjeravanje mikroskopskog prometnog modela primjenom genetskog algoritma*. Diss. University of Zagreb. Faculty of Transport and Traffic Sciences. Division of Intelligent Transport Systems and Logistics. Department of Intelligent Transport Systems, 2021. -* Zhu, Mingda. *Genetic Algorithm-based Parameter Identification for Ship Manoeuvring Model under Wind Disturbance*. MS thesis. NTNU, 2021. -* Abdalrahman, Ahmed, and Weihua Zhuang. "Dynamic pricing for differentiated pev charging services using deep reinforcement learning." *IEEE Transactions on Intelligent Transportation Systems* (2020). - -## More Links - -https://rodriguezanton.com/identifying-contact-states-for-2d-objects-using-pygad-and/ - -https://torvaney.github.io/projects/t9-optimised - -## For More Information - -There are different resources that can be used to get started with the genetic algorithm and building it in Python. - -### Tutorial: Implementing Genetic Algorithm in Python - -To start with coding the genetic algorithm, you can check the tutorial titled [**Genetic Algorithm Implementation in Python**](https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad) available at these links: - -- [LinkedIn](https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad) -- [Towards Data Science](https://towardsdatascience.com/genetic-algorithm-implementation-in-python-5ab67bb124a6) -- [KDnuggets](https://www.kdnuggets.com/2018/07/genetic-algorithm-implementation-python.html) - -[This tutorial](https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad) is prepared based on a previous version of the project but it still a good resource to start with coding the genetic algorithm. - -[![Genetic Algorithm Implementation in Python](https://user-images.githubusercontent.com/16560492/78830052-a3c19300-79e7-11ea-8b9b-4b343ea4049c.png)](https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad) - -### Tutorial: Introduction to Genetic Algorithm - -Get started with the genetic algorithm by reading the tutorial titled [**Introduction to Optimization with Genetic Algorithm**](https://www.linkedin.com/pulse/introduction-optimization-genetic-algorithm-ahmed-gad) which is available at these links: - -* [LinkedIn](https://www.linkedin.com/pulse/introduction-optimization-genetic-algorithm-ahmed-gad) -* [Towards Data Science](https://www.kdnuggets.com/2018/03/introduction-optimization-with-genetic-algorithm.html) -* [KDnuggets](https://towardsdatascience.com/introduction-to-optimization-with-genetic-algorithm-2f5001d9964b) - -[![Introduction to Genetic Algorithm](https://user-images.githubusercontent.com/16560492/82078259-26252d00-96e1-11ea-9a02-52a99e1054b9.jpg)](https://www.linkedin.com/pulse/introduction-optimization-genetic-algorithm-ahmed-gad) - -### Tutorial: Build Neural Networks in Python - -Read about building neural networks in Python through the tutorial titled [**Artificial Neural Network Implementation using NumPy and Classification of the Fruits360 Image Dataset**](https://www.linkedin.com/pulse/artificial-neural-network-implementation-using-numpy-fruits360-gad) available at these links: - -* [LinkedIn](https://www.linkedin.com/pulse/artificial-neural-network-implementation-using-numpy-fruits360-gad) -* [Towards Data Science](https://towardsdatascience.com/artificial-neural-network-implementation-using-numpy-and-classification-of-the-fruits360-image-3c56affa4491) -* [KDnuggets](https://www.kdnuggets.com/2019/02/artificial-neural-network-implementation-using-numpy-and-image-classification.html) - -[![Building Neural Networks Python](https://user-images.githubusercontent.com/16560492/82078281-30472b80-96e1-11ea-8017-6a1f4383d602.jpg)](https://www.linkedin.com/pulse/artificial-neural-network-implementation-using-numpy-fruits360-gad) - -### Tutorial: Optimize Neural Networks with Genetic Algorithm - -Read about training neural networks using the genetic algorithm through the tutorial titled [**Artificial Neural Networks Optimization using Genetic Algorithm with Python**](https://www.linkedin.com/pulse/artificial-neural-networks-optimization-using-genetic-ahmed-gad) available at these links: - -- [LinkedIn](https://www.linkedin.com/pulse/artificial-neural-networks-optimization-using-genetic-ahmed-gad) -- [Towards Data Science](https://towardsdatascience.com/artificial-neural-networks-optimization-using-genetic-algorithm-with-python-1fe8ed17733e) -- [KDnuggets](https://www.kdnuggets.com/2019/03/artificial-neural-networks-optimization-genetic-algorithm-python.html) - -[![Training Neural Networks using Genetic Algorithm Python](https://user-images.githubusercontent.com/16560492/82078300-376e3980-96e1-11ea-821c-aa6b8ceb44d4.jpg)](https://www.linkedin.com/pulse/artificial-neural-networks-optimization-using-genetic-ahmed-gad) - -### Tutorial: Building CNN in Python - -To start with coding the genetic algorithm, you can check the tutorial titled [**Building Convolutional Neural Network using NumPy from Scratch**](https://www.linkedin.com/pulse/building-convolutional-neural-network-using-numpy-from-ahmed-gad) available at these links: - -- [LinkedIn](https://www.linkedin.com/pulse/building-convolutional-neural-network-using-numpy-from-ahmed-gad) -- [Towards Data Science](https://towardsdatascience.com/building-convolutional-neural-network-using-numpy-from-scratch-b30aac50e50a) -- [KDnuggets](https://www.kdnuggets.com/2018/04/building-convolutional-neural-network-numpy-scratch.html) -- [Chinese Translation](http://m.aliyun.com/yunqi/articles/585741) - -[This tutorial](https://www.linkedin.com/pulse/building-convolutional-neural-network-using-numpy-from-ahmed-gad)) is prepared based on a previous version of the project but it still a good resource to start with coding CNNs. - -[![Building CNN in Python](https://user-images.githubusercontent.com/16560492/82431022-6c3a1200-9a8e-11ea-8f1b-b055196d76e3.png)](https://www.linkedin.com/pulse/building-convolutional-neural-network-using-numpy-from-ahmed-gad) - -### Tutorial: Derivation of CNN from FCNN - -Get started with the genetic algorithm by reading the tutorial titled [**Derivation of Convolutional Neural Network from Fully Connected Network Step-By-Step**](https://www.linkedin.com/pulse/derivation-convolutional-neural-network-from-fully-connected-gad) which is available at these links: - -* [LinkedIn](https://www.linkedin.com/pulse/derivation-convolutional-neural-network-from-fully-connected-gad) -* [Towards Data Science](https://towardsdatascience.com/derivation-of-convolutional-neural-network-from-fully-connected-network-step-by-step-b42ebafa5275) -* [KDnuggets](https://www.kdnuggets.com/2018/04/derivation-convolutional-neural-network-fully-connected-step-by-step.html) - -[![Derivation of CNN from FCNN](https://user-images.githubusercontent.com/16560492/82431369-db176b00-9a8e-11ea-99bd-e845192873fc.png)](https://www.linkedin.com/pulse/derivation-convolutional-neural-network-from-fully-connected-gad) - -### Book: Practical Computer Vision Applications Using Deep Learning with CNNs - -You can also check my book cited as [**Ahmed Fawzy Gad 'Practical Computer Vision Applications Using Deep Learning with CNNs'. Dec. 2018, Apress, 978-1-4842-4167-7**](https://www.amazon.com/Practical-Computer-Vision-Applications-Learning/dp/1484241665) which discusses neural networks, convolutional neural networks, deep learning, genetic algorithm, and more. - -Find the book at these links: - -- [Amazon](https://www.amazon.com/Practical-Computer-Vision-Applications-Learning/dp/1484241665) -- [Springer](https://link.springer.com/book/10.1007/978-1-4842-4167-7) -- [Apress](https://www.apress.com/gp/book/9781484241660) -- [O'Reilly](https://www.oreilly.com/library/view/practical-computer-vision/9781484241677) -- [Google Books](https://books.google.com.eg/books?id=xLd9DwAAQBAJ) - -![Fig04](https://user-images.githubusercontent.com/16560492/78830077-ae7c2800-79e7-11ea-980b-53b6bd879eeb.jpg) - -## Contact Us - -* E-mail: ahmed.f.gad@gmail.com -* [LinkedIn](https://www.linkedin.com/in/ahmedfgad) -* [Amazon Author Page](https://amazon.com/author/ahmedgad) -* [Heartbeat](https://heartbeat.fritz.ai/@ahmedfgad) -* [Paperspace](https://blog.paperspace.com/author/ahmed) -* [KDnuggets](https://kdnuggets.com/author/ahmed-gad) -* [TowardsDataScience](https://towardsdatascience.com/@ahmedfgad) -* [GitHub](https://github.com/ahmedfgad) - -![PYGAD-LOGO](https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png) - -Thank you for using [PyGAD](https://github.com/ahmedfgad/GeneticAlgorithmPython) :) +help_support +help_tutorials +help_projects +help_languages +::: diff --git a/docs/source/help_languages.md b/docs/source/help_languages.md new file mode 100644 index 0000000..7549ea2 --- /dev/null +++ b/docs/source/help_languages.md @@ -0,0 +1,81 @@ +# PyGAD in Other Languages + +## French + +[Cómo los algoritmos genéticos pueden competir con el descenso de gradiente y el backprop](https://www.hebergementwebs.com/nouvelles/comment-les-algorithmes-genetiques-peuvent-rivaliser-avec-la-descente-de-gradient-et-le-backprop) + +Bien que la manière standard d'entraîner les réseaux de neurones soit la descente de gradient et la rétropropagation, il y a d'autres joueurs dans le jeu. L'un d'eux est les algorithmes évolutionnaires, tels que les algorithmes génétiques. + +Utiliser un algorithme génétique pour former un réseau de neurones simple pour résoudre le OpenAI CartPole Jeu. Dans cet article, nous allons former un simple réseau de neurones pour résoudre le OpenAI CartPole . J'utiliserai PyTorch et PyGAD . + +[![Cómo los algoritmos genéticos pueden competir con el descenso de gradiente y el backprop](https://user-images.githubusercontent.com/16560492/111009275-3178d180-8361-11eb-9e86-7fb1519acde7.png)](https://www.hebergementwebs.com/nouvelles/comment-les-algorithmes-genetiques-peuvent-rivaliser-avec-la-descente-de-gradient-et-le-backprop) + +## Spanish + +[Cómo los algoritmos genéticos pueden competir con el descenso de gradiente y el backprop](https://www.hebergementwebs.com/noticias/como-los-algoritmos-geneticos-pueden-competir-con-el-descenso-de-gradiente-y-el-backprop) + +Aunque la forma estandar de entrenar redes neuronales es el descenso de gradiente y la retropropagacion, hay otros jugadores en el juego, uno de ellos son los algoritmos evolutivos, como los algoritmos geneticos. + +Usa un algoritmo genetico para entrenar una red neuronal simple para resolver el Juego OpenAI CartPole. En este articulo, entrenaremos una red neuronal simple para resolver el OpenAI CartPole . Usare PyTorch y PyGAD . + +[![Cómo los algoritmos genéticos pueden competir con el descenso de gradiente y el backprop](https://user-images.githubusercontent.com/16560492/111009257-232ab580-8361-11eb-99a5-7226efbc3065.png)](https://www.hebergementwebs.com/noticias/como-los-algoritmos-geneticos-pueden-competir-con-el-descenso-de-gradiente-y-el-backprop) + +## Korean + +### [[PyGAD] Python 에서 Genetic Algorithm 을 사용해보기](https://data-newbie.tistory.com/m/685) + +[![Korean-1](https://user-images.githubusercontent.com/16560492/108586306-85bd0280-731b-11eb-874c-7ac4ce1326cd.jpg)](https://data-newbie.tistory.com/m/685) + +파이썬에서 genetic algorithm을 사용하는 패키지들을 다 사용해보진 않았지만, 확장성이 있어보이고, 시도할 일이 있어서 살펴봤다. + +이 패키지에서 가장 인상 깊었던 것은 neural network에서 hyper parameter 탐색을 gradient descent 방식이 아닌 GA로도 할 수 있다는 것이다. + +개인적으로 이 부분이 어느정도 초기치를 잘 잡아줄 수 있는 역할로도 쓸 수 있고, Loss가 gradient descent 하기 어려운 구조에서 대안으로 쓸 수 있을 것으로도 생각된다. + +일단 큰 흐름은 다음과 같이 된다. + +사실 완전히 흐름이나 각 parameter에 대한 이해는 부족한 상황 + +## Turkish + +### [PyGAD ile Genetik Algoritmayı Kullanarak Keras Modelleri Nasıl Eğitilir](https://erencan34.medium.com/pygad-ile-genetik-algoritmay%C4%B1-kullanarak-keras-modelleri-nas%C4%B1l-e%C4%9Fitilir-cf92639a478c) + +This is a translation of an original English tutorial published at Paperspace: [How To Train Keras Models Using the Genetic Algorithm with PyGAD](https://blog.paperspace.com/train-keras-models-using-genetic-algorithm-with-pygad) + +PyGAD, genetik algoritma oluşturmak ve makine öğrenimi algoritmalarını eğitmek için kullanılan açık kaynaklı bir Python kitaplığıdır. Genetik algoritmayı farklı problem türleri ile çalışacak şekilde özelleştirmek için çok çeşitli parametreler sunar. + +PyGAD, sinir ağları (NN’ler) ve evrişimli sinir ağları (CNN’ler) oluşturmayı ve eğitmeyi destekleyen kendi modüllerine sahiptir. Bu modüllerin iyi çalışmasına rağmen, herhangi bir ek optimizasyon önlemi olmaksızın Python’da uygulanırlar. Bu, basit problemler için bile nispeten yüksek hesaplama sürelerine yol açar. + +En son PyGAD sürümü 2.8.0 (20 Eylül 2020'de piyasaya sürüldü), Keras modellerini eğitmek için yeni bir modülü destekliyor. Keras Python’da oluşturulmuş olsa da hızlıdır. Bunun nedeni, Keras’ın arka uç olarak TensorFlow kullanması ve TensorFlow’un oldukça optimize edilmiş olmasıdır. + +Bu öğreticide, PyGAD kullanılarak Keras modellerinin nasıl eğitileceği anlatılmaktadır. Tartışma, Sıralı Modeli veya İşlevsel API’yi kullanarak Keras modellerini oluşturmayı, Keras model parametrelerinin ilk popülasyonunu oluşturmayı, uygun bir uygunluk işlevi oluşturmayı ve daha fazlasını içerir. + +[![national-cancer-institute-zz_3tCcrk7o-unsplash](https://user-images.githubusercontent.com/16560492/108586601-85be0200-731d-11eb-98a4-161c75a1f099.jpg)](https://erencan34.medium.com/pygad-ile-genetik-algoritmay%C4%B1-kullanarak-keras-modelleri-nas%C4%B1l-e%C4%9Fitilir-cf92639a478c) + +## Hungarian + +### [Tensorflow alapozó 10. Neurális hálózatok tenyésztése genetikus algoritmussal PyGAD és OpenAI Gym használatával](https://thebojda.medium.com/tensorflow-alapoz%C3%B3-10-24f7767d4a2c) + +Hogy kontextusba helyezzem a genetikus algoritmusokat, ismételjük kicsit át, hogy hogyan működik a gradient descent és a backpropagation, ami a neurális hálók tanításának általános módszere. Az erről írt cikkemet itt tudjátok elolvasni. + +A hálózatok tenyésztéséhez a [PyGAD](https://pygad.readthedocs.io/en/latest/) nevű programkönyvtárat használjuk, így mindenek előtt ezt kell telepítenünk, valamint a Tensorflow-t és a Gym-et, amit Colabban már eleve telepítve kapunk. + +Maga a PyGAD egy teljesen általános genetikus algoritmusok futtatására képes rendszer. Ennek a kiterjesztése a KerasGA, ami az általános motor Tensorflow (Keras) neurális hálókon történő futtatását segíti. A 47. sorban létrehozott KerasGA objektum ennek a kiterjesztésnek a része és arra szolgál, hogy a paraméterként átadott modellből a második paraméterben megadott számosságú populációt hozzon létre. Mivel a hálózatunk 386 állítható paraméterrel rendelkezik, ezért a DNS-ünk itt 386 elemből fog állni. A populáció mérete 10 egyed, így a kezdő populációnk egy 10x386 elemű mátrix lesz. Ezt adjuk át az 51. sorban az initial_population paraméterben. + +[![](https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png)](https://thebojda.medium.com/tensorflow-alapoz%C3%B3-10-24f7767d4a2c) + +## Russian + +### [PyGAD: библиотека для имплементации генетического алгоритма](https://neurohive.io/ru/frameworki/pygad-biblioteka-dlya-implementacii-geneticheskogo-algoritma) + +PyGAD — это библиотека для имплементации генетического алгоритма. Кроме того, библиотека предоставляет доступ к оптимизированным реализациям алгоритмов машинного обучения. PyGAD разрабатывали на Python 3. + +Библиотека PyGAD поддерживает разные типы скрещивания, мутации и селекции родителя. PyGAD позволяет оптимизировать проблемы с помощью генетического алгоритма через кастомизацию целевой функции. + +Кроме генетического алгоритма, библиотека содержит оптимизированные имплементации алгоритмов машинного обучения. На текущий момент PyGAD поддерживает создание и обучение нейросетей для задач классификации. + +Библиотека находится в стадии активной разработки. Создатели планируют добавление функционала для решения бинарных задач и имплементации новых алгоритмов. + +PyGAD разрабатывали на Python 3.7.3. Зависимости включают в себя NumPy для создания и манипуляции массивами и Matplotlib для визуализации. Один из изкейсов использования инструмента — оптимизация весов, которые удовлетворяют заданной функции. + +[![](https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png)](https://neurohive.io/ru/frameworki/pygad-biblioteka-dlya-implementacii-geneticheskogo-algoritma) diff --git a/docs/source/help_projects.md b/docs/source/help_projects.md new file mode 100644 index 0000000..fe065ac --- /dev/null +++ b/docs/source/help_projects.md @@ -0,0 +1,83 @@ +# Projects and Research + +The open-source projects that make up PyGAD, projects built with it, and research papers that use it. + +## PyGAD Projects at GitHub + +The PyGAD library is available at PyPI at this page https://pypi.org/project/pygad. PyGAD is built out of a number of open-source GitHub projects. A brief note about these projects is given in the next subsections. + +### [GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython) + +GitHub Link: https://github.com/ahmedfgad/GeneticAlgorithmPython + +[**GeneticAlgorithmPython**](https://github.com/ahmedfgad/GeneticAlgorithmPython) is the first project which is an open-source Python 3 project for implementing the genetic algorithm based on NumPy. + +### [NumPyANN](https://github.com/ahmedfgad/NumPyANN) + +GitHub Link: https://github.com/ahmedfgad/NumPyANN + +[**NumPyANN**](https://github.com/ahmedfgad/NumPyANN) builds artificial neural networks in **Python 3** using **NumPy** from scratch. The purpose of this project is to only implement the **forward pass** of a neural network without using a training algorithm. Currently, it only supports classification and later regression will be also supported. Moreover, only one class is supported per sample. + +### [NeuralGenetic](https://github.com/ahmedfgad/NeuralGenetic) + +GitHub Link: https://github.com/ahmedfgad/NeuralGenetic + +[NeuralGenetic](https://github.com/ahmedfgad/NeuralGenetic) trains neural networks using the genetic algorithm based on the previous 2 projects [GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython) and [NumPyANN](https://github.com/ahmedfgad/NumPyANN). + +### [NumPyCNN](https://github.com/ahmedfgad/NumPyCNN) + +GitHub Link: https://github.com/ahmedfgad/NumPyCNN + +[NumPyCNN](https://github.com/ahmedfgad/NumPyCNN) builds convolutional neural networks using NumPy. The purpose of this project is to only implement the **forward pass** of a convolutional neural network without using a training algorithm. + +### [CNNGenetic](https://github.com/ahmedfgad/CNNGenetic) + +GitHub Link: https://github.com/ahmedfgad/CNNGenetic + +[CNNGenetic](https://github.com/ahmedfgad/CNNGenetic) trains convolutional neural networks using the genetic algorithm. It uses the [GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython) project for building the genetic algorithm. + +### [KerasGA](https://github.com/ahmedfgad/KerasGA) + +GitHub Link: https://github.com/ahmedfgad/KerasGA + +[KerasGA](https://github.com/ahmedfgad/KerasGA) trains [Keras](https://keras.io) models using the genetic algorithm. It uses the [GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython) project for building the genetic algorithm. + +### [TorchGA](https://github.com/ahmedfgad/TorchGA) + +GitHub Link: https://github.com/ahmedfgad/TorchGA + +[TorchGA](https://github.com/ahmedfgad/TorchGA) trains [PyTorch](https://pytorch.org) models using the genetic algorithm. It uses the [GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython) project for building the genetic algorithm. + +[pygad.torchga](https://github.com/ahmedfgad/TorchGA): https://github.com/ahmedfgad/TorchGA + +## Projects Built using PyGAD + +If you created a project that uses PyGAD, then we can support you by mentioning this project here in PyGAD's documentation. + +To do that, please send a message at ahmed.f.gad@gmail.com or check the [**Contact Us**](https://pygad.readthedocs.io/en/latest/help_support.html#contact-us) section for more contact details. + +Within your message, please send the following details: + +- Project title +- Brief description +- Preferably, a link that directs the readers to your project + +## Research Papers using PyGAD + +A number of research papers used PyGAD and here are some of them: + +* Alberto Meola, Manuel Winkler, Sören Weinrich, Metaheuristic optimization of data preparation and machine learning hyperparameters for prediction of dynamic methane production, Bioresource Technology, Volume 372, 2023, 128604, ISSN 0960-8524. +* Jaros, Marta, and Jiri Jaros. "Performance-Cost Optimization of Moldable Scientific Workflows." +* Thorat, Divya. "Enhanced genetic algorithm to reduce makespan of multiple jobs in map-reduce application on serverless platform". Diss. Dublin, National College of Ireland, 2020. +* Koch, Chris, and Edgar Dobriban. "AttenGen: Generating Live Attenuated Vaccine Candidates using Machine Learning." (2021). +* Bhardwaj, Bhavya, et al. "Windfarm optimization using Nelder-Mead and Particle Swarm optimization." *2021 7th International Conference on Electrical Energy Systems (ICEES)*. IEEE, 2021. +* Bernardo, Reginald Christian S. and J. Said. “Towards a model-independent reconstruction approach for late-time Hubble data.” (2021). +* Duong, Tri Dung, Qian Li, and Guandong Xu. "Prototype-based Counterfactual Explanation for Causal Classification." *arXiv preprint arXiv:2105.00703* (2021). +* Farrag, Tamer Ahmed, and Ehab E. Elattar. "Optimized Deep Stacked Long Short-Term Memory Network for Long-Term Load Forecasting." *IEEE Access* 9 (2021): 68511-68522. +* Antunes, E. D. O., Caetano, M. F., Marotta, M. A., Araujo, A., Bondan, L., Meneguette, R. I., & Rocha Filho, G. P. (2021, August). Soluções Otimizadas para o Problema de Localização de Máxima Cobertura em Redes Militarizadas 4G/LTE. In *Anais do XXVI Workshop de Gerência e Operação de Redes e Serviços* (pp. 152-165). SBC. +* M. Yani, F. Ardilla, A. A. Saputra and N. Kubota, "Gradient-Free Deep Q-Networks Reinforcement learning: Benchmark and Evaluation," *2021 IEEE Symposium Series on Computational Intelligence (SSCI)*, 2021, pp. 1-5, doi: 10.1109/SSCI50451.2021.9659941. +* Yani, Mohamad, and Naoyuki Kubota. "Deep Convolutional Networks with Genetic Algorithm for Reinforcement Learning Problem." +* Mahendra, Muhammad Ihza, and Isman Kurniawan. "Optimizing Convolutional Neural Network by Using Genetic Algorithm for COVID-19 Detection in Chest X-Ray Image." *2021 International Conference on Data Science and Its Applications (ICoDSA)*. IEEE, 2021. +* Glibota, Vjeko. *Umjeravanje mikroskopskog prometnog modela primjenom genetskog algoritma*. Diss. University of Zagreb. Faculty of Transport and Traffic Sciences. Division of Intelligent Transport Systems and Logistics. Department of Intelligent Transport Systems, 2021. +* Zhu, Mingda. *Genetic Algorithm-based Parameter Identification for Ship Manoeuvring Model under Wind Disturbance*. MS thesis. NTNU, 2021. +* Abdalrahman, Ahmed, and Weihua Zhuang. "Dynamic pricing for differentiated pev charging services using deep reinforcement learning." *IEEE Transactions on Intelligent Transportation Systems* (2020). diff --git a/docs/source/help_support.md b/docs/source/help_support.md new file mode 100644 index 0000000..c6a8680 --- /dev/null +++ b/docs/source/help_support.md @@ -0,0 +1,64 @@ +# Getting Help + +Use these channels to get help with PyGAD, report a bug, or ask for a new feature. + +## Submitting Issues + +If there is an issue using PyGAD, then use any of your preferred option to discuss that issue. + +One way is [submitting an issue](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/new) into this GitHub project ([github.com/ahmedfgad/GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython)) in case something is not working properly or to ask for questions. + +If this is not a proper option for you, then check the [**Contact Us**](https://pygad.readthedocs.io/en/latest/help_support.html#contact-us) section for more contact details. + +## Ask for Feature + +PyGAD is actively developed with the goal of building a dynamic library for suporting a wide-range of problems to be optimized using the genetic algorithm. + +To ask for a new feature, either [submit an issue](https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/new) into this GitHub project ([github.com/ahmedfgad/GeneticAlgorithmPython](https://github.com/ahmedfgad/GeneticAlgorithmPython)) or send an e-mail to ahmed.f.gad@gmail.com. + +Also check the [**Contact Us**](https://pygad.readthedocs.io/en/latest/help_support.html#contact-us) section for more contact details. + +## Stackoverflow Questions about PyGAD + +### [How do I proceed to load a ga_instance as “.pkl” format in PyGad?](https://stackoverflow.com/questions/67424181/how-do-i-proceed-to-load-a-ga-instance-as-pkl-format-in-pygad) + +### [Binary Classification NN Model Weights not being Trained in PyGAD](https://stackoverflow.com/questions/67276696/binary-classification-nn-model-weights-not-being-trained-in-pygad) + +### [How to solve TSP problem using pyGAD package?](https://stackoverflow.com/questions/66298595/how-to-solve-tsp-problem-using-pygad-package) + +### [How can I save a matplotlib plot that is the output of a function in jupyter?](https://stackoverflow.com/questions/66055330/how-can-i-save-a-matplotlib-plot-that-is-the-output-of-a-function-in-jupyter) + +### [How do I query the best solution of a pyGAD GA instance?](https://stackoverflow.com/questions/65757722/how-do-i-query-the-best-solution-of-a-pygad-ga-instance) + +### [Multi-Input Multi-Output in Genetic algorithm (python)](https://stackoverflow.com/questions/64943711/multi-input-multi-output-in-genetic-algorithm-python) + +https://www.linkedin.com/pulse/validation-short-term-parametric-trading-model-genetic-landolfi + +https://itchef.ru/articles/397758 + +https://audhiaprilliant.medium.com/genetic-algorithm-based-clustering-algorithm-in-searching-robust-initial-centroids-for-k-means-e3b4d892a4be + +https://python.plainenglish.io/validation-of-a-short-term-parametric-trading-model-with-genetic-optimization-and-walk-forward-89708b789af6 + +https://ichi.pro/ko/pygadwa-hamkke-yujeon-algolijeum-eul-sayonghayeo-keras-model-eul-hunlyeonsikineun-bangbeob-173299286377169 + +https://ichi.pro/tr/pygad-ile-genetik-algoritmayi-kullanarak-keras-modelleri-nasil-egitilir-173299286377169 + +https://ichi.pro/ru/kak-obucit-modeli-keras-s-pomos-u-geneticeskogo-algoritma-s-pygad-173299286377169 + +https://blog.csdn.net/sinat_38079265/article/details/108449614 + +## Contact Us + +* E-mail: ahmed.f.gad@gmail.com +* [LinkedIn](https://www.linkedin.com/in/ahmedfgad) +* [Amazon Author Page](https://amazon.com/author/ahmedgad) +* [Heartbeat](https://heartbeat.fritz.ai/@ahmedfgad) +* [Paperspace](https://blog.paperspace.com/author/ahmed) +* [KDnuggets](https://kdnuggets.com/author/ahmed-gad) +* [TowardsDataScience](https://towardsdatascience.com/@ahmedfgad) +* [GitHub](https://github.com/ahmedfgad) + +![PYGAD-LOGO](https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png) + +Thank you for using [PyGAD](https://github.com/ahmedfgad/GeneticAlgorithmPython) :) diff --git a/docs/source/help_tutorials.md b/docs/source/help_tutorials.md new file mode 100644 index 0000000..e6db848 --- /dev/null +++ b/docs/source/help_tutorials.md @@ -0,0 +1,156 @@ +# Tutorials and Resources + +Tutorials, articles, and a book about PyGAD and the genetic algorithm. + +## Tutorials about PyGAD + +### [Adaptive Mutation in Genetic Algorithm with Python Examples](https://neptune.ai/blog/adaptive-mutation-in-genetic-algorithm-with-python-examples) + +In this tutorial, we’ll see why mutation with a fixed number of genes is bad, and how to replace it with adaptive mutation. Using the [PyGAD Python 3 library](https://pygad.readthedocs.io/), we’ll discuss a few examples that use both random and adaptive mutation. + +### [Clustering Using the Genetic Algorithm in Python](https://blog.paperspace.com/clustering-using-the-genetic-algorithm) + +This tutorial discusses how the genetic algorithm is used to cluster data, starting from random clusters and running until the optimal clusters are found. We'll start by briefly revising the K-means clustering algorithm to point out its weak points, which are later solved by the genetic algorithm. The code examples in this tutorial are implemented in Python using the [PyGAD library](https://pygad.readthedocs.io/). + +### [Working with Different Genetic Algorithm Representations in Python](https://blog.paperspace.com/working-with-different-genetic-algorithm-representations-python) + +Depending on the nature of the problem being optimized, the genetic algorithm (GA) supports two different gene representations: binary, and decimal. The binary GA has only two values for its genes, which are 0 and 1. This is easier to manage as its gene values are limited compared to the decimal GA, for which we can use different formats like float or integer, and limited or unlimited ranges. + +This tutorial discusses how the [PyGAD](https://pygad.readthedocs.io/) library supports the two GA representations, binary and decimal. + +### [5 Genetic Algorithm Applications Using PyGAD](https://blog.paperspace.com/genetic-algorithm-applications-using-pygad) + +This tutorial introduces PyGAD, an open-source Python library for implementing the genetic algorithm and training machine learning algorithms. PyGAD supports 19 parameters for customizing the genetic algorithm for various applications. + +Within this tutorial we'll discuss 5 different applications of the genetic algorithm and build them using PyGAD. + +### [Train Neural Networks Using a Genetic Algorithm in Python with PyGAD](https://heartbeat.fritz.ai/train-neural-networks-using-a-genetic-algorithm-in-python-with-pygad-862905048429?gi=ba58ee6b4bbd) + +The genetic algorithm (GA) is a biologically-inspired optimization algorithm. It has in recent years gained importance, as it’s simple while also solving complex problems like travel route optimization, training machine learning algorithms, working with single and multi-objective problems, game playing, and more. + +Deep neural networks are inspired by the idea of how the biological brain works. It’s a universal function approximator, which is capable of simulating any function, and is now used to solve the most complex problems in machine learning. What’s more, they’re able to work with all types of data (images, audio, video, and text). + +Both genetic algorithms (GAs) and neural networks (NNs) are similar, as both are biologically-inspired techniques. This similarity motivates us to create a hybrid of both to see whether a GA can train NNs with high accuracy. + +This tutorial uses [PyGAD](https://pygad.readthedocs.io/), a Python library that supports building and training NNs using a GA. [PyGAD](https://pygad.readthedocs.io/) offers both classification and regression NNs. + +### [Building a Game-Playing Agent for CoinTex Using the Genetic Algorithm](https://blog.paperspace.com/building-agent-for-cointex-using-genetic-algorithm) + +In this tutorial we'll see how to build a game-playing agent using only the genetic algorithm to play a game called [CoinTex](https://play.google.com/store/apps/details?id=coin.tex.cointexreactfast&hl=en), which is developed in the Kivy Python framework. The objective of CoinTex is to collect the randomly distributed coins while avoiding collision with fire and monsters (that move randomly). The source code of CoinTex can be found [on GitHub](https://github.com/ahmedfgad/CoinTex). + +The genetic algorithm is the only AI used here; there is no other machine/deep learning model used with it. We'll implement the genetic algorithm using [PyGad](https://blog.paperspace.com/genetic-algorithm-applications-using-pygad/). This tutorial starts with a quick overview of CoinTex followed by a brief explanation of the genetic algorithm, and how it can be used to create the playing agent. Finally, we'll see how to implement these ideas in Python. + +The source code of the genetic algorithm agent is available [here](https://github.com/ahmedfgad/CoinTex/tree/master/PlayerGA), and you can download the code used in this tutorial from [here](https://github.com/ahmedfgad/CoinTex/tree/master/PlayerGA/TutorialProject). + +### [How To Train Keras Models Using the Genetic Algorithm with PyGAD](https://blog.paperspace.com/train-keras-models-using-genetic-algorithm-with-pygad) + +PyGAD is an open-source Python library for building the genetic algorithm and training machine learning algorithms. It offers a wide range of parameters to customize the genetic algorithm to work with different types of problems. + +PyGAD has its own modules that support building and training neural networks (NNs) and convolutional neural networks (CNNs). Despite these modules working well, they are implemented in Python without any additional optimization measures. This leads to comparatively high computational times for even simple problems. + +The latest PyGAD version, 2.8.0 (released on 20 September 2020), supports a new module to train Keras models. Even though Keras is built in Python, it's fast. The reason is that Keras uses TensorFlow as a backend, and TensorFlow is highly optimized. + +This tutorial discusses how to train Keras models using PyGAD. The discussion includes building Keras models using either the Sequential Model or the Functional API, building an initial population of Keras model parameters, creating an appropriate fitness function, and more. + +[![PyGAD+Keras](https://user-images.githubusercontent.com/16560492/111009628-2b372500-8362-11eb-90cf-01b47d831624.png)](https://blog.paperspace.com/train-keras-models-using-genetic-algorithm-with-pygad) + +### [Train PyTorch Models Using Genetic Algorithm with PyGAD](https://neptune.ai/blog/train-pytorch-models-using-genetic-algorithm-with-pygad) + +[PyGAD](https://pygad.readthedocs.io/) is a genetic algorithm Python 3 library for solving optimization problems. One of these problems is training machine learning algorithms. + +PyGAD has a module called [pygad.kerasga](https://github.com/ahmedfgad/KerasGA). It trains Keras models using the genetic algorithm. On January 3rd, 2021, a new release of [PyGAD 2.10.0](https://pygad.readthedocs.io/) brought a new module called [pygad.torchga](https://github.com/ahmedfgad/TorchGA) to train PyTorch models. It’s very easy to use, but there are a few tricky steps. + +So, in this tutorial, we’ll explore how to use PyGAD to train PyTorch models. + +[![PyGAD+PyTorch](https://user-images.githubusercontent.com/16560492/111009678-5457b580-8362-11eb-899a-39e2f96984df.png)](https://neptune.ai/blog/train-pytorch-models-using-genetic-algorithm-with-pygad) + +### [A Guide to Genetic ‘Learning’ Algorithms for Optimization](https://towardsdatascience.com/a-guide-to-genetic-learning-algorithms-for-optimization-e1067cdc77e7) + +## For More Information + +There are different resources that can be used to get started with the genetic algorithm and building it in Python. + +### Tutorial: Implementing Genetic Algorithm in Python + +To start with coding the genetic algorithm, you can check the tutorial titled [**Genetic Algorithm Implementation in Python**](https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad) available at these links: + +- [LinkedIn](https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad) +- [Towards Data Science](https://towardsdatascience.com/genetic-algorithm-implementation-in-python-5ab67bb124a6) +- [KDnuggets](https://www.kdnuggets.com/2018/07/genetic-algorithm-implementation-python.html) + +[This tutorial](https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad) is prepared based on a previous version of the project but it still a good resource to start with coding the genetic algorithm. + +[![Genetic Algorithm Implementation in Python](https://user-images.githubusercontent.com/16560492/78830052-a3c19300-79e7-11ea-8b9b-4b343ea4049c.png)](https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad) + +### Tutorial: Introduction to Genetic Algorithm + +Get started with the genetic algorithm by reading the tutorial titled [**Introduction to Optimization with Genetic Algorithm**](https://www.linkedin.com/pulse/introduction-optimization-genetic-algorithm-ahmed-gad) which is available at these links: + +* [LinkedIn](https://www.linkedin.com/pulse/introduction-optimization-genetic-algorithm-ahmed-gad) +* [Towards Data Science](https://www.kdnuggets.com/2018/03/introduction-optimization-with-genetic-algorithm.html) +* [KDnuggets](https://towardsdatascience.com/introduction-to-optimization-with-genetic-algorithm-2f5001d9964b) + +[![Introduction to Genetic Algorithm](https://user-images.githubusercontent.com/16560492/82078259-26252d00-96e1-11ea-9a02-52a99e1054b9.jpg)](https://www.linkedin.com/pulse/introduction-optimization-genetic-algorithm-ahmed-gad) + +### Tutorial: Build Neural Networks in Python + +Read about building neural networks in Python through the tutorial titled [**Artificial Neural Network Implementation using NumPy and Classification of the Fruits360 Image Dataset**](https://www.linkedin.com/pulse/artificial-neural-network-implementation-using-numpy-fruits360-gad) available at these links: + +* [LinkedIn](https://www.linkedin.com/pulse/artificial-neural-network-implementation-using-numpy-fruits360-gad) +* [Towards Data Science](https://towardsdatascience.com/artificial-neural-network-implementation-using-numpy-and-classification-of-the-fruits360-image-3c56affa4491) +* [KDnuggets](https://www.kdnuggets.com/2019/02/artificial-neural-network-implementation-using-numpy-and-image-classification.html) + +[![Building Neural Networks Python](https://user-images.githubusercontent.com/16560492/82078281-30472b80-96e1-11ea-8017-6a1f4383d602.jpg)](https://www.linkedin.com/pulse/artificial-neural-network-implementation-using-numpy-fruits360-gad) + +### Tutorial: Optimize Neural Networks with Genetic Algorithm + +Read about training neural networks using the genetic algorithm through the tutorial titled [**Artificial Neural Networks Optimization using Genetic Algorithm with Python**](https://www.linkedin.com/pulse/artificial-neural-networks-optimization-using-genetic-ahmed-gad) available at these links: + +- [LinkedIn](https://www.linkedin.com/pulse/artificial-neural-networks-optimization-using-genetic-ahmed-gad) +- [Towards Data Science](https://towardsdatascience.com/artificial-neural-networks-optimization-using-genetic-algorithm-with-python-1fe8ed17733e) +- [KDnuggets](https://www.kdnuggets.com/2019/03/artificial-neural-networks-optimization-genetic-algorithm-python.html) + +[![Training Neural Networks using Genetic Algorithm Python](https://user-images.githubusercontent.com/16560492/82078300-376e3980-96e1-11ea-821c-aa6b8ceb44d4.jpg)](https://www.linkedin.com/pulse/artificial-neural-networks-optimization-using-genetic-ahmed-gad) + +### Tutorial: Building CNN in Python + +To start with coding the genetic algorithm, you can check the tutorial titled [**Building Convolutional Neural Network using NumPy from Scratch**](https://www.linkedin.com/pulse/building-convolutional-neural-network-using-numpy-from-ahmed-gad) available at these links: + +- [LinkedIn](https://www.linkedin.com/pulse/building-convolutional-neural-network-using-numpy-from-ahmed-gad) +- [Towards Data Science](https://towardsdatascience.com/building-convolutional-neural-network-using-numpy-from-scratch-b30aac50e50a) +- [KDnuggets](https://www.kdnuggets.com/2018/04/building-convolutional-neural-network-numpy-scratch.html) +- [Chinese Translation](http://m.aliyun.com/yunqi/articles/585741) + +[This tutorial](https://www.linkedin.com/pulse/building-convolutional-neural-network-using-numpy-from-ahmed-gad)) is prepared based on a previous version of the project but it still a good resource to start with coding CNNs. + +[![Building CNN in Python](https://user-images.githubusercontent.com/16560492/82431022-6c3a1200-9a8e-11ea-8f1b-b055196d76e3.png)](https://www.linkedin.com/pulse/building-convolutional-neural-network-using-numpy-from-ahmed-gad) + +### Tutorial: Derivation of CNN from FCNN + +Get started with the genetic algorithm by reading the tutorial titled [**Derivation of Convolutional Neural Network from Fully Connected Network Step-By-Step**](https://www.linkedin.com/pulse/derivation-convolutional-neural-network-from-fully-connected-gad) which is available at these links: + +* [LinkedIn](https://www.linkedin.com/pulse/derivation-convolutional-neural-network-from-fully-connected-gad) +* [Towards Data Science](https://towardsdatascience.com/derivation-of-convolutional-neural-network-from-fully-connected-network-step-by-step-b42ebafa5275) +* [KDnuggets](https://www.kdnuggets.com/2018/04/derivation-convolutional-neural-network-fully-connected-step-by-step.html) + +[![Derivation of CNN from FCNN](https://user-images.githubusercontent.com/16560492/82431369-db176b00-9a8e-11ea-99bd-e845192873fc.png)](https://www.linkedin.com/pulse/derivation-convolutional-neural-network-from-fully-connected-gad) + +### Book: Practical Computer Vision Applications Using Deep Learning with CNNs + +You can also check my book cited as [**Ahmed Fawzy Gad 'Practical Computer Vision Applications Using Deep Learning with CNNs'. Dec. 2018, Apress, 978-1-4842-4167-7**](https://www.amazon.com/Practical-Computer-Vision-Applications-Learning/dp/1484241665) which discusses neural networks, convolutional neural networks, deep learning, genetic algorithm, and more. + +Find the book at these links: + +- [Amazon](https://www.amazon.com/Practical-Computer-Vision-Applications-Learning/dp/1484241665) +- [Springer](https://link.springer.com/book/10.1007/978-1-4842-4167-7) +- [Apress](https://www.apress.com/gp/book/9781484241660) +- [O'Reilly](https://www.oreilly.com/library/view/practical-computer-vision/9781484241677) +- [Google Books](https://books.google.com.eg/books?id=xLd9DwAAQBAJ) + +![Fig04](https://user-images.githubusercontent.com/16560492/78830077-ae7c2800-79e7-11ea-980b-53b6bd879eeb.jpg) + +## More Links + +https://rodriguezanton.com/identifying-contact-states-for-2d-objects-using-pygad-and/ + +https://torvaney.github.io/projects/t9-optimised diff --git a/docs/source/lifecycle.md b/docs/source/lifecycle.md new file mode 100644 index 0000000..9e8345e --- /dev/null +++ b/docs/source/lifecycle.md @@ -0,0 +1,94 @@ +# Life Cycle of PyGAD + +The next figure shows the main steps in the life cycle of a `pygad.GA` instance. The genetic algorithm starts from an initial population and repeats the same steps once per generation. It measures the fitness of every solution, selects the parents, applies crossover and mutation to make offspring, and then builds the next generation. PyGAD stops when all generations are done or when the function passed to the `on_generation` parameter returns the string `stop`. + +:::{figure} images/ga_lifecycle.* +:alt: The PyGAD genetic algorithm life cycle +:width: 480px +:align: center + +The main steps of the genetic algorithm in PyGAD. +::: + +The next figure shows the same life cycle in more detail, including the callback functions that PyGAD calls at each stage. + +![PyGAD Lifecycle](https://user-images.githubusercontent.com/16560492/220486073-c5b6089d-81e4-44d9-a53c-385f479a7273.jpg) + +The next code implements all the callback functions to trace the execution of the genetic algorithm. Each callback function prints its name. + +```python +import pygad +import numpy + +function_inputs = [4,-2,3.5,5,-11,-4.7] +desired_output = 44 + +def fitness_func(ga_instance, solution, solution_idx): + output = numpy.sum(solution*function_inputs) + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + return fitness + +fitness_function = fitness_func + +def on_start(ga_instance): + print("on_start()") + +def on_fitness(ga_instance, population_fitness): + print("on_fitness()") + +def on_parents(ga_instance, selected_parents): + print("on_parents()") + +def on_crossover(ga_instance, offspring_crossover): + print("on_crossover()") + +def on_mutation(ga_instance, offspring_mutation): + print("on_mutation()") + +def on_generation(ga_instance): + print("on_generation()") + +def on_stop(ga_instance, last_population_fitness): + print("on_stop()") + +ga_instance = pygad.GA(num_generations=3, + num_parents_mating=5, + fitness_func=fitness_function, + sol_per_pop=10, + num_genes=len(function_inputs), + on_start=on_start, + on_fitness=on_fitness, + on_parents=on_parents, + on_crossover=on_crossover, + on_mutation=on_mutation, + on_generation=on_generation, + on_stop=on_stop) + +ga_instance.run() +``` + +Based on the used 3 generations as assigned to the `num_generations` argument, here is the output. + +``` +on_start() + +on_fitness() +on_parents() +on_crossover() +on_mutation() +on_generation() + +on_fitness() +on_parents() +on_crossover() +on_mutation() +on_generation() + +on_fitness() +on_parents() +on_crossover() +on_mutation() +on_generation() + +on_stop() +``` diff --git a/docs/source/pygad.md b/docs/source/pygad.md index 928a384..a4dd1ae 100644 --- a/docs/source/pygad.md +++ b/docs/source/pygad.md @@ -16,7 +16,7 @@ The `pygad.GA` class constructor supports the following parameters: - `num_generations`: Number of generations. - `num_parents_mating`: Number of solutions to be selected as parents. -- `fitness_func`: Accepts a function/method and returns the fitness value(s) of the solution. If a function is passed, then it must accept 3 parameters (1. the instance of the `pygad.GA` class, 2. a single solution, and 3. its index in the population). If method, then it accepts a fourth parameter representing the method's class instance. Check the [Preparing the fitness_func Parameter](https://pygad.readthedocs.io/en/latest/pygad.html#preparing-the-fitness-func-parameter) section for information about creating such a function. In [PyGAD 3.2.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-2-0), multi-objective optimization is supported. To consider the problem as multi-objective, just return a `list`, `tuple`, or `numpy.ndarray` from the fitness function. +- `fitness_func`: Accepts a function/method and returns the fitness value(s) of the solution. If a function is passed, then it must accept 3 parameters (1. the instance of the `pygad.GA` class, 2. a single solution, and 3. its index in the population). If method, then it accepts a fourth parameter representing the method's class instance. Check the [Preparing the fitness_func Parameter](https://pygad.readthedocs.io/en/latest/steps_to_use.html#preparing-the-fitness-func-parameter) section for information about creating such a function. In [PyGAD 3.2.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-2-0), multi-objective optimization is supported. To consider the problem as multi-objective, just return a `list`, `tuple`, or `numpy.ndarray` from the fitness function. - `fitness_batch_size=None`: A new optional parameter called `fitness_batch_size` is supported to calculate the fitness function in batches. If it is assigned the value `1` or `None` (default), then the normal flow is used where the fitness function is called for each individual solution. If the `fitness_batch_size` parameter is assigned a value satisfying this condition `1 < fitness_batch_size <= sol_per_pop`, then the solutions are grouped into batches of size `fitness_batch_size` and the fitness function is called once for each batch. Check the [Batch Fitness Calculation](https://pygad.readthedocs.io/en/latest/fitness_calculation.html#batch-fitness-calculation) section for more details and examples. Added in from [PyGAD 2.19.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-19-0). - `initial_population`: A user-defined initial population. It is useful when the user wants to start the generations with a custom initial population. It defaults to `None` which means no initial population is specified by the user. In this case, [PyGAD](https://pypi.org/project/pygad) creates an initial population using the `sol_per_pop` and `num_genes` parameters. An exception is raised if the `initial_population` is `None` while any of the 2 parameters (`sol_per_pop` or `num_genes`) is also `None`. Introduced in [PyGAD 2.0.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-0-0) and higher. - `sol_per_pop`: Number of solutions (i.e. chromosomes) within the population. This parameter has no action if `initial_population` parameter exists. @@ -24,13 +24,13 @@ The `pygad.GA` class constructor supports the following parameters: - `gene_type=float`: Controls the gene type. It can be assigned to a single data type that is applied to all genes or can specify the data type of each individual gene. It defaults to `float` which means all genes are of `float` data type. Starting from [PyGAD 2.9.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-9-0), the `gene_type` parameter can be assigned to a numeric value of any of these types: `int`, `float`, and `numpy.int/uint/float(8-64)`. Starting from [PyGAD 2.14.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-14-0), it can be assigned to a `list`, `tuple`, or a `numpy.ndarray` which hold a data type for each gene (e.g. `gene_type=[int, float, numpy.int8]`). This helps to control the data type of each individual gene. In [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0), a precision for the `float` data types can be specified (e.g. `gene_type=[float, 2]`. - `init_range_low=-4`: The lower value of the random range from which the gene values in the initial population are selected. `init_range_low` defaults to `-4`. Available in [PyGAD 1.0.20](https://pygad.readthedocs.io/en/latest/releases.html#pygad-1-0-20) and higher. This parameter has no action if the `initial_population` parameter exists. - `init_range_high=4`: The upper value of the random range from which the gene values in the initial population are selected. `init_range_high` defaults to `+4`. Available in [PyGAD 1.0.20](https://pygad.readthedocs.io/en/latest/releases.html#pygad-1-0-20) and higher. This parameter has no action if the `initial_population` parameter exists. -- `parent_selection_type="sss"`: The parent selection type. Supported types are `sss` (for steady-state selection), `rws` (for roulette wheel selection), `sus` (for stochastic universal selection), `rank` (for rank selection), `random` (for random selection), and `tournament` (for tournament selection). A custom parent selection function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/utils.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about building a user-defined parent selection function. +- `parent_selection_type="sss"`: The parent selection type. Supported types are `sss` (for steady-state selection), `rws` (for roulette wheel selection), `sus` (for stochastic universal selection), `rank` (for rank selection), `random` (for random selection), and `tournament` (for tournament selection). A custom parent selection function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/user_defined_operators.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about building a user-defined parent selection function. - `keep_parents=-1`: The number of parents to keep in the next population. `-1` (default) means keep all the parents. `0` means keep no parents. A value greater than `0` means keep that number of parents. The value of `keep_parents` cannot be less than `-1` or greater than the number of solutions in the population (`sol_per_pop`). Starting from [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0), this parameter has an effect only when the `keep_elitism` parameter is `0`. Starting from PyGAD 2.20.0, the parents' fitness from the last generation is not re-used if `keep_parents=0`. To see how `keep_parents` and `keep_elitism` work together, check the [How the Number of Offspring Is Decided](https://pygad.readthedocs.io/en/latest/generations.html#how-the-number-of-offspring-is-decided) section. - `keep_elitism=1`: Added in [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0). It takes the value `0` or a positive integer that meets the condition `0 <= keep_elitism <= sol_per_pop`. It defaults to `1`, which means only the best solution in the current generation is kept in the next generation. If set to `0`, it has no effect. If set to a positive integer `K`, then the best `K` solutions are kept in the next generation. It cannot be greater than the value of the `sol_per_pop` parameter. If this parameter is not `0`, then the `keep_parents` parameter has no effect. To see how `keep_elitism` and `keep_parents` work together, check the [How the Number of Offspring Is Decided](https://pygad.readthedocs.io/en/latest/generations.html#how-the-number-of-offspring-is-decided) section. - `K_tournament=3`: In case that the parent selection type is `tournament`, the `K_tournament` specifies the number of parents participating in the tournament selection. It defaults to `3`. -- `crossover_type="single_point"`: Type of the crossover operation. Supported types are `single_point` (for single-point crossover), `two_points` (for two points crossover), `uniform` (for uniform crossover), and `scattered` (for scattered crossover). Scattered crossover is supported from PyGAD [2.9.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-9-0) and higher. It defaults to `single_point`. A custom crossover function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/utils.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about creating a user-defined crossover function. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, if `crossover_type=None`, then the crossover step is bypassed which means 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. +- `crossover_type="single_point"`: Type of the crossover operation. Supported types are `single_point` (for single-point crossover), `two_points` (for two points crossover), `uniform` (for uniform crossover), and `scattered` (for scattered crossover). Scattered crossover is supported from PyGAD [2.9.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-9-0) and higher. It defaults to `single_point`. A custom crossover function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/user_defined_operators.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about creating a user-defined crossover function. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, if `crossover_type=None`, then the crossover step is bypassed which means 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. - `crossover_probability=None`: The probability of selecting a parent for applying the crossover operation. Its value must be between 0.0 and 1.0 inclusive. For each parent, a random value between 0.0 and 1.0 is generated. If this random value is less than or equal to the value assigned to the `crossover_probability` parameter, then the parent is selected. Added in [PyGAD 2.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-5-0) and higher. -- `mutation_type="random"`: Type of the mutation operation. Supported types are `random` (for random mutation), `swap` (for swap mutation), `inversion` (for inversion mutation), `scramble` (for scramble mutation), and `adaptive` (for adaptive mutation). It defaults to `random`. A custom mutation function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/utils.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about creating a user-defined mutation function. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, if `mutation_type=None`, then the mutation step is bypassed which means 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. `Adaptive` mutation is supported starting from [PyGAD 2.10.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-10-0). For more information about adaptive mutation, go to the [Adaptive Mutation](https://pygad.readthedocs.io/en/latest/utils.html#adaptive-mutation) section. For example about using adaptive mutation, check the [Use Adaptive Mutation in PyGAD](https://pygad.readthedocs.io/en/latest/utils.html#use-adaptive-mutation-in-pygad) section. +- `mutation_type="random"`: Type of the mutation operation. Supported types are `random` (for random mutation), `swap` (for swap mutation), `inversion` (for inversion mutation), `scramble` (for scramble mutation), and `adaptive` (for adaptive mutation). It defaults to `random`. A custom mutation function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/user_defined_operators.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about creating a user-defined mutation function. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, if `mutation_type=None`, then the mutation step is bypassed which means 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. `Adaptive` mutation is supported starting from [PyGAD 2.10.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-10-0). For more information about adaptive mutation, go to the [Adaptive Mutation](https://pygad.readthedocs.io/en/latest/adaptive_mutation.html#adaptive-mutation) section. For example about using adaptive mutation, check the [Use Adaptive Mutation in PyGAD](https://pygad.readthedocs.io/en/latest/adaptive_mutation.html#use-adaptive-mutation-in-pygad) section. - `mutation_probability=None`: The probability of selecting a gene for applying the mutation operation. Its value must be between 0.0 and 1.0 inclusive. For each gene in a solution, a random value between 0.0 and 1.0 is generated. If this random value is less than or equal to the value assigned to the `mutation_probability` parameter, then the gene is selected. If this parameter exists, then there is no need for the 2 parameters `mutation_percent_genes` and `mutation_num_genes`. Added in [PyGAD 2.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-5-0) and higher. - `mutation_by_replacement=False`: An optional bool parameter. It works only when the selected type of mutation is random (`mutation_type="random"`). In this case, `mutation_by_replacement=True` means replace the gene by the randomly generated value. If False, then it has no effect and random mutation works by adding the random value to the gene. Supported in [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher. Check the changes in [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) under the Release History section for an example. - `mutation_percent_genes="default"`: Percentage of genes to mutate. It defaults to the string `"default"` which is later translated into the integer `10` which means 10% of the genes will be mutated. It must be `>0` and `<=100`. Out of this percentage, the number of genes to mutate is deduced which is assigned to the `mutation_num_genes` parameter. The `mutation_percent_genes` parameter has no action if `mutation_probability` or `mutation_num_genes` exist. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, this parameter has no action if `mutation_type` is `None`. @@ -171,650 +171,50 @@ Accepts the following parameter: Returns the genetic algorithm instance. -## Steps to Use `pygad` +## Using PyGAD -To use the `pygad` module, here is a summary of the required steps: +::::{grid} 1 2 2 2 +:gutter: 3 -1. Prepare the `fitness_func` parameter. -2. Prepare the other parameters. -3. Import `pygad`. -4. Create an instance of the `pygad.GA` class. -5. Run the genetic algorithm. -6. Plot the results. -7. Get information about the best solution. -8. Save and load the results. +:::{grid-item-card} Steps to Use PyGAD +:link: steps_to_use +:link-type: doc -The next sections explain each step. - -### Preparing the `fitness_func` Parameter - -Some steps in the genetic algorithm work the same way for every problem, but the fitness calculation does not. There is no single way to calculate the fitness value, and it changes from one problem to another. - -PyGAD has a parameter called `fitness_func` that lets you pass your own function or method to calculate the fitness. This function must be a maximization function, so a solution with a higher fitness value is treated as better than a solution with a lower value. - -The fitness function is where the user can decide whether the optimization problem is single-objective or multi-objective. - -* If the fitness function returns a numeric value, then the problem is single-objective. The numeric data types supported by PyGAD are listed in the `supported_int_float_types` variable of the `pygad.GA` class. -* If the fitness function returns a `list`, `tuple`, or `numpy.ndarray`, then the problem is multi-objective. Even if there is only one element, the problem is still considered multi-objective. Each element represents the fitness value of its corresponding objective. - -A user-defined fitness function lets you use PyGAD to solve any problem by passing the right fitness function. It is very important to understand the problem well before you write the fitness function. - -Here is an example: - -> Given the following function: -> y = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + w6x6 -> where (x1,x2,x3,x4,x5,x6)=(4, -2, 3.5, 5, -11, -4.7) and y=44 -> What are the best values for the 6 weights (w1 to w6)? We are going to use the genetic algorithm to optimize this function. - -So, the task is to use the genetic algorithm to find the best values for the 6 weights `w1` to `w6`. The best solution is the one whose output is closest to the desired output `y=44`. So, the fitness function should return a higher value when the solution's output is closer to `y=44`. Here is a function that does that: - -```python -function_inputs = [4, -2, 3.5, 5, -11, -4.7] # Function inputs. -desired_output = 44 # Function output. - -def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution*function_inputs) - fitness = 1.0 / numpy.abs(output - desired_output) - return fitness -``` - -Because the fitness function returns a numeric value, the problem is single-objective. - -Such a user-defined function must accept 3 parameters: - -1. The instance of the `pygad.GA` class. This helps the user to fetch any property that helps when calculating the fitness. -2. The solution(s) to calculate the fitness value(s). Note that the fitness function can accept multiple solutions only if the `fitness_batch_size` is given a value greater than 1. -3. The indices of the solutions in the population. The number of indices also depends on the `fitness_batch_size` parameter. - -If a method is passed to the `fitness_func` parameter, then it accepts a fourth parameter representing the method's instance. - -The `__code__` object is used to check that this function accepts the required number of parameters. If more or fewer parameters are passed, an exception is raised. - -By writing this function, you have completed a very important step toward using PyGAD. - -#### Preparing Other Parameters - -Here is an example for preparing the other parameters: - -```python -num_generations = 50 -num_parents_mating = 4 - -fitness_function = fitness_func - -sol_per_pop = 8 -num_genes = len(function_inputs) - -init_range_low = -2 -init_range_high = 5 - -parent_selection_type = "sss" -keep_parents = 1 - -crossover_type = "single_point" - -mutation_type = "random" -mutation_percent_genes = 10 -``` - -#### The `on_generation` Parameter - -The optional `on_generation` parameter lets you call a function (with a single parameter) after each generation. Here is a simple function that prints the current generation number and the fitness value of the best solution in the current generation. The `generations_completed` attribute of the `GA` class returns the number of the last completed generation. - -```python -def on_gen(ga_instance): - print("Generation : ", ga_instance.generations_completed) - print("Fitness of the best solution :", ga_instance.best_solution()[1]) -``` - -After being defined, the function is assigned to the `on_generation` parameter of the GA class constructor. By doing that, the `on_gen()` function will be called after each generation. - -```python -ga_instance = pygad.GA(..., - on_generation=on_gen, - ...) -``` - -After the parameters are prepared, we can import PyGAD and build an instance of the `pygad.GA` class. - -### Import `pygad` - -The next step is to import PyGAD as follows: - -```python -import pygad -``` - -The `pygad.GA` class holds the implementation of all methods for running the genetic algorithm. - -### Create an Instance of the `pygad.GA` Class - -The `pygad.GA` class is instantiated where the previously prepared parameters are fed to its constructor. The constructor is responsible for creating the initial population. - -```python -ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - fitness_func=fitness_function, - sol_per_pop=sol_per_pop, - num_genes=num_genes, - init_range_low=init_range_low, - init_range_high=init_range_high, - parent_selection_type=parent_selection_type, - keep_parents=keep_parents, - crossover_type=crossover_type, - mutation_type=mutation_type, - mutation_percent_genes=mutation_percent_genes) -``` - -### Run the Genetic Algorithm - -After an instance of the `pygad.GA` class is created, the next step is to call the `run()` method as follows: - -```python -ga_instance.run() -``` - -Inside this method, the genetic algorithm evolves over the generations by doing the following tasks: - -1. Calculate the fitness values of the solutions in the current population. -2. Select the best solutions as parents in the mating pool. -3. Apply the crossover and mutation operations. -4. Repeat the process for the given number of generations. - -### Plotting Results - -There is a method named `plot_fitness()` which creates a figure summarizing how the fitness values of the solutions change with the generations. - -```python -ga_instance.plot_fitness() -``` - -![Fig02](https://user-images.githubusercontent.com/16560492/78830005-93111d00-79e7-11ea-9d8e-a8d8325a6101.png) - -### Information about the Best Solution - -The following information about the best solution in the last population is returned using the `best_solution()` method. - -- Solution -- Fitness value of the solution -- Index of the solution within the population - -```python -solution, solution_fitness, solution_idx = ga_instance.best_solution() -print(f"Parameters of the best solution : {solution}") -print(f"Fitness value of the best solution = {solution_fitness}") -print(f"Index of the best solution : {solution_idx}") -``` - -Using the `best_solution_generation` attribute of the `pygad.GA` instance, you can get the generation number at which the best fitness was reached. - -```python -if ga_instance.best_solution_generation != -1: - print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") -``` - -### Saving & Loading the Results - -After the `run()` method completes, it is possible to save the current instance of the genetic algorithm to avoid losing the progress made. The `save()` method is available for that purpose. Just pass the file name to it without an extension. According to the next code, a file named `genetic.pkl` will be created and saved in the current directory. - -```python -filename = 'genetic' -ga_instance.save(filename=filename) -``` - -You can also load the saved model using the `load()` function and continue using it. For example, you might run the genetic algorithm for some generations, save its current state using the `save()` method, load the model using the `load()` function, and then call the `run()` method again. - -```python -loaded_ga_instance = pygad.load(filename=filename) -``` - -After the instance is loaded, you can use it to run any method or access any property. - -```python -print(loaded_ga_instance.best_solution()) -``` - -## Life Cycle of PyGAD - -The next figure shows the main steps in the life cycle of a `pygad.GA` instance. The genetic algorithm starts from an initial population and repeats the same steps once per generation. It measures the fitness of every solution, selects the parents, applies crossover and mutation to make offspring, and then builds the next generation. PyGAD stops when all generations are done or when the function passed to the `on_generation` parameter returns the string `stop`. - -:::{figure} images/ga_lifecycle.* -:alt: The PyGAD genetic algorithm life cycle -:width: 480px -:align: center - -The main steps of the genetic algorithm in PyGAD. +A step-by-step walkthrough to build and run the genetic algorithm. ::: -The next figure shows the same life cycle in more detail, including the callback functions that PyGAD calls at each stage. - -![PyGAD Lifecycle](https://user-images.githubusercontent.com/16560492/220486073-c5b6089d-81e4-44d9-a53c-385f479a7273.jpg) - -The next code implements all the callback functions to trace the execution of the genetic algorithm. Each callback function prints its name. - -```python -import pygad -import numpy - -function_inputs = [4,-2,3.5,5,-11,-4.7] -desired_output = 44 - -def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution*function_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - -fitness_function = fitness_func - -def on_start(ga_instance): - print("on_start()") - -def on_fitness(ga_instance, population_fitness): - print("on_fitness()") +:::{grid-item-card} Life Cycle of PyGAD +:link: lifecycle +:link-type: doc -def on_parents(ga_instance, selected_parents): - print("on_parents()") - -def on_crossover(ga_instance, offspring_crossover): - print("on_crossover()") - -def on_mutation(ga_instance, offspring_mutation): - print("on_mutation()") - -def on_generation(ga_instance): - print("on_generation()") - -def on_stop(ga_instance, last_population_fitness): - print("on_stop()") - -ga_instance = pygad.GA(num_generations=3, - num_parents_mating=5, - fitness_func=fitness_function, - sol_per_pop=10, - num_genes=len(function_inputs), - on_start=on_start, - on_fitness=on_fitness, - on_parents=on_parents, - on_crossover=on_crossover, - on_mutation=on_mutation, - on_generation=on_generation, - on_stop=on_stop) - -ga_instance.run() -``` - -Based on the used 3 generations as assigned to the `num_generations` argument, here is the output. - -``` -on_start() - -on_fitness() -on_parents() -on_crossover() -on_mutation() -on_generation() - -on_fitness() -on_parents() -on_crossover() -on_mutation() -on_generation() - -on_fitness() -on_parents() -on_crossover() -on_mutation() -on_generation() +How a generation runs and where each callback is called. +::: -on_stop() -``` +:::: ## Examples This section gives the complete code of some examples that use `pygad`. Each subsection builds a different example. -### Linear Model Optimization - Single Objective - -This example is discussed in the [Steps to Use PyGAD](https://pygad.readthedocs.io/en/latest/pygad.html#steps-to-use-pygad) section which optimizes a linear model. Its complete code is listed below. - -```python -import pygad -import numpy - -""" -Given the following function: - y = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + w6x6 - where (x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7) and y=44 -What are the best values for the 6 weights (w1 to w6)? We are going to use the genetic algorithm to optimize this function. -""" - -function_inputs = [4,-2,3.5,5,-11,-4.7] # Function inputs. -desired_output = 44 # Function output. - -def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution*function_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - -num_generations = 100 # Number of generations. -num_parents_mating = 10 # Number of solutions to be selected as parents in the mating pool. - -sol_per_pop = 20 # Number of solutions in the population. -num_genes = len(function_inputs) - -last_fitness = 0 -def on_generation(ga_instance): - global last_fitness - print(f"Generation = {ga_instance.generations_completed}") - print(f"Fitness = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1]}") - print(f"Change = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1] - last_fitness}") - last_fitness = ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1] - -ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - sol_per_pop=sol_per_pop, - num_genes=num_genes, - fitness_func=fitness_func, - on_generation=on_generation) - -# Running the GA to optimize the parameters of the function. -ga_instance.run() - -ga_instance.plot_fitness() - -# Returning the details of the best solution. -solution, solution_fitness, solution_idx = ga_instance.best_solution(ga_instance.last_generation_fitness) -print(f"Parameters of the best solution : {solution}") -print(f"Fitness value of the best solution = {solution_fitness}") -print(f"Index of the best solution : {solution_idx}") +::::{grid} 1 2 2 2 +:gutter: 3 -prediction = numpy.sum(numpy.array(function_inputs)*solution) -print(f"Predicted output based on the best solution : {prediction}") - -if ga_instance.best_solution_generation != -1: - print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") - -# Saving the GA instance. -filename = 'genetic' # The filename to which the instance is saved. The name is without extension. -ga_instance.save(filename=filename) - -# Loading the saved GA instance. -loaded_ga_instance = pygad.load(filename=filename) -loaded_ga_instance.plot_fitness() -``` - -### Linear Model Optimization - Multi-Objective - -This is a multi-objective optimization example that optimizes these 2 functions: - -1. `y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + w6x6` -2. `y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + w6x12` - -Where: - -1. `(x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7)` and `y=50` -2. `(x7,x8,x9,x10,x11,x12)=(-2,0.7,-9,1.4,3,5)` and `y=30` - -The 2 functions use the same parameters (weights) `w1` to `w6`. - -The goal is to use PyGAD to find the optimal values for such weights that satisfy the 2 functions `y1` and `y2`. - -To use PyGAD to solve multi-objective problems, the only adjustment is to return a `list`, `tuple`, or `numpy.ndarray` from the fitness function. Each element represents the fitness of an objective in order. That is the first element is the fitness of the first objective, the second element is the fitness for the second objective, and so on. - -```python -import pygad -import numpy - -""" -Given these 2 functions: - y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + w6x6 - y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + w6x12 - where (x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7) and y=50 - and (x7,x8,x9,x10,x11,x12)=(-2,0.7,-9,1.4,3,5) and y=30 -What are the best values for the 6 weights (w1 to w6)? We are going to use the genetic algorithm to optimize these 2 functions. -This is a multi-objective optimization problem. - -PyGAD considers the problem as multi-objective if the fitness function returns: - 1) List. - 2) Or tuple. - 3) Or numpy.ndarray. -""" - -function_inputs1 = [4,-2,3.5,5,-11,-4.7] # Function 1 inputs. -function_inputs2 = [-2,0.7,-9,1.4,3,5] # Function 2 inputs. -desired_output1 = 50 # Function 1 output. -desired_output2 = 30 # Function 2 output. - -def fitness_func(ga_instance, solution, solution_idx): - output1 = numpy.sum(solution*function_inputs1) - output2 = numpy.sum(solution*function_inputs2) - fitness1 = 1.0 / (numpy.abs(output1 - desired_output1) + 0.000001) - fitness2 = 1.0 / (numpy.abs(output2 - desired_output2) + 0.000001) - return [fitness1, fitness2] - -num_generations = 100 -num_parents_mating = 10 - -sol_per_pop = 20 -num_genes = len(function_inputs1) - -ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - sol_per_pop=sol_per_pop, - num_genes=num_genes, - fitness_func=fitness_func, - parent_selection_type='nsga2') - -ga_instance.run() - -ga_instance.plot_fitness(label=['Obj 1', 'Obj 2']) - -solution, solution_fitness, solution_idx = ga_instance.best_solution(ga_instance.last_generation_fitness) -print(f"Parameters of the best solution : {solution}") -print(f"Fitness value of the best solution = {solution_fitness}") - -prediction = numpy.sum(numpy.array(function_inputs1)*solution) -print(f"Predicted output 1 based on the best solution : {prediction}") -prediction = numpy.sum(numpy.array(function_inputs2)*solution) -print(f"Predicted output 2 based on the best solution : {prediction}") -``` - -This is the result of the print statements. The predicted outputs are close to the desired outputs. - -``` -Parameters of the best solution : [ 0.79676439 -2.98823386 -4.12677662 5.70539445 -2.02797016 -1.07243922] -Fitness value of the best solution = [ 1.68090829 349.8591915 ] -Predicted output 1 based on the best solution : 50.59491545442283 -Predicted output 2 based on the best solution : 29.99714270722312 -``` - -This is the figure created by the `plot_fitness()` method. The fitness of the first objective is shown in green, and the fitness of the second objective is shown in blue. - -![multi-objective-pygad](https://github.com/ahmedfgad/GeneticAlgorithmPython/assets/16560492/7896f8d8-01c5-4ff9-8d15-52191c309b63) - -### Reproducing Images - -This project reproduces a single image using PyGAD by evolving pixel values. This project works with both color and gray images. Check this project at [GitHub](https://github.com/ahmedfgad/GARI): https://github.com/ahmedfgad/GARI. - -For more information about this project, read this tutorial titled [Reproducing Images using a Genetic Algorithm with Python](https://www.linkedin.com/pulse/reproducing-images-using-genetic-algorithm-python-ahmed-gad) available at these links: - -- [Heartbeat](https://heartbeat.fritz.ai/reproducing-images-using-a-genetic-algorithm-with-python-91fc701ff84): https://heartbeat.fritz.ai/reproducing-images-using-a-genetic-algorithm-with-python-91fc701ff84 -- [LinkedIn](https://www.linkedin.com/pulse/reproducing-images-using-genetic-algorithm-python-ahmed-gad): https://www.linkedin.com/pulse/reproducing-images-using-genetic-algorithm-python-ahmed-gad - -#### Project Steps - -The steps to follow in order to reproduce an image are as follows: - -- Read an image -- Prepare the fitness function -- Create an instance of the pygad.GA class with the appropriate parameters -- Run PyGAD -- Plot results -- Calculate some statistics - -The next sections discuss the code of each step. - -#### Read an Image - -There is an image named `fruit.jpg` in the [GARI project](https://github.com/ahmedfgad/GARI) which is read according to the next code. - -```python -import imageio -import numpy - -target_im = imageio.imread('fruit.jpg') -target_im = numpy.asarray(target_im/255, dtype=float) -``` - -Here is the read image. - -![fruit](https://user-images.githubusercontent.com/16560492/36948808-f0ac882e-1fe8-11e8-8d07-1307e3477fd0.jpg) - -Based on the chromosome representation used in the example, the pixel values can be in the 0-255 range, the 0-1 range, or any other range. - -Note that the range of pixel values affects other parameters, like the range from which random values are selected during mutation and the range of values used in the initial population. So, be consistent. - -#### Prepare the Fitness Function - -The next code creates a function that will be used as a fitness function for calculating the fitness value for each solution in the population. This function must be a maximization function that accepts 3 parameters representing the instance of the `pygad.GA` class, a solution, and its index. It returns a value representing the fitness value. - -```python -import gari - -target_chromosome = gari.img2chromosome(target_im) - -def fitness_fun(ga_instance, solution, solution_idx): - fitness = numpy.sum(numpy.abs(target_chromosome-solution)) - - # Negating the fitness value to make it increasing rather than decreasing. - fitness = numpy.sum(target_chromosome) - fitness - return fitness -``` - -The fitness value is calculated using the sum of absolute difference between genes values in the original and reproduced chromosomes. The `gari.img2chromosome()` function is called before the fitness function to represent the image as a vector because the genetic algorithm can work with 1D chromosomes. - -The implementation of the `gari` module is available at the [GARI GitHub project](https://github.com/ahmedfgad/GARI/blob/master/gari.py) and its code is listed below. - - ```python -import numpy -import functools -import operator - -def img2chromosome(img_arr): - return numpy.reshape(img_arr, (functools.reduce(operator.mul, img_arr.shape))) - -def chromosome2img(vector, shape): - if len(vector) != functools.reduce(operator.mul, shape): - raise ValueError(f"A vector of length {len(vector)} into an array of shape {shape}.") - - return numpy.reshape(vector, shape) - ``` - -#### Create an Instance of the `pygad.GA` Class - -It is very important to use random mutation and set the `mutation_by_replacement` to `True`. Based on the range of pixel values, the values assigned to the `init_range_low`, `init_range_high`, `random_mutation_min_val`, and `random_mutation_max_val` parameters should be changed. - -If the image pixel values range from 0 to 255, then set `init_range_low` and `random_mutation_min_val` to 0 as they are but change `init_range_high` and `random_mutation_max_val` to 255. - -Feel free to change the other parameters or add other parameters. Please check the [PyGAD's documentation](https://pygad.readthedocs.io) for the full list of parameters. - -```python -import pygad - -ga_instance = pygad.GA(num_generations=20000, - num_parents_mating=10, - fitness_func=fitness_fun, - sol_per_pop=20, - num_genes=target_im.size, - init_range_low=0.0, - init_range_high=1.0, - mutation_percent_genes=0.01, - mutation_type="random", - mutation_by_replacement=True, - random_mutation_min_val=0.0, - random_mutation_max_val=1.0) -``` - -#### Run PyGAD - -Simply, call the `run()` method to run PyGAD. - -```python -ga_instance.run() -``` - -#### Plot Results - -After the `run()` method completes, the fitness values of all generations can be viewed in a plot using the `plot_fitness()` method. - -```python -ga_instance.plot_fitness() -``` - -Here is the plot after 20,000 generations. - -![Fitness Values](https://user-images.githubusercontent.com/16560492/82232124-77762c00-992e-11ea-9fc6-14a1cd7a04ff.png) - -#### Calculate Some Statistics - -Here is some information about the best solution. - -```python -# Returning the details of the best solution. -solution, solution_fitness, solution_idx = ga_instance.best_solution() -print(f"Fitness value of the best solution = {solution_fitness}") -print(f"Index of the best solution : {solution_idx}") - -if ga_instance.best_solution_generation != -1: - print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") - -result = gari.chromosome2img(solution, target_im.shape) -matplotlib.pyplot.imshow(result) -matplotlib.pyplot.title("PyGAD & GARI for Reproducing Images") -matplotlib.pyplot.show() -``` - -#### Evolution by Generation - -The solution reached after the 20,000 generations is shown below. - -![solution](https://user-images.githubusercontent.com/16560492/82232405-e0f63a80-992e-11ea-984f-b6ed76465bd1.png) - -After more generations, the result can be improved, as shown below. - -![solution](https://user-images.githubusercontent.com/16560492/82232345-cf149780-992e-11ea-8390-bf1a57a19de7.png) - -The results can also be enhanced by changing the parameters passed to the constructor of the `pygad.GA` class. - -Here is how the image evolves from generation 0 to generation 20,000. - -Generation 0 - -![solution_0](https://user-images.githubusercontent.com/16560492/36948589-b47276f0-1fe5-11e8-8efe-0cd1a225ea3a.png) - -Generation 1,000 - -![solution_1000](https://user-images.githubusercontent.com/16560492/36948823-16f490ee-1fe9-11e8-97db-3e8905ad5440.png) - -Generation 2,500 - -![solution_2500](https://user-images.githubusercontent.com/16560492/36948832-3f314b60-1fe9-11e8-8f4a-4d9a53b99f3d.png) - -Generation 4,500 - -![solution_4500](https://user-images.githubusercontent.com/16560492/36948837-53d1849a-1fe9-11e8-9b36-e9e9291e347b.png) - -Generation 7,000 - -![solution_7000](https://user-images.githubusercontent.com/16560492/36948852-66f1b176-1fe9-11e8-9f9b-460804e94004.png) - -Generation 8,000 +:::{grid-item-card} Linear Model - Single Objective +:link: pygad_example_linear +:link-type: doc +::: -![solution_8500](https://user-images.githubusercontent.com/16560492/36948865-7fbb5158-1fe9-11e8-8c04-8ac3c1f7b1b1.png) +:::{grid-item-card} Linear Model - Multi-Objective +:link: pygad_example_multi_objective +:link-type: doc +::: -Generation 20,000 +:::{grid-item-card} Reproducing Images +:link: pygad_example_reproducing_images +:link-type: doc +::: -![solution](https://user-images.githubusercontent.com/16560492/82232405-e0f63a80-992e-11ea-984f-b6ed76465bd1.png) +:::: ### Clustering @@ -828,3 +228,12 @@ The code is available at the [CoinTex GitHub project](https://github.com/ahmedfg Check this [Paperspace tutorial](https://blog.paperspace.com/building-agent-for-cointex-using-genetic-algorithm) for how the genetic algorithm plays CoinTex: https://blog.paperspace.com/building-agent-for-cointex-using-genetic-algorithm. Check also this [YouTube video](https://youtu.be/Sp_0RGjaL-0) showing the genetic algorithm while playing CoinTex. +:::{toctree} +:hidden: + +steps_to_use +lifecycle +pygad_example_linear +pygad_example_multi_objective +pygad_example_reproducing_images +::: diff --git a/docs/source/pygad_example_linear.md b/docs/source/pygad_example_linear.md new file mode 100644 index 0000000..e31745d --- /dev/null +++ b/docs/source/pygad_example_linear.md @@ -0,0 +1,69 @@ +# Linear Model Optimization - Single Objective + +This example is discussed in the [Steps to Use PyGAD](https://pygad.readthedocs.io/en/latest/steps_to_use.html#steps-to-use-pygad) section which optimizes a linear model. Its complete code is listed below. + +```python +import pygad +import numpy + +""" +Given the following function: + y = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + w6x6 + where (x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7) and y=44 +What are the best values for the 6 weights (w1 to w6)? We are going to use the genetic algorithm to optimize this function. +""" + +function_inputs = [4,-2,3.5,5,-11,-4.7] # Function inputs. +desired_output = 44 # Function output. + +def fitness_func(ga_instance, solution, solution_idx): + output = numpy.sum(solution*function_inputs) + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + return fitness + +num_generations = 100 # Number of generations. +num_parents_mating = 10 # Number of solutions to be selected as parents in the mating pool. + +sol_per_pop = 20 # Number of solutions in the population. +num_genes = len(function_inputs) + +last_fitness = 0 +def on_generation(ga_instance): + global last_fitness + print(f"Generation = {ga_instance.generations_completed}") + print(f"Fitness = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1]}") + print(f"Change = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1] - last_fitness}") + last_fitness = ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1] + +ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=num_parents_mating, + sol_per_pop=sol_per_pop, + num_genes=num_genes, + fitness_func=fitness_func, + on_generation=on_generation) + +# Running the GA to optimize the parameters of the function. +ga_instance.run() + +ga_instance.plot_fitness() + +# Returning the details of the best solution. +solution, solution_fitness, solution_idx = ga_instance.best_solution(ga_instance.last_generation_fitness) +print(f"Parameters of the best solution : {solution}") +print(f"Fitness value of the best solution = {solution_fitness}") +print(f"Index of the best solution : {solution_idx}") + +prediction = numpy.sum(numpy.array(function_inputs)*solution) +print(f"Predicted output based on the best solution : {prediction}") + +if ga_instance.best_solution_generation != -1: + print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") + +# Saving the GA instance. +filename = 'genetic' # The filename to which the instance is saved. The name is without extension. +ga_instance.save(filename=filename) + +# Loading the saved GA instance. +loaded_ga_instance = pygad.load(filename=filename) +loaded_ga_instance.plot_fitness() +``` diff --git a/docs/source/pygad_example_multi_objective.md b/docs/source/pygad_example_multi_objective.md new file mode 100644 index 0000000..272de0f --- /dev/null +++ b/docs/source/pygad_example_multi_objective.md @@ -0,0 +1,88 @@ +# Linear Model Optimization - Multi-Objective + +This is a multi-objective optimization example that optimizes these 2 functions: + +1. `y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + w6x6` +2. `y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + w6x12` + +Where: + +1. `(x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7)` and `y=50` +2. `(x7,x8,x9,x10,x11,x12)=(-2,0.7,-9,1.4,3,5)` and `y=30` + +The 2 functions use the same parameters (weights) `w1` to `w6`. + +The goal is to use PyGAD to find the optimal values for such weights that satisfy the 2 functions `y1` and `y2`. + +To use PyGAD to solve multi-objective problems, the only adjustment is to return a `list`, `tuple`, or `numpy.ndarray` from the fitness function. Each element represents the fitness of an objective in order. That is the first element is the fitness of the first objective, the second element is the fitness for the second objective, and so on. + +```python +import pygad +import numpy + +""" +Given these 2 functions: + y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + w6x6 + y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + w6x12 + where (x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7) and y=50 + and (x7,x8,x9,x10,x11,x12)=(-2,0.7,-9,1.4,3,5) and y=30 +What are the best values for the 6 weights (w1 to w6)? We are going to use the genetic algorithm to optimize these 2 functions. +This is a multi-objective optimization problem. + +PyGAD considers the problem as multi-objective if the fitness function returns: + 1) List. + 2) Or tuple. + 3) Or numpy.ndarray. +""" + +function_inputs1 = [4,-2,3.5,5,-11,-4.7] # Function 1 inputs. +function_inputs2 = [-2,0.7,-9,1.4,3,5] # Function 2 inputs. +desired_output1 = 50 # Function 1 output. +desired_output2 = 30 # Function 2 output. + +def fitness_func(ga_instance, solution, solution_idx): + output1 = numpy.sum(solution*function_inputs1) + output2 = numpy.sum(solution*function_inputs2) + fitness1 = 1.0 / (numpy.abs(output1 - desired_output1) + 0.000001) + fitness2 = 1.0 / (numpy.abs(output2 - desired_output2) + 0.000001) + return [fitness1, fitness2] + +num_generations = 100 +num_parents_mating = 10 + +sol_per_pop = 20 +num_genes = len(function_inputs1) + +ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=num_parents_mating, + sol_per_pop=sol_per_pop, + num_genes=num_genes, + fitness_func=fitness_func, + parent_selection_type='nsga2') + +ga_instance.run() + +ga_instance.plot_fitness(label=['Obj 1', 'Obj 2']) + +solution, solution_fitness, solution_idx = ga_instance.best_solution(ga_instance.last_generation_fitness) +print(f"Parameters of the best solution : {solution}") +print(f"Fitness value of the best solution = {solution_fitness}") + +prediction = numpy.sum(numpy.array(function_inputs1)*solution) +print(f"Predicted output 1 based on the best solution : {prediction}") +prediction = numpy.sum(numpy.array(function_inputs2)*solution) +print(f"Predicted output 2 based on the best solution : {prediction}") +``` + +This is the result of the print statements. The predicted outputs are close to the desired outputs. + +``` +Parameters of the best solution : [ 0.79676439 -2.98823386 -4.12677662 5.70539445 -2.02797016 -1.07243922] +Fitness value of the best solution = [ 1.68090829 349.8591915 ] +Predicted output 1 based on the best solution : 50.59491545442283 +Predicted output 2 based on the best solution : 29.99714270722312 +``` + +This is the figure created by the `plot_fitness()` method. The fitness of the first objective is shown in green, and the fitness of the second objective is shown in blue. + +![multi-objective-pygad](https://github.com/ahmedfgad/GeneticAlgorithmPython/assets/16560492/7896f8d8-01c5-4ff9-8d15-52191c309b63) diff --git a/docs/source/pygad_example_reproducing_images.md b/docs/source/pygad_example_reproducing_images.md new file mode 100644 index 0000000..0f2950c --- /dev/null +++ b/docs/source/pygad_example_reproducing_images.md @@ -0,0 +1,183 @@ +# Reproducing Images + +This project reproduces a single image using PyGAD by evolving pixel values. This project works with both color and gray images. Check this project at [GitHub](https://github.com/ahmedfgad/GARI): https://github.com/ahmedfgad/GARI. + +For more information about this project, read this tutorial titled [Reproducing Images using a Genetic Algorithm with Python](https://www.linkedin.com/pulse/reproducing-images-using-genetic-algorithm-python-ahmed-gad) available at these links: + +- [Heartbeat](https://heartbeat.fritz.ai/reproducing-images-using-a-genetic-algorithm-with-python-91fc701ff84): https://heartbeat.fritz.ai/reproducing-images-using-a-genetic-algorithm-with-python-91fc701ff84 +- [LinkedIn](https://www.linkedin.com/pulse/reproducing-images-using-genetic-algorithm-python-ahmed-gad): https://www.linkedin.com/pulse/reproducing-images-using-genetic-algorithm-python-ahmed-gad + +## Project Steps + +The steps to follow in order to reproduce an image are as follows: + +- Read an image +- Prepare the fitness function +- Create an instance of the pygad.GA class with the appropriate parameters +- Run PyGAD +- Plot results +- Calculate some statistics + +The next sections discuss the code of each step. + +## Read an Image + +There is an image named `fruit.jpg` in the [GARI project](https://github.com/ahmedfgad/GARI) which is read according to the next code. + +```python +import imageio +import numpy + +target_im = imageio.imread('fruit.jpg') +target_im = numpy.asarray(target_im/255, dtype=float) +``` + +Here is the read image. + +![fruit](https://user-images.githubusercontent.com/16560492/36948808-f0ac882e-1fe8-11e8-8d07-1307e3477fd0.jpg) + +Based on the chromosome representation used in the example, the pixel values can be in the 0-255 range, the 0-1 range, or any other range. + +Note that the range of pixel values affects other parameters, like the range from which random values are selected during mutation and the range of values used in the initial population. So, be consistent. + +## Prepare the Fitness Function + +The next code creates a function that will be used as a fitness function for calculating the fitness value for each solution in the population. This function must be a maximization function that accepts 3 parameters representing the instance of the `pygad.GA` class, a solution, and its index. It returns a value representing the fitness value. + +```python +import gari + +target_chromosome = gari.img2chromosome(target_im) + +def fitness_fun(ga_instance, solution, solution_idx): + fitness = numpy.sum(numpy.abs(target_chromosome-solution)) + + # Negating the fitness value to make it increasing rather than decreasing. + fitness = numpy.sum(target_chromosome) - fitness + return fitness +``` + +The fitness value is calculated using the sum of absolute difference between genes values in the original and reproduced chromosomes. The `gari.img2chromosome()` function is called before the fitness function to represent the image as a vector because the genetic algorithm can work with 1D chromosomes. + +The implementation of the `gari` module is available at the [GARI GitHub project](https://github.com/ahmedfgad/GARI/blob/master/gari.py) and its code is listed below. + + ```python +import numpy +import functools +import operator + +def img2chromosome(img_arr): + return numpy.reshape(img_arr, (functools.reduce(operator.mul, img_arr.shape))) + +def chromosome2img(vector, shape): + if len(vector) != functools.reduce(operator.mul, shape): + raise ValueError(f"A vector of length {len(vector)} into an array of shape {shape}.") + + return numpy.reshape(vector, shape) + ``` + +## Create an Instance of the `pygad.GA` Class + +It is very important to use random mutation and set the `mutation_by_replacement` to `True`. Based on the range of pixel values, the values assigned to the `init_range_low`, `init_range_high`, `random_mutation_min_val`, and `random_mutation_max_val` parameters should be changed. + +If the image pixel values range from 0 to 255, then set `init_range_low` and `random_mutation_min_val` to 0 as they are but change `init_range_high` and `random_mutation_max_val` to 255. + +Feel free to change the other parameters or add other parameters. Please check the [PyGAD's documentation](https://pygad.readthedocs.io) for the full list of parameters. + +```python +import pygad + +ga_instance = pygad.GA(num_generations=20000, + num_parents_mating=10, + fitness_func=fitness_fun, + sol_per_pop=20, + num_genes=target_im.size, + init_range_low=0.0, + init_range_high=1.0, + mutation_percent_genes=0.01, + mutation_type="random", + mutation_by_replacement=True, + random_mutation_min_val=0.0, + random_mutation_max_val=1.0) +``` + +## Run PyGAD + +Simply, call the `run()` method to run PyGAD. + +```python +ga_instance.run() +``` + +## Plot Results + +After the `run()` method completes, the fitness values of all generations can be viewed in a plot using the `plot_fitness()` method. + +```python +ga_instance.plot_fitness() +``` + +Here is the plot after 20,000 generations. + +![Fitness Values](https://user-images.githubusercontent.com/16560492/82232124-77762c00-992e-11ea-9fc6-14a1cd7a04ff.png) + +## Calculate Some Statistics + +Here is some information about the best solution. + +```python +# Returning the details of the best solution. +solution, solution_fitness, solution_idx = ga_instance.best_solution() +print(f"Fitness value of the best solution = {solution_fitness}") +print(f"Index of the best solution : {solution_idx}") + +if ga_instance.best_solution_generation != -1: + print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") + +result = gari.chromosome2img(solution, target_im.shape) +matplotlib.pyplot.imshow(result) +matplotlib.pyplot.title("PyGAD & GARI for Reproducing Images") +matplotlib.pyplot.show() +``` + +## Evolution by Generation + +The solution reached after the 20,000 generations is shown below. + +![solution](https://user-images.githubusercontent.com/16560492/82232405-e0f63a80-992e-11ea-984f-b6ed76465bd1.png) + +After more generations, the result can be improved, as shown below. + +![solution](https://user-images.githubusercontent.com/16560492/82232345-cf149780-992e-11ea-8390-bf1a57a19de7.png) + +The results can also be enhanced by changing the parameters passed to the constructor of the `pygad.GA` class. + +Here is how the image evolves from generation 0 to generation 20,000. + +Generation 0 + +![solution_0](https://user-images.githubusercontent.com/16560492/36948589-b47276f0-1fe5-11e8-8efe-0cd1a225ea3a.png) + +Generation 1,000 + +![solution_1000](https://user-images.githubusercontent.com/16560492/36948823-16f490ee-1fe9-11e8-97db-3e8905ad5440.png) + +Generation 2,500 + +![solution_2500](https://user-images.githubusercontent.com/16560492/36948832-3f314b60-1fe9-11e8-8f4a-4d9a53b99f3d.png) + +Generation 4,500 + +![solution_4500](https://user-images.githubusercontent.com/16560492/36948837-53d1849a-1fe9-11e8-9b36-e9e9291e347b.png) + +Generation 7,000 + +![solution_7000](https://user-images.githubusercontent.com/16560492/36948852-66f1b176-1fe9-11e8-9f9b-460804e94004.png) + +Generation 8,000 + +![solution_8500](https://user-images.githubusercontent.com/16560492/36948865-7fbb5158-1fe9-11e8-8c04-8ac3c1f7b1b1.png) + +Generation 20,000 + +![solution](https://user-images.githubusercontent.com/16560492/82232405-e0f63a80-992e-11ea-984f-b6ed76465bd1.png) diff --git a/docs/source/releases.md b/docs/source/releases.md index e5e1ceb..0a8c72b 100644 --- a/docs/source/releases.md +++ b/docs/source/releases.md @@ -178,7 +178,7 @@ Release Date: 06 December 2020 Release Date: 03 January 2021 1. Support of a new module `pygad.torchga` to train PyTorch models using PyGAD. Check [its documentation](https://pygad.readthedocs.io/en/latest/torchga.html). -2. Support of adaptive mutation where the mutation rate is determined by the fitness value of each solution. Read the [Adaptive Mutation](https://pygad.readthedocs.io/en/latest/utils.html#adaptive-mutation) section for more details. Also, read this paper: [Libelli, S. Marsili, and P. Alba. "Adaptive mutation in genetic algorithms." Soft computing 4.2 (2000): 76-80.](https://www.researchgate.net/publication/225642916_Adaptive_mutation_in_genetic_algorithms) +2. Support of adaptive mutation where the mutation rate is determined by the fitness value of each solution. Read the [Adaptive Mutation](https://pygad.readthedocs.io/en/latest/adaptive_mutation.html#adaptive-mutation) section for more details. Also, read this paper: [Libelli, S. Marsili, and P. Alba. "Adaptive mutation in genetic algorithms." Soft computing 4.2 (2000): 76-80.](https://www.researchgate.net/publication/225642916_Adaptive_mutation_in_genetic_algorithms) 3. Before the `run()` method completes or exits, the fitness value of the best solution in the current population is appended to the `best_solution_fitness` list attribute. Note that the fitness value of the best solution in the initial population is already saved at the beginning of the list. So, the fitness value of the best solution is saved before the genetic algorithm starts and after it ends. 4. When the parameter `parent_selection_type` is set to `sss` (steady-state selection), then a warning message is printed if the value of the `keep_parents` parameter is set to 0. 5. More validations to the user input parameters. @@ -293,7 +293,7 @@ Release Date: 18 June 2021 Release Date: 19 June 2021 -1. A user-defined function can be passed to the `mutation_type`, `crossover_type`, and `parent_selection_type` parameters in the `pygad.GA` class to create a custom mutation, crossover, and parent selection operators. Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/utils.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details. https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/50 +1. A user-defined function can be passed to the `mutation_type`, `crossover_type`, and `parent_selection_type` parameters in the `pygad.GA` class to create a custom mutation, crossover, and parent selection operators. Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/user_defined_operators.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details. https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/50 ## PyGAD 2.16.1 diff --git a/docs/source/steps_to_use.md b/docs/source/steps_to_use.md new file mode 100644 index 0000000..e8eacf2 --- /dev/null +++ b/docs/source/steps_to_use.md @@ -0,0 +1,202 @@ +# Steps to Use `pygad` + +To use the `pygad` module, here is a summary of the required steps: + +1. Prepare the `fitness_func` parameter. +2. Prepare the other parameters. +3. Import `pygad`. +4. Create an instance of the `pygad.GA` class. +5. Run the genetic algorithm. +6. Plot the results. +7. Get information about the best solution. +8. Save and load the results. + +The next sections explain each step. + +## Preparing the `fitness_func` Parameter + +Some steps in the genetic algorithm work the same way for every problem, but the fitness calculation does not. There is no single way to calculate the fitness value, and it changes from one problem to another. + +PyGAD has a parameter called `fitness_func` that lets you pass your own function or method to calculate the fitness. This function must be a maximization function, so a solution with a higher fitness value is treated as better than a solution with a lower value. + +The fitness function is where the user can decide whether the optimization problem is single-objective or multi-objective. + +* If the fitness function returns a numeric value, then the problem is single-objective. The numeric data types supported by PyGAD are listed in the `supported_int_float_types` variable of the `pygad.GA` class. +* If the fitness function returns a `list`, `tuple`, or `numpy.ndarray`, then the problem is multi-objective. Even if there is only one element, the problem is still considered multi-objective. Each element represents the fitness value of its corresponding objective. + +A user-defined fitness function lets you use PyGAD to solve any problem by passing the right fitness function. It is very important to understand the problem well before you write the fitness function. + +Here is an example: + +> Given the following function: +> y = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + w6x6 +> where (x1,x2,x3,x4,x5,x6)=(4, -2, 3.5, 5, -11, -4.7) and y=44 +> What are the best values for the 6 weights (w1 to w6)? We are going to use the genetic algorithm to optimize this function. + +So, the task is to use the genetic algorithm to find the best values for the 6 weights `w1` to `w6`. The best solution is the one whose output is closest to the desired output `y=44`. So, the fitness function should return a higher value when the solution's output is closer to `y=44`. Here is a function that does that: + +```python +function_inputs = [4, -2, 3.5, 5, -11, -4.7] # Function inputs. +desired_output = 44 # Function output. + +def fitness_func(ga_instance, solution, solution_idx): + output = numpy.sum(solution*function_inputs) + fitness = 1.0 / numpy.abs(output - desired_output) + return fitness +``` + +Because the fitness function returns a numeric value, the problem is single-objective. + +Such a user-defined function must accept 3 parameters: + +1. The instance of the `pygad.GA` class. This helps the user to fetch any property that helps when calculating the fitness. +2. The solution(s) to calculate the fitness value(s). Note that the fitness function can accept multiple solutions only if the `fitness_batch_size` is given a value greater than 1. +3. The indices of the solutions in the population. The number of indices also depends on the `fitness_batch_size` parameter. + +If a method is passed to the `fitness_func` parameter, then it accepts a fourth parameter representing the method's instance. + +The `__code__` object is used to check that this function accepts the required number of parameters. If more or fewer parameters are passed, an exception is raised. + +By writing this function, you have completed a very important step toward using PyGAD. + +### Preparing Other Parameters + +Here is an example for preparing the other parameters: + +```python +num_generations = 50 +num_parents_mating = 4 + +fitness_function = fitness_func + +sol_per_pop = 8 +num_genes = len(function_inputs) + +init_range_low = -2 +init_range_high = 5 + +parent_selection_type = "sss" +keep_parents = 1 + +crossover_type = "single_point" + +mutation_type = "random" +mutation_percent_genes = 10 +``` + +### The `on_generation` Parameter + +The optional `on_generation` parameter lets you call a function (with a single parameter) after each generation. Here is a simple function that prints the current generation number and the fitness value of the best solution in the current generation. The `generations_completed` attribute of the `GA` class returns the number of the last completed generation. + +```python +def on_gen(ga_instance): + print("Generation : ", ga_instance.generations_completed) + print("Fitness of the best solution :", ga_instance.best_solution()[1]) +``` + +After being defined, the function is assigned to the `on_generation` parameter of the GA class constructor. By doing that, the `on_gen()` function will be called after each generation. + +```python +ga_instance = pygad.GA(..., + on_generation=on_gen, + ...) +``` + +After the parameters are prepared, we can import PyGAD and build an instance of the `pygad.GA` class. + +## Import `pygad` + +The next step is to import PyGAD as follows: + +```python +import pygad +``` + +The `pygad.GA` class holds the implementation of all methods for running the genetic algorithm. + +## Create an Instance of the `pygad.GA` Class + +The `pygad.GA` class is instantiated where the previously prepared parameters are fed to its constructor. The constructor is responsible for creating the initial population. + +```python +ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=num_parents_mating, + fitness_func=fitness_function, + sol_per_pop=sol_per_pop, + num_genes=num_genes, + init_range_low=init_range_low, + init_range_high=init_range_high, + parent_selection_type=parent_selection_type, + keep_parents=keep_parents, + crossover_type=crossover_type, + mutation_type=mutation_type, + mutation_percent_genes=mutation_percent_genes) +``` + +## Run the Genetic Algorithm + +After an instance of the `pygad.GA` class is created, the next step is to call the `run()` method as follows: + +```python +ga_instance.run() +``` + +Inside this method, the genetic algorithm evolves over the generations by doing the following tasks: + +1. Calculate the fitness values of the solutions in the current population. +2. Select the best solutions as parents in the mating pool. +3. Apply the crossover and mutation operations. +4. Repeat the process for the given number of generations. + +## Plotting Results + +There is a method named `plot_fitness()` which creates a figure summarizing how the fitness values of the solutions change with the generations. + +```python +ga_instance.plot_fitness() +``` + +![Fig02](https://user-images.githubusercontent.com/16560492/78830005-93111d00-79e7-11ea-9d8e-a8d8325a6101.png) + +## Information about the Best Solution + +The following information about the best solution in the last population is returned using the `best_solution()` method. + +- Solution +- Fitness value of the solution +- Index of the solution within the population + +```python +solution, solution_fitness, solution_idx = ga_instance.best_solution() +print(f"Parameters of the best solution : {solution}") +print(f"Fitness value of the best solution = {solution_fitness}") +print(f"Index of the best solution : {solution_idx}") +``` + +Using the `best_solution_generation` attribute of the `pygad.GA` instance, you can get the generation number at which the best fitness was reached. + +```python +if ga_instance.best_solution_generation != -1: + print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") +``` + +## Saving & Loading the Results + +After the `run()` method completes, it is possible to save the current instance of the genetic algorithm to avoid losing the progress made. The `save()` method is available for that purpose. Just pass the file name to it without an extension. According to the next code, a file named `genetic.pkl` will be created and saved in the current directory. + +```python +filename = 'genetic' +ga_instance.save(filename=filename) +``` + +You can also load the saved model using the `load()` function and continue using it. For example, you might run the genetic algorithm for some generations, save its current state using the `save()` method, load the model using the `load()` function, and then call the `run()` method again. + +```python +loaded_ga_instance = pygad.load(filename=filename) +``` + +After the instance is loaded, you can use it to run any method or access any property. + +```python +print(loaded_ga_instance.best_solution()) +``` diff --git a/docs/source/user_defined_operators.md b/docs/source/user_defined_operators.md new file mode 100644 index 0000000..61b6c5e --- /dev/null +++ b/docs/source/user_defined_operators.md @@ -0,0 +1,332 @@ +# User-Defined Crossover, Mutation, and Parent Selection Operators + +Previously, the user could select the type of the crossover, mutation, and parent selection operators by assigning the name of the operator to the following parameters of the `pygad.GA` class's constructor: + +1. `crossover_type` +2. `mutation_type` +3. `parent_selection_type` + +This way, the user can only use the built-in functions for each of these operators. + +Starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0), the user can create a custom crossover, mutation, and parent selection operators and assign these functions to the above parameters. Thus, a new operator can be plugged easily into the [PyGAD Lifecycle](https://pygad.readthedocs.io/en/latest/lifecycle.html#life-cycle-of-pygad). + +This is a sample code that does not use any custom function. + +```python +import pygad +import numpy + +equation_inputs = [4,-2,3.5] +desired_output = 44 + +def fitness_func(ga_instance, solution, solution_idx): + output = numpy.sum(solution * equation_inputs) + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + return fitness + +ga_instance = pygad.GA(num_generations=10, + sol_per_pop=5, + num_parents_mating=2, + num_genes=len(equation_inputs), + fitness_func=fitness_func) + +ga_instance.run() +ga_instance.plot_fitness() +``` + +This section describes the expected input parameters and outputs. For simplicity, all of these custom functions accept the instance of the `pygad.GA` class as the last parameter. + +## User-Defined Crossover Operator + +The user-defined crossover function is a Python function that accepts 3 parameters: + +1. The selected parents. +2. The size of the offspring as a tuple of 2 numbers: (the offspring size, number of genes). +3. The instance from the `pygad.GA` class. This instance helps to retrieve any property like `population`, `gene_type`, `gene_space`, etc. + +This function should return a NumPy array of shape equal to the value passed to the second parameter. + +The next code creates a template for the user-defined crossover operator. You can use any names for the parameters. Note how a NumPy array is returned. + +```python +def crossover_func(parents, offspring_size, ga_instance): + offspring = ... + ... + return numpy.array(offspring) +``` + +As an example, the next code creates a single-point crossover function. By randomly generating a random point (i.e. index of a gene), the function simply uses 2 parents to produce an offspring by copying the genes before the point from the first parent and the remaining from the second parent. + +```python +def crossover_func(parents, offspring_size, ga_instance): + offspring = [] + idx = 0 + while len(offspring) != offspring_size[0]: + parent1 = parents[idx % parents.shape[0], :].copy() + parent2 = parents[(idx + 1) % parents.shape[0], :].copy() + + random_split_point = numpy.random.choice(range(offspring_size[1])) + + parent1[random_split_point:] = parent2[random_split_point:] + + offspring.append(parent1) + + idx += 1 + + return numpy.array(offspring) +``` + +To use this user-defined function, simply assign its name to the `crossover_type` parameter in the constructor of the `pygad.GA` class. The next code gives an example. In this case, the custom function will be called in each generation rather than calling the built-in crossover functions defined in PyGAD. + +```python +ga_instance = pygad.GA(num_generations=10, + sol_per_pop=5, + num_parents_mating=2, + num_genes=len(equation_inputs), + fitness_func=fitness_func, + crossover_type=crossover_func) +``` + +## User-Defined Mutation Operator + +A user-defined mutation function/operator can be created the same way a custom crossover operator/function is created. Simply, it is a Python function that accepts 2 parameters: + +1. The offspring to be mutated. +2. The instance from the `pygad.GA` class. This instance helps to retrieve any property like `population`, `gene_type`, `gene_space`, etc. + +The template for the user-defined mutation function is given in the next code. According to the user preference, the function should make some random changes to the genes. + +```python +def mutation_func(offspring, ga_instance): + ... + return offspring +``` + +The next code builds the random mutation where a single gene from each chromosome is mutated by adding a random number between 0 and 1 to the gene's value. + +```python +def mutation_func(offspring, ga_instance): + + for chromosome_idx in range(offspring.shape[0]): + random_gene_idx = numpy.random.choice(range(offspring.shape[1])) + + offspring[chromosome_idx, random_gene_idx] += numpy.random.random() + + return offspring +``` + +Here is how this function is assigned to the `mutation_type` parameter. + +```python +ga_instance = pygad.GA(num_generations=10, + sol_per_pop=5, + num_parents_mating=2, + num_genes=len(equation_inputs), + fitness_func=fitness_func, + crossover_type=crossover_func, + mutation_type=mutation_func) +``` + +Note that there are other things to take into consideration like: + +- Making sure that each gene conforms to the data type(s) listed in the `gene_type` parameter. +- If the `gene_space` parameter is used, then the new value for the gene should conform to the values/ranges listed. +- Mutating a number of genes that conforms to the parameters `mutation_percent_genes`, `mutation_probability`, and `mutation_num_genes`. +- Whether mutation happens with or without replacement based on the `mutation_by_replacement` parameter. +- The minimum and maximum values from which a random value is generated based on the `random_mutation_min_val` and `random_mutation_max_val` parameters. +- Whether duplicates are allowed or not in the chromosome based on the `allow_duplicate_genes` parameter. + +and more. + +It all depends on your goal in building the mutation function. You may ignore or apply some of these points depending on your goal. + +## User-Defined Parent Selection Operator + +There is not much to add about building a user-defined parent selection function, as it is similar to building a crossover or mutation function. Just create a Python function that accepts 3 parameters: + +1. The fitness values of the current population. +2. The number of parents needed. +3. The instance from the `pygad.GA` class. This instance helps to retrieve any property like `population`, `gene_type`, `gene_space`, etc. + +The function should return 2 outputs: + +1. The selected parents as a NumPy array. Its shape is equal to (the number of selected parents, `num_genes`). Note that the number of selected parents is equal to the value assigned to the second input parameter. +2. The indices of the selected parents inside the population. It is a 1D list with length equal to the number of selected parents. + +The outputs must be of type `numpy.ndarray`. + +Here is a template for building a custom parent selection function. + +```python +def parent_selection_func(fitness, num_parents, ga_instance): + ... + return parents, fitness_sorted[:num_parents] +``` + +The next code builds the steady-state parent selection where the best parents are selected. The number of parents is equal to the value in the `num_parents` parameter. + +```python +def parent_selection_func(fitness, num_parents, ga_instance): + + fitness_sorted = sorted(range(len(fitness)), key=lambda k: fitness[k]) + fitness_sorted.reverse() + + parents = numpy.empty((num_parents, ga_instance.population.shape[1])) + + for parent_num in range(num_parents): + parents[parent_num, :] = ga_instance.population[fitness_sorted[parent_num], :].copy() + + return parents, numpy.array(fitness_sorted[:num_parents]) +``` + +Finally, the defined function is assigned to the `parent_selection_type` parameter as in the next code. + +```python +ga_instance = pygad.GA(num_generations=10, + sol_per_pop=5, + num_parents_mating=2, + num_genes=len(equation_inputs), + fitness_func=fitness_func, + crossover_type=crossover_func, + mutation_type=mutation_func, + parent_selection_type=parent_selection_func) +``` + +## Example + +Now that we have seen how to customize the 3 operators, the next code uses the previous 3 user-defined functions instead of the built-in ones. + +```python +import pygad +import numpy + +equation_inputs = [4,-2,3.5] +desired_output = 44 + +def fitness_func(ga_instance, solution, solution_idx): + output = numpy.sum(solution * equation_inputs) + + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + + return fitness + +def parent_selection_func(fitness, num_parents, ga_instance): + + fitness_sorted = sorted(range(len(fitness)), key=lambda k: fitness[k]) + fitness_sorted.reverse() + + parents = numpy.empty((num_parents, ga_instance.population.shape[1])) + + for parent_num in range(num_parents): + parents[parent_num, :] = ga_instance.population[fitness_sorted[parent_num], :].copy() + + return parents, numpy.array(fitness_sorted[:num_parents]) + +def crossover_func(parents, offspring_size, ga_instance): + + offspring = [] + idx = 0 + while len(offspring) != offspring_size[0]: + parent1 = parents[idx % parents.shape[0], :].copy() + parent2 = parents[(idx + 1) % parents.shape[0], :].copy() + + random_split_point = numpy.random.choice(range(offspring_size[1])) + + parent1[random_split_point:] = parent2[random_split_point:] + + offspring.append(parent1) + + idx += 1 + + return numpy.array(offspring) + +def mutation_func(offspring, ga_instance): + + for chromosome_idx in range(offspring.shape[0]): + random_gene_idx = numpy.random.choice(range(offspring.shape[0])) + + offspring[chromosome_idx, random_gene_idx] += numpy.random.random() + + return offspring + +ga_instance = pygad.GA(num_generations=10, + sol_per_pop=5, + num_parents_mating=2, + num_genes=len(equation_inputs), + fitness_func=fitness_func, + crossover_type=crossover_func, + mutation_type=mutation_func, + parent_selection_type=parent_selection_func) + +ga_instance.run() +ga_instance.plot_fitness() +``` + +This is the same example but using methods instead of functions. + +```python +import pygad +import numpy + +equation_inputs = [4,-2,3.5] +desired_output = 44 + +class Test: + def fitness_func(self, ga_instance, solution, solution_idx): + output = numpy.sum(solution * equation_inputs) + + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + + return fitness + + def parent_selection_func(self, fitness, num_parents, ga_instance): + + fitness_sorted = sorted(range(len(fitness)), key=lambda k: fitness[k]) + fitness_sorted.reverse() + + parents = numpy.empty((num_parents, ga_instance.population.shape[1])) + + for parent_num in range(num_parents): + parents[parent_num, :] = ga_instance.population[fitness_sorted[parent_num], :].copy() + + return parents, numpy.array(fitness_sorted[:num_parents]) + + def crossover_func(self, parents, offspring_size, ga_instance): + + offspring = [] + idx = 0 + while len(offspring) != offspring_size[0]: + parent1 = parents[idx % parents.shape[0], :].copy() + parent2 = parents[(idx + 1) % parents.shape[0], :].copy() + + random_split_point = numpy.random.choice(range(offspring_size[0])) + + parent1[random_split_point:] = parent2[random_split_point:] + + offspring.append(parent1) + + idx += 1 + + return numpy.array(offspring) + + def mutation_func(self, offspring, ga_instance): + + for chromosome_idx in range(offspring.shape[0]): + random_gene_idx = numpy.random.choice(range(offspring.shape[1])) + + offspring[chromosome_idx, random_gene_idx] += numpy.random.random() + + return offspring + +ga_instance = pygad.GA(num_generations=10, + sol_per_pop=5, + num_parents_mating=2, + num_genes=len(equation_inputs), + fitness_func=Test().fitness_func, + parent_selection_type=Test().parent_selection_func, + crossover_type=Test().crossover_func, + mutation_type=Test().mutation_func) + +ga_instance.run() +ga_instance.plot_fitness() +``` diff --git a/docs/source/utils.md b/docs/source/utils.md index e80ffbb..1bf5ee5 100644 --- a/docs/source/utils.md +++ b/docs/source/utils.md @@ -2,7 +2,7 @@ This section of the documentation discusses the **pygad.utils** module. -PyGAD supports different types of operators for selecting the parents, applying the crossover, and mutation. More features will be added in the future. To ask for a new feature, please check the [Ask for Feature](https://pygad.readthedocs.io/en/latest/help.html#ask-for-feature) section. +PyGAD supports different types of operators for selecting the parents, applying the crossover, and mutation. More features will be added in the future. To ask for a new feature, please check the [Ask for Feature](https://pygad.readthedocs.io/en/latest/help_support.html#ask-for-feature) section. The submodules in the `pygad.utils` module are: @@ -243,113 +243,6 @@ The `pygad.utils.mutation` module has some helper methods to assist applying the 9. `adaptive_mutation_randomly()`: Applies the adaptive mutation randomly. A number of genes are selected randomly for mutation. This number depends on the fitness of the solution. The random values are selected based on the 2 parameters `random_mutation_min_val` and `random_mutation_max_val`. 10. `adaptive_mutation_probs_randomly()`: Uses the mutation probabilities to decide which genes to apply the adaptive mutation randomly. -## Adaptive Mutation - -In the regular genetic algorithm, mutation uses a single fixed mutation rate for all solutions, regardless of their fitness values. So, no matter whether a solution has high or low quality, the same number of genes is mutated every time. - -The pitfalls of using a constant mutation rate for all solutions are summarized in this paper [Libelli, S. Marsili, and P. Alba. "Adaptive mutation in genetic algorithms." *Soft computing* 4.2 (2000): 76-80](https://idp.springer.com/authorize/casa?redirect_uri=https://link.springer.com/content/pdf/10.1007/s005000000042.pdf&casa_token=IT4NfJUvslcAAAAA:VegHW6tm2fe3e0R9cRKjuGKkKWXJTQSfNMT6z0kGbMsAllyK1NrEY3cEWg8bj7AJWEQPaqWIJxmHNBHg) as follows: - -> The weak point of "classical" GAs is the total randomness of mutation, which is applied equally to all chromosomes, irrespective of their fitness. Thus a very good chromosome is equally likely to be disrupted by mutation as a bad one. -> -> On the other hand, bad chromosomes are less likely to produce good ones through crossover, because of their lack of building blocks, until they remain unchanged. They would benefit the most from mutation and could be used to spread throughout the parameter space to increase the search thoroughness. So there are two conflicting needs in determining the best probability of mutation. -> -> Usually, a reasonable compromise in the case of a constant mutation is to keep the probability low to avoid disruption of good chromosomes, but this would prevent a high mutation rate of low-fitness chromosomes. Thus a constant probability of mutation would probably miss both goals and result in a slow improvement of the population. - -According to [Libelli, S. Marsili, and P. Alba.](https://idp.springer.com/authorize/casa?redirect_uri=https://link.springer.com/content/pdf/10.1007/s005000000042.pdf&casa_token=IT4NfJUvslcAAAAA:VegHW6tm2fe3e0R9cRKjuGKkKWXJTQSfNMT6z0kGbMsAllyK1NrEY3cEWg8bj7AJWEQPaqWIJxmHNBHg) work, the adaptive mutation solves the problems of constant mutation. - -Adaptive mutation works as follows: - -1. Calculate the average fitness value of the population (`f_avg`). -2. For each chromosome, calculate its fitness value (`f`). -3. If `ff_avg`, then this solution is regarded as a high-quality solution and thus the mutation rate should be kept low to avoid disrupting this high quality solution. - -In PyGAD, if `f=f_avg`, then the solution is regarded as high quality. - -The next figure summarizes the previous steps. - -![Adaptive-Mutation](https://user-images.githubusercontent.com/16560492/103468973-e3c26600-4d2c-11eb-8af3-b3bb39b50540.jpg) - -This strategy is applied in PyGAD. - -### Use Adaptive Mutation in PyGAD - -In [PyGAD 2.10.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-10-0), adaptive mutation is supported. To use it, just follow the following 2 simple steps: - -1. In the constructor of the `pygad.GA` class, set `mutation_type="adaptive"` to specify that the type of mutation is adaptive. -2. Specify the mutation rates for the low and high quality solutions using one of these 3 parameters according to your preference: `mutation_probability`, `mutation_num_genes`, and `mutation_percent_genes`. Please check the [documentation of each of these parameters](https://pygad.readthedocs.io/en/latest/pygad.html#init) for more information. - -When adaptive mutation is used, then the value assigned to any of the 3 parameters can be of any of these data types: - -1. `list` -2. `tuple` -3. `numpy.ndarray` - -Whatever the data type used, the length of the `list`, `tuple`, or the `numpy.ndarray` must be exactly 2. That is there are just 2 values: - -1. The first value is the mutation rate for the low-quality solutions. -2. The second value is the mutation rate for the high-quality solutions. - -PyGAD expects that the first value is higher than the second value and thus a warning is printed in case the first value is lower than the second one. - -Here are some examples to feed the mutation rates: - -```python -# mutation_probability -mutation_probability = [0.25, 0.1] -mutation_probability = (0.35, 0.17) -mutation_probability = numpy.array([0.15, 0.05]) - -# mutation_num_genes -mutation_num_genes = [4, 2] -mutation_num_genes = (3, 1) -mutation_num_genes = numpy.array([7, 2]) - -# mutation_percent_genes -mutation_percent_genes = [25, 12] -mutation_percent_genes = (15, 8) -mutation_percent_genes = numpy.array([21, 13]) -``` - -Assume that the average fitness is 12 and the fitness values of 2 solutions are 15 and 7. If the mutation probabilities are specified as follows: - -```python -mutation_probability = [0.25, 0.1] -``` - -Then the mutation probability of the first solution is 0.1 because its fitness is 15 which is higher than the average fitness 12. The mutation probability of the second solution is 0.25 because its fitness is 7 which is lower than the average fitness 12. - -Here is an example that uses adaptive mutation. - -```python -import pygad -import numpy - -function_inputs = [4,-2,3.5,5,-11,-4.7] # Function inputs. -desired_output = 44 # Function output. - -def fitness_func(ga_instance, solution, solution_idx): - # The fitness function calculates the sum of products between each input and its corresponding weight. - output = numpy.sum(solution*function_inputs) - # The value 0.000001 is used to avoid the Inf value when the denominator numpy.abs(output - desired_output) is 0.0. - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - -# Creating an instance of the GA class inside the ga module. Some parameters are initialized within the constructor. -ga_instance = pygad.GA(num_generations=200, - fitness_func=fitness_func, - num_parents_mating=10, - sol_per_pop=20, - num_genes=len(function_inputs), - mutation_type="adaptive", - mutation_num_genes=(3, 1)) - -# Running the GA to optimize the parameters of the function. -ga_instance.run() - -ga_instance.plot_fitness(title="PyGAD with Adaptive Mutation", linewidth=5) -``` - ## `pygad.utils.parent_selection` Submodule The `pygad.utils.parent_selection` module has a class named `ParentSelection` with the supported parent selection operations: @@ -424,336 +317,30 @@ The `pygad.utils.nsga2` module has a class named `NSGA2` that implements NSGA-II 3. `crowding_distance()`: Calculates the crowding distance for all solutions in the current pareto front. 4. `sort_solutions_nsga2()`: Sort the solutions. If the problem is single-objective, then the solutions are sorted by sorting the fitness values of the population. If it is multi-objective, then non-dominated sorting and crowding distance are applied to sort the solutions. -## User-Defined Crossover, Mutation, and Parent Selection Operators - -Previously, the user could select the type of the crossover, mutation, and parent selection operators by assigning the name of the operator to the following parameters of the `pygad.GA` class's constructor: - -1. `crossover_type` -2. `mutation_type` -3. `parent_selection_type` - -This way, the user can only use the built-in functions for each of these operators. - -Starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0), the user can create a custom crossover, mutation, and parent selection operators and assign these functions to the above parameters. Thus, a new operator can be plugged easily into the [PyGAD Lifecycle](https://pygad.readthedocs.io/en/latest/pygad.html#life-cycle-of-pygad). - -This is a sample code that does not use any custom function. - -```python -import pygad -import numpy - -equation_inputs = [4,-2,3.5] -desired_output = 44 - -def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - -ga_instance = pygad.GA(num_generations=10, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func) - -ga_instance.run() -ga_instance.plot_fitness() -``` - -This section describes the expected input parameters and outputs. For simplicity, all of these custom functions accept the instance of the `pygad.GA` class as the last parameter. - -### User-Defined Crossover Operator - -The user-defined crossover function is a Python function that accepts 3 parameters: - -1. The selected parents. -2. The size of the offspring as a tuple of 2 numbers: (the offspring size, number of genes). -3. The instance from the `pygad.GA` class. This instance helps to retrieve any property like `population`, `gene_type`, `gene_space`, etc. - -This function should return a NumPy array of shape equal to the value passed to the second parameter. - -The next code creates a template for the user-defined crossover operator. You can use any names for the parameters. Note how a NumPy array is returned. - -```python -def crossover_func(parents, offspring_size, ga_instance): - offspring = ... - ... - return numpy.array(offspring) -``` - -As an example, the next code creates a single-point crossover function. By randomly generating a random point (i.e. index of a gene), the function simply uses 2 parents to produce an offspring by copying the genes before the point from the first parent and the remaining from the second parent. - -```python -def crossover_func(parents, offspring_size, ga_instance): - offspring = [] - idx = 0 - while len(offspring) != offspring_size[0]: - parent1 = parents[idx % parents.shape[0], :].copy() - parent2 = parents[(idx + 1) % parents.shape[0], :].copy() - - random_split_point = numpy.random.choice(range(offspring_size[1])) - - parent1[random_split_point:] = parent2[random_split_point:] - - offspring.append(parent1) - - idx += 1 +## More about the Operators - return numpy.array(offspring) -``` +::::{grid} 1 2 2 2 +:gutter: 3 -To use this user-defined function, simply assign its name to the `crossover_type` parameter in the constructor of the `pygad.GA` class. The next code gives an example. In this case, the custom function will be called in each generation rather than calling the built-in crossover functions defined in PyGAD. +:::{grid-item-card} Adaptive Mutation +:link: adaptive_mutation +:link-type: doc -```python -ga_instance = pygad.GA(num_generations=10, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - crossover_type=crossover_func) -``` - -### User-Defined Mutation Operator - -A user-defined mutation function/operator can be created the same way a custom crossover operator/function is created. Simply, it is a Python function that accepts 2 parameters: - -1. The offspring to be mutated. -2. The instance from the `pygad.GA` class. This instance helps to retrieve any property like `population`, `gene_type`, `gene_space`, etc. - -The template for the user-defined mutation function is given in the next code. According to the user preference, the function should make some random changes to the genes. - -```python -def mutation_func(offspring, ga_instance): - ... - return offspring -``` - -The next code builds the random mutation where a single gene from each chromosome is mutated by adding a random number between 0 and 1 to the gene's value. - -```python -def mutation_func(offspring, ga_instance): - - for chromosome_idx in range(offspring.shape[0]): - random_gene_idx = numpy.random.choice(range(offspring.shape[1])) - - offspring[chromosome_idx, random_gene_idx] += numpy.random.random() - - return offspring -``` - -Here is how this function is assigned to the `mutation_type` parameter. - -```python -ga_instance = pygad.GA(num_generations=10, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - crossover_type=crossover_func, - mutation_type=mutation_func) -``` - -Note that there are other things to take into consideration like: - -- Making sure that each gene conforms to the data type(s) listed in the `gene_type` parameter. -- If the `gene_space` parameter is used, then the new value for the gene should conform to the values/ranges listed. -- Mutating a number of genes that conforms to the parameters `mutation_percent_genes`, `mutation_probability`, and `mutation_num_genes`. -- Whether mutation happens with or without replacement based on the `mutation_by_replacement` parameter. -- The minimum and maximum values from which a random value is generated based on the `random_mutation_min_val` and `random_mutation_max_val` parameters. -- Whether duplicates are allowed or not in the chromosome based on the `allow_duplicate_genes` parameter. - -and more. - -It all depends on your goal in building the mutation function. You may ignore or apply some of these points depending on your goal. - -### User-Defined Parent Selection Operator - -There is not much to add about building a user-defined parent selection function, as it is similar to building a crossover or mutation function. Just create a Python function that accepts 3 parameters: - -1. The fitness values of the current population. -2. The number of parents needed. -3. The instance from the `pygad.GA` class. This instance helps to retrieve any property like `population`, `gene_type`, `gene_space`, etc. - -The function should return 2 outputs: - -1. The selected parents as a NumPy array. Its shape is equal to (the number of selected parents, `num_genes`). Note that the number of selected parents is equal to the value assigned to the second input parameter. -2. The indices of the selected parents inside the population. It is a 1D list with length equal to the number of selected parents. - -The outputs must be of type `numpy.ndarray`. - -Here is a template for building a custom parent selection function. - -```python -def parent_selection_func(fitness, num_parents, ga_instance): - ... - return parents, fitness_sorted[:num_parents] -``` - -The next code builds the steady-state parent selection where the best parents are selected. The number of parents is equal to the value in the `num_parents` parameter. - -```python -def parent_selection_func(fitness, num_parents, ga_instance): - - fitness_sorted = sorted(range(len(fitness)), key=lambda k: fitness[k]) - fitness_sorted.reverse() - - parents = numpy.empty((num_parents, ga_instance.population.shape[1])) - - for parent_num in range(num_parents): - parents[parent_num, :] = ga_instance.population[fitness_sorted[parent_num], :].copy() - - return parents, numpy.array(fitness_sorted[:num_parents]) -``` - -Finally, the defined function is assigned to the `parent_selection_type` parameter as in the next code. - -```python -ga_instance = pygad.GA(num_generations=10, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - crossover_type=crossover_func, - mutation_type=mutation_func, - parent_selection_type=parent_selection_func) -``` - -### Example - -Now that we have seen how to customize the 3 operators, the next code uses the previous 3 user-defined functions instead of the built-in ones. - -```python -import pygad -import numpy - -equation_inputs = [4,-2,3.5] -desired_output = 44 - -def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - - return fitness - -def parent_selection_func(fitness, num_parents, ga_instance): - - fitness_sorted = sorted(range(len(fitness)), key=lambda k: fitness[k]) - fitness_sorted.reverse() - - parents = numpy.empty((num_parents, ga_instance.population.shape[1])) - - for parent_num in range(num_parents): - parents[parent_num, :] = ga_instance.population[fitness_sorted[parent_num], :].copy() - - return parents, numpy.array(fitness_sorted[:num_parents]) - -def crossover_func(parents, offspring_size, ga_instance): - - offspring = [] - idx = 0 - while len(offspring) != offspring_size[0]: - parent1 = parents[idx % parents.shape[0], :].copy() - parent2 = parents[(idx + 1) % parents.shape[0], :].copy() - - random_split_point = numpy.random.choice(range(offspring_size[1])) - - parent1[random_split_point:] = parent2[random_split_point:] - - offspring.append(parent1) - - idx += 1 - - return numpy.array(offspring) - -def mutation_func(offspring, ga_instance): - - for chromosome_idx in range(offspring.shape[0]): - random_gene_idx = numpy.random.choice(range(offspring.shape[0])) +Change the mutation rate per solution based on its fitness. +::: - offspring[chromosome_idx, random_gene_idx] += numpy.random.random() +:::{grid-item-card} User-Defined Operators +:link: user_defined_operators +:link-type: doc - return offspring +Plug in your own crossover, mutation, and parent selection. +::: -ga_instance = pygad.GA(num_generations=10, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - crossover_type=crossover_func, - mutation_type=mutation_func, - parent_selection_type=parent_selection_func) - -ga_instance.run() -ga_instance.plot_fitness() -``` +:::: -This is the same example but using methods instead of functions. - -```python -import pygad -import numpy - -equation_inputs = [4,-2,3.5] -desired_output = 44 - -class Test: - def fitness_func(self, ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - - return fitness - - def parent_selection_func(self, fitness, num_parents, ga_instance): - - fitness_sorted = sorted(range(len(fitness)), key=lambda k: fitness[k]) - fitness_sorted.reverse() - - parents = numpy.empty((num_parents, ga_instance.population.shape[1])) - - for parent_num in range(num_parents): - parents[parent_num, :] = ga_instance.population[fitness_sorted[parent_num], :].copy() - - return parents, numpy.array(fitness_sorted[:num_parents]) - - def crossover_func(self, parents, offspring_size, ga_instance): - - offspring = [] - idx = 0 - while len(offspring) != offspring_size[0]: - parent1 = parents[idx % parents.shape[0], :].copy() - parent2 = parents[(idx + 1) % parents.shape[0], :].copy() - - random_split_point = numpy.random.choice(range(offspring_size[0])) - - parent1[random_split_point:] = parent2[random_split_point:] - - offspring.append(parent1) - - idx += 1 - - return numpy.array(offspring) - - def mutation_func(self, offspring, ga_instance): - - for chromosome_idx in range(offspring.shape[0]): - random_gene_idx = numpy.random.choice(range(offspring.shape[1])) - - offspring[chromosome_idx, random_gene_idx] += numpy.random.random() - - return offspring - -ga_instance = pygad.GA(num_generations=10, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=Test().fitness_func, - parent_selection_type=Test().parent_selection_func, - crossover_type=Test().crossover_func, - mutation_type=Test().mutation_func) - -ga_instance.run() -ga_instance.plot_fitness() -``` +:::{toctree} +:hidden: +adaptive_mutation +user_defined_operators +::: From 5e3c4a2087acc403963f838b77392488ae3f5685 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 21 May 2026 16:33:35 -0400 Subject: [PATCH 39/42] docs: turn the pygad.GA constructor reference into grouped collapsibles The constructor accepts ~41 parameters, each previously a long bullet, making a wall of text. Group them into themed subsections (Population and Generations, Fitness, Genes, Parent Selection, Keeping Solutions, Crossover, Mutation, Lifecycle Callbacks, Saving and Logging, Performance) and render each parameter as a collapsible dropdown whose title shows the name, default, and a one-line summary, expanding to the full description. Every description is preserved verbatim and the init anchor is unchanged. Add a small CSS touch so the parameter name stands out in each dropdown title. --- docs/source/_static/custom.css | 10 ++ docs/source/pygad.md | 310 ++++++++++++++++++++++++++++----- 2 files changed, 277 insertions(+), 43 deletions(-) diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css index a65a6d8..7f356d0 100644 --- a/docs/source/_static/custom.css +++ b/docs/source/_static/custom.css @@ -31,3 +31,13 @@ table.docutils td, table.docutils th { padding: 0.4rem 0.7rem; } + +/* Collapsible parameter list (pygad.GA constructor). Make the parameter name + in each dropdown title stand out, and tidy the spacing between items. */ +details.sd-dropdown { + margin-bottom: 0.4rem; +} +details.sd-dropdown > summary.sd-summary-title code, +details.sd-dropdown > .sd-summary-title code { + font-weight: 600; +} diff --git a/docs/source/pygad.md b/docs/source/pygad.md index a4dd1ae..f5e0d1e 100644 --- a/docs/source/pygad.md +++ b/docs/source/pygad.md @@ -12,49 +12,273 @@ The `pygad` module has a class named `GA` for building the genetic algorithm. Th To create an instance of the `pygad.GA` class, the constructor accepts several parameters. These let you adjust the genetic algorithm for different types of applications. -The `pygad.GA` class constructor supports the following parameters: - -- `num_generations`: Number of generations. -- `num_parents_mating`: Number of solutions to be selected as parents. -- `fitness_func`: Accepts a function/method and returns the fitness value(s) of the solution. If a function is passed, then it must accept 3 parameters (1. the instance of the `pygad.GA` class, 2. a single solution, and 3. its index in the population). If method, then it accepts a fourth parameter representing the method's class instance. Check the [Preparing the fitness_func Parameter](https://pygad.readthedocs.io/en/latest/steps_to_use.html#preparing-the-fitness-func-parameter) section for information about creating such a function. In [PyGAD 3.2.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-2-0), multi-objective optimization is supported. To consider the problem as multi-objective, just return a `list`, `tuple`, or `numpy.ndarray` from the fitness function. -- `fitness_batch_size=None`: A new optional parameter called `fitness_batch_size` is supported to calculate the fitness function in batches. If it is assigned the value `1` or `None` (default), then the normal flow is used where the fitness function is called for each individual solution. If the `fitness_batch_size` parameter is assigned a value satisfying this condition `1 < fitness_batch_size <= sol_per_pop`, then the solutions are grouped into batches of size `fitness_batch_size` and the fitness function is called once for each batch. Check the [Batch Fitness Calculation](https://pygad.readthedocs.io/en/latest/fitness_calculation.html#batch-fitness-calculation) section for more details and examples. Added in from [PyGAD 2.19.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-19-0). -- `initial_population`: A user-defined initial population. It is useful when the user wants to start the generations with a custom initial population. It defaults to `None` which means no initial population is specified by the user. In this case, [PyGAD](https://pypi.org/project/pygad) creates an initial population using the `sol_per_pop` and `num_genes` parameters. An exception is raised if the `initial_population` is `None` while any of the 2 parameters (`sol_per_pop` or `num_genes`) is also `None`. Introduced in [PyGAD 2.0.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-0-0) and higher. -- `sol_per_pop`: Number of solutions (i.e. chromosomes) within the population. This parameter has no action if `initial_population` parameter exists. -- `num_genes`: Number of genes in the solution/chromosome. This parameter is not needed if the user feeds the initial population to the `initial_population` parameter. -- `gene_type=float`: Controls the gene type. It can be assigned to a single data type that is applied to all genes or can specify the data type of each individual gene. It defaults to `float` which means all genes are of `float` data type. Starting from [PyGAD 2.9.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-9-0), the `gene_type` parameter can be assigned to a numeric value of any of these types: `int`, `float`, and `numpy.int/uint/float(8-64)`. Starting from [PyGAD 2.14.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-14-0), it can be assigned to a `list`, `tuple`, or a `numpy.ndarray` which hold a data type for each gene (e.g. `gene_type=[int, float, numpy.int8]`). This helps to control the data type of each individual gene. In [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0), a precision for the `float` data types can be specified (e.g. `gene_type=[float, 2]`. -- `init_range_low=-4`: The lower value of the random range from which the gene values in the initial population are selected. `init_range_low` defaults to `-4`. Available in [PyGAD 1.0.20](https://pygad.readthedocs.io/en/latest/releases.html#pygad-1-0-20) and higher. This parameter has no action if the `initial_population` parameter exists. -- `init_range_high=4`: The upper value of the random range from which the gene values in the initial population are selected. `init_range_high` defaults to `+4`. Available in [PyGAD 1.0.20](https://pygad.readthedocs.io/en/latest/releases.html#pygad-1-0-20) and higher. This parameter has no action if the `initial_population` parameter exists. -- `parent_selection_type="sss"`: The parent selection type. Supported types are `sss` (for steady-state selection), `rws` (for roulette wheel selection), `sus` (for stochastic universal selection), `rank` (for rank selection), `random` (for random selection), and `tournament` (for tournament selection). A custom parent selection function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/user_defined_operators.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about building a user-defined parent selection function. -- `keep_parents=-1`: The number of parents to keep in the next population. `-1` (default) means keep all the parents. `0` means keep no parents. A value greater than `0` means keep that number of parents. The value of `keep_parents` cannot be less than `-1` or greater than the number of solutions in the population (`sol_per_pop`). Starting from [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0), this parameter has an effect only when the `keep_elitism` parameter is `0`. Starting from PyGAD 2.20.0, the parents' fitness from the last generation is not re-used if `keep_parents=0`. To see how `keep_parents` and `keep_elitism` work together, check the [How the Number of Offspring Is Decided](https://pygad.readthedocs.io/en/latest/generations.html#how-the-number-of-offspring-is-decided) section. -- `keep_elitism=1`: Added in [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0). It takes the value `0` or a positive integer that meets the condition `0 <= keep_elitism <= sol_per_pop`. It defaults to `1`, which means only the best solution in the current generation is kept in the next generation. If set to `0`, it has no effect. If set to a positive integer `K`, then the best `K` solutions are kept in the next generation. It cannot be greater than the value of the `sol_per_pop` parameter. If this parameter is not `0`, then the `keep_parents` parameter has no effect. To see how `keep_elitism` and `keep_parents` work together, check the [How the Number of Offspring Is Decided](https://pygad.readthedocs.io/en/latest/generations.html#how-the-number-of-offspring-is-decided) section. -- `K_tournament=3`: In case that the parent selection type is `tournament`, the `K_tournament` specifies the number of parents participating in the tournament selection. It defaults to `3`. -- `crossover_type="single_point"`: Type of the crossover operation. Supported types are `single_point` (for single-point crossover), `two_points` (for two points crossover), `uniform` (for uniform crossover), and `scattered` (for scattered crossover). Scattered crossover is supported from PyGAD [2.9.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-9-0) and higher. It defaults to `single_point`. A custom crossover function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/user_defined_operators.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about creating a user-defined crossover function. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, if `crossover_type=None`, then the crossover step is bypassed which means 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. -- `crossover_probability=None`: The probability of selecting a parent for applying the crossover operation. Its value must be between 0.0 and 1.0 inclusive. For each parent, a random value between 0.0 and 1.0 is generated. If this random value is less than or equal to the value assigned to the `crossover_probability` parameter, then the parent is selected. Added in [PyGAD 2.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-5-0) and higher. -- `mutation_type="random"`: Type of the mutation operation. Supported types are `random` (for random mutation), `swap` (for swap mutation), `inversion` (for inversion mutation), `scramble` (for scramble mutation), and `adaptive` (for adaptive mutation). It defaults to `random`. A custom mutation function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/user_defined_operators.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about creating a user-defined mutation function. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, if `mutation_type=None`, then the mutation step is bypassed which means 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. `Adaptive` mutation is supported starting from [PyGAD 2.10.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-10-0). For more information about adaptive mutation, go to the [Adaptive Mutation](https://pygad.readthedocs.io/en/latest/adaptive_mutation.html#adaptive-mutation) section. For example about using adaptive mutation, check the [Use Adaptive Mutation in PyGAD](https://pygad.readthedocs.io/en/latest/adaptive_mutation.html#use-adaptive-mutation-in-pygad) section. -- `mutation_probability=None`: The probability of selecting a gene for applying the mutation operation. Its value must be between 0.0 and 1.0 inclusive. For each gene in a solution, a random value between 0.0 and 1.0 is generated. If this random value is less than or equal to the value assigned to the `mutation_probability` parameter, then the gene is selected. If this parameter exists, then there is no need for the 2 parameters `mutation_percent_genes` and `mutation_num_genes`. Added in [PyGAD 2.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-5-0) and higher. -- `mutation_by_replacement=False`: An optional bool parameter. It works only when the selected type of mutation is random (`mutation_type="random"`). In this case, `mutation_by_replacement=True` means replace the gene by the randomly generated value. If False, then it has no effect and random mutation works by adding the random value to the gene. Supported in [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher. Check the changes in [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) under the Release History section for an example. -- `mutation_percent_genes="default"`: Percentage of genes to mutate. It defaults to the string `"default"` which is later translated into the integer `10` which means 10% of the genes will be mutated. It must be `>0` and `<=100`. Out of this percentage, the number of genes to mutate is deduced which is assigned to the `mutation_num_genes` parameter. The `mutation_percent_genes` parameter has no action if `mutation_probability` or `mutation_num_genes` exist. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, this parameter has no action if `mutation_type` is `None`. -- `mutation_num_genes=None`: Number of genes to mutate which defaults to `None` meaning that no number is specified. The `mutation_num_genes` parameter has no action if the parameter `mutation_probability` exists. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, this parameter has no action if `mutation_type` is `None`. -- `random_mutation_min_val=-1.0`: For `random` mutation, the `random_mutation_min_val` parameter specifies the start value of the range from which a random value is selected to be added to the gene. It defaults to `-1`. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, this parameter has no action if `mutation_type` is `None`. -- `random_mutation_max_val=1.0`: For `random` mutation, the `random_mutation_max_val` parameter specifies the end value of the range from which a random value is selected to be added to the gene. It defaults to `+1`. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, this parameter has no action if `mutation_type` is `None`. -- `gene_space=None`: It is used to specify the possible values for each gene in case the user wants to restrict the gene values. It is useful if the gene space is restricted to a certain range or to discrete values. It accepts a `list`, `range`, or `numpy.ndarray`. When all genes have the same global space, specify their values as a `list`/`tuple`/`range`/`numpy.ndarray`. For example, `gene_space = [0.3, 5.2, -4, 8]` restricts the gene values to the 4 specified values. If each gene has its own space, then the `gene_space` parameter can be nested like `[[0.4, -5], [0.5, -3.2, 8.2, -9], ...]` where the first sublist determines the values for the first gene, the second sublist for the second gene, and so on. If the nested list/tuple has a `None` value, then the gene's initial value is selected randomly from the range specified by the 2 parameters `init_range_low` and `init_range_high` and its mutation value is selected randomly from the range specified by the 2 parameters `random_mutation_min_val` and `random_mutation_max_val`. `gene_space` is added in [PyGAD 2.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-5-0). Check the [Release History of PyGAD 2.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-5-0) section of the documentation for more details. In [PyGAD 2.9.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-9-0), NumPy arrays can be assigned to the `gene_space` parameter. In [PyGAD 2.11.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-11-0), the `gene_space` parameter itself or any of its elements can be assigned to a dictionary to specify the lower and upper limits of the genes. For example, `{'low': 2, 'high': 4}` means the minimum and maximum values are 2 and 4, respectively. In [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0), a new key called `"step"` is supported to specify the step of moving from the start to the end of the range specified by the 2 existing keys `"low"` and `"high"`. -- `gene_constraint=None`: A list of callables (i.e. functions) acting as constraints for the gene values. Before selecting a value for a gene, the callable is called to ensure the candidate value is valid. Added in [PyGAD 3.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-5-0). Check the [Gene Constraint](https://pygad.readthedocs.io/en/latest/gene_values.html#gene-constraint) section for more information. -- `sample_size=100`: In some cases where a gene value is to be selected, this variable defines the size of the sample from which a value is selected randomly. Useful if either `allow_duplicate_genes` or `gene_constraint` is used. If PyGAD failed to find a unique value or a value that meets a gene constraint, it is recommended to increases this parameter's value. Added in [PyGAD 3.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-5-0). Check the [sample_size Parameter](https://pygad.readthedocs.io/en/latest/gene_values.html#sample-size-parameter) section for more information. -- `on_start=None`: Accepts a function/method to be called only once before the genetic algorithm starts its evolution. If function, then it must accept a single parameter representing the instance of the genetic algorithm. If method, then it must accept 2 parameters where the second one refers to the method's object. Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). -- `on_fitness=None`: Accepts a function/method to be called after calculating the fitness values of all solutions in the population. If function, then it must accept 2 parameters: 1) a list of all solutions' fitness values 2) the instance of the genetic algorithm. If method, then it must accept 3 parameters where the third one refers to the method's object. Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). -- `on_parents=None`: Accepts a function/method to be called after selecting the parents that mates. If function, then it must accept 2 parameters: 1) the selected parents 2) the instance of the genetic algorithm If method, then it must accept 3 parameters where the third one refers to the method's object. Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). -- `on_crossover=None`: Accepts a function to be called each time the crossover operation is applied. This function must accept 2 parameters: the first one represents the instance of the genetic algorithm and the second one represents the offspring generated using crossover. Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). -- `on_mutation=None`: Accepts a function to be called each time the mutation operation is applied. This function must accept 2 parameters: the first one represents the instance of the genetic algorithm and the second one represents the offspring after applying the mutation. Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). -- `on_generation=None`: Accepts a function to be called after each generation. This function must accept a single parameter representing the instance of the genetic algorithm. If the function returned the string `stop`, then the `run()` method stops without completing the other generations. Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). -- `on_stop=None`: Accepts a function to be called only once exactly before the genetic algorithm stops or when it completes all the generations. This function must accept 2 parameters: the first one represents the instance of the genetic algorithm and the second one is a list of fitness values of the last population's solutions. Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). -- `save_best_solutions=False`: When `True`, then the best solution after each generation is saved into an attribute named `best_solutions`. If `False` (default), then no solutions are saved and the `best_solutions` attribute will be empty. Supported in [PyGAD 2.9.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-9-0). -- `save_solutions=False`: If `True`, then all solutions in each generation are appended into an attribute called `solutions` which is NumPy array. Supported in [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0). -- `suppress_warnings=False`: A bool parameter to control whether the warning messages are printed or not. It defaults to `False`. -- `allow_duplicate_genes=True`: Added in [PyGAD 2.13.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-13-0). If `True`, then a solution/chromosome may have duplicate gene values. If `False`, then each gene will have a unique value in its solution. -- `stop_criteria=None`: Some criteria to stop the evolution. Added in [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0). Each criterion is passed as `str` which has a stop word. The current 2 supported words are `reach` and `saturate`. `reach` stops the `run()` method if the fitness value is equal to or greater than a given fitness value. An example for `reach` is `"reach_40"` which stops the evolution if the fitness is >= 40. `saturate` means stop the evolution if the fitness saturates for a given number of consecutive generations. An example for `saturate` is `"saturate_7"` which means stop the `run()` method if the fitness does not change for 7 consecutive generations. -- `parallel_processing=None`: Added in [PyGAD 2.17.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-17-0). If `None` (Default), this means no parallel processing is applied. It can accept a list/tuple of 2 elements [1) Can be either `'process'` or `'thread'` to indicate whether processes or threads are used, respectively., 2) The number of processes or threads to use.]. For example, `parallel_processing=['process', 10]` applies parallel processing with 10 processes. If a positive integer is assigned, then it is used as the number of threads. For example, `parallel_processing=5` uses 5 threads which is equivalent to `parallel_processing=["thread", 5]`. For more information, check the [Parallel Processing in PyGAD](https://pygad.readthedocs.io/en/latest/fitness_calculation.html#parallel-processing-in-pygad) section. -- `random_seed=None`: Added in [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0). It defines the random seed to be used by the random function generators (we use random functions in the NumPy and random modules). This helps to reproduce the same results by setting the same random seed (e.g. `random_seed=2`). If given the value `None`, then it has no effect. -- `logger=None`: Accepts an instance of the `logging.Logger` class to log the outputs. Any message is no longer printed using `print()` but logged. If `logger=None`, then a logger is created that uses `StreamHandler` to logs the messages to the console. Added in [PyGAD 3.0.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-0-0). Check the [Logging Outputs](https://pygad.readthedocs.io/en/latest/logging.html#logging-outputs) for more information. +The `pygad.GA` class constructor supports the parameters below, grouped by purpose. Click a parameter to expand its full description. + +#### Population and Generations + +:::{dropdown} `num_generations`: Number of generations to run. +:animate: fade-in-slide-down + +Number of generations. +::: + +:::{dropdown} `num_parents_mating`: How many solutions are selected as parents. +:animate: fade-in-slide-down + +Number of solutions to be selected as parents. +::: + +:::{dropdown} `sol_per_pop`: Number of solutions in the population. +:animate: fade-in-slide-down + +Number of solutions (i.e. chromosomes) within the population. This parameter has no action if `initial_population` parameter exists. +::: + +:::{dropdown} `num_genes`: Number of genes in each solution. +:animate: fade-in-slide-down + +Number of genes in the solution/chromosome. This parameter is not needed if the user feeds the initial population to the `initial_population` parameter. +::: + +:::{dropdown} `initial_population`: Start from your own population. +:animate: fade-in-slide-down + +A user-defined initial population. It is useful when the user wants to start the generations with a custom initial population. It defaults to `None` which means no initial population is specified by the user. In this case, [PyGAD](https://pypi.org/project/pygad) creates an initial population using the `sol_per_pop` and `num_genes` parameters. An exception is raised if the `initial_population` is `None` while any of the 2 parameters (`sol_per_pop` or `num_genes`) is also `None`. Introduced in [PyGAD 2.0.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-0-0) and higher. +::: + +:::{dropdown} `stop_criteria=None`: Stop early when a condition is met. +:animate: fade-in-slide-down + +Some criteria to stop the evolution. Added in [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0). Each criterion is passed as `str` which has a stop word. The current 2 supported words are `reach` and `saturate`. `reach` stops the `run()` method if the fitness value is equal to or greater than a given fitness value. An example for `reach` is `"reach_40"` which stops the evolution if the fitness is >= 40. `saturate` means stop the evolution if the fitness saturates for a given number of consecutive generations. An example for `saturate` is `"saturate_7"` which means stop the `run()` method if the fitness does not change for 7 consecutive generations. +::: + +#### Fitness Function + +:::{dropdown} `fitness_func`: Function that scores each solution. +:animate: fade-in-slide-down + +Accepts a function/method and returns the fitness value(s) of the solution. If a function is passed, then it must accept 3 parameters (1. the instance of the `pygad.GA` class, 2. a single solution, and 3. its index in the population). If method, then it accepts a fourth parameter representing the method's class instance. Check the [Preparing the fitness_func Parameter](https://pygad.readthedocs.io/en/latest/steps_to_use.html#preparing-the-fitness-func-parameter) section for information about creating such a function. In [PyGAD 3.2.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-2-0), multi-objective optimization is supported. To consider the problem as multi-objective, just return a `list`, `tuple`, or `numpy.ndarray` from the fitness function. +::: + +:::{dropdown} `fitness_batch_size=None`: Score the solutions in batches. +:animate: fade-in-slide-down + +A new optional parameter called `fitness_batch_size` is supported to calculate the fitness function in batches. If it is assigned the value `1` or `None` (default), then the normal flow is used where the fitness function is called for each individual solution. If the `fitness_batch_size` parameter is assigned a value satisfying this condition `1 < fitness_batch_size <= sol_per_pop`, then the solutions are grouped into batches of size `fitness_batch_size` and the fitness function is called once for each batch. Check the [Batch Fitness Calculation](https://pygad.readthedocs.io/en/latest/fitness_calculation.html#batch-fitness-calculation) section for more details and examples. Added in from [PyGAD 2.19.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-19-0). +::: + +#### Genes: Values and Types + +:::{dropdown} `gene_type=float`: Data type (and precision) of the genes. +:animate: fade-in-slide-down + +Controls the gene type. It can be assigned to a single data type that is applied to all genes or can specify the data type of each individual gene. It defaults to `float` which means all genes are of `float` data type. Starting from [PyGAD 2.9.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-9-0), the `gene_type` parameter can be assigned to a numeric value of any of these types: `int`, `float`, and `numpy.int/uint/float(8-64)`. Starting from [PyGAD 2.14.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-14-0), it can be assigned to a `list`, `tuple`, or a `numpy.ndarray` which hold a data type for each gene (e.g. `gene_type=[int, float, numpy.int8]`). This helps to control the data type of each individual gene. In [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0), a precision for the `float` data types can be specified (e.g. `gene_type=[float, 2]`. +::: + +:::{dropdown} `gene_space=None`: Allowed values or range for each gene. +:animate: fade-in-slide-down + +It is used to specify the possible values for each gene in case the user wants to restrict the gene values. It is useful if the gene space is restricted to a certain range or to discrete values. It accepts a `list`, `range`, or `numpy.ndarray`. When all genes have the same global space, specify their values as a `list`/`tuple`/`range`/`numpy.ndarray`. For example, `gene_space = [0.3, 5.2, -4, 8]` restricts the gene values to the 4 specified values. If each gene has its own space, then the `gene_space` parameter can be nested like `[[0.4, -5], [0.5, -3.2, 8.2, -9], ...]` where the first sublist determines the values for the first gene, the second sublist for the second gene, and so on. If the nested list/tuple has a `None` value, then the gene's initial value is selected randomly from the range specified by the 2 parameters `init_range_low` and `init_range_high` and its mutation value is selected randomly from the range specified by the 2 parameters `random_mutation_min_val` and `random_mutation_max_val`. `gene_space` is added in [PyGAD 2.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-5-0). Check the [Release History of PyGAD 2.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-5-0) section of the documentation for more details. In [PyGAD 2.9.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-9-0), NumPy arrays can be assigned to the `gene_space` parameter. In [PyGAD 2.11.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-11-0), the `gene_space` parameter itself or any of its elements can be assigned to a dictionary to specify the lower and upper limits of the genes. For example, `{'low': 2, 'high': 4}` means the minimum and maximum values are 2 and 4, respectively. In [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0), a new key called `"step"` is supported to specify the step of moving from the start to the end of the range specified by the 2 existing keys `"low"` and `"high"`. +::: + +:::{dropdown} `gene_constraint=None`: Functions that restrict gene values. +:animate: fade-in-slide-down + +A list of callables (i.e. functions) acting as constraints for the gene values. Before selecting a value for a gene, the callable is called to ensure the candidate value is valid. Added in [PyGAD 3.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-5-0). Check the [Gene Constraint](https://pygad.readthedocs.io/en/latest/gene_values.html#gene-constraint) section for more information. +::: + +:::{dropdown} `init_range_low=-4`: Lower bound for the initial gene values. +:animate: fade-in-slide-down + +The lower value of the random range from which the gene values in the initial population are selected. `init_range_low` defaults to `-4`. Available in [PyGAD 1.0.20](https://pygad.readthedocs.io/en/latest/releases.html#pygad-1-0-20) and higher. This parameter has no action if the `initial_population` parameter exists. +::: + +:::{dropdown} `init_range_high=4`: Upper bound for the initial gene values. +:animate: fade-in-slide-down + +The upper value of the random range from which the gene values in the initial population are selected. `init_range_high` defaults to `+4`. Available in [PyGAD 1.0.20](https://pygad.readthedocs.io/en/latest/releases.html#pygad-1-0-20) and higher. This parameter has no action if the `initial_population` parameter exists. +::: + +:::{dropdown} `allow_duplicate_genes=True`: Allow repeated values within a solution. +:animate: fade-in-slide-down + +Added in [PyGAD 2.13.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-13-0). If `True`, then a solution/chromosome may have duplicate gene values. If `False`, then each gene will have a unique value in its solution. +::: + +:::{dropdown} `sample_size=100`: Sample size used when searching for a valid value. +:animate: fade-in-slide-down + +In some cases where a gene value is to be selected, this variable defines the size of the sample from which a value is selected randomly. Useful if either `allow_duplicate_genes` or `gene_constraint` is used. If PyGAD failed to find a unique value or a value that meets a gene constraint, it is recommended to increases this parameter's value. Added in [PyGAD 3.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-5-0). Check the [sample_size Parameter](https://pygad.readthedocs.io/en/latest/gene_values.html#sample-size-parameter) section for more information. +::: + +#### Parent Selection + +:::{dropdown} `parent_selection_type="sss"`: How the parents are selected. +:animate: fade-in-slide-down + +The parent selection type. Supported types are `sss` (for steady-state selection), `rws` (for roulette wheel selection), `sus` (for stochastic universal selection), `rank` (for rank selection), `random` (for random selection), and `tournament` (for tournament selection). A custom parent selection function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/user_defined_operators.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about building a user-defined parent selection function. +::: + +:::{dropdown} `K_tournament=3`: Contestants per tournament selection. +:animate: fade-in-slide-down + +In case that the parent selection type is `tournament`, the `K_tournament` specifies the number of parents participating in the tournament selection. It defaults to `3`. +::: + +#### Keeping Solutions + +:::{dropdown} `keep_elitism=1`: Keep the best solutions each generation. +:animate: fade-in-slide-down + +Added in [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0). It takes the value `0` or a positive integer that meets the condition `0 <= keep_elitism <= sol_per_pop`. It defaults to `1`, which means only the best solution in the current generation is kept in the next generation. If set to `0`, it has no effect. If set to a positive integer `K`, then the best `K` solutions are kept in the next generation. It cannot be greater than the value of the `sol_per_pop` parameter. If this parameter is not `0`, then the `keep_parents` parameter has no effect. To see how `keep_elitism` and `keep_parents` work together, check the [How the Number of Offspring Is Decided](https://pygad.readthedocs.io/en/latest/generations.html#how-the-number-of-offspring-is-decided) section. +::: + +:::{dropdown} `keep_parents=-1`: Keep the parents in the next generation. +:animate: fade-in-slide-down + +The number of parents to keep in the next population. `-1` (default) means keep all the parents. `0` means keep no parents. A value greater than `0` means keep that number of parents. The value of `keep_parents` cannot be less than `-1` or greater than the number of solutions in the population (`sol_per_pop`). Starting from [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0), this parameter has an effect only when the `keep_elitism` parameter is `0`. Starting from PyGAD 2.20.0, the parents' fitness from the last generation is not re-used if `keep_parents=0`. To see how `keep_parents` and `keep_elitism` work together, check the [How the Number of Offspring Is Decided](https://pygad.readthedocs.io/en/latest/generations.html#how-the-number-of-offspring-is-decided) section. +::: + +#### Crossover + +:::{dropdown} `crossover_type="single_point"`: How parents are combined into offspring. +:animate: fade-in-slide-down + +Type of the crossover operation. Supported types are `single_point` (for single-point crossover), `two_points` (for two points crossover), `uniform` (for uniform crossover), and `scattered` (for scattered crossover). Scattered crossover is supported from PyGAD [2.9.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-9-0) and higher. It defaults to `single_point`. A custom crossover function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/user_defined_operators.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about creating a user-defined crossover function. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, if `crossover_type=None`, then the crossover step is bypassed which means 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. +::: + +:::{dropdown} `crossover_probability=None`: Chance a parent is used for crossover. +:animate: fade-in-slide-down + +The probability of selecting a parent for applying the crossover operation. Its value must be between 0.0 and 1.0 inclusive. For each parent, a random value between 0.0 and 1.0 is generated. If this random value is less than or equal to the value assigned to the `crossover_probability` parameter, then the parent is selected. Added in [PyGAD 2.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-5-0) and higher. +::: + +#### Mutation + +:::{dropdown} `mutation_type="random"`: How offspring genes are mutated. +:animate: fade-in-slide-down + +Type of the mutation operation. Supported types are `random` (for random mutation), `swap` (for swap mutation), `inversion` (for inversion mutation), `scramble` (for scramble mutation), and `adaptive` (for adaptive mutation). It defaults to `random`. A custom mutation function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/user_defined_operators.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about creating a user-defined mutation function. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, if `mutation_type=None`, then the mutation step is bypassed which means 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. `Adaptive` mutation is supported starting from [PyGAD 2.10.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-10-0). For more information about adaptive mutation, go to the [Adaptive Mutation](https://pygad.readthedocs.io/en/latest/adaptive_mutation.html#adaptive-mutation) section. For example about using adaptive mutation, check the [Use Adaptive Mutation in PyGAD](https://pygad.readthedocs.io/en/latest/adaptive_mutation.html#use-adaptive-mutation-in-pygad) section. +::: + +:::{dropdown} `mutation_probability=None`: Per-gene chance of mutation. +:animate: fade-in-slide-down + +The probability of selecting a gene for applying the mutation operation. Its value must be between 0.0 and 1.0 inclusive. For each gene in a solution, a random value between 0.0 and 1.0 is generated. If this random value is less than or equal to the value assigned to the `mutation_probability` parameter, then the gene is selected. If this parameter exists, then there is no need for the 2 parameters `mutation_percent_genes` and `mutation_num_genes`. Added in [PyGAD 2.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-5-0) and higher. +::: + +:::{dropdown} `mutation_by_replacement=False`: Replace the gene value instead of adding to it. +:animate: fade-in-slide-down + +An optional bool parameter. It works only when the selected type of mutation is random (`mutation_type="random"`). In this case, `mutation_by_replacement=True` means replace the gene by the randomly generated value. If False, then it has no effect and random mutation works by adding the random value to the gene. Supported in [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher. Check the changes in [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) under the Release History section for an example. +::: + +:::{dropdown} `mutation_percent_genes="default"`: Percentage of genes to mutate. +:animate: fade-in-slide-down + +Percentage of genes to mutate. It defaults to the string `"default"` which is later translated into the integer `10` which means 10% of the genes will be mutated. It must be `>0` and `<=100`. Out of this percentage, the number of genes to mutate is deduced which is assigned to the `mutation_num_genes` parameter. The `mutation_percent_genes` parameter has no action if `mutation_probability` or `mutation_num_genes` exist. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, this parameter has no action if `mutation_type` is `None`. +::: + +:::{dropdown} `mutation_num_genes=None`: Number of genes to mutate. +:animate: fade-in-slide-down + +Number of genes to mutate which defaults to `None` meaning that no number is specified. The `mutation_num_genes` parameter has no action if the parameter `mutation_probability` exists. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, this parameter has no action if `mutation_type` is `None`. +::: + +:::{dropdown} `random_mutation_min_val=-1.0`: Lower bound of the random mutation value. +:animate: fade-in-slide-down + +For `random` mutation, the `random_mutation_min_val` parameter specifies the start value of the range from which a random value is selected to be added to the gene. It defaults to `-1`. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, this parameter has no action if `mutation_type` is `None`. +::: + +:::{dropdown} `random_mutation_max_val=1.0`: Upper bound of the random mutation value. +:animate: fade-in-slide-down + +For `random` mutation, the `random_mutation_max_val` parameter specifies the end value of the range from which a random value is selected to be added to the gene. It defaults to `+1`. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, this parameter has no action if `mutation_type` is `None`. +::: + +#### Lifecycle Callbacks + +:::{dropdown} `on_start=None`: Called once before the run starts. +:animate: fade-in-slide-down + +Accepts a function/method to be called only once before the genetic algorithm starts its evolution. If function, then it must accept a single parameter representing the instance of the genetic algorithm. If method, then it must accept 2 parameters where the second one refers to the method's object. Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). +::: + +:::{dropdown} `on_fitness=None`: Called after the fitness is calculated. +:animate: fade-in-slide-down + +Accepts a function/method to be called after calculating the fitness values of all solutions in the population. If function, then it must accept 2 parameters: 1) a list of all solutions' fitness values 2) the instance of the genetic algorithm. If method, then it must accept 3 parameters where the third one refers to the method's object. Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). +::: + +:::{dropdown} `on_parents=None`: Called after the parents are selected. +:animate: fade-in-slide-down + +Accepts a function/method to be called after selecting the parents that mates. If function, then it must accept 2 parameters: 1) the selected parents 2) the instance of the genetic algorithm If method, then it must accept 3 parameters where the third one refers to the method's object. Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). +::: + +:::{dropdown} `on_crossover=None`: Called after crossover. +:animate: fade-in-slide-down + +Accepts a function to be called each time the crossover operation is applied. This function must accept 2 parameters: the first one represents the instance of the genetic algorithm and the second one represents the offspring generated using crossover. Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). +::: + +:::{dropdown} `on_mutation=None`: Called after mutation. +:animate: fade-in-slide-down + +Accepts a function to be called each time the mutation operation is applied. This function must accept 2 parameters: the first one represents the instance of the genetic algorithm and the second one represents the offspring after applying the mutation. Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). +::: + +:::{dropdown} `on_generation=None`: Called after each generation. +:animate: fade-in-slide-down + +Accepts a function to be called after each generation. This function must accept a single parameter representing the instance of the genetic algorithm. If the function returned the string `stop`, then the `run()` method stops without completing the other generations. Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). +::: + +:::{dropdown} `on_stop=None`: Called once when the run ends. +:animate: fade-in-slide-down + +Accepts a function to be called only once exactly before the genetic algorithm stops or when it completes all the generations. This function must accept 2 parameters: the first one represents the instance of the genetic algorithm and the second one is a list of fitness values of the last population's solutions. Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). +::: + +#### Saving and Logging + +:::{dropdown} `save_best_solutions=False`: Save the best solution of each generation. +:animate: fade-in-slide-down + +When `True`, then the best solution after each generation is saved into an attribute named `best_solutions`. If `False` (default), then no solutions are saved and the `best_solutions` attribute will be empty. Supported in [PyGAD 2.9.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-9-0). +::: + +:::{dropdown} `save_solutions=False`: Save every solution of each generation. +:animate: fade-in-slide-down + +If `True`, then all solutions in each generation are appended into an attribute called `solutions` which is NumPy array. Supported in [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0). +::: + +:::{dropdown} `logger=None`: Custom logger for the outputs. +:animate: fade-in-slide-down + +Accepts an instance of the `logging.Logger` class to log the outputs. Any message is no longer printed using `print()` but logged. If `logger=None`, then a logger is created that uses `StreamHandler` to logs the messages to the console. Added in [PyGAD 3.0.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-0-0). Check the [Logging Outputs](https://pygad.readthedocs.io/en/latest/logging.html#logging-outputs) for more information. +::: + +:::{dropdown} `suppress_warnings=False`: Turn warning messages on or off. +:animate: fade-in-slide-down + +A bool parameter to control whether the warning messages are printed or not. It defaults to `False`. +::: + +#### Performance and Reproducibility + +:::{dropdown} `parallel_processing=None`: Use threads or processes to speed up fitness. +:animate: fade-in-slide-down + +Added in [PyGAD 2.17.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-17-0). If `None` (Default), this means no parallel processing is applied. It can accept a list/tuple of 2 elements [1) Can be either `'process'` or `'thread'` to indicate whether processes or threads are used, respectively., 2) The number of processes or threads to use.]. For example, `parallel_processing=['process', 10]` applies parallel processing with 10 processes. If a positive integer is assigned, then it is used as the number of threads. For example, `parallel_processing=5` uses 5 threads which is equivalent to `parallel_processing=["thread", 5]`. For more information, check the [Parallel Processing in PyGAD](https://pygad.readthedocs.io/en/latest/fitness_calculation.html#parallel-processing-in-pygad) section. +::: + +:::{dropdown} `random_seed=None`: Seed for reproducible runs. +:animate: fade-in-slide-down + +Added in [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0). It defines the random seed to be used by the random function generators (we use random functions in the NumPy and random modules). This helps to reproduce the same results by setting the same random seed (e.g. `random_seed=2`). If given the value `None`, then it has no effect. +::: You do not have to set all of these parameters when you create an instance of the `GA` class. The most important one is `fitness_func`, which defines the fitness function. From 9905940b8df8f7fb5cc81b3046441023cc2b3bfc Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 21 May 2026 16:39:39 -0400 Subject: [PATCH 40/42] docs: format the pygad.GA parameter descriptions for readability Each parameter's expanded description was a single run-on paragraph. Reformat the dense ones into a short opening sentence, bulleted options and conditions, inline code examples, and a clear version-history list (for example gene_space, gene_type, keep_elitism/keep_parents, crossover_type, mutation_type, the callbacks, and parallel_processing). All wording, examples, and links are preserved; the init anchor is unchanged. --- docs/source/pygad.md | 234 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 203 insertions(+), 31 deletions(-) diff --git a/docs/source/pygad.md b/docs/source/pygad.md index f5e0d1e..b7b1f68 100644 --- a/docs/source/pygad.md +++ b/docs/source/pygad.md @@ -43,13 +43,24 @@ Number of genes in the solution/chromosome. This parameter is not needed if the :::{dropdown} `initial_population`: Start from your own population. :animate: fade-in-slide-down -A user-defined initial population. It is useful when the user wants to start the generations with a custom initial population. It defaults to `None` which means no initial population is specified by the user. In this case, [PyGAD](https://pypi.org/project/pygad) creates an initial population using the `sol_per_pop` and `num_genes` parameters. An exception is raised if the `initial_population` is `None` while any of the 2 parameters (`sol_per_pop` or `num_genes`) is also `None`. Introduced in [PyGAD 2.0.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-0-0) and higher. +A population you provide yourself to start the run instead of a random one. It defaults to `None`, in which case PyGAD builds the initial population from the `sol_per_pop` and `num_genes` parameters. + +If `initial_population` is `None` and either `sol_per_pop` or `num_genes` is also `None`, an exception is raised. + +Introduced in [PyGAD 2.0.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-0-0) and higher. ::: :::{dropdown} `stop_criteria=None`: Stop early when a condition is met. :animate: fade-in-slide-down -Some criteria to stop the evolution. Added in [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0). Each criterion is passed as `str` which has a stop word. The current 2 supported words are `reach` and `saturate`. `reach` stops the `run()` method if the fitness value is equal to or greater than a given fitness value. An example for `reach` is `"reach_40"` which stops the evolution if the fitness is >= 40. `saturate` means stop the evolution if the fitness saturates for a given number of consecutive generations. An example for `saturate` is `"saturate_7"` which means stop the `run()` method if the fitness does not change for 7 consecutive generations. +One or more conditions that stop the evolution early. Each criterion is a string made of a stop word and a number, like `"reach_40"`. + +Two stop words are supported: + +- `reach`: stop when the fitness is greater than or equal to a given value. Example: `"reach_40"` stops once the fitness is `>= 40`. +- `saturate`: stop when the fitness does not change for a given number of generations. Example: `"saturate_7"` stops if the fitness stays the same for 7 generations in a row. + +Added in [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0). ::: #### Fitness Function @@ -57,13 +68,30 @@ Some criteria to stop the evolution. Added in [PyGAD 2.15.0](https://pygad.readt :::{dropdown} `fitness_func`: Function that scores each solution. :animate: fade-in-slide-down -Accepts a function/method and returns the fitness value(s) of the solution. If a function is passed, then it must accept 3 parameters (1. the instance of the `pygad.GA` class, 2. a single solution, and 3. its index in the population). If method, then it accepts a fourth parameter representing the method's class instance. Check the [Preparing the fitness_func Parameter](https://pygad.readthedocs.io/en/latest/steps_to_use.html#preparing-the-fitness-func-parameter) section for information about creating such a function. In [PyGAD 3.2.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-2-0), multi-objective optimization is supported. To consider the problem as multi-objective, just return a `list`, `tuple`, or `numpy.ndarray` from the fitness function. +The function (or method) that calculates the fitness of a solution. This is the one parameter you almost always need to set. + +A fitness **function** must accept 3 parameters: + +1. The instance of the `pygad.GA` class. +2. A single solution. +3. The index of the solution in the population. + +If you pass a **method**, it takes a fourth parameter for the method's class instance. + +Return a single number for a single-objective problem, or a `list`, `tuple`, or `numpy.ndarray` for a multi-objective problem (supported since [PyGAD 3.2.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-2-0)). + +See [Preparing the fitness_func Parameter](https://pygad.readthedocs.io/en/latest/steps_to_use.html#preparing-the-fitness-func-parameter) for how to build one. ::: :::{dropdown} `fitness_batch_size=None`: Score the solutions in batches. :animate: fade-in-slide-down -A new optional parameter called `fitness_batch_size` is supported to calculate the fitness function in batches. If it is assigned the value `1` or `None` (default), then the normal flow is used where the fitness function is called for each individual solution. If the `fitness_batch_size` parameter is assigned a value satisfying this condition `1 < fitness_batch_size <= sol_per_pop`, then the solutions are grouped into batches of size `fitness_batch_size` and the fitness function is called once for each batch. Check the [Batch Fitness Calculation](https://pygad.readthedocs.io/en/latest/fitness_calculation.html#batch-fitness-calculation) section for more details and examples. Added in from [PyGAD 2.19.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-19-0). +Calculates the fitness in batches instead of one solution at a time. + +- `1` or `None` (default): the fitness function is called once per solution. +- An integer where `1 < fitness_batch_size <= sol_per_pop`: solutions are grouped into batches of this size, and the fitness function is called once per batch. + +See [Batch Fitness Calculation](https://pygad.readthedocs.io/en/latest/fitness_calculation.html#batch-fitness-calculation) for details and examples. Added in [PyGAD 2.19.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-19-0). ::: #### Genes: Values and Types @@ -71,19 +99,47 @@ A new optional parameter called `fitness_batch_size` is supported to calculate t :::{dropdown} `gene_type=float`: Data type (and precision) of the genes. :animate: fade-in-slide-down -Controls the gene type. It can be assigned to a single data type that is applied to all genes or can specify the data type of each individual gene. It defaults to `float` which means all genes are of `float` data type. Starting from [PyGAD 2.9.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-9-0), the `gene_type` parameter can be assigned to a numeric value of any of these types: `int`, `float`, and `numpy.int/uint/float(8-64)`. Starting from [PyGAD 2.14.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-14-0), it can be assigned to a `list`, `tuple`, or a `numpy.ndarray` which hold a data type for each gene (e.g. `gene_type=[int, float, numpy.int8]`). This helps to control the data type of each individual gene. In [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0), a precision for the `float` data types can be specified (e.g. `gene_type=[float, 2]`. +Sets the data type (and optional precision) of the genes. It defaults to `float`, so every gene is a `float`. + +You can set it to: + +- **One type for all genes:** a numeric type such as `int`, `float`, or any `numpy.int/uint/float(8-64)` type. Example: `gene_type=int`. +- **A type per gene:** a `list`, `tuple`, or `numpy.ndarray` with one type per gene. Example: `gene_type=[int, float, numpy.int8]`. +- **A float precision:** pair a `float` type with the number of decimal places. Example: `gene_type=[float, 2]`. + +Version history: + +- [PyGAD 2.9.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-9-0): a single numeric type can be used. +- [PyGAD 2.14.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-14-0): a type per gene can be used. +- [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0): a precision can be set for `float` types. ::: :::{dropdown} `gene_space=None`: Allowed values or range for each gene. :animate: fade-in-slide-down -It is used to specify the possible values for each gene in case the user wants to restrict the gene values. It is useful if the gene space is restricted to a certain range or to discrete values. It accepts a `list`, `range`, or `numpy.ndarray`. When all genes have the same global space, specify their values as a `list`/`tuple`/`range`/`numpy.ndarray`. For example, `gene_space = [0.3, 5.2, -4, 8]` restricts the gene values to the 4 specified values. If each gene has its own space, then the `gene_space` parameter can be nested like `[[0.4, -5], [0.5, -3.2, 8.2, -9], ...]` where the first sublist determines the values for the first gene, the second sublist for the second gene, and so on. If the nested list/tuple has a `None` value, then the gene's initial value is selected randomly from the range specified by the 2 parameters `init_range_low` and `init_range_high` and its mutation value is selected randomly from the range specified by the 2 parameters `random_mutation_min_val` and `random_mutation_max_val`. `gene_space` is added in [PyGAD 2.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-5-0). Check the [Release History of PyGAD 2.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-5-0) section of the documentation for more details. In [PyGAD 2.9.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-9-0), NumPy arrays can be assigned to the `gene_space` parameter. In [PyGAD 2.11.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-11-0), the `gene_space` parameter itself or any of its elements can be assigned to a dictionary to specify the lower and upper limits of the genes. For example, `{'low': 2, 'high': 4}` means the minimum and maximum values are 2 and 4, respectively. In [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0), a new key called `"step"` is supported to specify the step of moving from the start to the end of the range specified by the 2 existing keys `"low"` and `"high"`. +Sets the allowed values for each gene, so you can limit the search space to a range or to a set of discrete values. + +You can set it to: + +- **The same space for all genes:** a `list`/`tuple`/`range`/`numpy.ndarray`. Example: `gene_space=[0.3, 5.2, -4, 8]` limits every gene to those 4 values. +- **A space per gene:** a nested list/tuple, one sub-list per gene. Example: `gene_space=[[0.4, -5], [0.5, -3.2, 8.2, -9], ...]` (the first sub-list is for the first gene, and so on). +- **A continuous range:** a dictionary with `low` and `high` (and an optional `step`). Example: `{'low': 2, 'high': 4}` limits the gene to the range from 2 to 4. +- **`None` for a gene:** that gene is initialized from `init_range_low`/`init_range_high`, and mutated using `random_mutation_min_val`/`random_mutation_max_val`. + +Version history: + +- Added in [PyGAD 2.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-5-0). +- [PyGAD 2.9.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-9-0): NumPy arrays can be used. +- [PyGAD 2.11.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-11-0): a dictionary can set the low and high limits. +- [PyGAD 2.15.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-15-0): the `"step"` key was added. ::: :::{dropdown} `gene_constraint=None`: Functions that restrict gene values. :animate: fade-in-slide-down -A list of callables (i.e. functions) acting as constraints for the gene values. Before selecting a value for a gene, the callable is called to ensure the candidate value is valid. Added in [PyGAD 3.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-5-0). Check the [Gene Constraint](https://pygad.readthedocs.io/en/latest/gene_values.html#gene-constraint) section for more information. +A list of callables (functions), one per gene, that restrict the values a gene can take. Before a value is chosen for a gene, its callable checks that the candidate value is valid. + +Added in [PyGAD 3.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-5-0). See the [Gene Constraint](https://pygad.readthedocs.io/en/latest/gene_values.html#gene-constraint) section for more information. ::: :::{dropdown} `init_range_low=-4`: Lower bound for the initial gene values. @@ -107,7 +163,11 @@ Added in [PyGAD 2.13.0](https://pygad.readthedocs.io/en/latest/releases.html#pyg :::{dropdown} `sample_size=100`: Sample size used when searching for a valid value. :animate: fade-in-slide-down -In some cases where a gene value is to be selected, this variable defines the size of the sample from which a value is selected randomly. Useful if either `allow_duplicate_genes` or `gene_constraint` is used. If PyGAD failed to find a unique value or a value that meets a gene constraint, it is recommended to increases this parameter's value. Added in [PyGAD 3.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-5-0). Check the [sample_size Parameter](https://pygad.readthedocs.io/en/latest/gene_values.html#sample-size-parameter) section for more information. +The size of the sample of candidate values PyGAD draws when it needs to pick a gene value. It defaults to `100`. + +It is useful when `allow_duplicate_genes=False` or `gene_constraint` is used. If PyGAD cannot find a unique value or a value that meets a constraint, increase this parameter. + +Added in [PyGAD 3.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-5-0). See the [sample_size Parameter](https://pygad.readthedocs.io/en/latest/gene_values.html#sample-size-parameter) section for more information. ::: #### Parent Selection @@ -115,7 +175,18 @@ In some cases where a gene value is to be selected, this variable defines the si :::{dropdown} `parent_selection_type="sss"`: How the parents are selected. :animate: fade-in-slide-down -The parent selection type. Supported types are `sss` (for steady-state selection), `rws` (for roulette wheel selection), `sus` (for stochastic universal selection), `rank` (for rank selection), `random` (for random selection), and `tournament` (for tournament selection). A custom parent selection function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/user_defined_operators.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about building a user-defined parent selection function. +How the parents are selected. It defaults to `"sss"`. + +The built-in types are: + +- `sss`: steady-state selection. +- `rws`: roulette wheel selection. +- `sus`: stochastic universal selection. +- `rank`: rank selection. +- `random`: random selection. +- `tournament`: tournament selection. + +You can also pass your own parent selection function (since [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0)). See [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/user_defined_operators.html#user-defined-crossover-mutation-and-parent-selection-operators). ::: :::{dropdown} `K_tournament=3`: Contestants per tournament selection. @@ -129,13 +200,30 @@ In case that the parent selection type is `tournament`, the `K_tournament` speci :::{dropdown} `keep_elitism=1`: Keep the best solutions each generation. :animate: fade-in-slide-down -Added in [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0). It takes the value `0` or a positive integer that meets the condition `0 <= keep_elitism <= sol_per_pop`. It defaults to `1`, which means only the best solution in the current generation is kept in the next generation. If set to `0`, it has no effect. If set to a positive integer `K`, then the best `K` solutions are kept in the next generation. It cannot be greater than the value of the `sol_per_pop` parameter. If this parameter is not `0`, then the `keep_parents` parameter has no effect. To see how `keep_elitism` and `keep_parents` work together, check the [How the Number of Offspring Is Decided](https://pygad.readthedocs.io/en/latest/generations.html#how-the-number-of-offspring-is-decided) section. +The number of best solutions (the elitism) to keep in the next generation. It defaults to `1`, so only the best solution is kept. + +- `0`: elitism is turned off. +- A positive integer `K` (with `0 <= keep_elitism <= sol_per_pop`): the best `K` solutions are kept. + +If this parameter is not `0`, then `keep_parents` has no effect. + +Added in [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0). To see how `keep_elitism` and `keep_parents` work together, see [How the Number of Offspring Is Decided](https://pygad.readthedocs.io/en/latest/generations.html#how-the-number-of-offspring-is-decided). ::: :::{dropdown} `keep_parents=-1`: Keep the parents in the next generation. :animate: fade-in-slide-down -The number of parents to keep in the next population. `-1` (default) means keep all the parents. `0` means keep no parents. A value greater than `0` means keep that number of parents. The value of `keep_parents` cannot be less than `-1` or greater than the number of solutions in the population (`sol_per_pop`). Starting from [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0), this parameter has an effect only when the `keep_elitism` parameter is `0`. Starting from PyGAD 2.20.0, the parents' fitness from the last generation is not re-used if `keep_parents=0`. To see how `keep_parents` and `keep_elitism` work together, check the [How the Number of Offspring Is Decided](https://pygad.readthedocs.io/en/latest/generations.html#how-the-number-of-offspring-is-decided) section. +The number of parents to keep in the next population. It defaults to `-1`. + +- `-1`: keep all the parents. +- `0`: keep no parents. +- A positive integer: keep that many parents. + +The value cannot be less than `-1` or greater than `sol_per_pop`. + +This parameter has an effect only when `keep_elitism=0` (since [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0)). Since PyGAD 2.20.0, the parents' fitness from the last generation is not re-used if `keep_parents=0`. + +To see how `keep_parents` and `keep_elitism` work together, see [How the Number of Offspring Is Decided](https://pygad.readthedocs.io/en/latest/generations.html#how-the-number-of-offspring-is-decided). ::: #### Crossover @@ -143,13 +231,28 @@ The number of parents to keep in the next population. `-1` (default) means keep :::{dropdown} `crossover_type="single_point"`: How parents are combined into offspring. :animate: fade-in-slide-down -Type of the crossover operation. Supported types are `single_point` (for single-point crossover), `two_points` (for two points crossover), `uniform` (for uniform crossover), and `scattered` (for scattered crossover). Scattered crossover is supported from PyGAD [2.9.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-9-0) and higher. It defaults to `single_point`. A custom crossover function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/user_defined_operators.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about creating a user-defined crossover function. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, if `crossover_type=None`, then the crossover step is bypassed which means 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. +The type of crossover. It defaults to `"single_point"`. + +The built-in types are: + +- `single_point`: single-point crossover. +- `two_points`: two-point crossover. +- `uniform`: uniform crossover. +- `scattered`: scattered crossover (since [PyGAD 2.9.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-9-0)). + +You can also pass your own crossover function (since [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0)). See [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/user_defined_operators.html#user-defined-crossover-mutation-and-parent-selection-operators). + +If `crossover_type=None`, the crossover step is skipped and no offspring are created, so the next generation reuses the current population (since [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2)). ::: :::{dropdown} `crossover_probability=None`: Chance a parent is used for crossover. :animate: fade-in-slide-down -The probability of selecting a parent for applying the crossover operation. Its value must be between 0.0 and 1.0 inclusive. For each parent, a random value between 0.0 and 1.0 is generated. If this random value is less than or equal to the value assigned to the `crossover_probability` parameter, then the parent is selected. Added in [PyGAD 2.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-5-0) and higher. +The probability of selecting a parent for crossover. Its value must be between 0.0 and 1.0. + +For each parent, a random value between 0.0 and 1.0 is generated. If that value is less than or equal to `crossover_probability`, the parent is selected. + +Added in [PyGAD 2.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-5-0) and higher. ::: #### Mutation @@ -157,43 +260,74 @@ The probability of selecting a parent for applying the crossover operation. Its :::{dropdown} `mutation_type="random"`: How offspring genes are mutated. :animate: fade-in-slide-down -Type of the mutation operation. Supported types are `random` (for random mutation), `swap` (for swap mutation), `inversion` (for inversion mutation), `scramble` (for scramble mutation), and `adaptive` (for adaptive mutation). It defaults to `random`. A custom mutation function can be passed starting from [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0). Check the [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/user_defined_operators.html#user-defined-crossover-mutation-and-parent-selection-operators) section for more details about creating a user-defined mutation function. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, if `mutation_type=None`, then the mutation step is bypassed which means 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. `Adaptive` mutation is supported starting from [PyGAD 2.10.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-10-0). For more information about adaptive mutation, go to the [Adaptive Mutation](https://pygad.readthedocs.io/en/latest/adaptive_mutation.html#adaptive-mutation) section. For example about using adaptive mutation, check the [Use Adaptive Mutation in PyGAD](https://pygad.readthedocs.io/en/latest/adaptive_mutation.html#use-adaptive-mutation-in-pygad) section. +The type of mutation. It defaults to `"random"`. + +The built-in types are: + +- `random`: random mutation. +- `swap`: swap mutation. +- `inversion`: inversion mutation. +- `scramble`: scramble mutation. +- `adaptive`: adaptive mutation (since [PyGAD 2.10.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-10-0)). See [Adaptive Mutation](https://pygad.readthedocs.io/en/latest/adaptive_mutation.html#adaptive-mutation) and [Use Adaptive Mutation in PyGAD](https://pygad.readthedocs.io/en/latest/adaptive_mutation.html#use-adaptive-mutation-in-pygad). + +You can also pass your own mutation function (since [PyGAD 2.16.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-16-0)). See [User-Defined Crossover, Mutation, and Parent Selection Operators](https://pygad.readthedocs.io/en/latest/user_defined_operators.html#user-defined-crossover-mutation-and-parent-selection-operators). + +If `mutation_type=None`, the mutation step is skipped and the offspring are used unchanged (since [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2)). ::: :::{dropdown} `mutation_probability=None`: Per-gene chance of mutation. :animate: fade-in-slide-down -The probability of selecting a gene for applying the mutation operation. Its value must be between 0.0 and 1.0 inclusive. For each gene in a solution, a random value between 0.0 and 1.0 is generated. If this random value is less than or equal to the value assigned to the `mutation_probability` parameter, then the gene is selected. If this parameter exists, then there is no need for the 2 parameters `mutation_percent_genes` and `mutation_num_genes`. Added in [PyGAD 2.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-5-0) and higher. +The probability of selecting a gene for mutation. Its value must be between 0.0 and 1.0. + +For each gene, a random value between 0.0 and 1.0 is generated. If that value is less than or equal to `mutation_probability`, the gene is mutated. + +If this parameter is set, you do not need `mutation_percent_genes` or `mutation_num_genes`. Added in [PyGAD 2.5.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-5-0) and higher. ::: :::{dropdown} `mutation_by_replacement=False`: Replace the gene value instead of adding to it. :animate: fade-in-slide-down -An optional bool parameter. It works only when the selected type of mutation is random (`mutation_type="random"`). In this case, `mutation_by_replacement=True` means replace the gene by the randomly generated value. If False, then it has no effect and random mutation works by adding the random value to the gene. Supported in [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher. Check the changes in [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) under the Release History section for an example. +A bool that controls how `random` mutation changes a gene. It works only when `mutation_type="random"`. + +- `True`: replace the gene with the randomly generated value. +- `False` (default): add the random value to the gene. + +Supported in [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher. See the [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) release notes for an example. ::: :::{dropdown} `mutation_percent_genes="default"`: Percentage of genes to mutate. :animate: fade-in-slide-down -Percentage of genes to mutate. It defaults to the string `"default"` which is later translated into the integer `10` which means 10% of the genes will be mutated. It must be `>0` and `<=100`. Out of this percentage, the number of genes to mutate is deduced which is assigned to the `mutation_num_genes` parameter. The `mutation_percent_genes` parameter has no action if `mutation_probability` or `mutation_num_genes` exist. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, this parameter has no action if `mutation_type` is `None`. +The percentage of genes to mutate. It defaults to the string `"default"`, which becomes `10` (10% of the genes). The value must be `> 0` and `<= 100`. + +PyGAD uses this percentage to compute `mutation_num_genes`. + +This parameter has no effect if `mutation_probability` or `mutation_num_genes` is set, or if `mutation_type` is `None` (since [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2)). ::: :::{dropdown} `mutation_num_genes=None`: Number of genes to mutate. :animate: fade-in-slide-down -Number of genes to mutate which defaults to `None` meaning that no number is specified. The `mutation_num_genes` parameter has no action if the parameter `mutation_probability` exists. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, this parameter has no action if `mutation_type` is `None`. +The number of genes to mutate. It defaults to `None`, meaning no number is set. + +This parameter has no effect if `mutation_probability` is set, or if `mutation_type` is `None` (since [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2)). ::: :::{dropdown} `random_mutation_min_val=-1.0`: Lower bound of the random mutation value. :animate: fade-in-slide-down -For `random` mutation, the `random_mutation_min_val` parameter specifies the start value of the range from which a random value is selected to be added to the gene. It defaults to `-1`. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, this parameter has no action if `mutation_type` is `None`. +For `random` mutation, the start of the range from which a random value is drawn and added to the gene. It defaults to `-1`. + +This parameter has no effect if `mutation_type` is `None` (since [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2)). ::: :::{dropdown} `random_mutation_max_val=1.0`: Upper bound of the random mutation value. :animate: fade-in-slide-down -For `random` mutation, the `random_mutation_max_val` parameter specifies the end value of the range from which a random value is selected to be added to the gene. It defaults to `+1`. Starting from [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2) and higher, this parameter has no action if `mutation_type` is `None`. +For `random` mutation, the end of the range from which a random value is drawn and added to the gene. It defaults to `+1`. + +This parameter has no effect if `mutation_type` is `None` (since [PyGAD 2.2.2](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-2-2)). ::: #### Lifecycle Callbacks @@ -201,43 +335,68 @@ For `random` mutation, the `random_mutation_max_val` parameter specifies the end :::{dropdown} `on_start=None`: Called once before the run starts. :animate: fade-in-slide-down -Accepts a function/method to be called only once before the genetic algorithm starts its evolution. If function, then it must accept a single parameter representing the instance of the genetic algorithm. If method, then it must accept 2 parameters where the second one refers to the method's object. Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). +A function (or method) called once before the run starts. + +- As a **function**, it takes 1 parameter: the instance of the genetic algorithm. +- As a **method**, it takes a second parameter for the method's object. + +Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). ::: :::{dropdown} `on_fitness=None`: Called after the fitness is calculated. :animate: fade-in-slide-down -Accepts a function/method to be called after calculating the fitness values of all solutions in the population. If function, then it must accept 2 parameters: 1) a list of all solutions' fitness values 2) the instance of the genetic algorithm. If method, then it must accept 3 parameters where the third one refers to the method's object. Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). +A function (or method) called after the fitness of all solutions is calculated. + +- As a **function**, it takes 2 parameters: a list of all the solutions' fitness values, and the instance of the genetic algorithm. +- As a **method**, it takes a third parameter for the method's object. + +Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). ::: :::{dropdown} `on_parents=None`: Called after the parents are selected. :animate: fade-in-slide-down -Accepts a function/method to be called after selecting the parents that mates. If function, then it must accept 2 parameters: 1) the selected parents 2) the instance of the genetic algorithm If method, then it must accept 3 parameters where the third one refers to the method's object. Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). +A function (or method) called after the parents are selected. + +- As a **function**, it takes 2 parameters: the selected parents, and the instance of the genetic algorithm. +- As a **method**, it takes a third parameter for the method's object. + +Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). ::: :::{dropdown} `on_crossover=None`: Called after crossover. :animate: fade-in-slide-down -Accepts a function to be called each time the crossover operation is applied. This function must accept 2 parameters: the first one represents the instance of the genetic algorithm and the second one represents the offspring generated using crossover. Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). +A function called each time crossover is applied. It takes 2 parameters: the instance of the genetic algorithm, and the offspring generated by crossover. + +Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). ::: :::{dropdown} `on_mutation=None`: Called after mutation. :animate: fade-in-slide-down -Accepts a function to be called each time the mutation operation is applied. This function must accept 2 parameters: the first one represents the instance of the genetic algorithm and the second one represents the offspring after applying the mutation. Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). +A function called each time mutation is applied. It takes 2 parameters: the instance of the genetic algorithm, and the offspring after mutation. + +Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). ::: :::{dropdown} `on_generation=None`: Called after each generation. :animate: fade-in-slide-down -Accepts a function to be called after each generation. This function must accept a single parameter representing the instance of the genetic algorithm. If the function returned the string `stop`, then the `run()` method stops without completing the other generations. Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). +A function called after each generation. It takes 1 parameter: the instance of the genetic algorithm. + +If it returns the string `"stop"`, the `run()` method stops without completing the remaining generations. + +Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). ::: :::{dropdown} `on_stop=None`: Called once when the run ends. :animate: fade-in-slide-down -Accepts a function to be called only once exactly before the genetic algorithm stops or when it completes all the generations. This function must accept 2 parameters: the first one represents the instance of the genetic algorithm and the second one is a list of fitness values of the last population's solutions. Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). +A function called once just before the run ends (or after the last generation). It takes 2 parameters: the instance of the genetic algorithm, and the list of the last population's fitness values. + +Added in [PyGAD 2.6.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-6-0). ::: #### Saving and Logging @@ -245,7 +404,9 @@ Accepts a function to be called only once exactly before the genetic algorithm s :::{dropdown} `save_best_solutions=False`: Save the best solution of each generation. :animate: fade-in-slide-down -When `True`, then the best solution after each generation is saved into an attribute named `best_solutions`. If `False` (default), then no solutions are saved and the `best_solutions` attribute will be empty. Supported in [PyGAD 2.9.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-9-0). +When `True`, the best solution of each generation is saved into the `best_solutions` attribute. When `False` (default), nothing is saved and `best_solutions` stays empty. + +Supported in [PyGAD 2.9.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-9-0). ::: :::{dropdown} `save_solutions=False`: Save every solution of each generation. @@ -257,7 +418,9 @@ If `True`, then all solutions in each generation are appended into an attribute :::{dropdown} `logger=None`: Custom logger for the outputs. :animate: fade-in-slide-down -Accepts an instance of the `logging.Logger` class to log the outputs. Any message is no longer printed using `print()` but logged. If `logger=None`, then a logger is created that uses `StreamHandler` to logs the messages to the console. Added in [PyGAD 3.0.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-0-0). Check the [Logging Outputs](https://pygad.readthedocs.io/en/latest/logging.html#logging-outputs) for more information. +An instance of the `logging.Logger` class used to log the outputs. When set, messages are logged instead of printed with `print()`. If `None`, PyGAD creates a logger that uses a `StreamHandler` to write the messages to the console. + +Added in [PyGAD 3.0.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-0-0). See [Logging Outputs](https://pygad.readthedocs.io/en/latest/logging.html#logging-outputs) for more information. ::: :::{dropdown} `suppress_warnings=False`: Turn warning messages on or off. @@ -271,13 +434,22 @@ A bool parameter to control whether the warning messages are printed or not. It :::{dropdown} `parallel_processing=None`: Use threads or processes to speed up fitness. :animate: fade-in-slide-down -Added in [PyGAD 2.17.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-17-0). If `None` (Default), this means no parallel processing is applied. It can accept a list/tuple of 2 elements [1) Can be either `'process'` or `'thread'` to indicate whether processes or threads are used, respectively., 2) The number of processes or threads to use.]. For example, `parallel_processing=['process', 10]` applies parallel processing with 10 processes. If a positive integer is assigned, then it is used as the number of threads. For example, `parallel_processing=5` uses 5 threads which is equivalent to `parallel_processing=["thread", 5]`. For more information, check the [Parallel Processing in PyGAD](https://pygad.readthedocs.io/en/latest/fitness_calculation.html#parallel-processing-in-pygad) section. +Runs the fitness calculation in parallel. It defaults to `None` (no parallel processing). + +You can set it to: + +- **A positive integer:** the number of threads. Example: `parallel_processing=5` uses 5 threads (the same as `["thread", 5]`). +- **A list/tuple of 2 elements:** the first is `"process"` or `"thread"`; the second is the number of processes or threads. Example: `parallel_processing=["process", 10]` uses 10 processes. + +Added in [PyGAD 2.17.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-17-0). See [Parallel Processing in PyGAD](https://pygad.readthedocs.io/en/latest/fitness_calculation.html#parallel-processing-in-pygad) for more information. ::: :::{dropdown} `random_seed=None`: Seed for reproducible runs. :animate: fade-in-slide-down -Added in [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0). It defines the random seed to be used by the random function generators (we use random functions in the NumPy and random modules). This helps to reproduce the same results by setting the same random seed (e.g. `random_seed=2`). If given the value `None`, then it has no effect. +The random seed used by the NumPy and `random` number generators. Setting it makes runs reproducible (for example, `random_seed=2`). It defaults to `None`, which means no seed is used. + +Added in [PyGAD 2.18.0](https://pygad.readthedocs.io/en/latest/releases.html#pygad-2-18-0). ::: You do not have to set all of these parameters when you create an instance of the `GA` class. The most important one is `fitness_func`, which defines the fitness function. From af58ca21da2cb1a05182cd8f7ab58aca7711044d Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 21 May 2026 17:04:26 -0400 Subject: [PATCH 41/42] docs: fix the English of comments and docstrings across the pygad package Clean up the English of the `#` comments and docstrings in all pygad/ modules: fix typos (for, sure, across, precedes, dimensions, architecture, consider, validated, latest, from, coming, value), subject-verb agreement (appear), wrong words (confirms that), and awkward phrasing, using simple English. Also fix a few copy-paste docstring issues (a wrong parameter name, a wrong return type, and a stale default value) and a comment that contradicted its code branch. No code, exception messages, or behavior were changed. --- pygad/cnn/cnn.py | 32 ++++++++++++++++---------------- pygad/gacnn/gacnn.py | 6 +++--- pygad/gann/gann.py | 20 ++++++++++---------- pygad/helper/activations.py | 2 +- pygad/helper/misc.py | 2 +- pygad/helper/unique.py | 10 +++++----- pygad/kerasga/kerasga.py | 2 +- pygad/nn/nn.py | 22 +++++++++++----------- pygad/pygad.py | 8 ++++---- pygad/torchga/torchga.py | 10 +++++----- pygad/utils/crossover.py | 10 +++++----- pygad/utils/engine.py | 10 +++++----- pygad/utils/mutation.py | 24 ++++++++++++------------ pygad/utils/nsga2.py | 10 +++++----- pygad/utils/parent_selection.py | 16 ++++++++-------- pygad/utils/validation.py | 4 ++-- pygad/visualize/plot.py | 2 +- 17 files changed, 95 insertions(+), 95 deletions(-) diff --git a/pygad/cnn/cnn.py b/pygad/cnn/cnn.py index ac6f37b..e479403 100644 --- a/pygad/cnn/cnn.py +++ b/pygad/cnn/cnn.py @@ -12,7 +12,7 @@ It is also translated into Chinese: http://m.aliyun.com/yunqi/articles/585741 """ -# Supported activation functions by the cnn.py module. +# The activation functions supported by the cnn.py module. supported_activation_functions = ("sigmoid", "relu", "softmax") def layers_weights(model, @@ -52,7 +52,7 @@ def layers_weights(model, raise TypeError(msg) # Currently, the weights of the layers are in the reverse order. In other words, the weights of the first layer are at the last index of the 'network_weights' list while the weights of the last layer are at the first index. - # Reversing the 'network_weights' list to order the layers' weights according to their location in the network architecture (i.e. the weights of the first layer appears at index 0 of the list). + # Reversing the 'network_weights' list to order the layers' weights according to their location in the network architecture (i.e. the weights of the first layer appear at index 0 of the list). network_weights.reverse() return network_weights @@ -94,7 +94,7 @@ def layers_weights_as_matrix(model, vector_weights): raise TypeError(msg) # Currently, the weights of the layers are in the reverse order. In other words, the weights of the first layer are at the last index of the 'network_weights' list while the weights of the last layer are at the first index. - # Reversing the 'network_weights' list to order the layers' weights according to their location in the network architecture (i.e. the weights of the first layer appears at index 0 of the list). + # Reversing the 'network_weights' list to order the layers' weights according to their location in the network architecture (i.e. the weights of the first layer appear at index 0 of the list). network_weights.reverse() return numpy.array(network_weights, dtype=object) # NEP 34: https://numpy.org/neps/nep-0034-infer-dtype-is-object.html @@ -138,7 +138,7 @@ def layers_weights_as_vector(model, initial=True): raise TypeError(msg) # Currently, the weights of the layers are in the reverse order. In other words, the weights of the first layer are at the last index of the 'network_weights' list while the weights of the last layer are at the first index. - # Reversing the 'network_weights' list to order the layers' weights according to their location in the network architecture (i.e. the weights of the first layer appears at index 0 of the list). + # Reversing the 'network_weights' list to order the layers' weights according to their location in the network architecture (i.e. the weights of the first layer appear at index 0 of the list). network_weights.reverse() return numpy.array(network_weights) @@ -301,7 +301,7 @@ def __init__(self, msg = "The previous layer cannot be of Type 'None'. Please pass a valid layer to the 'previous_layer' parameter." self.logger.error(msg) raise TypeError(msg) - # A reference to the layer that preceeds the current layer in the network architecture. + # A reference to the layer that precedes the current layer in the network architecture. self.previous_layer = previous_layer # A reference to the bank of filters. @@ -323,7 +323,7 @@ def __init__(self, self.layer_input_size = self.previous_layer.layer_output_size # Size of the output from the layer. - # Later, it must conider strides and paddings + # Later, it must consider strides and padding. self.layer_output_size = (self.previous_layer.layer_output_size[0] - self.kernel_size + 1, self.previous_layer.layer_output_size[1] - self.kernel_size + 1, num_filters) @@ -398,7 +398,7 @@ def conv(self, input2D): msg = 'A filter must be a square matrix. I.e. number of rows and columns must match.' self.logger.error(msg) raise ValueError(msg) - if self.initial_weights.shape[1]%2==0: # Check if filter diemnsions are odd. + if self.initial_weights.shape[1]%2==0: # Check if filter dimensions are odd. msg = 'A filter must have an odd size. I.e. number of rows and columns must be odd.' self.logger.error(msg) raise ValueError(msg) @@ -452,7 +452,7 @@ def __init__(self, msg = "The previous layer cannot be of Type 'None'. Please pass a valid layer to the 'previous_layer' parameter." self.logger.error(msg) raise TypeError(msg) - # A reference to the layer that preceeds the current layer in the network architecture. + # A reference to the layer that precedes the current layer in the network architecture. self.previous_layer = previous_layer # Size of the input to the layer. @@ -538,7 +538,7 @@ def __init__(self, msg = "The previous layer cannot be of Type 'None'. Please pass a valid layer to the 'previous_layer' parameter." self.logger.error(msg) raise TypeError(msg) - # A reference to the layer that preceeds the current layer in the network architecture. + # A reference to the layer that precedes the current layer in the network architecture. self.previous_layer = previous_layer # Size of the input to the layer. @@ -604,7 +604,7 @@ def __init__(self, self.logger.error(msg) raise TypeError(msg) - # A reference to the layer that preceeds the current layer in the network architecture. + # A reference to the layer that precedes the current layer in the network architecture. self.previous_layer = previous_layer # Size of the input to the layer. @@ -655,7 +655,7 @@ def __init__(self, msg = "The previous layer cannot be of Type 'None'. Please pass a valid layer to the 'previous_layer' parameter." self.logger.error(msg) raise TypeError(msg) - # A reference to the layer that preceeds the current layer in the network architecture. + # A reference to the layer that precedes the current layer in the network architecture. self.previous_layer = previous_layer # Size of the input to the layer. @@ -706,7 +706,7 @@ def __init__(self, msg = "The previous layer cannot be of Type 'None'. Please pass a valid layer to the 'previous_layer' parameter." self.logger.error(msg) raise TypeError(msg) - # A reference to the layer that preceeds the current layer in the network architecture. + # A reference to the layer that precedes the current layer in the network architecture. self.previous_layer = previous_layer # Size of the input to the layer. @@ -734,7 +734,7 @@ def flatten(self, input2D): class Dense(CustomLogger): """ - Implementing the input dense (fully connected) layer of a CNN. + Implementing the dense (fully connected) layer of a CNN. """ def __init__(self, @@ -784,7 +784,7 @@ def __init__(self, msg = "The previous layer cannot be of Type 'None'. Please pass a valid layer to the 'previous_layer' parameter." self.logger.error(msg) raise TypeError(msg) - # A reference to the layer that preceeds the current layer in the network architecture. + # A reference to the layer that precedes the current layer in the network architecture. self.previous_layer = previous_layer if type(self.previous_layer.layer_output_size) in [list, tuple, numpy.ndarray] and len(self.previous_layer.layer_output_size) > 1: @@ -865,13 +865,13 @@ def __init__(self, def get_layers(self): """ - Prepares a list of all layers in the CNN model. + Prepares a list of all layers in the CNN model. Returns the list. """ network_layers = [] - # The last layer in the network archietcture. + # The last layer in the network architecture. layer = self.last_layer while "previous_layer" in layer.__init__.__code__.co_varnames: diff --git a/pygad/gacnn/gacnn.py b/pygad/gacnn/gacnn.py index 1623c7d..7de3f34 100644 --- a/pygad/gacnn/gacnn.py +++ b/pygad/gacnn/gacnn.py @@ -14,7 +14,7 @@ def population_as_vectors(population_networks): population_vectors = [] for solution in population_networks: - # Converting the weights of single layer from the current CNN (i.e. solution) to a vector. + # Converting the weights of the current CNN (i.e. solution) into a vector. solution_weights_vector = cnn.layers_weights_as_vector(solution) # Appending the weights vector of the current layer of a CNN (i.e. solution) to the weights of the previous layers of the same CNN (i.e. solution). population_vectors.append(solution_weights_vector) @@ -35,7 +35,7 @@ def population_as_matrices(population_networks, population_vectors): population_matrices = [] for solution, solution_weights_vector in zip(population_networks, population_vectors): - # Converting the weights of single layer from the current CNN (i.e. solution) from a vector to a matrix. + # Converting the weights of the current CNN (i.e. solution) from a vector into a matrix. solution_weights_matrix = cnn.layers_weights_as_matrix(solution, solution_weights_vector) # Appending the weights matrix of the current layer of a CNN (i.e. solution) to the weights of the previous layers of the same network (i.e. solution). population_matrices.append(solution_weights_matrix) @@ -89,7 +89,7 @@ def update_population_trained_weights(self, population_trained_weights): """ idx = 0 - # Fetches all layers weights matrices for a single solution (i.e. CNN) + # Loop through each solution (i.e. CNN) to update its weights. for solution in self.population_networks: # Calling the cnn.update_layers_trained_weights() function for updating the 'trained_weights' attribute for all layers in the current solution (i.e. CNN). cnn.update_layers_trained_weights(model=solution, diff --git a/pygad/gann/gann.py b/pygad/gann/gann.py index 2c3c5a7..a4f91d6 100644 --- a/pygad/gann/gann.py +++ b/pygad/gann/gann.py @@ -107,10 +107,10 @@ def create_network(num_neurons_input, num_neurons_output: Number of neurons in the output layer. num_neurons_hidden_layers=[]: A list holding the number of neurons in the hidden layer(s). If empty [], then no hidden layers are used. For each int value it holds, then a hidden layer is created with number of hidden neurons specified by the corresponding int value. For example, num_neurons_hidden_layers=[10] creates a single hidden layer with 10 neurons. num_neurons_hidden_layers=[10, 5] creates 2 hidden layers with 10 neurons for the first and 5 neurons for the second hidden layer. output_activation="softmax": The name of the activation function of the output layer which defaults to "softmax". - hidden_activations="relu": The name(s) of the activation function(s) of the hidden layer(s). It defaults to "relu". If passed as a string, this means the specified activation function will be used across all the hidden layers. If passed as a list, then it must has the same length as the length of the num_neurons_hidden_layers list. An exception is raised if there lengths are different. When hidden_activations is a list, a one-to-one mapping between the num_neurons_hidden_layers and hidden_activations lists occurs. + hidden_activations="relu": The name(s) of the activation function(s) of the hidden layer(s). It defaults to "relu". If passed as a string, this means the specified activation function will be used across all the hidden layers. If passed as a list, then it must have the same length as the num_neurons_hidden_layers list. An exception is raised if their lengths are different. When hidden_activations is a list, a one-to-one mapping between the num_neurons_hidden_layers and hidden_activations lists occurs. parameters_validated=False: If False, then the parameters are not validated and a call to the validate_network_parameters() function is made. - Returns the reference to the last layer in the network architecture which is the output layer. Based on such reference, all network layer can be fetched. + Returns the reference to the last layer in the network architecture which is the output layer. Based on such reference, all network layers can be fetched. """ # When parameters_validated is False, then the parameters are not yet validated and a call to validate_network_parameters() is required. @@ -149,7 +149,7 @@ def create_network(num_neurons_input, previous_layer=input_layer, activation_function=output_activation) - # Returning the reference to the last layer in the network architecture which is the output layer. Based on such reference, all network layer can be fetched. + # Returning the reference to the last layer in the network architecture which is the output layer. Based on such reference, all network layers can be fetched. return output_layer def population_as_vectors(population_networks): @@ -163,7 +163,7 @@ def population_as_vectors(population_networks): """ population_vectors = [] for solution in population_networks: - # Converting the weights of single layer from the current network (i.e. solution) to a vector. + # Converting the weights of the current network (i.e. solution) into a vector. solution_weights_vector = nn.layers_weights_as_vector(solution) # Appending the weights vector of the current layer of a network (i.e. solution) to the weights of the previous layers of the same network (i.e. solution). population_vectors.append(solution_weights_vector) @@ -182,7 +182,7 @@ def population_as_matrices(population_networks, population_vectors): """ population_matrices = [] for solution, solution_weights_vector in zip(population_networks, population_vectors): - # Converting the weights of single layer from the current network (i.e. solution) from a vector to a matrix. + # Converting the weights of the current network (i.e. solution) from a vector into a matrix. solution_weights_matrix = nn.layers_weights_as_matrix(solution, solution_weights_vector) # Appending the weights matrix of the current layer of a network (i.e. solution) to the weights of the previous layers of the same network (i.e. solution). population_matrices.append(solution_weights_matrix) @@ -200,7 +200,7 @@ def create_population(self): population_networks = [] for solution in range(self.num_solutions): # Creating a network (i.e. solution) in the population. A network or a solution can be used interchangeably. - # .copy() is so important to avoid modification in the original vale passed to the 'num_neurons_hidden_layers' and 'hidden_activations' parameters. + # .copy() is important to avoid changing the original values passed to the 'num_neurons_hidden_layers' and 'hidden_activations' parameters. network = create_network(num_neurons_input=self.num_neurons_input, num_neurons_output=self.num_neurons_output, num_neurons_hidden_layers=self.num_neurons_hidden_layers.copy(), @@ -224,17 +224,17 @@ def __init__(self, Creates an instance of the GANN class for training a neural network using the genetic algorithm. The constructor of the GANN class creates an initial population of multiple neural networks using the create_population() method. The population returned holds references to the last (i.e. output) layers of all created networks. - Besides creating the initial population, the passed parameters are vaidated using the validate_network_parameters() method. + Besides creating the initial population, the passed parameters are validated using the validate_network_parameters() method. num_solutions: Number of neural networks (i.e. solutions) in the population. Based on the value passed to this parameter, a number of identical neural networks are created where their parameters are optimized using the genetic algorithm. num_neurons_input: Number of neurons in the input layer. num_neurons_output: Number of neurons in the output layer. num_neurons_hidden_layers=[]: A list holding the number of neurons in the hidden layer(s). If empty [], then no hidden layers are used. For each int value it holds, then a hidden layer is created with number of hidden neurons specified by the corresponding int value. For example, num_neurons_hidden_layers=[10] creates a single hidden layer with 10 neurons. num_neurons_hidden_layers=[10, 5] creates 2 hidden layers with 10 neurons for the first and 5 neurons for the second hidden layer. output_activation="softmax": The name of the activation function of the output layer which defaults to "softmax". - hidden_activations="relu": The name(s) of the activation function(s) of the hidden layer(s). It defaults to "relu". If passed as a string, this means the specified activation function will be used across all the hidden layers. If passed as a list, then it must has the same length as the length of the num_neurons_hidden_layers list. An exception is raised if there lengths are different. When hidden_activations is a list, a one-to-one mapping between the num_neurons_hidden_layers and hidden_activations lists occurs. + hidden_activations="relu": The name(s) of the activation function(s) of the hidden layer(s). It defaults to "relu". If passed as a string, this means the specified activation function will be used across all the hidden layers. If passed as a list, then it must have the same length as the num_neurons_hidden_layers list. An exception is raised if their lengths are different. When hidden_activations is a list, a one-to-one mapping between the num_neurons_hidden_layers and hidden_activations lists occurs. """ - self.parameters_validated = False # If True, then the parameters passed to the GANN class constructor are valid. + self.parameters_validated = False # If True, then the parameters passed to the GANN class constructor are valid. # Validating the passed parameters before building the initial population. hidden_activations = validate_network_parameters(num_solutions=num_solutions, @@ -262,7 +262,7 @@ def update_population_trained_weights(self, population_trained_weights): population_trained_weights: A list holding the trained weights of all networks as matrices. Such matrices are to be assigned to the 'trained_weights' attribute of all layers of all networks. """ idx = 0 - # Fetches all layers weights matrices for a single solution (i.e. network) + # Loop through each solution (i.e. network) to update its weights. for solution in self.population_networks: # Calling the nn.update_layers_trained_weights() function for updating the 'trained_weights' attribute for all layers in the current solution (i.e. network). nn.update_layers_trained_weights(last_layer=solution, diff --git a/pygad/helper/activations.py b/pygad/helper/activations.py index f656249..6b0e4f2 100644 --- a/pygad/helper/activations.py +++ b/pygad/helper/activations.py @@ -40,7 +40,7 @@ def softmax(layer_outputs): """ Applies the softmax function. - sop: The input to which the softmax function is applied. + layer_outputs: The input to which the softmax function is applied. Returns the result of the softmax function. """ diff --git a/pygad/helper/misc.py b/pygad/helper/misc.py index 49157d1..25f6b2e 100644 --- a/pygad/helper/misc.py +++ b/pygad/helper/misc.py @@ -91,7 +91,7 @@ def print_mutation_params(): m = f"Mutation Percentage: {self.mutation_percent_genes}" self.logger.info(m) summary_output = summary_output + m + "\n" - # Number of mutation genes is already showed above. + # Number of mutation genes is already shown above. m = f"Mutation Genes: {self.mutation_num_genes}" self.logger.info(m) summary_output = summary_output + m + "\n" diff --git a/pygad/helper/unique.py b/pygad/helper/unique.py index ec79d9c..435cc72 100644 --- a/pygad/helper/unique.py +++ b/pygad/helper/unique.py @@ -126,7 +126,7 @@ def solve_duplicate_genes_by_space(self, # DEEP-DUPLICATE-REMOVAL-NEEDED # Search by this phrase to find where deep duplicates removal should be applied. # If there exist duplicate genes, then changing either of the 2 duplicating genes (with indices 2 and 3) will not solve the problem. - # This problem can be solved by randomly changing one of the non-duplicating genes that may make a room for a unique value in one the 2 duplicating genes. + # This problem can be solved by randomly changing one of the non-duplicating genes that may make room for a unique value in one of the 2 duplicating genes. # For example, if gene_space=[[3, 0, 1], [4, 1, 2], [0, 2], [3, 2, 0]] and the solution is [3 2 0 0], then the values of the last 2 genes duplicate. # There are no possible changes in the last 2 genes to solve the problem. But it could be solved by changing the second gene from 2 to 4. # As a result, any of the last 2 genes can take the value 2 and solve the duplicates. @@ -217,7 +217,7 @@ def unique_float_gene_from_range(self, sample_size (int): The maximum number of random values to generate to find a unique value. Returns: - int: The new floating-point value of the gene. If no unique value can be found, the original gene value is returned. + float: The new floating-point value of the gene. If no unique value can be found, the original gene value is returned. """ if self.gene_constraint and self.gene_constraint[gene_index]: @@ -414,7 +414,7 @@ def find_two_duplicates(self, if number_alternate_values > 1: return gene_idx, gene # This means there is no way to solve the duplicates between the genes. - # Because the space of the duplicates genes only has a single value and there is no alternatives. + # Because the space of the duplicate genes only has a single value and there are no alternatives. return None, gene def unpack_gene_space(self, @@ -433,7 +433,7 @@ def unpack_gene_space(self, list: A list representing the unpacked gene space. """ - # Copy the gene_space to keep it isolated form the changes. + # Copy the gene_space to keep it isolated from the changes. if self.gene_space is None: return None @@ -546,7 +546,7 @@ def solve_duplicates_deeply(self, solution): """ Sometimes it is impossible to solve the duplicate genes by simply selecting another value for either genes. - This function solve the duplicates between 2 genes by searching for a third gene that can make assist in the solution. + This function solves the duplicates between 2 genes by searching for a third gene that can assist in the solution. Args: solution (list): The current solution containing genes, potentially with duplicates. diff --git a/pygad/kerasga/kerasga.py b/pygad/kerasga/kerasga.py index 738e971..afc2274 100644 --- a/pygad/kerasga/kerasga.py +++ b/pygad/kerasga/kerasga.py @@ -130,7 +130,7 @@ def __init__(self, model, num_solutions): def create_population(self): """ - Creates the initial population of the genetic algorithm as a list of networks' weights (i.e. solutions). Each element in the list holds a different weights of the Keras model. + Creates the initial population of the genetic algorithm as a list of networks' weights (i.e. solutions). Each element in the list holds a different set of weights for the Keras model. The method returns a list holding the weights of all solutions. """ diff --git a/pygad/nn/nn.py b/pygad/nn/nn.py index dd426de..56f457d 100644 --- a/pygad/nn/nn.py +++ b/pygad/nn/nn.py @@ -38,7 +38,7 @@ def layers_weights(last_layer, initial=True): raise TypeError("The first layer in the network architecture must be an input layer.") # Currently, the weights of the layers are in the reverse order. In other words, the weights of the first layer are at the last index of the 'network_weights' list while the weights of the last layer are at the first index. - # Reversing the 'network_weights' list to order the layers' weights according to their location in the network architecture (i.e. the weights of the first layer appears at index 0 of the list). + # Reversing the 'network_weights' list to order the layers' weights according to their location in the network architecture (i.e. the weights of the first layer appear at index 0 of the list). network_weights.reverse() return network_weights @@ -75,7 +75,7 @@ def layers_weights_as_vector(last_layer, initial=True): raise TypeError("The first layer in the network architecture must be an input layer.") # Currently, the weights of the layers are in the reverse order. In other words, the weights of the first layer are at the last index of the 'network_weights' list while the weights of the last layer are at the first index. - # Reversing the 'network_weights' list to order the layers' weights according to their location in the network architecture (i.e. the weights of the first layer appears at index 0 of the list). + # Reversing the 'network_weights' list to order the layers' weights according to their location in the network architecture (i.e. the weights of the first layer appear at index 0 of the list). network_weights.reverse() return numpy.array(network_weights) @@ -112,7 +112,7 @@ def layers_weights_as_matrix(last_layer, vector_weights): raise TypeError("The first layer in the network architecture must be an input layer.") # Currently, the weights of the layers are in the reverse order. In other words, the weights of the first layer are at the last index of the 'network_weights' list while the weights of the last layer are at the first index. - # Reversing the 'network_weights' list to order the layers' weights according to their location in the network architecture (i.e. the weights of the first layer appears at index 0 of the list). + # Reversing the 'network_weights' list to order the layers' weights according to their location in the network architecture (i.e. the weights of the first layer appear at index 0 of the list). network_weights.reverse() return network_weights @@ -136,8 +136,8 @@ def layers_activations(last_layer): if not (type(layer) is InputLayer): raise TypeError("The first layer in the network architecture must be an input layer.") - # Currently, the activations of layers are in the reverse order. In other words, the activation function of the first layer are at the last index of the 'activations' list while the activation function of the last layer are at the first index. - # Reversing the 'activations' list to order the layers' weights according to their location in the network architecture (i.e. the activation function of the first layer appears at index 0 of the list). + # Currently, the activations of layers are in the reverse order. In other words, the activation function of the first layer is at the last index of the 'activations' list while the activation function of the last layer is at the first index. + # Reversing the 'activations' list to order the activation functions according to their location in the network architecture (i.e. the activation function of the first layer appears at index 0 of the list). activations.reverse() return activations @@ -196,13 +196,13 @@ def train(num_epochs, last_layer, network_error=network_error, learning_rate=learning_rate) - # Initially, the 'trained_weights' attribute of the layers are set to None. After the is trained, the 'trained_weights' attribute is updated by the trained weights using the update_layers_trained_weights() function. + # Initially, the 'trained_weights' attribute of the layers is set to None. After the network is trained, the 'trained_weights' attribute is updated by the trained weights using the update_layers_trained_weights() function. update_layers_trained_weights(last_layer, weights) def update_weights(weights, network_error, learning_rate): """ Updates the network weights using the learning rate only. - The purpose of this project is to only apply the forward pass of training a neural network. Thus, there is no optimization algorithm is used like the gradient descent. + The purpose of this project is to apply only the forward pass of training a neural network. Thus, no optimization algorithm such as gradient descent is used. For optimizing the neural network, check this project (https://github.com/ahmedfgad/NeuralGenetic) in which the genetic algorithm is used for training the network. weights: The current weights of the network. @@ -279,7 +279,7 @@ def predict(last_layer, data_inputs, problem_type="classification"): def to_vector(array): """ - Converts a passed NumPy array (of any dimensionality) to its `array` parameter into a 1D vector and returns the vector. + Converts the NumPy array passed to the `array` parameter (of any number of dimensions) into a 1D vector and returns the vector. array: The NumPy array to be converted into a 1D vector. @@ -293,7 +293,7 @@ def to_vector(array): def to_array(vector, shape): """ - Converts a passed vector to its `vector` parameter into a NumPy array and returns the array. + Converts the 1D vector passed to the `vector` parameter into a NumPy array and returns the array. vector: The 1D vector to be converted into an array. shape: The target shape of the array. @@ -322,7 +322,7 @@ def __init__(self, num_inputs): class DenseLayer: """ - Implementing the input dense (fully connected) layer of a neural network. + Implementing the dense (fully connected) layer of a neural network. """ def __init__(self, num_neurons, previous_layer, activation_function="sigmoid"): if num_neurons <= 0: @@ -337,7 +337,7 @@ def __init__(self, num_neurons, previous_layer, activation_function="sigmoid"): if previous_layer is None: raise TypeError("The previous layer cannot be of Type 'None'. Please pass a valid layer to the 'previous_layer' parameter.") - # A reference to the layer that preceeds the current layer in the network architecture. + # A reference to the layer that precedes the current layer in the network architecture. self.previous_layer = previous_layer # Initializing the weights of the layer. diff --git a/pygad/pygad.py b/pygad/pygad.py index f17bd99..7a1ac92 100644 --- a/pygad/pygad.py +++ b/pygad/pygad.py @@ -4,7 +4,7 @@ from pygad import helper from pygad import visualize -# Extend all the classes so that they can be referenced by just the `self` object of the `pygad.GA` class. +# Inherit from all these classes so that their methods can be accessed through the `self` object of the `pygad.GA` class. class GA(utils.parent_selection.ParentSelection, utils.crossover.Crossover, utils.mutation.Mutation, @@ -78,8 +78,8 @@ def __init__(self, num_genes: Number of genes in the solution. init_range_low: The lower value of the random range from which the gene values in the initial population are selected. It defaults to -4. Available in PyGAD 1.0.20 and higher. - init_range_high: The upper value of the random range from which the gene values in the initial population are selected. It defaults to -4. Available in PyGAD 1.0.20. - # It is OK to set the value of the 2 parameters ('init_range_low' and 'init_range_high') to be equal, higher or lower than the other parameter (i.e. init_range_low is not needed to be lower than init_range_high). + init_range_high: The upper value of the random range from which the gene values in the initial population are selected. It defaults to 4. Available in PyGAD 1.0.20. + It is OK for the 2 parameters ('init_range_low' and 'init_range_high') to be equal, or for one to be higher or lower than the other (i.e. 'init_range_low' does not need to be lower than 'init_range_high'). gene_type: The type of the gene. It is assigned to any of these types (int, numpy.int8, numpy.int16, numpy.int32, numpy.int64, numpy.uint, numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64, float, numpy.float16, numpy.float32, numpy.float64) and forces all the genes to be of that type. @@ -102,7 +102,7 @@ def __init__(self, random_mutation_min_val: The minimum value of the range from which a random value is selected to be added to the selected gene(s) to mutate. It defaults to -1.0. random_mutation_max_val: The maximum value of the range from which a random value is selected to be added to the selected gene(s) to mutate. It defaults to 1.0. - gene_space: It accepts a list of all possible values of the gene. This list is used in the mutation step. Should be used only if the gene space is a set of discrete values. No need for the 2 parameters (random_mutation_min_val and random_mutation_max_val) if the parameter gene_space exists. Added in PyGAD 2.5.0. In PyGAD 2.11.0, the gene_space can be assigned a dict. + gene_space: It accepts a list of all possible values of the gene. This list is used in the mutation step. It should be used only if the gene space is a set of discrete values. No need for the 2 parameters (random_mutation_min_val and random_mutation_max_val) if the parameter gene_space exists. Added in PyGAD 2.5.0. In PyGAD 2.11.0, the gene_space can be assigned a dict. gene_constraint: It accepts a list of constraints for the genes. Each constraint is a Python function. Added in PyGAD 3.5.0. sample_size: To select a gene value that respects a constraint, this variable defines the size of the sample from which a value is selected randomly. Useful if either allow_duplicate_genes or gene_constraint is used. Added in PyGAD 3.5.0. diff --git a/pygad/torchga/torchga.py b/pygad/torchga/torchga.py index 2682cec..f7a25b1 100644 --- a/pygad/torchga/torchga.py +++ b/pygad/torchga/torchga.py @@ -7,8 +7,8 @@ def model_weights_as_vector(model): for curr_weights in model.state_dict().values(): # Calling detach() to remove the computational graph from the layer. - # cpu() is called for making shore the data is moved from GPU to cpu - # numpy() is called for converting the tensor into a NumPy array. + # cpu() is called to make sure the data is moved from the GPU to the CPU. + # numpy() is called to convert the tensor into a NumPy array. curr_weights = curr_weights.cpu().detach().numpy() vector = numpy.reshape(curr_weights, (curr_weights.size)) weights_vector.extend(vector) @@ -21,8 +21,8 @@ def model_weights_as_dict(model, weights_vector): start = 0 for key in weights_dict: # Calling detach() to remove the computational graph from the layer. - # cpu() is called for making shore the data is moved from GPU to cpu - # numpy() is called for converting the tensor into a NumPy array. + # cpu() is called to make sure the data is moved from the GPU to the CPU. + # numpy() is called to convert the tensor into a NumPy array. w_matrix = weights_dict[key].cpu().detach().numpy() layer_weights_shape = w_matrix.shape layer_weights_size = w_matrix.size @@ -70,7 +70,7 @@ def __init__(self, model, num_solutions): def create_population(self): """ - Creates the initial population of the genetic algorithm as a list of networks' weights (i.e. solutions). Each element in the list holds a different weights of the PyTorch model. + Creates the initial population of the genetic algorithm as a list of networks' weights (i.e. solutions). Each element in the list holds a different set of weights for the PyTorch model. The method returns a list holding the weights of all solutions. """ diff --git a/pygad/utils/crossover.py b/pygad/utils/crossover.py index f8c8ff4..fbd6bdd 100644 --- a/pygad/utils/crossover.py +++ b/pygad/utils/crossover.py @@ -58,9 +58,9 @@ def single_point_crossover(self, parents, offspring_size): # Index of the second parent to mate. parent2_idx = (k+1) % parents.shape[0] - # The new offspring has its first half of its genes from the first parent. + # The first half of the offspring's genes comes from the first parent. offspring[k, 0:crossover_points[k]] = parents[parent1_idx, 0:crossover_points[k]] - # The new offspring has its second half of its genes from the second parent. + # The second half of the offspring's genes comes from the second parent. offspring[k, crossover_points[k]:] = parents[parent2_idx, crossover_points[k]:] if self.allow_duplicate_genes == False: @@ -87,7 +87,7 @@ def two_points_crossover(self, parents, offspring_size): It accepts 2 parameters: -parents: The parents to mate for producing the offspring. -offspring_size: The size of the offspring to produce. - It returns an array the produced offspring. + It returns an array of the produced offspring. """ if self.gene_type_single == True: @@ -160,7 +160,7 @@ def uniform_crossover(self, parents, offspring_size): It accepts 2 parameters: -parents: The parents to mate for producing the offspring. -offspring_size: The size of the offspring to produce. - It returns an array the produced offspring. + It returns an array of the produced offspring. """ if self.gene_type_single == True: @@ -228,7 +228,7 @@ def scattered_crossover(self, parents, offspring_size): It accepts 2 parameters: -parents: The parents to mate for producing the offspring. -offspring_size: The size of the offspring to produce. - It returns an array the produced offspring. + It returns an array of the produced offspring. """ if self.gene_type_single == True: diff --git a/pygad/utils/engine.py b/pygad/utils/engine.py index 5f406f8..fea148a 100644 --- a/pygad/utils/engine.py +++ b/pygad/utils/engine.py @@ -425,7 +425,7 @@ def run(self): generation_first_idx = self.generations_completed generation_last_idx = self.num_generations + self.generations_completed else: - # If the 'self.generations_completed' parameter is '0', then stat from scratch. + # If the 'self.generations_completed' parameter is '0', then start from scratch. generation_first_idx = 0 generation_last_idx = self.num_generations @@ -488,8 +488,8 @@ def run(self): if self.save_best_solutions: self.best_solutions.append(list(best_solution)) - # Note: Any code that has loop-dependant statements (e.g. continue, break, etc.) must be kept inside the loop of the 'run()' method. It can be moved to another method to clean the run() method. - # If the on_generation attribute is not None, then cal the callback function after the generation. + # Note: Any code that has loop-dependent statements (e.g. continue, break, etc.) must be kept inside the loop of the 'run()' method. It cannot be moved to another method to clean up the run() method. + # If the on_generation attribute is not None, then call the callback function after the generation. if not (self.on_generation is None): r = self.on_generation(self) if type(r) is str and r.lower() == "stop": @@ -874,7 +874,7 @@ def best_solution(self, pop_fitness=None): """ Returns information about the best solution found by the genetic algorithm. Accepts the following parameters: - pop_fitness: An optional parameter holding the fitness values of the solutions in the latest population. If passed, then it save time calculating the fitness. If None, then the 'cal_pop_fitness()' method is called to calculate the fitness of the latest population. + pop_fitness: An optional parameter holding the fitness values of the solutions in the latest population. If passed, then it saves time calculating the fitness. If None, then the 'cal_pop_fitness()' method is called to calculate the fitness of the latest population. The following are returned: -best_solution: Best solution in the current population. -best_solution_fitness: Fitness value of the best solution. @@ -883,7 +883,7 @@ def best_solution(self, pop_fitness=None): try: if pop_fitness is None: - # If the 'pop_fitness' parameter is not passed, then we have to call the 'cal_pop_fitness()' method to calculate the fitness of all solutions in the lastest population. + # If the 'pop_fitness' parameter is not passed, then we have to call the 'cal_pop_fitness()' method to calculate the fitness of all solutions in the latest population. pop_fitness = self.cal_pop_fitness() # Verify the type of the 'pop_fitness' parameter. elif type(pop_fitness) in [tuple, list, numpy.ndarray]: diff --git a/pygad/utils/mutation.py b/pygad/utils/mutation.py index a9ad051..e520f53 100644 --- a/pygad/utils/mutation.py +++ b/pygad/utils/mutation.py @@ -40,7 +40,7 @@ def random_mutation(self, offspring): else: # When the 'mutation_probability' parameter exists (i.e. not None), then it is used in the mutation. if not (self.gene_space is None): - # When the attribute 'gene_space' does not exist (i.e. None), the mutation values are selected randomly based on the continuous range specified by the 2 attributes 'random_mutation_min_val' and 'random_mutation_max_val'. + # When the attribute 'gene_space' exists (i.e. not None), the mutation values are selected from the space of values of each gene. offspring = self.mutation_probs_by_space(offspring) else: offspring = self.mutation_probs_randomly(offspring) @@ -79,7 +79,7 @@ def mutation_by_space(self, offspring): def mutation_probs_by_space(self, offspring): """ - Applies the random mutation using the mutation values' space and the mutation probability. For each gene, if its probability is <= that the mutation probability, then it will be mutated based on the mutation space. + Applies the random mutation using the mutation values' space and the mutation probability. For each gene, if its probability is <= the mutation probability, then it will be mutated based on the mutation space. It accepts: -offspring: The offspring to mutate. It returns an array of the mutated offspring using the mutation space. @@ -121,7 +121,7 @@ def mutation_process_gene_value(self, -solution: The solution where the target gene exists. -gene_idx: The index of the gene in the solution. -sample_size: The number of random values to generate from which a value is selected. It tries to generate a number of values up to a maximum of sample_size. But it is not always guaranteed because the total number of values might not be enough or the random generator creates duplicate random values. - It returns a single numeric value the satisfies the gene constraint if exists in the gene_constraint parameter. + It returns a single numeric value that satisfies the gene constraint, if one exists in the gene_constraint parameter. """ # Check if the gene has a constraint. @@ -152,7 +152,7 @@ def mutation_process_gene_value(self, solution=solution, mutation_by_replacement=self.mutation_by_replacement, sample_size=1) - # Even that its name is singular, it might have a multiple values. + # Even though its name is singular, it might hold multiple values. return value_selected def mutation_randomly(self, offspring): @@ -173,7 +173,7 @@ def mutation_randomly(self, offspring): range_min, range_max = self.get_random_mutation_range(gene_idx) - # Generate a random value for mutation that meet the gene constraint if exists. + # Generate a random value for mutation that meets the gene constraint, if one exists. random_value = self.mutation_process_gene_value(range_min=range_min, range_max=range_max, solution=offspring[offspring_idx], @@ -195,7 +195,7 @@ def mutation_randomly(self, offspring): def mutation_probs_randomly(self, offspring): """ - Applies the random mutation using the mutation probability. For each gene, if its probability is <= that mutation probability, then it will be mutated randomly. + Applies the random mutation using the mutation probability. For each gene, if its probability is <= the mutation probability, then it will be mutated randomly. It accepts: -offspring: The offspring to mutate. It returns an array of the mutated offspring. @@ -212,7 +212,7 @@ def mutation_probs_randomly(self, offspring): # A gene is mutated only if its mutation probability is less than or equal to the threshold. if probs[gene_idx] <= self.mutation_probability: - # Generate a random value fpr mutation that meet the gene constraint if exists. + # Generate a random value for mutation that meets the gene constraint, if one exists. random_value = self.mutation_process_gene_value(range_min=range_min, range_max=range_max, solution=offspring[offspring_idx], @@ -471,7 +471,7 @@ def adaptive_mutation_by_space(self, offspring): """ Applies the adaptive mutation based on the 2 parameters 'mutation_num_genes' and 'gene_space'. - A number of genes equal are selected randomly for mutation. This number depends on the fitness of the solution. + A number of genes are selected randomly for mutation. This number depends on the fitness of the solution. The random values are selected from the 'gene_space' parameter. It accepts: -offspring: The offspring to mutate. @@ -532,7 +532,7 @@ def adaptive_mutation_randomly(self, offspring): """ Applies the adaptive mutation based on the 'mutation_num_genes' parameter. - A number of genes equal are selected randomly for mutation. This number depends on the fitness of the solution. + A number of genes are selected randomly for mutation. This number depends on the fitness of the solution. The random values are selected based on the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val'. It accepts: -offspring: The offspring to mutate. @@ -596,7 +596,7 @@ def adaptive_mutation_probs_by_space(self, offspring): """ Applies the adaptive mutation based on the 2 parameters 'mutation_probability' and 'gene_space'. - Based on whether the solution fitness is above or below a threshold, the mutation is applied diffrently by mutating high or low number of genes. + Based on whether the solution fitness is above or below a threshold, the mutation is applied differently by mutating a high or low number of genes. The random values are selected based on space of values for each gene. It accepts: -offspring: The offspring to mutate. @@ -659,7 +659,7 @@ def adaptive_mutation_probs_randomly(self, offspring): """ Applies the adaptive mutation based on the 'mutation_probability' parameter. - Based on whether the solution fitness is above or below a threshold, the mutation is applied diffrently by mutating high or low number of genes. + Based on whether the solution fitness is above or below a threshold, the mutation is applied differently by mutating a high or low number of genes. The random values are selected based on the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val'. It accepts: -offspring: The offspring to mutate. @@ -702,7 +702,7 @@ def adaptive_mutation_probs_randomly(self, offspring): range_min, range_max = self.get_random_mutation_range(gene_idx) if probs[gene_idx] <= adaptive_mutation_probability: - # Generate a random value fpr mutation that meet the gene constraint if exists. + # Generate a random value for mutation that meets the gene constraint, if one exists. random_value = self.mutation_process_gene_value(range_min=range_min, range_max=range_max, solution=offspring[offspring_idx], diff --git a/pygad/utils/nsga2.py b/pygad/utils/nsga2.py index e904fed..ae74792 100644 --- a/pygad/utils/nsga2.py +++ b/pygad/utils/nsga2.py @@ -20,7 +20,7 @@ def get_non_dominated_set(self, curr_solutions): dominated_set : TYPE A set of the dominated solutions. non_dominated_set : TYPE - A set of the non-dominated set. + A set of the non-dominated solutions. """ # List of the members of the current dominated pareto front/set. @@ -69,12 +69,12 @@ def get_non_dominated_set(self, curr_solutions): def non_dominated_sorting(self, fitness): """ - Apply non-dominant sorting over the fitness to create the pareto fronts based on non-dominated sorting of the solutions. + Apply non-dominated sorting over the fitness to create the pareto fronts based on the non-dominated sorting of the solutions. Parameters ---------- fitness : TYPE - An array of the population fitness across all objective function. + An array of the population's fitness values across all objective functions. Returns ------- @@ -164,7 +164,7 @@ def crowding_distance(self, pareto_front, fitness): # Loop through the objectives to calculate the crowding distance of each solution across all objectives. for obj_idx in range(pareto_front_no_indices.shape[1]): obj = pareto_front_no_indices[:, obj_idx] - # This variable has a nested list where each child list zip the following together: + # This variable has a nested list where each child list zips the following together: # 1) The index of the objective value. # 2) The objective value. # 3) Initialize the crowding distance by zero. @@ -213,7 +213,7 @@ def crowding_distance(self, pareto_front, fitness): # An array of the sum of crowding distances across all objectives. # Each row has 2 elements: # 1) The index of the solution. - # 2) The sum of all crowding distances for all objective of the solution. + # 2) The sum of all crowding distances for all objectives of the solution. crowding_dist_sum = numpy.array(list(zip(obj_crowding_dist_list[0, :, 0], crowding_dist_sum))) crowding_dist_sum = sorted(crowding_dist_sum, key=lambda x: x[1], reverse=True) diff --git a/pygad/utils/parent_selection.py b/pygad/utils/parent_selection.py index 52e8547..5922369 100644 --- a/pygad/utils/parent_selection.py +++ b/pygad/utils/parent_selection.py @@ -12,8 +12,8 @@ def __init__(): def steady_state_selection(self, fitness, num_parents): """ - Selects the parents using the steady-state selection technique. - This is by sorting the solutions based on the fitness and select the best ones as parents. + Selects the parents using the steady-state selection technique. + This works by sorting the solutions based on the fitness and selecting the best ones as parents. Later, these parents will mate to produce the offspring. It accepts 2 parameters: @@ -52,7 +52,7 @@ def rank_selection(self, fitness, num_parents): # This function works with both single- and multi-objective optimization problems. fitness_sorted = self.sort_solutions_nsga2(fitness=fitness) - # Rank the solutions based on their fitness. The worst is gives the rank 1. The best has the rank N. + # Rank the solutions based on their fitness. The worst is given rank 1. The best is given rank N. rank = numpy.arange(1, self.sol_per_pop+1) probs = rank / numpy.sum(rank) @@ -66,7 +66,7 @@ def rank_selection(self, fitness, num_parents): rand_prob = numpy.random.rand() for idx in range(probs.shape[0]): if (rand_prob >= probs_start[idx] and rand_prob < probs_end[idx]): - # The variable idx has the rank of solution but not its index in the population. + # The variable idx holds the rank of the solution, not its index in the population. # Return the correct index of the solution. mapped_idx = fitness_sorted[idx] parents[parent_num, :] = self.population[mapped_idx, :].copy() @@ -158,7 +158,7 @@ def roulette_wheel_selection(self, fitness, num_parents): # Single-objective optimization problem. pass - # Reaching this step extends that fitness is a 1D array. + # Reaching this step confirms that fitness is a 1D array. fitness_sum = numpy.sum(fitness) if fitness_sum == 0: self.logger.error("Cannot proceed because the sum of fitness values is zero. Cannot divide by zero.") @@ -191,7 +191,7 @@ def wheel_cumulative_probs(self, probs, num_parents): It accepts a single 1D array representing the probabilities of selecting each solution. It returns 2 1D arrays: 1) probs_start has the start of each range. - 2) probs_start has the end of each range. + 2) probs_end has the end of each range. It also returns an empty array for the parents. """ @@ -235,7 +235,7 @@ def stochastic_universal_selection(self, fitness, num_parents): # Single-objective optimization problem. pass - # Reaching this step extends that fitness is a 1D array. + # Reaching this step confirms that fitness is a 1D array. fitness_sum = numpy.sum(fitness) if fitness_sum == 0: self.logger.error("Cannot proceed because the sum of fitness values is zero. Cannot divide by zero.") @@ -276,7 +276,7 @@ def tournament_selection_nsga2(self, Select the parents using the tournament selection technique for NSGA-II. The traditional tournament selection uses the fitness values. But the tournament selection for NSGA-II uses non-dominated sorting and crowding distance. Using non-dominated sorting, the solutions are distributed across pareto fronts. The fronts are given the indices 0, 1, 2, ..., N where N is the number of pareto fronts. The lower the index of the pareto front, the better its solutions. - To select the parents solutions, 2 solutions are selected randomly. If the 2 solutions are in different pareto fronts, then the solution comming from a pareto front with lower index is selected. + To select the parents solutions, 2 solutions are selected randomly. If the 2 solutions are in different pareto fronts, then the solution coming from a pareto front with lower index is selected. If 2 solutions are in the same pareto front, then crowding distance is calculated. The solution with the higher crowding distance is selected. If the 2 solutions are in the same pareto front and have the same crowding distance, then a solution is randomly selected. Later, the selected parents will mate to produce the offspring. diff --git a/pygad/utils/validation.py b/pygad/utils/validation.py index 1d65299..62bbe77 100644 --- a/pygad/utils/validation.py +++ b/pygad/utils/validation.py @@ -13,7 +13,7 @@ def _validate_header(self, mutation_by_replacement, sample_size, allow_duplicate_genes): - # If no logger is passed, then create a logger that logs only the messages to the console. + # If no logger is passed, then create a logger that logs the messages only to the console. if logger is None: # Create a logger named with the module name. logger = logging.getLogger(__name__) @@ -771,7 +771,7 @@ def _validate_parent_selection(self, self.valid_parameters = False raise ValueError(f"When 'parent_selection_type' is assigned to a method, then it must accept 3 parameters:\n1) The fitness values of the current population.\n2) The number of parents needed.\n3) The instance from the pygad.GA class.\n\nThe passed parent selection method named '{parent_selection_type.__code__.co_name}' accepts {len(inspect.signature(parent_selection_type).parameters)} parameter(s).") elif inspect.isfunction(parent_selection_type): - # Check if the parent_selection_type is a function that accepts 2 parameters. + # Check if the parent_selection_type is a function that accepts 3 parameters. if len(inspect.signature(parent_selection_type).parameters) == 3: # The parent selection function assigned to the parent_selection_type parameter is validated. self.select_parents = parent_selection_type diff --git a/pygad/visualize/plot.py b/pygad/visualize/plot.py index 3341c84..db3987c 100644 --- a/pygad/visualize/plot.py +++ b/pygad/visualize/plot.py @@ -78,7 +78,7 @@ def plot_fitness(self, current_color = color[objective_idx] current_linewidth = linewidth[objective_idx] current_label = label[objective_idx] - # Return the fitness values for the current objective function across all best solutions acorss all generations. + # Return the fitness values for the current objective function across all generations. fitness = numpy.array(self.best_solutions_fitness)[:, objective_idx] if plot_type == "plot": matplt.plot(fitness, From 3b9879605742ce7e1e4bcce7786a2d2e9d53426a Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 21 May 2026 17:10:32 -0400 Subject: [PATCH 42/42] docs: fix English in exception/warning messages and code-identifier typos Apply the simple-English pass to the items skipped earlier: - Fix the English of exception and warning message strings: "the the" -> "the", "constrains" -> "constraints", "is subset of" -> "is a subset of", "a better solution that the best" -> "than the best", "A list of NumPy array" -> "A list or NumPy array", and "For format of a single criterion" -> "The format ...". - Rename two misspelled local variables: boxeplots -> boxplots and max_lengthes -> max_lengths. No behavior changes. No test asserts on these messages, and all 711 runnable tests pass. --- pygad/helper/misc.py | 6 +++--- pygad/utils/engine.py | 4 ++-- pygad/utils/validation.py | 8 ++++---- pygad/visualize/plot.py | 10 +++++----- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pygad/helper/misc.py b/pygad/helper/misc.py index 25f6b2e..0c31472 100644 --- a/pygad/helper/misc.py +++ b/pygad/helper/misc.py @@ -194,10 +194,10 @@ def print_params_summary(): print_crossover_params, None, print_mutation_params, None, print_on_generation_params, None] if not columns_equal_len: - max_lengthes = [max(list(map(len, lifecycle_steps))), max( + max_lengths = [max(list(map(len, lifecycle_steps))), max( list(map(len, lifecycle_functions))), max(list(map(len, lifecycle_output)))] split_percentages = [ - int((column_len / sum(max_lengthes)) * 100) for column_len in max_lengthes] + int((column_len / sum(max_lengths)) * 100) for column_len in max_lengths] else: split_percentages = None @@ -409,7 +409,7 @@ def filter_gene_values_by_constraint(self, if result: pass else: - raise Exception("The output from the gene_constraint callable/function must be a list or NumPy array that is subset of the passed values (second argument).") + raise Exception("The output from the gene_constraint callable/function must be a list or NumPy array that is a subset of the passed values (second argument).") # After going through all the values, check if any value satisfies the constraint. if len(filtered_values) > 0: diff --git a/pygad/utils/engine.py b/pygad/utils/engine.py index fea148a..1dff46e 100644 --- a/pygad/utils/engine.py +++ b/pygad/utils/engine.py @@ -100,7 +100,7 @@ def initialize_population(self, if result: pass else: - raise Exception("The output from the gene_constraint callable/function must be a list or NumPy array that is subset of the passed values (second argument).") + raise Exception("The output from the gene_constraint callable/function must be a list or NumPy array that is a subset of the passed values (second argument).") if len(filtered_values) ==1 and filtered_values[0] != solution[gene_idx]: # Error by the user's defined gene constraint callable. @@ -515,7 +515,7 @@ def run(self): pass else: self.valid_parameters = False - raise ValueError(f"When the the 'reach' keyword is used with the 'stop_criteria' parameter for solving a multi-objective problem, then the number of numeric values following the keyword can be:\n1) A single numeric value to be used across all the objective functions.\n2) A number of numeric values equal to the number of objective functions.\nBut the value {criterion} found with {len(criterion)-1} numeric values which is not equal to the number of objective functions {len(self.last_generation_fitness[0])}.") + raise ValueError(f"When the 'reach' keyword is used with the 'stop_criteria' parameter for solving a multi-objective problem, then the number of numeric values following the keyword can be:\n1) A single numeric value to be used across all the objective functions.\n2) A number of numeric values equal to the number of objective functions.\nBut the value {criterion} found with {len(criterion)-1} numeric values which is not equal to the number of objective functions {len(self.last_generation_fitness[0])}.") stop_run = True for obj_idx in range(len(self.last_generation_fitness[0])): diff --git a/pygad/utils/validation.py b/pygad/utils/validation.py index 62bbe77..b893e7f 100644 --- a/pygad/utils/validation.py +++ b/pygad/utils/validation.py @@ -442,13 +442,13 @@ def _validate_gene_constraint(self, pass else: self.valid_parameters = False - raise ValueError(f"Every callable inside the gene_constraint parameter must accept 2 arguments representing 1) The solution/chromosome where the gene exists 2) A list of NumPy array of values to check if they meet the constraint. But the callable at index {constraint_idx} named '{item.__code__.co_name}' accepts {item.__code__.co_argcount} argument(s).") + raise ValueError(f"Every callable inside the gene_constraint parameter must accept 2 arguments representing 1) The solution/chromosome where the gene exists 2) A list or NumPy array of values to check if they meet the constraint. But the callable at index {constraint_idx} named '{item.__code__.co_name}' accepts {item.__code__.co_argcount} argument(s).") else: self.valid_parameters = False raise TypeError(f"The expected type of an element in the 'gene_constraint' parameter is None or a callable (e.g. function). But {item} at index {constraint_idx} of type {type(item)} found.") else: self.valid_parameters = False - raise ValueError(f"The number of constrains ({len(gene_constraint)}) in the 'gene_constraint' parameter must be equal to the number of genes ({self.num_genes}).") + raise ValueError(f"The number of constraints ({len(gene_constraint)}) in the 'gene_constraint' parameter must be equal to the number of genes ({self.num_genes}).") else: self.valid_parameters = False raise TypeError(f"The expected type of the 'gene_constraint' parameter is either a list or tuple. But the value {gene_constraint} of type {type(gene_constraint)} found.") @@ -752,7 +752,7 @@ def _validate_mutation(self, # Check if crossover and mutation are both disabled. if (self.mutation_type is None) and (self.crossover_type is None): if not self.suppress_warnings: - warnings.warn("The 2 parameters mutation_type and crossover_type are None. This disables any type of evolution the genetic algorithm can make. As a result, the genetic algorithm cannot find a better solution that the best solution in the initial population.") + warnings.warn("The 2 parameters mutation_type and crossover_type are None. This disables any type of evolution the genetic algorithm can make. As a result, the genetic algorithm cannot find a better solution than the best solution in the initial population.") return mutation_num_genes, mutation_percent_genes def _validate_parent_selection(self, @@ -1222,7 +1222,7 @@ def _validate_stop_criteria(self, self.stop_criteria.append([stop_word] + number) else: self.valid_parameters = False - raise ValueError(f"For format of a single criterion in the 'stop_criteria' parameter is 'word_number' but '{stop_criteria}' found.") + raise ValueError(f"The format of a single criterion in the 'stop_criteria' parameter is 'word_number' but '{stop_criteria}' found.") elif type(stop_criteria) in [list, tuple, numpy.ndarray]: # Remove duplicate criteria by converting the list to a set then back to a list. diff --git a/pygad/visualize/plot.py b/pygad/visualize/plot.py index db3987c..73842f1 100644 --- a/pygad/visualize/plot.py +++ b/pygad/visualize/plot.py @@ -332,23 +332,23 @@ def plot_genes(self, if "tick_labels" in inspect.signature(ax.boxplot).parameters else "labels" ) - boxeplots = ax.boxplot(solutions_to_plot, + boxplots = ax.boxplot(solutions_to_plot, patch_artist=True, **{_tick_kw: range(self.num_genes)}) # adding horizontal grid lines ax.yaxis.grid(True) - for box in boxeplots['boxes']: + for box in boxplots['boxes']: # change outline color box.set(color='black', linewidth=linewidth) # change fill color https://color.adobe.com/create/color-wheel box.set_facecolor(fill_color) - for whisker in boxeplots['whiskers']: + for whisker in boxplots['whiskers']: whisker.set(color=color, linewidth=linewidth) - for median in boxeplots['medians']: + for median in boxplots['medians']: median.set(color=color, linewidth=linewidth) - for cap in boxeplots['caps']: + for cap in boxplots['caps']: cap.set(color=color, linewidth=linewidth) matplt.title(title, fontsize=font_size)