Skip to content

Commit

Permalink
PyGAD 3.3.0
Browse files Browse the repository at this point in the history
Release Date 29 January 2024
1. Solve bugs when multi-objective optimization is used. #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. #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. #201
5. The population size can be changed during runtime by changing all the parameters that would affect the size of any thing used by the GA. For more information, check the [Change Population Size during Runtime](https://pygad.readthedocs.io/en/latest/pygad_more.html#change-population-size-during-runtime) section. #234
6. When a dictionary exists in the `gene_space` parameter without a step, then mutation occurs by adding a random value to the gene value. The random vaue is generated based on the 2 parameters `random_mutation_min_val` and `random_mutation_max_val`. For more information, check the [How Mutation Works with the gene_space Parameter?](https://pygad.readthedocs.io/en/latest/pygad_more.html#how-mutation-works-with-the-gene-space-parameter) section. #229
7. Add `object` as a supported data type for int (GA.supported_int_types) and float (GA.supported_float_types). #174
8. Use the `raise` clause instead of the `sys.exit(-1)` to terminate the execution. #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.
  • Loading branch information
ahmedfgad committed Jan 29, 2024
1 parent 1615baf commit f83c973
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 45 deletions.
4 changes: 2 additions & 2 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@
# -- Project information -----------------------------------------------------

project = 'PyGAD'
copyright = '2023, Ahmed Fawzy Gad'
copyright = '2024, Ahmed Fawzy Gad'
author = 'Ahmed Fawzy Gad'

# The full version, including alpha/beta/rc tags
release = '3.2.0'
release = '3.3.0'

master_doc = 'index'

Expand Down
108 changes: 104 additions & 4 deletions docs/source/pygad_more.rst
Original file line number Diff line number Diff line change
Expand Up @@ -344,10 +344,13 @@ is applied based on this parameter.
How Mutation Works with the ``gene_space`` Parameter?
-----------------------------------------------------

If a gene has its static 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.
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
Expand Down Expand Up @@ -377,6 +380,39 @@ 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
======================

Expand Down Expand Up @@ -663,6 +699,70 @@ Note that the 2 attributes (``self.best_solutions`` and
attributes (``self.solutions`` and ``self.solutions_fitness``) only work
if the ``save_solutions`` parameter is ``True``.

Change Population Size during Runtime
=====================================

Starting from `PyGAD
3.3.0 <https://pygad.readthedocs.io/en/latest/releases.html#pygad-3-3-0>`__,
the population size can 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 <https://pygad.readthedocs.io/en/latest/pygad.html#init>`__
and decide what to change.

1. ``population``: The population. It *must* be changed.

2. ``num_offspring``: The number of offspring to produce out of the
crossover and mutation operations. Change this parameter if the
number of offspring have to be changed to be consistent with the new
population size.

3. ``num_parents_mating``: The number of solutions to select as parents.
Change this parameter if the number of parents have to be changed to
be consistent with the new population size.

4. ``fitness_func``: If the way of calculating the fitness changes after
the new population size, then the fitness function have to be
changed.

5. ``sol_per_pop``: The number of solutions per population. It is not
critical to change it but it is recommended to keep this number
consistent with the number of solutions in the ``population``
parameter.

These are examples of the instance attributes that might be changed. The
user should check the `list of instance
attributes <https://pygad.readthedocs.io/en/latest/pygad.html#other-instance-attributes-methods>`__
and decide what to change.

1. All the ``last_generation_*`` parameters

1. ``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
=================================

Expand Down
73 changes: 60 additions & 13 deletions docs/source/releases.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1464,26 +1464,73 @@ Release Date 7 September 2023
class is removed. Instead, please use the ``plot_fitness()`` if you
did not upgrade yet.

.. _pygad-321:
.. _pygad-330:

PyGAD 3.2.1
PyGAD 3.3.0
-----------

Release Date ... 2023
Release Date 29 January 2024

1. Fix a bug when multi-objective optimization is used with batch
fitness calculation (e.g. ``fitness_batch_size`` set to a non-zero
number).
1. Solve bugs when multi-objective optimization is used.
https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/238

2. 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.
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

.. code:: python
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.

self.best_solution_generation = numpy.where(numpy.array(
self.best_solutions_fitness) == numpy.max(numpy.array(self.best_solutions_fitness)))[0][0]
4. Parallel processing now supports calculating the fitness during
adaptive mutation.
https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/201

5. The population size can be changed during runtime by changing all
the parameters that would affect the size of any thing used by the
GA. For more information, check the `Change Population Size during
Runtime <https://pygad.readthedocs.io/en/latest/pygad_more.html#change-population-size-during-runtime>`__
section.
https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/234

6. When a dictionary exists in the ``gene_space`` parameter without a
step, then mutation occurs by adding a random value to the gene
value. The random vaue is generated based on the 2 parameters
``random_mutation_min_val`` and ``random_mutation_max_val``. For
more information, check the `How Mutation Works with the gene_space
Parameter? <https://pygad.readthedocs.io/en/latest/pygad_more.html#how-mutation-works-with-the-gene-space-parameter>`__
section.
https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/229

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 Projects at GitHub
========================
Expand Down
87 changes: 63 additions & 24 deletions examples/example_dynamic_population_size.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,83 @@

"""
This is an example to dynamically change the population size (i.e. number of solutions/chromosomes per population) during runtime.
The following 2 instance attributes must be changed to meet the new desired population size:
1) population: This is a NumPy array holding the population.
2) num_offspring: This represents the number of offspring to produce during crossover.
For example, if the population initially has 20 solutions and 6 genes. To change it to have 30 solutions, then:
1)population: Create a new NumPy array with the desired size (30, 6) and assign it to the population instance attribute.
2)num_offspring: Set the num_offspring attribute accordingly (e.g. 29 assuming that keep_elitism has the default value of 1).
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.random.rand(), numpy.random.rand()]
return numpy.sum(solution)

def on_generation(ga_i):
# The population starts with 20 solutions.
print(ga_i.generations_completed, ga_i.num_offspring, ga_i.population.shape)
# At generation 15, increase the population size to 40 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.num_offspring = 49
new_population = numpy.zeros(shape=(ga_i.num_offspring+1, ga_i.population.shape[1]), dtype=ga_i.population.dtype)
new_population[:ga_i.population.shape[0], :] = ga_i.population
ga_i.population = new_population
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:
ga_i.num_offspring = 39
new_population = numpy.zeros(shape=(ga_i.num_offspring+1, ga_i.population.shape[1]), dtype=ga_i.population.dtype)
new_population[:ga_i.population.shape[0], :] = ga_i.population
ga_i.population = new_population
# At generation 10, increase the population size to 30 solutions.
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:
ga_i.num_offspring = 29
new_population = numpy.zeros(shape=(ga_i.num_offspring+1, ga_i.population.shape[1]), dtype=ga_i.population.dtype)
new_population[:ga_i.population.shape[0], :] = ga_i.population
ga_i.population = new_population
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,
parent_selection_type='nsga2')
on_generation=on_generation)

ga_instance.run()

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "pygad"
version = "3.2.0"
version = "3.3.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"
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setuptools.setup(
name="pygad",
version="3.2.0",
version="3.3.0",
author="Ahmed Fawzy Gad",
install_requires=["numpy", "matplotlib", "cloudpickle",],
author_email="ahmed.f.gad@gmail.com",
Expand Down

0 comments on commit f83c973

Please sign in to comment.