How To Train Keras Models Using the Genetic Algorithm with PyGAD | by Ahmed Gad | Towards Data Science

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.
Starting from PyGAD 2.8.0 (released on 20 September 2020), a new module called kerasga supports training 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.
You can also follow along with the code in this tutorial and run it for free on a Gradient Community Notebook from the ML Showcase.
The full tutorial outline is as follows:
Getting Started with PyGAD
pygad.kerasga Module
Steps to Train a Keras Model using PyGAD
Decide the Problem Type
Create a Keras Model
Instantiate the pygad.kerasga.KerasGA Class
Prepare the Training Data
Loss Function
Fitness Function
Generation Callback Function (Optional)
Create an Instance of the pygad.GA Class
Run the Genetic Algorithm
Fitness vs. Generation Plot
Statistics about the Trained Model
Complete Code for Regression
Complete Code for Classification using CNN
Let’s get started.
To start this tutorial, it is essential to install PyGAD. If you already have PyGAD installed, check the __version__ attribute to make sure at least PyGAD 2.8.0 is installed according to the next code.
import pygadprint(pygad.__version__)
Being available at PyPI (Python Package Index), then it can be installed using the pip installer. Make sure to install PyGAD 2.8.0 or higher.
pip install pygad>=2.8.0
Once installed, then you are ready to start. Read the documentation at Read the Docs: pygad.readthedocs.io. The documentation includes some examples.
The next code solves a simple problem to optimize the parameters of a linear model.
import pygadimport numpyfunction_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 fitnessnum_generations = 100num_parents_mating = 10sol_per_pop = 20num_genes = len(function_inputs)ga_instance = pygad.GA(num_generations=num_generations,                       num_parents_mating=num_parents_mating,                       fitness_func=fitness_func,                       sol_per_pop=sol_per_pop,                       num_genes=num_genes)ga_instance.run()ga_instance.plot_result()
Starting from PyGAD 2.8.0, a new module named kerasga is introduced. Its name is short for Keras Genetic Algorithm. The module offers the following functions:
Build the initial population of solutions using the KerasGA class. Each solution holds all the parameters in the Keras model.
Represent the Keras model’s parameters as a chromosome (i.e. 1D vector) using the model_weights_as_vector() function.
Restore the Keras model’s parameters from the chromosome using the model_weights_as_matrix() function.
The pygad.kerasga module has a class named KerasGA. The constructor of this class accepts 2 parameters:
model: The Keras model.num_solutions: Number of solutions in the population.
model: The Keras model.
num_solutions: Number of solutions in the population.
Based on those 2 parameters, the pygad.kerasga.KerasGA class creates 3 instance attributes:
model: A reference to the Keras model.num_solutions: Number of solutions in the population.population_weights: A nested list holding the model parameters. This list is updated after each generation.
model: A reference to the Keras model.
num_solutions: Number of solutions in the population.
population_weights: A nested list holding the model parameters. This list is updated after each generation.
Assuming that the Keras model is saved into the model variable, the next code creates an instance of the KerasGA class and saves it into the keras_ga variable. The num_solutions argument is assigned the value 10 which means the population has 10 solutions.
The constructor creates a list of length equal to the value of the num_solutions argument. Each element in the list holds different values for the model's parameters after being converted into a 1D vector using the model_weights_as_vector() function.
Based on the instance of the KerasGA class, the initial population can be returned from the population_weights attribute. Assuming the model has 60 parameters and there are 10 solutions, then the shape of the initial population is 10x60.
import pygad.kerasgakeras_ga = pygad.kerasga.KerasGA(model=model,                                 num_solutions=10)initial_population = keras_ga.population_weights
The next section summarizes the steps to train a Keras model using PyGAD. Each of these steps will be discussed in a separate section.
The steps to train a Keras model using PyGAD are summarized as follows:
Decide the Problem Type
Create a Keras Model
Instantiate the pygad.kerasga.KerasGA Class
Prepare the Training Data
Loss Function
Fitness Function
Generation Callback Function (Optional)
Create an Instance of the pygad.GA Class
Run the Genetic Algorithm
The next subsections discuss each of these steps.
The problem type (either classification or regression) helps to prepare the following:
Loss function (which is used to build the fitness function).Output layer in the Keras model.Training data.
Loss function (which is used to build the fitness function).
Output layer in the Keras model.
Training data.
For a regression problem, the loss function could be the mean absolute error, mean square error, or another function as listed on this page which summarizes the Keras loss functions for regression: keras.io/api/losses/regression_losses.
For a classification problem, the loss function can be binary cross-entropy (for binary classification), categorical cross-entropy (for multi-class problems), or another function as listed in this page which summarizes the Keras classification loss functions: keras.io/api/losses/probabilistic_losses.
The activation function in the output layer differs based on whether the problem is classification or regression. For a classification problem, it might be softmax compared to linear for regression.
If the problem is regression, then the output of each sample is a continuous number compared to a class label in classification problems.
As a summary, it is critical to decide the type of the problem so that the training data and loss function are selected properly.
There are 3 ways to build a Keras model:
Sequential ModelFunctional APIModel Subclassing
Sequential Model
Functional API
Model Subclassing
PyGAD supports building a Keras model using both the Sequential Model and Functional API.
For the Sequential Model case, here is an example of building a Keras model. Simply, create each layer using the tensorflow.keras.layers module. Then, create an instance of the tensorflow.keras.Sequential class. Finally, use the add() method to add the layers into the model.
import tensorflow.kerasinput_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)
Note that the output layer’s activation function is linear which means the problem is regression. For a classification problem, the function can be softmax. In the next line the output layer has 2 neurons (1 for each class) and it uses the softmax activation function.
output_layer = tensorflow.keras.layers.Dense(2, activation="linear")
For the Functional API case, each layer is created normally as the Sequential Model case. Each layer, otherwise the input layer, is used as a function that accepts the preceding layer as an argument. Finally, an instance of the tensorflow.keras.Model class is created which accepts the input and output layers as arguments.
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)
After the Keras model is created, the next step is to create an initial population of Keras model’s parameters using the KerasGA class.
By creating an instance of the pygad.kerasga.KerasGA class, then an initial population of the Keras model's parameters is created. The next code passes the Keras model created in the previous section to the model argument of the KerasGA class constructor.
import pygad.kerasgakeras_ga = pygad.kerasga.KerasGA(model=model,                                 num_solutions=10)
he next section creates the training data used to train the Keras model.
Based on the type of problem (classification or regression), the training data is prepared.
For a regression problem with 1 output, here is a randomly generated training data where each sample has 3 inputs.
import numpy​# Data inputsdata_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 outputsdata_outputs = numpy.array([[0.1],                            [0.6],                            [1.3],                            [2.5]])
For a binary classification problem like XOR, here is its training data. Each sample has 2 inputs. The outputs are prepared so that the output layer has 2 neurons, 1 for each class.
import numpy​# XOR problem inputsdata_inputs = numpy.array([[0, 0],                           [0, 1],                           [1, 0],                           [1, 1]])​# XOR problem outputsdata_outputs = numpy.array([[1, 0],                            [0, 1],                            [0, 1],                            [1, 0]])
The next section discusses the loss function for regression and classification problems.
The loss function differs based on the problem type. This section discusses some loss functions in the tensorflow.keras.losses module of Keras for the regression and classification problems.
For a regression problem, the loss functions include:
tensorflow.keras.losses.MeanAbsoluteError()
tensorflow.keras.losses.MeanSquaredError()
Check this page for more information.
Here is an example that calculates the mean absolute error where y_true and y_pred represent the true and predicted outputs.
mae = tensorflow.keras.losses.MeanAbsoluteError()loss = mae(y_true, y_pred).numpy()
For a classification problem, the loss functions include:
tensorflow.keras.losses.BinaryCrossentropy(): Binary classification.
tensorflow.keras.losses.CategoricalCrossentropy(): Multi-class classification.
Check this page for more information.
Here is an example of calculating the binary class entropy:
bce = tensorflow.keras.losses.BinaryCrossentropy()loss = bce(y_true, y_pred).numpy()
Based on the loss function, the fitness function is prepared according to the next section.
The loss functions for either the classification or regression problems are minimization functions. The fitness functions for the genetic algorithm are maximization ones. So, the fitness value is calculated as the reciprocal of the loss value.
fitness_value = 1.0 / loss
The steps used to calculate the fitness value of the model are as follows:
Restore the model parameters from the 1D vector.Set the model parameters.Make predictions.Calculate the loss value.Calculate the fitness value.Return the fitness value.
Restore the model parameters from the 1D vector.
Set the model parameters.
Make predictions.
Calculate the loss value.
Calculate the fitness value.
Return the fitness value.
The next code builds the complete fitness function that works with PyGAD for a regression problem. The fitness function in PyGAD is a regular Python function that must accept 2 arguments. The first one represents the solution to which the fitness value is to be calculated. The other argument is the index of the solution within the population which may be useful in some cases.
The solution passed to the fitness function is a 1D vector. To restore the Keras model’s parameters from this vector, the pygad.kerasga.model_weights_as_matrix() is used.
model_weights_matrix = pygad.kerasga.model_weights_as_matrix(model=model, weights_vector=solution)
Once the parameters are restored, then they are used as the model’s current parameters by the set_weights() method.
model.set_weights(weights=model_weights_matrix)
Based on the current parameters, the model predicts the outputs using the predict() method.
predictions = model.predict(data_inputs)
The predicted outputs are used to calculate the loss value. The mean absolute error is used as a loss function.
mae = tensorflow.keras.losses.MeanAbsoluteError()
Because the loss value may be 0.0, then it is preferred to add a small value to it like 0.00000001 to avoid diving by zero while calculating the fitness value.
solution_fitness = 1.0 / (mae(data_outputs, predictions).numpy() + 0.00000001)
Finally, the fitness value is returned.
def fitness_func(solution, sol_idx):    global data_inputs, data_outputs, keras_ga, model​    model_weights_matrix = pygad.kerasga.model_weights_as_matrix(model=model,                                                                 weights_vector=solution)​    model.set_weights(weights=model_weights_matrix)​    predictions = model.predict(data_inputs)        mae = tensorflow.keras.losses.MeanAbsoluteError()    solution_fitness = 1.0 / (mae(data_outputs, predictions).numpy() + 0.00000001)​    return solution_fitness
For a binary classification problem, here is a fitness function that works with PyGAD. It calculates the binary cross-entropy assuming that the classification problem is binary.
def fitness_func(solution, sol_idx):    global data_inputs, data_outputs, keras_ga, model​    model_weights_matrix = pygad.kerasga.model_weights_as_matrix(model=model,                                                                 weights_vector=solution)​    model.set_weights(weights=model_weights_matrix)​    predictions = model.predict(data_inputs)        bce = tensorflow.keras.losses.BinaryCrossentropy()    solution_fitness = 1.0 / (bce(data_outputs, predictions).numpy() + 0.00000001)​    return solution_fitness
The next section builds a callback function executed at the end of each generation.
For each generation, the genetic algorithm makes changes over the solutions. A callback function can be called after each generation completes to calculate some statistics about the latest parameters reached.
This step is optional and for debugging purposes only.
The generation callback function is implemented below. In PyGAD, this callback function must accept a parameter referring to the instance of the genetic algorithm by which the current population can be fetched using the population attribute.
In this function, some information is printed like the current generation number and the fitness value of the best solution. Such information keeps the user updated by the progress of the genetic algorithm.
def callback_generation(ga_instance):    print("Generation = {generation}".format(generation=ga_instance.generations_completed))    print("Fitness    = {fitness}".format(fitness=ga_instance.best_solution()[1]))
The next step towards training a Keras model using PyGAD is to create an instance of the pygad.GA class. The constructor of this class accepts many arguments that can be explored at the documentation.
The next code instantiates the pygad.GA class by passing using the minimum arguments in this application which are:
num_generations: Number of generations.
num_parents_mating: Number of parents to mate.
initial_population: The initial population of Keras model's parameters.
fitness_func: The fitness function.
on_generation: The generation callback function.
Note that the number of solutions within the population was previously set to 10 in the constructor of the KerasGA class. Thus, the number of parents to mate must be less than 10.
num_generations = 250num_parents_mating = 5initial_population = keras_ga.population_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)
The next section runs the genetic algorithm to start training the Keras model.
The instance of the pygad.GA class runs by calling the run() method.
ga_instance.run()
By executing this method, the lifecycle of PyGAD starts according to the next figure.
The next section discusses how to draw some conclusions about the trained model.
Using the plot_result() method in the pygad.GA class, PyGAD creates a figure that shows how the fitness value changes by generation.
ga_instance.plot_result(title="PyGAD & Keras - Iteration vs. Fitness", linewidth=4)
The pygad.GA class has a method called best_solution() which returns 3 outputs:
Best solution found.Fitness value of the best solution.The index of the best solution within the population.
Best solution found.
Fitness value of the best solution.
The index of the best solution within the population.
The next code calls the best_solution() method and prints information about 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))
The next code restores the Keras model’s weights from the best solution. Based on the restored weights, the model predicts the outputs of the training samples. You can also predict the outputs of new samples.
# Fetch the parameters of the best solution.best_solution_weights = pygad.kerasga.model_weights_as_matrix(model=model,                                                              weights_vector=solution)model.set_weights(best_solution_weights)predictions = model.predict(data_inputs)print("Predictions : \n", predictions)
Given that the loss function used is the mean absolute error, the next code calculates it.
mae = tensorflow.keras.losses.MeanAbsoluteError()abs_error = mae(data_outputs, predictions).numpy()print("Absolute Error : ", abs_error)
The next sections list the complete code to build and train Keras models using PyGAD.
For a regression problem that uses the mean absolute error as a loss function, here is its complete code.
import tensorflow.kerasimport pygad.kerasgaimport numpyimport pygad​def fitness_func(solution, sol_idx):    global data_inputs, data_outputs, keras_ga, model​    model_weights_matrix = pygad.kerasga.model_weights_as_matrix(model=model,                                                                 weights_vector=solution)​    model.set_weights(weights=model_weights_matrix)​    predictions = model.predict(data_inputs)​    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)​weights_vector = pygad.kerasga.model_weights_as_vector(model=model)​keras_ga = pygad.kerasga.KerasGA(model=model,                                 num_solutions=10)​# Data inputsdata_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 outputsdata_outputs = numpy.array([[0.1],                            [0.6],                            [1.3],                            [2.5]])​num_generations = 250num_parents_mating = 5initial_population = keras_ga.population_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_result(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))​# Fetch the parameters of the best solution.best_solution_weights = pygad.kerasga.model_weights_as_matrix(model=model,                                                              weights_vector=solution)model.set_weights(best_solution_weights)predictions = model.predict(data_inputs)print("Predictions : \n", predictions)​mae = tensorflow.keras.losses.MeanAbsoluteError()abs_error = mae(data_outputs, predictions).numpy()print("Absolute Error : ", abs_error)
After the code completes, the next figure shows that the fitness value is increasing which is a good sign as the Keras model is learning properly.
Here are some more details about the trained model. Note that the predicted values are close to the right ones. The MAE is 0.018.
Fitness value of the best solution = 54.79189095217631Index of the best solution : 0Predictions : [[0.11471477] [0.6034051 ] [1.3416876 ] [2.486804  ]]Absolute Error :  0.018250866
The next code builds a convolutional neural network using Keras for classifying a dataset of 80 images where the size of each image is 100x100x3. Note that the categorical cross-entropy is used because the dataset has 4 classes.
The training data can be downloaded from these links:
dataset_inputs.npy: https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_inputs.npydataset_outputs.npy: https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_outputs.npy
dataset_inputs.npy: https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_inputs.npy
dataset_outputs.npy: https://github.com/ahmedfgad/NumPyCNN/blob/master/dataset_outputs.npy
import tensorflow.kerasimport pygad.kerasgaimport numpyimport pygad​def fitness_func(solution, sol_idx):    global data_inputs, data_outputs, keras_ga, model​    model_weights_matrix = pygad.kerasga.model_weights_as_matrix(model=model,                                                                 weights_vector=solution)​    model.set_weights(weights=model_weights_matrix)​    predictions = model.predict(data_inputs)​    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)​keras_ga = pygad.kerasga.KerasGA(model=model,                                 num_solutions=10)​# Data inputsdata_inputs = numpy.load("dataset_inputs.npy")​# Data outputsdata_outputs = numpy.load("dataset_outputs.npy")data_outputs = tensorflow.keras.utils.to_categorical(data_outputs)​num_generations = 200num_parents_mating = 5initial_population = keras_ga.population_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()​ga_instance.plot_result(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))​# Fetch the parameters of the best solution.best_solution_weights = pygad.kerasga.model_weights_as_matrix(model=model,                                                              weights_vector=solution)model.set_weights(best_solution_weights)predictions = model.predict(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)
The next figure shows how the fitness value evolves by generation. As long as the fitness value increases, then increase the number of generations to achieve better accuracy.
Here is some information about the trained model.
Fitness value of the best solution = 2.7462310258668805Categorical Crossentropy :  0.3641354Accuracy :  0.75
This article was originally published on the Paperspace blog. You can run the code for my tutorials for free on Gradient.
This tutorial discussed how to train Keras models using the genetic algorithm using a Python 3 library called PyGAD. The Keras models can be created using the Sequential Model or the Functional API.
Using the pygad.kerasga module, an initial population of Keras model's weights is created where each solution holds a different set of weights for the model. This population is later evolved according to the lifecycle of PyGAD until all the generations complete.
Due to the high-speed nature of TensorFlow, which is the backend of Keras, PyGAD can train complex architectures in a reasonable time.