diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 73b3f205..5892ed6e 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,7 +1,7 @@ # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -PayPal: # Replace with a single Patreon username +# paypal: http://paypal.me/ahmedfgad # Replace with a single Patreon username open_collective: pygad ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel @@ -9,4 +9,4 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username -custom: http://paypal.me/ahmedfgad # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] +custom: ['https://donate.stripe.com/eVa5kO866elKgM0144', 'http://paypal.me/ahmedfgad'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/main_py310.yml b/.github/workflows/main_py310.yml new file mode 100644 index 00000000..9602f17c --- /dev/null +++ b/.github/workflows/main_py310.yml @@ -0,0 +1,42 @@ +name: PyGAD PyTest / Python 3.10 + +on: + push: + branches: + - github-actions + # - master + +jobs: + job_id_1: + runs-on: ubuntu-latest + name: PyTest Workflow Job + + steps: + - name: Checkout Pre-Built Action + uses: actions/checkout@v3 + + - name: Setup Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Build PyGAD from the Repository + run: | + python3 -m pip install --upgrade build + python3 -m build + + - name: Install PyGAD after Building the .whl File + run: | + find ./dist/*.whl | xargs pip install + + - name: Install PyTest + run: pip install pytest + + - name: Run the Tests by Calling PyTest + run: | + pytest diff --git a/.github/workflows/main_py311.yml b/.github/workflows/main_py311.yml new file mode 100644 index 00000000..d4682438 --- /dev/null +++ b/.github/workflows/main_py311.yml @@ -0,0 +1,42 @@ +name: PyGAD PyTest / Python 3.11 + +on: + push: + branches: + - github-actions + # - master + +jobs: + job_id_1: + runs-on: ubuntu-latest + name: PyTest Workflow Job + + steps: + - name: Checkout Pre-Built Action + uses: actions/checkout@v3 + + - name: Setup Python 3.11 + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Build PyGAD from the Repository + run: | + python3 -m pip install --upgrade build + python3 -m build + + - name: Install PyGAD after Building the .whl File + run: | + find ./dist/*.whl | xargs pip install + + - name: Install PyTest + run: pip install pytest + + - name: Run the Tests by Calling PyTest + run: | + pytest diff --git a/.github/workflows/main_py312.yml b/.github/workflows/main_py312.yml new file mode 100644 index 00000000..87bd6480 --- /dev/null +++ b/.github/workflows/main_py312.yml @@ -0,0 +1,50 @@ +name: PyGAD PyTest / Python 3.12 + +# Cannot install packages in Python 3.12. +# The reason is that we use pip for installing packages. +# pip uses setuptools for the installation. +# setuptools depends on distutils. +# But Python 3.12 does not support distutils. +# Let's wait until setuptools changes its dependencies. + +# on: +# push: +# branches: + # - github-actions + # - master +on: workflow_dispatch + +jobs: + job_id_1: + runs-on: ubuntu-latest + name: PyTest Workflow Job + + steps: + - name: Checkout Pre-Built Action + uses: actions/checkout@v3 + + - name: Setup Python 3.12 + uses: actions/setup-python@v4 + with: + python-version: '3.12.0-beta.2' + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Build PyGAD from the Repository + run: | + python3 -m pip install --upgrade build + python3 -m build + + - name: Install PyGAD after Building the .whl File + run: | + find ./dist/*.whl | xargs pip install + + - name: Install PyTest + run: pip install pytest + + - name: Run the Tests by Calling PyTest + run: | + pytest diff --git a/.github/workflows/main_py37.yml b/.github/workflows/main_py37.yml new file mode 100644 index 00000000..037086e8 --- /dev/null +++ b/.github/workflows/main_py37.yml @@ -0,0 +1,42 @@ +name: PyGAD PyTest / Python 3.7 + +on: + push: + branches: + - github-actions + # - master + +jobs: + job_id_1: + runs-on: ubuntu-20.04 + name: PyTest Workflow Job + + steps: + - name: Checkout Pre-Built Action + uses: actions/checkout@v3 + + - name: Setup Python 3.7 + uses: actions/setup-python@v4 + with: + python-version: '3.7' + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Build PyGAD from the Repository + run: | + python3 -m pip install --upgrade build + python3 -m build + + - name: Install PyGAD after Building the .whl File + run: | + find ./dist/*.whl | xargs pip install + + - name: Install PyTest + run: pip install pytest + + - name: Run the Tests by Calling PyTest + run: | + pytest diff --git a/.github/workflows/main_py38.yml b/.github/workflows/main_py38.yml new file mode 100644 index 00000000..602f9176 --- /dev/null +++ b/.github/workflows/main_py38.yml @@ -0,0 +1,42 @@ +name: PyGAD PyTest / Python 3.8 + +on: + push: + branches: + - github-actions + # - master + +jobs: + job_id_1: + runs-on: ubuntu-latest + name: PyTest Workflow Job + + steps: + - name: Checkout Pre-Built Action + uses: actions/checkout@v3 + + - name: Setup Python 3.8 + uses: actions/setup-python@v4 + with: + python-version: '3.8' + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Build PyGAD from the Repository + run: | + python3 -m pip install --upgrade build + python3 -m build + + - name: Install PyGAD after Building the .whl File + run: | + find ./dist/*.whl | xargs pip install + + - name: Install PyTest + run: pip install pytest + + - name: Run the Tests by Calling PyTest + run: | + pytest diff --git a/.github/workflows/main_py39.yml b/.github/workflows/main_py39.yml new file mode 100644 index 00000000..c6b61fc4 --- /dev/null +++ b/.github/workflows/main_py39.yml @@ -0,0 +1,42 @@ +name: PyGAD PyTest / Python 3.9 + +on: + push: + branches: + - github-actions + # - master + +jobs: + job_id_1: + runs-on: ubuntu-latest + name: PyTest Workflow Job + + steps: + - name: Checkout Pre-Built Action + uses: actions/checkout@v3 + + - name: Setup Python 3.9 + uses: actions/setup-python@v4 + with: + python-version: '3.9' + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Build PyGAD from the Repository + run: | + python3 -m pip install --upgrade build + python3 -m build + + - name: Install PyGAD after Building the .whl File + run: | + find ./dist/*.whl | xargs pip install + + - name: Install PyTest + run: pip install pytest + + - name: Run the Tests by Calling PyTest + run: | + pytest diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 00000000..30d789a6 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,73 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '42 5 * * 2' + push: + branches: [ "master" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + # Uncomment the permissions below if installing in a private repository. + # contents: read + # actions: read + + steps: + - name: "Checkout code" + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecard on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@97a0fba1372883ab732affbe8f94b823f91727db # v3.pre.node20 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard (optional). + # Commenting out will disable upload of results to your repo's Code Scanning dashboard + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..73f9a191 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,35 @@ +# Read the Docs configuration file for Sphinx projects +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.12" + # You can also specify other tool versions: + # nodejs: "20" + # rust: "1.70" + # golang: "1.20" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/source/conf.py + # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs + # builder: "dirhtml" + # Fail on all warnings to avoid broken references + # fail_on_warning: true + +# Optionally build your docs in additional formats such as PDF and ePub +formats: + - pdf + - epub + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +# python: +# install: +# - requirements: docs/requirements.txt \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..e4a79caf --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +Copyright GeneticAlgorithmPython Contributors + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index b7753f7e..08bd7e41 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,14 @@ # 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](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. Check documentation of the [PyGAD](https://pygad.readthedocs.io/en/latest). -[![Downloads](https://pepy.tech/badge/pygad)](https://pepy.tech/project/pygad) ![Docs](https://readthedocs.org/projects/pygad/badge) +[![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://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) @@ -14,13 +18,14 @@ The library is under active development and more features are added regularly. I # Donation -You can donate via [Open Collective](https://opencollective.com/pygad): [opencollective.com/pygad](https://opencollective.com/pygad). - -To donate using PayPal, use either this link: [paypal.me/ahmedfgad](https://paypal.me/ahmedfgad) or the e-mail address ahmed.f.gad@gmail.com. +* [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 # 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), 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. Install PyGAD with the following command: @@ -28,8 +33,6 @@ Install PyGAD with the following command: pip install pygad ``` -PyGAD is developed in Python 3.7.3 and depends on NumPy for creating and manipulating arrays and Matplotlib for creating figures. The exact NumPy version used in developing PyGAD is 1.16.4. For Matplotlib, the version is 3.1.0. - To get started with PyGAD, please read the documentation at [Read The Docs](https://pygad.readthedocs.io/) https://pygad.readthedocs.io. # PyGAD Source Code @@ -64,7 +67,7 @@ Please check the **Contact Us** section for more contact details. 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`. -![PyGAD Lifecycle](https://user-images.githubusercontent.com/16560492/89446279-9c6f8380-d754-11ea-83fd-a60ea2f53b85.jpg) +![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. @@ -75,7 +78,7 @@ import numpy function_inputs = [4,-2,3.5,5,-11,-4.7] desired_output = 44 -def fitness_func(solution, solution_idx): +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 @@ -147,7 +150,7 @@ on_stop() # Example -Check the [PyGAD's documentation](https://pygad.readthedocs.io/en/latest/README_pygad_ReadTheDocs.html) for information about the implementation of this example. +Check the [PyGAD's documentation](https://pygad.readthedocs.io/en/latest/pygad.html) for information about the implementation of this example. It solves a single-objective problem. ```python import pygad @@ -163,7 +166,7 @@ What are the best values for the 6 weights (w1 to w6)? We are going to use the g function_inputs = [4,-2,3.5,5,-11,-4.7] # Function inputs. desired_output = 44 # Function output. -def fitness_func(solution, solution_idx): +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. output = numpy.sum(solution*function_inputs) @@ -184,9 +187,9 @@ num_genes = len(function_inputs) last_fitness = 0 def callback_generation(ga_instance): global last_fitness - print("Generation = {generation}".format(generation=ga_instance.generations_completed)) - print("Fitness = {fitness}".format(fitness=ga_instance.best_solution()[1])) - print("Change = {change}".format(change=ga_instance.best_solution()[1] - last_fitness)) + 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] # Creating an instance of the GA class inside the ga module. Some parameters are initialized within the constructor. @@ -205,15 +208,15 @@ ga_instance.plot_fitness() # Returning the details of the best solution. 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)) -print("Index of the best solution : {solution_idx}".format(solution_idx=solution_idx)) +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("Predicted output based on the best solution : {prediction}".format(prediction=prediction)) +print(f"Predicted output based on the best solution : {prediction}") if ga_instance.best_solution_generation != -1: - print("Best fitness value reached after {best_solution_generation} generations.".format(best_solution_generation=ga_instance.best_solution_generation)) + 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. @@ -245,8 +248,8 @@ To start with coding the genetic algorithm, you can check the tutorial titled [* 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) +* [Towards Data Science](https://towardsdatascience.com/introduction-to-optimization-with-genetic-algorithm-2f5001d9964b) +* [KDnuggets](https://www.kdnuggets.com/2018/03/introduction-optimization-with-genetic-algorithm.html) [![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) @@ -312,13 +315,13 @@ Find the book at these links: If you used PyGAD, please consider adding a citation to the following paper about PyGAD: ``` -@misc{gad2021pygad, - title={PyGAD: An Intuitive Genetic Algorithm Python Library}, - author={Ahmed Fawzy Gad}, - year={2021}, - eprint={2106.06158}, - archivePrefix={arXiv}, - primaryClass={cs.NE} +@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} } ``` @@ -330,4 +333,3 @@ If you used PyGAD, please consider adding a citation to the following paper abou * [KDnuggets](https://kdnuggets.com/author/ahmed-gad) * [TowardsDataScience](https://towardsdatascience.com/@ahmedfgad) * [GitHub](https://github.com/ahmedfgad) - diff --git a/Tutorial Project/README.md b/Tutorial Project/README.md index e9e32eb8..3b2f2fb0 100644 --- a/Tutorial Project/README.md +++ b/Tutorial Project/README.md @@ -1,24 +1,11 @@ # GeneticAlgorithmPython -Genetic algorithm implementation in Python - -This folder under the project has the code built in the tutorial titled [**Genetic Algorithm Implementation in Python**](https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad) which is available in these links: +This folder has the code for the tutorial titled [**Genetic Algorithm Implementation in Python**](https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad) which is available at these links: * https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad * https://towardsdatascience.com/genetic-algorithm-implementation-in-python-5ab67bb124a6 * https://www.kdnuggets.com/2018/07/genetic-algorithm-implementation-python.html -The `ga.py` file holds the implementation of the GA operations such as mutation and crossover. The other file gives an example of using the GA.py file. - -It is important to note that this project does not implement everything in GA and there are a wide number of variations to be applied. For example, this project uses decimal representation for the chromosome and the binary representations might be preferred for other problems. - -## For Contacting the Author +The `ga.py` file has the implementation of the GA operations such as mutation and crossover. It is a primitive implementation of the genetic algorithm. The other file gives an example of using the ga.py file. -* E-mail: ahmed.f.gad@gmail.com -* [LinkedIn](https://www.linkedin.com/in/ahmedfgad) -* [Amazon Author Page](https://amazon.com/author/ahmedgad) -* [Paperspace](https://blog.paperspace.com/author/ahmed) -* [Hearbeat](https://heartbeat.fritz.ai/@ahmedfgad) -* [KDnuggets](https://kdnuggets.com/author/ahmed-gad) -* [TowardsDataScience](https://towardsdatascience.com/@ahmedfgad) -* [GitHub](https://github.com/ahmedfgad) +It is important to note that this project does not implement everything in GA and there are a wide number of variations to be applied. For example, this project uses decimal representation for the chromosome and the binary representations might be preferred for other problems. Check [PyGAD](https://pygad.readthedocs.io/en) for extensive features. diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..d0c3cbf1 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..6247f7e2 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/README_pygad_ReadTheDocs.rst b/docs/source/README_pygad_ReadTheDocs.rst deleted file mode 100644 index 4de86ec5..00000000 --- a/docs/source/README_pygad_ReadTheDocs.rst +++ /dev/null @@ -1,3301 +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. - -.. _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 that must accept 2 parameters (a - single solution and its index in the population) and return the - fitness value of the solution. Available starting from `PyGAD - 1.0.17 `__ - until - `1.0.20 `__ - with a single parameter representing the solution. Changed in `PyGAD - 2.0.0 `__ - and higher to include a second parameter representing the solution - index. Check the **Preparing the ``fitness_func`` Parameter** section - for information about creating such a function. - -- ``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``. - -- ``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``, ``tuple``, ``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"``. - -- ``on_start=None``: Accepts a function to be called only once before - the genetic algorithm starts its evolution. This function must accept - a single parameter representing the instance of the genetic - algorithm. Added in `PyGAD - 2.6.0 `__. - -- ``on_fitness=None``: Accepts a function to be called after - calculating the fitness values of all solutions in the population. - This function must accept 2 parameters: the first one represents the - instance of the genetic algorithm and the second one is a list of all - solutions' fitness values. Added in `PyGAD - 2.6.0 `__. - -- ``on_parents=None``: Accepts a function to be called after selecting - the parents that mates. This function must accept 2 parameters: the - first one represents the instance of the genetic algorithm and the - second one represents the selected parents. 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 `__. - -- ``callback_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. Supported in - `PyGAD - 2.0.0 `__ - and higher. In `PyGAD - 2.4.0 `__, - if this function returned the string ``stop``, then the ``run()`` - method stops at the current generation without completing the - remaining generations. Check the **Release History** section of the - documentation for an example. Starting from `PyGAD - 2.6.0 `__, - the ``callback_generation`` parameter is deprecated and should be - replaced by the ``on_generation`` parameter. The - ``callback_generation`` parameter will be removed in a later version. - -- ``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 `__. - -- ``delay_after_gen=0.0``: It 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. Available in `PyGAD - 2.4.0 `__ - and higher. - -- ``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. - -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 within the constructor. If at least a -parameter is not correct, an exception is thrown. - -.. _plotting-methods-in-pygadga-class: - -Plotting Methods in ``pygad.GA`` Class --------------------------------------- - -- ``plot_fitness()``: Shows how the fitness evolves by generation. - -- ``plot_genes()``: Shows how the gene value changes for each - generation. - -- ``plot_new_solution_rate()``: Shows the number of new solutions - explored in each solution. - -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. - -Other Attributes -~~~~~~~~~~~~~~~~ - -- ``generations_completed``: Holds the number of the last completed - generation. - -- ``population``: A NumPy array holding the initial population. - -- ``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 `__. - -- ``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 `__. - -Note that the attributes with its name start 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. - -- ``solve_duplicate_genes_randomly``: Solves the duplicates in a - solution by randomly selecting new values for the duplicating genes. - -- ``solve_duplicate_genes_by_space``: Solves the duplicates in a - solution by selecting values for the duplicating genes from the gene - space - -- ``unique_int_gene_from_range``: Finds a unique integer value for the - gene. - -- ``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. - -- ``unique_gene_by_space``: Returns a unique gene value for a single - gene based on its value space to solve the duplicates. - -The next sections discuss the methods available in the **pygad.GA** -class. - -.. _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()`` ---------------------- - -Calculating the fitness values of all solutions in the current -population. - -It works by iterating through the solutions and calling the function -assigned to the ``fitness_func`` parameter in the **pygad.GA** class -constructor for each solution. - -It returns an array 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 - ``callback_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``. - -Parent Selection Methods ------------------------- - -The **pygad.GA** class has several methods for selecting the parents -that will mate to produce the offspring. All of such methods accept the -same parameters which are: - -- ``fitness``: The fitness values of the solutions in the current - population. - -- ``num_parents``: The number of parents to be selected. - -All of such methods return an array of the selected parents. - -The next subsections list the supported methods for parent selection. - -.. _steadystateselection: - -``steady_state_selection()`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Selects the parents using the steady-state selection technique. - -.. _rankselection: - -``rank_selection()`` -~~~~~~~~~~~~~~~~~~~~ - -Selects the parents using the rank selection technique. - -.. _randomselection: - -``random_selection()`` -~~~~~~~~~~~~~~~~~~~~~~ - -Selects the parents randomly. - -.. _tournamentselection: - -``tournament_selection()`` -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Selects the parents using the tournament selection technique. - -.. _roulettewheelselection: - -``roulette_wheel_selection()`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Selects the parents using the roulette wheel selection technique. - -.. _stochasticuniversalselection: - -``stochastic_universal_selection()`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Selects the parents using the stochastic universal selection technique. - -Crossover Methods ------------------ - -The **pygad.GA** class supports several methods for applying crossover -between the selected parents. All of these methods accept the same -parameters which are: - -- ``parents``: The parents to mate for producing the offspring. - -- ``offspring_size``: The size of the offspring to produce. - -All of such methods return an array of the produced offspring. - -The next subsections list the supported methods for crossover. - -.. _singlepointcrossover: - -``single_point_crossover()`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Applies the single-point crossover. It selects a point randomly at which -crossover takes place between the pairs of parents. - -.. _twopointscrossover: - -``two_points_crossover()`` -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Applies the 2 points crossover. It selects the 2 points randomly at -which crossover takes place between the pairs of parents. - -.. _uniformcrossover: - -``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. - -.. _scatteredcrossover: - -``scattered_crossover()`` -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Applies the scattered crossover. It randomly selects the gene from one -of the 2 parents. - -Mutation Methods ----------------- - -The **pygad.GA** class supports several methods for applying mutation. -All of these methods accept the same parameter which is: - -- ``offspring``: The offspring to mutate. - -All of such methods return an array of the mutated offspring. - -The next subsections list the supported methods for mutation. - -.. _randommutation: - -``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. - -.. _swapmutation: - -``swap_mutation()`` -~~~~~~~~~~~~~~~~~~~ - -Applies the swap mutation which interchanges the values of 2 randomly -selected genes. - -.. _inversionmutation: - -``inversion_mutation()`` -~~~~~~~~~~~~~~~~~~~~~~~~ - -Applies the inversion mutation which selects a subset of genes and -inverts them. - -.. _scramblemutation: - -``scramble_mutation()`` -~~~~~~~~~~~~~~~~~~~~~~~ - -Applies the scramble mutation which selects a subset of genes and -shuffles their order randomly. - -.. _adaptivemutation: - -``adaptive_mutation()`` -~~~~~~~~~~~~~~~~~~~~~~~ - -Applies the adaptive mutation which selects a subset of genes and -shuffles their order randomly. - -.. _bestsolution: - -``best_solution()`` -------------------- - -Returns information about the best solution found by the genetic -algorithm. - -It accepts the following parameters: - -- ``pop_fitness=None``: An optional parameter that 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. - -It returns the following: - -- ``best_solution``: Best solution in the current population. - -- ``best_solution_fitness``: Fitness value of the best solution. - -- ``best_match_idx``: Index of the best solution in the current - population. - -.. _plotfitness-1: - -``plot_fitness()`` ------------------- - -Previously named ``plot_result()``, this method creates, shows, and -returns a figure that summarizes how the fitness value evolves by -generation. It works only after completing at least 1 generation. - -If no generation is completed (at least 1), an exception is raised. - -Starting from `PyGAD -2.15.0 `__ -and higher, 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 ``"#3870FF"``. - -8. ``save_dir``: Directory to save the figure. - -.. _plotnewsolutionrate-1: - -``plot_new_solution_rate()`` ----------------------------- - -The ``plot_new_solution_rate()`` method creates, shows, and returns a -figure that shows the number of new solutions explored in each -generation. This method works only when ``save_solutions=True`` in the -constructor of the ``pygad.GA`` class. It also 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 ``"#3870FF"``. - -8. ``save_dir``: Directory to save the figure. - -.. _plotgenes-1: - -``plot_genes()`` ----------------- - -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. - -This is controlled by the ``graph_type`` parameter. - -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. - -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. . - -``save()`` ----------- - -Saves the genetic algorithm instance - -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 there are some steps in the genetic algorithm pipeline that 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. - -On **``15 April 2020``**, a new argument named ``fitness_func`` is added -to PyGAD 1.0.17 that allows the user to specify a custom function to be -used as a fitness function. This function must be a **maximization -function** so that a solution with a high fitness value returned is -selected compared to a solution with a low value. Doing that allows the -user to freely use PyGAD to solve any problem by passing the appropriate -fitness function. It is very important to understand the problem well -for creating this function. - -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 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(solution, solution_idx): - output = numpy.sum(solution*function_inputs) - fitness = 1.0 / numpy.abs(output - desired_output) - return fitness - -Such a user-defined function must accept 2 parameters: - -1. 1D vector representing a single solution. Introduced in `PyGAD - 1.0.17 `__. - -2. Solution index within the population. Introduced in `PyGAD - 2.0.0 `__ - and higher. - -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 almost did an awesome 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-callbackgeneration-parameter: - -The ``callback_generation`` Parameter -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This parameter should be replaced by ``on_generation``. The -``callback_generation`` parameter will be removed in a later release of -PyGAD. - -In `PyGAD -2.0.0 `__ -and higher, an optional parameter named ``callback_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 callback_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 -``callback_generation`` parameter of the GA class constructor. By doing -that, the ``callback_gen()`` function will be called after each -generation. - -.. code:: python - - ga_instance = pygad.GA(..., - callback_generation=callback_gen, - ...) - -After the parameters are prepared, we can import PyGAD and build an -instance of the **pygad.GA** class. - -Import the ``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-1: - -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() - -.. figure:: https://user-images.githubusercontent.com/16560492/78830005-93111d00-79e7-11ea-9d8e-a8d8325a6101.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. - -- 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("Parameters of the best solution : {solution}".format(solution=solution)) - print("Fitness value of the best solution = {solution_fitness}".format(solution_fitness=solution_fitness)) - print("Index of the best solution : {solution_idx}".format(solution_idx=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("Best fitness value reached after {best_solution_generation} generations.".format(best_solution_generation=ga_instance.best_solution_generation)) - -.. _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()) - -Crossover, Mutation, and Parent Selection -========================================= - -PyGAD supports different types for selecting the parents and applying -the crossover & mutation operators. More features will be added in the -future. To ask for a new feature, please check the **Ask for Feature** -section. - -Supported Crossover Operations ------------------------------- - -The supported crossover operations at this time 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. - -Supported Mutation Operations ------------------------------ - -The supported mutation operations at this time 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. - -Supported Parent Selection Operations -------------------------------------- - -The supported parent selection techniques at this time 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. - -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``. - -.. figure:: https://user-images.githubusercontent.com/16560492/89446279-9c6f8380-d754-11ea-83fd-a60ea2f53b85.jpg - :alt: - -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(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() - -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. - -.. figure:: https://user-images.githubusercontent.com/16560492/103468973-e3c26600-4d2c-11eb-8af3-b3bb39b50540.jpg - :alt: - -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 low-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(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) - -Limit the Gene Value Range -========================== - -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}] - -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 ``callback_generation``. When this callback function is -implemented and assigned to the ``callback_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(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("Number of generations passed is {generations_completed}".format(generations_completed=ga_instance.generations_completed)) - -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(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, - 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(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, - 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. - -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.** - -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(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[0])) - - 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[0])) - - 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. - -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, 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(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, 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[0])) - - 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() - -.. _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} - -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. - -.. _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(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 ``numpy.float`` data type. In this case, all genes are of type -``numpy.float`` and their maximum precision is 3. - -.. code:: python - - gene_type=[numpy.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(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, numpy.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(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, numpy.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, [numpy.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(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, [numpy.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]] - -Visualization in PyGAD -====================== - -This section discusses the different options to visualize the results in -PyGAD through these methods: - -1. ``plot_fitness()`` - -2. ``plot_genes()`` - -3. ``plot_new_solution_rate()`` - -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(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. - -.. _plotfitness-2: - -``plot_fitness()`` ------------------- - -The ``plot_fitness()`` method shows the fitness value for each -generation. - -.. _plottypeplot-1: - -``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") - -.. figure:: https://user-images.githubusercontent.com/16560492/122472609-d02f5280-cf8e-11eb-88a7-f9366ff6e7c6.png - :alt: - -.. _plottypescatter-1: - -``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") - -.. figure:: https://user-images.githubusercontent.com/16560492/122473159-75e2c180-cf8f-11eb-942d-31279b286dbd.png - :alt: - -.. _plottypebar-1: - -``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") - -.. figure:: https://user-images.githubusercontent.com/16560492/122473340-b7736c80-cf8f-11eb-89c5-4f7db3b653cc.png - :alt: - -.. _plotnewsolutionrate-2: - -``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 accepts the same parameters as -in the ``plot_fitness()`` method with 3 possible values for -``plot_type`` parameter. - -.. _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. - -.. figure:: https://user-images.githubusercontent.com/16560492/122475815-3322e880-cf93-11eb-9648-bf66f823234b.png - :alt: - -.. _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") - -.. figure:: https://user-images.githubusercontent.com/16560492/122476108-adec0380-cf93-11eb-80ac-7588bf90492f.png - :alt: - -.. _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") - -.. figure:: https://user-images.githubusercontent.com/16560492/122476173-c2c89700-cf93-11eb-9e77-d39737cd3a96.png - :alt: - -.. _plotgenes-2: - -``plot_genes()`` ----------------- - -The ``plot_genes()`` method is the third option to visualize the PyGAD -results. 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. - -.. _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") - -.. figure:: https://user-images.githubusercontent.com/16560492/122477158-4a62d580-cf95-11eb-8c93-9b6e74cb814c.png - :alt: - -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') - -.. figure:: https://user-images.githubusercontent.com/16560492/122477273-73836600-cf95-11eb-828f-f357c7b0f815.png - :alt: - -.. _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') - -.. figure:: https://user-images.githubusercontent.com/16560492/122477370-99106f80-cf95-11eb-8643-865b55e6b844.png - :alt: - -.. _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') - -.. figure:: https://user-images.githubusercontent.com/16560492/122479260-beeb4380-cf98-11eb-8f08-23707929b12c.png - :alt: - -.. _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') - -.. figure:: https://user-images.githubusercontent.com/16560492/122477314-8007be80-cf95-11eb-9c95-da3f49204151.png - :alt: - -All the previous figures can be created for only the best solutions by -setting ``solutions="best"``. - -Parallel Processing in PyGAD -============================ - -Some time was spent on doing some experiments to use parallel processing -with PyGAD. From all operations in the genetic algorithm, the 2 -operations that can be parallelized are: - -1. Fitness value calculation - -2. Mutation - -The reason is that these 2 operations are independent and can be -distributed across different processes or threads. Unfortunately, all -experiments proved that parallel processing does not reduce the time -compared to regular processing. Most of the time, parallel processing -increased the time. The best case was that parallel processing gave a -close time to normal processing. - -The interpretation of that is that the genetic algorithm operations like -mutation does not take much CPU processing time. Thus, using parallel -processing adds more time to manage the processes/threads which -increases the overall time. - -But there still a chance that parallel processing is efficient with the -genetic algorithm. This is in case the fitness function makes intensive -processing and takes much processing time from the CPU. In this case, -parallelizing the fitness function would help you cut down the overall -time. - -To know about how to parallelize the fitness function with PyGAD, please -check `this -tutorial `__ -by `László -Fazekas `__: -`How Genetic Algorithms Can Compete with Gradient Descent and -Backprop `__ - -Examples -======== - -This section gives the complete code of some examples that use -``pygad``. Each subsection builds a different example. - -Linear Model Optimization -------------------------- - -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(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("Generation = {generation}".format(generation=ga_instance.generations_completed)) - print("Fitness = {fitness}".format(fitness=ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1])) - print("Change = {change}".format(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("Parameters of the best solution : {solution}".format(solution=solution)) - print("Fitness value of the best solution = {solution_fitness}".format(solution_fitness=solution_fitness)) - print("Index of the best solution : {solution_idx}".format(solution_idx=solution_idx)) - - prediction = numpy.sum(numpy.array(function_inputs)*solution) - print("Predicted output based on the best solution : {prediction}".format(prediction=prediction)) - - if ga_instance.best_solution_generation != -1: - print("Best fitness value reached after {best_solution_generation} generations.".format(best_solution_generation=ga_instance.best_solution_generation)) - - # 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() - -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=numpy.float) - -Here is the read image. - -.. figure:: https://user-images.githubusercontent.com/16560492/36948808-f0ac882e-1fe8-11e8-8d07-1307e3477fd0.jpg - :alt: - -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 2 parameters -representing 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(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(a=img_arr, newshape=(functools.reduce(operator.mul, img_arr.shape))) - - def chromosome2img(vector, shape): - if len(vector) != functools.reduce(operator.mul, shape): - raise ValueError("A vector of length {vector_length} into an array of shape {shape}.".format(vector_length=len(vector), shape=shape)) - - return numpy.reshape(a=vector, newshape=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. - -.. figure:: https://user-images.githubusercontent.com/16560492/82232124-77762c00-992e-11ea-9fc6-14a1cd7a04ff.png - :alt: - -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("Fitness value of the best solution = {solution_fitness}".format(solution_fitness=solution_fitness)) - print("Index of the best solution : {solution_idx}".format(solution_idx=solution_idx)) - - if ga_instance.best_solution_generation != -1: - print("Best fitness value reached after {best_solution_generation} generations.".format(best_solution_generation=ga_instance.best_solution_generation)) - - 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. - -.. figure:: https://user-images.githubusercontent.com/16560492/82232405-e0f63a80-992e-11ea-984f-b6ed76465bd1.png - :alt: - -After more generations, the result can be enhanced like what shown -below. - -.. figure:: https://user-images.githubusercontent.com/16560492/82232345-cf149780-992e-11ea-8390-bf1a57a19de7.png - :alt: - -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** - -.. figure:: https://user-images.githubusercontent.com/16560492/36948589-b47276f0-1fe5-11e8-8efe-0cd1a225ea3a.png - :alt: - -**Generation 1,000** - -.. figure:: https://user-images.githubusercontent.com/16560492/36948823-16f490ee-1fe9-11e8-97db-3e8905ad5440.png - :alt: - -**Generation 2,500** - -.. figure:: https://user-images.githubusercontent.com/16560492/36948832-3f314b60-1fe9-11e8-8f4a-4d9a53b99f3d.png - :alt: - -**Generation 4,500** - -.. figure:: https://user-images.githubusercontent.com/16560492/36948837-53d1849a-1fe9-11e8-9b36-e9e9291e347b.png - :alt: - -**Generation 7,000** - -.. figure:: https://user-images.githubusercontent.com/16560492/36948852-66f1b176-1fe9-11e8-9f9b-460804e94004.png - :alt: - -**Generation 8,000** - -.. figure:: https://user-images.githubusercontent.com/16560492/36948865-7fbb5158-1fe9-11e8-8c04-8ac3c1f7b1b1.png - :alt: - -**Generation 20,000** - -.. figure:: https://user-images.githubusercontent.com/16560492/82232405-e0f63a80-992e-11ea-984f-b6ed76465bd1.png - :alt: - -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. diff --git a/docs/source/README_pygad_cnn_ReadTheDocs.rst b/docs/source/cnn.rst similarity index 94% rename from docs/source/README_pygad_cnn_ReadTheDocs.rst rename to docs/source/cnn.rst index 6dc90507..ce2bfe8c 100644 --- a/docs/source/README_pygad_cnn_ReadTheDocs.rst +++ b/docs/source/cnn.rst @@ -1,748 +1,748 @@ -.. _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("Number of correct classifications : {num_correct}.".format(num_correct=num_correct)) - print("Number of wrong classifications : {num_wrong}.".format(num_wrong=num_wrong.size)) - print("Classification accuracy : {accuracy}.".format(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("Number of correct classifications : {num_correct}.".format(num_correct=num_correct)) - print("Number of wrong classifications : {num_wrong}.".format(num_wrong=num_wrong.size)) - print("Classification accuracy : {accuracy}.".format(accuracy=accuracy)) +.. _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 1749a5ae..3ae40c99 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -18,11 +18,11 @@ # -- Project information ----------------------------------------------------- project = 'PyGAD' -copyright = '2020, Ahmed Fawzy Gad' +copyright = '2025, Ahmed Fawzy Gad' author = 'Ahmed Fawzy Gad' # The full version, including alpha/beta/rc tags -release = '2.16.3' +release = '3.4.0' master_doc = 'index' @@ -31,9 +31,8 @@ 'inputenc': '', 'utf8extra': '', 'preamble': ''' - -\usepackage{kotex} -\usepackage{fontspec} +\\usepackage{kotex} +\\usepackage{fontspec} \setsansfont{Arial} \setromanfont{Arial} ''', diff --git a/docs/source/README_pygad_gacnn_ReadTheDocs.rst b/docs/source/gacnn.rst similarity index 89% rename from docs/source/README_pygad_gacnn_ReadTheDocs.rst rename to docs/source/gacnn.rst index ea3282dc..e9f89bae 100644 --- a/docs/source/README_pygad_gacnn_ReadTheDocs.rst +++ b/docs/source/gacnn.rst @@ -1,662 +1,662 @@ -.. _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(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("Generation = {generation}".format(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() - -.. figure:: 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("Parameters of the best solution : {solution}".format(solution=solution)) - print("Fitness value of the best solution = {solution_fitness}".format(solution_fitness=solution_fitness)) - print("Index of the best solution : {solution_idx}".format(solution_idx=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("Predictions of the trained network : {predictions}".format(predictions=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("Number of correct classifications : {num_correct}.".format(num_correct=num_correct)) - print("Number of wrong classifications : {num_wrong}.".format(num_wrong=num_wrong.size)) - print("Classification accuracy : {accuracy}.".format(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(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("Generation = {generation}".format(generation=ga_instance.generations_completed)) - print("Fitness = {fitness}".format(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("Parameters of the best solution : {solution}".format(solution=solution)) - print("Fitness value of the best solution = {solution_fitness}".format(solution_fitness=solution_fitness)) - print("Index of the best solution : {solution_idx}".format(solution_idx=solution_idx)) - - if ga_instance.best_solution_generation != -1: - print("Best fitness value reached after {best_solution_generation} generations.".format(best_solution_generation=ga_instance.best_solution_generation)) - - # Predicting the outputs of the data using the best solution. - predictions = GACNN_instance.population_networks[solution_idx].predict(data_inputs=data_inputs) - print("Predictions of the trained network : {predictions}".format(predictions=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("Number of correct classifications : {num_correct}.".format(num_correct=num_correct)) - print("Number of wrong classifications : {num_wrong}.".format(num_wrong=num_wrong.size)) - print("Classification accuracy : {accuracy}.".format(accuracy=accuracy)) +.. _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/source/README_pygad_gann_ReadTheDocs.rst b/docs/source/gann.rst similarity index 86% rename from docs/source/README_pygad_gann_ReadTheDocs.rst rename to docs/source/gann.rst index 6e87c4e1..c3c85b32 100644 --- a/docs/source/README_pygad_gann_ReadTheDocs.rst +++ b/docs/source/gann.rst @@ -1,1262 +1,1267 @@ -.. _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(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("Generation = {generation}".format(generation=ga_instance.generations_completed)) - print("Fitness = {fitness}".format(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() - -.. figure:: 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("Parameters of the best solution : {solution}".format(solution=solution)) - print("Fitness value of the best solution = {solution_fitness}".format(solution_fitness=solution_fitness)) - print("Index of the best solution : {solution_idx}".format(solution_idx=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("Best fitness value reached after {best_solution_generation} generations.".format(best_solution_generation=ga_instance.best_solution_generation)) - -.. 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("Predictions of the trained network : {predictions}".format(predictions=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("Number of correct classifications : {num_correct}.".format(num_correct=num_correct)) - print("Number of wrong classifications : {num_wrong}.".format(num_wrong=num_wrong.size)) - print("Classification accuracy : {accuracy}.".format(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(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("Generation = {generation}".format(generation=ga_instance.generations_completed)) - print("Fitness = {fitness}".format(fitness=ga_instance.best_solution()[1])) - print("Change = {change}".format(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 # 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 = -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) - - 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("Parameters of the best solution : {solution}".format(solution=solution)) - print("Fitness value of the best solution = {solution_fitness}".format(solution_fitness=solution_fitness)) - print("Index of the best solution : {solution_idx}".format(solution_idx=solution_idx)) - - if ga_instance.best_solution_generation != -1: - print("Best fitness value reached after {best_solution_generation} generations.".format(best_solution_generation=ga_instance.best_solution_generation)) - - # 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("Predictions of the trained network : {predictions}".format(predictions=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("Number of correct classifications : {num_correct}.".format(num_correct=num_correct)) - print("Number of wrong classifications : {num_wrong}.".format(num_wrong=num_wrong.size)) - print("Classification accuracy : {accuracy}.".format(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(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("Generation = {generation}".format(generation=ga_instance.generations_completed)) - print("Fitness = {fitness}".format(fitness=ga_instance.best_solution()[1])) - print("Change = {change}".format(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("Parameters of the best solution : {solution}".format(solution=solution)) - print("Fitness value of the best solution = {solution_fitness}".format(solution_fitness=solution_fitness)) - print("Index of the best solution : {solution_idx}".format(solution_idx=solution_idx)) - - if ga_instance.best_solution_generation != -1: - print("Best fitness value reached after {best_solution_generation} generations.".format(best_solution_generation=ga_instance.best_solution_generation)) - - # 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("Predictions of the trained network : {predictions}".format(predictions=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("Number of correct classifications : {num_correct}.".format(num_correct=num_correct)) - print("Number of wrong classifications : {num_wrong}.".format(num_wrong=num_wrong.size)) - print("Classification accuracy : {accuracy}.".format(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. - -.. figure:: 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(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(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("Generation = {generation}".format(generation=ga_instance.generations_completed)) - print("Fitness = {fitness}".format(fitness=ga_instance.best_solution()[1])) - print("Change = {change}".format(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([[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 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() - print("Parameters of the best solution : {solution}".format(solution=solution)) - print("Fitness value of the best solution = {solution_fitness}".format(solution_fitness=solution_fitness)) - print("Index of the best solution : {solution_idx}".format(solution_idx=solution_idx)) - - if ga_instance.best_solution_generation != -1: - print("Best fitness value reached after {best_solution_generation} generations.".format(best_solution_generation=ga_instance.best_solution_generation)) - - # 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("Predictions of the trained network : {predictions}".format(predictions=predictions)) - - # Calculating some statistics - abs_error = numpy.mean(numpy.abs(predictions - data_outputs)) - print("Absolute error : {abs_error}.".format(abs_error=abs_error)) - -The next figure shows how the fitness value changes for the generations -used. - -.. figure:: 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(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("Generation = {generation}".format(generation=ga_instance.generations_completed)) - print("Fitness = {fitness}".format(fitness=ga_instance.best_solution()[1])) - print("Change = {change}".format(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 - - 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) - - # 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() - print("Parameters of the best solution : {solution}".format(solution=solution)) - print("Fitness value of the best solution = {solution_fitness}".format(solution_fitness=solution_fitness)) - print("Index of the best solution : {solution_idx}".format(solution_idx=solution_idx)) - - if ga_instance.best_solution_generation != -1: - print("Best fitness value reached after {best_solution_generation} generations.".format(best_solution_generation=ga_instance.best_solution_generation)) - - # 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("Predictions of the trained network : {predictions}".format(predictions=predictions)) - - # Calculating some statistics - abs_error = numpy.mean(numpy.abs(predictions - data_outputs)) - print("Absolute error : {abs_error}.".format(abs_error=abs_error)) - -The next figure shows how the fitness value changes for the 500 -generations used. - -.. figure:: https://user-images.githubusercontent.com/16560492/92948486-bbe78380-f459-11ea-9e31-0d4c7269d606.png - :alt: +.. _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/source/helper.rst b/docs/source/helper.rst new file mode 100644 index 00000000..dddfaac3 --- /dev/null +++ b/docs/source/helper.rst @@ -0,0 +1,29 @@ +.. _pygadhelper-module: + +``pygad.helper`` Module +======================= + +This section of the PyGAD's library documentation discusses the +**pygad.helper** module. + +Yet, this module has a submodule called ``unique`` that 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. + +- ``solve_duplicate_genes_randomly()``: Solves the duplicates in a + solution by randomly selecting new values for the duplicating genes. + +- ``solve_duplicate_genes_by_space()``: Solves the duplicates in a + solution by selecting values for the duplicating genes from the gene + space + +- ``unique_int_gene_from_range()``: Finds a unique integer value for + the gene. + +- ``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. + +- ``unique_gene_by_space()``: Returns a unique gene value for a single + gene based on its value space to solve the duplicates. diff --git a/docs/source/index.rst b/docs/source/index.rst index 0a238f11..9b2513d1 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -19,13 +19,13 @@ optimizing machine learning algorithms. It works with 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. +by customizing the fitness function. It works with both single-objective +and multi-objective optimization problems. - -.. figure:: https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png +.. image:: https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png :alt: -*Logo designed by*\ `Asmaa +*Logo designed by* `Asmaa Kabil `__ Besides building the genetic algorithm, it builds and optimizes machine @@ -44,6 +44,9 @@ Donation & Support You can donate to PyGAD via: +- `Credit/Debit Card `__: + https://donate.stripe.com/eVa5kO866elKgM0144 + - `Open Collective `__: `opencollective.com/pygad `__ @@ -70,13 +73,6 @@ Install PyGAD with the following command: pip3 install pygad -PyGAD is developed in Python 3.7.3 and depends on NumPy for creating and -manipulating arrays and Matplotlib for creating figures. The exact NumPy -version used in developing PyGAD is 1.16.4. For Matplotlib, the version -is 3.1.0. - -.. _header-n69: - Quick Start =========== @@ -94,7 +90,7 @@ set of weights that satisfy the following function: .. code:: - 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 The first step is to prepare the inputs and the outputs of this @@ -108,9 +104,14 @@ equation. 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(solution, solution_idx): + def fitness_func(ga_instance, solution, solution_idx): output = numpy.sum(solution*function_inputs) fitness = 1.0 / numpy.abs(output - desired_output) return fitness @@ -186,75 +187,130 @@ solution found by PyGAD can be accessed. There is more to do using PyGAD. Read its documentation to explore the features of PyGAD. -.. _header-n88: - PyGAD's Modules =============== `PyGAD `__ has the following modules: -1. The main module has the same name as the library which is ``pygad`` - that builds the genetic algorithm. +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. -2. The ``nn`` module builds artificial neural networks. +4. The ``cnn`` module builds convolutional neural networks. -3. The ``gann`` module optimizes neural networks (for classification and - regression) using the genetic algorithm. +5. The ``gacnn`` module optimizes convolutional neural networks using + the genetic algorithm. -4. The ``cnn`` module builds convolutional neural networks. +6. The ``kerasga`` module to train `Keras `__ models + using the genetic algorithm. -5. The ``gacnn`` module optimizes convolutional neural networks using - the genetic algorithm. +7. The ``torchga`` module to train `PyTorch `__ + models using the genetic algorithm. -6. The ``kerasga`` module to train `Keras `__ models - using the genetic algorithm. +8. The ``visualize`` module to visualize the results. -7. The ``torchga`` module to train `PyTorch `__ - models using the genetic algorithm. +9. The ``utils`` module contains the operators (crossover, mutation, + and parent selection) and the NSGA-II code. -The documentation discusses each of these modules. +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:: - @misc{gad2021pygad, + @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} + } - title={PyGAD: An Intuitive Genetic Algorithm Python Library}, - author={Ahmed Fawzy Gad}, +.. _header-n4: - year={2021}, +pygad Module +=============== - eprint={2106.06158}, - archivePrefix={arXiv}, +.. toctree:: + :maxdepth: 4 + :caption: pygad Module TOC - primaryClass={cs.NE} + pygad.rst - } -.. _header-n4: +.. _header-n5: -pygad Module +More About pygad Module =============== .. toctree:: :maxdepth: 4 - :caption: pygad Module TOC + :caption: More About pygad Module TOC - README_pygad_ReadTheDocs.rst + pygad_more.rst -.. _header-n5: +.. _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 =============== @@ -264,13 +320,13 @@ pygad.nn Module :maxdepth: 4 :caption: pygad.nn Module TOC - README_pygad_nn_ReadTheDocs.rst + nn.rst -.. _header-n6: +.. _header-n10: pygad.gann Module ================= @@ -280,7 +336,7 @@ pygad.gann Module :maxdepth: 4 :caption: pygad.gann Module TOC - README_pygad_gann_ReadTheDocs.rst + gann.rst @@ -290,7 +346,7 @@ pygad.gann Module -.. _header-n7: +.. _header-n11: pygad.cnn Module ================= @@ -300,17 +356,11 @@ pygad.cnn Module :maxdepth: 4 :caption: pygad.cnn Module TOC - README_pygad_cnn_ReadTheDocs.rst - - - - + cnn.rst - - -.. _header-n8: +.. _header-n12: pygad.gacnn Module ================= @@ -320,12 +370,12 @@ pygad.gacnn Module :maxdepth: 4 :caption: pygad.gacnn Module TOC - README_pygad_gacnn_ReadTheDocs.rst + gacnn.rst -.. _header-n9: +.. _header-n13: pygad.kerasga Module ================= @@ -335,12 +385,12 @@ pygad.kerasga Module :maxdepth: 4 :caption: pygad.kerasga Module TOC - README_pygad_kerasga_ReadTheDocs.rst + kerasga.rst -.. _header-n10: +.. _header-n14: pygad.torchga Module ================= @@ -350,23 +400,20 @@ pygad.torchga Module :maxdepth: 4 :caption: pygad.torchga Module TOC - README_pygad_torchga_ReadTheDocs.rst + torchga.rst +.. _header-n15: - -.. _header-n11: - -More Information +Releases ================= .. toctree:: :maxdepth: 4 - :caption: More Information - - Footer.rst + :caption: Releases + releases.rst diff --git a/docs/source/README_pygad_kerasga_ReadTheDocs.rst b/docs/source/kerasga.rst similarity index 77% rename from docs/source/README_pygad_kerasga_ReadTheDocs.rst rename to docs/source/kerasga.rst index 0ae8db24..f39ffec6 100644 --- a/docs/source/README_pygad_kerasga_ReadTheDocs.rst +++ b/docs/source/kerasga.rst @@ -1,971 +1,1078 @@ -.. _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. - -It returns the predictions for 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(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 callback_generation(ga_instance): - print("Generation = {generation}".format(generation=ga_instance.generations_completed)) - print("Fitness = {fitness}".format(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/README_pygad_ReadTheDocs.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=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(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("Fitness value of the best solution = {solution_fitness}".format(solution_fitness=solution_fitness)) - print("Index of the best solution : {solution_idx}".format(solution_idx=solution_idx)) - - # Make prediction based on the best solution. - predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=data_inputs) - print("Predictions : \n", predictions) - - mae = tensorflow.keras.losses.MeanAbsoluteError() - abs_error = mae(data_outputs, predictions).numpy() - print("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-1: - -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(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/README_pygad_ReadTheDocs.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=callback_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. - -.. figure:: 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("Fitness value of the best solution = {solution_fitness}".format(solution_fitness=solution_fitness)) - print("Index of the best solution : {solution_idx}".format(solution_idx=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("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("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(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 callback_generation(ga_instance): - print("Generation = {generation}".format(generation=ga_instance.generations_completed)) - print("Fitness = {fitness}".format(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/README_pygad_ReadTheDocs.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=callback_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("Fitness value of the best solution = {solution_fitness}".format(solution_fitness=solution_fitness)) - print("Index of the best solution : {solution_idx}".format(solution_idx=solution_idx)) - - # Make predictions based on the best solution. - predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=data_inputs) - print("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("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. - -.. figure:: 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(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 callback_generation(ga_instance): - print("Generation = {generation}".format(generation=ga_instance.generations_completed)) - print("Fitness = {fitness}".format(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("dataset_features.npy") - - # Data outputs - data_outputs = numpy.load("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/README_pygad_ReadTheDocs.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=callback_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("Fitness value of the best solution = {solution_fitness}".format(solution_fitness=solution_fitness)) - print("Index of the best solution : {solution_idx}".format(solution_idx=solution_idx)) - - # Make predictions based on the best solution. - predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=data_inputs) - # print("Predictions : \n", predictions) - - # Calculate the categorical crossentropy for the trained model. - cce = tensorflow.keras.losses.CategoricalCrossentropy() - print("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("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("dataset_features.npy") - - data_outputs = numpy.load("outputs.npy") - data_outputs = tensorflow.keras.utils.to_categorical(data_outputs) - -The next figure shows how the fitness value changes. - -.. figure:: 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(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 callback_generation(ga_instance): - print("Generation = {generation}".format(generation=ga_instance.generations_completed)) - print("Fitness = {fitness}".format(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("dataset_inputs.npy") - - # Data outputs - data_outputs = numpy.load("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/README_pygad_ReadTheDocs.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=callback_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("Fitness value of the best solution = {solution_fitness}".format(solution_fitness=solution_fitness)) - print("Index of the best solution : {solution_idx}".format(solution_idx=solution_idx)) - - # Make predictions based on the best solution. - predictions = pygad.kerasga.predict(model=model, - solution=solution, - data=data_inputs) - # print("Predictions : \n", predictions) - - # Calculate the categorical crossentropy for the trained model. - cce = tensorflow.keras.losses.CategoricalCrossentropy() - print("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("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("dataset_inputs.npy") - - data_outputs = numpy.load("dataset_outputs.npy") - data_outputs = tensorflow.keras.utils.to_categorical(data_outputs) - -The next figure shows how the fitness value changes. - -.. figure:: 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) +.. _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/source/README_pygad_nn_ReadTheDocs.rst b/docs/source/nn.rst similarity index 93% rename from docs/source/README_pygad_nn_ReadTheDocs.rst rename to docs/source/nn.rst index ef4b3827..26b0af6c 100644 --- a/docs/source/README_pygad_nn_ReadTheDocs.rst +++ b/docs/source/nn.rst @@ -1,976 +1,976 @@ -.. _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). - -.. figure:: 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("Number of correct classifications : {num_correct}.".format(num_correct=num_correct)) - print("Number of wrong classifications : {num_wrong}.".format(num_wrong=num_wrong.size)) - print("Classification accuracy : {accuracy}.".format(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("Number of correct classifications : {num_correct}.".format(num_correct=num_correct)) - print("Number of wrong classifications : {num_wrong}.".format(num_wrong=num_wrong.size)) - print("Classification accuracy : {accuracy}.".format(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("Number of correct classifications : {num_correct}.".format(num_correct=num_correct)) - print("Number of wrong classifications : {num_wrong}.".format(num_wrong=num_wrong.size)) - print("Classification accuracy : {accuracy}.".format(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("Absolute error : {abs_error}.".format(abs_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("Absolute error : {abs_error}.".format(abs_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("Absolute error : {abs_error}.".format(abs_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("Absolute error : {abs_error}.".format(abs_error=abs_error)) +.. _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/source/pygad.rst b/docs/source/pygad.rst new file mode 100644 index 00000000..df843368 --- /dev/null +++ b/docs/source/pygad.rst @@ -0,0 +1,1879 @@ +``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"``. + +- ``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 within the constructor. If at least a +parameter is not correct, an exception is thrown. + +.. _plotting-methods-in-pygadga-class: + +Plotting Methods in ``pygad.GA`` Class +-------------------------------------- + +- ``plot_fitness()``: Shows how the fitness evolves by generation. + +- ``plot_genes()``: Shows how the gene value changes for each + generation. + +- ``plot_new_solution_rate()``: Shows the number of new solutions + explored in each solution. + +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. + +Other Attributes +~~~~~~~~~~~~~~~~ + +- ``generations_completed``: Holds the number of the last completed + generation. + +- ``population``: A NumPy array holding the initial population. + +- ``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 + ``last_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. + +- 4 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_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. + + 2. ``run_crossover()``: Apply crossover and call the callable + ``on_crossover()`` if defined. + + 3. ``run_mutation()``: Apply mutation and call the callable + ``on_mutation()`` if defined. + + 4. ``run_update_population()``: Update the ``population`` attribute + after completing the processes of crossover and mutation. + +The next sections discuss the methods available in the ``pygad.GA`` +class. + +.. _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 behaviour 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 behaviour 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``. + +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: + +- ``fitness``: The fitness values of the solutions in the current + population. + +- ``num_parents``: The number of parents to be selected. + +All of such methods return an array of the selected parents. + +The next subsections list the supported methods for parent selection. + +.. _steadystateselection: + +``steady_state_selection()`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Selects the parents using the steady-state selection technique. + +.. _rankselection: + +``rank_selection()`` +~~~~~~~~~~~~~~~~~~~~ + +Selects the parents using the rank selection technique. + +.. _randomselection: + +``random_selection()`` +~~~~~~~~~~~~~~~~~~~~~~ + +Selects the parents randomly. + +.. _tournamentselection: + +``tournament_selection()`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Selects the parents using the tournament selection technique. + +.. _roulettewheelselection: + +``roulette_wheel_selection()`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Selects the parents using the roulette wheel selection technique. + +.. _stochasticuniversalselection: + +``stochastic_universal_selection()`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Selects the parents using the stochastic universal selection technique. + +.. _nsga2selection: + +``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. + +.. _tournamentselectionnsga2: + +``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. + +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: + +- ``parents``: The parents to mate for producing the offspring. + +- ``offspring_size``: The size of the offspring to produce. + +All of such methods return an array of the produced offspring. + +The next subsections list the supported methods for crossover. + +.. _singlepointcrossover: + +``single_point_crossover()`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Applies the single-point crossover. It selects a point randomly at which +crossover takes place between the pairs of parents. + +.. _twopointscrossover: + +``two_points_crossover()`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Applies the 2 points crossover. It selects the 2 points randomly at +which crossover takes place between the pairs of parents. + +.. _uniformcrossover: + +``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. + +.. _scatteredcrossover: + +``scattered_crossover()`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Applies the scattered crossover. It randomly selects the gene from one +of the 2 parents. + +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: + +- ``offspring``: The offspring to mutate. + +All of such methods return an array of the mutated offspring. + +The next subsections list the supported methods for mutation. + +.. _randommutation: + +``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. + +.. _swapmutation: + +``swap_mutation()`` +~~~~~~~~~~~~~~~~~~~ + +Applies the swap mutation which interchanges the values of 2 randomly +selected genes. + +.. _inversionmutation: + +``inversion_mutation()`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +Applies the inversion mutation which selects a subset of genes and +inverts them. + +.. _scramblemutation: + +``scramble_mutation()`` +~~~~~~~~~~~~~~~~~~~~~~~ + +Applies the scramble mutation which selects a subset of genes and +shuffles their order randomly. + +.. _adaptivemutation: + +``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. + +.. _bestsolution: + +``best_solution()`` +------------------- + +Returns information about the best solution found by the genetic +algorithm. + +It accepts the following parameters: + +- ``pop_fitness=None``: An optional parameter that 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. + +It returns the following: + +- ``best_solution``: Best solution in the current population. + +- ``best_solution_fitness``: Fitness value of the best solution. + +- ``best_match_idx``: Index of the best solution in the current + population. + +.. _plotfitness: + +``plot_fitness()`` +------------------ + +Previously named ``plot_result()``, this method creates, shows, and +returns a figure that summarizes how the fitness value evolves by +generation. + +It works only after completing at least 1 generation. If no generation +is completed (at least 1), an exception is raised. + +.. _plotnewsolutionrate: + +``plot_new_solution_rate()`` +---------------------------- + +The ``plot_new_solution_rate()`` method creates, shows, and returns a +figure that shows the number of new solutions explored in each +generation. This method works only when ``save_solutions=True`` in the +constructor of the ``pygad.GA`` class. + +It works only after completing at least 1 generation. If no generation +is completed (at least 1), an exception is raised. + +.. _plotgenes: + +``plot_genes()`` +---------------- + +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. + +This is controlled by the ``graph_type`` parameter. + +It works only after completing at least 1 generation. If no generation +is completed (at least 1), an exception is raised. + +``save()`` +---------- + +Saves the genetic algorithm instance + +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(a=img_arr, newshape=(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(a=vector, newshape=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/source/pygad_more.rst b/docs/source/pygad_more.rst new file mode 100644 index 00000000..a992b1ed --- /dev/null +++ b/docs/source/pygad_more.rst @@ -0,0 +1,2454 @@ +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} + +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, + 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, + 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. + +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/source/Footer.rst b/docs/source/releases.rst similarity index 64% rename from docs/source/Footer.rst rename to docs/source/releases.rst index 57affe79..fddddf97 100644 --- a/docs/source/Footer.rst +++ b/docs/source/releases.rst @@ -1,1703 +1,2385 @@ -Release History -=============== - -.. figure:: https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png - :alt: - -.. _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(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(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 - -1. 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 - -2. 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 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) `__ --------------------------------------------------------------------------------------------------------------------------------------------------------------- - -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. - -|image1| - -`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. - -|image2| - -`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 . - -|image3| - -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 . - -|image4| - -Korean ------- - -`[PyGAD] Python 에서 Genetic Algorithm 을 사용해보기 `__ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -|image5| - -파이썬에서 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. - -|image6| - -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. - -|image7| - -Russian -------- - -`PyGAD: библиотека для имплементации генетического алгоритма `__ -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -PyGAD — это библиотека для имплементации генетического алгоритма. Кроме -того, библиотека предоставляет доступ к оптимизированным реализациям -алгоритмов машинного обучения. PyGAD разрабатывали на Python 3. - -Библиотека PyGAD поддерживает разные типы скрещивания, мутации и -селекции родителя. PyGAD позволяет оптимизировать проблемы с помощью -генетического алгоритма через кастомизацию целевой функции. - -Кроме генетического алгоритма, библиотека содержит оптимизированные -имплементации алгоритмов машинного обучения. На текущий момент PyGAD -поддерживает создание и обучение нейросетей для задач классификации. - -Библиотека находится в стадии активной разработки. Создатели планируют -добавление функционала для решения бинарных задач и имплементации новых -алгоритмов. - -PyGAD разрабатывали на Python 3.7.3. Зависимости включают в себя NumPy -для создания и манипуляции массивами и Matplotlib для визуализации. Один -из изкейсов использования инструмента — оптимизация весов, которые -удовлетворяют заданной функции. - -|image8| - -Research Papers using PyGAD -=========================== - -A number of research papers used PyGAD and here are some of them: - -- 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. - -|image9| - -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 `__ - -|image10| - -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 `__ - -|image11| - -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 `__ - -|image12| - -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. - -|image13| - -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 `__ - -|image14| - -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 `__ - -.. figure:: https://user-images.githubusercontent.com/16560492/78830077-ae7c2800-79e7-11ea-980b-53b6bd879eeb.jpg - :alt: - -Contact Us -========== - -- E-mail: ahmed.f.gad@gmail.com - -- `LinkedIn `__ - -- `Amazon Author Page `__ - -- `Heartbeat `__ - -- `Paperspace `__ - -- `KDnuggets `__ - -- `TowardsDataScience `__ - -- `GitHub `__ - -.. figure:: https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png - :alt: - -Thank you for using -`PyGAD `__ :) - -.. |image1| 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 -.. |image2| 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 -.. |image3| 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 -.. |image4| 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 -.. |image5| image:: https://user-images.githubusercontent.com/16560492/108586306-85bd0280-731b-11eb-874c-7ac4ce1326cd.jpg - :target: https://data-newbie.tistory.com/m/685 -.. |image6| 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 -.. |image7| 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 -.. |image8| 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 -.. |image9| 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 -.. |image10| 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 -.. |image11| 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 -.. |image12| 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 -.. |image13| 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 -.. |image14| 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 +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. 4 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. + +.. _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 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/source/README_pygad_torchga_ReadTheDocs.rst b/docs/source/torchga.rst similarity index 86% rename from docs/source/README_pygad_torchga_ReadTheDocs.rst rename to docs/source/torchga.rst index 1f070279..27825e83 100644 --- a/docs/source/README_pygad_torchga_ReadTheDocs.rst +++ b/docs/source/torchga.rst @@ -1,946 +1,944 @@ -.. _pygadtorchga-module: - -``pygad.torchga`` Module -======================== - -This section of the PyGAD's library documentation discusses the -**pygad.torchga** module. - -The ``pygad.torchga`` module has helper a 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(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 callback_generation(ga_instance): - print("Generation = {generation}".format(generation=ga_instance.generations_completed)) - print("Fitness = {fitness}".format(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/README_pygad_ReadTheDocs.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=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(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("Fitness value of the best solution = {solution_fitness}".format(solution_fitness=solution_fitness)) - print("Index of the best solution : {solution_idx}".format(solution_idx=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-1: - -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(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/README_pygad_ReadTheDocs.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=callback_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. - -.. figure:: 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("Fitness value of the best solution = {solution_fitness}".format(solution_fitness=solution_fitness)) - print("Index of the best solution : {solution_idx}".format(solution_idx=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(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 callback_generation(ga_instance): - print("Generation = {generation}".format(generation=ga_instance.generations_completed)) - print("Fitness = {fitness}".format(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/README_pygad_ReadTheDocs.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=callback_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("Fitness value of the best solution = {solution_fitness}".format(solution_fitness=solution_fitness)) - print("Index of the best solution : {solution_idx}".format(solution_idx=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. - -.. figure:: 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(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 callback_generation(ga_instance): - print("Generation = {generation}".format(generation=ga_instance.generations_completed)) - print("Fitness = {fitness}".format(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/README_pygad_ReadTheDocs.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=callback_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("Fitness value of the best solution = {solution_fitness}".format(solution_fitness=solution_fitness)) - print("Index of the best solution : {solution_idx}".format(solution_idx=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. - -.. figure:: 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(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 callback_generation(ga_instance): - print("Generation = {generation}".format(generation=ga_instance.generations_completed)) - print("Fitness = {fitness}".format(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/README_pygad_ReadTheDocs.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=callback_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("Fitness value of the best solution = {solution_fitness}".format(solution_fitness=solution_fitness)) - print("Index of the best solution : {solution_idx}".format(solution_idx=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. - -.. figure:: 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 +.. _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/source/utils.rst b/docs/source/utils.rst new file mode 100644 index 00000000..d3a69512 --- /dev/null +++ b/docs/source/utils.rst @@ -0,0 +1,707 @@ +.. _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. ``crossover``: Has the ``Crossover`` class that implements the + crossover operators. + +2. ``mutation``: Has the ``Mutation`` class that implements the mutation + operators. + +3. ``parent_selection``: Has the ``ParentSelection`` class that + implements the parent selection operators. + +4. ``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. + +.. _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. Scramble: Implemented using the ``adaptive_mutation()`` method. + +All mutation methods accept this parameter: + +1. ``offspring``: The offspring to mutate. + +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. + +.. image:: https://user-images.githubusercontent.com/16560492/103468973-e3c26600-4d2c-11eb-8af3-b3bb39b50540.jpg + :alt: + +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_nsga2_selection()`` 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. + +.. _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 set of non-dominated + solutions from the passed solutions. + +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() diff --git a/docs/source/visualize.rst b/docs/source/visualize.rst new file mode 100644 index 00000000..629d4dfa --- /dev/null +++ b/docs/source/visualize.rst @@ -0,0 +1,509 @@ +.. _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. + +2. ``plot_genes()``: Creates plots for the genes. + +3. ``plot_new_solution_rate()``: Creates plots for the new solution + rate. + +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. + +.. _plottype=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: + +.. code:: python + + ga_instance.plot_fitness() + # ga_instance.plot_fitness(plot_type="plot") + +|image1| + +.. _plottype=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. + +.. code:: python + + ga_instance.plot_fitness(plot_type="scatter") + +|image2| + +.. _plottype=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. + +.. 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. + +.. _plottype=plot-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| + +.. _plottype=scatter-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| + +.. _plottype=bar-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. . + +.. _graphtype=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. + +.. _plottype=plot-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") + +.. _plottype=scatter-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| + +.. _plottype=bar-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| + +.. _graphtype=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. + +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| + +.. _graphtype=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. + +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 diff --git a/examples/KerasGA/XOR_classification.py b/examples/KerasGA/XOR_classification.py new file mode 100644 index 00000000..2d7e4ee5 --- /dev/null +++ b/examples/KerasGA/XOR_classification.py @@ -0,0 +1,86 @@ +import tensorflow.keras +import pygad.kerasga +import numpy +import pygad + +def fitness_func(ga_instanse, 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.0], + [0.0, 1.0], + [1.0, 0.0], + [1.0, 1.0]]) + +# XOR problem outputs +data_outputs = numpy.array([[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/README_pygad_ReadTheDocs.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}") + +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(f"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}") + +# model.compile(optimizer="Adam", loss="mse", metrics=["mae"]) + +# _ = model.fit(x, y, verbose=0) +# r = model.predict(data_inputs) diff --git a/examples/KerasGA/cancer_dataset.py b/examples/KerasGA/cancer_dataset.py new file mode 100644 index 00000000..f5e87d39 --- /dev/null +++ b/examples/KerasGA/cancer_dataset.py @@ -0,0 +1,95 @@ +import tensorflow as tf +import tensorflow.keras +import pygad.kerasga +import pygad +import numpy + +def fitness_func(ga_instanse, solution, sol_idx): + global train_data, data_outputs, keras_ga, model + + predictions = pygad.kerasga.predict(model=model, + solution=solution, + data=train_data) + + 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]}") + +# 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) + +train_data = tf.keras.utils.image_dataset_from_directory( + directory=dataset_path, + image_size=(img_size, img_size), + label_mode="categorical", + batch_size=32 +) + +# Get the dataset labels. +# train_data.class_indices +data_outputs = numpy.array([]) +for x, y in train_data: + data_outputs = numpy.concatenate([data_outputs, numpy.argmax(y.numpy(), axis=-1)]) +data_outputs = tf.keras.utils.to_categorical(data_outputs) + +# Check the documentation for more information about the parameters: https://pygad.readthedocs.io/en/latest/README_pygad_ReadTheDocs.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_data) +# print("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}") + +# model.compile(optimizer="Adam", loss="mse", metrics=["mae"]) + +# _ = model.fit(x, y, verbose=0) +# r = model.predict(data_inputs) diff --git a/examples/KerasGA/cancer_dataset_generator.py b/examples/KerasGA/cancer_dataset_generator.py new file mode 100644 index 00000000..9746e907 --- /dev/null +++ b/examples/KerasGA/cancer_dataset_generator.py @@ -0,0 +1,89 @@ +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(f"Generation = {ga_instance.generations_completed}") + print(f"Fitness = {ga_instance.best_solution()[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/README_pygad_ReadTheDocs.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("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}") + +# model.compile(optimizer="Adam", loss="mse", metrics=["mae"]) + +# _ = model.fit(x, y, verbose=0) +# r = model.predict(data_inputs) diff --git a/examples/KerasGA/image_classification_CNN.py b/examples/KerasGA/image_classification_CNN.py new file mode 100644 index 00000000..5e467607 --- /dev/null +++ b/examples/KerasGA/image_classification_CNN.py @@ -0,0 +1,101 @@ +import tensorflow.keras +import pygad.kerasga +import numpy +import pygad +import gc + + +def fitness_func(ga_instanse, 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]}") + gc.collect() # can useful for not exploding the memory usage on notebooks (ipynb) freeing memory + + +# 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/README_pygad_ReadTheDocs.html#pygad-ga-class +num_generations = 200 # Number of generations. +# Number of solutions to be selected as parents in the mating pool. +num_parents_mating = 5 +# Initial population of network weights. +initial_population = keras_ga.population_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}") + +predictions = pygad.kerasga.predict(model=model, + solution=solution, + data=data_inputs) +# print("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}") + +# model.compile(optimizer="Adam", loss="mse", metrics=["mae"]) + +# _ = model.fit(x, y, verbose=0) +# r = model.predict(data_inputs) diff --git a/examples/KerasGA/image_classification_Dense.py b/examples/KerasGA/image_classification_Dense.py new file mode 100644 index 00000000..986282a3 --- /dev/null +++ b/examples/KerasGA/image_classification_Dense.py @@ -0,0 +1,82 @@ +import tensorflow.keras +import pygad.kerasga +import numpy +import pygad + +def fitness_func(ga_instanse, 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/README_pygad_ReadTheDocs.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}") + +# Fetch the parameters of the best solution. +predictions = pygad.kerasga.predict(model=model, + solution=solution, + data=data_inputs) +# print("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}") + +# model.compile(optimizer="Adam", loss="mse", metrics=["mae"]) + +# _ = model.fit(x, y, verbose=0) +# r = model.predict(data_inputs) diff --git a/examples/KerasGA/regression_example.py b/examples/KerasGA/regression_example.py new file mode 100644 index 00000000..11312c35 --- /dev/null +++ b/examples/KerasGA/regression_example.py @@ -0,0 +1,79 @@ +import tensorflow.keras +import pygad.kerasga +import numpy +import pygad + +def fitness_func(ga_instanse, 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]}") + +# Create the Keras model. +input_layer = tensorflow.keras.layers.Input(3) +dense_layer1 = tensorflow.keras.layers.Dense(5, activation="relu")(input_layer) +dense_layer1.trainable = False +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/README_pygad_ReadTheDocs.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}") + +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}") + +# model.compile(optimizer="Adam", loss="mse", metrics=["mae"]) + +# _ = model.fit(x, y, verbose=0) +# r = model.predict(data_inputs) diff --git a/examples/TorchGA/XOR_classification.py b/examples/TorchGA/XOR_classification.py new file mode 100644 index 00000000..be2e9e86 --- /dev/null +++ b/examples/TorchGA/XOR_classification.py @@ -0,0 +1,86 @@ +import torch +import pygad.torchga +import pygad + +def fitness_func(ga_instanse, 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 = pygad.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/README_pygad_ReadTheDocs.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, + parallel_processing=3, + 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}") + +predictions = pygad.torchga.predict(model=model, + solution=solution, + data=data_inputs) +print(f"Predictions : \n{predictions.detach().numpy()}") + +# Calculate the binary crossentropy for the trained model. +print(f"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.true_divide(torch.sum(a.indices == b.indices), len(data_outputs)) +print(f"Accuracy : {accuracy.detach().numpy()}") diff --git a/examples/TorchGA/image_classification_CNN.py b/examples/TorchGA/image_classification_CNN.py new file mode 100644 index 00000000..295cdc59 --- /dev/null +++ b/examples/TorchGA/image_classification_CNN.py @@ -0,0 +1,94 @@ +import torch +import pygad.torchga +import pygad +import numpy + +def fitness_func(ga_instanse, 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 = pygad.torchga.TorchGA(model=model, + num_solutions=10) + +loss_function = torch.nn.CrossEntropyLoss() + +# Data inputs +data_inputs = torch.from_numpy(numpy.load("../data/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("../data/dataset_outputs.npy")).long() + +# Prepare the PyGAD parameters. Check the documentation for more information: https://pygad.readthedocs.io/en/latest/README_pygad_ReadTheDocs.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}") + +predictions = pygad.torchga.predict(model=model, + solution=solution, + data=data_inputs) +# print("Predictions : \n", predictions) + +# Calculate the crossentropy for the trained model. +print(f"Crossentropy : {loss_function(predictions, data_outputs).detach().numpy()}") + +# Calculate the classification accuracy for the trained model. +accuracy = torch.true_divide(torch.sum(torch.max(predictions, axis=1).indices == data_outputs), len(data_outputs)) +print(f"Accuracy : {accuracy.detach().numpy()}") diff --git a/examples/TorchGA/image_classification_Dense.py b/examples/TorchGA/image_classification_Dense.py new file mode 100644 index 00000000..85e8b1f3 --- /dev/null +++ b/examples/TorchGA/image_classification_Dense.py @@ -0,0 +1,80 @@ +import torch +import pygad.torchga +import pygad +import numpy + +def fitness_func(ga_instanse, 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 = pygad.torchga.TorchGA(model=model, + num_solutions=10) + +loss_function = torch.nn.CrossEntropyLoss() + +# Data inputs +data_inputs = torch.from_numpy(numpy.load("../data/dataset_features.npy")).float() + +# Data outputs +data_outputs = torch.from_numpy(numpy.load("../data/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/README_pygad_ReadTheDocs.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}") + +predictions = pygad.torchga.predict(model=model, + solution=solution, + data=data_inputs) +# print("Predictions : \n", predictions) + +# Calculate the crossentropy loss of the trained model. +print(f"Crossentropy : {loss_function(predictions, data_outputs).detach().numpy()}") + +# Calculate the classification accuracy for the trained model. +accuracy = torch.true_divide(torch.sum(torch.max(predictions, axis=1).indices == data_outputs), len(data_outputs)) +print(f"Accuracy : {accuracy.detach().numpy()}") diff --git a/examples/TorchGA/regression_example.py b/examples/TorchGA/regression_example.py new file mode 100644 index 00000000..a7feb31a --- /dev/null +++ b/examples/TorchGA/regression_example.py @@ -0,0 +1,76 @@ +import torch +import pygad.torchga +import pygad + +def fitness_func(ga_instanse, 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, 2) +relu_layer = torch.nn.ReLU() +output_layer = torch.nn.Linear(2, 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 = pygad.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/README_pygad_ReadTheDocs.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}") + +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()}") diff --git a/example_clustering_2.py b/examples/clustering/example_clustering_2.py similarity index 92% rename from example_clustering_2.py rename to examples/clustering/example_clustering_2.py index c30e0c9f..9b846541 100644 --- a/example_clustering_2.py +++ b/examples/clustering/example_clustering_2.py @@ -1,122 +1,122 @@ -import numpy -import matplotlib.pyplot -import pygad - -cluster1_num_samples = 10 -cluster1_x1_start = 0 -cluster1_x1_end = 5 -cluster1_x2_start = 2 -cluster1_x2_end = 6 -cluster1_x1 = numpy.random.random(size=(cluster1_num_samples)) -cluster1_x1 = cluster1_x1 * (cluster1_x1_end - cluster1_x1_start) + cluster1_x1_start -cluster1_x2 = numpy.random.random(size=(cluster1_num_samples)) -cluster1_x2 = cluster1_x2 * (cluster1_x2_end - cluster1_x2_start) + cluster1_x2_start - -cluster2_num_samples = 10 -cluster2_x1_start = 10 -cluster2_x1_end = 15 -cluster2_x2_start = 8 -cluster2_x2_end = 12 -cluster2_x1 = numpy.random.random(size=(cluster2_num_samples)) -cluster2_x1 = cluster2_x1 * (cluster2_x1_end - cluster2_x1_start) + cluster2_x1_start -cluster2_x2 = numpy.random.random(size=(cluster2_num_samples)) -cluster2_x2 = cluster2_x2 * (cluster2_x2_end - cluster2_x2_start) + cluster2_x2_start - -c1 = numpy.array([cluster1_x1, cluster1_x2]).T -c2 = numpy.array([cluster2_x1, cluster2_x2]).T - -data = numpy.concatenate((c1, c2), axis=0) - -matplotlib.pyplot.scatter(cluster1_x1, cluster1_x2) -matplotlib.pyplot.scatter(cluster2_x1, cluster2_x2) -matplotlib.pyplot.title("Optimal Clustering") -matplotlib.pyplot.show() - -def euclidean_distance(X, Y): - """ - Calculate the euclidean distance between X and Y. It accepts: - :X should be a matrix of size (N, f) where N is the number of samples and f is the number of features for each sample. - :Y should be of size f. In other words, it is a single sample. - - Returns a vector of N elements with the distances between the N samples and the Y. - """ - - return numpy.sqrt(numpy.sum(numpy.power(X - Y, 2), axis=1)) - -def cluster_data(solution, solution_idx): - """ - Clusters the data based on the current solution. - """ - - global num_cluster, data - feature_vector_length = data.shape[1] - cluster_centers = [] # A list of size (C, f) where C is the number of clusters and f is the number of features representing each sample. - all_clusters_dists = [] # A list of size (C, N) where C is the number of clusters and N is the number of data samples. It holds the distances between each cluster center and all the data samples. - clusters = [] # A list with C elements where each element holds the indices of the samples within a cluster. - clusters_sum_dist = [] # A list with C elements where each element represents the sum of distances of the samples with a cluster. - - for clust_idx in range(num_clusters): - # Return the current cluster center. - cluster_centers.append(solution[feature_vector_length*clust_idx:feature_vector_length*(clust_idx+1)]) - # Calculate the distance (e.g. euclidean) between the current cluster center and all samples. - cluster_center_dists = euclidean_distance(data, cluster_centers[clust_idx]) - all_clusters_dists.append(numpy.array(cluster_center_dists)) - - cluster_centers = numpy.array(cluster_centers) - all_clusters_dists = numpy.array(all_clusters_dists) - - # A 1D array that, for each sample, holds the index of the cluster with the smallest distance. - # In other words, the array holds the sample's cluster index. - cluster_indices = numpy.argmin(all_clusters_dists, axis=0) - for clust_idx in range(num_clusters): - clusters.append(numpy.where(cluster_indices == clust_idx)[0]) - # Calculate the sum of distances for the cluster. - if len(clusters[clust_idx]) == 0: - # In case the cluster is empty (i.e. has zero samples). - clusters_sum_dist.append(0) - else: - # When the cluster is not empty (i.e. has at least 1 sample). - clusters_sum_dist.append(numpy.sum(all_clusters_dists[clust_idx, clusters[clust_idx]])) - # clusters_sum_dist.append(numpy.sum(euclidean_distance(data[clusters[clust_idx], :], cluster_centers[clust_idx]))) - - clusters_sum_dist = numpy.array(clusters_sum_dist) - - return cluster_centers, all_clusters_dists, cluster_indices, clusters, clusters_sum_dist - -def fitness_func(solution, solution_idx): - _, _, _, _, clusters_sum_dist = cluster_data(solution, solution_idx) - - # The tiny value 0.00000001 is added to the denominator in case the average distance is 0. - fitness = 1.0 / (numpy.sum(clusters_sum_dist) + 0.00000001) - - return fitness - -num_clusters = 2 -num_genes = num_clusters * data.shape[1] - -ga_instance = pygad.GA(num_generations=100, - sol_per_pop=10, - num_parents_mating=5, - init_range_low=-6, - init_range_high=20, - keep_parents=2, - num_genes=num_genes, - fitness_func=fitness_func, - suppress_warnings=True) - -ga_instance.run() - -best_solution, best_solution_fitness, best_solution_idx = ga_instance.best_solution() -print("Best solution is {bs}".format(bs=best_solution)) -print("Fitness of the best solution is {bsf}".format(bsf=best_solution_fitness)) -print("Best solution found after {gen} generations".format(gen=ga_instance.best_solution_generation)) - -cluster_centers, all_clusters_dists, cluster_indices, clusters, clusters_sum_dist = cluster_data(best_solution, best_solution_idx) - -for cluster_idx in range(num_clusters): - cluster_x = data[clusters[cluster_idx], 0] - cluster_y = data[clusters[cluster_idx], 1] - matplotlib.pyplot.scatter(cluster_x, cluster_y) - matplotlib.pyplot.scatter(cluster_centers[cluster_idx, 0], cluster_centers[cluster_idx, 1], linewidths=5) -matplotlib.pyplot.title("Clustering using PyGAD") -matplotlib.pyplot.show() +import numpy +import matplotlib.pyplot +import pygad + +cluster1_num_samples = 10 +cluster1_x1_start = 0 +cluster1_x1_end = 5 +cluster1_x2_start = 2 +cluster1_x2_end = 6 +cluster1_x1 = numpy.random.random(size=(cluster1_num_samples)) +cluster1_x1 = cluster1_x1 * (cluster1_x1_end - cluster1_x1_start) + cluster1_x1_start +cluster1_x2 = numpy.random.random(size=(cluster1_num_samples)) +cluster1_x2 = cluster1_x2 * (cluster1_x2_end - cluster1_x2_start) + cluster1_x2_start + +cluster2_num_samples = 10 +cluster2_x1_start = 10 +cluster2_x1_end = 15 +cluster2_x2_start = 8 +cluster2_x2_end = 12 +cluster2_x1 = numpy.random.random(size=(cluster2_num_samples)) +cluster2_x1 = cluster2_x1 * (cluster2_x1_end - cluster2_x1_start) + cluster2_x1_start +cluster2_x2 = numpy.random.random(size=(cluster2_num_samples)) +cluster2_x2 = cluster2_x2 * (cluster2_x2_end - cluster2_x2_start) + cluster2_x2_start + +c1 = numpy.array([cluster1_x1, cluster1_x2]).T +c2 = numpy.array([cluster2_x1, cluster2_x2]).T + +data = numpy.concatenate((c1, c2), axis=0) + +matplotlib.pyplot.scatter(cluster1_x1, cluster1_x2) +matplotlib.pyplot.scatter(cluster2_x1, cluster2_x2) +matplotlib.pyplot.title("Optimal Clustering") +matplotlib.pyplot.show() + +def euclidean_distance(X, Y): + """ + Calculate the euclidean distance between X and Y. It accepts: + :X should be a matrix of size (N, f) where N is the number of samples and f is the number of features for each sample. + :Y should be of size f. In other words, it is a single sample. + + Returns a vector of N elements with the distances between the N samples and the Y. + """ + + return numpy.sqrt(numpy.sum(numpy.power(X - Y, 2), axis=1)) + +def cluster_data(solution, solution_idx): + """ + Clusters the data based on the current solution. + """ + + global num_cluster, data + feature_vector_length = data.shape[1] + cluster_centers = [] # A list of size (C, f) where C is the number of clusters and f is the number of features representing each sample. + all_clusters_dists = [] # A list of size (C, N) where C is the number of clusters and N is the number of data samples. It holds the distances between each cluster center and all the data samples. + clusters = [] # A list with C elements where each element holds the indices of the samples within a cluster. + clusters_sum_dist = [] # A list with C elements where each element represents the sum of distances of the samples with a cluster. + + for clust_idx in range(num_clusters): + # Return the current cluster center. + cluster_centers.append(solution[feature_vector_length*clust_idx:feature_vector_length*(clust_idx+1)]) + # Calculate the distance (e.g. euclidean) between the current cluster center and all samples. + cluster_center_dists = euclidean_distance(data, cluster_centers[clust_idx]) + all_clusters_dists.append(numpy.array(cluster_center_dists)) + + cluster_centers = numpy.array(cluster_centers) + all_clusters_dists = numpy.array(all_clusters_dists) + + # A 1D array that, for each sample, holds the index of the cluster with the smallest distance. + # In other words, the array holds the sample's cluster index. + cluster_indices = numpy.argmin(all_clusters_dists, axis=0) + for clust_idx in range(num_clusters): + clusters.append(numpy.where(cluster_indices == clust_idx)[0]) + # Calculate the sum of distances for the cluster. + if len(clusters[clust_idx]) == 0: + # In case the cluster is empty (i.e. has zero samples). + clusters_sum_dist.append(0) + else: + # When the cluster is not empty (i.e. has at least 1 sample). + clusters_sum_dist.append(numpy.sum(all_clusters_dists[clust_idx, clusters[clust_idx]])) + # clusters_sum_dist.append(numpy.sum(euclidean_distance(data[clusters[clust_idx], :], cluster_centers[clust_idx]))) + + clusters_sum_dist = numpy.array(clusters_sum_dist) + + return cluster_centers, all_clusters_dists, cluster_indices, clusters, clusters_sum_dist + +def fitness_func(ga_instance, solution, solution_idx): + _, _, _, _, clusters_sum_dist = cluster_data(solution, solution_idx) + + # The tiny value 0.00000001 is added to the denominator in case the average distance is 0. + fitness = 1.0 / (numpy.sum(clusters_sum_dist) + 0.00000001) + + return fitness + +num_clusters = 2 +num_genes = num_clusters * data.shape[1] + +ga_instance = pygad.GA(num_generations=100, + sol_per_pop=10, + num_parents_mating=5, + init_range_low=-6, + init_range_high=20, + keep_parents=2, + num_genes=num_genes, + fitness_func=fitness_func, + suppress_warnings=True) + +ga_instance.run() + +best_solution, best_solution_fitness, best_solution_idx = ga_instance.best_solution() +print(f"Best solution is {best_solution}") +print(f"Fitness of the best solution is {best_solution_fitness}") +print(f"Best solution found after {ga_instance.best_solution_generation} generations") + +cluster_centers, all_clusters_dists, cluster_indices, clusters, clusters_sum_dist = cluster_data(best_solution, best_solution_idx) + +for cluster_idx in range(num_clusters): + cluster_x = data[clusters[cluster_idx], 0] + cluster_y = data[clusters[cluster_idx], 1] + matplotlib.pyplot.scatter(cluster_x, cluster_y) + matplotlib.pyplot.scatter(cluster_centers[cluster_idx, 0], cluster_centers[cluster_idx, 1], linewidths=5) +matplotlib.pyplot.title("Clustering using PyGAD") +matplotlib.pyplot.show() diff --git a/example_clustering_3.py b/examples/clustering/example_clustering_3.py similarity index 93% rename from example_clustering_3.py rename to examples/clustering/example_clustering_3.py index bfec5ef5..5c0381d8 100644 --- a/example_clustering_3.py +++ b/examples/clustering/example_clustering_3.py @@ -1,134 +1,134 @@ -import numpy -import matplotlib.pyplot -import pygad - -cluster1_num_samples = 20 -cluster1_x1_start = 0 -cluster1_x1_end = 5 -cluster1_x2_start = 2 -cluster1_x2_end = 6 -cluster1_x1 = numpy.random.random(size=(cluster1_num_samples)) -cluster1_x1 = cluster1_x1 * (cluster1_x1_end - cluster1_x1_start) + cluster1_x1_start -cluster1_x2 = numpy.random.random(size=(cluster1_num_samples)) -cluster1_x2 = cluster1_x2 * (cluster1_x2_end - cluster1_x2_start) + cluster1_x2_start - -cluster2_num_samples = 20 -cluster2_x1_start = 4 -cluster2_x1_end = 12 -cluster2_x2_start = 14 -cluster2_x2_end = 18 -cluster2_x1 = numpy.random.random(size=(cluster2_num_samples)) -cluster2_x1 = cluster2_x1 * (cluster2_x1_end - cluster2_x1_start) + cluster2_x1_start -cluster2_x2 = numpy.random.random(size=(cluster2_num_samples)) -cluster2_x2 = cluster2_x2 * (cluster2_x2_end - cluster2_x2_start) + cluster2_x2_start - -cluster3_num_samples = 20 -cluster3_x1_start = 12 -cluster3_x1_end = 18 -cluster3_x2_start = 8 -cluster3_x2_end = 11 -cluster3_x1 = numpy.random.random(size=(cluster3_num_samples)) -cluster3_x1 = cluster3_x1 * (cluster3_x1_end - cluster3_x1_start) + cluster3_x1_start -cluster3_x2 = numpy.random.random(size=(cluster3_num_samples)) -cluster3_x2 = cluster3_x2 * (cluster3_x2_end - cluster3_x2_start) + cluster3_x2_start - -c1 = numpy.array([cluster1_x1, cluster1_x2]).T -c2 = numpy.array([cluster2_x1, cluster2_x2]).T -c3 = numpy.array([cluster3_x1, cluster3_x2]).T - -data = numpy.concatenate((c1, c2, c3), axis=0) - -matplotlib.pyplot.scatter(cluster1_x1, cluster1_x2) -matplotlib.pyplot.scatter(cluster2_x1, cluster2_x2) -matplotlib.pyplot.scatter(cluster3_x1, cluster3_x2) -matplotlib.pyplot.title("Optimal Clustering") -matplotlib.pyplot.show() - -def euclidean_distance(X, Y): - """ - Calculate the euclidean distance between X and Y. It accepts: - :X should be a matrix of size (N, f) where N is the number of samples and f is the number of features for each sample. - :Y should be of size f. In other words, it is a single sample. - - Returns a vector of N elements with the distances between the N samples and the Y. - """ - - return numpy.sqrt(numpy.sum(numpy.power(X - Y, 2), axis=1)) - -def cluster_data(solution, solution_idx): - """ - Clusters the data based on the current solution. - """ - - global num_clusters, feature_vector_length, data - cluster_centers = [] # A list of size (C, f) where C is the number of clusters and f is the number of features representing each sample. - all_clusters_dists = [] # A list of size (C, N) where C is the number of clusters and N is the number of data samples. It holds the distances between each cluster center and all the data samples. - clusters = [] # A list with C elements where each element holds the indices of the samples within a cluster. - clusters_sum_dist = [] # A list with C elements where each element represents the sum of distances of the samples with a cluster. - - for clust_idx in range(num_clusters): - # Return the current cluster center. - cluster_centers.append(solution[feature_vector_length*clust_idx:feature_vector_length*(clust_idx+1)]) - # Calculate the distance (e.g. euclidean) between the current cluster center and all samples. - cluster_center_dists = euclidean_distance(data, cluster_centers[clust_idx]) - all_clusters_dists.append(numpy.array(cluster_center_dists)) - - cluster_centers = numpy.array(cluster_centers) - all_clusters_dists = numpy.array(all_clusters_dists) - - # A 1D array that, for each sample, holds the index of the cluster with the smallest distance. - # In other words, the array holds the sample's cluster index. - cluster_indices = numpy.argmin(all_clusters_dists, axis=0) - for clust_idx in range(num_clusters): - clusters.append(numpy.where(cluster_indices == clust_idx)[0]) - # Calculate the sum of distances for the cluster. - if len(clusters[clust_idx]) == 0: - # In case the cluster is empty (i.e. has zero samples). - clusters_sum_dist.append(0) - else: - # When the cluster is not empty (i.e. has at least 1 sample). - clusters_sum_dist.append(numpy.sum(all_clusters_dists[clust_idx, clusters[clust_idx]])) - # clusters_sum_dist.append(numpy.sum(euclidean_distance(data[clusters[clust_idx], :], cluster_centers[clust_idx]))) - - clusters_sum_dist = numpy.array(clusters_sum_dist) - - return cluster_centers, all_clusters_dists, cluster_indices, clusters, clusters_sum_dist - -def fitness_func(solution, solution_idx): - _, _, _, _, clusters_sum_dist = cluster_data(solution, solution_idx) - - # The tiny value 0.00000001 is added to the denominator in case the average distance is 0. - fitness = 1.0 / (numpy.sum(clusters_sum_dist) + 0.00000001) - - return fitness - -num_clusters = 3 -feature_vector_length = data.shape[1] -num_genes = num_clusters * feature_vector_length - -ga_instance = pygad.GA(num_generations=100, - sol_per_pop=10, - init_range_low=0, - init_range_high=20, - num_parents_mating=5, - keep_parents=2, - num_genes=num_genes, - fitness_func=fitness_func, - suppress_warnings=True) - -ga_instance.run() - -best_solution, best_solution_fitness, best_solution_idx = ga_instance.best_solution() -print("Best solution is {bs}".format(bs=best_solution)) -print("Fitness of the best solution is {bsf}".format(bsf=best_solution_fitness)) -print("Best solution found after {gen} generations".format(gen=ga_instance.best_solution_generation)) - -cluster_centers, all_clusters_dists, cluster_indices, clusters, clusters_sum_dist = cluster_data(best_solution, best_solution_idx) - -for cluster_idx in range(num_clusters): - cluster_x = data[clusters[cluster_idx], 0] - cluster_y = data[clusters[cluster_idx], 1] - matplotlib.pyplot.scatter(cluster_x, cluster_y) - matplotlib.pyplot.scatter(cluster_centers[cluster_idx, 0], cluster_centers[cluster_idx, 1], linewidths=5) -matplotlib.pyplot.title("Clustering using PyGAD") -matplotlib.pyplot.show() +import numpy +import matplotlib.pyplot +import pygad + +cluster1_num_samples = 20 +cluster1_x1_start = 0 +cluster1_x1_end = 5 +cluster1_x2_start = 2 +cluster1_x2_end = 6 +cluster1_x1 = numpy.random.random(size=(cluster1_num_samples)) +cluster1_x1 = cluster1_x1 * (cluster1_x1_end - cluster1_x1_start) + cluster1_x1_start +cluster1_x2 = numpy.random.random(size=(cluster1_num_samples)) +cluster1_x2 = cluster1_x2 * (cluster1_x2_end - cluster1_x2_start) + cluster1_x2_start + +cluster2_num_samples = 20 +cluster2_x1_start = 4 +cluster2_x1_end = 12 +cluster2_x2_start = 14 +cluster2_x2_end = 18 +cluster2_x1 = numpy.random.random(size=(cluster2_num_samples)) +cluster2_x1 = cluster2_x1 * (cluster2_x1_end - cluster2_x1_start) + cluster2_x1_start +cluster2_x2 = numpy.random.random(size=(cluster2_num_samples)) +cluster2_x2 = cluster2_x2 * (cluster2_x2_end - cluster2_x2_start) + cluster2_x2_start + +cluster3_num_samples = 20 +cluster3_x1_start = 12 +cluster3_x1_end = 18 +cluster3_x2_start = 8 +cluster3_x2_end = 11 +cluster3_x1 = numpy.random.random(size=(cluster3_num_samples)) +cluster3_x1 = cluster3_x1 * (cluster3_x1_end - cluster3_x1_start) + cluster3_x1_start +cluster3_x2 = numpy.random.random(size=(cluster3_num_samples)) +cluster3_x2 = cluster3_x2 * (cluster3_x2_end - cluster3_x2_start) + cluster3_x2_start + +c1 = numpy.array([cluster1_x1, cluster1_x2]).T +c2 = numpy.array([cluster2_x1, cluster2_x2]).T +c3 = numpy.array([cluster3_x1, cluster3_x2]).T + +data = numpy.concatenate((c1, c2, c3), axis=0) + +matplotlib.pyplot.scatter(cluster1_x1, cluster1_x2) +matplotlib.pyplot.scatter(cluster2_x1, cluster2_x2) +matplotlib.pyplot.scatter(cluster3_x1, cluster3_x2) +matplotlib.pyplot.title("Optimal Clustering") +matplotlib.pyplot.show() + +def euclidean_distance(X, Y): + """ + Calculate the euclidean distance between X and Y. It accepts: + :X should be a matrix of size (N, f) where N is the number of samples and f is the number of features for each sample. + :Y should be of size f. In other words, it is a single sample. + + Returns a vector of N elements with the distances between the N samples and the Y. + """ + + return numpy.sqrt(numpy.sum(numpy.power(X - Y, 2), axis=1)) + +def cluster_data(solution, solution_idx): + """ + Clusters the data based on the current solution. + """ + + global num_clusters, feature_vector_length, data + cluster_centers = [] # A list of size (C, f) where C is the number of clusters and f is the number of features representing each sample. + all_clusters_dists = [] # A list of size (C, N) where C is the number of clusters and N is the number of data samples. It holds the distances between each cluster center and all the data samples. + clusters = [] # A list with C elements where each element holds the indices of the samples within a cluster. + clusters_sum_dist = [] # A list with C elements where each element represents the sum of distances of the samples with a cluster. + + for clust_idx in range(num_clusters): + # Return the current cluster center. + cluster_centers.append(solution[feature_vector_length*clust_idx:feature_vector_length*(clust_idx+1)]) + # Calculate the distance (e.g. euclidean) between the current cluster center and all samples. + cluster_center_dists = euclidean_distance(data, cluster_centers[clust_idx]) + all_clusters_dists.append(numpy.array(cluster_center_dists)) + + cluster_centers = numpy.array(cluster_centers) + all_clusters_dists = numpy.array(all_clusters_dists) + + # A 1D array that, for each sample, holds the index of the cluster with the smallest distance. + # In other words, the array holds the sample's cluster index. + cluster_indices = numpy.argmin(all_clusters_dists, axis=0) + for clust_idx in range(num_clusters): + clusters.append(numpy.where(cluster_indices == clust_idx)[0]) + # Calculate the sum of distances for the cluster. + if len(clusters[clust_idx]) == 0: + # In case the cluster is empty (i.e. has zero samples). + clusters_sum_dist.append(0) + else: + # When the cluster is not empty (i.e. has at least 1 sample). + clusters_sum_dist.append(numpy.sum(all_clusters_dists[clust_idx, clusters[clust_idx]])) + # clusters_sum_dist.append(numpy.sum(euclidean_distance(data[clusters[clust_idx], :], cluster_centers[clust_idx]))) + + clusters_sum_dist = numpy.array(clusters_sum_dist) + + return cluster_centers, all_clusters_dists, cluster_indices, clusters, clusters_sum_dist + +def fitness_func(ga_instance, solution, solution_idx): + _, _, _, _, clusters_sum_dist = cluster_data(solution, solution_idx) + + # The tiny value 0.00000001 is added to the denominator in case the average distance is 0. + fitness = 1.0 / (numpy.sum(clusters_sum_dist) + 0.00000001) + + return fitness + +num_clusters = 3 +feature_vector_length = data.shape[1] +num_genes = num_clusters * feature_vector_length + +ga_instance = pygad.GA(num_generations=100, + sol_per_pop=10, + init_range_low=0, + init_range_high=20, + num_parents_mating=5, + keep_parents=2, + num_genes=num_genes, + fitness_func=fitness_func, + suppress_warnings=True) + +ga_instance.run() + +best_solution, best_solution_fitness, best_solution_idx = ga_instance.best_solution() +print(f"Best solution is {best_solution}") +print(f"Fitness of the best solution is {best_solution_fitness}") +print(f"Best solution found after {ga_instance.best_solution_generation} generations") + +cluster_centers, all_clusters_dists, cluster_indices, clusters, clusters_sum_dist = cluster_data(best_solution, best_solution_idx) + +for cluster_idx in range(num_clusters): + cluster_x = data[clusters[cluster_idx], 0] + cluster_y = data[clusters[cluster_idx], 1] + matplotlib.pyplot.scatter(cluster_x, cluster_y) + matplotlib.pyplot.scatter(cluster_centers[cluster_idx, 0], cluster_centers[cluster_idx, 1], linewidths=5) +matplotlib.pyplot.title("Clustering using PyGAD") +matplotlib.pyplot.show() diff --git a/examples/cnn/example_image_classification.py b/examples/cnn/example_image_classification.py new file mode 100644 index 00000000..13347ec1 --- /dev/null +++ b/examples/cnn/example_image_classification.py @@ -0,0 +1,72 @@ +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("../data/dataset_inputs.npy") +train_outputs = numpy.load("../data/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/examples/data/Fish.csv b/examples/data/Fish.csv new file mode 100644 index 00000000..a0bbd322 --- /dev/null +++ b/examples/data/Fish.csv @@ -0,0 +1,160 @@ +Species,Weight,Length1,Length2,Length3,Height,Width +Bream,242,23.2,25.4,30,11.52,4.02 +Bream,290,24,26.3,31.2,12.48,4.3056 +Bream,340,23.9,26.5,31.1,12.3778,4.6961 +Bream,363,26.3,29,33.5,12.73,4.4555 +Bream,430,26.5,29,34,12.444,5.134 +Bream,450,26.8,29.7,34.7,13.6024,4.9274 +Bream,500,26.8,29.7,34.5,14.1795,5.2785 +Bream,390,27.6,30,35,12.67,4.69 +Bream,450,27.6,30,35.1,14.0049,4.8438 +Bream,500,28.5,30.7,36.2,14.2266,4.9594 +Bream,475,28.4,31,36.2,14.2628,5.1042 +Bream,500,28.7,31,36.2,14.3714,4.8146 +Bream,500,29.1,31.5,36.4,13.7592,4.368 +Bream,340,29.5,32,37.3,13.9129,5.0728 +Bream,600,29.4,32,37.2,14.9544,5.1708 +Bream,600,29.4,32,37.2,15.438,5.58 +Bream,700,30.4,33,38.3,14.8604,5.2854 +Bream,700,30.4,33,38.5,14.938,5.1975 +Bream,610,30.9,33.5,38.6,15.633,5.1338 +Bream,650,31,33.5,38.7,14.4738,5.7276 +Bream,575,31.3,34,39.5,15.1285,5.5695 +Bream,685,31.4,34,39.2,15.9936,5.3704 +Bream,620,31.5,34.5,39.7,15.5227,5.2801 +Bream,680,31.8,35,40.6,15.4686,6.1306 +Bream,700,31.9,35,40.5,16.2405,5.589 +Bream,725,31.8,35,40.9,16.36,6.0532 +Bream,720,32,35,40.6,16.3618,6.09 +Bream,714,32.7,36,41.5,16.517,5.8515 +Bream,850,32.8,36,41.6,16.8896,6.1984 +Bream,1000,33.5,37,42.6,18.957,6.603 +Bream,920,35,38.5,44.1,18.0369,6.3063 +Bream,955,35,38.5,44,18.084,6.292 +Bream,925,36.2,39.5,45.3,18.7542,6.7497 +Bream,975,37.4,41,45.9,18.6354,6.7473 +Bream,950,38,41,46.5,17.6235,6.3705 +Roach,40,12.9,14.1,16.2,4.1472,2.268 +Roach,69,16.5,18.2,20.3,5.2983,2.8217 +Roach,78,17.5,18.8,21.2,5.5756,2.9044 +Roach,87,18.2,19.8,22.2,5.6166,3.1746 +Roach,120,18.6,20,22.2,6.216,3.5742 +Roach,0,19,20.5,22.8,6.4752,3.3516 +Roach,110,19.1,20.8,23.1,6.1677,3.3957 +Roach,120,19.4,21,23.7,6.1146,3.2943 +Roach,150,20.4,22,24.7,5.8045,3.7544 +Roach,145,20.5,22,24.3,6.6339,3.5478 +Roach,160,20.5,22.5,25.3,7.0334,3.8203 +Roach,140,21,22.5,25,6.55,3.325 +Roach,160,21.1,22.5,25,6.4,3.8 +Roach,169,22,24,27.2,7.5344,3.8352 +Roach,161,22,23.4,26.7,6.9153,3.6312 +Roach,200,22.1,23.5,26.8,7.3968,4.1272 +Roach,180,23.6,25.2,27.9,7.0866,3.906 +Roach,290,24,26,29.2,8.8768,4.4968 +Roach,272,25,27,30.6,8.568,4.7736 +Roach,390,29.5,31.7,35,9.485,5.355 +Whitefish,270,23.6,26,28.7,8.3804,4.2476 +Whitefish,270,24.1,26.5,29.3,8.1454,4.2485 +Whitefish,306,25.6,28,30.8,8.778,4.6816 +Whitefish,540,28.5,31,34,10.744,6.562 +Whitefish,800,33.7,36.4,39.6,11.7612,6.5736 +Whitefish,1000,37.3,40,43.5,12.354,6.525 +Parkki,55,13.5,14.7,16.5,6.8475,2.3265 +Parkki,60,14.3,15.5,17.4,6.5772,2.3142 +Parkki,90,16.3,17.7,19.8,7.4052,2.673 +Parkki,120,17.5,19,21.3,8.3922,2.9181 +Parkki,150,18.4,20,22.4,8.8928,3.2928 +Parkki,140,19,20.7,23.2,8.5376,3.2944 +Parkki,170,19,20.7,23.2,9.396,3.4104 +Parkki,145,19.8,21.5,24.1,9.7364,3.1571 +Parkki,200,21.2,23,25.8,10.3458,3.6636 +Parkki,273,23,25,28,11.088,4.144 +Parkki,300,24,26,29,11.368,4.234 +Perch,5.9,7.5,8.4,8.8,2.112,1.408 +Perch,32,12.5,13.7,14.7,3.528,1.9992 +Perch,40,13.8,15,16,3.824,2.432 +Perch,51.5,15,16.2,17.2,4.5924,2.6316 +Perch,70,15.7,17.4,18.5,4.588,2.9415 +Perch,100,16.2,18,19.2,5.2224,3.3216 +Perch,78,16.8,18.7,19.4,5.1992,3.1234 +Perch,80,17.2,19,20.2,5.6358,3.0502 +Perch,85,17.8,19.6,20.8,5.1376,3.0368 +Perch,85,18.2,20,21,5.082,2.772 +Perch,110,19,21,22.5,5.6925,3.555 +Perch,115,19,21,22.5,5.9175,3.3075 +Perch,125,19,21,22.5,5.6925,3.6675 +Perch,130,19.3,21.3,22.8,6.384,3.534 +Perch,120,20,22,23.5,6.11,3.4075 +Perch,120,20,22,23.5,5.64,3.525 +Perch,130,20,22,23.5,6.11,3.525 +Perch,135,20,22,23.5,5.875,3.525 +Perch,110,20,22,23.5,5.5225,3.995 +Perch,130,20.5,22.5,24,5.856,3.624 +Perch,150,20.5,22.5,24,6.792,3.624 +Perch,145,20.7,22.7,24.2,5.9532,3.63 +Perch,150,21,23,24.5,5.2185,3.626 +Perch,170,21.5,23.5,25,6.275,3.725 +Perch,225,22,24,25.5,7.293,3.723 +Perch,145,22,24,25.5,6.375,3.825 +Perch,188,22.6,24.6,26.2,6.7334,4.1658 +Perch,180,23,25,26.5,6.4395,3.6835 +Perch,197,23.5,25.6,27,6.561,4.239 +Perch,218,25,26.5,28,7.168,4.144 +Perch,300,25.2,27.3,28.7,8.323,5.1373 +Perch,260,25.4,27.5,28.9,7.1672,4.335 +Perch,265,25.4,27.5,28.9,7.0516,4.335 +Perch,250,25.4,27.5,28.9,7.2828,4.5662 +Perch,250,25.9,28,29.4,7.8204,4.2042 +Perch,300,26.9,28.7,30.1,7.5852,4.6354 +Perch,320,27.8,30,31.6,7.6156,4.7716 +Perch,514,30.5,32.8,34,10.03,6.018 +Perch,556,32,34.5,36.5,10.2565,6.3875 +Perch,840,32.5,35,37.3,11.4884,7.7957 +Perch,685,34,36.5,39,10.881,6.864 +Perch,700,34,36,38.3,10.6091,6.7408 +Perch,700,34.5,37,39.4,10.835,6.2646 +Perch,690,34.6,37,39.3,10.5717,6.3666 +Perch,900,36.5,39,41.4,11.1366,7.4934 +Perch,650,36.5,39,41.4,11.1366,6.003 +Perch,820,36.6,39,41.3,12.4313,7.3514 +Perch,850,36.9,40,42.3,11.9286,7.1064 +Perch,900,37,40,42.5,11.73,7.225 +Perch,1015,37,40,42.4,12.3808,7.4624 +Perch,820,37.1,40,42.5,11.135,6.63 +Perch,1100,39,42,44.6,12.8002,6.8684 +Perch,1000,39.8,43,45.2,11.9328,7.2772 +Perch,1100,40.1,43,45.5,12.5125,7.4165 +Perch,1000,40.2,43.5,46,12.604,8.142 +Perch,1000,41.1,44,46.6,12.4888,7.5958 +Pike,200,30,32.3,34.8,5.568,3.3756 +Pike,300,31.7,34,37.8,5.7078,4.158 +Pike,300,32.7,35,38.8,5.9364,4.3844 +Pike,300,34.8,37.3,39.8,6.2884,4.0198 +Pike,430,35.5,38,40.5,7.29,4.5765 +Pike,345,36,38.5,41,6.396,3.977 +Pike,456,40,42.5,45.5,7.28,4.3225 +Pike,510,40,42.5,45.5,6.825,4.459 +Pike,540,40.1,43,45.8,7.786,5.1296 +Pike,500,42,45,48,6.96,4.896 +Pike,567,43.2,46,48.7,7.792,4.87 +Pike,770,44.8,48,51.2,7.68,5.376 +Pike,950,48.3,51.7,55.1,8.9262,6.1712 +Pike,1250,52,56,59.7,10.6863,6.9849 +Pike,1600,56,60,64,9.6,6.144 +Pike,1550,56,60,64,9.6,6.144 +Pike,1650,59,63.4,68,10.812,7.48 +Smelt,6.7,9.3,9.8,10.8,1.7388,1.0476 +Smelt,7.5,10,10.5,11.6,1.972,1.16 +Smelt,7,10.1,10.6,11.6,1.7284,1.1484 +Smelt,9.7,10.4,11,12,2.196,1.38 +Smelt,9.8,10.7,11.2,12.4,2.0832,1.2772 +Smelt,8.7,10.8,11.3,12.6,1.9782,1.2852 +Smelt,10,11.3,11.8,13.1,2.2139,1.2838 +Smelt,9.9,11.3,11.8,13.1,2.2139,1.1659 +Smelt,9.8,11.4,12,13.2,2.2044,1.1484 +Smelt,12.2,11.5,12.2,13.4,2.0904,1.3936 +Smelt,13.4,11.7,12.4,13.5,2.43,1.269 +Smelt,12.2,12.1,13,13.8,2.277,1.2558 +Smelt,19.7,13.2,14.3,15.2,2.8728,2.0672 +Smelt,19.9,13.8,15,16.2,2.9322,1.8792 diff --git a/examples/data/Fruit360/apple/0_100.jpg b/examples/data/Fruit360/apple/0_100.jpg new file mode 100644 index 00000000..c0dee8fa Binary files /dev/null and b/examples/data/Fruit360/apple/0_100.jpg differ diff --git a/examples/data/Fruit360/apple/100_100.jpg b/examples/data/Fruit360/apple/100_100.jpg new file mode 100644 index 00000000..0fa24cd4 Binary files /dev/null and b/examples/data/Fruit360/apple/100_100.jpg differ diff --git a/examples/data/Fruit360/apple/101_100.jpg b/examples/data/Fruit360/apple/101_100.jpg new file mode 100644 index 00000000..041dc837 Binary files /dev/null and b/examples/data/Fruit360/apple/101_100.jpg differ diff --git a/examples/data/Fruit360/apple/102_100.jpg b/examples/data/Fruit360/apple/102_100.jpg new file mode 100644 index 00000000..a673cbfb Binary files /dev/null and b/examples/data/Fruit360/apple/102_100.jpg differ diff --git a/examples/data/Fruit360/apple/103_100.jpg b/examples/data/Fruit360/apple/103_100.jpg new file mode 100644 index 00000000..75be237b Binary files /dev/null and b/examples/data/Fruit360/apple/103_100.jpg differ diff --git a/examples/data/Fruit360/apple/104_100.jpg b/examples/data/Fruit360/apple/104_100.jpg new file mode 100644 index 00000000..12b16928 Binary files /dev/null and b/examples/data/Fruit360/apple/104_100.jpg differ diff --git a/examples/data/Fruit360/apple/105_100.jpg b/examples/data/Fruit360/apple/105_100.jpg new file mode 100644 index 00000000..d277484e Binary files /dev/null and b/examples/data/Fruit360/apple/105_100.jpg differ diff --git a/examples/data/Fruit360/apple/106_100.jpg b/examples/data/Fruit360/apple/106_100.jpg new file mode 100644 index 00000000..2c650148 Binary files /dev/null and b/examples/data/Fruit360/apple/106_100.jpg differ diff --git a/examples/data/Fruit360/apple/107_100.jpg b/examples/data/Fruit360/apple/107_100.jpg new file mode 100644 index 00000000..80cffdf1 Binary files /dev/null and b/examples/data/Fruit360/apple/107_100.jpg differ diff --git a/examples/data/Fruit360/apple/108_100.jpg b/examples/data/Fruit360/apple/108_100.jpg new file mode 100644 index 00000000..a91e833d Binary files /dev/null and b/examples/data/Fruit360/apple/108_100.jpg differ diff --git a/examples/data/Fruit360/apple/109_100.jpg b/examples/data/Fruit360/apple/109_100.jpg new file mode 100644 index 00000000..bbfda498 Binary files /dev/null and b/examples/data/Fruit360/apple/109_100.jpg differ diff --git a/examples/data/Fruit360/apple/10_100.jpg b/examples/data/Fruit360/apple/10_100.jpg new file mode 100644 index 00000000..988971a2 Binary files /dev/null and b/examples/data/Fruit360/apple/10_100.jpg differ diff --git a/examples/data/Fruit360/apple/110_100.jpg b/examples/data/Fruit360/apple/110_100.jpg new file mode 100644 index 00000000..f8071891 Binary files /dev/null and b/examples/data/Fruit360/apple/110_100.jpg differ diff --git a/examples/data/Fruit360/apple/111_100.jpg b/examples/data/Fruit360/apple/111_100.jpg new file mode 100644 index 00000000..218e71a0 Binary files /dev/null and b/examples/data/Fruit360/apple/111_100.jpg differ diff --git a/examples/data/Fruit360/apple/112_100.jpg b/examples/data/Fruit360/apple/112_100.jpg new file mode 100644 index 00000000..54d192a0 Binary files /dev/null and b/examples/data/Fruit360/apple/112_100.jpg differ diff --git a/examples/data/Fruit360/apple/113_100.jpg b/examples/data/Fruit360/apple/113_100.jpg new file mode 100644 index 00000000..a0fa8d97 Binary files /dev/null and b/examples/data/Fruit360/apple/113_100.jpg differ diff --git a/examples/data/Fruit360/apple/114_100.jpg b/examples/data/Fruit360/apple/114_100.jpg new file mode 100644 index 00000000..ab2c682e Binary files /dev/null and b/examples/data/Fruit360/apple/114_100.jpg differ diff --git a/examples/data/Fruit360/apple/115_100.jpg b/examples/data/Fruit360/apple/115_100.jpg new file mode 100644 index 00000000..de47f01e Binary files /dev/null and b/examples/data/Fruit360/apple/115_100.jpg differ diff --git a/examples/data/Fruit360/apple/116_100.jpg b/examples/data/Fruit360/apple/116_100.jpg new file mode 100644 index 00000000..c94ade64 Binary files /dev/null and b/examples/data/Fruit360/apple/116_100.jpg differ diff --git a/examples/data/Fruit360/apple/117_100.jpg b/examples/data/Fruit360/apple/117_100.jpg new file mode 100644 index 00000000..ec49e33f Binary files /dev/null and b/examples/data/Fruit360/apple/117_100.jpg differ diff --git a/examples/data/Fruit360/apple/118_100.jpg b/examples/data/Fruit360/apple/118_100.jpg new file mode 100644 index 00000000..7d475075 Binary files /dev/null and b/examples/data/Fruit360/apple/118_100.jpg differ diff --git a/examples/data/Fruit360/apple/119_100.jpg b/examples/data/Fruit360/apple/119_100.jpg new file mode 100644 index 00000000..cacae9d1 Binary files /dev/null and b/examples/data/Fruit360/apple/119_100.jpg differ diff --git a/examples/data/Fruit360/apple/11_100.jpg b/examples/data/Fruit360/apple/11_100.jpg new file mode 100644 index 00000000..9a7f1336 Binary files /dev/null and b/examples/data/Fruit360/apple/11_100.jpg differ diff --git a/examples/data/Fruit360/apple/120_100.jpg b/examples/data/Fruit360/apple/120_100.jpg new file mode 100644 index 00000000..f676ff47 Binary files /dev/null and b/examples/data/Fruit360/apple/120_100.jpg differ diff --git a/examples/data/Fruit360/apple/121_100.jpg b/examples/data/Fruit360/apple/121_100.jpg new file mode 100644 index 00000000..8750f413 Binary files /dev/null and b/examples/data/Fruit360/apple/121_100.jpg differ diff --git a/examples/data/Fruit360/apple/122_100.jpg b/examples/data/Fruit360/apple/122_100.jpg new file mode 100644 index 00000000..6a32024d Binary files /dev/null and b/examples/data/Fruit360/apple/122_100.jpg differ diff --git a/examples/data/Fruit360/apple/123_100.jpg b/examples/data/Fruit360/apple/123_100.jpg new file mode 100644 index 00000000..4f1efd59 Binary files /dev/null and b/examples/data/Fruit360/apple/123_100.jpg differ diff --git a/examples/data/Fruit360/apple/124_100.jpg b/examples/data/Fruit360/apple/124_100.jpg new file mode 100644 index 00000000..df313539 Binary files /dev/null and b/examples/data/Fruit360/apple/124_100.jpg differ diff --git a/examples/data/Fruit360/apple/125_100.jpg b/examples/data/Fruit360/apple/125_100.jpg new file mode 100644 index 00000000..ae83255b Binary files /dev/null and b/examples/data/Fruit360/apple/125_100.jpg differ diff --git a/examples/data/Fruit360/apple/126_100.jpg b/examples/data/Fruit360/apple/126_100.jpg new file mode 100644 index 00000000..7234c186 Binary files /dev/null and b/examples/data/Fruit360/apple/126_100.jpg differ diff --git a/examples/data/Fruit360/apple/127_100.jpg b/examples/data/Fruit360/apple/127_100.jpg new file mode 100644 index 00000000..1c99075c Binary files /dev/null and b/examples/data/Fruit360/apple/127_100.jpg differ diff --git a/examples/data/Fruit360/apple/128_100.jpg b/examples/data/Fruit360/apple/128_100.jpg new file mode 100644 index 00000000..e0213ae8 Binary files /dev/null and b/examples/data/Fruit360/apple/128_100.jpg differ diff --git a/examples/data/Fruit360/apple/129_100.jpg b/examples/data/Fruit360/apple/129_100.jpg new file mode 100644 index 00000000..f9ee9664 Binary files /dev/null and b/examples/data/Fruit360/apple/129_100.jpg differ diff --git a/examples/data/Fruit360/apple/12_100.jpg b/examples/data/Fruit360/apple/12_100.jpg new file mode 100644 index 00000000..54d3950c Binary files /dev/null and b/examples/data/Fruit360/apple/12_100.jpg differ diff --git a/examples/data/Fruit360/apple/130_100.jpg b/examples/data/Fruit360/apple/130_100.jpg new file mode 100644 index 00000000..972a060e Binary files /dev/null and b/examples/data/Fruit360/apple/130_100.jpg differ diff --git a/examples/data/Fruit360/apple/131_100.jpg b/examples/data/Fruit360/apple/131_100.jpg new file mode 100644 index 00000000..76aff803 Binary files /dev/null and b/examples/data/Fruit360/apple/131_100.jpg differ diff --git a/examples/data/Fruit360/apple/132_100.jpg b/examples/data/Fruit360/apple/132_100.jpg new file mode 100644 index 00000000..3557f96e Binary files /dev/null and b/examples/data/Fruit360/apple/132_100.jpg differ diff --git a/examples/data/Fruit360/apple/133_100.jpg b/examples/data/Fruit360/apple/133_100.jpg new file mode 100644 index 00000000..d3002a6f Binary files /dev/null and b/examples/data/Fruit360/apple/133_100.jpg differ diff --git a/examples/data/Fruit360/apple/134_100.jpg b/examples/data/Fruit360/apple/134_100.jpg new file mode 100644 index 00000000..1bc291ac Binary files /dev/null and b/examples/data/Fruit360/apple/134_100.jpg differ diff --git a/examples/data/Fruit360/apple/135_100.jpg b/examples/data/Fruit360/apple/135_100.jpg new file mode 100644 index 00000000..893e7087 Binary files /dev/null and b/examples/data/Fruit360/apple/135_100.jpg differ diff --git a/examples/data/Fruit360/apple/136_100.jpg b/examples/data/Fruit360/apple/136_100.jpg new file mode 100644 index 00000000..38b72138 Binary files /dev/null and b/examples/data/Fruit360/apple/136_100.jpg differ diff --git a/examples/data/Fruit360/apple/137_100.jpg b/examples/data/Fruit360/apple/137_100.jpg new file mode 100644 index 00000000..d0ed60fd Binary files /dev/null and b/examples/data/Fruit360/apple/137_100.jpg differ diff --git a/examples/data/Fruit360/apple/138_100.jpg b/examples/data/Fruit360/apple/138_100.jpg new file mode 100644 index 00000000..4f6daa97 Binary files /dev/null and b/examples/data/Fruit360/apple/138_100.jpg differ diff --git a/examples/data/Fruit360/apple/139_100.jpg b/examples/data/Fruit360/apple/139_100.jpg new file mode 100644 index 00000000..a0c4cd50 Binary files /dev/null and b/examples/data/Fruit360/apple/139_100.jpg differ diff --git a/examples/data/Fruit360/apple/13_100.jpg b/examples/data/Fruit360/apple/13_100.jpg new file mode 100644 index 00000000..2307d9bf Binary files /dev/null and b/examples/data/Fruit360/apple/13_100.jpg differ diff --git a/examples/data/Fruit360/apple/140_100.jpg b/examples/data/Fruit360/apple/140_100.jpg new file mode 100644 index 00000000..4d08dba1 Binary files /dev/null and b/examples/data/Fruit360/apple/140_100.jpg differ diff --git a/examples/data/Fruit360/apple/141_100.jpg b/examples/data/Fruit360/apple/141_100.jpg new file mode 100644 index 00000000..508c791a Binary files /dev/null and b/examples/data/Fruit360/apple/141_100.jpg differ diff --git a/examples/data/Fruit360/apple/142_100.jpg b/examples/data/Fruit360/apple/142_100.jpg new file mode 100644 index 00000000..2740085f Binary files /dev/null and b/examples/data/Fruit360/apple/142_100.jpg differ diff --git a/examples/data/Fruit360/apple/143_100.jpg b/examples/data/Fruit360/apple/143_100.jpg new file mode 100644 index 00000000..811fd3ac Binary files /dev/null and b/examples/data/Fruit360/apple/143_100.jpg differ diff --git a/examples/data/Fruit360/apple/144_100.jpg b/examples/data/Fruit360/apple/144_100.jpg new file mode 100644 index 00000000..37b05c08 Binary files /dev/null and b/examples/data/Fruit360/apple/144_100.jpg differ diff --git a/examples/data/Fruit360/apple/145_100.jpg b/examples/data/Fruit360/apple/145_100.jpg new file mode 100644 index 00000000..eb673dc9 Binary files /dev/null and b/examples/data/Fruit360/apple/145_100.jpg differ diff --git a/examples/data/Fruit360/apple/146_100.jpg b/examples/data/Fruit360/apple/146_100.jpg new file mode 100644 index 00000000..298d9ef3 Binary files /dev/null and b/examples/data/Fruit360/apple/146_100.jpg differ diff --git a/examples/data/Fruit360/apple/147_100.jpg b/examples/data/Fruit360/apple/147_100.jpg new file mode 100644 index 00000000..f0624850 Binary files /dev/null and b/examples/data/Fruit360/apple/147_100.jpg differ diff --git a/examples/data/Fruit360/apple/148_100.jpg b/examples/data/Fruit360/apple/148_100.jpg new file mode 100644 index 00000000..2c8da9b5 Binary files /dev/null and b/examples/data/Fruit360/apple/148_100.jpg differ diff --git a/examples/data/Fruit360/apple/149_100.jpg b/examples/data/Fruit360/apple/149_100.jpg new file mode 100644 index 00000000..7eaeff54 Binary files /dev/null and b/examples/data/Fruit360/apple/149_100.jpg differ diff --git a/examples/data/Fruit360/apple/14_100.jpg b/examples/data/Fruit360/apple/14_100.jpg new file mode 100644 index 00000000..603ad529 Binary files /dev/null and b/examples/data/Fruit360/apple/14_100.jpg differ diff --git a/examples/data/Fruit360/apple/150_100.jpg b/examples/data/Fruit360/apple/150_100.jpg new file mode 100644 index 00000000..e9afe6ff Binary files /dev/null and b/examples/data/Fruit360/apple/150_100.jpg differ diff --git a/examples/data/Fruit360/apple/151_100.jpg b/examples/data/Fruit360/apple/151_100.jpg new file mode 100644 index 00000000..1e1a6cf1 Binary files /dev/null and b/examples/data/Fruit360/apple/151_100.jpg differ diff --git a/examples/data/Fruit360/apple/152_100.jpg b/examples/data/Fruit360/apple/152_100.jpg new file mode 100644 index 00000000..89d49014 Binary files /dev/null and b/examples/data/Fruit360/apple/152_100.jpg differ diff --git a/examples/data/Fruit360/apple/153_100.jpg b/examples/data/Fruit360/apple/153_100.jpg new file mode 100644 index 00000000..c16bea91 Binary files /dev/null and b/examples/data/Fruit360/apple/153_100.jpg differ diff --git a/examples/data/Fruit360/apple/154_100.jpg b/examples/data/Fruit360/apple/154_100.jpg new file mode 100644 index 00000000..14d05403 Binary files /dev/null and b/examples/data/Fruit360/apple/154_100.jpg differ diff --git a/examples/data/Fruit360/apple/155_100.jpg b/examples/data/Fruit360/apple/155_100.jpg new file mode 100644 index 00000000..5e002790 Binary files /dev/null and b/examples/data/Fruit360/apple/155_100.jpg differ diff --git a/examples/data/Fruit360/apple/156_100.jpg b/examples/data/Fruit360/apple/156_100.jpg new file mode 100644 index 00000000..2e1c7153 Binary files /dev/null and b/examples/data/Fruit360/apple/156_100.jpg differ diff --git a/examples/data/Fruit360/apple/157_100.jpg b/examples/data/Fruit360/apple/157_100.jpg new file mode 100644 index 00000000..1888e7ff Binary files /dev/null and b/examples/data/Fruit360/apple/157_100.jpg differ diff --git a/examples/data/Fruit360/apple/158_100.jpg b/examples/data/Fruit360/apple/158_100.jpg new file mode 100644 index 00000000..464fceaf Binary files /dev/null and b/examples/data/Fruit360/apple/158_100.jpg differ diff --git a/examples/data/Fruit360/apple/159_100.jpg b/examples/data/Fruit360/apple/159_100.jpg new file mode 100644 index 00000000..363c0973 Binary files /dev/null and b/examples/data/Fruit360/apple/159_100.jpg differ diff --git a/examples/data/Fruit360/apple/15_100.jpg b/examples/data/Fruit360/apple/15_100.jpg new file mode 100644 index 00000000..cedb6113 Binary files /dev/null and b/examples/data/Fruit360/apple/15_100.jpg differ diff --git a/examples/data/Fruit360/apple/160_100.jpg b/examples/data/Fruit360/apple/160_100.jpg new file mode 100644 index 00000000..66f1a37f Binary files /dev/null and b/examples/data/Fruit360/apple/160_100.jpg differ diff --git a/examples/data/Fruit360/apple/161_100.jpg b/examples/data/Fruit360/apple/161_100.jpg new file mode 100644 index 00000000..84a9db8f Binary files /dev/null and b/examples/data/Fruit360/apple/161_100.jpg differ diff --git a/examples/data/Fruit360/apple/162_100.jpg b/examples/data/Fruit360/apple/162_100.jpg new file mode 100644 index 00000000..689b9993 Binary files /dev/null and b/examples/data/Fruit360/apple/162_100.jpg differ diff --git a/examples/data/Fruit360/apple/163_100.jpg b/examples/data/Fruit360/apple/163_100.jpg new file mode 100644 index 00000000..4d25272b Binary files /dev/null and b/examples/data/Fruit360/apple/163_100.jpg differ diff --git a/examples/data/Fruit360/apple/164_100.jpg b/examples/data/Fruit360/apple/164_100.jpg new file mode 100644 index 00000000..6501f746 Binary files /dev/null and b/examples/data/Fruit360/apple/164_100.jpg differ diff --git a/examples/data/Fruit360/apple/165_100.jpg b/examples/data/Fruit360/apple/165_100.jpg new file mode 100644 index 00000000..8198af35 Binary files /dev/null and b/examples/data/Fruit360/apple/165_100.jpg differ diff --git a/examples/data/Fruit360/apple/166_100.jpg b/examples/data/Fruit360/apple/166_100.jpg new file mode 100644 index 00000000..c491134f Binary files /dev/null and b/examples/data/Fruit360/apple/166_100.jpg differ diff --git a/examples/data/Fruit360/apple/167_100.jpg b/examples/data/Fruit360/apple/167_100.jpg new file mode 100644 index 00000000..44f54acd Binary files /dev/null and b/examples/data/Fruit360/apple/167_100.jpg differ diff --git a/examples/data/Fruit360/apple/168_100.jpg b/examples/data/Fruit360/apple/168_100.jpg new file mode 100644 index 00000000..5075e7ef Binary files /dev/null and b/examples/data/Fruit360/apple/168_100.jpg differ diff --git a/examples/data/Fruit360/apple/169_100.jpg b/examples/data/Fruit360/apple/169_100.jpg new file mode 100644 index 00000000..497edef4 Binary files /dev/null and b/examples/data/Fruit360/apple/169_100.jpg differ diff --git a/examples/data/Fruit360/apple/16_100.jpg b/examples/data/Fruit360/apple/16_100.jpg new file mode 100644 index 00000000..5e348ad8 Binary files /dev/null and b/examples/data/Fruit360/apple/16_100.jpg differ diff --git a/examples/data/Fruit360/apple/170_100.jpg b/examples/data/Fruit360/apple/170_100.jpg new file mode 100644 index 00000000..7ef46f6f Binary files /dev/null and b/examples/data/Fruit360/apple/170_100.jpg differ diff --git a/examples/data/Fruit360/apple/171_100.jpg b/examples/data/Fruit360/apple/171_100.jpg new file mode 100644 index 00000000..b190fbf9 Binary files /dev/null and b/examples/data/Fruit360/apple/171_100.jpg differ diff --git a/examples/data/Fruit360/apple/172_100.jpg b/examples/data/Fruit360/apple/172_100.jpg new file mode 100644 index 00000000..ab87ccc3 Binary files /dev/null and b/examples/data/Fruit360/apple/172_100.jpg differ diff --git a/examples/data/Fruit360/apple/173_100.jpg b/examples/data/Fruit360/apple/173_100.jpg new file mode 100644 index 00000000..e9cdb6fb Binary files /dev/null and b/examples/data/Fruit360/apple/173_100.jpg differ diff --git a/examples/data/Fruit360/apple/174_100.jpg b/examples/data/Fruit360/apple/174_100.jpg new file mode 100644 index 00000000..e8af0f46 Binary files /dev/null and b/examples/data/Fruit360/apple/174_100.jpg differ diff --git a/examples/data/Fruit360/apple/175_100.jpg b/examples/data/Fruit360/apple/175_100.jpg new file mode 100644 index 00000000..e63eec67 Binary files /dev/null and b/examples/data/Fruit360/apple/175_100.jpg differ diff --git a/examples/data/Fruit360/apple/176_100.jpg b/examples/data/Fruit360/apple/176_100.jpg new file mode 100644 index 00000000..23addf02 Binary files /dev/null and b/examples/data/Fruit360/apple/176_100.jpg differ diff --git a/examples/data/Fruit360/apple/177_100.jpg b/examples/data/Fruit360/apple/177_100.jpg new file mode 100644 index 00000000..8ccd0759 Binary files /dev/null and b/examples/data/Fruit360/apple/177_100.jpg differ diff --git a/examples/data/Fruit360/apple/178_100.jpg b/examples/data/Fruit360/apple/178_100.jpg new file mode 100644 index 00000000..833b46c6 Binary files /dev/null and b/examples/data/Fruit360/apple/178_100.jpg differ diff --git a/examples/data/Fruit360/apple/179_100.jpg b/examples/data/Fruit360/apple/179_100.jpg new file mode 100644 index 00000000..1d70f850 Binary files /dev/null and b/examples/data/Fruit360/apple/179_100.jpg differ diff --git a/examples/data/Fruit360/apple/17_100.jpg b/examples/data/Fruit360/apple/17_100.jpg new file mode 100644 index 00000000..f1fd2855 Binary files /dev/null and b/examples/data/Fruit360/apple/17_100.jpg differ diff --git a/examples/data/Fruit360/apple/180_100.jpg b/examples/data/Fruit360/apple/180_100.jpg new file mode 100644 index 00000000..26a3b7c1 Binary files /dev/null and b/examples/data/Fruit360/apple/180_100.jpg differ diff --git a/examples/data/Fruit360/apple/181_100.jpg b/examples/data/Fruit360/apple/181_100.jpg new file mode 100644 index 00000000..ee09a266 Binary files /dev/null and b/examples/data/Fruit360/apple/181_100.jpg differ diff --git a/examples/data/Fruit360/apple/182_100.jpg b/examples/data/Fruit360/apple/182_100.jpg new file mode 100644 index 00000000..7e334543 Binary files /dev/null and b/examples/data/Fruit360/apple/182_100.jpg differ diff --git a/examples/data/Fruit360/apple/183_100.jpg b/examples/data/Fruit360/apple/183_100.jpg new file mode 100644 index 00000000..65b7b597 Binary files /dev/null and b/examples/data/Fruit360/apple/183_100.jpg differ diff --git a/examples/data/Fruit360/apple/184_100.jpg b/examples/data/Fruit360/apple/184_100.jpg new file mode 100644 index 00000000..dd71543a Binary files /dev/null and b/examples/data/Fruit360/apple/184_100.jpg differ diff --git a/examples/data/Fruit360/apple/185_100.jpg b/examples/data/Fruit360/apple/185_100.jpg new file mode 100644 index 00000000..92a84bfd Binary files /dev/null and b/examples/data/Fruit360/apple/185_100.jpg differ diff --git a/examples/data/Fruit360/apple/186_100.jpg b/examples/data/Fruit360/apple/186_100.jpg new file mode 100644 index 00000000..d3140700 Binary files /dev/null and b/examples/data/Fruit360/apple/186_100.jpg differ diff --git a/examples/data/Fruit360/apple/187_100.jpg b/examples/data/Fruit360/apple/187_100.jpg new file mode 100644 index 00000000..a6451792 Binary files /dev/null and b/examples/data/Fruit360/apple/187_100.jpg differ diff --git a/examples/data/Fruit360/apple/188_100.jpg b/examples/data/Fruit360/apple/188_100.jpg new file mode 100644 index 00000000..a359d1df Binary files /dev/null and b/examples/data/Fruit360/apple/188_100.jpg differ diff --git a/examples/data/Fruit360/apple/189_100.jpg b/examples/data/Fruit360/apple/189_100.jpg new file mode 100644 index 00000000..05afbe61 Binary files /dev/null and b/examples/data/Fruit360/apple/189_100.jpg differ diff --git a/examples/data/Fruit360/apple/18_100.jpg b/examples/data/Fruit360/apple/18_100.jpg new file mode 100644 index 00000000..220de227 Binary files /dev/null and b/examples/data/Fruit360/apple/18_100.jpg differ diff --git a/examples/data/Fruit360/apple/190_100.jpg b/examples/data/Fruit360/apple/190_100.jpg new file mode 100644 index 00000000..a334c6e7 Binary files /dev/null and b/examples/data/Fruit360/apple/190_100.jpg differ diff --git a/examples/data/Fruit360/apple/191_100.jpg b/examples/data/Fruit360/apple/191_100.jpg new file mode 100644 index 00000000..810c89fb Binary files /dev/null and b/examples/data/Fruit360/apple/191_100.jpg differ diff --git a/examples/data/Fruit360/apple/192_100.jpg b/examples/data/Fruit360/apple/192_100.jpg new file mode 100644 index 00000000..7e9a6f5d Binary files /dev/null and b/examples/data/Fruit360/apple/192_100.jpg differ diff --git a/examples/data/Fruit360/apple/193_100.jpg b/examples/data/Fruit360/apple/193_100.jpg new file mode 100644 index 00000000..2d9ab13e Binary files /dev/null and b/examples/data/Fruit360/apple/193_100.jpg differ diff --git a/examples/data/Fruit360/apple/194_100.jpg b/examples/data/Fruit360/apple/194_100.jpg new file mode 100644 index 00000000..8a2ed912 Binary files /dev/null and b/examples/data/Fruit360/apple/194_100.jpg differ diff --git a/examples/data/Fruit360/apple/195_100.jpg b/examples/data/Fruit360/apple/195_100.jpg new file mode 100644 index 00000000..80f7825f Binary files /dev/null and b/examples/data/Fruit360/apple/195_100.jpg differ diff --git a/examples/data/Fruit360/apple/196_100.jpg b/examples/data/Fruit360/apple/196_100.jpg new file mode 100644 index 00000000..c5149e50 Binary files /dev/null and b/examples/data/Fruit360/apple/196_100.jpg differ diff --git a/examples/data/Fruit360/apple/197_100.jpg b/examples/data/Fruit360/apple/197_100.jpg new file mode 100644 index 00000000..f4c8e750 Binary files /dev/null and b/examples/data/Fruit360/apple/197_100.jpg differ diff --git a/examples/data/Fruit360/apple/198_100.jpg b/examples/data/Fruit360/apple/198_100.jpg new file mode 100644 index 00000000..af2513d1 Binary files /dev/null and b/examples/data/Fruit360/apple/198_100.jpg differ diff --git a/examples/data/Fruit360/apple/199_100.jpg b/examples/data/Fruit360/apple/199_100.jpg new file mode 100644 index 00000000..787dadf0 Binary files /dev/null and b/examples/data/Fruit360/apple/199_100.jpg differ diff --git a/examples/data/Fruit360/apple/19_100.jpg b/examples/data/Fruit360/apple/19_100.jpg new file mode 100644 index 00000000..c6fffee5 Binary files /dev/null and b/examples/data/Fruit360/apple/19_100.jpg differ diff --git a/examples/data/Fruit360/apple/1_100.jpg b/examples/data/Fruit360/apple/1_100.jpg new file mode 100644 index 00000000..96cbfd77 Binary files /dev/null and b/examples/data/Fruit360/apple/1_100.jpg differ diff --git a/examples/data/Fruit360/apple/200_100.jpg b/examples/data/Fruit360/apple/200_100.jpg new file mode 100644 index 00000000..b28ff367 Binary files /dev/null and b/examples/data/Fruit360/apple/200_100.jpg differ diff --git a/examples/data/Fruit360/apple/201_100.jpg b/examples/data/Fruit360/apple/201_100.jpg new file mode 100644 index 00000000..e5729793 Binary files /dev/null and b/examples/data/Fruit360/apple/201_100.jpg differ diff --git a/examples/data/Fruit360/apple/202_100.jpg b/examples/data/Fruit360/apple/202_100.jpg new file mode 100644 index 00000000..ad54850d Binary files /dev/null and b/examples/data/Fruit360/apple/202_100.jpg differ diff --git a/examples/data/Fruit360/apple/203_100.jpg b/examples/data/Fruit360/apple/203_100.jpg new file mode 100644 index 00000000..79b019e6 Binary files /dev/null and b/examples/data/Fruit360/apple/203_100.jpg differ diff --git a/examples/data/Fruit360/apple/204_100.jpg b/examples/data/Fruit360/apple/204_100.jpg new file mode 100644 index 00000000..8303ba09 Binary files /dev/null and b/examples/data/Fruit360/apple/204_100.jpg differ diff --git a/examples/data/Fruit360/apple/205_100.jpg b/examples/data/Fruit360/apple/205_100.jpg new file mode 100644 index 00000000..7470292c Binary files /dev/null and b/examples/data/Fruit360/apple/205_100.jpg differ diff --git a/examples/data/Fruit360/apple/206_100.jpg b/examples/data/Fruit360/apple/206_100.jpg new file mode 100644 index 00000000..8ac8c710 Binary files /dev/null and b/examples/data/Fruit360/apple/206_100.jpg differ diff --git a/examples/data/Fruit360/apple/207_100.jpg b/examples/data/Fruit360/apple/207_100.jpg new file mode 100644 index 00000000..3876c8c9 Binary files /dev/null and b/examples/data/Fruit360/apple/207_100.jpg differ diff --git a/examples/data/Fruit360/apple/208_100.jpg b/examples/data/Fruit360/apple/208_100.jpg new file mode 100644 index 00000000..4844db98 Binary files /dev/null and b/examples/data/Fruit360/apple/208_100.jpg differ diff --git a/examples/data/Fruit360/apple/209_100.jpg b/examples/data/Fruit360/apple/209_100.jpg new file mode 100644 index 00000000..7a961584 Binary files /dev/null and b/examples/data/Fruit360/apple/209_100.jpg differ diff --git a/examples/data/Fruit360/apple/20_100.jpg b/examples/data/Fruit360/apple/20_100.jpg new file mode 100644 index 00000000..37550094 Binary files /dev/null and b/examples/data/Fruit360/apple/20_100.jpg differ diff --git a/examples/data/Fruit360/apple/210_100.jpg b/examples/data/Fruit360/apple/210_100.jpg new file mode 100644 index 00000000..4ce8c4b4 Binary files /dev/null and b/examples/data/Fruit360/apple/210_100.jpg differ diff --git a/examples/data/Fruit360/apple/211_100.jpg b/examples/data/Fruit360/apple/211_100.jpg new file mode 100644 index 00000000..811fa109 Binary files /dev/null and b/examples/data/Fruit360/apple/211_100.jpg differ diff --git a/examples/data/Fruit360/apple/212_100.jpg b/examples/data/Fruit360/apple/212_100.jpg new file mode 100644 index 00000000..349ff662 Binary files /dev/null and b/examples/data/Fruit360/apple/212_100.jpg differ diff --git a/examples/data/Fruit360/apple/213_100.jpg b/examples/data/Fruit360/apple/213_100.jpg new file mode 100644 index 00000000..b5b85f70 Binary files /dev/null and b/examples/data/Fruit360/apple/213_100.jpg differ diff --git a/examples/data/Fruit360/apple/214_100.jpg b/examples/data/Fruit360/apple/214_100.jpg new file mode 100644 index 00000000..e836c680 Binary files /dev/null and b/examples/data/Fruit360/apple/214_100.jpg differ diff --git a/examples/data/Fruit360/apple/215_100.jpg b/examples/data/Fruit360/apple/215_100.jpg new file mode 100644 index 00000000..66edf846 Binary files /dev/null and b/examples/data/Fruit360/apple/215_100.jpg differ diff --git a/examples/data/Fruit360/apple/216_100.jpg b/examples/data/Fruit360/apple/216_100.jpg new file mode 100644 index 00000000..1e9ea95d Binary files /dev/null and b/examples/data/Fruit360/apple/216_100.jpg differ diff --git a/examples/data/Fruit360/apple/217_100.jpg b/examples/data/Fruit360/apple/217_100.jpg new file mode 100644 index 00000000..c9dbbf99 Binary files /dev/null and b/examples/data/Fruit360/apple/217_100.jpg differ diff --git a/examples/data/Fruit360/apple/218_100.jpg b/examples/data/Fruit360/apple/218_100.jpg new file mode 100644 index 00000000..33c907ea Binary files /dev/null and b/examples/data/Fruit360/apple/218_100.jpg differ diff --git a/examples/data/Fruit360/apple/219_100.jpg b/examples/data/Fruit360/apple/219_100.jpg new file mode 100644 index 00000000..c66d0315 Binary files /dev/null and b/examples/data/Fruit360/apple/219_100.jpg differ diff --git a/examples/data/Fruit360/apple/21_100.jpg b/examples/data/Fruit360/apple/21_100.jpg new file mode 100644 index 00000000..fe125846 Binary files /dev/null and b/examples/data/Fruit360/apple/21_100.jpg differ diff --git a/examples/data/Fruit360/apple/220_100.jpg b/examples/data/Fruit360/apple/220_100.jpg new file mode 100644 index 00000000..359c35f8 Binary files /dev/null and b/examples/data/Fruit360/apple/220_100.jpg differ diff --git a/examples/data/Fruit360/apple/221_100.jpg b/examples/data/Fruit360/apple/221_100.jpg new file mode 100644 index 00000000..d3d3f42c Binary files /dev/null and b/examples/data/Fruit360/apple/221_100.jpg differ diff --git a/examples/data/Fruit360/apple/222_100.jpg b/examples/data/Fruit360/apple/222_100.jpg new file mode 100644 index 00000000..07fcf06f Binary files /dev/null and b/examples/data/Fruit360/apple/222_100.jpg differ diff --git a/examples/data/Fruit360/apple/223_100.jpg b/examples/data/Fruit360/apple/223_100.jpg new file mode 100644 index 00000000..1f2dbe02 Binary files /dev/null and b/examples/data/Fruit360/apple/223_100.jpg differ diff --git a/examples/data/Fruit360/apple/224_100.jpg b/examples/data/Fruit360/apple/224_100.jpg new file mode 100644 index 00000000..94a7c152 Binary files /dev/null and b/examples/data/Fruit360/apple/224_100.jpg differ diff --git a/examples/data/Fruit360/apple/225_100.jpg b/examples/data/Fruit360/apple/225_100.jpg new file mode 100644 index 00000000..347da0da Binary files /dev/null and b/examples/data/Fruit360/apple/225_100.jpg differ diff --git a/examples/data/Fruit360/apple/226_100.jpg b/examples/data/Fruit360/apple/226_100.jpg new file mode 100644 index 00000000..b332b689 Binary files /dev/null and b/examples/data/Fruit360/apple/226_100.jpg differ diff --git a/examples/data/Fruit360/apple/227_100.jpg b/examples/data/Fruit360/apple/227_100.jpg new file mode 100644 index 00000000..89e4a4b4 Binary files /dev/null and b/examples/data/Fruit360/apple/227_100.jpg differ diff --git a/examples/data/Fruit360/apple/228_100.jpg b/examples/data/Fruit360/apple/228_100.jpg new file mode 100644 index 00000000..25be0e2a Binary files /dev/null and b/examples/data/Fruit360/apple/228_100.jpg differ diff --git a/examples/data/Fruit360/apple/229_100.jpg b/examples/data/Fruit360/apple/229_100.jpg new file mode 100644 index 00000000..52c22d04 Binary files /dev/null and b/examples/data/Fruit360/apple/229_100.jpg differ diff --git a/examples/data/Fruit360/apple/22_100.jpg b/examples/data/Fruit360/apple/22_100.jpg new file mode 100644 index 00000000..0f89d072 Binary files /dev/null and b/examples/data/Fruit360/apple/22_100.jpg differ diff --git a/examples/data/Fruit360/apple/230_100.jpg b/examples/data/Fruit360/apple/230_100.jpg new file mode 100644 index 00000000..05512771 Binary files /dev/null and b/examples/data/Fruit360/apple/230_100.jpg differ diff --git a/examples/data/Fruit360/apple/231_100.jpg b/examples/data/Fruit360/apple/231_100.jpg new file mode 100644 index 00000000..93694167 Binary files /dev/null and b/examples/data/Fruit360/apple/231_100.jpg differ diff --git a/examples/data/Fruit360/apple/232_100.jpg b/examples/data/Fruit360/apple/232_100.jpg new file mode 100644 index 00000000..2f5120df Binary files /dev/null and b/examples/data/Fruit360/apple/232_100.jpg differ diff --git a/examples/data/Fruit360/apple/233_100.jpg b/examples/data/Fruit360/apple/233_100.jpg new file mode 100644 index 00000000..6e03cbab Binary files /dev/null and b/examples/data/Fruit360/apple/233_100.jpg differ diff --git a/examples/data/Fruit360/apple/234_100.jpg b/examples/data/Fruit360/apple/234_100.jpg new file mode 100644 index 00000000..437c872c Binary files /dev/null and b/examples/data/Fruit360/apple/234_100.jpg differ diff --git a/examples/data/Fruit360/apple/235_100.jpg b/examples/data/Fruit360/apple/235_100.jpg new file mode 100644 index 00000000..0e75238b Binary files /dev/null and b/examples/data/Fruit360/apple/235_100.jpg differ diff --git a/examples/data/Fruit360/apple/236_100.jpg b/examples/data/Fruit360/apple/236_100.jpg new file mode 100644 index 00000000..c32be1da Binary files /dev/null and b/examples/data/Fruit360/apple/236_100.jpg differ diff --git a/examples/data/Fruit360/apple/237_100.jpg b/examples/data/Fruit360/apple/237_100.jpg new file mode 100644 index 00000000..d14030b6 Binary files /dev/null and b/examples/data/Fruit360/apple/237_100.jpg differ diff --git a/examples/data/Fruit360/apple/238_100.jpg b/examples/data/Fruit360/apple/238_100.jpg new file mode 100644 index 00000000..bdf9820e Binary files /dev/null and b/examples/data/Fruit360/apple/238_100.jpg differ diff --git a/examples/data/Fruit360/apple/239_100.jpg b/examples/data/Fruit360/apple/239_100.jpg new file mode 100644 index 00000000..63b14cd4 Binary files /dev/null and b/examples/data/Fruit360/apple/239_100.jpg differ diff --git a/examples/data/Fruit360/apple/23_100.jpg b/examples/data/Fruit360/apple/23_100.jpg new file mode 100644 index 00000000..bae47f34 Binary files /dev/null and b/examples/data/Fruit360/apple/23_100.jpg differ diff --git a/examples/data/Fruit360/apple/240_100.jpg b/examples/data/Fruit360/apple/240_100.jpg new file mode 100644 index 00000000..5e071c92 Binary files /dev/null and b/examples/data/Fruit360/apple/240_100.jpg differ diff --git a/examples/data/Fruit360/apple/241_100.jpg b/examples/data/Fruit360/apple/241_100.jpg new file mode 100644 index 00000000..ff9cdc21 Binary files /dev/null and b/examples/data/Fruit360/apple/241_100.jpg differ diff --git a/examples/data/Fruit360/apple/242_100.jpg b/examples/data/Fruit360/apple/242_100.jpg new file mode 100644 index 00000000..dfc6b356 Binary files /dev/null and b/examples/data/Fruit360/apple/242_100.jpg differ diff --git a/examples/data/Fruit360/apple/243_100.jpg b/examples/data/Fruit360/apple/243_100.jpg new file mode 100644 index 00000000..6320ad3f Binary files /dev/null and b/examples/data/Fruit360/apple/243_100.jpg differ diff --git a/examples/data/Fruit360/apple/244_100.jpg b/examples/data/Fruit360/apple/244_100.jpg new file mode 100644 index 00000000..5b74e66f Binary files /dev/null and b/examples/data/Fruit360/apple/244_100.jpg differ diff --git a/examples/data/Fruit360/apple/245_100.jpg b/examples/data/Fruit360/apple/245_100.jpg new file mode 100644 index 00000000..f55c14d3 Binary files /dev/null and b/examples/data/Fruit360/apple/245_100.jpg differ diff --git a/examples/data/Fruit360/apple/246_100.jpg b/examples/data/Fruit360/apple/246_100.jpg new file mode 100644 index 00000000..8f72965d Binary files /dev/null and b/examples/data/Fruit360/apple/246_100.jpg differ diff --git a/examples/data/Fruit360/apple/247_100.jpg b/examples/data/Fruit360/apple/247_100.jpg new file mode 100644 index 00000000..8910753c Binary files /dev/null and b/examples/data/Fruit360/apple/247_100.jpg differ diff --git a/examples/data/Fruit360/apple/248_100.jpg b/examples/data/Fruit360/apple/248_100.jpg new file mode 100644 index 00000000..9095f113 Binary files /dev/null and b/examples/data/Fruit360/apple/248_100.jpg differ diff --git a/examples/data/Fruit360/apple/249_100.jpg b/examples/data/Fruit360/apple/249_100.jpg new file mode 100644 index 00000000..12b68d29 Binary files /dev/null and b/examples/data/Fruit360/apple/249_100.jpg differ diff --git a/examples/data/Fruit360/apple/24_100.jpg b/examples/data/Fruit360/apple/24_100.jpg new file mode 100644 index 00000000..7d7fdcdd Binary files /dev/null and b/examples/data/Fruit360/apple/24_100.jpg differ diff --git a/examples/data/Fruit360/apple/250_100.jpg b/examples/data/Fruit360/apple/250_100.jpg new file mode 100644 index 00000000..7121b696 Binary files /dev/null and b/examples/data/Fruit360/apple/250_100.jpg differ diff --git a/examples/data/Fruit360/apple/251_100.jpg b/examples/data/Fruit360/apple/251_100.jpg new file mode 100644 index 00000000..b12e82f5 Binary files /dev/null and b/examples/data/Fruit360/apple/251_100.jpg differ diff --git a/examples/data/Fruit360/apple/252_100.jpg b/examples/data/Fruit360/apple/252_100.jpg new file mode 100644 index 00000000..b46aaa6a Binary files /dev/null and b/examples/data/Fruit360/apple/252_100.jpg differ diff --git a/examples/data/Fruit360/apple/253_100.jpg b/examples/data/Fruit360/apple/253_100.jpg new file mode 100644 index 00000000..2520bd55 Binary files /dev/null and b/examples/data/Fruit360/apple/253_100.jpg differ diff --git a/examples/data/Fruit360/apple/254_100.jpg b/examples/data/Fruit360/apple/254_100.jpg new file mode 100644 index 00000000..d5d4dc61 Binary files /dev/null and b/examples/data/Fruit360/apple/254_100.jpg differ diff --git a/examples/data/Fruit360/apple/255_100.jpg b/examples/data/Fruit360/apple/255_100.jpg new file mode 100644 index 00000000..fcdbfd7b Binary files /dev/null and b/examples/data/Fruit360/apple/255_100.jpg differ diff --git a/examples/data/Fruit360/apple/256_100.jpg b/examples/data/Fruit360/apple/256_100.jpg new file mode 100644 index 00000000..b10cedf3 Binary files /dev/null and b/examples/data/Fruit360/apple/256_100.jpg differ diff --git a/examples/data/Fruit360/apple/257_100.jpg b/examples/data/Fruit360/apple/257_100.jpg new file mode 100644 index 00000000..0243dfc7 Binary files /dev/null and b/examples/data/Fruit360/apple/257_100.jpg differ diff --git a/examples/data/Fruit360/apple/258_100.jpg b/examples/data/Fruit360/apple/258_100.jpg new file mode 100644 index 00000000..01b7fc51 Binary files /dev/null and b/examples/data/Fruit360/apple/258_100.jpg differ diff --git a/examples/data/Fruit360/apple/259_100.jpg b/examples/data/Fruit360/apple/259_100.jpg new file mode 100644 index 00000000..4050ff26 Binary files /dev/null and b/examples/data/Fruit360/apple/259_100.jpg differ diff --git a/examples/data/Fruit360/apple/25_100.jpg b/examples/data/Fruit360/apple/25_100.jpg new file mode 100644 index 00000000..1e874e86 Binary files /dev/null and b/examples/data/Fruit360/apple/25_100.jpg differ diff --git a/examples/data/Fruit360/apple/260_100.jpg b/examples/data/Fruit360/apple/260_100.jpg new file mode 100644 index 00000000..bdbe6bea Binary files /dev/null and b/examples/data/Fruit360/apple/260_100.jpg differ diff --git a/examples/data/Fruit360/apple/261_100.jpg b/examples/data/Fruit360/apple/261_100.jpg new file mode 100644 index 00000000..68557c0f Binary files /dev/null and b/examples/data/Fruit360/apple/261_100.jpg differ diff --git a/examples/data/Fruit360/apple/262_100.jpg b/examples/data/Fruit360/apple/262_100.jpg new file mode 100644 index 00000000..fafe052e Binary files /dev/null and b/examples/data/Fruit360/apple/262_100.jpg differ diff --git a/examples/data/Fruit360/apple/263_100.jpg b/examples/data/Fruit360/apple/263_100.jpg new file mode 100644 index 00000000..b37d49cc Binary files /dev/null and b/examples/data/Fruit360/apple/263_100.jpg differ diff --git a/examples/data/Fruit360/apple/264_100.jpg b/examples/data/Fruit360/apple/264_100.jpg new file mode 100644 index 00000000..cc795bc6 Binary files /dev/null and b/examples/data/Fruit360/apple/264_100.jpg differ diff --git a/examples/data/Fruit360/apple/265_100.jpg b/examples/data/Fruit360/apple/265_100.jpg new file mode 100644 index 00000000..d3c798cb Binary files /dev/null and b/examples/data/Fruit360/apple/265_100.jpg differ diff --git a/examples/data/Fruit360/apple/266_100.jpg b/examples/data/Fruit360/apple/266_100.jpg new file mode 100644 index 00000000..0752a3e2 Binary files /dev/null and b/examples/data/Fruit360/apple/266_100.jpg differ diff --git a/examples/data/Fruit360/apple/267_100.jpg b/examples/data/Fruit360/apple/267_100.jpg new file mode 100644 index 00000000..6b00eb3e Binary files /dev/null and b/examples/data/Fruit360/apple/267_100.jpg differ diff --git a/examples/data/Fruit360/apple/268_100.jpg b/examples/data/Fruit360/apple/268_100.jpg new file mode 100644 index 00000000..22fa1cc0 Binary files /dev/null and b/examples/data/Fruit360/apple/268_100.jpg differ diff --git a/examples/data/Fruit360/apple/269_100.jpg b/examples/data/Fruit360/apple/269_100.jpg new file mode 100644 index 00000000..21a6b5d2 Binary files /dev/null and b/examples/data/Fruit360/apple/269_100.jpg differ diff --git a/examples/data/Fruit360/apple/26_100.jpg b/examples/data/Fruit360/apple/26_100.jpg new file mode 100644 index 00000000..c0c4ac3b Binary files /dev/null and b/examples/data/Fruit360/apple/26_100.jpg differ diff --git a/examples/data/Fruit360/apple/270_100.jpg b/examples/data/Fruit360/apple/270_100.jpg new file mode 100644 index 00000000..1c2a2e98 Binary files /dev/null and b/examples/data/Fruit360/apple/270_100.jpg differ diff --git a/examples/data/Fruit360/apple/271_100.jpg b/examples/data/Fruit360/apple/271_100.jpg new file mode 100644 index 00000000..b3026999 Binary files /dev/null and b/examples/data/Fruit360/apple/271_100.jpg differ diff --git a/examples/data/Fruit360/apple/272_100.jpg b/examples/data/Fruit360/apple/272_100.jpg new file mode 100644 index 00000000..efd7165f Binary files /dev/null and b/examples/data/Fruit360/apple/272_100.jpg differ diff --git a/examples/data/Fruit360/apple/273_100.jpg b/examples/data/Fruit360/apple/273_100.jpg new file mode 100644 index 00000000..3050c069 Binary files /dev/null and b/examples/data/Fruit360/apple/273_100.jpg differ diff --git a/examples/data/Fruit360/apple/274_100.jpg b/examples/data/Fruit360/apple/274_100.jpg new file mode 100644 index 00000000..8041ff50 Binary files /dev/null and b/examples/data/Fruit360/apple/274_100.jpg differ diff --git a/examples/data/Fruit360/apple/275_100.jpg b/examples/data/Fruit360/apple/275_100.jpg new file mode 100644 index 00000000..f93c7ba4 Binary files /dev/null and b/examples/data/Fruit360/apple/275_100.jpg differ diff --git a/examples/data/Fruit360/apple/276_100.jpg b/examples/data/Fruit360/apple/276_100.jpg new file mode 100644 index 00000000..b583cb2f Binary files /dev/null and b/examples/data/Fruit360/apple/276_100.jpg differ diff --git a/examples/data/Fruit360/apple/277_100.jpg b/examples/data/Fruit360/apple/277_100.jpg new file mode 100644 index 00000000..d8d36216 Binary files /dev/null and b/examples/data/Fruit360/apple/277_100.jpg differ diff --git a/examples/data/Fruit360/apple/278_100.jpg b/examples/data/Fruit360/apple/278_100.jpg new file mode 100644 index 00000000..3f09c684 Binary files /dev/null and b/examples/data/Fruit360/apple/278_100.jpg differ diff --git a/examples/data/Fruit360/apple/279_100.jpg b/examples/data/Fruit360/apple/279_100.jpg new file mode 100644 index 00000000..d8c51613 Binary files /dev/null and b/examples/data/Fruit360/apple/279_100.jpg differ diff --git a/examples/data/Fruit360/apple/27_100.jpg b/examples/data/Fruit360/apple/27_100.jpg new file mode 100644 index 00000000..0a073861 Binary files /dev/null and b/examples/data/Fruit360/apple/27_100.jpg differ diff --git a/examples/data/Fruit360/apple/280_100.jpg b/examples/data/Fruit360/apple/280_100.jpg new file mode 100644 index 00000000..48f3f8b4 Binary files /dev/null and b/examples/data/Fruit360/apple/280_100.jpg differ diff --git a/examples/data/Fruit360/apple/281_100.jpg b/examples/data/Fruit360/apple/281_100.jpg new file mode 100644 index 00000000..cc29f063 Binary files /dev/null and b/examples/data/Fruit360/apple/281_100.jpg differ diff --git a/examples/data/Fruit360/apple/282_100.jpg b/examples/data/Fruit360/apple/282_100.jpg new file mode 100644 index 00000000..5728920e Binary files /dev/null and b/examples/data/Fruit360/apple/282_100.jpg differ diff --git a/examples/data/Fruit360/apple/283_100.jpg b/examples/data/Fruit360/apple/283_100.jpg new file mode 100644 index 00000000..7e6ac234 Binary files /dev/null and b/examples/data/Fruit360/apple/283_100.jpg differ diff --git a/examples/data/Fruit360/apple/284_100.jpg b/examples/data/Fruit360/apple/284_100.jpg new file mode 100644 index 00000000..114b625c Binary files /dev/null and b/examples/data/Fruit360/apple/284_100.jpg differ diff --git a/examples/data/Fruit360/apple/285_100.jpg b/examples/data/Fruit360/apple/285_100.jpg new file mode 100644 index 00000000..c042352a Binary files /dev/null and b/examples/data/Fruit360/apple/285_100.jpg differ diff --git a/examples/data/Fruit360/apple/286_100.jpg b/examples/data/Fruit360/apple/286_100.jpg new file mode 100644 index 00000000..3c402748 Binary files /dev/null and b/examples/data/Fruit360/apple/286_100.jpg differ diff --git a/examples/data/Fruit360/apple/287_100.jpg b/examples/data/Fruit360/apple/287_100.jpg new file mode 100644 index 00000000..af28ed4d Binary files /dev/null and b/examples/data/Fruit360/apple/287_100.jpg differ diff --git a/examples/data/Fruit360/apple/288_100.jpg b/examples/data/Fruit360/apple/288_100.jpg new file mode 100644 index 00000000..f6914fbe Binary files /dev/null and b/examples/data/Fruit360/apple/288_100.jpg differ diff --git a/examples/data/Fruit360/apple/289_100.jpg b/examples/data/Fruit360/apple/289_100.jpg new file mode 100644 index 00000000..b03e4847 Binary files /dev/null and b/examples/data/Fruit360/apple/289_100.jpg differ diff --git a/examples/data/Fruit360/apple/28_100.jpg b/examples/data/Fruit360/apple/28_100.jpg new file mode 100644 index 00000000..d63b7233 Binary files /dev/null and b/examples/data/Fruit360/apple/28_100.jpg differ diff --git a/examples/data/Fruit360/apple/290_100.jpg b/examples/data/Fruit360/apple/290_100.jpg new file mode 100644 index 00000000..e333708a Binary files /dev/null and b/examples/data/Fruit360/apple/290_100.jpg differ diff --git a/examples/data/Fruit360/apple/291_100.jpg b/examples/data/Fruit360/apple/291_100.jpg new file mode 100644 index 00000000..e6bca991 Binary files /dev/null and b/examples/data/Fruit360/apple/291_100.jpg differ diff --git a/examples/data/Fruit360/apple/292_100.jpg b/examples/data/Fruit360/apple/292_100.jpg new file mode 100644 index 00000000..cc46c15e Binary files /dev/null and b/examples/data/Fruit360/apple/292_100.jpg differ diff --git a/examples/data/Fruit360/apple/293_100.jpg b/examples/data/Fruit360/apple/293_100.jpg new file mode 100644 index 00000000..200f592e Binary files /dev/null and b/examples/data/Fruit360/apple/293_100.jpg differ diff --git a/examples/data/Fruit360/apple/294_100.jpg b/examples/data/Fruit360/apple/294_100.jpg new file mode 100644 index 00000000..3e7a568e Binary files /dev/null and b/examples/data/Fruit360/apple/294_100.jpg differ diff --git a/examples/data/Fruit360/apple/295_100.jpg b/examples/data/Fruit360/apple/295_100.jpg new file mode 100644 index 00000000..2980d3e7 Binary files /dev/null and b/examples/data/Fruit360/apple/295_100.jpg differ diff --git a/examples/data/Fruit360/apple/296_100.jpg b/examples/data/Fruit360/apple/296_100.jpg new file mode 100644 index 00000000..7048fdeb Binary files /dev/null and b/examples/data/Fruit360/apple/296_100.jpg differ diff --git a/examples/data/Fruit360/apple/297_100.jpg b/examples/data/Fruit360/apple/297_100.jpg new file mode 100644 index 00000000..ca6a1f4a Binary files /dev/null and b/examples/data/Fruit360/apple/297_100.jpg differ diff --git a/examples/data/Fruit360/apple/298_100.jpg b/examples/data/Fruit360/apple/298_100.jpg new file mode 100644 index 00000000..38f91908 Binary files /dev/null and b/examples/data/Fruit360/apple/298_100.jpg differ diff --git a/examples/data/Fruit360/apple/299_100.jpg b/examples/data/Fruit360/apple/299_100.jpg new file mode 100644 index 00000000..3e066556 Binary files /dev/null and b/examples/data/Fruit360/apple/299_100.jpg differ diff --git a/examples/data/Fruit360/apple/29_100.jpg b/examples/data/Fruit360/apple/29_100.jpg new file mode 100644 index 00000000..a69777c7 Binary files /dev/null and b/examples/data/Fruit360/apple/29_100.jpg differ diff --git a/examples/data/Fruit360/apple/2_100.jpg b/examples/data/Fruit360/apple/2_100.jpg new file mode 100644 index 00000000..6e1130cd Binary files /dev/null and b/examples/data/Fruit360/apple/2_100.jpg differ diff --git a/examples/data/Fruit360/apple/300_100.jpg b/examples/data/Fruit360/apple/300_100.jpg new file mode 100644 index 00000000..a5f57b48 Binary files /dev/null and b/examples/data/Fruit360/apple/300_100.jpg differ diff --git a/examples/data/Fruit360/apple/301_100.jpg b/examples/data/Fruit360/apple/301_100.jpg new file mode 100644 index 00000000..57b5720d Binary files /dev/null and b/examples/data/Fruit360/apple/301_100.jpg differ diff --git a/examples/data/Fruit360/apple/302_100.jpg b/examples/data/Fruit360/apple/302_100.jpg new file mode 100644 index 00000000..6ac36d43 Binary files /dev/null and b/examples/data/Fruit360/apple/302_100.jpg differ diff --git a/examples/data/Fruit360/apple/303_100.jpg b/examples/data/Fruit360/apple/303_100.jpg new file mode 100644 index 00000000..66aa72d4 Binary files /dev/null and b/examples/data/Fruit360/apple/303_100.jpg differ diff --git a/examples/data/Fruit360/apple/304_100.jpg b/examples/data/Fruit360/apple/304_100.jpg new file mode 100644 index 00000000..551304d1 Binary files /dev/null and b/examples/data/Fruit360/apple/304_100.jpg differ diff --git a/examples/data/Fruit360/apple/305_100.jpg b/examples/data/Fruit360/apple/305_100.jpg new file mode 100644 index 00000000..9438e032 Binary files /dev/null and b/examples/data/Fruit360/apple/305_100.jpg differ diff --git a/examples/data/Fruit360/apple/306_100.jpg b/examples/data/Fruit360/apple/306_100.jpg new file mode 100644 index 00000000..7dd2eaab Binary files /dev/null and b/examples/data/Fruit360/apple/306_100.jpg differ diff --git a/examples/data/Fruit360/apple/307_100.jpg b/examples/data/Fruit360/apple/307_100.jpg new file mode 100644 index 00000000..172ec55d Binary files /dev/null and b/examples/data/Fruit360/apple/307_100.jpg differ diff --git a/examples/data/Fruit360/apple/308_100.jpg b/examples/data/Fruit360/apple/308_100.jpg new file mode 100644 index 00000000..4fc621e1 Binary files /dev/null and b/examples/data/Fruit360/apple/308_100.jpg differ diff --git a/examples/data/Fruit360/apple/309_100.jpg b/examples/data/Fruit360/apple/309_100.jpg new file mode 100644 index 00000000..4745490e Binary files /dev/null and b/examples/data/Fruit360/apple/309_100.jpg differ diff --git a/examples/data/Fruit360/apple/30_100.jpg b/examples/data/Fruit360/apple/30_100.jpg new file mode 100644 index 00000000..df75ac91 Binary files /dev/null and b/examples/data/Fruit360/apple/30_100.jpg differ diff --git a/examples/data/Fruit360/apple/310_100.jpg b/examples/data/Fruit360/apple/310_100.jpg new file mode 100644 index 00000000..f40653f3 Binary files /dev/null and b/examples/data/Fruit360/apple/310_100.jpg differ diff --git a/examples/data/Fruit360/apple/311_100.jpg b/examples/data/Fruit360/apple/311_100.jpg new file mode 100644 index 00000000..718aa1a7 Binary files /dev/null and b/examples/data/Fruit360/apple/311_100.jpg differ diff --git a/examples/data/Fruit360/apple/312_100.jpg b/examples/data/Fruit360/apple/312_100.jpg new file mode 100644 index 00000000..2c4cbcc3 Binary files /dev/null and b/examples/data/Fruit360/apple/312_100.jpg differ diff --git a/examples/data/Fruit360/apple/313_100.jpg b/examples/data/Fruit360/apple/313_100.jpg new file mode 100644 index 00000000..58e0c377 Binary files /dev/null and b/examples/data/Fruit360/apple/313_100.jpg differ diff --git a/examples/data/Fruit360/apple/314_100.jpg b/examples/data/Fruit360/apple/314_100.jpg new file mode 100644 index 00000000..5a51ff96 Binary files /dev/null and b/examples/data/Fruit360/apple/314_100.jpg differ diff --git a/examples/data/Fruit360/apple/315_100.jpg b/examples/data/Fruit360/apple/315_100.jpg new file mode 100644 index 00000000..1222a6c1 Binary files /dev/null and b/examples/data/Fruit360/apple/315_100.jpg differ diff --git a/examples/data/Fruit360/apple/316_100.jpg b/examples/data/Fruit360/apple/316_100.jpg new file mode 100644 index 00000000..e0ca9b57 Binary files /dev/null and b/examples/data/Fruit360/apple/316_100.jpg differ diff --git a/examples/data/Fruit360/apple/317_100.jpg b/examples/data/Fruit360/apple/317_100.jpg new file mode 100644 index 00000000..fd46fe4a Binary files /dev/null and b/examples/data/Fruit360/apple/317_100.jpg differ diff --git a/examples/data/Fruit360/apple/318_100.jpg b/examples/data/Fruit360/apple/318_100.jpg new file mode 100644 index 00000000..2ecf23b4 Binary files /dev/null and b/examples/data/Fruit360/apple/318_100.jpg differ diff --git a/examples/data/Fruit360/apple/319_100.jpg b/examples/data/Fruit360/apple/319_100.jpg new file mode 100644 index 00000000..8ff7f1bb Binary files /dev/null and b/examples/data/Fruit360/apple/319_100.jpg differ diff --git a/examples/data/Fruit360/apple/31_100.jpg b/examples/data/Fruit360/apple/31_100.jpg new file mode 100644 index 00000000..bddabbe5 Binary files /dev/null and b/examples/data/Fruit360/apple/31_100.jpg differ diff --git a/examples/data/Fruit360/apple/320_100.jpg b/examples/data/Fruit360/apple/320_100.jpg new file mode 100644 index 00000000..d2b91a82 Binary files /dev/null and b/examples/data/Fruit360/apple/320_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_0_100.jpg b/examples/data/Fruit360/apple/r_0_100.jpg new file mode 100644 index 00000000..60be829f Binary files /dev/null and b/examples/data/Fruit360/apple/r_0_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_100_100.jpg b/examples/data/Fruit360/apple/r_100_100.jpg new file mode 100644 index 00000000..fe0ae332 Binary files /dev/null and b/examples/data/Fruit360/apple/r_100_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_101_100.jpg b/examples/data/Fruit360/apple/r_101_100.jpg new file mode 100644 index 00000000..f5f03f3b Binary files /dev/null and b/examples/data/Fruit360/apple/r_101_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_102_100.jpg b/examples/data/Fruit360/apple/r_102_100.jpg new file mode 100644 index 00000000..4bf432f5 Binary files /dev/null and b/examples/data/Fruit360/apple/r_102_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_103_100.jpg b/examples/data/Fruit360/apple/r_103_100.jpg new file mode 100644 index 00000000..edd1a5d2 Binary files /dev/null and b/examples/data/Fruit360/apple/r_103_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_104_100.jpg b/examples/data/Fruit360/apple/r_104_100.jpg new file mode 100644 index 00000000..e16b0590 Binary files /dev/null and b/examples/data/Fruit360/apple/r_104_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_105_100.jpg b/examples/data/Fruit360/apple/r_105_100.jpg new file mode 100644 index 00000000..a763e246 Binary files /dev/null and b/examples/data/Fruit360/apple/r_105_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_106_100.jpg b/examples/data/Fruit360/apple/r_106_100.jpg new file mode 100644 index 00000000..e60dd2db Binary files /dev/null and b/examples/data/Fruit360/apple/r_106_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_107_100.jpg b/examples/data/Fruit360/apple/r_107_100.jpg new file mode 100644 index 00000000..afb36b70 Binary files /dev/null and b/examples/data/Fruit360/apple/r_107_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_108_100.jpg b/examples/data/Fruit360/apple/r_108_100.jpg new file mode 100644 index 00000000..e21d3428 Binary files /dev/null and b/examples/data/Fruit360/apple/r_108_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_109_100.jpg b/examples/data/Fruit360/apple/r_109_100.jpg new file mode 100644 index 00000000..f2ee4aec Binary files /dev/null and b/examples/data/Fruit360/apple/r_109_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_10_100.jpg b/examples/data/Fruit360/apple/r_10_100.jpg new file mode 100644 index 00000000..f5f0d1c0 Binary files /dev/null and b/examples/data/Fruit360/apple/r_10_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_110_100.jpg b/examples/data/Fruit360/apple/r_110_100.jpg new file mode 100644 index 00000000..4fec89a2 Binary files /dev/null and b/examples/data/Fruit360/apple/r_110_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_111_100.jpg b/examples/data/Fruit360/apple/r_111_100.jpg new file mode 100644 index 00000000..61ff1a4b Binary files /dev/null and b/examples/data/Fruit360/apple/r_111_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_112_100.jpg b/examples/data/Fruit360/apple/r_112_100.jpg new file mode 100644 index 00000000..1e9dbb57 Binary files /dev/null and b/examples/data/Fruit360/apple/r_112_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_113_100.jpg b/examples/data/Fruit360/apple/r_113_100.jpg new file mode 100644 index 00000000..21e89934 Binary files /dev/null and b/examples/data/Fruit360/apple/r_113_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_114_100.jpg b/examples/data/Fruit360/apple/r_114_100.jpg new file mode 100644 index 00000000..c927fc16 Binary files /dev/null and b/examples/data/Fruit360/apple/r_114_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_115_100.jpg b/examples/data/Fruit360/apple/r_115_100.jpg new file mode 100644 index 00000000..fd31e37d Binary files /dev/null and b/examples/data/Fruit360/apple/r_115_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_116_100.jpg b/examples/data/Fruit360/apple/r_116_100.jpg new file mode 100644 index 00000000..488c2a1d Binary files /dev/null and b/examples/data/Fruit360/apple/r_116_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_117_100.jpg b/examples/data/Fruit360/apple/r_117_100.jpg new file mode 100644 index 00000000..682780d5 Binary files /dev/null and b/examples/data/Fruit360/apple/r_117_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_118_100.jpg b/examples/data/Fruit360/apple/r_118_100.jpg new file mode 100644 index 00000000..2370a4a7 Binary files /dev/null and b/examples/data/Fruit360/apple/r_118_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_119_100.jpg b/examples/data/Fruit360/apple/r_119_100.jpg new file mode 100644 index 00000000..9cf6ada7 Binary files /dev/null and b/examples/data/Fruit360/apple/r_119_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_11_100.jpg b/examples/data/Fruit360/apple/r_11_100.jpg new file mode 100644 index 00000000..19c33924 Binary files /dev/null and b/examples/data/Fruit360/apple/r_11_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_120_100.jpg b/examples/data/Fruit360/apple/r_120_100.jpg new file mode 100644 index 00000000..a8be6e2b Binary files /dev/null and b/examples/data/Fruit360/apple/r_120_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_121_100.jpg b/examples/data/Fruit360/apple/r_121_100.jpg new file mode 100644 index 00000000..ba49bed4 Binary files /dev/null and b/examples/data/Fruit360/apple/r_121_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_122_100.jpg b/examples/data/Fruit360/apple/r_122_100.jpg new file mode 100644 index 00000000..601ea383 Binary files /dev/null and b/examples/data/Fruit360/apple/r_122_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_123_100.jpg b/examples/data/Fruit360/apple/r_123_100.jpg new file mode 100644 index 00000000..f23a426f Binary files /dev/null and b/examples/data/Fruit360/apple/r_123_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_124_100.jpg b/examples/data/Fruit360/apple/r_124_100.jpg new file mode 100644 index 00000000..860af4b7 Binary files /dev/null and b/examples/data/Fruit360/apple/r_124_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_125_100.jpg b/examples/data/Fruit360/apple/r_125_100.jpg new file mode 100644 index 00000000..da08d84b Binary files /dev/null and b/examples/data/Fruit360/apple/r_125_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_126_100.jpg b/examples/data/Fruit360/apple/r_126_100.jpg new file mode 100644 index 00000000..f9c828f5 Binary files /dev/null and b/examples/data/Fruit360/apple/r_126_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_127_100.jpg b/examples/data/Fruit360/apple/r_127_100.jpg new file mode 100644 index 00000000..dc18c0ef Binary files /dev/null and b/examples/data/Fruit360/apple/r_127_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_128_100.jpg b/examples/data/Fruit360/apple/r_128_100.jpg new file mode 100644 index 00000000..0ca5dc72 Binary files /dev/null and b/examples/data/Fruit360/apple/r_128_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_129_100.jpg b/examples/data/Fruit360/apple/r_129_100.jpg new file mode 100644 index 00000000..2b942b3c Binary files /dev/null and b/examples/data/Fruit360/apple/r_129_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_12_100.jpg b/examples/data/Fruit360/apple/r_12_100.jpg new file mode 100644 index 00000000..8da031cf Binary files /dev/null and b/examples/data/Fruit360/apple/r_12_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_130_100.jpg b/examples/data/Fruit360/apple/r_130_100.jpg new file mode 100644 index 00000000..685e3076 Binary files /dev/null and b/examples/data/Fruit360/apple/r_130_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_131_100.jpg b/examples/data/Fruit360/apple/r_131_100.jpg new file mode 100644 index 00000000..a5d998dc Binary files /dev/null and b/examples/data/Fruit360/apple/r_131_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_132_100.jpg b/examples/data/Fruit360/apple/r_132_100.jpg new file mode 100644 index 00000000..966bc778 Binary files /dev/null and b/examples/data/Fruit360/apple/r_132_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_133_100.jpg b/examples/data/Fruit360/apple/r_133_100.jpg new file mode 100644 index 00000000..fea31def Binary files /dev/null and b/examples/data/Fruit360/apple/r_133_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_134_100.jpg b/examples/data/Fruit360/apple/r_134_100.jpg new file mode 100644 index 00000000..4d93e76c Binary files /dev/null and b/examples/data/Fruit360/apple/r_134_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_135_100.jpg b/examples/data/Fruit360/apple/r_135_100.jpg new file mode 100644 index 00000000..704d6779 Binary files /dev/null and b/examples/data/Fruit360/apple/r_135_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_136_100.jpg b/examples/data/Fruit360/apple/r_136_100.jpg new file mode 100644 index 00000000..71d22435 Binary files /dev/null and b/examples/data/Fruit360/apple/r_136_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_137_100.jpg b/examples/data/Fruit360/apple/r_137_100.jpg new file mode 100644 index 00000000..3f65d1fb Binary files /dev/null and b/examples/data/Fruit360/apple/r_137_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_138_100.jpg b/examples/data/Fruit360/apple/r_138_100.jpg new file mode 100644 index 00000000..ab8df40b Binary files /dev/null and b/examples/data/Fruit360/apple/r_138_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_139_100.jpg b/examples/data/Fruit360/apple/r_139_100.jpg new file mode 100644 index 00000000..118df630 Binary files /dev/null and b/examples/data/Fruit360/apple/r_139_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_13_100.jpg b/examples/data/Fruit360/apple/r_13_100.jpg new file mode 100644 index 00000000..12a1c699 Binary files /dev/null and b/examples/data/Fruit360/apple/r_13_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_140_100.jpg b/examples/data/Fruit360/apple/r_140_100.jpg new file mode 100644 index 00000000..db829d21 Binary files /dev/null and b/examples/data/Fruit360/apple/r_140_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_141_100.jpg b/examples/data/Fruit360/apple/r_141_100.jpg new file mode 100644 index 00000000..3c4b28da Binary files /dev/null and b/examples/data/Fruit360/apple/r_141_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_142_100.jpg b/examples/data/Fruit360/apple/r_142_100.jpg new file mode 100644 index 00000000..36d39996 Binary files /dev/null and b/examples/data/Fruit360/apple/r_142_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_143_100.jpg b/examples/data/Fruit360/apple/r_143_100.jpg new file mode 100644 index 00000000..b69ae654 Binary files /dev/null and b/examples/data/Fruit360/apple/r_143_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_144_100.jpg b/examples/data/Fruit360/apple/r_144_100.jpg new file mode 100644 index 00000000..3e0bd377 Binary files /dev/null and b/examples/data/Fruit360/apple/r_144_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_145_100.jpg b/examples/data/Fruit360/apple/r_145_100.jpg new file mode 100644 index 00000000..5c775879 Binary files /dev/null and b/examples/data/Fruit360/apple/r_145_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_146_100.jpg b/examples/data/Fruit360/apple/r_146_100.jpg new file mode 100644 index 00000000..532d1960 Binary files /dev/null and b/examples/data/Fruit360/apple/r_146_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_147_100.jpg b/examples/data/Fruit360/apple/r_147_100.jpg new file mode 100644 index 00000000..925cacbf Binary files /dev/null and b/examples/data/Fruit360/apple/r_147_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_148_100.jpg b/examples/data/Fruit360/apple/r_148_100.jpg new file mode 100644 index 00000000..e414cce4 Binary files /dev/null and b/examples/data/Fruit360/apple/r_148_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_149_100.jpg b/examples/data/Fruit360/apple/r_149_100.jpg new file mode 100644 index 00000000..7278982b Binary files /dev/null and b/examples/data/Fruit360/apple/r_149_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_14_100.jpg b/examples/data/Fruit360/apple/r_14_100.jpg new file mode 100644 index 00000000..b85da3c7 Binary files /dev/null and b/examples/data/Fruit360/apple/r_14_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_150_100.jpg b/examples/data/Fruit360/apple/r_150_100.jpg new file mode 100644 index 00000000..2ccc7c45 Binary files /dev/null and b/examples/data/Fruit360/apple/r_150_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_151_100.jpg b/examples/data/Fruit360/apple/r_151_100.jpg new file mode 100644 index 00000000..3efe8e0c Binary files /dev/null and b/examples/data/Fruit360/apple/r_151_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_152_100.jpg b/examples/data/Fruit360/apple/r_152_100.jpg new file mode 100644 index 00000000..b6da9f8f Binary files /dev/null and b/examples/data/Fruit360/apple/r_152_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_153_100.jpg b/examples/data/Fruit360/apple/r_153_100.jpg new file mode 100644 index 00000000..729bfd04 Binary files /dev/null and b/examples/data/Fruit360/apple/r_153_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_154_100.jpg b/examples/data/Fruit360/apple/r_154_100.jpg new file mode 100644 index 00000000..5e858449 Binary files /dev/null and b/examples/data/Fruit360/apple/r_154_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_155_100.jpg b/examples/data/Fruit360/apple/r_155_100.jpg new file mode 100644 index 00000000..2e1dc997 Binary files /dev/null and b/examples/data/Fruit360/apple/r_155_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_156_100.jpg b/examples/data/Fruit360/apple/r_156_100.jpg new file mode 100644 index 00000000..f68a44e3 Binary files /dev/null and b/examples/data/Fruit360/apple/r_156_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_157_100.jpg b/examples/data/Fruit360/apple/r_157_100.jpg new file mode 100644 index 00000000..e8a5d368 Binary files /dev/null and b/examples/data/Fruit360/apple/r_157_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_158_100.jpg b/examples/data/Fruit360/apple/r_158_100.jpg new file mode 100644 index 00000000..3b66d764 Binary files /dev/null and b/examples/data/Fruit360/apple/r_158_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_159_100.jpg b/examples/data/Fruit360/apple/r_159_100.jpg new file mode 100644 index 00000000..d93d8b62 Binary files /dev/null and b/examples/data/Fruit360/apple/r_159_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_15_100.jpg b/examples/data/Fruit360/apple/r_15_100.jpg new file mode 100644 index 00000000..9cd29dda Binary files /dev/null and b/examples/data/Fruit360/apple/r_15_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_160_100.jpg b/examples/data/Fruit360/apple/r_160_100.jpg new file mode 100644 index 00000000..ffe5149b Binary files /dev/null and b/examples/data/Fruit360/apple/r_160_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_161_100.jpg b/examples/data/Fruit360/apple/r_161_100.jpg new file mode 100644 index 00000000..2765af8e Binary files /dev/null and b/examples/data/Fruit360/apple/r_161_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_162_100.jpg b/examples/data/Fruit360/apple/r_162_100.jpg new file mode 100644 index 00000000..5a9c4644 Binary files /dev/null and b/examples/data/Fruit360/apple/r_162_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_163_100.jpg b/examples/data/Fruit360/apple/r_163_100.jpg new file mode 100644 index 00000000..ead3e2b3 Binary files /dev/null and b/examples/data/Fruit360/apple/r_163_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_164_100.jpg b/examples/data/Fruit360/apple/r_164_100.jpg new file mode 100644 index 00000000..e149c56f Binary files /dev/null and b/examples/data/Fruit360/apple/r_164_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_165_100.jpg b/examples/data/Fruit360/apple/r_165_100.jpg new file mode 100644 index 00000000..512f9df5 Binary files /dev/null and b/examples/data/Fruit360/apple/r_165_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_166_100.jpg b/examples/data/Fruit360/apple/r_166_100.jpg new file mode 100644 index 00000000..c3bdcf2e Binary files /dev/null and b/examples/data/Fruit360/apple/r_166_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_167_100.jpg b/examples/data/Fruit360/apple/r_167_100.jpg new file mode 100644 index 00000000..ce850cb2 Binary files /dev/null and b/examples/data/Fruit360/apple/r_167_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_168_100.jpg b/examples/data/Fruit360/apple/r_168_100.jpg new file mode 100644 index 00000000..be8e996a Binary files /dev/null and b/examples/data/Fruit360/apple/r_168_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_169_100.jpg b/examples/data/Fruit360/apple/r_169_100.jpg new file mode 100644 index 00000000..d6d1efd3 Binary files /dev/null and b/examples/data/Fruit360/apple/r_169_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_16_100.jpg b/examples/data/Fruit360/apple/r_16_100.jpg new file mode 100644 index 00000000..21eabd42 Binary files /dev/null and b/examples/data/Fruit360/apple/r_16_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_170_100.jpg b/examples/data/Fruit360/apple/r_170_100.jpg new file mode 100644 index 00000000..47ec2def Binary files /dev/null and b/examples/data/Fruit360/apple/r_170_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_171_100.jpg b/examples/data/Fruit360/apple/r_171_100.jpg new file mode 100644 index 00000000..8ac327e9 Binary files /dev/null and b/examples/data/Fruit360/apple/r_171_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_172_100.jpg b/examples/data/Fruit360/apple/r_172_100.jpg new file mode 100644 index 00000000..e854f06f Binary files /dev/null and b/examples/data/Fruit360/apple/r_172_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_173_100.jpg b/examples/data/Fruit360/apple/r_173_100.jpg new file mode 100644 index 00000000..2af59309 Binary files /dev/null and b/examples/data/Fruit360/apple/r_173_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_174_100.jpg b/examples/data/Fruit360/apple/r_174_100.jpg new file mode 100644 index 00000000..a8709086 Binary files /dev/null and b/examples/data/Fruit360/apple/r_174_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_175_100.jpg b/examples/data/Fruit360/apple/r_175_100.jpg new file mode 100644 index 00000000..d5eceee6 Binary files /dev/null and b/examples/data/Fruit360/apple/r_175_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_176_100.jpg b/examples/data/Fruit360/apple/r_176_100.jpg new file mode 100644 index 00000000..b81a06e6 Binary files /dev/null and b/examples/data/Fruit360/apple/r_176_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_177_100.jpg b/examples/data/Fruit360/apple/r_177_100.jpg new file mode 100644 index 00000000..93e00abb Binary files /dev/null and b/examples/data/Fruit360/apple/r_177_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_178_100.jpg b/examples/data/Fruit360/apple/r_178_100.jpg new file mode 100644 index 00000000..e78097c2 Binary files /dev/null and b/examples/data/Fruit360/apple/r_178_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_179_100.jpg b/examples/data/Fruit360/apple/r_179_100.jpg new file mode 100644 index 00000000..41d49755 Binary files /dev/null and b/examples/data/Fruit360/apple/r_179_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_17_100.jpg b/examples/data/Fruit360/apple/r_17_100.jpg new file mode 100644 index 00000000..3866adfa Binary files /dev/null and b/examples/data/Fruit360/apple/r_17_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_180_100.jpg b/examples/data/Fruit360/apple/r_180_100.jpg new file mode 100644 index 00000000..b37b2c66 Binary files /dev/null and b/examples/data/Fruit360/apple/r_180_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_181_100.jpg b/examples/data/Fruit360/apple/r_181_100.jpg new file mode 100644 index 00000000..9bacb9eb Binary files /dev/null and b/examples/data/Fruit360/apple/r_181_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_182_100.jpg b/examples/data/Fruit360/apple/r_182_100.jpg new file mode 100644 index 00000000..59cc5eb1 Binary files /dev/null and b/examples/data/Fruit360/apple/r_182_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_183_100.jpg b/examples/data/Fruit360/apple/r_183_100.jpg new file mode 100644 index 00000000..1181717a Binary files /dev/null and b/examples/data/Fruit360/apple/r_183_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_184_100.jpg b/examples/data/Fruit360/apple/r_184_100.jpg new file mode 100644 index 00000000..2bdcd20e Binary files /dev/null and b/examples/data/Fruit360/apple/r_184_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_185_100.jpg b/examples/data/Fruit360/apple/r_185_100.jpg new file mode 100644 index 00000000..41e33aa4 Binary files /dev/null and b/examples/data/Fruit360/apple/r_185_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_186_100.jpg b/examples/data/Fruit360/apple/r_186_100.jpg new file mode 100644 index 00000000..9be282fb Binary files /dev/null and b/examples/data/Fruit360/apple/r_186_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_187_100.jpg b/examples/data/Fruit360/apple/r_187_100.jpg new file mode 100644 index 00000000..c6498500 Binary files /dev/null and b/examples/data/Fruit360/apple/r_187_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_188_100.jpg b/examples/data/Fruit360/apple/r_188_100.jpg new file mode 100644 index 00000000..ddeb7c31 Binary files /dev/null and b/examples/data/Fruit360/apple/r_188_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_189_100.jpg b/examples/data/Fruit360/apple/r_189_100.jpg new file mode 100644 index 00000000..2e8bafa4 Binary files /dev/null and b/examples/data/Fruit360/apple/r_189_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_18_100.jpg b/examples/data/Fruit360/apple/r_18_100.jpg new file mode 100644 index 00000000..daa85e29 Binary files /dev/null and b/examples/data/Fruit360/apple/r_18_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_190_100.jpg b/examples/data/Fruit360/apple/r_190_100.jpg new file mode 100644 index 00000000..f239e0f3 Binary files /dev/null and b/examples/data/Fruit360/apple/r_190_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_191_100.jpg b/examples/data/Fruit360/apple/r_191_100.jpg new file mode 100644 index 00000000..e79122a5 Binary files /dev/null and b/examples/data/Fruit360/apple/r_191_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_192_100.jpg b/examples/data/Fruit360/apple/r_192_100.jpg new file mode 100644 index 00000000..15903d63 Binary files /dev/null and b/examples/data/Fruit360/apple/r_192_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_193_100.jpg b/examples/data/Fruit360/apple/r_193_100.jpg new file mode 100644 index 00000000..b5cf324d Binary files /dev/null and b/examples/data/Fruit360/apple/r_193_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_194_100.jpg b/examples/data/Fruit360/apple/r_194_100.jpg new file mode 100644 index 00000000..d1fdd7d6 Binary files /dev/null and b/examples/data/Fruit360/apple/r_194_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_195_100.jpg b/examples/data/Fruit360/apple/r_195_100.jpg new file mode 100644 index 00000000..ebd59fef Binary files /dev/null and b/examples/data/Fruit360/apple/r_195_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_196_100.jpg b/examples/data/Fruit360/apple/r_196_100.jpg new file mode 100644 index 00000000..b99334f8 Binary files /dev/null and b/examples/data/Fruit360/apple/r_196_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_197_100.jpg b/examples/data/Fruit360/apple/r_197_100.jpg new file mode 100644 index 00000000..cf180d34 Binary files /dev/null and b/examples/data/Fruit360/apple/r_197_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_198_100.jpg b/examples/data/Fruit360/apple/r_198_100.jpg new file mode 100644 index 00000000..44ec5f0e Binary files /dev/null and b/examples/data/Fruit360/apple/r_198_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_199_100.jpg b/examples/data/Fruit360/apple/r_199_100.jpg new file mode 100644 index 00000000..511d620d Binary files /dev/null and b/examples/data/Fruit360/apple/r_199_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_19_100.jpg b/examples/data/Fruit360/apple/r_19_100.jpg new file mode 100644 index 00000000..a4f82030 Binary files /dev/null and b/examples/data/Fruit360/apple/r_19_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_1_100.jpg b/examples/data/Fruit360/apple/r_1_100.jpg new file mode 100644 index 00000000..5f360423 Binary files /dev/null and b/examples/data/Fruit360/apple/r_1_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_200_100.jpg b/examples/data/Fruit360/apple/r_200_100.jpg new file mode 100644 index 00000000..69405929 Binary files /dev/null and b/examples/data/Fruit360/apple/r_200_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_201_100.jpg b/examples/data/Fruit360/apple/r_201_100.jpg new file mode 100644 index 00000000..a49e6e7b Binary files /dev/null and b/examples/data/Fruit360/apple/r_201_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_202_100.jpg b/examples/data/Fruit360/apple/r_202_100.jpg new file mode 100644 index 00000000..9ae3314d Binary files /dev/null and b/examples/data/Fruit360/apple/r_202_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_203_100.jpg b/examples/data/Fruit360/apple/r_203_100.jpg new file mode 100644 index 00000000..d824d135 Binary files /dev/null and b/examples/data/Fruit360/apple/r_203_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_204_100.jpg b/examples/data/Fruit360/apple/r_204_100.jpg new file mode 100644 index 00000000..90aa1110 Binary files /dev/null and b/examples/data/Fruit360/apple/r_204_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_205_100.jpg b/examples/data/Fruit360/apple/r_205_100.jpg new file mode 100644 index 00000000..f7d24f9d Binary files /dev/null and b/examples/data/Fruit360/apple/r_205_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_206_100.jpg b/examples/data/Fruit360/apple/r_206_100.jpg new file mode 100644 index 00000000..0e5924aa Binary files /dev/null and b/examples/data/Fruit360/apple/r_206_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_207_100.jpg b/examples/data/Fruit360/apple/r_207_100.jpg new file mode 100644 index 00000000..8e833236 Binary files /dev/null and b/examples/data/Fruit360/apple/r_207_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_208_100.jpg b/examples/data/Fruit360/apple/r_208_100.jpg new file mode 100644 index 00000000..2ced1ec1 Binary files /dev/null and b/examples/data/Fruit360/apple/r_208_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_209_100.jpg b/examples/data/Fruit360/apple/r_209_100.jpg new file mode 100644 index 00000000..488900d6 Binary files /dev/null and b/examples/data/Fruit360/apple/r_209_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_20_100.jpg b/examples/data/Fruit360/apple/r_20_100.jpg new file mode 100644 index 00000000..18ea7abf Binary files /dev/null and b/examples/data/Fruit360/apple/r_20_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_210_100.jpg b/examples/data/Fruit360/apple/r_210_100.jpg new file mode 100644 index 00000000..70db7061 Binary files /dev/null and b/examples/data/Fruit360/apple/r_210_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_211_100.jpg b/examples/data/Fruit360/apple/r_211_100.jpg new file mode 100644 index 00000000..eb10dfb0 Binary files /dev/null and b/examples/data/Fruit360/apple/r_211_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_212_100.jpg b/examples/data/Fruit360/apple/r_212_100.jpg new file mode 100644 index 00000000..1cc131ac Binary files /dev/null and b/examples/data/Fruit360/apple/r_212_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_213_100.jpg b/examples/data/Fruit360/apple/r_213_100.jpg new file mode 100644 index 00000000..e99da110 Binary files /dev/null and b/examples/data/Fruit360/apple/r_213_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_214_100.jpg b/examples/data/Fruit360/apple/r_214_100.jpg new file mode 100644 index 00000000..91e345cd Binary files /dev/null and b/examples/data/Fruit360/apple/r_214_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_215_100.jpg b/examples/data/Fruit360/apple/r_215_100.jpg new file mode 100644 index 00000000..fe010700 Binary files /dev/null and b/examples/data/Fruit360/apple/r_215_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_216_100.jpg b/examples/data/Fruit360/apple/r_216_100.jpg new file mode 100644 index 00000000..60ba9c70 Binary files /dev/null and b/examples/data/Fruit360/apple/r_216_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_217_100.jpg b/examples/data/Fruit360/apple/r_217_100.jpg new file mode 100644 index 00000000..b5a0cd6e Binary files /dev/null and b/examples/data/Fruit360/apple/r_217_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_218_100.jpg b/examples/data/Fruit360/apple/r_218_100.jpg new file mode 100644 index 00000000..3c009c60 Binary files /dev/null and b/examples/data/Fruit360/apple/r_218_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_219_100.jpg b/examples/data/Fruit360/apple/r_219_100.jpg new file mode 100644 index 00000000..f97358ff Binary files /dev/null and b/examples/data/Fruit360/apple/r_219_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_21_100.jpg b/examples/data/Fruit360/apple/r_21_100.jpg new file mode 100644 index 00000000..63178730 Binary files /dev/null and b/examples/data/Fruit360/apple/r_21_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_220_100.jpg b/examples/data/Fruit360/apple/r_220_100.jpg new file mode 100644 index 00000000..05feac92 Binary files /dev/null and b/examples/data/Fruit360/apple/r_220_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_221_100.jpg b/examples/data/Fruit360/apple/r_221_100.jpg new file mode 100644 index 00000000..022e2e38 Binary files /dev/null and b/examples/data/Fruit360/apple/r_221_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_222_100.jpg b/examples/data/Fruit360/apple/r_222_100.jpg new file mode 100644 index 00000000..bc964559 Binary files /dev/null and b/examples/data/Fruit360/apple/r_222_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_223_100.jpg b/examples/data/Fruit360/apple/r_223_100.jpg new file mode 100644 index 00000000..b7db92ec Binary files /dev/null and b/examples/data/Fruit360/apple/r_223_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_224_100.jpg b/examples/data/Fruit360/apple/r_224_100.jpg new file mode 100644 index 00000000..a12a14b9 Binary files /dev/null and b/examples/data/Fruit360/apple/r_224_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_225_100.jpg b/examples/data/Fruit360/apple/r_225_100.jpg new file mode 100644 index 00000000..e51c7350 Binary files /dev/null and b/examples/data/Fruit360/apple/r_225_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_226_100.jpg b/examples/data/Fruit360/apple/r_226_100.jpg new file mode 100644 index 00000000..f219a427 Binary files /dev/null and b/examples/data/Fruit360/apple/r_226_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_227_100.jpg b/examples/data/Fruit360/apple/r_227_100.jpg new file mode 100644 index 00000000..0d3cce50 Binary files /dev/null and b/examples/data/Fruit360/apple/r_227_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_228_100.jpg b/examples/data/Fruit360/apple/r_228_100.jpg new file mode 100644 index 00000000..c6ebf365 Binary files /dev/null and b/examples/data/Fruit360/apple/r_228_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_229_100.jpg b/examples/data/Fruit360/apple/r_229_100.jpg new file mode 100644 index 00000000..691adc82 Binary files /dev/null and b/examples/data/Fruit360/apple/r_229_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_22_100.jpg b/examples/data/Fruit360/apple/r_22_100.jpg new file mode 100644 index 00000000..ca91c8e3 Binary files /dev/null and b/examples/data/Fruit360/apple/r_22_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_230_100.jpg b/examples/data/Fruit360/apple/r_230_100.jpg new file mode 100644 index 00000000..52596b19 Binary files /dev/null and b/examples/data/Fruit360/apple/r_230_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_231_100.jpg b/examples/data/Fruit360/apple/r_231_100.jpg new file mode 100644 index 00000000..a398fffd Binary files /dev/null and b/examples/data/Fruit360/apple/r_231_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_232_100.jpg b/examples/data/Fruit360/apple/r_232_100.jpg new file mode 100644 index 00000000..899bab8c Binary files /dev/null and b/examples/data/Fruit360/apple/r_232_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_233_100.jpg b/examples/data/Fruit360/apple/r_233_100.jpg new file mode 100644 index 00000000..de5f6480 Binary files /dev/null and b/examples/data/Fruit360/apple/r_233_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_234_100.jpg b/examples/data/Fruit360/apple/r_234_100.jpg new file mode 100644 index 00000000..58f29729 Binary files /dev/null and b/examples/data/Fruit360/apple/r_234_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_235_100.jpg b/examples/data/Fruit360/apple/r_235_100.jpg new file mode 100644 index 00000000..3926d3a0 Binary files /dev/null and b/examples/data/Fruit360/apple/r_235_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_236_100.jpg b/examples/data/Fruit360/apple/r_236_100.jpg new file mode 100644 index 00000000..8a2d1416 Binary files /dev/null and b/examples/data/Fruit360/apple/r_236_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_237_100.jpg b/examples/data/Fruit360/apple/r_237_100.jpg new file mode 100644 index 00000000..60dbfa6b Binary files /dev/null and b/examples/data/Fruit360/apple/r_237_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_238_100.jpg b/examples/data/Fruit360/apple/r_238_100.jpg new file mode 100644 index 00000000..3552e7bc Binary files /dev/null and b/examples/data/Fruit360/apple/r_238_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_239_100.jpg b/examples/data/Fruit360/apple/r_239_100.jpg new file mode 100644 index 00000000..7efa51cb Binary files /dev/null and b/examples/data/Fruit360/apple/r_239_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_23_100.jpg b/examples/data/Fruit360/apple/r_23_100.jpg new file mode 100644 index 00000000..bc335676 Binary files /dev/null and b/examples/data/Fruit360/apple/r_23_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_240_100.jpg b/examples/data/Fruit360/apple/r_240_100.jpg new file mode 100644 index 00000000..151e0ef2 Binary files /dev/null and b/examples/data/Fruit360/apple/r_240_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_241_100.jpg b/examples/data/Fruit360/apple/r_241_100.jpg new file mode 100644 index 00000000..eb930e77 Binary files /dev/null and b/examples/data/Fruit360/apple/r_241_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_242_100.jpg b/examples/data/Fruit360/apple/r_242_100.jpg new file mode 100644 index 00000000..cf1adcaf Binary files /dev/null and b/examples/data/Fruit360/apple/r_242_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_243_100.jpg b/examples/data/Fruit360/apple/r_243_100.jpg new file mode 100644 index 00000000..d91ee4e3 Binary files /dev/null and b/examples/data/Fruit360/apple/r_243_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_244_100.jpg b/examples/data/Fruit360/apple/r_244_100.jpg new file mode 100644 index 00000000..12fa9a1f Binary files /dev/null and b/examples/data/Fruit360/apple/r_244_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_245_100.jpg b/examples/data/Fruit360/apple/r_245_100.jpg new file mode 100644 index 00000000..d55d05a5 Binary files /dev/null and b/examples/data/Fruit360/apple/r_245_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_246_100.jpg b/examples/data/Fruit360/apple/r_246_100.jpg new file mode 100644 index 00000000..86eb4c55 Binary files /dev/null and b/examples/data/Fruit360/apple/r_246_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_247_100.jpg b/examples/data/Fruit360/apple/r_247_100.jpg new file mode 100644 index 00000000..9caef1ed Binary files /dev/null and b/examples/data/Fruit360/apple/r_247_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_248_100.jpg b/examples/data/Fruit360/apple/r_248_100.jpg new file mode 100644 index 00000000..dbf4ca23 Binary files /dev/null and b/examples/data/Fruit360/apple/r_248_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_249_100.jpg b/examples/data/Fruit360/apple/r_249_100.jpg new file mode 100644 index 00000000..2b0bc284 Binary files /dev/null and b/examples/data/Fruit360/apple/r_249_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_24_100.jpg b/examples/data/Fruit360/apple/r_24_100.jpg new file mode 100644 index 00000000..993f32d8 Binary files /dev/null and b/examples/data/Fruit360/apple/r_24_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_250_100.jpg b/examples/data/Fruit360/apple/r_250_100.jpg new file mode 100644 index 00000000..d6e01527 Binary files /dev/null and b/examples/data/Fruit360/apple/r_250_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_251_100.jpg b/examples/data/Fruit360/apple/r_251_100.jpg new file mode 100644 index 00000000..8786eb3a Binary files /dev/null and b/examples/data/Fruit360/apple/r_251_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_252_100.jpg b/examples/data/Fruit360/apple/r_252_100.jpg new file mode 100644 index 00000000..09043c16 Binary files /dev/null and b/examples/data/Fruit360/apple/r_252_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_253_100.jpg b/examples/data/Fruit360/apple/r_253_100.jpg new file mode 100644 index 00000000..e76186a4 Binary files /dev/null and b/examples/data/Fruit360/apple/r_253_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_254_100.jpg b/examples/data/Fruit360/apple/r_254_100.jpg new file mode 100644 index 00000000..7815bb84 Binary files /dev/null and b/examples/data/Fruit360/apple/r_254_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_255_100.jpg b/examples/data/Fruit360/apple/r_255_100.jpg new file mode 100644 index 00000000..b9e4b77c Binary files /dev/null and b/examples/data/Fruit360/apple/r_255_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_256_100.jpg b/examples/data/Fruit360/apple/r_256_100.jpg new file mode 100644 index 00000000..287c53c3 Binary files /dev/null and b/examples/data/Fruit360/apple/r_256_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_257_100.jpg b/examples/data/Fruit360/apple/r_257_100.jpg new file mode 100644 index 00000000..e630f1e8 Binary files /dev/null and b/examples/data/Fruit360/apple/r_257_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_258_100.jpg b/examples/data/Fruit360/apple/r_258_100.jpg new file mode 100644 index 00000000..7ea184a4 Binary files /dev/null and b/examples/data/Fruit360/apple/r_258_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_259_100.jpg b/examples/data/Fruit360/apple/r_259_100.jpg new file mode 100644 index 00000000..6bd1e29b Binary files /dev/null and b/examples/data/Fruit360/apple/r_259_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_25_100.jpg b/examples/data/Fruit360/apple/r_25_100.jpg new file mode 100644 index 00000000..494129a4 Binary files /dev/null and b/examples/data/Fruit360/apple/r_25_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_260_100.jpg b/examples/data/Fruit360/apple/r_260_100.jpg new file mode 100644 index 00000000..97487390 Binary files /dev/null and b/examples/data/Fruit360/apple/r_260_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_261_100.jpg b/examples/data/Fruit360/apple/r_261_100.jpg new file mode 100644 index 00000000..8a65d4e9 Binary files /dev/null and b/examples/data/Fruit360/apple/r_261_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_262_100.jpg b/examples/data/Fruit360/apple/r_262_100.jpg new file mode 100644 index 00000000..9426f045 Binary files /dev/null and b/examples/data/Fruit360/apple/r_262_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_263_100.jpg b/examples/data/Fruit360/apple/r_263_100.jpg new file mode 100644 index 00000000..6d1013fc Binary files /dev/null and b/examples/data/Fruit360/apple/r_263_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_264_100.jpg b/examples/data/Fruit360/apple/r_264_100.jpg new file mode 100644 index 00000000..76d5f1dc Binary files /dev/null and b/examples/data/Fruit360/apple/r_264_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_265_100.jpg b/examples/data/Fruit360/apple/r_265_100.jpg new file mode 100644 index 00000000..953c247d Binary files /dev/null and b/examples/data/Fruit360/apple/r_265_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_266_100.jpg b/examples/data/Fruit360/apple/r_266_100.jpg new file mode 100644 index 00000000..efa03699 Binary files /dev/null and b/examples/data/Fruit360/apple/r_266_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_267_100.jpg b/examples/data/Fruit360/apple/r_267_100.jpg new file mode 100644 index 00000000..3b5ea4fe Binary files /dev/null and b/examples/data/Fruit360/apple/r_267_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_268_100.jpg b/examples/data/Fruit360/apple/r_268_100.jpg new file mode 100644 index 00000000..12813d58 Binary files /dev/null and b/examples/data/Fruit360/apple/r_268_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_269_100.jpg b/examples/data/Fruit360/apple/r_269_100.jpg new file mode 100644 index 00000000..72dfbbc8 Binary files /dev/null and b/examples/data/Fruit360/apple/r_269_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_26_100.jpg b/examples/data/Fruit360/apple/r_26_100.jpg new file mode 100644 index 00000000..d0a381eb Binary files /dev/null and b/examples/data/Fruit360/apple/r_26_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_270_100.jpg b/examples/data/Fruit360/apple/r_270_100.jpg new file mode 100644 index 00000000..07e8a804 Binary files /dev/null and b/examples/data/Fruit360/apple/r_270_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_271_100.jpg b/examples/data/Fruit360/apple/r_271_100.jpg new file mode 100644 index 00000000..a6b6bf65 Binary files /dev/null and b/examples/data/Fruit360/apple/r_271_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_272_100.jpg b/examples/data/Fruit360/apple/r_272_100.jpg new file mode 100644 index 00000000..6241940d Binary files /dev/null and b/examples/data/Fruit360/apple/r_272_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_273_100.jpg b/examples/data/Fruit360/apple/r_273_100.jpg new file mode 100644 index 00000000..df8fbb66 Binary files /dev/null and b/examples/data/Fruit360/apple/r_273_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_274_100.jpg b/examples/data/Fruit360/apple/r_274_100.jpg new file mode 100644 index 00000000..e085af8a Binary files /dev/null and b/examples/data/Fruit360/apple/r_274_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_275_100.jpg b/examples/data/Fruit360/apple/r_275_100.jpg new file mode 100644 index 00000000..b85de3ac Binary files /dev/null and b/examples/data/Fruit360/apple/r_275_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_276_100.jpg b/examples/data/Fruit360/apple/r_276_100.jpg new file mode 100644 index 00000000..c6fe6cee Binary files /dev/null and b/examples/data/Fruit360/apple/r_276_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_277_100.jpg b/examples/data/Fruit360/apple/r_277_100.jpg new file mode 100644 index 00000000..5c2b5b67 Binary files /dev/null and b/examples/data/Fruit360/apple/r_277_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_278_100.jpg b/examples/data/Fruit360/apple/r_278_100.jpg new file mode 100644 index 00000000..b9a22a45 Binary files /dev/null and b/examples/data/Fruit360/apple/r_278_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_279_100.jpg b/examples/data/Fruit360/apple/r_279_100.jpg new file mode 100644 index 00000000..280e781d Binary files /dev/null and b/examples/data/Fruit360/apple/r_279_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_27_100.jpg b/examples/data/Fruit360/apple/r_27_100.jpg new file mode 100644 index 00000000..35bd1365 Binary files /dev/null and b/examples/data/Fruit360/apple/r_27_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_280_100.jpg b/examples/data/Fruit360/apple/r_280_100.jpg new file mode 100644 index 00000000..11145e9c Binary files /dev/null and b/examples/data/Fruit360/apple/r_280_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_281_100.jpg b/examples/data/Fruit360/apple/r_281_100.jpg new file mode 100644 index 00000000..11eb3ed4 Binary files /dev/null and b/examples/data/Fruit360/apple/r_281_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_282_100.jpg b/examples/data/Fruit360/apple/r_282_100.jpg new file mode 100644 index 00000000..a2efad90 Binary files /dev/null and b/examples/data/Fruit360/apple/r_282_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_283_100.jpg b/examples/data/Fruit360/apple/r_283_100.jpg new file mode 100644 index 00000000..50148581 Binary files /dev/null and b/examples/data/Fruit360/apple/r_283_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_284_100.jpg b/examples/data/Fruit360/apple/r_284_100.jpg new file mode 100644 index 00000000..e8f23100 Binary files /dev/null and b/examples/data/Fruit360/apple/r_284_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_285_100.jpg b/examples/data/Fruit360/apple/r_285_100.jpg new file mode 100644 index 00000000..f43ae476 Binary files /dev/null and b/examples/data/Fruit360/apple/r_285_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_286_100.jpg b/examples/data/Fruit360/apple/r_286_100.jpg new file mode 100644 index 00000000..44d40432 Binary files /dev/null and b/examples/data/Fruit360/apple/r_286_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_287_100.jpg b/examples/data/Fruit360/apple/r_287_100.jpg new file mode 100644 index 00000000..df818916 Binary files /dev/null and b/examples/data/Fruit360/apple/r_287_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_288_100.jpg b/examples/data/Fruit360/apple/r_288_100.jpg new file mode 100644 index 00000000..bb3c05f6 Binary files /dev/null and b/examples/data/Fruit360/apple/r_288_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_289_100.jpg b/examples/data/Fruit360/apple/r_289_100.jpg new file mode 100644 index 00000000..76d12049 Binary files /dev/null and b/examples/data/Fruit360/apple/r_289_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_28_100.jpg b/examples/data/Fruit360/apple/r_28_100.jpg new file mode 100644 index 00000000..77b4efb7 Binary files /dev/null and b/examples/data/Fruit360/apple/r_28_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_290_100.jpg b/examples/data/Fruit360/apple/r_290_100.jpg new file mode 100644 index 00000000..16c6df03 Binary files /dev/null and b/examples/data/Fruit360/apple/r_290_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_291_100.jpg b/examples/data/Fruit360/apple/r_291_100.jpg new file mode 100644 index 00000000..b16baef4 Binary files /dev/null and b/examples/data/Fruit360/apple/r_291_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_292_100.jpg b/examples/data/Fruit360/apple/r_292_100.jpg new file mode 100644 index 00000000..e45007fe Binary files /dev/null and b/examples/data/Fruit360/apple/r_292_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_293_100.jpg b/examples/data/Fruit360/apple/r_293_100.jpg new file mode 100644 index 00000000..90d708b4 Binary files /dev/null and b/examples/data/Fruit360/apple/r_293_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_294_100.jpg b/examples/data/Fruit360/apple/r_294_100.jpg new file mode 100644 index 00000000..4e549119 Binary files /dev/null and b/examples/data/Fruit360/apple/r_294_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_295_100.jpg b/examples/data/Fruit360/apple/r_295_100.jpg new file mode 100644 index 00000000..4f975b87 Binary files /dev/null and b/examples/data/Fruit360/apple/r_295_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_296_100.jpg b/examples/data/Fruit360/apple/r_296_100.jpg new file mode 100644 index 00000000..043c481c Binary files /dev/null and b/examples/data/Fruit360/apple/r_296_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_297_100.jpg b/examples/data/Fruit360/apple/r_297_100.jpg new file mode 100644 index 00000000..ff1269a3 Binary files /dev/null and b/examples/data/Fruit360/apple/r_297_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_298_100.jpg b/examples/data/Fruit360/apple/r_298_100.jpg new file mode 100644 index 00000000..54b168e5 Binary files /dev/null and b/examples/data/Fruit360/apple/r_298_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_299_100.jpg b/examples/data/Fruit360/apple/r_299_100.jpg new file mode 100644 index 00000000..b5f16e23 Binary files /dev/null and b/examples/data/Fruit360/apple/r_299_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_29_100.jpg b/examples/data/Fruit360/apple/r_29_100.jpg new file mode 100644 index 00000000..3bd591ea Binary files /dev/null and b/examples/data/Fruit360/apple/r_29_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_2_100.jpg b/examples/data/Fruit360/apple/r_2_100.jpg new file mode 100644 index 00000000..3ec7d289 Binary files /dev/null and b/examples/data/Fruit360/apple/r_2_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_300_100.jpg b/examples/data/Fruit360/apple/r_300_100.jpg new file mode 100644 index 00000000..f1126a95 Binary files /dev/null and b/examples/data/Fruit360/apple/r_300_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_301_100.jpg b/examples/data/Fruit360/apple/r_301_100.jpg new file mode 100644 index 00000000..c0244c97 Binary files /dev/null and b/examples/data/Fruit360/apple/r_301_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_302_100.jpg b/examples/data/Fruit360/apple/r_302_100.jpg new file mode 100644 index 00000000..c1a9c4e6 Binary files /dev/null and b/examples/data/Fruit360/apple/r_302_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_303_100.jpg b/examples/data/Fruit360/apple/r_303_100.jpg new file mode 100644 index 00000000..f1055adb Binary files /dev/null and b/examples/data/Fruit360/apple/r_303_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_304_100.jpg b/examples/data/Fruit360/apple/r_304_100.jpg new file mode 100644 index 00000000..81f900c8 Binary files /dev/null and b/examples/data/Fruit360/apple/r_304_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_305_100.jpg b/examples/data/Fruit360/apple/r_305_100.jpg new file mode 100644 index 00000000..5c67ccb4 Binary files /dev/null and b/examples/data/Fruit360/apple/r_305_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_306_100.jpg b/examples/data/Fruit360/apple/r_306_100.jpg new file mode 100644 index 00000000..95c8daf2 Binary files /dev/null and b/examples/data/Fruit360/apple/r_306_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_307_100.jpg b/examples/data/Fruit360/apple/r_307_100.jpg new file mode 100644 index 00000000..79e45f92 Binary files /dev/null and b/examples/data/Fruit360/apple/r_307_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_308_100.jpg b/examples/data/Fruit360/apple/r_308_100.jpg new file mode 100644 index 00000000..a3058da8 Binary files /dev/null and b/examples/data/Fruit360/apple/r_308_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_309_100.jpg b/examples/data/Fruit360/apple/r_309_100.jpg new file mode 100644 index 00000000..e99e213f Binary files /dev/null and b/examples/data/Fruit360/apple/r_309_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_30_100.jpg b/examples/data/Fruit360/apple/r_30_100.jpg new file mode 100644 index 00000000..bb3f6281 Binary files /dev/null and b/examples/data/Fruit360/apple/r_30_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_310_100.jpg b/examples/data/Fruit360/apple/r_310_100.jpg new file mode 100644 index 00000000..287a4cbb Binary files /dev/null and b/examples/data/Fruit360/apple/r_310_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_311_100.jpg b/examples/data/Fruit360/apple/r_311_100.jpg new file mode 100644 index 00000000..5bf8c6a5 Binary files /dev/null and b/examples/data/Fruit360/apple/r_311_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_312_100.jpg b/examples/data/Fruit360/apple/r_312_100.jpg new file mode 100644 index 00000000..1b18510a Binary files /dev/null and b/examples/data/Fruit360/apple/r_312_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_313_100.jpg b/examples/data/Fruit360/apple/r_313_100.jpg new file mode 100644 index 00000000..495329fc Binary files /dev/null and b/examples/data/Fruit360/apple/r_313_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_314_100.jpg b/examples/data/Fruit360/apple/r_314_100.jpg new file mode 100644 index 00000000..f88ffb26 Binary files /dev/null and b/examples/data/Fruit360/apple/r_314_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_315_100.jpg b/examples/data/Fruit360/apple/r_315_100.jpg new file mode 100644 index 00000000..3a61aedf Binary files /dev/null and b/examples/data/Fruit360/apple/r_315_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_316_100.jpg b/examples/data/Fruit360/apple/r_316_100.jpg new file mode 100644 index 00000000..de069a88 Binary files /dev/null and b/examples/data/Fruit360/apple/r_316_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_317_100.jpg b/examples/data/Fruit360/apple/r_317_100.jpg new file mode 100644 index 00000000..f368e97e Binary files /dev/null and b/examples/data/Fruit360/apple/r_317_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_318_100.jpg b/examples/data/Fruit360/apple/r_318_100.jpg new file mode 100644 index 00000000..ad89371a Binary files /dev/null and b/examples/data/Fruit360/apple/r_318_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_319_100.jpg b/examples/data/Fruit360/apple/r_319_100.jpg new file mode 100644 index 00000000..5091b5d8 Binary files /dev/null and b/examples/data/Fruit360/apple/r_319_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_31_100.jpg b/examples/data/Fruit360/apple/r_31_100.jpg new file mode 100644 index 00000000..268bbc10 Binary files /dev/null and b/examples/data/Fruit360/apple/r_31_100.jpg differ diff --git a/examples/data/Fruit360/apple/r_320_100.jpg b/examples/data/Fruit360/apple/r_320_100.jpg new file mode 100644 index 00000000..d33d52aa Binary files /dev/null and b/examples/data/Fruit360/apple/r_320_100.jpg differ diff --git a/examples/data/Fruit360/lemon/0_100.jpg b/examples/data/Fruit360/lemon/0_100.jpg new file mode 100644 index 00000000..513d32c7 Binary files /dev/null and b/examples/data/Fruit360/lemon/0_100.jpg differ diff --git a/examples/data/Fruit360/lemon/100_100.jpg b/examples/data/Fruit360/lemon/100_100.jpg new file mode 100644 index 00000000..298ccc45 Binary files /dev/null and b/examples/data/Fruit360/lemon/100_100.jpg differ diff --git a/examples/data/Fruit360/lemon/101_100.jpg b/examples/data/Fruit360/lemon/101_100.jpg new file mode 100644 index 00000000..3d5a8018 Binary files /dev/null and b/examples/data/Fruit360/lemon/101_100.jpg differ diff --git a/examples/data/Fruit360/lemon/102_100.jpg b/examples/data/Fruit360/lemon/102_100.jpg new file mode 100644 index 00000000..02bccfe8 Binary files /dev/null and b/examples/data/Fruit360/lemon/102_100.jpg differ diff --git a/examples/data/Fruit360/lemon/103_100.jpg b/examples/data/Fruit360/lemon/103_100.jpg new file mode 100644 index 00000000..ed841a85 Binary files /dev/null and b/examples/data/Fruit360/lemon/103_100.jpg differ diff --git a/examples/data/Fruit360/lemon/104_100.jpg b/examples/data/Fruit360/lemon/104_100.jpg new file mode 100644 index 00000000..bef1daed Binary files /dev/null and b/examples/data/Fruit360/lemon/104_100.jpg differ diff --git a/examples/data/Fruit360/lemon/105_100.jpg b/examples/data/Fruit360/lemon/105_100.jpg new file mode 100644 index 00000000..38812475 Binary files /dev/null and b/examples/data/Fruit360/lemon/105_100.jpg differ diff --git a/examples/data/Fruit360/lemon/106_100.jpg b/examples/data/Fruit360/lemon/106_100.jpg new file mode 100644 index 00000000..c3101dc1 Binary files /dev/null and b/examples/data/Fruit360/lemon/106_100.jpg differ diff --git a/examples/data/Fruit360/lemon/107_100.jpg b/examples/data/Fruit360/lemon/107_100.jpg new file mode 100644 index 00000000..fc991791 Binary files /dev/null and b/examples/data/Fruit360/lemon/107_100.jpg differ diff --git a/examples/data/Fruit360/lemon/108_100.jpg b/examples/data/Fruit360/lemon/108_100.jpg new file mode 100644 index 00000000..2387a3a5 Binary files /dev/null and b/examples/data/Fruit360/lemon/108_100.jpg differ diff --git a/examples/data/Fruit360/lemon/109_100.jpg b/examples/data/Fruit360/lemon/109_100.jpg new file mode 100644 index 00000000..422a5b16 Binary files /dev/null and b/examples/data/Fruit360/lemon/109_100.jpg differ diff --git a/examples/data/Fruit360/lemon/10_100.jpg b/examples/data/Fruit360/lemon/10_100.jpg new file mode 100644 index 00000000..a4951bdf Binary files /dev/null and b/examples/data/Fruit360/lemon/10_100.jpg differ diff --git a/examples/data/Fruit360/lemon/110_100.jpg b/examples/data/Fruit360/lemon/110_100.jpg new file mode 100644 index 00000000..01509f59 Binary files /dev/null and b/examples/data/Fruit360/lemon/110_100.jpg differ diff --git a/examples/data/Fruit360/lemon/111_100.jpg b/examples/data/Fruit360/lemon/111_100.jpg new file mode 100644 index 00000000..1724908c Binary files /dev/null and b/examples/data/Fruit360/lemon/111_100.jpg differ diff --git a/examples/data/Fruit360/lemon/112_100.jpg b/examples/data/Fruit360/lemon/112_100.jpg new file mode 100644 index 00000000..b6756c37 Binary files /dev/null and b/examples/data/Fruit360/lemon/112_100.jpg differ diff --git a/examples/data/Fruit360/lemon/113_100.jpg b/examples/data/Fruit360/lemon/113_100.jpg new file mode 100644 index 00000000..3d8c4c1d Binary files /dev/null and b/examples/data/Fruit360/lemon/113_100.jpg differ diff --git a/examples/data/Fruit360/lemon/114_100.jpg b/examples/data/Fruit360/lemon/114_100.jpg new file mode 100644 index 00000000..10d084f4 Binary files /dev/null and b/examples/data/Fruit360/lemon/114_100.jpg differ diff --git a/examples/data/Fruit360/lemon/115_100.jpg b/examples/data/Fruit360/lemon/115_100.jpg new file mode 100644 index 00000000..e4b91d91 Binary files /dev/null and b/examples/data/Fruit360/lemon/115_100.jpg differ diff --git a/examples/data/Fruit360/lemon/116_100.jpg b/examples/data/Fruit360/lemon/116_100.jpg new file mode 100644 index 00000000..8fb8118f Binary files /dev/null and b/examples/data/Fruit360/lemon/116_100.jpg differ diff --git a/examples/data/Fruit360/lemon/117_100.jpg b/examples/data/Fruit360/lemon/117_100.jpg new file mode 100644 index 00000000..dff9bfd4 Binary files /dev/null and b/examples/data/Fruit360/lemon/117_100.jpg differ diff --git a/examples/data/Fruit360/lemon/118_100.jpg b/examples/data/Fruit360/lemon/118_100.jpg new file mode 100644 index 00000000..c638fb78 Binary files /dev/null and b/examples/data/Fruit360/lemon/118_100.jpg differ diff --git a/examples/data/Fruit360/lemon/119_100.jpg b/examples/data/Fruit360/lemon/119_100.jpg new file mode 100644 index 00000000..e0b09351 Binary files /dev/null and b/examples/data/Fruit360/lemon/119_100.jpg differ diff --git a/examples/data/Fruit360/lemon/11_100.jpg b/examples/data/Fruit360/lemon/11_100.jpg new file mode 100644 index 00000000..7221eb63 Binary files /dev/null and b/examples/data/Fruit360/lemon/11_100.jpg differ diff --git a/examples/data/Fruit360/lemon/120_100.jpg b/examples/data/Fruit360/lemon/120_100.jpg new file mode 100644 index 00000000..f63bb290 Binary files /dev/null and b/examples/data/Fruit360/lemon/120_100.jpg differ diff --git a/examples/data/Fruit360/lemon/121_100.jpg b/examples/data/Fruit360/lemon/121_100.jpg new file mode 100644 index 00000000..3467b4c2 Binary files /dev/null and b/examples/data/Fruit360/lemon/121_100.jpg differ diff --git a/examples/data/Fruit360/lemon/122_100.jpg b/examples/data/Fruit360/lemon/122_100.jpg new file mode 100644 index 00000000..305b0590 Binary files /dev/null and b/examples/data/Fruit360/lemon/122_100.jpg differ diff --git a/examples/data/Fruit360/lemon/123_100.jpg b/examples/data/Fruit360/lemon/123_100.jpg new file mode 100644 index 00000000..3162d6ac Binary files /dev/null and b/examples/data/Fruit360/lemon/123_100.jpg differ diff --git a/examples/data/Fruit360/lemon/124_100.jpg b/examples/data/Fruit360/lemon/124_100.jpg new file mode 100644 index 00000000..2cabd4d4 Binary files /dev/null and b/examples/data/Fruit360/lemon/124_100.jpg differ diff --git a/examples/data/Fruit360/lemon/125_100.jpg b/examples/data/Fruit360/lemon/125_100.jpg new file mode 100644 index 00000000..ffcd1bb0 Binary files /dev/null and b/examples/data/Fruit360/lemon/125_100.jpg differ diff --git a/examples/data/Fruit360/lemon/126_100.jpg b/examples/data/Fruit360/lemon/126_100.jpg new file mode 100644 index 00000000..538e56be Binary files /dev/null and b/examples/data/Fruit360/lemon/126_100.jpg differ diff --git a/examples/data/Fruit360/lemon/127_100.jpg b/examples/data/Fruit360/lemon/127_100.jpg new file mode 100644 index 00000000..e4c84de1 Binary files /dev/null and b/examples/data/Fruit360/lemon/127_100.jpg differ diff --git a/examples/data/Fruit360/lemon/128_100.jpg b/examples/data/Fruit360/lemon/128_100.jpg new file mode 100644 index 00000000..209796bb Binary files /dev/null and b/examples/data/Fruit360/lemon/128_100.jpg differ diff --git a/examples/data/Fruit360/lemon/129_100.jpg b/examples/data/Fruit360/lemon/129_100.jpg new file mode 100644 index 00000000..21f08612 Binary files /dev/null and b/examples/data/Fruit360/lemon/129_100.jpg differ diff --git a/examples/data/Fruit360/lemon/12_100.jpg b/examples/data/Fruit360/lemon/12_100.jpg new file mode 100644 index 00000000..691abf3b Binary files /dev/null and b/examples/data/Fruit360/lemon/12_100.jpg differ diff --git a/examples/data/Fruit360/lemon/130_100.jpg b/examples/data/Fruit360/lemon/130_100.jpg new file mode 100644 index 00000000..4240a594 Binary files /dev/null and b/examples/data/Fruit360/lemon/130_100.jpg differ diff --git a/examples/data/Fruit360/lemon/131_100.jpg b/examples/data/Fruit360/lemon/131_100.jpg new file mode 100644 index 00000000..d586ea34 Binary files /dev/null and b/examples/data/Fruit360/lemon/131_100.jpg differ diff --git a/examples/data/Fruit360/lemon/132_100.jpg b/examples/data/Fruit360/lemon/132_100.jpg new file mode 100644 index 00000000..0e04e2c7 Binary files /dev/null and b/examples/data/Fruit360/lemon/132_100.jpg differ diff --git a/examples/data/Fruit360/lemon/133_100.jpg b/examples/data/Fruit360/lemon/133_100.jpg new file mode 100644 index 00000000..fa4a6f7b Binary files /dev/null and b/examples/data/Fruit360/lemon/133_100.jpg differ diff --git a/examples/data/Fruit360/lemon/134_100.jpg b/examples/data/Fruit360/lemon/134_100.jpg new file mode 100644 index 00000000..1eabadf2 Binary files /dev/null and b/examples/data/Fruit360/lemon/134_100.jpg differ diff --git a/examples/data/Fruit360/lemon/135_100.jpg b/examples/data/Fruit360/lemon/135_100.jpg new file mode 100644 index 00000000..fa008721 Binary files /dev/null and b/examples/data/Fruit360/lemon/135_100.jpg differ diff --git a/examples/data/Fruit360/lemon/136_100.jpg b/examples/data/Fruit360/lemon/136_100.jpg new file mode 100644 index 00000000..8a9022ba Binary files /dev/null and b/examples/data/Fruit360/lemon/136_100.jpg differ diff --git a/examples/data/Fruit360/lemon/137_100.jpg b/examples/data/Fruit360/lemon/137_100.jpg new file mode 100644 index 00000000..2544d947 Binary files /dev/null and b/examples/data/Fruit360/lemon/137_100.jpg differ diff --git a/examples/data/Fruit360/lemon/138_100.jpg b/examples/data/Fruit360/lemon/138_100.jpg new file mode 100644 index 00000000..2195d957 Binary files /dev/null and b/examples/data/Fruit360/lemon/138_100.jpg differ diff --git a/examples/data/Fruit360/lemon/139_100.jpg b/examples/data/Fruit360/lemon/139_100.jpg new file mode 100644 index 00000000..22755d12 Binary files /dev/null and b/examples/data/Fruit360/lemon/139_100.jpg differ diff --git a/examples/data/Fruit360/lemon/13_100.jpg b/examples/data/Fruit360/lemon/13_100.jpg new file mode 100644 index 00000000..588d3705 Binary files /dev/null and b/examples/data/Fruit360/lemon/13_100.jpg differ diff --git a/examples/data/Fruit360/lemon/140_100.jpg b/examples/data/Fruit360/lemon/140_100.jpg new file mode 100644 index 00000000..c7b5df03 Binary files /dev/null and b/examples/data/Fruit360/lemon/140_100.jpg differ diff --git a/examples/data/Fruit360/lemon/141_100.jpg b/examples/data/Fruit360/lemon/141_100.jpg new file mode 100644 index 00000000..26587e66 Binary files /dev/null and b/examples/data/Fruit360/lemon/141_100.jpg differ diff --git a/examples/data/Fruit360/lemon/142_100.jpg b/examples/data/Fruit360/lemon/142_100.jpg new file mode 100644 index 00000000..f6dc1cbd Binary files /dev/null and b/examples/data/Fruit360/lemon/142_100.jpg differ diff --git a/examples/data/Fruit360/lemon/143_100.jpg b/examples/data/Fruit360/lemon/143_100.jpg new file mode 100644 index 00000000..04bc6b92 Binary files /dev/null and b/examples/data/Fruit360/lemon/143_100.jpg differ diff --git a/examples/data/Fruit360/lemon/144_100.jpg b/examples/data/Fruit360/lemon/144_100.jpg new file mode 100644 index 00000000..44870d77 Binary files /dev/null and b/examples/data/Fruit360/lemon/144_100.jpg differ diff --git a/examples/data/Fruit360/lemon/145_100.jpg b/examples/data/Fruit360/lemon/145_100.jpg new file mode 100644 index 00000000..45d8ad71 Binary files /dev/null and b/examples/data/Fruit360/lemon/145_100.jpg differ diff --git a/examples/data/Fruit360/lemon/146_100.jpg b/examples/data/Fruit360/lemon/146_100.jpg new file mode 100644 index 00000000..5df2900d Binary files /dev/null and b/examples/data/Fruit360/lemon/146_100.jpg differ diff --git a/examples/data/Fruit360/lemon/14_100.jpg b/examples/data/Fruit360/lemon/14_100.jpg new file mode 100644 index 00000000..af818406 Binary files /dev/null and b/examples/data/Fruit360/lemon/14_100.jpg differ diff --git a/examples/data/Fruit360/lemon/15_100.jpg b/examples/data/Fruit360/lemon/15_100.jpg new file mode 100644 index 00000000..6b02c7b8 Binary files /dev/null and b/examples/data/Fruit360/lemon/15_100.jpg differ diff --git a/examples/data/Fruit360/lemon/167_100.jpg b/examples/data/Fruit360/lemon/167_100.jpg new file mode 100644 index 00000000..107ef301 Binary files /dev/null and b/examples/data/Fruit360/lemon/167_100.jpg differ diff --git a/examples/data/Fruit360/lemon/169_100.jpg b/examples/data/Fruit360/lemon/169_100.jpg new file mode 100644 index 00000000..f533a5c9 Binary files /dev/null and b/examples/data/Fruit360/lemon/169_100.jpg differ diff --git a/examples/data/Fruit360/lemon/16_100.jpg b/examples/data/Fruit360/lemon/16_100.jpg new file mode 100644 index 00000000..f33f33ec Binary files /dev/null and b/examples/data/Fruit360/lemon/16_100.jpg differ diff --git a/examples/data/Fruit360/lemon/170_100.jpg b/examples/data/Fruit360/lemon/170_100.jpg new file mode 100644 index 00000000..f2b38188 Binary files /dev/null and b/examples/data/Fruit360/lemon/170_100.jpg differ diff --git a/examples/data/Fruit360/lemon/171_100.jpg b/examples/data/Fruit360/lemon/171_100.jpg new file mode 100644 index 00000000..067f2566 Binary files /dev/null and b/examples/data/Fruit360/lemon/171_100.jpg differ diff --git a/examples/data/Fruit360/lemon/172_100.jpg b/examples/data/Fruit360/lemon/172_100.jpg new file mode 100644 index 00000000..f30e5c01 Binary files /dev/null and b/examples/data/Fruit360/lemon/172_100.jpg differ diff --git a/examples/data/Fruit360/lemon/173_100.jpg b/examples/data/Fruit360/lemon/173_100.jpg new file mode 100644 index 00000000..a29713e8 Binary files /dev/null and b/examples/data/Fruit360/lemon/173_100.jpg differ diff --git a/examples/data/Fruit360/lemon/174_100.jpg b/examples/data/Fruit360/lemon/174_100.jpg new file mode 100644 index 00000000..aec21f6d Binary files /dev/null and b/examples/data/Fruit360/lemon/174_100.jpg differ diff --git a/examples/data/Fruit360/lemon/176_100.jpg b/examples/data/Fruit360/lemon/176_100.jpg new file mode 100644 index 00000000..1e08acdc Binary files /dev/null and b/examples/data/Fruit360/lemon/176_100.jpg differ diff --git a/examples/data/Fruit360/lemon/178_100.jpg b/examples/data/Fruit360/lemon/178_100.jpg new file mode 100644 index 00000000..dc79d2d1 Binary files /dev/null and b/examples/data/Fruit360/lemon/178_100.jpg differ diff --git a/examples/data/Fruit360/lemon/179_100.jpg b/examples/data/Fruit360/lemon/179_100.jpg new file mode 100644 index 00000000..80ab87e1 Binary files /dev/null and b/examples/data/Fruit360/lemon/179_100.jpg differ diff --git a/examples/data/Fruit360/lemon/17_100.jpg b/examples/data/Fruit360/lemon/17_100.jpg new file mode 100644 index 00000000..27f47f07 Binary files /dev/null and b/examples/data/Fruit360/lemon/17_100.jpg differ diff --git a/examples/data/Fruit360/lemon/180_100.jpg b/examples/data/Fruit360/lemon/180_100.jpg new file mode 100644 index 00000000..e7328fd5 Binary files /dev/null and b/examples/data/Fruit360/lemon/180_100.jpg differ diff --git a/examples/data/Fruit360/lemon/181_100.jpg b/examples/data/Fruit360/lemon/181_100.jpg new file mode 100644 index 00000000..6aed01ce Binary files /dev/null and b/examples/data/Fruit360/lemon/181_100.jpg differ diff --git a/examples/data/Fruit360/lemon/182_100.jpg b/examples/data/Fruit360/lemon/182_100.jpg new file mode 100644 index 00000000..03bca35f Binary files /dev/null and b/examples/data/Fruit360/lemon/182_100.jpg differ diff --git a/examples/data/Fruit360/lemon/183_100.jpg b/examples/data/Fruit360/lemon/183_100.jpg new file mode 100644 index 00000000..b639901a Binary files /dev/null and b/examples/data/Fruit360/lemon/183_100.jpg differ diff --git a/examples/data/Fruit360/lemon/184_100.jpg b/examples/data/Fruit360/lemon/184_100.jpg new file mode 100644 index 00000000..7dd724e9 Binary files /dev/null and b/examples/data/Fruit360/lemon/184_100.jpg differ diff --git a/examples/data/Fruit360/lemon/185_100.jpg b/examples/data/Fruit360/lemon/185_100.jpg new file mode 100644 index 00000000..187ea097 Binary files /dev/null and b/examples/data/Fruit360/lemon/185_100.jpg differ diff --git a/examples/data/Fruit360/lemon/186_100.jpg b/examples/data/Fruit360/lemon/186_100.jpg new file mode 100644 index 00000000..325b334b Binary files /dev/null and b/examples/data/Fruit360/lemon/186_100.jpg differ diff --git a/examples/data/Fruit360/lemon/187_100.jpg b/examples/data/Fruit360/lemon/187_100.jpg new file mode 100644 index 00000000..57c58eb7 Binary files /dev/null and b/examples/data/Fruit360/lemon/187_100.jpg differ diff --git a/examples/data/Fruit360/lemon/188_100.jpg b/examples/data/Fruit360/lemon/188_100.jpg new file mode 100644 index 00000000..c6c2b0cf Binary files /dev/null and b/examples/data/Fruit360/lemon/188_100.jpg differ diff --git a/examples/data/Fruit360/lemon/18_100.jpg b/examples/data/Fruit360/lemon/18_100.jpg new file mode 100644 index 00000000..ae927f31 Binary files /dev/null and b/examples/data/Fruit360/lemon/18_100.jpg differ diff --git a/examples/data/Fruit360/lemon/1_100.jpg b/examples/data/Fruit360/lemon/1_100.jpg new file mode 100644 index 00000000..c5c5b50a Binary files /dev/null and b/examples/data/Fruit360/lemon/1_100.jpg differ diff --git a/examples/data/Fruit360/lemon/20_100.jpg b/examples/data/Fruit360/lemon/20_100.jpg new file mode 100644 index 00000000..ead33c30 Binary files /dev/null and b/examples/data/Fruit360/lemon/20_100.jpg differ diff --git a/examples/data/Fruit360/lemon/215_100.jpg b/examples/data/Fruit360/lemon/215_100.jpg new file mode 100644 index 00000000..ca61ddf9 Binary files /dev/null and b/examples/data/Fruit360/lemon/215_100.jpg differ diff --git a/examples/data/Fruit360/lemon/216_100.jpg b/examples/data/Fruit360/lemon/216_100.jpg new file mode 100644 index 00000000..dc3f2eef Binary files /dev/null and b/examples/data/Fruit360/lemon/216_100.jpg differ diff --git a/examples/data/Fruit360/lemon/217_100.jpg b/examples/data/Fruit360/lemon/217_100.jpg new file mode 100644 index 00000000..673cd425 Binary files /dev/null and b/examples/data/Fruit360/lemon/217_100.jpg differ diff --git a/examples/data/Fruit360/lemon/218_100.jpg b/examples/data/Fruit360/lemon/218_100.jpg new file mode 100644 index 00000000..e00d9a87 Binary files /dev/null and b/examples/data/Fruit360/lemon/218_100.jpg differ diff --git a/examples/data/Fruit360/lemon/219_100.jpg b/examples/data/Fruit360/lemon/219_100.jpg new file mode 100644 index 00000000..4e108140 Binary files /dev/null and b/examples/data/Fruit360/lemon/219_100.jpg differ diff --git a/examples/data/Fruit360/lemon/21_100.jpg b/examples/data/Fruit360/lemon/21_100.jpg new file mode 100644 index 00000000..ee231197 Binary files /dev/null and b/examples/data/Fruit360/lemon/21_100.jpg differ diff --git a/examples/data/Fruit360/lemon/220_100.jpg b/examples/data/Fruit360/lemon/220_100.jpg new file mode 100644 index 00000000..767f3200 Binary files /dev/null and b/examples/data/Fruit360/lemon/220_100.jpg differ diff --git a/examples/data/Fruit360/lemon/221_100.jpg b/examples/data/Fruit360/lemon/221_100.jpg new file mode 100644 index 00000000..1fde8ed0 Binary files /dev/null and b/examples/data/Fruit360/lemon/221_100.jpg differ diff --git a/examples/data/Fruit360/lemon/222_100.jpg b/examples/data/Fruit360/lemon/222_100.jpg new file mode 100644 index 00000000..4bbee3d0 Binary files /dev/null and b/examples/data/Fruit360/lemon/222_100.jpg differ diff --git a/examples/data/Fruit360/lemon/223_100.jpg b/examples/data/Fruit360/lemon/223_100.jpg new file mode 100644 index 00000000..97d5838e Binary files /dev/null and b/examples/data/Fruit360/lemon/223_100.jpg differ diff --git a/examples/data/Fruit360/lemon/224_100.jpg b/examples/data/Fruit360/lemon/224_100.jpg new file mode 100644 index 00000000..6987cf69 Binary files /dev/null and b/examples/data/Fruit360/lemon/224_100.jpg differ diff --git a/examples/data/Fruit360/lemon/225_100.jpg b/examples/data/Fruit360/lemon/225_100.jpg new file mode 100644 index 00000000..55f5404b Binary files /dev/null and b/examples/data/Fruit360/lemon/225_100.jpg differ diff --git a/examples/data/Fruit360/lemon/226_100.jpg b/examples/data/Fruit360/lemon/226_100.jpg new file mode 100644 index 00000000..03a831bd Binary files /dev/null and b/examples/data/Fruit360/lemon/226_100.jpg differ diff --git a/examples/data/Fruit360/lemon/227_100.jpg b/examples/data/Fruit360/lemon/227_100.jpg new file mode 100644 index 00000000..2e6eca8d Binary files /dev/null and b/examples/data/Fruit360/lemon/227_100.jpg differ diff --git a/examples/data/Fruit360/lemon/228_100.jpg b/examples/data/Fruit360/lemon/228_100.jpg new file mode 100644 index 00000000..15d8d779 Binary files /dev/null and b/examples/data/Fruit360/lemon/228_100.jpg differ diff --git a/examples/data/Fruit360/lemon/229_100.jpg b/examples/data/Fruit360/lemon/229_100.jpg new file mode 100644 index 00000000..048fa5fd Binary files /dev/null and b/examples/data/Fruit360/lemon/229_100.jpg differ diff --git a/examples/data/Fruit360/lemon/22_100.jpg b/examples/data/Fruit360/lemon/22_100.jpg new file mode 100644 index 00000000..6c56d4df Binary files /dev/null and b/examples/data/Fruit360/lemon/22_100.jpg differ diff --git a/examples/data/Fruit360/lemon/230_100.jpg b/examples/data/Fruit360/lemon/230_100.jpg new file mode 100644 index 00000000..bb10c8f5 Binary files /dev/null and b/examples/data/Fruit360/lemon/230_100.jpg differ diff --git a/examples/data/Fruit360/lemon/231_100.jpg b/examples/data/Fruit360/lemon/231_100.jpg new file mode 100644 index 00000000..467440f1 Binary files /dev/null and b/examples/data/Fruit360/lemon/231_100.jpg differ diff --git a/examples/data/Fruit360/lemon/232_100.jpg b/examples/data/Fruit360/lemon/232_100.jpg new file mode 100644 index 00000000..6e21125a Binary files /dev/null and b/examples/data/Fruit360/lemon/232_100.jpg differ diff --git a/examples/data/Fruit360/lemon/233_100.jpg b/examples/data/Fruit360/lemon/233_100.jpg new file mode 100644 index 00000000..db7cc870 Binary files /dev/null and b/examples/data/Fruit360/lemon/233_100.jpg differ diff --git a/examples/data/Fruit360/lemon/234_100.jpg b/examples/data/Fruit360/lemon/234_100.jpg new file mode 100644 index 00000000..9690c239 Binary files /dev/null and b/examples/data/Fruit360/lemon/234_100.jpg differ diff --git a/examples/data/Fruit360/lemon/235_100.jpg b/examples/data/Fruit360/lemon/235_100.jpg new file mode 100644 index 00000000..0895be53 Binary files /dev/null and b/examples/data/Fruit360/lemon/235_100.jpg differ diff --git a/examples/data/Fruit360/lemon/236_100.jpg b/examples/data/Fruit360/lemon/236_100.jpg new file mode 100644 index 00000000..1c046c28 Binary files /dev/null and b/examples/data/Fruit360/lemon/236_100.jpg differ diff --git a/examples/data/Fruit360/lemon/237_100.jpg b/examples/data/Fruit360/lemon/237_100.jpg new file mode 100644 index 00000000..1c219013 Binary files /dev/null and b/examples/data/Fruit360/lemon/237_100.jpg differ diff --git a/examples/data/Fruit360/lemon/238_100.jpg b/examples/data/Fruit360/lemon/238_100.jpg new file mode 100644 index 00000000..2de2a9e1 Binary files /dev/null and b/examples/data/Fruit360/lemon/238_100.jpg differ diff --git a/examples/data/Fruit360/lemon/239_100.jpg b/examples/data/Fruit360/lemon/239_100.jpg new file mode 100644 index 00000000..df8cafbb Binary files /dev/null and b/examples/data/Fruit360/lemon/239_100.jpg differ diff --git a/examples/data/Fruit360/lemon/23_100.jpg b/examples/data/Fruit360/lemon/23_100.jpg new file mode 100644 index 00000000..fdf36dc7 Binary files /dev/null and b/examples/data/Fruit360/lemon/23_100.jpg differ diff --git a/examples/data/Fruit360/lemon/240_100.jpg b/examples/data/Fruit360/lemon/240_100.jpg new file mode 100644 index 00000000..1812f1fd Binary files /dev/null and b/examples/data/Fruit360/lemon/240_100.jpg differ diff --git a/examples/data/Fruit360/lemon/241_100.jpg b/examples/data/Fruit360/lemon/241_100.jpg new file mode 100644 index 00000000..6588ef0e Binary files /dev/null and b/examples/data/Fruit360/lemon/241_100.jpg differ diff --git a/examples/data/Fruit360/lemon/242_100.jpg b/examples/data/Fruit360/lemon/242_100.jpg new file mode 100644 index 00000000..18ff9486 Binary files /dev/null and b/examples/data/Fruit360/lemon/242_100.jpg differ diff --git a/examples/data/Fruit360/lemon/243_100.jpg b/examples/data/Fruit360/lemon/243_100.jpg new file mode 100644 index 00000000..1362a4e0 Binary files /dev/null and b/examples/data/Fruit360/lemon/243_100.jpg differ diff --git a/examples/data/Fruit360/lemon/244_100.jpg b/examples/data/Fruit360/lemon/244_100.jpg new file mode 100644 index 00000000..198ead43 Binary files /dev/null and b/examples/data/Fruit360/lemon/244_100.jpg differ diff --git a/examples/data/Fruit360/lemon/245_100.jpg b/examples/data/Fruit360/lemon/245_100.jpg new file mode 100644 index 00000000..efcdba69 Binary files /dev/null and b/examples/data/Fruit360/lemon/245_100.jpg differ diff --git a/examples/data/Fruit360/lemon/246_100.jpg b/examples/data/Fruit360/lemon/246_100.jpg new file mode 100644 index 00000000..8d52c754 Binary files /dev/null and b/examples/data/Fruit360/lemon/246_100.jpg differ diff --git a/examples/data/Fruit360/lemon/247_100.jpg b/examples/data/Fruit360/lemon/247_100.jpg new file mode 100644 index 00000000..adce19ff Binary files /dev/null and b/examples/data/Fruit360/lemon/247_100.jpg differ diff --git a/examples/data/Fruit360/lemon/248_100.jpg b/examples/data/Fruit360/lemon/248_100.jpg new file mode 100644 index 00000000..be9f291e Binary files /dev/null and b/examples/data/Fruit360/lemon/248_100.jpg differ diff --git a/examples/data/Fruit360/lemon/249_100.jpg b/examples/data/Fruit360/lemon/249_100.jpg new file mode 100644 index 00000000..381851e7 Binary files /dev/null and b/examples/data/Fruit360/lemon/249_100.jpg differ diff --git a/examples/data/Fruit360/lemon/24_100.jpg b/examples/data/Fruit360/lemon/24_100.jpg new file mode 100644 index 00000000..c8a36a0d Binary files /dev/null and b/examples/data/Fruit360/lemon/24_100.jpg differ diff --git a/examples/data/Fruit360/lemon/250_100.jpg b/examples/data/Fruit360/lemon/250_100.jpg new file mode 100644 index 00000000..25ab60d8 Binary files /dev/null and b/examples/data/Fruit360/lemon/250_100.jpg differ diff --git a/examples/data/Fruit360/lemon/251_100.jpg b/examples/data/Fruit360/lemon/251_100.jpg new file mode 100644 index 00000000..b8583e87 Binary files /dev/null and b/examples/data/Fruit360/lemon/251_100.jpg differ diff --git a/examples/data/Fruit360/lemon/252_100.jpg b/examples/data/Fruit360/lemon/252_100.jpg new file mode 100644 index 00000000..821aeb47 Binary files /dev/null and b/examples/data/Fruit360/lemon/252_100.jpg differ diff --git a/examples/data/Fruit360/lemon/253_100.jpg b/examples/data/Fruit360/lemon/253_100.jpg new file mode 100644 index 00000000..734b8ef9 Binary files /dev/null and b/examples/data/Fruit360/lemon/253_100.jpg differ diff --git a/examples/data/Fruit360/lemon/254_100.jpg b/examples/data/Fruit360/lemon/254_100.jpg new file mode 100644 index 00000000..78410905 Binary files /dev/null and b/examples/data/Fruit360/lemon/254_100.jpg differ diff --git a/examples/data/Fruit360/lemon/255_100.jpg b/examples/data/Fruit360/lemon/255_100.jpg new file mode 100644 index 00000000..5bf7938b Binary files /dev/null and b/examples/data/Fruit360/lemon/255_100.jpg differ diff --git a/examples/data/Fruit360/lemon/256_100.jpg b/examples/data/Fruit360/lemon/256_100.jpg new file mode 100644 index 00000000..0573db4b Binary files /dev/null and b/examples/data/Fruit360/lemon/256_100.jpg differ diff --git a/examples/data/Fruit360/lemon/257_100.jpg b/examples/data/Fruit360/lemon/257_100.jpg new file mode 100644 index 00000000..57193274 Binary files /dev/null and b/examples/data/Fruit360/lemon/257_100.jpg differ diff --git a/examples/data/Fruit360/lemon/258_100.jpg b/examples/data/Fruit360/lemon/258_100.jpg new file mode 100644 index 00000000..38704237 Binary files /dev/null and b/examples/data/Fruit360/lemon/258_100.jpg differ diff --git a/examples/data/Fruit360/lemon/259_100.jpg b/examples/data/Fruit360/lemon/259_100.jpg new file mode 100644 index 00000000..7dec9b89 Binary files /dev/null and b/examples/data/Fruit360/lemon/259_100.jpg differ diff --git a/examples/data/Fruit360/lemon/25_100.jpg b/examples/data/Fruit360/lemon/25_100.jpg new file mode 100644 index 00000000..1aab6c9a Binary files /dev/null and b/examples/data/Fruit360/lemon/25_100.jpg differ diff --git a/examples/data/Fruit360/lemon/260_100.jpg b/examples/data/Fruit360/lemon/260_100.jpg new file mode 100644 index 00000000..7da2ecb1 Binary files /dev/null and b/examples/data/Fruit360/lemon/260_100.jpg differ diff --git a/examples/data/Fruit360/lemon/261_100.jpg b/examples/data/Fruit360/lemon/261_100.jpg new file mode 100644 index 00000000..8485d987 Binary files /dev/null and b/examples/data/Fruit360/lemon/261_100.jpg differ diff --git a/examples/data/Fruit360/lemon/262_100.jpg b/examples/data/Fruit360/lemon/262_100.jpg new file mode 100644 index 00000000..659ea1bb Binary files /dev/null and b/examples/data/Fruit360/lemon/262_100.jpg differ diff --git a/examples/data/Fruit360/lemon/263_100.jpg b/examples/data/Fruit360/lemon/263_100.jpg new file mode 100644 index 00000000..12736cb8 Binary files /dev/null and b/examples/data/Fruit360/lemon/263_100.jpg differ diff --git a/examples/data/Fruit360/lemon/264_100.jpg b/examples/data/Fruit360/lemon/264_100.jpg new file mode 100644 index 00000000..c021d491 Binary files /dev/null and b/examples/data/Fruit360/lemon/264_100.jpg differ diff --git a/examples/data/Fruit360/lemon/265_100.jpg b/examples/data/Fruit360/lemon/265_100.jpg new file mode 100644 index 00000000..b7faba4e Binary files /dev/null and b/examples/data/Fruit360/lemon/265_100.jpg differ diff --git a/examples/data/Fruit360/lemon/266_100.jpg b/examples/data/Fruit360/lemon/266_100.jpg new file mode 100644 index 00000000..8ac04262 Binary files /dev/null and b/examples/data/Fruit360/lemon/266_100.jpg differ diff --git a/examples/data/Fruit360/lemon/267_100.jpg b/examples/data/Fruit360/lemon/267_100.jpg new file mode 100644 index 00000000..e071f87f Binary files /dev/null and b/examples/data/Fruit360/lemon/267_100.jpg differ diff --git a/examples/data/Fruit360/lemon/268_100.jpg b/examples/data/Fruit360/lemon/268_100.jpg new file mode 100644 index 00000000..86d79cec Binary files /dev/null and b/examples/data/Fruit360/lemon/268_100.jpg differ diff --git a/examples/data/Fruit360/lemon/269_100.jpg b/examples/data/Fruit360/lemon/269_100.jpg new file mode 100644 index 00000000..140d2522 Binary files /dev/null and b/examples/data/Fruit360/lemon/269_100.jpg differ diff --git a/examples/data/Fruit360/lemon/270_100.jpg b/examples/data/Fruit360/lemon/270_100.jpg new file mode 100644 index 00000000..eda62aaa Binary files /dev/null and b/examples/data/Fruit360/lemon/270_100.jpg differ diff --git a/examples/data/Fruit360/lemon/271_100.jpg b/examples/data/Fruit360/lemon/271_100.jpg new file mode 100644 index 00000000..c0ec7a16 Binary files /dev/null and b/examples/data/Fruit360/lemon/271_100.jpg differ diff --git a/examples/data/Fruit360/lemon/272_100.jpg b/examples/data/Fruit360/lemon/272_100.jpg new file mode 100644 index 00000000..1b9349d6 Binary files /dev/null and b/examples/data/Fruit360/lemon/272_100.jpg differ diff --git a/examples/data/Fruit360/lemon/273_100.jpg b/examples/data/Fruit360/lemon/273_100.jpg new file mode 100644 index 00000000..b879e6ee Binary files /dev/null and b/examples/data/Fruit360/lemon/273_100.jpg differ diff --git a/examples/data/Fruit360/lemon/274_100.jpg b/examples/data/Fruit360/lemon/274_100.jpg new file mode 100644 index 00000000..815c482e Binary files /dev/null and b/examples/data/Fruit360/lemon/274_100.jpg differ diff --git a/examples/data/Fruit360/lemon/275_100.jpg b/examples/data/Fruit360/lemon/275_100.jpg new file mode 100644 index 00000000..a66bf1d3 Binary files /dev/null and b/examples/data/Fruit360/lemon/275_100.jpg differ diff --git a/examples/data/Fruit360/lemon/276_100.jpg b/examples/data/Fruit360/lemon/276_100.jpg new file mode 100644 index 00000000..d9ffb886 Binary files /dev/null and b/examples/data/Fruit360/lemon/276_100.jpg differ diff --git a/examples/data/Fruit360/lemon/277_100.jpg b/examples/data/Fruit360/lemon/277_100.jpg new file mode 100644 index 00000000..620dc8f1 Binary files /dev/null and b/examples/data/Fruit360/lemon/277_100.jpg differ diff --git a/examples/data/Fruit360/lemon/278_100.jpg b/examples/data/Fruit360/lemon/278_100.jpg new file mode 100644 index 00000000..32575ef6 Binary files /dev/null and b/examples/data/Fruit360/lemon/278_100.jpg differ diff --git a/examples/data/Fruit360/lemon/279_100.jpg b/examples/data/Fruit360/lemon/279_100.jpg new file mode 100644 index 00000000..ef75528b Binary files /dev/null and b/examples/data/Fruit360/lemon/279_100.jpg differ diff --git a/examples/data/Fruit360/lemon/27_100.jpg b/examples/data/Fruit360/lemon/27_100.jpg new file mode 100644 index 00000000..be2f53f0 Binary files /dev/null and b/examples/data/Fruit360/lemon/27_100.jpg differ diff --git a/examples/data/Fruit360/lemon/280_100.jpg b/examples/data/Fruit360/lemon/280_100.jpg new file mode 100644 index 00000000..783aebf6 Binary files /dev/null and b/examples/data/Fruit360/lemon/280_100.jpg differ diff --git a/examples/data/Fruit360/lemon/281_100.jpg b/examples/data/Fruit360/lemon/281_100.jpg new file mode 100644 index 00000000..52223115 Binary files /dev/null and b/examples/data/Fruit360/lemon/281_100.jpg differ diff --git a/examples/data/Fruit360/lemon/282_100.jpg b/examples/data/Fruit360/lemon/282_100.jpg new file mode 100644 index 00000000..4b1aed46 Binary files /dev/null and b/examples/data/Fruit360/lemon/282_100.jpg differ diff --git a/examples/data/Fruit360/lemon/283_100.jpg b/examples/data/Fruit360/lemon/283_100.jpg new file mode 100644 index 00000000..a96b0a12 Binary files /dev/null and b/examples/data/Fruit360/lemon/283_100.jpg differ diff --git a/examples/data/Fruit360/lemon/284_100.jpg b/examples/data/Fruit360/lemon/284_100.jpg new file mode 100644 index 00000000..de09129d Binary files /dev/null and b/examples/data/Fruit360/lemon/284_100.jpg differ diff --git a/examples/data/Fruit360/lemon/285_100.jpg b/examples/data/Fruit360/lemon/285_100.jpg new file mode 100644 index 00000000..dbfdd0f5 Binary files /dev/null and b/examples/data/Fruit360/lemon/285_100.jpg differ diff --git a/examples/data/Fruit360/lemon/286_100.jpg b/examples/data/Fruit360/lemon/286_100.jpg new file mode 100644 index 00000000..fd1993a8 Binary files /dev/null and b/examples/data/Fruit360/lemon/286_100.jpg differ diff --git a/examples/data/Fruit360/lemon/287_100.jpg b/examples/data/Fruit360/lemon/287_100.jpg new file mode 100644 index 00000000..812f7c44 Binary files /dev/null and b/examples/data/Fruit360/lemon/287_100.jpg differ diff --git a/examples/data/Fruit360/lemon/288_100.jpg b/examples/data/Fruit360/lemon/288_100.jpg new file mode 100644 index 00000000..22a0deff Binary files /dev/null and b/examples/data/Fruit360/lemon/288_100.jpg differ diff --git a/examples/data/Fruit360/lemon/289_100.jpg b/examples/data/Fruit360/lemon/289_100.jpg new file mode 100644 index 00000000..0f9b7099 Binary files /dev/null and b/examples/data/Fruit360/lemon/289_100.jpg differ diff --git a/examples/data/Fruit360/lemon/290_100.jpg b/examples/data/Fruit360/lemon/290_100.jpg new file mode 100644 index 00000000..20959797 Binary files /dev/null and b/examples/data/Fruit360/lemon/290_100.jpg differ diff --git a/examples/data/Fruit360/lemon/291_100.jpg b/examples/data/Fruit360/lemon/291_100.jpg new file mode 100644 index 00000000..d70d0794 Binary files /dev/null and b/examples/data/Fruit360/lemon/291_100.jpg differ diff --git a/examples/data/Fruit360/lemon/292_100.jpg b/examples/data/Fruit360/lemon/292_100.jpg new file mode 100644 index 00000000..41dea971 Binary files /dev/null and b/examples/data/Fruit360/lemon/292_100.jpg differ diff --git a/examples/data/Fruit360/lemon/293_100.jpg b/examples/data/Fruit360/lemon/293_100.jpg new file mode 100644 index 00000000..4a917e6a Binary files /dev/null and b/examples/data/Fruit360/lemon/293_100.jpg differ diff --git a/examples/data/Fruit360/lemon/294_100.jpg b/examples/data/Fruit360/lemon/294_100.jpg new file mode 100644 index 00000000..14d52e42 Binary files /dev/null and b/examples/data/Fruit360/lemon/294_100.jpg differ diff --git a/examples/data/Fruit360/lemon/295_100.jpg b/examples/data/Fruit360/lemon/295_100.jpg new file mode 100644 index 00000000..51924472 Binary files /dev/null and b/examples/data/Fruit360/lemon/295_100.jpg differ diff --git a/examples/data/Fruit360/lemon/296_100.jpg b/examples/data/Fruit360/lemon/296_100.jpg new file mode 100644 index 00000000..80f89a8c Binary files /dev/null and b/examples/data/Fruit360/lemon/296_100.jpg differ diff --git a/examples/data/Fruit360/lemon/297_100.jpg b/examples/data/Fruit360/lemon/297_100.jpg new file mode 100644 index 00000000..e6679b24 Binary files /dev/null and b/examples/data/Fruit360/lemon/297_100.jpg differ diff --git a/examples/data/Fruit360/lemon/298_100.jpg b/examples/data/Fruit360/lemon/298_100.jpg new file mode 100644 index 00000000..e10ec86e Binary files /dev/null and b/examples/data/Fruit360/lemon/298_100.jpg differ diff --git a/examples/data/Fruit360/lemon/299_100.jpg b/examples/data/Fruit360/lemon/299_100.jpg new file mode 100644 index 00000000..211eaf9a Binary files /dev/null and b/examples/data/Fruit360/lemon/299_100.jpg differ diff --git a/examples/data/Fruit360/lemon/2_100.jpg b/examples/data/Fruit360/lemon/2_100.jpg new file mode 100644 index 00000000..ca1144e8 Binary files /dev/null and b/examples/data/Fruit360/lemon/2_100.jpg differ diff --git a/examples/data/Fruit360/lemon/300_100.jpg b/examples/data/Fruit360/lemon/300_100.jpg new file mode 100644 index 00000000..6cb826d2 Binary files /dev/null and b/examples/data/Fruit360/lemon/300_100.jpg differ diff --git a/examples/data/Fruit360/lemon/301_100.jpg b/examples/data/Fruit360/lemon/301_100.jpg new file mode 100644 index 00000000..96f12908 Binary files /dev/null and b/examples/data/Fruit360/lemon/301_100.jpg differ diff --git a/examples/data/Fruit360/lemon/302_100.jpg b/examples/data/Fruit360/lemon/302_100.jpg new file mode 100644 index 00000000..d069f4c1 Binary files /dev/null and b/examples/data/Fruit360/lemon/302_100.jpg differ diff --git a/examples/data/Fruit360/lemon/303_100.jpg b/examples/data/Fruit360/lemon/303_100.jpg new file mode 100644 index 00000000..b109e985 Binary files /dev/null and b/examples/data/Fruit360/lemon/303_100.jpg differ diff --git a/examples/data/Fruit360/lemon/304_100.jpg b/examples/data/Fruit360/lemon/304_100.jpg new file mode 100644 index 00000000..5f06e05e Binary files /dev/null and b/examples/data/Fruit360/lemon/304_100.jpg differ diff --git a/examples/data/Fruit360/lemon/305_100.jpg b/examples/data/Fruit360/lemon/305_100.jpg new file mode 100644 index 00000000..df5244c7 Binary files /dev/null and b/examples/data/Fruit360/lemon/305_100.jpg differ diff --git a/examples/data/Fruit360/lemon/306_100.jpg b/examples/data/Fruit360/lemon/306_100.jpg new file mode 100644 index 00000000..ca9b49fd Binary files /dev/null and b/examples/data/Fruit360/lemon/306_100.jpg differ diff --git a/examples/data/Fruit360/lemon/307_100.jpg b/examples/data/Fruit360/lemon/307_100.jpg new file mode 100644 index 00000000..f15a98e0 Binary files /dev/null and b/examples/data/Fruit360/lemon/307_100.jpg differ diff --git a/examples/data/Fruit360/lemon/308_100.jpg b/examples/data/Fruit360/lemon/308_100.jpg new file mode 100644 index 00000000..4e72786f Binary files /dev/null and b/examples/data/Fruit360/lemon/308_100.jpg differ diff --git a/examples/data/Fruit360/lemon/309_100.jpg b/examples/data/Fruit360/lemon/309_100.jpg new file mode 100644 index 00000000..ef4eb636 Binary files /dev/null and b/examples/data/Fruit360/lemon/309_100.jpg differ diff --git a/examples/data/Fruit360/lemon/310_100.jpg b/examples/data/Fruit360/lemon/310_100.jpg new file mode 100644 index 00000000..d106a494 Binary files /dev/null and b/examples/data/Fruit360/lemon/310_100.jpg differ diff --git a/examples/data/Fruit360/lemon/311_100.jpg b/examples/data/Fruit360/lemon/311_100.jpg new file mode 100644 index 00000000..a7621527 Binary files /dev/null and b/examples/data/Fruit360/lemon/311_100.jpg differ diff --git a/examples/data/Fruit360/lemon/312_100.jpg b/examples/data/Fruit360/lemon/312_100.jpg new file mode 100644 index 00000000..cd80c888 Binary files /dev/null and b/examples/data/Fruit360/lemon/312_100.jpg differ diff --git a/examples/data/Fruit360/lemon/313_100.jpg b/examples/data/Fruit360/lemon/313_100.jpg new file mode 100644 index 00000000..e3f0e36d Binary files /dev/null and b/examples/data/Fruit360/lemon/313_100.jpg differ diff --git a/examples/data/Fruit360/lemon/314_100.jpg b/examples/data/Fruit360/lemon/314_100.jpg new file mode 100644 index 00000000..6b56c746 Binary files /dev/null and b/examples/data/Fruit360/lemon/314_100.jpg differ diff --git a/examples/data/Fruit360/lemon/315_100.jpg b/examples/data/Fruit360/lemon/315_100.jpg new file mode 100644 index 00000000..fff6b66f Binary files /dev/null and b/examples/data/Fruit360/lemon/315_100.jpg differ diff --git a/examples/data/Fruit360/lemon/316_100.jpg b/examples/data/Fruit360/lemon/316_100.jpg new file mode 100644 index 00000000..ef5bdd83 Binary files /dev/null and b/examples/data/Fruit360/lemon/316_100.jpg differ diff --git a/examples/data/Fruit360/lemon/317_100.jpg b/examples/data/Fruit360/lemon/317_100.jpg new file mode 100644 index 00000000..6c42e658 Binary files /dev/null and b/examples/data/Fruit360/lemon/317_100.jpg differ diff --git a/examples/data/Fruit360/lemon/318_100.jpg b/examples/data/Fruit360/lemon/318_100.jpg new file mode 100644 index 00000000..96940778 Binary files /dev/null and b/examples/data/Fruit360/lemon/318_100.jpg differ diff --git a/examples/data/Fruit360/lemon/319_100.jpg b/examples/data/Fruit360/lemon/319_100.jpg new file mode 100644 index 00000000..a8f2d4d8 Binary files /dev/null and b/examples/data/Fruit360/lemon/319_100.jpg differ diff --git a/examples/data/Fruit360/lemon/320_100.jpg b/examples/data/Fruit360/lemon/320_100.jpg new file mode 100644 index 00000000..1e8a64f9 Binary files /dev/null and b/examples/data/Fruit360/lemon/320_100.jpg differ diff --git a/examples/data/Fruit360/lemon/321_100.jpg b/examples/data/Fruit360/lemon/321_100.jpg new file mode 100644 index 00000000..897d46e5 Binary files /dev/null and b/examples/data/Fruit360/lemon/321_100.jpg differ diff --git a/examples/data/Fruit360/lemon/322_100.jpg b/examples/data/Fruit360/lemon/322_100.jpg new file mode 100644 index 00000000..7cd3e8a3 Binary files /dev/null and b/examples/data/Fruit360/lemon/322_100.jpg differ diff --git a/examples/data/Fruit360/lemon/323_100.jpg b/examples/data/Fruit360/lemon/323_100.jpg new file mode 100644 index 00000000..638a6806 Binary files /dev/null and b/examples/data/Fruit360/lemon/323_100.jpg differ diff --git a/examples/data/Fruit360/lemon/325_100.jpg b/examples/data/Fruit360/lemon/325_100.jpg new file mode 100644 index 00000000..60e6e22f Binary files /dev/null and b/examples/data/Fruit360/lemon/325_100.jpg differ diff --git a/examples/data/Fruit360/lemon/327_100.jpg b/examples/data/Fruit360/lemon/327_100.jpg new file mode 100644 index 00000000..dcfec975 Binary files /dev/null and b/examples/data/Fruit360/lemon/327_100.jpg differ diff --git a/examples/data/Fruit360/lemon/33_100.jpg b/examples/data/Fruit360/lemon/33_100.jpg new file mode 100644 index 00000000..5684833a Binary files /dev/null and b/examples/data/Fruit360/lemon/33_100.jpg differ diff --git a/examples/data/Fruit360/lemon/34_100.jpg b/examples/data/Fruit360/lemon/34_100.jpg new file mode 100644 index 00000000..635d50f8 Binary files /dev/null and b/examples/data/Fruit360/lemon/34_100.jpg differ diff --git a/examples/data/Fruit360/lemon/35_100.jpg b/examples/data/Fruit360/lemon/35_100.jpg new file mode 100644 index 00000000..dba61fb4 Binary files /dev/null and b/examples/data/Fruit360/lemon/35_100.jpg differ diff --git a/examples/data/Fruit360/lemon/36_100.jpg b/examples/data/Fruit360/lemon/36_100.jpg new file mode 100644 index 00000000..2b309ac7 Binary files /dev/null and b/examples/data/Fruit360/lemon/36_100.jpg differ diff --git a/examples/data/Fruit360/lemon/3_100.jpg b/examples/data/Fruit360/lemon/3_100.jpg new file mode 100644 index 00000000..9bdc655f Binary files /dev/null and b/examples/data/Fruit360/lemon/3_100.jpg differ diff --git a/examples/data/Fruit360/lemon/4_100.jpg b/examples/data/Fruit360/lemon/4_100.jpg new file mode 100644 index 00000000..fcc9d989 Binary files /dev/null and b/examples/data/Fruit360/lemon/4_100.jpg differ diff --git a/examples/data/Fruit360/lemon/5_100.jpg b/examples/data/Fruit360/lemon/5_100.jpg new file mode 100644 index 00000000..77ca1bd8 Binary files /dev/null and b/examples/data/Fruit360/lemon/5_100.jpg differ diff --git a/examples/data/Fruit360/lemon/62_100.jpg b/examples/data/Fruit360/lemon/62_100.jpg new file mode 100644 index 00000000..f462fbd2 Binary files /dev/null and b/examples/data/Fruit360/lemon/62_100.jpg differ diff --git a/examples/data/Fruit360/lemon/63_100.jpg b/examples/data/Fruit360/lemon/63_100.jpg new file mode 100644 index 00000000..1c30033d Binary files /dev/null and b/examples/data/Fruit360/lemon/63_100.jpg differ diff --git a/examples/data/Fruit360/lemon/64_100.jpg b/examples/data/Fruit360/lemon/64_100.jpg new file mode 100644 index 00000000..9e29a65d Binary files /dev/null and b/examples/data/Fruit360/lemon/64_100.jpg differ diff --git a/examples/data/Fruit360/lemon/65_100.jpg b/examples/data/Fruit360/lemon/65_100.jpg new file mode 100644 index 00000000..1a8895cb Binary files /dev/null and b/examples/data/Fruit360/lemon/65_100.jpg differ diff --git a/examples/data/Fruit360/lemon/66_100.jpg b/examples/data/Fruit360/lemon/66_100.jpg new file mode 100644 index 00000000..ded0f1a1 Binary files /dev/null and b/examples/data/Fruit360/lemon/66_100.jpg differ diff --git a/examples/data/Fruit360/lemon/67_100.jpg b/examples/data/Fruit360/lemon/67_100.jpg new file mode 100644 index 00000000..5d7c5226 Binary files /dev/null and b/examples/data/Fruit360/lemon/67_100.jpg differ diff --git a/examples/data/Fruit360/lemon/68_100.jpg b/examples/data/Fruit360/lemon/68_100.jpg new file mode 100644 index 00000000..6a5c4092 Binary files /dev/null and b/examples/data/Fruit360/lemon/68_100.jpg differ diff --git a/examples/data/Fruit360/lemon/69_100.jpg b/examples/data/Fruit360/lemon/69_100.jpg new file mode 100644 index 00000000..aa3963f0 Binary files /dev/null and b/examples/data/Fruit360/lemon/69_100.jpg differ diff --git a/examples/data/Fruit360/lemon/6_100.jpg b/examples/data/Fruit360/lemon/6_100.jpg new file mode 100644 index 00000000..80a57f13 Binary files /dev/null and b/examples/data/Fruit360/lemon/6_100.jpg differ diff --git a/examples/data/Fruit360/lemon/70_100.jpg b/examples/data/Fruit360/lemon/70_100.jpg new file mode 100644 index 00000000..95515b39 Binary files /dev/null and b/examples/data/Fruit360/lemon/70_100.jpg differ diff --git a/examples/data/Fruit360/lemon/71_100.jpg b/examples/data/Fruit360/lemon/71_100.jpg new file mode 100644 index 00000000..1bca6c98 Binary files /dev/null and b/examples/data/Fruit360/lemon/71_100.jpg differ diff --git a/examples/data/Fruit360/lemon/72_100.jpg b/examples/data/Fruit360/lemon/72_100.jpg new file mode 100644 index 00000000..0eb736a2 Binary files /dev/null and b/examples/data/Fruit360/lemon/72_100.jpg differ diff --git a/examples/data/Fruit360/lemon/73_100.jpg b/examples/data/Fruit360/lemon/73_100.jpg new file mode 100644 index 00000000..ae7fd2c2 Binary files /dev/null and b/examples/data/Fruit360/lemon/73_100.jpg differ diff --git a/examples/data/Fruit360/lemon/74_100.jpg b/examples/data/Fruit360/lemon/74_100.jpg new file mode 100644 index 00000000..99275757 Binary files /dev/null and b/examples/data/Fruit360/lemon/74_100.jpg differ diff --git a/examples/data/Fruit360/lemon/75_100.jpg b/examples/data/Fruit360/lemon/75_100.jpg new file mode 100644 index 00000000..173b938a Binary files /dev/null and b/examples/data/Fruit360/lemon/75_100.jpg differ diff --git a/examples/data/Fruit360/lemon/76_100.jpg b/examples/data/Fruit360/lemon/76_100.jpg new file mode 100644 index 00000000..8244de63 Binary files /dev/null and b/examples/data/Fruit360/lemon/76_100.jpg differ diff --git a/examples/data/Fruit360/lemon/77_100.jpg b/examples/data/Fruit360/lemon/77_100.jpg new file mode 100644 index 00000000..8208eda9 Binary files /dev/null and b/examples/data/Fruit360/lemon/77_100.jpg differ diff --git a/examples/data/Fruit360/lemon/78_100.jpg b/examples/data/Fruit360/lemon/78_100.jpg new file mode 100644 index 00000000..21ba8a0c Binary files /dev/null and b/examples/data/Fruit360/lemon/78_100.jpg differ diff --git a/examples/data/Fruit360/lemon/79_100.jpg b/examples/data/Fruit360/lemon/79_100.jpg new file mode 100644 index 00000000..e049a95c Binary files /dev/null and b/examples/data/Fruit360/lemon/79_100.jpg differ diff --git a/examples/data/Fruit360/lemon/7_100.jpg b/examples/data/Fruit360/lemon/7_100.jpg new file mode 100644 index 00000000..8d565881 Binary files /dev/null and b/examples/data/Fruit360/lemon/7_100.jpg differ diff --git a/examples/data/Fruit360/lemon/80_100.jpg b/examples/data/Fruit360/lemon/80_100.jpg new file mode 100644 index 00000000..b45d0114 Binary files /dev/null and b/examples/data/Fruit360/lemon/80_100.jpg differ diff --git a/examples/data/Fruit360/lemon/81_100.jpg b/examples/data/Fruit360/lemon/81_100.jpg new file mode 100644 index 00000000..191b473b Binary files /dev/null and b/examples/data/Fruit360/lemon/81_100.jpg differ diff --git a/examples/data/Fruit360/lemon/82_100.jpg b/examples/data/Fruit360/lemon/82_100.jpg new file mode 100644 index 00000000..2a2b86ec Binary files /dev/null and b/examples/data/Fruit360/lemon/82_100.jpg differ diff --git a/examples/data/Fruit360/lemon/83_100.jpg b/examples/data/Fruit360/lemon/83_100.jpg new file mode 100644 index 00000000..afaa4a2f Binary files /dev/null and b/examples/data/Fruit360/lemon/83_100.jpg differ diff --git a/examples/data/Fruit360/lemon/84_100.jpg b/examples/data/Fruit360/lemon/84_100.jpg new file mode 100644 index 00000000..f083267b Binary files /dev/null and b/examples/data/Fruit360/lemon/84_100.jpg differ diff --git a/examples/data/Fruit360/lemon/85_100.jpg b/examples/data/Fruit360/lemon/85_100.jpg new file mode 100644 index 00000000..b7338ad5 Binary files /dev/null and b/examples/data/Fruit360/lemon/85_100.jpg differ diff --git a/examples/data/Fruit360/lemon/86_100.jpg b/examples/data/Fruit360/lemon/86_100.jpg new file mode 100644 index 00000000..1c9edd76 Binary files /dev/null and b/examples/data/Fruit360/lemon/86_100.jpg differ diff --git a/examples/data/Fruit360/lemon/87_100.jpg b/examples/data/Fruit360/lemon/87_100.jpg new file mode 100644 index 00000000..85cede35 Binary files /dev/null and b/examples/data/Fruit360/lemon/87_100.jpg differ diff --git a/examples/data/Fruit360/lemon/88_100.jpg b/examples/data/Fruit360/lemon/88_100.jpg new file mode 100644 index 00000000..071200dd Binary files /dev/null and b/examples/data/Fruit360/lemon/88_100.jpg differ diff --git a/examples/data/Fruit360/lemon/89_100.jpg b/examples/data/Fruit360/lemon/89_100.jpg new file mode 100644 index 00000000..06a5a5ff Binary files /dev/null and b/examples/data/Fruit360/lemon/89_100.jpg differ diff --git a/examples/data/Fruit360/lemon/8_100.jpg b/examples/data/Fruit360/lemon/8_100.jpg new file mode 100644 index 00000000..4ddc8f60 Binary files /dev/null and b/examples/data/Fruit360/lemon/8_100.jpg differ diff --git a/examples/data/Fruit360/lemon/90_100.jpg b/examples/data/Fruit360/lemon/90_100.jpg new file mode 100644 index 00000000..4ff4c3ab Binary files /dev/null and b/examples/data/Fruit360/lemon/90_100.jpg differ diff --git a/examples/data/Fruit360/lemon/91_100.jpg b/examples/data/Fruit360/lemon/91_100.jpg new file mode 100644 index 00000000..164051cb Binary files /dev/null and b/examples/data/Fruit360/lemon/91_100.jpg differ diff --git a/examples/data/Fruit360/lemon/92_100.jpg b/examples/data/Fruit360/lemon/92_100.jpg new file mode 100644 index 00000000..ea2f6970 Binary files /dev/null and b/examples/data/Fruit360/lemon/92_100.jpg differ diff --git a/examples/data/Fruit360/lemon/93_100.jpg b/examples/data/Fruit360/lemon/93_100.jpg new file mode 100644 index 00000000..423c99be Binary files /dev/null and b/examples/data/Fruit360/lemon/93_100.jpg differ diff --git a/examples/data/Fruit360/lemon/94_100.jpg b/examples/data/Fruit360/lemon/94_100.jpg new file mode 100644 index 00000000..af6b7e68 Binary files /dev/null and b/examples/data/Fruit360/lemon/94_100.jpg differ diff --git a/examples/data/Fruit360/lemon/95_100.jpg b/examples/data/Fruit360/lemon/95_100.jpg new file mode 100644 index 00000000..a54b84b2 Binary files /dev/null and b/examples/data/Fruit360/lemon/95_100.jpg differ diff --git a/examples/data/Fruit360/lemon/96_100.jpg b/examples/data/Fruit360/lemon/96_100.jpg new file mode 100644 index 00000000..00b9d24e Binary files /dev/null and b/examples/data/Fruit360/lemon/96_100.jpg differ diff --git a/examples/data/Fruit360/lemon/97_100.jpg b/examples/data/Fruit360/lemon/97_100.jpg new file mode 100644 index 00000000..cc6f79ac Binary files /dev/null and b/examples/data/Fruit360/lemon/97_100.jpg differ diff --git a/examples/data/Fruit360/lemon/98_100.jpg b/examples/data/Fruit360/lemon/98_100.jpg new file mode 100644 index 00000000..dfca98ed Binary files /dev/null and b/examples/data/Fruit360/lemon/98_100.jpg differ diff --git a/examples/data/Fruit360/lemon/99_100.jpg b/examples/data/Fruit360/lemon/99_100.jpg new file mode 100644 index 00000000..5c65ffae Binary files /dev/null and b/examples/data/Fruit360/lemon/99_100.jpg differ diff --git a/examples/data/Fruit360/lemon/9_100.jpg b/examples/data/Fruit360/lemon/9_100.jpg new file mode 100644 index 00000000..df945f3a Binary files /dev/null and b/examples/data/Fruit360/lemon/9_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_100_100.jpg b/examples/data/Fruit360/lemon/r_100_100.jpg new file mode 100644 index 00000000..5212b3be Binary files /dev/null and b/examples/data/Fruit360/lemon/r_100_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_101_100.jpg b/examples/data/Fruit360/lemon/r_101_100.jpg new file mode 100644 index 00000000..f8c2a32a Binary files /dev/null and b/examples/data/Fruit360/lemon/r_101_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_102_100.jpg b/examples/data/Fruit360/lemon/r_102_100.jpg new file mode 100644 index 00000000..c09d52c6 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_102_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_103_100.jpg b/examples/data/Fruit360/lemon/r_103_100.jpg new file mode 100644 index 00000000..0ea17f25 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_103_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_104_100.jpg b/examples/data/Fruit360/lemon/r_104_100.jpg new file mode 100644 index 00000000..b270df93 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_104_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_105_100.jpg b/examples/data/Fruit360/lemon/r_105_100.jpg new file mode 100644 index 00000000..679939e8 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_105_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_106_100.jpg b/examples/data/Fruit360/lemon/r_106_100.jpg new file mode 100644 index 00000000..57e4cfee Binary files /dev/null and b/examples/data/Fruit360/lemon/r_106_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_107_100.jpg b/examples/data/Fruit360/lemon/r_107_100.jpg new file mode 100644 index 00000000..f0000843 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_107_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_108_100.jpg b/examples/data/Fruit360/lemon/r_108_100.jpg new file mode 100644 index 00000000..a67897e1 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_108_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_109_100.jpg b/examples/data/Fruit360/lemon/r_109_100.jpg new file mode 100644 index 00000000..74145b00 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_109_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_110_100.jpg b/examples/data/Fruit360/lemon/r_110_100.jpg new file mode 100644 index 00000000..b1deb36c Binary files /dev/null and b/examples/data/Fruit360/lemon/r_110_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_111_100.jpg b/examples/data/Fruit360/lemon/r_111_100.jpg new file mode 100644 index 00000000..c1710654 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_111_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_112_100.jpg b/examples/data/Fruit360/lemon/r_112_100.jpg new file mode 100644 index 00000000..65e5f0f9 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_112_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_113_100.jpg b/examples/data/Fruit360/lemon/r_113_100.jpg new file mode 100644 index 00000000..5a659a8f Binary files /dev/null and b/examples/data/Fruit360/lemon/r_113_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_114_100.jpg b/examples/data/Fruit360/lemon/r_114_100.jpg new file mode 100644 index 00000000..a390fa83 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_114_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_115_100.jpg b/examples/data/Fruit360/lemon/r_115_100.jpg new file mode 100644 index 00000000..0e41b38f Binary files /dev/null and b/examples/data/Fruit360/lemon/r_115_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_116_100.jpg b/examples/data/Fruit360/lemon/r_116_100.jpg new file mode 100644 index 00000000..ad747e27 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_116_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_117_100.jpg b/examples/data/Fruit360/lemon/r_117_100.jpg new file mode 100644 index 00000000..7bdf1c42 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_117_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_118_100.jpg b/examples/data/Fruit360/lemon/r_118_100.jpg new file mode 100644 index 00000000..96c0f6db Binary files /dev/null and b/examples/data/Fruit360/lemon/r_118_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_119_100.jpg b/examples/data/Fruit360/lemon/r_119_100.jpg new file mode 100644 index 00000000..c97427a6 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_119_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_120_100.jpg b/examples/data/Fruit360/lemon/r_120_100.jpg new file mode 100644 index 00000000..6f4f7ad2 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_120_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_121_100.jpg b/examples/data/Fruit360/lemon/r_121_100.jpg new file mode 100644 index 00000000..22436409 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_121_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_122_100.jpg b/examples/data/Fruit360/lemon/r_122_100.jpg new file mode 100644 index 00000000..a5ca614e Binary files /dev/null and b/examples/data/Fruit360/lemon/r_122_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_123_100.jpg b/examples/data/Fruit360/lemon/r_123_100.jpg new file mode 100644 index 00000000..9c0ed686 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_123_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_124_100.jpg b/examples/data/Fruit360/lemon/r_124_100.jpg new file mode 100644 index 00000000..597c0404 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_124_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_125_100.jpg b/examples/data/Fruit360/lemon/r_125_100.jpg new file mode 100644 index 00000000..e5feaccf Binary files /dev/null and b/examples/data/Fruit360/lemon/r_125_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_126_100.jpg b/examples/data/Fruit360/lemon/r_126_100.jpg new file mode 100644 index 00000000..fe8ad510 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_126_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_127_100.jpg b/examples/data/Fruit360/lemon/r_127_100.jpg new file mode 100644 index 00000000..9f7b9c77 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_127_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_128_100.jpg b/examples/data/Fruit360/lemon/r_128_100.jpg new file mode 100644 index 00000000..369a9841 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_128_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_129_100.jpg b/examples/data/Fruit360/lemon/r_129_100.jpg new file mode 100644 index 00000000..16534230 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_129_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_130_100.jpg b/examples/data/Fruit360/lemon/r_130_100.jpg new file mode 100644 index 00000000..310bd7c3 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_130_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_131_100.jpg b/examples/data/Fruit360/lemon/r_131_100.jpg new file mode 100644 index 00000000..8b27bc43 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_131_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_132_100.jpg b/examples/data/Fruit360/lemon/r_132_100.jpg new file mode 100644 index 00000000..57231455 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_132_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_133_100.jpg b/examples/data/Fruit360/lemon/r_133_100.jpg new file mode 100644 index 00000000..2e8c2bc5 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_133_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_134_100.jpg b/examples/data/Fruit360/lemon/r_134_100.jpg new file mode 100644 index 00000000..e0e8f238 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_134_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_135_100.jpg b/examples/data/Fruit360/lemon/r_135_100.jpg new file mode 100644 index 00000000..30659861 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_135_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_136_100.jpg b/examples/data/Fruit360/lemon/r_136_100.jpg new file mode 100644 index 00000000..b34e7f4f Binary files /dev/null and b/examples/data/Fruit360/lemon/r_136_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_137_100.jpg b/examples/data/Fruit360/lemon/r_137_100.jpg new file mode 100644 index 00000000..2ec46a48 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_137_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_138_100.jpg b/examples/data/Fruit360/lemon/r_138_100.jpg new file mode 100644 index 00000000..d194def6 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_138_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_139_100.jpg b/examples/data/Fruit360/lemon/r_139_100.jpg new file mode 100644 index 00000000..28023714 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_139_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_140_100.jpg b/examples/data/Fruit360/lemon/r_140_100.jpg new file mode 100644 index 00000000..1e8aee19 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_140_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_141_100.jpg b/examples/data/Fruit360/lemon/r_141_100.jpg new file mode 100644 index 00000000..69afb4bd Binary files /dev/null and b/examples/data/Fruit360/lemon/r_141_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_142_100.jpg b/examples/data/Fruit360/lemon/r_142_100.jpg new file mode 100644 index 00000000..cd2a7dc5 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_142_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_143_100.jpg b/examples/data/Fruit360/lemon/r_143_100.jpg new file mode 100644 index 00000000..8ac54098 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_143_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_144_100.jpg b/examples/data/Fruit360/lemon/r_144_100.jpg new file mode 100644 index 00000000..05678e4e Binary files /dev/null and b/examples/data/Fruit360/lemon/r_144_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_145_100.jpg b/examples/data/Fruit360/lemon/r_145_100.jpg new file mode 100644 index 00000000..e00dbd0d Binary files /dev/null and b/examples/data/Fruit360/lemon/r_145_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_146_100.jpg b/examples/data/Fruit360/lemon/r_146_100.jpg new file mode 100644 index 00000000..8b008bfb Binary files /dev/null and b/examples/data/Fruit360/lemon/r_146_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_147_100.jpg b/examples/data/Fruit360/lemon/r_147_100.jpg new file mode 100644 index 00000000..b2dd3a19 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_147_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_148_100.jpg b/examples/data/Fruit360/lemon/r_148_100.jpg new file mode 100644 index 00000000..cf6f80be Binary files /dev/null and b/examples/data/Fruit360/lemon/r_148_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_149_100.jpg b/examples/data/Fruit360/lemon/r_149_100.jpg new file mode 100644 index 00000000..dc4571cd Binary files /dev/null and b/examples/data/Fruit360/lemon/r_149_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_150_100.jpg b/examples/data/Fruit360/lemon/r_150_100.jpg new file mode 100644 index 00000000..9c9be3ca Binary files /dev/null and b/examples/data/Fruit360/lemon/r_150_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_151_100.jpg b/examples/data/Fruit360/lemon/r_151_100.jpg new file mode 100644 index 00000000..2ad3aafb Binary files /dev/null and b/examples/data/Fruit360/lemon/r_151_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_152_100.jpg b/examples/data/Fruit360/lemon/r_152_100.jpg new file mode 100644 index 00000000..7a9c9630 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_152_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_153_100.jpg b/examples/data/Fruit360/lemon/r_153_100.jpg new file mode 100644 index 00000000..e9ba0fcb Binary files /dev/null and b/examples/data/Fruit360/lemon/r_153_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_154_100.jpg b/examples/data/Fruit360/lemon/r_154_100.jpg new file mode 100644 index 00000000..c832b119 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_154_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_155_100.jpg b/examples/data/Fruit360/lemon/r_155_100.jpg new file mode 100644 index 00000000..dae6045e Binary files /dev/null and b/examples/data/Fruit360/lemon/r_155_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_156_100.jpg b/examples/data/Fruit360/lemon/r_156_100.jpg new file mode 100644 index 00000000..cdaf4519 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_156_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_157_100.jpg b/examples/data/Fruit360/lemon/r_157_100.jpg new file mode 100644 index 00000000..f7b25dac Binary files /dev/null and b/examples/data/Fruit360/lemon/r_157_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_158_100.jpg b/examples/data/Fruit360/lemon/r_158_100.jpg new file mode 100644 index 00000000..639ff5ca Binary files /dev/null and b/examples/data/Fruit360/lemon/r_158_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_159_100.jpg b/examples/data/Fruit360/lemon/r_159_100.jpg new file mode 100644 index 00000000..2e4830b9 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_159_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_160_100.jpg b/examples/data/Fruit360/lemon/r_160_100.jpg new file mode 100644 index 00000000..727fc394 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_160_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_161_100.jpg b/examples/data/Fruit360/lemon/r_161_100.jpg new file mode 100644 index 00000000..8ebb4cd7 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_161_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_162_100.jpg b/examples/data/Fruit360/lemon/r_162_100.jpg new file mode 100644 index 00000000..6be2f584 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_162_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_163_100.jpg b/examples/data/Fruit360/lemon/r_163_100.jpg new file mode 100644 index 00000000..3183c02e Binary files /dev/null and b/examples/data/Fruit360/lemon/r_163_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_164_100.jpg b/examples/data/Fruit360/lemon/r_164_100.jpg new file mode 100644 index 00000000..090cc5e6 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_164_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_165_100.jpg b/examples/data/Fruit360/lemon/r_165_100.jpg new file mode 100644 index 00000000..5d53c6db Binary files /dev/null and b/examples/data/Fruit360/lemon/r_165_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_166_100.jpg b/examples/data/Fruit360/lemon/r_166_100.jpg new file mode 100644 index 00000000..db4a8019 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_166_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_167_100.jpg b/examples/data/Fruit360/lemon/r_167_100.jpg new file mode 100644 index 00000000..e90e5c8b Binary files /dev/null and b/examples/data/Fruit360/lemon/r_167_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_168_100.jpg b/examples/data/Fruit360/lemon/r_168_100.jpg new file mode 100644 index 00000000..dd663cd7 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_168_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_169_100.jpg b/examples/data/Fruit360/lemon/r_169_100.jpg new file mode 100644 index 00000000..5473ef87 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_169_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_170_100.jpg b/examples/data/Fruit360/lemon/r_170_100.jpg new file mode 100644 index 00000000..635a691d Binary files /dev/null and b/examples/data/Fruit360/lemon/r_170_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_171_100.jpg b/examples/data/Fruit360/lemon/r_171_100.jpg new file mode 100644 index 00000000..c0a22fe9 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_171_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_172_100.jpg b/examples/data/Fruit360/lemon/r_172_100.jpg new file mode 100644 index 00000000..a94c9e47 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_172_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_173_100.jpg b/examples/data/Fruit360/lemon/r_173_100.jpg new file mode 100644 index 00000000..0c070782 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_173_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_174_100.jpg b/examples/data/Fruit360/lemon/r_174_100.jpg new file mode 100644 index 00000000..af0cbf01 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_174_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_175_100.jpg b/examples/data/Fruit360/lemon/r_175_100.jpg new file mode 100644 index 00000000..6f9c2b55 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_175_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_176_100.jpg b/examples/data/Fruit360/lemon/r_176_100.jpg new file mode 100644 index 00000000..af575e26 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_176_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_177_100.jpg b/examples/data/Fruit360/lemon/r_177_100.jpg new file mode 100644 index 00000000..e9906d88 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_177_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_179_100.jpg b/examples/data/Fruit360/lemon/r_179_100.jpg new file mode 100644 index 00000000..a668b0a4 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_179_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_200_100.jpg b/examples/data/Fruit360/lemon/r_200_100.jpg new file mode 100644 index 00000000..5d70ebe8 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_200_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_201_100.jpg b/examples/data/Fruit360/lemon/r_201_100.jpg new file mode 100644 index 00000000..b4a232ad Binary files /dev/null and b/examples/data/Fruit360/lemon/r_201_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_202_100.jpg b/examples/data/Fruit360/lemon/r_202_100.jpg new file mode 100644 index 00000000..5c360baf Binary files /dev/null and b/examples/data/Fruit360/lemon/r_202_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_203_100.jpg b/examples/data/Fruit360/lemon/r_203_100.jpg new file mode 100644 index 00000000..3daa7330 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_203_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_204_100.jpg b/examples/data/Fruit360/lemon/r_204_100.jpg new file mode 100644 index 00000000..0d370490 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_204_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_205_100.jpg b/examples/data/Fruit360/lemon/r_205_100.jpg new file mode 100644 index 00000000..0ca2f6e2 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_205_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_206_100.jpg b/examples/data/Fruit360/lemon/r_206_100.jpg new file mode 100644 index 00000000..2217532f Binary files /dev/null and b/examples/data/Fruit360/lemon/r_206_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_207_100.jpg b/examples/data/Fruit360/lemon/r_207_100.jpg new file mode 100644 index 00000000..f31cbe67 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_207_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_208_100.jpg b/examples/data/Fruit360/lemon/r_208_100.jpg new file mode 100644 index 00000000..2bc64908 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_208_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_209_100.jpg b/examples/data/Fruit360/lemon/r_209_100.jpg new file mode 100644 index 00000000..240feedc Binary files /dev/null and b/examples/data/Fruit360/lemon/r_209_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_210_100.jpg b/examples/data/Fruit360/lemon/r_210_100.jpg new file mode 100644 index 00000000..3fabed71 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_210_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_211_100.jpg b/examples/data/Fruit360/lemon/r_211_100.jpg new file mode 100644 index 00000000..b3ca2d1c Binary files /dev/null and b/examples/data/Fruit360/lemon/r_211_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_212_100.jpg b/examples/data/Fruit360/lemon/r_212_100.jpg new file mode 100644 index 00000000..78ce2d75 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_212_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_213_100.jpg b/examples/data/Fruit360/lemon/r_213_100.jpg new file mode 100644 index 00000000..983c40bc Binary files /dev/null and b/examples/data/Fruit360/lemon/r_213_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_214_100.jpg b/examples/data/Fruit360/lemon/r_214_100.jpg new file mode 100644 index 00000000..dd26f264 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_214_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_215_100.jpg b/examples/data/Fruit360/lemon/r_215_100.jpg new file mode 100644 index 00000000..84618288 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_215_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_216_100.jpg b/examples/data/Fruit360/lemon/r_216_100.jpg new file mode 100644 index 00000000..3dc80f8b Binary files /dev/null and b/examples/data/Fruit360/lemon/r_216_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_217_100.jpg b/examples/data/Fruit360/lemon/r_217_100.jpg new file mode 100644 index 00000000..79ffac76 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_217_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_218_100.jpg b/examples/data/Fruit360/lemon/r_218_100.jpg new file mode 100644 index 00000000..929a6735 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_218_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_219_100.jpg b/examples/data/Fruit360/lemon/r_219_100.jpg new file mode 100644 index 00000000..54264aa5 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_219_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_220_100.jpg b/examples/data/Fruit360/lemon/r_220_100.jpg new file mode 100644 index 00000000..0e0ea42f Binary files /dev/null and b/examples/data/Fruit360/lemon/r_220_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_221_100.jpg b/examples/data/Fruit360/lemon/r_221_100.jpg new file mode 100644 index 00000000..614eb836 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_221_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_222_100.jpg b/examples/data/Fruit360/lemon/r_222_100.jpg new file mode 100644 index 00000000..cf6e5fac Binary files /dev/null and b/examples/data/Fruit360/lemon/r_222_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_223_100.jpg b/examples/data/Fruit360/lemon/r_223_100.jpg new file mode 100644 index 00000000..b89262d0 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_223_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_224_100.jpg b/examples/data/Fruit360/lemon/r_224_100.jpg new file mode 100644 index 00000000..d387980a Binary files /dev/null and b/examples/data/Fruit360/lemon/r_224_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_225_100.jpg b/examples/data/Fruit360/lemon/r_225_100.jpg new file mode 100644 index 00000000..a86e0e41 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_225_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_226_100.jpg b/examples/data/Fruit360/lemon/r_226_100.jpg new file mode 100644 index 00000000..fe7d0d3f Binary files /dev/null and b/examples/data/Fruit360/lemon/r_226_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_227_100.jpg b/examples/data/Fruit360/lemon/r_227_100.jpg new file mode 100644 index 00000000..0a673d31 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_227_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_228_100.jpg b/examples/data/Fruit360/lemon/r_228_100.jpg new file mode 100644 index 00000000..20dd9cd9 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_228_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_229_100.jpg b/examples/data/Fruit360/lemon/r_229_100.jpg new file mode 100644 index 00000000..4d7e1f2d Binary files /dev/null and b/examples/data/Fruit360/lemon/r_229_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_230_100.jpg b/examples/data/Fruit360/lemon/r_230_100.jpg new file mode 100644 index 00000000..68451634 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_230_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_231_100.jpg b/examples/data/Fruit360/lemon/r_231_100.jpg new file mode 100644 index 00000000..7a8e2b3f Binary files /dev/null and b/examples/data/Fruit360/lemon/r_231_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_232_100.jpg b/examples/data/Fruit360/lemon/r_232_100.jpg new file mode 100644 index 00000000..f804cbb7 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_232_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_233_100.jpg b/examples/data/Fruit360/lemon/r_233_100.jpg new file mode 100644 index 00000000..1dd8ba13 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_233_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_234_100.jpg b/examples/data/Fruit360/lemon/r_234_100.jpg new file mode 100644 index 00000000..9d34d7f4 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_234_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_235_100.jpg b/examples/data/Fruit360/lemon/r_235_100.jpg new file mode 100644 index 00000000..98c6683e Binary files /dev/null and b/examples/data/Fruit360/lemon/r_235_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_236_100.jpg b/examples/data/Fruit360/lemon/r_236_100.jpg new file mode 100644 index 00000000..52a9e37c Binary files /dev/null and b/examples/data/Fruit360/lemon/r_236_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_237_100.jpg b/examples/data/Fruit360/lemon/r_237_100.jpg new file mode 100644 index 00000000..44a5f69b Binary files /dev/null and b/examples/data/Fruit360/lemon/r_237_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_238_100.jpg b/examples/data/Fruit360/lemon/r_238_100.jpg new file mode 100644 index 00000000..a47abdc6 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_238_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_239_100.jpg b/examples/data/Fruit360/lemon/r_239_100.jpg new file mode 100644 index 00000000..eab56731 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_239_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_240_100.jpg b/examples/data/Fruit360/lemon/r_240_100.jpg new file mode 100644 index 00000000..709df9eb Binary files /dev/null and b/examples/data/Fruit360/lemon/r_240_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_241_100.jpg b/examples/data/Fruit360/lemon/r_241_100.jpg new file mode 100644 index 00000000..305a64c0 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_241_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_242_100.jpg b/examples/data/Fruit360/lemon/r_242_100.jpg new file mode 100644 index 00000000..6d276826 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_242_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_243_100.jpg b/examples/data/Fruit360/lemon/r_243_100.jpg new file mode 100644 index 00000000..f826cfd5 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_243_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_244_100.jpg b/examples/data/Fruit360/lemon/r_244_100.jpg new file mode 100644 index 00000000..cf398eff Binary files /dev/null and b/examples/data/Fruit360/lemon/r_244_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_245_100.jpg b/examples/data/Fruit360/lemon/r_245_100.jpg new file mode 100644 index 00000000..a8735d61 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_245_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_246_100.jpg b/examples/data/Fruit360/lemon/r_246_100.jpg new file mode 100644 index 00000000..e2765a72 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_246_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_247_100.jpg b/examples/data/Fruit360/lemon/r_247_100.jpg new file mode 100644 index 00000000..888dd7ae Binary files /dev/null and b/examples/data/Fruit360/lemon/r_247_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_248_100.jpg b/examples/data/Fruit360/lemon/r_248_100.jpg new file mode 100644 index 00000000..0514294d Binary files /dev/null and b/examples/data/Fruit360/lemon/r_248_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_249_100.jpg b/examples/data/Fruit360/lemon/r_249_100.jpg new file mode 100644 index 00000000..13603f2a Binary files /dev/null and b/examples/data/Fruit360/lemon/r_249_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_250_100.jpg b/examples/data/Fruit360/lemon/r_250_100.jpg new file mode 100644 index 00000000..02f7e9d6 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_250_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_251_100.jpg b/examples/data/Fruit360/lemon/r_251_100.jpg new file mode 100644 index 00000000..2c2ad26d Binary files /dev/null and b/examples/data/Fruit360/lemon/r_251_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_252_100.jpg b/examples/data/Fruit360/lemon/r_252_100.jpg new file mode 100644 index 00000000..9259dbde Binary files /dev/null and b/examples/data/Fruit360/lemon/r_252_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_253_100.jpg b/examples/data/Fruit360/lemon/r_253_100.jpg new file mode 100644 index 00000000..358d8c9a Binary files /dev/null and b/examples/data/Fruit360/lemon/r_253_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_254_100.jpg b/examples/data/Fruit360/lemon/r_254_100.jpg new file mode 100644 index 00000000..03ff6f26 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_254_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_255_100.jpg b/examples/data/Fruit360/lemon/r_255_100.jpg new file mode 100644 index 00000000..bbf22c86 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_255_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_256_100.jpg b/examples/data/Fruit360/lemon/r_256_100.jpg new file mode 100644 index 00000000..0247a06b Binary files /dev/null and b/examples/data/Fruit360/lemon/r_256_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_257_100.jpg b/examples/data/Fruit360/lemon/r_257_100.jpg new file mode 100644 index 00000000..82c219ec Binary files /dev/null and b/examples/data/Fruit360/lemon/r_257_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_258_100.jpg b/examples/data/Fruit360/lemon/r_258_100.jpg new file mode 100644 index 00000000..afcee446 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_258_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_259_100.jpg b/examples/data/Fruit360/lemon/r_259_100.jpg new file mode 100644 index 00000000..f1dd89dc Binary files /dev/null and b/examples/data/Fruit360/lemon/r_259_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_260_100.jpg b/examples/data/Fruit360/lemon/r_260_100.jpg new file mode 100644 index 00000000..e9b42887 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_260_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_261_100.jpg b/examples/data/Fruit360/lemon/r_261_100.jpg new file mode 100644 index 00000000..9b29c81f Binary files /dev/null and b/examples/data/Fruit360/lemon/r_261_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_262_100.jpg b/examples/data/Fruit360/lemon/r_262_100.jpg new file mode 100644 index 00000000..3ded584c Binary files /dev/null and b/examples/data/Fruit360/lemon/r_262_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_263_100.jpg b/examples/data/Fruit360/lemon/r_263_100.jpg new file mode 100644 index 00000000..2b7ceb14 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_263_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_264_100.jpg b/examples/data/Fruit360/lemon/r_264_100.jpg new file mode 100644 index 00000000..d28af1e2 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_264_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_265_100.jpg b/examples/data/Fruit360/lemon/r_265_100.jpg new file mode 100644 index 00000000..c21fb392 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_265_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_266_100.jpg b/examples/data/Fruit360/lemon/r_266_100.jpg new file mode 100644 index 00000000..aa7144c0 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_266_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_267_100.jpg b/examples/data/Fruit360/lemon/r_267_100.jpg new file mode 100644 index 00000000..b87cc8f5 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_267_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_268_100.jpg b/examples/data/Fruit360/lemon/r_268_100.jpg new file mode 100644 index 00000000..036868e9 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_268_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_269_100.jpg b/examples/data/Fruit360/lemon/r_269_100.jpg new file mode 100644 index 00000000..89a37b6d Binary files /dev/null and b/examples/data/Fruit360/lemon/r_269_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_26_100.jpg b/examples/data/Fruit360/lemon/r_26_100.jpg new file mode 100644 index 00000000..4d965355 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_26_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_270_100.jpg b/examples/data/Fruit360/lemon/r_270_100.jpg new file mode 100644 index 00000000..66be862e Binary files /dev/null and b/examples/data/Fruit360/lemon/r_270_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_271_100.jpg b/examples/data/Fruit360/lemon/r_271_100.jpg new file mode 100644 index 00000000..87ac6dd1 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_271_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_272_100.jpg b/examples/data/Fruit360/lemon/r_272_100.jpg new file mode 100644 index 00000000..cd6e5b34 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_272_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_273_100.jpg b/examples/data/Fruit360/lemon/r_273_100.jpg new file mode 100644 index 00000000..4783426d Binary files /dev/null and b/examples/data/Fruit360/lemon/r_273_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_274_100.jpg b/examples/data/Fruit360/lemon/r_274_100.jpg new file mode 100644 index 00000000..77cd786b Binary files /dev/null and b/examples/data/Fruit360/lemon/r_274_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_275_100.jpg b/examples/data/Fruit360/lemon/r_275_100.jpg new file mode 100644 index 00000000..5124f11f Binary files /dev/null and b/examples/data/Fruit360/lemon/r_275_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_276_100.jpg b/examples/data/Fruit360/lemon/r_276_100.jpg new file mode 100644 index 00000000..17af9a4a Binary files /dev/null and b/examples/data/Fruit360/lemon/r_276_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_277_100.jpg b/examples/data/Fruit360/lemon/r_277_100.jpg new file mode 100644 index 00000000..438126c1 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_277_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_278_100.jpg b/examples/data/Fruit360/lemon/r_278_100.jpg new file mode 100644 index 00000000..7f186741 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_278_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_279_100.jpg b/examples/data/Fruit360/lemon/r_279_100.jpg new file mode 100644 index 00000000..2de71150 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_279_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_27_100.jpg b/examples/data/Fruit360/lemon/r_27_100.jpg new file mode 100644 index 00000000..e26b0df0 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_27_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_280_100.jpg b/examples/data/Fruit360/lemon/r_280_100.jpg new file mode 100644 index 00000000..60788319 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_280_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_281_100.jpg b/examples/data/Fruit360/lemon/r_281_100.jpg new file mode 100644 index 00000000..616d14f5 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_281_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_282_100.jpg b/examples/data/Fruit360/lemon/r_282_100.jpg new file mode 100644 index 00000000..1b0dfc6c Binary files /dev/null and b/examples/data/Fruit360/lemon/r_282_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_283_100.jpg b/examples/data/Fruit360/lemon/r_283_100.jpg new file mode 100644 index 00000000..856c8a1d Binary files /dev/null and b/examples/data/Fruit360/lemon/r_283_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_284_100.jpg b/examples/data/Fruit360/lemon/r_284_100.jpg new file mode 100644 index 00000000..2c02ab3c Binary files /dev/null and b/examples/data/Fruit360/lemon/r_284_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_285_100.jpg b/examples/data/Fruit360/lemon/r_285_100.jpg new file mode 100644 index 00000000..fceab225 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_285_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_286_100.jpg b/examples/data/Fruit360/lemon/r_286_100.jpg new file mode 100644 index 00000000..961e4b08 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_286_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_287_100.jpg b/examples/data/Fruit360/lemon/r_287_100.jpg new file mode 100644 index 00000000..02983e6d Binary files /dev/null and b/examples/data/Fruit360/lemon/r_287_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_288_100.jpg b/examples/data/Fruit360/lemon/r_288_100.jpg new file mode 100644 index 00000000..df653e95 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_288_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_289_100.jpg b/examples/data/Fruit360/lemon/r_289_100.jpg new file mode 100644 index 00000000..2e75c690 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_289_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_290_100.jpg b/examples/data/Fruit360/lemon/r_290_100.jpg new file mode 100644 index 00000000..373abf3f Binary files /dev/null and b/examples/data/Fruit360/lemon/r_290_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_291_100.jpg b/examples/data/Fruit360/lemon/r_291_100.jpg new file mode 100644 index 00000000..4afb466a Binary files /dev/null and b/examples/data/Fruit360/lemon/r_291_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_292_100.jpg b/examples/data/Fruit360/lemon/r_292_100.jpg new file mode 100644 index 00000000..699279e3 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_292_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_293_100.jpg b/examples/data/Fruit360/lemon/r_293_100.jpg new file mode 100644 index 00000000..87109966 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_293_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_294_100.jpg b/examples/data/Fruit360/lemon/r_294_100.jpg new file mode 100644 index 00000000..1e663dc5 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_294_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_295_100.jpg b/examples/data/Fruit360/lemon/r_295_100.jpg new file mode 100644 index 00000000..9d2ccfa3 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_295_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_296_100.jpg b/examples/data/Fruit360/lemon/r_296_100.jpg new file mode 100644 index 00000000..7205f438 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_296_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_297_100.jpg b/examples/data/Fruit360/lemon/r_297_100.jpg new file mode 100644 index 00000000..0f25739c Binary files /dev/null and b/examples/data/Fruit360/lemon/r_297_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_298_100.jpg b/examples/data/Fruit360/lemon/r_298_100.jpg new file mode 100644 index 00000000..5ecccbea Binary files /dev/null and b/examples/data/Fruit360/lemon/r_298_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_299_100.jpg b/examples/data/Fruit360/lemon/r_299_100.jpg new file mode 100644 index 00000000..89f180f4 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_299_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_300_100.jpg b/examples/data/Fruit360/lemon/r_300_100.jpg new file mode 100644 index 00000000..a09e47f9 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_300_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_311_100.jpg b/examples/data/Fruit360/lemon/r_311_100.jpg new file mode 100644 index 00000000..df50c518 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_311_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_314_100.jpg b/examples/data/Fruit360/lemon/r_314_100.jpg new file mode 100644 index 00000000..33076219 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_314_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_321_100.jpg b/examples/data/Fruit360/lemon/r_321_100.jpg new file mode 100644 index 00000000..37627a76 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_321_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_32_100.jpg b/examples/data/Fruit360/lemon/r_32_100.jpg new file mode 100644 index 00000000..58f39eb8 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_32_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_33_100.jpg b/examples/data/Fruit360/lemon/r_33_100.jpg new file mode 100644 index 00000000..2f1ba82b Binary files /dev/null and b/examples/data/Fruit360/lemon/r_33_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_34_100.jpg b/examples/data/Fruit360/lemon/r_34_100.jpg new file mode 100644 index 00000000..616bb243 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_34_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_35_100.jpg b/examples/data/Fruit360/lemon/r_35_100.jpg new file mode 100644 index 00000000..57deb32c Binary files /dev/null and b/examples/data/Fruit360/lemon/r_35_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_36_100.jpg b/examples/data/Fruit360/lemon/r_36_100.jpg new file mode 100644 index 00000000..9a422f10 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_36_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_37_100.jpg b/examples/data/Fruit360/lemon/r_37_100.jpg new file mode 100644 index 00000000..61ec449d Binary files /dev/null and b/examples/data/Fruit360/lemon/r_37_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_38_100.jpg b/examples/data/Fruit360/lemon/r_38_100.jpg new file mode 100644 index 00000000..6806c0b1 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_38_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_39_100.jpg b/examples/data/Fruit360/lemon/r_39_100.jpg new file mode 100644 index 00000000..41f07d5f Binary files /dev/null and b/examples/data/Fruit360/lemon/r_39_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_40_100.jpg b/examples/data/Fruit360/lemon/r_40_100.jpg new file mode 100644 index 00000000..4763ae98 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_40_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_41_100.jpg b/examples/data/Fruit360/lemon/r_41_100.jpg new file mode 100644 index 00000000..9948d0af Binary files /dev/null and b/examples/data/Fruit360/lemon/r_41_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_42_100.jpg b/examples/data/Fruit360/lemon/r_42_100.jpg new file mode 100644 index 00000000..0eb78dc4 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_42_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_43_100.jpg b/examples/data/Fruit360/lemon/r_43_100.jpg new file mode 100644 index 00000000..f8e92218 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_43_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_44_100.jpg b/examples/data/Fruit360/lemon/r_44_100.jpg new file mode 100644 index 00000000..020547dc Binary files /dev/null and b/examples/data/Fruit360/lemon/r_44_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_45_100.jpg b/examples/data/Fruit360/lemon/r_45_100.jpg new file mode 100644 index 00000000..0f64f8d7 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_45_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_46_100.jpg b/examples/data/Fruit360/lemon/r_46_100.jpg new file mode 100644 index 00000000..6e983c13 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_46_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_47_100.jpg b/examples/data/Fruit360/lemon/r_47_100.jpg new file mode 100644 index 00000000..628fd006 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_47_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_48_100.jpg b/examples/data/Fruit360/lemon/r_48_100.jpg new file mode 100644 index 00000000..44d4535e Binary files /dev/null and b/examples/data/Fruit360/lemon/r_48_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_49_100.jpg b/examples/data/Fruit360/lemon/r_49_100.jpg new file mode 100644 index 00000000..a8360793 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_49_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_51_100.jpg b/examples/data/Fruit360/lemon/r_51_100.jpg new file mode 100644 index 00000000..3d0697de Binary files /dev/null and b/examples/data/Fruit360/lemon/r_51_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_52_100.jpg b/examples/data/Fruit360/lemon/r_52_100.jpg new file mode 100644 index 00000000..a0b4c449 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_52_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_53_100.jpg b/examples/data/Fruit360/lemon/r_53_100.jpg new file mode 100644 index 00000000..a3c926bd Binary files /dev/null and b/examples/data/Fruit360/lemon/r_53_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_58_100.jpg b/examples/data/Fruit360/lemon/r_58_100.jpg new file mode 100644 index 00000000..150a608e Binary files /dev/null and b/examples/data/Fruit360/lemon/r_58_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_61_100.jpg b/examples/data/Fruit360/lemon/r_61_100.jpg new file mode 100644 index 00000000..49e16f92 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_61_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_62_100.jpg b/examples/data/Fruit360/lemon/r_62_100.jpg new file mode 100644 index 00000000..32d7c331 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_62_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_64_100.jpg b/examples/data/Fruit360/lemon/r_64_100.jpg new file mode 100644 index 00000000..f891d3d1 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_64_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_65_100.jpg b/examples/data/Fruit360/lemon/r_65_100.jpg new file mode 100644 index 00000000..7559bf80 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_65_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_66_100.jpg b/examples/data/Fruit360/lemon/r_66_100.jpg new file mode 100644 index 00000000..aafd37d6 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_66_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_67_100.jpg b/examples/data/Fruit360/lemon/r_67_100.jpg new file mode 100644 index 00000000..027e64aa Binary files /dev/null and b/examples/data/Fruit360/lemon/r_67_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_68_100.jpg b/examples/data/Fruit360/lemon/r_68_100.jpg new file mode 100644 index 00000000..799347ae Binary files /dev/null and b/examples/data/Fruit360/lemon/r_68_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_69_100.jpg b/examples/data/Fruit360/lemon/r_69_100.jpg new file mode 100644 index 00000000..b9fd5ac5 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_69_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_70_100.jpg b/examples/data/Fruit360/lemon/r_70_100.jpg new file mode 100644 index 00000000..99efd038 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_70_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_71_100.jpg b/examples/data/Fruit360/lemon/r_71_100.jpg new file mode 100644 index 00000000..95d35ced Binary files /dev/null and b/examples/data/Fruit360/lemon/r_71_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_72_100.jpg b/examples/data/Fruit360/lemon/r_72_100.jpg new file mode 100644 index 00000000..d066b697 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_72_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_73_100.jpg b/examples/data/Fruit360/lemon/r_73_100.jpg new file mode 100644 index 00000000..0ce89207 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_73_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_74_100.jpg b/examples/data/Fruit360/lemon/r_74_100.jpg new file mode 100644 index 00000000..b2dc99aa Binary files /dev/null and b/examples/data/Fruit360/lemon/r_74_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_75_100.jpg b/examples/data/Fruit360/lemon/r_75_100.jpg new file mode 100644 index 00000000..da05a89b Binary files /dev/null and b/examples/data/Fruit360/lemon/r_75_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_76_100.jpg b/examples/data/Fruit360/lemon/r_76_100.jpg new file mode 100644 index 00000000..d9c8a9b5 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_76_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_77_100.jpg b/examples/data/Fruit360/lemon/r_77_100.jpg new file mode 100644 index 00000000..57149769 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_77_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_78_100.jpg b/examples/data/Fruit360/lemon/r_78_100.jpg new file mode 100644 index 00000000..c0d5b717 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_78_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_79_100.jpg b/examples/data/Fruit360/lemon/r_79_100.jpg new file mode 100644 index 00000000..2b83e68c Binary files /dev/null and b/examples/data/Fruit360/lemon/r_79_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_80_100.jpg b/examples/data/Fruit360/lemon/r_80_100.jpg new file mode 100644 index 00000000..a85db312 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_80_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_81_100.jpg b/examples/data/Fruit360/lemon/r_81_100.jpg new file mode 100644 index 00000000..413e659c Binary files /dev/null and b/examples/data/Fruit360/lemon/r_81_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_82_100.jpg b/examples/data/Fruit360/lemon/r_82_100.jpg new file mode 100644 index 00000000..9751d804 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_82_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_83_100.jpg b/examples/data/Fruit360/lemon/r_83_100.jpg new file mode 100644 index 00000000..160156e8 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_83_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_84_100.jpg b/examples/data/Fruit360/lemon/r_84_100.jpg new file mode 100644 index 00000000..ff2aeb02 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_84_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_85_100.jpg b/examples/data/Fruit360/lemon/r_85_100.jpg new file mode 100644 index 00000000..b4c42ee8 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_85_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_86_100.jpg b/examples/data/Fruit360/lemon/r_86_100.jpg new file mode 100644 index 00000000..0998f719 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_86_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_87_100.jpg b/examples/data/Fruit360/lemon/r_87_100.jpg new file mode 100644 index 00000000..4cb4c70e Binary files /dev/null and b/examples/data/Fruit360/lemon/r_87_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_88_100.jpg b/examples/data/Fruit360/lemon/r_88_100.jpg new file mode 100644 index 00000000..fb9ce3f7 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_88_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_89_100.jpg b/examples/data/Fruit360/lemon/r_89_100.jpg new file mode 100644 index 00000000..bc243f95 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_89_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_90_100.jpg b/examples/data/Fruit360/lemon/r_90_100.jpg new file mode 100644 index 00000000..ce4b302f Binary files /dev/null and b/examples/data/Fruit360/lemon/r_90_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_91_100.jpg b/examples/data/Fruit360/lemon/r_91_100.jpg new file mode 100644 index 00000000..0a621220 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_91_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_92_100.jpg b/examples/data/Fruit360/lemon/r_92_100.jpg new file mode 100644 index 00000000..230c40a1 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_92_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_93_100.jpg b/examples/data/Fruit360/lemon/r_93_100.jpg new file mode 100644 index 00000000..95824a81 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_93_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_94_100.jpg b/examples/data/Fruit360/lemon/r_94_100.jpg new file mode 100644 index 00000000..841b5e4d Binary files /dev/null and b/examples/data/Fruit360/lemon/r_94_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_95_100.jpg b/examples/data/Fruit360/lemon/r_95_100.jpg new file mode 100644 index 00000000..385690cc Binary files /dev/null and b/examples/data/Fruit360/lemon/r_95_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_96_100.jpg b/examples/data/Fruit360/lemon/r_96_100.jpg new file mode 100644 index 00000000..d8b312e6 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_96_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_97_100.jpg b/examples/data/Fruit360/lemon/r_97_100.jpg new file mode 100644 index 00000000..7860e4eb Binary files /dev/null and b/examples/data/Fruit360/lemon/r_97_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_98_100.jpg b/examples/data/Fruit360/lemon/r_98_100.jpg new file mode 100644 index 00000000..1a3242be Binary files /dev/null and b/examples/data/Fruit360/lemon/r_98_100.jpg differ diff --git a/examples/data/Fruit360/lemon/r_99_100.jpg b/examples/data/Fruit360/lemon/r_99_100.jpg new file mode 100644 index 00000000..ac0d1f93 Binary files /dev/null and b/examples/data/Fruit360/lemon/r_99_100.jpg differ diff --git a/examples/data/Fruit360/mango/100_100.jpg b/examples/data/Fruit360/mango/100_100.jpg new file mode 100644 index 00000000..f70461ff Binary files /dev/null and b/examples/data/Fruit360/mango/100_100.jpg differ diff --git a/examples/data/Fruit360/mango/101_100.jpg b/examples/data/Fruit360/mango/101_100.jpg new file mode 100644 index 00000000..e04f7b88 Binary files /dev/null and b/examples/data/Fruit360/mango/101_100.jpg differ diff --git a/examples/data/Fruit360/mango/102_100.jpg b/examples/data/Fruit360/mango/102_100.jpg new file mode 100644 index 00000000..3d00c803 Binary files /dev/null and b/examples/data/Fruit360/mango/102_100.jpg differ diff --git a/examples/data/Fruit360/mango/103_100.jpg b/examples/data/Fruit360/mango/103_100.jpg new file mode 100644 index 00000000..413fcb0b Binary files /dev/null and b/examples/data/Fruit360/mango/103_100.jpg differ diff --git a/examples/data/Fruit360/mango/104_100.jpg b/examples/data/Fruit360/mango/104_100.jpg new file mode 100644 index 00000000..d6ac22f5 Binary files /dev/null and b/examples/data/Fruit360/mango/104_100.jpg differ diff --git a/examples/data/Fruit360/mango/105_100.jpg b/examples/data/Fruit360/mango/105_100.jpg new file mode 100644 index 00000000..4b1dd106 Binary files /dev/null and b/examples/data/Fruit360/mango/105_100.jpg differ diff --git a/examples/data/Fruit360/mango/106_100.jpg b/examples/data/Fruit360/mango/106_100.jpg new file mode 100644 index 00000000..b7294f6a Binary files /dev/null and b/examples/data/Fruit360/mango/106_100.jpg differ diff --git a/examples/data/Fruit360/mango/107_100.jpg b/examples/data/Fruit360/mango/107_100.jpg new file mode 100644 index 00000000..49a8c3d2 Binary files /dev/null and b/examples/data/Fruit360/mango/107_100.jpg differ diff --git a/examples/data/Fruit360/mango/108_100.jpg b/examples/data/Fruit360/mango/108_100.jpg new file mode 100644 index 00000000..0e3ae52f Binary files /dev/null and b/examples/data/Fruit360/mango/108_100.jpg differ diff --git a/examples/data/Fruit360/mango/109_100.jpg b/examples/data/Fruit360/mango/109_100.jpg new file mode 100644 index 00000000..652a5f4b Binary files /dev/null and b/examples/data/Fruit360/mango/109_100.jpg differ diff --git a/examples/data/Fruit360/mango/110_100.jpg b/examples/data/Fruit360/mango/110_100.jpg new file mode 100644 index 00000000..ed65565d Binary files /dev/null and b/examples/data/Fruit360/mango/110_100.jpg differ diff --git a/examples/data/Fruit360/mango/111_100.jpg b/examples/data/Fruit360/mango/111_100.jpg new file mode 100644 index 00000000..0c2198e9 Binary files /dev/null and b/examples/data/Fruit360/mango/111_100.jpg differ diff --git a/examples/data/Fruit360/mango/112_100.jpg b/examples/data/Fruit360/mango/112_100.jpg new file mode 100644 index 00000000..9847d2b8 Binary files /dev/null and b/examples/data/Fruit360/mango/112_100.jpg differ diff --git a/examples/data/Fruit360/mango/113_100.jpg b/examples/data/Fruit360/mango/113_100.jpg new file mode 100644 index 00000000..e34355d5 Binary files /dev/null and b/examples/data/Fruit360/mango/113_100.jpg differ diff --git a/examples/data/Fruit360/mango/114_100.jpg b/examples/data/Fruit360/mango/114_100.jpg new file mode 100644 index 00000000..b1985d6a Binary files /dev/null and b/examples/data/Fruit360/mango/114_100.jpg differ diff --git a/examples/data/Fruit360/mango/115_100.jpg b/examples/data/Fruit360/mango/115_100.jpg new file mode 100644 index 00000000..60e1f912 Binary files /dev/null and b/examples/data/Fruit360/mango/115_100.jpg differ diff --git a/examples/data/Fruit360/mango/116_100.jpg b/examples/data/Fruit360/mango/116_100.jpg new file mode 100644 index 00000000..f6861d25 Binary files /dev/null and b/examples/data/Fruit360/mango/116_100.jpg differ diff --git a/examples/data/Fruit360/mango/117_100.jpg b/examples/data/Fruit360/mango/117_100.jpg new file mode 100644 index 00000000..e803e941 Binary files /dev/null and b/examples/data/Fruit360/mango/117_100.jpg differ diff --git a/examples/data/Fruit360/mango/118_100.jpg b/examples/data/Fruit360/mango/118_100.jpg new file mode 100644 index 00000000..499f6a98 Binary files /dev/null and b/examples/data/Fruit360/mango/118_100.jpg differ diff --git a/examples/data/Fruit360/mango/119_100.jpg b/examples/data/Fruit360/mango/119_100.jpg new file mode 100644 index 00000000..4ebc78be Binary files /dev/null and b/examples/data/Fruit360/mango/119_100.jpg differ diff --git a/examples/data/Fruit360/mango/120_100.jpg b/examples/data/Fruit360/mango/120_100.jpg new file mode 100644 index 00000000..3fe446b7 Binary files /dev/null and b/examples/data/Fruit360/mango/120_100.jpg differ diff --git a/examples/data/Fruit360/mango/121_100.jpg b/examples/data/Fruit360/mango/121_100.jpg new file mode 100644 index 00000000..cdef65c9 Binary files /dev/null and b/examples/data/Fruit360/mango/121_100.jpg differ diff --git a/examples/data/Fruit360/mango/122_100.jpg b/examples/data/Fruit360/mango/122_100.jpg new file mode 100644 index 00000000..6d2e93e6 Binary files /dev/null and b/examples/data/Fruit360/mango/122_100.jpg differ diff --git a/examples/data/Fruit360/mango/123_100.jpg b/examples/data/Fruit360/mango/123_100.jpg new file mode 100644 index 00000000..1ca16b97 Binary files /dev/null and b/examples/data/Fruit360/mango/123_100.jpg differ diff --git a/examples/data/Fruit360/mango/124_100.jpg b/examples/data/Fruit360/mango/124_100.jpg new file mode 100644 index 00000000..c2a2fd55 Binary files /dev/null and b/examples/data/Fruit360/mango/124_100.jpg differ diff --git a/examples/data/Fruit360/mango/126_100.jpg b/examples/data/Fruit360/mango/126_100.jpg new file mode 100644 index 00000000..a4211c51 Binary files /dev/null and b/examples/data/Fruit360/mango/126_100.jpg differ diff --git a/examples/data/Fruit360/mango/127_100.jpg b/examples/data/Fruit360/mango/127_100.jpg new file mode 100644 index 00000000..102f415d Binary files /dev/null and b/examples/data/Fruit360/mango/127_100.jpg differ diff --git a/examples/data/Fruit360/mango/128_100.jpg b/examples/data/Fruit360/mango/128_100.jpg new file mode 100644 index 00000000..e300c5db Binary files /dev/null and b/examples/data/Fruit360/mango/128_100.jpg differ diff --git a/examples/data/Fruit360/mango/129_100.jpg b/examples/data/Fruit360/mango/129_100.jpg new file mode 100644 index 00000000..8c90e8e9 Binary files /dev/null and b/examples/data/Fruit360/mango/129_100.jpg differ diff --git a/examples/data/Fruit360/mango/13_100.jpg b/examples/data/Fruit360/mango/13_100.jpg new file mode 100644 index 00000000..7a24cb51 Binary files /dev/null and b/examples/data/Fruit360/mango/13_100.jpg differ diff --git a/examples/data/Fruit360/mango/154_100.jpg b/examples/data/Fruit360/mango/154_100.jpg new file mode 100644 index 00000000..9dcf547b Binary files /dev/null and b/examples/data/Fruit360/mango/154_100.jpg differ diff --git a/examples/data/Fruit360/mango/15_100.jpg b/examples/data/Fruit360/mango/15_100.jpg new file mode 100644 index 00000000..fdf5d2f1 Binary files /dev/null and b/examples/data/Fruit360/mango/15_100.jpg differ diff --git a/examples/data/Fruit360/mango/160_100.jpg b/examples/data/Fruit360/mango/160_100.jpg new file mode 100644 index 00000000..d8fd8624 Binary files /dev/null and b/examples/data/Fruit360/mango/160_100.jpg differ diff --git a/examples/data/Fruit360/mango/161_100.jpg b/examples/data/Fruit360/mango/161_100.jpg new file mode 100644 index 00000000..fcf7d506 Binary files /dev/null and b/examples/data/Fruit360/mango/161_100.jpg differ diff --git a/examples/data/Fruit360/mango/163_100.jpg b/examples/data/Fruit360/mango/163_100.jpg new file mode 100644 index 00000000..faea8729 Binary files /dev/null and b/examples/data/Fruit360/mango/163_100.jpg differ diff --git a/examples/data/Fruit360/mango/164_100.jpg b/examples/data/Fruit360/mango/164_100.jpg new file mode 100644 index 00000000..baf75874 Binary files /dev/null and b/examples/data/Fruit360/mango/164_100.jpg differ diff --git a/examples/data/Fruit360/mango/165_100.jpg b/examples/data/Fruit360/mango/165_100.jpg new file mode 100644 index 00000000..06e6145f Binary files /dev/null and b/examples/data/Fruit360/mango/165_100.jpg differ diff --git a/examples/data/Fruit360/mango/166_100.jpg b/examples/data/Fruit360/mango/166_100.jpg new file mode 100644 index 00000000..5d03e95d Binary files /dev/null and b/examples/data/Fruit360/mango/166_100.jpg differ diff --git a/examples/data/Fruit360/mango/167_100.jpg b/examples/data/Fruit360/mango/167_100.jpg new file mode 100644 index 00000000..aacb4eb7 Binary files /dev/null and b/examples/data/Fruit360/mango/167_100.jpg differ diff --git a/examples/data/Fruit360/mango/168_100.jpg b/examples/data/Fruit360/mango/168_100.jpg new file mode 100644 index 00000000..03f0f196 Binary files /dev/null and b/examples/data/Fruit360/mango/168_100.jpg differ diff --git a/examples/data/Fruit360/mango/169_100.jpg b/examples/data/Fruit360/mango/169_100.jpg new file mode 100644 index 00000000..23e9ddcc Binary files /dev/null and b/examples/data/Fruit360/mango/169_100.jpg differ diff --git a/examples/data/Fruit360/mango/16_100.jpg b/examples/data/Fruit360/mango/16_100.jpg new file mode 100644 index 00000000..6b403e0d Binary files /dev/null and b/examples/data/Fruit360/mango/16_100.jpg differ diff --git a/examples/data/Fruit360/mango/170_100.jpg b/examples/data/Fruit360/mango/170_100.jpg new file mode 100644 index 00000000..9b2436fb Binary files /dev/null and b/examples/data/Fruit360/mango/170_100.jpg differ diff --git a/examples/data/Fruit360/mango/171_100.jpg b/examples/data/Fruit360/mango/171_100.jpg new file mode 100644 index 00000000..5d5be4e8 Binary files /dev/null and b/examples/data/Fruit360/mango/171_100.jpg differ diff --git a/examples/data/Fruit360/mango/172_100.jpg b/examples/data/Fruit360/mango/172_100.jpg new file mode 100644 index 00000000..9a98817e Binary files /dev/null and b/examples/data/Fruit360/mango/172_100.jpg differ diff --git a/examples/data/Fruit360/mango/173_100.jpg b/examples/data/Fruit360/mango/173_100.jpg new file mode 100644 index 00000000..a3c6d18b Binary files /dev/null and b/examples/data/Fruit360/mango/173_100.jpg differ diff --git a/examples/data/Fruit360/mango/174_100.jpg b/examples/data/Fruit360/mango/174_100.jpg new file mode 100644 index 00000000..840e3193 Binary files /dev/null and b/examples/data/Fruit360/mango/174_100.jpg differ diff --git a/examples/data/Fruit360/mango/175_100.jpg b/examples/data/Fruit360/mango/175_100.jpg new file mode 100644 index 00000000..67d2c4ff Binary files /dev/null and b/examples/data/Fruit360/mango/175_100.jpg differ diff --git a/examples/data/Fruit360/mango/176_100.jpg b/examples/data/Fruit360/mango/176_100.jpg new file mode 100644 index 00000000..81576fe6 Binary files /dev/null and b/examples/data/Fruit360/mango/176_100.jpg differ diff --git a/examples/data/Fruit360/mango/177_100.jpg b/examples/data/Fruit360/mango/177_100.jpg new file mode 100644 index 00000000..df303119 Binary files /dev/null and b/examples/data/Fruit360/mango/177_100.jpg differ diff --git a/examples/data/Fruit360/mango/178_100.jpg b/examples/data/Fruit360/mango/178_100.jpg new file mode 100644 index 00000000..44827b36 Binary files /dev/null and b/examples/data/Fruit360/mango/178_100.jpg differ diff --git a/examples/data/Fruit360/mango/179_100.jpg b/examples/data/Fruit360/mango/179_100.jpg new file mode 100644 index 00000000..4399bd19 Binary files /dev/null and b/examples/data/Fruit360/mango/179_100.jpg differ diff --git a/examples/data/Fruit360/mango/17_100.jpg b/examples/data/Fruit360/mango/17_100.jpg new file mode 100644 index 00000000..6dd668dd Binary files /dev/null and b/examples/data/Fruit360/mango/17_100.jpg differ diff --git a/examples/data/Fruit360/mango/180_100.jpg b/examples/data/Fruit360/mango/180_100.jpg new file mode 100644 index 00000000..e96f17b2 Binary files /dev/null and b/examples/data/Fruit360/mango/180_100.jpg differ diff --git a/examples/data/Fruit360/mango/181_100.jpg b/examples/data/Fruit360/mango/181_100.jpg new file mode 100644 index 00000000..6a75f9e8 Binary files /dev/null and b/examples/data/Fruit360/mango/181_100.jpg differ diff --git a/examples/data/Fruit360/mango/185_100.jpg b/examples/data/Fruit360/mango/185_100.jpg new file mode 100644 index 00000000..27a47104 Binary files /dev/null and b/examples/data/Fruit360/mango/185_100.jpg differ diff --git a/examples/data/Fruit360/mango/186_100.jpg b/examples/data/Fruit360/mango/186_100.jpg new file mode 100644 index 00000000..fc8b5471 Binary files /dev/null and b/examples/data/Fruit360/mango/186_100.jpg differ diff --git a/examples/data/Fruit360/mango/187_100.jpg b/examples/data/Fruit360/mango/187_100.jpg new file mode 100644 index 00000000..34bc1cbf Binary files /dev/null and b/examples/data/Fruit360/mango/187_100.jpg differ diff --git a/examples/data/Fruit360/mango/188_100.jpg b/examples/data/Fruit360/mango/188_100.jpg new file mode 100644 index 00000000..0c9112bc Binary files /dev/null and b/examples/data/Fruit360/mango/188_100.jpg differ diff --git a/examples/data/Fruit360/mango/189_100.jpg b/examples/data/Fruit360/mango/189_100.jpg new file mode 100644 index 00000000..1b5f0fdf Binary files /dev/null and b/examples/data/Fruit360/mango/189_100.jpg differ diff --git a/examples/data/Fruit360/mango/18_100.jpg b/examples/data/Fruit360/mango/18_100.jpg new file mode 100644 index 00000000..abbfa7e6 Binary files /dev/null and b/examples/data/Fruit360/mango/18_100.jpg differ diff --git a/examples/data/Fruit360/mango/190_100.jpg b/examples/data/Fruit360/mango/190_100.jpg new file mode 100644 index 00000000..8f5f4d6b Binary files /dev/null and b/examples/data/Fruit360/mango/190_100.jpg differ diff --git a/examples/data/Fruit360/mango/191_100.jpg b/examples/data/Fruit360/mango/191_100.jpg new file mode 100644 index 00000000..8567793f Binary files /dev/null and b/examples/data/Fruit360/mango/191_100.jpg differ diff --git a/examples/data/Fruit360/mango/192_100.jpg b/examples/data/Fruit360/mango/192_100.jpg new file mode 100644 index 00000000..908f437c Binary files /dev/null and b/examples/data/Fruit360/mango/192_100.jpg differ diff --git a/examples/data/Fruit360/mango/193_100.jpg b/examples/data/Fruit360/mango/193_100.jpg new file mode 100644 index 00000000..28a1a67b Binary files /dev/null and b/examples/data/Fruit360/mango/193_100.jpg differ diff --git a/examples/data/Fruit360/mango/194_100.jpg b/examples/data/Fruit360/mango/194_100.jpg new file mode 100644 index 00000000..f6a41e05 Binary files /dev/null and b/examples/data/Fruit360/mango/194_100.jpg differ diff --git a/examples/data/Fruit360/mango/195_100.jpg b/examples/data/Fruit360/mango/195_100.jpg new file mode 100644 index 00000000..bba0b2a1 Binary files /dev/null and b/examples/data/Fruit360/mango/195_100.jpg differ diff --git a/examples/data/Fruit360/mango/196_100.jpg b/examples/data/Fruit360/mango/196_100.jpg new file mode 100644 index 00000000..46fc84e1 Binary files /dev/null and b/examples/data/Fruit360/mango/196_100.jpg differ diff --git a/examples/data/Fruit360/mango/197_100.jpg b/examples/data/Fruit360/mango/197_100.jpg new file mode 100644 index 00000000..33a549f7 Binary files /dev/null and b/examples/data/Fruit360/mango/197_100.jpg differ diff --git a/examples/data/Fruit360/mango/198_100.jpg b/examples/data/Fruit360/mango/198_100.jpg new file mode 100644 index 00000000..aabbe4ed Binary files /dev/null and b/examples/data/Fruit360/mango/198_100.jpg differ diff --git a/examples/data/Fruit360/mango/199_100.jpg b/examples/data/Fruit360/mango/199_100.jpg new file mode 100644 index 00000000..194c987b Binary files /dev/null and b/examples/data/Fruit360/mango/199_100.jpg differ diff --git a/examples/data/Fruit360/mango/19_100.jpg b/examples/data/Fruit360/mango/19_100.jpg new file mode 100644 index 00000000..7d950c24 Binary files /dev/null and b/examples/data/Fruit360/mango/19_100.jpg differ diff --git a/examples/data/Fruit360/mango/200_100.jpg b/examples/data/Fruit360/mango/200_100.jpg new file mode 100644 index 00000000..dca24698 Binary files /dev/null and b/examples/data/Fruit360/mango/200_100.jpg differ diff --git a/examples/data/Fruit360/mango/201_100.jpg b/examples/data/Fruit360/mango/201_100.jpg new file mode 100644 index 00000000..2b9c6ec1 Binary files /dev/null and b/examples/data/Fruit360/mango/201_100.jpg differ diff --git a/examples/data/Fruit360/mango/202_100.jpg b/examples/data/Fruit360/mango/202_100.jpg new file mode 100644 index 00000000..6f96cc6b Binary files /dev/null and b/examples/data/Fruit360/mango/202_100.jpg differ diff --git a/examples/data/Fruit360/mango/203_100.jpg b/examples/data/Fruit360/mango/203_100.jpg new file mode 100644 index 00000000..452f8649 Binary files /dev/null and b/examples/data/Fruit360/mango/203_100.jpg differ diff --git a/examples/data/Fruit360/mango/204_100.jpg b/examples/data/Fruit360/mango/204_100.jpg new file mode 100644 index 00000000..3c2b4e85 Binary files /dev/null and b/examples/data/Fruit360/mango/204_100.jpg differ diff --git a/examples/data/Fruit360/mango/205_100.jpg b/examples/data/Fruit360/mango/205_100.jpg new file mode 100644 index 00000000..32c65074 Binary files /dev/null and b/examples/data/Fruit360/mango/205_100.jpg differ diff --git a/examples/data/Fruit360/mango/206_100.jpg b/examples/data/Fruit360/mango/206_100.jpg new file mode 100644 index 00000000..2c63f78e Binary files /dev/null and b/examples/data/Fruit360/mango/206_100.jpg differ diff --git a/examples/data/Fruit360/mango/207_100.jpg b/examples/data/Fruit360/mango/207_100.jpg new file mode 100644 index 00000000..7974f059 Binary files /dev/null and b/examples/data/Fruit360/mango/207_100.jpg differ diff --git a/examples/data/Fruit360/mango/208_100.jpg b/examples/data/Fruit360/mango/208_100.jpg new file mode 100644 index 00000000..eee0e3b6 Binary files /dev/null and b/examples/data/Fruit360/mango/208_100.jpg differ diff --git a/examples/data/Fruit360/mango/209_100.jpg b/examples/data/Fruit360/mango/209_100.jpg new file mode 100644 index 00000000..43b0c3cf Binary files /dev/null and b/examples/data/Fruit360/mango/209_100.jpg differ diff --git a/examples/data/Fruit360/mango/20_100.jpg b/examples/data/Fruit360/mango/20_100.jpg new file mode 100644 index 00000000..8b11cc55 Binary files /dev/null and b/examples/data/Fruit360/mango/20_100.jpg differ diff --git a/examples/data/Fruit360/mango/210_100.jpg b/examples/data/Fruit360/mango/210_100.jpg new file mode 100644 index 00000000..879ae28d Binary files /dev/null and b/examples/data/Fruit360/mango/210_100.jpg differ diff --git a/examples/data/Fruit360/mango/211_100.jpg b/examples/data/Fruit360/mango/211_100.jpg new file mode 100644 index 00000000..2ca1a7a6 Binary files /dev/null and b/examples/data/Fruit360/mango/211_100.jpg differ diff --git a/examples/data/Fruit360/mango/212_100.jpg b/examples/data/Fruit360/mango/212_100.jpg new file mode 100644 index 00000000..377bae46 Binary files /dev/null and b/examples/data/Fruit360/mango/212_100.jpg differ diff --git a/examples/data/Fruit360/mango/213_100.jpg b/examples/data/Fruit360/mango/213_100.jpg new file mode 100644 index 00000000..9f375648 Binary files /dev/null and b/examples/data/Fruit360/mango/213_100.jpg differ diff --git a/examples/data/Fruit360/mango/214_100.jpg b/examples/data/Fruit360/mango/214_100.jpg new file mode 100644 index 00000000..78728111 Binary files /dev/null and b/examples/data/Fruit360/mango/214_100.jpg differ diff --git a/examples/data/Fruit360/mango/215_100.jpg b/examples/data/Fruit360/mango/215_100.jpg new file mode 100644 index 00000000..fd348678 Binary files /dev/null and b/examples/data/Fruit360/mango/215_100.jpg differ diff --git a/examples/data/Fruit360/mango/216_100.jpg b/examples/data/Fruit360/mango/216_100.jpg new file mode 100644 index 00000000..03d10aa0 Binary files /dev/null and b/examples/data/Fruit360/mango/216_100.jpg differ diff --git a/examples/data/Fruit360/mango/217_100.jpg b/examples/data/Fruit360/mango/217_100.jpg new file mode 100644 index 00000000..2e910632 Binary files /dev/null and b/examples/data/Fruit360/mango/217_100.jpg differ diff --git a/examples/data/Fruit360/mango/218_100.jpg b/examples/data/Fruit360/mango/218_100.jpg new file mode 100644 index 00000000..40395468 Binary files /dev/null and b/examples/data/Fruit360/mango/218_100.jpg differ diff --git a/examples/data/Fruit360/mango/219_100.jpg b/examples/data/Fruit360/mango/219_100.jpg new file mode 100644 index 00000000..9de89ec2 Binary files /dev/null and b/examples/data/Fruit360/mango/219_100.jpg differ diff --git a/examples/data/Fruit360/mango/21_100.jpg b/examples/data/Fruit360/mango/21_100.jpg new file mode 100644 index 00000000..73186432 Binary files /dev/null and b/examples/data/Fruit360/mango/21_100.jpg differ diff --git a/examples/data/Fruit360/mango/220_100.jpg b/examples/data/Fruit360/mango/220_100.jpg new file mode 100644 index 00000000..d72c9070 Binary files /dev/null and b/examples/data/Fruit360/mango/220_100.jpg differ diff --git a/examples/data/Fruit360/mango/221_100.jpg b/examples/data/Fruit360/mango/221_100.jpg new file mode 100644 index 00000000..2372ab9a Binary files /dev/null and b/examples/data/Fruit360/mango/221_100.jpg differ diff --git a/examples/data/Fruit360/mango/222_100.jpg b/examples/data/Fruit360/mango/222_100.jpg new file mode 100644 index 00000000..ad602d4a Binary files /dev/null and b/examples/data/Fruit360/mango/222_100.jpg differ diff --git a/examples/data/Fruit360/mango/223_100.jpg b/examples/data/Fruit360/mango/223_100.jpg new file mode 100644 index 00000000..2d34fce5 Binary files /dev/null and b/examples/data/Fruit360/mango/223_100.jpg differ diff --git a/examples/data/Fruit360/mango/224_100.jpg b/examples/data/Fruit360/mango/224_100.jpg new file mode 100644 index 00000000..358089ef Binary files /dev/null and b/examples/data/Fruit360/mango/224_100.jpg differ diff --git a/examples/data/Fruit360/mango/225_100.jpg b/examples/data/Fruit360/mango/225_100.jpg new file mode 100644 index 00000000..1e327316 Binary files /dev/null and b/examples/data/Fruit360/mango/225_100.jpg differ diff --git a/examples/data/Fruit360/mango/226_100.jpg b/examples/data/Fruit360/mango/226_100.jpg new file mode 100644 index 00000000..12bd4d2b Binary files /dev/null and b/examples/data/Fruit360/mango/226_100.jpg differ diff --git a/examples/data/Fruit360/mango/227_100.jpg b/examples/data/Fruit360/mango/227_100.jpg new file mode 100644 index 00000000..8d5f8979 Binary files /dev/null and b/examples/data/Fruit360/mango/227_100.jpg differ diff --git a/examples/data/Fruit360/mango/228_100.jpg b/examples/data/Fruit360/mango/228_100.jpg new file mode 100644 index 00000000..291450b3 Binary files /dev/null and b/examples/data/Fruit360/mango/228_100.jpg differ diff --git a/examples/data/Fruit360/mango/229_100.jpg b/examples/data/Fruit360/mango/229_100.jpg new file mode 100644 index 00000000..736106fe Binary files /dev/null and b/examples/data/Fruit360/mango/229_100.jpg differ diff --git a/examples/data/Fruit360/mango/22_100.jpg b/examples/data/Fruit360/mango/22_100.jpg new file mode 100644 index 00000000..04c249b8 Binary files /dev/null and b/examples/data/Fruit360/mango/22_100.jpg differ diff --git a/examples/data/Fruit360/mango/230_100.jpg b/examples/data/Fruit360/mango/230_100.jpg new file mode 100644 index 00000000..811038bc Binary files /dev/null and b/examples/data/Fruit360/mango/230_100.jpg differ diff --git a/examples/data/Fruit360/mango/231_100.jpg b/examples/data/Fruit360/mango/231_100.jpg new file mode 100644 index 00000000..dcda8641 Binary files /dev/null and b/examples/data/Fruit360/mango/231_100.jpg differ diff --git a/examples/data/Fruit360/mango/232_100.jpg b/examples/data/Fruit360/mango/232_100.jpg new file mode 100644 index 00000000..afa6a014 Binary files /dev/null and b/examples/data/Fruit360/mango/232_100.jpg differ diff --git a/examples/data/Fruit360/mango/233_100.jpg b/examples/data/Fruit360/mango/233_100.jpg new file mode 100644 index 00000000..9d3e3b32 Binary files /dev/null and b/examples/data/Fruit360/mango/233_100.jpg differ diff --git a/examples/data/Fruit360/mango/234_100.jpg b/examples/data/Fruit360/mango/234_100.jpg new file mode 100644 index 00000000..46e8dfae Binary files /dev/null and b/examples/data/Fruit360/mango/234_100.jpg differ diff --git a/examples/data/Fruit360/mango/235_100.jpg b/examples/data/Fruit360/mango/235_100.jpg new file mode 100644 index 00000000..d9a1b360 Binary files /dev/null and b/examples/data/Fruit360/mango/235_100.jpg differ diff --git a/examples/data/Fruit360/mango/236_100.jpg b/examples/data/Fruit360/mango/236_100.jpg new file mode 100644 index 00000000..1135612e Binary files /dev/null and b/examples/data/Fruit360/mango/236_100.jpg differ diff --git a/examples/data/Fruit360/mango/237_100.jpg b/examples/data/Fruit360/mango/237_100.jpg new file mode 100644 index 00000000..3069cf69 Binary files /dev/null and b/examples/data/Fruit360/mango/237_100.jpg differ diff --git a/examples/data/Fruit360/mango/238_100.jpg b/examples/data/Fruit360/mango/238_100.jpg new file mode 100644 index 00000000..fe185682 Binary files /dev/null and b/examples/data/Fruit360/mango/238_100.jpg differ diff --git a/examples/data/Fruit360/mango/239_100.jpg b/examples/data/Fruit360/mango/239_100.jpg new file mode 100644 index 00000000..88e7579e Binary files /dev/null and b/examples/data/Fruit360/mango/239_100.jpg differ diff --git a/examples/data/Fruit360/mango/23_100.jpg b/examples/data/Fruit360/mango/23_100.jpg new file mode 100644 index 00000000..7cc8f486 Binary files /dev/null and b/examples/data/Fruit360/mango/23_100.jpg differ diff --git a/examples/data/Fruit360/mango/240_100.jpg b/examples/data/Fruit360/mango/240_100.jpg new file mode 100644 index 00000000..0a252b49 Binary files /dev/null and b/examples/data/Fruit360/mango/240_100.jpg differ diff --git a/examples/data/Fruit360/mango/241_100.jpg b/examples/data/Fruit360/mango/241_100.jpg new file mode 100644 index 00000000..51f9c3be Binary files /dev/null and b/examples/data/Fruit360/mango/241_100.jpg differ diff --git a/examples/data/Fruit360/mango/242_100.jpg b/examples/data/Fruit360/mango/242_100.jpg new file mode 100644 index 00000000..854f5de7 Binary files /dev/null and b/examples/data/Fruit360/mango/242_100.jpg differ diff --git a/examples/data/Fruit360/mango/243_100.jpg b/examples/data/Fruit360/mango/243_100.jpg new file mode 100644 index 00000000..0da9df91 Binary files /dev/null and b/examples/data/Fruit360/mango/243_100.jpg differ diff --git a/examples/data/Fruit360/mango/244_100.jpg b/examples/data/Fruit360/mango/244_100.jpg new file mode 100644 index 00000000..72f5b419 Binary files /dev/null and b/examples/data/Fruit360/mango/244_100.jpg differ diff --git a/examples/data/Fruit360/mango/245_100.jpg b/examples/data/Fruit360/mango/245_100.jpg new file mode 100644 index 00000000..04c017ae Binary files /dev/null and b/examples/data/Fruit360/mango/245_100.jpg differ diff --git a/examples/data/Fruit360/mango/246_100.jpg b/examples/data/Fruit360/mango/246_100.jpg new file mode 100644 index 00000000..b2833e0f Binary files /dev/null and b/examples/data/Fruit360/mango/246_100.jpg differ diff --git a/examples/data/Fruit360/mango/247_100.jpg b/examples/data/Fruit360/mango/247_100.jpg new file mode 100644 index 00000000..5cb62a9f Binary files /dev/null and b/examples/data/Fruit360/mango/247_100.jpg differ diff --git a/examples/data/Fruit360/mango/248_100.jpg b/examples/data/Fruit360/mango/248_100.jpg new file mode 100644 index 00000000..534c3ff8 Binary files /dev/null and b/examples/data/Fruit360/mango/248_100.jpg differ diff --git a/examples/data/Fruit360/mango/249_100.jpg b/examples/data/Fruit360/mango/249_100.jpg new file mode 100644 index 00000000..8fd827b1 Binary files /dev/null and b/examples/data/Fruit360/mango/249_100.jpg differ diff --git a/examples/data/Fruit360/mango/24_100.jpg b/examples/data/Fruit360/mango/24_100.jpg new file mode 100644 index 00000000..6e5373fc Binary files /dev/null and b/examples/data/Fruit360/mango/24_100.jpg differ diff --git a/examples/data/Fruit360/mango/250_100.jpg b/examples/data/Fruit360/mango/250_100.jpg new file mode 100644 index 00000000..ff22ce9e Binary files /dev/null and b/examples/data/Fruit360/mango/250_100.jpg differ diff --git a/examples/data/Fruit360/mango/251_100.jpg b/examples/data/Fruit360/mango/251_100.jpg new file mode 100644 index 00000000..9c45a32d Binary files /dev/null and b/examples/data/Fruit360/mango/251_100.jpg differ diff --git a/examples/data/Fruit360/mango/252_100.jpg b/examples/data/Fruit360/mango/252_100.jpg new file mode 100644 index 00000000..f520ca99 Binary files /dev/null and b/examples/data/Fruit360/mango/252_100.jpg differ diff --git a/examples/data/Fruit360/mango/253_100.jpg b/examples/data/Fruit360/mango/253_100.jpg new file mode 100644 index 00000000..b1a69128 Binary files /dev/null and b/examples/data/Fruit360/mango/253_100.jpg differ diff --git a/examples/data/Fruit360/mango/254_100.jpg b/examples/data/Fruit360/mango/254_100.jpg new file mode 100644 index 00000000..2d35f03b Binary files /dev/null and b/examples/data/Fruit360/mango/254_100.jpg differ diff --git a/examples/data/Fruit360/mango/255_100.jpg b/examples/data/Fruit360/mango/255_100.jpg new file mode 100644 index 00000000..bb67f24c Binary files /dev/null and b/examples/data/Fruit360/mango/255_100.jpg differ diff --git a/examples/data/Fruit360/mango/256_100.jpg b/examples/data/Fruit360/mango/256_100.jpg new file mode 100644 index 00000000..4ba5cee5 Binary files /dev/null and b/examples/data/Fruit360/mango/256_100.jpg differ diff --git a/examples/data/Fruit360/mango/257_100.jpg b/examples/data/Fruit360/mango/257_100.jpg new file mode 100644 index 00000000..bf0e077b Binary files /dev/null and b/examples/data/Fruit360/mango/257_100.jpg differ diff --git a/examples/data/Fruit360/mango/258_100.jpg b/examples/data/Fruit360/mango/258_100.jpg new file mode 100644 index 00000000..34f84771 Binary files /dev/null and b/examples/data/Fruit360/mango/258_100.jpg differ diff --git a/examples/data/Fruit360/mango/259_100.jpg b/examples/data/Fruit360/mango/259_100.jpg new file mode 100644 index 00000000..9e2efa01 Binary files /dev/null and b/examples/data/Fruit360/mango/259_100.jpg differ diff --git a/examples/data/Fruit360/mango/25_100.jpg b/examples/data/Fruit360/mango/25_100.jpg new file mode 100644 index 00000000..5f08d727 Binary files /dev/null and b/examples/data/Fruit360/mango/25_100.jpg differ diff --git a/examples/data/Fruit360/mango/263_100.jpg b/examples/data/Fruit360/mango/263_100.jpg new file mode 100644 index 00000000..525978eb Binary files /dev/null and b/examples/data/Fruit360/mango/263_100.jpg differ diff --git a/examples/data/Fruit360/mango/265_100.jpg b/examples/data/Fruit360/mango/265_100.jpg new file mode 100644 index 00000000..e4daffcf Binary files /dev/null and b/examples/data/Fruit360/mango/265_100.jpg differ diff --git a/examples/data/Fruit360/mango/266_100.jpg b/examples/data/Fruit360/mango/266_100.jpg new file mode 100644 index 00000000..5e611185 Binary files /dev/null and b/examples/data/Fruit360/mango/266_100.jpg differ diff --git a/examples/data/Fruit360/mango/267_100.jpg b/examples/data/Fruit360/mango/267_100.jpg new file mode 100644 index 00000000..181f3bdd Binary files /dev/null and b/examples/data/Fruit360/mango/267_100.jpg differ diff --git a/examples/data/Fruit360/mango/268_100.jpg b/examples/data/Fruit360/mango/268_100.jpg new file mode 100644 index 00000000..64c1a19c Binary files /dev/null and b/examples/data/Fruit360/mango/268_100.jpg differ diff --git a/examples/data/Fruit360/mango/26_100.jpg b/examples/data/Fruit360/mango/26_100.jpg new file mode 100644 index 00000000..2c206ea8 Binary files /dev/null and b/examples/data/Fruit360/mango/26_100.jpg differ diff --git a/examples/data/Fruit360/mango/271_100.jpg b/examples/data/Fruit360/mango/271_100.jpg new file mode 100644 index 00000000..e8af6212 Binary files /dev/null and b/examples/data/Fruit360/mango/271_100.jpg differ diff --git a/examples/data/Fruit360/mango/272_100.jpg b/examples/data/Fruit360/mango/272_100.jpg new file mode 100644 index 00000000..0ef74af4 Binary files /dev/null and b/examples/data/Fruit360/mango/272_100.jpg differ diff --git a/examples/data/Fruit360/mango/275_100.jpg b/examples/data/Fruit360/mango/275_100.jpg new file mode 100644 index 00000000..a944284e Binary files /dev/null and b/examples/data/Fruit360/mango/275_100.jpg differ diff --git a/examples/data/Fruit360/mango/276_100.jpg b/examples/data/Fruit360/mango/276_100.jpg new file mode 100644 index 00000000..a1a2fa1b Binary files /dev/null and b/examples/data/Fruit360/mango/276_100.jpg differ diff --git a/examples/data/Fruit360/mango/277_100.jpg b/examples/data/Fruit360/mango/277_100.jpg new file mode 100644 index 00000000..09be73ce Binary files /dev/null and b/examples/data/Fruit360/mango/277_100.jpg differ diff --git a/examples/data/Fruit360/mango/27_100.jpg b/examples/data/Fruit360/mango/27_100.jpg new file mode 100644 index 00000000..d33cd366 Binary files /dev/null and b/examples/data/Fruit360/mango/27_100.jpg differ diff --git a/examples/data/Fruit360/mango/280_100.jpg b/examples/data/Fruit360/mango/280_100.jpg new file mode 100644 index 00000000..eec6c28a Binary files /dev/null and b/examples/data/Fruit360/mango/280_100.jpg differ diff --git a/examples/data/Fruit360/mango/281_100.jpg b/examples/data/Fruit360/mango/281_100.jpg new file mode 100644 index 00000000..b3569d8d Binary files /dev/null and b/examples/data/Fruit360/mango/281_100.jpg differ diff --git a/examples/data/Fruit360/mango/282_100.jpg b/examples/data/Fruit360/mango/282_100.jpg new file mode 100644 index 00000000..d9d78b84 Binary files /dev/null and b/examples/data/Fruit360/mango/282_100.jpg differ diff --git a/examples/data/Fruit360/mango/283_100.jpg b/examples/data/Fruit360/mango/283_100.jpg new file mode 100644 index 00000000..87c23c2f Binary files /dev/null and b/examples/data/Fruit360/mango/283_100.jpg differ diff --git a/examples/data/Fruit360/mango/284_100.jpg b/examples/data/Fruit360/mango/284_100.jpg new file mode 100644 index 00000000..ab036726 Binary files /dev/null and b/examples/data/Fruit360/mango/284_100.jpg differ diff --git a/examples/data/Fruit360/mango/285_100.jpg b/examples/data/Fruit360/mango/285_100.jpg new file mode 100644 index 00000000..c4b45f68 Binary files /dev/null and b/examples/data/Fruit360/mango/285_100.jpg differ diff --git a/examples/data/Fruit360/mango/286_100.jpg b/examples/data/Fruit360/mango/286_100.jpg new file mode 100644 index 00000000..d940be9f Binary files /dev/null and b/examples/data/Fruit360/mango/286_100.jpg differ diff --git a/examples/data/Fruit360/mango/287_100.jpg b/examples/data/Fruit360/mango/287_100.jpg new file mode 100644 index 00000000..93740d7a Binary files /dev/null and b/examples/data/Fruit360/mango/287_100.jpg differ diff --git a/examples/data/Fruit360/mango/288_100.jpg b/examples/data/Fruit360/mango/288_100.jpg new file mode 100644 index 00000000..50394472 Binary files /dev/null and b/examples/data/Fruit360/mango/288_100.jpg differ diff --git a/examples/data/Fruit360/mango/289_100.jpg b/examples/data/Fruit360/mango/289_100.jpg new file mode 100644 index 00000000..288f9b1b Binary files /dev/null and b/examples/data/Fruit360/mango/289_100.jpg differ diff --git a/examples/data/Fruit360/mango/28_100.jpg b/examples/data/Fruit360/mango/28_100.jpg new file mode 100644 index 00000000..c232ec80 Binary files /dev/null and b/examples/data/Fruit360/mango/28_100.jpg differ diff --git a/examples/data/Fruit360/mango/290_100.jpg b/examples/data/Fruit360/mango/290_100.jpg new file mode 100644 index 00000000..7387307d Binary files /dev/null and b/examples/data/Fruit360/mango/290_100.jpg differ diff --git a/examples/data/Fruit360/mango/291_100.jpg b/examples/data/Fruit360/mango/291_100.jpg new file mode 100644 index 00000000..03c963c8 Binary files /dev/null and b/examples/data/Fruit360/mango/291_100.jpg differ diff --git a/examples/data/Fruit360/mango/294_100.jpg b/examples/data/Fruit360/mango/294_100.jpg new file mode 100644 index 00000000..90402ed8 Binary files /dev/null and b/examples/data/Fruit360/mango/294_100.jpg differ diff --git a/examples/data/Fruit360/mango/295_100.jpg b/examples/data/Fruit360/mango/295_100.jpg new file mode 100644 index 00000000..25423241 Binary files /dev/null and b/examples/data/Fruit360/mango/295_100.jpg differ diff --git a/examples/data/Fruit360/mango/296_100.jpg b/examples/data/Fruit360/mango/296_100.jpg new file mode 100644 index 00000000..742a5b36 Binary files /dev/null and b/examples/data/Fruit360/mango/296_100.jpg differ diff --git a/examples/data/Fruit360/mango/297_100.jpg b/examples/data/Fruit360/mango/297_100.jpg new file mode 100644 index 00000000..c6d250c6 Binary files /dev/null and b/examples/data/Fruit360/mango/297_100.jpg differ diff --git a/examples/data/Fruit360/mango/298_100.jpg b/examples/data/Fruit360/mango/298_100.jpg new file mode 100644 index 00000000..459c5a50 Binary files /dev/null and b/examples/data/Fruit360/mango/298_100.jpg differ diff --git a/examples/data/Fruit360/mango/29_100.jpg b/examples/data/Fruit360/mango/29_100.jpg new file mode 100644 index 00000000..663ade5f Binary files /dev/null and b/examples/data/Fruit360/mango/29_100.jpg differ diff --git a/examples/data/Fruit360/mango/30_100.jpg b/examples/data/Fruit360/mango/30_100.jpg new file mode 100644 index 00000000..58cb7fb5 Binary files /dev/null and b/examples/data/Fruit360/mango/30_100.jpg differ diff --git a/examples/data/Fruit360/mango/311_100.jpg b/examples/data/Fruit360/mango/311_100.jpg new file mode 100644 index 00000000..4134562a Binary files /dev/null and b/examples/data/Fruit360/mango/311_100.jpg differ diff --git a/examples/data/Fruit360/mango/31_100.jpg b/examples/data/Fruit360/mango/31_100.jpg new file mode 100644 index 00000000..e72f64bd Binary files /dev/null and b/examples/data/Fruit360/mango/31_100.jpg differ diff --git a/examples/data/Fruit360/mango/322_100.jpg b/examples/data/Fruit360/mango/322_100.jpg new file mode 100644 index 00000000..c71aa768 Binary files /dev/null and b/examples/data/Fruit360/mango/322_100.jpg differ diff --git a/examples/data/Fruit360/mango/324_100.jpg b/examples/data/Fruit360/mango/324_100.jpg new file mode 100644 index 00000000..6991ffb5 Binary files /dev/null and b/examples/data/Fruit360/mango/324_100.jpg differ diff --git a/examples/data/Fruit360/mango/325_100.jpg b/examples/data/Fruit360/mango/325_100.jpg new file mode 100644 index 00000000..66f21721 Binary files /dev/null and b/examples/data/Fruit360/mango/325_100.jpg differ diff --git a/examples/data/Fruit360/mango/326_100.jpg b/examples/data/Fruit360/mango/326_100.jpg new file mode 100644 index 00000000..c1d638fb Binary files /dev/null and b/examples/data/Fruit360/mango/326_100.jpg differ diff --git a/examples/data/Fruit360/mango/327_100.jpg b/examples/data/Fruit360/mango/327_100.jpg new file mode 100644 index 00000000..2515369f Binary files /dev/null and b/examples/data/Fruit360/mango/327_100.jpg differ diff --git a/examples/data/Fruit360/mango/32_100.jpg b/examples/data/Fruit360/mango/32_100.jpg new file mode 100644 index 00000000..a7a3c4cb Binary files /dev/null and b/examples/data/Fruit360/mango/32_100.jpg differ diff --git a/examples/data/Fruit360/mango/33_100.jpg b/examples/data/Fruit360/mango/33_100.jpg new file mode 100644 index 00000000..f9e5a8b6 Binary files /dev/null and b/examples/data/Fruit360/mango/33_100.jpg differ diff --git a/examples/data/Fruit360/mango/34_100.jpg b/examples/data/Fruit360/mango/34_100.jpg new file mode 100644 index 00000000..4ba006b8 Binary files /dev/null and b/examples/data/Fruit360/mango/34_100.jpg differ diff --git a/examples/data/Fruit360/mango/35_100.jpg b/examples/data/Fruit360/mango/35_100.jpg new file mode 100644 index 00000000..759c824b Binary files /dev/null and b/examples/data/Fruit360/mango/35_100.jpg differ diff --git a/examples/data/Fruit360/mango/36_100.jpg b/examples/data/Fruit360/mango/36_100.jpg new file mode 100644 index 00000000..dadc4f77 Binary files /dev/null and b/examples/data/Fruit360/mango/36_100.jpg differ diff --git a/examples/data/Fruit360/mango/37_100.jpg b/examples/data/Fruit360/mango/37_100.jpg new file mode 100644 index 00000000..3125bb66 Binary files /dev/null and b/examples/data/Fruit360/mango/37_100.jpg differ diff --git a/examples/data/Fruit360/mango/38_100.jpg b/examples/data/Fruit360/mango/38_100.jpg new file mode 100644 index 00000000..c90967c1 Binary files /dev/null and b/examples/data/Fruit360/mango/38_100.jpg differ diff --git a/examples/data/Fruit360/mango/39_100.jpg b/examples/data/Fruit360/mango/39_100.jpg new file mode 100644 index 00000000..05a7e061 Binary files /dev/null and b/examples/data/Fruit360/mango/39_100.jpg differ diff --git a/examples/data/Fruit360/mango/40_100.jpg b/examples/data/Fruit360/mango/40_100.jpg new file mode 100644 index 00000000..8c66ccaa Binary files /dev/null and b/examples/data/Fruit360/mango/40_100.jpg differ diff --git a/examples/data/Fruit360/mango/41_100.jpg b/examples/data/Fruit360/mango/41_100.jpg new file mode 100644 index 00000000..728701b8 Binary files /dev/null and b/examples/data/Fruit360/mango/41_100.jpg differ diff --git a/examples/data/Fruit360/mango/42_100.jpg b/examples/data/Fruit360/mango/42_100.jpg new file mode 100644 index 00000000..c171f1b9 Binary files /dev/null and b/examples/data/Fruit360/mango/42_100.jpg differ diff --git a/examples/data/Fruit360/mango/43_100.jpg b/examples/data/Fruit360/mango/43_100.jpg new file mode 100644 index 00000000..64f3110f Binary files /dev/null and b/examples/data/Fruit360/mango/43_100.jpg differ diff --git a/examples/data/Fruit360/mango/44_100.jpg b/examples/data/Fruit360/mango/44_100.jpg new file mode 100644 index 00000000..03bb870d Binary files /dev/null and b/examples/data/Fruit360/mango/44_100.jpg differ diff --git a/examples/data/Fruit360/mango/45_100.jpg b/examples/data/Fruit360/mango/45_100.jpg new file mode 100644 index 00000000..8f613ee3 Binary files /dev/null and b/examples/data/Fruit360/mango/45_100.jpg differ diff --git a/examples/data/Fruit360/mango/46_100.jpg b/examples/data/Fruit360/mango/46_100.jpg new file mode 100644 index 00000000..8330c0b3 Binary files /dev/null and b/examples/data/Fruit360/mango/46_100.jpg differ diff --git a/examples/data/Fruit360/mango/47_100.jpg b/examples/data/Fruit360/mango/47_100.jpg new file mode 100644 index 00000000..82d865e0 Binary files /dev/null and b/examples/data/Fruit360/mango/47_100.jpg differ diff --git a/examples/data/Fruit360/mango/48_100.jpg b/examples/data/Fruit360/mango/48_100.jpg new file mode 100644 index 00000000..819444ca Binary files /dev/null and b/examples/data/Fruit360/mango/48_100.jpg differ diff --git a/examples/data/Fruit360/mango/49_100.jpg b/examples/data/Fruit360/mango/49_100.jpg new file mode 100644 index 00000000..f2145bd2 Binary files /dev/null and b/examples/data/Fruit360/mango/49_100.jpg differ diff --git a/examples/data/Fruit360/mango/50_100.jpg b/examples/data/Fruit360/mango/50_100.jpg new file mode 100644 index 00000000..4ec612d1 Binary files /dev/null and b/examples/data/Fruit360/mango/50_100.jpg differ diff --git a/examples/data/Fruit360/mango/51_100.jpg b/examples/data/Fruit360/mango/51_100.jpg new file mode 100644 index 00000000..3445c766 Binary files /dev/null and b/examples/data/Fruit360/mango/51_100.jpg differ diff --git a/examples/data/Fruit360/mango/52_100.jpg b/examples/data/Fruit360/mango/52_100.jpg new file mode 100644 index 00000000..02cc1390 Binary files /dev/null and b/examples/data/Fruit360/mango/52_100.jpg differ diff --git a/examples/data/Fruit360/mango/53_100.jpg b/examples/data/Fruit360/mango/53_100.jpg new file mode 100644 index 00000000..e1539204 Binary files /dev/null and b/examples/data/Fruit360/mango/53_100.jpg differ diff --git a/examples/data/Fruit360/mango/54_100.jpg b/examples/data/Fruit360/mango/54_100.jpg new file mode 100644 index 00000000..936e9c31 Binary files /dev/null and b/examples/data/Fruit360/mango/54_100.jpg differ diff --git a/examples/data/Fruit360/mango/55_100.jpg b/examples/data/Fruit360/mango/55_100.jpg new file mode 100644 index 00000000..dc78cf1f Binary files /dev/null and b/examples/data/Fruit360/mango/55_100.jpg differ diff --git a/examples/data/Fruit360/mango/56_100.jpg b/examples/data/Fruit360/mango/56_100.jpg new file mode 100644 index 00000000..826763b2 Binary files /dev/null and b/examples/data/Fruit360/mango/56_100.jpg differ diff --git a/examples/data/Fruit360/mango/57_100.jpg b/examples/data/Fruit360/mango/57_100.jpg new file mode 100644 index 00000000..d2689392 Binary files /dev/null and b/examples/data/Fruit360/mango/57_100.jpg differ diff --git a/examples/data/Fruit360/mango/58_100.jpg b/examples/data/Fruit360/mango/58_100.jpg new file mode 100644 index 00000000..a999488a Binary files /dev/null and b/examples/data/Fruit360/mango/58_100.jpg differ diff --git a/examples/data/Fruit360/mango/59_100.jpg b/examples/data/Fruit360/mango/59_100.jpg new file mode 100644 index 00000000..3529c5fb Binary files /dev/null and b/examples/data/Fruit360/mango/59_100.jpg differ diff --git a/examples/data/Fruit360/mango/60_100.jpg b/examples/data/Fruit360/mango/60_100.jpg new file mode 100644 index 00000000..27711699 Binary files /dev/null and b/examples/data/Fruit360/mango/60_100.jpg differ diff --git a/examples/data/Fruit360/mango/61_100.jpg b/examples/data/Fruit360/mango/61_100.jpg new file mode 100644 index 00000000..4f2883eb Binary files /dev/null and b/examples/data/Fruit360/mango/61_100.jpg differ diff --git a/examples/data/Fruit360/mango/62_100.jpg b/examples/data/Fruit360/mango/62_100.jpg new file mode 100644 index 00000000..2f86c75a Binary files /dev/null and b/examples/data/Fruit360/mango/62_100.jpg differ diff --git a/examples/data/Fruit360/mango/63_100.jpg b/examples/data/Fruit360/mango/63_100.jpg new file mode 100644 index 00000000..3103c64e Binary files /dev/null and b/examples/data/Fruit360/mango/63_100.jpg differ diff --git a/examples/data/Fruit360/mango/64_100.jpg b/examples/data/Fruit360/mango/64_100.jpg new file mode 100644 index 00000000..437fa75c Binary files /dev/null and b/examples/data/Fruit360/mango/64_100.jpg differ diff --git a/examples/data/Fruit360/mango/65_100.jpg b/examples/data/Fruit360/mango/65_100.jpg new file mode 100644 index 00000000..909cdaf3 Binary files /dev/null and b/examples/data/Fruit360/mango/65_100.jpg differ diff --git a/examples/data/Fruit360/mango/66_100.jpg b/examples/data/Fruit360/mango/66_100.jpg new file mode 100644 index 00000000..659548d8 Binary files /dev/null and b/examples/data/Fruit360/mango/66_100.jpg differ diff --git a/examples/data/Fruit360/mango/67_100.jpg b/examples/data/Fruit360/mango/67_100.jpg new file mode 100644 index 00000000..9421bdd5 Binary files /dev/null and b/examples/data/Fruit360/mango/67_100.jpg differ diff --git a/examples/data/Fruit360/mango/68_100.jpg b/examples/data/Fruit360/mango/68_100.jpg new file mode 100644 index 00000000..7d0fadfe Binary files /dev/null and b/examples/data/Fruit360/mango/68_100.jpg differ diff --git a/examples/data/Fruit360/mango/69_100.jpg b/examples/data/Fruit360/mango/69_100.jpg new file mode 100644 index 00000000..690c143b Binary files /dev/null and b/examples/data/Fruit360/mango/69_100.jpg differ diff --git a/examples/data/Fruit360/mango/70_100.jpg b/examples/data/Fruit360/mango/70_100.jpg new file mode 100644 index 00000000..505d029d Binary files /dev/null and b/examples/data/Fruit360/mango/70_100.jpg differ diff --git a/examples/data/Fruit360/mango/71_100.jpg b/examples/data/Fruit360/mango/71_100.jpg new file mode 100644 index 00000000..5735e886 Binary files /dev/null and b/examples/data/Fruit360/mango/71_100.jpg differ diff --git a/examples/data/Fruit360/mango/72_100.jpg b/examples/data/Fruit360/mango/72_100.jpg new file mode 100644 index 00000000..0d16d2ee Binary files /dev/null and b/examples/data/Fruit360/mango/72_100.jpg differ diff --git a/examples/data/Fruit360/mango/73_100.jpg b/examples/data/Fruit360/mango/73_100.jpg new file mode 100644 index 00000000..69f285f6 Binary files /dev/null and b/examples/data/Fruit360/mango/73_100.jpg differ diff --git a/examples/data/Fruit360/mango/74_100.jpg b/examples/data/Fruit360/mango/74_100.jpg new file mode 100644 index 00000000..a618264b Binary files /dev/null and b/examples/data/Fruit360/mango/74_100.jpg differ diff --git a/examples/data/Fruit360/mango/75_100.jpg b/examples/data/Fruit360/mango/75_100.jpg new file mode 100644 index 00000000..9191a48c Binary files /dev/null and b/examples/data/Fruit360/mango/75_100.jpg differ diff --git a/examples/data/Fruit360/mango/76_100.jpg b/examples/data/Fruit360/mango/76_100.jpg new file mode 100644 index 00000000..a01fd52b Binary files /dev/null and b/examples/data/Fruit360/mango/76_100.jpg differ diff --git a/examples/data/Fruit360/mango/77_100.jpg b/examples/data/Fruit360/mango/77_100.jpg new file mode 100644 index 00000000..bc391dab Binary files /dev/null and b/examples/data/Fruit360/mango/77_100.jpg differ diff --git a/examples/data/Fruit360/mango/78_100.jpg b/examples/data/Fruit360/mango/78_100.jpg new file mode 100644 index 00000000..83369820 Binary files /dev/null and b/examples/data/Fruit360/mango/78_100.jpg differ diff --git a/examples/data/Fruit360/mango/79_100.jpg b/examples/data/Fruit360/mango/79_100.jpg new file mode 100644 index 00000000..3fe0036b Binary files /dev/null and b/examples/data/Fruit360/mango/79_100.jpg differ diff --git a/examples/data/Fruit360/mango/80_100.jpg b/examples/data/Fruit360/mango/80_100.jpg new file mode 100644 index 00000000..53433ce7 Binary files /dev/null and b/examples/data/Fruit360/mango/80_100.jpg differ diff --git a/examples/data/Fruit360/mango/81_100.jpg b/examples/data/Fruit360/mango/81_100.jpg new file mode 100644 index 00000000..bfc5d821 Binary files /dev/null and b/examples/data/Fruit360/mango/81_100.jpg differ diff --git a/examples/data/Fruit360/mango/82_100.jpg b/examples/data/Fruit360/mango/82_100.jpg new file mode 100644 index 00000000..c1fbe240 Binary files /dev/null and b/examples/data/Fruit360/mango/82_100.jpg differ diff --git a/examples/data/Fruit360/mango/83_100.jpg b/examples/data/Fruit360/mango/83_100.jpg new file mode 100644 index 00000000..5c0e62b4 Binary files /dev/null and b/examples/data/Fruit360/mango/83_100.jpg differ diff --git a/examples/data/Fruit360/mango/84_100.jpg b/examples/data/Fruit360/mango/84_100.jpg new file mode 100644 index 00000000..41ea2836 Binary files /dev/null and b/examples/data/Fruit360/mango/84_100.jpg differ diff --git a/examples/data/Fruit360/mango/85_100.jpg b/examples/data/Fruit360/mango/85_100.jpg new file mode 100644 index 00000000..a01b55d4 Binary files /dev/null and b/examples/data/Fruit360/mango/85_100.jpg differ diff --git a/examples/data/Fruit360/mango/86_100.jpg b/examples/data/Fruit360/mango/86_100.jpg new file mode 100644 index 00000000..abac2f6d Binary files /dev/null and b/examples/data/Fruit360/mango/86_100.jpg differ diff --git a/examples/data/Fruit360/mango/87_100.jpg b/examples/data/Fruit360/mango/87_100.jpg new file mode 100644 index 00000000..cfa43869 Binary files /dev/null and b/examples/data/Fruit360/mango/87_100.jpg differ diff --git a/examples/data/Fruit360/mango/88_100.jpg b/examples/data/Fruit360/mango/88_100.jpg new file mode 100644 index 00000000..af3a90de Binary files /dev/null and b/examples/data/Fruit360/mango/88_100.jpg differ diff --git a/examples/data/Fruit360/mango/89_100.jpg b/examples/data/Fruit360/mango/89_100.jpg new file mode 100644 index 00000000..0cb64e56 Binary files /dev/null and b/examples/data/Fruit360/mango/89_100.jpg differ diff --git a/examples/data/Fruit360/mango/90_100.jpg b/examples/data/Fruit360/mango/90_100.jpg new file mode 100644 index 00000000..bc867d7e Binary files /dev/null and b/examples/data/Fruit360/mango/90_100.jpg differ diff --git a/examples/data/Fruit360/mango/91_100.jpg b/examples/data/Fruit360/mango/91_100.jpg new file mode 100644 index 00000000..4d6b31de Binary files /dev/null and b/examples/data/Fruit360/mango/91_100.jpg differ diff --git a/examples/data/Fruit360/mango/92_100.jpg b/examples/data/Fruit360/mango/92_100.jpg new file mode 100644 index 00000000..aa9e2e8f Binary files /dev/null and b/examples/data/Fruit360/mango/92_100.jpg differ diff --git a/examples/data/Fruit360/mango/93_100.jpg b/examples/data/Fruit360/mango/93_100.jpg new file mode 100644 index 00000000..9be02b81 Binary files /dev/null and b/examples/data/Fruit360/mango/93_100.jpg differ diff --git a/examples/data/Fruit360/mango/94_100.jpg b/examples/data/Fruit360/mango/94_100.jpg new file mode 100644 index 00000000..a19dc4ff Binary files /dev/null and b/examples/data/Fruit360/mango/94_100.jpg differ diff --git a/examples/data/Fruit360/mango/95_100.jpg b/examples/data/Fruit360/mango/95_100.jpg new file mode 100644 index 00000000..42d5fbab Binary files /dev/null and b/examples/data/Fruit360/mango/95_100.jpg differ diff --git a/examples/data/Fruit360/mango/96_100.jpg b/examples/data/Fruit360/mango/96_100.jpg new file mode 100644 index 00000000..93de6f74 Binary files /dev/null and b/examples/data/Fruit360/mango/96_100.jpg differ diff --git a/examples/data/Fruit360/mango/97_100.jpg b/examples/data/Fruit360/mango/97_100.jpg new file mode 100644 index 00000000..ae58ec56 Binary files /dev/null and b/examples/data/Fruit360/mango/97_100.jpg differ diff --git a/examples/data/Fruit360/mango/98_100.jpg b/examples/data/Fruit360/mango/98_100.jpg new file mode 100644 index 00000000..5806ca00 Binary files /dev/null and b/examples/data/Fruit360/mango/98_100.jpg differ diff --git a/examples/data/Fruit360/mango/99_100.jpg b/examples/data/Fruit360/mango/99_100.jpg new file mode 100644 index 00000000..a01f17ab Binary files /dev/null and b/examples/data/Fruit360/mango/99_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_0_100.jpg b/examples/data/Fruit360/mango/r_0_100.jpg new file mode 100644 index 00000000..4557f6f1 Binary files /dev/null and b/examples/data/Fruit360/mango/r_0_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_100_100.jpg b/examples/data/Fruit360/mango/r_100_100.jpg new file mode 100644 index 00000000..a5b0018a Binary files /dev/null and b/examples/data/Fruit360/mango/r_100_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_101_100.jpg b/examples/data/Fruit360/mango/r_101_100.jpg new file mode 100644 index 00000000..181ee79b Binary files /dev/null and b/examples/data/Fruit360/mango/r_101_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_102_100.jpg b/examples/data/Fruit360/mango/r_102_100.jpg new file mode 100644 index 00000000..9323387a Binary files /dev/null and b/examples/data/Fruit360/mango/r_102_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_103_100.jpg b/examples/data/Fruit360/mango/r_103_100.jpg new file mode 100644 index 00000000..d3c217d7 Binary files /dev/null and b/examples/data/Fruit360/mango/r_103_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_104_100.jpg b/examples/data/Fruit360/mango/r_104_100.jpg new file mode 100644 index 00000000..d35875f9 Binary files /dev/null and b/examples/data/Fruit360/mango/r_104_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_105_100.jpg b/examples/data/Fruit360/mango/r_105_100.jpg new file mode 100644 index 00000000..93ba5d88 Binary files /dev/null and b/examples/data/Fruit360/mango/r_105_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_106_100.jpg b/examples/data/Fruit360/mango/r_106_100.jpg new file mode 100644 index 00000000..30e4c018 Binary files /dev/null and b/examples/data/Fruit360/mango/r_106_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_107_100.jpg b/examples/data/Fruit360/mango/r_107_100.jpg new file mode 100644 index 00000000..d26c14cf Binary files /dev/null and b/examples/data/Fruit360/mango/r_107_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_108_100.jpg b/examples/data/Fruit360/mango/r_108_100.jpg new file mode 100644 index 00000000..4d84186a Binary files /dev/null and b/examples/data/Fruit360/mango/r_108_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_109_100.jpg b/examples/data/Fruit360/mango/r_109_100.jpg new file mode 100644 index 00000000..0d5e8995 Binary files /dev/null and b/examples/data/Fruit360/mango/r_109_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_10_100.jpg b/examples/data/Fruit360/mango/r_10_100.jpg new file mode 100644 index 00000000..13d51dc0 Binary files /dev/null and b/examples/data/Fruit360/mango/r_10_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_110_100.jpg b/examples/data/Fruit360/mango/r_110_100.jpg new file mode 100644 index 00000000..6414a8f5 Binary files /dev/null and b/examples/data/Fruit360/mango/r_110_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_111_100.jpg b/examples/data/Fruit360/mango/r_111_100.jpg new file mode 100644 index 00000000..315fa8cb Binary files /dev/null and b/examples/data/Fruit360/mango/r_111_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_112_100.jpg b/examples/data/Fruit360/mango/r_112_100.jpg new file mode 100644 index 00000000..8c2fb8cc Binary files /dev/null and b/examples/data/Fruit360/mango/r_112_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_113_100.jpg b/examples/data/Fruit360/mango/r_113_100.jpg new file mode 100644 index 00000000..77d6873c Binary files /dev/null and b/examples/data/Fruit360/mango/r_113_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_114_100.jpg b/examples/data/Fruit360/mango/r_114_100.jpg new file mode 100644 index 00000000..7b36f4dd Binary files /dev/null and b/examples/data/Fruit360/mango/r_114_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_115_100.jpg b/examples/data/Fruit360/mango/r_115_100.jpg new file mode 100644 index 00000000..574939fd Binary files /dev/null and b/examples/data/Fruit360/mango/r_115_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_116_100.jpg b/examples/data/Fruit360/mango/r_116_100.jpg new file mode 100644 index 00000000..605e3a0f Binary files /dev/null and b/examples/data/Fruit360/mango/r_116_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_117_100.jpg b/examples/data/Fruit360/mango/r_117_100.jpg new file mode 100644 index 00000000..281e4d6c Binary files /dev/null and b/examples/data/Fruit360/mango/r_117_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_118_100.jpg b/examples/data/Fruit360/mango/r_118_100.jpg new file mode 100644 index 00000000..17aba309 Binary files /dev/null and b/examples/data/Fruit360/mango/r_118_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_119_100.jpg b/examples/data/Fruit360/mango/r_119_100.jpg new file mode 100644 index 00000000..2733a307 Binary files /dev/null and b/examples/data/Fruit360/mango/r_119_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_11_100.jpg b/examples/data/Fruit360/mango/r_11_100.jpg new file mode 100644 index 00000000..87245bfc Binary files /dev/null and b/examples/data/Fruit360/mango/r_11_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_120_100.jpg b/examples/data/Fruit360/mango/r_120_100.jpg new file mode 100644 index 00000000..48771bf2 Binary files /dev/null and b/examples/data/Fruit360/mango/r_120_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_121_100.jpg b/examples/data/Fruit360/mango/r_121_100.jpg new file mode 100644 index 00000000..4c1d95af Binary files /dev/null and b/examples/data/Fruit360/mango/r_121_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_122_100.jpg b/examples/data/Fruit360/mango/r_122_100.jpg new file mode 100644 index 00000000..1ae2accb Binary files /dev/null and b/examples/data/Fruit360/mango/r_122_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_123_100.jpg b/examples/data/Fruit360/mango/r_123_100.jpg new file mode 100644 index 00000000..da2da8c2 Binary files /dev/null and b/examples/data/Fruit360/mango/r_123_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_124_100.jpg b/examples/data/Fruit360/mango/r_124_100.jpg new file mode 100644 index 00000000..d7f2aa56 Binary files /dev/null and b/examples/data/Fruit360/mango/r_124_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_125_100.jpg b/examples/data/Fruit360/mango/r_125_100.jpg new file mode 100644 index 00000000..5d3f7a3c Binary files /dev/null and b/examples/data/Fruit360/mango/r_125_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_126_100.jpg b/examples/data/Fruit360/mango/r_126_100.jpg new file mode 100644 index 00000000..9440a1b1 Binary files /dev/null and b/examples/data/Fruit360/mango/r_126_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_127_100.jpg b/examples/data/Fruit360/mango/r_127_100.jpg new file mode 100644 index 00000000..d0d556d0 Binary files /dev/null and b/examples/data/Fruit360/mango/r_127_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_128_100.jpg b/examples/data/Fruit360/mango/r_128_100.jpg new file mode 100644 index 00000000..ac6e337d Binary files /dev/null and b/examples/data/Fruit360/mango/r_128_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_129_100.jpg b/examples/data/Fruit360/mango/r_129_100.jpg new file mode 100644 index 00000000..dcfa3a39 Binary files /dev/null and b/examples/data/Fruit360/mango/r_129_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_12_100.jpg b/examples/data/Fruit360/mango/r_12_100.jpg new file mode 100644 index 00000000..be3f3c65 Binary files /dev/null and b/examples/data/Fruit360/mango/r_12_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_130_100.jpg b/examples/data/Fruit360/mango/r_130_100.jpg new file mode 100644 index 00000000..f80398e9 Binary files /dev/null and b/examples/data/Fruit360/mango/r_130_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_131_100.jpg b/examples/data/Fruit360/mango/r_131_100.jpg new file mode 100644 index 00000000..9f7ce450 Binary files /dev/null and b/examples/data/Fruit360/mango/r_131_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_132_100.jpg b/examples/data/Fruit360/mango/r_132_100.jpg new file mode 100644 index 00000000..8875cbab Binary files /dev/null and b/examples/data/Fruit360/mango/r_132_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_133_100.jpg b/examples/data/Fruit360/mango/r_133_100.jpg new file mode 100644 index 00000000..8d1de0c5 Binary files /dev/null and b/examples/data/Fruit360/mango/r_133_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_134_100.jpg b/examples/data/Fruit360/mango/r_134_100.jpg new file mode 100644 index 00000000..558054ca Binary files /dev/null and b/examples/data/Fruit360/mango/r_134_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_135_100.jpg b/examples/data/Fruit360/mango/r_135_100.jpg new file mode 100644 index 00000000..10ab198f Binary files /dev/null and b/examples/data/Fruit360/mango/r_135_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_136_100.jpg b/examples/data/Fruit360/mango/r_136_100.jpg new file mode 100644 index 00000000..2b5ce368 Binary files /dev/null and b/examples/data/Fruit360/mango/r_136_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_137_100.jpg b/examples/data/Fruit360/mango/r_137_100.jpg new file mode 100644 index 00000000..676e0637 Binary files /dev/null and b/examples/data/Fruit360/mango/r_137_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_138_100.jpg b/examples/data/Fruit360/mango/r_138_100.jpg new file mode 100644 index 00000000..aab68eac Binary files /dev/null and b/examples/data/Fruit360/mango/r_138_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_139_100.jpg b/examples/data/Fruit360/mango/r_139_100.jpg new file mode 100644 index 00000000..6c293d12 Binary files /dev/null and b/examples/data/Fruit360/mango/r_139_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_140_100.jpg b/examples/data/Fruit360/mango/r_140_100.jpg new file mode 100644 index 00000000..0e9101c0 Binary files /dev/null and b/examples/data/Fruit360/mango/r_140_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_141_100.jpg b/examples/data/Fruit360/mango/r_141_100.jpg new file mode 100644 index 00000000..37c139d2 Binary files /dev/null and b/examples/data/Fruit360/mango/r_141_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_142_100.jpg b/examples/data/Fruit360/mango/r_142_100.jpg new file mode 100644 index 00000000..9f8f20b1 Binary files /dev/null and b/examples/data/Fruit360/mango/r_142_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_143_100.jpg b/examples/data/Fruit360/mango/r_143_100.jpg new file mode 100644 index 00000000..13211da1 Binary files /dev/null and b/examples/data/Fruit360/mango/r_143_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_144_100.jpg b/examples/data/Fruit360/mango/r_144_100.jpg new file mode 100644 index 00000000..13a550a2 Binary files /dev/null and b/examples/data/Fruit360/mango/r_144_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_145_100.jpg b/examples/data/Fruit360/mango/r_145_100.jpg new file mode 100644 index 00000000..cff7b1ba Binary files /dev/null and b/examples/data/Fruit360/mango/r_145_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_146_100.jpg b/examples/data/Fruit360/mango/r_146_100.jpg new file mode 100644 index 00000000..8ce461d1 Binary files /dev/null and b/examples/data/Fruit360/mango/r_146_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_147_100.jpg b/examples/data/Fruit360/mango/r_147_100.jpg new file mode 100644 index 00000000..7a0ce6b6 Binary files /dev/null and b/examples/data/Fruit360/mango/r_147_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_148_100.jpg b/examples/data/Fruit360/mango/r_148_100.jpg new file mode 100644 index 00000000..20ba8ca6 Binary files /dev/null and b/examples/data/Fruit360/mango/r_148_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_149_100.jpg b/examples/data/Fruit360/mango/r_149_100.jpg new file mode 100644 index 00000000..d1208f56 Binary files /dev/null and b/examples/data/Fruit360/mango/r_149_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_150_100.jpg b/examples/data/Fruit360/mango/r_150_100.jpg new file mode 100644 index 00000000..2867609b Binary files /dev/null and b/examples/data/Fruit360/mango/r_150_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_151_100.jpg b/examples/data/Fruit360/mango/r_151_100.jpg new file mode 100644 index 00000000..8221a3ba Binary files /dev/null and b/examples/data/Fruit360/mango/r_151_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_152_100.jpg b/examples/data/Fruit360/mango/r_152_100.jpg new file mode 100644 index 00000000..d9316222 Binary files /dev/null and b/examples/data/Fruit360/mango/r_152_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_153_100.jpg b/examples/data/Fruit360/mango/r_153_100.jpg new file mode 100644 index 00000000..2633e7b4 Binary files /dev/null and b/examples/data/Fruit360/mango/r_153_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_154_100.jpg b/examples/data/Fruit360/mango/r_154_100.jpg new file mode 100644 index 00000000..b616363c Binary files /dev/null and b/examples/data/Fruit360/mango/r_154_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_155_100.jpg b/examples/data/Fruit360/mango/r_155_100.jpg new file mode 100644 index 00000000..d57af146 Binary files /dev/null and b/examples/data/Fruit360/mango/r_155_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_156_100.jpg b/examples/data/Fruit360/mango/r_156_100.jpg new file mode 100644 index 00000000..425561e7 Binary files /dev/null and b/examples/data/Fruit360/mango/r_156_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_157_100.jpg b/examples/data/Fruit360/mango/r_157_100.jpg new file mode 100644 index 00000000..1dccd41c Binary files /dev/null and b/examples/data/Fruit360/mango/r_157_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_158_100.jpg b/examples/data/Fruit360/mango/r_158_100.jpg new file mode 100644 index 00000000..76a7e4de Binary files /dev/null and b/examples/data/Fruit360/mango/r_158_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_159_100.jpg b/examples/data/Fruit360/mango/r_159_100.jpg new file mode 100644 index 00000000..0aff8043 Binary files /dev/null and b/examples/data/Fruit360/mango/r_159_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_160_100.jpg b/examples/data/Fruit360/mango/r_160_100.jpg new file mode 100644 index 00000000..3a10f12f Binary files /dev/null and b/examples/data/Fruit360/mango/r_160_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_161_100.jpg b/examples/data/Fruit360/mango/r_161_100.jpg new file mode 100644 index 00000000..757c287d Binary files /dev/null and b/examples/data/Fruit360/mango/r_161_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_162_100.jpg b/examples/data/Fruit360/mango/r_162_100.jpg new file mode 100644 index 00000000..8c869915 Binary files /dev/null and b/examples/data/Fruit360/mango/r_162_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_163_100.jpg b/examples/data/Fruit360/mango/r_163_100.jpg new file mode 100644 index 00000000..169df135 Binary files /dev/null and b/examples/data/Fruit360/mango/r_163_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_164_100.jpg b/examples/data/Fruit360/mango/r_164_100.jpg new file mode 100644 index 00000000..9bbeadad Binary files /dev/null and b/examples/data/Fruit360/mango/r_164_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_165_100.jpg b/examples/data/Fruit360/mango/r_165_100.jpg new file mode 100644 index 00000000..4380c74a Binary files /dev/null and b/examples/data/Fruit360/mango/r_165_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_166_100.jpg b/examples/data/Fruit360/mango/r_166_100.jpg new file mode 100644 index 00000000..7f3dd579 Binary files /dev/null and b/examples/data/Fruit360/mango/r_166_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_167_100.jpg b/examples/data/Fruit360/mango/r_167_100.jpg new file mode 100644 index 00000000..7021fea8 Binary files /dev/null and b/examples/data/Fruit360/mango/r_167_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_168_100.jpg b/examples/data/Fruit360/mango/r_168_100.jpg new file mode 100644 index 00000000..c7782e66 Binary files /dev/null and b/examples/data/Fruit360/mango/r_168_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_169_100.jpg b/examples/data/Fruit360/mango/r_169_100.jpg new file mode 100644 index 00000000..0b5e84d8 Binary files /dev/null and b/examples/data/Fruit360/mango/r_169_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_16_100.jpg b/examples/data/Fruit360/mango/r_16_100.jpg new file mode 100644 index 00000000..59af5416 Binary files /dev/null and b/examples/data/Fruit360/mango/r_16_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_170_100.jpg b/examples/data/Fruit360/mango/r_170_100.jpg new file mode 100644 index 00000000..5624e7cc Binary files /dev/null and b/examples/data/Fruit360/mango/r_170_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_171_100.jpg b/examples/data/Fruit360/mango/r_171_100.jpg new file mode 100644 index 00000000..c5b7fc92 Binary files /dev/null and b/examples/data/Fruit360/mango/r_171_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_172_100.jpg b/examples/data/Fruit360/mango/r_172_100.jpg new file mode 100644 index 00000000..2a5ead53 Binary files /dev/null and b/examples/data/Fruit360/mango/r_172_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_173_100.jpg b/examples/data/Fruit360/mango/r_173_100.jpg new file mode 100644 index 00000000..7563e325 Binary files /dev/null and b/examples/data/Fruit360/mango/r_173_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_174_100.jpg b/examples/data/Fruit360/mango/r_174_100.jpg new file mode 100644 index 00000000..78c6ff7f Binary files /dev/null and b/examples/data/Fruit360/mango/r_174_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_175_100.jpg b/examples/data/Fruit360/mango/r_175_100.jpg new file mode 100644 index 00000000..10ad113b Binary files /dev/null and b/examples/data/Fruit360/mango/r_175_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_176_100.jpg b/examples/data/Fruit360/mango/r_176_100.jpg new file mode 100644 index 00000000..80e7a73b Binary files /dev/null and b/examples/data/Fruit360/mango/r_176_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_177_100.jpg b/examples/data/Fruit360/mango/r_177_100.jpg new file mode 100644 index 00000000..0f94f0ca Binary files /dev/null and b/examples/data/Fruit360/mango/r_177_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_178_100.jpg b/examples/data/Fruit360/mango/r_178_100.jpg new file mode 100644 index 00000000..a7df56f6 Binary files /dev/null and b/examples/data/Fruit360/mango/r_178_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_179_100.jpg b/examples/data/Fruit360/mango/r_179_100.jpg new file mode 100644 index 00000000..43c621f6 Binary files /dev/null and b/examples/data/Fruit360/mango/r_179_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_17_100.jpg b/examples/data/Fruit360/mango/r_17_100.jpg new file mode 100644 index 00000000..828054bc Binary files /dev/null and b/examples/data/Fruit360/mango/r_17_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_180_100.jpg b/examples/data/Fruit360/mango/r_180_100.jpg new file mode 100644 index 00000000..9238237f Binary files /dev/null and b/examples/data/Fruit360/mango/r_180_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_181_100.jpg b/examples/data/Fruit360/mango/r_181_100.jpg new file mode 100644 index 00000000..201306dd Binary files /dev/null and b/examples/data/Fruit360/mango/r_181_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_182_100.jpg b/examples/data/Fruit360/mango/r_182_100.jpg new file mode 100644 index 00000000..f864f43a Binary files /dev/null and b/examples/data/Fruit360/mango/r_182_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_183_100.jpg b/examples/data/Fruit360/mango/r_183_100.jpg new file mode 100644 index 00000000..fecced05 Binary files /dev/null and b/examples/data/Fruit360/mango/r_183_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_184_100.jpg b/examples/data/Fruit360/mango/r_184_100.jpg new file mode 100644 index 00000000..4687cff6 Binary files /dev/null and b/examples/data/Fruit360/mango/r_184_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_185_100.jpg b/examples/data/Fruit360/mango/r_185_100.jpg new file mode 100644 index 00000000..26068f85 Binary files /dev/null and b/examples/data/Fruit360/mango/r_185_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_186_100.jpg b/examples/data/Fruit360/mango/r_186_100.jpg new file mode 100644 index 00000000..aafc81e2 Binary files /dev/null and b/examples/data/Fruit360/mango/r_186_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_187_100.jpg b/examples/data/Fruit360/mango/r_187_100.jpg new file mode 100644 index 00000000..8a53bcc6 Binary files /dev/null and b/examples/data/Fruit360/mango/r_187_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_188_100.jpg b/examples/data/Fruit360/mango/r_188_100.jpg new file mode 100644 index 00000000..9b561019 Binary files /dev/null and b/examples/data/Fruit360/mango/r_188_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_189_100.jpg b/examples/data/Fruit360/mango/r_189_100.jpg new file mode 100644 index 00000000..5653cb0c Binary files /dev/null and b/examples/data/Fruit360/mango/r_189_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_18_100.jpg b/examples/data/Fruit360/mango/r_18_100.jpg new file mode 100644 index 00000000..e8322bdf Binary files /dev/null and b/examples/data/Fruit360/mango/r_18_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_190_100.jpg b/examples/data/Fruit360/mango/r_190_100.jpg new file mode 100644 index 00000000..62d69251 Binary files /dev/null and b/examples/data/Fruit360/mango/r_190_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_191_100.jpg b/examples/data/Fruit360/mango/r_191_100.jpg new file mode 100644 index 00000000..b8efb298 Binary files /dev/null and b/examples/data/Fruit360/mango/r_191_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_192_100.jpg b/examples/data/Fruit360/mango/r_192_100.jpg new file mode 100644 index 00000000..8f952f48 Binary files /dev/null and b/examples/data/Fruit360/mango/r_192_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_193_100.jpg b/examples/data/Fruit360/mango/r_193_100.jpg new file mode 100644 index 00000000..d1e16138 Binary files /dev/null and b/examples/data/Fruit360/mango/r_193_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_194_100.jpg b/examples/data/Fruit360/mango/r_194_100.jpg new file mode 100644 index 00000000..7bf9310b Binary files /dev/null and b/examples/data/Fruit360/mango/r_194_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_195_100.jpg b/examples/data/Fruit360/mango/r_195_100.jpg new file mode 100644 index 00000000..3b1a4eaf Binary files /dev/null and b/examples/data/Fruit360/mango/r_195_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_196_100.jpg b/examples/data/Fruit360/mango/r_196_100.jpg new file mode 100644 index 00000000..04025c70 Binary files /dev/null and b/examples/data/Fruit360/mango/r_196_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_197_100.jpg b/examples/data/Fruit360/mango/r_197_100.jpg new file mode 100644 index 00000000..e67f0403 Binary files /dev/null and b/examples/data/Fruit360/mango/r_197_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_198_100.jpg b/examples/data/Fruit360/mango/r_198_100.jpg new file mode 100644 index 00000000..dd25ad36 Binary files /dev/null and b/examples/data/Fruit360/mango/r_198_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_199_100.jpg b/examples/data/Fruit360/mango/r_199_100.jpg new file mode 100644 index 00000000..35810e38 Binary files /dev/null and b/examples/data/Fruit360/mango/r_199_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_19_100.jpg b/examples/data/Fruit360/mango/r_19_100.jpg new file mode 100644 index 00000000..9ec56cf5 Binary files /dev/null and b/examples/data/Fruit360/mango/r_19_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_1_100.jpg b/examples/data/Fruit360/mango/r_1_100.jpg new file mode 100644 index 00000000..413e1cf2 Binary files /dev/null and b/examples/data/Fruit360/mango/r_1_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_200_100.jpg b/examples/data/Fruit360/mango/r_200_100.jpg new file mode 100644 index 00000000..e967a8c5 Binary files /dev/null and b/examples/data/Fruit360/mango/r_200_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_201_100.jpg b/examples/data/Fruit360/mango/r_201_100.jpg new file mode 100644 index 00000000..86b0b0e7 Binary files /dev/null and b/examples/data/Fruit360/mango/r_201_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_202_100.jpg b/examples/data/Fruit360/mango/r_202_100.jpg new file mode 100644 index 00000000..de0d6af2 Binary files /dev/null and b/examples/data/Fruit360/mango/r_202_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_203_100.jpg b/examples/data/Fruit360/mango/r_203_100.jpg new file mode 100644 index 00000000..213169c7 Binary files /dev/null and b/examples/data/Fruit360/mango/r_203_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_204_100.jpg b/examples/data/Fruit360/mango/r_204_100.jpg new file mode 100644 index 00000000..26ec6ad9 Binary files /dev/null and b/examples/data/Fruit360/mango/r_204_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_205_100.jpg b/examples/data/Fruit360/mango/r_205_100.jpg new file mode 100644 index 00000000..4842a045 Binary files /dev/null and b/examples/data/Fruit360/mango/r_205_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_206_100.jpg b/examples/data/Fruit360/mango/r_206_100.jpg new file mode 100644 index 00000000..67272698 Binary files /dev/null and b/examples/data/Fruit360/mango/r_206_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_207_100.jpg b/examples/data/Fruit360/mango/r_207_100.jpg new file mode 100644 index 00000000..5dbb1b20 Binary files /dev/null and b/examples/data/Fruit360/mango/r_207_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_208_100.jpg b/examples/data/Fruit360/mango/r_208_100.jpg new file mode 100644 index 00000000..68dcd816 Binary files /dev/null and b/examples/data/Fruit360/mango/r_208_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_209_100.jpg b/examples/data/Fruit360/mango/r_209_100.jpg new file mode 100644 index 00000000..225b3508 Binary files /dev/null and b/examples/data/Fruit360/mango/r_209_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_20_100.jpg b/examples/data/Fruit360/mango/r_20_100.jpg new file mode 100644 index 00000000..8479934d Binary files /dev/null and b/examples/data/Fruit360/mango/r_20_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_21_100.jpg b/examples/data/Fruit360/mango/r_21_100.jpg new file mode 100644 index 00000000..15e32489 Binary files /dev/null and b/examples/data/Fruit360/mango/r_21_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_22_100.jpg b/examples/data/Fruit360/mango/r_22_100.jpg new file mode 100644 index 00000000..0d926dfd Binary files /dev/null and b/examples/data/Fruit360/mango/r_22_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_23_100.jpg b/examples/data/Fruit360/mango/r_23_100.jpg new file mode 100644 index 00000000..20a1e664 Binary files /dev/null and b/examples/data/Fruit360/mango/r_23_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_24_100.jpg b/examples/data/Fruit360/mango/r_24_100.jpg new file mode 100644 index 00000000..0543a5c0 Binary files /dev/null and b/examples/data/Fruit360/mango/r_24_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_267_100.jpg b/examples/data/Fruit360/mango/r_267_100.jpg new file mode 100644 index 00000000..50b72931 Binary files /dev/null and b/examples/data/Fruit360/mango/r_267_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_268_100.jpg b/examples/data/Fruit360/mango/r_268_100.jpg new file mode 100644 index 00000000..58201893 Binary files /dev/null and b/examples/data/Fruit360/mango/r_268_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_269_100.jpg b/examples/data/Fruit360/mango/r_269_100.jpg new file mode 100644 index 00000000..be3276b1 Binary files /dev/null and b/examples/data/Fruit360/mango/r_269_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_26_100.jpg b/examples/data/Fruit360/mango/r_26_100.jpg new file mode 100644 index 00000000..360f56d1 Binary files /dev/null and b/examples/data/Fruit360/mango/r_26_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_272_100.jpg b/examples/data/Fruit360/mango/r_272_100.jpg new file mode 100644 index 00000000..b616d0f0 Binary files /dev/null and b/examples/data/Fruit360/mango/r_272_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_276_100.jpg b/examples/data/Fruit360/mango/r_276_100.jpg new file mode 100644 index 00000000..a6c668ed Binary files /dev/null and b/examples/data/Fruit360/mango/r_276_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_278_100.jpg b/examples/data/Fruit360/mango/r_278_100.jpg new file mode 100644 index 00000000..1e0cd08f Binary files /dev/null and b/examples/data/Fruit360/mango/r_278_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_279_100.jpg b/examples/data/Fruit360/mango/r_279_100.jpg new file mode 100644 index 00000000..ecd3bf39 Binary files /dev/null and b/examples/data/Fruit360/mango/r_279_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_27_100.jpg b/examples/data/Fruit360/mango/r_27_100.jpg new file mode 100644 index 00000000..55a3f17c Binary files /dev/null and b/examples/data/Fruit360/mango/r_27_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_280_100.jpg b/examples/data/Fruit360/mango/r_280_100.jpg new file mode 100644 index 00000000..8b93e7ee Binary files /dev/null and b/examples/data/Fruit360/mango/r_280_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_281_100.jpg b/examples/data/Fruit360/mango/r_281_100.jpg new file mode 100644 index 00000000..dd49dd6b Binary files /dev/null and b/examples/data/Fruit360/mango/r_281_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_282_100.jpg b/examples/data/Fruit360/mango/r_282_100.jpg new file mode 100644 index 00000000..164bae90 Binary files /dev/null and b/examples/data/Fruit360/mango/r_282_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_283_100.jpg b/examples/data/Fruit360/mango/r_283_100.jpg new file mode 100644 index 00000000..27d7b765 Binary files /dev/null and b/examples/data/Fruit360/mango/r_283_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_284_100.jpg b/examples/data/Fruit360/mango/r_284_100.jpg new file mode 100644 index 00000000..04755939 Binary files /dev/null and b/examples/data/Fruit360/mango/r_284_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_285_100.jpg b/examples/data/Fruit360/mango/r_285_100.jpg new file mode 100644 index 00000000..1292d31d Binary files /dev/null and b/examples/data/Fruit360/mango/r_285_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_286_100.jpg b/examples/data/Fruit360/mango/r_286_100.jpg new file mode 100644 index 00000000..1fd58089 Binary files /dev/null and b/examples/data/Fruit360/mango/r_286_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_287_100.jpg b/examples/data/Fruit360/mango/r_287_100.jpg new file mode 100644 index 00000000..02637a71 Binary files /dev/null and b/examples/data/Fruit360/mango/r_287_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_288_100.jpg b/examples/data/Fruit360/mango/r_288_100.jpg new file mode 100644 index 00000000..13d9debb Binary files /dev/null and b/examples/data/Fruit360/mango/r_288_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_289_100.jpg b/examples/data/Fruit360/mango/r_289_100.jpg new file mode 100644 index 00000000..e55332a8 Binary files /dev/null and b/examples/data/Fruit360/mango/r_289_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_28_100.jpg b/examples/data/Fruit360/mango/r_28_100.jpg new file mode 100644 index 00000000..afc3c933 Binary files /dev/null and b/examples/data/Fruit360/mango/r_28_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_290_100.jpg b/examples/data/Fruit360/mango/r_290_100.jpg new file mode 100644 index 00000000..73599a42 Binary files /dev/null and b/examples/data/Fruit360/mango/r_290_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_291_100.jpg b/examples/data/Fruit360/mango/r_291_100.jpg new file mode 100644 index 00000000..3998d9ce Binary files /dev/null and b/examples/data/Fruit360/mango/r_291_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_292_100.jpg b/examples/data/Fruit360/mango/r_292_100.jpg new file mode 100644 index 00000000..3e062b9b Binary files /dev/null and b/examples/data/Fruit360/mango/r_292_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_293_100.jpg b/examples/data/Fruit360/mango/r_293_100.jpg new file mode 100644 index 00000000..390e0596 Binary files /dev/null and b/examples/data/Fruit360/mango/r_293_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_294_100.jpg b/examples/data/Fruit360/mango/r_294_100.jpg new file mode 100644 index 00000000..95d189f0 Binary files /dev/null and b/examples/data/Fruit360/mango/r_294_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_295_100.jpg b/examples/data/Fruit360/mango/r_295_100.jpg new file mode 100644 index 00000000..e2e903c3 Binary files /dev/null and b/examples/data/Fruit360/mango/r_295_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_296_100.jpg b/examples/data/Fruit360/mango/r_296_100.jpg new file mode 100644 index 00000000..2f691366 Binary files /dev/null and b/examples/data/Fruit360/mango/r_296_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_297_100.jpg b/examples/data/Fruit360/mango/r_297_100.jpg new file mode 100644 index 00000000..a4701241 Binary files /dev/null and b/examples/data/Fruit360/mango/r_297_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_298_100.jpg b/examples/data/Fruit360/mango/r_298_100.jpg new file mode 100644 index 00000000..ae0d8745 Binary files /dev/null and b/examples/data/Fruit360/mango/r_298_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_299_100.jpg b/examples/data/Fruit360/mango/r_299_100.jpg new file mode 100644 index 00000000..08547b6c Binary files /dev/null and b/examples/data/Fruit360/mango/r_299_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_29_100.jpg b/examples/data/Fruit360/mango/r_29_100.jpg new file mode 100644 index 00000000..7cd9b4ea Binary files /dev/null and b/examples/data/Fruit360/mango/r_29_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_2_100.jpg b/examples/data/Fruit360/mango/r_2_100.jpg new file mode 100644 index 00000000..7874d130 Binary files /dev/null and b/examples/data/Fruit360/mango/r_2_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_300_100.jpg b/examples/data/Fruit360/mango/r_300_100.jpg new file mode 100644 index 00000000..4ade6773 Binary files /dev/null and b/examples/data/Fruit360/mango/r_300_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_301_100.jpg b/examples/data/Fruit360/mango/r_301_100.jpg new file mode 100644 index 00000000..a00bfa14 Binary files /dev/null and b/examples/data/Fruit360/mango/r_301_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_302_100.jpg b/examples/data/Fruit360/mango/r_302_100.jpg new file mode 100644 index 00000000..f62d6f6e Binary files /dev/null and b/examples/data/Fruit360/mango/r_302_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_303_100.jpg b/examples/data/Fruit360/mango/r_303_100.jpg new file mode 100644 index 00000000..a279ae54 Binary files /dev/null and b/examples/data/Fruit360/mango/r_303_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_304_100.jpg b/examples/data/Fruit360/mango/r_304_100.jpg new file mode 100644 index 00000000..6bc1d1f2 Binary files /dev/null and b/examples/data/Fruit360/mango/r_304_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_305_100.jpg b/examples/data/Fruit360/mango/r_305_100.jpg new file mode 100644 index 00000000..fca1d3f1 Binary files /dev/null and b/examples/data/Fruit360/mango/r_305_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_306_100.jpg b/examples/data/Fruit360/mango/r_306_100.jpg new file mode 100644 index 00000000..f5cf8fc5 Binary files /dev/null and b/examples/data/Fruit360/mango/r_306_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_307_100.jpg b/examples/data/Fruit360/mango/r_307_100.jpg new file mode 100644 index 00000000..6da0622d Binary files /dev/null and b/examples/data/Fruit360/mango/r_307_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_308_100.jpg b/examples/data/Fruit360/mango/r_308_100.jpg new file mode 100644 index 00000000..9d76bd0b Binary files /dev/null and b/examples/data/Fruit360/mango/r_308_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_309_100.jpg b/examples/data/Fruit360/mango/r_309_100.jpg new file mode 100644 index 00000000..1cb6974a Binary files /dev/null and b/examples/data/Fruit360/mango/r_309_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_30_100.jpg b/examples/data/Fruit360/mango/r_30_100.jpg new file mode 100644 index 00000000..6959061e Binary files /dev/null and b/examples/data/Fruit360/mango/r_30_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_310_100.jpg b/examples/data/Fruit360/mango/r_310_100.jpg new file mode 100644 index 00000000..41509e5d Binary files /dev/null and b/examples/data/Fruit360/mango/r_310_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_311_100.jpg b/examples/data/Fruit360/mango/r_311_100.jpg new file mode 100644 index 00000000..6646f419 Binary files /dev/null and b/examples/data/Fruit360/mango/r_311_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_312_100.jpg b/examples/data/Fruit360/mango/r_312_100.jpg new file mode 100644 index 00000000..e6626e29 Binary files /dev/null and b/examples/data/Fruit360/mango/r_312_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_313_100.jpg b/examples/data/Fruit360/mango/r_313_100.jpg new file mode 100644 index 00000000..9447516b Binary files /dev/null and b/examples/data/Fruit360/mango/r_313_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_314_100.jpg b/examples/data/Fruit360/mango/r_314_100.jpg new file mode 100644 index 00000000..03800511 Binary files /dev/null and b/examples/data/Fruit360/mango/r_314_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_315_100.jpg b/examples/data/Fruit360/mango/r_315_100.jpg new file mode 100644 index 00000000..250407d0 Binary files /dev/null and b/examples/data/Fruit360/mango/r_315_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_316_100.jpg b/examples/data/Fruit360/mango/r_316_100.jpg new file mode 100644 index 00000000..55113a08 Binary files /dev/null and b/examples/data/Fruit360/mango/r_316_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_317_100.jpg b/examples/data/Fruit360/mango/r_317_100.jpg new file mode 100644 index 00000000..b9a6c422 Binary files /dev/null and b/examples/data/Fruit360/mango/r_317_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_318_100.jpg b/examples/data/Fruit360/mango/r_318_100.jpg new file mode 100644 index 00000000..71f97f5c Binary files /dev/null and b/examples/data/Fruit360/mango/r_318_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_319_100.jpg b/examples/data/Fruit360/mango/r_319_100.jpg new file mode 100644 index 00000000..e4c8fd71 Binary files /dev/null and b/examples/data/Fruit360/mango/r_319_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_31_100.jpg b/examples/data/Fruit360/mango/r_31_100.jpg new file mode 100644 index 00000000..ef550a51 Binary files /dev/null and b/examples/data/Fruit360/mango/r_31_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_320_100.jpg b/examples/data/Fruit360/mango/r_320_100.jpg new file mode 100644 index 00000000..e85e132f Binary files /dev/null and b/examples/data/Fruit360/mango/r_320_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_324_100.jpg b/examples/data/Fruit360/mango/r_324_100.jpg new file mode 100644 index 00000000..7b6cdaee Binary files /dev/null and b/examples/data/Fruit360/mango/r_324_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_325_100.jpg b/examples/data/Fruit360/mango/r_325_100.jpg new file mode 100644 index 00000000..2fa71ff2 Binary files /dev/null and b/examples/data/Fruit360/mango/r_325_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_326_100.jpg b/examples/data/Fruit360/mango/r_326_100.jpg new file mode 100644 index 00000000..cd7a0dc2 Binary files /dev/null and b/examples/data/Fruit360/mango/r_326_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_327_100.jpg b/examples/data/Fruit360/mango/r_327_100.jpg new file mode 100644 index 00000000..b383a04e Binary files /dev/null and b/examples/data/Fruit360/mango/r_327_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_32_100.jpg b/examples/data/Fruit360/mango/r_32_100.jpg new file mode 100644 index 00000000..8dd6360c Binary files /dev/null and b/examples/data/Fruit360/mango/r_32_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_33_100.jpg b/examples/data/Fruit360/mango/r_33_100.jpg new file mode 100644 index 00000000..dade8d72 Binary files /dev/null and b/examples/data/Fruit360/mango/r_33_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_34_100.jpg b/examples/data/Fruit360/mango/r_34_100.jpg new file mode 100644 index 00000000..9bfbb7e6 Binary files /dev/null and b/examples/data/Fruit360/mango/r_34_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_35_100.jpg b/examples/data/Fruit360/mango/r_35_100.jpg new file mode 100644 index 00000000..67f880b3 Binary files /dev/null and b/examples/data/Fruit360/mango/r_35_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_36_100.jpg b/examples/data/Fruit360/mango/r_36_100.jpg new file mode 100644 index 00000000..4a970e16 Binary files /dev/null and b/examples/data/Fruit360/mango/r_36_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_37_100.jpg b/examples/data/Fruit360/mango/r_37_100.jpg new file mode 100644 index 00000000..f12e3f2f Binary files /dev/null and b/examples/data/Fruit360/mango/r_37_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_38_100.jpg b/examples/data/Fruit360/mango/r_38_100.jpg new file mode 100644 index 00000000..acbded20 Binary files /dev/null and b/examples/data/Fruit360/mango/r_38_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_39_100.jpg b/examples/data/Fruit360/mango/r_39_100.jpg new file mode 100644 index 00000000..ce528225 Binary files /dev/null and b/examples/data/Fruit360/mango/r_39_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_3_100.jpg b/examples/data/Fruit360/mango/r_3_100.jpg new file mode 100644 index 00000000..5fe57d22 Binary files /dev/null and b/examples/data/Fruit360/mango/r_3_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_40_100.jpg b/examples/data/Fruit360/mango/r_40_100.jpg new file mode 100644 index 00000000..9488c1ff Binary files /dev/null and b/examples/data/Fruit360/mango/r_40_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_41_100.jpg b/examples/data/Fruit360/mango/r_41_100.jpg new file mode 100644 index 00000000..b632ba1a Binary files /dev/null and b/examples/data/Fruit360/mango/r_41_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_42_100.jpg b/examples/data/Fruit360/mango/r_42_100.jpg new file mode 100644 index 00000000..350499aa Binary files /dev/null and b/examples/data/Fruit360/mango/r_42_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_43_100.jpg b/examples/data/Fruit360/mango/r_43_100.jpg new file mode 100644 index 00000000..a703d335 Binary files /dev/null and b/examples/data/Fruit360/mango/r_43_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_44_100.jpg b/examples/data/Fruit360/mango/r_44_100.jpg new file mode 100644 index 00000000..45177b49 Binary files /dev/null and b/examples/data/Fruit360/mango/r_44_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_45_100.jpg b/examples/data/Fruit360/mango/r_45_100.jpg new file mode 100644 index 00000000..4c15f2c6 Binary files /dev/null and b/examples/data/Fruit360/mango/r_45_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_46_100.jpg b/examples/data/Fruit360/mango/r_46_100.jpg new file mode 100644 index 00000000..4429d53b Binary files /dev/null and b/examples/data/Fruit360/mango/r_46_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_47_100.jpg b/examples/data/Fruit360/mango/r_47_100.jpg new file mode 100644 index 00000000..a039603f Binary files /dev/null and b/examples/data/Fruit360/mango/r_47_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_48_100.jpg b/examples/data/Fruit360/mango/r_48_100.jpg new file mode 100644 index 00000000..c68008d5 Binary files /dev/null and b/examples/data/Fruit360/mango/r_48_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_49_100.jpg b/examples/data/Fruit360/mango/r_49_100.jpg new file mode 100644 index 00000000..cab2bf21 Binary files /dev/null and b/examples/data/Fruit360/mango/r_49_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_4_100.jpg b/examples/data/Fruit360/mango/r_4_100.jpg new file mode 100644 index 00000000..508cb0ad Binary files /dev/null and b/examples/data/Fruit360/mango/r_4_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_50_100.jpg b/examples/data/Fruit360/mango/r_50_100.jpg new file mode 100644 index 00000000..df24d483 Binary files /dev/null and b/examples/data/Fruit360/mango/r_50_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_51_100.jpg b/examples/data/Fruit360/mango/r_51_100.jpg new file mode 100644 index 00000000..2487b8f2 Binary files /dev/null and b/examples/data/Fruit360/mango/r_51_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_52_100.jpg b/examples/data/Fruit360/mango/r_52_100.jpg new file mode 100644 index 00000000..423e37f8 Binary files /dev/null and b/examples/data/Fruit360/mango/r_52_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_53_100.jpg b/examples/data/Fruit360/mango/r_53_100.jpg new file mode 100644 index 00000000..3f95268c Binary files /dev/null and b/examples/data/Fruit360/mango/r_53_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_54_100.jpg b/examples/data/Fruit360/mango/r_54_100.jpg new file mode 100644 index 00000000..a967852b Binary files /dev/null and b/examples/data/Fruit360/mango/r_54_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_55_100.jpg b/examples/data/Fruit360/mango/r_55_100.jpg new file mode 100644 index 00000000..869d3093 Binary files /dev/null and b/examples/data/Fruit360/mango/r_55_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_56_100.jpg b/examples/data/Fruit360/mango/r_56_100.jpg new file mode 100644 index 00000000..4a4e3630 Binary files /dev/null and b/examples/data/Fruit360/mango/r_56_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_57_100.jpg b/examples/data/Fruit360/mango/r_57_100.jpg new file mode 100644 index 00000000..5044018c Binary files /dev/null and b/examples/data/Fruit360/mango/r_57_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_58_100.jpg b/examples/data/Fruit360/mango/r_58_100.jpg new file mode 100644 index 00000000..e6f07ebb Binary files /dev/null and b/examples/data/Fruit360/mango/r_58_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_59_100.jpg b/examples/data/Fruit360/mango/r_59_100.jpg new file mode 100644 index 00000000..4141761d Binary files /dev/null and b/examples/data/Fruit360/mango/r_59_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_5_100.jpg b/examples/data/Fruit360/mango/r_5_100.jpg new file mode 100644 index 00000000..f57c75db Binary files /dev/null and b/examples/data/Fruit360/mango/r_5_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_60_100.jpg b/examples/data/Fruit360/mango/r_60_100.jpg new file mode 100644 index 00000000..3c736dd2 Binary files /dev/null and b/examples/data/Fruit360/mango/r_60_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_61_100.jpg b/examples/data/Fruit360/mango/r_61_100.jpg new file mode 100644 index 00000000..6b93037f Binary files /dev/null and b/examples/data/Fruit360/mango/r_61_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_62_100.jpg b/examples/data/Fruit360/mango/r_62_100.jpg new file mode 100644 index 00000000..b38b0910 Binary files /dev/null and b/examples/data/Fruit360/mango/r_62_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_63_100.jpg b/examples/data/Fruit360/mango/r_63_100.jpg new file mode 100644 index 00000000..54cce8f5 Binary files /dev/null and b/examples/data/Fruit360/mango/r_63_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_64_100.jpg b/examples/data/Fruit360/mango/r_64_100.jpg new file mode 100644 index 00000000..1287f643 Binary files /dev/null and b/examples/data/Fruit360/mango/r_64_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_65_100.jpg b/examples/data/Fruit360/mango/r_65_100.jpg new file mode 100644 index 00000000..6a4d2112 Binary files /dev/null and b/examples/data/Fruit360/mango/r_65_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_66_100.jpg b/examples/data/Fruit360/mango/r_66_100.jpg new file mode 100644 index 00000000..5cd61139 Binary files /dev/null and b/examples/data/Fruit360/mango/r_66_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_67_100.jpg b/examples/data/Fruit360/mango/r_67_100.jpg new file mode 100644 index 00000000..7e780e7e Binary files /dev/null and b/examples/data/Fruit360/mango/r_67_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_68_100.jpg b/examples/data/Fruit360/mango/r_68_100.jpg new file mode 100644 index 00000000..33268a61 Binary files /dev/null and b/examples/data/Fruit360/mango/r_68_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_69_100.jpg b/examples/data/Fruit360/mango/r_69_100.jpg new file mode 100644 index 00000000..e8531899 Binary files /dev/null and b/examples/data/Fruit360/mango/r_69_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_6_100.jpg b/examples/data/Fruit360/mango/r_6_100.jpg new file mode 100644 index 00000000..dce98ac9 Binary files /dev/null and b/examples/data/Fruit360/mango/r_6_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_70_100.jpg b/examples/data/Fruit360/mango/r_70_100.jpg new file mode 100644 index 00000000..261939f6 Binary files /dev/null and b/examples/data/Fruit360/mango/r_70_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_71_100.jpg b/examples/data/Fruit360/mango/r_71_100.jpg new file mode 100644 index 00000000..97689dbb Binary files /dev/null and b/examples/data/Fruit360/mango/r_71_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_72_100.jpg b/examples/data/Fruit360/mango/r_72_100.jpg new file mode 100644 index 00000000..bd53d580 Binary files /dev/null and b/examples/data/Fruit360/mango/r_72_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_73_100.jpg b/examples/data/Fruit360/mango/r_73_100.jpg new file mode 100644 index 00000000..3b377327 Binary files /dev/null and b/examples/data/Fruit360/mango/r_73_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_74_100.jpg b/examples/data/Fruit360/mango/r_74_100.jpg new file mode 100644 index 00000000..a44878fc Binary files /dev/null and b/examples/data/Fruit360/mango/r_74_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_75_100.jpg b/examples/data/Fruit360/mango/r_75_100.jpg new file mode 100644 index 00000000..11530f01 Binary files /dev/null and b/examples/data/Fruit360/mango/r_75_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_76_100.jpg b/examples/data/Fruit360/mango/r_76_100.jpg new file mode 100644 index 00000000..3d8bf195 Binary files /dev/null and b/examples/data/Fruit360/mango/r_76_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_77_100.jpg b/examples/data/Fruit360/mango/r_77_100.jpg new file mode 100644 index 00000000..c9b29f83 Binary files /dev/null and b/examples/data/Fruit360/mango/r_77_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_7_100.jpg b/examples/data/Fruit360/mango/r_7_100.jpg new file mode 100644 index 00000000..646b5dad Binary files /dev/null and b/examples/data/Fruit360/mango/r_7_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_80_100.jpg b/examples/data/Fruit360/mango/r_80_100.jpg new file mode 100644 index 00000000..ea71a9f5 Binary files /dev/null and b/examples/data/Fruit360/mango/r_80_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_8_100.jpg b/examples/data/Fruit360/mango/r_8_100.jpg new file mode 100644 index 00000000..fd37b61d Binary files /dev/null and b/examples/data/Fruit360/mango/r_8_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_92_100.jpg b/examples/data/Fruit360/mango/r_92_100.jpg new file mode 100644 index 00000000..b229a6bb Binary files /dev/null and b/examples/data/Fruit360/mango/r_92_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_93_100.jpg b/examples/data/Fruit360/mango/r_93_100.jpg new file mode 100644 index 00000000..ee895cfd Binary files /dev/null and b/examples/data/Fruit360/mango/r_93_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_94_100.jpg b/examples/data/Fruit360/mango/r_94_100.jpg new file mode 100644 index 00000000..95b29e16 Binary files /dev/null and b/examples/data/Fruit360/mango/r_94_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_95_100.jpg b/examples/data/Fruit360/mango/r_95_100.jpg new file mode 100644 index 00000000..ddc65535 Binary files /dev/null and b/examples/data/Fruit360/mango/r_95_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_96_100.jpg b/examples/data/Fruit360/mango/r_96_100.jpg new file mode 100644 index 00000000..82f66c5b Binary files /dev/null and b/examples/data/Fruit360/mango/r_96_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_97_100.jpg b/examples/data/Fruit360/mango/r_97_100.jpg new file mode 100644 index 00000000..6b679de3 Binary files /dev/null and b/examples/data/Fruit360/mango/r_97_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_98_100.jpg b/examples/data/Fruit360/mango/r_98_100.jpg new file mode 100644 index 00000000..c00ec0bb Binary files /dev/null and b/examples/data/Fruit360/mango/r_98_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_99_100.jpg b/examples/data/Fruit360/mango/r_99_100.jpg new file mode 100644 index 00000000..049f24b7 Binary files /dev/null and b/examples/data/Fruit360/mango/r_99_100.jpg differ diff --git a/examples/data/Fruit360/mango/r_9_100.jpg b/examples/data/Fruit360/mango/r_9_100.jpg new file mode 100644 index 00000000..6019a5a4 Binary files /dev/null and b/examples/data/Fruit360/mango/r_9_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/0_100.jpg b/examples/data/Fruit360/raspberry/0_100.jpg new file mode 100644 index 00000000..e968a0df Binary files /dev/null and b/examples/data/Fruit360/raspberry/0_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/10_100.jpg b/examples/data/Fruit360/raspberry/10_100.jpg new file mode 100644 index 00000000..0c3716f0 Binary files /dev/null and b/examples/data/Fruit360/raspberry/10_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/113_100.jpg b/examples/data/Fruit360/raspberry/113_100.jpg new file mode 100644 index 00000000..a108cd62 Binary files /dev/null and b/examples/data/Fruit360/raspberry/113_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/114_100.jpg b/examples/data/Fruit360/raspberry/114_100.jpg new file mode 100644 index 00000000..6fae2ea1 Binary files /dev/null and b/examples/data/Fruit360/raspberry/114_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/115_100.jpg b/examples/data/Fruit360/raspberry/115_100.jpg new file mode 100644 index 00000000..2a08c89e Binary files /dev/null and b/examples/data/Fruit360/raspberry/115_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/116_100.jpg b/examples/data/Fruit360/raspberry/116_100.jpg new file mode 100644 index 00000000..028ca1be Binary files /dev/null and b/examples/data/Fruit360/raspberry/116_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/117_100.jpg b/examples/data/Fruit360/raspberry/117_100.jpg new file mode 100644 index 00000000..a2b0af17 Binary files /dev/null and b/examples/data/Fruit360/raspberry/117_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/118_100.jpg b/examples/data/Fruit360/raspberry/118_100.jpg new file mode 100644 index 00000000..e23fc7f7 Binary files /dev/null and b/examples/data/Fruit360/raspberry/118_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/119_100.jpg b/examples/data/Fruit360/raspberry/119_100.jpg new file mode 100644 index 00000000..e2256829 Binary files /dev/null and b/examples/data/Fruit360/raspberry/119_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/11_100.jpg b/examples/data/Fruit360/raspberry/11_100.jpg new file mode 100644 index 00000000..3ab8587a Binary files /dev/null and b/examples/data/Fruit360/raspberry/11_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/120_100.jpg b/examples/data/Fruit360/raspberry/120_100.jpg new file mode 100644 index 00000000..d4f15496 Binary files /dev/null and b/examples/data/Fruit360/raspberry/120_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/121_100.jpg b/examples/data/Fruit360/raspberry/121_100.jpg new file mode 100644 index 00000000..4fc8190e Binary files /dev/null and b/examples/data/Fruit360/raspberry/121_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/122_100.jpg b/examples/data/Fruit360/raspberry/122_100.jpg new file mode 100644 index 00000000..711a7f01 Binary files /dev/null and b/examples/data/Fruit360/raspberry/122_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/123_100.jpg b/examples/data/Fruit360/raspberry/123_100.jpg new file mode 100644 index 00000000..c71fd7dc Binary files /dev/null and b/examples/data/Fruit360/raspberry/123_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/124_100.jpg b/examples/data/Fruit360/raspberry/124_100.jpg new file mode 100644 index 00000000..6eba7332 Binary files /dev/null and b/examples/data/Fruit360/raspberry/124_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/125_100.jpg b/examples/data/Fruit360/raspberry/125_100.jpg new file mode 100644 index 00000000..4b9dd9a5 Binary files /dev/null and b/examples/data/Fruit360/raspberry/125_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/126_100.jpg b/examples/data/Fruit360/raspberry/126_100.jpg new file mode 100644 index 00000000..39b67649 Binary files /dev/null and b/examples/data/Fruit360/raspberry/126_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/127_100.jpg b/examples/data/Fruit360/raspberry/127_100.jpg new file mode 100644 index 00000000..bf921f72 Binary files /dev/null and b/examples/data/Fruit360/raspberry/127_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/128_100.jpg b/examples/data/Fruit360/raspberry/128_100.jpg new file mode 100644 index 00000000..e410e758 Binary files /dev/null and b/examples/data/Fruit360/raspberry/128_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/129_100.jpg b/examples/data/Fruit360/raspberry/129_100.jpg new file mode 100644 index 00000000..1224e8f5 Binary files /dev/null and b/examples/data/Fruit360/raspberry/129_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/12_100.jpg b/examples/data/Fruit360/raspberry/12_100.jpg new file mode 100644 index 00000000..5949993f Binary files /dev/null and b/examples/data/Fruit360/raspberry/12_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/130_100.jpg b/examples/data/Fruit360/raspberry/130_100.jpg new file mode 100644 index 00000000..ee868805 Binary files /dev/null and b/examples/data/Fruit360/raspberry/130_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/131_100.jpg b/examples/data/Fruit360/raspberry/131_100.jpg new file mode 100644 index 00000000..eda63ef5 Binary files /dev/null and b/examples/data/Fruit360/raspberry/131_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/132_100.jpg b/examples/data/Fruit360/raspberry/132_100.jpg new file mode 100644 index 00000000..d333fb4b Binary files /dev/null and b/examples/data/Fruit360/raspberry/132_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/133_100.jpg b/examples/data/Fruit360/raspberry/133_100.jpg new file mode 100644 index 00000000..f5f611e2 Binary files /dev/null and b/examples/data/Fruit360/raspberry/133_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/134_100.jpg b/examples/data/Fruit360/raspberry/134_100.jpg new file mode 100644 index 00000000..08654d6c Binary files /dev/null and b/examples/data/Fruit360/raspberry/134_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/135_100.jpg b/examples/data/Fruit360/raspberry/135_100.jpg new file mode 100644 index 00000000..f2d4c20e Binary files /dev/null and b/examples/data/Fruit360/raspberry/135_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/136_100.jpg b/examples/data/Fruit360/raspberry/136_100.jpg new file mode 100644 index 00000000..72b76588 Binary files /dev/null and b/examples/data/Fruit360/raspberry/136_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/137_100.jpg b/examples/data/Fruit360/raspberry/137_100.jpg new file mode 100644 index 00000000..65c8e601 Binary files /dev/null and b/examples/data/Fruit360/raspberry/137_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/138_100.jpg b/examples/data/Fruit360/raspberry/138_100.jpg new file mode 100644 index 00000000..a51bfe39 Binary files /dev/null and b/examples/data/Fruit360/raspberry/138_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/139_100.jpg b/examples/data/Fruit360/raspberry/139_100.jpg new file mode 100644 index 00000000..bc22ceee Binary files /dev/null and b/examples/data/Fruit360/raspberry/139_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/13_100.jpg b/examples/data/Fruit360/raspberry/13_100.jpg new file mode 100644 index 00000000..cdab1b53 Binary files /dev/null and b/examples/data/Fruit360/raspberry/13_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/140_100.jpg b/examples/data/Fruit360/raspberry/140_100.jpg new file mode 100644 index 00000000..0dad6193 Binary files /dev/null and b/examples/data/Fruit360/raspberry/140_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/141_100.jpg b/examples/data/Fruit360/raspberry/141_100.jpg new file mode 100644 index 00000000..abde19b1 Binary files /dev/null and b/examples/data/Fruit360/raspberry/141_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/142_100.jpg b/examples/data/Fruit360/raspberry/142_100.jpg new file mode 100644 index 00000000..d76a2a05 Binary files /dev/null and b/examples/data/Fruit360/raspberry/142_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/143_100.jpg b/examples/data/Fruit360/raspberry/143_100.jpg new file mode 100644 index 00000000..aa89e8b6 Binary files /dev/null and b/examples/data/Fruit360/raspberry/143_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/144_100.jpg b/examples/data/Fruit360/raspberry/144_100.jpg new file mode 100644 index 00000000..6a1f4474 Binary files /dev/null and b/examples/data/Fruit360/raspberry/144_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/145_100.jpg b/examples/data/Fruit360/raspberry/145_100.jpg new file mode 100644 index 00000000..b3c53946 Binary files /dev/null and b/examples/data/Fruit360/raspberry/145_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/146_100.jpg b/examples/data/Fruit360/raspberry/146_100.jpg new file mode 100644 index 00000000..f24511a1 Binary files /dev/null and b/examples/data/Fruit360/raspberry/146_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/147_100.jpg b/examples/data/Fruit360/raspberry/147_100.jpg new file mode 100644 index 00000000..83d89aac Binary files /dev/null and b/examples/data/Fruit360/raspberry/147_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/148_100.jpg b/examples/data/Fruit360/raspberry/148_100.jpg new file mode 100644 index 00000000..96a9668a Binary files /dev/null and b/examples/data/Fruit360/raspberry/148_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/149_100.jpg b/examples/data/Fruit360/raspberry/149_100.jpg new file mode 100644 index 00000000..228c4155 Binary files /dev/null and b/examples/data/Fruit360/raspberry/149_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/14_100.jpg b/examples/data/Fruit360/raspberry/14_100.jpg new file mode 100644 index 00000000..48662fa9 Binary files /dev/null and b/examples/data/Fruit360/raspberry/14_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/150_100.jpg b/examples/data/Fruit360/raspberry/150_100.jpg new file mode 100644 index 00000000..ead82c55 Binary files /dev/null and b/examples/data/Fruit360/raspberry/150_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/151_100.jpg b/examples/data/Fruit360/raspberry/151_100.jpg new file mode 100644 index 00000000..da24e075 Binary files /dev/null and b/examples/data/Fruit360/raspberry/151_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/152_100.jpg b/examples/data/Fruit360/raspberry/152_100.jpg new file mode 100644 index 00000000..2527a8db Binary files /dev/null and b/examples/data/Fruit360/raspberry/152_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/153_100.jpg b/examples/data/Fruit360/raspberry/153_100.jpg new file mode 100644 index 00000000..6722ff2e Binary files /dev/null and b/examples/data/Fruit360/raspberry/153_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/154_100.jpg b/examples/data/Fruit360/raspberry/154_100.jpg new file mode 100644 index 00000000..9f548503 Binary files /dev/null and b/examples/data/Fruit360/raspberry/154_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/155_100.jpg b/examples/data/Fruit360/raspberry/155_100.jpg new file mode 100644 index 00000000..466b5ff0 Binary files /dev/null and b/examples/data/Fruit360/raspberry/155_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/156_100.jpg b/examples/data/Fruit360/raspberry/156_100.jpg new file mode 100644 index 00000000..9f1a6520 Binary files /dev/null and b/examples/data/Fruit360/raspberry/156_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/157_100.jpg b/examples/data/Fruit360/raspberry/157_100.jpg new file mode 100644 index 00000000..7b73df94 Binary files /dev/null and b/examples/data/Fruit360/raspberry/157_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/158_100.jpg b/examples/data/Fruit360/raspberry/158_100.jpg new file mode 100644 index 00000000..1e2d80d7 Binary files /dev/null and b/examples/data/Fruit360/raspberry/158_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/159_100.jpg b/examples/data/Fruit360/raspberry/159_100.jpg new file mode 100644 index 00000000..cf27e347 Binary files /dev/null and b/examples/data/Fruit360/raspberry/159_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/15_100.jpg b/examples/data/Fruit360/raspberry/15_100.jpg new file mode 100644 index 00000000..4093e59e Binary files /dev/null and b/examples/data/Fruit360/raspberry/15_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/160_100.jpg b/examples/data/Fruit360/raspberry/160_100.jpg new file mode 100644 index 00000000..d97f2cf2 Binary files /dev/null and b/examples/data/Fruit360/raspberry/160_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/161_100.jpg b/examples/data/Fruit360/raspberry/161_100.jpg new file mode 100644 index 00000000..ade761b7 Binary files /dev/null and b/examples/data/Fruit360/raspberry/161_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/162_100.jpg b/examples/data/Fruit360/raspberry/162_100.jpg new file mode 100644 index 00000000..51ab281d Binary files /dev/null and b/examples/data/Fruit360/raspberry/162_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/163_100.jpg b/examples/data/Fruit360/raspberry/163_100.jpg new file mode 100644 index 00000000..17e88ed2 Binary files /dev/null and b/examples/data/Fruit360/raspberry/163_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/164_100.jpg b/examples/data/Fruit360/raspberry/164_100.jpg new file mode 100644 index 00000000..94dfa6ba Binary files /dev/null and b/examples/data/Fruit360/raspberry/164_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/165_100.jpg b/examples/data/Fruit360/raspberry/165_100.jpg new file mode 100644 index 00000000..6c02e0fb Binary files /dev/null and b/examples/data/Fruit360/raspberry/165_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/166_100.jpg b/examples/data/Fruit360/raspberry/166_100.jpg new file mode 100644 index 00000000..a93b19ae Binary files /dev/null and b/examples/data/Fruit360/raspberry/166_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/167_100.jpg b/examples/data/Fruit360/raspberry/167_100.jpg new file mode 100644 index 00000000..d1b7b36e Binary files /dev/null and b/examples/data/Fruit360/raspberry/167_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/168_100.jpg b/examples/data/Fruit360/raspberry/168_100.jpg new file mode 100644 index 00000000..390dd1e1 Binary files /dev/null and b/examples/data/Fruit360/raspberry/168_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/169_100.jpg b/examples/data/Fruit360/raspberry/169_100.jpg new file mode 100644 index 00000000..5a8fcda2 Binary files /dev/null and b/examples/data/Fruit360/raspberry/169_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/16_100.jpg b/examples/data/Fruit360/raspberry/16_100.jpg new file mode 100644 index 00000000..7aecafaf Binary files /dev/null and b/examples/data/Fruit360/raspberry/16_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/170_100.jpg b/examples/data/Fruit360/raspberry/170_100.jpg new file mode 100644 index 00000000..f84a52c0 Binary files /dev/null and b/examples/data/Fruit360/raspberry/170_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/171_100.jpg b/examples/data/Fruit360/raspberry/171_100.jpg new file mode 100644 index 00000000..9d008a62 Binary files /dev/null and b/examples/data/Fruit360/raspberry/171_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/172_100.jpg b/examples/data/Fruit360/raspberry/172_100.jpg new file mode 100644 index 00000000..138b6d1d Binary files /dev/null and b/examples/data/Fruit360/raspberry/172_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/173_100.jpg b/examples/data/Fruit360/raspberry/173_100.jpg new file mode 100644 index 00000000..dd0b43ef Binary files /dev/null and b/examples/data/Fruit360/raspberry/173_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/174_100.jpg b/examples/data/Fruit360/raspberry/174_100.jpg new file mode 100644 index 00000000..f4ddcd67 Binary files /dev/null and b/examples/data/Fruit360/raspberry/174_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/175_100.jpg b/examples/data/Fruit360/raspberry/175_100.jpg new file mode 100644 index 00000000..a8395944 Binary files /dev/null and b/examples/data/Fruit360/raspberry/175_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/176_100.jpg b/examples/data/Fruit360/raspberry/176_100.jpg new file mode 100644 index 00000000..e26074f6 Binary files /dev/null and b/examples/data/Fruit360/raspberry/176_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/177_100.jpg b/examples/data/Fruit360/raspberry/177_100.jpg new file mode 100644 index 00000000..3b7a78cd Binary files /dev/null and b/examples/data/Fruit360/raspberry/177_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/178_100.jpg b/examples/data/Fruit360/raspberry/178_100.jpg new file mode 100644 index 00000000..930c7918 Binary files /dev/null and b/examples/data/Fruit360/raspberry/178_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/179_100.jpg b/examples/data/Fruit360/raspberry/179_100.jpg new file mode 100644 index 00000000..efb4a10f Binary files /dev/null and b/examples/data/Fruit360/raspberry/179_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/17_100.jpg b/examples/data/Fruit360/raspberry/17_100.jpg new file mode 100644 index 00000000..c10db47e Binary files /dev/null and b/examples/data/Fruit360/raspberry/17_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/180_100.jpg b/examples/data/Fruit360/raspberry/180_100.jpg new file mode 100644 index 00000000..738e5734 Binary files /dev/null and b/examples/data/Fruit360/raspberry/180_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/181_100.jpg b/examples/data/Fruit360/raspberry/181_100.jpg new file mode 100644 index 00000000..8901ebb1 Binary files /dev/null and b/examples/data/Fruit360/raspberry/181_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/182_100.jpg b/examples/data/Fruit360/raspberry/182_100.jpg new file mode 100644 index 00000000..04b551ef Binary files /dev/null and b/examples/data/Fruit360/raspberry/182_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/183_100.jpg b/examples/data/Fruit360/raspberry/183_100.jpg new file mode 100644 index 00000000..ec06ee96 Binary files /dev/null and b/examples/data/Fruit360/raspberry/183_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/184_100.jpg b/examples/data/Fruit360/raspberry/184_100.jpg new file mode 100644 index 00000000..e9ad1318 Binary files /dev/null and b/examples/data/Fruit360/raspberry/184_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/185_100.jpg b/examples/data/Fruit360/raspberry/185_100.jpg new file mode 100644 index 00000000..e0c4de28 Binary files /dev/null and b/examples/data/Fruit360/raspberry/185_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/186_100.jpg b/examples/data/Fruit360/raspberry/186_100.jpg new file mode 100644 index 00000000..97f534f8 Binary files /dev/null and b/examples/data/Fruit360/raspberry/186_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/187_100.jpg b/examples/data/Fruit360/raspberry/187_100.jpg new file mode 100644 index 00000000..e7a59f7d Binary files /dev/null and b/examples/data/Fruit360/raspberry/187_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/188_100.jpg b/examples/data/Fruit360/raspberry/188_100.jpg new file mode 100644 index 00000000..23dcbe5f Binary files /dev/null and b/examples/data/Fruit360/raspberry/188_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/189_100.jpg b/examples/data/Fruit360/raspberry/189_100.jpg new file mode 100644 index 00000000..e71ff3be Binary files /dev/null and b/examples/data/Fruit360/raspberry/189_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/18_100.jpg b/examples/data/Fruit360/raspberry/18_100.jpg new file mode 100644 index 00000000..213f9cdc Binary files /dev/null and b/examples/data/Fruit360/raspberry/18_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/190_100.jpg b/examples/data/Fruit360/raspberry/190_100.jpg new file mode 100644 index 00000000..b8a7a971 Binary files /dev/null and b/examples/data/Fruit360/raspberry/190_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/191_100.jpg b/examples/data/Fruit360/raspberry/191_100.jpg new file mode 100644 index 00000000..c21a65ed Binary files /dev/null and b/examples/data/Fruit360/raspberry/191_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/192_100.jpg b/examples/data/Fruit360/raspberry/192_100.jpg new file mode 100644 index 00000000..42f72dfc Binary files /dev/null and b/examples/data/Fruit360/raspberry/192_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/193_100.jpg b/examples/data/Fruit360/raspberry/193_100.jpg new file mode 100644 index 00000000..49b3aa47 Binary files /dev/null and b/examples/data/Fruit360/raspberry/193_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/194_100.jpg b/examples/data/Fruit360/raspberry/194_100.jpg new file mode 100644 index 00000000..bbbb66f4 Binary files /dev/null and b/examples/data/Fruit360/raspberry/194_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/195_100.jpg b/examples/data/Fruit360/raspberry/195_100.jpg new file mode 100644 index 00000000..4dcfa5f3 Binary files /dev/null and b/examples/data/Fruit360/raspberry/195_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/196_100.jpg b/examples/data/Fruit360/raspberry/196_100.jpg new file mode 100644 index 00000000..1e7211e4 Binary files /dev/null and b/examples/data/Fruit360/raspberry/196_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/197_100.jpg b/examples/data/Fruit360/raspberry/197_100.jpg new file mode 100644 index 00000000..c5c56eee Binary files /dev/null and b/examples/data/Fruit360/raspberry/197_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/198_100.jpg b/examples/data/Fruit360/raspberry/198_100.jpg new file mode 100644 index 00000000..efb5a014 Binary files /dev/null and b/examples/data/Fruit360/raspberry/198_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/19_100.jpg b/examples/data/Fruit360/raspberry/19_100.jpg new file mode 100644 index 00000000..e2f3b42f Binary files /dev/null and b/examples/data/Fruit360/raspberry/19_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/1_100.jpg b/examples/data/Fruit360/raspberry/1_100.jpg new file mode 100644 index 00000000..c04caef4 Binary files /dev/null and b/examples/data/Fruit360/raspberry/1_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/201_100.jpg b/examples/data/Fruit360/raspberry/201_100.jpg new file mode 100644 index 00000000..b148a792 Binary files /dev/null and b/examples/data/Fruit360/raspberry/201_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/20_100.jpg b/examples/data/Fruit360/raspberry/20_100.jpg new file mode 100644 index 00000000..ecebb14f Binary files /dev/null and b/examples/data/Fruit360/raspberry/20_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/21_100.jpg b/examples/data/Fruit360/raspberry/21_100.jpg new file mode 100644 index 00000000..8f9fd7d4 Binary files /dev/null and b/examples/data/Fruit360/raspberry/21_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/22_100.jpg b/examples/data/Fruit360/raspberry/22_100.jpg new file mode 100644 index 00000000..00720ec5 Binary files /dev/null and b/examples/data/Fruit360/raspberry/22_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/23_100.jpg b/examples/data/Fruit360/raspberry/23_100.jpg new file mode 100644 index 00000000..d2b4c01c Binary files /dev/null and b/examples/data/Fruit360/raspberry/23_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/248_100.jpg b/examples/data/Fruit360/raspberry/248_100.jpg new file mode 100644 index 00000000..27d321f6 Binary files /dev/null and b/examples/data/Fruit360/raspberry/248_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/249_100.jpg b/examples/data/Fruit360/raspberry/249_100.jpg new file mode 100644 index 00000000..b55072a5 Binary files /dev/null and b/examples/data/Fruit360/raspberry/249_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/24_100.jpg b/examples/data/Fruit360/raspberry/24_100.jpg new file mode 100644 index 00000000..9dd792b3 Binary files /dev/null and b/examples/data/Fruit360/raspberry/24_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/250_100.jpg b/examples/data/Fruit360/raspberry/250_100.jpg new file mode 100644 index 00000000..b6449398 Binary files /dev/null and b/examples/data/Fruit360/raspberry/250_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/251_100.jpg b/examples/data/Fruit360/raspberry/251_100.jpg new file mode 100644 index 00000000..91ca5838 Binary files /dev/null and b/examples/data/Fruit360/raspberry/251_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/252_100.jpg b/examples/data/Fruit360/raspberry/252_100.jpg new file mode 100644 index 00000000..ada29ae5 Binary files /dev/null and b/examples/data/Fruit360/raspberry/252_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/253_100.jpg b/examples/data/Fruit360/raspberry/253_100.jpg new file mode 100644 index 00000000..676a3758 Binary files /dev/null and b/examples/data/Fruit360/raspberry/253_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/254_100.jpg b/examples/data/Fruit360/raspberry/254_100.jpg new file mode 100644 index 00000000..9cb1dc71 Binary files /dev/null and b/examples/data/Fruit360/raspberry/254_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/255_100.jpg b/examples/data/Fruit360/raspberry/255_100.jpg new file mode 100644 index 00000000..ff37e216 Binary files /dev/null and b/examples/data/Fruit360/raspberry/255_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/256_100.jpg b/examples/data/Fruit360/raspberry/256_100.jpg new file mode 100644 index 00000000..0ed9a409 Binary files /dev/null and b/examples/data/Fruit360/raspberry/256_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/257_100.jpg b/examples/data/Fruit360/raspberry/257_100.jpg new file mode 100644 index 00000000..cea91374 Binary files /dev/null and b/examples/data/Fruit360/raspberry/257_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/258_100.jpg b/examples/data/Fruit360/raspberry/258_100.jpg new file mode 100644 index 00000000..b45829a8 Binary files /dev/null and b/examples/data/Fruit360/raspberry/258_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/259_100.jpg b/examples/data/Fruit360/raspberry/259_100.jpg new file mode 100644 index 00000000..1cff8ff0 Binary files /dev/null and b/examples/data/Fruit360/raspberry/259_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/25_100.jpg b/examples/data/Fruit360/raspberry/25_100.jpg new file mode 100644 index 00000000..0656b6a4 Binary files /dev/null and b/examples/data/Fruit360/raspberry/25_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/260_100.jpg b/examples/data/Fruit360/raspberry/260_100.jpg new file mode 100644 index 00000000..4a00a093 Binary files /dev/null and b/examples/data/Fruit360/raspberry/260_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/261_100.jpg b/examples/data/Fruit360/raspberry/261_100.jpg new file mode 100644 index 00000000..000f9b88 Binary files /dev/null and b/examples/data/Fruit360/raspberry/261_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/262_100.jpg b/examples/data/Fruit360/raspberry/262_100.jpg new file mode 100644 index 00000000..91f0a8f3 Binary files /dev/null and b/examples/data/Fruit360/raspberry/262_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/263_100.jpg b/examples/data/Fruit360/raspberry/263_100.jpg new file mode 100644 index 00000000..e912fead Binary files /dev/null and b/examples/data/Fruit360/raspberry/263_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/264_100.jpg b/examples/data/Fruit360/raspberry/264_100.jpg new file mode 100644 index 00000000..9eea565c Binary files /dev/null and b/examples/data/Fruit360/raspberry/264_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/265_100.jpg b/examples/data/Fruit360/raspberry/265_100.jpg new file mode 100644 index 00000000..26c453f8 Binary files /dev/null and b/examples/data/Fruit360/raspberry/265_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/266_100.jpg b/examples/data/Fruit360/raspberry/266_100.jpg new file mode 100644 index 00000000..71a06762 Binary files /dev/null and b/examples/data/Fruit360/raspberry/266_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/267_100.jpg b/examples/data/Fruit360/raspberry/267_100.jpg new file mode 100644 index 00000000..c3803d14 Binary files /dev/null and b/examples/data/Fruit360/raspberry/267_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/268_100.jpg b/examples/data/Fruit360/raspberry/268_100.jpg new file mode 100644 index 00000000..d090e62a Binary files /dev/null and b/examples/data/Fruit360/raspberry/268_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/269_100.jpg b/examples/data/Fruit360/raspberry/269_100.jpg new file mode 100644 index 00000000..5be7275a Binary files /dev/null and b/examples/data/Fruit360/raspberry/269_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/26_100.jpg b/examples/data/Fruit360/raspberry/26_100.jpg new file mode 100644 index 00000000..65a3c7c8 Binary files /dev/null and b/examples/data/Fruit360/raspberry/26_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/270_100.jpg b/examples/data/Fruit360/raspberry/270_100.jpg new file mode 100644 index 00000000..72ecf98e Binary files /dev/null and b/examples/data/Fruit360/raspberry/270_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/271_100.jpg b/examples/data/Fruit360/raspberry/271_100.jpg new file mode 100644 index 00000000..24a1c479 Binary files /dev/null and b/examples/data/Fruit360/raspberry/271_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/272_100.jpg b/examples/data/Fruit360/raspberry/272_100.jpg new file mode 100644 index 00000000..e901f105 Binary files /dev/null and b/examples/data/Fruit360/raspberry/272_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/273_100.jpg b/examples/data/Fruit360/raspberry/273_100.jpg new file mode 100644 index 00000000..7a8c017f Binary files /dev/null and b/examples/data/Fruit360/raspberry/273_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/274_100.jpg b/examples/data/Fruit360/raspberry/274_100.jpg new file mode 100644 index 00000000..f137aea3 Binary files /dev/null and b/examples/data/Fruit360/raspberry/274_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/275_100.jpg b/examples/data/Fruit360/raspberry/275_100.jpg new file mode 100644 index 00000000..1f7149df Binary files /dev/null and b/examples/data/Fruit360/raspberry/275_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/276_100.jpg b/examples/data/Fruit360/raspberry/276_100.jpg new file mode 100644 index 00000000..c07f70a1 Binary files /dev/null and b/examples/data/Fruit360/raspberry/276_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/277_100.jpg b/examples/data/Fruit360/raspberry/277_100.jpg new file mode 100644 index 00000000..43227d3c Binary files /dev/null and b/examples/data/Fruit360/raspberry/277_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/278_100.jpg b/examples/data/Fruit360/raspberry/278_100.jpg new file mode 100644 index 00000000..cf73b41f Binary files /dev/null and b/examples/data/Fruit360/raspberry/278_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/279_100.jpg b/examples/data/Fruit360/raspberry/279_100.jpg new file mode 100644 index 00000000..c992178a Binary files /dev/null and b/examples/data/Fruit360/raspberry/279_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/27_100.jpg b/examples/data/Fruit360/raspberry/27_100.jpg new file mode 100644 index 00000000..813dc3a1 Binary files /dev/null and b/examples/data/Fruit360/raspberry/27_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/280_100.jpg b/examples/data/Fruit360/raspberry/280_100.jpg new file mode 100644 index 00000000..b0980ee3 Binary files /dev/null and b/examples/data/Fruit360/raspberry/280_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/281_100.jpg b/examples/data/Fruit360/raspberry/281_100.jpg new file mode 100644 index 00000000..def66167 Binary files /dev/null and b/examples/data/Fruit360/raspberry/281_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/282_100.jpg b/examples/data/Fruit360/raspberry/282_100.jpg new file mode 100644 index 00000000..4cb4d31c Binary files /dev/null and b/examples/data/Fruit360/raspberry/282_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/283_100.jpg b/examples/data/Fruit360/raspberry/283_100.jpg new file mode 100644 index 00000000..81e81628 Binary files /dev/null and b/examples/data/Fruit360/raspberry/283_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/284_100.jpg b/examples/data/Fruit360/raspberry/284_100.jpg new file mode 100644 index 00000000..d31db0b1 Binary files /dev/null and b/examples/data/Fruit360/raspberry/284_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/285_100.jpg b/examples/data/Fruit360/raspberry/285_100.jpg new file mode 100644 index 00000000..bdeb2626 Binary files /dev/null and b/examples/data/Fruit360/raspberry/285_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/286_100.jpg b/examples/data/Fruit360/raspberry/286_100.jpg new file mode 100644 index 00000000..d9bab21f Binary files /dev/null and b/examples/data/Fruit360/raspberry/286_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/287_100.jpg b/examples/data/Fruit360/raspberry/287_100.jpg new file mode 100644 index 00000000..d7345b07 Binary files /dev/null and b/examples/data/Fruit360/raspberry/287_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/288_100.jpg b/examples/data/Fruit360/raspberry/288_100.jpg new file mode 100644 index 00000000..4cd1ae2c Binary files /dev/null and b/examples/data/Fruit360/raspberry/288_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/289_100.jpg b/examples/data/Fruit360/raspberry/289_100.jpg new file mode 100644 index 00000000..b8b9583a Binary files /dev/null and b/examples/data/Fruit360/raspberry/289_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/28_100.jpg b/examples/data/Fruit360/raspberry/28_100.jpg new file mode 100644 index 00000000..767b3047 Binary files /dev/null and b/examples/data/Fruit360/raspberry/28_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/290_100.jpg b/examples/data/Fruit360/raspberry/290_100.jpg new file mode 100644 index 00000000..338cad1d Binary files /dev/null and b/examples/data/Fruit360/raspberry/290_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/291_100.jpg b/examples/data/Fruit360/raspberry/291_100.jpg new file mode 100644 index 00000000..4987f824 Binary files /dev/null and b/examples/data/Fruit360/raspberry/291_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/292_100.jpg b/examples/data/Fruit360/raspberry/292_100.jpg new file mode 100644 index 00000000..3f808412 Binary files /dev/null and b/examples/data/Fruit360/raspberry/292_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/293_100.jpg b/examples/data/Fruit360/raspberry/293_100.jpg new file mode 100644 index 00000000..87aaebbf Binary files /dev/null and b/examples/data/Fruit360/raspberry/293_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/294_100.jpg b/examples/data/Fruit360/raspberry/294_100.jpg new file mode 100644 index 00000000..abb75921 Binary files /dev/null and b/examples/data/Fruit360/raspberry/294_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/295_100.jpg b/examples/data/Fruit360/raspberry/295_100.jpg new file mode 100644 index 00000000..d0eb293f Binary files /dev/null and b/examples/data/Fruit360/raspberry/295_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/296_100.jpg b/examples/data/Fruit360/raspberry/296_100.jpg new file mode 100644 index 00000000..9daa4479 Binary files /dev/null and b/examples/data/Fruit360/raspberry/296_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/297_100.jpg b/examples/data/Fruit360/raspberry/297_100.jpg new file mode 100644 index 00000000..5f99bd58 Binary files /dev/null and b/examples/data/Fruit360/raspberry/297_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/298_100.jpg b/examples/data/Fruit360/raspberry/298_100.jpg new file mode 100644 index 00000000..0d05be17 Binary files /dev/null and b/examples/data/Fruit360/raspberry/298_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/299_100.jpg b/examples/data/Fruit360/raspberry/299_100.jpg new file mode 100644 index 00000000..abfb7b0a Binary files /dev/null and b/examples/data/Fruit360/raspberry/299_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/29_100.jpg b/examples/data/Fruit360/raspberry/29_100.jpg new file mode 100644 index 00000000..da848157 Binary files /dev/null and b/examples/data/Fruit360/raspberry/29_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/2_100.jpg b/examples/data/Fruit360/raspberry/2_100.jpg new file mode 100644 index 00000000..e4173534 Binary files /dev/null and b/examples/data/Fruit360/raspberry/2_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/300_100.jpg b/examples/data/Fruit360/raspberry/300_100.jpg new file mode 100644 index 00000000..6181012b Binary files /dev/null and b/examples/data/Fruit360/raspberry/300_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/301_100.jpg b/examples/data/Fruit360/raspberry/301_100.jpg new file mode 100644 index 00000000..fcea0be5 Binary files /dev/null and b/examples/data/Fruit360/raspberry/301_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/302_100.jpg b/examples/data/Fruit360/raspberry/302_100.jpg new file mode 100644 index 00000000..3ce2eb45 Binary files /dev/null and b/examples/data/Fruit360/raspberry/302_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/303_100.jpg b/examples/data/Fruit360/raspberry/303_100.jpg new file mode 100644 index 00000000..04faf8f1 Binary files /dev/null and b/examples/data/Fruit360/raspberry/303_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/304_100.jpg b/examples/data/Fruit360/raspberry/304_100.jpg new file mode 100644 index 00000000..a63c7b2e Binary files /dev/null and b/examples/data/Fruit360/raspberry/304_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/305_100.jpg b/examples/data/Fruit360/raspberry/305_100.jpg new file mode 100644 index 00000000..656ec173 Binary files /dev/null and b/examples/data/Fruit360/raspberry/305_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/306_100.jpg b/examples/data/Fruit360/raspberry/306_100.jpg new file mode 100644 index 00000000..22a36846 Binary files /dev/null and b/examples/data/Fruit360/raspberry/306_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/307_100.jpg b/examples/data/Fruit360/raspberry/307_100.jpg new file mode 100644 index 00000000..b23e4517 Binary files /dev/null and b/examples/data/Fruit360/raspberry/307_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/308_100.jpg b/examples/data/Fruit360/raspberry/308_100.jpg new file mode 100644 index 00000000..defcbe86 Binary files /dev/null and b/examples/data/Fruit360/raspberry/308_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/309_100.jpg b/examples/data/Fruit360/raspberry/309_100.jpg new file mode 100644 index 00000000..6ad237d8 Binary files /dev/null and b/examples/data/Fruit360/raspberry/309_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/30_100.jpg b/examples/data/Fruit360/raspberry/30_100.jpg new file mode 100644 index 00000000..27d00fec Binary files /dev/null and b/examples/data/Fruit360/raspberry/30_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/310_100.jpg b/examples/data/Fruit360/raspberry/310_100.jpg new file mode 100644 index 00000000..a48716d9 Binary files /dev/null and b/examples/data/Fruit360/raspberry/310_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/311_100.jpg b/examples/data/Fruit360/raspberry/311_100.jpg new file mode 100644 index 00000000..4471a98e Binary files /dev/null and b/examples/data/Fruit360/raspberry/311_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/312_100.jpg b/examples/data/Fruit360/raspberry/312_100.jpg new file mode 100644 index 00000000..80916158 Binary files /dev/null and b/examples/data/Fruit360/raspberry/312_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/313_100.jpg b/examples/data/Fruit360/raspberry/313_100.jpg new file mode 100644 index 00000000..a152597e Binary files /dev/null and b/examples/data/Fruit360/raspberry/313_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/314_100.jpg b/examples/data/Fruit360/raspberry/314_100.jpg new file mode 100644 index 00000000..6becbfbe Binary files /dev/null and b/examples/data/Fruit360/raspberry/314_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/315_100.jpg b/examples/data/Fruit360/raspberry/315_100.jpg new file mode 100644 index 00000000..8938b46c Binary files /dev/null and b/examples/data/Fruit360/raspberry/315_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/316_100.jpg b/examples/data/Fruit360/raspberry/316_100.jpg new file mode 100644 index 00000000..79d29db4 Binary files /dev/null and b/examples/data/Fruit360/raspberry/316_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/317_100.jpg b/examples/data/Fruit360/raspberry/317_100.jpg new file mode 100644 index 00000000..3fa89152 Binary files /dev/null and b/examples/data/Fruit360/raspberry/317_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/318_100.jpg b/examples/data/Fruit360/raspberry/318_100.jpg new file mode 100644 index 00000000..88b1e18d Binary files /dev/null and b/examples/data/Fruit360/raspberry/318_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/319_100.jpg b/examples/data/Fruit360/raspberry/319_100.jpg new file mode 100644 index 00000000..fcf4df1e Binary files /dev/null and b/examples/data/Fruit360/raspberry/319_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/31_100.jpg b/examples/data/Fruit360/raspberry/31_100.jpg new file mode 100644 index 00000000..b6ec38c3 Binary files /dev/null and b/examples/data/Fruit360/raspberry/31_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/320_100.jpg b/examples/data/Fruit360/raspberry/320_100.jpg new file mode 100644 index 00000000..0424a7e6 Binary files /dev/null and b/examples/data/Fruit360/raspberry/320_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/321_100.jpg b/examples/data/Fruit360/raspberry/321_100.jpg new file mode 100644 index 00000000..796fe2e6 Binary files /dev/null and b/examples/data/Fruit360/raspberry/321_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/322_100.jpg b/examples/data/Fruit360/raspberry/322_100.jpg new file mode 100644 index 00000000..b1c8f759 Binary files /dev/null and b/examples/data/Fruit360/raspberry/322_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/323_100.jpg b/examples/data/Fruit360/raspberry/323_100.jpg new file mode 100644 index 00000000..ab1f30fe Binary files /dev/null and b/examples/data/Fruit360/raspberry/323_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/324_100.jpg b/examples/data/Fruit360/raspberry/324_100.jpg new file mode 100644 index 00000000..4d47fffa Binary files /dev/null and b/examples/data/Fruit360/raspberry/324_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/325_100.jpg b/examples/data/Fruit360/raspberry/325_100.jpg new file mode 100644 index 00000000..f507b02f Binary files /dev/null and b/examples/data/Fruit360/raspberry/325_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/326_100.jpg b/examples/data/Fruit360/raspberry/326_100.jpg new file mode 100644 index 00000000..b5368614 Binary files /dev/null and b/examples/data/Fruit360/raspberry/326_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/327_100.jpg b/examples/data/Fruit360/raspberry/327_100.jpg new file mode 100644 index 00000000..73ed78c5 Binary files /dev/null and b/examples/data/Fruit360/raspberry/327_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/32_100.jpg b/examples/data/Fruit360/raspberry/32_100.jpg new file mode 100644 index 00000000..2286d996 Binary files /dev/null and b/examples/data/Fruit360/raspberry/32_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/33_100.jpg b/examples/data/Fruit360/raspberry/33_100.jpg new file mode 100644 index 00000000..d5e2d3bb Binary files /dev/null and b/examples/data/Fruit360/raspberry/33_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/34_100.jpg b/examples/data/Fruit360/raspberry/34_100.jpg new file mode 100644 index 00000000..6c9930a7 Binary files /dev/null and b/examples/data/Fruit360/raspberry/34_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/35_100.jpg b/examples/data/Fruit360/raspberry/35_100.jpg new file mode 100644 index 00000000..7e5985d7 Binary files /dev/null and b/examples/data/Fruit360/raspberry/35_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/36_100.jpg b/examples/data/Fruit360/raspberry/36_100.jpg new file mode 100644 index 00000000..10c34c45 Binary files /dev/null and b/examples/data/Fruit360/raspberry/36_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/37_100.jpg b/examples/data/Fruit360/raspberry/37_100.jpg new file mode 100644 index 00000000..014bfc45 Binary files /dev/null and b/examples/data/Fruit360/raspberry/37_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/38_100.jpg b/examples/data/Fruit360/raspberry/38_100.jpg new file mode 100644 index 00000000..8fdcde79 Binary files /dev/null and b/examples/data/Fruit360/raspberry/38_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/39_100.jpg b/examples/data/Fruit360/raspberry/39_100.jpg new file mode 100644 index 00000000..eee6bda8 Binary files /dev/null and b/examples/data/Fruit360/raspberry/39_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/3_100.jpg b/examples/data/Fruit360/raspberry/3_100.jpg new file mode 100644 index 00000000..4c193258 Binary files /dev/null and b/examples/data/Fruit360/raspberry/3_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/40_100.jpg b/examples/data/Fruit360/raspberry/40_100.jpg new file mode 100644 index 00000000..dd06432f Binary files /dev/null and b/examples/data/Fruit360/raspberry/40_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/41_100.jpg b/examples/data/Fruit360/raspberry/41_100.jpg new file mode 100644 index 00000000..ee76f85a Binary files /dev/null and b/examples/data/Fruit360/raspberry/41_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/42_100.jpg b/examples/data/Fruit360/raspberry/42_100.jpg new file mode 100644 index 00000000..2490b719 Binary files /dev/null and b/examples/data/Fruit360/raspberry/42_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/43_100.jpg b/examples/data/Fruit360/raspberry/43_100.jpg new file mode 100644 index 00000000..6b5557fd Binary files /dev/null and b/examples/data/Fruit360/raspberry/43_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/44_100.jpg b/examples/data/Fruit360/raspberry/44_100.jpg new file mode 100644 index 00000000..e15aed4b Binary files /dev/null and b/examples/data/Fruit360/raspberry/44_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/45_100.jpg b/examples/data/Fruit360/raspberry/45_100.jpg new file mode 100644 index 00000000..50d0ec1d Binary files /dev/null and b/examples/data/Fruit360/raspberry/45_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/46_100.jpg b/examples/data/Fruit360/raspberry/46_100.jpg new file mode 100644 index 00000000..5346da09 Binary files /dev/null and b/examples/data/Fruit360/raspberry/46_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/47_100.jpg b/examples/data/Fruit360/raspberry/47_100.jpg new file mode 100644 index 00000000..fe26a54f Binary files /dev/null and b/examples/data/Fruit360/raspberry/47_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/48_100.jpg b/examples/data/Fruit360/raspberry/48_100.jpg new file mode 100644 index 00000000..d651aec2 Binary files /dev/null and b/examples/data/Fruit360/raspberry/48_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/49_100.jpg b/examples/data/Fruit360/raspberry/49_100.jpg new file mode 100644 index 00000000..dcaf7930 Binary files /dev/null and b/examples/data/Fruit360/raspberry/49_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/4_100.jpg b/examples/data/Fruit360/raspberry/4_100.jpg new file mode 100644 index 00000000..b5bbf418 Binary files /dev/null and b/examples/data/Fruit360/raspberry/4_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/50_100.jpg b/examples/data/Fruit360/raspberry/50_100.jpg new file mode 100644 index 00000000..cd3ad671 Binary files /dev/null and b/examples/data/Fruit360/raspberry/50_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/51_100.jpg b/examples/data/Fruit360/raspberry/51_100.jpg new file mode 100644 index 00000000..d75eb6a2 Binary files /dev/null and b/examples/data/Fruit360/raspberry/51_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/52_100.jpg b/examples/data/Fruit360/raspberry/52_100.jpg new file mode 100644 index 00000000..33c7fa89 Binary files /dev/null and b/examples/data/Fruit360/raspberry/52_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/53_100.jpg b/examples/data/Fruit360/raspberry/53_100.jpg new file mode 100644 index 00000000..61494809 Binary files /dev/null and b/examples/data/Fruit360/raspberry/53_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/54_100.jpg b/examples/data/Fruit360/raspberry/54_100.jpg new file mode 100644 index 00000000..571f67b0 Binary files /dev/null and b/examples/data/Fruit360/raspberry/54_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/55_100.jpg b/examples/data/Fruit360/raspberry/55_100.jpg new file mode 100644 index 00000000..34c60366 Binary files /dev/null and b/examples/data/Fruit360/raspberry/55_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/56_100.jpg b/examples/data/Fruit360/raspberry/56_100.jpg new file mode 100644 index 00000000..6e96b065 Binary files /dev/null and b/examples/data/Fruit360/raspberry/56_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/57_100.jpg b/examples/data/Fruit360/raspberry/57_100.jpg new file mode 100644 index 00000000..fd7438ed Binary files /dev/null and b/examples/data/Fruit360/raspberry/57_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/58_100.jpg b/examples/data/Fruit360/raspberry/58_100.jpg new file mode 100644 index 00000000..b18a31b7 Binary files /dev/null and b/examples/data/Fruit360/raspberry/58_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/59_100.jpg b/examples/data/Fruit360/raspberry/59_100.jpg new file mode 100644 index 00000000..1aca169a Binary files /dev/null and b/examples/data/Fruit360/raspberry/59_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/5_100.jpg b/examples/data/Fruit360/raspberry/5_100.jpg new file mode 100644 index 00000000..0f133f87 Binary files /dev/null and b/examples/data/Fruit360/raspberry/5_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/60_100.jpg b/examples/data/Fruit360/raspberry/60_100.jpg new file mode 100644 index 00000000..7da61f1d Binary files /dev/null and b/examples/data/Fruit360/raspberry/60_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/61_100.jpg b/examples/data/Fruit360/raspberry/61_100.jpg new file mode 100644 index 00000000..fe497eae Binary files /dev/null and b/examples/data/Fruit360/raspberry/61_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/62_100.jpg b/examples/data/Fruit360/raspberry/62_100.jpg new file mode 100644 index 00000000..1d2201bd Binary files /dev/null and b/examples/data/Fruit360/raspberry/62_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/63_100.jpg b/examples/data/Fruit360/raspberry/63_100.jpg new file mode 100644 index 00000000..339f376c Binary files /dev/null and b/examples/data/Fruit360/raspberry/63_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/64_100.jpg b/examples/data/Fruit360/raspberry/64_100.jpg new file mode 100644 index 00000000..d4ac8c8f Binary files /dev/null and b/examples/data/Fruit360/raspberry/64_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/65_100.jpg b/examples/data/Fruit360/raspberry/65_100.jpg new file mode 100644 index 00000000..0dbbbda0 Binary files /dev/null and b/examples/data/Fruit360/raspberry/65_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/66_100.jpg b/examples/data/Fruit360/raspberry/66_100.jpg new file mode 100644 index 00000000..aada0ce9 Binary files /dev/null and b/examples/data/Fruit360/raspberry/66_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/67_100.jpg b/examples/data/Fruit360/raspberry/67_100.jpg new file mode 100644 index 00000000..5e7e705a Binary files /dev/null and b/examples/data/Fruit360/raspberry/67_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/68_100.jpg b/examples/data/Fruit360/raspberry/68_100.jpg new file mode 100644 index 00000000..0c6b3157 Binary files /dev/null and b/examples/data/Fruit360/raspberry/68_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/69_100.jpg b/examples/data/Fruit360/raspberry/69_100.jpg new file mode 100644 index 00000000..008e9f20 Binary files /dev/null and b/examples/data/Fruit360/raspberry/69_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/6_100.jpg b/examples/data/Fruit360/raspberry/6_100.jpg new file mode 100644 index 00000000..687f639d Binary files /dev/null and b/examples/data/Fruit360/raspberry/6_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/70_100.jpg b/examples/data/Fruit360/raspberry/70_100.jpg new file mode 100644 index 00000000..6820050b Binary files /dev/null and b/examples/data/Fruit360/raspberry/70_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/71_100.jpg b/examples/data/Fruit360/raspberry/71_100.jpg new file mode 100644 index 00000000..0bda1fff Binary files /dev/null and b/examples/data/Fruit360/raspberry/71_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/72_100.jpg b/examples/data/Fruit360/raspberry/72_100.jpg new file mode 100644 index 00000000..01f68ba2 Binary files /dev/null and b/examples/data/Fruit360/raspberry/72_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/73_100.jpg b/examples/data/Fruit360/raspberry/73_100.jpg new file mode 100644 index 00000000..58abad52 Binary files /dev/null and b/examples/data/Fruit360/raspberry/73_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/74_100.jpg b/examples/data/Fruit360/raspberry/74_100.jpg new file mode 100644 index 00000000..b6e687c3 Binary files /dev/null and b/examples/data/Fruit360/raspberry/74_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/75_100.jpg b/examples/data/Fruit360/raspberry/75_100.jpg new file mode 100644 index 00000000..d6667b6f Binary files /dev/null and b/examples/data/Fruit360/raspberry/75_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/76_100.jpg b/examples/data/Fruit360/raspberry/76_100.jpg new file mode 100644 index 00000000..0bfea24d Binary files /dev/null and b/examples/data/Fruit360/raspberry/76_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/78_100.jpg b/examples/data/Fruit360/raspberry/78_100.jpg new file mode 100644 index 00000000..1a5fefad Binary files /dev/null and b/examples/data/Fruit360/raspberry/78_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/7_100.jpg b/examples/data/Fruit360/raspberry/7_100.jpg new file mode 100644 index 00000000..35be697c Binary files /dev/null and b/examples/data/Fruit360/raspberry/7_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/8_100.jpg b/examples/data/Fruit360/raspberry/8_100.jpg new file mode 100644 index 00000000..03852926 Binary files /dev/null and b/examples/data/Fruit360/raspberry/8_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/9_100.jpg b/examples/data/Fruit360/raspberry/9_100.jpg new file mode 100644 index 00000000..6b547ab1 Binary files /dev/null and b/examples/data/Fruit360/raspberry/9_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_0_100.jpg b/examples/data/Fruit360/raspberry/r_0_100.jpg new file mode 100644 index 00000000..99a81ce3 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_0_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_113_100.jpg b/examples/data/Fruit360/raspberry/r_113_100.jpg new file mode 100644 index 00000000..0704b161 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_113_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_11_100.jpg b/examples/data/Fruit360/raspberry/r_11_100.jpg new file mode 100644 index 00000000..1cef7605 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_11_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_123_100.jpg b/examples/data/Fruit360/raspberry/r_123_100.jpg new file mode 100644 index 00000000..2003681c Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_123_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_124_100.jpg b/examples/data/Fruit360/raspberry/r_124_100.jpg new file mode 100644 index 00000000..5e474c0b Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_124_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_125_100.jpg b/examples/data/Fruit360/raspberry/r_125_100.jpg new file mode 100644 index 00000000..4a0b2b6f Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_125_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_126_100.jpg b/examples/data/Fruit360/raspberry/r_126_100.jpg new file mode 100644 index 00000000..dc0569c2 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_126_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_127_100.jpg b/examples/data/Fruit360/raspberry/r_127_100.jpg new file mode 100644 index 00000000..2963d047 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_127_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_128_100.jpg b/examples/data/Fruit360/raspberry/r_128_100.jpg new file mode 100644 index 00000000..ef5fae8f Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_128_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_129_100.jpg b/examples/data/Fruit360/raspberry/r_129_100.jpg new file mode 100644 index 00000000..24706417 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_129_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_12_100.jpg b/examples/data/Fruit360/raspberry/r_12_100.jpg new file mode 100644 index 00000000..f8a16c9f Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_12_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_130_100.jpg b/examples/data/Fruit360/raspberry/r_130_100.jpg new file mode 100644 index 00000000..87e786ff Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_130_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_131_100.jpg b/examples/data/Fruit360/raspberry/r_131_100.jpg new file mode 100644 index 00000000..8dfbced1 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_131_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_132_100.jpg b/examples/data/Fruit360/raspberry/r_132_100.jpg new file mode 100644 index 00000000..db7dd088 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_132_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_133_100.jpg b/examples/data/Fruit360/raspberry/r_133_100.jpg new file mode 100644 index 00000000..1a088510 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_133_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_134_100.jpg b/examples/data/Fruit360/raspberry/r_134_100.jpg new file mode 100644 index 00000000..541b92fb Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_134_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_135_100.jpg b/examples/data/Fruit360/raspberry/r_135_100.jpg new file mode 100644 index 00000000..b4302f78 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_135_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_136_100.jpg b/examples/data/Fruit360/raspberry/r_136_100.jpg new file mode 100644 index 00000000..0e74f1d6 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_136_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_137_100.jpg b/examples/data/Fruit360/raspberry/r_137_100.jpg new file mode 100644 index 00000000..c123ff59 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_137_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_138_100.jpg b/examples/data/Fruit360/raspberry/r_138_100.jpg new file mode 100644 index 00000000..1478075c Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_138_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_139_100.jpg b/examples/data/Fruit360/raspberry/r_139_100.jpg new file mode 100644 index 00000000..f3b32503 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_139_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_13_100.jpg b/examples/data/Fruit360/raspberry/r_13_100.jpg new file mode 100644 index 00000000..cf9a0085 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_13_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_140_100.jpg b/examples/data/Fruit360/raspberry/r_140_100.jpg new file mode 100644 index 00000000..c7895ec4 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_140_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_141_100.jpg b/examples/data/Fruit360/raspberry/r_141_100.jpg new file mode 100644 index 00000000..98d41df9 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_141_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_142_100.jpg b/examples/data/Fruit360/raspberry/r_142_100.jpg new file mode 100644 index 00000000..237921aa Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_142_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_143_100.jpg b/examples/data/Fruit360/raspberry/r_143_100.jpg new file mode 100644 index 00000000..3ebac326 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_143_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_144_100.jpg b/examples/data/Fruit360/raspberry/r_144_100.jpg new file mode 100644 index 00000000..a6b6d521 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_144_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_145_100.jpg b/examples/data/Fruit360/raspberry/r_145_100.jpg new file mode 100644 index 00000000..99c554ed Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_145_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_146_100.jpg b/examples/data/Fruit360/raspberry/r_146_100.jpg new file mode 100644 index 00000000..6b27b2f7 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_146_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_147_100.jpg b/examples/data/Fruit360/raspberry/r_147_100.jpg new file mode 100644 index 00000000..a95bd189 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_147_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_148_100.jpg b/examples/data/Fruit360/raspberry/r_148_100.jpg new file mode 100644 index 00000000..1e34aaf3 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_148_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_149_100.jpg b/examples/data/Fruit360/raspberry/r_149_100.jpg new file mode 100644 index 00000000..e5137f55 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_149_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_14_100.jpg b/examples/data/Fruit360/raspberry/r_14_100.jpg new file mode 100644 index 00000000..123899bb Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_14_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_150_100.jpg b/examples/data/Fruit360/raspberry/r_150_100.jpg new file mode 100644 index 00000000..0b53d0a2 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_150_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_151_100.jpg b/examples/data/Fruit360/raspberry/r_151_100.jpg new file mode 100644 index 00000000..ef74aa09 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_151_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_152_100.jpg b/examples/data/Fruit360/raspberry/r_152_100.jpg new file mode 100644 index 00000000..572544ea Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_152_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_153_100.jpg b/examples/data/Fruit360/raspberry/r_153_100.jpg new file mode 100644 index 00000000..5ddeb646 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_153_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_154_100.jpg b/examples/data/Fruit360/raspberry/r_154_100.jpg new file mode 100644 index 00000000..6701cc66 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_154_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_155_100.jpg b/examples/data/Fruit360/raspberry/r_155_100.jpg new file mode 100644 index 00000000..d27145f7 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_155_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_156_100.jpg b/examples/data/Fruit360/raspberry/r_156_100.jpg new file mode 100644 index 00000000..10bfa5a2 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_156_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_157_100.jpg b/examples/data/Fruit360/raspberry/r_157_100.jpg new file mode 100644 index 00000000..e6ab7366 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_157_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_158_100.jpg b/examples/data/Fruit360/raspberry/r_158_100.jpg new file mode 100644 index 00000000..f38e6b24 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_158_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_159_100.jpg b/examples/data/Fruit360/raspberry/r_159_100.jpg new file mode 100644 index 00000000..f9c560a1 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_159_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_15_100.jpg b/examples/data/Fruit360/raspberry/r_15_100.jpg new file mode 100644 index 00000000..8ef6ec88 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_15_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_160_100.jpg b/examples/data/Fruit360/raspberry/r_160_100.jpg new file mode 100644 index 00000000..468da6bd Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_160_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_161_100.jpg b/examples/data/Fruit360/raspberry/r_161_100.jpg new file mode 100644 index 00000000..fb944df0 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_161_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_162_100.jpg b/examples/data/Fruit360/raspberry/r_162_100.jpg new file mode 100644 index 00000000..127cc440 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_162_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_163_100.jpg b/examples/data/Fruit360/raspberry/r_163_100.jpg new file mode 100644 index 00000000..5320111b Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_163_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_164_100.jpg b/examples/data/Fruit360/raspberry/r_164_100.jpg new file mode 100644 index 00000000..2ac92ffd Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_164_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_165_100.jpg b/examples/data/Fruit360/raspberry/r_165_100.jpg new file mode 100644 index 00000000..387cc23a Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_165_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_166_100.jpg b/examples/data/Fruit360/raspberry/r_166_100.jpg new file mode 100644 index 00000000..7f3564d9 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_166_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_167_100.jpg b/examples/data/Fruit360/raspberry/r_167_100.jpg new file mode 100644 index 00000000..8dc855ae Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_167_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_168_100.jpg b/examples/data/Fruit360/raspberry/r_168_100.jpg new file mode 100644 index 00000000..27e4fa6f Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_168_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_169_100.jpg b/examples/data/Fruit360/raspberry/r_169_100.jpg new file mode 100644 index 00000000..5e35a891 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_169_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_16_100.jpg b/examples/data/Fruit360/raspberry/r_16_100.jpg new file mode 100644 index 00000000..544ca70d Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_16_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_170_100.jpg b/examples/data/Fruit360/raspberry/r_170_100.jpg new file mode 100644 index 00000000..71dde814 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_170_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_171_100.jpg b/examples/data/Fruit360/raspberry/r_171_100.jpg new file mode 100644 index 00000000..54c3f82c Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_171_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_172_100.jpg b/examples/data/Fruit360/raspberry/r_172_100.jpg new file mode 100644 index 00000000..7f5c0223 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_172_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_173_100.jpg b/examples/data/Fruit360/raspberry/r_173_100.jpg new file mode 100644 index 00000000..ffa0551d Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_173_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_174_100.jpg b/examples/data/Fruit360/raspberry/r_174_100.jpg new file mode 100644 index 00000000..5e9d200e Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_174_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_175_100.jpg b/examples/data/Fruit360/raspberry/r_175_100.jpg new file mode 100644 index 00000000..48d01035 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_175_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_176_100.jpg b/examples/data/Fruit360/raspberry/r_176_100.jpg new file mode 100644 index 00000000..7f203725 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_176_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_177_100.jpg b/examples/data/Fruit360/raspberry/r_177_100.jpg new file mode 100644 index 00000000..87a71227 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_177_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_178_100.jpg b/examples/data/Fruit360/raspberry/r_178_100.jpg new file mode 100644 index 00000000..21cc390a Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_178_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_179_100.jpg b/examples/data/Fruit360/raspberry/r_179_100.jpg new file mode 100644 index 00000000..e1fd7f17 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_179_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_17_100.jpg b/examples/data/Fruit360/raspberry/r_17_100.jpg new file mode 100644 index 00000000..ecb0703d Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_17_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_180_100.jpg b/examples/data/Fruit360/raspberry/r_180_100.jpg new file mode 100644 index 00000000..4e17af36 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_180_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_181_100.jpg b/examples/data/Fruit360/raspberry/r_181_100.jpg new file mode 100644 index 00000000..3d2b4718 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_181_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_182_100.jpg b/examples/data/Fruit360/raspberry/r_182_100.jpg new file mode 100644 index 00000000..eed06f9f Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_182_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_183_100.jpg b/examples/data/Fruit360/raspberry/r_183_100.jpg new file mode 100644 index 00000000..44448573 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_183_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_184_100.jpg b/examples/data/Fruit360/raspberry/r_184_100.jpg new file mode 100644 index 00000000..6c676a24 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_184_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_185_100.jpg b/examples/data/Fruit360/raspberry/r_185_100.jpg new file mode 100644 index 00000000..187ccddb Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_185_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_186_100.jpg b/examples/data/Fruit360/raspberry/r_186_100.jpg new file mode 100644 index 00000000..2d52bab4 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_186_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_187_100.jpg b/examples/data/Fruit360/raspberry/r_187_100.jpg new file mode 100644 index 00000000..1787a40e Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_187_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_188_100.jpg b/examples/data/Fruit360/raspberry/r_188_100.jpg new file mode 100644 index 00000000..f8c737ce Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_188_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_189_100.jpg b/examples/data/Fruit360/raspberry/r_189_100.jpg new file mode 100644 index 00000000..22885fc3 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_189_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_18_100.jpg b/examples/data/Fruit360/raspberry/r_18_100.jpg new file mode 100644 index 00000000..de4116da Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_18_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_190_100.jpg b/examples/data/Fruit360/raspberry/r_190_100.jpg new file mode 100644 index 00000000..5887bcb5 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_190_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_191_100.jpg b/examples/data/Fruit360/raspberry/r_191_100.jpg new file mode 100644 index 00000000..98f5742b Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_191_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_192_100.jpg b/examples/data/Fruit360/raspberry/r_192_100.jpg new file mode 100644 index 00000000..2b78c4ec Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_192_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_193_100.jpg b/examples/data/Fruit360/raspberry/r_193_100.jpg new file mode 100644 index 00000000..baab70c1 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_193_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_194_100.jpg b/examples/data/Fruit360/raspberry/r_194_100.jpg new file mode 100644 index 00000000..d7061002 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_194_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_195_100.jpg b/examples/data/Fruit360/raspberry/r_195_100.jpg new file mode 100644 index 00000000..ac52d0cc Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_195_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_196_100.jpg b/examples/data/Fruit360/raspberry/r_196_100.jpg new file mode 100644 index 00000000..d7fbbb6d Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_196_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_197_100.jpg b/examples/data/Fruit360/raspberry/r_197_100.jpg new file mode 100644 index 00000000..dd72975f Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_197_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_198_100.jpg b/examples/data/Fruit360/raspberry/r_198_100.jpg new file mode 100644 index 00000000..5ea98121 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_198_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_199_100.jpg b/examples/data/Fruit360/raspberry/r_199_100.jpg new file mode 100644 index 00000000..ef435cb0 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_199_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_1_100.jpg b/examples/data/Fruit360/raspberry/r_1_100.jpg new file mode 100644 index 00000000..be883c43 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_1_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_200_100.jpg b/examples/data/Fruit360/raspberry/r_200_100.jpg new file mode 100644 index 00000000..d8c4f4a4 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_200_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_201_100.jpg b/examples/data/Fruit360/raspberry/r_201_100.jpg new file mode 100644 index 00000000..968b8696 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_201_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_202_100.jpg b/examples/data/Fruit360/raspberry/r_202_100.jpg new file mode 100644 index 00000000..749de404 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_202_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_203_100.jpg b/examples/data/Fruit360/raspberry/r_203_100.jpg new file mode 100644 index 00000000..ae98cb00 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_203_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_204_100.jpg b/examples/data/Fruit360/raspberry/r_204_100.jpg new file mode 100644 index 00000000..50c97a12 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_204_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_205_100.jpg b/examples/data/Fruit360/raspberry/r_205_100.jpg new file mode 100644 index 00000000..c9968660 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_205_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_206_100.jpg b/examples/data/Fruit360/raspberry/r_206_100.jpg new file mode 100644 index 00000000..81d2f66a Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_206_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_207_100.jpg b/examples/data/Fruit360/raspberry/r_207_100.jpg new file mode 100644 index 00000000..039b07a9 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_207_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_208_100.jpg b/examples/data/Fruit360/raspberry/r_208_100.jpg new file mode 100644 index 00000000..7f19dd67 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_208_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_209_100.jpg b/examples/data/Fruit360/raspberry/r_209_100.jpg new file mode 100644 index 00000000..0c1230f5 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_209_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_20_100.jpg b/examples/data/Fruit360/raspberry/r_20_100.jpg new file mode 100644 index 00000000..e112ccab Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_20_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_210_100.jpg b/examples/data/Fruit360/raspberry/r_210_100.jpg new file mode 100644 index 00000000..69bfa246 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_210_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_211_100.jpg b/examples/data/Fruit360/raspberry/r_211_100.jpg new file mode 100644 index 00000000..bf8ae9fe Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_211_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_212_100.jpg b/examples/data/Fruit360/raspberry/r_212_100.jpg new file mode 100644 index 00000000..8584625e Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_212_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_213_100.jpg b/examples/data/Fruit360/raspberry/r_213_100.jpg new file mode 100644 index 00000000..e08f03eb Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_213_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_214_100.jpg b/examples/data/Fruit360/raspberry/r_214_100.jpg new file mode 100644 index 00000000..b0eea893 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_214_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_215_100.jpg b/examples/data/Fruit360/raspberry/r_215_100.jpg new file mode 100644 index 00000000..18a512fd Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_215_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_221_100.jpg b/examples/data/Fruit360/raspberry/r_221_100.jpg new file mode 100644 index 00000000..9315d6d2 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_221_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_222_100.jpg b/examples/data/Fruit360/raspberry/r_222_100.jpg new file mode 100644 index 00000000..b8ccbb7f Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_222_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_224_100.jpg b/examples/data/Fruit360/raspberry/r_224_100.jpg new file mode 100644 index 00000000..f2a7d230 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_224_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_225_100.jpg b/examples/data/Fruit360/raspberry/r_225_100.jpg new file mode 100644 index 00000000..e3503627 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_225_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_226_100.jpg b/examples/data/Fruit360/raspberry/r_226_100.jpg new file mode 100644 index 00000000..32b2175c Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_226_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_229_100.jpg b/examples/data/Fruit360/raspberry/r_229_100.jpg new file mode 100644 index 00000000..a8e16bd4 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_229_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_23_100.jpg b/examples/data/Fruit360/raspberry/r_23_100.jpg new file mode 100644 index 00000000..eccc3b04 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_23_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_241_100.jpg b/examples/data/Fruit360/raspberry/r_241_100.jpg new file mode 100644 index 00000000..6b22d6e2 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_241_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_242_100.jpg b/examples/data/Fruit360/raspberry/r_242_100.jpg new file mode 100644 index 00000000..9729a274 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_242_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_243_100.jpg b/examples/data/Fruit360/raspberry/r_243_100.jpg new file mode 100644 index 00000000..05770092 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_243_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_244_100.jpg b/examples/data/Fruit360/raspberry/r_244_100.jpg new file mode 100644 index 00000000..2b551f22 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_244_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_245_100.jpg b/examples/data/Fruit360/raspberry/r_245_100.jpg new file mode 100644 index 00000000..9f7082e3 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_245_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_246_100.jpg b/examples/data/Fruit360/raspberry/r_246_100.jpg new file mode 100644 index 00000000..9c109466 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_246_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_247_100.jpg b/examples/data/Fruit360/raspberry/r_247_100.jpg new file mode 100644 index 00000000..96d769ab Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_247_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_249_100.jpg b/examples/data/Fruit360/raspberry/r_249_100.jpg new file mode 100644 index 00000000..29bd485d Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_249_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_24_100.jpg b/examples/data/Fruit360/raspberry/r_24_100.jpg new file mode 100644 index 00000000..cdb12084 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_24_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_250_100.jpg b/examples/data/Fruit360/raspberry/r_250_100.jpg new file mode 100644 index 00000000..124e2365 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_250_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_251_100.jpg b/examples/data/Fruit360/raspberry/r_251_100.jpg new file mode 100644 index 00000000..a80bc98a Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_251_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_253_100.jpg b/examples/data/Fruit360/raspberry/r_253_100.jpg new file mode 100644 index 00000000..6f47e690 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_253_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_254_100.jpg b/examples/data/Fruit360/raspberry/r_254_100.jpg new file mode 100644 index 00000000..3ca84bce Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_254_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_255_100.jpg b/examples/data/Fruit360/raspberry/r_255_100.jpg new file mode 100644 index 00000000..e1047bd4 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_255_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_256_100.jpg b/examples/data/Fruit360/raspberry/r_256_100.jpg new file mode 100644 index 00000000..04cfa928 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_256_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_257_100.jpg b/examples/data/Fruit360/raspberry/r_257_100.jpg new file mode 100644 index 00000000..4a38c7c6 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_257_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_258_100.jpg b/examples/data/Fruit360/raspberry/r_258_100.jpg new file mode 100644 index 00000000..e193a5fd Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_258_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_259_100.jpg b/examples/data/Fruit360/raspberry/r_259_100.jpg new file mode 100644 index 00000000..e9d415a8 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_259_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_260_100.jpg b/examples/data/Fruit360/raspberry/r_260_100.jpg new file mode 100644 index 00000000..68a8bf79 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_260_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_261_100.jpg b/examples/data/Fruit360/raspberry/r_261_100.jpg new file mode 100644 index 00000000..a67745ae Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_261_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_262_100.jpg b/examples/data/Fruit360/raspberry/r_262_100.jpg new file mode 100644 index 00000000..9ab48fd5 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_262_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_263_100.jpg b/examples/data/Fruit360/raspberry/r_263_100.jpg new file mode 100644 index 00000000..28a9e3ca Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_263_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_264_100.jpg b/examples/data/Fruit360/raspberry/r_264_100.jpg new file mode 100644 index 00000000..06e56af9 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_264_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_265_100.jpg b/examples/data/Fruit360/raspberry/r_265_100.jpg new file mode 100644 index 00000000..817f7940 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_265_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_266_100.jpg b/examples/data/Fruit360/raspberry/r_266_100.jpg new file mode 100644 index 00000000..f4db8888 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_266_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_267_100.jpg b/examples/data/Fruit360/raspberry/r_267_100.jpg new file mode 100644 index 00000000..017e5b87 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_267_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_268_100.jpg b/examples/data/Fruit360/raspberry/r_268_100.jpg new file mode 100644 index 00000000..11bd5570 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_268_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_269_100.jpg b/examples/data/Fruit360/raspberry/r_269_100.jpg new file mode 100644 index 00000000..777c1aad Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_269_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_270_100.jpg b/examples/data/Fruit360/raspberry/r_270_100.jpg new file mode 100644 index 00000000..a4cd42b5 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_270_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_271_100.jpg b/examples/data/Fruit360/raspberry/r_271_100.jpg new file mode 100644 index 00000000..0e8275a4 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_271_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_272_100.jpg b/examples/data/Fruit360/raspberry/r_272_100.jpg new file mode 100644 index 00000000..992a2572 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_272_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_273_100.jpg b/examples/data/Fruit360/raspberry/r_273_100.jpg new file mode 100644 index 00000000..0d33d256 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_273_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_274_100.jpg b/examples/data/Fruit360/raspberry/r_274_100.jpg new file mode 100644 index 00000000..6bc72bbd Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_274_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_275_100.jpg b/examples/data/Fruit360/raspberry/r_275_100.jpg new file mode 100644 index 00000000..49db9eda Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_275_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_276_100.jpg b/examples/data/Fruit360/raspberry/r_276_100.jpg new file mode 100644 index 00000000..32037db4 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_276_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_277_100.jpg b/examples/data/Fruit360/raspberry/r_277_100.jpg new file mode 100644 index 00000000..ca82511e Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_277_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_278_100.jpg b/examples/data/Fruit360/raspberry/r_278_100.jpg new file mode 100644 index 00000000..f372578d Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_278_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_279_100.jpg b/examples/data/Fruit360/raspberry/r_279_100.jpg new file mode 100644 index 00000000..62c2cb50 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_279_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_27_100.jpg b/examples/data/Fruit360/raspberry/r_27_100.jpg new file mode 100644 index 00000000..5c3678b3 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_27_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_280_100.jpg b/examples/data/Fruit360/raspberry/r_280_100.jpg new file mode 100644 index 00000000..735c4cfa Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_280_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_281_100.jpg b/examples/data/Fruit360/raspberry/r_281_100.jpg new file mode 100644 index 00000000..9ac1e4b3 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_281_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_282_100.jpg b/examples/data/Fruit360/raspberry/r_282_100.jpg new file mode 100644 index 00000000..3ee80379 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_282_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_283_100.jpg b/examples/data/Fruit360/raspberry/r_283_100.jpg new file mode 100644 index 00000000..c50a6f43 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_283_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_284_100.jpg b/examples/data/Fruit360/raspberry/r_284_100.jpg new file mode 100644 index 00000000..d62c90e2 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_284_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_285_100.jpg b/examples/data/Fruit360/raspberry/r_285_100.jpg new file mode 100644 index 00000000..07cd666b Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_285_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_286_100.jpg b/examples/data/Fruit360/raspberry/r_286_100.jpg new file mode 100644 index 00000000..e388df0a Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_286_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_287_100.jpg b/examples/data/Fruit360/raspberry/r_287_100.jpg new file mode 100644 index 00000000..bec54c84 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_287_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_288_100.jpg b/examples/data/Fruit360/raspberry/r_288_100.jpg new file mode 100644 index 00000000..6cbb9553 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_288_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_289_100.jpg b/examples/data/Fruit360/raspberry/r_289_100.jpg new file mode 100644 index 00000000..2d119ccc Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_289_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_290_100.jpg b/examples/data/Fruit360/raspberry/r_290_100.jpg new file mode 100644 index 00000000..49d2ddb3 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_290_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_291_100.jpg b/examples/data/Fruit360/raspberry/r_291_100.jpg new file mode 100644 index 00000000..5431bf1b Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_291_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_292_100.jpg b/examples/data/Fruit360/raspberry/r_292_100.jpg new file mode 100644 index 00000000..33d1a87a Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_292_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_293_100.jpg b/examples/data/Fruit360/raspberry/r_293_100.jpg new file mode 100644 index 00000000..493e22e0 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_293_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_294_100.jpg b/examples/data/Fruit360/raspberry/r_294_100.jpg new file mode 100644 index 00000000..a25e280a Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_294_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_296_100.jpg b/examples/data/Fruit360/raspberry/r_296_100.jpg new file mode 100644 index 00000000..b28461c9 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_296_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_297_100.jpg b/examples/data/Fruit360/raspberry/r_297_100.jpg new file mode 100644 index 00000000..fd89f09b Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_297_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_298_100.jpg b/examples/data/Fruit360/raspberry/r_298_100.jpg new file mode 100644 index 00000000..316d1161 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_298_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_299_100.jpg b/examples/data/Fruit360/raspberry/r_299_100.jpg new file mode 100644 index 00000000..059f24b9 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_299_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_2_100.jpg b/examples/data/Fruit360/raspberry/r_2_100.jpg new file mode 100644 index 00000000..94fa92af Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_2_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_300_100.jpg b/examples/data/Fruit360/raspberry/r_300_100.jpg new file mode 100644 index 00000000..b43e3de1 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_300_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_301_100.jpg b/examples/data/Fruit360/raspberry/r_301_100.jpg new file mode 100644 index 00000000..bad6eba2 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_301_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_307_100.jpg b/examples/data/Fruit360/raspberry/r_307_100.jpg new file mode 100644 index 00000000..785e6d28 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_307_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_318_100.jpg b/examples/data/Fruit360/raspberry/r_318_100.jpg new file mode 100644 index 00000000..2f8475ee Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_318_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_319_100.jpg b/examples/data/Fruit360/raspberry/r_319_100.jpg new file mode 100644 index 00000000..c0f78f1b Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_319_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_322_100.jpg b/examples/data/Fruit360/raspberry/r_322_100.jpg new file mode 100644 index 00000000..92c735b9 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_322_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_323_100.jpg b/examples/data/Fruit360/raspberry/r_323_100.jpg new file mode 100644 index 00000000..87a90b0f Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_323_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_324_100.jpg b/examples/data/Fruit360/raspberry/r_324_100.jpg new file mode 100644 index 00000000..3b28b518 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_324_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_326_100.jpg b/examples/data/Fruit360/raspberry/r_326_100.jpg new file mode 100644 index 00000000..1287da30 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_326_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_327_100.jpg b/examples/data/Fruit360/raspberry/r_327_100.jpg new file mode 100644 index 00000000..03fbb241 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_327_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_39_100.jpg b/examples/data/Fruit360/raspberry/r_39_100.jpg new file mode 100644 index 00000000..62425640 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_39_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_3_100.jpg b/examples/data/Fruit360/raspberry/r_3_100.jpg new file mode 100644 index 00000000..855ad664 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_3_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_40_100.jpg b/examples/data/Fruit360/raspberry/r_40_100.jpg new file mode 100644 index 00000000..5e40bbbc Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_40_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_41_100.jpg b/examples/data/Fruit360/raspberry/r_41_100.jpg new file mode 100644 index 00000000..93fc3a8e Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_41_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_43_100.jpg b/examples/data/Fruit360/raspberry/r_43_100.jpg new file mode 100644 index 00000000..614583cc Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_43_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_44_100.jpg b/examples/data/Fruit360/raspberry/r_44_100.jpg new file mode 100644 index 00000000..228d042a Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_44_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_45_100.jpg b/examples/data/Fruit360/raspberry/r_45_100.jpg new file mode 100644 index 00000000..4f7a629e Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_45_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_46_100.jpg b/examples/data/Fruit360/raspberry/r_46_100.jpg new file mode 100644 index 00000000..a93a6f25 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_46_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_47_100.jpg b/examples/data/Fruit360/raspberry/r_47_100.jpg new file mode 100644 index 00000000..e4d5dc41 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_47_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_48_100.jpg b/examples/data/Fruit360/raspberry/r_48_100.jpg new file mode 100644 index 00000000..5c02c630 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_48_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_49_100.jpg b/examples/data/Fruit360/raspberry/r_49_100.jpg new file mode 100644 index 00000000..99b9dd49 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_49_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_4_100.jpg b/examples/data/Fruit360/raspberry/r_4_100.jpg new file mode 100644 index 00000000..4a5acd0e Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_4_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_50_100.jpg b/examples/data/Fruit360/raspberry/r_50_100.jpg new file mode 100644 index 00000000..2eda65ce Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_50_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_51_100.jpg b/examples/data/Fruit360/raspberry/r_51_100.jpg new file mode 100644 index 00000000..52aeb5e5 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_51_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_52_100.jpg b/examples/data/Fruit360/raspberry/r_52_100.jpg new file mode 100644 index 00000000..3db58e84 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_52_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_53_100.jpg b/examples/data/Fruit360/raspberry/r_53_100.jpg new file mode 100644 index 00000000..47a23ad3 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_53_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_54_100.jpg b/examples/data/Fruit360/raspberry/r_54_100.jpg new file mode 100644 index 00000000..94bfe8e4 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_54_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_55_100.jpg b/examples/data/Fruit360/raspberry/r_55_100.jpg new file mode 100644 index 00000000..9389fa1f Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_55_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_56_100.jpg b/examples/data/Fruit360/raspberry/r_56_100.jpg new file mode 100644 index 00000000..051ca23e Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_56_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_57_100.jpg b/examples/data/Fruit360/raspberry/r_57_100.jpg new file mode 100644 index 00000000..33216cd1 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_57_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_58_100.jpg b/examples/data/Fruit360/raspberry/r_58_100.jpg new file mode 100644 index 00000000..99343c2e Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_58_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_59_100.jpg b/examples/data/Fruit360/raspberry/r_59_100.jpg new file mode 100644 index 00000000..b76f8348 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_59_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_5_100.jpg b/examples/data/Fruit360/raspberry/r_5_100.jpg new file mode 100644 index 00000000..58d9a111 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_5_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_60_100.jpg b/examples/data/Fruit360/raspberry/r_60_100.jpg new file mode 100644 index 00000000..342edcbf Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_60_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_61_100.jpg b/examples/data/Fruit360/raspberry/r_61_100.jpg new file mode 100644 index 00000000..699aa00a Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_61_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_62_100.jpg b/examples/data/Fruit360/raspberry/r_62_100.jpg new file mode 100644 index 00000000..d94baaca Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_62_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_63_100.jpg b/examples/data/Fruit360/raspberry/r_63_100.jpg new file mode 100644 index 00000000..7796d4ef Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_63_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_64_100.jpg b/examples/data/Fruit360/raspberry/r_64_100.jpg new file mode 100644 index 00000000..eb3cc5da Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_64_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_65_100.jpg b/examples/data/Fruit360/raspberry/r_65_100.jpg new file mode 100644 index 00000000..654080e8 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_65_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_66_100.jpg b/examples/data/Fruit360/raspberry/r_66_100.jpg new file mode 100644 index 00000000..e654b9ce Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_66_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_67_100.jpg b/examples/data/Fruit360/raspberry/r_67_100.jpg new file mode 100644 index 00000000..156044c1 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_67_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_68_100.jpg b/examples/data/Fruit360/raspberry/r_68_100.jpg new file mode 100644 index 00000000..532ef453 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_68_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_69_100.jpg b/examples/data/Fruit360/raspberry/r_69_100.jpg new file mode 100644 index 00000000..dda1279f Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_69_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_6_100.jpg b/examples/data/Fruit360/raspberry/r_6_100.jpg new file mode 100644 index 00000000..9e695dad Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_6_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_70_100.jpg b/examples/data/Fruit360/raspberry/r_70_100.jpg new file mode 100644 index 00000000..ed111bcb Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_70_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_71_100.jpg b/examples/data/Fruit360/raspberry/r_71_100.jpg new file mode 100644 index 00000000..ec261710 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_71_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_72_100.jpg b/examples/data/Fruit360/raspberry/r_72_100.jpg new file mode 100644 index 00000000..3d315550 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_72_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_73_100.jpg b/examples/data/Fruit360/raspberry/r_73_100.jpg new file mode 100644 index 00000000..45c9e31d Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_73_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_74_100.jpg b/examples/data/Fruit360/raspberry/r_74_100.jpg new file mode 100644 index 00000000..5b759cc7 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_74_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_75_100.jpg b/examples/data/Fruit360/raspberry/r_75_100.jpg new file mode 100644 index 00000000..50095f57 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_75_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_76_100.jpg b/examples/data/Fruit360/raspberry/r_76_100.jpg new file mode 100644 index 00000000..ed212f56 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_76_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_77_100.jpg b/examples/data/Fruit360/raspberry/r_77_100.jpg new file mode 100644 index 00000000..28e5c9ba Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_77_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_78_100.jpg b/examples/data/Fruit360/raspberry/r_78_100.jpg new file mode 100644 index 00000000..26daba57 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_78_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_79_100.jpg b/examples/data/Fruit360/raspberry/r_79_100.jpg new file mode 100644 index 00000000..4408326a Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_79_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_7_100.jpg b/examples/data/Fruit360/raspberry/r_7_100.jpg new file mode 100644 index 00000000..60ee81e0 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_7_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_80_100.jpg b/examples/data/Fruit360/raspberry/r_80_100.jpg new file mode 100644 index 00000000..a669da66 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_80_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_81_100.jpg b/examples/data/Fruit360/raspberry/r_81_100.jpg new file mode 100644 index 00000000..8fd251fa Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_81_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_82_100.jpg b/examples/data/Fruit360/raspberry/r_82_100.jpg new file mode 100644 index 00000000..f4ce960c Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_82_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_83_100.jpg b/examples/data/Fruit360/raspberry/r_83_100.jpg new file mode 100644 index 00000000..779d4046 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_83_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_84_100.jpg b/examples/data/Fruit360/raspberry/r_84_100.jpg new file mode 100644 index 00000000..3daa7c14 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_84_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_85_100.jpg b/examples/data/Fruit360/raspberry/r_85_100.jpg new file mode 100644 index 00000000..ed63fa44 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_85_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_86_100.jpg b/examples/data/Fruit360/raspberry/r_86_100.jpg new file mode 100644 index 00000000..e3123246 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_86_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_87_100.jpg b/examples/data/Fruit360/raspberry/r_87_100.jpg new file mode 100644 index 00000000..60648330 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_87_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_88_100.jpg b/examples/data/Fruit360/raspberry/r_88_100.jpg new file mode 100644 index 00000000..66c0341f Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_88_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_8_100.jpg b/examples/data/Fruit360/raspberry/r_8_100.jpg new file mode 100644 index 00000000..e831c36d Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_8_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_90_100.jpg b/examples/data/Fruit360/raspberry/r_90_100.jpg new file mode 100644 index 00000000..69d3ccdd Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_90_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_91_100.jpg b/examples/data/Fruit360/raspberry/r_91_100.jpg new file mode 100644 index 00000000..fa6be1b6 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_91_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_92_100.jpg b/examples/data/Fruit360/raspberry/r_92_100.jpg new file mode 100644 index 00000000..4bd66459 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_92_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_93_100.jpg b/examples/data/Fruit360/raspberry/r_93_100.jpg new file mode 100644 index 00000000..db9a7c0a Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_93_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_94_100.jpg b/examples/data/Fruit360/raspberry/r_94_100.jpg new file mode 100644 index 00000000..64b10646 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_94_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_97_100.jpg b/examples/data/Fruit360/raspberry/r_97_100.jpg new file mode 100644 index 00000000..826ab529 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_97_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_98_100.jpg b/examples/data/Fruit360/raspberry/r_98_100.jpg new file mode 100644 index 00000000..69bf9fd3 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_98_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_99_100.jpg b/examples/data/Fruit360/raspberry/r_99_100.jpg new file mode 100644 index 00000000..e22be672 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_99_100.jpg differ diff --git a/examples/data/Fruit360/raspberry/r_9_100.jpg b/examples/data/Fruit360/raspberry/r_9_100.jpg new file mode 100644 index 00000000..cdf74387 Binary files /dev/null and b/examples/data/Fruit360/raspberry/r_9_100.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/1.jpg b/examples/data/Skin_Cancer_Dataset/benign/1.jpg new file mode 100644 index 00000000..60cc36ea Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/1.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/10.jpg b/examples/data/Skin_Cancer_Dataset/benign/10.jpg new file mode 100644 index 00000000..bb8e34f0 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/10.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/100.jpg b/examples/data/Skin_Cancer_Dataset/benign/100.jpg new file mode 100644 index 00000000..3bbab935 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/100.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/11.jpg b/examples/data/Skin_Cancer_Dataset/benign/11.jpg new file mode 100644 index 00000000..3d8ecfed Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/11.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/12.jpg b/examples/data/Skin_Cancer_Dataset/benign/12.jpg new file mode 100644 index 00000000..f47c0ab8 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/12.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/13.jpg b/examples/data/Skin_Cancer_Dataset/benign/13.jpg new file mode 100644 index 00000000..d84106b2 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/13.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/14.jpg b/examples/data/Skin_Cancer_Dataset/benign/14.jpg new file mode 100644 index 00000000..f8442dd9 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/14.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/15.jpg b/examples/data/Skin_Cancer_Dataset/benign/15.jpg new file mode 100644 index 00000000..9d451f4c Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/15.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/16.jpg b/examples/data/Skin_Cancer_Dataset/benign/16.jpg new file mode 100644 index 00000000..f4f75b26 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/16.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/17.jpg b/examples/data/Skin_Cancer_Dataset/benign/17.jpg new file mode 100644 index 00000000..8f30c398 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/17.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/18.jpg b/examples/data/Skin_Cancer_Dataset/benign/18.jpg new file mode 100644 index 00000000..70061f6b Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/18.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/19.jpg b/examples/data/Skin_Cancer_Dataset/benign/19.jpg new file mode 100644 index 00000000..9605db88 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/19.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/2.jpg b/examples/data/Skin_Cancer_Dataset/benign/2.jpg new file mode 100644 index 00000000..c5095053 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/2.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/20.jpg b/examples/data/Skin_Cancer_Dataset/benign/20.jpg new file mode 100644 index 00000000..b6cc9c39 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/20.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/21.jpg b/examples/data/Skin_Cancer_Dataset/benign/21.jpg new file mode 100644 index 00000000..ca55f2a5 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/21.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/22.jpg b/examples/data/Skin_Cancer_Dataset/benign/22.jpg new file mode 100644 index 00000000..b928dfec Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/22.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/23.jpg b/examples/data/Skin_Cancer_Dataset/benign/23.jpg new file mode 100644 index 00000000..e113fb65 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/23.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/24.jpg b/examples/data/Skin_Cancer_Dataset/benign/24.jpg new file mode 100644 index 00000000..0262e9ba Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/24.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/25.jpg b/examples/data/Skin_Cancer_Dataset/benign/25.jpg new file mode 100644 index 00000000..e84d5296 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/25.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/26.jpg b/examples/data/Skin_Cancer_Dataset/benign/26.jpg new file mode 100644 index 00000000..603b8e15 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/26.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/27.jpg b/examples/data/Skin_Cancer_Dataset/benign/27.jpg new file mode 100644 index 00000000..8382e018 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/27.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/28.jpg b/examples/data/Skin_Cancer_Dataset/benign/28.jpg new file mode 100644 index 00000000..5b7b776b Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/28.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/29.jpg b/examples/data/Skin_Cancer_Dataset/benign/29.jpg new file mode 100644 index 00000000..371a95f3 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/29.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/3.jpg b/examples/data/Skin_Cancer_Dataset/benign/3.jpg new file mode 100644 index 00000000..01fcb1c5 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/3.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/30.jpg b/examples/data/Skin_Cancer_Dataset/benign/30.jpg new file mode 100644 index 00000000..b3ce109f Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/30.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/31.jpg b/examples/data/Skin_Cancer_Dataset/benign/31.jpg new file mode 100644 index 00000000..d322bbfd Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/31.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/32.jpg b/examples/data/Skin_Cancer_Dataset/benign/32.jpg new file mode 100644 index 00000000..50e83631 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/32.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/33.jpg b/examples/data/Skin_Cancer_Dataset/benign/33.jpg new file mode 100644 index 00000000..c040ab02 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/33.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/34.jpg b/examples/data/Skin_Cancer_Dataset/benign/34.jpg new file mode 100644 index 00000000..9e3628f7 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/34.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/35.jpg b/examples/data/Skin_Cancer_Dataset/benign/35.jpg new file mode 100644 index 00000000..eb7fa5e3 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/35.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/36.jpg b/examples/data/Skin_Cancer_Dataset/benign/36.jpg new file mode 100644 index 00000000..eca336bc Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/36.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/37.jpg b/examples/data/Skin_Cancer_Dataset/benign/37.jpg new file mode 100644 index 00000000..dd064867 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/37.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/38.jpg b/examples/data/Skin_Cancer_Dataset/benign/38.jpg new file mode 100644 index 00000000..13a0509a Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/38.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/39.jpg b/examples/data/Skin_Cancer_Dataset/benign/39.jpg new file mode 100644 index 00000000..ae848274 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/39.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/4.jpg b/examples/data/Skin_Cancer_Dataset/benign/4.jpg new file mode 100644 index 00000000..ac6cf50a Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/4.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/40.jpg b/examples/data/Skin_Cancer_Dataset/benign/40.jpg new file mode 100644 index 00000000..29ad202d Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/40.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/41.jpg b/examples/data/Skin_Cancer_Dataset/benign/41.jpg new file mode 100644 index 00000000..743a598f Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/41.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/42.jpg b/examples/data/Skin_Cancer_Dataset/benign/42.jpg new file mode 100644 index 00000000..e1815148 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/42.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/43.jpg b/examples/data/Skin_Cancer_Dataset/benign/43.jpg new file mode 100644 index 00000000..b596a600 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/43.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/44.jpg b/examples/data/Skin_Cancer_Dataset/benign/44.jpg new file mode 100644 index 00000000..00286ea4 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/44.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/45.jpg b/examples/data/Skin_Cancer_Dataset/benign/45.jpg new file mode 100644 index 00000000..79096acf Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/45.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/46.jpg b/examples/data/Skin_Cancer_Dataset/benign/46.jpg new file mode 100644 index 00000000..0c38c2c5 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/46.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/47.jpg b/examples/data/Skin_Cancer_Dataset/benign/47.jpg new file mode 100644 index 00000000..1d1de9ff Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/47.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/48.jpg b/examples/data/Skin_Cancer_Dataset/benign/48.jpg new file mode 100644 index 00000000..7317ed12 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/48.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/49.jpg b/examples/data/Skin_Cancer_Dataset/benign/49.jpg new file mode 100644 index 00000000..9d5f5120 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/49.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/5.jpg b/examples/data/Skin_Cancer_Dataset/benign/5.jpg new file mode 100644 index 00000000..7e3bbfff Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/5.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/50.jpg b/examples/data/Skin_Cancer_Dataset/benign/50.jpg new file mode 100644 index 00000000..c3d67430 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/50.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/51.jpg b/examples/data/Skin_Cancer_Dataset/benign/51.jpg new file mode 100644 index 00000000..ff175df1 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/51.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/52.jpg b/examples/data/Skin_Cancer_Dataset/benign/52.jpg new file mode 100644 index 00000000..3fbed5cf Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/52.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/53.jpg b/examples/data/Skin_Cancer_Dataset/benign/53.jpg new file mode 100644 index 00000000..1be8d1c4 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/53.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/54.jpg b/examples/data/Skin_Cancer_Dataset/benign/54.jpg new file mode 100644 index 00000000..87c93fe9 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/54.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/55.jpg b/examples/data/Skin_Cancer_Dataset/benign/55.jpg new file mode 100644 index 00000000..0cf1a764 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/55.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/56.jpg b/examples/data/Skin_Cancer_Dataset/benign/56.jpg new file mode 100644 index 00000000..5877a65a Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/56.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/57.jpg b/examples/data/Skin_Cancer_Dataset/benign/57.jpg new file mode 100644 index 00000000..6f3c81f0 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/57.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/58.jpg b/examples/data/Skin_Cancer_Dataset/benign/58.jpg new file mode 100644 index 00000000..7743f138 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/58.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/59.jpg b/examples/data/Skin_Cancer_Dataset/benign/59.jpg new file mode 100644 index 00000000..bd8239d7 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/59.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/6.jpg b/examples/data/Skin_Cancer_Dataset/benign/6.jpg new file mode 100644 index 00000000..ffa901e4 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/6.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/60.jpg b/examples/data/Skin_Cancer_Dataset/benign/60.jpg new file mode 100644 index 00000000..179b2957 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/60.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/61.jpg b/examples/data/Skin_Cancer_Dataset/benign/61.jpg new file mode 100644 index 00000000..1d0344e7 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/61.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/62.jpg b/examples/data/Skin_Cancer_Dataset/benign/62.jpg new file mode 100644 index 00000000..d33b4828 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/62.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/63.jpg b/examples/data/Skin_Cancer_Dataset/benign/63.jpg new file mode 100644 index 00000000..52375b1b Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/63.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/64.jpg b/examples/data/Skin_Cancer_Dataset/benign/64.jpg new file mode 100644 index 00000000..ce81a488 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/64.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/65.jpg b/examples/data/Skin_Cancer_Dataset/benign/65.jpg new file mode 100644 index 00000000..9753e813 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/65.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/66.jpg b/examples/data/Skin_Cancer_Dataset/benign/66.jpg new file mode 100644 index 00000000..c0127e69 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/66.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/67.jpg b/examples/data/Skin_Cancer_Dataset/benign/67.jpg new file mode 100644 index 00000000..d119d00e Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/67.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/68.jpg b/examples/data/Skin_Cancer_Dataset/benign/68.jpg new file mode 100644 index 00000000..c68f5522 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/68.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/69.jpg b/examples/data/Skin_Cancer_Dataset/benign/69.jpg new file mode 100644 index 00000000..f6cda529 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/69.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/7.jpg b/examples/data/Skin_Cancer_Dataset/benign/7.jpg new file mode 100644 index 00000000..0ab462cb Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/7.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/70.jpg b/examples/data/Skin_Cancer_Dataset/benign/70.jpg new file mode 100644 index 00000000..dbfbefb2 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/70.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/71.jpg b/examples/data/Skin_Cancer_Dataset/benign/71.jpg new file mode 100644 index 00000000..559456af Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/71.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/72.jpg b/examples/data/Skin_Cancer_Dataset/benign/72.jpg new file mode 100644 index 00000000..0921796b Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/72.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/73.jpg b/examples/data/Skin_Cancer_Dataset/benign/73.jpg new file mode 100644 index 00000000..72aaaf57 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/73.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/74.jpg b/examples/data/Skin_Cancer_Dataset/benign/74.jpg new file mode 100644 index 00000000..dd7734d4 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/74.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/75.jpg b/examples/data/Skin_Cancer_Dataset/benign/75.jpg new file mode 100644 index 00000000..e83da995 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/75.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/76.jpg b/examples/data/Skin_Cancer_Dataset/benign/76.jpg new file mode 100644 index 00000000..4190c79d Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/76.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/77.jpg b/examples/data/Skin_Cancer_Dataset/benign/77.jpg new file mode 100644 index 00000000..143eb7fc Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/77.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/78.jpg b/examples/data/Skin_Cancer_Dataset/benign/78.jpg new file mode 100644 index 00000000..766a82e4 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/78.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/79.jpg b/examples/data/Skin_Cancer_Dataset/benign/79.jpg new file mode 100644 index 00000000..f5bc6ba0 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/79.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/8.jpg b/examples/data/Skin_Cancer_Dataset/benign/8.jpg new file mode 100644 index 00000000..b4aaa209 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/8.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/80.jpg b/examples/data/Skin_Cancer_Dataset/benign/80.jpg new file mode 100644 index 00000000..e8a12fa3 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/80.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/81.jpg b/examples/data/Skin_Cancer_Dataset/benign/81.jpg new file mode 100644 index 00000000..d2729be0 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/81.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/82.jpg b/examples/data/Skin_Cancer_Dataset/benign/82.jpg new file mode 100644 index 00000000..bdcee129 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/82.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/83.jpg b/examples/data/Skin_Cancer_Dataset/benign/83.jpg new file mode 100644 index 00000000..6a891852 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/83.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/84.jpg b/examples/data/Skin_Cancer_Dataset/benign/84.jpg new file mode 100644 index 00000000..ae3330e7 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/84.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/85.jpg b/examples/data/Skin_Cancer_Dataset/benign/85.jpg new file mode 100644 index 00000000..63ae63c0 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/85.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/86.jpg b/examples/data/Skin_Cancer_Dataset/benign/86.jpg new file mode 100644 index 00000000..3af6247a Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/86.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/87.jpg b/examples/data/Skin_Cancer_Dataset/benign/87.jpg new file mode 100644 index 00000000..ff94ca90 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/87.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/88.jpg b/examples/data/Skin_Cancer_Dataset/benign/88.jpg new file mode 100644 index 00000000..06295087 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/88.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/89.jpg b/examples/data/Skin_Cancer_Dataset/benign/89.jpg new file mode 100644 index 00000000..e1b55e27 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/89.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/9.jpg b/examples/data/Skin_Cancer_Dataset/benign/9.jpg new file mode 100644 index 00000000..87f6972e Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/9.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/90.jpg b/examples/data/Skin_Cancer_Dataset/benign/90.jpg new file mode 100644 index 00000000..589db4c3 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/90.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/91.jpg b/examples/data/Skin_Cancer_Dataset/benign/91.jpg new file mode 100644 index 00000000..b8879643 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/91.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/92.jpg b/examples/data/Skin_Cancer_Dataset/benign/92.jpg new file mode 100644 index 00000000..f8106900 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/92.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/93.jpg b/examples/data/Skin_Cancer_Dataset/benign/93.jpg new file mode 100644 index 00000000..4a90dd22 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/93.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/94.jpg b/examples/data/Skin_Cancer_Dataset/benign/94.jpg new file mode 100644 index 00000000..a5da9ece Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/94.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/95.jpg b/examples/data/Skin_Cancer_Dataset/benign/95.jpg new file mode 100644 index 00000000..3bc0fed2 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/95.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/96.jpg b/examples/data/Skin_Cancer_Dataset/benign/96.jpg new file mode 100644 index 00000000..5e59ce9f Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/96.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/97.jpg b/examples/data/Skin_Cancer_Dataset/benign/97.jpg new file mode 100644 index 00000000..018def70 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/97.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/98.jpg b/examples/data/Skin_Cancer_Dataset/benign/98.jpg new file mode 100644 index 00000000..2edecb76 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/98.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/benign/99.jpg b/examples/data/Skin_Cancer_Dataset/benign/99.jpg new file mode 100644 index 00000000..438b3cb9 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/benign/99.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/1.jpg b/examples/data/Skin_Cancer_Dataset/malignant/1.jpg new file mode 100644 index 00000000..c73d797a Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/1.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/10.jpg b/examples/data/Skin_Cancer_Dataset/malignant/10.jpg new file mode 100644 index 00000000..c5c98c8f Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/10.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/100.jpg b/examples/data/Skin_Cancer_Dataset/malignant/100.jpg new file mode 100644 index 00000000..e9e1a347 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/100.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/11.jpg b/examples/data/Skin_Cancer_Dataset/malignant/11.jpg new file mode 100644 index 00000000..077a2c52 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/11.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/12.jpg b/examples/data/Skin_Cancer_Dataset/malignant/12.jpg new file mode 100644 index 00000000..daef157a Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/12.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/13.jpg b/examples/data/Skin_Cancer_Dataset/malignant/13.jpg new file mode 100644 index 00000000..a47cf529 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/13.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/14.jpg b/examples/data/Skin_Cancer_Dataset/malignant/14.jpg new file mode 100644 index 00000000..bdf815d3 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/14.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/15.jpg b/examples/data/Skin_Cancer_Dataset/malignant/15.jpg new file mode 100644 index 00000000..b8573bf3 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/15.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/16.jpg b/examples/data/Skin_Cancer_Dataset/malignant/16.jpg new file mode 100644 index 00000000..83ea997c Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/16.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/17.jpg b/examples/data/Skin_Cancer_Dataset/malignant/17.jpg new file mode 100644 index 00000000..90dab86f Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/17.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/18.jpg b/examples/data/Skin_Cancer_Dataset/malignant/18.jpg new file mode 100644 index 00000000..b75bd28d Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/18.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/19.jpg b/examples/data/Skin_Cancer_Dataset/malignant/19.jpg new file mode 100644 index 00000000..4dd30bb3 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/19.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/2.jpg b/examples/data/Skin_Cancer_Dataset/malignant/2.jpg new file mode 100644 index 00000000..a550d6a1 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/2.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/20.jpg b/examples/data/Skin_Cancer_Dataset/malignant/20.jpg new file mode 100644 index 00000000..8aca62fa Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/20.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/21.jpg b/examples/data/Skin_Cancer_Dataset/malignant/21.jpg new file mode 100644 index 00000000..5e0159e2 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/21.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/22.jpg b/examples/data/Skin_Cancer_Dataset/malignant/22.jpg new file mode 100644 index 00000000..a6b007f4 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/22.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/23.jpg b/examples/data/Skin_Cancer_Dataset/malignant/23.jpg new file mode 100644 index 00000000..0a346e41 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/23.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/24.jpg b/examples/data/Skin_Cancer_Dataset/malignant/24.jpg new file mode 100644 index 00000000..3c3d9e7b Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/24.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/25.jpg b/examples/data/Skin_Cancer_Dataset/malignant/25.jpg new file mode 100644 index 00000000..6bd74dbf Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/25.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/26.jpg b/examples/data/Skin_Cancer_Dataset/malignant/26.jpg new file mode 100644 index 00000000..4c42fc10 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/26.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/27.jpg b/examples/data/Skin_Cancer_Dataset/malignant/27.jpg new file mode 100644 index 00000000..046d59e0 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/27.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/28.jpg b/examples/data/Skin_Cancer_Dataset/malignant/28.jpg new file mode 100644 index 00000000..eff036d1 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/28.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/29.jpg b/examples/data/Skin_Cancer_Dataset/malignant/29.jpg new file mode 100644 index 00000000..83bb7019 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/29.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/3.jpg b/examples/data/Skin_Cancer_Dataset/malignant/3.jpg new file mode 100644 index 00000000..b3287c68 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/3.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/30.jpg b/examples/data/Skin_Cancer_Dataset/malignant/30.jpg new file mode 100644 index 00000000..dbe824e4 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/30.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/31.jpg b/examples/data/Skin_Cancer_Dataset/malignant/31.jpg new file mode 100644 index 00000000..1d214ead Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/31.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/32.jpg b/examples/data/Skin_Cancer_Dataset/malignant/32.jpg new file mode 100644 index 00000000..306e049f Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/32.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/33.jpg b/examples/data/Skin_Cancer_Dataset/malignant/33.jpg new file mode 100644 index 00000000..16b20b36 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/33.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/34.jpg b/examples/data/Skin_Cancer_Dataset/malignant/34.jpg new file mode 100644 index 00000000..905918f6 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/34.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/35.jpg b/examples/data/Skin_Cancer_Dataset/malignant/35.jpg new file mode 100644 index 00000000..5000518f Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/35.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/36.jpg b/examples/data/Skin_Cancer_Dataset/malignant/36.jpg new file mode 100644 index 00000000..f53f9521 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/36.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/37.jpg b/examples/data/Skin_Cancer_Dataset/malignant/37.jpg new file mode 100644 index 00000000..2573a86d Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/37.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/38.jpg b/examples/data/Skin_Cancer_Dataset/malignant/38.jpg new file mode 100644 index 00000000..a12966c7 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/38.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/39.jpg b/examples/data/Skin_Cancer_Dataset/malignant/39.jpg new file mode 100644 index 00000000..20feb075 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/39.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/4.jpg b/examples/data/Skin_Cancer_Dataset/malignant/4.jpg new file mode 100644 index 00000000..014ab216 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/4.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/40.jpg b/examples/data/Skin_Cancer_Dataset/malignant/40.jpg new file mode 100644 index 00000000..a2d35b73 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/40.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/41.jpg b/examples/data/Skin_Cancer_Dataset/malignant/41.jpg new file mode 100644 index 00000000..abcc1c2a Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/41.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/42.jpg b/examples/data/Skin_Cancer_Dataset/malignant/42.jpg new file mode 100644 index 00000000..a61692d7 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/42.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/43.jpg b/examples/data/Skin_Cancer_Dataset/malignant/43.jpg new file mode 100644 index 00000000..b047bb90 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/43.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/44.jpg b/examples/data/Skin_Cancer_Dataset/malignant/44.jpg new file mode 100644 index 00000000..fe2adb89 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/44.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/45.jpg b/examples/data/Skin_Cancer_Dataset/malignant/45.jpg new file mode 100644 index 00000000..f7524fcc Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/45.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/46.jpg b/examples/data/Skin_Cancer_Dataset/malignant/46.jpg new file mode 100644 index 00000000..c86111e3 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/46.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/47.jpg b/examples/data/Skin_Cancer_Dataset/malignant/47.jpg new file mode 100644 index 00000000..8d4cfe9b Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/47.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/48.jpg b/examples/data/Skin_Cancer_Dataset/malignant/48.jpg new file mode 100644 index 00000000..f96a51c8 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/48.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/49.jpg b/examples/data/Skin_Cancer_Dataset/malignant/49.jpg new file mode 100644 index 00000000..125434ec Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/49.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/5.jpg b/examples/data/Skin_Cancer_Dataset/malignant/5.jpg new file mode 100644 index 00000000..f62e5379 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/5.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/50.jpg b/examples/data/Skin_Cancer_Dataset/malignant/50.jpg new file mode 100644 index 00000000..ef2d4a1d Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/50.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/51.jpg b/examples/data/Skin_Cancer_Dataset/malignant/51.jpg new file mode 100644 index 00000000..c9781199 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/51.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/52.jpg b/examples/data/Skin_Cancer_Dataset/malignant/52.jpg new file mode 100644 index 00000000..89b2748a Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/52.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/53.jpg b/examples/data/Skin_Cancer_Dataset/malignant/53.jpg new file mode 100644 index 00000000..216fddd0 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/53.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/54.jpg b/examples/data/Skin_Cancer_Dataset/malignant/54.jpg new file mode 100644 index 00000000..93e2f170 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/54.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/55.jpg b/examples/data/Skin_Cancer_Dataset/malignant/55.jpg new file mode 100644 index 00000000..7b831874 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/55.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/56.jpg b/examples/data/Skin_Cancer_Dataset/malignant/56.jpg new file mode 100644 index 00000000..42c02b7a Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/56.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/57.jpg b/examples/data/Skin_Cancer_Dataset/malignant/57.jpg new file mode 100644 index 00000000..e5fba7eb Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/57.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/58.jpg b/examples/data/Skin_Cancer_Dataset/malignant/58.jpg new file mode 100644 index 00000000..a7435e0b Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/58.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/59.jpg b/examples/data/Skin_Cancer_Dataset/malignant/59.jpg new file mode 100644 index 00000000..e16364cb Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/59.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/6.jpg b/examples/data/Skin_Cancer_Dataset/malignant/6.jpg new file mode 100644 index 00000000..fa68cb09 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/6.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/60.jpg b/examples/data/Skin_Cancer_Dataset/malignant/60.jpg new file mode 100644 index 00000000..65f951e1 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/60.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/61.jpg b/examples/data/Skin_Cancer_Dataset/malignant/61.jpg new file mode 100644 index 00000000..3e0a7735 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/61.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/62.jpg b/examples/data/Skin_Cancer_Dataset/malignant/62.jpg new file mode 100644 index 00000000..16712fd5 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/62.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/63.jpg b/examples/data/Skin_Cancer_Dataset/malignant/63.jpg new file mode 100644 index 00000000..5d8c4a8d Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/63.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/64.jpg b/examples/data/Skin_Cancer_Dataset/malignant/64.jpg new file mode 100644 index 00000000..f63c8519 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/64.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/65.jpg b/examples/data/Skin_Cancer_Dataset/malignant/65.jpg new file mode 100644 index 00000000..a3e522d1 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/65.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/66.jpg b/examples/data/Skin_Cancer_Dataset/malignant/66.jpg new file mode 100644 index 00000000..708590b0 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/66.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/67.jpg b/examples/data/Skin_Cancer_Dataset/malignant/67.jpg new file mode 100644 index 00000000..ec82ab26 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/67.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/68.jpg b/examples/data/Skin_Cancer_Dataset/malignant/68.jpg new file mode 100644 index 00000000..0b01e895 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/68.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/69.jpg b/examples/data/Skin_Cancer_Dataset/malignant/69.jpg new file mode 100644 index 00000000..e8172a57 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/69.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/7.jpg b/examples/data/Skin_Cancer_Dataset/malignant/7.jpg new file mode 100644 index 00000000..7ff0a120 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/7.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/70.jpg b/examples/data/Skin_Cancer_Dataset/malignant/70.jpg new file mode 100644 index 00000000..3b2c3527 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/70.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/71.jpg b/examples/data/Skin_Cancer_Dataset/malignant/71.jpg new file mode 100644 index 00000000..389bf5fe Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/71.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/72.jpg b/examples/data/Skin_Cancer_Dataset/malignant/72.jpg new file mode 100644 index 00000000..54447c73 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/72.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/73.jpg b/examples/data/Skin_Cancer_Dataset/malignant/73.jpg new file mode 100644 index 00000000..89c1036b Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/73.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/74.jpg b/examples/data/Skin_Cancer_Dataset/malignant/74.jpg new file mode 100644 index 00000000..cc8500ba Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/74.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/75.jpg b/examples/data/Skin_Cancer_Dataset/malignant/75.jpg new file mode 100644 index 00000000..71a3329c Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/75.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/76.jpg b/examples/data/Skin_Cancer_Dataset/malignant/76.jpg new file mode 100644 index 00000000..760e1fe9 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/76.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/77.jpg b/examples/data/Skin_Cancer_Dataset/malignant/77.jpg new file mode 100644 index 00000000..70ecf1e4 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/77.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/78.jpg b/examples/data/Skin_Cancer_Dataset/malignant/78.jpg new file mode 100644 index 00000000..6d08f872 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/78.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/79.jpg b/examples/data/Skin_Cancer_Dataset/malignant/79.jpg new file mode 100644 index 00000000..cfdbc096 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/79.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/8.jpg b/examples/data/Skin_Cancer_Dataset/malignant/8.jpg new file mode 100644 index 00000000..72b6b0cb Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/8.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/80.jpg b/examples/data/Skin_Cancer_Dataset/malignant/80.jpg new file mode 100644 index 00000000..292e4ec8 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/80.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/81.jpg b/examples/data/Skin_Cancer_Dataset/malignant/81.jpg new file mode 100644 index 00000000..1a484c9d Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/81.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/82.jpg b/examples/data/Skin_Cancer_Dataset/malignant/82.jpg new file mode 100644 index 00000000..3583976e Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/82.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/83.jpg b/examples/data/Skin_Cancer_Dataset/malignant/83.jpg new file mode 100644 index 00000000..1c461ab4 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/83.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/84.jpg b/examples/data/Skin_Cancer_Dataset/malignant/84.jpg new file mode 100644 index 00000000..8c424b69 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/84.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/85.jpg b/examples/data/Skin_Cancer_Dataset/malignant/85.jpg new file mode 100644 index 00000000..f9124372 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/85.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/86.jpg b/examples/data/Skin_Cancer_Dataset/malignant/86.jpg new file mode 100644 index 00000000..7fa2f5e3 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/86.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/87.jpg b/examples/data/Skin_Cancer_Dataset/malignant/87.jpg new file mode 100644 index 00000000..39395793 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/87.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/88.jpg b/examples/data/Skin_Cancer_Dataset/malignant/88.jpg new file mode 100644 index 00000000..1a816706 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/88.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/89.jpg b/examples/data/Skin_Cancer_Dataset/malignant/89.jpg new file mode 100644 index 00000000..b8976efd Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/89.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/9.jpg b/examples/data/Skin_Cancer_Dataset/malignant/9.jpg new file mode 100644 index 00000000..f0603217 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/9.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/90.jpg b/examples/data/Skin_Cancer_Dataset/malignant/90.jpg new file mode 100644 index 00000000..1fe6e34a Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/90.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/91.jpg b/examples/data/Skin_Cancer_Dataset/malignant/91.jpg new file mode 100644 index 00000000..6f948d16 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/91.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/92.jpg b/examples/data/Skin_Cancer_Dataset/malignant/92.jpg new file mode 100644 index 00000000..b7045248 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/92.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/93.jpg b/examples/data/Skin_Cancer_Dataset/malignant/93.jpg new file mode 100644 index 00000000..550a33ef Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/93.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/94.jpg b/examples/data/Skin_Cancer_Dataset/malignant/94.jpg new file mode 100644 index 00000000..5958099a Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/94.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/95.jpg b/examples/data/Skin_Cancer_Dataset/malignant/95.jpg new file mode 100644 index 00000000..036cd43b Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/95.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/96.jpg b/examples/data/Skin_Cancer_Dataset/malignant/96.jpg new file mode 100644 index 00000000..e24ffb59 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/96.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/97.jpg b/examples/data/Skin_Cancer_Dataset/malignant/97.jpg new file mode 100644 index 00000000..6b6b8777 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/97.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/98.jpg b/examples/data/Skin_Cancer_Dataset/malignant/98.jpg new file mode 100644 index 00000000..f80b0515 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/98.jpg differ diff --git a/examples/data/Skin_Cancer_Dataset/malignant/99.jpg b/examples/data/Skin_Cancer_Dataset/malignant/99.jpg new file mode 100644 index 00000000..c0db0926 Binary files /dev/null and b/examples/data/Skin_Cancer_Dataset/malignant/99.jpg differ diff --git a/examples/data/dataset_features.npy b/examples/data/dataset_features.npy new file mode 100644 index 00000000..b4d6039b Binary files /dev/null and b/examples/data/dataset_features.npy differ diff --git a/examples/data/dataset_inputs.npy b/examples/data/dataset_inputs.npy new file mode 100644 index 00000000..c30f0e2b Binary files /dev/null and b/examples/data/dataset_inputs.npy differ diff --git a/examples/data/dataset_outputs.npy b/examples/data/dataset_outputs.npy new file mode 100644 index 00000000..9f9e5187 Binary files /dev/null and b/examples/data/dataset_outputs.npy differ diff --git a/examples/data/outputs.npy b/examples/data/outputs.npy new file mode 100644 index 00000000..3523b07b Binary files /dev/null and b/examples/data/outputs.npy differ diff --git a/example.py b/examples/example.py similarity index 65% rename from example.py rename to examples/example.py index 3ecd8e69..72d895cd 100644 --- a/example.py +++ b/examples/example.py @@ -11,7 +11,7 @@ function_inputs = [4,-2,3.5,5,-11,-4.7] # Function inputs. desired_output = 44 # Function output. -def fitness_func(solution, solution_idx): +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 @@ -25,9 +25,9 @@ def fitness_func(solution, solution_idx): last_fitness = 0 def on_generation(ga_instance): global last_fitness - print("Generation = {generation}".format(generation=ga_instance.generations_completed)) - print("Fitness = {fitness}".format(fitness=ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1])) - print("Change = {change}".format(change=ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1] - 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, @@ -44,15 +44,15 @@ def on_generation(ga_instance): # Returning the details of the best solution. solution, solution_fitness, solution_idx = ga_instance.best_solution(ga_instance.last_generation_fitness) -print("Parameters of the best solution : {solution}".format(solution=solution)) -print("Fitness value of the best solution = {solution_fitness}".format(solution_fitness=solution_fitness)) -print("Index of the best solution : {solution_idx}".format(solution_idx=solution_idx)) +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("Predicted output based on the best solution : {prediction}".format(prediction=prediction)) +print(f"Predicted output based on the best solution : {prediction}") if ga_instance.best_solution_generation != -1: - print("Best fitness value reached after {best_solution_generation} generations.".format(best_solution_generation=ga_instance.best_solution_generation)) + 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. diff --git a/example_custom_operators.py b/examples/example_custom_operators.py similarity index 91% rename from example_custom_operators.py rename to examples/example_custom_operators.py index 0bb452da..84949aa3 100644 --- a/example_custom_operators.py +++ b/examples/example_custom_operators.py @@ -13,7 +13,7 @@ equation_inputs = [4,-2,3.5] desired_output = 44 -def fitness_func(solution, solution_idx): +def fitness_func(ga_instance, solution, solution_idx): output = numpy.sum(solution * equation_inputs) fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) @@ -31,7 +31,7 @@ def parent_selection_func(fitness, num_parents, ga_instance): for parent_num in range(num_parents): parents[parent_num, :] = ga_instance.population[fitness_sorted[parent_num], :].copy() - return parents, fitness_sorted[:num_parents] + return parents, numpy.array(fitness_sorted[:num_parents]) def crossover_func(parents, offspring_size, ga_instance): # This is single-point crossover. @@ -41,7 +41,7 @@ def crossover_func(parents, offspring_size, ga_instance): 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])) + random_split_point = numpy.random.choice(range(offspring_size[1])) parent1[random_split_point:] = parent2[random_split_point:] @@ -55,7 +55,7 @@ def mutation_func(offspring, ga_instance): # This is random mutation that mutates a single gene. for chromosome_idx in range(offspring.shape[0]): # Make some random changes in 1 or more genes. - random_gene_idx = numpy.random.choice(range(offspring.shape[0])) + random_gene_idx = numpy.random.choice(range(offspring.shape[1])) offspring[chromosome_idx, random_gene_idx] += numpy.random.random() @@ -71,4 +71,4 @@ def mutation_func(offspring, ga_instance): mutation_type=mutation_func) ga_instance.run() -ga_instance.plot_fitness() \ No newline at end of file +ga_instance.plot_fitness() diff --git a/examples/example_dynamic_population_size.py b/examples/example_dynamic_population_size.py new file mode 100644 index 00000000..8a724b3b --- /dev/null +++ b/examples/example_dynamic_population_size.py @@ -0,0 +1,85 @@ +import pygad +import numpy + +""" +This is an example to dynamically change the population size (i.e. number of solutions/chromosomes per population) during runtime. + +The user has to carefully inspect the parameters and instance attributes to select those that must be changed to be consistent with the new population size. +Check this link for more information: https://pygad.readthedocs.io/en/latest/pygad_more.html#change-population-size-during-runtime +""" + +def update_GA(ga_i, + pop_size): + """ + Update the parameters and instance attributes to match the new population size. + + Parameters + ---------- + ga_i : TYPE + The pygad.GA instance. + pop_size : TYPE + The new population size. + + Returns + ------- + None. + """ + + ga_i.pop_size = pop_size + ga_i.sol_per_pop = ga_i.pop_size[0] + ga_i.num_parents_mating = int(ga_i.pop_size[0]/2) + + # Calculate the new value for the num_offspring parameter. + if ga_i.keep_elitism != 0: + ga_i.num_offspring = ga_i.sol_per_pop - ga_i.keep_elitism + elif ga_i.keep_parents != 0: + if ga_i.keep_parents == -1: + ga_i.num_offspring = ga_i.sol_per_pop - ga_i.num_parents_mating + else: + ga_i.num_offspring = ga_i.sol_per_pop - ga_i.keep_parents + + ga_i.num_genes = ga_i.pop_size[1] + ga_i.population = numpy.random.uniform(low=ga_i.init_range_low, + high=ga_i.init_range_low, + size=ga_i.pop_size) + fitness = [] + for solution, solution_idx in enumerate(ga_i.population): + fitness.append(fitness_func(ga_i, solution, solution_idx)) + ga_i.last_generation_fitness = numpy.array(fitness) + parents, parents_fitness = ga_i.steady_state_selection(ga_i.last_generation_fitness, + ga_i.num_parents_mating) + ga_i.last_generation_elitism = parents[:ga_i.keep_elitism] + ga_i.last_generation_elitism_indices = parents_fitness[:ga_i.keep_elitism] + + ga_i.last_generation_parents = parents + ga_i.last_generation_parents_indices = parents_fitness + +def fitness_func(ga_instance, solution, solution_idx): + return numpy.sum(solution) + +def on_generation(ga_i): + # The population starts with 20 solutions. + print(ga_i.generations_completed, ga_i.population.shape) + # At generation 15, set the population size to 30 solutions and 10 genes. + if ga_i.generations_completed >= 15: + ga_i.pop_size = (30, 10) + update_GA(ga_i=ga_i, + pop_size=(30, 10)) + # At generation 10, set the population size to 15 solutions and 8 genes. + elif ga_i.generations_completed >= 10: + update_GA(ga_i=ga_i, + pop_size=(15, 8)) + # At generation 5, set the population size to 10 solutions and 3 genes. + elif ga_i.generations_completed >= 5: + update_GA(ga_i=ga_i, + pop_size=(10, 3)) + +ga_instance = pygad.GA(num_generations=20, + sol_per_pop=20, + num_genes=6, + num_parents_mating=10, + fitness_func=fitness_func, + on_generation=on_generation) + +ga_instance.run() + diff --git a/examples/example_fitness_wrapper.py b/examples/example_fitness_wrapper.py new file mode 100644 index 00000000..6a9acf11 --- /dev/null +++ b/examples/example_fitness_wrapper.py @@ -0,0 +1,41 @@ +import pygad +import numpy + +""" +All the callback functions/methods in PyGAD have limits in the number of arguments passed. +For example, the fitness function accepts only 3 arguments: + 1. The pygad.GA instance. + 2. The solution(s). + 3. The index (indices) of the passed solution(s). +If it is necessary to pass extra arguments to the fitness function, for example, then follow these steps: + 1. Create a wrapper function that accepts only the number of arguments meeded by PyGAD. + 2. Define the extra arguments in the body of the wrapper function. + 3. Create an inner fitness function inside the wrapper function with whatever extra arguments needed. + 4. Call the inner fitness function from the wrapper function while passing the extra arguments. + +This is an example that passes a list ([10, 20, 30]) to the inner fitness function. The list has 3 numbers. +A number is randomly selected from the list and added to the calculated fitness. +""" + +function_inputs = [4,-2,3.5,5,-11,-4.7] +desired_output = 44 + +def fitness_func_wrapper(ga_instanse, solution, solution_idx): + def fitness_func(ga_instanse, solution, solution_idx, *args): + output = numpy.sum(solution*function_inputs) + output += numpy.random.choice(args) + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + return fitness + args = [10, 20, 30] + fitness = fitness_func(ga_instanse, solution, solution_idx, *args) + return fitness + +ga_instance = pygad.GA(num_generations=3, + num_parents_mating=5, + fitness_func=fitness_func_wrapper, + sol_per_pop=10, + num_genes=len(function_inputs), + suppress_warnings=True) + +ga_instance.run() +ga_instance.plot_fitness() diff --git a/examples/example_logger.py b/examples/example_logger.py new file mode 100644 index 00000000..bbf44e97 --- /dev/null +++ b/examples/example_logger.py @@ -0,0 +1,45 @@ +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() diff --git a/examples/example_multi_objective.py b/examples/example_multi_objective.py new file mode 100644 index 00000000..048248eb --- /dev/null +++ b/examples/example_multi_objective.py @@ -0,0 +1,72 @@ +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 # 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_inputs1) + +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, + parent_selection_type='nsga2', + on_generation=on_generation) + +# Running the GA to optimize the parameters of the function. +ga_instance.run() + +ga_instance.plot_fitness(label=['Obj 1', 'Obj 2']) +ga_instance.plot_pareto_front_curve() + +# 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_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}") + +if ga_instance.best_solution_generation != -1: + print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.") + diff --git a/examples/example_parallel_processing.py b/examples/example_parallel_processing.py new file mode 100644 index 00000000..9efd1ea5 --- /dev/null +++ b/examples/example_parallel_processing.py @@ -0,0 +1,39 @@ +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): + output = numpy.sum(solution*function_inputs) + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + return fitness + +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] + +if __name__ == '__main__': + ga_instance = pygad.GA(num_generations=100, + num_parents_mating=10, + sol_per_pop=20, + num_genes=len(function_inputs), + fitness_func=fitness_func, + on_generation=on_generation, + # parallel_processing=['process', 2], + parallel_processing=['thread', 2] + ) + + # Running the GA to optimize the parameters of the function. + ga_instance.run() + + # 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}") + diff --git a/examples/example_travelling_salesman.ipynb b/examples/example_travelling_salesman.ipynb new file mode 100644 index 00000000..f59d57e6 --- /dev/null +++ b/examples/example_travelling_salesman.ipynb @@ -0,0 +1,26535 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "95ba26f7" + }, + "source": [ + "# The Travelling Coffee Drinker - Genetic Algorithm\n", + "\n", + "Solving a travelling salesman problem for United Kingdom Starbucks Cafés" + ], + "id": "95ba26f7" + }, + { + "cell_type": "markdown", + "source": [ + "## 1. Load and transform data\n", + "\n", + "The data comes from Kaggle, which is accessed using the API wrapper.\n", + "\n", + "The transformation needed is just to filter only GB Starbucks restaurants with a valid lon/lat pair." + ], + "metadata": { + "id": "sEtmfvLsvMC2" + }, + "id": "sEtmfvLsvMC2" + }, + { + "cell_type": "code", + "execution_count": 101, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "_HBlGrIyYa7G", + "outputId": "cf89fc50-e566-4fe2-f54c-178b9d6e1acd" + }, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n", + "Requirement already satisfied: pygad==2.17 in /usr/local/lib/python3.7/dist-packages (2.17.0)\n", + "Requirement already satisfied: numpy in /usr/local/lib/python3.7/dist-packages (from pygad==2.17) (1.21.6)\n", + "Requirement already satisfied: matplotlib in /usr/local/lib/python3.7/dist-packages (from pygad==2.17) (3.2.2)\n", + "Requirement already satisfied: python-dateutil>=2.1 in /usr/local/lib/python3.7/dist-packages (from matplotlib->pygad==2.17) (2.8.2)\n", + "Requirement already satisfied: kiwisolver>=1.0.1 in /usr/local/lib/python3.7/dist-packages (from matplotlib->pygad==2.17) (1.4.4)\n", + "Requirement already satisfied: pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.1 in /usr/local/lib/python3.7/dist-packages (from matplotlib->pygad==2.17) (3.0.9)\n", + "Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.7/dist-packages (from matplotlib->pygad==2.17) (0.11.0)\n", + "Requirement already satisfied: typing-extensions in /usr/local/lib/python3.7/dist-packages (from kiwisolver>=1.0.1->matplotlib->pygad==2.17) (4.1.1)\n", + "Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.7/dist-packages (from python-dateutil>=2.1->matplotlib->pygad==2.17) (1.15.0)\n" + ] + } + ], + "source": [ + "!pip install pygad==2.17" + ], + "id": "_HBlGrIyYa7G" + }, + { + "cell_type": "code", + "execution_count": 76, + "metadata": { + "id": "buoFAtjz2duB" + }, + "outputs": [], + "source": [ + "!pip install -q kaggle" + ], + "id": "buoFAtjz2duB" + }, + { + "cell_type": "code", + "execution_count": 77, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 375, + "resources": { + "http://localhost:8080/nbextensions/google.colab/files.js": { + "data": "Ly8gQ29weXJpZ2h0IDIwMTcgR29vZ2xlIExMQwovLwovLyBMaWNlbnNlZCB1bmRlciB0aGUgQXBhY2hlIExpY2Vuc2UsIFZlcnNpb24gMi4wICh0aGUgIkxpY2Vuc2UiKTsKLy8geW91IG1heSBub3QgdXNlIHRoaXMgZmlsZSBleGNlcHQgaW4gY29tcGxpYW5jZSB3aXRoIHRoZSBMaWNlbnNlLgovLyBZb3UgbWF5IG9idGFpbiBhIGNvcHkgb2YgdGhlIExpY2Vuc2UgYXQKLy8KLy8gICAgICBodHRwOi8vd3d3LmFwYWNoZS5vcmcvbGljZW5zZXMvTElDRU5TRS0yLjAKLy8KLy8gVW5sZXNzIHJlcXVpcmVkIGJ5IGFwcGxpY2FibGUgbGF3IG9yIGFncmVlZCB0byBpbiB3cml0aW5nLCBzb2Z0d2FyZQovLyBkaXN0cmlidXRlZCB1bmRlciB0aGUgTGljZW5zZSBpcyBkaXN0cmlidXRlZCBvbiBhbiAiQVMgSVMiIEJBU0lTLAovLyBXSVRIT1VUIFdBUlJBTlRJRVMgT1IgQ09ORElUSU9OUyBPRiBBTlkgS0lORCwgZWl0aGVyIGV4cHJlc3Mgb3IgaW1wbGllZC4KLy8gU2VlIHRoZSBMaWNlbnNlIGZvciB0aGUgc3BlY2lmaWMgbGFuZ3VhZ2UgZ292ZXJuaW5nIHBlcm1pc3Npb25zIGFuZAovLyBsaW1pdGF0aW9ucyB1bmRlciB0aGUgTGljZW5zZS4KCi8qKgogKiBAZmlsZW92ZXJ2aWV3IEhlbHBlcnMgZm9yIGdvb2dsZS5jb2xhYiBQeXRob24gbW9kdWxlLgogKi8KKGZ1bmN0aW9uKHNjb3BlKSB7CmZ1bmN0aW9uIHNwYW4odGV4dCwgc3R5bGVBdHRyaWJ1dGVzID0ge30pIHsKICBjb25zdCBlbGVtZW50ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnc3BhbicpOwogIGVsZW1lbnQudGV4dENvbnRlbnQgPSB0ZXh0OwogIGZvciAoY29uc3Qga2V5IG9mIE9iamVjdC5rZXlzKHN0eWxlQXR0cmlidXRlcykpIHsKICAgIGVsZW1lbnQuc3R5bGVba2V5XSA9IHN0eWxlQXR0cmlidXRlc1trZXldOwogIH0KICByZXR1cm4gZWxlbWVudDsKfQoKLy8gTWF4IG51bWJlciBvZiBieXRlcyB3aGljaCB3aWxsIGJlIHVwbG9hZGVkIGF0IGEgdGltZS4KY29uc3QgTUFYX1BBWUxPQURfU0laRSA9IDEwMCAqIDEwMjQ7CgpmdW5jdGlvbiBfdXBsb2FkRmlsZXMoaW5wdXRJZCwgb3V0cHV0SWQpIHsKICBjb25zdCBzdGVwcyA9IHVwbG9hZEZpbGVzU3RlcChpbnB1dElkLCBvdXRwdXRJZCk7CiAgY29uc3Qgb3V0cHV0RWxlbWVudCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKG91dHB1dElkKTsKICAvLyBDYWNoZSBzdGVwcyBvbiB0aGUgb3V0cHV0RWxlbWVudCB0byBtYWtlIGl0IGF2YWlsYWJsZSBmb3IgdGhlIG5leHQgY2FsbAogIC8vIHRvIHVwbG9hZEZpbGVzQ29udGludWUgZnJvbSBQeXRob24uCiAgb3V0cHV0RWxlbWVudC5zdGVwcyA9IHN0ZXBzOwoKICByZXR1cm4gX3VwbG9hZEZpbGVzQ29udGludWUob3V0cHV0SWQpOwp9CgovLyBUaGlzIGlzIHJvdWdobHkgYW4gYXN5bmMgZ2VuZXJhdG9yIChub3Qgc3VwcG9ydGVkIGluIHRoZSBicm93c2VyIHlldCksCi8vIHdoZXJlIHRoZXJlIGFyZSBtdWx0aXBsZSBhc3luY2hyb25vdXMgc3RlcHMgYW5kIHRoZSBQeXRob24gc2lkZSBpcyBnb2luZwovLyB0byBwb2xsIGZvciBjb21wbGV0aW9uIG9mIGVhY2ggc3RlcC4KLy8gVGhpcyB1c2VzIGEgUHJvbWlzZSB0byBibG9jayB0aGUgcHl0aG9uIHNpZGUgb24gY29tcGxldGlvbiBvZiBlYWNoIHN0ZXAsCi8vIHRoZW4gcGFzc2VzIHRoZSByZXN1bHQgb2YgdGhlIHByZXZpb3VzIHN0ZXAgYXMgdGhlIGlucHV0IHRvIHRoZSBuZXh0IHN0ZXAuCmZ1bmN0aW9uIF91cGxvYWRGaWxlc0NvbnRpbnVlKG91dHB1dElkKSB7CiAgY29uc3Qgb3V0cHV0RWxlbWVudCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKG91dHB1dElkKTsKICBjb25zdCBzdGVwcyA9IG91dHB1dEVsZW1lbnQuc3RlcHM7CgogIGNvbnN0IG5leHQgPSBzdGVwcy5uZXh0KG91dHB1dEVsZW1lbnQubGFzdFByb21pc2VWYWx1ZSk7CiAgcmV0dXJuIFByb21pc2UucmVzb2x2ZShuZXh0LnZhbHVlLnByb21pc2UpLnRoZW4oKHZhbHVlKSA9PiB7CiAgICAvLyBDYWNoZSB0aGUgbGFzdCBwcm9taXNlIHZhbHVlIHRvIG1ha2UgaXQgYXZhaWxhYmxlIHRvIHRoZSBuZXh0CiAgICAvLyBzdGVwIG9mIHRoZSBnZW5lcmF0b3IuCiAgICBvdXRwdXRFbGVtZW50Lmxhc3RQcm9taXNlVmFsdWUgPSB2YWx1ZTsKICAgIHJldHVybiBuZXh0LnZhbHVlLnJlc3BvbnNlOwogIH0pOwp9CgovKioKICogR2VuZXJhdG9yIGZ1bmN0aW9uIHdoaWNoIGlzIGNhbGxlZCBiZXR3ZWVuIGVhY2ggYXN5bmMgc3RlcCBvZiB0aGUgdXBsb2FkCiAqIHByb2Nlc3MuCiAqIEBwYXJhbSB7c3RyaW5nfSBpbnB1dElkIEVsZW1lbnQgSUQgb2YgdGhlIGlucHV0IGZpbGUgcGlja2VyIGVsZW1lbnQuCiAqIEBwYXJhbSB7c3RyaW5nfSBvdXRwdXRJZCBFbGVtZW50IElEIG9mIHRoZSBvdXRwdXQgZGlzcGxheS4KICogQHJldHVybiB7IUl0ZXJhYmxlPCFPYmplY3Q+fSBJdGVyYWJsZSBvZiBuZXh0IHN0ZXBzLgogKi8KZnVuY3Rpb24qIHVwbG9hZEZpbGVzU3RlcChpbnB1dElkLCBvdXRwdXRJZCkgewogIGNvbnN0IGlucHV0RWxlbWVudCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKGlucHV0SWQpOwogIGlucHV0RWxlbWVudC5kaXNhYmxlZCA9IGZhbHNlOwoKICBjb25zdCBvdXRwdXRFbGVtZW50ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQob3V0cHV0SWQpOwogIG91dHB1dEVsZW1lbnQuaW5uZXJIVE1MID0gJyc7CgogIGNvbnN0IHBpY2tlZFByb21pc2UgPSBuZXcgUHJvbWlzZSgocmVzb2x2ZSkgPT4gewogICAgaW5wdXRFbGVtZW50LmFkZEV2ZW50TGlzdGVuZXIoJ2NoYW5nZScsIChlKSA9PiB7CiAgICAgIHJlc29sdmUoZS50YXJnZXQuZmlsZXMpOwogICAgfSk7CiAgfSk7CgogIGNvbnN0IGNhbmNlbCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2J1dHRvbicpOwogIGlucHV0RWxlbWVudC5wYXJlbnRFbGVtZW50LmFwcGVuZENoaWxkKGNhbmNlbCk7CiAgY2FuY2VsLnRleHRDb250ZW50ID0gJ0NhbmNlbCB1cGxvYWQnOwogIGNvbnN0IGNhbmNlbFByb21pc2UgPSBuZXcgUHJvbWlzZSgocmVzb2x2ZSkgPT4gewogICAgY2FuY2VsLm9uY2xpY2sgPSAoKSA9PiB7CiAgICAgIHJlc29sdmUobnVsbCk7CiAgICB9OwogIH0pOwoKICAvLyBXYWl0IGZvciB0aGUgdXNlciB0byBwaWNrIHRoZSBmaWxlcy4KICBjb25zdCBmaWxlcyA9IHlpZWxkIHsKICAgIHByb21pc2U6IFByb21pc2UucmFjZShbcGlja2VkUHJvbWlzZSwgY2FuY2VsUHJvbWlzZV0pLAogICAgcmVzcG9uc2U6IHsKICAgICAgYWN0aW9uOiAnc3RhcnRpbmcnLAogICAgfQogIH07CgogIGNhbmNlbC5yZW1vdmUoKTsKCiAgLy8gRGlzYWJsZSB0aGUgaW5wdXQgZWxlbWVudCBzaW5jZSBmdXJ0aGVyIHBpY2tzIGFyZSBub3QgYWxsb3dlZC4KICBpbnB1dEVsZW1lbnQuZGlzYWJsZWQgPSB0cnVlOwoKICBpZiAoIWZpbGVzKSB7CiAgICByZXR1cm4gewogICAgICByZXNwb25zZTogewogICAgICAgIGFjdGlvbjogJ2NvbXBsZXRlJywKICAgICAgfQogICAgfTsKICB9CgogIGZvciAoY29uc3QgZmlsZSBvZiBmaWxlcykgewogICAgY29uc3QgbGkgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCdsaScpOwogICAgbGkuYXBwZW5kKHNwYW4oZmlsZS5uYW1lLCB7Zm9udFdlaWdodDogJ2JvbGQnfSkpOwogICAgbGkuYXBwZW5kKHNwYW4oCiAgICAgICAgYCgke2ZpbGUudHlwZSB8fCAnbi9hJ30pIC0gJHtmaWxlLnNpemV9IGJ5dGVzLCBgICsKICAgICAgICBgbGFzdCBtb2RpZmllZDogJHsKICAgICAgICAgICAgZmlsZS5sYXN0TW9kaWZpZWREYXRlID8gZmlsZS5sYXN0TW9kaWZpZWREYXRlLnRvTG9jYWxlRGF0ZVN0cmluZygpIDoKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJ24vYSd9IC0gYCkpOwogICAgY29uc3QgcGVyY2VudCA9IHNwYW4oJzAlIGRvbmUnKTsKICAgIGxpLmFwcGVuZENoaWxkKHBlcmNlbnQpOwoKICAgIG91dHB1dEVsZW1lbnQuYXBwZW5kQ2hpbGQobGkpOwoKICAgIGNvbnN0IGZpbGVEYXRhUHJvbWlzZSA9IG5ldyBQcm9taXNlKChyZXNvbHZlKSA9PiB7CiAgICAgIGNvbnN0IHJlYWRlciA9IG5ldyBGaWxlUmVhZGVyKCk7CiAgICAgIHJlYWRlci5vbmxvYWQgPSAoZSkgPT4gewogICAgICAgIHJlc29sdmUoZS50YXJnZXQucmVzdWx0KTsKICAgICAgfTsKICAgICAgcmVhZGVyLnJlYWRBc0FycmF5QnVmZmVyKGZpbGUpOwogICAgfSk7CiAgICAvLyBXYWl0IGZvciB0aGUgZGF0YSB0byBiZSByZWFkeS4KICAgIGxldCBmaWxlRGF0YSA9IHlpZWxkIHsKICAgICAgcHJvbWlzZTogZmlsZURhdGFQcm9taXNlLAogICAgICByZXNwb25zZTogewogICAgICAgIGFjdGlvbjogJ2NvbnRpbnVlJywKICAgICAgfQogICAgfTsKCiAgICAvLyBVc2UgYSBjaHVua2VkIHNlbmRpbmcgdG8gYXZvaWQgbWVzc2FnZSBzaXplIGxpbWl0cy4gU2VlIGIvNjIxMTU2NjAuCiAgICBsZXQgcG9zaXRpb24gPSAwOwogICAgZG8gewogICAgICBjb25zdCBsZW5ndGggPSBNYXRoLm1pbihmaWxlRGF0YS5ieXRlTGVuZ3RoIC0gcG9zaXRpb24sIE1BWF9QQVlMT0FEX1NJWkUpOwogICAgICBjb25zdCBjaHVuayA9IG5ldyBVaW50OEFycmF5KGZpbGVEYXRhLCBwb3NpdGlvbiwgbGVuZ3RoKTsKICAgICAgcG9zaXRpb24gKz0gbGVuZ3RoOwoKICAgICAgY29uc3QgYmFzZTY0ID0gYnRvYShTdHJpbmcuZnJvbUNoYXJDb2RlLmFwcGx5KG51bGwsIGNodW5rKSk7CiAgICAgIHlpZWxkIHsKICAgICAgICByZXNwb25zZTogewogICAgICAgICAgYWN0aW9uOiAnYXBwZW5kJywKICAgICAgICAgIGZpbGU6IGZpbGUubmFtZSwKICAgICAgICAgIGRhdGE6IGJhc2U2NCwKICAgICAgICB9LAogICAgICB9OwoKICAgICAgbGV0IHBlcmNlbnREb25lID0gZmlsZURhdGEuYnl0ZUxlbmd0aCA9PT0gMCA/CiAgICAgICAgICAxMDAgOgogICAgICAgICAgTWF0aC5yb3VuZCgocG9zaXRpb24gLyBmaWxlRGF0YS5ieXRlTGVuZ3RoKSAqIDEwMCk7CiAgICAgIHBlcmNlbnQudGV4dENvbnRlbnQgPSBgJHtwZXJjZW50RG9uZX0lIGRvbmVgOwoKICAgIH0gd2hpbGUgKHBvc2l0aW9uIDwgZmlsZURhdGEuYnl0ZUxlbmd0aCk7CiAgfQoKICAvLyBBbGwgZG9uZS4KICB5aWVsZCB7CiAgICByZXNwb25zZTogewogICAgICBhY3Rpb246ICdjb21wbGV0ZScsCiAgICB9CiAgfTsKfQoKc2NvcGUuZ29vZ2xlID0gc2NvcGUuZ29vZ2xlIHx8IHt9OwpzY29wZS5nb29nbGUuY29sYWIgPSBzY29wZS5nb29nbGUuY29sYWIgfHwge307CnNjb3BlLmdvb2dsZS5jb2xhYi5fZmlsZXMgPSB7CiAgX3VwbG9hZEZpbGVzLAogIF91cGxvYWRGaWxlc0NvbnRpbnVlLAp9Owp9KShzZWxmKTsK", + "headers": [ + [ + "content-type", + "application/javascript" + ] + ], + "ok": true, + "status": 200, + "status_text": "" + } + } + }, + "id": "WYcYq9YV2gi9", + "outputId": "aef25ebb-cdaf-449f-c280-05f0faf1157f" + }, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "\n", + " \n", + " \n", + " Upload widget is only available when the cell has been executed in the\n", + " current browser session. Please rerun this cell to enable.\n", + " \n", + " " + ] + }, + "metadata": {} + }, + { + "output_type": "error", + "ename": "KeyboardInterrupt", + "evalue": "ignored", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0mgoogle\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcolab\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mfiles\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0mfiles\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mupload\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", + "\u001b[0;32m/usr/local/lib/python3.7/dist-packages/google/colab/files.py\u001b[0m in \u001b[0;36mupload\u001b[0;34m()\u001b[0m\n\u001b[1;32m 39\u001b[0m \"\"\"\n\u001b[1;32m 40\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 41\u001b[0;31m \u001b[0muploaded_files\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_upload_files\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmultiple\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mTrue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 42\u001b[0m \u001b[0;31m# Mapping from original filename to filename as saved locally.\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 43\u001b[0m \u001b[0mlocal_filenames\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mdict\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.7/dist-packages/google/colab/files.py\u001b[0m in \u001b[0;36m_upload_files\u001b[0;34m(multiple)\u001b[0m\n\u001b[1;32m 116\u001b[0m result = _output.eval_js(\n\u001b[1;32m 117\u001b[0m 'google.colab._files._uploadFiles(\"{input_id}\", \"{output_id}\")'.format(\n\u001b[0;32m--> 118\u001b[0;31m input_id=input_id, output_id=output_id))\n\u001b[0m\u001b[1;32m 119\u001b[0m \u001b[0mfiles\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_collections\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdefaultdict\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mbytes\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 120\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.7/dist-packages/google/colab/output/_js.py\u001b[0m in \u001b[0;36meval_js\u001b[0;34m(script, ignore_result, timeout_sec)\u001b[0m\n\u001b[1;32m 38\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mignore_result\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 39\u001b[0m \u001b[0;32mreturn\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 40\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0m_message\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mread_reply_from_input\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mrequest_id\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtimeout_sec\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 41\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 42\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m/usr/local/lib/python3.7/dist-packages/google/colab/_message.py\u001b[0m in \u001b[0;36mread_reply_from_input\u001b[0;34m(message_id, timeout_sec)\u001b[0m\n\u001b[1;32m 95\u001b[0m \u001b[0mreply\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_read_next_input_message\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 96\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mreply\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0m_NOT_READY\u001b[0m \u001b[0;32mor\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mreply\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdict\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 97\u001b[0;31m \u001b[0mtime\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msleep\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m0.025\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 98\u001b[0m \u001b[0;32mcontinue\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 99\u001b[0m if (reply.get('type') == 'colab_reply' and\n", + "\u001b[0;31mKeyboardInterrupt\u001b[0m: " + ] + } + ], + "source": [ + "from google.colab import files\n", + "files.upload() # upload a Kaggle JSON file to make request for data " + ], + "id": "WYcYq9YV2gi9" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "TBtskqtn29H3" + }, + "outputs": [], + "source": [ + "!mkdir kaggle " + ], + "id": "TBtskqtn29H3" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "oK1Z1Yr12tEv" + }, + "outputs": [], + "source": [ + "!cp kaggle.json ~/.kaggle/\n", + "!chmod 600 ~/.kaggle/kaggle.json" + ], + "id": "oK1Z1Yr12tEv" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "8U2rpJ1j191n" + }, + "outputs": [], + "source": [ + "!kaggle datasets download kukuroo3/starbucks-locations-worldwide-2021-version -p /content/sample_data/ --unzip" + ], + "id": "8U2rpJ1j191n" + }, + { + "cell_type": "code", + "execution_count": 165, + "metadata": { + "id": "sTkY5cLb3age", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "a064aedc-8e4d-4712-d223-e35db3d1d7ae" + }, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "Index(['Unnamed: 0', 'storeNumber', 'countryCode', 'ownershipTypeCode',\n", + " 'schedule', 'slug', 'latitude', 'longitude', 'streetAddressLine1',\n", + " 'streetAddressLine2', 'streetAddressLine3', 'city',\n", + " 'countrySubdivisionCode', 'postalCode', 'currentTimeOffset',\n", + " 'windowsTimeZoneId', 'olsonTimeZoneId'],\n", + " dtype='object')" + ] + }, + "metadata": {}, + "execution_count": 165 + } + ], + "source": [ + "import pandas as pd \n", + "\n", + "# read in data and check column names \n", + "data = pd.read_csv('/content/sample_data/startbucks.csv')\n", + "data.columns" + ], + "id": "sTkY5cLb3age" + }, + { + "cell_type": "code", + "execution_count": 166, + "metadata": { + "id": "SImMYnfe3n_w", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "4ee033d9-9f66-489a-c470-b45a1a3b0ac0" + }, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "0" + ] + }, + "metadata": {}, + "execution_count": 166 + } + ], + "source": [ + "df = data[data['countryCode']=='GB']\n", + "df.reset_index(inplace=True)\n", + "\n", + "# check for invalid lon/lat pairs\n", + "len(df.dropna(subset=['latitude', 'longitude'])) - len(df)" + ], + "id": "SImMYnfe3n_w" + }, + { + "cell_type": "markdown", + "source": [ + "## 2. Exploratory analysis\n", + "\n", + "Find the distribution of cafés across the United Kingdom. \n", + "\n", + "How are restaurants distributed across towns?\n", + "What does a geospatial representation of the data look like?" + ], + "metadata": { + "id": "ovWqqNFIvydy" + }, + "id": "ovWqqNFIvydy" + }, + { + "cell_type": "markdown", + "source": [ + "### 2.1 Distribution of cafés by town" + ], + "metadata": { + "id": "SokLPN9j2fn1" + }, + "id": "SokLPN9j2fn1" + }, + { + "cell_type": "code", + "source": [ + "import plotly.express as px\n", + "vis = df.groupby('city').storeNumber.count().reset_index()\n", + "px.bar(vis, x='city', y='storeNumber', template='seaborn')" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 0 + }, + "id": "HGOBgqnX2lIo", + "outputId": "0f19d0f2-0063-458a-cd02-987e30b3d9cf" + }, + "id": "HGOBgqnX2lIo", + "execution_count": 167, + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "
\n", + "
\n", + "\n", + "" + ] + }, + "metadata": {} + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "\n", + "### 2.2 Map of cafés in the UK" + ], + "metadata": { + "id": "z9a3NPSM2lb4" + }, + "id": "z9a3NPSM2lb4" + }, + { + "cell_type": "code", + "source": [ + "import folium" + ], + "metadata": { + "id": "dAq1AQyfwN_l" + }, + "id": "dAq1AQyfwN_l", + "execution_count": 168, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "map = folium.Map(location=[51.509685, -0.118092], zoom_start=6, tiles=\"stamentoner\")" + ], + "metadata": { + "id": "V9b0_8g_xM5K" + }, + "id": "V9b0_8g_xM5K", + "execution_count": 169, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "for _, r in df.iterrows():\n", + " folium.Marker(\n", + " [r['latitude'], r['longitude']], popup=f'{r[\"storeNumber\"]}'\n", + " ).add_to(map)" + ], + "metadata": { + "id": "ZiprKw6ExhQR" + }, + "id": "ZiprKw6ExhQR", + "execution_count": 170, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "map" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 0 + }, + "id": "iVnEBooHc0ze", + "outputId": "b6ee729b-739f-4db5-975f-12a6a2c82246" + }, + "id": "iVnEBooHc0ze", + "execution_count": 171, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "
Make this Notebook Trusted to load map: File -> Trust Notebook
" + ] + }, + "metadata": {}, + "execution_count": 171 + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "## 3. Testing the distance methodology\n", + "\n", + "To assess how good each solution is there needs to be a measure of fitness. For the purpose of this example the distance 'as the crow flies' is used without taking into account actual road distances however this could be explored in future." + ], + "metadata": { + "id": "1sg-1A4Ih1L4" + }, + "id": "1sg-1A4Ih1L4" + }, + { + "cell_type": "code", + "source": [ + "from geopy.distance import geodesic" + ], + "metadata": { + "id": "IbG9XRkLh0Aj" + }, + "id": "IbG9XRkLh0Aj", + "execution_count": 173, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "The tested origin is the first Starbucks in the data and the destination is the second Starbucks in the dataset." + ], + "metadata": { + "id": "zgmh6b1l3liN" + }, + "id": "zgmh6b1l3liN" + }, + { + "cell_type": "code", + "execution_count": 174, + "metadata": { + "id": "TyoeO2I-4bGq" + }, + "outputs": [], + "source": [ + "origin = (df['latitude'][0], df['longitude'][0])\n", + "dest = (df['latitude'][100], df['longitude'][100])" + ], + "id": "TyoeO2I-4bGq" + }, + { + "cell_type": "markdown", + "source": [ + "The distance between the two points as the crow flies in kilometres is given below." + ], + "metadata": { + "id": "VUr7ZAzw3srI" + }, + "id": "VUr7ZAzw3srI" + }, + { + "cell_type": "code", + "execution_count": 175, + "metadata": { + "id": "zXo1I-5Q4Lwn", + "colab": { + "base_uri": "https://localhost:8080/" + }, + "outputId": "4672edf2-9142-4276-ab6a-2b35b65b8b3d" + }, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "81.63683980420957" + ] + }, + "metadata": {}, + "execution_count": 175 + } + ], + "source": [ + "geodesic(origin, dest).kilometers" + ], + "id": "zXo1I-5Q4Lwn" + }, + { + "cell_type": "markdown", + "source": [ + "## 4. Preparing data structures\n", + "\n", + "The data structures needed for testing solutions are the \"genes\" or store options to select from named *genes*\n", + "\n", + "A lookup to access these genes known as *stores* \n", + "\n", + "A *check_range* which is used to check that every option is given in a solution (a key criteria in the TSP).\n" + ], + "metadata": { + "id": "GeRIsd5G378L" + }, + "id": "GeRIsd5G378L" + }, + { + "cell_type": "code", + "source": [ + "test = df.head(10)\n", + "genes = {store_num:[lat, lon] for store_num, lat, lon in zip(test['storeNumber'], test['latitude'], test['longitude'])}\n", + "stores = list(genes.keys())\n", + "check_range = [i for i in range(0, 10)]" + ], + "metadata": { + "id": "rkKStcn4iIrN" + }, + "id": "rkKStcn4iIrN", + "execution_count": 176, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "## 5. Defining functions \n", + "\n", + "The algorithm requires a set of functions to be pre-defined as the out of the box genetic algorithm does not support a TSP.\n", + "\n", + " 1. build_population: builds a population of chromosomes to test with proper restrictions applied\n", + " 2. fitness_func: Used to test a solution to see how well it performs, in this case the fitness_func will be assessed based on the distance as the crow flies between each successive point\n", + " 3. pmx_crossover: performs the crossover of a parent and child with proper Partially Matched Crossover (PMX) logic\n", + " 4. crossover_func: applies the crossover\n", + " 5. on_crossover: applies the mutation after crossover\n", + " 6. on_generation: used to print the progress and results at each generation" + ], + "metadata": { + "id": "5mKC2lKO4tRY" + }, + "id": "5mKC2lKO4tRY" + }, + { + "cell_type": "code", + "source": [ + "import random\n", + "import numpy as np\n", + "from geopy.distance import geodesic" + ], + "metadata": { + "id": "15-IiuRNljOC" + }, + "id": "15-IiuRNljOC", + "execution_count": 177, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Assess the quality or fitness of a solution so that only the fittest are selected for the next generation and to breed." + ], + "metadata": { + "id": "BhCEN9165in3" + }, + "id": "BhCEN9165in3" + }, + { + "cell_type": "code", + "source": [ + "def build_population(size, chromosome_size):\n", + " population = []\n", + " for i in range(size):\n", + " home_city = 0\n", + " added = {home_city:'Added'}\n", + " chromosome = [home_city]\n", + "\n", + " while len(chromosome) < chromosome_size:\n", + " proposed_gene = random.randint(0, chromosome_size-1)\n", + " if added.get(proposed_gene) is None:\n", + " chromosome.append(proposed_gene)\n", + " added.update({proposed_gene:'Added'})\n", + " else:\n", + " pass\n", + "\n", + " chromosome.append(home_city)\n", + "\n", + " population.append(chromosome)\n", + "\n", + " return np.array(population)" + ], + "metadata": { + "id": "BLz2sU2n78Ui" + }, + "id": "BLz2sU2n78Ui", + "execution_count": 178, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "population = build_population(100, 10)\n", + "population.shape" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "vs8CrTNV9iqe", + "outputId": "4178c652-2593-44e9-ca0d-1829f11c6d5e" + }, + "id": "vs8CrTNV9iqe", + "execution_count": 179, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "(100, 11)" + ] + }, + "metadata": {}, + "execution_count": 179 + } + ] + }, + { + "cell_type": "code", + "source": [ + "def fitness_func(solution, solution_idx):\n", + " # loop through the length of the chromosome finding the distance between each\n", + " # gene added \n", + "\n", + " # to increment\n", + " total_dist = 0\n", + "\n", + " for gene in range(0, len(solution)):\n", + "\n", + " # get the lon lat of the two points\n", + " a = genes.get(stores[solution[gene]])\n", + " \n", + " try:\n", + " b = genes.get(stores[solution[gene + 1]])\n", + "\n", + " # find the distance (crow flies)\n", + " dist = geodesic(a, b).kilometers\n", + "\n", + " except IndexError:\n", + " dist = 0\n", + "\n", + " total_dist += dist\n", + "\n", + " # to optimise this value in the positive direction the inverse of dist is used\n", + " fitness = 1 / total_dist\n", + "\n", + " return fitness " + ], + "metadata": { + "id": "5u-5msoj-84i" + }, + "id": "5u-5msoj-84i", + "execution_count": 180, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "def pmx_crossover(parent1, parent2, sequence_start, sequence_end):\n", + " # initialise a child\n", + " child = np.zeros(parent1.shape[0])\n", + "\n", + " # get the genes for parent one that are passed on to child one\n", + " parent1_to_child1_genes = parent1[sequence_start:sequence_end]\n", + "\n", + " # get the position of genes for each respective combination\n", + " parent1_to_child1 = np.isin(parent1,parent1_to_child1_genes).nonzero()[0]\n", + "\n", + " for gene in parent1_to_child1:\n", + " child[gene] = parent1[gene]\n", + "\n", + " # gene of parent 2 not in the child\n", + " genes_not_in_child = parent2[np.isin(parent2, parent1_to_child1_genes, invert=True).nonzero()[0]]\n", + " \n", + " # if the gene is not already\n", + " if genes_not_in_child.shape[0] >= 1:\n", + " for gene in genes_not_in_child:\n", + " if gene >= 1:\n", + " lookup = gene\n", + " not_in_sequence = True\n", + "\n", + " while not_in_sequence:\n", + " position_in_parent2 = np.where(parent2==lookup)[0][0]\n", + "\n", + " if position_in_parent2 in range(sequence_start, sequence_end):\n", + " lookup = parent1[position_in_parent2]\n", + "\n", + " else:\n", + " child[position_in_parent2] = gene\n", + " not_in_sequence = False\n", + "\n", + " return child" + ], + "metadata": { + "id": "OpbfyShQes_q" + }, + "id": "OpbfyShQes_q", + "execution_count": 181, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "def crossover_func(parents, offspring_size, ga_instance):\n", + " offspring = []\n", + " idx = 0\n", + " while len(offspring) != offspring_size[0]:\n", + "\n", + " # locate the parents\n", + " parent1 = parents[idx % parents.shape[0], :].copy()\n", + " parent2 = parents[(idx + 1) % parents.shape[0], :].copy()\n", + "\n", + " # find gene sequence in parent 1 \n", + " sequence_start = random.randint(1, parent1.shape[0]-4)\n", + " sequence_end = random.randint(sequence_start, parent1.shape[0]-1)\n", + "\n", + " # perform crossover\n", + " child1 = pmx_crossover(parent1, parent2, sequence_start, sequence_end)\n", + " child2 = pmx_crossover(parent2, parent1, sequence_start, sequence_end) \n", + "\n", + " offspring.append(child1)\n", + " offspring.append(child2)\n", + "\n", + "\n", + " idx += 1\n", + "\n", + " return np.array(offspring)" + ], + "metadata": { + "id": "shgFWqH2NinO" + }, + "id": "shgFWqH2NinO", + "execution_count": 182, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "The mutation function chosen is inversion as it does not invalidate the solution." + ], + "metadata": { + "id": "bCjaaeofA7Bg" + }, + "id": "bCjaaeofA7Bg" + }, + { + "cell_type": "code", + "source": [ + "def mutation_func(offspring, ga_instance):\n", + "\n", + " for chromosome_idx in range(offspring.shape[0]):\n", + " # define a sequence of genes to reverse\n", + " sequence_start = random.randint(1, offspring[chromosome_idx].shape[0] - 2)\n", + " sequence_end = random.randint(sequence_start, offspring[chromosome_idx].shape[0] - 1)\n", + " \n", + " genes = offspring[chromosome_idx, sequence_start:sequence_end]\n", + "\n", + " # start at the start of the sequence assigning the reverse sequence back to the chromosome\n", + " index = 0\n", + " if len(genes) > 0:\n", + " for gene in range(sequence_start, sequence_end):\n", + "\n", + " offspring[chromosome_idx, gene] = genes[index]\n", + "\n", + " index += 1\n", + "\n", + " return offspring" + ], + "metadata": { + "id": "0kMm7J1WAsvH" + }, + "id": "0kMm7J1WAsvH", + "execution_count": 183, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Used in the genetic algorithm flow to apply the custom mutation after crossover" + ], + "metadata": { + "id": "HTeKwuPl5vVt" + }, + "id": "HTeKwuPl5vVt" + }, + { + "cell_type": "code", + "source": [ + "def on_crossover(ga_instance, offspring_crossover):\n", + " # apply mutation to ensure uniqueness \n", + " offspring_mutation = mutation_func(offspring_crossover, ga_instance)\n", + "\n", + " # save the new offspring set as the parents of the next generation\n", + " ga_instance.last_generation_offspring_mutation = offspring_mutation" + ], + "metadata": { + "id": "ucwYe4rgwLQC" + }, + "id": "ucwYe4rgwLQC", + "execution_count": 184, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "Added for debugging and assessing progress by generation at runtime" + ], + "metadata": { + "id": "Xf7rgtuO532X" + }, + "id": "Xf7rgtuO532X" + }, + { + "cell_type": "code", + "source": [ + "def on_generation(ga):\n", + " print(\"Generation\", ga.generations_completed)\n", + " print(ga.population)" + ], + "metadata": { + "id": "eqRwVsSBMf_B" + }, + "id": "eqRwVsSBMf_B", + "execution_count": 185, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "## 6. Executing the algorithm\n", + "\n", + "The genetic algorithm is set up as instance and at initialisation several parameters are given. \n", + "\n", + "The algorithm then runs to find the best solution for a set number of generations." + ], + "metadata": { + "id": "C9AtXoqx58x0" + }, + "id": "C9AtXoqx58x0" + }, + { + "cell_type": "code", + "source": [ + "import pygad" + ], + "metadata": { + "id": "BrJVLZlBW07R" + }, + "id": "BrJVLZlBW07R", + "execution_count": 186, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "### 6.1 Example Initialising the algorithm\n", + "\n", + "The algorithm is initialised below.\n", + "\n", + "Notable parameters include:\n", + " - The use of gene space to limit the possible genes chosen to just be those in the TSP range\n", + " - Mutations being turned off temporarily\n", + " - Implementation of custom on_ functions \n", + " - Allow duplication of genes parameter set to false to ensure any newly introduced chromosomes/chromosomes created as population is initialised have no duplicate genes" + ], + "metadata": { + "id": "6ni9VkQv6TJR" + }, + "id": "6ni9VkQv6TJR" + }, + { + "cell_type": "code", + "source": [ + "ga_instance = pygad.GA(num_generations=100,\n", + " num_parents_mating=40,\n", + " fitness_func=fitness_func,\n", + " sol_per_pop=200,\n", + " initial_population=population,\n", + " gene_space=range(0, 10),\n", + " gene_type=int,\n", + " mutation_type=mutation_func,\n", + " on_generation=on_generation,\n", + " crossover_type=crossover_func, \n", + " keep_parents=6,\n", + " mutation_probability=0.4)" + ], + "metadata": { + "id": "FfFdncf-G3Mr" + }, + "id": "FfFdncf-G3Mr", + "execution_count": 187, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [ + "### 6.2 Running the algorithm \n", + "\n", + "The genetic algorithm is run with a simple function call" + ], + "metadata": { + "id": "sfqme_5461A4" + }, + "id": "sfqme_5461A4" + }, + { + "cell_type": "code", + "source": [ + "ga_instance.run()" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "g-3CjxWYZh3H", + "outputId": "dc1fbda5-6b22-4245-9b99-e22b0e41684f" + }, + "id": "g-3CjxWYZh3H", + "execution_count": 188, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Generation 1\n", + "[[0 3 2 ... 4 5 0]\n", + " [0 3 6 ... 1 2 0]\n", + " [0 8 3 ... 6 1 0]\n", + " ...\n", + " [0 9 5 ... 7 4 0]\n", + " [0 2 7 ... 8 6 0]\n", + " [0 3 5 ... 6 8 0]]\n", + "Generation 2\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 9 ... 8 3 0]\n", + " [0 3 2 ... 4 5 0]\n", + " ...\n", + " [0 3 6 ... 1 2 0]\n", + " [0 3 6 ... 1 2 0]\n", + " [0 3 1 ... 6 2 0]]\n", + "Generation 3\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 9 ... 8 3 0]\n", + " [0 3 2 ... 4 5 0]\n", + " [0 9 2 ... 8 3 0]]\n", + "Generation 4\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 5\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 6\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 7\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 8\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 9\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 10\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 11\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 12\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 13\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 14\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 15\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 16\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 17\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 18\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 19\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 20\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 21\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 22\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 23\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 24\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 25\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 26\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 27\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 28\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 29\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 30\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 31\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 32\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 33\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 34\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 35\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 36\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 37\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 38\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 39\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 40\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 41\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 42\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 43\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 44\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 45\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 46\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 47\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 48\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 49\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 50\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 51\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 52\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 53\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 54\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 55\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 56\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 57\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 58\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 59\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 60\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 61\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 62\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 63\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 64\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 65\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 66\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 67\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 68\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 69\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 70\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 71\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 72\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 73\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 74\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 75\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 76\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 77\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 78\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 79\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 80\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 81\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 82\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 83\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 84\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 85\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 86\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 87\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 88\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 89\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 90\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 91\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 92\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 93\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 94\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 95\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 96\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 97\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 98\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 99\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n", + "Generation 100\n", + "[[0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " ...\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]\n", + " [0 1 2 ... 8 6 0]]\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "## 7. Assessing results \n", + "\n", + "The result solution can be checked and analysed using the ga_instance itself" + ], + "metadata": { + "id": "woJTWlOrYLwl" + }, + "id": "woJTWlOrYLwl" + }, + { + "cell_type": "code", + "source": [ + "solution, solution_fitness, solution_idx = ga_instance.best_solution()" + ], + "metadata": { + "id": "BZxvMCZ-aFtJ" + }, + "id": "BZxvMCZ-aFtJ", + "execution_count": 189, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "solution, solution_fitness, solution_idx = ga_instance.best_solution()\n", + "print(f'Generation of best solution: {ga_instance.best_solution_generation}')\n", + "print(\"Fitness value of the best solution = {solution_fitness}\".format(solution_fitness=solution_fitness))\n", + "print(\"Index of the best solution : {solution_idx}\".format(solution_idx=solution_idx))" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "IOTGnna-QXsB", + "outputId": "30d494d6-e88e-4061-a453-9a44a9409176" + }, + "id": "IOTGnna-QXsB", + "execution_count": 190, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Generation of best solution: 1\n", + "Fitness value of the best solution = 0.010681933534441102\n", + "Index of the best solution : 0\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "if ga_instance.best_solution_generation != -1:\n", + " print(\"Best fitness value reached after {best_solution_generation} generations.\".format(best_solution_generation=ga_instance.best_solution_generation))\n" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "WsjTUVloQhez", + "outputId": "fdd41ccb-9ead-49d7-d180-ccc1dd2d13f9" + }, + "id": "WsjTUVloQhez", + "execution_count": 191, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Best fitness value reached after 1 generations.\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "### 7.1 Verifying a solution\n", + "\n", + "For a solution to be valid it needs to have:\n", + " - A maximum gene value that matches the total number of stores \n", + " - A minimum gene value of 0 \n", + " - Each gene must be unique" + ], + "metadata": { + "id": "FiLlGlILYWhw" + }, + "id": "FiLlGlILYWhw" + }, + { + "cell_type": "code", + "source": [ + "def verify_solution(solution, max_gene):\n", + " if min(solution) != 0:\n", + " print('Failed values below 0')\n", + "\n", + " if max(solution) != max_gene:\n", + " print('Failed values less than or above max possible value')\n", + "\n", + " if len(set(solution)) - len(solution) != -1:\n", + " print(len(set(solution)) - len(solution))\n", + " print('Failed solution does not contain unique values')" + ], + "metadata": { + "id": "vEhveNG4VQBF" + }, + "id": "vEhveNG4VQBF", + "execution_count": 192, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "verify_solution(solution, 9)" + ], + "metadata": { + "id": "IQg1sULKYRZe" + }, + "id": "IQg1sULKYRZe", + "execution_count": 193, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "solution" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "VqjdGMGnYqz-", + "outputId": "152c6b82-4787-4776-b9e9-bc3ceb1ccec1" + }, + "id": "VqjdGMGnYqz-", + "execution_count": 194, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "array([0, 1, 2, 3, 4, 5, 7, 9, 8, 6, 0])" + ] + }, + "metadata": {}, + "execution_count": 194 + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "### 7.2 Interpreting the result \n", + "\n", + "The result sequence can be used to access latitude and longitude for each store in the solution." + ], + "metadata": { + "id": "5ixLDfvtY3cI" + }, + "id": "5ixLDfvtY3cI" + }, + { + "cell_type": "code", + "source": [ + "points = [genes.get(stores[id]) + [stores[id]] for id in solution]\n", + "points[:5]" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "tv-AjzcoQwGc", + "outputId": "a5187ac8-ea84-4dc9-992c-60cfa4dcab8a" + }, + "id": "tv-AjzcoQwGc", + "execution_count": 195, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "[[51.483556, -1.557143, '9155-152277'],\n", + " [51.482387, -1.555109, '22194-218828'],\n", + " [51.481264, -1.556526, '18362-190424'],\n", + " [51.481177, -1.557422, '9136-152279'],\n", + " [51.562617, -1.798111, '47832-260044']]" + ] + }, + "metadata": {}, + "execution_count": 195 + } + ] + }, + { + "cell_type": "code", + "source": [ + "import folium \n", + "\n", + "map = folium.Map(location=[51.509685, -0.118092], zoom_start=6, tiles=\"stamentoner\")\n", + "\n", + "for point in range(0, len(points)):\n", + " folium.Marker(\n", + " [points[point][0], points[point][1]], popup=f'{points[point][2]}'\n", + " ).add_to(map)\n", + "\n", + " try:\n", + " folium.PolyLine([(points[point][0], points[point][1]), \n", + " (points[point+1][0], points[point+1][1])],\n", + " color='red',\n", + " weight=5,\n", + " opacity=0.8).add_to(map)\n", + "\n", + " except IndexError:\n", + " pass\n", + " " + ], + "metadata": { + "id": "Tq0hTc5cstWX" + }, + "id": "Tq0hTc5cstWX", + "execution_count": 196, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "map" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 0 + }, + "id": "ebkOuHleuGU1", + "outputId": "801c457d-49d9-452d-9c4b-872aef5816d4" + }, + "id": "ebkOuHleuGU1", + "execution_count": 197, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "
Make this Notebook Trusted to load map: File -> Trust Notebook
" + ] + }, + "metadata": {}, + "execution_count": 197 + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "The map shows the shortest path that has been found. So that the travelling coffee drinker can maximise the time on coffee and minimise the time on travelling.\n", + "\n", + "Now the algorithm can be scaled up for the whole of the UK, or tailored to just one town. An example of the solution scaled to the UK is given below." + ], + "metadata": { + "id": "5Sz8ykeEYDS3" + }, + "id": "5Sz8ykeEYDS3" + }, + { + "cell_type": "markdown", + "source": [ + "## 8. Scaling up the solution\n", + "\n", + "This is where the fun begins!" + ], + "metadata": { + "id": "gscT4SjxZ9h4" + }, + "id": "gscT4SjxZ9h4" + }, + { + "cell_type": "code", + "source": [ + "df = df[df['city'] == 'London']\n", + "genes = {store_num:[lat, lon] for store_num, lat, lon in zip(df['storeNumber'], df['latitude'], df['longitude'])}\n", + "stores = list(genes.keys())\n", + "len(stores)" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "yYY3-gP1aFkH", + "outputId": "907e2a7c-3292-4409-e83e-4db372f09a45" + }, + "id": "yYY3-gP1aFkH", + "execution_count": 157, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "165" + ] + }, + "metadata": {}, + "execution_count": 157 + } + ] + }, + { + "cell_type": "code", + "source": [ + "population = build_population(200, 165)\n", + "len(population[0])" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "oykOz7l4KhJE", + "outputId": "fe7fdabc-a909-40c4-e69a-daba8169b07c" + }, + "id": "oykOz7l4KhJE", + "execution_count": 155, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "166" + ] + }, + "metadata": {}, + "execution_count": 155 + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "### 8.1 Building the final algorithm\n", + "\n", + "The code to build the algorithm has to be re-run with the above data structures altered." + ], + "metadata": { + "id": "ioy8EGjEbnBJ" + }, + "id": "ioy8EGjEbnBJ" + }, + { + "cell_type": "code", + "source": [ + "def fitness_func(solution, solution_idx):\n", + " # loop through the length of the chromosome finding the distance between each\n", + " # gene added \n", + "\n", + " # to increment\n", + " total_dist = 0\n", + "\n", + " for gene in range(0, len(solution)):\n", + "\n", + " # get the lon lat of the two points\n", + " a = genes.get(stores[solution[gene]])\n", + " \n", + " try:\n", + " b = genes.get(stores[solution[gene + 1]])\n", + "\n", + " # find the distance (crow flies)\n", + " dist = geodesic(a, b).kilometers\n", + "\n", + " except IndexError:\n", + " dist = 0\n", + "\n", + " total_dist += dist\n", + "\n", + " # to optimise this value in the positive direction the inverse of dist is used\n", + " fitness = 1 / total_dist\n", + "\n", + " return fitness " + ], + "metadata": { + "id": "uBfcikkma5hP" + }, + "id": "uBfcikkma5hP", + "execution_count": 108, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "def pmx_crossover(parent1, parent2, sequence_start, sequence_end):\n", + " # initialise a child\n", + " child = np.zeros(parent1.shape[0])\n", + "\n", + " # get the genes for parent one that are passed on to child one\n", + " parent1_to_child1_genes = parent1[sequence_start:sequence_end]\n", + "\n", + " # get the position of genes for each respective combination\n", + " parent1_to_child1 = np.isin(parent1,parent1_to_child1_genes).nonzero()[0]\n", + "\n", + " for gene in parent1_to_child1:\n", + " child[gene] = parent1[gene]\n", + "\n", + " # gene of parent 2 not in the child\n", + " genes_not_in_child = parent2[np.isin(parent2, parent1_to_child1_genes, invert=True).nonzero()[0]]\n", + " \n", + " if genes_not_in_child.shape[0] >= 1:\n", + " for gene in genes_not_in_child:\n", + " if gene >= 1:\n", + " lookup = gene\n", + " not_in_sequence = True\n", + "\n", + " while not_in_sequence:\n", + " position_in_parent2 = np.where(parent2==lookup)[0][0]\n", + "\n", + " if position_in_parent2 in range(sequence_start, sequence_end):\n", + " lookup = parent1[position_in_parent2]\n", + "\n", + " else:\n", + " child[position_in_parent2] = gene\n", + " not_in_sequence = False\n", + "\n", + " return child" + ], + "metadata": { + "id": "FuOkiStta7Pz" + }, + "id": "FuOkiStta7Pz", + "execution_count": 109, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "def crossover_func(parents, offspring_size, ga_instance):\n", + " offspring = []\n", + " idx = 0\n", + " while len(offspring) != offspring_size[0]:\n", + "\n", + " # locate the parents\n", + " parent1 = parents[idx % parents.shape[0], :].copy()\n", + " parent2 = parents[(idx + 1) % parents.shape[0], :].copy()\n", + "\n", + " # find gene sequence in parent 1 \n", + " sequence_start = random.randint(1, parent1.shape[0]-4)\n", + " sequence_end = random.randint(sequence_start, parent1.shape[0]-1)\n", + "\n", + " # perform crossover\n", + " child1 = pmx_crossover(parent1, parent2, sequence_start, sequence_end)\n", + " child2 = pmx_crossover(parent2, parent1, sequence_start, sequence_end)\n", + " \n", + "\n", + " offspring.append(child1)\n", + " offspring.append(child2)\n", + "\n", + " idx += 1\n", + "\n", + " return np.array(offspring)" + ], + "metadata": { + "id": "O10ZgScUa_bj" + }, + "id": "O10ZgScUa_bj", + "execution_count": 130, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "def mutation_func(offspring, ga_instance):\n", + "\n", + " for chromosome_idx in range(offspring.shape[0]):\n", + " # define a sequence of genes to reverse\n", + " sequence_start = random.randint(1, offspring[chromosome_idx].shape[0] - 2)\n", + " sequence_end = random.randint(sequence_start, offspring[chromosome_idx].shape[0] - 1)\n", + " \n", + " genes = offspring[chromosome_idx, sequence_start:sequence_end]\n", + "\n", + " # start at the start of the sequence assigning the reverse sequence back to the chromosome\n", + " index = 0\n", + " if len(genes) > 0:\n", + " for gene in range(sequence_start, sequence_end):\n", + "\n", + " offspring[chromosome_idx, gene] = genes[index]\n", + "\n", + " index += 1\n", + "\n", + " return offspring" + ], + "metadata": { + "id": "mLLY7Ub4K_Y5" + }, + "id": "mLLY7Ub4K_Y5", + "execution_count": 144, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "def on_crossover(ga_instance, offspring_crossover):\n", + " # apply mutation to ensure uniqueness \n", + " offspring_mutation = mutation_func(offspring_crossover, ga_instance)\n", + "\n", + " # save the new offspring set as the parents of the next generation\n", + " ga_instance.last_generation_offspring_mutation = offspring_mutation" + ], + "metadata": { + "id": "QLtP6in4LFSw" + }, + "id": "QLtP6in4LFSw", + "execution_count": 126, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "def on_generation(ga):\n", + " print(\"Generation\", ga.generations_completed)\n", + " print(ga.population)" + ], + "metadata": { + "id": "SnR2LaDJLGRj" + }, + "id": "SnR2LaDJLGRj", + "execution_count": 127, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "ga_instance = pygad.GA(num_generations=100,\n", + " num_parents_mating=40,\n", + " fitness_func=fitness_func,\n", + " sol_per_pop=200,\n", + " initial_population=population,\n", + " gene_space=range(0, 165),\n", + " gene_type=int,\n", + " mutation_type=mutation_func,\n", + " on_generation=on_generation,\n", + " crossover_type=crossover_func, \n", + " keep_parents=6,\n", + " mutation_probability=0.4)" + ], + "metadata": { + "id": "j2J5jlh9bDxR" + }, + "id": "j2J5jlh9bDxR", + "execution_count": 145, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "ga_instance.run()" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "lhW7JkMAbS6E", + "outputId": "c6130b2a-1e6b-4b7d-c2d6-f891b178fef9" + }, + "id": "lhW7JkMAbS6E", + "execution_count": 146, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Generation 1\n", + "[[ 0 1 111 ... 127 108 0]\n", + " [ 0 62 141 ... 26 161 0]\n", + " [ 0 137 155 ... 158 3 0]\n", + " ...\n", + " [ 0 142 162 ... 2 159 0]\n", + " [ 0 161 159 ... 112 66 0]\n", + " [ 0 152 108 ... 72 58 0]]\n", + "Generation 2\n", + "[[ 0 1 111 ... 127 108 0]\n", + " [ 0 1 111 ... 127 108 0]\n", + " [ 0 62 141 ... 26 161 0]\n", + " ...\n", + " [ 0 137 155 ... 135 76 0]\n", + " [ 0 137 155 ... 158 3 0]\n", + " [ 0 96 40 ... 135 5 0]]\n", + "Generation 3\n", + "[[ 0 1 145 ... 26 94 0]\n", + " [ 0 1 111 ... 127 108 0]\n", + " [ 0 1 111 ... 127 108 0]\n", + " ...\n", + " [ 0 89 155 ... 158 32 0]\n", + " [ 0 1 110 ... 127 94 0]\n", + " [ 0 96 40 ... 64 90 0]]\n", + "Generation 4\n", + "[[ 0 1 56 ... 26 81 0]\n", + " [ 0 1 164 ... 127 108 0]\n", + " [ 0 1 110 ... 127 94 0]\n", + " ...\n", + " [ 0 1 111 ... 127 108 0]\n", + " [ 0 1 145 ... 26 22 0]\n", + " [ 0 1 77 ... 127 142 0]]\n", + "Generation 5\n", + "[[ 0 1 111 ... 127 108 0]\n", + " [ 0 1 164 ... 127 108 0]\n", + " [ 0 1 56 ... 127 81 0]\n", + " ...\n", + " [ 0 1 60 ... 127 118 0]\n", + " [ 0 1 154 ... 127 7 0]\n", + " [ 0 1 164 ... 127 94 0]]\n", + "Generation 6\n", + "[[ 0 1 56 ... 26 81 0]\n", + " [ 0 1 164 ... 127 108 0]\n", + " [ 0 1 164 ... 127 108 0]\n", + " ...\n", + " [ 0 1 93 ... 127 108 0]\n", + " [ 0 1 30 ... 127 108 0]\n", + " [ 0 1 111 ... 127 108 0]]\n", + "Generation 7\n", + "[[ 0 1 56 ... 127 81 0]\n", + " [ 0 1 164 ... 127 108 0]\n", + " [ 0 1 164 ... 127 94 0]\n", + " ...\n", + " [ 0 1 164 ... 127 108 0]\n", + " [ 0 1 56 ... 26 81 0]\n", + " [ 0 1 56 ... 127 81 0]]\n", + "Generation 8\n", + "[[ 0 1 164 ... 127 108 0]\n", + " [ 0 1 164 ... 26 108 0]\n", + " [ 0 1 164 ... 127 108 0]\n", + " ...\n", + " [ 0 1 164 ... 127 94 0]\n", + " [ 0 1 164 ... 127 94 0]\n", + " [ 0 1 56 ... 127 94 0]]\n", + "Generation 9\n", + "[[ 0 1 164 ... 127 108 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 108 0]\n", + " ...\n", + " [ 0 1 164 ... 127 94 0]\n", + " [ 0 1 33 ... 26 94 0]\n", + " [ 0 1 110 ... 127 108 0]]\n", + "Generation 10\n", + "[[ 0 1 164 ... 127 94 0]\n", + " [ 0 1 164 ... 127 108 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 108 0]\n", + " [ 0 1 164 ... 127 108 0]\n", + " [ 0 1 164 ... 26 108 0]]\n", + "Generation 11\n", + "[[ 0 1 164 ... 127 94 0]\n", + " [ 0 1 164 ... 127 81 0]\n", + " [ 0 1 164 ... 127 108 0]\n", + " ...\n", + " [ 0 1 164 ... 127 108 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 81 0]]\n", + "Generation 12\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 94 0]\n", + " [ 0 1 164 ... 127 108 0]\n", + " ...\n", + " [ 0 1 164 ... 127 94 0]\n", + " [ 0 1 164 ... 127 81 0]\n", + " [ 0 1 164 ... 127 81 0]]\n", + "Generation 13\n", + "[[ 0 1 164 ... 127 108 0]\n", + " [ 0 1 164 ... 127 94 0]\n", + " [ 0 1 164 ... 127 81 0]\n", + " ...\n", + " [ 0 1 164 ... 127 108 0]\n", + " [ 0 1 164 ... 127 108 0]\n", + " [ 0 1 164 ... 127 81 0]]\n", + "Generation 14\n", + "[[ 0 1 164 ... 127 108 0]\n", + " [ 0 1 164 ... 127 94 0]\n", + " [ 0 1 164 ... 127 81 0]\n", + " ...\n", + " [ 0 1 164 ... 127 94 0]\n", + " [ 0 1 164 ... 127 94 0]\n", + " [ 0 1 164 ... 127 108 0]]\n", + "Generation 15\n", + "[[ 0 1 164 ... 127 108 0]\n", + " [ 0 1 164 ... 127 81 0]\n", + " [ 0 1 164 ... 127 108 0]\n", + " ...\n", + " [ 0 1 164 ... 127 108 0]\n", + " [ 0 1 164 ... 127 94 0]\n", + " [ 0 1 164 ... 127 108 0]]\n", + "Generation 16\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 108 0]\n", + " [ 0 1 164 ... 127 81 0]\n", + " ...\n", + " [ 0 1 164 ... 127 108 0]\n", + " [ 0 1 164 ... 127 108 0]\n", + " [ 0 1 164 ... 127 94 0]]\n", + "Generation 17\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 81 0]\n", + " [ 0 1 164 ... 127 108 0]\n", + " [ 0 1 164 ... 127 108 0]]\n", + "Generation 18\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 108 0]]\n", + "Generation 19\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 20\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 21\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 22\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 23\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 24\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 25\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 26\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 27\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 28\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 29\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 30\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 31\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 32\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 33\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 34\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 35\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 36\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 37\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 38\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 39\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 40\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 41\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 42\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 43\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 44\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 45\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 46\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 47\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 48\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 49\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 50\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 51\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 52\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 53\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 54\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 55\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 56\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 57\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 58\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 59\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 60\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 61\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 62\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 63\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 64\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 65\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 66\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 67\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 68\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 69\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 70\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 71\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 72\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 73\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 74\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 75\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 76\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 77\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 78\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 79\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 80\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 81\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 82\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 83\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 84\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 85\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 86\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 87\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 88\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 89\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 90\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 91\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 92\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 93\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 94\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 95\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 96\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 97\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 98\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 99\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n", + "Generation 100\n", + "[[ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " ...\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]\n", + " [ 0 1 164 ... 127 119 0]]\n" + ] + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "## 8.2 Evaluating the final algorithm \n", + "\n", + "The overall solution can now be assessed." + ], + "metadata": { + "id": "KJY5PcFabWdL" + }, + "id": "KJY5PcFabWdL" + }, + { + "cell_type": "code", + "source": [ + "solution, solution_fitness, solution_idx = ga_instance.best_solution()\n", + "print(f'Generation of best solution: {ga_instance.best_solution_generation}')\n", + "print(\"Fitness value of the best solution = {solution_fitness}\".format(solution_fitness=solution_fitness))\n", + "print(\"Index of the best solution : {solution_idx}\".format(solution_idx=solution_idx))" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "yxGGtFfE3j_1", + "outputId": "d5b68eaa-167f-47c8-aadd-10ecb3eb080c" + }, + "id": "yxGGtFfE3j_1", + "execution_count": 147, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Generation of best solution: 25\n", + "Fitness value of the best solution = 0.0010087414431375688\n", + "Index of the best solution : 0\n" + ] + } + ] + }, + { + "cell_type": "code", + "source": [ + "verify_solution(solution, len(stores))\n", + "solution" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "NKvGE63abjmU", + "outputId": "75249196-afeb-4ee6-9b96-9674ea8493c9" + }, + "id": "NKvGE63abjmU", + "execution_count": 148, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Failed values less than or above max possible value\n" + ] + }, + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "array([ 0, 1, 164, 19, 77, 23, 10, 9, 154, 158, 157, 26, 22,\n", + " 92, 42, 137, 75, 143, 149, 12, 100, 85, 86, 124, 128, 135,\n", + " 147, 54, 24, 3, 58, 123, 153, 51, 29, 69, 20, 110, 59,\n", + " 95, 113, 115, 121, 91, 36, 64, 65, 32, 53, 35, 105, 52,\n", + " 21, 34, 133, 109, 47, 71, 98, 106, 131, 89, 108, 56, 152,\n", + " 150, 7, 38, 43, 94, 8, 132, 155, 4, 16, 84, 90, 27,\n", + " 2, 144, 151, 39, 45, 159, 125, 79, 156, 40, 6, 74, 139,\n", + " 141, 145, 76, 104, 50, 37, 129, 130, 72, 142, 97, 25, 93,\n", + " 134, 126, 138, 140, 148, 120, 96, 28, 160, 116, 18, 112, 31,\n", + " 41, 55, 63, 73, 122, 162, 161, 163, 66, 107, 17, 87, 103,\n", + " 80, 81, 88, 83, 82, 14, 33, 11, 46, 61, 60, 136, 146,\n", + " 15, 70, 44, 48, 67, 78, 111, 13, 62, 30, 118, 114, 99,\n", + " 102, 5, 68, 49, 57, 101, 117, 127, 119, 0])" + ] + }, + "metadata": {}, + "execution_count": 148 + } + ] + }, + { + "cell_type": "code", + "source": [ + "points = [genes.get(stores[id]) + [stores[id]] for id in solution]\n", + "points[:5]" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "G8bOm7PGcPx_", + "outputId": "f78d25f6-3679-489b-c533-2971ac389941" + }, + "id": "G8bOm7PGcPx_", + "execution_count": 150, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "[[51.877854, -0.376379, '12851-253386'],\n", + " [51.877854, -0.376289, '7187-253385'],\n", + " [51.655847, -0.277039, '47771-259784'],\n", + " [51.51402, -0.13925, '12021-10341'],\n", + " [51.54541, -0.16269, '12158-22023']]" + ] + }, + "metadata": {}, + "execution_count": 150 + } + ] + }, + { + "cell_type": "code", + "source": [ + "map = folium.Map(location=[51.509685, -0.118092], zoom_start=6, tiles=\"stamentoner\")\n", + "\n", + "for point in range(0, len(points)):\n", + " folium.Marker(\n", + " [points[point][0], points[point][1]], popup=f'{points[point][2]}'\n", + " ).add_to(map)\n", + "\n", + " try:\n", + " folium.PolyLine([(points[point][0], points[point][1]), \n", + " (points[point+1][0], points[point+1][1])],\n", + " color='red',\n", + " weight=5,\n", + " opacity=0.8).add_to(map)\n", + "\n", + " except IndexError:\n", + " pass" + ], + "metadata": { + "id": "_KtJJkkvcY-E" + }, + "id": "_KtJJkkvcY-E", + "execution_count": 151, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "map" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 531 + }, + "id": "PMH_yECHcaH8", + "outputId": "708aca8b-c6c9-4008-cc54-e14bb37b180f" + }, + "id": "PMH_yECHcaH8", + "execution_count": 152, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "" + ], + "text/html": [ + "
Make this Notebook Trusted to load map: File -> Trust Notebook
" + ] + }, + "metadata": {}, + "execution_count": 152 + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "## 10. Total result \n", + "\n", + "The total resulting distance around London after optimising the solution is:" + ], + "metadata": { + "id": "4ek_Es2DcbXU" + }, + "id": "4ek_Es2DcbXU" + }, + { + "cell_type": "code", + "source": [ + "def distance(solution):\n", + " # loop through the length of the chromosome finding the distance between each\n", + " # gene added \n", + "\n", + " # to increment\n", + " total_dist = 0\n", + "\n", + " for gene in range(0, len(solution)):\n", + "\n", + " # get the lon lat of the two points\n", + " a = genes.get(stores[solution[gene]])\n", + " \n", + " try:\n", + " b = genes.get(stores[solution[gene + 1]])\n", + "\n", + " # find the distance (crow flies)\n", + " dist = geodesic(a, b).kilometers\n", + "\n", + " except IndexError:\n", + " dist = 0\n", + "\n", + " \n", + " total_dist += dist\n", + "\n", + " # to optimise this value in the positive direction the inverse of dist is used\n", + "\n", + " return total_dist " + ], + "metadata": { + "id": "SiCFxx7WcwxV" + }, + "id": "SiCFxx7WcwxV", + "execution_count": 153, + "outputs": [] + }, + { + "cell_type": "code", + "source": [ + "distance(solution)" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "nds6G5tmnJ8K", + "outputId": "fe4ccfc6-a5c0-4e53-c0cd-acfe62bd6052" + }, + "id": "nds6G5tmnJ8K", + "execution_count": 154, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "991.3343075204886" + ] + }, + "metadata": {}, + "execution_count": 154 + } + ] + }, + { + "cell_type": "markdown", + "source": [ + "Which is not too bad for 975 cups of joe. 🥤" + ], + "metadata": { + "id": "bukLealWcxOz" + }, + "id": "bukLealWcxOz" + } + ], + "metadata": { + "colab": { + "provenance": [], + "collapsed_sections": [ + "sEtmfvLsvMC2", + "1sg-1A4Ih1L4" + ], + "toc_visible": true + }, + "gpuClass": "standard", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + }, + "accelerator": "GPU" + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/gacnn/example_image_classification.py b/examples/gacnn/example_image_classification.py new file mode 100644 index 00000000..daaec5a3 --- /dev/null +++ b/examples/gacnn/example_image_classification.py @@ -0,0 +1,128 @@ +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("Generation = {generation}".format(generation=ga_instance.generations_completed)) + print("Fitness = {fitness}".format(fitness=ga_instance.best_solutions_fitness)) + +data_inputs = numpy.load("../data/dataset_inputs.npy") +data_outputs = numpy.load("../data/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. + +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(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 = GACNN_instance.population_networks[solution_idx].predict(data_inputs=data_inputs) +print("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/examples/gann/example_XOR_classification.py b/examples/gann/example_XOR_classification.py new file mode 100644 index 00000000..b5ed6d7a --- /dev/null +++ b/examples/gann/example_XOR_classification.py @@ -0,0 +1,128 @@ +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("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}.") \ No newline at end of file diff --git a/examples/gann/example_classification.py b/examples/gann/example_classification.py new file mode 100644 index 00000000..a1eb1505 --- /dev/null +++ b/examples/gann/example_classification.py @@ -0,0 +1,116 @@ +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(pop_fitness=ga_instance.last_generation_fitness)[1].copy() + +# Holds the fitness value of the previous generation. +last_fitness = 0 + +# Reading the input data. +data_inputs = numpy.load("../data/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("../data/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 = 100 # 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(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) +print("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}.") \ No newline at end of file diff --git a/examples/gann/example_regression.py b/examples/gann/example_regression.py new file mode 100644 index 00000000..427176e9 --- /dev/null +++ b/examples/gann/example_regression.py @@ -0,0 +1,113 @@ +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()[1]}") + print(f"Change = {ga_instance.best_solution()[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("Predictions of the trained network : {predictions}") + +# Calculating some statistics +abs_error = numpy.mean(numpy.abs(predictions - data_outputs)) +print(f"Absolute error : {abs_error}.") diff --git a/examples/gann/example_regression_fish.py b/examples/gann/example_regression_fish.py new file mode 100644 index 00000000..3c5beaa1 --- /dev/null +++ b/examples/gann/example_regression_fish.py @@ -0,0 +1,114 @@ +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()[1]}") + print(f"Change = {ga_instance.best_solution()[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("Predictions of the trained network : {predictions}") + +# Calculating some statistics +abs_error = numpy.mean(numpy.abs(predictions - data_outputs)) +print(f"Absolute error : {abs_error}.") diff --git a/examples/nn/example_XOR_classification.py b/examples/nn/example_XOR_classification.py new file mode 100644 index 00000000..f40d97ff --- /dev/null +++ b/examples/nn/example_XOR_classification.py @@ -0,0 +1,51 @@ +import numpy +import pygad.nn + +""" +This project creates a neural network where the architecture has input and dense layers only. More layers will be added in the future. +The project only implements the forward pass of a neural network and no training algorithm is used. +For training a neural network using the genetic algorithm, check this project (https://github.com/ahmedfgad/NeuralGenetic) in which the genetic algorithm is used for training the network. +Feel free to leave an issue in this project (https://github.com/ahmedfgad/NumPyANN) in case something is not working properly or to ask for questions. I am also available for e-mails at ahmed.f.gad@gmail.com +""" + +# 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=100, + 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}.") \ No newline at end of file diff --git a/examples/nn/example_classification.py b/examples/nn/example_classification.py new file mode 100644 index 00000000..ac5f97a6 --- /dev/null +++ b/examples/nn/example_classification.py @@ -0,0 +1,51 @@ +import numpy +import pygad.nn + +""" +This project creates a neural network where the architecture has input and dense layers only. More layers will be added in the future. +The project only implements the forward pass of a neural network and no training algorithm is used. +For training a neural network using the genetic algorithm, check this project (https://github.com/ahmedfgad/NeuralGenetic) in which the genetic algorithm is used for training the network. +Feel free to leave an issue in this project (https://github.com/ahmedfgad/NumPyANN) in case something is not working properly or to ask for questions. I am also available for e-mails at ahmed.f.gad@gmail.com +""" + +# Reading the data features. Check the 'extract_features.py' script for extracting the features & preparing the outputs of the dataset. +data_inputs = numpy.load("../data/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("../data/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/examples/nn/example_regression.py b/examples/nn/example_regression.py new file mode 100644 index 00000000..3f0f5af7 --- /dev/null +++ b/examples/nn/example_regression.py @@ -0,0 +1,46 @@ +import numpy +import pygad.nn + +""" +This example creates a neural network for regression where the architecture has input and dense layers only. More layers will be added in the future. +The project only implements the forward pass of a neural network and no training algorithm is used. +For training a neural network using the genetic algorithm, check this project (https://github.com/ahmedfgad/NeuralGenetic) in which the genetic algorithm is used for training the network. +Feel free to leave an issue in this project (https://github.com/ahmedfgad/NumPyANN) in case something is not working properly or to ask for questions. I am also available for e-mails at ahmed.f.gad@gmail.com +""" + +# 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 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/examples/nn/example_regression_fish.py b/examples/nn/example_regression_fish.py new file mode 100644 index 00000000..49e6522f --- /dev/null +++ b/examples/nn/example_regression_fish.py @@ -0,0 +1,47 @@ +import numpy +import pygad.nn +import pandas + +""" +This example creates a neural network for regression where the architecture has input and dense layers only. More layers will be added in the future. +The project only implements the forward pass of a neural network and no training algorithm is used. +For training a neural network using the genetic algorithm, check this project (https://github.com/ahmedfgad/NeuralGenetic) in which the genetic algorithm is used for training the network. +Feel free to leave an issue in this project (https://github.com/ahmedfgad/NumPyANN) in case something is not working properly or to ask for questions. I am also available for e-mails at ahmed.f.gad@gmail.com +""" + +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) # 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}.") \ No newline at end of file diff --git a/examples/nn/extract_features.py b/examples/nn/extract_features.py new file mode 100644 index 00000000..e1373056 --- /dev/null +++ b/examples/nn/extract_features.py @@ -0,0 +1,29 @@ +import numpy +import skimage.io, skimage.color, skimage.feature +import os + +dataset_dir = "../data/Fruit360" +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.path.join(dataset_dir+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([dataset_dir, 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("../data/dataset_features.npy", dataset_features) +numpy.save("../data/outputs.npy", outputs) diff --git a/lifecycle.py b/examples/pygad_lifecycle.py similarity index 93% rename from lifecycle.py rename to examples/pygad_lifecycle.py index 9c5c078e..8eeae5b6 100644 --- a/lifecycle.py +++ b/examples/pygad_lifecycle.py @@ -1,48 +1,48 @@ -import pygad -import numpy - -function_inputs = [4,-2,3.5,5,-11,-4.7] -desired_output = 44 - -def fitness_func(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() +import pygad +import numpy + +function_inputs = [4,-2,3.5,5,-11,-4.7] +desired_output = 44 + +def fitness_func(ga_instanse, 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() diff --git a/pygad.py b/pygad.py deleted file mode 100644 index 7d058150..00000000 --- a/pygad.py +++ /dev/null @@ -1,3491 +0,0 @@ -import numpy -import random -import matplotlib.pyplot -import pickle -import time -import warnings - -class GA: - - supported_int_types = [int, numpy.int, numpy.int8, numpy.int16, numpy.int32, numpy.int64, numpy.uint, numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64] - supported_float_types = [float, numpy.float, numpy.float16, numpy.float32, numpy.float64] - supported_int_float_types = supported_int_types + supported_float_types - - def __init__(self, - num_generations, - num_parents_mating, - fitness_func, - initial_population=None, - sol_per_pop=None, - num_genes=None, - init_range_low=-4, - init_range_high=4, - gene_type=float, - parent_selection_type="sss", - keep_parents=-1, - K_tournament=3, - crossover_type="single_point", - crossover_probability=None, - mutation_type="random", - mutation_probability=None, - mutation_by_replacement=False, - mutation_percent_genes='default', - mutation_num_genes=None, - random_mutation_min_val=-1.0, - random_mutation_max_val=1.0, - gene_space=None, - allow_duplicate_genes=True, - on_start=None, - on_fitness=None, - on_parents=None, - on_crossover=None, - on_mutation=None, - callback_generation=None, - on_generation=None, - on_stop=None, - delay_after_gen=0.0, - save_best_solutions=False, - save_solutions=False, - suppress_warnings=False, - stop_criteria=None): - - """ - The constructor of the GA class accepts all parameters required to create an instance of the GA class. It validates such parameters. - - num_generations: Number of generations. - num_parents_mating: Number of solutions to be selected as parents in the mating pool. - - fitness_func: Accepts a function that must accept 2 parameters (a single solution and its index in the population) and return the fitness value of the solution. Available starting from PyGAD 1.0.17 until 1.0.20 with a single parameter representing the solution. Changed in PyGAD 2.0.0 and higher to include the second parameter representing the solution index. - - 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. - sol_per_pop: Number of solutions in the population. - num_genes: Number of parameters in the function. - - 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 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). - - gene_type: The type of the gene. It is assigned to any of these types (int, float, numpy.int, numpy.int8, numpy.int16, numpy.int32, numpy.int64, numpy.uint, numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64, numpy.float, numpy.float16, numpy.float32, numpy.float64) and forces all the genes to be of that type. - - parent_selection_type: Type of parent selection. - keep_parents: If 0, this means no parent in the current population will be used in the next population. If -1, this means all parents in the current population will be used in the next population. If set to a value > 0, then the specified value refers to the number of parents in the current population to be used in the next population. For some parent selection operators like rank selection, the parents are of high quality and it is beneficial to keep them in the next generation. In some other parent selection operators like roulette wheel selection (RWS), it is not guranteed that the parents will be of high quality and thus keeping the parents might degarde the quality of the population. - K_tournament: When the value of 'parent_selection_type' is 'tournament', the 'K_tournament' parameter specifies the number of solutions from which a parent is selected randomly. - - crossover_type: Type of the crossover opreator. 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: The probability of selecting a solution for the crossover operation. If the solution probability is <= crossover_probability, the solution is selected. The value must be between 0 and 1 inclusive. - - mutation_type: Type of the mutation opreator. 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. - mutation_probability: The probability of selecting a gene for the mutation operation. If the gene probability is <= mutation_probability, the gene is selected. It accepts either a single value for fixed mutation or a list/tuple/numpy.ndarray of 2 values for adaptive mutation. The values must be between 0 and 1 inclusive. If specified, then no need for the 2 parameters mutation_percent_genes and mutation_num_genes. - - mutation_by_replacement: An optional bool parameter. 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. - - mutation_percent_genes: Percentage of genes to mutate which defaults to the string 'default' which means 10%. This parameter has no action if any of the 2 parameters mutation_probability or mutation_num_genes exist. - mutation_num_genes: Number of genes to mutate which defaults to None. If the parameter mutation_num_genes exists, then no need for the parameter mutation_percent_genes. This parameter has no action if the mutation_probability parameter exists. - 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. - - on_start: Accepts a function to be called only once before the genetic algorithm starts its evolution. This function must accept a single parameter representing the instance of the genetic algorithm. Added in PyGAD 2.6.0. - on_fitness: Accepts a function to be called after calculating the fitness values of all solutions in the population. This function must accept 2 parameters: the first one represents the instance of the genetic algorithm and the second one is a list of all solutions' fitness values. Added in PyGAD 2.6.0. - on_parents: Accepts a function to be called after selecting the parents that mates. This function must accept 2 parameters: the first one represents the instance of the genetic algorithm and the second one represents the selected parents. Added in PyGAD 2.6.0. - on_crossover: 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: 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. - callback_generation: 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 "stop", then the run() method stops without completing the other generations. Starting from PyGAD 2.6.0, the callback_generation parameter is deprecated and should be replaced by the on_generation parameter. - on_generation: 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 "stop", then the run() method stops without completing the other generations. Added in PyGAD 2.6.0. - on_stop: 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. - - delay_after_gen: Added in PyGAD 2.4.0. It accepts a non-negative number specifying the number of 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. - - save_best_solutions: Added in PyGAD 2.9.0 and its type is bool. If True, then the best solution in each generation is saved into the 'best_solutions' attribute. Use this parameter with caution as it may cause memory overflow when either the number of generations or the number of genes is large. - save_solutions: Added in PyGAD 2.15.0 and its type is bool. If True, then all solutions in each generation are saved into the 'solutions' attribute. Use this 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. - - suppress_warnings: Added in PyGAD 2.10.0 and its type is bool. If True, then no warning messages will be displayed. It defaults to False. - - allow_duplicate_genes: 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: Added in PyGAD 2.15.0. It is assigned to some criteria to stop the evolution if at least one criterion holds. - """ - - # If suppress_warnings is bool and its valud is False, then print warning messages. - if type(suppress_warnings) is bool: - self.suppress_warnings = suppress_warnings - else: - self.valid_parameters = False - raise TypeError("The expected type of the 'suppress_warnings' parameter is bool but {suppress_warnings_type} found.".format(suppress_warnings_type=type(suppress_warnings))) - - # Validating mutation_by_replacement - if not (type(mutation_by_replacement) is bool): - self.valid_parameters = False - raise TypeError("The expected type of the 'mutation_by_replacement' parameter is bool but ({mutation_by_replacement_type}) found.".format(mutation_by_replacement_type=type(mutation_by_replacement))) - - self.mutation_by_replacement = mutation_by_replacement - - # Validate gene_space - self.gene_space_nested = False - if type(gene_space) is type(None): - pass - elif type(gene_space) in [list, tuple, range, numpy.ndarray]: - if len(gene_space) == 0: - self.valid_parameters = False - raise TypeError("'gene_space' cannot be empty (i.e. its length must be >= 0).") - else: - for index, el in enumerate(gene_space): - if type(el) in [list, tuple, range, numpy.ndarray]: - if len(el) == 0: - self.valid_parameters = False - raise TypeError("The element indexed {index} of 'gene_space' with type {el_type} cannot be empty (i.e. its length must be >= 0).".format(index=index, el_type=type(el))) - else: - for val in el: - if not (type(val) in [type(None)] + GA.supported_int_float_types): - raise TypeError("All values in the sublists inside the 'gene_space' attribute must be numeric of type int/float/None but ({val}) of type {typ} found.".format(val=val, typ=type(val))) - self.gene_space_nested = True - elif type(el) == type(None): - pass - # self.gene_space_nested = True - elif type(el) is dict: - if len(el.items()) == 2: - if ('low' in el.keys()) and ('high' in el.keys()): - pass - else: - self.valid_parameters = False - raise TypeError("When an element in the 'gene_space' parameter is of type dict, then it can have the keys 'low', 'high', and 'step' (optional) but the following keys found: {gene_space_dict_keys}".format(gene_space_dict_keys=el.keys())) - elif len(el.items()) == 3: - if ('low' in el.keys()) and ('high' in el.keys()) and ('step' in el.keys()): - pass - else: - self.valid_parameters = False - raise TypeError("When an element in the 'gene_space' parameter is of type dict, then it can have the keys 'low', 'high', and 'step' (optional) but the following keys found: {gene_space_dict_keys}".format(gene_space_dict_keys=el.keys())) - else: - self.valid_parameters = False - raise TypeError("When an element in the 'gene_space' parameter is of type dict, then it must have only 2 items but ({num_items}) items found.".format(num_items=len(el.items()))) - self.gene_space_nested = True - elif not (type(el) in GA.supported_int_float_types): - self.valid_parameters = False - raise TypeError("Unexpected type {el_type} for the element indexed {index} of 'gene_space'. The accepted types are list/tuple/range/numpy.ndarray of numbers, a single number (int/float), or None.".format(index=index, el_type=type(el))) - - elif type(gene_space) is dict: - if len(gene_space.items()) == 2: - if ('low' in gene_space.keys()) and ('high' in gene_space.keys()): - pass - else: - self.valid_parameters = False - raise TypeError("When the 'gene_space' parameter is of type dict, then it can have only the keys 'low', 'high', and 'step' (optional) but the following keys found: {gene_space_dict_keys}".format(gene_space_dict_keys=gene_space.keys())) - elif len(gene_space.items()) == 3: - if ('low' in gene_space.keys()) and ('high' in gene_space.keys()) and ('step' in gene_space.keys()): - pass - else: - self.valid_parameters = False - raise TypeError("When the 'gene_space' parameter is of type dict, then it can have only the keys 'low', 'high', and 'step' (optional) but the following keys found: {gene_space_dict_keys}".format(gene_space_dict_keys=gene_space.keys())) - else: - self.valid_parameters = False - raise TypeError("When the 'gene_space' parameter is of type dict, then it must have only 2 items but ({num_items}) items found.".format(num_items=len(gene_space.items()))) - - else: - self.valid_parameters = False - raise TypeError("The expected type of 'gene_space' is list, tuple, range, or numpy.ndarray but ({gene_space_type}) found.".format(gene_space_type=type(gene_space))) - - self.gene_space = gene_space - - # Validate init_range_low and init_range_high - if type(init_range_low) in GA.supported_int_float_types: - if type(init_range_high) in GA.supported_int_float_types: - self.init_range_low = init_range_low - self.init_range_high = init_range_high - else: - self.valid_parameters = False - raise ValueError("The value passed to the 'init_range_high' parameter must be either integer or floating-point number but the value ({init_range_high_value}) of type {init_range_high_type} found.".format(init_range_high_value=init_range_high, init_range_high_type=type(init_range_high))) - else: - self.valid_parameters = False - raise ValueError("The value passed to the 'init_range_low' parameter must be either integer or floating-point number but the value ({init_range_low_value}) of type {init_range_low_type} found.".format(init_range_low_value=init_range_low, init_range_low_type=type(init_range_low))) - - - # Validate random_mutation_min_val and random_mutation_max_val - if type(random_mutation_min_val) in GA.supported_int_float_types: - if type(random_mutation_max_val) in GA.supported_int_float_types: - if random_mutation_min_val == random_mutation_max_val: - if not self.suppress_warnings: warnings.warn("The values of the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val' are equal and this causes a fixed change to all genes.") - else: - self.valid_parameters = False - raise TypeError("The expected type of the 'random_mutation_max_val' parameter is numeric but ({random_mutation_max_val_type}) found.".format(random_mutation_max_val_type=type(random_mutation_max_val))) - else: - self.valid_parameters = False - raise TypeError("The expected type of the 'random_mutation_min_val' parameter is numeric but ({random_mutation_min_val_type}) found.".format(random_mutation_min_val_type=type(random_mutation_min_val))) - self.random_mutation_min_val = random_mutation_min_val - self.random_mutation_max_val = random_mutation_max_val - - # Validate gene_type - if gene_type in GA.supported_int_float_types: - self.gene_type = [gene_type, None] - self.gene_type_single = True - # A single data type of float with precision. - elif len(gene_type) == 2 and gene_type[0] in GA.supported_float_types and (type(gene_type[1]) in GA.supported_int_types or gene_type[1] is None): - self.gene_type = gene_type - self.gene_type_single = True - elif type(gene_type) in [list, tuple, numpy.ndarray]: - if not len(gene_type) == num_genes: - self.valid_parameters = False - raise TypeError("When the parameter 'gene_type' is nested, then it can be either [float, int] or with length equal to the value passed to the 'num_genes' parameter. Instead, value {gene_type_val} with len(gene_type) ({len_gene_type}) != len(num_genes) ({num_genes}) found.".format(gene_type_val=gene_type, len_gene_type=len(gene_type), num_genes=num_genes)) - for gene_type_idx, gene_type_val in enumerate(gene_type): - if gene_type_val in GA.supported_float_types: - # If the gene type is float and no precision is passed, set it to None. - gene_type[gene_type_idx] = [gene_type_val, None] - elif gene_type_val in GA.supported_int_types: - gene_type[gene_type_idx] = [gene_type_val, None] - elif type(gene_type_val) in [list, tuple, numpy.ndarray]: - # A float type is expected in a list/tuple/numpy.ndarray of length 2. - if len(gene_type_val) == 2: - if gene_type_val[0] in GA.supported_float_types: - if type(gene_type_val[1]) in GA.supported_int_types: - pass - else: - self.valid_parameters = False - raise ValueError("In the 'gene_type' parameter, the precision for float gene data types must be an integer but the element {gene_type_val} at index {gene_type_idx} has a precision of {gene_type_precision_val} with type {gene_type_type} .".format(gene_type_val=gene_type_val, gene_type_precision_val=gene_type_val[1], gene_type_type=gene_type_val[0], gene_type_idx=gene_type_idx)) - else: - self.valid_parameters = False - raise ValueError("In the 'gene_type' parameter, a precision is expected only for float gene data types but the element {gene_type} found at index {gene_type_idx}. Note that the data type must be at index 0 followed by precision at index 1.".format(gene_type=gene_type_val, gene_type_idx=gene_type_idx)) - else: - self.valid_parameters = False - raise ValueError("In the 'gene_type' parameter, a precision is specified in a list/tuple/numpy.ndarray of length 2 but value ({gene_type_val}) of type {gene_type_type} with length {gene_type_length} found at index {gene_type_idx}.".format(gene_type_val=gene_type_val, gene_type_type=type(gene_type_val), gene_type_idx=gene_type_idx, gene_type_length=len(gene_type_val))) - else: - self.valid_parameters = False - raise ValueError("When a list/tuple/numpy.ndarray is assigned to the 'gene_type' parameter, then its elements must be of integer, floating-point, list, tuple, or numpy.ndarray data types but the value ({gene_type_val}) of type {gene_type_type} found at index {gene_type_idx}.".format(gene_type_val=gene_type_val, gene_type_type=type(gene_type_val), gene_type_idx=gene_type_idx)) - self.gene_type = gene_type - self.gene_type_single = False - else: - self.valid_parameters = False - raise ValueError("The value passed to the 'gene_type' parameter must be either a single integer, floating-point, list, tuple, or numpy.ndarray but ({gene_type_val}) of type {gene_type_type} found.".format(gene_type_val=gene_type, gene_type_type=type(gene_type))) - - # Build the initial population - if initial_population is None: - if (sol_per_pop is None) or (num_genes is None): - self.valid_parameters = False - raise ValueError("Error creating the initail population\n\nWhen the parameter initial_population is None, then neither of the 2 parameters sol_per_pop and num_genes can be None at the same time.\nThere are 2 options to prepare the initial population:\n1) Create an initial population and assign it to the initial_population parameter. In this case, the values of the 2 parameters sol_per_pop and num_genes will be deduced.\n2) Allow the genetic algorithm to create the initial population automatically by passing valid integer values to the sol_per_pop and num_genes parameters.") - elif (type(sol_per_pop) is int) and (type(num_genes) is int): - # Validating the number of solutions in the population (sol_per_pop) - if sol_per_pop <= 0: - self.valid_parameters = False - raise ValueError("The number of solutions in the population (sol_per_pop) must be > 0 but ({sol_per_pop}) 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".format(sol_per_pop=sol_per_pop)) - # Validating the number of gene. - if (num_genes <= 0): - self.valid_parameters = False - raise ValueError("The number of genes cannot be <= 0 but ({num_genes}) found.\n".format(num_genes=num_genes)) - # When initial_population=None and the 2 parameters sol_per_pop and num_genes have valid integer values, then the initial population is created. - # Inside the initialize_population() method, the initial_population attribute is assigned to keep the initial population accessible. - self.num_genes = num_genes # Number of genes in the solution. - - # 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 TypeError("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 ({num_genes})".format(len_gene_space=len(gene_space), num_genes=self.num_genes)) - - self.sol_per_pop = sol_per_pop # Number of solutions in the population. - self.initialize_population(self.init_range_low, self.init_range_high, allow_duplicate_genes, True, self.gene_type) - else: - self.valid_parameters = False - raise TypeError("The expected type of both the sol_per_pop and num_genes parameters is int but ({sol_per_pop_type}) and {num_genes_type} found.".format(sol_per_pop_type=type(sol_per_pop), num_genes_type=type(num_genes))) - elif numpy.array(initial_population).ndim != 2: - self.valid_parameters = False - raise ValueError("A 2D list is expected to the initail_population parameter but a ({initial_population_ndim}-D) list found.".format(initial_population_ndim=numpy.array(initial_population).ndim)) - else: - # Forcing the initial_population array to have the data type assigned to the gene_type parameter. - if self.gene_type_single == True: - if self.gene_type[1] == None: - self.initial_population = numpy.array(initial_population, dtype=self.gene_type[0]) - else: - self.initial_population = numpy.round(numpy.array(initial_population, dtype=self.gene_type[0]), self.gene_type[1]) - else: - initial_population = numpy.array(initial_population) - self.initial_population = numpy.zeros(shape=(initial_population.shape[0], initial_population.shape[1]), dtype=object) - for gene_idx in range(initial_population.shape[1]): - if self.gene_type[gene_idx][1] is None: - self.initial_population[:, gene_idx] = numpy.asarray(initial_population[:, gene_idx], - dtype=self.gene_type[gene_idx][0]) - else: - self.initial_population[:, gene_idx] = numpy.round(numpy.asarray(initial_population[:, gene_idx], - dtype=self.gene_type[gene_idx][0]), - self.gene_type[gene_idx][1]) - - self.population = self.initial_population.copy() # A NumPy array holding the initial population. - self.num_genes = self.initial_population.shape[1] # Number of genes in the solution. - self.sol_per_pop = self.initial_population.shape[0] # Number of solutions in the population. - self.pop_size = (self.sol_per_pop,self.num_genes) # The population size. - - # Round initial_population and population - self.initial_population = self.round_genes(self.initial_population) - self.population = self.round_genes(self.population) - - # 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 TypeError("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 ({len_num_genes})".format(len_gene_space=len(gene_space), len_num_genes=self.num_genes)) - - # Validating the number of parents to be selected for mating (num_parents_mating) - if num_parents_mating <= 0: - self.valid_parameters = False - raise ValueError("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".format(num_parents_mating=num_parents_mating)) - - # 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("The number of parents to select for mating ({num_parents_mating}) cannot be greater than the number of solutions in the population ({sol_per_pop}) (i.e., num_parents_mating must always be <= sol_per_pop).\n".format(num_parents_mating=num_parents_mating, sol_per_pop=self.sol_per_pop)) - - self.num_parents_mating = num_parents_mating - - # 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): - self.crossover = None - elif callable(crossover_type): - # Check if the crossover_type is a function that accepts 2 paramaters. - if (crossover_type.__code__.co_argcount == 3): - # The crossover function assigned to the crossover_type parameter is validated. - self.crossover = crossover_type - else: - self.valid_parameters = False - raise ValueError("When 'crossover_type' is assigned to a function, then this crossover function must accept 2 parameters:\n1) The selected parents.\n2) The size of the offspring to be produced.3) The instance from the pygad.GA class to retrieve any property like population, gene data type, gene space, etc.\n\nThe passed crossover function named '{funcname}' accepts {argcount} parameter(s).".format(funcname=crossover_type.__code__.co_name, argcount=crossover_type.__code__.co_argcount)) - elif not (type(crossover_type) is str): - self.valid_parameters = False - raise TypeError("The expected type of the 'crossover_type' parameter is either callable or str but ({crossover_type}) found.".format(crossover_type=type(crossover_type))) - else: # type crossover_type is str - crossover_type = crossover_type.lower() - if (crossover_type == "single_point"): - self.crossover = self.single_point_crossover - elif (crossover_type == "two_points"): - self.crossover = self.two_points_crossover - elif (crossover_type == "uniform"): - self.crossover = self.uniform_crossover - elif (crossover_type == "scattered"): - self.crossover = self.scattered_crossover - else: - self.valid_parameters = False - raise ValueError("Undefined crossover type. \nThe assigned value to the crossover_type ({crossover_type}) parameter does not refer to one of the supported crossover types which are: \n-single_point (for single point crossover)\n-two_points (for two points crossover)\n-uniform (for uniform crossover)\n-scattered (for scattered crossover).\n".format(crossover_type=crossover_type)) - - self.crossover_type = crossover_type - - # Calculate the value of crossover_probability - if crossover_probability is None: - self.crossover_probability = None - elif type(crossover_probability) in GA.supported_int_float_types: - if crossover_probability >= 0 and crossover_probability <= 1: - self.crossover_probability = crossover_probability - else: - self.valid_parameters = False - raise ValueError("The value assigned to the 'crossover_probability' parameter must be between 0 and 1 inclusive but ({crossover_probability_value}) found.".format(crossover_probability_value=crossover_probability)) - else: - self.valid_parameters = False - raise ValueError("Unexpected type for the 'crossover_probability' parameter. Float is expected but ({crossover_probability_value}) of type {crossover_probability_type} found.".format(crossover_probability_value=crossover_probability, crossover_probability_type=type(crossover_probability))) - - # 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 - if mutation_type is None: - self.mutation = None - elif callable(mutation_type): - # Check if the mutation_type is a function that accepts 1 paramater. - if (mutation_type.__code__.co_argcount == 2): - # The mutation function assigned to the mutation_type parameter is validated. - self.mutation = mutation_type - else: - self.valid_parameters = False - raise ValueError("When 'mutation_type' is assigned to a function, then this mutation function must accept 2 parameters:\n1) The offspring to be mutated.\n2) The instance from the pygad.GA class to retrieve any property like population, gene data type, gene space, etc.\n\nThe passed mutation function named '{funcname}' accepts {argcount} parameter(s).".format(funcname=mutation_type.__code__.co_name, argcount=mutation_type.__code__.co_argcount)) - elif not (type(mutation_type) is str): - self.valid_parameters = False - raise TypeError("The expected type of the 'mutation_type' parameter is either callable or str but ({mutation_type}) found.".format(mutation_type=type(mutation_type))) - else: # type mutation_type is str - mutation_type = mutation_type.lower() - if (mutation_type == "random"): - self.mutation = self.random_mutation - elif (mutation_type == "swap"): - self.mutation = self.swap_mutation - elif (mutation_type == "scramble"): - self.mutation = self.scramble_mutation - elif (mutation_type == "inversion"): - self.mutation = self.inversion_mutation - elif (mutation_type == "adaptive"): - self.mutation = self.adaptive_mutation - else: - self.valid_parameters = False - raise ValueError("Undefined mutation type. \nThe assigned string value to the 'mutation_type' parameter ({mutation_type}) does not refer to one of the supported mutation types which are: \n-random (for random mutation)\n-swap (for swap mutation)\n-inversion (for inversion mutation)\n-scramble (for scramble mutation)\n-adaptive (for adaptive mutation).\n".format(mutation_type=mutation_type)) - - self.mutation_type = mutation_type - - # Calculate the value of mutation_probability - if not (self.mutation_type is None): - if mutation_probability is None: - self.mutation_probability = None - elif (mutation_type != "adaptive"): - # Mutation probability is fixed not adaptive. - if type(mutation_probability) in GA.supported_int_float_types: - if mutation_probability >= 0 and mutation_probability <= 1: - self.mutation_probability = mutation_probability - else: - self.valid_parameters = False - raise ValueError("The value assigned to the 'mutation_probability' parameter must be between 0 and 1 inclusive but ({mutation_probability_value}) found.".format(mutation_probability_value=mutation_probability)) - else: - self.valid_parameters = False - raise ValueError("Unexpected type for the 'mutation_probability' parameter. A numeric value is expected but ({mutation_probability_value}) of type {mutation_probability_type} found.".format(mutation_probability_value=mutation_probability, mutation_probability_type=type(mutation_probability))) - else: - # Mutation probability is adaptive not fixed. - if type(mutation_probability) in [list, tuple, numpy.ndarray]: - if len(mutation_probability) == 2: - for el in mutation_probability: - if type(el) in GA.supported_int_float_types: - if el >= 0 and el <= 1: - pass - else: - self.valid_parameters = False - raise ValueError("The values assigned to the 'mutation_probability' parameter must be between 0 and 1 inclusive but ({mutation_probability_value}) found.".format(mutation_probability_value=el)) - else: - self.valid_parameters = False - raise ValueError("Unexpected type for a value assigned to the 'mutation_probability' parameter. A numeric value is expected but ({mutation_probability_value}) of type {mutation_probability_type} found.".format(mutation_probability_value=el, mutation_probability_type=type(el))) - if mutation_probability[0] < mutation_probability[1]: - if not self.suppress_warnings: warnings.warn("The first element in the 'mutation_probability' parameter is {first_el} which is smaller than the second element {second_el}. This means the mutation rate for the high-quality solutions is higher than the mutation rate of the low-quality ones. This causes high disruption in the high qualitiy solutions while making little changes in the low quality solutions. Please make the first element higher than the second element.".format(first_el=mutation_probability[0], second_el=mutation_probability[1])) - self.mutation_probability = mutation_probability - else: - self.valid_parameters = False - raise ValueError("When mutation_type='adaptive', then the 'mutation_probability' parameter must have only 2 elements but ({mutation_probability_length}) element(s) found.".format(mutation_probability_length=len(mutation_probability))) - else: - self.valid_parameters = False - raise ValueError("Unexpected type for the 'mutation_probability' parameter. When mutation_type='adaptive', then list/tuple/numpy.ndarray is expected but ({mutation_probability_value}) of type {mutation_probability_type} found.".format(mutation_probability_value=mutation_probability, mutation_probability_type=type(mutation_probability))) - else: - pass - - # Calculate the value of mutation_num_genes - if not (self.mutation_type is None): - if mutation_num_genes is None: - # The mutation_num_genes parameter does not exist. Checking whether adaptive mutation is used. - if (mutation_type != "adaptive"): - # The percent of genes to mutate is fixed not adaptive. - if mutation_percent_genes == 'default'.lower(): - mutation_percent_genes = 10 - # Based on the mutation percentage in the 'mutation_percent_genes' parameter, the number of genes to mutate is calculated. - mutation_num_genes = numpy.uint32((mutation_percent_genes*self.num_genes)/100) - # Based on the mutation percentage of genes, if the number of selected genes for mutation is less than the least possible value which is 1, then the number will be set to 1. - if mutation_num_genes == 0: - if self.mutation_probability is None: - if not self.suppress_warnings: warnings.warn("The percentage of genes to mutate (mutation_percent_genes={mutation_percent}) resutled in selecting ({mutation_num}) genes. The number of genes to mutate is set to 1 (mutation_num_genes=1).\nIf you do not want to mutate any gene, please set mutation_type=None.".format(mutation_percent=mutation_percent_genes, mutation_num=mutation_num_genes)) - mutation_num_genes = 1 - - elif type(mutation_percent_genes) in GA.supported_int_float_types: - if (mutation_percent_genes <= 0 or mutation_percent_genes > 100): - self.valid_parameters = False - raise ValueError("The percentage of selected genes for mutation (mutation_percent_genes) must be > 0 and <= 100 but ({mutation_percent_genes}) found.\n".format(mutation_percent_genes=mutation_percent_genes)) - else: - # If mutation_percent_genes equals the string "default", then it is replaced by the numeric value 10. - if mutation_percent_genes == 'default'.lower(): - mutation_percent_genes = 10 - - # Based on the mutation percentage in the 'mutation_percent_genes' parameter, the number of genes to mutate is calculated. - mutation_num_genes = numpy.uint32((mutation_percent_genes*self.num_genes)/100) - # Based on the mutation percentage of genes, if the number of selected genes for mutation is less than the least possible value which is 1, then the number will be set to 1. - if mutation_num_genes == 0: - if self.mutation_probability is None: - if not self.suppress_warnings: warnings.warn("The percentage of genes to mutate (mutation_percent_genes={mutation_percent}) resutled in selecting ({mutation_num}) genes. The number of genes to mutate is set to 1 (mutation_num_genes=1).\nIf you do not want to mutate any gene, please set mutation_type=None.".format(mutation_percent=mutation_percent_genes, mutation_num=mutation_num_genes)) - mutation_num_genes = 1 - else: - self.valid_parameters = False - raise ValueError("Unexpected value or type of the 'mutation_percent_genes' parameter. It only accepts the string 'default' or a numeric value but ({mutation_percent_genes_value}) of type {mutation_percent_genes_type} found.".format(mutation_percent_genes_value=mutation_percent_genes, mutation_percent_genes_type=type(mutation_percent_genes))) - else: - # The percent of genes to mutate is adaptive not fixed. - if type(mutation_percent_genes) in [list, tuple, numpy.ndarray]: - if len(mutation_percent_genes) == 2: - mutation_num_genes = numpy.zeros_like(mutation_percent_genes, dtype=numpy.uint32) - for idx, el in enumerate(mutation_percent_genes): - if type(el) in GA.supported_int_float_types: - if (el <= 0 or el > 100): - self.valid_parameters = False - raise ValueError("The values assigned to the 'mutation_percent_genes' must be > 0 and <= 100 but ({mutation_percent_genes}) found.\n".format(mutation_percent_genes=mutation_percent_genes)) - else: - self.valid_parameters = False - raise ValueError("Unexpected type for a value assigned to the 'mutation_percent_genes' parameter. An integer value is expected but ({mutation_percent_genes_value}) of type {mutation_percent_genes_type} found.".format(mutation_percent_genes_value=el, mutation_percent_genes_type=type(el))) - # At this point of the loop, the current value assigned to the parameter 'mutation_percent_genes' is validated. - # Based on the mutation percentage in the 'mutation_percent_genes' parameter, the number of genes to mutate is calculated. - mutation_num_genes[idx] = numpy.uint32((mutation_percent_genes[idx]*self.num_genes)/100) - # Based on the mutation percentage of genes, if the number of selected genes for mutation is less than the least possible value which is 1, then the number will be set to 1. - if mutation_num_genes[idx] == 0: - if not self.suppress_warnings: warnings.warn("The percentage of genes to mutate ({mutation_percent}) resutled in selecting ({mutation_num}) genes. The number of genes to mutate is set to 1 (mutation_num_genes=1).\nIf you do not want to mutate any gene, please set mutation_type=None.".format(mutation_percent=mutation_percent_genes[idx], mutation_num=mutation_num_genes[idx])) - mutation_num_genes[idx] = 1 - if mutation_percent_genes[0] < mutation_percent_genes[1]: - if not self.suppress_warnings: warnings.warn("The first element in the 'mutation_percent_genes' parameter is ({first_el}) which is smaller than the second element ({second_el}).\nThis means the mutation rate for the high-quality solutions is higher than the mutation rate of the low-quality ones. This causes high disruption in the high qualitiy solutions while making little changes in the low quality solutions.\nPlease make the first element higher than the second element.".format(first_el=mutation_percent_genes[0], second_el=mutation_percent_genes[1])) - # At this point outside the loop, all values of the parameter 'mutation_percent_genes' are validated. Eveyrthing is OK. - else: - self.valid_parameters = False - raise ValueError("When mutation_type='adaptive', then the 'mutation_percent_genes' parameter must have only 2 elements but ({mutation_percent_genes_length}) element(s) found.".format(mutation_percent_genes_length=len(mutation_percent_genes))) - else: - if self.mutation_probability is None: - self.valid_parameters = False - raise ValueError("Unexpected type for the 'mutation_percent_genes' parameter. When mutation_type='adaptive', then the 'mutation_percent_genes' parameter should exist and assigned a list/tuple/numpy.ndarray with 2 values but ({mutation_percent_genes_value}) found.".format(mutation_percent_genes_value=mutation_percent_genes)) - # The mutation_num_genes parameter exists. Checking whether adaptive mutation is used. - elif (mutation_type != "adaptive"): - # Number of genes to mutate is fixed not adaptive. - if type(mutation_num_genes) in GA.supported_int_types: - if (mutation_num_genes <= 0): - self.valid_parameters = False - raise ValueError("The number of selected genes for mutation (mutation_num_genes) cannot be <= 0 but ({mutation_num_genes}) found. If you do not want to use mutation, please set mutation_type=None\n".format(mutation_num_genes=mutation_num_genes)) - elif (mutation_num_genes > self.num_genes): - self.valid_parameters = False - raise ValueError("The number of selected genes for mutation (mutation_num_genes), which is ({mutation_num_genes}), cannot be greater than the number of genes ({num_genes}).\n".format(mutation_num_genes=mutation_num_genes, num_genes=self.num_genes)) - else: - self.valid_parameters = False - raise ValueError("The 'mutation_num_genes' parameter is expected to be a positive integer but the value ({mutation_num_genes_value}) of type {mutation_num_genes_type} found.\n".format(mutation_num_genes_value=mutation_num_genes, mutation_num_genes_type=type(mutation_num_genes))) - else: - # Number of genes to mutate is adaptive not fixed. - if type(mutation_num_genes) in [list, tuple, numpy.ndarray]: - if len(mutation_num_genes) == 2: - for el in mutation_num_genes: - if type(el) in GA.supported_int_types: - if (el <= 0): - self.valid_parameters = False - raise ValueError("The values assigned to the 'mutation_num_genes' cannot be <= 0 but ({mutation_num_genes_value}) found. If you do not want to use mutation, please set mutation_type=None\n".format(mutation_num_genes_value=el)) - elif (el > self.num_genes): - self.valid_parameters = False - raise ValueError("The values assigned to the 'mutation_num_genes' cannot be greater than the number of genes ({num_genes}) but ({mutation_num_genes_value}) found.\n".format(mutation_num_genes_value=el, num_genes=self.num_genes)) - else: - self.valid_parameters = False - raise ValueError("Unexpected type for a value assigned to the 'mutation_num_genes' parameter. An integer value is expected but ({mutation_num_genes_value}) of type {mutation_num_genes_type} found.".format(mutation_num_genes_value=el, mutation_num_genes_type=type(el))) - # At this point of the loop, the current value assigned to the parameter 'mutation_num_genes' is validated. - if mutation_num_genes[0] < mutation_num_genes[1]: - if not self.suppress_warnings: warnings.warn("The first element in the 'mutation_num_genes' parameter is {first_el} which is smaller than the second element {second_el}. This means the mutation rate for the high-quality solutions is higher than the mutation rate of the low-quality ones. This causes high disruption in the high qualitiy solutions while making little changes in the low quality solutions. Please make the first element higher than the second element.".format(first_el=mutation_num_genes[0], second_el=mutation_num_genes[1])) - # At this point outside the loop, all values of the parameter 'mutation_num_genes' are validated. Eveyrthing is OK. - else: - self.valid_parameters = False - raise ValueError("When mutation_type='adaptive', then the 'mutation_num_genes' parameter must have only 2 elements but ({mutation_num_genes_length}) element(s) found.".format(mutation_num_genes_length=len(mutation_num_genes))) - else: - self.valid_parameters = False - raise ValueError("Unexpected type for the 'mutation_num_genes' parameter. When mutation_type='adaptive', then list/tuple/numpy.ndarray is expected but ({mutation_num_genes_value}) of type {mutation_num_genes_type} found.".format(mutation_num_genes_value=mutation_num_genes, mutation_num_genes_type=type(mutation_num_genes))) - else: - pass - - # Validating mutation_by_replacement and mutation_type - if self.mutation_type != "random" and self.mutation_by_replacement: - if not self.suppress_warnings: warnings.warn("The mutation_by_replacement parameter is set to True while the mutation_type parameter is not set to random but ({mut_type}). Note that the mutation_by_replacement parameter has an effect only when mutation_type='random'.".format(mut_type=mutation_type)) - - # 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.") - - # 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 callable(parent_selection_type): - # Check if the parent_selection_type is a function that accepts 3 paramaters. - if (parent_selection_type.__code__.co_argcount == 3): - # population: Added in PyGAD 2.16.0. It should used only to support custom parent selection functions. Otherwise, it should be left to None to retirve the population by self.population. - # The parent selection function assigned to the parent_selection_type parameter is validated. - self.select_parents = parent_selection_type - else: - self.valid_parameters = False - raise ValueError("When 'parent_selection_type' is assigned to a user-defined function, then this parent selection function 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 to retrieve any property like population, gene data type, gene space, etc.\n\nThe passed parent selection function named '{funcname}' accepts {argcount} parameter(s).".format(funcname=parent_selection_type.__code__.co_name, argcount=parent_selection_type.__code__.co_argcount)) - elif not (type(parent_selection_type) is str): - self.valid_parameters = False - raise TypeError("The expected type of the 'parent_selection_type' parameter is either callable or str but ({parent_selection_type}) found.".format(parent_selection_type=type(parent_selection_type))) - else: - parent_selection_type = parent_selection_type.lower() - if (parent_selection_type == "sss"): - self.select_parents = self.steady_state_selection - elif (parent_selection_type == "rws"): - self.select_parents = self.roulette_wheel_selection - elif (parent_selection_type == "sus"): - self.select_parents = self.stochastic_universal_selection - elif (parent_selection_type == "random"): - self.select_parents = self.random_selection - elif (parent_selection_type == "tournament"): - self.select_parents = self.tournament_selection - elif (parent_selection_type == "rank"): - self.select_parents = self.rank_selection - else: - self.valid_parameters = False - raise ValueError("Undefined parent selection type: {parent_selection_type}. \nThe assigned value to the 'parent_selection_type' parameter does not refer to one of the supported parent selection techniques which are: \n-sss (for steady state selection)\n-rws (for roulette wheel selection)\n-sus (for stochastic universal selection)\n-rank (for rank selection)\n-random (for random selection)\n-tournament (for tournament selection).\n".format(parent_selection_type=parent_selection_type)) - - # For tournament selection, validate the K value. - if(parent_selection_type == "tournament"): - if (K_tournament > self.sol_per_pop): - K_tournament = self.sol_per_pop - if not self.suppress_warnings: warnings.warn("K of the tournament selection ({K_tournament}) should not be greater than the number of solutions within the population ({sol_per_pop}).\nK will be clipped to be equal to the number of solutions in the population (sol_per_pop).\n".format(K_tournament=K_tournament, sol_per_pop=self.sol_per_pop)) - elif (K_tournament <= 0): - self.valid_parameters = False - raise ValueError("K of the tournament selection cannot be <=0 but ({K_tournament}) found.\n".format(K_tournament=K_tournament)) - - self.K_tournament = K_tournament - - # Validating the number of parents to keep in the next population: keep_parents - if (keep_parents > self.sol_per_pop or keep_parents > self.num_parents_mating or keep_parents < -1): - self.valid_parameters = False - raise ValueError("Incorrect value to the keep_parents parameter: {keep_parents}. \nThe assigned value to the keep_parent parameter must satisfy the following conditions: \n1) Less than or equal to sol_per_pop\n2) Less than or equal to num_parents_mating\n3) Greater than or equal to -1.".format(keep_parents=keep_parents)) - - self.keep_parents = keep_parents - - if parent_selection_type == "sss" and self.keep_parents == 0: - if not self.suppress_warnings: warnings.warn("The steady-state parent (sss) selection operator is used despite that no parents are kept in the next generation.") - - # Validate keep_parents. - if (self.keep_parents == -1): # Keep all parents in the next population. - self.num_offspring = self.sol_per_pop - self.num_parents_mating - elif (self.keep_parents == 0): # Keep no parents in the next population. - self.num_offspring = self.sol_per_pop - elif (self.keep_parents > 0): # Keep the specified number of parents in the next population. - self.num_offspring = self.sol_per_pop - self.keep_parents - - # Check if the fitness_func is a function. - if callable(fitness_func): - # Check if the fitness function accepts 2 paramaters. - if (fitness_func.__code__.co_argcount == 2): - self.fitness_func = fitness_func - else: - self.valid_parameters = False - raise ValueError("The fitness function must accept 2 parameters:\n1) A solution to calculate its fitness value.\n2) The solution's index within the population.\n\nThe passed fitness function named '{funcname}' accepts {argcount} parameter(s).".format(funcname=fitness_func.__code__.co_name, argcount=fitness_func.__code__.co_argcount)) - else: - self.valid_parameters = False - raise ValueError("The value assigned to the fitness_func parameter is expected to be of type function but ({fitness_func_type}) found.".format(fitness_func_type=type(fitness_func))) - - # Check if the on_start exists. - if not (on_start is None): - # Check if the on_start is a function. - if callable(on_start): - # Check if the on_start function accepts only a single paramater. - if (on_start.__code__.co_argcount == 1): - self.on_start = on_start - else: - self.valid_parameters = False - raise ValueError("The function assigned to the on_start parameter must accept only 1 parameter representing the instance of the genetic algorithm.\nThe passed function named '{funcname}' accepts {argcount} parameter(s).".format(funcname=on_start.__code__.co_name, argcount=on_start.__code__.co_argcount)) - else: - self.valid_parameters = False - raise ValueError("The value assigned to the on_start parameter is expected to be of type function but ({on_start_type}) found.".format(on_start_type=type(on_start))) - else: - self.on_start = None - - # Check if the on_fitness exists. - if not (on_fitness is None): - # Check if the on_fitness is a function. - if callable(on_fitness): - # Check if the on_fitness function accepts 2 paramaters. - if (on_fitness.__code__.co_argcount == 2): - self.on_fitness = on_fitness - else: - self.valid_parameters = False - raise ValueError("The function assigned to the on_fitness parameter must accept 2 parameters representing the instance of the genetic algorithm and the fitness values of all solutions.\nThe passed function named '{funcname}' accepts {argcount} parameter(s).".format(funcname=on_fitness.__code__.co_name, argcount=on_fitness.__code__.co_argcount)) - else: - self.valid_parameters = False - raise ValueError("The value assigned to the on_fitness parameter is expected to be of type function but ({on_fitness_type}) found.".format(on_fitness_type=type(on_fitness))) - else: - self.on_fitness = None - - # Check if the on_parents exists. - if not (on_parents is None): - # Check if the on_parents is a function. - if callable(on_parents): - # Check if the on_parents function accepts 2 paramaters. - if (on_parents.__code__.co_argcount == 2): - self.on_parents = on_parents - else: - self.valid_parameters = False - raise ValueError("The function assigned to the on_parents parameter must accept 2 parameters representing the instance of the genetic algorithm and the fitness values of all solutions.\nThe passed function named '{funcname}' accepts {argcount} parameter(s).".format(funcname=on_parents.__code__.co_name, argcount=on_parents.__code__.co_argcount)) - else: - self.valid_parameters = False - raise ValueError("The value assigned to the on_parents parameter is expected to be of type function but ({on_parents_type}) found.".format(on_parents_type=type(on_parents))) - else: - self.on_parents = None - - # Check if the on_crossover exists. - if not (on_crossover is None): - # Check if the on_crossover is a function. - if callable(on_crossover): - # Check if the on_crossover function accepts 2 paramaters. - if (on_crossover.__code__.co_argcount == 2): - self.on_crossover = on_crossover - else: - self.valid_parameters = False - raise ValueError("The function assigned to the on_crossover parameter must accept 2 parameters representing the instance of the genetic algorithm and the offspring generated using crossover.\nThe passed function named '{funcname}' accepts {argcount} parameter(s).".format(funcname=on_crossover.__code__.co_name, argcount=on_crossover.__code__.co_argcount)) - else: - self.valid_parameters = False - raise ValueError("The value assigned to the on_crossover parameter is expected to be of type function but ({on_crossover_type}) found.".format(on_crossover_type=type(on_crossover))) - else: - self.on_crossover = None - - # Check if the on_mutation exists. - if not (on_mutation is None): - # Check if the on_mutation is a function. - if callable(on_mutation): - # Check if the on_mutation function accepts 2 paramaters. - if (on_mutation.__code__.co_argcount == 2): - self.on_mutation = on_mutation - else: - self.valid_parameters = False - raise ValueError("The function assigned to the on_mutation parameter must accept 2 parameters representing the instance of the genetic algorithm and the offspring after applying the mutation operation.\nThe passed function named '{funcname}' accepts {argcount} parameter(s).".format(funcname=on_mutation.__code__.co_name, argcount=on_mutation.__code__.co_argcount)) - else: - self.valid_parameters = False - raise ValueError("The value assigned to the on_mutation parameter is expected to be of type function but ({on_mutation_type}) found.".format(on_mutation_type=type(on_mutation))) - else: - self.on_mutation = None - - # Check if the callback_generation exists. - if not (callback_generation is None): - # Check if the callback_generation is a function. - if callable(callback_generation): - # Check if the callback_generation function accepts only a single paramater. - if (callback_generation.__code__.co_argcount == 1): - self.callback_generation = callback_generation - on_generation = callback_generation - if not self.suppress_warnings: warnings.warn("Starting from PyGAD 2.6.0, the callback_generation parameter is deprecated and will be removed in a later release of PyGAD. Please use the on_generation parameter instead.") - else: - self.valid_parameters = False - raise ValueError("The function assigned to the callback_generation parameter must accept only 1 parameter representing the instance of the genetic algorithm.\nThe passed function named '{funcname}' accepts {argcount} parameter(s).".format(funcname=callback_generation.__code__.co_name, argcount=callback_generation.__code__.co_argcount)) - else: - self.valid_parameters = False - raise ValueError("The value assigned to the callback_generation parameter is expected to be of type function but ({callback_generation_type}) found.".format(callback_generation_type=type(callback_generation))) - else: - self.callback_generation = None - - # Check if the on_generation exists. - if not (on_generation is None): - # Check if the on_generation is a function. - if callable(on_generation): - # Check if the on_generation function accepts only a single paramater. - if (on_generation.__code__.co_argcount == 1): - self.on_generation = on_generation - else: - self.valid_parameters = False - raise ValueError("The function assigned to the on_generation parameter must accept only 1 parameter representing the instance of the genetic algorithm.\nThe passed function named '{funcname}' accepts {argcount} parameter(s).".format(funcname=on_generation.__code__.co_name, argcount=on_generation.__code__.co_argcount)) - else: - self.valid_parameters = False - raise ValueError("The value assigned to the on_generation parameter is expected to be of type function but ({on_generation_type}) found.".format(on_generation_type=type(on_generation))) - else: - self.on_generation = None - - # Check if the on_stop exists. - if not (on_stop is None): - # Check if the on_stop is a function. - if callable(on_stop): - # Check if the on_stop function accepts 2 paramaters. - if (on_stop.__code__.co_argcount == 2): - self.on_stop = on_stop - else: - self.valid_parameters = False - raise ValueError("The function assigned to the on_stop parameter must accept 2 parameters representing the instance of the genetic algorithm and a list of the fitness values of the solutions in the last population.\nThe passed function named '{funcname}' accepts {argcount} parameter(s).".format(funcname=on_stop.__code__.co_name, argcount=on_stop.__code__.co_argcount)) - else: - self.valid_parameters = False - raise ValueError("The value assigned to the 'on_stop' parameter is expected to be of type function but ({on_stop_type}) found.".format(on_stop_type=type(on_stop))) - else: - self.on_stop = None - - # Validate delay_after_gen - if type(delay_after_gen) in GA.supported_int_float_types: - if delay_after_gen >= 0.0: - self.delay_after_gen = delay_after_gen - else: - self.valid_parameters = False - raise ValueError("The value passed to the 'delay_after_gen' parameter must be a non-negative number. The value passed is {delay_after_gen} of type {delay_after_gen_type}.".format(delay_after_gen=delay_after_gen, delay_after_gen_type=type(delay_after_gen))) - else: - self.valid_parameters = False - raise ValueError("The value passed to the 'delay_after_gen' parameter must be of type int or float but ({delay_after_gen_type}) found.".format(delay_after_gen_type=type(delay_after_gen))) - - # 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("The value passed to the 'save_best_solutions' parameter must be of type bool but ({save_best_solutions_type}) found.".format(save_best_solutions_type=type(save_best_solutions))) - - # 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 ValueError("The value passed to the 'save_solutions' parameter must be of type bool but ({save_solutions_type}) found.".format(save_solutions_type=type(save_solutions))) - - # Validate allow_duplicate_genes - if not (type(allow_duplicate_genes) is bool): - self.valid_parameters = False - raise TypeError("The expected type of the 'allow_duplicate_genes' parameter is bool but ({allow_duplicate_genes_type}) found.".format(allow_duplicate_genes_type=type(allow_duplicate_genes))) - - self.allow_duplicate_genes = allow_duplicate_genes - - self.stop_criteria = [] - self.supported_stop_words = ["reach", "saturate"] - if stop_criteria is None: - # None: Stop after passing through all generations. - self.stop_criteria = None - elif type(stop_criteria) is str: - # reach_{target_fitness}: Stop if the target fitness value is reached. - # saturate_{num_generations}: Stop if the fitness value does not change (saturates) for the given number of generations. - criterion = stop_criteria.split("_") - if len(criterion) == 2: - stop_word = criterion[0] - number = criterion[1] - - if stop_word in self.supported_stop_words: - pass - else: - self.valid_parameters = False - raise TypeError("In the 'stop_criteria' parameter, the supported stop words are '{supported_stop_words}' but '{stop_word}' found.".format(supported_stop_words=self.supported_stop_words, stop_word=stop_word)) - - if number.replace(".", "").isnumeric(): - number = float(number) - else: - self.valid_parameters = False - raise TypeError("The value following the stop word in the 'stop_criteria' parameter must be a number but the value '{stop_val}' of type {stop_val_type} found.".format(stop_val=number, stop_val_type=type(number))) - - self.stop_criteria.append([stop_word, number]) - - else: - self.valid_parameters = False - raise TypeError("For format of a single criterion in the 'stop_criteria' parameter is 'word_number' but '{stop_criteria}' found.".format(stop_criteria=stop_criteria)) - - elif type(stop_criteria) in [list, tuple, numpy.ndarray]: - # Remove duplicate criterira by converting the list to a set then back to a list. - stop_criteria = list(set(stop_criteria)) - for idx, val in enumerate(stop_criteria): - if type(val) is str: - criterion = val.split("_") - if len(criterion) == 2: - stop_word = criterion[0] - number = criterion[1] - - if stop_word in self.supported_stop_words: - pass - else: - self.valid_parameters = False - raise TypeError("In the 'stop_criteria' parameter, the supported stop words are {supported_stop_words} but '{stop_word}' found.".format(supported_stop_words=self.supported_stop_words, stop_word=stop_word)) - - if number.replace(".", "").isnumeric(): - number = float(number) - else: - self.valid_parameters = False - raise TypeError("The value following the stop word in the 'stop_criteria' parameter must be a number but the value '{stop_val}' of type {stop_val_type} found.".format(stop_val=number, stop_val_type=type(number))) - - self.stop_criteria.append([stop_word, number]) - - else: - self.valid_parameters = False - raise TypeError("For format of a single criterion in the 'stop_criteria' parameter is 'word_number' but {stop_criteria} found.".format(stop_criteria=criterion)) - else: - self.valid_parameters = False - raise TypeError("When the 'stop_criteria' parameter is assigned a tuple/list/numpy.ndarray, then its elements must be strings but the value '{stop_criteria_val}' of type {stop_criteria_val_type} found at index {stop_criteria_val_idx}.".format(stop_criteria_val=val, stop_criteria_val_type=type(val), stop_criteria_val_idx=idx)) - else: - self.valid_parameters = False - raise TypeError("The expected value of the 'stop_criteria' is a single string or a list/tuple/numpy.ndarray of strings but the value {stop_criteria_val} of type {stop_criteria_type} found.".format(stop_criteria_val=stop_criteria, stop_criteria_type=type(stop_criteria))) - - # The number of completed generations. - self.generations_completed = 0 - - # At this point, all necessary parameters validation is done successfully and we are sure that the parameters are valid. - self.valid_parameters = True # Set to True when all the parameters passed in the GA class constructor are valid. - - # Parameters of the genetic algorithm. - self.num_generations = abs(num_generations) - self.parent_selection_type = parent_selection_type - - # Parameters of the mutation operation. - self.mutation_percent_genes = mutation_percent_genes - self.mutation_num_genes = mutation_num_genes - - # Even such this parameter is declared in the class header, it is assigned to the object here to access it after saving the object. - self.best_solutions_fitness = [] # A list holding the fitness value of the best solution for each generation. - - self.best_solution_generation = -1 # 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. - - self.save_best_solutions = save_best_solutions - self.best_solutions = [] # Holds the best solution in each generation. - - self.save_solutions = save_solutions - self.solutions = [] # Holds the solutions in each generation. - self.solutions_fitness = [] # Holds the fitness of the solutions in each generation. - - self.last_generation_fitness = None # A list holding the fitness values of all solutions in the last generation. - self.last_generation_parents = None # A list holding the parents of the last generation. - self.last_generation_offspring_crossover = None # A list holding the offspring after applying crossover in the last generation. - self.last_generation_offspring_mutation = None # A list holding the offspring after applying mutation in the last generation. - self.previous_generation_fitness = None # Holds the fitness values of one generation before the fitness values saved in the last_generation_fitness attribute. Added in PyGAD 2.26.2 - - 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 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]), - self.gene_type[gene_idx][1]) - return solutions - - def initialize_population(self, low, high, allow_duplicate_genes, mutation_by_replacement, gene_type): - - """ - Creates an initial population randomly as a NumPy array. The array is saved in the instance attribute named 'population'. - - 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, holds the initial population and later updated after each generation. - 3. init_population: Keeping the initial population. - """ - - # Population size = (number of chromosomes, number of genes per chromosome) - self.pop_size = (self.sol_per_pop,self.num_genes) # The population will have sol_per_pop chromosome where each chromosome has num_genes genes. - - if self.gene_space is None: - # Creating the initial population randomly. - if self.gene_type_single == True: - self.population = numpy.asarray(numpy.random.uniform(low=low, - high=high, - size=self.pop_size), - dtype=self.gene_type[0]) # A NumPy array holding the initial population. - else: - # Create an empty population of dtype=object to support storing mixed data types within the same array. - self.population = numpy.zeros(shape=self.pop_size, dtype=object) - # Loop through the genes, randomly generate the values of a single gene across the entire population, and add the values of each gene to the population. - for gene_idx in range(self.num_genes): - # A vector of all values of this single gene across all solutions in the population. - gene_values = numpy.asarray(numpy.random.uniform(low=low, - high=high, - size=self.pop_size[0]), - dtype=self.gene_type[gene_idx][0]) - # Adding the current gene values to the population. - self.population[:, gene_idx] = gene_values - - if allow_duplicate_genes == False: - for solution_idx in range(self.population.shape[0]): - # print("Before", self.population[solution_idx]) - self.population[solution_idx], _, _ = self.solve_duplicate_genes_randomly(solution=self.population[solution_idx], - min_val=low, - max_val=high, - mutation_by_replacement=True, - gene_type=gene_type, - num_trials=10) - # print("After", self.population[solution_idx]) - - elif self.gene_space_nested: - if self.gene_type_single == True: - self.population = numpy.zeros(shape=self.pop_size, dtype=self.gene_type[0]) - for sol_idx in range(self.sol_per_pop): - for gene_idx in range(self.num_genes): - if type(self.gene_space[gene_idx]) in [list, tuple, range]: - # Check if the gene space has None values. If any, then replace it with randomly generated values according to the 3 attributes init_range_low, init_range_high, and gene_type. - if type(self.gene_space[gene_idx]) is range: - temp = self.gene_space[gene_idx] - else: - temp = self.gene_space[gene_idx].copy() - for idx, val in enumerate(self.gene_space[gene_idx]): - if val is None: - self.gene_space[gene_idx][idx] = numpy.asarray(numpy.random.uniform(low=low, - high=high, - size=1), - dtype=self.gene_type[0])[0] - self.population[sol_idx, gene_idx] = random.choice(self.gene_space[gene_idx]) - self.population[sol_idx, gene_idx] = self.gene_type[0](self.population[sol_idx, gene_idx]) - self.gene_space[gene_idx] = temp - elif type(self.gene_space[gene_idx]) is dict: - if 'step' in self.gene_space[gene_idx].keys(): - self.population[sol_idx, gene_idx] = numpy.asarray(numpy.random.choice(numpy.arange(start=self.gene_space[gene_idx]['low'], - stop=self.gene_space[gene_idx]['high'], - step=self.gene_space[gene_idx]['step']), - size=1), - dtype=self.gene_type[0])[0] - else: - self.population[sol_idx, gene_idx] = numpy.asarray(numpy.random.uniform(low=self.gene_space[gene_idx]['low'], - high=self.gene_space[gene_idx]['high'], - size=1), - dtype=self.gene_type[0])[0] - elif type(self.gene_space[gene_idx]) == type(None): - - # The following commented code replace the None value with a single number that will not change again. - # This means the gene value will be the same across all solutions. - # self.gene_space[gene_idx] = numpy.asarray(numpy.random.uniform(low=low, - # high=high, - # size=1), dtype=self.gene_type[0])[0] - # self.population[sol_idx, gene_idx] = self.gene_space[gene_idx].copy() - - # The above problem is solved by keeping the None value in the gene_space parameter. This forces PyGAD to generate this value for each solution. - self.population[sol_idx, gene_idx] = numpy.asarray(numpy.random.uniform(low=low, - high=high, - size=1), - dtype=self.gene_type[0])[0] - elif type(self.gene_space[gene_idx]) in GA.supported_int_float_types: - self.population[sol_idx, gene_idx] = self.gene_space[gene_idx].copy() - else: - self.population = numpy.zeros(shape=self.pop_size, dtype=object) - for sol_idx in range(self.sol_per_pop): - for gene_idx in range(self.num_genes): - if type(self.gene_space[gene_idx]) in [list, tuple, range]: - # Check if the gene space has None values. If any, then replace it with randomly generated values according to the 3 attributes init_range_low, init_range_high, and gene_type. - temp = self.gene_space[gene_idx].copy() - for idx, val in enumerate(self.gene_space[gene_idx]): - if val is None: - self.gene_space[gene_idx][idx] = numpy.asarray(numpy.random.uniform(low=low, - high=high, - size=1), - dtype=self.gene_type[gene_idx][0])[0] - self.population[sol_idx, gene_idx] = random.choice(self.gene_space[gene_idx]) - self.population[sol_idx, gene_idx] = self.gene_type[gene_idx][0](self.population[sol_idx, gene_idx]) - self.gene_space[gene_idx] = temp.copy() - elif type(self.gene_space[gene_idx]) is dict: - if 'step' in self.gene_space[gene_idx].keys(): - self.population[sol_idx, gene_idx] = numpy.asarray(numpy.random.choice(numpy.arange(start=self.gene_space[gene_idx]['low'], - stop=self.gene_space[gene_idx]['high'], - step=self.gene_space[gene_idx]['step']), - size=1), - dtype=self.gene_type[gene_idx][0])[0] - else: - self.population[sol_idx, gene_idx] = numpy.asarray(numpy.random.uniform(low=self.gene_space[gene_idx]['low'], - high=self.gene_space[gene_idx]['high'], - size=1), - dtype=self.gene_type[gene_idx][0])[0] - elif type(self.gene_space[gene_idx]) == type(None): - # self.gene_space[gene_idx] = numpy.asarray(numpy.random.uniform(low=low, - # high=high, - # size=1), - # dtype=self.gene_type[gene_idx][0])[0] - - # self.population[sol_idx, gene_idx] = self.gene_space[gene_idx].copy() - - temp = numpy.asarray(numpy.random.uniform(low=low, - high=high, - size=1), - dtype=self.gene_type[gene_idx][0])[0] - self.population[sol_idx, gene_idx] = temp - elif type(self.gene_space[gene_idx]) in GA.supported_int_float_types: - self.population[sol_idx, gene_idx] = self.gene_space[gene_idx] - else: - if self.gene_type_single == True: - # Replace all the None values with random values using the init_range_low, init_range_high, and gene_type attributes. - for idx, curr_gene_space in enumerate(self.gene_space): - if curr_gene_space is None: - self.gene_space[idx] = numpy.asarray(numpy.random.uniform(low=low, - high=high, - size=1), - dtype=self.gene_type[0])[0] - - # Creating the initial population by randomly selecting the genes' values from the values inside the 'gene_space' parameter. - if type(self.gene_space) is dict: - if 'step' in self.gene_space.keys(): - self.population = numpy.asarray(numpy.random.choice(numpy.arange(start=self.gene_space['low'], - stop=self.gene_space['high'], - step=self.gene_space['step']), - size=self.pop_size), - dtype=self.gene_type[0]) - else: - self.population = numpy.asarray(numpy.random.uniform(low=self.gene_space['low'], - high=self.gene_space['high'], - size=self.pop_size), - dtype=self.gene_type[0]) # A NumPy array holding the initial population. - else: - self.population = numpy.asarray(numpy.random.choice(self.gene_space, - size=self.pop_size), - dtype=self.gene_type[0]) # A NumPy array holding the initial population. - else: - # Replace all the None values with random values using the init_range_low, init_range_high, and gene_type attributes. - for gene_idx, curr_gene_space in enumerate(self.gene_space): - if curr_gene_space is None: - self.gene_space[gene_idx] = numpy.asarray(numpy.random.uniform(low=low, - high=high, - size=1), - dtype=self.gene_type[gene_idx][0])[0] - - # Creating the initial population by randomly selecting the genes' values from the values inside the 'gene_space' parameter. - if type(self.gene_space) is dict: - # Create an empty population of dtype=object to support storing mixed data types within the same array. - self.population = numpy.zeros(shape=self.pop_size, dtype=object) - # Loop through the genes, randomly generate the values of a single gene across the entire population, and add the values of each gene to the population. - for gene_idx in range(self.num_genes): - # A vector of all values of this single gene across all solutions in the population. - if 'step' in self.gene_space[gene_idx].keys(): - gene_values = numpy.asarray(numpy.random.choice(numpy.arange(start=self.gene_space[gene_idx]['low'], - stop=self.gene_space[gene_idx]['high'], - step=self.gene_space[gene_idx]['step']), - size=self.pop_size[0]), - dtype=self.gene_type[gene_idx][0]) - else: - gene_values = numpy.asarray(numpy.random.uniform(low=self.gene_space['low'], - high=self.gene_space['high'], - size=self.pop_size[0]), - dtype=self.gene_type[gene_idx][0]) - # Adding the current gene values to the population. - self.population[:, gene_idx] = gene_values - - else: - # Create an empty population of dtype=object to support storing mixed data types within the same array. - self.population = numpy.zeros(shape=self.pop_size, dtype=object) - # Loop through the genes, randomly generate the values of a single gene across the entire population, and add the values of each gene to the population. - for gene_idx in range(self.num_genes): - # A vector of all values of this single gene across all solutions in the population. - gene_values = numpy.asarray(numpy.random.choice(self.gene_space, - size=self.pop_size[0]), - dtype=self.gene_type[gene_idx][0]) - # Adding the current gene values to the population. - self.population[:, gene_idx] = gene_values - - if not (self.gene_space is None): - if allow_duplicate_genes == False: - for sol_idx in range(self.population.shape[0]): - self.population[sol_idx], _, _ = self.solve_duplicate_genes_by_space(solution=self.population[sol_idx], - gene_type=self.gene_type, - num_trials=10, - build_initial_pop=True) - - # Keeping the initial population in the initial_population attribute. - self.initial_population = self.population.copy() - - def cal_pop_fitness(self): - - """ - Calculating the fitness values of all solutions in the current population. - It returns: - -fitness: An array of the calculated fitness values. - """ - - if self.valid_parameters == False: - raise ValueError("ERROR calling the cal_pop_fitness() method: \nPlease check the parameters passed while creating an instance of the GA class.\n") - - pop_fitness = [] - # Calculating the fitness value of each solution in the current population. - for sol_idx, sol in enumerate(self.population): - - # Check if this solution is a parent from the previous generation and its fitness value is already calculated. If so, use the fitness value instead of calling the fitness function. - if (self.last_generation_parents is not None) and len(numpy.where(numpy.all(self.last_generation_parents == sol, axis=1))[0] > 0): - # Index of the parent in the parents array (self.last_generation_parents). This is not its index within the population. - parent_idx = numpy.where(numpy.all(self.last_generation_parents == sol, axis=1))[0][0] - # Index of the parent in the population. - parent_idx = self.last_generation_parents_indices[parent_idx] - # Use the parent's index to return its pre-calculated fitness value. - fitness = self.previous_generation_fitness[parent_idx] - else: - fitness = self.fitness_func(sol, sol_idx) - if type(fitness) in GA.supported_int_float_types: - pass - else: - raise ValueError("The fitness function should return a number but the value {fit_val} of type {fit_type} found.".format(fit_val=fitness, fit_type=type(fitness))) - pop_fitness.append(fitness) - - pop_fitness = numpy.array(pop_fitness) - - return pop_fitness - - def run(self): - - """ - Runs the genetic algorithm. This is the main method in which the genetic algorithm is evolved through a number of generations. - """ - - if self.valid_parameters == False: - raise ValueError("Error calling the run() method: \nThe run() method cannot be executed with invalid parameters. Please check the parameters passed while creating an instance of the GA class.\n") - - # Reset the variables that store the solutions and their fitness after each generation. If not reset, then for each call to the run() method the new solutions and their fitness values will be appended to the old variables and their length double. Some errors arise if not reset. - # If, in the future, new variables are created that get appended after each generation, please consider resetting them here. - self.best_solutions = [] # Holds the best solution in each generation. - self.best_solutions_fitness = [] # A list holding the fitness value of the best solution for each generation. - self.solutions = [] # Holds the solutions in each generation. - self.solutions_fitness = [] # Holds the fitness of the solutions in each generation. - - if not (self.on_start is None): - self.on_start(self) - - stop_run = False - - # Measuring the fitness of each chromosome in the population. Save the fitness in the last_generation_fitness attribute. - self.last_generation_fitness = self.cal_pop_fitness() - - best_solution, best_solution_fitness, best_match_idx = self.best_solution(pop_fitness=self.last_generation_fitness) - - # Appending the best solution in the initial population to the best_solutions list. - if self.save_best_solutions: - self.best_solutions.append(best_solution) - - # Appending the solutions in the initial population to the solutions list. - if self.save_solutions: - self.solutions.extend(self.population.copy()) - - for generation in range(self.num_generations): - if not (self.on_fitness is None): - self.on_fitness(self, self.last_generation_fitness) - - # Appending the fitness value of the best solution in the current generation to the best_solutions_fitness attribute. - self.best_solutions_fitness.append(best_solution_fitness) - - if self.save_solutions: - self.solutions_fitness.extend(self.last_generation_fitness) - - # Selecting the best parents in the population for mating. - if callable(self.parent_selection_type): - self.last_generation_parents, self.last_generation_parents_indices = self.select_parents(self.last_generation_fitness, self.num_parents_mating, self) - else: - self.last_generation_parents, self.last_generation_parents_indices = self.select_parents(self.last_generation_fitness, num_parents=self.num_parents_mating) - if not (self.on_parents is None): - self.on_parents(self, self.last_generation_parents) - - # If self.crossover_type=None, then no crossover is applied and thus no offspring will be created in the next generations. The next generation will use the solutions in the current population. - if self.crossover_type is None: - if self.num_offspring <= self.keep_parents: - self.last_generation_offspring_crossover = self.last_generation_parents[0:self.num_offspring] - else: - self.last_generation_offspring_crossover = numpy.concatenate((self.last_generation_parents, self.population[0:(self.num_offspring - self.last_generation_parents.shape[0])])) - else: - # Generating offspring using crossover. - if callable(self.crossover_type): - self.last_generation_offspring_crossover = self.crossover(self.last_generation_parents, - (self.num_offspring, self.num_genes), - self) - else: - self.last_generation_offspring_crossover = self.crossover(self.last_generation_parents, - offspring_size=(self.num_offspring, self.num_genes)) - if not (self.on_crossover is None): - self.on_crossover(self, self.last_generation_offspring_crossover) - - # If self.mutation_type=None, then no mutation is applied and thus no changes are applied to the offspring created using the crossover operation. The offspring will be used unchanged in the next generation. - if self.mutation_type is None: - self.last_generation_offspring_mutation = self.last_generation_offspring_crossover - else: - # Adding some variations to the offspring using mutation. - if callable(self.mutation_type): - self.last_generation_offspring_mutation = self.mutation(self.last_generation_offspring_crossover, self) - else: - self.last_generation_offspring_mutation = self.mutation(self.last_generation_offspring_crossover) - if not (self.on_mutation is None): - self.on_mutation(self, self.last_generation_offspring_mutation) - - # Update the population attribute according to the offspring generated. - if (self.keep_parents == 0): - self.population = self.last_generation_offspring_mutation - elif (self.keep_parents == -1): - # Creating the new population based on the parents and offspring. - self.population[0:self.last_generation_parents.shape[0], :] = self.last_generation_parents - self.population[self.last_generation_parents.shape[0]:, :] = self.last_generation_offspring_mutation - elif (self.keep_parents > 0): - parents_to_keep, _ = self.steady_state_selection(self.last_generation_fitness, num_parents=self.keep_parents) - self.population[0:parents_to_keep.shape[0], :] = parents_to_keep - self.population[parents_to_keep.shape[0]:, :] = self.last_generation_offspring_mutation - - self.generations_completed = generation + 1 # The generations_completed attribute holds the number of the last completed generation. - - self.previous_generation_fitness = self.last_generation_fitness.copy() - # Measuring the fitness of each chromosome in the population. Save the fitness in the last_generation_fitness attribute. - self.last_generation_fitness = self.cal_pop_fitness() - - best_solution, best_solution_fitness, best_match_idx = self.best_solution(pop_fitness=self.last_generation_fitness) - - # Appending the best solution in the current generation to the best_solutions list. - if self.save_best_solutions: - self.best_solutions.append(best_solution) - - # Appending the solutions in the current generation to the solutions list. - if self.save_solutions: - self.solutions.extend(self.population.copy()) - - # If the callback_generation attribute is not None, then cal 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": - # 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: - for criterion in self.stop_criteria: - if criterion[0] == "reach": - if max(self.last_generation_fitness) >= criterion[1]: - stop_run = True - break - elif criterion[0] == "saturate": - criterion[1] = int(criterion[1]) - if (self.generations_completed >= criterion[1]): - if (self.best_solutions_fitness[self.generations_completed - criterion[1]] - self.best_solutions_fitness[self.generations_completed - 1]) == 0: - stop_run = True - break - - if stop_run: - break - - time.sleep(self.delay_after_gen) - - # Save the fitness of the last generation. - if self.save_solutions: - self.solutions_fitness.extend(self.last_generation_fitness) - - # Save the fitness value of the best solution. - _, best_solution_fitness, _ = self.best_solution(pop_fitness=self.last_generation_fitness) - self.best_solutions_fitness.append(best_solution_fitness) - - self.best_solution_generation = numpy.where(numpy.array(self.best_solutions_fitness) == numpy.max(numpy.array(self.best_solutions_fitness)))[0][0] - # After the run() method completes, the run_completed flag is changed from False to True. - self.run_completed = True # Set to True only after the run() method completes gracefully. - - if not (self.on_stop is None): - self.on_stop(self, self.last_generation_fitness) - - # Converting the 'best_solutions' list into a NumPy array. - self.best_solutions = numpy.array(self.best_solutions) - - # Converting the 'solutions' list into a NumPy array. - self.solutions = numpy.array(self.solutions) - - def steady_state_selection(self, fitness, num_parents): - - """ - Selects the parents using the steady-state selection technique. Later, these parents will mate to produce the offspring. - It accepts 2 parameters: - -fitness: The fitness values of the solutions in the current population. - -num_parents: The number of parents to be selected. - It returns an array of the selected parents. - """ - - fitness_sorted = sorted(range(len(fitness)), key=lambda k: fitness[k]) - fitness_sorted.reverse() - # Selecting the best individuals in the current generation as parents for producing the offspring of the next generation. - if self.gene_type_single == True: - parents = numpy.empty((num_parents, self.population.shape[1]), dtype=self.gene_type[0]) - else: - parents = numpy.empty((num_parents, self.population.shape[1]), dtype=object) - for parent_num in range(num_parents): - parents[parent_num, :] = self.population[fitness_sorted[parent_num], :].copy() - - return parents, fitness_sorted[:num_parents] - - def rank_selection(self, fitness, num_parents): - - """ - Selects the parents using the rank selection technique. Later, these parents will mate to produce the offspring. - It accepts 2 parameters: - -fitness: The fitness values of the solutions in the current population. - -num_parents: The number of parents to be selected. - It returns an array of the selected parents. - """ - - fitness_sorted = sorted(range(len(fitness)), key=lambda k: fitness[k]) - fitness_sorted.reverse() - # Selecting the best individuals in the current generation as parents for producing the offspring of the next generation. - if self.gene_type_single == True: - parents = numpy.empty((num_parents, self.population.shape[1]), dtype=self.gene_type[0]) - else: - parents = numpy.empty((num_parents, self.population.shape[1]), dtype=object) - for parent_num in range(num_parents): - parents[parent_num, :] = self.population[fitness_sorted[parent_num], :].copy() - - return parents, fitness_sorted[:num_parents] - - def random_selection(self, fitness, num_parents): - - """ - Selects the parents randomly. Later, these parents will mate to produce the offspring. - It accepts 2 parameters: - -fitness: The fitness values of the solutions in the current population. - -num_parents: The number of parents to be selected. - It returns an array of the selected parents. - """ - - if self.gene_type_single == True: - parents = numpy.empty((num_parents, self.population.shape[1]), dtype=self.gene_type[0]) - else: - parents = numpy.empty((num_parents, self.population.shape[1]), dtype=object) - - rand_indices = numpy.random.randint(low=0.0, high=fitness.shape[0], size=num_parents) - - for parent_num in range(num_parents): - parents[parent_num, :] = self.population[rand_indices[parent_num], :].copy() - - return parents, rand_indices - - def tournament_selection(self, fitness, num_parents): - - """ - Selects the parents using the tournament selection technique. Later, these parents will mate to produce the offspring. - It accepts 2 parameters: - -fitness: The fitness values of the solutions in the current population. - -num_parents: The number of parents to be selected. - It returns an array of the selected parents. - """ - - if self.gene_type_single == True: - parents = numpy.empty((num_parents, self.population.shape[1]), dtype=self.gene_type[0]) - else: - parents = numpy.empty((num_parents, self.population.shape[1]), dtype=object) - - parents_indices = [] - - for parent_num in range(num_parents): - rand_indices = numpy.random.randint(low=0.0, high=len(fitness), size=self.K_tournament) - K_fitnesses = fitness[rand_indices] - selected_parent_idx = numpy.where(K_fitnesses == numpy.max(K_fitnesses))[0][0] - parents_indices.append(rand_indices[selected_parent_idx]) - parents[parent_num, :] = self.population[rand_indices[selected_parent_idx], :].copy() - - return parents, parents_indices - - def roulette_wheel_selection(self, fitness, num_parents): - - """ - Selects the parents using the roulette wheel selection technique. Later, these parents will mate to produce the offspring. - It accepts 2 parameters: - -fitness: The fitness values of the solutions in the current population. - -num_parents: The number of parents to be selected. - It returns an array of the selected parents. - """ - - fitness_sum = numpy.sum(fitness) - probs = fitness / fitness_sum - probs_start = numpy.zeros(probs.shape, dtype=numpy.float) # An array holding the start values of the ranges of probabilities. - probs_end = numpy.zeros(probs.shape, dtype=numpy.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] = 99999999999 - - # Selecting the best individuals in the current generation as parents for producing the offspring of the next generation. - if self.gene_type_single == True: - parents = numpy.empty((num_parents, self.population.shape[1]), dtype=self.gene_type[0]) - else: - parents = numpy.empty((num_parents, self.population.shape[1]), dtype=object) - - parents_indices = [] - - for parent_num in range(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]): - parents[parent_num, :] = self.population[idx, :].copy() - parents_indices.append(idx) - break - return parents, parents_indices - - def stochastic_universal_selection(self, fitness, num_parents): - - """ - Selects the parents using the stochastic universal selection technique. Later, these parents will mate to produce the offspring. - It accepts 2 parameters: - -fitness: The fitness values of the solutions in the current population. - -num_parents: The number of parents to be selected. - It returns an array of the selected parents. - """ - - fitness_sum = numpy.sum(fitness) - probs = fitness / fitness_sum - probs_start = numpy.zeros(probs.shape, dtype=numpy.float) # An array holding the start values of the ranges of probabilities. - probs_end = numpy.zeros(probs.shape, dtype=numpy.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] = 99999999999 - - pointers_distance = 1.0 / self.num_parents_mating # Distance between different pointers. - first_pointer = numpy.random.uniform(low=0.0, high=pointers_distance, size=1) # Location of the first pointer. - - # Selecting the best individuals in the current generation as parents for producing the offspring of the next generation. - if self.gene_type_single == True: - parents = numpy.empty((num_parents, self.population.shape[1]), dtype=self.gene_type[0]) - else: - parents = numpy.empty((num_parents, self.population.shape[1]), dtype=object) - - parents_indices = [] - - for parent_num in range(num_parents): - rand_pointer = first_pointer + parent_num*pointers_distance - for idx in range(probs.shape[0]): - if (rand_pointer >= probs_start[idx] and rand_pointer < probs_end[idx]): - parents[parent_num, :] = self.population[idx, :].copy() - parents_indices.append(idx) - break - return parents, parents_indices - - def single_point_crossover(self, parents, offspring_size): - - """ - Applies the single-point crossover. It selects a point randomly at which crossover takes place between the pairs of parents. - 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. - """ - - if self.gene_type_single == True: - offspring = numpy.empty(offspring_size, dtype=self.gene_type[0]) - else: - offspring = numpy.empty(offspring_size, dtype=object) - - for k in range(offspring_size[0]): - # The point at which crossover takes place between two parents. Usually, it is at the center. - crossover_point = numpy.random.randint(low=0, high=parents.shape[1], size=1)[0] - - if not (self.crossover_probability is None): - probs = numpy.random.random(size=parents.shape[0]) - indices = numpy.where(probs <= self.crossover_probability)[0] - - # If no parent satisfied the probability, no crossover is applied and a parent is selected. - if len(indices) == 0: - offspring[k, :] = parents[k % parents.shape[0], :] - continue - elif len(indices) == 1: - parent1_idx = indices[0] - parent2_idx = parent1_idx - else: - indices = random.sample(set(indices), 2) - parent1_idx = indices[0] - parent2_idx = indices[1] - else: - # Index of the first parent to mate. - parent1_idx = k % parents.shape[0] - # 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. - offspring[k, 0:crossover_point] = parents[parent1_idx, 0:crossover_point] - # The new offspring has its second half of its genes from the second parent. - offspring[k, crossover_point:] = parents[parent2_idx, crossover_point:] - return offspring - - def two_points_crossover(self, parents, offspring_size): - - """ - Applies the 2 points crossover. It selects the 2 points randomly at which crossover takes place between the pairs of parents. - 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. - """ - - if self.gene_type_single == True: - offspring = numpy.empty(offspring_size, dtype=self.gene_type[0]) - else: - offspring = numpy.empty(offspring_size, dtype=object) - - for k in range(offspring_size[0]): - if (parents.shape[1] == 1): # If the chromosome has only a single gene. In this case, this gene is copied from the second parent. - crossover_point1 = 0 - else: - crossover_point1 = numpy.random.randint(low=0, high=numpy.ceil(parents.shape[1]/2 + 1), size=1)[0] - - crossover_point2 = crossover_point1 + int(parents.shape[1]/2) # The second point must always be greater than the first point. - - if not (self.crossover_probability is None): - probs = numpy.random.random(size=parents.shape[0]) - indices = numpy.where(probs <= self.crossover_probability)[0] - - # If no parent satisfied the probability, no crossover is applied and a parent is selected. - if len(indices) == 0: - offspring[k, :] = parents[k % parents.shape[0], :] - continue - elif len(indices) == 1: - parent1_idx = indices[0] - parent2_idx = parent1_idx - else: - indices = random.sample(set(indices), 2) - parent1_idx = indices[0] - parent2_idx = indices[1] - else: - # Index of the first parent to mate. - parent1_idx = k % parents.shape[0] - # Index of the second parent to mate. - parent2_idx = (k+1) % parents.shape[0] - - # The genes from the beginning of the chromosome up to the first point are copied from the first parent. - offspring[k, 0:crossover_point1] = parents[parent1_idx, 0:crossover_point1] - # The genes from the second point up to the end of the chromosome are copied from the first parent. - offspring[k, crossover_point2:] = parents[parent1_idx, crossover_point2:] - # The genes between the 2 points are copied from the second parent. - offspring[k, crossover_point1:crossover_point2] = parents[parent2_idx, crossover_point1:crossover_point2] - return offspring - - def uniform_crossover(self, parents, offspring_size): - - """ - 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. - 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. - """ - - if self.gene_type_single == True: - offspring = numpy.empty(offspring_size, dtype=self.gene_type[0]) - else: - offspring = numpy.empty(offspring_size, dtype=object) - - for k in range(offspring_size[0]): - if not (self.crossover_probability is None): - probs = numpy.random.random(size=parents.shape[0]) - indices = numpy.where(probs <= self.crossover_probability)[0] - - # If no parent satisfied the probability, no crossover is applied and a parent is selected. - if len(indices) == 0: - offspring[k, :] = parents[k % parents.shape[0], :] - continue - elif len(indices) == 1: - parent1_idx = indices[0] - parent2_idx = parent1_idx - else: - indices = random.sample(set(indices), 2) - parent1_idx = indices[0] - parent2_idx = indices[1] - else: - # Index of the first parent to mate. - parent1_idx = k % parents.shape[0] - # Index of the second parent to mate. - parent2_idx = (k+1) % parents.shape[0] - - genes_source = numpy.random.randint(low=0, high=2, size=offspring_size[1]) - for gene_idx in range(offspring_size[1]): - if (genes_source[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_source[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] - return offspring - - def scattered_crossover(self, parents, offspring_size): - - """ - Applies the scattered crossover. It randomly selects the gene from one of the 2 parents. - 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. - """ - - if self.gene_type_single == True: - offspring = numpy.empty(offspring_size, dtype=self.gene_type[0]) - else: - offspring = numpy.empty(offspring_size, dtype=object) - - for k in range(offspring_size[0]): - if not (self.crossover_probability is None): - probs = numpy.random.random(size=parents.shape[0]) - indices = numpy.where(probs <= self.crossover_probability)[0] - - # If no parent satisfied the probability, no crossover is applied and a parent is selected. - if len(indices) == 0: - offspring[k, :] = parents[k % parents.shape[0], :] - continue - elif len(indices) == 1: - parent1_idx = indices[0] - parent2_idx = parent1_idx - else: - indices = random.sample(set(indices), 2) - parent1_idx = indices[0] - parent2_idx = indices[1] - else: - # Index of the first parent to mate. - parent1_idx = k % parents.shape[0] - # Index of the second parent to mate. - parent2_idx = (k+1) % parents.shape[0] - - # A 0/1 vector where 0 means the gene is taken from the first parent and 1 means the gene is taken from the second parent. - gene_sources = numpy.random.randint(0, 2, size=self.num_genes) - offspring[k, :] = numpy.where(gene_sources == 0, parents[parent1_idx, :], parents[parent2_idx, :]) - - return offspring - - def random_mutation(self, offspring): - - """ - Applies the random mutation which changes the values of a number of genes randomly. - The random value is selected either using the 'gene_space' parameter or the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val'. - It accepts a single parameter: - -offspring: The offspring to mutate. - It returns an array of the mutated offspring. - """ - - # If the mutation values are selected from the mutation space, the attribute 'gene_space' is not None. Otherwise, it is None. - # When the 'mutation_probability' parameter exists (i.e. not None), then it is used in the mutation. Otherwise, the 'mutation_num_genes' parameter is used. - - if self.mutation_probability is None: - # When the 'mutation_probability' parameter does not exist (i.e. None), then the parameter 'mutation_num_genes' is used in the mutation. - if not (self.gene_space is None): - # When the attribute 'gene_space' exists (i.e. not None), the mutation values are selected randomly from the space of values of each gene. - offspring = self.mutation_by_space(offspring) - else: - offspring = self.mutation_randomly(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'. - offspring = self.mutation_probs_by_space(offspring) - else: - offspring = self.mutation_probs_randomly(offspring) - - return offspring - - def mutation_by_space(self, offspring): - - """ - Applies the random mutation using the mutation values' space. - It accepts a single parameter: - -offspring: The offspring to mutate. - It returns an array of the mutated offspring using the mutation space. - """ - - # For each offspring, a value from the gene space is selected randomly and assigned to the selected mutated gene. - for offspring_idx in range(offspring.shape[0]): - mutation_indices = numpy.array(random.sample(range(0, self.num_genes), self.mutation_num_genes)) - for gene_idx in mutation_indices: - - if self.gene_space_nested: - # Returning the current gene space from the 'gene_space' attribute. - if type(self.gene_space[gene_idx]) in [numpy.ndarray, list]: - curr_gene_space = self.gene_space[gene_idx].copy() - else: - curr_gene_space = self.gene_space[gene_idx] - - # If the gene space has only a single value, use it as the new gene value. - if type(curr_gene_space) in GA.supported_int_float_types: - value_from_space = curr_gene_space - # If the gene space is None, apply mutation by adding a random value between the range defined by the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val'. - elif curr_gene_space is None: - rand_val = numpy.random.uniform(low=self.random_mutation_min_val, - high=self.random_mutation_max_val, - size=1) - if self.mutation_by_replacement: - value_from_space = rand_val - else: - value_from_space = offspring[offspring_idx, gene_idx] + rand_val - elif type(curr_gene_space) is dict: - # The gene's space of type dict specifies the lower and upper limits of a gene. - if 'step' in curr_gene_space.keys(): - value_from_space = numpy.random.choice(numpy.arange(start=curr_gene_space['low'], - stop=curr_gene_space['high'], - step=curr_gene_space['step']), - size=1) - else: - value_from_space = numpy.random.uniform(low=curr_gene_space['low'], - high=curr_gene_space['high'], - size=1) - else: - # Selecting a value randomly based on the current gene's space in the 'gene_space' attribute. - # If the gene space has only 1 value, then select it. The old and new values of the gene are identical. - if len(curr_gene_space) == 1: - value_from_space = curr_gene_space[0] - # If the gene space has more than 1 value, then select a new one that is different from the current value. - else: - values_to_select_from = list(set(curr_gene_space) - set([offspring[offspring_idx, gene_idx]])) - if len(values_to_select_from) == 0: - value_from_space = offspring[offspring_idx, gene_idx] - else: - value_from_space = random.choice(values_to_select_from) - else: - # Selecting a value randomly from the global gene space in the 'gene_space' attribute. - if type(self.gene_space) is dict: - # When the gene_space is assigned a dict object, then it specifies the lower and upper limits of all genes in the space. - if 'step' in self.gene_space.keys(): - value_from_space = numpy.random.choice(numpy.arange(start=self.gene_space['low'], - stop=self.gene_space['high'], - step=self.gene_space['step']), - size=1) - else: - value_from_space = numpy.random.uniform(low=self.gene_space['low'], - high=self.gene_space['high'], - size=1) - else: - # If the space type is not of type dict, then a value is randomly selected from the gene_space attribute. - values_to_select_from = list(set(self.gene_space) - set([offspring[offspring_idx, gene_idx]])) - if len(values_to_select_from) == 0: - value_from_space = offspring[offspring_idx, gene_idx] - else: - value_from_space = random.choice(values_to_select_from) - # value_from_space = random.choice(self.gene_space) - - if value_from_space is None: - value_from_space = numpy.random.uniform(low=self.random_mutation_min_val, - high=self.random_mutation_max_val, - size=1) - - # Assinging the selected value from the space to the gene. - if self.gene_type_single == True: - if not self.gene_type[1] is None: - offspring[offspring_idx, gene_idx] = numpy.round(self.gene_type[0](value_from_space), - self.gene_type[1]) - else: - offspring[offspring_idx, gene_idx] = self.gene_type[0](value_from_space) - else: - if not self.gene_type[gene_idx][1] is None: - offspring[offspring_idx, gene_idx] = numpy.round(self.gene_type[gene_idx][0](value_from_space), - self.gene_type[gene_idx][1]) - else: - offspring[offspring_idx, gene_idx] = self.gene_type[gene_idx][0](value_from_space) - - if self.allow_duplicate_genes == False: - offspring[offspring_idx], _, _ = self.solve_duplicate_genes_by_space(solution=offspring[offspring_idx], - gene_type=self.gene_type, - num_trials=10) - return 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 mutation probability, then it will be mutated based on the mutation space. - It accepts a single parameter: - -offspring: The offspring to mutate. - It returns an array of the mutated offspring using the mutation space. - """ - - # For each offspring, a value from the gene space is selected randomly and assigned to the selected mutated gene. - for offspring_idx in range(offspring.shape[0]): - probs = numpy.random.random(size=offspring.shape[1]) - for gene_idx in range(offspring.shape[1]): - if probs[gene_idx] <= self.mutation_probability: - if self.gene_space_nested: - # Returning the current gene space from the 'gene_space' attribute. - if type(self.gene_space[gene_idx]) in [numpy.ndarray, list]: - curr_gene_space = self.gene_space[gene_idx].copy() - else: - curr_gene_space = self.gene_space[gene_idx] - - # If the gene space has only a single value, use it as the new gene value. - if type(curr_gene_space) in GA.supported_int_float_types: - value_from_space = curr_gene_space - # If the gene space is None, apply mutation by adding a random value between the range defined by the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val'. - elif curr_gene_space is None: - rand_val = numpy.random.uniform(low=self.random_mutation_min_val, - high=self.random_mutation_max_val, - size=1) - if self.mutation_by_replacement: - value_from_space = rand_val - else: - value_from_space = offspring[offspring_idx, gene_idx] + rand_val - elif type(curr_gene_space) is dict: - # Selecting a value randomly from the current gene's space in the 'gene_space' attribute. - if 'step' in curr_gene_space.keys(): - value_from_space = numpy.random.choice(numpy.arange(start=curr_gene_space['low'], - stop=curr_gene_space['high'], - step=curr_gene_space['step']), - size=1) - else: - value_from_space = numpy.random.uniform(low=curr_gene_space['low'], - high=curr_gene_space['high'], - size=1) - else: - # Selecting a value randomly from the current gene's space in the 'gene_space' attribute. - # If the gene space has only 1 value, then select it. The old and new values of the gene are identical. - if len(curr_gene_space) == 1: - value_from_space = curr_gene_space[0] - # If the gene space has more than 1 value, then select a new one that is different from the current value. - else: - values_to_select_from = list(set(curr_gene_space) - set([offspring[offspring_idx, gene_idx]])) - if len(values_to_select_from) == 0: - value_from_space = offspring[offspring_idx, gene_idx] - else: - value_from_space = random.choice(values_to_select_from) - else: - # Selecting a value randomly from the global gene space in the 'gene_space' attribute. - if type(self.gene_space) is dict: - if 'step' in self.gene_space.keys(): - value_from_space = numpy.random.choice(numpy.arange(start=self.gene_space['low'], - stop=self.gene_space['high'], - step=self.gene_space['step']), - size=1) - else: - value_from_space = numpy.random.uniform(low=self.gene_space['low'], - high=self.gene_space['high'], - size=1) - else: - values_to_select_from = list(set(self.gene_space) - set([offspring[offspring_idx, gene_idx]])) - if len(values_to_select_from) == 0: - value_from_space = offspring[offspring_idx, gene_idx] - else: - value_from_space = random.choice(values_to_select_from) - - # Assigning the selected value from the space to the gene. - if self.gene_type_single == True: - if not self.gene_type[1] is None: - offspring[offspring_idx, gene_idx] = numpy.round(self.gene_type[0](value_from_space), - self.gene_type[1]) - else: - offspring[offspring_idx, gene_idx] = self.gene_type[0](value_from_space) - else: - if not self.gene_type[gene_idx][1] is None: - offspring[offspring_idx, gene_idx] = numpy.round(self.gene_type[gene_idx][0](value_from_space), - self.gene_type[gene_idx][1]) - else: - offspring[offspring_idx, gene_idx] = self.gene_type[gene_idx][0](value_from_space) - - if self.allow_duplicate_genes == False: - offspring[offspring_idx], _, _ = self.solve_duplicate_genes_by_space(solution=offspring[offspring_idx], - gene_type=self.gene_type, - num_trials=10) - return offspring - - def mutation_randomly(self, offspring): - - """ - Applies the random mutation the mutation probability. For each gene, if its probability is <= that mutation probability, then it will be mutated randomly. - It accepts a single parameter: - -offspring: The offspring to mutate. - It returns an array of the mutated offspring. - """ - - # Random mutation changes one or more genes in each offspring randomly. - for offspring_idx in range(offspring.shape[0]): - mutation_indices = numpy.array(random.sample(range(0, self.num_genes), self.mutation_num_genes)) - for gene_idx in mutation_indices: - # Generating a random value. - random_value = numpy.random.uniform(low=self.random_mutation_min_val, - high=self.random_mutation_max_val, - size=1) - # If the mutation_by_replacement attribute is True, then the random value replaces the current gene value. - if self.mutation_by_replacement: - if self.gene_type_single == True: - random_value = self.gene_type[0](random_value) - else: - random_value = self.gene_type[gene_idx][0](random_value) - if type(random_value) is numpy.ndarray: - random_value = random_value[0] - # If the mutation_by_replacement attribute is False, then the random value is added to the gene value. - else: - if self.gene_type_single == True: - random_value = self.gene_type[0](offspring[offspring_idx, gene_idx] + random_value) - else: - random_value = self.gene_type[gene_idx][0](offspring[offspring_idx, gene_idx] + random_value) - if type(random_value) is numpy.ndarray: - random_value = random_value[0] - - # Round the gene - if self.gene_type_single == True: - if not self.gene_type[1] is None: - random_value = numpy.round(random_value, self.gene_type[1]) - else: - if not self.gene_type[gene_idx][1] is None: - random_value = numpy.round(random_value, self.gene_type[gene_idx][1]) - - offspring[offspring_idx, gene_idx] = random_value - - if self.allow_duplicate_genes == False: - offspring[offspring_idx], _, _ = self.solve_duplicate_genes_randomly(solution=offspring[offspring_idx], - min_val=self.random_mutation_min_val, - max_val=self.random_mutation_max_val, - mutation_by_replacement=self.mutation_by_replacement, - gene_type=self.gene_type, - num_trials=10) - - return 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. - It accepts a single parameter: - -offspring: The offspring to mutate. - It returns an array of the mutated offspring. - """ - - # Random mutation changes one or more gene in each offspring randomly. - for offspring_idx in range(offspring.shape[0]): - probs = numpy.random.random(size=offspring.shape[1]) - for gene_idx in range(offspring.shape[1]): - if probs[gene_idx] <= self.mutation_probability: - # Generating a random value. - random_value = numpy.random.uniform(low=self.random_mutation_min_val, - high=self.random_mutation_max_val, - size=1) - # If the mutation_by_replacement attribute is True, then the random value replaces the current gene value. - if self.mutation_by_replacement: - if self.gene_type_single == True: - random_value = self.gene_type[0](random_value) - else: - random_value = self.gene_type[gene_idx][0](random_value) - if type(random_value) is numpy.ndarray: - random_value = random_value[0] - # If the mutation_by_replacement attribute is False, then the random value is added to the gene value. - else: - if self.gene_type_single == True: - random_value = self.gene_type[0](offspring[offspring_idx, gene_idx] + random_value) - else: - random_value = self.gene_type[gene_idx][0](offspring[offspring_idx, gene_idx] + random_value) - if type(random_value) is numpy.ndarray: - random_value = random_value[0] - - # Round the gene - if self.gene_type_single == True: - if not self.gene_type[1] is None: - random_value = numpy.round(random_value, self.gene_type[1]) - else: - if not self.gene_type[gene_idx][1] is None: - random_value = numpy.round(random_value, self.gene_type[gene_idx][1]) - - offspring[offspring_idx, gene_idx] = random_value - - if self.allow_duplicate_genes == False: - offspring[offspring_idx], _, _ = self.solve_duplicate_genes_randomly(solution=offspring[offspring_idx], - min_val=self.random_mutation_min_val, - max_val=self.random_mutation_max_val, - mutation_by_replacement=self.mutation_by_replacement, - gene_type=self.gene_type, - num_trials=10) - return offspring - - def swap_mutation(self, offspring): - - """ - Applies the swap mutation which interchanges the values of 2 randomly selected genes. - It accepts a single parameter: - -offspring: The offspring to mutate. - It returns an array of the mutated offspring. - """ - - for idx in range(offspring.shape[0]): - mutation_gene1 = numpy.random.randint(low=0, high=offspring.shape[1]/2, size=1)[0] - mutation_gene2 = mutation_gene1 + int(offspring.shape[1]/2) - - temp = offspring[idx, mutation_gene1] - offspring[idx, mutation_gene1] = offspring[idx, mutation_gene2] - offspring[idx, mutation_gene2] = temp - return offspring - - def inversion_mutation(self, offspring): - - """ - Applies the inversion mutation which selects a subset of genes and inverts them (in order). - It accepts a single parameter: - -offspring: The offspring to mutate. - It returns an array of the mutated offspring. - """ - - for idx in range(offspring.shape[0]): - mutation_gene1 = numpy.random.randint(low=0, high=numpy.ceil(offspring.shape[1]/2 + 1), size=1)[0] - mutation_gene2 = mutation_gene1 + int(offspring.shape[1]/2) - - genes_to_scramble = numpy.flip(offspring[idx, mutation_gene1:mutation_gene2]) - offspring[idx, mutation_gene1:mutation_gene2] = genes_to_scramble - return offspring - - def scramble_mutation(self, offspring): - - """ - Applies the scramble mutation which selects a subset of genes and shuffles their order randomly. - It accepts a single parameter: - -offspring: The offspring to mutate. - It returns an array of the mutated offspring. - """ - - for idx in range(offspring.shape[0]): - mutation_gene1 = numpy.random.randint(low=0, high=numpy.ceil(offspring.shape[1]/2 + 1), size=1)[0] - mutation_gene2 = mutation_gene1 + int(offspring.shape[1]/2) - genes_range = numpy.arange(start=mutation_gene1, stop=mutation_gene2) - numpy.random.shuffle(genes_range) - - genes_to_scramble = numpy.flip(offspring[idx, genes_range]) - offspring[idx, genes_range] = genes_to_scramble - return offspring - - def adaptive_mutation_population_fitness(self, offspring): - - """ - A helper method to calculate the average fitness of the solutions before applying the adaptive mutation. - It accepts a single parameter: - -offspring: The offspring to mutate. - It returns the average fitness to be used in adaptive mutation. - """ - - fitness = self.last_generation_fitness.copy() - temp_population = numpy.zeros_like(self.population) - - if (self.keep_parents == 0): - parents_to_keep = [] - elif (self.keep_parents == -1): - parents_to_keep = self.last_generation_parents.copy() - temp_population[0:len(parents_to_keep), :] = parents_to_keep - elif (self.keep_parents > 0): - parents_to_keep, _ = self.steady_state_selection(self.last_generation_fitness, num_parents=self.keep_parents) - temp_population[0:len(parents_to_keep), :] = parents_to_keep - - temp_population[len(parents_to_keep):, :] = offspring - - fitness[:self.last_generation_parents.shape[0]] = self.last_generation_fitness[self.last_generation_parents_indices] - - for idx in range(len(parents_to_keep), fitness.shape[0]): - fitness[idx] = self.fitness_func(temp_population[idx], None) - average_fitness = numpy.mean(fitness) - - return average_fitness, fitness[len(parents_to_keep):] - - def adaptive_mutation(self, offspring): - - """ - Applies the adaptive mutation which changes the values of a number of genes randomly. In adaptive mutation, the number of genes to mutate differs based on the fitness value of the solution. - The random value is selected either using the 'gene_space' parameter or the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val'. - It accepts a single parameter: - -offspring: The offspring to mutate. - It returns an array of the mutated offspring. - """ - - # If the attribute 'gene_space' exists (i.e. not None), then the mutation values are selected from the 'gene_space' parameter according to the space of values of each gene. Otherwise, it is selected randomly based on the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val'. - # When the 'mutation_probability' parameter exists (i.e. not None), then it is used in the mutation. Otherwise, the 'mutation_num_genes' parameter is used. - - if self.mutation_probability is None: - # When the 'mutation_probability' parameter does not exist (i.e. None), then the parameter 'mutation_num_genes' is used in the mutation. - if not (self.gene_space is None): - # When the attribute 'gene_space' exists (i.e. not None), the mutation values are selected randomly from the space of values of each gene. - offspring = self.adaptive_mutation_by_space(offspring) - else: - # 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'. - offspring = self.adaptive_mutation_randomly(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' exists (i.e. not None), the mutation values are selected randomly from the space of values of each gene. - offspring = self.adaptive_mutation_probs_by_space(offspring) - else: - # 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'. - offspring = self.adaptive_mutation_probs_randomly(offspring) - - return offspring - - 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. - The random values are selected from the 'gene_space' parameter. - It accepts a single parameter: - -offspring: The offspring to mutate. - It returns an array of the mutated offspring. - """ - - # For each offspring, a value from the gene space is selected randomly and assigned to the selected gene for mutation. - - average_fitness, offspring_fitness = self.adaptive_mutation_population_fitness(offspring) - - # Adaptive mutation changes one or more genes in each offspring randomly. - # The number of genes to mutate depends on the solution's fitness value. - for offspring_idx in range(offspring.shape[0]): - if offspring_fitness[offspring_idx] < average_fitness: - adaptive_mutation_num_genes = self.mutation_num_genes[0] - else: - adaptive_mutation_num_genes = self.mutation_num_genes[1] - mutation_indices = numpy.array(random.sample(range(0, self.num_genes), adaptive_mutation_num_genes)) - for gene_idx in mutation_indices: - - if self.gene_space_nested: - # Returning the current gene space from the 'gene_space' attribute. - if type(self.gene_space[gene_idx]) in [numpy.ndarray, list]: - curr_gene_space = self.gene_space[gene_idx].copy() - else: - curr_gene_space = self.gene_space[gene_idx] - - # If the gene space has only a single value, use it as the new gene value. - if type(curr_gene_space) in GA.supported_int_float_types: - value_from_space = curr_gene_space - # If the gene space is None, apply mutation by adding a random value between the range defined by the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val'. - elif curr_gene_space is None: - rand_val = numpy.random.uniform(low=self.random_mutation_min_val, - high=self.random_mutation_max_val, - size=1) - if self.mutation_by_replacement: - value_from_space = rand_val - else: - value_from_space = offspring[offspring_idx, gene_idx] + rand_val - elif type(curr_gene_space) is dict: - # Selecting a value randomly from the current gene's space in the 'gene_space' attribute. - if 'step' in curr_gene_space.keys(): - value_from_space = numpy.random.choice(numpy.arange(start=curr_gene_space['low'], - stop=curr_gene_space['high'], - step=curr_gene_space['step']), - size=1) - else: - value_from_space = numpy.random.uniform(low=curr_gene_space['low'], - high=curr_gene_space['high'], - size=1) - else: - # Selecting a value randomly from the current gene's space in the 'gene_space' attribute. - # If the gene space has only 1 value, then select it. The old and new values of the gene are identical. - if len(curr_gene_space) == 1: - value_from_space = curr_gene_space[0] - # If the gene space has more than 1 value, then select a new one that is different from the current value. - else: - values_to_select_from = list(set(curr_gene_space) - set([offspring[offspring_idx, gene_idx]])) - if len(values_to_select_from) == 0: - value_from_space = offspring[offspring_idx, gene_idx] - else: - value_from_space = random.choice(values_to_select_from) - else: - # Selecting a value randomly from the global gene space in the 'gene_space' attribute. - if type(self.gene_space) is dict: - if 'step' in self.gene_space.keys(): - value_from_space = numpy.random.choice(numpy.arange(start=self.gene_space['low'], - stop=self.gene_space['high'], - step=self.gene_space['step']), - size=1) - else: - value_from_space = numpy.random.uniform(low=self.gene_space['low'], - high=self.gene_space['high'], - size=1) - else: - values_to_select_from = list(set(self.gene_space) - set([offspring[offspring_idx, gene_idx]])) - if len(values_to_select_from) == 0: - value_from_space = offspring[offspring_idx, gene_idx] - else: - value_from_space = random.choice(values_to_select_from) - - - if value_from_space is None: - value_from_space = numpy.random.uniform(low=self.random_mutation_min_val, - high=self.random_mutation_max_val, - size=1) - - # Assinging the selected value from the space to the gene. - if self.gene_type_single == True: - if not self.gene_type[1] is None: - offspring[offspring_idx, gene_idx] = numpy.round(self.gene_type[0](value_from_space), - self.gene_type[1]) - else: - offspring[offspring_idx, gene_idx] = self.gene_type[0](value_from_space) - else: - if not self.gene_type[gene_idx][1] is None: - offspring[offspring_idx, gene_idx] = numpy.round(self.gene_type[gene_idx][0](value_from_space), - self.gene_type[gene_idx][1]) - else: - offspring[offspring_idx, gene_idx] = self.gene_type[gene_idx][0](value_from_space) - - if self.allow_duplicate_genes == False: - offspring[offspring_idx], _, _ = self.solve_duplicate_genes_by_space(solution=offspring[offspring_idx], - gene_type=self.gene_type, - num_trials=10) - return offspring - - 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. - The random values are selected based on the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val'. - It accepts a single parameter: - -offspring: The offspring to mutate. - It returns an array of the mutated offspring. - """ - - average_fitness, offspring_fitness = self.adaptive_mutation_population_fitness(offspring) - - # Adaptive random mutation changes one or more genes in each offspring randomly. - # The number of genes to mutate depends on the solution's fitness value. - for offspring_idx in range(offspring.shape[0]): - if offspring_fitness[offspring_idx] < average_fitness: - adaptive_mutation_num_genes = self.mutation_num_genes[0] - else: - adaptive_mutation_num_genes = self.mutation_num_genes[1] - mutation_indices = numpy.array(random.sample(range(0, self.num_genes), adaptive_mutation_num_genes)) - for gene_idx in mutation_indices: - # Generating a random value. - random_value = numpy.random.uniform(low=self.random_mutation_min_val, - high=self.random_mutation_max_val, - size=1) - # If the mutation_by_replacement attribute is True, then the random value replaces the current gene value. - if self.mutation_by_replacement: - if self.gene_type_single == True: - random_value = self.gene_type[0](random_value) - else: - random_value = self.gene_type[gene_idx][0](random_value) - if type(random_value) is numpy.ndarray: - random_value = random_value[0] - # If the mutation_by_replacement attribute is False, then the random value is added to the gene value. - else: - if self.gene_type_single == True: - random_value = self.gene_type[0](offspring[offspring_idx, gene_idx] + random_value) - else: - random_value = self.gene_type[gene_idx][0](offspring[offspring_idx, gene_idx] + random_value) - if type(random_value) is numpy.ndarray: - random_value = random_value[0] - - if self.gene_type_single == True: - if not self.gene_type[1] is None: - random_value = numpy.round(random_value, self.gene_type[1]) - else: - if not self.gene_type[gene_idx][1] is None: - random_value = numpy.round(random_value, self.gene_type[gene_idx][1]) - - offspring[offspring_idx, gene_idx] = random_value - - if self.allow_duplicate_genes == False: - offspring[offspring_idx], _, _ = self.solve_duplicate_genes_randomly(solution=offspring[offspring_idx], - min_val=self.random_mutation_min_val, - max_val=self.random_mutation_max_val, - mutation_by_replacement=self.mutation_by_replacement, - gene_type=self.gene_type, - num_trials=10) - return offspring - - 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. - The random values are selected based on space of values for each gene. - It accepts a single parameter: - -offspring: The offspring to mutate. - It returns an array of the mutated offspring. - """ - - # For each offspring, a value from the gene space is selected randomly and assigned to the selected gene for mutation. - - average_fitness, offspring_fitness = self.adaptive_mutation_population_fitness(offspring) - - # Adaptive random mutation changes one or more genes in each offspring randomly. - # The probability of mutating a gene depends on the solution's fitness value. - for offspring_idx in range(offspring.shape[0]): - if offspring_fitness[offspring_idx] < average_fitness: - adaptive_mutation_probability = self.mutation_probability[0] - else: - adaptive_mutation_probability = self.mutation_probability[1] - - probs = numpy.random.random(size=offspring.shape[1]) - for gene_idx in range(offspring.shape[1]): - if probs[gene_idx] <= adaptive_mutation_probability: - if self.gene_space_nested: - # Returning the current gene space from the 'gene_space' attribute. - if type(self.gene_space[gene_idx]) in [numpy.ndarray, list]: - curr_gene_space = self.gene_space[gene_idx].copy() - else: - curr_gene_space = self.gene_space[gene_idx] - - # If the gene space has only a single value, use it as the new gene value. - if type(curr_gene_space) in GA.supported_int_float_types: - value_from_space = curr_gene_space - # If the gene space is None, apply mutation by adding a random value between the range defined by the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val'. - elif curr_gene_space is None: - rand_val = numpy.random.uniform(low=self.random_mutation_min_val, - high=self.random_mutation_max_val, - size=1) - if self.mutation_by_replacement: - value_from_space = rand_val - else: - value_from_space = offspring[offspring_idx, gene_idx] + rand_val - elif type(curr_gene_space) is dict: - # Selecting a value randomly from the current gene's space in the 'gene_space' attribute. - if 'step' in curr_gene_space.keys(): - value_from_space = numpy.random.choice(numpy.arange(start=curr_gene_space['low'], - stop=curr_gene_space['high'], - step=curr_gene_space['step']), - size=1) - else: - value_from_space = numpy.random.uniform(low=curr_gene_space['low'], - high=curr_gene_space['high'], - size=1) - else: - # Selecting a value randomly from the current gene's space in the 'gene_space' attribute. - # If the gene space has only 1 value, then select it. The old and new values of the gene are identical. - if len(curr_gene_space) == 1: - value_from_space = curr_gene_space[0] - # If the gene space has more than 1 value, then select a new one that is different from the current value. - else: - values_to_select_from = list(set(curr_gene_space) - set([offspring[offspring_idx, gene_idx]])) - if len(values_to_select_from) == 0: - value_from_space = offspring[offspring_idx, gene_idx] - else: - value_from_space = random.choice(values_to_select_from) - else: - # Selecting a value randomly from the global gene space in the 'gene_space' attribute. - if type(self.gene_space) is dict: - if 'step' in self.gene_space.keys(): - value_from_space = numpy.random.choice(numpy.arange(start=self.gene_space['low'], - stop=self.gene_space['high'], - step=self.gene_space['step']), - size=1) - else: - value_from_space = numpy.random.uniform(low=self.gene_space['low'], - high=self.gene_space['high'], - size=1) - else: - values_to_select_from = list(set(self.gene_space) - set([offspring[offspring_idx, gene_idx]])) - if len(values_to_select_from) == 0: - value_from_space = offspring[offspring_idx, gene_idx] - else: - value_from_space = random.choice(values_to_select_from) - - if value_from_space is None: - value_from_space = numpy.random.uniform(low=self.random_mutation_min_val, - high=self.random_mutation_max_val, - size=1) - - # Assinging the selected value from the space to the gene. - if self.gene_type_single == True: - if not self.gene_type[1] is None: - offspring[offspring_idx, gene_idx] = numpy.round(self.gene_type[0](value_from_space), - self.gene_type[1]) - else: - offspring[offspring_idx, gene_idx] = self.gene_type[0](value_from_space) - else: - if not self.gene_type[gene_idx][1] is None: - offspring[offspring_idx, gene_idx] = numpy.round(self.gene_type[gene_idx][0](value_from_space), - self.gene_type[gene_idx][1]) - else: - offspring[offspring_idx, gene_idx] = self.gene_type[gene_idx][0](value_from_space) - - if self.allow_duplicate_genes == False: - offspring[offspring_idx], _, _ = self.solve_duplicate_genes_by_space(solution=offspring[offspring_idx], - gene_type=self.gene_type, - num_trials=10) - return offspring - - 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. - The random values are selected based on the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val'. - It accepts a single parameter: - -offspring: The offspring to mutate. - It returns an array of the mutated offspring. - """ - - average_fitness, offspring_fitness = self.adaptive_mutation_population_fitness(offspring) - - # Adaptive random mutation changes one or more genes in each offspring randomly. - # The probability of mutating a gene depends on the solution's fitness value. - for offspring_idx in range(offspring.shape[0]): - if offspring_fitness[offspring_idx] < average_fitness: - adaptive_mutation_probability = self.mutation_probability[0] - else: - adaptive_mutation_probability = self.mutation_probability[1] - - probs = numpy.random.random(size=offspring.shape[1]) - for gene_idx in range(offspring.shape[1]): - if probs[gene_idx] <= adaptive_mutation_probability: - # Generating a random value. - random_value = numpy.random.uniform(low=self.random_mutation_min_val, - high=self.random_mutation_max_val, - size=1) - # If the mutation_by_replacement attribute is True, then the random value replaces the current gene value. - if self.mutation_by_replacement: - if self.gene_type_single == True: - random_value = self.gene_type[0](random_value) - else: - random_value = self.gene_type[gene_idx][0](random_value) - if type(random_value) is numpy.ndarray: - random_value = random_value[0] - # If the mutation_by_replacement attribute is False, then the random value is added to the gene value. - else: - if self.gene_type_single == True: - random_value = self.gene_type[0](offspring[offspring_idx, gene_idx] + random_value) - else: - random_value = self.gene_type[gene_idx][0](offspring[offspring_idx, gene_idx] + random_value) - if type(random_value) is numpy.ndarray: - random_value = random_value[0] - - if self.gene_type_single == True: - if not self.gene_type[1] is None: - random_value = numpy.round(random_value, self.gene_type[1]) - else: - if not self.gene_type[gene_idx][1] is None: - random_value = numpy.round(random_value, self.gene_type[gene_idx][1]) - - offspring[offspring_idx, gene_idx] = random_value - - if self.allow_duplicate_genes == False: - offspring[offspring_idx], _, _ = self.solve_duplicate_genes_randomly(solution=offspring[offspring_idx], - min_val=self.random_mutation_min_val, - max_val=self.random_mutation_max_val, - mutation_by_replacement=self.mutation_by_replacement, - gene_type=self.gene_type, - num_trials=10) - return offspring - - def solve_duplicate_genes_randomly(self, solution, min_val, max_val, mutation_by_replacement, gene_type, num_trials=10): - - """ - Solves the duplicates in a solution by randomly selecting new values for the duplicating genes. - - solution: A solution with duplicate values. - min_val: Minimum value of the range to sample a number randomly. - max_val: Maximum value of the range to sample a number randomly. - mutation_by_replacement: Identical to the self.mutation_by_replacement attribute. - gene_type: Exactly the same as the self.gene_type attribute. - num_trials: Maximum number of trials to change the gene value to solve the duplicates. - - Returns: - new_solution: Solution after trying to solve its duplicates. If no duplicates solved, then it is identical to the passed solution parameter. - not_unique_indices: Indices of the genes with duplicate values. - num_unsolved_duplicates: Number of unsolved duplicates. - """ - - new_solution = solution.copy() - - _, unique_gene_indices = numpy.unique(solution, return_index=True) - not_unique_indices = set(range(len(solution))) - set(unique_gene_indices) - - num_unsolved_duplicates = 0 - if len(not_unique_indices) > 0: - for duplicate_index in not_unique_indices: - for trial_index in range(num_trials): - if self.gene_type_single == True: - if gene_type[0] in GA.supported_int_types: - temp_val = self.unique_int_gene_from_range(solution=new_solution, - gene_index=duplicate_index, - min_val=min_val, - max_val=max_val, - mutation_by_replacement=mutation_by_replacement, - gene_type=gene_type) - else: - temp_val = numpy.random.uniform(low=min_val, - high=max_val, - size=1) - if mutation_by_replacement: - pass - else: - temp_val = new_solution[duplicate_index] + temp_val - else: - if gene_type[duplicate_index] in GA.supported_int_types: - temp_val = self.unique_int_gene_from_range(solution=new_solution, - gene_index=duplicate_index, - min_val=min_val, - max_val=max_val, - mutation_by_replacement=mutation_by_replacement, - gene_type=gene_type) - else: - temp_val = numpy.random.uniform(low=min_val, - high=max_val, - size=1) - if mutation_by_replacement: - pass - else: - temp_val = new_solution[duplicate_index] + temp_val - - if self.gene_type_single == True: - if not gene_type[1] is None: - temp_val = numpy.round(gene_type[0](temp_val), - gene_type[1]) - else: - temp_val = gene_type[0](temp_val) - else: - if not gene_type[duplicate_index][1] is None: - temp_val = numpy.round(gene_type[duplicate_index][0](temp_val), - gene_type[duplicate_index][1]) - else: - temp_val = gene_type[duplicate_index][0](temp_val) - - if temp_val in new_solution and trial_index == (num_trials - 1): - num_unsolved_duplicates = num_unsolved_duplicates + 1 - if not self.suppress_warnings: warnings.warn("Failed to find a unique value for gene with index {gene_idx}. Consider adding more values in the gene space or use a wider range for initial population or random mutation.".format(gene_idx=duplicate_index)) - elif temp_val in new_solution: - continue - else: - new_solution[duplicate_index] = temp_val - break - - # Update the list of duplicate indices after each iteration. - _, unique_gene_indices = numpy.unique(new_solution, return_index=True) - not_unique_indices = set(range(len(solution))) - set(unique_gene_indices) - # print("not_unique_indices INSIDE", not_unique_indices) - - return new_solution, not_unique_indices, num_unsolved_duplicates - - def solve_duplicate_genes_by_space(self, solution, gene_type, num_trials=10, build_initial_pop=False): - - """ - Solves the duplicates in a solution by selecting values for the duplicating genes from the gene space. - - solution: A solution with duplicate values. - gene_type: Exactly the same as the self.gene_type attribute. - num_trials: Maximum number of trials to change the gene value to solve the duplicates. - - Returns: - new_solution: Solution after trying to solve its duplicates. If no duplicates solved, then it is identical to the passed solution parameter. - not_unique_indices: Indices of the genes with duplicate values. - num_unsolved_duplicates: Number of unsolved duplicates. - """ - - new_solution = solution.copy() - - _, unique_gene_indices = numpy.unique(solution, return_index=True) - not_unique_indices = set(range(len(solution))) - set(unique_gene_indices) - # print("not_unique_indices OUTSIDE", not_unique_indices) - - # First try to solve the duplicates. - # For a solution like [3 2 0 0], the indices of the 2 duplicating genes are 2 and 3. - # The next call to the find_unique_value() method tries to change the value of the gene with index 3 to solve the duplicate. - if len(not_unique_indices) > 0: - new_solution, not_unique_indices, num_unsolved_duplicates = self.unique_genes_by_space(new_solution=new_solution, - gene_type=gene_type, - not_unique_indices=not_unique_indices, - num_trials=10, - build_initial_pop=build_initial_pop) - else: - return new_solution, not_unique_indices, len(not_unique_indices) - - # Do another try if there exist duplicate genes. - # If there are no possible values for the gene 3 with index 3 to solve the duplicate, try to change the value of the other gene with index 2. - if len(not_unique_indices) > 0: - not_unique_indices = set(numpy.where(new_solution == new_solution[list(not_unique_indices)[0]])[0]) - set([list(not_unique_indices)[0]]) - new_solution, not_unique_indices, num_unsolved_duplicates = self.unique_genes_by_space(new_solution=new_solution, - gene_type=gene_type, - not_unique_indices=not_unique_indices, - num_trials=10, - build_initial_pop=build_initial_pop) - else: - # 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. - # 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. - return new_solution, not_unique_indices, len(not_unique_indices) - - return new_solution, not_unique_indices, num_unsolved_duplicates - - def solve_duplicate_genes_by_space_OLD(self, solution, gene_type, num_trials=10): - # ///////////////////////// - # Just for testing purposes. - # ///////////////////////// - - new_solution = solution.copy() - - _, unique_gene_indices = numpy.unique(solution, return_index=True) - not_unique_indices = set(range(len(solution))) - set(unique_gene_indices) - # print("not_unique_indices OUTSIDE", not_unique_indices) - - num_unsolved_duplicates = 0 - if len(not_unique_indices) > 0: - for duplicate_index in not_unique_indices: - for trial_index in range(num_trials): - temp_val = self.unique_gene_by_space(solution=solution, - gene_idx=duplicate_index, - gene_type=gene_type) - - if temp_val in new_solution and trial_index == (num_trials - 1): - # print("temp_val, duplicate_index", temp_val, duplicate_index, new_solution) - num_unsolved_duplicates = num_unsolved_duplicates + 1 - if not self.suppress_warnings: warnings.warn("Failed to find a unique value for gene with index {gene_idx}".format(gene_idx=duplicate_index)) - elif temp_val in new_solution: - continue - else: - new_solution[duplicate_index] = temp_val - # print("SOLVED", duplicate_index) - break - - # Update the list of duplicate indices after each iteration. - _, unique_gene_indices = numpy.unique(new_solution, return_index=True) - not_unique_indices = set(range(len(solution))) - set(unique_gene_indices) - # print("not_unique_indices INSIDE", not_unique_indices) - - return new_solution, not_unique_indices, num_unsolved_duplicates - - def unique_int_gene_from_range(self, solution, gene_index, min_val, max_val, mutation_by_replacement, gene_type, step=None): - - """ - Finds a unique integer value for the gene. - - solution: A solution with duplicate values. - gene_index: Index of the gene to find a unique value. - min_val: Minimum value of the range to sample a number randomly. - max_val: Maximum value of the range to sample a number randomly. - mutation_by_replacement: Identical to the self.mutation_by_replacement attribute. - gene_type: Exactly the same as the self.gene_type attribute. - - Returns: - selected_value: The new value of the gene. It may be identical to the original gene value in case there are no possible unique values for the gene. - """ - - if self.gene_type_single == True: - if step is None: - all_gene_values = numpy.arange(min_val, max_val, dtype=gene_type[0]) - else: - # For non-integer steps, the numpy.arange() function returns zeros id the dtype parameter is set to an integer data type. So, this returns zeros if step is non-integer and dtype is set to an int data type: numpy.arange(min_val, max_val, step, dtype=gene_type[0]) - # To solve this issue, the data type casting will not be handled inside numpy.arange(). The range is generated by numpy.arange() and then the data type is converted using the numpy.asarray() function. - all_gene_values = numpy.asarray(numpy.arange(min_val, max_val, step), dtype=gene_type[0]) - else: - if step is None: - all_gene_values = numpy.arange(min_val, max_val, dtype=gene_type[gene_index][0]) - else: - all_gene_values = numpy.asarray(numpy.arange(min_val, max_val, step), dtype=gene_type[gene_index][0]) - - if mutation_by_replacement: - pass - else: - all_gene_values = all_gene_values + solution[gene_index] - - if self.gene_type_single == True: - if not gene_type[1] is None: - all_gene_values = numpy.round(gene_type[0](all_gene_values), - gene_type[1]) - else: - if type(all_gene_values) is numpy.ndarray: - all_gene_values = numpy.asarray(all_gene_values, dtype=gene_type[0]) - else: - all_gene_values = gene_type[0](all_gene_values) - else: - if not gene_type[gene_index][1] is None: - all_gene_values = numpy.round(gene_type[gene_index][0](all_gene_values), - gene_type[gene_index][1]) - else: - all_gene_values = gene_type[gene_index][0](all_gene_values) - - values_to_select_from = list(set(all_gene_values) - set(solution)) - - if len(values_to_select_from) == 0: - if not self.suppress_warnings: warnings.warn("You set 'allow_duplicate_genes=False' but there is no enough values to prevent duplicates.") - selected_value = solution[gene_index] - else: - selected_value = random.choice(values_to_select_from) - - #if self.gene_type_single == True: - # selected_value = gene_type[0](selected_value) - #else: - # selected_value = gene_type[gene_index][0](selected_value) - - return selected_value - - def unique_genes_by_space(self, new_solution, gene_type, not_unique_indices, num_trials=10, build_initial_pop=False): - - """ - 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. - - new_solution: A solution with duplicate values. - gene_type: Exactly the same as the self.gene_type attribute. - not_unique_indices: Indices with duplicating values. - num_trials: Maximum number of trials to change the gene value to solve the duplicates. - - Returns: - new_solution: Solution after trying to solve all of its duplicates. If no duplicates solved, then it is identical to the passed solution parameter. - not_unique_indices: Indices of the genes with duplicate values. - num_unsolved_duplicates: Number of unsolved duplicates. - """ - - num_unsolved_duplicates = 0 - for duplicate_index in not_unique_indices: - for trial_index in range(num_trials): - temp_val = self.unique_gene_by_space(solution=new_solution, - gene_idx=duplicate_index, - gene_type=gene_type, - build_initial_pop=build_initial_pop) - - if temp_val in new_solution and trial_index == (num_trials - 1): - # print("temp_val, duplicate_index", temp_val, duplicate_index, new_solution) - num_unsolved_duplicates = num_unsolved_duplicates + 1 - if not self.suppress_warnings: warnings.warn("Failed to find a unique value for gene with index {gene_idx}. Consider adding more values in the gene space or use a wider range for initial population or random mutation.".format(gene_idx=duplicate_index)) - elif temp_val in new_solution: - continue - else: - new_solution[duplicate_index] = temp_val - # print("SOLVED", duplicate_index) - break - - # Update the list of duplicate indices after each iteration. - _, unique_gene_indices = numpy.unique(new_solution, return_index=True) - not_unique_indices = set(range(len(new_solution))) - set(unique_gene_indices) - # print("not_unique_indices INSIDE", not_unique_indices) - - return new_solution, not_unique_indices, num_unsolved_duplicates - - def unique_gene_by_space(self, solution, gene_idx, gene_type, build_initial_pop=False): - - """ - Returns a unique gene value for a single gene based on its value space to solve the duplicates. - - solution: A solution with duplicate values. - gene_idx: The index of the gene that duplicates its value with another gene. - gene_type: Exactly the same as the self.gene_type attribute. - - Returns: - A unique value, if exists, for the gene. - """ - - if self.gene_space_nested: - # Returning the current gene space from the 'gene_space' attribute. - if type(self.gene_space[gene_idx]) in [numpy.ndarray, list]: - curr_gene_space = self.gene_space[gene_idx].copy() - else: - curr_gene_space = self.gene_space[gene_idx] - - # If the gene space has only a single value, use it as the new gene value. - if type(curr_gene_space) in GA.supported_int_float_types: - value_from_space = curr_gene_space - # If the gene space is None, apply mutation by adding a random value between the range defined by the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val'. - elif curr_gene_space is None: - if self.gene_type_single == True: - if gene_type[0] in GA.supported_int_types: - if build_initial_pop == True: - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=self.random_mutation_min_val, - max_val=self.random_mutation_max_val, - mutation_by_replacement=True, - gene_type=gene_type) - else: - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=self.random_mutation_min_val, - max_val=self.random_mutation_max_val, - mutation_by_replacement=True, #self.mutation_by_replacement, - gene_type=gene_type) - else: - value_from_space = numpy.random.uniform(low=self.random_mutation_min_val, - high=self.random_mutation_max_val, - size=1) - if self.mutation_by_replacement: - pass - else: - value_from_space = solution[gene_idx] + value_from_space - else: - if gene_type[gene_idx] in GA.supported_int_types: - if build_initial_pop == True: - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=self.random_mutation_min_val, - max_val=self.random_mutation_max_val, - mutation_by_replacement=True, - gene_type=gene_type) - else: - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=self.random_mutation_min_val, - max_val=self.random_mutation_max_val, - mutation_by_replacement=True, #self.mutation_by_replacement, - gene_type=gene_type) - else: - value_from_space = numpy.random.uniform(low=self.random_mutation_min_val, - high=self.random_mutation_max_val, - size=1) - if self.mutation_by_replacement: - pass - else: - value_from_space = solution[gene_idx] + value_from_space - - elif type(curr_gene_space) is dict: - if self.gene_type_single == True: - if gene_type[0] in GA.supported_int_types: - if build_initial_pop == True: - if 'step' in curr_gene_space.keys(): - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=curr_gene_space['low'], - max_val=curr_gene_space['high'], - step=curr_gene_space['step'], - mutation_by_replacement=True, - gene_type=gene_type) - else: - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=curr_gene_space['low'], - max_val=curr_gene_space['high'], - step=None, - mutation_by_replacement=True, - gene_type=gene_type) - else: - if 'step' in curr_gene_space.keys(): - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=curr_gene_space['low'], - max_val=curr_gene_space['high'], - step=curr_gene_space['step'], - mutation_by_replacement=True, - gene_type=gene_type) - else: - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=curr_gene_space['low'], - max_val=curr_gene_space['high'], - step=None, - mutation_by_replacement=True, - gene_type=gene_type) - else: - if 'step' in curr_gene_space.keys(): - value_from_space = numpy.random.choice(numpy.arange(start=curr_gene_space['low'], - stop=curr_gene_space['high'], - step=curr_gene_space['step']), - size=1) - else: - value_from_space = numpy.random.uniform(low=curr_gene_space['low'], - high=curr_gene_space['high'], - size=1) - if self.mutation_by_replacement: - pass - else: - value_from_space = solution[gene_idx] + value_from_space - else: - if gene_type[gene_idx] in GA.supported_int_types: - if build_initial_pop == True: - if 'step' in curr_gene_space.keys(): - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=curr_gene_space['low'], - max_val=curr_gene_space['high'], - step=curr_gene_space['step'], - mutation_by_replacement=True, - gene_type=gene_type) - else: - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=curr_gene_space['low'], - max_val=curr_gene_space['high'], - step=None, - mutation_by_replacement=True, - gene_type=gene_type) - else: - if 'step' in curr_gene_space.keys(): - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=curr_gene_space['low'], - max_val=curr_gene_space['high'], - step=curr_gene_space['step'], - mutation_by_replacement=True, - gene_type=gene_type) - else: - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=curr_gene_space['low'], - max_val=curr_gene_space['high'], - step=None, - mutation_by_replacement=True, - gene_type=gene_type) - else: - if 'step' in curr_gene_space.keys(): - value_from_space = numpy.random.choice(numpy.arange(start=curr_gene_space['low'], - stop=curr_gene_space['high'], - step=curr_gene_space['step']), - size=1) - else: - value_from_space = numpy.random.uniform(low=curr_gene_space['low'], - high=curr_gene_space['high'], - size=1) - if self.mutation_by_replacement: - pass - else: - value_from_space = solution[gene_idx] + value_from_space - - else: - # Selecting a value randomly based on the current gene's space in the 'gene_space' attribute. - # If the gene space has only 1 value, then select it. The old and new values of the gene are identical. - if len(curr_gene_space) == 1: - value_from_space = curr_gene_space[0] - if not self.suppress_warnings: warnings.warn("You set 'allow_duplicate_genes=False' but the space of the gene with index {gene_idx} has only a single value. Thus, duplicates are possible.".format(gene_idx=gene_idx)) - # If the gene space has more than 1 value, then select a new one that is different from the current value. - else: - values_to_select_from = list(set(curr_gene_space) - set(solution)) - if len(values_to_select_from) == 0: - if not self.suppress_warnings: warnings.warn("You set 'allow_duplicate_genes=False' but the gene space does not have enough values to prevent duplicates.") - value_from_space = solution[gene_idx] - else: - value_from_space = random.choice(values_to_select_from) - else: - # Selecting a value randomly from the global gene space in the 'gene_space' attribute. - if type(self.gene_space) is dict: - if self.gene_type_single == True: - if gene_type[0] in GA.supported_int_types: - if build_initial_pop == True: - if 'step' in self.gene_space.keys(): - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=self.gene_space['low'], - max_val=self.gene_space['high'], - step=self.gene_space['step'], - mutation_by_replacement=True, - gene_type=gene_type) - else: - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=self.gene_space['low'], - max_val=self.gene_space['high'], - step=None, - mutation_by_replacement=True, - gene_type=gene_type) - else: - if 'step' in self.gene_space.keys(): - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=self.gene_space['low'], - max_val=self.gene_space['high'], - step=self.gene_space['step'], - mutation_by_replacement=True, - gene_type=gene_type) - else: - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=self.gene_space['low'], - max_val=self.gene_space['high'], - step=None, - mutation_by_replacement=True, - gene_type=gene_type) - else: - # When the gene_space is assigned a dict object, then it specifies the lower and upper limits of all genes in the space. - if 'step' in self.gene_space.keys(): - value_from_space = numpy.random.choice(numpy.arange(start=self.gene_space['low'], - stop=self.gene_space['high'], - step=self.gene_space['step']), - size=1) - else: - value_from_space = numpy.random.uniform(low=self.gene_space['low'], - high=self.gene_space['high'], - size=1) - if self.mutation_by_replacement: - pass - else: - value_from_space = solution[gene_idx] + value_from_space - else: - if gene_type[gene_idx] in GA.supported_int_types: - if build_initial_pop == True: - if 'step' in self.gene_space.keys(): - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=self.gene_space['low'], - max_val=self.gene_space['high'], - step=self.gene_space['step'], - mutation_by_replacement=True, - gene_type=gene_type) - else: - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=self.gene_space['low'], - max_val=self.gene_space['high'], - step=None, - mutation_by_replacement=True, - gene_type=gene_type) - else: - if 'step' in self.gene_space.keys(): - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=self.gene_space['low'], - max_val=self.gene_space['high'], - step=self.gene_space['step'], - mutation_by_replacement=True, - gene_type=gene_type) - else: - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=self.gene_space['low'], - max_val=self.gene_space['high'], - step=None, - mutation_by_replacement=True, - gene_type=gene_type) - else: - # When the gene_space is assigned a dict object, then it specifies the lower and upper limits of all genes in the space. - if 'step' in self.gene_space.keys(): - value_from_space = numpy.random.choice(numpy.arange(start=self.gene_space['low'], - stop=self.gene_space['high'], - step=self.gene_space['step']), - size=1) - else: - value_from_space = numpy.random.uniform(low=self.gene_space['low'], - high=self.gene_space['high'], - size=1) - if self.mutation_by_replacement: - pass - else: - value_from_space = solution[gene_idx] + value_from_space - - else: - # If the space type is not of type dict, then a value is randomly selected from the gene_space attribute. - values_to_select_from = list(set(self.gene_space) - set(solution)) - if len(values_to_select_from) == 0: - if not self.suppress_warnings: warnings.warn("You set 'allow_duplicate_genes=False' but the gene space does not have enough values to prevent duplicates.") - value_from_space = solution[gene_idx] - else: - value_from_space = random.choice(values_to_select_from) - - if value_from_space is None: - value_from_space = numpy.random.uniform(low=self.random_mutation_min_val, - high=self.random_mutation_max_val, - size=1) - - if self.gene_type_single == True: - if not gene_type[1] is None: - value_from_space = numpy.round(gene_type[0](value_from_space), - gene_type[1]) - else: - value_from_space = gene_type[0](value_from_space) - else: - if not gene_type[gene_idx][1] is None: - value_from_space = numpy.round(gene_type[gene_idx][0](value_from_space), - gene_type[gene_idx][1]) - else: - value_from_space = gene_type[gene_idx][0](value_from_space) - - return value_from_space - - 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 current population. If None, then the cal_pop_fitness() method is called to calculate the fitness of the population. - The following are returned: - -best_solution: Best solution in the current population. - -best_solution_fitness: Fitness value of the best solution. - -best_match_idx: Index of the best solution in the current population. - """ - - # Getting the best solution after finishing all generations. - # At first, the fitness is calculated for each solution in the final generation. - if pop_fitness is None: - pop_fitness = self.cal_pop_fitness() - # Then return the index of that solution corresponding to the best fitness. - best_match_idx = numpy.where(pop_fitness == numpy.max(pop_fitness))[0][0] - - best_solution = self.population[best_match_idx, :].copy() - best_solution_fitness = pop_fitness[best_match_idx] - - return best_solution, best_solution_fitness, best_match_idx - - def plot_result(self, - title="PyGAD - Generation vs. Fitness", - xlabel="Generation", - ylabel="Fitness", - linewidth=3, - font_size=14, - plot_type="plot", - color="#3870FF", - save_dir=None): - - if not self.suppress_warnings: - warnings.warn("Please use the plot_fitness() method instead of plot_result(). The plot_result() method will be removed in the future.") - - return self.plot_fitness(title=title, - xlabel=xlabel, - ylabel=ylabel, - linewidth=linewidth, - font_size=font_size, - plot_type=plot_type, - color=color, - save_dir=save_dir) - - def plot_fitness(self, - title="PyGAD - Generation vs. Fitness", - xlabel="Generation", - ylabel="Fitness", - linewidth=3, - font_size=14, - plot_type="plot", - color="#3870FF", - save_dir=None): - - """ - Creates, shows, and returns a figure that summarizes how the fitness value evolved by generation. Can only be called after completing at least 1 generation. If no generation is completed, an exception is raised. - - Accepts the following: - title: Figure title. - xlabel: Label on the X-axis. - ylabel: Label on the Y-axis. - linewidth: Line width of the plot. Defaults to 3. - font_size: Font size for the labels and title. Defaults to 14. - plot_type: Type of the plot which can be either "plot" (default), "scatter", or "bar". - color: Color of the plot which defaults to "#3870FF". - save_dir: Directory to save the figure. - - Returns the figure. - """ - - if self.generations_completed < 1: - raise RuntimeError("The plot_fitness() (i.e. plot_result()) method can only be called after completing at least 1 generation but ({generations_completed}) is completed.".format(generations_completed=self.generations_completed)) - -# if self.run_completed == False: -# if not self.suppress_warnings: warnings.warn("Warning calling the plot_result() method: \nGA is not executed yet and there are no results to display. Please call the run() method before calling the plot_result() method.\n") - - fig = matplotlib.pyplot.figure() - if plot_type == "plot": - matplotlib.pyplot.plot(self.best_solutions_fitness, linewidth=linewidth, color=color) - elif plot_type == "scatter": - matplotlib.pyplot.scatter(range(self.generations_completed + 1), self.best_solutions_fitness, linewidth=linewidth, color=color) - elif plot_type == "bar": - matplotlib.pyplot.bar(range(self.generations_completed + 1), self.best_solutions_fitness, linewidth=linewidth, color=color) - matplotlib.pyplot.title(title, fontsize=font_size) - matplotlib.pyplot.xlabel(xlabel, fontsize=font_size) - matplotlib.pyplot.ylabel(ylabel, fontsize=font_size) - - if not save_dir is None: - matplotlib.pyplot.savefig(fname=save_dir, - bbox_inches='tight') - matplotlib.pyplot.show() - - return fig - - def plot_new_solution_rate(self, - title="PyGAD - Generation vs. New Solution Rate", - xlabel="Generation", - ylabel="New Solution Rate", - linewidth=3, - font_size=14, - plot_type="plot", - color="#3870FF", - save_dir=None): - - """ - Creates, shows, and returns a figure that summarizes the rate of exploring new solutions. This method works only when save_solutions=True in the constructor of the pygad.GA class. - - Accepts the following: - title: Figure title. - xlabel: Label on the X-axis. - ylabel: Label on the Y-axis. - linewidth: Line width of the plot. Defaults to 3. - font_size: Font size for the labels and title. Defaults to 14. - plot_type: Type of the plot which can be either "plot" (default), "scatter", or "bar". - color: Color of the plot which defaults to "#3870FF". - save_dir: Directory to save the figure. - - Returns the figure. - """ - - if self.generations_completed < 1: - raise RuntimeError("The plot_new_solution_rate() method can only be called after completing at least 1 generation but ({generations_completed}) is completed.".format(generations_completed=self.generations_completed)) - - if self.save_solutions == False: - raise RuntimeError("The plot_new_solution_rate() method works only when save_solutions=True in the constructor of the pygad.GA class.") - - unique_solutions = set() - num_unique_solutions_per_generation = [] - for generation_idx in range(self.generations_completed): - - len_before = len(unique_solutions) - - start = generation_idx * self.sol_per_pop - end = start + self.sol_per_pop - - for sol in self.solutions[start:end]: - unique_solutions.add(tuple(sol)) - - len_after = len(unique_solutions) - - generation_num_unique_solutions = len_after - len_before - num_unique_solutions_per_generation.append(generation_num_unique_solutions) - - fig = matplotlib.pyplot.figure() - if plot_type == "plot": - matplotlib.pyplot.plot(num_unique_solutions_per_generation, linewidth=linewidth, color=color) - elif plot_type == "scatter": - matplotlib.pyplot.scatter(range(self.generations_completed), num_unique_solutions_per_generation, linewidth=linewidth, color=color) - elif plot_type == "bar": - matplotlib.pyplot.bar(range(self.generations_completed), num_unique_solutions_per_generation, linewidth=linewidth, color=color) - matplotlib.pyplot.title(title, fontsize=font_size) - matplotlib.pyplot.xlabel(xlabel, fontsize=font_size) - matplotlib.pyplot.ylabel(ylabel, fontsize=font_size) - - if not save_dir is None: - matplotlib.pyplot.savefig(fname=save_dir, - bbox_inches='tight') - matplotlib.pyplot.show() - - return fig - - def plot_genes(self, - title="PyGAD - Gene", - xlabel="Gene", - ylabel="Value", - linewidth=3, - font_size=14, - plot_type="plot", - graph_type="plot", - fill_color="#3870FF", - color="black", - solutions="all", - save_dir=None): - - """ - Creates, shows, and returns a figure with number of subplots equal to the number of genes. Each subplot shows the gene value for each generation. - This method works only when save_solutions=True in the constructor of the pygad.GA class. - It also works only after completing at least 1 generation. If no generation is completed, an exception is raised. - - Accepts the following: - title: Figure title. - xlabel: Label on the X-axis. - ylabel: Label on the Y-axis. - linewidth: Line width of the plot. Defaults to 3. - font_size: Font size for the labels and title. Defaults to 14. - plot_type: Type of the plot which can be either "plot" (default), "scatter", or "bar". - graph_type: Type of the graph which can be either "plot" (default), "boxplot", or "histogram". - fill_color: Fill color of the graph which defaults to "#3870FF". This has no effect if graph_type="plot". - color: Color of the plot which defaults to "black". - solutions: Defaults to "all" which means use all solutions. If "best" then only the best solutions are used. - save_dir: Directory to save the figure. - - Returns the figure. - """ - - if self.generations_completed < 1: - raise RuntimeError("The plot_genes() method can only be called after completing at least 1 generation but ({generations_completed}) is completed.".format(generations_completed=self.generations_completed)) - - if type(solutions) is str: - if solutions == 'all': - if self.save_solutions: - solutions_to_plot = self.solutions - else: - raise RuntimeError("The plot_genes() method with solutions='all' can only be called if 'save_solutions=True' in the pygad.GA class constructor.") - elif solutions == 'best': - if self.save_best_solutions: - solutions_to_plot = self.best_solutions - else: - raise RuntimeError("The plot_genes() method with solutions='best' can only be called if 'save_best_solutions=True' in the pygad.GA class constructor.") - else: - raise RuntimeError("The solutions parameter can be either 'all' or 'best' but {solutions} found.".format(solutions=solutions)) - else: - raise RuntimeError("The solutions parameter must be a string but {solutions_type} found.".format(solutions_type=type(solutions))) - - if graph_type == "plot": - # num_rows will be always be >= 1 - # num_cols can only be 0 if num_genes=1 - num_rows = int(numpy.ceil(self.num_genes/5.0)) - num_cols = int(numpy.ceil(self.num_genes/num_rows)) - - if num_cols == 0: - figsize = (10, 8) - # There is only a single gene - fig, ax = matplotlib.pyplot.subplots(num_rows, figsize=figsize) - if plot_type == "plot": - ax.plot(solutions_to_plot[:, 0], linewidth=linewidth, color=fill_color) - elif plot_type == "scatter": - ax.scatter(range(self.generations_completed + 1), solutions_to_plot[:, 0], linewidth=linewidth, color=fill_color) - elif plot_type == "bar": - ax.bar(range(self.generations_completed + 1), solutions_to_plot[:, 0], linewidth=linewidth, color=fill_color) - ax.set_xlabel(0, fontsize=font_size) - else: - fig, axs = matplotlib.pyplot.subplots(num_rows, num_cols) - - if num_cols == 1 and num_rows == 1: - fig.set_figwidth(5 * num_cols) - fig.set_figheight(4) - axs.plot(solutions_to_plot[:, 0], linewidth=linewidth, color=fill_color) - axs.set_xlabel("Gene " + str(0), fontsize=font_size) - elif num_cols == 1 or num_rows == 1: - fig.set_figwidth(5 * num_cols) - fig.set_figheight(4) - for gene_idx in range(len(axs)): - if plot_type == "plot": - axs[gene_idx].plot(solutions_to_plot[:, gene_idx], linewidth=linewidth, color=fill_color) - elif plot_type == "scatter": - axs[gene_idx].scatter(range(solutions_to_plot.shape[0]), solutions_to_plot[:, gene_idx], linewidth=linewidth, color=fill_color) - elif plot_type == "bar": - axs[gene_idx].bar(range(solutions_to_plot.shape[0]), solutions_to_plot[:, gene_idx], linewidth=linewidth, color=fill_color) - axs[gene_idx].set_xlabel("Gene " + str(gene_idx), fontsize=font_size) - else: - gene_idx = 0 - fig.set_figwidth(25) - fig.set_figheight(4*num_rows) - for row_idx in range(num_rows): - for col_idx in range(num_cols): - if gene_idx >= self.num_genes: - # axs[row_idx, col_idx].remove() - break - if plot_type == "plot": - axs[row_idx, col_idx].plot(solutions_to_plot[:, gene_idx], linewidth=linewidth, color=fill_color) - elif plot_type == "scatter": - axs[row_idx, col_idx].scatter(range(solutions_to_plot.shape[0]), solutions_to_plot[:, gene_idx], linewidth=linewidth, color=fill_color) - elif plot_type == "bar": - axs[row_idx, col_idx].bar(range(solutions_to_plot.shape[0]), solutions_to_plot[:, gene_idx], linewidth=linewidth, color=fill_color) - axs[row_idx, col_idx].set_xlabel("Gene " + str(gene_idx), fontsize=font_size) - gene_idx += 1 - - fig.suptitle(title, fontsize=font_size, y=1.001) - matplotlib.pyplot.tight_layout() - - elif graph_type == "boxplot": - fig = matplotlib.pyplot.figure(1, figsize=(0.7*self.num_genes, 6)) - - # Create an axes instance - ax = fig.add_subplot(111) - boxeplots = ax.boxplot(solutions_to_plot, - labels=range(self.num_genes), - patch_artist=True) - # adding horizontal grid lines - ax.yaxis.grid(True) - - for box in boxeplots['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']: - whisker.set(color=color, linewidth=linewidth) - for median in boxeplots['medians']: - median.set(color=color, linewidth=linewidth) - for cap in boxeplots['caps']: - cap.set(color=color, linewidth=linewidth) - - matplotlib.pyplot.title(title, fontsize=font_size) - matplotlib.pyplot.xlabel(xlabel, fontsize=font_size) - matplotlib.pyplot.ylabel(ylabel, fontsize=font_size) - matplotlib.pyplot.tight_layout() - - elif graph_type == "histogram": - # num_rows will be always be >= 1 - # num_cols can only be 0 if num_genes=1 - num_rows = int(numpy.ceil(self.num_genes/5.0)) - num_cols = int(numpy.ceil(self.num_genes/num_rows)) - - if num_cols == 0: - figsize = (10, 8) - # There is only a single gene - fig, ax = matplotlib.pyplot.subplots(num_rows, - figsize=figsize) - ax.hist(solutions_to_plot[:, 0], color=fill_color) - ax.set_xlabel(0, fontsize=font_size) - else: - fig, axs = matplotlib.pyplot.subplots(num_rows, num_cols) - - if num_cols == 1 and num_rows == 1: - fig.set_figwidth(4 * num_cols) - fig.set_figheight(3) - axs.hist(solutions_to_plot[:, 0], - color=fill_color, - rwidth=0.95) - axs.set_xlabel("Gene " + str(0), fontsize=font_size) - elif num_cols == 1 or num_rows == 1: - fig.set_figwidth(4 * num_cols) - fig.set_figheight(3) - for gene_idx in range(len(axs)): - axs[gene_idx].hist(solutions_to_plot[:, gene_idx], - color=fill_color, - rwidth=0.95) - axs[gene_idx].set_xlabel("Gene " + str(gene_idx), fontsize=font_size) - else: - gene_idx = 0 - fig.set_figwidth(20) - fig.set_figheight(3*num_rows) - for row_idx in range(num_rows): - for col_idx in range(num_cols): - if gene_idx >= self.num_genes: - # axs[row_idx, col_idx].remove() - break - axs[row_idx, col_idx].hist(solutions_to_plot[:, gene_idx], - color=fill_color, - rwidth=0.95) - axs[row_idx, col_idx].set_xlabel("Gene " + str(gene_idx), fontsize=font_size) - gene_idx += 1 - - fig.suptitle(title, fontsize=font_size, y=1.001) - matplotlib.pyplot.tight_layout() - - if not save_dir is None: - matplotlib.pyplot.savefig(fname=save_dir, - bbox_inches='tight') - - matplotlib.pyplot.show() - - return fig - - def save(self, filename): - - """ - Saves the genetic algorithm instance: - -filename: Name of the file to save the instance. No extension is needed. - """ - - with open(filename + ".pkl", 'wb') as file: - pickle.dump(self, file) - -def load(filename): - - """ - Reads a saved instance of the genetic algorithm: - -filename: Name of the file to read the instance. No extension is needed. - Returns the genetic algorithm instance. - """ - - try: - with open(filename + ".pkl", 'rb') as file: - ga_in = pickle.load(file) - except FileNotFoundError: - raise FileNotFoundError("Error reading the file {filename}. Please check your inputs.".format(filename=filename)) - except: - raise BaseException("Error loading the file. If the file already exists, please reload all the functions previously used (e.g. fitness function).") - return ga_in \ No newline at end of file diff --git a/__init__.py b/pygad/__init__.py similarity index 64% rename from __init__.py rename to pygad/__init__.py index fcc9653c..17959b7d 100644 --- a/__init__.py +++ b/pygad/__init__.py @@ -1,3 +1,3 @@ from .pygad import * # Relative import. -__version__ = "2.16.3" +__version__ = "3.3.1" diff --git a/pygad/cnn/__init__.py b/pygad/cnn/__init__.py new file mode 100644 index 00000000..da317b5b --- /dev/null +++ b/pygad/cnn/__init__.py @@ -0,0 +1,4 @@ +from .cnn import * + +__version__ = "1.1.0" + diff --git a/pygad/cnn/cnn.py b/pygad/cnn/cnn.py new file mode 100644 index 00000000..1bd90330 --- /dev/null +++ b/pygad/cnn/cnn.py @@ -0,0 +1,1059 @@ +import numpy +import functools +import logging + +""" +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 +""" + +# 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): + + """ + Creates a list holding the weights of all layers in the CNN. + + model: A reference to the instance from the cnn.Model class. + initial: When True, the function returns the initial weights of the layers. When False, the trained weights of the layers are returned. The initial weights are only needed before network training starts. The trained weights are needed to predict the network outputs. + + Returns a list (network_weights) holding the weights of the layers in the CNN. + """ + + network_weights = [] + + layer = model.last_layer + while "previous_layer" in layer.__init__.__code__.co_varnames: + if type(layer) in [Conv2D, Dense]: + # If the 'initial' parameter is True, append the initial weights. Otherwise, append the trained weights. + if initial == True: + network_weights.append(layer.initial_weights) + elif initial == False: + network_weights.append(layer.trained_weights) + else: + msg = f"Unexpected value to the 'initial' parameter: {initial}." + model.logger.error(msg) + raise ValueError(msg) + + # Go to the previous layer. + layer = layer.previous_layer + + # If the first layer in the network is not an input layer (i.e. an instance of the Input2D class), raise an error. + if not (type(layer) is Input2D): + msg = "The first layer in the network architecture must be an input layer." + model.logger.error(msg) + 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). + network_weights.reverse() + return numpy.array(network_weights) + +def layers_weights_as_matrix(model, vector_weights): + + """ + Converts the network weights from vectors to matrices. + + model: A reference to the instance from the cnn.Model class. + vector_weights: The network weights as vectors where the weights of each layer form a single vector. + + Returns a list (network_weights) holding the weights of the CNN layers as matrices. + """ + + network_weights = [] + + start = 0 + layer = model.last_layer + vector_weights = vector_weights[::-1] + while "previous_layer" in layer.__init__.__code__.co_varnames: + if type(layer) in [Conv2D, Dense]: + layer_weights_shape = layer.initial_weights.shape + 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 = numpy.reshape(weights_vector, newshape=(layer_weights_shape)) + network_weights.append(matrix) + + start = start + layer_weights_size + + # Go to the previous layer. + layer = layer.previous_layer + + # If the first layer in the network is not an input layer (i.e. an instance of the Input2D class), raise an error. + if not (type(layer) is Input2D): + msg = "The first layer in the network architecture must be an input layer." + model.logger.error(msg) + 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). + network_weights.reverse() + return numpy.array(network_weights) + +def layers_weights_as_vector(model, initial=True): + + """ + Creates a list holding the weights of each layer (Conv and Dense) in the CNN as a vector. + + model: A reference to the instance from the cnn.Model class. + initial: When True, the function returns the initial weights of the CNN. When False, the trained weights of the CNN layers are returned. The initial weights are only needed before network training starts. The trained weights are needed to predict the network outputs. + + Returns a list (network_weights) holding the weights of the CNN layers as a vector. + """ + + network_weights = [] + + layer = model.last_layer + while "previous_layer" in layer.__init__.__code__.co_varnames: + if type(layer) in [Conv2D, Dense]: + # If the 'initial' parameter is True, append the initial weights. Otherwise, append the trained weights. + if initial == True: + vector = numpy.reshape(layer.initial_weights, newshape=(layer.initial_weights.size)) + # vector = pygad.nn.DenseLayer.to_vector(matrix=layer.initial_weights) + network_weights.extend(vector) + elif initial == False: + vector = numpy.reshape(layer.trained_weights, newshape=(layer.trained_weights.size)) + # vector = pygad.nn.DenseLayer.to_vector(array=layer.trained_weights) + network_weights.extend(vector) + else: + msg = f"Unexpected value to the 'initial' parameter: {initial}." + model.logger.error(msg) + raise ValueError(msg) + + # Go to the previous layer. + layer = layer.previous_layer + + # If the first layer in the network is not an input layer (i.e. an instance of the Input2D class), raise an error. + if not (type(layer) is Input2D): + msg = "The first layer in the network architecture must be an input layer." + model.logger.error(msg) + 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). + network_weights.reverse() + return numpy.array(network_weights) + +def update_layers_trained_weights(model, final_weights): + + """ + After the network weights are trained, the 'trained_weights' attribute of each layer is updated 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. + + model: A reference to the instance from the cnn.Model class. + final_weights: An array of layers weights as matrices after passing through all the epochs. + """ + + layer = model.last_layer + layer_idx = len(final_weights) - 1 + while "previous_layer" in layer.__init__.__code__.co_varnames: + if type(layer) in [Conv2D, Dense]: + layer.trained_weights = final_weights[layer_idx] + + layer_idx = layer_idx - 1 + + # Go to the previous layer. + layer = layer.previous_layer + + +class CustomLogger: + + def __init__(self): + # Create a logger named with the module name. + logger = logging.getLogger(__name__) + # Set the logger log level to 'DEBUG' to log all kinds of messages. + logger.setLevel(logging.DEBUG) + + # Clear any attached handlers to the logger from the previous runs. + # If the handlers are not cleared, then the new handler will be appended to the list of handlers. + # This makes the single log message be repeated according to the length of the list of handlers. + logger.handlers.clear() + + # Create the handlers. + stream_handler = logging.StreamHandler() + # Set the handler log level to 'DEBUG' to log all kinds of messages received from the logger. + stream_handler.setLevel(logging.DEBUG) + + # Create the formatter that just includes the log message. + formatter = logging.Formatter('%(message)s') + + # Add the formatter to the handler. + stream_handler.setFormatter(formatter) + + # Add the handler to the logger. + logger.addHandler(stream_handler) + + # Create the 'self.logger' attribute to hold the logger. + # Instead of using 'print()', use 'self.logger.info()' + self.logger = logger + +class Input2D(CustomLogger): + + """ + Implementing the input layer of a CNN. + The CNN architecture must start with an input layer. + """ + + def __init__(self, + input_shape, + logger=None): + + """ + input_shape: Shape of the input sample to the CNN. + """ + + super().__init__() + + # If logger is None, then the CustomLogger.logger is created. + if logger is None: + pass + else: + self.logger = logger + + # If the input sample has less than 2 dimensions, then an exception is raised. + if len(input_shape) < 2: + msg = f"The Input2D class creates an input layer for data inputs with at least 2 dimensions but ({len(input_shape)}) dimensions found." + self.logger.error(msg) + raise ValueError(msg) + # If the input sample has exactly 2 dimensions, the third dimension is set to 1. + elif len(input_shape) == 2: + input_shape = (input_shape[0], input_shape[1], 1) + + for dim_idx, dim in enumerate(input_shape): + if dim <= 0: + msg = "The dimension size of the inputs cannot be <= 0. Please pass a valid value to the 'input_size' parameter." + self.logger.error(msg) + raise ValueError(msg) + + self.input_shape = input_shape # Shape of the input sample. + self.layer_output_size = input_shape # Shape of the output from the current layer. For an input layer, it is the same as the shape of the input sample. + +class Conv2D(CustomLogger): + + """ + Implementing the convolution layer. + """ + + def __init__(self, + num_filters, + kernel_size, + previous_layer, + activation_function=None, + logger=None): + + """ + num_filters: Number of filters in the convolution layer. + kernel_size: Kernel size of the filter. + previous_layer: A reference to the previous layer. + activation_function=None: The name of the activation function to be used in the conv layer. If None, then no activation function is applied besides the convolution operation. The activation function can be applied by a separate layer. + """ + + super().__init__() + + # If logger is None, then the CustomLogger.logger is created. + if logger is None: + pass + else: + self.logger = logger + + if num_filters <= 0: + msg = "Number of filters cannot be <= 0. Please pass a valid value to the 'num_filters' parameter." + self.logger.error(msg) + raise ValueError(msg) + # Number of filters in the conv layer. + self.num_filters = num_filters + + if kernel_size <= 0: + msg = "The kernel size cannot be <= 0. Please pass a valid value to the 'kernel_size' parameter." + self.logger.error(msg) + raise ValueError(msg) + # Kernel size of each filter. + self.kernel_size = kernel_size + + # Validating the activation function + if (activation_function is None): + self.activation = None + elif (activation_function == "relu"): + self.activation = relu + elif (activation_function == "sigmoid"): + self.activation = sigmoid + elif (activation_function == "softmax"): + msg = "The softmax activation function cannot be used in a conv layer." + self.logger.error(msg) + raise ValueError(msg) + else: + msg = f"The specified activation function '{activation_function}' is not among the supported activation functions {supported_activation_functions}. Please use one of the supported functions." + self.logger.error(msg) + raise ValueError(msg) + + # The activation function used in the current layer. + self.activation_function = activation_function + + if previous_layer is None: + 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. + self.previous_layer = previous_layer + + # A reference to the bank of filters. + self.filter_bank_size = (self.num_filters, + self.kernel_size, + self.kernel_size, + self.previous_layer.layer_output_size[-1]) + + # Initializing the filters of the conv layer. + self.initial_weights = numpy.random.uniform(low=-0.1, + high=0.1, + size=self.filter_bank_size) + + # The trained filters of the conv layer. Only assigned a value after the network is trained (i.e. the train_network() function completes). + # Just initialized to be equal to the initial filters + self.trained_weights = self.initial_weights.copy() + + # Size of the input to the layer. + self.layer_input_size = self.previous_layer.layer_output_size + + # Size of the output from the layer. + # Later, it must conider strides and paddings + 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) + + # The layer_output attribute holds the latest output from the layer. + self.layer_output = None + + def conv_(self, input2D, conv_filter): + + """ + Convolves the input (input2D) by a single filter (conv_filter). + + input2D: The input to be convolved by a single filter. + conv_filter: The filter convolving the input. + + Returns the result of convolution. + """ + + result = numpy.zeros(shape=(input2D.shape[0], input2D.shape[1], conv_filter.shape[0])) + # Looping through the image to apply the convolution operation. + for r in numpy.uint16(numpy.arange(self.filter_bank_size[1]/2.0, + input2D.shape[0]-self.filter_bank_size[1]/2.0+1)): + for c in numpy.uint16(numpy.arange(self.filter_bank_size[1]/2.0, + input2D.shape[1]-self.filter_bank_size[1]/2.0+1)): + """ + Getting the current region to get multiplied with the filter. + How to loop through the image and get the region based on + the image and filer sizes is the most tricky part of convolution. + """ + if len(input2D.shape) == 2: + curr_region = input2D[r-numpy.uint16(numpy.floor(self.filter_bank_size[1]/2.0)):r+numpy.uint16(numpy.ceil(self.filter_bank_size[1]/2.0)), + c-numpy.uint16(numpy.floor(self.filter_bank_size[1]/2.0)):c+numpy.uint16(numpy.ceil(self.filter_bank_size[1]/2.0))] + else: + curr_region = input2D[r-numpy.uint16(numpy.floor(self.filter_bank_size[1]/2.0)):r+numpy.uint16(numpy.ceil(self.filter_bank_size[1]/2.0)), + c-numpy.uint16(numpy.floor(self.filter_bank_size[1]/2.0)):c+numpy.uint16(numpy.ceil(self.filter_bank_size[1]/2.0)), :] + # Element-wise multipliplication between the current region and the filter. + + for filter_idx in range(conv_filter.shape[0]): + curr_result = curr_region * conv_filter[filter_idx] + conv_sum = numpy.sum(curr_result) # Summing the result of multiplication. + + if self.activation is None: + result[r, c, filter_idx] = conv_sum # Saving the SOP in the convolution layer feature map. + else: + result[r, c, filter_idx] = self.activation(conv_sum) # Saving the activation function result in the convolution layer feature map. + + # Clipping the outliers of the result matrix. + final_result = result[numpy.uint16(self.filter_bank_size[1]/2.0):result.shape[0]-numpy.uint16(self.filter_bank_size[1]/2.0), + numpy.uint16(self.filter_bank_size[1]/2.0):result.shape[1]-numpy.uint16(self.filter_bank_size[1]/2.0), :] + return final_result + + def conv(self, input2D): + + """ + Convolves the input (input2D) by a filter bank. + + input2D: The input to be convolved by the filter bank. + + The conv() method saves the result of convolving the input by the filter bank in the layer_output attribute. + """ + + if len(input2D.shape) != len(self.initial_weights.shape) - 1: # Check if there is a match in the number of dimensions between the image and the filters. + msg = "Number of dimensions in the conv filter and the input do not match." + self.logger.error(msg) + raise ValueError(msg) + if len(input2D.shape) > 2 or len(self.initial_weights.shape) > 3: # Check if number of image channels matches the filter depth. + if input2D.shape[-1] != self.initial_weights.shape[-1]: + msg = "Number of channels in both the input and the filter must match." + self.logger.error(msg) + raise ValueError(msg) + if self.initial_weights.shape[1] != self.initial_weights.shape[2]: # Check if filter dimensions are equal. + 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. + 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) + + self.layer_output = self.conv_(input2D, self.trained_weights) + +class AveragePooling2D(CustomLogger): + + """ + Implementing the average pooling layer. + """ + + def __init__(self, + pool_size, + previous_layer, + stride=2, + logger=None): + + """ + pool_size: Pool size. + previous_layer: Reference to the previous layer in the CNN architecture. + stride=2: Stride + """ + + super().__init__() + + # If logger is None, then the CustomLogger.logger is created. + if logger is None: + pass + else: + self.logger = logger + + if not (type(pool_size) is int): + msg = "The expected type of the pool_size is int but {pool_size_type} found.".format(pool_size_type=type(pool_size)) + self.logger.error(msg) + raise ValueError(msg) + + if pool_size <= 0: + msg = "The passed value to the pool_size parameter cannot be <= 0." + self.logger.error(msg) + raise ValueError(msg) + self.pool_size = pool_size + + if stride <= 0: + msg = "The passed value to the stride parameter cannot be <= 0." + self.logger.error(msg) + raise ValueError(msg) + self.stride = stride + + if previous_layer is None: + 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. + self.previous_layer = previous_layer + + # Size of the input to the layer. + self.layer_input_size = self.previous_layer.layer_output_size + + # Size of the output from the layer. + self.layer_output_size = (numpy.uint16((self.previous_layer.layer_output_size[0] - self.pool_size + 1)/stride + 1), + numpy.uint16((self.previous_layer.layer_output_size[1] - self.pool_size + 1)/stride + 1), + self.previous_layer.layer_output_size[-1]) + + # The layer_output attribute holds the latest output from the layer. + self.layer_output = None + + def average_pooling(self, input2D): + + """ + Applies the average pooling operation. + + input2D: The input to which the average pooling operation is applied. + + The average_pooling() method saves its result in the layer_output attribute. + """ + + # Preparing the output of the pooling operation. + pool_out = numpy.zeros((numpy.uint16((input2D.shape[0]-self.pool_size+1)/self.stride+1), + numpy.uint16((input2D.shape[1]-self.pool_size+1)/self.stride+1), + input2D.shape[-1])) + for map_num in range(input2D.shape[-1]): + r2 = 0 + for r in numpy.arange(0,input2D.shape[0]-self.pool_size+1, self.stride): + c2 = 0 + for c in numpy.arange(0, input2D.shape[1]-self.pool_size+1, self.stride): + pool_out[r2, c2, map_num] = numpy.mean([input2D[r:r+self.pool_size, c:c+self.pool_size, map_num]]) + c2 = c2 + 1 + r2 = r2 +1 + + self.layer_output = pool_out + +class MaxPooling2D(CustomLogger): + + """ + Similar to the AveragePooling2D class except that it implements max pooling. + """ + + def __init__(self, + pool_size, + previous_layer, + stride=2, + logger=None): + + """ + pool_size: Pool size. + previous_layer: Reference to the previous layer in the CNN architecture. + stride=2: Stride + """ + + super().__init__() + + # If logger is None, then the CustomLogger.logger is created. + if logger is None: + pass + else: + self.logger = logger + + if not (type(pool_size) is int): + msg = f"The expected type of the pool_size is int but {type(pool_size)} found." + self.logger.error(msg) + raise ValueError(msg) + + if pool_size <= 0: + msg = "The passed value to the pool_size parameter cannot be <= 0." + self.logger.error(msg) + raise ValueError(msg) + self.pool_size = pool_size + + if stride <= 0: + msg = "The passed value to the stride parameter cannot be <= 0." + self.logger.error(msg) + raise ValueError(msg) + self.stride = stride + + if previous_layer is None: + 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. + self.previous_layer = previous_layer + + # Size of the input to the layer. + self.layer_input_size = self.previous_layer.layer_output_size + + # Size of the output from the layer. + self.layer_output_size = (numpy.uint16((self.previous_layer.layer_output_size[0] - self.pool_size + 1)/stride + 1), + numpy.uint16((self.previous_layer.layer_output_size[1] - self.pool_size + 1)/stride + 1), + self.previous_layer.layer_output_size[-1]) + + # The layer_output attribute holds the latest output from the layer. + self.layer_output = None + + def max_pooling(self, input2D): + + """ + Applies the max pooling operation. + + input2D: The input to which the max pooling operation is applied. + + The max_pooling() method saves its result in the layer_output attribute. + """ + + # Preparing the output of the pooling operation. + pool_out = numpy.zeros((numpy.uint16((input2D.shape[0]-self.pool_size+1)/self.stride+1), + numpy.uint16((input2D.shape[1]-self.pool_size+1)/self.stride+1), + input2D.shape[-1])) + for map_num in range(input2D.shape[-1]): + r2 = 0 + for r in numpy.arange(0,input2D.shape[0]-self.pool_size+1, self.stride): + c2 = 0 + for c in numpy.arange(0, input2D.shape[1]-self.pool_size+1, self.stride): + pool_out[r2, c2, map_num] = numpy.max([input2D[r:r+self.pool_size, c:c+self.pool_size, map_num]]) + c2 = c2 + 1 + r2 = r2 +1 + + self.layer_output = pool_out + +class ReLU(CustomLogger): + + """ + Implementing the ReLU layer. + """ + + def __init__(self, + previous_layer, + logger=None): + + """ + previous_layer: Reference to the previous layer. + """ + + super().__init__() + + # If logger is None, then the CustomLogger.logger is created. + if logger is None: + pass + else: + self.logger = logger + + if previous_layer is None: + 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. + self.previous_layer = previous_layer + + # Size of the input to the layer. + self.layer_input_size = self.previous_layer.layer_output_size + + # Size of the output from the layer. + self.layer_output_size = self.previous_layer.layer_output_size + + # The layer_output attribute holds the latest output from the layer. + self.layer_output = None + + def relu_layer(self, layer_input): + + """ + Applies the ReLU function over all elements in input to the ReLU layer. + + layer_input: The input to which the ReLU function is applied. + + The relu_layer() method saves its result in the layer_output attribute. + """ + + self.layer_output_size = layer_input.size + self.layer_output = relu(layer_input) + +class Sigmoid(CustomLogger): + + """ + Implementing the sigmoid layer. + """ + + def __init__(self, + previous_layer, + logger=None): + + """ + previous_layer: Reference to the previous layer. + """ + + super().__init__() + + # If logger is None, then the CustomLogger.logger is created. + if logger is None: + pass + else: + self.logger = logger + + if previous_layer is None: + 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. + self.previous_layer = previous_layer + + # Size of the input to the layer. + self.layer_input_size = self.previous_layer.layer_output_size + + # Size of the output from the layer. + self.layer_output_size = self.previous_layer.layer_output_size + + # The layer_output attribute holds the latest output from the layer. + self.layer_output = None + + def sigmoid_layer(self, layer_input): + + """ + Applies the sigmoid function over all elements in input to the sigmoid layer. + + layer_input: The input to which the sigmoid function is applied. + + The sigmoid_layer() method saves its result in the layer_output attribute. + """ + + self.layer_output_size = layer_input.size + self.layer_output = sigmoid(layer_input) + +class Flatten(CustomLogger): + + """ + Implementing the flatten layer. + """ + + def __init__(self, + previous_layer, + logger=None): + + """ + previous_layer: Reference to the previous layer. + """ + + super().__init__() + + # If logger is None, then the CustomLogger.logger is created. + if logger is None: + pass + else: + self.logger = logger + + if previous_layer is None: + 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. + self.previous_layer = previous_layer + + # Size of the input to the layer. + self.layer_input_size = self.previous_layer.layer_output_size + + # Size of the output from the layer. + self.layer_output_size = functools.reduce(lambda x, y: x*y, self.previous_layer.layer_output_size) + + # The layer_output attribute holds the latest output from the layer. + self.layer_output = None + + def flatten(self, input2D): + + """ + Reshapes the input into a 1D vector. + + input2D: The input to the Flatten layer that will be converted into a 1D vector. + + The flatten() method saves its result in the layer_output attribute. + """ + + self.layer_output_size = input2D.size + self.layer_output = numpy.ravel(input2D) + +class Dense(CustomLogger): + + """ + Implementing the input dense (fully connected) layer of a CNN. + """ + + def __init__(self, + num_neurons, + previous_layer, + activation_function="relu", + logger=None): + + """ + num_neurons: Number of neurons in the dense layer. + previous_layer: Reference to the previous layer. + activation_function: Name of the activation function to be used in the current layer. + logger=None: Reference to the instance of the logging.Logger class. + """ + + super().__init__() + + # If logger is None, then the CustomLogger.logger is created. + if logger is None: + pass + else: + self.logger = logger + + if num_neurons <= 0: + msg = "Number of neurons cannot be <= 0. Please pass a valid value to the 'num_neurons' parameter." + self.logger.error(msg) + raise ValueError(msg) + + # Number of neurons in the dense layer. + self.num_neurons = num_neurons + + # Validating the activation function + if (activation_function == "relu"): + self.activation = relu + elif (activation_function == "sigmoid"): + self.activation = sigmoid + elif (activation_function == "softmax"): + self.activation = softmax + else: + msg = f"The specified activation function '{activation_function}' is not among the supported activation functions {supported_activation_functions}. Please use one of the supported functions." + self.logger.error(msg) + raise ValueError(msg) + + self.activation_function = activation_function + + if previous_layer is None: + 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. + 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: + msg = f"The input to the dense layer must be of type int but {type(self.previous_layer.layer_output_size)} found." + self.logger.error(msg) + raise ValueError(msg) + # Initializing the weights of the layer. + self.initial_weights = numpy.random.uniform(low=-0.1, + high=0.1, + size=(self.previous_layer.layer_output_size, self.num_neurons)) + + # The trained weights of the layer. Only assigned a value after the network is trained (i.e. the train_network() function completes). + # Just initialized to be equal to the initial weights + self.trained_weights = self.initial_weights.copy() + + # Size of the input to the layer. + self.layer_input_size = self.previous_layer.layer_output_size + + # Size of the output from the layer. + self.layer_output_size = num_neurons + + # The layer_output attribute holds the latest output from the layer. + self.layer_output = None + + def dense_layer(self, layer_input): + + """ + Calculates the output of the dense layer. + + layer_input: The input to the dense layer + + The dense_layer() method saves its result in the layer_output attribute. + """ + + if self.trained_weights is None: + msg = "The weights of the dense layer cannot be of Type 'None'." + self.logger.error(msg) + raise TypeError(msg) + + sop = numpy.matmul(layer_input, self.trained_weights) + + self.layer_output = self.activation(sop) + +class Model(CustomLogger): + + """ + Creating a CNN model. + """ + + def __init__(self, + last_layer, + epochs=10, + learning_rate=0.01, + logger=None): + + """ + last_layer: A reference to the last layer in the CNN architecture. + epochs=10: Number of epochs. + learning_rate=0.01: Learning rate. + logger=None: Reference to the instance of the logging.Logger class. + """ + + super().__init__() + + # If logger is None, then the CustomLogger.logger is created. + if logger is None: + pass + else: + self.logger = logger + + self.last_layer = last_layer + self.epochs = epochs + self.learning_rate = learning_rate + + # The network_layers attribute is a list holding references to all CNN layers. + self.network_layers = self.get_layers() + + def get_layers(self): + + """ + Prepares a list of all layers in the CNN model. + Returns the list. + """ + + network_layers = [] + + # The last layer in the network archietcture. + layer = self.last_layer + + while "previous_layer" in layer.__init__.__code__.co_varnames: + network_layers.insert(0, layer) + layer = layer.previous_layer + + return network_layers + + def train(self, train_inputs, train_outputs): + + """ + Trains the CNN model. + It is important to note that no learning algorithm is used for training the CNN. Just the learning rate is used for making some changes which is better than leaving the weights unchanged. + + train_inputs: Training data inputs. + train_outputs: Training data outputs. + """ + + if (train_inputs.ndim != 4): + msg = f"The training data input has {train_inputs.ndim} but it must have 4 dimensions. The first dimension is the number of training samples, the second & third dimensions represent the width and height of the sample, and the fourth dimension represents the number of channels in the sample." + self.logger.error(msg) + raise ValueError(msg) + + if (train_inputs.shape[0] != len(train_outputs)): + msg = f"Mismatch between the number of input samples and number of labels: {train_inputs.shape[0]} != {len(train_outputs)}." + self.logger.error(msg) + raise ValueError(msg) + + network_predictions = [] + network_error = 0 + + for epoch in range(self.epochs): + self.logger.info(f"Epoch {epoch}") + for sample_idx in range(train_inputs.shape[0]): + # print("Sample {sample_idx}".format(sample_idx=sample_idx)) + self.feed_sample(train_inputs[sample_idx, :]) + + try: + predicted_label = numpy.where(numpy.max(self.last_layer.layer_output) == self.last_layer.layer_output)[0][0] + except IndexError: + self.logger.info(self.last_layer.layer_output) + msg = "Index out of range" + self.logger.error(msg) + raise IndexError(msg) + network_predictions.append(predicted_label) + + network_error = network_error + abs(predicted_label - train_outputs[sample_idx]) + + self.update_weights(network_error) + + def feed_sample(self, sample): + + """ + Feeds a sample in the CNN layers. + + sample: The samples to be fed to the CNN layers. + + Returns results of the last layer in the CNN. + """ + + last_layer_outputs = sample + for layer in self.network_layers: + if type(layer) is Conv2D: +# import time +# time1 = time.time() + layer.conv(input2D=last_layer_outputs) +# time2 = time.time() +# print(time2 - time1) + elif type(layer) is Dense: + layer.dense_layer(layer_input=last_layer_outputs) + elif type(layer) is MaxPooling2D: + layer.max_pooling(input2D=last_layer_outputs) + elif type(layer) is AveragePooling2D: + layer.average_pooling(input2D=last_layer_outputs) + elif type(layer) is ReLU: + layer.relu_layer(layer_input=last_layer_outputs) + elif type(layer) is Sigmoid: + layer.sigmoid_layer(layer_input=last_layer_outputs) + elif type(layer) is Flatten: + layer.flatten(input2D=last_layer_outputs) + elif type(layer) is Input2D: + pass + else: + # self.logger.info("Other") + msg = "The layer of type {type(layer)} is not supported." + self.logger.error(msg) + raise TypeError(msg) + + last_layer_outputs = layer.layer_output + return self.network_layers[-1].layer_output + + def update_weights(self, network_error): + + """ + Updates the weights of the CNN. + It is important to note that no learning algorithm is used for training the CNN. Just the learning rate is used for making some changes which is better than leaving the weights unchanged. + + This method loops through the layers and updates their weights. + + network_error: The network error in the last epoch. + """ + + for layer in self.network_layers: + if "trained_weights" in vars(layer).keys(): + layer.trained_weights = layer.trained_weights - network_error * self.learning_rate * layer.trained_weights + + def predict(self, data_inputs): + + """ + Uses the trained CNN for making predictions. + + data_inputs: The inputs to predict their label. + + Returns a list holding the samples predictions. + """ + + if (data_inputs.ndim != 4): + msg = "The data input has {data_inputs.ndim} but it must have 4 dimensions. The first dimension is the number of training samples, the second & third dimensions represent the width and height of the sample, and the fourth dimension represents the number of channels in the sample." + self.logger.error(msg) + raise ValueError(msg) + + predictions = [] + for sample in data_inputs: + probs = self.feed_sample(sample=sample) + predicted_label = numpy.where(numpy.max(probs) == probs)[0][0] + predictions.append(predicted_label) + return predictions + + def summary(self): + + """ + Prints a summary of the CNN architecture. + """ + + self.logger.info("\n----------Network Architecture----------") + for layer in self.network_layers: + self.logger.info(type(layer)) + self.logger.info("----------------------------------------\n") diff --git a/pygad/gacnn/__init__.py b/pygad/gacnn/__init__.py new file mode 100644 index 00000000..95cb1046 --- /dev/null +++ b/pygad/gacnn/__init__.py @@ -0,0 +1,4 @@ +from .gacnn import * + +__version__ = "1.0.0" + diff --git a/pygad/gacnn/gacnn.py b/pygad/gacnn/gacnn.py new file mode 100644 index 00000000..1623c7d1 --- /dev/null +++ b/pygad/gacnn/gacnn.py @@ -0,0 +1,97 @@ +from ..cnn import cnn +import copy + +def population_as_vectors(population_networks): + + """ + Accepts the population as networks and returns a list holding all weights of the CNN layers of each solution (i.e. network) in the population as a vector. + 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 CNN. + + population_networks: A list holding references to the CNN models used in the population. + + Returns a list holding the weights vectors for all solutions (i.e. networks). + """ + + population_vectors = [] + for solution in population_networks: + # Converting the weights of single layer from the current CNN (i.e. solution) to 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) + + return population_vectors + +def population_as_matrices(population_networks, population_vectors): + + """ + Accepts the population as both networks and weights vectors and returns the weights of all layers of each solution (i.e. CNN) in the population as a matrix. + 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. + + 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). + """ + + 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. + 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) + + return population_matrices + +class GACNN: + + def create_population(self): + + """ + Creates the initial population of the genetic algorithm as a list of CNNs (i.e. solutions). Each element in the list holds a reference to the instance of the cnn.Model class. + + The method returns the list holding the references to the CNN models. + """ + + population_networks = [] + for solution in range(self.num_solutions): + + network = copy.deepcopy(self.model) + + # Appending the CNN model to the list of population networks. + population_networks.append(network) + + return population_networks + + def __init__(self, model, num_solutions): + + """ + Creates an instance of the GACNN class for training a CNN using the genetic algorithm. + The constructor of the GACNN class creates an initial population of multiple CNNs using the create_population() method. + The population returned holds references to instances of the cnn.Model class. + + 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. + """ + + self.model = model + + self.num_solutions = num_solutions + + # A list holding references to all the solutions (i.e. CNNs) used in the population. + self.population_networks = self.create_population() + + def update_population_trained_weights(self, population_trained_weights): + + """ + The `update_population_trained_weights()` method updates the `trained_weights` attribute of each CNN according to the weights passed in the `population_trained_weights` parameter. + + 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 CNNs. + """ + + idx = 0 + # Fetches all layers weights matrices for a single solution (i.e. CNN) + 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, + final_weights=population_trained_weights[idx]) + idx = idx + 1 diff --git a/pygad/gann/__init__.py b/pygad/gann/__init__.py new file mode 100644 index 00000000..ff458d41 --- /dev/null +++ b/pygad/gann/__init__.py @@ -0,0 +1,4 @@ +from .gann import * + +__version__ = "1.0.1" + diff --git a/pygad/gann/gann.py b/pygad/gann/gann.py new file mode 100644 index 00000000..2c3c5a75 --- /dev/null +++ b/pygad/gann/gann.py @@ -0,0 +1,270 @@ +from ..nn import nn +import warnings + +def validate_network_parameters(num_neurons_input, + num_neurons_output, + num_neurons_hidden_layers, + output_activation, + hidden_activations, + num_solutions=None): + """ + Validating the parameters passed to initial_population_networks() in addition to creating a list of the name(s) of the activation function(s) for the hidden layer(s). + In case that the value passed to the 'hidden_activations' parameter is a string not a list, then a list is created by replicating the passed name a number of times equal to the number of hidden layers (i.e. the length of the 'num_neurons_hidden_layers' parameter). + If an invalid parameter found, an exception is raised and the execution stops. + + The function accepts the same parameters passed to the constructor of the GANN class. + + 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). + output_activation: The name of the activation function of the output layer. + hidden_activations: The name(s) of the activation function(s) of the hidden layer(s). + num_solutions: Number of solutions (i.e. networks) in the population which defaults to None. 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. + + Returns a list holding the name(s) of the activation function(s) for the hidden layer(s). + """ + + # Validating the number of solutions within the population. + if not (num_solutions is None): + if num_solutions < 2: + raise ValueError(f"num_solutions: The number of solutions within the population must be at least 2. The current value is {num_solutions}.") + + # Validating the number of neurons in the input layer. + if num_neurons_input is int and num_neurons_input <= 0: + raise ValueError("num_neurons_input: The number of neurons in the input layer must be > 0.") + + # Validating the number of neurons in the output layer. + if num_neurons_output is int and num_neurons_output <= 0: + raise ValueError("num_neurons_output: The number of neurons in the output layer must be > 0.") + + # Validating the type of the 'num_neurons_hidden_layers' parameter which is expected to be list or tuple. + if not (type(num_neurons_hidden_layers) in [list, tuple]): + raise TypeError(f"num_neurons_hidden_layers: A list or a tuple is expected but {type(num_neurons_hidden_layers)} found.") + + # Frequently used error messages. + unexpected_output_activation_value = f"Output activation function: The activation function of the output layer is passed as a string not {type(output_activation)}." + unexpected_activation_value = "Activation function: The supported values for the activation function are {supported_activations} but an unexpected value is found:\n{activations}" + unexpected_activation_type = "Activation Function: A list, tuple, or a string is expected but {activations_type} found." + length_mismatch = "Hidden activation functions: When passing the activation function(s) as a list or a tuple, its length must match the length of the 'num_neurons_hidden_layers' parameter but a mismatch is found:\n{mismatched_lengths}" + + # A list of the names of the supported activation functions. + supported_activations = ["sigmoid", "relu", "softmax", "None"] + + # Validating the output layer activation function. + if not (type(output_activation) is str): + raise ValueError(unexpected_output_activation_value) + if not (output_activation in supported_activations): #activation_type + raise ValueError(unexpected_activation_value.format(activations=output_activation, supported_activations=supported_activations)) + + # Number of hidden layers. + num_hidden_layers = len(num_neurons_hidden_layers) + if num_hidden_layers > 1: # In case there are more than 1 hidden layer. + if type(hidden_activations) in [list, tuple]: + num_activations = len(hidden_activations) + if num_activations != num_hidden_layers: + raise ValueError(length_mismatch.format(mismatched_lengths="{num_activations} != {num_layers}".format(num_layers=num_hidden_layers, num_activations=num_activations))) + elif type(hidden_activations) is str: + if hidden_activations in supported_activations: + hidden_activations = [hidden_activations]*num_hidden_layers + else: + raise ValueError(unexpected_activation_value.format(supported_activations=supported_activations, activations=hidden_activations)) + else: + raise TypeError(unexpected_activation_type.format(activations_type=type(hidden_activations))) + elif num_hidden_layers == 1: # In case there is only 1 hidden layer. + if (type(hidden_activations) in [list, tuple]): + if len(hidden_activations) != 1: + raise ValueError(length_mismatch.format(mismatched_lengths="{num_activations} != {num_layers}".format(num_layers=num_hidden_layers, num_activations=len(hidden_activations)))) + elif type(hidden_activations) is str: + if not (hidden_activations in supported_activations): + raise ValueError(unexpected_activation_value.format(supported_activations=supported_activations, activations=hidden_activations)) + else: + hidden_activations = [hidden_activations] + else: + raise TypeError(unexpected_activation_type.format(activations_type=type(hidden_activations))) + else: # In case there are no hidden layers (num_hidden_layers == 0) + warnings.warn("WARNING: There are no hidden layers however a value is assigned to the parameter 'hidden_activations'. It will be reset to [].") + hidden_activations = [] + + # If the value passed to the 'hidden_activations' parameter is actually a list, then its elements are checked to make sure the listed name(s) of the activation function(s) are supported. + for act in hidden_activations: + if not (act in supported_activations): + raise ValueError(unexpected_activation_value.format(supported_activations=supported_activations, activations=act)) + + return hidden_activations + +def create_network(num_neurons_input, + num_neurons_output, + num_neurons_hidden_layers=[], + output_activation="softmax", + hidden_activations="relu", + parameters_validated=False): + """ + 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 gann.GANN class except for the num_solutions parameter because only a single network is created out of the create_network() function. + + 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. + 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. + """ + + # When parameters_validated is False, then the parameters are not yet validated and a call to validate_network_parameters() is required. + if parameters_validated == False: + # Validating the passed parameters before creating the network. + hidden_activations = validate_network_parameters(num_neurons_input=num_neurons_input, + num_neurons_output=num_neurons_output, + num_neurons_hidden_layers=num_neurons_hidden_layers, + output_activation=output_activation, + hidden_activations=hidden_activations) + + # Creating the input layer as an instance of the nn.InputLayer class. + input_layer = nn.InputLayer(num_neurons_input) + + if len(num_neurons_hidden_layers) > 0: + # If there are hidden layers, then the first hidden layer is connected to the input layer. + hidden_layer = nn.DenseLayer(num_neurons=num_neurons_hidden_layers.pop(0), + previous_layer=input_layer, + activation_function=hidden_activations.pop(0)) + # For the other hidden layers, each hidden layer is connected to its preceding hidden layer. + for hidden_layer_idx in range(len(num_neurons_hidden_layers)): + hidden_layer = nn.DenseLayer(num_neurons=num_neurons_hidden_layers.pop(0), + previous_layer=hidden_layer, + activation_function=hidden_activations.pop(0)) + + # The last hidden layer is connected to the output layer. + # The output layer is created as an instance of the nn.DenseLayer class. + output_layer = nn.DenseLayer(num_neurons=num_neurons_output, + previous_layer=hidden_layer, + activation_function=output_activation) + + # If there are no hidden layers, then the output layer is connected directly to the input layer. + elif len(num_neurons_hidden_layers) == 0: + # The output layer is created as an instance of the nn.DenseLayer class. + output_layer = nn.DenseLayer(num_neurons=num_neurons_output, + 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. + return output_layer + +def population_as_vectors(population_networks): + """ + 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. + 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. + + 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). + """ + population_vectors = [] + for solution in population_networks: + # Converting the weights of single layer from the current network (i.e. solution) to 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) + + return population_vectors + +def population_as_matrices(population_networks, population_vectors): + """ + 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. + 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. + + 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). + """ + 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. + 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) + + return population_matrices + +class GANN: + def create_population(self): + """ + Creates the initial population of the genetic algorithm as a list of neural networks (i.e. solutions). 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 `GANN` instance. + + The method returns the list holding the references to the networks. + """ + + 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. + 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(), + output_activation=self.output_activation, + hidden_activations=self.hidden_activations.copy(), + parameters_validated=True) + + # Appending the created network to the list of population networks. + population_networks.append(network) + + return population_networks + + def __init__(self, + num_solutions, + num_neurons_input, + num_neurons_output, + num_neurons_hidden_layers=[], + output_activation="softmax", + hidden_activations="relu"): + """ + 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. + + 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. + """ + + 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, + num_neurons_input=num_neurons_input, + num_neurons_output=num_neurons_output, + num_neurons_hidden_layers=num_neurons_hidden_layers, + output_activation=output_activation, + hidden_activations=hidden_activations) + + self.num_solutions = num_solutions + self.num_neurons_input = num_neurons_input + self.num_neurons_output = num_neurons_output + self.num_neurons_hidden_layers = num_neurons_hidden_layers + self.output_activation = output_activation + self.hidden_activations = hidden_activations + self.parameters_validated = True + + # After the parameters are validated, the initial population is created. + self.population_networks = self.create_population() # A list holding references to all the solutions (i.e. neural networks) used in the population. + + def update_population_trained_weights(self, population_trained_weights): + """ + The `update_population_trained_weights()` method updates the `trained_weights` attribute 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. + + 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) + 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, + final_weights=population_trained_weights[idx]) + idx = idx + 1 diff --git a/pygad/helper/__init__.py b/pygad/helper/__init__.py new file mode 100644 index 00000000..e781d27f --- /dev/null +++ b/pygad/helper/__init__.py @@ -0,0 +1,3 @@ +from pygad.helper import unique + +__version__ = "1.1.0" \ No newline at end of file diff --git a/pygad/helper/unique.py b/pygad/helper/unique.py new file mode 100644 index 00000000..8b523f33 --- /dev/null +++ b/pygad/helper/unique.py @@ -0,0 +1,739 @@ +""" +The pygad.helper.unique module has helper methods to solve duplicate genes and make sure every gene is unique. +""" + +import numpy +import warnings +import random +import pygad + +class Unique: + + def solve_duplicate_genes_randomly(self, + solution, + min_val, + max_val, + mutation_by_replacement, + gene_type, + num_trials=10): + """ + Resolves duplicates in a solution by randomly selecting new values for the duplicate genes. + + Args: + solution (list): A solution containing genes, potentially with duplicate values. + min_val (int): The minimum value of the range to sample a number randomly. + max_val (int): The maximum value of the range to sample a number randomly. + mutation_by_replacement (bool): Indicates if mutation is performed by replacement. + gene_type (type): The data type of the gene (e.g., int, float). + num_trials (int): The maximum number of attempts to resolve duplicates by changing the gene values. Only works for floating-point gene types. + + Returns: + tuple: + list: The updated solution after attempting to resolve duplicates. If no duplicates are resolved, the solution remains unchanged. + list: The indices of genes that still have duplicate values. + int: The number of duplicates that could not be resolved. + """ + + new_solution = solution.copy() + + _, unique_gene_indices = numpy.unique(solution, return_index=True) + not_unique_indices = set(range(len(solution))) - set(unique_gene_indices) + + num_unsolved_duplicates = 0 + if len(not_unique_indices) > 0: + for duplicate_index in not_unique_indices: + if self.gene_type_single == True: + dtype = gene_type + else: + dtype = gene_type[duplicate_index] + + if dtype[0] in pygad.GA.supported_int_types: + temp_val = self.unique_int_gene_from_range(solution=new_solution, + gene_index=duplicate_index, + min_val=min_val, + max_val=max_val, + mutation_by_replacement=mutation_by_replacement, + gene_type=gene_type) + else: + temp_val = self.unique_float_gene_from_range(solution=new_solution, + gene_index=duplicate_index, + min_val=min_val, + max_val=max_val, + mutation_by_replacement=mutation_by_replacement, + gene_type=gene_type, + num_trials=num_trials) + + 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.") + else: + # Unique gene value found. + new_solution[duplicate_index] = temp_val + + # Update the list of duplicate indices after each iteration. + _, unique_gene_indices = numpy.unique(new_solution, return_index=True) + not_unique_indices = set(range(len(solution))) - set(unique_gene_indices) + # self.logger.info("not_unique_indices INSIDE", not_unique_indices) + + return new_solution, not_unique_indices, num_unsolved_duplicates + + def solve_duplicate_genes_by_space(self, + solution, + gene_type, + num_trials=10, + build_initial_pop=False): + + """ + Resolves duplicates in a solution by selecting new values for the duplicate genes from the gene space. + + Args: + solution (list): A solution containing genes, potentially with duplicate values. + gene_type (type): The data type of the gene (e.g., int, float). + num_trials (int): The maximum number of attempts to resolve duplicates by selecting values from the gene space. + + Returns: + tuple: + list: The updated solution after attempting to resolve duplicates. If no duplicates are resolved, the solution remains unchanged. + list: The indices of genes that still have duplicate values. + int: The number of duplicates that could not be resolved. + """ + + new_solution = solution.copy() + + _, unique_gene_indices = numpy.unique(solution, return_index=True) + not_unique_indices = set(range(len(solution))) - set(unique_gene_indices) + # self.logger.info("not_unique_indices OUTSIDE", not_unique_indices) + + # First try to solve the duplicates. + # For a solution like [3 2 0 0], the indices of the 2 duplicating genes are 2 and 3. + # The next call to the find_unique_value() method tries to change the value of the gene with index 3 to solve the duplicate. + if len(not_unique_indices) > 0: + new_solution, not_unique_indices, num_unsolved_duplicates = self.unique_genes_by_space(new_solution=new_solution, + gene_type=gene_type, + not_unique_indices=not_unique_indices, + num_trials=10, + build_initial_pop=build_initial_pop) + else: + return new_solution, not_unique_indices, len(not_unique_indices) + + # Do another try if there exist duplicate genes. + # If there are no possible values for the gene 3 with index 3 to solve the duplicate, try to change the value of the other gene with index 2. + if len(not_unique_indices) > 0: + not_unique_indices = set(numpy.where(new_solution == new_solution[list(not_unique_indices)[0]])[0]) - set([list(not_unique_indices)[0]]) + new_solution, not_unique_indices, num_unsolved_duplicates = self.unique_genes_by_space(new_solution=new_solution, + gene_type=gene_type, + not_unique_indices=not_unique_indices, + num_trials=10, + build_initial_pop=build_initial_pop) + else: + # 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. + # 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. + return new_solution, not_unique_indices, len(not_unique_indices) + + return new_solution, not_unique_indices, num_unsolved_duplicates + + def unique_int_gene_from_range(self, + solution, + gene_index, + min_val, + max_val, + mutation_by_replacement, + gene_type, + step=1): + + """ + Finds a unique integer value for a specific gene in a solution. + + Args: + solution (list): A solution containing genes, potentially with duplicate values. + gene_index (int): The index of the gene for which to find a unique value. + min_val (int): The minimum value of the range to sample an integer randomly. + max_val (int): The maximum value of the range to sample an integer randomly. + mutation_by_replacement (bool): Indicates if mutation is performed by replacement. + gene_type (type): The data type of the gene (e.g., int, int8, uint16, etc). + step (int, optional): The step size for generating candidate values. Defaults to 1. + + Returns: + int: The new integer value of the gene. If no unique value can be found, the original gene value is returned. + """ + + # The gene_type is of the form [type, precision] + dtype = gene_type + + # For non-integer steps, the numpy.arange() function returns zeros if the dtype parameter is set to an integer data type. So, this returns zeros if step is non-integer and dtype is set to an int data type: numpy.arange(min_val, max_val, step, dtype=gene_type[0]) + # To solve this issue, the data type casting will not be handled inside numpy.arange(). The range is generated by numpy.arange() and then the data type is converted using the numpy.asarray() function. + all_gene_values = numpy.asarray(numpy.arange(min_val, + max_val, + step), + dtype=dtype[0]) + + # If mutation is by replacement, do not add the current gene value into the list. + # This is to avoid replacing the value by itself again. We are doing nothing in this case. + if mutation_by_replacement: + pass + else: + all_gene_values = all_gene_values + solution[gene_index] + + # After adding solution[gene_index] to the list, we have to change the data type again. + all_gene_values = numpy.asarray(all_gene_values, + dtype[0]) + + values_to_select_from = list(set(list(all_gene_values)) - set(solution)) + + if len(values_to_select_from) == 0: + # If there are no values, then keep the current gene value. + selected_value = solution[gene_index] + else: + selected_value = random.choice(values_to_select_from) + + selected_value = dtype[0](selected_value) + + return selected_value + + def unique_float_gene_from_range(self, + solution, + gene_index, + min_val, + max_val, + mutation_by_replacement, + gene_type, + num_trials=10): + + """ + Finds a unique floating-point value for a specific gene in a solution. + + Args: + solution (list): A solution containing genes, potentially with duplicate values. + gene_index (int): The index of the gene for which to find a unique value. + min_val (int): The minimum value of the range to sample a floating-point number randomly. + max_val (int): The maximum value of the range to sample a floating-point number randomly. + mutation_by_replacement (bool): Indicates if mutation is performed by replacement. + gene_type (type): The data type of the gene (e.g., float, float16, float32, etc). + num_trials (int): The maximum number of attempts to resolve duplicates by changing the gene values. + + Returns: + int: The new floating-point value of the gene. If no unique value can be found, the original gene value is returned. + """ + + # The gene_type is of the form [type, precision] + dtype = gene_type + + for trial_index in range(num_trials): + temp_val = numpy.random.uniform(low=min_val, + high=max_val, + size=1)[0] + + # If mutation is by replacement, do not add the current gene value into the list. + # This is to avoid replacing the value by itself again. We are doing nothing in this case. + if mutation_by_replacement: + pass + else: + temp_val = temp_val + solution[gene_index] + + if not dtype[1] is None: + # Precision is available and we have to round the number. + # Convert the data type and round the number. + temp_val = numpy.round(dtype[0](temp_val), + dtype[1]) + else: + # There is no precision and rounding the number is not needed. The type is [type, None] + # Just convert the data type. + temp_val = dtype[0](temp_val) + + if temp_val in solution and trial_index == (num_trials - 1): + # If there are no values, then keep the current gene value. + if not self.suppress_warnings: warnings.warn("You set 'allow_duplicate_genes=False' but cannot find a value to prevent duplicates.") + selected_value = solution[gene_index] + elif temp_val in solution: + # Keep trying in the other remaining trials. + continue + else: + # Unique gene value found. + selected_value = temp_val + break + + return selected_value + + def unique_genes_by_space(self, + new_solution, + gene_type, + not_unique_indices, + num_trials=10, + build_initial_pop=False): + + """ + Iterates through all duplicate genes to find unique values from their gene spaces and resolve duplicates. + For each duplicate gene, a call is made to the `unique_gene_by_space()` function. + + Args: + new_solution (list): A solution containing genes with duplicate values. + gene_type (type): The data type of the all the genes (e.g., int, float). + not_unique_indices (list): The indices of genes with duplicate values. + num_trials (int): The maximum number of attempts to resolve duplicates for each gene. Only works for floating-point numbers. + + Returns: + tuple: + list: The updated solution after attempting to resolve all duplicates. If no duplicates are resolved, the solution remains unchanged. + list: The indices of genes that still have duplicate values. + int: The number of duplicates that could not be resolved. + """ + + num_unsolved_duplicates = 0 + for duplicate_index in not_unique_indices: + temp_val = self.unique_gene_by_space(solution=new_solution, + gene_idx=duplicate_index, + gene_type=gene_type, + build_initial_pop=build_initial_pop, + num_trials=num_trials) + + if temp_val in new_solution: + # self.logger.info("temp_val, duplicate_index", temp_val, duplicate_index, 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 {new_solution[duplicate_index]}. Consider adding more values in the gene space or use a wider range for initial population or random mutation.") + else: + new_solution[duplicate_index] = temp_val + + # Update the list of duplicate indices after each iteration. + _, unique_gene_indices = numpy.unique(new_solution, return_index=True) + not_unique_indices = set(range(len(new_solution))) - set(unique_gene_indices) + # self.logger.info("not_unique_indices INSIDE", not_unique_indices) + + return new_solution, not_unique_indices, num_unsolved_duplicates + + def unique_gene_by_space(self, + solution, + gene_idx, + gene_type, + build_initial_pop=False, + num_trials=10): + + """ + Returns a unique value for a specific gene based on its value space to resolve duplicates. + + Args: + solution (list): A solution containing genes with duplicate values. + gene_idx (int): The index of the gene that has a duplicate value. + gene_type (type): The data type of the gene (e.g., int, float). + num_trials (int): The maximum number of attempts to resolve duplicates for each gene. Only works for floating-point numbers. + + Returns: + Any: A unique value for the gene, if one exists; otherwise, the original gene value. """ + + if self.gene_space_nested: + if type(self.gene_space[gene_idx]) in [numpy.ndarray, list, tuple]: + # Return the current gene space from the 'gene_space' attribute. + curr_gene_space = list(self.gene_space[gene_idx]).copy() + else: + # Return the entire gene space from the 'gene_space' attribute. + # curr_gene_space = list(self.gene_space[gene_idx]).copy() + curr_gene_space = self.gene_space[gene_idx] + + # If the gene space has only a single value, use it as the new gene value. + if type(curr_gene_space) in pygad.GA.supported_int_float_types: + value_from_space = curr_gene_space + # If the gene space is None, apply mutation by adding a random value between the range defined by the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val'. + elif curr_gene_space is None: + if self.gene_type_single == True: + dtype = gene_type + else: + dtype = gene_type[gene_idx] + + if dtype[0] in pygad.GA.supported_int_types: + if build_initial_pop == True: + # If we are building the initial population, then use the range of the initial population. + min_val = self.init_range_low + max_val = self.init_range_high + else: + # If we are NOT building the initial population, then use the range of the random mutation. + min_val = self.random_mutation_min_val + max_val = self.random_mutation_max_val + + value_from_space = self.unique_int_gene_from_range(solution=solution, + gene_index=gene_idx, + min_val=min_val, + max_val=max_val, + mutation_by_replacement=True, + gene_type=dtype) + else: + if build_initial_pop == True: + low = self.init_range_low + high = self.init_range_high + else: + low = self.random_mutation_min_val + high = self.random_mutation_max_val + + """ + value_from_space = numpy.random.uniform(low=low, + high=high, + size=1)[0] + """ + + value_from_space = self.unique_float_gene_from_range(solution=solution, + gene_index=gene_idx, + min_val=low, + max_val=high, + mutation_by_replacement=True, + gene_type=dtype, + num_trials=num_trials) + + + elif type(curr_gene_space) is dict: + if self.gene_type_single == True: + dtype = gene_type + else: + dtype = gene_type[gene_idx] + + # Use index 0 to return the type from the list (e.g. [int, None] or [float, 2]). + if dtype[0] in pygad.GA.supported_int_types: + if 'step' in curr_gene_space.keys(): + step = curr_gene_space['step'] + else: + step = None + + value_from_space = self.unique_int_gene_from_range(solution=solution, + gene_index=gene_idx, + min_val=curr_gene_space['low'], + max_val=curr_gene_space['high'], + step=step, + mutation_by_replacement=True, + gene_type=dtype) + else: + if 'step' in curr_gene_space.keys(): + value_from_space = numpy.random.choice(numpy.arange(start=curr_gene_space['low'], + stop=curr_gene_space['high'], + step=curr_gene_space['step']), + size=1)[0] + else: + value_from_space = numpy.random.uniform(low=curr_gene_space['low'], + high=curr_gene_space['high'], + size=1)[0] + else: + # Selecting a value randomly based on the current gene's space in the 'gene_space' attribute. + # If the gene space has only 1 value, then select it. The old and new values of the gene are identical. + if len(curr_gene_space) == 1: + value_from_space = curr_gene_space[0] + if not self.suppress_warnings: warnings.warn(f"You set 'allow_duplicate_genes=False' but the space of the gene with index {gene_idx} has only a single value. Thus, duplicates are possible.") + # If the gene space has more than 1 value, then select a new one that is different from the current value. + else: + values_to_select_from = list(set(curr_gene_space) - set(solution)) + + if len(values_to_select_from) == 0: + # DEEP-DUPLICATE-REMOVAL-NEEDED + # Search by this phrase to find where deep duplicates removal should be applied. + + # Reaching this block means there is no value in the gene space of this gene to solve the duplicates. + # To solve the duplicate between the 2 genes, the solution is to change the value of a third gene that makes a room to solve the duplicate. + + if not self.suppress_warnings: warnings.warn("You set 'allow_duplicate_genes=False' but the gene space does not have enough values to prevent duplicates.") + + solution2 = self.solve_duplicates_deeply(solution) + if solution2 is None: + # Cannot solve duplicates. At the moment, we are changing the value of a third gene to solve the duplicates between 2 genes. + # Maybe a 4th, 5th, 6th, or even more genes need to be changed to solve the duplicates. + pass + else: + solution = solution2 + value_from_space = solution[gene_idx] + + else: + value_from_space = random.choice(values_to_select_from) + else: + # Selecting a value randomly from the global gene space in the 'gene_space' attribute. + if type(self.gene_space) is dict: + if self.gene_type_single == True: + dtype = gene_type + else: + dtype = gene_type[gene_idx] + + if dtype[0] in pygad.GA.supported_int_types: + if 'step' in self.gene_space.keys(): + step = self.gene_space['step'] + else: + step = None + + value_from_space = self.unique_int_gene_from_range(solution=solution, + gene_index=gene_idx, + min_val=self.gene_space['low'], + max_val=self.gene_space['high'], + step=step, + mutation_by_replacement=True, + gene_type=dtype) + else: + # When the gene_space is assigned a dict object, then it specifies the lower and upper limits of all genes in the space. + if 'step' in self.gene_space.keys(): + value_from_space = numpy.random.choice(numpy.arange(start=self.gene_space['low'], + stop=self.gene_space['high'], + step=self.gene_space['step']), + size=1)[0] + else: + value_from_space = numpy.random.uniform(low=self.gene_space['low'], + high=self.gene_space['high'], + size=1)[0] + else: + # If the space type is not of type dict, then a value is randomly selected from the gene_space attribute. + # Remove all the genes in the current solution from the gene_space. + # This only leaves the unique values that could be selected for the gene. + values_to_select_from = list(set(self.gene_space) - set(solution)) + + if len(values_to_select_from) == 0: + if not self.suppress_warnings: warnings.warn("You set 'allow_duplicate_genes=False' but the gene space does not have enough values to prevent duplicates.") + value_from_space = solution[gene_idx] + else: + value_from_space = random.choice(values_to_select_from) + + if value_from_space is None: + if build_initial_pop == True: + low = self.init_range_low + high = self.init_range_high + else: + low = self.random_mutation_min_val + high = self.random_mutation_max_val + + value_from_space = numpy.random.uniform(low=low, + high=high, + size=1)[0] + + # Similar to the round_genes() method in the pygad module, + # Create a round_gene() method to round a single gene. + if self.gene_type_single == True: + dtype = gene_type + else: + dtype = gene_type[gene_idx] + + if not dtype[1] is None: + value_from_space = numpy.round(dtype[0](value_from_space), + dtype[1]) + else: + value_from_space = dtype[0](value_from_space) + + return value_from_space + + def find_two_duplicates(self, + solution, + gene_space_unpacked): + """ + Identifies the first occurrence of a duplicate gene in the solution. + + Returns: + tuple: + int: The index of the first gene with a duplicate value. + Any: The value of the duplicate gene. + """ + + for gene in set(solution): + gene_indices = numpy.where(numpy.array(solution) == gene)[0] + if len(gene_indices) == 1: + continue + for gene_idx in gene_indices: + number_alternate_values = len(set(gene_space_unpacked[gene_idx])) + 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. + return None, gene + + def unpack_gene_space(self, + range_min, + range_max, + num_values_from_inf_range=100): + """ + Unpacks the gene space for selecting a value to resolve duplicates by converting ranges into lists of values. + + Args: + range_min (float or int): The minimum value of the range. + range_max (float or int): The maximum value of the range. + num_values_from_inf_range (int): The number of values to generate for an infinite range of float values using `numpy.linspace()`. + + Returns: + list: A list representing the unpacked gene space. + """ + + # Copy the gene_space to keep it isolated form the changes. + if self.gene_space is None: + return None + + if self.gene_space_nested == False: + if type(self.gene_space) is range: + gene_space_unpacked = list(self.gene_space) + elif type(self.gene_space) in [numpy.ndarray, list]: + gene_space_unpacked = self.gene_space.copy() + elif type(self.gene_space) is dict: + if 'step' in self.gene_space.keys(): + gene_space_unpacked = numpy.arange(start=self.gene_space['low'], + stop=self.gene_space['high'], + step=self.gene_space['step']) + else: + gene_space_unpacked = numpy.linspace(start=self.gene_space['low'], + stop=self.gene_space['high'], + num=num_values_from_inf_range, + endpoint=False) + + if self.gene_type_single == True: + # Change the data type. + gene_space_unpacked = numpy.array(gene_space_unpacked, + dtype=self.gene_type[0]) + if not self.gene_type[1] is None: + # Round the values for float (non-int) data types. + gene_space_unpacked = numpy.round(gene_space_unpacked, + self.gene_type[1]) + else: + temp_gene_space_unpacked = gene_space_unpacked.copy() + gene_space_unpacked = [] + # Get the number of genes from the length of gene_type. + # The num_genes attribute is not set yet when this method (unpack_gene_space) is called for the first time. + for gene_idx in range(len(self.gene_type)): + # Change the data type. + gene_space_item_unpacked = numpy.array(temp_gene_space_unpacked, + self.gene_type[gene_idx][0]) + if not self.gene_type[gene_idx][1] is None: + # Round the values for float (non-int) data types. + gene_space_item_unpacked = numpy.round(temp_gene_space_unpacked, + self.gene_type[gene_idx][1]) + gene_space_unpacked.append(gene_space_item_unpacked) + + elif self.gene_space_nested == True: + gene_space_unpacked = self.gene_space.copy() + for space_idx, space in enumerate(gene_space_unpacked): + if type(space) in pygad.GA.supported_int_float_types: + gene_space_unpacked[space_idx] = [space] + elif space is None: + # Randomly generate the value using the mutation range. + gene_space_unpacked[space_idx] = numpy.arange(start=range_min, + stop=range_max) + elif type(space) is range: + # Convert the range to a list. + gene_space_unpacked[space_idx] = list(space) + elif type(space) is dict: + # Create a list of values using the dict range. + # Use numpy.linspace() + if self.gene_type_single == True: + dtype = self.gene_type + else: + dtype = self.gene_type[space_idx] + + if dtype[0] in pygad.GA.supported_int_types: + if 'step' in space.keys(): + step = space['step'] + else: + step = 1 + + gene_space_unpacked[space_idx] = numpy.arange(start=space['low'], + stop=space['high'], + step=step) + else: + if 'step' in space.keys(): + gene_space_unpacked[space_idx] = numpy.arange(start=space['low'], + stop=space['high'], + step=space['step']) + else: + gene_space_unpacked[space_idx] = numpy.linspace(start=space['low'], + stop=space['high'], + num=num_values_from_inf_range, + endpoint=False) + elif type(space) in [numpy.ndarray, list, tuple]: + # list/tuple/numpy.ndarray + # Convert all to list + gene_space_unpacked[space_idx] = list(space) + + # Check if there is an item with the value None. If so, replace it with a random value using the mutation range. + none_indices = numpy.where(numpy.array(gene_space_unpacked[space_idx]) == None)[0] + if len(none_indices) > 0: + for idx in none_indices: + random_value = numpy.random.uniform(low=range_min, + high=range_max, + size=1)[0] + gene_space_unpacked[space_idx][idx] = random_value + + if self.gene_type_single == True: + dtype = self.gene_type + else: + dtype = self.gene_type[space_idx] + + # Change the data type. + gene_space_unpacked[space_idx] = numpy.array(gene_space_unpacked[space_idx], + dtype=dtype[0]) + if not dtype[1] is None: + # Round the values for float (non-int) data types. + gene_space_unpacked[space_idx] = numpy.round(gene_space_unpacked[space_idx], + dtype[1]) + + return gene_space_unpacked + + 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. + + Args: + solution (list): The current solution containing genes, potentially with duplicates. + gene_idx1 (int): The index of the first gene involved in the duplication. + gene_idx2 (int): The index of the second gene involved in the duplication. + assist_gene_idx (int): The index of the third gene used to assist in resolving the duplication. + + Returns: + list or None: The updated solution with duplicates resolved, or `None` if the duplicates cannot be resolved. + """ + + # gene_space_unpacked = self.unpack_gene_space() + # Create a copy of the gene_space_unpacked attribute because it will be changed later. + gene_space_unpacked = self.gene_space_unpacked.copy() + + duplicate_index, duplicate_value = self.find_two_duplicates(solution, + gene_space_unpacked) + + if duplicate_index is None: + # Impossible to solve the duplicates for the genes with value duplicate_value. + return None + + + # Without copy(), the gene will be removed from the gene_space. + # Convert the space to list because tuples do not have copy() + gene_other_values = list(gene_space_unpacked[duplicate_index]).copy() + + # This removes all the occurrences of this value. + gene_other_values = [v for v in gene_other_values if v != duplicate_value] + + # The remove() function only removes the first occurrence of the value. + # Do not use it. + # gene_other_values.remove(duplicate_value) + + # Two conditions to solve the duplicates of the value D: + # 1. From gene_other_values, select a value V such that it is available in the gene space of another gene X. + # 2. Find an alternate value for the gene X that will not cause any duplicates. + # 2.1 If the gene X does not have alternatives, then go back to step 1 to find another gene. + # 2.2 Set the gene X to the value D. + # 2.3 Set the target gene to the value V. + # Set the space of the duplicate gene to empty list []. Do not remove it to not alter the indices of the gene spaces. + gene_space_unpacked[duplicate_index] = [] + + for other_value in gene_other_values: + for space_idx, space in enumerate(gene_space_unpacked): + if other_value in space: + if other_value in solution and list(solution).index(other_value) != space_idx: + continue + else: + # Find an alternate value for the third gene. + # Copy the space so that the original space is not changed after removing the value. + space_other_values = space.copy() + # This removes all the occurrences of this value. It is not enough to use the remove() function because it only removes the first occurrence. + space_other_values = [v for v in space_other_values if v != other_value] + + for val in space_other_values: + if val in solution: + # If the value exists in another gene of the solution, then we cannot use this value as it will cause another duplicate. + # End the current iteration and go check another value. + continue + else: + solution[space_idx] = val + solution[duplicate_index] = other_value + return solution + + # Reaching here means we cannot solve the duplicate genes. + return None diff --git a/pygad/kerasga/__init__.py b/pygad/kerasga/__init__.py new file mode 100644 index 00000000..735d549c --- /dev/null +++ b/pygad/kerasga/__init__.py @@ -0,0 +1,3 @@ +from .kerasga import * + +__version__ = "1.3.0" diff --git a/pygad/kerasga/kerasga.py b/pygad/kerasga/kerasga.py new file mode 100644 index 00000000..cda2c4b9 --- /dev/null +++ b/pygad/kerasga/kerasga.py @@ -0,0 +1,151 @@ +import copy +import numpy +import tensorflow.keras + +def model_weights_as_vector(model): + """ + Reshapes the Keras model weight as a vector. + + Parameters + ---------- + model : TYPE + The Keras model. + + Returns + ------- + TYPE + The weights as a 1D vector. + + """ + weights_vector = [] + + for layer in model.layers: # model.get_weights(): + if layer.trainable: + layer_weights = layer.get_weights() + for l_weights in layer_weights: + vector = numpy.reshape(l_weights, newshape=(l_weights.size)) + weights_vector.extend(vector) + + return numpy.array(weights_vector) + +def model_weights_as_matrix(model, weights_vector): + """ + Reshapes the PyGAD 1D solution as a Keras weight matrix. + + Parameters + ---------- + model : TYPE + The Keras model. + weights_vector : TYPE + The PyGAD solution as a 1D vector. + + Returns + ------- + weights_matrix : TYPE + The Keras weights as a matrix. + + """ + weights_matrix = [] + + start = 0 + for layer_idx, layer in enumerate(model.layers): # model.get_weights(): + # for w_matrix in model.get_weights(): + layer_weights = layer.get_weights() + if layer.trainable: + for l_weights in layer_weights: + layer_weights_shape = l_weights.shape + layer_weights_size = l_weights.size + + layer_weights_vector = weights_vector[start:start + layer_weights_size] + layer_weights_matrix = numpy.reshape(layer_weights_vector, newshape=(layer_weights_shape)) + weights_matrix.append(layer_weights_matrix) + + start = start + layer_weights_size + else: + for l_weights in layer_weights: + weights_matrix.append(l_weights) + + return weights_matrix + +def predict(model, + solution, + data, + batch_size=None, + verbose=0, + steps=None): + """ + Use the PyGAD's solution to make predictions using the Keras model. + + Parameters + ---------- + model : TYPE + The Keras model. + solution : TYPE + A single PyGAD solution as 1D vector. + data : TYPE + The data or a generator. + batch_size : TYPE, optional + The batch size (i.e. number of samples per step or batch). The default is None. Check documentation of the Keras Model.predict() method for more information. + verbose : TYPE, optional + Verbosity mode. The default is 0. Check documentation of the Keras Model.predict() method for more information. + steps : TYPE, optional + The total number of steps (batches of samples). The default is None. Check documentation of the Keras Model.predict() method for more information. + + Returns + ------- + predictions : TYPE + The Keras model predictions. + + """ + # Fetch the parameters of the best solution. + solution_weights = model_weights_as_matrix(model=model, + weights_vector=solution) + _model = tensorflow.keras.models.clone_model(model) + _model.set_weights(solution_weights) + predictions = _model.predict(x=data, + batch_size=batch_size, + verbose=verbose, + steps=steps) + + return predictions + +class KerasGA: + + def __init__(self, model, num_solutions): + + """ + Creates an instance of the KerasGA class to build a population of model parameters. + + model: A Keras model class. + num_solutions: Number of solutions in the population. Each solution has different model parameters. + """ + + self.model = model + + self.num_solutions = num_solutions + + # A list holding references to all the solutions (i.e. networks) used in the population. + self.population_weights = self.create_population() + + 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. + + The method returns a list holding the weights of all solutions. + """ + + model_weights_vector = model_weights_as_vector(model=self.model) + + net_population_weights = [] + net_population_weights.append(model_weights_vector) + + for idx in range(self.num_solutions-1): + + net_weights = copy.deepcopy(model_weights_vector) + net_weights = numpy.array(net_weights) + numpy.random.uniform(low=-1.0, high=1.0, size=model_weights_vector.size) + + # Appending the weights to the population. + net_population_weights.append(net_weights) + + return net_population_weights diff --git a/pygad/nn/__init__.py b/pygad/nn/__init__.py new file mode 100644 index 00000000..224d9843 --- /dev/null +++ b/pygad/nn/__init__.py @@ -0,0 +1,4 @@ +from .nn import * + +__version__ = "1.2.1" + diff --git a/pygad/nn/nn.py b/pygad/nn/nn.py new file mode 100644 index 00000000..023c4e28 --- /dev/null +++ b/pygad/nn/nn.py @@ -0,0 +1,399 @@ +import numpy +import functools + +""" +This project creates a neural network where the architecture has input and dense layers only. More layers will be added in the future. +The project only implements the forward pass of a neural network and no training algorithm is used. +For training a neural network using the genetic algorithm, check this project (https://github.com/ahmedfgad/NeuralGenetic) in which the genetic algorithm is used for training the network. +Feel free to leave an issue in this project (https://github.com/ahmedfgad/NumPyANN) in case something is not working properly or to ask for questions. I am also available for e-mails at ahmed.f.gad@gmail.com +""" + +def layers_weights(last_layer, initial=True): + """ + Creates a list holding the weights of all layers in the neural network. + + last_layer: A reference to the last (output) layer in the network architecture. + initial: When True, the function returns the initial weights of the layers. When False, the trained weights of the layers are returned. The initial weights are only needed before network training starts. The trained weights are needed to predict the network outputs. + + Returns a list (network_weights) holding the weights of the layers. + """ + network_weights = [] + + layer = last_layer + while "previous_layer" in layer.__init__.__code__.co_varnames: + # If the 'initial' parameter is True, append the initial weights. Otherwise, append the trained weights. + if initial == True: + network_weights.append(layer.initial_weights) + elif initial == False: + network_weights.append(layer.trained_weights) + else: + raise ValueError(f"Unexpected value to the 'initial' parameter: {initial}.") + + # Go to the previous layer. + layer = layer.previous_layer + + # If the first layer in the network is not an input layer (i.e. an instance of the InputLayer class), raise an error. + if not (type(layer) is InputLayer): + 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). + network_weights.reverse() + return network_weights + +def layers_weights_as_vector(last_layer, initial=True): + """ + Creates a list holding the weights of each layer in the network as a vector. + + last_layer: A reference to the last (output) layer in the network architecture. + initial: When True, the function returns the initial weights of the layers. When False, the trained weights of the layers are returned. The initial weights are only needed before network training starts. The trained weights are needed to predict the network outputs. + + Returns a list (network_weights) holding the weights of the layers as a vector. + """ + network_weights = [] + + layer = last_layer + while "previous_layer" in layer.__init__.__code__.co_varnames: + # If the 'initial' parameter is True, append the initial weights. Otherwise, append the trained weights. + if initial == True: + vector = numpy.reshape(layer.initial_weights, newshape=(layer.initial_weights.size)) +# vector = DenseLayer.to_vector(matrix=layer.initial_weights) + network_weights.extend(vector) + elif initial == False: + vector = numpy.reshape(layer.trained_weights, newshape=(layer.trained_weights.size)) +# vector = DenseLayer.to_vector(array=layer.trained_weights) + network_weights.extend(vector) + else: + raise ValueError(f"Unexpected value to the 'initial' parameter: {initial}.") + + # Go to the previous layer. + layer = layer.previous_layer + + # If the first layer in the network is not an input layer (i.e. an instance of the InputLayer class), raise an error. + if not (type(layer) is InputLayer): + 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). + network_weights.reverse() + return numpy.array(network_weights) + +def layers_weights_as_matrix(last_layer, vector_weights): + """ + Converts the network weights from vectors to matrices. + + 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. + + Returns a list (network_weights) holding the weights of the layers as matrices. + """ + network_weights = [] + + start = 0 + layer = last_layer + vector_weights = vector_weights[::-1] + while "previous_layer" in layer.__init__.__code__.co_varnames: + layer_weights_shape = layer.initial_weights.shape + layer_weights_size = layer.initial_weights.size + + weights_vector=vector_weights[start:start + layer_weights_size] +# matrix = DenseLayer.to_array(vector=weights_vector, shape=layer_weights_shape) + matrix = numpy.reshape(weights_vector, newshape=(layer_weights_shape)) + network_weights.append(matrix) + + start = start + layer_weights_size + + # Go to the previous layer. + layer = layer.previous_layer + + # If the first layer in the network is not an input layer (i.e. an instance of the InputLayer class), raise an error. + if not (type(layer) is InputLayer): + 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). + network_weights.reverse() + return network_weights + +def layers_activations(last_layer): + """ + Creates a list holding the activation functions of all layers in the network. + + last_layer: A reference to the last (output) layer in the network architecture. + + Returns a list (activations) holding the activation functions of the layers. + """ + activations = [] + + layer = last_layer + while "previous_layer" in layer.__init__.__code__.co_varnames: + activations.append(layer.activation_function) + + # Go to the previous layer. + layer = layer.previous_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). + 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, + data_inputs, + data_outputs, + problem_type="classification", + learning_rate=0.01): + """ + Trains the neural network. + + 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: Can be either classification or regression to define the problem type. + learning_rate: Learning rate which defaults to 0.01. + """ + + if not (problem_type in ["classification", "regression"]): + raise ValueError(f"The value of the problem_type parameter can be either classification or regression but {problem_type} found.") + + # To fetch the initial weights of the layer, the 'initial' argument is set to True. + weights = layers_weights(last_layer, initial=True) + activations = layers_activations(last_layer) + + network_error = 0 + for epoch in range(num_epochs): + print("Epoch ", epoch) + for sample_idx in range(data_inputs.shape[0]): + r1 = data_inputs[sample_idx, :] + for idx in range(len(weights) - 1): + curr_weights = weights[idx] + r1 = numpy.matmul(r1, curr_weights) + if activations[idx] == "relu": + r1 = relu(r1) + elif activations[idx] == "sigmoid": + r1 = sigmoid(r1) + elif activations[idx] == "softmax": + r1 = softmax(r1) + elif activations[idx] == None: + pass + + curr_weights = weights[-1] + r1 = numpy.matmul(r1, curr_weights) + + if problem_type == "classification": + prediction = numpy.where(r1 == numpy.max(r1))[0][0] + else: + prediction = r1 + + network_error = network_error + numpy.mean(numpy.abs((prediction - data_outputs[sample_idx]))) + + # Updating the network weights once after completing an epoch (i.e. passing through all the samples). + weights = update_weights(weights=weights, + 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. + 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. + 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. + network_error: The network error. + learning_rate: The learning rate. + + It returns the new weights. + """ + # weights = numpy.array(weights) + for layer_idx in range(len(weights)): + weights[layer_idx] = network_error * learning_rate * weights[layer_idx] + + return weights + +def update_layers_trained_weights(last_layer, final_weights): + """ + After the network weights are trained, the 'trained_weights' attribute of each layer is updated 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. + + 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. + """ + layer = last_layer + layer_idx = len(final_weights) - 1 + while "previous_layer" in layer.__init__.__code__.co_varnames: + layer.trained_weights = final_weights[layer_idx] + + layer_idx = layer_idx - 1 + # Go to the previous layer. + layer = layer.previous_layer + +def predict(last_layer, data_inputs, problem_type="classification"): + """ + Uses the trained weights for predicting the samples' outputs. + + last_layer: A reference to the last (output) layer in the network architecture. + data_inputs: Data features. + problem_type: Can be either classification or regression to define the problem type. + + Returns the predictions of all samples. + """ + if not (problem_type in ["classification", "regression"]): + raise ValueError(f"The value of the problem_type parameter can be either classification or regression but {problem_type} found.") + + # To fetch the trained weights of the layer, the 'initial' argument is set to False. + weights = layers_weights(last_layer, initial=False) + activations = layers_activations(last_layer) + + if len(weights) != len(activations): + raise TypeError(f"The length of layers {len(weights)} is not equal to the number of activations functions {len(activations)} and they must be equal.") + + predictions = [] + for sample_idx in range(data_inputs.shape[0]): + r1 = data_inputs[sample_idx, :] + for curr_weights, activation in zip(weights, activations): + r1 = numpy.matmul(r1, curr_weights) + if activation == "relu": + r1 = relu(r1) + elif activation == "sigmoid": + r1 = sigmoid(r1) + elif activation == "softmax": + r1 = softmax(r1) + elif activation == None: + pass + + if problem_type == "classification": + prediction = numpy.where(r1 == numpy.max(r1))[0][0] + else: + prediction = r1 + + predictions.append(prediction) + + return predictions + +def to_vector(array): + """ + Converts a passed NumPy array (of any dimensionality) to its `array` parameter into a 1D vector and returns the vector. + + array: The NumPy array to be converted into a 1D vector. + + Returns the array after being reshaped into a NumPy 1D vector. + + Example: weights_vector = nn.DenseLayer.to_vector(array=array) + """ + if not (type(array) is numpy.ndarray): + raise TypeError(f"An input of type numpy.ndarray is expected but an input of type {type(array)} found.") + return numpy.reshape(array, newshape=(array.size)) + +def to_array(vector, shape): + """ + Converts a passed vector to its `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. + + Returns the NumPy 1D vector after being reshaped into an array. + + Example: weights_matrix = nn.DenseLayer.to_array(vector=vector, shape=shape) + """ + if not (type(vector) is numpy.ndarray): + raise TypeError(f"An input of type numpy.ndarray is expected but an input of type {type(vector)} found.") + if vector.ndim > 1: + raise ValueError(f"A 1D NumPy array is expected but an array of {vector.ndim} dimensions found.") + if vector.size != functools.reduce(lambda x,y:x*y, shape, 1): # (operator.mul == lambda x,y:x*y + raise ValueError(f"Mismatch between the vector length and the array shape. A vector of length {vector.size} cannot be converted into a array of shape ({shape}).") + return numpy.reshape(vector, newshape=shape) + +class InputLayer: + """ + Implementing the input layer of a neural network. + """ + def __init__(self, num_inputs): + if num_inputs <= 0: + raise ValueError("Number of input neurons cannot be <= 0. Please pass a valid value to the 'num_inputs' parameter.") + # The number of neurons in the input layer. + self.num_neurons = num_inputs + +class DenseLayer: + """ + Implementing the input dense (fully connected) layer of a neural network. + """ + def __init__(self, num_neurons, previous_layer, activation_function="sigmoid"): + if num_neurons <= 0: + raise ValueError("Number of neurons cannot be <= 0. Please pass a valid value to the 'num_neurons' parameter.") + # Number of neurons in the dense layer. + self.num_neurons = num_neurons + + supported_activation_functions = ("sigmoid", "relu", "softmax", "None") + if not (activation_function in supported_activation_functions): + raise ValueError(f"The specified activation function '{activation_function}' is not among the supported activation functions {supported_activation_functions}. Please use one of the supported functions.") + self.activation_function = activation_function + + 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. + self.previous_layer = previous_layer + + # Initializing the weights of the layer. + self.initial_weights = numpy.random.uniform(low=-0.1, + high=0.1, + size=(previous_layer.num_neurons, num_neurons)) + + # The trained weights of the layer. Only assigned a value after the network is trained (i.e. the train() function completes). + # Just initialized to be equal to the initial weights + self.trained_weights = self.initial_weights.copy() \ No newline at end of file diff --git a/pygad/pygad.py b/pygad/pygad.py new file mode 100644 index 00000000..88a2d9dd --- /dev/null +++ b/pygad/pygad.py @@ -0,0 +1,2637 @@ +import numpy +import random +import cloudpickle +import warnings +import concurrent.futures +import inspect +import logging +from pygad import utils +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. +class GA(utils.parent_selection.ParentSelection, + utils.crossover.Crossover, + utils.mutation.Mutation, + utils.nsga2.NSGA2, + helper.unique.Unique, + visualize.plot.Plot): + + supported_int_types = [int, numpy.int8, numpy.int16, numpy.int32, numpy.int64, + numpy.uint, numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64, + object] + supported_float_types = [float, numpy.float16, numpy.float32, numpy.float64, + object] + supported_int_float_types = supported_int_types + supported_float_types + + def __init__(self, + num_generations, + num_parents_mating, + fitness_func, + fitness_batch_size=None, + initial_population=None, + sol_per_pop=None, + num_genes=None, + init_range_low=-4, + init_range_high=4, + gene_type=float, + parent_selection_type="sss", + keep_parents=-1, + keep_elitism=1, + K_tournament=3, + crossover_type="single_point", + crossover_probability=None, + mutation_type="random", + mutation_probability=None, + mutation_by_replacement=False, + mutation_percent_genes='default', + mutation_num_genes=None, + random_mutation_min_val=-1.0, + random_mutation_max_val=1.0, + gene_space=None, + allow_duplicate_genes=True, + on_start=None, + on_fitness=None, + on_parents=None, + on_crossover=None, + on_mutation=None, + on_generation=None, + on_stop=None, + save_best_solutions=False, + save_solutions=False, + suppress_warnings=False, + stop_criteria=None, + parallel_processing=None, + random_seed=None, + logger=None): + """ + The constructor of the GA class accepts all parameters required to create an instance of the GA class. It validates such parameters. + + num_generations: Number of generations. + num_parents_mating: Number of solutions to be selected as parents in the mating pool. + + fitness_func: Accepts a function/method and returns the fitness value of the solution. In PyGAD 2.20.0, a third parameter is passed referring to the 'pygad.GA' instance. If method, then it must accept 4 parameters where the fourth one refers to the method's object. + fitness_batch_size: Added in PyGAD 2.19.0. Supports calculating the fitness in batches. If the value is 1 or None, then the fitness function is called for each invidiaul solution. If given another value X where X is neither 1 nor None (e.g. X=3), then the fitness function is called once for each X (3) solutions. + + 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. + sol_per_pop: Number of solutions in the population. + num_genes: Number of parameters in the function. + + 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 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). + + 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. + + parent_selection_type: Type of parent selection. + keep_parents: If 0, this means no parent in the current population will be used in the next population. If -1, this means all parents in the current population will be used in the next population. If set to a value > 0, then the specified value refers to the number of parents in the current population to be used in the next population. Some parent selection operators such as rank selection, favor population diversity and therefore keeping the parents in the next generation can be beneficial. However, some other parent selection operators, such as roulette wheel selection (RWS), have higher selection pressure and keeping more than one parent in the next generation can seriously harm population diversity. This parameter have an effect only when the keep_elitism parameter is 0. Thanks to Prof. Fernando Jiménez Barrionuevo (http://webs.um.es/fernan) for editing this sentence. + K_tournament: When the value of 'parent_selection_type' is 'tournament', the 'K_tournament' parameter specifies the number of solutions from which a parent is selected randomly. + + keep_elitism: 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. + + crossover_type: Type of the crossover opreator. 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: The probability of selecting a solution for the crossover operation. If the solution probability is <= crossover_probability, the solution is selected. The value must be between 0 and 1 inclusive. + + mutation_type: Type of the mutation opreator. 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. + mutation_probability: The probability of selecting a gene for the mutation operation. If the gene probability is <= mutation_probability, the gene is selected. It accepts either a single value for fixed mutation or a list/tuple/numpy.ndarray of 2 values for adaptive mutation. The values must be between 0 and 1 inclusive. If specified, then no need for the 2 parameters mutation_percent_genes and mutation_num_genes. + + mutation_by_replacement: An optional bool parameter. 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. + + mutation_percent_genes: Percentage of genes to mutate which defaults to the string 'default' which means 10%. This parameter has no action if any of the 2 parameters mutation_probability or mutation_num_genes exist. + mutation_num_genes: Number of genes to mutate which defaults to None. If the parameter mutation_num_genes exists, then no need for the parameter mutation_percent_genes. This parameter has no action if the mutation_probability parameter exists. + 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. + + on_start: 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: 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: Accepts a function/method to be called after selecting the parents that mates. If function, then it must accept 2 parameters: the first one represents the instance of the genetic algorithm and the second one represents the selected parents. 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: Accepts a function/method to be called each time the crossover operation is applied. If function, then it must accept 2 parameters: the first one represents the instance of the genetic algorithm and the second one represents the offspring generated using crossover. 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_mutation: Accepts a function/method to be called each time the mutation operation is applied. If function, then it 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. 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_generation: Accepts a function/method to be called after each generation. If function, then it must accept a single parameter representing the instance of the genetic algorithm. If the function returned "stop", then the run() method stops without completing the other generations. 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_stop: Accepts a function/method to be called only once exactly before the genetic algorithm stops or when it completes all the generations. If function, then it 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. If method, then it must accept 3 parameters where the third one refers to the method's object. Added in PyGAD 2.6.0. + + save_best_solutions: Added in PyGAD 2.9.0 and its type is bool. If True, then the best solution in each generation is saved into the 'best_solutions' attribute. Use this parameter with caution as it may cause memory overflow when either the number of generations or the number of genes is large. + save_solutions: Added in PyGAD 2.15.0 and its type is bool. If True, then all solutions in each generation are saved into the 'solutions' attribute. Use this 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. + + suppress_warnings: Added in PyGAD 2.10.0 and its type is bool. If True, then no warning messages will be displayed. It defaults to False. + + allow_duplicate_genes: 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: Added in PyGAD 2.15.0. It is assigned to some criteria to stop the evolution if at least one criterion holds. + + parallel_processing: Added in PyGAD 2.17.0. Defaults to `None` which means no parallel processing is used. If a positive integer is assigned, it specifies the number of threads to be used. 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 parallel processing is not used. This is identical to setting 'parallel_processing=None'. 3) None to use the default value as calculated by the concurrent.futures module. + + random_seed: 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. + + logger: Added in PyGAD 2.20.0. It accepts a logger object of the 'logging.Logger' class to log the messages. If no logger is passed, then a default logger is created to log/print the messages to the console exactly like using the 'print()' function. + """ + try: + # 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. + logger = logging.getLogger(__name__) + # Set the logger log level to 'DEBUG' to log all kinds of messages. + logger.setLevel(logging.DEBUG) + + # Clear any attached handlers to the logger from the previous runs. + # If the handlers are not cleared, then the new handler will be appended to the list of handlers. + # This makes the single log message be repeated according to the length of the list of handlers. + logger.handlers.clear() + + # Create the handlers. + stream_handler = logging.StreamHandler() + # Set the handler log level to 'DEBUG' to log all kinds of messages received from the logger. + stream_handler.setLevel(logging.DEBUG) + + # Create the formatter that just includes the log message. + formatter = logging.Formatter('%(message)s') + + # Add the formatter to the handler. + stream_handler.setFormatter(formatter) + + # Add the handler to the logger. + logger.addHandler(stream_handler) + else: + # Validate that the passed logger is of type 'logging.Logger'. + if isinstance(logger, logging.Logger): + pass + else: + raise TypeError(f"The expected type of the 'logger' parameter is 'logging.Logger' but {type(logger)} found.") + + # Create the 'self.logger' attribute to hold the logger. + # Instead of using 'print()', use 'self.logger.info()' + self.logger = logger + + self.random_seed = random_seed + if random_seed is None: + pass + else: + numpy.random.seed(self.random_seed) + random.seed(self.random_seed) + + # If suppress_warnings is bool and its valud is False, then print warning messages. + if type(suppress_warnings) is bool: + self.suppress_warnings = suppress_warnings + else: + self.valid_parameters = False + raise TypeError(f"The expected type of the 'suppress_warnings' parameter is bool but {type(suppress_warnings)} found.") + + # Validating mutation_by_replacement + if not (type(mutation_by_replacement) is bool): + self.valid_parameters = False + raise TypeError(f"The expected type of the 'mutation_by_replacement' parameter is bool but {type(mutation_by_replacement)} found.") + + self.mutation_by_replacement = mutation_by_replacement + + # Validate allow_duplicate_genes + if not (type(allow_duplicate_genes) is bool): + self.valid_parameters = False + 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 + + # Validate gene_space + self.gene_space_nested = False + if type(gene_space) is type(None): + pass + elif type(gene_space) is range: + if len(gene_space) == 0: + self.valid_parameters = False + raise ValueError("'gene_space' cannot be empty (i.e. its length must be >= 0).") + elif type(gene_space) in [list, numpy.ndarray]: + if len(gene_space) == 0: + self.valid_parameters = False + raise ValueError("'gene_space' cannot be empty (i.e. its length must be >= 0).") + else: + for index, el in enumerate(gene_space): + if type(el) in [numpy.ndarray, list, tuple, range]: + if len(el) == 0: + self.valid_parameters = False + raise ValueError(f"The element indexed {index} of 'gene_space' with type {type(el)} cannot be empty (i.e. its length must be >= 0).") + else: + for val in el: + if not (type(val) in [type(None)] + GA.supported_int_float_types): + raise TypeError(f"All values in the sublists inside the 'gene_space' attribute must be numeric of type int/float/None but ({val}) of type {type(val)} found.") + self.gene_space_nested = True + elif type(el) == type(None): + pass + # self.gene_space_nested = True + elif type(el) is dict: + if len(el.items()) == 2: + if ('low' in el.keys()) and ('high' in el.keys()): + pass + else: + self.valid_parameters = False + raise ValueError(f"When an element in the 'gene_space' parameter is of type dict, then it can have the keys 'low', 'high', and 'step' (optional) but the following keys found: {el.keys()}") + elif len(el.items()) == 3: + if ('low' in el.keys()) and ('high' in el.keys()) and ('step' in el.keys()): + pass + else: + self.valid_parameters = False + raise ValueError(f"When an element in the 'gene_space' parameter is of type dict, then it can have the keys 'low', 'high', and 'step' (optional) but the following keys found: {el.keys()}") + else: + self.valid_parameters = False + raise ValueError(f"When an element in the 'gene_space' parameter is of type dict, then it must have only 2 items but ({len(el.items())}) items found.") + self.gene_space_nested = True + elif not (type(el) in GA.supported_int_float_types): + self.valid_parameters = False + raise TypeError(f"Unexpected type {type(el)} for the element indexed {index} of 'gene_space'. The accepted types are list/tuple/range/numpy.ndarray of numbers, a single number (int/float), or None.") + + elif type(gene_space) is dict: + if len(gene_space.items()) == 2: + if ('low' in gene_space.keys()) and ('high' in gene_space.keys()): + pass + else: + self.valid_parameters = False + raise ValueError(f"When the 'gene_space' parameter is of type dict, then it can have only the keys 'low', 'high', and 'step' (optional) but the following keys found: {gene_space.keys()}") + elif len(gene_space.items()) == 3: + if ('low' in gene_space.keys()) and ('high' in gene_space.keys()) and ('step' in gene_space.keys()): + pass + else: + self.valid_parameters = False + raise ValueError(f"When the 'gene_space' parameter is of type dict, then it can have only the keys 'low', 'high', and 'step' (optional) but the following keys found: {gene_space.keys()}") + else: + self.valid_parameters = False + raise ValueError(f"When the 'gene_space' parameter is of type dict, then it must have only 2 items but ({len(gene_space.items())}) items found.") + + else: + self.valid_parameters = False + raise TypeError(f"The expected type of 'gene_space' is list, range, or numpy.ndarray but {type(gene_space)} found.") + + self.gene_space = gene_space + + # Validate init_range_low and init_range_high + # if type(init_range_low) in GA.supported_int_float_types: + # if type(init_range_high) in GA.supported_int_float_types: + # self.init_range_low = init_range_low + # self.init_range_high = init_range_high + # else: + # self.valid_parameters = False + # raise ValueError(f"The value passed to the 'init_range_high' parameter must be either integer or floating-point number but the value ({init_range_high}) of type {type(init_range_high)} found.") + # else: + # self.valid_parameters = False + # raise ValueError(f"The value passed to the 'init_range_low' parameter must be either integer or floating-point number but the value ({init_range_low}) of type {type(init_range_low)} found.") + + # Validate init_range_low and init_range_high + if type(init_range_low) in GA.supported_int_float_types: + if type(init_range_high) in GA.supported_int_float_types: + if init_range_low == init_range_high: + if not self.suppress_warnings: + warnings.warn("The values of the 2 parameters 'init_range_low' and 'init_range_high' are equal and this might return the same value for some genes in the initial population.") + else: + self.valid_parameters = False + raise TypeError(f"Type mismatch between the 2 parameters 'init_range_low' {type(init_range_low)} and 'init_range_high' {type(init_range_high)}.") + elif type(init_range_low) in [list, tuple, numpy.ndarray]: + # The self.num_genes attribute is not created yet. + # if len(init_range_low) == self.num_genes: + # pass + # else: + # self.valid_parameters = False + # raise ValueError(f"The length of the 'init_range_low' parameter is {len(init_range_low)} which is different from the number of genes {self.num_genes}.") + + # Get the number of genes before validating the num_genes parameter. + if num_genes is None: + if initial_population is None: + self.valid_parameters = False + raise TypeError("When the parameter 'initial_population' is None, then the 2 parameters 'sol_per_pop' and 'num_genes' cannot be None too.") + elif not len(init_range_low) == len(initial_population[0]): + self.valid_parameters = False + raise ValueError(f"The length of the 'init_range_low' parameter is {len(init_range_low)} which is different from the number of genes {len(initial_population[0])}.") + elif not len(init_range_low) == num_genes: + self.valid_parameters = False + raise ValueError(f"The length of the 'init_range_low' parameter is {len(init_range_low)} which is different from the number of genes {num_genes}.") + + if type(init_range_high) in [list, tuple, numpy.ndarray]: + if len(init_range_low) == len(init_range_high): + pass + else: + self.valid_parameters = False + raise ValueError(f"Size mismatch between the 2 parameters 'init_range_low' {len(init_range_low)} and 'init_range_high' {len(init_range_high)}.") + + # Validate the values in init_range_low + for val in init_range_low: + if type(val) in GA.supported_int_float_types: + pass + else: + self.valid_parameters = False + raise TypeError(f"When an iterable (list/tuple/numpy.ndarray) is assigned to the 'init_range_low' parameter, its elements must be numeric but the value {val} of type {type(val)} found.") + + # Validate the values in init_range_high + for val in init_range_high: + if type(val) in GA.supported_int_float_types: + pass + else: + self.valid_parameters = False + raise TypeError(f"When an iterable (list/tuple/numpy.ndarray) is assigned to the 'init_range_high' parameter, its elements must be numeric but the value {val} of type {type(val)} found.") + else: + self.valid_parameters = False + raise TypeError(f"Type mismatch between the 2 parameters 'init_range_low' {type(init_range_low)} and 'init_range_high' {type(init_range_high)}. Both of them can be either numeric or iterable (list/tuple/numpy.ndarray).") + else: + self.valid_parameters = False + raise TypeError(f"The expected type of the 'init_range_low' parameter is numeric or list/tuple/numpy.ndarray but {type(init_range_low)} found.") + + self.init_range_low = init_range_low + self.init_range_high = init_range_high + + # Validate gene_type + if gene_type in GA.supported_int_float_types: + self.gene_type = [gene_type, None] + self.gene_type_single = True + # A single data type of float with precision. + elif len(gene_type) == 2 and gene_type[0] in GA.supported_float_types and (type(gene_type[1]) in GA.supported_int_types or gene_type[1] is None): + self.gene_type = gene_type + self.gene_type_single = True + # A single data type of integer with precision None ([int, None]). + elif len(gene_type) == 2 and gene_type[0] in GA.supported_int_types and gene_type[1] is None: + self.gene_type = gene_type + self.gene_type_single = True + # Raise an exception for a single data type of int with integer precision. + elif len(gene_type) == 2 and gene_type[0] in GA.supported_int_types and (type(gene_type[1]) in GA.supported_int_types or gene_type[1] is None): + self.gene_type_single = False + raise ValueError(f"Integers cannot have precision. Please use the integer data type directly instead of {gene_type}.") + elif type(gene_type) in [list, tuple, numpy.ndarray]: + # Get the number of genes before validating the num_genes parameter. + if num_genes is None: + if initial_population is None: + self.valid_parameters = False + raise TypeError("When the parameter 'initial_population' is None, then the 2 parameters 'sol_per_pop' and 'num_genes' cannot be None too.") + elif not len(gene_type) == len(initial_population[0]): + self.valid_parameters = False + raise ValueError(f"When the parameter 'gene_type' is nested, then it can be either [float, int] or with length equal to the number of genes parameter. Instead, value {gene_type} with len(gene_type) ({len(gene_type)}) != number of genes ({len(initial_population[0])}) found.") + elif not len(gene_type) == num_genes: + self.valid_parameters = False + raise ValueError(f"When the parameter 'gene_type' is nested, then it can be either [float, int] or with length equal to the value passed to the 'num_genes' parameter. Instead, value {gene_type} with len(gene_type) ({len(gene_type)}) != len(num_genes) ({num_genes}) found.") + for gene_type_idx, gene_type_val in enumerate(gene_type): + if gene_type_val in GA.supported_int_float_types: + # If the gene type is float and no precision is passed or an integer, set its precision to None. + gene_type[gene_type_idx] = [gene_type_val, None] + elif type(gene_type_val) in [list, tuple, numpy.ndarray]: + # A float type is expected in a list/tuple/numpy.ndarray of length 2. + if len(gene_type_val) == 2: + if gene_type_val[0] in GA.supported_float_types: + if type(gene_type_val[1]) in GA.supported_int_types: + pass + else: + self.valid_parameters = False + raise TypeError(f"In the 'gene_type' parameter, the precision for float gene data types must be an integer but the element {gene_type_val} at index {gene_type_idx} has a precision of {gene_type_val[1]} with type {gene_type_val[0]}.") + elif gene_type_val[0] in GA.supported_int_types: + if gene_type_val[1] is None: + pass + else: + self.valid_parameters = False + raise TypeError(f"In the 'gene_type' parameter, either do not set a precision for integer data types or set it to None. But the element {gene_type_val} at index {gene_type_idx} has a precision of {gene_type_val[1]} with type {gene_type_val[0]}.") + else: + self.valid_parameters = False + raise TypeError( + f"In the 'gene_type' parameter, a precision is expected only for float gene data types but the element {gene_type_val} found at index {gene_type_idx}.\nNote that the data type must be at index 0 of the item followed by precision at index 1.") + else: + self.valid_parameters = False + raise ValueError(f"In the 'gene_type' parameter, a precision is specified in a list/tuple/numpy.ndarray of length 2 but value ({gene_type_val}) of type {type(gene_type_val)} with length {len(gene_type_val)} found at index {gene_type_idx}.") + else: + self.valid_parameters = False + raise ValueError(f"When a list/tuple/numpy.ndarray is assigned to the 'gene_type' parameter, then its elements must be of integer, floating-point, list, tuple, or numpy.ndarray data types but the value ({gene_type_val}) of type {type(gene_type_val)} found at index {gene_type_idx}.") + self.gene_type = gene_type + self.gene_type_single = False + 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) + + # Build the initial population + if initial_population is None: + if (sol_per_pop is None) or (num_genes is None): + self.valid_parameters = False + raise TypeError("Error creating the initial population:\n\nWhen the parameter 'initial_population' is None, then the 2 parameters 'sol_per_pop' and 'num_genes' cannot be None too.\nThere are 2 options to prepare the initial population:\n1) Assinging the initial population to the 'initial_population' parameter. In this case, the values of the 2 parameters sol_per_pop and num_genes will be deduced.\n2) Assign integer values to the 'sol_per_pop' and 'num_genes' parameters so that PyGAD can create the initial population automatically.") + elif (type(sol_per_pop) is int) and (type(num_genes) is int): + # Validating the number of solutions in the population (sol_per_pop) + if sol_per_pop <= 0: + self.valid_parameters = False + raise ValueError(f"The number of solutions in the population (sol_per_pop) must be > 0 but ({sol_per_pop}) 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 gene. + if (num_genes <= 0): + self.valid_parameters = False + raise ValueError(f"The number of genes cannot be <= 0 but ({num_genes}) found.\n") + # When initial_population=None and the 2 parameters sol_per_pop and num_genes have valid integer values, then the initial population is created. + # Inside the initialize_population() method, the initial_population attribute is assigned to keep the initial population accessible. + self.num_genes = num_genes # Number of genes in the solution. + + # 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})") + + # Number of solutions in the population. + self.sol_per_pop = sol_per_pop + self.initialize_population(low=self.init_range_low, + high=self.init_range_high, + allow_duplicate_genes=allow_duplicate_genes, + mutation_by_replacement=True, + gene_type=self.gene_type) + else: + self.valid_parameters = False + raise TypeError(f"The expected type of both the sol_per_pop and num_genes parameters is int but {type(sol_per_pop)} and {type(num_genes)} found.") + elif not type(initial_population) in [list, tuple, numpy.ndarray]: + self.valid_parameters = False + raise TypeError(f"The value assigned to the 'initial_population' parameter is expected to by of type list, tuple, or ndarray but {type(initial_population)} found.") + elif numpy.array(initial_population).ndim != 2: + self.valid_parameters = False + raise ValueError(f"A 2D list is expected to the initial_population parameter but a ({numpy.array(initial_population).ndim}-D) list found.") + else: + # Validate the type of each value in the 'initial_population' parameter. + for row_idx in range(len(initial_population)): + for col_idx in range(len(initial_population[0])): + if type(initial_population[row_idx][col_idx]) in GA.supported_int_float_types: + pass + else: + self.valid_parameters = False + raise TypeError(f"The values in the initial population can be integers or floats but the value ({initial_population[row_idx][col_idx]}) of type {type(initial_population[row_idx][col_idx])} found.") + + # Forcing the initial_population array to have the data type assigned to the gene_type parameter. + if self.gene_type_single == True: + if self.gene_type[1] == None: + self.initial_population = numpy.array(initial_population, + dtype=self.gene_type[0]) + else: + # This block is reached only for non-integer data types (i.e. float). + self.initial_population = numpy.round(numpy.array(initial_population, + dtype=self.gene_type[0]), + self.gene_type[1]) + else: + initial_population = numpy.array(initial_population) + self.initial_population = numpy.zeros(shape=(initial_population.shape[0], + initial_population.shape[1]), + dtype=object) + for gene_idx in range(initial_population.shape[1]): + if self.gene_type[gene_idx][1] is None: + self.initial_population[:, gene_idx] = numpy.asarray(initial_population[:, gene_idx], + dtype=self.gene_type[gene_idx][0]) + else: + # This block is reached only for non-integer data types (i.e. float). + self.initial_population[:, gene_idx] = numpy.round(numpy.asarray(initial_population[:, gene_idx], + dtype=self.gene_type[gene_idx][0]), + self.gene_type[gene_idx][1]) + + # Check if duplicates are allowed. If not, then solve any exisiting duplicates in the passed initial population. + if self.allow_duplicate_genes == False: + for initial_solution_idx, initial_solution in enumerate(self.initial_population): + if self.gene_space is None: + self.initial_population[initial_solution_idx], _, _ = self.solve_duplicate_genes_randomly(solution=initial_solution, + min_val=self.init_range_low, + max_val=self.init_range_high, + mutation_by_replacement=self.mutation_by_replacement, + gene_type=self.gene_type, + num_trials=10) + else: + self.initial_population[initial_solution_idx], _, _ = self.solve_duplicate_genes_by_space(solution=initial_solution, + gene_type=self.gene_type, + num_trials=10) + + # A NumPy array holding the initial population. + self.population = self.initial_population.copy() + # Number of genes in the solution. + self.num_genes = self.initial_population.shape[1] + # Number of solutions in the population. + self.sol_per_pop = self.initial_population.shape[0] + # The population size. + self.pop_size = (self.sol_per_pop, self.num_genes) + + # Round initial_population and population + self.initial_population = self.round_genes(self.initial_population) + self.population = self.round_genes(self.population) + + # 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})") + + # Validate random_mutation_min_val and random_mutation_max_val + if type(random_mutation_min_val) in GA.supported_int_float_types: + if type(random_mutation_max_val) in GA.supported_int_float_types: + if random_mutation_min_val == random_mutation_max_val: + if not self.suppress_warnings: + warnings.warn("The values of the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val' are equal and this might cause a fixed mutation to some genes.") + else: + self.valid_parameters = False + raise TypeError(f"Type mismatch between the 2 parameters 'random_mutation_min_val' {type(random_mutation_min_val)} and 'random_mutation_max_val' {type(random_mutation_max_val)}.") + elif type(random_mutation_min_val) in [list, tuple, numpy.ndarray]: + if len(random_mutation_min_val) == self.num_genes: + pass + else: + self.valid_parameters = False + raise ValueError(f"The length of the 'random_mutation_min_val' parameter is {len(random_mutation_min_val)} which is different from the number of genes {self.num_genes}.") + if type(random_mutation_max_val) in [list, tuple, numpy.ndarray]: + if len(random_mutation_min_val) == len(random_mutation_max_val): + pass + else: + self.valid_parameters = False + raise ValueError(f"Size mismatch between the 2 parameters 'random_mutation_min_val' {len(random_mutation_min_val)} and 'random_mutation_max_val' {len(random_mutation_max_val)}.") + + # Validate the values in random_mutation_min_val + for val in random_mutation_min_val: + if type(val) in GA.supported_int_float_types: + pass + else: + self.valid_parameters = False + raise TypeError(f"When an iterable (list/tuple/numpy.ndarray) is assigned to the 'random_mutation_min_val' parameter, its elements must be numeric but the value {val} of type {type(val)} found.") + + # Validate the values in random_mutation_max_val + for val in random_mutation_max_val: + if type(val) in GA.supported_int_float_types: + pass + else: + self.valid_parameters = False + raise TypeError(f"When an iterable (list/tuple/numpy.ndarray) is assigned to the 'random_mutation_max_val' parameter, its elements must be numeric but the value {val} of type {type(val)} found.") + else: + self.valid_parameters = False + raise TypeError(f"Type mismatch between the 2 parameters 'random_mutation_min_val' {type(random_mutation_min_val)} and 'random_mutation_max_val' {type(random_mutation_max_val)}.") + else: + self.valid_parameters = False + raise TypeError(f"The expected type of the 'random_mutation_min_val' parameter is numeric or list/tuple/numpy.ndarray but {type(random_mutation_min_val)} found.") + + self.random_mutation_min_val = random_mutation_min_val + self.random_mutation_max_val = random_mutation_max_val + + # 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 + + # 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): + self.crossover = None + elif inspect.ismethod(crossover_type): + # Check if the crossover_type is a method that accepts 4 paramaters. + if (crossover_type.__code__.co_argcount == 4): + # The crossover method assigned to the crossover_type parameter is validated. + self.crossover = crossover_type + else: + self.valid_parameters = False + raise ValueError(f"When 'crossover_type' is assigned to a method, then this crossover method must accept 4 parameters:\n1) Expected to be the 'self' object.\n2) The selected parents.\n3) The size of the offspring to be produced.\n4) The instance from the pygad.GA class.\n\nThe passed crossover method named '{crossover_type.__code__.co_name}' accepts {crossover_type.__code__.co_argcount} parameter(s).") + elif callable(crossover_type): + # Check if the crossover_type is a function that accepts 2 paramaters. + if (crossover_type.__code__.co_argcount == 3): + # The crossover function assigned to the crossover_type parameter is validated. + self.crossover = crossover_type + else: + self.valid_parameters = False + raise ValueError(f"When 'crossover_type' is assigned to a function, then this crossover function must accept 3 parameters:\n1) The selected parents.\n2) The size of the offspring to be produced.3) The instance from the pygad.GA class to retrieve any property like population, gene data type, gene space, etc.\n\nThe passed crossover function named '{crossover_type.__code__.co_name}' accepts {crossover_type.__code__.co_argcount} parameter(s).") + elif not (type(crossover_type) is str): + self.valid_parameters = False + raise TypeError(f"The expected type of the 'crossover_type' parameter is either callable or str but {type(crossover_type)} found.") + else: # type crossover_type is str + crossover_type = crossover_type.lower() + if (crossover_type == "single_point"): + self.crossover = self.single_point_crossover + elif (crossover_type == "two_points"): + self.crossover = self.two_points_crossover + elif (crossover_type == "uniform"): + self.crossover = self.uniform_crossover + elif (crossover_type == "scattered"): + self.crossover = self.scattered_crossover + else: + self.valid_parameters = False + raise TypeError(f"Undefined crossover type. \nThe assigned value to the crossover_type ({crossover_type}) parameter does not refer to one of the supported crossover types which are: \n-single_point (for single point crossover)\n-two_points (for two points crossover)\n-uniform (for uniform crossover)\n-scattered (for scattered crossover).\n") + + self.crossover_type = crossover_type + + # Calculate the value of crossover_probability + if crossover_probability is None: + self.crossover_probability = None + elif type(crossover_probability) in GA.supported_int_float_types: + if crossover_probability >= 0 and crossover_probability <= 1: + self.crossover_probability = crossover_probability + else: + self.valid_parameters = False + raise ValueError(f"The value assigned to the 'crossover_probability' parameter must be between 0 and 1 inclusive but ({crossover_probability}) found.") + 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.") + + # 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 + if mutation_type is None: + self.mutation = None + elif inspect.ismethod(mutation_type): + # Check if the mutation_type is a method that accepts 3 paramater. + if (mutation_type.__code__.co_argcount == 3): + # The mutation method assigned to the mutation_type parameter is validated. + self.mutation = mutation_type + else: + self.valid_parameters = False + raise ValueError(f"When 'mutation_type' is assigned to a method, then it must accept 3 parameters:\n1) Expected to be the 'self' object.\n2) The offspring to be mutated.\n3) The instance from the pygad.GA class.\n\nThe passed mutation method named '{mutation_type.__code__.co_name}' accepts {mutation_type.__code__.co_argcount} parameter(s).") + elif callable(mutation_type): + # Check if the mutation_type is a function that accepts 2 paramater. + if (mutation_type.__code__.co_argcount == 2): + # The mutation function assigned to the mutation_type parameter is validated. + self.mutation = mutation_type + else: + self.valid_parameters = False + raise ValueError(f"When 'mutation_type' is assigned to a function, then this mutation function must accept 2 parameters:\n1) The offspring to be mutated.\n2) The instance from the pygad.GA class to retrieve any property like population, gene data type, gene space, etc.\n\nThe passed mutation function named '{mutation_type.__code__.co_name}' accepts {mutation_type.__code__.co_argcount} parameter(s).") + elif not (type(mutation_type) is str): + self.valid_parameters = False + raise TypeError(f"The expected type of the 'mutation_type' parameter is either callable or str but {type(mutation_type)} found.") + else: # type mutation_type is str + mutation_type = mutation_type.lower() + if (mutation_type == "random"): + self.mutation = self.random_mutation + elif (mutation_type == "swap"): + self.mutation = self.swap_mutation + elif (mutation_type == "scramble"): + self.mutation = self.scramble_mutation + elif (mutation_type == "inversion"): + self.mutation = self.inversion_mutation + elif (mutation_type == "adaptive"): + self.mutation = self.adaptive_mutation + else: + self.valid_parameters = False + raise TypeError(f"Undefined mutation type. \nThe assigned string value to the 'mutation_type' parameter ({mutation_type}) does not refer to one of the supported mutation types which are: \n-random (for random mutation)\n-swap (for swap mutation)\n-inversion (for inversion mutation)\n-scramble (for scramble mutation)\n-adaptive (for adaptive mutation).\n") + + self.mutation_type = mutation_type + + # Calculate the value of mutation_probability + if not (self.mutation_type is None): + if mutation_probability is None: + self.mutation_probability = None + elif (mutation_type != "adaptive"): + # Mutation probability is fixed not adaptive. + if type(mutation_probability) in GA.supported_int_float_types: + if mutation_probability >= 0 and mutation_probability <= 1: + self.mutation_probability = mutation_probability + else: + self.valid_parameters = False + raise ValueError(f"The value assigned to the 'mutation_probability' parameter must be between 0 and 1 inclusive but ({mutation_probability}) found.") + else: + self.valid_parameters = False + raise TypeError(f"Unexpected type for the 'mutation_probability' parameter. A numeric value is expected but ({mutation_probability}) of type {type(mutation_probability)} found.") + else: + # Mutation probability is adaptive not fixed. + if type(mutation_probability) in [list, tuple, numpy.ndarray]: + if len(mutation_probability) == 2: + for el in mutation_probability: + if type(el) in GA.supported_int_float_types: + if el >= 0 and el <= 1: + pass + else: + self.valid_parameters = False + raise ValueError(f"The values assigned to the 'mutation_probability' parameter must be between 0 and 1 inclusive but ({el}) found.") + else: + self.valid_parameters = False + raise TypeError(f"Unexpected type for a value assigned to the 'mutation_probability' parameter. A numeric value is expected but ({el}) of type {type(el)} found.") + if mutation_probability[0] < mutation_probability[1]: + if not self.suppress_warnings: + warnings.warn(f"The first element in the 'mutation_probability' parameter is {mutation_probability[0]} which is smaller than the second element {mutation_probability[1]}. This means the mutation rate for the high-quality solutions is higher than the mutation rate of the low-quality ones. This causes high disruption in the high qualitiy solutions while making little changes in the low quality solutions. Please make the first element higher than the second element.") + self.mutation_probability = mutation_probability + else: + self.valid_parameters = False + raise ValueError(f"When mutation_type='adaptive', then the 'mutation_probability' parameter must have only 2 elements but ({len(mutation_probability)}) element(s) found.") + else: + self.valid_parameters = False + raise TypeError(f"Unexpected type for the 'mutation_probability' parameter. When mutation_type='adaptive', then list/tuple/numpy.ndarray is expected but ({mutation_probability}) of type {type(mutation_probability)} found.") + else: + pass + + # Calculate the value of mutation_num_genes + if not (self.mutation_type is None): + if mutation_num_genes is None: + # The mutation_num_genes parameter does not exist. Checking whether adaptive mutation is used. + if (mutation_type != "adaptive"): + # The percent of genes to mutate is fixed not adaptive. + if mutation_percent_genes == 'default'.lower(): + mutation_percent_genes = 10 + # Based on the mutation percentage in the 'mutation_percent_genes' parameter, the number of genes to mutate is calculated. + mutation_num_genes = numpy.uint32( + (mutation_percent_genes*self.num_genes)/100) + # Based on the mutation percentage of genes, if the number of selected genes for mutation is less than the least possible value which is 1, then the number will be set to 1. + if mutation_num_genes == 0: + if self.mutation_probability is None: + if not self.suppress_warnings: + warnings.warn( + f"The percentage of genes to mutate (mutation_percent_genes={mutation_percent_genes}) resulted in selecting ({mutation_num_genes}) genes. The number of genes to mutate is set to 1 (mutation_num_genes=1).\nIf you do not want to mutate any gene, please set mutation_type=None.") + mutation_num_genes = 1 + + elif type(mutation_percent_genes) in GA.supported_int_float_types: + if (mutation_percent_genes <= 0 or mutation_percent_genes > 100): + self.valid_parameters = False + raise ValueError(f"The percentage of selected genes for mutation (mutation_percent_genes) must be > 0 and <= 100 but ({mutation_percent_genes}) found.\n") + else: + # If mutation_percent_genes equals the string "default", then it is replaced by the numeric value 10. + if mutation_percent_genes == 'default'.lower(): + mutation_percent_genes = 10 + + # Based on the mutation percentage in the 'mutation_percent_genes' parameter, the number of genes to mutate is calculated. + mutation_num_genes = numpy.uint32( + (mutation_percent_genes*self.num_genes)/100) + # Based on the mutation percentage of genes, if the number of selected genes for mutation is less than the least possible value which is 1, then the number will be set to 1. + if mutation_num_genes == 0: + if self.mutation_probability is None: + if not self.suppress_warnings: + warnings.warn(f"The percentage of genes to mutate (mutation_percent_genes={mutation_percent_genes}) resulted in selecting ({mutation_num_genes}) genes. The number of genes to mutate is set to 1 (mutation_num_genes=1).\nIf you do not want to mutate any gene, please set mutation_type=None.") + mutation_num_genes = 1 + else: + self.valid_parameters = False + raise TypeError(f"Unexpected value or type of the 'mutation_percent_genes' parameter. It only accepts the string 'default' or a numeric value but ({mutation_percent_genes}) of type {type(mutation_percent_genes)} found.") + else: + # The percent of genes to mutate is adaptive not fixed. + if type(mutation_percent_genes) in [list, tuple, numpy.ndarray]: + if len(mutation_percent_genes) == 2: + mutation_num_genes = numpy.zeros_like( + mutation_percent_genes, dtype=numpy.uint32) + for idx, el in enumerate(mutation_percent_genes): + if type(el) in GA.supported_int_float_types: + if (el <= 0 or el > 100): + self.valid_parameters = False + raise ValueError(f"The values assigned to the 'mutation_percent_genes' must be > 0 and <= 100 but ({mutation_percent_genes}) found.\n") + else: + self.valid_parameters = False + raise TypeError(f"Unexpected type for a value assigned to the 'mutation_percent_genes' parameter. An integer value is expected but ({el}) of type {type(el)} found.") + # At this point of the loop, the current value assigned to the parameter 'mutation_percent_genes' is validated. + # Based on the mutation percentage in the 'mutation_percent_genes' parameter, the number of genes to mutate is calculated. + mutation_num_genes[idx] = numpy.uint32( + (mutation_percent_genes[idx]*self.num_genes)/100) + # Based on the mutation percentage of genes, if the number of selected genes for mutation is less than the least possible value which is 1, then the number will be set to 1. + if mutation_num_genes[idx] == 0: + if not self.suppress_warnings: + warnings.warn(f"The percentage of genes to mutate ({mutation_percent_genes[idx]}) resulted in selecting ({mutation_num_genes[idx]}) genes. The number of genes to mutate is set to 1 (mutation_num_genes=1).\nIf you do not want to mutate any gene, please set mutation_type=None.") + mutation_num_genes[idx] = 1 + if mutation_percent_genes[0] < mutation_percent_genes[1]: + if not self.suppress_warnings: + warnings.warn(f"The first element in the 'mutation_percent_genes' parameter is ({mutation_percent_genes[0]}) which is smaller than the second element ({mutation_percent_genes[1]}).\nThis means the mutation rate for the high-quality solutions is higher than the mutation rate of the low-quality ones. This causes high disruption in the high qualitiy solutions while making little changes in the low quality solutions.\nPlease make the first element higher than the second element.") + # At this point outside the loop, all values of the parameter 'mutation_percent_genes' are validated. Eveyrthing is OK. + else: + self.valid_parameters = False + raise ValueError(f"When mutation_type='adaptive', then the 'mutation_percent_genes' parameter must have only 2 elements but ({len(mutation_percent_genes)}) element(s) found.") + else: + if self.mutation_probability is None: + self.valid_parameters = False + raise TypeError(f"Unexpected type of the 'mutation_percent_genes' parameter. When mutation_type='adaptive', then the 'mutation_percent_genes' parameter should exist and assigned a list/tuple/numpy.ndarray with 2 values but ({mutation_percent_genes}) found.") + # The mutation_num_genes parameter exists. Checking whether adaptive mutation is used. + elif (mutation_type != "adaptive"): + # Number of genes to mutate is fixed not adaptive. + if type(mutation_num_genes) in GA.supported_int_types: + if (mutation_num_genes <= 0): + self.valid_parameters = False + raise ValueError(f"The number of selected genes for mutation (mutation_num_genes) cannot be <= 0 but ({mutation_num_genes}) found. If you do not want to use mutation, please set mutation_type=None\n") + elif (mutation_num_genes > self.num_genes): + self.valid_parameters = False + raise ValueError(f"The number of selected genes for mutation (mutation_num_genes), which is ({mutation_num_genes}), cannot be greater than the number of genes ({self.num_genes}).\n") + else: + self.valid_parameters = False + raise TypeError(f"The 'mutation_num_genes' parameter is expected to be a positive integer but the value ({mutation_num_genes}) of type {type(mutation_num_genes)} found.\n") + else: + # Number of genes to mutate is adaptive not fixed. + if type(mutation_num_genes) in [list, tuple, numpy.ndarray]: + if len(mutation_num_genes) == 2: + for el in mutation_num_genes: + if type(el) in GA.supported_int_types: + if (el <= 0): + self.valid_parameters = False + raise ValueError(f"The values assigned to the 'mutation_num_genes' cannot be <= 0 but ({el}) found. If you do not want to use mutation, please set mutation_type=None\n") + elif (el > self.num_genes): + self.valid_parameters = False + raise ValueError(f"The values assigned to the 'mutation_num_genes' cannot be greater than the number of genes ({self.num_genes}) but ({el}) found.\n") + else: + self.valid_parameters = False + raise TypeError(f"Unexpected type for a value assigned to the 'mutation_num_genes' parameter. An integer value is expected but ({el}) of type {type(el)} found.") + # At this point of the loop, the current value assigned to the parameter 'mutation_num_genes' is validated. + if mutation_num_genes[0] < mutation_num_genes[1]: + if not self.suppress_warnings: + warnings.warn(f"The first element in the 'mutation_num_genes' parameter is {mutation_num_genes[0]} which is smaller than the second element {mutation_num_genes[1]}. This means the mutation rate for the high-quality solutions is higher than the mutation rate of the low-quality ones. This causes high disruption in the high qualitiy solutions while making little changes in the low quality solutions. Please make the first element higher than the second element.") + # At this point outside the loop, all values of the parameter 'mutation_num_genes' are validated. Eveyrthing is OK. + else: + self.valid_parameters = False + raise ValueError(f"When mutation_type='adaptive', then the 'mutation_num_genes' parameter must have only 2 elements but ({len(mutation_num_genes)}) element(s) found.") + else: + self.valid_parameters = False + raise TypeError(f"Unexpected type for the 'mutation_num_genes' parameter. When mutation_type='adaptive', then list/tuple/numpy.ndarray is expected but ({mutation_num_genes}) of type {type(mutation_num_genes)} found.") + else: + pass + + # Validating mutation_by_replacement and mutation_type + if self.mutation_type != "random" and self.mutation_by_replacement: + if not self.suppress_warnings: + warnings.warn(f"The mutation_by_replacement parameter is set to True while the mutation_type parameter is not set to random but ({mutation_type}). Note that the mutation_by_replacement parameter has an effect only when mutation_type='random'.") + + # 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.") + + # 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): + # Check if the parent_selection_type is a method that accepts 4 paramaters. + if (parent_selection_type.__code__.co_argcount == 4): + # population: Added in PyGAD 2.16.0. It should used only to support custom parent selection functions. Otherwise, it should be left to None to retirve the population by self.population. + # The parent selection method assigned to the parent_selection_type parameter is validated. + self.select_parents = parent_selection_type + else: + self.valid_parameters = False + raise ValueError(f"When 'parent_selection_type' is assigned to a method, then it must accept 4 parameters:\n1) Expected to be the 'self' object.\n2) The fitness values of the current population.\n3) The number of parents needed.\n4) The instance from the pygad.GA class.\n\nThe passed parent selection method named '{parent_selection_type.__code__.co_name}' accepts {parent_selection_type.__code__.co_argcount} parameter(s).") + elif callable(parent_selection_type): + # Check if the parent_selection_type is a function that accepts 3 paramaters. + if (parent_selection_type.__code__.co_argcount == 3): + # population: Added in PyGAD 2.16.0. It should used only to support custom parent selection functions. Otherwise, it should be left to None to retirve the population by self.population. + # The parent selection function assigned to the parent_selection_type parameter is validated. + self.select_parents = parent_selection_type + else: + self.valid_parameters = False + raise ValueError(f"When 'parent_selection_type' is assigned to a user-defined function, then this parent selection function 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 to retrieve any property like population, gene data type, gene space, etc.\n\nThe passed parent selection function named '{parent_selection_type.__code__.co_name}' accepts {parent_selection_type.__code__.co_argcount} parameter(s).") + elif not (type(parent_selection_type) is str): + self.valid_parameters = False + + raise TypeError(f"The expected type of the 'parent_selection_type' parameter is either callable or str but {type(parent_selection_type)} found.") + else: + parent_selection_type = parent_selection_type.lower() + if (parent_selection_type == "sss"): + self.select_parents = self.steady_state_selection + elif (parent_selection_type == "rws"): + self.select_parents = self.roulette_wheel_selection + elif (parent_selection_type == "sus"): + self.select_parents = self.stochastic_universal_selection + elif (parent_selection_type == "random"): + self.select_parents = self.random_selection + elif (parent_selection_type == "tournament"): + self.select_parents = self.tournament_selection + elif (parent_selection_type == "tournament_nsga2"): # Supported in PyGAD >= 3.2 + self.select_parents = self.tournament_selection_nsga2 + elif (parent_selection_type == "nsga2"): # Supported in PyGAD >= 3.2 + self.select_parents = self.nsga2_selection + elif (parent_selection_type == "rank"): + self.select_parents = self.rank_selection + else: + self.valid_parameters = False + raise TypeError(f"Undefined parent selection type: {parent_selection_type}. \nThe assigned value to the 'parent_selection_type' parameter does not refer to one of the supported parent selection techniques which are: \n-sss (steady state selection)\n-rws (roulette wheel selection)\n-sus (stochastic universal selection)\n-rank (rank selection)\n-random (random selection)\n-tournament (tournament selection)\n-tournament_nsga2: (Tournament selection for NSGA-II)\n-nsga2: (NSGA-II parent selection).\n") + + # For tournament selection, validate the K value. + if (parent_selection_type == "tournament"): + if (K_tournament > self.sol_per_pop): + K_tournament = self.sol_per_pop + if not self.suppress_warnings: + warnings.warn(f"K of the tournament selection ({K_tournament}) should not be greater than the number of solutions within the population ({self.sol_per_pop}).\nK will be clipped to be equal to the number of solutions in the population (sol_per_pop).\n") + elif (K_tournament <= 0): + self.valid_parameters = False + raise ValueError(f"K of the tournament selection cannot be <=0 but ({K_tournament}) found.\n") + + self.K_tournament = K_tournament + + # Validating the number of parents to keep in the next population: keep_parents + if not (type(keep_parents) in GA.supported_int_types): + self.valid_parameters = False + raise TypeError(f"Incorrect type of the value assigned to the keep_parents parameter. The value ({keep_parents}) of type {type(keep_parents)} found but an integer is expected.") + elif (keep_parents > self.sol_per_pop or keep_parents > self.num_parents_mating or keep_parents < -1): + self.valid_parameters = False + raise ValueError(f"Incorrect value to the keep_parents parameter: {keep_parents}. \nThe assigned value to the keep_parent parameter must satisfy the following conditions: \n1) Less than or equal to sol_per_pop\n2) Less than or equal to num_parents_mating\n3) Greater than or equal to -1.") + + self.keep_parents = keep_parents + + if parent_selection_type == "sss" and self.keep_parents == 0: + if not self.suppress_warnings: + warnings.warn("The steady-state parent (sss) selection operator is used despite that no parents are kept in the next generation.") + + # Validating the number of elitism to keep in the next population: keep_elitism + if not (type(keep_elitism) in GA.supported_int_types): + self.valid_parameters = False + raise TypeError(f"Incorrect type of the value assigned to the keep_elitism parameter. The value ({keep_elitism}) of type {type(keep_elitism)} found but an integer is expected.") + elif (keep_elitism > self.sol_per_pop or keep_elitism < 0): + self.valid_parameters = False + raise ValueError(f"Incorrect value to the keep_elitism parameter: {keep_elitism}. \nThe assigned value to the keep_elitism parameter must satisfy the following conditions: \n1) Less than or equal to sol_per_pop\n2) Greater than or equal to 0.") + + self.keep_elitism = keep_elitism + + # Validate keep_parents. + if self.keep_elitism == 0: + # Keep all parents in the next population. + if (self.keep_parents == -1): + self.num_offspring = self.sol_per_pop - self.num_parents_mating + # Keep no parents in the next population. + elif (self.keep_parents == 0): + self.num_offspring = self.sol_per_pop + # Keep the specified number of parents in the next population. + elif (self.keep_parents > 0): + self.num_offspring = self.sol_per_pop - self.keep_parents + else: + self.num_offspring = self.sol_per_pop - self.keep_elitism + + # Check if the fitness_func is a method. + # In PyGAD 2.19.0, a method can be passed to the fitness function. If function is passed, then it accepts 2 parameters. If method, then it accepts 3 parameters. + # In PyGAD 2.20.0, a new parameter is passed referring to the instance of the `pygad.GA` class. So, the function accepts 3 parameters and the method accepts 4 parameters. + if inspect.ismethod(fitness_func): + # If the fitness is calculated through a method, not a function, then there is a fourth 'self` paramaters. + if (fitness_func.__code__.co_argcount == 4): + self.fitness_func = fitness_func + else: + self.valid_parameters = False + raise ValueError(f"In PyGAD 2.20.0, if a method is used to calculate the fitness value, then it must accept 4 parameters\n1) Expected to be the 'self' object.\n2) The instance of the 'pygad.GA' class.\n3) A solution to calculate its fitness value.\n4) The solution's index within the population.\n\nThe passed fitness method named '{fitness_func.__code__.co_name}' accepts {fitness_func.__code__.co_argcount} parameter(s).") + elif callable(fitness_func): + # Check if the fitness function accepts 2 paramaters. + if (fitness_func.__code__.co_argcount == 3): + self.fitness_func = fitness_func + else: + self.valid_parameters = False + raise ValueError(f"In PyGAD 2.20.0, the fitness function must accept 3 parameters:\n1) The instance of the 'pygad.GA' class.\n2) A solution to calculate its fitness value.\n3) The solution's index within the population.\n\nThe passed fitness function named '{fitness_func.__code__.co_name}' accepts {fitness_func.__code__.co_argcount} parameter(s).") + else: + self.valid_parameters = False + + raise TypeError(f"The value assigned to the fitness_func parameter is expected to be of type function but {type(fitness_func)} found.") + + if fitness_batch_size is None: + pass + elif not (type(fitness_batch_size) in GA.supported_int_types): + self.valid_parameters = False + raise TypeError(f"The value assigned to the fitness_batch_size parameter is expected to be integer but the value ({fitness_batch_size}) of type {type(fitness_batch_size)} found.") + elif fitness_batch_size <= 0 or fitness_batch_size > self.sol_per_pop: + self.valid_parameters = False + 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 + + # Check if the on_start exists. + if not (on_start is None): + if inspect.ismethod(on_start): + # Check if the on_start method accepts 2 paramaters. + if (on_start.__code__.co_argcount == 2): + 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) Expected to be the 'self' object.\n2) The instance of the genetic algorithm.\nThe passed method named '{on_start.__code__.co_name}' accepts {on_start.__code__.co_argcount} parameter(s).") + # Check if the on_start is a function. + elif callable(on_start): + # Check if the on_start function accepts only a single paramater. + if (on_start.__code__.co_argcount == 1): + self.on_start = on_start + else: + self.valid_parameters = False + raise ValueError(f"The function assigned to the on_start parameter must accept only 1 parameter representing the instance of the genetic algorithm.\nThe passed function named '{on_start.__code__.co_name}' accepts {on_start.__code__.co_argcount} parameter(s).") + else: + self.valid_parameters = False + + raise TypeError(f"The value assigned to the on_start parameter is expected to be of type function but {type(on_start)} found.") + else: + self.on_start = None + + # Check if the on_fitness exists. + if not (on_fitness is None): + # Check if the on_fitness is a method. + if inspect.ismethod(on_fitness): + # Check if the on_fitness method accepts 3 paramaters. + if (on_fitness.__code__.co_argcount == 3): + self.on_fitness = on_fitness + else: + self.valid_parameters = False + raise ValueError(f"The method assigned to the on_fitness parameter must accept 3 parameters:\n1) Expected to be the 'self' object.\n2) The instance of the genetic algorithm.3) The fitness values of all solutions.\nThe passed method named '{on_fitness.__code__.co_name}' accepts {on_fitness.__code__.co_argcount} parameter(s).") + # Check if the on_fitness is a function. + elif callable(on_fitness): + # Check if the on_fitness function accepts 2 paramaters. + if (on_fitness.__code__.co_argcount == 2): + self.on_fitness = on_fitness + else: + self.valid_parameters = False + raise ValueError(f"The function assigned to the on_fitness parameter must accept 2 parameters representing the instance of the genetic algorithm and the fitness values of all solutions.\nThe passed function named '{on_fitness.__code__.co_name}' accepts {on_fitness.__code__.co_argcount} parameter(s).") + else: + self.valid_parameters = False + raise TypeError(f"The value assigned to the on_fitness parameter is expected to be of type function but {type(on_fitness)} found.") + else: + self.on_fitness = None + + # Check if the on_parents exists. + if not (on_parents is None): + # Check if the on_parents is a method. + if inspect.ismethod(on_parents): + # Check if the on_parents method accepts 3 paramaters. + if (on_parents.__code__.co_argcount == 3): + self.on_parents = on_parents + else: + self.valid_parameters = False + raise ValueError(f"The method assigned to the on_parents parameter must accept 3 parameters:\n1) Expected to be the 'self' object.\n2) The instance of the genetic algorithm.\n3) The fitness values of all solutions.\nThe passed method named '{on_parents.__code__.co_name}' accepts {on_parents.__code__.co_argcount} parameter(s).") + # Check if the on_parents is a function. + elif callable(on_parents): + # Check if the on_parents function accepts 2 paramaters. + if (on_parents.__code__.co_argcount == 2): + self.on_parents = on_parents + else: + self.valid_parameters = False + raise ValueError(f"The function assigned to the on_parents parameter must accept 2 parameters representing the instance of the genetic algorithm and the fitness values of all solutions.\nThe passed function named '{on_parents.__code__.co_name}' accepts {on_parents.__code__.co_argcount} parameter(s).") + else: + self.valid_parameters = False + raise TypeError(f"The value assigned to the on_parents parameter is expected to be of type function but {type(on_parents)} found.") + else: + self.on_parents = None + + # Check if the on_crossover exists. + if not (on_crossover is None): + # Check if the on_crossover is a method. + if inspect.ismethod(on_crossover): + # Check if the on_crossover method accepts 3 paramaters. + if (on_crossover.__code__.co_argcount == 3): + self.on_crossover = on_crossover + else: + self.valid_parameters = False + raise ValueError(f"The method assigned to the on_crossover parameter must accept 3 parameters:\n1) Expected to be the 'self' object.\n2) The instance of the genetic algorithm.\n2) The offspring generated using crossover.\nThe passed method named '{on_crossover.__code__.co_name}' accepts {on_crossover.__code__.co_argcount} parameter(s).") + # Check if the on_crossover is a function. + elif callable(on_crossover): + # Check if the on_crossover function accepts 2 paramaters. + if (on_crossover.__code__.co_argcount == 2): + self.on_crossover = on_crossover + else: + self.valid_parameters = False + raise ValueError(f"The function assigned to the on_crossover parameter must accept 2 parameters representing the instance of the genetic algorithm and the offspring generated using crossover.\nThe passed function named '{on_crossover.__code__.co_name}' accepts {on_crossover.__code__.co_argcount} parameter(s).") + else: + self.valid_parameters = False + raise TypeError(f"The value assigned to the on_crossover parameter is expected to be of type function but {type(on_crossover)} found.") + else: + self.on_crossover = None + + # Check if the on_mutation exists. + if not (on_mutation is None): + # Check if the on_mutation is a method. + if inspect.ismethod(on_mutation): + # Check if the on_mutation method accepts 3 paramaters. + if (on_mutation.__code__.co_argcount == 3): + self.on_mutation = on_mutation + else: + self.valid_parameters = False + raise ValueError(f"The method assigned to the on_mutation parameter must accept 3 parameters:\n1) Expected to be the 'self' object.\n2) The instance of the genetic algorithm.\n2) The offspring after applying the mutation operation.\nThe passed method named '{on_mutation.__code__.co_name}' accepts {on_mutation.__code__.co_argcount} parameter(s).") + # Check if the on_mutation is a function. + elif callable(on_mutation): + # Check if the on_mutation function accepts 2 paramaters. + if (on_mutation.__code__.co_argcount == 2): + self.on_mutation = on_mutation + else: + self.valid_parameters = False + raise ValueError(f"The function assigned to the on_mutation parameter must accept 2 parameters representing the instance of the genetic algorithm and the offspring after applying the mutation operation.\nThe passed function named '{on_mutation.__code__.co_name}' accepts {on_mutation.__code__.co_argcount} parameter(s).") + else: + self.valid_parameters = False + raise TypeError(f"The value assigned to the on_mutation parameter is expected to be of type function but {type(on_mutation)} found.") + else: + self.on_mutation = None + + # Check if the on_generation exists. + if not (on_generation is None): + # Check if the on_generation is a method. + if inspect.ismethod(on_generation): + # Check if the on_generation method accepts 2 paramaters. + if (on_generation.__code__.co_argcount == 2): + self.on_generation = on_generation + else: + self.valid_parameters = False + raise ValueError(f"The method assigned to the on_generation parameter must accept 2 parameters:\n1) Expected to be the 'self' object.\n2) The instance of the genetic algorithm.\nThe passed method named '{on_generation.__code__.co_name}' accepts {on_generation.__code__.co_argcount} parameter(s).") + # Check if the on_generation is a function. + elif callable(on_generation): + # Check if the on_generation function accepts only a single paramater. + if (on_generation.__code__.co_argcount == 1): + self.on_generation = on_generation + else: + self.valid_parameters = False + raise ValueError(f"The function assigned to the on_generation parameter must accept only 1 parameter representing the instance of the genetic algorithm.\nThe passed function named '{on_generation.__code__.co_name}' accepts {on_generation.__code__.co_argcount} parameter(s).") + else: + self.valid_parameters = False + raise TypeError(f"The value assigned to the on_generation parameter is expected to be of type function but {type(on_generation)} found.") + else: + self.on_generation = None + + # Check if the on_stop exists. + if not (on_stop is None): + # Check if the on_stop is a method. + if inspect.ismethod(on_stop): + # Check if the on_stop method accepts 3 paramaters. + if (on_stop.__code__.co_argcount == 3): + self.on_stop = on_stop + else: + self.valid_parameters = False + raise ValueError(f"The method assigned to the on_stop parameter must accept 3 parameters:\n1) Expected to be the 'self' object.\n2) The instance of the genetic algorithm.\n2) A list of the fitness values of the solutions in the last population.\nThe passed method named '{on_stop.__code__.co_name}' accepts {on_stop.__code__.co_argcount} parameter(s).") + # Check if the on_stop is a function. + elif callable(on_stop): + # Check if the on_stop function accepts 2 paramaters. + if (on_stop.__code__.co_argcount == 2): + self.on_stop = on_stop + else: + self.valid_parameters = False + raise ValueError(f"The function assigned to the on_stop parameter must accept 2 parameters representing the instance of the genetic algorithm and a list of the fitness values of the solutions in the last population.\nThe passed function named '{on_stop.__code__.co_name}' accepts {on_stop.__code__.co_argcount} parameter(s).") + else: + self.valid_parameters = False + raise TypeError(f"The value assigned to the 'on_stop' parameter is expected to be of type function but {type(on_stop)} found.") + 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_multi_stop_criteria(self, stop_word, number): + if stop_word == 'reach': + pass + else: + self.valid_parameters = False + raise ValueError(f"Passing multiple numbers following the keyword in the 'stop_criteria' parameter is expected only with the 'reach' keyword but the keyword ({stop_word}) found.") + + for idx, num in enumerate(number): + if num.replace(".", "").replace("-", "").isnumeric(): + number[idx] = float(num) + else: + self.valid_parameters = False + raise ValueError(f"The value(s) following the stop word in the 'stop_criteria' parameter must be numeric but the value ({num}) of type {type(num)} found.") + return number + + self.stop_criteria = [] + self.supported_stop_words = ["reach", "saturate"] + if stop_criteria is None: + # None: Stop after passing through all generations. + self.stop_criteria = None + elif type(stop_criteria) is str: + # reach_{target_fitness}: Stop if the target fitness value is reached. + # saturate_{num_generations}: Stop if the fitness value does not change (saturates) for the given number of generations. + criterion = stop_criteria.split("_") + stop_word = criterion[0] + # criterion[1] might be a single or multiple numbers. + number = criterion[1:] + if stop_word in self.supported_stop_words: + pass + else: + self.valid_parameters = False + raise ValueError(f"In the 'stop_criteria' parameter, the supported stop words are '{self.supported_stop_words}' but '{stop_word}' found.") + + if len(criterion) == 2: + # There is only a single number. + number = number[0] + if number.replace(".", "").replace("-", "").isnumeric(): + number = float(number) + else: + self.valid_parameters = False + raise ValueError(f"The value following the stop word in the 'stop_criteria' parameter must be a number but the value ({number}) of type {type(number)} found.") + + self.stop_criteria.append([stop_word, number]) + elif len(criterion) > 2: + number = validate_multi_stop_criteria(self, stop_word, number) + 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.") + + elif type(stop_criteria) in [list, tuple, numpy.ndarray]: + # Remove duplicate criterira by converting the list to a set then back to a list. + stop_criteria = list(set(stop_criteria)) + for idx, val in enumerate(stop_criteria): + if type(val) is str: + criterion = val.split("_") + stop_word = criterion[0] + number = criterion[1:] + if len(criterion) == 2: + # There is only a single number. + number = number[0] + if stop_word in self.supported_stop_words: + pass + else: + self.valid_parameters = False + raise ValueError(f"In the 'stop_criteria' parameter, the supported stop words are {self.supported_stop_words} but '{stop_word}' found.") + + if number.replace(".", "").replace("-", "").isnumeric(): + number = float(number) + else: + self.valid_parameters = False + raise ValueError(f"The value following the stop word in the 'stop_criteria' parameter must be a number but the value ({number}) of type {type(number)} found.") + + self.stop_criteria.append([stop_word, number]) + elif len(criterion) > 2: + number = validate_multi_stop_criteria(self, stop_word, number) + self.stop_criteria.append([stop_word] + number) + else: + self.valid_parameters = False + raise ValueError(f"The format of a single criterion in the 'stop_criteria' parameter is 'word_number' but {criterion} found.") + else: + self.valid_parameters = False + raise TypeError(f"When the 'stop_criteria' parameter is assigned a tuple/list/numpy.ndarray, then its elements must be strings but the value ({val}) of type {type(val)} found at index {idx}.") + 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.") + + if parallel_processing is None: + self.parallel_processing = None + elif type(parallel_processing) in GA.supported_int_types: + if parallel_processing > 0: + self.parallel_processing = ["thread", parallel_processing] + else: + self.valid_parameters = False + raise ValueError(f"When the 'parallel_processing' parameter is assigned an integer, then the integer must be positive but the value ({parallel_processing}) found.") + elif type(parallel_processing) in [list, tuple]: + if len(parallel_processing) == 2: + if type(parallel_processing[0]) is str: + if parallel_processing[0] in ["process", "thread"]: + if (type(parallel_processing[1]) in GA.supported_int_types and parallel_processing[1] > 0) or (parallel_processing[1] == 0) or (parallel_processing[1] is None): + if parallel_processing[1] == 0: + # If the number of processes/threads is 0, this means no parallel processing is used. It is equivelant to setting parallel_processing=None. + self.parallel_processing = None + else: + # Whether the second value is None or a positive integer. + self.parallel_processing = parallel_processing + else: + self.valid_parameters = False + raise TypeError(f"When a list or tuple is assigned to the 'parallel_processing' parameter, then the second element must be an integer but the value ({parallel_processing[1]}) of type {type(parallel_processing[1])} found.") + else: + self.valid_parameters = False + raise ValueError(f"When a list or tuple is assigned to the 'parallel_processing' parameter, then the value of the first element must be either 'process' or 'thread' but the value ({parallel_processing[0]}) found.") + else: + self.valid_parameters = False + raise TypeError(f"When a list or tuple is assigned to the 'parallel_processing' parameter, then the first element must be of type 'str' but the value ({parallel_processing[0]}) of type {type(parallel_processing[0])} found.") + else: + self.valid_parameters = False + raise ValueError(f"When a list or tuple is assigned to the 'parallel_processing' parameter, then it must have 2 elements but ({len(parallel_processing)}) found.") + else: + 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.") + + # Set the `run_completed` property to False. It is set to `True` only after the `run()` method is complete. + self.run_completed = False + + # The number of completed generations. + self.generations_completed = 0 + + # At this point, all necessary parameters validation is done successfully and we are sure that the parameters are valid. + # Set to True when all the parameters passed in the GA class constructor are valid. + 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. + self.mutation_percent_genes = mutation_percent_genes + self.mutation_num_genes = mutation_num_genes + + # Even such this parameter is declared in the class header, it is assigned to the object here to access it after saving the object. + # A list holding the fitness value of the best solution for each generation. + self.best_solutions_fitness = [] + + # 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. + self.best_solution_generation = -1 + + self.save_best_solutions = save_best_solutions + self.best_solutions = [] # Holds the best solution in each generation. + + self.save_solutions = save_solutions + self.solutions = [] # Holds the solutions in each generation. + # Holds the fitness of the solutions in each generation. + self.solutions_fitness = [] + + # A list holding the fitness values of all solutions in the last generation. + self.last_generation_fitness = None + # A list holding the parents of the last generation. + self.last_generation_parents = None + # A list holding the offspring after applying crossover in the last generation. + self.last_generation_offspring_crossover = None + # A list holding the offspring after applying mutation in the last generation. + self.last_generation_offspring_mutation = None + # Holds the fitness values of one generation before the fitness values saved in the last_generation_fitness attribute. Added in PyGAD 2.16.2. + # They are used inside the cal_pop_fitness() method to fetch the fitness of the parents in one generation before the latest generation. + # This is to avoid re-calculating the fitness for such parents again. + self.previous_generation_fitness = None + # Added in PyGAD 2.18.0. A NumPy array holding the elitism of the current generation according to the value passed in the 'keep_elitism' parameter. It works only if the 'keep_elitism' parameter has a non-zero value. + self.last_generation_elitism = None + # Added in PyGAD 2.19.0. A NumPy array holding the indices of the elitism of the current generation. It works only if the 'keep_elitism' parameter has a non-zero value. + 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 + except Exception as e: + self.logger.exception(e) + # sys.exit(-1) + raise e + + 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 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]), + self.gene_type[gene_idx][1]) + return solutions + + def initialize_population(self, + low, + high, + allow_duplicate_genes, + mutation_by_replacement, + gene_type): + """ + Creates an initial population randomly as a NumPy array. The array is saved in the instance attribute named 'population'. + + 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, holds the initial population and later updated after each generation. + 3. init_population: Keeping the initial population. + """ + + # Population size = (number of chromosomes, number of genes per chromosome) + # The population will have sol_per_pop chromosome where each chromosome has num_genes genes. + self.pop_size = (self.sol_per_pop, self.num_genes) + + if self.gene_space is None: + # Creating the initial population randomly. + if self.gene_type_single == True: + self.population = numpy.asarray(numpy.random.uniform(low=low, + high=high, + size=self.pop_size), + dtype=self.gene_type[0]) # A NumPy array holding the initial population. + else: + # Create an empty population of dtype=object to support storing mixed data types within the same array. + self.population = numpy.zeros( + shape=self.pop_size, dtype=object) + # Loop through the genes, randomly generate the values of a single gene across the entire population, and add the values of each gene to the population. + for gene_idx in range(self.num_genes): + + if type(self.init_range_low) in self.supported_int_float_types: + range_min = self.init_range_low + range_max = self.init_range_high + else: + range_min = self.init_range_low[gene_idx] + range_max = self.init_range_high[gene_idx] + + # A vector of all values of this single gene across all solutions in the population. + gene_values = numpy.asarray(numpy.random.uniform(low=range_min, + high=range_max, + size=self.pop_size[0]), + dtype=self.gene_type[gene_idx][0]) + # Adding the current gene values to the population. + self.population[:, gene_idx] = gene_values + + if allow_duplicate_genes == False: + for solution_idx in range(self.population.shape[0]): + # self.logger.info("Before", self.population[solution_idx]) + self.population[solution_idx], _, _ = self.solve_duplicate_genes_randomly(solution=self.population[solution_idx], + min_val=low, + max_val=high, + mutation_by_replacement=True, + gene_type=gene_type, + num_trials=10) + # self.logger.info("After", self.population[solution_idx]) + + elif self.gene_space_nested: + if self.gene_type_single == True: + # Reaching this block means: + # 1) gene_space is nested (gene_space_nested is True). + # 2) gene_type is not nested (gene_type_single is True). + self.population = numpy.zeros(shape=self.pop_size, + dtype=self.gene_type[0]) + for sol_idx in range(self.sol_per_pop): + for gene_idx in range(self.num_genes): + + if type(self.init_range_low) in self.supported_int_float_types: + range_min = self.init_range_low + range_max = self.init_range_high + else: + range_min = self.init_range_low[gene_idx] + range_max = self.init_range_high[gene_idx] + + if self.gene_space[gene_idx] is None: + + # The following commented code replace the None value with a single number that will not change again. + # This means the gene value will be the same across all solutions. + # self.gene_space[gene_idx] = numpy.asarray(numpy.random.uniform(low=low, + # high=high, + # size=1), dtype=self.gene_type[0])[0] + # self.population[sol_idx, gene_idx] = list(self.gene_space[gene_idx]).copy() + + # The above problem is solved by keeping the None value in the gene_space parameter. This forces PyGAD to generate this value for each solution. + self.population[sol_idx, gene_idx] = numpy.asarray(numpy.random.uniform(low=range_min, + high=range_max, + size=1), + dtype=self.gene_type[0])[0] + elif type(self.gene_space[gene_idx]) in [numpy.ndarray, list, tuple, range]: + # Check if the gene space has None values. If any, then replace it with randomly generated values according to the 3 attributes init_range_low, init_range_high, and gene_type. + if type(self.gene_space[gene_idx]) is range: + temp_gene_space = self.gene_space[gene_idx] + else: + # Convert to list because tuple and range do not have copy(). + # We copy the gene_space to a temp variable to keep its original value. + # In the next for loop, the gene_space is changed. + # Later, the gene_space is restored to its original value using the temp variable. + temp_gene_space = list( + self.gene_space[gene_idx]).copy() + + for idx, val in enumerate(self.gene_space[gene_idx]): + if val is None: + self.gene_space[gene_idx][idx] = numpy.asarray(numpy.random.uniform(low=range_min, + high=range_max, + size=1), + dtype=self.gene_type[0])[0] + # Find the difference between the current gene space and the current values in the solution. + unique_gene_values = list(set(self.gene_space[gene_idx]).difference( + set(self.population[sol_idx, :gene_idx]))) + if len(unique_gene_values) > 0: + self.population[sol_idx, gene_idx] = random.choice(unique_gene_values) + else: + # If there is no unique values, then we have to select a duplicate value. + self.population[sol_idx, gene_idx] = random.choice( + self.gene_space[gene_idx]) + + self.population[sol_idx, gene_idx] = self.gene_type[0]( + self.population[sol_idx, gene_idx]) + # Restore the gene_space from the temp_gene_space variable. + self.gene_space[gene_idx] = list( + temp_gene_space).copy() + elif type(self.gene_space[gene_idx]) is dict: + if 'step' in self.gene_space[gene_idx].keys(): + self.population[sol_idx, gene_idx] = numpy.asarray(numpy.random.choice(numpy.arange(start=self.gene_space[gene_idx]['low'], + stop=self.gene_space[gene_idx]['high'], + step=self.gene_space[gene_idx]['step']), + size=1), + dtype=self.gene_type[0])[0] + else: + self.population[sol_idx, gene_idx] = numpy.asarray(numpy.random.uniform(low=self.gene_space[gene_idx]['low'], + high=self.gene_space[gene_idx]['high'], + size=1), + dtype=self.gene_type[0])[0] + elif type(self.gene_space[gene_idx]) in GA.supported_int_float_types: + self.population[sol_idx, gene_idx] = self.gene_space[gene_idx] + else: + # There is no more options. + pass + else: + # Reaching this block means: + # 1) gene_space is nested (gene_space_nested is True). + # 2) gene_type is nested (gene_type_single is False). + self.population = numpy.zeros(shape=self.pop_size, + dtype=object) + for sol_idx in range(self.sol_per_pop): + for gene_idx in range(self.num_genes): + + if type(self.init_range_low) in self.supported_int_float_types: + range_min = self.init_range_low + range_max = self.init_range_high + else: + range_min = self.init_range_low[gene_idx] + range_max = self.init_range_high[gene_idx] + + if type(self.gene_space[gene_idx]) in [numpy.ndarray, list, tuple, range]: + # Convert to list because tuple and range do not have copy(). + # We copy the gene_space to a temp variable to keep its original value. + # In the next for loop, the gene_space is changed. + # Later, the gene_space is restored to its original value using the temp variable. + temp_gene_space = list(self.gene_space[gene_idx]).copy() + + # Check if the gene space has None values. If any, then replace it with randomly generated values according to the 3 attributes init_range_low, init_range_high, and gene_type. + for idx, val in enumerate(self.gene_space[gene_idx]): + if val is None: + self.gene_space[gene_idx][idx] = numpy.asarray(numpy.random.uniform(low=range_min, + high=range_max, + size=1), + dtype=self.gene_type[gene_idx][0])[0] + + self.population[sol_idx, gene_idx] = random.choice(self.gene_space[gene_idx]) + self.population[sol_idx, gene_idx] = self.gene_type[gene_idx][0](self.population[sol_idx, gene_idx]) + # Restore the gene_space from the temp_gene_space variable. + self.gene_space[gene_idx] = temp_gene_space.copy() + elif type(self.gene_space[gene_idx]) is dict: + if 'step' in self.gene_space[gene_idx].keys(): + self.population[sol_idx, gene_idx] = numpy.asarray(numpy.random.choice(numpy.arange(start=self.gene_space[gene_idx]['low'], + stop=self.gene_space[gene_idx]['high'], + step=self.gene_space[gene_idx]['step']), + size=1), + dtype=self.gene_type[gene_idx][0])[0] + else: + self.population[sol_idx, gene_idx] = numpy.asarray(numpy.random.uniform(low=self.gene_space[gene_idx]['low'], + high=self.gene_space[gene_idx]['high'], + size=1), + dtype=self.gene_type[gene_idx][0])[0] + elif type(self.gene_space[gene_idx]) == type(None): + temp_gene_value = numpy.asarray(numpy.random.uniform(low=range_min, + high=range_max, + size=1), + dtype=self.gene_type[gene_idx][0])[0] + + self.population[sol_idx, gene_idx] = temp_gene_value.copy() + elif type(self.gene_space[gene_idx]) in GA.supported_int_float_types: + self.population[sol_idx, gene_idx] = self.gene_space[gene_idx] + else: + # There is no more options. + pass + else: + # Handle the non-nested gene_space. It can be assigned a numeric value, list, numpy.ndarray, or a dict. + if self.gene_type_single == True: + # Reaching this block means: + # 1) gene_space is not nested (gene_space_nested is False). + # 2) gene_type is not nested (gene_type_single is True). + + # Replace all the None values with random values using the init_range_low, init_range_high, and gene_type attributes. + for gene_idx, curr_gene_space in enumerate(self.gene_space): + + if type(self.init_range_low) in self.supported_int_float_types: + range_min = self.init_range_low + range_max = self.init_range_high + else: + range_min = self.init_range_low[gene_idx] + range_max = self.init_range_high[gene_idx] + + if curr_gene_space is None: + self.gene_space[gene_idx] = numpy.asarray(numpy.random.uniform(low=range_min, + high=range_max, + size=1), + dtype=self.gene_type[0])[0] + + # Creating the initial population by randomly selecting the genes' values from the values inside the 'gene_space' parameter. + if type(self.gene_space) is dict: + if 'step' in self.gene_space.keys(): + self.population = numpy.asarray(numpy.random.choice(numpy.arange(start=self.gene_space['low'], + stop=self.gene_space['high'], + step=self.gene_space['step']), + size=self.pop_size), + dtype=self.gene_type[0]) + else: + self.population = numpy.asarray(numpy.random.uniform(low=self.gene_space['low'], + high=self.gene_space['high'], + size=self.pop_size), + dtype=self.gene_type[0]) # A NumPy array holding the initial population. + else: + self.population = numpy.asarray(numpy.random.choice(self.gene_space, + size=self.pop_size), + dtype=self.gene_type[0]) # A NumPy array holding the initial population. + else: + # Reaching this block means: + # 1) gene_space is not nested (gene_space_nested is False). + # 2) gene_type is nested (gene_type_single is False). + + # Creating the initial population by randomly selecting the genes' values from the values inside the 'gene_space' parameter. + if type(self.gene_space) is dict: + # Create an empty population of dtype=object to support storing mixed data types within the same array. + self.population = numpy.zeros(shape=self.pop_size, + dtype=object) + # Loop through the genes, randomly generate the values of a single gene across the entire population, and add the values of each gene to the population. + for gene_idx in range(self.num_genes): + # Generate the values of the current gene across all solutions. + # A vector of all values of this single gene across all solutions in the population. + if 'step' in self.gene_space.keys(): + gene_values = numpy.asarray(numpy.random.choice(numpy.arange(start=self.gene_space['low'], + stop=self.gene_space['high'], + step=self.gene_space['step']), + size=self.pop_size[0]), + dtype=self.gene_type[gene_idx][0]) + else: + gene_values = numpy.asarray(numpy.random.uniform(low=self.gene_space['low'], + high=self.gene_space['high'], + size=self.pop_size[0]), + dtype=self.gene_type[gene_idx][0]) + # Adding the current gene values to the population. + self.population[:, gene_idx] = gene_values + + else: + # Reaching this block means that the gene_space is not None or dict. + # It can be either range, numpy.ndarray, or list. + + # Create an empty population of dtype=object to support storing mixed data types within the same array. + self.population = numpy.zeros(shape=self.pop_size, dtype=object) + # Loop through the genes, randomly generate the values of a single gene across the entire population, and add the values of each gene to the population. + for gene_idx in range(self.num_genes): + # A vector of all values of this single gene across all solutions in the population. + gene_values = numpy.asarray(numpy.random.choice(self.gene_space, + size=self.pop_size[0]), + dtype=self.gene_type[gene_idx][0]) + # Adding the current gene values to the population. + self.population[:, gene_idx] = gene_values + + if not (self.gene_space is None): + if allow_duplicate_genes == False: + for sol_idx in range(self.population.shape[0]): + self.population[sol_idx], _, _ = self.solve_duplicate_genes_by_space(solution=self.population[sol_idx], + gene_type=self.gene_type, + num_trials=10, + build_initial_pop=True) + + # Keeping the initial population in the initial_population attribute. + self.initial_population = self.population.copy() + + def cal_pop_fitness(self): + """ + Calculating the fitness values of batches of solutions in the current population. + It returns: + -fitness: An array of the calculated fitness values. + """ + try: + if self.valid_parameters == False: + raise Exception("ERROR calling the cal_pop_fitness() method: \nPlease check the parameters passed while creating an instance of the GA class.\n") + + # 'last_generation_parents_as_list' is the list version of 'self.last_generation_parents' + # It is used to return the parent index using the 'in' membership operator of Python lists. This is much faster than using 'numpy.where()'. + if self.last_generation_parents is not None: + last_generation_parents_as_list = self.last_generation_parents.tolist() + + # 'last_generation_elitism_as_list' is the list version of 'self.last_generation_elitism' + # It is used to return the elitism index using the 'in' membership operator of Python lists. This is much faster than using 'numpy.where()'. + if self.last_generation_elitism is not None: + last_generation_elitism_as_list = self.last_generation_elitism.tolist() + + pop_fitness = ["undefined"] * len(self.population) + if self.parallel_processing is None: + # Calculating the fitness value of each solution in the current population. + for sol_idx, sol in enumerate(self.population): + # Check if the `save_solutions` parameter is `True` and whether the solution already exists in the `solutions` list. If so, use its fitness rather than calculating it again. + # The functions numpy.any()/numpy.all()/numpy.where()/numpy.equal() are very slow. + # So, list membership operator 'in' is used to check if the solution exists in the 'self.solutions' list. + # Make sure that both the solution and 'self.solutions' are of type 'list' not 'numpy.ndarray'. + # if (self.save_solutions) and (len(self.solutions) > 0) and (numpy.any(numpy.all(self.solutions == numpy.array(sol), axis=1))) + # if (self.save_solutions) and (len(self.solutions) > 0) and (numpy.any(numpy.all(numpy.equal(self.solutions, numpy.array(sol)), axis=1))) + + # Make sure self.best_solutions is a list of lists before proceeding. + # Because the second condition expects that best_solutions is a list of lists. + if type(self.best_solutions) is numpy.ndarray: + self.best_solutions = self.best_solutions.tolist() + + if (self.save_solutions) and (len(self.solutions) > 0) and (list(sol) in self.solutions): + solution_idx = self.solutions.index(list(sol)) + fitness = self.solutions_fitness[solution_idx] + elif (self.save_best_solutions) and (len(self.best_solutions) > 0) and (list(sol) in self.best_solutions): + solution_idx = self.best_solutions.index(list(sol)) + fitness = self.best_solutions_fitness[solution_idx] + elif (self.keep_elitism > 0) and (self.last_generation_elitism is not None) and (len(self.last_generation_elitism) > 0) and (list(sol) in last_generation_elitism_as_list): + # Return the index of the elitism from the elitism array 'self.last_generation_elitism'. + # This is not its index within the population. It is just its index in the 'self.last_generation_elitism' array. + elitism_idx = last_generation_elitism_as_list.index(list(sol)) + # Use the returned elitism index to return its index in the last population. + elitism_idx = self.last_generation_elitism_indices[elitism_idx] + # Use the elitism's index to return its pre-calculated fitness value. + fitness = self.previous_generation_fitness[elitism_idx] + # If the solutions are not saved (i.e. `save_solutions=False`), check if this solution is a parent from the previous generation and its fitness value is already calculated. If so, use the fitness value instead of calling the fitness function. + # We cannot use the `numpy.where()` function directly because it does not support the `axis` parameter. This is why the `numpy.all()` function is used to match the solutions on axis=1. + # elif (self.last_generation_parents is not None) and len(numpy.where(numpy.all(self.last_generation_parents == sol, axis=1))[0] > 0): + elif ((self.keep_parents == -1) or (self.keep_parents > 0)) and (self.last_generation_parents is not None) and (len(self.last_generation_parents) > 0) and (list(sol) in last_generation_parents_as_list): + # Index of the parent in the 'self.last_generation_parents' array. + # This is not its index within the population. It is just its index in the 'self.last_generation_parents' array. + # parent_idx = numpy.where(numpy.all(self.last_generation_parents == sol, axis=1))[0][0] + parent_idx = last_generation_parents_as_list.index(list(sol)) + # Use the returned parent index to return its index in the last population. + parent_idx = self.last_generation_parents_indices[parent_idx] + # Use the parent's index to return its pre-calculated fitness value. + fitness = self.previous_generation_fitness[parent_idx] + else: + # Check if batch processing is used. If not, then calculate this missing fitness value. + if self.fitness_batch_size in [1, None]: + fitness = self.fitness_func(self, sol, sol_idx) + if type(fitness) in GA.supported_int_float_types: + # The fitness function returns a single numeric value. + # This is a single-objective optimization problem. + pass + elif type(fitness) in [list, tuple, numpy.ndarray]: + # The fitness function returns a list/tuple/numpy.ndarray. + # This is a multi-objective optimization problem. + pass + else: + raise ValueError(f"The fitness function should return a number or an iterable (list, tuple, or numpy.ndarray) but the value {fitness} of type {type(fitness)} found.") + else: + # Reaching this point means that batch processing is in effect to calculate the fitness values. + # Do not continue the loop as no fitness is calculated. The fitness will be calculated later in batch mode. + continue + + # This is only executed if the fitness value was already calculated. + pop_fitness[sol_idx] = fitness + + if self.fitness_batch_size not in [1, None]: + # Reaching this block means that batch fitness calculation is used. + + # Indices of the solutions to calculate their fitness. + solutions_indices = [idx for idx, fit in enumerate(pop_fitness) if type(fit) is str and fit == "undefined"] + # Number of batches. + num_batches = int(numpy.ceil(len(solutions_indices) / self.fitness_batch_size)) + # For each batch, get its indices and call the fitness function. + for batch_idx in range(num_batches): + batch_first_index = batch_idx * self.fitness_batch_size + batch_last_index = (batch_idx + 1) * self.fitness_batch_size + batch_indices = solutions_indices[batch_first_index:batch_last_index] + batch_solutions = self.population[batch_indices, :] + + batch_fitness = self.fitness_func( + self, batch_solutions, batch_indices) + if type(batch_fitness) not in [list, tuple, numpy.ndarray]: + raise TypeError(f"Expected to receive a list, tuple, or numpy.ndarray from the fitness function but the value ({batch_fitness}) of type {type(batch_fitness)}.") + elif len(numpy.array(batch_fitness)) != len(batch_indices): + raise ValueError(f"There is a mismatch between the number of solutions passed to the fitness function ({len(batch_indices)}) and the number of fitness values returned ({len(batch_fitness)}). They must match.") + + for index, fitness in zip(batch_indices, batch_fitness): + if type(fitness) in GA.supported_int_float_types: + # The fitness function returns a single numeric value. + # This is a single-objective optimization problem. + pop_fitness[index] = fitness + elif type(fitness) in [list, tuple, numpy.ndarray]: + # The fitness function returns a list/tuple/numpy.ndarray. + # This is a multi-objective optimization problem. + pop_fitness[index] = fitness + else: + raise ValueError(f"The fitness function should return a number or an iterable (list, tuple, or numpy.ndarray) but the value {fitness} of type {type(fitness)} found.") + else: + # Calculating the fitness value of each solution in the current population. + for sol_idx, sol in enumerate(self.population): + # Check if the `save_solutions` parameter is `True` and whether the solution already exists in the `solutions` list. If so, use its fitness rather than calculating it again. + # The functions numpy.any()/numpy.all()/numpy.where()/numpy.equal() are very slow. + # So, list membership operator 'in' is used to check if the solution exists in the 'self.solutions' list. + # Make sure that both the solution and 'self.solutions' are of type 'list' not 'numpy.ndarray'. + if (self.save_solutions) and (len(self.solutions) > 0) and (list(sol) in self.solutions): + solution_idx = self.solutions.index(list(sol)) + fitness = self.solutions_fitness[solution_idx] + pop_fitness[sol_idx] = fitness + elif (self.keep_elitism > 0) and (self.last_generation_elitism is not None) and (len(self.last_generation_elitism) > 0) and (list(sol) in last_generation_elitism_as_list): + # Return the index of the elitism from the elitism array 'self.last_generation_elitism'. + # This is not its index within the population. It is just its index in the 'self.last_generation_elitism' array. + elitism_idx = last_generation_elitism_as_list.index( + list(sol)) + # Use the returned elitism index to return its index in the last population. + elitism_idx = self.last_generation_elitism_indices[elitism_idx] + # Use the elitism's index to return its pre-calculated fitness value. + fitness = self.previous_generation_fitness[elitism_idx] + + pop_fitness[sol_idx] = fitness + # If the solutions are not saved (i.e. `save_solutions=False`), check if this solution is a parent from the previous generation and its fitness value is already calculated. If so, use the fitness value instead of calling the fitness function. + # We cannot use the `numpy.where()` function directly because it does not support the `axis` parameter. This is why the `numpy.all()` function is used to match the solutions on axis=1. + # elif (self.last_generation_parents is not None) and len(numpy.where(numpy.all(self.last_generation_parents == sol, axis=1))[0] > 0): + elif ((self.keep_parents == -1) or (self.keep_parents > 0)) and (self.last_generation_parents is not None) and (len(self.last_generation_parents) > 0) and (list(sol) in last_generation_parents_as_list): + # Index of the parent in the 'self.last_generation_parents' array. + # This is not its index within the population. It is just its index in the 'self.last_generation_parents' array. + # parent_idx = numpy.where(numpy.all(self.last_generation_parents == sol, axis=1))[0][0] + parent_idx = last_generation_parents_as_list.index( + list(sol)) + # Use the returned parent index to return its index in the last population. + parent_idx = self.last_generation_parents_indices[parent_idx] + # Use the parent's index to return its pre-calculated fitness value. + fitness = self.previous_generation_fitness[parent_idx] + + pop_fitness[sol_idx] = fitness + + # Decide which class to use based on whether the user selected "process" or "thread" + if self.parallel_processing[0] == "process": + ExecutorClass = concurrent.futures.ProcessPoolExecutor + else: + ExecutorClass = concurrent.futures.ThreadPoolExecutor + + # We can use a with statement to ensure threads are cleaned up promptly (https://docs.python.org/3/library/concurrent.futures.html#threadpoolexecutor-example) + with ExecutorClass(max_workers=self.parallel_processing[1]) as executor: + solutions_to_submit_indices = [] + solutions_to_submit = [] + for sol_idx, sol in enumerate(self.population): + # The "undefined" value means that the fitness of this solution must be calculated. + if type(pop_fitness[sol_idx]) is str: + if pop_fitness[sol_idx] == "undefined": + solutions_to_submit.append(sol.copy()) + solutions_to_submit_indices.append(sol_idx) + elif type(pop_fitness[sol_idx]) in [list, tuple, numpy.ndarray]: + # This is a multi-objective problem. The fitness is already calculated. Nothing to do. + pass + + # Check if batch processing is used. If not, then calculate the fitness value for individual solutions. + if self.fitness_batch_size in [1, None]: + for index, fitness in zip(solutions_to_submit_indices, executor.map(self.fitness_func, [self]*len(solutions_to_submit_indices), solutions_to_submit, solutions_to_submit_indices)): + if type(fitness) in GA.supported_int_float_types: + # The fitness function returns a single numeric value. + # This is a single-objective optimization problem. + pop_fitness[index] = fitness + elif type(fitness) in [list, tuple, numpy.ndarray]: + # The fitness function returns a list/tuple/numpy.ndarray. + # This is a multi-objective optimization problem. + pop_fitness[index] = fitness + else: + raise ValueError(f"The fitness function should return a number or an iterable (list, tuple, or numpy.ndarray) but the value {fitness} of type {type(fitness)} found.") + else: + # Reaching this block means that batch processing is used. The fitness values are calculated in batches. + + # Number of batches. + num_batches = int(numpy.ceil(len(solutions_to_submit_indices) / self.fitness_batch_size)) + # Each element of the `batches_solutions` list represents the solutions in one batch. + batches_solutions = [] + # Each element of the `batches_indices` list represents the solutions' indices in one batch. + batches_indices = [] + # For each batch, get its indices and call the fitness function. + for batch_idx in range(num_batches): + batch_first_index = batch_idx * self.fitness_batch_size + batch_last_index = (batch_idx + 1) * self.fitness_batch_size + batch_indices = solutions_to_submit_indices[batch_first_index:batch_last_index] + batch_solutions = self.population[batch_indices, :] + + batches_solutions.append(batch_solutions) + batches_indices.append(batch_indices) + + for batch_indices, batch_fitness in zip(batches_indices, executor.map(self.fitness_func, [self]*len(solutions_to_submit_indices), batches_solutions, batches_indices)): + if type(batch_fitness) not in [list, tuple, numpy.ndarray]: + raise TypeError(f"Expected to receive a list, tuple, or numpy.ndarray from the fitness function but the value ({batch_fitness}) of type {type(batch_fitness)}.") + elif len(numpy.array(batch_fitness)) != len(batch_indices): + raise ValueError(f"There is a mismatch between the number of solutions passed to the fitness function ({len(batch_indices)}) and the number of fitness values returned ({len(batch_fitness)}). They must match.") + + for index, fitness in zip(batch_indices, batch_fitness): + if type(fitness) in GA.supported_int_float_types: + # The fitness function returns a single numeric value. + # This is a single-objective optimization problem. + pop_fitness[index] = fitness + elif type(fitness) in [list, tuple, numpy.ndarray]: + # The fitness function returns a list/tuple/numpy.ndarray. + # This is a multi-objective optimization problem. + pop_fitness[index] = fitness + else: + raise ValueError(f"The fitness function should return a number or an iterable (list, tuple, or numpy.ndarray) but the value ({fitness}) of type {type(fitness)} found.") + + pop_fitness = numpy.array(pop_fitness) + except Exception as ex: + self.logger.exception(ex) + # sys.exit(-1) + raise ex + return pop_fitness + + def run(self): + """ + Runs the genetic algorithm. This is the main method in which the genetic algorithm is evolved through a number of generations. + """ + try: + if self.valid_parameters == False: + raise Exception("Error calling the run() method: \nThe run() method cannot be executed with invalid parameters. Please check the parameters passed while creating an instance of the GA class.\n") + + # Starting from PyGAD 2.18.0, the 4 properties (best_solutions, best_solutions_fitness, solutions, and solutions_fitness) are no longer reset with each call to the run() method. Instead, they are extended. + # For example, if there are 50 generations and the user set save_best_solutions=True, then the length of the 2 properties best_solutions and best_solutions_fitness will be 50 after the first call to the run() method, then 100 after the second call, 150 after the third, and so on. + + # self.best_solutions: Holds the best solution in each generation. + if type(self.best_solutions) is numpy.ndarray: + self.best_solutions = self.best_solutions.tolist() + # self.best_solutions_fitness: A list holding the fitness value of the best solution for each generation. + if type(self.best_solutions_fitness) is numpy.ndarray: + self.best_solutions_fitness = list(self.best_solutions_fitness) + # self.solutions: Holds the solutions in each generation. + if type(self.solutions) is numpy.ndarray: + self.solutions = self.solutions.tolist() + # self.solutions_fitness: Holds the fitness of the solutions in each generation. + if type(self.solutions_fitness) is numpy.ndarray: + self.solutions_fitness = list(self.solutions_fitness) + + if not (self.on_start is None): + self.on_start(self) + + stop_run = False + + # To continue from where we stopped, the first generation index should start from the value of the 'self.generations_completed' parameter. + if self.generations_completed != 0 and type(self.generations_completed) in GA.supported_int_types: + # If the 'self.generations_completed' parameter is not '0', then this means we continue execution. + 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. + generation_first_idx = 0 + generation_last_idx = self.num_generations + + # Measuring the fitness of each chromosome in the population. Save the fitness in the last_generation_fitness attribute. + self.last_generation_fitness = self.cal_pop_fitness() + + # Know whether the problem is SOO or MOO. + if type(self.last_generation_fitness[0]) in GA.supported_int_float_types: + # Single-objective problem. + # If the problem is SOO, the parent selection type cannot be nsga2 or tournament_nsga2. + if self.parent_selection_type in ['nsga2', 'tournament_nsga2']: + raise TypeError(f"Incorrect parent selection type. The fitness function returned a single numeric fitness value which means the problem is single-objective. But the parent selection type {self.parent_selection_type} is used which only works for multi-objective optimization problems.") + elif type(self.last_generation_fitness[0]) in [list, tuple, numpy.ndarray]: + # Multi-objective problem. + pass + + best_solution, best_solution_fitness, best_match_idx = self.best_solution(pop_fitness=self.last_generation_fitness) + + # Appending the best solution in the initial population to the best_solutions list. + if self.save_best_solutions: + self.best_solutions.append(list(best_solution)) + + for generation in range(generation_first_idx, generation_last_idx): + + self.run_loop_head(best_solution_fitness) + + # Call the 'run_select_parents()' method to select the parents. + # It edits these 2 instance attributes: + # 1) last_generation_parents: A NumPy array of the selected parents. + # 2) last_generation_parents_indices: A 1D NumPy array of the indices of the selected parents. + self.run_select_parents() + + # Call the 'run_crossover()' method to select the offspring. + # It edits these 2 instance attributes: + # 1) last_generation_offspring_crossover: A NumPy array of the selected offspring. + # 2) last_generation_elitism: A NumPy array of the current generation elitism. Applicable only if the 'keep_elitism' parameter > 0. + self.run_crossover() + + # Call the 'run_mutation()' method to mutate the selected offspring. + # It edits this instance attribute: + # 1) last_generation_offspring_mutation: A NumPy array of the mutated offspring. + self.run_mutation() + + # Call the 'run_update_population()' method to update the population after both crossover and mutation operations complete. + # It edits this instance attribute: + # 1) population: A NumPy array of the population of solutions/chromosomes. + self.run_update_population() + + # The generations_completed attribute holds the number of the last completed generation. + self.generations_completed = generation + 1 + + self.previous_generation_fitness = self.last_generation_fitness.copy() + # Measuring the fitness of each chromosome in the population. Save the fitness in the last_generation_fitness attribute. + self.last_generation_fitness = self.cal_pop_fitness() + + best_solution, best_solution_fitness, best_match_idx = self.best_solution( + pop_fitness=self.last_generation_fitness) + + # Appending the best solution in the current generation to the best_solutions list. + 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. + 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: + for criterion in self.stop_criteria: + if criterion[0] == "reach": + # Single-objective problem. + if type(self.last_generation_fitness[0]) in GA.supported_int_float_types: + if max(self.last_generation_fitness) >= criterion[1]: + stop_run = True + break + # Multi-objective problem. + elif type(self.last_generation_fitness[0]) in [list, tuple, numpy.ndarray]: + # Validate the value passed to the criterion. + if len(criterion[1:]) == 1: + # There is a single value used across all the objectives. + pass + elif len(criterion[1:]) > 1: + # There are multiple values. The number of values must be equal to the number of objectives. + if len(criterion[1:]) == len(self.last_generation_fitness[0]): + 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])}.") + + stop_run = True + for obj_idx in range(len(self.last_generation_fitness[0])): + # Use the objective index to return the proper value for the criterion. + + if len(criterion[1:]) == len(self.last_generation_fitness[0]): + reach_fitness_value = criterion[obj_idx + 1] + elif len(criterion[1:]) == 1: + reach_fitness_value = criterion[1] + + if max(self.last_generation_fitness[:, obj_idx]) >= reach_fitness_value: + pass + else: + stop_run = False + break + elif criterion[0] == "saturate": + criterion[1] = int(criterion[1]) + if (self.generations_completed >= criterion[1]): + # Single-objective problem. + if type(self.last_generation_fitness[0]) in GA.supported_int_float_types: + if (self.best_solutions_fitness[self.generations_completed - criterion[1]] - self.best_solutions_fitness[self.generations_completed - 1]) == 0: + stop_run = True + break + # Multi-objective problem. + elif type(self.last_generation_fitness[0]) in [list, tuple, numpy.ndarray]: + stop_run = True + for obj_idx in range(len(self.last_generation_fitness[0])): + if (self.best_solutions_fitness[self.generations_completed - criterion[1]][obj_idx] - self.best_solutions_fitness[self.generations_completed - 1][obj_idx]) == 0: + pass + else: + stop_run = False + break + + if stop_run: + break + + # Save the fitness of the last generation. + if self.save_solutions: + # self.solutions.extend(self.population.copy()) + population_as_list = self.population.copy() + population_as_list = [list(item) for item in population_as_list] + self.solutions.extend(population_as_list) + + self.solutions_fitness.extend(self.last_generation_fitness) + + # Call the run_select_parents() method to update these 2 attributes according to the 'last_generation_fitness' attribute: + # 1) last_generation_parents 2) last_generation_parents_indices + # Set 'call_on_parents=False' to avoid calling the callable 'on_parents' because this step is not part of the cycle. + self.run_select_parents(call_on_parents=False) + + # Save the fitness value of the best solution. + _, best_solution_fitness, _ = self.best_solution( + pop_fitness=self.last_generation_fitness) + self.best_solutions_fitness.append(best_solution_fitness) + + self.best_solution_generation = numpy.where(numpy.array( + self.best_solutions_fitness) == numpy.max(numpy.array(self.best_solutions_fitness)))[0][0] + # After the run() method completes, the run_completed flag is changed from False to True. + # Set to True only after the run() method completes gracefully. + self.run_completed = True + + if not (self.on_stop is None): + self.on_stop(self, self.last_generation_fitness) + + # Converting the 'best_solutions' list into a NumPy array. + self.best_solutions = numpy.array(self.best_solutions) + + # Update previous_generation_fitness because it is used to get the fitness of the parents. + self.previous_generation_fitness = self.last_generation_fitness.copy() + + # Converting the 'solutions' list into a NumPy array. + # self.solutions = numpy.array(self.solutions) + except Exception as ex: + self.logger.exception(ex) + # sys.exit(-1) + raise ex + + def run_loop_head(self, best_solution_fitness): + if not (self.on_fitness is None): + on_fitness_output = self.on_fitness(self, + self.last_generation_fitness) + + if on_fitness_output is None: + pass + else: + if type(on_fitness_output) in [tuple, list, numpy.ndarray, range]: + on_fitness_output = numpy.array(on_fitness_output) + if on_fitness_output.shape == self.last_generation_fitness.shape: + self.last_generation_fitness = on_fitness_output + else: + raise ValueError(f"Size mismatch between the output of on_fitness() {on_fitness_output.shape} and the expected fitness output {self.last_generation_fitness.shape}.") + else: + raise ValueError(f"The output of on_fitness() is expected to be tuple/list/range/numpy.ndarray but {type(on_fitness_output)} found.") + + # Appending the fitness value of the best solution in the current generation to the best_solutions_fitness attribute. + self.best_solutions_fitness.append(best_solution_fitness) + + # Appending the solutions in the current generation to the solutions list. + if self.save_solutions: + # self.solutions.extend(self.population.copy()) + population_as_list = self.population.copy() + population_as_list = [list(item) for item in population_as_list] + self.solutions.extend(population_as_list) + + self.solutions_fitness.extend(self.last_generation_fitness) + + def run_select_parents(self, call_on_parents=True): + """ + This method must be only callled from inside the run() method. It is not meant for use by the user. + Generally, any method with a name starting with 'run_' is meant to be only called by PyGAD from inside the 'run()' method. + + The objective of the 'run_select_parents()' method is to select the parents and call the callable on_parents() if defined. + It does not return any variables. However, it changes these 2 attributes of the pygad.GA class instances: + 1) last_generation_parents: A NumPy array of the selected parents. + 2) last_generation_parents_indices: A 1D NumPy array of the indices of the selected parents. + + Parameters + ---------- + call_on_parents : bool, optional + If True, then the callable 'on_parents()' is called. The default is True. + + Returns + ------- + None. + """ + + # Selecting the best parents in the population for mating. + if callable(self.parent_selection_type): + self.last_generation_parents, self.last_generation_parents_indices = self.select_parents(self.last_generation_fitness, + self.num_parents_mating, + self) + if not type(self.last_generation_parents) is numpy.ndarray: + raise TypeError(f"The type of the iterable holding the selected parents is expected to be (numpy.ndarray) but {type(self.last_generation_parents)} found.") + if not type(self.last_generation_parents_indices) is numpy.ndarray: + raise TypeError(f"The type of the iterable holding the selected parents' indices is expected to be (numpy.ndarray) but {type(self.last_generation_parents_indices)} found.") + else: + self.last_generation_parents, self.last_generation_parents_indices = self.select_parents(self.last_generation_fitness, + num_parents=self.num_parents_mating) + + # Validate the output of the parent selection step: self.select_parents() + if self.last_generation_parents.shape != (self.num_parents_mating, self.num_genes): + if self.last_generation_parents.shape[0] != self.num_parents_mating: + raise ValueError(f"Size mismatch between the size of the selected parents {self.last_generation_parents.shape} and the expected size {(self.num_parents_mating, self.num_genes)}. It is expected to select ({self.num_parents_mating}) parents but ({self.last_generation_parents.shape[0]}) selected.") + elif self.last_generation_parents.shape[1] != self.num_genes: + raise ValueError(f"Size mismatch between the size of the selected parents {self.last_generation_parents.shape} and the expected size {(self.num_parents_mating, self.num_genes)}. Parents are expected to have ({self.num_genes}) genes but ({self.last_generation_parents.shape[1]}) produced.") + + if self.last_generation_parents_indices.ndim != 1: + raise ValueError(f"The iterable holding the selected parents indices is expected to have 1 dimension but ({len(self.last_generation_parents_indices)}) found.") + elif len(self.last_generation_parents_indices) != self.num_parents_mating: + raise ValueError(f"The iterable holding the selected parents indices is expected to have ({self.num_parents_mating}) values but ({len(self.last_generation_parents_indices)}) found.") + + if call_on_parents: + if not (self.on_parents is None): + on_parents_output = self.on_parents(self, + self.last_generation_parents) + + if on_parents_output is None: + pass + elif type(on_parents_output) in [list, tuple, numpy.ndarray]: + if len(on_parents_output) == 2: + on_parents_selected_parents, on_parents_selected_parents_indices = on_parents_output + else: + raise ValueError(f"The output of on_parents() is expected to be tuple/list/numpy.ndarray of length 2 but {type(on_parents_output)} of length {len(on_parents_output)} found.") + + # Validate the parents. + if on_parents_selected_parents is None: + raise ValueError("The returned outputs of on_parents() cannot be None but the first output is None.") + else: + if type(on_parents_selected_parents) in [tuple, list, numpy.ndarray]: + on_parents_selected_parents = numpy.array(on_parents_selected_parents) + if on_parents_selected_parents.shape == self.last_generation_parents.shape: + self.last_generation_parents = on_parents_selected_parents + else: + raise ValueError(f"Size mismatch between the parents retrned by on_parents() {on_parents_selected_parents.shape} and the expected parents shape {self.last_generation_parents.shape}.") + else: + raise ValueError(f"The output of on_parents() is expected to be tuple/list/numpy.ndarray but the first output type is {type(on_parents_selected_parents)}.") + + # Validate the parents indices. + if on_parents_selected_parents_indices is None: + raise ValueError("The returned outputs of on_parents() cannot be None but the second output is None.") + else: + if type(on_parents_selected_parents_indices) in [tuple, list, numpy.ndarray, range]: + on_parents_selected_parents_indices = numpy.array(on_parents_selected_parents_indices) + if on_parents_selected_parents_indices.shape == self.last_generation_parents_indices.shape: + self.last_generation_parents_indices = on_parents_selected_parents_indices + else: + raise ValueError(f"Size mismatch between the parents indices returned by on_parents() {on_parents_selected_parents_indices.shape} and the expected crossover output {self.last_generation_parents_indices.shape}.") + else: + raise ValueError(f"The output of on_parents() is expected to be tuple/list/range/numpy.ndarray but the second output type is {type(on_parents_selected_parents_indices)}.") + + else: + raise TypeError(f"The output of on_parents() is expected to be tuple/list/numpy.ndarray but {type(on_parents_output)} found.") + + def run_crossover(self): + """ + This method must be only callled from inside the run() method. It is not meant for use by the user. + Generally, any method with a name starting with 'run_' is meant to be only called by PyGAD from inside the 'run()' method. + + The objective of the 'run_crossover()' method is to apply crossover and call the callable on_crossover() if defined. + It does not return any variables. However, it changes these 2 attributes of the pygad.GA class instances: + 1) last_generation_offspring_crossover: A NumPy array of the selected offspring. + 2) last_generation_elitism: A NumPy array of the current generation elitism. Applicable only if the 'keep_elitism' parameter > 0. + + Returns + ------- + None. + """ + + # If self.crossover_type=None, then no crossover is applied and thus no offspring will be created in the next generations. The next generation will use the solutions in the current population. + if self.crossover_type is None: + if self.keep_elitism == 0: + num_parents_to_keep = self.num_parents_mating if self.keep_parents == - 1 else self.keep_parents + if self.num_offspring <= num_parents_to_keep: + self.last_generation_offspring_crossover = self.last_generation_parents[0:self.num_offspring] + else: + self.last_generation_offspring_crossover = numpy.concatenate( + (self.last_generation_parents, self.population[0:(self.num_offspring - self.last_generation_parents.shape[0])])) + else: + # The steady_state_selection() function is called to select the best solutions (i.e. elitism). The keep_elitism parameter defines the number of these solutions. + # The steady_state_selection() function is still called here even if its output may not be used given that the condition of the next if statement is True. The reason is that it will be used later. + self.last_generation_elitism, _ = self.steady_state_selection(self.last_generation_fitness, + num_parents=self.keep_elitism) + if self.num_offspring <= self.keep_elitism: + self.last_generation_offspring_crossover = self.last_generation_parents[0:self.num_offspring] + else: + self.last_generation_offspring_crossover = numpy.concatenate( + (self.last_generation_elitism, self.population[0:(self.num_offspring - self.last_generation_elitism.shape[0])])) + else: + # Generating offspring using crossover. + if callable(self.crossover_type): + self.last_generation_offspring_crossover = self.crossover(self.last_generation_parents, + (self.num_offspring, self.num_genes), + self) + if not type(self.last_generation_offspring_crossover) is numpy.ndarray: + raise TypeError(f"The output of the crossover step is expected to be of type (numpy.ndarray) but {type(self.last_generation_offspring_crossover)} found.") + else: + self.last_generation_offspring_crossover = self.crossover(self.last_generation_parents, + offspring_size=(self.num_offspring, self.num_genes)) + if self.last_generation_offspring_crossover.shape != (self.num_offspring, self.num_genes): + if self.last_generation_offspring_crossover.shape[0] != self.num_offspring: + raise ValueError(f"Size mismatch between the crossover output {self.last_generation_offspring_crossover.shape} and the expected crossover output {(self.num_offspring, self.num_genes)}. It is expected to produce ({self.num_offspring}) offspring but ({self.last_generation_offspring_crossover.shape[0]}) produced.") + elif self.last_generation_offspring_crossover.shape[1] != self.num_genes: + raise ValueError(f"Size mismatch between the crossover output {self.last_generation_offspring_crossover.shape} and the expected crossover output {(self.num_offspring, self.num_genes)}. It is expected that the offspring has ({self.num_genes}) genes but ({self.last_generation_offspring_crossover.shape[1]}) produced.") + + # PyGAD 2.18.2 // The on_crossover() callback function is called even if crossover_type is None. + if not (self.on_crossover is None): + on_crossover_output = self.on_crossover(self, + self.last_generation_offspring_crossover) + if on_crossover_output is None: + pass + else: + if type(on_crossover_output) in [tuple, list, numpy.ndarray]: + on_crossover_output = numpy.array(on_crossover_output) + if on_crossover_output.shape == self.last_generation_offspring_crossover.shape: + self.last_generation_offspring_crossover = on_crossover_output + else: + raise ValueError(f"Size mismatch between the output of on_crossover() {on_crossover_output.shape} and the expected crossover output {self.last_generation_offspring_crossover.shape}.") + else: + raise ValueError(f"The output of on_crossover() is expected to be tuple/list/numpy.ndarray but {type(on_crossover_output)} found.") + + def run_mutation(self): + """ + This method must be only callled from inside the run() method. It is not meant for use by the user. + Generally, any method with a name starting with 'run_' is meant to be only called by PyGAD from inside the 'run()' method. + + The objective of the 'run_mutation()' method is to apply mutation and call the callable on_mutation() if defined. + It does not return any variables. However, it changes this attribute of the pygad.GA class instances: + 1) last_generation_offspring_mutation: A NumPy array of the mutated offspring. + + Returns + ------- + None. + """ + + # If self.mutation_type=None, then no mutation is applied and thus no changes are applied to the offspring created using the crossover operation. The offspring will be used unchanged in the next generation. + if self.mutation_type is None: + self.last_generation_offspring_mutation = self.last_generation_offspring_crossover + else: + # Adding some variations to the offspring using mutation. + if callable(self.mutation_type): + self.last_generation_offspring_mutation = self.mutation(self.last_generation_offspring_crossover, + self) + if not type(self.last_generation_offspring_mutation) is numpy.ndarray: + raise TypeError(f"The output of the mutation step is expected to be of type (numpy.ndarray) but {type(self.last_generation_offspring_mutation)} found.") + else: + self.last_generation_offspring_mutation = self.mutation(self.last_generation_offspring_crossover) + + if self.last_generation_offspring_mutation.shape != (self.num_offspring, self.num_genes): + if self.last_generation_offspring_mutation.shape[0] != self.num_offspring: + raise ValueError(f"Size mismatch between the mutation output {self.last_generation_offspring_mutation.shape} and the expected mutation output {(self.num_offspring, self.num_genes)}. It is expected to produce ({self.num_offspring}) offspring but ({self.last_generation_offspring_mutation.shape[0]}) produced.") + elif self.last_generation_offspring_mutation.shape[1] != self.num_genes: + raise ValueError(f"Size mismatch between the mutation output {self.last_generation_offspring_mutation.shape} and the expected mutation output {(self.num_offspring, self.num_genes)}. It is expected that the offspring has ({self.num_genes}) genes but ({self.last_generation_offspring_mutation.shape[1]}) produced.") + + # PyGAD 2.18.2 // The on_mutation() callback function is called even if mutation_type is None. + if not (self.on_mutation is None): + on_mutation_output = self.on_mutation(self, + self.last_generation_offspring_mutation) + + if on_mutation_output is None: + pass + else: + if type(on_mutation_output) in [tuple, list, numpy.ndarray]: + on_mutation_output = numpy.array(on_mutation_output) + if on_mutation_output.shape == self.last_generation_offspring_mutation.shape: + self.last_generation_offspring_mutation = on_mutation_output + else: + raise ValueError(f"Size mismatch between the output of on_mutation() {on_mutation_output.shape} and the expected mutation output {self.last_generation_offspring_mutation.shape}.") + else: + raise ValueError(f"The output of on_mutation() is expected to be tuple/list/numpy.ndarray but {type(on_mutation_output)} found.") + + def run_update_population(self): + """ + This method must be only callled from inside the run() method. It is not meant for use by the user. + Generally, any method with a name starting with 'run_' is meant to be only called by PyGAD from inside the 'run()' method. + + The objective of the 'run_update_population()' method is to update the 'population' attribute after completing the processes of crossover and mutation. + It does not return any variables. However, it changes this attribute of the pygad.GA class instances: + 1) population: A NumPy array of the population of solutions/chromosomes. + + Returns + ------- + None. + """ + + # Update the population attribute according to the offspring generated. + if self.keep_elitism == 0: + # If the keep_elitism parameter is 0, then the keep_parents parameter will be used to decide if the parents are kept in the next generation. + if (self.keep_parents == 0): + self.population = self.last_generation_offspring_mutation + elif (self.keep_parents == -1): + # Creating the new population based on the parents and offspring. + self.population[0:self.last_generation_parents.shape[0],:] = self.last_generation_parents + self.population[self.last_generation_parents.shape[0]:, :] = self.last_generation_offspring_mutation + elif (self.keep_parents > 0): + parents_to_keep, _ = self.steady_state_selection(self.last_generation_fitness, + num_parents=self.keep_parents) + self.population[0:parents_to_keep.shape[0],:] = parents_to_keep + self.population[parents_to_keep.shape[0]:,:] = self.last_generation_offspring_mutation + else: + self.last_generation_elitism, self.last_generation_elitism_indices = self.steady_state_selection(self.last_generation_fitness, + num_parents=self.keep_elitism) + self.population[0:self.last_generation_elitism.shape[0],:] = self.last_generation_elitism + self.population[self.last_generation_elitism.shape[0]:, :] = self.last_generation_offspring_mutation + + 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. + The following are returned: + -best_solution: Best solution in the current population. + -best_solution_fitness: Fitness value of the best solution. + -best_match_idx: Index of the best solution in the current population. + """ + + 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. + pop_fitness = self.cal_pop_fitness() + # Verify the type of the 'pop_fitness' parameter. + elif type(pop_fitness) in [tuple, list, numpy.ndarray]: + # Verify that the length of the passed population fitness matches the length of the 'self.population' attribute. + if len(pop_fitness) == len(self.population): + # This successfully verifies the 'pop_fitness' parameter. + pass + else: + raise ValueError(f"The length of the list/tuple/numpy.ndarray passed to the 'pop_fitness' parameter ({len(pop_fitness)}) must match the length of the 'self.population' attribute ({len(self.population)}).") + else: + raise ValueError(f"The type of the 'pop_fitness' parameter is expected to be list, tuple, or numpy.ndarray but ({type(pop_fitness)}) found.") + + # Return the index of the best solution that has the best fitness value. + best_match_idx = numpy.where( + pop_fitness == numpy.max(pop_fitness))[0][0] + + best_solution = self.population[best_match_idx, :].copy() + best_solution_fitness = pop_fitness[best_match_idx] + except Exception as ex: + self.logger.exception(ex) + # sys.exit(-1) + raise ex + + return best_solution, best_solution_fitness, best_match_idx + + def save(self, filename): + """ + Saves the genetic algorithm instance: + -filename: Name of the file to save the instance. No extension is needed. + """ + + cloudpickle_serialized_object = cloudpickle.dumps(self) + with open(filename + ".pkl", 'wb') as file: + file.write(cloudpickle_serialized_object) + cloudpickle.dump(self, file) + + def summary(self, + line_length=70, + fill_character=" ", + line_character="-", + line_character2="=", + columns_equal_len=False, + print_step_parameters=True, + print_parameters_summary=True): + """ + The summary() method prints a summary of the PyGAD lifecycle in a Keras style. + The parameters are: + line_length: 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: The table rows are split into equal-sized columns or split subjective to the width needed. + print_step_parameters: 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: 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. + """ + + summary_output = "" + + def fill_message(msg, line_length=line_length, fill_character=fill_character): + num_spaces = int((line_length - len(msg))/2) + num_spaces = int(num_spaces / len(fill_character)) + msg = "{spaces}{msg}{spaces}".format( + msg=msg, spaces=fill_character * num_spaces) + return msg + + def line_separator(line_length=line_length, line_character=line_character): + num_characters = int(line_length / len(line_character)) + return line_character * num_characters + + def create_row(columns, line_length=line_length, fill_character=fill_character, split_percentages=None): + filled_columns = [] + if split_percentages == None: + split_percentages = [int(100/len(columns))] * 3 + columns_lengths = [int((split_percentages[idx] * line_length) / 100) + for idx in range(len(split_percentages))] + for column_idx, column in enumerate(columns): + current_column_length = len(column) + extra_characters = columns_lengths[column_idx] - \ + current_column_length + filled_column = column + fill_character * extra_characters + filled_column = column + fill_character * extra_characters + filled_columns.append(filled_column) + + return "".join(filled_columns) + + def print_parent_selection_params(): + nonlocal summary_output + m = f"Number of Parents: {self.num_parents_mating}" + self.logger.info(m) + summary_output = summary_output + m + "\n" + if self.parent_selection_type == "tournament": + m = f"K Tournament: {self.K_tournament}" + self.logger.info(m) + summary_output = summary_output + m + "\n" + + def print_fitness_params(): + nonlocal summary_output + if not self.fitness_batch_size is None: + m = f"Fitness batch size: {self.fitness_batch_size}" + self.logger.info(m) + summary_output = summary_output + m + "\n" + + def print_crossover_params(): + nonlocal summary_output + if not self.crossover_probability is None: + m = f"Crossover probability: {self.crossover_probability}" + self.logger.info(m) + summary_output = summary_output + m + "\n" + + def print_mutation_params(): + nonlocal summary_output + if not self.mutation_probability is None: + m = f"Mutation Probability: {self.mutation_probability}" + self.logger.info(m) + summary_output = summary_output + m + "\n" + if self.mutation_percent_genes == "default": + 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. + m = f"Mutation Genes: {self.mutation_num_genes}" + self.logger.info(m) + summary_output = summary_output + m + "\n" + m = f"Random Mutation Range: ({self.random_mutation_min_val}, {self.random_mutation_max_val})" + self.logger.info(m) + summary_output = summary_output + m + "\n" + if not self.gene_space is None: + m = f"Gene Space: {self.gene_space}" + self.logger.info(m) + summary_output = summary_output + m + "\n" + m = f"Mutation by Replacement: {self.mutation_by_replacement}" + self.logger.info(m) + summary_output = summary_output + m + "\n" + m = f"Allow Duplicated Genes: {self.allow_duplicate_genes}" + self.logger.info(m) + summary_output = summary_output + m + "\n" + + def print_on_generation_params(): + nonlocal summary_output + if not self.stop_criteria is None: + m = f"Stop Criteria: {self.stop_criteria}" + self.logger.info(m) + summary_output = summary_output + m + "\n" + + def print_params_summary(): + nonlocal summary_output + m = f"Population Size: ({self.sol_per_pop}, {self.num_genes})" + self.logger.info(m) + summary_output = summary_output + m + "\n" + m = f"Number of Generations: {self.num_generations}" + self.logger.info(m) + summary_output = summary_output + m + "\n" + m = f"Initial Population Range: ({self.init_range_low}, {self.init_range_high})" + self.logger.info(m) + summary_output = summary_output + m + "\n" + + if not print_step_parameters: + print_fitness_params() + + if not print_step_parameters: + print_parent_selection_params() + + if self.keep_elitism != 0: + m = f"Keep Elitism: {self.keep_elitism}" + self.logger.info(m) + summary_output = summary_output + m + "\n" + else: + m = f"Keep Parents: {self.keep_parents}" + self.logger.info(m) + summary_output = summary_output + m + "\n" + m = f"Gene DType: {self.gene_type}" + self.logger.info(m) + summary_output = summary_output + m + "\n" + + if not print_step_parameters: + print_crossover_params() + + if not print_step_parameters: + print_mutation_params() + + if not print_step_parameters: + print_on_generation_params() + + if not self.parallel_processing is None: + m = f"Parallel Processing: {self.parallel_processing}" + self.logger.info(m) + summary_output = summary_output + m + "\n" + if not self.random_seed is None: + m = f"Random Seed: {self.random_seed}" + self.logger.info(m) + summary_output = summary_output + m + "\n" + m = f"Save Best Solutions: {self.save_best_solutions}" + self.logger.info(m) + summary_output = summary_output + m + "\n" + m = f"Save Solutions: {self.save_solutions}" + self.logger.info(m) + summary_output = summary_output + m + "\n" + + m = line_separator(line_character=line_character) + self.logger.info(m) + summary_output = summary_output + m + "\n" + m = fill_message("PyGAD Lifecycle") + self.logger.info(m) + summary_output = summary_output + m + "\n" + m = line_separator(line_character=line_character2) + self.logger.info(m) + summary_output = summary_output + m + "\n" + + lifecycle_steps = ["on_start()", "Fitness Function", "On Fitness", "Parent Selection", "On Parents", + "Crossover", "On Crossover", "Mutation", "On Mutation", "On Generation", "On Stop"] + lifecycle_functions = [self.on_start, self.fitness_func, self.on_fitness, self.select_parents, self.on_parents, + self.crossover, self.on_crossover, self.mutation, self.on_mutation, self.on_generation, self.on_stop] + lifecycle_functions = [getattr( + lifecycle_func, '__name__', "None") for lifecycle_func in lifecycle_functions] + lifecycle_functions = [lifecycle_func + "()" if lifecycle_func != + "None" else "None" for lifecycle_func in lifecycle_functions] + lifecycle_output = ["None", "(1)", "None", f"({self.num_parents_mating}, {self.num_genes})", "None", + f"({self.num_parents_mating}, {self.num_genes})", "None", f"({self.num_parents_mating}, {self.num_genes})", "None", "None", "None"] + lifecycle_step_parameters = [None, print_fitness_params, None, print_parent_selection_params, None, + 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( + 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] + else: + split_percentages = None + + header_columns = ["Step", "Handler", "Output Shape"] + header_row = create_row( + header_columns, split_percentages=split_percentages) + m = header_row + self.logger.info(m) + summary_output = summary_output + m + "\n" + m = line_separator(line_character=line_character2) + self.logger.info(m) + summary_output = summary_output + m + "\n" + + for lifecycle_idx in range(len(lifecycle_steps)): + lifecycle_column = [lifecycle_steps[lifecycle_idx], + lifecycle_functions[lifecycle_idx], lifecycle_output[lifecycle_idx]] + if lifecycle_column[1] == "None": + continue + lifecycle_row = create_row( + lifecycle_column, split_percentages=split_percentages) + m = lifecycle_row + self.logger.info(m) + summary_output = summary_output + m + "\n" + if print_step_parameters: + if not lifecycle_step_parameters[lifecycle_idx] is None: + lifecycle_step_parameters[lifecycle_idx]() + m = line_separator(line_character=line_character) + self.logger.info(m) + summary_output = summary_output + m + "\n" + + m = line_separator(line_character=line_character2) + self.logger.info(m) + summary_output = summary_output + m + "\n" + if print_parameters_summary: + print_params_summary() + m = line_separator(line_character=line_character2) + self.logger.info(m) + summary_output = summary_output + m + "\n" + return summary_output + +def load(filename): + """ + Reads a saved instance of the genetic algorithm: + -filename: Name of the file to read the instance. No extension is needed. + Returns the genetic algorithm instance. + """ + + try: + with open(filename + ".pkl", 'rb') as file: + ga_in = cloudpickle.load(file) + except FileNotFoundError: + raise FileNotFoundError(f"Error reading the file {filename}. Please check your inputs.") + except: + # raise BaseException("Error loading the file. If the file already exists, please reload all the functions previously used (e.g. fitness function).") + raise BaseException("Error loading the file.") + return ga_in \ No newline at end of file diff --git a/pygad/torchga/__init__.py b/pygad/torchga/__init__.py new file mode 100644 index 00000000..b7b98f58 --- /dev/null +++ b/pygad/torchga/__init__.py @@ -0,0 +1,3 @@ +from .torchga import * + +__version__ = "1.4.0" diff --git a/pygad/torchga/torchga.py b/pygad/torchga/torchga.py new file mode 100644 index 00000000..e552d3dd --- /dev/null +++ b/pygad/torchga/torchga.py @@ -0,0 +1,91 @@ +import copy +import numpy +import torch + +def model_weights_as_vector(model): + weights_vector = [] + + 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. + curr_weights = curr_weights.cpu().detach().numpy() + vector = numpy.reshape(curr_weights, newshape=(curr_weights.size)) + weights_vector.extend(vector) + + return numpy.array(weights_vector) + +def model_weights_as_dict(model, weights_vector): + weights_dict = model.state_dict() + + 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. + w_matrix = weights_dict[key].cpu().detach().numpy() + layer_weights_shape = w_matrix.shape + layer_weights_size = w_matrix.size + + layer_weights_vector = weights_vector[start:start + layer_weights_size] + layer_weights_matrix = numpy.reshape(layer_weights_vector, newshape=(layer_weights_shape)) + weights_dict[key] = torch.from_numpy(layer_weights_matrix) + + start = start + layer_weights_size + + return weights_dict + +def predict(model, solution, data): + # Fetch the parameters of the best solution. + model_weights_dict = model_weights_as_dict(model=model, + weights_vector=solution) + + # Use the current solution as the model parameters. + _model = copy.deepcopy(model) + _model.load_state_dict(model_weights_dict) + + with torch.no_grad(): + predictions = _model(data) + + return predictions + +class TorchGA: + + def __init__(self, model, num_solutions): + + """ + Creates an instance of the TorchGA class to build a population of model parameters. + + model: A PyTorch model class. + num_solutions: Number of solutions in the population. Each solution has different model parameters. + """ + + self.model = model + + self.num_solutions = num_solutions + + # A list holding references to all the solutions (i.e. networks) used in the population. + self.population_weights = self.create_population() + + 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. + + The method returns a list holding the weights of all solutions. + """ + + model_weights_vector = model_weights_as_vector(model=self.model) + + net_population_weights = [] + net_population_weights.append(model_weights_vector) + + for idx in range(self.num_solutions-1): + + net_weights = copy.deepcopy(model_weights_vector) + net_weights = numpy.array(net_weights) + numpy.random.uniform(low=-1.0, high=1.0, size=model_weights_vector.size) + + # Appending the weights to the population. + net_population_weights.append(net_weights) + + return net_population_weights diff --git a/pygad/utils/__init__.py b/pygad/utils/__init__.py new file mode 100644 index 00000000..b39eda15 --- /dev/null +++ b/pygad/utils/__init__.py @@ -0,0 +1,6 @@ +from pygad.utils import parent_selection +from pygad.utils import crossover +from pygad.utils import mutation +from pygad.utils import nsga2 + +__version__ = "1.2.1" \ No newline at end of file diff --git a/pygad/utils/crossover.py b/pygad/utils/crossover.py new file mode 100644 index 00000000..d7cd86ef --- /dev/null +++ b/pygad/utils/crossover.py @@ -0,0 +1,282 @@ +""" +The pygad.utils.crossover module has all the built-in crossover operators. +""" + +import numpy +import random + +class Crossover: + + def __init__(): + pass + + def single_point_crossover(self, parents, offspring_size): + + """ + Applies single-point crossover between pairs of parents. + This function selects a random point at which crossover occurs between the parents, generating offspring. + + Parameters: + parents (array-like): The parents to mate for producing the offspring. + offspring_size (int): The number of offspring to produce. + + Returns: + array-like: An array containing the produced offspring. + """ + + if self.gene_type_single == True: + offspring = numpy.empty(offspring_size, dtype=self.gene_type[0]) + else: + offspring = numpy.empty(offspring_size, dtype=object) + + # Randomly generate all the K points at which crossover takes place between each two parents. The point does not have to be always at the center of the solutions. + # This saves time by calling the numpy.random.randint() function only once. + crossover_points = numpy.random.randint(low=0, + high=parents.shape[1], + size=offspring_size[0]) + + for k in range(offspring_size[0]): + # Check if the crossover_probability parameter is used. + if not (self.crossover_probability is None): + probs = numpy.random.random(size=parents.shape[0]) + indices = list(set(numpy.where(probs <= self.crossover_probability)[0])) + + # If no parent satisfied the probability, no crossover is applied and a parent is selected as is. + if len(indices) == 0: + offspring[k, :] = parents[k % parents.shape[0], :] + continue + elif len(indices) == 1: + parent1_idx = indices[0] + parent2_idx = parent1_idx + else: + indices = random.sample(indices, 2) + parent1_idx = indices[0] + parent2_idx = indices[1] + else: + # Index of the first parent to mate. + parent1_idx = k % parents.shape[0] + # 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. + 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. + offspring[k, crossover_points[k]:] = parents[parent2_idx, crossover_points[k]:] + + if self.allow_duplicate_genes == False: + if self.gene_space is None: + offspring[k], _, _ = self.solve_duplicate_genes_randomly(solution=offspring[k], + min_val=self.random_mutation_min_val, + max_val=self.random_mutation_max_val, + mutation_by_replacement=self.mutation_by_replacement, + gene_type=self.gene_type, + num_trials=10) + else: + offspring[k], _, _ = self.solve_duplicate_genes_by_space(solution=offspring[k], + gene_type=self.gene_type, + num_trials=10) + + + return offspring + + def two_points_crossover(self, parents, offspring_size): + + """ + Applies the 2 points crossover. It selects the 2 points randomly at which crossover takes place between the pairs of parents. + 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. + """ + + if self.gene_type_single == True: + offspring = numpy.empty(offspring_size, dtype=self.gene_type[0]) + else: + offspring = numpy.empty(offspring_size, dtype=object) + + # Randomly generate all the first K points at which crossover takes place between each two parents. + # This saves time by calling the numpy.random.randint() function only once. + if (parents.shape[1] == 1): # If the chromosome has only a single gene. In this case, this gene is copied from the second parent. + crossover_points_1 = numpy.zeros(offspring_size[0]) + else: + crossover_points_1 = numpy.random.randint(low=0, + high=numpy.ceil(parents.shape[1]/2 + 1), + size=offspring_size[0]) + + # The second point must always be greater than the first point. + crossover_points_2 = crossover_points_1 + int(parents.shape[1]/2) + + for k in range(offspring_size[0]): + + if not (self.crossover_probability is None): + probs = numpy.random.random(size=parents.shape[0]) + indices = list(set(numpy.where(probs <= self.crossover_probability)[0])) + + # If no parent satisfied the probability, no crossover is applied and a parent is selected. + if len(indices) == 0: + offspring[k, :] = parents[k % parents.shape[0], :] + continue + elif len(indices) == 1: + parent1_idx = indices[0] + parent2_idx = parent1_idx + else: + indices = random.sample(indices, 2) + parent1_idx = indices[0] + parent2_idx = indices[1] + else: + # Index of the first parent to mate. + parent1_idx = k % parents.shape[0] + # Index of the second parent to mate. + parent2_idx = (k+1) % parents.shape[0] + + # The genes from the beginning of the chromosome up to the first point are copied from the first parent. + offspring[k, 0:crossover_points_1[k]] = parents[parent1_idx, 0:crossover_points_1[k]] + # The genes from the second point up to the end of the chromosome are copied from the first parent. + offspring[k, crossover_points_2[k]:] = parents[parent1_idx, crossover_points_2[k]:] + # The genes between the 2 points are copied from the second parent. + offspring[k, crossover_points_1[k]:crossover_points_2[k]] = parents[parent2_idx, crossover_points_1[k]:crossover_points_2[k]] + + if self.allow_duplicate_genes == False: + if self.gene_space is None: + offspring[k], _, _ = self.solve_duplicate_genes_randomly(solution=offspring[k], + min_val=self.random_mutation_min_val, + max_val=self.random_mutation_max_val, + mutation_by_replacement=self.mutation_by_replacement, + gene_type=self.gene_type, + num_trials=10) + else: + offspring[k], _, _ = self.solve_duplicate_genes_by_space(solution=offspring[k], + gene_type=self.gene_type, + num_trials=10) + return offspring + + def uniform_crossover(self, parents, offspring_size): + + """ + 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. + 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. + """ + + if self.gene_type_single == True: + offspring = numpy.empty(offspring_size, dtype=self.gene_type[0]) + else: + offspring = numpy.empty(offspring_size, dtype=object) + + # Randomly generate all the genes sources at which crossover takes place between each two parents. + # This saves time by calling the numpy.random.randint() function only once. + # There is a list of 0 and 1 for each offspring. + # [0, 1, 0, 0, 1, 1]: If the value is 0, then take the gene from the first parent. If 1, take it from the second parent. + genes_sources = numpy.random.randint(low=0, + high=2, + size=offspring_size) + + for k in range(offspring_size[0]): + if not (self.crossover_probability is None): + probs = numpy.random.random(size=parents.shape[0]) + indices = list(set(numpy.where(probs <= self.crossover_probability)[0])) + + # If no parent satisfied the probability, no crossover is applied and a parent is selected. + if len(indices) == 0: + offspring[k, :] = parents[k % parents.shape[0], :] + continue + elif len(indices) == 1: + parent1_idx = indices[0] + parent2_idx = parent1_idx + else: + indices = random.sample(indices, 2) + parent1_idx = indices[0] + parent2_idx = indices[1] + else: + # Index of the first parent to mate. + parent1_idx = k % parents.shape[0] + # 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] + + if self.allow_duplicate_genes == False: + if self.gene_space is None: + offspring[k], _, _ = self.solve_duplicate_genes_randomly(solution=offspring[k], + min_val=self.random_mutation_min_val, + max_val=self.random_mutation_max_val, + mutation_by_replacement=self.mutation_by_replacement, + gene_type=self.gene_type, + num_trials=10) + else: + offspring[k], _, _ = self.solve_duplicate_genes_by_space(solution=offspring[k], + gene_type=self.gene_type, + num_trials=10) + + return offspring + + def scattered_crossover(self, parents, offspring_size): + + """ + Applies the scattered crossover. It randomly selects the gene from one of the 2 parents. + 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. + """ + + if self.gene_type_single == True: + offspring = numpy.empty(offspring_size, dtype=self.gene_type[0]) + else: + offspring = numpy.empty(offspring_size, dtype=object) + + # Randomly generate all the genes sources at which crossover takes place between each two parents. + # This saves time by calling the numpy.random.randint() function only once. + # There is a list of 0 and 1 for each offspring. + # [0, 1, 0, 0, 1, 1]: If the value is 0, then take the gene from the first parent. If 1, take it from the second parent. + genes_sources = numpy.random.randint(low=0, + high=2, + size=offspring_size) + + for k in range(offspring_size[0]): + if not (self.crossover_probability is None): + probs = numpy.random.random(size=parents.shape[0]) + indices = list(set(numpy.where(probs <= self.crossover_probability)[0])) + + # If no parent satisfied the probability, no crossover is applied and a parent is selected. + if len(indices) == 0: + offspring[k, :] = parents[k % parents.shape[0], :] + continue + elif len(indices) == 1: + parent1_idx = indices[0] + parent2_idx = parent1_idx + else: + indices = random.sample(indices, 2) + parent1_idx = indices[0] + parent2_idx = indices[1] + else: + # Index of the first parent to mate. + parent1_idx = k % parents.shape[0] + # Index of the second parent to mate. + parent2_idx = (k+1) % parents.shape[0] + + 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: + offspring[k], _, _ = self.solve_duplicate_genes_randomly(solution=offspring[k], + min_val=self.random_mutation_min_val, + max_val=self.random_mutation_max_val, + mutation_by_replacement=self.mutation_by_replacement, + gene_type=self.gene_type, + num_trials=10) + else: + offspring[k], _, _ = self.solve_duplicate_genes_by_space(solution=offspring[k], + gene_type=self.gene_type, + num_trials=10) + return offspring diff --git a/pygad/utils/mutation.py b/pygad/utils/mutation.py new file mode 100644 index 00000000..d0ca1b4e --- /dev/null +++ b/pygad/utils/mutation.py @@ -0,0 +1,1127 @@ +""" +The pygad.utils.mutation module has all the built-in mutation operators. +""" + +import numpy +import random + +import pygad +import concurrent.futures + +class Mutation: + + def __init__(): + pass + + def random_mutation(self, offspring): + + """ + Applies the random mutation which changes the values of a number of genes randomly. + The random value is selected either using the 'gene_space' parameter or the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val'. + It accepts a single parameter: + -offspring: The offspring to mutate. + It returns an array of the mutated offspring. + """ + + # If the mutation values are selected from the mutation space, the attribute 'gene_space' is not None. Otherwise, it is None. + # When the 'mutation_probability' parameter exists (i.e. not None), then it is used in the mutation. Otherwise, the 'mutation_num_genes' parameter is used. + + if self.mutation_probability is None: + # When the 'mutation_probability' parameter does not exist (i.e. None), then the parameter 'mutation_num_genes' is used in the mutation. + if not (self.gene_space is None): + # When the attribute 'gene_space' exists (i.e. not None), the mutation values are selected randomly from the space of values of each gene. + offspring = self.mutation_by_space(offspring) + else: + offspring = self.mutation_randomly(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'. + offspring = self.mutation_probs_by_space(offspring) + else: + offspring = self.mutation_probs_randomly(offspring) + + return offspring + + def mutation_by_space(self, offspring): + + """ + Applies the random mutation using the mutation values' space. + It accepts a single parameter: + -offspring: The offspring to mutate. + It returns an array of the mutated offspring using the mutation space. + """ + + # For each offspring, a value from the gene space is selected randomly and assigned to the selected mutated gene. + for offspring_idx in range(offspring.shape[0]): + mutation_indices = numpy.array(random.sample(range(0, self.num_genes), self.mutation_num_genes)) + for gene_idx in mutation_indices: + + if type(self.random_mutation_min_val) in self.supported_int_float_types: + range_min = self.random_mutation_min_val + range_max = self.random_mutation_max_val + else: + range_min = self.random_mutation_min_val[gene_idx] + range_max = self.random_mutation_max_val[gene_idx] + + if self.gene_space_nested: + # Returning the current gene space from the 'gene_space' attribute. + if type(self.gene_space[gene_idx]) in [numpy.ndarray, list]: + curr_gene_space = self.gene_space[gene_idx].copy() + else: + curr_gene_space = self.gene_space[gene_idx] + + # If the gene space has only a single value, use it as the new gene value. + if type(curr_gene_space) in pygad.GA.supported_int_float_types: + value_from_space = curr_gene_space + # If the gene space is None, apply mutation by adding a random value between the range defined by the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val'. + elif curr_gene_space is None: + rand_val = numpy.random.uniform(low=range_min, + high=range_max, + size=1)[0] + if self.mutation_by_replacement: + value_from_space = rand_val + else: + value_from_space = offspring[offspring_idx, gene_idx] + rand_val + elif type(curr_gene_space) is dict: + # The gene's space of type dict specifies the lower and upper limits of a gene. + if 'step' in curr_gene_space.keys(): + # The numpy.random.choice() and numpy.random.uniform() functions return a NumPy array as the output even if the array has a single value. + # We have to return the output at index 0 to force a numeric value to be returned not an object of type numpy.ndarray. + # If numpy.ndarray is returned, then it will cause an issue later while using the set() function. + # Randomly select a value from a discrete range. + value_from_space = numpy.random.choice(numpy.arange(start=curr_gene_space['low'], + stop=curr_gene_space['high'], + step=curr_gene_space['step']), + size=1)[0] + else: + # Return the current gene value. + value_from_space = offspring[offspring_idx, gene_idx] + # Generate a random value to be added to the current gene value. + rand_val = numpy.random.uniform(low=range_min, + high=range_max, + size=1)[0] + # The objective is to have a new gene value that respects the gene_space boundaries. + # The next if-else block checks if adding the random value keeps the new gene value within the gene_space boundaries. + temp_val = value_from_space + rand_val + if temp_val < curr_gene_space['low']: + # Restrict the new value to be > curr_gene_space['low'] + # If subtracting the random value makes the new gene value outside the boundaries [low, high), then use the lower boundary the gene value. + if curr_gene_space['low'] <= value_from_space - rand_val < curr_gene_space['high']: + # Because subtracting the random value keeps the new gene value within the boundaries [low, high), then use such a value as the gene value. + temp_val = value_from_space - rand_val + else: + # Because subtracting the random value makes the new gene value outside the boundaries [low, high), then use the lower boundary as the gene value. + temp_val = curr_gene_space['low'] + elif temp_val >= curr_gene_space['high']: + # Restrict the new value to be < curr_gene_space['high'] + # If subtracting the random value makes the new gene value outside the boundaries [low, high), then use such a value as the gene value. + if curr_gene_space['low'] <= value_from_space - rand_val < curr_gene_space['high']: + # Because subtracting the random value keeps the new value within the boundaries [low, high), then use such a value as the gene value. + temp_val = value_from_space - rand_val + else: + # Because subtracting the random value makes the new gene value outside the boundaries [low, high), then use the lower boundary as the gene value. + temp_val = curr_gene_space['low'] + value_from_space = temp_val + else: + # Selecting a value randomly based on the current gene's space in the 'gene_space' attribute. + # If the gene space has only 1 value, then select it. The old and new values of the gene are identical. + if len(curr_gene_space) == 1: + value_from_space = curr_gene_space[0] + # If the gene space has more than 1 value, then select a new one that is different from the current value. + else: + values_to_select_from = list(set(curr_gene_space) - set([offspring[offspring_idx, gene_idx]])) + + if len(values_to_select_from) == 0: + value_from_space = offspring[offspring_idx, gene_idx] + else: + value_from_space = random.choice(values_to_select_from) + else: + # Selecting a value randomly from the global gene space in the 'gene_space' attribute. + if type(self.gene_space) is dict: + # When the gene_space is assigned a dict object, then it specifies the lower and upper limits of all genes in the space. + if 'step' in self.gene_space.keys(): + value_from_space = numpy.random.choice(numpy.arange(start=self.gene_space['low'], + stop=self.gene_space['high'], + step=self.gene_space['step']), + size=1)[0] + else: + value_from_space = numpy.random.uniform(low=self.gene_space['low'], + high=self.gene_space['high'], + size=1)[0] + else: + # If the space type is not of type dict, then a value is randomly selected from the gene_space attribute. + values_to_select_from = list(set(self.gene_space) - set([offspring[offspring_idx, gene_idx]])) + + if len(values_to_select_from) == 0: + value_from_space = offspring[offspring_idx, gene_idx] + else: + value_from_space = random.choice(values_to_select_from) + # value_from_space = random.choice(self.gene_space) + + if value_from_space is None: + # TODO: Return index 0. + # TODO: Check if this if statement is necessary. + value_from_space = numpy.random.uniform(low=range_min, + high=range_max, + size=1)[0] + + # Assinging the selected value from the space to the gene. + if self.gene_type_single == True: + if not self.gene_type[1] is None: + offspring[offspring_idx, gene_idx] = numpy.round(self.gene_type[0](value_from_space), + self.gene_type[1]) + else: + offspring[offspring_idx, gene_idx] = self.gene_type[0](value_from_space) + else: + if not self.gene_type[gene_idx][1] is None: + offspring[offspring_idx, gene_idx] = numpy.round(self.gene_type[gene_idx][0](value_from_space), + self.gene_type[gene_idx][1]) + + else: + offspring[offspring_idx, gene_idx] = self.gene_type[gene_idx][0](value_from_space) + + if self.allow_duplicate_genes == False: + offspring[offspring_idx], _, _ = self.solve_duplicate_genes_by_space(solution=offspring[offspring_idx], + gene_type=self.gene_type, + num_trials=10) + return 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 mutation probability, then it will be mutated based on the mutation space. + It accepts a single parameter: + -offspring: The offspring to mutate. + It returns an array of the mutated offspring using the mutation space. + """ + + # For each offspring, a value from the gene space is selected randomly and assigned to the selected mutated gene. + for offspring_idx in range(offspring.shape[0]): + probs = numpy.random.random(size=offspring.shape[1]) + for gene_idx in range(offspring.shape[1]): + + if type(self.random_mutation_min_val) in self.supported_int_float_types: + range_min = self.random_mutation_min_val + range_max = self.random_mutation_max_val + else: + range_min = self.random_mutation_min_val[gene_idx] + range_max = self.random_mutation_max_val[gene_idx] + + if probs[gene_idx] <= self.mutation_probability: + if self.gene_space_nested: + # Returning the current gene space from the 'gene_space' attribute. + if type(self.gene_space[gene_idx]) in [numpy.ndarray, list]: + curr_gene_space = self.gene_space[gene_idx].copy() + else: + curr_gene_space = self.gene_space[gene_idx] + + # If the gene space has only a single value, use it as the new gene value. + if type(curr_gene_space) in pygad.GA.supported_int_float_types: + value_from_space = curr_gene_space + # If the gene space is None, apply mutation by adding a random value between the range defined by the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val'. + elif curr_gene_space is None: + rand_val = numpy.random.uniform(low=range_min, + high=range_max, + size=1)[0] + if self.mutation_by_replacement: + value_from_space = rand_val + else: + value_from_space = offspring[offspring_idx, gene_idx] + rand_val + elif type(curr_gene_space) is dict: + # Selecting a value randomly from the current gene's space in the 'gene_space' attribute. + if 'step' in curr_gene_space.keys(): + value_from_space = numpy.random.choice(numpy.arange(start=curr_gene_space['low'], + stop=curr_gene_space['high'], + step=curr_gene_space['step']), + size=1)[0] + else: + value_from_space = numpy.random.uniform(low=curr_gene_space['low'], + high=curr_gene_space['high'], + size=1)[0] + else: + # Selecting a value randomly from the current gene's space in the 'gene_space' attribute. + # If the gene space has only 1 value, then select it. The old and new values of the gene are identical. + if len(curr_gene_space) == 1: + value_from_space = curr_gene_space[0] + # If the gene space has more than 1 value, then select a new one that is different from the current value. + else: + values_to_select_from = list(set(curr_gene_space) - set([offspring[offspring_idx, gene_idx]])) + + if len(values_to_select_from) == 0: + value_from_space = offspring[offspring_idx, gene_idx] + else: + value_from_space = random.choice(values_to_select_from) + else: + # Selecting a value randomly from the global gene space in the 'gene_space' attribute. + if type(self.gene_space) is dict: + if 'step' in self.gene_space.keys(): + value_from_space = numpy.random.choice(numpy.arange(start=self.gene_space['low'], + stop=self.gene_space['high'], + step=self.gene_space['step']), + size=1)[0] + else: + value_from_space = numpy.random.uniform(low=self.gene_space['low'], + high=self.gene_space['high'], + size=1)[0] + else: + values_to_select_from = list(set(self.gene_space) - set([offspring[offspring_idx, gene_idx]])) + + if len(values_to_select_from) == 0: + value_from_space = offspring[offspring_idx, gene_idx] + else: + value_from_space = random.choice(values_to_select_from) + + # Assigning the selected value from the space to the gene. + if self.gene_type_single == True: + if not self.gene_type[1] is None: + offspring[offspring_idx, gene_idx] = numpy.round(self.gene_type[0](value_from_space), + self.gene_type[1]) + else: + offspring[offspring_idx, gene_idx] = self.gene_type[0](value_from_space) + else: + if not self.gene_type[gene_idx][1] is None: + offspring[offspring_idx, gene_idx] = numpy.round(self.gene_type[gene_idx][0](value_from_space), + self.gene_type[gene_idx][1]) + else: + offspring[offspring_idx, gene_idx] = self.gene_type[gene_idx][0](value_from_space) + + if self.allow_duplicate_genes == False: + offspring[offspring_idx], _, _ = self.solve_duplicate_genes_by_space(solution=offspring[offspring_idx], + gene_type=self.gene_type, + num_trials=10) + return offspring + + def mutation_randomly(self, offspring): + + """ + Applies the random mutation the mutation probability. For each gene, if its probability is <= that mutation probability, then it will be mutated randomly. + It accepts a single parameter: + -offspring: The offspring to mutate. + It returns an array of the mutated offspring. + """ + + # Random mutation changes one or more genes in each offspring randomly. + for offspring_idx in range(offspring.shape[0]): + mutation_indices = numpy.array(random.sample(range(0, self.num_genes), self.mutation_num_genes)) + for gene_idx in mutation_indices: + + if type(self.random_mutation_min_val) in self.supported_int_float_types: + range_min = self.random_mutation_min_val + range_max = self.random_mutation_max_val + else: + range_min = self.random_mutation_min_val[gene_idx] + range_max = self.random_mutation_max_val[gene_idx] + + # Generating a random value. + random_value = numpy.random.uniform(low=range_min, + high=range_max, + size=1)[0] + # If the mutation_by_replacement attribute is True, then the random value replaces the current gene value. + if self.mutation_by_replacement: + if self.gene_type_single == True: + random_value = self.gene_type[0](random_value) + else: + random_value = self.gene_type[gene_idx][0](random_value) + if type(random_value) is numpy.ndarray: + random_value = random_value[0] + # If the mutation_by_replacement attribute is False, then the random value is added to the gene value. + else: + if self.gene_type_single == True: + random_value = self.gene_type[0](offspring[offspring_idx, gene_idx] + random_value) + else: + random_value = self.gene_type[gene_idx][0](offspring[offspring_idx, gene_idx] + random_value) + if type(random_value) is numpy.ndarray: + random_value = random_value[0] + + # Round the gene + if self.gene_type_single == True: + if not self.gene_type[1] is None: + random_value = numpy.round(random_value, self.gene_type[1]) + else: + if not self.gene_type[gene_idx][1] is None: + random_value = numpy.round(random_value, self.gene_type[gene_idx][1]) + + offspring[offspring_idx, gene_idx] = random_value + + if self.allow_duplicate_genes == False: + offspring[offspring_idx], _, _ = self.solve_duplicate_genes_randomly(solution=offspring[offspring_idx], + min_val=range_min, + max_val=range_max, + mutation_by_replacement=self.mutation_by_replacement, + gene_type=self.gene_type, + num_trials=10) + + return 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. + It accepts a single parameter: + -offspring: The offspring to mutate. + It returns an array of the mutated offspring. + """ + + # Random mutation changes one or more gene in each offspring randomly. + for offspring_idx in range(offspring.shape[0]): + probs = numpy.random.random(size=offspring.shape[1]) + for gene_idx in range(offspring.shape[1]): + + if type(self.random_mutation_min_val) in self.supported_int_float_types: + range_min = self.random_mutation_min_val + range_max = self.random_mutation_max_val + else: + range_min = self.random_mutation_min_val[gene_idx] + range_max = self.random_mutation_max_val[gene_idx] + + if probs[gene_idx] <= self.mutation_probability: + # Generating a random value. + random_value = numpy.random.uniform(low=range_min, + high=range_max, + size=1)[0] + # If the mutation_by_replacement attribute is True, then the random value replaces the current gene value. + if self.mutation_by_replacement: + if self.gene_type_single == True: + random_value = self.gene_type[0](random_value) + else: + random_value = self.gene_type[gene_idx][0](random_value) + if type(random_value) is numpy.ndarray: + random_value = random_value[0] + # If the mutation_by_replacement attribute is False, then the random value is added to the gene value. + else: + if self.gene_type_single == True: + random_value = self.gene_type[0](offspring[offspring_idx, gene_idx] + random_value) + else: + random_value = self.gene_type[gene_idx][0](offspring[offspring_idx, gene_idx] + random_value) + if type(random_value) is numpy.ndarray: + random_value = random_value[0] + + # Round the gene + if self.gene_type_single == True: + if not self.gene_type[1] is None: + random_value = numpy.round(random_value, self.gene_type[1]) + else: + if not self.gene_type[gene_idx][1] is None: + random_value = numpy.round(random_value, self.gene_type[gene_idx][1]) + + offspring[offspring_idx, gene_idx] = random_value + + if self.allow_duplicate_genes == False: + offspring[offspring_idx], _, _ = self.solve_duplicate_genes_randomly(solution=offspring[offspring_idx], + min_val=range_min, + max_val=range_max, + mutation_by_replacement=self.mutation_by_replacement, + gene_type=self.gene_type, + num_trials=10) + return offspring + + def swap_mutation(self, offspring): + + """ + Applies the swap mutation which interchanges the values of 2 randomly selected genes. + It accepts a single parameter: + -offspring: The offspring to mutate. + It returns an array of the mutated offspring. + """ + + for idx in range(offspring.shape[0]): + mutation_gene1 = numpy.random.randint(low=0, high=offspring.shape[1]/2, size=1)[0] + mutation_gene2 = mutation_gene1 + int(offspring.shape[1]/2) + + temp = offspring[idx, mutation_gene1] + offspring[idx, mutation_gene1] = offspring[idx, mutation_gene2] + offspring[idx, mutation_gene2] = temp + return offspring + + def inversion_mutation(self, offspring): + + """ + Applies the inversion mutation which selects a subset of genes and inverts them (in order). + It accepts a single parameter: + -offspring: The offspring to mutate. + It returns an array of the mutated offspring. + """ + + for idx in range(offspring.shape[0]): + mutation_gene1 = numpy.random.randint(low=0, high=numpy.ceil(offspring.shape[1]/2 + 1), size=1)[0] + mutation_gene2 = mutation_gene1 + int(offspring.shape[1]/2) + + genes_to_scramble = numpy.flip(offspring[idx, mutation_gene1:mutation_gene2]) + offspring[idx, mutation_gene1:mutation_gene2] = genes_to_scramble + return offspring + + def scramble_mutation(self, offspring): + + """ + Applies the scramble mutation which selects a subset of genes and shuffles their order randomly. + It accepts a single parameter: + -offspring: The offspring to mutate. + It returns an array of the mutated offspring. + """ + + for idx in range(offspring.shape[0]): + mutation_gene1 = numpy.random.randint(low=0, high=numpy.ceil(offspring.shape[1]/2 + 1), size=1)[0] + mutation_gene2 = mutation_gene1 + int(offspring.shape[1]/2) + genes_range = numpy.arange(start=mutation_gene1, stop=mutation_gene2) + numpy.random.shuffle(genes_range) + + genes_to_scramble = numpy.flip(offspring[idx, genes_range]) + offspring[idx, genes_range] = genes_to_scramble + return offspring + + def adaptive_mutation_population_fitness(self, offspring): + + """ + A helper method to calculate the average fitness of the solutions before applying the adaptive mutation. + It accepts a single parameter: + -offspring: The offspring to mutate. + It returns the average fitness to be used in adaptive mutation. + """ + + fitness = self.last_generation_fitness.copy() + temp_population = numpy.zeros_like(self.population) + + if (self.keep_elitism == 0): + if (self.keep_parents == 0): + parents_to_keep = [] + elif (self.keep_parents == -1): + parents_to_keep = self.last_generation_parents.copy() + temp_population[0:len(parents_to_keep), :] = parents_to_keep + elif (self.keep_parents > 0): + parents_to_keep, _ = self.steady_state_selection(self.last_generation_fitness, num_parents=self.keep_parents) + temp_population[0:len(parents_to_keep), :] = parents_to_keep + else: + parents_to_keep, _ = self.steady_state_selection(self.last_generation_fitness, num_parents=self.keep_elitism) + temp_population[0:len(parents_to_keep), :] = parents_to_keep + + temp_population[len(parents_to_keep):, :] = offspring + + fitness[:self.last_generation_parents.shape[0]] = self.last_generation_fitness[self.last_generation_parents_indices] + + first_idx = len(parents_to_keep) + last_idx = fitness.shape[0] + if len(fitness.shape) > 1: + # TODO This is a multi-objective optimization problem. + # fitness[first_idx:last_idx] = [0]*(last_idx - first_idx) + fitness[first_idx:last_idx] = numpy.zeros(shape=(last_idx - first_idx, fitness.shape[1])) + # raise ValueError('Edit adaptive mutation to work with multi-objective optimization problems.') + else: + # This is a single-objective optimization problem. + fitness[first_idx:last_idx] = [0]*(last_idx - first_idx) + + # # No parallel processing. + if self.parallel_processing is None: + if self.fitness_batch_size in [1, None]: + # Calculate the fitness for each individual solution. + for idx in range(first_idx, last_idx): + # We cannot return the index of the solution within the population. + # Because the new solution (offspring) does not yet exist in the population. + # The user should handle this situation if the solution index is used anywhere. + fitness[idx] = self.fitness_func(self, + temp_population[idx], + None) + else: + # Calculate the fitness for batch of solutions. + + # Number of batches. + num_batches = int(numpy.ceil((last_idx - first_idx) / self.fitness_batch_size)) + + for batch_idx in range(num_batches): + # The index of the first solution in the current batch. + batch_first_index = first_idx + batch_idx * self.fitness_batch_size + # The index of the last solution in the current batch. + if batch_idx == (num_batches - 1): + batch_last_index = last_idx + else: + batch_last_index = first_idx + (batch_idx + 1) * self.fitness_batch_size + + # Calculate the fitness values for the batch. + # We cannot return the index/indices of the solution(s) within the population. + # Because the new solution(s) (offspring) do(es) not yet exist in the population. + # The user should handle this situation if the solution index is used anywhere. + fitness_temp = self.fitness_func(self, + temp_population[batch_first_index:batch_last_index], + None) + # Insert the fitness of each solution at the proper index. + for idx in range(batch_first_index, batch_last_index): + fitness[idx] = fitness_temp[idx - batch_first_index] + + else: + # Parallel processing + # Decide which class to use based on whether the user selected "process" or "thread" + # TODO Add ExecutorClass as an instance attribute in the pygad.GA instances. Then retrieve this instance here instead of creating a new one. + if self.parallel_processing[0] == "process": + ExecutorClass = concurrent.futures.ProcessPoolExecutor + else: + ExecutorClass = concurrent.futures.ThreadPoolExecutor + + # We can use a with statement to ensure threads are cleaned up promptly (https://docs.python.org/3/library/concurrent.futures.html#threadpoolexecutor-example) + with ExecutorClass(max_workers=self.parallel_processing[1]) as executor: + # Indices of the solutions to calculate its fitness. + solutions_to_submit_indices = list(range(first_idx, last_idx)) + # The solutions to calculate its fitness. + solutions_to_submit = [temp_population[sol_idx].copy() for sol_idx in solutions_to_submit_indices] + if self.fitness_batch_size in [1, None]: + # Use parallel processing to calculate the fitness of the solutions. + for index, sol_fitness in zip(solutions_to_submit_indices, executor.map(self.fitness_func, [self]*len(solutions_to_submit_indices), solutions_to_submit, solutions_to_submit_indices)): + if type(sol_fitness) in self.supported_int_float_types: + # The fitness function returns a single numeric value. + # This is a single-objective optimization problem. + fitness[index] = sol_fitness + elif type(sol_fitness) in [list, tuple, numpy.ndarray]: + # The fitness function returns a list/tuple/numpy.ndarray. + # This is a multi-objective optimization problem. + fitness[index] = sol_fitness + else: + raise ValueError(f"The fitness function should return a number or an iterable (list, tuple, or numpy.ndarray) but the value {sol_fitness} of type {type(sol_fitness)} found.") + else: + # Reaching this point means that batch processing is in effect to calculate the fitness values. + # Number of batches. + num_batches = int(numpy.ceil(len(solutions_to_submit_indices) / self.fitness_batch_size)) + # Each element of the `batches_solutions` list represents the solutions in one batch. + batches_solutions = [] + # Each element of the `batches_indices` list represents the solutions' indices in one batch. + batches_indices = [] + # For each batch, get its indices and call the fitness function. + for batch_idx in range(num_batches): + batch_first_index = batch_idx * self.fitness_batch_size + batch_last_index = (batch_idx + 1) * self.fitness_batch_size + batch_indices = solutions_to_submit_indices[batch_first_index:batch_last_index] + batch_solutions = self.population[batch_indices, :] + + batches_solutions.append(batch_solutions) + batches_indices.append(batch_indices) + + for batch_indices, batch_fitness in zip(batches_indices, executor.map(self.fitness_func, [self]*len(solutions_to_submit_indices), batches_solutions, batches_indices)): + if type(batch_fitness) not in [list, tuple, numpy.ndarray]: + raise TypeError(f"Expected to receive a list, tuple, or numpy.ndarray from the fitness function but the value ({batch_fitness}) of type {type(batch_fitness)}.") + elif len(numpy.array(batch_fitness)) != len(batch_indices): + raise ValueError(f"There is a mismatch between the number of solutions passed to the fitness function ({len(batch_indices)}) and the number of fitness values returned ({len(batch_fitness)}). They must match.") + + for index, sol_fitness in zip(batch_indices, batch_fitness): + if type(sol_fitness) in self.supported_int_float_types: + # The fitness function returns a single numeric value. + # This is a single-objective optimization problem. + fitness[index] = sol_fitness + elif type(sol_fitness) in [list, tuple, numpy.ndarray]: + # The fitness function returns a list/tuple/numpy.ndarray. + # This is a multi-objective optimization problem. + fitness[index] = sol_fitness + else: + raise ValueError(f"The fitness function should return a number or an iterable (list, tuple, or numpy.ndarray) but the value ({sol_fitness}) of type {type(sol_fitness)} found.") + + + + if len(fitness.shape) > 1: + # TODO This is a multi-objective optimization problem. + # Calculate the average of each objective's fitness across all solutions in the population. + average_fitness = numpy.mean(fitness, axis=0) + else: + # This is a single-objective optimization problem. + average_fitness = numpy.mean(fitness) + + return average_fitness, fitness[len(parents_to_keep):] + + def adaptive_mutation(self, offspring): + + """ + Applies the adaptive mutation which changes the values of a number of genes randomly. In adaptive mutation, the number of genes to mutate differs based on the fitness value of the solution. + The random value is selected either using the 'gene_space' parameter or the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val'. + It accepts a single parameter: + -offspring: The offspring to mutate. + It returns an array of the mutated offspring. + """ + + # If the attribute 'gene_space' exists (i.e. not None), then the mutation values are selected from the 'gene_space' parameter according to the space of values of each gene. Otherwise, it is selected randomly based on the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val'. + # When the 'mutation_probability' parameter exists (i.e. not None), then it is used in the mutation. Otherwise, the 'mutation_num_genes' parameter is used. + + if self.mutation_probability is None: + # When the 'mutation_probability' parameter does not exist (i.e. None), then the parameter 'mutation_num_genes' is used in the mutation. + if not (self.gene_space is None): + # When the attribute 'gene_space' exists (i.e. not None), the mutation values are selected randomly from the space of values of each gene. + offspring = self.adaptive_mutation_by_space(offspring) + else: + # 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'. + offspring = self.adaptive_mutation_randomly(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' exists (i.e. not None), the mutation values are selected randomly from the space of values of each gene. + offspring = self.adaptive_mutation_probs_by_space(offspring) + else: + # 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'. + offspring = self.adaptive_mutation_probs_randomly(offspring) + + return offspring + + 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. + The random values are selected from the 'gene_space' parameter. + It accepts a single parameter: + -offspring: The offspring to mutate. + It returns an array of the mutated offspring. + """ + + # For each offspring, a value from the gene space is selected randomly and assigned to the selected gene for mutation. + + average_fitness, offspring_fitness = self.adaptive_mutation_population_fitness(offspring) + + # Adaptive mutation changes one or more genes in each offspring randomly. + # The number of genes to mutate depends on the solution's fitness value. + for offspring_idx in range(offspring.shape[0]): + ## TODO Make edits to work with multi-objective optimization. + # Compare the fitness of each offspring to the average fitness of each objective function. + fitness_comparison = offspring_fitness[offspring_idx] < average_fitness + + # Check if the problem is single or multi-objective optimization. + if type(fitness_comparison) in [bool, numpy.bool_]: + # Single-objective optimization problem. + if offspring_fitness[offspring_idx] < average_fitness: + adaptive_mutation_num_genes = self.mutation_num_genes[0] + else: + adaptive_mutation_num_genes = self.mutation_num_genes[1] + else: + # Multi-objective optimization problem. + + # Get the sum of the pool array (result of comparison). + # True is considered 1 and False is 0. + fitness_comparison_sum = sum(fitness_comparison) + # Check if more than or equal to 50% of the objectives have fitness greater than the average. + # If True, then use the first percentage. + # If False, use the second percentage. + if fitness_comparison_sum >= len(fitness_comparison)/2: + adaptive_mutation_num_genes = self.mutation_num_genes[0] + else: + adaptive_mutation_num_genes = self.mutation_num_genes[1] + + mutation_indices = numpy.array(random.sample(range(0, self.num_genes), adaptive_mutation_num_genes)) + for gene_idx in mutation_indices: + + if type(self.random_mutation_min_val) in self.supported_int_float_types: + range_min = self.random_mutation_min_val + range_max = self.random_mutation_max_val + else: + range_min = self.random_mutation_min_val[gene_idx] + range_max = self.random_mutation_max_val[gene_idx] + + if self.gene_space_nested: + # Returning the current gene space from the 'gene_space' attribute. + if type(self.gene_space[gene_idx]) in [numpy.ndarray, list]: + curr_gene_space = self.gene_space[gene_idx].copy() + else: + curr_gene_space = self.gene_space[gene_idx] + + # If the gene space has only a single value, use it as the new gene value. + if type(curr_gene_space) in pygad.GA.supported_int_float_types: + value_from_space = curr_gene_space + # If the gene space is None, apply mutation by adding a random value between the range defined by the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val'. + elif curr_gene_space is None: + rand_val = numpy.random.uniform(low=range_min, + high=range_max, + size=1)[0] + if self.mutation_by_replacement: + value_from_space = rand_val + else: + value_from_space = offspring[offspring_idx, gene_idx] + rand_val + elif type(curr_gene_space) is dict: + # Selecting a value randomly from the current gene's space in the 'gene_space' attribute. + if 'step' in curr_gene_space.keys(): + # The numpy.random.choice() and numpy.random.uniform() functions return a NumPy array as the output even if the array has a single value. + # We have to return the output at index 0 to force a numeric value to be returned not an object of type numpy.ndarray. + # If numpy.ndarray is returned, then it will cause an issue later while using the set() function. + value_from_space = numpy.random.choice(numpy.arange(start=curr_gene_space['low'], + stop=curr_gene_space['high'], + step=curr_gene_space['step']), + size=1)[0] + else: + value_from_space = numpy.random.uniform(low=curr_gene_space['low'], + high=curr_gene_space['high'], + size=1)[0] + else: + # Selecting a value randomly from the current gene's space in the 'gene_space' attribute. + # If the gene space has only 1 value, then select it. The old and new values of the gene are identical. + if len(curr_gene_space) == 1: + value_from_space = curr_gene_space[0] + # If the gene space has more than 1 value, then select a new one that is different from the current value. + else: + values_to_select_from = list(set(curr_gene_space) - set([offspring[offspring_idx, gene_idx]])) + + if len(values_to_select_from) == 0: + value_from_space = offspring[offspring_idx, gene_idx] + else: + value_from_space = random.choice(values_to_select_from) + else: + # Selecting a value randomly from the global gene space in the 'gene_space' attribute. + if type(self.gene_space) is dict: + if 'step' in self.gene_space.keys(): + value_from_space = numpy.random.choice(numpy.arange(start=self.gene_space['low'], + stop=self.gene_space['high'], + step=self.gene_space['step']), + size=1)[0] + else: + value_from_space = numpy.random.uniform(low=self.gene_space['low'], + high=self.gene_space['high'], + size=1)[0] + else: + values_to_select_from = list(set(self.gene_space) - set([offspring[offspring_idx, gene_idx]])) + + if len(values_to_select_from) == 0: + value_from_space = offspring[offspring_idx, gene_idx] + else: + value_from_space = random.choice(values_to_select_from) + + + if value_from_space is None: + value_from_space = numpy.random.uniform(low=range_min, + high=range_max, + size=1)[0] + + # Assinging the selected value from the space to the gene. + if self.gene_type_single == True: + if not self.gene_type[1] is None: + offspring[offspring_idx, gene_idx] = numpy.round(self.gene_type[0](value_from_space), + self.gene_type[1]) + else: + offspring[offspring_idx, gene_idx] = self.gene_type[0](value_from_space) + else: + if not self.gene_type[gene_idx][1] is None: + offspring[offspring_idx, gene_idx] = numpy.round(self.gene_type[gene_idx][0](value_from_space), + self.gene_type[gene_idx][1]) + else: + offspring[offspring_idx, gene_idx] = self.gene_type[gene_idx][0](value_from_space) + + if self.allow_duplicate_genes == False: + offspring[offspring_idx], _, _ = self.solve_duplicate_genes_by_space(solution=offspring[offspring_idx], + gene_type=self.gene_type, + num_trials=10) + return offspring + + 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. + The random values are selected based on the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val'. + It accepts a single parameter: + -offspring: The offspring to mutate. + It returns an array of the mutated offspring. + """ + + average_fitness, offspring_fitness = self.adaptive_mutation_population_fitness(offspring) + + # Adaptive random mutation changes one or more genes in each offspring randomly. + # The number of genes to mutate depends on the solution's fitness value. + for offspring_idx in range(offspring.shape[0]): + ## TODO Make edits to work with multi-objective optimization. + # Compare the fitness of each offspring to the average fitness of each objective function. + fitness_comparison = offspring_fitness[offspring_idx] < average_fitness + + # Check if the problem is single or multi-objective optimization. + if type(fitness_comparison) in [bool, numpy.bool_]: + # Single-objective optimization problem. + if fitness_comparison: + adaptive_mutation_num_genes = self.mutation_num_genes[0] + else: + adaptive_mutation_num_genes = self.mutation_num_genes[1] + else: + # Multi-objective optimization problem. + + # Get the sum of the pool array (result of comparison). + # True is considered 1 and False is 0. + fitness_comparison_sum = sum(fitness_comparison) + # Check if more than or equal to 50% of the objectives have fitness greater than the average. + # If True, then use the first percentage. + # If False, use the second percentage. + if fitness_comparison_sum >= len(fitness_comparison)/2: + adaptive_mutation_num_genes = self.mutation_num_genes[0] + else: + adaptive_mutation_num_genes = self.mutation_num_genes[1] + + mutation_indices = numpy.array(random.sample(range(0, self.num_genes), adaptive_mutation_num_genes)) + for gene_idx in mutation_indices: + + if type(self.random_mutation_min_val) in self.supported_int_float_types: + range_min = self.random_mutation_min_val + range_max = self.random_mutation_max_val + else: + range_min = self.random_mutation_min_val[gene_idx] + range_max = self.random_mutation_max_val[gene_idx] + + # Generating a random value. + random_value = numpy.random.uniform(low=range_min, + high=range_max, + size=1)[0] + # If the mutation_by_replacement attribute is True, then the random value replaces the current gene value. + if self.mutation_by_replacement: + if self.gene_type_single == True: + random_value = self.gene_type[0](random_value) + else: + random_value = self.gene_type[gene_idx][0](random_value) + if type(random_value) is numpy.ndarray: + random_value = random_value[0] + # If the mutation_by_replacement attribute is False, then the random value is added to the gene value. + else: + if self.gene_type_single == True: + random_value = self.gene_type[0](offspring[offspring_idx, gene_idx] + random_value) + else: + random_value = self.gene_type[gene_idx][0](offspring[offspring_idx, gene_idx] + random_value) + if type(random_value) is numpy.ndarray: + random_value = random_value[0] + + if self.gene_type_single == True: + if not self.gene_type[1] is None: + random_value = numpy.round(random_value, self.gene_type[1]) + else: + if not self.gene_type[gene_idx][1] is None: + random_value = numpy.round(random_value, self.gene_type[gene_idx][1]) + + offspring[offspring_idx, gene_idx] = random_value + + if self.allow_duplicate_genes == False: + offspring[offspring_idx], _, _ = self.solve_duplicate_genes_randomly(solution=offspring[offspring_idx], + min_val=range_min, + max_val=range_max, + mutation_by_replacement=self.mutation_by_replacement, + gene_type=self.gene_type, + num_trials=10) + return offspring + + 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. + The random values are selected based on space of values for each gene. + It accepts a single parameter: + -offspring: The offspring to mutate. + It returns an array of the mutated offspring. + """ + + # For each offspring, a value from the gene space is selected randomly and assigned to the selected gene for mutation. + + average_fitness, offspring_fitness = self.adaptive_mutation_population_fitness(offspring) + + # Adaptive random mutation changes one or more genes in each offspring randomly. + # The probability of mutating a gene depends on the solution's fitness value. + for offspring_idx in range(offspring.shape[0]): + ## TODO Make edits to work with multi-objective optimization. + # Compare the fitness of each offspring to the average fitness of each objective function. + fitness_comparison = offspring_fitness[offspring_idx] < average_fitness + + # Check if the problem is single or multi-objective optimization. + if type(fitness_comparison) in [bool, numpy.bool_]: + # Single-objective optimization problem. + if offspring_fitness[offspring_idx] < average_fitness: + adaptive_mutation_probability = self.mutation_probability[0] + else: + adaptive_mutation_probability = self.mutation_probability[1] + else: + # Multi-objective optimization problem. + + # Get the sum of the pool array (result of comparison). + # True is considered 1 and False is 0. + fitness_comparison_sum = sum(fitness_comparison) + # Check if more than or equal to 50% of the objectives have fitness greater than the average. + # If True, then use the first percentage. + # If False, use the second percentage. + if fitness_comparison_sum >= len(fitness_comparison)/2: + adaptive_mutation_probability = self.mutation_probability[0] + else: + adaptive_mutation_probability = self.mutation_probability[1] + + probs = numpy.random.random(size=offspring.shape[1]) + for gene_idx in range(offspring.shape[1]): + + if type(self.random_mutation_min_val) in self.supported_int_float_types: + range_min = self.random_mutation_min_val + range_max = self.random_mutation_max_val + else: + range_min = self.random_mutation_min_val[gene_idx] + range_max = self.random_mutation_max_val[gene_idx] + + if probs[gene_idx] <= adaptive_mutation_probability: + if self.gene_space_nested: + # Returning the current gene space from the 'gene_space' attribute. + if type(self.gene_space[gene_idx]) in [numpy.ndarray, list]: + curr_gene_space = self.gene_space[gene_idx].copy() + else: + curr_gene_space = self.gene_space[gene_idx] + + # If the gene space has only a single value, use it as the new gene value. + if type(curr_gene_space) in pygad.GA.supported_int_float_types: + value_from_space = curr_gene_space + # If the gene space is None, apply mutation by adding a random value between the range defined by the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val'. + elif curr_gene_space is None: + rand_val = numpy.random.uniform(low=range_min, + high=range_max, + size=1)[0] + if self.mutation_by_replacement: + value_from_space = rand_val + else: + value_from_space = offspring[offspring_idx, gene_idx] + rand_val + elif type(curr_gene_space) is dict: + # Selecting a value randomly from the current gene's space in the 'gene_space' attribute. + if 'step' in curr_gene_space.keys(): + value_from_space = numpy.random.choice(numpy.arange(start=curr_gene_space['low'], + stop=curr_gene_space['high'], + step=curr_gene_space['step']), + size=1)[0] + else: + value_from_space = numpy.random.uniform(low=curr_gene_space['low'], + high=curr_gene_space['high'], + size=1)[0] + else: + # Selecting a value randomly from the current gene's space in the 'gene_space' attribute. + # If the gene space has only 1 value, then select it. The old and new values of the gene are identical. + if len(curr_gene_space) == 1: + value_from_space = curr_gene_space[0] + # If the gene space has more than 1 value, then select a new one that is different from the current value. + else: + values_to_select_from = list(set(curr_gene_space) - set([offspring[offspring_idx, gene_idx]])) + + if len(values_to_select_from) == 0: + value_from_space = offspring[offspring_idx, gene_idx] + else: + value_from_space = random.choice(values_to_select_from) + else: + # Selecting a value randomly from the global gene space in the 'gene_space' attribute. + if type(self.gene_space) is dict: + if 'step' in self.gene_space.keys(): + # The numpy.random.choice() and numpy.random.uniform() functions return a NumPy array as the output even if the array has a single value. + # We have to return the output at index 0 to force a numeric value to be returned not an object of type numpy.ndarray. + # If numpy.ndarray is returned, then it will cause an issue later while using the set() function. + value_from_space = numpy.random.choice(numpy.arange(start=self.gene_space['low'], + stop=self.gene_space['high'], + step=self.gene_space['step']), + size=1)[0] + else: + value_from_space = numpy.random.uniform(low=self.gene_space['low'], + high=self.gene_space['high'], + size=1)[0] + else: + values_to_select_from = list(set(self.gene_space) - set([offspring[offspring_idx, gene_idx]])) + + if len(values_to_select_from) == 0: + value_from_space = offspring[offspring_idx, gene_idx] + else: + value_from_space = random.choice(values_to_select_from) + + if value_from_space is None: + value_from_space = numpy.random.uniform(low=range_min, + high=range_max, + size=1)[0] + + # Assinging the selected value from the space to the gene. + if self.gene_type_single == True: + if not self.gene_type[1] is None: + offspring[offspring_idx, gene_idx] = numpy.round(self.gene_type[0](value_from_space), + self.gene_type[1]) + else: + offspring[offspring_idx, gene_idx] = self.gene_type[0](value_from_space) + else: + if not self.gene_type[gene_idx][1] is None: + offspring[offspring_idx, gene_idx] = numpy.round(self.gene_type[gene_idx][0](value_from_space), + self.gene_type[gene_idx][1]) + else: + offspring[offspring_idx, gene_idx] = self.gene_type[gene_idx][0](value_from_space) + + if self.allow_duplicate_genes == False: + offspring[offspring_idx], _, _ = self.solve_duplicate_genes_by_space(solution=offspring[offspring_idx], + gene_type=self.gene_type, + num_trials=10) + return offspring + + 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. + The random values are selected based on the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val'. + It accepts a single parameter: + -offspring: The offspring to mutate. + It returns an array of the mutated offspring. + """ + + average_fitness, offspring_fitness = self.adaptive_mutation_population_fitness(offspring) + + # Adaptive random mutation changes one or more genes in each offspring randomly. + # The probability of mutating a gene depends on the solution's fitness value. + for offspring_idx in range(offspring.shape[0]): + ## TODO Make edits to work with multi-objective optimization. + # Compare the fitness of each offspring to the average fitness of each objective function. + fitness_comparison = offspring_fitness[offspring_idx] < average_fitness + + # Check if the problem is single or multi-objective optimization. + if type(fitness_comparison) in [bool, numpy.bool_]: + # Single-objective optimization problem. + if offspring_fitness[offspring_idx] < average_fitness: + adaptive_mutation_probability = self.mutation_probability[0] + else: + adaptive_mutation_probability = self.mutation_probability[1] + else: + # Multi-objective optimization problem. + + # Get the sum of the pool array (result of comparison). + # True is considered 1 and False is 0. + fitness_comparison_sum = sum(fitness_comparison) + # Check if more than or equal to 50% of the objectives have fitness greater than the average. + # If True, then use the first percentage. + # If False, use the second percentage. + if fitness_comparison_sum >= len(fitness_comparison)/2: + adaptive_mutation_probability = self.mutation_probability[0] + else: + adaptive_mutation_probability = self.mutation_probability[1] + + probs = numpy.random.random(size=offspring.shape[1]) + for gene_idx in range(offspring.shape[1]): + + if type(self.random_mutation_min_val) in self.supported_int_float_types: + range_min = self.random_mutation_min_val + range_max = self.random_mutation_max_val + else: + range_min = self.random_mutation_min_val[gene_idx] + range_max = self.random_mutation_max_val[gene_idx] + + if probs[gene_idx] <= adaptive_mutation_probability: + # Generating a random value. + random_value = numpy.random.uniform(low=range_min, + high=range_max, + size=1)[0] + # If the mutation_by_replacement attribute is True, then the random value replaces the current gene value. + if self.mutation_by_replacement: + if self.gene_type_single == True: + random_value = self.gene_type[0](random_value) + else: + random_value = self.gene_type[gene_idx][0](random_value) + if type(random_value) is numpy.ndarray: + random_value = random_value[0] + # If the mutation_by_replacement attribute is False, then the random value is added to the gene value. + else: + if self.gene_type_single == True: + random_value = self.gene_type[0](offspring[offspring_idx, gene_idx] + random_value) + else: + random_value = self.gene_type[gene_idx][0](offspring[offspring_idx, gene_idx] + random_value) + if type(random_value) is numpy.ndarray: + random_value = random_value[0] + + if self.gene_type_single == True: + if not self.gene_type[1] is None: + random_value = numpy.round(random_value, self.gene_type[1]) + else: + if not self.gene_type[gene_idx][1] is None: + random_value = numpy.round(random_value, self.gene_type[gene_idx][1]) + + offspring[offspring_idx, gene_idx] = random_value + + if self.allow_duplicate_genes == False: + offspring[offspring_idx], _, _ = self.solve_duplicate_genes_randomly(solution=offspring[offspring_idx], + min_val=range_min, + max_val=range_max, + mutation_by_replacement=self.mutation_by_replacement, + gene_type=self.gene_type, + num_trials=10) + return offspring diff --git a/pygad/utils/nsga2.py b/pygad/utils/nsga2.py new file mode 100644 index 00000000..2b56b342 --- /dev/null +++ b/pygad/utils/nsga2.py @@ -0,0 +1,268 @@ +import numpy +import pygad + +class NSGA2: + + def __init__(): + pass + + def get_non_dominated_set(self, curr_solutions): + """ + Get the set of non-dominated solutions from the current set of solutions. + + Parameters + ---------- + curr_solutions : TYPE + The set of solutions to find its non-dominated set. + + Returns + ------- + dominated_set : TYPE + A set of the dominated solutions. + non_dominated_set : TYPE + A set of the non-dominated set. + + """ + # List of the members of the current dominated pareto front/set. + dominated_set = [] + # List of the non-members of the current dominated pareto front/set. + non_dominated_set = [] + for idx1, sol1 in enumerate(curr_solutions): + # Flag indicates whether the solution is a member of the current dominated set. + is_dominated = True + for idx2, sol2 in enumerate(curr_solutions): + if idx1 == idx2: + continue + + # Zipping the 2 solutions so the corresponding genes are in the same list. + # The returned array is of size (N, 2) where N is the number of genes. + two_solutions = numpy.array(list(zip(sol1[1], sol2[1]))) + + # Use < for minimization problems and > for maximization problems. + # Checking if any solution dominates the current solution by applying the 2 conditions. + # gr_eq (greater than or equal): All elements must be True. + # gr (greater than): Only 1 element must be True. + gr_eq = two_solutions[:, 1] >= two_solutions[:, 0] + gr = two_solutions[:, 1] > two_solutions[:, 0] + + # If the 2 conditions hold, then a solution dominates the current solution. + # The current solution is not considered a member of the dominated set. + if gr_eq.all() and gr.any(): + # Set the is_dominated flag to False to NOT insert the current solution in the current dominated set. + # Instead, insert it into the non-dominated set. + is_dominated = False + non_dominated_set.append(sol1) + break + else: + # Reaching here means the solution does not dominate the current solution. + pass + + # If the flag is True, then no solution dominates the current solution. + if is_dominated: + dominated_set.append(sol1) + + # Return the dominated and non-dominated sets. + return dominated_set, non_dominated_set + + def non_dominated_sorting(self, fitness): + """ + Apply non-dominant sorting over the fitness to create the pareto fronts based on non-dominaned sorting of the solutions. + + Parameters + ---------- + fitness : TYPE + An array of the population fitness across all objective function. + + Returns + ------- + pareto_fronts : TYPE + An array of the pareto fronts. + + """ + + # Verify that the problem is multi-objective optimization as non-dominated sorting is only applied to multi-objective problems. + if type(fitness[0]) in [list, tuple, numpy.ndarray]: + pass + elif type(fitness[0]) in self.supported_int_float_types: + raise TypeError('Non-dominated sorting is only applied when optimizing multi-objective problems.\n\nBut a single-objective optimization problem found as the fitness function returns a single numeric value.\n\nTo use multi-objective optimization, consider returning an iterable of any of these data types:\n1)list\n2)tuple\n3)numpy.ndarray') + else: + raise TypeError(f'Non-dominated sorting is only applied when optimizing multi-objective problems. \n\nTo use multi-objective optimization, consider returning an iterable of any of these data types:\n1)list\n2)tuple\n3)numpy.ndarray\n\nBut the data type {type(fitness[0])} found.') + + # A list of all non-dominated sets. + pareto_fronts = [] + + # The remaining set to be explored for non-dominance. + # Initially it is set to the entire population. + # The solutions of each non-dominated set are removed after each iteration. + remaining_set = fitness.copy() + + # Zipping the solution index with the solution's fitness. + # This helps to easily identify the index of each solution. + # Each element has: + # 1) The index of the solution. + # 2) An array of the fitness values of this solution across all objectives. + # remaining_set = numpy.array(list(zip(range(0, fitness.shape[0]), non_dominated_set))) + remaining_set = list(zip(range(0, fitness.shape[0]), remaining_set)) + + # A list mapping the index of each pareto front to the set of solutions in this front. + solutions_fronts_indices = [-1]*len(remaining_set) + solutions_fronts_indices = numpy.array(solutions_fronts_indices) + + # Index of the current pareto front. + front_index = -1 + while len(remaining_set) > 0: + front_index += 1 + + # Get the current non-dominated set of solutions. + pareto_front, remaining_set = self.get_non_dominated_set(curr_solutions=remaining_set) + pareto_front = numpy.array(pareto_front, dtype=object) + pareto_fronts.append(pareto_front) + + solutions_indices = pareto_front[:, 0].astype(int) + solutions_fronts_indices[solutions_indices] = front_index + + return pareto_fronts, solutions_fronts_indices + + def crowding_distance(self, pareto_front, fitness): + """ + Calculate the crowding distance for all solutions in the current pareto front. + + Parameters + ---------- + pareto_front : TYPE + The set of solutions in the current pareto front. + fitness : TYPE + The fitness of the current population. + + Returns + ------- + obj_crowding_dist_list : TYPE + A nested list of the values for all objectives alongside their crowding distance. + crowding_dist_sum : TYPE + A list of the sum of crowding distances across all objectives for each solution. + crowding_dist_front_sorted_indices : TYPE + The indices of the solutions (relative to the current front) sorted by the crowding distance. + crowding_dist_pop_sorted_indices : TYPE + The indices of the solutions (relative to the population) sorted by the crowding distance. + """ + + # Each solution in the pareto front has 2 elements: + # 1) The index of the solution in the population. + # 2) A list of the fitness values for all objectives of the solution. + # Before proceeding, remove the indices from each solution in the pareto front. + pareto_front_no_indices = numpy.array([pareto_front[:, 1][idx] for idx in range(pareto_front.shape[0])]) + + # If there is only 1 solution, then return empty arrays for the crowding distance. + if pareto_front_no_indices.shape[0] == 1: + # There is only 1 index. + return numpy.array([]), numpy.array([]), numpy.array([0]), pareto_front[:, 0].astype(int) + + # An empty list holding info about the objectives of each solution. The info includes the objective value and crowding distance. + obj_crowding_dist_list = [] + # 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: + # 1) The index of the objective value. + # 2) The objective value. + # 3) Initialize the crowding distance by zero. + obj = list(zip(range(len(obj)), obj, [0]*len(obj))) + obj = [list(element) for element in obj] + # This variable is the sorted version where sorting is done by the objective value (second element). + # Note that the first element is still the original objective index before sorting. + obj_sorted = sorted(obj, key=lambda x: x[1]) + + # Get the minimum and maximum values for the current objective. + obj_min_val = min(fitness[:, obj_idx]) + obj_max_val = max(fitness[:, obj_idx]) + denominator = obj_max_val - obj_min_val + # To avoid division by zero, set the denominator to a tiny value. + if denominator == 0: + denominator = 0.0000001 + + # Set the crowding distance to the first and last solutions (after being sorted) to infinity. + inf_val = float('inf') + # crowding_distance[0] = inf_val + obj_sorted[0][2] = inf_val + # crowding_distance[-1] = inf_val + obj_sorted[-1][2] = inf_val + + # If there are only 2 solutions in the current pareto front, then do not proceed. + # The crowding distance for such 2 solutions is infinity. + if len(obj_sorted) <= 2: + obj_crowding_dist_list.append(obj_sorted) + break + + for idx in range(1, len(obj_sorted)-1): + # Calculate the crowding distance. + crowding_dist = obj_sorted[idx+1][1] - obj_sorted[idx-1][1] + crowding_dist = crowding_dist / denominator + # Insert the crowding distance back into the list to override the initial zero. + obj_sorted[idx][2] = crowding_dist + + # Sort the objective by the original index at index 0 of the each child list. + obj_sorted = sorted(obj_sorted, key=lambda x: x[0]) + obj_crowding_dist_list.append(obj_sorted) + + obj_crowding_dist_list = numpy.array(obj_crowding_dist_list) + crowding_dist = numpy.array([obj_crowding_dist_list[idx, :, 2] for idx in range(len(obj_crowding_dist_list))]) + crowding_dist_sum = numpy.sum(crowding_dist, axis=0) + + # 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. + 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) + + # The sorted solutions' indices by the crowding distance. + crowding_dist_front_sorted_indices = numpy.array(crowding_dist_sum)[:, 0] + crowding_dist_front_sorted_indices = crowding_dist_front_sorted_indices.astype(int) + # Note that such indices are relative to the front, NOT the population. + # It is mandatory to map such front indices to population indices before using them to refer to the population. + crowding_dist_pop_sorted_indices = pareto_front[:, 0] + crowding_dist_pop_sorted_indices = crowding_dist_pop_sorted_indices[crowding_dist_front_sorted_indices] + crowding_dist_pop_sorted_indices = crowding_dist_pop_sorted_indices.astype(int) + + return obj_crowding_dist_list, crowding_dist_sum, crowding_dist_front_sorted_indices, crowding_dist_pop_sorted_indices + + def sort_solutions_nsga2(self, fitness): + """ + Sort the solutions based on the fitness. + The sorting procedure differs based on whether the problem is single-objective or multi-objective optimization. + If it is multi-objective, then non-dominated sorting and crowding distance are applied. + At first, non-dominated sorting is applied to classify the solutions into pareto fronts. + Then the solutions inside each front are sorted using crowded distance. + The solutions inside pareto front X always come before those in front X+1. + + Parameters + ---------- + fitness : TYPE + The fitness of the entire population. + + Returns + ------- + solutions_sorted : TYPE + The indices of the sorted solutions. + + """ + if type(fitness[0]) in [list, tuple, numpy.ndarray]: + # Multi-objective optimization problem. + solutions_sorted = [] + # Split the solutions into pareto fronts using non-dominated sorting. + pareto_fronts, solutions_fronts_indices = self.non_dominated_sorting(fitness) + self.pareto_fronts = pareto_fronts.copy() + for pareto_front in pareto_fronts: + # Sort the solutions in the front using crowded distance. + _, _, _, crowding_dist_pop_sorted_indices = self.crowding_distance(pareto_front=pareto_front.copy(), + fitness=fitness) + crowding_dist_pop_sorted_indices = list(crowding_dist_pop_sorted_indices) + # Append the sorted solutions into the list. + solutions_sorted.extend(crowding_dist_pop_sorted_indices) + elif type(fitness[0]) in pygad.GA.supported_int_float_types: + # Single-objective optimization problem. + solutions_sorted = sorted(range(len(fitness)), key=lambda k: fitness[k]) + # Reverse the sorted solutions so that the best solution comes first. + solutions_sorted.reverse() + + return solutions_sorted diff --git a/pygad/utils/parent_selection.py b/pygad/utils/parent_selection.py new file mode 100644 index 00000000..ac293c28 --- /dev/null +++ b/pygad/utils/parent_selection.py @@ -0,0 +1,523 @@ +""" +The pygad.utils.parent_selection module has all the built-in parent selection operators. +""" + +import numpy + +class ParentSelection: + + def __init__(): + pass + + 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. + Later, these parents will mate to produce the offspring. + + It accepts 2 parameters: + -fitness: The fitness values of the solutions in the current population. + -num_parents: The number of parents to be selected. + It returns: + -An array of the selected parents. + -The indices of the selected solutions. + """ + + # Return the indices of the sorted solutions (all solutions in the population). + # This function works with both single- and multi-objective optimization problems. + fitness_sorted = self.sort_solutions_nsga2(fitness=fitness) + + # Selecting the best individuals in the current generation as parents for producing the offspring of the next generation. + if self.gene_type_single == True: + parents = numpy.empty((num_parents, self.population.shape[1]), dtype=self.gene_type[0]) + else: + parents = numpy.empty((num_parents, self.population.shape[1]), dtype=object) + + for parent_num in range(num_parents): + parents[parent_num, :] = self.population[fitness_sorted[parent_num], :].copy() + + return parents, numpy.array(fitness_sorted[:num_parents]) + + def rank_selection(self, fitness, num_parents): + + """ + Selects the parents using the rank selection technique. Later, these parents will mate to produce the offspring. + Rank selection gives a rank from 1 to N (number of solutions) to each solution based on its fitness. + It accepts 2 parameters: + -fitness: The fitness values of the solutions in the current population. + -num_parents: The number of parents to be selected. + It returns: + -An array of the selected parents. + -The indices of the selected solutions. + """ + + # Return the indices of the sorted solutions (all solutions in the population). + # 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 = numpy.arange(1, self.sol_per_pop+1) + + probs = rank / numpy.sum(rank) + + probs_start, probs_end, parents = self.wheel_cumulative_probs(probs=probs.copy(), + num_parents=num_parents) + + parents_indices = [] + + for parent_num in range(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. + # Return the correct index of the solution. + mapped_idx = fitness_sorted[idx] + parents[parent_num, :] = self.population[mapped_idx, :].copy() + parents_indices.append(mapped_idx) + break + + return parents, numpy.array(parents_indices) + + def random_selection(self, fitness, num_parents): + + """ + Selects the parents randomly. Later, these parents will mate to produce the offspring. + It accepts 2 parameters: + -fitness: The fitness values of the solutions in the current population. + -num_parents: The number of parents to be selected. + It returns: + -An array of the selected parents. + -The indices of the selected solutions. + """ + + if self.gene_type_single == True: + parents = numpy.empty((num_parents, self.population.shape[1]), dtype=self.gene_type[0]) + else: + parents = numpy.empty((num_parents, self.population.shape[1]), dtype=object) + + rand_indices = numpy.random.randint(low=0.0, high=fitness.shape[0], size=num_parents) + + for parent_num in range(num_parents): + parents[parent_num, :] = self.population[rand_indices[parent_num], :].copy() + + return parents, rand_indices + + def tournament_selection(self, fitness, num_parents): + + """ + Selects the parents using the tournament selection technique. Later, these parents will mate to produce the offspring. + It accepts 2 parameters: + -fitness: The fitness values of the solutions in the current population. + -num_parents: The number of parents to be selected. + It returns: + -An array of the selected parents. + -The indices of the selected solutions. + """ + + # Return the indices of the sorted solutions (all solutions in the population). + # This function works with both single- and multi-objective optimization problems. + fitness_sorted = self.sort_solutions_nsga2(fitness=fitness) + + if self.gene_type_single == True: + parents = numpy.empty((num_parents, self.population.shape[1]), dtype=self.gene_type[0]) + else: + parents = numpy.empty((num_parents, self.population.shape[1]), dtype=object) + + parents_indices = [] + + for parent_num in range(num_parents): + # Generate random indices for the candiadate solutions. + rand_indices = numpy.random.randint(low=0.0, high=len(fitness), size=self.K_tournament) + # K_fitnesses = fitness[rand_indices] + # selected_parent_idx = numpy.where(K_fitnesses == numpy.max(K_fitnesses))[0][0] + + # 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] + # Select the solution with the lowest rank as a parent. + selected_parent_idx = rand_indices_rank.index(min(rand_indices_rank)) + + # Append the index of the selected parent. + parents_indices.append(rand_indices[selected_parent_idx]) + # Insert the selected parent. + parents[parent_num, :] = self.population[rand_indices[selected_parent_idx], :].copy() + + return parents, numpy.array(parents_indices) + + def roulette_wheel_selection(self, fitness, num_parents): + + """ + Selects the parents using the roulette wheel selection technique. Later, these parents will mate to produce the offspring. + It accepts 2 parameters: + -fitness: The fitness values of the solutions in the current population. + -num_parents: The number of parents to be selected. + It returns: + -An array of the selected parents. + -The indices of the selected solutions. + """ + + ## Make edits to work with multi-objective optimization. + ## The objective is to convert the fitness from M-D array to just 1D array. + ## There are 2 ways: + # 1) By summing the fitness values of each solution. + # 2) By using only 1 objective to create the roulette wheel and excluding the others. + + # Take the sum of the fitness values of each solution. + if len(fitness.shape) > 1: + # Multi-objective optimization problem. + # Sum the fitness values of each solution to reduce the fitness from M-D array to just 1D array. + fitness = numpy.sum(fitness, axis=1) + else: + # Single-objective optimization problem. + pass + + # Reaching this step extends 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.") + raise ZeroDivisionError("Cannot proceed because the sum of fitness values is zero. Cannot divide by zero.") + + probs = fitness / fitness_sum + + probs_start, probs_end, parents = self.wheel_cumulative_probs(probs=probs.copy(), + num_parents=num_parents) + + parents_indices = [] + + for parent_num in range(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]): + parents[parent_num, :] = self.population[idx, :].copy() + parents_indices.append(idx) + break + + return parents, numpy.array(parents_indices) + + def wheel_cumulative_probs(self, probs, num_parents): + """ + A helper function to calculate the wheel probabilities for these 2 methods: + 1) roulette_wheel_selection + 2) rank_selection + 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. + It also returns an empty array for the 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') + + # Selecting the best individuals in the current generation as parents for producing the offspring of the next generation. + if self.gene_type_single == True: + parents = numpy.empty((num_parents, self.population.shape[1]), dtype=self.gene_type[0]) + else: + parents = numpy.empty((num_parents, self.population.shape[1]), dtype=object) + + return probs_start, probs_end, parents + + def stochastic_universal_selection(self, fitness, num_parents): + + """ + Selects the parents using the stochastic universal selection technique. Later, these parents will mate to produce the offspring. + It accepts 2 parameters: + -fitness: The fitness values of the solutions in the current population. + -num_parents: The number of parents to be selected. + It returns: + -An array of the selected parents. + -The indices of the selected solutions. + """ + + ## Make edits to work with multi-objective optimization. + ## The objective is to convert the fitness from M-D array to just 1D array. + ## There are 2 ways: + # 1) By summing the fitness values of each solution. + # 2) By using only 1 objective to create the roulette wheel and excluding the others. + + # Take the sum of the fitness values of each solution. + if len(fitness.shape) > 1: + # Multi-objective optimization problem. + # Sum the fitness values of each solution to reduce the fitness from M-D array to just 1D array. + fitness = numpy.sum(fitness, axis=1) + else: + # Single-objective optimization problem. + pass + + # Reaching this step extends 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.") + raise ZeroDivisionError("Cannot proceed because the sum of fitness values is zero. Cannot divide by zero.") + + 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 + # Replace 99999999999 by float('inf') + # probs[min_probs_idx] = 99999999999 + probs[min_probs_idx] = float('inf') + + pointers_distance = 1.0 / self.num_parents_mating # Distance between different pointers. + first_pointer = numpy.random.uniform(low=0.0, + high=pointers_distance, + size=1)[0] # Location of the first pointer. + + # Selecting the best individuals in the current generation as parents for producing the offspring of the next generation. + if self.gene_type_single == True: + parents = numpy.empty((num_parents, self.population.shape[1]), dtype=self.gene_type[0]) + else: + parents = numpy.empty((num_parents, self.population.shape[1]), dtype=object) + + parents_indices = [] + + for parent_num in range(num_parents): + rand_pointer = first_pointer + parent_num*pointers_distance + for idx in range(probs.shape[0]): + if (rand_pointer >= probs_start[idx] and rand_pointer < probs_end[idx]): + parents[parent_num, :] = self.population[idx, :].copy() + parents_indices.append(idx) + break + + return parents, numpy.array(parents_indices) + + def tournament_selection_nsga2(self, + fitness, + num_parents + ): + + """ + 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. + 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. + + It accepts 2 parameters: + -fitness: The fitness values for the current population. + -num_parents: The number of parents to be selected. + -pareto_fronts: A nested array of all the pareto fronts. Each front has its solutions. + -solutions_fronts_indices: A list of the pareto front index of each solution in the current population. + + It returns: + -An array of the selected parents. + -The indices of the selected solutions. + """ + + if self.gene_type_single == True: + parents = numpy.empty((num_parents, self.population.shape[1]), dtype=self.gene_type[0]) + else: + parents = numpy.empty((num_parents, self.population.shape[1]), dtype=object) + + # Verify that the problem is multi-objective optimization as the tournament NSGA-II selection is only applied to multi-objective problems. + if type(fitness[0]) in [list, tuple, numpy.ndarray]: + pass + elif type(fitness[0]) in self.supported_int_float_types: + raise ValueError('The tournament NSGA-II parent selection operator is only applied when optimizing multi-objective problems.\n\nBut a single-objective optimization problem found as the fitness function returns a single numeric value.\n\nTo use multi-objective optimization, consider returning an iterable of any of these data types:\n1)list\n2)tuple\n3)numpy.ndarray') + + # The indices of the selected parents. + parents_indices = [] + + # If there is only a single objective, each pareto front is expected to have only 1 solution. + # TODO Make a test to check for that behaviour and add it to the GitHub actions tests. + pareto_fronts, solutions_fronts_indices = self.non_dominated_sorting(fitness) + self.pareto_fronts = pareto_fronts.copy() + + # Randomly generate pairs of indices to apply for NSGA-II tournament selection for selecting the parents solutions. + rand_indices = numpy.random.randint(low=0.0, + high=len(solutions_fronts_indices), + size=(num_parents, self.K_tournament)) + + for parent_num in range(num_parents): + # Return the indices of the current 2 solutions. + current_indices = rand_indices[parent_num] + # Return the front index of the 2 solutions. + parent_fronts_indices = solutions_fronts_indices[current_indices] + + if parent_fronts_indices[0] < parent_fronts_indices[1]: + # If the first solution is in a lower pareto front than the second, then select it. + selected_parent_idx = current_indices[0] + elif parent_fronts_indices[0] > parent_fronts_indices[1]: + # If the second solution is in a lower pareto front than the first, then select it. + selected_parent_idx = current_indices[1] + else: + # The 2 solutions are in the same pareto front. + # The selection is made using the crowding distance. + + # A list holding the crowding distance of the current 2 solutions. It is initialized to -1. + solutions_crowding_distance = [-1, -1] + + # Fetch the current pareto front. + pareto_front = pareto_fronts[parent_fronts_indices[0]] # Index 1 can also be used. + + # If there is only 1 solution in the pareto front, just return it without calculating the crowding distance (it is useless). + if pareto_front.shape[0] == 1: + selected_parent_idx = current_indices[0] # Index 1 can also be used. + else: + # Reaching here means the pareto front has more than 1 solution. + + # Calculate the crowding distance of the solutions of the pareto front. + obj_crowding_distance_list, crowding_distance_sum, crowding_dist_front_sorted_indices, crowding_dist_pop_sorted_indices = self.crowding_distance(pareto_front=pareto_front.copy(), + fitness=fitness) + + # This list has the sorted front-based indices for the solutions in the current pareto front. + crowding_dist_front_sorted_indices = list(crowding_dist_front_sorted_indices) + # This list has the sorted population-based indices for the solutions in the current pareto front. + crowding_dist_pop_sorted_indices = list(crowding_dist_pop_sorted_indices) + + # Return the indices of the solutions from the pareto front. + solution1_idx = crowding_dist_pop_sorted_indices.index(current_indices[0]) + solution2_idx = crowding_dist_pop_sorted_indices.index(current_indices[1]) + + # Fetch the crowding distance using the indices. + solutions_crowding_distance[0] = crowding_distance_sum[solution1_idx][1] + solutions_crowding_distance[1] = crowding_distance_sum[solution2_idx][1] + + # # Instead of using the crowding distance, we can select the solution that comes first in the list. + # # Its limitation is that it is biased towards the low indexed solution if the 2 solutions have the same crowding distance. + # if solution1_idx < solution2_idx: + # # Select the first solution if it has higher crowding distance. + # selected_parent_idx = current_indices[0] + # else: + # # Select the second solution if it has higher crowding distance. + # selected_parent_idx = current_indices[1] + + if solutions_crowding_distance[0] > solutions_crowding_distance[1]: + # Select the first solution if it has higher crowding distance. + selected_parent_idx = current_indices[0] + elif solutions_crowding_distance[1] > solutions_crowding_distance[0]: + # Select the second solution if it has higher crowding distance. + selected_parent_idx = current_indices[1] + else: + # If the crowding distance is equal, select the parent randomly. + rand_num = numpy.random.uniform() + if rand_num < 0.5: + # If the random number is < 0.5, then select the first solution. + selected_parent_idx = current_indices[0] + else: + # If the random number is >= 0.5, then select the second solution. + selected_parent_idx = current_indices[1] + + # Insert the selected parent index. + parents_indices.append(selected_parent_idx) + # Insert the selected parent. + parents[parent_num, :] = self.population[selected_parent_idx, :].copy() + + # Make sure the parents indices is returned as a NumPy array. + return parents, numpy.array(parents_indices) + + def nsga2_selection(self, + fitness, + num_parents + ): + + """ + Select the parents using the Non-Dominated Sorting Genetic Algorithm II (NSGA-II). + The selection is done using 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. + The parents are selected from the lower pareto fronts and moving up until selecting the number of desired parents. + A solution from a pareto front X cannot be taken as a parent until all solutions in pareto front Y is selected given that Y < X. + For a pareto front X, if only a subset of its solutions is needed, then the corwding distance is used to determine which solutions to be selected from the front. 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. + + It accepts 2 parameters: + -fitness: The fitness values for the current population. + -num_parents: The number of parents to be selected. + -pareto_fronts: A nested array of all the pareto fronts. Each front has its solutions. + -solutions_fronts_indices: A list of the pareto front index of each solution in the current population. + + It returns: + -An array of the selected parents. + -The indices of the selected solutions. + """ + + if self.gene_type_single == True: + parents = numpy.empty((num_parents, self.population.shape[1]), dtype=self.gene_type[0]) + else: + parents = numpy.empty((num_parents, self.population.shape[1]), dtype=object) + + # Verify that the problem is multi-objective optimization as the NSGA-II selection is only applied to multi-objective problems. + if type(fitness[0]) in [list, tuple, numpy.ndarray]: + pass + elif type(fitness[0]) in self.supported_int_float_types: + raise ValueError('The NSGA-II parent selection operator is only applied when optimizing multi-objective problems.\n\nBut a single-objective optimization problem found as the fitness function returns a single numeric value.\n\nTo use multi-objective optimization, consider returning an iterable of any of these data types:\n1)list\n2)tuple\n3)numpy.ndarray') + + # The indices of the selected parents. + parents_indices = [] + + # If there is only a single objective, each pareto front is expected to have only 1 solution. + # TODO Make a test to check for that behaviour. + pareto_fronts, solutions_fronts_indices = self.non_dominated_sorting(fitness) + self.pareto_fronts = pareto_fronts.copy() + + # The number of remaining parents to be selected. + num_remaining_parents = num_parents + + # Index of the current parent. + current_parent_idx = 0 + # A loop variable holding the index of the current pareto front. + pareto_front_idx = 0 + while num_remaining_parents != 0 and pareto_front_idx < len(pareto_fronts): + # Return the current pareto front. + current_pareto_front = pareto_fronts[pareto_front_idx] + # Check if the entire front fits into the parents array. + # If so, then insert all the solutions in the current front into the parents array. + if num_remaining_parents >= len(current_pareto_front): + for sol_idx in range(len(current_pareto_front)): + selected_solution_idx = current_pareto_front[sol_idx, 0] + # Insert the parent into the parents array. + parents[current_parent_idx, :] = self.population[selected_solution_idx, :].copy() + # Insert the index of the selected parent. + parents_indices.append(selected_solution_idx) + # Increase the parent index. + current_parent_idx += 1 + + # Decrement the number of remaining parents by the length of the pareto front. + num_remaining_parents -= len(current_pareto_front) + else: + # If only a subset of the front is needed, then use the crowding distance to sort the solutions and select only the number needed. + + # Calculate the crowding distance of the solutions of the pareto front. + obj_crowding_distance_list, crowding_distance_sum, crowding_dist_front_sorted_indices, crowding_dist_pop_sorted_indices = self.crowding_distance(pareto_front=current_pareto_front.copy(), + fitness=fitness) + + for selected_solution_idx in crowding_dist_pop_sorted_indices[0:num_remaining_parents]: + # Insert the parent into the parents array. + parents[current_parent_idx, :] = self.population[selected_solution_idx, :].copy() + # Insert the index of the selected parent. + parents_indices.append(selected_solution_idx) + # Increase the parent index. + current_parent_idx += 1 + + # Decrement the number of remaining parents by the number of selected parents. + num_remaining_parents -= num_remaining_parents + + # Increase the pareto front index to take parents from the next front. + pareto_front_idx += 1 + + # Make sure the parents indices is returned as a NumPy array. + return parents, numpy.array(parents_indices) diff --git a/pygad/visualize/__init__.py b/pygad/visualize/__init__.py new file mode 100644 index 00000000..056dc670 --- /dev/null +++ b/pygad/visualize/__init__.py @@ -0,0 +1,3 @@ +from pygad.visualize import plot + +__version__ = "1.1.0" \ No newline at end of file diff --git a/pygad/visualize/plot.py b/pygad/visualize/plot.py new file mode 100644 index 00000000..ab6bd256 --- /dev/null +++ b/pygad/visualize/plot.py @@ -0,0 +1,504 @@ +""" +The pygad.visualize.plot module has methods to create plots. +""" + +import numpy +# import matplotlib.pyplot +import pygad + +def get_matplotlib(): + # Importing matplotlib.pyplot at the module scope causes performance issues. + # This causes matplotlib.pyplot to be imported once pygad is imported. + # An efficient approach is to import matplotlib.pyplot only when needed. + # Inside each function, call get_matplotlib() to return the library object. + # If a function called get_matplotlib() once, then the library object is reused. + import matplotlib.pyplot as matplt + return matplt + +class Plot: + + def __init__(): + pass + + def plot_fitness(self, + title="PyGAD - Generation vs. Fitness", + xlabel="Generation", + ylabel="Fitness", + linewidth=3, + font_size=14, + plot_type="plot", + color="#64f20c", + label=None, + save_dir=None): + + """ + Creates, shows, and returns a figure that summarizes how the fitness value evolved by generation. Can only be called after completing at least 1 generation. If no generation is completed, an exception is raised. + + Accepts the following: + title: Figure title. + xlabel: Label on the X-axis. + ylabel: Label on the Y-axis. + linewidth: Line width of the plot. Defaults to 3. + font_size: Font size for the labels and title. Defaults to 14. Can be a list/tuple/numpy.ndarray if the problem is multi-objective optimization. + plot_type: Type of the plot which can be either "plot" (default), "scatter", or "bar". + color: Color of the plot which defaults to "#64f20c". Can be a list/tuple/numpy.ndarray if the problem is multi-objective optimization. + label: The label used for the legend in the figures of multi-objective problems. It is not used for single-objective problems. + save_dir: Directory to save the figure. + + Returns the figure. + """ + + if self.generations_completed < 1: + self.logger.error("The plot_fitness() (i.e. plot_result()) method can only be called after completing at least 1 generation but ({self.generations_completed}) is completed.") + raise RuntimeError("The plot_fitness() (i.e. plot_result()) method can only be called after completing at least 1 generation but ({self.generations_completed}) is completed.") + + matplt = get_matplotlib() + + fig = matplt.figure() + if type(self.best_solutions_fitness[0]) in [list, tuple, numpy.ndarray] and len(self.best_solutions_fitness[0]) > 1: + # Multi-objective optimization problem. + if type(linewidth) in pygad.GA.supported_int_float_types: + linewidth = [linewidth] + linewidth.extend([linewidth[0]]*len(self.best_solutions_fitness[0])) + elif type(linewidth) in [list, tuple, numpy.ndarray]: + pass + + if type(color) is str: + color = [color] + color.extend([None]*len(self.best_solutions_fitness[0])) + elif type(color) in [list, tuple, numpy.ndarray]: + pass + + if label is None: + label = [None]*len(self.best_solutions_fitness[0]) + + # Loop through each objective to plot its fitness. + for objective_idx in range(len(self.best_solutions_fitness[0])): + # Return the color, line width, and label of the current plot. + 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. + fitness = numpy.array(self.best_solutions_fitness)[:, objective_idx] + if plot_type == "plot": + matplt.plot(fitness, + linewidth=current_linewidth, + color=current_color, + label=current_label) + elif plot_type == "scatter": + matplt.scatter(range(len(fitness)), + fitness, + linewidth=current_linewidth, + color=current_color, + label=current_label) + elif plot_type == "bar": + matplt.bar(range(len(fitness)), + fitness, + linewidth=current_linewidth, + color=current_color, + label=current_label) + else: + # Single-objective optimization problem. + if plot_type == "plot": + matplt.plot(self.best_solutions_fitness, + linewidth=linewidth, + color=color) + elif plot_type == "scatter": + matplt.scatter(range(len(self.best_solutions_fitness)), + self.best_solutions_fitness, + linewidth=linewidth, + color=color) + elif plot_type == "bar": + matplt.bar(range(len(self.best_solutions_fitness)), + self.best_solutions_fitness, + linewidth=linewidth, + color=color) + matplt.title(title, fontsize=font_size) + matplt.xlabel(xlabel, fontsize=font_size) + matplt.ylabel(ylabel, fontsize=font_size) + # Create a legend out of the labels. + matplt.legend() + + if not save_dir is None: + matplt.savefig(fname=save_dir, + bbox_inches='tight') + matplt.show() + + return fig + + def plot_new_solution_rate(self, + title="PyGAD - Generation vs. New Solution Rate", + xlabel="Generation", + ylabel="New Solution Rate", + linewidth=3, + font_size=14, + plot_type="plot", + color="#64f20c", + save_dir=None): + + """ + Creates, shows, and returns a figure that summarizes the rate of exploring new solutions. This method works only when save_solutions=True in the constructor of the pygad.GA class. + + Accepts the following: + title: Figure title. + xlabel: Label on the X-axis. + ylabel: Label on the Y-axis. + linewidth: Line width of the plot. Defaults to 3. + font_size: Font size for the labels and title. Defaults to 14. + plot_type: Type of the plot which can be either "plot" (default), "scatter", or "bar". + color: Color of the plot which defaults to "#64f20c". + save_dir: Directory to save the figure. + + Returns the figure. + """ + + if self.generations_completed < 1: + self.logger.error("The plot_new_solution_rate() method can only be called after completing at least 1 generation but ({self.generations_completed}) is completed.") + raise RuntimeError("The plot_new_solution_rate() method can only be called after completing at least 1 generation but ({self.generations_completed}) is completed.") + + if self.save_solutions == False: + self.logger.error("The plot_new_solution_rate() method works only when save_solutions=True in the constructor of the pygad.GA class.") + raise RuntimeError("The plot_new_solution_rate() method works only when save_solutions=True in the constructor of the pygad.GA class.") + + unique_solutions = set() + num_unique_solutions_per_generation = [] + for generation_idx in range(self.generations_completed): + + len_before = len(unique_solutions) + + start = generation_idx * self.sol_per_pop + end = start + self.sol_per_pop + + for sol in self.solutions[start:end]: + unique_solutions.add(tuple(sol)) + + len_after = len(unique_solutions) + + generation_num_unique_solutions = len_after - len_before + num_unique_solutions_per_generation.append(generation_num_unique_solutions) + + matplt = get_matplotlib() + + fig = matplt.figure() + if plot_type == "plot": + matplt.plot(num_unique_solutions_per_generation, linewidth=linewidth, color=color) + elif plot_type == "scatter": + matplt.scatter(range(self.generations_completed), num_unique_solutions_per_generation, linewidth=linewidth, color=color) + elif plot_type == "bar": + matplt.bar(range(self.generations_completed), num_unique_solutions_per_generation, linewidth=linewidth, color=color) + matplt.title(title, fontsize=font_size) + matplt.xlabel(xlabel, fontsize=font_size) + matplt.ylabel(ylabel, fontsize=font_size) + + if not save_dir is None: + matplt.savefig(fname=save_dir, + bbox_inches='tight') + matplt.show() + + return fig + + def plot_genes(self, + title="PyGAD - Gene", + xlabel="Gene", + ylabel="Value", + linewidth=3, + font_size=14, + plot_type="plot", + graph_type="plot", + fill_color="#64f20c", + color="black", + solutions="all", + save_dir=None): + + """ + Creates, shows, and returns a figure with number of subplots equal to the number of genes. Each subplot shows the gene value for each generation. + This method works only when save_solutions=True in the constructor of the pygad.GA class. + It also works only after completing at least 1 generation. If no generation is completed, an exception is raised. + + Accepts the following: + title: Figure title. + xlabel: Label on the X-axis. + ylabel: Label on the Y-axis. + linewidth: Line width of the plot. Defaults to 3. + font_size: Font size for the labels and title. Defaults to 14. + plot_type: Type of the plot which can be either "plot" (default), "scatter", or "bar". + graph_type: Type of the graph which can be either "plot" (default), "boxplot", or "histogram". + fill_color: Fill color of the graph which defaults to "#64f20c". This has no effect if graph_type="plot". + color: Color of the plot which defaults to "black". + solutions: Defaults to "all" which means use all solutions. If "best" then only the best solutions are used. + save_dir: Directory to save the figure. + + Returns the figure. + """ + + if self.generations_completed < 1: + self.logger.error("The plot_genes() method can only be called after completing at least 1 generation but ({self.generations_completed}) is completed.") + raise RuntimeError("The plot_genes() method can only be called after completing at least 1 generation but ({self.generations_completed}) is completed.") + + matplt = get_matplotlib() + + if type(solutions) is str: + if solutions == 'all': + if self.save_solutions: + solutions_to_plot = numpy.array(self.solutions) + else: + self.logger.error("The plot_genes() method with solutions='all' can only be called if 'save_solutions=True' in the pygad.GA class constructor.") + raise RuntimeError("The plot_genes() method with solutions='all' can only be called if 'save_solutions=True' in the pygad.GA class constructor.") + elif solutions == 'best': + if self.save_best_solutions: + solutions_to_plot = self.best_solutions + else: + self.logger.error("The plot_genes() method with solutions='best' can only be called if 'save_best_solutions=True' in the pygad.GA class constructor.") + raise RuntimeError("The plot_genes() method with solutions='best' can only be called if 'save_best_solutions=True' in the pygad.GA class constructor.") + else: + self.logger.error("The solutions parameter can be either 'all' or 'best' but {solutions} found.") + raise RuntimeError("The solutions parameter can be either 'all' or 'best' but {solutions} found.") + else: + self.logger.error("The solutions parameter must be a string but {solutions_type} found.".format(solutions_type=type(solutions))) + raise RuntimeError("The solutions parameter must be a string but {solutions_type} found.".format(solutions_type=type(solutions))) + + if graph_type == "plot": + # num_rows will be always be >= 1 + # num_cols can only be 0 if num_genes=1 + num_rows = int(numpy.ceil(self.num_genes/5.0)) + num_cols = int(numpy.ceil(self.num_genes/num_rows)) + + if num_cols == 0: + figsize = (10, 8) + # There is only a single gene + fig, ax = matplt.subplots(num_rows, figsize=figsize) + if plot_type == "plot": + ax.plot(solutions_to_plot[:, 0], linewidth=linewidth, color=fill_color) + elif plot_type == "scatter": + ax.scatter(range(self.generations_completed + 1), solutions_to_plot[:, 0], linewidth=linewidth, color=fill_color) + elif plot_type == "bar": + ax.bar(range(self.generations_completed + 1), solutions_to_plot[:, 0], linewidth=linewidth, color=fill_color) + ax.set_xlabel(0, fontsize=font_size) + else: + fig, axs = matplt.subplots(num_rows, num_cols) + + if num_cols == 1 and num_rows == 1: + fig.set_figwidth(5 * num_cols) + fig.set_figheight(4) + axs.plot(solutions_to_plot[:, 0], linewidth=linewidth, color=fill_color) + axs.set_xlabel("Gene " + str(0), fontsize=font_size) + elif num_cols == 1 or num_rows == 1: + fig.set_figwidth(5 * num_cols) + fig.set_figheight(4) + for gene_idx in range(len(axs)): + if plot_type == "plot": + axs[gene_idx].plot(solutions_to_plot[:, gene_idx], linewidth=linewidth, color=fill_color) + elif plot_type == "scatter": + axs[gene_idx].scatter(range(solutions_to_plot.shape[0]), solutions_to_plot[:, gene_idx], linewidth=linewidth, color=fill_color) + elif plot_type == "bar": + axs[gene_idx].bar(range(solutions_to_plot.shape[0]), solutions_to_plot[:, gene_idx], linewidth=linewidth, color=fill_color) + axs[gene_idx].set_xlabel("Gene " + str(gene_idx), fontsize=font_size) + else: + gene_idx = 0 + fig.set_figwidth(25) + fig.set_figheight(4*num_rows) + for row_idx in range(num_rows): + for col_idx in range(num_cols): + if gene_idx >= self.num_genes: + # axs[row_idx, col_idx].remove() + break + if plot_type == "plot": + axs[row_idx, col_idx].plot(solutions_to_plot[:, gene_idx], linewidth=linewidth, color=fill_color) + elif plot_type == "scatter": + axs[row_idx, col_idx].scatter(range(solutions_to_plot.shape[0]), solutions_to_plot[:, gene_idx], linewidth=linewidth, color=fill_color) + elif plot_type == "bar": + axs[row_idx, col_idx].bar(range(solutions_to_plot.shape[0]), solutions_to_plot[:, gene_idx], linewidth=linewidth, color=fill_color) + axs[row_idx, col_idx].set_xlabel("Gene " + str(gene_idx), fontsize=font_size) + gene_idx += 1 + + fig.suptitle(title, fontsize=font_size, y=1.001) + matplt.tight_layout() + + elif graph_type == "boxplot": + fig = matplt.figure(1, figsize=(0.7*self.num_genes, 6)) + + # Create an axes instance + ax = fig.add_subplot(111) + boxeplots = ax.boxplot(solutions_to_plot, + labels=range(self.num_genes), + patch_artist=True) + # adding horizontal grid lines + ax.yaxis.grid(True) + + for box in boxeplots['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']: + whisker.set(color=color, linewidth=linewidth) + for median in boxeplots['medians']: + median.set(color=color, linewidth=linewidth) + for cap in boxeplots['caps']: + cap.set(color=color, linewidth=linewidth) + + matplt.title(title, fontsize=font_size) + matplt.xlabel(xlabel, fontsize=font_size) + matplt.ylabel(ylabel, fontsize=font_size) + matplt.tight_layout() + + elif graph_type == "histogram": + # num_rows will always be >= 1 + # num_cols can only be 0 if num_genes=1 + num_rows = int(numpy.ceil(self.num_genes/5.0)) + num_cols = int(numpy.ceil(self.num_genes/num_rows)) + + if num_cols == 0: + figsize = (10, 8) + # There is only a single gene + fig, ax = matplt.subplots(num_rows, + figsize=figsize) + ax.hist(solutions_to_plot[:, 0], color=fill_color) + ax.set_xlabel(0, fontsize=font_size) + else: + fig, axs = matplt.subplots(num_rows, num_cols) + + if num_cols == 1 and num_rows == 1: + fig.set_figwidth(4 * num_cols) + fig.set_figheight(3) + axs.hist(solutions_to_plot[:, 0], + color=fill_color, + rwidth=0.95) + axs.set_xlabel("Gene " + str(0), fontsize=font_size) + elif num_cols == 1 or num_rows == 1: + fig.set_figwidth(4 * num_cols) + fig.set_figheight(3) + for gene_idx in range(len(axs)): + axs[gene_idx].hist(solutions_to_plot[:, gene_idx], + color=fill_color, + rwidth=0.95) + axs[gene_idx].set_xlabel("Gene " + str(gene_idx), fontsize=font_size) + else: + gene_idx = 0 + fig.set_figwidth(20) + fig.set_figheight(3*num_rows) + for row_idx in range(num_rows): + for col_idx in range(num_cols): + if gene_idx >= self.num_genes: + # axs[row_idx, col_idx].remove() + break + axs[row_idx, col_idx].hist(solutions_to_plot[:, gene_idx], + color=fill_color, + rwidth=0.95) + axs[row_idx, col_idx].set_xlabel("Gene " + str(gene_idx), fontsize=font_size) + gene_idx += 1 + + fig.suptitle(title, fontsize=font_size, y=1.001) + matplt.tight_layout() + + if not save_dir is None: + matplt.savefig(fname=save_dir, + bbox_inches='tight') + + matplt.show() + + return fig + + def plot_pareto_front_curve(self, + title="Pareto Front Curve", + xlabel="Objective 1", + ylabel="Objective 2", + linewidth=3, + font_size=14, + label="Pareto Front", + color="#FF6347", + color_fitness="#4169E1", + grid=True, + alpha=0.7, + marker="o", + save_dir=None): + """ + Creates, shows, and returns the pareto front curve. Can only be used with multi-objective problems. + It only works with 2 objectives. + It also works only after completing at least 1 generation. If no generation is completed, an exception is raised. + + Accepts the following: + title: Figure title. + xlabel: Label on the X-axis. + ylabel: Label on the Y-axis. + linewidth: Line width of the plot. Defaults to 3. + font_size: Font size for the labels and title. Defaults to 14. + label: The label used for the legend. + color: Color of the plot. + color_fitness: Color of the fitness points. + grid: Either True or False to control the visibility of the grid. + alpha: The transparency of the pareto front curve. + marker: The marker of the fitness points. + save_dir: Directory to save the figure. + + Returns the figure. + """ + + if self.generations_completed < 1: + self.logger.error("The plot_pareto_front_curve() method can only be called after completing at least 1 generation but ({self.generations_completed}) is completed.") + raise RuntimeError("The plot_pareto_front_curve() method can only be called after completing at least 1 generation but ({self.generations_completed}) is completed.") + + if type(self.best_solutions_fitness[0]) in [list, tuple, numpy.ndarray] and len(self.best_solutions_fitness[0]) > 1: + # Multi-objective optimization problem. + if len(self.best_solutions_fitness[0]) == 2: + # Only 2 objectives. Proceed. + pass + else: + # More than 2 objectives. + self.logger.error(f"The plot_pareto_front_curve() method only supports 2 objectives but there are {self.best_solutions_fitness[0]} objectives.") + raise RuntimeError(f"The plot_pareto_front_curve() method only supports 2 objectives but there are {self.best_solutions_fitness[0]} objectives.") + else: + # Single-objective optimization problem. + self.logger.error("The plot_pareto_front_curve() method only works with multi-objective optimization problems.") + raise RuntimeError("The plot_pareto_front_curve() method only works with multi-objective optimization problems.") + + # Plot the pareto front curve. + remaining_set = list(zip(range(0, self.last_generation_fitness.shape[0]), self.last_generation_fitness)) + dominated_set, non_dominated_set = self.get_non_dominated_set(remaining_set) + + # Extract the fitness values (objective values) of the non-dominated solutions for plotting. + pareto_front_x = [self.last_generation_fitness[item[0]][0] for item in dominated_set] + pareto_front_y = [self.last_generation_fitness[item[0]][1] for item in dominated_set] + + # Sort the Pareto front solutions (optional but can make the plot cleaner) + sorted_pareto_front = sorted(zip(pareto_front_x, pareto_front_y)) + + matplt = get_matplotlib() + + # Plotting + fig = matplt.figure() + # First, plot the scatter of all points (population) + all_points_x = [self.last_generation_fitness[i][0] for i in range(self.sol_per_pop)] + all_points_y = [self.last_generation_fitness[i][1] for i in range(self.sol_per_pop)] + matplt.scatter(all_points_x, + all_points_y, + marker=marker, + color=color_fitness, + label='Fitness', + alpha=1.0) + + # Then, plot the Pareto front as a curve + pareto_front_x_sorted, pareto_front_y_sorted = zip(*sorted_pareto_front) + matplt.plot(pareto_front_x_sorted, + pareto_front_y_sorted, + marker=marker, + label=label, + alpha=alpha, + color=color, + linewidth=linewidth) + + matplt.title(title, fontsize=font_size) + matplt.xlabel(xlabel, fontsize=font_size) + matplt.ylabel(ylabel, fontsize=font_size) + matplt.legend() + + matplt.grid(grid) + + if not save_dir is None: + matplt.savefig(fname=save_dir, + bbox_inches='tight') + + matplt.show() + + return fig diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..aed6e1d8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,62 @@ +[build-system] +# Consider migrating to hatching later: https://hatch.pypa.io/latest/intro/#existing-project +# requires = ["setuptools>=61.0"] +# requires = ["setuptools==59.2"] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" +# requires = ["hatchling"] +# build-backend = "hatchling.build" + +[project] +name = "pygad" +version = "3.4.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"} +authors = [ + {name = "Ahmed Gad", email = "ahmed.f.gad@gmail.com"}, +] +maintainers = [ +{name = "Ahmed Gad", email = "ahmed.f.gad@gmail.com"} +] +classifiers = [ + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Natural Language :: English", + "Operating System :: OS Independent", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Bio-Informatics", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Software Development", + "Topic :: Utilities", + "Intended Audience :: Information Technology", + "Intended Audience :: Science/Research", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: Other Audience" +] +keywords = ["genetic algorithm", "GA", "optimization", "evolutionary algorithm", "natural evolution", "pygad", "machine learning", "deep learning", "neural networks", "tensorflow", "keras", "pytorch"] +dependencies = [ + "numpy", + "matplotlib", + "cloudpickle", +] + +[project.urls] +"Homepage" = "https://github.com/ahmedfgad/GeneticAlgorithmPython" +"Documentation" = "https://pygad.readthedocs.io" +"GitHub Repository" = "https://github.com/ahmedfgad/GeneticAlgorithmPython" +"PyPI Project" = "https://pypi.org/project/pygad" +"Conda Forge Project" = "https://anaconda.org/conda-forge/pygad" +"Donation Stripe" = "https://donate.stripe.com/eVa5kO866elKgM0144" +"Donation Open Collective" = "https://opencollective.com/pygad" +"Donation Paypal" = "http://paypal.me/ahmedfgad" + +[project.optional-dependencies] +deep_learning = ["keras", "torch"] + +# PyTest Configuration. Later, PyTest will support the [tool.pytest] table. +[tool.pytest.ini_options] +testpaths = ["tests"] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c29fbc3a..a344de1c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ numpy -matplotlib \ No newline at end of file +matplotlib +cloudpickle \ No newline at end of file diff --git a/setup.py b/setup.py index 61c1ed23..d9ec3090 100644 --- a/setup.py +++ b/setup.py @@ -5,11 +5,11 @@ setuptools.setup( name="pygad", - version="2.14.3", + version="3.4.0", author="Ahmed Fawzy Gad", - install_requires=["numpy", "matplotlib",], + install_requires=["numpy", "matplotlib", "cloudpickle",], author_email="ahmed.f.gad@gmail.com", - description="PyGAD: A Python 3 Library for Building the Genetic Algorithm and Training Machine Learning Algoithms (Keras & PyTorch).", + 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", url="https://github.com/ahmedfgad/GeneticAlgorithmPython", diff --git a/tests/test_adaptive_mutation.py b/tests/test_adaptive_mutation.py new file mode 100644 index 00000000..afac8d8f --- /dev/null +++ b/tests/test_adaptive_mutation.py @@ -0,0 +1,2393 @@ +import pygad +import random +import numpy + +num_generations = 1 + +initial_population = [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]] + +#### Define the fitness functions in the top-level of the module so that they are picklable and usable in the process-based parallel processing works. +#### If the functions are defined inside a class/method/function, they are not picklable and this error is raised: AttributeError: Can't pickle local object +#### Process-based parallel processing must have the used functions picklable. +def fitness_func_no_batch_single(ga, solution, idx): + return random.random() + +def fitness_func_batch_single(ga, soluions, idxs): + return numpy.random.uniform(size=len(soluions)) + +def fitness_func_no_batch_multi(ga, solution, idx): + return [random.random(), random.random()] + +def fitness_func_batch_multi(ga, soluions, idxs): + f = [] + for sol in soluions: + f.append([random.random(), random.random()]) + return f + +def output_adaptive_mutation(gene_space=None, + gene_type=float, + num_genes=10, + mutation_by_replacement=False, + random_mutation_min_val=-1, + random_mutation_max_val=1, + init_range_low=-4, + init_range_high=4, + initial_population=None, + mutation_probability=None, + mutation_num_genes=None, + fitness_batch_size=None, + mutation_type="adaptive", + parent_selection_type='sss', + parallel_processing=None, + multi_objective=False): + + if fitness_batch_size is None or (type(fitness_batch_size) in pygad.GA.supported_int_types and fitness_batch_size == 1): + if multi_objective == True: + fitness_func = fitness_func_no_batch_multi + else: + fitness_func = fitness_func_no_batch_single + elif (type(fitness_batch_size) in pygad.GA.supported_int_types and fitness_batch_size > 1): + if multi_objective == True: + fitness_func = fitness_func_batch_multi + else: + fitness_func = fitness_func_batch_single + + ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=5, + fitness_func=fitness_func, + sol_per_pop=10, + num_genes=num_genes, + gene_space=gene_space, + gene_type=gene_type, + initial_population=initial_population, + init_range_low=init_range_low, + init_range_high=init_range_high, + parent_selection_type=parent_selection_type, + random_mutation_min_val=random_mutation_min_val, + random_mutation_max_val=random_mutation_max_val, + allow_duplicate_genes=True, + mutation_by_replacement=mutation_by_replacement, + save_solutions=True, + ## Use a static 'mutation_probability'. + ## An ambigius error in GitHub actions happen when using mutation_num_genes and mutation_probability. I do not know the reason. + # mutation_num_genes=mutation_num_genes, + mutation_probability=[0.2, 0.1], + mutation_type=mutation_type, + suppress_warnings=True, + fitness_batch_size=fitness_batch_size, + parallel_processing=parallel_processing, + random_seed=1) + + ga_instance.run() + + return None, ga_instance + +def test_adaptive_mutation(): + result, ga_instance = output_adaptive_mutation() + + # assert result == True + +def test_adaptive_mutation_gene_space(): + result, ga_instance = output_adaptive_mutation(gene_space=range(10)) + + # assert result == True + +def test_adaptive_mutation_int_gene_type(): + result, ga_instance = output_adaptive_mutation(gene_type=int) + + # assert result == True + +def test_adaptive_mutation_gene_space_gene_type(): + result, ga_instance = output_adaptive_mutation(gene_space={"low": 0, "high": 10}, + gene_type=[float, 2]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space(): + result, ga_instance = output_adaptive_mutation(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]]) + # assert result == True + +def test_adaptive_mutation_nested_gene_type(): + result, ga_instance = output_adaptive_mutation(gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_nested_gene_type(): + result, ga_instance = output_adaptive_mutation(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + + # assert result == True + +def test_adaptive_mutation_initial_population(): + global initial_population + result, ga_instance = output_adaptive_mutation(initial_population=initial_population) + + # assert result == True + +def test_adaptive_mutation_initial_population_nested_gene_type(): + global initial_population + result, ga_instance = output_adaptive_mutation(initial_population=initial_population, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + + # assert result == True + +def test_adaptive_mutation_fitness_batch_size_1(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=1) + +def test_adaptive_mutation_fitness_batch_size_2(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=2) + +def test_adaptive_mutation_fitness_batch_size_3(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=3) + +def test_adaptive_mutation_fitness_batch_size_4(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=4) + +def test_adaptive_mutation_fitness_batch_size_5(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=5) + +def test_adaptive_mutation_fitness_batch_size_6(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=6) + +def test_adaptive_mutation_fitness_batch_size_7(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=7) + +def test_adaptive_mutation_fitness_batch_size_8(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=8) + +def test_adaptive_mutation_fitness_batch_size_9(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=9) + +def test_adaptive_mutation_fitness_batch_size_10(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=10) + +#### Single-Objective Mutation Probability +def test_adaptive_mutation_mutation_probability(): + result, ga_instance = output_adaptive_mutation(mutation_probability=[0.2, 0.1]) + + # assert result == True + +def test_adaptive_mutation_gene_space_mutation_probability(): + result, ga_instance = output_adaptive_mutation(gene_space=range(10), + mutation_probability=[0.2, 0.1]) + + # assert result == True + +def test_adaptive_mutation_int_gene_type_mutation_probability(): + result, ga_instance = output_adaptive_mutation(gene_type=int, + mutation_probability=[0.2, 0.1]) + + # assert result == True + +def test_adaptive_mutation_gene_space_gene_type_mutation_probability(): + result, ga_instance = output_adaptive_mutation(gene_space={"low": 0, "high": 10}, + gene_type=[float, 2], + mutation_probability=[0.2, 0.1]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_mutation_probability(): + result, ga_instance = output_adaptive_mutation(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + mutation_probability=[0.2, 0.1]) + # assert result == True + +def test_adaptive_mutation_nested_gene_type_mutation_probability(): + result, ga_instance = output_adaptive_mutation(gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_probability=[0.2, 0.1]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_nested_gene_type_mutation_probability(): + result, ga_instance = output_adaptive_mutation(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_probability=[0.2, 0.1]) + + # assert result == True + +def test_adaptive_mutation_initial_population_mutation_probability(): + global initial_population + result, ga_instance = output_adaptive_mutation(initial_population=initial_population, + mutation_probability=[0.2, 0.1]) + + # assert result == True + +def test_adaptive_mutation_initial_population_nested_gene_type_mutation_probability(): + global initial_population + result, ga_instance = output_adaptive_mutation(initial_population=initial_population, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_probability=[0.2, 0.1]) + + # assert result == True + +def test_adaptive_mutation_fitness_batch_size_1_mutation_probability(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=1, + mutation_probability=[0.2, 0.1]) + +def test_adaptive_mutation_fitness_batch_size_2_mutation_probability(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=2, + mutation_probability=[0.2, 0.1]) + +def test_adaptive_mutation_fitness_batch_size_3_mutation_probability(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=3, + mutation_probability=[0.2, 0.1]) + +def test_adaptive_mutation_fitness_batch_size_4_mutation_probability(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=4, + mutation_probability=[0.2, 0.1]) + +def test_adaptive_mutation_fitness_batch_size_5_mutation_probability(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=5, + mutation_probability=[0.2, 0.1]) + +def test_adaptive_mutation_fitness_batch_size_6_mutation_probability(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=6, + mutation_probability=[0.2, 0.1]) + +def test_adaptive_mutation_fitness_batch_size_7_mutation_probability(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=7, + mutation_probability=[0.2, 0.1]) + +def test_adaptive_mutation_fitness_batch_size_8_mutation_probability(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=8, + mutation_probability=[0.2, 0.1]) + +def test_adaptive_mutation_fitness_batch_size_9_mutation_probability(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=9, + mutation_probability=[0.2, 0.1]) + +def test_adaptive_mutation_fitness_batch_size_10_mutation_probability(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=10, + mutation_probability=[0.2, 0.1]) + + +#### Single-Objective Mutation Number of Genes +def test_adaptive_mutation_mutation_num_genes(): + result, ga_instance = output_adaptive_mutation(mutation_num_genes=[6, 4]) + + # assert result == True + +def test_adaptive_mutation_gene_space_mutation_num_genes(): + result, ga_instance = output_adaptive_mutation(gene_space=range(10), + mutation_num_genes=[6, 4]) + + # assert result == True + +def test_adaptive_mutation_int_gene_type_mutation_num_genes(): + result, ga_instance = output_adaptive_mutation(gene_type=int, + mutation_num_genes=[6, 4]) + + # assert result == True + +def test_adaptive_mutation_gene_space_gene_type_mutation_num_genes(): + result, ga_instance = output_adaptive_mutation(gene_space={"low": 0, "high": 10}, + gene_type=[float, 2], + mutation_num_genes=[6, 4]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_mutation_num_genes(): + result, ga_instance = output_adaptive_mutation(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + mutation_num_genes=[6, 4]) + # assert result == True + +def test_adaptive_mutation_nested_gene_type_mutation_num_genes(): + result, ga_instance = output_adaptive_mutation(gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_num_genes=[6, 4]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_nested_gene_type_mutation_num_genes(): + result, ga_instance = output_adaptive_mutation(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_num_genes=[6, 4]) + + # assert result == True + +def test_adaptive_mutation_initial_population_mutation_num_genes(): + global initial_population + result, ga_instance = output_adaptive_mutation(initial_population=initial_population, + mutation_num_genes=[6, 4]) + + # assert result == True + +def test_adaptive_mutation_initial_population_nested_gene_type_mutation_num_genes(): + global initial_population + result, ga_instance = output_adaptive_mutation(initial_population=initial_population, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_num_genes=[6, 4]) + + # assert result == True + +def test_adaptive_mutation_fitness_batch_size_1_mutation_num_genes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=1, + mutation_num_genes=[6, 4]) + +def test_adaptive_mutation_fitness_batch_size_2_mutation_num_genes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=2, + mutation_num_genes=[6, 4]) + +def test_adaptive_mutation_fitness_batch_size_3_mutation_num_genes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=3, + mutation_num_genes=[6, 4]) + +def test_adaptive_mutation_fitness_batch_size_4_mutation_num_genes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=4, + mutation_num_genes=[6, 4]) + +def test_adaptive_mutation_fitness_batch_size_5_mutation_num_genes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=5, + mutation_num_genes=[6, 4]) + +def test_adaptive_mutation_fitness_batch_size_6_mutation_num_genes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=6, + mutation_num_genes=[6, 4]) + +def test_adaptive_mutation_fitness_batch_size_7_mutation_num_genes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=7, + mutation_num_genes=[6, 4]) + +def test_adaptive_mutation_fitness_batch_size_8_mutation_num_genes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=8, + mutation_num_genes=[6, 4]) + +def test_adaptive_mutation_fitness_batch_size_9_mutation_num_genes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=9, + mutation_num_genes=[6, 4]) + +def test_adaptive_mutation_fitness_batch_size_10_mutation_num_genes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=10, + mutation_num_genes=[6, 4]) + +#### Multi-Objective Mutation Probability +def test_adaptive_mutation_mutation_probability_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + mutation_probability=[0.2, 0.1]) + + # assert result == True + +def test_adaptive_mutation_gene_space_mutation_probability_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_space=range(10), + mutation_probability=[0.2, 0.1]) + + # assert result == True + +def test_adaptive_mutation_int_gene_type_mutation_probability_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_type=int, + mutation_probability=[0.2, 0.1]) + + # assert result == True + +def test_adaptive_mutation_gene_space_gene_type_mutation_probability_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_space={"low": 0, "high": 10}, + gene_type=[float, 2], + mutation_probability=[0.2, 0.1]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_mutation_probability_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + mutation_probability=[0.2, 0.1]) + # assert result == True + +def test_adaptive_mutation_nested_gene_type_mutation_probability_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_probability=[0.2, 0.1]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_nested_gene_type_mutation_probability_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_probability=[0.2, 0.1]) + + # assert result == True + +def test_adaptive_mutation_initial_population_mutation_probability_multi_objective(): + global initial_population + result, ga_instance = output_adaptive_mutation(multi_objective=True, + initial_population=initial_population, + mutation_probability=[0.2, 0.1]) + + # assert result == True + +def test_adaptive_mutation_initial_population_nested_gene_type_mutation_probability_multi_objective(): + global initial_population + result, ga_instance = output_adaptive_mutation(multi_objective=True, + initial_population=initial_population, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_probability=[0.2, 0.1]) + + # assert result == True + +def test_adaptive_mutation_fitness_batch_size_1_mutation_probability_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=1, + mutation_probability=[0.2, 0.1]) + +def test_adaptive_mutation_fitness_batch_size_2_mutation_probability_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=2, + mutation_probability=[0.2, 0.1]) + +def test_adaptive_mutation_fitness_batch_size_3_mutation_probability_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=3, + mutation_probability=[0.2, 0.1]) + +def test_adaptive_mutation_fitness_batch_size_4_mutation_probability_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=4, + mutation_probability=[0.2, 0.1]) + +def test_adaptive_mutation_fitness_batch_size_5_mutation_probability_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=5, + mutation_probability=[0.2, 0.1]) + +def test_adaptive_mutation_fitness_batch_size_6_mutation_probability_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=6, + mutation_probability=[0.2, 0.1]) + +def test_adaptive_mutation_fitness_batch_size_7_mutation_probability_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=7, + mutation_probability=[0.2, 0.1]) + +def test_adaptive_mutation_fitness_batch_size_8_mutation_probability_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=8, + mutation_probability=[0.2, 0.1]) + +def test_adaptive_mutation_fitness_batch_size_9_mutation_probability_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=9, + mutation_probability=[0.2, 0.1]) + +def test_adaptive_mutation_fitness_batch_size_10_mutation_probability_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=10, + mutation_probability=[0.2, 0.1]) + +#### Multi-Objective Mutation Number of Genes +def test_adaptive_mutation_mutation_num_genes_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + mutation_num_genes=[6, 4]) + + # assert result == True + +def test_adaptive_mutation_gene_space_mutation_num_genes_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_space=range(10), + mutation_num_genes=[6, 4]) + + # assert result == True + +def test_adaptive_mutation_int_gene_type_mutation_num_genes_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_type=int, + mutation_num_genes=[6, 4]) + + # assert result == True + +def test_adaptive_mutation_gene_space_gene_type_mutation_num_genes_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_space={"low": 0, "high": 10}, + gene_type=[float, 2], + mutation_num_genes=[6, 4]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_mutation_num_genes_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + mutation_num_genes=[6, 4]) + # assert result == True + +def test_adaptive_mutation_nested_gene_type_mutation_num_genes_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_num_genes=[6, 4]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_nested_gene_type_mutation_num_genes_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_num_genes=[6, 4]) + + # assert result == True + +def test_adaptive_mutation_initial_population_mutation_num_genes_multi_objective(): + global initial_population + result, ga_instance = output_adaptive_mutation(multi_objective=True, + initial_population=initial_population, + mutation_num_genes=[6, 4]) + + # assert result == True + +def test_adaptive_mutation_initial_population_nested_gene_type_mutation_num_genes_multi_objective(): + global initial_population + result, ga_instance = output_adaptive_mutation(multi_objective=True, + initial_population=initial_population, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_num_genes=[6, 4]) + + # assert result == True + +def test_adaptive_mutation_fitness_batch_size_1_mutation_num_genes_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=1, + mutation_num_genes=[6, 4]) + +def test_adaptive_mutation_fitness_batch_size_2_mutation_num_genes_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=2, + mutation_num_genes=[6, 4]) + +def test_adaptive_mutation_fitness_batch_size_3_mutation_num_genes_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=3, + mutation_num_genes=[6, 4]) + +def test_adaptive_mutation_fitness_batch_size_4_mutation_num_genes_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=4, + mutation_num_genes=[6, 4]) + +def test_adaptive_mutation_fitness_batch_size_5_mutation_num_genes_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=5, + mutation_num_genes=[6, 4]) + +def test_adaptive_mutation_fitness_batch_size_6_mutation_num_genes_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=6, + mutation_num_genes=[6, 4]) + +def test_adaptive_mutation_fitness_batch_size_7_mutation_num_genes_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=7, + mutation_num_genes=[6, 4]) + +def test_adaptive_mutation_fitness_batch_size_8_mutation_num_genes_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=8, + mutation_num_genes=[6, 4]) + +def test_adaptive_mutation_fitness_batch_size_9_mutation_num_genes_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=9, + mutation_num_genes=[6, 4]) + +def test_adaptive_mutation_fitness_batch_size_10_mutation_num_genes_multi_objective(): + result, ga_instance = output_adaptive_mutation(multi_objective=True,fitness_batch_size=10, + mutation_num_genes=[6, 4]) + +######## Parallel Processing +#### #### Threads + +#### Single-Objective Mutation Probability +def test_adaptive_mutation_mutation_probability_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_gene_space_mutation_probability_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(gene_space=range(10), + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_int_gene_type_mutation_probability_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(gene_type=int, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_gene_space_gene_type_mutation_probability_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(gene_space={"low": 0, "high": 10}, + gene_type=[float, 2], + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_mutation_probability_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + # assert result == True + +def test_adaptive_mutation_nested_gene_type_mutation_probability_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_nested_gene_type_mutation_probability_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_initial_population_mutation_probability_parallel_processing_threads(): + global initial_population + result, ga_instance = output_adaptive_mutation(initial_population=initial_population, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_initial_population_nested_gene_type_mutation_probability_parallel_processing_threads(): + global initial_population + result, ga_instance = output_adaptive_mutation(initial_population=initial_population, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_fitness_batch_size_1_mutation_probability_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=1, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_2_mutation_probability_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=2, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_3_mutation_probability_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=3, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_4_mutation_probability_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=4, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_5_mutation_probability_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=5, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_6_mutation_probability_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=6, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_7_mutation_probability_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=7, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_8_mutation_probability_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=8, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_9_mutation_probability_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=9, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_10_mutation_probability_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=10, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + + +#### Single-Objective Mutation Number of Genes +def test_adaptive_mutation_mutation_num_genes_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_gene_space_mutation_num_genes_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(gene_space=range(10), + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_int_gene_type_mutation_num_genes_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(gene_type=int, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_gene_space_gene_type_mutation_num_genes_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(gene_space={"low": 0, "high": 10}, + gene_type=[float, 2], + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_mutation_num_genes_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + # assert result == True + +def test_adaptive_mutation_nested_gene_type_mutation_num_genes_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_nested_gene_type_mutation_num_genes_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_initial_population_mutation_num_genes_parallel_processing_threads(): + global initial_population + result, ga_instance = output_adaptive_mutation(initial_population=initial_population, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_initial_population_nested_gene_type_mutation_num_genes_parallel_processing_threads(): + global initial_population + result, ga_instance = output_adaptive_mutation(initial_population=initial_population, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_fitness_batch_size_1_mutation_num_genes_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=1, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_2_mutation_num_genes_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=2, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_3_mutation_num_genes_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=3, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_4_mutation_num_genes_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=4, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_5_mutation_num_genes_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=5, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_6_mutation_num_genes_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=6, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_7_mutation_num_genes_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=7, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_8_mutation_num_genes_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=8, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_9_mutation_num_genes_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=9, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_10_mutation_num_genes_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=10, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + +#### Multi-Objective Mutation Probability +def test_adaptive_mutation_mutation_probability_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_gene_space_mutation_probability_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_space=range(10), + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_int_gene_type_mutation_probability_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_type=int, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_gene_space_gene_type_mutation_probability_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_space={"low": 0, "high": 10}, + gene_type=[float, 2], + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_mutation_probability_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + # assert result == True + +def test_adaptive_mutation_nested_gene_type_mutation_probability_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_nested_gene_type_mutation_probability_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_initial_population_mutation_probability_multi_objective_parallel_processing_threads(): + global initial_population + result, ga_instance = output_adaptive_mutation(multi_objective=True, + initial_population=initial_population, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_initial_population_nested_gene_type_mutation_probability_multi_objective_parallel_processing_threads(): + global initial_population + result, ga_instance = output_adaptive_mutation(multi_objective=True, + initial_population=initial_population, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_fitness_batch_size_1_mutation_probability_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=1, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_2_mutation_probability_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=2, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_3_mutation_probability_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=3, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_4_mutation_probability_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=4, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_5_mutation_probability_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=5, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_6_mutation_probability_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=6, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_7_mutation_probability_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=7, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_8_mutation_probability_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=8, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_9_mutation_probability_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=9, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_10_mutation_probability_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=10, + mutation_probability=[0.2, 0.1], + parallel_processing=['thread', 4]) + +#### Multi-Objective Mutation Number of Genes +def test_adaptive_mutation_mutation_num_genes_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_gene_space_mutation_num_genes_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_space=range(10), + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_int_gene_type_mutation_num_genes_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_type=int, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_gene_space_gene_type_mutation_num_genes_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_space={"low": 0, "high": 10}, + gene_type=[float, 2], + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_mutation_num_genes_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + # assert result == True + +def test_adaptive_mutation_nested_gene_type_mutation_num_genes_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_nested_gene_type_mutation_num_genes_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_initial_population_mutation_num_genes_multi_objective_parallel_processing_threads(): + global initial_population + result, ga_instance = output_adaptive_mutation(multi_objective=True, + initial_population=initial_population, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_initial_population_nested_gene_type_mutation_num_genes_multi_objective_parallel_processing_threads(): + global initial_population + result, ga_instance = output_adaptive_mutation(multi_objective=True, + initial_population=initial_population, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + + # assert result == True + +def test_adaptive_mutation_fitness_batch_size_1_mutation_num_genes_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=1, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_2_mutation_num_genes_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=2, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_3_mutation_num_genes_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=3, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_4_mutation_num_genes_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=4, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_5_mutation_num_genes_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=5, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_6_mutation_num_genes_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=6, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_7_mutation_num_genes_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=7, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_8_mutation_num_genes_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=8, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_9_mutation_num_genes_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=9, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + +def test_adaptive_mutation_fitness_batch_size_10_mutation_num_genes_multi_objective_parallel_processing_threads(): + result, ga_instance = output_adaptive_mutation(multi_objective=True,fitness_batch_size=10, + mutation_num_genes=[6, 4], + parallel_processing=['thread', 4]) + + +#### #### Processes + +#### Single-Objective Mutation Probability +def test_adaptive_mutation_mutation_probability_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_gene_space_mutation_probability_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(gene_space=range(10), + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_int_gene_type_mutation_probability_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(gene_type=int, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_gene_space_gene_type_mutation_probability_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(gene_space={"low": 0, "high": 10}, + gene_type=[float, 2], + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_mutation_probability_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + # assert result == True + +def test_adaptive_mutation_nested_gene_type_mutation_probability_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_nested_gene_type_mutation_probability_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_initial_population_mutation_probability_parallel_processing_processes(): + global initial_population + result, ga_instance = output_adaptive_mutation(initial_population=initial_population, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_initial_population_nested_gene_type_mutation_probability_parallel_processing_processes(): + global initial_population + result, ga_instance = output_adaptive_mutation(initial_population=initial_population, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_fitness_batch_size_1_mutation_probability_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=1, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_2_mutation_probability_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=2, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_3_mutation_probability_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=3, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_4_mutation_probability_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=4, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_5_mutation_probability_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=5, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_6_mutation_probability_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=6, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_7_mutation_probability_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=7, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_8_mutation_probability_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=8, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_9_mutation_probability_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=9, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_10_mutation_probability_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=10, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + + +#### Single-Objective Mutation Number of Genes +def test_adaptive_mutation_mutation_num_genes_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_gene_space_mutation_num_genes_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(gene_space=range(10), + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_int_gene_type_mutation_num_genes_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(gene_type=int, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_gene_space_gene_type_mutation_num_genes_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(gene_space={"low": 0, "high": 10}, + gene_type=[float, 2], + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_mutation_num_genes_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + # assert result == True + +def test_adaptive_mutation_nested_gene_type_mutation_num_genes_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_nested_gene_type_mutation_num_genes_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_initial_population_mutation_num_genes_parallel_processing_processes(): + global initial_population + result, ga_instance = output_adaptive_mutation(initial_population=initial_population, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_initial_population_nested_gene_type_mutation_num_genes_parallel_processing_processes(): + global initial_population + result, ga_instance = output_adaptive_mutation(initial_population=initial_population, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_fitness_batch_size_1_mutation_num_genes_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=1, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_2_mutation_num_genes_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=2, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_3_mutation_num_genes_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=3, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_4_mutation_num_genes_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=4, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_5_mutation_num_genes_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=5, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_6_mutation_num_genes_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=6, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_7_mutation_num_genes_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=7, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_8_mutation_num_genes_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=8, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_9_mutation_num_genes_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=9, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_10_mutation_num_genes_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(fitness_batch_size=10, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + +#### Multi-Objective Mutation Probability +def test_adaptive_mutation_mutation_probability_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_gene_space_mutation_probability_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_space=range(10), + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_int_gene_type_mutation_probability_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_type=int, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_gene_space_gene_type_mutation_probability_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_space={"low": 0, "high": 10}, + gene_type=[float, 2], + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_mutation_probability_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + # assert result == True + +def test_adaptive_mutation_nested_gene_type_mutation_probability_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_nested_gene_type_mutation_probability_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_initial_population_mutation_probability_multi_objective_parallel_processing_processes(): + global initial_population + result, ga_instance = output_adaptive_mutation(multi_objective=True, + initial_population=initial_population, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_initial_population_nested_gene_type_mutation_probability_multi_objective_parallel_processing_processes(): + global initial_population + result, ga_instance = output_adaptive_mutation(multi_objective=True, + initial_population=initial_population, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_fitness_batch_size_1_mutation_probability_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=1, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_2_mutation_probability_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=2, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_3_mutation_probability_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=3, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_4_mutation_probability_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=4, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_5_mutation_probability_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=5, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_6_mutation_probability_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=6, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_7_mutation_probability_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=7, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_8_mutation_probability_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=8, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_9_mutation_probability_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=9, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_10_mutation_probability_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=10, + mutation_probability=[0.2, 0.1], + parallel_processing=['process', 4]) + +#### Multi-Objective Mutation Number of Genes +def test_adaptive_mutation_mutation_num_genes_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_gene_space_mutation_num_genes_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_space=range(10), + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_int_gene_type_mutation_num_genes_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_type=int, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_gene_space_gene_type_mutation_num_genes_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_space={"low": 0, "high": 10}, + gene_type=[float, 2], + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_mutation_num_genes_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + # assert result == True + +def test_adaptive_mutation_nested_gene_type_mutation_num_genes_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_nested_gene_space_nested_gene_type_mutation_num_genes_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_initial_population_mutation_num_genes_multi_objective_parallel_processing_processes(): + global initial_population + result, ga_instance = output_adaptive_mutation(multi_objective=True, + initial_population=initial_population, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_initial_population_nested_gene_type_mutation_num_genes_multi_objective_parallel_processing_processes(): + global initial_population + result, ga_instance = output_adaptive_mutation(multi_objective=True, + initial_population=initial_population, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + + # assert result == True + +def test_adaptive_mutation_fitness_batch_size_1_mutation_num_genes_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=1, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_2_mutation_num_genes_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=2, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_3_mutation_num_genes_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=3, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_4_mutation_num_genes_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=4, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_5_mutation_num_genes_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=5, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_6_mutation_num_genes_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=6, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_7_mutation_num_genes_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=7, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_8_mutation_num_genes_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=8, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_9_mutation_num_genes_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True, + fitness_batch_size=9, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + +def test_adaptive_mutation_fitness_batch_size_10_mutation_num_genes_multi_objective_parallel_processing_processes(): + result, ga_instance = output_adaptive_mutation(multi_objective=True,fitness_batch_size=10, + mutation_num_genes=[6, 4], + parallel_processing=['process', 4]) + + +if __name__ == "__main__": + #### Single-objective mutation_probability + print() + test_adaptive_mutation_mutation_probability() + print() + + test_adaptive_mutation_int_gene_type_mutation_probability() + print() + + test_adaptive_mutation_gene_space_mutation_probability() + print() + + test_adaptive_mutation_gene_space_gene_type_mutation_probability() + print() + + test_adaptive_mutation_nested_gene_space_mutation_probability() + print() + + test_adaptive_mutation_nested_gene_type_mutation_probability() + print() + + test_adaptive_mutation_initial_population_mutation_probability() + print() + + test_adaptive_mutation_initial_population_nested_gene_type_mutation_probability() + print() + + test_adaptive_mutation_fitness_batch_size_1_mutation_probability() + print() + + test_adaptive_mutation_fitness_batch_size_1_mutation_probability() + print() + + test_adaptive_mutation_fitness_batch_size_2_mutation_probability() + print() + + test_adaptive_mutation_fitness_batch_size_3_mutation_probability() + print() + + test_adaptive_mutation_fitness_batch_size_4_mutation_probability() + print() + + test_adaptive_mutation_fitness_batch_size_5_mutation_probability() + print() + + test_adaptive_mutation_fitness_batch_size_6_mutation_probability() + print() + + test_adaptive_mutation_fitness_batch_size_7_mutation_probability() + print() + + test_adaptive_mutation_fitness_batch_size_8_mutation_probability() + print() + + test_adaptive_mutation_fitness_batch_size_9_mutation_probability() + print() + + test_adaptive_mutation_fitness_batch_size_10_mutation_probability() + print() + + #### Single-objective mutation_num_genes + print() + test_adaptive_mutation_mutation_num_genes() + print() + + test_adaptive_mutation_int_gene_type_mutation_num_genes() + print() + + test_adaptive_mutation_gene_space_mutation_num_genes() + print() + + test_adaptive_mutation_gene_space_gene_type_mutation_num_genes() + print() + + test_adaptive_mutation_nested_gene_space_mutation_num_genes() + print() + + test_adaptive_mutation_nested_gene_type_mutation_num_genes() + print() + + test_adaptive_mutation_initial_population_mutation_num_genes() + print() + + test_adaptive_mutation_initial_population_nested_gene_type_mutation_num_genes() + print() + + test_adaptive_mutation_fitness_batch_size_1_mutation_num_genes() + print() + + test_adaptive_mutation_fitness_batch_size_1_mutation_num_genes() + print() + + test_adaptive_mutation_fitness_batch_size_2_mutation_num_genes() + print() + + test_adaptive_mutation_fitness_batch_size_3_mutation_num_genes() + print() + + test_adaptive_mutation_fitness_batch_size_4_mutation_num_genes() + print() + + test_adaptive_mutation_fitness_batch_size_5_mutation_num_genes() + print() + + test_adaptive_mutation_fitness_batch_size_6_mutation_num_genes() + print() + + test_adaptive_mutation_fitness_batch_size_7_mutation_num_genes() + print() + + test_adaptive_mutation_fitness_batch_size_8_mutation_num_genes() + print() + + test_adaptive_mutation_fitness_batch_size_9_mutation_num_genes() + print() + + test_adaptive_mutation_fitness_batch_size_10_mutation_num_genes() + print() + + #### Multi-objective mutation_probability + print() + test_adaptive_mutation_mutation_probability() + print() + + test_adaptive_mutation_int_gene_type_mutation_probability() + print() + + test_adaptive_mutation_gene_space_mutation_probability() + print() + + test_adaptive_mutation_gene_space_gene_type_mutation_probability() + print() + + test_adaptive_mutation_nested_gene_space_mutation_probability() + print() + + test_adaptive_mutation_nested_gene_type_mutation_probability() + print() + + test_adaptive_mutation_initial_population_mutation_probability() + print() + + test_adaptive_mutation_initial_population_nested_gene_type_mutation_probability() + print() + + test_adaptive_mutation_fitness_batch_size_1_mutation_probability() + print() + + test_adaptive_mutation_fitness_batch_size_1_mutation_probability() + print() + + test_adaptive_mutation_fitness_batch_size_2_mutation_probability() + print() + + test_adaptive_mutation_fitness_batch_size_3_mutation_probability() + print() + + test_adaptive_mutation_fitness_batch_size_4_mutation_probability() + print() + + test_adaptive_mutation_fitness_batch_size_5_mutation_probability() + print() + + test_adaptive_mutation_fitness_batch_size_6_mutation_probability() + print() + + test_adaptive_mutation_fitness_batch_size_7_mutation_probability() + print() + + test_adaptive_mutation_fitness_batch_size_8_mutation_probability() + print() + + test_adaptive_mutation_fitness_batch_size_9_mutation_probability() + print() + + test_adaptive_mutation_fitness_batch_size_10_mutation_probability() + print() + + + + ######## Parallel Processing + #### #### Threads + #### Single-objective mutation_probability + print() + test_adaptive_mutation_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_int_gene_type_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_gene_space_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_gene_space_gene_type_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_nested_gene_space_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_nested_gene_type_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_initial_population_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_initial_population_nested_gene_type_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_1_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_1_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_2_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_3_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_4_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_5_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_6_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_7_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_8_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_9_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_10_mutation_probability_parallel_processing_threads() + print() + + #### Single-objective mutation_num_genes + print() + test_adaptive_mutation_mutation_num_genes_parallel_processing_threads() + print() + + test_adaptive_mutation_int_gene_type_mutation_num_genes_parallel_processing_threads() + print() + + test_adaptive_mutation_gene_space_mutation_num_genes_parallel_processing_threads() + print() + + test_adaptive_mutation_gene_space_gene_type_mutation_num_genes_parallel_processing_threads() + print() + + test_adaptive_mutation_nested_gene_space_mutation_num_genes_parallel_processing_threads() + print() + + test_adaptive_mutation_nested_gene_type_mutation_num_genes_parallel_processing_threads() + print() + + test_adaptive_mutation_initial_population_mutation_num_genes_parallel_processing_threads() + print() + + test_adaptive_mutation_initial_population_nested_gene_type_mutation_num_genes_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_1_mutation_num_genes_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_1_mutation_num_genes_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_2_mutation_num_genes_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_3_mutation_num_genes_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_4_mutation_num_genes_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_5_mutation_num_genes_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_6_mutation_num_genes_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_7_mutation_num_genes_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_8_mutation_num_genes_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_9_mutation_num_genes_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_10_mutation_num_genes_parallel_processing_threads() + print() + + #### Multi-objective mutation_probability + print() + test_adaptive_mutation_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_int_gene_type_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_gene_space_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_gene_space_gene_type_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_nested_gene_space_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_nested_gene_type_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_initial_population_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_initial_population_nested_gene_type_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_1_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_1_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_2_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_3_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_4_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_5_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_6_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_7_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_8_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_9_mutation_probability_parallel_processing_threads() + print() + + test_adaptive_mutation_fitness_batch_size_10_mutation_probability_parallel_processing_threads() + print() + + #### #### Processes + #### Single-objective mutation_probability + print() + test_adaptive_mutation_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_int_gene_type_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_gene_space_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_gene_space_gene_type_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_nested_gene_space_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_nested_gene_type_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_initial_population_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_initial_population_nested_gene_type_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_1_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_1_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_2_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_3_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_4_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_5_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_6_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_7_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_8_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_9_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_10_mutation_probability_parallel_processing_processes() + print() + + #### Single-objective mutation_num_genes + print() + test_adaptive_mutation_mutation_num_genes_parallel_processing_processes() + print() + + test_adaptive_mutation_int_gene_type_mutation_num_genes_parallel_processing_processes() + print() + + test_adaptive_mutation_gene_space_mutation_num_genes_parallel_processing_processes() + print() + + test_adaptive_mutation_gene_space_gene_type_mutation_num_genes_parallel_processing_processes() + print() + + test_adaptive_mutation_nested_gene_space_mutation_num_genes_parallel_processing_processes() + print() + + test_adaptive_mutation_nested_gene_type_mutation_num_genes_parallel_processing_processes() + print() + + test_adaptive_mutation_initial_population_mutation_num_genes_parallel_processing_processes() + print() + + test_adaptive_mutation_initial_population_nested_gene_type_mutation_num_genes_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_1_mutation_num_genes_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_1_mutation_num_genes_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_2_mutation_num_genes_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_3_mutation_num_genes_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_4_mutation_num_genes_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_5_mutation_num_genes_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_6_mutation_num_genes_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_7_mutation_num_genes_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_8_mutation_num_genes_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_9_mutation_num_genes_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_10_mutation_num_genes_parallel_processing_processes() + print() + + #### Multi-objective mutation_probability + print() + test_adaptive_mutation_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_int_gene_type_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_gene_space_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_gene_space_gene_type_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_nested_gene_space_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_nested_gene_type_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_initial_population_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_initial_population_nested_gene_type_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_1_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_1_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_2_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_3_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_4_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_5_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_6_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_7_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_8_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_9_mutation_probability_parallel_processing_processes() + print() + + test_adaptive_mutation_fitness_batch_size_10_mutation_probability_parallel_processing_processes() + print() + + diff --git a/tests/test_allow_duplicate_genes.py b/tests/test_allow_duplicate_genes.py new file mode 100644 index 00000000..63037f3f --- /dev/null +++ b/tests/test_allow_duplicate_genes.py @@ -0,0 +1,634 @@ +import pygad +import random +import numpy + +num_generations = 1 + +initial_population = [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]] + +def number_duplicate_genes(gene_space=None, + gene_type=float, + num_genes=10, + mutation_by_replacement=False, + random_mutation_min_val=-1, + random_mutation_max_val=1, + init_range_low=-4, + init_range_high=4, + random_seed=123, + initial_population=None, + parent_selection_type='sss', + multi_objective=False): + + def fitness_func_no_batch_single(ga, solution, idx): + return random.random() + + def fitness_func_no_batch_multi(ga, solution, idx): + return [random.random(), random.random()] + + if multi_objective == True: + fitness_func = fitness_func_no_batch_multi + else: + fitness_func = fitness_func_no_batch_single + + ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=5, + fitness_func=fitness_func, + sol_per_pop=10, + num_genes=num_genes, + gene_space=gene_space, + gene_type=gene_type, + initial_population=initial_population, + parent_selection_type=parent_selection_type, + init_range_low=init_range_low, + init_range_high=init_range_high, + random_mutation_min_val=random_mutation_min_val, + random_mutation_max_val=random_mutation_max_val, + allow_duplicate_genes=False, + mutation_by_replacement=mutation_by_replacement, + random_seed=random_seed, + save_solutions=True, + suppress_warnings=True) + + ga_instance.run() + + num_duplicates = 0 + for solution in ga_instance.solutions: + num = len(solution) - len(set(solution)) + if num != 0: + print(solution) + num_duplicates += num + + print(f"Number of duplicates is {num_duplicates}.") + return num_duplicates + +#### Single-Objective +def test_number_duplicates_default(): + num_duplicates = number_duplicate_genes() + + assert num_duplicates == 0 + +def test_number_duplicates_default_initial_population(): + num_duplicates = number_duplicate_genes(initial_population=initial_population) + + assert num_duplicates == 0 + +def test_number_duplicates_float_gene_type(): + num_genes = 10 + num_duplicates = number_duplicate_genes(gene_type=float, + num_genes=num_genes, + init_range_low=0, + init_range_high=1, + random_mutation_min_val=0, + random_mutation_max_val=1) + + assert num_duplicates == 0 + +def test_number_duplicates_float_gene_type_initial_population(): + num_genes = 10 + num_duplicates = number_duplicate_genes(gene_type=float, + num_genes=num_genes, + init_range_low=0, + init_range_high=1, + initial_population=initial_population, + random_mutation_min_val=0, + random_mutation_max_val=1) + + assert num_duplicates == 0 + +def test_number_duplicates_int_gene_type(): + num_genes = 10 + init_range_low = 0 + init_range_high = init_range_low + num_genes + random_mutation_min_val = 0 + random_mutation_max_val = random_mutation_min_val + num_genes + num_duplicates = number_duplicate_genes(gene_type=int, + mutation_by_replacement=False, + num_genes=num_genes, + init_range_low=init_range_low, + init_range_high=init_range_high, + random_mutation_min_val=random_mutation_min_val, + random_mutation_max_val=random_mutation_max_val) + + assert num_duplicates == 0 + +def test_number_duplicates_int_gene_type_initial_population(): + num_genes = 10 + init_range_low = 0 + init_range_high = init_range_low + num_genes + random_mutation_min_val = 0 + random_mutation_max_val = random_mutation_min_val + num_genes + num_duplicates = number_duplicate_genes(gene_type=int, + mutation_by_replacement=False, + num_genes=num_genes, + init_range_low=init_range_low, + init_range_high=init_range_high, + initial_population=initial_population, + random_mutation_min_val=random_mutation_min_val, + random_mutation_max_val=random_mutation_max_val) + + assert num_duplicates == 0 + +def test_number_duplicates_int_gene_type_replacement(): + num_genes = 10 + init_range_low = 0 + init_range_high = init_range_low + num_genes + random_mutation_min_val = 0 + random_mutation_max_val = random_mutation_min_val + num_genes + num_duplicates = number_duplicate_genes(gene_type=int, + mutation_by_replacement=True, + num_genes=num_genes, + init_range_low=init_range_low, + init_range_high=init_range_high, + random_mutation_min_val=random_mutation_min_val, + random_mutation_max_val=random_mutation_max_val) + + assert num_duplicates == 0 + +def test_number_duplicates_int_gene_type_replacement_initial_population(): + num_genes = 10 + init_range_low = 0 + init_range_high = init_range_low + num_genes + random_mutation_min_val = 0 + random_mutation_max_val = random_mutation_min_val + num_genes + num_duplicates = number_duplicate_genes(gene_type=int, + mutation_by_replacement=True, + num_genes=num_genes, + init_range_low=init_range_low, + init_range_high=init_range_high, + initial_population=initial_population, + random_mutation_min_val=random_mutation_min_val, + random_mutation_max_val=random_mutation_max_val) + + assert num_duplicates == 0 + +def test_number_duplicates_single_gene_space(): + num_duplicates = number_duplicate_genes(gene_space=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + num_genes=10) + + assert num_duplicates == 0 + +def test_number_duplicates_single_gene_space_initial_population(): + num_duplicates = number_duplicate_genes(gene_space=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + num_genes=10, + initial_population=initial_population) + + assert num_duplicates == 0 + +def test_number_duplicates_single_range_gene_space(): + num_genes = 10 + num_duplicates = number_duplicate_genes(gene_space=range(num_genes), + num_genes=num_genes) + + assert num_duplicates == 0 + +def test_number_duplicates_single_range_gene_space_initial_population(): + num_genes = 10 + num_duplicates = number_duplicate_genes(gene_space=range(num_genes), + num_genes=num_genes, + initial_population=initial_population) + + assert num_duplicates == 0 + +def test_number_duplicates_single_numpy_range_gene_space(): + num_genes = 10 + num_duplicates = number_duplicate_genes(gene_space=numpy.arange(num_genes), + num_genes=num_genes) + + assert num_duplicates == 0 + +def test_number_duplicates_single_numpy_range_gene_space_initial_population(): + num_genes = 10 + num_duplicates = number_duplicate_genes(gene_space=numpy.arange(num_genes), + num_genes=num_genes, + initial_population=initial_population) + + assert num_duplicates == 0 + +def test_number_duplicates_nested_gene_space(): + num_duplicates = number_duplicate_genes(gene_space=[[0, 1], + [1, 2], + [2, 3], + [3, 4], + [4, 5], + [5, 6], + [6, 7], + [7, 8], + [8, 9], + [9, 10]], + gene_type=int, + num_genes=10) + + assert num_duplicates == 0 + +def test_number_duplicates_nested_gene_space_initial_population(): + num_duplicates = number_duplicate_genes(gene_space=[[0, 1], + [1, 2], + [2, 3], + [3, 4], + [4, 5], + [5, 6], + [6, 7], + [7, 8], + [8, 9], + [9, 10]], + gene_type=int, + num_genes=10, + initial_population=initial_population) + + assert num_duplicates == 0 + + +# def test_number_duplicates_nested_gene_space_nested_gene_type(): + """ + This example causes duplicate genes that can only be solved by changing the values of a chain of genes. + Let's explain it using this solution: [0, 2, 3, 4, 5, 6, 6, 7, 8, 9] + It has 2 genes with the value 6 at indices 5 and 6. + According to the gene space, none of these genes can has a different value that solves the duplicates. + -If the value of the gene at index 5 is changed from 6 to 5, then it causes another duplicate with the gene at index 4. + -If the value of the gene at index 6 is changed from 6 to 7, then it causes another duplicate with the gene at index 7. + The solution is to change a chain of genes that make a room to solve the duplicates between the 2 genes. + 1) Change the second gene from 2 to 1. + 2) Change the third gene from 3 to 2. + 3) Change the fourth gene from 4 to 3. + 4) Change the fifth gene from 5 to 4. + 5) Change the sixth gene from 6 to 5. This solves the duplicates. + But this is NOT SUPPORTED yet. + We support changing only a single gene that makes a room to solve the duplicates. + + Let's explain it using this solution: [1, 2, 2, 4, 5, 6, 6, 7, 8, 9] + It has 2 genes with the value 2 at indices 1 and 2. + This is how the duplicates are solved: + 1) Change the first gene from 1 to 0. + 2) Change the second gene from 2 to 1. This solves the duplicates. + The result is [0, 1, 2, 4, 5, 6, 6, 7, 8, 9] + """ + # num_duplicates = number_duplicate_genes(gene_space=[[0, 1], + # [1, 2], + # [2, 3], + # [3, 4], + # [4, 5], + # [5, 6], + # [6, 7], + # [7, 8], + # [8, 9], + # [9, 10]], + # gene_type=[int, int, int, int, int, int, int, int, int, int], + # num_genes=10) + + # assert num_duplicates == 0 + +def test_number_duplicates_nested_gene_space_nested_gene_type_initial_population(): + num_duplicates = number_duplicate_genes(gene_space=[[0, 1], + [1, 2], + [2, 3], + [3, 4], + [4, 5], + [5, 6], + [6, 7], + [7, 8], + [8, 9], + [9, 10]], + gene_type=[int, int, int, int, int, int, int, int, int, int], + num_genes=10, + initial_population=initial_population) + + assert num_duplicates == 0 + +#### Multi-Objective +def test_number_duplicates_default_multi_objective(): + num_duplicates = number_duplicate_genes() + + assert num_duplicates == 0 + +def test_number_duplicates_default_initial_population_multi_objective(): + num_duplicates = number_duplicate_genes(initial_population=initial_population) + + assert num_duplicates == 0 + +def test_number_duplicates_float_gene_type_multi_objective(): + num_genes = 10 + num_duplicates = number_duplicate_genes(gene_type=float, + num_genes=num_genes, + init_range_low=0, + init_range_high=1, + random_mutation_min_val=0, + random_mutation_max_val=1) + + assert num_duplicates == 0 + +def test_number_duplicates_float_gene_type_initial_population_multi_objective(): + num_genes = 10 + num_duplicates = number_duplicate_genes(gene_type=float, + num_genes=num_genes, + init_range_low=0, + init_range_high=1, + initial_population=initial_population, + random_mutation_min_val=0, + random_mutation_max_val=1) + + assert num_duplicates == 0 + +def test_number_duplicates_int_gene_type_multi_objective(): + num_genes = 10 + init_range_low = 0 + init_range_high = init_range_low + num_genes + random_mutation_min_val = 0 + random_mutation_max_val = random_mutation_min_val + num_genes + num_duplicates = number_duplicate_genes(gene_type=int, + mutation_by_replacement=False, + num_genes=num_genes, + init_range_low=init_range_low, + init_range_high=init_range_high, + random_mutation_min_val=random_mutation_min_val, + random_mutation_max_val=random_mutation_max_val) + + assert num_duplicates == 0 + +def test_number_duplicates_int_gene_type_initial_population_multi_objective(): + num_genes = 10 + init_range_low = 0 + init_range_high = init_range_low + num_genes + random_mutation_min_val = 0 + random_mutation_max_val = random_mutation_min_val + num_genes + num_duplicates = number_duplicate_genes(gene_type=int, + mutation_by_replacement=False, + num_genes=num_genes, + init_range_low=init_range_low, + init_range_high=init_range_high, + initial_population=initial_population, + random_mutation_min_val=random_mutation_min_val, + random_mutation_max_val=random_mutation_max_val) + + assert num_duplicates == 0 + +def test_number_duplicates_int_gene_type_replacement_multi_objective(): + num_genes = 10 + init_range_low = 0 + init_range_high = init_range_low + num_genes + random_mutation_min_val = 0 + random_mutation_max_val = random_mutation_min_val + num_genes + num_duplicates = number_duplicate_genes(gene_type=int, + mutation_by_replacement=True, + num_genes=num_genes, + init_range_low=init_range_low, + init_range_high=init_range_high, + random_mutation_min_val=random_mutation_min_val, + random_mutation_max_val=random_mutation_max_val) + + assert num_duplicates == 0 + +def test_number_duplicates_int_gene_type_replacement_initial_population_multi_objective(): + num_genes = 10 + init_range_low = 0 + init_range_high = init_range_low + num_genes + random_mutation_min_val = 0 + random_mutation_max_val = random_mutation_min_val + num_genes + num_duplicates = number_duplicate_genes(gene_type=int, + mutation_by_replacement=True, + num_genes=num_genes, + init_range_low=init_range_low, + init_range_high=init_range_high, + initial_population=initial_population, + random_mutation_min_val=random_mutation_min_val, + random_mutation_max_val=random_mutation_max_val) + + assert num_duplicates == 0 + +def test_number_duplicates_single_gene_space_multi_objective(): + num_duplicates = number_duplicate_genes(gene_space=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + num_genes=10) + + assert num_duplicates == 0 + +def test_number_duplicates_single_gene_space_initial_population_multi_objective(): + num_duplicates = number_duplicate_genes(gene_space=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + num_genes=10, + initial_population=initial_population) + + assert num_duplicates == 0 + +def test_number_duplicates_single_range_gene_space_multi_objective(): + num_genes = 10 + num_duplicates = number_duplicate_genes(gene_space=range(num_genes), + num_genes=num_genes) + + assert num_duplicates == 0 + +def test_number_duplicates_single_range_gene_space_initial_population_multi_objective(): + num_genes = 10 + num_duplicates = number_duplicate_genes(gene_space=range(num_genes), + num_genes=num_genes, + initial_population=initial_population) + + assert num_duplicates == 0 + +def test_number_duplicates_single_numpy_range_gene_space_multi_objective(): + num_genes = 10 + num_duplicates = number_duplicate_genes(gene_space=numpy.arange(num_genes), + num_genes=num_genes) + + assert num_duplicates == 0 + +def test_number_duplicates_single_numpy_range_gene_space_initial_population_multi_objective(): + num_genes = 10 + num_duplicates = number_duplicate_genes(gene_space=numpy.arange(num_genes), + num_genes=num_genes, + initial_population=initial_population) + + assert num_duplicates == 0 + +def test_number_duplicates_nested_gene_space_multi_objective(): + num_duplicates = number_duplicate_genes(gene_space=[[0, 1], + [1, 2], + [2, 3], + [3, 4], + [4, 5], + [5, 6], + [6, 7], + [7, 8], + [8, 9], + [9, 10]], + gene_type=int, + num_genes=10) + + assert num_duplicates == 0 + +def test_number_duplicates_nested_gene_space_initial_population_multi_objective(): + num_duplicates = number_duplicate_genes(gene_space=[[0, 1], + [1, 2], + [2, 3], + [3, 4], + [4, 5], + [5, 6], + [6, 7], + [7, 8], + [8, 9], + [9, 10]], + gene_type=int, + num_genes=10, + initial_population=initial_population) + + assert num_duplicates == 0 + + +# def test_number_duplicates_nested_gene_space_nested_gene_type_multi_objective(): + """ + This example causes duplicate genes that can only be solved by changing the values of a chain of genes. + Let's explain it using this solution: [0, 2, 3, 4, 5, 6, 6, 7, 8, 9] + It has 2 genes with the value 6 at indices 5 and 6. + According to the gene space, none of these genes can has a different value that solves the duplicates. + -If the value of the gene at index 5 is changed from 6 to 5, then it causes another duplicate with the gene at index 4. + -If the value of the gene at index 6 is changed from 6 to 7, then it causes another duplicate with the gene at index 7. + The solution is to change a chain of genes that make a room to solve the duplicates between the 2 genes. + 1) Change the second gene from 2 to 1. + 2) Change the third gene from 3 to 2. + 3) Change the fourth gene from 4 to 3. + 4) Change the fifth gene from 5 to 4. + 5) Change the sixth gene from 6 to 5. This solves the duplicates. + But this is NOT SUPPORTED yet. + We support changing only a single gene that makes a room to solve the duplicates. + + Let's explain it using this solution: [1, 2, 2, 4, 5, 6, 6, 7, 8, 9] + It has 2 genes with the value 2 at indices 1 and 2. + This is how the duplicates are solved: + 1) Change the first gene from 1 to 0. + 2) Change the second gene from 2 to 1. This solves the duplicates. + The result is [0, 1, 2, 4, 5, 6, 6, 7, 8, 9] + """ + # num_duplicates = number_duplicate_genes(gene_space=[[0, 1], + # [1, 2], + # [2, 3], + # [3, 4], + # [4, 5], + # [5, 6], + # [6, 7], + # [7, 8], + # [8, 9], + # [9, 10]], + # gene_type=[int, int, int, int, int, int, int, int, int, int], + # num_genes=10) + + # assert num_duplicates == 0 + +def test_number_duplicates_nested_gene_space_nested_gene_type_initial_population_multi_objective(): + num_duplicates = number_duplicate_genes(gene_space=[[0, 1], + [1, 2], + [2, 3], + [3, 4], + [4, 5], + [5, 6], + [6, 7], + [7, 8], + [8, 9], + [9, 10]], + gene_type=[int, int, int, int, int, int, int, int, int, int], + num_genes=10, + initial_population=initial_population) + + assert num_duplicates == 0 + +if __name__ == "__main__": + #### Single-objective + print() + test_number_duplicates_default() + print() + test_number_duplicates_default_initial_population() + print() + + test_number_duplicates_float_gene_type() + print() + test_number_duplicates_float_gene_type_initial_population() + print() + + test_number_duplicates_int_gene_type() + print() + test_number_duplicates_int_gene_type_initial_population() + print() + + test_number_duplicates_int_gene_type_replacement() + print() + test_number_duplicates_int_gene_type_replacement_initial_population() + print() + + test_number_duplicates_single_gene_space() + print() + test_number_duplicates_single_gene_space_initial_population() + print() + + test_number_duplicates_single_range_gene_space() + print() + test_number_duplicates_single_range_gene_space_initial_population() + print() + + test_number_duplicates_single_numpy_range_gene_space() + print() + test_number_duplicates_single_numpy_range_gene_space_initial_population() + print() + + test_number_duplicates_nested_gene_space() + print() + test_number_duplicates_nested_gene_space_initial_population() + print() + + # This example causes duplicates that can only be solved by changing a chain of genes. + # test_number_duplicates_nested_gene_space_nested_gene_type() + # print() + test_number_duplicates_nested_gene_space_nested_gene_type_initial_population() + print() + + #### Multi-objective + print() + test_number_duplicates_default_initial_population_multi_objective() + print() + + test_number_duplicates_float_gene_type_multi_objective() + print() + test_number_duplicates_float_gene_type_initial_population_multi_objective() + print() + + test_number_duplicates_int_gene_type_multi_objective() + print() + test_number_duplicates_int_gene_type_initial_population_multi_objective() + print() + + test_number_duplicates_int_gene_type_replacement_multi_objective() + print() + test_number_duplicates_int_gene_type_replacement_initial_population_multi_objective() + print() + + test_number_duplicates_single_gene_space_multi_objective() + print() + test_number_duplicates_single_gene_space_initial_population_multi_objective() + print() + + test_number_duplicates_single_range_gene_space_multi_objective() + print() + test_number_duplicates_single_range_gene_space_initial_population_multi_objective() + print() + + test_number_duplicates_single_numpy_range_gene_space_multi_objective() + print() + test_number_duplicates_single_numpy_range_gene_space_initial_population_multi_objective() + print() + + test_number_duplicates_nested_gene_space_multi_objective() + print() + test_number_duplicates_nested_gene_space_initial_population_multi_objective() + print() + + # This example causes duplicates that can only be solved by changing a chain of genes. + # test_number_duplicates_nested_gene_space_nested_gene_type_multi_objective() + # print() + test_number_duplicates_nested_gene_space_nested_gene_type_initial_population_multi_objective() + print() + + diff --git a/tests/test_crossover_mutation.py b/tests/test_crossover_mutation.py new file mode 100644 index 00000000..acc38942 --- /dev/null +++ b/tests/test_crossover_mutation.py @@ -0,0 +1,287 @@ +import pygad +import random +import numpy + +num_generations = 1 + +initial_population = [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]] + +def output_crossover_mutation(gene_space=None, + gene_type=float, + num_genes=10, + mutation_by_replacement=False, + random_mutation_min_val=-1, + random_mutation_max_val=1, + init_range_low=-4, + init_range_high=4, + initial_population=None, + crossover_probability=None, + mutation_probability=None, + crossover_type=None, + mutation_type=None, + parent_selection_type='sss', + multi_objective=False): + + def fitness_func_no_batch_single(ga, solution, idx): + return random.random() + + def fitness_func_no_batch_multi(ga, solution, idx): + return [random.random(), random.random()] + + if multi_objective == True: + fitness_func = fitness_func_no_batch_multi + else: + fitness_func = fitness_func_no_batch_single + + ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=5, + fitness_func=fitness_func, + sol_per_pop=10, + num_genes=num_genes, + gene_space=gene_space, + gene_type=gene_type, + parent_selection_type=parent_selection_type, + initial_population=initial_population, + init_range_low=init_range_low, + init_range_high=init_range_high, + random_mutation_min_val=random_mutation_min_val, + random_mutation_max_val=random_mutation_max_val, + allow_duplicate_genes=True, + mutation_by_replacement=mutation_by_replacement, + save_solutions=True, + crossover_probability=crossover_probability, + mutation_probability=mutation_probability, + crossover_type=crossover_type, + mutation_type=mutation_type, + suppress_warnings=True, + random_seed=1) + + ga_instance.run() + + comparison_result = [] + for solution_idx, solution in enumerate(ga_instance.population): + if list(solution) in ga_instance.initial_population.tolist(): + comparison_result.append(True) + else: + comparison_result.append(False) + + comparison_result = numpy.array(comparison_result) + result = numpy.all(comparison_result == True) + + print(f"Comparison result is {result}") + return result, ga_instance + +def test_no_crossover_no_mutation(): + result, ga_instance = output_crossover_mutation() + + assert result == True + +def test_no_crossover_no_mutation_gene_space(): + result, ga_instance = output_crossover_mutation(gene_space=range(10)) + + assert result == True + +def test_no_crossover_no_mutation_int_gene_type(): + result, ga_instance = output_crossover_mutation(gene_type=int) + + assert result == True + + +def test_no_crossover_no_mutation_gene_space_gene_type(): + result, ga_instance = output_crossover_mutation(gene_space={"low": 0, "high": 10}, + gene_type=[float, 2]) + + assert result == True + + +def test_no_crossover_no_mutation_nested_gene_space(): + result, ga_instance = output_crossover_mutation(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]]) + assert result == True + +def test_no_crossover_no_mutation_nested_gene_type(): + result, ga_instance = output_crossover_mutation(gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + + assert result == True + +def test_no_crossover_no_mutation_nested_gene_space_nested_gene_type(): + result, ga_instance = output_crossover_mutation(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + + assert result == True + +def test_no_crossover_no_mutation_initial_population(): + global initial_population + result, ga_instance = output_crossover_mutation(initial_population=initial_population) + + assert result == True + +def test_no_crossover_no_mutation_initial_population_nested_gene_type(): + global initial_population + result, ga_instance = output_crossover_mutation(initial_population=initial_population, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + + assert result == True + +def test_crossover_no_mutation_zero_crossover_probability(): + global initial_population + result, ga_instance = output_crossover_mutation(crossover_type="single_point", + crossover_probability=0.0) + + assert result == True + +def test_zero_crossover_probability_zero_mutation_probability(): + global initial_population + result, ga_instance = output_crossover_mutation(crossover_type="single_point", + crossover_probability=0.0, + mutation_type="random", + mutation_probability=0.0) + + assert result == True + +def test_random_mutation_manual_call(): + result, ga_instance = output_crossover_mutation(mutation_type="random", + random_mutation_min_val=888, + random_mutation_max_val=999) + ga_instance.mutation_num_genes = 9 + + temp_offspring = numpy.array(initial_population[0:1]) + offspring = ga_instance.random_mutation(offspring=temp_offspring.copy()) + + comp = offspring - temp_offspring + comp_sorted = sorted(comp.copy()) + comp_sorted = numpy.abs(numpy.unique(comp_sorted)) + + # The other 1 added to include the last value in the range. + assert len(comp_sorted) in range(1, 1 + 1 + ga_instance.mutation_num_genes) + assert comp_sorted[0] == 0 + +def test_random_mutation_manual_call2(): + result, ga_instance = output_crossover_mutation(mutation_type="random", + random_mutation_min_val=888, + random_mutation_max_val=999) + ga_instance.mutation_num_genes = 10 + + temp_offspring = numpy.array(initial_population[0:1]) + offspring = ga_instance.random_mutation(offspring=temp_offspring.copy()) + + comp = offspring - temp_offspring + comp_sorted = sorted(comp.copy()) + comp_sorted = numpy.abs(numpy.unique(comp_sorted)) + + # The other 1 added to include the last value in the range. + assert len(comp_sorted) in range(1, 1 + 1 + ga_instance.mutation_num_genes) + # assert comp_sorted[0] == 0 + +def test_random_mutation_manual_call3(): + # Use random_mutation_min_val & random_mutation_max_val as numbers. + random_mutation_min_val = 888 + random_mutation_max_val = 999 + result, ga_instance = output_crossover_mutation(mutation_type="random", + random_mutation_min_val=random_mutation_min_val, + random_mutation_max_val=random_mutation_max_val, + mutation_by_replacement=True) + ga_instance.mutation_num_genes = 10 + + temp_offspring = numpy.array(initial_population[0:1]) + offspring = ga_instance.random_mutation(offspring=temp_offspring.copy()) + + comp = offspring + comp_sorted = sorted(comp.copy()) + comp_sorted = numpy.abs(numpy.unique(comp)) + + value_space = list(range(random_mutation_min_val, random_mutation_max_val)) + for value in comp_sorted: + assert value in value_space + +def test_random_mutation_manual_call4(): + # Use random_mutation_min_val & random_mutation_max_val as lists. + random_mutation_min_val = [888]*10 + random_mutation_max_val = [999]*10 + result, ga_instance = output_crossover_mutation(mutation_type="random", + random_mutation_min_val=random_mutation_min_val, + random_mutation_max_val=random_mutation_max_val, + mutation_by_replacement=True) + ga_instance.mutation_num_genes = 10 + + temp_offspring = numpy.array(initial_population[0:1]) + offspring = ga_instance.random_mutation(offspring=temp_offspring.copy()) + + comp = offspring + comp_sorted = sorted(comp.copy()) + comp_sorted = numpy.abs(numpy.unique(comp)) + + value_space = list(range(random_mutation_min_val[0], random_mutation_max_val[0])) + for value in comp_sorted: + assert value in value_space + +if __name__ == "__main__": + #### Single-objective + print() + test_no_crossover_no_mutation() + print() + + test_no_crossover_no_mutation_int_gene_type() + print() + + test_no_crossover_no_mutation_gene_space() + print() + + test_no_crossover_no_mutation_gene_space_gene_type() + print() + + test_no_crossover_no_mutation_nested_gene_space() + print() + + test_no_crossover_no_mutation_nested_gene_type() + print() + + test_no_crossover_no_mutation_initial_population() + print() + + test_no_crossover_no_mutation_initial_population_nested_gene_type() + print() + + test_crossover_no_mutation_zero_crossover_probability() + print() + + test_zero_crossover_probability_zero_mutation_probability() + print() + + test_random_mutation_manual_call() + print() + + test_random_mutation_manual_call2() + print() + + test_random_mutation_manual_call3() + print() + + test_random_mutation_manual_call4() + print() diff --git a/tests/test_gene_space.py b/tests/test_gene_space.py new file mode 100644 index 00000000..b633880b --- /dev/null +++ b/tests/test_gene_space.py @@ -0,0 +1,1605 @@ +""" +This script is identical to the test_gene_space_allow_duplicate_genes.py script except for: + Setting allow_duplicate_genes=True instead of False. +""" + +import pygad +import random +import numpy + +num_generations = 100 + +initial_population = [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]] + +# Test single gene space with nested gene type. + +def number_respect_gene_space(gene_space=None, + gene_type=float, + num_genes=10, + mutation_by_replacement=False, + random_mutation_min_val=-1, + random_mutation_max_val=1, + init_range_low=-4, + init_range_high=4, + mutation_type="random", + mutation_percent_genes="default", + mutation_probability=None, + initial_population=None, + parent_selection_type='sss', + multi_objective=False): + + def fitness_func_no_batch_single(ga, solution, idx): + return random.random() + + def fitness_func_no_batch_multi(ga, solution, idx): + return [random.random(), random.random()] + + if multi_objective == True: + fitness_func = fitness_func_no_batch_multi + else: + fitness_func = fitness_func_no_batch_single + + ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=5, + fitness_func=fitness_func, + sol_per_pop=10, + num_genes=num_genes, + gene_space=gene_space, + gene_type=gene_type, + initial_population=initial_population, + init_range_low=init_range_low, + init_range_high=init_range_high, + random_mutation_min_val=random_mutation_min_val, + random_mutation_max_val=random_mutation_max_val, + allow_duplicate_genes=True, + mutation_by_replacement=mutation_by_replacement, + parent_selection_type=parent_selection_type, + save_solutions=True, + mutation_type=mutation_type, + mutation_percent_genes=mutation_percent_genes, + mutation_probability=mutation_probability, + suppress_warnings=True, + random_seed=2) + + ga_instance.run() + ga_instance.solutions = numpy.array(ga_instance.solutions, + dtype=object) + + # gene_space_unpacked = ga_instance.unpack_gene_space(num_values_from_inf_range=100) + num_outside = 0 + if ga_instance.gene_space_nested == True: + for gene_idx in range(ga_instance.num_genes): + + if type(ga_instance.init_range_low) in ga_instance.supported_int_float_types: + range_min_init = ga_instance.init_range_low + range_max_init = ga_instance.init_range_high + else: + range_min_init = ga_instance.init_range_low[gene_idx] + range_max_init = ga_instance.init_range_high[gene_idx] + if type(ga_instance.random_mutation_min_val) in ga_instance.supported_int_float_types: + range_min_mutation = ga_instance.random_mutation_min_val + range_max_mutation = ga_instance.random_mutation_max_val + else: + range_min_mutation = ga_instance.random_mutation_min_val[gene_idx] + range_max_mutation = ga_instance.random_mutation_max_val[gene_idx] + + all_gene_values = ga_instance.solutions[:, gene_idx] + if type(ga_instance.gene_space[gene_idx]) in [list, tuple, range, numpy.ndarray]: + current_gene_space = list(ga_instance.gene_space[gene_idx]) + # print("current_gene_space", current_gene_space) + for val_idx, val in enumerate(all_gene_values): + if None in current_gene_space: + if (val in current_gene_space) or (val >= range_min_init and val < range_max_init) or (val >= range_min_mutation and val < range_max_mutation): + pass + else: + # print("###########") + # print(gene_idx, val) + # print(current_gene_space) + # print(range_min_mutation, range_max_mutation) + # print("\n\n") + num_outside += 1 + elif val in current_gene_space: + # print("val, current_gene_space", val, current_gene_space) + pass + else: + # print(gene_idx, val, current_gene_space) + num_outside += 1 + elif type(ga_instance.gene_space[gene_idx]) is dict: + if not "step" in ga_instance.gene_space[gene_idx].keys(): + for val in all_gene_values: + if val >= ga_instance.gene_space[gene_idx]["low"] and val < ga_instance.gene_space[gene_idx]["high"]: + pass + else: + print(gene_idx, val, current_gene_space) + num_outside += 1 + else: + gene_space_values = numpy.arange(ga_instance.gene_space[gene_idx]["low"], + ga_instance.gene_space[gene_idx]["high"], + ga_instance.gene_space[gene_idx]["step"]) + for val in all_gene_values: + if val in gene_space_values: + pass + else: + num_outside += 1 + elif type(ga_instance.gene_space[gene_idx]) in ga_instance.supported_int_float_types: + for val in all_gene_values: + if val == ga_instance.gene_space[gene_idx]: + pass + else: + num_outside += 1 + elif ga_instance.gene_space[gene_idx] is None: + for val in all_gene_values: + # print(val) + if (val >= range_min_init and val < range_max_init) or (val >= range_min_mutation and val < range_max_mutation): + pass + else: + # print("###########") + # print(gene_idx, val) + # print(ga_instance.gene_space[gene_idx]) + # print(range_min_init, range_max_init) + # print(range_min_mutation, range_max_mutation) + # print("\n\n") + num_outside += 1 + else: + for gene_idx in range(ga_instance.num_genes): + + if type(ga_instance.init_range_low) in ga_instance.supported_int_float_types: + range_min_init = ga_instance.init_range_low + range_max_init = ga_instance.init_range_high + else: + range_min_init = ga_instance.init_range_low[gene_idx] + range_max_init = ga_instance.init_range_high[gene_idx] + if type(ga_instance.random_mutation_min_val) in ga_instance.supported_int_float_types: + range_min_mutation = ga_instance.random_mutation_min_val + range_max_mutation = ga_instance.random_mutation_max_val + else: + range_min_mutation = ga_instance.random_mutation_min_val[gene_idx] + range_max_mutation = ga_instance.random_mutation_max_val[gene_idx] + + all_gene_values = ga_instance.solutions[:, gene_idx] + # print("all_gene_values", gene_idx, all_gene_values) + if type(ga_instance.gene_space) in [list, tuple, range, numpy.ndarray]: + current_gene_space = list(ga_instance.gene_space) + for val in all_gene_values: + if None in current_gene_space: + if (val in current_gene_space) or (val >= range_min_init and val < range_max_init) or (val >= range_min_mutation and val < range_max_mutation): + pass + else: + # print("###########") + # print(gene_idx, val) + # print(current_gene_space) + # print(range_min_mutation, range_max_mutation) + # print("\n\n") + num_outside += 1 + elif val in current_gene_space: + pass + else: + num_outside += 1 + elif type(ga_instance.gene_space) is dict: + if not "step" in ga_instance.gene_space.keys(): + for val in all_gene_values: + if val >= ga_instance.gene_space["low"] and val < ga_instance.gene_space["high"]: + pass + else: + num_outside += 1 + else: + gene_space_values = numpy.arange(ga_instance.gene_space["low"], + ga_instance.gene_space["high"], + ga_instance.gene_space["step"]) + for val in all_gene_values: + if val in gene_space_values: + pass + else: + num_outside += 1 + + print(f"Number of outside range is {num_outside}.") + return num_outside, ga_instance + +#### Single-Objective +def test_gene_space_range(): + num_outside, _ = number_respect_gene_space(gene_space=range(10)) + + assert num_outside == 0 + +def test_gene_space_numpy_arange(): + num_outside, _ = number_respect_gene_space(gene_space=numpy.arange(10)) + + assert num_outside == 0 + +def test_gene_space_list(): + num_outside, _ = number_respect_gene_space(gene_space=list(range(10))) + + assert num_outside == 0 + +def test_gene_space_list_None(): + num_outside, _ = number_respect_gene_space(gene_space=[30, None, 40, 50, None, 60, 70, None, None, None]) + + assert num_outside == 0 + +def test_gene_space_numpy(): + num_outside, _ = number_respect_gene_space(gene_space=numpy.array(list(range(10)))) + + assert num_outside == 0 + +def test_gene_space_dict_without_step(): + num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10}) + + assert num_outside == 0 + +def test_gene_space_dict_with_step(): + num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10, "step": 2}) + + assert num_outside == 0 + +def test_gene_space_list_single_value(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[5]) + + assert num_outside == 0 + +def test_gene_space_range_nested_gene_type(): + num_outside, _ = number_respect_gene_space(gene_space=range(10), + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + + assert num_outside == 0 + +def test_gene_space_numpy_arange_nested_gene_type(): + num_outside, _ = number_respect_gene_space(gene_space=numpy.arange(10), + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + + assert num_outside == 0 + +def test_gene_space_list_nested_gene_type(): + num_outside, _ = number_respect_gene_space(gene_space=list(range(10)), + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + + assert num_outside == 0 + +def test_gene_space_numpy_nested_gene_type(): + num_outside, _ = number_respect_gene_space(gene_space=numpy.array(list(range(10))), + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + + assert num_outside == 0 + +def test_gene_space_dict_without_step_nested_gene_type(): + num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10}, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + + assert num_outside == 0 + +def test_gene_space_dict_with_step_nested_gene_type(): + num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10, "step": 2}, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + + assert num_outside == 0 + +def test_gene_space_list_single_value_nested_gene_type(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[5], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + + assert num_outside == 0 + +def test_nested_gene_space_range(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[range(0, 10), + range(10, 20), + range(20, 30), + range(30, 40), + range(40, 50), + range(50, 60), + range(60, 70), + range(70, 80), + range(80, 90), + range(90, 100)]) + + assert num_outside == 0 + +def test_nested_gene_space_dict_without_step(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[{"low": 0, "high": 10}, + {"low": 10, "high": 20}, + {"low": 20, "high": 30}, + {"low": 30, "high": 40}, + {"low": 40, "high": 50}, + {"low": 50, "high": 60}, + {"low": 60, "high": 70}, + {"low": 70, "high": 80}, + {"low": 80, "high": 90}, + {"low": 90, "high": 100}]) + + assert num_outside == 0 + +def test_nested_gene_space_dict_without_step_float_gene_type(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[{"low": 0, "high": 10}, + {"low": 10, "high": 20}, + {"low": 20, "high": 30}, + {"low": 30, "high": 40}, + {"low": 40, "high": 50}, + {"low": 50, "high": 60}, + {"low": 60, "high": 70}, + {"low": 70, "high": 80}, + {"low": 80, "high": 90}, + {"low": 90, "high": 100}], + gene_type=[float, 3]) + + assert num_outside == 0 + +def test_nested_gene_space_dict_with_step(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[{"low": 0, "high": 10, "step": 1}, + {"low": 10, "high": 20, "step": 1.5}, + {"low": 20, "high": 30, "step": 2}, + {"low": 30, "high": 40, "step": 2.5}, + {"low": 40, "high": 50, "step": 3}, + {"low": 50, "high": 60, "step": 3.5}, + {"low": 60, "high": 70, "step": 4}, + {"low": 70, "high": 80, "step": 4.5}, + {"low": 80, "high": 90, "step": 5}, + {"low": 90, "high": 100, "step": 5.5}]) + + assert num_outside == 0 + + +def test_nested_gene_space_numpy_arange(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[numpy.arange(0, 10), + numpy.arange(10, 20), + numpy.arange(20, 30), + numpy.arange(30, 40), + numpy.arange(40, 50), + numpy.arange(50, 60), + numpy.arange(60, 70), + numpy.arange(70, 80), + numpy.arange(80, 90), + numpy.arange(90, 100)]) + + assert num_outside == 0 + +def test_nested_gene_space_list(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [-10, 10, 20, 30, 40, 50, 60, 70, 80, 90], + [-11, 11, 22, 33, 44, 55, 66, 77, 88, 99], + [-100, 100, 200, 300, 400, 500, 600, 700, 800, 900], + [-4.1, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9], + [-5.1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9], + [-10.5, 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7, 10.8, 10.9], + [-15, 15, 25, 35, 45, 55, 65, 75, 85, 95], + [30, 31, 32, 33, 34, 35, 36, 37, 38, 39], + [40, 41, 42, 43, 44, 45, 46, 47, 48, 49]]) + + assert num_outside == 0 + +def test_nested_gene_space_list2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1], + [1, 2], + [2, 3], + [3, 4], + [4, 5], + [5, 6], + [6, 7], + [7, 8], + [8, 9], + [9, 10]]) + + assert num_outside == 0 + +def test_nested_gene_space_list3_None(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, None], + [1, 2], + [2, None], + [3, 4], + [None, 5], + None, + [None, 7], + [None, None], + [8, 9], + None], + mutation_by_replacement=True) + + assert num_outside == 0 + +def test_nested_gene_space_list4_None_custom_mutation_range(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, None], + [1, 2], + [2, None], + [3, 4], + [None, 5], + None, + [None, 7], + [None, None], + [8, 9], + None], + random_mutation_min_val=20, + random_mutation_max_val=40, + mutation_by_replacement=True) + + assert num_outside == 0 + +def test_nested_gene_space_mix(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=int, + mutation_by_replacement=True) + + assert num_outside == 0 + +def test_nested_gene_space_mix_nested_gene_type(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=[int, float, numpy.float64, [float, 3], int, numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + + assert num_outside == 0 + +def test_nested_gene_space_mix_initial_population(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + numpy.arange(0, 10), + range(0, 10), + {"low": 0, "high": 10}, + {"low": 00, "high": 10, "step": 1}, + range(0, 10), + numpy.arange(0, 10), + numpy.arange(0, 10), + {"low": 0, "high": 10}, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + + assert num_outside == 0 + +def test_nested_gene_space_mix_initial_population_single_gene_type(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + numpy.arange(0, 10), + range(0, 10), + {"low": 0, "high": 10}, + {"low": 0, "high": 10}, + range(0, 10), + numpy.arange(0, 10), + numpy.arange(0, 10), + {"low": 0, "high": 10}, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + gene_type=[float, 4]) + + assert num_outside == 0 + +def test_nested_gene_space_single_gene_type_adaptive_mutation(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + numpy.arange(0, 10), + range(0, 10), + {"low": 0, "high": 10}, + {"low": 0, "high": 10}, + range(0, 10), + numpy.arange(0, 10), + numpy.arange(0, 10), + {"low": 0, "high": 10}, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + # Due to rounding the genes, a gene at index 4 will have a value of 10 (outside the dict range) if [float, 2] is used. + gene_type=[float, 4], + mutation_percent_genes=[70, 50], + mutation_type="adaptive") + + assert num_outside == 0 + +def test_nested_gene_space_single_gene_type_adaptive_mutation_probability(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + numpy.arange(0, 10), + range(0, 10), + {"low": 0, "high": 10}, + {"low": 0, "high": 10}, + range(0, 10), + numpy.arange(0, 10), + numpy.arange(0, 10), + {"low": 0, "high": 10}, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + # Due to rounding the genes, a gene at index 4 will have a value of 10 (outside the dict range) if [float, 2] is used. + gene_type=[float, 4], + mutation_probability=[0.7, 0.5], + mutation_type="adaptive") + + assert num_outside == 0 + +def test_nested_gene_space_nested_gene_type_adaptive_mutation(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + numpy.arange(0, 10), + range(0, 10), + {"low": 0, "high": 10}, + {"low": 0, "high": 10}, + range(0, 10), + numpy.arange(0, 10), + numpy.arange(0, 10), + {"low": 0, "high": 10}, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_percent_genes=[70, 50], + mutation_type="adaptive") + + assert num_outside == 0 + +def test_nested_gene_space_nested_gene_type_adaptive_mutation_probability(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + numpy.arange(0, 10), + range(0, 10), + {"low": 0, "high": 10}, + {"low": 0, "high": 10}, + range(0, 10), + numpy.arange(0, 10), + numpy.arange(0, 10), + {"low": 0, "high": 10}, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_probability=[0.7, 0.5], + mutation_type="adaptive") + assert num_outside == 0 + +#### Multi-Objective +def test_gene_space_range_multi_objective(): + num_outside, _ = number_respect_gene_space(gene_space=range(10), + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_numpy_arange_multi_objective(): + num_outside, _ = number_respect_gene_space(gene_space=numpy.arange(10), + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_list_multi_objective(): + num_outside, _ = number_respect_gene_space(gene_space=list(range(10)), + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_list_None_multi_objective(): + num_outside, _ = number_respect_gene_space(gene_space=[30, None, 40, 50, None, 60, 70, None, None, None], + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_numpy_multi_objective(): + num_outside, _ = number_respect_gene_space(gene_space=numpy.array(list(range(10))), + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_dict_without_step_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10}, + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_dict_with_step_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10, "step": 2}, + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_list_single_value_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[5], + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_range_nested_gene_type_multi_objective(): + num_outside, _ = number_respect_gene_space(gene_space=range(10), + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_numpy_arange_nested_gene_type_multi_objective(): + num_outside, _ = number_respect_gene_space(gene_space=numpy.arange(10), + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_list_nested_gene_type_multi_objective(): + num_outside, _ = number_respect_gene_space(gene_space=list(range(10)), + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_numpy_nested_gene_type_multi_objective(): + num_outside, _ = number_respect_gene_space(gene_space=numpy.array(list(range(10))), + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_dict_without_step_nested_gene_type_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10}, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_dict_with_step_nested_gene_type_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10, "step": 2}, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_list_single_value_nested_gene_type_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[5], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_range_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[range(0, 10), + range(10, 20), + range(20, 30), + range(30, 40), + range(40, 50), + range(50, 60), + range(60, 70), + range(70, 80), + range(80, 90), + range(90, 100)], + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_dict_without_step_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[{"low": 0, "high": 10}, + {"low": 10, "high": 20}, + {"low": 20, "high": 30}, + {"low": 30, "high": 40}, + {"low": 40, "high": 50}, + {"low": 50, "high": 60}, + {"low": 60, "high": 70}, + {"low": 70, "high": 80}, + {"low": 80, "high": 90}, + {"low": 90, "high": 100}], + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_dict_without_step_float_gene_type_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[{"low": 0, "high": 10}, + {"low": 10, "high": 20}, + {"low": 20, "high": 30}, + {"low": 30, "high": 40}, + {"low": 40, "high": 50}, + {"low": 50, "high": 60}, + {"low": 60, "high": 70}, + {"low": 70, "high": 80}, + {"low": 80, "high": 90}, + {"low": 90, "high": 100}], + gene_type=[float, 3], + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_dict_with_step_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[{"low": 0, "high": 10, "step": 1}, + {"low": 10, "high": 20, "step": 1.5}, + {"low": 20, "high": 30, "step": 2}, + {"low": 30, "high": 40, "step": 2.5}, + {"low": 40, "high": 50, "step": 3}, + {"low": 50, "high": 60, "step": 3.5}, + {"low": 60, "high": 70, "step": 4}, + {"low": 70, "high": 80, "step": 4.5}, + {"low": 80, "high": 90, "step": 5}, + {"low": 90, "high": 100, "step": 5.5}], + multi_objective=True) + + assert num_outside == 0 + + +def test_nested_gene_space_numpy_arange_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[numpy.arange(0, 10), + numpy.arange(10, 20), + numpy.arange(20, 30), + numpy.arange(30, 40), + numpy.arange(40, 50), + numpy.arange(50, 60), + numpy.arange(60, 70), + numpy.arange(70, 80), + numpy.arange(80, 90), + numpy.arange(90, 100)], + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_list_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [-10, 10, 20, 30, 40, 50, 60, 70, 80, 90], + [-11, 11, 22, 33, 44, 55, 66, 77, 88, 99], + [-100, 100, 200, 300, 400, 500, 600, 700, 800, 900], + [-4.1, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9], + [-5.1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9], + [-10.5, 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7, 10.8, 10.9], + [-15, 15, 25, 35, 45, 55, 65, 75, 85, 95], + [30, 31, 32, 33, 34, 35, 36, 37, 38, 39], + [40, 41, 42, 43, 44, 45, 46, 47, 48, 49]], + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_list2_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1], + [1, 2], + [2, 3], + [3, 4], + [4, 5], + [5, 6], + [6, 7], + [7, 8], + [8, 9], + [9, 10]], + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_list3_None_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, None], + [1, 2], + [2, None], + [3, 4], + [None, 5], + None, + [None, 7], + [None, None], + [8, 9], + None], + mutation_by_replacement=True, + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_list4_None_custom_mutation_range_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, None], + [1, 2], + [2, None], + [3, 4], + [None, 5], + None, + [None, 7], + [None, None], + [8, 9], + None], + random_mutation_min_val=20, + random_mutation_max_val=40, + mutation_by_replacement=True, + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_mix_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=int, + mutation_by_replacement=True, + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_mix_nested_gene_type_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=[int, float, numpy.float64, [float, 3], int, numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_mix_initial_population_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + numpy.arange(0, 10), + range(0, 10), + {"low": 0, "high": 10}, + {"low": 00, "high": 10, "step": 1}, + range(0, 10), + numpy.arange(0, 10), + numpy.arange(0, 10), + {"low": 0, "high": 10}, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_mix_initial_population_single_gene_type_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + numpy.arange(0, 10), + range(0, 10), + {"low": 0, "high": 10}, + {"low": 0, "high": 10}, + range(0, 10), + numpy.arange(0, 10), + numpy.arange(0, 10), + {"low": 0, "high": 10}, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + gene_type=[float, 4], + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_single_gene_type_adaptive_mutation_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + numpy.arange(0, 10), + range(0, 10), + {"low": 0, "high": 10}, + {"low": 0, "high": 10}, + range(0, 10), + numpy.arange(0, 10), + numpy.arange(0, 10), + {"low": 0, "high": 10}, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + # Due to rounding the genes, a gene at index 4 will have a value of 10 (outside the dict range) if [float, 2] is used. + gene_type=[float, 4], + mutation_percent_genes=[70, 50], + mutation_type="adaptive", + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_single_gene_type_adaptive_mutation_probability_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + numpy.arange(0, 10), + range(0, 10), + {"low": 0, "high": 10}, + {"low": 0, "high": 10}, + range(0, 10), + numpy.arange(0, 10), + numpy.arange(0, 10), + {"low": 0, "high": 10}, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + # Due to rounding the genes, a gene at index 4 will have a value of 10 (outside the dict range) if [float, 2] is used. + gene_type=[float, 4], + mutation_probability=[0.7, 0.5], + mutation_type="adaptive", + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_nested_gene_type_adaptive_mutation_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + numpy.arange(0, 10), + range(0, 10), + {"low": 0, "high": 10}, + {"low": 0, "high": 10}, + range(0, 10), + numpy.arange(0, 10), + numpy.arange(0, 10), + {"low": 0, "high": 10}, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_percent_genes=[70, 50], + mutation_type="adaptive", + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_nested_gene_type_adaptive_mutation_probability_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + numpy.arange(0, 10), + range(0, 10), + {"low": 0, "high": 10}, + {"low": 0, "high": 10}, + range(0, 10), + numpy.arange(0, 10), + numpy.arange(0, 10), + {"low": 0, "high": 10}, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_probability=[0.7, 0.5], + mutation_type="adaptive", + multi_objective=True) + assert num_outside == 0 + +#### Multi-Objective NSGA-II Parent Selection +def test_gene_space_range_multi_objective_nsga2(): + num_outside, _ = number_respect_gene_space(gene_space=range(10), + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_numpy_arange_multi_objective_nsga2(): + num_outside, _ = number_respect_gene_space(gene_space=numpy.arange(10), + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_list_multi_objective_nsga2(): + num_outside, _ = number_respect_gene_space(gene_space=list(range(10)), + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_list_None_multi_objective_nsga2(): + num_outside, _ = number_respect_gene_space(gene_space=[30, None, 40, 50, None, 60, 70, None, None, None], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_numpy_multi_objective_nsga2(): + num_outside, _ = number_respect_gene_space(gene_space=numpy.array(list(range(10))), + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_dict_without_step_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10}, + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_dict_with_step_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10, "step": 2}, + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_list_single_value_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[5], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_range_nested_gene_type_multi_objective_nsga2(): + num_outside, _ = number_respect_gene_space(gene_space=range(10), + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_numpy_arange_nested_gene_type_multi_objective_nsga2(): + num_outside, _ = number_respect_gene_space(gene_space=numpy.arange(10), + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_list_nested_gene_type_multi_objective_nsga2(): + num_outside, _ = number_respect_gene_space(gene_space=list(range(10)), + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_numpy_nested_gene_type_multi_objective_nsga2(): + num_outside, _ = number_respect_gene_space(gene_space=numpy.array(list(range(10))), + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_dict_without_step_nested_gene_type_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10}, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_dict_with_step_nested_gene_type_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10, "step": 2}, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_list_single_value_nested_gene_type_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[5], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_range_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[range(0, 10), + range(10, 20), + range(20, 30), + range(30, 40), + range(40, 50), + range(50, 60), + range(60, 70), + range(70, 80), + range(80, 90), + range(90, 100)], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_dict_without_step_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[{"low": 0, "high": 10}, + {"low": 10, "high": 20}, + {"low": 20, "high": 30}, + {"low": 30, "high": 40}, + {"low": 40, "high": 50}, + {"low": 50, "high": 60}, + {"low": 60, "high": 70}, + {"low": 70, "high": 80}, + {"low": 80, "high": 90}, + {"low": 90, "high": 100}], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_dict_without_step_float_gene_type_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[{"low": 0, "high": 10}, + {"low": 10, "high": 20}, + {"low": 20, "high": 30}, + {"low": 30, "high": 40}, + {"low": 40, "high": 50}, + {"low": 50, "high": 60}, + {"low": 60, "high": 70}, + {"low": 70, "high": 80}, + {"low": 80, "high": 90}, + {"low": 90, "high": 100}], + gene_type=[float, 3], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_dict_with_step_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[{"low": 0, "high": 10, "step": 1}, + {"low": 10, "high": 20, "step": 1.5}, + {"low": 20, "high": 30, "step": 2}, + {"low": 30, "high": 40, "step": 2.5}, + {"low": 40, "high": 50, "step": 3}, + {"low": 50, "high": 60, "step": 3.5}, + {"low": 60, "high": 70, "step": 4}, + {"low": 70, "high": 80, "step": 4.5}, + {"low": 80, "high": 90, "step": 5}, + {"low": 90, "high": 100, "step": 5.5}], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + + +def test_nested_gene_space_numpy_arange_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[numpy.arange(0, 10), + numpy.arange(10, 20), + numpy.arange(20, 30), + numpy.arange(30, 40), + numpy.arange(40, 50), + numpy.arange(50, 60), + numpy.arange(60, 70), + numpy.arange(70, 80), + numpy.arange(80, 90), + numpy.arange(90, 100)], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_list_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [-10, 10, 20, 30, 40, 50, 60, 70, 80, 90], + [-11, 11, 22, 33, 44, 55, 66, 77, 88, 99], + [-100, 100, 200, 300, 400, 500, 600, 700, 800, 900], + [-4.1, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9], + [-5.1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9], + [-10.5, 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7, 10.8, 10.9], + [-15, 15, 25, 35, 45, 55, 65, 75, 85, 95], + [30, 31, 32, 33, 34, 35, 36, 37, 38, 39], + [40, 41, 42, 43, 44, 45, 46, 47, 48, 49]], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_list2_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1], + [1, 2], + [2, 3], + [3, 4], + [4, 5], + [5, 6], + [6, 7], + [7, 8], + [8, 9], + [9, 10]], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_list3_None_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, None], + [1, 2], + [2, None], + [3, 4], + [None, 5], + None, + [None, 7], + [None, None], + [8, 9], + None], + mutation_by_replacement=True, + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_list4_None_custom_mutation_range_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, None], + [1, 2], + [2, None], + [3, 4], + [None, 5], + None, + [None, 7], + [None, None], + [8, 9], + None], + random_mutation_min_val=20, + random_mutation_max_val=40, + mutation_by_replacement=True, + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_mix_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=int, + mutation_by_replacement=True, + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_mix_nested_gene_type_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=[int, float, numpy.float64, [float, 3], int, numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_mix_initial_population_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + numpy.arange(0, 10), + range(0, 10), + {"low": 0, "high": 10}, + {"low": 00, "high": 10, "step": 1}, + range(0, 10), + numpy.arange(0, 10), + numpy.arange(0, 10), + {"low": 0, "high": 10}, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_mix_initial_population_single_gene_type_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + numpy.arange(0, 10), + range(0, 10), + {"low": 0, "high": 10}, + {"low": 0, "high": 10}, + range(0, 10), + numpy.arange(0, 10), + numpy.arange(0, 10), + {"low": 0, "high": 10}, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + gene_type=[float, 4], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_single_gene_type_adaptive_mutation_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + numpy.arange(0, 10), + range(0, 10), + {"low": 0, "high": 10}, + {"low": 0, "high": 10}, + range(0, 10), + numpy.arange(0, 10), + numpy.arange(0, 10), + {"low": 0, "high": 10}, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + # Due to rounding the genes, a gene at index 4 will have a value of 10 (outside the dict range) if [float, 2] is used. + gene_type=[float, 4], + mutation_percent_genes=[70, 50], + mutation_type="adaptive", + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_single_gene_type_adaptive_mutation_probability_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + numpy.arange(0, 10), + range(0, 10), + {"low": 0, "high": 10}, + {"low": 0, "high": 10}, + range(0, 10), + numpy.arange(0, 10), + numpy.arange(0, 10), + {"low": 0, "high": 10}, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + # Due to rounding the genes, a gene at index 4 will have a value of 10 (outside the dict range) if [float, 2] is used. + gene_type=[float, 4], + mutation_probability=[0.7, 0.5], + mutation_type="adaptive", + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_nested_gene_type_adaptive_mutation_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + numpy.arange(0, 10), + range(0, 10), + {"low": 0, "high": 10}, + {"low": 0, "high": 10}, + range(0, 10), + numpy.arange(0, 10), + numpy.arange(0, 10), + {"low": 0, "high": 10}, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_percent_genes=[70, 50], + mutation_type="adaptive", + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_nested_gene_type_adaptive_mutation_probability_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + numpy.arange(0, 10), + range(0, 10), + {"low": 0, "high": 10}, + {"low": 0, "high": 10}, + range(0, 10), + numpy.arange(0, 10), + numpy.arange(0, 10), + {"low": 0, "high": 10}, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + mutation_probability=[0.7, 0.5], + mutation_type="adaptive", + multi_objective=True, + parent_selection_type='nsga2') + assert num_outside == 0 + + + + +if __name__ == "__main__": + #### Single-objective + print() + test_gene_space_range() + print() + test_gene_space_range_nested_gene_type() + print() + + test_gene_space_numpy_arange() + print() + test_gene_space_numpy_arange_nested_gene_type() + print() + + test_gene_space_list() + print() + test_gene_space_list_None() + print() + test_gene_space_list_nested_gene_type() + print() + + test_gene_space_list_single_value() + print() + test_gene_space_list_single_value_nested_gene_type() + print() + + test_gene_space_numpy() + print() + test_gene_space_numpy_nested_gene_type() + print() + + test_gene_space_dict_without_step() + print() + test_gene_space_dict_without_step_nested_gene_type() + print() + + test_gene_space_dict_with_step() + print() + test_gene_space_dict_with_step_nested_gene_type() + print() + + test_nested_gene_space_range() + print() + + test_nested_gene_space_dict_without_step() + print() + + test_nested_gene_space_dict_without_step_float_gene_type() + print() + + test_nested_gene_space_dict_with_step() + print() + + test_nested_gene_space_numpy_arange() + print() + + test_nested_gene_space_list() + print() + + test_nested_gene_space_list2() + print() + + test_nested_gene_space_list3_None() + print() + + test_nested_gene_space_list4_None_custom_mutation_range() + print() + + test_nested_gene_space_mix() + print() + + test_nested_gene_space_mix_nested_gene_type() + print() + + test_nested_gene_space_mix_initial_population() + print() + + test_nested_gene_space_mix_initial_population_single_gene_type() + print() + + test_nested_gene_space_single_gene_type_adaptive_mutation() + print() + test_nested_gene_space_single_gene_type_adaptive_mutation_probability() + print() + + test_nested_gene_space_nested_gene_type_adaptive_mutation() + print() + test_nested_gene_space_nested_gene_type_adaptive_mutation_probability() + print() + + #### Multi-objective + print() + test_gene_space_range_multi_objective() + print() + test_gene_space_range_nested_gene_type_multi_objective() + print() + + test_gene_space_numpy_arange_multi_objective() + print() + test_gene_space_numpy_arange_nested_gene_type_multi_objective() + print() + + test_gene_space_list_multi_objective() + print() + test_gene_space_list_None_multi_objective() + print() + test_gene_space_list_nested_gene_type_multi_objective() + print() + + test_gene_space_list_single_value_multi_objective() + print() + test_gene_space_list_single_value_nested_gene_type_multi_objective() + print() + + test_gene_space_numpy_multi_objective() + print() + test_gene_space_numpy_nested_gene_type_multi_objective() + print() + + test_gene_space_dict_without_step_multi_objective() + print() + test_gene_space_dict_without_step_nested_gene_type_multi_objective() + print() + + test_gene_space_dict_with_step_multi_objective() + print() + test_gene_space_dict_with_step_nested_gene_type_multi_objective() + print() + + test_nested_gene_space_range_multi_objective() + print() + + test_nested_gene_space_dict_without_step_multi_objective() + print() + + test_nested_gene_space_dict_without_step_float_gene_type_multi_objective() + print() + + test_nested_gene_space_dict_with_step_multi_objective() + print() + + test_nested_gene_space_numpy_arange_multi_objective() + print() + + test_nested_gene_space_list_multi_objective() + print() + + test_nested_gene_space_list2_multi_objective() + print() + + test_nested_gene_space_list3_None_multi_objective() + print() + + test_nested_gene_space_list4_None_custom_mutation_range_multi_objective() + print() + + test_nested_gene_space_mix_multi_objective() + print() + + test_nested_gene_space_mix_nested_gene_type_multi_objective() + print() + + test_nested_gene_space_mix_initial_population_multi_objective() + print() + + test_nested_gene_space_mix_initial_population_single_gene_type_multi_objective() + print() + + test_nested_gene_space_single_gene_type_adaptive_mutation_multi_objective() + print() + test_nested_gene_space_single_gene_type_adaptive_mutation_probability_multi_objective() + print() + + test_nested_gene_space_nested_gene_type_adaptive_mutation_multi_objective() + print() + test_nested_gene_space_nested_gene_type_adaptive_mutation_probability_multi_objective() + print() + + + #### Multi-objective NSGA-II Parent Selection + print() + test_gene_space_range_multi_objective_nsga2() + print() + test_gene_space_range_nested_gene_type_multi_objective_nsga2() + print() + + test_gene_space_numpy_arange_multi_objective_nsga2() + print() + test_gene_space_numpy_arange_nested_gene_type_multi_objective_nsga2() + print() + + test_gene_space_list_multi_objective_nsga2() + print() + test_gene_space_list_None_multi_objective_nsga2() + print() + test_gene_space_list_nested_gene_type_multi_objective_nsga2() + print() + + test_gene_space_list_single_value_multi_objective_nsga2() + print() + test_gene_space_list_single_value_nested_gene_type_multi_objective_nsga2() + print() + + test_gene_space_numpy_multi_objective_nsga2() + print() + test_gene_space_numpy_nested_gene_type_multi_objective_nsga2() + print() + + test_gene_space_dict_without_step_multi_objective_nsga2() + print() + test_gene_space_dict_without_step_nested_gene_type_multi_objective_nsga2() + print() + + test_gene_space_dict_with_step_multi_objective_nsga2() + print() + test_gene_space_dict_with_step_nested_gene_type_multi_objective_nsga2() + print() + + test_nested_gene_space_range_multi_objective_nsga2() + print() + + test_nested_gene_space_dict_without_step_multi_objective_nsga2() + print() + + test_nested_gene_space_dict_without_step_float_gene_type_multi_objective_nsga2() + print() + + test_nested_gene_space_dict_with_step_multi_objective_nsga2() + print() + + test_nested_gene_space_numpy_arange_multi_objective_nsga2() + print() + + test_nested_gene_space_list_multi_objective_nsga2() + print() + + test_nested_gene_space_list2_multi_objective_nsga2() + print() + + test_nested_gene_space_list3_None_multi_objective_nsga2() + print() + + test_nested_gene_space_list4_None_custom_mutation_range_multi_objective_nsga2() + print() + + test_nested_gene_space_mix_multi_objective_nsga2() + print() + + test_nested_gene_space_mix_nested_gene_type_multi_objective_nsga2() + print() + + test_nested_gene_space_mix_initial_population_multi_objective_nsga2() + print() + + test_nested_gene_space_mix_initial_population_single_gene_type_multi_objective_nsga2() + print() + + test_nested_gene_space_single_gene_type_adaptive_mutation_multi_objective_nsga2() + print() + test_nested_gene_space_single_gene_type_adaptive_mutation_probability_multi_objective_nsga2() + print() + + test_nested_gene_space_nested_gene_type_adaptive_mutation_multi_objective_nsga2() + print() + test_nested_gene_space_nested_gene_type_adaptive_mutation_probability_multi_objective_nsga2() + print() diff --git a/tests/test_gene_space_allow_duplicate_genes.py b/tests/test_gene_space_allow_duplicate_genes.py new file mode 100644 index 00000000..c35f3886 --- /dev/null +++ b/tests/test_gene_space_allow_duplicate_genes.py @@ -0,0 +1,1139 @@ +""" +This script is identical to the test_gene_space.py script except for: + Setting allow_duplicate_genes=False instead of True. +""" + +import pygad +import random +import numpy + +num_generations = 100 + +initial_population = [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]] + +# Test single gene space with nested gene type. + +def number_respect_gene_space(gene_space=None, + gene_type=float, + num_genes=10, + mutation_by_replacement=False, + random_mutation_min_val=-1, + random_mutation_max_val=1, + init_range_low=-4, + init_range_high=4, + initial_population=None, + parent_selection_type='sss', + multi_objective=False): + + def fitness_func_no_batch_single(ga, solution, idx): + return random.random() + + def fitness_func_no_batch_multi(ga, solution, idx): + return [random.random(), random.random()] + + if multi_objective == True: + fitness_func = fitness_func_no_batch_multi + else: + fitness_func = fitness_func_no_batch_single + + ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=5, + fitness_func=fitness_func, + sol_per_pop=10, + parent_selection_type=parent_selection_type, + num_genes=num_genes, + gene_space=gene_space, + gene_type=gene_type, + initial_population=initial_population, + init_range_low=init_range_low, + init_range_high=init_range_high, + random_mutation_min_val=random_mutation_min_val, + random_mutation_max_val=random_mutation_max_val, + allow_duplicate_genes=False, + mutation_by_replacement=mutation_by_replacement, + save_solutions=True, + suppress_warnings=True, + random_seed=2) + + ga_instance.run() + ga_instance.solutions = numpy.array(ga_instance.solutions, + dtype=object) + + # gene_space_unpacked = ga_instance.unpack_gene_space(num_values_from_inf_range=100) + num_outside = 0 + if ga_instance.gene_space_nested == True: + for gene_idx in range(ga_instance.num_genes): + all_gene_values = ga_instance.solutions[:, gene_idx] + if type(ga_instance.gene_space[gene_idx]) in [list, tuple, range, numpy.ndarray]: + current_gene_space = list(ga_instance.gene_space[gene_idx]) + for val in all_gene_values: + if val in current_gene_space: + # print(val, current_gene_space) + pass + else: + # print(gene_idx, val, current_gene_space) + num_outside += 1 + elif type(ga_instance.gene_space[gene_idx]) is dict: + if not "step" in ga_instance.gene_space[gene_idx].keys(): + for val in all_gene_values: + if val >= ga_instance.gene_space[gene_idx]["low"] and val < ga_instance.gene_space[gene_idx]["high"]: + pass + else: + num_outside += 1 + else: + gene_space_values = numpy.arange(ga_instance.gene_space[gene_idx]["low"], + ga_instance.gene_space[gene_idx]["high"], + ga_instance.gene_space[gene_idx]["step"]) + for val in all_gene_values: + if val in gene_space_values: + pass + else: + num_outside += 1 + elif type(ga_instance.gene_space[gene_idx]) in ga_instance.supported_int_float_types: + for val in all_gene_values: + if val == ga_instance.gene_space[gene_idx]: + pass + else: + num_outside += 1 + else: + for gene_idx in range(ga_instance.num_genes): + all_gene_values = ga_instance.solutions[:, gene_idx] + # print("all_gene_values", gene_idx, all_gene_values) + if type(ga_instance.gene_space) in [list, tuple, range, numpy.ndarray]: + current_gene_space = list(ga_instance.gene_space) + for val in all_gene_values: + if val in current_gene_space: + pass + else: + num_outside += 1 + elif type(ga_instance.gene_space) is dict: + if not "step" in ga_instance.gene_space.keys(): + for val in all_gene_values: + if val >= ga_instance.gene_space["low"] and val < ga_instance.gene_space["high"]: + pass + else: + num_outside += 1 + else: + gene_space_values = numpy.arange(ga_instance.gene_space["low"], + ga_instance.gene_space["high"], + ga_instance.gene_space["step"]) + for val in all_gene_values: + if val in gene_space_values: + pass + else: + num_outside += 1 + + print(f"Number of outside range is {num_outside}.") + return num_outside, ga_instance + +#### Single-Objective +def test_gene_space_range(): + num_outside, _ = number_respect_gene_space(gene_space=range(10)) + + assert num_outside == 0 + +def test_gene_space_numpy_arange(): + num_outside, _ = number_respect_gene_space(gene_space=numpy.arange(10)) + + assert num_outside == 0 + +def test_gene_space_list(): + num_outside, _ = number_respect_gene_space(gene_space=list(range(10))) + + assert num_outside == 0 + +def test_gene_space_numpy(): + num_outside, _ = number_respect_gene_space(gene_space=numpy.array(list(range(10)))) + + assert num_outside == 0 + +def test_gene_space_dict_without_step(): + num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10}) + + assert num_outside == 0 + +def test_gene_space_dict_with_step(): + num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10, "step": 2}) + + assert num_outside == 0 + +def test_gene_space_list_single_value(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[5]) + + assert num_outside == 0 + +def test_gene_space_range_nested_gene_type(): + num_outside, _ = number_respect_gene_space(gene_space=range(10), + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + + assert num_outside == 0 + +def test_gene_space_numpy_arange_nested_gene_type(): + num_outside, _ = number_respect_gene_space(gene_space=numpy.arange(10), + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + + assert num_outside == 0 + +def test_gene_space_list_nested_gene_type(): + num_outside, _ = number_respect_gene_space(gene_space=list(range(10)), + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + + assert num_outside == 0 + +def test_gene_space_numpy_nested_gene_type(): + num_outside, _ = number_respect_gene_space(gene_space=numpy.array(list(range(10))), + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + + assert num_outside == 0 + +def test_gene_space_dict_without_step_nested_gene_type(): + num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10}, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + assert num_outside == 0 + +def test_gene_space_dict_with_step_nested_gene_type(): + num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10, "step": 2}, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + + assert num_outside == 0 + +def test_gene_space_list_single_value_nested_gene_type(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[5], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + + assert num_outside == 0 + +def test_nested_gene_space_range(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[range(0, 10), + range(10, 20), + range(20, 30), + range(30, 40), + range(40, 50), + range(50, 60), + range(60, 70), + range(70, 80), + range(80, 90), + range(90, 100)]) + + assert num_outside == 0 + +def test_nested_gene_space_dict_without_step(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[{"low": 0, "high": 10}, + {"low": 10, "high": 20}, + {"low": 20, "high": 30}, + {"low": 30, "high": 40}, + {"low": 40, "high": 50}, + {"low": 50, "high": 60}, + {"low": 60, "high": 70}, + {"low": 70, "high": 80}, + {"low": 80, "high": 90}, + {"low": 90, "high": 100}]) + + assert num_outside == 0 + +def test_nested_gene_space_dict_without_step_float_gene_type(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[{"low": 0, "high": 10}, + {"low": 10, "high": 20}, + {"low": 20, "high": 30}, + {"low": 30, "high": 40}, + {"low": 40, "high": 50}, + {"low": 50, "high": 60}, + {"low": 60, "high": 70}, + {"low": 70, "high": 80}, + {"low": 80, "high": 90}, + {"low": 90, "high": 100}], + gene_type=[float, 3]) + + assert num_outside == 0 + +def test_nested_gene_space_dict_with_step(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[{"low": 0, "high": 10, "step": 1}, + {"low": 10, "high": 20, "step": 1.5}, + {"low": 20, "high": 30, "step": 2}, + {"low": 30, "high": 40, "step": 2.5}, + {"low": 40, "high": 50, "step": 3}, + {"low": 50, "high": 60, "step": 3.5}, + {"low": 60, "high": 70, "step": 4}, + {"low": 70, "high": 80, "step": 4.5}, + {"low": 80, "high": 90, "step": 5}, + {"low": 90, "high": 100, "step": 5.5}]) + + assert num_outside == 0 + + +def test_nested_gene_space_numpy_arange(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[numpy.arange(0, 10), + numpy.arange(10, 20), + numpy.arange(20, 30), + numpy.arange(30, 40), + numpy.arange(40, 50), + numpy.arange(50, 60), + numpy.arange(60, 70), + numpy.arange(70, 80), + numpy.arange(80, 90), + numpy.arange(90, 100)]) + + assert num_outside == 0 + +def test_nested_gene_space_list(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [-10, 10, 20, 30, 40, 50, 60, 70, 80, 90], + [-11, 11, 22, 33, 44, 55, 66, 77, 88, 99], + [-100, 100, 200, 300, 400, 500, 600, 700, 800, 900], + [-4.1, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9], + [-5.1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9], + [-10.5, 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7, 10.8, 10.9], + [-15, 15, 25, 35, 45, 55, 65, 75, 85, 95], + [30, 31, 32, 33, 34, 35, 36, 37, 38, 39], + [40, 41, 42, 43, 44, 45, 46, 47, 48, 49]]) + + assert num_outside == 0 + +def test_nested_gene_space_list2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1], + [1, 2], + [2, 3], + [3, 4], + [4, 5], + [5, 6], + [6, 7], + [7, 8], + [8, 9], + [9, 10]]) + + assert num_outside == 0 + +def test_nested_gene_space_mix(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=int) + + assert num_outside == 0 + +def test_nested_gene_space_mix_nested_gene_type(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + + assert num_outside == 0 + +def test_nested_gene_space_mix_initial_population(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + numpy.arange(0, 10), + range(0, 10), + {"low": 0, "high": 10}, + {"low": 00, "high": 10, "step": 1}, + range(0, 10), + numpy.arange(0, 10), + numpy.arange(0, 10), + {"low": 0, "high": 10}, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + + assert num_outside == 0 + +def test_nested_gene_space_mix_initial_population_single_gene_type(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + numpy.arange(0, 10), + range(0, 10), + {"low": 0, "high": 10}, + {"low": 0, "high": 10}, + range(0, 10), + numpy.arange(0, 10), + numpy.arange(0, 10), + {"low": 0, "high": 10}, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + gene_type=[float, 4]) + + assert num_outside == 0 + +#### Multi-Objective +def test_gene_space_range_multi_objective(): + num_outside, _ = number_respect_gene_space(gene_space=range(10), + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_numpy_arange_multi_objective(): + num_outside, _ = number_respect_gene_space(gene_space=numpy.arange(10), + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_list_multi_objective(): + num_outside, _ = number_respect_gene_space(gene_space=list(range(10)), + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_numpy_multi_objective(): + num_outside, _ = number_respect_gene_space(gene_space=numpy.array(list(range(10))), + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_dict_without_step_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10}, + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_dict_with_step_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10, "step": 2}, + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_list_single_value_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[5], + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_range_nested_gene_type_multi_objective(): + num_outside, _ = number_respect_gene_space(gene_space=range(10), + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_numpy_arange_nested_gene_type_multi_objective(): + num_outside, _ = number_respect_gene_space(gene_space=numpy.arange(10), + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_list_nested_gene_type_multi_objective(): + num_outside, _ = number_respect_gene_space(gene_space=list(range(10)), + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_numpy_nested_gene_type_multi_objective(): + num_outside, _ = number_respect_gene_space(gene_space=numpy.array(list(range(10))), + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_dict_without_step_nested_gene_type_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10}, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True) + assert num_outside == 0 + +def test_gene_space_dict_with_step_nested_gene_type_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10, "step": 2}, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_list_single_value_nested_gene_type_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[5], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_range_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[range(0, 10), + range(10, 20), + range(20, 30), + range(30, 40), + range(40, 50), + range(50, 60), + range(60, 70), + range(70, 80), + range(80, 90), + range(90, 100)], + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_dict_without_step_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[{"low": 0, "high": 10}, + {"low": 10, "high": 20}, + {"low": 20, "high": 30}, + {"low": 30, "high": 40}, + {"low": 40, "high": 50}, + {"low": 50, "high": 60}, + {"low": 60, "high": 70}, + {"low": 70, "high": 80}, + {"low": 80, "high": 90}, + {"low": 90, "high": 100}], + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_dict_without_step_float_gene_type_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[{"low": 0, "high": 10}, + {"low": 10, "high": 20}, + {"low": 20, "high": 30}, + {"low": 30, "high": 40}, + {"low": 40, "high": 50}, + {"low": 50, "high": 60}, + {"low": 60, "high": 70}, + {"low": 70, "high": 80}, + {"low": 80, "high": 90}, + {"low": 90, "high": 100}], + gene_type=[float, 3], + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_dict_with_step_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[{"low": 0, "high": 10, "step": 1}, + {"low": 10, "high": 20, "step": 1.5}, + {"low": 20, "high": 30, "step": 2}, + {"low": 30, "high": 40, "step": 2.5}, + {"low": 40, "high": 50, "step": 3}, + {"low": 50, "high": 60, "step": 3.5}, + {"low": 60, "high": 70, "step": 4}, + {"low": 70, "high": 80, "step": 4.5}, + {"low": 80, "high": 90, "step": 5}, + {"low": 90, "high": 100, "step": 5.5}], + multi_objective=True) + + assert num_outside == 0 + + +def test_nested_gene_space_numpy_arange_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[numpy.arange(0, 10), + numpy.arange(10, 20), + numpy.arange(20, 30), + numpy.arange(30, 40), + numpy.arange(40, 50), + numpy.arange(50, 60), + numpy.arange(60, 70), + numpy.arange(70, 80), + numpy.arange(80, 90), + numpy.arange(90, 100)], + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_list_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [-10, 10, 20, 30, 40, 50, 60, 70, 80, 90], + [-11, 11, 22, 33, 44, 55, 66, 77, 88, 99], + [-100, 100, 200, 300, 400, 500, 600, 700, 800, 900], + [-4.1, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9], + [-5.1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9], + [-10.5, 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7, 10.8, 10.9], + [-15, 15, 25, 35, 45, 55, 65, 75, 85, 95], + [30, 31, 32, 33, 34, 35, 36, 37, 38, 39], + [40, 41, 42, 43, 44, 45, 46, 47, 48, 49]], + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_list2_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1], + [1, 2], + [2, 3], + [3, 4], + [4, 5], + [5, 6], + [6, 7], + [7, 8], + [8, 9], + [9, 10]], + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_mix_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=int, + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_mix_nested_gene_type_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_mix_initial_population_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + numpy.arange(0, 10), + range(0, 10), + {"low": 0, "high": 10}, + {"low": 00, "high": 10, "step": 1}, + range(0, 10), + numpy.arange(0, 10), + numpy.arange(0, 10), + {"low": 0, "high": 10}, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True) + + assert num_outside == 0 + +def test_nested_gene_space_mix_initial_population_single_gene_type_multi_objective(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + numpy.arange(0, 10), + range(0, 10), + {"low": 0, "high": 10}, + {"low": 0, "high": 10}, + range(0, 10), + numpy.arange(0, 10), + numpy.arange(0, 10), + {"low": 0, "high": 10}, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + gene_type=[float, 4], + multi_objective=True) + + assert num_outside == 0 + +#### Multi-Objective NSGA-II Parent Selection +def test_gene_space_range_multi_objective_nsga2(): + num_outside, _ = number_respect_gene_space(gene_space=range(10), + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_numpy_arange_multi_objective_nsga2(): + num_outside, _ = number_respect_gene_space(gene_space=numpy.arange(10), + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_list_multi_objective_nsga2(): + num_outside, _ = number_respect_gene_space(gene_space=list(range(10)), + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_numpy_multi_objective_nsga2(): + num_outside, _ = number_respect_gene_space(gene_space=numpy.array(list(range(10))), + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_dict_without_step_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10}, + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_dict_with_step_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10, "step": 2}, + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_list_single_value_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[5], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_range_nested_gene_type_multi_objective_nsga2(): + num_outside, _ = number_respect_gene_space(gene_space=range(10), + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True) + + assert num_outside == 0 + +def test_gene_space_numpy_arange_nested_gene_type_multi_objective_nsga2(): + num_outside, _ = number_respect_gene_space(gene_space=numpy.arange(10), + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_list_nested_gene_type_multi_objective_nsga2(): + num_outside, _ = number_respect_gene_space(gene_space=list(range(10)), + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_numpy_nested_gene_type_multi_objective_nsga2(): + num_outside, _ = number_respect_gene_space(gene_space=numpy.array(list(range(10))), + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_dict_without_step_nested_gene_type_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10}, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True, + parent_selection_type='nsga2') + assert num_outside == 0 + +def test_gene_space_dict_with_step_nested_gene_type_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10, "step": 2}, + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_gene_space_list_single_value_nested_gene_type_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[5], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_range_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[range(0, 10), + range(10, 20), + range(20, 30), + range(30, 40), + range(40, 50), + range(50, 60), + range(60, 70), + range(70, 80), + range(80, 90), + range(90, 100)], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_dict_without_step_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[{"low": 0, "high": 10}, + {"low": 10, "high": 20}, + {"low": 20, "high": 30}, + {"low": 30, "high": 40}, + {"low": 40, "high": 50}, + {"low": 50, "high": 60}, + {"low": 60, "high": 70}, + {"low": 70, "high": 80}, + {"low": 80, "high": 90}, + {"low": 90, "high": 100}], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_dict_without_step_float_gene_type_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[{"low": 0, "high": 10}, + {"low": 10, "high": 20}, + {"low": 20, "high": 30}, + {"low": 30, "high": 40}, + {"low": 40, "high": 50}, + {"low": 50, "high": 60}, + {"low": 60, "high": 70}, + {"low": 70, "high": 80}, + {"low": 80, "high": 90}, + {"low": 90, "high": 100}], + gene_type=[float, 3], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_dict_with_step_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[{"low": 0, "high": 10, "step": 1}, + {"low": 10, "high": 20, "step": 1.5}, + {"low": 20, "high": 30, "step": 2}, + {"low": 30, "high": 40, "step": 2.5}, + {"low": 40, "high": 50, "step": 3}, + {"low": 50, "high": 60, "step": 3.5}, + {"low": 60, "high": 70, "step": 4}, + {"low": 70, "high": 80, "step": 4.5}, + {"low": 80, "high": 90, "step": 5}, + {"low": 90, "high": 100, "step": 5.5}], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + + +def test_nested_gene_space_numpy_arange_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[numpy.arange(0, 10), + numpy.arange(10, 20), + numpy.arange(20, 30), + numpy.arange(30, 40), + numpy.arange(40, 50), + numpy.arange(50, 60), + numpy.arange(60, 70), + numpy.arange(70, 80), + numpy.arange(80, 90), + numpy.arange(90, 100)], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_list_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [-10, 10, 20, 30, 40, 50, 60, 70, 80, 90], + [-11, 11, 22, 33, 44, 55, 66, 77, 88, 99], + [-100, 100, 200, 300, 400, 500, 600, 700, 800, 900], + [-4.1, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9], + [-5.1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9], + [-10.5, 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7, 10.8, 10.9], + [-15, 15, 25, 35, 45, 55, 65, 75, 85, 95], + [30, 31, 32, 33, 34, 35, 36, 37, 38, 39], + [40, 41, 42, 43, 44, 45, 46, 47, 48, 49]], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_list2_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1], + [1, 2], + [2, 3], + [3, 4], + [4, 5], + [5, 6], + [6, 7], + [7, 8], + [8, 9], + [9, 10]], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_mix_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=int, + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_mix_nested_gene_type_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_mix_initial_population_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + numpy.arange(0, 10), + range(0, 10), + {"low": 0, "high": 10}, + {"low": 00, "high": 10, "step": 1}, + range(0, 10), + numpy.arange(0, 10), + numpy.arange(0, 10), + {"low": 0, "high": 10}, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + +def test_nested_gene_space_mix_initial_population_single_gene_type_multi_objective_nsga2(): + num_outside, ga_instance = number_respect_gene_space(gene_space=[[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + numpy.arange(0, 10), + range(0, 10), + {"low": 0, "high": 10}, + {"low": 0, "high": 10}, + range(0, 10), + numpy.arange(0, 10), + numpy.arange(0, 10), + {"low": 0, "high": 10}, + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]], + gene_type=[float, 4], + multi_objective=True, + parent_selection_type='nsga2') + + assert num_outside == 0 + + +if __name__ == "__main__": + #### Single-objective + print() + test_gene_space_range() + print() + test_gene_space_range_nested_gene_type() + print() + + test_gene_space_numpy_arange() + print() + test_gene_space_numpy_arange_nested_gene_type() + print() + + test_gene_space_list() + print() + test_gene_space_list_nested_gene_type() + print() + + test_gene_space_list_single_value() + print() + test_gene_space_list_single_value_nested_gene_type() + print() + + test_gene_space_numpy() + print() + test_gene_space_numpy_nested_gene_type() + print() + + test_gene_space_dict_without_step() + print() + test_gene_space_dict_without_step_nested_gene_type() + print() + + test_gene_space_dict_with_step() + print() + test_gene_space_dict_with_step_nested_gene_type() + print() + + test_nested_gene_space_range() + print() + + test_nested_gene_space_dict_without_step() + print() + + test_nested_gene_space_dict_without_step_float_gene_type() + print() + + test_nested_gene_space_dict_with_step() + print() + + test_nested_gene_space_numpy_arange() + print() + + test_nested_gene_space_list() + print() + + test_nested_gene_space_list2() + print() + + test_nested_gene_space_mix() + print() + + test_nested_gene_space_mix_nested_gene_type() + print() + + test_nested_gene_space_mix_initial_population() + print() + + test_nested_gene_space_mix_initial_population_single_gene_type() + print() + + + + #### Multi-objective + print() + test_gene_space_range_multi_objective() + print() + test_gene_space_range_nested_gene_type_multi_objective() + print() + + test_gene_space_numpy_arange_multi_objective() + print() + test_gene_space_numpy_arange_nested_gene_type_multi_objective() + print() + + test_gene_space_list_multi_objective() + print() + test_gene_space_list_nested_gene_type_multi_objective() + print() + + test_gene_space_list_single_value_multi_objective() + print() + test_gene_space_list_single_value_nested_gene_type_multi_objective() + print() + + test_gene_space_numpy_multi_objective() + print() + test_gene_space_numpy_nested_gene_type_multi_objective() + print() + + test_gene_space_dict_without_step_multi_objective() + print() + test_gene_space_dict_without_step_nested_gene_type_multi_objective() + print() + + test_gene_space_dict_with_step_multi_objective() + print() + test_gene_space_dict_with_step_nested_gene_type_multi_objective() + print() + + test_nested_gene_space_range_multi_objective() + print() + + test_nested_gene_space_dict_without_step_multi_objective() + print() + + test_nested_gene_space_dict_without_step_float_gene_type_multi_objective() + print() + + test_nested_gene_space_dict_with_step_multi_objective() + print() + + test_nested_gene_space_numpy_arange_multi_objective() + print() + + test_nested_gene_space_list_multi_objective() + print() + + test_nested_gene_space_list2_multi_objective() + print() + + test_nested_gene_space_mix_multi_objective() + print() + + test_nested_gene_space_mix_nested_gene_type_multi_objective() + print() + + test_nested_gene_space_mix_initial_population_multi_objective() + print() + + test_nested_gene_space_mix_initial_population_single_gene_type_multi_objective() + print() + + + #### Multi-objective NSGA-II Parent Selection + print() + test_gene_space_range_multi_objective_nsga2() + print() + test_gene_space_range_nested_gene_type_multi_objective_nsga2() + print() + + test_gene_space_numpy_arange_multi_objective_nsga2() + print() + test_gene_space_numpy_arange_nested_gene_type_multi_objective_nsga2() + print() + + test_gene_space_list_multi_objective_nsga2() + print() + test_gene_space_list_nested_gene_type_multi_objective_nsga2() + print() + + test_gene_space_list_single_value_multi_objective_nsga2() + print() + test_gene_space_list_single_value_nested_gene_type_multi_objective_nsga2() + print() + + test_gene_space_numpy_multi_objective_nsga2() + print() + test_gene_space_numpy_nested_gene_type_multi_objective_nsga2() + print() + + test_gene_space_dict_without_step_multi_objective_nsga2() + print() + test_gene_space_dict_without_step_nested_gene_type_multi_objective_nsga2() + print() + + test_gene_space_dict_with_step_multi_objective_nsga2() + + print() + test_gene_space_dict_with_step_nested_gene_type_multi_objective_nsga2() + print() + + test_nested_gene_space_range_multi_objective_nsga2() + print() + + test_nested_gene_space_dict_without_step_multi_objective_nsga2() + print() + + test_nested_gene_space_dict_without_step_float_gene_type_multi_objective_nsga2() + print() + + test_nested_gene_space_dict_with_step_multi_objective_nsga2() + print() + + test_nested_gene_space_numpy_arange_multi_objective_nsga2() + print() + + test_nested_gene_space_list_multi_objective_nsga2() + print() + + test_nested_gene_space_list2_multi_objective_nsga2() + print() + + test_nested_gene_space_mix_multi_objective_nsga2() + print() + + test_nested_gene_space_mix_nested_gene_type_multi_objective_nsga2() + print() + + test_nested_gene_space_mix_initial_population_multi_objective_nsga2() + print() + + test_nested_gene_space_mix_initial_population_single_gene_type_multi_objective_nsga2() + print() + + diff --git a/tests/test_lifecycle_callbacks_calls.py b/tests/test_lifecycle_callbacks_calls.py new file mode 100644 index 00000000..b3159e8b --- /dev/null +++ b/tests/test_lifecycle_callbacks_calls.py @@ -0,0 +1,247 @@ +import pygad +import random + +num_generations = 100 + +def number_lifecycle_callback_functions_calls(stop_criteria=None, + on_generation_stop=None, + crossover_type="single_point", + mutation_type="random"): + actual_num_callbacks_calls = 0 + + def fitness_func(ga_instanse, solution, solution_idx): + return random.random() + + def on_start(ga_instance): + nonlocal actual_num_callbacks_calls + actual_num_callbacks_calls = actual_num_callbacks_calls + 1 + + def on_fitness(ga_instance, population_fitness): + nonlocal actual_num_callbacks_calls + actual_num_callbacks_calls = actual_num_callbacks_calls + 1 + + def on_parents(ga_instance, selected_parents): + nonlocal actual_num_callbacks_calls + actual_num_callbacks_calls = actual_num_callbacks_calls + 1 + + def on_crossover(ga_instance, offspring_crossover): + nonlocal actual_num_callbacks_calls + actual_num_callbacks_calls = actual_num_callbacks_calls + 1 + + def on_mutation(ga_instance, offspring_mutation): + nonlocal actual_num_callbacks_calls + actual_num_callbacks_calls = actual_num_callbacks_calls + 1 + + def on_generation(ga_instance): + nonlocal actual_num_callbacks_calls + actual_num_callbacks_calls = actual_num_callbacks_calls + 1 + + if on_generation_stop: + if ga_instance.generations_completed == on_generation_stop: + return "stop" + + def on_stop(ga_instance, last_population_fitness): + nonlocal actual_num_callbacks_calls + actual_num_callbacks_calls = actual_num_callbacks_calls + 1 + + ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=5, + fitness_func=fitness_func, + sol_per_pop=10, + num_genes=5, + crossover_type=crossover_type, + mutation_type=mutation_type, + 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, + stop_criteria=stop_criteria, + suppress_warnings=True) + + ga_instance.run() + + # The total number is: + # 1 [for on_start()] + + # num_generations [for on_fitness()] + + # num_generations [for on_parents()] + + # num_generations [for on_crossover()] + + # num_generations [for on_mutation()] + + # num_generations [for on_generation()] + + # 1 [for on_stop()] + # = 1 + num_generations * 5 + 1 + + # Use 'generations_completed' instead of 'num_generations' because the evolution may stops in the on_generation() callback. + expected_num_callbacks_calls = 1 + ga_instance.generations_completed * 5 + 1 + + print(f"Expected {expected_num_callbacks_calls}.") + print(f"Actual {actual_num_callbacks_calls}.") + return actual_num_callbacks_calls, expected_num_callbacks_calls + +def number_lifecycle_callback_methods_calls(stop_criteria=None, + on_generation_stop=None, + crossover_type="single_point", + mutation_type="random"): + actual_num_callbacks_calls = 0 + + class Callbacks: + def fitness_func(self, ga_instanse, solution, solution_idx): + return 1 + + def on_start(self, ga_instance): + nonlocal actual_num_callbacks_calls + actual_num_callbacks_calls = actual_num_callbacks_calls + 1 + + def on_fitness(self, ga_instance, population_fitness): + nonlocal actual_num_callbacks_calls + actual_num_callbacks_calls = actual_num_callbacks_calls + 1 + + def on_parents(self, ga_instance, selected_parents): + nonlocal actual_num_callbacks_calls + actual_num_callbacks_calls = actual_num_callbacks_calls + 1 + + def on_crossover(self, ga_instance, offspring_crossover): + nonlocal actual_num_callbacks_calls + actual_num_callbacks_calls = actual_num_callbacks_calls + 1 + + def on_mutation(self, ga_instance, offspring_mutation): + nonlocal actual_num_callbacks_calls + actual_num_callbacks_calls = actual_num_callbacks_calls + 1 + + def on_generation(self, ga_instance): + nonlocal actual_num_callbacks_calls + actual_num_callbacks_calls = actual_num_callbacks_calls + 1 + + if on_generation_stop: + if ga_instance.generations_completed == on_generation_stop: + return "stop" + + def on_stop(self, ga_instance, last_population_fitness): + nonlocal actual_num_callbacks_calls + actual_num_callbacks_calls = actual_num_callbacks_calls + 1 + + Callbacks_obj = Callbacks() + ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=5, + fitness_func=Callbacks_obj.fitness_func, + sol_per_pop=10, + num_genes=5, + crossover_type=crossover_type, + mutation_type=mutation_type, + on_start=Callbacks_obj.on_start, + on_fitness=Callbacks_obj.on_fitness, + on_parents=Callbacks_obj.on_parents, + on_crossover=Callbacks_obj.on_crossover, + on_mutation=Callbacks_obj.on_mutation, + on_generation=Callbacks_obj.on_generation, + on_stop=Callbacks_obj.on_stop, + stop_criteria=stop_criteria, + suppress_warnings=True) + + ga_instance.run() + + # The total number is: + # 1 [for on_start()] + + # num_generations [for on_fitness()] + + # num_generations [for on_parents()] + + # num_generations [for on_crossover()] + + # num_generations [for on_mutation()] + + # num_generations [for on_generation()] + + # 1 [for on_stop()] + # = 1 + num_generations * 5 + 1 + + # Use 'generations_completed' instead of 'num_generations' because the evolution may stops in the on_generation() callback. + expected_num_callbacks_calls = 1 + ga_instance.generations_completed * 5 + 1 + + print(f"Expected {expected_num_callbacks_calls}.") + print(f"Actual {actual_num_callbacks_calls}.") + return actual_num_callbacks_calls, expected_num_callbacks_calls + +def test_number_lifecycle_callback_functions_calls(): + actual, expected = number_lifecycle_callback_functions_calls() + + assert actual == expected + +def test_number_lifecycle_callback_functions_calls_stop_criteria(): + actual, expected = number_lifecycle_callback_functions_calls(on_generation_stop=30) + + assert actual == expected + +def test_number_lifecycle_callback_methods_calls(): + actual, expected = number_lifecycle_callback_methods_calls() + + assert actual == expected + +def test_number_lifecycle_callback_methods_calls_stop_criteria(): + actual, expected = number_lifecycle_callback_methods_calls(on_generation_stop=30) + + assert actual == expected + +def test_number_lifecycle_callback_functions_calls_no_crossover(): + actual, expected = number_lifecycle_callback_functions_calls(crossover_type=None) + + assert actual == expected + +def test_number_lifecycle_callback_functions_calls_no_mutation(): + actual, expected = number_lifecycle_callback_functions_calls(mutation_type=None) + + assert actual == expected + +def test_number_lifecycle_callback_functions_calls_no_crossover_no_mutation(): + actual, expected = number_lifecycle_callback_functions_calls(crossover_type=None, + mutation_type=None) + + assert actual == expected + +def test_number_lifecycle_callback_methods_calls_no_crossover(): + actual, expected = number_lifecycle_callback_methods_calls(crossover_type=None) + + assert actual == expected + +def test_number_lifecycle_callback_methods_calls_no_mutation(): + actual, expected = number_lifecycle_callback_methods_calls(mutation_type=None) + + assert actual == expected + +def test_number_lifecycle_callback_methods_calls_no_crossover_no_mutation(): + actual, expected = number_lifecycle_callback_methods_calls(crossover_type=None, + mutation_type=None) + + assert actual == expected + +if __name__ == "__main__": + print() + test_number_lifecycle_callback_functions_calls() + print() + + test_number_lifecycle_callback_functions_calls_stop_criteria() + print() + + test_number_lifecycle_callback_methods_calls() + print() + + test_number_lifecycle_callback_methods_calls_stop_criteria() + print() + + test_number_lifecycle_callback_functions_calls_no_crossover() + print() + + test_number_lifecycle_callback_functions_calls_no_crossover() + print() + + test_number_lifecycle_callback_functions_calls_no_mutation() + print() + + test_number_lifecycle_callback_functions_calls_no_crossover_no_mutation() + print() + + test_number_lifecycle_callback_methods_calls_no_crossover() + print() + + test_number_lifecycle_callback_methods_calls_no_mutation() + print() + + test_number_lifecycle_callback_methods_calls_no_crossover_no_mutation() + print() diff --git a/tests/test_number_fitness_function_calls.py b/tests/test_number_fitness_function_calls.py new file mode 100644 index 00000000..916a88f6 --- /dev/null +++ b/tests/test_number_fitness_function_calls.py @@ -0,0 +1,597 @@ +import pygad +import random +import numpy + +actual_num_fitness_calls_default_keep = 0 +actual_num_fitness_calls_no_keep = 0 +actual_num_fitness_calls_keep_elitism = 0 +actual_num_fitness_calls_keep_parents = 0 + +num_generations = 100 +sol_per_pop = 10 +num_parents_mating = 5 + +# TODO: Calculate the number when fitness_batch_size is used. + +def number_calls_fitness_function(keep_elitism=1, + keep_parents=-1, + mutation_type="random", + mutation_percent_genes="default", + parent_selection_type='sss', + multi_objective=False, + fitness_batch_size=None): + + actual_num_fitness_calls = 0 + def fitness_func_no_batch_single(ga, solution, idx): + nonlocal actual_num_fitness_calls + actual_num_fitness_calls = actual_num_fitness_calls + 1 + return random.random() + + def fitness_func_no_batch_multi(ga_instance, solution, solution_idx): + nonlocal actual_num_fitness_calls + actual_num_fitness_calls = actual_num_fitness_calls + 1 + return [random.random(), random.random()] + + def fitness_func_batch_single(ga_instance, solution, solution_idx): + nonlocal actual_num_fitness_calls + actual_num_fitness_calls = actual_num_fitness_calls + 1 + f = [] + for sol in solution: + f.append(random.random()) + return f + + def fitness_func_batch_multi(ga_instance, solution, solution_idx): + nonlocal actual_num_fitness_calls + actual_num_fitness_calls = actual_num_fitness_calls + 1 + f = [] + for sol in solution: + f.append([random.random(), random.random()]) + return f + + if fitness_batch_size is None or (type(fitness_batch_size) in pygad.GA.supported_int_types and fitness_batch_size == 1): + if multi_objective == True: + fitness_func = fitness_func_no_batch_multi + else: + fitness_func = fitness_func_no_batch_single + elif (type(fitness_batch_size) in pygad.GA.supported_int_types and fitness_batch_size > 1): + if multi_objective == True: + fitness_func = fitness_func_batch_multi + else: + fitness_func = fitness_func_batch_single + + 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, + mutation_type=mutation_type, + parent_selection_type=parent_selection_type, + mutation_percent_genes=mutation_percent_genes, + keep_elitism=keep_elitism, + keep_parents=keep_parents, + suppress_warnings=True, + fitness_batch_size=fitness_batch_size) + + ga_optimizer.run() + + if fitness_batch_size is None: + if keep_elitism == 0: + if keep_parents == 0: + # 10 (for initial population) + 100*10 (for other generations) = 1010 + expected_num_fitness_calls = sol_per_pop + num_generations * sol_per_pop + if mutation_type == "adaptive": + expected_num_fitness_calls += num_generations * sol_per_pop + elif keep_parents == -1: + # 10 (for initial population) + 100*num_parents_mating (for other generations) + expected_num_fitness_calls = sol_per_pop + num_generations * (sol_per_pop - num_parents_mating) + if mutation_type == "adaptive": + expected_num_fitness_calls += num_generations * (sol_per_pop - num_parents_mating) + else: + # 10 (for initial population) + 100*keep_parents (for other generations) + expected_num_fitness_calls = sol_per_pop + num_generations * (sol_per_pop - keep_parents) + if mutation_type == "adaptive": + expected_num_fitness_calls += num_generations * (sol_per_pop - keep_parents) + else: + # 10 (for initial population) + 100*keep_elitism (for other generations) + expected_num_fitness_calls = sol_per_pop + num_generations * (sol_per_pop - keep_elitism) + if mutation_type == "adaptive": + expected_num_fitness_calls += num_generations * (sol_per_pop - keep_elitism) + else: + if keep_elitism == 0: + if keep_parents == 0: + # 10 (for initial population) + 100*10 (for other generations) = 1010 + expected_num_fitness_calls = int(numpy.ceil(sol_per_pop/fitness_batch_size)) + num_generations * int(numpy.ceil(sol_per_pop/fitness_batch_size)) + if mutation_type == "adaptive": + expected_num_fitness_calls += num_generations * int(numpy.ceil(sol_per_pop/fitness_batch_size)) + elif keep_parents == -1: + # 10 (for initial population) + 100*num_parents_mating (for other generations) + expected_num_fitness_calls = int(numpy.ceil(sol_per_pop/fitness_batch_size)) + num_generations * int(numpy.ceil((sol_per_pop - num_parents_mating)/fitness_batch_size)) + if mutation_type == "adaptive": + expected_num_fitness_calls += num_generations * int(numpy.ceil((sol_per_pop - num_parents_mating)/fitness_batch_size)) + else: + # 10 (for initial population) + 100*keep_parents (for other generations) + expected_num_fitness_calls = int(numpy.ceil(sol_per_pop/fitness_batch_size)) + num_generations * int(numpy.ceil((sol_per_pop - keep_parents)/fitness_batch_size)) + if mutation_type == "adaptive": + expected_num_fitness_calls += num_generations * int(numpy.ceil((sol_per_pop - keep_parents)/fitness_batch_size)) + else: + # 10 (for initial population) + 100*keep_elitism (for other generations) + expected_num_fitness_calls = int(numpy.ceil(sol_per_pop/fitness_batch_size)) + num_generations * int(numpy.ceil((sol_per_pop - keep_elitism)/fitness_batch_size)) + if mutation_type == "adaptive": + expected_num_fitness_calls += num_generations * int(numpy.ceil((sol_per_pop - keep_elitism)/fitness_batch_size)) + + print(f"Expected number of fitness function calls is {expected_num_fitness_calls}.") + print(f"Actual number of fitness function calls is {actual_num_fitness_calls}.") + return actual_num_fitness_calls, expected_num_fitness_calls + +def test_number_calls_fitness_function_default_keep(): + actual, expected = number_calls_fitness_function() + assert actual == expected + +def test_number_calls_fitness_function_no_keep(): + actual, expected = number_calls_fitness_function(keep_elitism=0, + keep_parents=0) + assert actual == expected + +def test_number_calls_fitness_function_keep_elitism(): + actual, expected = number_calls_fitness_function(keep_elitism=3, + keep_parents=0) + assert actual == expected + +def test_number_calls_fitness_function_keep_parents(): + actual, expected = number_calls_fitness_function(keep_elitism=0, + keep_parents=4) + assert actual == expected + +def test_number_calls_fitness_function_both_keep(): + actual, expected = number_calls_fitness_function(keep_elitism=3, + keep_parents=4) + assert actual == expected + +def test_number_calls_fitness_function_no_keep_adaptive_mutation(): + actual, expected = number_calls_fitness_function(keep_elitism=0, + keep_parents=0, + mutation_type="adaptive", + mutation_percent_genes=[10, 5]) + assert actual == expected + +def test_number_calls_fitness_function_default_adaptive_mutation(): + actual, expected = number_calls_fitness_function(mutation_type="adaptive", + mutation_percent_genes=[10, 5]) + assert actual == expected + +def test_number_calls_fitness_function_both_keep_adaptive_mutation(): + actual, expected = number_calls_fitness_function(keep_elitism=3, + keep_parents=4, + mutation_type="adaptive", + mutation_percent_genes=[10, 5]) + assert actual == expected + +#### Multi Objective +def test_number_calls_fitness_function_no_keep_multi_objective(): + actual, expected = number_calls_fitness_function(keep_elitism=0, + keep_parents=0, + multi_objective=True) + assert actual == expected + +def test_number_calls_fitness_function_keep_elitism_multi_objective(): + actual, expected = number_calls_fitness_function(keep_elitism=3, + keep_parents=0, + multi_objective=True) + assert actual == expected + +def test_number_calls_fitness_function_keep_parents_multi_objective(): + actual, expected = number_calls_fitness_function(keep_elitism=0, + keep_parents=4, + multi_objective=True) + assert actual == expected + +def test_number_calls_fitness_function_both_keep_multi_objective(): + actual, expected = number_calls_fitness_function(keep_elitism=3, + keep_parents=4, + multi_objective=True) + assert actual == expected + +def test_number_calls_fitness_function_no_keep_adaptive_mutation_multi_objective(): + actual, expected = number_calls_fitness_function(keep_elitism=0, + keep_parents=0, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + multi_objective=True) + assert actual == expected + +def test_number_calls_fitness_function_default_adaptive_mutation_multi_objective(): + actual, expected = number_calls_fitness_function(mutation_type="adaptive", + mutation_percent_genes=[10, 5], + multi_objective=True) + assert actual == expected + +def test_number_calls_fitness_function_both_keep_adaptive_mutation_multi_objective(): + actual, expected = number_calls_fitness_function(keep_elitism=3, + keep_parents=4, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + multi_objective=True) + assert actual == expected + +#### Multi Objective NSGA-II Parent Selection +def test_number_calls_fitness_function_no_keep_multi_objective_nsga2(): + actual, expected = number_calls_fitness_function(keep_elitism=0, + keep_parents=0, + multi_objective=True) + assert actual == expected + +def test_number_calls_fitness_function_keep_elitism_multi_objective_nsga2(): + actual, expected = number_calls_fitness_function(keep_elitism=3, + keep_parents=0, + multi_objective=True) + assert actual == expected + +def test_number_calls_fitness_function_keep_parents_multi_objective_nsga2(): + actual, expected = number_calls_fitness_function(keep_elitism=0, + keep_parents=4, + multi_objective=True) + assert actual == expected + +def test_number_calls_fitness_function_both_keep_multi_objective_nsga2(): + actual, expected = number_calls_fitness_function(keep_elitism=3, + keep_parents=4, + multi_objective=True) + assert actual == expected + +def test_number_calls_fitness_function_no_keep_adaptive_mutation_multi_objective_nsga2(): + actual, expected = number_calls_fitness_function(keep_elitism=0, + keep_parents=0, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + multi_objective=True) + assert actual == expected + +def test_number_calls_fitness_function_default_adaptive_mutation_multi_objective_nsga2(): + actual, expected = number_calls_fitness_function(mutation_type="adaptive", + mutation_percent_genes=[10, 5], + multi_objective=True) + assert actual == expected + +def test_number_calls_fitness_function_both_keep_adaptive_mutation_multi_objective_nsga2(): + actual, expected = number_calls_fitness_function(keep_elitism=3, + keep_parents=4, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + multi_objective=True) + assert actual == expected + + +######## Batch Fitness Calculation +#### Single Objective +def test_number_calls_fitness_function_no_keep_batch_1(): + actual, expected = number_calls_fitness_function(fitness_batch_size=1) + assert actual == expected + +def test_number_calls_fitness_function_no_keep_batch_4(): + actual, expected = number_calls_fitness_function(fitness_batch_size=4) + assert actual == expected + +def test_number_calls_fitness_function_no_keep_batch_9(): + actual, expected = number_calls_fitness_function(fitness_batch_size=9) + assert actual == expected + +def test_number_calls_fitness_function_no_keep_batch_10(): + actual, expected = number_calls_fitness_function(fitness_batch_size=10) + assert actual == expected + +def test_number_calls_fitness_function_keep_elitism_batch_4(): + actual, expected = number_calls_fitness_function(keep_elitism=3, + keep_parents=0, + fitness_batch_size=4) + assert actual == expected + +def test_number_calls_fitness_function_keep_parents_batch_4(): + actual, expected = number_calls_fitness_function(keep_elitism=0, + keep_parents=4, + fitness_batch_size=4) + assert actual == expected + +def test_number_calls_fitness_function_both_keep_batch_4(): + actual, expected = number_calls_fitness_function(keep_elitism=3, + keep_parents=4, + fitness_batch_size=4) + assert actual == expected + +def test_number_calls_fitness_function_no_keep_adaptive_mutation_batch_4(): + actual, expected = number_calls_fitness_function(keep_elitism=0, + keep_parents=0, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + fitness_batch_size=4) + assert actual == expected + +def test_number_calls_fitness_function_default_adaptive_mutation_batch_4(): + actual, expected = number_calls_fitness_function(mutation_type="adaptive", + mutation_percent_genes=[10, 5], + fitness_batch_size=4) + assert actual == expected + +def test_number_calls_fitness_function_both_keep_adaptive_mutation_batch_4(): + actual, expected = number_calls_fitness_function(keep_elitism=3, + keep_parents=4, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + fitness_batch_size=4) + assert actual == expected + +#### Multi Objective +def test_number_calls_fitness_function_no_keep_multi_objective_batch_1(): + actual, expected = number_calls_fitness_function(keep_elitism=0, + keep_parents=0, + multi_objective=True, + fitness_batch_size=1) + assert actual == expected + +def test_number_calls_fitness_function_no_keep_multi_objective_batch_4(): + actual, expected = number_calls_fitness_function(keep_elitism=0, + keep_parents=0, + multi_objective=True, + fitness_batch_size=4) + assert actual == expected + +def test_number_calls_fitness_function_no_keep_multi_objective_batch_9(): + actual, expected = number_calls_fitness_function(keep_elitism=0, + keep_parents=0, + multi_objective=True, + fitness_batch_size=9) + assert actual == expected + +def test_number_calls_fitness_function_no_keep_multi_objective_batch_10(): + actual, expected = number_calls_fitness_function(keep_elitism=0, + keep_parents=0, + multi_objective=True, + fitness_batch_size=10) + assert actual == expected + +def test_number_calls_fitness_function_keep_elitism_multi_objective_batch_4(): + actual, expected = number_calls_fitness_function(keep_elitism=3, + keep_parents=0, + multi_objective=True, + fitness_batch_size=4) + assert actual == expected + +def test_number_calls_fitness_function_keep_parents_multi_objective_batch_4(): + actual, expected = number_calls_fitness_function(keep_elitism=0, + keep_parents=4, + multi_objective=True, + fitness_batch_size=4) + assert actual == expected + +def test_number_calls_fitness_function_both_keep_multi_objective_batch_4(): + actual, expected = number_calls_fitness_function(keep_elitism=3, + keep_parents=4, + multi_objective=True, + fitness_batch_size=4) + assert actual == expected + +def test_number_calls_fitness_function_no_keep_adaptive_mutation_multi_objective_batch_4(): + actual, expected = number_calls_fitness_function(keep_elitism=0, + keep_parents=0, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + multi_objective=True, + fitness_batch_size=4) + assert actual == expected + +def test_number_calls_fitness_function_default_adaptive_mutation_multi_objective_batch_4(): + actual, expected = number_calls_fitness_function(mutation_type="adaptive", + mutation_percent_genes=[10, 5], + multi_objective=True, + fitness_batch_size=4) + assert actual == expected + +def test_number_calls_fitness_function_both_keep_adaptive_mutation_multi_objective_batch_4(): + actual, expected = number_calls_fitness_function(keep_elitism=3, + keep_parents=4, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + multi_objective=True, + fitness_batch_size=4) + assert actual == expected + +#### Multi Objective NSGA-II Parent Selection +def test_number_calls_fitness_function_no_keep_multi_objective_nsga2_batch_1(): + actual, expected = number_calls_fitness_function(keep_elitism=0, + keep_parents=0, + multi_objective=True, + fitness_batch_size=1) + assert actual == expected + +def test_number_calls_fitness_function_no_keep_multi_objective_nsga2_batch_4(): + actual, expected = number_calls_fitness_function(keep_elitism=0, + keep_parents=0, + multi_objective=True, + fitness_batch_size=4) + assert actual == expected + +def test_number_calls_fitness_function_no_keep_multi_objective_nsga2_batch_9(): + actual, expected = number_calls_fitness_function(keep_elitism=0, + keep_parents=0, + multi_objective=True, + fitness_batch_size=9) + assert actual == expected + +def test_number_calls_fitness_function_no_keep_multi_objective_nsga2_batch_10(): + actual, expected = number_calls_fitness_function(keep_elitism=0, + keep_parents=0, + multi_objective=True, + fitness_batch_size=10) + assert actual == expected + +def test_number_calls_fitness_function_keep_elitism_multi_objective_nsga2_batch_4(): + actual, expected = number_calls_fitness_function(keep_elitism=3, + keep_parents=0, + multi_objective=True, + fitness_batch_size=4) + assert actual == expected + +def test_number_calls_fitness_function_keep_parents_multi_objective_nsga2_batch_4(): + actual, expected = number_calls_fitness_function(keep_elitism=0, + keep_parents=4, + multi_objective=True, + fitness_batch_size=4) + assert actual == expected + +def test_number_calls_fitness_function_both_keep_multi_objective_nsga2_batch_4(): + actual, expected = number_calls_fitness_function(keep_elitism=3, + keep_parents=4, + multi_objective=True, + fitness_batch_size=4) + assert actual == expected + +def test_number_calls_fitness_function_no_keep_adaptive_mutation_multi_objective_nsga2_batch_4(): + actual, expected = number_calls_fitness_function(keep_elitism=0, + keep_parents=0, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + multi_objective=True, + fitness_batch_size=4) + assert actual == expected + +def test_number_calls_fitness_function_default_adaptive_mutation_multi_objective_nsga2_batch_4(): + actual, expected = number_calls_fitness_function(mutation_type="adaptive", + mutation_percent_genes=[10, 5], + multi_objective=True, + fitness_batch_size=4) + assert actual == expected + +def test_number_calls_fitness_function_both_keep_adaptive_mutation_multi_objective_nsga2_batch_4(): + actual, expected = number_calls_fitness_function(keep_elitism=3, + keep_parents=4, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + multi_objective=True, + fitness_batch_size=4) + assert actual == expected + + +if __name__ == "__main__": + #### Single-objective + print() + test_number_calls_fitness_function_default_keep() + print() + test_number_calls_fitness_function_no_keep() + print() + test_number_calls_fitness_function_keep_elitism() + print() + test_number_calls_fitness_function_keep_parents() + print() + test_number_calls_fitness_function_both_keep() + print() + test_number_calls_fitness_function_no_keep_adaptive_mutation() + print() + test_number_calls_fitness_function_default_adaptive_mutation() + print() + test_number_calls_fitness_function_both_keep_adaptive_mutation() + print() + + #### Multi-Objective + print() + test_number_calls_fitness_function_no_keep_multi_objective() + print() + test_number_calls_fitness_function_keep_elitism_multi_objective() + print() + test_number_calls_fitness_function_keep_parents_multi_objective() + print() + test_number_calls_fitness_function_both_keep_multi_objective() + print() + test_number_calls_fitness_function_no_keep_adaptive_mutation_multi_objective() + print() + test_number_calls_fitness_function_default_adaptive_mutation_multi_objective() + print() + test_number_calls_fitness_function_both_keep_adaptive_mutation_multi_objective() + print() + + #### Multi-Objective NSGA-II Parent Selection + print() + test_number_calls_fitness_function_no_keep_multi_objective_nsga2() + print() + test_number_calls_fitness_function_keep_elitism_multi_objective_nsga2() + print() + test_number_calls_fitness_function_keep_parents_multi_objective_nsga2() + print() + test_number_calls_fitness_function_both_keep_multi_objective_nsga2() + print() + test_number_calls_fitness_function_no_keep_adaptive_mutation_multi_objective_nsga2() + print() + test_number_calls_fitness_function_default_adaptive_mutation_multi_objective_nsga2() + print() + test_number_calls_fitness_function_both_keep_adaptive_mutation_multi_objective_nsga2() + print() + + + ######## Batch Fitness + #### Single-objective + print() + test_number_calls_fitness_function_no_keep_batch_1() + print() + test_number_calls_fitness_function_no_keep_batch_4() + print() + test_number_calls_fitness_function_no_keep_batch_9() + print() + test_number_calls_fitness_function_no_keep_batch_10() + print() + test_number_calls_fitness_function_keep_elitism_batch_4() + print() + test_number_calls_fitness_function_keep_parents_batch_4() + print() + test_number_calls_fitness_function_both_keep_batch_4() + print() + test_number_calls_fitness_function_no_keep_adaptive_mutation_batch_4() + print() + test_number_calls_fitness_function_default_adaptive_mutation_batch_4() + print() + test_number_calls_fitness_function_both_keep_adaptive_mutation_batch_4() + print() + + #### Multi-Objective + print() + test_number_calls_fitness_function_no_keep_multi_objective_batch_1() + print() + test_number_calls_fitness_function_no_keep_multi_objective_batch_4() + print() + test_number_calls_fitness_function_no_keep_multi_objective_batch_9() + print() + test_number_calls_fitness_function_no_keep_multi_objective_batch_10() + print() + test_number_calls_fitness_function_keep_elitism_multi_objective_batch_4() + print() + test_number_calls_fitness_function_keep_parents_multi_objective_batch_4() + print() + test_number_calls_fitness_function_both_keep_multi_objective_batch_4() + print() + test_number_calls_fitness_function_no_keep_adaptive_mutation_multi_objective_batch_4() + print() + test_number_calls_fitness_function_default_adaptive_mutation_multi_objective_batch_4() + print() + test_number_calls_fitness_function_both_keep_adaptive_mutation_multi_objective_batch_4() + print() + + #### Multi-Objective NSGA-II Parent Selection + print() + test_number_calls_fitness_function_no_keep_multi_objective_nsga2_batch_1() + print() + test_number_calls_fitness_function_no_keep_multi_objective_nsga2_batch_4() + print() + test_number_calls_fitness_function_no_keep_multi_objective_nsga2_batch_9() + print() + test_number_calls_fitness_function_no_keep_multi_objective_nsga2_batch_10() + print() + test_number_calls_fitness_function_keep_elitism_multi_objective_nsga2_batch_4() + print() + test_number_calls_fitness_function_keep_parents_multi_objective_nsga2_batch_4() + print() + test_number_calls_fitness_function_both_keep_multi_objective_nsga2_batch_4() + print() + test_number_calls_fitness_function_no_keep_adaptive_mutation_multi_objective_nsga2_batch_4() + print() + test_number_calls_fitness_function_default_adaptive_mutation_multi_objective_nsga2_batch_4() + print() + test_number_calls_fitness_function_both_keep_adaptive_mutation_multi_objective_nsga2_batch_4() + print() + diff --git a/tests/test_save_solutions.py b/tests/test_save_solutions.py new file mode 100644 index 00000000..45216e1e --- /dev/null +++ b/tests/test_save_solutions.py @@ -0,0 +1,1289 @@ +import pygad +import random + +num_generations = 100 +sol_per_pop = 10 +num_parents_mating = 5 + +# TODO Verify that the each entry in 'solutions_fitness' and 'best_solutions_fitness' have values equal to the number of objectives. + +def number_saved_solutions(keep_elitism=1, + keep_parents=-1, + mutation_type="random", + mutation_percent_genes="default", + parent_selection_type='sss', + multi_objective=False, + fitness_batch_size=None, + save_solutions=False, + save_best_solutions=False): + + def fitness_func_no_batch_single(ga, solution, idx): + return random.random() + + def fitness_func_no_batch_multi(ga_instance, solution, solution_idx): + return [random.random(), random.random()] + + def fitness_func_batch_single(ga_instance, solution, solution_idx): + f = [] + for sol in solution: + f.append(random.random()) + return f + + def fitness_func_batch_multi(ga_instance, solution, solution_idx): + f = [] + for sol in solution: + f.append([random.random(), random.random()]) + return f + + if fitness_batch_size is None or (type(fitness_batch_size) in pygad.GA.supported_int_types and fitness_batch_size == 1): + if multi_objective == True: + fitness_func = fitness_func_no_batch_multi + else: + fitness_func = fitness_func_no_batch_single + elif (type(fitness_batch_size) in pygad.GA.supported_int_types and fitness_batch_size > 1): + if multi_objective == True: + fitness_func = fitness_func_batch_multi + else: + fitness_func = fitness_func_batch_single + + 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, + mutation_type=mutation_type, + parent_selection_type=parent_selection_type, + mutation_percent_genes=mutation_percent_genes, + keep_elitism=keep_elitism, + keep_parents=keep_parents, + suppress_warnings=True, + fitness_batch_size=fitness_batch_size, + save_best_solutions=save_best_solutions, + save_solutions=save_solutions) + + ga_optimizer.run() + + if save_solutions == True: + expected_num_solutions = sol_per_pop + num_generations * sol_per_pop + else: + expected_num_solutions = 0 + + if save_best_solutions == True: + expected_num_best_solutions = 1 + num_generations + else: + expected_num_best_solutions = 0 + + print(f"Expected number of solutions is {expected_num_solutions}.") + print(f"Actual number of solutions is {len(ga_optimizer.solutions)}.") + print(f"Expected number of best solutions is {expected_num_best_solutions}.") + print(f"Actual number of best solutions is {len(ga_optimizer.best_solutions)}.") + return expected_num_solutions, len(ga_optimizer.solutions), len(ga_optimizer.solutions_fitness), expected_num_best_solutions, len(ga_optimizer.best_solutions) + +#### Single Objective +def test_save_solutions_default_keep(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions() + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions() + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_save_solutions(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(save_solutions=True, + save_best_solutions=True) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_keep_elitism(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=0) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_keep_elitism_save_solutions(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=0, + save_solutions=True, + save_best_solutions=True) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_keep_parents(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=0, + keep_parents=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_keep_parents_save_solutions(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=0, + keep_parents=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_both_keep(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_both_keep_save_solutions(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=4, + save_solutions=True, + save_best_solutions=True) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_adaptive_mutation(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=0, + keep_parents=0, + mutation_type="adaptive", + mutation_percent_genes=[10, 5]) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_adaptive_mutation_save_solutions(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=0, + keep_parents=0, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + save_solutions=True, + save_best_solutions=True) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_default_adaptive_mutation(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(mutation_type="adaptive", + mutation_percent_genes=[10, 5]) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_both_keep_adaptive_mutation(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=4, + mutation_type="adaptive", + mutation_percent_genes=[10, 5]) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_both_keep_adaptive_mutation_save_solutions(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=4, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + save_solutions=True, + save_best_solutions=True) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + + +#### Multi Objective +def test_save_solutions_default_keep_multi_objective(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(multi_objective=True) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_multi_objective(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(multi_objective=True) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_save_solutions_multi_objective(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(save_solutions=True, + save_best_solutions=True, + multi_objective=True) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_keep_elitism_multi_objective(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=0, + multi_objective=True) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_keep_elitism_save_solutions_multi_objective(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=0, + save_solutions=True, + save_best_solutions=True, + multi_objective=True) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_keep_parents_multi_objective(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=0, + keep_parents=4, + multi_objective=True) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_keep_parents_save_solutions_multi_objective(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=0, + keep_parents=4, + multi_objective=True) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_both_keep_multi_objective(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=4, + multi_objective=True) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_both_keep_save_solutions_multi_objective(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=4, + save_solutions=True, + save_best_solutions=True, + multi_objective=True) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_adaptive_mutation_multi_objective(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=0, + keep_parents=0, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + multi_objective=True) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_adaptive_mutation_save_solutions_multi_objective(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=0, + keep_parents=0, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + save_solutions=True, + save_best_solutions=True, + multi_objective=True) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_default_adaptive_mutation_multi_objective(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(mutation_type="adaptive", + mutation_percent_genes=[10, 5], + multi_objective=True) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_both_keep_adaptive_mutation_multi_objective(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=4, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + multi_objective=True) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_both_keep_adaptive_mutation_save_solutions_multi_objective(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=4, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + save_solutions=True, + save_best_solutions=True, + multi_objective=True) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + + +#### Multi Objective NSGA-II Parent Selection +def test_save_solutions_default_keep_multi_objective_nsga2(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(multi_objective=True, + parent_selection_type='nsga2') + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_multi_objective_nsga2(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(multi_objective=True, + parent_selection_type='nsga2') + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_save_solutions_multi_objective_nsga2(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(save_solutions=True, + save_best_solutions=True, + multi_objective=True, + parent_selection_type='nsga2') + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_keep_elitism_multi_objective_nsga2(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=0, + multi_objective=True, + parent_selection_type='nsga2') + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_keep_elitism_save_solutions_multi_objective_nsga2(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=0, + save_solutions=True, + save_best_solutions=True, + multi_objective=True, + parent_selection_type='nsga2') + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_keep_parents_multi_objective_nsga2(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=0, + keep_parents=4, + multi_objective=True, + parent_selection_type='nsga2') + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_keep_parents_save_solutions_multi_objective_nsga2(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=0, + keep_parents=4, + multi_objective=True, + parent_selection_type='nsga2') + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_both_keep_multi_objective_nsga2(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=4, + multi_objective=True, + parent_selection_type='nsga2') + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_both_keep_save_solutions_multi_objective_nsga2(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=4, + save_solutions=True, + save_best_solutions=True, + multi_objective=True, + parent_selection_type='nsga2') + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_adaptive_mutation_multi_objective_nsga2(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=0, + keep_parents=0, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + multi_objective=True, + parent_selection_type='nsga2') + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_adaptive_mutation_save_solutions_multi_objective_nsga2(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=0, + keep_parents=0, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + save_solutions=True, + save_best_solutions=True, + multi_objective=True, + parent_selection_type='nsga2') + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_default_adaptive_mutation_multi_objective_nsga2(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(mutation_type="adaptive", + mutation_percent_genes=[10, 5], + multi_objective=True, + parent_selection_type='nsga2') + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_both_keep_adaptive_mutation_multi_objective_nsga2(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=4, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + multi_objective=True, + parent_selection_type='nsga2') + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_both_keep_adaptive_mutation_save_solutions_multi_objective_nsga2(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=4, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + save_solutions=True, + save_best_solutions=True, + multi_objective=True, + parent_selection_type='nsga2') + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +######## Batch Fitness +#### Single Objective +def test_save_solutions_no_keep_batch_1(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(fitness_batch_size=1) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_batch_2(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(fitness_batch_size=2) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_batch_3(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(fitness_batch_size=3) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_batch_5(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(fitness_batch_size=5) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_batch_6(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(fitness_batch_size=6) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_batch_7(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(fitness_batch_size=7) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_batch_8(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(fitness_batch_size=8) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_batch_9(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(fitness_batch_size=9) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_batch_10(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(fitness_batch_size=10) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + + +def test_save_solutions_no_keep_save_solutions_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(save_solutions=True, + save_best_solutions=True, + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_keep_elitism_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=0, + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_keep_elitism_save_solutions_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=0, + save_solutions=True, + save_best_solutions=True, + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_keep_parents_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=0, + keep_parents=4, + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_keep_parents_save_solutions_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=0, + keep_parents=4, + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_both_keep_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=4, + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_both_keep_save_solutions_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=4, + save_solutions=True, + save_best_solutions=True, + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_adaptive_mutation_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=0, + keep_parents=0, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_adaptive_mutation_save_solutions_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=0, + keep_parents=0, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + save_solutions=True, + save_best_solutions=True, + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_default_adaptive_mutation_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(mutation_type="adaptive", + mutation_percent_genes=[10, 5], + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_both_keep_adaptive_mutation_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=4, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_both_keep_adaptive_mutation_save_solutions_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=4, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + save_solutions=True, + save_best_solutions=True, + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + + +#### Multi Objective +def test_save_solutions_no_keep_multi_objective_batch_1(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(multi_objective=True, + fitness_batch_size=1) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_multi_objective_batch_2(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(multi_objective=True, + fitness_batch_size=2) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_multi_objective_batch_3(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(multi_objective=True, + fitness_batch_size=3) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_multi_objective_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(multi_objective=True, + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_multi_objective_batch_5(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(multi_objective=True, + fitness_batch_size=5) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_multi_objective_batch_6(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(multi_objective=True, + fitness_batch_size=6) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_multi_objective_batch_7(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(multi_objective=True, + fitness_batch_size=7) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_multi_objective_batch_8(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(multi_objective=True, + fitness_batch_size=8) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_multi_objective_batch_9(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(multi_objective=True, + fitness_batch_size=9) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_multi_objective_batch_10(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(multi_objective=True, + fitness_batch_size=10) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_save_solutions_multi_objective_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(save_solutions=True, + save_best_solutions=True, + multi_objective=True, + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_keep_elitism_multi_objective_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=0, + multi_objective=True, + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_keep_elitism_save_solutions_multi_objective_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=0, + save_solutions=True, + save_best_solutions=True, + multi_objective=True, + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_keep_parents_multi_objective_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=0, + keep_parents=4, + multi_objective=True, + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_keep_parents_save_solutions_multi_objective_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=0, + keep_parents=4, + multi_objective=True, + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_both_keep_multi_objective_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=4, + multi_objective=True, + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_both_keep_save_solutions_multi_objective_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=4, + save_solutions=True, + save_best_solutions=True, + multi_objective=True, + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_adaptive_mutation_multi_objective_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=0, + keep_parents=0, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + multi_objective=True, + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_adaptive_mutation_save_solutions_multi_objective_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=0, + keep_parents=0, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + save_solutions=True, + save_best_solutions=True, + multi_objective=True, + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_default_adaptive_mutation_multi_objective_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(mutation_type="adaptive", + mutation_percent_genes=[10, 5], + multi_objective=True, + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_both_keep_adaptive_mutation_multi_objective_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=4, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + multi_objective=True, + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_both_keep_adaptive_mutation_save_solutions_multi_objective_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=4, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + save_solutions=True, + save_best_solutions=True, + multi_objective=True, + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + + +#### Multi Objective NSGA-II Parent Selection +def test_save_solutions_no_keep_multi_objective_nsga2_batch_1(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(multi_objective=True, + parent_selection_type='nsga2', + fitness_batch_size=1) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_multi_objective_nsga2_batch_2(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(multi_objective=True, + parent_selection_type='nsga2', + fitness_batch_size=2) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_multi_objective_nsga2_batch_3(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(multi_objective=True, + parent_selection_type='nsga2', + fitness_batch_size=3) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_multi_objective_nsga2_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(multi_objective=True, + parent_selection_type='nsga2', + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_multi_objective_nsga2_batch_5(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(multi_objective=True, + parent_selection_type='nsga2', + fitness_batch_size=5) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_multi_objective_nsga2_batch_6(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(multi_objective=True, + parent_selection_type='nsga2', + fitness_batch_size=6) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_multi_objective_nsga2_batch_7(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(multi_objective=True, + parent_selection_type='nsga2', + fitness_batch_size=7) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_multi_objective_nsga2_batch_8(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(multi_objective=True, + parent_selection_type='nsga2', + fitness_batch_size=8) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_multi_objective_nsga2_batch_9(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(multi_objective=True, + parent_selection_type='nsga2', + fitness_batch_size=9) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_multi_objective_nsga2_batch_10(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(multi_objective=True, + parent_selection_type='nsga2', + fitness_batch_size=10) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_save_solutions_multi_objective_nsga2_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(save_solutions=True, + save_best_solutions=True, + multi_objective=True, + parent_selection_type='nsga2', + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_keep_elitism_multi_objective_nsga2_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=0, + multi_objective=True, + parent_selection_type='nsga2', + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_keep_elitism_save_solutions_multi_objective_nsga2_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=0, + save_solutions=True, + save_best_solutions=True, + multi_objective=True, + parent_selection_type='nsga2', + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_keep_parents_multi_objective_nsga2_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=0, + keep_parents=4, + multi_objective=True, + parent_selection_type='nsga2', + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_keep_parents_save_solutions_multi_objective_nsga2_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=0, + keep_parents=4, + multi_objective=True, + parent_selection_type='nsga2', + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_both_keep_multi_objective_nsga2_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=4, + multi_objective=True, + parent_selection_type='nsga2', + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_both_keep_save_solutions_multi_objective_nsga2_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=4, + save_solutions=True, + save_best_solutions=True, + multi_objective=True, + parent_selection_type='nsga2', + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_adaptive_mutation_multi_objective_nsga2_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=0, + keep_parents=0, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + multi_objective=True, + parent_selection_type='nsga2', + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_no_keep_adaptive_mutation_save_solutions_multi_objective_nsga2_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=0, + keep_parents=0, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + save_solutions=True, + save_best_solutions=True, + multi_objective=True, + parent_selection_type='nsga2', + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_default_adaptive_mutation_multi_objective_nsga2_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(mutation_type="adaptive", + mutation_percent_genes=[10, 5], + multi_objective=True, + parent_selection_type='nsga2', + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_both_keep_adaptive_mutation_multi_objective_nsga2_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=4, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + multi_objective=True, + parent_selection_type='nsga2', + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + +def test_save_solutions_both_keep_adaptive_mutation_save_solutions_multi_objective_nsga2_batch_4(): + expected_solutions, actual_solutions, actual_solutions_fitness, expected_best_solutions, actual_best_solutions = number_saved_solutions(keep_elitism=3, + keep_parents=4, + mutation_type="adaptive", + mutation_percent_genes=[10, 5], + save_solutions=True, + save_best_solutions=True, + multi_objective=True, + parent_selection_type='nsga2', + fitness_batch_size=4) + assert expected_solutions == actual_solutions + assert expected_solutions == actual_solutions_fitness + assert expected_best_solutions == actual_best_solutions + + + + +if __name__ == "__main__": + #### Single Objective + print() + test_save_solutions_default_keep() + print() + test_save_solutions_no_keep() + print() + test_save_solutions_no_keep_save_solutions() + print() + test_save_solutions_keep_elitism() + print() + test_save_solutions_keep_elitism_save_solutions() + print() + test_save_solutions_keep_parents() + print() + test_save_solutions_keep_parents_save_solutions() + print() + test_save_solutions_both_keep() + print() + test_save_solutions_both_keep_save_solutions() + print() + test_save_solutions_no_keep_adaptive_mutation() + print() + test_save_solutions_no_keep_adaptive_mutation_save_solutions() + print() + test_save_solutions_default_adaptive_mutation() + print() + test_save_solutions_both_keep_adaptive_mutation() + print() + test_save_solutions_both_keep_adaptive_mutation_save_solutions() + print() + + #### Multi-Objective + print() + test_save_solutions_default_keep_multi_objective() + print() + test_save_solutions_no_keep_multi_objective() + print() + test_save_solutions_no_keep_save_solutions_multi_objective() + print() + test_save_solutions_keep_elitism_multi_objective() + print() + test_save_solutions_keep_elitism_save_solutions_multi_objective() + print() + test_save_solutions_keep_parents_multi_objective() + print() + test_save_solutions_keep_parents_save_solutions_multi_objective() + print() + test_save_solutions_both_keep_multi_objective() + print() + test_save_solutions_both_keep_save_solutions_multi_objective() + print() + test_save_solutions_no_keep_adaptive_mutation_multi_objective() + print() + test_save_solutions_no_keep_adaptive_mutation_save_solutions_multi_objective() + print() + test_save_solutions_default_adaptive_mutation_multi_objective() + print() + test_save_solutions_both_keep_adaptive_mutation_multi_objective() + print() + test_save_solutions_both_keep_adaptive_mutation_save_solutions_multi_objective() + print() + + + #### Multi-Objective NSGA-II Parent Selection + print() + test_save_solutions_default_keep_multi_objective_nsga2() + print() + test_save_solutions_no_keep_multi_objective_nsga2() + print() + test_save_solutions_no_keep_save_solutions_multi_objective_nsga2() + print() + test_save_solutions_keep_elitism_multi_objective_nsga2() + print() + test_save_solutions_keep_elitism_save_solutions_multi_objective_nsga2() + print() + test_save_solutions_keep_parents_multi_objective_nsga2() + print() + test_save_solutions_keep_parents_save_solutions_multi_objective_nsga2() + print() + test_save_solutions_both_keep_multi_objective_nsga2() + print() + test_save_solutions_both_keep_save_solutions_multi_objective_nsga2() + print() + test_save_solutions_no_keep_adaptive_mutation_multi_objective_nsga2() + print() + test_save_solutions_no_keep_adaptive_mutation_save_solutions_multi_objective_nsga2() + print() + test_save_solutions_default_adaptive_mutation_multi_objective_nsga2() + print() + test_save_solutions_both_keep_adaptive_mutation_multi_objective_nsga2() + print() + test_save_solutions_both_keep_adaptive_mutation_save_solutions_multi_objective_nsga2() + print() + + ######## Batch Fitness Calculation + #### Single Objective + print() + test_save_solutions_no_keep_batch_1() + print() + test_save_solutions_no_keep_batch_2() + print() + test_save_solutions_no_keep_batch_3() + print() + test_save_solutions_no_keep_batch_4() + print() + test_save_solutions_no_keep_batch_5() + print() + test_save_solutions_no_keep_batch_6() + print() + test_save_solutions_no_keep_batch_7() + print() + test_save_solutions_no_keep_batch_8() + print() + test_save_solutions_no_keep_batch_9() + print() + test_save_solutions_no_keep_batch_10() + print() + test_save_solutions_no_keep_save_solutions_batch_4() + print() + test_save_solutions_keep_elitism_batch_4() + print() + test_save_solutions_keep_elitism_save_solutions_batch_4() + print() + test_save_solutions_keep_parents_batch_4() + print() + test_save_solutions_keep_parents_save_solutions_batch_4() + print() + test_save_solutions_both_keep_batch_4() + print() + test_save_solutions_both_keep_save_solutions_batch_4() + print() + test_save_solutions_no_keep_adaptive_mutation_batch_4() + print() + test_save_solutions_no_keep_adaptive_mutation_save_solutions_batch_4() + print() + test_save_solutions_default_adaptive_mutation_batch_4() + print() + test_save_solutions_both_keep_adaptive_mutation_batch_4() + print() + test_save_solutions_both_keep_adaptive_mutation_save_solutions_batch_4() + print() + + #### Multi-Objective + print() + test_save_solutions_no_keep_multi_objective_batch_1() + print() + test_save_solutions_no_keep_multi_objective_batch_2() + print() + test_save_solutions_no_keep_multi_objective_batch_3() + print() + test_save_solutions_no_keep_multi_objective_batch_4() + print() + test_save_solutions_no_keep_multi_objective_batch_5() + print() + test_save_solutions_no_keep_multi_objective_batch_6() + print() + test_save_solutions_no_keep_multi_objective_batch_7() + print() + test_save_solutions_no_keep_multi_objective_batch_8() + print() + test_save_solutions_no_keep_multi_objective_batch_9() + print() + test_save_solutions_no_keep_multi_objective_batch_10() + print() + test_save_solutions_no_keep_save_solutions_multi_objective_batch_4() + print() + test_save_solutions_keep_elitism_multi_objective_batch_4() + print() + test_save_solutions_keep_elitism_save_solutions_multi_objective_batch_4() + print() + test_save_solutions_keep_parents_multi_objective_batch_4() + print() + test_save_solutions_keep_parents_save_solutions_multi_objective_batch_4() + print() + test_save_solutions_both_keep_multi_objective_batch_4() + print() + test_save_solutions_both_keep_save_solutions_multi_objective_batch_4() + print() + test_save_solutions_no_keep_adaptive_mutation_multi_objective_batch_4() + print() + test_save_solutions_no_keep_adaptive_mutation_save_solutions_multi_objective_batch_4() + print() + test_save_solutions_default_adaptive_mutation_multi_objective_batch_4() + print() + test_save_solutions_both_keep_adaptive_mutation_multi_objective_batch_4() + print() + test_save_solutions_both_keep_adaptive_mutation_save_solutions_multi_objective_batch_4() + print() + + + #### Multi-Objective NSGA-II Parent Selection + print() + test_save_solutions_no_keep_multi_objective_nsga2_batch_1() + print() + test_save_solutions_no_keep_multi_objective_nsga2_batch_2() + print() + test_save_solutions_no_keep_multi_objective_nsga2_batch_3() + print() + test_save_solutions_no_keep_multi_objective_nsga2_batch_4() + print() + test_save_solutions_no_keep_multi_objective_nsga2_batch_5() + print() + test_save_solutions_no_keep_multi_objective_nsga2_batch_6() + print() + test_save_solutions_no_keep_multi_objective_nsga2_batch_7() + print() + test_save_solutions_no_keep_multi_objective_nsga2_batch_8() + print() + test_save_solutions_no_keep_multi_objective_nsga2_batch_9() + print() + test_save_solutions_no_keep_multi_objective_nsga2_batch_10() + print() + test_save_solutions_no_keep_save_solutions_multi_objective_nsga2_batch_4() + print() + test_save_solutions_keep_elitism_multi_objective_nsga2_batch_4() + print() + test_save_solutions_keep_elitism_save_solutions_multi_objective_nsga2_batch_4() + print() + test_save_solutions_keep_parents_multi_objective_nsga2_batch_4() + print() + test_save_solutions_keep_parents_save_solutions_multi_objective_nsga2_batch_4() + print() + test_save_solutions_both_keep_multi_objective_nsga2_batch_4() + print() + test_save_solutions_both_keep_save_solutions_multi_objective_nsga2_batch_4() + print() + test_save_solutions_no_keep_adaptive_mutation_multi_objective_nsga2_batch_4() + print() + test_save_solutions_no_keep_adaptive_mutation_save_solutions_multi_objective_nsga2_batch_4() + print() + test_save_solutions_default_adaptive_mutation_multi_objective_nsga2_batch_4() + print() + test_save_solutions_both_keep_adaptive_mutation_multi_objective_nsga2_batch_4() + print() + test_save_solutions_both_keep_adaptive_mutation_save_solutions_multi_objective_nsga2_batch_4() + print() diff --git a/tests/test_stop_criteria.py b/tests/test_stop_criteria.py new file mode 100644 index 00000000..3ee17376 --- /dev/null +++ b/tests/test_stop_criteria.py @@ -0,0 +1,234 @@ +import pygad +import numpy + +actual_num_fitness_calls_default_keep = 0 +actual_num_fitness_calls_no_keep = 0 +actual_num_fitness_calls_keep_elitism = 0 +actual_num_fitness_calls_keep_parents = 0 + +num_generations = 50 +sol_per_pop = 10 +num_parents_mating = 5 + +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. + +#### Define the fitness functions in the top-level of the module so that they are picklable and usable in the process-based parallel processing works. +#### If the functions are defined inside a class/method/function, they are not picklable and this error is raised: AttributeError: Can't pickle local object +#### Process-based parallel processing must have the used functions picklable. +def fitness_func_batch_multi(ga_instance, solution, solution_idx): + f = [] + for sol in solution: + output1 = numpy.sum(sol*function_inputs1) + output2 = numpy.sum(sol*function_inputs2) + fitness1 = 1.0 / (numpy.abs(output1 - desired_output1) + 0.000001) + fitness2 = 1.0 / (numpy.abs(output2 - desired_output2) + 0.000001) + f.append([fitness1, fitness2]) + return f + +def fitness_func_no_batch_multi(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] + +def fitness_func_batch_single(ga_instance, solution, solution_idx): + f = [] + for sol in solution: + output = numpy.sum(solution*function_inputs1) + fitness = 1.0 / (numpy.abs(output - desired_output1) + 0.000001) + f.append(fitness) + return f + +def fitness_func_no_batch_single(ga_instance, solution, solution_idx): + output = numpy.sum(solution*function_inputs1) + fitness = 1.0 / (numpy.abs(output - desired_output1) + 0.000001) + return fitness + + +def multi_objective_problem(keep_elitism=1, + keep_parents=-1, + fitness_batch_size=None, + stop_criteria=None, + parent_selection_type='sss', + mutation_type="random", + mutation_percent_genes="default", + multi_objective=False, + parallel_processing=None): + + if fitness_batch_size is None or (type(fitness_batch_size) in pygad.GA.supported_int_types and fitness_batch_size == 1): + if multi_objective == True: + fitness_func = fitness_func_no_batch_multi + else: + fitness_func = fitness_func_no_batch_single + elif (type(fitness_batch_size) in pygad.GA.supported_int_types and fitness_batch_size > 1): + if multi_objective == True: + fitness_func = fitness_func_batch_multi + else: + fitness_func = fitness_func_batch_single + + 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, + fitness_batch_size=fitness_batch_size, + mutation_type=mutation_type, + mutation_percent_genes=mutation_percent_genes, + keep_elitism=keep_elitism, + keep_parents=keep_parents, + stop_criteria=stop_criteria, + parent_selection_type=parent_selection_type, + parallel_processing=parallel_processing, + suppress_warnings=True) + + ga_optimizer.run() + + return None + +def test_number_calls_fitness_function_no_parallel_processing(): + multi_objective_problem(multi_objective=False, + fitness_batch_size=None, + parallel_processing=None) + +def test_number_calls_fitness_function_parallel_processing_thread_1(): + multi_objective_problem(multi_objective=False, + fitness_batch_size=None, + parallel_processing=['thread', 1]) + +def test_number_calls_fitness_function_parallel_processing_thread_2(): + multi_objective_problem(multi_objective=False, + fitness_batch_size=None, + parallel_processing=['thread', 2]) + +def test_number_calls_fitness_function_parallel_processing_thread_5(): + multi_objective_problem(multi_objective=False, + fitness_batch_size=None, + parallel_processing=['thread', 5]) + +def test_number_calls_fitness_function_parallel_processing_thread_5_patch_4(): + multi_objective_problem(multi_objective=False, + fitness_batch_size=4, + parallel_processing=['thread', 5]) + +def test_number_calls_fitness_function_parallel_processing_thread_5_patch_4_multi_objective(): + multi_objective_problem(multi_objective=True, + fitness_batch_size=4, + parallel_processing=['thread', 5]) + +def test_number_calls_fitness_function_parallel_processing_process_1(): + multi_objective_problem(multi_objective=False, + fitness_batch_size=None, + parallel_processing=['process', 1]) + +def test_number_calls_fitness_function_parallel_processing_process_2(): + multi_objective_problem(multi_objective=False, + fitness_batch_size=None, + parallel_processing=['process', 2]) + +def test_number_calls_fitness_function_parallel_processing_process_5(): + multi_objective_problem(multi_objective=False, + fitness_batch_size=None, + parallel_processing=['process', 5]) + +def test_number_calls_fitness_function_parallel_processing_process_5_patch_4(): + multi_objective_problem(multi_objective=False, + fitness_batch_size=4, + parallel_processing=['process', 5]) + +def test_number_calls_fitness_function_parallel_processing_process_5_patch_4_multi_objective(): + multi_objective_problem(multi_objective=True, + fitness_batch_size=4, + parallel_processing=['process', 5]) + +# Stop Criteria +def test_number_calls_fitness_function_multi_objective_stop_criteria_str_single_value(): + multi_objective_problem(multi_objective=True, + stop_criteria='reach_10') + +def test_number_calls_fitness_function_multi_objective_stop_criteria_str(): + multi_objective_problem(multi_objective=True, + stop_criteria='reach_10_20') + +def test_number_calls_fitness_function_multi_objective_stop_criteria_str_decimal(): + multi_objective_problem(multi_objective=True, + stop_criteria='reach_-1.0_0.5') + +def test_number_calls_fitness_function_multi_objective_stop_criteria_list(): + multi_objective_problem(multi_objective=True, + stop_criteria=['reach_10_20', 'reach_5_2']) + +def test_number_calls_fitness_function_multi_objective_stop_criteria_list_decimal(): + multi_objective_problem(multi_objective=True, + stop_criteria=['reach_-1.0_0.5', 'reach_5_-2.8']) + +def test_number_calls_fitness_function_single_objective_stop_criteria_str(): + multi_objective_problem(multi_objective=True, + stop_criteria='reach_10') + +def test_number_calls_fitness_function_single_objective_stop_criteria_str_decimal(): + multi_objective_problem(multi_objective=True, + stop_criteria='reach_-1.7') + +def test_number_calls_fitness_function_single_objective_stop_criteria_list(): + multi_objective_problem(multi_objective=True, + stop_criteria=['reach_10', 'reach_5']) + +def test_number_calls_fitness_function_single_objective_stop_criteria_list_decimal(): + multi_objective_problem(multi_objective=True, + stop_criteria=['reach_-1.5', 'reach_-2.8']) + +if __name__ == "__main__": + print() + test_number_calls_fitness_function_no_parallel_processing() + print() + + #### Thread-based Parallel Processing + test_number_calls_fitness_function_parallel_processing_thread_1() + print() + test_number_calls_fitness_function_parallel_processing_thread_2() + print() + test_number_calls_fitness_function_parallel_processing_thread_5() + print() + test_number_calls_fitness_function_parallel_processing_thread_5_patch_4() + print() + test_number_calls_fitness_function_parallel_processing_thread_5_patch_4_multi_objective() + print() + + #### Thread-based Parallel Processing + test_number_calls_fitness_function_parallel_processing_process_1() + print() + test_number_calls_fitness_function_parallel_processing_process_2() + print() + test_number_calls_fitness_function_parallel_processing_process_5() + print() + test_number_calls_fitness_function_parallel_processing_process_5_patch_4() + print() + test_number_calls_fitness_function_parallel_processing_process_5_patch_4_multi_objective() + print() + + #### Multi-Objective Stop Criteria + test_number_calls_fitness_function_multi_objective_stop_criteria_str_single_value() + print() + test_number_calls_fitness_function_multi_objective_stop_criteria_str() + print() + test_number_calls_fitness_function_multi_objective_stop_criteria_str_decimal() + print() + test_number_calls_fitness_function_multi_objective_stop_criteria_list() + print() + test_number_calls_fitness_function_multi_objective_stop_criteria_list_decimal() + print() + + #### Single-Objective Stop Criteria + test_number_calls_fitness_function_single_objective_stop_criteria_str() + print() + test_number_calls_fitness_function_single_objective_stop_criteria_str_decimal() + print() + test_number_calls_fitness_function_single_objective_stop_criteria_list() + print() + test_number_calls_fitness_function_single_objective_stop_criteria_list_decimal() + print() +