diff --git a/README.md b/README.md index 08bd7e4..33710b4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Check documentation of the [PyGAD](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.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) [![PyGAD PyTest / Python 3.7](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py37.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py37.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)]( +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)]( https://stackoverflow.com/questions/tagged/pygad) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/ahmedfgad/GeneticAlgorithmPython/badge)](https://securityscorecards.dev/viewer/?uri=github.com/ahmedfgad/GeneticAlgorithmPython) [![DOI](https://zenodo.org/badge/DOI/10.1007/s11042-023-17167-y.svg)](https://doi.org/10.1007/s11042-023-17167-y) ![PYGAD-LOGO](https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png) @@ -253,16 +253,6 @@ Get started with the genetic algorithm by reading the tutorial titled [**Introdu [![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: @@ -273,29 +263,6 @@ Read about training neural networks using the genetic algorithm through the tuto [![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. diff --git a/examples/example_gene_constraint.py b/examples/example_gene_constraint.py new file mode 100644 index 0000000..ae9db32 --- /dev/null +++ b/examples/example_gene_constraint.py @@ -0,0 +1,37 @@ +import pygad +import numpy + +""" +An example of using the gene_constraint parameter. + +""" + +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 + +ga_instance = pygad.GA(num_generations=100, + num_parents_mating=5, + sol_per_pop=10, + num_genes=len(function_inputs), + mutation_num_genes=6, + fitness_func=fitness_func, + allow_duplicate_genes=False, + gene_space=range(100), + gene_type=int, + sample_size=100, + random_seed=10, + gene_constraint=[lambda x,v: [val for val in v if val>=98], + lambda x,v: [val for val in v if val>=98], + lambda x,v: [val for val in v if 80 0: + if len(filtered_values) > 0: # At least one value was found that meets the gene constraint. pass else: # No value found for the current gene that satisfies the constraint. - if not self.suppress_warnings: - warnings.warn(f"No value found for the gene at index {gene_idx} with value {solution[gene_idx]} that satisfies its gene constraint.") + if not self.suppress_warnings: warnings.warn(f"Failed to find a value that satisfies its gene constraint for the gene at index {gene_idx} with value {solution[gene_idx]} at generation {self.generations_completed}.") return None - filtered_values = values[filtered_values_indices] - return filtered_values def get_gene_dtype(self, gene_index): diff --git a/pygad/helper/unique.py b/pygad/helper/unique.py index 60b2507..ec79d9c 100644 --- a/pygad/helper/unique.py +++ b/pygad/helper/unique.py @@ -69,7 +69,7 @@ def solve_duplicate_genes_randomly(self, if temp_val in new_solution: num_unsolved_duplicates = num_unsolved_duplicates + 1 - if not self.suppress_warnings: warnings.warn(f"Failed to find a unique value for gene with index {duplicate_index} whose value is {solution[duplicate_index]}. Consider adding more values in the gene space or use a wider range for initial population or random mutation.") + if not self.suppress_warnings: warnings.warn(f"Failed to find a unique value for gene with index {duplicate_index} whose value is {solution[duplicate_index]} at generation {self.generations_completed}. Consider adding more values in the gene space or use a wider range for initial population or random mutation.") else: # Unique gene value found. new_solution[duplicate_index] = temp_val @@ -320,7 +320,7 @@ def unique_genes_by_space(self, if temp_val in solution: num_unsolved_duplicates = num_unsolved_duplicates + 1 - if not self.suppress_warnings: warnings.warn(f"Failed to find a unique value for gene with index {duplicate_index} whose value is {solution[duplicate_index]}. Consider adding more values in the gene space or use a wider range for initial population or random mutation.") + if not self.suppress_warnings: warnings.warn(f"Failed to find a unique value for gene with index {duplicate_index} whose value is {solution[duplicate_index]} at generation {self.generations_completed+1}. Consider adding more values in the gene space or use a wider range for initial population or random mutation.") else: solution[duplicate_index] = temp_val diff --git a/pygad/pygad.py b/pygad/pygad.py index 0ed9d82..3661dab 100644 --- a/pygad/pygad.py +++ b/pygad/pygad.py @@ -545,13 +545,13 @@ def __init__(self, if item is None: pass elif item and callable(item): - if item.__code__.co_argcount == 1: - # Every callable is valid if it receives a single argument. - # This argument represents the solution. + if item.__code__.co_argcount == 2: + # Every callable is valid if it receives 2 arguments. + # The 2 arguments: 1) solution 2) A list or numpy.ndarray of values to check if they meet the constraint. pass else: self.valid_parameters = False - raise ValueError(f"Every callable inside the gene_constraint parameter must accept a single argument representing the solution/chromosome. 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 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).") 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.") @@ -1424,7 +1424,25 @@ def initialize_population(self, for gene_idx in range(self.num_genes): # Check that a constraint is available for the gene and that the current value does not satisfy that constraint if self.gene_constraint[gene_idx]: - if not self.gene_constraint[gene_idx](solution): + # Remember that the second argument to the gene constraint callable is a list/numpy.ndarray of the values to check if they meet the gene constraint. + values = [solution[gene_idx]] + filtered_values = self.gene_constraint[gene_idx](solution, values) + result = self.validate_gene_constraint_callable_output(selected_values=filtered_values, + values=values) + 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).") + + if len(filtered_values) ==1 and filtered_values[0] != solution[gene_idx]: + # Error by the user's defined gene constraint callable. + raise Exception(f"It is expected to receive a list/numpy.ndarray from the gene_constraint callable with a single value equal to {values[0]}, but the value {filtered_values[0]} found.") + + # Check if the gene value does not satisfy the gene constraint. + # Note that we already passed a list of a single value. + # It is expected to receive a list of either a single value or an empty list. + if len(filtered_values) < 1: + # Search for a value that satisfies the gene constraint. range_min, range_max = self.get_initial_population_range(gene_index=gene_idx) # While initializing the population, we follow a mutation by replacement approach. So, the original gene value is not needed. values_filtered = self.get_valid_gene_constraint_values(range_min=range_min, @@ -1439,6 +1457,12 @@ def initialize_population(self, warnings.warn(f"No value satisfied the constraint for the gene at index {gene_idx} with value {solution[gene_idx]} while creating the initial population.") else: self.population[sol_idx, gene_idx] = random.choice(values_filtered) + elif len(filtered_values) == 1: + # The value already satisfied the gene constraint. + pass + else: + # Error by the user's defined gene constraint callable. + raise Exception(f"It is expected to receive a list/numpy.ndarray from the gene_constraint callable that is either empty or has a single value equal, but received a list/numpy.ndarray of length {len(filtered_values)}.") # 4) Solve duplicate genes. if allow_duplicate_genes == False: diff --git a/pyproject.toml b/pyproject.toml index aed6e1d..34ae65c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta" [project] name = "pygad" -version = "3.4.0" +version = "3.5.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" diff --git a/tests/test_gene_constraint.py b/tests/test_gene_constraint.py index 43cf38a..7b6912e 100644 --- a/tests/test_gene_constraint.py +++ b/tests/test_gene_constraint.py @@ -69,7 +69,11 @@ def fitness_func_no_batch_multi(ga, solution, idx): #### Single-Objective def test_initial_population_int_by_replacement(): - gene_constraint=[lambda x: x[0]>=8,lambda x: x[1]>=8,lambda x: 5>=x[2]>=1,lambda x: 5>x[3]>3,lambda x: x[4]<2] + gene_constraint=[lambda x,v: [val for val in v if val>=8], + lambda x,v: [val for val in v if val>=8], + lambda x,v: [val for val in v if 5>=val>=1], + lambda x,v: [val for val in v if 5>val>3], + lambda x,v: [val for val in v if val<2]] ga_instance = population_gene_constraint(gene_constraint=gene_constraint, init_range_low=0, init_range_high=10, @@ -79,7 +83,6 @@ def test_initial_population_int_by_replacement(): gene_type=int, mutation_by_replacement=True) initial_population = ga_instance.initial_population - # print(initial_population) assert numpy.all(initial_population[:, 0] >= 8), "Not all values in column 0 are >= 8" assert numpy.all(initial_population[:, 1] >= 8), "Not all values in column 1 are >= 8" @@ -87,9 +90,14 @@ def test_initial_population_int_by_replacement(): assert numpy.all((initial_population[:, 2] >= 1) & (initial_population[:, 2] <= 5)), "Not all values in column 2 between 1 and 5 (inclusive)" assert numpy.all(initial_population[:, 3] == 4), "Not all values in column 3 between 3 and 5 (exclusive)" assert numpy.all(initial_population[:, 4] < 2), "Not all values in column 4 < 2" + print("All constraints are met") def test_initial_population_int_by_replacement_no_duplicates(): - gene_constraint=[lambda x: x[0]>=5,lambda x: x[1]>=5,lambda x: x[2]>=5,lambda x: x[3]>=5,lambda x: x[4]>=5] + gene_constraint=[lambda x,v: [val for val in v if val>=5], + lambda x,v: [val for val in v if val>=5], + lambda x,v: [val for val in v if val>=5], + lambda x,v: [val for val in v if val>=5], + lambda x,v: [val for val in v if val>=5]] ga_instance = population_gene_constraint(gene_constraint=gene_constraint, init_range_low=1, init_range_high=10, @@ -117,9 +125,15 @@ def test_initial_population_int_by_replacement_no_duplicates(): assert numpy.all(initial_population[:, 2] >= 5), "Not all values in column 2 >= 5" assert numpy.all(initial_population[:, 3] >= 5), "Not all values in column 3 >= 5" assert numpy.all(initial_population[:, 4] >= 5), "Not all values in column 4 >= 5" + print("All constraints are met") def test_initial_population_int_by_replacement_no_duplicates2(): - gene_constraint=[lambda x: x[0]>=98,lambda x: x[1]>=98,lambda x: 20=98], + lambda x,v: [val for val in v if val>=98], + lambda x,v: [val for val in v if 20=5,lambda x: x[1]>=5,lambda x: x[2]>=5,lambda x: x[3]>=5,lambda x: x[4]>=5] + gene_constraint=[lambda x,v: [val for val in v if val>=5], + lambda x,v: [val for val in v if val>=5], + lambda x,v: [val for val in v if val>=5], + lambda x,v: [val for val in v if val>=5], + lambda x,v: [val for val in v if val>=5]] ga_instance = population_gene_constraint(gene_constraint=gene_constraint, init_range_low=1, init_range_high=10, @@ -178,9 +197,48 @@ def test_initial_population_float_by_replacement_no_duplicates(): assert numpy.all(initial_population[:, 2] >= 5), "Not all values in column 2 >= 5" assert numpy.all(initial_population[:, 3] >= 5), "Not all values in column 3 >= 5" assert numpy.all(initial_population[:, 4] >= 5), "Not all values in column 4 >= 5" + print("All constraints are met") def test_initial_population_float_by_replacement_no_duplicates2(): - gene_constraint=[lambda x: x[0]>=1,lambda x: x[1]>=1,lambda x: x[2]>=1,lambda x: x[3]>=1,lambda x: x[4]>=1] + gene_constraint=[lambda x,v: [val for val in v if val>=1], + lambda x,v: [val for val in v if val>=1], + lambda x,v: [val for val in v if val>=1], + lambda x,v: [val for val in v if val>=1], + lambda x,v: [val for val in v if val>=1]] + ga_instance = population_gene_constraint(gene_constraint=gene_constraint, + init_range_low=1, + init_range_high=2, + gene_type=[float, 1], + num_genes=5, + crossover_type=None, + mutation_by_replacement=False, + allow_duplicate_genes=False) + + num_duplicates = 0 + for idx, solution in enumerate(ga_instance.solutions): + num = len(solution) - len(set(solution)) + if num != 0: + print(solution, idx) + num_duplicates += num + + assert num_duplicates == 0 + + initial_population = ga_instance.initial_population + # print(initial_population) + + assert numpy.all(initial_population[:, 0] >= 1), "Not all values in column 0 >= 1" + assert numpy.all(initial_population[:, 1] >= 1), "Not all values in column 1 >= 1" + assert numpy.all(initial_population[:, 2] >= 1), "Not all values in column 2 >= 1" + assert numpy.all(initial_population[:, 3] >= 1), "Not all values in column 3 >= 1" + assert numpy.all(initial_population[:, 4] >= 1), "Not all values in column 4 >= 1" + print("All constraints are met") + +def test_initial_population_float_by_replacement_no_duplicates_None_constraints(): + gene_constraint=[lambda x,v: [val for val in v if val>=1], + None, + lambda x,v: [val for val in v if val>=1], + None, + lambda x,v: [val for val in v if val>=1]] ga_instance = population_gene_constraint(gene_constraint=gene_constraint, init_range_low=1, init_range_high=2, @@ -207,6 +265,7 @@ def test_initial_population_float_by_replacement_no_duplicates2(): assert numpy.all(initial_population[:, 2] >= 1), "Not all values in column 2 >= 1" assert numpy.all(initial_population[:, 3] >= 1), "Not all values in column 3 >= 1" assert numpy.all(initial_population[:, 4] >= 1), "Not all values in column 4 >= 1" + print("All constraints are met") if __name__ == "__main__": #### Single-objective @@ -221,3 +280,5 @@ def test_initial_population_float_by_replacement_no_duplicates2(): print() test_initial_population_float_by_replacement_no_duplicates2() print() + test_initial_population_float_by_replacement_no_duplicates_None_constraints() + print()