Added more structure methods and some quality of life changes

Overall cleaned up a lot of comments.

EasyGA:
- Code cleanup.

Population:
- Added sort_by_best_fitness
- Added parent/mating pool methods.
- Renamed some methods for consistency.

Chromosome:
- Added get_gene(index).

Parent Selection:
- Improved selection methods to use the ga.selection_probability so that the roulette selection actually works well.
- Added stochastic selection.

Survivor Selection:
- Added fill_in_random and fill_in_parents_then_random.

Crossover/Mutation:
- Cleaned up code.
This commit is contained in:
SimpleArt
2020-10-13 12:48:20 -04:00
parent 5e6f9b0427
commit fb213f04dd
8 changed files with 223 additions and 77 deletions

View File

@ -10,11 +10,11 @@ from fitness_function import Fitness_Examples
from initialization import Initialization_Methods
from termination_point import Termination_Methods
# Population Methods
from survivor_selection import Survivor_Selection
# Parent/Survivor Selection Methods
from parent_selection import Parent_Selection
from survivor_selection import Survivor_Selection
# Manipulation Methods
# Genetic Operator Methods
from mutation import Mutation_Methods
from crossover import Crossover_Methods
@ -23,25 +23,24 @@ class GA:
def __init__(self):
"""Initialize the GA."""
# Initilization variables
self.chromosome_length = 10
self.population_size = 10
self.chromosome_impl = None
self.gene_impl = [random.randint,1,10]
self.population = None
self.chromosome_length = 10
self.population_size = 10
self.chromosome_impl = None
self.gene_impl = [random.randint,1,10]
self.population = None
self.target_fitness_type = 'maximum'
self.update_fitness = True
self.update_fitness = True
# Selection variables
self.parent_ratio = 0.1
self.parent_ratio = 0.1
self.selection_probability = 0.95
self.tournament_size_ratio = 0.1
# Termination variables
self.current_generation = 0
self.current_fitness = 0
self.generation_goal = 15
self.fitness_goal = 9
self.current_fitness = 0
self.generation_goal = 15
self.fitness_goal = 9
# Mutation variables
self.mutation_rate = 0.10
@ -53,13 +52,11 @@ class GA:
self.make_chromosome = create_chromosome
self.make_gene = create_gene
# Selects which chromosomes should be automaticly moved to the next population
self.survivor_selection_impl = Survivor_Selection.fill_in_best
# Methods for accomplishing parent-selection -> Crossover -> Mutation
# Methods for accomplishing Parent-Selection -> Crossover -> Survivor_Selection -> Mutation
self.parent_selection_impl = Parent_Selection.Tournament.with_replacement
self.crossover_individual_impl = Crossover_Methods.Individual.single_point_crossover
self.crossover_population_impl = Crossover_Methods.Population.random_selection
self.survivor_selection_impl = Survivor_Selection.fill_in_best
self.mutation_individual_impl = Mutation_Methods.Individual.single_gene
self.mutation_population_impl = Mutation_Methods.Population.random_selection
@ -69,57 +66,63 @@ class GA:
def evolve_generation(self, number_of_generations = 1, consider_termination = True):
"""Evolves the ga the specified number of generations."""
while(number_of_generations > 0 and (consider_termination == False or self.termination_impl(self))):
# Evolve the specified number of generations
# and if consider_termination flag is set then
# also check if termination conditions reached
while(number_of_generations > 0 and (not consider_termination or self.termination_impl(self))):
# If its the first generation then initialize the population
if self.current_generation == 0:
self.initialize_population()
self.set_all_fitness(self.population.chromosome_list)
self.population.set_all_chromosomes(self.sort_by_best_fitness(self.population.get_all_chromosomes()))
self.set_all_fitness()
self.population.sort_by_best_fitness(self)
# Otherwise evolve the population
else:
self.set_all_fitness(self.population.chromosome_list)
self.population.reset_mating_pool()
self.set_all_fitness()
self.population.set_all_chromosomes(self.sort_by_best_fitness(self.population.get_all_chromosomes()))
self.parent_selection_impl(self)
next_population = self.crossover_population_impl(self)
next_population = self.survivor_selection_impl(self, next_population)
self.population = next_population
self.survivor_selection_impl(self, next_population)
self.mutation_population_impl(self)
self.set_all_fitness(self.population.chromosome_list)
self.population.set_all_chromosomes(self.sort_by_best_fitness(self.population.get_all_chromosomes()))
number_of_generations -= 1
self.current_generation += 1
def evolve(self):
"""Runs the ga until the termination point has been satisfied."""
# While the termination point hasnt been reached keep running
while(self.active()):
self.evolve_generation()
def active(self):
"""Returns if the ga should terminate base on the termination implimented"""
# Send termination_impl the whole ga class
"""Returns if the ga should terminate based on the termination implimented."""
return self.termination_impl(self)
def initialize_population(self):
"""Initialize the population using the initialization
implimentation that is currently set
"""Initialize the population using
the initialization implimentation
that is currently set.
"""
self.population = self.initialization_impl(self)
def set_all_fitness(self, chromosome_set):
def set_all_fitness(self):
"""Will get and set the fitness of each chromosome in the population.
If update_fitness is set then all fitness values are updated.
Otherwise only fitness values set to None (i.e. uninitialized
fitness values) are updated."""
# Get each chromosome in the population
fitness values) are updated.
"""
for chromosome in chromosome_set:
if(chromosome.fitness == None or self.update_fitness == True):
# Set the chromosomes fitness using the fitness function
# Check each chromosome
for chromosome in self.population.get_all_chromosomes():
# Update fitness if needed or asked by the user
if(chromosome.get_fitness() is None or self.update_fitness):
chromosome.set_fitness(self.fitness_function_impl(chromosome))

View File

@ -6,17 +6,22 @@ class Crossover_Methods:
"""Methods for selecting chromosomes to crossover"""
def sequential_selection(ga):
"""Select sequential pairs from the mating pool"""
"""Select sequential pairs from the mating pool.
Every parent is paired with the previous parent.
The first parent is paired with the last parent.
"""
mating_pool = ga.population.mating_pool
return ga.make_population([ga.crossover_individual_impl(ga, mating_pool[index], mating_pool[index+1]) for index in range(len(mating_pool)-1)])
mating_pool = ga.population.get_mating_pool()
return ga.make_population([ga.crossover_individual_impl(ga, mating_pool[index], mating_pool[index-1]) for index in range(len(mating_pool))])
def random_selection(ga):
"""Select random pairs from the mating pool"""
"""Select random pairs from the mating pool.
Every parent is paired with a random parent.
"""
mating_pool = ga.population.mating_pool
return ga.make_population([ga.crossover_individual_impl(ga, random.choice(mating_pool), random.choice(mating_pool)) for n in mating_pool])
mating_pool = ga.population.get_mating_pool()
return ga.make_population([ga.crossover_individual_impl(ga, parent, random.choice(mating_pool)) for parent in mating_pool])
class Individual:

View File

@ -13,7 +13,7 @@ class Mutation_Methods:
# Randomly apply mutations
if random.uniform(0, 1) < ga.mutation_rate:
ga.population.set_chromosome(ga.mutation_individual_impl(ga, ga.population.get_all_chromosomes()[index]), index)
ga.population.set_chromosome(ga.mutation_individual_impl(ga, ga.population.get_chromosome(index)), index)
class Individual:

View File

@ -10,26 +10,40 @@ class Parent_Selection:
The total number of parents selected is determined by parent_ratio, an attribute to the GA object.
"""
# Error if can't select parents
if ga.selection_probability <= 0:
print("Selection probability must be greater than 0 to select parents.")
return
# Make sure the population is sorted by fitness
ga.population.sort_by_best_fitness(ga)
# Choose the tournament size.
# Use no less than 5 chromosomes per tournament.
tournament_size = int(ga.population.size()*ga.tournament_size_ratio)
if tournament_size < 5:
tournament_size = 5
# Probability used for determining if a chromosome should enter the mating pool.
selection_probability = ga.selection_probability
# Repeat tournaments until the mating pool is large enough.
while (len(ga.population.mating_pool) < ga.population.size()*ga.parent_ratio):
while (len(ga.population.get_mating_pool()) < ga.population.size()*ga.parent_ratio):
# Generate a random tournament group and sort by fitness.
tournament_group = ga.sort_by_best_fitness([random.choice(ga.population.get_all_chromosomes()) for n in range(tournament_size)])
tournament_group = sorted([random.randint(0, ga.population.size()-1) for n in range(tournament_size)])
# For each chromosome, add it to the mating pool based on its rank in the tournament.
for index in range(tournament_size):
# Probability required is selection_probability * (1-selection_probability) ^ (tournament_size-index+1)
# Probability required is selection_probability * (1-selection_probability) ^ index
# e.g. top ranked fitness has probability: selection_probability
# second ranked fitness has probability: selection_probability * (1-selection_probability)
# third ranked fitness has probability: selection_probability * (1-selection_probability)^2
# etc.
if random.uniform(0, 1) < selection_probability * pow(1-selection_probability, index):
ga.population.mating_pool.append(tournament_group[index])
if random.uniform(0, 1) < ga.selection_probability * pow(1-ga.selection_probability, index):
ga.population.set_parent(tournament_group[index])
# Stop if parent ratio reached
if len(ga.population.get_mating_pool()) >= ga.population.size()*ga.parent_ratio:
break
class Roulette:
@ -40,27 +54,74 @@ class Parent_Selection:
that it will be selected. Using the example of a casino roulette wheel.
Where the chromosomes are the numbers to be selected and the board size for
those numbers are directly proportional to the chromosome's current fitness. Where
the ball falls is a randomly generated number between 0 and 1"""
the ball falls is a randomly generated number between 0 and 1."""
# Make sure the population is sorted by fitness
ga.population.sort_by_best_fitness(ga)
# Error if can't select parents
if ga.selection_probability <= 0:
print("Selection probability must be greater than 0 to select parents.")
return
# Error if not all chromosomes has positive fitness
if (ga.population.get_chromosome(0).get_fitness() == 0 or ga.population.get_chromosome(-1).get_fitness() < 0):
print("Error using roulette selection, all fitnesses must be positive.")
print("Consider using stockastic roulette selection or tournament selection.")
return
# The sum of all the fitnessess in a population
total_fitness = sum(ga.population.chromosome_list[i].get_fitness() for i in range(len(ga.population.chromosome_list)))
rel_fitnesses = []
# A list of each chromosome's relative chance of getting chosen
for chromosome in ga.population.chromosome_list:
if (total_fitness != 0):
rel_fitnesses.append(float(chromosome.fitness)/total_fitness)
fitness_sum = sum(chromosome.get_fitness() for chromosome in ga.population.get_all_chromosomes())
# A list of ranges that represent the probability of a chromosome getting chosen
probability = [sum(rel_fitnesses[:i+1]) for i in range(len(rel_fitnesses))]
probability = [ga.selection_probability]
# The chance of being selected increases incrementally
for chromosome in ga.population.chromosome_list:
probability.append(probability[-1]+chromosome.fitness/fitness_sum)
probability = probability[1:]
# Loops until it reaches a desired mating pool size
while (len(ga.population.mating_pool) < len(ga.population.get_all_chromosomes())*ga.parent_ratio):
while (len(ga.population.get_mating_pool()) < ga.population.size()*ga.parent_ratio):
# Spin the roulette
rand_number = random.random()
# Loop through the list of probabilities
for i in range(len(probability)):
# If the probability is greater than the random_number, then select that chromosome
if (probability[i] >= rand_number):
ga.population.mating_pool.append(ga.population.chromosome_list[i])
# Find where the roulette landed.
for index in range(len(probability)):
if (probability[index] >= rand_number):
ga.population.set_parent(index)
break
def stochastic_selection(ga):
"""Stochastic roulette selection works based off of how strong the fitness is of the
chromosomes in the population. The stronger the fitness the higher the probability
that it will be selected. Instead of dividing the fitness by the sum of all fitnesses
and incrementally increasing the chance something is selected, the stochastic method
just divides by the highest fitness and selects randomly."""
# Make sure the population is sorted by fitness
ga.population.sort_by_best_fitness(ga)
# Error if can't select parents
if ga.selection_probability <= 0 or ga.selection_probability >= 1:
print("Selection probability must be between 0 and 1 to select parents.")
return
# Error if the highest fitness is not positive
if ga.population.get_chromosome(0).get_fitness() <= 0:
print("Error using stochastic roulette selection, best fitness must be positive.")
print("Consider using tournament selection.")
return
# Loops until it reaches a desired mating pool size
while (len(ga.population.get_mating_pool()) < ga.population.size()*ga.parent_ratio):
# Selected chromosome
index = random.randint(0, ga.population.size()-1)
# Probability of becoming a parent is fitness/max_fitness
if random.uniform(ga.selection_probability, 1) < ga.population.get_chromosome(index).get_fitness() / ga.population.get_chromosome(0).get_fitness():
ga.population.set_parent(index)

View File

@ -3,8 +3,15 @@ import EasyGA
# Create the Genetic algorithm
ga = EasyGA.GA()
ga.population_size = 100
ga.generation_goal = 200
ga.parent_selection_impl = EasyGA.Parent_Selection.Roulette.stochastic_selection
ga.crossover_population_impl = EasyGA.Crossover_Methods.Population.sequential_selection
ga.survivor_selection_impl = EasyGA.Survivor_Selection.fill_in_parents_then_random
ga.evolve()
ga.set_all_fitness()
ga.population.set_all_chromosomes(ga.sort_by_best_fitness(ga.population.get_all_chromosomes()))
print(f"Current Generation: {ga.current_generation}")
ga.population.print_all()

View File

@ -24,9 +24,15 @@ class Chromosome:
def remove_gene(self, index):
"""Removes the gene at the given index"""
del self.gene_list[index]
def get_gene(self, index):
"""Returns the gene at the given index"""
return gene_list[index]
def get_gene_list(self):
return self.gene_list

View File

@ -12,6 +12,11 @@ class Population:
self.mating_pool = []
def sort_by_best_fitness(self, ga):
"""Sorts the population by fitness"""
self.set_all_chromosomes(ga.sort_by_best_fitness(self.chromosome_list))
def size(self):
"""Returns the size of the population"""
return len(self.chromosome_list)
@ -29,28 +34,68 @@ class Population:
self.chromosome_list.insert(index, chromosome)
def add_parent(self, chromosome):
"""Adds a chromosome to the mating pool"""
self.mating_pool.append(chromosome)
def remove_chromosome(self, index):
"""removes a chromosome from the indicated index"""
"""Removes a chromosome from the indicated index from the population"""
del self.chromosome_list[index]
def remove_parent(self, index):
"""Removes a parent from the indicated index from the mating pool"""
del self.mating_pool[index]
def reset_mating_pool(self):
"""Clears the mating pool"""
self.mating_pool = []
def get_chromosome(self, index):
"""Returns the chromosome at the given index in the population"""
return self.chromosome_list[index]
def get_parent(self, index):
"""Returns the parent at the given index in the mating pool"""
return self.mating_pool[index]
def get_all_chromosomes(self):
"""returns all chromosomes in the population"""
"""Returns all chromosomes in the population"""
return self.chromosome_list
def get_mating_pool(self):
"""Returns chromosomes in the mating pool"""
return self.mating_pool
def get_fitness(self):
"""returns the population's fitness"""
"""Returns the population's fitness"""
return self.fitness
def set_all_chromosomes(self, chromosomes):
self.chromosome_list = chromosomes
def set_parent(self, index):
"""Sets the index chromosome from the population as a parent"""
self.add_parent(self.get_chromosome(index))
def set_chromosome(self, chromosome, index = -1):
if index == -1:
index = len(self.chromosomes)-1
def set_all_chromosomes(self, chromosome_list):
"""Sets the chromosome list"""
self.chromosome_list = chromosome_list
def set_mating_pool(self, chromosome_list):
"""Sets entire mating pool"""
self.mating_pool = chromosome_list
def set_chromosome(self, chromosome, index):
"""Sets the chromosome at the given index"""
self.chromosome_list[index] = chromosome

View File

@ -4,5 +4,24 @@ class Survivor_Selection:
"""Survivor selection determines which individuals should be brought to the next generation"""
def fill_in_best(ga, next_population):
"""Fills in the next population with the best chromosomes from the last population until the population size is met."""
return ga.make_population(ga.population.get_all_chromosomes()[:ga.population.size()-next_population.size()] + next_population.get_all_chromosomes())
"""Fills in the next population with the best chromosomes from the last population"""
ga.population.set_all_chromosomes(ga.population.get_all_chromosomes()[:ga.population.size()-next_population.size()] + next_population.get_all_chromosomes())
def fill_in_random(ga, next_population):
"""Fills in the next population with random chromosomes from the last population"""
ga.population.set_all_chromosomes([
random.choice(ga.population.get_all_chromosomes())
for n in range(ga.population.size()-next_population.size())]
+ next_population.get_all_chromosomes())
def fill_in_parents_then_random(ga, next_population):
"""Fills in the next population with all parents followed by random chromosomes from the last population"""
ga.population.set_all_chromosomes([
random.choice(ga.population.get_all_chromosomes())
for n in range(ga.population.size()-len(ga.population.get_mating_pool())-next_population.size())]
+ ga.population.get_mating_pool() + next_population.get_all_chromosomes())