Steps to Use pygad

To use the pygad module, here is a summary of the required steps:

  1. Prepare the fitness_func parameter.

  2. Prepare the other parameters.

  3. Import pygad.

  4. Create an instance of the pygad.GA class.

  5. Run the genetic algorithm.

  6. Plot the results.

  7. Get information about the best solution.

  8. Save and load the results.

The next sections explain each step.

Preparing the fitness_func Parameter

Some steps in the genetic algorithm work the same way for every problem, but the fitness calculation does not. There is no single way to calculate the fitness value, and it changes from one problem to another.

PyGAD has a parameter called fitness_func that lets you pass your own function or method to calculate the fitness. This function must be a maximization function, so a solution with a higher fitness value is treated as better than a solution with a lower value.

The fitness function is where the user can decide whether the optimization problem is single-objective or multi-objective.

  • If the fitness function returns a numeric value, then the problem is single-objective. The numeric data types supported by PyGAD are listed in the supported_int_float_types variable of the pygad.GA class.

  • If the fitness function returns a list, tuple, or numpy.ndarray, then the problem is multi-objective. Even if there is only one element, the problem is still considered multi-objective. Each element represents the fitness value of its corresponding objective.

A user-defined fitness function lets you use PyGAD to solve any problem by passing the right fitness function. It is very important to understand the problem well before you write the fitness function.

Here is an example:

Given the following function: y = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + w6x6 where (x1,x2,x3,x4,x5,x6)=(4, -2, 3.5, 5, -11, -4.7) and y=44 What are the best values for the 6 weights (w1 to w6)? We are going to use the genetic algorithm to optimize this function.

So, the task is to use the genetic algorithm to find the best values for the 6 weights w1 to w6. The best solution is the one whose output is closest to the desired output y=44. So, the fitness function should return a higher value when the solution’s output is closer to y=44. Here is a function that does that:

function_inputs = [4, -2, 3.5, 5, -11, -4.7] # Function inputs.
desired_output = 44 # Function output.

def fitness_func(ga_instance, solution, solution_idx):
    output = numpy.sum(solution*function_inputs)
    fitness = 1.0 / numpy.abs(output - desired_output)
    return fitness

Because the fitness function returns a numeric value, the problem is single-objective.

Such a user-defined function must accept 3 parameters:

  1. The instance of the pygad.GA class. This helps the user to fetch any property that helps when calculating the fitness.

  2. The solution(s) to calculate the fitness value(s). Note that the fitness function can accept multiple solutions only if the fitness_batch_size is given a value greater than 1.

  3. The indices of the solutions in the population. The number of indices also depends on the fitness_batch_size parameter.

If a method is passed to the fitness_func parameter, then it accepts a fourth parameter representing the method’s instance.

The __code__ object is used to check that this function accepts the required number of parameters. If more or fewer parameters are passed, an exception is raised.

By writing this function, you have completed a very important step toward using PyGAD.

Preparing Other Parameters

Here is an example for preparing the other parameters:

num_generations = 50
num_parents_mating = 4

fitness_function = fitness_func

sol_per_pop = 8
num_genes = len(function_inputs)

init_range_low = -2
init_range_high = 5

parent_selection_type = "sss"
keep_parents = 1

crossover_type = "single_point"

mutation_type = "random"
mutation_percent_genes = 10

The on_generation Parameter

The optional on_generation parameter lets you call a function (with a single parameter) after each generation. Here is a simple function that prints the current generation number and the fitness value of the best solution in the current generation. The generations_completed attribute of the GA class returns the number of the last completed generation.

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.

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:

import pygad

The pygad.GA class holds the implementation of all methods for running the genetic algorithm.

Create an Instance of the pygad.GA Class

The pygad.GA class is instantiated where the previously prepared parameters are fed to its constructor. The constructor is responsible for creating the initial population.

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:

ga_instance.run()

Inside this method, the genetic algorithm evolves over the generations by doing the following tasks:

  1. Calculate the fitness values of the solutions in the current population.

  2. Select the best solutions as parents in the mating pool.

  3. Apply the crossover and mutation operations.

  4. Repeat the process for the given number of generations.

Plotting Results

There is a method named plot_fitness() which creates a figure summarizing how the fitness values of the solutions change with the generations.

ga_instance.plot_fitness()

Fig02

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

solution, solution_fitness, solution_idx = ga_instance.best_solution()
print(f"Parameters of the best solution : {solution}")
print(f"Fitness value of the best solution = {solution_fitness}")
print(f"Index of the best solution : {solution_idx}")

Using the best_solution_generation attribute of the pygad.GA instance, you can get the generation number at which the best fitness was reached.

if ga_instance.best_solution_generation != -1:
    print(f"Best fitness value reached after {ga_instance.best_solution_generation} generations.")

Saving & Loading the Results

After the run() method completes, it is possible to save the current instance of the genetic algorithm to avoid losing the progress made. The save() method is available for that purpose. Just pass the file name to it without an extension. According to the next code, a file named genetic.pkl will be created and saved in the current directory.

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.

loaded_ga_instance = pygad.load(filename=filename)

After the instance is loaded, you can use it to run any method or access any property.

print(loaded_ga_instance.best_solution())