Tutorial: Solving your own problems with EasyGA - danielwilczak101/EasyGA GitHub Wiki
Below is a complete tutorial on how to use EasyGA to solve your own problems. Further customization is discussed in the rest of the wiki.
In this tutorial, we will learn how to
- use EasyGA's data structure.
- encode a problem into a fitness function.
- select methods appropriate for the problem.
- run the GA and show the results.
Problem Statement
The below tutorial uses the assignment problem:
There is a collection of tasks that need to be completed and a collection of workers that can complete tasks. The amount of tasks and workers are the same. However, tasks may be more expensive to assign to some workers than others, and each worker can complete at most one task. Find which task should be assigned to each worker so that the least amount of cost is incurred.
Data Structure
from EasyGA import GA
# create ga to run example
ga = GA()
# initialize population
ga.evolve(1)
# show the population
print(ga.population)
# show the best chromosome
print(ga.population[0])
# show its first gene
print(ga.population[0][0])
# show that gene's value
print(ga.population[0][0].value)
Genetic algorithms use array-like data structures. The GA contains a population, which is a list of chromosomes, and each chromosome is a list of genes. Each gene contains a .value
attribute, which may be numeric, string, or otherwise, depending on the nature of your problem.
Looping over the data
from EasyGA import GA
# create ga to run example
ga = GA()
# initialize population
ga.evolve(1)
# get the best chromosome
chromosome = ga.population[0]
# loop over the chromosome
for gene in chromosome:
print(gene)
# loop over gene values
for value in chromosome.gene_value_iter:
print(value)
Populations and chromosomes may also be looped over. Chromosomes additionally have the .gene_value_iter
and .gene_value_list
properties, which allow direct looping over their genes' values.
Initializing chromosomes
import random
from EasyGA import GA
# Inherit the GA class to modify its methods
class Assignment_Problem_GA(GA):
# initialize the ga with the costs matrix
def __init__(ga, costs):
super().__init__()
ga.costs = costs
# initialize a random chromosome
def chromosome_impl(ga):
# shuffle tasks around
chromosome = list(range(len(ga.costs)))
random.shuffle(chromosome)
return chromosome
When the GA needs to create new chromosomes, the chromosome_impl
method is called. A list of values should be returned to create the new chromosome. In the above example, a random shuffle is used to assign tasks to workers randomly.
Fitness Functions
import random
from EasyGA import GA
class Assignment_Problem_GA(GA):
def __init__(ga, costs):
super().__init__()
ga.costs = costs
# our goal is to minimize the costs
ga.target_fitness_type = 'min'
def chromosome_impl(ga):
chromosome = list(range(len(ga.costs)))
random.shuffle(chromosome)
return chromosome
# the "value" of a chromosome is the total costs
def fitness_function_impl(ga, chromosome):
total_cost = 0
# for each worker, get the cost of their task
for task_costs, task_chosen in zip(ga.costs, chromosome.gene_value_iter):
total_cost += task_costs[task_chosen]
return total_cost
# costs for worker 1 are [1, 2, 3] for each task
costs = [[1, 2, 3],
[3, 4, 7],
[8, 1, 3]]
# create the genetic algorithm
ga = Assignment_Problem_GA(costs)
To measure how good a chromosome is, a fitness function is called. In our Assignment Problem GA, we loop over each worker, getting the cost of the task they're assigned to. The total of these costs is the fitness of the chromosome.
Since we want to find the minimum cost, ga.target_fitness_type
is set to 'min'
in the __init__
. If we wanted the maximum cost instead, we could set ga.target_fitness_type
to 'max'
instead.
Making Appropriate Methods
import random
from EasyGA import GA
class Assignment_Problem_GA(GA):
def __init__(ga, costs):
super().__init__()
ga.costs = costs
ga.target_fitness_type = 'min'
def chromosome_impl(ga):
chromosome = list(range(len(ga.costs)))
random.shuffle(chromosome)
return chromosome
def fitness_function_impl(ga, chromosome):
total_cost = 0
for task_costs, task_chosen in zip(ga.costs, chromosome.gene_value_iter):
total_cost += task_costs[task_chosen]
return total_cost
# we don't need to combine chromosomes
def crossover_individual_impl(ga, parent1, parent2):
pass
# randomly swap assigned tasks between two workers
def mutation_individual_impl(ga, chromosome):
chromosome.fitness = None
index1, index2 = random.sample(range(len(chromosome)), 2)
chromosome[index1], chromosome[index2] = chromosome[index2], chromosome[index1]
costs = [[1, 2, 3],
[3, 4, 7],
[8, 1, 3]]
ga = Assignment_Problem_GA(costs)
Not all GA methods are appropriate for our specific problem. For example, we have to keep in mind that every task is assigned to only one worker. The methods related to chromosome modifications are the crossover_individual_impl
and mutation_individual_impl
methods.
The crossover_individual_impl
is for combining two chromosomes. This doesn't make much sense for our problem, so we do nothing.
The mutation_individual_impl
is for changing a chromosome. In the above example, we randomly swap two tasks.
Running the GA and Showing Results
ga.evolve()
ga.print_best_chromosome()
To run the ga
, we use ga.evolve()
. To show the results, we use ga.print_best_chromosome()
. The results are:
Best Chromosome : [2][0][1]
Best Fitness : 7
Looking back at the costs, we can see our end result was:
costs = [[1, 2, 3], # last task costs $3
[3, 4, 7], # first task costs $3
[8, 1, 3]] # second task costs $1
And this is indeed the solution to our problem!