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

View File

@ -6,17 +6,22 @@ class Crossover_Methods:
"""Methods for selecting chromosomes to crossover""" """Methods for selecting chromosomes to crossover"""
def sequential_selection(ga): 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 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)-1)]) 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): 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 mating_pool = ga.population.get_mating_pool()
return ga.make_population([ga.crossover_individual_impl(ga, random.choice(mating_pool), random.choice(mating_pool)) for n in mating_pool]) return ga.make_population([ga.crossover_individual_impl(ga, parent, random.choice(mating_pool)) for parent in mating_pool])
class Individual: class Individual:

View File

@ -13,7 +13,7 @@ class Mutation_Methods:
# Randomly apply mutations # Randomly apply mutations
if random.uniform(0, 1) < ga.mutation_rate: 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: 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. 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) tournament_size = int(ga.population.size()*ga.tournament_size_ratio)
if tournament_size < 5: if tournament_size < 5:
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. # 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. # 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 each chromosome, add it to the mating pool based on its rank in the tournament.
for index in range(tournament_size): 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 # e.g. top ranked fitness has probability: selection_probability
# second ranked fitness has probability: selection_probability * (1-selection_probability) # second ranked fitness has probability: selection_probability * (1-selection_probability)
# third ranked fitness has probability: selection_probability * (1-selection_probability)^2 # third ranked fitness has probability: selection_probability * (1-selection_probability)^2
# etc. # etc.
if random.uniform(0, 1) < selection_probability * pow(1-selection_probability, index): if random.uniform(0, 1) < ga.selection_probability * pow(1-ga.selection_probability, index):
ga.population.mating_pool.append(tournament_group[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: class Roulette:
@ -40,27 +54,74 @@ class Parent_Selection:
that it will be selected. Using the example of a casino roulette wheel. 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 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 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 # 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))) fitness_sum = sum(chromosome.get_fitness() for chromosome in ga.population.get_all_chromosomes())
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)
# A list of ranges that represent the probability of a chromosome getting chosen # 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 # 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() rand_number = random.random()
# Loop through the list of probabilities # Find where the roulette landed.
for i in range(len(probability)): for index in range(len(probability)):
# If the probability is greater than the random_number, then select that chromosome if (probability[index] >= rand_number):
if (probability[i] >= rand_number): ga.population.set_parent(index)
ga.population.mating_pool.append(ga.population.chromosome_list[i])
break 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 # Create the Genetic algorithm
ga = EasyGA.GA() 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.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}") print(f"Current Generation: {ga.current_generation}")
ga.population.print_all() ga.population.print_all()

View File

@ -24,9 +24,15 @@ class Chromosome:
def remove_gene(self, index): def remove_gene(self, index):
"""Removes the gene at the given index"""
del self.gene_list[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): def get_gene_list(self):
return self.gene_list return self.gene_list

View File

@ -12,6 +12,11 @@ class Population:
self.mating_pool = [] 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): def size(self):
"""Returns the size of the population""" """Returns the size of the population"""
return len(self.chromosome_list) return len(self.chromosome_list)
@ -29,28 +34,68 @@ class Population:
self.chromosome_list.insert(index, chromosome) 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): 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] 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): def get_all_chromosomes(self):
"""returns all chromosomes in the population""" """Returns all chromosomes in the population"""
return self.chromosome_list return self.chromosome_list
def get_mating_pool(self):
"""Returns chromosomes in the mating pool"""
return self.mating_pool
def get_fitness(self): def get_fitness(self):
"""returns the population's fitness""" """Returns the population's fitness"""
return self.fitness return self.fitness
def set_all_chromosomes(self, chromosomes): def set_parent(self, index):
self.chromosome_list = chromosomes """Sets the index chromosome from the population as a parent"""
self.add_parent(self.get_chromosome(index))
def set_chromosome(self, chromosome, index = -1): def set_all_chromosomes(self, chromosome_list):
if index == -1: """Sets the chromosome list"""
index = len(self.chromosomes)-1 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 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""" """Survivor selection determines which individuals should be brought to the next generation"""
def fill_in_best(ga, next_population): 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.""" """Fills in the next population with the best chromosomes from the last population"""
return ga.make_population(ga.population.get_all_chromosomes()[:ga.population.size()-next_population.size()] + next_population.get_all_chromosomes())
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())